Instrumentation Test Framework

Instrumentation Test Class VS JUnit Test Class

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    @Rule
    public ActivityTestRule<MainActivity> activity = new ActivityTestRule<MainActivity>(MainActivity.class);

    @Test
    public void testTxt(){
        Espresso.onView(withId(R.id.textview0)).check(matches(withText("Hello World")));
    }

}

上面的代码给出的是一个很简单的Instrumentation Test Class。在代码中我们看到了熟悉的@Rule@Test注解。其实,除此之外,我们还能够使用JUnit支持的其他大部分注解,包括@Suite@ClassRule@BeforeClass@AfterClass@Before@After等等,只是在这个Test Class中没有体现出来而已。可以这样说,一个Instrumentation Test Class本质上就是一个JUnit Test Class。因为Android Instrumentation Test本来就是基于JUnit框架的。如果非要说它们之间的区别,那就只能是它们的默认Runner不同。对于JUnit4而言, 它的默认Runner是BlockJUnit4ClassRunner(请参照前面一篇文章),而对于Android Test而言,一个Test Class的默认Runner就是上面的代码中@RunWith所指明的AndroidJUnit4类。具体类的定义如下:

public final class AndroidJUnit4 extends AndroidJUnit4ClassRunner { ...}
public class AndroidJUnit4ClassRunner extends BlockJUnit4ClassRunner { ...}

AndroidJUnit4只是AndroidJUnit4ClassRunner的一个别名而已(让你调皮,取那么长的类名),而AndroidJUnit4ClassRunner其实又是继承于BlockJUnit4ClassRunner的。查看其源码,可以发现AndroidJUnit4ClassRunner的执行逻辑99%都交由BlockJUnit4ClassRunner处理,也就是上一篇文章所分析的流程,而它们唯一的一点区别就是AndroidJUnit4ClassRunner@Test中的timeout的处理稍有不同,这里不再具体分析。所以我们可以这样说,一个Instrumentation Test Class的执行流程同一个Normal JUnit Test Class是一致的。这里所说的执行流程指的仅仅是Test Class对应的Runner执行的逻辑,不包括Runner的构造和Instrumentation Test的入口流程,这个流程我们放在下文进行分析。

About Instrumentation

我们先来看看Instrumentation Test中的Instrumentation到底是什么,它又是干嘛的?不卖关子了,Instrumentation其实是Android Framework中的一个类,它的作用简而言之就是能够监控Android系统和我们Application之间的交互。我们都知道,一个Application有一个ActivityThread对象,负责和ActivityMangerService打交道来管理App的运行,比如启动某个Activity,发送广播等其他操作。而这个Instrumentation会在App启动阶段被初始化,然后作为一个实例变量保存到ActivityThread对象中。Application的创建、Activity生命周期方法的回调等其他操作,都会经过Instrumentation来完成。e.g.

public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
        ...
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
        ...
    }
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
            ...
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            ...
            mInstrumentation.callActivityOnCreate(activity, r.state);
            ...
            mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
                    r.persistentState);
            ...
            mInstrumentation.callActivityOnPostCreate(activity, r.state);
                    
            ...
        }

这里不再一一列举,可以自行查看Instrumentation的代码。那这个Instrumentation在Android Test中有什么作用呢?在App正常运行的时候,系统会帮助App维护运行组件的状态和信息,这些管理是有必要的,因为这个过程是非常复杂的,交由开发者自己去完成很容易造成系统的混乱。系统管理的好处就是简单方便,但同时也造成了开发者不能很方便的得到运行组件的信息,好在正常运行的极大多数情况下我们都不需要访问这些信息。然而当我们在测试时,就另当别论了,我们可能需要频繁地访问当前正在运行的某个组件的信息,比如Activity。这个时候,Instrumentation就派上用场了。有些时候我们可能需要扩展当前的Instrumentation类,为此Android允许我们在AndroidManifest文件中创建<instrumentaion>标签,用来指定在创建Instrumentation时使用我们自定义的类,<instrumentaion>中需要至少包含以下两个属性:

  • android:name:指定使用这个类来创建,而不是系统默认的Instrumentation类,需要为Instrumentation的子类才合法。
  • android:targetPackage:指定要监控的目标app包名;这里一般是指我们需要测试的目标app包名。

请注意,Android仅在开发者进行app测试的时候,也就是说只能在我们的Instrumentation Test Class的测试代码中才能访问到Instrumentation。当App正常运行时没有可供访问的开放接口使用(当然不排除某些黑科技方法),这一点非常重要。当然,这并不代表app正常运行时,系统没有构建Instrumentation对象;只是系统在这个时候会忽略我们的<instrumentaion>标签,创建一个默认的Instrumentation对象。

Run An Instrumentation Test Class

下面,我们通过Android Studio来运行上面的Test Class,来看看IDE是怎么做的?当然,我们首先需要在build.gradle文件中添加下面的配置:

android {
    ...
    defaultConfig {
        ...
        testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
    }
    ...
}

现在,我们先不用去考虑上面的配置到底起了的是什么作用?右键Test Class->Run 起来再说,同时请注意观察Run窗口的相应输出。直接粘贴如下:

$ adb push D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\outputs\apk\app-debug.apk /data/local/tmp/com.lcd.androidtestpractice
$ adb shell pm install -r "/data/local/tmp/com.lcd.androidtestpractice"
pkg: /data/local/tmp/com.lcd.androidtestpractice
Success

$ adb push D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\outputs\apk\app-debug-androidTest-unaligned.apk /data/local/tmp/com.lcd.androidtestpractice.test
$ adb shell pm install -r "/data/local/tmp/com.lcd.androidtestpractice.test"
pkg: /data/local/tmp/com.lcd.androidtestpractice.test
Success

Running tests

$ adb shell am instrument -w -r -e debug false -e class com.lcd.androidtestpractice.MainActivityTest com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner
Client not ready yet..Test running started
Tests ran to completion.

Run窗口的输出可以很清晰的看到IDE进行了哪些暗箱操作?

  1. 打包并安装com.lcd.androidtestpractice包,这个包是我们的主程序App包,没什么好说的。
  2. 打包并安装com.lcd.androidtestpractice.test。原来,IDE帮我们打了一个新的apk包,包名为主包名后面加上.test后缀。我们找到对应的目录,发现确实是存在对应的新apk。我们可以反编译一下这个apk,看看这个包里都包含了哪些内容。下面给出反编译后结果的几张截图:
    AndroidTestCompile依赖的代码

    我们的测试代码

    代码部分包含了build.gradle文件中androidTestCompile指定要编译的部分、包含有我们的测试代码;再来看看AndroidManifest文件的内容。
    manifest文件

    这个清单文件内容较少,首先里面没有声明任何的组件,所以安装之后,不会在Launcher上看到对应的应用图标。再仔细看看,我们发现了<instrumentation>标签,里面指定了name属性值为android.support.test.runner.AndroidJUnitRunnertargetPackage属性值为com.lcd.androidtestpracticetargetPackage属性比较好理解,这个包名的值就是我们主程序的app包名,这里的意思就是指定它为要测试的目标app;再来看看name属性,我们发现这个值和我们刚刚在build.gradle中配置的testInstrumentationRunner属性值相等。它们会不会有什么联系呢?Bingo!当我们在build.gradle文件中运用testInstrumentationRunner 'customname'时,Android Studio在打包测试APK时就会在manifest中添加<Instrumentation>标签,并且将name属性值指定为customname。那这里为什么要指定testInstrumentationRunner值为AndroidJUnitRunner呢?能不能是另外一个其他的值呢?当然可以!这里的值其实只需要是指向一个Instrumentation类或者子类的全域限定的类名。我们之所以指定为AndroidJUnitRunner是因为Android将测试代码的执行逻辑放到这个类中,测试代码就靠它来运行的。显然,我们完全可以实现一个自定义类继承于AndroidJUnitRunner,并通过testInstrumentationRunner来声明使用它,这样不仅不会阻碍我们测试代码的运行,还可以通过覆写它的某些方法来达成某些目标。这在某些情况下很有用,比如我们想要自定义测试时创建的Application对象,就可以通过继承AndroidJUnitRunner并重写public Application newApplication(ClassLoader cl, String className, Context context)来实现。这里有个例子可以看看
  3. 不多说了,我们现在再回过头来看看IDE执行的第三步。

adb shell am instrument -w -r -e debug false -e class com.lcd.androidtestpractice.MainActivityTest com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner

其实就是通过adb运行am instrument命令。所以AndroidTest也是可以通过ADB手动运行的,IDE只是为我们简化了流程。来看具体的命令,很明显里面指定的MainActivityTest就是我们要执行的Test Class,而com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner这一部分其实标识了一个Instrumentation。前半部分com.lcd.androidtestpractice.test为apk包名,后半部分AndroidJUnitRunner为目标类名,两个部分加起来就唯一确定了一个Instrumentation对象。命令中其他各种配置参数和使用方法就不多说了,更多详情在这里。我们关注的是这个命令到底干了些什么?

Am Instrument

Am Instrument命令会调用Am中的runInstrument()方法,在这个方法中,解析输入的参数并最终将请求发送到ActivityManagerService中的startInstrumentation方法。好吧,一切还是得由AMS来完成。

public boolean startInstrumentation(ComponentName className,
            String profileFile, int flags, Bundle arguments,
            IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection,
            int userId, String abiOverride) { 
            ...
            InstrumentationInfo ii = null; //包含当前App的<Instrumentation>标签信息
            ApplicationInfo ai = null; //包含目标App的<Application>标签信息
            try {
                ii = mContext.getPackageManager().getInstrumentationInfo(
                    className, STOCK_PM_FLAGS);
                ai = AppGlobals.getPackageManager().getApplicationInfo(
                        ii.targetPackage, STOCK_PM_FLAGS, userId);
            } catch (PackageManager.NameNotFoundException e) {
            } catch (RemoteException e) {
            }
            
            //通过PackageManager检查目标App和测试App的签名是否相同
            //只有签名相同才能进行Instrumentation Test
            //签名不相同,失败并抛出异常
            int match = mContext.getPackageManager().checkSignatures(
                    ii.targetPackage, ii.packageName);
            if (match < 0 && match != PackageManager.SIGNATURE_FIRST_NOT_SIGNED) {
                ...
                reportStartInstrumentationFailure(watcher, className, msg);
                throw new SecurityException(msg);
            }

            //先停止当前的Target App
            forceStopPackageLocked(ii.targetPackage, -1, true, false, true, true, false, userId,
                    "start instr");

            //重新启动目标App进程
            ProcessRecord app = addAppLocked(ai, false, abiOverride);
            //记录必要信息,注意这里是唯一赋值的地方
            //所以只有从这个入口过去的,才会创建Instrumentation实例
            //否则,即使在manifest中指定,也不会创建对应实例
            app.instrumentationClass = className;
            app.instrumentationInfo = ai;
            app.instrumentationArguments = arguments;
            ...
        }

        return true;
    }

这里传入startInstrumentation的参数className是一个Component对象,它是在Am中由com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner解析过来的,而argument这个bundle在这里则包含有我们需要执行的Test Class类名。其他流程请看上面代码中的注释说明。这里要强调一下,一旦检查通过,该方法会停止当前正在运行的目标App,然后重新启动目标进程。当目标进程的ActivityThread对象创建以后,会通过attachApplication()方法请求Ams给它绑定一个Application。来看看Ams的处理:

private final boolean attachApplicationLocked(IApplicationThread thread, int pid) {
            ...
            //执行Test App和Target App的dexopt
            ensurePackageDexOpt(app.instrumentationInfo != null
                    ? app.instrumentationInfo.packageName
                    : app.info.packageName);
            if (app.instrumentationClass != null) {
                ensurePackageDexOpt(app.instrumentationClass.getPackageName());
            }
            
            //因为app.instrumentationInfo已经在startInstrumentation方法中赋值为目标App的ApplicationInfo
            //所以不为空
            ApplicationInfo appInfo = app.instrumentationInfo != null
                    ? app.instrumentationInfo : app.info;
            ...
            //返回到ActivityThread中去处理,这里的appInfo为Target App的ApplicationInfo
            thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,
                    profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,
                    app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace,
                    isRestrictedBackupMode || !normalMode, app.persistent,
                    new Configuration(mConfiguration), app.compat,
                    getCommonServicesLocked(app.isolated),
                    mCoreSettingsObserver.getCoreSettingsLocked());
            ...
    }

之后,辗转回到ActivityThread的handleBindApplication方法。

private void handleBindApplication(AppBindData data) {
        mBoundApplication = data;
        
        //data.appInfo是Ams传过来的参数,为target app的ApplicationInfo
        //所以这里会根据ApplicationInfo去load Target Apk    
        data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);

        //创建一个Target App的context对象
        final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
        ...
        //同样是从Ams传过来的参数
        //= 'com.lcd.androidtestpractice.test/android.support.test.runner.AndroidJUnitRunner`
        if (data.instrumentationName != null) {
            InstrumentationInfo ii = null;
            try {
            //获取<instrumentation>标签的信息
                ii = appContext.getPackageManager().
                    getInstrumentationInfo(data.instrumentationName, 0);
            } catch (PackageManager.NameNotFoundException e) {
            }
            ...
            //记录Instrumentation相关信息
            mInstrumentationPackageName = ii.packageName; 
            mInstrumentationAppDir = ii.sourceDir;//Test App的路径
            mInstrumentationSplitAppDirs = ii.splitSourceDirs;
            mInstrumentationLibDir = ii.nativeLibraryDir;
            mInstrumentedAppDir = data.info.getAppDir();//Target App的路径
            mInstrumentedSplitAppDirs = data.info.getSplitAppDirs();
            mInstrumentedLibDir = data.info.getLibDir();

            //根据InstrumentataionInfo构造对应Test App的ApplicationInfo
            ApplicationInfo instrApp = new ApplicationInfo();
            instrApp.packageName = ii.packageName;
            instrApp.sourceDir = ii.sourceDir;
            instrApp.publicSourceDir = ii.publicSourceDir;
            instrApp.splitSourceDirs = ii.splitSourceDirs;
            instrApp.splitPublicSourceDirs = ii.splitPublicSourceDirs;
            instrApp.dataDir = ii.dataDir;
            instrApp.nativeLibraryDir = ii.nativeLibraryDir;

            //根据ApplicationInfo Load Test App
            //appContext.getClassLoader()是target app的classloader
            //这里传入它作为test apk的base class loader
            LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
                    appContext.getClassLoader(), false, true, false);

            //构造一个Test App的context对象
            ContextImpl instrContext = ContextImpl.createAppContext(this, pi);

            try {
                //这个classloader对应为test app的classloader,所以能够加载test apk中的类
                java.lang.ClassLoader cl = instrContext.getClassLoader();
                //从test apk中创建一个Instrumentation实例对象
                //这里其实就是构造一个android.support.test.runner.AndroidJUnitRunner实例
                mInstrumentation = (Instrumentation)
                    cl.loadClass(data.instrumentationName.getClassName()).newInstance();
            } catch (Exception e) {
                ...
            }

            //初始化
            mInstrumentation.init(this, 
                    instrContext,/*对应test app的context,从Instrumentation调用getContext就返回这个context*/
                   appContext,/*对应target app的context,从Instrumentation调用getTargetContext就返回这个context*/
                   new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher,
                   data.instrumentationUiAutomationConnection);

            ...
        } else {
            //创建一个系统默认的Instrumentation对象
            mInstrumentation = new Instrumentation();
        }
        //创建application对象,这里data.info指向target app的loadedapk对象
        Application app = data.info.makeApplication(data.restrictedBackupMode,null);
        mInitialApplication = app;
        ...
        //调用Instrumentation的onCreate方法
        mInstrumentation.onCreate(data.instrumentationArgs);
        ...
    }

具体的说明请看上面的注释。上面的方法调用之后,ActivityThread中的mPackages变量会包含两个LoadedApk,分别对应Test app和target app。我们可以看到,通过使用Instrumentation,Android将Test App和Target App同时加载到了同一个进程中。到这里,我们已经创建了AndroidJUnitRunner对象实例。来看看onCreate方法。

public class AndroidJUnitRunner extends MonitoringInstrumentation {

        ...
        @Override
        public void onCreate(Bundle arguments) {
            //保存并解析参数
            mArguments = arguments;
            parseRunnerArgs(mArguments);

            //调用父类实现
            super.onCreate(arguments);

            ...
            start();
        }
    }

再来看看父类MonitoringInstrumentationonCreate实现。

public void onCreate(Bundle arguments) {
        
        //向InstrumentationRegistry中注册这个Instrumentation实例
        //这样在测试代码中,就可以通过InstrumentationRegistry.getInstrumentation()方法获取这个实例
        InstrumentationRegistry.registerInstance(this, arguments);
        ActivityLifecycleMonitorRegistry.registerInstance(mLifecycleMonitor);
        ApplicationLifecycleMonitorRegistry.registerInstance(mApplicationMonitor);
        IntentMonitorRegistry.registerInstance(mIntentMonitor);

        mHandlerForMainLooper = new Handler(Looper.getMainLooper());
        final int corePoolSize = 0;
        final long keepAliveTime = 0L;
        mExecutorService = new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, keepAliveTime,
                TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread thread = Executors.defaultThreadFactory().newThread(runnable);
                thread.setName(MonitoringInstrumentation.class.getSimpleName());
                return thread;
            }
        });
        Looper.myQueue().addIdleHandler(mIdleHandler);
        //调用Instrumentation的onCreate方法,空方法,不关注
        super.onCreate(arguments);
        specifyDexMakerCacheProperty();
        setupDexmakerClassloader();
    }

onCreate方法结束之后,AndroidJUnitRunner紧接着调用start()方法。start()的实现位于基类Instrumentation中,如下:

public void start() {
        if (mRunner != null) {
            throw new RuntimeException("Instrumentation already started");
        }
        mRunner = new InstrumentationThread("Instr: " + getClass().getName());
        mRunner.start();
    }
private final class InstrumentationThread extends Thread {
        ...
        public void run() {
            ...
            onStart();
        }
    }

InstrumentationThread是一个线程类,在start()方法中会新建这个线程并启动,线程转而执行onStart()方法。该方法的具体实现在AndroidJUnitRunner中。

@Override
    public void onStart() {
        ...
        TestExecutor.Builder executorBuilder = new TestExecutor.Builder(this);
        addListeners(mRunnerArgs, executorBuilder);
        TestRequest testRequest = buildRequest(mRunnerArgs, getArguments());
        results = executorBuilder.build().execute(testRequest);
        ...
        finish(Activity.RESULT_OK, results);
    }

山重水复疑无路,很接近了,耐心!在onStart()这个方法中构建了一个TestRequest,然后又构建一个TestExecutor,并调用其execute()方法执行这个test request,最后调用finish()方法。我们先来看看finish做了什么?

public void finish(int resultCode, Bundle results) {
        ...
        mThread.finishInstrumentation(resultCode, results);
    }

finish方法通过ActivityThread的finishInstrumentation方法通知Ams完成测试工作,Ams最后来做一些收尾的清理工作并结束当前进程。代码就不给出了。所以到finish的时候,我们的测试工作已经完成了,所以我们可以肯定我们的测试代码就是通过TestExecutor来运行的。现在回头来看看它的execute()方法,看看具体怎么执行。

public Bundle execute(TestRequest testRequest) {
        ...
        JUnitCore testRunner = new JUnitCore();
        setUpListeners(testRunner);
        junitResults = testRunner.run(testRequest.getRequest());
        junitResults.getFailures().addAll(testRequest.getFailures());
        ...
    }

柳暗花明又一村啊!execute()方法中构建了一个JUnitCore对象,调用其run(request)方法执行。等等,这里的JUnitCore不就是我们上篇分析的JUnit执行Test Class的入口吗?还需要继续吗?我想,到这里应该可以告一段落了,因为往后的执行逻辑跟我们前面分析的JUnit的执行逻辑是一样一样的。这里在提一点,Android通过覆写AllDefaultPossibilitiesBuilder来为Test Class生成默认Runner。

class AndroidRunnerBuilder extends AllDefaultPossibilitiesBuilder {

    ...
    public AndroidRunnerBuilder(AndroidRunnerParams runnerParams) {
        super(true);
        mAndroidJUnit3Builder = new AndroidJUnit3Builder(runnerParams);
        mAndroidJUnit4Builder = new AndroidJUnit4Builder(runnerParams);
        mAndroidSuiteBuilder = new AndroidSuiteBuilder(runnerParams);
        mAndroidAnnotatedBuilder = new AndroidAnnotatedBuilder(this, runnerParams);
        mIgnoredBuilder = new IgnoredBuilder();
    }

    ...
}

Android在为一个Test Class构造Runner时,使用的就是这个RunnerBuilder。该RunnerBuilder提供了基于不同JUnit版本的默认Android Runner版本。比如我们的Test Class不显示的声明@RunWith,那么基于JUnit4,RunnerBuilder给我们构造的就是AndroidJUnit4ClassRunner,其实就是AndroidJUnit4这个Runner。

最后的最后,再提一点。那就是class loader的问题。因为是两个apk运行在同一个进程里面,怎么保证类的加载不会出错呢?其实Android这点已经为我们处理了。在ActivityThread中有几个成员变量,保存了Test App和Target App的apk路径信息。

    String mInstrumentationPackageName = null; //test app package name
    String mInstrumentationAppDir = null; //test app apk path
    String[] mInstrumentationSplitAppDirs = null;
    String mInstrumentationLibDir = null;
    String mInstrumentedAppDir = null; //target app apk path
    String[] mInstrumentedSplitAppDirs = null;
    String mInstrumentedLibDir = null;

这些信息用于LoadedApk构建相应的classloader,从而可以满足从test app或者target app中正确的load我们想要的类。具体可以看LoadedApk中的getClassLoader()方法,这里就不再讲述。最后的最后的最后,给出android官方的一张图,请自行脑补!

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

推荐阅读更多精彩内容