android中的oom,Android OOM Adjustments

之前有写过文章分析LMK,那篇主要是分析LMK实现原理,并没有仔细分析AMS中OOM Adj的调整。这次参考Android 9.0的代码来分析一下,主要是分析代码实现。首先看一下OOM Adj的定义都有哪些。

Adj

Value

Comments

UNKNOWN_ADJ

1001

无法确定的Adj,通常是将要缓存的进程

CACHED_APP_MAX_ADJ

906

不可见进程的Adj最大值

CACHED_APP_MIN_ADJ

900

不可见进程的Adj最小值

SERVICE_B_ADJ

800

B List中的Service,和A list相比,他们对用户的黏合度要小些

PREVIOUS_APP_ADJ

700

用户前一次交互的进程

HOME_APP_ADJ

600

Launcher进程

SERVICE_ADJ

500

应用服务进程

HEAVY_WEIGHT_APP_ADJ

400

后台的重量级进程

BACKUP_APP_ADJ

300

承载backup相关操作的进程

PERCEPTIBLE_APP_ADJ

200

可感知进程,比如后台音乐播放

VISIBLE_APP_ADJ

100

前台可见的Activity进程

FOREGROUND_APP_ADJ

0

当前正在前台运行的进程,也就是用户正在交互的那个程序

PERSISTENT_SERVICE_ADJ

-700

与系统进程或Persistent进程绑定的进程

PERSISTENT_PROC_ADJ

-800

Persistent属性的进程,如telephony

SYSTEM_ADJ

-900

系统进程

NATIVE_ADJ

-1000

Native进程,不被系统管理

updateOomAdj

OOM Adj的更新是通过AMS中updateOomAdjLocked() 函数完成的。这个函数中不仅仅更新了OOM Adj,同时还进行了内存调整。我们先看一下OOM Adj的调整实现。

final void updateOomAdjLocked() {

......

boolean retryCycles = false;

// 因为service连接,需要重置进程的cycle状态,

for (int i=N-1; i>=0; i--) {

ProcessRecord app = mLruProcesses.get(i);

app.containsCycle = false;

}

for (int i=N-1; i>=0; i--) {

ProcessRecord app = mLruProcesses.get(i);

if (!app.killedByAm && app.thread != null) {

app.procStateChanged = false;

// 计算app的OOM adj

computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);

// 如果任意一个进程处于cycle中,需要增加一次循环

retryCycles |= app.containsCycle;

// 对于没有分配Adj的后台缓存进程,在这里进行分配。

if (app.curAdj >= ProcessList.UNKNOWN_ADJ) {

......

}

}

}

// 存在处于cycle中的进程时,重新计数OOM Adj,直到没有进程提高优先级

int cycleCount = 0;

while (retryCycles) {

cycleCount++;

retryCycles = false;

for (int i=0; i

ProcessRecord app = mLruProcesses.get(i);

// 恢复处于cycle中进程的Adj序列号

if (!app.killedByAm && app.thread != null && app.containsCycle == true) {

app.adjSeq--;

app.completedAdjSeq--;

}

}

for (int i=0; i

ProcessRecord app = mLruProcesses.get(i);

// 重新计算处于cycle中进程的OOM Adj

if (!app.killedByAm && app.thread != null && app.containsCycle == true) {

if (computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now)) {

retryCycles = true;

}

}

}

}

for (int i=N-1; i>=0; i--) {

ProcessRecord app = mLruProcesses.get(i);

if (!app.killedByAm && app.thread != null) {

// 设置进程的OOM Adj

applyOomAdjLocked(app, true, now, nowElapsed);

// 统计各种进程类型的数量,并杀掉超过限制的后台缓存进程和empty进程

switch (app.curProcState) {

case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:

case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:

......

break;

case ActivityManager.PROCESS_STATE_CACHED_EMPTY:

......

break;

default:

mNumNonCachedProcs++;

break;

}

if (app.isolated && app.services.size() <= 0 && app.isolatedEntryPoint == null) {

// 杀掉孤立进程

app.kill("isolated not needed", true);

} else {

// 保留的进程,更新uid

final UidRecord uidRec = app.uidRecord;

......

}

// 统计进程状态大于HOME的数量,也就是不太重要的进程

if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME

&& !app.killedByAm) {

numTrimming++;

}

}

}

// 检查是否有uid从后台切换到前台或从前台切换到后台,并通知需要去阻止的应用

incrementProcStateSeqAndNotifyAppsLocked();

mNumServiceProcs = mNewNumServiceProcs;

......

// 进行内存调整和回收

......

// 如果设置总是销毁后台Activity

if (mAlwaysFinishActivities) {

mStackSupervisor.scheduleDestroyAllActivities(null, "always-finish");

}

if (allChanged) {

requestPssAllProcsLocked(now, false, mProcessStats.isMemFactorLowered());

}

ArrayList becameIdle = null;

// 更新UidRecord

if (mLocalPowerManager != null) {

mLocalPowerManager.startUidChanges();

}

for (int i=mActiveUids.size()-1; i>=0; i--) {

......

}

if (mLocalPowerManager != null) {

mLocalPowerManager.finishUidChanges();

}

......

}

OOM Adj的更新过程主要完成以下工作,

重新计算进程的OOM Adj值,并进行更新。

未分配Adj值的进程根据进程状态分为后台缓存进程和empty进程,在CACHED_APP_MIN_ADJ到CACHED_APP_MAX_ADJ分配Adj值。后台缓存进程和empty进程的Adj值交叉递增,每一个级别上的进程个数都不超过预先计算的最大值。

逆序处理LRU中的进程,回收超过限制的后台缓存进程和empty进程。默认的限制时后台缓存进程和empty进程各16个。

isolated进程果已经不包含服务,直接回收。

更新进程的UidRecord。

接下来分析一下updateOomAdjLocked() 中关于内存调整的部分。Android将系统内存的状态分为了4个等级,定义如下。

Adj

Value

Comments

ADJ_MEM_FACTOR_NORMAL

0

系统内存正常,不需要调整

ADJ_MEM_FACTOR_MODERATE

1

系统内存中等,低于正常状态

ADJ_MEM_FACTOR_LOW

2

系统内存低,需要回收内存

ADJ_MEM_FACTOR_CRITICAL

3

系统内存紧张,必须回收些内存

在ComponentCallbacks2中还定义了内存回收的级别,其中前三个是后台缓存的回收级别,后三个是进程运行时的回收级别。

Adj

Value

Comments

TRIM_MEMORY_COMPLETE

80

处于后台LRU列表尾部的进程,如果找不到更多内存,很快将被杀死。

TRIM_MEMORY_MODERATE

60

处于后台LRU列表中部的进程,清理内存可以让后续运行的进程获得更好的性能。

TRIM_MEMORY_BACKGROUND

40

后台进程,处于LRU列表的头部,这时清理内存可以让进程更高效的返回前台。

TRIM_MEMORY_UI_HIDDEN

20

进程UI已经不可见,可以释放UI资源。

TRIM_MEMORY_RUNNING_CRITICAL

15

设备正运行在低内存上,无法保证后台进程存活。应该尽可能的释放非关键资源。接下来要调用onLowMemory()报告系统内存低,已经显著影响用户

TRIM_MEMORY_RUNNING_LOW

10

设备正运行在低内存上,应释放不必要的资源。

TRIM_MEMORY_RUNNING_MODERATE

5

设备的运行内存偏低,可能需要释放不必要的资源

Android系统是根据后台缓存进程和empty进程的数量来区分内存等级的。因为系统总是尽可能多的保留后台进程,以便于进程再次启动时可以减少启动时间,用户体验更好。但当系统内存不足时,lowmemeorykiller机制会优先杀死不重要的后台进程,所以可以认为后台进程的数量是与lowmemrorykiller的触发挂钩的。剩余的后台进程越少,表明通过Lowmemroykiller需要回收的内存越多,整个系统的内存就越紧张。

final void updateOomAdjLocked() {

......

// 后台缓存进程与empty进程的总和

final int numCachedAndEmpty = numCached + numEmpty;

int memFactor;

// 只有cache进程和empty进程同时小于各自的TRIM值时,才认为存在内存不足的情况

// 默认情况下CUR_TRIM_EMPTY_PROCESSES=8,CUR_TRIM_CACHED_PROCESSES=5

if (numCached <= mConstants.CUR_TRIM_CACHED_PROCESSES

&& numEmpty <= mConstants.CUR_TRIM_EMPTY_PROCESSES) {

if (numCachedAndEmpty <= ProcessList.TRIM_CRITICAL_THRESHOLD) {

// 当cache+empty进程数小于3时,表明系统内存紧张

memFactor = ProcessStats.ADJ_MEM_FACTOR_CRITICAL;

} else if (numCachedAndEmpty <= ProcessList.TRIM_LOW_THRESHOLD) {

// 当cache+empty进程数小于5时,表明系统内存低

memFactor = ProcessStats.ADJ_MEM_FACTOR_LOW;

} else {

// 其他情况表明内存中等

memFactor = ProcessStats.ADJ_MEM_FACTOR_MODERATE;

}

} else {

memFactor = ProcessStats.ADJ_MEM_FACTOR_NORMAL;

}

......

mLastMemoryLevel = memFactor;

mLastNumProcesses = mLruProcesses.size();

// 设置内存调整等级,如果成功返回true

boolean allChanged = mProcessStats.setMemFactorLocked(memFactor, !isSleepingLocked(), now);

final int trackerMemFactor = mProcessStats.getMemFactorLocked();

// 内存不处于正常级别时,需要回收内存

if (memFactor != ProcessStats.ADJ_MEM_FACTOR_NORMAL) {

if (mLowRamStartTime == 0) {

mLowRamStartTime = now;

}

int step = 0;

int fgTrimLevel;

// 根据内存等级获取fgTrimLevel,在ComponentCallbacks2定义

switch (memFactor) {

......

}

// 计算factory,用于每个trimLevel上的进程数

int factor = numTrimming/3;

int minFactor = 2;

if (mHomeProcess != null) minFactor++;

if (mPreviousProcess != null) minFactor++;

if (factor < minFactor) factor = minFactor;

// 默认的trimLevel,为最高

int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;

// 逆序处理LRU中的所有进程

for (int i=N-1; i>=0; i--) {

ProcessRecord app = mLruProcesses.get(i);

......

// 处理不太重要的而进程,进程状态大于HOME

if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME

&& !app.killedByAm) {

// 进程的trimLevel小于当前级别,则进行回收

if (app.trimMemoryLevel < curLevel && app.thread != null) {

try {

app.thread.scheduleTrimMemory(curLevel);

} catch (RemoteException e) {

}

......

}

// 更新进程的trimLevel,根据factor逐渐降低级别:COMPLETE->MODERATE->BACKGROUND

app.trimMemoryLevel = curLevel;

step++;

if (step >= factor) {

......

}

} else if (app.curProcState == ActivityManager.PROCESS_STATE_HEAVY_WEIGHT

&& !app.killedByAm) {

// heavy weight进程以TRIM_MEMORY_BACKGROUND时进行回收

if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND

&& app.thread != null) {

try {

app.thread.scheduleTrimMemory(

ComponentCallbacks2.TRIM_MEMORY_BACKGROUND);

} catch (RemoteException e) {

}

}

app.trimMemoryLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;

} else {

// 进程处于后台并带有UI时,以TRIM_MEMORY_UI_HIDDEN进行回收

if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND

|| app.systemNoUi) && app.pendingUiClean) {

final int level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN;

if (app.trimMemoryLevel < level && app.thread != null) {

try {

app.thread.scheduleTrimMemory(level);

} catch (RemoteException e) {

}

}

app.pendingUiClean = false;

}

// 当fgTrimLevel大于当前trimLevel时,以fgTrimLevel进行回收

if (app.trimMemoryLevel < fgTrimLevel && app.thread != null) {

try {

app.thread.scheduleTrimMemory(fgTrimLevel);

} catch (RemoteException e) {

}

}

app.trimMemoryLevel = fgTrimLevel;

}

}

} else {

// 内存正常时的处理

if (mLowRamStartTime != 0) {

mLowRamTimeSinceLastIdle += now - mLowRamStartTime;

mLowRamStartTime = 0;

}

for (int i=N-1; i>=0; i--) {

ProcessRecord app = mLruProcesses.get(i);

if (allChanged || app.procStateChanged) {

setProcessTrackerStateLocked(app, trackerMemFactor, now);

app.procStateChanged = false;

}

// 后台带有UI的进程以TRIM_MEMORY_UI_HIDDEN进行回收

if ((app.curProcState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND

|| app.systemNoUi) && app.pendingUiClean) {

if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN

&& app.thread != null) {

try {

app.thread.scheduleTrimMemory(

ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);

} catch (RemoteException e) {

}

}

app.pendingUiClean = false;

}

app.trimMemoryLevel = 0;

}

}

......

}

内存调整的主要工作是在不杀死进程的情况下,根据需要对内存进行回收。

内存低时,对于进程状态大于HOME的不太重要的进程,根据LRU倒序内存回收级别逐渐降低。

内存低时,对于重要的进程,越重要内存回收等级越高。

内存正常时,对后台带有UI的进程进行内存回收。

computeOomAdj

上面分析updateOomAdjLocked() 的大致流程,接着分析一下其中的一个重要函数computeOomAdjLocked(),是如何计算OOM Adj的。

private final boolean computeOomAdjLocked(ProcessRecord app, int cachedAdj,

ProcessRecord TOP_APP, boolean doingAll, long now) {

// 判断Adj序列号,相等表示已经计算过或在计算中

if (mAdjSeq == app.adjSeq) {

......

}

// 设置空进程的Adj

if (app.thread == null) {

......

}

......

// 计算Adj最大值小于FOREGROUND进程的Adj,系统进程或Persistent进程

if (app.maxAdj <= ProcessList.FOREGROUND_APP_ADJ) {

// 设置Adj,SchedGroup,ProcState,UI状态等

......

app.curAdj = app.maxAdj;

app.completedAdjSeq = app.adjSeq;

// 如果Adj小于计算前的值,则进程Adj被提升

return app.curAdj < prevAppAdj;

}

......

// 根据进程的状态设置相应的Adj,SchedGroup,ProcState

if (PROCESS_STATE_CUR_TOP == ActivityManager.PROCESS_STATE_TOP && app == TOP_APP) {

// 前台进程

......

} else if (app.runningRemoteAnimation) {

// 正在运行远端的动画

......

} else if (app.instr != null) {

// 正在运行测试程序

......

} else if (isReceivingBroadcastLocked(app, mTmpBroadcastQueue)) {

// 正在处理广播

......

} else if (app.executingServices.size() > 0) {

// 正在执行Service的回调

......

} else if (app == TOP_APP) {

// 前台进程,但系统灭屏

......

} else {

// 空进程

......

}

// 非前台的activities,继续调整Adj

if (!foregroundActivities && activitiesSize > 0) {

int minLayer = ProcessList.VISIBLE_APP_LAYER_MAX;

for (int j = 0; j < activitiesSize; j++) {

......

if (r.visible) {

// 如果进程包含可见activity,Adj仅升级调整,升至VISIBLE

......

} else if (r.isState(ActivityState.PAUSING, ActivityState.PAUSED)) {

// 如果进程activity处理暂停状态,Adj升至PERCEPTIBLE

......

} else if (r.isState(ActivityState.STOPPING)) {

// 如果进程activity处理正在停止状态,Adj升至PERCEPTIBLE

......

} else {

// 如果进程只包含cached-activity,仅调整procState

......

}

}

// 非前台包含可见activity进程的Adj跟随层次变化,越往下Adj越大

if (adj == ProcessList.VISIBLE_APP_ADJ) {

adj += minLayer;

}

}

......

if (adj > ProcessList.PERCEPTIBLE_APP_ADJ

|| procState > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {

// 对于前台服务进程或显示overylay UI的进程,Adj设置为PERCEPTIBLE,但procState不同

......

}

if (adj > ProcessList.PERCEPTIBLE_APP_ADJ

|| procState > ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND) {

// 进程显示toasts,Adj升至PERCEPTIBLE

......

}

if (app == mHeavyWeightProcess) {

// 重量级进程,Adj升至HEAVY_WEIGHT

......

}

if (app == mHomeProcess) {

// home进程,Adj升至HOME

......

}

if (app == mPreviousProcess && app.activities.size() > 0) {

// 上一个前台进程,Adj升至PREVIOUS

......

}

......

if (mBackupTarget != null && app == mBackupTarget.app) {

// 进程正在执行备份时,应该避免被啥掉,Adj升至BACKUP

......

}

......

// 处理service的Adj

for (int is = app.services.size()-1;

is >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ

|| schedGroup == ProcessList.SCHED_GROUP_BACKGROUND

|| procState > ActivityManager.PROCESS_STATE_TOP);

is--) {

ServiceRecord s = app.services.valueAt(is);

if (s.startRequested) {

// 当进程中含有Unbounded Service时

......

}

for (int conni = s.connections.size()-1;

conni >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ

|| schedGroup == ProcessList.SCHED_GROUP_BACKGROUND

|| procState > ActivityManager.PROCESS_STATE_TOP);

conni--) {

// 当进程中含有Bounded Service时

......

}

}

// 处理含有ContentProvider的进程

for (int provi = app.pubProviders.size()-1;

provi >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ

|| schedGroup == ProcessList.SCHED_GROUP_BACKGROUND

|| procState > ActivityManager.PROCESS_STATE_TOP);

provi--) {

ContentProviderRecord cpr = app.pubProviders.valueAt(provi);

......

}

// 如果之前运行ContentProvider存活时间没有超时,Adj升至PREVIOU

if (app.lastProviderTime > 0 &&

(app.lastProviderTime+mConstants.CONTENT_PROVIDER_RETAIN_TIME) > now) {

......

}

// 如果services或providers的客户端处于top状态,进一步处理

if (mayBeTop && procState > ActivityManager.PROCESS_STATE_TOP) {

......

}

// Cache进程,进一步处理

if (procState >= ActivityManager.PROCESS_STATE_CACHED_EMPTY) {

......

}

// 对service进程做特殊处理

if (adj == ProcessList.SERVICE_ADJ) {

......

}

......

return app.curAdj < prevAppAdj;

}

computeOomAdjLocked()的代码量很大,逻辑非常复杂。简单来说就是根据进程的各种状态,来调整Adj、schedGroup、procState等。这里只是简单撸了一下大致的流程,细节没有写,太多了。

applyOomAdj

计算完进程的OOM Adj后,需要通过applyOomAdjLocked()将Adj值设置到Android LowMemoryKiller(LMK)机制中去,具体源码如下。

private final boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now,

long nowElapsed) {

......

if (app.curAdj != app.setAdj) {

// 将curAdj设置到LMK系统中,Adj值最终会写入到线程对应的Proc文件中

ProcessList.setOomAdj(app.pid, app.uid, app.curAdj);

app.setAdj = app.curAdj;

app.verifiedAdj = ProcessList.INVALID_ADJ;

}

if (app.setSchedGroup != app.curSchedGroup) {

int oldSchedGroup = app.setSchedGroup;

app.setSchedGroup = app.curSchedGroup;

if (app.waitingToKill != null && app.curReceivers.isEmpty()

&& app.setSchedGroup == ProcessList.SCHED_GROUP_BACKGROUND) {

// 当进程处于后台等待被杀时,杀掉进程

app.kill(app.waitingToKill, true);

success = false;

} else {

......

try {

// 设置整个进程的Group

setProcessGroup(app.pid, processGroup);

if (app.curSchedGroup == ProcessList.SCHED_GROUP_TOP_APP) {

// 如果进程的Group从非TOP变为TOP时,提高UI线程和Render线程的调度优先级。

// 或者使用RT调度策略,或者在标准调度策略下将优先级设置为-10。

......

} else if (oldSchedGroup == ProcessList.SCHED_GROUP_TOP_APP &&

app.curSchedGroup != ProcessList.SCHED_GROUP_TOP_APP) {

// 如果进程的Group从TOP变为非TOP时,降低UI线程和Render线程的调度优先级。

// 或者将调度策略改为SCHED_OTHER,或者将优先级恢为0。

......

} catch (Exception e) {

......

}

}

// 调整上一次上报的前台activities和ProcessState的状态

......

if (app.setProcState == ActivityManager.PROCESS_STATE_NONEXISTENT

|| ProcessList.procStatesDifferForMem(app.curProcState, app.setProcState)) {

// 关于内存的进程状态发生变化时更新下次收集PSS数据得到时间

......

} else {

// 如果定时时间到了,收集PSS数据

......

}

if (app.setProcState != app.curProcState) {

// 更新进程状态

......

} else if (app.reportedInteraction && (nowElapsed-app.interactionEventTime)

> mConstants.USAGE_STATS_INTERACTION_INTERVAL) {

// 长时间处于交互状态的进程,每天至少上报一次使用状态

maybeUpdateUsageStatsLocked(app, nowElapsed);

}

// 进程activities发生改变时需要发送广播,放入待处理队列。

if (changes != 0) {

......

}

return success;

}

applyOomAdjLocked()不仅仅设置了LMK的Adj值,还完成了调整了TOP进程的调度策略或优先级、收集PSS数据、发送状态改变广播等工作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android OOM(Out of Memory)是一种常见的运行时异常,指的是应用程序内存不足的错误。当应用程序试图使用超过系统分配给它的内存时,就会出现这种异常。这可能是由于应用程序在后台加载大量数据、存储过多的对象或图像,或者由于系统资源管理器分配的内存不足所致。 为了解决Android OOM问题,您可以采取以下几种策略: 1. 优化您的代码以减少内存使用量:使用正确的数据类型,避免创建不必要的对象,限制图像和资源的数量,以及优化后台加载过程等。 2. 回收不再使用的内存:当您的应用程序不再需要使用某些内存时,应该及时回收它们。这可以通过调用垃圾回收器(Garbage Collector)来完成。 3. 避免在主线程上执行耗时操作:如果您的应用程序在主线程上执行耗时操作(如大量数据处理),这可能导致系统资源管理器超载,从而引发OOM异常。应该将这些操作移至后台线程。 4. 使用内存分析工具:内存分析工具可以帮助您识别内存泄漏和无效内存引用等问题,从而避免OOM异常的发生。 5. 配置您的应用程序以适应不同的内存配置:如果您正在开发一个需要大量内存的应用程序,您应该考虑在AndroidManifest.xml文件配置您的应用程序以适应不同的内存配置。例如,您可以设置您的应用程序需要的最低和最高内存限制。 请注意,解决Android OOM问题是一个复杂的过程,需要您仔细分析和优化您的代码。如果您遇到了OOM问题,建议寻求专业的帮助或与开发社区进行讨论。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值