android动态加载插件apk -尊龙游戏旗舰厅官网
问题起因
我曾经在开发android application的过程中遇到过那个有名的65k方法数的问题。如果你开发的应用程序变得非常庞大,你八成会遇到这个问题。
这个问题实际上体现为两个方面:
一、65k方法数
android的apk安装包将编译后的字节码放在dex格式的文件中,供android的jvm加载执行。不幸的是,单个dex文件的方法数被限制在了65536之内,这其中除了我们自己实现的方法之外,还包括了我们用到的android framework方法、其他library包含的方法。如果我们的方法总数超过了这个限制,那么我们在尝试打包时,会抛出如下异常:
在比较新的android构建工具下可能是如下异常:
trouble writing output: too many field references: 131000; max is 65536. you may try using --multi-dex option.二、apk安装失败
android官方推荐了一个叫做multidex的工具,用来在打包时将方法分散放到多个dex内,以此来解决65k方法数的问题。但是,除此之外,方法数过多还会带来dex文件过大的问题。
在安装apk时,系统会运行一个叫做dexopt的程序,dexopt会使用dalvik linearalloc缓冲区来存储应用的方法信息。在android 2.x的系统中,该缓冲区大小仅为5m,当我们的dex文件过大超过该缓冲区大小时,就会遇到apk安装失败的问题。
思路
对于如上的两个问题,有个非常有名的方案,就是采用动态加载插件化apk的方法。
插件化apk的思路为:将部分代码分离出来放在另外的apk中,做成插件apk的形式,在我们的应用程序启动后,在使用时动态加载该插件apk中的内容。
该思路简单来说便是将部分代码放在了另外一个独立的apk中,而不是放在我们自己的dex中。这样一方面减少了我们自己dex中方法总数,另一方面也减小了dex文件的大小,因此可以解决如上两个方面的问题。对于这个插件apk包含的类,我们可以在使用到的时候再加载进来,这便是动态加载的思路。
要实现插件化apk,我们只需要解决如下3个问题:
如何生成插件apk
如何加载插件apk
如何使用插件apk中的内容
类加载器
在实现插件化apk之前,我们需要先了解一下android中的类加载机制,作为实现动态加载的基础。
在android中,我们通过classloader来加载应用程序运行需要的类。classloader是一个抽象类,我们需要继承该类来实现具体的类加载器的行为。在android中,classloader的实现类采用了代理模型(delegation model)来执行类的加载。每一个classloader类都有一个与之相关联的父加载器,当一个classloader类尝试加载某个类时,首先会委托其父加载器加载该类。如果父加载器成功加载了该类,则不会再由该子加载器进行加载;如果父加载器未能加载成功,则再由子加载器进行类加载的动作。
在android中,我们一般使用dexclassloader和pathclassloader进行类的加载。
dexclassloader: 可以从.jar或者.apk文件中加载类;
pathclassloader: 只能从系统内存中已安装的内容中加载类。
对于我们的插件化apk,显然需要使用dexclassloader进行自定义类加载。我们看一下dexclassloader的构造方法:
/*** create dexclassloader* @param dexpath string: the list of jar/apk files containing classes and resources, delimited by file.pathseparator, which defaults to ":" on android* @param optimizeddirectory string: directory where optimized dex files should be written; must not be null* @param librarysearchpath string: the list of directories containing native libraries, delimited by file.pathseparator; may be null* @param parent classloader: the parent class loader*/ dexclassloader (string dexpath, string optimizeddirectory, string librarysearchpath, classloader parent)从以上可以看到,该构造方法的入参中除了指定各种加载路径外,还需要指定一个父加载器,以此实现我们以上提到的类加载代理模型。
步骤规划
为了让整个coding过程变得简单,我们来实现一个简单得不能再简单的功能:在主activity上以"年-月-日"的格式显示当前的日期。为了让插件apk的整个思路清晰一点,我们想要实现如下设定:
提供一个插件化apk,提供一个生成日期的方法;
应用程序主activity中通过插件apk中的方法获取到该日期,显示在textview中。
有了如上的铺垫,我们现在可以明确我们的实现步骤:
创建我们的application;
创建一个共享接口的library module;
生成插件apk;
实现自定义类加载器;
实现动态加载。
好了,让我们开始coding吧!
1. 创建application
在android studio中创建一个application,作为我们最终需要发布的应用程序。
该application暂时不需要做特别的配置,你只要实现一个mainactivity,然后显示一个textview就可以了!
这时,你的工程可能长这个样子:
2. 创建共享接口
在创建插件apk之前,我们还需要再做一些准备。
由于我们将一部分方法放到了插件apk里,这也就意味着,我们在自己的app module中对这些方法是不可见的,这就需要有一个机制让app module中使用这些方法变成可能。
在这里,我们采用一个公共的接口来进行方法的定义。你可以理解为我们在app和插件apk之间搭了一座桥,我们在app module中使用接口定义的这些方法,而方法的具体实现放在了插件apk中。
我们创建一个library module,命名为library。在该library module中,我们创建一个testinterface接口,在该接口中定义如下方法:
/*** 定义方法: 将时间戳转换成日期* @param dateformat 日期格式* @param timestamp 时间戳,单位为ms*/ string getdatefromtimestamp(string dateformat, long timestamp);如上注释所示,该方法将给定的时间戳按照指定的格式转换成一个日期字符串。我们期待在插件apk中实现该方法,并且在app中通过该方法获取到我们需要的日期。
为了让插件apk引用该library定义的接口,我们需要生成一个jar包,首先,在library module的gradle脚本中增加如下配置:
android.libraryvariants.all { variant ->def name = variant.buildtype.nameif (name.equals(com.android.builder.core.builderconstants.debug)) {return; // skip debug builds.}def task = project.tasks.create "jar${name.capitalize()}", jartask.dependson variant.javacompiletask.from variant.javacompile.destinationdirartifacts.add('archives', task); }然后在工程根目录执行如下命令:
./gradlew :library:jarrelease然后就可以在该library module的/build/libs目录下看到一个library.jar包。
此时,你的工程是这样的:
3. 生成插件apk
我们终于要实现我们的插件apk了!
在工程中创建一个module,类型选择为application(而不是library),取名为plugin。
将上一步中生成的library.jar放到该plugin module的libs目录下,在gradle脚本中添加
provided files('libs/library.jar')便可以引用library中定义的共享接口了。
正如如上所说,我们在该plugin module中做方法的具体实现,因此,我们创建一个testutil类,实现如上定义的testinterface接口定义的方法:
/*** 测试插件包含的工具类* created by anchorer on 16/7/31.*/ public class testutil implements testinterface {/*** 将时间戳转换成日期* @param dateformat 日期格式* @param timestamp 时间戳,单位为ms*/public string getdatefromtimestamp(string dateformat, long timestamp) {dateformat format = new simpledateformat(dateformat);date date = new date(timestamp);return format.format(date);}}这样一来,插件部分的代码就写完了!接下来,我们需要生成一个插件apk,将该apk放在应用程序app module的sourceset下,供app module的类加载器进行加载。为此,我们在plugin的gradle脚本中添加如下配置:
buildtypes {release {minifyenabled falseproguardfiles getdefaultproguardfile('proguard-android.txt'), 'proguard-rules.pro'applicationvariants.all { variant ->variant.outputs.each { output ->def apkname = "plugin.apk"output.outputfile = file("$rootproject.projectdir/app/src/main/assets/plugin/" apkname)}}}}该脚本将生成的apk放在app的assets目录下。
最后,在工程根目录执行:
./gradlew :plugin:assemblerelease便可以在/app/src/main/assets/plugin目录下生成了一个plugin.apk文件。到此为止,我们便生成了我们的插件apk。
此时,我们的工程长这个样子,这已经是我们工程的最终样子了:
4. 实现自定义类加载器
有了插件apk,接下来我们需要在应用程序运行时,在需要的时候加载这个apk中的内容。实现我们自己的类加载器,我们分为如下两个步骤:
将该apk复制到sd卡中;
从sd卡中加载该apk。
我们实现一个pluginloader类,来执行插件的加载。在这个类中,实现如上提供的两个关键方法。
首先,将apk复制到sd卡的代码比较简单:
/*** 将插件apk保存至sd卡* @param pluginname 插件apk的名称*/ private boolean savepluginapktostorage(string pluginname) {string pluginapkpath = this.getplguinapkdirectory() pluginname;file plugapkfile = new file(pluginapkpath);if (plugapkfile.exists()) {try {plugapkfile.delete();} catch (throwable e) {}}bufferedinputstream instream = null;bufferedoutputstream outstream = null;try {inputstream stream = testapplication.getinstance().getassets().open("plugin/" pluginname);instream = new bufferedinputstream(stream);outstream = new bufferedoutputstream(new fileoutputstream(pluginapkpath));final int buf_size = 4096;byte[] buf = new byte[buf_size];while(true) {int readcount = instream.read(buf, 0, buf_size);if (readcount == -1) {break;}outstream.write(buf,0, readcount);}} catch(exception e) {return false;} finally {if (instream != null) {try {instream.close();} catch (ioexception e) {}instream = null;}if (outstream != null) {try {outstream.close();} catch (ioexception e) {}outstream = null;}}return true; }其次,我们要创建自己的dexclassloader:
dexclassloader classloader = null; try {string apkpath = getplguinapkdirectory() pluginname;file dexoutputdir = testapplication.getinstance().getdir("dex", 0);string dexoutputdirpath = dexoutputdir.getabsolutepath();classloader cl = testapplication.getinstance().getclassloader();classloader = new dexclassloader(apkpath, dexoutputdirpath, null, cl); } catch(throwable e) {}这里我们使用如上提到的dexclassloader的构造方法,其中第一个参数是我们插件apk的路径,最后一个参数是application生成的父classloader。
5. 实现动态加载
实现了自己的类加载器之后,我们使用该classloader进行类的加载就可以了!
使用classloader加载类,我们调用loadclass(string classname)就可以了。这一步比较简单:
/*** 加载指定名称的类* @param classname 类名(包含包名)*/ public object newinstance(string classname) {if (mdexclassloader == null) {return null;}try {class clazz = mdexclassloader.loadclass(classname);object instance = clazz.newinstance();return instance;} catch (exception e) {log.e(const.log, "newinstance classname = " classname " failed" " exception = " e.getmessage());}return null; }有了这个加载方法之后,我们就可以加载以上实现的testutil类了:
testinterface testmanager = (testinterface) mpluginloader.newinstance("org.anchorer.pluginapk.plugin.testutil"); mmaintextview.settext(testplugin.getdatefromtimestamp("yyyy-mm-dd", system.currenttimemillis()));至此为止,代码全部完成。启动应用程序,我们可以看到主界面成功显示了当前的日期。
源码
该示例工程的源代码我放到了自己的github上:
github/anchorer/pluginapk
这个工程对代码进行了一定程度的封装:
pluginmanager: 该类统一提供了创建类加载器和加载具体类的所有入口;
pluginloader: 该类具体创建了类加载器,执行具体的加载类的行为;
mainactivity: 主activity,展示了如何调用插件内的方法。
参考
提供一些我自己在探索过程中参考的文章:
1. classloader
2. dexclassloader
3. multidex
4. 动态加载基础
总结
以上是尊龙游戏旗舰厅官网为你收集整理的android动态加载插件apk的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: php 验证码
- 下一篇: