十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
android 如何动态的加载类----app插件技术
创新互联专注于北仑网站建设服务及定制,我们拥有丰富的企业做网站经验。 热诚为您提供北仑营销型网站建设,北仑网站制作、北仑网页设计、北仑网站官网定制、微信平台小程序开发服务,打造北仑网络公司原创品牌,更为您提供北仑网站排名全网营销落地服务。
转自:
?
前言:
?
? ? ? 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势。现如今很多项目要求需要采用类似于微信或Q游这样的插件化开发模式越来越多,本文就是阐述android的动态加载技术来满足插件化开发模式的文章。
?
1.基本概念
1.1??在Android中可以动态加载,但无法像Java中那样方便动态加载jar。
Android的虚拟机(DalvikVM)是不认识Java打出jar的byte code,需要通过dx工具来优化转换成Dalvikbyte code才行。这一点在咱们Android项目打包的apk中可以看出:引入其他Jar的内容都被打包进了classes.dex。即android要加载的java类必须dex格式的代码文件.
1.2??在Android中可以加载基于NDK的so库。
NDK的执行效率很高,加密性很好,但同时开发入门难度大,一般用于加解密、数学运算等场合。so的加载很简单,如果APK发布时已经携带了so文件,只需要在加载时调用System.loadLibrary(libName)方法即可。由于软件的安装目录中存放so的目录是没有写权限的,开发者不能更改该目录的内容,所以如果要动态加载存放在其他地方的so文件,用System.load(pathName)方法即可。
1.3??在Android中支持动态加载dex文件的两种方式:
DexClassLoader:这个可以加载jar/apk/dex,也可以从SD卡中加载,也是本文的重点
PathClassLoader:只能加载已经安装到Android系统中的apk文件。也就是 /data/app 目录下的 apk 文件。其它位置的文件加载的时候都会出现 ClassNotFoundException.因为 PathClassLoader 会去读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的。
?
2.注意
2.1 采用不用安装的插件开发模式,只能够使用?DexClassLoader进行加载.不过动态加载是有一些限制的,比如插件(子apk)包中的Activity、Service类是不能动态加载的,因为缺少声明;即使你在Manifest文件中进行了声明,系统默认也是到安装apk所在的路径中去寻找类,所以你会遇到一个ClassNotFound的异常。插件里你可以用主apk中先前放入的layout、strings等资源。但是插件中自带的界面只能用纯代码进行编写,插件中是不能加载插件(子apk)中的xml作为layout等资源使用的。所以在开发上一些特效会比较困难些,建议预先植入主apk中。
2.2?大家可以看看DexClassLoader的API文档,里面不提倡从SD卡加载,不安全
3.如何制作插件
3.1 把工程导出为jar包
3.2 执行SDK安装目录android-sdk-windows\platform-tools下的dx命令,把jar包转换为dex格式
dx?--dex?--output=dex名 jar包名
4.如何做到启动未安装的apk中的activity?
采用反射机制,把主apk中的activity的context传递到插件的activity中,然后采用反射进行回调插件activity的方法。不足之出就是,插件中的activity并不是真正的activity,它只是运行在主activity中。比如:点击返回直接退出当前activity而不是回到主activity。实例如下:
?
这是调用的Activity:
?
[java]?view plaincopy ? ?
package?com.beyondsoft.activity;??
??
import?java.lang.reflect.Constructor;??
import?java.lang.reflect.InvocationTargetException;??
import?java.lang.reflect.Method;??
??
import?dalvik.system.DexClassLoader;??
import?android.app.Activity;??
import?android.content.pm.PackageInfo;??
import?android.os.Bundle;??
import?android.util.Log;??
??
public?class?PlugActivity?extends?Activity?{??
??
????private?Class?mActivityClass;??
????private?Object?mActivityInstance;??
????Class?localClass;??
????private?Object?instance;??
??
????@Override??
????protected?void?onCreate(Bundle?savedInstanceState)?{??
????????super.onCreate(savedInstanceState);??
??
????????Bundle?paramBundle?=?new?Bundle();??
????????paramBundle.putBoolean("KEY_START_FROM_OTHER_ACTIVITY",?true);??
????????paramBundle.putString("str",?"PlugActivity");??
????????String?dexpath?=?"/sdcard/FragmentProject.apk";??
????????String?dexoutputpath?=?"/mnt/sdcard/";??
????????LoadAPK(paramBundle,?dexpath,?dexoutputpath);??
????}??
??
????@Override??
????protected?void?onStart()?{??
????????super.onStart();??
????????Method?start;??
????????try?{??
????????????start?=?localClass.getMethod("onStart");??
????????????????start.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????//?TODO?Auto-generated?catch?block??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onResume()?{??
????????//?TODO?Auto-generated?method?stub??
????????super.onResume();??
????????Method?resume;??
????????try?{??
????????????resume?=?localClass.getMethod("onResume");??
????????????resume.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????//?TODO?Auto-generated?catch?block??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onPause()?{??
????????super.onPause();??
????????Method?pause;??
????????try?{??
????????????pause?=?localClass.getMethod("onPause");??
????????????pause.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onStop()?{??
????????super.onStop();??
????????try?{??
????????????Method?stop?=?localClass.getMethod("onStop");??
????????????stop.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onDestroy()?{??
????????//?TODO?Auto-generated?method?stub??
????????super.onDestroy();??
????????try?{??
????????????Method?des?=?localClass.getMethod("onDestroy");??
????????????des.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????//?TODO?Auto-generated?catch?block??
????????????e.printStackTrace();??
????????}??
????}??
在开发Android App的过程当中,可能希望实现插件式软件架构,将一部分代码以另外一个APK的形式单独发布,而在主程序中加载并执行这个APK中的代码。
实现这个任务的一般方法是:
复制代码
// 加载类cls
Context pluginContext = mainContext.createPackageContext(PLUGIN_PKG, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
ClassLoader loader = pluginContext.getClassLoader();
Class? cls = loader.loadClass(CLASS_NAME);
// 通过反射技术,调用cls中的方法,下面是一个示例,实际代码因情况而定
Object obj = cls.newInstance();
Method method = cls.getDeclaredMethod("someMethod");
method.invoke(obj);
复制代码
但是,这个方法在Android 4.1及之后的系统中存在一些问题:对于收费应用,Google Play会将其安装在一个加密目录之下(具体就是/data/app-asec),而不是一个普通目录之下(具体就是/data/app);安装在加密目录中的应用,我们是无法使用上述方法来加载并执行代码的;而实际情况是,我们经常就是依靠插件应用来收费的。
解决上述问题的一个方案是:将插件的二进制代码拷贝到SD卡中,主程序从SD卡中加载并执行其代码。
实现这个任务的具体方法是:
复制代码
Class? cls = null;
try {
// 尝试第一种方法
cls = loadClass1(mainContext, pkg, entryCls);
} catch (Exception e) {
// 尝试第二种方法
cls = loadClass2(mainContext, pkg, entryCls);
}
// 示例代码
Object obj = cls.newInstance();
Method method = cls.getDeclaredMethod("someMethod");
method.invoke(obj);
// 第一种加载方法
private Class? loadClass1(Context mainContext, String pkg, String entryCls) throws Exception {
Context pluginContext = mainContext.createPackageContext(pkg, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
ClassLoader loader = pluginContext.getClassLoader();
return loader.loadClass(entryCls);
}
// 第二种加载方法
private Class? loadClass2(Context mainContext, String pkg, String entryCls) throws Exception {
Context pluginContext = mainContext.createPackageContext(pkg, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
String path = generatePluginDexPath(mainContext, pkg);
ensureFileExist(pluginContext, pkg, path);
// cacheDir必须是主程序的私有目录,否则DexClassLoader可能会拒绝加载
String cacheDir = mainContext.getApplicationInfo().dataDir;
ClassLoader parentLoader = pluginContext.getClassLoader();
DexClassLoader loader = new DexClassLoader(path, cacheDir, null, parentLoader);
return loader.loadClass(entryCls);
}
// 获取程序版本号
private int getVersionCode(Context context, String pkg) {
PackageInfo info = null;
int versionCode = 0;
try {
info = context.getPackageManager().getPackageInfo(pkg, PackageManager.GET_ACTIVITIES);
versionCode = info.versionCode;
} catch (Exception e) {}
return versionCode;
}
// 获取插件二进制代码的存储位置,注意做好版本控制;路径必须是以.dex结束,否则加载会出问题
private String generatePluginDexPath(Context context, String pkg) {
int version = getVersionCode(context, pkg);
String path = getMyAppPath() + ".classes/" + pkg + version + ".dex";
return path;
}
// 主程序在SD卡上的数据目录
private String getMyAppPath() {
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/MyApp/";
}
// 拷贝插件的二进制代码到SD卡
private void ensureFileExist(Context pluginContext, String pkg, String path) throws Exception {
File file = new File(path);
if(file.exists()) return;
file.getParentFile().mkdirs();
Resources res = pluginContext.getResources();
int id = res.getIdentifier("classes", "raw", pkg);
InputStream in = res.openRawResource(id);
FileOutputStream out = new FileOutputStream(file);
try {
byte[] buffer = new byte[1024 * 1024];
int n = 0;
while((n = in.read(buffer)) 0) {
out.write(buffer, 0, n);
} out.flush();
} catch (IOException e) {
in.close();
out.close();
}
}
复制代码
插件工程这边也需要做相应的修改:
1.编译插件工程;
2.将bin目录之下的classes.dex拷贝到/res/raw目录之下;
3.重新编译插件工程;
4.发布插件APK。
Fruit[] data = {"apple", "banana", "orange"};
int[] bitmaps= {R.drawable.apple, R.drawable.banana, R.drawable.orange};
for ( int i= 0; i data.size; i ++ ){
Fruit temp= new Fruit(fruit, bitmaps[i]);
fruitList.add(temp);
}
此处需要注意DexClassLoader的四个参数:
参数1 dexPath:待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限(uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/ ),否则会报与上面一样的错误,这点参考文章2中说这个权限可有可无是错误的。(更正下:Android4.4 KitKat及以后的版本需要此权限,之前的版本不需要权限)
两者区别:
一,静态库的使用需要:
1
包含一个对应的头文件告知编译器lib文件里面的具体内容
2
设置lib文件允许编译器去查找已经编译好的二进制代码
二,动态库的使用:
程序运行时需要加载动态库,对动态库有依赖性,需要手动加入动态库
三,依赖性:
静态链接表示静态性,在编译链接之后,
lib库中需要的资源已经在可执行程序中了,
也就是静态存在,没有依赖性了
动态,就是实时性,在运行的时候载入需要的资源,那么必须在运行的时候提供
需要的
动态库,有依赖性,
运行时候没有找到库就不能运行了
四,区别:
简单讲,静态库就是直接将需要的代码连接进可执行程序;动态库就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。
做成静态库可执行文件本身比较大,但不必附带动态库
做成动态库可执行文件本身比较小,但需要附带动态库
五:
首先纠正所谓“静态连接就是把需要的库函数放进你的exe之中”的说法。在真实世界中,有三个概念:Use
static
libary,
static
linked
DLL,
dynamic
linked
DLL.
多数人混淆了static
libary
和
static
linked
DLL的概念,当然他们有似是而非的“相似之处”,比如都用到.lib,下面具体说明。
使用静态库(Use
static
libary)是把.lib和其他.obj一起build在目标文件中,目标文件可以是.exe,也可以是.dll或.oxc等。一般情况下,可以根本就没有“对应的”.dll
文件,如C
Run
Time(CRT)库。一个例子就是,写一个main(){},build出来并不是只有几个字节,当然有人会说那还有exe文件头呢?是,即使加上文件头的尺寸,build出的执行文件仍然“莫名的大”。实际上那多出来的部分就是CRT静态库。姑且可以把静态库.lib理解成外部程序的obj文件比较合理,它包含了函数的实现。
由于前段时间项目需要,需要在一个页面上加载根据不同的按钮加载不同的布局页面,当时想到用 tabhot 。不过美工提供的界面图完全用不上tabhot ,所以想到了动态加载的方法来解决这一需求。在这里我整理了一下,写了一个 DEMO 希望大家以后少走点弯路。
首先,我们先把界面的框架图画出来,示意图如下:
中间白色部门是一个线性布局文件,我喜欢在画图的时候用不同的颜色将一块布局标示出来,方便查看。布局文件代码如下: