Android SDK 开发总结

SDK 开发和 APP 开发的区别还是很大的。APP 更倾向于用户体验、功能更偏于特定业务、讲究的是快速迭代、快速占领市场。而 SDK 是为 APP 服务的,提供的大多是公共基础服务,如网络请求、打点统计、帐号服务等。

体积和功能

体积上:小!小!小!体积要尽可能的小!否则导致业务方接入后APP包体积暴增

  • 去除无用资源(主要是图片和 so 库)和代码
  • 不要依赖第三方库,至少大体积的库是不可以的,可以考虑移植开源代码(只需移植关键代码)
  • 优化代码结构,去除冗余的代码逻辑(考虑各种设计模式和分包策略)
  • 打包时进行混淆优化

功能上:

  • SDK 讲究功能专一,去除那些花里胡哨的东西
  • 基本功能完善,适用于所有的业务线
  • 根据业务方的需求反馈,考虑优化或者丰富 SDK 功能

兼容性

SDK 的兼容性主要考虑几个方面:

  • 对外接口(API)的兼容性:每次版本更新后,对外接口要尽可能保持不变。对于更改较大的接口,可以使用@Deprecated注解对老接口进行标记,并且做新接口调用的兼容,而不是直接删除老接口。
  • 功能的兼容性:在不影响整体功能和项目结构的基础上提供部分业务的需求定制化,可以形成配置项(以满足业务方提出一些乱七八糟的需求)。
  • 如果部分业务较难适配,那只能新开 SDK 分支,做业务的定制化版本(尽量不要这么干,可以和业务商量,因为分支太多后期很难维护)。
  • SDK 支持的 Android 版本的兼容性:minSdkVersion 的值应该尽可能的小,当然现在市场上基本都是 4.4 以上的手机。这也从侧面要求不要随便依赖第三方库。

稳定性

SDK 极其注重稳定性,要保证在不同 APP 环境下都能正常工作。如果出现问题就会导致发新版本,一方面要通知所有业务方做版本更新(麻烦),另一方面会打乱业务方 APP 的版本更新安排(背锅)。

所以:

  • 版本迭代要稳定:一般版本号都采用 x.y.z 模式,对于小功能或者是小的修复,增加 z 值即可,不能影响已经上线的服务。
  • 对于大版本的改动,增加 y 值甚至 x 值后,需要让 PM 告知业务方下次发版时使用最新版本的 SDK(如果是大 BUG 的修复,那就必须强制要求业务方更新)。
  • SDK 上线前必须经过完整的测试流程,保证功能正确、性能达到要求、对不同机型进行适配、对不同 Android 版本进行适配。业务方接入后,可以让业务方也走一遍测试,提供反馈报告。
  • SDK 的结构设计应该要有好的扩展性,比如接入一个新功能,就不能影响整体的代码框架,否则可能造成一些潜在的威胁,也会增加测试的工作量。

安全性

不光是 APP 需要一些安全措施,SDK 也是有必要保证安全性的。

  • SDK 混淆、加固、安全审核,这个一般是公司级别的安全管控。针对安全报告做对应修复即可。
  • 隐私数据的保护,必须进行加密或者掩码处理。比如:本地保存用户的登录态,手机号的掩码显示等。
  • 网络请求时的数据加密保护,部门一般都有自己的加密机制,大部分都是模仿 ssl 握手协议,采用非对称加密和对称加密结合的方式。更严格的话,可以增加自定义证书校验,不过这个成本较高。
  • 对于 SDK 中接入的部分第三方功能或者服务需要提供云控机制。因为第三方服务存在不稳定性和弱安全性。

配置清单

  • AndroidManifest,若SDK在配置清单中申请权限,编译时就会报合并重复错误,只能去除主项目中原有的声明。因此,不建议在SDK内部声明,应改为在接入文档中说明,由主项目配置。

  • <application/>标签,常见默认属性如:android:nameandroid:themeandroid:lable... 如无切实必要,请去除这些属性,避免打包时与主项目冲突。

Gradle文件

打包编译重命名,抽取aar+版本号自动重命名并复制到指定目录下

apply plugin: 'com.android.library'
static def releaseTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
android {
    compileSdkVersion 25
    buildToolsVersion '26.0.2'
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 6
        versionName "5.1.0"
    }
    ...
    repositories {
        flatDir {
            dirs 'libs'
        }
    }
    libraryVariants.all { variant ->
        if (variant.buildType.name == 'release') {
            variant.assemble.doLast {
                variant.outputs.each { output ->
                    def outputFile = output.outputFile
                    if (outputFile != null && outputFile.name.endsWith('release.aar')) {
                        def fileName = "${project.name}-release-${android.defaultConfig.versionName}-${releaseTime()}"
                        def outputPath = "/build/aar"
                        copy {
                            from outputFile
                            into outputPath
                        }
                        copy {
                            from outputFile
                            into outputPath
                            rename { fileName + ".aar" }
                        }
                    }
                }
            }
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    testImplementation 'junit:junit:4.12'
    implementation 'com.android.support:appcompat-v7:25.4.0'
}

资源id命名规范

Values中的colorsstringsstyles中的id命名应当注意保持一定的唯一性(如:命名统一加项目前缀),避免与主项目中的资源id冲突,造成SDK中的资源被覆盖,如themestring等等。

当然,从另一个角度来看,这个特性也可以作为定制UI的一个思路,用主项目中定义的相同资源覆盖掉SDK中的资源属性,从而实现灵活改变SDK的样式。

避免创建Application对象

若SDK中定义了Application对象,而主项目也定义了Application或者应用了第三方Application,则需将android:name属性替换掉,才能正常编译,因为一个App只能指定一个Application,若直接替换,则会造成SDK中的Application无法初始化,引起一大波问题。

解决方案有二:

1、避免在SDK中创建Application对象,暴露出一个初始化方法,在主项目的Application相关方法如onCreate()中注入相关参数执行

2、若无法避免,则可指定主项目的Application继承自SDK中的内置Application,问题可以解决

/**
 * SDK初始化
 */
public class MySDK {
    private static Context sContext;
    public MySDK() {
    }
    //提供给第三方调用,进行初始化
    public static void initSDK(Context context) {
        sContext = context;
        initLog(context);
    }
    private static void initLog(Context context) {
        new LogUtil.Builder(context)
                .setLogSwitch(true)
                .setGlobalTag("yezhou")
                .setLogHeadSwitch(true)
                .setLogFilter(LogUtil.D);
    }
    public static Context getContext() {
        return sContext;
    }
}

无法将第三方库打包进aar的问题

library module打包出来的 AAR,不会将依赖的第三方库打包进去。这个问题也是由来已久,详情可见:Android Studio how to package single AAR from multiple library projects?

解决方案有两个:

1、将AAR发布到远程仓库,这样gradle依赖下来的时候就会自动依赖第三方库

2、在主项目中显式指定SDK中的第三方依赖包,如常见的gson、okhttp等等

混淆问题

在Android开发中,我们一般都会对代码进行混淆后发布,所以在测试SDK时必须考虑到这个情况。

默认情况下,proguard-rules.pro中的混淆配置是不会被打包进aar中的,所以一般需要在主项目中手动指定混淆规则。

但是,为了提高接入体验,能否将SDK中的混淆配置也打包进aar中,让项目自动配置SDK的混淆文件呢?答案是肯定的,我们可以指定consumerProguardFiles属性,自定义引入的混淆规则,即可将*.pro文件打包进入aar中,项目打包时就会自动合并该配置文件。值得一提的是该属性只镇对library有效,对app无效。

consumerProguardFiles配置如下:

defaultConfig {
    minSdkVersion 17
    targetSdkVersion 26
    versionCode 1
    versionName "1.0"
    consumerProguardFiles 'proguard-rules.pro'//一行代码解决SDK内部混淆问题
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

文档规范

这个是被广泛忽视的一点,文档真的很重要!文档真的很重要!文档真的很重要!

  • SDK 的最终形态中一定要包含接入文档和演示 Demo 。虽然大部分业务方都不看文档,但一定要写(至少甩锅的时候好甩...)。至于演示 Demo ,一定要考虑尽可能多的场景,把 SDK 的功能都展示出来。
  • 文档要尽可能详细,但最好不要把所有内容都集中在一个文档里面,这样导致文档过长,业务方更加反感阅读。可以对文档做细划,比如:如何接入(jar、aar?或者是离线包还是 maven 库?)、基本功能如何使用(大部分业务只需要基本功能)、定制化功能如何使用、接入中可能遇到什么问题、怎么解决这些问题等等。
  • 最好能有个 Web 页面的服务平台(类似开放平台),业务方可以直接在平台上进行应用的注册、管理、文档阅读等。服务平台也可以做一些数据统计的可视化,方便 SDK 的后续发展。

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/29/summary-of-android-sdk-development/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android SDK 开发总结
SDK 开发和 APP 开发的区别还是很大的。APP 更倾向于用户体验、功能更偏于特定业务、讲究的是快速迭代、快速占领市场。而 SDK 是为 APP 服务的,提供的大多是……
<<上一篇
下一篇>>
文章目录
关闭
目 录