DroidPlugin总结

本文旨在总结插件化主要解决的问题,即假设没有使用任何框架,从使用流程来一步步分析,如何打开本地某个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> 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 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(packageInfo));
                } else {
                    mResourcePackages.put(aInfo.packageName,
                            new WeakReference(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 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);
DroidPlugin总结_第1张图片
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 suppressedExceptions = new ArrayList();
        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 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 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总结)