美文网首页
Android 中的类加载器

Android 中的类加载器

作者: as_pixar | 来源:发表于2020-04-15 17:33 被阅读0次

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类。

相关文章

网友评论

      本文标题:Android 中的类加载器

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