异步网络下载案例(AsyncTask + 前台Service + OkHttp + Android8.0的Notification适配注意)

ServiceBestPractice项目(模块)GitHub地址

案例代码逻辑概述

  • interface DownloadListener 回调机制核心接口

  • class DownloadTask extends AsyncTask<String, Integer, Integer>
    描述异步网络下载逻辑(网络请求,文件线上状态处理,文件本地状态处理,文件写入本地),
    抽象调用接口对象方法;

  • class DownloadService extends Service

    • 匿名类方式具体实现回调接口的方法 而后将这个匿名类放入一个接口类实例
      (回调方法负责状态处理,方式是:Toast、对downloadTask归为、开关通知等);(Binder类定义中)
    • 实例化DownloadTask,把实现好的接口类实例传进去DownloadTask的构造器;
      为外部(如Activity)调用准备好业务Binder实例class DownloadBinder extends Binder
    • 封装NotificationManager以及NotificationgetNotificationManager() getNotification(String title, int progress)
    • Binder类定义中执行DownloadTask实例downloadTask.execute(downloadUrl);
      并完成开关通知、删除文件逻辑;
getNotificationManager();// 配置 NotificationManager!!!!!!!!
startForeground(1, getNotification("Downloading...", 0));
------------------------------------
file.delete();
...
getNotificationManager().cancel(1);
stopForeground(true);
  • class MainActivity extends AppCompatActivity implements View.OnClickListener
    • 实例化UI(主要是按钮);
    • 启动、绑定、解绑服务;startForegroundService(intent); startService(intent); unbindService(connection);
    • 运行时权限动态申请;
    • 准备监听事件,事件中通过Servicebinder对象来产生业务;




开始实战

  • 创建ServiceBestPractice项目或模块。
    首先在/build.gradle中dependencies下添加OKHttp库依赖(网络相关功能使用):
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
  • 运用回调机制编程,
    定义一个回调接口,
    用于对下载过程中的各种状态进行监听和回调:
    (在DownloadTask的onPostExecute中抽象调用,
    在DownloadService中具体实现)
public interface DownloadListener {
    void onProgress(int progress);//通知当前下载进度
    void onSuccess();//通知下载成功事件
    void onFailed();//通知下载失败事件
    void onPaused();//通知下载暂停事件
    void onCanceled();//通知下载取消事件
}
  • 编写下载功能,新建DownloadTask类继承自AsyncTask
/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/11/9 17:29
 *     desc   :三个泛型参数,
 *     第一个表示在执行AsyncTask时需传入一个字符串参数给后台任务,
 *     第二个使用整型数据最为进度显示单位,
 *     第三个表示使用整型数据来反馈结果执行
 * </pre>
 */
public class DownloadTask extends AsyncTask<String, Integer, Integer> {

    //定义四个整型常量分别表示下载的不同状态
    public static final int TYPE_SUCCESS = 0;//表示下载取消
    public static final int TYPE_FAILED = 1;//表示下载失败
    public static final int TYPE_PAUSE = 2;//表示下载暂停
    public static final int TYPE_CANCELED = 3;//表示下载取消

    private DownloadListener listener;

    //取消位以及暂停位
    // 由外部调用,在doInBackground()中生效
    private boolean isCanceled = false;
    private boolean isPaused = false;

    private int lastProgress;//记录上次的进度

    //构造方法
    public DownloadTask(DownloadListener listener){
        //将下载的状态通过此参数进行回调,此处负责调用,外部具体编写逻辑
        this.listener = listener;
    }

    //在后台执行具体的下载逻辑
    // String... params:可变长参数列表,必须是String类型,转化为数组处理
    @Override
    protected Integer doInBackground(String... params) {

        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;

        try{

            long downloadedLength = 0;//记录 已下载的文件 长度!!!!!!!

            String downloadUrl = params[0];//获取 下载的URL地址!!!!!!!!!

            // 根据URL地址解析出下载的文件名
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            // 指定文件下载到 Environment.DIRECTORY_DOWNLOADS 目录下,即SD卡的Download目录
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            //用以上的 文件下载路径 以及 要下载的文件名 得到 file句柄!!!!!!!!!!!!
            file = new File(directory + fileName);


            //判断是否已存在要下载的文件,
            // 存在则 读取 已下载的字节数(以 启用 断点续传 功能)
            if (file.exists()){
                downloadedLength = file.length();
            }


            //获取 待下载文件 的总长度!!!!!!
            // 判断 文件情况—— 有问题 或者 已下载完毕!!!!!
            long contentLength = getContentLength(downloadUrl);
            if (contentLength == 0){//总长度为0,说明文件有问题
                return TYPE_FAILED;

            }else if (contentLength == downloadedLength){//已下载字节和文件总字节相等,说明已经下载完成了
                return TYPE_SUCCESS;

            }


            //注意这里,断点续传 功能!!!!!!!!!!
            //使用.addHeader 往请求中添加一个Header,用于告诉服务器我们想要
            // 从哪个字节开始下载(已下载部分不需再重新下载)
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();//得到服务器响应的数据

            //使用 Java文件流方式 不断从网络上 读取数据!!
            // 不断写入到本地,
            // 直到文件全部下载完为止!!
            if (response != null){

                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");//封装本地文件句柄
                savedFile.seek(downloadedLength);//跳过已下载的字节


                byte[] b = new byte[1024];
                int total = 0; //本轮!!!下载的总长度!!
                int len;

                //使用 Java文件流方式 不断从网络上 读取数据!!
                // 不断写入到本地,直到文件全部下载完为止!!
                while ((len = is.read(b)) != -1){

                    //判断用户有没触发暂停或取消操作,如果有则返回相应值来中断下载
                    if (isCanceled){
                        return TYPE_CANCELED;

                    }else if (isPaused){
                        return TYPE_PAUSE;


                    }else {

                        //用户没有触发暂停或取消操作,继续下载
                        total += len;
                        savedFile.write(b, 0, len);

                        //计算已下载的百分比 == (本轮下载的长度 + 已经下载的长度)/ 要下载的 文件总长度
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);

                        publishProgress(progress);//抛出进度给 onProgressUpdate(),回调之!!!!
                    }
                }

                //执行到此,说明以上循环已执行完毕,文件下载完毕
                response.body().close();

                return TYPE_SUCCESS;

            }
        } catch (Exception e) {
            e.printStackTrace();

        }finally {

            //分开关闭资源!!!!!!
            try {

                if (is != null){
                    is.close();
                }

                if (savedFile != null){
                    savedFile.close();
                }

                if (isCanceled && file != null){
                    file.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //不从上面成功退出则执行至此,证明失败!!!
        return TYPE_FAILED;
    }

    /**
     * 在界面更新当前的下载进度
     *
     * doInBackground()的每一次!!!while 读 输入流 ,
     * 写入file,都会publishProgress(progress); 抛出进度
     * 此时就会回调此方法!!! 对进度进行处理!!!
     *
     * @param values
     */
    @Override
    protected void onProgressUpdate(Integer... values) {

        //获取当前下载进度,
        // 参数来自 doInBackground()中 publishProgress()抛出的进度
        int progress  = values[0];

        if (progress > lastProgress){//与上一次下载进度对比

            listener.onProgress(progress);//有变化则调用DownloadListener的onProgress()通知下载进度更新

            lastProgress = progress;//更新记录
        }
    }

    /**
     *  通知最终的下载结果
     *
     * 当任务执行完了,即doInBackground()一旦return,
     * 其return的值就会传到这里,作为参数,
     * 参数类型即定义泛型时的第三个参数
     *
     * 这里用了回调机制,listener负责抽象调用!!!
     * 外部负责具体实现!!!
     */
    @Override
    protected void onPostExecute(Integer status) {
        switch (status){//根据传入的下载状态进行回调
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;

            case TYPE_FAILED:
                listener.onFailed();
                break;

            case TYPE_PAUSE:
                listener.onPaused();
                break;

            case TYPE_CANCELED:
                listener.onCanceled();
                break;

            default:
                break;
        }
    }

    //取消位以及暂停位
    // 由外部调用,在doInBackground()中生效
    public void pauseDownload(){
        isPaused = true;
    }
    public void cancelDownload(){
        isCanceled = true;
    }

    private long getContentLength(String downloadUrl) throws IOException {
        //请求得到需下载的文件
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(downloadUrl).build();
        Response response = client.newCall(request).execute();

        //得到文件长度
        if (response != null && response.isSuccessful()){
            long contentLength = response.body().contentLength();
            response.close();

            return contentLength;
        }
        return 0;
    }

}

普及:关于RandomAccessFile

Java除了File类之外,还提供了专门处理文件的类,
即RandomAccessFile(随机访问文件)类。
该类是Java语言中功能最为丰富的文件访问类,
它提供了众多的文件访问方法。
RandomAccessFile类支持“随机访问”方式,
这里“随机”是指可以跳转到文件的任意位置处读写数据。
在访问一个文件的时候,不必把文件从头读到尾,
而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分,
这时使用RandomAccessFile类就是最佳选择。

RandomAccessFile对象类有个位置指示器,指向当前读写处的位置,
当前读写n个字节后,文件指示器将指向这n个字节后面的下一个字节处
刚打开文件时,文件指示器指向文件的开头处,
可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。
RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,
但该类仅限于操作文件,
不能访问其他的I/O设备,如网络、内存映像等;

  • RandomAccessFile对象,
    当前读写(read/write)n个字节后,
    文件指示器将自动指向这n个字节后面的下一个字节处
    RandomAccessFile是面向文件(file对象)的,可以用来读写本地SD、硬盘;

    BufferReader、BufferWriter也有类似的指示器
    使用readline()write()读写(read/write)n个字节后,
    指示器将自动指向这n个字节后面的下一个字节处
    只不过BufferReader、BufferWriter面向的是IO流。
  • 为了保证DownloadTask可一直在后台运行,
    需创建一个下载的服务DownloadService
public class DownloadService extends Service {

    private DownloadTask downloadTask;
    private String downloadUrl;

    private String notificationId = "nyd001";
    private String notificationName = "downloadTask";

    /**
     * 创建DownloadListener 匿名内部类实例,
     * 然后赋值给其父类类型DownloadListener引用
     *
     * 这里实现的方法!!
     * 直接在DownloadTask 的 onPostExecute()中被调用
     *
     * 而onPostExecute() 中要调用那个回调方法
     *
     * 则由doInBackground() 的返回值位决定
     *
     * 而doInBackground() 的返回值 中
     * 成功位 和 失败位 是 客观判断的结果
     * 暂停位 和 取消位 可以 由人为点击置位
     */
    private DownloadListener listener = new DownloadListener() {

        /**
         * 在 DownloadTask 中的 onProgressUpdate()处调用
         * @param progress 来自对应的DownloadTask 的 doInBackground() 中的 publishProgress(progress);
         */
        @Override
        public void onProgress(int progress) {
            //getNotification()是自定义的封装方法,
            // 其中构造了一个用于显示下载进度的通知,
            //调用NotificationManager的 notify() 去触发这个通知,
            // 这样就可以在下拉状态栏中实时看到当前的下载进度了
            getNotificationManager().notify(1, getNotification("Downloading...", progress));
        }

        @Override
        public void onSuccess() {

            downloadTask = null;

            //下载成功时将正在下载的前台服务通知关闭
            stopForeground(true);

            //创建一个下载成功的通知
            getNotificationManager().notify(1, getNotification("Download Success", -1));
            Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onFailed() {
            downloadTask  = null;

            //下载失败时将前台服务通知关闭,并创建一个下载失败的通知,
            // !!!!!后面几个方法(暂停、取消)的逻辑 与此类似!!!!
            stopForeground(true);
            getNotificationManager().notify(1, getNotification("Download Failed", -1));
            Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onPaused() {
            downloadTask  = null;
            Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onCanceled() {
            downloadTask  = null;
            stopForeground(true);
            Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();

        }
    };


    /**
     * 创建DownloadBinder内部类,
     * 把需要放给外部调用的Service服务方法写好,
     * 实例化一个DownloadBinder内部类示例,在onBind()中返回,
     * 这样,
     * 当外部界面与本Service绑定,
     * 就可以在 ServiceConnection实例 的 onServiceConnected 回调方法中,
     * 获得这个 具备了 各种准备好的业务方法的 DownloadBinder(Binder、IBinder)实例了
     *
     */
    private DownloadBinder mBinder = new DownloadBinder();
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    //创建DownloadBinder内部类,
    //把需要放给外部调用的Service服务方法写好
    class DownloadBinder extends Binder {

        /**
         * 开启下载任务
         * @param url 要下载的资源地址
         */
        public void startDownload(String url){

            if (downloadTask == null){

                downloadUrl = url;

                //创建DownloadTask实例
                downloadTask = new DownloadTask(listener);

                //传入下载地址,启动下载任务!!!!
                downloadTask.execute(downloadUrl);

                //让这个下载任务服务成为一个前台服务!!!
                // 使用时在Activity处 先 startService(intent);  启动! 本服务DownloadService
                //
                // 然后 绑定本服务 bindService(intent, connection, BIND_AUTO_CREATE);!!!!
                // 再调用本方法 downloadBinder【即这里的mBinder】.startDownload(url);
                // 运行到下面的startForeground()!!
                // 从而使刚刚已经启动(start)的服务变成前台服务!!!!!
                //这样就会在 系统状态栏 中 创建一个持续运行的通知了
                // .
                // 注意这里有个id!!! 后续取消时 可以用!!

                getNotificationManager();// 配置 NotificationManager!!!!!!!!
                startForeground(1, getNotification("Downloading...", 0));
                //!!!!!!!!!!!

                Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
            }
        }
        public void pauseDownload(){
            if (downloadTask != null){
                //使下载任务downloadTask 的 暂停位 置位
                downloadTask.pauseDownload();
            }
        }
        public void cancelDownload(){
            if (downloadTask != null){

                //首先,使下载任务downloadTask 的 取消位 置位,终止下载!!!!
                downloadTask.cancelDownload();
                //调用流程:
                // downloadTask.cancelDownload();
                // --> isCanceled = true;   取消位 置位
                // .
                // -->downloadTask 的 doInBackground 中 取消位 置位生效
                // doInBackground() 中的 下载文件的while循环中
                // if (isCanceled){ return TYPE_CANCELED;} 返回取消位 并终止下载!!!
                // .
                // -->onPostExecute() 接收到 doInBackground()返回的取消位
                // (只要onPostExecute() 接收到了取消位, 便已经终止下载了!! 这时候回调接口...)
                // .
                // --> listener.onCanceled(); 回调 接口的 取消方法 ,
                // 即这里 DownloadService 实现的方法, 接着进行下一步操作...
                // .
                // --> downloadTask  = null;

            }else {
                //如果 downloadTask  = null; 则 执行到此

                //纵观 接口处几个方法 无论成功、失败、暂停、取消
                // 都会执行 downloadTask  = null;
                // .
                // 也就是说 只要 downloadTask 调用过 一次 接口方法!!!!
                // 之后再调用  downloadBinder.cancelDownload(); 的话,
                // 都会已 downloadTask  = null;
                // 即 会执行至此, 删除文件,关闭通知 !!!

                if (downloadUrl != null){

                    //取消下载时需将文件删除,并将通知关闭

                    //获取file 的过程 同DownloadTask 的 doInBackground()
                    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//得到文件名
                    String directroy = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directroy + fileName);
                    if (file.exists()){
                        file.delete();
                    }

                    //取消对应id 前台通知或者服务
                    getNotificationManager().cancel(1);
                    stopForeground(true);

                    Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }


    //封装 NotificationManager
    private NotificationManager getNotificationManager(){
        NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(notificationId, notificationName, NotificationManager.IMPORTANCE_HIGH);
            notificationManager.createNotificationChannel(channel);

            return notificationManager;
        } else {
            return notificationManager;
        }

    }

    /**
     * 封装进度条通知
     * 返回一个封装配置好的 Notification
     *
     * Notification
     * 遇 startForeground() 则成前台服务!!!
     * 遇 NotificationManager.notify() 则成通知!!!
     */
    private Notification getNotification(String title, int progress){

        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);

        //拿着Notification 的 建造者Builder, 去各种配置(set()),
        // 配置完毕了,调用builder.build(),返回 一个 Notification !!!
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationId);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        builder.setContentIntent(pi);
        builder.setContentTitle(title);

//        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//            builder.setChannelId(notificationId);
//        }

        if (progress > 0){
            //当progress大于或等于0时才需显示下载进度
            builder.setContentText(progress + "%");
            builder.setProgress(100, progress, false);//三个参数:通知的最大进度,通知的当前进度,是否使用模糊进度条
        }

        return builder.build();
    }
}


.

普及


  • 后端基本完成,编写前端,修改布局文件,
    放置三个按钮分别用于开始下载、暂停下载和取消下载。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/start_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Download"/>

    <Button
        android:id="@+id/pause_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Pause Download"/>

    <Button
        android:id="@+id/cancel_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Cancel Download"/>

</LinearLayout>

修改MainActivity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private DownloadService.DownloadBinder downloadBinder;

    //创建了一个ServiceConnection 的 匿名内部类,
    // 重写方法后 赋值给ServiceConnection 实例
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (DownloadService.DownloadBinder) service;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

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

        initViews();
    }

    private void initViews() {
        //初始化 UI 按钮
        Button startDownload = (Button) findViewById(R.id.start_download);
        Button pauseDownload = (Button) findViewById(R.id.pause_download);
        Button cancelDownload = (Button) findViewById(R.id.cancel_download);
        startDownload.setOnClickListener(this);
        pauseDownload.setOnClickListener(this);
        cancelDownload.setOnClickListener(this);

        //启动服务 以及 绑定服务 二者在这里 缺一不可
        Intent intent =new Intent(this, DownloadService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(intent);//启动服务,保证服务一直在后台运行!!!
        } else {
            startService(intent);
        }
        bindService(intent, connection, BIND_AUTO_CREATE);//绑定服务,让MainActivity和服务进行通信!!!

        //运行时权限申请
        if (ContextCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){

            ActivityCompat.requestPermissions(MainActivity.this, new String[]{
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,}, 1);
        }
    }

    @Override
    public void onClick(View v) {
        if (downloadBinder == null){
            return;
        }
        switch (v.getId()){
            case R.id.start_download:
                String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
                downloadBinder.startDownload(url);
                break;

            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;

            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;

            default:
                break;
        }

    }

    //运行时权限申请结果
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:

                for (int grantResult : grantResults) {
                    if (grantResult != PackageManager.PERMISSION_GRANTED) {
                        Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
                        finish();
                    }
                }

                break;
            default:
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(connection);//解绑服务,避免内存泄漏
    }
}

  • 声明权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
注意Android 8.0 之后,开启前台服务需要关注一下几点!!!
  • 开启服务需要用startForegroundService(intent),
    不能用startService(intent);
    且调用完startForegroundService(intent)之后,
    五秒内需要调用startForeground()!!!
    否则app可能会ANR!

    实战如上,
    MainActivityinitViews()里边的startForegroundService(intent)

  • 需要为 NotificationManager 配置 NotificationChannel
    DownloadService里面的getNotificationManager()

  • 需要为 Notification 设置 channelId
    DownloadService里面的的getNotification()

  • 需要静态声明权限 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

参考文章:

运行测试:

  • 首先是请求权限:
  • Toast提示
  • 开始+暂停+开始(断点续传)
  • 开始+暂停+取消(重新下载)


  • 下载完毕提示
  • 下载完毕点击再开始,不会再下载
  • 下载完毕点击取消会删除文件,再点击开始会重新下载








参考自《第一行代码》

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