美文网首页
DroidPlugin总结

DroidPlugin总结

作者: 一线游骑兵 | 来源:发表于2019-01-07 19:59 被阅读30次

    本文旨在总结插件化主要解决的问题,即假设没有使用任何框架,从使用流程来一步步分析,如何打开本地某个apk文件的launchActivity。

    1. 占坑

    首先要在宿主的清单文件中声明一个StubActivity。用来绕过AMS检查。可声明多个不同启动模式的Activity。因为如果打开没有在清单文件中声明过的Activity,就会抛出ActivityNotFound异常。该StubActivity就是用来绕过AMS的该项检查。

    2. 加载插件

    想要打开插件,获取插件的信息,比如launchActivity,首先要将其加载到内存中。

    2.1 解析并获取插件的ApplicationInfo

    通过PackageParser来解析插件apk的清单文件信息,然后生成对应的ApplicationInfo。

    2.2 获取插件的启动Activity

    获取到插件的ApplicationInfo对象,即可通过:

    Intent intent = pm.getLaunchIntentForPackage(packageInfo.packageName);
    

    来获取插件的启动Activity的Intent。因为宿主并不知道插件的启动Activity是哪个,也不知道插件中都有什么类。

    3. 启动插件

    想要启动一个插件,需要解决的问题如下:
    1. 如何绕过AMS的检查
    2. 如何加载插件中的类

    问题一:如何绕过AMS检查

    我们都知道,如果通过Intent打开一个没有在清单文件中注册过的Activity,系统会抛出一个ActivityNotFoundException异常。因此,我们需要在宿主的清单文件中声明一个StubActivity,然后在将该意图交给AMS的最后一步,hook掉该Intent中的受检信息,将该Intent的插件ComponentName替换为宿主的ComponentName,然后将原始的Intent作为param存放到新的Intent中,将新的Intent交给ASM。此时启动一个未注册的Activity就不会报异常了。

    在经过AMS一系列操作后,最终AMS会通过ApplicationThread来通知宿主app来启动该Activity。最终会分发到H(Handler)中的handleLaunchActivity方法。通过hook掉ActivityThread中的mH,在dispatchMessage中,handleMessage之前,将之前被替换掉的插件的Intent中的信息再替换回来。

    另外,由于Activity与AMS声明周期的回调时通过token来认证的,因此更换之后并不会影响其生命周期。该token是在AMS操作返回之后,在ActivityThread的handleLaunchActivity方法中,反射创建完Activity后,在随后的activity.attach方法中进行复制的,并会将AMS返回的token存放到插件Activity的mToken变量中。因此后续也可以进行正常的IPC。
    详情可参考:Android 插件化原理解析——Activity生命周期管理

    替换回来之后,在handleLaunchActivity方法中最终会通过反射创建该插件的Activity,但是插件中的类肯定没有被宿主app给加载到指定的内存或者目录,因此肯定会报出ClassNotFound异常。接下来该怎么办呢?

    问题二:如何加载插件中的类

    再看一下Activity的创建过程:

    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    StrictMode.incrementExpectedActivityCount(activity.getClass());
    r.intent.setExtrasClassLoader(cl);
    

    通过r.packageInfo.getClassLoader();获取到一个classLoader,然后通过反射创建该Activity。
    而r.packageInfo是一个LoadedApk对象。最终也是通过该对象中的getClassLoader方法来获取一个类加载器。LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。

    因此在创建Activity的时候,会先根据一个LoadedApk对象来获取一个类加载器,然后通过类加载器来加载该类的代码。

    跟踪代码,我们找到了最终这个LoadedApk生成的方法中:

      //存放 pkgName--LoadedApk 的缓存
       final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
    
       private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
                ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
                boolean registerPackage) {
            final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
            synchronized (mResourcesManager) {
                //读取缓存
                WeakReference<LoadedApk> ref;  
                if (differentUser) {
                    // Caching not supported across users
                    ref = null;
                } else if (includeCode) {
                    ref = mPackages.get(aInfo.packageName);
                } else {
                    ref = mResourcePackages.get(aInfo.packageName);
                }
            
                LoadedApk packageInfo = ref != null ? ref.get() : null;
                //没有缓存,新建
                if (packageInfo == null || (packageInfo.mResources != null
                        && !packageInfo.mResources.getAssets().isUpToDate())) {
                    packageInfo =
                        new LoadedApk(this, aInfo, compatInfo, baseLoader,
                                securityViolation, includeCode &&
                                (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
    
                    if (mSystemThread && "android".equals(aInfo.packageName)) {
                        packageInfo.installSystemApplicationInfo(aInfo,
                                getSystemContext().mPackageInfo.getClassLoader());
                    }
                    //存入缓存
                    if (differentUser) {
                        // Caching not supported across users
                    } else if (includeCode) {
                        mPackages.put(aInfo.packageName,
                                new WeakReference<LoadedApk>(packageInfo));
                    } else {
                        mResourcePackages.put(aInfo.packageName,
                                new WeakReference<LoadedApk>(packageInfo));
                    }
                }
                return packageInfo;
            }
        }
    

    该方法主要分三个步骤:

    1. 从缓存获取,命中则直接返回。
    2. 如没有在缓存中,则直接创建一个LoadedApk。
    3. 将创建的LoadedApk加入缓存。

    方法一:

    第一个方法就是我们自己创建一个插件对应的LoadedApk,然后通过反射将生成的插件的LoadedApk对象加入上述的Map缓存中,这样每次肯定会命中缓存,然后获取该对象中的类记载器来加载插件的类,从而实现加载插件类的目的。

    所以下边的主要矛盾就成为了如何通过一个插件apk构建为LoadedApk。

    但是此处创建LoadedApk所需参数甚多,中间过程不好把握,而且是私有的,容易有兼容问题,所以我们通过查看源码,找到该方法的上级调用方法:

        public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
                CompatibilityInfo compatInfo) {
            return getPackageInfo(ai, compatInfo, null, false, true, false);
        }
    
    

    此处只需两个参数,第二个传默认的兼容信息即可。因此现在的首要任务就是如何构建插件的ApplicationInfo对象。ApplicationInfo内存中对清单文件解析结果的映射。系统是通过PackageParser来对apk文件的清单文件进行xml解析的。DroidPlugin也是通过反射该api进行插件的ApplicationInfo的解析。如对解析细节感兴趣的同学请自行观看源码。

    在解析完成之后,我们的LoadedApk也就可以通过反射构建完毕,构建完成之后,再通过反射将其添加到mPackages的map缓存集合中。具体实现细节请参考Android 插件化原理解析——插件加载机制

    这样一来,宿主中就会存在多个类加载器,来加载各自对应的LoadedApk中对应的类和资源。

    上述的方法是多 类加载器机制,宿主中存在多个类记载器,每个插件对应自己的classLoader,即宿主与插件之间类的加载相互隔离互不干扰。因为默认情况下,宿主的类加载器无法加载插件中的类。

    那么有没有一种方法可以让宿主的ClassLoader可以加载插件中的类呢?请看方法二。

    方法二:
    首先要看通过LoadedApk.getClassLoader返回的是什么。

        public ClassLoader getClassLoader() {
            synchronized (this) {
                if (mClassLoader == null) {
                    createOrUpdateClassLoaderLocked(null /*addedPaths*/);
                }
                return mClassLoader;
            }
        }
    
        private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
            if (mPackageName.equals("android")) {
                if (mClassLoader != null) {
                    return;
                }
    
                if (mBaseClassLoader != null) {
                    mClassLoader = mBaseClassLoader;
                } else {
                    mClassLoader = ClassLoader.getSystemClassLoader();
                }
    
                return;
            }
    
            if (mClassLoader == null) {
                mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
                        mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                        libraryPermittedPath, mBaseClassLoader);
            }
        }
    

    非android开头,进入ApplicationLoader.getClassLoader:
    最终调用了如下方法新建了一个PathClassLoader:

                    PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
                                                          zip,
                                                          librarySearchPath,
                                                          libraryPermittedPath,
                                                          parent,
                                                          targetSdkVersion,
                                                          isBundled);
    
    image.png

    PathClassLoader是ClassLoader一个子类。
    在通过该PathClassLoader查找类的实现是在其父类BaseDexClassLoader中:

    public class BaseDexClassLoader extends ClassLoader {
        private final DexPathList pathList;
    
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {        //根据构造的pathList查找dexFile中的类
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            Class c = pathList.findClass(name, suppressedExceptions);
            if (c == null) {        //没有找到类,则抛出ClassNotFound异常。
                ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
                for (Throwable t : suppressedExceptions) {
                    cnfe.addSuppressed(t);
                }
                throw cnfe;
            }
            return c;
        }
    
        @Override
        protected URL findResource(String name) {        //查找插件中的资源
            return pathList.findResource(name);
        }
    
        @Override
        protected Enumeration<URL> findResources(String name) {
            return pathList.findResources(name);
        }
    
        @Override
        public String findLibrary(String name) {
            return pathList.findLibrary(name);
        }
    }
    

    最后发现,findClass的真正实现是通过DexPathList

          /**
         * List of dex/resource (class path) elements.
         * Should be called pathElements, but the Facebook app uses reflection
         * to modify 'dexElements' (http://b/7726934).
         */
        private final Element[] dexElements;
    
        public Class findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                DexFile dex = element.dexFile;
    
                if (dex != null) {
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                    if (clazz != null) {
                        return clazz;
                    }
                }
            }
            return null;
        }
    

    可以看到,最终的findClass是通过轮询Element数组来查找类。
    因此,第二种方法就是构建一个插件的Element对象,然后通过反射将其插入dexElements数组中,使其实现加载插件类的功能
    Element类构造函数如下:

    /**
         * Element of the dex/resource file path
         * dex/resource 文件路径的元素
         */
        /*package*/ static class Element {
            private final File dir;    
            private final boolean isDirectory;
            private final File zip;
            private final DexFile dexFile;    
    
            private ZipFile zipFile;        //This class is used to read entries from a zip file.【该类用来从zip文件中读取条目】
            private boolean initialized;
    
            public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
                this.dir = dir;
                this.isDirectory = isDirectory;
                this.zip = zip;
                this.dexFile = dexFile;
            }
            ...
    }
    

    对于这两种方法各自的优缺点,请参考Android 插件化原理解析——插件加载机制一文。

    通过上边的两种方法,宿主已经能够正常加载插件中的类,并通过hook来实现Activity的替换。

    总结

    解决宿主中接在插件类的问题:

    方法一:多ClassLoader机制,每个插件有自己的类加载器。

    • 通过PackageParser解析插件构建插件的ApplicationInfo。
    • 根据构建的ApplicationInfo构建插件的LoadedApk。
    • 通过反射将插件的LoadedApk存入ActivityThread中的mPackages缓存中。

    方法二:单ClassLoader机制,让宿主类加载器可以加载插件类

    • 查看源码,找到宿主的ClassLoader的findClass方法。【PathClassLoader的父类BaseDexClassLoader中,通过变量PathDexList中持有的dexElements数组来查找】
    • 通过插件路径构建插件的Element对象,然后反射将其插入dexElements数组。
    解决启动不再清单文件中声明的Activity问题:
    • 占坑,宿主清单文件中声明StubActivity。
    • 获取插件启动Activity的Intent,在startActivity后交给AMS检查之前,将Intent中插件的信息替换为宿主的信息,并将旧的Inent作为extra保存到新的Intent中
    • 在AMS检查回来之后,新建Activity之前,再将其替换回来。此时通过反射创建的Activity就是插件中的Activity了,同时由于插件Activity持有的与AMS通信的binder,因此可以进行后续的生命周期回调及IPC。

    相关文章

      网友评论

          本文标题:DroidPlugin总结

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