Android动态加载机制

Android能够实现动态加载机制,得益于java虚拟机团队设计的类加载,把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何获取所需要的的类,实现这个动作的代码模块称为“类加载器”(参考《深入理解Java虚拟机–JVM高级特性与最佳实践》7.4类加载器)。

一、Android中的ClassLoader

Java中的ClassLoader是加载class文件,而Android中的虚拟机无论是dvm还是art都只能识别dex文件。因此Java中的ClassLoader在Android中不适用。Android中的java.lang.ClassLoader这个类也不同于Java中的java.lang.ClassLoader。
为什么Android要自创dex,而不用java的class?

  • dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。
  • 传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。

Android中的ClassLoader类型也可分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种分别是:

  • BootClassLoader,Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。BootClassLoader是ClassLoader的一个内部类。
  • PathClassLoader,全名是dalvik/system.PathClassLoader,可以加载已经安装的Apk,也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。
  • DexClassLoader,全名是dalvik/system.DexClassLoader,可以加载一个未安装的apk文件。

在MainActivity中打印当前的ClassLoader,

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            System.out.println("classLoader: " + classLoader);
            classLoader = classLoader.getParent();
        }
    }
}

结果如下:

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sososeen09.classloadtest-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
java.lang.BootClassLoader@aced87d

从打印的结果也可以证实:App系统类加载器是PathClassLoader,而BootClassLoader是其parent类加载器。

二、ClassLoader分析
在Android中我们主要关心的是PathClassLoader和DexClassLoader。
PathClassLoader和DexClasLoader都是继承自 dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在BaseDexClassLoader中。PathClassLoader用来操作本地文件系统中的文件和目录的集合。DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件。DexClassLoader需要一个对应用私有且可读写的文件夹来缓存优化后的class文件。而且一定要注意不要把优化后的文件存放到外部存储上,避免使自己的应用遭受代码注入攻击。
Android中具体负责类加载的并不是哪个ClassLoader,而是通过DexFile的defineClassNative()方法来加载的。
在这里插入图片描述三、动态加载(热修复)实例
1、新建一个module叫dextest,加入这就是我们要动态加载进来的模块。
里面新建一个叫做ShowToast 的类:

public class ShowToast {

    public void showToast(Context context) {
        Toast.makeText(context, "动态加载ShowToast", Toast.LENGTH_SHORT).show();
    }
}

ShowToast 类里面只有一个showToast方法,该方法需要一个context参数,展示一个toast。
2、把java类打包到dex中
编译之后,在如图所示的目录(不同版本的android studio中可能路径会不一样)中会生成相应的class文件,由于class文件是java虚拟机所用的,但是在安卓中的Dalvik虚拟机要使用dex文件,所以我们需要再进行转换。
在这里插入图片描述还是这个module中的build.gradle文件最后添加:

//删除isshowtoast.jar包任务
task clearJar(type: Delete) {
    delete 'build/libs/in.jar'
}
task makeJar(type:org.gradle.api.tasks.bundling.Jar){
    //指定生成的jar名
    baseName 'in'
    //从哪里打包class文件
    from('build/intermediates/javac/debug/classes/com/example/dextest/')
    //打包到jar后的目录结构
    into('com/example/dextest/')
    //去掉不需要打包的目录和文件
    exclude('test/','BuildConfig.class','R.class')
    //去掉R$开头的文件
    exclude{it.name.startsWith('R$')}
}
makeJar.dependsOn(clearJar,build)

这样就把我们需要的文件打包到jar包中了,然而jar包并不是我们需要的,继续。
在Android的SDK中为我们提供了一个dx命令(在\android-sdk\build-tools\version[23.0.1] 或 \android-sdk\platform-tools下能找到);命令使用方式为:dx --dex --output=out.jar in.jar,该命令将包含class的in.jar转化为包含dex的out.jar文件。
3、app端调用
把生成的out.jar包复制到app项目中的assets目录下:
在这里插入图片描述我们这里是为了方便,真是场景可能是远程服务器下载到指定位置。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadDex();
            }
        });
    }

    /**
     * 点击事件
     */
    public void loadDex() {
        File cacheFile = getDir("dex",0);
        String internalPath = cacheFile.getAbsolutePath() + File.separator + "out.jar";
        File desFile=new File(internalPath);
        try {
            if (!desFile.exists()) {
                desFile.createNewFile();
                FileUtils.copyFiles(this,"out.jar",desFile);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //下面开始加载dex class
        //1.待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限,
        //2.解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写
        //3.指向包含本地库(so)的文件夹路径,可以设为null
        //4.父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
        DexClassLoader dexClassLoader=new DexClassLoader(internalPath,cacheFile.getAbsolutePath(),null,getClassLoader());
        try {
            Class clz = dexClassLoader.loadClass("com.example.dextest.ShowToast");
            Method method = clz.getDeclaredMethod("showToast", Context.class);
            method.invoke(clz.newInstance(), this);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里的FileUtils文件就是把jar包从assets目录拷贝到app的data目录下:

public class FileUtils {
    // 把assets目录的文件复制到desFile中
    public static void copyFiles(Context context, String fileName, File desFile){
        InputStream in=null;
        OutputStream out=null;

        try {
            in=context.getApplicationContext().getAssets().open(fileName);
            out=new FileOutputStream(desFile.getAbsolutePath());
            byte[] bytes=new byte[1024];
            int len=0;
            while ((len=in.read(bytes))!=-1)
                out.write(bytes,0,len);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (in!=null)
                    in.close();
                if (out!=null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

一开始在这里我碰到一个ClassNotFoundException的异常:

ClassLoader referenced unknown path: android.content.res.AssetManager@d75e3f9/out.jar

 java.lang.ClassNotFoundException: Didn't find class "com.example.dextest.ShowToast" on path: DexPathList[[],nativeLibraryDirectories=[/system/lib, /system/vendor/lib]]
     at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
     at com.example.test.MainActivity.loadDex(MainActivity.java:44)

因为一开始我直接使用assets目录下的jar包,好像加载不进来,然后我就使用FileUtils把jar包复制到另外一个目录就可以了。
在测试机点击按钮就可以看到效果了:
在这里插入图片描述
总结一下就是使用DexClassLoader来获取dex中的类,然后通过反射的方式运行类中的方法。

还有几个问题:
a、如果原有的类有bug,现在要动态替换原有类,如何操作?
参考android热修复,使用Javassist。
b、如果要动态加载新的activity,如何管理生命周期?
c、如何加载资源文件?

参考:类加载机制系列2——深入理解Android中的类加载器
Android动态加载dex入门
Android动态加载Dex过程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值