Android动态加载黑科技 动态创建Activity模式
基本信息
-
Author:kaedea
-
GitHub:android-dynamical-loading
代理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。
總不能把插件APK所有的Activity都事先注冊到主項目里面吧,想到代理模式需要注冊一個代理的ProxyActivity,那么能不能在主項目里注冊一個通用的Activity(比如TargetActivity)給插件里所有的Activity用呢?解決對策就是,在需要啟動插件的某一個Activity(比如PlugActivity)的時候,動態創建一個TargetActivity,新創建的TargetActivity會繼承PlugActivity的所有共有行為,而這個TargetActivity的包名與類名剛好與我們事先注冊的TargetActivity一致,我們就能以標準的方式啟動這個Activity。
動態創建Activity模式
運行時動態創建并編譯一個Activity類,這種想法不是天方夜譚,動態創建類的工具有dexmaker和asmdex,二者均能實現動態字節碼操作,最大的區別是前者是創建dex文件,而后者是創建class文件。
使用dexmaker動態創建一個類
運行時創建一個編譯好并能運行的類叫做“動態字節碼操作(runtime bytecode manipulation)”,使用dexmaker工具能創建一個dex文件,之后我們再反編譯這個dex看看創建出來的類是什么樣子。
public class MainActivity extends AppCompatActivity {@Overrideprotected 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目錄下找到剛創建的文件“Generated1532509318.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{@overridepublic Class loadClass(String className){if(當前上下文插件不為空) {if( className 是 TargetActivity){找到當前實際要加載的原始PlugActivity,動態創建類(TargetActivity extends PlugActivity )的dex文件return 從dex文件中加載的TargetActivity}else{return 使用對應的PluginClassLoader加載普通類} }else{return super.loadClass() //使用原來的類加載方法} } }這樣就能把啟動插件里的PlugActivity變成啟動動態創建的TargetActivity。
不過還有一個問題,主項目啟動插件Activity的時候,我們可以替換Activity,但是如果在插件Activity(比如MainActivity)啟動另一個Activity(SubActivity)的時候怎么辦?插件時普通的第三方APK,我們無法更改里面跳轉Activity的邏輯。其實,從主項目啟動插件MainActivity的時候,其實啟動的是我們動態創建的TargetActivity(extends MainActivity),而我們知道Activity啟動另一個Activity的時候都是使用其“startActivityForResult”方法,所以我們可以在創建TargetActivity時,重寫其“startActivityForResult”方法,讓它在啟動其他Activity的時候,也采用動態創建Activity的方式,這樣就能解決問題。
動態創建Activity開源項目?android-pluginmgr
這種腦洞大開的動態加載思路來自于houkx的開源項目android-pluginmgr
android-pluginmgr;項目中有三種ClassLoader,一是用于替換宿主APK的Application的CJClassLoader,二是用于加載插件APK的PluginClassLoader,再來是用于加載啟動插件Activity時動態生成的PlugActivity的dex包的DexClassLoader(存放在Map集合proxyActivityLoaderMap里面)。其中CJClassLoader是PluginClassLoader的Parent,而PluginClassLoader又是第三種DexClassLoader的Parent。
ClassLoader類加載Class的時候,會先使用Parent的ClassLoader,但Parent不能完成加載工作時,才會調用Child的ClassLoader去完成工作。
java.lang.ClassLoader
Loads classes and resources from a repository. One or more class loaders are installed at runtime. These are consulted whenever the runtime system needs a specific class that is not yet available in-memory. Typically, class loaders are grouped into a tree where child class loaders delegate all requests to parent class loaders. Only if the parent class loader cannot satisfy the request, the child class loader itself tries to handle it.
具體分析請參考?Android動態加載基礎 ClassLoader的工作機制。
所以每加載一個Activity的時候都會調用到最上級的CJClassLoader的loadClass方法,從而保證啟動插件Activity的時候能順利替換成PlugActivity。當然如何控制著三種ClassLoader的加載工作,也是pluginmgr項目的設計難度之一。
存在的問題
動態類創建的方式,使得注冊一個通用的Activity就能給多給Activity使用,對這種做法存在的問題也是明顯的
使用同一個注冊的Activity,所以一些需要在Manifest注冊的屬性無法做到每個Activity都自定義配置;
插件中的權限,無法動態注冊,插件需要的權限都得在宿主中注冊,無法動態添加權限;
插件的Activity無法開啟獨立進程,因為這需要在Manifest里面注冊;
動態字節碼操作涉及到Hack開發,所以相比代理模式起來不穩定;
其中不穩定的問題出現在對Service的支持上,使用動態創建類的方式可以搞定Activity和Broadcast Receiver,但是使用類似的方式處理Service卻不行,因為“ContextImpl.getApplicationContext” 期待得到一個非ContextWrapper的context,如果不是則繼續下次循環,目前的Context實例都是wrapper,所以會進入死循環。
據houkx稱他現在有另外的思路實現“啟動為安裝的普通第三方APK”的目的,而且不是基于動態類創建的原理,期待他的開源項目的更新。
代理Activity模式與動態創建Activity模式的區別
簡單地說,最大的不同是代理模式使用了一個代理的Activity,而動態創建Activity模式使用了一個通用的Activity。
代理模式中,使用一個代理Activity去完成本應該由插件Activity完成的工作,這個代理Activity是一個標準的Android Activity組件,具有生命周期和上下文環境(ContextWrapper和ContextCompl),但是它自身只是一個空殼,并沒有承擔什么業務邏輯;而插件Activity其實只是一個普通的Java對象,它沒有上下文環境,但是卻能正常執行業務邏輯的代碼。代理Activity和不同的插件Activity配合起來,就能完成不同的業務邏輯了。所以代理模式其實還是使用常規的Android開發技術,只是在處理插件資源的時候強制調用了系統的隱藏API,因此這種模式還是可以穩定工作和升級的。
動態創建Activity模式,被動態創建出來的Activity類是有在主項目里面注冊的,它是一個標準的Activity,它有自己的Context和生命周期,不需要代理的Activity。
總結
以上是生活随笔為你收集整理的Android动态加载黑科技 动态创建Activity模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android动态加载进阶 代理Acti
- 下一篇: DL动态加载框架技术