本文主要介绍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) { } 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); Log.i("DEMO", "Context的类加载加载器:"+Context.class.getClassLoader()); Log.i("DEMO", "ListView的类加载器:"+ListView.class.getClassLoader()); Log.i("DEMO", "应用程序默认加载器:"+getClassLoader()); Log.i("DEMO", "系统类加载器:"+ClassLoader.getSystemClassLoader()); Log.i("DEMO","打印应用程序默认加载器的委派机制:"); ClassLoader classLoader = getClassLoader(); while(classLoader != null){ Log.i("DEMO", "类加载器:"+classLoader); classLoader = classLoader.getParent(); } 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); String jarPath = getApplicationContext().getFilesDir().getAbsolutePath() + File.separator + "dynamic_temp.jar"; String dexOutputDirs = getApplicationContext().getCacheDir().getAbsolutePath(); DexClassLoader cl = new DexClassLoader(jarPath, dexOutputDirs, null, getClassLoader()); Intent intent = new Intent("com.dynamic.impl", null); PackageManager pm = getPackageManager(); List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0); ActivityInfo actInfo = resolveinfoes.get(0).activityInfo; String apkPath = actInfo.applicationInfo.sourceDir; String libPath = actInfo.applicationInfo.nativeLibraryDir; PathClassLoader pcl = new PathClassLoader(apkPath, libPath, this.getClassLoader()); try { Class libProviderClazz = cl.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