课程 3: 线程与并行

这节课是 Android 开发(入门)课程 的第三部分《访问网络》的第三节课,导师是 Chris Lei 和 Joe Lewis。这节课在前两节课的基础上将网络传输的代码添加到 Quake Report App 中,重点介绍多任务处理的概念和处理方案,最后完善一些功能细节和 UI 优化。

关键词:Threads & Parallelism、AsyncTask Class、Inner Class、Loader、Empty States、ProgressBar、ConnectivityManager

Multitasking

在 Android 中,网络操作 (Networking) 需要经历创建连接、等待响应等耗时较长的过程,应用在执行这类任务时,期间为了保持活跃状态,不被用户认为应用无响应,所以应用需要同时进行其它任务,这就是多任务处理 (Multitasking) 的概念。在 Java 中可以把一个或一系列任务看成一个线程 (Thread)(不是进程 (Process),两者的差异可以查看这篇 Android Developers 文档),多线程形成并行 (Parallelism) 的结构关系。

线程是保存指令(任务)序列的容器,指令默认保存在主线程 (Main Thread) 中,主线程通常处理绘制布局,响应用户交互等任务,所以也叫做 UI 线程。线程内的任务是串行的,当同一时间内发生多起事件时,任务会放到队列 (Queue) 中,按顺序依次执行。就像在高速公路的出入口,收费站只开了一个窗口,车辆只能排队一个一个地缴费。

在 Quake Report App 中,如果网络操作放在主线程中,那么应用在完成网络操作前就无法进行其它任务,例如响应用户的点击事件;如果此时用户认为应用无响应,反复点击屏幕的话,主线程的任务队列就会越来越长;当 Android 判断应用的主线程被阻塞 (block) 超过一定时间(五秒左右),就会触发 ANR (Application Not Responding),弹出对话框告知用户应用无响应,并询问用户是否要关闭它。

ANR 对话框示例

事实上,Android 了解网络操作阻塞主线程是一个经常发生的场景,所以 Android 不允许将网络操作放在主线程中,它会触发 NetworkOnMainThreadException 异常使应用崩溃。因此这里需要引入后台线程 (Background Thread),也叫工作线程 (Worker Thread),将网络操作放入一个独立的后台线程中,这样一来,即使网络操作的耗时较长,也不会影响主线程响应用户交互。关于保持应用迅速响应的更多讨论可以查看这篇 Android Developers 文档

AsyncTask

Android 提供了 AsyncTask Class 用于引入后台线程,它适用于短期的一次性任务,相对其它工作线程比较简单。AsyncTask 是一个抽象类,它可以在后台线程中执行任务,任务完成后将结果传递给主线程以更新 UI(不能在后台线程中更新 UI)。数据在主线程与后台线程之间的传递,是通过 AsyncTask 中运行在不同线程的 method 完成的。四个常用的 AsyncTask method 之间的关系和所在线程如下表。

Method When is it called Thread
onPreExecute Before the task is executed Main
doInBackground After onPreExecute Background
onProgressUpdate After publishProgress is called, while onPreExecute is executing Main
onPostExecute After doInBackground finish Main
  1. 所有在后台线程执行的指令都放在 doInBackground 中,这是一个必须实现的抽象 method。
  2. 在执行 doInBackground 中的后台线程的指令之前,可以将一些必要的准备指令放入在主线程运行的 onPreExecute 中。
  3. 对于一些耗时很长的后台线程,可以在 doInBackground 中调用 publishProgress 并传入进度值,然后在 onProgressUpdate 中更新任务进度,实现进度条的功能。因为 onProgressUpdate 运行在主线程,所以它可以更新 UI。
  4. doInBackground 中的后台线程的指令执行完毕后,会执行 onPostExecute 中的指令,输入参数为 doInBackground 的返回值;因为 onPostExecute 运行在主线程,所以可以将返回值用于更新 UI,实现主线程与后台线程之间的数据传递。

下面是一段 AsyncTask 的代码示例。

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected Long doInBackground(URL... urls) {
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             if (isCancelled()) break;
         }
         return totalSize;
     }

     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }

     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
 }

这里创建了一个 AsyncTask 的自定义类 DownloadFilesTask。AsyncTask 是一个泛型类,输入参数为三个泛型参数:Params、Progress、Result。泛型参数与抽象类和接口的概念类似,它是参数化的数据类型,在具体实现时需要指定数据类型,在使用时必须传入对应的数据类型,这称为类型安全 (Type Safety)。

Note:
1. 泛型数据类型 (Generic Type) 必须是对象数据类型,所以指定 void 时需要写成它的对象类型 Void;类似地,在指定原始数据类型时,也要写成对应的对象类型:

Primitive Data Types Object Data Types
int Integer
boolean Boolean
short Short
long Long
double Double
float Float
byte Byte
char Character

2. 数据类型后面的 ... 表示可传入任意数量的参数,称为可变参数 (Variable Argument, abbr. Varargs)。可变参数可看作是数组,两者访问元素的方法相同,即在变量名后加 [index] 按索引访问,例如上面的 urls[i]progress[0]

在 DownloadFilesTask 中分别将 Params、Progress、Result 三个泛型参数指定为 URL、Integer、Long。

(1)Params > URL
Params 参数指后台线程的任务的输入参数,也就是 doInBackground(Params... params) 的形参。它是可变参数,实参通过在主线程中调用 execute(Params... params) 传入。例如 DownloadFilesTask 在主线程中传入三个 URL 参数:

new DownloadFilesTask().execute(url1, url2, url3);

然后在 doInBackground 中通过 urls[i] 使用传入的三个 URL 参数。

Note: 通常 doInBackground 需要考虑 Params 实参为空的异常情况,若未正确处理会导致应用崩溃,这里可以添加 if-else 语句判断如果不存在 Params 参数,那么提前返回 null,不执行下面的任务。

(2)Progress > Integer
Progress 参数指后台线程的任务的执行进度,是 publishProgress(Progress... values)onProgressUpdate(Progress... values) 两个进度相关的 method 的输入参数。它是可变参数,所以对于执行多个任务的 AsyncTask 来说,可以生成多个 Progress 参数,通过 progress[index] 分别访问每个参数。在 DownloadFilesTask 中 Progress 参数指定为 Integer,通过在 doInBackground 调用 publishProgress 并传入 Progress 参数,然后在 onProgressUpdate 根据 Progress 输入参数更新进度。

Note: 对于一些短期的后台线程,不需要实现进度条的情况,可以将 Progress 参数指定为 Void(注意首字母大写),无需实现 onProgressUpdate method。

(3)Result > Long
Result 参数指后台线程的任务的输出结果,也就是 doInBackground(Params... params) 的返回值,同时也是 onPostExecute 的输入参数。这一点符合逻辑,后台线程的任务完成后,将结果传入随后在主线程执行的 method 中进行 UI 更新。注意 Result 参数不是可变参数。

Note: 通常 onPostExecute 需要考虑 Result 实参为 null 的异常情况,若未正确处理会导致应用崩溃,这里可以添加 if-else 语句判断如果 Result 参数为 null,那么提前结束 method (如 return;),不执行下面的任务。

Inner Class

在 AsyncTask 中,后台线程的任务完成后通常需要在 onPostExecute 根据任务结果来更新 UI,此时 onPostExecute 需要引用 Activity 的视图,说明 AsyncTask 要与 Activity 紧密合作,因此 AsyncTask class 通常作为 Activity 的内部类 (Inner Class),包括在 Activity 内,声明为 private,而不是作为一个单独的 Java Class 文件。这样一来,不仅精简了代码,减少了 Java 文件的数量;AsyncTask 也能够访问 Activity 内的全局变量与 method 了,例如 AsyncTask 能够在 onPostExecute 对 Activity 内的一个全局变量 TextView 调用 setText method,实现更新 UI 操作。

Loader

在 Quake Report App 中,AsyncTask 作为 EarthquakeActivity 的内部类,并且在 onCreate 创建并执行了 AsyncTask。这意味着当 EarthquakeActivity 创建时,也会创建一个 AsyncTask 在后台线程执行网络操作任务。如果在 AsyncTask 完成后台线程的任务之前,设备切换了旋转方向,Android 为了能够显示正确的视图会重新创建一个 EarthquakeActivity ,此时又会创建并执行一个新的 AsyncTask;之前的 Activity 会被销毁并回收内存,但此时却无法回收正在进行网络操作的 AsyncTask,只能等待任务完成后才能回收;而且即使 AsyncTask 完成了任务,从网络获取的数据也不再有用,因为 Activity 已经被销毁了。如果用户频繁旋转设备,每次创建新的 Activity 也会创建一个 AsyncTask,导致设备重复进行无意义的网络操作,消耗大量内存。这些问题的解决方案是 Loader。

当例如旋转设备、更改语言等设备配置变更 (Device Configuration Changes) 发生在应用运行时 (Runtime),默认情况下 Android 会使 Activity 重启,而 Loader 不受此影响,它会保持已有数据,在新的 Activity 创建后,将数据传给新的 Activity。另外,Loader 在 Activity 被永久销毁时,也会跟着销毁,不会造成多余的操作。

引入 Loader

// Get a reference to the LoaderManager, in order to interact with loaders.
LoaderManager loaderManager = getLoaderManager();

// Initialize the loader. Pass in the int ID constant defined above and pass in null for
// the bundle. Pass in this activity for the LoaderCallbacks parameter (which is valid
// because this activity implements the LoaderCallbacks interface).
loaderManager.initLoader(EARTHQUAKE_LOADER_ID, null, this);

在 Activity 的 onCreate() 或 Fragment 的 onActivityCreated() 通过实例化 LoaderManager 引入 Loader,随后调用 initLoader 初始化,需要传入三个参数:

  1. ID: 识别 Loader 的唯一标识,可以是任意数字。当应用内有多个 Loader 时,就根据 ID 区分每一个 Loader。
  2. args: 传入构造函数的可选参数,如无设置为 null
  3. LoaderCallbacks<D> callback: Loader 的回调对象,可设置为 this 表示回调对象即 Activity 本身,回调函数放在 Activity 内,在 Activity 类名后面添加 implements 参数,例如在 Quake Report App 中:
public class EarthquakeActivity extends AppCompatActivity
        implements LoaderCallbacks<List<Earthquake>> {
    ...

    // LoaderManager.LoaderCallbacks inside EarthquakeActivity class.
}

Note:
1. 一个 Activity 或 Fragment 内只有一个 LoaderManager,它可以管理多个 Loader。
2. 如果在 initLoader 传入的 ID 已经属于一个 Loader,那么就会使用那个 Loader。这就是设备配置变更不会产生新的 Loader 造成内存浪费的原因,因为新的 Activity 执行 initLoader 时会发现传入的 ID 已属于之前创建的 Loader。如果 ID 之前不存在,那么就会调用 onCreateLoader() 回调函数创建一个新的 Loader。
3. initLoader 的返回值为 Loader 对象,但并不需要获取它 (capture a reference to it),LoaderManager 会自动管理 Loader 对象。因此,开发者几乎不需要直接操作 Loader,往往是通过回调函数来处理数据加载的事件。

实现 LoaderManager.LoaderCallbacks 的三个回调函数

@Override
public Loader<List<Earthquake>> onCreateLoader(int i, Bundle bundle) {
    // Create a new loader for the given URL
    return new EarthquakeLoader(this, USGS_REQUEST_URL);
}

@Override
public void onLoadFinished(Loader<List<Earthquake>> loader, List<Earthquake> earthquakes) {
    // Hide loading indicator because the data has been loaded
    View loadingIndicator = findViewById(R.id.loading_indicator);
    loadingIndicator.setVisibility(View.GONE);

    // Set empty state text to display "No earthquakes found."
    mEmptyStateTextView.setText(R.string.no_earthquakes);

    // Clear the adapter of previous earthquake data
    mAdapter.clear();

    // If there is a valid list of {@link Earthquake}s, then add them to the adapter's
    // data set. This will trigger the ListView to update.
    if (earthquakes != null && !earthquakes.isEmpty()) {
        mAdapter.addAll(earthquakes);
    }
}

@Override
public void onLoaderReset(Loader<List<Earthquake>> loader) {
    // Loader reset, so we can clear out our existing data.
    mAdapter.clear();
}
  1. onLoadFinished: 当 Loader 在后台线程完成数据加载后调用,有两个输入参数,分别为 Loader 实例和后台线程的任务的运行结果。此时应该根据结果更新 UI。

  2. onLoaderReset: 当 Loader 被重置时调用,意味着其加载的数据不再有用,此时应该清除应用获取的数据。

  3. onCreateLoader: 创建并返回一个新的 Loader,通常是 CursorLoader。在 Quake Report App 中,创建并返回了一个 AsyncTaskLoader 的自定义类。

AsyncTaskLoader 是一个 Loader 的子类,能够由 LoaderManager 管理,而实际的工作是由 AsyncTask 完成的。同时 AsyncTaskLoader<D> 也是一个泛型类,输入参数为泛型参数 D,是 doInBackground 抽象 method 的返回值数据类型。在 Quake Report App 中,新建一个 Java Class 文件,实现 AsyncTaskLoader 的自定义类 EarthquakeLoader。

In EarthquakeLoader.java

/**
 * Loads a list of earthquakes by using an AsyncTask to perform the
 * network request to the given URL.
 */
public class EarthquakeLoader extends AsyncTaskLoader<List<Earthquake>> {
    /** Tag for log messages */
    private static final String LOG_TAG = EarthquakeLoader.class.getName();

    /** Query URL */
    private String mUrl;

    /**
     * Constructs a new {@link EarthquakeLoader}.
     *
     * @param context of the activity
     * @param url to load data from
     */
    public EarthquakeLoader(Context context, String url) {
        super(context);
        mUrl = url;
    }

    @Override
    protected void onStartLoading() {
        forceLoad();
    }

    /**
     * This is on a background thread.
     */
    @Override
    public List<Earthquake> loadInBackground() {
        if (mUrl == null) {
            return null;
        }

        // Perform the network request, parse the response, and extract a list of earthquakes.
        List<Earthquake> earthquakes = QueryUtils.fetchEarthquakeData(mUrl);
        return earthquakes;
    }
}
  1. 将泛型参数 D 指定为 List<Earthquake> 对象,在 doInBackground 中创建并返回对象实例。List<Earthquake> 对象在三个回调函数中都以 Loader<List<Earthquake>> 作为单独的对象传入,称为 BLOB。
  2. 调用 initLoader 时会自动调用 onStartLoading,此时应该调用 forceLoad 启动 Loader。

综上所述,在 Quake Report App 中 AsyncTaskLoader 的工作流程为:

  1. 在 EarthquakeActivity 的 onCreate() 实例化 LoaderManager,随后调用 initLoader 初始化 Loader。
  2. initLoader 自动调用 onStartLoading,此时调用 forceLoad 启动 Loader。
  3. Loader 启动后,在后台线程执行 doInBackground 创建并返回 List<Earthquake> 对象。
  4. 当 Loader 加载数据完毕时,会通知 LoaderManager 将数据传入 onLoadFinished 更新 UI。
  5. 如果 EarthquakeActivity 被销毁,LoaderManager 也会销毁对应的 Loader,然后调用 onLoaderReset 表示当前加载的数据已无效,此时将应用获取的数据清除。
  6. 如果在 Loader 完成加载数据之前 EarthquakeActivity 被销毁,LoaderManager 不会销毁对应的 Loader,也不会调用 onLoaderReset,而是随后将加载好的数据传入新的 EarthquakeActivity 中。
功能实现和布局优化
  1. Empty States

当 ListView 没有元素或其它对象无法显示时,应用默认显示空白,为了提供更好的用户体验,应用应该处理这种空状态 (Empty States) 的情况,解决方案可以参考 Material Design

在 Quake Report App 中,为 ListView 添加一个空视图。首先在 XML 中添加一个 TextView,ID 为 "empty_view"。

In earthquake_activity.xml

<RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">

     <ListView
         android:id="@+id/list"
         android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:divider="@null"
         android:dividerHeight="0dp"/>

     <!-- Empty view is only visible when the list has no items. -->
     <TextView
         android:id="@+id/empty_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_centerInParent="true"
         android:textAppearance="?android:textAppearanceMedium"/>
  </RelativeLayout>

然后在 Java 中设置 TextView 为 ListView 的 EmptyView。

In EarthquakeActivity.java

private TextView mEmptyStateTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    …
    mEmptyStateTextView = (TextView) findViewById(R.id.empty_view);
    earthquakeListView.setEmptyView(mEmptyStateTextView);
    … 
}

为了避免在启动应用时屏幕首先显示 ListView 的空视图, 所以不在 XML 中设置 TextView 的文本,而在 onLoadFinished 中,完成数据加载后,将文本设置为 "No earthquakes found." 字符串。

In EarthquakeActivity.java

@Override
public void onLoadFinished(Loader<List<Earthquake>> loader, List<Earthquake> earthquakes) {
    // Set empty state text to display "No earthquakes found."
    mEmptyStateTextView.setText(R.string.no_earthquakes);
    …
}

测试空状态时,可以把 onLoadFinished 内更新 UI 的指令注释掉,使应用接收不到 Loader 加载的数据。记得在测试完毕后取消注释。

  1. Progress and Activity Indicator

应用在加载内容时应该使用进度和活动指示符 (Progress and Activity Indicator) 告知用户当前的状态,例如视频缓冲进度。Material Design 提供了线性和圆形两种指示符,可分为确定指示符 (Determinate Indicator) 用于明确知道任务进度的场景,如文件下载的进度;以及不确定指示符 (Indeterminate Indicator) 用于不明确进度的场景,如从网络刷新推文。

Android 提供了 ProgressBar 来实现进度指示符。首先在 XML 中添加一个 ProgressBar,ID 为 "loading_indicator"。

In earthquake_activity.xml

<!-- Loading indicator is only shown before the first load -->
<ProgressBar
    android:id="@+id/loading_indicator"
    style="@style/Widget.AppCompat.ProgressBar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"/>

在应用启动后,ProgressBar 就会在屏幕中显示,直到完成数据加载时,在 onLoadFinished 中隐藏 ProgressBar。

In EarthquakeActivity.java

@Override
public void onLoadFinished(Loader<List<Earthquake>> loader, List<Earthquake> earthquakes) {
    View loadingIndicator = findViewById(R.id.loading_indicator);
    loadingIndicator.setVisibility(View.GONE);
    …
}

测试进度指示符时,可以使用 Thread.sleep(2000); 强制后台线程睡眠两秒钟,使开发者有充分的时间观察进度指示符。注意 Thread.sleep() method 需要处理 InterruptedException 异常,所以要把它放进 try/catch 区块中。

try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
  1. Network Connectivity Status

应用在进行网络操作时会遇到设备无蜂窝或 Wi-Fi 连接的情况,将这一情况告知用户是一种好的做法。在 Android 中通过 ConnectivityManager 来检查设备的连接状态,并作出相应的处理方案。

(1)请求 ACCESS_NETWORK_STATE 权限

In AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

这是一个正常权限,Android 会自动授予应用该权限,无需用户介入。

(2)实例化 ConnectivityManager 并获取设备的连接状态

In EarthquakeActivity.java

// Get a reference to the ConnectivityManager to check state of network connectivity
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

// Get details on the currently active default data network
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();

(3)利用 if-else 语句根据设备的连接状态作出处理方案

In EarthquakeActivity.java

// If there is a network connection, fetch data
if (networkInfo != null && networkInfo.isConnected()) {
    // Get a reference to the LoaderManager, in order to interact with loaders.
    LoaderManager loaderManager = getLoaderManager();

    // Initialize the loader. Pass in the int ID constant defined above and pass in null for
    // the bundle. Pass in this activity for the LoaderCallbacks parameter (which is valid
    // because this activity implements the LoaderCallbacks interface).
    loaderManager.initLoader(EARTHQUAKE_LOADER_ID, null, this);
} else {
    // Otherwise, display error
    // First, hide loading indicator so error message will be visible
    View loadingIndicator = findViewById(R.id.loading_indicator);
    loadingIndicator.setVisibility(View.GONE);

    // Update empty state with no connection error message
    mEmptyStateTextView.setText(R.string.no_internet_connection);
}

当设备已连接网络的情况下才开始通过 LoaderManager 从网络获取数据,当设备无连接时将 ListView 的空视图显示为 "No Internet Connection."

Tips:
1. 在 Quake Report App 中使用了 ArrayList,与它相似的有 LinkedList,两者都属于 List 接口的具象类。如果 App 需要重构代码,由 ArrayList 改为 LinkedList,那么就要修改多处代码,这很麻烦。因此最佳做法是,无论 ArrayList 还是 LinkedList,只要使用 List 对象,就使用 List,仅在对象实例的定义处指定一个具象类即可。这样可以保持代码的灵活性。例如:

List<Earthquake> earthquakeList = new ArrayList<Earthquake>();

List<Earthquake> earthquakeList = new LinkedList<Earthquake>();

2. 在 Quake Report App 中使用了 ArrayAdapter 的两个 method,分别为 mAdapter.clear() 表示清除所有元素数据,mAdapter.addAll(data) 表示将 data 添加到适配器的数据集中。

3. 在 Android Studio 中选择 File > New > Import Sample...,在弹出的对话框搜索关键字,可以导入 Google 提供的示例应用。例如搜索 Network 可以找到 Network Connect App,它使用了 HttpsURLConnection 进行网络操作,AsyncTask 作为 Activity 的内部类,实现从 google.com 获取前 500 个 HTML 响应字符的功能。

如果遇到无法下载示例目录的情况 (Failed to download samples index, please check your connection and try again),检查 Android Studio 中 Preference 的 HTTP Proxy 选项是否选中 Auto-detect proxy settings。选中此选项可以让 Android Studio 通过系统代理科学上网。如果仍无法解决问题,所有示例应用也可以在 Android Developers 网站 中找到。

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

推荐阅读更多精彩内容

  • Android开发者:你真的会用AsyncTask吗? 导读.1 在Android应用开发中,我们需要时刻注意保证...
    cxm11阅读 2,659评论 0 29
  • Processes and Threads Processes and Threads Android Devel...
    mlya阅读 590评论 0 3
  • Android Handler机制系列文章整体内容如下: Android Handler机制1之ThreadAnd...
    隔壁老李头阅读 3,086评论 1 15
  • 社交聊天软件: 1,进化撤回按钮或者添加一个新功能,如果可以再原本想撤回的消息上直接进行编辑,修改聊天的效率就会变...
    匿铭鹿阅读 251评论 0 0
  • 午睡醒来,喵喵在床头睡得正香,一人一猫,和谐又温馨,厚爱无需多言,你对我,我对你~
    安德丽阅读 239评论 0 0