换肤思路:

我们需要解决的几个问题

1.什么时候换肤?

xml加载前换肤,如果xml加载后换肤,用户将会看见换肤之前的色彩,用户体验不好。

2.皮肤是什么?

皮肤就是apk,是一个资源包,包含了颜色、图片等。

3.什么样的控件应该进行换肤?

包含背景图片的控件,例如textView文字颜色。

4.皮肤与已安装的资源如何匹配?

资源名字匹配

思路解析

首先换肤的基本思想是更换资源索引和路径(图片,颜色值,背景等等),需要注意就是规划好,比如颜色值 要提出来到color.xml 中,不要写死成“#xxxxxx”

我们制作一个皮肤插件包,这个就由一个资源apk来承载,用到了系统application初始化时候的原理,会在LoadApk的时候加载Resources通过AssertManager进行资源的加载

实现这个就需要在布局Xml加载之前setContentView()替换掉,这样用户体验比较好(之后替换也是可以但是会不自然发生切换)

根据源码:需要在自己代码setContentView 之前,自己实现一个SkinFactory extends Factory2 。 并且把这个SkinFactory 设置

类似,LayoutInflaterCompat.setFactory(getLayoutInflater(),skinFactory);

这里需要注意,setFactory 之后会有一个标记 为 true,我们需要用反射吧这个true改成false,来避免只能设置一次

整体思路:

用一个SkinFactoryManager 管理类处理资源管理过滤。我们需要对要更换的view进行扫描记录,过滤和更换对应的属性和资源的路径等。

1. 设置自己实现Factory2 的SkinFactory

2.我们需要 用一个数据结构记录下,我们应该换肤的View,然后过滤View的属性进行换肤替换

List

List

LIst

3. 获取资源,通过SkinManager 加载apk 资源

4. 执行更换,通过记录的List 循环View 设置颜色,背景等属性

原理分析

四个方面去分析原理

1.UI布局流程分析

2.LayoutInflate原理

3.Android资源加载流程

4 .插件化换肤原理分析

UI 布局流程分析

一般情况下我们会有这两个入口

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_mvp);

ActivityThread------>main函数 是app启动过程,这个过程先忽略,

我们入口从performLaunchActivity开始

->「起始点」

我们跟踪到方法临时变量 window = r.mPendingRemoveWindow

r.mPendingRemoveWindow 初始值 null

继续搜索r.mPendingRemoveWindow = 找到赋值点

r.mPendingRemoveWindow = r.window;

继续搜索 r.window 赋值点,为null的不用看

r.window = r.activity.getWindow();—————就是Activity上的属性mWindow

目前位置看回到起始点,我们的赋值为空,只是拿到Activity的属性mWindow的引用

随后我们会调用activity.attach

Activity————mWindow = new PhoneWindow(this, window, activityConfigCallback);

「总结点1:--------------- 层次Activity---> PhoneWindow」

上面的setContentView 就是在PhoneWindow中实现的,所以看下PhoneWindow的源代码

installDecor();———其实就是 mDecor = new DecorView 中间做了写操作

查看 mDector 就是PhoneWindow的成员

「总结点2: 层次 Activity——>PhoneWindow——->DecorView」

installDector—> generateLayout

看到源码注释: Inflate the window decor 解析decor

layoutResource 会有各种R.layout.xxxxx 这些其实都是AS中的模板等,还有一些其他的

去Framework 源码搜索,screen_simplev 看下他的结构

-> screen_simple.xml 这就是我们的root布局文件

android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />

如果我们的布局文件选定,就会调用

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)

DectorVIew 就会调用 final View root = inflater.inflate(layoutResource, null);解析刚刚的布局

再通过addView的方式把他加到DectorView

「总结点3: 层次 Activity——>PhoneWindow——->DecorView()---> 我们的content的布局xml」

PhoneWindow 再次 inflate ———> mLayoutInflater.inflate(layoutResID, mContentParent);

而这个mContentParent 就是我们的DectorView

回到 PhoneWindow 创建generateLayout-》mContentParent

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

content 就是我们布局中的content。 所以 DectorView—mContentParent—FrameLayout

最终调用了LayouInflater->inflate 方法

根据Tag创建临时temp

final View temp = createViewFromTag(root, name, inflaterContext, attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}

判断如果attchToRoot = false 我们的参数就会通过xml 获取属性写进去 如果true 就需要我们addView 手动将布局参数填写进去

LayoutInflater继续往下看createViewFromTag ——> createView()

看到是通过根据view name进行反射构造方法。

继续看tryCreateView 在onCreateVIew 之前

会有三个工厂,Factory2(包含父类的),Factory(无相关父类),Factory2 privateFactory. 如果工厂不为null后面的onCreateView就会被拦截

——————Hook点

所以换肤的思路,一个是 实现Factory,对 onCrateView 提前进行拦截

第二个思路,重写Inflate(会有一定的侵入性质)

资源加载的原理

apk 包 resource.asrc 二进制文件信息

-> 入口:handleBindApplication

看到一个关键信息

mInstrumentation = new Instrumentation(); 创建了一个仪表盘

初始化我们的Application

new ContextImpl

然后获取我们的资源

LoadedApk->getResources-> getOrCreateResources——> createResourcesImpl(ResourceImpl)

->final AssetManager assets = createAssetManager(key);

总结点1:加载的层次调用

LoadedAPK

Resources

AssertManager

根据mInstrumentation 信息调用Application初始化

mInstrumentation.callApplicationOnCreate(app);