Android之lmkd使用篇

一、簡介

進程的啟動分冷啟動和熱啟動,當用戶退出某一個進程的時候,并不會真正的將進程退出,而是將這個進程放到后臺,以便下次啟動的時候可以馬上啟動起來,這個過程名為熱啟動,這也是Android的設計理念之一。這個機制會帶來一個問題,每個進程都有自己獨立的內存地址空間,隨著應用打開數量的增多,系統(tǒng)已使用的內存越來越大,就很有可能導致系統(tǒng)內存不足。

lmkd(Low Memory Killer Daemon)是低內存終止守護進程,用來監(jiān)控運行中android系統(tǒng)內存的狀態(tài),通過終止最不必要的進程來應對內存壓力較高的問題,使系統(tǒng)以可接受的水平運行。

這里以Android7.1和Android10為對象分析

二、實現(xiàn)原理

所有應用進程都是從zygote孵化出來的,記錄在AMS中mLruProcesses列表中,由AMS進行統(tǒng)一管理,AMS中會根據進程的狀態(tài)更新進程對應的oom_adj值,這個值會通過socket傳遞給lmkd。lmdk根據內核的版本情況,或傳遞給kernel或自身處理低內存回收機制。為騰出更多的內存空間,在內存達到一定閥值時會觸發(fā)清理oom_adj值高的進程。

三、必備知識
1.內存基礎概念

在linux下表示內存的耗用情況有四種不同的表現(xiàn)形式:
 VSS - Virtual Set Size 虛擬耗用內存(包含共享庫占用的內存)
 RSS - Resident Set Size 實際使用物理內存(包含共享庫占用的內存)
 PSS - Proportional Set Size 實際使用的物理內存(比例分配共享庫占用的內存)
 USS - Unique Set Size 進程獨自占用的物理內存(不包含共享庫占用的內存)

VSS:VSS表示一個進程可訪問的全部內存地址空間的大小。這個大小包括了進程已經申請但尚未使用的內存空間。在實際中很少用這種方式來表示進程占用內存的情況,用它來表示單個進程的內存使用情況是不準確的。

RSS:表示一個進程在RAM中實際使用的空間地址大小,包括了全部共享庫占用的內存,這種表示進程占用內存的情況也是不準確的。

PSS:表示一個進程在RAM中實際使用的空間地址大小,它按比例包含了共享庫占用的內存。假如有3個進程使用同一個共享庫,那么每個進程的PSS就包括了1/3大小的共享庫內存。這種方式表示進程的內存使用情況較準確,但當只有一個進程使用共享庫時,其情況和RSS一模一樣。

USS:表示一個進程本身占用的內存空間大小,不包含其它任何成分,這是表示進程內存大小的最好方式!

總結
VSS>=RSS>=PSS>=USS

2.ADJ值可在ProcessList中查詢
注:adj越大,越容易被kill,對于同等的adj值,內存占有越大的越容易被kill.

    static final int UNKNOWN_ADJ = 1001;

    // This is a process only hosting activities that are not visible,
    // so it can be killed without any disruption.
    static final int CACHED_APP_MAX_ADJ = 906;
    static final int CACHED_APP_MIN_ADJ = 900;

    // The B list of SERVICE_ADJ -- these are the old and decrepit
    // services that aren't as shiny and interesting as the ones in the A list.
    static final int SERVICE_B_ADJ = 800;

    // This is the process of the previous application that the user was in.
    // This process is kept above other things, because it is very common to
    // switch back to the previous app.  This is important both for recent
    // task switch (toggling between the two top recent apps) as well as normal
    // UI flow such as clicking on a URI in the e-mail app to view in the browser,
    // and then pressing back to return to e-mail.
    static final int PREVIOUS_APP_ADJ = 700;

    // This is a process holding the home application -- we want to try
    // avoiding killing it, even if it would normally be in the background,
    // because the user interacts with it so much.
    static final int HOME_APP_ADJ = 600;

    // This is a process holding an application service -- killing it will not
    // have much of an impact as far as the user is concerned.
    static final int SERVICE_ADJ = 500;

    // This is a process with a heavy-weight application.  It is in the
    // background, but we want to try to avoid killing it.  Value set in
    // system/rootdir/init.rc on startup.
    static final int HEAVY_WEIGHT_APP_ADJ = 400;

    // This is a process currently hosting a backup operation.  Killing it
    // is not entirely fatal but is generally a bad idea.
    static final int BACKUP_APP_ADJ = 300;

    // This is a process only hosting components that are perceptible to the
    // user, and we really want to avoid killing them, but they are not
    // immediately visible. An example is background music playback.
    static final int PERCEPTIBLE_APP_ADJ = 200;

    // This is a process only hosting activities that are visible to the
    // user, so we'd prefer they don't disappear.
    static final int VISIBLE_APP_ADJ = 100;
    static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

    // This is the process running the current foreground app.  We'd really
    // rather not kill it!
    static final int FOREGROUND_APP_ADJ = 0;

    // This is a process that the system or a persistent process has bound to,
    // and indicated it is important.
    static final int PERSISTENT_SERVICE_ADJ = -700;

    // This is a system persistent process, such as telephony.  Definitely
    // don't want to kill it, but doing so is not completely fatal.
    static final int PERSISTENT_PROC_ADJ = -800;

    // The system process runs at the default adjustment.
    static final int SYSTEM_ADJ = -900;

    // Special code for native processes that are not being managed by the system (so
    // don't have an oom adj assigned by the system).
    static final int NATIVE_ADJ = -1000;

四、查詢命令
1.adb shell dumpsys activity o
可查詢app對應的adj等級,同時可通過OOM levels查看在某一個等級升級內存清理

注:
(ActivityManagerService dump 可查 activity o)

C:\Users\99418>adb shell dumpsys activity o
[dump_debug] dumpAppId:-1, dumpPackage:null
  OOM levels:
    -900: SYSTEM_ADJ (   73,728K)
    -800: PERSISTENT_PROC_ADJ (   73,728K)
    -700: PERSISTENT_SERVICE_ADJ (   73,728K)
      0: FOREGROUND_APP_ADJ (   73,728K)
     100: VISIBLE_APP_ADJ (   92,160K)
     200: PERCEPTIBLE_APP_ADJ (  110,592K)
     250: PERCEPTIBLE_LOW_APP_ADJ (  129,024K)
     300: BACKUP_APP_ADJ (  221,184K)
     400: HEAVY_WEIGHT_APP_ADJ (  221,184K)
     500: SERVICE_ADJ (  221,184K)
     600: HOME_APP_ADJ (  221,184K)
     700: PREVIOUS_APP_ADJ (  221,184K)
     800: SERVICE_B_ADJ (  221,184K)
     900: CACHED_APP_MIN_ADJ (  221,184K)
     999: CACHED_APP_MAX_ADJ (  322,560K)

  Process OOM control (43 total, non-act at 1, non-svc at 1):
    PERS #42: sys    F/ /PER  trm: 0 2811:system/1000 (fixed)
        oom: max=-900 curRaw=-900 setRaw=-900 cur=-900 set=-900 //-900 adj最高級別SYSTEM_ADJ
        state: cur=PER  set=PER  lastPss=144MB lastSwapPss=21MB lastCachedPss=0.00
        cached=false empty=false hasAboveClient=false
    ···
    Proc # 0: fore   R/A/BFGS trm: 0 3930:com.android.launcher3/u0a98 (service)
        com.android.launcher3/com.android.quickstep.TouchInteractionService<=Proc{2979:com.android.systemui/u0a97}
        oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0 //0 ADJ級別FOREGROUND_APP_ADJ
        state: cur=BFGS set=BFGS lastPss=39MB lastSwapPss=20MB lastCachedPss=0.00
        cached=false empty=false hasAboveClient=false
    ···
    Proc #31: cch+75 B/ /CEM  trm: 0 4303:com.android.externalstorage/u0a49 (cch-empty)
        oom: max=1001 curRaw=975 setRaw=975 cur=975 set=975  //975 ADJ值太大,容易被清理
        state: cur=CEM  set=CEM  lastPss=6.7MB lastSwapPss=0.00 lastCachedPss=6.7MB
        cached=true empty=true hasAboveClient=false
  mHomeProcess: ProcessRecord{6b95785 3930:com.android.launcher3/u0a98}
  mPreviousProcess: null

2.adb shell dumpsys activity p
針對進程的oom查詢更加精確

(ActivityManagerService dump 可查 activity p)
C:\Users\99418>adb shell dumpsys activity p
  ···
  *APP* UID 10098 ProcessRecord{6b95785 3930:com.android.launcher3/u0a98}  //uid及pid
    user #0 uid=10098 gids={50098, 20098, 9997}
    mRequiredAbi=arm64-v8a instructionSet=null
    dir=/product/priv-app/MtkLauncher3QuickStep/MtkLauncher3QuickStep.apk //安裝位置 publicDir=/product/priv-app/MtkLauncher3QuickStep/MtkLauncher3QuickStep.apk data=/data/user/0/com.android.launcher3  //app數據位置
    packageList={com.android.launcher3}  //包名
    compat={240dpi always-compat}
    thread=android.app.IApplicationThread$Stub$Proxy@9e5af
    pid=3930 starting=false
    lastActivityTime=-1h2m37s269ms lastPssTime=-1m3s479ms pssStatType=0 nextPssTime=+13m56s441ms
    adjSeq=834 lruSeq=234 lastPss=38MB lastSwapPss=13MB lastCachedPss=0.00 lastCachedSwapPss=0.00 //pss 內存情況
    procStateMemTracker: best=1 (1=1 1.5x, 2=1 2.25x)
    cached=false empty=false
    oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0  //oom相關情況
    lastCompactTime=3986304 lastCompactAction=4    mCurSchedGroup=2 setSchedGroup=2 systemNoUi=false trimMemoryLevel=0
    curProcState=6 mRepProcState=6 pssProcState=6 setProcState=6 lastStateTime=-1h1m37s411ms
    hasShownUi=true pendingUiClean=true hasAboveClient=false treatLikeActivity=false
    reportedInteraction=true time=-1h6m45s79ms
    hasClientActivities=false foregroundActivities=true (rep=true)
    lastTopTime=-1h1m37s411ms
    startSeq=17
    mountMode=DEFAULT
    lastRequestedGc=-1h5m21s110ms lastLowMemory=-1h5m21s110ms reportLowMemory=false
    whitelistManager=true
    Activities:
      - ActivityRecord{550ca96 u0 com.android.launcher3/.Launcher t4}
    Recent Tasks:
      - TaskRecord{c8c4dbc #4 I=com.android.launcher3/.Launcher U=0 StackId=0 sz=1}
      ···
    Services:
      - ServiceRecord{a7b3c5e u0 com.android.launcher3/.notification.NotificationListener}
      ···
    Published Providers:
      - com.android.launcher3.LauncherProvider
        -> ContentProviderRecord{2d49145 u0 com.android.launcher3/.LauncherProvider}
      ···
    Connected Providers:
      - 15f9e1/com.android.providers.settings/.SettingsProvider->3930:com.android.launcher3/u0a98 s1/1 u0/0 +1h6m44s879ms
    Receivers:
      - ReceiverList{6197c9 3930 com.android.launcher3/10098/u0 remote:78f0bd0}
      ···
  ···

3.adb shell dumpsys meminfo
系統(tǒng)內存總體情況查詢及各個進程的占用情況。例如RAM總共多大、launcher3使用內存情況等等

(ActivityManagerService中MemBinder dump 可查 meminfo
ActivityManagerService.setSystemProcess:
ServiceManager.addService("meminfo", new MemBinder(this));
)
C:\Users\99418>adb shell dumpsys meminfo

Applications Memory Usage (in Kilobytes):
Uptime: 4300431 Realtime: 4384400

Total PSS by process:
    159,633K: system (pid 2811)
     96,309K: com.android.systemui (pid 2979)
     ···
        672K: lmkd (pid 467)
        669K: thermal (pid 677)
        575K: usb_mode_switch (pid 325)

Total PSS by OOM adjustment:
    644,944K: Native
         79,077K: surfaceflinger (pid 468)
         60,618K: zygote (pid 2683)
         35,930K: app_process32 (pid 17377)
         33,885K: zygote64 (pid 2682)
         21,714K: init (pid 1)
         ···
    159,633K: System
        159,633K: system (pid 2811)
    145,094K: Persistent
         96,309K: com.android.systemui (pid 2979)
         ···
     21,633K: Persistent Service
         21,633K: com.android.bluetooth (pid 3709)
     41,161K: Foreground
         41,161K: com.android.launcher3 (pid 3930 / activities)
     17,132K: Visible
          8,801K: android.ext.services (pid 3657)
          8,331K: com.android.smspush (pid 4100)
     18,848K: Perceptible
         18,848K: com.android.inputmethod.latin (pid 3583)
    122,876K: Cached
         28,258K: com.mediatek.security (pid 4325)
         ···

Total PSS by category:
    332,730K: .so mmap
    160,164K: EGL mtrack
     81,429K: Native
     67,972K: .apk mmap
     56,812K: .jar mmap
     ···

Total RAM: 3,921,160K (status normal)
 Free RAM: 2,671,288K (  122,876K cached pss +   695,064K cached kernel + 1,853,348K free)
 Used RAM: 1,234,709K (1,048,445K used pss +   186,264K kernel)
 Lost RAM:   163,273K
     ZRAM:    44,100K physical used for   217,916K in swap (2,156,632K total swap)
   Tuning: 384 (large 512), oom   322,560K, restore limit   107,520K (high-end-gfx)

4.adb shell dumpsys procstats -a
procstats工具用于分析應用內存在一段時間內的使用情況(而不像 meminfo 一樣在特定時間點捕獲快照)。

(ProcessStatsService dump 可查 procstats
ActivityManagerService.setSystemProcess:
ServiceManager.addService(ProcessStats.SERVICE_NAME, mProcessStats);
)

C:\Users\99418>adb shell dumpsys procstats -a
···
  * com.android.launcher3 / u0a17 / v25:
      Process com.android.launcher3 (unique, 5 entries):
        SOn /Norm/Top    : +1h16m18s862ms
                  Home   : +10m38s557ms
             Mod /Home   : +4m9s959ms
             Low /Top    : +10m0s237ms
                  Home   : +20h51m49s712ms
                  TOTAL  : +22h32m57s327ms
        myID=1d6fe68 mCommonProcess=1d6fe68 mPackage=com.android.launcher3
···
Memory usage:
  Kernel : 140MB (16 samples)
  Native : 573KB (16 samples)
  Persist: 52MB (12 samples)
  Top    : 5.6MB (33 samples)
  ImpFg  : 56 (9 samples)
  Service: 5.3MB (23 samples)
  Receivr: 418 (17 samples)
  Home   : 4.6MB (3 samples)
  CchEmty: 16MB (13 samples)
  Cached : 204MB (16 samples)
  Free   : 1.3GB (16 samples)
  Z-Ram  : 4.0KB (16 samples)
  TOTAL  : 1.7GB

          Start time: 2013-01-18 16:50:09
  Total elapsed time: +2h20m46s35ms (partial) (swapped-out-pss) libart.so
···

五、配置

lmkd目的為了保護設備在低內存下的正常運行。

1.查詢

Android 9及以下
adj閥門值:
/sys/module/lowmemorykiller/parameters/adj

minfree閥門值:
/sys/module/lowmemorykiller/parameters/minfree

例如
Q6-Q260S(S0):/ $ cat /sys/module/lowmemorykiller/parameters/minfree
18432,23040,27648,32256,36864,46080
Q6-Q260S(S0):/ $ cat /sys/module/lowmemorykiller/parameters/adj
0,100,200,300,900,906


Android 10
adb shell getprop sys.lmk.minfree_levels

例如:
C:\Users\99418>adb shell getprop sys.lmk.minfree_levels
18432:0,23040:100,27648:200,32256:250,55296:900,80640:950

xxx:xx //minfree:adj  注意:minfree * 4 = 真實值kb

2.原理

a.框架
system_server <--> lmkd (<--> kernel)

system_server: ActivityManagerService (ProcessList)
lmkd:system/core/lmkd

b.adj和minfree的值都有AMS通過socket傳遞到lmkd  -- 注:Android 7.1源碼
關注核心類、方法、變量:
com.android.server.am.ProcessList

    // Memory pages are 4K.
    static final int PAGE_SIZE = 4*1024;

    // These are the various interesting memory levels that we will give to
    // the OOM killer.  Note that the OOM killer only supports 6 slots, so we
    // can't give it a different value for every possible kind of process.
    private int[] mOomAdj = new int[] {
            FOREGROUND_APP_ADJ, VISIBLE_APP_ADJ, PERCEPTIBLE_APP_ADJ,
            BACKUP_APP_ADJ, CACHED_APP_MIN_ADJ, CACHED_APP_MAX_ADJ
    };
    // These are the low-end OOM level limits.  This is appropriate for an
    // HVGA or smaller phone with less than 512MB.  Values are in KB.
    private final int[] mOomMinFreeLow = new int[] {
            12288, 18432, 24576,
            36864, 43008, 49152
    };
    // These are the high-end OOM level limits.  This is appropriate for a
    // 1280x800 or larger screen with around 1GB RAM.  Values are in KB.
    private final int[] mOomMinFreeHigh = new int[] {
            73728, 92160, 110592,
            129024, 147456, 184320
    };
    
    //方法參數:displayWidth\displayHeight 關注設備屏幕尺寸。注:adb shell wm size 可查
    private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
        // Scale buckets from avail memory: at 300MB we use the lowest values to
        // 700MB or more for the top values.
        float scaleMem = ((float)(mTotalMemMb-350))/(700-350);//mTotalMemMb 關注設備RAM總量

        // Scale buckets from screen size.
        int minSize = 480*800;  //  384000
        int maxSize = 1280*800; // 1024000  230400 870400  .264
        float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize);
        if (false) {
            Slog.i("XXXXXX", "scaleMem=" + scaleMem);
            Slog.i("XXXXXX", "scaleDisp=" + scaleDisp + " dw=" + displayWidth
                    + " dh=" + displayHeight);
        }

        float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp;
        if (scale < 0) scale = 0;
        else if (scale > 1) scale = 1;
        int minfree_adj = Resources.getSystem().getInteger(
                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust);
        int minfree_abs = Resources.getSystem().getInteger(
                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute);
        if (false) {
            Slog.i("XXXXXX", "minfree_adj=" + minfree_adj + " minfree_abs=" + minfree_abs);
        }

        final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;

        //第一次:更新mOomMinFree數據,由設備RAM和屏幕尺寸決定
        for (int i=0; i<mOomAdj.length; i++) {
            int low = mOomMinFreeLow[i];
            int high = mOomMinFreeHigh[i];
            if (is64bit) {
                // Increase the high min-free levels for cached processes for 64-bit
                if (i == 4) high = (high*3)/2;
                else if (i == 5) high = (high*7)/4;
            }
            mOomMinFree[i] = (int)(low + ((high-low)*scale));
        }

       //第二次:更新mOomMinFree數據
       //由framework-res自定義的config_lowMemoryKillerMinFreeKbytesAbsolute值決定
       //minfree_abs的取值 一般比mOomMinFree[mOomAdj.length - 1]的值大一點或小一點
        if (minfree_abs >= 0) {
            for (int i=0; i<mOomAdj.length; i++) {
                mOomMinFree[i] = (int)((float)minfree_abs * mOomMinFree[i]
                        / mOomMinFree[mOomAdj.length - 1]);
            }
        }

       //第三次:更新mOomMinFree數據
       //由framework-res自定義的config_lowMemoryKillerMinFreeKbytesAdjust值決定
       //minfree_adj的取值 就是從mOomMinFree[i]中增加或減小一點,屬于微調型
        if (minfree_adj != 0) {
            for (int i=0; i<mOomAdj.length; i++) {
                mOomMinFree[i] += (int)((float)minfree_adj * mOomMinFree[i]
                        / mOomMinFree[mOomAdj.length - 1]);
                if (mOomMinFree[i] < 0) {
                    mOomMinFree[i] = 0;
                }
            }
        }

        // The maximum size we will restore a process from cached to background, when under
        // memory duress, is 1/3 the size we have reserved for kernel caches and other overhead
        // before killing background processes.
        mCachedRestoreLevel = (getMemLevel(ProcessList.CACHED_APP_MAX_ADJ)/1024) / 3;

        // Ask the kernel to try to keep enough memory free to allocate 3 full
        // screen 32bpp buffers without entering direct reclaim.
        int reserve = displayWidth * displayHeight * 4 * 3 / 1024;
        int reserve_adj = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAdjust);
        int reserve_abs = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAbsolute);

        if (reserve_abs >= 0) {
            reserve = reserve_abs;
        }

        if (reserve_adj != 0) {
            reserve += reserve_adj;
            if (reserve < 0) {
                reserve = 0;
            }
        }

        if (write) {
            ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
            buf.putInt(LMK_TARGET);
            for (int i=0; i<mOomAdj.length; i++) {
                buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);//PAGE_SIZE 內存頁占用4kb
                buf.putInt(mOomAdj[i]);
            }

            writeLmkd(buf);//將mOomAdj和mOomMinFree通過socket傳遞給lmkd
            SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve));
        }
        // GB: 2048,3072,4096,6144,7168,8192
        // HC: 8192,10240,12288,14336,16384,20480
    }
    
注:
sys.sysctl.extra_free_kbytes
可用于保留啟動后臺回收 (kswapd)的閾值和啟動直接回收(通過分配進程)的閾值之間的額外可用內存

c.總結
adj值。保持不變
minfree值。可通過overlay自定義config_lowMemoryKillerMinFreeKbytesAbsolute/config_lowMemoryKillerMinFreeKbytesAdjust

3.改動建議

因系統(tǒng)根據RAM和設備屏幕大小經過一輪初始化,一般來說,minfree也不用特定更改。
但是對于1G或2G的RAM,涉及內存使用情況,也可能需要調節(jié)。這里就直接overlay
config_lowMemoryKillerMinFreeKbytesAbsolute/config_lowMemoryKillerMinFreeKbytesAdjust即可

六、日志分析

關注TAG(lowmemorykiller) 查看是否有對應的kill行為
Android 9及之前 kill行為由內核層操作,同步在kernel日志中關注kswapd
Android 10 kill行為直接由lmkd操作,主要關注logcat日志即可

七、參考學習

http://www.itdecent.cn/p/221f4a246b45
http://www.itdecent.cn/p/980b6ce4e051
http://www.itdecent.cn/p/7bd3d0ee8a56

http://gityuan.com/2018/05/19/android-process-adj/
https://blog.csdn.net/zhzhangnews/article/details/109671845
https://lightingsui.github.io/2021/01/21/Android%E4%B8%AD%E7%9A%84LowMemoryKiller%E6%9C%BA%E5%88%B6/

https://source.android.com/devices/tech/perf/low-ram
https://source.android.google.cn/devices/architecture/kernel/reqs-interfaces?hl=zh-cn
https://source.android.google.cn/devices/tech/perf/lmkd?hl=zh-cn
https://github.com/reklaw-tech/reklaw_blog/blob/master/Android/ULMK

https://source.android.com/devices/tech/debug/procstats?hl=zh-cn
https://blog.csdn.net/Invoker123/article/details/108632459
https://blog.csdn.net/liaochaoyun/article/details/103035222
https://blog.csdn.net/mcsbary/article/details/104609109
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容