Android跨平台动态化系列(二)-插件化之占位式插件化

1,267 阅读13分钟

前言

       本系列文章我打算以动态化的方案为主线写,第一个方案就是Native动态化,接下来几篇我都会来介绍Native动态化。在Native动态化方案中主要分为热修复、插件化和布局动态化两种大类,本篇文章及接下来几篇文章我打算好好介绍一下插件化。

一、插件化和热更新

1、1 概念

  • 插件化:App的一些功能模块不以传统的方式打进apk文件中,以另一种形式二次封装进app内部,或者放在网络上适时下载,这样子在有需要时可以动态的对这些功能模块进行加载,称此为插件化。初始安装的apk称为宿主,单独进行二次封装的功能模块apk称为插件
  • 热更新:通俗意义上讲就是不安装新版本的软件,直接从网络下载新功能模块来对软件进行局部更新。

1.2 区别和联系

       插件化的原理其实很简单就是动态加载,通过自定义ClassLoader来加载新的dex文件,从而让程序员原本没有的类可以被使用。当然里面的具体实现还要考虑很多,例如四大组件需要注册才可以,这些后面详细分析插件化方案的时候再细细的讲。至于插件化的作用,可以减少安装包大小,还可以动态部署。

       热更新现在有两个流派,第一是Native流派:代表有阿里的DexposedAndFix与腾讯的内部方案KKFix;另一个是Java流派,代表有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix、 饿了么的amigo、美团的robust以及微信的Tinker。其原理大概就是ClassLoader的dex替换,和直接修改字节码等。

       热更新和插件化的区别:一是插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动;二是插件化在代码中有固定的入口,而热更新则可能改变任何⼀个位置的代码。至于他们两个具体的分析,后面的文章进行详细分析。

二、 占位式插件化

2.1 原理

       占位式插件化和其他的插件化方案不太一样,他没有对android系统的底层方法进行Hook,而是从上层,也就是app应用层解决问题。通过创建一个ProxyXXX的代理类,由他进行分发。由于插件apk是没有安装的,也就是插件apk没有组件的一些环境,比如context上下文对象之类的,如果要用到这些环境就必须依赖宿主的环境运行。所以我们就要宿主跟插件之间定义一个标准,用来传递宿主中的环境给插件。在这里一般的插件中我们需要代理的无非就是四大组件,但是其中的ContentProvider这里不需要做代理,所以我们只需要代理ActivityService、、BroadcastReceiver,下面就分别以这三个代理来进行讲解。

2.2 占位Activity

       首先我们需要在标准中建立一个Activity的标准,如下:

public interface ActivityInterface {
    /**
     * 把宿主(app)的环境  给  插件
     * @param appActivity
     */
    void insertAppContext(Activity appActivity);
    // 生命周期方法
    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onDestroy();
}

       上面这个就是插件需要遵循的一个规范,在插件apk中的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);
    }

    public View findViewById(int layoutId) {
        return appActivity.findViewById(layoutId);
    }
    
   @Override
    public void startActivity(Intent intent) {
        Intent intentNew = new Intent();
        intentNew.putExtra("className", intent.getComponent().getClassName());
        appActivity.startActivity(intentNew);
    }
}

       这里需要注意的就是,需要使用到 “this” 的地方都需要用通过注入的activity来进行替换,这里的原因很简单,在插件中的Activity是没有在AndroidManifest文件中注册,而通过注入的Activity是在宿主的activity中进行注册的,也就是占位的Activity-ProxyActivity。这个activity就是我们代理的Activity,他是插件的入口,需要提前在宿主Activity中进行注册,接下来我们看看这个Activity,如下:

// 代理的Activity 代理/占位 插件里面的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");

        try {
            Class mPluginActivityClass = getClassLoader().loadClass(className);
            // 实例化 插件包里面的 Activity
            Constructor constructor = mPluginActivityClass.getConstructor(new Class[]{});
            Object mPluginActivity = constructor.newInstance(new Object[]{});

            ActivityInterface activityInterface = (ActivityInterface) mPluginActivity;

            // 注入
            activityInterface.insertAppContext(this);

            Bundle bundle = new Bundle();
            bundle.putString("appName", "我是宿主传递过来的信息");

            // 执行插件里面的onCreate方法
            activityInterface.onCreate(bundle);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Override
    public void startActivity(Intent intent) {
        String className = intent.getStringExtra("className");

        Intent proxyIntent = new Intent(this, ProxyActivity.class);
        proxyIntent.putExtra("className", className); // 包名+TestActivity

        // 要给TestActivity 进栈
        super.startActivity(proxyIntent);
    }
}

       这里可以看到,我们对getResourcesgetClassLoader进行了重写,首先说明一下为什么进行重写,因为当我们跳转到插件的apk中时加载的是插件中的apk所以我们需要自定义ClassLoader去加载插件apk中的class,另外我们插件中的Activity加载的资源也是插件中的资源,所以在这里也需要我们去重写getResources方法,去加载插件中的插件。这里我们重写这个方法,具体的实现放在了一个PluginManager的单例类中,这里主要是方便管理后面的ServiceBroadcastReceiver的代理方法具体实现我也会放在里面,那么下面我们具体的看看这个类的方法,如下:

 class PluginManager {

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

    private static PluginManager pluginManager;

    private Context context;

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

    public PluginManager(Context context) {
        this.context = context;
    }

    private DexClassLoader dexClassLoader;
    private Resources resources;

    /**
     * (2.1 Activity class,  2.2layout)
     * 加载插件
     */
    public void loadPlugin() {
        try {
            File file = new File("说明:具体的插件包全路径名");
            if (!file.exists()) {
                Log.d(TAG, "插件包 不存在...");
                return;
            }
            String pluginPaht = file.getAbsolutePath();

            /**
             *   下面是加载插件里面的 class
             */

            // dexClassLoader需要一个缓存目录   /data/data/当前应用的包名/pDir
            File fileDir = context.getDir("pDir", Context.MODE_PRIVATE);

            // Activity class
            dexClassLoader = new DexClassLoader(pluginPaht, fileDir.getAbsolutePath(), null, context.getClassLoader());


            /**
             *   下面是加载插件里面的layout
             */
            // 加载资源
            AssetManager assetManager = AssetManager.class.newInstance();

            // 我们要执行此方法,为了把插件包的路径 添加进去
            // public final int addAssetPath(String path)
            Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class); // 他是类类型 Class
            addAssetPathMethod.invoke(assetManager, pluginPaht); // 插件包的路径   pluginPaht

            Resources r = context.getResources(); // 宿主的资源配置信息

            // 特殊的 Resources,加载插件里面的资源的 Resources
            resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration()); // 参数2 3  资源配置信息

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

    public ClassLoader getClassLoader() {
        return dexClassLoader;
    }

    public Resources getResources() {
        return resources;
    }
}

       上面的代码注释也写的很清楚,主要是一个loadPlugin方法,这个方法创建了一个自定义的ClassLoad和加载了插件apk中的资源文件。那么下面我们写一个方法来具体的模拟一下这个跳转插件apk中界面的例子,如下:

 // 启动插件里面的Activity
    public void startPluginActivity(View view) {
        File file = new File("说明:具体的插件包全路径名");
        String path = file.getAbsolutePath();

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

        // 占位  代理Activity
        Intent intent = new Intent(this, ProxyActivity.class);
        intent.putExtra("className", activityInfo.name);
        startActivity(intent);
    }

       这里我具体的说一下,我们跳转的时候具体的是跳转到ProxyActivity这个代理Activity中,但是我们以 “className” 这个为key传了需要具体跳转的Activity全类名。在我们先前的ProxyActivity中的onCreate方法中我们可以看到,他会先得到这个类名通过反射创建真实的Activity,然后通过我们自定义的ClassLoad进行类加载,紧接着会调用真实Activity的onCreate方法。另外具体的插件中的activity中的相互跳转,其实也是通过intent来进行全类名传递,通过同样的方式进行跳转,但是其实就是ProxyActivity跳转ProxyActivity,具体的startActivity重写,可以看上面的源码,下面我们就以衣服图来说明这个过程,如下:

2.3 占位Service

       这里其实占位Service的套路和前面的占位Activity的模式很相似,首先是建议一个Service的标准,如下:

public interface ServiceInterface {
    /**
     * 把宿主(app)的环境  给  插件
     * @param appService
     */
    void insertAppContext(Service appService);

    public void onCreate();

    public int onStartCommand(Intent intent, int flags, int startId);

    public void onDestroy();

}

       这里需要说明的是,我们这个标砖中的接口方法是根据自身所需要的原生组件中的方法来写的,你需要用到啥方法就在这里面来进行声明,相应的调用放在占坑(代理)组件中进行调用即可。那么下面我们看看插件中是继承这个标准,代码如下:

public class BaseService extends Service implements ServiceInterface {

    public Service appService;

    /**
     * 把宿主(app)的环境  给  插件
     * @param appService
     */
    public void insertAppContext(Service appService){
        this.appService = appService;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {

    }

    @SuppressLint("WrongConstant")
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return 0;
    }

    @Override
    public void onDestroy() {

    }
}

       这里插件中的Service使用和先前的Activity一样,需要注意的就是,需要使用 “this” 的地方用注入进来的appService来提换,这里我们看看占坑的Service,如下:

public class ProxyService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        String className = intent.getStringExtra("className");

        // com.netease.plugin_package.TestService

        try {
            Class mTestServiceClass = PluginManager.getInstance(this).getClassLoader().loadClass(className);
            Object mTestService = mTestServiceClass.newInstance();

            ServiceInterface serviceInterface = (ServiceInterface) mTestService;

            // 注入 组件环境
            serviceInterface.insertAppContext(this);

            serviceInterface.onStartCommand(intent, flags, startId);

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

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

       其实这个占坑的Service里面的实现很简单,就是在onStartCommand方法中,解析传来的真实Service的全类名,然后相同的套路用我们实现的自定义ClassLoad去加载,然后在每一个组件方法中调用标准中定义的方法。下面我们看看startService的重写,这里我们加在ProxyActivity中,代码如下:

    @Override
    public ComponentName startService(Intent service) {
        String className = service.getStringExtra("className");
        Intent intent = new Intent(this, ProxyService.class);
        intent.putExtra("className", className);
        return super.startService(intent);
    }

//具体的使用
  startService(new Intent(appActivity, TestService.class));

       这里可以看到,其实代理Service其实和代理Activity差不多,我们这里也用一幅图来说明这女过程,如下:

2.4 占位BroadcastReceiver

2.4.1 占位动态BroadcastReceiver

       占位BroadcastReceiver分为动态广播和静态广播,动态广播的代理很容易实现,和前面的两种方式一致,主要是静态广播的占位方式麻烦一点,这里我们先看看动态广播的占方式,如下先建立标准:

 public interface ReceiverInterface {
    public void onReceive(Context context, Intent intent);
}

       这根据自己的需求添加方法,接下来看看插件中具体的实现,如下:

public class TestReceiver extends BroadcastReceiver implements ReceiverInterface {

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "我是插件里面的广播接收者,我收到广播了", Toast.LENGTH_SHORT).show();
    }
}

       下面看看宿主中的占坑,具体实现如下:

// 能够接收的 广播接收者
public class ProxyReceiver extends BroadcastReceiver {
    // 插件里面的 MyReceiver 全类名
    private String pluginMyReceiverClassName;

    public ProxyReceiver(String pluginMyReceiverClassName) {
        this.pluginMyReceiverClassName = pluginMyReceiverClassName;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // 加载插件里面的 MyReceiver
        try {
            Class mMyRecevierClass = PluginManager.getInstance(context).getClassLoader().loadClass(pluginMyReceiverClassName);
            // 实例化class
            Object mMyReceiver = mMyRecevierClass.newInstance();
            ReceiverInterface receiverInterface = (ReceiverInterface) mMyReceiver;
            // 执行插件里面的 MyReceiver onReceive方法
            receiverInterface.onReceive(context, intent);

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

    }
}

       这里还是和上面一样的套路,我这里就不过多的介绍了,下面我们在ProxyActivity中他的发送和接收实现,具体的源码如下:

    @Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        String pluginMyReceiverClassName = receiver.getClass().getName(); // MyReceiver的全类名

        // 在宿主 注册广播
        return super.registerReceiver(new ProxyReceiver(pluginMyReceiverClassName), filter);
    }

    @Override
    public void sendBroadcast(Intent intent) {
        super.sendBroadcast(intent); // 发送
    }

       其实这里就是在宿主中注册和发送,然后代理给插件,我们还是用一幅图来说明,具体如下:

2.4.2 占位静态BroadcastReceiver

       占位静态BroadcastReceiver,其实这个需要实现的方法最少但是,实现起来最困难,他只需要我们解析出在插件apk中AndroidManifest中静态注册的BroadcastReceiver,然后注册到宿主中的一个context中即可,这里的思路就是这样子简单,但是我们来看一下具体的实现,我们只需要在PluginManager添加一个方法来完成此过程,具体的源码如下:

// 反射系统源码,来解析 apk 文件里面的 所有信息
    public void parserApkAction() {
        try {
            // 插件包路径
            File file = new File(Environment.getExternalStorageDirectory() + File.separator + "p.apk");
            if (!file.exists()) {
                Log.d(TAG, "插件包 不存在...");
                return;
            }
            String pluginPaht = file.getAbsolutePath();

            // 实例化 PackageParser对象
            Class mPackageParserClass = Class.forName("android.content.pm.PackageParser");
            Object mPackageParser = mPackageParserClass.newInstance();

            // 1.执行此方法 public Package parsePackage(File packageFile, int flags),就是为了,拿到Package

            Method mPackageParserMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class); // 类类型
            Object mPackage = mPackageParserMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);  // 执行方法

            // 继续分析 Package
            // 得到 receivers
            Field receiversField = mPackage.getClass().getDeclaredField("receivers");
            // receivers 本质就是 ArrayList 集合
            Object receivers = receiversField.get(mPackage);

            ArrayList arrayList = (ArrayList) receivers;

            // 此Activity 不是组件的Activity,是PackageParser里面的内部类

            for (Object mActivity : arrayList) { // mActivity --> <receiver android:name=".StaticReceiver">

                // 获取 <intent-filter>    intents== 很多 Intent-Filter
                // 通过反射拿到 intents
                Class mComponentClass = Class.forName("android.content.pm.PackageParser$Component");
                Field intentsField = mComponentClass.getDeclaredField("intents");
                ArrayList<IntentFilter> intents = (ArrayList) intentsField.get(mActivity); // intents 所属的对象是谁 ?

                // 我们还有一个任务,就是要拿到 android:name=".StaticReceiver"
                // activityInfo.name; == android:name=".StaticReceiver"
                // 分析源码 如何 拿到 ActivityInfo

                Class mPackageUserState = Class.forName("android.content.pm.PackageUserState");

                Class mUserHandle = Class.forName("android.os.UserHandle");
                int userId = (int) mUserHandle.getMethod("getCallingUserId").invoke(null);



                /**
                 * 执行此方法,就能拿到 ActivityInfo
                 * public static final ActivityInfo generateActivityInfo(Activity a, int flags,
                 *             PackageUserState state, int userId)
                 */
               Method  generateActivityInfoMethod = mPackageParserClass.getDeclaredMethod("generateActivityInfo", mActivity.getClass()
                        , int.class, mPackageUserState, int.class);
               generateActivityInfoMethod.setAccessible(true);
               // 执行此方法,拿到ActivityInfo
               ActivityInfo mActivityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null,mActivity, 0, mPackageUserState.newInstance(), userId);
               String receiverClassName = mActivityInfo.name; // com.netease.plugin_package.StaticReceiver

                Class mStaticReceiverClass = getClassLoader().loadClass(receiverClassName);

                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) mStaticReceiverClass.newInstance();

                for (IntentFilter intentFilter : intents) {

                    // 注册广播
                    context.registerReceiver(broadcastReceiver, intentFilter);

                }
            }

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

       这里的具体每一步骤都有详细的注释,我只说一下他的具体思路,其实就是借鉴了源码解析AndroidManifest文件的方式,解析出静态注入的Receiver类,紧接着实例化他,还解析出xml中的intent-filter,动态注入实例化类中,最后通过动态注入的方式注入这个广播,其实上面的代码还是很复杂的,需要细细的分析。下面我们空过两个函数来看看具体的使用,如下:

  // 注册 插件里面 配置的 静态广播接收者
    public void loadStaticReceiver(View view) {
        PluginManager.getInstance(this).parserApkAction();
    }

    // 发送静态广播
    public void sendStaticReceiver(View view) {
        Intent intent = new Intent();
        intent.setAction("静态注入的Action");
        sendBroadcast(intent);
    }

       上面的实现就先讲到这里,上面的代码其实涉及到系统对Apk解析原理过程,还有上面所有代理都用到的ClassLoad类加载机制,这些基础的原理,我想放在后面更为复杂的插件化方案中详细进行介绍。

三、 总结

       这篇文章是插件化方案中最简单的一种方式,其实整个方式就是一个代理模式,当然其中也有复杂的部分例如静态广播的解析。这篇文章留下了很多比较深入的知识,包括类加载机制、apk的安装、解析和启动这些,我将放在后面的插件化方案的解析中,他们用到的这些知识点比较深入,后面的还会有更深入的知识,例如Hook机制等,请期待。

#参考资料