结合代码看更好理解: https://github.com/zcwfeng/zcw_android_demo/tree/master/android_skin
换肤思路:
我们需要解决的几个问题
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<我们要更换的View>
List<(View 的属性)>
LIst<Paie(属性,view)>
3. 获取资源,通过SkinManager 加载apk 资源
4. 执行更换,通过记录的List<View> 循环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布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
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" />
<FrameLayout
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" />
</LinearLayout>
如果我们的布局文件选定,就会调用
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);