项目背景,公司要将完整APP打包成AAR包,供其它厂商内嵌。外部厂商提供壳工程和相应的Application调用我们提供的aar包
一路走来踩了一堆坑。。。
这里先简要解释一下相关概念
1 什么是AAR包? AAR包相比于jar包,区别在哪儿?
aar包含所有资源,class,xml布局文件以及res资源文件全部包含。注意是全部。
jar只包含了class文件与清单文件,不包含资源文件,如图片等所有res中的文件。捎带解释一下so库,
2 什么是so库?什么是ABI?相关的处理器型号在构建APP时有什么区别?
android系统目前支持以下七种不同的CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI。
应用程序二进制接口ABI(Application Binary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。
so库的好处:
so机制让开发者最大化利用已有的C和C++代码,达到重用的效果,利用软件世界积累了几十年的优秀代码;
so是二进制,没有解释编译的开消,用so实现的功能比纯java实现的功能要快;
so内存分配不受Dalivik/ART的单个应用限制,减少OOM;
相对于java代码,二进制代码的反编译难度更大,一些核心代码可以考虑放在so中。
在Android Studio构建APP时可以选择构建时匹配的CPU架构。在project的build.gradle可以明确指定,代码如下
在buildType标签下声明
ndk{
abiFilters "armeabi","armeabi-v7a","x86"
}
以上代码可以指定在构建时,生成支持这三类CPU的so库。
so库的load:
1:相对路径load: System.loadLibrary("media_jni"); 其中media_jni名字会被自动替换成libmedia_jni.so
在使用相对路径load时,需要注意相应的so库是否被打入到 aar包的libs目录下。此处需要注意ABI类型
2: 绝对路径load:System.load("/绝对路径/libmedia_jni.so");
绝对路径可以避免这个问题,但是要确保具有相应路径的访问权限,在接入AAR时候,假设合作方是厂商ROM级别的,部分路径需要提前协调。
jni层的方法对应关系:
全路径,将.置换为_ 例如,假设当前函数native_init函数位于android.media这个包中,它的全路径名应该是android.media.MediaScanner.native_init,而JNI层函数的名字是android_media_MediaScanner_native_init。
完整项目改造生成aar包的过程:
1 将原先module下的Application改为Library,正常调用assembleDebug 或 assembleRelease时就会在该module下的build/output目录下生成aar文件。
1.1 项目代码中的switch语句需要改为if语句
1.2 修改Manifest.xml文件
2 原有项目所依赖的jar包会被正常打包进aar中,但原项目依赖的aar则不会打包进aar
2.1 以外部compile形式所依赖的包,也不会被打包进aar
2.2 记得不要重复引用,避免壳工程引用的jar与打包好的aar冲突
3 声明具体支持的so库类型
3.1 最好在构建过程中声明所支持的CPU类型。
Android系统的匹配过程为从高到低,向下兼容,例如:armeabi-v7a类型的CPU支持armeabi
3.2 如果不在BuildType中声明,则默认支持所有类型的so库文件,通过反编译在aar中的lib目录下可以看到所支持的SO库类型
3.3 部分so库在不声明的情况下,默认打在armeabi下,这样会导致armeabi-v7a类型的包找不到相应的so库文件。解决办法就是强制声明为armeabi类型
3.4 注意so库存放的路径
3.5 so库本身是含有包名的,在jni使用的时候,需要将so库方法的名称,与调用so库的代码包名一致
3.6 外部调用aar的壳工程,一般来说,会从aar中使用DexClassLoader,拷贝aar中的so库到相应的目录中。可以使用adb shell 到壳工程指定目录下查看是否so库成功拷贝
3.7 so库可以存放在aar的jniLibs下,也可以存放在壳工程的jniLibs和libs下。
3.8 遇到一次so库崩溃,信息与下段信息类似:
http://blog.csdn.net/tankai19880619/article/details/9004619
在其中有相应的解决办法,此处感谢博主,运用文中方法,定位了问题。
4 Application参数的传递问题。
每一个Android App都有一个application context,这个参数需要壳工程传递给我们,调试的时候可以在壳工程的Manifest.xml中指定默认的Application.
并在默认的Application中初始化aar,
5 混淆
aar包也可以指定混淆方式,在提供给对方时,我们需要将代码混淆,在厂商发布时,也需要混淆,这样就存在二次混淆后,AAR包找不到相应类的问题。
解决方法可以让厂商在二次混淆时,keep住我们aar相关代码
6 路径获取
留意相关路径的获取,在普通的Appliaction中,数据默认存储在 /data/data/packageName/中,当aar内嵌在其他应用中,存储路径跟随主工程,在获取数据时路径切记不能写死。
7 Assert资源获取
因为是内嵌在其他应用中,原本APK中的Assert目录内的资源可能找不到。解决方法是,可以将Assert目录内的资源打包成一个ZIP文件放在 /RAW目录下。
在项目初始化的时候,解压到指定目录下。
使用的时候在AssertManager中使用绝对路径获取资源
在于前端页面交互上,JS相关代码及资源一般也存放在Assert目录下,在webview.load()时,可以通过 file:/ 绝对路径 ,来加载
PS:Assert 目录与 /raw 目录的区别:访问方式,目录结构,大小写,压缩方式等。
8 第三方登录
因为微信微博等三方登录需要使用PackageName申请APPID,APPKEY, 需要使用壳工程的packageName。
9 常见BUG
9.1 java.lang.NoClassDefFoundError
NoClassDefFoundError错误的发生,是因为Java虚拟机在编译时能找到合适的类,而在运行时不能找到合适的类导致的错误。例如在运行时我们想调用某个类的方法或者访问这个类的静态成员的时候,发现这个类不可用,此时Java虚拟机就会抛出NoClassDefFoundError错误。与ClassNotFoundException的不同在于,这个错误发生只在运行时需要加载对应的类不成功,而不是编译时发生。
9.2 jar包引用重复
9.3 contentProvider在注册时出现重名情况
9.4 注意不要在壳工程的Activity中传递Context,可能出现Context为NULL的情况,最好在 壳工程的Application来初始化Context
9.5 java.lang.ExceptionInInitializerError
原因如果你在别的类调用getInstance,就会报错ExceptionInInitializerError。这是因为类加载时不会为实例变量赋值,对象创建时不会为静态变量赋值。我们调用getInstance时,此类就开始加载,加载的时候不会为实例变量赋值,但是会按顺序给静态变量赋值。需要检查变量初始化过程。
9.6 系统切换广播监听
在系统配置改变时,例如横竖屏切换,会导致Activity生命周期的改变。在开发过程中碰到一个问题,在用户将应用点击home键置于后台的情况下切换语言,会导致原有的注册的receiver报错。
排查后发现,在语言切换且应用存活的情况下,并不会走到应用的onDestory方法,而是重新走一次onCreate。
这就导致了注册两次而报错。