Android R 新特性分析及適配指南

xiaxveliang發表於2021-03-12

Android R(Android 11 API 30)於2020年9月9日正式釋出,隨國內各終端廠商在售Android裝置的版本更新升級,應用軟體對Android R 版本的相容適配已迫在眉睫。

對於Android R的新特性,這裡按照以下幾個方面進行了歸納:分割槽儲存、許可權、隱私、效能、安全

官方文件描述:https://developer.android.google.cn/about/versions/11

一、分割槽儲存

從Android 10(API 29)開始,Android預設開啟分割槽儲存功能,不過Android 10 可通過增加android:requestLegacyExternalStorage="true"配置停用分割槽儲存
從Android 11(API 30)開始,強制執行分割槽儲存,對於Android 11及以上裝置,android:requestLegacyExternalStorage="true"配置將不再有效。

Android 11 分割槽儲存官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage
Android 10 預設開啟分割槽儲存:
https://xiaxl.blog.csdn.net/article/details/103125117

1.1、訪問目錄

開啟分割槽儲存後,應用預設情況下只能訪問應用專屬目錄(內部儲存、外部儲存應用專屬目錄),以及本應用所建立的特定型別的媒體檔案

  • 應用專屬目錄
    包括內部儲存外部儲存專屬目錄(若應用包名com.xiaxl.demo):
    /data/data/com.xiaxl.demo/files,
    /sdcard/Android/data/com.xiaxl.demo/files
    分別採用以下API進行訪問:
    File appFile = new File(context.getFilesDir(), filename);
    File appExternalFile = new File(context.getExternalFilesDir(), filename);

  • 共享儲存目錄
    包括媒體、文件和其他檔案。例如DCIM、Pictures、Movies、Download等目錄;
    注:
    Android 10(Android Q)中共享儲存目錄使用MediaStore API訪問;
    Android 11(Android R)中共享儲存目錄支援MediaStore API與File API訪問。
    為保證應用在Android 10、Android 11裝置中,使用File API對共享儲存目錄具有相同的檔案訪問許可權。建議在應用 AndroidManifest配置檔案中,增加requestLegacyExternalStorage="true"標識,以關閉Android 10裝置上的分割槽儲存功能,使分割槽儲存只對Android 11以上裝置生效

1.2、訪問所需許可權

  • 應用專屬目錄
    應用專屬目錄(內部儲存外部儲存專屬目錄)的讀寫,Android 4.4以上裝置不需要任何許可權;
  • 共享儲存目錄
    共享儲存路徑的讀寫,需要READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 許可權;

檔案路徑的訪問許可權

Android 11以上裝置中,如果您的應用再次請求READ_EXTERNAL_STORAGE許可權時,動態許可權申請彈窗將變化為“您的應用正在請求訪問照片和媒體”

您的應用正在請求訪問照片和媒體

檔案媒體訪問 官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage

1.3、共享檔案

如果需要與其他應用共享單個檔案或應用資料,可以使用API:

  • FileProvider(分享自己的一個或多個檔案)
    如果應用需要將自己的一個或多個檔案提供給其他應用,安全的做法是向接收方應用傳送檔案的內容 URI,並授予對該 URI 的臨時訪問許可權。
    Android FileProvider 元件提供了 getUriForFile() 方法,用於生成檔案的內容 URI
  • ContentProvider(獲取替他應用提供的資料)
    如果您需要向其他應用提供資料,可以使用ContentProvider
    ContentProvider是一種標準介面,可將一個程式中的資料與另一個程式中執行的程式碼進行連。

ContentProvider管理儲存空間

Android 11 共享檔案官方描述:
https://developer.android.google.cn/training/data-storage#scoped-storage

1.4、所有檔案的訪問許可權

有一些應用需要獲取所有檔案的訪問許可權,例如:檔案管理器軟體。
獲取所有檔案的訪問許可權,可申請MANAGE_EXTERNAL_STORAGE許可權。

// 許可權配置
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

// 是否擁有MANAGE_EXTERNAL_STORAGE許可權判斷
Environment.isExternalStorageManager();

// 跳轉到設定頁,請求使用者授權
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);

MANAGE_EXTERNAL_STORAGE相關官方描述:
https://developer.android.google.cn/training/data-storage/manage-all-files

二、許可權

Android 11 中對許可權進行了如下更改:

  • 新增 READ_PHONE_NUMBERS許可權,獲取手機號碼;
  • 後臺訪問位置許可權調整;
  • 使用者多次針對某項特定的許可權請求拒絕,表示使用者希望不再詢問
  • 應用長時間未使用,系統會自動重置使用者已授予敏感許可權
  • 針對位置、麥克風、攝像頭授權彈窗新增僅限這一次授權按鈕;
  • SYSTEM_ALERT_WINDOW 許可權授權方式改變為系統自動授權;

參考 Android 11 許可權更新官方文件:
https://developer.android.google.cn/about/versions/11/privacy/permissions#one-time

2.1、新增 READ_PHONE_NUMBERS 許可權

當應用的 targetSdkVersion>=30 時,使用以下API獲取手機號碼時,需要申請READ_PHONE_NUMBERS許可權,而不再是READ_PHONE_STATE 許可權。

  • TelephonyManager 類和 TelecomManager 類中的 getLine1Number() 方法。
  • TelephonyManager 類中不受支援的 getMsisdn() 方法。

在Android 10及之前的裝置,可以繼續使用READ_PHONE_STATE獲取手機號;
對Android11及以上裝置,需獲取READ_PHONE_NUMBERS許可權,才能獲取手機號;

<manifest>
    <!-- 僅在Android 10及以下裝置獲取READ_PHONE_STATE許可權,以獲取終端手機號碼-->
    <uses-permission android:name="READ_PHONE_STATE"
                     android:maxSdkVersion="29" />
	<!-- Android 11及以上裝置獲取READ_PHONE_NUMBERS許可權,以獲取終端手機號碼-->
    <uses-permission android:name="READ_PHONE_NUMBERS" />
</manifest>

對於READ_PHONE_STATE許可權

READ_PHONE_NUMBERS許可權官方API描述:
https://developer.android.google.cn/reference/android/Manifest.permission#READ_PHONE_NUMBERS

2.2、後臺訪問位置許可權調整

  • 在Android10裝置上,同時申請前臺、後臺位置許可權時,並在使用者選擇始終允許後,才能獲得後臺位置許可權。
  • 在Android11裝置上,對於targetSdkVersion<=29(Android 10)的應用,同時申請前臺、後臺位置許可權時,對話方塊不再提示始終允許字樣,而是提供了位置許可權的設定入口,需要使用者在設定頁面選擇始終允許才能獲得後臺位置許可權。
  • 在Android11裝置上,對於targetSdkVersion=30(Android 11)的應用,同時申請前臺、後臺位置許可權時,系統會忽略該請求,無任何響應(需首先獲取前臺位置許可權,再次申請後臺位置許可權)。
  • 在Android11裝置上,對於targetSdkVersion=30(Android 11)的應用,先申請前臺位置許可權,後申請後臺位置許可權

後臺訪問位置許可權 官方描述:
https://developer.android.google.cn/training/location/background

a、Android10裝置

在Android10裝置上,同時申請前臺、後臺位置許可權時,並在使用者選擇始終允許後,才能獲得後臺位置許可權。

// 在Android10裝置上,同時 申請前臺、後臺位置許可權
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

在Android10裝置上,同時 申請前臺、後臺位置許可權

b、Android11裝置 targetSdkVersion<=29

在Android11裝置上,對於targetSdkVersion<=29(Android 10)的應用,同時申請前臺、後臺位置許可權時,對話方塊不再提示始終允許字樣,而是提供了位置許可權的設定入口,需要使用者在設定頁面選擇始終允許才能獲得後臺位置許可權。

// 在Android11裝置上,targetSdkVersion<=29的應用,同時 申請前臺、後臺位置許可權
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

Android11裝置上targetSdkVersion<=29的應用,申請前臺、後臺位置許可權

c、Android11裝置 targetSdkVersion=30 同時申請前臺、後臺位置許可權

  • 在Android11裝置上,對於targetSdkVersion=30(Android 11)的應用,同時申請前臺、後臺位置許可權時,系統會忽略該請求,無任何響應(需首先獲取前臺位置許可權,再次申請後臺位置許可權)。
// 在Android11裝置上,targetSdkVersion=30的應用,同時 申請前臺、後臺位置許可權
// 請求無反應,此為錯誤寫法
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

d、Android11裝置 targetSdkVersion=30 依次申請前臺、後臺位置許可權

在Android11裝置上,對於targetSdkVersion=30(Android 11)的應用,先申請前臺位置許可權,後申請後臺位置許可權

// 在Android11裝置上,targetSdkVersion=30的應用,申請前臺位置許可權
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_COARSE_LOCATION}, 101);

Android11裝置上,targetSdkVersion=30的應用,申請前臺位置許可權

Android11裝置上,targetSdkVersion=30的應用,申請後臺位置許可權,直接跳轉到設定頁面。

// 在Android11裝置上,targetSdkVersion=30的應用,申請後臺位置許可權
ActivityCompat.requestPermissions(this,
    new String[]{
        Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 101);

Android11裝置上,targetSdkVersion=30的應用,申請後臺位置許可權

2.3、使用者多次針對某項特定的許可權請求拒絕

在 Android 11 中,使用者多次針對某項特定的許可權請求點選了拒絕,那麼應用再次請求該項許可權時,使用者將不會看到系統許可權彈窗,該操作表示使用者希望不再詢問

2.4、長時間未使用,自動重置已授予敏感許可權

在 Android 11 中,當targetSdkVersion>=30時,應用在一段時間內未使用,系統會通過自動重置使用者已授予應用的執行時敏感許可權來保護使用者資料;

2.5、新增“僅限這一次”授權按鈕

攝像頭、位置、麥克風 新增臨時訪問許可權

從 Android 11(API 級別 30)開始,當應用請求與位置、麥克風、攝像頭相關許可權時,面向使用者的授權對話方塊會包含僅限這一次選項;如果使用者在對話方塊中選擇僅限這一次,系統會嚮應用授予臨時的單次授權。

單次授權

許可權申請API使用方式不變:

private void showCameraPreview() {
    // 判斷是否擁有Camera許可權
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            == PackageManager.PERMISSION_GRANTED) {
        // 進入Camera頁面
        // startCamera();
    } else {
        // 請求Camera許可權
        requestCameraPermission();
    }
}

private void requestCameraPermission() {
    // 判斷Camera許可權,之前是否已被使用者"拒絕"
    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.CAMERA)) {
        // 彈窗告訴使用者,為什麼需要Camera許可權
        Snackbar.make(mLayout, R.string.camera_access_required,
                Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 請求Camera許可權
                ActivityCompat.requestPermissions(MainActivity.this,
                        new String[]{Manifest.permission.CAMERA},
                        PERMISSION_REQUEST_CAMERA);
            }
        }).show();

    } else {
        // 請求Camera許可權
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    if (requestCode == PERMISSION_REQUEST_CAMERA) {
        // 使用者授權Camera(使用者選擇"使用使用時允許"、"僅這一次允許")
        if (grantResults.length == 1
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // Permission has been granted. Start camera preview Activity.
            Snackbar.make(mLayout, R.string.camera_permission_granted,
                    Snackbar.LENGTH_SHORT)
                    .show();
            startCamera();
        }
        // 使用者選擇"拒絕"
        else {
            // Permission request was denied.
            Snackbar.make(mLayout, R.string.camera_permission_denied,
                    Snackbar.LENGTH_SHORT)
                    .show();
        }
    }
}

原始碼參考:
https://github.com/android/permissions-samples/tree/main/RuntimePermissionsBasic

2.6、SYSTEM_ALERT_WINDOW 許可權授權方式

在 Android 11 中,SYSTEM_ALERT_WINDOW 許可權授權方式更改為:根據請求自動向某些應用授予 SYSTEM_ALERT_WINDOW 許可權

  • 系統會自動向具有 ROLE_CALL_SCREENING 且請求 SYSTEM_ALERT_WINDOW 的所有應用授予該許可權。如果應用失去 ROLE_CALL_SCREENING,就會失去該許可權。
    ROLE_CALL_SCREENINGRoleManager中的常量類,多用於通知使用者將我們的應用替換掉手機自帶的預搭載應用(簡訊、電話撥號);
  • 系統會自動向通過 MediaProjection 擷取螢幕且請求 SYSTEM_ALERT_WINDOW 的所有應用授予該許可權,除非使用者已明確拒絕嚮應用授予該許可權。當應用停止擷取螢幕時,就會失去該許可權。此用例主要用於遊戲直播應用。

SYSTEM_ALERT_WINDOW許可權 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/permissions#system-alert

三、隱私保護

主要更改涉及以下幾個方面:

  • 軟體包可見性:獲取其他應用資訊需在AndroidManifest中增加<queries>標籤;
  • 前臺服務:訪問位置資訊、攝像頭、麥克風限制;
  • 永久 SIM 卡識別符號 ICCID 獲取受限;
  • 新增AppOpsManager.OnOpNotedCallback監聽危險許可權的呼叫,從而保護使用者的私密資料;
    這樣對於第三方依賴庫的許可權使用申請可以做一個監控

3.1、軟體包可見性

  • 在 Android 11 及更高版本裝置中,當應用的 targetSdkVersion>=30 時,如果應用希望獲取其他應用的資訊(比如:包名、軟體名稱),原有方式將無法獲取到。
  • 如需獲取其他應用資訊,需要在AndroidManifest中增加<queries>元素標籤,告知系統希望獲取哪些應用的資訊或者哪一類應用的資訊。
  • 如果需要獲取所有應用的資訊(比如:Launcher應用、裝置管理器應用):這種情況只需要在AndroidManifest中新增QUERY_ALL_PACKAGES許可權即可。
    QUERY_ALL_PACKAGES許可權為普通許可權,不需要進行動態申請。但提交應用市場後,應用市場可能會進行稽核

軟體包可見性 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/package-visibility

 <manifest package="com.xiaxl.myapp">

	// 1、若知道具體應用的包名
    <queries>
        <package android:name="com.xiaxl.otherapp01" />
        <package android:name="com.xiaxl.otherapp01" />
    </queries>
	// 2、不知道包名,但想知道某一類App的應用資訊
    <queries>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="image/jpeg" />
        </intent>
    </queries>	
</manifest>

3.2、前臺服務:訪問位置資訊、攝像頭、麥克風限制

當應用的 targetSdkVersion>=30 時,前臺服務訪問位置資訊、攝像頭、麥克風時,需新增foregroundServiceType

<manifest>
	// 前臺服務訪問:位置資訊、攝像頭、麥克風
    <service
        android:foregroundServiceType="location|camera|microphone" />
</manifest>

前臺服務 官方描述:
https://developer.android.google.cn/about/versions/11/privacy/foreground-services

3.3、永久 SIM 卡識別符號 ICCID 獲取受限

在 Android 11 及更高版本中,使用 SubscriptionInfo.getIccId() 方法訪問不可重置的 ICCID 受到限制。

SubscriptionInfo.getIccId() 方法會返回一個非null的空字串

如需唯一標識裝置上安裝的 SIM 卡,請改用 getSubscriptionId() 方法。SubscriptionId會提供一個索引值,用於唯一識別已安裝的 SIM 卡(包括實體 SIM 卡和電子 SIM 卡),除非裝置恢復出廠設定,否則此識別符號的值對於給定 SIM 卡是保持不變的。

3.4、監聽危險許可權的呼叫

Android 11新增AppOpsManager.OnOpNotedCallback為開發者提供對應用危險許可權的使用監聽,從而保護使用者的私密資料
當應用以及應用的依賴包中,申請某項危險許可權時,AppOpsManager.OnOpNotedCallback的對應回撥方法將會被呼叫,從而列印申請的許可權對應的API呼叫棧

舉例:
使用位置許可權獲取位置資訊時,將會回撥AppOpsManager.OnOpNotedCallback中的onNoted方法,並列印使用的許可權對應的API呼叫棧

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //
    AppOpsManager.OnOpNotedCallback appOpsCallback =
            new AppOpsManager.OnOpNotedCallback() {
                private void logPrivateDataAccess(String opCode, String trace) {
                    Log.i("xiaxl: ", "opCode: " + opCode + "\n trace: " + trace);
                }

                @Override
                public void onNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
                    Log.i("xiaxl: ", "---onNoted---");
                    logPrivateDataAccess(syncNotedAppOp.getOp(),
                            Arrays.toString(new Throwable().getStackTrace()));
                }

                @Override
                public void onSelfNoted(@NonNull SyncNotedAppOp syncNotedAppOp) {
                    Log.i("xiaxl: ", "---onSelfNoted---");
                    logPrivateDataAccess(syncNotedAppOp.getOp(),
                            Arrays.toString(new Throwable().getStackTrace()));
                }

                @Override
                public void onAsyncNoted(@NonNull AsyncNotedAppOp asyncNotedAppOp) {
                    Log.i("xiaxl: ", "---onAsyncNoted---");
                    logPrivateDataAccess(asyncNotedAppOp.getOp(),
                            asyncNotedAppOp.getMessage());
                }
            };

    AppOpsManager appOpsManager = getSystemService(AppOpsManager.class);
    if (appOpsManager != null) {
        appOpsManager.setOnOpNotedCallback(getMainExecutor(), appOpsCallback);
    }
}

public void getLocation() {
    // 建立歸因
    Context attributionContext = createAttributionContext("shareLocation");
    // 獲取位置資訊
    LocationManager locationManager =
            attributionContext.getSystemService(LocationManager.class);
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
            && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
        return;
    }
    Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}

列印日誌如下:

---onNoted---
opCode: android:coarse_location
trace: 
[com.xiaxl.android_test.MainActivity$1.onNoted(MainActivity.java:42), 
 android.app.AppOpsManager.readAndLogNotedAppops(AppOpsManager.java:8204), 
 android.os.Parcel.readExceptionCode(Parcel.java:2304), 
 android.os.Parcel.readException(Parcel.java:2279), 
 android.location.ILocationManager$Stub$Proxy.getLastLocation(ILocationManager.java:1225),
 android.location.LocationManager.getLastKnownLocation(LocationManager.java:648),
 com.xiaxl.android_test.MainActivity.getLocation(MainActivity.java:87),
 com.xiaxl.android_test.MainActivity$2.onClick(MainActivity.java:70),
 android.view.View.performClick(View.java:7448),
 com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:967),
 android.view.View.performClickInternal(View.java:7425),
 android.view.View.access$3600(View.java:810),
 android.view.View$PerformClick.run(View.java:28305),
 android.os.Handler.handleCallback(Handler.java:938),
 android.os.Handler.dispatchMessage(Handler.java:99),
 android.os.Looper.loop(Looper.java:223),
 android.app.ActivityThread.main(ActivityThread.java:7656),
 java.lang.reflect.Method.invoke(Native Method),
 com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592),
 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)]
 

從以上日誌可以看出,當應用申請ACCESS_COARSE_LOCATION許可權並獲取位置資訊時,列印了應用申請的許可權對應的API呼叫棧

AppOpsManager 相關官方描述:
https://developer.android.google.cn/guide/topics/data/audit-access#audit-by-attribution-tag

四、效能

  • JobScheduler使用頻率進行限制

4.1、JobScheduler使用頻率進行限制

Android 11 為對JobScheduler使用頻率進行一定限制。
對於 debuggable 清單屬性設定為 true 的應用,過多的呼叫 JobScheduler API 將返回 RESULT_FAILURE

JobScheduler主要用於在未來某個時間下滿足一定條件時觸發執行某項任務,例如:當裝置在空閒狀態, 並且使用wifi時, 自動下載Apk
JobScheduler典型的使用舉例如下:

 JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);  
ComponentName jobService = new ComponentName(this, MyJobService.class);

//任務Id等於123
JobInfo jobInfo = new JobInfo.Builder(123, jobService) 
	 // 任務最少延遲時間  
     .setMinimumLatency(5000)
	 // 任務deadline,當到期沒達到指定條件也會開始執行  
     .setOverrideDeadline(60000)
	 // 網路條件,網路無需付費時執行
     .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
	 // 是否充電  
     .setRequiresCharging(true)
	 // 是否在空閒時執行
     .setRequiresDeviceIdle(true)
	 // 裝置重啟後是否繼續執行
     .setPersisted(true) 
	 // 設定退避/重試策略
     .setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR) 
     .build();  
scheduler.schedule(jobInfo);

官方描述參考:
https://developer.android.google.cn/about/versions/11/behavior-changes-all

官方Demo參考:
https://github.com/googlearchive/android-JobScheduler

五、安全

  • 非 SDK 介面限制

5.1、非 SDK 介面限制

官方從 Android 9(API 級別 28)開始,對應用使用的非 SDK 介面實施了限制。
如果你的APP通過引用非 SDK 介面或嘗試使用反射或 JNI 來獲取控制程式碼,這些限制就會起作用。官方給出的解釋是為了提升使用者體驗、降低應用崩潰風險

a、非SDK介面檢測工具

官方給出了一個檢測工具,下載地址:veridex

veridex使用方法:

appcompat.sh --dex-file=apk.apk

veridex檢測截圖

b、blacklist、greylist、greylist-max-o、greylist-max-p含義

以上截圖中,blacklist、greylist、greylist-max-o、greylist-max-p含義如下:

  • blacklist 黑名單:禁止使用的非SDK介面,執行時直接Crash(因此必須解決)
  • greylist 灰名單:即當前版本仍能使用的非SDK介面,但在下一版本中可能變成被限制的非SDK介面
  • greylist-max-o: 在targetSDK<=O中能使用,但是在targetSDK>=P中被禁止使用的非SDK介面
  • greylist-max-p: 在targetSDK<=P中能使用,但是在targetSDK>=Q中被禁止使用的非SDK介面

非SDK介面限制 官方描述:
https://developer.android.google.cn/about/versions/11/non-sdk-11

========== THE END ==========

歡迎關注我的公眾號

相關文章