Android 插件化 02 —— 提线木偶

回顾:

上一篇通过一个小实验,验证了容器项目能够通过一些偏门的方法获取插件apk的资源,并且显示插件简单的Activity界面。

但插件作为一个独立的应用,不会像Demo那样只有一个简单的界面。每个实际应用的Activity都会涉及大量的控件、事件响应以及交互操作等等。作为插件的Container,实在没必要去关心插件功能上的具体实现细节,它只要做一个规则的制定者就行了。
就如总理说的那样:让政府的归政府,让市场的归市场……

展望:

先来看我们写Android应用的一般规律(Activity方面的):
onCreate()方法一般布局并初始化控件,添加事件监听等;
onDestory()方法一般用于释放一些资源;
onPause()、onResume()用于前后台切换响应等,Activity中几乎所有行为都与其生命周期息息相关。

Activity生命周期

如果Container掌控了插件Activity生命周期的各个方法,那么插件就会像提线木偶一样,为其所用。

也就是说,Container —— PluginActivity中,关于生命周期的方法大致应该是这样的:

protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    
    //...
    
    //读取apk资源
    loadResources(apkPath);
    
    //...
    
    //调用插件PluginDemoActivity对象的onCreate()
    //...
}

@Override
protected void onDestroy()
{
    super.onDestroy();
    //调用插件PluginDemoActivity对象的onDestroy()
    //...
}

@Override
protected void onPause()
{
    super.onPause();
    //调用插件PluginDemoActivity对象的onPause()
    //...
}

@Override
protected void onResume()
{
    super.onResume();
    //调用插件PluginDemoActivity对象的onResume()
    //...
}

@Override
protected void onStart()
{
    super.onStart();
    //调用插件PluginDemoActivity对象的onStart()
    //...
}

@Override
protected void onStop()
{
    super.onStop();
    //调用插件PluginDemoActivity对象的onStop()
    //...
}

涉及到的问题:

如何获得插件Activity的对象,或者说是如何加载另一个apk中的class

DexClassLoader是一个类加载器,可以用来从.jar和.apk文件中加载class。可以用来加载执行没有和应用程序一起安装的那部分代码。

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent);

参数:
dexPath:apk路径
optimizedDirectory:解压后的.dex文件的存储路径,这个路径强烈建议使用应用程序的私有路径,不要放在sdcard上,否则代码容易被注入攻击。
libraryPath:系统库的存放路径,可以为空。
parent:父亲加载器,一般为context.getClassLoader(),使用当前上下文的类加载器。

使用,上PluginActivity的代码:

public class PluginActivity extends Activity
{
    protected AssetManager mAssetManager;
    protected Resources mResources;
    protected Theme mTheme;

    private Class<?> mPluginDemoClass;
    private Object mPluginActivity;

    //生命周期的每个方法
    private Method method_onStart;
    private Method method_onPause;
    private Method method_onResume;
    private Method method_onStop;
    private Method method_onDestroy;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        try
        {
            //得到插件apk路径
            String dir = Environment.getExternalStorageDirectory().toString();
            String apkPath = dir + "/PluginDemo.apk"; //apk存放路径
            if (!new File(apkPath).exists())
            {
                return;
            }

            //读取apk资源
            loadResources(apkPath);

            //得到DexClassLoader
            File dexOutputDir = getDir("dex", 0);
            ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
            DexClassLoader loader = new DexClassLoader(apkPath, dexOutputDir.getAbsolutePath(), null, localClassLoader);

            //动态加载插件Activity
            mPluginDemoClass = loader.loadClass("com.test.plugindemo.MainActivity");
            Constructor<?> localConstructor = mPluginDemoClass.getConstructor(new Class[]{});
            mPluginActivity = localConstructor.newInstance(new Object[]{});

            //将代理对象设置给插件Activity
            //因为插件Activity对象需要使用过的资源都在当前Activity中,所以需要把容器Activity对象传过去
            Method setProxy = mPluginDemoClass.getMethod("setProxy", new Class[]{Activity.class});
            setProxy.setAccessible(true);
            setProxy.invoke(mPluginActivity, new Object[]{this});

            //存储每个生命周期的方法
            method_onStart = mPluginDemoClass.getDeclaredMethod("onStart");
            method_onStart.setAccessible(true);
            method_onPause = mPluginDemoClass.getDeclaredMethod("onPause");
            method_onPause.setAccessible(true);
            method_onResume = mPluginDemoClass.getDeclaredMethod("onResume");
            method_onResume.setAccessible(true);
            method_onStop = mPluginDemoClass.getDeclaredMethod("onStop");
            method_onStop.setAccessible(true);
            method_onDestroy = mPluginDemoClass.getDeclaredMethod("onDestroy");
            method_onDestroy.setAccessible(true);

            //调用它的onCreate方法
            Method onCreate = mPluginDemoClass.getDeclaredMethod("onCreate", new Class[]{Bundle.class});
            onCreate.setAccessible(true);
            onCreate.invoke(mPluginActivity, new Object[]{new Bundle()});

            this.setContentView(R.layout.activity_main);
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
    }

    protected void loadResources(String apkPath)
    {
        try
        {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, apkPath);
            mAssetManager = assetManager;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        Resources superRes = super.getResources();
        superRes.getDisplayMetrics();
        superRes.getConfiguration();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }


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

        try
        {
            method_onDestroy.invoke(mPluginActivity, new Object[]{});
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
    }

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

        try
        {
            method_onPause.invoke(mPluginActivity, new Object[]{});
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
    }

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

        try
        {
            method_onResume.invoke(mPluginActivity, new Object[]{});
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
    }

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

        try
        {
            method_onStart.invoke(mPluginActivity, new Object[]{});
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
    }

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

        try
        {
            method_onStop.invoke(mPluginActivity, new Object[]{});
        }
        catch (Exception e)
        {
            System.out.println(e);
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    
    @Override
    public AssetManager getAssets()
    {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }

    @Override
    public Resources getResources()
    {
        return mResources == null ? super.getResources() : mResources;
    }

    @Override
    public Theme getTheme()
    {
        return mTheme == null ? super.getTheme() : mTheme;
    }
}

容器的代码基本就这些,需要注意的是,在创建出插件的mPluginActivity对象后,需要把容器(或者说是代理)对象设置到对象中,因为所有资源都需要从这里获取。

接下来修改插件工程:

上插件主Activity的代码,为了增加点交互,我在里面增加了一个Button,点击此Button弹出一个AlertDialog

public class MainActivity extends Activity
{
    private Button mButton = null;
    private Activity mProxyActivity = null; //代理Activity

    public void setProxy(Activity proxyActivity)
    {
        mProxyActivity = proxyActivity;
    }

    //这里需要注意的是,以下重载的方法中,不能调用super.xxxx了,不然报错,原因也很简单,
    //这个Activity实质上不是真正的Activity了,没有生命周期的概念了,调用super的方法肯定报错
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
//        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.button);
        mButton.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                if (v.getId() == R.id.button)
                {
                    new AlertDialog.Builder(mProxyActivity)
                            .setMessage("show me the money")
                            .setPositiveButton("ok", null)
                            .show();
                }
            }
        });
    }

    @Override
    public void setContentView(int layoutResID)
    {
//        super.setContentView(layoutResID);
        mProxyActivity.setContentView(layoutResID);
    }

    @Override
    public View findViewById(int id)
    {
//        return super.findViewById(id);
        return mProxyActivity.findViewById(id);
    }

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

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

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

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

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

}

对于插件来说,需要非常注意的就是:使用的资源一定要是来自代理(容器)对象的。

到这里,简单的插件化就基本完成了。同上一篇一样,制作PluginDemo.apk,放到指定目录,运行Container工程,点击加载插件,成功!


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

推荐阅读更多精彩内容