CodeReview工具Jupiter

Code Review的作用和意义已在很多技术团队内达成共识,可是很多时候并未被有效执行,甚至被认为是一项费时费力的工作。借助一些工具可以更容易,更有效率地来进行Code Review,本文介绍的Jupiter即是其中之一。
  Jupiter概述
  Jupiter是一款开源的Eclipse插件,以XML形式存储review数据,通过SVN/CVS将review结果在团队内共享。一个很方便的功能是其建立了review问题跟具体源代码的对应关系(通过点击review问题列表中的问题可以跳转到对应的代码段,通过点击代码段上的review问题标记可对应到具体的问题描述),review问题列表支持各种filter规则(根据review问题状态、责任人等,通过这个filter可以列出具体阶段需关注的问题)。

评论:
灬锋:
还不错,不过我现在用idea了,没找到类似的插件

2019-07-13

gaohan256:
不错,可以进行代码评审

2019-04-29

lify030721:
可以用,还不错

2019-04-20

guoguo902:
这个jupiter插件版本是4.0的,eclipse 4.X的版本貌似用不了。

2017-02-09

qkjoe:
可以用,还不错

2016-09-14

anfaee:
挺好用的,虽然麻烦点儿

2016-04-08

wangsuming123:
不是想要的那种,功能不够强大

2015-11-06

流星L:
非常好用的工具。

2015-09-01

jinzhun100:
不错,刚开始不太习惯,用了一次后顺手了。

2015-07-23

lith123:
不错,可以进行代码评审

南京-小超:
适合自己用吧 但是导出功能不完善
2015-04-29

huangduixiang_123:
不是很好用
2015-04-20

都市放牛娃0618:
挺实用的 只是和以前的不一样了 感觉有点繁琐
2015-04-17

yinyihui:
Excellent!非常好用!
2014-11-27

花木难:
可以用,还不错
2014-10-09

马力2020:
还不错,就是不能导出为excel表格形式
2014-09-04

lfz2738942:
虽然不是想要的那种,但还是有用的
2014-07-14

大力水手POPEYE:
跟之前使用的不大一样,使用的不是很方便,标记较为麻烦,无法导出
2013-11-28

hnsujunfeng:
项目中用到这个东西,CodeReview是项目中很有必要的一个环节
2013-06-27

sunball250:
还可以吧,有用的

qiaoenxin:
我以前用的CodeReview不是这样子的。不明白为什么和我以前用的不一样
2013-05-21

arne3166:
项目中用到这个东西,CodeReview是项目中很有必要的一个环节

什么是代码Review?

代码review是指在软件开发过程中,通过对源代码进行系统性检查来确认代码实现的质量保证机制

为什么不做代码Review?
​业务需求大,工作时间紧张
项目小,协作的人少,没必要
为什么要做代码Review?
提高代码质量,提升自身水平
及早发现潜在缺陷与BUG,降低事故成本
促进团队内部知识共享,提高团队整体水平
保证项目组人员的良好沟通
避免开发人员犯一些很常见,很普通的错误
总而言之目的是查找系统缺陷,保证软件总体质量和提高开发者自身水平,使项目代码更加容易维护。

代码Review的好处
在代码提交之前如果有很多双眼睛盯着看可以发现bug,这是代码审查*广为人知的好处。(人们的确可以在代码审查中发现bug,但是这些bug大部分都是显而易见的小bug,开发者分分钟可以发现,而那些真正需要花时间发现的bug通常是在代码审查中发现的)

代码审查*大的好处是纯社会性的。(如果你编程的时候知道你的同事将要看你的代码,你的编程方式会不一样,你的代码会写的更整洁,注释更加清楚,组织得更好。因为你知道其他人会看你的代码,他们的意见是你需要关注的。如果没有审查,你虽然知道人们*后会去看你的代码,但是那样不会给你一种紧迫感,也不会给你同样的个人评判的感觉。)

还有一个更大的好处就是代码审查可以传播知识。(在很多开发小组里,每个人都负责某一个核心组件,专注于自己的这一块,只要其他同事的模块不会破坏自己的代码就不会去关注,这种模式导致一个模块只有一个人熟悉对应的代码,如果一个人请教或者离职,其他人对他负责的模块将一无所知。如果采用代码审查,那么至少有两个人熟悉代码-作者和审查者。审查者知道的代码不如作者多,但是他们都熟悉代码的设计和结构,这意义重大)

Code Review的前提
重视代码review
(Code Review人员是否理解了Code Review的概念和Code Review将做什么如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。)

代码是否已经正确的build,build的目的使得代码已 经不存在基本语法错误
(我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。 )

代码执行时功能是否正确
(Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。 )

开发人员是否对代码做了单元测试
(这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。 )

Code Review需要注意什么?
完整性检查(Completeness)

代码是否完全实现了设计文档中提出的功能需求
代码是否已按照设计文档进行了集成和Debug
代码是否已创建了需要的数据库,包括正确的初始化数据
代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型
一致性检查(Consistency)

代码的逻辑是否符合设计文档
代码中使用的格式、符号、结构等风格是否保持一致
正确性检查(Correctness)

所有的变量都被正确定义和使用
所有的注释都是准确的
所有的程序调用都使用了正确的参数个数
可修改性检查(Modifiability)

代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等)
代码是否只有一个出口和一个入口(严重的异常处理除外)
健壮性检查(Robustness)

可理解性检查(Understandability)

注释是否足够清晰的描述每个子程序
是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释
使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
是否在定义命名规则时采用了便于记忆,反映类型等方法
每个变量都定义了合法的取值范围
代码中的算法是否符合开发文档中描述的数学模型
可验证性检查(Verifiability)

代码中的实现技术是否便于测试
Code Review经验检查项
1、 编码规范方面检查项
2、面向对象设计方面检查项
– 类设计和抽象是否合适
– 是否符合面向接口编程的思想
– 是否采用合适的设计模式

3、性能方面检查项
– 对hashtable,vector等集合类数据结构的选择和设置是否合适
– 有无滥用String对象的现象
– 是否采用通用的线程池、对象池模块等cache技术以提高性能
– I/O方面是否使用了合适的类或采用良好的方法以提高性能(如减少序列化,使用buffer类封装流等)
– 同步方法的使用是否得当,是否过度使用

4、数据库处理方面
– 数据库资源是否正常关闭和释放
– 数据库访问模块是否正确封装,便于管理和提高性能
– 是否采用合适的事务隔离级别
– 资源泄漏处理方面检查项 cursor

5、通讯方面检查项
– socket通讯是否存在长期阻塞问题

6、重复代码
7、其他
– 日志是否正常输出和控制
– 配置信息如何获得,是否有硬编码

怎么更有效的做Code Review

1.一次评审量要低于 200–400 行代码缺陷密度 就是每 1000 行代码之中所发现的错误(bug)数

%title插图%num
2.每小时低于 300–500 LOC 检查率的目标

%title插图%num
3.花足够的时间进行适当缓慢的评审,但是不要超过 60-90 分钟
但反过来说,评审代码所花的时间不得低于五分钟,就算代码只有一行也是如此。通常来说,单行的代码也会影响到整个的系统,所以花上五分钟时间去检查更改可能造成的结果是值得的

4.确定在评审开始之前代码开发者已经注释源代码了

%title插图%num
5.使用检查表,因为它能*大地影响代码开发者和评审者的结果
另外一个有用的概念就是 个人检查表 。每个人一般都会犯 15-20 个错误(bug)。如果您注意到了一些典型的错误(bug),那么您就可以开发自己的个人检查表

6.确认缺陷得到了修复

*后,让Code Review成为一种习惯

The biggest thing that makes Google’s code so good is simple:code review

Android自动化构建之Ant多渠道打包实践(下)

前言
上一篇(Android自动化构建之Ant多渠道打包实践(上))已经介绍了Android的apk是如何构建的,本篇博客继续Ant打包的实践过程。

集成友盟统计SDK
这里以友盟统计为例,对各个渠道进行统计,我们需要先集成它的SDK

配置权限

<!– 权限 –>
<uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE” >
</uses-permission>
<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE” />
<uses-permission android:name=”android.permission.INTERNET” >
</uses-permission>
<uses-permission android:name=”android.permission.READ_PHONE_STATE” >
</uses-permission>

渠道配置

<!– 友盟统计配置 –>
<meta-data
android:name=”UMENG_APPKEY”
android:value=”56f0b1ef67e58eded700015b” >
</meta-data>
<meta-data android:name=”UMENG_CHANNEL” android:value=”Umeng” />

使用Ant打包的时候替换的渠道号就是<meta-data android:name=”UMENG_CHANNEL” android:value=”Umeng” /> 将Umeng替换为具体的渠道,比如将Umeng替换为xiaomi。

定义build.properties文件
这个文件定义了Ant脚本要用到的一些参数值,我们的渠道也是定义在这里,具体看代码:

#project name and version
project.name=AntBuild
project.version=4.1.4

#android platform version
android-platform=android-19

#keysore file
ketstore.file=release.keystore
key.alias=release.keystore
key.alias.password=123456
key.store.password=123456

#publish channel
channelname=Umeng
channelkey=360,QQ,xiaomi,liangxiang
key=360,QQ,xiaomi,liangxiang

#library project
library-dir=../Library
library-dir2=../Library2
# generate R.java for libraries. Separate libraries with ‘:’.
extra-library-packages=

#filnal out dir
out.dir=publish

完整的Ant脚本
<?xml version=”1.0″ encoding=”UTF-8″?>
<project name=”iReaderApp” default=”deploy” >

<!–打包配置 –>
<property file=”build.properties” />

<!– ANT环境变量 –>
<property environment=”env” />

<!– 版本 –>
<property name=”version” value=”${project.version}” />

<!– 应用名称 –>
<property name=”appName” value=”${project.name}” />

<!– SDK目录(获取操作系统环境变量ANDROID_SDK_HOME的值) –>
<property name=”sdk-folder” value=”${env.ANDROID_SDK_HOME}” />

<!– SDK指定平台目录 –>
<property name=”sdk-platform-folder” value=”${sdk-folder}/platforms/android-19″/>

<!– SDK中tools目录 –>
<property name=”sdk-tools” value=”${sdk-folder}/tools” />

<!– SDK指定平台中tools目录 –>
<property name=”sdk-platform-tools” value=”${sdk-folder}/build-tools/android-4.4.2″ />

<!– 使用到的命令 –>
<property name=”aapt” value=”${sdk-platform-tools}/aapt” />

<!– 第三方library –>
<property name=”library-dir” value=”${library-dir}” />
<property name=”library-dir2″ value=”${library-dir2}” />

<!– 使用到的命令(当前系统为windows,如果系统为linux,可将.bat文件替换成相对应的命令) –>
<property name=”aapt” value=”${sdk-platform-tools}/aapt” />
<property name=”aidl” value=”${sdk-platform-tools}/aidl” />
<property name=”dx” value=”${sdk-platform-tools}/dx.bat” />
<property name=”apkbuilder” value=”${sdk-tools}/apkbuilder.bat” />
<property name=”jarsigner” value=”${env.JAVA_HOME}/bin/jarsigner” />
<property name=”zipalign” value=”${sdk-tools}/zipalign” />

<!– 编译需要的jar; 如果项目使用到地图服务则需要maps.jar –>
<property name=”android-jar” value=”${sdk-platform-folder}/android.jar” />
<property name=”proguard-home” value=”${sdk-tools}/proguard/lib” />

<!– 编译aidl文件所需的预处理框架文件framework.aidl –>
<property name=”framework-aidl” value=”${sdk-platform-folder}/framework.aidl” />

<!– 清单文件 –>
<property name=”manifest-xml” value=”AndroidManifest.xml” />

<!– 源文件目录 –>
<property name=”resource-dir” value=”res” />
<property name=”asset-dir” value=”assets” />

<!– java源文件目录 –>
<property name=”srcdir” value=”src” />
<property name=”srcdir-ospath” value=”${basedir}/${srcdir}” />

<!– 外部类库所在目录 –>
<property name=”external-lib” value=”libs” />
<property name=”external-compile-lib” value=”compile-libs” />

<property name=”external-lib-ospath” value=”${basedir}/${external-lib}” />
<property name=”external-compile-lib-ospath” value=”${basedir}/${external-compile-lib}” />

<property name=”external-library-dir-lib-ospath” value=”${library-dir}/${external-lib}” />
<property name=”external-library-dir2-lib-ospath” value=”${library-dir2}/${external-lib}” />

<!– 使用第三方的ant包,使ant支持for循环–>
<taskdef resource=”net/sf/antcontrib/antcontrib.properties”>
<classpath>
<pathelement location=”${external-lib-ospath}/ant-contrib-1.0b3.jar” />
</classpath>
</taskdef>

<property name=”channelname” value=”${channelname}” />
<property name=”channelkey” value=”${channelkey}” />

<!– 渠道名:渠道号 –>
<!– <property name=”key” value=”UMENG_CHANNEL:goapk,UMENG_CHANNEL:QQ” /> –>
<property name=”key” value=”${key}” />

<!–循环打包 –>
<target name=”deploy”>
<foreach target=”modify_manifest” list=”${key}” param=”nameandchannel” delimiter=”,”>
</foreach>
</target>

<target name=”modify_manifest”>
<!– 获取渠道名字 –>
<!– <propertyregex override=”true” property=”channelname” input=”${nameandchannel}” regexp=”(.*):” select=”\1″ /> –>
<!– 获取渠道号码 –>
<propertyregex override=”true” property=”channelkey” input=”${nameandchannel}” regexp=”(.*)” select=”\1″ />
<!– 正则匹配替换渠道号(这里pattern里的内容要与mainfest文件的内容一致,包括顺序,空格) –>
<replaceregexp flags=”g” byline=”false” encoding=”UTF-8″>
<regexp pattern=’meta-data android:name=”UMENG_CHANNEL” android:value=”(.*)”‘ />
<substitution expression=’meta-data android:name=”UMENG_CHANNEL” android:value=”${channelkey}”‘ />
<fileset dir=”” includes=”AndroidManifest.xml” />
</replaceregexp>
<antcall target=”zipalign” />
</target>

<!– 初始化工作 –>
<target name=”init”>
<echo>目录初始化….</echo>

<!– 生成R文件的相对目录 –>
<var name=”outdir-gen” value=”gen” />

<!– 编译后的文件放置目录 –>
<var name=”outdir-bin” value=”${out.dir}/${channelkey}” />

<!– 生成class目录 –>
<var name=”outdir-classes” value=”${outdir-bin}/otherfile” />
<var name=”outdir-classes-ospath” value=”${basedir}/${outdir-classes}” />

<!– classes.dex相关变量 –>
<var name=”dex-file” value=”classes.dex” />
<var name=”dex-path” value=”${outdir-bin}/${dex-file}” />
<var name=”dex-ospath” value=”${basedir}/${dex-path}” />

<!– 经过aapt生成的资源包文件 –>
<var name=”resources-package” value=”${outdir-bin}/resources.ap_” />
<var name=”resources-package-ospath” value=”${basedir}/${resources-package}” />

<!– 未认证apk包 –>
<var name=”out-unsigned-package” value=”${outdir-bin}/${appName}-unsigned.apk” />
<var name=”out-unsigned-package-ospath” value=”${basedir}/${out-unsigned-package}” />

<!– 证书文件 –>
<var name=”keystore-file” value=”${basedir}/${ketstore.file}” />
<!– <span style=”white-space:pre”> </span> 当前时间 –>
<!– <span style=”white-space:pre”> </span><tstamp> –>
<!– <span style=”white-space:pre”> </span> <format property=”nowtime” pattern=”yyyyMMdd”></format>–>
<!– <span style=”white-space:pre”> </span></tstamp> –>

<!– 已认证apk包 –>
<var name=”out-signed-package” value=”${outdir-bin}/${appName}_${channelkey}_${version}.apk” />
<var name=”out-signed-package-ospath” value=”${basedir}/${out-signed-package}” />
<delete dir=”${outdir-bin}” />
<mkdir dir=”${outdir-bin}” />
<mkdir dir=”${outdir-classes}” />
</target>

<!– 根据工程中的资源文件生成R.java文件 –>
<target name=”gen-R” depends=”init”>
<echo>生成R.java文件….</echo>
<exec executable=”${aapt}” failonerror=”true”>
<arg value=”package” />
<arg value=”-m” />
<arg value=”–auto-add-overlay” />
<arg value=”-J” />

<!–R.java文件的生成路径–>
<arg value=”${outdir-gen}” />

<!– 指定清单文件 –>
<arg value=”-M” />
<arg value=”${manifest-xml}” />

<!– 指定资源目录 –>
<arg value=”-S” />
<arg value=”${resource-dir}” />

<arg value=”-S” />
<arg value=”${library-dir}/res” /><!– 注意点:同时需要调用Library的res–>

<arg value=”-S” />
<arg value=”${library-dir2}/res” /><!– 注意点:同时需要调用Library的res–>

<!– 导入类库 –>
<arg value=”-I” />
<arg value=”${android-jar}” />
</exec>
</target>

<!– 编译aidl文件 –>
<target name=”aidl” depends=”gen-R”>
<echo>编译aidl文件….</echo>
<apply executable=”${aidl}” failonerror=”true”>
<!– 指定预处理文件 –>
<arg value=”-p${framework-aidl}” />
<!– aidl声明的目录 –>
<arg value=”-I${srcdir}” />
<!– 目标文件目录 –>
<arg value=”-o${outdir-gen}” />
<!– 指定哪些文件需要编译 –>
<fileset dir=”${srcdir}”>
<include name=”**/*.aidl” />
</fileset>
</apply>
</target>

<!– 将工程中的java源文件编译成class文件 –>
<target name=”compile” depends=”aidl”>
<echo>java源文件编译成class文件….</echo>

<!– 库应用1编译class 生成的class文件全部保存到outdir-classes目录下–>
<javac encoding=”UTF-8″ destdir=”${outdir-classes}” bootclasspath=”${android-jar}”>
<src path=”${library-dir}/src” /><!– 库应用源码 –>
<src path=”${outdir-gen}” /><!– R.java 资源类的导入 –>
<classpath>
<fileset dir=”${external-library-dir-lib-ospath}” includes=”*.jar” /><!– 第三方jar包需要引用,用于辅助编译 –>
</classpath>
</javac>

<!– 库应用2编译class –>
<javac encoding=”UTF-8″ destdir=”${outdir-classes}” bootclasspath=”${android-jar}”>
<src path=”${library-dir2}/src” /><!– 库应用源码 –>
<src path=”${outdir-gen}” /><!–生成的class文件全部保存到bin/classes目录下 –>
<classpath>
<fileset dir=”${external-library-dir2-lib-ospath}” includes=”*.jar” /><!– 第三方jar包需要引用,用于辅助编译 –>
</classpath>
</javac>

<!– 主应用编译class –>
<javac encoding=”UTF-8″ destdir=”${outdir-classes}” bootclasspath=”${android-jar}” >
<compilerarg line=”-encoding UTF-8 ” />
<!– <compilerarg line=”-encoding UTF-8 “/> –>
<src path=”${basedir}/src” /><!– 工程源码–>
<src path=”${outdir-gen}” /><!–R.java 资源类的导入 –>

<!– 编译java文件依赖jar包的位置 –>
<classpath>
<fileset dir=”${external-lib}” includes=”*.jar” /><!– 第三方jar包需要引用,用于辅助编译 –>
<!– <fileset dir=”${external-compile-lib}” includes=”*.jar”/>第三方jar包需要引用,用于辅助编译
–> <fileset dir=”${external-library-dir-lib-ospath}” includes=”*.jar” /><!– 第三方jar包需要引用,用于辅助编译 –>
</classpath>
</javac>
</target>

<!–执行代码混淆–>

<!– 将.class文件转化成.dex文件 –>
<target name=”dex” depends=”compile” unless=”do.not.compile”>
<echo>将.class文件转化成.dex文件….</echo>
<exec executable=”${dx}” failonerror=”true”>
<arg value=”–dex” />

<!– 输出文件 –>
<arg value=”–output=${dex-ospath}” />

<!– 要生成.dex文件的源classes和libraries –>
<arg value=”${outdir-classes-ospath}” />
<arg value=”${external-lib-ospath}” />
<!– <arg value=”${external-library-dir-lib-ospath}” />
<arg value=”${external-library-dir2-lib-ospath}” /> –>
</exec>
</target>

<!– 将资源文件放进输出目录 –>
<target name=”package-res-and-assets”>
<echo>将资源文件放进输出目录….</echo>
<exec executable=”${aapt}” failonerror=”true”>
<arg value=”package” />
<arg value=”-f” />
<arg value=”-M” />
<arg value=”${manifest-xml}” />

<arg value=”-S” />
<arg value=”${resource-dir}” />

<arg value=”-S”/>
<arg value=”${library-dir}/res”/>

<arg value=”-S”/>
<arg value=”${library-dir2}/res”/>

<arg value=”-A” />
<arg value=”${asset-dir}” />
<arg value=”-I” />
<arg value=”${android-jar}” />
<arg value=”-F” />
<arg value=”${resources-package}” />
<arg value=”–auto-add-overlay” />
</exec>
</target>

<!– 打包成未签证的apk –>
<target name=”package” depends=”dex,package-res-and-assets”>
<echo>打包成未签证的apk….</echo>
<java classpath=”${sdk-tools}/lib/sdklib.jar” classname=”com.android.sdklib.build.ApkBuilderMain”>

<!– 输出路径 –>
<arg value=”${out-unsigned-package-ospath}” />
<arg value=”-u” />
<arg value=”-z” />

<!– 资源压缩包 –>
<arg value=”${resources-package-ospath}” />
<arg value=”-f” />

<!– dex压缩文件 –>
<arg value=”${dex-ospath}” />

<arg value=”-rj” />
<arg value=”${external-lib-ospath}”/>

<!– 将主项目libs下面的so库打包 –>
<arg value=”-nf” />
<arg value=”${external-lib-ospath}” />
</java>
</target>

<!– 对apk进行签证 –>
<target name=”jarsigner” depends=”package”>
<echo>Packaging signed apk for release…</echo>
<exec executable=”${jarsigner}” failonerror=”true”>
<arg value=”-keystore” />
<arg value=”${keystore-file}” />
<arg value=”-storepass” />
<arg value=”${key.store.password}” />
<arg value=”-keypass” />
<arg value=”${key.alias.password}” />
<arg value=”-signedjar” />
<arg value=”${out-signed-package-ospath}” />
<arg value=”${out-unsigned-package-ospath}” />
<!– 不要忘了证书的别名 –>
<arg value=”${key.alias}” />
</exec>
</target>

<!– 发布 –>
<target name=”release” depends=”jarsigner”>
<!– 删除未签证apk –>
<delete file=”${out-unsigned-package-ospath}” />
<echo>APK is released. path:${out-signed-package-ospath}</echo>
<echo>删除其他文件,*后只保留apk</echo>
<delete dir=”${outdir-classes}”/>
<delete file=”${dex-ospath}” />
<delete file=”${resources-package-ospath}” />
<echo>生成apk完成</echo>
</target>

<!– 打包的应用程序进行优化 –>
<target name=”zipalign” depends=”release”>
<exec executable=”${zipalign}” failonerror=”true”>
<arg value=”-v” />
<arg value=”4″ />
<arg value=”${out-signed-package-ospath}” />
<arg value=”${out-signed-package-ospath}-zipaligned.apk” />
</exec>
</target>

</project>

上面就是完整的Ant脚本,实现了自动化构建和多渠道的打包,笔者在实践的过程踩过不少坑才*终把apk包成功打出。

这里总结下可能遇到的坑:
– 生成R.java文件,一定要注意先后顺序,主项目之后才到关联项目
– 编译生成class文件,可能会遇到找不到类,一定要按照添加库的顺序来编译class文件
– 替换渠道号的时候,Ant中pattern里的内容要与mainfest文件的内容一致,包括顺序,空格),笔者试过格式化后代码之后就不能写入成功

build.bat脚本

@echo off
call ant -buildfile “build.xml” deploy
echo done
pause
exit

测试结果
我们可以在项目中的publish目录下生成不同渠道的apk文件:

%title插图%num

安装apk到设备,启动之后在友盟后台集成测试,看app发布的渠道:

%title插图%num
Demo例子欢迎大家star
https://github.com/devilWwj/Android-Tech/tree/master/AntBuildTest

总结
实现Ant多渠道打包整个过程还是比较繁琐的,主要在Ant脚本上,比较容易出错,需要对命令比较了解,但确实能够缩短我们打渠道包的时间,基于本次实践是基于Eclipse,目前Android Studio使用gradle来实现多渠道打包,以后会把gradle进行多渠道打包的实现分享给大家,大家可以对比下这两种打包方式的区别,主要目的是更加深入的了解apk的构建过程。

Android自动化构建之Ant多渠道打包实践

前言
Ant是历史比较悠久的一个自动化构建工具,Android开发者可以通过它来实现自动化构建,也可以实现多渠道打包,关于apk打包的方式一般有Ant、Python、Gradle三种,这三种打包方式都各自有优点和缺点,本篇博文先给大家介绍如何使用Ant来实现自动构建和多渠道发布。

开发环境
Window7
Ant
jdk
android sdk
mac系统下所需要的运行环境也是类似的,我们都需要配置Ant、jdk、sdk的环境变量,我们可以看一下window下是环境变量配了些什么:
ANT_HOME : D:\android\apache-ant-1.9.4
JAVA_HOME : C:\Program Files\Java\jdk1.6.0_45
ANDROID_SDK_HOME : D:\android\adt-bundle-windows-x86_64-20140321\sdk
PATH: %JAVA_HOME%/bin;%ANDROID_SDK_HOME%\platform-tools;%ANDROID_SDK_HOME%\tools;%ANT_HOME%\bin;
CLASSPATH : .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\tools.jar

以上环境变量配好之后,你才可以进入下一步,不会配?回家吧,开发不适合你。

先说APK构建过程
下面来简单描述下apk构建的过程:
1. 使用aapt生成R.java类文件
2. 使用android SDK提供的aidl.exe把.aidl转成Java文件
3. 使用javac命令编译.java类文件生成class文件
4. 使用android SDK提供的dx.bat命令行脚本生成classes.dex文件
5. 使用android SDK提供的aapt.exe生成资源包文件
6. 使用apkBuilder.bat生成未签名的apk安装文件
7. 使用jdk的jarsigner对未签名的包进行apk签名
8. 使用Zipalign工具对apk进行优化

我们从上面的描述可以知道apk具体的步骤和使用到的工具,对应的工具在sdk中都可以找到,自己去翻翻吧,但你会发现新的sdk版本,aapt被放到了build-tools目录下,apkBuilder.bat文件在tools目录找不到了,你可以去网上去下一个,然后放到tools目录下。为了让大家更清楚apk构建的过程,放上官方的一张图:

%title插图%num

有了这张图,相信大家已经清楚了apk到底是如何生成的吧,不多说了。

构建命令详解
aapt命令生成R.java文件
示例命令:

aapt package -m -J <R.java文件夹> -S <res路径> -I <android.jar路径> -M<AndroidManifest.xml路径>

命令解释:

-f 如果编译出来的文件已经存在,强制覆盖
-m 使生成的包的目录存放在-J参数指定的目录
-J 指定生成的R.java 的输出目录
-S res文件夹路径
-A assert文件夹路径
-I 某个版本平台的android.jar的路径
-F 具体指定APK文件的输出

aidl命令生成.aidl文件
示例命令:

aidl -p<framework.aidl路径> -I<src路径> -o<目标文件目录> .aidl文件

注意:命令和路径是没有空格的。

javac命令生成.class文件
示例命令:

javac -d destdir srcFile

参数解释:
-d 指定存放类的文件夹
-bootclasspath 覆盖引导类文件的位置
-encoding 指定源文件使用的编码
-sourcepath 指定查找输入源文件位置

dx命令生成classes.dex文件
示例命令:
dx –dex –output classes.dex bin/classes/ libs/

命令解释:将bin/classes下的class文件和libs下的jar文件编译成classes.dex文件

aapt生成资源包文件resources.ap_
命令示例:

aapt package -m -J <R.java文件夹> -S <res路径> -I <android.jar路径> -A <asset路径> -M<AndroidManifest.xml路径> -F <resources.ap_文件路径>

apkbuilder.bat已经过时了,使用以下方法
示例命令:

java -cp <sdklib.jar路径> com.android.sdklib.build.ApkBuilderMain <未签名.apk> -v -u -z bin\resources.ap_ -f bin\classes.dex -rf src

通过jarsigner来生成
示例命令:
jarsigner -verbose -keystore <keystore路径> -signedjar -<输出签名apk路径> <未签名apk路径> <keystore别名>

*后一步使用zipalign工具进行apk对齐优化
示例命令:

zipalign [-f] [-v] <alignment> infile.apk outfile.apk

上面的8个步骤就是实现apk构建的过程,都是通过命令来一步一步实现,要注意每一步生成的东西。

小结
本篇博文主要给大家介绍了Android中apk构建的过程,也详细的讲解了每一步具体的命令操作,由于不想一篇把所有东西堆在一起,我将会在下一篇来具体使用Ant脚本实现自动化构建和多渠道打包,大家可以继续关注。

 

注册JNI函数的两种方式

前言
前面介绍过如何实现在Android Studio中制作我们自己的so库,相信大家看过之后基本清楚如何在Android studio创建JNI函数并*终编译成不同cpu架构的so库,但那篇文章介绍注册JNI函数的方法(静态方法)存在一些弊端,本篇将介绍另外一种方法(动态注册)来克服这些弊端。

注册JNI函数的两种方法
静态方法
这种方法我们比较常见,但比较麻烦,大致流程如下:
– 先创建Java类,声明Native方法,编译成.class文件。
– 使用Javah命令生成C/C++的头文件,例如:javah -jni com.devilwwj.jnidemo.TestJNI,则会生成一个以.h为后缀的文件com_devilwwj_jnidemo_TestJNI.h。
– 创建.h对应的源文件,然后实现对应的native方法,如下图所示:

%title插图%num

说一下这种方法的弊端:

需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah命令生成一个头文件。
javah生成的JNI层函数名特别长,书写起来很不方便
初次调用native函数时要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率
摘自:深入理解Android卷I
既然有这么多弊端,我们自然要考虑一下有没有其他更好的方法下一节就是我要讲的替代方法,Android用的也是这种方法。

动态注册
我们知道Java Native函数和JNI函数时一一对应的,JNI中就有一个叫JNINativeMethod的结构体来保存这个对应关系,实现动态注册方就需要用到这个结构体。举个例子,你就一下子明白了:

声明native方法还是一样的:

public class JavaHello {
public static native String hello();
}

创建jni目录,然后在该目录创建hello.c文件,如下:

//
// Created by DevilWwj on 16/8/28.
//
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <jni.h>
#include <assert.h>

/**
* 定义native方法
*/
JNIEXPORT jstring JNICALL native_hello(JNIEnv *env, jclass clazz)
{
printf(“hello in c native code./n”);
return (*env)->NewStringUTF(env, “hello world returned.”);
}

// 指定要注册的类
#define JNIREG_CLASS “com/devilwwj/library/JavaHello”

// 定义一个JNINativeMethod数组,其中的成员就是Java代码中对应的native方法
static JNINativeMethod gMethods[] = {
{ “hello”, “()Ljava/lang/String;”, (void*)native_hello},
};

static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
return JNI_FALSE;
}
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}

/***
* 注册native方法
*/
static int registerNatives(JNIEnv* env) {
if (!registerNativeMethods(env, JNIREG_CLASS, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}

/**
* 如果要实现动态注册,这个方法一定要实现
* 动态注册工作在这里进行
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;

if ((*vm)-> GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);

if (!registerNatives(env)) { //注册
return -1;
}
result = JNI_VERSION_1_4;

return result;

}

先仔细看一下上面的代码,看起来好像多了一些代码,稍微解释下,如果要实现动态注册就必须实现JNI_OnLoad方法,这个是JNI的一个入口函数,我们在Java层通过System.loadLibrary加载完动态库后,紧接着就会去查找一个叫JNI_OnLoad的方法。如果有,就会调用它,而动态注册的工作就是在这里完成的。在这里我们会去拿到JNI中一个很重要的结构体JNIEnv,env指向的就是这个结构体,通过env指针可以找到指定类名的类,并且调用JNIEnv的RegisterNatives方法来完成注册native方法和JNI函数的对应关系。

我们在上面看到声明了一个JNINativeMethod数组,这个数组就是用来定义我们在Java代码中声明的native方法,我们可以在jni.h文件中查看这个结构体的声明:

typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;

结构体成员变量分别对应的是Java中的native方法的名字,如本文的hello;Java函数的签名信息、JNI层对应函数的函数指针。

以上就是动态注册JNI函数的方法,上面只是一个简单的例子,如果你还想再实现一个native方法,只需要在JNINativeMethod数组中添加一个元素,然后实现对应的JNI层函数即可,下次我们加载动态库时就会动态的将你声明的方法注册到JNI环境中,而不需要你做其他任何操作。

总结
关于JNI技术,在Android中使用是非常多的,我们在实际开发中或多或少可能会使用到第三方或者需要自己开发相应的so库,所以学习和理解JNI中的一些实现原理还是很有必要的,从以前在Eclipse来实现so库开发到现在可以通过Android Studio来开发so库,会发现会方便很多,这个也是技术的发展带来的一些便捷。笔者也计划学习NDK开发的相关技术,后续也会将自己学到的内容总结分享出来。

Android混淆代码错误堆栈还原

前言
相信做过app的同学对代码混淆应该不陌生吧,如果陌生就自行百度,这里不做普及。我们先思考一个问题,如果我们把代码混淆了,如果出错了怎么定位问题?答案非常简单,只要稍微实践下你就明白了,下面就是给你整理的对混淆代码错误堆栈还原的方法。

如何混淆?
Android Studio实现混淆很简单,只需要在build.gradle进行如下配置即可:

buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
}
}

这个是在发布模式下去混淆代码的,如果想在调试模式下混淆代码,就增加一个debug的配置即可:

debug {
minifyEnabled true
proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
}

mapping.txt文件
mapping文件是我们要实现混淆代码还原必须要关注的一个文件,它里面存放着源码到混淆之后的代码的映射信息,这个文件是在我们执行proguard之后产生的,产生的位置如下图所示:

%title插图%num

制造一个混淆代码的Crash

%title插图%num

%title插图%num
启动MainActivity的时候就会直接Crash并抛出以下异常:

%title插图%num

是不是发现出错的堆栈完全看不懂呢,如果你不会还原的话,你可能根本就定位不到出错的位置。

怎么还原?
一个字,简单。利用SDK给我提供的工具就可以,大家要有一颗发现美的眼睛啊,SDK提供了很多有用的工具哦。我们来到sdk/tools/proguard/bin目录下,会发现三个bat文件:

%title插图%num

我们直接双击打开proguardgui.bat文件,通过gui来完成我们的还原工作:

%title插图%num

哇,好神奇啊,竟然被还原了。但是你有没有发现,那个Unknown Source还在,根本不知道出错行在哪,怎么办?先思考下为什么会出现Unknown Source?原因是我们在Proguard的时候没有保留源文件名及行号,所以我们只需要在proguard-rules.pro文件增加以下配置:

-keepattributes SourceFile,LineNumberTable

然后我们再次编译源码,再还原一次:

%title插图%num

完美!

总结
是不是发现总有些自己讲不清楚说明白的东西?我们获取知识都是先从问为什么开始,本篇文章讲的只是开发中的一个解决问题的技巧,但你想想如果有很多这种混淆的异常,难道你要一个一个去看,拜托太低效啦。下篇告诉你如果通过Bugly配置Mapping文件,实现线上堆栈还原,让你解决问题的速度变得杠杠的。*后,送个鸡汤:程序员是为解决问题而存在的,而不是为了解决编程问题,你的价值取决于你能解决多少问题。

 

Bugly使用篇之Java错误堆栈还原

前面介绍了 Android混淆代码错误堆栈还原,相信大家已经知道如何通过Retrace在本地进行混淆代码还原了,上一篇提到,如果崩溃异常很多,你总不能一个一个去手动还原吧,不觉得这样做很没有效率么,有没有想过如果能实现线上监控崩溃并且能上传mapping文件进行快速还原,而不需要自己手动去做这样的一件事?没错,Bugly就是这样的一个平台,可以很方便快捷实现你这样的需求,能帮助到你提高开发效率,更加敏捷。本篇文章就跟大家分享如何使用Bugly进行错误堆栈还原。

集成Bugly
关于如何集成Bugly SDK这里不详细说明,可以到官网查看我们的SDK使用指南。

前面我也写过一篇文章快速集成Bugly Android SDK,可以参考下。

Bugly混淆配置
# 请避免混淆Bugly,在Proguard混淆文件中增加以下配置:
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}

# 保留源文件名及行号
-keepattributes SourceFile,LineNumberTable

mapping文件
Android混淆代码错误堆栈还原,这篇文章已经说过mapping文件生成的目录,它主要用来对于我们混淆过后的代码进行还原,里面列出了原始的类,方法和字段名与混淆后代码间的映射。可以举个例子:

%title插图%num

可以通过这个映射表知道我们编写的代码大致会被混淆成什么样子,我们每次发布一个版本*好要保留一份Release版的mapping文件,这样我们就可以针对不同的版本进行还原,也能更好的定位问题。

线上还原
通过集成我们Bugly SDK,就能在线上监控你的app的崩溃情况,一有崩溃发生就会上报到平台,我们制造一个Crash,看它在Bugly平台的表现:

%title插图%num

在崩溃分析可以看到Demo上报的一条异常,而这个异常的代码是被混淆过后的,这时我们需要对它进行还原。点击异常进入异常详情页,找到符号表并上传:

%title插图%num

上传成功之后,我们刷新页面就可以看到解析的结果:

%title插图%num

以后在这个版本出现的异常都能通过这个mapping文件进行堆栈还原了。

这里有个问题,每次都要上传mapping文件会不会很麻烦,能不能实现自动上传符号表?当然可以,Bugly早已帮你实现自动上传符号表的插件,详情的话看符号表配置。

总结
对代码进行混淆可以减少被破解的风险,也能达到对代码优化的作用,但如果发生了崩溃了就比较难定位问题,不过android中可以通过mapping文件进行反推,人工来做这件事的话会比较费时,所以使用Bugly能够让用户上传mapping文件来进行线上还原无疑是减少了开发同学的工作量,也能更有效的定位问题,因为不仅仅只是堆栈哦,也提供了很多辅助信息能帮组到开放同学解决问题。没有试过的同学,赶紧试试吧,老是崩溃的程序会影响产品的口碑,自然也影响你的升职加薪,不信的话,就试试吧,哈哈。

Bugly升级SDK适配Android N

前几天有个用户在我们论坛反馈一个问题,说他们的app在Android N机型中升级失败了,看了一下反馈的问题,基本确定了是因为Android N收敛了访问共享文件权限,即在Android N中使用intent不允许跨package共享file://URI,如果在工程中设置targetSDK版本为Android N并且有通过Intent传递文件它会抛出FileUriExposedException异常。发现这个问题之后呢,我自然尝试复现一下,由于没有Android 7.0的真机,我就在优测线上租用了一个7.0设备,发现我们SDK在Android 7.0在下载文件完成安装的时候就出现问题了。大家如果以后遇到类似的问题,可以利用优测的云真机来解决没有真机的痛点,节省了成本也提高了效率。

问题所在
前面已经把问题进行了一下描述,我们可以看下出错的代码:

Intent i = new Intent(Intent.ACTION_VIEW);
i.setDataAndType(Uri.fromFile(file), “application/vnd.android.package-archive”);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);

这段代码的意思是,通过intent设置数据和类型,然后通过context在新的task中启动安装apk的程序。
我们看到intent设置数据时,传递的是一个Uri,这个在API<24是没有问题的,但在Android N已经禁止你对外公开file://URI.所以我们SDK的问题就出自Uri.fromFile(file)获取uri的时候。

如何解决?
Android N已经给出明确解决方案,如果你的程序需要在应用间共享文件,您应发送一项 content://URI,并授予 URI 临时访问权限。进行此授权的*简单方式是使用 FileProvider类。

首先在AndroidManifest中注册FileProvider
代码示例:
<provider
android:name=”android.support.v4.content.FileProvider”
android:authorities=”com.bugly.upgrade.demo.fileProvider”
android:exported=”false”
android:grantUriPermissions=”true”>
<meta-data
android:name=”android.support.FILE_PROVIDER_PATHS”
android:resource=”@xml/provider_paths”/>
</provider>

这里要注意一下,FileProvider是support-v4包里面的,所以在你的程序必须要引入support-v4包。
我们可以看到在provider中需要配置相应的meta-data,这个是共享文件的路径,在res目录下新建xml文件夹并新建对应的xml文件(如下面的provider_paths),如下所示:

<?xml version=”1.0″ encoding=”utf-8″?>
<paths xmlns:android=”http://schemas.android.com/apk/res/android”>
<!– /storage/emulated/0/Download/com.bugly.upgrade.demo/.beta/apk–>
<external-path name=”beta_external_apk” path=”Download/com.bugly.upgrade.demo/.beta/apk/”/>
<!–/storage/emulated/0/Android/data/com.bugly.upgrade.demo/files/apk/–>
<external-path name=”beta_external_apk2″ path=”Android/data/com.bugly.upgrade.demo/files/apk/”/>
</paths>

%title插图%num
name表示一个URI路径段,path表示指定要分享路径的子目录。可以看到我配置了两个external-path,这两个路径都是beta下载的文件可能存在的路径,举个例子,*个路径存在的uri如下:
content://com.bugly.upgrade.demo.fileProvider/beta_external_apk/combuglyupgradedemo_21_d778bda9-f3c5-4608-b5ea-2df2a2372f91.apk。
可以看到我们配置的beta_external_apk成为了URI的一个路径段。

我们还可以指定以下路径:

<files-path name=”name” path=”path” />

表示路径在应用中的内部存储区域中files目录下的子目录下,files-path表示Context.getFilesDir()的根目录。
例如:/data/data/com.bugly.upgrade.demo/files

<cache-path name=”name” path=”path” />

表示路径在应用中红内部存储区域中cache目录下的子目录下,cache-path表示Context.getCacheDir()的根目录。
例如:/data/data/com.bugly.upgrade.demo/cache

<external-path name=”name” path=”path” />

表示路径在外部存储区域根目录的子目录,external-path表示Environment.getExternalStorageDirectory()的根目录。
例如:/storage/emulated/0

<external-cache-path name=”name” path=”path” />

表示路径在外部存储区域根目录的缓存目录,external-cache-path表示Context.getExternalCacheDir()。
例如:/storage/emulated/0/Android/data/com.bugly.upgrade.demo/cache

通过FileProvider获取Uri路径
示例代码:
Uri uri = Uri.fromFile(file);

可以更改为:

Uri uri = FileProvider.getUriForFile(context,
BuildConfig.APPLICATION_ID + “.fileProvider”, file);

因为我们SDK不会引入support-v4包,所以不能通过上面这种方式直接获取uri,*后考虑通过反射来调用getUriForFile方法,具体实现如下:

Intent i = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) {
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 反射获取FileProvider类型
Class<?> clazz = Class.forName(“android.support.v4.content.FileProvider”);
if (clazz == null) {
ELog.error(“can’t find class android.support.v4.content.FileProvider”);
return false;
}

// 通过反射调用FileProvider.getUriForFile
Uri contentUri = (Uri) Utils.invokeReflectMethod(“android.support.v4.content.FileProvider”, “getUriForFile”
, null, new Class[]{Context.class, String.class, File.class},
new Object[]{context, ComInfoManager.getCommonInfo(context).boundID + “.fileProvider”, file});

if (contentUri == null) {
ELog.error(“file location is ” + file.toString());
ELog.error(“install failed, contentUri is null!”);
return false;
}
i.setDataAndType(contentUri, “application/vnd.android.package-archive”);
} else {
i.setDataAndType(Uri.fromFile(file), “application/vnd.android.package-archive”);
}

i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);

调用反射方法定义如下:

public static Object invokeReflectMethod(String className, String methodName, Object instance,
Class<?>[] paramTypes, Object[] params) {
try {
Class<?> clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
return method.invoke(instance, params);
} catch (Exception e) {
return null;
}
}

大致的解决方案就如上所示啦,已经在Android 7.0验证通过了,由于在Android 7.0以上强制要求配置FileProvider,但考虑API低于24以下还是沿用之前的方法,所以只在API高于24才会去使用FileProvider。

总结
关于Android N共享文件权限的适配已经完成,还有其他特性还需要我们去验证看是否存在一些bug,其实Android每一个版本的发布都会面临这样一个问题,所以我们去了解每个版本特性的变化还是很有必要的,每次更新targetSdkVersion的时候,*好的实践就是根据特性变化列一个checklist来进行适配。好了,本篇文章内容就这么多,可能有讲得不清楚的地方,请见谅。

SDK性能自测小技巧

初始化耗时统计
利用时间差:

%title插图%num

通过这种方式可以较准确的得到SDK的初始化耗时。

内存消耗统计
获取内存信息方法

方法一:使用命令

adb shell dumpsys meminfo -d <process-name>

方法二:使用IDE工具

打开logcat,选中侧边栏的系统信息,选择Memory Usage:

%title插图%num

内存信息

示例场景:SDK初始化

SDK初始化前:

%title插图%num

SDK初始化后:

%title插图%num

 

内存消耗计算规则

这里我们只关注Pss Total,取前后Total之差:

20388 – 17317 = 3071 kB

流量消耗统计
流量相关的状态数据存储在/proc/uid_stat/<UID>/目录下,其中<UID> 表示apk对应的UID。

获取UID
方法一: 查看进程

%title插图%num

UID = 10000 + 539 = 10539

注:Java程序的UID从10000起。

获取流量数据

示例:初始化前后的流量

因为初始化前没有任何网络请求操作,所以系统还没有任何对应UID的流量数据,我们点击按钮初始化之后再看的话就有流量数据了:

%title插图%num

采集到前后两次流量数值后,即可计算得到总的流量消耗:

初始化流量消耗 = 1479 + 497 = 1976 bytes ≈ 1.93kb

线程数统计
运行Demo之后,打开Android Studio monitor

%title插图%num

选中进程,然后Update Threads:

初始化前:

%title插图%num

初始化后:

%title插图%num

可以通过前后新增的线程来判断哪些是SDK初始化后的开的线程,从截图来看Bugly常驻线程有5个。

Android多模块构建合并aar解决方案

前段时间,我在实现gradle多模块构建遇到一个问题,以前我们基本上是以jar包形式让开发者集成到工程中使用,但自从Android Studio中有了多module的概念,而我们的SDK也是分了多个模块进行构建的,但我们这里有个问题就是模块之间是相互关联的,不能针对每个模块单独打包,而每个module都会生成对应的aar,但并不会把依赖的module代码打进去,别问我为什么知道,你将aar后缀改为zip,然后反编译classes.jar就可以看到。所以我们这边就有了合并aar这样的一个需求,下面就告诉大家怎么来实现。

android-fat-aar

当时我遇到这个问题,就去github搜了一下,已经有人将合并aar的脚本开源出来了,开源地址如下:

https://github.com/adwiv/android-fat-aar

什么是aar?

什么是aar?它跟jar包有什么区别?它该怎么样使用?相信大家一定会有这些疑问。首先aar是针对Android Library而言的,你可以理解为IDE针对Android Library的打包,一个aar包含什么东西?
它的文件后缀名是.aar,它本身是一个zip文件,强制包含以下文件:

/AndroidManifest.xml
/classes.jar
/res/
/R.txt

另外,AAR文件可以包括以下可选条目中的一个或多个:

/assets/
/libs/name.jar
/jni/abi_name/name.so (where abi_name is one of the Android supported ABIs)
/proguard.txt
/lint.jar

具体看到这里看如何创建一个Android Library:
https://developer.android.com/studio/projects/android-library.html#aar-contents

jar包跟aar包有什么区别?
jar:只包含了class文件与清单文件,不包含资源文件,如图片等所有res中的文件。
aar:包含所有资源,class以及res资源文件全部包含。

如果你只是简单实用一些类库,你可以直接使用*.jar文件,而如果你想既想使用类库,又想实用资源,那么你就可以创建一个Android Library,使用它生成的*.aar文件。

jar文件的使用方式我们应该比较熟悉了,将它复制到工程的libs目录下,然后在gradle中添加以下脚本:

dependencies {
compile fileTree(include: [‘*.jar’], dir:’libs’)
}

aar文件使用同样需要复制到libs目录下,并按照以下方式集成:

repositories {
flatDir {
dirs’libs’
}
}

dependencies {
compile(name:’your aar’, ext:’aar’)

多模块构建合并aar

这个是本文的重点,我们可以再每个module下的build/outputs/aar下找到编译生成的*.aar文件。

步骤1:
将gradle文件’fat-aar.gradle’到你的项目目录,然后apply:

apply from: ‘fat-aar.gradle’

步骤2:定义嵌入的依赖关系
你需要修改你以前依赖项,并将compile更改为embedded,作为你想要合并的aar。使用例子如下:

%title插图%num

通过以上的方式你可以将多个module生成的aar合成一个,大家可以新建一个demo工程来测试下

%title插图%num