一次被迫的排查安卓编译问题

下载了工程,里面有两个模块,一个是 app,一个是其依赖。工程时间距今也有两年了,而老夫的 Android Studio 却总是保持最新,所以最开始编译不顺是理所应当的事。不管是 support 库的版本还是 gradle 的版本,都基本应对了下来。

直到最后的一个问题。我们姑且把被依赖的工程称之为 sdk 工程。这个工程里除了常规的 Java 类以外,还有 native 层供调用的 .so 文件。app 中调用 sdk 中的 Java 类,并间接使用 .so 里的能力。但实际的情况是加载 .so 就会出现 UnsatisfiedLinkError。打开 .apk 安装包检查,发现 .so 文件没有被打入包内。

在 app 的 build.gradle 文件里,依赖的写法很正规:

而且其中的 Java 类可以编译进入 app 也说明并非依赖完全失效。单独编译 sdk 工程,发现其产出为 .aar 包,而包里是有 .so 文件的,这一发现更使得情况具有迷惑性。

网上查阅资料,大都语焉不详。不过有一篇启发了我,在这里:https://blog.csdn.net/pang_gua/article/details/88196330,它主要讲 aar 如何被依赖。由于现在网络上的文章生命周期往往不可信赖,因此全文附于下,也方便引用讨论(原文作者为 pang_gua,版权归其所有;如有异议请联系我):


项目依赖 aar 以及依赖嵌套 aar

原创 pang_gua 发布于 2019-03-05 18:51:11

一、application 直接依赖 aar(单一依赖)

复制 aar 至 app/libs 目录,app/build.gradle 添加以下代码:

二、application 依赖 library,library 依赖 aar(嵌套依赖)

(library 名称以下用 aarlibs 代替, library 内依赖的子 aar 名称用 sublib 代替)

复制 sublib.aar 至 aarlibs/libs 目录,aarlibs/build.gradle 添加以下代码:

[重点] 此时 aarlibs 作为 library 有两种使用方式:(1) 作为 module 被 application 依赖;(2) 打包为新的 aar 被 application 依赖。

下面分别展示两种依赖方式下 application 的配置(主要是为了在引用 library 的同时能让 library 内部的 aar 也生效)。

2.1 作为 module 被 application 依赖
application/app/build.gradle 添加以下代码:

2.2 作为 aar 被 application 依赖

将 aarlibs.aar 和 sublib.aar 复制至 applicatioon/app/libs 目录。虽然 aarlibs/libs 目录已经包含 sublib.aar 了,但是项目的 libs 里也需要复制一份,否则报错找不到 sublib.aar 内部的 Class。不过经简单测试,apk 的体积并不会因为重复有一份 sublib.aar 而将其做双倍纳入 apk 体内。

application/app/build.gradle 添加以下代码:

附:清除 aar 缓存

terminal 进入项目根目录
cd .idea/libraries
rm Gradle__xxxx_aar.xml
Android Studio 点击 左上角 SyncProject 和 SyncFile 按钮


让我灵光显现的,正是上文中的 2.2,当 aar 工程被依赖时,为了将其中的某些文件包含到最终目标包中,使用了个.file() 的指令。老夫依葫芦画瓢,在自己的 app 工程的 build.gradle 里增加了以下一级节点:

Sync 后检查编译结果,所有的 .so 就已经都乖乖地待在 .apk 安装包内了。:)

更新,另附一篇相关文章(原始链接为 https://www.jianshu.com/p/59fd653a54d2,作者为“曾是放牛娃”):

Gradle依赖详解

曾是放牛娃
之前对 Android Gradle 构建的依赖一直傻傻分不清,这段时间正好接入集团的一个二方库,踩了很多坑,也顺带把 Gradle 依赖这块搞清楚了,主要整理了下 Gradle 依赖的类型、依赖配置、如何查看依赖、依赖冲突如何解决。

依赖类型

dependencies DSL 标签是标准 Gradle API 中的一部分,而不是 Android Gradle 插件的特性,所以它不属于 Android 标签。
依赖有三种方式,如下例:

  • 本地 library 模块依赖

这种依赖方式是直接依赖本地库工程代码的(需要注意的是,mylibrary 的名字必须匹配在 settings.gradle 中 include 标签下定义的模块名字)。

  • 本地二进制依赖

这种依赖方式是依赖工程中的 module_name/libs/ 目录下的 .jar 文件(注意 Gradle 的路径是相对于 build.gradle 文件来读取的,所以上面是这样的相对路径)。

如果只想依赖单个特定本地二进制库,可以如下配置:

  • 远程二进制依赖

上面是简写的方式,这种依赖完整的写法如下:

group、name、version 共同定位一个远程依赖库。需要注意的点是,version 最好不要写成 “12.3+” 这种方式,除非有明确的预期,因为非预期的版本更新会带来构建问题。远程依赖需要在 repositories 标签下声明远程仓库,例如 jcenter()、google()、maven 仓库等。

依赖配置

目前 Gradle 版本支持的依赖配置有:implementation、api、compileOnly、runtimeOnly和annotationProcessor,已经废弃的配置有:compile、provided、apk、providedCompile。此外依赖配置还可以加一些配置项,例如 AndroidTestImplementation、debugApi 等等。

常用的是 implementation、api、compileOnly 等几个依赖配置,含义如下:

  • implementation
    与 compile 对应,会添加依赖到编译路径,并且会将依赖打包到输出(.aar 或 .apk),但是在编译时不会将依赖的实现暴露给其他 module,也就是只有在运行时其他 module 才能访问这个依赖中的实现。使用这个配置,可以显著提升构建速度,因为它可以减少重新编译的 module 的数量。建议,尽量使用这个依赖配置。
  • api
    与 compile 对应,功能完全一样,会添加依赖到编译路径,并且会将依赖打包到输出(.aar 或 .apk),与 implementation 不同,这个依赖可以传递,其他 module 无论在编译时和运行时都可以访问这个依赖的实现,也就是会泄漏一些不应该使用的实现。举个例子,A 依赖 B,B 依赖 C,如果都是使用 api 配置的话,A 可以直接使用 C 中的类(编译时和运行时),而如果是使用 implementation 配置的话,在编译时,A 是无法访问 C 中的类的。
  • compileOnly
    与 provided 对应,Gradle 把依赖加到编译路径,编译时使用,不会打包到输出(.aar 或 .apk)。这可以减少输出的体积,在仅编译时需要、运行时可选的情况下,很有用。
  • runtimeOnly
    与 apk 对应,Gradle 添加依赖只打包到 .apk,运行时使用,但不会添加到编译路径。这个没有使用过。
  • annotationProcessor
    与 compile 对应,用于注解处理器的依赖配置,这个没用过。

查看依赖树

可以通过运行依赖的 Gradle 任务,查看单个 module 或者这个 project 的依赖,如下:

  1. View -> Tools Windows -> Gradle(或者点击右侧的 Gradle 栏);
  2. 展开 AppName -> Tasks -> android,然后双击运行 AndroidDependencies。运行完,就会在 Run 窗口打出依赖树了。

依赖冲突解决

随着很多依赖加入到项目中,难免会出现依赖冲突,出现依赖冲突如何解决?

定位冲突

依赖冲突可能会报类似下面的错误:

通过查找类的方式(command + O)定位到冲突的依赖,进行排除。

如何排除依赖
  • dependencies 中排除(细粒度)

  • 全局配置排除

  • 禁用依赖传递

还可以在单个依赖项中使用 @jar 标识符忽略传递依赖:

  • 强制使用某个版本。如果某个依赖项是必需的,而又存在依赖冲突时,此时没必要逐个进行排除,可以使用 force 属性标识需要进行依赖统一。当然这也是可以全局配置的:

  • 在打包时排除依赖。先看一个示例:

代码表示在打 zip 包的时候会过滤掉名称中包含“unwanted”和“log”的 jar 包。这里调用的 exclude 方法的参数和前面的例子不太一样,前面的参数多是 map 结构,这里则是一个正则表达式字符串。
也可以使用在打包时调用 include 方法选择只打包某些需要的依赖项:

主要是使用 dependencies 中排除和全局配置排除。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注