天天看點

插件化換膚

1、引言

    1.插件化的特點

       應用在運作的時候通過加載一些本地不存在的可執行檔案實作一些特定的功能;Android中動态加載的核心思想是動态調用外部的 dex檔案。

    2.插件化需要解決的問題

      插件包中代碼和資源的加載問題

   3.插件化必備基礎

     ClassLoader類加載器 、Java反射 、插件資源通路 、代理模式

2、例子

   分析一個插件式換膚的源碼來了解插件資源通路。

     源碼下載下傳位址:https://github.com/ximsfei/Android-skin-support

3、源碼分析

  3.1 LayoutInflater簡介

       LayoutInflater 的作用就是将XML布局檔案執行個體化為相應的 View 對象,需要通過Activity.getLayoutInflater() 或 Context.getSystemService(Class) 來擷取與目前Context已經關聯且正确配置的标準LayoutInflater。

        過程如下:

  • 通過 XML 的 Pull 解析方式擷取 View 的标簽。
  • 通過标簽以反射的方式來建立 View 對象。
  • 如果是 ViewGroup 的話則會對子 View 周遊并重複以上步驟,然後 add 到父 View 中。
  • 與之相關的幾個方法:inflate ——》 rInflate ——》 createViewFromTag ——》 createView。
/**

* createViewFromTag 方法比較簡單,首先嘗試通過 Factory 來建立View;

* 如果沒有 Factory 的話則通過 createView 來建立View;

**/

ViewcreateViewFromTag(Viewparent, String name, Context context, AttributeSet attrs,

        boolean ignoreThemeAttr) {

        ...

    try {

        Viewview;

        if (mFactory2 != null) {

//有mFactory2,則調用mFactory2的onCreateView方法

            view = mFactory2.onCreateView(parent, name, context, attrs);

        } else if (mFactory != null) {

// 有mFactory,則調用mFactory的onCreateView方法

            view = mFactory.onCreateView(name, context, attrs);

        } else {

            view = null;

        }



        if (view == null && mPrivateFactory != null) {

//有mPrivateFactory,則調用mPrivateFactory的onCreateView方法

            view = mPrivateFactory.onCreateView(parent, name, context, attrs);

        }



        if (view == null) {

// ④ 走到這步說明三個Factory都沒有,則開始自己建立View

            final Object lastContext = mConstructorArgs[0];

            mConstructorArgs[0] = context;

            try {

                if (-1 == name.indexOf('.')) {

// ⑤ 如果View的name中不包含 '.' 則說明是系統控件,會在接下來的調用鍊在name前面加上 'android.view.'

                    view = onCreateView(parent, name, attrs);

                } else {

// ⑥ 如果name中包含 '.' 則直接調用createView方法,onCreateView 後續也是調用了createView

                    view = createView(name, null, attrs);

                }

            } finally {

                mConstructorArgs[0] = lastContext;

            }

        }



        return view;

    } 

   ...

}
           

3.2 LayoutInflater.Factory

  簡介:通過 LayoutInflater 建立View時候的一個回調,可以通過LayoutInflater.Factory來改造 XML 中存在的 tag。

public interface Factory2 extends Factory {

    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);

}
           

設定setFactory方法:

  • Activity的onCreate裡的super.onCreate之前調用setFactory方法。
  • 利用反射修改LayoutInflater裡面的mFactorySet變量。
  • private void installLayoutFactory(Context context) {
    
        LayoutInflater layoutInflater = LayoutInflater.from(context);
    
        try {
    
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
    
            field.setAccessible(true);
    
            field.setBoolean(layoutInflater, false);
    
            LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
    
        } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
    
            e.printStackTrace();
    
        }
    
    }
               

3.3 擷取插件包中的資源

      方法一:

/**

* 擷取皮膚包資源{@link Resources}.

*

* @param skinPkgPath sdcard中皮膚包路徑.

* @return

*/

@Nullable

public Resources getSkinResources(String skinPkgPath) {

    try {

        PackageInfo packageInfo = mAppContext.getPackageManager().getPackageArchiveInfo(skinPkgPath, 0);

        packageInfo.applicationInfo.sourceDir = skinPkgPath;

        packageInfo.applicationInfo.publicSourceDir = skinPkgPath;

        Resources res = mAppContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);

        Resources superRes = mAppContext.getResources();

        return new Resources(res.getAssets(), superRes.getDisplayMetrics(), superRes.getConfiguration());

    } catch (Exception e) {

        e.printStackTrace();

    }

    return null;

}
           

方法二:

/**

* @return 得到對應插件的Resource對象

*/

@Nullable

public Resources getSkinResources(String skinPkgPath) {

    try {

        AssetManager assetManager = AssetManager.class.newInstance();

        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);

        addAssetPath.invoke(assetManager, skinPkgPath);

        Resources superRes = mAppContext.getResources();

        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),

                superRes.getConfiguration());

        return mResources;

    } catch (Exception e) {

        e.printStackTrace();

    }

    return null;

}
           

3.4換膚

源碼中可以通過攔截View建立過程, 替換一些基礎的元件(比如TextView -> AppCompatTextView), 然後對一些特殊的屬性(比如:background, textColor) 做處理, 那我們為什麼不能将這種思想拿到換膚架構中來使用呢?我擦,一語驚醒夢中人啊,老哥.我們也可以搞一個委托啊,我們也可以搞一個類似于AppCompatViewInflater的控件加載器啊,我們也可以設定mFactory2啊,相當于建立View的過程由我們接手.

/**

* 初始化換膚架構. 通過該方法初始化,應用中Activity需繼承自{@link skin.support.app.SkinCompatActivity}.

*

* @param context

* @return

*/

public static SkinCompatManager init(Context context) {

    if (sInstance == null) {

        synchronized (SkinCompatManager.class) {

            if (sInstance == null) {

                sInstance = new SkinCompatManager(context);

            }

        }

    }

    SkinPreference.init(context);

    return sInstance;

}



public static SkinCompatManager getInstance() {

    return sInstance;

}
           
/**

* 初始化換膚架構,監聽Activity生命周期. 通過該方法初始化,應用中Activity無需繼承{@link skin.support.app.SkinCompatActivity}.

*

* @param application 應用Application.

* @return

*/

public static SkinCompatManager withoutActivity(Application application) {

    init(application);

    SkinActivityLifecycle.init(application);

    return sInstance;

}



public static SkinActivityLifecycle init(Application application) {

    if (sInstance == null) {

        synchronized (SkinActivityLifecycle.class) {

            if (sInstance == null) {

                sInstance = new SkinActivityLifecycle(application);

            }

        }

    }

    return sInstance;

}



private SkinActivityLifecycle(Application application) {

    application.registerActivityLifecycleCallbacks(this);

    installLayoutFactory(application);

    SkinCompatManager.getInstance().addObserver(getObserver(application));

}
           

SkinCompatDelegate實作LayoutInflaterFactory,這裡hook:

@Override

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

    View view = createView(parent, name, context, attrs);



    if (view == null) {

        return null;

    }

    if (view instanceof SkinCompatSupportable) {

        mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));

    }



    return view;

}



public View createView(View parent, final String name, @NonNull Context context,

                       @NonNull AttributeSet attrs) {

    if (mSkinCompatViewInflater == null) {

        mSkinCompatViewInflater = new SkinCompatViewInflater();

    }

    ...

    return mSkinCompatViewInflater.createView(parent, name, context, attrs);

}



public static SkinCompatDelegate create(Context context) {

    return new SkinCompatDelegate(context);

}
           

SkinCompatViewInflater透傳到各個子控件的Inflater處理,自定義子控件的Inflater是在Application裡面設定的。

public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {

    view = createViewFromInflater(context, name, attrs);



    if (view == null) {

        view = createViewFromTag(context, name, attrs);

    }



    if (view != null) {

        // If we have created a view, check it's android:onClick

        checkOnClickListener(view, attrs);

    }



    return view;

}



private View createViewFromInflater(Context context, String name, AttributeSet attrs) {

    View view = null;

    for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {

        view = inflater.createView(context, name, attrs);

        if (view == null) {

            continue;

        } else {

            break;

        }

    }

    return view;

}
           

注:為什麼要在SkinCompatViewInflater還要細化,還需要交由更細的SkinLayoutInflater來處理呢?

   —-友善擴充,庫中給出了幾個SkinLayoutInflater,有SkinAppCompatViewInflater(基礎控件建構器)、SkinMaterialViewInflater(material design控件構造器)、SkinConstraintViewInflater(ConstraintLayout建構器)、SkinCardViewInflater(CardView v7建構器)。

SkinAppCompatViewInflater :基礎元件的Inflater

@Override

public View createView(Context context, String name, AttributeSet attrs) {

    View view = createViewFromFV(context, name, attrs);



    if (view == null) {

        view = createViewFromV7(context, name, attrs);

    }

    return view;

}



private View createViewFromFV(Context context, String name, AttributeSet attrs) {

    View view = null;

    if (name.contains(".")) {

        return null;

    }

    switch (name) {

        case "View":

            view = new SkinCompatView(context, attrs);

            break;

        case "LinearLayout":

            view = new SkinCompatLinearLayout(context, attrs);

            break;

        case "RelativeLayout":

            view = new SkinCompatRelativeLayout(context, attrs);

            break;

        case "FrameLayout":

            view = new SkinCompatFrameLayout(context, attrs);

            break;

        case "TextView":

            view = new SkinCompatTextView(context, attrs);

            break;

        case "ImageView":

            view = new SkinCompatImageView(context, attrs);

            break;

        case "Button":

            view = new SkinCompatButton(context, attrs);

            break;

        case "EditText":

            view = new SkinCompatEditText(context, attrs);

            break;

        case "Spinner":

            view = new SkinCompatSpinner(context, attrs);

            break;

        case "ImageButton":

            view = new SkinCompatImageButton(context, attrs);

            break;

        case "CheckBox":

            view = new SkinCompatCheckBox(context, attrs);

            break;

        case "RadioButton":

            view = new SkinCompatRadioButton(context, attrs);

            break;

        case "RadioGroup":

            view = new SkinCompatRadioGroup(context, attrs);

            break;

        case "CheckedTextView":

            view = new SkinCompatCheckedTextView(context, attrs);

            break;

        case "AutoCompleteTextView":

            view = new SkinCompatAutoCompleteTextView(context, attrs);

            break;

        case "MultiAutoCompleteTextView":

            view = new SkinCompatMultiAutoCompleteTextView(context, attrs);

            break;

        case "RatingBar":

            view = new SkinCompatRatingBar(context, attrs);

            break;

        case "SeekBar":

            view = new SkinCompatSeekBar(context, attrs);

            break;

        case "ProgressBar":

            view = new SkinCompatProgressBar(context, attrs);

            break;

        case "ScrollView":

            view = new SkinCompatScrollView(context, attrs);

            break;

        default:

            break;

    }

    return view;

}
           

AppCompatActivity的實作,将background相關的屬性交給SkinCompatBackgroundHelper去處理,将textColor相關的操作交給SkinCompatTextHelper去處理。該換膚源碼也是這樣設計的。并且每個支援換膚的View實作SkinCompatSupportable接口,比如:

public class SkinCompatView extends View implements SkinCompatSupportable {

    private SkinCompatBackgroundHelper mBackgroundTintHelper;



    public SkinCompatView(Context context) {

        this(context, null);

    }



    public SkinCompatView(Context context, AttributeSet attrs) {

        this(context, attrs, 0);

    }



    public SkinCompatView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);

        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);



    }



    @Override

    public void setBackgroundResource(int resId) {

        super.setBackgroundResource(resId);

        if (mBackgroundTintHelper != null) {

            mBackgroundTintHelper.onSetBackgroundResource(resId);

        }

    }



    @Override

    public void applySkin() {

        if (mBackgroundTintHelper != null) {

            mBackgroundTintHelper.applySkin();

        }

    }

}



public interface SkinCompatSupportable {

    void applySkin();

}
           

3.5總結:

  • 監聽APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
  • 在每個Activity的onCreate()方法調用時setFactory(),設定建立View的工廠.将建立View的瑣事交給SkinCompatViewInflater去處理.
  • 庫中自己重寫了系統的控件(比如View對應于庫中的SkinCompatView),實作換膚接口(接口裡面隻有一個applySkin()方法),表示該控件是支援換膚的.并且将這些控件在建立之後收集起來,友善随時換膚.
  • 在庫中自己寫的控件裡面去解析出一些特殊的屬性(比如:background, textColor),并将其儲存起來
  • 在切換皮膚的時候,周遊一次之前緩存的View,調用其實作的接口方法applySkin(),在applySkin()中從皮膚資源(可以是從網絡或者本地擷取皮膚包)中擷取資源.擷取資源後設定其控件的background或textColor等,就可實作換膚.

(完)