Android高级进阶之-插件化开发原理与实践

写在前面,什么是插件化开发?

所谓插件化开发就是将APP中的一些功能模块单独抽离出来,打包成可以单独运行的apk包(当然如果需要一些登录态或者运行参数环境时不可以单独运行,但是技术条件上是可以的),当APP程序需要运行这些模块的时候,就可以直接加载这些模块apk,然后运行。

举个易懂的例子,支付宝内部集成了很多功能模块,其中就有类似淘票票这样的不可能在支付宝一个apk包就全部打包好,这样不仅安装包体积过大,也不利于功能模块的拔插。

现在给出插件化开发的总体思路,然后我们逐个击破。

首先宿主APP提供ProxyActivity,当宿主需要打开插件包中的Activity时,一律是启动的ProxyActivity,在启动ProxyActivity的intent中携带我们真正需要去打开的插件包中Activity的类全名。

        Intent intent = new Intent(context, ProxyActivity.class);
        intent.putExtra("className", className);//className 对应插件包中需要被打开的Activity的类全名
        return intent;

ProxyActivity会被正常启动,但是这个ProxyActivity会在其onCreate回调中去反射出这个真实需要被开启的Activity对象,

            Class activityClass = getClassLoader().loadClass(className);
            Constructor constructor = activityClass.getConstructor(new Class[]{});
            Activity instance = constructor.newInstance(new Object[]{});

现在我们拿到了instance对象,当然它不是系统new出来的,它的生命周期方法都需要我们去通知,那么接下来就好办了,我们重写ProxyActivity所有的生命周期方法,然后去手动调用instance相对应的生命周期方法。

public class ProxyActivity extends Activity {
    private IPluginActivity mPluginActivity;//用来接收插件包所有Activity的接口对象

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String className = getIntent().getStringExtra("className");
        try {
            Class activityClass = getClassLoader().loadClass(className);
            Constructor constructor = activityClass.getConstructor(new Class[]{});
            Object instance = constructor.newInstance(new Object[]{});
            mPluginActivity = (PluginBaseActivity) instance;
            mPluginActivity.onCreate(savedInstanceState);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mPluginActivity.onStart();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mPluginActivity.onResume();
    }

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

    @Override
    protected void onStop() {
        super.onStop();
        mPluginActivity.onStop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPluginActivity.onDestroy();
    }
}

当然ProxyActivity中还有一些回调方法需要去通知mPluginActivity做出对应的调用,这里不一一例举。

在Activity中有很重要的两个回调方法:

    @Override
    public ClassLoader getClassLoader() {
        return PluginManager.getInstance().getPluginBean().getDexClassLoader();
    }

    @Override
    public Resources getResources() {
        return PluginManager.getInstance().getPluginBean().getResources();
    }
  • getClassLoader返回的是ClassLoader 对象,Activity内部在使用反射new对象时,都会去使用这里返回的ClassLoader 来进行反射,所以我们ProxyActivity中需要提供的应该是当前加载插件包对应的ClassLoader 。
  • getResources返回的是Resources 对象,Activity内部在使用资源文件时,都会去使用这里返回的Resources 来获取资源,所以我们ProxyActivity中需要提供的应该是当前加载插件包对应的Resources 。

相信讲到这里,你大概知道插件化开发打开插件包中某个Activity的原理了:我们宿主APP提供了一个ProxyActivity,我们打开插件包Activity其实都是打开的ProxyActivity,只不过ProxyActivity不做任何其他事情,只负责实例化插件包Activity,并且将ProxyActivity中的所有事件驱动通知给插件包Activity,也就是ProxyActivity调用了插件包Activity中的代码来实现插件包要实现的功能。

这个时候我们再来看看如何去加载外部插件包apk,并且获得classLoader和resource:

            File pluginFile;//插件包文件
            PackageManager packageManager = context.getPackageManager();
            PackageInfo packageInfo = packageManager.getPackageArchiveInfo(pluginFile.getAbsolutePath(), PackageManager.GET_ACTIVITIES);

            File dexOutFile = context.getDir("dex", Context.MODE_PRIVATE);
            DexClassLoader dexClassLoader = new DexClassLoader(pluginFile.getAbsolutePath(), dexOutFile.getAbsolutePath()
                    , null, context.getClassLoader());

            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, pluginFile.getAbsolutePath());
            Resources resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

上述代码中,PackageInfo 可以获取插件包manifest中所有注册了的组件信息,例如主Activity的全类名的获取为:

packageInfo.activities[0].name

OK,到了这里我们可以贴上来插件包中,所有Activity都需要继承的BaseActivity代码:

public abstract class PluginBaseActivity extends AppCompatActivity implements IPluginActivity {

    protected Activity that;//宿主Activity

    @Override
    public void attach(ProxyActivity proxyActivity) {
        if (that != null)
            throw new RuntimeException("Plugin's activity already has been attached!");
        this.that = proxyActivity;
        attachBaseContext(proxyActivity);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        if (that == null) {
            super.onCreate(savedInstanceState);
        }
    }

    @Override
    public void onStart() {
        if (that == null) {
            super.onStart();
        }
    }

    @Override
    public void onResume() {
        if (that == null) {
            super.onResume();
        }
    }

    @Override
    public void onPause() {
        if (that == null) {
            super.onPause();
        }
    }

    @Override
    public void onStop() {
        if (that == null) {
            super.onStop();
        }
    }

    @Override
    public void onDestroy() {
        if (that == null) {
            super.onDestroy();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (that == null) {
            super.onSaveInstanceState(outState);
        }
    }

    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState) {
        if (that == null) {
            super.onRestoreInstanceState(savedInstanceState);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (that == null) {
            return super.onTouchEvent(event);
        }
        return false;
    }

    @Override
    public void onBackPressed() {
        if (that == null) {
            super.onBackPressed();
        }
    }

    @Override
    public void setContentView(View view) {
        if (that == null) {
            super.setContentView(view);
        } else {
            that.setContentView(view);
        }
    }

    @Override
    public void setContentView(int layoutResID) {
        if (that == null) {
            super.setContentView(layoutResID);
        } else {
            that.setContentView(layoutResID);
        }
    }

    @Override
    public void startActivity(Intent intent) {
        if (that == null) {
            super.startActivity(intent);
        } else {//篡改intent打开页面为proxyActivity
            intent.putExtra("className", intent.getComponent().getClassName());
            intent.setClassName(intent.getComponent().getPackageName(), ProxyActivity.class.getName());
            that.startActivity(intent);
        }
    }

    @Override
    public ComponentName startService(Intent intent) {
        if (that == null) {
            return super.startService(intent);
        } else {
            intent.putExtra("className", intent.getComponent().getClassName());
            intent.setClassName(intent.getComponent().getPackageName(), ProxyService.class.getName());
            return that.startService(intent);
        }
    }

    @Override
    public View findViewById(int id) {
        if (that == null) {
            return super.findViewById(id);
        } else {
            return that.findViewById(id);
        }
    }


    @Override
    public Intent getIntent() {
        if (that == null) {
            return super.getIntent();
        } else {
            return that.getIntent();
        }
    }


    @Override
    public Window getWindow() {
        if (that == null) {
            return super.getWindow();
        } else {
            return that.getWindow();
        }
    }

    @Override
    public WindowManager getWindowManager() {
        if (that == null) {
            return super.getWindowManager();
        } else {
            return that.getWindowManager();
        }
    }
}
public interface IPluginActivity {
    void attach(ProxyActivity proxyActivity);

    void onCreate(@Nullable Bundle savedInstanceState);

    void onStart();


    void onResume();

    void onPause();

    void onStop();

    void onDestroy();

    void onSaveInstanceState(Bundle outState);

    void onRestoreInstanceState(Bundle savedInstanceState);

    boolean onTouchEvent(MotionEvent event);

    void onBackPressed();

    void setContentView(View view);

    void setContentView(int layoutResID);

    void startActivity(Intent intent);

    ComponentName startService(Intent intent);

    View findViewById(int id);


    Intent getIntent();


    Window getWindow();

    WindowManager getWindowManager();
}

IPluginActivity 除了声明一个attach()方法外,其它都是activity默认提供的方法。插件包的PluginBaseActivity 之所以对that做非空判断目的就是为了允许插件包apk可以单独安装并且运行。

好了,写到这里献上源码。
链接:https://share.weiyun.com/5ne9oLj

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容