先讲讲为什么会有这篇blog,话说前几天做个模块,要求是这样的:
做一个webview的页面,功能类似于微信发朋友圈一样,要求能上传本地图片到webview中进行展示,并按用户喜好添加和删除,当用户点击发布的时候,将这些图片上传至阿里云oss,收到oss响应后封装页面信息提交给服务器
关于交互其实网上有很多文章,在之前我也写过一篇关于webview与native交互方案的blog: Android混合开发的入门和方案
因此在这个demo中我采用的也是JsBridge的方式来让webview与native进行通信,所以主要提一下其中一些坑:
拿到本地文件的路径,然后拼装便签,设置其src属性为”file://”+路径,webview就会自动去找此地址(“file://”是属性webview的一种协议,就像我们的http协议的道理)
//imgPaths是我们选择的那些图片的本地路径
private void adapterH5Data(List imgPaths){
String result = "";
//拼装标签
for(int i=0;i"+imgPaths.get(i)+"\" height=\"70\" width=\"70\"/>";
}
//发送给webview容器
mWebView.callHandler("adapter",result,null);
}
那么在我们的html文件中,有一个这样的方法:
function adapter(pars){
//直接把native拼装好的html添加到id为info_img的便签里面
$("#info_img").prepend(pars);
}
base64编码大家应该都有接触过,还记得宝宝当年刚撸项目的时候,图片上传就是利用app把图片转化为base64,然后把这段字符串发送给服务器接收再进行解码,差点把老师气吐血。
那么这个方案,就是把目标路径的文件,通过base64编码编写成字符串,然后设置到img便签的src属性,这样img便可以显示出图片。
/**
* 在这里必须使用多线程,因为base64对图片进行编码非常耗时,
* 所以必须在子线程并且多线程处理每张图片的编码
* RxJava的线程切换(这里暂时还没有使用多线程)
* @param imgPaths
*/
private void adapterH5Data(final List imgPaths) {
Observable.create(new Observable.OnSubscribe() {
@Override
public void call(Subscriber super String> subscriber) {
for (int i = 0; i < imgPaths.size(); i++) {
String result = "";
String item = imgPaths.get(i);
Log.i("wang","item:"+item);
String s = Base64.encodeToString(getBytes(item), Base64.NO_WRAP);
result += " + s + "\" height=\"70\" width=\"70\"/>";
subscriber.onNext(result);
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1() {
@Override
public void call(String result) {
size++;
tv.setText("编码完成:"+size+"张");
mWebView.callHandler("adapter", result, null);
}
})
;
}
图一为第一种方案,图二为第二种方案
很明显,上面的俩种方案都只是一个入门级的方案,根本走不上台面,缺点:
ps:本来压缩想单独做个demo出来的,后面发现压缩模块如果没有缓存模块一起合作的化,每次加载图片都要进行压缩,非常恐怖哈哈!
这里讲一下主要思路:
在这里我的缓存系统是使用RxJAva+DiskLruCache实现的:
/**
* 获取本地的图片
* 先从硬盘获取,如果硬盘获取不到
* 再拿图片进行压缩,再存到硬盘缓存
* 然后返回
* @param path
* @return
*/
public Observable<byte[]> getLocalImg(final String path){
Observable<byte[]> diskCache = DiskCache.getInstance().getDiskCache(path);
//存入缓存的操作,先压缩,再缓存,CompressUtil是图片压缩工具类
Observable<byte[]> doCache = CompressUtil.compressImg(path, 100, 100)
.flatMap(new Func1<byte[], Observable<byte[]>>() {
@Override
public Observable<byte[]> call(final byte[] bytes) {
return DiskCache.getInstance().toDiskCache(path, bytes);
}
});
//使用concat操作符来实现先调用diskCache,再调用doCache
return Observable.concat(diskCache,doCache)
.first(new Func1<byte[], Boolean>() {
@Override
public Boolean call(byte[] bytes) {
return bytes != null;
}
});
}
上面是缓存系统的核心代码,避免太多代码太混乱。其中关于压缩模块的代码,DiskLruCache工具类的代码可以直接看demo。
拿到本地图片路径之后的操作:
private void adapter(List photoList) {
CacheEngine.getInstance().getLocalImgsMany2(photoList)
.flatMap((Fun1) (bytes) -> { return byte2Base64(bytes); })
.ObserverOn(AndroidSchedulers.mainThread())
.subscribe((Action1)(result) -> { sendToH5(result) });
然后就是关于多线程的实现,使用RxJava一个最大的亮点就是线程切换,在发出每个源数据的时候,只需要调用subscribeOn(Schedulers.newThread())即可,如果要看具体实现请移步demo,下面是效果:
这个方案大体的思想就是这样的,总体的速度提升了超级多(后面有截图对比),下面看看第四种方案
从上面第二种方案可以看出,base64编码多么消耗资源(很慢),只能说base64能绕过尽量绕过,我们都知道,webview在访问每一个连接的时候,都必须要经过shouldInterceptRequest这个方法:
/**
* 自定义的WebViewClient
*/
protected class MyWebViewClient extends BridgeWebViewClient {
public MyWebViewClient(BridgeWebView webView) {
super(webView);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return super.shouldInterceptRequest(view, request);
}
}
WebResourceResponse就是返回给webview的资源,然后在WebResourceResponse 有这样一个构造函数:
public WebResourceResponse(String mimeType, String encoding, InputStream data) {
throw new RuntimeException("Stub!");
}
通过传入一个流InputStream来实现发送数据流给webview,那么我现在的思路是这样的:
那么核心的代码就是:
//自定义的WebViewClient,重载shouldInterceptRequest方法
class MyWebViewClient extends BridgeWebViewClient {
public MyWebViewClient(BridgeWebView webView) {
super(webView);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
String key = "http://localhost";
final WebResourceResponse[] response = {null};
if(url.contains(key)){
//拿到文件地址
String imgPath = url.replace(key,"");
CacheEngine.getInstance().getLocalImgInMain(imgPath)
.subscribe(new Action1<byte[]>() {
@Override
public void call(byte[] bytes) {
response[0] = new WebResourceResponse("image/png", "UTF-8", new ByteArrayInputStream(bytes));
}
});
return response[0];
}else {
return super.shouldInterceptRequest(view, url);
}
}
}
ps:细心的朋友可能发现了的key是”http://localhost“而不是”file:///”,这是因为如果是在服务器的html文件后者会被webview主动拦截了,不会经过shouldInterceptRequest方法,所以我们假装成http协议就行了(这是我暂时的解决办法,如果有更好的可以提出来哈)
(图一为第三种方案。图四为第四种方案)
总体来说个人比较喜欢第四种解决方式,因为搭配缓存系统不仅可以做到本地图片的压缩+缓存,还可以做到网络图片的缓存,只需要通过前缀名来判断是哪种图片即可,而且shouldInterceptRequest方法默认是在io线程工作的,就算你的图片有多大都不会阻塞主线程。另外有一个点要说的就是,上面我的缓存系统只用到了硬盘缓存,其实还可以再加一层内存缓存,那样效果就真的完美了(不过因为这种情景下图片的再利用率不高,所以没做内存缓存,如果是网络图片的加载的话,就很有必要使用内存缓存了)
文章中的代码(start哦谢谢):github地址