Android 源码分析Resource加载,实现换肤

2018-04-08  本文已影响0人  青果果

android中资源有图片,颜色,string,styles等等...
那是如何加载出来的呢?

通过资源id,在activity中调用getResources()方法
看源码,其实这个方法来自于context

public class ContextThemeWrapper extends ContextWrapper {
...
  @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;
    }
...

Context是抽象类,最终的实现类是ContextImpl
在ContextImpl构造方法中创建Resources
packageInfo.getResources(mainThread)

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

        mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
                : ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);
        // packageInfo类型是LoadedApk 
        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;

LoadedApk类中getResources()方法

 public Resources getResources() {
        if (mResources == null) {
            final String[] splitPaths;
            try {
                splitPaths = getSplitPaths(null);
            } catch (NameNotFoundException e) {
                // This should never fail.
                throw new AssertionError("null split not found");
            }

            mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader());
        }
        return mResources;
    }

Resources实际是在ResourcesManager中直接new出来的

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

    /**
     * Creates a new Resources object with CompatibilityInfo.
     *
     * @param classLoader class loader for the package used to load custom
     *                    resource classes, may be {@code null} to use system
     *                    class loader
     * @hide
     */
    public Resources(@Nullable ClassLoader classLoader) {
        mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
    }

那一样的我们可以打包自己的资源,仿照源码的方式来加载资源
自己创建Resources

 Resources resources = context.getResources();
        Class<AssetManager> assetManagerClass = AssetManager.class;
        AssetManager assetManager = null;
        try {
            //创建AssetManager
            assetManager = assetManagerClass.newInstance();
            //反射执行addAssetPath方法,添加资源所在路径
            Method addAssetPath = assetManagerClass.getDeclaredMethod("addAssetPath", String.class);
            addAssetPath.setAccessible(true);
            addAssetPath.invoke(assetManager, skinPath);
            mSkinResource = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
            mSkinPageName = context.getPackageManager()
                    .getPackageArchiveInfo(skinPath, PackageManager.GET_RECEIVERS).packageName;
        } catch (Exception e) {
            e.printStackTrace();
        }

AssetManager 初始化调用的jni方法 init()底层会创建AssetManager
并添加默认的系统资源路径“framework/framwork-res.apk”+“assets”,所以我们可以使用系统的资源
然后加载resources.arsc


image.png

apk打包会生成R.java文件, resources.arsc(资源映射信息)就是App的资源索引表

有个注意点:添加资源路径,底层会判断有没有注册清单文件,如果没有不会加载

     // 检查路径是否有androidmanifest . xml
    Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
            kAndroidManifest, Asset::ACCESS_BUFFER, ap);
    if (manifestAsset == NULL) {
        delete manifestAsset;
        return false;
    }

资源加载通过Resources,而我们可以做到自己构建Resources
来加载其他apk里的相同名称的资源
从而实现替换资源,达到换肤的目的还需要拦截view的创建

//拦截View的创建 LayoutInflater是单例,由系统创建的服务,存在static hashMap中
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);

源码已经告诉我们了Hook you can supply that is called when inflating from a LayoutInflater

public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         *
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         *
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

view的创建过程中,mFactory判断是否为null,不是null会回调factory的方法,可以创建view

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

通过LayoutInflaterCompat.setFactory2(layoutInflater, this),拦截View的创建
加载其他资源包的资源就能够实现换肤

上一篇下一篇

猜你喜欢

热点阅读