占位式插件化之加载Activity

占位式插件化之加载Activity

在一些大型的项目中,经常会用到插件化,插件化的优点有不少,即插即用,把不同的功能打包成不同的APK文件,通过网络下发到APP端,直接就可以使用,不用通过应用市场即可随时增加新功能,非常适用于功能多又需要敏捷开发的应用

可以实现插件化的方式有很多种,本系列先通过占位式的方法来实现。

我们知道,一个apk文件需要通过安装才能运行使用,那我们的插件apk是直接通过网络下载到本地的,不通过用户的安装,也就没有上下文环境context,怎么才能运行里面的功能呢?

其中的一种方式就是使用占位式来开发,首先我们肯定有一个宿主APP,这个APP已经发布到市场上并安装到了用户的手机上,这个APP中有一个APK运行所需要的所有的环境,那我们想办法把这里面的环境传到我们的插件包APK中,插件包中都使用穿过来的环境就能正常的工作了。

然后就是宿主APP中怎么加载插件apk中的类和资源文件呢?这个需要了解一下Android中的类加载技术,简单说一下,Andorid中使用PathClassLoader来加载自身应用中的类,使用DexClassLoader来加载外部的文件(apk,zip等),使用Resources类来加载资源文件。

最后类加载完了,宿主APP中怎么调用插件中的对应的方法呢,它不知道什么时候该调用什么方法啊。这时候我们就可以用到面向接口编程了,让宿主APP和插件APP都依赖一套相同的接口标准,到时候通过这个相同的接口标准来调用对应的方法。

OK说了一大堆,现在开始干吧,先撸一个加载Activity的

首先如图在AndroidStudio中建立两个app和一个module,这两个app分别是宿主app和插件app,他们两个都依赖同一个module,这个module中定义了一些接口标准

先来看看Activity的接口标准:

public interface ActivityInterface {

    /**
     * 把宿主(app)的环境给插件
     * @param appActivity 宿主环境
     */
    void insertAppContext(Activity appActivity);

    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onDestroy();

}

标准很简单,主要分为两部分,第一部分插件中不是没有运行环境吗,那定义一个方法,专门用来把宿主的环境传过来。第二部分,在里面实现所有我们需要用到的activity的声明周期方法,这里就实现了几个常用的。

OK,标准包中就完事了

下面我们来到插件包中,定义一个BaseActivity,用它来实现标准接口和接收宿主传过来的环境,还有重写Activity中的相关方法。

public class BaseActivity extends Activity implements ActivityInterface {

    public Activity appActivity;

    @Override
    public void insertAppContext(Activity appActivity) {
         this.appActivity = appActivity;
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate(Bundle savedInstanceState) {

    }
    @SuppressLint("MissingSuperCall")
    @Override
    public void onStart() {

    }
    @SuppressLint("MissingSuperCall")
    @Override
    public void onResume() {

    }
    @SuppressLint("MissingSuperCall")
    @Override
    public void onDestroy() {

    }
     public void setContentView(int resId){
        appActivity.setContentView(resId);
    }
}

BaseActivity实现了ActivityInterface接口,并实现了接口中的方法。

注意:BaseActivity中重写了setContentView方法,为什么呢?因为setContentView是当前插件Activity中的方法,而当前的插件Activity是没有上下文环境的,调用这个肯定就报错啦,为了能正常运行,我们只能通过宿主传过来的环境来调用相关的方法。这只是开始,后面很多跟环境有关的方法都需要在这里重写一下转为通过宿主的环境调用,这也是占位式插件化的一个缺点。

定义一个PluginActivity来继承自BaseActivity,等会我们将从宿主APP中跳转到此Activity。这是我们在插件中的第一个Activity,需要注册到manifest中,后面宿主跳转时需要用到,在后面创建的Activity就不用注册了。

public class PluginActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin);
        Toast.makeText(appActivity,"我是插件中的activity",Toast.LENGTH_SHORT).show();
}

OK,插件包中的类写完了,现在我们来到宿主app中创建一个工具来,用来加载插件包中的类和资源

public class PluginManager {
    private static final String TAG = PluginManager.class.getSimpleName();

    public static PluginManager instance;

    private Context mContext;

    public static PluginManager getInstance(Context context){
        if(instance == null){
            synchronized (PluginManager.class){
                if(instance == null){
                    instance = new PluginManager(context);
                }
            }
        }
        return instance;
    }

    private PluginManager(Context context) {
        mContext = context;
    }

    private DexClassLoader mClassLoader;
    private Resources mResources;

    public void loadPlugin(){
        try {
            File file = new File(Environment.getExternalStorageDirectory()+File.separator+"p.apk");
            if(!file.exists()){
                Log.i(TAG,"插件包不存在");
            }
            String pluginPath = file.getAbsolutePath();
            //创建classloader用来加载插件中的类
            //创建一个缓存目录 /data/data/包名/pDir
            File fileDir = mContext.getDir("pDir",Context.MODE_PRIVATE);

            mClassLoader = new DexClassLoader(pluginPath,fileDir.getAbsolutePath(),
                    null,mContext.getClassLoader());

            //创建resource用来加载插件中的资源
            //AssetManager 资源管理器 final修饰的不能new
            AssetManager assetManager = AssetManager.class.newInstance();
            //addAssetPath方法可以加载apk文件
            Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath",
                    String.class);
            addAssetPathMethod.invoke(assetManager, pluginPath);
            //拿到当前宿主的resource 用来回去当前应用的分辨率等信息
            Resources resources = mContext.getResources();
            //用来加载插件包中的资源
            mResources = new Resources(assetManager,resources.getDisplayMetrics(),resources.getConfiguration());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public DexClassLoader getClassLoader() {
        return mClassLoader;
    }

    public Resources getResources() {
        return mResources;
    }
}

首先是加载类,我们通过创建一个DexClassLoader来加载,创建DexClassLoader需要三个参数一个是插件包的路径,一个是缓存 /data/data/包名/pDir pDir是我们自己命名。和一个classloader。

然后是加载资源,通过创建Resources来加载资源,它需要三个参数,AssetManager ,分辨率信息和配置信息,分辨率信息和配置信息我们可以通过当前宿主中的Resources拿到。AssetManager可以通过反射执行它内部的addAssetPath方法来拿到。

然后我创建一个代理Activity

public class ProxyActivity extends Activity {

    @Override
    public Resources getResources() {
        return PluginManager.getInstance(this).getResources();
    }

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

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 插件里面的 Activity
        String className = getIntent().getStringExtra("className");
        //实例化插件包中的activity
        try {
            Class pluginClass = getClassLoader().loadClass(className);
            Constructor constructor = pluginClass.getConstructor(new Class[]{});
            Object pluginActivity = constructor.newInstance(new Object[]{});
            //强转为对应的接口
            ActivityInterface activityInterface = (ActivityInterface) pluginActivity;
            activityInterface.insertAppContext(this);

            Bundle bundle = new Bundle();
            bundle.putString("content","从宿主传过来");
            //执行插件中的方法
            activityInterface.onCreate(bundle);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

这个代理的Activity非常重要,它是一个真正的Activity,需要注册到manifest中,插件中的Activity最终都是通过它来展示。

  • 首先我们重写它里面的getResources和getClassLoader方法,返回我们工具类中自己定义的classloader和resource。
  • 然后在onCreate方法中,通过插件中需要启动的Activity的全类名来加载插件中的Activity。

  • 由于我们知道插件中的Activity都实现了ActivityInterface接口,所以这里我们就可以直接强转成ActivityInterface,

  • 最后调用ActivityInterface中的对应的生命周期方法即可。

那这个PluginActivity的全类名怎么来呢,在点击跳转到PorxyActivity的时候通过Intent传过来

下面我们来到MainActivity中加载插件并找到PluginActivity的全类名并跳转。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }


    public void loadPlugin(View view) {
        PluginManager.getInstance(this).loadPlugin();
    }

    public void startPlugin(View view) {
        //获取插件包中的activity的全类名
        File file = new File(Environment.getExternalStorageDirectory() + File.separator + "p.apk");
        String path = file.getAbsolutePath();

        // 获取插件包 里面的 Activity
        PackageManager packageManager = getPackageManager();
        PackageInfo packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
        ActivityInfo activityInfo = packageInfo.activities[0];


        Intent intent = new Intent(this,ProxyActivity.class);
        intent.putExtra("className",activityInfo.name);
        startActivity(intent);
    }
}

寻找PluginActivity的全类名,通过PackageManager 这个类,传入插件的路径最后通过getPackageArchiveInfo方法就可以拿到啦。ActivityInfo 中记录了manifest中所有的activity,因为我们插件的manifest中只注册一个Activity就可以了,所以直接取第0个就可以啦。

OK,到这里我们就可以顺利的从宿主的APP中跳转到插件APK中的PluginActivity了。

当然一个插件不能跳到插件的首页就完事了,插件有很多功能,内部也需要继续跳转到别的界面,插件内部怎么跳转呢,直接startActivity吗?当然不行啦,就跟前面的setContentView不能直接用一样,插件中是没有上下文环境的,而startActivity最终会进入到当前插件的Activity中,会报错,需要使用宿主传过来的环境,所以插件中的BaseActivity中还的需要重写startActivity方法。

public View findViewById(int layoutId) {
      return appActivity.findViewById(layoutId);
  }

  @Override
  public void startActivity(Intent intent) {

      Intent intentNew = new Intent();
      // PluginActivity 全类名
      intentNew.putExtra("className", intent.getComponent().getClassName()); 
      appActivity.startActivity(intentNew);
  }

当然findViewById这个方法内部也是通过上下文环境调用的,所以也需要重写,然后转化为宿主的环境来调用。主要注意的是,后面凡是用到上下文环境的方法,都需要重写,转化为宿主的环境,这也时占位式插件化的一个非常麻烦的地方,不过它的好处是比较稳定,相对于通过hook来做兼容性比较好。

PluginActivity中添加点击事件

findViewById(R.id.bt_start_activity).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(appActivity, Plugin2Activity.class));
       }
});

启动插件中的首页是启动了一个代理的Activity(ProxyActivity),而插件内部的跳转的本质就是在启动一个ProxyActivity,把当前要启动的Activity的全类名带过去,然后通过类加载,流程跟启动第一个Activity一样。

插件首页的Activity的全类名我们需要去manifest中拿,插件内部跳转就不用那么麻烦了,只需要通过intent就能拿到了。

所以我们需要在ProxyActivity中重写startActivity方法,拿到插件包中的Activity之后,自己跳自己,这样我们就能让插件中的一个新的Activity进栈出栈了,点击返回键可以返回上一个Activity。

@Override
   public void startActivity(Intent intent) {
       String className = intent.getStringExtra("className");
       Intent proxyIntent = new Intent(this,ProxyActivity.class);
       proxyIntent.putExtra("className",className);
       super.startActivity(proxyIntent);
   }

OK 这样就实现了跳转到插件首页和插件内部跳转的功能啦。下一篇来聊一下加载Service

把插件包打包成apk,放到手机根目录中

效果