本章介绍基础应用框架层的Image Pool模块。Image Pool模块提供了服务端图片下载的统一解决方案。通过控制图片内存使用量;减少图片重复下载;与Android View的无缝结合等,提供图片下载的最佳实践。
在Android中图片是一个Bitmap对象,而该对象保存图片数据的内存是在Native层的。因此只是依靠Java自身的垃圾收集机制是不够的,使用图片较多时容易引起OOM。基于引用计数和LRU机制的图片内存和Cache管理,既可以保证下载的图片所占内存低于设置的上限值,又能利用可用内存资源提供较好的用户体验--减少用户的流量消耗和等待时间。
当图片对象被替换出内存时,会被放在本地的cache中,以减少不必要的流量。缓存具有不同的类型,以适应不同的业务需要,如:
· 本地永久存储,在未达到上限前,不自动删除的本地文件系统缓存。应用负责删除
· 应用程序为生命周期,退出后清除
在Android中的图片的显示是以一个Drawable的形式通过view的setDrawable或者setBackgroudDrawable方式现在是屏幕上的。而服务端下载的图片有不同的状态:未下载,在Cache中,在内存中等。这里我们使用了代理模式,Image Pool管理的是Drawable的一个代理Drawable Proxy. 它让状态对使用者透明,而它本身又是一个Drawable对象。这种方式和自定义Imageview相比,更自然灵活。
在应用开发中遇到的图片下载的场景,很多适合是组的形式来操作,比如:店铺的一组宝贝展示;某宝贝的一组详情图片。这些图片的显示方式,生命周期是完全相同的。
Image Pool基于组的形式调度图片的加载。同组的图片是窜行加载的,而顺序按不同的策略来决定。使用者可以比较简单灵活的控制加载的顺序,保证优先加载的总是用户当前期望先看到的图片。一个典型的例子是,宝贝搜索的结果一般是以List的形式分页展示的,用户在List上快速的滑动过程中,总是希望看到当前屏幕中的宝贝的图片。对应Group的调度可以保证始终加载当前页中的图片。
同一张的图片,图片服务器端一般有一套不同尺寸的图片。分为高清,标准,低彩等级别。对于不同屏幕大小的手机和不同强弱的网络,综合体验和流量的因素,选择不同质量的图片。比如:在wifi下选择质量较高的图片,而在移动网络下倾向于流量的节省。
在Image Pool中的图片策略机制,使得选择的过程对应用开发透明。只需要制定一次策略对所有的图片下载就都适用了。
Webp图片压缩格式相比同样大小的JPEG,PNG压缩格式。在不损失质量的前提下,图片大小有20%的减小。淘宝的图片服务器,已经全面支持webp格式。而Image Pool通过自定义图片格式支持和图片选择策略结合,只需要几行代码就能在应用程序中集成Webp的支持。
这一节是Image Pool使用的快速入门。介绍使用Image Pool的必要步骤和典型用法。
在Image Pool功能使用起来前,需要做一些初始化工作。
/**
* 初始化,需要保证在使用功能前被调用
* @paramcontext 设置context
* @paramuserAgent 图片下载时使用的Agent
* @parampicPattern 图片的Pattern,图片地址必须含有Pattern中的字符串才认为是有效图片,可传"http"匹配到说有的URL图片。
*/
public synchronized void Init(Applicationcontext, String userAgent , String picPattern)
Sample code
ImagePool.getInstance().Init(context,"taobao_android","taobao");
SdkEntry提供SDK所有模块的统一初始化入口,如果使用SdkEntry则可跳过这一步。
设置Image Pool的内存上限,缺省值是2M
/** 设置管理图片的占用内存上限,设置是立即生效的
* @paramsize 内存上限,单位是字节
*/
public void setMaxMemory(int size)
2M是一个比较保守的设置,内存上限的设置应该充分利用手机中每个应用可获得的内存资源。按我们的经验内存占可用内存的1/3左右。SDK的Util包中的MemoryManager类提供了一个获得这个内存的方案。
/**
* 获取进程内存信息
*
*
* @return 如果找到进程,则返回进程内存信息,否则返回null
*/
public ProcessMemoryInfo getMemoryInfo()
public class ProcessMemoryInfo {
intmem_dalvik;
intmem_native;
intmem_max;
intmem_limit; //dalvik能分到的最大内存
......
}
mem_limit是一个Application所能获得的内存上限。当然这里也有适配问题,在某些手机上这个值不一定能获得,或者不一定准确,因此需要限定上,下的极限值。
在不同的网络环境下,对于不同屏幕大小的设备,选择合适的图片。可以节省图片流量。图片质量分为三种模式,供用户选择:
· 低彩模式 - 节省流量优先
· 高清模式 - 图片质量优先
· 智能模式 - 在Wifi下,等同于高清模式;在2G/3G下等同于低彩模式
TODO:插入图片
Figure 1 - 图片质量设置接口
Image Pool支持应用自定义策略, 应用需要提供一个实现IImageQualityStrategy 接口的策略实现者。
/**
* 这个接口定义了图片选择策略
* 对于不同的分辨率的手机和不同的网络状态,一种灵活的图片选择策略,根据这些条件动态的
* 在流量和用户体验上平衡。而对应用开发者来说,只要提供基准图片就可以,不需要关心这些实现的细节。
*/
public interface IImageQualityStrategy {
/*
* 以下是策略的常量定义
*/
/**
* 不采用策略
*/
publicstatic int STRATEGY_MODE_OFF = 0;
/**
* 智能模式
*/
publicstatic int STRATEGY_MODE_SMART = 1;
/**
* 低彩模式
*/
publicstatic int STRATEGY_MODE_LOW = 2; //低彩模式
/**
* 高清模式
*/
publicstatic int STRATEGY_MODE_HIGH = 3; //高清模式
/** 根据当时的状态决定最终下载的图片地址
*@param originUrl 应用传入的图片地址
*@return 最终下载的图片地址
*/
publicString decideUrl( String originUrl );
/** 提取URL中的基础url信息,用于在FileCache中做匹配
* @param originUrl 应用传入的图片地址
*@return 最终下载的图片地址
*/
publicString getBaseUrl(String originUrl );
/** 在从Cache中加载时选择cache中图片的策略
*@param originPath 应用传入的图片在Cache的地址
*@param files 以基础URL和匹配规则在Cache中匹配到的列表,在这个列表中选择适合的加载
*@return 从Cache中下载的图片的路径
*/
publicString decideStoragePath(String originPath, String [] files);
/** 设置策略模式
*@param mode 策略模式
*/
publicvoid setStrategyMode( int mode );
/** 返回当前的策略模式
*@return 策略模式
*/
publicint getStrategyMode();
}
在初始化阶段, 需要设好应用的图片策略实现者
IImageQualityStrategy s = new TBImageQuailtyStrategy(contex,screen_width,screen_height);//实例化策略实现者对象
s.setStrategyMode(savedStrategyMode); //从设置中获得当前用户选择的策略,设置给实现者
ImagePool.instance().setImageQualityStrategy( s); //把策略设置到Image Pool
在退出应用或者应用转到后台时,需要释放Image Pool所占的资源。Image Pool提供的两个函数分别用于这两种场景。
/* 结束调度线程,释放文件缓存句柄
* 注:一旦结束,调度工作不会再进行,将无法下载图片。
*/
publicsynchronized final void release();
/**
* 释放文件缓存句柄
*/
NOTE: 应用再次转到前台时,需要先重新调用Init。
一组图片总是在同一Activity中,而随着Activity处于生命周期的不同状态,图片的状态也随着变化。
对于相同个数一一对应的的一组View和一组图片的场景,使用ImageGroupAdvImp是最简单的。首先,生成Group的时候,指定对应关系,和自定义的绑定、解绑操作。
mImageGroup = newImageGroupAdvImp("GuarantLabelLayout", imgUrls, imgViewList,
new MyImageBinder(), context,ImageGroup.PRIORITY_NORMAL, ImageCache.CACHE_CATEGORY_MRU);
mImageGroup.start();
其中MyImageBinder必须是一个实现ImageBinder接口的实例,它实现了图片和View间的绑定和解绑操作。
/**
*ImageBinder
* 实现绑定的接口定义
* */
public interface ImageBinder
{
/**
* bindImg2View
* 把Drawable设置到view上
* @param d Drawable对象
* @param view 待绑定的控件
* @return 是否绑定成功
* */
publicboolean bindImg2View( Drawable d, View view);
/**
* unbind
* Destroy时和View解绑的操作
* @param view 绑定的控件
* @return 是否解绑成功
* */
publicboolean unbind(View view);
}
其次,和任何Group一样,在Activity的onStop操作中,调用:
public void onStop() {
if(mImgGroup != null){
mImgGroup.pause();
}
}
注:不在onPause操作中调用的原因是,此时View的onDraw还可能被调用,而在onStop时,view上的图片就不会被使用了,这时可以把图片的状态设为可回收。
在Activity的onResume操作中,调用:
public void onResume() {
if(mImgGroup != null){
mImgGroup.resume();
}
}
在Activity的onDestroy操作中,调用:
public void onDestroy() {
if(mImgGroup != null){
mImgGroup.onDestroy();
mImgGroup =null;
}
}
NOTE: 这三个生命周期相关的函数式定义在ImageGroup接口中的,这意味着所有类型的Group都需要实现他们。
比起一一对应的场景,更常见的是View比图片少很多,View是被图片所复用的。例如在List View中,界面上显示的View是固定的,而随着List View的滑动,不同的图片绑定到对应的view上。 DataLogic模块中有适合这种场景的Group的实现。Image Pool和Data Logic组合是这个场景的最佳解决方案。
具体如何在此种场景下使用Data Logic,见Data Logic章节。
有时只有一张图片需要加载。这时需要看这张图片是否需要Image Pool来帮助管理内存,如果需要,则使用一个只包含这张图片Group。如果不需要,则不使用Image Pool。一个典型的例子是,登陆时的验证图片,在这个场景下同一个URL对应不同的图片,Image Pool就不适合在这里使用了。建议使用ApiRequestMgr提供的DownloadImage方法,并自己管理图片的内存。
有时需要的图片是在下载下来的图片上做一些处理,比如做圆角。而处理过的图片才是Image Pool管理的图片。在这张场景下,可以在从ImagePool获得图片对象时提供一个转换操作。这个转换操作在传入的原图基础上做操作并返回转换后的图片,由Image Pool管理起来。
定义转换操作的接口是:
/** BitmapConvertor定义了图像转换的接口
* 获得ImageHandler时可以提供一个转换方法(如做圆角),来改变ImageHandler
* 管理的Bitmap
*/
public interface BitmapConvertor
{
/**从原先的Bitmap转换为新的Bitmap
* @param origianl 老的Bitmap
* @return 转换后的Bitmap
*/
publicBitmap convertTo(Bitmap original);
}
使用带转换操作的方法获得图片的代码:
Drawable drawable =ih.getDrawable(new RoundIconConvertor());
public static classRoundIconConvertor implements BitmapConvertor {
//.......
}
本节总结了在淘宝主客户端中使用Image Pool的一些经验和思路。
Android设备多种多样,屏幕分辨率从小到大差别很大。同一张图片在不同的设备上最合适的尺寸是不同的。另一个因素是,不同的网络环境下流量和体验的取舍。比如Wifi下体验优于流量的考虑,而2G/3G下则相反。
淘宝的CDN图片服务器,对同一图片有一组不同尺寸:
24,30,40,60,64,70,80,90,100,110,120,128,160,170,250,310,430,670。
在淘宝主客户端中我们按屏幕大小分为:小(480以下),中(480至640),大(640以上)三类。界面的开发者不必考虑三套,他只需要按照480的设备来决定原始图片尺寸。策略实现中会考虑设备大小和网络因素决定真正使用的图片尺寸。
NOTE: 图片策略的实现者不属于SDK的部分,在Ref下有源码的参考实现。
Webp格式比相同大小的PNG和JPG格式要小,如果图片服务器支持 - 淘宝图片服务器已全面支持webp格式 - 则流量的减少有非常显著的效。ImagePool已经管理了所有图片的下载,且策略机制让应用可以决定实际下载的图片。因此支持Webp格式只需要:
· 集成webp的Decoder
Android 4.0中已经有了webp支持;但4.0前的手机还是占了大部分,在应用框架的dep目录下,提供了一个webp decoder的实现,libwebp.so
这是一个arm格式的库文件,应加入到工程的assets/so/armeabi下。如图所示:
Figure 2 - libwebp.so的位置
libwebp是一个C 语言实现的Native层的库,为了使用它需要一个Java层的Binding类- WebPBitmapHelper类。在Image Pool中,定义了图片解码的扩展机制。应用可以把实现IBitmapHelper接口的解码器注册到BitmapHelperFactory类。这样Image Pool就会根据后缀名和文件内容找到合适的解码器了。
IBitmapHelper接口:
/** BitmapHelperFactory用于支持可扩展的图片格式的解压
* IBitmapHelper定义了用于解压图片的BitmapHelper的接口
* */
public interface IBitmapHelper
{
/**
* 给予数据流和文件URL,判断是否支持
* @param b 数据流
* @param url 文件URL
* @return 是否支持这个图片文件的解压
* */
booleanisSupport( byte[] b , String url);
/**
* 给予数据流和文件URL,返回解压出来的Bitamp
* @param b 数据流
* @param url 文件URL
* @return 解压出来的Bitamp,失败返回Null
* */
BitmapBytes2Bimap(byte[] b, String url);
}
注册解码器:
BitmapHelperFactory. registerHelper( new WebpBitmapHelperImp());
NOTE: WebpBitmapHelperImp和WebPBitmapHelper不属于Image Pool的SDK,但在包中提供源代码的实现参考。
· 在策略机制建立到webp格式图片的映像关系
以淘宝的图片CDN服务器为例,jpg/png到webp的映像关系如:
http://q.i01.wimg.taobao.com/bao/uploaded/i1/T1TVLAXeBhXXXM8XM9_102307.jpg_100x100.jpg
http://q.i01.wimg.taobao.com/bao/uploaded/i1/T1TVLAXeBhXXXM8XM9_102307.jpg_100x100.jpg_.webp
即在JPG/PNG的URL地址上增加后缀_.webp。
Image Pool最初是为管理服务端图片-即通过HTTP下载的图片。它们的特点时,加载需要比较大的时间开销,为了尽量避免重新加载的开销,一般在客户端需要有内存和文件的Cache。在应用开发的过程中,遇到一些其他类型的图片有相同的特征。比如手机上应用程序的Logo图片。Image Pool通过扩展协议的方式,用统一的方式来管理不同加载方式的图片。比如为Logo的图片,定义了如下协议格式:
package://PackageName