您的位置:首页 > 移动开发 > Android开发

Android动态加载学习总结(一):类加载器

2016-08-01 15:15 441 查看
参考资料:

《深入理解Java虚拟机》 -周志明

Android中的动态加载机制

本篇不深入涉及Java类加载器,如果想更深入了解,可以看一下这篇博客http://blog.csdn.net/zhoudaxia/article/details/35824249

前言:动态加载在应用开发中有着很重要的地位,当我们项目越来越大,我们可以通过插件化来减少应用的内存,然后动态加载那些插件。还有一个方面,如果我们的应用频繁的更新,频繁的发布新版本,肯定会造成用户体验下降,那么我们可以用动态加载技术在不发布新版本的情况下更新一些模块。

那么既然要用动态加载,就肯定涉及到类加载器,我们先看一下Java中的类加载器。

一、Java中类加载器

在这里,我们不去说类加载的具体过程,只是总结一下Java中类加载器与双亲委派模型。


1.类加载器与类本身确定类的唯一性

对于一个类,这个类本身和加载它的类加载器共同确定其在虚拟机中的唯一性。

我们使用两个类加载器进行加载同一个类,那么这两个类是不相等的,那么虚拟机中会存在两个同名的类。

2.Java三种预定义类型类加载器

启动类加载器(Bootstrap ClassLoader,也称为引导类加载器)

该类加载器负责将存放在\lib目录中,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(这点很重要)类加载到虚拟机中。

加载虚拟机识别的这一点与双亲委派模型配合很重要。在下面介绍,先留意这一点。

对于启动类加载器还有一点需要注意,也是和双亲委派模型有关,如果我们自定义一个类加载器,想把一个加载请求委派给启动类加载器,只需要使用null替代即可(可以看下面的loadClass()方法中的代码实现)。

开发者不可以直接使用该加载器。

扩展类加载器(Extension ClassLoader)

该加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路程中的所有类库。

开发者可以直接使用该加载器。

应用程序类加载器(Application ClassLoader,也称为系统类加载器)

该类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。该加载器负责加载用户类路径(ClassPath)上所指定的类库。

开发者可以直接使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下该加载器是程序中的默认的类加载器。

3.双亲委派模型



上图显示了类加载器之间的层次关系,被称为类加载器的双亲委派模型。

除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

那么根据上图,如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把请求委派给父类加载器去加载,每一个层次的类加载器都是这样,那么所有的请求其实最后都是会到启动类加载器中,如果父类加载器反馈无法加载,子加载器才会尝试自己加载。

实现双亲委派模型的代码在loadClass()方法中:

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//检查请求加载的类是否已经加载过了
Class<?> c = findLoadedClass(className);
//如果没有,进行以下步骤
if (clazz == null) {
try {
//尝试使用父类加载器加载,父类加载器不为空,则使用父类加载器尝试加载
if(parent != null){
c = parent.loadClass(className, false);
}
//如果父类加载器为null,则使用启动类加载器作为父加载器,关于这一点,参考上面启动类加载器的介绍
else{
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出异常,说明父类加载器无法完成加载请求
}
if (c == null) {
//父类无法完成加载时调用本身的findClass方法来进行类加载
c = findClass(className);
}
}
if(resolve){
resolveClass(c);
}
//如果加载过了,就直接返回已经加载的类
return clazz;
}


4.双亲委派模型的优点

大家都知道Object类是个基础类,如果我们自己写了一个Object类,那么如果没有双亲委派模型的话,再加上我们并没有用启动类加载器去加载我们写的这个Object类的话,系统中会存在两个Object类(参考上述的类在虚拟机中的唯一性)。那么有了双亲委派模型,我们写了一个Object类,会先去检查它是否加载了(肯定已经加载了),那么我们写的这个就不会被重复加载,也就保证了基础类的唯一性,防止混乱破坏。就算没有检查,根据上面关于启动类加载器的介绍,必须是虚拟机识别的,Object存放在rt.jar中,我们写的不会被识别。

那么从另一个方面,基础类Object怎么保证的在任何环境下都是同一个类(即加载器在任何情况下都是同一个),这就是双亲委派模型的作用了,每次这个加载请求都会委派给处于最顶端的启动类加载器进行加载,虚拟机识别rt.jar,那么就保证了每次都是由启动类加载器加载的Object,那么根据第1条的唯一性,这样做就保证了Object的唯一性。

5.自定义类加载器符合双亲委派模型

我们根据上面的介绍知道,双亲委派模型的逻辑都在loadClass()方法中,那么我们为了不破坏双亲委派模型,自定义类加载器时不去重写loadClass()方法,而是重写findClass()方法,将自己的类加载逻辑写到findClass()方法中,在loadClass()方法中,最后父类加载器无法加载的时候,调用的就是findClass()方法。这样我们就保证了我们自定义的类加载器是符合双亲委派模型的。如果重写loadClass()方法,会出现一系列错误,比如基础类加载不上等。

findClass()方法是在JDK1.2以后引入的。

5.defineClass()方法

//定义类型,一般在findClass方法中读取到对应字节码后调用,final,不可继承,一般不用重写,直接调用。在findClass()方法中调用。

protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
{ … }


介绍这个是为了注明下面的Android类加载与Java中类加载的区别。

二、Android类加载器

前面介绍的是Java中的类加载器,我们知道android中的虚拟机是Dalvik,它不是标准的Java虚拟机,所以在类加载机制上,和Java中的类加载器有一些区别。

我们在java标准的虚拟机中,如果自定义类加载器,会继承ClassLoader,并重写findClass()方法,在内部调用defineClass()去从一个二进制流中加载Class。那么在Android中,这个defineClass()方法去调用VMClassLoader的defineClass本地静态方法,而这个方法内部除了抛出了异常“UnsupportedOperationException”还有一些属性值,其他什么都没做。在博客开头写的链接中有源码,这就不贴了。

那么在Dalvik虚拟中,动态加载类就需要另外由ClassLoader派生出的两个类:DexClassLoader和PathClassLoader。

这两个类重载了ClassLoader的findClass()方法,并没有重写loadClass()方法,所以这两个类加载器符合双亲委派模型。

在介绍这两个类加载器区别之前,说明两点

(1)Dalvik虚拟机识别的是dex文件,而不是class文件,因此,我们加载的是dex文件,或者包含dex文件的apk文件或jar文件。

(2)DexFile类。上述两个类加载器都是通过DexFile这个类去加载类。

区别:

PathClassLoader不能主动从zip包中释放出dex,所以只支持直接操作dex格式文件,或者已经安装的apk(已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。

下面会有加载的demo。

加载好类以后,我们可以通过反射来使用加载好的类,但过多的使用反射会有一定的性能开销,代码复杂凌乱。我们还有一种方式,即接口。我们可以将一些方法提取出来作为一个接口,将待加载的类实现这个接口,在写待加载的类时,注意要有一个参数为空的构造函数,我们在主代码中就能将类对象强制转为接口对象,直接调用成员方法。

三、Demo

这个Demo是Android中的动态加载机制里的,为了展示,简略了一下,并且由于android系统的变更,其中有一些小坑,我们需要改一下代码。

1.接口与待加载类的实现与处理



接口代码

public interface IDynamic {
void init(Activity var1);
//弹出消息
void showSomething();

void destory();
}


待加载类

//导入接口
import com.example.interfaces.IDynamic;

import android.app.Activity;
import android.widget.Toast;
//实现上述的接口
public class Dynamic implements IDynamic{

private Activity mActivity;

@Override
public void init(Activity activity) {
mActivity = activity;
}
//复写接口的showSomething()方法,内部代码只是弹出一个消息
@Override
public void showSomething() {
Toast.makeText(mActivity, "something is here!", Toast.LENGTH_SHORT).show();
}

@Override
public void destory() {
}

}


接口的处理

将接口导出一个jar包(由于AS导包得修改Gradle,嫌麻烦加上电脑上有Eclipse for Android ,我用它打的包):



然后将接口的jar包放入AS工程里的libs目录下。

待加载类的处理

将待加载类按上述方式打包,将打好的jar包复制到SDK的build-tools目录下,打开命令行,进入build-tools目录,输入:

dx –dex –output dynamic1.apk dynamic.jar

注意:上面dex和output前是两个‘-’ ,csdn显示的是一个,只不过长度长一点,坑了我一会。

如下图:



经过上述步骤,build-tools目录下就会生成一个名为dynamic1.apk的文件,那么上述步骤究竟做了什么?

我们知道DexClassLoader和PathClassLoader这两个类加载器加载的并不是class文件,而是将class文件优化后的dex文件,上述步骤便是将jar包解压,将其中的class文件优化成dex文件,然后压缩为apk文件。

目标类的实现与处理

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.example.interfaces.IDynamic;
import java.util.List;
import dalvik.system.PathClassLoader;

/**
* Created by Administrator on 2016/7/27.
*/
public class MainActivity extends Activity {
private IDynamic lib;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
//弹出事件的按钮
Button showSomething_btn = (Button) findViewById(R.id.show_something);

/**使用PathClassLoader方法加载类*/
//创建一个意图,用来找到指定的apk:这里的"com.dynamic.impl是指定apk中在AndroidMainfest.xml文件中定义的<action name="com.dynamic.impl"/>
Intent intent = new Intent("com.example.impl", null);
//获得包管理器
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes =  pm.queryIntentActivities(intent, 0);
//获得指定的activity的信息
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
//这个我修改了参考资料中博客的代码,按照他的代码,我没有运行成功
final String apkPath = "/data/app/com.example.administrator.dynamicloading-1/dynamic1.apk";

//native代码的目录
String libPath = actInfo.applicationInfo.nativeLibraryDir;
//创建类加载器,把dex加载到虚拟机中
//第一个参数:是指定apk安装的路径,这个路径要注意只能是通过actInfo.applicationInfo.sourceDir来获取
//第二个参数:是C/C++依赖的本地库文件目录,可以为null
//第三个参数:是上一级的类加载器
PathClassLoader pcl = new PathClassLoader(apkPath,libPath,this.getClassLoader());
//加载类
try {
//注意,加上你的Dynamic所在的包名
Class libProviderClazz = pcl.loadClass("com.example.impl.Dynamic");
lib = (IDynamic)libProviderClazz.newInstance();
if(lib != null){
lib.init(MainActivity.this);
}
} catch (Exception exception) {
exception.printStackTrace();
}
//弹出事件
showSomething_btn.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
if(lib != null){
lib.showSomething();
}else{
Toast.makeText(getApplicationContext(), "类加载失败", Toast.LENGTH_SHORT).show();
}
}
});
}
}


AndroidManifest的处理

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.administrator.dynamicloading">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
//注意,和目标类中相对应
<action android:name="com.example.impl"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

</manifest>


将dynamic1.apk放入相应目录

好了,处理的差不多了,该处理我们在待加载类处理那一步中生成的dynamic1.apk了。因为用DexClassLoader按照Android中的动态加载机制中的方法我并没有成功,因为博主是将jar包放进SD卡中,现在好像并不支持这样做,所以这次我打算用PathClassLoader,看了上面介绍的小伙伴已经知道,PathClassLoader只能识别dex文件和已经安装好的apk文件(安装好的会有相应的缓存dex文件),那么我们接下来就要想办法把咱们生成的apk文件放到一个目录下,放在哪个目录下呢?

一开始我也不清楚,不过我将下面这个属性用Toast显示了一下,便得到了目录

String apkPath = actInfo.applicationInfo.sourceDir;

要放的目录是:

/data/app/com.example.administrator.dynamicloadi

ng-1

这里需要说明一点,如果你用的是真机的话,可能需要root权限才能进去这些目录,我用的模拟器,不需要权限。

还有一个坑,这个项目需要先运行一下,app目录下才会生成com.example.administrator.dynamicloading-1这个文件(当然你生成的不一定是这个,取决于的你的包名)

注意:下面的一些命令都有一个前提,需要开启模拟器。或者连着真机。

项目运行完后,你可以使用以下命令看看有没有相应包名的文件:



adb shell进入模拟器,然后cd /data/app进入app目录,然后ls命令查看一下该目录中都有哪些文件。

我要进入的目录如下(因为上面那个属性Toast显示的是它):



好了,那么用命令行将apk文件放入这个目录,先使用exit命令退出模拟器。

接下来,用命令将dynamic1.apk放入这个目录

命令如下:

adb push apk所在目录 要放入的目录

例如我的apk在刚才的build-tools目录下,要放进/data/app/com.example.administrator.dynamicloadi

ng-1,所以命令如下图:



如果想查看有没有复制成功,可以按照上述的方法检查一下,adb shell ,然后进入这个目录,ls一下。

好了,运行项目,看看结果:



成功加载了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐