本页面旨在为开发者提供指导,帮助他们了解 API 委员会在 API 审核中强制执行的一般原则。
除了在编写 API 时遵循这些准则外,开发者还应运行 API Lint 工具,该工具会将许多此类规则编码到针对 API 运行的检查中。
您可以将其视为该 lint 工具遵循的规则的指南,以及有关无法以高精确度编码到该工具中的规则的一般建议。
API lint 工具
API lint 集成到 Metalava 静态分析工具中,并会在 CI 中的验证期间自动运行。您可以使用 m
checkapi
从本地平台检出手动运行该测试,也可以使用 ./gradlew :path:to:project:checkApi
从本地 AndroidX 检出手动运行该测试。
API 规则
Android 平台和许多 Jetpack 库早于这组准则的制定而存在,而本页面后面所述的政策会不断发展,以满足 Android 生态系统的需求。
因此,部分现有 API 可能不遵循这些准则。在其他情况下,如果新 API 与现有 API 保持一致,而不是严格遵循指南,可能会为应用开发者提供更好的用户体验。
如果您遇到需要解决的有关 API 的难题或需要更新的准则,请根据自己的判断与 API 委员会联系。
API 基础知识
此类别与 Android API 的核心方面相关。
必须实现所有 API
无论 API 的受众群体(例如公开或 @SystemApi
)如何,在合并或公开为 API 时,都必须实现所有 API 接口。请勿将 API 桩与日后要实现的代码合并。
没有实现的 API 接口会存在多种问题:
- 我们无法保证已露出合适或完整的表面。在客户端测试或使用 API 之前,无法验证客户端是否具有能够使用该功能的适当 API。
- 未实现的 API 无法在开发者预览版中进行测试。
- 没有实现的 API 无法在 CTS 中进行测试。
必须测试所有 API
这符合平台 CTS 要求、AndroidX 政策以及必须实现 API 的一般理念。
测试 API Surface 可提供基本保证,即 API Surface 可供使用,并且我们已解决预期的用例。仅测试是否存在 API 是不够的;必须测试 API 本身的行为。
添加新 API 的更改应在同一 CL 或 Gerrit 主题中包含相应的测试。
API 还应可测试。您应该能够回答“应用开发者将如何测试使用您的 API 的代码?”这个问题。
所有 API 都必须有文档
文档是 API 易用性的关键部分。虽然 API 接口的语法可能看起来很明显,但任何新客户端都无法理解 API 背后的语义、行为或上下文。
所有生成的 API 都必须符合相关准则
工具生成的 API 必须遵循与手写代码相同的 API 准则。
不建议用于生成 API 的工具:
AutoValue
:以各种方式违反准则,例如,无法使用 AutoValue 的工作方式实现最终值类或最终构建器。
代码样式
此类别与开发者应使用的一般代码样式有关,尤其是在编写公共 API 时。
遵循标准编码规范(除非另有说明)
外部贡献者可在此处查看 Android 编码规范文档:
https://source.android.com/source/code-style.html
总体而言,我们倾向于遵循标准的 Java 和 Kotlin 编码规范。
方法名称中的首字母缩写词不应采用大写形式
例如:方法名称应为 runCtsTests
,而非 runCTSTests
。
名称不得以 Impl 结尾
这会泄露实现细节,请避免这样做。
类
本部分介绍了与类、接口和继承相关的规则。
从适当的基类继承新的公共类
继承会在子类中公开可能不合适的 API 元素。例如,FrameLayout
的新公共子类看起来像 FrameLayout
,但包含新的行为和 API 元素。如果继承的 API 不适合您的用例,请从树上更高层级的类(例如 ViewGroup
或 View
)继承。
如果您想替换基类中的方法以抛出 UnsupportedOperationException
,请重新考虑您使用的基类。
使用基准集合类
无论是将集合作为参数还是将其作为值返回,始终优先使用基类而非具体实现(例如,返回 List<Foo>
而非 ArrayList<Foo>
)。
使用可针对 API 表达适当约束条件的基类。例如,对于集合必须有序的 API,请使用 List
;对于集合必须由唯一元素组成的 API,请使用 Set
。
在 Kotlin 中,请优先使用不可变集合。如需了解详情,请参阅集合可变性。
抽象类与接口
Java 8 添加了对默认接口方法的支持,这让 API 设计者能够向接口添加方法,同时保持二进制兼容性。平台代码和所有 Jetpack 库都应以 Java 8 或更高版本为目标平台。
如果默认实现是无状态的,API 设计者应优先使用接口而非抽象类,也就是说,默认接口方法可以实现为对其他接口方法的调用。
如果默认实现需要构造函数或内部状态,则必须使用抽象类。
在这两种情况下,API 设计者都可以选择保留单个方法抽象,以简化作为 lambda 的使用:
public interface AnimationEndCallback {
// Always called, must be implemented.
public void onFinished(Animation anim);
// Optional callbacks.
public default void onStopped(Animation anim) { }
public default void onCanceled(Animation anim) { }
}
类名称应反映其扩展的类
例如,为了清晰起见,扩展 Service
的类应命名为 FooService
:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
通用后缀
避免为实用程序方法集使用通用类名称后缀,例如 Helper
和 Util
。而是将这些方法直接放入关联的类或 Kotlin 扩展函数中。
如果方法是多个类之间的桥梁,请为包含类指定一个有意义的名称,说明其用途。
在极少数情况下,使用 Helper
后缀可能是适当的:
- 用于组合默认行为
- 可能涉及将现有行为委托给新类
- 可能需要持久化状态
- 通常涉及
View
例如,如果向后移植提示需要保留与 View
关联的状态,并在 View
上调用多个方法来安装向后移植的功能,则 TooltipHelper
是一个可接受的类名称。
请勿直接将 IDL 生成的代码公开为公共 API
将 IDL 生成的代码保留为实现详情。这包括 protobuf、套接字、FlatBuffers 或任何其他非 Java 和非 NDK API 接口。不过,Android 中的大多数 IDL 都采用 AIDL 格式,因此本页将重点介绍 AIDL。
生成的 AIDL 类不符合 API 样式指南要求(例如,它们无法使用重载),并且 AIDL 工具并未明确设计为保持语言 API 兼容性,因此您无法将其嵌入到公共 API 中。
而是在 AIDL 接口上添加一个公共 API 层,即使它最初是浅层封装容器也是如此。
Binder 接口
如果 Binder
接口是实现细节,则日后可以随意更改,而公共层可确保保持所需的向后兼容性。例如,您可能需要向内部调用添加新参数,或者通过使用批处理或流式传输、使用共享内存等方式优化 IPC 流量。如果您的 AIDL 接口也是公共 API,则无法执行上述任何操作。
例如,请勿直接将 FooService
公开为 API:
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
请改为将 Binder
接口封装在管理器或其他类中:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
如果日后此调用需要新的参数,则可以将内部接口设为最小,并向公共 API 添加方便的重载。随着实现的演变,您可以使用封装层来处理其他向后兼容性问题:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo, int flags);
}
public IFooManager {
public void doFoo(String foo) {
if (mAppTargetSdkLevel < 26) {
useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
} else {
mFooService.doFoo(foo, 0);
}
}
public void doFoo(String foo, int flags) {
mFooService.doFoo(foo, flags);
}
}
对于不属于 Android 平台的 Binder
接口(例如,Google Play 服务为应用导出的服务接口),稳定、已发布且具有版本号的 IPC 接口的要求意味着,接口本身的演变难度会大大增加。不过,仍然值得为其添加封装容器层,以符合其他 API 准则,并在需要时更轻松地将相同的公共 API 用于新版本的 IPC 接口。
请勿在公共 API 中使用原始 Binder 对象
Binder
对象本身没有任何意义,因此不应在公共 API 中使用。一种常见的用例是将 Binder
或 IBinder
用作令牌,因为它们具有身份语义。请改用封装容器令牌类,而不是使用原始 Binder
对象。
public final class IdentifiableObject {
public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
/**
* @hide
*/
public Binder getRawValue() {...}
/**
* @hide
*/
public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}
public final class IdentifiableObject {
public IdentifiableObjectToken getToken() {...}
}
经理类必须为最终类
管理器类应声明为 final
。管理器类与系统服务进行通信,是唯一的互动点。无需进行自定义,因此请将其声明为 final
。
请勿使用 CompletableFuture 或 Future
java.util.concurrent.CompletableFuture
具有庞大的 API Surface,允许对 Future 的值进行任意更改,并且具有容易出错的默认值。
相反,java.util.concurrent.Future
缺少非阻塞监听,因此难以与异步代码搭配使用。
在平台代码和 Kotlin 和 Java 都使用的低级库 API 中,最好结合使用完成回调 Executor
和 CancellationSignal
(如果 API 支持取消)。
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
如果您以 Kotlin 为目标平台,请优先使用 suspend
函数。
suspend fun asyncLoadFoo(): Foo
在特定于 Java 的集成库中,您可以使用 Guava 的 ListenableFuture
。
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
请勿使用“可选”
虽然 Optional
在某些 API Surface 中可能具有优势,但与现有的 Android API Surface 区域不一致。@Nullable
和 @NonNull
可为 null
安全性提供工具协助,Kotlin 会在编译器级别强制执行可为 null 性协定,因此无需使用 Optional
。
对于可选基元,请使用配对的 has
和 get
方法。如果未设置该值(has
返回 false
),get
方法应抛出 IllegalStateException
。
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
为不可实例化的类使用私有构造函数
只能由 Builder
创建的类、仅包含常量或静态方法的类,或其他不可实例化的类都应包含至少一个私有构造函数,以防止使用默认的无参数构造函数进行实例化。
public final class Log {
// Not instantiable.
private Log() {}
}
单例
我们不建议使用单例,因为它们存在以下与测试相关的缺点:
- 构建由类管理,从而防止使用虚构对象
- 由于单例的静态性质,测试无法做到完全隔离
- 如需解决这些问题,开发者必须知道单例的内部详细信息,或者为其创建封装容器
建议使用单个实例模式,该模式依赖于抽象基类来解决这些问题。
单个实例
单实例类使用带有 private
或 internal
构造函数的抽象基类,并提供静态 getInstance()
方法来获取实例。getInstance()
方法必须在后续调用中返回相同的对象。
getInstance()
返回的对象应为抽象基类的私有实现。
class Singleton private constructor(...) {
companion object {
private val _instance: Singleton by lazy { Singleton(...) }
fun getInstance(): Singleton {
return _instance
}
}
}
abstract class SingleInstance private constructor(...) {
companion object {
private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
fun getInstance(): SingleInstance {
return _instance
}
}
}
单个实例与单例不同,因为开发者可以创建 SingleInstance
的虚构版本,并使用自己的依赖项注入框架来管理实现,而无需创建封装容器,或者库可以在 -testing
工件中提供自己的虚构对象。
释放资源的类应实现 AutoCloseable
通过 close
、release
、destroy
或类似方法释放资源的类应实现 java.lang.AutoCloseable
,以允许开发者在使用 try-with-resources
代码块时自动清理这些资源。
避免在 android.* 中引入新的 View 子类。
请勿在平台公共 API(即 android.*
)中引入直接或间接从 android.view.View
继承的新类。
Android 的界面工具包现在采用 Compose 优先的设计。平台公开的新界面功能应公开为较低级别的 API,这些 API 可用于在 Jetpack 库中为开发者实现 Jetpack Compose 和基于 View 的界面组件(可选)。在库中提供这些组件,可在平台功能不可用时提供向后移植的实现。
字段
这些规则与类的公共字段有关。
请勿公开原始字段
Java 类不应直接公开字段。字段应设为私有,并且只能使用公共 getter 和 setter 访问,无论这些字段是否为最终字段。
少数例外情况包括基本数据结构,其中无需增强指定或检索字段的行为。在这种情况下,应使用标准变量命名惯例来命名字段,例如 Point.x
和 Point.y
。
Kotlin 类可以公开属性。
公开的字段应标记为 final
强烈建议不要使用原始字段(@see 请勿公开原始字段)。但在极少数情况下,如果某个字段被公开为公共字段,请将该字段标记为 final
。
不应公开内部字段
请勿在公共 API 中引用内部字段名称。
public int mFlags;
使用 public 而非 protected
常量
以下是关于公共常量的规则。
标志常量不应与整数或长整数值重叠
标志是指可以组合成某个联合值的位。如果不是这种情况,请勿调用变量或常量 flag
。
public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;
如需详细了解如何定义公共标志常量,请参阅@IntDef
中的位掩码标志。
static final 常量应使用全大写、下划线分隔的命名惯例
常量中的所有字词都应大写,多个字词应以 _
分隔。例如:
public static final int fooThing = 5
public static final int FOO_THING = 5
为常量使用标准前缀
Android 中使用的许多常量都适用于标准内容,例如标志、按键和操作。这些常量应带有标准前缀,以便更容易识别它们。
例如,intent extra 应以 EXTRA_
开头。intent 操作应以 ACTION_
开头。与 Context.bindService()
搭配使用的常量应以 BIND_
开头。
键常量名称和作用域
字符串常量值应与常量名称本身保持一致,并且通常应限定在软件包或网域中。例如:
public static final String FOO_THING = "foo"
命名不一致或作用域不当。您应考虑:
public static final String FOO_THING = "android.fooservice.FOO_THING"
作用域限定字符串常量中的 android
前缀预留给 Android 开源项目。
intent 操作和 extra 以及 Bundle 条目应使用其定义所在的软件包名称进行命名空间划分。
package android.foo.bar {
public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}
使用 public 而非 protected
使用一致的前缀
相关常量都应以相同的前缀开头。例如,对于要与标志值搭配使用的一组常量:
public static final int SOME_VALUE = 0x01;
public static final int SOME_OTHER_VALUE = 0x10;
public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;
public static final int FLAG_SOME_OTHER_VALUE = 0x10;
public static final int FLAG_SOME_THIRD_VALUE = 0x100;
@see 为常量使用标准前缀
使用一致的资源名称
公共标识符、属性和值必须使用驼峰命名惯例进行命名,例如 @id/accessibilityActionPageUp
或 @attr/textAppearance
,类似于 Java 中的公共字段。
在某些情况下,公开标识符或属性包含以下划线分隔的通用前缀:
- config.xml 中的平台配置值,例如
@string/config_recentsComponentName
- 特定于布局的视图属性,例如 attrs.xml 中的
@attr/layout_marginStart
公共主题和样式必须遵循分层 PascalCase 命名惯例,例如 @style/Theme.Material.Light.DarkActionBar
或 @style/Widget.Material.SearchView.ActionBar
,类似于 Java 中的嵌套类。
布局和可绘制资源不应作为公共 API 公开。不过,如果必须公开这些资源,则必须使用下划线命名惯例(例如 layout/simple_list_item_1.xml
或 drawable/title_bar_tall.xml
)为公共布局和可绘制对象命名。
如果常量可能会发生变化,请将其设为动态常量
编译器可能会内嵌常量值,因此保持值不变被视为 API 协定中的一部分。如果 MIN_FOO
或 MAX_FOO
常量的值将来可能会发生变化,不妨考虑改为将它们设为动态方法。
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
考虑回调的向前兼容性
以旧版 API 为目标平台的应用无法识别在未来 API 版本中定义的常量。因此,向应用传送的常量应考虑应用的目标 API 版本,并将较新的常量映射到一致的值。请考虑以下场景:
假设的 SDK 来源:
// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;
包含 targetSdkVersion="22"
的假设应用:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
在本例中,应用是在 API 级别 22 的限制下设计的,并做出了一个(在某种程度上)合理的假设,即只有两种可能的状态。不过,如果应用收到新添加的 STATUS_FAILURE_RETRY
,则会将其解读为成功。
返回常量的方法可以通过将其输出限制为与应用的目标 API 级别相匹配,从而安全地处理此类情况:
private int mapResultForTargetSdk(Context context, int result) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
if (targetSdkVersion < 26) {
if (result == STATUS_FAILURE_ABORT) {
return STATUS_FAILURE;
}
if (targetSdkVersion < 23) {
if (result == STATUS_FAILURE_RETRY) {
return STATUS_FAILURE;
}
}
}
return result;
}
开发者无法预测常量列表将来是否会发生变化。如果您使用看起来像万能字符串的 UNKNOWN
或 UNSPECIFIED
常量定义 API,开发者会假定在编写应用时发布的常量是详尽无遗的。如果您不愿意设置此预期,请重新考虑是否应为您的 API 使用万能常量。
此外,库无法单独于应用指定自己的 targetSdkVersion
,并且通过库代码处理 targetSdkVersion
行为更改非常复杂且容易出错。
整数或字符串常量
如果值的命名空间无法在软件包之外扩展,请使用整数常量和 @IntDef
。如果命名空间是共享的,或者可以由软件包之外的代码扩展,请使用字符串常量。
数据类
数据类代表一组不可变的属性,并提供一组小且定义明确的实用程序函数来与这些数据进行交互。
请勿在公共 Kotlin API 中使用 data class
,因为 Kotlin 编译器无法保证所生成代码的语言 API 或二进制兼容性。请改为手动实现所需的函数。
实例化
在 Java 中,如果属性较少,数据类应提供构造函数;如果属性较多,则应使用 Builder
模式。
在 Kotlin 中,无论属性数量如何,数据类都应提供带有默认参数的构造函数。在以 Java 客户端为目标平台时,提供构建器对在 Kotlin 中定义的数据类也可能有益。
修改和复制
如果需要修改数据,请提供具有复制构造函数的 Builder
类(Java),或返回新对象的 copy()
成员函数(Kotlin)。
在 Kotlin 中提供 copy()
函数时,参数必须与类的构造函数相匹配,并且必须使用对象的当前值填充默认值:
class Typography(
val labelMedium: TextStyle = TypographyTokens.LabelMedium,
val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
fun copy(
labelMedium: TextStyle = this.labelMedium,
labelSmall: TextStyle = this.labelSmall
): Typography = Typography(
labelMedium = labelMedium,
labelSmall = labelSmall
)
}
其他行为
数据类应同时实现 equals()
和 hashCode()
,并且必须在这些方法的实现中考虑每个属性。
数据类可以使用与 Kotlin 的数据类实现匹配的推荐格式(例如 User(var1=Alex, var2=42)
)来实现 toString()
。
方法
这些规则涉及方法中的各种具体事项,包括参数、方法名称、返回值类型和访问权限说明符。
时间
这些规则涵盖了如何在 API 中表达日期和时长等时间概念。
尽可能使用 java.time.* 类型
java.time.Duration
、java.time.Instant
和许多其他 java.time.*
类型可通过脱糖在所有平台版本上使用,在 API 参数或返回值中表示时间时,应优先使用这些类型。
最好仅公开接受或返回 java.time.Duration
或 java.time.Instant
的 API 变体,并忽略具有相同用例的原始变体,除非 API 网域是预期使用模式中的对象分配会产生不可接受的性能影响。
用于表示时长的方法应命名为 duration
如果时间值表示所涉及的时间长度,请将参数命名为“时长”,而不是“时间”。
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
例外情况:
如果时长专门适用于超时值,则适合使用“timeout”。
当引用特定时间点(而非时长)时,类型为 java.time.Instant
的“time”是合适的。
将时长或时间表示为基元的方法应使用其时间单位进行命名,并使用 long
接受或以基元形式返回时长的方法应在方法名称后附上关联的时间单位(例如 Millis
、Nanos
、Seconds
),以保留未修饰的名称以便与 java.time.Duration
搭配使用。请参阅时间。
还应为方法添加相应的单位和时间基准注解:
@CurrentTimeMillisLong
:值是一个非负时间戳,以自 1970-01-01T00:00:00Z 以来的毫秒数为衡量标准。@CurrentTimeSecondsLong
:值是一个非负时间戳,以自 1970-01-01T00:00:00Z 以来的秒数表示。@DurationMillisLong
:值为正时长(以毫秒为单位)。@ElapsedRealtimeLong
:值为SystemClock.elapsedRealtime()
时间基准中的非负时间戳。@UptimeMillisLong
:值为SystemClock.uptimeMillis()
时间基准中的非负时间戳。
基元时间参数或返回值应使用 long
,而非 int
。
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
用于表示时间单位的方法应优先使用非缩写的单位名称简写形式
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
为长时间参数添加注解
该平台包含多个注解,用于为 long
类型的时间单位提供更严格的类型化:
@CurrentTimeMillisLong
:值是一个非负时间戳,以自1970-01-01T00:00:00Z
以来的毫秒数表示,因此采用System.currentTimeMillis()
时间基准。@CurrentTimeSecondsLong
:值是一个非负时间戳,以从1970-01-01T00:00:00Z
开始算起的秒数表示。@DurationMillisLong
:值为正时长(以毫秒为单位)。@ElapsedRealtimeLong
:值为SystemClock#elapsedRealtime()
时间基准中的非负时间戳。@UptimeMillisLong
:值为SystemClock#uptimeMillis()
时间基准中的非负时间戳。
度量单位
对于表示时间以外的计量单位的所有方法,请优先使用驼峰式命名的 SI 单位前缀。
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
将可选参数放在重载的末尾
如果您有包含可选参数的方法重载,请将这些参数放在最后,并与其他参数保持一致的顺序:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
为可选参数添加重载时,更简单的方法的行为应与向更复杂的方法提供默认参数时完全相同。
推论:除非添加可选参数或接受不同类型的参数(如果方法是多态的),否则请勿重载方法。如果重载方法执行的操作完全不同,请为其指定一个新名称。
包含默认参数的方法必须带有 @JvmOverloads 注解(仅限 Kotlin)
具有默认参数的方法和构造函数必须带有 @JvmOverloads
注解,以保持二进制兼容性。
如需了解详情,请参阅 Kotlin-Java 官方互操作指南中的默认值的函数重载。
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
请勿移除默认参数值(仅限 Kotlin)
如果某个方法在发布时附带了具有默认值的参数,则移除默认值会导致源代码中断更改。
最具特征和标识性的方法参数应放在前面
如果方法有多个参数,请将最相关的参数放在前面。指定标志和其他选项的参数不如描述要执行操作的对象的参数重要。如果有完成回调,请将其放在最后。
public void openFile(int flags, String name);
public void openFileAsync(OnFileOpenedListener listener, String name, int flags);
public void setFlags(int mask, int flags);
public void openFile(String name, int flags);
public void openFileAsync(String name, int flags, OnFileOpenedListener listener);
public void setFlags(int flags, int mask);
另请参阅:在重载中将可选参数放在最后
构建器
建议使用构建器模式创建复杂的 Java 对象,并且在 Android 中,通常在以下情况下使用此模式:
- 生成的对象的属性应为不可变
- 存在大量必需的属性,例如许多构造函数参数
- 在构建时,属性之间存在复杂的关系,例如需要执行验证步骤。请注意,这种复杂程度通常表明 API 的可用性存在问题。
考虑是否需要构建器。如果构建器用于执行以下操作,则在 API 接口中非常有用:
- 仅配置一组可能包含大量可选创建参数中的几个参数
- 配置许多不同的可选或必需的创建参数,这些参数有时是类似或匹配的类型,否则调用站点在读取时可能会令人困惑,在写入时可能会出错
- 增量配置对象的创建,其中几段不同的配置代码可能会各自对构建器进行调用,作为实现细节
- 在未来的 API 版本中添加其他可选的创建参数,以允许类型不断发展
如果您的类型只有 3 个或更少的必需参数且没有可选参数,您几乎可以随时跳过构建器并使用普通构造函数。
Kotlin 源类应优先使用带有默认参数的 @JvmOverloads
注解的构造函数,而不是构建器,但在前面所述的情况下,也可以选择同时提供构建器,以提高 Java 客户端的可用性。
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
构建器类必须返回构建器
构建器类必须通过从 build()
以外的每个方法返回构建器对象(例如 this
)来启用方法链接。应将其他构建的对象作为参数传递 - 请勿返回其他对象的构建器。例如:
public static class Builder {
public void setDuration(long);
public void setFrequency(int);
public DtmfConfigBuilder addDtmfConfig();
public Tone build();
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
在极少数情况下,基构建器类必须支持扩展,请使用泛型返回类型:
public abstract class Builder<T extends Builder<T>> {
abstract T setValue(int);
}
public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
T setValue(int);
T setTypeSpecificValue(long);
}
必须通过构造函数创建构建器类
为了通过 Android API Surface 保持一致的构建器创建方式,所有构建器都必须通过构造函数创建,而不是通过静态创建方方法创建。对于基于 Kotlin 的 API,Builder
必须是公共的,即使 Kotlin 用户预计会通过工厂方法/DSL 风格的创建机制隐式依赖于构建器也是如此。库不得使用 @PublishedApi internal
从 Kotlin 客户端选择性地隐藏 Builder
类构造函数。
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
构建器构造函数的所有实参都必须是必需的(例如 @NonNull)
可选参数(例如 @Nullable
)应移至 setter 方法。如果未指定任何必需参数,构建器构造函数应抛出 NullPointerException
(考虑使用 Objects.requireNonNull
)。
构建器类应是其构建类型的最终静态内部类
为了在软件包中进行逻辑整理,构建器类通常应公开为其构建类型的最终内部类,例如 Tone.Builder
而非 ToneBuilder
。
构建器可以包含构造函数,以便根据现有实例创建新实例
构建器可以包含复制构造函数,以便从现有构建器或构建的对象创建新的构建器实例。它们不应提供从现有构建器或构建对象创建构建器实例的替代方法。
public class Tone {
public static class Builder {
public Builder clone();
}
public Builder toBuilder();
}
public class Tone {
public static class Builder {
public Builder(Builder original);
public Builder(Tone original);
}
}
如果构建器具有复制构造函数,则构建器 setter 应采用 @Nullable 参数
如果可以从现有实例创建构建器的新实例,则必须进行重置。如果没有可用的复制构造函数,则构建器可以包含 @Nullable
或 @NonNullable
参数。
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
构建器 setter 可以接受可选属性的 @Nullable 参数
通常,使用可为 null 的值对二阶输入进行处理更简单,尤其是在 Kotlin 中,它使用默认参数,而不是构建器和重载。
此外,@Nullable
设置器会将它们与其 getter 进行匹配,对于可选属性,getter 必须为 @Nullable
。
Value createValue(@Nullable OptionalValue optionalValue) {
Value.Builder builder = new Value.Builder();
if (optionalValue != null) {
builder.setOptionalValue(optionalValue);
}
return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
return new Value.Builder()
.setOptionalValue(optionalValue);
.build();
}
// Or in other cases:
Value createValue() {
return new Value.Builder()
.setOptionalValue(condition ? new OptionalValue() : null);
.build();
}
Kotlin 中的常见用法:
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
默认值(如果未调用 setter)和 null
的含义必须在 setter 和 getter 中都进行适当的记录。
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
可以为可变属性提供构建器 setter,前提是构建的类上有 setter
如果您的类具有可变属性且需要 Builder
类,请先自问您的类是否实际上应具有可变属性。
接下来,如果您确定需要可变属性,请确定以下哪种场景更适合您的预期用例:
构建的对象应立即可用,因此应为所有相关属性(无论是可变还是不可变属性)提供 setter。
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
在构建的对象可供使用之前,可能需要进行一些额外的调用,因此不应为可变属性提供 setter。
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
请勿混用这两种情况。
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
构建器不应包含 getter
Getter 应位于构建的对象上,而不是构建器上。
构建器 setter 必须在构建的类上具有相应的 getter
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
public long getDuration();
public int getFrequency();
public @NonNull List<DtmfConfig> getDtmfConfigs();
}
构建器方法命名
构建器方法名称应采用 setFoo()
、addFoo()
或 clearFoo()
样式。
Builder 类应声明 build() 方法
Builder 类应声明一个返回构建对象实例的 build()
方法。
构建器 build() 方法必须返回 @NonNull 对象
构建器的 build()
方法应返回构建的对象的非 null 实例。如果由于参数无效而无法创建对象,则可以将验证推迟到 build 方法,并应抛出 IllegalStateException
。
请勿公开内部锁
公共 API 中的方法不应使用 synchronized
关键字。此关键字会导致您的对象或类被用作锁,并且由于它会公开给其他代码,因此如果类外的其他代码开始出于锁定目的使用它,您可能会遇到意外的副作用。
请改为对内部私有对象执行任何必要的锁定操作。
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
访问器样式的方法应遵循 Kotlin 属性准则
从 Kotlin 源代码查看时,访问器样式的方法(即使用 get
、set
或 is
前缀的方法)也将作为 Kotlin 属性提供。例如,在 Java 中定义的 int getField()
在 Kotlin 中可用作属性 val field: Int
。
因此,为了通常满足开发者对访问器方法行为的预期,使用访问器方法前缀的方法应与 Java 字段的行为类似。在以下情况下,请避免使用访问器样式前缀:
- 该方法有副作用,请使用更具描述性的方法名称
- 该方法涉及计算密集型工作,最好使用
compute
- 该方法涉及阻塞或其他长时间运行的工作来返回值,例如 IPC 或其他 I/O;首选
fetch
- 该方法会阻塞线程,直到它可以返回值 - 首选
await
- 该方法会在每次调用时返回一个新的对象实例 - 首选
create
- 该方法可能无法成功返回值,建议使用
request
请注意,一次执行计算密集型工作并将值缓存以供后续调用,仍会计为执行计算密集型工作。卡顿不会跨帧平均分配。
为布尔值访问器方法使用 is 前缀
这是 Java 中布尔值方法和字段的标准命名惯例。通常,布尔值方法和变量名称应编写为可由返回值回答的问题。
Java 布尔值访问器方法应遵循 set
/is
命名方案,字段应首选 is
,如以下示例所示:
// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();
// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();
final boolean isAvailable;
为 Java 访问器方法使用 set
/is
,或为 Java 字段使用 is
,即可将它们用作 Kotlin 中的属性:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
属性和访问器方法通常应使用正面命名法,例如 Enabled
而非 Disabled
。使用否定术语会颠倒 true
和 false
的含义,使行为推理变得更加困难。
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
如果布尔值描述的是某个属性的包含或所有权,您可以使用 has 而非 is;不过,这不适用于 Kotlin 属性语法:
// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();
一些可能更合适的替代前缀包括 can 和 should:
// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();
// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();
用于切换行为或功能的方法可以使用 is 前缀和 Enabled 后缀:
// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()
同样,用于指明对其他行为或功能的依赖项的方法可以使用 is 前缀和 Supported 或 Required 后缀:
// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()
通常,方法名称应编写为可通过返回值回答的问题。
Kotlin 属性方法
对于类属性 var foo: Foo
,Kotlin 将使用一致的规则生成 get
/set
方法:为 getter 添加 get
并将第一个字符转换为大写,为 setter 添加 set
并将第一个字符转换为大写。属性声明将分别生成名为 public Foo getFoo()
和 public void setFoo(Foo foo)
的方法。
如果属性的类型为 Boolean
,则在生成名称时会应用额外规则:如果属性名称以 is
开头,则不会在 getter 方法名称前面添加 get
,而是将属性名称本身用作 getter。因此,最好使用 is
前缀命名 Boolean
属性,以遵循命名准则:
var isVisible: Boolean
如果您的媒体资源属于上述例外情况之一,并且以适当的前缀开头,请在媒体资源上使用 @get:JvmName
注解手动指定适当的名称:
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
位掩码访问器
如需了解有关定义位掩码标志的 API 指南,请参阅对位掩码标志使用 @IntDef
。
setter
应提供两个 setter 方法:一个接受完整的位字符串并覆盖所有现有标志,另一个接受自定义位掩码以实现更大的灵活性。
/**
* Sets the state of all scroll indicators.
* <p>
* See {@link #setScrollIndicators(int, int)} for usage information.
*
* @param indicators a bitmask of indicators that should be enabled, or
* {@code 0} to disable all indicators
* @see #setScrollIndicators(int, int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators);
/**
* Sets the state of the scroll indicators specified by the mask. To change
* all scroll indicators at once, see {@link #setScrollIndicators(int)}.
* <p>
* When a scroll indicator is enabled, it will be displayed if the view
* can scroll in the direction of the indicator.
* <p>
* Multiple indicator types may be enabled or disabled by passing the
* logical OR of the specified types. If multiple types are specified, they
* will all be set to the same enabled state.
* <p>
* For example, to enable the top scroll indicator:
* {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
* <p>
* To disable the top scroll indicator:
* {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
*
* @param indicators a bitmask of values to set; may be a single flag,
* the logical OR of multiple flags, or 0 to clear
* @param mask a bitmask indicating which indicator flags to modify
* @see #setScrollIndicators(int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);
Getter
应提供一个 getter 来获取完整的位掩码。
/**
* Returns a bitmask representing the enabled scroll indicators.
* <p>
* For example, if the top and left scroll indicators are enabled and all
* other indicators are disabled, the return value will be
* {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
* <p>
* To check whether the bottom scroll indicator is enabled, use the value
* of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
*
* @return a bitmask representing the enabled scroll indicators
*/
@ScrollIndicators
public int getScrollIndicators();
使用 public 而非 protected
在公共 API 中,始终优先使用 public
而非 protected
。从长远来看,受保护的访问最终会带来诸多问题,因为在默认情况下外部访问也能达到相同效果的情况下,实现者必须替换以提供公共访问器。
请注意,protected
可见性不会阻止开发者调用 API,只会让调用变得稍微麻烦一些。
未实现 equals() 和 hashCode() 或同时未实现这两者
如果您替换其中一个,则必须替换另一个。
为数据类实现 toString()
建议数据类替换 toString()
,以帮助开发者调试代码。
记录输出是用于程序行为还是调试
确定是否希望程序行为依赖于您的实现。例如,UUID.toString() 和 File.toString() 会记录其特定格式以供程序使用。如果您仅出于调试目的公开信息(例如 Intent),则可以从父类隐式继承文档。
请勿添加额外信息
toString()
提供的所有信息也应可通过该对象的公共 API 获取。否则,您将鼓励开发者解析并依赖您的 toString()
输出,这将阻止日后进行更改。良好的做法是仅使用对象的公共 API 实现 toString()
。
不鼓励依赖于调试输出
虽然无法阻止开发者依赖于调试输出,但在 toString()
输出中添加对象的 System.identityHashCode
会大大降低两个不同对象具有相同 toString()
输出的可能性。
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
这可以有效地阻止开发者对对象编写 assertThat(a.toString()).isEqualTo(b.toString())
等测试断言。
在返回新创建的对象时使用 createFoo
对于将创建返回值的方法(例如通过构造新对象),请使用前缀 create
(而非 get
或 new
)。
如果方法将创建要返回的对象,请在方法名称中明确说明这一点。
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
接受 File 对象的方法也应接受流
Android 上的数据存储位置并不总是磁盘上的文件。例如,跨用户边界传递的内容表示为 content://
Uri
。为了能够处理各种数据源,接受 File
对象的 API 也应接受 InputStream
和/或 OutputStream
。
public void setDataSource(File file)
public void setDataSource(InputStream stream)
接受和返回原始基元,而不是封装版本
如果您需要传达缺失或 null 值,请考虑使用 -1
、Integer.MAX_VALUE
或 Integer.MIN_VALUE
。
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
避免使用与原始类型等效的类,可以避免这些类的内存开销、方法对值的访问,以及更重要的是,避免因在原始类型和对象类型之间进行类型转换而导致的自动装箱。避免这些行为有助于节省内存和减少临时分配,这可能会导致频繁且昂贵的垃圾回收。
使用注解阐明有效的参数和返回值
添加了开发者注释,以帮助阐明各种情况下允许的值。这样,当开发者提供错误的值时(例如,在框架需要一组特定常量值中的某个值时传递任意 int
),工具便能更轻松地为开发者提供帮助。请根据需要使用以下任一注解:
是否可为 null
Java API 需要显式可为 null 性注解,但可为 null 性概念是 Kotlin 语言的一部分,因此 Kotlin API 中绝不应使用可为 null 性注解。
@Nullable
:表示给定的返回值、参数或字段可以为 null:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
:表示给定的返回值、参数或字段不能为 null。将内容标记为 @Nullable
在 Android 中相对较新,因此 Android 的大多数 API 方法都没有一致的文档。因此,我们有三种状态:“未知、@Nullable
、@NonNull
”,这就是 @NonNull
属于 API 准则的原因:
@NonNull
public String getName()
public void setName(@NonNull String name)
对于 Android 平台文档,为方法参数添加注解会自动生成采用“此值可能为 null”格式的文档,除非参数文档中的其他位置明确使用了“null”。
现有的“实际上不为 null”方法:如果 API 中不带声明的 @Nullable
注解的现有方法可以在特定的明显情况下(例如 findViewById()
)返回 null
,则可以添加 @Nullable
注解。对于不想进行 null 检查的开发者,应添加会抛出 IllegalArgumentException
的伴生 @NotNull requireFoo()
方法。
接口方法:新 API 在实现接口方法(例如 Parcelable.writeToParcel()
)时应添加适当的注解(即实现类中的该方法应为 writeToParcel(@NonNull Parcel,
int)
,而非 writeToParcel(Parcel, int)
);不过,缺少注解的现有 API 无需“修复”。
null 性强制执行
在 Java 中,建议使用 Objects.requireNonNull()
对 @NonNull
参数执行输入验证,并在参数为 null 时抛出 NullPointerException
。这在 Kotlin 中会自动执行。
资源
资源标识符:用于表示特定资源的 ID 的整数参数应带有适当的资源类型定义注解。除了万能注解 @AnyRes
之外,每种类型的资源(例如 @StringRes
、@ColorRes
和 @AnimRes
)都有对应的注解。例如:
public void setTitle(@StringRes int resId)
适用于常量集的 @IntDef
魔法常量:String
和 int
参数旨在接收公共常量表示的一组有限可能值中的某个值,应使用 @StringDef
或 @IntDef
进行适当注解。借助这些注解,您可以创建一个可用作允许参数的 typedef 的新注解。例如:
/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
NAVIGATION_MODE_STANDARD,
NAVIGATION_MODE_LIST,
NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);
建议使用方法检查带注解的参数的有效性,如果参数不是 @IntDef
的一部分,则抛出 IllegalArgumentException
适用于位掩码标志的 @IntDef
注解还可以指定常量是标志,并且可以与 & 和 I 组合使用:
/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_USE_LOGO,
FLAG_SHOW_HOME,
FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}
适用于字符串常量集的 @StringDef
还有 @StringDef
注解,它与上一部分中的 @IntDef
完全相同,但适用于 String
常量。您可以添加多个“前缀”值,这些值用于自动为所有值生成文档。
@SdkConstant(适用于 SDK 常量)
@SdkConstant 为公共字段添加注解,如果它们是以下 SdkConstant
值之一:ACTIVITY_INTENT_ACTION
、BROADCAST_INTENT_ACTION
、SERVICE_ACTION
、INTENT_CATEGORY
、FEATURE
。
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CALL = "android.intent.action.CALL";
为替换项提供兼容的可为 null 性
为了实现 API 兼容性,替换项的可为 null 性应与父项的当前可为 null 性兼容。下表显示了兼容性预期。显然,替换项的限制条件只能与被替换的元素相同或更严格。
类型 | 家长 | 儿童 |
---|---|---|
返回值类型 | 未加注解 | 未加注解或非 null |
返回值类型 | 是否可为 null? | 可为 null 或非 null |
返回值类型 | Nonnull | Nonnull |
趣味参数 | 未加注解 | 未加注解或可为 null |
趣味参数 | 是否可为 null? | 是否可为 null? |
趣味参数 | Nonnull | 可为 null 或非 null |
尽可能使用非 null 参数(例如 @NonNull)
过载方法时,最好让所有参数均不为 null。
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
此规则也适用于重载的属性 setter。主要参数应为非 null,并且清除该属性应作为单独的方法实现。这可以防止出现“无意义”调用,在这种调用中,开发者必须设置尾随参数,即使这些参数不是必需的也是如此。
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)
// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()
为容器优先使用非 null 类型(例如 @NonNull)的返回值
对于 Bundle
或 Collection
等容器类型,请返回一个空容器(如果适用,则为不可变容器)。如果要使用 null
来区分容器的可用性,请考虑提供单独的布尔值方法。
@NonNull
public Bundle getExtras() { ... }
get 和 set 对的 null 可为性注解必须一致
单个逻辑属性的 get 和 set 方法对的 null 可检查注解应始终保持一致。如果不遵循此准则,将破坏 Kotlin 的属性语法,因此,向现有属性方法添加不一致的可为 null 注解对 Kotlin 用户来说会破坏源代码。
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
在失败或错误情况下返回值
所有 API 都应允许应用对错误做出响应。返回 false
、-1
、null
或其他“出错”的通用值并不能让开发者充分了解其应用未能设置用户预期或无法准确跟踪其在现场的可靠性。在设计 API 时,请假设您正在构建应用。如果您遇到错误,API 是否会为您提供足够的信息来向用户显示该错误或做出适当的响应?
- 在异常消息中添加详细信息是可以的(并且我们鼓励这样做),但开发者不应必须解析该信息才能妥善处理错误。详细错误代码或其他信息应以方法的形式公开。
- 确保您选择的错误处理选项可让您在将来灵活地引入新错误类型。对于
@IntDef
,这意味着要包含OTHER
或UNKNOWN
值。在返回新代码时,您可以检查调用方的targetSdkVersion
,以避免返回应用不了解的错误代码。对于异常,请为异常实现一个通用的超类,以便处理该类型的任何代码也会捕获和处理子类型。 - 开发者不应轻易或根本无法无意中忽略错误。如果您通过返回值传达错误,请使用
@CheckResult
为方法添加注解。
如果由于开发者做错了某些事情(例如忽略输入参数的约束条件或未能检查可观察状态)而导致达到失败或错误条件,请优先抛出 ? extends RuntimeException
。
如果操作可能会因异步更新的状态或开发者无法控制的条件而失败,则设置器或操作(例如 perform
)方法可能会返回一个整数状态代码。
状态代码应在包含类中定义为 public static final
字段,前缀为 ERROR_
,并在 @hide
@IntDef
注解中枚举。
方法名称应始终以动词开头,而非以正文开头
方法的名称应始终以动词(例如 get
、create
、reload
等)开头,而不是以您要操作的对象开头。
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
首选将集合 类型用作返回或参数类型,而不是数组
与数组相比,泛型集合接口具有多项优势,包括围绕唯一性和排序更强大的 API 协定、对泛型的支持,以及一些对开发者友好的便捷方法。
基元类型的例外情况
如果元素是基元,请改用数组,以避免自动装箱的开销。请参阅接受和返回原始基元,而不是封装版本
对性能敏感的代码的例外情况
在某些情况下,如果 API 在对性能敏感的代码(例如图形或其他测量/布局/绘制 API)中使用,则可以使用数组而非集合,以减少分配和内存抖动。
Kotlin 的例外情况
Kotlin 数组是不可变的,Kotlin 语言针对数组提供了充足的实用程序 API,因此对于打算从 Kotlin 访问的 Kotlin API,数组与 List
和 Collection
相当。
优先使用 @NonNull 集合
始终优先使用 @NonNull
来创建集合对象。返回空集合时,请使用适当的 Collections.empty
方法返回低成本、类型正确且不可变的集合对象。
在支持类型注解的情况下,请始终为集合元素使用 @NonNull
。
此外,在使用数组(而非集合)时,也应优先使用 @NonNull
(请参阅上一个条目)。如果对象分配存在问题,请创建一个常量并将其传递给其他代码,毕竟空数组是不可变的。示例:
private static final int[] EMPTY_USER_IDS = new int[0];
@NonNull
public int[] getUserIds() {
int [] userIds = mService.getUserIds();
return userIds != null ? userIds : EMPTY_USER_IDS;
}
集合可变性
默认情况下,Kotlin API 应为集合首选只读(而非 Mutable
)返回类型,除非 API 协定明确要求可变返回类型。
不过,Java API 应默认首选可变返回类型,因为 Java API 的 Android 平台实现尚未提供便捷的不可变集合实现。Collections.empty
返回类型是此规则的例外,它们不可变。如果客户端可能会有意或无意地利用可变性破坏 API 的预期使用模式,Java API 应强烈考虑返回集合的浅层副本。
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
显式可变的返回值类型
返回集合的 API 在返回后最好不要修改返回的集合对象。如果返回的集合必须以某种方式更改或重复使用(例如,可变数据集的适应视图),则必须明确记录内容可以更改的时间,或遵循既定的 API 命名惯例。
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
下文介绍了 Kotlin .asFoo()
惯例,该惯例允许在原始集合发生变化时,.asList()
返回的集合也发生变化。
返回的数据类型对象的可变性
与返回集合的 API 类似,返回数据类型对象的 API 在返回后最好不要修改返回对象的属性。
val tempResult = DataContainer()
fun add(other: DataContainer): DataContainer {
tempResult.innerValue = innerValue + other.innerValue
return tempResult
}
fun add(other: DataContainer): DataContainer {
return DataContainer(innerValue + other.innerValue)
}
在极少数情况下,一些对性能敏感的代码可能会受益于对象池化或重复使用。请勿编写自己的对象池数据结构,也请勿在公共 API 中公开重复使用的对象。无论是哪种情况,请务必小心管理并发访问。
使用 vararg 参数类型
如果开发者可能会在调用点创建数组,仅出于传递同一类型的多个相关参数的目的,我们建议 Kotlin 和 Java API 都使用 vararg
。
public void setFeatures(Feature[] features) { ... }
// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }
// Developer code
setFeatures(Features.A, Features.B, Features.C);
防御性复制
vararg
参数的 Java 和 Kotlin 实现都会编译为相同的数组支持的字节码,因此可以从包含可变数组的 Java 代码调用。对于将数组参数持久化到字段或匿名内部类的情况,强烈建议 API 设计者创建数组参数的防御性浅层副本。
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
请注意,创建防御性副本无法防止在初始方法调用和创建副本之间发生并发修改,也无法防止对数组中包含的对象进行突变。
使用集合类型参数或返回类型提供正确的语义
List<Foo>
是默认选项,但您可以考虑使用其他类型来提供额外含义:
如果您的 API 对元素的顺序不敏感,并且不允许重复或重复无意义,请使用
Set<Foo>
。Collection<Foo>,
,如果您的 API 对顺序不敏感且允许重复项。
Kotlin 转换函数
Kotlin 经常使用 .toFoo()
和 .asFoo()
从现有对象中获取其他类型的对象,其中 Foo
是转换的返回值类型的名称。这与熟悉的 JDK Object.toString()
保持一致。Kotlin 更进一步,将其用于 25.toFloat()
等基元转换。
名称为 .toFoo()
和 .asFoo()
的转化之间存在明显区别:
创建新的独立对象时,请使用 .toFoo()
与 .toString()
一样,“转换为”会返回一个新的独立对象。如果原始对象稍后被修改,新对象将不会反映这些更改。同样,如果新对象稍后被修改,旧对象不会反映这些更改。
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
在创建依赖封装容器、装饰对象或进行类型转换时,请使用 .asFoo()
在 Kotlin 中,使用 as
关键字执行类型转换。它反映了接口的更改,但并未反映身份的更改。在扩展函数中用作前缀时,.asFoo()
会修饰接收器。原始接收器对象中的更改将反映在 asFoo()
返回的对象中。新 Foo
对象中的更改可能会反映在原始对象中。
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
转换函数应编写为扩展函数
在接收器和结果类定义之外编写转换函数可减少类型之间的耦合。理想的转换只需要对原始对象的公共 API 访问权限。通过此示例,我们证明了开发者也可以编写类似的转换,以便将值转换为自己的首选类型。
抛出适当的特定异常
方法不得抛出 java.lang.Exception
或 java.lang.Throwable
等通用异常,而必须使用 java.lang.NullPointerException
等适当的具体异常,以便开发者处理异常,而不至于过于宽泛。
与直接向公开调用的方法提供的参数无关的错误应抛出 java.lang.IllegalStateException
,而不是 java.lang.IllegalArgumentException
或 java.lang.NullPointerException
。
监听器和回调
以下是与监听器和回调机制所用类和方法相关的规则。
回调类名称应采用单数形式
不过,应使用 MyObjectCallback
代替 MyObjectCallbacks
。
回调方法名称应采用 中的格式
onFooEvent
表示 FooEvent
正在发生,并且回调应做出响应。
过去时与现在时的使用应描述时间行为
与事件相关的回调方法应命名为指明事件是否已发生或正在发生。
例如,如果在执行点击操作后调用该方法:
public void onClicked()
不过,如果该方法负责执行点击操作:
public boolean onClick()
回调注册
当可以向对象添加或从对象中移除监听器或回调时,应将关联的方法命名为“添加”和“移除”或“注册”和“取消注册”。与该类或同一软件包中的其他类使用的现有惯例保持一致。如果不存在此类先例,请优先使用“添加”和“移除”。
涉及注册或取消注册回调的方法应指定回调类型的完整名称。
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
避免使用回调的 getter
请勿添加 getFooCallback()
方法。如果开发者希望将现有回调与自己的替换项链接在一起,这是一个诱人的逃生舱,但它很脆弱,会使组件开发者难以推理当前状态。例如,
- 开发者 A 调用
setFooCallback(a)
- 开发者 B 调用
setFooCallback(new B(getFooCallback()))
- 开发者 A 希望移除其回调
a
,但由于不知道B
的类型,而B
的构建方式不允许对其封装的回调进行此类修改,因此无法移除。
接受 Executor 以控制回调调度
注册没有明确线程预期(几乎在界面工具包之外的任何位置)的回调时,强烈建议在注册过程中添加 Executor
参数,以便开发者指定将在哪个线程中调用回调。
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
作为我们通常关于可选参数的指南的例外情况,我们接受提供省略 Executor
的重载,即使它不是参数列表中的最后一个参数也是如此。如果未提供 Executor
,则应使用 Looper.getMainLooper()
在主线程上调用回调,并且应在关联的过载方法中记录这一点。
/**
* ...
* Note that the callback will be executed on the main thread using
* {@link Looper.getMainLooper()}. To specify the execution thread, use
* {@link registerFooCallback(Executor, FooCallback)}.
* ...
*/
public void registerFooCallback(
@NonNull FooCallback callback)
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Executor
实现注意事项:请注意,以下是有效的执行器!
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
这意味着,在实现采用这种形式的 API 时,应用进程端的传入 binder 对象实现必须先调用 Binder.clearCallingIdentity()
,然后才能在应用提供的 Executor
上调用应用的回调。这样一来,任何使用 binder 身份(例如 Binder.getCallingUid()
)进行权限检查的应用代码都会将运行的代码正确归因于应用,而不是归因于调用应用的系统进程。如果您的 API 用户想要调用方的 UID 或 PID 信息,则这应是 API 接口的显式部分,而不是根据他们提供的 Executor
运行位置而隐式提供。
您的 API 应支持指定 Executor
。在对性能至关重要的情况下,应用可能需要立即运行代码,或者在收到 API 反馈时同步运行代码。接受 Executor
即可执行此操作。从防御角度创建额外的 HandlerThread
或类似于 trampoline 的结构会破坏此理想用例。
如果应用要在自己的进程中某个位置运行耗时代码,请允许。应用开发者为克服您的限制而找到的权宜解决方法,从长远来看将更难以获得支持。
单个回调的例外情况:如果所报告事件的性质要求仅支持单个回调实例,请使用以下样式:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
使用 Executor 而非 Handler
过去,Android 的 Handler
曾用作将回调执行重定向到特定 Looper
线程的标准。由于大多数应用开发者都管理自己的线程池,因此主线程或界面线程是应用可用的唯一 Looper
线程,因此此标准已更改为优先使用 Executor
。使用 Executor
可让开发者获得重复使用现有/首选执行上下文所需的控制。
kotlinx.coroutines 或 RxJava 等新型并发库提供自己的调度机制,可在需要时执行自己的调度,因此请务必提供使用直接执行器(例如 Runnable::run
)的能力,以避免因双线程跳转而产生延迟。例如,使用 Handler
向 Looper
线程发帖的一次跳转,然后是应用并发框架中的另一次跳转。
此准则的例外情况很少见。常见的例外情况申诉包括:
我必须使用 Looper
,因为我需要 Looper
来为事件 epoll
。
由于在这种情况下无法实现 Executor
的好处,因此我们已批准此例外情况请求。
我不希望应用代码阻止发布事件的线程。系统通常不会为在应用进程中运行的代码授予此异常请求。应用若处理不当,只会自食其果,不会影响整体系统运行状况。正确处理或使用常见并发框架的应用不应受到额外的延迟惩罚。
Handler
在本地与同一类中的其他类似 API 一致。
我们会视情况批准此类例外请求。建议添加基于 Executor
的重载,并迁移 Handler
实现以使用新的 Executor
实现。(myHandler::post
是一个有效的 Executor
!)根据类的大小、现有 Handler
方法的数量,以及开发者需要同时使用现有基于 Handler
的方法和新方法的可能性,系统可能会授予添加基于 Handler
的新方法的例外情况。
注册中的对称性
如果有添加或注册某项内容的方法,也应该有移除/取消注册该内容的方法。方法
registerThing(Thing)
应具有匹配的
unregisterThing(Thing)
提供请求标识符
如果开发者可以合理地重复使用回调,请提供标识符对象以将回调与请求相关联。
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
多方法回调对象
添加到之前发布的接口时,多方法回调应优先使用 interface
,并使用 default
方法。之前,由于 Java 7 中缺少 default
方法,因此本指南建议使用 abstract class
。
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
在对非阻塞函数调用进行建模时,使用 android.os.OutcomeReceiver
OutcomeReceiver<R,E>
在成功时报告结果值 R
,否则报告 E : Throwable
,这与普通方法调用可以执行的操作相同。将返回结果或抛出异常的阻塞方法转换为非阻塞异步方法时,请使用 OutcomeReceiver
作为回调类型:
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
以这种方式转换的异步方法始终会返回 void
。而是通过对提供的 executor
进行调用,将 requestFoo
会返回的任何结果报告给 requestFooAsync
的 callback
参数的 OutcomeReceiver.onResult
。而是以相同的方式将 requestFoo
会抛出的任何异常报告给 OutcomeReceiver.onError
方法。
使用 OutcomeReceiver
报告异步方法结果还会为异步方法提供 Kotlin suspend fun
封装容器,使用 androidx.core:core-ktx
中的 Continuation.asOutcomeReceiver
扩展程序:
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
借助此类扩展程序,Kotlin 客户端可以像调用普通函数一样方便地调用非阻塞异步方法,而不会阻塞调用线程。结合标准版本兼容性检查和注意事项,这些平台 API 的 1-1 扩展程序可能会作为 Jetpack 中的 androidx.core:core-ktx
工件提供。如需了解详情、取消注意事项和示例,请参阅 asOutcomeReceiver 文档。
如果异步方法与返回结果或在工作完成时抛出异常的方法的语义不符,则不应将 OutcomeReceiver
用作回调类型。请改为考虑下一部分中列出的其他选项之一。
优先使用功能接口,而不是创建新的单一抽象方法 (SAM) 类型
API 级别 24 添加了 java.util.function.*
(参考文档)类型,这些类型提供通用 SAM 接口(例如 Consumer<T>
),适合用作回调 lambda。在许多情况下,创建新的 SAM 接口在类型安全性或传达 intent 方面几乎没有价值,同时还会不必要地扩大 Android API Surface 区域。
请考虑使用以下通用接口,而不是创建新的接口:
Runnable
:() -> Unit
Supplier<R>
:() -> R
Consumer<T>
:(T) -> Unit
Function<T,R>
:(T) -> R
Predicate<T>
:(T) -> Boolean
- 如需了解更多信息,请参阅参考文档
SAM 参数的放置
SAM 参数应放在最后,以便从 Kotlin 以惯用方式使用,即使方法因额外参数而被重载也是如此。
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
文档
以下是有关 API 公开文档 (Javadoc) 的规则。
所有公共 API 都必须有文档
所有公共 API 都必须提供足够的文档来说明开发者如何使用该 API。假设开发者使用自动补全功能或在浏览 API 参考文档时找到了该方法,并且从相邻的 API Surface(例如同一类)获得的上下文信息非常少。
方法
必须分别使用 @param
和 @return
文档注解记录方法参数和返回值。将 Javadoc 正文的格式设置为“此方法...”前置。
如果某个方法不接受任何参数,没有任何特殊注意事项,并且返回的结果与方法名称所述的一致,您可以省略 @return
,并编写类似以下内容的文档:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
始终在 Javadoc 中使用链接
文档应链接到其他文档,以便查看相关常量、方法和其他元素。使用 Javadoc 标记(例如 @see
和 {@link foo}
),而不仅仅是纯文本字词。
对于以下来源示例:
public static final int FOO = 0;
public static final int BAR = 1;
请勿使用原始文本或代码字体:
/**
* Sets value to one of FOO or <code>BAR</code>.
*
* @param value the value being set, one of FOO or BAR
*/
public void setValue(int value) { ... }
请改用链接:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
请注意,对参数使用 IntDef
注解(例如 @ValueType
)会自动生成文档,其中指定了允许的类型。如需详细了解 IntDef
,请参阅注解指南。
添加 Javadoc 时运行 update-api 或 docs 目标
在添加 @link
或 @see
标记时,此规则尤为重要,可确保输出结果看起来符合预期。Javadoc 中的 ERROR 输出通常是由链接不良所致。update-api
或 docs
Make 目标都会执行此检查,但如果您只更改 Javadoc 且不需要运行 update-api
目标,则 docs
目标的速度可能会更快。
使用 {@code foo} 来区分 Java 值
使用 {@code...}
将 true
、false
和 null
等 Java 值括起来,以将其与文档文本区分开来。
在 Kotlin 源代码中编写文档时,您可以使用反引号将代码括起来,就像使用 Markdown 一样。
@param 和 @return 摘要应为单个句子 fragment
参数和返回值摘要应以小写字符开头,且仅包含一个句子片段。如果您有超过一句话的其他信息,请将其移至方法 Javadoc 正文:
/**
* @param e The element to be appended to the list. This must not be
* null. If the list contains no entries, this element will
* be added at the beginning.
* @return This method returns true on success.
*/
应更改为:
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
Google 文档注释需要说明
说明为何将注解 @hide
和 @removed
从公共 API 中隐藏。
添加有关如何替换标记了 @deprecated
注解的 API 元素的说明。
使用 @throws 记录异常
如果某个方法抛出受检异常(例如 IOException
),请使用 @throws
记录该异常。对于供 Java 客户端使用的 Kotlin 源 API,请使用 @Throws
为函数添加注解。
如果某个方法抛出未检查的异常(例如 IllegalArgumentException
或 IllegalStateException
),表明存在可防范的错误,请记录该异常并说明抛出该异常的原因。抛出的异常还应指明抛出的原因。
某些未检查异常情况被视为隐式异常,无需记录,例如 NullPointerException
或 IllegalArgumentException
,其中参数与将 API 协定嵌入到方法签名中的 @IntDef
或类似注解不匹配:
/**
* ...
* @throws IOException If it cannot find the schema for {@code toVersion}
* @throws IllegalStateException If the schema validation fails
*/
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
boolean validateDroppedTables, Migration... migrations) throws IOException {
// ...
if (!dbPath.exists()) {
throw new IllegalStateException("Cannot find the database file for " + name
+ ". Before calling runMigrations, you must first create the database "
+ "using createDatabase.");
}
// ...
或者,使用 Kotlin 编写:
/**
* ...
* @throws IOException If something goes wrong reading the file, such as a bad
* database header or missing permissions
*/
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
// ...
val read = input.read(buffer)
if (read != 4) {
throw IOException("Bad database header, unable to read 4 bytes at " +
"offset 60")
}
}
// ...
如果该方法调用可能会抛出异常的异步代码,请考虑开发者如何发现和响应此类异常。通常,这涉及将异常转发到回调,并记录接收异常的方法抛出的异常。除非异步异常实际上是从带注解的方法中重新抛出,否则不应使用 @throws
记录异常。
以句号结尾
Doclava 工具会以简单的方式解析文档,只要看到英文句号 (.) 后跟空格,就会结束摘要文档(第一个句子,用于课程文档顶部的简短说明)。这会导致两个问题:
- 如果短文档不以句号结尾,并且该成员有被该工具提取的继承文档,则摘要也会提取这些继承文档。例如,请参阅
R.attr
文档中的actionBarTabStyle
,其中包含添加到摘要中的维度说明。 - 出于同样的原因,请避免在第一句中使用“例如”,因为 Doclava 会在“g.”后结束摘要文档。例如,请参阅
View.java
中的TEXT_ALIGNMENT_CENTER
。请注意,Metalava 会在英文句号后插入一个不可分割空格来自动更正此错误;不过,最好不要犯这种错误。
设置文档格式以便以 HTML 格式呈现
Javadoc 以 HTML 格式呈现,因此请相应地设置这些文档的格式:
换行应使用显式
<p>
标记。请勿添加结束</p>
标记。请勿使用 ASCII 呈现列表或表格。
列表应分别使用
<ul>
或<ol>
表示无序和有序。每个项都应以<li>
标记开头,但不需要闭合</li>
标记。最后一个项后面必须有结束</ul>
或</ol>
标记。表格应使用
<table>
,<tr>
用于行,<th>
用于标题,<td>
用于单元格。所有表格标记都需要匹配的闭合标记。您可以在任何代码中使用class="deprecated"
来表示已废弃。如需创建内嵌代码字体,请使用
{@code foo}
。如需创建代码块,请使用
<pre>
。浏览器会解析
<pre>
代码块中的所有文本,因此请谨慎使用括号<>
。您可以使用<
和>
HTML 实体对其进行转义。或者,如果您将有问题的部分封装在
{@code foo}
中,则可以在代码段中保留原始括号<>
。例如:<pre>{@code <manifest>}</pre>
遵循 API 参考文档样式指南
为确保类摘要、方法说明、参数说明和其他项的样式保持一致,请遵循如何为 Javadoc 工具编写文档注释中官方 Java 语言准则中的建议。
针对 Android 框架的规则
这些规则与特定于 Android 框架内置 API 和行为的 API、模式和数据结构(例如 Bundle
或 Parcelable
)有关。
intent 构建器应使用 create*Intent() 模式
intent 的创建者应使用名为 createFooIntent()
的方法。
使用 Bundle 而非创建新的通用数据结构
避免创建新的通用数据结构来表示任意键与类型化值的映射。请改用 Bundle
。
在编写用作非平台应用和服务之间的通信渠道的平台 API 时,通常会出现这种情况,其中平台不会读取通过渠道发送的数据,并且 API 协定可能会在平台之外部分定义(例如,在 Jetpack 库中)。
如果平台会读取数据,请避免使用 Bundle
,而应使用强类型数据类。
Parcelable 实现必须具有公共 CREATOR 字段
Parcelable 膨胀是通过 CREATOR
公开的,而不是通过原始构造函数。如果某个类实现了 Parcelable
,则其 CREATOR
字段也必须是公共 API,并且接受 Parcel
参数的类构造函数必须是私有的。
对界面字符串使用 CharSequence
在界面中显示字符串时,请使用 CharSequence
来允许 Spannable
实例。
如果它只是一个键或其他标签或值,并且对用户不可见,则可以使用 String
。
避免使用枚举
在所有平台 API 中,必须使用 IntDef
而非枚举,并且在未捆绑的库 API 中也应强烈考虑使用 IntDef
。仅当您确定不会添加新值时,才使用枚举。
IntDef
的优势:
- 支持随时间添加值
- 如果 Kotlin
when
语句因平台中添加了枚举值而不再详尽,则可能会在运行时失败。
- 如果 Kotlin
- 运行时不使用任何类或对象,仅使用基元
- 虽然 R8 或缩减功能可以避免为未捆绑的库 API 产生此开销,但此优化无法影响平台 API 类。
枚举的优势
- Java、Kotlin 的惯用语言功能
- 启用详尽的 switch、
when
语句用法- 注意 - 值不得随时间而变化,请参阅上一个列表
- 具有明确作用域且可被发现的命名
- 启用编译时验证
- 例如,Kotlin 中返回值的
when
语句
- 例如,Kotlin 中返回值的
- 是一个可以实现接口、具有静态帮助程序、公开成员或扩展方法以及公开字段的正常类。
遵循 Android 软件包分层层次结构
android.*
软件包层次结构具有隐式排序,其中较低级别的软件包不能依赖于较高级别的软件包。
避免提及 Google、其他公司及其产品
Android 平台是一个开源项目,旨在做到不受供应商影响。该 API 应是通用的,并且系统集成商或具有必要权限的应用都可以使用。
Parcelable 实现应为最终实现
平台定义的 Parcelable 类始终从 framework.jar
加载,因此应用尝试替换 Parcelable
实现是无效的。
如果发送应用扩展了 Parcelable
,接收应用将没有发送应用的自定义实现可用于解压缩。关于向后兼容性的注意事项:如果您的类过去不是最终类,但没有公开构造函数,您仍然可以将其标记为 final
。
调用系统进程的方法应将 RemoteException 重新抛出为 RuntimeException
RemoteException
通常由内部 AIDL 抛出,表示系统进程已终止,或者应用尝试发送的数据量过多。在这两种情况下,公共 API 都应以 RuntimeException
的形式重新抛出,以防止应用保留安全或政策决策。
如果您知道 Binder
调用的另一端是系统进程,则最佳实践是使用以下样板代码:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
针对 API 更改抛出特定异常
公共 API 行为可能会因 API 级别而异,并导致应用崩溃(例如,强制执行新的安全政策)。
当 API 需要针对之前有效的请求抛出异常时,请抛出新的特定异常,而不是通用异常。例如,使用 ExportedFlagRequired
而非 SecurityException
(并且 ExportedFlagRequired
可以扩展 SecurityException
)。
这有助于应用开发者和工具检测 API 行为变化。
实现复制构造函数,而不是克隆
强烈建议不要使用 Java clone()
方法,因为 Object
类未提供 API 协定,并且使用 clone()
的类扩展固有困难。请改用接受同一类型对象的复制构造函数。
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
依赖于构建器进行构建的类应考虑添加构建器复制构造函数,以允许对副本进行修改。
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
使用 ParcelFileDescriptor 而非 FileDescriptor
java.io.FileDescriptor
对象的所有权定义不佳,可能会导致不易发现的“使用后关闭”bug。相反,API 应返回或接受 ParcelFileDescriptor
实例。旧版代码可以使用 dup() 或 getFileDescriptor() 在 PFD 和 FD 之间进行转换(如果需要)。
避免使用奇数大小的数值
避免直接使用 short
或 byte
值,因为这类值通常会限制您未来对 API 进行演进的方式。
避免使用 BitSet
java.util.BitSet
非常适合实现,但不适用于公共 API。它是可变的,需要为高频率方法调用进行分配,并且不会为每个位提供表示的语义含义。
对于高性能场景,请将 int
或 long
与 @IntDef
结合使用。对于低性能场景,请考虑使用 Set<EnumType>
。对于原始二进制数据,请使用 byte[]
。
首选 android.net.Uri
android.net.Uri
是 Android API 中 URI 的首选封装容器。
避免使用 java.net.URI
,因为它在解析 URI 时过于严格;也绝不要使用 java.net.URL
,因为它的等式定义严重破坏。
隐藏标记为 @IntDef、@LongDef 或 @StringDef 的注解
标记为 @IntDef
、@LongDef
或 @StringDef
的注解表示一组可传递给 API 的有效常量。不过,当它们本身作为 API 导出时,编译器会内嵌常量,并且只有(现在无用的)值会保留在注解的 API 桩(对于平台)或 JAR(对于库)中。
因此,必须在平台中使用 @hide
docs 注解或在库中使用 @RestrictTo.Scope.LIBRARY)
代码注解来标记这些注解的使用情况。在上述两种情况下,都必须将它们标记为 @Retention(RetentionPolicy.SOURCE)
,以防止它们显示在 API 桩或 JAR 中。
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
构建平台 SDK 和库 AAR 时,工具会提取注解,并将其与编译的源代码分开打包。Android Studio 会读取此捆绑格式并强制执行类型定义。
不添加新的设置提供程序密钥
请勿公开 Settings.Global
、Settings.System
或 Settings.Secure
中的新密钥。
相反,请在相关类(通常是“管理器”类)中添加适当的 getter 和 setter Java API。添加监听器机制或广播,以便根据需要通知客户端发生的更改。
与 getter/setter 相比,SettingsProvider
设置存在一些问题:
- 没有类型安全性。
- 没有统一的方法来提供默认值。
- 没有适当的方式来自定义权限。
- 例如,您无法使用自定义权限保护设置。
- 没有正确添加自定义逻辑的正确方法。
- 例如,您无法根据设置 B 的值更改设置 A 的值。
示例:Settings.Secure.LOCATION_MODE
已存在很长时间,但位置信息团队已将其弃用,改用适当的 Java API LocationManager.isLocationEnabled()
和 MODE_CHANGED_ACTION
广播,这让该团队获得了更大的灵活性,并且 API 的语义现在更加清晰。
请勿扩展 activity 和 AsyncTask
AsyncTask
是实现细节。请改为公开监听器,或者在 androidx 中公开 ListenableFuture
API。
Activity
子类无法组合。为功能扩展 activity 会导致该功能与需要用户执行相同操作的其他功能不兼容。而是应使用 LifecycleObserver 等工具依赖于组合。
使用上下文的 getUser()
绑定到 Context
的类(例如从 Context.getSystemService()
返回的任何内容)应使用绑定到 Context
的用户,而不是公开定位特定用户的成员。
class FooManager {
Context mContext;
void fooBar() {
mIFooBar.fooBarForUser(mContext.getUser());
}
}
class FooManager {
Context mContext;
Foobar getFoobar() {
// Bad: doesn't appy mContext.getUserId().
mIFooBar.fooBarForUser(Process.myUserHandle());
}
Foobar getFoobar() {
// Also bad: doesn't appy mContext.getUserId().
mIFooBar.fooBar();
}
Foobar getFoobarForUser(UserHandle user) {
mIFooBar.fooBarForUser(user);
}
}
例外情况:如果方法接受的值不代表单个用户(例如 UserHandle.ALL
),则可以接受用户参数。
使用 UserHandle 而非普通 int
建议使用 UserHandle
来提供类型安全性,并避免将用户 ID 与 uid 混淆。
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
如果无法避免,表示用户 ID 的 int
必须带有 @UserIdInt
注解。
Foobar getFoobarForUser(@UserIdInt int user);
优先使用监听器或回调来广播 intent
广播 intent 非常强大,但会导致可能对系统运行状况产生负面影响的新行为,因此应谨慎添加新的广播 intent。
以下是我们不鼓励引入新广播 intent 的一些具体原因:
在发送不带
FLAG_RECEIVER_REGISTERED_ONLY
标志的广播时,它们会强制启动尚未运行的任何应用。虽然这有时可能是预期的结果,但可能会导致数十个应用被标记,对系统运行状况产生负面影响。我们建议您使用JobScheduler
等替代策略,以便在满足各种前提条件时更好地协调。发送广播时,您几乎无法过滤或调整传送给应用的内容。这使得您很难或根本无法应对未来的隐私问题,也无法根据接收应用的目标 SDK 引入行为更改。
由于广播队列是共享资源,因此可能会出现过载,进而导致事件无法及时传送。我们在实际环境中观察到,有多个广播队列的端到端延迟时间为 10 分钟或更长时间。
因此,我们建议新功能考虑使用监听器、回调或 JobScheduler
等其他设施,而不是广播 intent。
如果广播 intent 仍然是理想的设计,请考虑以下最佳实践:
- 如果可能,请使用
Intent.FLAG_RECEIVER_REGISTERED_ONLY
将广播限制为已在运行的应用。例如,ACTION_SCREEN_ON
使用此设计来避免唤醒应用。 - 如果可能,请使用
Intent.setPackage()
或Intent.setComponent()
将广播定位到感兴趣的特定应用。例如,ACTION_MEDIA_BUTTON
使用此设计来聚焦于当前处理播放控件的应用。 - 如果可能,请将广播定义为
<protected-broadcast>
,以防止恶意应用冒充操作系统。
系统绑定的开发者服务中的 intent
旨在由开发者扩展并由系统绑定的服务(例如 NotificationListenerService
等抽象服务)可能会响应来自系统的 Intent
操作。此类服务应符合以下条件:
- 在包含服务的完全限定类名称的类上定义
SERVICE_INTERFACE
字符串常量。此常量必须带有@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
注解。 - 在该类中记录开发者必须向其
AndroidManifest.xml
添加<intent-filter>
,才能从平台接收 intent。 - 强烈建议添加系统级权限,以防止恶意应用向开发者服务发送
Intent
。
Kotlin-Java 互操作性
如需查看完整的指南列表,请参阅官方 Android Kotlin-Java 互操作指南。为提高可检测性,我们已将部分准则复制到本指南中。
API 公开范围
某些 Kotlin API(例如 suspend fun
)不适合 Java 开发者使用;不过,请勿尝试使用 @JvmSynthetic
控制特定于语言的可见性,因为这会对调试程序中 API 的呈现方式产生副作用,从而使调试变得更加困难。
如需获取具体指导,请参阅 Kotlin-Java 互操作指南或异步指南。
伴生对象
Kotlin 使用 companion object
公开静态成员。在某些情况下,这些方法会在名为 Companion
的内部类(而非包含类)中显示。Companion
类在 API 文本文件中可能会显示为空类,这是正常现象。
为了最大限度地提高与 Java 的兼容性,请使用 @JvmField
为伴生对象的非常量字段添加注解,并使用 @JvmStatic
为公共函数添加注解,以便直接在包含类中公开它们。
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Android 平台 API 的演变
本部分介绍了有关您可以对现有 Android API 进行哪些类型的更改,以及您应如何实现这些更改以最大限度地提高与现有应用和代码库的兼容性,的政策。
二进制文件破坏性更改
避免在已完成的公共 API Surface 中进行二进制破坏性更改。运行 make update-api
时,这类更改通常会引发错误,但 Metalava 的 API 检查可能无法捕获一些极端情况。如有疑问,请参阅 Eclipse Foundation 的 Evolving Java-based APIs 指南,详细了解哪些类型的 API 更改在 Java 中是兼容的。隐藏 API(例如系统 API)中的二进制破坏性更改应遵循废弃/替换周期。
破坏源代码的更改
我们不鼓励进行破坏源代码的更改,即使这些更改不会破坏二进制文件也是如此。与二进制文件兼容但会破坏源代码的变更的一个示例是向现有类添加泛型,这与二进制文件兼容,但可能会因继承或模糊引用而引入编译错误。运行 make update-api
时,破坏源代码的更改不会引发错误,因此您必须仔细了解更改现有 API 签名的影响。
在某些情况下,为了改进开发者体验或代码正确性,您可能需要进行破坏源代码的更改。例如,向 Java 源代码添加可为 null 性注解可提高与 Kotlin 代码的互操作性并降低出错几率,但通常需要更改源代码(有时是重大更改)。
对私有 API 的更改
您可以随时更改带有 @TestApi
注解的 API。
您必须将带有 @SystemApi
注解的 API 保留三年。您必须按照以下时间表移除或重构系统 API:
- API y - 已添加
- API y+1 - 废弃
- 使用
@Deprecated
标记代码。 - 添加替换项,并使用
@deprecated
文档注解在已废弃代码的 Javadoc 中链接到替换项。 - 在开发周期内,针对内部用户提交 bug,告知他们该 API 即将弃用。这有助于验证替换 API 是否足够。
- 使用
- API y+2 - 软删除
- 使用
@removed
标记代码。 - 可选地,针对以当前 SDK 级别为目标平台的应用抛出异常或执行无操作。
- 使用
- API y+3 - 硬删除
- 从源代码树中彻底移除相应代码。
废弃
我们将废弃视为 API 更改,这种更改可能会发生在主要版本(例如字母版本)中。废弃 API 时,请将 @Deprecated
源注解和 @deprecated
<summary>
文档注解搭配使用。您的摘要必须包含迁移策略。此策略可能链接到替代 API,或者说明不应使用该 API 的原因:
/**
* Simple version of ...
*
* @deprecated Use the {@link androidx.fragment.app.DialogFragment}
* class with {@link androidx.fragment.app.FragmentManager}
* instead.
*/
@Deprecated
public final void showDialog(int id)
您还必须弃用在 XML 中定义并在 Java 中公开的 API,包括在 android.R
类中公开的属性和可样式化属性,并提供摘要:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
何时弃用 API
废弃功能最适合用于禁止在新代码中采用某个 API。
我们还要求您在 API 被@removed
之前将其标记为 @deprecated
,但这并不能给开发者提供强大的动力来迁离他们已在使用的 API。
在弃用 API 之前,请考虑对开发者的影响。废弃 API 的影响包括:
javac
会在编译期间发出警告。- 您无法全局抑制或设定基准以抑制废弃警告,因此使用
-Werror
的开发者需要单独修正或抑制废弃 API 的每一次使用,然后才能更新其编译 SDK 版本。 - 无法抑制有关已废弃类导入的废弃警告。因此,开发者需要在每次使用已废弃的类时内嵌完全限定类名,然后才能更新其编译 SDK 版本。
- 您无法全局抑制或设定基准以抑制废弃警告,因此使用
d.android.com
的文档显示了废弃通知。- Android Studio 等 IDE 会在 API 使用情况网站上显示警告。
- IDE 可能会降低 API 的排名或将其从自动补全功能中隐藏。
因此,废弃 API 可能会让最关心代码健康状况的开发者(使用 -Werror
的开发者)不愿采用新 SDK。如果开发者不关心现有代码中的警告,则可能会完全忽略废弃。
如果 SDK 引入大量废弃项,这两种情况都会变得更糟糕。
因此,我们建议仅在以下情况下弃用 API:
- 我们计划在未来的版本中
@remove
该 API。 - API 使用会导致错误或未定义的行为,如果不破坏兼容性,我们无法修复此问题。
废弃某个 API 并将其替换为新 API 时,我们强烈建议向 androidx.core
等 Jetpack 库添加相应的兼容性 API,以简化对旧设备和新设备的支持。
我们不建议废弃在当前和未来版本中按预期运行的 API:
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
如果 API 无法再保持其记录的行为,则应废弃该 API:
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
已废弃的 API 的变更
您必须维护已废弃 API 的行为。这意味着测试实现必须保持不变,并且在您废弃 API 后,测试必须继续通过。如果 API 没有测试,您应添加测试。
请勿在未来版本中扩展已废弃的 API Surface。您可以向现有的已废弃 API 添加 lint 正确性注解(例如 @Nullable
),但不应添加新的 API。
请勿将新 API 添加为已废弃。如果在预发布周期内添加了任何 API,但随后将其废弃(因此最初会以废弃状态进入公共 API 接口),您必须在最终确定 API 之前将其移除。
软移除
软删除是一项破坏源代码的更改,除非 API 委员会明确批准,否则您应避免在公共 API 中进行此类更改。对于系统 API,您必须在主要版本的整个生命周期内废弃该 API,然后才能软删除该 API。移除对 API 的所有文档引用,并在软删除 API 时使用 @removed <summary>
文档注解。如弃用中所述,您的摘要必须包含移除原因,并且可以包含迁移策略。
您可以保持软移除的 API 的行为不变,但更重要的是必须保留这些行为,以便现有调用方在调用 API 时不会崩溃。在某些情况下,这可能意味着保留行为。
必须保持测试覆盖率,但测试内容可能需要更改,以适应行为变化。测试仍必须验证现有调用方在运行时不会崩溃。您可以保持软移除的 API 的行为不变,但更重要的是,您必须保留该行为,以便现有调用方在调用该 API 时不会崩溃。在某些情况下,这可能意味着保留行为。
您必须保持测试覆盖率,但测试内容可能需要更改以适应行为变化。测试仍必须验证现有调用方在运行时不会崩溃。
在技术层面,我们使用 @remove
Javadoc 注解从 SDK 桩 JAR 和编译时类路径中移除该 API,但它仍然存在于运行时类路径中,与 @hide
API 类似:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
从应用开发者的角度来看,当 compileSdk
等于或高于移除该 API 的 SDK 时,该 API 将不再显示在自动补全中,并且引用该 API 的源代码将无法编译;不过,源代码仍可针对较低版本的 SDK 成功编译,并且引用该 API 的二进制文件仍可正常运行。
某些类别的 API 不得被软删除。您不得软删除某些类别的 API。
抽象方法
您不得对开发者可能会扩展的类软删除抽象方法。这样一来,开发者将无法在所有 SDK 级别成功扩展该类。
在极少数情况下,开发者从来无法也不会扩展类,但您仍然可以软删除抽象方法。
硬性移除
硬移除是一项会破坏二进制文件的更改,绝不应在公共 API 中发生。
不建议使用的注释
我们使用 @Discouraged
注解来指明在大多数(超过 95%)情况下不建议使用某个 API。不建议使用的 API 与已废弃的 API 不同,因为存在一个狭窄的关键用例会阻止废弃。将某个 API 标记为不建议使用时,您必须提供说明和替代解决方案:
@Discouraged(message = "Use of this function is discouraged because resource
reflection makes it harder to perform build
optimizations and compile-time verification of code. It
is much more efficient to retrieve resources by
identifier (such as `R.foo.bar`) than by name (such as
`getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}
我们不建议您添加新的 API。
现有 API 行为的变化
在某些情况下,您可能需要更改现有 API 的实现行为。例如,在 Android 7.0 中,我们改进了 DropBoxManager
,以便在开发者尝试发布的事件太大而无法通过 Binder
发送时明确进行通信。
不过,为避免给现有应用造成问题,我们强烈建议为旧版应用保留安全行为。过去,我们会根据应用的 ApplicationInfo.targetSdkVersion
来防范这些行为变更,但最近我们已改为要求使用应用兼容性框架。以下示例展示了如何使用以下新框架实现行为更改:
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
public class MyClass {
@ChangeId
// This means the change will be enabled for target SDK R and higher.
@EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
// Use a bug number as the value, provide extra detail in the bug.
// FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
static final long FOO_NOW_DOES_X = 123456789L;
public void doFoo() {
if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
// do the new thing
} else {
// do the old thing
}
}
}
借助此应用兼容性框架设计,开发者可以在调试应用的过程中,在预览版和 Beta 版期间暂时停用特定行为变更,而不是强制他们同时适应数十项行为变更。
向前兼容性
向前兼容性是一种设计特性,可让系统接受为其较新版本而设计的输入。对于 API 设计,您必须特别注意初始设计以及未来的更改,因为开发者希望只编写一次代码、测试一次,然后便能在任何地方顺利运行。
以下是导致 Android 中最常见的向前兼容性问题的原因:
- 向之前假定为完整的集合(例如
@IntDef
或enum
)添加新的常量(例如,switch
包含会抛出异常的default
)。 - 添加了对未直接在 API Surface 中捕获的功能的支持(例如,支持在 XML 中分配
ColorStateList
类型的资源,而之前仅支持<color>
资源)。 - 放宽了对运行时检查的限制,例如移除了较低版本中存在的
requireNotNull()
检查。
在所有这些情况下,开发者只有在运行时才会发现问题。更糟糕的是,他们可能会通过来自现场旧版设备的崩溃报告发现此问题。
此外,这些情况在技术上都是有效的 API 更改。它们不会破坏二进制文件或源代码兼容性,并且 API lint 不会捕获任何此类问题。
因此,API 设计人员在修改现有类时必须特别注意。问自己一个问题:“这项更改会导致仅针对最新版平台编写和测试的代码在较低版本上失败吗?”
XML 架构
如果 XML 架构用作组件之间的稳定接口,则必须明确指定该架构,并且必须以向后兼容的方式进行演变,与其他 Android API 类似。例如,必须保留 XML 元素和属性的结构,就像在其他 Android API 接口上维护方法和变量一样。
废弃 XML
如果您想废弃某个 XML 元素或属性,可以添加 xs:annotation
标记,但您必须遵循典型的 @SystemApi
演变生命周期,继续支持所有现有 XML 文件。
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
必须保留元素类型
架构支持将 sequence
元素、choice
元素和 all
元素作为 complexType
元素的子元素。但是,这些子元素的子元素数量和顺序不同,因此修改现有类型将是不可兼容的更改。
如果您想修改现有类型,最佳实践是废弃旧类型并引入新类型来替换它。
<!-- Original "sequence" value -->
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- New "choice" value -->
<xs:element name="fooChoice">
<xs:complexType>
<xs:choice>
<xs:element name="name" type="xs:string"/>
</xs:choice>
</xs:complexType>
</xs:element>
Mainline 专用模式
Mainline 是一个项目,可让您单独更新 Android OS 的子系统(“Mainline 模块”),而不是更新整个系统映像。
Mainline 模块必须与核心平台“解捆绑”,这意味着每个模块与外界之间的所有交互都必须使用正式(公共或系统)API 进行。
Mainline 模块应遵循特定的设计模式。本部分将对此进行介绍。
<Module>FrameworkInitializer 模式
如果主模块需要公开 @SystemService
类(例如 JobScheduler
),请使用以下模式:
从模块中公开
<YourModule>FrameworkInitializer
类。此类需要位于$BOOTCLASSPATH
中。示例: StatsFrameworkInitializer并使用
@SystemApi(client = MODULE_LIBRARIES)
为其添加注解。为其添加
public static void registerServiceWrappers()
方法。当服务管理器类需要引用
Context
时,请使用SystemServiceRegistry.registerContextAwareService()
注册该类。当服务管理器类不需要引用
Context
时,请使用SystemServiceRegistry.registerStaticService()
注册该类。从
SystemServiceRegistry
的静态初始化程序调用registerServiceWrappers()
方法。
<Module>ServiceManager 模式
通常,为了注册系统服务 binder 对象或获取对它们的引用,用户会使用 ServiceManager
,但主线模块无法使用它,因为它是隐藏的。此类处于隐藏状态,因为 Mainline 模块不应注册或引用静态平台或其他模块公开的系统服务 binder 对象。
Mainline 模块可以改用以下模式,以便注册和获取对模块内实现的 binder 服务的引用。
按照 TelephonyServiceManager 的设计创建
<YourModule>ServiceManager
类将该类公开为
@SystemApi
。如果您只需要从$BOOTCLASSPATH
类或系统服务器类访问它,可以使用@SystemApi(client = MODULE_LIBRARIES)
;否则,@SystemApi(client = PRIVILEGED_APPS)
也适用。此类将包含以下内容:
- 一个隐藏的构造函数,因此只有静态平台代码才能将其实例化。
- 用于返回特定名称的
ServiceRegisterer
实例的公共 getter 方法。如果您有一个 binder 对象,则需要一个 getter 方法。如果有两个,则需要两个 getter。 - 在
ActivityThread.initializeMainlineModules()
中,实例化此类,并将其传递给模块公开的静态方法。通常,您会在FrameworkInitializer
类中添加一个静态@SystemApi(client = MODULE_LIBRARIES)
API 来接受它。
此模式会阻止其他 Mainline 模块访问这些 API,因为其他模块无法获取 <YourModule>ServiceManager
的实例,即使 get()
和 register()
API 对它们可见也是如此。
以下是电话服务获取电话服务引用的方式:代码搜索链接。
如果您在原生代码中实现服务 binder 对象,则需要使用 AServiceManager
原生 API。这些 API 与 ServiceManager
Java API 相对应,但原生 API 会直接公开给主线模块。请勿使用它们注册或引用不归模块所有的 binder 对象。如果您从原生代码公开 binder 对象,则 <YourModule>ServiceManager.ServiceRegisterer
不需要 register()
方法。
Mainline 模块中的权限定义
包含 APK 的 Mainline 模块可以在其 APK AndroidManifest.xml
中定义(自定义)权限,方法与常规 APK 相同。
如果定义的权限仅在模块内部使用,则其权限名称应带有 APK 软件包名称作为前缀,例如:
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
如果要将定义的权限作为可更新平台 API 的一部分提供给其他应用,则其权限名称应带有“android.permission.”前缀。(如任何静态平台权限)以及模块软件包名称,以表明它是模块中的平台 API,同时避免任何命名冲突,例如:
<permission
android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
android:label="@string/active_calories_burned_read_content_description"
android:protectionLevel="dangerous"
android:permissionGroup="android.permission-group.HEALTH" />
然后,该模块可以在其 API 接口中将此权限名称公开为 API 常量,例如 HealthPermissions.READ_ACTIVE_CALORIES_BURNED
。