Android Small 源碼分析(一) 啟動流程

17年春節(jié)前這段時間一直抱著Small(aarVersion = '1.1.0-alpha1'),打算從零開始一個新的項目筒占。心想Small源代碼也該好好看看,究竟從哪里開始才好呢蜘犁? 還是從官方例子出發(fā)翰苫,從啟動開始深入。

首先從Small在Github 上的Sample項目開始了解Small的基本狀況这橙,如果要深入了解Small 的具體實現(xiàn)奏窑,就必須深入到其中的DevSample項目,因為這才是Small的源碼所在屈扎。

源碼目錄結(jié)構(gòu)

通過UML工具(CodeIris 插件)可以一窺其內(nèi)部埃唯,得到相關(guān)的類圖,這里是以Smalll類為源頭鹰晨,省略一部分類墨叛,大致了解到核心所在。

從實際應(yīng)用Small過程中模蜡,想必會關(guān)注到bundle.json這個文件漠趁,可以看得出BundleLaucher 是個關(guān)鍵類。因此將其抽取出來忍疾,分析關(guān)聯(lián)的類結(jié)構(gòu)(這里省略覆蓋方法)闯传。

BundleLauncher以及 SoBundleLauncher 都是抽象類,具體的應(yīng)用得細看ApkBundleLauncher膝昆、ActivityLauncher這兩個類丸边,我會在后面具體分析到。

到此荚孵,我們已經(jīng)大致了解Small的源碼目錄結(jié)構(gòu)妹窖,我們要退一步從Small Sample項目開始,也就是用實際應(yīng)用的項目開始溯源收叶。

一. Small 啟動流程

(一)Small 預(yù)處理

首先我們看一下宿主app的Application類:

public class Application extends android.app.Application {
    public Application() {
        // This should be the very first of the application lifecycle.
        // It's also ahead of the installing of content providers by what we can avoid
        // the ClassNotFound exception on if the provider is unimplemented in the host.
        Small.preSetUp(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // Optional
        Small.setBaseUri("http://m.wequick.net/demo/");//設(shè)定基本的跳轉(zhuǎn)地址
        Small.setWebViewClient(new MyWebViewClient());//設(shè)置網(wǎng)頁的基本回調(diào)
        Small.setLoadFromAssets(BuildConfig.LOAD_FROM_ASSETS);
    }

    private static final class MyWebViewClient extends WebViewClient {
     ...
    }
}

Small.java內(nèi)部基本都是靜態(tài)方法骄呼,是Small框架的程序入口。在Application的構(gòu)造方法中調(diào)用Small.preSetUp(this),意思就是這應(yīng)該在application的生命周期中最早調(diào)用的蜓萄,也是安裝Content Providers 之前隅茎,可以避免在宿主中發(fā)生ClassNotFound 異常.
  具體來看看preSetUp里面吧。

public static void preSetUp(Application context) {
        if (sContext != null) {
            return;
        }

        sContext = context;

        // Register default bundle launchers
        registerLauncher(new ActivityLauncher());
        registerLauncher(new ApkBundleLauncher());
        registerLauncher(new WebBundleLauncher());
        Bundle.onCreateLaunchers(context);
    }

通過Bundle.registerLauncher方法添加三個BundleLauncher到Bundle.sBundleLaunchers列表嫉沽,在通過 Bundle.onCreateLaunchers啟動這三者的onCreate方法辟犀,但其實只有ApkBundleLauncher覆蓋了OnCreate 方法。
  ApkBundleLauncher是bundle加載的管理類绸硕,我們看看它的OnCreate是怎么定義的堂竟。

@Override
    public void onCreate(Application app) {
        super.onCreate(app);

        Object/*ActivityThread*/ thread;
        List<ProviderInfo> providers;
        Instrumentation base;
        ApkBundleLauncher.InstrumentationWrapper wrapper;
        Field f;

        // 通過getActivityThread反射獲取ActivityThread的對象
        thread = ReflectAccelerator.getActivityThread(app);

        // 將自定義的InstrumentationWrapper替換掉原來的mInstumentation
       // Small通過占坑的方式管理Activity的.重點就在這里
        try {
            f = thread.getClass().getDeclaredField("mInstrumentation");//取得mInstrumentation屬性
            f.setAccessible(true);
            base = (Instrumentation) f.get(thread);
            wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
            f.set(thread, wrapper);
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
        }

        // 繼續(xù)替換Message Handler,用于恢復(fù)Activity Info 到真實的Activity
        try {
            f = thread.getClass().getDeclaredField("mH");
            f.setAccessible(true);
            Handler ah = (Handler) f.get(thread);
            f = Handler.class.getDeclaredField("mCallback");
            f.setAccessible(true);
            f.set(ah, new ApkBundleLauncher.ActivityThreadHandlerCallback());
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace message handler for thread: " + thread);
        }

        // 獲取App的provider列表
        try {
            f = thread.getClass().getDeclaredField("mBoundApplication");
            f.setAccessible(true);
            Object/*AppBindData*/ data = f.get(thread);
            f = data.getClass().getDeclaredField("providers");
            f.setAccessible(true);
            providers = (List<ProviderInfo>) f.get(data);
        } catch (Exception e) {
            throw new RuntimeException("Failed to get providers from thread: " + thread);
        }
        // 保存到全局變量玻佩,便于以后替換管理
        sActivityThread = thread;
        sProviders = providers;
        sHostInstrumentation = base;
        sBundleInstrumentation = wrapper;
    }

ApkBundleLauncher.InstrumentationWrapper 這個是關(guān)鍵類出嘹,Android activities受Instrumentation監(jiān)控。每一個Activity由Activity的startActivityForResult
方法啟動咬崔,通過instrumentation的execStartActivity方法激活生命周期税稼;

public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
    if (mParent == null) {
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity( // Override entry 1
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
        ...
    }
}

ActivityThread的performLaunchActivity方法中通過instrumentation的newActivity方法實例化。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...

    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity( // Override entry 2
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }
    ...
}

Small 想要做到動態(tài)注冊Activity垮斯,首先在宿主manifest中注冊一個命名特殊的占坑activity來欺騙startActivityForResult以獲得生命周期郎仆,再欺騙performLaunchActivity來獲得插件activity實例。又為了處理之間的信息傳遞兜蠕,因此有了后面的ActivityThreadHandlerCallback丸升。

(二)LaunchActivity 啟動初始化

Small.setUp這個方法需要放在OnStart()中執(zhí)行,避免一些問題的產(chǎn)生牺氨,初始化完成之后,回調(diào)onComplete方法墩剖。完成加載之后猴凹,之后就交給插件的業(yè)務(wù)邏輯了。

@Override
    protected void onStart() {
        super.onStart();

        SharedPreferences sp = LaunchActivity.this.getSharedPreferences("profile", 0);
        final SharedPreferences.Editor se = sp.edit();
        final long tStart = System.nanoTime();
        se.putLong("setUpStart", tStart);
        Small.setUp(LaunchActivity.this, new net.wequick.small.Small.OnCompleteListener() {
            @Override
            public void onComplete() {
                long tEnd = System.nanoTime();
                se.putLong("setUpFinish", tEnd).apply();
                long offset = tEnd - tStart;
                if (offset < MIN_INTRO_DISPLAY_TIME) {
                    // 這個延遲僅為了讓 "Small Logo" 顯示足夠的時間, 實際應(yīng)用中不需要
                    getWindow().getDecorView().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            Small.openUri("main", LaunchActivity.this);
                            finish();
                        }
                    }, (MIN_INTRO_DISPLAY_TIME - offset) / 1000000);
                } else {
                    Small.openUri("main", LaunchActivity.this);
                    finish();
                }
            }
        });
    }

現(xiàn)在我們跟蹤進setUp方法岭皂,看看里面究竟做了什么郊霎。

public static void setUp(Context context, OnCompleteListener listener) {
        if (sContext == null) {
            // Tips for CODE-BREAKING
            throw new UnsupportedOperationException(
                    "Please call `Small.preSetUp' in your application first");
        }

        if (sHasSetUp) {
            if (listener != null) {
                listener.onComplete();
            }
            return;
        }

        Bundle.loadLaunchableBundles(listener);
        sHasSetUp = true;
    }

可以看到Small確認還沒初始化之后,就遞給了Bundle類爷绘,在其內(nèi)部執(zhí)行靜態(tài)方法loadLaunchableBundles书劝,我們繼續(xù)往下看。

 protected static void loadLaunchableBundles(Small.OnCompleteListener listener) {
        Context context = Small.getContext();

        boolean synchronous = (listener == null);
        if (synchronous) {
            loadBundles(context);
            return;
        }

        // Asynchronous
        if (sThread == null) {
            sThread = new LoadBundleThread(context);
            sHandler = new LoadBundleHandler(listener);
            sThread.start();
        }
    }

這個方法很簡單土至,啟動一個LoadBundleThread線程购对,一個Handler處理完成后的事項。在LoadBundleThread里面我們肯定會看到bundle.json的處理陶因。

private static class LoadBundleThread extends Thread {
        Context mContext;
        public LoadBundleThread(Context context) {
            mContext = context;
        }

        @Override
        public void run() {
            loadBundles(mContext);
            sHandler.obtainMessage(MSG_COMPLETE).sendToTarget();
        }
    }

這里和之前的版本比較骡苞,會發(fā)現(xiàn)少了Bundle.setupLaunchers(Context)方法,仔細往下就會發(fā)現(xiàn)其實它被延后到loadBundles內(nèi)部了。

private static void loadBundles(Context context) {
        JSONObject manifestData;
        try {
            File patchManifestFile = getPatchManifestFile();// app路徑/file/bundle.json
            String manifestJson = getCacheManifest();//SharedPreferences的bundle.json字段
            if (manifestJson != null) {
                // Load from cache and save as patch
                if (!patchManifestFile.exists()) patchManifestFile.createNewFile();
                PrintWriter pw = new PrintWriter(new FileOutputStream(patchManifestFile));
                pw.print(manifestJson);
                pw.flush();
                pw.close();
                // Clear cache
                setCacheManifest(null);
            } else if (patchManifestFile.exists()) {
                // Load from patch
                BufferedReader br = new BufferedReader(new FileReader(patchManifestFile));
                StringBuilder sb = new StringBuilder();
                String line;
                while ((line = br.readLine()) != null) {
                    sb.append(line);
                }

                br.close();
                manifestJson = sb.toString();
            } else {
                // Load from built-in `assets/bundle.json'
                InputStream builtinManifestStream = context.getAssets().open(BUNDLE_MANIFEST_NAME);
                int builtinSize = builtinManifestStream.available();
                byte[] buffer = new byte[builtinSize];
                builtinManifestStream.read(buffer);
                builtinManifestStream.close();
                manifestJson = new String(buffer, 0, builtinSize);
            }

            // Parse manifest file
            manifestData = new JSONObject(manifestJson);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        Manifest manifest = parseManifest(manifestData);
        if (manifest == null) return;

        setupLaunchers(context);

        loadBundles(manifest.bundles);
    }

由上可知解幽,處理bundle.json 有位置優(yōu)先級關(guān)系:manifestJson > patchManifestFile > assets/bundle.json 贴见,也就是SharedPreferences > File > Assets 。因此躲株,我們最初的配置在Assets里面的bundle.json 會在最后處理片部。
  接著回到先前提及的setupLaunchers,回憶一下霜定,在preSetUp中我們看到添加了3個BundleLauncher档悠,這里就是對三者的setUp進行調(diào)用。

protected static void setupLaunchers(Context context) {
        if (sBundleLaunchers == null) return;

        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.setUp(context);
        }
    }
  1. ActivityLauncher的setUp方法
      我們繼續(xù)跟蹤進去然爆,首先看看ActivityLauncher的setUp方法,它將注冊在宿主的activities添加到sActivityClasses里面站粟。
public class ActivityLauncher extends BundleLauncher {
    ...

    @Override
    public void setUp(Context context) {
        super.setUp(context);

        // Read the registered classes in host's manifest file
        PackageInfo pi;
        try {
            pi = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), PackageManager.GET_ACTIVITIES);
        } catch (PackageManager.NameNotFoundException ignored) {
            // Never reach
            return;
        }
        ActivityInfo[] as = pi.activities;
        if (as != null) {
            sActivityClasses = new HashSet<String>();
            for (ActivityInfo ai : as) {
                sActivityClasses.add(ai.name);
            }
        }
    }
    ...
}
  1. ApkBundleLauncher的setUp方法
      這里與之前的ApkBundleLauncher的onCreate方法相呼應(yīng),通過動態(tài)代理的方式曾雕,將Intent參數(shù)重新包裝來完成上面所說的欺騙方式奴烙。
public class ApkBundleLauncher extends SoBundleLauncher {
     ...

    @Override
    public void setUp(Context context) {
        super.setUp(context);

        Field f;

        // AOP for pending intent
        try {
            f = TaskStackBuilder.class.getDeclaredField("IMPL");
            f.setAccessible(true);
            final Object impl = f.get(TaskStackBuilder.class);
            InvocationHandler aop = new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Intent[] intents = (Intent[]) args[1];
                    for (Intent intent : intents) {
                        sBundleInstrumentation.wrapIntent(intent);
                        intent.setAction(Intent.ACTION_MAIN);
                        intent.addCategory(Intent.CATEGORY_LAUNCHER);
                    }
                    return method.invoke(impl, args);
                }
            };
            Object newImpl = Proxy.newProxyInstance(context.getClassLoader(), impl.getClass().getInterfaces(), aop);
            f.set(TaskStackBuilder.class, newImpl);
        } catch (Exception ignored) {
            ignored.printStackTrace();
        }
    }
    ...
}

我們進去wrapIntent看看,如果不是精確命中,intent.getComponent()為空剖张,因此首先處理交給宿主的沖突問題切诀,如果是系統(tǒng)或者宿主的Action,直接退出搔弄;

private void wrapIntent(Intent intent) {
            ComponentName component = intent.getComponent();
            String realClazz;
            if (component == null) {
                // Try to resolve the implicit action which has registered in host.
                component = intent.resolveActivity(Small.getContext().getPackageManager());
                if (component != null) {
                    // A system or host action, nothing to be done.
                    return;
                }

                // Try to resolve the implicit action which has registered in bundles.
                realClazz = resolveActivity(intent);
                if (realClazz == null) {
                    // Cannot resolved, nothing to be done.
                    return;
                }
            } else {
                realClazz = component.getClassName();
                if (realClazz.startsWith(STUB_ACTIVITY_PREFIX)) {
                    // Re-wrap to ensure the launch mode works.
                    realClazz = unwrapIntent(intent);
                }
            }

            if (sLoadedActivities == null) return;

            ActivityInfo ai = sLoadedActivities.get(realClazz);
            if (ai == null) return;

            // Carry the real(plugin) class for incoming `newActivity' method.
            intent.addCategory(REDIRECT_FLAG + realClazz);
            String stubClazz = dequeueStubActivity(ai, realClazz);
            intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
        }

否則嘗試處理插件內(nèi)的沖突(resolveActivity(intent)),初始失敗則退出幅虑。


        private String resolveActivity(Intent intent) {
            if (sLoadedIntentFilters == null) return null;

            Iterator<Map.Entry<String, List<IntentFilter>>> it =
                    sLoadedIntentFilters.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, List<IntentFilter>> entry = it.next();
                List<IntentFilter> filters = entry.getValue();
                for (IntentFilter filter : filters) {
                    if (filter.hasAction(Intent.ACTION_VIEW)) {
                        // TODO: match uri
                    }
                    if (filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
                        // custom action
                        if (filter.hasAction(intent.getAction())) {
                            // hit
                            return entry.getKey();
                        }
                    }
                }
            }
            return null;
        }

如果精確命中,先解開(unwrapIntent(intent);),后面重新選取一個未使用的activity坑與之使用顾犹。

        private String[] mStubQueue;
        /** Get an usable stub activity clazz from real activity */
        private String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) {
            if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
                // In standard mode, the stub activity is reusable.
                // Cause the `windowIsTranslucent' attribute cannot be dynamically set,
                // We should choose the STUB activity with translucent or not here.
                Resources.Theme theme = Small.getContext().getResources().newTheme();
                theme.applyStyle(ai.getThemeResource(), true);
                TypedArray sa = theme.obtainStyledAttributes(
                        new int[] { android.R.attr.windowIsTranslucent });
                boolean translucent = sa.getBoolean(0, false);
                sa.recycle();
                return translucent ? STUB_ACTIVITY_TRANSLUCENT : STUB_ACTIVITY_PREFIX;
            }

            int availableId = -1;
            int stubId = -1;
            int countForMode = STUB_ACTIVITIES_COUNT;
            int countForAll = countForMode * 3; // 3=[singleTop, singleTask, singleInstance]
            if (mStubQueue == null) {
                // Lazy init
                mStubQueue = new String[countForAll];
            }
            int offset = (ai.launchMode - 1) * countForMode;
            for (int i = 0; i < countForMode; i++) {
                String usedActivityClazz = mStubQueue[i + offset];
                if (usedActivityClazz == null) {
                    if (availableId == -1) availableId = i;
                } else if (usedActivityClazz.equals(realActivityClazz)) {
                    stubId = i;
                }
            }
            if (stubId != -1) {
                availableId = stubId;
            } else if (availableId != -1) {
                mStubQueue[availableId + offset] = realActivityClazz;
            } else {
                // TODO:
                Log.e(TAG, "Launch mode " + ai.launchMode + " is full");
            }
            return STUB_ACTIVITY_PREFIX + ai.launchMode + availableId;
        }

        private static String unwrapIntent(Intent intent) {
            Set<String> categories = intent.getCategories();
            if (categories == null) return null;

            // Get plugin activity class name from categories
            Iterator<String> it = categories.iterator();
            while (it.hasNext()) {
                String category = it.next();
                if (category.charAt(0) == REDIRECT_FLAG) {
                    return category.substring(1);
                }
            }
            return null;
      }
  1. ApkBundleLauncher的setUp方法的setUp方法
      啟動一個新的android 本身的WebView倒庵,這里作者特別注釋到:在Android7.0上,在第一次創(chuàng)建WebView的時候炫刷,它會用WebView的Assets路徑替換掉原Application Assets路徑擎宝,一旦發(fā)生我們先前所做的努力化為泡影,因此我們盡可能將它推到前面設(shè)置浑玛。
 @Override
    public void setUp(Context context) {
        super.setUp(context);
        if (Build.VERSION.SDK_INT < 24) return;

        Bundle.postUI(new Runnable() {
            @Override
            public void run() {
                // In android 7.0+, on firstly create WebView, it will replace the application
                // assets with the one who has join the WebView asset path.
                // If this happens after our assets replacement,
                // what we have done would be come to naught!
                // So, we need to push it enOOOgh ahead! (#347)
                new android.webkit.WebView(Small.getContext());
            }
        });
    }

準備工作都完成了绍申,接著到調(diào)用loadBundles的方法,加載所有模塊顾彰,這里在 prepareForLaunch() 包含了很多處理极阅,這里暫且不詳細講了,不然又要扯很遠再回來涨享,下一節(jié)會詳細講筋搏。
  等待這一切完成之后,就通過Handler通知完成厕隧,回到LaunchActivity的OnStart內(nèi)繼續(xù)執(zhí)行拆又。

private static void loadBundles(List<Bundle> bundles) {
        sPreloadBundles = bundles;

        // Prepare bundle
        for (Bundle bundle : bundles) {
            bundle.prepareForLaunch();//會產(chǎn)生IO操作
        }

        // Handle I/O
        if (sIOActions != null) {
            ExecutorService executor = Executors.newFixedThreadPool(sIOActions.size());
            for (Runnable action : sIOActions) {
                executor.execute(action);
            }
            executor.shutdown();
            try {
                if (!executor.awaitTermination(LOADING_TIMEOUT_MINUTES, TimeUnit.MINUTES)) {
                    throw new RuntimeException("Failed to load bundles! (TIMEOUT > "
                            + LOADING_TIMEOUT_MINUTES + "minutes)");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sIOActions = null;
        }

        // Wait for the things to be done on UI thread before `postSetUp`,
        // as on 7.0+ we should wait a WebView been initialized. (#347)
        while (sRunningUIActionCount != 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // Notify `postSetUp' to all launchers
        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.postSetUp();
        }

        // Wait for the things to be done on UI thread after `postSetUp`,
        // like creating a bundle application.
        while (sRunningUIActionCount != 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // Free all unused temporary variables
        for (Bundle bundle : bundles) {
            if (bundle.parser != null) {
                bundle.parser.close();
                bundle.parser = null;
            }
            bundle.mBuiltinFile = null;
            bundle.mExtractPath = null;
        }
    }

至此儒旬,啟動流程基本講完了,插件的加載過程帖族,也就是prepareForLaunch()部分栈源,我將在下一節(jié)再詳細介紹。
  敬請期待J恪甚垦!!

第二節(jié):Android Small 源碼分析(二) 插件加載過程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市涣雕,隨后出現(xiàn)的幾起案子艰亮,更是在濱河造成了極大的恐慌,老刑警劉巖挣郭,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迄埃,死亡現(xiàn)場離奇詭異,居然都是意外死亡兑障,警方通過查閱死者的電腦和手機侄非,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來流译,“玉大人逞怨,你說我怎么就攤上這事「T瑁” “怎么了叠赦?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長革砸。 經(jīng)常有香客問我除秀,道長,這世上最難降的妖魔是什么算利? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任鳞仙,我火速辦了婚禮,結(jié)果婚禮上笔时,老公的妹妹穿的比我還像新娘。我一直安慰自己仗岸,他們只是感情好允耿,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扒怖,像睡著了一般较锡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盗痒,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天蚂蕴,我揣著相機與錄音低散,去河邊找鬼。 笑死骡楼,一個胖子當(dāng)著我的面吹牛熔号,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鸟整,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼引镊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了篮条?” 一聲冷哼從身側(cè)響起弟头,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涉茧,沒想到半個月后赴恨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡伴栓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年伦连,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挣饥。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡除师,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扔枫,到底是詐尸還是另有隱情汛聚,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布短荐,位于F島的核電站倚舀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏忍宋。R本人自食惡果不足惜痕貌,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望糠排。 院中可真熱鬧舵稠,春花似錦、人聲如沸入宦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乾闰。三九已至落追,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涯肩,已是汗流浹背轿钠。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工巢钓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疗垛。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓症汹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親继谚。 傳聞我的和親對象是個殘疾皇子烈菌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容