Android使用代理+接口实现插件化 仿支付宝微信加载未安装的第三方应用apk

Android使用代理+接口实现插件化 仿支付宝微信加载未安装的第三方应用apk-蒲公英云

插件化

  • 前言
  • 什么是插件化
  • 插件化发展历程
  • 为什么需要插件化
  • 插件化的难点
  • 插件化需要掌握的技术
  • 实践
    • 总体逻辑
    • 加载dex,res等资源文件
    • 生命周期管理
    • 插件Activity编写
    • 宿主Activity编写

前言

在上篇文章Android热修复与插件化之–ClassLoader(类加载器)详解和双亲委派模型以及如何自定义类加载器详细介绍了ClassLoader原理,为今天的插件化开发做了些铺垫;可能很多人没有接触过插件化,但是你的生活中肯定有用到过与插件化技术有关的应用,看下图

想必大家能看出来,图一是支付宝的应用界面,图二是微信的应用界面,这两个APP都集成了很多第三方应用,那你有没有想过它们是怎么集成这么多应用的呢?难道是将这么多应用的代码与主应用一起打包成APP?这想想感觉就不可能,这么多的代码和资源文件,这支付宝和微信的APK体积不得上天;难不成是需要用户手机里安装这些APP,然后跳转?这也是不对的,我们在使用的时候都是在主应用内跳转的;那它们到底是怎么做到的呢?这就涉及到今天所讲的插件化技术了

什么是插件化

有插件一般都会伴随着宿主的存在,宿主APK实现了一套插件的加载和管理的框架,它作为应用的主工程存在,插件APK是依附于宿主APK存在的

插件也称为Plug-in,或者add-in,俗称外挂,是宿主应用的功能扩展,对宿主来说可有可无,但是一定程度上能提高宿主应用的高可用性;插件化就是在Android开发领域,在不改变宿主应用的情况下,通过插件动态扩展应用功能,在运行时将功能植入到应用系统中

正因如此,支付宝微信等这些大厂的应用才能集成如此之多的第三方应用

插件化发展历程

  • 2012年7月,AndroidDynamicLoader,大众点评,陶毅敏:思想是通过Fragment以及schema的方式实现的,这是一种可行的技术方案,但是还有限制太多,这意味这你的activity必须通过Fragment去实现,这在activity跳转和灵活性上有一定的不便,在实际的使用中会有一些很奇怪的bug不好解决,总之,这还是一种不是特别完备的动态加载技术。
  • 2013年,23Code,自定义控件的动态下载:主要利用 Java ClassLoader 的原理,可动态加载的内容包括 apk、dex、jar等。
  • 2014年初,Altas,阿里伯奎的技术分享:提出了插件化的思想以及一些思考的问题,相关资料比较少。
  • 2014年底,Dynamic-load-apk,任玉刚:动态加载APK,通过Activity代理的方式给插件Activity添加生命周期。
  • 2015年4月,OpenAltas/ACCD:Altas的开源项目,一款强大的Android非代理动态部署框架,目前已经处于稳定状态。
  • 2015年8月,DroidPlugin,360的张勇:DroidPlugin 是360手机助手在 Android 系统上实现了一种新的插件机制:通过Hook思想来实现,它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。
  • 2015年9月,AndFix,阿里:通过NDK的Hook来实现热修复。
  • 2015年11月,Nuwa,大众点评:通过dex分包方案实现热修复。
  • 2015年底,Small,林光亮:打通了宿主与插件之间的资源与代码共享。
  • 2016年4月,ZeusPlugin,掌阅:ZeusPlugin最大特点是:简单易懂,核心类只有6个,类总数只有13个
  • 2017年6月,VirtualAPK,滴滴开源了自研的插件化框架,该框架功能完备,支持Android四大组件,良好的兼容性,且入侵性较低

为什么需要插件化

插件化技术听起来很美好,但是要注意的一个问题就是使用插件化技术的APP是不能在Google Play上线的,因为Google是禁止开发商这种行为的;为什么呢?防止开发商窃取用户隐私!想想你一个做闹钟的应用,通过Google应用商店审核用户安装后,通过插件化技术给自己的APP增加了额外的功能,这就有很大的风险了,接下来你想做什么Google就不能控制其风险了

再者说这种技术有点钻操作系统空子,与原生系统对着干的意思,但是在国内特殊环境下,却发展的挺火热的,有时还成为了一道技术面试的门槛,可能这就是XXX特色吧,为什么会这样呢?

  • 第一个原因还是国内Android碎片化太严重,没有统一的应用分发市场,还有因为国内的信息政策,Google Play并不能起作用,也就谈不上对应用进行风险监测了;然后各大应用发布渠道良莠不齐,你懂的
  • 以前App是受到方法数的限制的(65535),通过插件化可以解决这一问题,但是Google后来推出了Multidex方案,即分包;如果你的应用仅仅只是为了突破65535,那不需要使用插件化,使用分包可以解决
  • 像上面说的支付宝微信,当然还有美团,滴滴等应用,它们的功能非常多,如果都将代码和资源都打包到一个APK中,显然这个APK的大小可能会突破用户承受的极限,如果将其中的很多功能通过插件动态下发可以解决这个问题
  • 动态控制功能使用情况,比如应用的某个功能模块突然出了bug或者因为什么政策需要停止使用,那就可以在线修改插件的使用权限,强制停止用户使用该插件的功能
  • 快速发布新功能,比如应用需要临时新增某项业务,这时候你的APP才刚升级,那再继续升级,显然对用户会是一个考验,同时该功能也不能及时的让用户使用到,使用插件化就可以做到
  • 可以按需加载不同的模块,实现灵活的功能配置,不同插件之间是相互独立的,可以很容易做到根据业务需求实现插件的热插拔

插件化的难点

  • 插件与宿主间通信
  • 插件Activity的生命周期管理和组件注册
  • 插件APK中资源与代码的加载
  • 插件APK与宿主APK资源引用的冲突

插件化需要掌握的技术

  • ClassLoader加载原理
  • Android资源加载与管理
  • Activity启动流程
  • 四大组件的加载与管理
  • Java反射原理
  • so库的加载原理
  • Gradle打包原理
  • 清单文件的合并处理

实践

这里在Andriod开发–如何实现组件化开发以及解决ButterKnife报错,了解一下文章的基础上继续开发;下面给出的插件化实现只是众多方案中的一种

场景介绍:首先用户安装了一个宿主APK,这里就仿微信了,在第四个tab页面,如下:


接着点击滴滴出行,这时候就需要去服务端下载滴滴APK到本地,然后加载对应页面

总体逻辑

接下来就进行实战了,首先以一幅图来展示下代码实现逻辑


工程目录如图


这里就把插件APK放在工程里一起开发了,主界面几个tab也是module,只不过是library形式

加载dex,res等资源文件

上图里说的很清楚了,加载一个APK需要加载dex文件,加载资源文件,所以就需要DexClassLoader,Resource,AssetManager等几个对象,所以就对封装一个APK加载对象,如下

 
  1. public class PluginApk {
  2. private PackageInfo mPackageInfo;
  3. private DexClassLoader mClassLoader;
  4. private Resources mResources;
  5. private AssetManager mManager;
  6. public PluginApk(PackageInfo mPackageInfo, DexClassLoader mClassLoader, Resources mResources) {
  7. this.mPackageInfo = mPackageInfo;
  8. this.mClassLoader = mClassLoader;
  9. this.mResources = mResources;
  10. if (mResources != null) {
  11. mManager = mResources.getAssets();
  12. }
  13. }
  14. }

接下来就需要创建PluginApk对象,定义一个插件管理类来实例化它

 
  1. public class PluginManager {
  2. private static class Instance{
  3. static final PluginManager INSTANCE = new PluginManager();
  4. }
  5. private PluginManager() {
  6. }
  7. public static PluginManager getInstance(){
  8. return Instance.INSTANCE;
  9. }
  10. private Context mContext;
  11. private PluginApk mPluginApk;
  12. public void init(Context context){
  13. //避免单例对象引起内存泄漏
  14. mContext = context.getApplicationContext();
  15. }
  16. /**
  17. * 根据APK 路径实例化PluginApk对象
  18. * @param path
  19. */
  20. public void loadPluginApk(String path){
  21. PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(path,
  22. PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
  23. if (packageInfo == null) {
  24. return;
  25. }
  26. DexClassLoader classLoader = createDexClassLoader(path);
  27. AssetManager assetManager;
  28. try {
  29. assetManager = createAssetManager(path);
  30. } catch (Exception e) {
  31. e.printStackTrace();
  32. return;
  33. }
  34. Resources resources = createResource(assetManager);
  35. mPluginApk = new PluginApk(packageInfo,classLoader,resources);
  36. }
  37. public PluginApk getPluginApk(){
  38. return mPluginApk;
  39. }
  40. /**
  41. * 创建访问插件APK dex文件的类加载器
  42. * @param path
  43. * @return
  44. */
  45. private DexClassLoader createDexClassLoader(String path) {
  46. /**
  47. * 在宿主APK的内部存储中的data/data/包名 目录上创建一个文件夹,存放优化后的文件
  48. */
  49. File file = mContext.getDir("odex",Context.MODE_PRIVATE);
  50. return new DexClassLoader(path,file.getAbsolutePath(),null,mContext.getClassLoader());
  51. }
  52. private AssetManager createAssetManager(String path) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
  53. /**
  54. * AssetManager的构造方和addAssetPath方法都是hide的,需要使用反射构造
  55. */
  56. AssetManager assetManager = AssetManager.class.newInstance();
  57. Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
  58. method.invoke(assetManager,path);
  59. return assetManager;
  60. }
  61. /**
  62. * 创建访问插件APK资源的Resource
  63. * @param assetManager
  64. * @return
  65. */
  66. private Resources createResource(AssetManager assetManager) {
  67. Resources resources = mContext.getResources();
  68. return new Resources(assetManager,resources.getDisplayMetrics(),resources.getConfiguration());
  69. }
  70. }

生命周期管理

接下来就是加载Activity了,这里通过代理的形式加载插件Activity

 
  1. public class ProxyActivity extends AppCompatActivity {
  2. private String mActivityClassName;
  3. private PluginApk mPluginApk;
  4. @Override
  5. protected void onCreate(@Nullable Bundle savedInstanceState) {
  6. super.onCreate(savedInstanceState);
  7. mActivityClassName = getIntent().getStringExtra("classname");
  8. mPluginApk = PluginManager.getInstance().getPluginApk();
  9. lunchActivity();
  10. }
  11. private void lunchActivity() {
  12. if (mPluginApk == null) {
  13. throw new NullPointerException("未获取到插件APK");
  14. }
  15. DexClassLoader classLoader = mPluginApk.getmClassLoader();
  16. try {
  17. Class clazz = classLoader.loadClass(mActivityClassName);
  18. //将Activity加载到内存中了
  19. Object o = clazz.newInstance();
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }

但是这样加载进来的Activity并没有生命周期,所以就需要先定义一套规则,这样插件里的Activity才能进行生命周期方法的回调

规则如下

 
  1. public interface IPluginActivity {
  2. //内部跳转
  3. int FORM_INTERNAL = 1;
  4. //外部跳转
  5. int FROM_EXTERNAL = 2;
  6. void attach(Activity proxyActivity);
  7. void onCreate(Bundle savedInstanceState);
  8. void onStart();
  9. void onRestart();
  10. void onResume();
  11. void onPause();
  12. void onStop();
  13. void onDestroy();
  14. void onActivityResult(int requestCode, int resultCode, Intent data)
  15. }

这样ProxyActivity就可以这样写了

 
  1. public class ProxyActivity extends AppCompatActivity {
  2. private String mActivityClassName;
  3. private PluginApk mPluginApk;
  4. private IPluginActivity mIPluginActivity;
  5. @Override
  6. protected void onCreate(@Nullable Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. mActivityClassName = getIntent().getStringExtra("classname");
  9. mPluginApk = PluginManager.getInstance().getPluginApk();
  10. lunchActivity();
  11. }
  12. private void lunchActivity() {
  13. if (mPluginApk == null) {
  14. throw new NullPointerException("未获取到插件APK");
  15. }
  16. DexClassLoader classLoader = mPluginApk.getmClassLoader();
  17. try {
  18. Class clazz = classLoader.loadClass(mActivityClassName);
  19. //将Activity加载到内存中了
  20. Object o = clazz.newInstance();
  21. //判断要跳转到的插件Activity是否实现了规则接口
  22. if (o instanceof IPluginActivity) {
  23. mIPluginActivity = (IPluginActivity) o;
  24. //赋予插件Activity上下文信息
  25. mIPluginActivity.attach(this);
  26. Bundle bundle = new Bundle();
  27. //表明是由宿主Activity跳转过去的
  28. bundle.putInt("from",IPluginActivity.FROM_INTERNAL);
  29. //回调插件Activity的onCreate方法,使其具有生命周期回调
  30. mIPluginActivity.onCreate(bundle);
  31. }
  32. } catch (Exception e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. @Override
  37. protected void onStart() {
  38. //回调插件Activity的onStart方法
  39. mIPluginActivity.onStart();
  40. super.onStart();
  41. }
  42. @Override
  43. protected void onRestart() {
  44. mIPluginActivity.onRestart();
  45. super.onRestart();
  46. }
  47. @Override
  48. protected void onResume() {
  49. mIPluginActivity.onResume();
  50. super.onResume();
  51. }
  52. @Override
  53. protected void onPause() {
  54. mIPluginActivity.onPause();
  55. super.onPause();
  56. }
  57. @Override
  58. protected void onStop() {
  59. mIPluginActivity.onStop();
  60. super.onStop();
  61. }
  62. @Override
  63. protected void onDestroy() {
  64. mIPluginActivity.onDestroy();
  65. super.onDestroy();
  66. }
  67. @Override
  68. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  69. mIPluginActivity.onActivityResult(requestCode,resultCode,data);
  70. super.onActivityResult(requestCode, resultCode, data);
  71. }
  72. /**
  73. * 下面这三个对象当前Activity是不具备的
  74. * 需要返回PluginApk对象的
  75. * @return
  76. */
  77. @Override
  78. public Resources getResources() {
  79. return mIPluginActivity == null ? super.getResources() : mPluginApk.getmResources();
  80. }
  81. @Override
  82. public AssetManager getAssets() {
  83. return mIPluginActivity == null ? super.getAssets() : mPluginApk.getmManager();
  84. }
  85. @Override
  86. public ClassLoader getClassLoader() {
  87. return mIPluginActivity == null ? super.getClassLoader() : mPluginApk.getmClassLoader();
  88. }
  89. }

接下来还需要编写一个插件Activity的父类,以处理跳转过来的是宿主APK中的Activity还是插件APK中的Activity

 
  1. public class PluginActivityImpl extends AppCompatActivity implements IPluginActivity {
  2. private int from = FROM_INTERNAL;
  3. //赋予当前Activity上下文
  4. private Activity mProxyActivity;
  5. @Override
  6. public void attach(Activity proxyActivity) {
  7. mProxyActivity = proxyActivity;
  8. }
  9. @Override
  10. public void onCreate(Bundle savedInstanceState) {
  11. if (savedInstanceState != null) {
  12. from = savedInstanceState.getInt("from");
  13. }
  14. if (from == FROM_INTERNAL) {
  15. super.onCreate(savedInstanceState);
  16. mProxyActivity = this;
  17. }
  18. }
  19. @Override
  20. public void setContentView(int layoutResID) {
  21. if (from == FROM_INTERNAL) {
  22. super.setContentView(layoutResID);
  23. } else {
  24. mProxyActivity.setContentView(layoutResID);
  25. }
  26. }
  27. @Override
  28. public View findViewById(int id) {
  29. if (from == FROM_INTERNAL) {
  30. return super.findViewById(id);
  31. } else {
  32. return mProxyActivity.findViewById(id);
  33. }
  34. }
  35. @Override
  36. public void onStart() {
  37. if (from == FROM_INTERNAL) {
  38. super.onStart();
  39. }
  40. }
  41. @Override
  42. public void onRestart() {
  43. if (from == FROM_INTERNAL) {
  44. super.onRestart();
  45. }
  46. }
  47. @Override
  48. public void onResume() {
  49. if (from == FROM_INTERNAL) {
  50. super.onResume();
  51. }
  52. }
  53. @Override
  54. public void onPause() {
  55. if (from == FROM_INTERNAL) {
  56. super.onPause();
  57. }
  58. }
  59. @Override
  60. public void onStop() {
  61. if (from == FROM_INTERNAL) {
  62. super.onStop();
  63. }
  64. }
  65. @Override
  66. public void onDestroy() {
  67. if (from == FROM_INTERNAL) {
  68. super.onDestroy();
  69. }
  70. }
  71. @Override
  72. public void onActivityResult(int requestCode, int resultCode, Intent data) {
  73. if (from == FROM_INTERNAL) {
  74. super.onActivityResult(requestCode,resultCode,data);
  75. }
  76. }
  77. }

插件Activity编写

插件Activity需要继承PluginActivityImpl

 
  1. package com.example.didi;
  2. import android.os.Bundle;
  3. import com.mango.library.plugin.PluginActivityImpl;
  4. public class DidiActivity extends PluginActivityImpl {
  5. @Override
  6. public void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_main);
  9. }
  10. }

宿主Activity编写

正常情况下是通过服务器下载需要执行的插件APK,我这里就从Assets目录获取apk文件模拟下载了

 
  1. public static String copyAssetFile2APPCache(Context context,String fileName){
  2. //获取应用内部存储中私有缓冲目录 data/data/包名/cache
  3. File cacheDir = context.getCacheDir();
  4. if (cacheDir.exists()) {
  5. cacheDir.mkdirs();
  6. }
  7. try {
  8. File outFile = new File(cacheDir,fileName);
  9. if (outFile.exists()) {
  10. return outFile.getAbsolutePath();
  11. } else {
  12. BufferedInputStream bis = new BufferedInputStream(context.getResources().getAssets().open(fileName));
  13. BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outFile));
  14. int len;
  15. byte[] buff = new byte[1024*8];
  16. while ((len = bis.read(buff)) != -1) {
  17. bos.write(buff,0,len);
  18. }
  19. bos.flush();
  20. bis.close();
  21. bos.close();
  22. SharedPreferences sp = context.getSharedPreferences("APK",Context.MODE_PRIVATE);
  23. SharedPreferences.Editor editor = sp.edit();
  24. editor.putBoolean("load",true);
  25. editor.putString("path",outFile.getAbsolutePath());
  26. editor.commit();
  27. return outFile.getAbsolutePath();
  28. }
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. return "";
  32. }
  33. }

主要是将插件apk文件拷贝到私有缓存目录去,当然了也可以直接将插件apk放在SD卡上

然后在点击滴滴出行的时候再调用该方法

 
  1. @Override
  2. public void onClick(View v) {
  3. super.onClick(v);
  4. SharedPreferences sp = getContext().getSharedPreferences("APK",Context.MODE_PRIVATE);
  5. boolean load = sp.getBoolean("load",false);
  6. if (load) {
  7. String path = sp.getString("path","");
  8. intentActivity(path);
  9. } else {
  10. //模拟下载过程
  11. new LoadApk().execute();
  12. }
  13. }
  14. class LoadApk extends AsyncTask<Void,Void,String>{
  15. @Override
  16. protected String doInBackground(Void... voids) {
  17. return FileUtil.copyAssetFile2APPCache(getContext(),"didi.apk");
  18. }
  19. @Override
  20. protected void onPostExecute(String path) {
  21. super.onPostExecute(path);
  22. intentActivity(path);
  23. }
  24. }
  25. private void intentActivity(String path){
  26. Toast.makeText(getContext(),path,Toast.LENGTH_LONG).show();
  27. if (TextUtils.isEmpty(path)) {
  28. } else {
  29. PluginManager.getInstance()
  30. .init(getContext())
  31. .loadPluginApk(path);//如果插件APK是放在SD卡上,那这里的path就是文件路径,也就不需要上面的拷贝操作了
  32. Intent intent = new Intent(getContext(),ProxyActivity.class);
  33. intent.putExtra("classname","com.example.didi.DidiActivity");
  34. startActivity(intent);
  35. }
  36. }

接下来看看运行效果

这样就从宿主APK成功加载一个未安装的APK中的Activity了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值