Android?Java?crash?處理流程詳解
一、背景
當(dāng)Android系統(tǒng)發(fā)生native crash時(shí),在日志臺打印日志和生成tombstone_xxx文件,會通過 socket 通知 AMS 從而進(jìn)入到Java crash側(cè) 處理流程中。 同時(shí),當(dāng)發(fā)生Java crash時(shí),系統(tǒng)會捕捉到該crash,從而也進(jìn)入到Java crash的處理流程。
由此可見,Java crash處理流程是非常重要的。 native crash流程上篇文章已經(jīng)分析過了,今天再來看看Java crash的處理流程。
二、App端Crash注冊
不管是系統(tǒng)進(jìn)程還是App進(jìn)程,啟動的時(shí)候都會走到這里。
2.1 commonInit()
RuntimeInit.java
@UnsupportedAppUsage
protected static final void commonInit() {
if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
LoggingHandler loggingHandler = new LoggingHandler();
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
// 注冊處理器
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
}
注冊 殺掉App進(jìn)程的處理器 KillApplicationHandler 。
2.2 KillApplicationHandler 類
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
private final LoggingHandler mLoggingHandler;
/**
* Create a new KillApplicationHandler that follows the given LoggingHandler.
* If {@link #uncaughtException(Thread, Throwable) uncaughtException} is called
* on the created instance without {@code loggingHandler} having been triggered,
* {@link LoggingHandler#uncaughtException(Thread, Throwable)
* loggingHandler.uncaughtException} will be called first.
*
* @param loggingHandler the {@link LoggingHandler} expected to have run before
* this instance's {@link #uncaughtException(Thread, Throwable) uncaughtException}
* is being called.
*/
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
// 在日志臺打印崩潰時(shí)的日志
ensureLogging(t, e);
// Don't re-enter -- avoid infinite loops if crash-reporting crashes.
if (mCrashing) return;
mCrashing = true;
// Try to end profiling. If a profiler is running at this point, and we kill the
// process (below), the in-memory buffer will be lost. So try to stop, which will
// flush the buffer. (This makes method trace profiling useful to debug crashes.)
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.
// 最終關(guān)閉kill調(diào)進(jìn)程
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.
}
}
}
}
職責(zé):
- 在日志臺打印崩潰日志
- 調(diào)用 AMS的handleApplicationCrash()方法
- 在finally中殺掉App進(jìn)程
2.2.1 ensureLogging()
內(nèi)部調(diào)用了 LoggingHandler.uncaughtException()方法。LoggingHandler 也實(shí)現(xiàn)了 Thread.UncaughtExceptionHandler接口。 重寫了 uncaughtException() 方法。
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
public volatile boolean mTriggered = false;
@Override
public void uncaughtException(Thread t, Throwable e) {
mTriggered = true;
// Don't re-enter if KillApplicationHandler has already run
if (mCrashing) return;
// mApplicationObject is null for non-zygote java programs (e.g. "am")
// There are also apps running with the system UID. We don't want the
// first clause in either of these two cases, only for system_server.
if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
} else {
// 準(zhǔn)備拼接 FATAL EXCEPTION ,打印到控制臺
StringBuilder message = new StringBuilder();
//
// The "FATAL EXCEPTION" string is still used on Android even though
// apps can set a custom UncaughtExceptionHandler that renders uncaught
// exceptions non-fatal.
message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
final String processName = ActivityThread.currentProcessName();
if (processName != null) {
// 拼上進(jìn)程名字
message.append("Process: ").append(processName).append(", ");
}
// 進(jìn)程id
message.append("PID: ").append(Process.myPid());
// 打印message和 e異常信息
Clog_e(TAG, message.toString(), e);
}
}
}
拼接 FATAL EXCEPTION 開頭的字符串,同時(shí)打印崩潰的信息。
因此,可以通過過濾出 FATAL EXCEPTION精準(zhǔn)定位崩潰的日志。
2.2.2 ApplicationErrorReport
new ApplicationErrorReport.ParcelableCrashInfo(e) 創(chuàng)建了一個(gè)crashinfo對象。 這個(gè)對象其實(shí)就是從throwable中 解析得到的。
App端打印了日志后,就進(jìn)入到AMS端的處理邏輯中。
三、AMS端處理崩潰邏輯
3.1 AMS.handleApplicationCrash
public void handleApplicationCrash(IBinder app,
ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
//找到 ProcessRecord對象
ProcessRecord r = findAppProcess(app, "Crash");
// app=null,表示system_server進(jìn)程
final String processName = app == null ? "system_server"
: (r == null ? "unknown" : r.processName);
handleApplicationCrashInner("crash", r, processName, crashInfo);
}
該方法是 RuntimeInit用來上報(bào)app崩潰時(shí)調(diào)用。 當(dāng)這個(gè)方法返回后,App進(jìn)程將會退出。
- 找出崩潰進(jìn)程對應(yīng)的 ProcessRecord對象,如果app為空,則是system server進(jìn)程。
- 繼續(xù)調(diào)用
handleApplicationCrashInner()。
3.1.1 AMS.handleApplicationCrashInner()
/* Native crash reporting uses this inner version because it needs to be somewhat
* decoupled from the AM-managed cleanup lifecycle
*/
void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
ApplicationErrorReport.CrashInfo crashInfo) {
// ...
final int relaunchReason = r == null ? RELAUNCH_REASON_NONE
: r.getWindowProcessController().computeRelaunchReason();
final String relaunchReasonString = relaunchReasonToString(relaunchReason);
if (crashInfo.crashTag == null) {
crashInfo.crashTag = relaunchReasonString;
} else {
crashInfo.crashTag = crashInfo.crashTag + " " + relaunchReasonString;
}
// 1 寫入崩潰信息到Dropbox
addErrorToDropBox(
eventType, r, processName, null, null, null, null, null, null, crashInfo);
// 2 調(diào)用mAppErrors 的crashApplication方法
mAppErrors.crashApplication(r, crashInfo);
}
這個(gè)方法不僅Java crash回調(diào),Native crash也會通過AMS的之前注冊的socket服務(wù),調(diào)用到這里。可以參考Native crash流程。
- 寫入崩潰信息到
Dropbox - 繼續(xù)調(diào)用 mAppErrors 的
crashApplication()。
3.2 addErrorToDropBox()
把crash、WTF、ANR的描述寫到drop box中。
public void addErrorToDropBox(String eventType,
ProcessRecord process, String processName, String activityShortComponentName,
String parentShortComponentName, ProcessRecord parentProcess,
String subject, final String report, final File dataFile,
final ApplicationErrorReport.CrashInfo crashInfo) {
// Bail early if not published yet
if (ServiceManager.getService(Context.DROPBOX_SERVICE) == null) return;
// 獲取 DBMS服務(wù)
final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class);
// Exit early if the dropbox isn't configured to accept this report type.
// 確定錯(cuò)誤類型
final String dropboxTag = processClass(process) + "_" + eventType;
if (dbox == null || !dbox.isTagEnabled(dropboxTag)) return;
// Rate-limit how often we're willing to do the heavy lifting below to
// collect and record logs; currently 5 logs per 10 second period.
final long now = SystemClock.elapsedRealtime();
if (now - mWtfClusterStart > 10 * DateUtils.SECOND_IN_MILLIS) {
mWtfClusterStart = now;
mWtfClusterCount = 1;
} else {
if (mWtfClusterCount++ >= 5) return;
}
// 開始拼接錯(cuò)誤信息
final StringBuilder sb = new StringBuilder(1024);
appendDropBoxProcessHeaders(process, processName, sb);
if (process != null) {
// 是否前臺
sb.append("Foreground: ")
.append(process.isInterestingToUserLocked() ? "Yes" : "No")
.append("\n");
}
if (activityShortComponentName != null) {
sb.append("Activity: ").append(activityShortComponentName).append("\n");
}
if (parentShortComponentName != null) {
if (parentProcess != null && parentProcess.pid != process.pid) {
sb.append("Parent-Process: ").append(parentProcess.processName).append("\n");
}
if (!parentShortComponentName.equals(activityShortComponentName)) {
sb.append("Parent-Activity: ").append(parentShortComponentName).append("\n");
}
}
if (subject != null) {
sb.append("Subject: ").append(subject).append("\n");
}
sb.append("Build: ").append(Build.FINGERPRINT).append("\n");
if (Debug.isDebuggerConnected()) {
sb.append("Debugger: Connected\n");
}
if (crashInfo != null && crashInfo.crashTag != null && !crashInfo.crashTag.isEmpty()) {
sb.append("Crash-Tag: ").append(crashInfo.crashTag).append("\n");
}
sb.append("\n");
// Do the rest in a worker thread to avoid blocking the caller on I/O
// (After this point, we shouldn't access AMS internal data structures.)
// dump錯(cuò)誤信息
Thread worker = new Thread("Error dump: " + dropboxTag) {
@Override
public void run() {
if (report != null) {
sb.append(report);
}
String setting = Settings.Global.ERROR_LOGCAT_PREFIX + dropboxTag;
int lines = Settings.Global.getInt(mContext.getContentResolver(), setting, 0);
int maxDataFileSize = DROPBOX_MAX_SIZE - sb.length()
- lines * RESERVED_BYTES_PER_LOGCAT_LINE;
if (dataFile != null && maxDataFileSize > 0) {
try {
sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
"\n\n[[TRUNCATED]]"));
} catch (IOException e) {
Slog.e(TAG, "Error reading " + dataFile, e);
}
}
if (crashInfo != null && crashInfo.stackTrace != null) {
sb.append(crashInfo.stackTrace);
}
if (lines > 0) {
sb.append("\n");
// Merge several logcat streams, and take the last N lines
InputStreamReader input = null;
try {
java.lang.Process logcat = new ProcessBuilder(
"/system/bin/timeout", "-k", "15s", "10s",
"/system/bin/logcat", "-v", "threadtime", "-b", "events", "-b", "system",
"-b", "main", "-b", "crash", "-t", String.valueOf(lines))
.redirectErrorStream(true).start();
try { logcat.getOutputStream().close(); } catch (IOException e) {}
try { logcat.getErrorStream().close(); } catch (IOException e) {}
input = new InputStreamReader(logcat.getInputStream());
int num;
char[] buf = new char[8192];
while ((num = input.read(buf)) > 0) sb.append(buf, 0, num);
} catch (IOException e) {
Slog.e(TAG, "Error running logcat", e);
} finally {
if (input != null) try { input.close(); } catch (IOException e) {}
}
}
dbox.addText(dropboxTag, sb.toString());
}
};
if (process == null) {
// If process is null, we are being called from some internal code
// and may be about to die -- run this synchronously.
final int oldMask = StrictMode.allowThreadDiskWritesMask();
try {
// 直接在當(dāng)前線程執(zhí)行
worker.run();
} finally {
StrictMode.setThreadPolicyMask(oldMask);
}
} else {
// 開個(gè)新的線程執(zhí)行
worker.start();
}
}
dropbox是system-server進(jìn)程在 StartOtherServices中注冊的服務(wù)DropBoxManager。它會記錄系統(tǒng)的關(guān)鍵log信息,用來debug 調(diào)試。在ServiceManager 中的注冊名字為 dropbox。 dropbox服務(wù)的數(shù)據(jù)保存在 /data/system/dropbox/中。
dropbox 支持保存的錯(cuò)誤類型為:
- anr 進(jìn)程發(fā)生未響應(yīng)
- watchdog 進(jìn)程觸發(fā)watchdog
- crash 進(jìn)程發(fā)生java崩潰
- native_crash 進(jìn)程發(fā)生native崩潰
- wtf 進(jìn)程發(fā)生嚴(yán)重錯(cuò)誤
- lowmem 進(jìn)程內(nèi)存不足
寫入到Dropbox文件后,繼續(xù)看看 AppErrors.crashApplication()方法:
3.3 AppErrors.crashApplication()
AppErrors.java
void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
try {
crashApplicationInner(r, crashInfo, callingPid, callingUid);
} finally {
Binder.restoreCallingIdentity(origId);
}
}
3.3.1 AppErrors.crashApplicationInner()
AppErrors.java
void crashApplicationInner(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo,
int callingPid, int callingUid) {
long timeMillis = System.currentTimeMillis();
String shortMsg = crashInfo.exceptionClassName;
String longMsg = crashInfo.exceptionMessage;
String stackTrace = crashInfo.stackTrace;
if (shortMsg != null && longMsg != null) {
longMsg = shortMsg + ": " + longMsg;
} else if (shortMsg != null) {
longMsg = shortMsg;
}
// ...
final int relaunchReason = r != null
? r.getWindowProcessController().computeRelaunchReason() : RELAUNCH_REASON_NONE;
AppErrorResult result = new AppErrorResult();
int taskId;
synchronized (mService) {
// ...
// If we can't identify the process or it's already exceeded its crash quota,
// quit right away without showing a crash dialog.
// 繼續(xù)調(diào)用 makeAppCrashingLocked()
if (r == null || !makeAppCrashingLocked(r, shortMsg, longMsg, stackTrace, data)) {
return;
}
AppErrorDialog.Data data = new AppErrorDialog.Data();
data.result = result;
data.proc = r;
final Message msg = Message.obtain();
msg.what = ActivityManagerService.SHOW_ERROR_UI_MSG;
taskId = data.taskId;
msg.obj = data;
// 發(fā)送消息,彈出crash對話框,等待用戶選擇
mService.mUiHandler.sendMessage(msg);
}
// 得到用戶選擇結(jié)果
int res = result.get();
Intent appErrorIntent = null;
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_CRASH, res);
// 如果是超時(shí)或者取消,則當(dāng)成是強(qiáng)制退出
if (res == AppErrorDialog.TIMEOUT || res == AppErrorDialog.CANCEL) {
res = AppErrorDialog.FORCE_QUIT;
}
synchronized (mService) {
if (res == AppErrorDialog.MUTE) {
stopReportingCrashesLocked(r);
}
// 如果是重新啟動
if (res == AppErrorDialog.RESTART) {
mService.mProcessList.removeProcessLocked(r, false, true, "crash");
if (taskId != INVALID_TASK_ID) {
try {
//1. 從最近的任務(wù)列表中找到崩潰進(jìn)程,再次啟動
mService.startActivityFromRecents(taskId,
ActivityOptions.makeBasic().toBundle());
} catch (IllegalArgumentException e) {
// Hmm...that didn't work. Task should either be in recents or associated
// with a stack.
Slog.e(TAG, "Could not restart taskId=" + taskId, e);
}
}
}
// 如果是退出
if (res == AppErrorDialog.FORCE_QUIT) {
long orig = Binder.clearCallingIdentity();
try {
// Kill it with fire!
// 殺掉這個(gè)進(jìn)程
mService.mAtmInternal.onHandleAppCrash(r.getWindowProcessController());
if (!r.isPersistent()) {
mService.mProcessList.removeProcessLocked(r, false, false, "crash");
mService.mAtmInternal.resumeTopActivities(false /* scheduleIdle */);
}
} finally {
Binder.restoreCallingIdentity(orig);
}
}
// 如果是顯示應(yīng)用信息
if (res == AppErrorDialog.APP_INFO) {
appErrorIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
appErrorIntent.setData(Uri.parse("package:" + r.info.packageName));
appErrorIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (res == AppErrorDialog.FORCE_QUIT_AND_REPORT) {
appErrorIntent = createAppErrorIntentLocked(r, timeMillis, crashInfo);
}
if (r != null && !r.isolated && res != AppErrorDialog.RESTART) {
// XXX Can't keep track of crash time for isolated processes,
// since they don't have a persistent identity.
mProcessCrashTimes.put(r.info.processName, r.uid,
SystemClock.uptimeMillis());
}
}
if (appErrorIntent != null) {
try {
// 2. 啟動一個(gè)系統(tǒng)頁面的intent 來顯示應(yīng)用信息
mContext.startActivityAsUser(appErrorIntent, new UserHandle(r.userId));
} catch (ActivityNotFoundException e) {
Slog.w(TAG, "bug report receiver dissappeared", e);
}
}
}
職責(zé):
繼續(xù)調(diào)用 makeAppCrashingLocked()
發(fā)送 SHOW_ERROR_UI_MSG 消息,根據(jù)錯(cuò)誤信息彈出crash對話框,等待用戶選擇
- 如果選擇重新啟動,則從最近任務(wù)列表中找到崩潰進(jìn)程,再次拉起
- 如果選擇強(qiáng)制退出,則殺掉app,進(jìn)入kill流程
- 如果選擇顯示應(yīng)用信息,則啟動系統(tǒng)頁面的intent,打開應(yīng)用詳情頁面
我們先來看看 makeAppCrashingLocked()方法:
3.4 makeAppCrashingLocked()
private boolean makeAppCrashingLocked(ProcessRecord app,
String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) {
app.setCrashing(true);
// 封裝崩潰信息到 ProcessErrorStateInfo 中
app.crashingReport = generateProcessError(app,
ActivityManager.ProcessErrorStateInfo.CRASHED, null, shortMsg, longMsg, stackTrace);
// 獲取當(dāng)前user的 error receiver;停止廣播接收
app.startAppProblemLocked();
// 停是凍結(jié)屏幕
app.getWindowProcessController().stopFreezingActivities();
// 繼續(xù)調(diào)用 handleAppCrashLocked
return handleAppCrashLocked(app, "force-crash" /*reason*/, shortMsg, longMsg, stackTrace,
data);
}
- 封裝崩潰信息到 ProcessErrorStateInfo 中
- 獲取當(dāng)前user的 error receiver;停止廣播接收
- 停是凍結(jié)屏幕
- 繼續(xù)調(diào)用 handleAppCrashLocked()
3.4.1 ProcessRecord.startAppProblemLocked()
ProcessRecord.java
void startAppProblemLocked() {
// If this app is not running under the current user, then we can't give it a report button
// because that would require launching the report UI under a different user.
errorReportReceiver = null;
for (int userId : mService.mUserController.getCurrentProfileIds()) {
if (this.userId == userId) {
// 找到當(dāng)前用戶的error receiver
errorReportReceiver = ApplicationErrorReport.getErrorReportReceiver(
mService.mContext, info.packageName, info.flags);
}
}
// 停止接收廣播
mService.skipCurrentReceiverLocked(this);
}
//
void skipCurrentReceiverLocked(ProcessRecord app) {
for (BroadcastQueue queue : mBroadcastQueues) {
queue.skipCurrentReceiverLocked(app);
}
}
private void skipReceiverLocked(BroadcastRecord r) {
logBroadcastReceiverDiscardLocked(r);
// 停止廣播接收
finishReceiverLocked(r, r.resultCode, r.resultData,
r.resultExtras, r.resultAbort, false);
scheduleBroadcastsLocked();
}
- 找到當(dāng)前用戶的error receiver 最終會返回 注冊 Intent.ACTION_APP_ERROR的ActivityComponent。
- 停止接收廣播
3.4.2 WindowProcessController.stopFreezingActivities()
WindowProcessController.java
public void stopFreezingActivities() {
synchronized (mAtm.mGlobalLock) {
int i = mActivities.size();
while (i > 0) {
i--;
// mActivities存儲的類型為 ActivityRecord
mActivities.get(i).stopFreezingScreenLocked(true);
}
}
}
ActivityRecord.stopFreezingScreenLocked()
ActivityRecord.java
public void stopFreezingScreenLocked(boolean force) {
if (force || frozenBeforeDestroy) {
frozenBeforeDestroy = false;
if (mAppWindowToken == null) {
return;
}
mAppWindowToken.stopFreezingScreen(true, force);
}
}
最終調(diào)到 AMS的 stopFreezingDisplayLocked() 方法來凍結(jié)屏幕。
3.4.3 handleAppCrashLocked()
boolean handleAppCrashLocked(ProcessRecord app, String reason,
String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) {
final long now = SystemClock.uptimeMillis();
final boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
final boolean procIsBoundForeground =
(app.getCurProcState() == ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
// 確定崩潰的時(shí)間
Long crashTime;
Long crashTimePersistent;
boolean tryAgain = false;
if (!app.isolated) {
crashTime = mProcessCrashTimes.get(app.info.processName, app.uid);
crashTimePersistent = mProcessCrashTimesPersistent.get(app.info.processName, app.uid);
} else {
crashTime = crashTimePersistent = null;
}
// Bump up the crash count of any services currently running in the proc.
// 增加ServiceRecord中crashCount
for (int i = app.services.size() - 1; i >= 0; i--) {
// Any services running in the application need to be placed
// back in the pending list.
ServiceRecord sr = app.services.valueAt(i);
// If the service was restarted a while ago, then reset crash count, else increment it.
if (now > sr.restartTime + ProcessList.MIN_CRASH_INTERVAL) {
sr.crashCount = 1;
} else {
sr.crashCount++;
}
// Allow restarting for started or bound foreground services that are crashing.
// This includes wallpapers.
if (sr.crashCount < mService.mConstants.BOUND_SERVICE_MAX_CRASH_RETRY
&& (sr.isForeground || procIsBoundForeground)) {
tryAgain = true;
}
}
// 同一個(gè)進(jìn)程,如果連續(xù)兩次崩潰的間隔小于 一分鐘,則認(rèn)為崩潰過于頻繁
if (crashTime != null && now < crashTime + ProcessList.MIN_CRASH_INTERVAL) {
// The process crashed again very quickly. If it was a bound foreground service, let's
// try to restart again in a while, otherwise the process loses!
Slog.w(TAG, "Process " + app.info.processName
+ " has crashed too many times: killing!");
EventLog.writeEvent(EventLogTags.AM_PROCESS_CRASHED_TOO_MUCH,
app.userId, app.info.processName, app.uid);
// 2.8.1 回調(diào) atm的onHandleAppCrash
mService.mAtmInternal.onHandleAppCrash(app.getWindowProcessController());
if (!app.isPersistent()) {
// 如果不是persistent進(jìn)程,則不再重啟,除非用戶主動觸發(fā)
// We don't want to start this process again until the user
// explicitly does so... but for persistent process, we really
// need to keep it running. If a persistent process is actually
// repeatedly crashing, then badness for everyone.
if (!app.isolated) {
// XXX We don't have a way to mark isolated processes
// as bad, since they don't have a peristent identity.
mBadProcesses.put(app.info.processName, app.uid,
new BadProcessInfo(now, shortMsg, longMsg, stackTrace));
mProcessCrashTimes.remove(app.info.processName, app.uid);
}
app.bad = true;
app.removed = true;
// Don't let services in this process be restarted and potentially
// annoy the user repeatedly. Unless it is persistent, since those
// processes run critical code.
// 移除進(jìn)程中的所有服務(wù)
mService.mProcessList.removeProcessLocked(app, false, tryAgain, "crash");
// 恢復(fù)頂部的activity
mService.mAtmInternal.resumeTopActivities(false /* scheduleIdle */);
if (!showBackground) {
return false;
}
}
mService.mAtmInternal.resumeTopActivities(false /* scheduleIdle */);
} else {
// 不是一分鐘內(nèi)連續(xù)崩潰
final int affectedTaskId = mService.mAtmInternal.finishTopCrashedActivities(
app.getWindowProcessController(), reason);
if (data != null) {
data.taskId = affectedTaskId;
}
if (data != null && crashTimePersistent != null
&& now < crashTimePersistent + ProcessList.MIN_CRASH_INTERVAL) {
data.repeating = true;
}
}
if (data != null && tryAgain) {
data.isRestartableForService = true;
}
// If the crashing process is what we consider to be the "home process" and it has been
// replaced by a third-party app, clear the package preferred activities from packages
// with a home activity running in the process to prevent a repeatedly crashing app
// from blocking the user to manually clear the list.
final WindowProcessController proc = app.getWindowProcessController();
final WindowProcessController homeProc = mService.mAtmInternal.getHomeProcess();
if (proc == homeProc && proc.hasActivities()
&& (((ProcessRecord) homeProc.mOwner).info.flags & FLAG_SYSTEM) == 0) {
proc.clearPackagePreferredForHomeActivities();
}
if (!app.isolated) {
// XXX Can't keep track of crash times for isolated processes,
// because they don't have a persistent identity.
mProcessCrashTimes.put(app.info.processName, app.uid, now);
mProcessCrashTimesPersistent.put(app.info.processName, app.uid, now);
}
// 如果 app的crashHandler存在,則交給其處理
if (app.crashHandler != null) mService.mHandler.post(app.crashHandler);
return true;
}
職責(zé):
記錄崩潰之間
增加 ServiceRecord 中crashCount數(shù)量
是否是一分鐘內(nèi)連續(xù)崩潰如果是兩次連續(xù)崩潰小于一分鐘,則認(rèn)為是頻繁崩潰。
- 調(diào)用
onHandleAppCrash方法 - 如果不是persistent進(jìn)程,則不再重啟,除非用戶主動觸發(fā)
- 移除進(jìn)程中的所有服務(wù),且不再重啟
- 恢復(fù)棧頂?shù)腶ctivity
- 不是連續(xù)崩潰,則記錄崩潰受影響的taskid
- 如果 app的crashHandler存在,則交給其處理
ATMS.onHandleAppCrash()
ActivityTaskManagerService.java
@Override
public void onHandleAppCrash(WindowProcessController wpc) {
synchronized (mGlobalLock) {
mRootActivityContainer.handleAppCrash(wpc);
}
}
//RootActivityContainer.java
void handleAppCrash(WindowProcessController app) {
// 遍歷所有的ActivityDisplay
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
final ActivityDisplay display = mActivityDisplays.get(displayNdx);
// 遍歷ActivityDisplay中管理的所有 ActivityStack
for (int stackNdx = display.getChildCount() - 1; stackNdx >= 0; --stackNdx) {
// 獲取activity stack對象
final ActivityStack stack = display.getChildAt(stackNdx);
stack.handleAppCrash(app);
}
}
}
>ActivityStack.java
void handleAppCrash(WindowProcessController app) {
// 循環(huán)ActivityStack中管理的 TaskRecord
for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
// 得到 TaskRecord中管理的所有 ActivityRecord集合
final ArrayList<ActivityRecord> activities = mTaskHistory.get(taskNdx).mActivities;
// 遍歷 ActivityRecord集合,得到每一個(gè) ActivityRecord對象
for (int activityNdx = activities.size() - 1; activityNdx >= 0; --activityNdx) {
final ActivityRecord r = activities.get(activityNdx);
// 如果是崩潰的進(jìn)程,則銷毀activity
if (r.app == app) {
// Force the destroy to skip right to removal.
r.app = null;
//
getDisplay().mDisplayContent.prepareAppTransition(
TRANSIT_CRASHING_ACTIVITY_CLOSE, false /* alwaysKeepCurrent */);
// finish銷毀當(dāng)前activity
finishCurrentActivityLocked(r, FINISH_IMMEDIATELY, false,
"handleAppCrashedLocked");
}
}
}
}
職責(zé):
- 遍歷所有ActivityDisplay,得到ActivityDisplay對象 display
- 然后在遍歷display中的所有 ActivityStack對象,stack
- 再遍歷 stack中所有的 TaskRecord對象,record
- 在遍歷record中的所有 ActivityRecord對象,如果屬于崩潰進(jìn)程則銷毀它
3.5 小結(jié)
AMS端在收到App的崩潰后,大概流程如下:
- 把崩潰信息通過 DBS 服務(wù),寫入到Dropbox文件中。dropbox支持錯(cuò)誤類型:
crash、wtf、anr - 停止崩潰進(jìn)程接收廣播;增加ServiceRecord中的crashcount數(shù);銷毀所有的activies;
- 彈出崩潰對話框,等待用戶選擇 3.1. 如果選擇重新啟動,則從最近任務(wù)列表中找到崩潰進(jìn)程,再次拉起 3.2. 如果選擇強(qiáng)制退出,則殺掉app,進(jìn)入kill流程 3.3. 如果選擇顯示應(yīng)用信息,則啟動系統(tǒng)頁面的intent,打開應(yīng)用詳情頁面
回到3.3.1中,當(dāng)處理完 makeAppCrashingLocked()方法邏輯后,會通過AMS的 mUiHandler 發(fā)送 SHOW_ERROR_UI_MSG 彈出 對話框。
四、 mUiHandler發(fā)送 SHOW_ERROR_UI_MSG
AMS.java
final class UiHandler extends Handler {
public UiHandler() {
super(com.android.server.UiThread.get().getLooper(), null, true);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_ERROR_UI_MSG: {
mAppErrors.handleShowAppErrorUi(msg);
ensureBootCompleted();
} break;
// ...
4.1 handleShowAppErrorUi()
AppErrors.java
void handleShowAppErrorUi(Message msg) {
AppErrorDialog.Data data = (AppErrorDialog.Data) msg.obj;
boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
AppErrorDialog dialogToShow = null;
final String packageName;
final int userId;
synchronized (mService) {
// 獲取進(jìn)程信息
final ProcessRecord proc = data.proc;
final AppErrorResult res = data.result;
if (proc == null) {
Slog.e(TAG, "handleShowAppErrorUi: proc is null");
return;
}
packageName = proc.info.packageName;
userId = proc.userId;
// 如果已經(jīng)有對話框,則不再彈出
if (proc.crashDialog != null) {
Slog.e(TAG, "App already has crash dialog: " + proc);
if (res != null) {
res.set(AppErrorDialog.ALREADY_SHOWING);
}
return;
}
boolean isBackground = (UserHandle.getAppId(proc.uid)
>= Process.FIRST_APPLICATION_UID
&& proc.pid != MY_PID);
for (int profileId : mService.mUserController.getCurrentProfileIds()) {
isBackground &= (userId != profileId);
}
if (isBackground && !showBackground) {
Slog.w(TAG, "Skipping crash dialog of " + proc + ": background");
if (res != null) {
res.set(AppErrorDialog.BACKGROUND_USER);
}
return;
}
final boolean showFirstCrash = Settings.Global.getInt(
mContext.getContentResolver(),
Settings.Global.SHOW_FIRST_CRASH_DIALOG, 0) != 0;
final boolean showFirstCrashDevOption = Settings.Secure.getIntForUser(
mContext.getContentResolver(),
Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
0,
mService.mUserController.getCurrentUserId()) != 0;
final boolean crashSilenced = mAppsNotReportingCrashes != null &&
mAppsNotReportingCrashes.contains(proc.info.packageName);
if ((mService.mAtmInternal.canShowErrorDialogs() || showBackground)
&& !crashSilenced
&& (showFirstCrash || showFirstCrashDevOption || data.repeating)) {
// 創(chuàng)建對話框,5分鐘超時(shí)等待,超時(shí)后自動關(guān)閉
proc.crashDialog = dialogToShow = new AppErrorDialog(mContext, mService, data);
} else {
// The device is asleep, so just pretend that the user
// saw a crash dialog and hit "force quit".
if (res != null) {
res.set(AppErrorDialog.CANT_SHOW);
}
}
}
// If we've created a crash dialog, show it without the lock held
if (dialogToShow != null) {
Slog.i(TAG, "Showing crash dialog for package " + packageName + " u" + userId);
// 彈出對話框
dialogToShow.show();
}
}
邏輯很簡單,就是獲取進(jìn)程的信息,并且展示錯(cuò)誤對話框。5分鐘用戶沒有選擇,則自動關(guān)閉。
- 如果用戶選擇應(yīng)用信息,則展示應(yīng)用的運(yùn)行信息
- 如果選擇關(guān)閉應(yīng)用,則執(zhí)行殺應(yīng)用流程
- 如果不選擇,5分鐘后自動關(guān)閉。
在1和3中都還沒有執(zhí)行殺應(yīng)用流程,回顧2.2中的流程,在finally語句中都會走殺進(jìn)程邏輯。
finally {
// Try everything to make sure this process goes away.
// 最終關(guān)閉kill掉進(jìn)程
Process.killProcess(Process.myPid());
System.exit(10);
}
4.2 Process.killProcess()
public static final void killProcess(int pid) {
sendSignal(pid, SIGNAL_KILL);
}
public static final native void sendSignal(int pid, int signal);
給指定的進(jìn)程發(fā)送一個(gè) SIGNAL_KILL 信號。具體的殺進(jìn)程流程,后續(xù)再單獨(dú)分析。
至此,應(yīng)用進(jìn)程已經(jīng)被殺死,但是還沒完。因?yàn)閟ystem server進(jìn)程中有注冊Binder服務(wù)的死亡監(jiān)聽。當(dāng)App進(jìn)程死亡后,會回調(diào)到AMS 的死亡監(jiān)聽中,此時(shí)還需要處理Binder死亡通知回調(diào)邏輯。
五、Binder服務(wù)死亡通知
那么,AMS是什么時(shí)候注冊死亡通知的呢?
還記得在創(chuàng)建進(jìn)程的過程中,ActivityThread會調(diào)用AMS的 attachApplication(), 內(nèi)部會調(diào)用到 attachApplicationLocked()方法。在這里注冊的Binder的死亡通知。
5.1 AMS.attachApplicationLocked()
@GuardedBy("this")
private final boolean attachApplicationLocked(IApplicationThread thread,
int pid, int callingUid, long startSeq) {
//...
try {
AppDeathRecipient adr = new AppDeathRecipient(
app, pid, thread);
thread.asBinder().linkToDeath(adr, 0);
app.deathRecipient = adr;
} catch (RemoteException e) {
app.resetPackageList(mProcessStats);
mProcessList.startProcessLocked(app,
new HostingRecord("link fail", processName));
return false;
}
//...
}
當(dāng)有binder服務(wù)死亡,會調(diào)用 AppDeathRecipient 的 binderDied()方法:
5.2 AppDeathRecipient.binderDied()
AMS.java
@Override
public void binderDied() {
if (DEBUG_ALL) Slog.v(
TAG, "Death received in " + this
+ " for thread " + mAppThread.asBinder());
synchronized(ActivityManagerService.this) {
appDiedLocked(mApp, mPid, mAppThread, true);
}
}
5.2.1 appDiedLocked()
@GuardedBy("this")
final void appDiedLocked(ProcessRecord app, int pid, IApplicationThread thread,
boolean fromBinderDied) {
// First check if this ProcessRecord is actually active for the pid.
synchronized (mPidsSelfLocked) {
ProcessRecord curProc = mPidsSelfLocked.get(pid);
if (curProc != app) {
Slog.w(TAG, "Spurious death for " + app + ", curProc for " + pid + ": " + curProc);
return;
}
}
BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics();
synchronized (stats) {
stats.noteProcessDiedLocked(app.info.uid, pid);
}
// 如果沒有被殺,再次殺app
if (!app.killed) {
if (!fromBinderDied) {
killProcessQuiet(pid);
}
ProcessList.killProcessGroup(app.uid, pid);
app.killed = true;
}
// Clean up already done if the process has been re-started.
if (app.pid == pid && app.thread != null &&
app.thread.asBinder() == thread.asBinder()) {
boolean doLowMem = app.getActiveInstrumentation() == null;
boolean doOomAdj = doLowMem;
if (!app.killedByAm) {
reportUidInfoMessageLocked(TAG,
"Process " + app.processName + " (pid " + pid + ") has died: "
+ ProcessList.makeOomAdjString(app.setAdj, true) + " "
+ ProcessList.makeProcStateString(app.setProcState), app.info.uid);
mAllowLowerMemLevel = true;
} else {
// Note that we always want to do oom adj to update our state with the
// new number of procs.
mAllowLowerMemLevel = false;
doLowMem = false;
}
// 調(diào)用 handleAppDiedLocked
handleAppDiedLocked(app, false, true);
if (doOomAdj) {
updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_PROCESS_END);
}
if (doLowMem) {
doLowMemReportIfNeededLocked(app);
}
}
//...
}
5.2.2 handleAppDiedLocked()
final void handleAppDiedLocked(ProcessRecord app,
boolean restarting, boolean allowRestart) {
int pid = app.pid;
// 清理service、broadcastreveiver、contentprovider等信息
boolean kept = cleanUpApplicationRecordLocked(app, restarting, allowRestart, -1,
false /*replacingPid*/);
if (!kept && !restarting) {
// 移除崩潰進(jìn)程在AMS中的代表 ProcessRecord
removeLruProcessLocked(app);
if (pid > 0) {
ProcessList.remove(pid);
}
}
if (mProfileData.getProfileProc() == app) {
clearProfilerLocked();
}
// 繼續(xù)調(diào)用 atm的 handleAppDied
mAtmInternal.handleAppDied(app.getWindowProcessController(), restarting, () -> {
Slog.w(TAG, "Crash of app " + app.processName
+ " running instrumentation " + app.getActiveInstrumentation().mClass);
Bundle info = new Bundle();
info.putString("shortMsg", "Process crashed.");
finishInstrumentationLocked(app, Activity.RESULT_CANCELED, info);
});
}
- 清理service、broadcastreveiver、contentprovider等信息
- 移除移除崩潰進(jìn)程 ProcessRecord
- 繼續(xù)調(diào)用 atm的 handleAppDied
5.3 cleanUpApplicationRecordLocked()
該方法清理崩潰進(jìn)程相關(guān)的所有信息。
final boolean cleanUpApplicationRecordLocked(ProcessRecord app,
boolean restarting, boolean allowRestart, int index, boolean replacingPid) {
if (index >= 0) {
removeLruProcessLocked(app);
ProcessList.remove(app.pid);
}
mProcessesToGc.remove(app);
mPendingPssProcesses.remove(app);
ProcessList.abortNextPssTime(app.procStateMemTracker);
// 關(guān)閉所有已經(jīng)打開的對話框: crash、anr、wait等
// Dismiss any open dialogs.
if (app.crashDialog != null && !app.forceCrashReport) {
app.crashDialog.dismiss();
app.crashDialog = null;
}
if (app.anrDialog != null) {
app.anrDialog.dismiss();
app.anrDialog = null;
}
if (app.waitDialog != null) {
app.waitDialog.dismiss();
app.waitDialog = null;
}
app.setCrashing(false);
app.setNotResponding(false);
app.resetPackageList(mProcessStats);
app.unlinkDeathRecipient();
app.makeInactive(mProcessStats);
app.waitingToKill = null;
app.forcingToImportant = null;
updateProcessForegroundLocked(app, false, 0, false);
app.setHasForegroundActivities(false);
app.hasShownUi = false;
app.treatLikeActivity = false;
app.hasAboveClient = false;
app.setHasClientActivities(false);
// 移除所有service 信息
mServices.killServicesLocked(app, allowRestart);
boolean restart = false;
// 移除所有的contentprovicer信息
// Remove published content providers.
for (int i = app.pubProviders.size() - 1; i >= 0; i--) {
ContentProviderRecord cpr = app.pubProviders.valueAt(i);
final boolean always = app.bad || !allowRestart;
boolean inLaunching = removeDyingProviderLocked(app, cpr, always);
if ((inLaunching || always) && cpr.hasConnectionOrHandle()) {
// We left the provider in the launching list, need to
// restart it.
restart = true;
}
cpr.provider = null;
cpr.setProcess(null);
}
app.pubProviders.clear();
// Take care of any launching providers waiting for this process.
if (cleanupAppInLaunchingProvidersLocked(app, false)) {
restart = true;
}
// Unregister from connected content providers.
if (!app.conProviders.isEmpty()) {
for (int i = app.conProviders.size() - 1; i >= 0; i--) {
ContentProviderConnection conn = app.conProviders.get(i);
conn.provider.connections.remove(conn);
stopAssociationLocked(app.uid, app.processName, conn.provider.uid,
conn.provider.appInfo.longVersionCode, conn.provider.name,
conn.provider.info.processName);
}
app.conProviders.clear();
}
// At this point there may be remaining entries in mLaunchingProviders
// where we were the only one waiting, so they are no longer of use.
// Look for these and clean up if found.
// XXX Commented out for now. Trying to figure out a way to reproduce
// the actual situation to identify what is actually going on.
if (false) {
for (int i = mLaunchingProviders.size() - 1; i >= 0; i--) {
ContentProviderRecord cpr = mLaunchingProviders.get(i);
if (cpr.connections.size() <= 0 && !cpr.hasExternalProcessHandles()) {
synchronized (cpr) {
cpr.launchingApp = null;
cpr.notifyAll();
}
}
}
}
//移除所有的廣播信息
skipCurrentReceiverLocked(app);
// Unregister any receivers.
for (int i = app.receivers.size() - 1; i >= 0; i--) {
removeReceiverLocked(app.receivers.valueAt(i));
}
app.receivers.clear();
//清理App所有的備份 信息
// If the app is undergoing backup, tell the backup manager about it
final BackupRecord backupTarget = mBackupTargets.get(app.userId);
if (backupTarget != null && app.pid == backupTarget.app.pid) {
if (DEBUG_BACKUP || DEBUG_CLEANUP) Slog.d(TAG_CLEANUP, "App "
+ backupTarget.appInfo + " died during backup");
mHandler.post(new Runnable() {
@Override
public void run(){
try {
IBackupManager bm = IBackupManager.Stub.asInterface(
ServiceManager.getService(Context.BACKUP_SERVICE));
bm.agentDisconnectedForUser(app.userId, app.info.packageName);
} catch (RemoteException e) {
// can't happen; backup manager is local
}
}
});
}
for (int i = mPendingProcessChanges.size() - 1; i >= 0; i--) {
ProcessChangeItem item = mPendingProcessChanges.get(i);
if (app.pid > 0 && item.pid == app.pid) {
mPendingProcessChanges.remove(i);
mAvailProcessChanges.add(item);
}
}
mUiHandler.obtainMessage(DISPATCH_PROCESS_DIED_UI_MSG, app.pid, app.info.uid,
null).sendToTarget();
// If the caller is restarting this app, then leave it in its
// current lists and let the caller take care of it.
if (restarting) {
return false;
}
if (!app.isPersistent() || app.isolated) {
if (DEBUG_PROCESSES || DEBUG_CLEANUP) Slog.v(TAG_CLEANUP,
"Removing non-persistent process during cleanup: " + app);
if (!replacingPid) {
mProcessList.removeProcessNameLocked(app.processName, app.uid, app);
}
mAtmInternal.clearHeavyWeightProcessIfEquals(app.getWindowProcessController());
} else if (!app.removed) {
// This app is persistent, so we need to keep its record around.
// If it is not already on the pending app list, add it there
// and start a new process for it.
if (mPersistentStartingProcesses.indexOf(app) < 0) {
mPersistentStartingProcesses.add(app);
restart = true;
}
}
if ((DEBUG_PROCESSES || DEBUG_CLEANUP) && mProcessesOnHold.contains(app)) Slog.v(
TAG_CLEANUP, "Clean-up removing on hold: " + app);
mProcessesOnHold.remove(app);
mAtmInternal.onCleanUpApplicationRecord(app.getWindowProcessController());
if (restart && !app.isolated) {
// We have components that still need to be running in the
// process, so re-launch it.
if (index < 0) {
ProcessList.remove(app.pid);
}
mProcessList.addProcessNameLocked(app);
app.pendingStart = false;
mProcessList.startProcessLocked(app,
new HostingRecord("restart", app.processName));
return true;
} else if (app.pid > 0 && app.pid != MY_PID) {
// Goodbye!
mPidsSelfLocked.remove(app);
mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
mBatteryStatsService.noteProcessFinish(app.processName, app.info.uid);
if (app.isolated) {
mBatteryStatsService.removeIsolatedUid(app.uid, app.info.uid);
}
app.setPid(0);
}
return false;
}
職責(zé):
清理所有跟崩潰進(jìn)程相關(guān)的service、provider、receiver等信息。
5.4 atms.handleAppDied()
ActivityTaskManagerService.java
@HotPath(caller = HotPath.PROCESS_CHANGE)
@Override
public void handleAppDied(WindowProcessController wpc, boolean restarting,
Runnable finishInstrumentationCallback) {
synchronized (mGlobalLockWithoutBoost) {
// Remove this application's activities from active lists.
// 清理activities相關(guān)信息
boolean hasVisibleActivities = mRootActivityContainer.handleAppDied(wpc);
wpc.clearRecentTasks();
wpc.clearActivities();
if (wpc.isInstrumenting()) {
finishInstrumentationCallback.run();
}
if (!restarting && hasVisibleActivities) {
mWindowManager.deferSurfaceLayout();
try {
if (!mRootActivityContainer.resumeFocusedStacksTopActivities()) {
// If there was nothing to resume, and we are not already restarting
// this process, but there is a visible activity that is hosted by the
// process...then make sure all visible activities are running, taking
// care of restarting this process.
// 確?;謴?fù)頂部的activity
mRootActivityContainer.ensureActivitiesVisible(null, 0,
!PRESERVE_WINDOWS);
}
} finally {
// windows相關(guān)
mWindowManager.continueSurfaceLayout();
}
}
}
}
- 清理activities相關(guān)信息
- 確?;謴?fù)頂部的activity
- 更新windows相關(guān)信息
至此,Binder死亡通知后的處理流程也基本走完,App的整個(gè)java crash流程也宣告結(jié)束了。
小結(jié)
當(dāng)App發(fā)生崩潰后,除了彈出對話框,發(fā)送kill命令殺掉自身后。AMS還會收到App進(jìn)程的Binder服務(wù)死亡通知,只有當(dāng)走完Binder的 binderDied()流程后,整個(gè)崩潰流程才算真正結(jié)束。
參考:
http://www.dbjr.com.cn/article/263031.htm
以上就是Android Java crash 處理流程詳解的詳細(xì)內(nèi)容,更多關(guān)于Android Java crash處理流程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)獲取聯(lián)系人電話號碼功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)獲取聯(lián)系人電話號碼功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android自定義View實(shí)現(xiàn)五子棋小游戲
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)五子棋小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11
Flutter給控件實(shí)現(xiàn)鉆石般的微光特效
這篇文章主要給大家介紹了關(guān)于Flutter給控件實(shí)現(xiàn)鉆石般的微光特效的相關(guān)資料,實(shí)現(xiàn)的效果非常不錯(cuò),非常適合大家做開發(fā)的時(shí)候參考,需要的朋友可以參考下2021-08-08
Android利用ViewPager實(shí)現(xiàn)用戶引導(dǎo)界面效果的方法
這篇文章主要介紹了Android利用ViewPager實(shí)現(xiàn)用戶引導(dǎo)界面效果的方法,結(jié)合實(shí)例形式詳細(xì)分析了Android軟件功能界面的初始化、view實(shí)例化、動畫功能實(shí)現(xiàn)與布局相關(guān)技巧,需要的朋友可以參考下2016-07-07
android studio的使用sdk manager的方法
這篇文章主要介紹了android studio的使用sdk manager的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
Android ContentProvider實(shí)現(xiàn)獲取手機(jī)聯(lián)系人功能
這篇文章主要為大家詳細(xì)介紹了Android ContentProvider實(shí)現(xiàn)獲取手機(jī)聯(lián)系人功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07
Android應(yīng)用圖標(biāo)在狀態(tài)欄上顯示實(shí)現(xiàn)原理
Android應(yīng)用圖標(biāo)在狀態(tài)欄上顯示,以及顯示不同的圖標(biāo),其實(shí)很研究完后,才發(fā)現(xiàn),很簡單,具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06

