插件化换肤方案
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了插件化换肤方案相关的知识,希望对你有一定的参考价值。
参考技术A 插件化换肤,需要考虑两个核心的问题。第一、如何收集到所有需要换肤的View,因为我们需要在换肤的时机中调用这些View的setBackGround、setColor等方法,所以我们需要再收集到这些需要换肤的属性,一个View可能对应多个需要修改的属性。第二个、皮肤包来自于哪里,如何获取到皮肤包里的资源针对第一个问题,我们要考虑的是怎么找到一个合适的Hook点,帮我们收集到所有的View。我们都知道,在Activity中,通过setContentView()方法设置布局。那么 猜测大概率能从这个方法中找到合适的Hook点
View的加载流程整体如上图所示,省去了些细节。setContentView最终会通过PhoneWindow去执行,在PhoneWindow的setContetView方法中,主要做两件事。首先判断DecorView有没有被生成,如果没有,会去解析Activity的样式属性,选择合适的主题布局文件进行加载,每个主题布局中有个id为content的Layout,作为我们自己页面布局的父容器。
当DecorView相关的已经初始化好之后,接下来会调用LayoutInflater对象的inflater方法开始解析xml布局文件。在这个方法中,会使用XmlResourceParser对象对XML格式文件进行解析。这种解析方法 是根据Tag 即<> 节点进行解析,所以第一次解析到的是父布局。接着会调用rInflateChilder方法 去解析子Viewe。直到所有VIew都被解析出来。我们重点看一下 createViewFromTag()是如何将XML解析成View对象的。
在这个方法中,首先会判断有没有mFactory2 和mFactory,如果有会调用他们的onCreateView方法。默认没有设置的情况,这两个对象是空的,那就会走自己的onCreateView方法。在这里会解析节点名,判断是系统控件还是自定义的控件。如果是系统控件,我们需要拼上它的全类名。所以这里很明显了,是通过反射生成View对象的。
到这里我们可以注意点,如果我们自定义一个Factory2,重写它的onCreateView方法,那在这个方法中 就能收集到所有的View了。
这里我们使用的皮肤包本质上就是一个apk,只要皮肤包的资源名称和我们源app中一致即可。我们如果想使用资源,一般是通过getResource().getXXX()方法获取drawable、color等等。所以我们想使用皮肤包的资源,是不是也需要一个皮肤包的Resource,那么这个Resource到底是什么,怎么产生,我们需要看一下源码
通过源码可以看到 通过ResourceManager类,首先生成一个ResourceImpl对象,在这个对象中又会引用一个AssetManager对象,这个对象调用native方法,去加载指定路径下的资源。所以我们可以反射一个AssetManager对象,调它的addAseetPath方法,传入皮肤包的路径,再生成一个Resource。通过这个Resouce我们就可以拿到皮肤包的资源了
Activity布局流程+资源加载过程+插件化换肤思路
Activity布局流程
Activity框架
1.Activity里有一个window,在初始化的时候是空的,然后在activity.attach()里赋值为PhoneWindow
2.PhoneWindow里有一个DecorView,这个DecorView就是一个FrameLayout,是整个Activity的根布局
3.当新建Activity时选择不同的主题会有不同的根布局
4.比如选择的是空白的Activity,那就是分为两部分ViewStub预留和FrameLayout 这个FrameLayout就是root
5.自己写的布局就是挂在View temp下面
第4.5步:generateLayout()加载主题布局
ActivityThread.java
先从ActivityThread.java这个类开始看
- performLaunchActivity(翻译 执行启动Activity)
在performLaunchActivity方法中
2. 先建Context 根据createBaseContextForActivity
3. 根据Context获取ClassLoader getClassLoader()
4. 根据ClassLoader 新建出一个Activity newActivity
补充:mInstrumentation(翻译 仪器)
这里存了activity的基本信息,创建activity啥的都在这
- 建了一个Window,并将它与activity关联
- 将Window传入了activity.attach()
- 在activity.attach()里,其实是将window传入,new 了一个 PhoneWindow
- 最后是通过callActivityOnCreate 将activity初始化并执行
PhoneWindow.java
在ActivityThread.java的performLaunchActivity方法中,是创建activity的主流程,其中建了一个Window,并将它与activity关联,而这个Window又传入了activity.attach(),new了一个PhoneWindow.java
-
先看setContentView(int layoutResID)
-
setContentView里重要的就两步:
①installDecor() 初始化DecorView
②mLayoutInflater.inflate(layoutResID, mContentParent)(加载XML布局)
installDecor()
- 初始化DecorView,这里的mContentParent其实就是DecorView
- 将DecorView传入generateLayout()生成布局
1.1 generateDecor里就是直接return了new DecorView
- onResourcesLoaded加载资源
3.1 onResourcesLoaded()方法里,其实调用了inflater.inflate()来生成View,然后addView添加到父布局下面
mLayoutInflater.inflate() - LayoutInflater.java
- LayoutInflater.inflate()就是用来加载xml的
- View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)三个参数分别是 XML解析器、父布局的类型、是否要添加到父布局下面(详情看第四步)
attachToRoot 用到的地方
- 查看inflate方法,重点方法 createViewFromTag()创建View。
3.1 查看 createViewFromTag() 方法,如果有mFactory、mFactory2、mPrivateFactory就走Factory的onCreateView,不然就走系统的onCreateView
重点中的重点!!! 因为这里是这样构造view的,所以可以通过自定义的Factory或者Factory2,来使构造View的代码走我们自己的,这样就可以实现 -- 插件化换肤
还有一种方法就是直接给LayoutInflate.java这一整个类都给替换了 -- 插件化换肤
3.2 系统的onCreateView其实是根据反射来创建的
①根据name创建constructor 构造器(之前见过的先从HashMap的缓存里取,没取到才自己创建)
②通过构造器创建View View view = constructor.newInstance(args);
③补充:因为View是通过反射constructor.newInstance(args)创建的所以这时候其实View是空的,在xml里写的各种属性都是没有的
①反射获取constructor
② constructor.newInstance(args); 构建View
③ 补充这时候xml的属性都是空的
- 因为第三步,View创建是通过反射的,所以现在xml里自己写的属性还是没有的,所以得通过获取 LayoutParams在将属性设置给View
- View有了(反射),属性也设置了,也添加到布局树下面了 root.addView(temp, params),所以view就构建完事了
资源加载流程
刚刚上面的全都是Activity的加载流程,下面看资源的加载流程
Resoureces的结构图
1.ResourcesManager管理着一个Resources类
2.Resources类里有他的实现类ResourcesImpl,各种创建,调用,getColor等方法都是在实现类里实现的
3.ResourcesImpl里管理着一个AssetManager
4.AssetManager负责从apk里获取资源,写入资源等 addAssetPath()
resources.arsc这个文件就是apk中存资源的(字典表),addAssetPath()取的就是他的值
handleBindApplication()–ActivityThread.java
- 起点是ActivityThread.java handleBindApplication()方法 ,在这加载Application的
- 先new了一个Instrumentation(翻译仪表),这个类是用来加载Activity的
- 生产了makeApplication,然后调用了Application.onCreate();
makeApplication()-- LoadedApk.java
先看Application的生产过程
- 先创建了上下文createAppContext
- 通过反射ClassLoader.loadClass(className).newInstance()创建Application
- 第一步创建上下文createAppContext,点进去
重要方法context.setResources(packageInfo.getResources()); 根据LoadedApk获取资源
- 点进LoadedApk.getResources(),发现在ResourcesManager包里。具体里面方法是getOrCreateResources获取或者创建资源
- 点进getOrCreateResources方法,发现是ResourcesManager.java里的方法。他先从缓存
WeakReference<ResourcesImpl>
里取,取不到再创建资源
- 看createResourcesImpl()创建资源,是通过AssetManager创建的
- createAssetManager()方法里,调用了
addApkAssets()重点
方法,这个方法是native方法,作用是给一个路径,用来加载apk里的资源在android27 里 叫addAssetPath()
插件化换肤思路
根据上面最后第7步:
1.apk里所有的资源都是通过/resources.arsc 这个表格,然后去找具体apk的资源的。
2.而Resoures实际是通过AssetManager来资源加载的,外面的都是一层包装
3.AssetManager重点方法根据资源id找包名、类型、资源等等。根据name获取资源
插件化换肤思路:
1.插件apk跟宿主apk有同名的资源
2.取宿主apk的资源名,去插件apk里找
3.将插件apk的值替换掉宿主apk的值
具体代码
完整的思路:
1.收集xml数据,根据View创建过程的Factory2(源码里拷贝过来就行)需要修改的地方就是View创建完事以后,将需要修改的属性及他的View记录下来(比如要改color、src、backgrand)
2.记录的类型大概这样,所有要换的属性->List<View> -> List<View中要换的属性>
3.读取皮肤包里的内容。
先通过assets.addAssetPath()加载进来,这样就能通过assetManager来获取皮肤包里的资源了
4.如果遇到了需要替换的属性(color、src、backgrand等)那就替换,通过assetManager里的方法
具体代码:
4具体设置,需要改的view的属性,替换成皮肤包的里的id
最后
小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接点击下方小卡片进行访问查阅。
以上是关于插件化换肤方案的主要内容,如果未能解决你的问题,请参考以下文章
Android 手写实现插件化换肤 兼容Android10 Android11