阅读 178

Android 插件化入门(1)-基础

写在前面

目的

Android插件化有非常多的实现方案,本文中列举的,也并不是最优解或普遍方案,旨在是在对四大组件的一些探索中,总结出在插件化开发过程中,有哪些需要注意的地方,需要解决的问题。

计划

此篇是介绍插件化的一些基础,预计是写完三篇

  • 插件化入门(1) -基础
  • 插件化入门(2) -欺上瞒下的方案 链接
  • 插件化入门(3) -代理式插件化 todo

什么是插件化?

仅安装宿主APK,无需单独安装插件apk,即可运行,无需升级宿主应用

  • 减少app的更新频率

  • 降低模块耦合

  • 按需加载,节省流量

如何加载没有安装的插件apk

一个APK有哪些东西是需要加载并运行的

  • AndroidManifest.xml android描述文件

  • classes.dex Android平台上的可执行文件,由java字节码编译而来

  • META-INF 签名文件夹

  • res 资源文件夹

  • resources.arsc 资源目录信息

  • lib so库

实际需要运行的为dex,即java代码,以及资源文件res,以及so库

加载代码dex(java代码)

要加载dex,需要用到classloader Android有两个类加载器DexClassLoader和PathClassLoader

AndoridClassLoader
网上大多数的文章都是使用DexClassLoader来加载,但是实际测试PathClassLoader也是可以加载外部dex的 两个加载器的构造函数区别主要为optimizedDirectory

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
     }
复制代码
      //android8.0前
public DexClassLoader(String dexPath, String optimizedDirectory,
                  String librarySearchPath, ClassLoader parent) {
              super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
          }
      //android8.0之后
public DexClassLoader(String dexPath, String optimizedDirectory,
                 String librarySearchPath, ClassLoader parent) {
             super(dexPath, null, librarySearchPath, parent);
         }
复制代码

实际上在Android8.0后使用DexClassLoader和PathClassLoader是没有区别的 而在8.0 之前的区别是ptmizedDirectory 会影响dex优化后.odex的存放目录 关于PathClassLoader和DexClassLoader的区别,可以参考这篇文章的分析

所以加载外部dex代码可以直接如下

    PathClassLoader pathClassLoader = new PathClassLoader(apkFile.getAbsolutePath(), null, context.getClassLoader().getParent());
    Class<?  clz = pathClassLoader.loadClass("com.dennisce.testplugin.PluginActivity");
复制代码

加载资源res

实际管理资源的东西

在开发过程中,使用资源文件,主要使用R来访问资源文件,如下

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getResources().getColor(R.color.colorAccent);
        getResources().getDrawable(R.drawable.ic_launcher_background);
    }
复制代码

先看下这些方法最后都做了什么?

  • setContentView
    setContentView
    根据调用可以看到,最后是调用到了Resource这个类
  • getResources.getxxxx() 也是使用Resource类获取资源

Resource.getColor()的调用逻辑

getResources
可以看到资源最后是由AssetManager来管理的

结论:资源是由Resources及AssetManager来管理的

怎么加载外部的资源

在AssetManager中可以找到以下方法

  /**
      * Add an additional set of assets to the asset manager.  This can be
      * either a directory or ZIP file.  Not for use by applications.  Returns
      * the cookie of the added asset, or 0 on failure.
      * {@hide}
      */
     public final int addAssetPath(String path) {
         return  addAssetPathInternal(path, false);
     }
复制代码

可以添加一组资源文件到AssetManager,APK本身就是一个zip文件,所以可以用这个方法 注意点:

  • 此方法被标记hide,需要反射调用
  • 在Android9.0此方法已被废弃,替代方法为setApkAssets
  • 在Android master代码中,此方法已标记UnsupportedAppUsage,但setApkAssets方法仍可使用 这里不考虑兼容性,以addAssetPath 为例
        AssetManager assetManager = null;
        try {
             assetManager = AssetManager.class.newInstance();
             Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
             addAssetPath.invoke(assetManager, apkPath);
         } catch (Exception e) {
             e.printStackTrace();
         }
         Resources superRes = context.getResources();
         superRes.getDisplayMetrics();
         superRes.getConfiguration();
         resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
复制代码

通过以上代码即可获得可以访问插件资源的AssetManager及Resources

加载 so

使用so文件的方法

方法名 解释
System.load(filename) filename为so文件的绝对路径,如 /data/user/0/com.dennisce.pluginstudydemo/cache/plugin/libnative-lib.so
System.loadLibrary(libname) libname为jniLibs文件夹下的lib名,如native-lib

加载插件so的方式

  • System.loadLibrary 之前加载dex的时候,提到PathClassLoader的构造方法
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
             super(dexPath,null, librarySearchPath, parent);
      }
复制代码

librarySearchPath参数直接传的是null,其实这里的librarySearchPath即是so的搜索路径 所以只需要做以下几个步骤就可以了

  • 解压插件APK中的so文件
  • 将so文件以逗号隔开生成String
  • 在创建PathClassLoader的时候,将该String作为librarySearchPath传入
  • System.load
    • 插件代码应有个入口调用System.load(filename)
    • 解压插件APK so文件到插件System.load(filename) 的filename路径

解压so需要注意的问题

生成插件APK的时候,so的目录会根据gpu的不同来生成,结果如下

所以在加载so文件的时候,需要检测当前运行手机的gpu,去加载不同的so文件

代码

代码仓库

写在最后

由于本人水平有限,如有错误,欢迎交流指出