Android热修复学习(一)

##Classloader基础
Classloader的简单定义:
通过类的全限定名来获取描述此类的二进制字节流,负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个类加载器都有一个父类加载器(包含的关系),顶级类加载器(native)除外。

  • 独立的类名称空间
    能够结合java类本身来确定该类在Java虚拟机中的唯一性。用通俗的话来说就是:比较两个类是否相等,只有这两个类是由同一个类加载器加载才有意义
  • 双亲委托模型
    如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

这部分文件在Android源码路径分别为:

    libcore/dalvik/src/main/java/dalvik/system/ 、art/runtime/native/
public abstract class ClassLoader {
	ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
        if (parentLoader == null && !nullAllowed) {
            throw new NullPointerException("parentLoader == null && !nullAllowed");
        }
        parent = parentLoader;
    }
    
	public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }
    
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                // Don't want to see this.
            }

            if (clazz == null) {
                clazz = findClass(className);
            }
        }
        return clazz;
    }

	protected final Class<?> findLoadedClass(String className) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, className);
    }

在Android中,提供了PathClassLoader和DexClassLoader两个加载器,它们都继承于BaseDexClassLoader,而BaseDexClassLoader则继承于ClassLoader。对于PathClassLoader,Android是使用这个类作为其系统类和应用类的加载器,只能去加载已经安装到Android系统中的apk文件。而DexClassLoader可以用来从.jar和.apk类型的文件内部加载classes.dex文件,可以用来执行非安装的程序代码。从上面ClassLoader的源码可以看出当需要PathClassLoader和DexClassLoader自己加载类的时候,则会掉用findClass(String name)方法,而PathClassLoader和DexClassLoader都只是简单调用了BaseDexClassLoader的构造方法,所以我们只用看BaseDexClassLoader的findClass(String name)方法。

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 {
        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;
    }
}

可以看出BaseDexClassLoader会掉用pathListfindClass()方法,而pathList是在BaseDexClassLoader构造时创建的,是DexPathList的实例。

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

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = file;

                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) { 
              suppressedExceptions.add(suppressed);
                }
            } else if (file.isDirectory()) {
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else {
                System.logW("Unknown file type for: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }

可以看出DexPathList会轮询自己的Element数组,通过Element的DexFile属性对象通过loadClassBinaryName()方法来加载类,而且一旦加载成功,则返回,不会继续寻找。而Element数组则是在DexPathList的构造方法中通过makeDexElements()来生成的,每个Element对象则对应一个dex单元,它的DexFile属性对象对应这个单元里面的dex文件。这个方法是后面一个热修复方案原理的重要来源。下面我们看DexFile的loadClassBinaryName()方法是如何加载类的。

public final class DexFile {
	    private DexFile(String sourceName, String outputName, int flags) throws IOException {
        if (outputName != null) {
            try {
                String parent = new File(outputName).getParent();
                if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                    throw new IllegalArgumentException("Optimized data directory " + parent
                            + " is not owned by the current user. Shared storage cannot protect"
                            + " your application from code injection attacks.");
                }
            } catch (ErrnoException ignored) {
                // assume we'll fail with a more contextual error later
            }
        }

        mCookie = openDexFile(sourceName, outputName, flags);
        mFileName = sourceName;
        guard.open("close");
        //System.out.println("DEX FILE cookie is " + mCookie);
    }
    
	public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, int cookie,
                                     List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

    private static native Class defineClassNative(String name, ClassLoader loader, int cookie)
        throws ClassNotFoundException, NoClassDefFoundError;
}

可以看出DexFile的loadClassBinaryName()方法最终会掉用native方法defineClassNative()来加载类。而从DexFile的私有构造方法DexFile(String sourceName, String outputName, int flags)来看加载输出文件路径必须所属应用本身,否则会报错,其中DexClassLoader最终会调用到该方法,所以需要注意。

/###########################################################################################################更新分割线############################################################################################################/

从参考文章中,我们对PathClassLoader有下面的认识:

 PathClassLoader只能加载已经安装到 Android 系统中的 apk 文件,也就是 /data/app 目录下的 apk 文件。因为PathClassLoader 
 会去读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候
 由 Dalvik 生成的。例如,如果包的名字是 com.qihoo360.test,Android 应用安装之后都保存在 /data/app 目录下,即 
 /data/app/com.qihoo360.test-1.apk,那么 /data/dalvik-cache 目录下就会生成 data@app@com.qihoo360.test-1.apk@classes.dex 
 文件。  在调用 PathClassLoader 时,它就会按照这个规则去找 dex 文件,如果你指定的 apk 文件是 /sdcard/test.apk,它按照这个
 规则就会去读 /data/dalvik-cache/sdcard@test.apk@classes.dex 文件,显然这个文件不会存在,所以 PathClassLoader 会报错。

下面我们写一个demo来验证上面的结论,我们分别通过三种方式来加载一个写好的类:DexClassLoader、PathClassLoader和DexFile。首先把写好的类打成dex放在zip包中,这个我们直接用AndFix的demo里面的out.apatch即可。

	 private Class dexLoadClass() {
        try {
            DexClassLoader dexClassLoader = new DexClassLoader(extraDexPath, outputPath, getApplicationInfo().nativeLibraryDir, getClassLoader());
            printElements(dexClassLoader);
            Class clazz = dexClassLoader.loadClass(loadClassName);
            Log.i(TAG, "dexClassLoader load result:" + (clazz != null ? clazz.getSimpleName() : "null"));
            return clazz;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            Log.i(TAG, "dexClassLoader load failed");
        }
        return null;
    }

    private Class dexFileLoadClass() {
        try {
            DexFile dexFile = DexFile.loadDex(extraDexPath, optimizedPathFor(new File(extraDexPath), new File(outputPath)), 0);
            Class clazz = dexFile.loadClass(loadClassName, getClassLoader());
            Log.i(TAG, "dexFile load result:" + (clazz != null ? clazz.getSimpleName() : "null"));
            return clazz;
        } catch (IOException e) {
            e.printStackTrace();
            Log.i(TAG, "dexFile load failed");
        }
        return null;
    }

    private Class pathLoadClass() {
        try {
            PathClassLoader pathClassLoader = new PathClassLoader(extraDexPath, getClassLoader());
            printElements(pathClassLoader);
            Class clazz = pathClassLoader.loadClass(loadClassName);
            Log.i(TAG, "pathClassLoader load result:" + (clazz != null ? clazz.getSimpleName() : "null"));
            return clazz;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            Log.i(TAG, "pathClassLoader load failed");
        }
        return null;
    }

可以看出我们是直接构造了这三个对象来进行类的加载的,然后打印出了Element数组的个数。我们再写个测试方法来调用它们,把这个测试方法放在Application的attachBaseContext()方法里运行。

	private final String SECONDARY_FOLDER_NAME = "code_cache";
    private final String DexDir = "dex";
    private final String DEX_SUFFIX = ".dex";
    private final String loadClassName = "com.euler.test.A_CF";
    private final String dexFileName = "out.zip";
    private final String TAG = "popo";
    private String extraDexPath;
    private String outputPath;

	private void init() {
        extraDexPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + dexFileName;
//        copyDex(extraDexPath);
        Log.i(TAG, "apk exist:" + new File(extraDexPath).exists());
        outputPath = getApplicationInfo().dataDir + File.separator + SECONDARY_FOLDER_NAME;
        File file = new File(outputPath);
        if (!file.exists() && !file.mkdirs()) {
            return;
        }


        Class dexClazz = dexLoadClass();
        Class pathClazz = pathLoadClass();
        Class dexFileClazz = dexFileLoadClass();

        Log.i(TAG, "dexClazz == pathClazz:" + (dexClazz == pathClazz ? "true" : "false"));
        Log.i(TAG, "dexClazz == dexFileClass:" + (dexClazz == dexFileClazz ? "true" : "false"));
        Log.i(TAG, "dexFileClass == pathClass:" + (dexFileClazz == pathClazz ? "true" : "false"));

        if (dexClazz != null) {
            Log.i(TAG, "dexClazz classloader:" + dexClazz.getClassLoader());
        }
        if (pathClazz != null) {
            Log.i(TAG, "pathClazz classloader:" + pathClazz.getClassLoader());
        }
        if (dexFileClazz != null) {
            Log.i(TAG, "dexFileClazz classloader:" + dexFileClazz.getClassLoader());
        }
        Log.i(TAG, "app classloader:" + getClassLoader());

        try {
            Class appClazz = getClassLoader().loadClass(loadClassName);
            if (appClazz != null) {
                Log.i(TAG, "dexFileClazz == appClazz:" + (dexFileClazz == appClazz ? "true" : "false"));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

首先我们在乐视手机Le X620 - 6.0上运行,日志为下面的结果:

 I/popo﹕ apk exist:true
 I/popo﹕ DexClassLoader elements size:1
 I/popo﹕ dexClassLoader load result:A_CF
 I/popo﹕ PathClassLoader elements size:1
 I/popo﹕ pathClassLoader load result:A_CF
 I/popo﹕ dexFile load result:A_CF
 I/popo﹕ dexClazz == pathClazz:false
 I/popo﹕ dexClazz == dexFileClass:false
 I/popo﹕ dexFileClass == pathClass:false
 I/popo﹕ dexClazz classloader:dalvik.system.DexClassLoader[DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/data/app/com.example.pop.testandroidclassloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
 I/popo﹕ pathClazz classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]
 I/popo﹕ dexFileClazz classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.pop.testandroidclassloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
 I/popo﹕ app classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.pop.testandroidclassloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
 I/popo﹕ dexFileClazz == appClazz:true

从日志信息我们可以看出三种方式都加载成功了,并且它们三个各不相等,说明不是一个加载器加载的,从分别打印出的classloader信息我们也可以看出来。最后我们又用了app的classloader(也就是PathClassLoader的一个实例)来加载这个类,我们看到也加载成功了,并且加载出来的Class对象和DexFile加载出来的Class对象是相等的,而且两者打印出来的classloader信息也是一致的,所以最后的app的classloader加载应该只是直接返回了之前DexFile加载出来的Class对象,这是因为我们在构造DexFile(即调用DexFile的loadDex()方法)时传入的参数为app的classloader。
接着我们再在华为手机H60-L02 - 4.4.2上运行,看看会有什么样的结果:

 I/popo﹕ apk exist:true
 I/popo﹕ DexClassLoader elements size:1
 I/popo﹕ dexClassLoader load result:A_CF
 I/popo﹕ PathClassLoader elements size:1
 I/popo﹕ pathClassLoader load failed
 I/popo﹕ dexFile load result:A_CF
 I/popo﹕ dexClazz == pathClazz:false
 I/popo﹕ dexClazz == dexFileClass:false
 I/popo﹕ dexFileClass == pathClass:false
 I/popo﹕ dexClazz classloader:dalvik.system.DexClassLoader[DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/data/app-lib/com.example.pop.testandroidclassloader-1, /vendor/lib, /system/lib, /data/datalib]]]
 I/popo﹕ dexFileClazz classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.example.pop.testandroidclassloader-1, /vendor/lib, /system/lib, /data/datalib]]]
 I/popo﹕ app classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.example.pop.testandroidclassloader-1, /vendor/lib, /system/lib, /data/datalib]]]
 I/popo﹕ dexFileClazz == appClazz:true

从结果可以看出除了我们自己new的pathClassLoader没有加载成功,其它的基本跟在6.0上的结果是一样的,在运用app本身的pathClassLoader构造的DexFile成功加载了该类后,app本身的pathClassLoader自然也成功加载了该类。只是我们自己new的pathClassLoader为什么没有加载成功,是否又和参考文章中说的原因一样呢,下面我们把其它的都注释掉,只调用自己构造的pathClassLoader来加载,看情况如何:

//Le X620 - 6.0
 I/popo﹕ apk exist:true
 W/dex2oat﹕ type=1400 audit(0.0:16537): avc: denied { module_request } for kmod="personality-8" scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:r:kernel:s0 tclass=system permissive=0
 E/dex2oat﹕ Failed to create oat file: /data/dalvik-cache/arm64/storage@emulated@0@out.zip@classes.dex: Permission denied
 I/dex2oat﹕ dex2oat took 379.308us (threads: 10)
 W/art﹕ Failed execv(/system/bin/dex2oat --runtime-arg -classpath --runtime-arg  --instruction-set=arm64 --instruction-set-features=smp,a53 --runtime-arg -Xrelocate --boot-image=/system/framework/boot.art --runtime-arg -Xms64m --runtime-arg -Xmx512m --instruction-set-variant=cortex-a53 --instruction-set-features=default --dex-file=/storage/emulated/0/out.zip --oat-file=/data/dalvik-cache/arm64/storage@emulated@0@out.zip@classes.dex) because non-0 exit status
 I/popo﹕ PathClassLoader elements size:1
 I/popo﹕ pathClassLoader load result:A_CF

//H60-L02 - 4.4.2
 I/popo﹕ apk exist:true
 E/dalvikvm﹕ Dex cache directory isn't writable: /data/dalvik-cache
 I/dalvikvm﹕ Unable to open or create cache for /storage/emulated/0/out.zip (/data/dalvik-cache/storage@emulated@0@out.zip@classes.dex)
 I/popo﹕ PathClassLoader elements size:1
 W/System.err﹕ java.lang.ClassNotFoundException: Didn't find class "com.euler.test.A_CF" on path: DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/vendor/lib, /system/lib, /data/datalib]]
 W/System.err﹕ at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
 W/System.err﹕ at com.example.pop.testandroidclassloader.MyApplication.pathLoadClass(MyApplication.java:110)
 W/System.err﹕ Suppressed: java.io.IOException: unable to open DEX file
 W/System.err﹕ at dalvik.system.DexFile.openDexFileNative(Native Method)
 W/System.err﹕ at com.example.pop.testandroidclassloader.MyApplication.pathLoadClass(MyApplication.java:108)
 W/System.err﹕ ... 17 more
 I/popo﹕ pathClassLoader load failed

可以看到单独运行,在6.0上pathClassLoader依旧可以成功加载,而在4.4.2上依旧失败,并且从日志上可以看出它们都尝试在/data/dalvik-cache下创建加载后文件,但都因为权限问题失败了。从老罗的ART运行时无缝替换Dalvik虚拟机的分析中我们知道加载时6.0中art执行的是dex2oat生成的是本地运行文件oat,而4.4.2上dalvik执行的是dex2opt生成的是odex优化文件。而且第三篇参考文章写于2010年,当时都还未有art,所以猜测应该是5.0版本更改虚拟机为art时做了调整pathClassLoader可以加载外部包含dex的zip包了,不过这个需要源码的验证!

小结
1.Android中继续使用类命名空间隔离和双亲委托模型机制。
2.Android中应用大部份类都是通过PathClassLoader来加载,而且Android系统在启动应用的时候只会加载apk中的classes.dex文件。如果有多个dex文件,则在5.0以下版本需要自己动态加载。
3.Android在第一次加载dex文件的时候,如果虚拟机是dalvik会掉用dexopt进行优化,生成生成一个odex文件,即 Optimised Dex,如果是art则会调用dex2oat生成本地运行文件oat。
4.在虚拟机是dalvik时,PathClassLoader只能加载已经安装的apk文件,而art时则可能加载外部包含dex的zip文件也能成功。不过为了兼容建议还是通过DexClassLoader来加载。

以上有些结论都是我自己通过程序结果的推论和部份猜测,有什么不对的地方,希望有兴趣阅读后发现的朋友及时告知。下面为测试的demo下载地址,demo中本来用的AndFix的out.apatch但是后来在华为4.4.2的手机上跑无法识别,所以后来改为zip后缀才好,还有如果要放在sd卡上的话,要特别注意权限问题,最开始我忘记添加权限,一直加载不了,弄了很长时间特别苦恼,而且因为后面运行在native层并不能从日志看出是权限的问题,后来加上了有些6.0的手机上默认权限又没有打开,还是加载失败。
demo下载地址

参考链接:
http://blog.csdn.net/xyang81/article/details/7292380
https://segmentfault.com/a/1190000004062880
http://blog.csdn.net/quaful/article/details/6096951
http://blog.kifile.com/android/2015/11/10/dex_class_loader.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值