Android SDK 封装

作为方案商的我们,工作中很多时候会引用第三方的SDK,同时也需要把我们做的内容封装成 SDK 供项目或客户调用,因此封装是工作中很常用的技能。这种对外封装 SDK 的能力对公司来说是财富实现的资本(不能被外界窥探或者破解成免费使用)。

一、前言

  • SDK:软件开发工具包(缩写:SDK、外语全称:Software Development Kit)一般都是一些软件工程师为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件时的开发工具的集合。

  • 封装 SDK:SDK 是提供给别人调用的工具。所以常见的 SDK 都是以 jar 包,so 库,aar 包等方式导入 APP 项目中。然后提供一些公开的API供接入方调用。

SDK 开发设计与 APK 开发不同的地方还是有些明显的不同之处,明显的区别是使用对象不一样,SDK 是基于开发者使用的,都是有一定的开发水平,相对于 APK 用户而言基本是大众用户,做得好不好不仅仅是运行起来性能、稳定行以及功能,还需要顾忌开发者调用是否方便、嵌入成本、更新维护成本,因为 SDK 是作为一个库给对方使用,但又不是开源的,还得注意保护知识产权。

二、SDK 方案选择

SDK 以什么样的形式提供,当前对外发布的 SDK 有三种:

  • jar 包
  • aar 包
  • so 库:是 C 或 C++ 语言而打包成的库(涉及NDK开发,不在本文讨论之中)

那 jar 和 aar 这两者是有什么区别?

2.1 格式差别

jar 包是延用 Java 的 SDK,在使用 Eclipse 时期,打包出来的基本都是 jar,当时也是可以用 AS 打 jar 包; 而 aar 是在 AS 诞生之后的产物,方便开发。

之前干过把 aar 处理成 jar,然后并到 Eclipse 工程中,因为早期的项目工程是 Eclipse,而第三方提供的是 aar SDK,Eclipse 用不了 aar。随着后面 AS 的稳定,项目就全部切换到 AS 环境下,就不存在这种事情。

2.2 内容差别

jar 中只包含了 class 文件与清单文件;
aar 中除了包含 jar 中的 class 文件还包含工程中使用的所有资源,class 及 res 资源文件全部包含。

简单一句话, aar 可以打包资源。

2.3 生成位置差别

生成 SDK 的位置不同,即运行生成的文件位置不同

  • jar: /build/intermediates/packaged-classes/release/classes.jar

  • aar: /build/outputs/aar/libbledemo.aar

2.4 使用方式差别

jar 使用:

  • 将打包出来的 jar 文件加入到 libs 中

  • 在 module 的 build.gradle 中加入代码,例如:

    implementation files('src/lib/libcchip.jar')
    //或
    implementation files('libs\\libcchip.jar')
    

aar 使用 (这里只讲解单层 aar 依赖):

  • 将打包出来的 aar 文件加入到 libs 中

  • 在 module 的 build.gradle 中与 android{} 平级下加入

       repositories {
           flatDir {
           dirs 'libs'
               }
           }
    
  • 在 module 的 build.gradle 中的 dependencies 里加入:
    implementation(name: 'libcchip', ext: 'aar')//注意这里加入的名字没有后缀名
    
  • 同步后可以 在External Libraries 中查看新加入的包

综合而言,看项目需要及结合实际情况来决,没有最好的 SDK,只有最合适的 SDK。

三、jar SDK 封装

3.1 配置 library 下的 build.gradle 文件

在 library 下的 build.gradle 中添加如下代码:

apply plugin: 'com.android.library'

//打 jar 包
def SDK_BASENAME = "libbledemo_V1.0.2";
def sdkJarPath = "build";
def zipFile = file('build/intermediates/packaged-classes/release/classes.jar')

task makeJar(type: Jar) {
    from zipTree(zipFile)
    from fileTree(dir: 'src/main', includes: ['assets/**'])
    baseName = SDK_BASENAME
    destinationDir = file(sdkJarPath)
}
makeJar.dependsOn(build)

3.2 配置工程目录下的 settings.gradle 文件

在 工程目录(根目录) 下的 settings.gradle 中添加如下代码:

include ':libBleDistribution'

如果不模块名称,则 Gladle 中不会出现 本 library 菜单。

3.3 执行 makeJar 指令

点击 IDE 右侧的边条 Gladle 菜单,找到本 library 下 other 文件夹下的 makeJar 命令,双击运行。

四、aar SDK 封装

aar 的打包就比较简单了。

4.1 配置工程目录下的 settings.gradle 文件

在 工程目录(根目录) 下的 settings.gradle中添加如下代码:

include ':libBleDistribution'

如果不模块名称,则 Gladle 中不会出现 本 library 菜单。

3.3 执行 assemble 指令

点击 IDE 右侧的边条 Gladle 菜单,找到本 library 下 build 文件夹下的 assemble 命令,双击运行。

四、SDK接口设计注意点

对于 SDK 而言,接口是连接 SDK 与客户产品的纽带,接口设计的优劣是衡量 SDK 产品易用性的重要指标。糟糕的 SDK 接口不仅仅给开发者带来的难用的主观印象,更有可能增加客户的开发成本,甚至影响产品质量。

前面在从事SDK开发的几年起初并不重视,在一次次填坑的过程中也逐渐意识到优秀的SDK接口设计必须要思考以下几点:

4.1 风格统一

统一的接口设计风格不仅仅是为了给开发者留下专业的印象。更进一步的,它可以传递给开发者 SDK 的设计理念。开发者通过这种风格上的暗示,可以更直观,不易犯错的调用 SDK 功能。

举例来说,我们可以设计 SDK 所有接口都通过 init 方法初始化,uninit 方法反初始化。开发者一旦接受了这种设定,会潜意识的注意有没有调用过 uninit 释放资源。

4.2 升级扩展

接口设计考虑升级扩展这一点对每一个负责持续迭代的产品的程序员来讲都是基本的要求。但是对于 SDK 产品,这一点显得尤为重要。原因在于,一般接口设计如果不合理可以通过后期迭代重构来补救,而 SDK 一旦发布之后,想要客户修改代码来升级 SDK 的成本是很高的。接口兼容性如果做的不好,客户就不会愿意升级,随之带来的问题是维护多个版本给我们自身带来的巨大的支撑压力。

4.3 注释说明

注释和接口是 SDK 的一体两面,它在 SDK 中有着和接口同等重要的作用。SDK 没有一个接口是多余的,同样,没有一个接口是可以被允许没有注释的。我们在开发SDK 时,始终要预设这样一个前提:开发者不会看我们的集成文档。因此,我们只有尽可能的丰富注释,才有可能最大程度将问题消灭在开发者翻阅文档或是提问之前。

五、SDK接口设计规范

我们基于通用编码规范,平台命名规范,以及行业通用惯例制定了自己的 SDK 接口设计规范。目前还没有特别完善和细化,但有了一个统一的标准之后,相信对未来的接口设计会有一个原则性的指导。尽管本篇讨论的是 Android 平台,但我们的 SDK 主要是针对移动端 iOS/Android 平台,采用的语言主要是 oc/java,其他平台的设计规范可以做一个参考。

5.1 类/接口

类/接口的命名必须使用名词。接口需要加 I 字符来区分。

// good case
public interface CChipIRecorder;   // android; 接口加I区分

// bad case
public interface CchipIImport;     // android; import不是名词

5.2 方法/函数

方法/函数名足够清晰易懂,除了行业内通用的专有名词,其他情况下不能使用缩写。

// good case
void setExposureCompensationRatio(float value); // android; 方法名即解释

参数尽可能少,超多4个参数需要考虑采用结构体/类封装。

采用结构体/类封装的好处除了减少方法长度,更重要的意义在于未来版本接口功能升级只需要新增配置属性,而不用提供新的接口。

// bad case
void setMusic(String path,long startTime,long duration);        // android; 使用music类封装

能采用同步接口的尽量不要用异步,异步接口需要在注释里说明。

设计同步接口的行为是明确的,设计异步接口可能让开发者误以为接口行为已经完成,从而做出一些错误调用。异步接口必须强调说明,并注明对应的回调方法是什么。

// bad case
int finishRecording();     // android; 异步接口需要说明对应的回调方法

一个接口只做一件事,使用doSomething命名。

// bad case
int editCompleted();            // android; 改为stopEdit/finishEdit

5.3 枚举

采用平台惯例的命名方法,枚举成员名称需要全大写,单词间用下划线隔开。

// good case

public enum FormatStyle {
    FULL,
    LONG,
    MEDIUM,
    SHORT;
}              // android; 系统api命名方法

// bad case
public enum AlivcLogLevel {
    AlivcLogLevelDebug(0),
    AlivcLogLevelInfo(1),
    AlivcLogLevelWarn(2),
    AlivcLogLevelError(3),
    AlivcLogLevelFatal(4);
}              // android; 枚举值命名不规范

5.4 成员变量/属性

不要直接暴露属性,提供 getter/setter 方法。
Param 类必须有构造器。

// bad case
public int videoWidth;                          // android; 提供getter/setter方法

5.5 命名惯例

获取已经存在的对象使用 get 命名。

// bad case
AliyunICanvasController obtainCanvasController(Context var1, int var2, int var3);  // android; 使用get代替obtain

获取一个 new 的新对象使用 create 命名。

create 可以给开发者暗示资源的是被创建出来的。

// good case
public static AliyunIEditor createAliyunEditor();  // android; 工厂方法使用create创建新实例

// bad case
AnimPlayerView newPasterPlayer();              // android; 使用create代替new

参数设置类/结构体命名需要声明用途,以 Param 结尾。

后缀可以是 Config,Setting 等等,一旦确定好需要所有接口保持统一。

// good case
public class AliyunVideoParam;    // android; 命名符合规范

// bad case
public class CropParam;         // android; 没有前缀
public class MediaInfo;         // android; 没有声明是录制参数,没有以Param结尾

设置效果:setXxx,移除效果:clearXxx

// bad case
int addAnimationFilter(EffectFilter var1);                  // android; 不符合规范

新增:addXxx,删除:removeXxx,清空:clearXxx

// good case
int addMediaClip(AliyunClip clip);        // android; 替换为clearPartList

// bad case
void deletePart();                        // android; 替换为removePart
void deleteAllPart();                     // android; 替换为clearPartList

数组/字典命名使用变量名 +List/Map/Array/Dictionary

之所以不用复数形式定义变量名,主要考虑到我们开发团队对名词的复数形式并没有深入掌握。与其命名不专业的变量名,不如用这种方式更为直观。

// good case
public List<Frame> getFrameList();         // android; 符合命名规范   

5.6 回调

回调方法除非特殊需求一般在主线程回调,回调接口注释必须说明在哪个线程。

保证开发者的所有接口调用都在主线程执行可以避免很多线程问题。
有一些回调需要在特定线程如渲染回调等需要注释说明。

回调类统一以 Callback 结尾,会调方法以 on+ 回调类名开始。

5.7 初始化/反初始化

主要模块必须通过 init 方法并使用 Param 配置参数初始化。

统一的初始化方式能让开发者快速熟悉 SDK 调用方式。

主要模块类一定要有 release 方法,release 删除底层资源与 java 同生命周期。

// bad case
void dispose();             // android; 没有使用release()方法

5.8 前缀

类,接口,结构体,枚举值必须加前缀。

java 虽然有包名区分不同类,但还是建议加前缀,这样可以让开发者更容易区分SDK接口。

5.9 包名

安卓所有 SDK 接口必须在统一的包名内。

5.10 注释

对外接口使用/** */注释,不要使用//注释。

很多接口需要多行注释才能解释清楚,所以在这里统一使用多行注释。

六、总结

我们封装的时候,需要考虑第一是灵活,第二是简单易用,第三是效率,第四是可维护性,第五是容错率。考虑到了这些,就可以封装适合自己的类。我们编程最重要的是全局考虑的一种思维!