BruceFan's Blog

Stay hungry, stay foolish

0%

Android中的动态加载机制

本文主要介绍Android动态加载jar的技术,如何开发一个可以自定义控件的Android应用?就像eclipse一样,可以动态加载插件;如何让Android应用执行服务器上的不可预知的代码?如何对Android应用加密,而只在执行时自解密,从而防止被破解?这篇文章是一个基础,后面的一些文章会以此继续深入。

类加载机制

Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流,因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的。
然而Dalvik虚拟机毕竟不算是标准的Java虚拟机,因此在类加载机制上,它们有相同的地方,也有不同之处。例如,在使用标准Java虚拟机时,我们经常自定义继承自ClassLoader的类加载器。然后通过defineClass方法来从一个二进制流中加载Class,然而,这在Android里是行不通的。参看源码我们知道,Android中ClassLoader的defineClass方法具体是调用VMClassLoader的defineClass本地静态方法。而这个本地方法除了抛出一个UnsupportedOperationException之外,什么都没做,甚至连返回值都为空。
代码清单 /dalvik/vm/native/java_lang_VMClassLoader.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void Dalvik_java_lang_VMClassLoader_defineClass(const u4* args, JValue* pResult) {  
Object* loader = (Object*) args[0];
StringObject* nameObj = (StringObject*) args[1];
const u1* data = (const u1*) args[2];
int offset = args[3];
int len = args[4];
Object* pd = (Object*) args[5];
char* name = NULL;
name = dvmCreateCstrFromString(nameObj);
LOGE("ERROR: defineClass(%p, %s, %p, %d, %d, %p)\n",loader, name, data, offset, len, pd);
dvmThrowException("Ljava/lang/UnsupportedOperationException;","can't load this type of class file");
free(name);
RETURN_VOID();
}

Dalvik虚拟机类加载机制

那如果在Dalvik虚拟机里,ClassLoader不好使,我们如何实现动态加载类呢?Android为我们从ClassLoader派生出了两个类:DexClassLoader(/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java)和PathClassLoader(/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java)。这两个继承自ClassLoader的类加载器,本质上是重载了ClassLoader的findClass方法。在执行loadClass时,我们可以参看ClassLoader部分源码:
代码清单 /libcore/libdvm/src/main/java/java/lang/ClassLoader.java: loadClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
// Don't want to see this.
}
if (clazz == null) {
clazz = findClass(className);
}
}
return clazz;
}

因此DexClassLoader和PathClassLoader都属于符合双亲委派模型的类加载器(因为它们没有重载loadClass方法)。也就是说,它们在加载一个类之前,回去检查自己以及自己以上的类加载器是否已经加载了这个类。如果已经加载过了,就会直接将之返回,而不会重复加载。
DexClassLoader和PathClassLoader其实都是通过DexFile这个类来实现类加载的。这里需要顺便提一下的是,Dalvik虚拟机识别的是dex文件,而不是class文件。因此,我们供类加载的文件也只能是dex文件,或者包含有dex文件的apk或jar文件。
PathClassLoader是通过构造函数new DexFile(path)来产生DexFile对象的;而DexClassLoader则是通过其静态方法loadDex(path, outpath, 0)得到DexFile对象。
PathClassLoader是Android应用中的默认加载器。这两者的区别在于:

  • DexClassLoader可以加载任意路径的apk、jar和dex文件,并且会在指定的outpath路径释放出dex文件。
  • PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。这个也是PathClassLoader作为默认类加载器的原因,因为一般程序都是安装了再打开。

另外,PathClassLoader在加载类时调用的是DexFile的loadClassBinaryName,而DexClassLoader调用的是loadClass。因此,在使用PathClassLoader时类全名需要用/替换.
下面看一下Android中的各种类加载器分别加载哪些类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.androiddemo;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.widget.ListView;

public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// java.lang.BootClassLoader,也继承了ClassLoader类
Log.i("DEMO", "Context的类加载加载器:"+Context.class.getClassLoader());
Log.i("DEMO", "ListView的类加载器:"+ListView.class.getClassLoader());
// dalvik.system.PathClassLoader
Log.i("DEMO", "应用程序默认加载器:"+getClassLoader());
Log.i("DEMO", "系统类加载器:"+ClassLoader.getSystemClassLoader());
// 默认加载器PathClassLoader的父亲是BootClassLoader
Log.i("DEMO","打印应用程序默认加载器的委派机制:");
ClassLoader classLoader = getClassLoader();
while(classLoader != null){
Log.i("DEMO", "类加载器:"+classLoader);
classLoader = classLoader.getParent();
}
// 系统加载器PathClassLoader的父亲也是BootClassLoader
Log.i("DEMO","打印系统加载器的委派机制:");
classLoader = ClassLoader.getSystemClassLoader();
while(classLoader != null){
Log.i("DEMO", "类加载器:"+classLoader);
classLoader = classLoader.getParent();
}
}
}

实际操作

使用到的工具都比较常规:javac、dx、eclipse等其中dx工具最好是指明–no-strict,因为class文件的路径可能不匹配。
加载好类后,通常我们可以通过Java反射机制来使用这个类,但是这样效率相对不高,而且老用反射代码也比较复杂凌乱。更好的做法是定义一个interface,并将这个interface写进宿主程序里。待加载的类,继承自这个interface,并且有一个参数为空的构造函数,以使我们能够通过Class的newInstance方法产生对象然后将对象强制转换为interface对象,于是就可以直接调用成员方法了,下面是具体的实现步骤了:
第一步
编写好动态代码类:

代码清单 IDynamic.java

1
2
3
4
5
6
7
8
9
10
11
package com.dynamic.interfaces;
import android.app.Activity;

public interface IDynamic {
public void init(Activity activity);
public void showBanner();
public void showDialog();
public void showFullScreen();
public void showAppWall();
public void destroy();
}

代码清单 Dynamic.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.dynamic.impl;
import android.app.Activity;
import android.widget.Toast;
import com.dynamic.interfaces.IDynamic;

// 动态类实现
public class Dynamic implements IDynamic {
private Activity mActivity;

@Override
public void init(Activity activity) {
mActivity = activity;
}

@Override
public void showBanner() {
Toast.makeText(mActivity, "我是ShowBanner方法", 1500).show();
}

@Override
public void showDialog() {
Toast.makeText(mActivity, "我是ShowDialog方法", 1500).show();
}

@Override
public void showFullScreen() {
Toast.makeText(mActivity, "我是ShowFullScreen方法", 1500).show();
}

@Override
public void showAppWall() {
Toast.makeText(mActivity, "我是ShowAppWall方法", 1500).show();
}

@Override
public void destroy() {

}
}

第二步
将上面开发好的动态类打包成jar,这里要注意的是只打包实现类Dynamic.java,不打包接口类IDynamic.java
右键项目->Export

点击next

然后使用dx命令:(我的jar文件是dynamic.jar)
dx --dex --output=dynamic_temp.jar dynamic.jar
这样就生成了dynamic_temp.jar,这个jar和dynamic.jar有什么区别呢?
其实这条命令主要做的工作是:首先将dynamic.jar编译成dynamic.dex文件(Android虚拟机认识的字节码文件),然后再将dynamic.dex文件压缩成dynamic_temp.jar,当然你也可以压缩成zip格式的,或者直接编译成apk文件都可以的,这个后面会说到。
同样的方法只打包接口类IDynamic.java,得到dynamic_int.jar,不用dx处理,第三步中会用到。
第三步
新建一个DynamicDemo项目,目录如下

下面看一下目标类:
代码清单 AndroidDynamicLoadClassActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package com.dynamic.demo;
import java.io.File;
import java.util.List;

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.os.Environment;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

import com.dynamic.interfaces.IDynamic;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class AndroidDynamicLoadClassActivity extends Activity {
// 动态类加载接口
private IDynamic lib;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化组件
Button showBannerBtn = (Button) findViewById(R.id.show_banner_btn);
Button showDialogBtn = (Button) findViewById(R.id.show_dialog_btn);
Button showFullScreenBtn = (Button) findViewById(R.id.show_fullscreen_btn);
Button showAppWallBtn = (Button) findViewById(R.id.show_appwall_btn);

/* 使用DexClassLoader方式加载类 */
// dex压缩文件的路径(可以是apk,jar,zip格式)
String jarPath = getApplicationContext().getFilesDir().getAbsolutePath() + File.separator + "dynamic_temp.jar";
// dex解压释放后的目录
String dexOutputDirs = getApplicationContext().getCacheDir().getAbsolutePath();
// 定义DexClassLoader
// 第一个参数:是dex压缩文件的路径
// 第二个参数:是dex解压缩后存放的目录
// 第三个参数:是C/C++依赖的本地库文件目录,可以为null
// 第四个参数:是上一级的类加载器
DexClassLoader cl = new DexClassLoader(jarPath, dexOutputDirs, null, getClassLoader());

/* 使用PathClassLoader方法加载类 */
// 创建一个intent,用来找到指定的apk:这里的"com.dynamic.impl"是指定apk中在AndroidMainfest.xml文件中定义的<action name="com.dynamic.impl"/>
Intent intent = new Intent("com.dynamic.impl", null);
// 获得包管理器
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
// 获得指定的activity信息
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
// 获得apk的目录或者jar的目录
String apkPath = actInfo.applicationInfo.sourceDir;
// native代码的目录
String libPath = actInfo.applicationInfo.nativeLibraryDir;
// 创建类加载器,把dex加载到虚拟机中
// 第一个参数:是指定apk安装的路径,这个路径要注意只能是通过actInfo.applicationInfo.sourceDir来获取
// 第二个参数:是C/C++依赖的本地库文件目录,可以为null
// 第三个参数:是上一级的类加载器
PathClassLoader pcl = new PathClassLoader(apkPath, libPath, this.getClassLoader());

// 加载类
try {
// com.dynamic.impl.Dynamic是动态类名
// 使用DexClassLoader加载类
Class libProviderClazz = cl.loadClass("com.dynamic.impl.Dynamic");
// 使用PathClassLoader加载类
// Class libProviderClazz = pcl.loadClass("com.dynamic.impl.Dynamic");
lib = (IDynamic)libProviderClazz.newInstance();
if (lib != null) {
lib.init(AndroidDynamicLoadClassActivity.this);
}
} catch (Exception exception) {
exception.printStackTrace();
}
// 下面分别调用动态类中的方法
showBannerBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showBanner();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
showDialogBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showDialog();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
showFullScreenBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showFullScreen();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
showAppWallBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (lib != null) {
lib.showAppWall();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
});
}
}

这里定义了一个IDynamic接口变量,而且给出了DexClassLoader和PathClassLoader加载类的使用方法。

1
DexClassLoader cl = new DexClassLoader(jarPath, dexOutputDirs, null, getClassLoader());

DexClassLoader是继承ClassLoader类的,这里面的参数说明:

  • 第一个参数是:dex压缩文件的路径,就是我们将上面编译后的dynamic_temp.jar存放的目录,当然也可以是zip和apk格式的。

  • 第二个参数是:dex解压后存放的目录,就是将jar、zip或apk文件解压出的dex文件存放的目录,这个就和PathClassLoader方法有区别了,同时你也可以看到PathClassLoader方法中没有这个参数,这个也真是这两个类的区别:
    PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在手机的data/dalvik目录中存在缓存的dex文件)。而DexClassLoader可以支持apk、jar和dex文件,并且会在指定的outpath路径释放出dex文件。

    我们可以通过DexClassLoader方法指定解压后的dex文件的存放目录,但是我们一般不这么做,因为这样做无疑的暴露了dex文件,所以我们一般不会将jar/zip/apk压缩文件存放到用户可以察觉到的位置,同时解压dex的目录也是不能让用户看到的。

  • 第三个参数是:加载的时候需要用到的lib库,这个一般不用。

  • 第四个参数是:给DexClassLoader指定父加载器。

com.dynamic.interfaces这个包是从Dynamic项目中剪切过来的,同时要在Dynamic项目中新建一个lib文件夹,将dynamic_int.jar文件拷进去,然后右键dynamic_int.jar->Build Path->Add to Build Path,后面会说为什么要这么做。
第四步
运行目标类:
如果用的是DexClassLoader方式加载类:这时候需要将jar或者zip或者apk文件放到指定的目录中,我这里放到了/data/data/com.dynamic.demo/files目录。
如果用的是PathClassLoader方法加载类:这时候需要先将Dynamic.apk安装到设备上,不然找不到这个指定的activity,同时需要注意:

1
Intent intent = new Intent("com.dynamic.impl", null);

这里com.dynamic.impl是一个action,需要定义在Dynamic项目中,这个名称是动态apk和目标apk之间约定好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".DynamicActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="com.dynamic.impl" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

运行结果:

点击按钮就弹出对应的Toast,成功地运行了动态类中的代码。
其实更好的办法就是将动态的jar,zip,apk文件从网络上获取,安全可靠,同时本地的目标项目不需要改动代码就可以执行不同的逻辑了。

问题解释

需要解释的是项目中接口和jar的位置以及导入方式,在解释原因之前先来了解一下Eclipse中引用项目的不同方式和区别:
第一种 最常用的将引用项目打成jar放到需要引用项目的libs下面(这里是将PluginImpl打成jar,放到HostProject项目的libs中)
这种方式是Eclipse推荐使用的,当我们在建立一个项目的时候也会自动产生这个文件夹,当我们将我们需要引用的项目打成jar,然后放到这个文件夹之后,Eclipse就自动导入了(这个功能是Eclipse3.7之后有的)。
第二种 和第一种的区别是,我们可以从新新建一个文件夹比如是lib,然后将引用的jar放到这个文件夹中,但是此时Eclipse是不会自动导入的,需要我们手动的导入(Add to Build Path…),但是这个是一个区别,还有一个区别,也是到这个这个报错原因的区别,就是libs文件夹中的jar,在运行的时候是会将这个jar集成到程序中的,而我们新建的文件夹(名字非libs即可),及时我们手动的导入,编译是没有问题的,但是运行的时候,是不会将jar集成到程序中。
第三种 和前两种的区别是不需要将引用项目打成jar,直接引用这个项目

这种方式其实效果和第一种差不多,唯一的区别就是不需要打成jar,但是运行的时候是不会将引用项目集成到程序中的。
第四种 和第三种的方式是一样的,也是不需要将引用项目打成jar,直接引用项目:

这个前提是需要设置引用项目为Library,同时引用的项目和被引用的项目必须在一个工作空间中,不然会报错,这种的效果和第二种是一样的,在运行的时候是会将引用项目集成到程序中的。
第五种 和第一种、第二种差不多,导入jar:

这里有很多种方式选择jar的位置,但是这些操作的效果和第一种是一样的,运行的时候是不会将引用的jar集成到程序中的。
上面两个项目可以将Dynamic看成一个插件项目,DynamicDemo看成一个宿主项目,要遵循的一个原则就是插件项目不能集成接口,宿主项目一定要集成接口
项目下载
reference
http://blog.csdn.net/jiangwei0910410003/article/details/17679823
http://blog.csdn.net/jiangwei0910410003/article/details/41384667