Android插件化探索(一)类加载器DexClassLoader

本文部分内容参考自《Android内核剖析》

基本概念

在Java环境中,有个概念叫做“类加载器”(ClassLoader),其作用是动态装载Class文件。标准的Java SDK中有一个ClassLoader类,借助它可以装载想要的Class文件,每个ClassLoader对象在初始化时必须指定Class文件的路径

没有使用过ClassLoader的读者可能会问:“在过去的程序开发中,当我们需要某个类时,只需使用import关键字包含该类就可以了,为什么还要类加载器呢?”简单的讲,import中所引用的类文件有两个特点:

  • 必须存在于本地,当程序运行时需要该类时,内部类装载器会自动装载该类,这对程序员来讲是透明的,即程序员感知不到这一过程。
  • 编译时必须在现场,否则编译不过会因为找不到引用文件而正常编译。

但在有些情况下,所需的类却不能满足以上两个条件。比如当该类时从远程下载并在本地执行时,典型的例子就是通过浏览器中的AppleLet执行的Java程序,这些要执行的程序是在服务器端。另一种情况是,要引用的Class文件不方便在编译时直接参与,而只能运行时动态调用。举例来讲,在Android Framework中,所包含的Class文件是一些通用的类文件,但对于一些设备商而言,他们需要扩充Framework,扩充的具体工作包括两点:

  • 需要增加一些额外的类文件,这些类文件提供厂商自定义的功能,这些文件一般以独立的Jar包存在。
  • 需要修改Framework中的已有类文件,比如WindowManagerServcie类,在该类中添加使用自定义Jar包中的代码。使用自定义Jar包的常用方法是使用import关键字包含的自定义的类,但为了保持和原生Framework的兼容性、对于原生Framework最少化修改,可以使类装载器动态装载自定义Jar包。

这就是使用ClassLoader的原因。

在一般情况下,应用程序不需要创建一个全新的ClassLoader对象,而是使用当前环境已经存在的ClassLoader。因为Javad的Runtime环境在初始化时,其内部会创建一个ClassLoader对象用于加载Runtime所需的各种Java类。

每个ClassLoader必须有一个父ClassLoader,在装载Class文件时,子ClassLoader会先请求父ClassLoader加载该Class文件,只有当其父ClassLoader找不到该Class文件时,子ClassLoader才会继续装载该类,这是一种安全机制。关系ClassLoader的内部过程,大家可以参考《Inside the Java Virtual Machine》一书,作者为Bill Venners,相关链接如下:

http://www.artima.com/insidejvm/ed2/index.html

对于Android的应用程序,本质上虽然也是用Java开发,并且使用标准的Java编译器编译出Class文件,但最终的APK文件中包含的却是dex类型的文件。dex文件是将所需的所有Class文件重新打包,打包的规则不是简单的压缩,而是完全对Class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是dex文件。由于dex文件是一种经过优化的Class文件,因此要加载这样特殊的Class文件就需要特殊的类装载器,这就是DexClassLoader,Android SDK中提供的DexClassLoader类就是出于这个目的。

初始API

//DexClassLoader的构造方法
DexClassLoader (String dexPath,
                String optimizedDirectory,
                String libraryPath,
                ClassLoader parent)
  • dexPath: 指目标类所在的jar/apk文件路径, 多个路径使用 File.pathSeparator分隔, Android里面默认为 “:”
  • optimizedDirectory: 解压出的dex文件的存放路径,以免被注入攻击,不可存放在外置存储。

    下面来看DexClassLoader的使用方法。

  • libraryPath :目标类中的C/C++库存放路径。
  • parent: 父类装载器

使用方法

DexClassLoader的使用方法一般有两种:

1. 从已安装的apk中读取dex

2. 从apk文件中读取dex

假如有两个APK,一个是宿主APK,叫作HOST,一个是插件APK,叫作Plugin。Plugin中有一个类叫PluginClass,代码如下:

public class PluginClass {
    public PluginClass() {
        Log.d("JG","初始化PluginClass");
    }

    public int function(int a, int b){
        return a+b;
    }
}  

现在如果想调用插件APK中PluginClass内的方法,应该怎么办?

从已安装的apk中读取dex

先来看第一种方法,这种方法必须建一个Activity,在清单文件中配置Action.

    <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="com.maplejaw.plugin"/>
            </intent-filter>
   </activity>

然后在宿主APK中如下使用

  /**
     * 这种方式用于从已安装的apk中读取,必须要有一个activity,且需要配置ACTION
     */
  private void useDexClassLoader(){
        //创建一个意图,用来找到指定的apk
        Intent intent = new Intent("com.maplejaw.plugin");
        //获得包管理器
        PackageManager pm = getPackageManager();
        List<ResolveInfo> resolveinfoes =  pm.queryIntentActivities(intent, 0);
        if(resolveinfoes.size()==0){
            return;
        }
        //获得指定的activity的信息
        ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;

        //获得包名
        String packageName = actInfo.packageName;
        //获得apk的目录或者jar的目录
        String apkPath = actInfo.applicationInfo.sourceDir;
        //dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己
        //目录下的文件
        String dexOutputDir = getApplicationInfo().dataDir;

        //native代码的目录
        String libPath = actInfo.applicationInfo.nativeLibraryDir;

        //创建类加载器,把dex加载到虚拟机中
        DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath,
                this.getClass().getClassLoader());

        //利用反射调用插件包内的类的方法

        try {
            Class<?> clazz = calssLoader.loadClass(packageName+".PluginClass");

            Object obj = clazz.newInstance();
            Class[] param = new Class[2];
            param[0] = Integer.TYPE;
            param[1] = Integer.TYPE;

            Method method = clazz.getMethod("function", param);

            Integer ret = (Integer)method.invoke(obj, 12,34);

            Log.d("JG", "返回的调用结果为:" + ret);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我们安装完两个APK后,在宿主中就可以直接调用,调用示例如下。

    public void btnClick(View view){
        useDexClassLoader();
    }

可以看出控制台打印结果如下。

05-24 14:43:12.239 4068-4068/com.maplejaw.host D/JG: 初始化PluginClass
05-24 14:43:12.240 4068-4068/com.maplejaw.host D/JG: 返回的调用结果为: 46

从apk文件中读取dex

这种方法由于并不需要安装,所以不需要通过Intent从activity中解析信息。换言之,这种方法不需要创建Activity。无需配置清单文件。我们只需要打包一个apk,然后放到SD卡中即可。

核心代码如下:

    //apk路径
    String path=Environment.getExternalStorageDirectory().getAbsolutePath()+"/1.apk";

    private void useDexClassLoader(String path){

        File codeDir=getDir("dex", Context.MODE_PRIVATE);

        //创建类加载器,把dex加载到虚拟机中
        DexClassLoader calssLoader = new DexClassLoader(path, codeDir.getAbsolutePath(), null,
                this.getClass().getClassLoader());

        //利用反射调用插件包内的类的方法

        try {
            Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");

            Object obj = clazz.newInstance();
            Class[] param = new Class[2];
            param[0] = Integer.TYPE;
            param[1] = Integer.TYPE;

            Method method = clazz.getMethod("function", param);

            Integer ret = (Integer)method.invoke(obj, 12,21);

            Log.d("JG", "返回的调用结果为: " + ret);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行结果如下:

05-24 14:45:12.239 4068-4068/com.maplejaw.host D/JG: 初始化PluginClass
05-24 14:45:12.240 4068-4068/com.maplejaw.host D/JG: 返回的调用结果为: 33

插件概念

插件是一个逻辑概念,而不是什么技术标准。总的来讲插件的概念包含以下意思:

  • 插件不能独立运行,必须运行与一个宿主程序中,即由宿主程序去调用插件程序。
  • 插件一般可以独立安装。
  • 宿主程序中可以管理不同的插件,包或查看插件的多少,禁用和使用某个插件,如果多个插件功能是互斥的,则可以切换插件。
  • 宿主程序应该保证参见的向下兼容性,即新版本的宿主程序可以运行较老版本的插件,或者说较老版本的插件能够在新版本的宿主程序中运行。
  • 由于ClassLoader具有动态装载程序的特点,因此,可以使用该技术来实现一种插件架构。

插件架构化

通过ClassLoader装载的类,调用其内部函数的过程有点繁琐,使用反射构造Method对象、构造参数等等。那么,有没有一种方法,既能通过动态装载,利用动态装载的灵活性,又能像直接类引用那样方便地调用其函数?答案是有的,接口(Interface)。

首先定义一个interface接口,interface仅仅定义函数的输入输出,不定义函数的具体实现。该interface类一方面存在于Plugin项目中,另一方面存在于HOST宿主项目中。

这种方法,需保证接口的完整类名(包名+类名)是一样的,否则将会报如下异常。

 java.lang.ClassCastException: com.maplejaw.plugin.PluginClass cannot be cast to com.maplejaw.host.Comm

我们应该保证两者的完整类名是一致的。一般会建一个插件接口库,给两个项目分别引用即可。又或者,在两个工程中创建一个同样名字的用于存放插件接口的包,然后把插件接口类统一放到那个包下即可。

接口定义如下:

public interface Comm {
    int function(int a, int b);
}

现将PluginClass修改成如下:

public class PluginClass implements Comm {
    public PluginClass() {
        Log.d("JG","初始化PluginClass");
    }

    @Override
    public int function(int a, int b) {
        return a+b;
    }
}  

相应的调用核心代码修改如下:

   Class<?> clazz = calssLoader.loadClass(pacageName+".PluginClass");
   Comm obj = (Comm) clazz.newInstance();
   Integer integer=obj.function(33,44);
   Log.d("JG", "返回的调用结果为:" + integer);

打印结果如下

05-24 16:17:22.033 12963-12963/com.maplejaw.host D/JG: 初始化PluginClass
05-24 16:17:22.035 12963-12963/com.maplejaw.host D/JG: 返回的调用结果为:77

注意!!!如果你按照上面进行操作,会发现这种方法在Android5.0以上运行没有任何问题,但是在5.0以下运行。你会发现报错了!!!

java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
   at dalvik.system.DexFile.defineClassNative(Native Method)
   at dalvik.system.DexFile.defineClass(DexFile.java:222)
   at dalvik.system.DexFile.loadClassBinaryName(DexFile.java:215)

从字面意思可以知道Class预校验出错,为什么会报这个错呢?那是因为插件接口被同一个加载器装载了两次。由于插件接口存在于两个不同的dex文件中,每个dex文件有一个类型id,检测到不一致所以报错。如果想加载两个相同的类,一定要用两个加载器去分别装载。你可能心想,我不是new了一个类加载器吗?明明不一样啊。由于双亲委托原则,会请求父加载器去加载,所以导致加载器是一样的。

那么怎么解决这一问题呢?思路很简单。只需保证插件接口只被装载一次就行了,一般选择让宿主APK加载。。

先把插件接口打包成jar包plugin.jar。

然后在宿主apk中如下引用

compile files(‘libs/plugin.jar‘)

在插件apk中如下引用,这种方式在打包时不会将jar包一起打包进去

provided files(‘libs/plugin.jar‘)

获取资源文件

在了解了ClassLoader的基本用法后,那么问题来了,如果想访问插件中的资源文件怎么办?

获取资源的方式比较简单,首先得知道名字,这些名字最好要事先约定好,根据名字获取相应的id,最后用id取相应资源。Android中提供的获取Resource得API:

 Resources res= pm.getResourcesForApplication(packageName);
  • 取图片资源

    首先在插件APK中的drawable文件夹中放进图片a.jpg。然后在宿主APK中编写核心代码如下。

    private void useDexClassLoader(){
        //创建一个意图,用来找到指定的apk
        Intent intent = new Intent("com.maplejaw.plugin");
        //获得包管理器
        PackageManager pm = getPackageManager();
        List<ResolveInfo> resolveinfoes =  pm.queryIntentActivities(intent, 0);
        if(resolveinfoes.size()==0){
            return;
        }
        //获得指定的activity的信息
        ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
        //获得包名
        String packageName = actInfo.packageName;

        try {
            Resources res= pm.getResourcesForApplication(packageName);
            int id=res.getIdentifier("a","drawable",packageName);//根据名字取id
            mImageView.setImageDrawable(res.getDrawable(id));//设置给ImageView
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

    }
  • 取String

    比如插件APK中的string中写了author信息。

     <resources>
       <string name="author">maplejaw</string>
    </resources>

    取出author信息

    Resources res= pm.getResourcesForApplication(packageName);
    int id=res.getIdentifier("author","string",packageName);
    Log.d("JG", res.getString(id));
  • 取颜色
    Resources res= pm.getResourcesForApplication(packageName);
    int id=res.getIdentifier("colorPrimary","color",packageName);
    mImageView.setBackgroundColor(  res.getColor(id));

源码解读

从上面的例子可以看出,一般使用Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");来加载类,那么这个类做了什么呢?

由于DexClassLoader继承自BaseDexClassLoader,且遵循着双亲委托,那我们先来看下BaseDexClassLoader中的源码。

构造方法如下,可见一个DexClassLoader包含一个DexPathList。DexPathList用一个来存放dex信息的列表

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

我们来简单看一下DexPathList的源码。

属性列表如下:

  private static final String DEX_SUFFIX = ".dex";

    //一个ClassLoader对象
    private final ClassLoader definingContext;
    //一个存放dex元素列表,Element是DexPathList的一个内部类
    private final Element[] dexElements;
    //本地库目录列表
    private final File[] nativeLibraryDirectories;
    //创建dexElements抛出的异常集合
    private final IOException[] dexElementsSuppressedExceptions;

Element内部类的构造方法如下:

        private final File file;
        private final boolean isDirectory;
        private final File zip;
        private final DexFile dexFile;

   public Element(File file, boolean isDirectory, File zip, DexFile dexFile) {
            this.file = file;
            this.isDirectory = isDirectory;
            this.zip = zip;
            this.dexFile = dexFile;
        }

DexPathList的构造方法如下

    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        //..
       //省略了异常相关源码

        //直接赋值ClassLoader对象
        this.definingContext = definingContext;

         //赋值数组,splitDexPath将多个路径拆分成集合,makeDexElements根据路径遍历存取
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
          //..
         //省略了异常相关源码

          //赋值本地库目录集合
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

我们来看看makeDexElements方法,看看dexElements数组是怎么赋值的。

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
        //开始遍历保存到dexElements集合
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            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 if (file.isFile()){
                if (name.endsWith(DEX_SUFFIX)) {
                        dex = loadDexFile(file, optimizedDirectory);
                } else {
                    zip = file;
                   dex = loadDexFile(file, optimizedDirectory);
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

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

我们现在回到Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");,从ClassLoader类中找到loadClass源码,如下

   //loadClass(String)
    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }

内部调用了loadClass的重载方法。

 //loadClass(String,boolean)
  protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);//从已装载过的类中找。

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);//由父类装载
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    clazz = findClass(className);//由子类装载
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

可以看出,该方法分三步装载类:

* 从已装载过的类中找

* 如果从已装载过的列表中找不到,则从父类装载

* 如果父类找不到,从子类装载

先来看看findLoadedClass(已装载过的类)源码如下,最终调用虚拟机的装载器去寻找。

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

由于第一次装载一定会走findClass这个方法,我们来看下源码,可以看出,最终会去pathList中寻找

    @Override
    protected Class<?> findClass(String name)  {
       //..
       //省略了部分源码
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            //..
         //省略了抛出异常的源码
        }
        return c;
    }

找到DexPathList的findClass方法。

 public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {//这里进行遍历查询
            DexFile dex = element.dexFile;

            if (dex != null) {
               //从DexFile中试图加载Class,从这里看出,从第一个开始遍历,如果查到就返回,这就是热修复的基本原理。
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        //..
        return null;
    }

最后

由于SDK中无法直接查看DexClassLoader相关源码。这里把源码链接贴出来。方便大家阅读。

时间: 2024-11-10 07:09:14

Android插件化探索(一)类加载器DexClassLoader的相关文章

Android插件化探索(三)免安装运行Activity(上)

[Android插件化探索(一)类加载器DexClassLoader] [Android插件化探索(二)资源加载] 前情提要 在上一篇中有一个细节没有提到,那就是getResourcesForApplication和AssetManager的区别. getResourcesForApplication getResourcesForApplication(String packageName),很显然需要传入一个包名,换言之,这个插件必须已经被安装在系统内,然后才能通过包名来获取资源.你可能会想

Android插件化探索(二)资源加载

前情提要 在探索资源加载方式之前,我们先来看看上一篇中没细讲的东西.还没看过的建议先看上一篇Android插件化探索(一)类加载器DexClassLoader. PathClassLoader和DexClassLoader的区别 DexClassLoader的源码如下: public class DexClassLoader extends BaseDexClassLoader { //支持从任何地方的apk/jar/dex中读取 public DexClassLoader(String dex

Android插件化开发之DexClassLoader动态加载dex、jar小Demo

一.温故动态加载ClassLoader机制 如果对Android的ClassLoader加载机制不熟悉,猛戳Android插件化开发动态加载基础之ClassLoader工作机制 http://blog.csdn.net/u011068702/article/details/53248960 二.介绍 我们知道在Android中可以跟java一样实现动态加载jar,但是Android使用德海Dalvik VM,不能直接加载java打包jar的byte code,需要通过dx工具来优化Dalvik

Android插件化(二):使用DexClassLoader动态加载assets中的apk

Android插件化(二):使用DexClassLoader动态加载assets中的apk 简介 上一篇博客讲到,我们可以使用MultiDex.java加载离线的apk文件.需要注意的是,apk中的类是加载到当前的PathClassLoader当中的,如果apk文件过多,可能会出现ANR的情况.那么,我们能不能使用DexClassLoader加载apk呢?当然是可以的!首先看一下Doc文档. A class loader that loads classes from .jar and .apk

Android插件化开发---运行未安装apk中的Service

如果你还不知道什么叫插件化开发,那么你应该先读一读之前写的这篇博客:Android插件化开发,初入殿堂 上一篇博客主要从整体角度分析了一下Android插件化开发的几个难点与动态加载没有被安装的apk中的Activity和资源的方法.其实一般的插件开发主要也就是加载个Activity,读取一些资源图片之类的.但是总有遇到特殊情况的时候,比如加载Service. 要动态加载Service,有两种思路:一是通过NDK的形式,将Service通过C++运行起来(这种方法我没有尝试,只听群里的朋友说实现

自己动手写Android插件化框架

最近在工作中接触到了Android插件内的开发,发现自己这种技术还缺乏最基本的了解,以至于在一些基本问题上浪费不少时间,如插件Context和主工程Context的区别,权限必须在主工程申明等,因此花了点时间了解了一下插件的历史,并写了两个Demo作为总结.本文旨在通过两个实例直观的说明插件的实现原理以加深对插件内开发的理解,因此不会深入探讨背景和原理,代码也尽量专注于核心逻辑. 原理与背景 Android插件化从技术上来说就是如何启动未安装的apk(主要是四大组件)里面的类,主要问题涉及如何加

android插件化-apkplug中OSGI服务基本原理-08

我们提供 apkplug 下OSGI使用demo 源码托管地址为 http://git.oschina.net/plug/OSGIService 一 OSGI与android Service 异同点 OSGI服务与android Service概念差不多也是Service ,Client 关系. android Service接口  --service.AIDL OSGI接口                --java interface 所以android 进程间通信Service只能传递序列

Android插件化开发,初入殿堂

好久没有写博客了,这次准备写写我这几天的研究成果--Android插件化开发框架CJFrameForAndroid. 好久没有写博客了,这次准备写写我这几天的研究成果--Android插件化开发框架CJFrameForAndroid. 背景交代 首先,你需要知道什么是插件化开发.就拿最常见的QQ来说,在第三个界面动态那里有个管理,点开后可以选择很多的增植功能,这里腾讯只放了一些网页应用,那么如果未来想加入一个打飞机游戏,要怎么做?让用户重新安装吗,这就是插件化开发所解决的问题. 用一句话来概括插

Android插件化的思考——仿QQ一键换肤,思考比实现更重要!

Android插件化的思考--仿QQ一键换肤,思考比实现更重要! 今天群友希望写一个关于插件的Blog,思来想去,插件也不是很懂,只是用大致的思路看看能不能模拟一个,思路还是比较重要的,如果你有兴趣的话,也可以加群:555974449,你也可以说出你想看的Blog哦,嘿嘿!好的,不多说,我们进入正题: 关于QQ的换肤,他们的实现思路我不是很清楚,但是你可以看一下这张换肤的截图 我们想使用哪个主题就直接下载就好了,这一实现的过程我们大致的可以猜想: 首选是下载到本地指定文件夹,然后通过插件加载到我