目前最流行的运行时权限请求框架PermissionsDispatcher、RxPermissions和easypermissions的使用和对比

本文出处
炎之铠csdn博客:http://blog.csdn.net/totond
炎之铠邮箱:yanzhikai_yjk@qq.com
本文demo地址:https://github.com/totond/PermissionsApplyDemo
本文原创,转载请注明本出处!
本文已授权公众号码个蛋发布。

前言

今天是2017年6月23日,到目前为止,Android6.0已经发布了两年的时间,随着时间的推移,Android6.0肯定会越来越普及,而6.0版本的一个重大改动就是增加了运行时权限(动态权限):一些危险的权限不单止要在AndroidMainifest文件声明,还要在运行的时候使用代码来申请,让用户同意才进行授权。
  由于Android自带的API使用起来(怎么使用就不写了,网上很多)比较麻烦,所以网上出现了一大堆简化这个过程的开源库,这里选取目前最流行的三个开源库(GitHub上Star最多)PermissionsDispatcher、RxPermissions和easypermissions进行体验并对比他们的用法,了解一下这三个库的功能,方便做出选择。

本文篇幅较长,如果不想看下面对这三个库的的使用的话,可以直接跳到结论。

准备工作

这里的demo使用一个Activity来测试每一种动态权限请求框架,分别测试它们的请求单个权限和请求多个权限的功能:



  这里检查权限的方法我采用了一个工具类封装(由于PermissionsDispatcher、RxPermissions都没带有单纯检查权限的功能,只有easypermissions有,这里用一个工具类封装一下检查权限的方法,返回我想要的字符串):

public class PermissionsLogUtils {
    private static StringBuffer logStringBuffer = new StringBuffer();
    // 查看权限是否已申请
    public static String checkPermissions(Context context,String... permissions) {
        logStringBuffer.delete(0,logStringBuffer.length());
        for (String permission : permissions) {
            logStringBuffer.append(permission);
            logStringBuffer.append(" is applied? \n     ");
            logStringBuffer.append(isAppliedPermission(context,permission));
            logStringBuffer.append("\n\n");
        }
        return logStringBuffer.toString();
    }


    //使用EasyPermissions查看权限是否已申请
    public static String easyCheckPermissions(Context context,String ... permissions) {
        logStringBuffer.delete(0,logStringBuffer.length());
        for (String permission : permissions) {
            logStringBuffer.append(permission);
            logStringBuffer.append(" is applied? \n     ");
            logStringBuffer.append(EasyPermissions.hasPermissions(context,permission));
            logStringBuffer.append("\n\n");
        }
        return logStringBuffer.toString();
    }


    // 查看权限是否已申请
    private static boolean isAppliedPermission(Context context,String permission) {
        return context.checkSelfPermission(permission) ==
                PackageManager.PERMISSION_GRANTED;
    }
}

然后,还要在AndroidMainifest文件声明demoAPP用到的权限,不在这里申明的话,无论后面在代码怎么动态申请,返回的结果都是权限拒绝并不再询问。

基于注解的PermissionsDispatcher

GitHub地址:https://github.com/hotchemi/PermissionsDispatcher
目前Star数:4.7k

集成方式

在app的build.gradle文件里:

dependencies {
  compile('com.github.hotchemi:permissionsdispatcher:${latest.version}') {
      // if you don't use android.app.Fragment you can exclude support for them
      exclude module: "support-v13"
  }
  annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:2.4.0'
}

或者直接使用插件添加依赖,怎么用插件?下面有讲。

使用

PermissionsDispatcher是基于注解来写的库,基本原理就是你给你写的一个方法加上一个注解,然后它就会在适当的时候调用这个被注解的方法(这种方法很有趣,让代码变得简洁和清晰,以后可以学一下)。
  目前PermissionsDispatcher支持5个注解,先看看GitHub主页的介绍:


  • @RuntimePermissions注解:这是必须使用的注解,用于标注在你想要申请权限的Activity或者Fragment上,如demo里面的PermissionsDispatcherActivity:
@RuntimePermissions
public class PermissionsDispatcherActivity extends AppCompatActivity implements View.OnClickListener {
}
  • @NeedsPermission注解:这也是必须使用的注解,用于标注在你要获取权限的方法,注解括号里面有参数,传入想要申请的权限。也就是说你获取了相应的权限之后就会执行这个方法
    //获取单个权限
    @NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
    public void getSingle() {
        Toast.makeText(this, "getSingle", Toast.LENGTH_SHORT).show();
    }

    //获取多个权限
    @NeedsPermission({Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO})
    public void getMulti() {
        Toast.makeText(this, "getMulti", Toast.LENGTH_SHORT).show();
    }
  • @OnShowRationale注解:这个不是必须的注解,用于标注申请权限前需要执行的方法,注解
    括号里面有参数,传入想要申请的权限,而且这个方法还要传入一个PermissionRequest对象,这个对象有两种方法:proceed()让权限请求继续,cancel()让请求中断。也就是说,这个方法会拦截你发出的请求,这个方法用于告诉用户你接下来申请的权限是干嘛的,说服用户给你权限。
 @OnShowRationale({Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO})
    //给用户解释要请求什么权限,为什么需要此权限
    void showRationale(final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setMessage("使用此功能需要WRITE_EXTERNAL_STORAGE和RECORD_AUDIO权限,下一步将继续请求权限")
                .setPositiveButton("下一步", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.proceed();//继续执行请求
                    }
                }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.cancel();//取消执行请求
                    }
                })
                .show();
    }

    @OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
        //给用户解释要请求什么权限,为什么需要此权限
    void showSingleRationale(final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setMessage("使用此功能需要WRITE_EXTERNAL_STORAGE,下一步将继续请求权限")
                .setPositiveButton("下一步", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.proceed();//继续执行请求
                    }
                }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                request.cancel();//取消执行请求
            }
        })
                .show();
    }
  • @OnPermissionDenied注解:这个也不是必须的注解,用于标注如果权限请求失败,但是用户没有勾选不再询问的时候执行的方法,注解括号里面有参数,传入想要申请的权限。也就是说,我们可以在这个方法做申请权限失败之后的处理,如像用户解释为什么要申请,或者重新申请操作等。
    @OnPermissionDenied({Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO})//一旦用户拒绝了
    public void multiDenied() {
        Toast.makeText(this, "已拒绝一个或以上权限", Toast.LENGTH_SHORT).show();
    }


    @OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)//一旦用户拒绝了
    public void StorageDenied() {
        Toast.makeText(this, "已拒绝WRITE_EXTERNAL_STORAGE权限", Toast.LENGTH_SHORT).show();
    }
  • @OnNeverAskAgain注解:这个也不是必须的注解,用于标注如果权限请求失败,而且用户勾选不再询问的时候执行的方法,注解括号里面有参数,传入想要申请的权限。也就是说,我们可以在这个方法做申请权限失败并选择不再询问之后的处理。例如,可以告诉作者想开启权限的就从手机设置里面开启。

注意,有些系统的不再询问勾选项是要用户拒绝授权一次才显示出来的。


    @OnNeverAskAgain({Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO})//用户选择的不再询问
    public void multiNeverAsk() {
        Toast.makeText(this, "已拒绝一个或以上权限,并不再询问", Toast.LENGTH_SHORT).show();
    }
    @OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)//用户选择的不再询问
    public void StorageNeverAsk() {
        Toast.makeText(this, "已拒绝WRITE_EXTERNAL_STORAGE权限,并不再询问", Toast.LENGTH_SHORT).show();
    }

注意,这些注解的方法都不能是private,原因看下面。

使用PermissionsDispatcher除了要实现注解之外,还要重写Activity的onRequestPermissionsResult()方法,在里面让一个PermissionsDispatcher执行回调。这个PermissionsDispatcher是什么来的呢?
  原来只要我们实现了@RuntimePermissions@NeedsPermission这两个必须的注解之后,再build一次project之后,编译器就会在在app\build\intermediates\classes\debug目录下与被注解的Activity同一个包下生成一个辅助类,名称为 “被注解的Activity的名称+PermissionsDispatcher” 的辅助类,用来调用被注解的Activity的方法(就是因为这个所以被注解的方法不能private,private方法的作用域不在其他的类)。所以,第一次用的话,要注解好之后,build一次,下面的方法里面的PermissionsDispatcherActivityPermissionsDispatcher才不会令AS报红。

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PermissionsDispatcherActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
    }

最后,申请权限的时候,调用辅助类的方法(名字从下面可以看出是被@OnPermissionDenied注解的方法加上WithCheck,参数是这个Activity或者Fragment)就行了:

    //申请单个权限
    PermissionsDispatcherActivityPermissionsDispatcher.getSingleWithCheck(this);
    //申请多个权限
    PermissionsDispatcherActivityPermissionsDispatcher.getMultiWithCheck(this);

来一个流程图:


插件

觉得这么多注解要自己一个一个弄不够方便,PermissionsDispatcher还在AndroidStudio做了插件,只要在setting设置里的plugins界面里搜索PermissionsDispatcher就可以安装了,安装完重启一下就能使用:


  • 在所需的Activity或者Fragment的代码里面右键,选择Generate,然后就可以选择Generate Runtime Permissions...(生成动态权限的生成)或者下面的Add PermissionsDispatcher dependencies(添加PermissionsDispatcher依赖)


  • 点击Generate Runtime Permissions...出现如下界面,输入方法名字就能生成,很简单粗暴:


生成了这个,如果你没onRequestPermissionsResult和@RuntimePermissions的话也会帮你加上:

    @NeedsPermission(Manifest.permission.CALL_PHONE)
    void call() {
    }

    @OnShowRationale(Manifest.permission.CALL_PHONE)
    void callshow(final PermissionRequest request) {
    }

效果

这里的效果录制时长有限,没有展示出全部的情况,想了解全部情况的可以clone下我的demo来试试。

谷歌推出的easypermissions

GitHub地址:https://github.com/googlesamples/easypermissions
目前Star数:3.5k

集成方式

在app的build.gradle文件里:

dependencies {
    compile 'pub.devrel:easypermissions:0.4.2'
}

使用

easypermissions是谷歌给出的一个运行时权限申请库(连谷歌自己都觉得自己的API用起来麻烦),下面我们来开始使用(下面步骤除了最后一步申请权限之外不分先后):

1.重写要申请权限的Activity或者Fragment的onRequestPermissionsResult()方法,在里面调用EasyPermissions.onRequestPermissionsResult(),实现回调。

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

2.让需要申请权限的Activity或者Fragment实现EasyPermissions.PermissionCallbacks接口,重写里面的方法:

public class EasyPermissionsActivity extends AppCompatActivity EasyPermissions.PermissionCallbacks{
}

  • onPermissionsGranted(int requestCode, List<String> list)方法:当权限被成功申请的时候执行回调,requestCode是代表你权限请求的识别码,list里面装着申请的权限的名字:
    @Override
    public void onPermissionsGranted(int requestCode, List<String> perms) {
        switch (requestCode){
            case 0:
                Toast.makeText(this, "已获取WRITE_EXTERNAL_STORAGE权限", Toast.LENGTH_SHORT).show();
                break;
            case 1:
                Toast.makeText(this, "已获取WRITE_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限", Toast.LENGTH_SHORT).show();
                break;
        }
    }
  • onPermissionsDenied(int requestCode, List<String> perms)方法:当权限申请失败的时候执行的回调,参数意义同上。在这个方法里面,官方还建议用EasyPermissions.somePermissionPermanentlyDenied(this, perms)方法来判断是否有权限被勾选了不再询问并拒绝,还提供了一个AppSettingsDialog来给我们使用,在这个对话框里面解释了APP需要这个权限的原因,用户按下是的话会跳到APP的设置界面,可以去设置权限(是不是很不要脸_),这个Dialog可以使用默认的样式new AppSettingsDialog.Builder(this).build().show(),也可以定制,像下面的一样:
    @Override
    public void onPermissionsDenied(int requestCode, List<String> perms) {
        //处理权限名字字符串
        StringBuffer sb = new StringBuffer();
        for (String str : perms){
            sb.append(str);
            sb.append("\n");
        }
        sb.replace(sb.length() - 2,sb.length(),"");
        
        switch (requestCode){
            case 0:
                Toast.makeText(this, "已拒绝权限" + perms.get(0), Toast.LENGTH_SHORT).show();
                break;
            case 1:
                Toast.makeText(this, "已拒绝WRITE_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限"+ perms.get(0), Toast.LENGTH_SHORT).show();
                break;
        }
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            Toast.makeText(this, "已拒绝权限" + sb + "并不再询问" , Toast.LENGTH_SHORT).show();
            new AppSettingsDialog
                    .Builder(this)
                    .setRationale("此功能需要" + sb + "权限,否则无法正常使用,是否打开设置")
                    .setPositiveButton("好")
                    .setNegativeButton("不行")
                    .build()
                    .show();
        }
    }

3.(可选)检查权限

easypermissions提供了EasyPermissions.hasPermissions(Context context, @NonNull String... perms)方法来检测一个或者多个权限是否被允许(当有一个权限被拒绝就会返回false),可能是因为Android自带的checkSelfPermission()比较方便(或者没这个必要?),PermissionsDispatcher和RxPermissions没有实现这个查询功能。这里我把它放到工具类里面封装了使用:

    //使用EasyPermissions查看权限是否已申请
    public static String easyCheckPermissions(Context context,String ... permissions) {
        logStringBuffer.delete(0,logStringBuffer.length());
        for (String permission : permissions) {
            logStringBuffer.append(permission);
            logStringBuffer.append(" is applied? \n     ");
            logStringBuffer.append(EasyPermissions.hasPermissions(context,permission));
            logStringBuffer.append("\n\n");
        }
        return logStringBuffer.toString();
    }

4.(可选)添加@AfterPermissionGranted()注解

要传入的参数是int类型的requestCode被这个注解标注的方法,当这个requestCode的请求成功的时候,会执行这个方法。其实就相当于在onPermissionsGranted()调用这个方法而已:

    @AfterPermissionGranted(0)
    private void afterGet(){
        Toast.makeText(this, "已获取权限,让我们干爱干的事吧!", Toast.LENGTH_SHORT).show();
    }

5.调用申请权限

最后,就是调用EasyPermissions.requestPermissions()方法来申请权限了:


    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_check:
                String str = PermissionsLogUtils.easyCheckPermissions(this,
                        Manifest.permission.RECORD_AUDIO,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE);
                tv_log.setText(str);
                break;
            case R.id.btn_getSingle:
                EasyPermissions.requestPermissions(this,
                        "接下来需要获取WRITE_EXTERNAL_STORAGE权限",
                        R.string.yes,
                        R.string.no,
                        0,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE);
                break;
            case R.id.btn_getMulti:
                EasyPermissions.requestPermissions(this,
                        "接下来需要获取WRITE_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限",
                        R.string.yes,
                        R.string.no,
                        1,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.RECORD_AUDIO);
                break;
        }

这个流程比较简单:


效果

这里的效果录制时长有限,没有展示出全部的情况,想了解全部情况的可以clone下我的demo来试试。

基于RxJava的RxPermissions

GitHub地址:https://github.com/tbruyelle/RxPermissions
目前Star数:3.8k

集成方式

在app的build.gradle文件里:

//Rxjava1.x用这个
dependencies {
    compile 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
}
//Rxjava2.x用这个
dependencies {
    compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
}

如果没有加入RxJava的还要加入它,我这里使用的是RxJava2,所以加上:

    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'io.reactivex.rxjava2:rxjava:2.1.0'

使用

下面的代码例子都是使用的RxJava2,和1版本有些不同,不过会用RxJava的基本都能看懂吧。

RxPermissions的使用比较简单清晰,就在申请权限的时候使用一个方法,再里面实现逻辑就行了:

  • 申请单个或者多个权限,不在乎是否不再询问和哪个权限申请失败,只要有一个失败就执行失败操作:
    //请求权限
    private void requestRxPermissions(String... permissions) {
        RxPermissions rxPermissions = new RxPermissions(this);
        rxPermissions.request(permissions).subscribe(new Consumer<Boolean>() {
            @Override
            public void accept(@NonNull Boolean granted) throws Exception {
                if (granted){
                    Toast.makeText(RxPermissionsActivity.this, "已获取权限", Toast.LENGTH_SHORT).show();
                }else {
                    Toast.makeText(RxPermissionsActivity.this, "已拒绝一个或以上权限", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
  • 申请多个权限,在乎是否不再询问和哪个权限申请失败:
    private void requestEachRxPermission(String... permissions) {
        RxPermissions rxPermissions = new RxPermissions(this);
        rxPermissions.requestEach(permissions).subscribe(new Consumer<Permission>() {
            @Override
            public void accept(@NonNull Permission permission) throws Exception {
                if (permission.granted) {
                    Toast.makeText(RxPermissionsActivity.this, "已获取权限"+ permission.name , Toast.LENGTH_SHORT).show();
                } else if (permission.shouldShowRequestPermissionRationale){
                    //拒绝权限请求
                    Toast.makeText(RxPermissionsActivity.this, "已拒绝权限"+ permission.name , Toast.LENGTH_SHORT).show();
                } else {
                    // 拒绝权限请求,并不再询问
                    // 可以提醒用户进入设置界面去设置权限
                    Toast.makeText(RxPermissionsActivity.this, "已拒绝权限"+ permission.name +"并不再询问", Toast.LENGTH_SHORT).show();
                }
            }
        });

    }

效果

这里的效果录制时长有限,没有展示出全部的情况,想了解全部情况的可以clone下我的demo来试试。

总结

共同点

三者都简化了Android6.0申请运行时权限的流程,比使用Android自带的API方便很多,可扩展性高。

不同点

功能 PermissionsDispatcher easypermissions RxPermissions
单独检查权限功能
申请权限前提示操作 有,可以自定义操作,弹出Dialog、Toast、SnackBar等等都行 有,而且定制了Dialog 无,需要自己实现
不再提示时的处理操作 有,而且可以使用Dialog让用户选择跳到APP设置界面
一次申请多个权限时,对单个失败的权限处理操作
结合RxJava
不能把方法私有

后话

了解到这些之后,我们应该就可以按照自己的需求来选择用什么样的动态权限请求库了。

以上经验经过本人demo测试,API Level 25,如有错漏,敬请指正。

参考资料

https://github.com/hotchemi/PermissionsDispatcher
https://github.com/googlesamples/easypermissions
https://github.com/tbruyelle/RxPermissions
http://www.cnblogs.com/duduhuo/p/6228426.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277

推荐阅读更多精彩内容