1. 程式人生 > >Android外掛化學習之路(六)之動態建立Activity

Android外掛化學習之路(六)之動態建立Activity

靜態代理Activity模式的限制

我們在代理Activity模式一文裡談到啟動外掛APK裡的Activity的兩個難題嗎,由於外掛裡的Activity沒在主專案的Manifest裡面註冊,所以無法經歷系統Framework層級的一系列初始化過程,最終導致獲得的Activity例項並沒有生命週期和無法使用res資源。

使用代理Activity能夠解決這兩個問題,但是有一些限制

  1. 實際執行的Activity例項其實都是ProxyActivity,並不是真正想要啟動的Activity;
  2. ProxyActivity只能指定一種LaunchMode,所以外掛裡的Activity無法自定義LaunchMode;
  3. 不支援靜態註冊的BroadcastReceiver;
  4. 往往不是所有的apk都可作為外掛被載入,外掛專案需要依賴特定的框架,還有需要遵循一定的”開發規範”;

特別是最後一個,無法直接把一個普通的APK作為外掛使用。怎麼避開這些限制呢?外掛的Activity不是標準的Activity物件才會有這些限制,使其成為標準的Activity是解決問題的關鍵,而要使其成為標準的Activity,則需要在主專案裡註冊這些Activity。

想到代理模式需要註冊一個代理的ProxyActivity,那麼能不能在主專案裡註冊一個通用的Activity(比如TargetActivity)給外掛裡所有的Activity用呢?解決對策就是,在需要啟動外掛的某一個Activity(比如PlugActivity)的時候,動態建立一個TargetActivity

,新建立的TargetActivity會繼承PlugActivity的所有共有行為,而這個TargetActivity的包名與類名剛好與我們事先註冊的TargetActivity一致,我們就能以標準的方式啟動這個Activity。

動態建立Activity模式

執行時動態建立並編譯一個Activity類,這種想法不是天方夜譚,動態建立類的工具有dexmakerasmdex,二者均能實現動態位元組碼操作,最大的區別是前者是建立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。