Android一键生成包含.dex的Jar及动态加载方案

                                  Android一键生成包含.dex的Jar及动态加载方案

背景:谈到动态加载相信很多小伙伴都会想到 热更新 动态加载dex 的技术,最近也因为项目重构的需求,折腾了下这方面的技术点,以前研究过但时间久了也就忘了,这里记录一下写个总结,也希望给有类似需求的小伙伴一点启发。

此次记录大致可以总结为以下几点:

1. 将项目中的模块进行剥离,独立成单独的module组件(一方面为了降低整个项目的耦合度让结构也更清晰;另一方面为了满足新的功能需求)。

2. 将剥离的module构建为jar包(注意这里的jar包并非AS构建后自动生成的jar包,这个jar中包含的都是class为后缀的文件,并非我们最终需要的。对这些jar进行二次编译后生成包含dex后缀文件的jar才是我们需要的,编译的方法下面会提到),使用WinRAR打开包含dex的jar包:

 

原因:Android使用Dalvik虚拟机加载可执行程序,故不能直接加载基于class的jar,而是需要将class转化为dex字节码才可执行代码。优化后的字节码文件可以存在一个.jar中,只要其内部存放的是.dex即可使用。

3. 将编译好的含有dex文件的jar包通过服务器下发,App请求接口下载存放到本地,通过DexClassLoader类加载器进行加载操作。

代码示例展示

以下我们通过简单的示例来进一步了解大致操作步骤和需要注意的点,先来看一下已完成Demo的目录结构

很简单,除了一个app外还有另外两个作为library的module,module的创建在一个已有的项目中新增或者新建一个项目进行添加都可以,示例中之所以放了2个,主要是为了展示清晰的调用gradle中task任务构建打包dex的问题,先来看一下 dexlibrary1 中的代码,也很简单,一个接口,一个类,这里仅仅是示例,具体代码怎么写都行,如下图:

/**
 * 动态加载测试接口
 */
public interface IMessage_one {
    String showMessage(Context context);
}

ShowMessageImpl_one 实现类:

/**
 * 动态加载测试实现(这里仅仅是一个示例)
 */
public class ShowMessageImpl_one implements IMessage_one {
    @Override
    public String showMessage(Context context) {
        Toast.makeText(context, "dexlibrary1生成的dex文件已加载", Toast.LENGTH_LONG).show();
        return "大家好才是真的好";
    }
}

 

编译生成Jar及进行转化

OK,接下来要做的就是将dexlibrary1打包为jar了,这里需要注意将哪些文件进行打包,从哪里获取到需要打包的文件呢?如下图所示:

即:对应module或project目录下—>build—>intermediates—>classes—>debug下(如果没有classes目录,需要先进行编译<bulid或Make Projrect>),如下图所示:

然后我们通过在build.gradle中定义task任务进行构建,构建的方式有多种,不过都大同小异,例如:

//删除jar包任务
task clearJar(type: Delete) {
    delete 'build/libs/dexlibrary1.jar'
}
task makeJar(type: Jar) {
    //指定生成的jar名
    baseName 'dexlibrary1'
    //archiveName = 'dexlibrary1'   //这样指定名称也可以
    //从哪里打包class文件
    from('build/intermediates/classes/debug/org/gaochun/dexlibrary1/')
    //将assets目录打入jar包
    //from fileTree(dir: 'src/main', includes: ['assets/**'])
    //打包到jar后的目录结构
    into('org/gaochun/dexlibrary1/')
    //去掉不需要打包的目录和文件
    exclude('test/', 'BuildConfig.class', 'R.class')
    //去掉R$开头的文件
    exclude { it.name.startsWith('R$') }
}
makeJar.dependsOn(clearJar, build)

代码中注释也描述的比较清晰了,这个涉及到Groovy脚本相关的一些知识点,感兴趣的同学可以去网上搜索资料进行学习,个人也推荐一个不错的资料:Gradle学习系列 ,这里也不多说我们继续,上面的task定义好之后就可以进行打包dex了,方式有两种,一种是在AS的Terminal输入命令:gradlew makeJar,另外一种就是在AS工具右边的Gradle中找到上面定义的Task,双击该任务就可以了,dexlibrary1--->other--->makeJar,如图:

任务执行完成之后,我们会发现在 build 目录下生成了一个jar包,但这个包并非我们所需要的,前面有提到,我们需要对其进行再次编译,生成对应的包含dex的jar,如下图所示:

接下来我们对该jar包进行编译转化,转化需要用到dx命令,在 android-sdk\build-tools\对应的sdk版本[如:27.0.0] 下,所以需要将该jar拷贝到对应目录下,在Android Studio的Terminal终端cd到对应的sdk版本目录(例如我们将jar拷贝到了27.0.0的目录下,此时就需要在终端将路径切到对应的目录下),然后输入编译命令:

dx --dex --output=dexlibrary1_dex.jar dexlibrary1.jar

注:--output是输出目录,默认在当前的根目录下,执行完后在当前目录下生成了Davilk虚拟机可执行的dex文件,
因为该命令同时会打包dex文件,因此后缀是jar,可使用WinRaR即可看到里面的class.dex文件)

以上操作完成后,我们可以拿到命令执行完后所生成的 dexlibrary1_dex.jar 文件进行动态加载了。

 

动态加载dex

关于动态加载技术相信移动端的小伙伴或多或少都有了解,毕竟Android中当初火热流行的热更新其原理就基于此。Java程序中JVM虚拟机通过类加载器ClassLoader来加载class文件和jar文件(本质还是class文件)。Android与Java类似,只不过Android使用的是Dalvik/ART虚拟机,加载的是dex文件(可以理解为一种对class文件优化的产物),Android中类加载器分为两种类型,一种是系统 ClassLoader 另一种是自定义 ClassLoader ,其中系统ClassLoader包括三种,分别是 BootClassLoader PathClassLoader 和 DexClassLoader,如下图所示:

根据图中所示,我们大致可以总结下 ClassLoader 继承关系,我们大致说下其原理就行,更深入的研究还请小伙伴们自己去探索:

  1. ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能;
  2. BootClassLoader是ClassLoader的内部类,用于预加载preload()常用类以及一些系统Framework层级需要的类;
  3. BaseDexClassLoader继承ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader和DexClassLoader都继承它;
  4. PathClassLoader加载系统类和应用程序的类,如果是加载非系统应用程序类,则会加载data/app/目录下的dex文件以及包含dex的apk文件或jar文件;
  5. DexClassLoader可以加载自定义的dex文件以及包含dex的apk文件或jar文件,也支持从SD卡进行加载。

所以我们这里需要用到的就是 DexClassLoader 类,对比 PathClassLoader ,DexClassLoader 的不同点是它可以加载任意目录下的 jar | dex | apk | zip 文件,比PathClassLoader更加灵活,是实现热修复和插件化技术的重点,划重点,下次要考,源码如下图所示:

/**
     * DexClassLoader类参数含义
     * @param dexPath   待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限
     * @param optimizedDirectory    解压后的.dex文件存储路径,不可为空,此位置一定要是可读写且仅该应用可读写
     * @param librarySearchPath     指向包含本地库(so)的文件夹路径,可以设为null
     * @param parent    父级类加载器,一般可以通过Context.getClassLoader获取到,也可通过ClassLoader.getSystemClassLoader()获取到
     */
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)

注:4.1以后不能够将第二个参数 optimizedDirectory 设置到sd卡目录, 否则抛出异常,强烈建议使用内部私有存储路径(即应用的data/data/xx包名/下面创建一个app_dex文件夹),不要放到sdcard上,代码容易被注入攻击。

下面我们将编译好的含有dex文件的 dexlibrary1_dex.jar 文件放到app下的assets目录下,当然也可以通过其他手段进行加载,例如放到服务器上Download下来 等等,下面演示通过放置到assets目录进行加载:

 /**
     * 加载dex文件中的class,并调用其中的showMessage方法
     */
    private void loadDexClass() {
        File dexOutputDir = getDir("dex", 0);//在data/data/xx包名/下面创建一个app_dex文件夹
        String internalPath = dexOutputDir.getAbsolutePath() + File.separator + "dexlibrary1_dex.jar";
        File dexFile = new File(internalPath);
        try {
            if (!dexFile.exists()) {
                dexFile.createNewFile();
                //将assets目录下的文件copy到app/data/cache目录
                FileUtils.copyFiles(this, "dexlibrary1_dex.jar", dexFile);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        //加载dex class
        DexClassLoader dexClassLoader = new DexClassLoader(internalPath, dexOutputDir.getAbsolutePath(), null, getClassLoader());
        try {
            //该name就是internalPath路径下的dex文件里面的ShowMessageImpl_one这个类的包名+类名
            Class<?> clz = dexClassLoader.loadClass("org.gaochun.dexlibrary1.ShowMessageImpl_one");
            IMessage_one impl = (IMessage_one) clz.newInstance();//通过该方法得到IMessage_one类
            if (impl != null) {
                String value = impl.showMessage(this);//调用打开弹窗并获取值
                mTextView.setText(value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

划重点:Class<?> clz = dexClassLoader.loadClass("org.gaochun.dexlibrary1.ShowMessageImpl_one"); 这个loadClass的包名必须保持一致,即app下的包名和 dexlibrary1 组件下的包名必须保持一致,不然会出现java.lang.ClassCastException或ClassNotFoundException 等错误,所以需要保持一致,如下图所示:

这里给个这样的建议,定义了一个Common的基类Module,里面存放各种interface接口文件,然后剥离出来的组件引用了Common且都implements了对应的接口,宿主app也同样引用了Common,这样在宿主app中加载dex包时就不会出现上面转换错误或者找不到类的错误了,也让项目变得更加清晰一些,画个粗糙的图吧,绿色箭头表示依赖,红色箭头表示对打包好的dex进行加载,大致是这么个意思:

ok,加载成功前后的效果图:

         

到此我们知识点和功能也都基本完善了,按照上面的操作流程,Demo也能正常的运行起来,用着用着,因为项目的需求,独立出来的module越来越多,每个module的build.gradle文件中都有一大坨clearJar、makeJar的任务代码,看着有些碍眼,这是其一,其二就是每次都需要将编译好的jar拷贝到指定目录通过命令再生成包含dex的jar,这重复机械性的工作做多了也是有点头皮发麻,所以针对这个下面做了一些优化。

 

优化编译脚本

优化的目的总结下来有以下几点:

     Module统一版本管理
      将clearJar/makeJar等任务抽离开,不要在每个module中都写一大堆
     ③ 通过自定义的Task一键生成包含class.dex的jar,省去手动编译重复性的工作
      上传到Git后确保让每个协同开发的小伙伴也能直接执行task任务进行编译,无需修改其他配置

下面分别来简单进行说明:

一、Module统一版本管理

首先可以在我们在项目的根目录创建一个 versionConfig.gradle 文件,该文件中定义的内容只做版本相关的定义和配置(也可以在根目录的build.gradle目录定义),例如:

ext {
    versions = [
            sdkMinVersion       : 14,
            sdkTargetVersion    : 27,
            sdkCompileSdkVersion: 27
            //其他...
    ]

    depVersion = [
            appCompatVersion       : "27.1.1",
            recyclerViewVersion    : "27.1.1",
            constraintLayoutVersion: "1.1.0"
    ]

    deps = [
            suport: [
                    appcompat        : "com.android.support:appcompat-v7:${depVersion.appCompatVersion}",
                    recyclerview     : "com.android.support:recyclerview-v7:${depVersion.recyclerViewVersion}",
                    constraint_layout: "com.android.support.constraint:constraint-layout:${depVersion.constraintLayoutVersion}"
            ]
    ]
}

注意由于各个module都需要引用到该配置信息,所以该文件需要在 根目录build.gradle中apply

接下来在各个module中使用:

apply plugin: 'com.android.application'
android {

    def versions = rootProject.ext.versions
    compileSdkVersion versions.sdkCompileSdkVersion
    defaultConfig {
        minSdkVersion versions.sdkMinVersion
        targetSdkVersion versions.sdkTargetVersion
        versionCode 1
        versionName "1.0"
        applicationId "org.gaochun.dexlibrary"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    def dependencies = rootProject.ext.deps
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation dependencies.suport.appcompat
    implementation dependencies.suport.constraint_layout
    //implementation 'com.android.support.constraint:constraint-layout:1.1.0'
}

二、抽离clearJar/makeJar等编译任务并自定义Task任务执行编译dex任务

同样我们单独定义个文件:makeDexJar.gradle,将上面我们编译jar所定义的 clearJar/makeJar 任务放到这个文件中,如下图所示:

这个时候问题来了,如何引用这个文件;这个给多个module引用的打包编译任务有很多公共的属性,怎么封装成方法;另外生成包含class.dex的jar编译命令怎么写;下面是优化好的代码,根据注释可以清楚每一行代码的含义及作用,供大家参考:

 

//-------------------------  构建Jar和包含Dex的Jar  ---------------------------------

ext {
    readLocalSDKPropertiesToMakeDexJar = this.&readLocalSDKPropertiesToMakeDexJar
   }

def readLocalSDKPropertiesToMakeDexJar(outputDexJarName, jarName, packagePath) {
    //println("我被调用了")
    //编译工具
    //def buildingToolPath = 'D:\\Android\\android-sdk\\build-tools\\28.0.0\\dx.bat'
    def dxbatVersion = '25.0.0' //因为项目用的是25Level,所以此处用25.0.0的版本构建
    def dxbat = '\\build-tools\\' + dxbatVersion + '\\dx.bat'
    def buildingToolPath
	
	//主要是为了读取local.properties文件中的sdk.dir路径,设置编译工具的位置
	//这样其他成员拉取代码后打包就不用手动更改编译工具的路径了
    File file = rootProject.file('local.properties')
    if (file.exists()) {
        InputStream inputStream = rootProject.file('local.properties').newDataInputStream();
        Properties properties = new Properties()
        properties.load(inputStream)
        if (properties.containsKey("sdk.dir")) {
            buildingToolPath = properties.getProperty("sdk.dir") + dxbat
        }
    }
	
    //删除jar包任务
    task clearJar(type: Delete) {
        delete 'build/libs/' + jarName
    }

    //生成不带dex的jar
    task makeJar(type: Jar) {
        //baseName 'SmartWebAPI'   //指定生成的jar名
        archiveName = jarName      //打包普通jar名称
        from('build/intermediates/classes/debug/' + packagePath)    //从哪里打包class文件
        into(packagePath)       //打包到jar后的目录结构
        exclude('test/', 'BuildConfig.class', 'R.class')    //去掉不需要打包的目录和文件
        exclude { it.name.startsWith('R$') }    //去掉R$开头的文件
    }
	
    //执行makeJar任务时会在之前执行clearjar任务 和 build
    makeJar.dependsOn(clearJar, build)

    //执行此任务生成包含dex的jar
    task makeDexJar(type: Exec) {
        def mCommond = [
                buildingToolPath, '--dex',//输出包含dex的jar路径及名称
                '--output=build/libs/' + outputDexJarName,
                'build/libs/' + jarName //使用dx将jar中的代码优化成dex文件,该步骤也可以手动命令行完成
        ]
        commandLine mCommond
    }
	
    //执行makeDexJar的时候会在之前执行makeJar
    makeDexJar.dependsOn(makeJar)
}

上面代码中新增了一个task任务:task makeDexJar(type: Exec) ,这个任务就是将编译好的jar通过sdk中的编译工具再次打包为含有dex的jar包,这样就不用将jar拷贝到指定目录再手动用命令打包了。还有上面有一段去读取 local.properties 的操作,代码注释中有提到,主要是为了获取sdk下的编译工具路径,动态读取出来其他小伙伴也不用去单独修改这个文件的路径了,读取示例:

def readLocalProperties(){
    File file = rootProject.file('local.properties')
    if(file.exists()){
        InputStream inputStream = rootProject.file('local.properties').newDataInputStream();
        Properties properties = new Properties()
        properties.load(inputStream)

        if (properties.containsKey("sdk.dir")){
            println properties.getProperty("sdk.dir")
        }
    }
}

三、多个Gradle文件中方法相互调用

这里要着重说明的是这一段代码:

ext {
    readLocalSDKPropertiesToMakeDexJar = this.&readLocalSDKPropertiesToMakeDexJar
   }

gradle提供了ext,所以我们可以很容易获取其他gradle的属性,例如 2.gradle 需要调用 1.gradle 文件中的方法,这个时候就需要像上面的写法一样,注意左右的方法名字是一样,this 后面多了一个 & 符号,其他Gradle文件如果想调用这个方法,一般可以这样:

def outputDexJarName = 'Smart24Decode_dex.jar'
def jarName = 'Smart24Decode.jar'
def packagePath = 'com/ccn/Smart24Decode/'
//直接调用
readLocalSDKPropertiesToMakeDexJar(outputDexJarName, jarName, packagePath)

或者通过task调用:

task CustomTask << {
    def outputDexJarName = 'Smart24Decode_dex.jar'
    def jarName = 'Smart24Decode.jar'
    def packagePath = 'com/ccn/Smart24Decode/'
    readLocalSDKPropertiesToMakeDexJar(outputDexJarName, jarName, packagePath)
}

CustomTask << ,是gradle的语法,如果不加 << 的话,每次编译时都会执行这个task,加了<< ,只有执行这个task的时候才会执行里面的代码。包括后面如果有用到构建打包自动上传到服务器或者第三方的蒲公英平台,也会运用到类似的方式。

OK 下面我们在 dexlibrary1 的build.gradle引用并调用打包方法,传递相关参数:

测试一下看看效果如何,在右边的Gradle工具栏找到dexlibrary1这个module,在 other下找到 makeDexJar 任务并双击,你会发现dexlibrary1下build目录中生成了一个libs目录,里面有我们构建成功的文件:

 

文章到此详细的讲述了整个demo示例和一些需要注意技术点,零零散散的整理了3天,如果有不清楚的地方就参考源码吧,最后祝大家生活愉快。

Demo下载地址:

https://github.com/gaochunchun/MakeDexJar_Application

 

尊重博主劳动成果,转载注明:https://gaochun.blog.csdn.net/article/details/102553809

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值