1. 程式人生 > >Android動態載入Activity原理

Android動態載入Activity原理

activity的啟動流程

載入一個Activity肯定不會像載入一般的類那樣,因為activity作為系統的元件有自己的生命週期,有系統的很多回調控制,所以自定義一個DexClassLoader類載入器來載入外掛中的Activity肯定是不可以的。

首先不得不瞭解一下activity的啟動流程,當然只是簡單的看一下,太詳細的話很難研究清楚。

通過startActivity啟動後,最終通過AMS進行跨程序回撥到ApplicationThread的scheduleLaunchActivity,這時會建立一個ActivityClientRecord物件,這個物件表示一個Acticity以及他的相關資訊,比如activityInfo欄位包括了啟動模式等,還有loadedApk,顧名思義指的是載入過了的APK,他會被放在一個Map中,應用包名到LoadedApk的鍵值對,包含了一個應用的相關資訊。然後通過Handler切換到主執行緒執performLaunchActivity

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ActivityInfo aInfo = r.activityInfo;
    // 1.建立ActivityClientRecord物件時沒有對他的packageInfo賦值,所以它是null
    if (r.packageInfo == null) {
        r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE);
    }
    // ...
    Activity activity = null;
    try {
    	// 2.非常重要!!這個ClassLoader保存於LoadedApk物件中,它是用來載入我們寫的activity的載入器
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        // 3.用載入器來載入activity類,這個會根據不同的intent載入匹配的activity
        activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
    	// 4.這裡的異常也是非常非常重要的!!!後面就根據這個提示找到突破口。。。
        if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
    }
        if (activity != null) {
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config = new Configuration(mCompatConfiguration);
            // 從這裡就會執行到我們通常看到的activity的生命週期的onCreate裡面
            mInstrumentation.callActivityOnCreate(activity, r.state);
            // 省略的是根據不同的狀態執行生命週期
        }
        r.paused = true;
        mActivities.put(r.token, r);
    } catch (SuperNotCalledException e) {
        throw e;
    } catch (Exception e) {
    	// ...
    }
    return activity;
}

1.getPackageInfo方法最終返回一個LoadedApk物件,它會從一個HashMap的資料結構中取,mPackages維護了包名和LoadedApk的對應關係,即每一個應用有一個鍵值對對應。如果為null,就新建立一個LoadedApk物件,並將其新增到Map中,重點是這個物件的ClassLoader欄位為null!

    public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
            int flags) {
    	// 為true
        boolean includeCode = (flags&Context.CONTEXT_INCLUDE_CODE) != 0;
        boolean securityViolation = includeCode && ai.uid != 0
                && ai.uid != Process.SYSTEM_UID && (mBoundApplication != null
                        ? !UserHandle.isSameApp(ai.uid, mBoundApplication.appInfo.uid)
                        : true);
		// ...
        // includeCode為true
        // classloader為null!!!
        return getPackageInfo(ai, compatInfo, null, securityViolation, includeCode);
    }

    private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
            ClassLoader baseLoader, boolean securityViolation, boolean includeCode) {
        synchronized (mPackages) {
            WeakReference<LoadedApk> ref;
            if (includeCode) {
            	// includeCode為true
                ref = mPackages.get(aInfo.packageName);
            } else {
                ref = mResourcePackages.get(aInfo.packageName);
            }
            LoadedApk packageInfo = ref != null ? ref.get() : null;
            if (packageInfo == null || (packageInfo.mResources != null && !packageInfo.mResources.getAssets().isUpToDate())) {
                if (localLOGV) // ...
                // packageInfo為null,建立一個LoadedApk,並且新增到mPackages裡面
                packageInfo = new LoadedApk(this, aInfo, compatInfo, this, baseLoader, securityViolation, includeCode &&
                            (aInfo.flags&ApplicationInfo. ) != 0);
                if (includeCode) {
                    mPackages.put(aInfo.packageName, new WeakReference<LoadedApk>(packageInfo));
                } else {
                    mResourcePackages.put(aInfo.packageName, new WeakReference<LoadedApk>(packageInfo));
                }
            }
            return packageInfo;
        }
    }

2.獲取這個activity對應的類載入器,由於上面說過,mClassLoader為null,那麼就會執行到ApplicationLoaders#getClassLoader(zip, libraryPath, mBaseClassLoader)方法。

public ClassLoader getClassLoader() {
    synchronized (this) {
        if (mClassLoader != null) {
            return mClassLoader;
        }
        // ...
        // 建立載入器,建立預設的載入器
        // zip為Apk的路徑,libraryPath也就是JNI的路徑
        mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, libraryPath, mBaseClassLoader);
        initializeJavaContextClassLoader();
        StrictMode.setThreadPolicy(oldPolicy);
        } else {
            if (mBaseClassLoader == null) {
                mClassLoader = ClassLoader.getSystemClassLoader();
            } else {
                mClassLoader = mBaseClassLoader;
            }
        }
        return mClassLoader;
    }
}
ApplicationLoaders使用單例它的getClassLoader方法根據傳入的zip路徑事實上也就是Apk的路徑來建立載入器,返回的是一個PathClassLoader。並且PathClassLoader只能載入安裝過的APK。這個載入器建立的時候傳入的是當前應用APK的路徑,理所應當的,想載入其他的APK就構造一個傳遞其他APK的類載入器。

3.用該類載入器載入我們要啟動的activity,並反射建立一個activity例項

public Activity newActivity(ClassLoader cl, String className,Intent intent) throws InstantiationException,  IllegalAccessException, ClassNotFoundException {
    return (Activity)cl.loadClass(className).newInstance();
}

總結一下上面的思路就是,當我們啟動一個activity時,通過系統預設的PathClassLoader來載入這個activity,當然預設情況下只能載入本應用裡面的activity,然後就由系統呼叫到這個activity的生命週期中。

4.這個地方的異常在後面的示例中會出現,到時候分析到原因後就可以找出我們動態載入Activity的思路了。

動態載入Activity:修改系統類載入器

按照這個思路,做這樣的一個示例,按下按鈕,開啟外掛中的Activity。

外掛專案

plugin.dl.pluginactivity

    |--MainActivity.java

內容很簡單,就是一個佈局上面寫了這是外掛中的Activity!並重寫了他的onStart和onDestroy方法。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 載入到宿主程式中之後,這個R.layout.activity_main就是宿主程式中的R.layout.activity_main了
        setContentView(R.layout.activity_main);
    }
    @Override
    protected void onStart() {
        super.onStart();
        Toast.makeText(this,"onStart", 0).show();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        Toast.makeText(this,"onDestroy", 0).show();
    }
}

宿主專案

host.dl.hostactivity

    |--MainActivity.java

包括兩個按鈕,第一個按鈕跳轉到外掛中的MainActivity.java,第二個按鈕調轉到本應用中的MainActivity.java

private Button btn;
    private Button btn1;
    DexClassLoader loader;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn = (Button) findViewById(R.id.btn);
        btn1 = (Button) findViewById(R.id.btn1);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Class activity = null;
                String dexPath = "/PluginActivity.apk";
                loader = new DexClassLoader(dexPath, MainActivity.this.getApplicationInfo().dataDir, null, getClass().getClassLoader());
                try {
                    activity = loader.loadClass("plugin.dl.pluginactivity.MainActivity");
                }catch (ClassNotFoundException e) {
                    Log.i("MainActivity", "ClassNotFoundException");
                }
                Intent intent = new Intent(MainActivity.this, activity);
                MainActivity.this.startActivity(intent);
            }
        });

        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, MainActivity2.class);
                MainActivity.this.startActivity(intent);
            }
        });


首先我們要將該activity在宿主工程的額AndroidManifest裡面註冊。點選按鈕開啟外掛中的activity,發現報錯

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{host.dl.hostactivity/plugin.dl.pluginactivity.MainActivity}: java.lang.ClassNotFoundException: plugin.dl.pluginactivity.MainActivity

#已經使用自定義的載入器,當startActivity時為什麼提示找不到外掛中的activity?

前面第四點說過這個異常。其實這個異常就是在performLaunchActivity中丟擲的,仔細看這個異常列印資訊,發現它說plugin.dl.pluginactivity.MainActivity類找不到,可是我們不是剛剛定義了一個DexClassLoader,成功載入了這個類的嗎??怎麼這裡又提示這個類找不到?

實際上,確實是這樣的,還記得前面說過,系統預設的類載入器PathClassLoader嗎?(因為LoadedApk物件的mClassLoader變數為null,就呼叫到ApplicationLoaders#getClassLoader方法,即根據當前應用的路徑返回一個預設的PathClassLoader),當執行到mPackages.get(aInfo.packageName);時從Map獲取的LoadedApk中未指定mClassLoader,因此會使用系統預設的類載入器。於是當執行這一句 mInstrumentation.newActivity(cl, component.getClassName(), r.intent);時,由於這個類載入器找不到我們外掛工程中的類,因此報錯了。

現在很清楚了,原因就是使用系統預設的這個類載入器不包含外掛工程路徑,無法正確載入我們想要的activity造成的

於是考慮替換系統的類載入器。

private void replaceClassLoader(DexClassLoader loader) {
    try {
        Class clazz_Ath = Class.forName("android.app.ActivityThread");
        Class clazz_LApk = Class.forName("android.app.LoadedApk");
        Object currentActivityThread = clazz_Ath.getMethod("currentActivityThread").invoke(null);
        Field field1 = clazz_Ath.getDeclaredField("mPackages");
        field1.setAccessible(true);
        Map mPackages = (Map) field1.get(currentActivitead);
        String packageName = MainActivity.this.getPackageName();
        WeakReference ref = (WeakReference) mPackages.get(packageName);
        Field field2 = clazz_LApk.getDeclaredField("mClassLoader");
        field2.setAccessible(true);
        field2.set(ref.get(), loader);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
這段程式碼的思路是將ActivityThread類中的mPackages變數中儲存的以當前包名為鍵的LoadedApk值的mClassLoader替換成我們自定義的類載入器。當下一次要載入存放在別的地方的外掛中的某個Activity時,直接在mPackages變數中能取到,因此用的就是我們修改了的類載入器了。

因此,在開啟外掛中的activity之前呼叫replaceClassLoader(loader);方法替換系統的類載入器,就可以了。

效果如下


此時發現可以啟動外掛中的activity,因為執行到了他的onStart方法,並且關閉的時候執行了onDestroy方法,但是奇怪的是介面上的控制元件貌似沒有變化?和啟動他的介面一模一樣,還不能點選。這是什麼原因呢?

顯然,我們只是把外掛中的MainActivity類載入過來了,當執行到他的onCreate方法時,在裡面呼叫setContentView使用的佈局引數是R.layout.activity_main,因為檔名是一樣的,他們的id也是一樣的,當然使用的就是當前應用的資源了。

##已經替換了系統的類載入器為什麼載入本應用的activity卻能正常執行?

不過在修正這個問題之前,有沒有發現一個很奇怪的現象,當載入過外掛中的activity後,再次啟動本地的activity也是能正常啟動的?這是為什麼呢?前面已經替換了預設的類載入器了,並且可以在開啟外掛中的activity後再點選第二個按鈕開啟本應用的activity之前檢視使用的activity,確實是我們已經替換了的類載入器。那這裡為什麼還能正常啟動本應用的activity呢?玄機就在我們建立DexClassLoader時的第四個引數,父載入器!設定父載入器為當前類的載入器,就能保證類的雙親委派模型不被破壞,在載入類時都是先由父載入器來載入,載入不成功時在由自己載入。不信可以在new這個載入器的時候父載入器的引數設定成其他值,比如系統類載入器,那麼當執行activity時肯定會報錯。

接下來解決前面出現的,跳轉到外掛activity中介面顯示不對的問題。這個現象出現的原因已經解釋過了,就是因為使用了本地的資源所導致的,因此需要在setContentView時,使用外掛中的資源佈局。因此在外掛Activity中作如下修改。也可以傳遞一個宿主Activity的引用作為Context,呼叫它的setContentView方法,這樣在找ID的時候就會找外掛中的資源,前提是在宿主中載入過外掛資源並且重寫getResources方法。

public class MainActivity2 extends Activity {
    private static View view;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
// 載入到宿主程式中之後,這個R.layout.activity_main就是宿主程式中的R.layout.activity_main了
//        setContentView(R.layout.activity_main);
        if (view != null)
        setContentView(view);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Toast.makeText(this,"onStart", 0).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Toast.makeText(this,"onDestroy", 0).show();
    }

    private static void setLayout(View v){
        view = v;
    }
}
然後在宿主Activity中獲取外掛資源並將佈局填充成View,然後設定給外掛中的activity,作為它的ContentView的內容。
Class<?> layout = loader.loadClass("plugin.dl.pluginactivity.R$layout");
Field field = layout.getField("activity_main");
Integer obj = (Integer) field.get(null);
// 使用包含外掛APK的Resources物件來獲取這個佈局才能正確獲取外掛中定義的介面效果
//View view = LayoutInflater.from(MainActivity.this).inflate(resources.getLayout(obj),null);
// 或者這樣,但一定要重寫getResources方法,才能這樣寫
View view = LayoutInflater.from(MainActivity.this).inflate(obj, null);
Method method = activity.getDeclaredMethod("setLayout", View.class);
method.setAccessible(true);
method.invoke(activity, view);
完整的程式碼
public class MainActivity extends Activity {
    private Resources resources;
    protected AssetManager assetManager;
    private Button btn;
    private Button btn1;
    DexClassLoader loader;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn = (Button) findViewById(R.id.btn);
        btn1 = (Button) findViewById(R.id.btn1);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String dexPath = "/PluginActivity.apk";
                loader = new DexClassLoader(dexPath, MainActivity.this.getApplicationInfo().dataDir, null, getClass().getClassLoader());
                Class<?> activity = null;
                Class<?> layout = null;
                try {
                    activity = loader.loadClass("plugin.dl.pluginactivity.MainActivity");
                    layout = loader.loadClass("plugin.dl.pluginactivity.R$layout");
                }catch (ClassNotFoundException e) {
                    Log.i("MainActivity", "ClassNotFoundException");
                }
                replaceClassLoader(loader);
                loadRes(dexPath);
                try {
                    Field field = layout.getField("activity_main");
                    Integer obj = (Integer) field.get(null);
                    // 使用包含外掛APK的Resources物件來獲取這個佈局才能正確獲取外掛中定義的介面效果
                  View view = LayoutInflater.from(MainActivity.this).inflate(resources.getLayout(obj),null);
                    // 或者這樣,但一定要重寫getResources方法,才能這樣寫
//                    View view = LayoutInflater.from(MainActivity.this).inflate(obj, null);
                    Method method = activity.getDeclaredMethod("setLayout", View.class);
                    method.setAccessible(true);
                    method.invoke(activity, view);

                } catch (Exception e) {
                    e.printStackTrace();
                }
                Intent intent = new Intent(MainActivity.this, activity);
                MainActivity.this.startActivity(intent);
            }
        });

        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, MainActivity2.class);
                MainActivity.this.startActivity(intent);
            }
        });
    }

    public void loadRes(String path){
        try {
            assetManager = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, path);
        } catch (Exception e) {
        }
        resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
        // 也可以根據資源獲取主題
    }

    private void replaceClassLoader(DexClassLoader loader){
        try {
            Class clazz_Ath = Class.forName("android.app.ActivityThread");
            Class clazz_LApk = Class.forName("android.app.LoadedApk");

            Object currentActivityThread = clazz_Ath.getMethod("currentActivityThread").invoke(null);
            Field field1 = clazz_Ath.getDeclaredField("mPackages");
            field1.setAccessible(true);
            Map mPackages = (Map)field1.get(currentActivityThread);

            String packageName = MainActivity.this.getPackageName();
            WeakReference ref = (WeakReference) mPackages.get(packageName);
            Field field2 = clazz_LApk.getDeclaredField("mClassLoader");
            field2.setAccessible(true);
            field2.set(ref.get(), loader);
        } catch (Exception e){
            System.out.println("-------------------------------------" + "click");
            e.printStackTrace();
        }
    }

    @Override
    public Resources getResources() {
        return resources == null ? super.getResources() : resources;
    }

    @Override
    public AssetManager getAssets() {
        return assetManager == null ? super.getAssets() : assetManager;
    }
}
效果

程式碼點此下載

動態載入Activity:使用代理

還有一種方式啟動外掛中的activity的方式就是將外掛中的activity當做一個一般的類,不把它當成元件activity,於是在啟動的時候啟動一個代理ProxyActivity,它才是真正的Activity,他的生命週期由系統管理,我們在它裡面呼叫外掛Activity裡的函式即可。同時,在外掛Activity裡面儲存一個代理Activity的引用,把這個引用當做上下文環境Context理解。

這裡外掛Activity的生命週期函式均由代理Activity調起,ProxyActivity其實就是一個真正的我們啟動的Activity,而不是啟動外掛中的Activity,外掛中的“要啟動”的Activity就當做一個很普通的類看待,當成一個包含了一些函式的普通類來理解,只是這個類裡面的函式名字起的有些“奇怪”罷了。涉及到訪問資源和更新UI相關的時候通過當前上下文環境,即儲存的proxyActivity引用來獲取。

以下面這個Demo為例


宿主專案

com.dl.host

    |--MainActivity.java

    |--ProxyActivity.java

MainActivity包括一個按鈕,按下按鈕跳轉到外掛Activity
public class MainActivity extends Activity{

	private Button btn;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		btn = (Button)findViewById(R.id.btn);
		btn.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				MainActivity.this.startActivity(new Intent(MainActivity.this, ProxyActivity.class));
			}
		});
	}
}
ProxyActivity就是我們要啟動的外掛Activity的一個傀儡,代理。是系統維護的Activity。
public class ProxyActivity extends Activity{
	
	private DexClassLoader loader;
	private Activity activity;
	private Class<?> clazz = null;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		loader = new DexClassLoader("/Plugin.apk", getApplicationInfo().dataDir, null, getClass().getClassLoader());
		try {
			clazz = loader.loadClass("com.dl.plugin.MainActivity");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		// 設定外掛activity的代理
		try {
			Method setProxy = clazz.getDeclaredMethod("setProxy", Activity.class);
			setProxy.setAccessible(true);
			
			activity = (Activity)clazz.newInstance();
			setProxy.invoke(activity, this);
			
			Method onCreate = clazz.getDeclaredMethod("onCreate", Bundle.class);
			onCreate.setAccessible(true);
			onCreate.invoke(activity, savedInstanceState);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	@Override
	protected void onStart() {
		super.onStart();
		// 呼叫外掛activity的onStart方法
		Method onStart = null;
		try {
			onStart = clazz.getDeclaredMethod("onStart");
			onStart.setAccessible(true);
			onStart.invoke(activity);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	@Override
	protected void onDestroy() {
		super.onStart();
		// 呼叫外掛activity的onDestroy方法
		Method onDestroy = null;
		try {
			onDestroy = clazz.getDeclaredMethod("onDestroy");
			onDestroy.setAccessible(true);
			onDestroy.invoke(activity);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
可以看到,ProxyActivity其實就是一個真正的Activity,我們啟動的就是這個Activity,而不是外掛中的Activity。

外掛專案

com.dl.plugin

    |--MainActivity.java

儲存了一個代理Activity的引用,值得注意的是,由於訪問外掛中的資源需要額外的操作,要載入資源,因此這裡未使用外掛專案裡面的資源,所以我使用程式碼新增的TextView,但原理和前面講的內容是一樣的。

public class MainActivity extends Activity {
	private Activity proxyActivity;
	public void setProxy(Activity proxyActivity) { 
		this.proxyActivity = proxyActivity;
	}
	
	// 裡面的所有操作都由代理activity來操作
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		TextView tv = new TextView(proxyActivity);
		tv.setText("外掛Activity");
		proxyActivity.setContentView(tv,new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
	}
	
	@Override
	protected void onStart() {
		Toast.makeText(proxyActivity, "外掛onStart", 0).show();
	}
	@Override
	protected void onDestroy() {
		Toast.makeText(proxyActivity, "外掛onDestroy", 0).show();
	}
}
這種方法相比較前面修改系統載入器的方法需要自己維護生命週期,比較麻煩,前一種方式由系統自己維護,並且啟動的就是外掛中實實在在的Activity。

前一種方式要在宿主的AndroidManifest裡面宣告外掛Activity,這樣當activity太多時就要宣告很多,比較繁瑣,不過也可以不宣告逃過系統檢查。後面這種方式就只需要一個代理ProxyActivity類即可。在他的onCreate裡面根據傳遞的值選擇載入外掛中的哪個Activity即可。