Android 插件化(加载插件)

604 阅读4分钟

Android 插件化其实可以很简单,无外乎就是加载外部资源(可能是apk或jar或资源包、so 等等),再将加载的外部资源运行起来。

image.png

首先要明白的是,如果我们要打开插件中的某一个Activity页面,要么你这个页面要提前放在 manifest 、要么Hook(这里不讲Hook,系统限制多,兼容差);提前放在 manifest的话,你也不能知道到底插件中会有多少个Activity。所以这里用的毕竟简单的方法,代理模式(用宿主中的一个PluginActivity,里面不做任何操作,只需要继承Activity就好了,并提前放到 manifest)。后续,我们所有的插件,都可以通过这个PluginActivity代理来完成。接下来,我们来实践一下:

一、在宿主中加载外部apk

  • 自定义 DexClassLoaderResources 目的是为了获取外部apk中的class 和资源信息

  • 自定义 PluginMannerImpl,统一管理加载工具

image.png

当我们的外部apk被下载,并存放在某一路径下时,可以通过该路径去加载:

public void loadPath(Context context, String path) {
    File dexDir = context.getDir("dex", Context.MODE_PRIVATE);
    mDexClassLoader = new DexClassLoader(path, dexDir.getAbsolutePath(), null, context.getClassLoader());

    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method method = AssetManager.class.getMethod("addAssetPath", String.class);
        method.invoke(assetManager, path);
        mResources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

    } catch (Exception e) {
        e.printStackTrace();
    }
    Log.e("loadPath", "path=" + path);

    Log.e("loadPath", "mResources=" + mResources);


}
注意:这里的上下文 context 是由宿主app中上下文,插件中的上下文都是由宿主来提供的。

加载外部apk 成功后,现在我们要打开一个页面,并用插件中的资源(activity_main.xml)来展示在当前页面中。

二、创建代理PluginActivity,并替换 getClassLoader 和 getResources

class PluginActivity : Activity() {

  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //假设 插件apk 中也有一个 activity_main.xml,由于getResources()是从插件中获取的,
        //所以这里的activity_main.xml 是插件中的
        setContentView(R.layout.activity_main)
 
    }
  

    override fun getClassLoader(): ClassLoader {
        return PluginMannerImpl.getPluginMannerImpl().dexClassLoader
    }

    override fun getResources(): Resources {
        return PluginMannerImpl.getPluginMannerImpl().resources
    }
     

}

到这里,PluginActivity 是在app 宿主中的一个Activity,并在 manifest 已经提前注册。 假设 插件apk和宿主app 中都有一个 activity_main.xml,但是由于getResources()是从插件中获取的,所以这里的activity_main.xml 是插件中的, 我们可以看看两个的区别:

image.png

image.png

两个页面是完全不一样的。但运行起来后,打开的PluginActivity 是 第二种样式(插件apk已经提前打包,并放在宿主app指定目录下)

1678707137709.gif

(资源冲突的问题这里不讲,可以参考 插件资源id冲突处理 )

好了,资源已经能够加载并使用了,下一步,使用插件中的类。

三、插件的生命周期管理

我们插件中的 Activity 不是一个Android中的四大组件,只是一个普通类,我们为了达到Android中的四大组件的同等效果,要自己实现一套生命周期的方法,并且,所有有关上下文的地方都要替换成宿主的。

public interface LifeInterface {
    void attach(Activity activity);
    void onCreate(Bundle savedInstanceState);
    void onResume();
    void onStart();
    void onPause();
    void onStop();
    void onDestroy();
}

单独创建这样的一个简单的生命周期接口,在插件中让LifeActivity 实现它。

/**
 * 用到上下文的地方全部重写
 */
public class LifeActivity implements com.example.server.LifeInterface {


    protected Activity context;

    public void setContentView(int layout) {
        if (context != null) {
            context.setContentView(layout);
        }
    }
    public void startActivity(Intent intent) {
        if (context != null) {
            if (intent==null||intent.getComponent()==null)return;
            Intent newIntent=new Intent();
            String className=intent.getComponent().getClassName();
            if (TextUtils.isEmpty(className))return;
            Log.e("startActivity","className="+className);
            newIntent.putExtra("className",intent.getComponent().getClassName());
            context.startActivity(newIntent);
        }
    }
    public <T extends View> T findViewById(int id) {
        if (context != null) {
            return context.findViewById(id);
        }
        return null;
    }
    public Window getWindow(){
        if (context != null) {
            return context.getWindow();
        }
        return null;
    }
    public ClassLoader getClassLoader(){
        if (context != null) {
            return context.getClassLoader();
        }
        return null;
    }
    public LayoutInflater getLayoutInflater(){
        if (context != null) {
            return context.getLayoutInflater();
        }
        return null;
    }
    public WindowManager getWindowManager(){
        if (context != null) {
            return context.getWindowManager();
        }
        return null;
    }


    //attach上下文
    @Override
    public void attach(Activity activity) {
        context = activity;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {

    }

    @Override
    public void onResume() {

    }

    @Override
    public void onStart() {

    }

    @Override
    public void onPause() {

    }

    @Override
    public void onStop() {

    }

    @Override
    public void onDestroy() {

    }
}

这样,我们插件中的第一个页面也就很快好了

image.png

可以看到,TestActivity 的实现方法,调用,跟我们原来Android中的一样。但实际上他确不是一个真正的Activity,只是个普通类,当我们在宿主中要展示他的时候,是这样的方式:

private fun loadClassActivity() {
    try {
        
        val lass =  classLoader.loadClass("com.example.server.TestActivity")//1、加载类并实例化
        val cs = lass.getConstructor(*arrayOf())
        val obj = cs.newInstance(*arrayOf())
        Log.e("LifeInterface", " obj is LifeInterface=" + (obj is LifeInterface))
        if (obj is LifeInterface) {//公共的组件
            obj.attach(this)//2、传入上下文
            obj.onCreate(Bundle().apply {//走生命周期
                this.putString("sdk", "11111")
            })
            Log.e("LifeInterface", "obj=$obj")
        }

    } catch (e: Exception) {
        e.printStackTrace()
    }

}
通过自定义的 classLoader 去加载 "com.example.server.TestActivity",反射创建他,关键的一步是传入宿主上下文,最后走自定义的生命周期方法,可以实践一下,效果和上面setContentView(R.layout.activity_main) 是一模一样的。因为 LifeActivity 中的 context 其实就是PluginActivity

总结

我们这里通过简单的加载外部apk 方式,展示了其中的资源文件,反射创建了插件中的类。很简单的demo 就可以做到,原理无外乎就是 DexClassLoader双亲委派那套。