Google针对非 SDK 接口的限制

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_DISABLED0表示可以访问所有hidden api,但只限于系统应用
HIDDEN_API_ENFORCEMENT_JUST_WARN1表示所有hidden api都不会被限制,只打印警告log
HIDDEN_API_ENFORCEMENT_ENABLED2表示深灰名单和黑名单里面的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什么会修改源码增加限制,到时候非常规方法解决的问题又会重新出现。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值