插件式换肤框架原理解析

作者:ak

插件换肤实现原理概述

  • 收集到需要换肤的控件
  • 确定控件中需要换肤的属性和资源ID
  • 加载插件APK,构造AssetManager并生成插件的Resource类,就可以加载插件包中的资源
  • 执行换肤:通过ID加载插件包中的资源,然后再通过控件的属性的set方法改变属性即可

要解决的问题:

1、怎样去获取皮肤包中的资源?

2、怎么确定当前页面中有哪些资源要进行替换?

一、加载插件资源

通过插件包,构造AssetManager并生成插件的Resources类,

PackageManager packageManager = mContext.getPackageManager();
//检索插件包信息
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
//拿到插件包的包名
mSkinPackageName = packageInfo.packageName;
//构造 AssetManager 类
AssetManager assetManager = AssetManager.class.newInstance();
//反射调用AssetManager的setApkAssets方法来设置路径
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, path);
//创建插件包的资源对象,管理资源包里面的资源
mResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
        mContext.getResources().getConfiguration());

拿到了插件的Resources对象,就可以去加载插件包中的资源。

二、确定换肤控件及属性

怎么确定当前客户端中有哪些资源要进行替换?
既然我们的布局和属性都写在 XML 文件中, 是不是可以通过 XML 文件中的属性来确定哪些控件需要进行换肤; 从而收集需要换肤的View,并找到那些需要更改的属性。

换肤框架一般是在activity加载View的时候使用LayoutInflater.Factory2来截获View的加载过程,然后记录Activity的每一个View需要调整的属性,也就是保存那些 需要换肤的控件 和识别 需要换肤的属性.

1、LayoutInflater源码解读

1.1、XML的解析过程

我们在页面上能够看到控件,都是通过View对象绘制出来的;而写在XML布局文件中的控件之所以能够被我们看见,它肯定是经过了 对XML文件进行解析,然后转化为View对象

那么页面和布局文件是如何关联起来的呢?

最为关键的一句就是setContentView() ;整个转化的过程都是在这里面
这里是AppcompatActivity,我们点进去看

调用了getDelegete()setContentView()

这里的delegete是AppcompatDelegete,点进去可以看到AppcompatDelegete它是一个抽象类,所以这里getDelegete()拿到的肯定是它的子类

而它的唯一实现类是AppcompatDelegateImpl,所以我们点到AppcompatDelegateImpl.setContentView()方法
这里第一行首先初始化了DecorView,DecorView是整个Activity中最顶级的View,我们知道Activity组件是用来管理Window的,我们在屏幕上看到的Activity页面就是它所持有的Window,而Window的唯一实现类是PhoneWindow,PhoneWindow中有一个最顶层的View,这个View就是DecorView

接着通过findViewById拿到了android.R.id.content,这个content就是我们根布局,后面会把我们的布局添加到根布局里面。

这里调用了LayoutInflaterinflate方法,并且把xml和根布局传了进去; 接着往下走

首先,得到了我们的Resources对象,然后通过Resource对象,初始化了XML解析器
Ok,在这里得到我们的XML解析器之后,它又调用了一个 inflate() 方法。 传入了XML解析器、根布局,来解析我们的XML,把它加载到我们的contentView里面。

点进去之后我们看最关键的,这里去创建了一个Temp,而在下面把这个Temp View给添加到了contentView里面,也就是添加到我们的android.R.id.content里面。 那么就可以知道,这里的temp View 其实就是在XML中找到的根布局,就也是XML里的第一个View。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {

        ......省略部分代码......

        View result = root;
           try {
           //第一个节点的名字,也就是 xml 中的根视图
            final String name = parser.getName();
            
            "节点一:创建XML中的根布局View"
            //Temp is the root view that was found in the xml
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);

            ViewGroup.LayoutParams params = null;

            if (root != null) {
                if (DEBUG) {
                    System.out.println("Creating params from root: " +
                            root);
                }
                // Create layout params that match root, if supplied
                params = root.generateLayoutParams(attrs);
                if (!attachToRoot) {
                    // Set the layout params for temp if we are not
                    // attaching. (If we are, we use addView, below)
                    temp.setLayoutParams(params);
                }
            }

            if (DEBUG) {
                System.out.println("-----> start inflating children");
            }

            "节点二:创建XML根布局内部的子View"
            // Inflate all children under temp against its context.
            rInflateChildren(parser, temp, attrs, true);

            if (DEBUG) {
                System.out.println("-----> done inflating children");
            }

            "我们找到所有子View附加到根布局temp后"
            // 就可以把根部局temp添加到 【android.R.id.content】中
            if (root != null && attachToRoot) {
                root.addView(temp, params);
            }

            // Decide whether to return the root that was passed in or the
            // top view found in xml.
            if (root == null || !attachToRoot) {
                result = temp;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        
        ......省略部分代码......

        return result;
    }
}

节点一:创建XML中的根布局View

createViewFromTag(root, name, inflaterContext, attrs);
根据解析的name创建View并返回

节点二:创建XML根布局内部的子View

rInflateChildren(parser, temp, attrs, true);

使用递归的方式调用createViewFromTag方法完成子View的加载

接下来我们看createViewFromTag()是如何创建View的

点进去看到返回值是View,我们去找一下View是在哪里返回的,以及它是在哪里创建的。

可以看View的创建和return都在这一块,首先通过tryCreateView()尝试创建View,如果创建失败了View为空,那么才会进到下面这一段默认逻辑去创建

我们先来看tryCreateView()方法:

public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }

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

    return view;
}

尝试用3个Factory来创建View,如果创建成功了就直接返回View; 如果创建失败返回null,则通过前面那一段默认逻辑去创建。

我们可以利用这一点,通过设置自己的Factory来收集到需要换肤的控件。

默认逻辑部分:

try {
    View view = tryCreateView(parent, name, context, attrs);

    if (view == null) {
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('.')) {  //判断`name`中是否包含小数点,
                view = onCreateView(context, parent, name, attrs);
            } else {
                view = createView(context, name, null, attrs);
            }
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }

    return view;
}
  • 如果name中没有.小数点,则认为它是android.view包下的控件,走oncreateView方法,此时会在name前面拼接上android.view.的包名

  • 如果包含.小数点,则认为是全包名路径,不需要拼接前缀。

最终使用构建出来的全包名路径,通过反射得到类的构造方法来创建实例对象。

@UnsupportedAppUsage
static final Class<?>[] mConstructorSignature = new Class[] {
        Context.class, AttributeSet.class};

public final View createView(@android.annotation.NonNull Context viewContext, @android.annotation.NonNull String name,
                             @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException {
                              ......省略部分代码......
    //从缓存中获取构造方法                         
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) { //校验
        constructor = null;
        sConstructorMap.remove(name);
    }
    Class<? extends View> clazz = null;

    if (constructor == null) {  //如果缓存中不存在
        // Class not found in the cache, see if it's real, and try to add it
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                mContext.getClassLoader()).asSubclass(View.class);

        if (mFilter != null && clazz != null) {
            boolean allowed = mFilter.onLoadClass(clazz);
            if (!allowed) {
                failNotAllowed(name, prefix, viewContext, attrs);
            }
        }
        //获得两个参数的构造方法
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);   //缓存构造方法
    }
    
     ......省略部分代码......

    mConstructorArgs[0] = viewContext; //构建参数Context和AttributeSet
    Object[] args = mConstructorArgs;
    args[1] = attrs;

    final View view = constructor.newInstance(args);
    return view;
}

到这里原理就介绍完了,我们可以自定义一个Factory继承自Factory2,去实现一个换肤插件式换肤框架了,关键在于实现Factory2onCreateView方法来收集属性。

题外

另外一种情况是原生主题切换换肤中,有些页面为了避免重走生命周期配置了android:configChanges="uiMode"
此时切换黑白模式时,一般是在onConfigurationChanged()回调中重新设置属性。
这时候我们其实可以写一个工具类来完成重设操作:通过解析XML,将属性查询设置给对应的View.

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/140926.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C# 并发编程

C# 并发编程 前言 对于现在很多编程语言来说&#xff0c;多线程已经得到了很好的支持&#xff0c; 以至于我们写多线程程序简单&#xff0c;但是一旦遇到并发产生的问题就会各种尝试。 因为不是明白为什么会产生并发问题&#xff0c;并发问题的根本原因是什么。 接下来就让…

基于ssm课堂考勤系统-计算机毕设 附源码 50546

ssm课堂考勤系统 摘 要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&am…

Go的优雅退出

Go优雅退出/停机以前主要通过signal来实现&#xff0c;当然现在也是通过signal来实现&#xff0c;只是从go 1.16开始&#xff0c;新增了更加友好的API: func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) 该…

北邮22级信通院数电:Verilog-FPGA(9)第九周实验(2)实现下降沿触发的JK触发器(带异步复位和置位功能)

北邮22信通一枚~ 跟随课程进度更新北邮信通院数字系统设计的笔记、代码和文章 持续关注作者 迎接数电实验学习~ 获取更多文章&#xff0c;请访问专栏&#xff1a; 北邮22级信通院数电实验_青山如墨雨如画的博客-CSDN博客 JK.v module JK (input clk,input J,input K,input…

【C++类和对象下:解锁面向对象编程的奇妙世界】

【本节目标】 1. 再谈构造函数 2. Static成员 3. 友元 4. 内部类 5.匿名对象 6.拷贝对象时的一些编译器优化 7. 再次理解封装 1. 再谈构造函数 1.1 构造函数体赋值 在创建对象时&#xff0c;编译器通过调用构造函数&#xff0c;给对象中各个成员变量一个合适的初始值。…

CRM系统的销售管理功能,你了解多少?

很多时候&#xff0c;CRM系统直接被当做是销售管理系统&#xff0c;其实无可厚非&#xff0c;因为CRM系统本身是围绕销售逻辑来实现的&#xff0c;不仅可以有效的优化商机&#xff0c;对商机的把握也是十分精确。下面我们就来详细说说&#xff0c;CRM系统都有哪些销售管理功能&…

2023年数维杯国际大学生数学建模挑战赛A题

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 cs数模团队在数维杯前为大家提供了许多资料的内容呀&#xff0…

2023.11.13【读书笔记】丨生物信息学与功能基因组学(第六章 多重序列比对 下)

目录 6.4 多重序列比对数据库6.5 基因组区域的多重序列比对6.6 展望6.7 常见问题总结 6.4 多重序列比对数据库 Pfam&#xff1a;基于谱隐马尔可夫模型构建的蛋白质家族数据库 SMART&#xff1a;简易分子构型研究工具&#xff0c;与细胞信号传导、细胞外结构域以及染色质功能…

炸裂!Sklearn 的 10 个宝藏级使用方法!

大家好&#xff0c;本次给大家介绍10个Sklearn方法&#xff0c;比较小众但非常好用。 1️.FunctionTransformer 虽然Sklearn中有很多内置的预处理操作可以放进pipeline管道&#xff0c;但很多时候并不能满足我们的需求。 如果是比较简单并且通过一个函数可以实现需求的情况&…

CSDN每日一题学习训练——Java版(字符串相乘、子集、删除链表的倒数第 N 个结点)

版本说明 当前版本号[20231112]。 版本修改说明20231112初版 目录 文章目录 版本说明目录字符串相乘题目解题思路代码思路补充说明参考代码 子集题目解题思路代码思路参考代码 删除链表的倒数第 N 个结点题目解题思路代码思路参考代码 字符串相乘 题目 给定两个以字符串形…

面向切面:AOP

面向切面&#xff1a;AOP 大家好&#xff0c;今天本篇博客我们来了解Spring里边的另一个重要部分&#xff0c;叫做AOP&#xff0c;也就是我们说的面向切面编程。 1、场景模拟 首先第一部分&#xff0c;咱们做一个场景模拟。我们先写一个简单的例子&#xff0c;然后通过例子引…

探索向量数据库 | 重新定义数据存储与分析

随着大模型带来的应用需求提升&#xff0c;最近以来多家海外知名向量数据库创业企业传出融资喜讯。 随着AI时代的到来&#xff0c;向量数据库市场空间巨大&#xff0c;目前处于从0-1阶段&#xff0c;预测到2030年&#xff0c;全球向量数据库市场规模有望达到500亿美元&#xff…

CSDN每日一题学习训练——Java版(数据流的中位数、乘积最大子数组、旋转链表)

版本说明 当前版本号[20231113]。 版本修改说明20231113初版 目录 文章目录 版本说明目录数据流的中位数题目解题思路代码思路参考代码 乘积最大子数组题目解题思路代码思路参考代码 旋转链表题目解题思路代码思路参考代码 数据流的中位数 题目 中位数是有序列表中间的数。…

DevChat:开发者专属的基于IDE插件化编程协助工具

DevChat&#xff1a;开发者专属的基于IDE插件化编程协助工具 一、DevChat 的介绍1.1 DevChat 简介1.2 DevChat 优势 二、DevChat 在 VSCode 上的使用2.1 安装 DevChat2.2 注册 DevChat2.3 使用 DevChat 三、DevChat 的实战四、总结 一、DevChat 的介绍 在AI浪潮的席卷下&#x…

国际化:i18n

什么是国际化&#xff1f; 国际化也称作i18n&#xff0c;其来源是英文单词 internationalization的首末字符和n&#xff0c;18为中间的字符数。由于软件发行可能面向多个国家&#xff0c;对于不同国家的用户&#xff0c;软件显示不同语言的过程就是国际化。通常来讲&#xff0…

【BMC】jsnbd介绍

jsnbd介绍 本文主要介绍一个名为jsnbd的开源项目&#xff0c;位于GitHub - openbmc/jsnbd&#xff0c;它实现了一个前端&#xff08;包含HTML和JS文件&#xff09;页面&#xff0c;作为存储服务器&#xff0c;可以指定存储内容&#xff1b;还包含一个后端的代理&#xff0c;这…

【chatglm3】(3):在AutoDL上,使用4090显卡,部署ChatGLM3API服务,并微调AdvertiseGen数据集,完成微调并测试成功!附视频演示。

在AutoDL上&#xff0c;使用4090显卡&#xff0c;部署ChatGLM3API服务&#xff0c;并微调AdvertiseGen数据集&#xff0c;完成微调并测试成功&#xff01; 其他chatgpt 和chatglm3 资料&#xff1a; https://blog.csdn.net/freewebsys/category_12270092.html 视频地址&#…

【C++入门篇】保姆级教程篇【下】

目录 一、运算符重载 1&#xff09;比较、赋值运算符重载 2&#xff09; 流插入留提取运算符重载 二、剩下的默认成员函数 1&#xff09;赋值运算符重载 2&#xff09;const成员函数 3&#xff09;取地址及const取地址操作符重载 三、再谈构造函数 1&#xff09;初始化列表 …