Android插件化基础的主要内容包括
本文涉及的内容如下:
我们写完一个java程序后,通过编译,形成若干个.class文件,而这些若干个.class文件组织成一个完成的java程序,当程序运行时,都会调用一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从一个class文件调用到另外一个class文件的某个方法,如果另外一个class不存在,则会引发系统异常。而在程序启动的时候,不会一次性加载程序所有的class文件,而是根据程序的需要,通过java类加载机制(ClassLoader)来动态加载某个class文件到内存中,从而只有class被记载到内存之后,才能被其他class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用到的
为了更好的理解类加载机制,我们来深入研究下ClassLoader和他的方法
public abstract class ClassLoader
/** * A classloader isan object thatisresponsible forloading classes. The
* classClassLoader isan abstract class. Given thebinary nameofa
*class, a classloader should attempt to* locate orgenerate data that
*constitutes a definition fortheclass. A
* typical strategy istotransform thenameintoa filenameandthenreada
* "class file"ofthatnamefroma filesystem. **/
大致的意思是: ClassLoader 是一个负责加载classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,ClassLoader尝试定位或者产生一个class数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件 以下是ClassLoader常用到的几个方法及其重载方法:
其中defineClass方法用来将byte字节流解析成JVM能够识别的Class对象,有了这个方法意味着我们不仅仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,如果我们通过网络接受到一个类的字节码,拿到这个字节码流直接创建类的Class对象形式实例化对象。如果直接调用这个方法生成类的Class对象,这个对象Class对象还没有resolve,这个resolve将会在这个对象真正实例化时才进行
ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但是可以作用于其他ClassLoader实例的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上到下依次检查的,首先由最顶层的类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没有加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给app ClassLoader进行加载。如果他没有家在得到的话,则返回给委托的发起者,由它到制定的文件系统或者网络等URL加载该类,如果他们都没有加载这个类,则票抛出ClassNotFoundException异常,否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。如下图
class_image.png
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要ClassLoader再加载一次了,考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大安全隐患,而双亲委托可以避免这种情况,因为String已经在启动的时候就被引导类加载器(Bootstrap ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中的ClassLoader的搜索类默认算法。
Android应用打包成apk时,class文件会被打包成一个或者多个dex文件,将一个apk文件后缀改成.zip格式后解压;里面有class.dex文件,由于android64K方法数的问题,使用MultiDex就会生成多个dex文件。如下图
image.png
当Android 系统安装包安装一个应用的时候,会针对不同的平台对Dex进行优化,这个过程由一个专门的工具来处理叫DexOpt。DexOpt是第一次加载Dex文件的时候执行,该过程会生成一个ODEX文件,即Optimised Dex,执行ODEX的效率会比直接执行Dex文件的效率要高很多,加快App的启动和响应。
PS:
Dex file compilation uses a tool called dex2oat and takes more time than
dexopt. The increase in time varies, but 2-3x increases in compile time
are not unusual. For example, apps that typically take a second to install
using dexopt might take 2-3 seconds.
这里解释下:DEX转换成OAT的这个过程是在用户安装程序或者刷入ROM,OTA更新后首次启动时执行的,按照google的说法,相比做过ODEX优化,未做过优化的DEX转成成OAT要花费更长的时间,比如2-3倍。比如安装一个odex优化过的程序假设需要1秒钟,未做过优化的程序就需要2-3秒。由此次可见,虽然dalvik被弃用了,但是ODEX优化在Android棒棒糖上依旧拥有显著的优化效果。首先ODEX优化不仅仅只是针对应用程序,还会对内核镜像,jar库文件等进行优化。其次,资源和可执行文件分离带来的性能提升无论是运行在ART还是Dalvik,都有效。
下载好的Android apk, 在安装过程中,其中文件内容是这样处理的:
要注意的是, 安装过程并没有把资源文件, assets目录下文件拷贝出来,他们还在apk包里面呆着,所以,当应用要访问资源的时候,其实是从apk包里读取出来的。其过程是,首先加载apk里的resources(这个文件是存储资源Id与值的映射文件),根据资源id读取加载相应的资源。
项目.jpeg
看下 AppTest 里面的目录结构
显示.jpeg
分别看下 IDexTest,IDexTestImpl和string.xml
public interface IDexTest {
String getText();
}
public class IDexTestImpl implements IDexTest {
@Override
public String getText() {
return "我是SD卡上的APK";
}
}
<resources>
<string name="app_name">AppTest</string>
<string name="showtext">我是SD上的字符串</string>
</resources>
当AppTest 被打包成apk的时候,我们要在 dexclassloaderapp 里面获取这些数据 。现在开始打包apk,名字为"AppTest-release.apk",然后把这个apk放到sd上。 现在run dexclassloaderapp 项目会出现下面的显示
device-1.png
点击 测试加载类 上面的textview会有原来的"类信息!"转变"我是SD卡上的APK",证明已经成功加载到SD卡上的apk (ps:6.0手机注意权限,有的手机没有开通权限会报找不到类)
类加载.gif
先看下 dexclassLoaderapp 是如何实现在家外部APK class的 具体实现是在load方法里面
private void load() {
// 获取到包含 class.dex 的 jar 包文件
final File apkFile =
new File(Environment.getExternalStorageDirectory().getPath() + File.separator + "apptest-release.apk");
if (!apkFile.exists()) {
Log.e("LGC", "文件不存在");
mHandler.post(new Runnable() {
@Override
public void run() {
pd.dismiss();
Toast.makeText(MainActivity.this, "文件不存在", Toast.LENGTH_LONG);
}
});
return;
}
if (!apkFile.canRead()) {
// 如果没有读权限,确定你在 AndroidManifest 中是否声明了读写权限
// 如果是6.0以上手机要查看手机的权限管理,你的这个app是否具有读写权限
Log.d("LGC", "apkFile.canRead()= " + apkFile.canRead());
mHandler.post(new Runnable() {
@Override
public void run() {
pd.dismiss();
Toast.makeText(MainActivity.this, "没有读写权限", Toast.LENGTH_LONG);
}
});
return;
}
// getCodeCacheDir() 方法在 API 21 才能使用,实际测试替换成 getExternalCacheDir() 等也是可以的
// 只要有读写权限的路径均可
Log.i("LGC", "getExternalCacheDir().getAbsolutePath()=" + getExternalCacheDir().getAbsolutePath());
Log.i("LGC", "apkFile.getAbsolutePath()=" + apkFile.getAbsolutePath());
try {
DexFile dx = DexFile.loadDex(apkFile.getAbsolutePath(), File.createTempFile("opt", "dex", getApplicationContext().getCacheDir()).getPath(), 0);
// Print all classes in the DexFile
for (Enumeration<String> classNames = dx.entries(); classNames.hasMoreElements(); ) {
String className = classNames.nextElement();
if (className.equals("com.yibao.test.IDexTestImpl")) {
Log.d("LGC", "#########################################################" + className);
Log.d("LGC", className);
Log.d("LGC", "#########################################################" + className);
}
Log.d("LGC", "Analyzing dex content, fonud class: " + className);
}
} catch (IOException e) {
Log.d("LGC", "Error opening " + apkFile.getAbsolutePath(), e);
}
DexClassLoader dexClassLoader =
new DexClassLoader(apkFile.getAbsolutePath(), getExternalCacheDir().getAbsolutePath(), null, getClassLoader());
try {
// 加载 com.test.IDexTestImpl 类
Class clazz = dexClassLoader.loadClass("com.test.IDexTestImpl");
Object dexTest = clazz.newInstance();
Method getText = clazz.getMethod("getText");
final String result = getText.invoke(dexTest).toString();
mHandler.post(new Runnable() {
@Override
public void run() {
pd.dismiss();
if (!TextUtils.isEmpty(result)) {
tv.setText(result);
}
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
大体的流程是先new 一个DexFile,然后loadClass就获取了这个SD卡上的apk资源了,为什么可以这样那?
Android的Dalvik/ART虚拟机如果标准Java的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此我们可以利用这一点,在程序运行时手动加载Class,从而达到代码动态加载可执行文建的目的。Android的Dalvik/ART虚拟机虽然与标准java的JVM不一样,所以ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似动态加载插件的功能,只是在Android应用中动态加载一个插件的工作要比java复杂的多。
结构图.jpg
image1.png
SecureClassLoader的子类是URLClassLoader,其只能用来加载jar文件,在android的Dal/ART上是没法使用的,这里就不过多的介绍了!
classloader.jpg
这里面有两个重要的类 PathClassLoader和DexClassLoader他们分别继承BaseDexClassLoader,那他们的区别是什么?那么看下他们的构造函数
public class PathClassLoader{
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader (String dexPath , String optimizedDirectory,String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
他们都是调用父类BaseDexClassLoader的构造函数 BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent); this.originalPath = dexPath;
this.originalLibraryPath = libraryPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
这四个参数分别的含义:
简单介绍了PathClassLoader和DexClassLoader,但这两者都是对BaseDexClassLoader的一层简单的封装,真正的实现都在BaseClassLoader内,那么咱们看下BaseClassLoader内的具体实现
通过上面的BaseDexClassLoader的构造函数,咱们知道了BaseDexClassLoader构造的时候创建了一个DexPathList类的对象 那咱们就继续跟踪看下DexPathList这个的类的构造函数
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
代码里面调用了makeDexElements()这个方法,其中一个参数是调用splitLibraryPath()方法的返回值。所以先看下splitLibraryPath()方法
private static ArrayList<File> splitDexPath(String path) {
return splitPaths(path, null, false);
}
private static ArrayList<File> splitPaths(String path1, String path2, boolean wantDirectories) {
ArrayList<File> result = new ArrayList<File>();
splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
}
private static void splitAndAdd(String path, boolean wantDirectories,
ArrayList<File> resultList) {
if (path == null) {
return;
}
String[] strings = path.split(Pattern.quote(File.pathSeparator));
for (String s : strings) {
File file = new File(s);
if (!(file.exists() && file.canRead())) {
continue;
}
/*
* Note: There are other entities in filesystems than
* regular files and directories.
*/
if (wantDirectories) {
if (!file.isDirectory()) {
continue;
}
} else {
if (!file.isFile()) {
continue;
}
}
resultList.add(file);
}
}
splitDexPath这个方法里面调用splitPaths()方法,而splitPaths方法调用了splitAndAdd()方法,通过代码查看,大概能明白,这个一系列的方法主要作用是过滤,过滤掉不可读的file和不存在的file,即剩下的都是canRead且是exists的,然后吧这些files add进一个ArrayList<File>,然把这个这个ArrayList<File>作为参数,调用makeDexElements这个方法,那么咱么一起看下这个方法
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
//上面是支持的后缀,由于在下面这个方法用到了,我就放到到这里
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile 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)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
上面的方法大概的意思是,遍历刚上传入的ArrayListM<File>,如果是.dex结尾的直接调用loadDexFile方法,如果是.apk或者.zip或者.jar结尾的用这个File去构造一个ZipFile对象,然后还是把这个ZipFile作为参数调用loadDexFile这个方法,那么咱们就去这个方法里面去看看
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
上面代码有个设置如果optimizedDirectory==null(PS:PathClassLoader其中的optimizedDirectory就是null)则直接new一个DexFile,如果不为空则调用optimizedPathFor方法,optimizedPathFor就是把复制一份file放到 optimizedDirectory目录下,最后把这个文件返回回去。 得到这个DexFile以后,用这个DexFile构造一个Element对象 在makeDexElements的for循环里面依照上面的方法获取一组DexFile,然后用这一组DexFile去组成Element数组对象放到内存中。
上述仅仅是构造DexClassLoader流程,下面咱们看下具体导入类的流程
loadClass()方法,由于DexClassLoader类本身就一个构造函数,所以知道这个方法是父类的方法,那么找下DexClassLoader的父类BaseDexClassLoader.java,结果发现BaseDexClassLoader.java也没有这个方法,所以应该在BaseDexClassLoader.java的父类里面,那么继续寻找BaseDexClassLoader.java的父类ClassLoader里面有这个方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
看上面的源代码发现是先调用findLoadedClass(name) 看注释应该可以理解为检查下是否已经加载了,如果已经加载了,则直接返回Class,如果没有加载,则看看有没有父类加载器,如果有父类加载器,则调用父类加载器的loadClass()方法,如果没有父类加载器即根类加载器,通过根加载器加载(想下是不是上面说的双亲委托模型)。如果都没有,则通过findClass()方法查找,那么进入findClass()进去看看,通过上面源代码发现ClassLoader是个空方法,而DexClassLoader大家也知道,就一个构造函数,所以可以确定这个方法的具体实现在BaseClassLoader里面,那么咱们现在进去BaseClassLoader里面看看
@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是通过pathList对象的findClass()方法来获取类的,那么咱们继续进去DexPathList.java的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;
}
最后调用两个DexFile的loadClassBinaryName来导入类的,现在进入DexFIle.java中的loadClassBinaryName()方法中去看下
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()方法里面调用了defineClass()方法,而defineClass里面又调用natvie方法defineClassNative()在C层去加载类,由于涉及到底层的业务,由于涉及到比较大的内容,这里就不过多的叙述了,后续有时间单独再出一个C层的分析文章。defineClassNative大家可以先理解为通过底层去加载类,如果有这个类,就加载出来,至此整个流程已经完全跑完。不知道大家理解没有。
由上述可以归结出android类 加载的时序图,如下图: (第一次画时序图,画的不好,大家将就的看下,(__) 嘻嘻……)
时序图.png
loadClass.png
可以看出BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile数组,由上面分析知道,dexPath传入的原始(.apk,.zip,jar等)文件在optimizedDirectory文件夹生成相应的优化后的odex文件,dexElements数组就是这些odex文件的集合,如果不分包,一般这个数组只有一个Element元素,也就只有一个DexFile文件,而对于这个类加载,就是遍历这个集合,通过DexFile去寻找。最终调用native方法的defineClass
DexClassLoader和PathClassLoader都属于双亲委托模型的类加载器。也就是说,它们在加载一个类之前,会去检查自己及自己以上的类加载器是否已经加载过这个类,如果加载过,就会直接将之返回,而不会重复加载 PathClassLoader是通过构造函数new DexFile(path)来产生DexFile对象的;而DexClassLoader则是通过静态方法loadDex(path,outpath,0)得到DexFile对象。这两者的区别在于DexClassLoader需要提供一个可写的outputpath路径,用来释放apk包或者jar包中的dex文件。换个说法来说,就是PathClassLoader不能从zip包中释放dex,因此只支持直接操作Dex格式的文件,或者已经安装apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持apk.jar,dex文件,并且会制定的outpath路径释放dex文件
感谢 http://androidxref.com/6.0.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java http://gityuan.com/2017/03/19/android-classloader/ http://www.jianshu.com/p/2c23a9e88e3d http://www.blogjava.net/zh-weir/archive/2011/10/29/362294.html https://segmentfault.com/a/1190000004062880 http://www.voidcn.com/blog/Mr_LiaBill/article/p-4979756.html https://yq.aliyun.com/ziliao/160711?spm=5176.8246799.blogcont.19.IRwTYy http://www.cnblogs.com/coding-way/p/5212208.html