简介面向Android开发者的第三方库学习合集围绕视图注入、网络请求、图片加载、依赖注入、事件通信、响应式编程、数据库与架构组件等适合初中级开发者做选型、集成与复盘。压缩包共2000个文件、约22.67MB以xml布局资源、png图片素材、json配置、java与class源码、jar/aar依赖包为主并含gradle构建文件与说明文本示例工程结构较完整。已有1349人学习下载可参考。内容涉及Butter Knife、Gson、Retrofit、OkHttp、Picasso、Glide、Dagger 2、EventBus、RxJava、Room、LiveData/ViewModel、LeakCanary等常用库可帮助读者理解用途、依赖组织与基础用法减少重复开发并提升可维护性。1. Android 第三方库选型先看依赖树再决定引哪一个一个模块里塞了四十多个依赖构建一次六分半改一行代码重跑要等两分钟——这是我见过的真实工程状态也是大多数团队在 Android 第三方库这件事上的共同处境。这些库确实把开发速度拉起来了代价却藏在看不见的地方方法数、编译期注解处理、运行时内存、版本冲突全在项目后期集中爆发。这份资源把 Android 开发各种第三方的库按网络、存储、图片、依赖注入、构建治理几条线拆开整理每个库给出了适用场景和接入要点适合已经能跑通一个 App、但还没形成选型标准的人。下面不谈“必装清单”只谈哪一类问题该用哪一层库去接。2. 网络与本地存储Retrofit 加 OkHttp 再叠层 Room 的接法Android 项目里第一波被引入的 Android 第三方库几乎都是网络和数据库。理由很功利这两块手写容易出事故而且一旦出事故排查成本最高。一个接口在弱网下超时不重试、一个列表在离线时白屏、一条缓存和服务器数据打架这些都不会在开发机上暴露只在用户手里暴露。2.1 网络层为什么先定拦截器和序列化很多人把 Retrofit 当成“网络库”其实它只做了一件事把接口方法翻译成 HTTP 请求。真正发包、连接池、超时、重定向、缓存全在 OkHttp 那一层。这意味着只要你想做统一的事情——加公共头、统一签名、统一打日志、统一处理 token 过期——落点只有一个就是拦截器链。把公共逻辑写进每个接口的注解里是这个环节最常见的返工来源。拦截器分两种用途完全不同混用会出问题类型注册方式拿到的请求典型用途应用拦截器addInterceptor最终发出的请求不含重定向公共头、日志、签名网络拦截器addNetworkInterceptor每次实际连接含重定向、重试抓包对账、统计真实流量序列化这块Gson 反射解析、字段名对不上时会静默给 null排查起来像黑匣子Moshi 和 kotlinx.serialization 走代码生成字段缺失或类型不符会在解析时直接抛问题暴露得早。我一般会优先选后者尤其是接口字段经常变动的项目。2.2 一套可直接抄的网络封装下面这段是可以直接放进工程的骨架重点在拦截器的注册顺序和错误分类object NetClient { private const val TIMEOUT_SECONDS 15L // 日志拦截器只在 debug 挂载, 线上包不输出请求体 private val loggingInterceptor HttpLoggingInterceptor().apply { level if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE } // 公共头统一注入, 避免每个接口手写 Header private val headerInterceptor Interceptor { chain - val origin chain.request() val request origin.newBuilder() .header(X-App-Version, BuildConfig.VERSION_NAME) .header(X-Platform, android) .build() chain.proceed(request) } private val okHttp OkHttpClient.Builder() .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .addInterceptor(headerInterceptor) // 应用拦截器先加 .addInterceptor(loggingInterceptor) // 日志要能看到公共头, 放在后面 .build() private val retrofit Retrofit.Builder() .baseUrl(https://api.example.com/) .client(okHttp) .addConverterFactory(MoshiConverterFactory.create()) .build() val api: ApiService retrofit.create(ApiService::class.java) }这段代码有三个地方值得展开。第一headerInterceptor必须放在loggingInterceptor之前否则日志里看不到注入的头联调时对不上账。第二HttpLoggingInterceptor.Level.BODY会把响应体整个读进内存超过 1MB 的图片或列表响应在 debug 包上就能把内存顶上去所以线上必须切到NONE。第三retryOnConnectionFailure只对连接层失败生效HTTP 5xx 和业务错误码它一概不管别指望它兜住所有重试。超时时间不是随便填的。15 秒适合普通 JSON 接口文件上传接口的writeTimeout要单独放大到 60 秒以上否则大文件必挂走 CDN 的静态资源请求反而可以压到 5 秒快速失败比一直转圈体验好。错误分类是最容易糊弄的一步也最容易在上线后翻车sealed interface NetResultout T { data class OkT(val data: T) : NetResultT data class Fail(val code: Int, val message: String) : NetResultNothing } suspend fun T safeCall(block: suspend () - T): NetResultT try { NetResult.Ok(block()) } catch (e: HttpException) { // 4xx / 5xx: 业务码通常在 errorBody 里, 要单独解析 NetResult.Fail(e.code(), e.response()?.errorBody()?.string().orEmpty()) } catch (e: IOException) { // 断网、DNS 失败、超时、连接被重置都归到这里 NetResult.Fail(-1, e.message ?: network error) } catch (e: Exception) { // 解析失败、字段类型不匹配 NetResult.Fail(-2, parse error) }把IOException和HttpException分开捕获是必须的。前者是“网络根本不通”UI 上应该给重试按钮后者是“服务器收到了但拒绝了”UI 上该给业务提示。我见过把两者合并成一句“加载失败”的实现用户断网时点十次重试都没反应因为重试逻辑压根没被触发。2.3 Room 落表还是落文件缓存边界的判断本地存储这一层的选型问题本质上是问这份数据要不要被查询。列表页、详情页这种需要按条件筛、按时间排、分页取的用 Room 落表纯配置、token、上次登录的手机号这类键值对用 DataStore图片、PDF、离线包这类二进制落文件目录数据库里只存路径。Room 接入时有两个细节容易被忽略Entity(tableName article) data class ArticleEntity( PrimaryKey val id: Long, val title: String, val updatedAt: Long ) Dao interface ArticleDao { // 返回 Flow, 表数据一变 UI 自动刷新, 不用手动通知 Query(SELECT * FROM article ORDER BY updatedAt DESC LIMIT :size) fun observeTop(size: Int): FlowListArticleEntity // Upsert 而不是先删后插, 避免列表闪一下 Upsert suspend fun upsertAll(items: ListArticleEntity) }Upsert是 Room 2.5 之后才有的早版本只能Insert(onConflict REPLACE)语义上会连带删除再插入触发外键级联。返回Flow的查询要在ViewModel的作用域里收集别在 Activity 里直接collect否则横竖屏切换会重复订阅。缓存策略上我一般按“先读库、再请求、回写库”三步走而不是“先请求、失败读库”。前者首屏永远有内容后者在弱网下白屏时间不可控。代价是要处理数据陈旧问题通常在表里加updatedAt超过阈值就在后台静默刷新。2.4 超时、重试与日志的参数取值把上面这些参数集中成一张表接入新项目时直接照抄参数建议值说明connectTimeout10s连接建立弱网下别低于 8sreadTimeout15s普通 JSON 接口writeTimeout60s有上传接口时单独配置retryOnConnectionFailuretrue仅连接层重试日志级别debug BODY / release NONE线上关掉避免泄露与内存问题数据库版本每次改表结构 1配 Migration别用 fallbackToDestructiveMigration最后一行是血泪经验。fallbackToDestructiveMigration()能让升级不崩代价是用户本地数据全清。开发期用着爽上线后每次发版都在赌用户不会骂人。3. 图片加载库Glide 的采样参数与列表复用的内存账图片是 Android 应用里最容易吃内存的一环也是最容易被“能用就行”糊弄过去的一环。一个 1080P 的列表页每张图原图解码出来是 4MB 级别的位图屏幕上同时可见 6 张就是一个 24MB 的峰值。图片加载库存在的意义一大半是把这件事压下去。3.1 图片库选型看三个硬指标选图片库别只看 API 好不好看看三件事生命周期绑定、采样策略、缓存分层。生命周期绑定决定请求会不会随页面销毁而取消。没绑定的库页面退出后请求还在跑回调回来时 View 已经没了轻则空指针重则内存泄漏。Glide 通过RequestManager自动跟随 Activity/Fragment 生命周期Coil 走协程作用域两者都能做到自动取消。采样策略决定解码出来的位图多大。这里的关键是“按控件尺寸采样”而不是“按原图解码再缩放”。一张 4000×3000 的图直接解码是 48MB按 720×720 采样后是 2MB 出头差距是二十倍。所有主流库默认都支持前提是你把目标尺寸告诉它。缓存分层决定二次打开快不快。内存缓存LruCache 磁盘缓存原始文件或转换后文件两层是标配。磁盘缓存策略有几种ALL缓存原图和转换图、SOURCE只缓存原图、RESOURCE只缓存转换图、NONE不缓存。列表页用ALL一次性大图用NONE防止把用户存储撑爆。3.2 加载链路的关键参数一段标准的列表图片加载长这样Glide.with(imageView) .load(url) .placeholder(R.drawable.ic_placeholder) // 占位图, 防止布局跳动 .error(R.drawable.ic_error) // 失败图, 别留空白 .override(720, 720) // 按控件尺寸采样, 关键参数 .centerCrop() // 裁剪方式要和 ImageView 的 scaleType 一致 .diskCacheStrategy(DiskCacheStrategy.ALL) .into(imageView)override(720, 720)这行的取值有讲究。写死一个数看着省事但同一张图在不同屏幕密度、不同控件尺寸上会被反复解码。更稳的写法是跟随控件实际测量尺寸或者按屏幕宽度和列数算一个合理值。我一般会按列表列数算三列网格、屏幕宽 1080单格约 360px取 1.5 倍密度的 540 就够再高就是浪费内存。centerCrop()必须和ImageView的scaleType保持一致。两者不一致时会出现图片被裁两次、人脸被切掉一半的情况。这类问题在测试机上不一定复现因为图片比例刚好合适换个数据源就露馅。placeholder的作用不只是好看。没有占位图时图片加载完成前后控件高度会变化列表滚动位置会跳用户滑到一半内容突然位移。占位图给了布局一个固定的初始尺寸这个问题就没了。3.3 列表滑动卡顿的定位方法列表滑动的卡顿有一半来自图片加载但很难一眼看出是哪一环。我通常按这个顺序排先看有没有在滚动时发起请求。快速滑动时每一帧都触发加载等于把所有请求堆在同一个时间窗口。做法是监听滚动状态SCROLL_STATE_FLING期间暂停加载停下来再恢复。多数库都提供了现成的暂停/恢复接口。再看是不是在onBindViewHolder里做了额外计算。解码、圆角处理、模糊滤镜这些如果在主线程做掉帧是必然的。圆角用CircleCrop或RoundedCorners变换别自己拿 Canvas 画。最后看缓存命中率。如果每次滑动都在重新解码同一张图说明缓存配置有问题常见原因是override尺寸每次算出来都不一样导致缓存 key 不同缓存等于没有。这个问题极其隐蔽因为功能上完全正常只是电量和流量在偷偷烧。还有一个高频踩坑点在onBindViewHolder里先setImageDrawable(null)再加载。有些老代码为了防止图片错位这么写结果每次复用都清一次占位图闪一下视觉上就是列表在抖。新版本库自己会处理复用错位不需要手动清。4. 依赖注入与注解处理Hilt 加 KSP 的编译期成本依赖注入这件事小项目里觉得多余大项目里离不开。它真正解决的问题不是“少写几个 new”而是把对象的构造和它的使用场景解耦让替换实现、注入测试替身变得可能。代价是编译期成本这个成本在引入注解处理器之后会变得非常直观。4.1 为什么编译期注解处理会成为瓶颈注解处理器在编译期扫描源码、生成辅助类这部分工作发生在每一次增量编译里。kapt 的机制更特殊它要先跑一遍 Java 注解处理再跑 Kotlin 编译等于同一份代码被处理两次。项目里同时有 Hilt、Room、Glide 的注解处理器时这个开销会叠加。KSP 是 Kotlin 编译器原生的符号处理接口跳过中间的 Java 存根生成环节只跑一遍。实测下来大型项目从 kapt 迁到 KSP全量构建时间能砍掉三到五成增量构建的收益更明显。这个数字因项目而异但方向是确定的。迁移不是无痛的。KSP 对注解处理器的 API 做了限制不是所有库都有 KSP 版本。迁移前先确认依赖清单里每个用了注解处理器的库都有对应支持否则会出现“迁了一半卡住”的尴尬局面新旧两套并存编译时间反而更长。4.2 从 kapt 迁到 KSP 的步骤迁移的核心改动只有三处插件、依赖声明方式、处理器参数。// build.gradle.kts (module 级) plugins { id(com.google.devtools.ksp) version 1.9.20-1.0.14 // 移除 id(kotlin-kapt) } dependencies { implementation(com.google.dagger:hilt-android:2.48) // kapt(com.google.dagger:hilt-compiler:2.48) ksp(com.google.dagger:hilt-compiler:2.48) implementation(androidx.room:room-runtime:2.6.1) ksp(androidx.room:room-compiler:2.6.1) } ksp { // 处理器参数走这里, 和 kapt 的 arguments 写法不同 arg(room.schemaLocation, $projectDir/schemas) }三个地方要注意。第一插件版本和 Kotlin 版本是绑定的后缀1.9.20-1.0.14里前半段必须和项目的 Kotlin 版本对齐否则插件直接不生效。第二kapt和ksp不能对同一个处理器同时声明会重复生成代码导致编译失败。第三ksp { arg(...) }是唯一的参数入口kapt 那套kapt { arguments { } }的写法在这里不认。Room 的 schema 导出路径这一项迁移时最容易漏。漏了之后编译会报一个警告而不是错误但数据库版本升级时的迁移校验就失效了等到线上改表才发现在没有历史 schema 可比对那时候只能靠猜。我一般会把schemas目录一起提交进版本库。4.3 模块划分与作用域的边界Hilt 的模块划分有个常见误区把所有Provides塞进一个SingletonComponent。这样做能跑但会带来两个问题——一是所有单例在应用启动时就被创建冷启动时间被拉长二是作用域失去意义本来该随页面销毁的对象变成了全局持有内存泄漏的温床。合理的划分是按存活周期分层组件生命周期放什么SingletonComponent应用级OkHttpClient、数据库、DataStoreActivityRetainedComponent跨配置变更ViewModel 依赖的 RepositoryViewModelComponentViewModel 级页面级 UseCaseActivityComponentActivity 级页面内共享的状态对象判断标准很简单这个对象需不需要在页面销毁时释放需要就用低一级的作用域不需要才往上提。网络客户端这种带连接池的必须全局唯一页面状态这种带 UI 引用的必须跟着页面走。还有一个隐性成本Hilt 生成的代码量和你注入的类数量成正比。一个两百个类的项目生成代码可能有上千个类方法数直接顶到 dex 上限。开启 multidex 是必然的但更要紧的是别滥用注入——一个只用一次的类手动构造比注入更划算。5. 避坑与排查五条血泪记录与依赖冲突定位依赖冲突是 Android 第三方库绕不过去的一道坎。同一个库被不同模块以不同版本引入Gradle 默认选高版本但高版本不一定兼容运行时就抛NoSuchMethodError或者ClassNotFoundException。这类崩溃的特点是堆栈指向业务代码真正的原因在依赖树里。5.1 先用命令把冲突摊开排查的第一步永远是看依赖树别靠猜。# 看某个模块的完整依赖树 ./gradlew :app:dependencies --configuration releaseRuntimeClasspath # 只看某个库被谁引入, 以及最终解析到哪个版本 ./gradlew :app:dependencyInsight \ --configuration releaseRuntimeClasspath \ --dependency okhttp # 输出到文件, 方便搜索和比对 ./gradlew :app:dependencies deps.txtdependencyInsight是最有用的一个它会明确告诉你“为什么选了这个版本”包括所有请求过这个库的路径和最终的版本裁决理由。找到冲突之后两种处理方式用constraints强制统一版本或者用exclude把传递依赖摘掉。前者更稳妥后者容易摘掉不该摘的东西导致运行时缺类。5.2 五条翻车记录的现象、原因与解决第一条只在 release 包崩溃的 NoSuchMethodError现象是 debug 构建一切正常release 包一启动就崩堆栈指向某次网络请求。原因是 debug 和 release 走了不同的依赖配置某个库在 release 下被另一个模块的低版本覆盖了。解决方式是把releaseRuntimeClasspath单独拉出来看一遍依赖树别只看 debug 的。这件事我吃过一次亏之后每次加依赖都会把两个 configuration 都过一遍。第二条注解处理器版本和运行时库版本不匹配现象是编译通过、启动崩溃报某个生成类找不到方法。原因是implementation引的库版本是 2.48ksp引的处理器版本还是 2.46两边生成的 API 签名对不上。解决方式是把这两行放在相邻位置用同一个版本变量控制改一个另一个跟着变。第三条图片库和压缩库的回调线程打架现象是列表滚动时偶发闪退堆栈里是CalledFromWrongThreadException。原因是某个图片变换库的回调在子线程触发而代码里直接更新了 UI。图片库默认把结果投递回主线程但自己写的Transformation里如果做了异步操作回调线程就不受控了。解决方式是所有 UI 更新统一走Handler(Looper.getMainLooper())或runOnUiThread别假设回调线程。第四条数据库升级后老用户白屏现象是新版本上线后一部分老用户打开列表页空白新装用户正常。原因是改了表结构但没写MigrationRoom 校验 schema 失败后抛异常异常被上层吞掉UI 就停在空状态。解决方式是每次改表都写迁移并且在 CI 里加一步 schema 校验改表不提迁移直接让构建失败。这件事的后悔药是没有的线上数据丢了就是丢了。第五条混淆后反射失效现象是 release 包某功能静默失效日志里没有任何异常。原因是序列化库或依赖注入框架依赖反射读取类信息混淆把类名改了之后反射找不到。解决方式是给相关类加 keep 规则而且要按库的官方文档加别自己凭感觉写。更稳的做法是把序列化改成代码生成方案从根上不依赖反射。这五条的共同点是都不是语法错误都能编译通过都在特定条件下才暴露。所以验证环节不能只靠跑一遍 demo得在 release 构建、混淆开启、低版本系统、弱网这几种条件下各过一遍。6. 依赖治理用版本目录和约束把库压到一处依赖治理这件事做不做在项目前期看不出差别到了三十个库以上差别就是“改一个版本号花五分钟”和“改一个版本号花一下午”。我现在所有新项目第一件事就是建版本目录。所有库的版本写在gradle/libs.versions.toml里模块里只引别名不写版本号[versions] okhttp 4.12.0 retrofit 2.9.0 room 2.6.1 [libraries] okhttp-core { module com.squareup.okhttp3:okhttp, version.ref okhttp } retrofit-core { module com.squareup.retrofit2:retrofit, version.ref retrofit } room-runtime { module androidx.room:room-runtime, version.ref room } room-compiler { module androidx.room:room-compiler, version.ref room }配合模块里的implementation(libs.retrofit.core)效果是版本只有一处来源升级时改 toml 一处全项目生效。更重要的是它强制你把“这个库为什么被引入”写清楚——写在目录里比散落在十几个 build 文件里好审得多。第二件事是用constraints兜底传递依赖dependencies { constraints { // 强制统一 okhttp 版本, 防止被传递依赖拉低 implementation(com.squareup.okhttp3:okhttp:4.12.0) { because(统一连接池行为, 避免多版本共存) } } }because那一行不是装饰。半年后有人想删掉这条约束时看到理由就会先想一想。没有理由的约束迟早会被当成历史遗留清掉。第三件事是定期跑一次依赖体检。我习惯在每个版本封板前执行一次./gradlew :app:dependencies重点看三样有没有同一个库出现两个大版本、有没有引入之后从没用过的库、有没有库的体积和它提供的功能明显不匹配。最后一项最容易被忽略——为了一个工具方法引入一个几 MB 的库在方法数紧张的项目里是实打实的负担。从那以后我每次往工程里加一个 Android 第三方库之前都会强制走一遍这三步先在版本目录里登记再确认它和现有依赖没有版本交叉最后在 release 构建上跑一次冒烟。多花十分钟省下的是上线前那一夜的排查。希望帮到你。本文还有配套的精品资源点击获取