1. 程式人生 > >Android 系統穩定性

Android 系統穩定性

   如果你是一個Android應用程序開發人員,你的人生中不可避免的三件事情是:死亡、繳稅和ANR。這麼說是誇張了,但是由於Android本身的設計,以及應用程式和系統在開發過程中的缺陷,經常會在測試過程中遇到各種各樣的ANR問題。在功能性的測試中還少一些,主要是在壓力測試中(例如Monkey測試)會遇到非常多的ANR問題。本章的目的就是彙總筆者在工作中遇到的各種ANR問題,將其歸納總結出一套分析和處理ANR問題的方法,希望能夠通過這套方法為大家提供思路,有效的減少大家處理ANR問題的時間。同時也會給出一些避免ANR的最佳實踐,更多的從預防做起,更少的做事後補救。

考慮到看本文的讀者大多是有實際經驗的開發人員,我會盡量少的提到一些基礎的概念,我也希望給大家更多的“乾貨”。

本章的主要內容如下:

  1. ANR簡介(什麼是ANR、為什麼會有ANRANR的異常長什麼樣)

  2. 如何分析ANR(引起ANR的原因分類、分析ANR的利器)

  3. 例項講解

  4. 避免ANR的最佳實踐(從錯誤中吸取教訓)

1.1 ANR簡介

ANR,是“Application Not Responding”的縮寫,即“應用程式無響應”。在Android中,ActivityManagerService(簡稱AMS)和WindowManagerService(簡稱WMS)會監測應用程式的響應時間,如果應用程式主執行緒(即UI執行緒)在超時時間內對輸入事件沒有處理完畢,或者對特定操作沒有執行完畢,就會出現

ANR。對於輸入事件沒有處理完畢產生的ANRAndroid會顯示一個對話方塊,提示使用者當前應用程式沒有響應,使用者可以選擇繼續等待或者關閉這個應用程式(也就是殺掉這個應用程式的程序)。

1.1.1為什麼會有ANR

如上所述,ANR的產生需要同時滿足三個條件:

  • 主執行緒:只有應用程式程序的主執行緒響應超時才會產生ANR

  • 超時時間:產生ANR的上下文不同,超時時間也會不同,但只要在這個時間上限內沒有響應就會ANR

  • 輸入事件/特定操作:輸入事件是指按鍵、觸屏等裝置輸入事件,特定操作是指BroadcastReceiverService的生命週期中的各個函式,產生ANR

    的上下文不同,導致ANR的原因也會不同;

針對這三個條件,有以下三種情況會觸發ANR,詳細說明如下

       1、主執行緒對輸入事件在5秒內沒有處理完畢

Android的事件系統從2.3開始做了完全不同的實現,原先2.2中是在Java層實現的,但在2.3中整體轉移到了C++層,本書基於2.3以後的版本進行說明。我們先簡單瞭解一下產生這種ANR的整個流程<!-- 在這章之前要先講Android的事件機制?還是放在後一章講?偏向於放在後面。 -->

當應用程式的Window處於Active狀態並且能夠接收輸入事件(例如按鍵事件、觸控事件等)時,系統底層上報的事件就會被InputDispatcher分發給這個應用程式,應用程式的主執行緒通過InputChannel讀取輸入事件並交給介面檢視處理,介面檢視是一個樹狀結構,DecorView是檢視樹的根,事件從樹根開始一層一層向端點(例如一個Button)傳遞。我們通常會註冊一個監聽器來接收並處理事件,或者建立自定義的檢視控制元件來處理事件。

InputDispatcher執行在系統程序(程序名為system_server)的一個單獨的執行緒中,應用程式的主執行緒在處理事件的過程中,InputDispatcher會不斷的檢測處理過程是否超時,一旦超時,會通過一系列的回撥通知WMSnotifyANR函式,最終會呼叫到AMSmHandler物件裡的SHOW_NOT_RESPONDING_MSG這個case,此時介面上就顯示系統提示對話方塊了,同時使用logcat命令檢視log(日誌資訊)也可以看到關於ANR的資訊。InputDispatcher就是那個愛打“小報告”的傢伙。

一下子出現了好幾個重要的名詞,要深入瞭解這種情況的ANR,需要熟悉Android的事件機制,本書會在別的章節中詳細分析,這裡只需要記住他們的功能:

  • Window:具體指的是PhoneWindow物件,表示一個能夠顯示的視窗,它能夠接收系統分發的各種輸入事件;

  • InputDispatcher:將系統上報的輸入事件分發給當前活動的視窗;

  • InputChannelInputDispatcher和應用程式分別執行在兩個不同的程序中,InputDispatcher就是通過InputChannel將事件物件傳遞給應用程序的。

注意:產生這種ANR的前提是要有輸入事件,如果使用者沒有觸發任何輸入事件,即便是主執行緒阻塞了,也不會產生ANR,因為InputDispatcher沒有分發事件給應用程式,當然也不會檢測處理超時和報告ANR了。


      2、主執行緒在執行BroadcastReceiveronReceive函式時10秒內沒有執行完畢

BroadcastReceiver(簡稱BR)的onReceive函式執行在主執行緒中,當這個函式超過10秒鐘沒有返回就會觸發ANR。不過對這種情況的ANR系統不會顯示對話方塊提示,僅是輸出log而已。


      3、主執行緒在執行Service的各個生命週期函式時20秒內沒有執行完畢

        Service的各個生命週期函式也執行在主執行緒中,當這些函式超過20秒鐘沒有返回就會觸發ANR。同樣對這種情況的ANR系統也不會顯示對話方塊提示,僅是輸出log

       三種ANR中只有第1種會顯示系統提示對話方塊,因為使用者正在做介面互動操作,如果長時間沒有任何響應,會讓使用者懷疑裝置宕機了,大多數人此時會開始亂按,甚至拔出電池重啟,給使用者的體驗肯定是非常糟糕的。

        三種ANR發生時都會在log中輸出錯誤資訊,你會發現各個應用程序和系統程序的函式堆疊資訊都輸出到了一個/data/anr/traces.txt的檔案中,這個檔案是分析ANR原因的關鍵檔案,同時在日誌中還會看到當時的CPU使用率,這也是重要資訊,在後面的章節會詳細介紹如何利用它們分析ANR問題。

這三種ANR不是孤立的,有可能會相互影響。例如一個應用程式程序中同時有一個正在顯示的Activity和一個正在處理訊息的BroadcastReceiver,它們都執行在這個程序的主執行緒中。如果BRonReceive函式沒有返回,此時使用者點選螢幕,而onReceive超過5秒仍然沒有返回,主執行緒無法處理使用者輸入事件,就會引起第1ANR。如果繼續超過10秒沒有返回,又會引起第2ANR


1.2  如何分析ANR問題

引起ANR問題的根本原因,總的來說可以歸納為兩類:

  • 應用程序自身引起的,例如:

主執行緒阻塞、掛起、死迴圈

應用程序的其他執行緒的CPU佔用率高,使得主執行緒無法搶佔到CPU時間片

  • 其他程序間接引起的,例如:

當前應用程序進行程序間通訊請求其他程序,其他程序的操作長時間沒有反饋

其他程序的CPU佔用率高,使得當前應用程序無法搶佔到CPU時間片

分析ANR問題時,以上述可能的幾種原因為線索,通過分析各種日誌資訊,大多數情況下你就可以很容易找到問題所在了。

注意:我很誠懇的向讀者說明,確實有一些ANR問題很難調查清楚,因為整個系統不穩定的因素很多,例如Linux Kernel本身的bug引起的記憶體碎片過多、硬體損壞等。這類比較底層的原因引起的ANR問題往往無從查起,並且這根本不是應用程式的問題,浪費了應用開發人員很多時間,如果你從事過整個系統的開發和維護工作的話會深有體會。所以我不能保證瞭解了本章的所有內容後能夠解決一切ANR問題,如果出現了很疑難的ANR問題,我建議最好去和做驅動和核心的朋友聊聊,或者,如果問題只是個十萬分之一的偶然現象,不影響程式的正常執行,我倒是建議不去理它。

1.2.1分析ANR的利器

會在ANR發生時輸出很多有用的資訊幫助分析問題原因,我們先來看一下ANR的異常資訊,使用logcat命令檢視會得到類似如下的log

//WindowManager所在的程序是system_server,程序號是127

I/WindowManager( 127): Input event dispatching timed out sending to com.example.anrdemo/com.example.anrdemo.ANRActivity
 
//system_server程序中的ActivityManagerService請求kernel向5033程序傳送SIGNAL_QUIT請求
//你可以在shell中使用命令達到相同的目的:adb shell kill -3 5033
//和其他的Java虛擬機器一樣,SIGNAL_QUIT也是Dalvik內部支援的功能之一

I/Process ( 127): Sending signal. PID: 5033 SIG: 3
 

//5033程序的虛擬機器例項接收到SIGNAL_QUIT訊號後會將程序中各個執行緒的函式堆疊資訊輸出到traces.txt檔案中
//發生ANR的程序正常情況下會第一個輸出

I/dalvikvm( 5033): threadid=4: reacting to signal 3
I/dalvikvm( 5033): Wrote stack traces to '/data/anr/traces.txt'

... ...//另外還有其他一些程序

 //隨後會輸出CPU使用情況

E/ActivityManager( 127): ANR in com.example.anrdemo (com.example.anrdemo/.ANRActivity)

//Reason表示導致ANR問題的直接原因

E/ActivityManager( 127): Reason: keyDispatchingTimedOut
E/ActivityManager( 127): Load: 3.85 / 3.41 / 3.16

//請注意ago,表示ANR發生之前的一段時間內的CPU使用率,並不是某一時刻的值

E/ActivityManager( 127): CPU usage from 26835ms to 3662ms ago with 99% awake:
E/ActivityManager( 127): 9.4% 98/mediaserver: 9.4% user + 0% kernel
E/ActivityManager( 127): 8.9% 127/system_server: 6.9% user + 2% kernel / faults: 1823 minor
... ...
E/ActivityManager( 127): +0% 5033/com.example.anrdemo: 0% user + 0% kernel
E/ActivityManager( 127): 39% TOTAL: 32% user + 6.1% kernel

 //這裡是later,表示ANR發生之後

E/ActivityManager( 127): CPU usage from 601ms to 1132ms later with 99% awake:
E/ActivityManager( 127): 10% 127/system_server: 1.7% user + 8.9% kernel / faults: 5 minor
E/ActivityManager( 127): 10% 163/InputDispatcher: 1.7% user + 8.9% kernel
E/ActivityManager( 127): 1.7% 127/system_server: 1.7% user + 0% kernel
E/ActivityManager( 127): 1.7% 135/SurfaceFlinger: 0% user + 1.7% kernel
E/ActivityManager( 127): 1.7% 2814/Binder Thread #: 1.7% user + 0% kernel

... ...

E/ActivityManager( 127): 37% TOTAL: 27% user + 9.2% kernel


  從log中能夠知道,發生ANRAndroid為我們提供了兩種“利器”:traces檔案和CPU使用率。以上做了簡單註釋,不過稍後再詳細分析它們。

1.2.2 ANR資訊是如何輸出的

      我們再來看看這些log是怎樣被輸出的,這很重要,知其然,也要知其所以然。程式碼在ActivityManagerService類中,找到它的appNotResponding函式。

final void appNotResponding(ProcessRecord app, ActivityRecord activity,ActivityRecord parent, final String annotation) {

//firstPids和lastPids兩個集合存放那些將會在traces中輸出資訊的程序的程序號

ArrayList<Integer> firstPids = new ArrayList<Integer>(5);
SparseArray<Boolean> lastPids = new SparseArray<Boolean>(20);

 
//mController是IActivityController介面的例項,是為Monkey測試程式預留的,預設為null

if (mController != null) {
... ...
}
 
long anrTime = SystemClock.uptimeMillis();
if (MONITOR_CPU_USAGE) {
updateCpuStatsNow(); //更新CPU使用率
}

synchronized (this) { //一些特定條件下會忽略ANR

if (mShuttingDown) {
Slog.i(TAG, "During shutdown skipping ANR: " + app + " " + annotation);
return;
} else if (app.notResponding) {
Slog.i(TAG, "Skipping duplicate ANR: " + app + " " + annotation);
return;
} else if (app.crashing) {
Slog.i(TAG, "Crashing app skipping ANR: " + app + " " + annotation);
return;
}

 
//使用一個標誌變數避免同一個應用在沒有處理完時重複輸出log

app.notResponding = true;
... ...

 
//①當前發生ANR的應用程序被第一個新增進firstPids集合中

firstPids.add(app.pid);
... ...
}

... ...

//②dumpStackTraces是輸出traces檔案的函式

File tracesFile = dumpStackTraces(true, firstPids, processStats, lastPids, null);
String cpuInfo = null;

if (MONITOR_CPU_USAGE) { //MONITOR_CPU_USAGE預設為true

updateCpuStatsNow(); //再次更新CPU資訊

synchronized (mProcessStatsThread) {

//輸出ANR發生前一段時間內的CPU使用率

cpuInfo = mProcessStats.printCurrentState(anrTime);

}

info.append(processStats.printCurrentLoad());

info.append(cpuInfo);

}

//輸出ANR發生後一段時間內的CPU使用率

info.append(processStats.printCurrentState(anrTime));

... ...


//③將ANR資訊同時輸出到DropBox中

addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,

cpuInfo, tracesFile, null);

... ...

//在Android4.0中可以設定是否不顯示ANR提示對話方塊,如果設定的話就不會顯示對話方塊,並且會殺掉ANR程序

boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
synchronized (this) {
if (!showBackground && !app.isInterestingToUserLocked() && app.pid != MY_PID) {
... ...
Process.killProcessQuiet(app.pid);
return;
}
... ...
 
//顯示ANR提示對話方塊

Message msg = Message.obtain();
HashMap map = new HashMap();
msg.what = SHOW_NOT_RESPONDING_MSG;
msg.obj = map;
map.put("app", app);
if (activity != null) {
map.put("activity", activity);
}
mHandler.sendMessage(msg);
}
}


 有三個關鍵點需要注意:

1、當前發生ANR的應用程序被第一個新增進firstPids集合中,所以會第一個向traces檔案中寫入資訊。反過來說,traces檔案中出現的第一個程序正常情況下就是發生ANR的那個程序。不過有時候會很不湊巧,發生ANR的程序還沒有來得及輸出trace資訊,就由於某種原因退出了,所以偶爾會遇到traces檔案中找不到發生ANR的程序資訊的情況。

2、dumpStackTraces是輸出traces檔案的函式,接下來分析這個函式

3、addErrorToDropBox函式將ANR資訊同時輸出到DropBox中,它也是個非常有用的日誌存放工具,後面也會分析它的作用。

public static File dumpStackTraces(boolean clearTraces, ArrayList<Integer> firstPids,

ProcessStats processStats, SparseArray<Boolean> lastPids, String[] nativeProcs) {

//系統屬性“dalvik.vm.stack-trace-file”用來配置trace資訊輸出檔案

String tracesPath = SystemProperties.get("dalvik.vm.stack-trace-file", null);

if (tracesPath == null || tracesPath.length() == 0) {
return null;
}

File tracesFile = new File(tracesPath);
try {
File tracesDir = tracesFile.getParentFile();
if (!tracesDir.exists()) tracesFile.mkdirs();

//FileUtils.setPermissions是個很有用的函式,設定檔案屬性時經常會用到

FileUtils.setPermissions(tracesDir.getPath(), 0775, -1, -1); // drwxrwxr-x

 
//clearTraces為true,會刪除舊檔案,建立新檔案

if (clearTraces && tracesFile.exists()) tracesFile.delete();
tracesFile.createNewFile();
FileUtils.setPermissions(tracesFile.getPath(), 0666, -1, -1); // -rw-rw-rw-
} catch (IOException e) {
Slog.w(TAG, "Unable to prepare ANR traces file: " + tracesPath, e);
return null;
}
 

//一個過載函式

dumpStackTraces(tracesPath, firstPids, processStats, lastPids, nativeProcs);
return tracesFile;

}



有兩個關鍵點需要注意:

聰明的你肯定已經知道,之所以trace資訊會輸出到“/data/anr/traces.txt”檔案中,就是系統屬性“dalvik.vm.stack-trace-file”設定的。你可以通過在裝置的shell中使用setpropgetprop對系統屬性進行設定和讀取:

getpropdalvik.vm.stack-trace-file

setprop dalvik.vm.stack-trace-file /tmp/stack-traces.txt

每次發生ANR時都會刪除舊的traces檔案,重新建立新檔案。也就是說Android只保留最後一次發生ANR時的traces資訊,那麼以前的traces資訊就丟失了麼?稍後回答。

接著來看過載的dumpStackTraces函式。

private static void dumpStackTraces(String tracesPath, ArrayList<Integer> firstPids,

ProcessStats processStats, SparseArray<Boolean> lastPids, String[] nativeProcs) {

//使用FileObserver監聽應用程序是否已經完成寫入traces檔案的操作

//Android在判斷桌面桌布檔案是否設定完成時也是用的FileObserver,很有用的類

FileObserver observer = new FileObserver(tracesPath, FileObserver.CLOSE_WRITE) {

public synchronized void onEvent(int event, String path) { notify(); }

};

... ...

//首先輸出firstPids集合中指定的程序,這些也是對ANR問題來說最重要的程序

if (firstPids != null) {

try {

int num = firstPids.size();

for (int i = 0; i < num; i++) {

synchronized (observer) {

//前面提到的SIGNAL_QUIT

Process.sendSignal(firstPids.get(i), Process.SIGNAL_QUIT);

observer.wait(200);

... ...

}

提示:如果你在解決其他問題時也需要檢視Java程序中各個執行緒的函式堆疊資訊,就可以使用向目標程序傳送SIGNAL_QUIT3)這個技巧。其實這個名稱有點誤導,它並不會讓程序真正退出。

1.2.3 DropBox

剛才留了一個問題:Android只保留最後一次發生ANR時的traces資訊,那麼以前的traces資訊就丟失了麼?為了增強Android的異常資訊收集管理能力,從2.2開始增加了DropBox功能。

DropBox(簡稱DB)是系統程序中的一個服務,在system_server程序啟動時建立,並且它沒有執行在單獨的執行緒中,而是執行在system_serverServerThread執行緒中。我們可以將ServerThread稱作system_server的主執行緒,ServerThread執行緒除了啟動並維護各個服務外,還負責檢測一些重要的服務是否死鎖,這一點到後面的Watchdog部分再分析<!-- Watchdog寫完後注意補充章節號 -->DB被建立的程式碼如下。

SystemServer.java → ServerThread.run()

Slog.i(TAG, "DropBox Service");

ServiceManager.addService(Context.DROPBOX_SERVICE, //服務名稱為“dropbox”

new DropBoxManagerService(context,new File("/data/system/dropbox")));


“/data/system/dropbox”DB指定的檔案存放位置。下面來看一下DB服務的主要功能。

1. DropBoxManagerService

DropBoxManagerService(簡稱DBMS

相關推薦

Android 系統穩定性之ANR

       相信大部分Android 開發人員都遇到過ANR問題,本文根據一些實際的開發經歷介紹一下如何解決和避免ANR問題。 一,何為ANR        ANR是“Application Not Responding”的縮寫,即“應用程式無響應”。在Android中,

Android系統穩定性----Crash

Crash、FC、froce close 發生場景 應用程序崩潰。強制關閉,android App 因為程式碼異常導致奔潰。 表現形式主要有兩種 1.      Java 程式碼引起,彈出錯誤的提示框,JVM虛擬機器退出,絕大多數的工具可以捕獲。 2.      C++程式

Android 系統穩定性

   如果你是一個Android應用程序開發人員,你的人生中不可避免的三件事情是:死亡、繳稅和ANR。這麼說是誇張了,但是由於Android本身的設計,以及應用程式和系統在開發過程中的缺陷,經常會在測試過程中遇到各種各樣的ANR問題。在功能性的測試中還少一些,主要

android系統穩定性-ANR(二)

P.S. OpenOffice貼上過來後格式有些混亂。 1.2如何分析ANR問題 引起ANR問題的根本原因,總的來說可以歸納為兩類: 應用程序自身引起的,例如: 主執行緒阻塞、掛起、死迴圈 應用程序的其他執行緒的CPU佔用率高,使得主執行緒無法搶

【分享】迅為iTOP4412開發板-Android系統屏幕旋轉設置

原生 android4 代碼 豎屏 == watermark ont 分辨率 軟件 1.1概述 Android4.0,Androd4.4源代碼能夠編譯成手機模式和平板模式,訊為iTop4412 開發平臺 的Android系統默認編譯為平板模式。客戶須要依據自己的產品

改動android 系統時間

nbsp pop -m andro shel content data popu 命令 命令如 date -s "yyyymmdd.[[[hh]mm]ss]" 直接在CRT上執行,舉例:date -s "20120801.120503" 但在adb shell下

Android核心服務解析篇(三)——Android系統的啟動

onf med cin gets get lld 系統屬性 基本 安裝模塊 從大的方面來說。Android系統的啟動能夠分為兩個部分:第一部分是Linux核心的啟動,第二部分是Android系統的啟動。第一部分主要包含系統引導,核心和驅動程序等,因為它們不屬於本篇要講的

分布式系統穩定性模式

包沖突 日誌 rep 減少 機房 並且 trace 調用接口 做到 本篇文章主要介紹了"分布式系統穩定性模式",主要涉及到分布式系統穩定性模式方面的內容,對於分布式系統穩定性模式感興趣的同學可以參考一下。 對集群大訪問量的應用很有針對性的一些註意點。 1.隔離。 發

高速改動android系統默認日期方法

port odi -a 個推 post setting moved ast 是不是 高速改動android系統默認日期方法 在android系統的設備上,都有一個默認的開始日期,看過非常多設備,有些設備在沒有聯網的時候沒有同步到系統時間的時候,竟然默

【樹莓派】樹莓派刷Android系統

net 文章 圖文 str 鍵盤操作 左右移動 ima 工具 解決 樹莓派3安裝Android TV系統圖文教程 http://www.mz6.net/news/android/6866.html 樹莓派3 Android TV系統怎樣安裝?樹莓派3一個重要用途就是當智

調用aNDROID系統自帶功能

ont hao123 provide music content vid uri android系統 系統 CONTENTpROVIDER%E5%9F%BA%E7%A1%80%E4%B9%8BURI http://music.hao123.com/songlist/495

技術文章 | 系統穩定性保障核武器——全鏈路壓測

tps alt 系統 f11 技術文章 無限 通過 技術分享 center 為什麽要做全鏈路壓測? 對阿裏巴巴而言,每年最重要的一天莫過於雙11。這是因為在雙11的零點,系統會遭遇史無前例的巨大洪峰流量沖擊,保證雙11當天系統的穩定性對高可用團隊來說是巨大的挑戰。在這個

Android系統編譯出錯

android 編譯出錯在Android7.1編譯過程中出現如下錯誤:Starting build with ninjaninja: Entering directory `.‘ [ 0% 8/30301] Ensure Jack server is installed and startedJack

Android系統默認語言改為中文

android targe base center lang rda 16px oca 語言 第一種方法:修改 build/tools/buildinfo.shecho "ro.product.locale.language=zh"echo "ro.product.loca

實際項目中對系統穩定性的一些思考

技術 場景 每次 自己 html 能說 控制 bsp 進行 說起系統穩定性,其實已經有很多文章了.我這裏結合自己實際項目中的一些情況,進行了反思. 業務場景其實也很簡單.就是我們需要做一個爬蟲去爬取別的網站的文章和圖片. 主要問題出在圖片上,當時我在想可不可以不爬取

如何直接打開android系統的wifi設置頁面,防止intent劫持

art intent pre fonts mark bsp androi 希望 andro 在android的app開發中,經常會遇到需要跳轉至系統設置頁面的需求。但是當你使用以下代碼時: 如 Intent intent = new Intent(Settings.ACT

Android系統版本、Platform版本、SDK版本、gradle修改

wearable versions 應用 示例 tro tex ria col ide 雖然之前分析了gradle,但是在eclipse導入Android studio的時候,各個版本出現的問題還是很模糊,下面對各種版本進行一下說明: 參考資料: https://

第4章系統穩定性4.1在線日誌分析

時間排序 txt ash 可執行 字符串 awk -c 歸檔 行數 cat -n access.log -n打印行號 more access.log 空格-下一頁、回車-下一行、F-下一屏,百分比的下一個、B-上一屏 less access.log /查

Android系統啟動流程(一)解析init進程啟動過程

option 寫入 android change failed miss 通知 target sna 前言 作為“Android框架層”這個大系列中的第一個系列,我們首先要了解的是Android系統啟動流程,在這個流程中會涉及到很多重要的知識點,這個系列我們就來一一講解它們

Android系統常用URI

詳細 dia strong esc trac roc contract raw body android系統常用URI android系統管理聯系人的URI如下: ContactsContract.Contacts.CONTENT_URI 管理聯系人的Uri Contact