Google针对非 SDK 接口的限制
前言
最近在项目中遇到一个问题,非系统签名应用预装到android sdk为28的系统vender或者system分区下,调用隐藏api时,程序崩溃。
报错信息如下:
06-30 12:24:09.684 4114 4119 W lloremoteserve: Accessing hidden method Landroid/view/RenderNode;->getClipToOutline()Z (dark greylist, linking)
06-30 12:24:10.844 4056 4070 D ap_server: BSSID list{eth0=172.23.38.70}
06-30 12:24:10.848 4056 4070 D ap_server: called getGatewayIp() method ,the return is
06-30 12:24:14.341 4114 4114 D TvShareActivity: Auto move to next page with duration : 5
06-30 12:24:14.453 4114 4114 W lloremoteserve: Accessing hidden method Landroid/view/RenderNode;->getClipToOutline()Z (dark greylist, linking)
06-30 12:24:14.470 4114 4114 D AndroidRuntime: Shutting down VM
--------- beginning of crash
06-30 12:24:14.501 4114 4114 E AndroidRuntime: FATAL EXCEPTION: main
06-30 12:24:14.501 4114 4114 E AndroidRuntime: Process: com.stark.bytelloremoteserver, PID: 4114
06-30 12:24:14.501 4114 4114 E AndroidRuntime: java.lang.NoSuchMethodError: No virtual method getClipToOutline()Z in class Landroid/view/RenderNode; or its super classes (declaration of 'android.view.RenderNode' appears in /system/framework/framework.jar!classes2.dex)
06-30 12:24:14.501 4114 4114 E AndroidRuntime: at androidx.compose.ui.platform.RenderNodeApi23.getClipToOutline(RenderNodeApi23.android.kt:153)
06-30 12:24:14.501 4114 4114 E AndroidRuntime: at androidx.compose.ui.platform.RenderNodeLayer.updateLayerProperties-dRfWZ4U(RenderNodeLayer.android.kt:118)
06-30 12:24:14.501 4114 4114 E AndroidRuntime: at androidx.compose.ui.node.LayoutNodeWrapper.updateLayerParameters(LayoutNodeWrapper.kt:310)
06-30 12:24:14.501 4114 4114 E AndroidRuntime: at androidx.compose.ui.node.LayoutNodeWrapper.onLayerBlockUpdated(LayoutNodeWrapper.kt:281)
06-30 12:24:14.501 4114 4114 E AndroidRuntime: at androidx.compose.ui.node.LayoutNodeWrapper.placeAt-f8xVGno(LayoutNodeWrapper.kt:214)
06-30 12:24:14.501 4114 4114 E AndroidRuntime: at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.placeAt-f8xVGno(DelegatingLayoutNodeWrapper.kt:98)
06-30 12:24:14.501 4114 4114 E AndroidRuntime: at androidx.compose.ui.layout.Placeable.access$placeAt-f8xVGno(Placeable.kt:31)
主要原因在于: Accessing hidden method Landroid/view/RenderNode;>getClipToOutline(),不能访问隐藏的getClipToOutline()方法。
预装到系统的应用为什么会受到这种限制呢?
Google针对非 SDK 接口的限制
从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。
什么是SDK接口和非SDK 接口?
一般而言,公共 SDK 接口是在 Android 框架软件包索引中记录的那些接口,即开发时可以正常调用的接口。非 SDK 接口的处理是 API 抽象出来的实现细节,因此这些接口可能会在不另行通知的情况下随时发生更改。
为了避免发生崩溃和意外行为,应用应仅使用 SDK 中经过正式记录的类。这也意味着当您的应用通过反射等机制与类互动时,不应访问 SDK 中未列出的方法或字段。
非SDK接口类型
为最大程度地降低非 SDK 使用限制对开发工作流的影响,Google将非 SDK 接口分成了几个名单,这些名单界定了非 SDK 接口使用限制的严格程度(取决于应用的目标 API 级别)。下表介绍了这些名单:
名单级别 | 说明 |
---|---|
屏蔽名单 (blacklist) | 无论应用的目标 API 级别是什么,您都无法使用的非 SDK 接口。 如果您的应用尝试访问其中任何一个接口,系统就会抛出错误。 |
有条件屏蔽 (greylist-max-x) | 从 Android 9(API 级别 28)开始,当有应用以该 API 级别为目标平台时,我们会在每个 API 级别分别限制某些非 SDK 接口。 这些名单会以应用无法再访问该名单中的非 SDK 接口之前可以作为目标平台的最高 API 级别 (max-target-x) 进行标记。例如,在 Android Pie 中未被屏蔽、但现在已被 Android 10 屏蔽的非 SDK 接口会列入 max-target-p (greylist-max-p) 名单,其中的“p”表示 Pie 或 Android 9(API 级别 28)。如果您的应用尝试访问受目标 API 级别限制的接口,系统就会将此 API 视为已列入屏蔽名单。 |
不支持 (greylist) | 当前不受限制且您的应用可以使用的非 SDK 接口。 但请注意,这些接口不受支持,可能会在未发出通知的情况下随时发生更改。预计这些接口在未来的 Android 版本中会被有条件地屏蔽,并列在 max-target-x 名单中。 |
SDK (whitelist) | 已在 Android 框架软件包索引中正式记录、受支持并且可以自由使用的接口。 |
尽管您目前仍可以使用某些非 SDK 接口(取决于应用的目标 API 级别),但只要您使用任何非 SDK 方法或字段,终归存在导致应用出问题的显著风险。如果您的应用依赖于非 SDK 接口,建议您开始计划迁移到 SDK 接口或其他替代方案。如果您无法为应用中的功能找到无需使用非 SDK 接口的替代方案,我们建议您请求添加新的公共 API。
对应用产生的影响
上面说了Google从Android P开始对部分api调用进行限制。
如果应用调用了这部分的api,对系统应用和普通会产生什么影响呢?
白名单:system/etc/sysconfig/hiddenapi-package-whitelist.xml
应用类型 | 浅灰名单(greylist) | 黑名单(blacklist) | SDK API |
---|---|---|---|
系统应用(系统签名) | 可以调用 | 可以调用 | 可以调用 |
系统应用(非系统签名) | 可以调用,会有警告 | 不可以调用 | 可以调用 |
非系统应用(系统签名) | 可以调用 | 可以调用 | 可以调用 |
非系统应用(非系统签名) | 可以调用,会有警告 | 不可以调用 | 可以调用 |
系统应用(添加到白名单) | 可以调用 | 可以调用 | 可以调用 |
非系统应用(添加到白名单) | 可以调用,会有警告 | 不可以调用 | 可以调用 |
上面的结论是如何得来的呢,看一下相关的源码。
以Android API 30的源码为例,相对API 30以下的源码改动不大。
源码解析
1、ActivityManagerService启动应用时,会调用startProcessLocked()方法启动进程;
// startProcessLocked() returns existing proc's record if it's already running
ProcessRecord proc = startProcessLocked(app.processName, app,
false, 0,
new HostingRecord("backup", hostingName),
ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS, false, false, false);
if (proc == null) {
Slog.e(TAG, "Unable to start backup agent process " + r);
return false;
}
2、接着会调用到ProcessList的startProcessLocked()方法,在getHiddenApiEnforcementPolicy()方法中获取该应用hidden API的策略;
if (!disableHiddenApiChecks && !mService.mHiddenApiBlacklist.isDisabled()) {
app.info.maybeUpdateHiddenApiEnforcementPolicy(
mService.mHiddenApiBlacklist.getPolicy());
@ApplicationInfo.HiddenApiEnforcementPolicy int policy =
app.info.getHiddenApiEnforcementPolicy();
int policyBits = (policy << Zygote.API_ENFORCEMENT_POLICY_SHIFT);
if ((policyBits & Zygote.API_ENFORCEMENT_POLICY_MASK) != policyBits) {
throw new IllegalStateException("Invalid API policy: " + policy);
}
runtimeFlags |= policyBits;
if (disableTestApiChecks) {
runtimeFlags |= Zygote.DISABLE_TEST_API_ENFORCEMENT_POLICY;
}
}
3、在ApplicationInfo类中看一下getHiddenApiEnforcementPolicy()方法的实现。第一步会调用isAllowedToUseHiddenApis()方法判断该应用是否被允许使用隐藏API,允许的话返回HIDDEN_API_ENFORCEMENT_DISABLED策略。第二个判断有没有设置其它的策略,有的话就返回设置的策略。都没有则返回HIDDEN_API_ENFORCEMENT_ENABLED策略。
private boolean isAllowedToUseHiddenApis() {
//判断是否有系统签名
if (isSignedWithPlatformKey()) {
return true;
} else if (isSystemApp() || isUpdatedSystemApp()) { //判断是否系统应用或者系统应用更新的
//是否设置android:usesNonSdkApi="true"属性 或者 添加了白名单
return usesNonSdkApi() || isPackageWhitelistedForHiddenApis();
} else {
return false;
}
}
/**
* @hide
*/
public @HiddenApiEnforcementPolicy int getHiddenApiEnforcementPolicy() {
if (isAllowedToUseHiddenApis()) {
return HIDDEN_API_ENFORCEMENT_DISABLED;
}
if (mHiddenApiPolicy != HIDDEN_API_ENFORCEMENT_DEFAULT) {
return mHiddenApiPolicy;
}
return HIDDEN_API_ENFORCEMENT_ENABLED;
}
执行的策略类型
策略类型 | 值 | 说明 |
---|---|---|
HIDDEN_API_ENFORCEMENT_DEFAULT | -1 | 表示是否限制该hidden api取决于targetsdk |
HIDDEN_API_ENFORCEMENT_DISABLED | 0 | 表示可以访问所有hidden api,但只限于系统应用 |
HIDDEN_API_ENFORCEMENT_JUST_WARN | 1 | 表示所有hidden api都不会被限制,只打印警告log |
HIDDEN_API_ENFORCEMENT_ENABLED | 2 | 表示深灰名单和黑名单里面的hidden api会被限制 |
如需改变hidden API执行的策略,可以通过adb 命令设置相应的策略值:
adb shell settings put global hidden_api_policy 1
如需将 API 强制执行策略重置为默认设置,执行指令删除hidden_api_policy属性:
adb shell settings delete global hidden_api_policy
总结
** 回到前言中的问题:预装到系统的应用使用隐藏API时会崩溃?**
通过上面的源码可以知道,是因为没有系统签名或者将包名添加到hiddenapi-package-whitelist.xml名单中。
解决方法
根据上面的分析,该问题有以下解决方法。
- 方法1:应用使用系统签名。
- 方法2:将包名添加到hiddenapi-package-whitelist.xml名单中,并且把应用预装到系统中。
- 方法3:通过adb命令改变hidden API的执行策略,如:adb shell settings put global hidden_api_policy 1。
- 方法4:只适用于Android 9平台,将targetSdkVersion改为28以下。
- 方法5:在自定义的Application类的attachBaseContext()方法中,调用下面方法可以绕过所有的限制。
public static void installReflection() {
try {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
Method forName = Class.class.getDeclaredMethod("forName", String.class);
Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
Method setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
Object sVmRuntime = getRuntime.invoke(null);
if (sVmRuntime == null || setHiddenApiExemptions == null) {
return;
}
setHiddenApiExemptions.invoke(sVmRuntime, new Object[]{new String[] {"L"}});
}
} catch (Throwable e) {
e.printStackTrace();
}
}
为什么将targetSdkVersion改为28以下就可以呢
因为Android P的系统对隐藏API的限制有点差异。下面看一下相关的源码:
private boolean isAllowedToUseHiddenApis() {
return isSignedWithPlatformKey()
|| (isPackageWhitelistedForHiddenApis() && (isSystemApp() || isUpdatedSystemApp()));
}
/**
* @hide
*/
public @HiddenApiEnforcementPolicy int getHiddenApiEnforcementPolicy() {
if (isAllowedToUseHiddenApis()) {
return HIDDEN_API_ENFORCEMENT_NONE;
}
if (mHiddenApiPolicy != HIDDEN_API_ENFORCEMENT_DEFAULT) {
return mHiddenApiPolicy;
}
if (targetSdkVersion < Build.VERSION_CODES.P) {
return HIDDEN_API_ENFORCEMENT_BLACK;
} else {
return HIDDEN_API_ENFORCEMENT_DARK_GREY_AND_BLACK;
}
}
getHiddenApiEnforcementPolicy()方法中会判断应用的targetSdkVersion是否小于Android P,小于Android P则只会限制黑名单接口的使用,浅灰和灰色API不受限制。
总的来说,最好还是遵循Google的开发规范,不能使用的API就查看有没有其它API可以替代。因为你不知道Google什么会修改源码增加限制,到时候非常规方法解决的问题又会重新出现。