Activity常見問題
通過這篇博客,我們能知道以下問題:
Activity各種情況下的生命周期- 彈出
Dialog對Activity生命周期有什么影響? onActivityResult()在哪兩個生命周期之間回呼?Activity在onResume()之后才顯示的原因是什么?- 通過 Sheme 協議打開
Activity Activity什么時候會發生重建?- 在
Activity的onCreate()方法里寫死回圈會 ANR 嗎?
1. Activity 生命周期方法相關
生命周期的回呼方法
Activity生命周期的回呼主要有 onCreate()、onRestart()、onStart()、onResume()、onPause()、onStop()、onDestory() 幾個方法,
呼叫時機說明:
onCreate():系統創建Activity時觸發,用來初始化 Activity 的基本組件,onRestart():當處于“已停止”狀態的Activity即將重啟時,系統就會呼叫此回呼,onStart():Activity將進入“已啟動”狀態,并對用戶可見,onResume():系統會在Activity開始與用戶互動之前呼叫此回呼,此時,該Activity位于Activity堆疊的頂部,并會捕獲所有用戶輸入onPause():當Activity失去焦點并進入“已暫停”狀態時,系統就會呼叫此回呼,onStop():當Activity對用戶不再可見時,系統就會呼叫此回呼,onDestory():系統會在銷毀Activity之前呼叫此回呼,此回呼是Activity接收的最后一個回呼,通常,實作onDestroy()是為了確保該Activity的所有資源,
onStart() 和 onStop() 是從 Activity 是否可見來回呼的,onResume() 和 onPause() 是從 Activity 是否位于前臺來回呼的,
在生命周期里幾種狀態:
-
Activity的整個生命周期發生在onCreate()與onDestroy()之間,在onCreate()中執行“全域”狀態設定(例如狀態布局),并在onDestroy()中釋放資源, -
Activity的可見生命周期發生在onStart()與onStop()之間,在這段時間內Activity對用戶可見,在整個生命周期中,當 Activity 在對用戶可見和隱藏倆種狀態中交替變化時,系統會多次呼叫onStart()和onStop(), -
Activity的前臺生命周期發生在onResume()與onPause()之間,Activity位于其他Activity之前(堆疊頂位置),可與用戶互動并具有輸入焦點,但狀態改變頻繁,系統會多次呼叫onResume()和onPause(),建議做些輕量級操作,
打開Activity時兩個Activity的生命周期方法的回呼順序
當一個 Activity 啟動另一個 Activity 時,它們都會經歷生命周期轉換,第一個 Activity 停止運行并進入“已暫停”或“已停止”狀態,同時創建另一個 Activity,
-
ActivityA 啟動ActivityB,BActivity的launchMode為standard或者 BActivity沒有可復用的實體時: A.onPause()-> B.onCreate()-> B.onStart()-> B.onResume()-> A.onStop()--> A.onDestory()(如果需要關閉Activity A[A 被移出堆疊]) -
ActivityA 啟動ActivityB,BActivity的launchMode為singleTop且 BActivity已經在堆疊頂時(一些特殊情況如通知欄點擊、連點、BActivity自己打開自己),此時只有 B 頁面自己有生命周期變化:B.onPause()-> B.onNewIntent()-> B.onResume() -
當 B
Activity的launchMode為singleInstance,singleTask且對應的 BActivity有可復用的實體時:A.onPause()-> B.onNewIntent()-> B.onRestart()-> B.onStart()-> B.onResume()-> A.onStop()--> A.onDestory()(如果需要關閉Activity A[A 被移出堆疊]) -
當 B
Activity的Theme為Dialog時:A.onPause()-> B.onCreate()-> B.onStart()-> B.onResume()(注意前一個Activity不會回呼onStop(),因為只有在Activity切到后臺(不在Activity堆疊頂)不可見才會回呼onStop();而彈出Dialog主題的Activity時前一個頁面還是可見的,只是失去了焦點而已所以僅有onPause()回呼)
2. 彈出 Dialog 對 Activity 生命周期有什么影響?
**先下結論:**通過 《Android Activity——啟動程序探索(一)》 《Android Activity——啟動程序探索(二)》 《Android Activity——啟動程序探索(三)》 我們知道Activity生命周期回呼都是通過ActivityTaskManagerService(ATMS)(在Android 10之前是通過 ActivityManagerService(AMS))來回呼的,但是彈出 Dialog、Toast、PopupWindow 本質上都直接是通過 WindowManager.addView() 顯示的(沒有經過 ATMS/AMS),所以不會對生命周期有任何影響,
Dialog顯示程序原始碼決議
原始碼版本為 Android 10(Api 29),不同Android版本可能有一些差別
-
構造方法中獲取
WindowManager和創建PhoneWindow并通過PhoneWindow的setWindowManager()方法關聯public Dialog(@NonNull Context context, @StyleRes int themeResId) { this(context, themeResId, true); } Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) { // 獲取 WindManager,實際為實作類 WindowManagerImpl mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // 創建 PhoneWindow 物件 final Window w = new PhoneWindow(mContext); mWindow = w; w.setCallback(this); w.setOnWindowDismissedCallback(this); w.setOnWindowSwipeDismissedCallback(() -> { if (mCancelable) { cancel(); } }); w.setWindowManager(mWindowManager, null, null); w.setGravity(Gravity.CENTER); mListenersHandler = new ListenersHandler(this); } -
Dialog#show()方法原始碼實作public void show() { mCanceled = false; if (!mCreated) { dispatchOnCreate(null); // 沒有創建過的話,呼叫 onCreate() 方法 } else { // 創建過的話,就不在呼叫 onCreate() 回呼,只是修改配置 final Configuration config = mContext.getResources().getConfiguration(); mWindow.getDecorView().dispatchConfigurationChanged(config); } onStart(); // 呼叫 onStart() 回呼方法 mDecor = mWindow.getDecorView(); // 通過 PhoneWindow 獲取 DecorView ... // 省略,設定默認logo、icon以及確定軟鍵盤模式等 // 通過 WindowManager(實際為WindowManagerImpl物件)將DecorView增加表單上 mWindowManager.addView(mDecor, l); ... // 設定軟鍵盤模式 mShowing = true; // 修改顯示標記為 true sendShowMessage(); } // WindowManagerImpl 的 addView() 方法 @Override public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); // 呼叫 WindowManagerGlobal 的 addView() 方法 mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); } // WindowManagerGlobal 的 addView() 方法核心代碼 public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ViewRootImpl root; View panelParentView = null; synchronized (mLock) { // 創建 ViewRootImpl 物件 root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); // 呼叫 ViewRootImpl 的 setView() 方法 root.setView(view, wparams, panelParentView); } } // ViewRootImpl 的 setView() 方法核心代碼 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; // 呼叫 requestLayout() 方法,進行布局(包括measue、layout、draw) requestLayout(); mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); // 通過呼叫 Session 的 addToDisplay() 方法 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel, mTempInsets); setFrame(mTmpFrame); } } }
ViewRootImpl 的 setView() 方法說明:
-
mWindowSession是通過WindowManagerGlobal的getWindowSession()方法獲取到的,回傳的是通過WindowManagerService的openSession()方法創建的Session物件 -
mWindow 物件為
ViewRootImpl的 內部類static class W extends IWindow.Stub,通過定義可以看出來,是一個Binder物件 -
Session物件的addToDisplay()方法Override public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel, InsetsState outInsetsState) { // mService 為 WindowManagerService,window 就是上面的 `ViewRootImpl` 的 內部類 `static class W extends IWindow.Stub` return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame, outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel, outInsetsState); }
通過這個方法就能夠實作客戶端與 WindowManagerService 的雙向呼叫了(應用端通過 mWindowSession 呼叫 WMS, WMS 通過 mWindow (一個 Binder 物件) 呼叫應用端)
DialogFragment原始碼決議
注意:該部分原始碼來自 support v4 包
看 DialogFragment 的定義
class DialogFragment extends Fragment
發現就是直接繼承了 Fragment,那么就可以肯定,Fragment 有的 DialogFragment 都有,像生命周期方法等,又因為我們可以通過 getDialog() 方法獲取到 Dialog ,所以可以肯定它包含一個了 Dialog,接著我們先看 show() 方法
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
發現就是將 DialogFragment 顯示出來,那我們接著找一下,看看 Dialog 是在哪里創建的,發現如下代碼:
@Override
public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
mDialog = onCreateDialog(savedInstanceState);
return (LayoutInflater) mActivity.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new Dialog(getActivity(), getTheme());
}
發現是在 getLayoutInflater() 方法中創建的,那么這個 getLayoutInflater() 方法又是在哪里被呼叫的呢?通過查找我們發現這個方法是在 FragmentManager 中呼叫的,呼叫完這個方法之后就會呼叫 Fragment 的 onViewCreated() 方法;而 方法的回傳值 LayoutInflater 就是 Fragment 中回呼方法 onCreateView() 的引數,
分析到這里,我們就知道了,DialogFragment 實際上是一個 Fragment ,只是他在 onViewCreated() 方法之前通過 getLayoutInflater() 方法創建了一個 Dialog,也就是一個普通的 Fragment 中包含了一個 Dialog,那么又出現了問題,我們剛剛看了 show() 方法,發現并沒有呼叫 Dialog,show() 方法,那么 Dialog 是怎么顯示的呢?我們前面就說過,DialogFragment 實際上是一個 Fragment,Fragment 有的 DialogFragment 都有;我們都知道Fragment有一個生命周期方法 onStart() 是在用戶可見時被呼叫的,那么 Dialog.show() 方法是否在該方法種呼叫了,看一下代碼:
@Override
public void onStart() {
super.onStart();
if (mDialog != null) {
mViewDestroyed = false;
mDialog.show();
}
}
果然,在 Fragment 的生命周期方法 onStart()回呼中,呼叫了 Dialog.show() 方法,那么當我么呼叫 DialogFragment 的 show() 方法時,把將 DialogFragment 顯示出來了,那么 Fragment 的生命周期方法 onStart() 自然會被回呼,Dialog 自然也就顯示出來了,Fragment 的生命周期方法回呼并非由 ActivityManagerService(AMS)【Android 10 以上 ActivityTaskManagerService(ATMS)】來控制,而是通過 FragmentManager 回掉的,也不會影響到 Activity 的生命周期,
3. onActivityResult 在哪兩個生命周期之間回呼?
onActivityResult 不屬于 Activity 的生命周期,但是答案很簡單,因為在 onActivityResult() 方法的注釋中就寫著答案:「You will receive this call immediately before onResume() when your activity is re-starting.」 所以 onActivityResult() 回呼先于該 Activity 的所有生命周期回呼,從 B Activity 回傳 A Activity 的生命周期呼叫為:
B.onPause() -> A.onActivityResult() -> A.onRestart() -> A.onStart() -> A.onResume()
4. Activity 在 onResume 之后才顯示的原因是什么?
通過《Android自定義View之Activity頁面的組成》我們知道了在 Activity 中呼叫 setContentView() 方法會將我們的 View 添加到 DecorView 中,DecorView 也是一個控制元件,他要加載到界面上讓用戶可見,是通過 Window (也就是 PhoneWindow)來完成的,所以我們只要知道 PhoneWindow 是在什么時候將 DecorView 加載出來的,就知道了用戶是在什么時候才可見布局資訊,那么,到底是什么時候開始將 DecorView 添加到 PhoneWindow 中的了, 在 《Android Activity——啟動程序探索(二)》 中,我們說到了 Activity 的回呼 onResume() 方法,中間有一段是會呼叫 ActivityThread#handleResumeActivity() 方法,我們來看一下這個方法的原始碼(省略大部分代碼之后的核心代碼):
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
// 回呼 onResume() 方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow(); // 獲取PhoneWindow并賦值
View decor = r.window.getDecorView(); // 獲取 DecorView 控制元件
decor.setVisibility(View.INVISIBLE);
// 獲取ViewManager,也就是WindowManager(因為interface WindowManager extends ViewManager)
ViewManager wm = a.getWindowManager();
a.mWindowAdded = true;
wm.addView(decor, l); // 關鍵點
}
}
方法說明:
-
ViewManager wm = a.getWindowManager()方法最侄訓傳的是WindowManagerImpl物件,原因如下:Activity.getWindowManager()方法回傳Activity的成員變數mWindowManagerActivity的成員變數mWindowManager在Activity的attach()方法中被賦值mWindowManager = mWindow.getWindowManager(),呼叫的是Window的getWindowManager()方法Window的getWindowManager()方法回傳的是他的成員變數mWindowManagerWindow的成員變數mWindowManager在他的setWindowManager()方法中被賦值mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this),所以得出結論a.getWindowManager()方法最侄訓傳的是WindowManagerImpl物件
-
關鍵點
wm.addView(decor, l)呼叫的就是WindowManagerImpl#addView()方法了,之后的呼叫程序在當前文章前面【Dialog#show()方法原始碼實作】 部分已經說過了,就不在重復了,
通過以上分析,我們就知道了實際上 Activity 中的布局內容是在 onResume() 回呼之后才掛載到 Window 上的,也就是這個時候才是對用戶可見的,
5. 通過 URL Sheme 協議打開 Activity
URL Scheme 是一種頁面內跳轉協議,通過這個協議可以比較方便的跳轉到app某一個頁面,也可以通過 h5 打開App頁面,
URL Sheme 的格式:[scheme]😕/[host][:port]/[path]?[query]
- scheme:協議名稱 [scheme]😕/ 為必填,其他為非必填
- host:主機名(域名)
- port:埠號
- path:路徑
- query:引數(多個引數之間用 & 分割,類似 get 請求)
使用
1. APP端需要能通過 sheme 打開的 Activity 進行配置 intent 過濾器(在清單檔案中配置)
<activity android:name=".TestActivity">
<!--Android 接收外部跳轉過濾器-->
<!--要想在別的App上能成功調起App,必須添加intent過濾器-->
<intent-filter>
<!-- 協議部分配置 ,注意需要跟web配置相同-->
<data
android:host="action.test.activity"
android:pathPrefix="/test"
android:port="999"
android:scheme="testapp" />
<!--下面這幾行也必須得設定-->
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
data屬性說明:
-
scheme: 協議名稱,必填,其他為非必填(由開發人員自定義)
-
host: 域名
-
port:埠
-
path: 完整的路徑,如:http://example.com/blog/abc.html,這里將 path 設定為 /blog/abc.html 才能夠進行匹配
-
pathPrefix:路徑的開頭部分,拿上來的 Uri 來說,這里將 pathPrefix 設定為 /blog 就能進行匹配了
-
pathPattern:用運算式來匹配整個路徑,匹配符如下:
- “” 用來匹配0次或更多,如:“a” 可以匹配“a”、“aa”、“aaa”…
- “.” 用來匹配任意字符,如:“.” 可以匹配“a”、“b”,“c”…
- 因此 “.*” 就是用來匹配任意字符0次或更多
2. 在 Activity 獲取 sheme 資訊:
Uri uri = getIntent().getData();
if (uri != null) {
// 完整的url資訊
Log.i(TAG, "url:" + uri);
// scheme部分
String scheme = uri.getScheme();
Log.i(TAG, "scheme:" + scheme);
// host部分
String host = uri.getHost();
Log.i(TAG, "host:" + host);
// port部分
int port = uri.getPort();
Log.i(TAG, "port:" + port);
// 訪問路勁
String path = uri.getPath();
Log.i(TAG, "path:" + path);
List<String> pathSegments = uri.getPathSegments();
Log.i(TAG, "pathSegments:" + pathSegments);
// Query部分
String query = uri.getQuery();
Log.i(TAG, "query:" + query);
//獲取指定引數值
String params1 = uri.getQueryParameter("params1");
String params2 = uri.getQueryParameter("params2");
Log.i(TAG, "params1:" + params1 + " params2:" + params2);
}
3. 打開 Activity 的方式
1. 通過原生代碼打開
String uri = "testapp://action.test.activity:999/test?params1=1¶ms2=true";
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
startActivity(intent);
1. 通過h5代碼打開(使用瀏覽器打開包含如下代碼的 html 檔案)
<html>
<title>測驗</title>
<body>
<a href="testapp://action.test.activity:999/test?params1=1¶ms2=true">打開APP</a>
</body>
</html>
6. Activity 什么時候會發生重建?
發生時機:
- 系統資源回收:記憶體不足,低優先級的Activity會被殺死
- 配置發生變化:當系統配置發生變化時,比如螢屏方向、語言的改變
橫豎屏切換不銷毀重建的方式
在清單檔案中為該 Activity 配置 android:configChanges 屬性,屬性值 orientation|screenSize(Api級別為13以下時只需要配置 orientation 即可) 對應著旋轉螢屏,locale 對應著語言變化,如此,在配置發生變化時,不會導致重建,而是走 onConfigurationChanged() 回呼方法,
保存和恢復住狀態
保存和恢復狀態分別通過 onSaveInstanceState() 方法和 onRestoreInstanceState() 方法實作,
Activity的onSaveInstanceState()執行在onStop()之前,與onPause()沒有固定的時序關系;onRestoreInstanceState()執行在onStart()之后,不止Activity有這兩個方法,每個View也有這兩個方法,用來保存View的資訊,- 正常情況下的活動銷毀并不會呼叫這兩個方法,只有當活動例外銷毀并且有機會重現展示的時候才會進行呼叫,
- 在
Activity中onRestoreInstanceState()和onCreate()都可以進行資料恢復作業,但建議采用在onRestoreInstanceState()中去恢復(因為呼叫了這個方法,用來存取資料的Bundle肯定不為null), - 在
onSaveInstanceState()和onRestoreInstanceState()這兩個方法中,系統會默認為我們進行一定的恢復作業,比如:會為布局中的每個View呼叫相應的onSaveInstanceState()方法,讓每個視圖都能提供有關自身的應保存資訊,
具體呼叫時機說明
Activity#onSaveInstanceState() 方法呼叫時機:
- 當用戶按下HOME鍵時
- 從最近應用中選擇運行其他的程式時
- 按下電源按鍵(關閉螢屏顯示)時
- 從當前activity啟動一個新的activity時
- 螢屏方向切換時(無論豎屏切橫屏還是橫屏切豎屏都會呼叫)
- 被系統回收時
Activity#onRestoreInstanceState() 方法呼叫時機:
- 被系統回收且重建時
- 螢屏方向切換時(無論豎屏切橫屏還是橫屏切豎屏都會呼叫)
- 語言切換時
7. 在 Activity 的 onCreate() 方法里寫死回圈會 ANR 嗎?
ANR全稱:Application Not Responding,也就是應用程式無回應
Android 中產生 ANR 的原因:
- Service TimeOut: service 未在規定時間執行完成:前臺服務 20s,后臺 200s
- BroadCastQueue TimeOut: 未在規定時間內未處理完廣播:前臺廣播 10s 內, 后臺 60s 內
- ContentProvider TimeOut: publish 在 10s 內沒有完成
- Input Dispatching timeout: 5s 內未回應鍵盤輸入、觸摸螢屏等事件
根據以上 ANR 產生的原因,說明在 Activity 的 onCreate() 方法中寫死回圈是不會發生ANR的,但是主執行緒被死回圈一直占用了,所以當再有其他事件產生時,就不能及時回應了,從而導致ANR發生,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/287172.html
標籤:其他
