Activity启动流程Ams


工作过程

App进程访问Ams服务,system_server进程开启Binder线程,处理注册请求,负责及时反馈App主线程组件注册结果,App主线程等待Ams服务线程的工作时,将休眠。
Ams服务处理,回调App进程,根据Binder机制,回调时重用休眠主线程或新建Binder线程,向主线程插入一个暂停上一个Activity组件的消息,返回Ams服务,结束。
Ams服务Binder线程不会耗时,否则会阻塞App主线程,导致ANR。
App主线程会从消息队列里查询消息,暂停上一个活动Activity的消息处理时,将再次访问Ams服务,继续新组件Resume逻辑。
Ams服务处理,回调App进程,插入通知Activity实例创建和开启生命周期的消息,然后结束Ams服务,App主线程将开启新Activity实例和生命周期的任务。


一、Ams服务流程

Ams服务流程包括,组件注册,进程创建(如必要),启动模式分析,任务栈管理,App进程通知。

Activity组件启动和Ams服务的交互流程。

Ams服务的入口是startActivity方法。ActivityStackSupervisor类是监管者,ActivityStack类是栈。
第一个方法,ActivityStackSupervisor类的startActivityMayWait方法。

// 不修改App传递的Intent,再次封装。
intent = new Intent(intent);
// 目标Activity信息。
ActivityInfo aInfo = resolveActivity(intent, resolvedType, 
                  startFlags, profilerInfo, userId);

PackageManager类,根据Intent类解析ActiityInfo对象,如果组件未在AndroidManifest.xml文件注册时,返回空。
第二个方法,ActivityStackSupervisor的startActivityLocked方法。

ProcessRecord callerApp = null;
if (caller != null) {//caller是IApplicationThread caller
    callerApp = mService.getRecordForAppLocked(caller);
    if (callerApp != null) {
        callingPid = callerApp.pid;
        callingUid = callerApp.info.uid;
    } else {
        err = ActivityManager.START_PERMISSION_DENIED;
    }
}
//ProcessRecord在Ams存储列表
final ArrayList<ProcessRecord> mLruProcesses = new ArrayList<ProcessRecord>(); 
//ProcessRecord类内部
IApplicationThread thread;

根据App进程的IApplicationThread,解析ProcessRecord,根据IBinder token入参查找发起者Activity在Ams服务的ActivityRecord记录。

ActivityRecord sourceRecord = null;
ActivityRecord resultRecord = null;
if (resultTo != null) {//resultTo是IBinder resultTo
    sourceRecord = isInAnyStackLocked(resultTo);
    if (sourceRecord != null) {
        if (requestCode >= 0 && !sourceRecord.finishing) {
             //ActivityRecord resultRecord作为结果接收Activity。
             resultRecord = sourceRecord;
        }
    }
}
//遍历ActivityDisplay集合,从mStacks中查找。
ArrayList<ActivityStack> mStacks,一个ActivityStack栈列表。

遍历每项ActivityStack,查找符合token的ActivityRecord。每一个Activity组件在Ams服务都有一个ActivityRecord记录对应。App进程通过IApplicationThread和Ams服务的一个ProcessRecord对应,表示在Ams服务中,保存一条进程记录。
从Intent类解析Flags,启动时未设置,默认是0。

final int launchFlags = intent.getFlags();

if (err == ActivityManager.START_SUCCESS && aInfo == null) {
    err = ActivityManager.START_CLASS_NOT_FOUND;
}

常见错误,解析的ActivityInfo是空,设置错误标志,表示类没有找到,直接返回App进程,不会创建ActivityRecord记录,App进程抛出异常。
创建ActivityRecord,构造方法有很多初始化工作,例如解析启动模式。

ActivityRecord r = new ActivityRecord(mService, callerApp, callingUid, callingPackage,
                intent, resolvedType, aInfo, mService.mConfiguration, resultRecord, resultWho,
                requestCode, componentSpecified, voiceSession != null, this, container, options);

构造方法中,创建Token对象,弱引用ActivityRecord,用于传递给App进程。App进程中保存一份token和Activity的Map,Ams服务访问App进程时,可根据token唤起对应Activty组件。
startActivityLocked方法,最后,调用startActivityUncheckedLocked方法,入参中包含新建的ActivityRecord和发起者ActivityRecord。

err = startActivityUncheckedLocked(r, sourceRecord, voiceSession, voiceInteractor,
                startFlags, true, options, inTask);

第三个方法,startActivityUncheckedLocked方法。

final boolean launchSingleTop = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP;
final boolean launchSingleInstance = r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE;
final boolean launchSingleTask = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK;
...
if (r.resultTo == null && inTask == null && !addingToTask
                && (launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
    newTask = true;
    targetStack = computeStackFocus(r, newTask);
    targetStack.moveToFront("startingNewTask");
} else if (sourceRecord != null) {
    //sourceRecord发起者不空,走这里的代码。
    final TaskRecord sourceTask = sourceRecord.task;
    ...
    targetStack = sourceTask.stack;    
    ...
    r.setTask(sourceTask, null);
} else if (inTask != null) {
    ....
    targetStack = inTask.stack;
    r.setTask(inTask, null);
} else {
    targetStack = computeStackFocus(r, newTask);
    targetStack.moveToFront("addingToTopTask");
    ActivityRecord prev = targetStack.topActivity();
    r.setTask(prev != null ? prev.task : targetStack.createTaskRecord(getNextTaskId(),
                            r.info, intent, null, null, true), null);
    ...
}
...
targetStack.mLastPausedActivity = null;
//入参doResume是true。
targetStack.startActivityLocked(r, newTask, doResume, keepCurTransition, options);

如果发起者Activity组件的ActivityRecord(即sourceRecord)不是空,新创建ActivityRecord的TaskRecord和发起者相同,设置TaskRecord任务,同时获取TaskRecord内部ActivityStack栈,确定目标栈targetStack。
系统根据不同的情况获取目标栈,当Intent存在FLAG_ACTIVITY_NEW_TASK标志时,新建TaskRecord任务。

ActivityRecord和ActivityStack、TaskRecord的关系

ActivityStack栈包含一个TaskRecord列表,每一项TaskRecord的内部引用外部ActivityStack。TaskRecord包含一个ActivityRecord列表,每一项ActivityRecord内部引用外部TaskRecord。ActivityRecord代表一个Activity记录,TaskRecord代表任务栈,进程与启动模式相关,TaskRecord关联到ActivityStack。

ActivityRecord调试记录

从上图可知,有两个ActivityStack,一个保存和系统UI相关的TaskRecord,另一个保存App的TaskRecord。开启了四个应用,两个自己编译的App,两个手机系统App,图中对应四个TaskRecord,其中,个人App启动了三个Activity,保存在TaskRecord的ActivityRecord列表mActivitys中。


二、启动模式处理

SingleTop模式

ActivityRecord top = topStack.topRunningNonDelayedActivityLocked(notTop);
//同一个Activity类
if (top.realActivity.equals(r.realActivity) && top.userId == r.userId) {
    if (top.app != null && top.app.thread != null) {
        if ((launchFlags & Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0
                            || launchSingleTop || launchSingleTask) {
            topStack.mLastPausedActivity = null;
            if (doResume) {
                resumeTopActivitiesLocked();
            }
            ...
            top.deliverNewIntentLocked(callingUid, r.intent, r.launchedFromPackage);
            return ActivityManager.START_DELIVERED_TO_TOP;
        }
    }
}

如果满足栈顶记录的组件名ComponentName和新建ActivityRecord记录的组件名相等,组件类名相同属同一个Activity。不需要确定ActivityStack栈,也不需要addActivityToTop方法将ActivityRecord加入到ActivityStack的TaskRecord中,直接resumeTopActivitiesLocked方法。

if (mResumedActivity == next && next.state == ActivityState.RESUMED &&
                    mStackSupervisor.allResumedActivitiesComplete()) {
    ...
    return false;
}

此时,新ActivityRecord记录没有执行addActivityToTop加入方法,因此,next记录还是以前的,和当前活跃的mResumedActivity相等。

还原Android的基础知识点,配置启动模式是SingleTop的Activity组件,栈顶已经存在一个该Activity类组件,将复用。

SingleTask处理

if ((launchFlags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))
                            == (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK)) {
    ...
    } else if ((launchFlags & FLAG_ACTIVITY_CLEAR_TOP) != 0
                            || launchSingleInstance || launchSingleTask) {        
    //将最上面ActivityRecord对应task的部分内容清理。
    ActivityRecord top = intentActivity.task.performClearTaskLocked(r, launchFlags);    
    ...
} else if (){
}

在任务栈TaskRecord查找ActivityRecord,从后向前遍历mActivitys列表,如果发现ActivityRecord与新建ActivityRecord组件类名相同,finishActivityLocked方法,将中间的一些Activity清理,最后,Resume恢复该ActivityRecord。

还原Android的基础知识点,启动模式是SingleTask的Activity组件,栈中已经存在一个该Activity类组件,将复用,并将它上部的所有Activity清除。


四、暂停前组件,恢复栈顶新组件

第四个方法,ActivityStack的startActivityLocked方法。

final void startActivityLocked(ActivityRecord r, boolean newTask,
                               boolean doResume, boolean keepCurTransition, Bundle options) {
    TaskRecord rTask = r.task;
    final int taskId = rTask.taskId;
    if (!r.mLaunchTaskBehind && (taskForIdLocked(taskId) == null || newTask)) {

        insertTaskAtTop(rTask, r);
        mWindowManager.moveTaskToTop(taskId);
    }
    TaskRecord task = null;
    if (!newTask) {
        //遍历mTaskHistory中的TaskRecord 
        boolean startIt = true;
        for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
            task = mTaskHistory.get(taskNdx);
            if (task.getTopActivity() == null) {
                //TaskRecord 所有Activity全部finish,跳过继续
                continue;
            }
            //与ActivityRecord关联的TaskRecord一致。
            if (task == r.task) {
                if (!startIt) {
                    ...
                    return;
                }
                break;
            } else if (task.numFullscreen > 0) {
                startIt = false;
            }
        }
    }
    ...
    task = r.task;
    task.addActivityToTop(r);
    task.setFrontOfTask();

    r.putInHistory();
    if (!isHomeStack() || numActivities() > 0) {
        boolean showStartingIcon = newTask;
        ProcessRecord proc = r.app;
        if (proc == null) {
            proc = mService.mProcessNames.get(r.processName, r.info.applicationInfo.uid);
        }
        if (proc == null || proc.thread == null) {
            showStartingIcon = true;
        }
        ....
        mWindowManager.addAppToken(task.mActivities.indexOf(r),
                r.appToken, r.task.taskId, mStackId, r.info.screenOrientation, r.fullscreen,
                (r.info.flags & ActivityInfo.FLAG_SHOW_FOR_ALL_USERS) != 0, r.userId,
                r.info.configChanges, task.voiceSession != null, r.mLaunchTaskBehind);
        boolean doShow = true;
        ...
    } else {
        ...
    }

    if (doResume) {
        //this即targetStack
        mStackSupervisor.resumeTopActivitiesLocked(this, r, options);
    }
}

newTask字段,代表是否为该ActivityRecord记录新建TaskRecord,启动模式和FLAG_ACTIVITY_NEW_TASK有关,这里不需新建。
将ActivityRecord加入TaskRecord中mActivities列表尾部,调用Wms服务的addAppToken方法,Activity组件界面提前注入窗体Token,token是前面介绍的IBinder token。最后,回到栈监管者ActivityStackSupervisor,恢复任务栈顶的ActivityRecord,调用ActivityStack的resumeTopActivitiesLocked方法。
第五个方法,ActivityStack#resumeTopActivityInnerLocked方法。

final ActivityRecord next = topRunningActivityLocked(null);
...
if (mResumedActivity != null) {
    pausing |= startPausingLocked(userLeaving, false, true, dontWaitForPause);
}

恢复栈ActivityStack中最Top的ActivityRecord记录。topRunningActivityLocked方法,获取任务栈顶ActivityRecord,即next。
此时,mResumedActivity字段,保存的仍是上一个ActivityRecord记录,和最上面的next不同,(标准启动模式时,新建记录已经入栈)。通过startPausingLocked方法,该记录暂停,让mPausingActivity指向它。
第六个方法,ActivityStack的startPausingLocked方法

ActivityRecord prev = mResumedActivity;
...
//置空mResumedActivity
mResumedActivity = null;
mPausingActivity = prev;
...
if (prev.app != null && prev.app.thread != null) {    
    try {
        prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing,
                        userLeaving, prev.configChangeFlags, dontWait);
    } catch (Exception e) {
        mPausingActivity = null;
        mLastPausedActivity = null;
        mLastNoHistoryActivity = null;
    }
} 

if (mPausingActivity != null) {
    if (dontWait) {
        completePauseLocked(false);
        return false;
    } else {
        Message msg = mHandler.obtainMessage(PAUSE_TIMEOUT_MSG);
        msg.obj = prev;
        prev.pauseTime = SystemClock.uptimeMillis();
        mHandler.sendMessageDelayed(msg, PAUSE_TIMEOUT);
        return true;
    }
}

暂停上一个Resume的Activity,即mResumedActivity,并置空。
获取ProcessRecord的ApplicationThread对象,schedulePauseActivity方法,回调App进程,appToken标识暂停的是哪一个Activity。向App进程的主线程发送一个PAUSE_ACTIVITY消息,实现上一个Activity的onPause生命周期方法。同时,延迟500毫秒发送一个PAUSE_TIMEOUT_MSG消息到system_server进程ActivityManager线程消息队列。
暂停后,设置pausing标志,原路一步步返回,Ams服务的Binder线程结束。回到App进程启动Activity组件的Instrumentation的execStartActivity方法位置,返回结果。

if (pausing) {
    if (next.app != null && next.app.thread != null) {
        mService.updateLruProcessLocked(next.app, true, null);
     }
    if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked();
    return true;
}

App主线程从休眠中唤醒,checkStartActivityResult方法结果处理。然后,继续读取主消息队列,下一个消息应该PAUSE_ACTIVITY消息。

如果Ams服务的返回是START_SUCCESS成功,checkStartActivityResult方法直接返回,否则将抛出异常。

switch (res) {
    case ActivityManager.START_INTENT_NOT_RESOLVED:
    case ActivityManager.START_CLASS_NOT_FOUND:
        if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
            throw new ActivityNotFoundException(
                    "Unable to find explicit activity class "
                    + ((Intent)intent).getComponent().toShortString()
                    + "; have you declared this activity in your AndroidManifest.xml?");
    ...
}

常见异常情况,组件未在AndroidManifest.xml文件注册,返回内容是未找到类,在App主线程和system_server进程ActivityManager线程均有待处理消息,并行。两者进入Ams的执行代码都是ActivityStack的activityPausedLocked()方法。在App进程中,ActivityThread处理前一个Activity生命周期onPause方法。

handlePauseActivity(IBinder token, boolean finished,
            boolean userLeaving, int configChanges, boolean dontReport){
    ActivityClientRecord r = mActivities.get(token);
    ...
    performPauseActivity(token, finished, r.isPreHoneycomb());
    ActivityManagerNative.getDefault().activityPaused(token);
}

最后,Ams服务调用activityPaused方法,通知Ams服务,token标识的Activity生命周期onPause方法已完成。

//Ams#activityPaused方法。
@Override
public final void activityPaused(IBinder token) {
    ...
    synchronized(this) {
        ActivityStack stack = ActivityRecord.getStackLocked(token);
        if (stack != null) {
            stack.activityPausedLocked(token, false);
        }
    }
    ...
}
//ActivityStack内部Handler
public void handleMessage(Message msg) {
    synchronized (mService) {//同步
        ...
        activityPausedLocked(r.appToken, true);
    }
}

Ams服务启动Binder线程,同时,ActivityManager线程消息队列get待处理的消息,两者都会并发进入activityPausedLocked方法,不过它有一个延迟,因此,一般情况Binder线程快一些。
第七个方法,ActivityStack的activityPausedLocked方法。

final void activityPausedLocked(IBinder token, boolean timeout) {
    final ActivityRecord r = isInStackLocked(token);
    if (r != null) {
        mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r);
        if (mPausingActivity == r) {
            completePauseLocked(true);
        } else {
            ....
        }
    }
}

如果一个线程已执行,mPausingActivity置空,另一个线程进入后判断,将停止执行。到栈监管者的resumeTopActivitiesLocked方法,已完成Activity组件暂停,mResumedActivity已置空,因此,无pause状态标志,继续向下走流程,恢复新ActivityRecord记录。

查看pause标志程序调试图

next代表即将启动的ActivityRecord。

//ActivityStack#resumeTopActivityInnerLocked代码片段
if (next.app != null && next.app.thread != null) {
    ...
}else{
    mStackSupervisor.startSpecificActivityLocked(next, true, true);
}

ActivityRecord内部ProcessRecord还未设置,next.app是空,进入栈监管者的startSpecificActivityLocked方法。

void startSpecificActivityLocked(ActivityRecord r,
            boolean andResume, boolean checkConfig) {
    ProcessRecord app = mService.getProcessRecordLocked(r.processName,
                r.info.applicationInfo.uid, true);
    .....
    //如果ProcessRecord不为空,说明进程已经存在,直接realStartActivityLocked后退出。
    if (app != null && app.thread != null) {
        try {
            .....
            realStartActivityLocked(r, app, andResume, checkConfig);
            return;
        } catch (RemoteException e) {
        }
    }
    //ProcessRecord不存在,创建进程
    mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
                "activity", r.intent.getComponent(), false, false, true);
}

根据ActivityRecord的包名和application的uid获取ProcessRecord,它代表此ActivityRecord记录的进程,如果ProcessRecord不是空,不需要创建进程。
ActivityRecord引用ProcessRecord,scheduleLaunchActivity方法,回调App进程,向App主线程发送一个LAUNCH_ACTIVITY消息。

final boolean realStartActivityLocked(ActivityRecord r,
            ProcessRecord app, boolean andResume, boolean checkConfig)
            throws RemoteException {
    mWindowManager.setAppVisibility(r.appToken, true);
    ...
    r.app = app;
    ...
    //回调App进程,发起生命周期方法。
    app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                    System.identityHashCode(r), r.info, new Configuration(mService.mConfiguration),
                    new Configuration(stack.mOverrideConfig), r.compat, r.launchedFromPackage,
                    task.voiceInteractor, app.repProcState, r.icicle, r.persistentState, results,
                    newIntents, !andResume, mService.isNextTransitionForward(), profilerInfo);
}

最后,不管是system_server进程的ActivityManager线程,或是Ams服务的Binder线程进入的ActivityStack的activityPausedLocked方法,处理逻辑相同,Binder线程执行完成后返回到App进程的以下代码位置。

ActivityManagerNative.getDefault().activityPaused(token)

以上代码,在App进程,说明主线程PAUSE_ACTIVITY消息处理结束,继续消息队列,下一个消息就是Ams服务回调的LAUNCH_ACTIVITY消息,即将开始Activity组件的对象创建,生命周期。Ams服务传给App的token参数,将作为一个IBinder连接,连接启动的该Activity组件和Ams服务的注册记录。


总结

启动一个Activity组件借助Instrumentation类的execStartActivity方法,跨进程访问system_server进程的Ams服务。
Ams服务负责注册启动的组件,包括创建ActivityRecord记录,根据启动模式进行组件的统一管理(暂停/恢复),进程判断及创建。
ActivityStack栈有一个TaskRecord列表,TaskRecord有一个ActivityRecord列表。在最普通情况下,TaskRecord保存相同栈的组件。
IApplicationThread是Ams服务回调客户端对象。启动时,Ams服务将多次回调App进程,如让前组件暂停,开启新组件生命周期,通过一个token标示客户端组件。
Ams服务会先暂停前组件,再发起新组件,因此,生命周期顺序是先onPause前Activity,后onCreate,onStart,onResume。
Ams服务回调App进程,通过ActivityThread类内部Handler向主线程发送消息。


任重而道远

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

推荐阅读更多精彩内容