本文分析的代码版本为Android 6.0
请尊重博主劳动成果,转载请标明出处。
在Activity或Fragment中常使用getResources().getDrawable()加载图片资源。getResources和getDrawable方法的调用需要经过哪些步骤和流程。
ContextThemeWrapper的
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
只分析mOverrideConfiguration==null条件下如何获取resource。super.getResources()调用的是ContextWrapper的方法,而ContextWrapper调用的是mBase.getResources()。mBase是ContextImpl实例,在Activity被ActivityThread创建时,通过Activity的attach()方法赋值给Activity的。Activity和ContextImpl的创建和赋值过程这里就不分析了。
ContextImpl的getResources()方法:
@Override
public Resources getResources() {
return mResources;
}
而mResources是在ContextImpl的私有构造方法内创建的。
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
…//省略部分无关代码
mMainThread = mainThread;
mActivityToken = activityToken;
mFlags = flags;
if (user == null) {
user = Process.myUserHandle();
}
mUser = user;
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
final int displayId = (createDisplayWithId != Display.INVALID_DISPLAY)
? createDisplayWithId
: (display != null) ? display.getDisplayId() : Display.DEFAULT_DISPLAY;
CompatibilityInfo compatInfo = null;
if (container != null) {
compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
}
if (compatInfo == null) {
compatInfo = (displayId == Display.DEFAULT_DISPLAY)
? packageInfo.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
}
//创建
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
if (container != null) {
// This is a nested Context, so it can't be a base Activity context.
// Just create a regular Resources object associated with the Activity.
resources = mResourcesManager.getResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
} else {
// This is not a nested Context, so it must be the root Activity context.
// All other nested Contexts will inherit the configuration set here.
//创建Activity的Context时的赋值流程
resources = mResourcesManager.createBaseActivityResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
}
}
}
mResources = resources;
…//省略部分无关代码
}
这里就不分析packageInfo.getResources,mResourcesManager.getResources和mResourcesManager.createBaseActivityResources如何创建Resources。
继续看Resources#getDrawable:
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
//获取TypedValue,这个对象很重要,获取图片流时会用到
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
//根据资源id获取信息,并对value赋值
//这一步很重要,赋值后,就获取到对应资源的基本信息了。
impl.getValue(id, value, true);
return impl.loadDrawable(this, value, id, theme, true);
} finally {
releaseTempTypedValue(value);
}
}
ResourcesImpl#getValue方法对TypedValue赋值,然后ResourcesImpl#loadDrawable加载图片。真正获取Drawable的是ResourcesImpl。继续往下看代码:
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
//使用AssetManager的native方法根据id和当前屏幕信息对value赋值
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
@Nullable
Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache) throws NotFoundException {
try {
...//省略无关代码
final boolean isColorDrawable;
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
//使用color的xml相关的Drawable
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
//非color的
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. Skip the cache if
// we're currently preloading or we're not using the cache.
//缓存获取
if (!mPreloading && useCache) {
final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
// Next, check preloaded drawables. Preloaded drawables may contain
// unresolved theme attributes.
final Drawable.ConstantState cs;
//从预先加载的缓存回去
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
if (cs != null) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
//******* 执行根据资源id加载图片 *******
dr = loadDrawableForCookie(wrapper, 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) {
dr = dr.mutate();
dr.applyTheme(theme);
dr.clearMutated();
}
// If we were able to obtain a drawable, store it in the appropriate
// cache: preload, not themed, null theme, or theme-specific. Don't
// pollute the cache with drawables loaded from a foreign density.
//缓存效果信息和资源
if (dr != null && useCache) {
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;
} catch (Exception e) {
String name;
try {
name = getResourceName(id);
} catch (NotFoundException e2) {
name = "(missing name)";
}
// The target drawable might fail to load for any number of
// reasons, but we always want to include the resource name.
// Since the client already expects this method to throw a
// NotFoundException, just throw one of those.
final NotFoundException nfe = new NotFoundException("Drawable " + name
+ " with resource ID #0x" + Integer.toHexString(id), e);
nfe.setStackTrace(new StackTraceElement[0]);
throw nfe;
}
}
查看loadDrawableForCookie(这是真正加载图片的方法,重点关注):
private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
Resources.Theme theme) {
if (value.string == null) {
throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
+ Integer.toHexString(id) + ") is not a Drawable (color or path): " + value);
}
//**** 重点信息:文件路径信息(是指存储在resources.arsc资源块中的文件路径,非目录信息)****
final String file = value.string.toString();
...//省略无关日志信息
final Drawable dr;
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
try {
if (file.endsWith(".xml")) {//获取xml类型的Drawable
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXml(wrapper, rp, theme);
rp.close();
} else {
//******* 开始加载图片 *******
//使用AssetManger的native方法获取图片资源的流
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
//使用Drawable的静态方法构建
dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
is.close();
//******* 加载图片结束 *******
}
} catch (Exception e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return dr;
}
查看Drawable#createFromResourceStream:
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();
//设置当前屏幕的信息(160,240,320,480或640等等)
//这些信息会决定生成bitmap的宽高
opts.inScreenDensity = Drawable.resolveDensity(res, 0);
//由于没有设置config,所以会默认采用Bitmap.Config.ARGB_8888。
//即在内存中一个像素使用4个字节来存储
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;
}
至此,根据资源Id加载图片的流程已分析完毕。
从上一节的流程分析,可以清楚的知道通过使用Resource,TypedValue,AssetManager和BitmapFactory,我们也可以像应用框架一样根据id加载图片并控制生成的图片大小。
那么就敲代码验证。
布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_bitmap_test"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:src="@mipmap/ic_launcher"/>
RelativeLayout>
测试Activity的主要代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bitmap_test);
testLoadBitmap(R.mipmap.ic_launcher);
}
public void testLoadBitmap(int resId) {
try {
//资源名称
String name = getResources().getResourceName(resId);
TypedValue value = new TypedValue();
//对value赋值
getResources().getValue(resId, value, true);
//资源路径
String fs = value.string.toString();
//Bitmap配置信息
BitmapFactory.Options options = new BitmapFactory.Options();
options.inScreenDensity = getResources().getDisplayMetrics().densityDpi;
//获取资源信息
AssetFileDescriptor afd = getAssets().openNonAssetFd(value.assetCookie, fs);
//生产Bitmap
Bitmap bm = BitmapFactory.decodeResourceStream(getResources(), value, afd.createInputStream(), new Rect(), options);
//日志信息
StringBuilder info = new StringBuilder();
info.append(" name: ").append(name).append(" \n");
info.append(" file info: ").append(fs).append("\n");
info.append(" AssetFileDescriptor: ").append(afd.toString()).append("\n");
info.append(" byteCount: ").append(bm.getByteCount()).append(" \n");
info.append(" width = ").append(bm.getWidth()).append(", height = ").append(bm.getWidth()).append(" \n");
info.append(" bm density = ").append(bm.getDensity()).append(" \n");
info.append(" screenDensity = ").append(options.inScreenDensity).append(" \n");
info.append(" config = ").append(bm.getConfig().toString());
Log.i("Test", info.toString());
bm.recycle();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onResume() {
super.onResume();
ImageView iv = (ImageView) findViewById(R.id.iv);
//用断点查看drawable中的bitmap信息
iv.setContentDescription("");
}
工程目录中的ic_launcher.png信息。
目录 | mipmap-mdpi | mipmap-hdpi | mipmap-xhdpi | mipmap-xxhdpi | mipmap-xxxhdpi |
---|---|---|---|---|---|
大小(byte) | 2206 | 3418 | 4842 | 7718 | 10486 |
测试手机屏幕尺寸为1920*1080,screenDensity为480。ImageView获取的图片应该为mipmap-xxhdpi目录中的ic_launcher.png。
测试的日志信息
name: org.md.train:mipmap/ic_launcher
file info: res/mipmap-xxhdpi-v4/ic_launcher.png
AssetFileDescriptor: {AssetFileDescriptor: {ParcelFileDescriptor: FileDescriptor[50]} start=26984 len=7007}
byteCount: 82944
width = 144, height = 144
bm density = 480
screenDensity = 480
config = ARGB_8888
使用代码加载的Bitmap与布局ImageView获取的字节数,宽高和density都一致,说明代码逻辑正确。
bitmap的字节数为什么比原图片文件大很多?现在我们来计算一下。
计算格式: byteCount = width * height * 4;
因为生成bitmap时,使用默认的Config,即Bitmap.Config.ARGB_8888,所以为乘4。如果使用Bitmap.Config.ARGB_565,采用2个字节存储一个像素,则为乘2。
144 * 144* 4 = 82944,刚好为所显示的字节数。
日志中AssetFileDescriptor的项信息,start为图片在resources.arsc资源块中的起始位置,len为大小。通过与信息表对比可知,图片在写入资源块时被压缩了711byte。
将mipmap-xhdpi目录的尺寸为96 * 96的ic_launcher.png复制一份,重命名为aaa.png,放入mipmap-hdpi目录。修改布局文件中ImageView的src值为aaa,使用testLoadBitmap方法加载该图片。
结果日志信息
name: org.md.train:mipmap/aaa
file info: res/mipmap-hdpi-v4/aaa.png
AssetFileDescriptor: {AssetFileDescriptor: {ParcelFileDescriptor: FileDescriptor[50]} start=220860 len=4366}
byteCount: 147456
width = 192, height = 192
bm density = 480
screenDensity = 480
config = ARGB_8888
这个结果是不是有点奇怪。为什么生成的bitmap尺寸变大了?从上一节的第3小节Drawable和Bitmap生成步骤的代码分析,这个现象就比较容易理解。
先看尺寸对照表
目录 | mipmap-mdpi | mipmap-hdpi | mipmap-xhdpi | mipmap-xxhdpi | mipmap-xxxhdpi |
---|---|---|---|---|---|
screenDensity | 160 | 240 | 320 | 480 | 640 |
aaa.png在480的手机上显示的真实宽度:96 * 480 / 240 = 192;显示的高度也是同比例放大。为什么是240,而不是320?因为aaa.png图片存放在mipmap-hdpi。如果存放在mipmap-xhdpi,则aaa.png在测试手机上显示的宽度为96 * 480 / 320 = 144。
依照以下测试条件,从当前的mipmap目录复制一份ic_launcher.png,重命名为abc.png。
(注:Y为有abc.png,N为无)
组 | mipmap-mdpi | mipmap-hdpi | mipmap-xhdpi | mipmap-xxhdpi | mipmap-xxxhdpi | 当前图片来源 |
---|---|---|---|---|---|---|
第一组 | Y | N | N | N | Y | mipmap-xxxhdpi |
第二组 | Y | Y | N | N | N | mipmap-hdpi |
第三组 | Y | Y | Y | N | N | mipmap-xhdpi |
第四组 | Y | Y | Y | N | Y | mipmap-xxxhdpi |
这个结果显示:如果当前屏幕尺寸的mipmap目录无图片,则会找最近目录中相同名称的图片。如果最近的两个目录都有,则会选择高分辨率目录的图片。
由以上的代码分析和测试结果,切图一定要放到匹配的目录。一般从1280 * 720的设计图切取的图片要放到mipmap-xhdpi目录。如果放入到低分辨率的目录,则在1280 * 720尺寸或更高的手机上时,由于需要放大,则加载图片所需的内存暴增。而放入到高分辨率的目录,则会导致在1280 * 720尺寸或更低的手机显示很模糊或很小,因为图片被缩放。
如果是代码加载图片,且图片没有透明色(即alpha通道),则最好将BitmapFactory.Options的config设置为Bitmap.Config.ARGB_565。如果是加载很大尺寸的图片,最好先只获取图片的宽高,然后按显示需要的尺寸进行缩放获取bitmap。
使用BitmapFactory.Options.inBitmap重用以前bitmap分配的内存。这个属性在Androiod 4.2版本被引入。不过需要注意:Android 4.4之前的版本,在使用inBitmap时,两者的size要相同。而4.4之后,只要新生成的Bitmap比之前的size小就可以。
源码上部分注释说明:
Prior to {@link android.os.Build.VERSION_CODES#KITKAT} additional
constraints apply: The image being decoded (whether as a resource or
as a stream) must be in jpeg or png format. Only equal sized bitmaps
are supported, with {@link #inSampleSize} set to 1. Additionally, the
{@link android.graphics.Bitmap.Config configuration} of the reused
bitmap will override the setting of {@link #inPreferredConfig}, if
set.As of {@link android.os.Build.VERSION_CODES#KITKAT}, any mutable
bitmap can be reused by {@link BitmapFactory} to decode any other
bitmaps as long as the resulting {@link Bitmap#getByteCount() byte
count} of the decoded bitmap is less than or equal to the {@link
Bitmap#getAllocationByteCount() allocated byte count} of the reused
bitmap. This can be because the intrinsic size is smaller, or its size
post scaling (for density / sample size) is smaller.
更详细的说明可以查看BitmapFactory内部类Options。