Android外掛化學習之路(六)之動態建立Activity
靜態代理Activity模式的限制
我們在代理Activity模式一文裡談到啟動外掛APK裡的Activity的兩個難題嗎,由於外掛裡的Activity沒在主專案的Manifest裡面註冊,所以無法經歷系統Framework層級的一系列初始化過程,最終導致獲得的Activity例項並沒有生命週期和無法使用res資源。
使用代理Activity能夠解決這兩個問題,但是有一些限制
- 實際執行的Activity例項其實都是ProxyActivity,並不是真正想要啟動的Activity;
- ProxyActivity只能指定一種LaunchMode,所以外掛裡的Activity無法自定義LaunchMode;
- 不支援靜態註冊的BroadcastReceiver;
- 往往不是所有的apk都可作為外掛被載入,外掛專案需要依賴特定的框架,還有需要遵循一定的”開發規範”;
特別是最後一個,無法直接把一個普通的APK作為外掛使用。怎麼避開這些限制呢?外掛的Activity不是標準的Activity物件才會有這些限制,使其成為標準的Activity是解決問題的關鍵,而要使其成為標準的Activity,則需要在主專案裡註冊這些Activity。
想到代理模式需要註冊一個代理的ProxyActivity,那麼能不能在主專案裡註冊一個通用的Activity(比如TargetActivity)給外掛裡所有的Activity用呢?解決對策就是,在需要啟動外掛的某一個Activity(比如PlugActivity)的時候,動態建立一個TargetActivity
動態建立Activity模式
執行時動態建立並編譯一個Activity類,這種想法不是天方夜譚,動態建立類的工具有dexmaker和asmdex,二者均能實現動態位元組碼操作,最大的區別是前者是建立dex檔案,而後者是建立class檔案。
使用dexmaker動態建立一個類
執行時建立一個編譯好並能執行的類叫做“動態位元組碼操作(runtime bytecode manipulation)”,使用dexmaker工具能建立一個dex檔案,之後我們再反編譯這個dex看看創建出來的類是什麼樣子。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onMakeDex(View view){
try {
DexMaker dexMaker = new DexMaker();
// Generate a HelloWorld class.
TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
generateHelloMethod(dexMaker, helloWorld);
// Create the dex file and load it.
File outputDir = new File(Environment.getExternalStorageDirectory() + File.separator + "dexmaker");
if (!outputDir.exists())outputDir.mkdir();
ClassLoader loader = dexMaker.generateAndLoad(this.getClassLoader(), outputDir);
Class<?> helloWorldClass = loader.loadClass("HelloWorld");
// Execute our newly-generated code in-process.
helloWorldClass.getMethod("hello").invoke(null);
} catch (Exception e) {
Log.e("MainActivity","[onMakeDex]",e);
}
}
/**
* Generates Dalvik bytecode equivalent to the following method.
* public static void hello() {
* int a = 0xabcd;
* int b = 0xaaaa;
* int c = a - b;
* String s = Integer.toHexString(c);
* System.out.println(s);
* return;
* }
*/
private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
// Lookup some types we'll need along the way.
TypeId<System> systemType = TypeId.get(System.class);
TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);
// Identify the 'hello()' method on declaringType.
MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");
// Declare that method on the dexMaker. Use the returned Code instance
// as a builder that we can append instructions to.
Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);
// Declare all the locals we'll need up front. The API requires this.
Local<Integer> a = code.newLocal(TypeId.INT);
Local<Integer> b = code.newLocal(TypeId.INT);
Local<Integer> c = code.newLocal(TypeId.INT);
Local<String> s = code.newLocal(TypeId.STRING);
Local<PrintStream> localSystemOut = code.newLocal(printStreamType);
// int a = 0xabcd;
code.loadConstant(a, 0xabcd);
// int b = 0xaaaa;
code.loadConstant(b, 0xaaaa);
// int c = a - b;
code.op(BinaryOp.SUBTRACT, c, a, b);
// String s = Integer.toHexString(c);
MethodId<Integer, String> toHexString
= TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
code.invokeStatic(toHexString, s, c);
// System.out.println(s);
FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
code.sget(systemOutField, localSystemOut);
MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
TypeId.VOID, "println", TypeId.STRING);
code.invokeVirtual(printlnMethod, null, localSystemOut, s);
// return;
code.returnVoid();
}
}
在SD卡的dexmaker目錄下找到剛建立的檔案“Generated1.jar”,把裡面的“classes.dex”解壓出來,然後再用“dex2jar”工具轉化成jar檔案,最後再用“jd-gui”工具反編譯jar的原始碼。
至此,已經成功在執行時建立一個編譯好的類。
修改需要啟動的目標Activity
接下來的問題是如何把需要啟動的、在Manifest裡面沒有註冊的PlugActivity換成有註冊的TargetActivity。
在Android,虛擬機器載入類的時候,是通過ClassLoader的loadClass方法,而loadClass方法並不是final型別的,這意味著我們可以建立自己的類去繼承ClassLoader,以過載loadClass方法並改寫類的載入邏輯,在需要載入PlugActivity的時候,偷偷把其換成TargetActivity。
大致思路如下
public class CJClassLoader extends ClassLoader{
@override
public Class loadClass(String className){
if(當前上下文外掛不為空) {
if( className 是 TargetActivity){
找到當前實際要載入的原始PlugActivity,動態建立類(TargetActivity extends PlugActivity )的dex檔案
return 從dex檔案中載入的TargetActivity
}else{
return 使用對應的PluginClassLoader載入普通類
}
}else{
return super.loadClass() //使用原來的類載入方法
}
}
}
不過還有一個問題,主專案啟動外掛Activity的時候,我們可以替換Activity,但是如果在外掛Activity(比如MainActivity)啟動另一個Activity(SubActivity)的時候怎麼辦?外掛時普通的第三方APK,我們無法更改裡面跳轉Activity的邏輯。其實,從主專案啟動外掛MainActivity的時候,其實啟動的是我們動態建立的TargetActivity(extends MainActivity),而我們知道Activity啟動另一個Activity的時候都是使用其“startActivityForResult”方法,所以我們可以在建立TargetActivity時,重寫其“startActivityForResult”方法,讓它在啟動其他Activity的時候,也採用動態建立Activity的方式,這樣就能解決問題。
動態類建立Activity缺陷
動態類建立的方式,使得註冊一個通用的Activity就能給多給Activity使用,對這種做法存在的問題也是明顯的
1. 使用同一個註冊的Activity,所以一些需要在Manifest註冊的屬性無法做到每個Activity都自定義配置;
2. 外掛中的許可權,無法動態註冊,外掛需要的許可權都得在宿主中註冊,無法動態新增許可權;
3. 外掛的Activity無法開啟獨立程序,因為這需要在Manifest裡面註冊;
4. 動態位元組碼操作涉及到Hack開發,所以相比代理模式起來不穩定;
其中不穩定的問題出現在對Service的支援上,使用動態建立類的方式可以搞定Activity和Broadcast Receiver,但是使用類似的方式處理Service卻不行,因為“ContextImpl.getApplicationContext” 期待得到一個非ContextWrapper的context,如果不是則繼續下次迴圈,目前的Context例項都是wrapper,所以會進入死迴圈。
推薦一個動態代理的開源專案:android-pluginmgr
代理Activity模式與動態建立Activity模式的區別
簡單地說,最大的不同是代理模式使用了一個代理的Activity,而動態建立Activity模式使用了一個通用的Activity。
代理模式中,使用一個代理Activity去完成本應該由外掛Activity完成的工作,這個代理Activity是一個標準的Android Activity元件,具有生命週期和上下文環境(ContextWrapper和ContextCompl),但是它自身只是一個空殼,並沒有承擔什麼業務邏輯;而外掛Activity其實只是一個普通的Java物件,它沒有上下文環境,但是卻能正常執行業務邏輯的程式碼。代理Activity和不同的外掛Activity配合起來,就能完成不同的業務邏輯了。所以代理模式其實還是使用常規的Android開發技術,只是在處理外掛資源的時候強制呼叫了系統的隱藏API,因此這種模式還是可以穩定工作和升級的。
動態建立Activity模式,被動態創建出來的Activity類是有在主專案裡面註冊的,它是一個標準的Activity,它有自己的Context和生命週期,不需要代理的Activity。