动态

详情 返回 返回

你的debug包在Android 14變卡了嗎?|得物技術 - 动态 详情

一、背景

我的App怎麼這麼卡,誰在代碼裏下毒了!

有一天突然發現debug包運行變的特別卡頓,經過下面的簡單測試發現debug包在Android 14上出了問題。 

image.png

二、問題排查紀錄

常規手段排查

使用了systrace以及內部的debug包 trace工具dutrace進行排查。

結論:CPU空閒,主線程無明顯阻塞,看上去就是純方法執行耗時。

發現懷疑點

第一步排查過程中沒有特別大的收穫,但是我用dutrace工具排查時發現了一個異常現象。這裏簡單介紹一下dutrace的實現原理:

dutrace是利用inline hook在artmethod的執行前後加上atrace的點再通過perfetto ui工具展示。有以下優點:

    1. 支持線下分析函數執行流程,函數耗時。

    2. 在分析函數調用流程下:

        a. 可以查看整個過程的函數調用(包括framework函數);

        b. 能夠指定監控的函數和線程有效過濾無用trace;

        c. 動態配置不需要重新打包。

    3. 可使用現成的UI分析工具,有系統關鍵線程的函數調用,例如渲染耗時、線程鎖,GC 耗時等,還有 I/O 操作、CPU 負載等事件。

image.png

在對artmethod執行前後進行hook時 這裏涉及到處理art方法解釋執行的三種情況。

ART Runtime 解釋器

  1. The C++ interpreter,也就是傳統的基於switch結構的解釋器,一般僅在調試環境、方法跟蹤、指令不支持或者在字節碼發生異常情況下(例如failed structured-locking verification)才走該分支。
  2. The mterp fast interpreter,核心是引入了handler table做指令映射,並通過手寫彙編以實現指令間的快速切換,提高了解釋器性能。
  3. Nterp是Mterp的再次優化。Nterp省去了managed code stacks的維護,採用了和Native方法一樣的棧幀結構,並且譯碼和翻譯執行全程都由彙編代碼實現,進一步拉進解釋器和compiled code的性能差距。

在這邊我發現了一個異常現象,就是Android 14的解釋執行居然都用的switch解釋執行方式。我又重新去測試了幾個Android 版本的解釋執行方式。Android 12走的mterp,Android 13走的是nterp,當進行調試的時候才會走到switch, 理論上Android 14應該也走nterp才對,怎麼會走了最慢的switch呢。以下按順序是12、13、14版本的方法執行backtrace。
image.pngimage.pngimage.png

排查懷疑點

開始懷疑是解釋執行導致的卡頓了,翻了下源碼 art/runtime/interpreter/mterp/nterp.cc 中確實有變動 如果是javaDebuggable 就不走nterp了。接下來嘗試去證明是是這個問題導致的。

image.png
isJavaDebuggable 是runtime.cc中的 RuntimeDebugState runtime_debug_state_ 中控制的。我們可以找到runtime的實例然後通過偏移量修改過runtime_debug_state_屬性,看了下源碼還可以通過_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE 進行設置。 

void Runtime::SetRuntimeDebugState(RuntimeDebugState state) { 
  if (state != RuntimeDebugState::kJavaDebuggableAtInit) {    
     // We never change the state if we started as a debuggable runtime.    
     DCHECK(runtime_debug_state_ != RuntimeDebugState::kJavaDebuggableAtInit);  
   }  
   runtime_debug_state_ = state;
}

我通過上述方式去進行嘗試驗證 把測試包的 isJavaDebuggable 設置為false 依然卡頓,把生產包的isJavaDebuggable設置為true,變得稍微卡了點。於是我推翻了自己解釋執行方式導致卡頓的猜想。

排查native耗時

懷疑nativie方法執行耗時, 再次嘗試用simpleperf定位問題。

結論:基本都是解釋執行代碼中的堆棧耗時,沒有其他特殊堆棧。

image.png
定位到DEBUG_JAVA_DEBUGGABLE

那就想着從debuggable的源頭入手,逐步縮小範圍定位影響變量。

AndroidManifest中的debuggable影響系統system進程啓動我們進程中的一個runtimeFlags。

frameworks/base/core/java/android/os/Process.java 中的start方法 其中第6個參數就是runtimeFlags而如果是debuggableFlag runtimeFlags會被添加以下一些flag 那就先縮小標籤範圍。

 if (debuggableFlag) {
                runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;                
                runtimeFlags |= Zygote.DEBUG_ENABLE_PTRACE;                
                runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;                
                // Also turn on CheckJNI for debuggable apps. It's quite                
                // awkward to turn on otherwise.                
                runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;
                
                // Check if the developer does not want ART verification                
                if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(),                        
                        android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {                    
                    runtimeFlags |= Zygote.DISABLE_VERIFIER;                    
                    Slog.w(TAG_PROCESSES, app + ": ART verification disabled");                
                 }            
              }

需要修改我們進程的啓動參數。那就需要去hook system進程了。這邊涉及到手機root,安裝hook框架的一些操作,然後通過hook Process的start去做一些參數修改。

hookAllMethods(
        Process.class,        
        "start",        
        new XC_MethodHook() {            
            @Override            
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {                
                final String niceName = (String) param.args[1];                
                final int uid = (int) param.args[2];                
                final int runtimeFlags = (int) param.args[5];                
                XposedBridge.log("process_xx " + runtimeFlags);                
                if (isDebuggable(niceName, user)) {                    
                    param.args[5] = runtimeFlags&~DEBUG_JAVA_DEBUGGABLE;                    
                    XposedBridge.log("process_xx " + param.args[5]);
                
                }            
             }        
          }
);

這次還是有一些明顯的結果的。測試包 runtimeflags 移除DEBUG_JAVA_DEBUGGABLE後不卡了。而生產包包括應用市場上的應用加上DEBUG_JAVA_DEBUGGABLE標記後全部都變卡了。那就可以證明是DEBUG_JAVA_DEBUGGABLE這個變量引起的。

定位到

DeoptimizeBootImage

繼續源碼觀察DEBUG_JAVA_DEBUGGABLE帶來的影響。

if ((runtime_flags & DEBUG_JAVA_DEBUGGABLE) != 0) {    
    runtime->AddCompilerOption("--debuggable");    
    runtime_flags |= DEBUG_GENERATE_MINI_DEBUG_INFO;    
    runtime->SetRuntimeDebugState(Runtime::RuntimeDebugState::kJavaDebuggableAtInit);    
    {      
      // Deoptimize the boot image as it may be non-debuggable.      
      ScopedSuspendAll ssa(__FUNCTION__);      
      runtime->DeoptimizeBootImage();    
      }    
      runtime_flags &= ~DEBUG_JAVA_DEBUGGABLE;    
      needs_non_debuggable_classes = true;  
    }

這裏有邏輯是DEBUG_JAVA_DEBUGGABLE帶來的影響點,SetRuntimeDebugState之前已經測試過了。也不是DEBUG_GENERATE_MINI_DEBUG_INFO帶來的影響,那是runtime->DeoptimizeBootImage()?於是我用debugable為false的包通過_ZN3art7Runtime19DeoptimizeBootImageEv主動去調用了DeoptimizeBootImage方法,然後復現了!

原因分析

DeoptimizeBootImage 將bootImage中AOT代碼方法轉換為java可調試。重新初始化方法入口點,走到解釋執行,而不使用AOT代碼。追溯到Instrumentation::InitializeMethodsCode方法,還是到了CanUseNterp(method) CanRuntimeUseNterp這個點。也是Android 13可以用nterp,android 14只能走switch了。

我再次hook代碼,讓CanRuntimeUseNterp 直接return true, 但是還是卡。我發現即使我hook了。下面的這些方法還是走到了switch解釋執行。反過來想一想是因為我hook已經滯後了DeoptimizeBootImage已經執行了,當調用到基礎方法的時候都是switch執行了。

image.png

我用Android 13 debugable true的包進行測試先hook CanRuntimeUseNterp return false,然後再執行DeoptimizeBootImage,復現卡頓 。

初步定位: bootimage中的方法 Android 13走的nterp而Android 14走的switch  bootimage裏面的方法特別基礎和零碎所以導致方法switch執行耗時嚴重。

驗證是系統問題

如果是系統問題,那大家都應該遇到的,不只我們App有這個問題, 於是我找到了幾個小夥伴幫忙驗證debug包這個問題。果然都有這個問題,同一個包安裝在Android 14 和 Android 13上體驗完全不一致。

反饋問題

在issuetracker上已經有人反饋android 14 debug包慢了 https://issuetracker.google.com/issues/311251587。但是還沒有結果,於是我補上了我定位到的問題。

image.png
順便也提了個issue https://issuetracker.google.com/issues/328477628

三、臨時解決

在等Google回覆的同時,也同時在思考App層可以有什麼辦法去規避這個問題,讓debug包的體驗也迴歸絲滑,比如如何去重新optimize bootimage中的方法。抱着這個想法又去學習了一下art的代碼,發現Android 14新增了一個UpdateEntrypointsForDebuggable方法,這個方法會去按照規則重新設置方法的執行方式比如aot和nterp,那我在這之前把CanRuntimeUseNterp hook了返回true 再去調用UpdateEntrypointsForDebuggable不就會重新走到nterp了嗎。  

void Instrumentation::UpdateEntrypointsForDebuggable() {  
  Runtime* runtime = Runtime::Current();  
  // If we are transitioning from non-debuggable to debuggable, we patch  
  // entry points of methods to remove any aot / JITed entry points.  
  InstallStubsClassVisitor visitor(this);  
  runtime->GetClassLinker()->VisitClasses(&visitor);
}

按照上面的思路嘗試了一波,果然變得流暢很多!!!

其實上面的解決方案還有遺留問題。對比debugable為false的包還是有些卡頓。我也發現了bootImage中的方法已經走到nterp上了,但是apk中的大部分代碼還是走到了switch解釋執行上,於是我改變思路。我在調用UpdateEntrypointsForDebuggable前先把RuntimeDebugState設置成非debugable,調用之後再把RuntimeDebugState設置會debugable不就行了嗎。最後的代碼如下,hook框架使用了https://github.com/bytedance/android-inline-hook。

Java_test_ArtMethodTrace_bootImageNterp(JNIEnv *env,                                                      
                                                       jclass clazz) {    
    void *handler = shadowhook_dlopen("libart.so");    
    instance_ = static_cast<void **>(shadowhook_dlsym(handler, "_ZN3art7Runtime9instance_E"));    
    jobject    
    (*getSystemThreadGroup)(void *runtime) =(jobject (*)(void *runtime)) shadowhook_dlsym(handler,                                                                                          
                                                                                          "_ZNK3art7Runtime20GetSystemThreadGroupEv");
    void
    (*UpdateEntrypointsForDebuggable)(void *instrumentation) = (void (*)(void *i)) shadowhook_dlsym(            
            handler, 
            "_ZN3art15instrumentation15Instrumentation30UpdateEntrypointsForDebuggableEv");    
    if (getSystemThreadGroup == nullptr || UpdateEntrypointsForDebuggable == nullptr) {        
        LOGE("getSystemThreadGroup  failed ");        
        shadowhook_dlclose(handler);        
        return;
     }    
     jobject thread_group = getSystemThreadGroup(*instance_);    
     int vm_offset = findOffset(*instance_, 0, 4000, thread_group);    
     if (vm_offset < 0) {        
         LOGE("vm_offset not found ");        
         shadowhook_dlclose(handler);        
         return;    
      }    
      void (*setRuntimeDebugState)(void *instance_, 
                                   int r) =(void (*)(void *runtime,                                                                    int r)) shadowhook_dlsym(            
             handler, "_ZN3art7Runtime20SetRuntimeDebugStateENS0_17RuntimeDebugStateE");    
      if (setRuntimeDebugState != nullptr) {        
          setRuntimeDebugState(*instance_, 0);    
      }    
      void *instrumentation = reinterpret_cast<void *>(reinterpret_cast<char *>(*instance_) +                                                     
                                                       vm_offset - 368 );
                                                       
      UpdateEntrypointsForDebuggable(instrumentation);    
      setRuntimeDebugState(*instance_, 2);    
      shadowhook_dlclose(handler);    
      LOGE("bootImageNterp success");
      
}

四、最後

最近在社區上也看到了高通工程師的一篇文章,他在我定位到的問題的基礎上做了更詳細的分析,確認了Google會在Android 15上修復這個問題,如果是海外版本的Android 14設備,Google計劃通過com.android.artapex模塊的更新來修復這個問題。但是國內由於網絡的問題,Google的推送無法工作,因此需要各個手機廠家來主動合入這兩筆改動。[1]

如果大家需要臨時解決debugable包的卡頓的問題也可以通過上述方式解決。 

參考文章:

[1] https://juejin.cn/post/7353106089296789556

*文/ 烏柚

本文屬得物技術原創,更多精彩文章請看:得物技術官網

未經得物技術許可嚴禁轉載,否則依法追究法律責任!

user avatar journey_64224c9377fd5 头像 pulsgarney 头像 razyliang 头像 longlong688 头像 huajianketang 头像 wanshoujidezhuantou 头像 wenzhongdejianpan 头像 xw-01 头像 explinks 头像 crow_5c1708a9c847d 头像 49u7s8yz 头像 maomaoxiaobo 头像
点赞 27 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.