Android進(jìn)階——Android6.0 動(dòng)態(tài)權(quán)限詳解及動(dòng)態(tài)申請(qǐng)權(quán)限的完全攻略

引言

Android系統(tǒng)雖然開源平绩,但是相對(duì)還是比較安全的臼勉,尤其是高版本的系統(tǒng),這得益于Android系統(tǒng)自身的安全機(jī)制晤斩,其中權(quán)限管理機(jī)制一直是首要的安全概念焕檬,Android 動(dòng)態(tài)權(quán)限又叫運(yùn)行時(shí)權(quán)限已經(jīng)面世很久了,網(wǎng)上很多文章都是只寫了用法澳泵,不客氣地說只是告訴了怎么用实愚,具體的機(jī)制并沒有很完整,讓一些初學(xué)者只知其然而不知其所然兔辅,對(duì)于動(dòng)態(tài)權(quán)限并沒有完全掌握腊敲,于是我就想結(jié)合自己的項(xiàng)目經(jīng)驗(yàn)和官方的文檔,一篇文章把重要關(guān)于動(dòng)態(tài)的知識(shí)點(diǎn)都總結(jié)出來幢妄,當(dāng)然不是所有的兔仰,比如說權(quán)限組的實(shí)際操作等等。

一蕉鸳、Android系統(tǒng)權(quán)限機(jī)制概述

我們知道在Android的權(quán)限系統(tǒng)一直是首要的安全概念乎赴,因?yàn)檫@些權(quán)限在Android M(6.0)之前在AndroidManifest文件中聲明之后忍法,僅App在安裝的時(shí)候被詢問一次,安裝成功之后運(yùn)行榕吼,就可以在用戶毫不知曉的情況下訪問權(quán)限內(nèi)的內(nèi)容饿序,毫無顧忌地收集信息(雖然現(xiàn)在也還是可以在一次申請(qǐng)之后無顧忌的使用)。而在Android M之后羹蚣,app將不會(huì)在安裝的時(shí)候授予權(quán)限原探。App不得不在運(yùn)行時(shí)一個(gè)一個(gè)詢問用戶授予權(quán)限,系統(tǒng)權(quán)限被按敏感級(jí)別分組(normal顽素、signature咽弦、dangerous、privileged胁出、signature|privileged)并且敏感權(quán)限必須在運(yùn)行的時(shí)候動(dòng)態(tài)申請(qǐng)型型,并且隨時(shí)可以在設(shè)置了取消已經(jīng)授權(quán)的權(quán)限,又可以為兩大類:普通權(quán)限和危險(xiǎn)權(quán)限全蝶,在M手機(jī)上闹蒜,對(duì)于敏感權(quán)限,需要在程序運(yùn)行時(shí)進(jìn)行動(dòng)態(tài)申請(qǐng)抑淫。對(duì)于非敏感權(quán)限绷落,即Normal Permissions,和M之前的使用相同始苇。如果APP是在Android 5.1 或更低版本的設(shè)備上運(yùn)行砌烁,或者APP的targetSdkVersion為 22 或更低時(shí),在清單中列出了危險(xiǎn)權(quán)限埂蕊,則用戶必須在安裝應(yīng)用時(shí)授予此權(quán)限往弓,若不授予此權(quán)限,系統(tǒng)則不會(huì)安裝蓄氧。而APP運(yùn)行在 Android 6.0 或更高版本的設(shè)備上,或者應(yīng)用的目標(biāo) SDK 為 23 或更高槐脏,應(yīng)用必須在清單中列出所有權(quán)限喉童,并且它必須在運(yùn)行時(shí)動(dòng)態(tài)申請(qǐng)危險(xiǎn)權(quán)限,用戶隨時(shí)可以授予或拒絕每項(xiàng)權(quán)限顿天。值得注意的是:從 Android 6.0(API 級(jí)別 23)開始堂氯,用戶可以隨時(shí)從任意應(yīng)用調(diào)用權(quán)限,即使應(yīng)用面向較低的 API 級(jí)別也可以調(diào)用牌废,所以無論APP面向哪個(gè) API 級(jí)別咽白,都應(yīng)對(duì)應(yīng)用進(jìn)行測(cè)試,以驗(yàn)證它在缺少需要的權(quán)限時(shí)行為是否正常鸟缕。

二晶框、Android M權(quán)限機(jī)制

1排抬、Normal級(jí)別的權(quán)限只需要在AndroidManifest中聲明就好,安裝時(shí)就授權(quán)授段,不需要每次使用時(shí)都檢查權(quán)限蹲蒲,而且用戶不能取消以上授權(quán)

這里寫圖片描述

2、其他級(jí)別的權(quán)限在編譯在Android M(即targetSdkVersion大于等于23時(shí)候)版本時(shí)候侵贵,不僅需要在AndroidManifest中聲明届搁,還得在運(yùn)行的時(shí)候需要?jiǎng)討B(tài)申請(qǐng),而且還可以隨時(shí)取消授權(quán)窍育。

  • 先在AndroidManifest中聲明
  • 再在運(yùn)行的時(shí)候動(dòng)態(tài)申請(qǐng)

3卡睦、Android6.0之后權(quán)限組管理

同一組的任何一個(gè)權(quán)限被授權(quán)了,其他權(quán)限也自動(dòng)被授權(quán)漱抓。比如說一旦WRITE_CONTACTS被授權(quán)了表锻,app也有READ_CONTACTS和GET_ACCOUNTS了。在api23中通過Activity的checkSelfPermission和requestPermissions來檢查和請(qǐng)求權(quán)限的方法辽旋。(或者可以使用( compile 'com.android.support:support-v4:25.2.0')v4庫中的ContextCompat.checkSelfPermission()浩嫌、ActivityCompat.requestPermissions()
危險(xiǎn)權(quán)限實(shí)際上才是運(yùn)行時(shí)權(quán)限主要處理的對(duì)象,這些權(quán)限可能引起隱私問題或者影響其他程序運(yùn)行补胚。Android中的危險(xiǎn)權(quán)限可以歸為以下幾個(gè)分組:

這里寫圖片描述

三码耐、運(yùn)行時(shí)申請(qǐng)權(quán)限

因?yàn)?.0權(quán)限授權(quán)的改變,即使你在Manifest中加入溶其,有些權(quán)限依然需要獲得用戶的手動(dòng)授權(quán)骚腥,但是這一機(jī)制——運(yùn)行時(shí)權(quán)限僅僅是在我們我們?cè)O(shè)置targetSdkVersion 大于等于23時(shí)且運(yùn)行在M系統(tǒng)以上的設(shè)備上才起作用(即你想要你的app支持這一新特性你得設(shè)置compileSdkVersion 和targetSdkVersion 設(shè)為大于等于23),所以如果希望app在6.0之前的設(shè)備依然使用舊的權(quán)限系統(tǒng)瓶逃,只需要把targetSdkVersion設(shè)置為23以下即可束铭,還有一點(diǎn)使用Android studio新建項(xiàng)目,targetSdkVersion 會(huì)自動(dòng)設(shè)置為 23厢绝,如果你還沒支持新運(yùn)行時(shí)權(quán)限契沫,個(gè)人建議先把targetSdkVersion 降級(jí)到22,在M以后昔汉,敏感權(quán)限默認(rèn)的值是每次詢問懈万,而且shouldShowRequestPermissionRationale()返回值機(jī)制手機(jī)系統(tǒng)不同。

  • 第一靶病、在AndroidManifest聲明所需的權(quán)限
  • 第二会通、使用相應(yīng)的api方法動(dòng)態(tài)申請(qǐng)

1、使用系統(tǒng)提供的API

動(dòng)態(tài)權(quán)限的核心工作流程:checkSelfPermission檢查是否已被授予——>requestPermissions申請(qǐng)權(quán)限——>自動(dòng)回調(diào)onRequestPermissionsResult——shouldShowRequestPermissionRationale娄周。無論什么框架變出花來都離不開這個(gè)基本的流程涕侈。

| API方法| 說明|
|: ------------- |:------------|
| int checkSelfPermission(@NonNull Context context, @NonNull String permission) |可用activity.checkSelfPermission或者v4包下的 ContextCompat.checkSelfPermission來檢查權(quán)限是否已授權(quán) |
| void requestPermissions(final @NonNull Activity activity,final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) | 可用activity.requestPermissions或者v4包下的 ContextCompat.requestPermissions來進(jìn)行權(quán)限申請(qǐng)。需要為每一個(gè)權(quán)限指定一個(gè)id煤辨,當(dāng)系統(tǒng)返回授權(quán)結(jié)果時(shí)裳涛,應(yīng)用根據(jù)id拿到授權(quán)結(jié)果木张。|
| void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,@NonNull int[] grantResults) | 由系統(tǒng)自動(dòng)觸發(fā),當(dāng)應(yīng)用申請(qǐng)權(quán)限后调违,Activity將觸發(fā)這個(gè)回調(diào)窟哺,告訴應(yīng)用用戶的授權(quán)結(jié)果。 |
|boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission) |當(dāng)應(yīng)用首次申請(qǐng)權(quán)限時(shí)技肩,如果用戶點(diǎn)擊拒絕且轨,下次再申請(qǐng)權(quán)限,Android允許你提示用戶虚婿,你為什么需要這個(gè)權(quán)限旋奢,更好引導(dǎo)用戶是否授權(quán),其中在Android原生系統(tǒng)中:如果應(yīng)用之前請(qǐng)求過此權(quán)限但用戶拒絕了請(qǐng)求然痊,此方法將返回true至朗;如果用戶在過去拒絕了權(quán)限請(qǐng)求且在權(quán)限請(qǐng)求系統(tǒng)對(duì)話框中選擇了 Don't ask again 選項(xiàng)將返回 false;如果第一次申請(qǐng)權(quán)限也返回false剧浸;如果設(shè)備規(guī)范禁止應(yīng)用具有該權(quán)限锹引,此方法也會(huì)返回 false,返回false則不在顯示提示對(duì)話框唆香,返回true則回顯示對(duì)話框(但并不一定任何系統(tǒng)的機(jī)制都是如此嫌变,不同廠商不同的Rom機(jī)制有可能不同,以HTC 6.0和聯(lián)想的系統(tǒng)為例躬它,在HTC上就不一定是返回true腾啥,目前測(cè)試結(jié)果一直都是false,而聯(lián)想手機(jī)上則和原生的機(jī)制一樣)|

public class MainActivity extends Activity implements View.OnClickListener {

    public static final int REQUEST_PERMISSION_CALL = 100;
    private static final String CALL_PHONE = Manifest.permission.CALL_PHONE;
    private static final String TAG = "Permission";
    private Button btnCheck, btnShow, btnCall;
    private Intent callIntent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        initViews();
        callIntent = new Intent(Intent.ACTION_CALL);
        Uri uri = Uri.parse("tel:" + "10086");
        callIntent.setData(uri);
    }

    private void initViews() {
        btnCheck = (Button) findViewById(R.id.btn_check);
        btnShow = (Button) findViewById(R.id.btn_showtip);
        btnCall = (Button) findViewById(R.id.btn_call);
        btnCheck.setOnClickListener(this);
        btnShow.setOnClickListener(this);
        btnCall.setOnClickListener(this);
    }

    private boolean checkPermission(String permission) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ContextCompat.checkSelfPermission(this, CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
                Log.e("checkPermission", "PERMISSION_GRANTED" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
                return true;
            } else {
                Log.e("checkPermission", "PERMISSION_DENIED" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
                return false;
            }
        } else {
            Log.e("checkPermission", "M以下" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
            return true;
        }
    }

    private void startRequestPermision(String permission) {
        ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CALL_PHONE}, REQUEST_PERMISSION_CALL);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == REQUEST_PERMISSION_CALL) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    startActivity(callIntent);
                } else {
                    //如果拒絕授予權(quán)限,且勾選了再也不提醒
                    if (!shouldShowRequestPermissionRationale(permissions[0])) {

                        AlertDialog.Builder builder = new AlertDialog.Builder(this);
                        builder.setTitle("說明")
                                .setMessage("需要使用電話權(quán)限冯吓,進(jìn)行電話測(cè)試")
                                .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        showTipGoSetting();
                                    }
                                })
                                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        dialog.cancel();
                                        return;
                                    }
                                })
                                .create()
                                .show();
                    } else {
                        showTipGoSetting();
                    }
                }
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @Override
    public void onClick(View v) {
        int viewId = v.getId();
        switch (viewId) {
            case R.id.btn_check:
                int isGrantd = ContextCompat.checkSelfPermission(getApplicationContext(), CALL_PHONE);
                Toast.makeText(MainActivity.this, "isGrantd" + isGrantd, Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_showtip:
                boolean isShow = ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, CALL_PHONE);
                Toast.makeText(MainActivity.this, "isShow" + isShow, Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_call:
                call();
                break;
            default:
                break;
        }
    }

    private void call() {
        if (checkPermission(CALL_PHONE)) {
            startActivity(callIntent);
        } else {
            startRequestPermision(CALL_PHONE);
        }
    }

    /**
     * 用于在用戶勾選“不再提示”并且拒絕時(shí)倘待,再次提示用戶
     */
    private void showTipGoSetting() {
        new AlertDialog.Builder(this)
                .setTitle("電話權(quán)限不可用")
                .setMessage("請(qǐng)?jiān)?應(yīng)用設(shè)置-權(quán)限-中,允許APP使用電話權(quán)限來測(cè)試")
                .setPositiveButton("立即開啟", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // 跳轉(zhuǎn)到應(yīng)用設(shè)置界面
                        goToAppSetting();
                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.cancel();
                    }
                }).setCancelable(false).show();

    }

    /**
     * 打開Setting
     */
    private void goToAppSetting() {
        Intent intent = new Intent();
        intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", getPackageName(), null);
        intent.setData(uri);
        startActivityForResult(intent, 123);
    }
}

2组贺、使用第三方開源框架

目前用得比較多的動(dòng)態(tài)權(quán)限第三方庫PermissionsDispatcher凸舵,核心也是依然離不開系統(tǒng)的API,PermissionsDispatcher采用注解失尖,源碼很簡(jiǎn)單就不單獨(dú)分析了贞间,主要就是在編譯時(shí)生成代理類(代理類名稱格式為XxxxPermissionsDispatcher,其中Xxxx為@RuntimePermissions標(biāo)注的Activity或Fragment類名稱)雹仿,在Activity/Fragment中通過代理類去完成權(quán)限的檢查工作,并且把系統(tǒng)的權(quán)限處理回調(diào)回傳到代理類內(nèi)部整以,進(jìn)而完成觸發(fā)內(nèi)部的回調(diào)胧辽,在效率上和官方差不多,唯一的區(qū)別在于調(diào)用的形式:由于采用代理的形式公黑,不是直接調(diào)用系統(tǒng)API的checkSelfPermission來檢查權(quán)限邑商,取而代之的是代理類里的XxxWithCheck的方法(其中Xxx代表被@NeedsPermission標(biāo)注的方法名)摄咆;處理權(quán)限申請(qǐng)的結(jié)果依然是在系統(tǒng)的onRequestPermissionsResult方法內(nèi),但是我們必須把回調(diào)結(jié)果回傳到代理類人断,所以還必須用代理類調(diào)用他對(duì)應(yīng)的onRequestPermissionsResult方法

注解名稱 是否必須 說明
@RuntimePermissions ? 用于表面該Activity or Fragment 使用動(dòng)態(tài)代理來管理權(quán)限
@NeedsPermission ? 用于表明在什么時(shí)候需要權(quán)限吭从,一般用在方法聲明上
@OnShowRationale 應(yīng)用首次申請(qǐng)權(quán)限被拒絕,再次申請(qǐng)權(quán)限時(shí)恶迈,給出提示信息涩金,自動(dòng)回調(diào)標(biāo)注有@OnShowRationale的方法
@OnPermissionDenied 應(yīng)用首次申請(qǐng)權(quán)限,用戶拒絕暇仲,使用@OnPermissionDenied標(biāo)識(shí)的方法將作為回調(diào):
@OnNeverAskAgain 應(yīng)用非首次申請(qǐng)權(quán)限時(shí)步做,授權(quán)對(duì)話框會(huì)多出一個(gè)復(fù)選框不再詢問,系統(tǒng)自動(dòng)回調(diào)標(biāo)注有@OnNeverAskAgain注解的方法
  • 在Module下build.gradle引入依賴
dependencies {
  compile 'com.github.hotchemi:permissionsdispatcher:2.3.2'
  annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:2.3.2'//java8不能使用apt
}
  • 在需要?jiǎng)討B(tài)權(quán)限的Activity或者Fragment加上注解RuntimePermissions
  • 在涉及到動(dòng)態(tài)權(quán)限的方法加上注解@NeedsPermission

  • 在需要申請(qǐng)動(dòng)態(tài)權(quán)限的方法處奈附,使用代理類的XxxWithCheck方法開啟動(dòng)態(tài)權(quán)限申請(qǐng)的第一步

  • 重寫權(quán)限處理回調(diào)方法onRequestPermissionsResult全度,并且通過回傳到代理類內(nèi)部的onRequestPermissionsResult方法

  • 然后根據(jù)各自的業(yè)務(wù)需求,使用@OnShowRationale斥滤、@OnPermissionDenied将鸵、@OnNeverAskAgain來標(biāo)注對(duì)應(yīng)的回調(diào)方法并處理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市佑颇,隨后出現(xiàn)的幾起案子顶掉,更是在濱河造成了極大的恐慌,老刑警劉巖漩符,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件一喘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡嗜暴,警方通過查閱死者的電腦和手機(jī)凸克,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闷沥,“玉大人萎战,你說我怎么就攤上這事∮咛樱” “怎么了蚂维?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)路狮。 經(jīng)常有香客問我虫啥,道長(zhǎng),這世上最難降的妖魔是什么奄妨? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任涂籽,我火速辦了婚禮,結(jié)果婚禮上砸抛,老公的妹妹穿的比我還像新娘评雌。我一直安慰自己树枫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布景东。 她就那樣靜靜地躺著砂轻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斤吐。 梳的紋絲不亂的頭發(fā)上搔涝,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音曲初,去河邊找鬼体谒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛臼婆,可吹牛的內(nèi)容都是我干的抒痒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼颁褂,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼故响!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起颁独,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤彩届,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后誓酒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體樟蠕,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年靠柑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寨辩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡歼冰,死狀恐怖靡狞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情隔嫡,我是刑警寧澤甸怕,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站腮恩,受9級(jí)特大地震影響梢杭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秸滴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一式曲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦吝羞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至均澳,卻和暖如春恨溜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背找前。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工糟袁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人躺盛。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓项戴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親槽惫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子周叮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容