当前位置: 首页 > article >正文

Android Framework AMS(16)进程管理

该系列文章总纲链接:专题总纲目录 Android Framework 总纲


本章关键点总结 & 说明:

说明:本章节主要解读AMS 进程方面的知识。关注思维导图中左上侧部分即可。

我们本章节主要是对Android进程管理相关知识有一个基本的了解。先来了解下Linux的进程管理,基于此,我们再谈Android的进程管理体系。最后了解AMS中2个关键的进程管理API:updateLruProcessLocked 和 updateOomAdjLocked。

1 Linux的进程管理

Linux的进程调度是操作系统核心功能之一,它负责决定哪个进程在何时运行以及运行多长时间。以下是Linux进程调度的基本机制,以及setprioritysched_setscheduler两个方法的详细解读。

1.1 Linux进程调度简要解读

Linux进程调度基本机制解读如下

  •  调度器(Scheduler):Linux使用调度器来决定进程的执行顺序。调度器根据进程的优先级和调度策略来选择下一个要执行的进程。
  • 调度策略(Scheduling Policies):Linux支持多种调度策略,包括SCHED_NORMAL(普通)、SCHED_FIFO(先进先出)、SCHED_RR(时间片轮转)、SCHED_BATCH(批处理)和SCHED_IDLE(空闲)等。
  • 优先级(Priority):每个调度策略都有自己的优先级范围。进程的优先级越低(数值越小),它被调度执行的优先级就越高。
  • 时间片(Time Slice):在SCHED_RRSCHED_NORMAL策略下,进程被分配一个时间片,即它在被抢占前可以运行的时间。
  • 上下文切换(Context Switching):当一个进程的时间片用完或者它主动放弃CPU时,会发生上下文切换,调度器会选择另一个进程来执行。

 1.2 setpriority 系统调用

setpriority系统调用用于设置进程的静态优先级。这个优先级是在进程创建时分配的,并且通常不会改变,除非显式地调用setpriority。API详细解读如下:

int setpriority(int which, int who, int value);
  • hich:指定要设置的优先级类型,可以是PRIO_PROCESS(进程)、PRIO_PGRP(进程组)或PRIO_USER(用户)。
  • who:指定要设置优先级的进程ID、进程组ID或用户ID。
  • value:指定新的优先级值,值越小优先级越高。

接下来我们来看看setpriority具体的使用方法,demo如下:

#include <sys/resource.h>
#include <stdio.h>

int main() {
    // 设置当前进程的nice值,值越小优先级越高
    int result = setpriority(PRIO_PROCESS, getpid(), -10);
    if (result == -1) {
        perror("setpriority");
        return 1;
    }

    printf("Priority set to: %d\n", getpriority(PRIO_PROCESS, getpid()));

    return 0;
}

这个程序将当前进程的nice值设置为-10,这通常会提高进程的优先级。getpriority函数用于获取当前进程的优先级,以验证setpriority是否成功。

1.3 sched_setscheduler 系统调用

sched_setscheduler系统调用用于设置进程的动态调度策略和相关参数。API详细解读如下:

struct sched_param {
    int sched_priority; /* Process execution priority */
};

int sched_setscheduler(pid_t pid, const struct sched_param *param);
  • pid:指定要设置调度策略的进程ID。如果pid是0,则设置调用进程的调度策略。
  • param:指向sched_param结构的指针,该结构包含了新的调度参数,如优先级。

接下来我们来看看sched_setscheduler具体的使用方法,demo如下:

#include <sched.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    // 定义调度参数结构体
    struct sched_param param;
    param.sched_priority = 1; // 设置优先级为1

    // 设置调度策略为SCHED_FIFO
    int policy = SCHED_FIFO;
    int result = sched_setscheduler(0, policy, &param);
    if (result == -1) {
        perror("sched_setscheduler");
        return 1;
    }

    printf("Scheduler set to: %d\n", policy);
    printf("Priority set to: %d\n", param.sched_priority);

    // 让进程睡眠一段时间,以便观察调度效果
    sleep(10);

    return 0;
}

这个程序将当前进程的调度策略设置为SCHED_FIFO,并设置优先级为1。sched_setscheduler函数的第一个参数是0,表示设置调用进程的调度策略。policy变量指定了新的调度策略,param结构体指定了调度参数。

注意事项解读:

  • 在使用sched_setscheduler时,需要确保进程有适当的权限(通常是root权限),因为改变进程的调度策略可能需要管理员权限。
  • setprioritysched_setscheduler的使用可能会受到操作系统安全策略的限制,特别是在生产环境中。
  • 这些调用的行为可能会因不同的Linux发行版和内核配置而异。

总结:setprioritysched_setscheduler是Linux进程调度中两个重要的系统调用。setpriority用于设置进程的静态优先级,而sched_setscheduler用于设置进程的动态调度策略和优先级。这两个调用共同决定了进程的调度行为,允许操作系统和应用程序精细控制进程的执行顺序和优先级。通过合理配置这些参数,可以优化系统性能,确保关键任务的及时响应。

2 Android进程解读

2.1 Android进程分类

Android进程管理和Linux进程管理之间的关系是密切的,因为Android系统是建立在Linux内核之上的。在Android中,进程的分类和管理与Linux中有许多相似之处,但也做了一些特定的优化和扩展以适应移动设备的特点。以下是Android进程分类的核心调用和它们与Linux进程管理的对应关系:

  • 前台进程(Foreground Processes):在Linux中,前台进程通常是指那些与用户直接交互的进程。在Android中,前台进程包括正在运行的Activity和正在执行的BroadcastReceiver。这些进程因为与用户直接交互,所以系统会优先保持它们的运行。
  • 可见进程(Visible Processes):可见进程在Android中是指那些对用户可见但不在前台的进程,例如用户最近查看的Activity。Linux中没有直接对应的概念,但可以通过进程的可见性来进行类比。
  • 服务进程(Service Processes):在Android中,服务进程是指那些运行Service组件的进程。这些服务可能在后台执行任务,但对应用程序的行为至关重要。Linux中,服务通常作为守护进程运行,与Android中的服务进程概念相似。
  • 后台进程(Background Processes):后台进程在Android中是指那些对用户不可见的Activity所在的进程。这些进程可能会被系统根据需要终止以释放内存。在Linux中,后台进程通常是指那些在后台运行的进程,如守护进程。
  • 空进程(Empty Processes):空进程在Android中是指那些不包含任何活跃组件的进程,但可能因为某些原因(如服务绑定)而保留在内存中。Linux中没有直接对应的概念,但可以通过进程的资源占用来进行类比。

在Android中,进程的创建和管理是通过ActivityManagerService来协调的,而在Linux中,进程的创建和管理是通过内核的调度器和系统调用(如fork、exec、exit)来实现的。Android在Linux的基础上增加了一些特定的管理机制,如进程的分类和优先级管理,以及与应用程序组件生命周期相关的进程管理策略。这些机制使得Android能够有效地管理有限的资源,并提供良好的用户体验。

2.2 进程调度相关API

在Android Framework中,Process.java文件提供了多个与进程调度相关的API。以下是一些最关键的API:

public class Process {
    private static final String LOG_TAG = "Process";
    //...
    /**
     * 设置当前线程的优先级。
     * 
     * @param priority 要设置的优先级值,值越小优先级越高。
     * @throws IllegalArgumentException 如果优先级值不在合法范围内。
     * @throws SecurityException 如果当前进程没有权限设置线程优先级。
     */
    public static final native void setThreadPriority(int priority)
            throws IllegalArgumentException, SecurityException;

    /**
     * 设置指定线程的优先级。
     * 
     * @param tid 目标线程的ID。
     * @param priority 要设置的优先级值,值越小优先级越高。
     * @throws IllegalArgumentException 如果优先级值不在合法范围内或线程ID无效。
     * @throws SecurityException 如果当前进程没有权限设置指定线程的优先级。
     */
    public static final native void setThreadPriority(int tid, int priority)
            throws IllegalArgumentException, SecurityException;

    /**
     * 获取指定线程的优先级。
     * 
     * @param tid 目标线程的ID。
     * @return 线程的当前优先级值。
     * @throws IllegalArgumentException 如果线程ID无效。
     */
    public static final native int getThreadPriority(int tid)
            throws IllegalArgumentException;

    /**
     * 将线程分组到一个线程组。
     * 
     * @param tid 目标线程的ID。
     * @param group 线程组的ID。
     * @throws IllegalArgumentException 如果线程ID或线程组ID无效。
     * @throws SecurityException 如果当前进程没有权限更改线程组。
     */
    public static final native void setThreadGroup(int tid, int group)
            throws IllegalArgumentException, SecurityException;

    /**
     * 将进程分组到一个进程组。
     * 
     * @param pid 目标进程的ID。
     * @param group 进程组的ID。
     * @throws IllegalArgumentException 如果进程ID或进程组ID无效。
     * @throws SecurityException 如果当前进程没有权限更改进程组。
     */
    public static final native void setProcessGroup(int pid, int group)
            throws IllegalArgumentException, SecurityException;

    /**
     * 获取进程所在的进程组ID。
     * 
     * @param pid 目标进程的ID。
     * @return 进程所在的进程组ID。
     * @throws IllegalArgumentException 如果进程ID无效。
     */
    public static final native int getProcessGroup(int pid)
            throws IllegalArgumentException;
    //...
}

这些native方法提供了对Android进程和线程调度参数的控制,它们通过JNI(Java Native Interface)与底层的C/C++代码交互,最终调用Linux内核的系统调用来实现具体的调度策略。API解读如下:

  • setThreadPriority(int priority):这个方法用于设置当前线程的优先级。在Android中,线程优先级的范围通常是-20(最高)到19(最低)。
  • setThreadPriority(int tid, int priority):这个方法用于设置指定线程的优先级。tid参数指定了目标线程的ID。
  • getThreadPriority(int tid):这个方法用于获取指定线程的优先级。tid参数指定了目标线程的ID。
  • setThreadGroup(int tid, int group):这个方法用于将线程分组到一个线程组。在Linux中,线程组通常用于调度和资源管理。
  • setProcessGroup(int pid, int group):这个方法用于将进程分组到一个进程组。在Linux中,进程组通常用于调度和资源管理。
  • getProcessGroup(int pid):这个方法用于获取进程所在的进程组ID。pid参数指定了目标进程的ID。

这些方法的使用需要相应的权限,特别是在修改其他进程的调度策略时,通常需要系统权限或者root权限。通过合理使用这些API,开发者可以优化应用的性能和响应速度,或者在特定场景下对系统资源进行更精细的控制。

2.3 Android进程关键变量解读

在Android系统中,OOM_ADJ和LRU是两个关键的进程管理参数,因为它们共同决定了Android系统中进程的内存分配和回收策略,具体如下:

  • 内存管理:OOM_ADJ和LRU帮助系统在内存不足时决定哪些进程可以被牺牲以释放内存资源。这对于保持系统稳定性和响应性至关重要,尤其是在内存资源有限的移动设备上。
  • 进程优先级:通过调整OOM_ADJ值,系统可以为不同类型的进程设置不同的优先级,确保关键任务(如前台应用)获得足够的资源,而后台或不常用的进程则可以被适当地牺牲。
  • 性能优化:合理使用LRU和OOM_ADJ可以提高应用的启动速度和响应性,因为系统可以更快地从缓存中恢复进程,而不是每次都从头开始启动进程。
  • 资源平衡:这两个参数使得系统能够在保证用户体验的同时,合理分配有限的资源,平衡不同应用和进程的需求。

可见,它们直接影响系统的内存管理和进程调度策略,对于优化系统性能和用户体验具有重要意义。

2.3.1 OOM_ADJ的解读

OOM_ADJ(Out-Of-Memory Adjustment):OOM_ADJ值是一个整数,用来表示进程的优先级,特别是在系统内存不足时决定哪些进程可以被杀死以回收内存。OOM_ADJ值的设计目的和设计意义如下:

  • 设计目的:OOM_ADJ值的主要目的是为了在系统内存不足时,能够根据进程的重要性进行合理的内存回收。系统会优先杀死那些对用户体验影响较小的进程,保护前台进程和对用户体验至关重要的后台进程。
  • 设计意义:OOM_ADJ值有助于提高Android设备的用户体验和系统稳定性。它允许系统在面临内存压力时,根据进程的重要性做出合理的决策,从而在保证用户体验的同时,也确保系统的流畅运行。

在Android中,OOM_ADJ值的范围通常是从-1000到1000,其中-1000表示最高优先级(最不容易被杀死),而1000表示最低优先级(最容易被杀死)。OOM_ADJ的值定义在ProcessList.java中,代码实现如下:

final class ProcessList {
    // 应用崩溃间隔的最短时间,用于判断应用是否不稳定,如果是,则停止其服务并拒绝广播。
    static final int MIN_CRASH_INTERVAL = 60 * 1000;

    // 处于不同状态的进程的OOM(Out Of Memory)调整值:

    // 在某些地方使用,我们还不确定具体的值。
    // (通常是要被缓存的值,但我们还不知道缓存范围中的确切值。)
    static final int UNKNOWN_ADJ = 16;

    // 只托管不可见活动的进程,可以在不造成任何干扰的情况下杀死。
    static final int CACHED_APP_MAX_ADJ = 15;
    static final int CACHED_APP_MIN_ADJ = 9;

    // 服务B列表的调整值——这些是旧的、不受欢迎的服务,不像A列表中的服务那样吸引人。
    static final int SERVICE_B_ADJ = 8;

    // 这是用户之前所在的应用进程。这个进程被保持在较高优先级,因为用户经常会切换回之前的应用。
    // 这对于最近任务切换(在两个最近的应用程序之间切换)和正常UI流程(例如,在电子邮件应用中点击一个URI在浏览器中查看,然后按返回键回到电子邮件)都很重要。
    static final int PREVIOUS_APP_ADJ = 7;

    // 这是托管Home应用的进程——我们希望尽量避免杀死它,即使它通常在后台,因为用户与之交互很多。
    static final int HOME_APP_ADJ = 6;

    // 这是托管应用服务的进程——杀死它对用户的影响不大。
    static final int SERVICE_ADJ = 5;

    // 这是一个托管重量级应用的进程。它在后台,但我们希望尽量避免杀死它。值在系统启动时在system/rootdir/init.rc中设置。
    static final int HEAVY_WEIGHT_APP_ADJ = 4;

    // 这是一个当前托管备份操作的进程。杀死它不是完全致命的,但通常是一个坏主意。
    static final int BACKUP_APP_ADJ = 3;

    // 这是一个只托管对用户可感知的组件的进程,我们真的希望避免杀死它们,但它们不是立即可见的。一个例子是后台音乐播放。
    static final int PERCEPTIBLE_APP_ADJ = 2;

    // 这是一个只托管对用户可见的活动的进程,所以我们希望它们不要消失。
    static final int VISIBLE_APP_ADJ = 1;

    // 这是运行当前前台应用的进程。我们真的不想杀死它!
    static final int FOREGROUND_APP_ADJ = 0;

    // 这个进程被系统或持久进程绑定,并表示它很重要。
    static final int PERSISTENT_SERVICE_ADJ = -11;

    // 这是一个系统持久进程,如电话进程。肯定不想杀死它,但如果这样做了,也不是完全致命的。
    static final int PERSISTENT_PROC_ADJ = -12;

    // 系统进程以默认的调整值运行。
    static final int SYSTEM_ADJ = -16;

    // 特殊代码,用于没有被系统管理的本地进程(所以没有被系统分配oom调整值)。
    static final int NATIVE_ADJ = -17;

    //...

    // 这些是我们要提供给OOM killer的各种内存级别。
    // 注意OOM killer只支持6个插槽,所以我们不能为每种可能的进程类型提供不同的值。
    private final int[] mOomAdj = new int[] {
            FOREGROUND_APP_ADJ, VISIBLE_APP_ADJ, PERCEPTIBLE_APP_ADJ,
            BACKUP_APP_ADJ, CACHED_APP_MIN_ADJ, CACHED_APP_MAX_ADJ
    };

    // 这些是低端OOM级别限制。这适用于HVGA或更小屏幕的手机,内存少于512MB。
    // 值以KB为单位。
    private final int[] mOomMinFreeLow = new int[] {
            12288, 18432, 24576,
            36864, 43008, 49152
    };

    // 这些是高端OOM级别限制。这适用于1280x800或更大屏幕,大约有1GB RAM。
    // 值以KB为单位。
    private final int[] mOomMinFreeHigh = new int[] {
            73728, 92160, 110592,
            129024, 147456, 184320
    };
}

这些常量代表了Android系统中不同类型进程的OOM(内存不足时的杀进程)调整值。这些值用于确定进程被系统杀死的优先级,值越小表示优先级越高,越不容易被系统杀死。这些调整值帮助系统在内存不足时决定哪些进程可以被牺牲以释放内存。用于更新oom_adj的方法为updateOomAdjLocked。

2.3.2 LRU相关变量解读

LRU(Least Recently Used):LRU是最近最少使用算法,核心思想是当缓存满时,优先淘汰那些近期最少使用的缓存对象(如果一个数据最近被访问过,那么它在未来被访问的概率更高;相反,如果一个数据长时间未被访问,那么它在未来被访问的概率较低),有效避免了OOM的出现。

lruWeight是一个用于衡量进程在最近最少使用(LRU)列表中排序权重的参数。它通常与进程的最后活动时间或其他活动相关的因素相关联,用于确定进程在LRU列表中的位置。lruWeight值的设计目的和设计意义如下:

  • 设计目的:lruWeight的设计目的是为了在系统内存不足时,根据进程的最近使用情况来决定哪些进程可以被优先杀死以释放内存。它帮助系统维护一个有序的进程列表,确保最近被使用的进程保持在内存中,而长时间未被使用的进程则可以被牺牲。
  • 设计意义:lruWeight的设计意义在于优化内存资源的分配和回收。通过动态调整进程的lruWeight值,系统能够更智能地管理内存,提高内存使用效率。这不仅有助于提升用户体验,比如通过快速恢复最近使用的应用程序,还能在内存紧张时减少系统的卡顿或崩溃。

lruWeight与OOM调整值的关系:lruWeight与进程的OOM调整值(OOM_ADJ)紧密相关,因为它们共同决定了进程在内存不足时的生存概率。一个进程的lruWeight值越高,其OOM调整值可能越低,表示它越容易被系统杀死。

此外,lruWeight还考虑了进程的类型和服务状态,比如活动、服务和内容提供者,这使得系统能够根据进程的实际作用和重要性来调整其在LRU列表中的位置,进一步优化内存管理。

lruWeight是Android系统中一个关键的内存管理参数,它通过反映进程的活跃度和重要性来帮助系统在有限的资源下做出合理的内存分配和回收决策。在Android中,LRU算法常用于内存缓存和硬盘缓存的管理。用于更新LRU的方法为updateLruProcessLocked。

3 AMS 进程管理相关函数分析

基于OOM_ADJ和LRU是两个关键的进程管理参数,这里我们主要探讨更新OOM_ADJ和LRU的方法。这里以startProcessLocked方法启动一个新进程为例,启动相关的代码可参考文章:

Android Framework AMS(04)startActivity分析-1(am启动到ActivityThread启动)

Android Framework AMS(05)startActivity分析-2(ActivityThread启动到Activity拉起)

这里我们主要从AMS的attachApplicationLocked方法入手进行分析。代码实现如下:

//AMS
    private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {
        ProcessRecord app;
        //...
        try {
            //...
            // 将应用程序线程与应用程序信息绑定
            thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,
                    profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,
                    app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace,
                    isRestrictedBackupMode || !normalMode, app.persistent,
                    new Configuration(mConfiguration), app.compat,
                    getCommonServicesLocked(app.isolated),
                    mCoreSettingsObserver.getCoreSettingsLocked());

            // 更新进程的LRU列表
            updateLruProcessLocked(app, false, null);
            //...
        } catch (Exception e) {
            //...
        }

        // 如果没有执行任何操作,则更新OOM调整值
        if (!didSomething) {
            updateOomAdjLocked();
        }

        return true;
    }

这里开始就已经调用了2个重要函数updateLruProcessLocked和updateOomAdjLocked。接下来对这2个方法进行更详细的解读。

3.1 updateLruProcessLocked分析

updateLruProcessLocked的代码实现,如下所示:

//AMS
    final void updateLruProcessLocked(ProcessRecord app, boolean activityChange,
            ProcessRecord client) {
        // 检查进程是否有activity
        final boolean hasActivity = app.activities.size() > 0 || app.hasClientActivities
                || app.treatLikeActivity;
        // 检查进程是否有服务
        final boolean hasService = false;

        // 如果没有activity变化且进程有activity,则直接返回
        if (!activityChange && hasActivity) {
            return;
        }

        // 更新LRU序列号
        mLruSeq++;
        final long now = SystemClock.uptimeMillis(); // 获取当前时间
        app.lastActivityTime = now; // 更新进程的最后activity时间

        // 如果进程有activity,检查它是否已经在LRU列表的最后
        if (hasActivity) {
            final int N = mLruProcesses.size();
            if (N > 0 && mLruProcesses.get(N - 1) == app) {
                return; // 如果进程已经是最后一个,直接返回
            }
        } else {
            // 如果进程没有activity,检查它是否在服务启动列表中
            if (mLruProcessServiceStart > 0
                    && mLruProcesses.get(mLruProcessServiceStart - 1) == app) {
                return; // 如果进程在服务启动列表中,直接返回
            }
        }

        // 查找进程在LRU列表中的索引
        int lrui = mLruProcesses.lastIndexOf(app);
        //...
        // 如果进程在LRU列表中,移除它
        if (lrui >= 0) {
            if (lrui < mLruProcessActivityStart) {
                mLruProcessActivityStart--; // 更新activity进程起始索引
            }
            if (lrui < mLruProcessServiceStart) {
                mLruProcessServiceStart--; // 更新服务进程起始索引
            }
            mLruProcesses.remove(lrui); // 从LRU列表中移除进程
        }

        int nextIndex;
        // 如果进程有activity,添加到LRU列表
        if (hasActivity) {
            final int N = mLruProcesses.size();
            if (app.activities.size() == 0 && mLruProcessActivityStart < (N - 1)) {
                mLruProcesses.add(N - 1, app); // 将进程添加到LRU列表的倒数第二个位置
                final int uid = app.info.uid;
                for (int i = N - 2; i > mLruProcessActivityStart; i--) {
                    ProcessRecord subProc = mLruProcesses.get(i);
                    if (subProc.info.uid == uid) {
                        if (mLruProcesses.get(i - 1).info.uid != uid) {
                            // 交换进程位置
                            ProcessRecord tmp = mLruProcesses.get(i);
                            mLruProcesses.set(i, mLruProcesses.get(i - 1));
                            mLruProcesses.set(i - 1, tmp);
                            i--;
                        }
                    } else {
                        break; // 如果UID不同,停止交换
                    }
                }
            } else {
                mLruProcesses.add(app); // 直接添加进程到LRU列表
            }
            nextIndex = mLruProcessServiceStart; // 设置下一个索引
        } else if (hasService) {
            // 如果进程有service,添加到LRU列表的activity起始位置
            mLruProcesses.add(mLruProcessActivityStart, app);
            nextIndex = mLruProcessServiceStart;
            mLruProcessActivityStart++;
        } else {
            // 处理没有activity和service的进程
            int index = mLruProcessServiceStart;
            if (client != null) {
                int clientIndex = mLruProcesses.lastIndexOf(client);
                if (clientIndex <= lrui) {
                    clientIndex = lrui; // 确保clientIndex不小于lrui
                }
                if (clientIndex >= 0 && index > clientIndex) {
                    index = clientIndex; // 更新索引
                }
            }
            mLruProcesses.add(index, app); // 在指定索引添加进程
            nextIndex = index - 1; // 更新下一个索引
            mLruProcessActivityStart++;
            mLruProcessServiceStart++;
        }

        // 更新与进程连接的服务的LRU状态
        for (int j = app.connections.size() - 1; j >= 0; j--) {
            ConnectionRecord cr = app.connections.valueAt(j);
            if (cr.binding != null && !cr.serviceDead && cr.binding.service != null
                    && cr.binding.service.app != null
                    && cr.binding.service.app.lruSeq != mLruSeq
                    && !cr.binding.service.app.persistent) {
                nextIndex = updateLruProcessInternalLocked(cr.binding.service.app, now, nextIndex,
                        "service connection", cr, app);
            }
        }
        // 更新与进程相关的内容提供者的LRU状态
        for (int j = app.conProviders.size() - 1; j >= 0; j--) {
            ContentProviderRecord cpr = app.conProviders.get(j).provider;
            if (cpr.proc != null && cpr.proc.lruSeq != mLruSeq && !cpr.proc.persistent) {
                nextIndex = updateLruProcessInternalLocked(cpr.proc, now, nextIndex,
                        "provider reference", cpr, app);
            }
        }
    }   

updateLruProcessLocked方法的主要目的是更新特定进程在LRU列表中的位置,基于进程的活动状态(如是否有活动或服务)。如果进程有活动(Activity),则不会移动该进程在LRU列表中的位置,因为活动进程应该被优先保留。如果进程没有活动,但有服务连接或内容提供者引用,该方法会调整这些进程在LRU列表中的位置,以反映它们的使用情况。通过mLruSeq序列号控制更新,确保只有在进程状态发生变化时才进行更新,减少不必要的列表操作。

在updateLruProcessLocked方法中,调整的具体是mLruProcesses这个关键变量。mLruProcesses是一个列表,它存储了系统中所有进程的最近最少使用(LRU)顺序。以下是对这个方法中涉及的关键变量和操作的分析:

  • mLruProcesses:这是一个列表,包含了系统中所有进程的ProcessRecord对象,按照它们的最近使用情况排序。列表的前端(即索引较小的位置)存放最近使用的进程,而列表的后端(即索引较大的位置)存放最少使用的进程。
  • app.lastActivityTime:每个ProcessRecord对象中的lastActivityTime字段被更新为当前时间,这表示进程最后一次活跃的时间。这个字段用于确定进程在LRU列表中的位置。
  • mLruProcessActivityStart和mLruProcessServiceStart:这两个变量分别标记了LRU列表中活动进程和服务进程的起始索引。它们用于区分不同类型的进程,并在添加进程到LRU列表时确定正确的位置。
  • mLruSeq:mLruSeq是一个序列号,每次调用updateLruProcessLocked方法时增加,用于标识LRU列表的更新周期。

在方法中执行的操作包括:

  • 更新进程的最后活动时间:通过设置app.lastActivityTime为当前时间来更新。
  • 检查进程是否有活动:如果进程有活动(hasActivity),并且没有活动变化(!activityChange),则不进行进一步操作。
  • 移除和添加进程:如果进程在LRU列表中的位置发生变化,它会从当前位置移除,并根据其活动状态被重新添加到LRU列表的适当位置。
  • 更新服务和内容提供者的LRU状态:对于与进程连接的服务和内容提供者,它们在LRU列表中的位置也可能需要更新,这是通过调用updateLruProcessInternalLocked方法实现的。

总结来说,updateLruProcessLocked方法通过直接操作mLruProcesses列表和相关索引变量来管理进程的LRU顺序,确保列表反映了进程的最新使用情况。这种方法有助于系统在内存不足时决定哪些进程可以被优先杀死。

这里最后在处理service和ContentProcider时调用了updateLruProcessInternalLocked方法,它的代码实现如下所示:

//AMS
    private int updateLruProcessInternalLocked(ProcessRecord app, long now, int index,
            String what, Object obj, ProcessRecord srcApp) {
        // 更新进程的最后活动时间
        app.lastActivityTime = now;

        // 如果进程有活动,不更新其在LRU列表中的位置
        if (app.activities.size() > 0) {
            return index; // 不处理包含活动的进程
        }

        // 查找进程在LRU列表中的索引
        int lrui = mLruProcesses.lastIndexOf(app);

        // 如果进程不在LRU列表中,记录错误并返回
        if (lrui < 0) {
            return index;
        }

        // 如果进程在LRU列表中的位置已经小于或等于目标索引,不更新位置
        if (lrui >= index) {
            return index; // 不使依赖进程在列表中向后移动
        }

        // 如果进程在活动进程的起始索引之后,不更新其位置
        if (lrui >= mLruProcessActivityStart) {
            return index; // 不处理包含活动的进程
        }

        // 从当前位置移除进程
        mLruProcesses.remove(lrui);

        // 如果目标索引大于0,将其减1
        if (index > 0) {
            index--;
        }

        // 在目标索引位置添加进程
        mLruProcesses.add(index, app);

        // 返回更新后的目标索引
        return index;
    }

updateLruProcessInternalLocked方法用于在LRU列表中移动依赖进程(即那些没有直接用户交互,但被其他进程依赖的进程)。如果依赖进程没有活动,并且不在活动进程的起始索引之后,该方法会将这些进程向前移动,以确保它们不会被错误地视为最近最少使用的进程。该方法确保依赖进程不会移动到活动进程之后,从而避免降低它们的优先级。如果依赖进程不在LRU列表中,该方法会记录错误并保持列表不变。

总体来说,updateLruProcessLocked负责处理进程的直接状态变化,而updateLruProcessInternalLocked负责处理由这些变化引起的依赖进程的间接影响。通过精确控制进程在LRU列表中的位置,这两个方法帮助系统优化内存管理,确保在内存不足时能够优先杀死那些最近最少使用的进程,同时保护前台进程和关键后台进程。

3.2 updateOomAdjLocked分析

updateOomAdjLocked的代码较长,这里分成2个部分进行解读。

3.2.1 updateOomAdjLocked前半段代码解读

updateOomAdjLocked前半段代码实现如下所示:

final void updateOomAdjLocked() {
        // 获取当前前台活动的Activity和对应的进程记录
        final ActivityRecord TOP_ACT = resumedAppLocked();
        final ProcessRecord TOP_APP = TOP_ACT != null ? TOP_ACT.app : null;
        // 获取当前时间,用于计算时间差
        final long now = SystemClock.uptimeMillis();
        // 计算30分钟前的时间,用于判断进程是否为空
        final long oldTime = now - ProcessList.MAX_EMPTY_TIME;
        // 获取LRU列表中的进程数量
        final int N = mLruProcesses.size();

        // 增加OOM调整值的序列号,用于标识调整周期
        mAdjSeq++;
        // 初始化服务进程的数量
        mNewNumServiceProcs = 0;
        mNewNumAServiceProcs = 0;

        // 根据系统配置计算空进程和缓存进程的最大数量
        final int emptyProcessLimit;
        final int cachedProcessLimit;
        if (mProcessLimit <= 0) {
            emptyProcessLimit = cachedProcessLimit = 0;
        } else if (mProcessLimit == 1) {
            emptyProcessLimit = 1;
            cachedProcessLimit = 0;
        } else {
            emptyProcessLimit = ProcessList.computeEmptyProcessLimit(mProcessLimit);
            cachedProcessLimit = mProcessLimit - emptyProcessLimit;
        }
        // 计算缓存进程的调整值步长
        int numSlots = (ProcessList.CACHED_APP_MAX_ADJ
                - ProcessList.CACHED_APP_MIN_ADJ + 1) / 2;
        // 计算空进程的数量
        int numEmptyProcs = N - mNumNonCachedProcs - mNumCachedHiddenProcs;
        if (numEmptyProcs > cachedProcessLimit) {
            numEmptyProcs = cachedProcessLimit;
        }
        // 计算空进程和缓存进程的调整值步长因子
        int emptyFactor = numEmptyProcs / numSlots;
        if (emptyFactor < 1) emptyFactor = 1;
        int cachedFactor = (mNumCachedHiddenProcs > 0 ? mNumCachedHiddenProcs : 1) / numSlots;
        if (cachedFactor < 1) cachedFactor = 1;
        // 初始化步长计数器
        int stepCached = 0;
        int stepEmpty = 0;
        // 初始化缓存和空进程的数量计数器
        int numCached = 0;
        int numEmpty = 0;
        int numTrimming = 0;

        // 初始化非缓存进程和缓存隐藏进程的数量计数器
        mNumNonCachedProcs = 0;
        mNumCachedHiddenProcs = 0;

        // 初始化缓存和空进程的OOM调整值
        int curCachedAdj = ProcessList.CACHED_APP_MIN_ADJ;
        int nextCachedAdj = curCachedAdj + 1;
        int curEmptyAdj = ProcessList.CACHED_APP_MIN_ADJ;
        int nextEmptyAdj = curEmptyAdj + 2;
        // 遍历LRU列表,更新每个进程的OOM调整值
        for (int i = N - 1; i >= 0; i--) {
            ProcessRecord app = mLruProcesses.get(i);
            // 如果进程没有被杀死并且有线程,更新其OOM调整值
            if (!app.killedByAm && app.thread != null) {
                app.procStateChanged = false;
                // 计算进程的OOM调整值
                computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
                if (app.curAdj >= ProcessList.UNKNOWN_ADJ) {
                    // 根据进程状态调整OOM值
                    switch (app.curProcState) {
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                        case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                            app.curRawAdj = curCachedAdj;
                            app.curAdj = app.modifyRawOomAdj(curCachedAdj);
                            if (curCachedAdj != nextCachedAdj) {
                                stepCached++;
                                if (stepCached >= cachedFactor) {
                                    stepCached = 0;
                                    curCachedAdj = nextCachedAdj;
                                    nextCachedAdj += 2;
                                    if (nextCachedAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextCachedAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                        default:
                            app.curRawAdj = curEmptyAdj;
                            app.curAdj = app.modifyRawOomAdj(curEmptyAdj);
                            if (curEmptyAdj != nextEmptyAdj) {
                                stepEmpty++;
                                if (stepEmpty >= emptyFactor) {
                                    stepEmpty = 0;
                                    curEmptyAdj = nextEmptyAdj;
                                    nextEmptyAdj += 2;
                                    if (nextEmptyAdj > ProcessList.CACHED_APP_MAX_ADJ) {
                                        nextEmptyAdj = ProcessList.CACHED_APP_MAX_ADJ;
                                    }
                                }
                            }
                            break;
                    }
                }

                // 应用OOM调整值
                applyOomAdjLocked(app, TOP_APP, true, now);

                // 根据进程类型计数
                switch (app.curProcState) {
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                        mNumCachedHiddenProcs++;
                        numCached++;
                        if (numCached > cachedProcessLimit) {
                            app.kill("cached #" + numCached, true);
                        }
                        break;
                    case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
                        if (numEmpty > ProcessList.TRIM_EMPTY_APPS
                                && app.lastActivityTime < oldTime) {
                            app.kill("empty for " + ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime) / 1000) + "s", true);
                        } else {
                            numEmpty++;
                            if (numEmpty > emptyProcessLimit) {
                                app.kill("empty #" + numEmpty, true);
                            }
                        }
                        break;
                    default:
                        mNumNonCachedProcs++;
                        break;
                }

                // 杀死孤立的进程
                if (app.isolated && app.services.size() <= 0) {
                    app.kill("isolated not needed", true);
                }

                // 计算需要调整的进程数量
                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                    numTrimming++;
                }
            }
        }
        //...
    }

updateOomAdjLocked方法的前半部分主要负责初始化OOM调整值的计算环境,包括确定进程的OOM调整值序列、计算空进程和缓存进程的限制数量、以及遍历LRU列表更新每个进程的OOM调整值。这一部分还涉及到根据进程的活动状态(如前台活动、服务状态等)来调整其在LRU列表中的位置,并决定是否需要杀死超出内存限制的进程。

同时我们这里关注两个关键方法:computeOomAdjLocked和applyOomAdjLocked:

@1 computeOomAdjLocked方法

computeOomAdjLocked方法用于计算进程的OOM调整值,在android的各个版本中该方法差异较大,但核心功能和设计目的不变,核心功能和设计目的解读如下:

  • 动态调整OOM值computeOomAdjLocked方法根据进程的活动状态(如前台活动、服务状态等)来计算其OOM值。这个值决定了进程在系统内存不足时被回收的可能性,值越高表示进程越不可能被杀死。
  • 进程优先级管理:方法通过评估进程的重要性来调整其优先级,确保在内存紧张时,前台进程和关键后台进程能够获得更多的保护,而后台或不活跃的进程则更容易被牺牲以释放内存。
  • 响应系统状态变化:当Android四大组件(Activity、Service、Broadcast Receiver、Content Provider)的状态发生变化时,该方法会被调用来更新相应进程的OOM值,以反映这些变化对进程优先级的影响。
  • 优化内存使用:设计目的是在保证用户体验的同时,合理利用系统资源。通过对进程OOM值的动态管理,系统能够在内存紧张时优先保留用户最可能需要的进程,同时释放那些对用户体验影响较小的进程所占用的内存。
  • 系统稳定性和响应性:通过合理分配内存资源,computeOomAdjLocked方法有助于提高系统的稳定性和响应性。它确保了在内存不足的情况下,系统能够做出合理的决策,避免因内存不足导致的系统崩溃或应用异常。

computeOomAdjLocked方法是Android系统中一个重要的内存管理机制,它通过动态调整进程的OOM值来优化内存使用,保护关键进程,并在内存紧张时做出合理的内存回收决策。

@2 applyOomAdjLocked方法

applyOomAdjLocked方法主要根据进程的状态和重要性,动态调整其OOM值,以优化系统内存管理。applyOomAdjLocked方法的核心功能和设计目的:

  • 其核心功能是将之前通过computeOomAdjLocked方法计算得出的OOM值应用到各个进程上,以此来调整进程的内存优先级。
  • 设计目的在于确保在系统内存紧张时,能够根据进程的重要性和当前状态,合理地决定哪些进程应该被优先保留,哪些可以被牺牲以释放内存资源。

通过这种方式,系统能够在保证用户体验的同时,有效管理内存资源,避免因内存不足导致的系统不稳定或崩溃。

3.2.2 updateOomAdjLocked后半段代码解读

updateOomAdjLocked后半段代码实现如下所示:

    final void updateOomAdjLocked() {
        //...

        // 更新服务进程数量
        mNumServiceProcs = mNewNumServiceProcs;
        // 计算缓存和空进程的总数
        final int numCachedAndEmpty = numCached + numEmpty;
        // 根据缓存和空进程的数量确定内存因子,用于调整进程的内存使用
        int memFactor;
        if (numCached <= ProcessList.TRIM_CACHED_APPS
                && numEmpty <= ProcessList.TRIM_EMPTY_APPS) {
            if (numCachedAndEmpty <= ProcessList.TRIM_CRITICAL_THRESHOLD) {
                memFactor = ProcessStats.ADJ_MEM_FACTOR_CRITICAL;
            } else if (numCachedAndEmpty <= ProcessList.TRIM_LOW_THRESHOLD) {
                memFactor = ProcessStats.ADJ_MEM_FACTOR_LOW;
            } else {
                memFactor = ProcessStats.ADJ_MEM_FACTOR_MODERATE;
            }
        } else {
            memFactor = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
        }
        // 如果新的内存因子大于上次的内存因子,且不允许降低内存级别或进程数量没有变化,则保持上次的内存因子
        if (memFactor > mLastMemoryLevel) {
            if (!mAllowLowerMemLevel || mLruProcesses.size() >= mLastNumProcesses) {
                memFactor = mLastMemoryLevel;
            }
        }
        // 更新上次的内存因子和进程数量
        mLastMemoryLevel = memFactor;
        mLastNumProcesses = mLruProcesses.size();
        // 根据内存因子更新进程的状态
        boolean allChanged = mProcessStats.setMemFactorLocked(memFactor, !isSleeping(), now);
        final int trackerMemFactor = mProcessStats.getMemFactorLocked();
        // 如果内存因子不是正常级别,则根据内存状况对进程进行内存修剪
        if (memFactor != ProcessStats.ADJ_MEM_FACTOR_NORMAL) {
            // 记录低内存开始时间
            if (mLowRamStartTime == 0) {
                mLowRamStartTime = now;
            }
            // 根据内存因子确定修剪级别
            int fgTrimLevel;
            switch (memFactor) {
                case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
                    break;
                case ProcessStats.ADJ_MEM_FACTOR_LOW:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
                    break;
                default:
                    fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
                    break;
            }
            // 计算修剪步长
            int factor = numTrimming / 3;
            int minFactor = 2;
            if (mHomeProcess != null) minFactor++;
            if (mPreviousProcess != null) minFactor++;
            if (factor < minFactor) factor = minFactor;
            // 遍历所有进程,根据修剪级别进行内存修剪
            int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
            for (int i = N - 1; i >= 0; i--) {
                ProcessRecord app = mLruProcesses.get(i);
                if (allChanged || app.procStateChanged) {
                    setProcessTrackerStateLocked(app, trackerMemFactor, now);
                    app.procStateChanged = false;
                }
                // 对于HOME和PREVIOUS进程,以及需要修剪的进程,进行内存修剪
                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                    if (app.trimMemoryLevel < curLevel && app.thread != null) {
                        try {
                            app.thread.scheduleTrimMemory(curLevel);
                        } catch (RemoteException e) {
                        }
                    }
                    app.trimMemoryLevel = curLevel;
                    // 更新修剪级别
                    step++;
                    if (step >= factor) {
                        step = 0;
                        switch (curLevel) {
                            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_MODERATE;
                                break;
                            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                                curLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
                                break;
                        }
                    }
                } else if (app.curProcState == ActivityManager.PROCESS_STATE_HEAVY_WEIGHT) {
                    // 对于重量级进程,进行背景内存修剪
                    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 {
                    // 对于其他进程,根据需要进行内存修剪
                    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;
                    }
                    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的进程,进行UI隐藏内存修剪
                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;
            }
        }

        // 如果设置了总是结束活动的选项,则安排销毁所有活动
        if (mAlwaysFinishActivities) {
            mStackSupervisor.scheduleDestroyAllActivities(null, "always-finish");
        }

        // 如果所有进程的状态都发生了变化,则请求所有进程的PSS(Proportional Set Size)数据
        if (allChanged) {
            requestPssAllProcsLocked(now, false, mProcessStats.isMemFactorLowered());
        }

        // 如果进程统计数据应该被写入,则异步写入
        if (mProcessStats.shouldWriteNowLocked(now)) {
            mHandler.post(new Runnable() {
                @Override public void run() {
                    synchronized (ActivityManagerService.this) {
                        mProcessStats.writeStateAsyncLocked();
                    }
                }
            });
        }
    }

后半部分则专注于根据当前的内存使用情况来调整进程的内存使用级别,即内存因子(memFactor)。它通过计算得出当前的内存因子,并根据该因子对进程进行内存修剪,以释放内存资源。这一部分还涉及到更新进程的内存修剪级别,并在必要时触发进程的内存修剪操作,以响应系统的内存压力。

updateOomAdjLocked方法中,根据前半段和后半段的代码,针对oom_adj关键变量的调整主要集中在以下几个方面,解读如下:

  • OOM调整值(curAdj:每个ProcessRecord对象中的curAdj字段被调整,这个字段代表了进程的当前OOM调整值,它直接影响到进程在内存不足时被杀死的优先级。
  • 进程状态(curProcStateProcessRecord中的curProcState字段被更新,反映了进程的当前状态,如前台、后台、服务等,这有助于确定进程的OOM调整值。
  • 内存因子(memFactormemFactor变量根据系统的内存状况和进程的数量被计算出来,用于决定整体的内存调整策略。
  • 服务进程计数(mNewNumServiceProcsmNumServiceProcsmNewNumServiceProcs用于临时计数服务进程的数量,而mNumServiceProcs是最终的服务进程数量,这些计数用于内存调整决策。
  • 非缓存进程和缓存隐藏进程计数(mNumNonCachedProcsmNumCachedHiddenProcs:这些变量用于计数非缓存进程和缓存隐藏进程的数量,它们在计算OOM调整值时起到重要作用。
  • 进程跟踪器状态(setProcessTrackerStateLocked:该方法被调用来更新进程的内存修剪级别,这影响了进程如何响应系统的内存压力。
  • 内存修剪级别(trimMemoryLeveltrimMemoryLevel字段在ProcessRecord中被设置,指示进程应该采取的内存修剪行动。
  • 低内存开始时间(mLowRamStartTime:记录系统进入低内存状态的时间,用于跟踪和管理低内存期间的内存调整策略。

这些关键变量的调整确保了Android系统能够根据当前的内存状况和进程的活动状态,动态地管理内存资源,优化用户体验,并在内存不足时做出合理的进程杀死决策。通过这种方式,系统能够在保证前台和关键进程的稳定性的同时,合理回收后台进程的内存资源。

updateOomAdjLocked方法整体上是Android系统中一个关键的内存管理机制,它确保了在内存资源有限的情况下,系统能够根据进程的重要性和活动状态动态调整其OOM调整值和内存使用级别。通过这种方法,系统能够在内存不足时优先保护前台和关键进程,同时合理地回收后台进程的内存资源,从而优化整体的系统性能和用户体验。这个方法体现了Android系统在面对不同内存压力时的响应策略,包括进程的OOM调整、内存修剪和进程状态的更新,这些都是为了在保证系统稳定性的同时,尽可能地提高内存使用效率。


http://www.kler.cn/a/393396.html

相关文章:

  • 在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
  • 拉普拉斯分布极大似然估计
  • 本原多项式
  • MyBatis如何处理延迟加载?
  • 汽车免拆诊断案例 | 2011 款奔驰 S400L HYBRID 车发动机故障灯异常点亮
  • 选择屏幕的用法
  • Qt第三课 ----------布局
  • 国内AI工具复现GPTs效果详解
  • vue文本高亮处理
  • 【Git】如何在 Git 项目中引用另一个 Git 项目:子模块与子树合并
  • 学习threejs,导入STL格式的模型
  • 【Linux】ELF可执行程序和动态库加载
  • CSS高级技巧_精灵图_字体图标_CSS三角_vertical-align(图像和文字居中在同一行)_溢出文字省略号显示
  • 随机森林(Random Forest)算法Python代码实现
  • 数据量大Excel卡顿严重?选对报表工具提高10倍效率
  • 同三维T85HU HDMI+USB摄像机多路多机位手机直播采集卡
  • 浅析pytorch中的常见函数和方法
  • 128.WEB渗透测试-信息收集-ARL(19)
  • DDE(深度桌面环境) Qt 6.8 适配说明
  • 嵌入式开发套件(golang版本)
  • 昇思大模型平台打卡体验活动:项目6基于MindSpore通过GPT实现情感分类
  • 力扣662:二叉树的最大宽度
  • Java面向对象编程进阶之包装类
  • Python---re模块(正则表达式)
  • 快递鸟快递查询API接口参数代码
  • 字符设备 - The most important !