Android源碼之DeskClock (三) Proxy/Delegate Application 框架應用

一.概述

       當項目有加殼子,插件化或熱修復等需求的時候,可以使用Proxy/Delegate Application框架的方式,在正常的模式中,一個程序一般只有一個Application入口,而Proxy/Delegate模式中需要有兩個Application,原程序的Application改爲Delegate Application,再新加一個Proxy Application,由Proxy Application 提供一系列的個性化定製,再將所有的context和context相關的引用全部轉化爲Delegate Application的實例,讓外界包括Delegate Application自身都以爲該App的Application入口就是Delegate Application.

二.實例

1.Proxy/Delegate 之前
       這裏就在Android 4.4原生的DeskClock程序上應用Proxy/Delegate框架爲示例
       原生的DeskClock程序沒有自定義Application,這裏先定義一個,並print該程序目前ApplicationContext的名字(在DeskClock中使用的Log是自定義的)
/**
 * Created by jesse on 15-7-17.
 */
public class MyApplication extends Application{
    private final String TAG = MyApplication.class.getSimpleName();

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG + ", onCreate " + this.getApplicationContext().getClass().getSimpleName());
    }
}
       並且在DeskClock的入口Activity,DeskClock處也print出該程序目前ApplicationContext的名字用於後續Proxy後的對比.
       Application的Manifest配置是
    <application android:name="cn.jesse.MyApplication"
                 android:label="@string/app_label"
                 android:icon="@mipmap/ic_launcher_alarmclock"
                 android:requiredForAllUsers="true"
                 android:supportsRtl="true">
       過濾後的運行Log: 簡單的流程就是先啓動自定義MyApplication 之後再launch DeskClock,同時都打印出來ApplicationContext的名字


2.使用Proxy/Delegate框架之後
       使用Proxy/Delegate框架,需要重新構建出來一個新的ProxyApplication,用來做代理Application,原先的MyApplication的作用爲DelegateApplication
       所以Manifest的配置需要更改,app的主入口更改爲MyProxyApplication,把DelegateApplication的信息以meta-data子元素的形式存儲(當然也可以用其他的方式)
    <application android:name="cn.jesse.MyProxyApplication"
                 android:label="@string/app_label"
                 android:icon="@mipmap/ic_launcher_alarmclock"
                 android:requiredForAllUsers="true"
                 android:supportsRtl="true">
        <meta-data
            android:name="DELEGATE_APPLICATION_CLASS_NAME"
            android:value="cn.jesse.MyApplication" >
        </meta-data>
    </application>
       定義一個抽象類,提供一個用於替換當前ProxyApplication 的ClassLoader成父類的ClassLoader的抽象方法(或者一些其他的個性化定製)
 * Created by jesse on 15-7-17.
 */
public abstract class ProxyApplication extends Application{
    protected abstract void initProxyApplication();
}
       當我們要替換當前ProxyApplication的ClassLoader爲父類的ClassLoader,所以這個替換的動作要足夠得早(要保證在app Context最早被構建的入口處替換ClassLoader),要不然就會出現替換不乾淨的情況,就會有程序中大部分使用的DelegateApplication的ClassLoader,而一小部分是使用的ProxyApplication的ClassLoader,這樣可能會出現一些意想不到的bug.
       通常來說在Application的OnCreate中來做替換就足夠了,但是當app有註冊ContentProvider的時候ContentProvider:OnCreate的調用是在Application:OnCreate之前的,所以我們必須保證替換ClassLoader的動作要在ContentProvider之前.
       通過查看源碼可以看到Application是繼承自ContextWrapper,而在ContextWrapper中系統在構建完成完善的Context之後第一次回調是通過attachBaseContext方法,既然這樣就通過在ProxyApplication中複寫該方法來獲取剛出爐熱噴噴的Context來轉換ClassLoader.
    /**
     * Set the base context for this ContextWrapper.  All calls will then be
     * delegated to the base context.  Throws
     * IllegalStateException if a base context has already been set.
     * 
     * @param base The new base context for this wrapper.
     */
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
       轉換ClassLoader的入口也確定之後就可以自定義一個MyProxyApplication,繼承自ProxyApplication並且複寫attachBaseContext方法,print相關信息
/**
 * Created by jesse on 15-7-17.
 */
public class MyProxyApplication extends ProxyApplication {
    private final String TAG = MyProxyApplication.class.getSimpleName();
    private Context mContext;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Log.i(TAG + ", attachBaseContext");
        mContext = base;
        this.initProxyApplication();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG + ", onCreate" + this.getApplicationContext().getClass().getSimpleName());
        BootLoader.boot(mContext);
    }

    @Override
    protected void initProxyApplication() {
        Log.i(TAG + ", initProxyApplication");
        BootLoader.resetClassLoader(mContext);
    }
}
       Log運行的順序,先進入attachBaseContext->initProxyApplication->onCreate->DeskClock:onCreate (這裏DeskClock的onCreate獲取到的ApplicationContext的名字是(MyProxyApplication)

       入口的順序沒問題了之後,就可以在initProxyApplication方法中替換當前的ClassLoader到父類的ClassLoader,並且在MyProxyApplication的onCreate中將應用層所有的Application的引用全部從ProxyApplication替換成MyApplication(當前在DeskClock程序中沒有替換ClassLoader的需求,只需要替換所有的Application的引用就能達到代理的效果,所以在initProxyApplication方法處就寫了一個空方法帶過).
       先從AndroidManifest配置文件中的metadata拿到DelegateApplication的屬性
            String className = CLASS_NAME;
            ApplicationInfo appInfo = getPackageManager().getApplicationInfo(super.getPackageName(), PackageManager.GET_META_DATA);
            Bundle bundle = appInfo.metaData;
            if (bundle != null && bundle.containsKey(KEY)) {
                className = bundle.getString(KEY);
                if (className.startsWith("."))
                    className = super.getPackageName() + className;
            }
       根據className反射得到MyApplication,創建MyApplication實例並且取得MyProxyApplication的實例
            Class delegateClass = Class.forName(className, true, getClassLoader());
            Application delegate = (Application) delegateClass.newInstance();
            Application proxyApplication = (Application)getApplicationContext();
       使用反射更換MyProxyApplication context成員中的mOuterContext屬性
            Class contextImplClass = Class.forName("android.app.ContextImpl");
            Field mOuterContext = contextImplClass.getDeclaredField("mOuterContext");
            mOuterContext.setAccessible(true);
            mOuterContext.set(mContext, delegate);
       獲取MyProxyApplication Context的PackageInfo對象,替換掉其中的mApplication屬性
            Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
            mPackageInfoField.setAccessible(true);
            Object mPackageInfo = mPackageInfoField.get(mContext);

            Class loadedApkClass = Class.forName("android.app.LoadedApk");
            Field mApplication = loadedApkClass.getDeclaredField("mApplication");
            mApplication.setAccessible(true);
            mApplication.set(mPackageInfo, delegate);
       再根據之前反射得到的packageInfo對象獲取到mActivityThread屬性,替換掉其中的mInitialApplication屬性
            Class activityThreadClass = Class.forName("android.app.ActivityThread");
            Field mAcitivityThreadField = loadedApkClass.getDeclaredField("mActivityThread");
            mAcitivityThreadField.setAccessible(true);
            Object mActivityThread = mAcitivityThreadField.get(mPackageInfo);

            Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
            mInitialApplicationField.setAccessible(true);
            mInitialApplicationField.set(mActivityThread, delegate);
       拿着之前的mActivityThread對象獲取到mAllApplications屬性,注意該屬性是一個list,這裏就移除MyProxyApplication添加DelegateApplication,至此應用層MyProxyApplication的Context的引用全部都替換成了MyApplication的引用.
            Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
            mAllApplicationsField.setAccessible(true);
            ArrayList<Application> al = (ArrayList<Application>)mAllApplicationsField.get(mActivityThread);
            al.add(delegate);
            al.remove(proxyApplication);
       給MyApplication通過反射和attach內部方法設置baseContext,並調用MyApplication的onCreate方法完成DelegateApplication的初始化.
            Method attach = Application.class.getDeclaredMethod("attach", Context.class);
            attach.setAccessible(true);
            attach.invoke(delegate, mContext);
            delegate.onCreate();
       完成這些步驟之後再重新運行查看Log,觀察DeskClock處獲取的ApplicationContext的名字已經變成MyApplication.

       但是這樣還沒有完全結束,還記得開頭說的ContentProvider嗎?他的構造是在Application的onCreate之前的,那麼ContentProvider部分有沒有需要替換的Context引用呢?從framework/base/core/java/android/app下可以找到ActivityThread.java從其中裝載ContentProvider的部分可以看到,如果當前Context的包名和ProviderInfo的包名一樣的話,ContentProvider就會引用當前的MyProxyApplication的Context.由於當前的MyProxyApplication只是做代理啓動用的,所以在MyProxyApplication處複寫getPackageName並且返回空就可以避免ContentProvider複用當前Context了.
    private IActivityManager.ContentProviderHolder installProvider(Context context,
            IActivityManager.ContentProviderHolder holder, ProviderInfo info,
            boolean noisy, boolean noReleaseNeeded, boolean stable) {
        ContentProvider localProvider = null;
        IContentProvider provider;
        if (holder == null || holder.provider == null) {
            if (DEBUG_PROVIDER || noisy) {
                Slog.d(TAG, "Loading provider " + info.authority + ": "
                        + info.name);
            }
            Context c = null;
            ApplicationInfo ai = info.applicationInfo;
            if (context.getPackageName().equals(ai.packageName)) {
                c = context;
            } else if (mInitialApplication != null &&
                    mInitialApplication.getPackageName().equals(ai.packageName)) {
                c = mInitialApplication;
            } else {
                try {
                    c = context.createPackageContext(ai.packageName,
                            Context.CONTEXT_INCLUDE_CODE);
                } catch (PackageManager.NameNotFoundException e) {
                    // Ignore
                }
            }

三.總結

       這篇只是先簡單的走了下Proxy/Delegate框架的流程,這個框架其實是有很多使用場景的,例如多dex動態加載,插件化,線上程序熱修復bug等可以靈活使用出很多有趣的技術,有時間的話還會再發一篇以Proxy/Delegate實現的線上程序熱修復bug的博客.

轉載請註明出處:http://blog.csdn.net/l2show/article/details/46914881

發佈了66 篇原創文章 · 獲贊 15 · 訪問量 78萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章