相關文章 Android系統架構與系統源碼目錄 Android系統啟動流程(一)解析init進程啟動過程 Android系統啟動流程(二)解析Zygote進程啟動過程 Android系統啟動流程(三)解析SyetemServer進程啟動過程 Android系統啟動流程(四)Launcher啟動過程與系統啟動流程
前言 在此前我講過Android系統的啟動流程,系統啟動后,我們就比較關心應用程序是如何啟動的,這一篇我們來一起學習Android7.0 應用程序進程啟動過程,需要注意的是“應用程序進程啟動過程”,而不是應用程序啟動過程。關于應用程序啟動過程,我會在后續系列的文章中講到。希望閱讀這篇文章前先閱讀本文列出的相關文章,要不你一臉蒙蔽,就別怪我了。
1.應用程序進程概述 要想啟動一個應用程序,首先要保證這個應用程序所需要的應用程序進程已經被啟動。ActivityManagerService在啟動應用程序時會檢查這個應用程序需要的應用程序進程是否存在,不存在就會請求Zygote進程將需要的應用程序進程啟動。在Android系統啟動流程(二)解析Zygote進程啟動過程這篇文章中,我提到了Zygote的Java框架層中,會創建一個Server端的Socket,這個Socket用來等待ActivityManagerService來請求Zygote來創建新的應用程序進程的。我們知道Zygote進程通過fock自身創建的應用程序進程,這樣應用程序程序進程就會獲得Zygote進程在啟動時創建的虛擬機實例。當然,在應用程序創建過程中除了獲取虛擬機實例,還可以獲得Binder線程池和消息循環,這樣運行在應用進程中應用程序就可以方便的使用Binder進行進程間通信以及消息處理機制了。先給出應用程序進程啟動過程的時序圖,然后對每一個步驟進行詳細分析,如下圖所示。
2.應用程序進程創建過程 發送創建應用程序進程請求 ActivityManagerService會通過調用startProcessLocked函數來向Zygote進程發送請求,如下所示。frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
private final
void startProcessLocked(ProcessRecord app,
String hostingType,
String hostingNameStr,
String abiOverride,
String entryPoint,
String [] entryPointArgs) {...try {
try {final int userId = UserHandle.getUserId(app.uid);AppGlobals.getPackageManager().checkPackageStartable(app.info.packageName, userId);}
catch (RemoteException e) {
throw e.rethrowAsRuntimeException();}int uid = app.uid;int[] gids =
null ;int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;
if (!app.isolated) {...
if (ArrayUtils.isEmpty(permGids)) {gids =
new int[
2 ];}
else {gids =
new int[permGids.length +
2 ];System.arraycopy(permGids,
0 , gids,
2 , permGids.length);}gids[
0 ] = UserHandle.getSharedAppGid(UserHandle.getAppId(uid));gids[
1 ] = UserHandle.getUserGid(UserHandle.getUserId(uid));}...if (entryPoint ==
null ) entryPoint =
"android.app.ActivityThread" ;Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"Start proc: " +app.processName);checkTime(startTime,
"startProcess: asking zygote to start proc" );Process.ProcessStartResult startResult = Process.start(entryPoint,app.processName, uid, uid, gids, debugFlags, mountExternal,app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,app.info.dataDir, entryPointArgs);...}
catch (RuntimeException e) {...}}...}
復制代碼 在注釋1處的達到創建應用程序進程的用戶ID,在注釋2處對用戶組ID:gids進行創建和賦值。注釋3處如果entryPoint 為null則賦值為"android.app.ActivityThread"。在注釋4處調用Process的start函數,將此前得到的應用程序進程用戶ID和用戶組ID傳進去,第一個參數entryPoint我們得知是"android.app.ActivityThread",后文會再次提到它。接下來我們來查看Process的start函數,如下所示。frameworks/base/core/java/android/os/Process.java
public
static final ProcessStartResult start(final
String processClass,final
String niceName,int uid, int gid, int[] gids,int debugFlags, int mountExternal,int targetSdkVersion,
String seInfo,
String abi,
String instructionSet,
String appDataDir,
String [] zygoteArgs) {
try {
return startViaZygote(processClass, niceName, uid, gid, gids,debugFlags, mountExternal, targetSdkVersion, seInfo,abi, instructionSet, appDataDir, zygoteArgs);}
catch (ZygoteStartFailedEx ex) {...}
}
復制代碼 start函數中只調用了startViaZygote函數:frameworks/base/core/java/android/os/Process.java
private
static ProcessStartResult startViaZygote(final
String processClass,final
String niceName,final int uid, final int gid,final int[] gids,int debugFlags, int mountExternal,int targetSdkVersion,
String seInfo,
String abi,
String instructionSet,
String appDataDir,
String [] extraArgs)throws ZygoteStartFailedEx {synchronized(Process.class) {ArrayList<
String > argsForZygote =
new ArrayList<
String >();argsForZygote.add(
"--runtime-args" );argsForZygote.add(
"--setuid=" + uid);argsForZygote.add(
"--setgid=" + gid);...if (gids !=
null && gids.length >
0 ) {StringBuilder sb =
new StringBuilder();sb.append(
"--setgroups=" );int sz = gids.length;
for (int i =
0 ; i < sz; i++) {
if (i !=
0 ) {sb.append(
',' );}sb.append(gids[i]);}argsForZygote.add(sb.toString());}...argsForZygote.add(processClass);
if (extraArgs !=
null ) {
for (
String arg : extraArgs) {argsForZygote.add(arg);}}
return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);}}
復制代碼 在注釋1處創建了字符串列表argsForZygote ,并將啟動應用進程的啟動參數保存在argsForZygote中,函數的最后會調用zygoteSendArgsAndGetResult函數,需要注意的是,zygoteSendArgsAndGetResult函數中第一個參數中調用了openZygoteSocketIfNeeded函數,而第二個參數是保存應用進程的啟動參數的argsForZygote。zygoteSendArgsAndGetResult函數如下所示。frameworks/base/core/java/android/os/Process.java
private
static ProcessStartResult zygoteSendArgsAndGetResult(ZygoteState zygoteState, ArrayList<
String > args)throws ZygoteStartFailedEx {
try {final BufferedWriter writer = zygoteState.writer;final DataInputStream inputStream = zygoteState.inputStream;writer.write(Integer.toString(args.size()));writer.newLine();int sz = args.size();
for (int i =
0 ; i < sz; i++) {
String arg = args.get(i);
if (arg.indexOf(
'\n' ) >=
0 ) {
throw new ZygoteStartFailedEx(
"embedded newlines not allowed" );}writer.write(arg);writer.newLine();}writer.flush();ProcessStartResult result =
new ProcessStartResult();result.pid = inputStream.readInt();
if (result.pid <
0 ) {
throw new ZygoteStartFailedEx(
"fork() failed" );}result.usingWrapper = inputStream.readBoolean();
return result;}
catch (IOException ex) {zygoteState.close();
throw new ZygoteStartFailedEx(ex);}}
復制代碼 zygoteSendArgsAndGetResult函數主要做的就是將傳入的應用進程的啟動參數argsForZygote,寫入到ZygoteState中,結合上文我們知道ZygoteState其實是由openZygoteSocketIfNeeded函數返回的,那么我們接著來看openZygoteSocketIfNeeded函數,代碼如下所示。frameworks/base/core/java/android/os/Process.java
private
static ZygoteState openZygoteSocketIfNeeded(
String abi) throws ZygoteStartFailedEx {
if (primaryZygoteState ==
null || primaryZygoteState.isClosed()) {
try {primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);}
catch (IOException ioe) {
throw new ZygoteStartFailedEx(
"Error connecting to primary zygote" , ioe);}}
if (primaryZygoteState.matches(abi)) {
return primaryZygoteState;}
if (secondaryZygoteState ==
null || secondaryZygoteState.isClosed()) {
try {secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET);}
catch (IOException ioe) {
throw new ZygoteStartFailedEx(
"Error connecting to secondary zygote" , ioe);}}
if (secondaryZygoteState.matches(abi)) {
return secondaryZygoteState;}
throw new ZygoteStartFailedEx(
"Unsupported zygote ABI: " + abi);
復制代碼 在講到Zygote進程啟動過程時我們得知,在Zygote的main函數中會創建name為“zygote”的Server端Socket。在注釋1處會調用ZygoteState的connect函數與名稱為ZYGOTE_SOCKET的Socket建立連接,這里ZYGOTE_SOCKET的值為“zygote”。注釋2處如果連接name為“zygote”的Socket返回的primaryZygoteState與當前的abi不匹配,則會在注釋3處連接name為“zygote_secondary”的Socket。這兩個Socket區別就是:name為"zygote”的Socket是運行在64位Zygote進程中的,而name為“zygote_secondary”的Socket則運行在32位Zygote進程中。既然應用程序進程是通過Zygote進程fock產生的,當要連接Zygote中的Socket時,也需要保證位數的一致。
接收請求并創建應用程序進程 Socket進行連接成功并匹配abi后會返回ZygoteState類型對象,我們在分析zygoteSendArgsAndGetResult函數中講過,會將應用進程的啟動參數argsForZygote寫入到ZygoteState中,這樣Zygote進程就會收到一個創建新的應用程序進程的請求,我們回到ZygoteInit的main函數,如下所示。
frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
public
static void main(
String argv[]) {...try {... registerZygoteSocket(socketName);...preload();...if (startSystemServer) {startSystemServer(abiList, socketName);}Log.i(TAG,
"Accepting command socket connections" );runSelectLoop(abiList);closeServerSocket();}
catch (MethodAndArgsCaller caller) {caller.run();}
catch (RuntimeException ex) {Log.e(TAG,
"Zygote died with exception" , ex);closeServerSocket();
throw ex;}}
復制代碼 這些內容在Android系統啟動流程(二)解析Zygote進程啟動過程講過,但為了更好的理解我再講一遍。注釋1處通過registerZygoteSocket函數來創建一個Server端的Socket,這個name為”zygote”的Socket用來等待ActivityManagerService來請求Zygote來創建新的應用程序進程。注釋2處用來預加載類和資源。注釋3處用來啟動SystemServer進程,這樣系統的關鍵服務也會由SystemServer進程啟動起來。注釋4處調用runSelectLoop函數來等待ActivityManagerService的請求。我們就來查看runSelectLoop函數:
frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
private
static void runSelectLoop(
String abiList) throws MethodAndArgsCaller {ArrayList<FileDescriptor> fds =
new ArrayList<FileDescriptor>();ArrayList<ZygoteConnection> peers =
new ArrayList<ZygoteConnection>();fds.add(sServerSocket.getFileDescriptor());peers.add(
null );
while (
true ) {...for (int i = pollFds.length -
1 ; i >=
0 ; --i) {
if ((pollFds[i].revents & POLLIN) ==
0 ) {
continue ;}
if (i ==
0 ) {ZygoteConnection newPeer = acceptCommandPeer(abiList);peers.add(newPeer);fds.add(newPeer.getFileDesciptor());}
else {boolean done = peers.get(i).runOnce();
if (done) {peers.remove(i);fds.remove(i);}}}}}
復制代碼 當有ActivityManagerService的請求數據到來時會調用注釋1處的代碼,結合注釋2處的代碼,我們得知注釋1處的代碼其實是調用ZygoteConnection的runOnce函數來處理請求的數據:frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java
boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {
String args[];Arguments parsedArgs =
null ;FileDescriptor[] descriptors;
try {args = readArgumentList();descriptors = mSocket.getAncillaryFileDescriptors();}
catch (IOException ex) {Log.w(TAG,
"IOException on command socket " + ex.getMessage());closeSocket();
return true ;}
...try {parsedArgs =
new Arguments(args);...pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,parsedArgs.appDataDir);}
catch (ErrnoException ex) {....}
try {
if (pid ==
0 ) {IoUtils.closeQuietly(serverPipeFd);serverPipeFd =
null ;handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);
return true ;}
else {IoUtils.closeQuietly(childPipeFd);childPipeFd =
null ;
return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);}}
finally {IoUtils.closeQuietly(childPipeFd);IoUtils.closeQuietly(serverPipeFd);}}
復制代碼 在注釋1處調用readArgumentList函數來獲取應用程序進程的啟動參數,并在注釋2處將readArgumentList函數返回的字符串封裝到Arguments對象parsedArgs中。注釋3處調用Zygote的forkAndSpecialize函數來創建應用程序進程,參數為parsedArgs中存儲的應用進程啟動參數,返回值為pid。forkAndSpecialize函數主要是通過fork當前進程來創建一個子進程的,如果pid等于0,則說明是在新創建的子進程中執行的,就會調用handleChildProc函數來啟動這個子進程也就是應用程序進程,如下所示。frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java
private
void handleChildProc(Arguments parsedArgs,FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)throws ZygoteInit.MethodAndArgsCaller {...RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,parsedArgs.remainingArgs,
null );}}
復制代碼 handleChildProc函數中調用了RuntimeInit的zygoteInit函數,如下所示。frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
public
static final
void zygoteInit(int targetSdkVersion,
String [] argv, ClassLoader classLoader)throws ZygoteInit.MethodAndArgsCaller {
if (DEBUG) Slog.d(TAG,
"RuntimeInit: Starting application from zygote" );Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"RuntimeInit" );redirectLogStreams();commonInit();nativeZygoteInit();applicationInit(targetSdkVersion, argv, classLoader);}
復制代碼 注釋1處會在新創建的應用程序進程中創建Binder線程池,這個在下一篇文章會詳細介紹。在注釋2處調用了applicationInit函數:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
private
static void applicationInit(int targetSdkVersion,
String [] argv, ClassLoader classLoader)throws ZygoteInit.MethodAndArgsCaller {...final Arguments args;
try {args =
new Arguments(argv);}
catch (IllegalArgumentException ex) {Slog.e(TAG, ex.getMessage());
return ;}Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);invokeStaticMain(args.startClass, args.startArgs, classLoader);}
復制代碼 在applicationInit中會在注釋1處調用invokeStaticMain函數,需要注意的是第一個參數args.startClass,這里指的就是此篇文章開頭提到的參數:android.app.ActivityThread。接下來我們查看invokeStaticMain函數,如下所示。frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
private
static void invokeStaticMain(
String className,
String [] argv, ClassLoader classLoader)throws ZygoteInit.MethodAndArgsCaller {Class<?> cl;
try {cl = Class.forName(className,
true , classLoader);}
catch (ClassNotFoundException ex) {
throw new RuntimeException(
"Missing class when invoking static main " + className,ex);}Method m;
try {m = cl.getMethod(
"main" ,
new Class[] {
String [].class });}
catch (NoSuchMethodException ex) {
throw new RuntimeException(
"Missing static main on " + className, ex);}...throw
new ZygoteInit.MethodAndArgsCaller(m, argv);
}
復制代碼 可以看到注釋1處通過反射來獲得android.app.ActivityThread類,接下來在注釋2處來獲得ActivityThread的main函數,并將main函數傳入到注釋3處的ZygoteInit中的MethodAndArgsCaller類的構造函數中,MethodAndArgsCaller類內部會通過反射調用ActivityThread的main函數,這樣應用程序進程就創建完成了。
3.Binder線程池啟動過程 我們首先來看RuntimeInit類的zygoteInit函數,如下所示frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
public
static final
void zygoteInit(int targetSdkVersion,
String [] argv, ClassLoader classLoader)throws ZygoteInit.MethodAndArgsCaller {
if (DEBUG) Slog.d(TAG,
"RuntimeInit: Starting application from zygote" );Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"RuntimeInit" );redirectLogStreams();commonInit();nativeZygoteInit();applicationInit(targetSdkVersion, argv, classLoader);}
復制代碼 注釋1處會在新創建的應用程序進程中創建Binder線程池,來查看nativeZygoteInit函數:
private
static final native
void nativeZygoteInit();
復制代碼 很明顯nativeZygoteInit是一個jni方法,它對應的函數是什么呢。在 AndroidRuntime.cpp的JNINativeMethod數組中我們得知它對應的函數是com_android_internal_os_RuntimeInit_nativeZygoteInit,如下所示。frameworks/base/core/jni/AndroidRuntime.cpp
static const JNINativeMethod gMethods[] = {{
"nativeFinishInit" ,
"()V" ,(
void *) com_android_internal_os_RuntimeInit_nativeFinishInit },{
"nativeZygoteInit" ,
"()V" ,(
void *) com_android_internal_os_RuntimeInit_nativeZygoteInit },{
"nativeSetExitWithoutCleanup" ,
"(Z)V" ,(
void *) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
};
復制代碼 接著來查看 com_android_internal_os_RuntimeInit_nativeZygoteInit函數:frameworks/base/core/jni/AndroidRuntime.cpp
static void com_android_internal_os_RuntimeInit_nativeZygoteInit(JNIEnv* env, jobject clazz)
{gCurRuntime->onZygoteInit();
}
復制代碼 gCurRuntime是在AndroidRuntime初始化就創建的。如下所示。frameworks/base/core/jni/AndroidRuntime.cpp
AndroidRuntime::AndroidRuntime(char* argBlockStart,
const size_t argBlockLength) :mExitWithoutCleanup(
false ),mArgBlockStart(argBlockStart),mArgBlockLength(argBlockLength)
{...gCurRuntime =
this ;
}
復制代碼 在Android系統啟動流程(二)解析Zygote進程啟動過程這篇文章我們得知AppRuntime繼承AndroidRuntime,AppRuntime創建時就會調用AndroidRuntime的構造函數,gCurRuntime就會被初始化,它指向的是AppRuntime,因此我們來查看AppRuntime的onZygoteInit函數,AppRuntime的實現在app_main.cpp中,如下所示。frameworks/base/cmds/app_process/app_main.cpp
virtual
void onZygoteInit(){sp<ProcessState> proc = ProcessState::self();ALOGV(
"App process: starting thread pool.\n" );proc->startThreadPool();}
復制代碼 最后一行會調用ProcessState的startThreadPool函數:frameworks/native/libs/binder/ProcessState.cpp
void ProcessState::startThreadPool()
{AutoMutex _l(mLock);
if (!mThreadPoolStarted) {mThreadPoolStarted =
true ;spawnPooledThread(
true );}
}
復制代碼 支持Binder通信的進程中都有一個ProcessState類,它里面有一個mThreadPoolStarted 變量,來表示Binder線程池是否已經被啟動過,默認值為false。在每次調用這個函數時都會先去檢查這個標記,從而確保Binder線程池只會被啟動一次。如果Binder線程池未被啟動則設置mThreadPoolStarted為true,最后調用spawnPooledThread函數來創建線程池中的第一個線程,也就是線程池的main線程,如下所示。frameworks/native/libs/binder/ProcessState.cpp
void ProcessState::spawnPooledThread(bool isMain)
{
if (mThreadPoolStarted) {String8 name = makeBinderThreadName();ALOGV(
"Spawning new pooled thread, name=%s\n" , name.string());sp<Thread> t =
new PoolThread(isMain);t->run(name.string());}
}
復制代碼 可以看到Binder線程為一個PoolThread。注釋1調用PoolThread的run函數來啟動一個啟動一個新的線程。來查看PoolThread類里做了什么:frameworks/native/libs/binder/ProcessState.cpp
class PoolThread : public Thread
{
..
protected:virtual bool threadLoop(){IPCThreadState::self()->joinThreadPool(mIsMain);//1return false;}const bool mIsMain;
};
復制代碼 PoolThread類繼承了Thread類。注釋1處會將調用IPCThreadState的joinThreadPool函數,將當前線程注冊到Binder驅動程序中,這樣我們創建的線程就加入了Binder線程池中,這樣新創建的應用程序進程就支持Binder進程間通信了,Binder線程池啟動過程就講到這,接下來我們來學習消息循環創建過程。
4.消息循環創建過程 首先我們回到上篇最后講到的RuntimeInit的invokeStaticMain函數,代碼如下所示。frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
private
static void invokeStaticMain(
String className,
String [] argv, ClassLoader classLoader)throws ZygoteInit.MethodAndArgsCaller {Class<?> cl;...throw
new ZygoteInit.MethodAndArgsCaller(m, argv);
}
復制代碼 invokeStaticMain函數在上篇已經講過,這里不再贅述,主要是看最后一行,會拋出一個MethodAndArgsCaller異常,這個異常會被ZygoteInit的main函數捕獲,如下所示。frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
public
static void main(
String argv[]) {...try {...}
catch (MethodAndArgsCaller caller) {caller.run();}
catch (RuntimeException ex) {Log.e(TAG,
"Zygote died with exception" , ex);closeServerSocket();
throw ex;}}
復制代碼 注釋1處捕獲到MethodAndArgsCaller 時會執行caller的run函數,如下所示。frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
public
static class MethodAndArgsCaller extends Exception implements Runnable {private final Method mMethod;private final
String [] mArgs;public MethodAndArgsCaller(Method method,
String [] args) {mMethod = method;mArgs = args;}public
void run() {
try {mMethod.invoke(
null ,
new Object [] { mArgs });}
catch (IllegalAccessException ex) {
throw new RuntimeException(ex);}...throw
new RuntimeException(ex);}}}
復制代碼 根據上一篇文章我們得知,mMethod指的就是ActivityThread的main函數,mArgs 指的是應用程序進程的啟動參數。在注釋1處調用ActivityThread的main函數,代碼如下所示。frameworks/base/core/java/android/app/ActivityThread.java
public
static void main(
String [] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"ActivityThreadMain" );SamplingProfilerIntegration.start();
...Looper.prepareMainLooper();ActivityThread thread =
new ActivityThread();thread.attach(
false );
if (sMainThreadHandler ==
null ) {sMainThreadHandler = thread.getHandler();}
if (
false ) {Looper.myLooper().setMessageLogging(
new LogPrinter(Log.DEBUG,
"ActivityThread" ));}Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);Looper.loop();
throw new RuntimeException(
"Main thread loop unexpectedly exited" );}
復制代碼 注釋1處在當前應用程序進程中創建消息循環,注釋2處創建ActivityThread,注釋3處調用Looper的loop,使得Looper開始工作,開始處理消息。可以看出,系統在應用程序進程啟動完成后,就會創建一個消息循環,用來方便的使用Android的消息處理機制。
歡迎關注我的微信公眾號,第一時間獲得博客更新提醒,以及更多成體系 的Android相關原創技術干貨。 掃一掃下方二維碼或者長按識別二維碼,即可關注。
總結
以上是生活随笔 為你收集整理的Android应用程序进程启动过程 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。