Res目录下资源如图片文件和xml文件资源如何被加载显示出来

紧接上一篇 Android App启动时Apk资源加载机制源码分析 分析App启动时是如何加载初始化资源之后,接下来分析开发过程中调用显示资源View的Api接口(如设置背景图片)具体是如何把启动时加载好的图片显示出来。上篇讲的启动时候加载资源只是把资源初始化准备好,而这篇是将具体实际显示资源时候,是如何把启动时准备好的res资源加载出来供View显示。可以说上篇就是AssetManager实现了全部相关资源加载的第一步,这篇要讲的就是Resources是如何把获取相应id的具体资源加载出来。

在View及子View中提供很多资源显示的Api,下面具体就图片资源文件和Xml文件加载来分别讲述其源码实现原理以及相关可以学习的技巧。

加载背景图片资源的源码实现分析

开发中设置加载背景图片,只需要通过Api即可完成,如View设置背景和ImageView设置,接下来具体分享实现:

 View加载背景图片

  public void setBackgroundResource(@DrawableRes int resid) {
        if (resid != 0 && resid == mBackgroundResource) {
            return;
        }

        Drawable d = null;
        if (resid != 0) {
            d = mContext.getDrawable(resid);
        }
        setBackground(d);

        mBackgroundResource = resid;
    }
ImageView加载背景图片

public void setImageResource(@DrawableRes int resId) {
      .......
        mUri = null;
        //加载资源
        resolveUri();

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }

private void resolveUri() {
       ......
        Drawable d = null;
        if (mResource != 0) {
            try {
                d = mContext.getDrawable(mResource);
            } catch (Exception e) {
                mUri = null;
            }
        } else if (mUri != null) {
            d = getDrawableFromUri(mUri);

            if (d == null) {
                Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri);
                // Don't try again.
                mUri = null;
            }
        } else {
            return;
        }

        updateDrawable(d);
    }

可以看到具体资源加载出来的Drawable对象,通过mContext.getDrawable(resid)来完成加载封装。下面先转而先介绍下所有Context的资源Resources具体来由。

Activity中资源Resources 的由来

上午的mContext一般都是Activity,继续看Context中的实现:

public abstract class Context {
 @Nullable
    public final Drawable getDrawable(@DrawableRes int id) {
        return getResources().getDrawable(id, getTheme());
    }

public abstract Resources getResources();
}

可以看到Context转给Resouces,并且本身是个抽象类,那么Activity作为Context子类,间接实现者,看看其如何实现:

public class ContextWrapper extends Context {
 Context mBase;
....
protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

 @Override
    public Resources getResources() {
        return mBase.getResources();
    }
 .....   
 }

可以看ContextWrapper 只是一个空壳具体实现交给mBase,那什么时候mBase被赋值,Activity的实现:

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback {

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
           //对mBase进行赋值
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
        ......
    }
}

那Activity的attach方法什么时候被调用,查看App启动流程,在上篇文章讲到ActivityManagerService在接受到attachApplication函数调用远程消息之后,一系列处理之后,会通过AIDL接口IApplicationThread远程通知,一个就是通知ActivityThread对象中调用handleBindApplication(),另一个是handleLaunchActivity。其中handleLaunchActivity通过performLaunchActivity方法创建Activity对象并初始化其BaseContext及调用attach方法。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
       .....
        Activity activity = null;
        try {
        //创建Activity对象
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            ...
        } catch (Exception e) {
        .....
        }

        try {
        //新建Application
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

           if (activity != null) {
           //创建BaseContext
   Context appContext = createBaseContextForActivity(r, activity);
        .....
      activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor);
        .......                    
        }
        return activity;
    }

 private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {

       int displayId = Display.DEFAULT_DISPLAY;
        try {
            displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
        } catch (RemoteException e) {
        }


        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, displayId, r.overrideConfig);
        appContext.setOuterContext(activity);
        Context baseContext = appContext;
        ....
        return baseContext;
    }

可以看到BaseContext(mBase)就是ContextImpl对象,所以上文中的mBase.getResources()就是调用ContextImpl的getResources(),并获取了displayId (ActivityManagerNative.getDefault().getActivityDisplayId(r.token))

class ContextImpl extends Context {
......
 static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        return new ContextImpl(null, mainThread, packageInfo, null, null, false,
                null, overrideConfiguration, displayId);
    }

    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
        mOuterContext = this;

        mMainThread = mainThread;
        mActivityToken = activityToken;
        mRestricted = restricted;

        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;

......

        mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
                : ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);

        Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {
                resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo);
            }
        }
        mResources = resources;

       ......
        mContentResolver = new ApplicationContentResolver(this, mainThread, user);
    }
  @Override
    public Resources getResources() {
        return mResources;
    }
}

从上篇中我们可以知道,mResources是在ContextImpl对象初始化时候赋值的,mResources对于Activity时是重新通过mResourcesManager.getTopLevelResources获取,因为此时 packageInfo.getResources(mainThread)获取的resources不为null,并且displayId != Display.DEFAULT_DISPLAY,在ActivityThread中 createBaseContextForActivity(ActivityClientRecord r, final Activity activity)会获取,如上文所述。

 if (resources != null) {
            if (displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {
                resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo);
            }
        }

但是mResourcesManager.getTopLevelResources传入的参数都是来自统一个packageInfo,所以资源都是一样的。

结论

其实不仅仅Activity,其他Context的子类的中mBase都是ContextImpl对象,但都对应了同一个资源地址packageInfo。因为都是同一个App,不论是那个页面,那个组件其获得的Resources内容都是相同的。

Resources具体资源加载实现

紧接上文mContext.getDrawable(resid)实现是由context中的mResources对象实现,定位Resources类:

 public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
            throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValue(id, value, true);
            return impl.loadDrawable(this, value, id, theme, true);
        } finally {
            releaseTempTypedValue(value);
        }
    }

发现ResourcesImpl 具体实现,是在Resources初始化就实现了mResourcesImpl对象:

 public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

具体看看ResourcesImpl 的加载Drawable实现:

 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) {
                isColorDrawable = true;
                caches = mColorDrawableCache;
                key = value.data;
            } else {
                isColorDrawable = false;
                caches = mDrawableCache;
                key = (((long) value.assetCookie) << 32) | value.data;
            }

            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    return cachedDrawable;
                }
            }

            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 {
            //加载图片
                dr = loadDrawableForCookie(wrapper, value, id, null);
            }

.........
            if (dr != null && useCache) {
                //缓存Drawable
                cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
            }

            return dr;
        } catch (Exception e) {

....    
        }
    }

从上文可以发现是先从缓存中查找是否有对应的Drawable,没有就加载并缓存对象,可以看到系统为了不重复加载资源都充分利用了缓存,加快加载速度,从而也发现,图片资源Id不论加载多少次(setBackgroudRes)并不会加大内存,并且同时若直接对返回的Drawable进行修改,会导致以后均被修改了,这就回答了:Android 图片着色Tint后向兼容DrawableCompat库实现原理分析并简化封装中为何会导致着色之后,原图被修改了的问题

继续看图片加载出来的drawable的实现:

private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
            Resources.Theme theme) {

        final Drawable dr;

        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
      try {
       if (file.endsWith(".xml")) {
           final XmlResourceParser rp = loadXmlResourceParser(file, id, value.assetCookie, "drawable");
     dr = Drawable.createFromXml(wrapper, rp, 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();
            }
        } catch (Exception e) {
           throw rnf;
        }

        return dr;
    }

从这个函数可以看到实现了加载xml文件和图片资源。若是Xml文件通过
Drawable.createFromXml(wrapper, rp, theme)
来实现,而对于图片就是通过mAssets即AssetManager直接获取对应的图片的字节流然后通过
Drawable.createFromResourceStream(wrapper, value, is, file, null)
转成Drawable对象。

下面具体看看Drawable的实现

public abstract class Drawable {
.....
 public static Drawable createFromResourceStream(Resources res, TypedValue value,
            InputStream is, String srcName, BitmapFactory.Options opts) {
        if (is == null) {
            return null;
        }
  Rect pad = new Rect();
  if (opts == null) opts = new BitmapFactory.Options();
        opts.inScreenDensity = Drawable.resolveDensity(res, 0);
        Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
        if (bm != null) {
            byte[] np = bm.getNinePatchChunk();
            //.9图片加载
            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;
    }

  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);
    }
    ....
}
结论2

这里可以看到图片加载成BitmapDrawable或NinePatchDrawable对应普通图片或.9图片。这个方法很有用,在后一篇文章中自定义加载图片,就是依据这个Api来加载任意图片。

资源Drawabel的显示

上文得到的drawable对象之后,View显示绘制内容,在draw(canvas)中完成的,直接把当前的canvas传入Drawable的draw(canvas),把drawable中的内容全部绘制到当前的View上。如下设置背景drawable源码所示

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
        ........
 public void draw(Canvas canvas) {


        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        .....
    }

 private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
       ......
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
        .....
    }
.....
}

猜你喜欢

转载自blog.csdn.net/u010019468/article/details/73718636