从源码角度上分析常用的换肤原理(一)

1.适用场景

所谓的换肤,更换主题,其实是对资源文件的替换,或者说使用额外的同id的资源文件进行更新;而换肤能操作的也只能是资源,比如颜色,图片,必须是应用中预先定义好id的资源类型。

也就是说,换主题是要额外的外置资源插件,可以是apk文件,而这个插件中有和应用内相同id名称的资源文件。当需要更换主题时,去加载这个插件,得到一个新的Resources对象(这个和原应用的进行区分,同时保留,根据特定的id名称进行查找替换), 重新加载或刷新页面,如果有插件中同名的资源id的文件,那么就使用主题的Resources,否则使用原来的Resources

2. Resources

我们在使用资源文件时,最常用的方式就是从预先的资源文件中进行获取,通常使用context.getResources()这个方法,那么这个方法是怎么定位到资源的呢?

说起Context,我们查看源码时基本上都只会盯着ContextImpl这个实现类进行,这个基本上算是Context的最核心的实现类了,就比如Window我们只会盯着PhoneWindow一样。

class ContextImpl extends Context {
    private @NonNull Resources mResources;
    ......
    @Override
    public Resources getResources() {
        return mResources;
    }
    void setResources(Resources r) {
        if (r instanceof CompatResources) {
            ((CompatResources) r).setContext(this);
        }
        mResources = r;
    }
}   

ContextImpl中并没有具体创建Resources的代码,那么我就得去源头去理,资源文件的初始化应该是在app创建时候进行的,而这个的创建应该也是伴随着Context一起进行创建,那么就去ActivityThread中去查找。

 private void attach(boolean system, long startSeq) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        if (!system) {
		......			
			RuntimeInit.setApplicationObject(mAppThread.asBinder());
            final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }         
        } 
        ......
    }

因为我们创建的是非system的应用,那么再这里会通过ActivityManager.getService()也就是ActivityManagerService中的attachApplication方法去绑定一个Application

而这里的attachApplication会进行一些初始化参数设置后,回调到ActivityThreadbindApplication发送一个BIND_APPLICATIONMessage,然后最终走到ActivityThread中的handleBindApplication方法中来

   private void handleBindApplication(AppBindData data) {
   	   ......
		data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
	    ......
   	    Application app;
        try {
            app = data.info.makeApplication(data.restrictedBackupMode, null);
            mInitialApplication = app;
            ......
            try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {           
        } 
        ......
}

public Application getApplication() {
        return mInitialApplication;
    }

这里会通过makeApplication方法创建一个Application对象,这个也是我们平时获取到那个App,然后调用callApplicationOnCreate方法去执行ApplicationonCreate方法。

这里的data.info比较重要,后面会使用到,data.info本身是一个LoadedApk对象

public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
        if (mApplication != null) {
            return mApplication;
        }
        Application app = null;
        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }
        try {
            java.lang.ClassLoader cl = getClassLoader();
            ......
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
        } catch (Exception e) {
         
        }
        mActivityThread.mAllApplications.add(app);
        mApplication = app;
        .....
        return app;
    }

这个方法最终会调用ContextImplcreateAppContext方法中来,使用这个创建一个Context,然后把这个传给InstrumentationnewApplication方法创建最终的App对象出来。

public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        Application app = getFactory(context.getPackageName())
                .instantiateApplication(cl, className);
        app.attach(context);
        return app;
    }

这里的attach方法中调用了我们经常重写的attachBaseContext方法,所以attachBaseContext方法可以认为是资源初始化完成后马上调用的一个方法。

回到上面的createAppContext方法中

 static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        return createAppContext(mainThread, packageInfo, null);
    }

    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
            String opPackageName) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null, opPackageName);
        context.setResources(packageInfo.getResources());
        return context;
    }

这里可以看到我们上面的设置资源的方法setResources,而资源是通过packageInfo获取的

注意到执行ContextImpl.createAppContext(mActivityThread, this);传入了一个this,这个对应了上面创建的data.info,也就是LoadedApk对象,所以getResource方法是

public Resources getResources() {
        if (mResources == null) {
            final String[] splitPaths;
            try {
                splitPaths = getSplitPaths(null);
            } catch (NameNotFoundException e) {
                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;
    }

而这个ResourcesManager.getInstance().getResources()最终会通过getOrCreateResources方法去创建一个Resources

 private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        	.....
        	//前面的代码是一些缓存的查找
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }

            // Add this ResourcesImpl to the cache.
            mResourceImpls.put(key, new WeakReference<>(resourcesImpl));

            final Resources resources;
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl, key.mCompatInfo);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
            return resources;
        }
    }

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

        return impl;
    }

其实这里可以看出Resources的核心是mResourcesImplResoureces可以看成一个代理方式,里面的逻辑都交给Impl去处理,比如getDrawable最终会执行到这个方法

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

那么我们就可以猜想,只要能创建一个ResourceImpl,那么就能创建Resources,如果能动态切换系统加载的资源,就能实现更换主题的功能了,那么问题回到上面,注意到ResourceImpl中有一个核心的AssetManager对象,只要能创建这个对象,那么就能创建ResourceImpl了吧

  protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        final AssetManager.Builder builder = new AssetManager.Builder();
        if (key.mResDir != null) {
            try {
                builder.addApkAssets(loadApkAssets(key.mResDir, false /
            } catch (IOException e) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }
        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                try {
                    builder.addApkAssets(loadApkAssets(splitResDir, false 
                } catch (IOException e) {
                    Log.e(TAG, "failed to add split asset path " + splitResDir);
                    return null;
                }
            }
        }
        if (key.mOverlayDirs != null) {
            for (final String idmapPath : key.mOverlayDirs) {
                try {
                    builder.addApkAssets(loadApkAssets(idmapPath, false 
                } catch (IOException e) {
                    Log.w(TAG, "failed to add overlay path " + idmapPath);
                }
            }
        }
        if (key.mLibDirs != null) {
            for (final String libDir : key.mLibDirs) {
                if (libDir.endsWith(".apk")) {
                    try {
                        builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
                                false /*overlay*/));
                    } catch (IOException e) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        return builder.build();
    }

这里区分了好几种路径资源的加载,其实我们只关心build方法的处理

 public Builder addApkAssets(ApkAssets apkAssets) {
       mUserApkAssets.add(apkAssets);
         return this;
    }

  public AssetManager build() {
            // Retrieving the system ApkAssets forces their creation as well.
            final ApkAssets[] systemApkAssets = getSystem().getApkAssets();

            final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
            final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];

            System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);

            final int userApkAssetCount = mUserApkAssets.size();
            for (int i = 0; i < userApkAssetCount; i++) {
                apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
            }

            // Calling this constructor prevents creation of system ApkAssets, which we took care
            // of in this Builder.
            final AssetManager assetManager = new AssetManager(false /*sentinel*/);
            assetManager.mApkAssets = apkAssets;
            AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
                    false /*invalidateCaches*/);
            return assetManager;
        }

可以看出,会把内置的资源库和系统自带的资源库目录资源进行合并,得到最终的AssetManager并返回,那么我们的推论就是满足的了,

那么我们只要能反射这个Builder去构建一个新的AssetManager,那么就能得到我们的Resources

其他有更简单的方法

 public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }

    private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
        Preconditions.checkNotNull(path, "path");
        synchronized (this) {
            ensureOpenLocked();
            final int count = mApkAssets.length;
            for (int i = 0; i < count; i++) {
                if (mApkAssets[i].getAssetPath().equals(path)) {
                    return i + 1;
                }
            }
            final ApkAssets assets;
            try {
                if (overlay) {
                    final String idmapPath = "/data/resource-cache/"
                            + path.substring(1).replace('/', '@')
                            + "@idmap";
                    assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);
                } else {
                    assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);
                }
            } catch (IOException e) {
                return 0;
            }
            mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
            mApkAssets[count] = assets;
            nativeSetApkAssets(mObject, mApkAssets, true);
            invalidateCachesLocked(-1);
            return count + 1;
        }
    }

AssetManager本身提供了addAssetPath方法,通过这个方法可以给当前的AssetManager新增新的资源目录,代码和上面是一样的,但因为AssetManager本身并不提供公开的实例化方法,这个需要我们自主去反射

其中Resources的一个创建构造方法是这样

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

那么一个简单的创建插件资源的方式就出来了

fun initTargetResource():Resources {
        try {
            val constru = AssetManager::class.java.getDeclaredConstructor()
            val assetManger = constru.newInstance()
            val method =
                AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
            method.isAccessible = true
            method.invoke(assetManger, skinPath)
            skinResouce =
                Resources(assetManger, appResource!!.displayMetrics, appResource!!.configuration)
			return skinResouce
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
        return null
    }

那么资源问题已经解决了,后续的操作就涉及到View的加载处理问题了,这个将在下一篇中进行讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值