美文网首页
ImageView加载图片源码分析——src是如何变成可见图片的

ImageView加载图片源码分析——src是如何变成可见图片的

作者: FENGAO | 来源:发表于2019-08-18 19:54 被阅读0次

    前段时间码代码时碰到了一个比较奇怪的问题:ImageView去加载不用分辨率文件夹下的同一张图片时,读出来的图片大小是不一样的。打个比方:

    我分别在drawable、drawable-xhdpi、drawable-xxhdpi文件夹中放入同一张图片,但是名字不同,分别是"a.png"、"b.png"、"c.png",这样就可以在不论在什么分辨率的情况下同一个src只会加载同一张图片。接下来在Activity的布局中写上三个ImageView 宽高模式都是wrap_content,分别去加载图片a、b、c。接下在你会发现这三个ImageView的大小完全不一样,加载a图片的最大,b次之,加载c的最小。如图: 对比.png
    其实有经验的同学一看就知道,这是Android系统为了适配不同分辨率屏幕做的处理,今天我们就带着这个问题,去看一下布局文件中的src属性,到底是如何变成肉眼可见的图片的,以及发生以上问题的原因,和基于此问题的优化建议。

    ImageView与AppCompatImageView

    AppCompatImageView是appcompatV7包中的AppCompatXX视图组件,假如工程中使用的是AppCompatActivity,那么布局文件中ImageView在LayoutInflater.inflate时都会被替换成AppCompatImageView,具体的替换是在AppCompatViewInflater类中:

      final View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs, boolean inheritContext,
                boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
            final Context originalContext = context;
    
            // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
            // by using the parent's context
            if (inheritContext && parent != null) {
                context = parent.getContext();
            }
            if (readAndroidTheme || readAppTheme) {
                // We then apply the theme on the context, if specified
                context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
            }
            if (wrapContext) {
                context = TintContextWrapper.wrap(context);
            }
    
            View view = null;
    
            // We need to 'inject' our tint aware Views in place of the standard framework versions
            switch (name) {
                ...
                case "ImageView":
                    view = createImageView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "Button":
                    view = createButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
              ...
            }
            return view;
        }
    
        protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
            return new AppCompatImageView(context, attrs);
        }
    

    AppCompatViewInflater解析到ImageView时直接返回了一个AppCompatImageView。
    虽然现在大部分工程都是用的AppCompatActivity,但其实对于由src到图片绘制这一块的逻辑基本还是在ImageView上。

    src转换成可绘制图片的过程

    从上段可以知道,xml在被读取之后经过view的名字对比,进而生成了AppCompatImageView。

        public AppCompatImageView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public AppCompatImageView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
    
            mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
            mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
    
            mImageHelper = new AppCompatImageHelper(this);
            mImageHelper.loadFromAttributes(attrs, defStyleAttr);
        }
    
    

    AppCompatImageView的构造函数中,主要是额外的实现了对高版本特性的适配,对于本身的绘制逻辑的初始化还是在父构造函数中实现的。即ImageView中

     public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
                int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
    
            initImageView();
    
            // ImageView is not important by default, unless app developer overrode attribute.
            if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
                setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
            }
    
            final TypedArray a = context.obtainStyledAttributes(
                    attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
    
            final Drawable d = a.getDrawable(R.styleable.ImageView_src);
            if (d != null) {
                setImageDrawable(d);
            }        
             ...
    }
    

    在这可以看到正是TypedArray 的getDrawable方法,返回了绘制用的Drawable ,后经setImageDrawable将返回的Drawable正式设置给ImageView。
    跟着方法一路看下去,最终调用了TypedArray 的getDrawableForDensity

        public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
            if (mRecycled) {
                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);
                }
    
                if (density > 0) {
                    // If the density is overridden, the value in the TypedArray will not reflect this.
                    // Do a separate lookup of the resourceId with the density override.
                    mResources.getValueForDensity(value.resourceId, density, value, true);
                }
                return mResources.loadDrawable(value, value.resourceId, density, mTheme);
            }
            return null;
        }
    

    index代表了src的属性标识,density表示了当前屏幕密度,这个方法主要是对value 进行赋值
    重点看下给value赋值这块

        private boolean getValueAt(int index, TypedValue outValue) {
            final int[] data = mData;
            final int type = data[index+AssetManager.STYLE_TYPE];
            if (type == TypedValue.TYPE_NULL) {
                return false;
            }
            outValue.type = type;
            outValue.data = data[index+AssetManager.STYLE_DATA];
            outValue.assetCookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
            outValue.resourceId = data[index+AssetManager.STYLE_RESOURCE_ID];
            outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
                    data[index + AssetManager.STYLE_CHANGING_CONFIGURATIONS]);
            outValue.density = data[index+AssetManager.STYLE_DENSITY];
            outValue.string = (type == TypedValue.TYPE_STRING) ? loadStringValueAt(index) : null;
            return true;
        }
    

    mData里边存储了控件属性的值,mData的长度是属性数量*6+1,数组的第一个元素储存的是属性的个数,乘以6是因为每个属性都有6个字段

        /*package*/ static final int STYLE_TYPE = 0;
        /*package*/ static final int STYLE_DATA = 1;
        /*package*/ static final int STYLE_ASSET_COOKIE = 2;
        /*package*/ static final int STYLE_RESOURCE_ID = 3;
    
        /* Offset within typed data array for native changingConfigurations. */
        static final int STYLE_CHANGING_CONFIGURATIONS = 4;
    
        /*package*/ static final int STYLE_DENSITY = 5;
    

    getValueAt的参数index就是要处理的属性的起始坐标,里边依次将属性的六个字段的值赋予outValue,在这完成了属性值的填充。outValue.string存储了ImageView加载图片的path路径
    再回来继续看mResources.loadDrawable
    density一般是0标识取系统的默认值,所以直接看mResources.loadDrawable

        Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
                throws NotFoundException {
            return mResourcesImpl.loadDrawable(this, value, id, density, theme);
        }
    

    可以看到具体的实现在ResourcesImpl中,但是这要说一下国产的ROM中基本都自己重写了ResourcesImpl,也就是说只有原生Android才是使用了ResourcesImpl。

        @Nullable
        Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
                int density, @Nullable Resources.Theme theme)
                throws NotFoundException {
                ...
       
                if (isColorDrawable) {
                    cs = sPreloadedColorDrawables.get(key);
                } else {
                    cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
                }
    
                Drawable dr;
                boolean needsNewDrawableAfterCache = false;
                if (cs != null) {
                    if (TRACE_FOR_DETAILED_PRELOAD) {
                        // Log only framework resources
                        if (((id >>> 24) == 0x1) && (android.os.Process.myUid() != 0)) {
                            final String name = getResourceName(id);
                            if (name != null) {
                                Log.d(TAG_PRELOAD, "Hit preloaded FW drawable #"
                                        + Integer.toHexString(id) + " " + name);
                            }
                        }
                    }
                    dr = cs.newDrawable(wrapper);
                } else if (isColorDrawable) {
                    dr = new ColorDrawable(value.data);
                } else {
                    dr = loadDrawableForCookie(wrapper, value, id, density, null);
                }
              ...
        }
    

    这里边的逻辑,会先尝试从缓存去拿Drawable,没有缓存的话会去判断是否是ColorDrawable,现在咱们要看的是图片类型的Drawable,所以最终是由loadDrawableForCookie方法生成的Drawable。

        private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
                int id, int density, @Nullable Resources.Theme theme) {
                ...
                if (file.endsWith(".xml")) {
                    final XmlResourceParser rp = loadXmlResourceParser(
                            file, id, value.assetCookie, "drawable");
                    dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme);
                    rp.close();
                } else {
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
                    is.close();
                }
                ...
        }
    

    这个方法主要也是做了一个简单的判断,file就是getValueAt()中填充进string字段的值。可以理解成图片文件的路径,但不是绝对路径类似“drawable-xxhdpi/image.png”,一个根据屏幕分辩率指定的相对文件地址。在这个方法中很明显会走下面的分支判断,也就是调用 Drawable.createFromResourceStream方法。mAssets.openNonAsset生成的流是AssetManager.AssetInputStream,这一点会在后续的判断中起到作用。
    接下来看createFromResourceStream,这里边其实逻辑比较简单,首先获取了屏幕像素密度,然后调用了BitmapFactory.decodeResourceStream方法。像素密度其实就是调用了getResources().getDisplayMetrics().density;获取了系统的逻辑像素密度,值存放到了opts.inScreenDensity 。
    然后看BitmapFactory.decodeResourceStream

        public static Bitmap decodeResourceStream(Resources res, TypedValue value,
                InputStream is, Rect pad, Options opts) {
            validate(opts);
            if (opts == null) {
                opts = new Options();
            }
    
            if ( opts.inTargetDensity  == 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);
        }
    

    这一段其实还是针对dpi做的文章,目标dpi,屏幕dpi,逻辑分辨率,在这详细的捋一下。这段代码主要是给 opts.inTargetDensity 和 opts.inDensity 赋值,
    opts.inTargetDensity 代表了绘制目标设备的像素密度,说白了就是手机屏幕的像素密度。
    opts.inDensity则指的是资源的目标像素密度“The pixel density to use for the bitmap”,即图片是哪个文件夹的:drawable(160dpi)、drawable-xhdpi(320dpi)、drawable-xxhdpi(480dpi)、drawable-xxxhdpi(720dpi)等。上个方法中的opts.inScreenDensity Google给的注释是“The pixel density of the actual screen that is being used”,正在使用的实际屏幕的像素密度,我是没太琢磨出来是啥意思,跟 opts.inTargetDensity 的意思有点相同。咱们来看他们的实际值
    opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    opts.inScreenDensity = getResources().getDisplayMetrics().density;
    这就能看出来区别了其实inScreenDensity 指的是屏幕的相对像素密度,值为dpi/160,inTargetDensity 的值则是屏幕的dpi。
    着重说一下TypedValue.DENSITY_NONE 这个类型,这指的是资源没有目标像素密度,没有对
    opts.inDensity 进行赋值。会对后续的缩放计算有很大的影响。decodeStreamb比较简单 就是对图片资源的流类型做了个判断。AssetInputStream流会调用nativeDecodeAsset方法,而nativeDecodeAsset是native方法。直接去androidxref找源码。位置在/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
    nativeDecodeAsset 又调用了doDecode方法,doDecode方法中有一个比较重要的变量:

    float scale = 1.0f;
    

    scale 正是代表了资源图片生成Bitmap时的缩放比例。scale的计算公式如下:

    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;
    }
    

    到现在我们终于可以看到这个缩放比例是怎么算出来了,其实比较简单就是手机dpi/目标dpi,比如现在主流手机dpi都是480dpi~640dpi,咱们取480计算,当你加载一张drawable文件夹的120 * 120 像素的图片时,实际生成的bitmap的大小(480/160)*120也就是 360 * 360像素。这也就解释了为什么文章开头 加载出来的a图片最大。
    并且从计算公式中还可以看出当density =0时是不会进行计算的,什么时候density =0呢,其实就是上面写的当density = TypedValue.DENSITY_NONE时,也就是只要吧图片放在drawable-nodpi文件夹的时候。

    总结

    好了,到这已经能看见imageView 的src属性是如何一步步的从一个资源id变成了一张肉眼可见的图片。其实整体过程并不复杂,稍微复杂的就是各种density的处理。后续生成Bitmap之后会利用这个bitmap生成一个BitmapDrawable,再把这个Drawable设置给ImageView。后续就是ImageView自己对Drawable进行缩放,绘制的逻辑,继而变成了一张显示在手机上的图片。本篇主要分析src到Drawable的转换,不详叙述ImageView自己的处理逻辑了。
    文章开篇的问题已经讲明白了。不过在这里会引申出另外一个问题:
    依旧如开头所述,设想这么一种情况:我分别在drawable、drawable-xhdpi、drawable-xxhdpi文件夹中放入同一张图片,但是名字不同,分别是"a.png"、"b.png"、"c.png",这样就可以在不论在什么分辨率的情况下同一个src只会加载同一张图片。接下来在Activity的布局中写上三个ImageView 宽高模式都是wrap_content,分别去加载图片a、b、c。那么虽然是一张图片,加载时间一样吗?我们用代码验证一下.

        private ImageView iv1;
        private ImageView iv2;
        private ImageView iv3;
    
        public void load1(View view) {
            Log.i(TAG, "load1 start: "+System.currentTimeMillis());
            iv1.setImageResource(R.drawable.a);
            Log.i(TAG, "load1 end: "+System.currentTimeMillis());
        }
    
        public void load2(View view) {
            Log.i(TAG, "load2 start: "+System.currentTimeMillis());
            iv2.setImageResource(R.drawable.b);
            Log.i(TAG, "load2 end: "+System.currentTimeMillis());
        }
    
        public void load3(View view) {
            Log.i(TAG, "load3 start: "+System.currentTimeMillis());
            iv3.setImageResource(R.drawable.c);
            Log.i(TAG, "load3 end: "+System.currentTimeMillis());
        }
    

    日志打印结果:

    2019-08-18 19:32:52.177 14560-14560/com.ebupt.imageview I/123: load1 start: 1566127972176
    2019-08-18 19:32:52.186 14560-14560/com.ebupt.imageview I/123: load1 end: 1566127972186
    2019-08-18 19:32:53.562 14560-14560/com.ebupt.imageview I/123: load2 start: 1566127973561
    2019-08-18 19:32:53.568 14560-14560/com.ebupt.imageview I/123: load2 end: 1566127973568
    2019-08-18 19:32:54.207 14560-14560/com.ebupt.imageview I/123: load3 start: 1566127974207
    2019-08-18 19:32:54.212 14560-14560/com.ebupt.imageview I/123: load3 end: 1566127974212
    

    虽然这只是一次的测试结果,可能存在误差,但其实经过我多次尝试,的确加载drawable文件夹下的图片时最耗时的,由此可见,资源的缩放如果没有很好的处理的话会浪费不少的时间。
    针对此情况就可以利用上述的提到的drawable-nodpi文件夹,将一些没做分辨率区分的通用图片都放在drawable-nodpi文件夹中。可以提升应用的性能。

    相关文章

      网友评论

          本文标题:ImageView加载图片源码分析——src是如何变成可见图片的

          本文链接:https://www.haomeiwen.com/subject/gumtlctx.html