剛畢業那會兒,一心想朝着java web的方向進軍,卻豈料實習的時候陰差陽錯地踏入了Android的大門,自此人生跌宕起伏、坎坎坷坷,在一家外企參與了幾個需要越過GFW才能使用的有關於體育賽事的項目,之后跳了個槽跟另外一個哥們做了好幾個沒見到陽光的充斥着濃濃果味兒的App。無數的努力付之東流會讓一個人厭倦某一件事情,所以我要開始寫寫能讓自己看懂的文章,借此找點樂子當然也是為了記錄記錄自己的學習歷程。
探究activity的各回調方法之前,首先插入一張官方的生命周期圖,然后用適合自己的語言記錄下簡化的生命周期中各回調方法的涵義。
onCreate()
一般人認為的activity的入口(然而不是),當activity第一次created之后會回調這個方法。如果把activity比作房子的話,回調這個方法之前activity還只是一個毛坯房,我們要在這個方法里邊對它進行裝修,這樣它隨后顯示的效果就跟我們所預想的一樣了。
onstart() :
這個時候我們可以看到activity了。
onResume() :
這個時候我們可以跟activity進行交互了。
onPause() :
這個時候我們還是能看到activity,但是不能進行交互了。舉個栗子,假設我們當前的activity為A,有一個啟動activity B的意圖,這個時候A會回調onPause(),當A的onPause()回調完成之后,B開始onCreate()->onStart()->onResume()...完了之后B就處於可見可交互的狀態了。那么A呢?如果此時A看不見了,A就會回調onStop()方法;另一種情況是此時A還是部分可見的(比如Activity B的主題是@android:style/Theme.Dialog),A就不會回調onStop(); 從上面的分析可以知道,onPause()方法里面不允許做耗時的操作,不然B等了半天都啟動不了。
有一點需要注意的是,不是說A處於部分可見但是不可交互的狀態就一定會回調onPause()的,不信你show一個Dialog,show一個DialogFragment,或者show一個設置焦點為true的PopupWindow試試看。
onStop():
這個時候activity已經一丁點兒都看不到了。
onDestroy():
activity的臨終遺言就在這里面寫了,因為回調完它就被摧毀了。activity被摧毀有兩種情況,一是someone調用了finish()方法,二是系統要節省內存空間而臨時干掉它. 如何區分呢?官方文檔里面說的是通過isFinishing()這個方法來判斷。 如果是someone調用了finish()方法,isFinishing()毫無疑問是return true的。如果是系統為了節省空間,isFinishing()=false?
onRestart():
activity准備重新出來見人了。典型的栗子是啟動一個能完全遮擋住前一個activity的新的activity之后,再按back鍵返回到前一個actvity,這樣前一個activity就會onRestart()->onStart()->onResume();另一個栗子是按下電源鍵,熄滅屏幕,再打開,點亮屏幕的時候;還有一個是按下home鍵,再從最近任務欄或者點擊應用圖標重新進去的時候....
到此為止,簡化的activity生命周期就大致掌握了,但是將近七千行的activity源碼可不止這幾個回調方法。當然,我們不必要去追究里面每個方法每個變量的意義與作用,我覺得那樣是一種浪費時間的表現,還不如去多看幾個用得上的api或者研究下目前一些流行的開源框架怎么使用(好吧,實際上是老子看不懂那一堆fucking source code~~),話雖如此,一些有用或者有意思的回調方法我們還是需要了解了解的,要不然怎么提升自己的編程逼格呢!!!!!!!!!!
onApplyThemeResource():
一般來說在AndroidManifest.xml的application標簽下會全局設置一個theme屬性,或者單獨為每個activity設置也可以,這樣onApplyThemeResource()方法會先於onCreate()調用,當然你若是不在AndroidManifest.xml設置,硬是單單在onCreate()里面調用setTheme()方法也是可以的,這樣onApplyThemeResource()就會在onCreate()后調用了。
這個方法顧名思義就是activity應用主題資源的,當然並不是說activity直接就調用onApplyThemeResource()了,我們可以稍微追蹤下它的調用路線。
我們知道要啟動一個activity,會調用ActivityThread的performLaunchActivity()方法來創建這個activity,下面我們點進去找到關於設置主題的幾行代碼。
private Activity performLaunchActivity(......) { ...... if (activity != null) { Context appContext = createBaseContextForActivity(r, activity); ...... activity.attach(appContext, this,......); ...... int theme = r.activityInfo.getThemeResource(); //如果在AndroidManifest.xml里面設置了有效的theme屬性,則調用setTheme() if (theme != 0) { activity.setTheme(theme); } } }
然后去activity的setTheme()里面看看,看之前先了解下activity的繼承關系,如下圖。activity是直接繼承自ContextThemeWrapper類的,所以才具有了變換主題的能力,實際上activity的setTheme()方法即是ContextThemeWrapper的setTheme()方法。
點進去ContextThemeWrapper的setTheme()方法:
public void setTheme(int resid) { if (mThemeResource != resid) { mThemeResource = resid; initializeTheme(); } }
然后initializeTheme():
private void initializeTheme() { final boolean first = mTheme == null; if (first) { mTheme = getResources().newTheme(); Resources.Theme theme = getBaseContext().getTheme(); if (theme != null) { mTheme.setTo(theme); } } //找到目標 onApplyThemeResource(mTheme, mThemeResource, first); }
以上用語言來表示就是,activity在創建的時候,如果有在AndroidManifest.xml里面設置有效的主題資源id,就會在onCreate()之前調用setTheme()方法,然后調用initializeTheme()方法,進而回調onApplyThemeResource()方法;其實前面看ActivityThread的performLaunchActivity()方法的時候,除了關於設置主題的代碼外,上面還多了幾行代碼,為什么要把它提出來呢,因為initializeTheme()方法有個getBaseContext()的方法,那多出來的幾行代碼就是為了此刻來說明下的。看看Contextwrapper類的源碼,發現它有一個名為mBase的Context類型的成員變量,這個mBase變量委托Contextwrapper類來調用自己的所有方法,大概就是下面這樣:
public class ContextWrapper extends Context { Context mBase; ...... protected void attachBaseContext(Context base) { if (mBase != null) { throw new IllegalStateException("Base context already set"); } mBase = base; } public Context getBaseContext() { return mBase; } @Override public AssetManager getAssets() { return mBase.getAssets(); } @Override public Resources getResources() { return mBase.getResources(); } ...... }
所以initializeTheme()方法調用的getBaseContext()方法取的就是這個名為mBase的Context類型的成員變量,但是實際上Context類是一個抽象類,里面根本沒有任何實現,這個時候就要重新回到上面的performLaunchActivity(),看看那多出來的兩行代碼到底干了什么。
首先是createBaseContextForActivity()方法,只貼重要的代碼,其它的不想看:
private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) { ...... ContextImpl appContext = ContextImpl.createActivityContext( this, r.packageInfo, displayId, r.overrideConfig); appContext.setOuterContext(activity); Context baseContext = appContext; ...... return baseContext; }
然后是Activity的attch()方法,就不貼代碼了,點進去發現調用了attachBaseContext()方法,對了,實則調用的就是上面ContextWrapper類的attachBaseContext(),由此,分析得知,activity在創建的時候通過createBaseContextForActivity()得到一個Context的實現類ContextImpl類的實例,然后在attch()方法里面通過調用attachBaseContext()方法讓ContextWrapper類的mBase成員變量指向這個實例.
綜上所述,前面initializeTheme()方法里面的getBaseContext()就說得通了,它實際得到的是一個ContextImpl類的實例,Activity第一次設置主題的時候,ContextImpl類的getTheme()方法會根據你的targetSdkVersion版本返回一個對應的默認的主題對象。以后有時間再去看看ContextImpl類,現在先跳過這個實際開發沒什么用的onApplyThemeResource()方法。
onContentChanged():
當屏幕的內容視圖發生改變時會調用,官方文檔上的這句話反正我是看不大明白,只能看看源碼了。這里以AppCompatActivity為例,發現AppCompatActivity的setContentView()或者addContentView()方法都是通過AppCompatDelegate來委托調用的,於是我們找到它的實現類AppCompatDelegateImplV7,然后把這兩個方法貼出來如下:
...... final Window.Callback mOriginalWindowCallback; ...... @Override public void setContentView(View v) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); contentParent.addView(v); mOriginalWindowCallback.onContentChanged(); } @Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mOriginalWindowCallback.onContentChanged(); } ......
所以,Activity在onCreate()里邊調用setContentView()方法或者addContentView()方法的時候,都會緊接着回調onContentChanged()方法,上面代碼mOriginalWindowCallback變量指向的就是委托的Activity對象。根據上述分析,onContentChanged()方法什么時候會回調呢?就是當Activity的根視圖中id為android.R.id.content的ViewGroup的子View發生改變時,這種改變指的是子View的替換。這樣來看的話,我們在onCreate()里邊就不用寫個initView()的方法來findViewById()了,直接把這些操作丟在onContentChanged()方法里邊,感覺吊吊的。遺憾的是,有了butterknife、AndroidAnnotations、Dagger亦或是其它的注解框架,誰還會用findViewById()?
當然,也可以直接看看Activity里邊的setContentView()方法,然后看看PhoneWindow類里邊的setContentView()方法,原理是一樣的。那么這個id為android.R.id.content的ViewGroup到底是什么呢?以后有時間的話要單獨記錄下有關DecorView的知識😄。
onPostCreate():
照例把官方文檔翻譯一下,當activity已經完全啟動的時候會回調這個方法,系統會在里邊做一些最終的初始化,我們的應用程序通常不用重寫這個方法。 嗯,就這樣,但是總感覺少了點什么,去ActivityThread類的performLaunchActivity()方法里面看看:
...... /** *activity通過這個mCalled變量來判斷你activity的回調方法有木有調用父類對應的方法, *如果沒有會拋SuperNotCalledException異常,原理非常簡單,先把mCalled置為false, *然后在父類的方法里面再置為true,由此判斷... */ activity.mCalled = false; //調用onCreate()... if (r.isPersistable()) { mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); }else { mInstrumentation.callActivityOnCreate(activity, r.state); } if (!activity.mCalled) { throw new SuperNotCalledException( "Activity " + r.intent.getComponent().toShortString() + " did not call through to super.onCreate()"); } r.activity = activity; r.stopped = true; /** *記得前面說的isFinishing()方法嗎,實際上它返回的就是這個mFinished變量,someone *調用finish()時候會把mfinished置為true...so理論上,activity啟動的時候光速般按下返 *回鍵,這樣onStart()就不會調了,當然,這只是理論。onStart()調完之后,將stopped置 *為false,說明activity已經不在后台了。 */ if (!r.activity.mFinished) { activity.performStart(); r.stopped = false; } //略過... if (!r.activity.mFinished) { if (r.isPersistable()) { if (r.state != null || r.persistentState != null) { mInstrumentation.callActivityOnRestoreInstanceState(activity,r.state, r.persistentState); } }else if (r.state != null) { mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); } } //這里就是說要去調onPostCreate()了... if (!r.activity.mFinished) { activity.mCalled = false; if (r.isPersistable()) { mInstrumentation.callActivityOnPostCreate(activity, r.state, r.persistentState); } else { mInstrumentation.callActivityOnPostCreate(activity, r.state); } if (!activity.mCalled) { throw new SuperNotCalledException( "Activity " + r.intent.getComponent().toShortString() + " did not call through to super.onPostCreate()"); } } } r.paused = true; ......
看到這,我突然發現不管是onPostCreate()還是onPostResume(),Activity的默認實現操作都是跟標題欄有關,或者說跟ActionBar有關,我覺得這不是一個巧合,你看ActionBarDrawerToggle也是。所以,我覺得當設備狀態變化時,比如橫豎屏轉換的時候,如果我們沒有在AndroidManifest.xml配置configChanges屬性,當Activity重新回調各生命周期方法的時候,我們在這兩個方法里面同步ActionBar的狀態是否會比較符合官方的思想。
onUserInteraction()、onUserLeaveHint():
兩個長的很像的方法,onUserInteraction()比較好理解,只要用戶與Activity有交互就會調用,比如說按了個鍵、觸了個屏、滾了個軌跡球...專業來講就是只要有事件分發給Activity的時候就會首先調用onUserInteraction(),所以你去看Activity的源碼可以發現,在dispatchXXXEvent()的方法體里面,首先就是調onUserInteraction()。哦,除了dispatchPopulateAccessibilityEvent(),這個好像是android系統設置里面有個什么輔助功能相關的交互吧。
onUserLeaveHint(),因為用戶的選擇從而讓當前的Activity進入后台的時候就會回調這個方法,一定要注意,"用戶的選擇"和"進入后台"。比如,在當前Acitivity按下home鍵會回調onUserLeaveHint()方法;啟動一個新的Activity(包括Dialog或者透明風格的Activity),前一個Activity會回調onUserLeaveHint()方法,其實這個情況這種說法不完全正確啊 ,如果你前一個Activity在startActivity()之前先調用了finish()方法,onUserLeaveHint()是不會調用的,因為此時前一個Activity就不僅僅是進入后台了,而是要被摧毀了...當然如果你的finish()方法寫在startActivity()之后的話,還是會調用onUserLeaveHint()的;第三種情況,因為系統的調用而讓你的Activity進入后台是不會走onUserLeaveHint()的,比如突然一個電話打進來的時候。以上三種情況的話特別要注意第二種,最后記錄一句,假若onUserLeaveHint()要回調的話是在onPause()之前的。
分析了這么多,那onUserLeaveHint()能做什么呢,捕獲home點擊事件?那啟動一個新的Activity也會被調用啊,這個時候我們就要問了,系統是怎么判斷當前的Activity進入后台是不是用戶的選擇呢,實際上,Intent有個FLAG叫做FLAG_ACTIVITY_NO_USER_ACTION。
所以啊,前一個Activity調用startActivity(Intent intent)啟動一個新的Activity的時候,如果intent設置了這個FLAG,前一個Activity的onUserLeaveHint()方法就會被阻止調用了,因為這代表不是USER的ACTION. 到此,我們就知道了,想要大致捕獲home點擊事件,應用內的Activity跳轉的時候加上這個FLAG就OK了。 那我們能在這個方法里面做什么呢,典型的,發送一個Notification告訴用戶,你的app現在跑在后台...
dispatchXXXEvent():
事件分發進Activity時被調用,這個方法里邊一般首先調用onUserInteraction();
"Event事件是首先到了 PhoneWindow 的 DecorView 的 dispatchTouchEvent 方法,此方法通過 CallBack 調用了 Activity 的 dispatchTouchEvent 方法,在 Activity 這里,我們可以重寫 Activity 的dispatchTouchEvent 方法阻斷 touch事件的傳播。接着在Activity里的dispatchTouchEvent 方法里,事件又再次傳遞到DecorView,DecorView通過調用父類(ViewGroup)的dispatchTouchEvent 將事件傳給父類處理,也就是我們下面要分析的方法,這才進入網上大部分文章講解的touch事件傳遞流程"
以上這段話出自 Android 事件分發機制詳解
onSaveInstanceState()、onRestoreInstanceState():
首先,當內存不足時,為了保證前台進程的正常運行,Android系統會kill掉一些后台進程來釋放內存。當某一個Activity存在一種被系統kill掉的可能性時,或者當一個Activty被系統摧毀但是又馬上重新創建時,onSaveInstanceState()就會回調,因為Activity在這種情況下被kill掉並不是我們的本意,所以系統給出這么一個地方讓我們保存自己的數據,等下次再進入這個Acticity的時候可以復原。Activity的onSaveInstanceState()方法里面,默認給我們保存了視圖層次與Fragmets的相關狀態,所以我們override這個方法的時候,先super調一下,然后再保存一些其它的臨時數據就是了(通過我們熟悉的Bundle對象來保存)。列舉一下onSaveInstanceState()會被回調的情況如下:
- 用戶按下home鍵
- 在最近任務切換到其它應用 (一般是長按home鍵)
- 啟動一個新的Actvity
- 按電源鍵熄滅屏幕
- 設備的配置信息發生改變時,比如橫豎屏切換、系統語言切換、調整設置里面的字體大小等
前四種情況都是由於Activity進入后台了,當內存不足時存在被系統kill掉的可能性,所以會回調onSaveInstanceState()。最后一種情況比較特殊,如果沒有在AndroidManifest.xml配置相應的configChanges屬性,系統會摧毀這個Activity並立馬重新創建。需要注意的,是存在被摧毀的可能性才會回調,所以像點擊back鍵,顯示地調用finish(),這種百分之一百會被摧毀的時候是不會給你保存數據的,因為這是你自己想要它去死的。
onRestoreInstanceState()與onSaveInstanceState()並不是成對出現的,只有當上述的可能性變成現實的時候才會回調,因為沒有摧毀就沒有復原。由此可見,對上述的第五種情況來說,onSaveInstanceState()與onRestoreInstanceState()是一定會成雙成對得出現的。
onSaveInstanceState()如果被回調的話,在onStop()之前,有可能在onPause()之前也有可能在onPause()之后;onRestoreInstanceState()如果被回調的話,在onStart()與onPostCreate()之間。
onConfigurationChanged():
這個方法是跟設備的配置信息改變有關的,如果在AndroidManifest.xml給Activity配置了相應的configChanges屬性,這個時候Activity就不會先被摧毀然后立馬重新創建了,而會只是回調這個方法。那么設備都有哪些配置呢?在此摘抄下《Android開發藝術探索》的有關於Configuration的說明。
mcc : SIM卡唯一標識IMSI(國際移動用戶識別碼)中的國家代碼,由三位數字組成,中國為460。此項標識mcc代碼發生了改變
mnc:SIM卡唯一標識IMSI(國際移動用戶識別碼)中的運營商代碼,由兩位數字組成,中國移動TD系統為00,中國聯通為01,中國電信為03。此項標識mnc發生了改變
locale:設備的本地位置發生了改變,一般指切換了系統語言
touchscreen:觸摸屏發生了改變,這個很費解,正常情況下無法發生,可以忽略它
keyboard:鍵盤類型發生了改變,比如用戶使用了外插鍵盤
keyboardHide:鍵盤的可訪問性發生了改變,比如用戶調出了鍵盤
navigation:系統導航方式發生了改變,比如采用了軌跡球導航,這個有點費解,很難發生,可以忽略它
screenLayout:屏幕布局發生了改變,很可能是用戶激活了另外一個顯示設備
fontScale:系統字體縮放比例發生了改變,比如用戶選擇了一個新字號
uiMode:用戶界面模式發生了改變,比如是否開啟了夜間模式(API 8添加)
orientation:屏幕方向發生了改變,這個是最常用的,比如旋轉了手機屏幕
screenSize:當屏幕的尺寸信息發生了改變,當旋轉設備屏幕時,屏幕尺寸會發生變化,這個選項比較特殊,它和編譯選項有關,當編譯選項中的minSdkVersion和TargetVersion均低於13時,此選項不會導致Activity重啟,否則會導致Activity重啟(API 13新添加)
smallestScreenSize:設備的物理屏幕尺寸發生改變,這個項目和屏幕的方向沒有關系,僅僅表示在實際的物理屏幕的尺寸改變的時候發生,比如用戶切換到了外部的顯示設備,這個選項和screenSize一樣,當編譯選項中的minSdkVersion和TargetVersion均低於13時,此選項不會導致Activity重啟,否則會導致Activity重啟(API 13新添加)
layoutDirection:當布局方向發生改變,這個屬性用的比較少,正常情況下無須修改布局的layoutDirection屬性(API 17新添加)
為什么要一定要抄一遍,因為可以加深印象。
onAttachedToWindow(),onDetachedFromWindow():
官方要我們去看View的這兩個對應的方法。。。那就再說吧。。。
onWindowFocusChanged():
當Activity的窗口獲取或失去焦點的時候就會回調這個方法。官方文檔上說,這個方法是判斷Activity是否對用戶可見的最好的標志了,個人理解的意思就是,當走到這個方法的時候,Activity里面的視圖都已經測量過了,我們的肉眼也確實能看到這個Activity了,但是可能還沒有繪制沒有渲染,整個界面是黑的還是灰的,我也不知道,我只知道在這個方法里面可以獲取到View的寬高。
列舉幾種Activity焦點變化的情況。
- 彈出/消失一個Dialog
- 上/下拉狀態欄
- 啟動一個新的Activity & 返回到前一個Activity
- 略......
最后,附一張所謂的Activity的完整的生命周期圖: