面试题一天一题 —— 第二天 · (图片加载原理)
在开发应用的时候,往往遇到过图片加载的问题,从XUtils(很早之前一种集成网络、加载图片、数据库操作的集合)这种大型框架再到ImageLoader、Picasso、Fresco、Glide,多多少少都接触过一些,在加载图片的时候,简单的时候只需要传个控件和图片地址就能将图片显示在控件上。然而更多时候我们不需要了解其中做的操作,像Glide这种专门处理图片的框架,可能进入源码之后无法自拔!
但是有些应用图片过多之后在某些机型上可能出现 OOM 的问题,了解也有些本地图片加载的时候直接报 OOM ,目前市面上的手机内存相比于以前内存有很大的提升不会轻易的出现OOM的问题,刚刚开始做APP的时候,直接加载drawable中的图片,setImageDrawable到ImageView上面,一运行就挂了,当时不知所措;只好跟UI说图片太大了,帮我重新设计一张小一点分辨率的图片吧; 要是当时知道了图片加载的原理,对图片做适当的优化,就不会出现上面的问题了!
图片加载原理
最常用的就是从drawable中找到资源,将本地图片转换成一个Drawable,但是Drawable对象并不是一张图片,而是一个构造图片的工具,它可以从图片中生成,也可以从xml中生成。
我们来看看ImageView.setImageDrawable()这个方法:
public void setImageDrawable(@Nullable Drawable drawable) {
if (mDrawable != drawable) {
mResource = 0;
mUri = null;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
// 将值设置给全局变量
updateDrawable(drawable);
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
上面调用ImageDrawable之后又调用了updateDrawable() 方法
private void updateDrawable(Drawable d) {
// ...
if (mDrawable != null) {
sameDrawable = mDrawable == d;
mDrawable.setCallback(null);
unscheduleDrawable(mDrawable);
if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {
mDrawable.setVisible(false, false);
}
}
mDrawable = d;
// ...
}
上面拿到Drawable对象之后,将其赋值给了全局的mDrawable,然后更新当前的view背景重画。而在onDraw() 方法中,他的操作主要交给了Drawable来处理,而Drawable是一个抽象类,具体的绘画操作交由具体实现类实现接口方法处理,下面拿BitmapDrawable作实例:
@Override
public void draw(Canvas canvas) {
// 拿到设置的Bitmap对象
final Bitmap bitmap = mBitmapState.mBitmap;
if (bitmap == null) {
return;
}
final BitmapState state = mBitmapState;
final Paint paint = state.mPaint;
if (state.mRebuildShader) {
// 伸展原理 CLAMP REPEAT MIRROR
final Shader.TileMode tmx = state.mTileModeX;
final Shader.TileMode tmy = state.mTileModeY;
if (tmx == null && tmy == null) {
paint.setShader(null);
} else {
paint.setShader(new BitmapShader(bitmap,
tmx == null ? Shader.TileMode.CLAMP : tmx,
tmy == null ? Shader.TileMode.CLAMP : tmy));
}
state.mRebuildShader = false;
}
// 设置画笔
final int restoreAlpha;
if (state.mBaseAlpha != 1.0f) {
final Paint p = getPaint();
restoreAlpha = p.getAlpha();
p.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));
} else {
restoreAlpha = -1;
}
// 设置颜色通道
final boolean clearColorFilter;
if (mTintFilter != null && paint.getColorFilter() == null) {
paint.setColorFilter(mTintFilter);
clearColorFilter = true;
} else {
clearColorFilter = false;
}
updateDstRectAndInsetsIfDirty();
final Shader shader = paint.getShader();
final boolean needMirroring = needMirroring();
if (shader == null) {
if (needMirroring) {
canvas.save();
// Mirror the bitmap
canvas.translate(mDstRect.right - mDstRect.left, 0);
canvas.scale(-1.0f, 1.0f);
}
// 画Bitmap
canvas.drawBitmap(bitmap, null, mDstRect, paint);
if (needMirroring) {
canvas.restore();
}
} else {
updateShaderMatrix(bitmap, paint, shader, needMirroring);
canvas.drawRect(mDstRect, paint);
}
if (clearColorFilter) {
paint.setColorFilter(null);
}
if (restoreAlpha >= 0) {
paint.setAlpha(restoreAlpha);
}
}
在BitmapDrawable中画Bitmap做的操作有很多:设置伸展原理、画笔透明度、颜色、还有是不是镜像处理,对于他就是画Bitmap,而我们看另外一个ColorDrawable实现类,该实现类相比Bitmap就简单很多了,就是在特定的矩形内部画颜色。:
@Override
public void draw(Canvas canvas) {
final ColorFilter colorFilter = mPaint.getColorFilter();
if ((mColorState.mUseColor >>> 24) != 0 || colorFilter != null || mTintFilter != null) {
if (colorFilter == null) {
mPaint.setColorFilter(mTintFilter);
}
// 设置颜色
mPaint.setColor(mColorState.mUseColor);
// 画矩形
canvas.drawRect(getBounds(), mPaint);
// Restore original color filter.
mPaint.setColorFilter(colorFilter);
}
}
第三方框架加载图片原理
前面说了很多Drawable,ImageView加载图片,那到底图片加载原理是咋样的呢?由于Glide源码过于复杂,包含生命周期处理对FragmentManager的巧妙运用和不同的注册器使用,所以我们讲古老的ImageLoader, ImageLoader使用也很简单,配置全局信息之后,只需要设置图片和链接等:
options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub)
.showImageForEmptyUri(R.drawable.ic_empty)
.showImageOnFail(R.drawable.ic_error)
.cacheInMemory(true)
.cacheOnDisk(true)
.considerExifParams(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.displayer(new RoundedBitmapDisplayer(20))
.build();
ImageLoader.getInstance().displayImage(IMAGE_URLS[position], imageView, options);
上面的代码我是从ImageLoader中demo中拿到,单例获取到ImageLoader之后直接displayImage即可,如此简单粗暴的使用真是爽到无可挑剔,当然,Glide的使用同样是简单;但是框架的简单使用必然是对复杂的事情封装得过于难懂。
接下来我们看看ImageLoader的时序图,对ImageLoader加载图片有一个清晰的认识:
ImageLoader时序图.png
ImageLoader最终将图片加载线程维护在线程中,然后保存到文件中,从文件中加载图片,最终以Bitmap的形式显示在图片控件上。对于ImageLoader的源码大家可以下载看看,有了时序图之后很清晰的调用了每个类,其中可能有些配置信息或者细节之处没有考虑到,望提出一起学习交流。
总结
1、 我们了解了图片Drawable其实只是一个加载图片的工具,并不是一张图片;它可以加载xml文件,解析出我们的图,也可以从drawable文件夹中加载图片处理交给画布画。
2、 主流的图片加载原理其实都是用到了缓存:内存缓存、活动缓存、磁盘缓存等等,首先都是从缓存中获取到,如果没有再从网络下载数据到本地加载;最终的形式都是将文件解析成Bitmap加载到图片上。