从踩坑到填坑:一个Android SDK开发者的aar依赖血泪史
从踩坑到填坑一个Android SDK开发者的aar依赖血泪史那天下午产品经理突然把我叫到会议室脸上挂着那种“有个好消息要告诉你”的微笑。他说“我们打算把你上次做的那个工具库做成SDK让其他团队也能用。”我表面点头心里却咯噔一下——SDK我从来没做过啊。当时我脑子里只有一个念头不就是把现有的项目打个包吗应该不难吧。事实证明我太天真了。接下来的两周我几乎每天都在和各种奇怪的编译错误、运行时崩溃、依赖丢失问题搏斗。最让我崩溃的是明明在开发环境下跑得好好的代码打包成aar后在其他项目里一运行就报资源找不到、类找不到。那种感觉就像你精心包装了一份礼物结果对方拆开发现里面是空的。如果你也在经历类似的痛苦或者正准备踏入SDK开发的深水区这篇文章就是为你准备的。我会用最真实的踩坑经历带你走一遍从发现问题到彻底解决的完整路径。这不是一篇干巴巴的教程而是一个开发者的实战记录——包括那些让我熬夜到凌晨三点的坑以及最终找到的优雅解决方案。1. 初识aar当打包不再是简单的压缩刚开始接触aar打包时我以为这就像把项目文件打个zip包那么简单。Android Studio的“Build Build Bundle(s) / APK(s) Build APK(s)”旁边不是有个“Build AAR”吗点一下不就完事了确实生成aar文件本身很简单。但问题就出在这个“简单”上。1.1 第一个坑资源冲突的幽灵我第一次打包完aar兴冲冲地在测试项目里引入结果一运行就崩了。错误日志是这样的AAPT: error: resource android:attr/layout_constraintWidth_default (aka com.example.mysdk:attr/layout_constraintWidth_default) not found.我当时就懵了——这什么情况我的SDK里明明没有用ConstraintLayout啊问题根源aar打包时只会包含你模块自己的资源文件但不会包含你依赖的第三方库的资源。如果你的SDK依赖了某个库而这个库又依赖了其他库比如AndroidX的某些组件这些传递依赖的资源在打包时可能会丢失。更糟糕的是如果宿主项目也用了相同的库但版本不同就会产生资源冲突。Android的资源合并机制在这种情况下会变得非常混乱。我当时的临时解决方案是在测试项目的build.gradle里强制指定版本configurations.all { resolutionStrategy { force androidx.constraintlayout:constraintlayout:2.1.4 } }但这只是权宜之计。你不能要求每个使用你SDK的开发者都在他们的项目里添加这些配置。1.2 深入aar内部看看里面到底有什么要理解问题首先得知道aar文件到底是什么。我用了最原始的方法——把.aar文件后缀改成.zip然后解压my-sdk-release.aar ├── AndroidManifest.xml ├── classes.jar ├── res/ │ ├── layout/ │ ├── drawable/ │ └── values/ ├── R.txt ├── assets/ └── libs/ └── 一些本地jar文件关键发现就在这里libs文件夹里只有我直接放在libs目录下的jar文件而通过Gradle依赖的第三方库无论是远程仓库还是本地aar一个都没有这就是aar依赖问题的核心aar文件不包含它的Gradle依赖。它只包含编译后的class文件在classes.jar里资源文件清单文件本地jar库可能的jni库但通过implementation、api、compileOnly等方式声明的依赖都不会被打包进去。2. 依赖传递的迷宫为什么简单的方案行不通意识到问题后我开始寻找解决方案。网上最常见的建议有两种fat-aar和Maven仓库。我先尝试了前者结果掉进了更深的坑。2.1 Fat-aar的诱惑与局限Fat-aar或者叫fat-aar-android是一个Gradle插件它的思路很直接把所有依赖都“嵌入”到最终的aar包里。这样使用者就不需要额外声明依赖了。安装很简单在项目的build.gradle里添加buildscript { repositories { mavenCentral() } dependencies { classpath com.github.kezong:fat-aar:1.3.8 } }然后在模块的build.gradle里apply plugin: com.kezong.fat-aar dependencies { implementation com.squareup.retrofit2:retrofit:2.9.0 embed com.squareup.retrofit2:retrofit:2.9.0 // 关键在这里 }看起来很美对吧但实际用起来问题一大堆问题一传递依赖处理不完全如果你的SDK依赖了库A而库A又依赖了库B即使你用embed声明了库A库B也不会被自动嵌入。你需要手动找到所有传递依赖并一一嵌入。问题二资源合并冲突当多个库有相同资源ID时fat-aar的合并策略可能导致不可预知的行为。我遇到过最诡异的情况是两个库都有R.string.app_name合并后随机选择一个导致运行时字符串错乱。问题三ProGuard/R8混淆问题如果嵌入的库已经是被混淆过的比如某些商业SDK再次混淆可能导致类名冲突或方法签名错误。问题四版本管理噩梦想象一下这个场景你的SDK嵌入了Retrofit 2.9.0但宿主项目用的是Retrofit 2.8.0。现在项目里有两个不同版本的Retrofit类加载器会加载哪一个答案是不确定。可能都加载导致ClassCastException也可能只加载一个但另一个库期望的API不存在。我花了三天时间调试fat-aar的各种问题后决定放弃这个方案。特别是当我发现我们公司内部的一个基础工具库它自己又依赖了五个其他库而这五个库还有各自的依赖……手动管理这些传递依赖几乎是不可能的。2.2 依赖声明的艺术api vs implementation vs compileOnly在寻找更好方案的过程中我重新审视了Gradle的依赖配置。很多人包括之前的我对这些配置的区别理解不够深入配置名称是否打包到aar是否传递给使用者适用场景api否是你希望使用者也能访问的接口或抽象类implementation否否内部实现细节使用者不需要知道compileOnly否否仅编译时需要运行时由宿主提供runtimeOnly否否仅运行时需要编译时不需要这里有个关键点无论用哪种配置依赖都不会被打包到aar里。它们只是告诉Gradle“我在编译或运行时需要这些库”。那么问题来了如果依赖不打包使用者怎么知道需要哪些库呢这就是pom.xml文件的作用。当你把aar发布到Maven仓库时Gradle会生成一个pom.xml文件里面记录了所有的依赖声明。其他项目通过Maven/Gradle引用你的SDK时会自动下载这些传递依赖。但这里又有一个坑Gradle默认不会为aar模块生成完整的pom文件。3. Maven发布从混乱到秩序在经历了fat-aar的失败后我把目光投向了Maven仓库方案。这个方案的核心思想是不要试图把所有东西塞进一个包里而是通过依赖管理来解决。3.1 搭建私有Nexus仓库我们公司没有现成的Maven仓库所以我决定自己搭一个。选择Nexus是因为它功能全面且开源。安装步骤其实比想象中简单下载Nexus从Sonatype官网下载最新版我用的3.45.0解压运行# Windows nexus.exe /run # Linux/Mac ./nexus run访问管理界面http://localhost:8081初始登录用户名admin密码在sonatype-work/nexus3/admin.password文件里创建仓库我创建了三个仓库maven-releases发布正式版maven-snapshots发布测试版maven-public组合仓库方便引用注意第一次启动可能需要几分钟初始化耐心等待控制台输出“Started Sonatype Nexus”后再访问网页。3.2 Gradle发布配置的演进史配置Gradle发布插件是个技术活而且不同Gradle版本的配置方式差异很大。我经历了从老式maven插件到新式maven-publish插件的完整迁移。Gradle 6.x及以下的老方法apply plugin: maven uploadArchives { repositories { mavenDeployer { repository(url: http://localhost:8081/repository/maven-releases/) { authentication(userName: admin, password: admin123) } pom.project { groupId com.company artifactId my-sdk version 1.0.0 } } } }这个方法在Gradle 7.0后被标记为废弃因为maven插件被移除了。Gradle 7.0的新标准plugins { id maven-publish } afterEvaluate { publishing { publications { release(MavenPublication) { // 关键groupId、artifactId、version groupId com.company artifactId my-sdk version 1.0.0 // 指定要发布的aar文件 artifact($buildDir/outputs/aar/${project.getName()}-release.aar) // 生成pom.xml并包含依赖信息 pom.withXml { def dependenciesNode asNode().appendNode(dependencies) // 处理所有配置的依赖 [api, implementation].each { configName - configurations[configName].allDependencies.each { dep - if (dep.group ! null dep.name ! null dep.version ! null) { def dependencyNode dependenciesNode.appendNode(dependency) dependencyNode.appendNode(groupId, dep.group) dependencyNode.appendNode(artifactId, dep.name) dependencyNode.appendNode(version, dep.version) // 根据配置类型设置scope if (configName api) { dependencyNode.appendNode(scope, compile) } else { dependencyNode.appendNode(scope, runtime) } } } } } } } repositories { maven { url http://localhost:8081/repository/maven-releases/ credentials { username admin password admin123 } } } } }这个配置有几个关键改进使用afterEvaluate确保所有依赖配置完成后再生成pom自动收集api和implementation依赖正确设置依赖的scope编译期或运行期3.3 多模块项目的发布策略我的SDK项目有多个模块核心模块、网络模块、UI组件模块。每个模块都需要单独发布但又希望保持统一的版本管理。方案一每个模块独立配置这是最直接但最繁琐的方法。每个模块的build.gradle里都要写一遍发布配置版本号要手动同步。方案二使用根项目的subprojects在根项目的build.gradle里subprojects { apply plugin: maven-publish afterEvaluate { project - if (project.plugins.hasPlugin(com.android.library)) { publishing { publications { release(MavenPublication) { // 为每个模块动态生成坐标 groupId com.company artifactId project.name // 使用模块名作为artifactId version rootProject.ext.versionName artifact($buildDir/outputs/aar/${project.getName()}-release.aar) // ... 同样的pom配置 } } } } } }方案三自定义Gradle插件高级对于大型项目我最终写了一个自定义插件// publish-plugin.gradle apply plugin: maven-publish ext { publishConfig { groupId com.company artifactId project.name version rootProject.ext.sdkVersion afterEvaluate { publishing { publications { release(MavenPublication) { groupId groupId artifactId artifactId version version // 自动找到release版本的aar def aarFile fileTree(dir: $buildDir/outputs/aar, include: *-release.aar) if (!aarFile.isEmpty()) { artifact(aarFile.singleFile) } // 包含源码和文档可选 artifact sourcesJar artifact javadocJar pom.withXml { // 依赖处理逻辑 } } } repositories { maven { url rootProject.ext.nexusUrl credentials { username rootProject.ext.nexusUsername password rootProject.ext.nexusPassword } } } } } } } // 在模块的build.gradle中只需一行 apply from: ../publish-plugin.gradle这个方案的好处是配置集中、版本统一、易于维护。4. 实战从零构建完整的SDK发布流程经过前面的摸索我总结出了一套完整的SDK开发和发布流程。这套流程在我们团队已经稳定运行了半年发布了十几个SDK版本。4.1 项目结构标准化首先建立标准的项目结构sdk-project/ ├── build.gradle # 根项目配置 ├── settings.gradle # 模块声明 ├── gradle.properties # 统一版本号等属性 ├── publish.gradle # 发布配置共享 ├── core/ # 核心模块 │ ├── build.gradle │ └── src/ ├── network/ # 网络模块 │ ├── build.gradle │ └── src/ └── ui/ # UI组件模块 ├── build.gradle └── src/在gradle.properties中定义统一属性# SDK版本 sdkVersion1.2.0 # Nexus配置 nexusUrlhttp://nexus.company.com/repository/maven-releases/ nexusSnapshotUrlhttp://nexus.company.com/repository/maven-snapshots/ nexusUsernamedeploy nexusPasswordyour_password_here # 发布信息 sdkGroupcom.company.sdk4.2 自动化发布脚本手动执行publish任务太麻烦我写了一个自动化脚本#!/bin/bash # publish.sh set -e # 遇到错误立即退出 echo SDK发布流程开始 # 1. 检查当前分支 CURRENT_BRANCH$(git branch --show-current) if [ $CURRENT_BRANCH ! main ] [ $CURRENT_BRANCH ! master ]; then echo 错误只能在main/master分支发布 exit 1 fi # 2. 运行测试 echo 运行单元测试... ./gradlew test if [ $? -ne 0 ]; then echo 单元测试失败停止发布 exit 1 fi # 3. 检查代码风格 echo 检查代码风格... ./gradlew ktlintCheck # 4. 获取版本号 VERSION$(grep sdkVersion gradle.properties | cut -d -f2) echo 准备发布版本: $VERSION # 5. 确认发布 read -p 确认发布版本 $VERSION 到Nexus(y/n): -n 1 -r echo if [[ ! $REPLY ~ ^[Yy]$ ]]; then echo 发布取消 exit 0 fi # 6. 执行发布 echo 开始构建和发布... ./gradlew clean ./gradlew :core:assembleRelease ./gradlew :network:assembleRelease ./gradlew :ui:assembleRelease ./gradlew publish # 7. 打标签 git tag -a v$VERSION -m Release version $VERSION git push origin v$VERSION echo 发布完成 echo 版本 $VERSION 已发布到Nexus echo Git标签 v$VERSION 已创建4.3 依赖管理的进阶技巧在SDK开发中依赖管理有几个特别需要注意的地方技巧一使用BOM统一版本如果你的SDK依赖多个Google或Jetpack库可以使用BOMBill of Materials来统一版本dependencies { // 使用BOM统一AndroidX版本 implementation platform(androidx.compose:compose-bom:2023.08.00) // 这些库会自动使用BOM中定义的版本 implementation androidx.compose.foundation:foundation implementation androidx.compose.material:material implementation androidx.compose.ui:ui-tooling // 可以覆盖特定库的版本 implementation androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1 }技巧二处理可选依赖有些功能是可选的比如你的SDK支持多种图片加载库。这时候可以用compileOnlydependencies { // 核心功能必须的依赖 implementation com.squareup.okhttp3:okhttp:4.11.0 // 可选功能如果用户项目有这些库就启用对应功能 compileOnly com.github.bumptech.glide:glide:4.15.1 compileOnly com.squareup.picasso:picasso:2.8 }然后在代码中通过反射检查类是否存在object ImageLoader { fun load(url: String, imageView: ImageView) { // 尝试Glide try { Class.forName(com.bumptech.glide.Glide) GlideLoader.load(url, imageView) return } catch (e: ClassNotFoundException) { // Glide不存在继续尝试Picasso } // 尝试Picasso try { Class.forName(com.squareup.picasso.Picasso) PicassoLoader.load(url, imageView) return } catch (e: ClassNotFoundException) { // 两个都不存在使用默认实现或抛出异常 throw IllegalStateException(需要添加图片加载库依赖Glide或Picasso) } } }技巧三避免依赖冲突的黄金法则尽量使用api而不是implementation除非你确定这个依赖是纯内部实现避免直接依赖具体版本使用版本范围或让使用者提供提供排除规则如果你的依赖和常见库冲突提供排除示例// 在你的SDK文档中提供这样的示例 dependencies { implementation(com.company:my-sdk:1.2.0) { // 排除可能冲突的模块 exclude group: com.google.code.gson, module: gson } implementation com.google.code.gson:gson:2.10.1 // 使用项目统一的版本 }4.4 版本策略与兼容性SDK的版本管理比普通应用复杂得多因为你要考虑向后兼容性。语义化版本SemVer是必须的MAJOR不兼容的API修改MINOR向下兼容的功能性新增PATCH向下兼容的问题修正但仅仅遵循SemVer还不够。我制定了一套更严格的规则所有公开API必须有SinceVersion注解/** * 新的数据模型从1.2.0开始支持 * SinceVersion(1.2.0) */ data class NewDataModel(val id: String, val value: Int)废弃的API必须保留至少两个大版本/** * Deprecated(使用NewDataModel代替, ReplaceWith(NewDataModel)) * UntilVersion(2.0.0) // 在2.0.0中移除 */ Deprecated(使用NewDataModel代替) class OldDataModel提供迁移指南每个大版本更新都提供详细的迁移文档维护变更日志CHANGELOG.md## [1.2.0] - 2024-01-15 ### 新增 - 添加NewDataModel支持 - 新增网络请求重试机制 ### 变更 - 最低Android版本提升到API 21 ### 废弃 - OldDataModel标记为废弃将在2.0.0移除 ### 修复 - 修复内存泄漏问题 #123 - 修复资源加载失败问题 #1254.5 持续集成与自动化测试SDK的质量直接影响所有使用它的项目所以自动化测试至关重要。GitLab CI配置示例# .gitlab-ci.yml stages: - test - build - publish variables: GRADLE_OPTS: -Dorg.gradle.daemonfalse before_script: - chmod x ./gradlew test: stage: test script: - ./gradlew test - ./gradlew lint artifacts: paths: - app/build/reports/ expire_in: 1 week build-snapshot: stage: build script: - ./gradlew clean - ./gradlew assembleRelease artifacts: paths: - core/build/outputs/aar/*.aar - network/build/outputs/aar/*.aar - ui/build/outputs/aar/*.aar expire_in: 1 week only: - develop publish-release: stage: publish script: - ./gradlew clean - ./gradlew publish only: - tags - /^release-.*$/多版本兼容性测试我创建了一个测试项目矩阵确保SDK在不同环境下都能正常工作Android Gradle PluginGradle版本最小SDK测试状态8.0.08.021✅7.4.07.521✅7.0.07.019✅4.2.06.7.116⚠️部分API受限这个测试矩阵帮我发现了几个只在特定Gradle版本下出现的问题。5. 高级话题当标准方案还不够时即使有了完整的Maven发布流程在实际项目中还是会遇到一些特殊场景需要特殊处理。5.1 资源混淆与冲突预防资源冲突是SDK开发中最头疼的问题之一。我总结了一套资源命名规范1. 所有资源加前缀在SDK模块的build.gradle中android { resourcePrefix mysdk_ // 强制所有资源以mysdk_开头 }这样你的colors.xml会变成!-- 之前 -- color nameprimary#6200EE/color color namesecondary#03DAC6/color !-- 之后 -- color namemysdk_primary#6200EE/color color namemysdk_secondary#03DAC6/color2. 使用资源合并规则创建res/values/public.xml来明确哪些资源要暴露?xml version1.0 encodingutf-8? resources !-- 只暴露这些资源给宿主项目 -- public namemysdk_primary typecolor / public namemysdk_secondary typecolor / public namemysdk_button_style typestyle / /resources3. 动态资源加载对于可能冲突的资源考虑运行时动态加载object SDKResources { private var resources: Resources? null fun init(context: Context) { // 创建独立的Resources实例 val packageManager context.packageManager val packageInfo packageManager.getPackageInfo( com.company.mysdk, PackageManager.GET_RESOURCES ) resources packageManager.getResourcesForApplication(packageInfo.applicationInfo) } fun getColor(name: String): Int { return resources?.getIdentifier(name, color, com.company.mysdk) ?.let { resources!!.getColor(it) } ?: throw IllegalStateException(资源未找到: $name) } }5.2 类加载器隔离在某些极端情况下即使资源不冲突类加载器冲突也会导致问题。特别是当SDK和宿主项目使用不同版本的同一个库时。解决方案自定义类加载器class IsolatedClassLoader(parent: ClassLoader) : ClassLoader(parent) { private val isolatedClasses mutableSetOfString() init { // 这些类使用隔离的类加载器 isolatedClasses.add(com.squareup.okhttp3.) isolatedClasses.add(com.google.gson.) } override fun loadClass(name: String, resolve: Boolean): Class* { // 检查是否需要隔离加载 val shouldIsolate isolatedClasses.any { name.startsWith(it) } return if (shouldIsolate) { // 从SDK自己的路径加载 findClass(name) } else { // 使用父类加载器宿主项目的类路径 super.loadClass(name, resolve) } } override fun findClass(name: String): Class* { // 实现从SDK的jar/aar中加载类 val path name.replace(., /) .class val resource getResourceAsStream(path) ?: throw ClassNotFoundException(name) val bytes resource.readBytes() return defineClass(name, bytes, 0, bytes.size) } }这个方法比较复杂而且有性能开销只建议在确实需要时使用。5.3 动态特性模块从Android 8.0开始Google引入了动态功能模块Dynamic Feature Module。这对SDK开发来说是个有趣的思路把可选功能做成动态模块。优势减少主包大小按需加载功能更好的模块化实现步骤创建动态功能模块File New New Module Dynamic Feature Module在SDK中动态加载class DynamicFeatureManager(private val context: Context) { private val splitInstallManager SplitInstallManagerFactory.create(context) suspend fun installFeature(feature: String): Boolean { val request SplitInstallRequest.newBuilder() .addModule(feature) .build() return try { splitInstallManager.startInstall(request) true } catch (e: Exception) { false } } fun getFeatureContext(feature: String): Context? { return try { context.createPackageContext( com.company.mysdk.$feature, Context.CONTEXT_INCLUDE_CODE or Context.CONTEXT_IGNORE_SECURITY ) } catch (e: PackageManager.NameNotFoundException) { null } } }使用反射调用动态模块中的类fun loadFeatureClass(feature: String, className: String): Any? { val featureContext dynamicFeatureManager.getFeatureContext(feature) ?: return null return try { val classLoader featureContext.classLoader val clazz classLoader.loadClass(className) clazz.newInstance() } catch (e: Exception) { null } }这个方案适合大型SDK可以把不常用的功能做成可选模块。6. 监控与反馈知道你的SDK在真实环境中的表现SDK发布后工作并没有结束。你需要知道它在真实项目中的表现。6.1 集成崩溃监控我在SDK中集成了轻量级的错误上报interface ErrorReporter { fun report(throwable: Throwable, metadata: MapString, String emptyMap()) } object SDKErrorReporter : ErrorReporter { private var delegate: ErrorReporter? null fun init(reporter: ErrorReporter) { this.delegate reporter } override fun report(throwable: Throwable, metadata: MapString, String) { // 添加SDK版本信息 val enhancedMetadata metadata mapOf( sdk_version BuildConfig.VERSION_NAME, sdk_build BuildConfig.BUILD_TYPE ) delegate?.report(throwable, enhancedMetadata) // 本地记录用于调试 logToFile(throwable, enhancedMetadata) } private fun logToFile(throwable: Throwable, metadata: MapString, String) { // 实现本地日志记录 } }这样使用者可以接入他们喜欢的监控服务Firebase Crashlytics、Bugsnag等而SDK的错误也会被统一上报。6.2 性能监控除了崩溃性能也很重要object PerformanceMonitor { private val traces mutableMapOfString, Long() fun startTrace(name: String) { traces[name] System.currentTimeMillis() } fun endTrace(name: String): Long { val start traces[name] ?: return -1 val duration System.currentTimeMillis() - start traces.remove(name) // 上报到监控平台 if (duration 1000) { // 超过1秒的才上报 reportSlowTrace(name, duration) } return duration } inline fun T measure(name: String, block: () - T): T { startTrace(name) try { return block() } finally { endTrace(name) } } } // 使用示例 val result PerformanceMonitor.measure(network_request) { apiService.getData() }6.3 使用情况统计了解哪些功能最常用可以帮助你决定开发重点class UsageTracker private constructor() { private val events mutableListOfEvent() data class Event( val name: String, val timestamp: Long System.currentTimeMillis(), val properties: MapString, Any emptyMap() ) fun track(eventName: String, properties: MapString, Any emptyMap()) { synchronized(this) { events.add(Event(eventName, System.currentTimeMillis(), properties)) // 每10个事件或每30秒上报一次 if (events.size 10) { flush() } } } fun flush() { val eventsToSend synchronized(this) { events.toList().also { events.clear() } } if (eventsToSend.isNotEmpty()) { // 实际上报到你的分析服务 AnalyticsService.report(eventsToSend) } } companion object { private val instance UsageTracker() fun track(eventName: String, properties: MapString, Any emptyMap()) { instance.track(eventName, properties) } } } // 在SDK的关键位置添加跟踪 class MySDK { fun initialize() { UsageTracker.track(sdk_initialized, mapOf( version to BuildConfig.VERSION_NAME )) } fun performAction(action: String) { UsageTracker.track(action_performed, mapOf( action to action, timestamp to System.currentTimeMillis() )) } }记得在隐私政策中说明数据收集情况并提供关闭选项。7. 文档与支持让使用者爱上你的SDK技术实现再完美如果文档不好用SDK也很难成功。7.1 自动化文档生成我使用DokkaKotlin版的Javadoc和MkDocs来自动生成文档build.gradle配置plugins { id(org.jetbrains.dokka) version 1.8.20 } tasks.register(dokkaHtml) { outputDirectory.set(file($buildDir/dokka)) moduleName.set(My SDK) moduleVersion.set(project.version.toString()) dokkaSourceSets { named(main) { includeNonPublic.set(false) skipEmptyPackages.set(true) skipDeprecated.set(true) perPackageOption { matchingRegex.set(.*\\.internal.*) suppress.set(true) } } } } tasks.register(generateDocumentation) { dependsOn(dokkaHtml) doLast { // 把Dokka输出转换成MkDocs格式 exec { commandLine(python, scripts/convert_docs.py) } // 构建静态网站 exec { commandLine(mkdocs, build) } } }7.2 完整的README模板一个好的README应该包含# My SDK [![Maven Central](https://img.shields.io/maven-central/v/com.company/mysdk)](https://search.maven.org/artifact/com.company/mysdk) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) 一个强大的Android SDK提供XXX功能。 ## 功能特性 - ✅ 功能一详细描述 - ✅ 功能二详细描述 - 功能三开发中 ## 快速开始 ### 添加依赖 groovy dependencies { implementation com.company:mysdk:1.2.0 }基本用法// 初始化 MySDK.init(context, your_api_key) // 使用主要功能 val result MySDK.doSomething()高级配置自定义设置MySDK.configure { timeout 30.seconds enableLogging BuildConfig.DEBUG cacheSize 50.megabytes }ProGuard规则如果你的项目使用了代码混淆请添加-keep class com.company.mysdk.** { *; } -dontwarn com.company.mysdk.**常见问题Q: 遇到ClassNotFoundException怎么办A: 请检查是否添加了所有必需依赖...Q: 资源冲突如何解决A: 我们的资源都带有mysdk_前缀如果仍有冲突...版本历史查看CHANGELOG.md技术支持提交Issue: GitHub Issues文档: https://docs.company.com/mysdk邮箱: sdk-supportcompany.com许可证Apache 2.0 - 查看LICENSE文件### 7.3 示例项目的重要性 我维护了三个示例项目 1. **基础示例**最小化集成展示核心功能 2. **高级示例**展示所有功能和配置选项 3. **问题排查示例**常见问题的重现和解决方案 每个示例项目都有独立的README说明要演示的功能和关键代码位置。 ## 8. 最后的思考SDK开发者的自我修养 回顾这段从踩坑到填坑的历程我最大的收获不是技术上的而是思维上的转变。SDK开发和普通应用开发有几个根本区别 **1. 你是服务提供者不是最终用户** 你的代码会被用在各种你无法预料的环境中。要假设最坏情况做最充分的测试。 **2. 向后兼容是生命线** 一次破坏性更新可能影响几十上百个应用。每次API变更都要慎之又慎。 **3. 文档和示例不是可选项** 没有文档的SDK就像没有说明书的电器——可能功能强大但没人知道怎么用。 **4. 错误信息要友好** 不要只抛出一个NullPointerException要告诉开发者可能的原因和解决方案。 **5. 监控和反馈循环至关重要** 你无法在发布前测试所有场景所以要建立机制收集真实使用中的问题。 现在当我在项目中引入第三方SDK时会特别关注它的文档质量、错误处理和版本策略。一个好的SDK能让开发者心情愉悦一个差的SDK能让人怀疑人生。我希望我的SDK属于前者。 SDK开发这条路我还在继续走。每个新版本发布时看到越来越多的团队在使用那种成就感是普通功能开发无法比拟的。虽然过程中有无数个想要放弃的瞬间但最终解决问题、看到一切正常运行的那一刻所有的努力都值得了。 如果你正准备开始SDK开发之旅我的建议是从简单的开始但要以高标准要求自己。处理好依赖关系写好文档做好测试倾听用户反馈。这条路不容易但走通了你会发现自己不仅是一个更好的开发者也是一个更好的协作者。

相关新闻

告别SSH黑窗口:5分钟用Windows远程桌面直连Linux图形界面(xrdp最新配置指南)

告别SSH黑窗口:5分钟用Windows远程桌面直连Linux图形界面(xrdp最新配置指南)

告别SSH黑窗口:5分钟用Windows远程桌面直连Linux图形界面(xrdp最新配置指南) 作为一名长期在Windows环境下工作,却又不得不与Linux服务器打交道的开发者,你是否也厌倦了在PuTTY或终端里敲打命令行的日子?尤…

2026/5/17 12:34:16 阅读更多 →
PCB设计软件选型指南:AD、Cadence与PADS的实战对比

PCB设计软件选型指南:AD、Cadence与PADS的实战对比

1. 从“画板子”说起:为什么选对软件比画对线更重要? 干了十几年硬件设计,从最初用Protel 99 SE画双面板,到后来折腾Allegro做高速背板,再到用AD做消费类产品快速迭代,我算是把这几款主流PCB设计软件都摸了…

2026/5/17 12:34:14 阅读更多 →
从随机游走到金融建模:布朗运动的性质与应用全景

从随机游走到金融建模:布朗运动的性质与应用全景

1. 从花粉到股价:布朗运动的“前世今生” 你可能想象不到,我们用来预测明天股票价格的数学模型,最初竟然源于一个植物学家观察花粉颗粒的“无心之举”。1827年,英国植物学家罗伯特布朗在显微镜下发现,悬浮在水中的花粉…

2026/7/3 20:52:58 阅读更多 →

最新新闻

BigFunctions终极指南:如何用150+函数超级增强BigQuery能力

BigFunctions终极指南:如何用150+函数超级增强BigQuery能力

BigFunctions终极指南:如何用150函数超级增强BigQuery能力 【免费下载链接】bigfunctions Supercharge BigQuery with BigFunctions 项目地址: https://gitcode.com/gh_mirrors/bi/bigfunctions BigFunctions是一个革命性的开源框架,它通过150预建…

2026/7/4 8:37:21 阅读更多 →
THSTrader完全指南:5步配置雷电模拟器与同花顺APP实战教程

THSTrader完全指南:5步配置雷电模拟器与同花顺APP实战教程

THSTrader完全指南:5步配置雷电模拟器与同花顺APP实战教程 【免费下载链接】THSTrader 量化交易工具。同花顺手机版模拟炒股python API,基于uiautomator2和图色方法实现。【可自行扩展到实盘】 项目地址: https://gitcode.com/gh_mirrors/th/THSTrader…

2026/7/4 8:35:20 阅读更多 →
用AI变声神器RVC实现10分钟语音转换:从零开始的完整实战指南

用AI变声神器RVC实现10分钟语音转换:从零开始的完整实战指南

用AI变声神器RVC实现10分钟语音转换&#xff1a;从零开始的完整实战指南 【免费下载链接】Retrieval-based-Voice-Conversion-WebUI Easily train a good VC model with voice data < 10 mins! 项目地址: https://gitcode.com/GitHub_Trending/re/Retrieval-based-Voice-C…

2026/7/4 8:31:20 阅读更多 →
从“是什么“到“为什么“:现代系统诊断工具witr如何重新定义进程分析范式

从“是什么“到“为什么“:现代系统诊断工具witr如何重新定义进程分析范式

从"是什么"到"为什么"&#xff1a;现代系统诊断工具witr如何重新定义进程分析范式 【免费下载链接】witr Why is this running? 项目地址: https://gitcode.com/GitHub_Trending/wi/witr 在当今复杂的系统环境中&#xff0c;当进程异常消耗资源、端…

2026/7/4 8:29:19 阅读更多 →
如何用Flask-profiler定位最耗时的API端点?实战案例分享

如何用Flask-profiler定位最耗时的API端点?实战案例分享

如何用Flask-profiler定位最耗时的API端点&#xff1f;实战案例分享 【免费下载链接】flask-profiler a flask profiler which watches endpoint calls and tries to make some analysis. 项目地址: https://gitcode.com/gh_mirrors/fl/flask-profiler Flask-profiler是…

2026/7/4 8:29:19 阅读更多 →
FlipperZeroHondaFirmware工作原理深度解析:433MHz RF信号捕获技术

FlipperZeroHondaFirmware工作原理深度解析:433MHz RF信号捕获技术

FlipperZeroHondaFirmware工作原理深度解析&#xff1a;433MHz RF信号捕获技术 【免费下载链接】FlipperZeroHondaFirmware Custom Firmware for the Flipper Zero, to add support for Honda key fobs (FCC ID: KR5V2X) 项目地址: https://gitcode.com/gh_mirrors/fl/Flippe…

2026/7/4 8:23:17 阅读更多 →

日新闻

Memcached 1.6.43 发布:关键安全修复版本,多项问题得到解决

Memcached 1.6.43 发布:关键安全修复版本,多项问题得到解决

Memcached 1.6.43 正式发布&#xff0c;这是一个关键的安全修复版本&#xff0c;修复了多个方面的问题&#xff0c;还对部分功能进行了优化。 安全修复亮点 此次发布在安全修复上表现突出。binprot 避免了项目引用计数溢出&#xff0c;mcmc 因安全问题提升了上游版本号&#xf…

2026/7/4 0:04:29 阅读更多 →
终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案

终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案

终极指南&#xff1a;使用HMCL启动器跨平台畅玩Minecraft的完整解决方案 【免费下载链接】HMCL A Minecraft Launcher which is multi-functional, cross-platform and popular 项目地址: https://gitcode.com/gh_mirrors/hm/HMCL HMCL&#xff08;Hello Minecraft! Lau…

2026/7/4 0:06:29 阅读更多 →
KMX63与PIC18F66K40在嵌入式HMI中的硬件协同与低功耗设计

KMX63与PIC18F66K40在嵌入式HMI中的硬件协同与低功耗设计

1. KMX63与PIC18F66K40的硬件协同架构解析KMX63作为一款三轴加速度计和磁力计组合传感器&#xff0c;与PIC18F66K40微控制器的搭配堪称嵌入式HMI开发的黄金组合。这套硬件组合的核心优势在于KMX63提供的高精度运动感知能力与PIC18F66K40强大的信号处理能力形成了完美互补。KMX6…

2026/7/4 0:06:29 阅读更多 →

周新闻

月新闻