主頁 > 移動端開發 > 原始碼剖析Android ANR產生機制

原始碼剖析Android ANR產生機制

2021-11-11 08:31:41 移動端開發

原始碼剖析Android ANR產生機制

如下采用Android原始碼的android-11.0.0_r48分支進行,不同版本原始碼差異巨大,
ANR的捕獲起點為ProcessRecord.appNotResponding方法,本文由appNotResponding逆推ANR的產生機制,
原始碼:frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java,

通過原始碼搜索appNotResponding,發現系統提供了AnrHelper類,封裝了ProcessRecord.appNotResponding,所有ANR產生后,呼叫都會走到這里,通過搜索發現Activity、Broadcast、Service、ContentProvider都會呼叫AnrHelper.appNotResponding,也就是說Android四大組件都有可能產生ANR,
原始碼:frameworks/base/services/core/java/com/android/server/am/AnrHelper.java

Activity ANR

在開始Activity ANR之前,先問個問題:
創建一個Android Hello World工程,添加一個Button,在Button的onClick中回呼SystemClock.sleep(10 * 1000),

  • 運行App,點擊一次按鈕,發現按鈕處于Pressed狀態,因為阻塞了UI執行緒,那么一直等待,會不會產生ANR呢?結果是不會,10秒后,會看到Button狀態恢復正常,并沒有ANR框彈出,也沒有產生ANR,
  • 運行App,連續點擊兩次按鈕,一直等待會不會產生ANR呢?結果是會,

下面開始原始碼分析:

  1. 從AnrHelper.appNotResponding出發,搜索呼叫地方,發現在ActivityManagerService中有inputDispatchingTimedOut方法,最終呼叫到AnrHelper.appNotResponding,最終走到ANR流程,此處通過函式名基本得到是輸入超時導致的ANR,
    原始碼:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

  2. 通過查找inputDispatchingTimedOut呼叫,最終發現ActivityRecord、AnrController兩個類中有呼叫
    原始碼:frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java
    原始碼:frameworks/base/services/core/java/com/android/server/wm/AnrController.java
    此處通過路徑發現,Activity過來的ANR的觸發點在WindowManager中,也就是Activity中的Window才會觸發ANR,
    繼續看原始碼ActivityRecord中的inputDispatchingTimedOut,發現其只有轉調WindowManager.inputDispatchingTimedOut,并沒有觸發邏輯,
    繼續看原始碼AnrController中的inputDispatchingTimedOut,發現其中提供了幾種型別的ANR函式供呼叫:notifyAppUnresponsive、notifyWindowUnresponsive、notifyGestureMonitorUnresponsive,如下逐個分析,

notifyAppUnresponsive呼叫堆疊:

frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.java (notifyNoFocusedWindowAnr)
frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp (NativeInputManager::notifyNoFocusedWindowAnr)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::doNotifyNoFocusedWindowAnrLockedInterruptible)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::onAnrLocked(std::shared_ptr<InputApplicationHandle> application))
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::processNoFocusedWindowAnrLocked)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::processAnrsLocked)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::dispatchOnce)

notifyWindowUnresponsive呼叫堆疊:

frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.java (notifyWindowUnresponsive)
frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp (NativeInputManager::notifyWindowUnresponsive)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::doNotifyWindowUnresponsiveLockedInterruptible)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::sendWindowUnresponsiveCommandLocked)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::processConnectionUnresponsiveLocked) 
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::onAnrLocked(const sp<Connection>& connection))
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::processAnrsLocked)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::dispatchOnce)

notifyGestureMonitorUnresponsive呼叫堆疊:

frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.java (notifyGestureMonitorUnresponsive)
frameworks/base/services/core/java/com/android/server/input/InputManagerService.java (notifyMonitorUnresponsive)
frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp (NativeInputManager::notifyMonitorUnresponsive)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::doNotifyMonitorUnresponsiveLockedInterruptible)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::sendMonitorUnresponsiveCommandLocked)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::processConnectionUnresponsiveLocked)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::onAnrLocked(const sp<Connection>& connection))
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::processAnrsLocked)
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp (InputDispatcher::dispatchOnce)

從呼叫堆疊可見,如上3種型別的ANR最終都會收斂至InputDispatcher::dispatchOnce函式,那么接下來先看此函式,
InputDispatcher::dispatchOnce為InputDispatcher::start函式中開啟的名為InputDispatcher執行緒的運行體,InputDispatcher執行緒啟動后,如果沒有停止,則一直回圈呼叫InputDispatcher::dispatchOnce,此函式最后的mLooper->pollOnce(timeoutMillis)將執行緒Block住,等待timeoutMillis時間,然后開啟下一輪InputDispatcher::dispatchOnce呼叫(mLooper采用Linux epoll機制進行block,等待timeoutMillis的呼叫為epoll_wait(…, timeoutMillis)),timeoutMillis的值為5000,也即每5秒,
在某些事件發生時(如:輸入事件),可以呼叫mLooper->wake(),停掉等待,立刻呼叫InputDispatcher::dispatchOnce,然后其中會呼叫processAnrsLocked判斷此次是否發生ANR,

接下來看InputDispatcher::processAnrsLocked,此函式判斷是否產生ANR,函式的注釋簡單來說就是“等待的事件時長超過Window timeout(5秒),就產生ANR”,接下來通過原始碼再行驗證一下,

  • 其中首先判斷if (currentTime >= *mNoFocusedWindowTimeoutTime),為true就呼叫processNoFocusedWindowAnrLocked()產生ANR,
  • 如果上述判斷沒通過,意味著具備了Focused Window,那么則繼續判斷操作是否超時,如果if (currentTime < nextAnrCheck)判斷沒通過,不幸超時,則走到InputDispatcher::onAnrLocked(const sp& connection),產生connection型別的ANR,即:Activity Window型別的ANR,下面會詳解,

再回到最開始的InputDispatcher::dispatchOnce函式,除了輪詢呼叫外,在某些事件發生時(呼叫mLooper->wake()停掉等待),也會被呼叫,那么也就意味著每個促使mLooper->wake()被呼叫的事件,都將可能產生ANR,
原始碼:frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
在原始碼中搜索mLooper->wake(),發現有22個匹配,原始碼中幾乎每個呼叫都有注釋,如下列舉幾個常見的事件:

  • InputDispatcher::notifyConfigurationChanged – 配置改變,如:橫豎屏切換
  • InputDispatcher::notifyKey – 發生輸入事件
  • InputDispatcher::notifySensor – 傳感器資訊改變
  • InputDispatcher::setInputWindows – 設定輸入視窗
  • InputDispatcher::setFocusedApplication – 為App設定焦點
  • InputDispatcher::transferTouchFocus – 傳遞觸摸焦點
  • InputDispatcher::setFocusedWindow – 設定視窗焦點
  • InputDispatcher::displayRemoved – 移除Display

回到上面3個ANR函式呼叫(notifyAppUnresponsive、notifyWindowUnresponsive、notifyGestureMonitorUnresponsive),這3個函式分別代表3種型別的ANR,

  • notifyAppUnresponsive:No Window Focus Timeout,指Activity還沒有Focused Window(包括No Attach Window),并且上述22種事件到來,長達5秒鐘無回應,從而導致ANR (InputDispatcher::processNoFocusedWindowAnrLocked)
  • notifyWindowUnresponsive:Window Timeout,指Activity中的Window在上述22種事件到來時,長達5秒鐘無回應,從而導致ANR (InputDispatcher::processConnectionUnresponsiveLocked)
  • notifyGestureMonitorUnresponsive:Window Monitor Timeout,跟notifyWindowUnresponsive類似,不過此處是注冊了Window事件的Monitor,如:PointerEventDispatcher (InputDispatcher::processConnectionUnresponsiveLocked)
  1. 無論UI阻塞多長時間,只要沒有必要事件需要Dispatch(mLooper->wake()),則不會產生ANR
  2. 如果在InputDispatcher::dispatchOnce的時刻,恰好Input事件沒有超時5秒,則也不會產生ANR

注:經測驗,華為/小米廠商將ANR的5000ms改為了8000ms,可以通過adb shell dumpsys input | grep dispatchingTimeout來獲取系統值,

adb logcat -s ActivityManager可以查看ANR的初步資訊,如:

E ActivityManager: ANR in com.huya.mtp.myanr (com.huya.mtp.myanr/.MainActivity)
E ActivityManager: PID: 10789
E ActivityManager: Reason: Input dispatching timed out (f69705 com.huya.mtp.myanr/com.huya.mtp.myanr.MainActivity (server) is not responding. Waited 5005ms for MotionEvent(deviceId=9, source=0x00005002, displayId=0, action=DOWN, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, classification=NONE, edgeFlags=0x00000000, xPrecision=30.3, yPrecision=17.1, xCursorPosition=nan, yCursorPosition=nan, pointers=[0: (405.0, 269.9)]), policyFlags=0x62000000)
E ActivityManager: Parent: com.huya.mtp.myanr/.MainActivity

總結:Activity有3種ANR型別,分別為:No Window Focus Timeout,Window Timeout,Window Monitor Timeout,而這3種型別具備22個觸發事件點,函式呼叫為:mLooper->wake(),Android官方設定的超時時間為5000ms,

Broadcast ANR

在開始Broadcast ANR之前,同樣問個問題:
創建一個Android Hello World工程,動態注冊一個BroadcastReceiver,在onReceiver中呼叫SystemClock.sleep(100 * 1000),添加一個Button,點擊后發送廣播讓動態注冊的BroadcastReceiver接收,

  • 運行App,點擊一次按鈕,發現有可能按鈕處于Pressed狀態,因為阻塞了UI執行緒,那么一直等待,會不會產生ANR呢?結果是不會
  • 如果將動態注冊改為靜態注冊,再次運行,會不會產生ANR呢?結果是會

下面開始原始碼分析:

1. 繼續來從AnrHelper.appNotResponding出發,搜索呼叫地方,發現在BroadcastQueue中有broadcastTimeoutLocked方法,最終呼叫到AnrHelper.appNotResponding,最終走到ANR流程,此處通過函式名基本得到是廣播超時導致的ANR,

原始碼:frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java

2. 查看broadcastTimeoutLocked原始碼發現,其從一個變數名為mDispatcher的物件中取活躍的廣播,然后對這個廣播進行發送,mDispatcher的型別為BroadcastDispatcher,根據類注釋知道,這個類是用來管理有序廣播的,有序廣播記錄在BroadcastDispatcher.mOrderedBroadcasts的ArrayList中,

原始碼:frameworks/base/services/core/java/com/android/server/am/BroadcastQueue/BroadcastDispatcher.java

3. 回到BroadcastQueue中,發現除了存盤有序廣播的BroadcastDispatcher mDispatcher物件外,還存在并行廣播型別ArrayList mParallelBroadcasts,這兩種型別的廣播在都會在processNextBroadcastLocked方法中被處理,processNextBroadcastLocked方法代碼700多行,直接反推比較繞,下面改變方向,采用順推方式,

原始碼:frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java

4. 現在回到Android四大組件的Broadcast,發送廣播有如下3種方式:

  • Context.sendBroadcast:發送普通廣播(并行廣播)
  • Context.sendOrderedBroadcast:發送有序廣播(串行廣播)
  • Context.sendStickyBroadcast:發送粘性廣播
    由于粘性廣播已被廢棄,所以接下來只看普通廣播與有序廣播,廣播的并行與串行是針對接收方來說的,

5. 下面進入原始碼呼叫鏈

Context.sendBroadcast --> ContextWrapper.sendBroadcast --> ContextImpl.sendBroadcast --> ActivityManagerService.broadcastIntentWithFeature
Context.sendOrderedBroadcast --> ContextWrapper.sendOrderedBroadcast --> ContextImpl.sendOrderedBroadcast --> ActivityManagerService.broadcastIntentWithFeature

呼叫最終通過Binder跨行程走到了ActivityManagerService.broadcastIntentWithFeature,通過引數設定了是否有序,引數為broadcastIntentWithFeature方法中的倒數第3個引數(boolean serialized),broadcastIntentWithFeature方法有15個引數,

6. 繼續深入ActivityManagerService.broadcastIntentWithFeature查看呼叫鏈

ActivityManagerService.broadcastIntentWithFeature ----> ActivityManagerService.broadcastIntentLocked

經過多載呼叫來到了ActivityManagerService.broadcastIntentLocked方法,這又是一個700行左右的大方法,看來接下來只能死磕了,此處核心關注點在于廣播如何被保存起來,
6.1. 函式最開始判斷是否為Instant狀態、判斷廣播白名單、判斷系統是否完成啟動、判斷UserId等,這些跟并行與串行廣播沒啥關系,跳過
6.2. 判斷bOptions是否為null(注意:不是變數brOptions),bOptions是給System發送廣播用的,App發送的廣播bOptions寫死為null,所以跳過bOptions判斷的整段代碼,
6.3. 判斷是否為Protected Broadcast,根據注釋,這仍然是System行為,跳過
6.4. 接下來對發送者與系統廣播做一次安全檢測,不符合則拋出SecurityException,繼續跟并行與串行沒啥關系,跳過
6.5. 如果廣播的action不為null,則進入一個巨大的switch case,用于處理Intent.ACTION_PACKAGE_REMOVED等預定義廣播,跟我們自定義的沒關系,繼續跳過
6.6. 接下來處理粘性廣播(sticky),并為之添加一些屬性,由于粘性不在本文范圍內,并且也已經廢棄,跳過
6.7. 接下來獲取廣播的users,跳過
6.8. 接下來看到注釋“// Figure out who all will receive this broadcast”,來了來了,讀完500行代碼后,終于來到廣播接收的處理邏輯處了,
6.9. 首先判斷Intent是否不具備Intent.FLAG_RECEIVER_REGISTERED_ONLY屬性,如果不具備則呼叫collectReceiverComponents收集receivers,根據Android檔案Intent.FLAG_RECEIVER_REGISTERED_ONLY表示動態注冊的廣播型別,不會Launch BroadcastReceiver components,
6.9.1. 接下去開啟支線任務,進到collectReceiverComponents查看收集到了一些什么receivers,方法回傳型別為List,ResolveInfo即為IntentFilter中的Intent,
6.9.2. collectReceiverComponents的主要是呼叫了AppGlobals.getPackageManager().queryIntentReceivers(intent, resolvedType, pmFlags, user).getList()來收集List
6.9.2.1. 接下來繼續開啟支線任務,進入到queryIntentReceivers看看怎么樣將List收集到的,

PackageManagerService.queryIntentReceivers --> PackageManagerService.queryIntentReceiversInternal --> ComponentResolver.queryReceivers --> ComponentResolver.queryIntent

ComponentResolver類用于決議Android四大組件,最終的呼叫ComponentResolver.queryIntent將所有的靜態與動態注冊廣播的Intent的query出來,并呼叫PackageManagerService.applyPostResolutionFilter決議后保存到List中,最侄訓傳給collectReceiverComponents(這個支線扯遠了,已經到PackageManagerService中了),
6.10. 回到ActivityManagerService,此時receivers已經賦值,其中的內容為所有通過Resolve的Intent的List,也就是所有靜態與動態注冊的廣播接收者List,
6.11. 接下來呼叫mReceiverResolver.queryIntent為registeredReceivers賦值,好家伙,又來一個廣播接收者List,此處支線下去看的話,發現回傳的僅為動態注冊的接收者List(支線任務就不展開講了,感興趣可以自己閱讀原始碼),
6.12. 接下來采用白名單對registeredReceivers進行處理,跟并行與串行無關,跳過,
6.13. 此處總結一下,上面分析了那么多,最終得到了兩個廣播接收者List:

  • receivers:所有注冊的廣播接收者,包括靜態與動態
  • registeredReceivers:僅為動態注冊的廣播接收者

6.14. 接下來判斷:如果不是串行廣播,并且具備接收者(registeredReceivers.size() > 0),則呼叫queue.enqueueParallelBroadcastLocked發送廣播,從方法名可以看出來,此處是并行的,enqueueParallelBroadcastLocked將廣播添加到mParallelBroadcasts,揮應了上面逆推中的第3點,而mParallelBroadcasts中的廣播怎么被處理,后面再講,
6.15. 接下來判斷是否存在receivers,如果存在,則進行安全檢測,避免監聽Intent.ACTION_PACKAGE_ADDED做App立即啟動的后門,此處與我們主題無關,跳過,
6.16. 接下來按照廣播的優先級priority進行排序,串行的廣播按照優先級發送,
6.17. 接下來做一些List操作后,最終呼叫queue.enqueueOrderedBroadcastLocked發送廣播,同樣從方法名可以看出,此處是串行的,enqueueOrderedBroadcastLocked將廣播添加到mDispatcher中,揮應了上面逆推中的第2點,至于廣播怎么被處理,同樣后面再講,
6.18. 看完這個700行的大方法后,最后總結一下:

  • mParallelBroadcasts:并行廣播發送佇列,其中要求廣播接收者必須是動態注冊的,并且采用非Order的sendBroadcast發送,
  • mDispatcher:串行廣播發送佇列管理類,如果廣播接收者是靜態注冊的或者采用Order的sendOrderedBroadcast發送,
    也就是說:
  • 動態注冊廣播接收者的廣播,采用sendBroadcast發送則是并行發送與接受的,
  • 靜態注冊廣播接收者的廣播,無論怎樣發送都是串行的接受的,
  • 采用sendOrderedBroadcast發送的廣播,無論接收者是靜態還是動態注冊的,都將串行接收,

7. 上面完成串行并行廣播的入列之后,接下去看一下佇列中的廣播是怎樣觸發消費的(也就是說了解了生產者邏輯后,看看如何消費的,并優先看看如何觸發消費的),

7.1. 在queue.enqueueParallelBroadcastLocked與queue.enqueueOrderedBroadcastLocked方法后,都能看到queue.scheduleBroadcastsLocked呼叫,從函式名看出這個應該是調度廣播去消費了,
7.2. 進入scheduleBroadcastsLocked方法,其中采用Handler發送了BROADCAST_INTENT_MSG訊息,
7.3. 在接受BROADCAST_INTENT_MSG Handler訊息的之處,看到呼叫processNextBroadcast,也就是處理下一條廣播,
7.4. 進入processNextBroadcast后看到其同步呼叫processNextBroadcastLocked,而processNextBroadcastLocked便是廣播消費的主體了,

8. 下面進入processNextBroadcastLocked查看廣播消費,這也揮應了如上逆推中的第3點,也就是mParallelBroadcasts與mDispatcher兩個佇列均在processNextBroadcastLocked中被處理,這就又回到這個700多行的大方法了,接下來又只能死磕了,由于具備上面第6點的順推知識,此處順推這個大方法也就輕松多了,此處核心關注點在這個廣播如何被發送出去,

原始碼:frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java
8.1. 方法開始定義一個變數BroadcastRecord r,這便是廣播的記錄,代表著當前廣播,也就是整個方法都圍繞著這個r進行,
8.2. 接下里更新CPU等內容,跳過,
8.3. 接下來看到一個while回圈(while (mParallelBroadcasts.size() > 0) {),mParallelBroadcasts是并行佇列,所以接下來這個回圈便是處理并行廣播的,這個回圈的核心呼叫為deliverToRegisteredReceiverLocked,將廣播deliver至RegisteredReceiver型別的的Receiver中,根據第6點知識,RegisteredReceiver便是動態注冊的廣播串列,
8.3.1. 接下來又該開啟支線任務了,deliverToRegisteredReceiverLocked又是一個200多行的大方法,繼續死磕
8.3.2. 方法開始就定義一個boolean skip = false,表示是否要跳過這個廣播的deliver,
8.3.3. 接下來大約150行都是各種if判斷,if判斷為真,則skip = true,if中判斷各種權限之類的,如果最終skip = true,則跳過這次deliver,此處忽略這種被skip的情況,核心關注如何被發送,
8.3.4. 接下來繼續判斷權限,然后繼續跳過deliver,跳過,
8.3.5. 如果是ordered的廣播,還要處理oomAdj,跟如何發送也無關,繼續跳過,
8.3.6. 接下來是一個try catch,看到if判斷,一般來說filter.receiverList.app.inFullBackup為false(inFullBackup:Process is currently hosting a backup agent for backup or restore),即會走到else處,最終呼叫核心函式performReceiveLocked,開始接受廣播,
8.3.6.1. 進入performReceiveLocked方法,根據不同條件,可能會轉調scheduleRegisteredReceiver或performReceive,
8.3.6.2. scheduleRegisteredReceiver的定義在IApplicationThread.aidl中,此aidl有oneway標記,即異步執行,
原始碼:frameworks/base/core/java/android/app/IApplicationThread.aidl
8.3.6.3. performReceive的定義在IIntentReceiver.aidl中,同樣,此aidl也有oneway標記,同樣是異步執行,
原始碼:frameworks/base/core/java/android/content/IIntentReceiver.aidl
8.3.7. 從performReceiveLocked函式中出來后,回到processNextBroadcastLocked函式的while回圈中,由于異步執行,所以while回圈會很快發送完所有訊息,讓registeredReceivers分別去處理,所以這也就是廣播的并行發送邏輯,
8.4. 接下去該到串行佇列mDispatcher的處理了(由于一個廣播可以被某應用動態注冊為并行廣播,也可以被其他應用靜態注冊為串行廣播,所以接下去還需要處理串行佇列),接著往下看processNextBroadcastLocked方法,代碼中也可看到注釋:// Now take care of the next serialized one…,
8.5. 首先處理行程等待的特殊邏輯,此處不關心,跳過,
8.6. 接下去看到一個有著200行代碼的巨大的do while回圈,首先呼叫r = mDispatcher.getNextBroadcastLocked(now)去取串行佇列中的BroadcastRecord r,在7.1中講過,整個大方法都圍繞著這個r進行,如果r為null,那么就該結束了,否則繼續往下,
8.7. 接著判斷些出錯邏輯,如果當前時間超出了2倍的廣播超時時間 * 接收者數,那么就彈出ANR,由于這里是特殊出錯邏輯,非主流程,所以可無視,跳過此處接著看,
8.8. 接著的兩個if判斷都是在處理特殊出錯邏輯,繼續跳過,
8.9. 接下去是一個大的延時廣播判斷if (!r.deferred),deferred僅在addDeferredBroadcast被賦值為true,此處進入后if后,再呼叫if (mDispatcher.isDeferringLocked(receiverUid))判斷是否需要延期處理,通常被判斷為不需要,最后跳出這個大的if判斷,
8.10. 繼續往下,進行一系列系統廣播除錯所需的Track與Log等操作后,計算timeout時間后,呼叫setBroadcastTimeoutLocked
8.10.1. 接下來支線任務到setBroadcastTimeoutLocked中,看到在超時的時間點,將采用Handler發送BROADCAST_TIMEOUT_MSG訊息
8.10.2. 在接受BROADCAST_TIMEOUT_MSG Handler訊息的之處,看到broadcastTimeoutLocked
8.10.3. 進入broadcastTimeoutLocked,這就揮應第1點了,這個方法進行一系列判斷后,最終呼叫AnrHelper.appNotResponding,走到ANR流程,繞了一大圈,終于找到廣播觸發ANR的導火索了,
8.10.4. setBroadcastTimeoutLocked在未來的時間點埋了個定時炸彈,如果時間到,還沒拆除,就ANR,
8.11. 先回來,上面埋了定時炸彈后,串行廣播還沒有被發送,所以接著往下看,if判斷是否需要直接呼叫呼叫receiver(if (nextReceiver instanceof BroadcastFilter)),此處不需要,跳過,
8.12. 接下去又是一個250行的skip判斷邏輯,走完后skip = false,無需skip此廣播,由于靜態注冊廣播的App如果未啟動,系統會將App啟動,于是這一系列skip判斷邏輯包括判斷App是否處于已安裝的正常狀態,權限上是否允許啟動,最后在必要時啟動App,
8.13. 最終呼叫processCurBroadcastLocked進行廣播的發送,然后便return結束這個長達700行的大方法,
8.14. processCurBroadcastLocked函式最終通過Binder呼叫IApplicationThread.scheduleReceiver,
原始碼:frameworks/base/core/java/android/app/IApplicationThread.aidl
等一下,此處的IApplicationThread仍然是oneway的,也就是說仍然是異步呼叫的,說好的同步呢?說好的串行呢?ANR定時炸彈何時拆除呢?

9. 為了解決上面的問題,這次從BroadcastReceiver再出發,在廣播接收處理結束,會呼叫BroadcastReceiver的sendFinished方法,不管是并行還是串行,都會呼叫ActivityManager.finishReceiver,如果是串行,則會傳遞一些列引數,用于后續事項,如果是并行或沒有接收器了,則基本傳0或null了,目的只為告訴ActivityManagerService,這個廣播最后一個接收器處理完了,

原始碼:frameworks/base/core/java/android/content/BroadcastReceiver.java

10. 在ActivityManager.finishReceiver中,呼叫getMatchingOrderedReceiver判斷此Receiver是否為串行的:

  • 如果是,則呼叫BroadcastQueue.finishReceiverLocked,然后再呼叫BroadcastQueue.processNextBroadcastLocked,
  • 如果為否,即為并行,那么就什么都不處理,回憶一下如上并行處理邏輯,在processNextBroadcastLocked中,通過一個while回圈將所有廣播發送至動態注冊的接收者中了,一次性就全部處理完了,也沒有埋什么定時炸彈,所以對于并行廣播,對于ActivityManagerService來說,沒有什么后事需要處理了,

11. 接下來再次回到BroadcastQueue中,查看BroadcastQueue.finishReceiverLocked,方法名為結束Receiver,根據上面的分析,并行接收器名稱為registeredReceivers,所以此方法是專門為串行接收器準備的,方法中主要是處理一些資料結構等善后作業,此處就不詳細展開了,

12. 繼續在BroadcastQueue中查看processNextBroadcastLocked,這個方法名有些眼熟,這又回到這個700多行代碼的大方法了,根據上面串行的分析,對于串行廣播佇列,每一次processNextBroadcastLocked呼叫僅deliver一個廣播,然后等待BroadcastReceiver的sendFinished,然后再行呼叫processNextBroadcastLocked來deliver下一個廣播,如此來實作了串行,

而拆掉定時炸彈的作業也是在processNextBroadcastLocked中進行的,其中有cancelBroadcastTimeoutLocked呼叫,函式實作為移除Handler訊息BROADCAST_TIMEOUT_MSG,

采用adb shell dumpsys activity broadcasts可以查看系統為廣播設定的超時值(Broadcast parameters)

  • key=bcast_fg_constants:前臺廣播(默認超時10秒)
  • key=bcast_bg_constants:后臺廣播(默認超時60秒)
  • key=bcast_offload_constants:offload廣播(默認超時60秒),已知需要耗費長時間的廣播,如:BOOT_COMPLETED

adb logcat -s ActivityManager可以查看ANR的初步資訊,如:

E ActivityManager: ANR in com.huya.mtp.mybroadcast
E ActivityManager: PID: 10653
E ActivityManager: Reason: Broadcast of Intent { act=com.huya.mtp.mybroadcast.BROADCAST_STATIC flg=0x10000010 pkg=com.huya.mtp.mybroadcast cmp=com.huya.mtp.mybroadcast/.MyBroadcastStaticReceiver (has extras) }

總結:Broadcast只有一種ANR,就是超時,對于并行廣播,是不存在超時這一說的,也就是并行廣播不會引發ANR,而對于串行廣播,每次發送給接收器之前,都會通過Handler在未來設定一個定時炸彈,如果此接收器在超時之內沒有完成處理,則引爆炸彈彈出ANR,按照編碼的說法:

  • 呼叫Context.sendBroadcast,發送給動態注冊的Receiver,則怎么都不會引發ANR
  • 呼叫Context.sendBroadcast,發送給靜態注冊的Receiver,如果Receiver在超時時間內未完成,則ANR
  • 呼叫Context.sendOrderedBroadcast,無論發送給動態或靜態注冊的Receiver,如果Receiver在超時時間內未完成,則ANR

Service ANR

在開始Service ANR前,再次同樣問個問題:
創建一個Android Hello World工程,寫一個服務,在Service.onCreate中呼叫SystemClock.sleep(300 * 1000),添加一個Button,點擊后呼叫Context.startForegroundService啟動這個服務,
運行App,點擊一次按鈕,一直等待,會不會產生ANR呢?結果是會,但通常看不到彈出ANR框,因為這個時候會崩潰,實際上此時即發生了ANR,又發生了崩潰,

上面硬磕完Broadcast的部分代碼后,此處看Service的代碼就輕松多了,仍然采用先逆推,后順推的方式,

1. 繼續從AnrHelper.appNotResponding出發,搜索呼叫地方,發現在ActiveServices中有serviceTimeout, serviceForegroundTimeout,最終呼叫到AnrHelper.appNotResponding,最終走到ANR流程,此處通過函式名基本得到是服務超時導致的ANR,

原始碼:frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

2. 搜索serviceTimeout的呼叫,發現在ActivityManagerService中有唯一呼叫,這就又回到ActivityManagerService的代碼中了,繼續發現當收到Handler訊息SERVICE_TIMEOUT_MSG后,便開始ANR流程,

原始碼:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

3. 繼續搜索SERVICE_TIMEOUT_MSG,發現又回到ActiveServices中了,在其中有兩處Handler.removeMessages呼叫(根據Broadcast原始碼猜測,則應該是Service拆定時炸彈邏輯了),有一處Handler.sendMessageDelayed(埋定時炸彈了),

4. 先看Handler.sendMessageDelayed的封裝方法scheduleServiceTimeoutLocked,翻譯成中文大概是“鎖定的調度服務超時”,上面Broadcast就多次出現schedule(調度),此處應該也就是到了Service被消費(被創建)的邏輯了,

5. 繼續逆推看scheduleServiceTimeoutLocked的呼叫出,發現被bumpServiceExecutingLocked方法呼叫了,繼續往上逆推,發現bumpServiceExecutingLocked又被realStartServiceLocked呼叫了,這就切實到了Service被創建的時刻了,這都幾乎推到最開始了,下面轉為順推,應該會看得更清晰,

6. 這兒我們順推Context.startService,進入呼叫鏈:

Context.startService --> ContextWrapper.startService --> ContextImpl.startService --> ContextImpl.startServiceCommon --> ActivityManagerService.startService

看到跟Broadcast的流程一摸一樣,下面再驗證一下Context.startForegroundService,進入呼叫鏈:

Context.startForegroundService --> ContextWrapper.startForegroundService --> ContextImpl.startForegroundService --> ContextImpl.startServiceCommon --> ActivityManagerService.startService

發現兩者的流程也一致,foreground與否的差別在于startServiceCommon的第二個引數boolean requireForeground,

7. 繼續深入ActivityManagerService.startService查看呼叫鏈

ActivityManagerService.startService ----> ActiveServices.startServiceLocked

經過多載來到了ActiveServices.startServiceLocked,這個方法大概250行代碼,接下來進去看看,
7.1. 判斷并設定final boolean callerFg的值,表示呼叫發起者是否為前臺,這個值將貫穿整個Service的啟動程序,ActivityManagerService.getRecordForAppLocked用于查找呼叫發起者的資訊,
7.2. 接著呼叫retrieveServiceLocked查找服務啟動接收者的資訊,并最終取得ServiceRecord r,跟廣播變數名一致,這個r變數是用于記錄服務資訊的,
7.3. 接下來一系列的判斷,最終為ServiceRecord r的成員變數賦值,
7.4. 然后又是一系列的判斷,最終來到內部的startServiceInnerLocked方法,
7.4.1. 接下去開啟支線任務,進入到startServiceInnerLocked
7.4.2 繼續進入,進行一些邏輯處理后,轉調bringUpServiceLocked
7.4.3. 繼續進入,進行一些邏輯處理后,轉調realStartServiceLocked
7.4.4. 繼續進入,進行一些邏輯處理后,呼叫bumpServiceExecutingLocked(此處也能揮應至如上第5點了),在7.1點中提到boolean callerFg,此值一直傳遞到bumpServiceExecutingLocked中,最終賦值給r變數的executeFg,在后續設定Service超時時使用,
7.4.5. 繼續進入,進行一些判斷后,發現呼叫了scheduleServiceTimeoutLocked,這兒就埋上ANR定時炸彈了,如果是前臺服務,超時設定為20秒,后臺則為200秒(此處揮應如上第4點),
7.4.6. 從scheduleServiceTimeoutLocked中退出
7.4.7. 從bumpServiceExecutingLocked中退出
7.4.8. 回到realStartServiceLocked繼續往下看,最終通過Binder呼叫到ActivityThread.scheduleCreateService,回呼已經來到Service的目標行程中了,scheduleCreateService發送Handler訊息CREATE_SERVICE,
原始碼:frameworks/base/core/java/android/app/ActivityThread.java
7.4.9. 在接受CREATE_SERVICE處呼叫handleCreateService,
7.4.10. handleCreateService進行一系列檢查后,呼叫service.onCreate,這就到了目標Service的onCreate方法了,也就是Service的@Override onCreate,
7.4.11. 上述onCreate執行完后,接著往下通過Binder呼叫到ActivityManagerService.serviceDoneExecuting,
7.4.12. 繼續一路轉調至ActiveServices.serviceDoneExecutingLocked
7.4.13. 然后再轉調至ActiveServices.serviceDoneExecutingLocked,在其中經過一系列判斷后,最終呼叫Handler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app)拆除ANR定時炸彈,
7.4.14. 這里補充說明一下,查看原始碼的同時,有些朋友可能會看到Handler訊息ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG,10秒的超時時間SERVICE_START_FOREGROUND_TIMEOUT,從命名上看,似乎說的是前臺服務,而前臺服務超時的20秒在7.4.5點已經講過了,此處的這個10秒的意思是什么呢?實際上,如果呼叫Context.startForegroundService啟動前臺服務,則系統要求在這個10秒鐘之內,Service必須呼叫Context.startForeground,否則會產生ANR,也會拋出RuntimeException崩潰,崩潰會將行程退掉,所以表面看上去是發生崩潰了,實際上ANR也發生了,只不過表面上看不到(崩潰是因為發送了Handler訊息SCHEDULE_CRASH,代碼在ActivityThread.java中)
logcat中的崩潰堆疊:

2021-11-08 15:20:13.931 11661-11661/com.huchao.myserviceanr E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.huchao.myserviceanr, PID: 11661
    android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{56a0ba u0 com.huchao.myserviceanr/.MyService}
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2005)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

logcat中的ANR資訊:

E ActivityManager: ANR in com.huchao.myserviceanr
E ActivityManager: PID: 11661
E ActivityManager: Reason: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{56a0ba u0 com.huchao.myserviceanr/.MyService}

adb logcat -s ActivityManager可以查看ANR的初步資訊,如:

ActivityManager: ANR in com.huchao.myserviceanr
ActivityManager: PID: 11033
ActivityManager: Reason: executing service com.huchao.myserviceanr/.ForegroundService

總結:Service在創建超時會產生ANR,有3個超時值:

  • ActiveServices.SERVICE_START_FOREGROUND_TIMEOUT:10秒,開啟前臺服務超時時長,是指Context.startForegroundService呼叫后,Service中必須呼叫Context.startForeground,否則會同時發生ANR與崩潰,
  • ActiveServices.SERVICE_TIMEOUT:20秒,前臺服務超時時長,是指Context.startForegroundService呼叫后,Service的onCreate執行的超時,如果超時則會發生ANR,
  • ActiveServices.SERVICE_BACKGROUND_TIMEOUT:200秒,后臺服務超時時長,是指Context.startService呼叫后,Service的onCreate執行的超時,如果超時則會發生ANR,
    除了如上Service在創建時超時外,在bind系結、unbind解綁、destroy銷毀、start執行、反start執行時(某些特殊的服務Pending邏輯),也受如上條件限制,
    按照編碼的說法:
    Service在onCreate、onDestroy、onStartCommand、onBind、onUnbind中執行都受超時限制,如果超時則ANR,

ContentProvider ANR

1. 繼續來從AnrHelper.appNotResponding出發,搜索呼叫地方,發現在ContentProviderHelper中有appNotRespondingViaProvider方法,最終呼叫到AnrHelper.appNotResponding,最終走到ANR流程,此處通過函式名基本得到是ContentProvider導致的ANR,

原始碼:frameworks/base/services/core/java/com/android/server/am/ContentProviderHelper.java

2. 接著搜索appNotRespondingViaProvider,發現好幾個地方有呼叫地方,接下來一步步看,

2.1. ContentProviderHelper中的appNotRespondingViaProvider呼叫,在ContentProviderHelper類中就有名為getProviderMimeType的方法呼叫appNotRespondingViaProvider,繼續搜索getProviderMimeType,發現在ActivityManagerShellCommand中有呼叫getProviderMimeType,不過這個類是給adb shell am start-activity使用的,已超出本文范圍,所以忽略,
原始碼:frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java

2.2. ActivityManagerService, ActivityThread, ContextImpl中均有appNotRespondingViaProvider呼叫,不過這3個類僅為轉調appNotRespondingViaProvider,并沒有觸發邏輯,所以先忽略,
原始碼:
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
frameworks/base/core/java/android/app/ActivityThread.java
frameworks/base/core/java/android/app/ContextImpl.java

2.3. ContentProviderClient中有appNotRespondingViaProvider呼叫,并且也有觸發邏輯,接下去深入進去看看,
2.3.1. 在ContentProviderClient中有NotRespondingRunnable,運行到這個Runnable的run就會呼叫appNotRespondingViaProvider,呼叫堆疊流程為:

NotRespondingRunnable.run --> ApplicationContentResolver.appNotRespondingViaProvider --> ActivityThread.appNotRespondingViaProvider --> ActivityManagerService.appNotRespondingViaProvider --> ContentProviderHelper.appNotRespondingViaProvider --> AnrHelper.appNotResponding

呼叫流程就回到了最開始產生ANR處,

2.3.2. ContentProviderClient與ContentResolver類似,都是提供給Client使用的,都可以用來獲取的資料,差別在于兩者的使用場景不同,

  • 針對相同ContentProvider的多次呼叫,建議使用ContentProviderClient,但用完需要釋放,
  • 針對不同ContentProvider的多次呼叫,建議使用ContentResolver,

2.3.3. 接下來看NotRespondingRunnable的觸發邏輯就能找到ANR的產生處了,在名為setDetectNotResponding的方法中找到了new NotRespondingRunnable(),并且ContentProviderClient中還提供了beforeRemote, afterRemote來安裝與拆除ANR定時炸彈,看樣子是這兒了,繼續看setDetectNotResponding方法,這是一個@SystemApi,并且不開放給第三方App使用(通過反射與Hook機制作業系統行為不在本文討論范圍內),

2.4. 通過如上分析,所有常規三方App可達的ContentProvider ANR可行路徑均已被堵死,也就是App自己的ContentProvider是不會產生ANR的,

總結:在Android 11版本中,ContentProvider有好幾處ANR觸發邏輯,但均已被堵死,也就是App的ContentProvider是不會觸發產生ANR的,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/355360.html

標籤:其他

上一篇:Android 11 Audio框架探索(一)

下一篇:uniapp-h5+獲取通知欄權限

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more