Android之版本更新

android中的版本更新是每一个APP的标配,记得最早的时候还是使用HttpUrlConnection+Handler来实现,如今时过境迁,特别是随着OkHttp、RxJava的流行,HttpUrlConnection+Handler的慢慢就用的少了,特别是在Android 7.0的手机上,系统对读取手机文件做了更进一步的限制,基于种种原因,最近又将版本更新做了一次整理,记录下来以防止后面遗忘。废话不多说,先上效果。

版本更新之通知栏.gif

这个效果是在点击立即更新之后,关闭AlertDialog,在后台下载,同时在通知栏显示下载进度(通知栏权限已打开)。

版本更新之界面显示进度.gif

这个效果是在点击立即更新之后,不关闭AlertDialog,直接显示下载进度。

具体来说,大致的流程如下:


版本更新流程图.png

进入正文

在正式编码之前,我们需要多方位思考,理清思路再编码,基于版本更新,首先我们需要知道几个点:
(1)下载的时候必须要有进度通知,能够很清晰明了知道下载了多少。
(2)有些时候我们需要对版本进行强制更新,所以这个功能必须有。
(3)当用户下载完成后但是一不小心没有点安装,那么下次再次更新的时候应该是直接安装(APK必须要下载到本地),而不是再次从网络获取。
(4)更新安装成功后最好能够自动删除原来的apk文件。

当然,在实际的开发过程中,可能还有一些更为详细的需求,这个就根据实际情况来定了。了解需求之后,接下来我们看看应该怎么去编写。

首先创建版本更新的类并声明相对应的变量
/**
 * author: zhoufan
 * data: 2021/8/10 10:04
 * content: 实现App的版本更新
 */
public class UpdateVersion {

    /**
     * 上下文
     */
    private Context mContext;

    /**
     * 版本号
     */
    private int mVersionCode;

    /**
     * 版本名称
     */
    private String mVersionName;

    /**
     * 更新提示内容
     */
    private String mUpdateContent;

    /**
     * 更新的APK对应的网络地址
     */
    private String mUpdateAPKUrl;

    /**
     * 更新的类型 1:正常更新,2:强制更新
     */
    private int mUpdateStatus = 1;

    /**
     * 更新的时候是否在前台开启进度条
     */
    private boolean isShowUpdateDialog;

    /**
     * 构造函数
     */
    public UpdateVersion(Context context, int versionCode, String versionName, String updateContent, String updateAPKUrl, int updateStatus, boolean isShowUpdateDialog) {
        this.mContext = context;
        this.mVersionCode = versionCode;
        this.mVersionName = versionName;
        this.mUpdateContent = updateContent;
        this.mUpdateAPKUrl = updateAPKUrl;
        this.mUpdateStatus = updateStatus;
        this.isShowUpdateDialog = isShowUpdateDialog;
    }
}

这里面声明了是否强制更新以及是否开启前台进度条,后面会使用到这两个参数。接下来就是我们的弹框了

/**
 * 弹出Dialog显示更新对话框
 */
private void createDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.bklistDialog);
    View updateView = LayoutInflater.from(mContext).inflate(R.layout.dialog_update_version, null);
    initDialogView(updateView);
    builder.setView(updateView);
    mDialog = builder.create();
    // 判断是否为强制更新
    if (mUpdateStatus == 2) {
        mDialog.setCanceledOnTouchOutside(false);
        mUpdateCancel.setVisibility(View.GONE);
        mDialog.setOnKeyListener(keyListener);
        mDialog.setCancelable(false);
    } else {
        mDialog.setCanceledOnTouchOutside(true);
    }
    // 显示
    mDialog.show();
}

/**
 * 获取View的控件并添加点击响应事件
 */
private void initDialogView(View updateView) {
    TextView newVersionName = updateView.findViewById(R.id.new_version_value);
    TextView newVersionContent = updateView.findViewById(R.id.update_content);
    mUpdateProgressLayout = updateView.findViewById(R.id.update_download_layout);
    mUpdateProgressbar = updateView.findViewById(R.id.update_download_progressbar);
    mUpdateDownloadNumber = updateView.findViewById(R.id.update_download_number);
    mUpdateCancel = updateView.findViewById(R.id.cancel);
    mUpdateSure = updateView.findViewById(R.id.sure);
    String versionName = "V" + mVersionName;
    newVersionName.setText(versionName);
    if (!TextUtils.isEmpty(mUpdateContent)) {
        newVersionContent.setText(mUpdateContent);
    } else {
        newVersionContent.setText(mContext.getResources().getString(R.string.updateNewVersion));
    }
    mUpdateSure.setText(mContext.getResources().getString(R.string.new_update));
}

在这里我们就需要判断是否为强制更新了,因为强制更新的话就表示我们在AlertDialog弹出后无法被关闭,同时我们的取消按钮也要被隐藏掉。

开始显示我们的AlertDialog
public void showUpdateDialog() {
    // 设置下载的安装路径
    mSavePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "Android/data/" + mContext.getPackageName() + File.separator + "apk";
    mSaveFileName = mSavePath + File.separator + mVersionCode + ".apk";
    createDialog();
    // 取消更新
    mUpdateCancel.setOnClickListener(v -> {
        if (mDialog != null && mDialog.isShowing())
            mDialog.dismiss();
    });
    // 确认更新
    mUpdateSure.setOnClickListener(v -> {
        File file = new File(mSaveFileName);
        if (file.exists()) {
            // apk文件已经存在,直接安装
            installApk(mSaveFileName);
        } else {
            // 后台开启下载功能,下载完毕后自动更新
            if (isShowUpdateDialog) {
                mUpdateProgressLayout.setVisibility(View.VISIBLE);
            } else {
                if (mDialog != null && mDialog.isShowing()) {
                    mDialog.dismiss();
                }
            }
            startDownloadApk();
        }
    });
}

首先确定好我们的安装路径,这里我是放置在我的项目的根目录下,然后相应的处理取消和立即更新的事件,取消对应的比较简单,直接将弹框关闭就好,立即更新稍微复杂一点,分为几步:
(1)首先判断要更新的apk文件存在不存在,如果存在的话直接安装apk文件即可。
(2)如果不存在的话,需要判断是在通知栏显示下载进度还是在界面直接显示下载进度。
(3)最后开启下载。

开始下载
/**
 * 使用OkHttp来进行网络下载
 */
private void startDownloadApk() {
    OkHttpClient okHttpClient = new OkHttpClient();
    Request request = new Request.Builder().url(mUpdateAPKUrl).get().build();
    Call call = okHttpClient.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(@NotNull Call call, @NotNull IOException e) {
            MyToast.showCenterSortToast(mContext, mContext.getString(R.string.download_fail));
        }

        @Override
        public void onResponse(@NotNull Call call, @NotNull Response response) {
            InputStream is = null;
            byte[] buf = new byte[2048];
            int len = 0;
            FileOutputStream fos = null;
            //储存下载文件的目录
            File dir = new File(mSavePath);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            File file = new File(mSaveFileName);
            try {
                is = response.body().byteStream();
                long total = response.body().contentLength();
                fos = new FileOutputStream(file);
                long sum = 0;
                while ((len = is.read(buf)) != -1) {
                    fos.write(buf, 0, len);
                    sum += len;
                    int progress = (int) (sum * 1.0f / total * 100);
                    if (mUpdateProgressLayout.getVisibility() == View.VISIBLE) {
                        //下载中更新进度条
                        Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                            mUpdateProgressbar.setProgress(integer);
                            String currentPercent = integer + "%";
                            mUpdateDownloadNumber.setText(currentPercent);
                        });
                    } else {
                        Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                            updateNotification(progress);
                        });
                    }
                }
                fos.flush();
                //下载完成
                Observable.just(100).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> installApk(mSaveFileName));
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                    if (fos != null) {
                        fos.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
}

开启下载这一块我采用的就是最新的OkHttp+RxJava,这个就不用多说了,一看就懂。最后当下载完成之后就可以直接安装APK了。

安装APK
/**
 * 安装apk
 */
private void installApk(String filePath) {
    if (mUpdateProgressLayout.getVisibility() == View.GONE) {
        NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        manager.cancel(1);
    }
    File file = new File(filePath);
    if (Build.VERSION.SDK_INT >= 24) {//判读版本是否在7.0以上
        String authority = mContext.getPackageName() + ".fileProvider";
        Uri apkUri = FileProvider.getUriForFile(mContext, authority, file);
        Intent install = new Intent(Intent.ACTION_VIEW);
        install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
        install.setDataAndType(apkUri, "application/vnd.android.package-archive");
        mContext.startActivity(install);
    } else {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(Uri.fromFile(file),
                "application/vnd.android.package-archive");
        mContext.startActivity(intent);
    }
}

安装APK稍微复杂一点,因为在android 7.0手机上面,系统做了一个更改。

在应用间共享文件

对于面向 Android 7.0 的应用,Android 框架执行的 StrictModeAPI 政策禁止在您的应用外部公开 file://URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content://URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件

这是android 官方的解释,什么意思呢?简单来说就是在android 7.0手机上面,原理的Uri模式玩不转了,系统建议采用FileProvider来实现。那么FileProvider怎么使用呢?也很简单。

FileProvider使用步骤

(1)在res目录下新建xml文件夹,在xml文件夹下面新建一个provider_paths.xml文件。
(2)在provider_paths.xml文件里面声明

<?xml version="1.0" encoding="utf-8"?>
 <resources>
    <paths>
        <external-path
            name="apk"  // 名字随便取
            path="" />  // path为空代表的是整个文件目录
    </paths>
</resources>

(3)在清单文件里面进行声明

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.steven.sunworld.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>

注意:authorities为包名+fileProvider,中间用 . 来连接,如上所示。
(4)在使用FileProvider.getUriForFile(mContext, authority, file);的时候authority的取值必须和你在清单文件里面设置的一致,file就是你要操作的文件。

到这里,版本更新的所有环节都基本结束了,最后贴上完整代码

/**
 * author: zhoufan
 * data: 2021/8/10 10:04
 * content: 实现App的版本更新
 */
public class UpdateVersion {

    /**
     * 上下文
     */
    private Context mContext;

    /**
     * 版本号
     */
    private int mVersionCode;

    /**
     * 版本名称
     */
    private String mVersionName;

    /**
     * 更新提示内容
     */
    private String mUpdateContent;

    /**
     * 更新的APK对应的网络地址
     */
    private String mUpdateAPKUrl;

    /**
     * 更新的类型 1:正常更新,2:强制更新
     */
    private int mUpdateStatus = 1;

    /**
     * 更新的时候是否在前台开启进度条
     */
    private boolean isShowUpdateDialog;

    /**
     * 更新弹出的确认更新确认框
     */
    private AlertDialog mDialog;

    /**
     * APK下载之后保存的地址
     */
    private String mSavePath;

    /**
     * 保存的文件名
     */
    private String mSaveFileName;

    /**
     * 当前下载的进度
     */
    private int mDownloadProgress;

    private RelativeLayout mUpdateProgressLayout;
    private ProgressBar mUpdateProgressbar;
    private TextView mUpdateDownloadNumber;
    private TextView mUpdateCancel;
    private TextView mUpdateSure;

    public UpdateVersion(Context context, int versionCode, String versionName, String updateContent, String updateAPKUrl, int updateStatus, boolean isShowUpdateDialog) {
        this.mContext = context;
        this.mVersionCode = versionCode;
        this.mVersionName = versionName;
        this.mUpdateContent = updateContent;
        this.mUpdateAPKUrl = updateAPKUrl;
        this.mUpdateStatus = updateStatus;
        this.isShowUpdateDialog = isShowUpdateDialog;
    }

    public void showUpdateDialog() {
        // 设置下载的安装路径
        mSavePath = Environment.getExternalStorageDirectory().getPath() + File.separator + "Android/data/" + mContext.getPackageName() + File.separator + "apk";
        mSaveFileName = mSavePath + File.separator + mVersionCode + ".apk";
        createDialog();
        // 取消更新
        mUpdateCancel.setOnClickListener(v -> {
            if (mDialog != null && mDialog.isShowing())
                mDialog.dismiss();
        });
        // 确认更新
        mUpdateSure.setOnClickListener(v -> {
            File file = new File(mSaveFileName);
            if (file.exists()) {
                // apk文件已经存在,直接安装
                installApk(mSaveFileName);
            } else {
                // 后台开启下载功能,下载完毕后自动更新
                if (isShowUpdateDialog) {
                    mUpdateProgressLayout.setVisibility(View.VISIBLE);
                } else {
                    if (mDialog != null && mDialog.isShowing()) {
                        mDialog.dismiss();
                    }
                }
                startDownloadApk();
            }
        });
    }

    /**
     * 弹出Dialog显示更新对话框
     */
    private void createDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.bklistDialog);
        View updateView = LayoutInflater.from(mContext).inflate(R.layout.dialog_update_version, null);
        initDialogView(updateView);
        builder.setView(updateView);
        mDialog = builder.create();
        // 判断是否为强制更新
        if (mUpdateStatus == 2) {
            mDialog.setCanceledOnTouchOutside(false);
            mUpdateCancel.setVisibility(View.GONE);
            mDialog.setOnKeyListener(keyListener);
            mDialog.setCancelable(false);
        } else {
            mDialog.setCanceledOnTouchOutside(true);
        }
        // 显示
        mDialog.show();
    }

    /**
     * 获取View的控件并添加点击响应事件
     */
    private void initDialogView(View updateView) {
        TextView newVersionName = updateView.findViewById(R.id.new_version_value);
        TextView newVersionContent = updateView.findViewById(R.id.update_content);
        mUpdateProgressLayout = updateView.findViewById(R.id.update_download_layout);
        mUpdateProgressbar = updateView.findViewById(R.id.update_download_progressbar);
        mUpdateDownloadNumber = updateView.findViewById(R.id.update_download_number);
        mUpdateCancel = updateView.findViewById(R.id.cancel);
        mUpdateSure = updateView.findViewById(R.id.sure);
        String versionName = "V" + mVersionName;
        newVersionName.setText(versionName);
        if (!TextUtils.isEmpty(mUpdateContent)) {
            newVersionContent.setText(mUpdateContent);
        } else {
            newVersionContent.setText(mContext.getResources().getString(R.string.updateNewVersion));
        }
        mUpdateSure.setText(mContext.getResources().getString(R.string.new_update));
    }

    /**
     * 禁用返回键
     */
    private DialogInterface.OnKeyListener keyListener = (dialog, keyCode, event) -> keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0;

    /**
     * 安装apk
     */
    private void installApk(String filePath) {
        if (mUpdateProgressLayout.getVisibility() == View.GONE) {
            NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
            manager.cancel(1);
        }
        File file = new File(filePath);
        if (Build.VERSION.SDK_INT >= 24) {//判读版本是否在7.0以上
            String authority = mContext.getPackageName() + ".fileProvider";
            Uri apkUri = FileProvider.getUriForFile(mContext, authority, file);
            Intent install = new Intent(Intent.ACTION_VIEW);
            install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加这一句表示对目标应用临时授权该Uri所代表的文件
            install.setDataAndType(apkUri, "application/vnd.android.package-archive");
            mContext.startActivity(install);
        } else {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setDataAndType(Uri.fromFile(file),
                    "application/vnd.android.package-archive");
            mContext.startActivity(intent);
        }
    }

    /**
     * 使用OkHttp来进行网络下载
     */
    private void startDownloadApk() {
        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder().url(mUpdateAPKUrl).get().build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                MyToast.showCenterSortToast(mContext, mContext.getString(R.string.download_fail));
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) {
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                //储存下载文件的目录
                File dir = new File(mSavePath);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File file = new File(mSaveFileName);
                try {
                    is = response.body().byteStream();
                    long total = response.body().contentLength();
                    fos = new FileOutputStream(file);
                    long sum = 0;
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                        sum += len;
                        int progress = (int) (sum * 1.0f / total * 100);
                        if (mUpdateProgressLayout.getVisibility() == View.VISIBLE) {
                            //下载中更新进度条
                            Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                                mUpdateProgressbar.setProgress(integer);
                                String currentPercent = integer + "%";
                                mUpdateDownloadNumber.setText(currentPercent);
                            });
                        } else {
                            Observable.just(progress).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> {
                                updateNotification(progress);
                            });
                        }
                    }
                    fos.flush();
                    //下载完成
                    Observable.just(100).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(integer -> installApk(mSaveFileName));
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (is != null) {
                            is.close();
                        }
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

    /**
     * 通知栏更新
     */
    private void updateNotification(int progress) {
        if (NotificationManagerCompat.from(mContext).areNotificationsEnabled()) {
            if (progress > mDownloadProgress) {
                mDownloadProgress = progress;
                NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
                if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                    NotificationChannel notificationChannel = new NotificationChannel("1", "update", NotificationManager.IMPORTANCE_HIGH);
                    notificationChannel.setSound(null, null);
                    notificationChannel.enableLights(false);
                    notificationChannel.setLightColor(Color.RED);
                    notificationChannel.setShowBadge(false);
                    notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
                    manager.createNotificationChannel(notificationChannel);
                }
                // 创建自定义的样式布局
                RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.download_progress_state_view);
                // 在这里可以设置RemoteView的初始布局
                NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "1");
                builder.setCustomContentView(remoteViews);
                // 不可以自动取消
                builder.setAutoCancel(false);
                // 必须要设置,否则在android 10手机上面会闪退
                builder.setSmallIcon(R.mipmap.ic_launcher_round);
                // 设置通知的优先级
                builder.setPriority(NotificationCompat.PRIORITY_MAX);
                Notification nn = builder.build();
                nn.contentView = remoteViews;
                nn.icon = R.mipmap.ic_launcher;
                remoteViews.setImageViewResource(R.id.download_progress_img, R.mipmap.ic_launcher);
                String loadShow = mContext.getResources().getString(R.string.app_download_show);
                remoteViews.setTextViewText(R.id.download_progress_name, loadShow);
                remoteViews.setProgressBar(R.id.download_progressbar, 100, mDownloadProgress, false);
                remoteViews.setTextViewText(R.id.download_progress_text, mDownloadProgress + "%");
                manager.notify(1, nn);
            }
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 142,461评论 1 300
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,089评论 1 256
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 94,078评论 0 212
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,938评论 0 175
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,698评论 1 255
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,733评论 1 175
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,340评论 2 268
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,138评论 0 165
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,949评论 6 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,493评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,269评论 2 214
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,591评论 1 229
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,216评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,107评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,485评论 3 204
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,615评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,006评论 0 167
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,496评论 2 230
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,572评论 2 230

推荐阅读更多精彩内容