Webview加载本地图片的方案对比
来源:互联网 发布:知乎for mac下载 编辑:程序博客网 时间:2024/05/17 23:29
前言
先讲讲为什么会有这篇blog,话说前几天做个模块,要求是这样的:
做一个webview的页面,功能类似于微信发朋友圈一样,要求能上传本地图片到webview中进行展示,并按用户喜好添加和删除,当用户点击发布的时候,将这些图片上传至阿里云oss,收到oss响应后封装页面信息提交给服务器
技术要点
- webview与native的交互
- webview显示客户端本地图片的方式
- webview加载大量图片的问题(优化方案)
- 自定义关于webview的缓存系统
webview与native的交互
关于交互其实网上有很多文章,在之前我也写过一篇关于webview与native交互方案的blog: Android混合开发的入门和方案
因此在这个demo中我采用的也是JsBridge的方式来让webview与native进行通信,所以主要提一下其中一些坑:
- 使用JsBridge的方式,在子线程是无法发送消息给WebView的
- 如果webview要加载本地文件,必须设置mWebView.getSettings().setAllowFileAccess(true);
- 如果html文件存在于服务器中,就算你按照第二点设置了,那webview也无法读取本地文件,会报not allowed to load local resource(解决办法后面我会给出),这个坑异常深!!!
- 使用base64编码来让webview中img便签加载编码后的图片的异常(大坑)
webview显示客户端本地图片的方式
直接设置
拿到本地文件的路径,然后拼装便签,设置其src属性为”file://”+路径,webview就会自动去找此地址(“file://”是属性webview的一种协议,就像我们的http协议的道理)
//imgPaths是我们选择的那些图片的本地路径 private void adapterH5Data(List<String> imgPaths){ String result = ""; //拼装标签 for(int i=0;i<imgPaths.size();i++){ result += "<img src=\"file://"+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编码
base64编码大家应该都有接触过,还记得宝宝当年刚撸项目的时候,图片上传就是利用app把图片转化为base64,然后把这段字符串发送给服务器接收再进行解码,差点把老师气吐血。
那么这个方案,就是把目标路径的文件,通过base64编码编写成字符串,然后设置到img便签的src属性,这样img便可以显示出图片。
/** * 在这里必须使用多线程,因为base64对图片进行编码非常耗时, * 所以必须在子线程并且多线程处理每张图片的编码 * RxJava的线程切换(这里暂时还没有使用多线程) * @param imgPaths */ private void adapterH5Data(final List<String> imgPaths) { Observable.create(new Observable.OnSubscribe<String>() { @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 += "<img src=\"data:image/png;base64," + s + "\" height=\"70\" width=\"70\"/>"; subscriber.onNext(result); } } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<String>() { @Override public void call(String result) { size++; tv.setText("编码完成:"+size+"张"); mWebView.callHandler("adapter", result, null); } }) ; }
方案的对比
图一为第一种方案,图二为第二种方案
- 其实还有通过shouldInterceptRequest方法来显示图片,在这里其实跟第一种方案的原理一样(放到后面讲)
- 总体来说第一种方案的速度是比较快的,但是第一种方案有一个缺点就是如果是服务器的html的文件,是无法读取本地文件的
- 上面写的第二种方案只是一个入门级的,对比第一种方案唯一的优点是就算是非本地的文件也可以读取,但是总体速度非常慢
优化
很明显,上面的俩种方案都只是一个入门级的方案,根本走不上台面,缺点:
- 本地图片没有经过压缩,直接放在webview中,如果图片比较多,会导致oom
- 由上面的截图也可以看出从选完照片到webview显示,速度还是非常慢的,卡顿的感觉非常明显
- 无法适应我们的需求,有时候html是在服务器上面的,有时候是存在本地的,无法做到统一处理
那么下面介绍我的第三种方案
多线程工作+压缩+缓存+base64编码
ps:本来压缩想单独做个demo出来的,后面发现压缩模块如果没有缓存模块一起合作的化,每次加载图片都要进行压缩,非常恐怖哈哈!
这里讲一下主要思路:
- 每当我们拿到本地图片的路径之后,把这个路径md5之后当作key,检查缓存中是否存在:
- 不存在:调用压缩模块对次路径的图片进行压缩,压缩完成再存入缓存中,再返回压缩后的文件的byte数组
- 存在:直接返回
在这里我的缓存系统是使用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<String> 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,那么我现在的思路是这样的:
- 取得本地图片路径之后,直接跟第一种方案一样生成img便签,例如:
<img src=\"file://sdcard0/picture/xxx.jpg\" height=\"70\" width=\"70\"/>
- 然后在webview加载到此标签的时候,就会被shouldInterceptRequest方法拦截
- 那么此时我们要解析含有”file://”的url,解析本地路径地址
- 调用我们的缓存系统,传入此path, (缓存+压缩)系统返回byte[],解析成inputStream
- 构造WebResourceResponse返回
那么核心的代码就是:
//自定义的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地址
- -
- Webview加载本地图片的方案对比
- Android webview加载本地图片
- Android webview加载本地图片
- webview中的img标签加载Android本地的图片
- WebView加载本地(SD卡)图片
- ios的webView加载本地html图片路径显示及webview适配
- Android webView 加载网页时,使用本地图片替换网页内的图片
- Android开发 WebView加载从本地加载图片
- Android用webView加载h5的页面,h5获取本地的图库中的图片
- android WebView 控件加载本地sdcard中html文件图片的问题
- 关于webview 加载本地图片资源 显示不出来的解决办法
- 关于webview 加载本地图片资源 显示不出来的解决办法
- 如何在webview中加载本地包含图片的html文件?
- 【WebView加载本地资源图片 字体等】一些不常用的用法集合
- android WebView 控件加载本地sdcard中html文件图片的问题
- Android - webview拦截css,js,图片后加载本地外部存储的文件(shouldOverrideUrlLoading)
- iOS WebView加载本地HTML的方法
- iOS webView加载本地html,及图片等问题
- MyBatis 为类型别名
- hdu1116欧拉道路
- 【1】Hibernate的入门
- UML--实现图(构件图、部署图)
- 解决QT中文5.6 QTextEdit QPlainTextEdit的右键菜单翻译不全
- Webview加载本地图片的方案对比
- 零假设和备择假设
- Android仿英雄联盟/斗鱼波形加载动画
- sakura
- - Warning: Skipping the data of table mysql.event. Specify the --events option explicitly.
- 第12章:Continuous Latent Variables exercise 13-23
- 二维数组中的查找
- 参加高通培训
- 给你的头像+1 的opencv实现