占位式插件化之加载Activity

在一些大型的项目中,常常会用到插件化,插件化的优势有很多,即插即用,把不一样的功能打包成不一样的APK文件,经过网络下发到APP端,直接就可使用,不用经过应用市场便可随时增长新功能,很是适用于功能多又须要敏捷开发的应用java

能够实现插件化的方式有不少种,本系列先经过占位式的方法来实现。编程

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

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

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

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

OK说了一大堆,如今开始干吧,先撸一个加载Activity的工具

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

先来看看Activity的接口标准:spa

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/包名/pDirpDir是咱们本身命名。和一个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,放到手机根目录中

效果

相关文章
相关标签/搜索