Android Glide 框架线程管理模块原理的源码级别深入分析

一、引言

在现代的 Android 应用开发中,图片加载是一个常见且重要的功能。Glide 作为一款广泛使用的图片加载框架,以其高效、灵活和易用的特点受到了开发者的青睐。其中,线程管理模块是 Glide 框架中至关重要的一部分,它负责协调不同线程之间的工作,确保图片的加载、解码、处理等操作能够高效、有序地进行。合理的线程管理可以提高应用的性能,避免主线程阻塞,从而为用户提供流畅的交互体验。

本文将深入 Glide 框架的源码,详细剖析其线程管理模块的原理。从线程池的创建和配置,到不同任务在各个线程之间的调度和执行,每一个步骤都会结合具体的代码进行分析。同时,还会探讨线程管理模块与 Glide 其他模块之间的协作关系,以及如何在实际开发中合理利用线程管理来优化图片加载性能。

二、线程管理模块概述

Glide 的线程管理模块主要负责以下几个方面的工作:

  1. 线程池的创建和管理:Glide 使用多个线程池来处理不同类型的任务,如网络请求、磁盘缓存读写、图片解码等。通过合理配置线程池的参数,可以提高任务的执行效率。
  2. 任务的调度和执行:根据任务的类型和优先级,将任务分配到合适的线程池中执行。同时,处理任务的排队和并发控制,确保系统资源的合理利用。
  3. 线程间的通信和同步:在不同线程之间传递数据和状态信息,保证各个模块之间的协作和数据的一致性。例如,在图片加载完成后,将结果从子线程传递到主线程进行显示。

三、线程池的创建和配置

3.1 线程池的种类

Glide 中主要使用了以下几种线程池:

  1. DiskCacheService:用于处理磁盘缓存的读写操作。磁盘 I/O 操作通常比较耗时,使用单独的线程池可以避免阻塞其他任务。
  2. SourceService:用于处理网络请求和图片解码等操作。这些操作可能会消耗大量的 CPU 和网络资源,使用专门的线程池可以提高处理效率。
  3. AnimationExecutor:用于处理动画相关的任务。动画需要在主线程或特定的线程中执行,以保证动画的流畅性。

3.2 线程池的创建和配置源码分析

3.2.1 DiskCacheService 线程池

DiskCacheService 是一个单线程的线程池,用于处理磁盘缓存的读写操作。以下是其创建和配置的源码:

java

java 复制代码
// GlideExecutor 类中创建 DiskCacheService 线程池的方法
private static GlideExecutor newDiskCacheExecutor() {
    // 线程池的核心线程数为 1,即单线程
    int corePoolSize = 1;
    // 线程池的最大线程数也为 1
    int maximumPoolSize = 1;
    // 线程空闲时的存活时间为 0 毫秒
    long keepAliveTime = 0L;
    // 时间单位为毫秒
    TimeUnit unit = TimeUnit.MILLISECONDS;
    // 使用 LinkedBlockingQueue 作为任务队列
    BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
    // 创建一个线程工厂,用于创建线程
    ThreadFactory threadFactory = new DefaultThreadFactory("disk-cache", Process.THREAD_PRIORITY_BACKGROUND);
    // 创建一个线程池执行器
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            workQueue,
            threadFactory
    );
    // 将线程池执行器包装成 GlideExecutor 对象并返回
    return new GlideExecutor(executor, /*isShutdownAllowed=*/ true);
}

在上述代码中,newDiskCacheExecutor 方法创建了一个单线程的线程池。核心线程数和最大线程数都设置为 1,确保只有一个线程在处理磁盘缓存的读写操作。使用 LinkedBlockingQueue 作为任务队列,保证任务按顺序执行。线程空闲时的存活时间为 0 毫秒,即线程在空闲时会立即终止。最后,将线程池执行器包装成 GlideExecutor 对象返回。

3.2.2 SourceService 线程池

SourceService 是一个多线程的线程池,用于处理网络请求和图片解码等操作。以下是其创建和配置的源码:

java

java 复制代码
// GlideExecutor 类中创建 SourceService 线程池的方法
private static GlideExecutor newSourceExecutor() {
    // 获取可用的处理器核心数
    int availableProcessors = Runtime.getRuntime().availableProcessors();
    // 线程池的核心线程数为可用处理器核心数的一半
    int corePoolSize = Math.max(1, availableProcessors / 2);
    // 线程池的最大线程数为可用处理器核心数
    int maximumPoolSize = availableProcessors;
    // 线程空闲时的存活时间为 60 秒
    long keepAliveTime = 60L;
    // 时间单位为秒
    TimeUnit unit = TimeUnit.SECONDS;
    // 使用 SynchronousQueue 作为任务队列
    BlockingQueue<Runnable> workQueue = new SynchronousQueue<>();
    // 创建一个线程工厂,用于创建线程
    ThreadFactory threadFactory = new DefaultThreadFactory("source", Process.THREAD_PRIORITY_BACKGROUND);
    // 创建一个线程池执行器
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            workQueue,
            threadFactory
    );
    // 将线程池执行器包装成 GlideExecutor 对象并返回
    return new GlideExecutor(executor, /*isShutdownAllowed=*/ true);
}

在上述代码中,newSourceExecutor 方法创建了一个多线程的线程池。核心线程数为可用处理器核心数的一半,最大线程数为可用处理器核心数,这样可以充分利用系统资源。使用 SynchronousQueue 作为任务队列,该队列不存储任务,而是直接将任务交给线程处理,提高了任务的执行效率。线程空闲时的存活时间为 60 秒,当线程空闲超过 60 秒时会被终止。最后,将线程池执行器包装成 GlideExecutor 对象返回。

3.2.3 AnimationExecutor 线程池

AnimationExecutor 用于处理动画相关的任务。以下是其创建和配置的源码:

java

java 复制代码
// GlideExecutor 类中创建 AnimationExecutor 线程池的方法
private static GlideExecutor newAnimationExecutor() {
    // 线程池的核心线程数为 1
    int corePoolSize = 1;
    // 线程池的最大线程数为 1
    int maximumPoolSize = 1;
    // 线程空闲时的存活时间为 0 毫秒
    long keepAliveTime = 0L;
    // 时间单位为毫秒
    TimeUnit unit = TimeUnit.MILLISECONDS;
    // 使用 LinkedBlockingQueue 作为任务队列
    BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
    // 创建一个线程工厂,用于创建线程
    ThreadFactory threadFactory = new DefaultThreadFactory("animation", Process.THREAD_PRIORITY_DISPLAY);
    // 创建一个线程池执行器
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            workQueue,
            threadFactory
    );
    // 将线程池执行器包装成 GlideExecutor 对象并返回
    return new GlideExecutor(executor, /*isShutdownAllowed=*/ true);
}

在上述代码中,newAnimationExecutor 方法创建了一个单线程的线程池。核心线程数和最大线程数都设置为 1,确保动画任务按顺序执行。使用 LinkedBlockingQueue 作为任务队列,保证任务的顺序性。线程空闲时的存活时间为 0 毫秒,即线程在空闲时会立即终止。最后,将线程池执行器包装成 GlideExecutor 对象返回。

3.3 线程池的获取和使用

在 Glide 中,可以通过 GlideExecutor 类的静态方法获取不同的线程池。以下是获取 DiskCacheService 线程池的示例代码:

java

java 复制代码
// 获取 DiskCacheService 线程池
GlideExecutor diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
// 提交一个任务到 DiskCacheService 线程池
diskCacheExecutor.execute(new Runnable() {
    @Override
    public void run() {
        // 执行磁盘缓存的读写操作
        // ...
    }
});

通过调用 GlideExecutor 类的 newDiskCacheExecutor 方法可以获取 DiskCacheService 线程池,然后使用 execute 方法提交一个任务到该线程池。其他线程池的获取和使用方式类似。

四、任务的调度和执行

4.1 任务的分类

在 Glide 中,任务主要分为以下几类:

  1. 磁盘缓存读写任务:如从磁盘缓存中读取图片数据或将图片数据写入磁盘缓存。
  2. 网络请求任务:从网络获取图片数据。
  3. 图片解码任务 :将图片数据解码为 Bitmap 或其他可显示的格式。
  4. 动画任务:处理图片的动画效果。

4.2 任务调度的源码分析

Glide 通过 EngineJob 类来管理任务的调度和执行。以下是 EngineJob 类中任务调度的部分源码:

java

java 复制代码
// EngineJob 类负责管理任务的调度和执行
public class EngineJob<R> implements DecodeJob.Callback<R> {
    private final GlideExecutor diskCacheExecutor; // 磁盘缓存线程池
    private final GlideExecutor sourceExecutor; // 源数据线程池
    private final GlideExecutor animationExecutor; // 动画线程池
    private DecodeJob<R> decodeJob; // 解码任务

    public EngineJob(
            GlideExecutor diskCacheExecutor,
            GlideExecutor sourceExecutor,
            GlideExecutor animationExecutor) {
        this.diskCacheExecutor = diskCacheExecutor;
        this.sourceExecutor = sourceExecutor;
        this.animationExecutor = animationExecutor;
    }

    // 开始执行任务
    public void start(DecodeJob<R> decodeJob) {
        this.decodeJob = decodeJob;
        // 将解码任务提交到磁盘缓存线程池执行
        diskCacheExecutor.execute(decodeJob);
    }

    @Override
    public void onResourceDecoded(Resource<R> resource, DataSource dataSource) {
        // 当资源解码完成后,根据数据源类型选择合适的线程池执行后续任务
        if (dataSource == DataSource.DATA_DISK_CACHE) {
            // 如果数据来自磁盘缓存,将任务提交到源数据线程池执行
            sourceExecutor.execute(new ResourceReadyCallback(resource, dataSource));
        } else {
            // 否则,直接在当前线程执行后续任务
            handleResultOnMainThread(resource, dataSource);
        }
    }

    // 在主线程处理结果
    private void handleResultOnMainThread(Resource<R> resource, DataSource dataSource) {
        // ...
    }

    // 资源准备好的回调任务
    private class ResourceReadyCallback implements Runnable {
        private final Resource<R> resource;
        private final DataSource dataSource;

        public ResourceReadyCallback(Resource<R> resource, DataSource dataSource) {
            this.resource = resource;
            this.dataSource = dataSource;
        }

        @Override
        public void run() {
            // 处理资源准备好的逻辑
            handleResultOnMainThread(resource, dataSource);
        }
    }
}

在上述代码中,EngineJob 类包含了三个线程池:diskCacheExecutorsourceExecutoranimationExecutor。在 start 方法中,将解码任务 decodeJob 提交到 diskCacheExecutor 线程池执行。当资源解码完成后,在 onResourceDecoded 方法中,根据数据源类型选择合适的线程池执行后续任务。如果数据来自磁盘缓存,将任务提交到 sourceExecutor 线程池执行;否则,直接在当前线程执行后续任务。

4.3 任务执行的源码分析

任务的执行主要通过 Runnable 接口实现。以 DecodeJob 类为例,以下是其实现 Runnable 接口的部分源码:

java

java 复制代码
// DecodeJob 类实现了 Runnable 接口,用于执行解码任务
public class DecodeJob<R> implements Runnable {
    private final EngineJob<R> engineJob; // 引擎任务
    private final DataFetcherGenerator generator; // 数据获取生成器

    public DecodeJob(EngineJob<R> engineJob, DataFetcherGenerator generator) {
        this.engineJob = engineJob;
        this.generator = generator;
    }

    @Override
    public void run() {
        try {
            // 执行解码任务
            boolean isResourceDecoded = decodeFromSource();
            if (isResourceDecoded) {
                // 如果资源解码成功,通知引擎任务
                engineJob.onResourceDecoded(resource, dataSource);
            } else {
                // 如果资源解码失败,通知引擎任务
                engineJob.onLoadFailed(new GlideException("Failed to decode resource"));
            }
        } catch (Exception e) {
            // 处理异常情况,通知引擎任务
            engineJob.onLoadFailed(e);
        }
    }

    // 从源数据解码资源
    private boolean decodeFromSource() throws Exception {
        // ...
        return false;
    }
}

在上述代码中,DecodeJob 类实现了 Runnable 接口的 run 方法。在 run 方法中,调用 decodeFromSource 方法执行解码任务。如果资源解码成功,调用 engineJobonResourceDecoded 方法通知引擎任务;如果解码失败,调用 engineJobonLoadFailed 方法通知引擎任务。

五、线程间的通信和同步

5.1 线程间通信的方式

在 Glide 中,线程间的通信主要通过以下几种方式实现:

  1. Handler 机制:用于在子线程和主线程之间传递消息。例如,在图片加载完成后,将结果从子线程传递到主线程进行显示。
  2. 回调接口 :通过定义回调接口,在不同线程之间传递数据和状态信息。例如,在 DecodeJob 完成解码任务后,通过回调接口通知 EngineJob

5.2 Handler 机制的源码分析

Glide 使用 Handler 机制在子线程和主线程之间传递消息。以下是相关的源码分析:

java

java 复制代码
// MainThreadExecutor 类用于在主线程执行任务
public class MainThreadExecutor implements Executor {
    private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper());

    @Override
    public void execute(Runnable command) {
        // 将任务提交到主线程的消息队列中执行
        MAIN_THREAD_HANDLER.post(command);
    }
}

在上述代码中,MainThreadExecutor 类实现了 Executor 接口,用于在主线程执行任务。通过 Handlerpost 方法将任务提交到主线程的消息队列中执行。以下是在 EngineJob 类中使用 MainThreadExecutor 的示例代码:

java

java 复制代码
// EngineJob 类中在主线程处理结果的方法
private void handleResultOnMainThread(Resource<R> resource, DataSource dataSource) {
    // 获取主线程执行器
    MainThreadExecutor mainThreadExecutor = new MainThreadExecutor();
    // 将任务提交到主线程执行
    mainThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            // 在主线程处理资源结果
            // ...
        }
    });
}

handleResultOnMainThread 方法中,创建了一个 MainThreadExecutor 对象,然后使用 execute 方法将任务提交到主线程执行。

5.3 回调接口的源码分析

回调接口是 Glide 中常用的线程间通信方式。以 DecodeJobEngineJob 之间的通信为例,以下是相关的源码分析:

java

java 复制代码
// DecodeJob 类的回调接口
public interface Callback<R> {
    // 资源解码完成的回调方法
    void onResourceDecoded(Resource<R> resource, DataSource dataSource);
    // 加载失败的回调方法
    void onLoadFailed(GlideException e);
}

// DecodeJob 类
public class DecodeJob<R> implements Runnable {
    private final EngineJob<R> engineJob;

    public DecodeJob(EngineJob<R> engineJob) {
        this.engineJob = engineJob;
    }

    @Override
    public void run() {
        try {
            // 执行解码任务
            boolean isResourceDecoded = decodeFromSource();
            if (isResourceDecoded) {
                // 如果资源解码成功,通知引擎任务
                engineJob.onResourceDecoded(resource, dataSource);
            } else {
                // 如果资源解码失败,通知引擎任务
                engineJob.onLoadFailed(new GlideException("Failed to decode resource"));
            }
        } catch (Exception e) {
            // 处理异常情况,通知引擎任务
            engineJob.onLoadFailed(e);
        }
    }
}

// EngineJob 类实现了 DecodeJob 的回调接口
public class EngineJob<R> implements DecodeJob.Callback<R> {
    @Override
    public void onResourceDecoded(Resource<R> resource, DataSource dataSource) {
        // 处理资源解码完成的逻辑
        // ...
    }

    @Override
    public void onLoadFailed(GlideException e) {
        // 处理加载失败的逻辑
        // ...
    }
}

在上述代码中,DecodeJob 类定义了一个回调接口 Callback,包含了 onResourceDecodedonLoadFailed 两个方法。EngineJob 类实现了该回调接口,当 DecodeJob 完成解码任务后,会调用 EngineJob 的相应回调方法,从而实现了线程间的通信。

六、线程管理模块与其他模块的协作

6.1 与缓存模块的协作

线程管理模块与缓存模块密切协作,确保磁盘缓存的读写操作在合适的线程中执行。例如,在 DecodeJob 中,如果需要从磁盘缓存中读取图片数据,会将该任务提交到 DiskCacheService 线程池执行。以下是相关的源码分析:

java

java 复制代码
// DecodeJob 类中从磁盘缓存读取数据的方法
private boolean decodeFromCache() {
    // 创建一个磁盘缓存读取任务
    DiskCacheGenerator generator = new DiskCacheGenerator(
            this,
            diskCache,
            diskCacheService
    );
    // 将任务提交到磁盘缓存线程池执行
    diskCacheService.execute(generator);
    return false;
}

在上述代码中,decodeFromCache 方法创建了一个 DiskCacheGenerator 对象,该对象负责从磁盘缓存中读取数据。然后将该任务提交到 diskCacheService 线程池执行。

6.2 与网络模块的协作

线程管理模块与网络模块协作,确保网络请求任务在合适的线程中执行。例如,在 SourceGenerator 中,如果需要从网络获取图片数据,会将该任务提交到 SourceService 线程池执行。以下是相关的源码分析:

java

java 复制代码
// SourceGenerator 类中从网络获取数据的方法
private boolean startNext() {
    // 创建一个网络数据获取器
    HttpUrlFetcher fetcher = new HttpUrlFetcher(url, timeout);
    // 将任务提交到源数据线程池执行
    sourceExecutor.execute(new Runnable() {
        @Override
        public void run() {
            try {
                // 执行网络请求
                InputStream inputStream = fetcher.loadData(priority, this);
                // 处理网络请求结果
                // ...
            } catch (IOException e) {
                // 处理网络请求异常
                // ...
            }
        }
    });
    return true;
}

在上述代码中,startNext 方法创建了一个 HttpUrlFetcher 对象,该对象负责从网络获取图片数据。然后将该任务提交到 sourceExecutor 线程池执行。

6.3 与动画模块的协作

线程管理模块与动画模块协作,确保动画任务在合适的线程中执行。例如,在 AnimatableDrawable 中,如果需要执行动画效果,会将该任务提交到 AnimationExecutor 线程池执行。以下是相关的源码分析:

java

java 复制代码
// AnimatableDrawable 类中执行动画的方法
public void start() {
    // 创建一个动画任务
    AnimationRunnable animationRunnable = new AnimationRunnable(this);
    // 将任务提交到动画线程池执行
    animationExecutor.execute(animationRunnable);
}

// 动画任务类
private class AnimationRunnable implements Runnable {
    private final AnimatableDrawable drawable;

    public AnimationRunnable(AnimatableDrawable drawable) {
        this.drawable = drawable;
    }

    @Override
    public void run() {
        // 执行动画逻辑
        // ...
    }
}

在上述代码中,start 方法创建了一个 AnimationRunnable 对象,该对象负责执行动画逻辑。然后将该任务提交到 animationExecutor 线程池执行。

七、线程管理模块的性能优化

7.1 合理配置线程池参数

合理配置线程池的参数可以提高任务的执行效率。例如,根据系统的处理器核心数和任务的类型,调整线程池的核心线程数和最大线程数。在 SourceService 线程池中,核心线程数设置为可用处理器核心数的一半,最大线程数设置为可用处理器核心数,这样可以充分利用系统资源。

7.2 任务的优先级管理

Glide 可以通过设置任务的优先级来管理任务的执行顺序。例如,在 Priority 枚举中定义了不同的优先级:

java

java 复制代码
// Priority 枚举定义了任务的优先级
public enum Priority {
    IMMEDIATE, // 立即执行
    HIGH, // 高优先级
    NORMAL, // 正常优先级
    LOW // 低优先级
}

在提交任务时,可以指定任务的优先级,线程池会根据优先级来调度任务。例如:

java

java 复制代码
// 提交一个高优先级的任务到源数据线程池
sourceExecutor.execute(new Runnable() {
    @Override
    public void run() {
        // 执行高优先级任务
        // ...
    }
}, Priority.HIGH);

7.3 避免线程阻塞

在任务执行过程中,要避免线程阻塞,以免影响其他任务的执行。例如,在进行网络请求时,要使用异步方式进行,避免在主线程中执行耗时的网络操作。Glide 在处理网络请求时,会将网络请求任务提交到 SourceService 线程池执行,避免阻塞主线程。

7.4 线程池的复用和关闭

在使用线程池时,要注意线程池的复用和关闭。避免频繁创建和销毁线程池,以减少系统资源的消耗。同时,在应用退出时,要及时关闭线程池,释放资源。例如,在 Glide 类的 shutdown 方法中,会关闭所有的线程池:

java

java 复制代码
// Glide 类的 shutdown 方法
public void shutdown() {
    // 关闭磁盘缓存线程池
    diskCacheExecutor.shutdown();
    // 关闭源数据线程池
    sourceExecutor.shutdown();
    // 关闭动画线程池
    animationExecutor.shutdown();
}

八、线程管理模块的异常处理

8.1 任务执行异常的处理

在任务执行过程中,可能会出现各种异常情况,如网络异常、解码异常等。Glide 在任务执行过程中会捕获这些异常,并进行相应的处理。例如,在 DecodeJob 类的 run 方法中,会捕获异常并通知 EngineJob

java

java 复制代码
// DecodeJob 类的 run 方法
@Override
public void run() {
    try {
        // 执行解码任务
        boolean isResourceDecoded = decodeFromSource();
        if (isResourceDecoded) {
            // 如果资源解码成功,通知引擎任务
            engineJob.onResourceDecoded(resource, dataSource);
        } else {
            // 如果资源解码失败,通知引擎任务
            engineJob.onLoadFailed(new GlideException("Failed to decode resource"));
        }
    } catch (Exception e) {
        // 处理异常情况,通知引擎任务
        engineJob.onLoadFailed(e);
    }
}

在上述代码中,run 方法捕获了所有异常,并调用 engineJobonLoadFailed 方法通知引擎任务。

8.2 线程池异常的处理

线程池在执行任务时也可能会出现异常,如线程池已满、线程创建失败等。Glide 通过设置线程池的拒绝策略来处理这些异常。例如,在 ThreadPoolExecutor 中,可以设置拒绝策略:

java

java 复制代码
// 创建一个线程池执行器,并设置拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        unit,
        workQueue,
        threadFactory,
        new ThreadPoolExecutor.CallerRunsPolicy() // 使用 CallerRunsPolicy 拒绝策略
);

在上述代码中,使用 CallerRunsPolicy 拒绝策略,当线程池已满时,会将任务返回给调用者线程执行。

九、总结

Glide 的线程管理模块是其高效运行的关键之一。通过合理创建和配置线程池,将不同类型的任务分配到合适的线程池中执行,实现了任务的高效调度和执行。同时,利用线程间的通信和同步机制,确保了各个模块之间的协作和数据的一致性。

线程管理模块与 Glide 的其他模块密切协作,如缓存模块、网络模块和动画模块,共同完成图片的加载、解码和显示等任务。在实际开发中,要合理利用线程管理模块的功能,进行性能优化,

相关推荐
云深不知处㊣2 小时前
【社交+陪玩服务】全场景陪玩系统源码 小程序+H5双端 社群互动+即时点单+搭建教程
android·小程序·社交源码·找搭子系统源码·陪玩系统源码
casual_clover2 小时前
Kotlin 中实现静态方法的几种方式
android·kotlin
yzpyzp2 小时前
kotlin的?: 操作符(Elvis操作符)
android·kotlin
buleideli2 小时前
Android项目优化同步速度
android·gradle
tangweiguo030519874 小时前
Android 蓝牙工具类封装:支持经典蓝牙与 BLE,兼容高版本权限
android·gitee
cheese-liang5 小时前
Excel中使用VBA自动生成排班表
android·excel
程序员正茂5 小时前
Unity安卓Android从StreamingAssets加载AssetBundle
android·unity·assetbundle·streamingassets
tmacfrank7 小时前
Compose 实践与探索一 —— 关键知识与概念详解
android·ui·kotlin·android jetpack
尼古拉斯大锅盖8 小时前
Android代码最新快速扫描获取手机内图片、视频、音频、文档等文件
android·kotlin
_祝你今天愉快8 小时前
Android12 系统源码编译及踩坑全攻略
android·源码