結(jié)合代碼看更好理解: https://github.com/zcwfeng/zcw_android_demo/tree/master/android_skin
換膚思路:
我們需要解決的幾個問題
1.什么時候換膚?
xml加載前換膚,如果xml加載后換膚,用戶將會看見換膚之前的色彩,用戶體驗不好。
2.皮膚是什么?
皮膚就是apk,是一個資源包,包含了顏色、圖片等。
3.什么樣的控件應(yīng)該進行換膚?
包含背景圖片的控件,例如textView文字顏色。
4.皮膚與已安裝的資源如何匹配?
資源名字匹配
思路解析
首先換膚的基本思想是更換資源索引和路徑(圖片,顏色值,背景等等),需要注意就是規(guī)劃好,比如顏色值 要提出來到color.xml 中,不要寫死成“#xxxxxx”
我們制作一個皮膚插件包,這個就由一個資源apk來承載,用到了系統(tǒng)application初始化時候的原理,會在LoadApk的時候加載Resources通過AssertManager進行資源的加載
實現(xiàn)這個就需要在布局Xml加載之前setContentView()替換掉,這樣用戶體驗比較好(之后替換也是可以但是會不自然發(fā)生切換)
根據(jù)源碼:需要在自己代碼setContentView 之前,自己實現(xiàn)一個SkinFactory extends Factory2 。 并且把這個SkinFactory 設(shè)置
類似,LayoutInflaterCompat.setFactory(getLayoutInflater(),skinFactory);
這里需要注意,setFactory 之后會有一個標記 為 true,我們需要用反射吧這個true改成false,來避免只能設(shè)置一次
整體思路:
用一個SkinFactoryManager 管理類處理資源管理過濾。我們需要對要更換的view進行掃描記錄,過濾和更換對應(yīng)的屬性和資源的路徑等。
1. 設(shè)置自己實現(xiàn)Factory2 的SkinFactory
2.我們需要 用一個數(shù)據(jù)結(jié)構(gòu)記錄下,我們應(yīng)該換膚的View,然后過濾View的屬性進行換膚替換
List<我們要更換的View>
List<(View 的屬性)>
LIst<Paie(屬性,view)>
3. 獲取資源,通過SkinManager 加載apk 資源
4. 執(zhí)行更換,通過記錄的List<View> 循環(huán)View 設(shè)置顏色,背景等屬性
原理分析
四個方面去分析原理
1.UI布局流程分析
2.LayoutInflate原理
3.Android資源加載流程
4 .插件化換膚原理分析
UI 布局流程分析
一般情況下我們會有這兩個入口
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvp);
ActivityThread------>main函數(shù) 是app啟動過程,這個過程先忽略,
我們?nèi)肟趶?code>performLaunchActivity開始
->「起始點」
我們跟蹤到方法臨時變量 window = r.mPendingRemoveWindow
r.mPendingRemoveWindow 初始值 null
繼續(xù)搜索r.mPendingRemoveWindow = 找到賦值點
r.mPendingRemoveWindow = r.window;
繼續(xù)搜索 r.window 賦值點,為null的不用看
r.window = r.activity.getWindow();—————就是Activity上的屬性mWindow
目前位置看回到起始點,我們的賦值為空,只是拿到Activity的屬性mWindow的引用
隨后我們會調(diào)用activity.attach
Activity————mWindow = new PhoneWindow(this, window, activityConfigCallback);
「總結(jié)點1:--------------- 層次Activity---> PhoneWindow」
上面的setContentView 就是在PhoneWindow中實現(xiàn)的,所以看下PhoneWindow的源代碼
installDecor();———其實就是 mDecor = new DecorView 中間做了寫操作
查看 mDector 就是PhoneWindow的成員
「總結(jié)點2: 層次 Activity——>PhoneWindow——->DecorView」
installDector—> generateLayout
看到源碼注釋: Inflate the window decor 解析decor
layoutResource 會有各種R.layout.xxxxx 這些其實都是AS中的模板等,還有一些其他的
去Framework 源碼搜索,screen_simplev 看下他的結(jié)構(gòu)
-> 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>
如果我們的布局文件選定,就會調(diào)用
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)
DectorVIew 就會調(diào)用 final View root = inflater.inflate(layoutResource, null);解析剛剛的布局
再通過addView的方式把他加到DectorView
「總結(jié)點3: 層次 Activity——>PhoneWindow——->DecorView()---> 我們的content的布局xml」
PhoneWindow 再次 inflate ———> mLayoutInflater.inflate(layoutResID, mContentParent);
而這個mContentParent 就是我們的DectorView
回到 PhoneWindow 創(chuàng)建generateLayout-》mContentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
content 就是我們布局中的content。 所以 DectorView—mContentParent—FrameLayout
最終調(diào)用了LayouInflater->inflate 方法
根據(jù)Tag創(chuàng)建臨時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 我們的參數(shù)就會通過xml 獲取屬性寫進去 如果true 就需要我們addView 手動將布局參數(shù)填寫進去
LayoutInflater繼續(xù)往下看createViewFromTag ——> createView()
看到是通過根據(jù)view name進行反射構(gòu)造方法。
繼續(xù)看tryCreateView 在onCreateVIew 之前
會有三個工廠,F(xiàn)actory2(包含父類的),F(xiàn)actory(無相關(guān)父類),F(xiàn)actory2 privateFactory. 如果工廠不為null后面的onCreateView就會被攔截
——————Hook點
所以換膚的思路,一個是 實現(xiàn)Factory,對 onCrateView 提前進行攔截
第二個思路,重寫Inflate(會有一定的侵入性質(zhì))
資源加載的原理
apk 包 resource.asrc 二進制文件信息
-> 入口:handleBindApplication
看到一個關(guān)鍵信息
mInstrumentation = new Instrumentation(); 創(chuàng)建了一個儀表盤
初始化我們的Application
new ContextImpl
然后獲取我們的資源
LoadedApk->getResources-> getOrCreateResources——> createResourcesImpl(ResourceImpl)
->final AssetManager assets = createAssetManager(key);
總結(jié)點1:加載的層次調(diào)用
LoadedAPK
Resources
AssertManager
根據(jù)mInstrumentation 信息調(diào)用Application初始化
mInstrumentation.callApplicationOnCreate(app);