Android Picasso 调度模块深度剖析(七)

Android Picasso 调度模块深度剖析

本人掘金号,欢迎点击关注:掘金号地址

本人公众号,欢迎点击关注:公众号地址

一、引言

在 Android 开发中,图片加载是一个常见且关键的功能。Picasso 作为一款广受欢迎的图片加载库,以其简洁的 API 和高效的性能受到开发者的青睐。其中,调度模块在整个 Picasso 框架中起着至关重要的作用,它负责协调图片加载任务的执行顺序、线程管理以及任务的分发等操作,确保图片能够高效、有序地加载和显示。本文将深入分析 Picasso 的调度模块,从源码层面详细解读其工作原理和实现细节。

二、调度模块概述

2.1 模块功能

Picasso 的调度模块主要承担以下几个核心功能:

  • 任务调度:根据任务的优先级和资源情况,合理安排图片加载任务的执行顺序,确保重要任务能够优先执行。
  • 线程管理:管理图片加载任务的执行线程,将耗时的任务分配到合适的线程池中执行,避免阻塞主线程,保证应用的流畅性。
  • 任务分发:将解码完成的图片或加载失败的信息分发给相应的处理逻辑,以便更新 UI 或进行错误处理。

2.2 主要类和接口

在调度模块中,有几个关键的类和接口起着重要作用:

  • Dispatcher:调度模块的核心类,负责任务的调度、线程管理和分发。
  • Hunter :代表一个图片加载任务,实现了 Runnable 接口,可在线程池中执行。
  • PicassoExecutorService:自定义的线程池,用于执行图片加载任务。

三、Dispatcher 类分析

3.1 类的定义和基本属性

java 复制代码
// Dispatcher 类是调度模块的核心,负责任务的调度、线程管理和分发
class Dispatcher {
    // 线程池,用于执行图片加载任务
    private final ExecutorService service;
    // 主线程的 Handler,用于在主线程中处理消息
    private final Handler mainThreadHandler;
    // 用于存储正在执行的任务
    private final Set<BitmapHunter> hunterSet;
    // 用于存储待执行的任务
    private final Deque<BitmapHunter> hunterQueue;
    // 用于存储暂停的任务
    private final Map<Object, Deque<BitmapHunter>> pausedTags;
    // 用于存储重试的任务
    private final Deque<BitmapHunter> retryQueue;
    // 缓存对象,用于存储解码后的图片
    private final Cache cache;
    // 统计信息对象,用于记录图片加载的相关统计信息
    private final Stats stats;
    // 下载器,用于从网络或其他数据源下载图片
    private final Downloader downloader;
    // 网络状态监测器,用于监测网络状态
    private final NetworkPolicy networkPolicy;
    // 重试延迟时间
    private final int retryDelay;

    // 构造函数,初始化各种属性
    Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
               Cache cache, Stats stats, Downloader downloader, NetworkPolicy networkPolicy,
               int retryDelay) {
        this.service = service;
        this.mainThreadHandler = mainThreadHandler;
        this.hunterSet = new LinkedHashSet<>();
        this.hunterQueue = new ArrayDeque<>();
        this.pausedTags = new HashMap<>();
        this.retryQueue = new ArrayDeque<>();
        this.cache = cache;
        this.stats = stats;
        this.downloader = downloader;
        this.networkPolicy = networkPolicy;
        this.retryDelay = retryDelay;
    }
}

Dispatcher 类包含了多个重要的属性,用于管理任务的执行、线程池、缓存、统计信息等。通过构造函数进行初始化,确保各个组件能够正常工作。

3.2 任务调度逻辑

3.2.1 提交任务
java 复制代码
// 提交一个图片加载任务
void dispatchSubmit(BitmapHunter hunter) {
    // 发送消息到主线程的 Handler,调用 performSubmit 方法处理任务
    mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(REQUEST_SUBMIT, hunter));
}

// 在主线程中处理任务提交
private void performSubmit(BitmapHunter hunter) {
    // 如果线程池已关闭,直接返回
    if (service.isShutdown()) {
        return;
    }
    // 将任务添加到正在执行的任务集合中
    hunterSet.add(hunter);
    // 如果线程池有空闲线程,直接执行任务
    if (service.trySubmit(hunter)) {
        // 记录任务开始执行的统计信息
        stats.dispatchSubmit(hunter);
    } else {
        // 否则,将任务添加到待执行的任务队列中
        hunterQueue.offer(hunter);
    }
}

dispatchSubmit 方法用于提交一个图片加载任务,它通过主线程的 Handler 发送消息,调用 performSubmit 方法进行处理。performSubmit 方法会检查线程池的状态,如果线程池有空闲线程,直接执行任务;否则,将任务添加到待执行的任务队列中。

3.2.2 任务完成处理
java 复制代码
// 处理任务完成的消息
void dispatchComplete(BitmapHunter hunter) {
    // 发送消息到主线程的 Handler,调用 performComplete 方法处理任务完成
    mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(REQUEST_COMPLETE, hunter));
}

// 在主线程中处理任务完成
private void performComplete(BitmapHunter hunter) {
    // 从正在执行的任务集合中移除该任务
    hunterSet.remove(hunter);
    // 如果任务有需要回调的监听器,调用相应的回调方法
    if (hunter.getCallback() != null) {
        hunter.getCallback().onSuccess(hunter.getResult());
    }
    // 检查待执行的任务队列,如果有任务,尝试执行
    if (!hunterQueue.isEmpty()) {
        BitmapHunter next = hunterQueue.poll();
        if (service.trySubmit(next)) {
            stats.dispatchSubmit(next);
        } else {
            hunterQueue.offerFirst(next);
        }
    }
}

dispatchComplete 方法用于处理任务完成的消息,它通过主线程的 Handler 发送消息,调用 performComplete 方法进行处理。performComplete 方法会从正在执行的任务集合中移除该任务,如果任务有回调监听器,调用相应的回调方法。同时,会检查待执行的任务队列,如果有任务,尝试执行。

3.2.3 任务失败处理
java 复制代码
// 处理任务失败的消息
void dispatchFailed(BitmapHunter hunter) {
    // 发送消息到主线程的 Handler,调用 performFailed 方法处理任务失败
    mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(REQUEST_FAILED, hunter));
}

// 在主线程中处理任务失败
private void performFailed(BitmapHunter hunter) {
    // 从正在执行的任务集合中移除该任务
    hunterSet.remove(hunter);
    // 如果任务有需要重试的条件,将任务添加到重试队列中
    if (hunter.shouldRetry()) {
        retryQueue.offer(hunter);
        // 延迟一段时间后重试任务
        mainThreadHandler.sendMessageDelayed(
                mainThreadHandler.obtainMessage(REQUEST_RETRY, hunter), retryDelay);
    } else {
        // 如果任务没有重试条件,直接调用失败回调方法
        if (hunter.getCallback() != null) {
            hunter.getCallback().onError(hunter.getException());
        }
    }
    // 检查待执行的任务队列,如果有任务,尝试执行
    if (!hunterQueue.isEmpty()) {
        BitmapHunter next = hunterQueue.poll();
        if (service.trySubmit(next)) {
            stats.dispatchSubmit(next);
        } else {
            hunterQueue.offerFirst(next);
        }
    }
}

dispatchFailed 方法用于处理任务失败的消息,它通过主线程的 Handler 发送消息,调用 performFailed 方法进行处理。performFailed 方法会从正在执行的任务集合中移除该任务,如果任务有重试条件,将任务添加到重试队列中,并延迟一段时间后重试;否则,直接调用失败回调方法。同时,会检查待执行的任务队列,如果有任务,尝试执行。

3.3 线程管理

3.3.1 线程池的创建
java 复制代码
// 创建自定义的线程池
private static ExecutorService createDefaultExecutorService(Context context) {
    // 获取设备的 CPU 核心数
    int cores = Runtime.getRuntime().availableProcessors();
    // 创建一个固定大小的线程池,线程数为 CPU 核心数加 1
    return new PicassoExecutorService(cores + 1, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>());
}

createDefaultExecutorService 方法用于创建自定义的线程池,线程池的大小为 CPU 核心数加 1。这样可以充分利用设备的多核性能,提高图片加载的效率。

3.3.2 线程池的使用
java 复制代码
// 在 Dispatcher 的构造函数中使用线程池
Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
           Cache cache, Stats stats, Downloader downloader, NetworkPolicy networkPolicy,
           int retryDelay) {
    this.service = service;
    // ... 其他初始化代码 ...
}

Dispatcher 的构造函数中,将创建好的线程池传递进来,并赋值给 service 属性。在任务调度时,会使用该线程池来执行图片加载任务。

3.4 任务分发

3.4.1 消息处理
java 复制代码
// 主线程的 Handler 处理消息
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case REQUEST_SUBMIT:
                // 处理任务提交消息
                performSubmit((BitmapHunter) msg.obj);
                break;
            case REQUEST_COMPLETE:
                // 处理任务完成消息
                performComplete((BitmapHunter) msg.obj);
                break;
            case REQUEST_FAILED:
                // 处理任务失败消息
                performFailed((BitmapHunter) msg.obj);
                break;
            case REQUEST_RETRY:
                // 处理任务重试消息
                performRetry((BitmapHunter) msg.obj);
                break;
            // 其他消息处理 ...
        }
    }
};

mainThreadHandler 是主线程的 Handler,用于处理各种消息。根据消息的类型,调用相应的处理方法,如 performSubmitperformCompleteperformFailed 等。

3.4.2 结果分发
java 复制代码
// 在 performComplete 方法中分发任务完成的结果
private void performComplete(BitmapHunter hunter) {
    // 从正在执行的任务集合中移除该任务
    hunterSet.remove(hunter);
    // 如果任务有需要回调的监听器,调用相应的回调方法
    if (hunter.getCallback() != null) {
        hunter.getCallback().onSuccess(hunter.getResult());
    }
    // ... 其他代码 ...
}

performComplete 方法中,如果任务有回调监听器,会调用监听器的 onSuccess 方法,将解码后的图片结果传递给回调方法进行处理。

四、Hunter 类分析

4.1 类的定义和基本属性

java 复制代码
// Hunter 类代表一个图片加载任务,实现了 Runnable 接口
class BitmapHunter implements Runnable {
    // Picasso 实例
    private final Picasso picasso;
    // 请求对象,包含图片加载的相关信息
    private final Request request;
    // 回调监听器,用于处理任务完成或失败的结果
    private final Callback callback;
    // 重试次数
    private int retryCount;
    // 任务结果
    private Bitmap result;
    // 任务异常
    private Exception exception;

    // 构造函数,初始化各种属性
    BitmapHunter(Picasso picasso, Request request, Callback callback) {
        this.picasso = picasso;
        this.request = request;
        this.callback = callback;
        this.retryCount = 0;
    }
}

BitmapHunter 类实现了 Runnable 接口,代表一个图片加载任务。它包含了 Picasso 实例、请求对象、回调监听器等属性,用于执行图片加载任务并处理结果。

4.2 任务执行逻辑

java 复制代码
// 实现 Runnable 接口的 run 方法,执行任务
@Override
public void run() {
    try {
        // 执行图片解码任务
        result = decode();
        if (result != null) {
            // 解码成功,通知调度器任务完成
            picasso.getDispatcher().dispatchComplete(this);
        } else {
            // 解码失败,通知调度器任务失败
            picasso.getDispatcher().dispatchFailed(this);
        }
    } catch (Exception e) {
        // 捕获异常,记录异常信息
        exception = e;
        // 通知调度器任务失败
        picasso.getDispatcher().dispatchFailed(this);
    }
}

// 执行图片解码任务
private Bitmap decode() throws IOException {
    // 从缓存中获取图片
    Bitmap bitmap = picasso.getCache().get(request.getKey());
    if (bitmap != null) {
        // 如果缓存中有图片,直接返回
        return bitmap;
    }
    // 从数据源下载图片
    InputStream stream = picasso.getDownloader().load(request.getUri(), request.getNetworkPolicy());
    if (stream == null) {
        // 下载失败,抛出异常
        throw new IOException("Failed to load image from " + request.getUri());
    }
    try {
        // 解码图片
        bitmap = BitmapFactory.decodeStream(stream);
    } finally {
        // 关闭输入流
        stream.close();
    }
    if (bitmap != null) {
        // 将解码后的图片存入缓存
        picasso.getCache().set(request.getKey(), bitmap);
    }
    return bitmap;
}

run 方法是任务的执行入口,它会调用 decode 方法进行图片解码。decode 方法会先从缓存中获取图片,如果缓存中没有,则从数据源下载图片并进行解码。解码成功后,将结果存入缓存并通知调度器任务完成;解码失败则通知调度器任务失败。

4.3 重试逻辑

java 复制代码
// 判断任务是否需要重试
boolean shouldRetry() {
    // 检查网络状态和重试次数
    return picasso.getNetworkPolicy().isRetryEnabled() && retryCount < picasso.getMaxRetryCount();
}

// 重试任务
void retry() {
    // 增加重试次数
    retryCount++;
    // 提交任务进行重试
    picasso.getDispatcher().dispatchSubmit(this);
}

shouldRetry 方法用于判断任务是否需要重试,它会检查网络状态和重试次数。retry 方法用于重试任务,会增加重试次数并提交任务进行重试。

五、PicassoExecutorService 类分析

5.1 类的定义和基本属性

java 复制代码
// PicassoExecutorService 类继承自 ThreadPoolExecutor,是自定义的线程池
class PicassoExecutorService extends ThreadPoolExecutor {
    // 线程池的名称前缀
    private static final String THREAD_NAME_PREFIX = "Picasso-";
    // 线程工厂,用于创建线程
    private static final ThreadFactory threadFactory = new ThreadFactory() {
        private final AtomicInteger count = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable r) {
            // 创建线程并设置线程名称
            Thread thread = new Thread(r, THREAD_NAME_PREFIX + count.getAndIncrement());
            // 设置线程为守护线程
            thread.setDaemon(true);
            return thread;
        }
    };

    // 构造函数,初始化线程池
    PicassoExecutorService(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                           TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }
}

PicassoExecutorService 类继承自 ThreadPoolExecutor,是自定义的线程池。它使用自定义的线程工厂创建线程,并设置线程为守护线程。

5.2 线程池的特点

  • 固定大小 :线程池的大小是固定的,由 corePoolSizemaximumPoolSize 决定。这样可以避免创建过多的线程,导致系统资源耗尽。
  • 守护线程:线程池中的线程都是守护线程,当主线程退出时,守护线程会自动退出,不会影响应用的正常关闭。
  • 自定义线程名称 :线程池中的线程名称都以 Picasso- 开头,并带有一个递增的编号,方便调试和监控。

六、调度模块的工作流程

6.1 任务提交

当开发者调用 Picasso 的 load 方法加载图片时,会创建一个 Request 对象,然后创建一个 BitmapHunter 任务,并将任务提交给 DispatcherDispatcher 会通过主线程的 Handler 调用 performSubmit 方法处理任务提交。

6.2 任务调度

performSubmit 方法会检查线程池的状态,如果线程池有空闲线程,直接将任务提交给线程池执行;否则,将任务添加到待执行的任务队列中。

6.3 任务执行

BitmapHunter 任务在线程池中执行 run 方法,调用 decode 方法进行图片解码。如果解码成功,将结果存入缓存并通知 Dispatcher 任务完成;如果解码失败,通知 Dispatcher 任务失败。

6.4 任务完成处理

Dispatcher 接收到任务完成的消息后,会调用 performComplete 方法处理。该方法会从正在执行的任务集合中移除该任务,如果任务有回调监听器,调用相应的回调方法。同时,会检查待执行的任务队列,如果有任务,尝试执行。

6.5 任务失败处理

Dispatcher 接收到任务失败的消息后,会调用 performFailed 方法处理。该方法会从正在执行的任务集合中移除该任务,如果任务有重试条件,将任务添加到重试队列中,并延迟一段时间后重试;否则,直接调用失败回调方法。同时,会检查待执行的任务队列,如果有任务,尝试执行。

七、调度模块的优化策略

7.1 线程池优化

  • 合理设置线程池大小:根据设备的 CPU 核心数和内存情况,合理设置线程池的大小。一般来说,线程池的大小可以设置为 CPU 核心数加 1,以充分利用设备的多核性能。
java 复制代码
// 创建自定义的线程池,线程数为 CPU 核心数加 1
private static ExecutorService createDefaultExecutorService(Context context) {
    int cores = Runtime.getRuntime().availableProcessors();
    return new PicassoExecutorService(cores + 1, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>());
}
  • 使用守护线程:将线程池中的线程设置为守护线程,当主线程退出时,守护线程会自动退出,不会影响应用的正常关闭。
java 复制代码
// 线程工厂,创建守护线程
private static final ThreadFactory threadFactory = new ThreadFactory() {
    private final AtomicInteger count = new AtomicInteger(1);

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, THREAD_NAME_PREFIX + count.getAndIncrement());
        thread.setDaemon(true);
        return thread;
    }
};

7.2 任务调度优化

  • 任务优先级管理 :可以根据任务的重要性和紧急程度,为任务设置不同的优先级,优先执行重要的任务。在 Dispatcher 中,可以对任务队列进行排序,确保高优先级的任务先执行。
  • 任务分组管理:将任务按照不同的类型或来源进行分组,对不同组的任务进行不同的调度策略。例如,将本地图片加载任务和网络图片加载任务分开处理,提高调度的效率。

7.3 缓存优化

  • 内存缓存 :使用内存缓存存储解码后的图片,避免重复的解码操作,提高图片加载的速度。在 BitmapHunterdecode 方法中,会先从缓存中获取图片,如果缓存中有则直接返回。
java 复制代码
// 从缓存中获取图片
Bitmap bitmap = picasso.getCache().get(request.getKey());
if (bitmap != null) {
    return bitmap;
}
  • 磁盘缓存:除了内存缓存,还可以使用磁盘缓存存储图片。当内存缓存中没有图片时,可以从磁盘缓存中获取,减少网络请求,提高图片加载的效率。

八、与其他调度方案的对比

8.1 与 Android 原生线程池的对比

8.1.1 功能特点
  • Picasso 调度模块:专门为图片加载任务设计,提供了任务调度、线程管理、任务分发等功能,并且与 Picasso 的其他模块紧密结合,实现了图片加载的高效处理。
  • Android 原生线程池:是通用的线程池,提供了基本的线程管理功能,但没有针对图片加载任务进行优化,需要开发者自己实现任务调度和分发逻辑。
8.1.2 性能表现
  • Picasso 调度模块:在图片加载任务中,能够根据任务的优先级和资源情况,合理安排任务的执行顺序,提高了图片加载的效率。同时,使用自定义的线程池和缓存机制,减少了不必要的资源消耗。
  • Android 原生线程池:在处理图片加载任务时,可能会因为缺乏任务调度和优化机制,导致任务执行顺序混乱,资源利用率不高。
8.1.3 使用场景
  • Picasso 调度模块:适用于需要高效加载图片的场景,特别是在 Android 应用中频繁进行图片加载的情况下,能够提供更好的性能和用户体验。
  • Android 原生线程池:适用于一般的多线程任务处理,当应用中没有特定的图片加载需求时,可以使用原生线程池。

8.2 与其他图片加载库的调度模块对比

8.2.1 功能特点
  • Picasso 调度模块:API 简洁,易于使用,提供了基本的任务调度和线程管理功能,能够满足大多数图片加载场景的需求。
  • 其他图片加载库的调度模块:不同的图片加载库可能有不同的调度策略和功能特点。例如,Glide 的调度模块提供了更复杂的任务调度和缓存管理功能,支持更多的图片格式和加载方式。
8.2.2 性能表现
  • Picasso 调度模块:在性能方面表现稳定,能够快速加载图片并进行合理的调度。但在处理大量高分辨率图片时,可能会因为内存管理和调度策略的限制,导致性能下降。
  • 其他图片加载库的调度模块:一些图片加载库的调度模块在性能方面进行了优化,能够更好地处理大尺寸图片和复杂的加载任务。例如,Fresco 的调度模块使用了独立的内存管理机制,能够有效避免内存溢出问题。
8.2.3 使用场景
  • Picasso 调度模块:适用于对图片加载功能要求不高,追求简洁易用的场景。例如,一些小型应用中只需要实现基本的图片加载功能。
  • 其他图片加载库的调度模块:适用于对图片加载功能要求较高,需要处理复杂图片类型和大量图片的场景。例如,社交应用、电商应用等。

九、常见问题及解决方案

9.1 图片加载缓慢

  • 原因分析:可能是网络问题、线程池资源不足、缓存未命中等原因导致。
  • 解决方案:检查网络连接,确保网络稳定;调整线程池的大小,增加线程池的资源;优化缓存策略,提高缓存命中率。

9.2 内存溢出

  • 原因分析:可能是加载了大量高分辨率的图片,或者缓存管理不当导致内存占用过高。
  • 解决方案:合理设置图片的采样率,减少图片的内存占用;优化缓存策略,定期清理缓存,避免缓存过大。

9.3 任务调度混乱

  • 原因分析:可能是任务优先级设置不合理、任务队列管理不当等原因导致。
  • 解决方案:根据任务的重要性和紧急程度,合理设置任务的优先级;优化任务队列的管理,确保任务按照正确的顺序执行。

十、总结与展望

10.1 总结

通过对 Android Picasso 调度模块的深入分析,我们了解到该模块在整个 Picasso 框架中起着至关重要的作用。它通过 Dispatcher 类实现了任务的调度、线程管理和分发,使用 BitmapHunter 类代表图片加载任务,PicassoExecutorService 类作为自定义的线程池,确保了图片加载任务能够高效、有序地执行。同时,调度模块采用了多种优化策略,如线程池优化、任务调度优化和缓存优化等,提高了图片加载的性能和用户体验。与其他调度方案相比,Picasso 调度模块具有 API 简洁、易于使用的特点,适用于大多数图片加载场景。

10.2 展望

未来,Android Picasso 调度模块可以在以下几个方面进行改进和扩展:

  • 智能化调度:引入机器学习算法,根据图片的类型、大小、来源等信息,智能地调整任务的调度策略,进一步提高图片加载的效率。
  • 分布式调度:在多设备、多线程的环境下,实现分布式调度,充分利用多个设备的资源,加速图片加载的过程。
  • 与其他组件的集成:与 Android 系统的其他组件(如传感器、网络管理模块等)进行更紧密的集成,根据设备的状态和网络环境,动态调整调度策略。
  • 性能优化:不断优化线程池的管理和任务调度算法,减少资源消耗,提高系统的响应速度和稳定性。

总之,Android Picasso 调度模块是一个功能强大、灵活可扩展的模块,通过不断的改进和优化,将为 Android 开发者提供更好的图片加载解决方案。

相关推荐
Kapaseker1 分钟前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋28 分钟前
Android 协程时代,Handler 应该退休了吗?
android
哈里谢顿11 小时前
1000台裸金属并发创建中的重难点问题分析
面试
哈里谢顿11 小时前
20260303面试总结(全栈)
面试
火柴就是我14 小时前
让我们实现一个更好看的内部阴影按钮
android·flutter
over69716 小时前
从 LLM 到全栈 Agent:MCP 协议 × RAG 技术如何重构 AI 的“做事能力”
面试·llm·mcp
SuperEugene17 小时前
Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置
前端·vue.js·面试
FunnySaltyFish18 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Sailing19 小时前
🚀 别再乱写 16px 了!CSS 单位体系已经进入“计算时代”,真正的响应式布局
前端·css·面试
砖厂小工21 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github