Android Native Crash - 线程挂起超时问题

背景

本年度一直在做线程相关的性能优化,例如线程收敛、线程栈优化,以及一些由线程导致的OOM问题。最近在检索崩溃大盘时,发现了一些由于线程挂起导致的Native Crash,发现此问题存在已久只不过量不是很大,属于长尾问题,就花精力研究一下,得出一些方案,就此探讨与分享一下。

堆栈分析

  • Case 1:
// Crash thread
signal:6 (SIGABRT),code:-1 (SI_QUEUE),fault addr:--------
Abort message:
Thread suspension timed out: 0x6f2e45d888:OkHttp https://dummy.global.com/...
backtrace:
// ignore more data

java stacktrace:
at dalvik.system.VMStack.getThreadStackTrace(VMStack.java)
at java.lang.Thread.getStackTrace(Thread.java:1841)
at java.lang.Thread.getAllStackTraces(Thread.java:1909)
at com.appsflyer.internal.AFa1xSDK$23740.AFInAppEventType(AFa1xSDK.java:113)
at com.appsflyer.internal.AFa1xSDK$23740.values(AFa1xSDK.java:168)
at com.appsflyer.internal.AFa1xSDK$23740.AFInAppEventParameterName(AFa1xSDK.java:73)
at com.appsflyer.internal.AFa1tSDK$28986.AFKeystoreWrapper(AFa1tSDK.java:38)
at java.lang.reflect.Method.invoke(Method.java)
at com.appsflyer.internal.AFc1oSDK.AFKeystoreWrapper(AFc1oSDK.java:159)
at com.appsflyer.internal.AFd1hSDK.values(AFd1hSDK.java:88)
at com.appsflyer.internal.AFd1oSDK.valueOf(AFd1oSDK.java:144)
at com.appsflyer.internal.AFd1zSDK.afErrorLog(AFd1zSDK.java:207)
at com.appsflyer.internal.AFc1bSDK.run(AFc1bSDK.java:4184)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487)
at java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)

  • Case 2:
// Crash thread
signal:6 (SIGABRT),code:-1 (SI_QUEUE),fault addr:--------
Abort message:
Thread suspension timed out: 0x70a383f4d8:DefaultDispatcher-worker-3
backtrace:
#00 pc 00000000000896fc  /apex/com.android.runtime/lib64/bionic/libc.so (abort+180)
#01 pc 000000000076fc20  /apex/com.android.art/lib64/libart.so (art::Runtime::Abort(char const*)+904)
#02 pc 00000000000357d0  /apex/com.android.art/lib64/libbase.so (android::base::SetAborter(std::__1::function<void (char const*)>&&)::$_0::__invoke(char const*)+80)
#03 pc 0000000000034d58  /apex/com.android.art/lib64/libbase.so (android::base::LogMessage::~LogMessage()+352)
#04 pc 000000000079bac0  /apex/com.android.art/lib64/libart.so (art::ThreadSuspendByPeerWarning(art::ScopedObjectAccess&, android::base::LogSeverity, char const*, _jobject*).__uniq.215660552210357940630679712151551015321+288)
#05 pc 000000000024c838  /apex/com.android.art/lib64/libart.so (art::ThreadList::SuspendThreadByPeer(_jobject*, art::SuspendReason, bool*)+3236)
#06 pc 00000000005949e8  /apex/com.android.art/lib64/libart.so (art::Thread_setNativeName(_JNIEnv*, _jobject*, _jstring*).__uniq.300150332875289415499171563183413458937+744)
#07 pc 0000000000439460  /data/misc/apexdata/com.android.art/dalvik-cache/arm64/boot.oat (art_jni_trampoline+128)

// ingore more data

java stacktrace:
at java.lang.Thread.setNativeName(Thread.java)
at java.lang.Thread.setName(Thread.java:1383)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.setIndexInArray(CoroutineScheduler.java:588)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.tryTerminateWorker(CoroutineScheduler.java:842)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.park(CoroutineScheduler.java:800)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.tryPark(CoroutineScheduler.java:740)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.java:711)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.java:664)

上述是崩溃时 dump 出的日志,其中能看到 Java 的日志,所以相对来说触发崩溃的时机比较好分析。 总结了所有因线程挂起所导致的问题一共有两类。

  • Appsflyer VMStack.getThreadStackTrace()
  • Coroutine Thread.setName()

上述两个方法的调用分别触发了一个abort()的 Linux终止信号,所导致了 App 的崩溃,接下来我们依次分析一下触发此次abort()信号的流程。

Thread.setName()

根据上述堆栈日志我们发现,修改线程名称是由协程所触发的,我们来看一下。 首先我们先追踪一下协程在执行任务的时候在切换调度器的过程中都做了哪些事情。

协程执行流程

在 Kotlin 中,协程与线程是两个不同的概念。协程在 JVM 上通过线程来执行,但它们不直接绑定到任何特定的线程上。多个协程可以在单个线程上运行,也可以灵活地在不同的线程间切换。这种设计允许协程在等待如 I/O 操作的完成时挂起,而不会阻塞其所在的线程,从而其他协程可以继续在该线程上执行。

Kotlin 中协程和线程的关系更多的是抽象层面的关联而非直接的依附关系。协程通过调度器(Dispatchers)来控制其在何种线程或线程池上执行。例如,Dispatchers.Default 是为 CPU 密集型任务准备的,默认使用共享的线程池;而 Dispatchers.IO 则优化用于 I/O 操作,同样操作共享的线程池。 在 Koltin 协程中,线程这个概念可以叫做 Worker

Worker的创建

当我们使用如下的协程代码时,我们就会创建一个 IO 调度器,用于做网络请求等等事件,此时就会触发 Worker的创建流程。

fun doSomething(){
    viewmodelScope.launch(Dispatchers.IO){
        //  do something...
    }
}

针对Dispatcher.IO调度器 & Dispatcher.Default调度器内部都使用了 CoroutineScheduler作为线程池的实现。

在协程 CoroutineScheduler中,内置对 Worker(线程)的创建。

private fun createNewWorker(): Int {
    synchronized(workers) {
        // Make sure we're not trying to resurrect terminated scheduler
        if (isTerminated) return -1
        val state = controlState.value
        val created = createdWorkers(state)
        val blocking = blockingTasks(state)
        val cpuWorkers = (created - blocking).coerceAtLeast(0)
        // Double check for overprovision
        if (cpuWorkers >= corePoolSize) return 0
        if (created >= maxPoolSize) return 0
        // start & register new worker, commit index only after successful creation
        val newIndex = createdWorkers + 1
        require(newIndex > 0 && workers[newIndex] == null)
        /*
         * 1) Claim the slot (under a lock) by the newly created worker
         * 2) Make it observable by increment created workers count
         * 3) Only then start the worker, otherwise it may miss its own creation
         */
        val worker = Worker(newIndex)
        workers.setSynchronized(newIndex, worker)
        require(newIndex == incrementCreatedWorkers())
        worker.start()
        return cpuWorkers + 1
    }
}

上述代码是计算一系列的数量判断,最终如果需要创建一个 Worker时,会初始化Worker对象,然后会调用Thread.start()

internal inner class Worker private constructor() : Thread() {
    init {
        isDaemon = true
    }

    // guarded by scheduler lock, index in workers array, 0 when not in array (terminated)
    @Volatile // volatile for push/pop operation into parkedWorkersStack
    var indexInArray = 0
        set(index) {
            name = "$schedulerName-worker-${if (index == 0) "TERMINATED" else index.toString()}"
            field = index
        }

    constructor(index: Int) : this() {
        indexInArray = index
    }

    // ignore more code...
}

ok,到了这里我们可以看到 Worker实际上就是一个线程。其中存在一个 indexInArray的成员变量,set()方法用于修改线程名称

我们什么场景会频繁调用 Thread.setName()?

我们已经知道协程调度器实际上是自己做了一个线程池的逻辑,内部如何创建的线程都封装在 CoroutineScheduler类中。 此时我们再回头看一下崩溃日志,这个Thread.setName()的执行顺序是什么。

graph TD
run --> runWorker --> 
tryPark --> park --> tryTerminateWorker -->
setIndexInArray --> Thread.setName

上述流程图可以理解为当我们创建了多个 task 协程挂起事件,这个 task就会去线程池上找一个 worker,如果没有就会 创建一个 worker, 当线程池内部的自旋检查 task & worker 数量状态时,如果当前没有 task 并且 woker 数量超过了 核心工作线程的数量,那么就会回收线程,因此存在了 tryPark 的方法,去终止线程,之后当终止了线程,要同步去修改对应的 worker 的 名称,因为 worker 整体的数据结构以AtomicReferenceArray数组存在的,然后会将index的值依次减1。

修改线程名或者获取堆栈,为什么会挂起 Thread?

了解了问题的发生场景,我们看下挂起线程的原因。

art/runtime/native/java_lang_Thread.cc
static void Thread_setNativeName(JNIEnv* env, jobject peer, jstring java_name) {
  ScopedUtfChars name(env, java_name);
  {
    ScopedObjectAccess soa(env);
    if (soa.Decode<mirror::Object>(peer) == soa.Self()->GetPeer()) {
      // 1.
      soa.Self()->SetThreadName(name.c_str());
      return;
    }
  }
  // Suspend thread to avoid it from killing itself while we set its name. We don't just hold the
  // thread list lock to avoid this, as setting the thread name causes mutator to lock/unlock
  // in the DDMS send code.
  ThreadList* thread_list = Runtime::Current()->GetThreadList();
  // Take suspend thread lock to avoid races with threads trying to suspend this one.
  // 2.
  Thread* thread = thread_list->SuspendThreadByPeer(peer, SuspendReason::kInternal);
  if (thread != nullptr) {
    {
      ScopedObjectAccess soa(env);
      thread->SetThreadName(name.c_str());
    }
    bool resumed = thread_list->Resume(thread, SuspendReason::kInternal);
    DCHECK(resumed);
  }
}

上述代码是在java中线程调用Thread.setNativeName()通过JNI最终调用的native侧的代码。

  • 代码 1 处是用来判断是否是线程改自己的名称,如果是,直接修改名称即可,无需挂起。
  • 代码 2 处是如果是A线程去修改B线程的名称,则需要挂起B线程,再修改线程名。

关于为何要先挂起,源码中也存在一个注释,在多线程环境中修改线程名称涉及到线程状态的同步和管理,直接修改活动线程的名称可能会引起线程自身的状态问题或与其他线程的交互问题。因此,先暂停线程,安全地修改名称后再恢复运行,是一种保证线程安全性的必要措施。

ART虚拟机是如何挂起线程的?

线程挂起检查

接下来我们进一步看下挂起的细节,去看一下SuspendThreadByPeer()函数如何实现的。

art/runtime/thread_list.cc
static constexpr useconds_t kThreadSuspendInitialSleepUs = 0;
static constexpr useconds_t kThreadSuspendMaxYieldUs = 3000;
static constexpr useconds_t kThreadSuspendMaxSleepUs = 5000;

Thread* ThreadList::SuspendThreadByPeer(jobject peer,
                                        SuspendReason reason,
                                        bool* timed_out) {
  bool request_suspension = true; // 标志是否需要请求暂停 
  const uint64_t start_time = NanoTime(); // 记录开始时间 
  int self_suspend_count = 0; // 自暂停计数 
  useconds_t sleep_us = kThreadSuspendInitialSleepUs; // 设置初次循环的休眠时间 这里是 0
  *timed_out = false; // 超时标志 
  Thread* const self = Thread::Current(); // 获取当前线程 
  Thread* suspended_thread = nullptr; // 初始化指向将要被暂停的线程的指针
  VLOG(threads) << "SuspendThreadByPeer starting";
  while (true) {
    Thread* thread; // 用于指向找到的线程
    {
      ScopedObjectAccess soa(self); // 保证对Java对象的访问是安全的
      MutexLock thread_list_mu(self, *Locks::thread_list_lock_); // 锁定线程列表,防止并发修改
      thread = Thread::FromManagedThread(soa, peer); // 通过Java对象找到对应的本地线程
      if (thread == nullptr) {
      // 如果没有找到线程,则检查是否已经有被挂起的线程需要恢复挂起计数
        if (suspended_thread != nullptr) {
          MutexLock suspend_count_mu(self, *Locks::thread_suspend_count_lock_);
          // 重点...
          // 逆向调整挂起计数,避免死锁
          bool updated = suspended_thread->ModifySuspendCount(soa.Self(),
                                                              -1,
                                                              nullptr,
                                                              reason);
          DCHECK(updated);
        }
        // 打印警告信息,并返回空,表示没有找到对应的线程
        ThreadSuspendByPeerWarning(soa,
                                   ::android::base::WARNING,
                                    "No such thread for suspend",
                                    peer);
        return nullptr;
      }
      // 检查找到的线程是否属于当前的线程列表
      if (!Contains(thread)) {
        CHECK(suspended_thread == nullptr);
        // 如果不属于,则打印日志并返回空
        VLOG(threads) << "SuspendThreadByPeer failed for unattached thread: "
            << reinterpret_cast<void*>(thread);
        return nullptr;
      }
      VLOG(threads) << "SuspendThreadByPeer found thread: " << *thread;
      {
        MutexLock suspend_count_mu(self, *Locks::thread_suspend_count_lock_);
        if (request_suspension) {
        // 如果需要请求挂起
          if (self->GetSuspendCount() > 0) {
            // 如果当前线程已经被标记挂起状态,增加自暂停计数并跳过当前循环
            ++self_suspend_count;
            continue;
          }
          CHECK(suspended_thread == nullptr);
          // 设置被暂停的线程
          suspended_thread = thread;
          // 重点...
          // 增加该线程的挂起计数
          bool updated = suspended_thread->ModifySuspendCount(self, +1, nullptr, reason);
          DCHECK(updated);
          request_suspension = false; // 设置不再请求挂起
        } else {
          // 如果不是请求挂起,检查已经有挂起计数
          CHECK_GT(thread->GetSuspendCount(), 0);
        }
        CHECK_NE(thread, self) << "Attempt to suspend the current thread for the debugger";
        if (thread->IsSuspended()) {
          // 如果目标线程已经是挂起状态,记录日志并返回该线程
          VLOG(threads) << "SuspendThreadByPeer thread suspended: " << *thread;
          if (ATraceEnabled()) {
            std::string name;
            thread->GetThreadName(name);
            ATraceBegin(StringPrintf("SuspendThreadByPeer suspended %s for peer=%p", name.c_str(),
                                      peer).c_str());
          }
          return thread;
        }
        // 计算从开始到现在的总延迟时间
        const uint64_t total_delay = NanoTime() - start_time;
        if (total_delay >= thread_suspend_timeout_ns_) 
          // 如果超时,则根据是否已经有挂起的线程分别处理
          if (suspended_thread == nullptr) {
            ThreadSuspendByPeerWarning(soa,
                                       ::android::base::FATAL,
                                       "Failed to issue suspend request",
                                       peer);
          } else {
            CHECK_EQ(suspended_thread, thread);
            LOG(WARNING) << "Suspended thread state_and_flags: "
                         << suspended_thread->StateAndFlagsAsHexString()
                         << ", self_suspend_count = " << self_suspend_count;
            // 记录超时警告 并发送 abort()信号终止进程。
            Locks::thread_suspend_count_lock_->Unlock(self);
            ThreadSuspendByPeerWarning(soa,
                                       ::android::base::FATAL,
                                       "Thread suspension timed out",
                                       peer);
          }
          // 标记代码不可达
          UNREACHABLE();
        } else if (sleep_us == 0 &&
            total_delay > static_cast<uint64_t>(kThreadSuspendMaxYieldUs) * 1000) 
            // 如果未设置休眠时间且延迟超过最大允许的自旋时间,设置休眠时间
          sleep_us = kThreadSuspendMaxYieldUs / 2;
        }
      }
    }
    VLOG(threads) << "SuspendThreadByPeer waiting to allow thread chance to suspend";
    // 休眠一定时间,以允许线程为机会进入挂起状态
    ThreadSuspendSleep(sleep_us);
    // 调整休眠时间,但不超过最大值
    sleep_us = std::min(sleep_us * 2, kThreadSuspendMaxSleepUs);
  }
}

上述代码每一行都加了注释,逻辑比较好理解,核心就是:

自旋等待 + 挂起标记

自旋等待特别像 Handler + Looper机制,都是采用死循环+休眠(挂起)的方式。 休眠的方式使用的是 ThreadSuspendSleep(sleep_us)来休眠的。

static void ThreadSuspendSleep(useconds_t delay_us) {
  if (delay_us == 0) {
    sched_yield(); // 如果延迟时间为0,则调用sched_yield()函数让出当前线程的CPU时间片给其他线程
  } else {
    usleep(delay_us); // 如果延迟时间不为0,则调用usleep函数使当前线程暂停执行指定的微秒数
  }
}

  • sched_yield()
#include <sched.h>
#include "syscall.h"
int sched_yield()
{
	return syscall(SYS_sched_yield);
}


让调度器放弃当前线程的剩余时间片,但它不会改变线程的状态,让出当前的CPU,立即给其他线程使用,当前的线程仍然保持在就绪状态。

syscall是一个在Linux和其他类UNIX操作系统中常见的低级函数,用于直接从用户空间发起系统调用。

  • usleep(delay_us)
#include <time.h>
#include "syscall.h"
int nanosleep(const struct timespec *req, struct timespec *rem)
{
  return syscall_cp(SYS_nanosleep, req, rem);
}

这个睡眠方式会改变线程状态,也会让出 CPU

为什么不总是使用usleep()?
  • 资源利用和响应速度: sched_yield()可以提高系统的响应速度和资源利用率。它允许当前线程主动让出CPU,但又不脱离就绪状态,这意味着一旦有执行机会,它可以立即继续执行。这在高并发环境下非常有用,可以减少等待时间和提高系统吞吐量。
  • 避免不必要的延迟: 使用usleep()意味着即使系统中没有其他线程需要运行,当前线程也必须等待指定的时间才能继续执行,这可能导致不必要的延迟。
挂起标记位

上述挂起检查中的代码,有两处重点代码都指向了同一个函数。

suspended_thread->ModifySuspendCount(self, +1, nullptr, reason);

这个suspend_thread对应的就是Thread.cc。我们去看一下:

bool Thread::ModifySuspendCountInternal(Thread* self,
                                        int delta,
                                        AtomicInteger* suspend_barrier,
                                        SuspendReason reason) {
  // 检查delta值是否合法,只能为-1或+1
  if (kIsDebugBuild) {
    DCHECK(delta == -1 || delta == +1)
          << reason << " " << delta << " " << this;
    // 确认当前线程持有线程挂起计数锁
    Locks::thread_suspend_count_lock_->AssertHeld(self);
    // 如果当前线程不是自己,并且不是处于挂起状态,确认持有线程列表锁
    if (this != self && !IsSuspended()) {
      Locks::thread_list_lock_->AssertHeld(self);
    }
  }
  // 如果挂起原因是用户代码调用,需要特别检查
  if (UNLIKELY(reason == SuspendReason::kForUserCode)) {
    // 确认持有用户代码挂起锁
    Locks::user_code_suspension_lock_->AssertHeld(self);
    // 检查挂起计数修改是否合法(不能使挂起计数变为负数)
    if (UNLIKELY(delta + tls32_.user_code_suspend_count < 0)) {
      LOG(ERROR) << "attempting to modify suspend count in an illegal way.";
      return false;
    }
  }
  // 如果减少挂起计数时已经为0或更小,记录错误并返回false
  if (UNLIKELY(delta < 0 && tls32_.suspend_count <= 0)) {
    UnsafeLogFatalForSuspendCount(self, this);
    return false;
  }

  // 如果增加挂起计数,并且当前线程不是自己,并且存在flip函数,则返回false以避免死锁
  if (delta > 0 && this != self && tlsPtr_.flip_function != nullptr) {
    return false;
  }

  uint32_t flags = enum_cast<uint32_t>(ThreadFlag::kSuspendRequest);
  // 如果增加挂起计数并指定了挂起屏障
  if (delta > 0 && suspend_barrier != nullptr) {
    uint32_t available_barrier = kMaxSuspendBarriers;
    // 查找可用的挂起屏障位置
    for (uint32_t i = 0; i < kMaxSuspendBarriers; ++i) {
      if (tlsPtr_.active_suspend_barriers[i] == nullptr) {
        available_barrier = i;
        break;
      }
    }
    // 如果没有可用的挂起屏障位置,返回false
    if (available_barrier == kMaxSuspendBarriers) {
      return false;
    }
    // 设置挂起屏障
    tlsPtr_.active_suspend_barriers[available_barrier] = suspend_barrier;
    flags |= enum_cast<uint32_t>(ThreadFlag::kActiveSuspendBarrier);
  }

  // 更新线程的挂起计数
  tls32_.suspend_count += delta;
  switch (reason) {
    case SuspendReason::kForUserCode:
      // 如果原因是用户代码,更新用户代码挂起计数
      tls32_.user_code_suspend_count += delta;
      break;
    case SuspendReason::kInternal:
      // 如果原因是内部原因,则不需要特别操作
      break;
  }

  // 如果挂起计数为0,清除挂起请求标志
  if (tls32_.suspend_count == 0) {
    AtomicClearFlag(ThreadFlag::kSuspendRequest);
  } else {
    // 如果挂起计数不为0,设置挂起请求和可能的挂起屏障标志
    tls32_.state_and_flags.fetch_or(flags, std::memory_order_seq_cst);
    TriggerSuspend();
  }
  return true;  // 返回成功
}

ModifySuspendCount()函数最终会执行ModifySuspendCountInternal(),核心在于设置挂起屏障的代码,实际上就是给 tlsPtr_ 设置一个挂起点,当 suspend_count > 0 说明当前线程需要被挂起,但是仅仅只是设置了一个标记而已,是不是非常像handler 机制中的同步屏障

什么时候执行的挂起?

这里不得不说到 Android 检查点机制,还记得我们GC流程吗? 举个例子, 当我们执行了 System.GC,一定会触发GC吗?面试老手肯定知道不是,一定要等待所有的线程都到了安全点的时候才会触发GC, 那么触发 GC 的时候需要进行 Stop the World(当然ART采用并发GC,无需所有线程都暂停),这个流程其实也涉及检查点(check point)机制。 由于不偏离本文,我们暂且可以理解为

每个线程都会定期检查自己是否有挂起请求,是否存在一个挂起标记位(kSuspendRequest),如果存在则挂起

这部分代码也比较复杂,后续会单独写一篇文章解释这里。

总结一下,我们只是将线程自身加入一个标记位,然后等待自身执行到了检查点后,检查这个标记位,如果是 kSuspendRequest,则触发挂起。

这里补充一下,真正执行挂起的代码流程,这里可能导致知识点不是很连贯,但是先写出来。

art/runtime/base/mutex.cc
void ConditionVariable::WaitHoldingLocks(Thread* self) {
  DCHECK(self == nullptr || self == Thread::Current());  // 断言:传入的线程对象要么为空,要么为当前线程
  guard_.AssertExclusiveHeld(self);  // 断言:当前线程必须独占持有锁
  unsigned int old_recursion_count = guard_.recursion_count_;  // 保存当前的递归锁计数

#if ART_USE_FUTEXES  // 如果使用futexes进行线程同步
  num_waiters_++;  // 等待者数量加一
  guard_.increment_contenders();  // 增加争用者的计数,以确保解锁时可以唤醒线程
  guard_.recursion_count_ = 1;  // 设置递归锁计数为1
  int32_t cur_sequence = sequence_.load(std::memory_order_relaxed);  // 获取当前的序列号,用于futex操作
  guard_.ExclusiveUnlock(self);  // 释放锁,以便其他线程可以进入临界区

  // FUTEX_WAIT_PRIVATE:等待条件变量,只对当前进程内部的线程可见
  if (futex(sequence_.Address(), FUTEX_WAIT_PRIVATE, cur_sequence, nullptr, nullptr, 0) != 0) {
    // 如果futex调用失败
    if ((errno != EINTR) && (errno != EAGAIN)) {  // 如果错误既不是中断也不是无法立即阻塞
      PLOG(FATAL) << "futex wait failed for " << name_;  // 记录致命错误日志
    }
  }
  SleepIfRuntimeDeleted(self);  // 检查运行时是否已删除,如果是,则使线程休眠
  guard_.ExclusiveLock(self);  // 重新获得锁
  CHECK_GT(num_waiters_, 0);  // 检查等待者计数是否大于0
  num_waiters_--;  // 等待者数量减一
  CHECK_GT(guard_.get_contenders(), 0);  // 检查争用者计数是否大于0
  guard_.decrement_contenders();  // 减少争用者计数

#else  // 如果不使用futexes,使用传统的pthread条件变量
  pid_t old_owner = guard_.GetExclusiveOwnerTid();  // 获取当前持有锁的线程ID
  guard_.exclusive_owner_.store(0 /* pid */, std::memory_order_relaxed);  // 清除持有者
  guard_.recursion_count_ = 0;  // 清零递归锁计数
  CHECK_MUTEX_CALL(pthread_cond_wait, (&cond_, &guard_.mutex_));  // 等待pthread条件变量
  guard_.exclusive_owner_.store(old_owner, std::memory_order_relaxed);  // 恢复持有者
#endif
  guard_.recursion_count_ = old_recursion_count;  // 恢复原来的递归锁计数
}


挂起超时原因

ok, 我们已经知道我们虽然执行了ModifySuspendCount()函数,但是还没有真正执行挂起的操作,等到检查点检测到 KSuspendRequest标记的时候,才会真正的执行挂起,而超时就是因为检查点执行超时。

因为这些检测触发的时机通常是在不会影响程序状态的位置,如方法调用、循环迭代末尾或返回之前,可能存在这些位置迟迟没有执行到导致检查点检测被推迟。

如何修复崩溃?

由于超时了,系统会打一个日志:

ThreadSuspendByPeerWarning(soa, ::android::base::FATAL, "Thread suspension timed out", peer);

static void ThreadSuspendByPeerWarning(ScopedObjectAccess& soa,
                                       LogSeverity severity,
                                       const char* message,
                                       jobject peer) REQUIRES_SHARED(Locks::mutator_lock_) {
  ObjPtr<mirror::Object> name =
      WellKnownClasses::java_lang_Thread_name->GetObject(soa.Decode<mirror::Object>(peer));
  if (name == nullptr) {
    LOG(severity) << message << ": " << peer;
  } else {
    LOG(severity) << message << ": " << peer << ":" << name->AsString()->ToModifiedUtf8();
  }
}

这个日志级别是::android::base::FATAL,最终会发射一个abort(),使得进程终止。

由于不可能一个一个去检查崩溃线程为什么推迟检查点,所以只能找些其他办法,所以最后的方案就是直接 hook 这个 ThreadSuspendByPeerWarning() 函数,调用前将LogSeverity的级别从 FATAL 改为 INFO 或者 warning.

示例代码

#include <jni.h>
#include "sys_stub.h"

// Function signatures updated for readability
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_14 "_ZN3artL26ThreadSuspendByPeerWarningERNS_18ScopedObjectAccessEN7android4base11LogSeverityEPKcP8_jobject"
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_8_13 "_ZN3artL26ThreadSuspendByPeerWarningEPNS_6ThreadEN7android4base11LogSeverityEPKcP8_jobject"
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_6_7 "_ZN3artL26ThreadSuspendByPeerWarningEPNS_6ThreadENS_11LogSeverityEPKcP8_jobject"
#define SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_5 "_ZN3artL26ThreadSuspendByPeerWarningEPNS_6ThreadEiPKcP8_jobject"

LogSeverity ToLogSeverity(int logLevel) {
    switch (logLevel) {
        case 0:
            return VERBOSE;
        case 1:
            return DEBUG;
        case 2:
            return INFO;
        case 3:
            return WARNING;
        case 4:
            return ERROR;
        case 5:
            return FATAL_WITHOUT_ABORT;
        case 6:
            return FATAL;
        default:
            return INFO;
    }
}

const char *getThreadSuspendByPeerWarningFunctionName() {
    int apiLevel = android_get_device_api_level();
    // Simplified logic based on Android API levels
    if (apiLevel < 23){
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_5;
    } else if (apiLevel < 26) {
        // below android 8
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_6_7;
    } else if (apiLevel < 34) {
        // above android 8 and below android 14
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_8_13;
    } else {
        // android 14+
        return SYMBOL_THREAD_SUSPEND_BY_PEER_WARNING_14;
    }
}

#include <jni.h>
#include <string>
#include <shadowhook.h> // 字节shadowhook的头文件,用于在运行时钩子(hook)函数
#include <android/log.h>
#include <pthread.h>
#include "sys_stub.h"
#include <android/api-level.h>

#define TARGET_ART_LIB "libart.so"
#define LOG_TAG "thread_suspend_hook"

namespace hookThreadSuspendAbort {
    JavaVM *gVm = nullptr; // 全局的Java虚拟机指针
    jobject callbackObj = nullptr; // 全局引用,指向Java层的回调对象

    std::atomic<LogSeverity> m_severity{INFO}; // 日志严重性级别的原子变量,默认为INFO

    void *originalFunction = nullptr; // 指向原始函数的指针
    void *stubFunction = nullptr; // 指向存根函数的指针

    typedef void (*ThreadSuspendByPeerWarning)(void *self, LogSeverity severity,
                                               const char *message, jobject peer); // 函数指针类型定义

    void triggerSuspendTimeout();

    JNIEnv *getJNIEnv(); // 获取JNIEnv的函数声明

    void hookPointFailed(const char *msg); // 钩子设置失败时的处理函数

    void cleanup(JNIEnv *env); // 清理资源的函数

    // Hook 函数实现,替换原始函数
    void threadSuspendByPeerWarning(void *self, LogSeverity severity, const char *message,
                                    jobject peer) {
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hooked point success : %s", message);
        if (severity == FATAL && strcmp(message, SUSPEND_LOG_MSG) == 0) {
            // 如果当前是 FATAL 并且 message 是 Thread suspend timeout 则设置一个非FATAL级别的。
            severity = m_severity.load();
            triggerSuspendTimeout();
        }
        ((ThreadSuspendByPeerWarning) originalFunction)(self, severity, message, peer);
    }

    void maskThreadSuspendTimeout(void *self, LogSeverity severity, const char *message, jobject peer) {
        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hooked point success : %s", message);
        if (severity == FATAL && strcmp(message, SUSPEND_LOG_MSG) == 0) {
            // 如果当前是 FATAL 并且 message 是 Thread suspend timeout 则不调用原始函数
            triggerSuspendTimeout();
        }
    }

    void setLogLevel(LogSeverity severity) {
        m_severity.store(severity);
    }

    void releaseHook(); // 释放钩子的函数

    void prepareSetSuspendTimeoutLevel() { // 准备设置挂起超时级别的函数
        releaseHook();
        stubFunction = shadowhook_hook_sym_name(TARGET_ART_LIB,
                                                getThreadSuspendByPeerWarningFunctionName(),
                                                (void *) threadSuspendByPeerWarning,
                                                (void **) &originalFunction);
        if (stubFunction == nullptr) {
            const int err_num = shadowhook_get_errno();
            const char *errMsg = shadowhook_to_errmsg(err_num);
            if (errMsg == nullptr || callbackObj == nullptr) {
                return;
            }
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup failed: %s", errMsg);
            hookPointFailed(errMsg);
            delete errMsg;
        } else {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup success");
        }
    }

    void preparedMaskThreadTimeoutAbort() {
        releaseHook();
        stubFunction = shadowhook_hook_sym_name(TARGET_ART_LIB,
                                                getThreadSuspendByPeerWarningFunctionName(),
                                                (void *) maskThreadSuspendTimeout,
                                                (void **) &originalFunction);
        if (stubFunction == nullptr) {
            const int err_num = shadowhook_get_errno();
            const char *errMsg = shadowhook_to_errmsg(err_num);
            if (errMsg == nullptr || callbackObj == nullptr) {
                return;
            }
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup failed: %s", errMsg);
            hookPointFailed(errMsg);
            delete errMsg;
        } else {
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Hook setup success");
        }
    }

    void releaseHook() { 
        // 实现释放钩子的功能
        if (stubFunction != nullptr) {
            shadowhook_unhook(stubFunction);
            stubFunction = nullptr;
        }
    }

    void cleanup(JNIEnv *env) { 
        // 清理全局引用和分离线程
        if (callbackObj) {
            env->DeleteGlobalRef(callbackObj);
            callbackObj = nullptr;
        }
        if (gVm->DetachCurrentThread() != JNI_OK) {
            __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "Could not detach current thread.");
        }
    }

    JNIEnv *getJNIEnv() { 
        // 实现获取JNIEnv指针的功能
        JNIEnv *env = nullptr;
        if (gVm == nullptr) {
            return nullptr;
        }
        jint result = gVm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
        if (result == JNI_EDETACHED) {
            if (gVm->AttachCurrentThread(&env, nullptr) != 0) {
                return nullptr;
            }
        } else if (result != JNI_OK) {
            return nullptr;
        }
        return env;
    }

    void hookPointFailed(const char *errMsg) { 
        // 处理钩子设置失败的情况
        JNIEnv *pEnv = getJNIEnv();
        if (pEnv == nullptr) {
            return;
        }
        jclass jThreadHookClass = pEnv->FindClass(
                "com/thread_hook/ThreadSuspendTimeoutCallback");
        if (jThreadHookClass != nullptr) {
            jmethodID jMethodId = pEnv->GetMethodID(jThreadHookClass, "onError",
                                                    "(Ljava/lang/String;)V");
            if (jMethodId != nullptr) {
                pEnv->CallVoidMethod(callbackObj, jMethodId, pEnv->NewStringUTF(errMsg));
            }
        }
        cleanup(pEnv);
    }

    void triggerSuspendTimeout() { 
        // 触发挂起超时处理
        JNIEnv *pEnv = getJNIEnv();
        if (pEnv == nullptr) {
            return;
        }
        jclass jThreadHookClass = pEnv->FindClass(
                "com/thread_hook/ThreadSuspendTimeoutCallback");
        if (jThreadHookClass != nullptr) {
            jmethodID jMethodId = pEnv->GetMethodID(jThreadHookClass, "triggerSuspendTimeout",
                                                    "()V");
            if (jMethodId != nullptr) {
                pEnv->CallVoidMethod(callbackObj, jMethodId);
            }
        }
    }
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { 
    // JNI入口点,初始化JavaVM指针
    using namespace hookThreadSuspendAbort;
    gVm = vm;
    return JNI_VERSION_1_6;
}

extern "C" JNIEXPORT void JNICALL
Java_com_thread_1hook_ThreadHook_setNativeThreadSuspendTimeoutLogLevel(JNIEnv *env,
                                                                                   jobject,
                                                                                   int logLevel,
                                                                                   jobject callback) {
    using namespace hookThreadSuspendAbort;
    if (callbackObj != nullptr) {
        env->DeleteGlobalRef(callbackObj);
    }
    callbackObj = env->NewGlobalRef(callback);
    setLogLevel(ToLogSeverity(logLevel)); // 设置日志级别
    prepareSetSuspendTimeoutLevel();
}


extern "C" JNIEXPORT void JNICALL
Java_com_thread_1hook_ThreadHook_maskNativeThreadSuspendTimeoutAbort(JNIEnv *env,
                                                                                 jobject /*this*/,
                                                                                 jobject callback) {
    using namespace hookThreadSuspendAbort;
    if (callbackObj != nullptr) {
        env->DeleteGlobalRef(callbackObj);
    }
    callbackObj = env->NewGlobalRef(callback);
    preparedMaskThreadTimeoutAbort();
}


比较复杂的是,多版本兼容问题,钩子函数的mangling name有变化,需要多适配一下。

如何测试生效?

由于本身这个问题不好复现,我们只能采取通过mock代码在某个时机去直接执行ThreadSuspendByPeerWarning()函数。

#include <jni.h>
#include <shadowhook.h>
#include <dlfcn.h>
#include <android/log.h>
#include "sys_stub.h"

#define TARGET_ART_LIB "libart.so"
#define LOG_TAG "suspend_hook_test"

namespace suspend_hook_test {


    typedef void (*ThreadSuspendByPeerWarning)(void *self,
                                               enum LogSeverity severity,
                                               const char *message,
                                               jobject peer);


    extern "C" JNIEXPORT
    void JNICALL
    Java_com_thread_1hook_ThreadHook_callNativeThreadSuspendTimeout(JNIEnv *env,
                                                                                jobject javaThread /* this */,
                                                                                jlong nativePeer,
                                                                                jobject peer) {
        void *handle = shadowhook_dlopen(TARGET_ART_LIB);
        auto hookPointFunc = (ThreadSuspendByPeerWarning) shadowhook_dlsym(handle,
                                                                           getThreadSuspendByPeerWarningFunctionName());
        if (hookPointFunc != nullptr) {
            void *child_thread = reinterpret_cast<void *>(nativePeer);
            // only 14 worked for test.
            __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "thread_point : %p", child_thread);
            hookPointFunc(child_thread, FATAL, SUSPEND_LOG_MSG, peer);
        } else {
            __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "ELF symbol not found!");
        }
    }
}

如上述代码,dlsym 去拿到句柄,直接执行对应的函数。这里有个注意:

在应用侧触发 mock 函数的时候,需要先通过反射拿到 Thread 中的 nativePeer,它对应的是 nativeThread.cc 的地址。

object Utils {
    fun getNativePeer(thread: Thread): Long? {
        try {
            val threadClass = Class.forName("java.lang.Thread")
            val nativePeerField: Field = threadClass.getDeclaredField("nativePeer")
            nativePeerField.isAccessible = true
            return nativePeerField.getLong(thread)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
        return null
    }
}

thread {
    myThread = thread(name = "EdisonLi-init-name") {
        callThreadSuspendTimeout(myThread!!)
        while (true) {
            // Log.d("EdisonLi",  this@SecondActivity.myThread?.name.toString())
        }
    }
    while (true) {
        Thread.sleep(1000)
        myThread?.name = "Thread-${Random.nextLong(1, 1000)}"
        break
    }
}

并且 callThreadSuspendTimeout(myThread!!) 一定要让被修改名称的线程去调用哦!不然会报错。 ok,经测试在Android14中,执行这个函数以后,不会被 abort()信号终止进程。

总结

虽然当前的解决方案可以减少由线程挂起导致的Native Crash,但仍需要进一步研究一下线程和协程的管理策略(不知道是否存在使用姿势问题>_<),以彻底解决问题并提高系统的稳定性和性能。

通过这次深入分析,我们不仅解决了一个长期存在的问题,还增强了对Android底层线程管理机制的理解,这将有助于未来更好地处理类似问题。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Android原生崩溃android native crash)是指在Android平台上,由于代码执行错误或者资源耗尽等原因,导致应用程序无法正常运行而崩溃或者闪退的现象。原生崩溃产生的原因可能是由于C或者C++代码编写错误、内存溢出、线程竞争等。针对原生崩溃问题开发人员需要使用调试工具进行定位和修复。 ### 回答2: Android Native Crash发生在安卓应用程序运行时,由于C或C++库的错误或者其他原因导致应用程序崩溃。有时候Native Crash可能会影响整个设备,尤其是当Native Crash发生在系统级别的代码中时。 产生Native Crash的原因通常包括以下几个方面: 1. 内存管理问题Native Crash通常与内存管理问题相关,这可能是由于访问未初始化的内存,使用错误的指针或释放已释放的内存等原因引起的。 2. 硬件问题Native Crash也可能与设备相关的硬件问题有关,例如访问不可用的硬件资源或硬件设备故障。 3. 应用程序代码问题Native Crash可能发生在应用程序代码的错误、资源泄漏、堆栈溢出等问题引起的。 4. 第三方库问题Native Crash也可能由第三方库中的错误或bug引起。这些库可能没有经过充分的测试,或者与设备硬件不兼容。 为了更好地解决Native Crash问题开发者可以通过日志或崩溃报告(Crash Report)来检测和分析崩溃日志,并查看堆栈跟踪信息来确定导致Native Crash的来源。在开发过程中,经常使用除了自己编写的代码之外的第三方库时,还可以考虑使用崩溃的回溯工具,如Firebase Crashlytics等。 总之,Native CrashAndroid应用程序开发过程中经常遇到的问题,它可能会对用户体验和开发进度产生重大影响,因此开发者需要强化对Native Crash的理解和分析能力,以更好地解决Native Crash问题。 ### 回答3: Android Native Crash,指的是在 Android 系统中发生的本地崩溃。本地崩溃是指应用程序使用本地代码,而不是 Java 代码,导致应用程序崩溃问题。本地代码可以是编写在 C/C++ 等语言中的库,或是应用程序本身所编写的 Native 代码。 本地代码崩溃后,会在应用程序崩溃的同时发生。本地崩溃可发生在 Android 应用程序中的任何部分,比如,应用程序本地库、Android 系统库等等。大多数情况下,本地崩溃是由于访问无效内存、访问不合法指针、数组越界等问题引起的。 为了解决本地崩溃问题Android 提供了一些工具和技术。比如,使用 ndk-stack 工具可以解析本地崩溃日志。Android Studio 也提供了一些工具来分析应用程序崩溃的原因。同时,我们也可以在应用程序中添加自定义的日志跟踪信息,以便更好地了解应用程序的崩溃原因。 还有一些其他的技术可以使用,如使用 Google 的 Crashlytics 来跟踪应用程序的崩溃问题。这个平台可以帮助开发者收集和分析应用程序在用户设备上的崩溃信息,并彻底解决这些问题。此外,Android 还提供了一些实用工具和技术,如 ANR(Application Not Responding)错误处理器、Tracer for OpenGL ES 和 Traceview 示例等。 总之,Android Native CrashAndroid 系统中常见的崩溃问题之一。了解它的原因并采用适当的解决方案可以使得我们更好地保持我们的应用程序的稳定性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值