Android效能優化 - 捕獲java crash的那些事

語言: CN / TW / HK

本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

背景

crash一直是影響app穩定性的大頭,同時在隨著專案逐漸迭代,複雜性越來越提高的同時,由於主觀或者客觀的的原因,都會造成意想不到的crash出現。同樣的,在android的歷史化過程中,就算是android系統本身,在迭代中也會存在著隱含的crash。我們常說的crash包括java層(虛擬機器層)crash與native層crash,本期我們著重講一下java層的crash。

java層crash由來

雖然說我們在開發過程中會遇到各種各樣的crash,但是這個crash是如果產生的呢?我們來探討一下一個crash是如何誕生的!

我們很容易就知道,在java中main函式是程式的開始(其實還有前置步驟),我們開發中,雖然android系統把應用的主執行緒建立封裝在了自己的系統中,但是無論怎麼封裝,一個java層的執行緒無論再怎麼強大,背後肯定是綁定了一個作業系統級別的執行緒,才真正得與驅動,也就是說,我們平常說的java執行緒,它其實是被作業系統真正的Thread的一個使用體罷了,java層的多個thread,可能會只對應著native層的一個Thread(便於區分,這裡thread統一隻java層的執行緒,Thread指的是native層的Thread。其實native的Thread也不是真正的執行緒,只是作業系統提供的一個api罷了,但是我們這裡先簡單這樣定義,假設了native的執行緒與作業系統執行緒為同一個東西)

每一個java層的thread呼叫start方法,就會來到native層Thread的世界 ``` public synchronized void start() { throw new IllegalThreadStateException(); group.add(this);

started = false;
try {
    nativeCreate(this, stackSize, daemon);
    started = true;
} finally {
    try {
        if (!started) {
            group.threadStartFailed(this);
        }
    } catch (Throwable ignore) {
        /* do nothing. If start0 threw a Throwable then
          it will be passed up the call stack */
    }
}

} 最終呼叫的是一個jni方法 private native static void nativeCreate(Thread t, long stackSize, boolean daemon); ``` 而nativeCreate最終在native層的實現是

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, jboolean daemon) { // There are sections in the zygote that forbid thread creation. Runtime* runtime = Runtime::Current(); if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) { jclass internal_error = env->FindClass("java/lang/InternalError"); CHECK(internal_error != nullptr); env->ThrowNew(internal_error, "Cannot create threads in zygote"); return; } // 這裡就是真正的建立執行緒方法 Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE); } CreateNativeThread 經過了一系列的校驗動作,終於到了真正建立執行緒的地方了,最終在CreateNativeThread方法中,通過了pthread_create建立了一個真正的Thread ``` Thread::CreateNativeThread 方法中 ... pthread_create_result = pthread_create(&new_pthread, &attr, Thread::CreateCallback, child_thread); CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");

if (pthread_create_result == 0) { // pthread_create started the new thread. The child is now responsible for managing the // JNIEnvExt we created. // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization // between the threads. child_jni_env_ext.release(); // NOLINT pthreads API. return; } ... 到這裡我們就能夠明白,一個java層的thread其實真正繫結的,是一個native層的Thread,有了這個知識,我們就可以回到我們的crash主題了,當發生異常的時候(即檢測到一些操作不符合虛擬機器規定時),注意,這個時候還是在虛擬機器的控制範圍之內,就可以直接呼叫 void Thread::ThrowNewException(const char exception_class_descriptor, const char msg) { // Callers should either clear or call ThrowNewWrappedException. AssertNoPendingExceptionForNewException(msg); ThrowNewWrappedException(exception_class_descriptor, msg); } ``` 進行對exception的丟擲,我們目前所有的java層crash都是如此,因為對crash的識別還屬於本虛擬機器所在的程序的範疇(native crash 虛擬機器就沒辦法直接識別),比如我們常見的各種crash

image.png

然後就會呼叫到Thread::ThrowNewWrappedException 方法,在這個方法裡面再次呼叫到Thread::SetException方法,成功的把當次引發異常的資訊記錄下來 void Thread::SetException(ObjPtr<mirror::Throwable> new_exception) { CHECK(new_exception != nullptr); // TODO: DCHECK(!IsExceptionPending()); tlsPtr_.exception = new_exception.Ptr(); } 此時,此時就會呼叫Thread的Destroy方法,這個時候,執行緒就會在裡面判斷,本次的異常該怎麼去處理 ``` void Thread::Destroy() { ...

if (tlsPtr_.opeer != nullptr) {
    ScopedObjectAccess soa(self);
    // We may need to call user-supplied managed code, do this before final clean-up.
    HandleUncaughtExceptions(soa);
    RemoveFromThreadGroup(soa);
    Runtime* runtime = Runtime::Current();
    if (runtime != nullptr) {
            runtime->GetRuntimeCallbacks()->ThreadDeath(self);
    }

HandleUncaughtExceptions 這個方式就是處理的函式,我們繼續看一下這個異常處理函式 void Thread::HandleUncaughtExceptions(ScopedObjectAccessAlreadyRunnable& soa) { if (!IsExceptionPending()) { return; } ScopedLocalRef peer(tlsPtr_.jni_env, soa.AddLocalReference(tlsPtr_.opeer)); ScopedThreadStateChange tsc(this, ThreadState::kNative);

// Get and clear the exception.
ScopedLocalRef<jthrowable> exception(tlsPtr_.jni_env, tlsPtr_.jni_env->ExceptionOccurred());
tlsPtr_.jni_env->ExceptionClear();

// Call the Thread instance's dispatchUncaughtException(Throwable)
// 關鍵點就在此,回到java層
tlsPtr_.jni_env->CallVoidMethod(peer.get(),
WellKnownClasses::java_lang_Thread_dispatchUncaughtException,
exception.get());

// If the dispatchUncaughtException threw, clear that exception too.
tlsPtr_.jni_env->ExceptionClear();

} 到這裡,我們就接近尾聲了,可以看到我們的處理函式最終通過jni,再次回到了java層的世界,而這個連線的java層函式就是dispatchUncaughtException(java_lang_Thread_dispatchUncaughtException) public final void dispatchUncaughtException(Throwable e) { // BEGIN Android-added: uncaughtExceptionPreHandler for use by platform. Thread.UncaughtExceptionHandler initialUeh = Thread.getUncaughtExceptionPreHandler(); if (initialUeh != null) { try { initialUeh.uncaughtException(this, e); } catch (RuntimeException | Error ignored) { // Throwables thrown by the initial handler are ignored } } // END Android-added: uncaughtExceptionPreHandler for use by platform. getUncaughtExceptionHandler().uncaughtException(this, e); } ``` 到這裡,我們就徹底瞭解到了一個java層異常的產生過程!

為什麼java層異常會導致crash

從上面我們文章我們能夠看到,一個異常是怎麼產生的,可能細心的讀者會了解到,筆者一直在用異常這個詞,而不是crash,因為異常發生了,crash是不一定產生的!我們可以看到dispatchUncaughtException方法最終會嘗試著呼叫UncaughtExceptionHandler去處理本次異常,好傢伙!那麼UncaughtExceptionHandler是在什麼時候設定的?其實就是在Init中,由系統提前設定好的!frameworks/base/core/java/com/android/internal/os/RuntimeInit.java ``` protected static final void commonInit() { if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

/*
 * set handlers; these apply to all threads in the VM. Apps can replace
 * the default handler, but not the pre handler.
 */
LoggingHandler loggingHandler = new LoggingHandler();
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

/*
 * Install a time zone supplier that uses the Android persistent time zone system property.
 */
RuntimeHooks.setTimeZoneIdSupplier(() -> SystemProperties.get("persist.sys.timezone"));

LogManager.getLogManager().reset();
new AndroidConfig();

/*
 * Sets the default HTTP User-Agent used by HttpURLConnection.
 */
String userAgent = getDefaultUserAgent();
System.setProperty("http.agent", userAgent);

/*
 * Wire socket tagging to traffic stats.
 */
TrafficStats.attachSocketTagger();

initialized = true;

} ``` 好傢伙,原來是KillApplicationHandler“搗蛋”,在異常到來時,就會通過KillApplicationHandler去處理,而這裡的處理就是,殺死app!!

```

private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler { private final LoggingHandler mLoggingHandler; public KillApplicationHandler(LoggingHandler loggingHandler) { this.mLoggingHandler = Objects.requireNonNull(loggingHandler); }

@Override
public void uncaughtException(Thread t, Throwable e) {
    try {
        ensureLogging(t, e);

        // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
        if (mCrashing) return;
        mCrashing = true;

        if (ActivityThread.currentActivityThread() != null) {
            ActivityThread.currentActivityThread().stopProfiling();
        }

        // Bring up crash dialog, wait for it to be dismissed
        ActivityManager.getService().handleApplicationCrash(
                mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
    } catch (Throwable t2) {
        if (t2 instanceof DeadObjectException) {
            // System process is dead; ignore
        } else {
            try {
                Clog_e(TAG, "Error reporting crash", t2);
            } catch (Throwable t3) {
                // Even Clog_e() fails!  Oh well.
            }
        }
    } finally {
        // Try everything to make sure this process goes away.
        Process.killProcess(Process.myPid());
        System.exit(10);
    }
}


private void ensureLogging(Thread t, Throwable e) {
    if (!mLoggingHandler.mTriggered) {
        try {
            mLoggingHandler.uncaughtException(t, e);
        } catch (Throwable loggingThrowable) {
            // Ignored.
        }
    }
}

} ``` 看到了嗎!異常的產生導致的crash,真正的源頭就是在此了!

捕獲crash

通過對前文的閱讀,我們瞭解到了crash的源頭就是KillApplicationHandler,因為它預設處理就是殺死app,此時我們也注意到,它是繼承於UncaughtExceptionHandler的。當然,有異常及時丟擲解決,是一件好事,但是我們也可能有一些異常,比如android系統sdk的問題,或者其他沒那麼重要的異常,直接崩潰app,這個處理就不是那麼好了。但是不要緊,java虛擬機器開發者也肯定注意到了這點,所以提供 ``` Thread.java

public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) 方式,匯入一個我們自定義的實現了UncaughtExceptionHandler介面的類 public interface UncaughtExceptionHandler { /* * Method invoked when the given thread terminates due to the * given uncaught exception. *

Any exception thrown by this method will be ignored by the * Java Virtual Machine. * @param t the thread * @param e the exception / void uncaughtException(Thread t, Throwable e); } 此時我們只需要寫一個類,模仿KillApplicationHandler一樣,就能寫出一個自己的異常處理類,去處理我們程式中的異常(或者Android系統中特定版本的異常)。例子demo比如 class MyExceptionHandler:Thread.UncaughtExceptionHandler { override fun uncaughtException(t: Thread, e: Throwable) { // 做自己的邏輯 Log.i("hello",e.toString()) } } ```

總結

到這裡,我們能夠了解到了一個java crash是怎麼產生的了,同時我們也瞭解到了常用的UncaughtExceptionHandler為什麼可以攔截一些我們不希望產生crash的異常,在接下來的android效能優化系列中,會持續帶來相關的其他分享,感謝觀看