转载请标明出处:https://blog.csdn.net/u013254166/article/details/80431288
本文出自: 【rhino博客】
一、宿主工程接入
1.在宿主工程根目录的build.gradle添加依赖,注意:我这里用的gradle时3.0,其他版本可能会有问题。
dependencies { classpath 'com.android.tools.build:gradle:3.0.0' classpath 'com.didi.virtualapk:gradle:0.9.8.3' }
2.在App的工程模块的build.gradle添加使用gradle插件
apply plugin: 'com.didi.virtualapk.host' ... dependencies { ... implementation 'com.didi.virtualapk:core:0.9.5' }
3.AppApplication.java类是继承了Application,覆写attachBaseContext函数,进行插件SDK初始化工作
package com.rhino.virtualapkdemo; import android.app.Application; import android.content.Context; import com.didi.virtualapk.PluginManager; /** * @author LuoLin * @since Create on 2018/5/23. */ public class AppApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); PluginManager.getInstance(base).init(); } }
4.在使用插件之前加载插件,可以根据具体业务场景选择合适时机加载,demo在MainActivity的onCreate时加载,加载需要权限READ_EXTERNAL_STORAGE,这里直接贴我的MainActivity.java。
package com.rhino.virtualapkdemo; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Environment; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; import com.didi.virtualapk.PluginManager; import com.didi.virtualapk.internal.LoadedPlugin; import java.io.File; /** * @author LuoLin * @since Create on 2018/5/24. */ public class MainActivity extends AppCompatActivity { /** 插件文件路径 **/ private static final String PLUGIN_FILE_PATH = Environment.getExternalStorageDirectory() + File.separator + "plugin_demo.apk"; /** 插件apk包名 **/ private static final String PLUGIN_PACKAGE_NAME = "com.rhino.virtualapkplugindemo"; /** 插件apk入口activity **/ private static final String PLUGIN_LAUNCHER_ACTIVITY = "com.rhino.virtualapkplugindemo.PluginMainActivity"; /** 请求权限请求码 **/ public static final int REQUEST_PERMISSION_CODE = 111; /** 权限 **/ public static final String[] PERMISSIONS = new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (lacksPermissions(PERMISSIONS)) { // 开始请求权限 requestPermissions(PERMISSIONS); } else { loadPlugin(); } findViewById(R.id.show_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(); intent.setClass(getApplicationContext(), SecondActivity.class); startActivity(intent); } }); findViewById(R.id.run_plugin).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showPluginActivity(); } }); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_PERMISSION_CODE) { loadPlugin(); } } /** 判断是否缺少权限 **/ private boolean lacksPermissions(String... permissions) { for (String permission : permissions) { if (ContextCompat.checkSelfPermission(getApplicationContext(), permission) == PackageManager.PERMISSION_DENIED) { return true; } } return false; } /** 请求权限 **/ private void requestPermissions(String... permissions) { ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSION_CODE); } /** 加载插件 **/ private void loadPlugin() { PluginManager pluginManager = PluginManager.getInstance(this); //此处是当查看插件apk是否存在,如果存在就去加载(比如修改线上的bug,把插件apk下载,达到热修复) File pluginApk = new File(PLUGIN_FILE_PATH); if (pluginApk.exists()) { try { pluginManager.loadPlugin(pluginApk); } catch (Exception e) { e.printStackTrace(); } } } /** 运行插件中activity **/ private void showPluginActivity() { if (PluginManager.getInstance(this).getLoadedPlugin(PLUGIN_PACKAGE_NAME) == null) { Toast.makeText(getApplicationContext(),"插件未加载,请尝试重启APP", Toast.LENGTH_SHORT).show(); return; } Intent intent = new Intent(); intent.setClassName(PLUGIN_PACKAGE_NAME, PLUGIN_LAUNCHER_ACTIVITY); startActivity(intent); } }
至此,VirtualAPK插件功能就集成到宿主中了,接下来,我们看下插件工程是如何集成的。
二、插件工程接入
1.在插件工程根目录的build.gradle添加依赖,注意:我这里用的gradle时3.0,其他版本可能会有问题。
dependencies { classpath 'com.android.tools.build:gradle:3.0.0' classpath 'com.didi.virtualapk:gradle:0.9.8.3' }
2.在App的工程模块的build.gradle添加使用gradle插件和插件配置信息,信息需要放在文件最下面。
apply plugin: 'com.didi.virtualapk.plugin' ... // 插件配置信息,放在文件最下面 virtualApk { packageId = 0x6f // 插件资源id,避免资源id冲突 targetHost='../../GitHub/VirtualApkDemo/app' // 宿主工程的路径 applyHostMapping = true // 插件编译时是否启用应用宿主的apply mapping }
解释一下上面3个参数的作用
- packageId用于定义每个插件的资源id,多个插件间的资源Id前缀要不同,避免资源合并时产生冲突
- targetHost指明宿主工程的应用模块,插件编译时需要获取宿主的一些信息,比如mapping文件、依赖的SDK版本信息、R资源文件,一定不能填错,否则在编译插件时会提示找不到宿主工程。
- applyHostMapping表示插件是否开启apply mapping功能。当宿主开启混淆时,一般情况下插件就要开启applyHostMapping功能。因为宿主混淆后函数名可能有fun()变为a(),插件使用宿主混淆后的mapping映射来编译插件包,这样插件调用fun()时实际调用的是a(),才能找到正确的函数调用。
3.生成插件,需要使用Gradle命令
gradle clean assemblePlugin 或者 gradlew clean assemblePlugin
执行成功后,会生成如下apk文件,即插件apk。
注意:这里的apk不能通过其他方式生成,如:run。
三、运行插件
1.将插件apk重命名为plugin_demo.apk(与宿主MainActiivty中加载apk名称保持一致),然后拷贝到sdcard根目录。
2.运行宿主apk,点击“进入插件中activity”,会发现成功启动了插件的activity。
四、测试实验
实验内容 |
现象 |
结论 |
宿主app加载插件apk文件,并启动插件中的Activity,以及activity是否需要在宿主manifest预注册? |
插件apk不需要安装,宿主app可以直接在代码中加载指定的插件apk文件,可以成功启动插件中activity,该activity没有在宿主中预注册。 |
插件不需要安装,动态加载后可以启动插件activity,插件activity不需要在宿主中预注册。(这里只测试了activity,四大组件也支持) |
修改插件ui界面,替换原来的apk文件,重启宿主app,观察修改能否及时生效? |
重启宿主app后,显示了修改后的界面。 |
支持插件apk文件动态替换。 |
宿主和插件中创建相同activity,观察启动的时候会加载哪一个activity? |
启动的是宿主中的activity。 |
插件中的class不能与宿主中class相同,加载的是宿主中class,不能做到替换。 |
宿主和插件中创建相同layout,观察宿主和插件分别会加载哪一个layout? |
宿主和插件都加载宿主中layout。 |
Layout文件名称不能和宿主中重名,加载的是宿主中layout,不能做到替换,对于其他资源文件也是一样。 |
五、与其他插件化
Atlas:https://alibaba.github.io/atlas/
VirtualAPK:https://github.com/didi/VirtualAPK/wiki
RePlugin:https://github.com/Qihoo360/RePlugin/wiki
框架 | Atlas | RePlugin | VirtualAPK |
定义 | Atlas是伴随着手机淘宝的不断发展而衍生出来的一个运行于Android系统上的一个容器化框架,我们也叫动态组件化(Dynamic Bundle)框架。 | RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案。 | VirtualAPK是滴滴出行自研的一款优秀的插件化框架。 |
功能 | 1.远程bundle 2.插件以so形式,放在sdcard,加载后会自动删除。 3.热修复,可以不升级apk就实现宿主和组件的更新。 |
1.远程bundle 2.插件以apk形式放在sdcard,加载后还在会自动备份apk到缓存目录。 |
1.远程bundle 2.插件以apk形式放在sdcard,加载后还在,删除会打不开。 |
更新插件方式 | 必须要和宿主一起,打差异补丁才能更新。 | 直接通过下载一个新的插件apk,然后调安装方法就能实现插件的更新。 | 直接通过下载一个新的插件apk,然后调安装方法就能实现插件的更新。 |
插件独立性 | 和宿主的依赖还是挺多,毕竟官方也强调是组件化,不是插件化。 | 插件不用声明和宿主的联系,所以你生成一个插件后,这个插件可以给其他宿主调用。 | 插件是一个独立的apk,但插件里面也定义和宿主的关联,就是说这个插件apk并不能给其他宿主用,只能给插件里面声明的那个宿主使用。 |
六、结论
VirtualAPK不支持对宿主进行热修复。如果app需要热更新和插件的功能,推荐使用Atlas;如果插件无法很好地从宿主中拆出来,或者插件的运行和宿主有较多的交互,推荐使用VirtualAPK;如果同一个插件希望其他宿主也能用的话,那就只能RePlugin了,RePlugin就像一个应用市场,宿主仅仅是一个壳,然后把需要的插件下载加载使用就行,更新的话也无需更新宿主,直接更新插件就行。
- VirtualAPK首页:点击查看
- VirtualAPK 的源码下载地址:点击查看
- VirtualAPK最新动态:VirtualAPK 0.9.5 发布,滴滴 Android 插件化框架