Android源碼之DeskClock(三) Proxy/Delegate Application 架構應用,androiddeskclock
一.概述 當項目有加殼子,外掛程式化或熱修複等需求的時候,可以使用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