Android 換膚的思路

結(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);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容