美文网首页框架【库】
浅析Android插件化

浅析Android插件化

作者: lycknight | 来源:发表于2019-04-28 21:43 被阅读274次

    前言

    在Android 9.0的jar包中,可以发现很多函数添加了@hide,这表示google显示了对@hide api的反射调用,上有政策,下有对策,我天朝人民的智慧是无穷尽的,具体的方案可以参考一种绕过Android P对非SDK接口限制的简单方法。按理说只要在Android 9.0的手机反射调用了@hide的api都不会work。但是我在华为的p20上测试,还是可以使用的,说明华为对google的做了一些“定制化”。

    划重点

    本文将从下面几个方面去讲述插件化的实现原理

    1. 简单梳理一下Activity的启动流程
    2. 了解反射和动态代理
    3. Android类的加载机制,主要了解PathClassLoader和DexClassLoader
    4. hook的两个方案
    5. 资源加载

    demo

    Activity启动流程

    image

    启动流程:

    1. 点击桌面APP图标,Launcher进程采用Binder IPC(IActivityManager)向system_server(AMS)进程发起startActivity请求
    2. system_server(AMS)进程接受到请求后,检查该进程是否存在,如果不存在则想zygote进程发送创建进程的请求
    3. zygote进程fork出新的子进程,即APP进程
    4. APP进程调用ActivityThread中的main函数,然后通过Binder IPC(IActivityManager)向system_server(AMS)进程发起attachApplication请求
    5. system_server(AMS)进程接受到请求后,先通过Binder IPC(IApplicationThread)向APP发送bindApplication通知,创建Application。发送创建Activity的消息需要分版本
      1. Android 9.0(28)以下,通过Binder IPC(IActivityManager)向APP进程发送scheduleLaunchActivity请求
      2. Andriod 9.0(28),通过Binder IPC(IActivityManager)向APP进程发送scheduleTransaction请求
    6. APP进程接受到请求后
      1. 小于28的版本,通过mH(handler)想App进程发送scheduleLaunchActivity请求
      2. 等于28的版本,通过scheduleTransaction,调用ActivityThread的handleLaunchActivity
    7. 主线程在接收到消息后,开始创建Activity
    8. 到此,APP便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染完成后便可以看到APP主界面了。

    反射和动态代理

    反射就是动态获取信息和动态代用对象的方法,具体的介绍可以看这篇文章反射、动态代理和注解,这里就不赘述了

    classloader

    • PathClassLoader只能加载已经安装到Android系统中的apk(/data/app目录),是Android默认使用类加载器
    • DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader要更加灵活,是实现热修复的重点

    基于API28的源码

    /**
     * Provides a simple {@link ClassLoader} implementation that operates on a list
     * of files and directories in the local file system, but does not attempt to
     * load classes from the network. Android uses this class for its system class
     * loader and for its application class loader(s).
     */
    public class PathClassLoader extends BaseDexClassLoader {
        
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }
    
        public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
            super(dexPath, null, librarySearchPath, parent);
       }
    }
    
    /**
     * A class loader that loads classes from {@code .jar} and {@code .apk} files
     * containing a {@code classes.dex} entry. This can be used to execute code not
     * installed as part of an application.
    */
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                              String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }
    

    从注释上可以看出PathClassLoader是用于系统类和应用程序类的加载,DexClassLoader可以用来加载任意目录的dex。集体实现还要看BaseDexClassLader的构造方法

    public class BaseDexClassLoader extends ClassLoader {
        private final DexPathList pathList;
    
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                                  String librarySearchPath, ClassLoader parent) {
            this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
        }
    
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                                  String librarySearchPath, ClassLoader parent, boolean isTrusted) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
            ...
        }
        
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            ...
            Class c = pathList.findClass(name, suppressedExceptions);
            ...
            return c;
        }
        
        @Override
        protected URL findResource(String name) {
            return pathList.findResource(name);
        }
        
        @Override
        public String findLibrary(String name) {
            return pathList.findLibrary(name);
        }
    }
    

    可以看出在构造方法中创建了一个DexPathList对象赋值给了pathList字段,然后findxxx()方法都是从DexPathList中查找。BaseDexClassLoader的构造函数包含四个参数:

    • dexPath:包含类和资源的jar/apk文件列表,由File.pathSeparator分割
    • optimizedDirectory:由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或者Jar文件中解压出dex文件,该参数就是制定解压出的dex文件存放路径。这也是对apk中dex根据平台进行ODEX优化过程,字API26开始无效
    • librarySearchPath:指目标类中所使用的c/c++库存放的路径,可以为null
    • parent:父ClassLoader引用

    接下来我们查看DexPathList的构造方法和findxxx()方法

    
    final class DexPathList {
        private Element[] dexElements;
        
        DexPathList(ClassLoader definingContext, String dexPath,
                String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
            ...
            // 加载dexPath路径下的dex和resource
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext, isTrusted);
            ...
        }
        
            private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
          Element[] elements = new Element[files.size()];
          int elementsPos = 0;
          for (File file : files) {
              if (file.isDirectory()) {
                  elements[elementsPos++] = new Element(file);
              } else if (file.isFile()) {
                  String name = file.getName();
    
                  DexFile dex = null;
                  if (name.endsWith(DEX_SUFFIX)) {
                      // Raw dex file (not inside a zip/jar).
                      try {
                          dex = loadDexFile(file, optimizedDirectory, loader, elements);
                          if (dex != null) {
                              elements[elementsPos++] = new Element(dex, null);
                          }
                      } catch (IOException suppressed) {
                          System.logE("Unable to load dex file: " + file, suppressed);
                          suppressedExceptions.add(suppressed);
                      }
                  } else {
                      try {
                          dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      } catch (IOException suppressed) {
                          suppressedExceptions.add(suppressed);
                      }
    
                      if (dex == null) {
                          elements[elementsPos++] = new Element(file);
                      } else {
                          elements[elementsPos++] = new Element(dex, file);
                      }
                  }
              } else {
                  System.logW("ClassLoader referenced unknown path: " + file);
              }
          }
          return elements;
        }
    }
    

    构造方法中调用makeDexElements()方法获取到了Elements[]数组赋值给了dexElements变量

     public Class<?> findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                Class<?> clazz = element.findClass(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
    
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    

    findClass()方法则是变量在构造方法初始化好的Element[]数组中从前往后遍历找到我们需要的类。
    这里我们总结一下类加载的过程:

    • PathClassLoaderDexClassLoader调用了父类BaseDexClassLoader的构造方法。
    • BaseDexClassLoader在构造方法中创建了DexPathList对象并赋值给pathList字段,加载类的findxxx()方法都是调用DexPathList类的findxxx()方法来实现类的加载
    • DexPathList在构造方法中调用makeDexElements()方法创建了Elements[]数组赋值给dexElements字段,findClass()方法就是从前往后遍历Element[]数组找到我们要的class

    插件化

    终于进入到正题了,插件化顾名思义,就是将一个APK拆成多个,在需要的时候进行网络下载,动态加载到内存中。目前有两种比较好的插件化方案:

    • Hook Instrumentation方案
    • Hook AMS(ActivityManagerService)和ActivityThread中的mH(handler)

    选择:这里我们选择第一个方案,第二个方案缺陷比较多,首先是Android 9.0的启动流程做了比较大的改动,hook mH已经失效,而且AMS的hook在Android 8.0和小于8.0也有区别。

    选择好了方案我们先梳理一下整个hook的流程:

    1. 将插件工程打包为APK,然后通过adb push命令发送到宿主手机目录下(模拟下载流程)
    2. 利用ClassLoader加载插件APK中的类(Android 9.0PathClassLoader也可以加载任意目录下的APK)
    3. 将APK加载出来的DexPathList中的Emement数据加入到原生的classloader对象中
    4. hook InstrumentationProxy,在发起execStartActivity时换成占位的Activity,在newActivity的时候换为APk中的Activity

    打包apk后面再说,这里先说下面的几步操作

    利用ClassLoader加载apk

    根据上一章节的叙述,我们知道类加载的时候,首先会到父类的classLoader去寻找,找不到才会到当前的classLoader去加载类,而在这里系统是用PathClassLoader去加载类,这里我们就需要将apk中Element加入到PathClassLoader的DexPathList中的Element数组中,看下面代码

    @Throws(Exception::class)
        private fun inject(context: Context, origin: ClassLoader, pluginPath: String) {
            val optimizeFile = context.getFileStreamPath("plugin") // /data/data/$packageName/files/plugin
            if (!optimizeFile.exists()) {
                optimizeFile.mkdirs()
            }
            val pluginClassLoader = DexClassLoader(pluginPath, optimizeFile.absolutePath, null, origin)
            val pluginDexPathList = FieldUtil.getField(
                Class.forName(CLASS_BASE_DES_CLASSLOADER), pluginClassLoader,
                FIELD_PATH_LIST
            )
            val pluginElements = FieldUtil.getField(
                Class.forName(CLASS_DEX_PATH_LIST),
                pluginDexPathList,
                FIELD_DES_ELEMENTS
            ) // 拿到插件中的Elements
    
            val originDexPathList = FieldUtil.getField(
                Class.forName(CLASS_BASE_DES_CLASSLOADER), origin,
                FIELD_PATH_LIST
            )
            val originElements =
                FieldUtil.getField(
                    Class.forName(CLASS_DEX_PATH_LIST),
                    originDexPathList,
                    FIELD_DES_ELEMENTS
                )
    
            val array = combineArray(originElements, pluginElements) // 合并数组
            FieldUtil.setField(
                Class.forName(CLASS_DEX_PATH_LIST),
                originDexPathList,
                FIELD_DES_ELEMENTS,
                array
            )// 设置回pathClassLoader
            Log.i(TAG, "plugin success to load")
        }
    
    
        fun combineArray(pathElements: Any, dexElements: Any): Any {
            val componentType = pathElements.javaClass.componentType
            val i = Array.getLength(pathElements)
            val j = Array.getLength(dexElements)
            val k = i + j
            val result = Array.newInstance(componentType, k)
            System.arraycopy(dexElements, 0, result, 0, j)
            System.arraycopy(pathElements, 0, result, j, i)
            return result
        }
    

    这样我们就将使用DexClassLoader加载的APK,成功的放到系统PathClassLoader的加载列表中,接下来我们就需要想办法绕过系统检查,启动activity

    Hook Instrumentation

    我们如果绕过检查呢?通过上面的分析的启动流程会发现,在Instrumentation#execStartActivity中,会有个checkStartActivityResult的方法去检查错误,因此,我们可以复写这个方法,让启动参数能通过系统的检查。首先,我们需要检查启动的Intent能不能匹配到,匹配不到的话,将ClassName修改为我们预先在AndroidManifest中配置的占坑Activity,并且把当前的这个ClassName放到当前的intent的extra中,以便后面做恢复。

     public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode, Bundle options) {
    
            List<ResolveInfo> resolveInfo = null;
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                resolveInfo = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
            } else {
                resolveInfo = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
            }
            //判断启动的插件Activity是否在AndroidManifest.xml中注册过
            if (null == resolveInfo || resolveInfo.size() == 0) {
                //保存目标插件
                intent.putExtra(REQUEST_TARGET_INTENT_NAME, intent.getComponent().getClassName());
                //设置为占坑Activity
                intent.setClassName(who, PlaceHolderActivity.class.getName());
                Log.i("liyachao", PlaceHolderActivity.class.getName());
            }
    
            try {
                Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
                        Context.class, IBinder.class, IBinder.class, Activity.class,
                        Intent.class, int.class, Bundle.class);
                return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            return null;
        }
    

    绕过了检查,现在需要解决的问题是还原,我们知道,系统启动Activity最后会调到ActivityThread里,在这里,会通过Instrumentation#newActivity方法去反射构造一个Activity对象,因此我们只需要在这里还原即可。

    
     public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
                IllegalAccessException, ClassNotFoundException {
            String intentName = intent.getStringExtra(REQUEST_TARGET_INTENT_NAME);
            if (!TextUtils.isEmpty(intentName)) {
                return super.newActivity(cl, intentName, intent);
            }
            return super.newActivity(cl, className, intent);
        }
    

    一切准备就绪,我们最后的问题是,如何替换到系统的Instrumentation。通过查看源代码,找到ActivityThread中的private static volatile ActivityThread sCurrentActivityThread;,这是一个静态变量,这就方便我们了

    
    @Throws(Exception::class)
        @JvmStatic
        fun hookActivityThreadInstrumentation(application: Application) {
            val activityThreadClazz = Class.forName("android.app.ActivityThread")
            val activityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread")
            activityThreadField.isAccessible = true
            val activityThread = activityThreadField.get(null)
    
            val instrumentationField = activityThreadClazz.getDeclaredField("mInstrumentation")
            instrumentationField.isAccessible = true
            val instrumentation = instrumentationField.get(activityThread) as Instrumentation
            val proxy = InstrumentationProxy(instrumentation, application.packageManager)
            instrumentationField.set(activityThread, proxy)
        }
    

    这样,我们就能启动一个没有注册在AndroidManifest文件中的Activity了。

    资源的插件化方案

    资源的插件化方案,目前大概有两种

    • 合并资源,这样做的缺点是,可能出现资源冲突,解决方案就是重写aapt,来自定义资源生成规则
    • 各个插件构造自己的资源方案

    这里我们使用自己构造资源方案,实现起来简单,我们给插件创建一个Resources,然后插件APK中都通过这个Resource去获取资源。这里看下Resources构造方法

     /**
         * Create a new Resources object on top of an existing set of assets in an
         * AssetManager.
         *
         * @deprecated Resources should not be constructed by apps.
         * See {@link android.content.Context#createConfigurationContext(Configuration)}.
         *
         * @param assets Previously created AssetManager.目前创建的AssetManager,用来加载资源,根据插件APK路径创建AssetManager加载资源
         * @param metrics Current display metrics to consider when
         *                selecting/computing resource values.显示配置,直接使用宿主的Resources的配置即可
         * @param config Desired device configuration to consider when。配置项,直接使用宿主的Resources的配置即可
         *               selecting/computing resource values (optional).
         */
        @Deprecated
        public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
            this(null);
            mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
        }
    

    接下来看AssetManager如何创建

    
    public final class AssetManager implements AutoCloseable {
            public AssetManager() {
            synchronized (this) {
                if (DEBUG_REFS) {
                    mNumRefs = 0;
                    incRefsLocked(this.hashCode());
                }
                init(false);
                if (localLOGV) Log.v(TAG, "New asset manager: " + this);
                ensureSystemAssets();
            }
        }
        
        /**
         * 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);
        }
    }
    

    直接通过空参构造方法创建,然后调用addAssetPath()去加载对路径的资源。

    接下来我们在Application中创建插件的Resources,之所有在这里创建是为了方便插件APK中获取到这个Resources,因为插件APK中的四大组建实际上是在宿主APK中创建的,那么它们拿到Application实际上也是宿主的,所以只需要通过getApplication().getResources()就可以非常方便的拿到插件Resource

     override fun attachBaseContext(base: Context?) {
            super.attachBaseContext(base)
            try {
                val path = FileUtil.initPath("com.knight.plugin")
                val file = File(path)
                var pluginPath = ""
                file.listFiles().forEach {
                    if (it.name.endsWith(".apk")) {
                        pluginPath = it.absolutePath
                    }
                }
                pluginResource =
                        PluginManager.initPlugin(this, pluginPath)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            KnightPermission.init(this)
        }
    
        override fun getResources(): Resources {
            return if (pluginResource == null) super.getResources() else pluginResource!!
        }
    

    模拟下载apk

    这个过程很简单,在demo中,我将apk放在assets目录下,启动的时候,将apk复制到手机应用的目录下,这样就模拟了下载apk的过程

    fun copyData2File(filesDir: File, assets: AssetManager, fileName: String) {
            val file = File(filesDir, fileName)
            if (file.exists()) {
                return
            }
            var outputStream: OutputStream? = null
            var inputStream: InputStream? = null
    
            try {
                outputStream = FileOutputStream(file)
                inputStream = assets.open(fileName)
                val buffer = ByteArray(1024)
                var len = inputStream.read(buffer)
                while (len != -1) {
                    outputStream.write(buffer, 0, len)
                    len = inputStream.read(buffer)
                }
                Log.i("liyachao", "copy $fileName success")
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                try {
                    outputStream?.close()
                    inputStream?.close()
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }
    

    相关文章

      网友评论

        本文标题:浅析Android插件化

        本文链接:https://www.haomeiwen.com/subject/tjrsnqtx.html