图片来源有网络图片 手机存储图片 资源图片 以前一般网络图片用图片加载库来加载 手机存储图片一般用BitmapFactory来加载 资源图片分为xml里的src与background 和hdpi或xhdpi里的图片 xml里的framework用a.getDrawable来加载 hdpi里的图片我们一般用setImageResource或setBackgroundResource来设置 其实它们3个都是调用Resource.loadDrawable(采用享元设计模式)通过BitmapFactory.decodeResourceStream来加载的
以上几种方式如果不通过jni,都会调用BitmapFactory.decodeStream来加载图片 如果图片分辨率过高可能会出现已升OOM异常 因为图片是由一个个的像素点构成,加载过程会创建一个二维数组,在数组中图片分辨率为x,y,每一个像素点由ARGB组成,占据2或4个字节 因此理论消耗内存公式如下
bitmap内存大小 = 分辨率x * 分辨率y * 像素大小即色彩模式(ARGB_8888为4字节 ARGB_4444与RGB_565为2字节)
如1200 * 900 的图片 以ARGB_8888模式加载 内存大小为1200 * 800 * 4 = 3840000 约为3.66MiB
图片加载时如果设置了密度的话可能会对图片分辨率进行成倍的缩放(图片目录密度大于设备密度分辨率缩小 图片目录密度小于设备密度分辨率放大) 从而影响内存大小 所以公式做如下修改
scale = (float) targetDensity / density;
//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
int bitmapWidth = decodingBitmap.width();
int bitmapHeight = decodingBitmap.height();
int scaledWidth = int(scaledWidth * scale + 0.5f);
int scaledHeight = int(scaledHeight * scale + 0.5f);
bitmapSize = scaledWidth * scaledHeight * pixelByte;
bitmapSize = int(bitmapWidth * scale + 0.5f) * int(bitmapHeight * scale + 0.5f) * pixelByte;
bitmapSize = int(bitmapWidth * bitmapHeight * scale² + bitmapWidth * scale *0.5f + bitmapHeight * scale * 0.5f + 0.5²) * pixelByte
bitmapSize = int(bitmapWidth * bitmapHeight * scale² + (bitmapWidth + bitmapHeight) * scale /2 + 0.25) * pixelByte
bitmap内存大小 = (原始分辨率x * 原始分辨率y * scale² + (原始分辨率x+原始分辨率y) * scale / 2 +0.5²) * 采样率² * 像素大小
bitmap内存大小 = 原始分辨率x * 原始分辨率y * 像素大小* 最终分辨率x/原始分辨率x * 最终分辨率y/原始分辨率y / 采样率²
估算 内存大小=原始分辨率x * 原始分辨率y * scale² / 采样率² * 像素大小
如果资源图片没有放到对应密度文件夹下 很可能造成OOM或图片显得很虚 所以要根据图片的分辨率与目的和内容来确认放到哪个密度的文件夹下 最后能用color就用color 能用shape就用shape 能用.9就用.9 都不能则用图片
如果觉得图片太大 想要成倍的缩小 可以设置采样率 该值为2的整数倍 如果为2 将缩小4倍 4将缩小16倍 该值越大图片越虚需合理设置
采样率和密度都是通过BitmapFactory.Option来设置 如采样率option.inSampleSize 设备密度dpi option.inTargetDensity 图片目录密度dpi option.inDensity
如果不能确定缩小的倍数 可以先设置option.inJustDecodeBounds为true来获取图片的宽高等信息(不生成bitmap) 在通过计算获取inSampleSize
创建bitmap例子
public Bitmap decodeBitmap() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// 通过这个bitmap获取图片的宽和高
String filePath = "/sdcard/jpg/mm.jpg";
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
if (bitmap == null) {
LogUtil.e(TAG,"decodeBitmap() first decode bitmap is null");
}
LogUtil.e(TAG,"decodeBitmap() first decode decodeBounds:{width:" + options.outWidth + " height:" + options.outHeight);
// 计算缩放比
options.inSampleSize = computeSampleSize(options,800,600);
// 需要把options.inJustDecodeBounds 设为 false,否则没有图片
options.inJustDecodeBounds = false;
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){
//用于存储Pixel的内存空间在系统内存不足时可以被回收
//在应用需要再次访问Bitmap的Pixel时(如绘制Bitmap或是调用getPixel)会自动重新解码
// 系统会再次调用BitmapFactory decoder重新生成Bitmap的Pixel数组
// 这选项会影响性能 可能会造成卡顿 KITKAT时被过时 不推荐使用
// 当然不管4.4以上还是以下只要内存捉急还是可以使用的 两害相比取其轻 接着可以尝试下优化内存 降下来以后在去掉
//options.inPurgeable = true;
//和inPurgeable结合使用在它为true的时候 在bitmap被回收后再次使用时重新解码的时候不用做深拷贝 而是共享编码数据
//options.inInputShareable = true;
}else {
//4.4以后 jni层设置密度 doDecode放开了对密度的支持 willScale能为true
DisplayMetrics dm = getResources().getDisplayMetrics();
//设计图720p下设计的 图片分辨率没超过720p 想压很点 该值加大
options.inDensity = DisplayMetrics.DENSITY_XHIGH;
options.inTargetDensity = dm.densityDpi;
}
bitmap = BitmapFactory.decodeFile(filePath, options);
int w = bitmap.getWidth();
int h = bitmap.getHeight();
if (bitmap != null) {
LogUtil.e(TAG,"decodeBitmap() second decode bitmap is not null bitmapRect:{width:" + w + " height:" + h);
}
return bitmap;
}
private int computeSampleSize(BitmapFactory.Options options, int requestWidth, int requestHeight) {
int simpleSize = 1;
if(options == null){
return simpleSize;
}
int realWidth = options.outWidth;
int realHeight = options.outHeight;
//下面的算法图片质量会比较高 只有请求宽高中大或等的一方 且比实际大小大时才会做缩小处理
//可以先求出宽和高的缩小比例 在返回高的或低的 求比例的时候 被除数可以根据实际情况选择相对应的 或宽高中小的或大的
//也可以直接使用官方源码中的computeSampleSize
if(requestWidth>realWidth && requestWidth>=requestHeight){
simpleSize = Math.round((float)requestWidth / realWidth);
}else if(requestHeight>realHeight && requestHeight>=requestWidth){
simpleSize = Math.round((float)requestHeight/realHeight);
}
return simpleSize;
}
设置资源图片
public void setImageResource(boolean isCache, ImageView imageView,int resId){
if(isCache){
imageView.setImageResource(resId);
}else {
Bitmap oldBitmap = getBitmap(imageView);
Bitmap bitmap = decodeResourceBitmap(resId);
imageView.setImageBitmap(bitmap);
if(oldBitmap!=null){
oldBitmap.recycle();
}
}
}
private Bitmap getBitmap(ImageView imageView) {
try {
Field field = ImageView.class.getDeclaredField("mDrawable");
field.setAccessible(true);
Drawable drawable = (Drawable) field.get(imageView);
if(drawable!=null && drawable instanceof BitmapDrawable){
return ((BitmapDrawable)drawable).getBitmap();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Bitmap decodeResourceBitmap(@DrawableRes int resId){
Bitmap bitmap;
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
boolean isLowMemory = am.isLowRamDevice();
if(isLowMemory && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){
@SuppressLint("ResourceType")
InputStream inputStream = getResources().openRawResource(R.drawable.back_pannel);
bitmap = BitmapFactory.decodeStream(inputStream);
}else {
bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.back_pannel);
}
return bitmap;
}
有些图片加载库已支持加载本地文件和资源图片 现可以统一使用图片加载库加载 如Glide
ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi 代表不同的密度下的图片 如密度160 为mdpi 在此密度下1dp=1px 1dp=density px
dpiFolder | resolution | icon size | densityDpi | ratio | density |
---|---|---|---|---|---|
ldpi | 320*240 | 36*36 | 120 | 3 | 0.75 |
mdpi | 480*320 | 48*48 | 160 | 4 | 1 |
hdpi | 480*800 | 72*72 | 240 | 6 | 1.5 |
xhdpi | 1280*720 | 96*96 | 320 | 8 | 2 |
xxhdpi | 1920*1080 | 144*144 | 480 | 12 | 3 |
xxxhdpi | 3840*2160 | 192*192 | 640 | 16 | 4 |
注:Android studio mipmap文件夹只存放启动图标icon
分析源码之前先介绍下View的下面两个方法
requestLayout()
这个方法会根据View树逐级向上传递,最后由ViewRootImpl.requestLayout方法处理该事件。ViewRootImpl.requestLayout中会调用scheduleTraversals()方法,这是一个异步方法,会调用performTraversals()方法。在performTraversals()方法中会先后调用performMeasure()和performLayout()方法。然后视条件调用performDraw()方法。
invalidate()
该方法会不断向父容器请求刷新并计算需要重绘的区域,最后会传递到ViewRootImpl中触发performTraversals方法,然后调用performDraw()进行重绘。
ImageView setImageBitmap 把bitmap构建成Drawable 在调用setImageDrawable方法 把drawable赋值给mDrawable 如果drawable的宽或高有改变重新布局 最后重新绘制 即通过onDrawable把mDrabable画到canvas上 最终显示到界面上
//ImageView
public Drawable getDrawable() {
if (mDrawable == mRecycleableBitmapDrawable) {
// Consider our cached version dirty since app code now has a reference to it
//只有在调用setImageBitmap后 没为mDrawable设置新值时 该条件才满足
//只有在这里会把mRecycleableBitmapDrawable置为null
mRecycleableBitmapDrawable = null;
}
return mDrawable;
}
//ImageView
public void setImageBitmap(Bitmap bm) {
// Hacky fix to force setImageDrawable to do a full setImageDrawable
// instead of doing an object reference comparison
mDrawable = null;
//setImageBitmap时复用drawable
if (mRecycleableBitmapDrawable == null) {
mRecycleableBitmapDrawable = new ImageViewBitmapDrawable(
mContext.getResources(), bm);
} else {
mRecycleableBitmapDrawable.setBitmap(bm);
}
setImageDrawable(mRecycleableBitmapDrawable);
}
//ImageView
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();
}
}
更新drawable并初始化 如设置回调 宽高 范围 等 干掉oldDrawable的回调
//ImageView
private void updateDrawable(Drawable d) {
if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
//不是通过setImageBitmap进来的把mRecycleableBitmapDrawable的bitmap置为null
//让mRecycleableBitmapDrawable里bitmap可以被垃圾回收
mRecycleableBitmapDrawable.setBitmap(null);
}
if (mDrawable != null) {
//干掉旧drawable的ImageView的弱引用
mDrawable.setCallback(null);
unscheduleDrawable(mDrawable);
}
mDrawable = d;
if (d != null) {
d.setCallback(this);
d.setLayoutDirection(getLayoutDirection());
if (d.isStateful()) {
d.setState(getDrawableState());
}
d.setVisible(getVisibility() == VISIBLE, true);
d.setLevel(mLevel);
mDrawableWidth = d.getIntrinsicWidth();
mDrawableHeight = d.getIntrinsicHeight();
applyImageTint();
applyColorMod();
configureBounds();
} else {
mDrawableWidth = mDrawableHeight = -1;
}
}
public void unscheduleDrawable(Drawable who) {
if (mAttachInfo != null && who != null) {
mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
Choreographer.CALLBACK_ANIMATION, null, who);
}
}
xml中的android:src设置图片 先通过Resources.loadDrawable来加载资源图片 在通过setImageDrawble来设置图片 代码片段如下
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ImageView, defStyleAttr, defStyleRes);
Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
a.recycle();
}
updateDrawable已分析 Resources.loadDrawble 等setImageResource时候在分析
//TypedArray
public Drawable getDrawable(int index) {
if (mRecycled) {
//调用过recycle方法
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
if (value.type == TypedValue.TYPE_ATTRIBUTE) {
//找不到该资源
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
return mResources.loadDrawable(value, value.resourceId, mTheme);
}
return null;
}
xml中的android:background设置背景 先通过Resources.loadDrawable来加载资源图片 在通过setBackgroundDrawable来设置背景给mBackground 视情况决定是否重新布局 最后重新绘制 即在draw方法中掉用drawBackground 把mBackground画到canvas上 代码片段如下
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
Drawable background = null;
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.View_background:
background = a.getDrawable(attr);
break;
}
}
if (background != null) {
setBackground(background);
}
}
a.getDrawable已分析 见上面 下面分析setBackground
//View
/**
* @param background The Drawable to use as the background, or null to remove the background
*/
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background)
}
//View
public void setBackgroundDrawable(Drawable background) {
//计算mPrivateFlags mPrivateFlags影响draw方法里是否调用drawBackground
computeOpaqueFlags();
if (background == mBackground) {
return;
}
boolean requestLayout = false;
mBackgroundResource = 0;
/*
* Regardless of whether we're setting a new background or not, we want
* to clear the previous drawable.
* 清除之前的背景回调
*/
if (mBackground != null) {
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (background != null) {
......
// Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
// if it has a different minimum size, we should layout again
//没有旧背景或新背景最小值跟旧背景最小值不同时需要重新布局
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
background.setCallback(this);
if (background.isStateful()) {
background.setState(getDrawableState());
}
background.setVisible(getVisibility() == VISIBLE, false);
mBackground = background;
applyBackgroundTint();
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
}else{
mBackground = null;
if ((mViewFlags & WILL_NOT_DRAW) != 0
&& (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
//需要重新布局
requestLayout = true;
}
//计算mPrivateFlags mPrivateFlags影响draw方法里是否调用drawBackground
computeOpaqueFlags();
//根据布局标志 决定是否重新布局
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
//重新绘制
invalidate(true);
}
setBackgroundResource 会通过Resource.loadDrawable来加载背景图片 接着用setBackground来设置背景图片
//View
public void setBackgroundResource(@DrawableRes int resid) {
//resId != 0才往下执行 所以setBackgroundResource(0) 并不能清除背景图片 可以使用setBackground(null)来清除
if (resid != 0 && resid == mBackgroundResource) {
return;
}
Drawable d = null;
if (resid != 0) {
//会走到Resources.loadDrawable
d = mContext.getDrawable(resid);
}
setBackground(d);
mBackgroundResource = resid;
}
setImageUri 调用updateDrawable(null)清理mDrawable 接著把mResource置为0 uri赋值给mUri 在调用resolveUri来获取并设置drawable 如果drawable的宽或高有改变重新布局 最后重新绘制
//ImageView
public void setImageURI(@Nullable Uri uri) {
if (mResource != 0 ||
(mUri != uri &&
(uri == null || mUri == null || !uri.equals(mUri)))) {
//干掉imageView建立的对mDrawable的引用 同时也是为了可以执行resolveUri方法
updateDrawable(null);
//让resolveUri方法通过mUri来加载drawable
mResource = 0;
mUri = uri;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
//加载并跟新drawable mResource为0 mUri不为null根据mUri加载drawable
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
当mUri以android.resource开头时调用的是Resource.getDrawable 如果以file://或content://开头的话先把支援加载成文件或流 再通过BitmapFactory加载成bitmap 在包装成drawable
//ImageView
private void resolveUri() {
if (mDrawable != null) {
return;
}
Resources rsrc = getResources();
if (rsrc == null) {
return;
}
Drawable d = null;
if (mResource != 0) {
......
} else if (mUri != null) {
通过内容观察者或文件加载Drawable
String scheme = mUri.getScheme();
if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
try {
//mUri以android.resource://开头
//格式1 android.resource://package_name/id_number
//如 android.resource://com.example.myapp/drawable/{{drawable_id}}
//如 android.resource://com.example.myapp/raw/{{my_resource}}
// Load drawable through Resources, to get the source density information
//通过mUri的authority(如com.example.myapp)获取资源对象和资源编号
//OpenResourceIdResult是资源对象和资源编号的包装类
ContentResolver.OpenResourceIdResult r =
mContext.getContentResolver().getResourceId(mUri);
//r.r为资源对象 r.id为资源id 即resources.getDrawable(resId,mContext.getTheme());
d = r.r.getDrawable(r.id, mContext.getTheme());
} catch (Exception e) {
Log.w("ImageView", "Unable to open content: " + mUri, e);
}
} else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
|| ContentResolver.SCHEME_FILE.equals(scheme)) {
//mUri以content://或file://开头
InputStream stream = null;
try {
//1.通过mUri的authority(如com.example.myapp)获取资源对象和资源编号
//2.在通过获取的资源对象打开资源编号所对应的资源获取流对象(r.r.openRawResource(r.id);)
stream = mContext.getContentResolver().openInputStream(mUri);
//1.通过BitmapFactory.decodeResourceStream创建bitmap
//2.调用drawableFromBitmap 把bitmap包装成drawable
d = Drawable.createFromStream(stream, null);
} catch (Exception e) {
Log.w("ImageView", "Unable to open content: " + mUri, e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
Log.w("ImageView", "Unable to close content: " + mUri, e);
}
}
}
} else {
//以文件形式加载Drawable
//1.通过BitmapFactory.decodeFile加载mUri获取bitmap
//2.调用drawableFromBitmap 把bitmap包装成drawable
d = Drawable.createFromPath(mUri.toString());
}
if (d == null) {
System.out.println("resolveUri failed on bad bitmap uri: " + mUri);
// Don't try again.
//通过mUri加载失败 把mUri置为空
mUri = null;
}
} else {
return;
}
//更新drawable
updateDrawable(d);
}
setImageResource 调用updateDrawable(null)清理mDrawable 接着把resId赋值给mResource mUri置为null 在调用resolveUri来获取并设置drawable 如果drawable的宽或高有改变重新布局 最后重新绘制
public void setImageResource(@DrawableRes int resId) {
// The resource configuration may have changed, so we should always
// try to load the resource even if the resId hasn't changed.
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
//干掉imageView建立的对mDrawable的引用 同时也是为了可以执行resolveUri方法
updateDrawable(null);
//让resolveUri方法通过mResource来加载drawable
mResource = resId;
mUri = null;
//如果mDrawable不为null直接返回
//如果mResource不为0 通过mResource加载drawable
//如果mUri不为null通过mUri加载drawable
//都不满足返回
//更新drawable
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
//宽高有改变重新布局
requestLayout();
}
//重新绘制
invalidate();
}
首先通过mResource或者mUri加载出相应的drawable 接着更新drawable
//ImageView
private void resolveUri() {
if (mDrawable != null) {
return;
}
Resources rsrc = getResources();
if (rsrc == null) {
return;
}
Drawable d = null;
if (mResource != 0) {
//通过图片资源id加载Drawable
try {
//会走到Resources.loadDrawable方法里
d = mContext.getDrawable(mResource);
} catch (Exception e) {
Log.w("ImageView", "Unable to find resource: " + mResource, e);
// Don't try again.
mUri = null;
}
} else if (mUri != null) {
......
} else {
return;
}
//更新drawable
updateDrawable(d);
}
//Context
public final Drawable getDrawable(int id) {
return getResources().getDrawable(id, getTheme());
}
//Resources
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
TypedValue value;
synchronized (mAccessLock) {
value = mTmpValue;
if (value == null) {
value = new TypedValue();
} else {
mTmpValue = null;
}
getValue(id, value, true);
}
final Drawable res = loadDrawable(value, id, theme);
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
return res;
}
首先通过key和theme到可绘制缓存中获取ConstantState 如果找到根据cs创建drawable并返回 接着根据key去预加载缓存中获取ConstantState 如果找到根据cs创建drawable 否则根据资源加载创建drawable 检查drawable是否应用主题 如果没有根据drawable的cs重行创建个cs并应用主题 并缓存cs
//Resource
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
if (TRACE_FOR_PRELOAD) {
// Log only framework resources
if ((id >>> 24) == 0x1) {
final String name = getResourceName(id);
if (name != null) {
Log.d("PreloadDrawable", name);
}
}
}
final boolean isColorDrawable;
//可绘制的缓存 存储以下3个集合
//主题不为null的arrayMap key为theme 值为LongSparseArray 命名为mThemedEntries
//主题为null的LongSparseArray缓存 命名为mNullThemedEntries
//没有主题的LongSparseArray缓存 命名为mUnthemedEntries
//LongSparseArray的值为Drawable.ConstantState的弱引用
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
//颜色资源
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
//非颜色资源
isColorDrawable = false;
caches = mDrawableCache;
key = (((long) value.assetCookie) << 32) | value.data;
}
// First, check whether we have a cached version of this drawable
// that was inflated against the specified theme.
if (!mPreloading) {
//检查是否有缓存的ConstantState 如果有根据ConstantState创建drawable返回
//1.如果thme不为null 以theme为键到mThemedEntries中获取 为null获取mNullThemedEntries 获取的集合命名为cache
//2.cache不为null 根据key到cache中获取对应的ConstantState 赋值给cs
//3.如果cs为null 根据key到mUnthemedEntries中去找对应的ConstantState 赋值给cs
//4.如果cs不为空调用cs.newDrawable 来创建个drawable并返回 通过cs创建的drawable公用同一个cs
final Drawable cachedDrawable = caches.getInstance(key, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
// Next, check preloaded drawables. These may contain unresolved theme
// attributes.
//从预加载的可绘制的缓存中获取ConstantState
final ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
if (cs != null) {
//如果cs不为null 根据cs创建drawable
dr = cs.newDrawable(this);
} else if (isColorDrawable) {
//如果是颜色资源 创建个colorDrawable
dr = new ColorDrawable(value.data);
} else {
//根据xml或资源流来加载drawable
dr = loadDrawableForCookie(value, id, null);
}
// Determine if the drawable has unresolved theme attributes. If it
// does, we'll need to apply a theme and store it in a theme-specific
// cache.
//是否应用主题没有的话让它应用主题
final boolean canApplyTheme = dr != null && dr.canApplyTheme();
if (canApplyTheme && theme != null) {
//根据cs重新创建个cs对象 并赋值给dr
dr = dr.mutate();
dr.applyTheme(theme);
//把mutate的标志清除
dr.clearMutated();
}
// If we were able to obtain a drawable, store it in the appropriate
// cache: preload, not themed, null theme, or theme-specific.
if (dr != null) {
dr.setChangingConfigurations(value.changingConfigurations);
//缓存cs 如果mPreloading为true缓存到预加载的可绘制的缓存中 否则存储到可绘制的缓存caches中
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;
}
缓存cs
//Resource
private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
Theme theme, boolean usesTheme, long key, Drawable dr) {
final ConstantState cs = dr.getConstantState();
if (cs == null) {
return;
}
if (mPreloading) {
//把cs缓存到预加载缓存中
final int changingConfigs = cs.getChangingConfigurations();
if (isColorDrawable) {
if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
sPreloadedColorDrawables.put(key, cs);
}
} else {
if (verifyPreloadConfig(
changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
// If this resource does not vary based on layout direction,
// we can put it in all of the preload maps.
sPreloadedDrawables[0].put(key, cs);
sPreloadedDrawables[1].put(key, cs);
} else {
// Otherwise, only in the layout dir we loaded it for.
sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
}
}
}
} else {
把cs缓存到可绘制缓存中
synchronized (mAccessLock) {
caches.put(key, theme, cs, usesTheme);
}
}
}
可绘制缓存
/**
* Data structure used for caching data against themes.
*
* @param type of data to cache
*/
abstract class ThemedResourceCache {
//先找主题缓存 在找无主题缓存
//主题key不为null的缓存
private ArrayMap>> mThemedEntries;
//无主题缓存
private LongSparseArray> mUnthemedEntries;
//主题key为null的缓存
private LongSparseArray> mNullThemedEntries;
/**
* Adds a new theme-dependent entry to the cache.
*
* @param key a key that uniquely identifies the entry
* @param theme the theme against which this entry was inflated, or
* {@code null} if the entry has no theme applied
* @param entry the entry to cache
*/
public void put(long key, @Nullable Theme theme, @NonNull T entry) {
put(key, theme, entry, true);
}
/**
* Adds a new entry to the cache.
*
* @param key a key that uniquely identifies the entry
* @param theme the theme against which this entry was inflated, or
* {@code null} if the entry has no theme applied
* @param entry the entry to cache
* @param usesTheme {@code true} if the entry is affected theme changes,
* {@code false} otherwise
*/
public void put(long key, @Nullable Theme theme, @NonNull T entry, boolean usesTheme) {
if (entry == null) {
return;
}
synchronized (this) {
final LongSparseArray> entries;
if (!usesTheme) {
entries = getUnthemedLocked(true);
} else {
entries = getThemedLocked(theme, true);
}
if (entries != null) {
entries.put(key, new WeakReference<>(entry));
}
}
}
/**
* Returns an entry from the cache.
*
* @param key a key that uniquely identifies the entry
* @param theme the theme where the entry will be used
* @return a cached entry, or {@code null} if not in the cache
*/
@Nullable
public T get(long key, @Nullable Theme theme) {
// The themed (includes null-themed) and unthemed caches are mutually
// exclusive, so we'll give priority to whichever one we think we'll
// hit first. Since most of the framework drawables are themed, that's
// probably going to be the themed cache.
synchronized (this) {
final LongSparseArray> themedEntries = getThemedLocked(theme, false);
if (themedEntries != null) {
final WeakReference themedEntry = themedEntries.get(key);
if (themedEntry != null) {
return themedEntry.get();
}
}
final LongSparseArray> unthemedEntries = getUnthemedLocked(false);
if (unthemedEntries != null) {
final WeakReference unthemedEntry = unthemedEntries.get(key);
if (unthemedEntry != null) {
return unthemedEntry.get();
}
}
}
return null;
}
/**
* Prunes cache entries that have been invalidated by a configuration
* change.
*
* @param configChanges a bitmask of configuration changes
*/
public void onConfigurationChange(int configChanges) {
prune(configChanges);
}
/**
* Returns whether a cached entry has been invalidated by a configuration
* change.
*
* @param entry a cached entry
* @param configChanges a non-zero bitmask of configuration changes
* @return {@code true} if the entry is invalid, {@code false} otherwise
*/
protected abstract boolean shouldInvalidateEntry(@NonNull T entry, int configChanges);
/**
* Returns the cached data for the specified theme, optionally creating a
* new entry if one does not already exist.
*
* @param t the theme for which to return cached data
* @param create {@code true} to create an entry if one does not already
* exist, {@code false} otherwise
* @return the cached data for the theme, or {@code null} if the cache is
* empty and {@code create} was {@code false}
*/
@Nullable
private LongSparseArray> getThemedLocked(@Nullable Theme t, boolean create) {
if (t == null) {
if (mNullThemedEntries == null && create) {
mNullThemedEntries = new LongSparseArray<>(1);
}
return mNullThemedEntries;
}
if (mThemedEntries == null) {
if (create) {
mThemedEntries = new ArrayMap<>(1);
} else {
return null;
}
}
final ThemeKey key = t.getKey();
LongSparseArray> cache = mThemedEntries.get(key);
if (cache == null && create) {
cache = new LongSparseArray<>(1);
final ThemeKey keyClone = key.clone();
mThemedEntries.put(keyClone, cache);
}
return cache;
}
/**
* Returns the theme-agnostic cached data.
*
* @param create {@code true} to create an entry if one does not already
* exist, {@code false} otherwise
* @return the theme-agnostic cached data, or {@code null} if the cache is
* empty and {@code create} was {@code false}
*/
@Nullable
private LongSparseArray> getUnthemedLocked(boolean create) {
if (mUnthemedEntries == null && create) {
mUnthemedEntries = new LongSparseArray<>(1);
}
return mUnthemedEntries;
}
/**
* Prunes cache entries affected by configuration changes or where weak
* references have expired.
*
* @param configChanges a bitmask of configuration changes, or {@code 0} to
* simply prune missing weak references
* @return {@code true} if the cache is completely empty after pruning
*/
private boolean prune(int configChanges) {
synchronized (this) {
if (mThemedEntries != null) {
for (int i = mThemedEntries.size() - 1; i >= 0; i--) {
if (pruneEntriesLocked(mThemedEntries.valueAt(i), configChanges)) {
mThemedEntries.removeAt(i);
}
}
}
pruneEntriesLocked(mNullThemedEntries, configChanges);
pruneEntriesLocked(mUnthemedEntries, configChanges);
return mThemedEntries == null && mNullThemedEntries == null
&& mUnthemedEntries == null;
}
}
private boolean pruneEntriesLocked(@Nullable LongSparseArray> entries,
int configChanges) {
if (entries == null) {
return true;
}
for (int i = entries.size() - 1; i >= 0; i--) {
final WeakReference ref = entries.valueAt(i);
if (ref == null || pruneEntryLocked(ref.get(), configChanges)) {
entries.removeAt(i);
}
}
return entries.size() == 0;
}
private boolean pruneEntryLocked(@Nullable T entry, int configChanges) {
return entry == null || (configChanges != 0
&& shouldInvalidateEntry(entry, configChanges));
}
}
/**
* Class which can be used to cache Drawable resources against a theme.
*/
class DrawableCache extends ThemedResourceCache {
private final Resources mResources;
/**
* Creates a cache for the given Resources instance.
*
* @param resources the resources to use when creating new instances
*/
public DrawableCache(Resources resources) {
mResources = resources;
}
/**
* If the resource is cached, creates and returns a new instance of it.
*
* @param key a key that uniquely identifies the drawable resource
* @param theme the theme where the resource will be used
* @return a new instance of the resource, or {@code null} if not in
* the cache
*/
public Drawable getInstance(long key, Resources.Theme theme) {
final Drawable.ConstantState entry = get(key, theme);
if (entry != null) {
return entry.newDrawable(mResources, theme);
}
return null;
}
@Override
public boolean shouldInvalidateEntry(Drawable.ConstantState entry, int configChanges) {
return Configuration.needNewResources(configChanges, entry.getChangingConfigurations());
}
}
本地根据图片类型加载图片
//Resources
/**
* Loads a drawable from XML or resources stream.
*/
private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
......
////资源值或对应文件路径 因为是R.drawable.xxx 这里就是对应文件路径
final String file = value.string.toString();
.......
final Drawable dr;
try {
if (file.endsWith(".xml")) {
//xml图片
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXml(this, rp, theme);
rp.close();
} else {
//图片文件加载 AssetInputStream
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(this, value, is, file, null);
is.close();
}
} catch (Exception e) {
......
}
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return dr;
}
根据xml加载
//Drawable
public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs,
Theme theme) throws XmlPullParserException, IOException {
final Drawable drawable;
final String name = parser.getName();
switch (name) {
case "selector":
drawable = new StateListDrawable();
break;
case "animated-selector":
drawable = new AnimatedStateListDrawable();
break;
case "level-list":
drawable = new LevelListDrawable();
break;
case "layer-list":
drawable = new LayerDrawable();
break;
case "transition":
drawable = new TransitionDrawable();
break;
case "ripple":
drawable = new RippleDrawable();
break;
case "color":
drawable = new ColorDrawable();
break;
case "shape":
drawable = new GradientDrawable();
break;
case "vector":
drawable = new VectorDrawable();
break;
case "animated-vector":
drawable = new AnimatedVectorDrawable();
break;
case "scale":
drawable = new ScaleDrawable();
break;
case "clip":
drawable = new ClipDrawable();
break;
case "rotate":
drawable = new RotateDrawable();
break;
case "animated-rotate":
drawable = new AnimatedRotateDrawable();
break;
case "animation-list":
drawable = new AnimationDrawable();
break;
case "inset":
drawable = new InsetDrawable();
break;
case "bitmap":
drawable = new BitmapDrawable();
break;
case "nine-patch":
drawable = new NinePatchDrawable();
break;
default:
throw new XmlPullParserException(parser.getPositionDescription() +
": invalid drawable tag " + name);
}
drawable.inflate(r, parser, attrs, theme);
return drawable;
}
根据图片文件加载
//Drawable
public static Drawable createFromResourceStream(Resources res, TypedValue value,
InputStream is, String srcName, BitmapFactory.Options opts) {
if (is == null) {
return null;
}
/* ugh. The decodeStream contract is that we have already allocated
the pad rect, but if the bitmap does not had a ninepatch chunk,
then the pad will be ignored. If we could change this to lazily
alloc/assign the rect, we could avoid the GC churn of making new
Rects only to drop them on the floor.
*/
Rect pad = new Rect();
// Special stuff for compatibility mode: if the target density is not
// the same as the display density, but the resource -is- the same as
// the display density, then don't scale it down to the target density.
// This allows us to load the system's density-correct resources into
// an application in compatibility mode, without scaling those down
// to the compatibility density only to have them scaled back up when
// drawn to the screen.
if (opts == null) opts = new BitmapFactory.Options();
opts.inScreenDensity = res != null
? res.getDisplayMetrics().noncompatDensityDpi : DisplayMetrics.DENSITY_DEVICE;
Bitmap bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
if (bm != null) {
byte[] np = bm.getNinePatchChunk();
if (np == null || !NinePatch.isNinePatchChunk(np)) {
np = null;
pad = null;
}
final Rect opticalInsets = new Rect();
bm.getOpticalInsets(opticalInsets);
return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
}
return null;
}
根据bitmap创建drawable
//Drawable
private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np,
Rect pad, Rect layoutBounds, String srcName) {
if (np != null) {
return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
}
return new BitmapDrawable(res, bm);
}
适配密度信息 scale=targetDensity / density 分辨率 * scale + 0.5f
/**
* Decode a new Bitmap from an InputStream. This InputStream was obtained from
* resources, which we pass to be able to scale the bitmap accordingly.
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
4.4及以上密度处理在jni的doDecode方法中 4.04及以下在finishDecode中 根据bitmap来缩放 4.1到4.3 decodeStream 有密度信息的情况下计算了scale并传到jni层 且不调用finishDecode 但是jni层忽略了scale 导致4.1到4.3并不处理密度信息
//BitmapFactory
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
// we don't throw in this case, thus allowing the caller to only check
// the cache, and not force the image to be decoded.
if (is == null) {
return null;
}
Bitmap bm = null;
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
//4.4及以后的 只是把密度信息设置给bitmap的mDensity属性 4.04及以前调用finishDecode方法
setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}
return bm;
}
//BitmapFactory
private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
// ASSERT(is != null);
byte [] tempStorage = null;
if (opts != null) tempStorage = opts.inTempStorage;
if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
return nativeDecodeStream(is, tempStorage, outPadding, opts);
}
4.04 decodeStream
//BitmapFactory
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
......
return finishDecode(bm, outPadding, opts);
}
private static Bitmap finishDecode(Bitmap bm, Rect outPadding, Options opts) {
if (bm == null || opts == null) {
return bm;
}
final int density = opts.inDensity;
if (density == 0) {
return bm;
}
bm.setDensity(density);
//这上面的逻辑跟setDensityFromOptions类似
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return bm;
}
byte[] np = bm.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled || isNinePatch) {
float scale = targetDensity / (float)density;
// TODO: This is very inefficient and should be done in native by Skia
final Bitmap oldBitmap = bm;
bm = Bitmap.createScaledBitmap(oldBitmap, (int) (bm.getWidth() * scale + 0.5f),
(int) (bm.getHeight() * scale + 0.5f), true);
oldBitmap.recycle();
if (isNinePatch) {
np = nativeScaleNinePatch(np, scale, outPadding);
bm.setNinePatchChunk(np);
}
bm.setDensity(targetDensity);
}
return bm;
}
4.1到4.3的BitmapFactory
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
boolean finish = true;
......
if (is instanceof AssetManager.AssetInputS
final int asset = ((AssetManager.AssetInputStream) is).getAssetInt();tream) {
if (opts == null || (opts.inScaled && opts.inBitmap == null)) {
float scale = 1.0f;
int targetDensity = 0;
if (opts != null) {
final int density = opts.inDensity;
targetDensity = opts.inTargetDensity;
if (density != 0 && targetDensity != 0) {
scale = targetDensity / (float) density;
}
}
bm = nativeDecodeAsset(asset, outPadding, opts, true, scale);
if (bm != null && targetDensity != 0) bm.setDensity(targetDensity);
finish = false;
} else {
bm = nativeDecodeAsset(asset, outPadding, opts);
}
} else {
......
if (opts == null || (opts.inScaled && opts.inBitmap == null)) {
float scale = 1.0f;
int targetDensity = 0;
if (opts != null) {
final int density = opts.inDensity;
targetDensity = opts.inTargetDensity;
if (density != 0 && targetDensity != 0) {
scale = targetDensity / (float) density;
}
}
bm = nativeDecodeStream(is, tempStorage, outPadding, opts, true, scale);
if (bm != null && targetDensity != 0) bm.setDensity(targetDensity);
finish = false;
} else {
bm = nativeDecodeStream(is, tempStorage, outPadding, opts);
}
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return finish ? finishDecode(bm, outPadding, opts) : bm;
}
jni 4.1和4.2 是scale信息没传给doDecode 4.3传了 但是在doDecode没有处理scale
//BitmapFactory.cpp
static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jint native_asset,
jobject padding, jobject options) {
//applyScale为false scale为1.0f
return nativeDecodeAssetScaled(env, clazz, native_asset, padding, options, false, 1.0f);
}
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
//applyScale为false scale为1.0f
return nativeDecodeStreamScaled(env, clazz, is, storage, padding, options, false, 1.0f);
}
static jobject nativeDecodeStreamScaled(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options, jboolean applyScale, jfloat scale) {
jobject bitmap = NULL;
SkStream* stream = CreateJavaInputStreamAdaptor(env, is, storage, 0);
if (stream) {
// for now we don't allow purgeable with java inputstreams
bitmap = doDecode(env, stream, padding, options, false, false, applyScale, scale);
stream->unref();
}
return bitmap;
}
doDecode applyScale为false scale为1.0f willScale为false 所以密度信息不起作用
//BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false,
bool applyScale = false, float scale = 1.0f) {
......
bool willScale = applyScale && scale != 1.0f;
jobject javaBitmap = NULL;
if (options != NULL) {
......
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
}
if (willScale && javaBitmap != NULL) {
return nullObjectReturn("Cannot pre-scale a reused bitmap");
}
......
SkBitmap* decoded;
if (willScale) {
decoded = new SkBitmap;
} else {
decoded = bitmap;
}
.......
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
// update options (if any)
if (options != NULL) {
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID,
getMimeTypeString(env, decoder->getFormat()));
}
// if we're in justBounds mode, return now (skip the java bitmap)
if (mode == SkImageDecoder::kDecodeBounds_Mode) {
return NULL;
}
jbyteArray ninePatchChunk = NULL;
if (peeker.fPatch != NULL) {
if (willScale) {
scaleNinePatchChunk(peeker.fPatch, scale);
}
.......
}
jintArray layoutBounds = NULL;
if (peeker.fLayoutBounds != NULL) {
layoutBounds = env->NewIntArray(4);
if (layoutBounds == NULL) {
return nullObjectReturn("layoutBounds == null");
}
jint scaledBounds[4];
if (willScale) {
for (int i=0; i<4; i++) {
scaledBounds[i] = (jint)((((jint*)peeker.fLayoutBounds)[i]*scale) + .5f);
}
} else {
memcpy(scaledBounds, (jint*)peeker.fLayoutBounds, sizeof(scaledBounds));
}
env->SetIntArrayRegion(layoutBounds, 0, 4, scaledBounds);
if (javaBitmap != NULL) {
env->SetObjectField(javaBitmap, gBitmap_layoutBoundsFieldID, layoutBounds);
}
}
if (willScale) {
// This is weird so let me explain: we could use the scale parameter
// directly, but for historical reasons this is how the corresponding
// Dalvik code has always behaved. We simply recreate the behavior here.
// The result is slightly different from simply using scale because of
// the 0.5f rounding bias applied when computing the target image size
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
SkBitmap::Config config = decoded->config();
switch (config) {
case SkBitmap::kNo_Config:
case SkBitmap::kIndex8_Config:
case SkBitmap::kRLE_Index8_Config:
config = SkBitmap::kARGB_8888_Config;
break;
default:
break;
}
bitmap->setConfig(config, scaledWidth, scaledHeight);
bitmap->setIsOpaque(decoded->isOpaque());
if (!bitmap->allocPixels(&javaAllocator, NULL)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
.......
if (javaBitmap != NULL) {
// If a java bitmap was passed in for reuse, pass it back
return javaBitmap;
}
// now create the java bitmap
return GraphicsJNI::createBitmap(env, bitmap, javaAllocator.getStorageObj(),
isMutable, ninePatchChunk, layoutBounds, -1);
}
4.4的decode 没有scale和applyScale的参数 而是根据option来计算 scale = (float) targetDensity / density willScale = scale!=1.0f
// since we "may" create a purgeable imageref, we require the stream be ref'able
// i.e. dynamically allocated, since its lifetime may exceed the current stack
// frame.
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false) {
int sampleSize = 1;
SkImageDecoder::Mode mode = SkImageDecoder::kDecodePixels_Mode;
SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config;
bool doDither = true;
bool isMutable = false;
float scale = 1.0f;
bool isPurgeable = forcePurgeable || (allowPurgeable && optionsPurgeable(env, options));
bool preferQualityOverSpeed = false;
bool requireUnpremultiplied = false;
jobject javaBitmap = NULL;
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (optionsJustBounds(env, options)) {
mode = SkImageDecoder::kDecodeBounds_Mode;
}
// initialize these, in case we fail later on
env->SetIntField(options, gOptions_widthFieldID, -1);
env->SetIntField(options, gOptions_heightFieldID, -1);
env->SetObjectField(options, gOptions_mimeFieldID, 0);
jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
doDither = env->GetBooleanField(options, gOptions_ditherFieldID);
preferQualityOverSpeed = env->GetBooleanField(options,
gOptions_preferQualityOverSpeedFieldID);
requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
}
const bool willScale = scale != 1.0f;
isPurgeable &= !willScale;
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
if (decoder == NULL) {
return nullObjectReturn("SkImageDecoder::Factory returned null");
}
decoder->setSampleSize(sampleSize);
decoder->setDitherImage(doDither);
decoder->setPreferQualityOverSpeed(preferQualityOverSpeed);
decoder->setRequireUnpremultipliedColors(requireUnpremultiplied);
SkBitmap* outputBitmap = NULL;
unsigned int existingBufferSize = 0;
if (javaBitmap != NULL) {
outputBitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID);
if (outputBitmap->isImmutable()) {
ALOGW("Unable to reuse an immutable bitmap as an image decoder target.");
javaBitmap = NULL;
outputBitmap = NULL;
} else {
existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount(env, javaBitmap);
}
}
SkAutoTDelete adb(outputBitmap == NULL ? new SkBitmap : NULL);
if (outputBitmap == NULL) outputBitmap = adb.get();
NinePatchPeeker peeker(decoder);
decoder->setPeeker(&peeker);
SkImageDecoder::Mode decodeMode = isPurgeable ? SkImageDecoder::kDecodeBounds_Mode : mode;
JavaPixelAllocator javaAllocator(env);
RecyclingPixelAllocator recyclingAllocator(outputBitmap->pixelRef(), existingBufferSize);
ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
(SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
if (!willScale) {
// If the java allocator is being used to allocate the pixel memory, the decoder
// need not write zeroes, since the memory is initialized to 0.
decoder->setSkipWritingZeroes(outputAllocator == &javaAllocator);
decoder->setAllocator(outputAllocator);
} else if (javaBitmap != NULL) {
// check for eventual scaled bounds at allocation time, so we don't decode the bitmap
// only to find the scaled result too large to fit in the allocation
decoder->setAllocator(&scaleCheckingAllocator);
}
}
// Only setup the decoder to be deleted after its stack-based, refcounted
// components (allocators, peekers, etc) are declared. This prevents RefCnt
// asserts from firing due to the order objects are deleted from the stack.
SkAutoTDelete add(decoder);
AutoDecoderCancel adc(options, decoder);
// To fix the race condition in case "requestCancelDecode"
// happens earlier than AutoDecoderCancel object is added
// to the gAutoDecoderCancelMutex linked list.
if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) {
return nullObjectReturn("gOptions_mCancelID");
}
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefConfig, decodeMode)) {
return nullObjectReturn("decoder->decode returned false");
}
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
// update options (if any)
if (options != NULL) {
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID,
getMimeTypeString(env, decoder->getFormat()));
}
// if we're in justBounds mode, return now (skip the java bitmap)
if (mode == SkImageDecoder::kDecodeBounds_Mode) {
return NULL;
}
jbyteArray ninePatchChunk = NULL;
if (peeker.fPatch != NULL) {
if (willScale) {
scaleNinePatchChunk(peeker.fPatch, scale);
}
size_t ninePatchArraySize = peeker.fPatch->serializedSize();
ninePatchChunk = env->NewByteArray(ninePatchArraySize);
if (ninePatchChunk == NULL) {
return nullObjectReturn("ninePatchChunk == null");
}
jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
if (array == NULL) {
return nullObjectReturn("primitive array == null");
}
peeker.fPatch->serialize(array);
env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
}
jintArray layoutBounds = NULL;
if (peeker.fLayoutBounds != NULL) {
layoutBounds = env->NewIntArray(4);
if (layoutBounds == NULL) {
return nullObjectReturn("layoutBounds == null");
}
jint scaledBounds[4];
if (willScale) {
for (int i=0; i<4; i++) {
scaledBounds[i] = (jint)((((jint*)peeker.fLayoutBounds)[i]*scale) + .5f);
}
} else {
memcpy(scaledBounds, (jint*)peeker.fLayoutBounds, sizeof(scaledBounds));
}
env->SetIntArrayRegion(layoutBounds, 0, 4, scaledBounds);
if (javaBitmap != NULL) {
env->SetObjectField(javaBitmap, gBitmap_layoutBoundsFieldID, layoutBounds);
}
}
if (willScale) {
// This is weird so let me explain: we could use the scale parameter
// directly, but for historical reasons this is how the corresponding
// Dalvik code has always behaved. We simply recreate the behavior here.
// The result is slightly different from simply using scale because of
// the 0.5f rounding bias applied when computing the target image size
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// TODO: avoid copying when scaled size equals decodingBitmap size
SkBitmap::Config config = configForScaledOutput(decodingBitmap.config());
// FIXME: If the alphaType is kUnpremul and the image has alpha, the
// colors may not be correct, since Skia does not yet support drawing
// to/from unpremultiplied bitmaps.
outputBitmap->setConfig(config, scaledWidth, scaledHeight, 0,
decodingBitmap.alphaType());
if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
// If outputBitmap's pixels are newly allocated by Java, there is no need
// to erase to 0, since the pixels were initialized to 0.
if (outputAllocator != &javaAllocator) {
outputBitmap->eraseColor(0);
}
SkPaint paint;
paint.setFilterLevel(SkPaint::kLow_FilterLevel);
SkCanvas canvas(*outputBitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
outputBitmap->swap(decodingBitmap);
}
if (padding) {
if (peeker.fPatch != NULL) {
GraphicsJNI::set_jrect(env, padding,
peeker.fPatch->paddingLeft, peeker.fPatch->paddingTop,
peeker.fPatch->paddingRight, peeker.fPatch->paddingBottom);
} else {
GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
}
}
SkPixelRef* pr;
if (isPurgeable) {
pr = installPixelRef(outputBitmap, stream, sampleSize, doDither);
} else {
// if we get here, we're in kDecodePixels_Mode and will therefore
// already have a pixelref installed.
pr = outputBitmap->pixelRef();
}
if (pr == NULL) {
return nullObjectReturn("Got null SkPixelRef");
}
if (!isMutable && javaBitmap == NULL) {
// promise we will never change our pixels (great for sharing and pictures)
pr->setImmutable();
}
// detach bitmap from its autodeleter, since we want to own it now
adb.detach();
if (javaBitmap != NULL) {
bool isPremultiplied = !requireUnpremultiplied;
GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap, isPremultiplied);
outputBitmap->notifyPixelsChanged();
// If a java bitmap was passed in for reuse, pass it back
return javaBitmap;
}
int bitmapCreateFlags = 0x0;
if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable;
if (!requireUnpremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied;
// now create the java bitmap
return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),
bitmapCreateFlags, ninePatchChunk, layoutBounds, -1);
}
根据array来decode
//BitmapFactory
static jobject nativeDecodeByteArray(JNIEnv* env, jobject, jbyteArray byteArray,
int offset, int length, jobject options) {
/* If optionsShareable() we could decide to just wrap the java array and
share it, but that means adding a globalref to the java array object
and managing its lifetime. For now we just always copy the array's data
if optionsPurgeable(), unless we're just decoding bounds.
*/
bool purgeable = optionsPurgeable(env, options) && !optionsJustBounds(env, options);
AutoJavaByteArray ar(env, byteArray);
SkMemoryStream* stream = new SkMemoryStream(ar.ptr() + offset, length, purgeable);
SkAutoUnref aur(stream);
return doDecode(env, stream, NULL, options, purgeable);
}
对图片进行质量压缩
/**
* 按图片质量压缩
* @param bitmap 要压缩的图片
* @return
*/
public static Bitmap compressImage(Bitmap bitmap){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
int options = 100;
//循环判断如果压缩后图片是否大于50kb,大于继续压缩
while ( baos.toByteArray().length / 1024>50) {
//清空baos
baos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
options -= 10;//每次都减少10
}
//把压缩后的数据baos存放到ByteArrayInputStream中
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
//把ByteArrayInputStream数据生成图片
Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);
return newBitmap;
}
对图片进行尺寸压缩
/**
* 按图片尺寸压缩
* @param bitmap 要压缩的图片
* @param pixelW 要压缩到的宽
* @param pixelH 要压缩到的高
* @return
*/
public static Bitmap compressImage(Bitmap bitmap, int pixelW, int pixelH) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
if( os.toByteArray().length / 1024>512) {
//如果图片大于0.5M进行压缩避免在用BitmapFactory.decodeStream生成图片时溢出
os.reset();
//这里压缩50%,把压缩后的数据存放到baos中
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);
}
ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is, null, options);
options.inJustDecodeBounds = false;
options.inSampleSize = computeSampleSize(options , pixelH > pixelW ? pixelW : pixelH ,pixelW * pixelH );
is = new ByteArrayInputStream(os.toByteArray());
Bitmap newBitmap = BitmapFactory.decodeStream(is, null, options);
return newBitmap;
}
动态算出缩放比例
/**
* 动态计算出图片的inSampleSize
* @param options
* @param minSideLength 宽和高小的那个
* @param maxNumOfPixels 宽乘以高
* @return
*/
public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels);
int roundedSize;
if (initialSize <= 8) {
roundedSize = 1;
while (roundedSize < initialSize) {
roundedSize <<= 1;
}
} else {
roundedSize = (initialSize + 7) / 8 * 8;
}
return roundedSize;
}
private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
return lowerBound;
}
if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
return 1;
} else if (minSideLength == -1) {
return lowerBound;
} else {
return upperBound;
}
}
使用libjpeg.so库进行压缩
Android中图片处理使用了一个叫做skia的开源图形处理引擎 是对libjpeg(广泛使用的开源JPEG图像库)进行了封装 位于android源码的/external/skia 目录。我们平时在java层使用一个图片处理的函数实际上底层就是调用了这个开源引擎中的相关的函数。
android在进行jpeg压缩编码的时候,考虑到了效率问题使用了定长编码方式进行编码,而IOS使用了变长编码的算法——哈夫曼算法且对skia引擎也做了优化 所以我们也可以使用哈夫曼算法
libjpeg在压缩图像时,有一个参数叫optimize_coding,该参数为true则是使用哈夫曼算法 可以根据该参数来控制是否使用哈夫曼算法
libjpeg图片压缩
自定义drawable