Android中Apk的下载与安装

原文地址:https://www.jianshu.com/p/a6cad97ea54f

相信很多应用都是采用内部下载的方式,这样的体验肯定比跳转到浏览器好得多!而应用商店审核周期长,无法实时更新最新应用!所以内部下载更新就显得尤为重要!

1.要美观好看,给用户实时的反馈下载情况:

界面体现为下载百分比%,下载速度 kb/s,圆环进度

2.下载完成后要自动安装:

Android6.0,需要动态申请权限,读取写入。
Android7.0,需要通过fileprovider的方式创建Uri
Android8.0,需要申请【安装未知来源应用权限】

针对第一个问题,我们采用自定义View来完成,可定制化高,样式想怎样改怎样改。而第二个问题就需要我们队权限的申请和对路径创建方式的注意了。

先来一个效果图:


下载安装apk.gif

这个其实就是简单的一个Dialog了,中间的狮子图片是应用LOGO,下面的正在下载就是一个文字描述,难点主要是中间的进度圆圈和圆圈点上的行星进度。

1.1新建一个Dialog弹出框:DownloadCircleDialog
public class DownloadCircleDialog extends Dialog {
    public DownloadCircleDialog(Context context) {
        super(context, R.style.Theme_Ios_Dialog);
    }
    DownloadCircleView circleView;
    TextView tvMsg;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.download_circle_dialog_layout);
        this.setCancelable(false);//设置点击弹出框外部,无法取消对话框
        circleView = findViewById(R.id.circle_view);
        tvMsg = findViewById(R.id.tv_msg);
    }
    public void setProgress(int progress) {
        circleView.setProgress(progress);
    }
    public void setMsg(String msg){
        tvMsg.setText(msg);
    }
}

在style.xml中写入样式:Theme.Ios.Dialog

<!-- IOSDialog -->
    <style name="Theme.Ios.Dialog" parent="@android:style/Theme.Dialog">
        <!-- Dialog的windowFrame框为无 -->
        <!-- <item name="android:windowFrame">@null</item> -->
        <!-- 边框 -->
        <item name="android:windowIsFloating">true</item>
        <!-- 是否浮现在activity之上 -->
        <item name="android:windowIsTranslucent">true</item>
        <!-- 半透明 -->
        <item name="android:windowNoTitle">true</item>
        <!-- 设置dialog的背景 -->
        <item name="android:windowBackground">@android:color/transparent</item>
        <!-- 背景是否模糊显示 -->
        <item name="android:backgroundDimEnabled">true</item>
        <!-- 模糊 -->
        <item name="android:textColorPrimaryInverse">@android:color/black</item>
    </style>

layout中的布局文件:download_circle_dialog_layout

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/layout_notice"
    android:background="@color/transparent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <com.qiqia.duosheng.custom.DownloadCircleView
        android:id="@+id/circle_view"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_logo"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_centerInParent="true"
        android:layout_marginBottom="24dp"
        android:src="@mipmap/logo"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_msg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/iv_logo"
        android:layout_centerHorizontal="true"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="正在下载..."
        android:textSize="12sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/iv_logo" />
</android.support.constraint.ConstraintLayout>
1.2中间下载进度圆圈:DownloadCircleView
public class DownloadCircleView extends View {
    Paint mBgPaint;
    Paint mStepPaint;
    Paint mTxtCirclePaint;
    Paint mTxtPaint;
    int outsideRadius=DpPxUtils.dp2px(100);
    int progressWidth =DpPxUtils.dp2px(2);
    float progressTextSize  = DpPxUtils.dp2px(12);
    Context context;
    public DownloadCircleView(Context context) {
        super(context);
    }
    public DownloadCircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
    public DownloadCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width;
        int height;
        int size = MeasureSpec.getSize(widthMeasureSpec);
        int mode = MeasureSpec.getMode(widthMeasureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            width = size;
        } else {
            width = (int) ((2 * outsideRadius) + progressWidth);
        }
        size = MeasureSpec.getSize(heightMeasureSpec);
        mode = MeasureSpec.getMode(heightMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            height = size;
        } else {
            height = (int) ((2 * outsideRadius) + progressWidth);
        }
        setMeasuredDimension(width, height);
    }

    private void init(Context context) {
        int progressColor = Color.parseColor("#FF5836");//进度球颜色
        this.context = context;
        //灰色背景圆环
        mBgPaint = new Paint();
        mBgPaint.setStrokeWidth(progressWidth);
        mBgPaint.setColor(Color.GRAY);
        this.mBgPaint.setAntiAlias(true);
        this.mBgPaint.setStyle(Paint.Style.STROKE); //绘制空心圆
        //进度圆环
        mStepPaint = new Paint();
        mStepPaint.setStrokeWidth(progressWidth);
        mStepPaint.setColor(progressColor);
        this.mStepPaint.setAntiAlias(true);
        this.mStepPaint.setStyle(Paint.Style.STROKE); //绘制空心圆
        //进度卫星球
        mTxtCirclePaint = new Paint();
        mTxtCirclePaint.setColor(progressColor);
        this.mTxtCirclePaint.setAntiAlias(true);
        this.mTxtCirclePaint.setStyle(Paint.Style.FILL); //绘制实心圆
        //进度文字5%
        mTxtPaint = new Paint();
        mTxtPaint.setTextSize(progressTextSize);
        mTxtPaint.setColor(Color.WHITE);
        this.mTxtPaint.setAntiAlias(true);

    }
    float maxProgress=100f;
    float progress  =0f;

    public void setProgress(float progress) {
        this.progress = progress;
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //灰色圆圈
        int circlePoint = getWidth() / 2;
        canvas.drawCircle(circlePoint, circlePoint, outsideRadius, mBgPaint); //画出圆
        //进度
        RectF oval = new RectF();
        oval.left=circlePoint - outsideRadius;
        oval.top=circlePoint - outsideRadius;
        oval.right=circlePoint + outsideRadius;
        oval.bottom=circlePoint + outsideRadius;
        float range = 360 * (progress / maxProgress);
        canvas.drawArc(oval, -90,  range, false, mStepPaint);  //根据进度画圆弧
        //轨道圆和文字
        double x1 = circlePoint + outsideRadius * Math.cos((range-90) * 3.14 / 180);
        double y1 = circlePoint + outsideRadius * Math.sin((range-90) * 3.14 / 180);
        canvas.drawCircle((float) x1, (float) y1, progressTextSize*1.3f, mTxtCirclePaint);
        String txt = (int) progress + "%";
        float strwid  = mTxtPaint.measureText(txt);//直接返回参数字符串所占用的宽度
        canvas.drawText(txt,(float) x1-strwid/2, (float) y1+progressTextSize/2-progressWidth/2,mTxtPaint);

    }
}

这样,下载样式基本就完成了,每次通过方法setProgress和setMsg就可以去设置下载的进度和速度了!

下面说说下载:采用 okhttp来下载apk文件,通过 ProgressManager来监听进度,通过 AndPermission简化动态申请权限

2.1首先我们写个下载工具类:DownloadUtils
import android.os.Handler;
import android.os.Looper;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import me.jessyan.progressmanager.ProgressListener;
import me.jessyan.progressmanager.ProgressManager;
import me.jessyan.progressmanager.body.ProgressInfo;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class DownloadUtils {
    private static DownloadUtils instance;
    private OkHttpClient okHttpClient;
    private Handler mHandler; //所有监听器在 Handler 中被执行,所以可以保证所有监听器在主线程中被执行

    public static DownloadUtils getInstance() {
        if (instance == null) instance = new DownloadUtils();
        return instance;
    }

    private DownloadUtils() {
        this.mHandler = new Handler(Looper.getMainLooper());
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        okHttpClient = ProgressManager.getInstance().with(builder).build();
    }

    public interface OnDownloadListener{
        /**
         * 下载成功
         */
        void onDownloadSuccess();

        /**
         * @param progress 下载进度
         */
        void onDownloading(ProgressInfo progress);

        /**
         * 下载失败
         */
        void onDownloadFailed();
    }

    /**
     * @param url      下载连接
     * @param saveDir  储存下载文件的SDCard目录
     * @param listener 下载监听
     */
    public void download(final String url, final String saveDir, final String saveName, final OnDownloadListener listener) {
        Request request = new Request.Builder().url(url).build();


        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                // 下载失败
                listener.onDownloadFailed();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                // Okhttp/Retofit 下载监听
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                // 储存下载文件的目录
                try {
                    is = response.body().byteStream();
                    File file = new File(saveDir, saveName);
                    if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
                    fos = new FileOutputStream(file);
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                    }
                    fos.flush();
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            // 下载完成
                            listener.onDownloadSuccess();
                        }
                    });

                } catch (Exception e) {
                    Log.e("下载异常", e.getMessage());
                    listener.onDownloadFailed();
                } finally {
                    try {
                        if (is != null) is.close();
                    } catch (IOException e) {
                    }
                    try {
                        if (fos != null) fos.close();
                    } catch (IOException e) {
                    }
                }
            }
        });
        ProgressManager.getInstance().addResponseListener(url, new ProgressListener() {
            @Override
            public void onProgress(ProgressInfo progressInfo) {
                listener.onDownloading(progressInfo);
            }

            @Override
            public void onError(long l, Exception e) {
                listener.onDownloadFailed();
            }
        });
    }

}

上面通过ProgressManager.getInstance().with(builder).build();创建的okHttpClient也就相当于把ProgressManager加入到了OkHttp中,这样ProgressManager监听才会有效!

3.使用
  • a .首先我们需要加入安装位置来源权限到AndroidManifest.xml中:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
  • b. 然后我们在需要下载apk的页面中加入:
   DownloadCircleDialog  dialogProgress;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dialogProgress = new DownloadCircleDialog(this);
        showNewVersion();
    }
  //1.权限申请,通过后开始下载
    private void showNewVersion() {
        AndPermission.with(this)
                .runtime()
                .permission(Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE)
                .onGranted(data -> {
                    L.e("以获得权限" + data.toString());
                    new AlertDialog.Builder(this).setTitle("软件更新").setMessage("发现新版本")
                            .setPositiveButton("确定", (dialog, which) -> {
                                String down_url = "https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk";
                                downloadApk(MainActivity.this, down_url);
                            })
                            .setNegativeButton("取消",null).show();

                })
                .onDenied(data -> L.e("未获得权限" + data.toString())).start();
    }
    //2.开始下载apk
 
    public void downloadApk(final Activity context, String down_url) {
        dialogProgress.show();
        DownloadUtils.getInstance().download(down_url, SdUtils.getDownloadPath(), "QQ.apk", new DownloadUtils.OnDownloadListener() {
            @Override
            public void onDownloadSuccess() {
                dialogProgress.dismiss();
                L.i("恭喜你下载成功,开始安装!==" + SdUtils.getDownloadPath() + "QQ.apk");
                ToastUtil.showShort("恭喜你下载成功,开始安装!");
                String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";
                installApkO(MainActivity.this, successDownloadApkPath);
            }
            @Override
            public void onDownloading(ProgressInfo progressInfo) {
                dialogProgress.setProgress(progressInfo.getPercent());
                boolean finish = progressInfo.isFinish();
                if (!finish) {
                    long speed = progressInfo.getSpeed();
                    dialogProgress.setMsg("(" + (speed > 0 ? FormatUtils.formatSize(context, speed) : speed) + "/s)正在下载...");
                } else {
                    dialogProgress.setMsg("下载完成!");
                }
            }
            @Override
            public void onDownloadFailed() {
                dialogProgress.dismiss();
                ToastUtil.showShort("下载失败!");
            }
        });

    }
     // 3.下载成功,开始安装,兼容8.0安装位置来源的权限
    private void installApkO(Context context, String downloadApkPath) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //是否有安装位置来源的权限
            boolean haveInstallPermission = getPackageManager().canRequestPackageInstalls();
            if (haveInstallPermission) {
                L.i("8.0手机已经拥有安装未知来源应用的权限,直接安装!");
                AppUtils.installApk(context, downloadApkPath);
            } else {
                new CakeResolveDialog(context, "安装应用需要打开安装未知来源应用权限,请去设置中开启权限", new CakeResolveDialog.OnOkListener() {
                    @Override
                    public void onOkClick() {
                        Uri packageUri = Uri.parse("package:"+ AppUtils.getAppPackageName());
                        Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageUri);
                        startActivityForResult(intent,10086);
                    }
                }).show();
            }
        } else {
            AppUtils.installApk(context, downloadApkPath);
        }
    }
    //4.开启了安装未知来源应用权限后,再次进行步骤3的安装。
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 10086) {
            L.i("设置了安装未知应用后的回调。。。");
            String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";
            installApkO(MainActivity.this, successDownloadApkPath);
        }
    }

上面代码第一段showNewVersion就是对读写权限的申请,downloadApk下载进度的监听,下载完成通过installApkO来安装,installApkO中判断如果没有安装位置来源的权限就跳转到设置开启安装位置来源权限的页面,设置完成后回到这个页面继续安装!
上面的AppUtils.installApk是我写的一个工具方法,

public static  void installApk(Context context,String downloadApk) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        File file = new File(downloadApk);
        L.i("安装路径=="+downloadApk);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        } else {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            Uri uri = Uri.fromFile(file);
            intent.setDataAndType(uri, "application/vnd.android.package-archive");
        }
        context.startActivity(intent);

    }

判断如果>=7.0就通过fileprovider来创建Uri,避免安装出现解析包异常!6.0的读写权限通过showNewVersion()方法进行了申明,8.0的安装未知来源应用权限在installApkO进行了判断申请,从而使安装APK兼容了6,7,8!9.0的机子还没用过,不过如果没改动,应该也可以安装!

这样一个apk从开始下载,进度显示到安装就完成了!说起来就是一个apk的下载安装,但是其实代码量和坑还是挺多的:

坑一:最开始没有使用ProgressManager来进度监听,而是在download方法的写文件中监听下载进度:

public void download(final String url, final String saveDir, final OnDownloadListener listener) {
        Request request = new Request.Builder().url(url).build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                // 下载失败
                listener.onDownloadFailed();
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                // 储存下载文件的目录
                String savePath = isExistDir(saveDir);
                try {
                    is = response.body().byteStream();
                    long total = response.body().contentLength();
                    File file = new File(savePath, getNameFromUrl(url));
                    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);
                        // 下载中
                        listener.onDownloading(progress);
                    }
                    fos.flush();
                    // 下载完成
                    listener.onDownloadSuccess();
                } catch (Exception e) {
                    listener.onDownloadFailed();
                } finally {
                    try {
                        if (is != null)
                            is.close();
                    } catch (IOException e) {
                    }
                    try {
                        if (fos != null)
                            fos.close();
                    } catch (IOException e) {
                    }
                }
            }
        });
    }

这样监听到的下载进度并不是真的下载进度,而是下载文件后写入到手机的速度,体现到界面就是最开始下载进度是0%一直不动,然后2秒钟就从0%转圈到100%,就下载完成了,给用户感觉一点都不真实!

坑二:下载完成后解析包错误:

a.主要原因就是没有使用Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);的方式来创建Uri 安装,
b.还有就是因为文件名字不正确,最开始我的download方法中没有saveName方法,而是通过下载地址截取最后的“/”来写入文件名的,但是有的下载地址并不是以apk结尾,从而导致解析包错误!
c.还有就是根本没有这个文件路径,从而导致写错误,所以在download方法中写入本地文件前我加入了如果没有文件路径就先创建当前路径

File file = new File(saveDir, saveName);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();

坑三:下载出错:CertPathValidatorException: Trust anchor for certification path not found,(上面的代码没有加入,因为每个人的OkHttpClient都不同,我是写了个工具类来创建的OkHttpClient,所以工具类中加入了进度读取和跳过SSL验证的,由于自私原因,大家自己加吧。)
相信有的应用是放在自己的服务器的,而又有https,但很多都是没有证书的,导致下载不了!所以我们就需要Okhttp绕过证书验证,参考:
https://blog.csdn.net/O0mm0O/article/details/76686917
坑四:无法安装:
Android8.0需要安装权限:<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>,同时7.0以上还需要安装未知来源的权限,不然也无法安装,可以参考:
https://www.jianshu.com/p/a6209440a518
坑五:无法安装:由于配置xml中的provider_paths.xml只写了

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="images"
        path="test/"/>
</paths>

导致无法读取路径而无法安装,因为我们是下载到Download的所以还要加入:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="images"
        path="test/"/>
    <external-path
        name="download"
        path="Download/"/>
</paths>

这样才算完整!关于fileprovider路径介绍:
https://blog.csdn.net/leilifengxingmw/article/details/57405908
坑六:安装完成后闪退,或安装完成后点击打开闪退
在大部分手机上没有问题,但在Vivo X9上居然安装完成后闪退了,我觉得这应该是应用已经死了,所有安装完成后立即启动有问题,而其他手机就没有问题,我觉得还是Vivo手机的厂商定制问题!所以解决办法就是安装的时候启动一个新的任务栈来安装:

 public static  void installApk(Context context,String downloadApk) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        File file = new File(downloadApk);
        L.i("安装路径=="+downloadApk);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        } else {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            Uri uri = Uri.fromFile(file);
            intent.setDataAndType(uri, "application/vnd.android.package-archive");
        }
        context.startActivity(intent);

    }

其中:intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);就是关键啦!

下个应用如果还需要下载,我就把上面的代码复制进去,免得每次都要去找写了下载安装APK代码的项目,然后一部分一部分去查找复制!这么多代码记肯定是记不住的,这辈子都不可能记住的,所以写这里方便下次Copy!

PS:由于一些同学要工具,我又不可能要一个工具发一个。所以我把我收集的工具类发出来,需要什么工具可以自行筛选:SmallUtils

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