插件式换肤框架原理解析

news2024/11/19 9:19:45

作者: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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习_12_softmax_图片识别优化版代码

因为图片识别很多代码都包装在d2l库里了&#xff0c;直接调用就行了 完整代码&#xff1a; import torch from torch import nn from d2l import torch as d2l"获取训练集&获取检测集" batch_size 256 train_iter, test_iter d2l.load_data_fashion_mnist(ba…

计算机提示“找不到emp.dll,无法继续执行代码”,这几种解决办法都可以解决

在计算机使用过程中&#xff0c;我们可能会遇到各种问题&#xff0c;其中之一就是系统文件丢失。emp.dll文件是Windows操作系统中的一个重要组件&#xff0c;如果丢失或损坏&#xff0c;可能会导致系统运行不稳定甚至无法正常启动。本文将详细介绍emp.dll文件丢失恢复的4个方法…

基于SpringBoot+Vue的高校心理教育管理系统

基于SpringBootVue的高校心理教育管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 测试列表 测试结果 用户界面 管理员界面 摘要 本文设计并实现了一款…

OpenGL_Learn10(颜色)

1. 颜色 我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色&#xff0c;而是它所反射的(Reflected)颜色。换句话说&#xff0c;那些不能被物体所吸收(Absorb)的颜色&#xff08;被拒绝的颜色&#xff09;就是我们能够感知到的物体的颜色。例如&#xff0c;太阳光…

问卷调查表单、表设计

一、DWSurvey实现&#xff1a; 参考文档&#xff1a;快速入门 | 调问开源问卷系统 管理员通过拖拽题型生成表单&#xff0c; 点击保存&#xff0c;预览&#xff0c;发布问卷。用户根据预览的地址&#xff0c;填写问卷提交。管理员可以在我的问卷里看到答卷情况。 关于数据存…

Zigbee智能家居方案设计

背景 目前智能家居物联网中最流行的三种通信协议&#xff0c;Zigbee、WiFi以及BLE&#xff08;蓝牙&#xff09;。这三种协议各有各的优势和劣势。本方案基于CC2530芯片来设计&#xff0c;CC2530是TI的Zigbee芯片。 网关使用了ESP8266CC2530。 硬件实物 节点板子上带有继电器…

Word转PDF简单示例,分别在windows和centos中完成转换

概述 本篇博客以简单的示例代码分别在Windows和Linux环境下完成Word转PDF的文档转换。 文章提供SpringBoot Vue3的示例代码。 文章为什么要分为Windows和Linux环境&#xff1f; 因为在如下提供的Windows后端示例代码中使用documents4j库做转换&#xff0c;此库需要调用命令行…

学习网络编程No.9【应用层协议之HTTPS】

引言&#xff1a; 北京时间&#xff1a;2023/10/29/7:34&#xff0c;好久没有在周末早起了&#xff0c;该有的困意一点不少。伴随着学习内容的深入&#xff0c;知识点越来越多&#xff0c;并且对于爱好刨根问底的我来说&#xff0c;需要了解的知识就像一座大山&#xff0c;压得…

初始MySQL(五)(自我复制数据,合并查询,外连接,MySQL约束:主键,not null,unique,foreign key)

目录 表复制 自我复制数据(蠕虫复制) 合并查询 union all(不会去重) union(会自动去重) MySQL表的外连接 左连接 右连接 MySQL的约束 主键 not null unique(唯一) foreign key(外键) 表复制 自我复制数据(蠕虫复制) #为了对某个sql语句进行效率测试,我们需要海量…

APP备案获取安卓app证书公钥获取方法和签名MD5值

前言 在开发和发布安卓应用程序时&#xff0c;了解应用程序证书的公钥和签名MD5值是很重要的。这些信息对于应用程序的安全性和合规性至关重要。现在又因为今年开始APP必须接入备案才能在国内各大应用市场上架&#xff0c;所以获取这两个值成了所有开发者的必经之路。本文将介…

Django路由层

路由层&#xff08;urls&#xff09; Django的路由层是负责将用户请求映射到相应的视图函数的一层。在Django的MVT架构中&#xff0c;路由层负责处理用户的请求&#xff0c;然后将请求交给相应的视图函数进行处理&#xff0c;最后将处理结果返回给用户。 在Django中&#xff0c…

【LIUNX】配置缓存DNS服务

配置缓存DNS服务 A.安装bind bind-utils1.尝试修改named.conf配置文件2.测试nslookup B.修改named.conf配置文件1.配置文件2.再次测试 缓存DNS服务器&#xff1a;只提供域名解析结果的缓存功能&#xff0c;目的在于提高数据查询速度和效率&#xff0c;但是没有自己控制的区域地…

大模型深入发展,数字化基础设施走向“算粒+电粒”,双粒协同

AI大模型爆发&#xff0c;千行百业期待用生成式人工智能挖掘创新应用与提升生产力。不过&#xff0c;高效的大模型应用底层实际需要更灵活、多元的算力去支撑。在这个重要的技术窗口下&#xff0c;11月10日&#xff0c;由中国智能计算产业联盟与ACM中国高性能计算专家委员会共同…

Redis应用之二分布式锁

一、前言 前一篇 Redis应用之一自增编号 我们主要介绍了使用INCR命令来生成不重复的编号&#xff0c;今天我们来了解Redis另外一个命令SET NX的用途&#xff0c;对于单体应用我们可以简单使用像synchronized这样的关键字来给代码块加锁&#xff0c;但对于分布式应用要实现锁机…

使用内网穿透实现U8用友ERP本地部署与异地访问

文章目录 前言1. 服务器本机安装U8并调试设置2. 用友U8借助cpolar实现企业远程办公2.1 在被控端电脑上&#xff0c;点击开始菜单栏&#xff0c;打开设置——系统2.2 找到远程桌面2.3 启用远程桌面 3. 安装cpolar内网穿透3.1 注册cpolar账号3.2 下载cpolar客户端 4. 获取远程桌面…

No193.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

什么是代理模式,用 Python 如何实现 Proxy(代理 或 Surrogate)对象结构型模式?

什么是代理模式&#xff1f; 代理&#xff08;Proxy&#xff09;是一种结构型设计模式&#xff0c;其目的是通过引入一个代理对象来控制对另一个对象的访问。代理对象充当目标对象的接口&#xff0c;这样客户端就可以通过代理对象间接地访问目标对象&#xff0c;从而在访问过程…

MyBatis 反射工具箱:带你领略不一样的反射设计思路

反射是 Java 世界中非常强大、非常灵活的一种机制。在面向对象的 Java 语言中&#xff0c;我们只能按照 public、private 等关键字的规范去访问一个 Java 对象的属性和方法&#xff0c;但反射机制可以让我们在运行时拿到任何 Java 对象的属性或方法。 有人说反射打破了类的封装…

基于php+thinkphp的网上书店购物商城系统

运行环境 开发语言&#xff1a;PHP 数据库:MYSQL数据库 应用服务:apache服务器 使用框架:ThinkPHPvue 开发工具:VScode/Dreamweaver/PhpStorm等均可 项目简介 系统主要分为管理员和用户二部分&#xff0c;管理员主要功能包括&#xff1a;首页、个人中心、用户管理、图书分类…

第28章_mysql缓存策略

文章目录 MySQL缓存方案目的分析缓存层作用举例 缓存方案选择场景分析 提升MySQL访问性能的方式MySQL主从复制读写分离连接池异步连接 缓存方案缓存和MySQL一致性状态分析制定读写策略 同步方案canalgo-mysql-transfer 缓存方案的故障问题及解决缓存穿透缓存击穿缓存雪崩缓存方…