Android 应用的线程世界:最少需要几个线程才能启动?

这篇文章主要介绍了 Android App 中的多种线程,包括守护线程(如 Signal Catcher 等)、渲染线程、主线程、三方线程(如 OkHttp、Glide、ARouter 相关线程)等,还提及三方库中线程池的情况及可能存在的问题。

这篇文章主要介绍了 Android App 中的多种线程,包括守护线程(如 Signal Catcher 等)、渲染线程、主线程、三方线程(如 OkHttp、Glide、ARouter 相关线程)等,还提及三方库中线程池的情况及可能存在的问题。

守护线程

Signal Catcher线程

Signal Catcher是一个守护线程,用于捕获 SIGQUIT, SIGUSR1 信号,并采取相应的行为。

Android系统中,由Zygote孵化而来的子进程,包含system_server进程和各种APP进程都存在一个Signal Catcher线程,但是Zygote进程本身没有这个线程。

在Process类中有SIGQUIT,SIGUSR1 的定义:

    public static final int SIGNAL_QUIT = 3;
    public static final int SIGNAL_KILL = 9;
    public static final int SIGNAL_USR1 = 10;

当前进程的Signal Catcher线程接收到信号SIGNAL_QUIT,则挂起进程中的所有线程并DUMP所有线程的状态。

当前进程的Signal Catcher线程接收到信号SIGNAL_USR1,则触发进程强制执行GC操作。

发送信号的方式:

    Process.sendSignal(android.os.Process.myPid(), Process.SIGNAL_QUIT);
    Process.sendSignal(android.os.Process.myPid(), Process.SIGNAL_USR1);

当我们直接在代码中调用上诉代码会发生什么?

Process.SIGNAL_USR1:

I/Process: Sending signal. PID: 12363 SIG: 10
I/etease.popo.ap: Thread[3,tid=12373,WaitingInMainSignalCatcherLoop,Thread*=0x793d816400,peer=0x13700020,"Signal Catcher"]: reacting to signal 10
I/etease.popo.ap: SIGUSR1 forcing GC (no HPROF) and profile save
I/etease.popo.ap: Explicit concurrent copying GC freed 18773(4MB) AllocSpace objects, 7(140KB) LOS objects, 68% free, 2MB/8MB, paused 130us total 13.633ms

Process.SIGNAL_QUIT

I/Process: Sending signal. PID: 12812 SIG: 3
I/etease.popo.ap: Thread[3,tid=12823,WaitingInMainSignalCatcherLoop,Thread*=0x793d816400,peer=0x148051b0,"Signal Catcher"]: reacting to signal 3
I/etease.popo.ap: Wrote stack traces to '[tombstoned]'

RenderThread 渲染线程

Android 5.0之后新增的一个线程,用来协助UI线程进行图形绘制。所有的GL命令执行都放在这个线程上。渲染线程在RenderNode中存有渲染帧的所有信息,并监听VSync信号,因此可以独立做一些属性动画。

在清单文件APP中添加:android:hardwareAccelerated="false" 启动APP将会不存在渲染线程;硬件加速在Android中试默认开启的。

Render Thread在运行时主要是做以下两件事情:

  • Task Queue的任务,这些Task一般就是由MainThread发送过来的,例如,MainThread通过发送一个DrawFrameTask给RenderThread的TaskQueue中,请求RenderThread渲染窗口的下一帧。
  • PendingRegistrationFrameCallbacks列表的IFrameCallback回调接口。每一个IFrameCallback回调接口代表的是一个动画帧,这些动画帧被同步到Vsync信号到来由RenderThread自动执行。具体来说,就是每当Vsync信号到来时,就将一个类型为DispatchFrameCallbacks的Task添加到RenderThread的TaskQueue去等待调度。一旦该Task被调度,就可以在RenderThread中执行注册在PendingRegistrationFrameCallbacks列表中的IFrameCallback回调接口了。

FinalizerDaemon 析构守护线程

对于重写成员函数finailze的对象,他们被GC决定回收时,并没有马上被回收,而是被放入到一个队列中,等待FinalizerDaemon守护线程去调用他们的成员函数finalize,然后再被回收。

FinalizerWatchdogDaemon 析构监护守护线程。

用来监控FinalizerDaemon线程的执行。一旦检测哪些重写了成员函数finalize的对象在执行成员函数finalize时超出一定的时候,那么就会退出VM。

ReferenceQueueDaemon 引用队列守护线程

我们知道在创建引用对象的时候,可以关联一个队列。当被引用对象引用的对象被GC回收的时候呀,被引用对象就会呗加入到其创建时关联的队列中去,这个加入队列的操作就是由ReferenceQueueDaemon守护线程来完成的。这样应用程序就可以知道哪些被引用对象引用的对象已经被回收了。

HeapTrimmerDaemon 堆裁剪守护线程

用于执行裁剪堆的操作,也就是用来将哪些空闲的堆内存归还给系统。

HeapTaskDaemon 线程

Android每个进程都有一个HeapTaskDaemon线程,在该线程内进行GC操作。 HeapTaskDaemon 继承自 Daemon 对象。Daemon 对象实际是一个Runnale,并且内部会创建一个线程,用于执行当前这个 Daemon unnable,这个内部线程的线程名就叫 HeapTaskDaemon。

private static class HeapTaskDaemon extends Daemon {
        private static final HeapTaskDaemon INSTANCE = new HeapTaskDaemon();

        HeapTaskDaemon() {
            super("HeapTaskDaemon");
        }

        // Overrides the Daemon.interupt method which is called from Daemons.stop.
        public synchronized void interrupt(Thread thread) {
            VMRuntime.getRuntime().stopHeapTaskProcessor();
        }

        @Override public void runInternal() {
            synchronized (this) {
                if (isRunning()) {
                  // Needs to be synchronized or else we there is a race condition where we start
                  // the thread, call stopHeapTaskProcessor before we start the heap task
                  // processor, resulting in a deadlock since startHeapTaskProcessor restarts it
                  // while the other thread is waiting in Daemons.stop().
                  VMRuntime.getRuntime().startHeapTaskProcessor();
                }
            }
            // This runs tasks until we are stopped and there is no more pending task.
            VMRuntime.getRuntime().runHeapTasks();
        }
    }

Binder 线程

每个APP进程在启动之后会创建一个binder线程池,用于相应IPC客户端的请求。

例如APP与AMS等服务之间可以通过IPC双向通信,当APP作为服务端的时候,就需要通过Binder线程相应来自AMS的请求。一个Server进程中有一个最大的Binder线程数限制,默认为16个binder线程。

与系统服务通信或者自行实现多进程Binder通信大致需要注意一下几点:

  • 与系统通信的方法,建议使用try cache进行防护,防止App出现崩溃。
  • 需要注意调用频率,部分API调用频率过快响应会比较慢,从而导致主线程卡顿甚至ANR。

主线程

主线程也较UI线程。

这个线程作为Android 开发都比较熟悉。

三方线程

OkHttp相关

如果你使用OkHttp作为网络请求库,那么工程中会有以下几类线程

OkHttp Dispatcher

用于实际执行HTTP请求

  @get:Synchronized
  @get:JvmName("executorService"val executorService: ExecutorService
    get() {
      if (executorServiceOrNull == null) {
        executorServiceOrNull = ThreadPoolExecutor(0Int.MAX_VALUE, 60, TimeUnit.SECONDS,
            SynchronousQueue(), threadFactory("$okHttpName Dispatcher"false))
      }
      return executorServiceOrNull!!
    }

OkHttp TaskRunner

4.x版本引入,用于管理和调度内部任务

  companion object {
    @JvmField
    val INSTANCE = TaskRunner(RealBackend(threadFactory("$okHttpName TaskRunner", daemon = true)))

    val logger: Logger = Logger.getLogger(TaskRunner::class.java.name)
  }

OkHttp ConnectionPool

负责清理和回收无用的连接池

  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce() = cleanup(System.nanoTime())
  }

Okio Watchdog

OkHttp中使用Watchdog来处理超时逻辑

  private class Watchdog internal constructor() : Thread("Okio Watchdog") {
    init {
      isDaemon = true
    }

    override fun run() {
      while (true) {
        try {
          var timedOut: AsyncTimeout? = null
          synchronized(AsyncTimeout::class.java) 
{
            timedOut = awaitTimeout()

            // The queue is completely empty. Let this thread exit and let another watchdog thread
            // get created on the next call to scheduleTimeout().
            if (timedOut === head) {
              head = null
              return
            }
          }

          // Close the timed out node, if one was found.
          timedOut?.timedOut()
        } catch (ignored: InterruptedException) {
        }
      }
    }
  }

Glide相关

如果你使用Glide加载图片,那么工程中会有以下几类线程

"glide-source-thread-x"

用于从网络、文件系统或其他数据源中加载原始图像数据

  public static GlideExecutor.Builder newSourceBuilder() {
    return new GlideExecutor.Builder(/* preventNetworkOperations= */ false)
        .setThreadCount(calculateBestThreadCount())
        .setName(DEFAULT_SOURCE_EXECUTOR_NAME);
  }

"glide-disk-cache-thread-x"

用于从缓存中读取数据

  public static GlideExecutor.Builder newDiskCacheBuilder() {
    return new GlideExecutor.Builder(/* preventNetworkOperations= */ true)
        .setThreadCount(DEFAULT_DISK_CACHE_EXECUTOR_THREADS)
        .setName(DEFAULT_DISK_CACHE_EXECUTOR_NAME);
  }

source-unlimited

  public static GlideExecutor newUnlimitedSourceExecutor() {
    return new GlideExecutor(
        new ThreadPoolExecutor(
            0,
            Integer.MAX_VALUE,
            KEEP_ALIVE_TIME_MS,
            TimeUnit.MILLISECONDS,
            new SynchronousQueue<Runnable>(),
            new DefaultThreadFactory(
                new DefaultPriorityThreadFactory(),
                DEFAULT_SOURCE_UNLIMITED_EXECUTOR_NAME,
                UncaughtThrowableStrategy.DEFAULT,
                false)));
  }

animation

用于执行动画

  public static GlideExecutor.Builder newAnimationBuilder() {
    int maximumPoolSize = calculateAnimationExecutorThreadCount();
    return new GlideExecutor.Builder(/* preventNetworkOperations= */ true)
        .setThreadCount(maximumPoolSize)
        .setName(DEFAULT_ANIMATION_EXECUTOR_NAME);
  }

ARouter相关

如果你使用ARouter作为路由

ARouter task pool No.x , thread No.X

创建位置

    public static DefaultPoolExecutor getInstance() {
        if (null == instance) {
            synchronized (DefaultPoolExecutor.class{
                if (null == instance) {
                    instance = new DefaultPoolExecutor(
                            INIT_THREAD_COUNT,
                            MAX_THREAD_COUNT,
                            SURPLUS_THREAD_LIFE,
                            TimeUnit.SECONDS,
                            new ArrayBlockingQueue<Runnable>(64),
                            new DefaultThreadFactory());
                }
            }
        }
        return instance;
    }

另外ARouter也为开发者提供了使用自己线程池的接口:

    static synchronized void setExecutor(ThreadPoolExecutor tpe) {
        executor = tpe;
    }

三方库总结

关于三方库中的线程池这里仅列举了几个库。如果你的工程中含有大量的三方库,我详细也会存在大量的工作线程。

一些三方库会提供接口允许开发者将其替换成工程内部统一维护的线程池,这样可以做到工程中线程的收敛。笔者遇到最离谱的应该是腾讯X5浏览器,由另外一个小组的同事引入到项目中,集成后发现其引入了大量的线程(大概几十个)。


来源: 公众号
本文观点不代表码客-全球程序员交流社区立场,不承担法律责任,文章及观点也不构成任何投资意见。

赞 ()

相关推荐

  • 置顶 开发项目接单群,免费入群了

    无论你是Android、ios、java、php,或者你是产品经理、老板,都可以免费入群接单或者发布项目,全程不收取任何费用。

    2025年02月21日 15点27分
  • Android 应用的线程世界:最少需要几个线程才能启动?

    这篇文章主要介绍了 Android App 中的多种线程,包括守护线程(如 Signal Catcher 等)、渲染线程、主线程、三方线程(如 OkHttp、Glide、ARouter 相关线程)等,还提及三方库中线程池的情况及可能存在的问题。

    2025年02月25日 15点13分
  • Android 复杂项目崩溃率收敛至0.01%实践

    在我们的项目中,每个版本发布之后,我们会创建一个opt分支,用于修复线上崩溃以及业务逻辑BUG。

    2025年02月25日 15点11分
  • Android 能悄悄知道用户截屏?这里有你想要的答案

    很多应用在当你截屏的时候能够感知到,并提示你是否要发送截屏等等。

    2025年02月25日 15点07分
  • 科大讯飞讯飞星火API能力免费开放,引领大模型商业化新篇章

    合肥本土科技巨头科大讯飞传来振奋人心的消息:讯飞星火API能力正式向公众免费开放,其中,讯飞星火Lite API更是实现了永久免费。这一举措无疑为整个行业注入了新的活力,也彰显了科大讯飞在大模型商业化进程中的坚定决心。

    2025年02月21日 15点57分
  • 一木林接入AI大模型,实现智能体功能

    一木林——全能型AI电子工具箱。这是一款集多功能于一体的工具类应用,凭借小巧的体积与全面且强大的功能,赢得了极高的口碑。应用内汇聚了上百款实用工具,诸如指南针、计算器、分贝仪等,一应俱全。如今,一木林已携手星火AI大模型,实现了AI对话与智能体的创新功能。

    2025年02月21日 15点42分
  • PrivacySentry:隐私政策守护利器

    近年来,工信部对APP个人隐私要求越来越多,之后各大应用市场也开始要求,有违规情况的会导致APP下架或者无法上架。这不,我的app就因为三方SDK频繁获取Android ID 导致无法上架,等SDK商场更新也很浪费时间,所以只能想办法去处理这件事,好在找到了PrivacySentry这个神器,可规避应用市场上架合规检测的大部分问题

    2025年02月18日 23点20分
  • Kotlin协程:MutableSharedFlow的实现原理

    在Koltin协程:异步热数据流的设计与使用中,提到了可以通过MutableSharedFlow方法创建一个MutableSharedFlow接口指向的对象,代码如下:

    2025年02月18日 11点03分
  • 探索Markdown之父John Gruber对Markdown的看法

    Markdown的缔造者——约翰·格鲁伯,其形象正如上方头像所示。不禁让人遐想,国外的大师们是否都偏爱蓄须? 约翰·格鲁伯,1973年出生于美国宾夕法尼亚州,他是一位才华横溢的作家、博主、UI设计师,更是Markdown的创始人。他毕业于Drexel University(卓克索大学),并获得了计算机科学学士学位。在职业生涯中,他曾在Joyent公司任职,为大型企业提供了基础架构和平台服务。自2002年起,他开始撰写备受欢迎的科技博客《Daring Fireball》,并主持了知名的播客节目The Talk Show。2013年,他与两位友人共同创立了Q Branch,并开发了Vesper笔记应用。

    2025年02月18日 10点52分
  • 浅聊一下JVM内存结构

    2025年02月17日 14点29分
  • Android屏幕适配(6) — 今日头条屏幕适配

    在之前的文章中,我们讲到了Android屏幕适配的一些知识,大家感兴趣的话可参考 Android屏幕适配(1) — 概念解释 Android屏幕适配(2) — drawable与mipmap Android屏幕适配(3) — 资源文件夹命名与匹配规则 Android屏幕适配(4) — 宽高限定符 Android屏幕适配(5) — 最小宽度smallWidth适配 这节我们讲讲今日头条屏幕适配方案。

    2025年02月15日 11点19分
  • Android屏幕适配(4) — 宽高限定符

    在之前的文章中,我们讲到了Android屏幕适配的一些知识,大家感兴趣的话可参考 Android屏幕适配(1) — 概念解释 Android屏幕适配(2) — drawable与mipmap Android屏幕适配(3) — 资源文件夹命名与匹配规则 今天就让我们来学习下Android屏幕适配的宽高限定符相关知识吧。

    2025年02月15日 11点14分
  • Android屏幕适配(3) — 资源文件夹命名与匹配规则

    在之前的文章中,我们已经讲到了Android屏幕适配的一些知识,大家感兴趣的话,可参考以下文章 Android屏幕适配(1) — 概念解释 Android屏幕适配(2) — drawable与mipmap 大家都知道在Android资源文件夹res/下,我们经常能看到layout-sw720,drawable-xhdpi,values-w480等字样文件夹,那么res/下到底有哪些文件夹可以命名,命名规则如何?这节我们来讲讲关于Android资源文件夹res/下各文件夹的命名。

    2025年02月15日 11点14分
  • Android屏幕适配(2) — drawable与mipmap

    上节我们讲到了屏幕适配的几个基本概念。大家感兴趣的可参考 Android屏幕适配(1) — 概念解释 这节我们讲讲屏幕适配中drawable与mipmap相关知识。

    2025年02月15日 11点08分
  • Android屏幕适配(1) — 概念解释

    2025年02月15日 11点05分
  • Android-Gson使用

    在日常的Android开发当中,我们肯定少不了要使用Gson框架解析JSON字符串。这篇博客分享一下我了解到Gson框架的知识,希望对看文章的小伙伴有所帮助。

    2025年02月15日 11点01分

发表回复

评论列表

点击查看更多

    联系我们

    在线咨询: QQ交谈

    微信:dxmcpjl

    邮件:1529097251#qq.com

    工作时间:周一至周五,9:30-18:30,节假日休息

    微信