问题起因我曾经在开发Android Application的过程中遇到过那个有名的65k方法数的问题。如果你开发的应用程序变得非常庞大,你八成会遇到这个问题。 这个问题实际上体现为两个方面: 一、65k方法数 Android的APK安装包将编译后的字节码放在dex格式的文件中,供Android的JVM加载执行。不幸的是,单个dex文件的方法数被限制在了 Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536 在比较新的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时,系统会运行一个叫做 思路对于如上的两个问题,有个非常有名的方案,就是采用动态加载插件化APK的方法。 插件化APK的思路为:将部分代码分离出来放在另外的APK中,做成插件APK的形式,在我们的应用程序启动后,在使用时动态加载该插件APK中的内容。 该思路简单来说便是将部分代码放在了另外一个独立的APK中,而不是放在我们自己的dex中。这样一方面减少了我们自己dex中方法总数,另一方 面也减小了dex文件的大小,因此可以解决如上两个方面的问题。对于这个插件APK包含的类,我们可以在使用到的时候再加载进来,这便是动态加载的思路。 要实现插件化APK,我们只需要解决如下3个问题:
类加载器在实现插件化APK之前,我们需要先了解一下Android中的类加载机制,作为实现动态加载的基础。 在Android中,我们通过 在Android中,我们一般使用
对于我们的插件化APK,显然需要使用 /**
* 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的整个思路清晰一点,我们想要实现如下设定:
有了如上的铺垫,我们现在可以明确我们的实现步骤:
好了,让我们开始coding吧! 1. 创建Application 在 该 这时,你的工程可能长这个样子:
2. 创建共享接口在创建插件APK之前,我们还需要再做一些准备。 由于我们将一部分方法放到了插件APK里,这也就意味着,我们在自己的 在这里,我们采用一个公共的接口来进行方法的定义。你可以理解为我们在 我们创建一个 /** * 定义方法: 将时间戳转换成日期 * @param dateFormat 日期格式 * @param timeStamp 时间戳,单位为ms */ String getDateFromTimeStamp(String dateFormat, long timeStamp); 如上注释所示,该方法将给定的时间戳按照指定的格式转换成一个日期字符串。我们期待在 为了让 android.libraryVariants.all { variant ->
def name = variant.buildType.name
if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
return; // Skip debug builds.
}
def task = project.tasks.create "jar${name.capitalize()}", Jar
task.dependsOn variant.javaCompile
task.from variant.javaCompile.destinationDir
artifacts.add('archives', task);
}
然后在工程根目录执行如下命令: ./gradlew :library:jarRelease
然后就可以在该library module的 此时,你的工程是这样的:
3. 生成插件APK我们终于要实现我们的插件APK了! 在工程中创建一个module,类型选择为application(而不是library),取名为 将上一步中生成的 provided files('libs/library.jar')
便可以引用library中定义的共享接口了。 正如如上所说,我们在该plugin module中做方法的具体实现,因此,我们创建一个 /** * 测试插件包含的工具类 * 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); } } 这样一来,插件部分的代码就写完了!接下来,我们需要生成一个 buildTypes {
release {
minifyEnabled false
proguardFiles 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
便可以在 此时,我们的工程长这个样子,这已经是我们工程的最终样子了: 4. 实现自定义类加载器有了插件APK,接下来我们需要在应用程序运行时,在需要的时候加载这个APK中的内容。实现我们自己的类加载器,我们分为如下两个步骤:
我们实现一个 首先,将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 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) {} 这里我们使用如上提到的 5. 实现动态加载 实现了自己的类加载器之后,我们使用该 使用 /** * 加载指定名称的类 * @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; } 有了这个加载方法之后,我们就可以加载以上实现的 TestInterface testManager = (TestInterface) mPluginLoader.newInstance("org.anchorer.pluginapk.plugin.TestUtil");
mMainTextView.setText(testPlugin.getDateFromTimeStamp("yyyy-MM-dd", System.currentTimeMillis()));
至此为止,代码全部完成。启动应用程序,我们可以看到主界面成功显示了当前的日期。 源码该示例工程的源代码我放到了自己的GitHub上: Github/Anchorer/PluginApk这个工程对代码进行了一定程度的封装:
参考 |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|