Android 中的ClassLoader
本质上,Android和传统的JVM一样,也需要通过ClassLoader 将字节码文件加载到虚拟机。类加载机制也符合双亲委派模型,不熟悉Java 的类加载器机制同学请看JAVA一看你就懂ClassLoader详解,但是在Android中,ClassLoader略有区别。
在Android 虚拟机无法直接运行字节码文件,Android 会将所有的.class文件转换成dex文件,并且Android加载dex文件的实现封装在BaseDexClassLoader中,而我们一般只使用它们的两个子类PathClassLoader和DexClassLoader。
PathClassLoader
PathClassLoader 用来加载系统APK和被安装到手机中APK内的dex文件。它的两个构造函数如下:
package dalvik.system;
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
public class PathClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
* <ul>
* <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
* <li>Raw ".dex" files (not inside a zip file).
* </ul>
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
参数说明:
- dexPath:dex文件路径,或者包含dex文件的jar包路劲
- librarySearchPath:c/c++ native库路径
PathClassLoader 里面除了两个构造方法就没有别的代码,其它的具体实现都在BaseDexClassLoader里面,其dexPath比较受限制,一般是已经安装应用的apk文件路径。
当一个apk被安装到手机中,apk里面的class.dex中的class文件均是通过PathClassLoader 来加载的,可以通过如下代码验证:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader classLoader = getClassLoader();
Log.i("tag", classLoader.toString());
}
}
打印结果如下:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.asmlifecycledemo-2/base.apk"],nativeLibraryDirectories=[/data/app/com.example.asmlifecycledemo-2/lib/x86_64, /system/lib64, /vendor/lib64]]]
DexClassLoader
先来看官方对DexClassLoader的描述
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code noti nstalled as part of an application.
很明显,对比PathClassLoader只能加载已安装的apk或者dex文件,DexClassLoader则没有此限制。可以从SDCard上加载 .apk 和 .dex文件,这也是热修复和插件化的基础。在不需要安装应用的情况下,完成需要使用的dex加载。
package dalvik.system;
import java.io.File;
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getCodeCacheDir();
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
dexPath:包含class.dex文件的apk,jar文件路径,多个路径用文件分隔符(默认是":")分隔。
optimizedDirectory:用来缓存优化的dex文件路径,既从apk或者jar文件中提取出来的dex文件。该路径不可以为空,且应该市应用私有的,具有读写权限的路径。
使用DexClassLoader实现热修复
理论知识都是作为实践基础,接下来我们就使用DexClassLoader来模拟热修复的实现。
创建Android项目 DexClassLoader
项目结构如下:
ISay是一个接口,内部只定义一个方法saySomething。
public interface ISay {
String saySomething();
}
SayException 实现了 ISay接口,但是在 saySomething 方法中,打印"天呀,今天下暴雨了!"来模拟一个线上的 bug。
public class SayException implements ISay {
@Override
public String saySomething() {
return "天呀,今天下暴雨了!";
}
}
最后在MainActivity.java中,当点击Button的时候,将saySomething返回的内容通过Toast显示在屏幕上。
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
public class MainActivity extends AppCompatActivity {
ISay iSay;
AppCompatButton btnSay;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnSay = findViewById(R.id.btnSay);
btnSay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
iSay = new SayException();
Toast.makeText(MainActivity.this, iSay.saySomething(), Toast.LENGTH_LONG).show();
}
});
}
}
运行结果图:
创建HotFix补丁包
创建Java项目,创建两个文件ISay.java和SayHotFix.java文件
package com.wwj.dexclassloader;
public interface ISay {
String saySomething();
}
package com.wwj.dexclassloader;
public class SayHotFix implements ISay {
@Override
public String saySomething() {
return "雨后天晴有好多彩虹";
}
}
ISay接口的包名和类型必须和Android项目中保持一致。SayHotFix 实现ISay接口,并在saySomething中返回新的结果,用来模拟bug修复后的结果。
将ISay.java和SayHotFix.java文件打包成say_something.jar,然后通过 dx 工具将生成的 say_something.jar 包中的 class 文件优化为 dex 文件。
dx --dex --output=say_something_hotfix.jar say_something.jar
上述 say_something_hotfix.jar 就是我们最终需要用作 hotfix 的 jar 包
将 HotFix 补丁包(patch) 包拷贝到 SD 卡主目录,并使用 DexClassLoader 加载 SD 卡中的 ISay 接口
首先将 HotFix patch 保存到本地目录下。一般在真实项目中,我们可以通过向后端发送请求的方式,将最新的HotFix patch下载到本地。
接下来,修改 MainActivity 中的逻辑,使用 DexClassLoader 加载 HotFix patch 中的 SayHotFix 类,如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//请求获取SDCard权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 10);
AppCompatButton btnSay = findViewById(R.id.btnSay);
btnSay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
File file = new File(Environment.getExternalStorageDirectory().getPath()
+ File.separator + "say_something_hotfix.jar");
if (!file.exists()) {
ISay iSay = new SayException();
Toast.makeText(MainActivity.this, iSay.saySomething(), Toast.LENGTH_LONG).show();
} else {
Log.i("tag", getExternalCacheDir().getAbsolutePath());
DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath()
, getExternalCacheDir().getAbsolutePath(), null, getClassLoader());
try {
Class<?> aClass = dexClassLoader.loadClass("com.wwj.dexclassloader.SayHotFix");
ISay iSay = (ISay) aClass.newInstance();
Toast.makeText(MainActivity.this, iSay.saySomething(), Toast.LENGTH_LONG).show();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
});
}
在货物清单中添加SDCard权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
最后运行效果如下:
有些朋友对BaseDexClassLoader感到好奇,我们来瞧一瞧看一看,又不要钱。
package dalvik.system;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
//加载字节码的对象
private final DexPathList pathList;
/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
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>();\
//通过DexPathList 对象查找一个class文件
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;
}
@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);
}
/**
* Returns package information for the given package.
* Unfortunately, instances of this class don't really have this
* information, and as a non-secure {@code ClassLoader}, it isn't
* even required to, according to the spec. Yet, we want to
* provide it, in order to make all those hopeful callers of
* {@code myClass.getPackage().getName()} happy. Thus we construct
* a {@code Package} object the first time it is being requested
* and fill most of the fields with dummy values. The {@code
* Package} object is then put into the {@code ClassLoader}'s
* package cache, so we see the same one next time. We don't
* create {@code Package} objects for {@code null} arguments or
* for the default package.
*
* <p>There is a limited chance that we end up with multiple
* {@code Package} objects representing the same package: It can
* happen when when a package is scattered across different JAR
* files which were loaded by different {@code ClassLoader}
* instances. This is rather unlikely, and given that this whole
* thing is more or less a workaround, probably not worth the
* effort to address.
*
* @param name the name of the class
* @return the package information for the class, or {@code null}
* if there is no package information available for it
*/
@Override
protected synchronized Package getPackage(String name) {
if (name != null && !name.isEmpty()) {
Package pack = super.getPackage(name);
if (pack == null) {
pack = definePackage(name, "Unknown", "0.0", "Unknown",
"Unknown", "0.0", "Unknown", null);
}
return pack;
}
return null;
}
/**
* @hide
*/
public String getLdLibraryPath() {
StringBuilder result = new StringBuilder();
for (File directory : pathList.getNativeLibraryDirectories()) {
if (result.length() > 0) {
result.append(':');
}
result.append(directory);
}
return result.toString();
}
@Override public String toString() {
return getClass().getName() + "[" + pathList + "]";
}
}
我们发现DexPathList对象负责字节码文件的加载和资源加载。感兴趣的同学自己先研究DexPathList这个类,我们改天在继续写这个东东。
总结:
Android 中常用的两种 ClassLoader 分别为:PathClassLoader 和 DexClassLoader,真正负责加载字节码文件的时DexPathList类。
网友评论