插件化换肤原理——黑夜模式(深色模式)实现

关于插件化换肤原理,我在之前的文章 Android插件化原理(三)——加载插件资源已经详细分析过原理,但是再次回顾,突然想起,好像没有做深色模式(昼夜模式)的适配介绍,现在补充以下

1.插件化换肤原理回顾

关于插件化换肤的原理,我们已经熟知,在ActivityThread.performLaunchActivity()中,创建Activity,并将真正管理资源的AssetManager包装到克隆出的Resourcese中,然后将Resources注入到Activity;

而资源加载的真正实现在AssetManager中:

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

    private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
        Objects.requireNonNull(path, "path");
        synchronized (this) {
            ensureOpenLocked();
            final int count = mApkAssets.length;

            // See if we already have it loaded.
            for (int i = 0; i < count; i++) {
                if (mApkAssets[i].getAssetPath().equals(path)) {
                    return i + 1;
                }
            }

            final ApkAssets assets;
            try {
                if (overlay) {
                    // TODO(b/70343104): This hardcoded path will be removed once
                    // addAssetPathInternal is deleted.
                    final String idmapPath = "/data/resource-cache/"
                            + path.substring(1).replace('/', '@')
                            + "@idmap";
                    assets = ApkAssets.loadOverlayFromPath(idmapPath, 0 /* flags */);
                } else {
                    assets = ApkAssets.loadFromPath(path,
                            appAsLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
                }
            } catch (IOException e) {
                return 0;
            }

            mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
            mApkAssets[count] = assets;
            nativeSetApkAssets(mObject, mApkAssets, true);
            invalidateCachesLocked(-1);
            return count + 1;
        }
    }


原理就是AssetManager根据path加载ApkAsets,然后缓存到内部的ApkAsets数组中管理,这里就是真正的资源所在了,然后我们就可以使用Resources了。

2.插件化换肤应用

上文讲到了如何加载皮肤插件资源,但是这个类是系统级别的,并未开放给开发者,所以我们需要使用反射:


    public static Resources loadResources(Context context, String pluginAbsolutePath) {
        try {
            Log.i(TAG, "loadResources: start");
            final File file = new File(pluginAbsolutePath);
            if (!file.exists()) {
                return null;
            }
            //创建加载插件的AssetManager
            final AssetManager assetManager = AssetManager.class.newInstance();
            final Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            //加载插件的资源
            addAssetPathMethod.invoke(assetManager, pluginAbsolutePath);
            //获取宿主的资源
            final Resources resources = context.getResources();
            final Resources realRes = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
            return realRes;

        } catch (IllegalAccessException | InstantiationException | NoSuchMethodException |
                 InvocationTargetException e) {
            e.printStackTrace();
            Log.e(TAG, "loadResources: ", e);
        } finally {
            Log.i(TAG, "loadResources: end");
        }

        return null;
    }

使用这个方法加载插件资源,然后我们就可以使用插件的资源了;

3.系统深色模式实现

众所周知,系统有一个功能叫黑夜模式,那么黑夜模式是如何实现的呢?我们的插件化换肤是如何适配的呢?

3.1原理分析

我们知道,黑夜模式发生变化时,Activity.onConfigurationChanged(newConfig: Configuration)会被调用,view也是在这个时刻做资源的更新,那么我们就一步步看一下系统是如何实现的

我们看AppCompatActivity.onConfigurationChanged()


    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        if (mResources != null) {
            // The real (and thus managed) resources object was already updated
            // by ResourcesManager, so pull the current metrics from there.
            final DisplayMetrics newMetrics = super.getResources().getDisplayMetrics();
            mResources.updateConfiguration(newConfig, newMetrics);
        }

        getDelegate().onConfigurationChanged(newConfig);
    }


是不是眼前一亮。

有的人可能不解,为什么uiMode变化会回调这里,configuration里面包含多项配置,比如屏幕方向orientation,分辨率densityDpi,黑夜模式uiMode等等,这些都属与屏幕的配置,所以任何一项发生变化,都会回调这里;而资源是和屏幕有关联关系的,所以会在这里更新资源;

继续往下看Resources:


    @Deprecated
    public void updateConfiguration(Configuration config, DisplayMetrics metrics) {
        updateConfiguration(config, metrics, null);
    }

    /**
     * @hide
     */
    public void updateConfiguration(Configuration config, DisplayMetrics metrics,
                                    CompatibilityInfo compat) {
        mResourcesImpl.updateConfiguration(config, metrics, compat);
    }


ResourcesImpl


 public void updateConfiguration(Configuration config, DisplayMetrics metrics,
                                    CompatibilityInfo compat) {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration");
        try {
            synchronized (mAccessLock) {
                if (compat != null) {
                    mDisplayAdjustments.setCompatibilityInfo(compat);
                }
                if (metrics != null) {
                    mMetrics.setTo(metrics);
                }
               
                mDisplayAdjustments.getCompatibilityInfo().applyToDisplayMetrics(mMetrics);

                //-比较configurarion不同之处
                final @Config int configChanges = calcConfigChanges(config);

                // If even after the update there are no Locales set, grab the default locales.
                LocaleList locales = mConfiguration.getLocales();
                if (locales.isEmpty()) {
                    locales = LocaleList.getDefault();
                    mConfiguration.setLocales(locales);
                }
                //- 应用新的LOCALE
                if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
                    if (locales.size() > 1) {
                        // The LocaleList has changed. We must query the AssetManager's available
                        // Locales and figure out the best matching Locale in the new LocaleList.
                        String[] availableLocales = mAssets.getNonSystemLocales();
                        if (LocaleList.isPseudoLocalesOnly(availableLocales)) {
                            // No app defined locales, so grab the system locales.
                            availableLocales = mAssets.getLocales();
                            if (LocaleList.isPseudoLocalesOnly(availableLocales)) {
                                availableLocales = null;
                            }
                        }

                        if (availableLocales != null) {
                            final Locale bestLocale = locales.getFirstMatchWithEnglishSupported(
                                    availableLocales);
                            if (bestLocale != null && bestLocale != locales.get(0)) {
                                mConfiguration.setLocales(new LocaleList(bestLocale, locales));
                            }
                        }
                    }
                }
                //应用新的densityDpi
                if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
                    mMetrics.densityDpi = mConfiguration.densityDpi;
                    mMetrics.density =
                            mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
                }

                // Protect against an unset fontScale.
                mMetrics.scaledDensity = mMetrics.density *
                        (mConfiguration.fontScale != 0 ? mConfiguration.fontScale : 1.0f);

       			//...省略部分不重要代码



       			//重点更新资源
                mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
                        adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()),
                        mConfiguration.orientation,
                        mConfiguration.touchscreen,
                        mConfiguration.densityDpi, mConfiguration.keyboard,
                        keyboardHidden, mConfiguration.navigation, width, height,
                        mConfiguration.smallestScreenWidthDp,
                        mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
                        mConfiguration.screenLayout, mConfiguration.uiMode,
                        mConfiguration.colorMode, Build.VERSION.RESOURCES_SDK_INT);

           		//更新缓存中的资源

                mDrawableCache.onConfigurationChange(configChanges);
                mColorDrawableCache.onConfigurationChange(configChanges);
                mComplexColorCache.onConfigurationChange(configChanges);
                mAnimatorCache.onConfigurationChange(configChanges);
                mStateListAnimatorCache.onConfigurationChange(configChanges);

                
            }
         	//...

        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }


可以看到,这里做了几项工作:

  • 将configuration内的配置项逐一比对应用;
  • 更新资源的配置mAssets.setConfiguration
  • 更新缓存中的资源(其实是删除了缓存)

继续看AssetManager.setConfiguration


    public void setConfiguration(int mcc, int mnc, @Nullable String locale, int orientation,
            int touchscreen, int density, int keyboard, int keyboardHidden, int navigation,
            int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp,
            int screenHeightDp, int screenLayout, int uiMode, int colorMode, int majorVersion) {
        synchronized (this) {
            ensureValidLocked();
            nativeSetConfiguration(mObject, mcc, mnc, locale, orientation, touchscreen, density,
                    keyboard, keyboardHidden, navigation, screenWidth, screenHeight,
                    smallestScreenWidthDp, screenWidthDp, screenHeightDp, screenLayout, uiMode,
                    colorMode, majorVersion);
        }
    }

然后就到了native层面。。。

后面在native层面可能进行了宇宙大爆炸,奥特曼打怪兽,春暖花开秋风落叶…最终,uiMode就切换完成了,然后,我们再获取资源的时候,就获取到了对应uiMode的资源。

3.2原理应用

👏👏👏

好的,我们已经掌握了系统resources uiMode切换的原理,然后就来应用一下:


    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        Resources currentRes =  ThemeManager.INSTANCE.getCurrentRes();
        if (currentRes != null) {
            if (currentRes.getConfiguration() != newConfig || currentRes.getDisplayMetrics() != getResources().getDisplayMetrics()) {
                currentRes.updateConfiguration(newConfig,getResources().getDisplayMetrics());
            }
        }
    }

原理就是这么简单(但是如何触发uiMode变更,具体怎么获取更新的参数,其实也有其它的方式,读者可以自行思考)

这样,就可以了!

  • 使用

    private final IThemeChangeListener listener = resources -> {
        updateTheme();
    };

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ThemeManager.INSTANCE.addOnThemeChangeListener(listener);
    }

    private void updateTheme() {
        AvatrResources currentRes = ThemeManager.INSTANCE.getCurrentRes();
        mBinding.tvTitle.setText(currentRes.getString(R.string.theme_name));
        mBinding.ivTheme.setImageDrawable(currentRes.getDrawable(R.drawable.theme));
        int color = currentRes.getColor(R.color.theme_color);
        mBinding.tvTitle.setTextColor(color);
    }


总结:虽然原理简单,但是重在不断的思考,不断地学习总结,技术才能一点点沉淀积累,最终能够成长。

最后,祝诸君技术有成,财源广进!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值