Activity的启动原理

前言

​ 我还记得上个学期意外看到一篇文章,是凯子哥的一篇博客,讲的是app第一个activity启动的底层过程(java),我当时几乎没接触过这块的源码,好像看了一半就放弃。还有一次刷知乎,看到一个人回答为什么ActivityThread里面Loop不会造成ANR,但是也是一脸萌比。这个学期开始刷艺术探索,已经看完了第十章,每一章都对着源码自己又看了一遍,再加上之前对Binder,IPC的浅显的研究。今天重新再来看这篇文章[【凯子哥带你学Framework】Activity启动过程全解析],发现基本上是看小说一样的轻松看完了。。这是一种进步!!感觉挺好的。感谢大神的知识分享,下面我用自己的方式来总结一下Activity启动的过程,但是我跟凯子哥不同,他讲的那个其实就多了一点Launcher和applicaiton创建的知识。

​ 说一点自己的学习感受。首先:

  • 源码海洋里面不要太追求了解每一个细节,不然分分钟迷失自我。
  • 有些知识是需要有其他知识储备才能理解的。所以看不懂的时候放一放,过一段时间再来看。比如binder设计原理那块我就留了一半。。因为后半部分有点看不太懂了,涉及到了C草。
  • 看书上的源码远远不够,一定一定要自己去独立的看一遍,自己去把整个流程过一遍。
  • 画草图,流程图。因为过程异常的复杂,在不同的类里面调来调用,又臭又长的调用链。
  • 永远不要放弃,静下心来研究,一切都不是问题。

知识储备

  1. Binder, IPC机制。可以看看我之前的两篇文章Android-IPC系列(一)Android-IPC系列(二) 。还有几篇我觉得写的非常好的文章Android Binder设计与实现 - 设计篇 Android进程间通信(IPC)机制Binder简要介绍和学习计划 。(这两篇讲的都比较深)看到艺术探索后面我终于知道为什么binder要在第二章就讲解了。因为Binder在后面几乎是无处不在!

  2. ActivityManagerService。本质就是一个Binder,并且实体在服务端。AMS在远端操作着activity的生命周期。这个进程由SystemServer进程fork出来。

  3. ActivityThread。大家都知道ActivityThread就是应用的UI线程,main方法就是整个应用的入口。ActivityThread本质并不是一个线程,他只是依附着主线程存在。ActivityThread通过和AMS进行IPC通信来共同管理Activity的生命周期。在后面我准备写一篇关于Handle的续篇,里面还会提到他,因为主线程里面的Looper就是在这里init的。

  4. ApplicationThread.。它是ActivityThread的内部类,本质上ActivityThread是通过它来进行和AMS的IPC通信的。它的本质也是一个Binder!只不过这次他的实体放在客户端,AMS通过他的代理类ApplicationThreadProxy来和ApplicationThread通信。

  5. Instrumentation.。这个类我在看完第一遍书的时候感觉操作调用链里面的最外层。因为最后一步Activity实例对象的生成和onCreat()方法的调用最终是来自这个类的。其实这个类是ActivityThread想要进行的操作的具体操作类。这个类是全局的,并且每个acitivity里面都拥有一个它的引用。

  6. ActivityStack(AMS中)。很好懂,一个activity栈。但是这个ActivityStack是有两种类型的,一种叫系统ActivityStack(HomeTask),这一类的ActivityStack包含着Launcher(或者有其他我不知道的),还有一种是普通应用的ActivityStack(安卓中Task这个概念的具体实现形式),其实就是一个Task(任务栈)。这个类是由AMS来管理,AMS通过这个数据结构来得知activity的状态。

  7. ActivityStackSuperisor(AMS中)。加了一个单词,就是activity栈的管理者。这个类的作用就是管理栈,并且通过ActivityStack来获得要启动的activity的信息。

  8. ActivityRecord。这个就是上面说的服务端的actiivty信息的载体类。并且也是服务端的类~这个类相当的重要,自始至终都贯穿在调用链里面,在安卓ActivityStack里面存储的并不是activity实例,其实就是这个ActivityRecord的实例。

  9. ActivityClientRecord。这个和上面区别就是这个类是客户端activity信息的载体类。

  10. TaskRecord。同样,这个类就是ActivityTask的信息记录类而已。

    ​好了,我觉得需要好好了解的几个最核心的类就是这几个了,大家若不清楚,可以自己去源码里面看,如果我有说错的也请原谅。。。其实还有很多概念我在binder那篇文章里面讲的不少了!这里算是又一次的补充吧。


Activity启动的流程

​ 源码之旅开始。科科.

1.Activity.startActivity(Intent intent)这个我们天天都在写,好,去看源码!

@Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

嗯,继续看startActivityForResult();

public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
        if (mParent == null) {
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            ...省略代码
    }

直接调用了Instrumentation去调用execStartActivity()方法。后面的调用先不管,直接去看execStartActivity()方法。需要注意的是mMainThread.getApplicationThread()就是通过ActivityThread去获得了一个ApplicaitonThread实例

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        ...省略代码
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess();
          //通过AMS启动了
            int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
          //用来检测这个activity启动的结果,具体可以去查看源码
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
        }
        return null;
    }

看到这里发现它又用了个什么玩意调用了startActivity()方法,这是什么呢?这里的ActivityManagerNative.getDefault()就是我们期待了很久的ActivityManagerService的代理类!如果你很清楚Binder的实现,这个应该难不倒你吧!AMS和代理类本质上都是IActivityManager的实现类。(IAcitivtyManager,其实就是一个AIDL接口,不信自己去看源码)。我给你看看getDefault()方法的实现:

private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
            IBinder b = ServiceManager.getService("activity");
            if (false) {
                Log.v("ActivityManager", "default service binder = " + b);
            }
            IActivityManager am = asInterface(b);
            if (false) {
                Log.v("ActivityManager", "default service = " + am);
            }
            return am;
        }
    };

就是这么简单!如果看到这里你看不懂了,建议你再回去看看Binder。接下来我们就可以继续看AMS中startActivity()方法的实现了:注意,从这里开始,调用链会变得很复杂

@Override
    public final int startActivity(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle options) {
      //调用了startActivityAsUser,caller是我们的ApplicaitonThread
        return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
            resultWho, requestCode, startFlags, profilerInfo, options,
            UserHandle.getCallingUserId());
    }

继续看这个startActivityAsUser()方法:

@Override
    public final int startActivityAsUser(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle options, int userId) {
        enforceNotIsolatedCaller("startActivity");
        userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId,
                false, ALLOW_FULL_ONLY, "startActivity", null);
        // TODO: Switch to user app stacks here.转换app里面的activity栈,caller是我们的ApplicaitonThread
        return mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent,
                resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
                profilerInfo, null, null, options, userId, null, null);
    }

​ 发现调用了之前提到的ActivityStackSupervisor的startActivityMayWait()方法。这里的目的就是想去通过栈的管理者来通过传进来的数据去改变栈,并且拿到被启动activity的ActivityRecord实例。

​ 在startActivityMayWait()里面,主要做了ActivityStack, ActivityInfo, LaunchMode等等各种信息的初始化工作,然后就又调用了startActivityLocked()方法, 这个方法里面又做了一系列的变量初始化,初始启动的错误判断, uid检查之后,又调用了startActivityUncheckedLocked()方法。这个方法里面根据前面一系列的工作,确定了最终的LanuchMode,这个LaunchMode会在后面的函数被拿来进行判断,构建了一个新的intent,ActivityInfo,ActivityTask。反正过程都TM又臭又长,细看下去一天就耗完了。接下来我们可以看到最终调用targetStack.resumeTopActivityLocked()方法。这个targetStack就是新的activity所要放入的Task的位置。也就是说,现在又要转到ActivityTask中去看源码了,科科!

​ 注意的是,LaunchMode和ActivityStack的选择是个很复杂的过程。我推荐一篇文章以及安卓的官方文档链接。官方文档,顿文的博客


需要非常注意的是,这个函数执行的时候,被启动的activity里面被添加到栈里面去了,top,next就是指代这个要被启动的activity。pre,current就是指代当前resumed的activity。你别弄混了,虽然这个问题我TM看了一整天的源码。注意,被启动的activity虽然还没有被实例,但是它的ActivityRecord实例已经被构建出来了,并且已经被添加到stack里面去了。具体的调用过程是:

​ Supervisor.setActivityUnchekedLocked->resumeTopActivityLocked(这里是ActivityStackSuperbisor里面的方法)->ActivityStack.startActivityLocked在这里将activity添加到stack里面。并且在之后才会调用ActivityStack的resumeTopActivityLocked方法

/**
     * Ensure that the top activity in the stack is resumed.
     *
     * @param prev The previously resumed activity, for when in the process
     * of pausing; can be null to call from elsewhere.
     *
     * @return Returns true if something is being resumed, or false if
     * nothing happened.
     */
final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
        if (inResumeTopActivity) {
            // Don't even start recursing.
            return false;
        }

        boolean result = false;
        try {
            // Protect against recursion.
            inResumeTopActivity = true;
            result = resumeTopActivityInnerLocked(prev, options);
        } finally {
            inResumeTopActivity = false;
        }
        return result;
    }

​ 不要有情绪,继续看resumeTopActivityInnerLocked(prev, options);这个方法太长了,我分析了一整天,终于所有体会吧。这个方法最终的作用是将启动者activity的生命周期变成paused,这样之后被启动的activity的实例创建了之后才能顺利的resumed。我们来看部分代码:

// Find the first activity that is not finishing.
        //找出栈顶中第一个没有在被finish的activity,既我们要启动的actiivty
        ActivityRecord next = topRunningActivityLocked(null);

        // Remember how we'll process this pause/resume situation, and ensure
        // that the state is reset however we wind up proceeding.
        final boolean userLeaving = mStackSupervisor.mUserLeaving;
        mStackSupervisor.mUserLeaving = false;

        final TaskRecord prevTask = prev != null ? prev.task : null;
        //如果整个stack里面是空的,那么直接启动launcher
        if (next == null) {
            // There are no more activities!  Let's just start up the
            // Launcher...
            ActivityOptions.abort(options);
            if (DEBUG_STATES) Slog.d(TAG, "resumeTopActivityLocked: No more activities go home");
            if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked();
            // Only resume home if on home display
            final int returnTaskType = prevTask == null || !prevTask.isOverHomeStack() ?
                    HOME_ACTIVITY_TYPE : prevTask.getTaskToReturnTo();
            //直接resume系统ActivityStack里面的根activity(Launcher)
            return isOnHomeDisplay() &&
                    mStackSupervisor.resumeHomeStackTask(returnTaskType, prev);
        }

​ 在这里,next这个变量很关键。它的注释说的是第一个没有正在被销毁的activity,显然我们要被启动的activity符合这个条件。并且如果这个应用级别的stack是空的,也就是说现在应该跳转到系统级别的stack去,也就是显示系统桌面。

// If the top activity is the resumed one, nothing to do.
        //如果被启动的activity就是当前处理Resumed状态的activity的话,就什么不做。(一个activity启动自己就是这种情况)
        if (mResumedActivity == next && next.state == ActivityState.RESUMED &&
                    mStackSupervisor.allResumedActivitiesComplete()) {
            // Make sure we have executed any pending transitions, since there
            // should be nothing left to do at this point.
            mWindowManager.executeAppTransition();
            mNoAnimActivities.clear();
            ActivityOptions.abort(options);
            if (DEBUG_STATES) Slog.d(TAG, "resumeTopActivityLocked: Top activity resumed " + next);
            if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked();

            // Make sure to notify Keyguard as well if it is waiting for an activity to be drawn.
            mStackSupervisor.notifyActivityDrawnForKeyguard();
            return false;
        }

​ 我注释说的很清楚了,如果要启动的activity已经是resumed了,就什么都不做。因为这就是一个当前resumed的activity启动它自己。

// If we are sleeping, and there is no resumed activity, and the top
        // activity is paused, well that is the state we want.
        //如果系统正在休眠,并且当前最上层的activity都已经是paused状态了(top activity就是我们要启动的activity)。那就是完美的状态。
        if (mService.isSleepingOrShuttingDown()
                && mLastPausedActivity == next
                && mStackSupervisor.allPausedActivitiesComplete()) {
            // Make sure we have executed any pending transitions, since there
            // should be nothing left to do at this point.
            mWindowManager.executeAppTransition();
            mNoAnimActivities.clear();
            ActivityOptions.abort(options);
            if (DEBUG_STATES) Slog.d(TAG, "resumeTopActivityLocked: Going to sleep and all paused");
            if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked();
            return false;
        }

​ 如果系统正在休眠,并且当前最上层的activity都已经是paused状态了(top activity就是我们要启动的activity)。那就是完美的状态。

// If we are currently pausing an activity, then don't do anything
        // until that is done.
        if (!mStackSupervisor.allPausedActivitiesComplete()) {
            if (DEBUG_SWITCH || DEBUG_PAUSE || DEBUG_STATES) Slog.v(TAG,
                                                                     /  "resumeTopActivityLocked: Skip resume: some activity pausing.");
            if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked();
            return false;
        }

​ 如果我们的app里面正在暂停某个activity,那么我们什么都不要做等这个做完,因为暂停activity的过程是串行的,必须要一个一个按顺序的来。不能同时来,我认为原因就是因为客户端调用远程服务的过程的时候本地的客户端所在线程会被挂起

/ We need to start pausing the current activity so the top one
        // can be resumed...
        //先把现在的当前还是resumed的activity pause了,这样新加进来的activity才能resume。基础知识
        boolean dontWaitForPause = (next.info.flags&ActivityInfo.FLAG_RESUME_WHILE_PAUSING) != 0;
        //开始暂定现在stack里面所有的activity
        boolean pausing = mStackSupervisor.pauseBackStacks(userLeaving, true, dontWaitForPause);
        if (mResumedActivity != null) {
            //开始pausing当前的所有activity,并且返回一个是否暂定成功的结果回来
            pausing |= startPausingLocked(userLeaving, false, true, dontWaitForPause);
            if (DEBUG_STATES) Slog.d(TAG, "resumeTopActivityLocked: Pausing " + mResumedActivity);
        }

​ 这里就是在做把当前启动者activity给pause掉,即至于为什么要这么做。。不用我多说了吧。

​ startPausingLocked(userLeaving, false, true, dontWaitForPause);这个函数跟着看下去就是通过AMS来暂停activity的过程。这个就不多说了,大同小异。

最终在函数的末尾会又调用ActivityStackSupervisor的startSpecificActivityLocked(next, true, true);方法。这个方法的源码如下:

void startSpecificActivityLocked(ActivityRecord r,
            boolean andResume, boolean checkConfig) {
        // Is this activity's application already running?
      //注意了,这里的app之后会用到,因为app.thread就是获得了applicationthread实例!
        ProcessRecord app = mService.getProcessRecordLocked(r.processName,
                r.info.applicationInfo.uid, true);

        r.task.stack.setLaunchTime(r);

        if (app != null && app.thread != null) {
            try {
                if ((r.info.flags&ActivityInfo.FLAG_MULTIPROCESS) == 0
                        || !"android".equals(r.info.packageName)) {
                    // Don't add this if it is a platform component that is marked
                    // to run in multiple processes, because this is actually
                    // part of the framework so doesn't make sense to track as a
                    // separate apk in the process.
                    app.addPackage(r.info.packageName, r.info.applicationInfo.versionCode,
                            mService.mProcessStats);
                }
              //将app,信息完整的要启动的ActivityRecord类的实例传到另一个方法里面去
                realStartActivityLocked(r, app, andResume, checkConfig);
                return;
            } catch (RemoteException e) {
                Slog.w(TAG, "Exception when starting activity "
                        + r.intent.getComponent().flattenToShortString(), e);
            }

            // If a dead object exception was thrown -- fall through to
            // restart the application.
        }

        mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
                "activity", r.intent.getComponent(), false, false, true);
    }

继续跟进这个realStartActivityLocked()(真正启动activity的过程在这里):

final boolean realStartActivityLocked(ActivityRecord r,
            ProcessRecord app, boolean andResume, boolean checkConfig)
            throws RemoteException {

        r.startFreezingScreenLocked(app, 0);
        if (false) Slog.d(TAG, "realStartActivity: setting app visibility true");
        mWindowManager.setAppVisibility(r.appToken, true);

        // schedule launch ticks to collect information about slow apps.
        r.startLaunchTickingLocked();

        // Have the window manager re-evaluate the orientation of
        // the screen based on the new activity order.  Note that
        // as a result of this, it can call back into the activity
        // manager with a new orientation.  We don't care about that,
        // because the activity is not currently running so we are
        // just restarting it anyway.
        if (checkConfig) {
            Configuration config = mWindowManager.updateOrientationFromAppTokens(
                    mService.mConfiguration,
                    r.mayFreezeScreenLocked(app) ? r.appToken : null);
            mService.updateConfigurationLocked(config, r, false, false);
        }

        r.app = app;
        app.waitingToKill = null;
        r.launchCount++;
        r.lastLaunchTime = SystemClock.uptimeMillis();

        if (localLOGV) Slog.v(TAG, "Launching: " + r);

        int idx = app.activities.indexOf(r);
        if (idx < 0) {
            app.activities.add(r);
        }
        mService.updateLruProcessLocked(app, true, null);
        mService.updateOomAdjLocked();

        final ActivityStack stack = r.task.stack;
        try {
            if (app.thread == null) {
                throw new RemoteException();
            }
            List<ResultInfo> results = null;
            List<Intent> newIntents = null;
            if (andResume) {
                results = r.results;
                newIntents = r.newIntents;
            }

...省略代码

  //通过applicaitonthread调用客户端binder实体的方法。
            app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                    System.identityHashCode(r), r.info, new Configuration(mService.mConfiguration),
                    r.compat, r.task.voiceInteractor, app.repProcState, r.icicle, r.persistentState,
                    results, newIntents, !andResume, mService.isNextTransitionForward(),
                    profilerInfo);

      ...省略代码

        return true;
    }

​ 这里面两个很重要的参数,一个是ActivityRecord,一个是app。前者就是在ActivityStack里面转了一圈之后得出来的最终要启动的Activity的信息记录类。后者就是用来获得ApplicationThread来通知客户端拿这个ActivityRecord去管理你的activity的生命周期吧!相当于AMS给了ActivityThread一个任务,让后者去执行。同样,这也是一个IPC的过程。最终调用链绕了好大一圈终于又回到了ApplicaitonThread。

​ 值得一提的是,凡是schedule开头的函数都是通过handler来做线程调度的,不服来辩。我们点进去

public final void scheduleResumeActivity(IBinder token, int processState,
                boolean isForward, Bundle resumeArgs) {
            updateProcessState(processState, false);
            sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0);
        }

​ 果然用handler发送了一个message。我们来看handler的处理会最终调用handlerLaunchActivity方法:

Activity a = performLaunchActivity(r, customIntent);

        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            Bundle oldState = r.state;
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);

​ 可以看到activity的实例是由performLaunchActivity方法生成的。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");

      //获得一些基本信息
        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }

        ComponentName component = r.intent.getComponent();
        if (component == null) {
            component = r.intent.resolveActivity(
                mInitialApplication.getPackageManager());
            r.intent.setComponent(component);
        }

        if (r.activityInfo.targetActivity != null) {
            component = new ComponentName(r.activityInfo.packageName,
                    r.activityInfo.targetActivity);
        }

      //通过Instrumentation利用类加载器来生成一个activity实例
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(
                    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);
            }
        }

      //尝试生成application
        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);


          //创建contextImpl对象,并且调用activity的attach()方法来进行一些activity初始化
            if (activity != null) {
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);

              //设置activity的theme
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }

                activity.mCalled = false;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
            mActivities.put(r.token, r);


        return activity;
    }

​ 做了这些事情

  • 从ActivityClientRecord中获取获取组件信息
  • 通过Instrumentation创建Activity对象
  • 通过LoadApk的makeApplication创建application,这个方法有兴趣自己去看,就是一个单例而已。
  • 创建contextImpl对象,并且调用activity的attach()方法来进行一些activity初始化
  • 调用activity的onCreat()方法

完成整个过程调用 !!!!

结束语

​ 系统源码看起来确实感觉自己置身茫茫大海一样,特别是要去找一些不理解的问题的时候。比如到底是被启动的activity的ActivityRecord实例先添加进去还是在实例创建了之后才添加进去,这个问题我看源码着了一天才还没有什么结果,后来在别人的帮助和查阅老罗的资料才找到问题所在。挺不错的一次过程,确实细节不必死扣,但是有些基本问题不懂的话还是应该去探究一下。

版权声明:本文为博主原创文章,未经博主允许不得转载。

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

推荐阅读更多精彩内容