美文网首页插件等 移动 前端 Python Android Java
Hook机制学习(四) -插件加载机制

Hook机制学习(四) -插件加载机制

作者: shuixingge | 来源:发表于2016-11-08 19:54 被阅读73次

    weishu_博客

    一:Classloader加载的基本原理

    基本原理:系统通过ClassLoader加载了需要的Activity类并通过反射调用构造函数创建出了Activity对象。

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

    必要性: Android系统使用了PathClassLoader来进行Activity等组件的加载;apk被安装之后,APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类;但是系统并不知道存在于插件中的Activity组件的信息因此正常情况下系统无法加载我们插件中的类。
    LoakApk:LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。

    二:两种加载方案

    1. 构建插件对应的ClassLoader来加载插件

    基本原理:
    1 先通过反射调用getPackageInfoNoCheck生成LoadApk,在创建该LoadApk对应的ClassLoader的对象,ClassLoader的路径设置为插件的路径,在把该LoadApk保存早ActivityThread的mPackages里面。这样在创建插件组件(如Activity)时,使用的就是构建的插件对应的ClassLoader来加载插件组件。
    2 getPackageInfoNoCheck需要三个参数,所以先需要反射出各个参数
    r.packageInfo: 为LoadApk,所以要想创建插件对应的ClassLoader,首先要创建插件LoadApk。

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

    ** 原理:** LoadApk的缓存
    r.packageInfo是通过getPackageInfoNoCheck方法获取的

    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
    r.packageInfo = getPackageInfoNoCheck(
            r.activityInfo.applicationInfo, r.compatInfo);
    handleLaunchActivity(r, null);
    

    getPackageInfoNoCheck简单的调用了getPackageInfo()

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

    getPackageInfo:使用mPackages进行LoadedApk缓存

    private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
            ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
            boolean registerPackage) {
            // 获取userid信息
        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())) {
                    // 缓存没有命中,直接new
                packageInfo =
                    new LoadedApk(this, aInfo, compatInfo, baseLoader,
                            securityViolation, includeCode &&
                            (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
    
            // 省略。。更新缓存
            return packageInfo;
        }
    }
    

    做法: 因为LoadApk使用mPackages进行缓存,所以可以通过反射 mPackages,然后把插件对应的LoadApk保存在mPackages

    第一步:反射获取ActivityThead中的mPackages

    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    
    // 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
    Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
    mPackagesField.setAccessible(true);
    Map mPackages = (Map) mPackagesField.get(currentActivityThread);
    

    第二步:创建插件对应的LoadApk保存在mPackages
    1 采取Hook getPackageInfoNoCheck而不是 getPackageInfo,因为public方法稳定性和兼容性更好。
    2 getPackageInfoNoCheck需要准备 两个参数:ApplicationInfo aInfo, CompatibilityInfo compatInfo
    第三步:准备ApplicationInfo信息:使用PackageParse来解析Androidmanifest文件中的ApplicationInfo信息。
    1 通过generateApplicationInfo来获得Application;需要准备三个参数

    public static ApplicationInfo generateApplicationInfo(Package p, int flags,
       PackageUserState state)
    

    1.1 构建PackageParser.Package:这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示;因此,要获取这个类,肯定需要解析整个apk文件。
    使用PackageParser.parsePackage()来解析。

    // 首先, 我们得创建出一个Package对象出来供这个方法调用
    // 而这个需要得对象可以通过 android.content.pm.PackageParser#parsePackage 这个方法返回得 Package对象得字段获取得到
    // 创建出一个PackageParser对象供使用
    Object packageParser = packageParserClass.newInstance();
    // 调用 PackageParser.parsePackage 解析apk的信息
    Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
    
    // 实际上是一个 android.content.pm.PackageParser.Package 对象
    Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);
    
    
    

    1.2 int flags:参数是解析包使用的flag,直接选择解析全部信息,也就是0;
    1.3构建PackageUserState:代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可;

    / 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可
    Object defaultPackageUserState = packageUserStateClass.newInstance();
    
    // 万事具备!!!!!!!!!!!!!!
    ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
            packageObj, 0, defaultPackageUserState);
    String apkPath = apkFile.getPath();
    applicationInfo.sourceDir = apkPath;
    applicationInfo.publicSourceDir = apkPath;
    

    第三步:替换ClassLoader
    1 调用getPackageInfoNoCheck获取LoadedApk

    // android.content.res.CompatibilityInfo
    Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
    Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);
    
    Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
    defaultCompatibilityInfoField.setAccessible(true);
    
    Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
    ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);
    
    Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);
    
    

    2 替换LoadApk的ClassLoader,然后把它添加进ActivityThread的mPackages中。

    2. 告诉宿主Classloader插件路径,使用宿主Classloader来加载

    基本原理:
    1 已安装的Apk使用的是PathClassLoader来加载data/package目录下类,PathClassLoader继承于BaseDexClassLoader,BaseDexClassLoader通过findClass()方案来加载一个类,findClass()调用了pathList.findClass()。
    2 DexPathList:通过DexElements来加载
    BaseDexClassLoader.findClass();

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    

    DexPathList.findClass

    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;
               }
           }
       }
       if (dexElementsSuppressedExceptions != null) {
           suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
       }
       return null;
    }
    
    

    3 把插件的信息保存在dexElements里面:给

    public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 获取 BaseDexClassLoader : pathList
        Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathListObj = pathListField.get(cl);
    
        // 获取 PathList: Element[] dexElements
        Field dexElementArray = pathListObj.getClass().getDeclaredField("dexElements");
        dexElementArray.setAccessible(true);
        Object[] dexElements = (Object[]) dexElementArray.get(pathListObj);
    
        // Element 类型
        Class<?> elementClass = dexElements.getClass().getComponentType();
    
        // 创建一个数组, 用来替换原始的数组
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);
    
        // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
        Constructor<?> constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);
        Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));
    
        Object[] toAddElementArray = new Object[] { o };
        // 把原始的elements复制进去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 插件的那个element复制进去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);
    
        // 替换
        dexElementArray.set(pathListObj, newElements);
    
    }
    

    两种加载方案的比较

    方案一:构建ClassLoader
    优点:多ClassLoader机制,每个插件都有一个对应的ClassLoader,隔离性好,比如两个不同的插件使用两个库的不同版本,那么不会出现冲突情况。
    缺点:兼容性差,实现过程复杂。
    方案二: 补丁方案
    优点:实现简单
    缺点:单ClassLoader方案,不同的插件都用PathClassLoader加载,一旦插件之间甚至插件与宿主之间使用的类库有冲突,会出现类型冲突的后果。

    相关文章

      网友评论

        本文标题:Hook机制学习(四) -插件加载机制

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