Android 9.0 Launcher源码分析(二)——Launcher应用启动流程,数据加载与绑定

转载请注明原地址:https://www.jianshu.com/p/725bdb3d08aa

上一篇文章中分析了系统是如何把桌面应用拉起的。(见Android 9.0 Launcher源码分析(一)——系统启动Launcher流程

现在接上文,分析一下Launcher应用的启动流程。
首先把Launcher的onCreate贴出来。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (DEBUG_STRICT_MODE) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()   // or .detectAll() for all detectable problems
                    .penaltyLog()
                    .build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .detectLeakedClosableObjects()
                    .penaltyLog()
                    .penaltyDeath()
                    .build());
        }
        TraceHelper.beginSection("Launcher-onCreate");

        super.onCreate(savedInstanceState);
        TraceHelper.partitionSection("Launcher-onCreate", "super call");

        LauncherAppState app = LauncherAppState.getInstance(this);
        mOldConfig = new Configuration(getResources().getConfiguration());
        mModel = app.setLauncher(this);
        initDeviceProfile(app.getInvariantDeviceProfile());

        mSharedPrefs = Utilities.getPrefs(this);
        mIconCache = app.getIconCache();
        mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);

        mDragController = new DragController(this);
        mAllAppsController = new AllAppsTransitionController(this);
        mStateManager = new LauncherStateManager(this);
        UiFactory.onCreate(this);

        mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);

        mAppWidgetHost = new LauncherAppWidgetHost(this);
        mAppWidgetHost.startListening();

        mLauncherView = LayoutInflater.from(this).inflate(R.layout.launcher, null);

        setupViews();
        mPopupDataProvider = new PopupDataProvider(this);

        mRotationHelper = new RotationHelper(this);
        mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);

        boolean internalStateHandled = InternalStateHandler.handleCreate(this, getIntent());
        if (internalStateHandled) {
            if (savedInstanceState != null) {
                // InternalStateHandler has already set the appropriate state.
                // We dont need to do anything.
                savedInstanceState.remove(RUNTIME_STATE);
            }
        }
        restoreState(savedInstanceState);

        // We only load the page synchronously if the user rotates (or triggers a
        // configuration change) while launcher is in the foreground
        int currentScreen = PagedView.INVALID_RESTORE_PAGE;
        if (savedInstanceState != null) {
            currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
        }

        if (!mModel.startLoader(currentScreen)) {
            if (!internalStateHandled) {
                // If we are not binding synchronously, show a fade in animation when
                // the first page bind completes.
                mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
            }
        } else {
            // Pages bound synchronously.
            mWorkspace.setCurrentPage(currentScreen);

            setWorkspaceLoading(true);
        }

        // For handling default keys
        setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);

        setContentView(mLauncherView);
        getRootView().dispatchInsets();

        // Listen for broadcasts
        registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));

        getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW,
                Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText));

        if (mLauncherCallbacks != null) {
            mLauncherCallbacks.onCreate(savedInstanceState);
        }
        mRotationHelper.initialize();

        TraceHelper.endSection("Launcher-onCreate");
    }

我们从头开始看,在super.onCreate过后,首先调用了LauncherAppState.getInstance(this)来初始化一个单例对象。LauncherAppState里面保存了一些比较常用的对象,方便其他地方通过单例来获取,比如IconCache(图标缓存)、LauncherModel(负责数据加载和处理各种回调)等。getInstance函数如下,注意这里初始化使用的application的Context,因为单例作为static对象,生命周期是与application生命周期一样长的,如果这里使用了Activity的Context,会导致activity退出后,该Context依然被单例持有而无法回收,于是出现内存泄露。

public static LauncherAppState getInstance(final Context context) {
        if (INSTANCE == null) {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                INSTANCE = new LauncherAppState(context.getApplicationContext());
            } else {
                try {
                    return new MainThreadExecutor().submit(new Callable<LauncherAppState>() {
                        @Override
                        public LauncherAppState call() throws Exception {
                            return LauncherAppState.getInstance(context);
                        }
                    }).get();
                } catch (InterruptedException|ExecutionException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return INSTANCE;
    }

继续看LauncherAppState的初始化过程。这里面其实就是各个对象的实例创建过程,并且注册了一些系统事件的监听。

    private LauncherAppState(Context context) {
        if (getLocalProvider(context) == null) {
            throw new RuntimeException(
                    "Initializing LauncherAppState in the absence of LauncherProvider");
        }
        Log.v(Launcher.TAG, "LauncherAppState initiated");
        Preconditions.assertUIThread();
        mContext = context;

        mInvariantDeviceProfile = new InvariantDeviceProfile(mContext);
        mIconCache = new IconCache(mContext, mInvariantDeviceProfile);
        mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
        mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));

        LauncherAppsCompat.getInstance(mContext).addOnAppsChangedCallback(mModel);

        // Register intent receivers
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
        // For handling managed profiles
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED);

        if (FeatureFlags.IS_DOGFOOD_BUILD) {
            filter.addAction(ACTION_FORCE_ROLOAD);
        }

        mContext.registerReceiver(mModel, filter);
        UserManagerCompat.getInstance(mContext).enableAndResetCache();
        new ConfigMonitor(mContext).register();

        if (!mContext.getResources().getBoolean(R.bool.notification_badging_enabled)) {
            mNotificationBadgingObserver = null;
        } else {
            // Register an observer to rebind the notification listener when badging is re-enabled.
            mNotificationBadgingObserver = new SettingsObserver.Secure(
                    mContext.getContentResolver()) {
                @Override
                public void onSettingChanged(boolean isNotificationBadgingEnabled) {
                    if (isNotificationBadgingEnabled) {
                        NotificationListener.requestRebind(new ComponentName(
                                mContext, NotificationListener.class));
                    }
                }
            };
            mNotificationBadgingObserver.register(NOTIFICATION_BADGING);
        }
    }

回到刚才的Launcher创建流程,LauncherAppState初始化完成后,有这样一句mModel = app.setLauncher(this),这里调用了mModel.initialize(launcher),这里将传过来的Callbacks对象(也就是Launcher,Launcher实现了Callbacks接口)保存为了弱引用。同样是基于避免内存泄露的考虑。还记得上文提到的LauncherAppState,LauncherModel是其内部的一个成员变量,生命周期也是比Launcher这个Activity要长的。

public void initialize(Callbacks callbacks) {
        synchronized (mLock) {
            Preconditions.assertUIThread();
            mCallbacks = new WeakReference<>(callbacks);
        }
    }

继续Launcher的create,之后是initDeviceProfile(app.getInvariantDeviceProfile()),DeviceProfile是与Launcher布局相关的一个重要类,这里面保存了所有布局相关数据比如图标大小、页面宽高、各种padding等等。然后创建一些其他对象后,终于inflate了R.layout.launcher。继续往下就执行到一个关键函数mModel.startLoader(currentScreen),前面执行的都是诸如对象创建、View的inflate等逻辑,并没有涉及到数据相关的内容,此函数就是开启Launcher数据加载的一个调用。

之后又是一些初始化的逻辑。所以我们前面啰嗦一大堆,其实onCreate干的事情简单说来就是初始化对象、加载布局、注册一些事件监听、以及开启数据加载。

接着看数据加载与绑定流程。数据加载的调用实际是这样的startLoader()→startLoaderForResults()。从如下代码中可知,数据加载时在一个工作线程去做的,这是很正常的一个选择,避免阻塞主线程。

public void startLoaderForResults(LoaderResults results) {
        synchronized (mLock) {
            stopLoader();
            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
            runOnWorkerThread(mLoaderTask);
        }
    }

private static void runOnWorkerThread(Runnable r) {
        if (sWorkerThread.getThreadId() == Process.myTid()) {
            r.run();
        } else {
            // If we are not on the worker thread, then post to the worker handler
            sWorker.post(r);
        }
    }

在工作线程跑的是一个LoaderTask类,实现了Runnable接口。我们直接来看其run函数的定义。

    public void run() {
        synchronized (this) {
            // Skip fast if we are already stopped.
            if (mStopped) {
                return;
            }
        }

        TraceHelper.beginSection(TAG);
        try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
            TraceHelper.partitionSection(TAG, "step 1.1: loading workspace");
            loadWorkspace();

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 1.2: bind workspace workspace");
            mResults.bindWorkspace();

            // Notify the installer packages of packages with active installs on the first screen.
            TraceHelper.partitionSection(TAG, "step 1.3: send first screen broadcast");
            sendFirstScreenActiveInstallsBroadcast();

            // Take a break
            TraceHelper.partitionSection(TAG, "step 1 completed, wait for idle");
            waitForIdle();
            verifyNotStopped();

            // second step
            TraceHelper.partitionSection(TAG, "step 2.1: loading all apps");
            loadAllApps();

            TraceHelper.partitionSection(TAG, "step 2.2: Binding all apps");
            verifyNotStopped();
            mResults.bindAllApps();

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 2.3: Update icon cache");
            updateIconCache();

            // Take a break
            TraceHelper.partitionSection(TAG, "step 2 completed, wait for idle");
            waitForIdle();
            verifyNotStopped();

            // third step
            TraceHelper.partitionSection(TAG, "step 3.1: loading deep shortcuts");
            loadDeepShortcuts();

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 3.2: bind deep shortcuts");
            mResults.bindDeepShortcuts();

            // Take a break
            TraceHelper.partitionSection(TAG, "step 3 completed, wait for idle");
            waitForIdle();
            verifyNotStopped();

            // fourth step
            TraceHelper.partitionSection(TAG, "step 4.1: loading widgets");
            mBgDataModel.widgetsModel.update(mApp, null);

            verifyNotStopped();
            TraceHelper.partitionSection(TAG, "step 4.2: Binding widgets");
            mResults.bindWidgets();

            transaction.commit();
        } catch (CancellationException e) {
            // Loader stopped, ignore
            TraceHelper.partitionSection(TAG, "Cancelled");
        }
        TraceHelper.endSection(TAG);
    }

非常清晰明了,一步一步通过注释和Log都标出来了。Launcher里面数据比较多,包括所有应用的图标和应用数据,所有应用的Widget数据,桌面已添加的用户数据等,随着Android大版本演进,还有DeepShortcuts等新的数据类型。如果按照常规的加载做法,等加载数据完成后再显示到View,耗时就太长了。为了优化体验,Launcher于是采用了分批加载、分批绑定的做法。这是大家在应用开发时可以借鉴的一种优化方案。整体的加载绑定流程如下。

我们以其中的加载与绑定桌面内容为例来进行说明,后面的三步在弄明白第一步如何做之后也就是业务逻辑上的差异,不再赘述。

加载桌面内容,调用函数为loadWorkspace()。这个函数很长,这里就不贴代码了。简述一下其流程。

首先我们要知道一个BgDataModel类,这个类用于把所有数据对应的实例管理起来。如下面代码,可以看到有workspaceItems(所有应用图标数据对应的ItemInfo),appWidgets(所有AppWidgets数据对应的LauncherAppWidgetInfo)等等。

    public final LongArrayMap<ItemInfo> itemsIdMap = new LongArrayMap<>();

    public final ArrayList<ItemInfo> workspaceItems = new ArrayList<>();

    public final ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();

    public final LongArrayMap<FolderInfo> folders = new LongArrayMap<>();

    public final ArrayList<Long> workspaceScreens = new ArrayList<>();

    public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();

    public boolean hasShortcutHostPermission;

    public final MultiHashMap<ComponentKey, String> deepShortcutMap = new MultiHashMap<>();

    public final WidgetsModel widgetsModel = new WidgetsModel();

然后正式来看loadWorkspace

  • 通过LauncherSettings.Favorites.CONTENT_URI查询Favorites表的所有内容,拿到cursor。
  • 遍历cursor,进行数据的整理。每一行数据都有一个对应的itemType,标志着这一行的数据对应的是一个应用、还是一个Widget或文件夹等。不同的类型会进行不同的处理。
  • 对于图标类型(itemType是ITEM_TYPE_SHORTCUT,ITEM_TYPE_APPLICATION,ITEM_TYPE_DEEP_SHORTCUT),首先经过一系列判断,判断其是否还可用(比如应用在Launcher未启动时被卸载导致不可用),不可用的话就标记为可删除,继续循环。如果可用的话,就根据当前cursor的内容,生成一个ShortcutInfo对象,保存到BgDataModel。
  • 对于文件夹类型(itemType是ITEM_TYPE_FOLDER),直接生成一个对应的FolderInfo对象,保存到BgDataModel。
  • 对于AppWidget(itemType是ITEM_TYPE_APPWIDGETITEM_TYPE_CUSTOM_APPWIDGET),也需要经过是否可用的判断,但是可用条件与图标类型是有差异的。如果可用,生成一个LauncherAppWidgetInfo对象,保存到BgDataModel。
  • 经过上述流程,现在所有数据库里读出的内容已经分类完毕,并且保存到了内存(BgDataModel)中。然后开始处理之前标记为可删除的内容。显示从数据库中删除对应的行,然后还要判断此次删除操作是否带来了其他需要删除的内容。比如某个文件夹或者某一页只有一个图标,这个图标因为某些原因被删掉了,那么此文件夹或页面也需要被删掉。

至此数据加载完毕,开始要进行绑定了,也就是mResults.bindWorkspace()

此函数在LoaderResults类中。函数在执行数据绑定之前,会执行这样一段代码。

        // Separate the items that are on the current screen, and all the other remaining items
        ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
        ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
        ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
        ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();

        filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
                otherWorkspaceItems);
        filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets,
                otherAppWidgets);
        sortWorkspaceItemsSpatially(currentWorkspaceItems);
        sortWorkspaceItemsSpatially(otherWorkspaceItems);

这段代码做的事情是,把Launcher启动后默认显示出来那一页所拥有的数据筛选到currentWorkspaceItems与currentAppWidgets,其他页的数据筛选到otherWorkspaceItems与otherAppWidgets。然后对每个list,按照从上到下,从左到右的顺序进行排序。然后可以开始绑定了。下面代码的Callbacks就是Launcher activity实例,首先通知Launcher要开始绑定了(callbacks.startBinding()),然后先把空页面添加到View tree中(callbacks.bindScreens(orderedScreenIds)),之后先绑定默认页的所有元素(下段代码的最后一句)。当然这些所有的操作都是通过mUiExecutor放到主线程执行的。

        // Tell the workspace that we're about to start binding items
        r = new Runnable() {
            public void run() {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.clearPendingBinds();
                    callbacks.startBinding();
                }
            }
        };
        mUiExecutor.execute(r);

        // Bind workspace screens
        mUiExecutor.execute(new Runnable() {
            @Override
            public void run() {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.bindScreens(orderedScreenIds);
                }
            }
        });

                Executor mainExecutor = mUiExecutor;
        // Load items on the current page.
        bindWorkspaceItems(currentWorkspaceItems, currentAppWidgets, mainExecutor);

最后一句的内容如下,也是通过callbacks调用在Launcher中的bindItems函数。

private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems,
            final ArrayList<LauncherAppWidgetInfo> appWidgets,
            final Executor executor) {

        // Bind the workspace items
        int N = workspaceItems.size();
        for (int i = 0; i < N; i += ITEMS_CHUNK) {
            final int start = i;
            final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
            final Runnable r = new Runnable() {
                @Override
                public void run() {
                    Callbacks callbacks = mCallbacks.get();
                    if (callbacks != null) {
                        callbacks.bindItems(workspaceItems.subList(start, start+chunkSize), false);
                    }
                }
            };
            executor.execute(r);
        }

        // Bind the widgets, one at a time
        N = appWidgets.size();
        for (int i = 0; i < N; i++) {
            final ItemInfo widget = appWidgets.get(i);
            final Runnable r = new Runnable() {
                public void run() {
                    Callbacks callbacks = mCallbacks.get();
                    if (callbacks != null) {
                        callbacks.bindItems(Collections.singletonList(widget), false);
                    }
                }
            };
            executor.execute(r);
        }
    }

bindItems函数我们看一下其中的关键代码。根据不同的itemType来生产不同的View,然后通过addInScreenFromBind函数将View add到相应的ViewGroup去。

    @Override
    public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
        ...
            switch (item.itemType) {
                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
                    ShortcutInfo info = (ShortcutInfo) item;
                    view = createShortcut(info);
                    break;
                }
                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
                    view = FolderIcon.fromXml(R.layout.folder_icon, this,
                            (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
                            (FolderInfo) item);
                    break;
                }
                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
                    view = inflateAppWidget((LauncherAppWidgetInfo) item);
                    if (view == null) {
                        continue;
                    }
                    break;
                }
                default:
                    throw new RuntimeException("Invalid Item Type");
            }

            ...
            workspace.addInScreenFromBind(view, item);
            ...
    }

默认页的元素绑定完了,然后继续绑定其他页的元素。这里我们从注释也可以看出,Launcher为了让默认页尽快显示,自定义了一个ViewOnDrawExecutor,这里面会让绑定其他页的操作在绑定完第一页的元素并且第一次onDraw执行完之后才执行。读者有兴趣的话可以去看看这个Executor的实现。

        // In case of validFirstPage, only bind the first screen, and defer binding the
        // remaining screens after first onDraw (and an optional the fade animation whichever
        // happens later).
        // This ensures that the first screen is immediately visible (eg. during rotation)
        // In case of !validFirstPage, bind all pages one after other.
        final Executor deferredExecutor =
                validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;

        mainExecutor.execute(new Runnable() {
            @Override
            public void run() {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.finishFirstPageBind(
                            validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null);
                }
            }
        });

        bindWorkspaceItems(otherWorkspaceItems, otherAppWidgets, deferredExecutor);

经过其他页的绑定之后,桌面数据的加载与绑定也就到此为止。接下来就是之前提到的另外三步后续加载与绑定内容了,不再赘述。但是在本文结束前,还想说一个值得一提的地方。

桌面数据的加载与绑定完之后,我们看这里执行了一个waitForIdle的函数,然后才是继续执行第二步。这个函数是做什么的呢?

            // Take a break
            TraceHelper.partitionSection(TAG, "step 1 completed, wait for idle");
            waitForIdle();
            verifyNotStopped();

            // second step
            TraceHelper.partitionSection(TAG, "step 2.1: loading all apps");
            loadAllApps();

我们看下它的实现。

    protected synchronized void waitForIdle() {
        // Wait until the either we're stopped or the other threads are done.
        // This way we don't start loading all apps until the workspace has settled
        // down.
        LooperIdleLock idleLock = mResults.newIdleLock(this);
        // Just in case mFlushingWorkerThread changes but we aren't woken up,
        // wait no longer than 1sec at a time
        while (!mStopped && idleLock.awaitLocked(1000));
    }

    public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable {

    private final Object mLock;

    private boolean mIsLocked;

    public LooperIdleLock(Object lock, Looper looper) {
        mLock = lock;
        mIsLocked = true;
        if (Utilities.ATLEAST_MARSHMALLOW) {
            looper.getQueue().addIdleHandler(this);
        } else {
            // Looper.myQueue() only gives the current queue. Move the execution to the UI thread
            // so that the IdleHandler is attached to the correct message queue.
            new LooperExecutor(looper).execute(this);
        }
    }

    @Override
    public void run() {
        Looper.myQueue().addIdleHandler(this);
    }

    @Override
    public boolean queueIdle() {
        synchronized (mLock) {
            mIsLocked = false;
            mLock.notify();
        }
        return false;
    }

    public boolean awaitLocked(long ms) {
        if (mIsLocked) {
            try {
                // Just in case mFlushingWorkerThread changes but we aren't woken up,
                // wait no longer than 1sec at a time
                mLock.wait(ms);
            } catch (InterruptedException ex) {
                // Ignore
            }
        }
        return mIsLocked;
    }
}

这里面涉及到一个应用启动优化的技术。我们知道应用的启动优化可以有延迟加载、懒加载、异步加载等手段。而用一个名为IdleHandler的类,就可以比较方便的实现延迟加载。这个后面有空的话再来细说吧,本文就先到这里。

下一篇将分析Launcher布局相关内容。

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