博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
android应用图片加载与存放目录分析
阅读量:7098 次
发布时间:2019-06-28

本文共 17428 字,大约阅读时间需要 58 分钟。

hot3.png

本文最先发布在CSDN博客,地址:

本文分析的代码版本为Android 5.0

一 使用资源id加载加载图片流程

在Activity或Fragment中常使用getResources().getDrawable()加载图片资源。getResources和getDrawable方法的调用需要经过哪些步骤和流程。

1 与Context相关的步骤

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。

2 Resources和ResourcesImpl中的步骤

继续看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;    }

3 Drawable和Bitmap生成步骤

查看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加载图片的流程已分析完毕。

二 图片在不同drawable/mipmap目录与生成Bitmap大小的关系

从上一节的流程分析,可以清楚的知道通过使用Resource,TypedValue,AssetManager和BitmapFactory,我们也可以像应用框架一样根据id加载图片并控制生成的图片大小。 那么就敲代码验证。 布局文件

测试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。

1 测试一

测试的日志信息

name: org.md.train:mipmap/ic_launcher file info: res/mipmap-xxhdpi-v4/ic_launcher.pngAssetFileDescriptor: {AssetFileDescriptor: {ParcelFileDescriptor: FileDescriptor[50]} start=26984 len=7007}byteCount: 82944 width = 144, height = 144 bm density = 480 screenDensity = 480 config = ARGB_8888

断点查看的信息 avatar

使用代码加载的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为大小。通过与信息表可知,图片在写入资源块时会被压缩。

2 测试二

将mipmap-xhdpi目录的尺寸为96 * 96的ic_launcher.png复制一份,重命名为aaa.png,放入mipmap-hdpi目录。修改布局文件中ImageView的src值为aaa,使用testLoadBitmap方法加载该图片。 结果日志信息

name: org.md.train:mipmap/aaafile info: res/mipmap-hdpi-v4/aaa.pngAssetFileDescriptor: {AssetFileDescriptor: {ParcelFileDescriptor: FileDescriptor[50]} start=220860 len=4366}byteCount: 147456width = 192, height = 192bm density = 480screenDensity = 480config = ARGB_8888

查看到ImageView中的bitmap信息 测试信息

这个结果是不是有点奇怪。为什么生成的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。

3 测试三

依照以下测试条件,从当前的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。

转载于:https://my.oschina.net/u/269239/blog/1236295

你可能感兴趣的文章
20170713L08-00老男孩Linux运维实战培训-DELL R710服务器RAID配置实战演示
查看>>
redis的批量删除
查看>>
php生成随机密码的几种方法
查看>>
我的友情链接
查看>>
在防火墙配置自定义服务
查看>>
vSphere 6.0 -Difference between vSphere 5.0, 5.1, 5.5 and vSphere 6.0
查看>>
Collect VMware support log&Performance Snapshot
查看>>
Enable PowerShell script execution policy
查看>>
aix 设置主机信任
查看>>
编程题:输入一串字符,程序会自动将大写字母转换为小写
查看>>
js赋值时特殊字符完美处理方案
查看>>
Linux基础之文本查看命令(cat,tac,rev,head,tail,more,less)
查看>>
同一表中重复数据处理
查看>>
Mail、计划任务
查看>>
yii框架中model映射数据库中不存在的表,做请求转发的接口
查看>>
我的友情链接
查看>>
RHEL6下YUM安装源的配置
查看>>
轻松搞定面试中的链表题目
查看>>
利用google-authenticator给SSH加密
查看>>
asp.net 多个空格转成一个空格
查看>>