Android外掛化研究代ACTIVITY註冊

jerrysun發表於2021-09-09

最近在研究Android應用的外掛化開發,看了好幾個相關的開源專案。外掛化都是在解決以下幾個問題:

  • 如何把外掛apk中的程式碼和資源載入到當前虛擬機器。

  • 如何把外掛apk中的四大元件註冊到程式中。

  • 如何防止外掛apk中的資源和宿主apk中的資源引用衝突。

在上篇文章中我研究了的問題(文字、圖片、佈局等),前面兩篇文章解決了外掛化研究的第一個問題。本篇文章開始研究第二個問題:“註冊”外掛中的四大元件。

在安裝apk的時候,應用管理服務PackageManagerService會解析apk,解析應用程式配置檔案AndroidManifest.xml,並從裡面得到得到應用得到應用程式的元件Activity、Service、Broadcast Receiver和Content Provider等資訊,對應用的每個元件“登記”,“登記”之後,在啟動某個Activity過程在ASM執行時對比“登記”然後“查有此人”允許後續的啟動行為。詳細過程可以參考。然而,外掛apk並沒有進行安裝,自然apk中定義的四大元件也沒有進行“登記”,那麼問題來了:以Activity為例,如何啟動外掛中的Acivity?大體兩種思路。


一、 代理方式實現。

宿主端實現一個 PluginProxyActivity,使用這個Activity代理外掛中的Activity的重要事務,例如生命週期呼叫、contentview設定、Activity跳轉等事務。PluginProxyActivity註冊在宿主中,啟動外掛中的Activity實際就是啟動PluginProxyActivity,只是載入的佈局和方法邏輯不一樣而已。百度的外掛框架就是使用的這種方式。


二、“佔坑”方式實現。

啟動Activity是一個複雜的過程,有很多環節:Activity.startActivity()->Activity.startActivityForResult()->Instrument.excuteStartActivity()->ASM.startActivity()。大概又這麼幾個環節,詳細瞭解可以參考文章:。 所謂“佔坑”在宿主端的AndroidManifest.xml註冊一個不存在的Activity,可以取名為StubActivity,同樣啟動外掛的Activity都是啟動StubActivity,然後在啟動Activity的某個環節,我們找個“臨時”演員來代替StubActivity,這個臨時演員就是外掛中定義的Activity,這叫“瞞天過海”。如何找“臨時”演員,這個過程又有很多種實現手段,DroidPlugin、dwarf等框架實現手段各有不同,詳細後續文章再討論。


簡單解釋了兩種思路,本文先用demo來說說如何實現第一種思路(後續文章研究下第二思路)。

PluginProxyActivity的實現

一、重寫setContentView(int layoutResID)方法,使用外掛的AssertManager載入佈局資源。該方法提供給外掛Activity呼叫。


@Override
    public void setContentView(int layoutResID) {
        // do something plugin need
        Resources resources = PluginManager.getInstace().getResources();
        XmlPullParser xmlResourceParser = resources.getLayout(layoutResID);
        View viewFromPlugin = LayoutInflater.from(this).inflate(xmlResourceParser, null);
        setContentView(viewFromPlugin);

    }


二、 重寫onCreate(Bundle savedInstanceState)方法,在這個方法中透過外掛的Activity的類名,利用反射例項化外掛Activity物件,並呼叫其onCreate方法。


@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String className = getIntent().getStringExtra("class");
        initPluginInstance(className);
        invokePluginOnCreate(savedInstanceState);
    }

    private void initPluginInstance(String className) {
        try {
            pluginClass = PluginManager.getInstace().getCloassLoader().loadClass(className);
            Constructor> localConstructor = pluginClass.getConstructor(new Class[]{});
            pluginInstance = localConstructor.newInstance(new Object[] {});
            // 把當前的代理Activity注入到外掛中
            Method setProxy = pluginClass.getMethod("setProxy",
                    new Class[]{PluginProxyActivity.class});
            setProxy.setAccessible(true);
            setProxy.invoke(pluginInstance, new Object[] { this });
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, e.getMessage());
        }
    }

private void invokePluginOnCreate(Bundle savedInstanceState) {
        try {
            Method onCreate = pluginClass.getDeclaredMethod("onCreate",
                    new Class[]{Bundle.class});
            onCreate.setAccessible(true);
            onCreate.invoke(pluginInstance, new Object[] { savedInstanceState });
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }


三、 重寫其他生命週期函式並利用反射呼叫外掛Activity的對應的生命週期函式,例如onPause方法。


@Override
    protected void onPause() {
        super.onPause();
        invokePluginOnPause();
    }

    private void invokePluginOnPause() {
        try {
            Method onPause = pluginClass.getDeclaredMethod("onPause",
                    new Class[]{});
            onPause.setAccessible(true);
            onPause.invoke(pluginInstance, new Object[] {});
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }


四、 過載startAcivity(String className)方法,也就是使用使用定製的startActivity方法來啟動外掛Activity啦。


public void startActivity(String className) {
        // do something plugin need
        Intent intent = new Intent(this,PluginProxyActivity.class);
        intent.putExtra("class",className);
        startActivity(intent);
    }



約定外掛Activity,外掛Activity基類:BasePluginActivity的實現


一、 提供public void setProxy(PluginProxyActivity proxyPluginAct)方法,以獲得PluginProxyActivity的引用。


 /**
     *
     * @param proxyPluginAct
     * provided this method to invoke by reflect . inject proxy
     */
    public void setProxy(PluginProxyActivity proxyPluginAct){
        mProxy = proxyPluginAct;
    }


二、重寫setContentView(int layoutResID),呼叫proxy的setContentView(int layoutResID)方法。


 /**
     * set layout to proxyActivity
     * @param layoutResID
     */
    @Override
    public void setContentView(int layoutResID) {
        if (mProxy != null){
            mProxy.setContentView(layoutResID);
        } else {
            super.setContentView(layoutResID);
        }

    }


三、 定製startActivity(String className)方法來啟動Activity,呼叫proxy的startActivity方法。



    public void startActivity(String className) {
        mProxy.startActivity(className);
    }


最重要的demo

demo實現啦一個外掛的框架的最基本雛形,地址:,如果你有興趣,一定要star,日後研究研究。代理方式實現起來比較簡單,也比較好理解,但是有很多缺陷:一、外掛Activity不能使用this關鍵字,比如this.finish()方法是無效的,真正掌管生命週期的是proxy應該呼叫proxy.finish(),所以百度開源框架 dynamic-load-apk使用that指向proxy,約定外掛中使用that來代替this。二、 外掛Activity無法深度演繹真正的Activity元件,可能有些高階特性無法使用。
總之,不夠透明,外掛開發需要定義自己的規範。既然如此,有沒有更好的方案?當然有,後續文章繼續研究外掛化如何註冊元件的第二類思路:“佔坑”方式實現外掛Activity的註冊。.

原文連結:http://www.apkbus.com/blog-705730-61637.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/151/viewspace-2814716/,如需轉載,請註明出處,否則將追究法律責任。

相關文章