/LoadJar

android 动态加载外部jar/dex

Primary LanguageJavaMIT LicenseMIT

title date tags categories comments toc
Android 动态调用外部jar/dex
2018-07-25 13:42:54 -0700
Android
jar
dex
Android
true
true

Android 动态调用外部jar/dex

Codacy Badge

需求分析

现有需求,需要做一个生成外部jar,去验证已发布App有效性,这个外部jar可更新,而App不用重新发布之需要重新发布这个jar包即可。此次记录这种需求开发,jar包中以MD5加密为例。

创建项目

跟往常一样,创建android studio 项目,其中包含两个app Module和两个library Module,目前都是空项目。如下图:

其中:

  • app:发布App,需要验证的App项目。
  • app2:用于直接依赖测试打包jar项目。
  • lib_interface:这个项目里只有一个接口interface,提供了一个或多个可供调用的方法,所有用到验证jar包的项目包括发布jar项目本身都要依赖于它,比如此项目中另外三个项目都要依赖于此library Module。
  • lib_md5:用于发包jar包的项目。

创建接口类

首先处理lib_interface,在lib_interface中新建一个interface接口类Md5JarInterface.java

里面只有一个方法:

    /**
     * 获取Md5值
     * @param content 原字符串
     * @return
     */
    String getMd5(String content);

实现接口类方法

要实现上面接口类方法,转到lib_md5 module,首先需要先依赖lib_interface:

lib_md5build.gradle多了句:

    implementation project(':lib_interface')

依赖好之后,在lib_md5新建一个Md5Utils.java实现Md5JarInterface接口:

下面引进MD5.java(md5算法网上多得是),并实现getMd5()方法:

    @Override
    public String getMd5(String content) {
        return MD5.MD5(content);
    }

此时一个简单的库项目功能基本完成,测试通过后就能发包jar包了。

依赖测试

完成了库项目功能开发,先直接依赖测试下结果。让app2 module依赖于lib_interfacelib_md5

简单修改activity_main.xml:

MainActivity.java添加下面代码:

        TextView txtResult = findViewById(R.id.txt_result);
        txtResult.setText(new Md5Utils().getMd5("123456"));

编译运行测试:

测试正常。

混淆打包jar

经过测试lib_md5项目功能正常,下面准备混淆打包。

混淆模板参考这里

但须注意,对外调用的接口方法是不能被混淆,否则后找不到,修改proguard-rules.pro添加如下:

-keep public class org.sogrey.md5.impl.Md5Utils
-keepclasseswithmembers public class org.sogrey.md5.impl.Md5Utils{
   public String getMd5();
}
-keep class org.sogrey.md5.impl.Md5Utils{
   public <methods>;
}

编辑lib_md5build.gradle,修改buildTypes.release.minifyEnabled 为 true.

添加task:

def SDK_BASENAME = "MD5" //名称
def SDK_VERSION = "_V1.0" //版本
def sdkDestinationPath = "build" //生成保存位置
def zipFile = file('build/intermediates/bundles/release/classes.jar') //打包源文件

task deleteBuild(type: Delete) {
    delete sdkDestinationPath + SDK_BASENAME + SDK_VERSION + ".jar"
}
task makeJar(type: Jar) {
    from zipTree(zipFile)
    from fileTree(dir: 'src/main',includes: ['assets/**'])//将assets目录打入jar包
    baseName = SDK_BASENAME + SDK_VERSION
    destinationDir = file(sdkDestinationPath)
}

makeJar.dependsOn(deleteBuild, build)

makeJar task作用是打包生成jar,但是生成的jar是没有混淆的,再添加task:

task proguardJar(dependsOn: ['makeJar'], type: proguard.gradle.ProGuardTask) {
    //Android 默认的 proguard 文件
    configuration android.getDefaultProguardFile('proguard-android.txt')
    //manifest 注册的组件对应的 proguard 文件
    configuration 'proguard-rules.pro'
    String inJar = makeJar.archivePath.getAbsolutePath()
    //输入 jar
    injars inJar
    //输出 jar
    outjars inJar.substring(0, inJar.lastIndexOf(File.separator)) + "/proguard-${makeJar.archiveName}"
    //设置不删除未引用的资源(类,方法等)
    dontshrink
    Plugin plugin = getPlugins().hasPlugin("AppPlugin") ?
            getPlugins().findPlugin("AppPlugin") :
            getPlugins().findPlugin("LibraryPlugin")
    if (plugin != null) {
        List<String> runtimeJarList
        if (plugin.getMetaClass().getMetaMethod("getRuntimeJarList")) {
            runtimeJarList = plugin.getRuntimeJarList()
        } else if (android.getMetaClass().getMetaMethod("getBootClasspath")) {
            runtimeJarList = android.getBootClasspath()
        } else {
            runtimeJarList = plugin.getBootClasspath()
        }
        for (String runtimeJar : runtimeJarList) {
            //给 proguard 添加 runtime
            libraryjars(runtimeJar)
        }
    }
}

proguardJar task 用于混淆打包。可以看到proguardJar里调用了makeJar

执行task,点击android studio 右上角Gradle展开找到:lib_md5,在task>other里找到我们刚定义的task:makeJarproguardJar,直接双击执行,我们需要混淆的直接双击proguardJar task,等待编译完成,会在build里生成了两个jar包:MD5_V1.0.jarproguard-MD5_V1.0.jar,从文件名就能看出proguard-MD5_V1.0.jar是混淆过的。

直接zip解压可直接看到包结构可class文件:

jar包dx处理

jar包生成好之后,下面就要进行dx处理,把生成的jar拷贝到Android SDK目录下build-tools\28.0.1,后面的版本根据你自己的版本:

执行下面命令:

dx --dex --output=proguard-MD5-dex_V1.0.jar proguard-MD5_V1.0.jar

将会生成目标jar包:proguard-MD5-dex_V1.0.jar

同样我们zip解压后看到的是一个dex文件。

引入外部jar测试

jar包dx处理完毕后就可以使用app module加载外部jar测试了,当然首先app须依赖于lib_interface

为方便安装测试,我们把dx处理好的jar放在assets文件夹下,app安装后拷贝到sd卡再加载。

MainActivity.java中代码实现:

        File cacheFile = FileUtils.getCacheDir(getApplicationContext());
        File libFile = new File(cacheFile, "lib");
        if (!libFile.exists()) libFile.mkdirs();
        String internalPath = cacheFile.getAbsolutePath() + File.separator + "lib" + File.separator + jarName;
        File desFile = new File(internalPath);
        try {
            if (!desFile.exists()) {
                desFile.createNewFile();
                FileUtils.copyFiles(this, jarName, desFile);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //以上是将jar拷贝到sd卡,是为测试方便,实际应用中应该是下载保存到sd卡.
        //下面开始加载dex class
        DexClassLoader dexClassLoader = new DexClassLoader(internalPath, libFile.getAbsolutePath(), null, getClassLoader());
        try {
            //加载的类名为jar文件里面完整类名,写错会找不到此类hh
            Class libClazz = dexClassLoader.loadClass(className);
            final Md5JarInterface md5JarInterface = (Md5JarInterface) libClazz.newInstance();
            if (md5JarInterface != null) {
                txtResult.post(new Runnable() {
                    @Override
                    public void run() {
                        txtResult.setText(md5JarInterface.getMd5("123456"));
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

以上代码通过DexClassLoader类加载器找到对应的类,该类实现了Md5JarInterface接口方法,调用该方法得到结果。

最后因为有SD卡文件读写,别忘了添加权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

android 6.0动态权限申请请自行百度。

测试之:

项目地址:github