要想成爲一名優秀的Android開發,一份 知識體系 是必不可少的~
Activity是我們常用App開發中最重要的組件,主要用於展示界面和用戶交互。本文分爲三個部分:
Activity源碼和常見的問題
- Activity的生命週期,正常情況和異常情況?
- Activity的四種啓動模式,啓動頁設置singleTask/singleInstance可能造成什麼後果?
- 任務,任務棧,前臺任務棧,後臺任務棧,返回棧分別是什麼?
- startActivityForResult導致的一系列問題?
- 清除返回棧(Clearing the back stack)的一些概念
- allowTaskReparenting的使用
- Activity的隱式啓動
- Activity啓動流程
Activity深層次問題
- Activity生命週期的變化對進程的優先級有什麼影響?
- 如果App還存在緩存進程,這個時候啓動App,應用Application的onCreate方法會執行嗎?
- 一個Activity A啓動另一個Activity B,爲何會先走A的onPause方法,等到B執行完onResume方法後,纔會走A的onStop方法呢?
- 爲什麼要這麼設計Activity生命週期?
第三方App中一些Activity的設置
- 今日頭條極速版-新聞界面打開的一些限制和首頁
Activity源碼和常見的問題
1.Activity的生命週期,正常情況和異常情況?
首先來看看官網上Activity的生命週期,如下圖所示
生命週期在開發中會常常被我們用到,比如在界面的恢復和銷燬等回調具體的方法,我們在這些方法做一些數據的處理等。當然這裏面還少了方法onSaveInstance和onRestoreInstance
方法用於狀態的保存和恢復,還有一個方法onConfigurationChanged()
用於配置變更後的回調。
下面是一些常用的生命週期回調流程:
啓動Activity:
onCreate()->onStart()->onResume()
點擊返回鍵:
onPause()->onStop()->onDestroy()
點擊Home鍵:
onPause()->onSaveInstanceState()->onStop()
,注意在API28之後onSaveInstanceState()
方法的執行放在了onStop()
之後。用戶再次回到原Activity:
onRestart()->onStart()->onResume()
A Activity啓動B Activity:
A#onPause()->B#onCreate()->B#onStart()->B#onResume()->A#onStop()
再來看一下異常情況下的生命週期分析:
-
系統配置發配置變化時生命週期的回調(API28+)
onPause()->onStop()->onSaveInstanceState()->onDestroy()
,然後當Activity被重新創建後執行onCreate()->onStart()->onRestoreInstanceState()->onResume()
這裏的配置發生變化可以指屏幕發生旋轉或者切換到多窗口模式等等。
系統配置發生改變時,如果不想重新創建Activity,可以通過在AndroidManifest.xml中配置
android:configChanges
屬性,如果想做一些額外的操作可以在onConfigurationChanged回調中處理。 資源內存不足導致低優先級進程被回收,當系統資源不足時,會殺死低優先級進程,此時會調用
onSaveInstanceState()和onRestoreInstanceState()
進行數據的存儲和恢復。
2.Activity的四種啓動模式,啓動頁設置SingleTask/SingleInstance可能造成什麼後果?
在清單文件中聲明 Activity 時,可以使用 Activity
元素的 launchMode
屬性指定 Activity 應該如何與任務關聯。
stardard:默認模式,系統在啓動該 Activity 的任務中創建 Activity 的新實例,並將 intent 傳送給該實例。Activity 可以多次實例化,每個實例可以屬於不同的任務,一個任務可以擁有多個實例。注意在該模式下配合FLAG_ACTIVITY_NEW_TASK 與 FLAG_ACTIVITY_CLEAR_TOP,單獨/一起配合,都會重新創建實例。
singleTop:棧頂複用模式,如果當前任務的頂部已存在 Activity 的實例,則系統會通過調用其
onNewIntent()
方法來將 intent 轉送給該實例,而不是創建 Activity 的新實例。在該模式下配合FLAG_ACTIVITY_CLEAR_TOP是用哪個,不會重新創建實例,會有類似SingleTask的效果,但是如果再加上FLAG_ACTIVITY_NEW_TASK,還是會創建新實例。singleTask:棧內複用模式,系統會創建新任務,並實例化新任務的根 Activity。但是,如果另外的任務中已存在該 Activity 的實例,則系統會通過調用其
onNewIntent()
方法將 intent 轉送到該現有實例,而不是創建新實例。Activity 一次只能有一個實例存在。該模式默認具有clearTop的效果。-
singleInstance:單實例模式,與
"singleTask"
相似,唯一不同的是系統不會將任何其他 Activity 啓動到包含該實例的任務中。該 Activity 始終是其任務唯一的成員;由該 Activity 啓動的任何 Activity 都會在其他的任務中打開。關於singleInstance有個特殊的情況,如果一個A Activity(standard)啓動B Activity(singleInstance),這個時候用戶點擊了手機最近訪問列表,然後在再點擊該App所在的界面(卡片),然後這個時候點擊返回鍵竟然就直接退出了App,而不是我們預期的退到A Activity界面。其實最近訪問列表也是一個Activity(假設爲C Activity),當我們從這個C Activity點擊App卡片顯示我們的singleInstance所在的界面B,這個時候就相當於C啓動了B,所以我們點擊返回鍵,就直接回到了桌面(有興趣可以自己看看源碼)。
還有一個特殊的情況,就是在最近任務裏看見的 Task 未必還活着,最近任務裏看不見的 Task,也未必就死了,比如 singleInstance。當我們查看最近任務的時候,不同的 Task 會並列展示出來,但有一個前提:它們的 taskAffinity 需要不一樣。在 Android 裏,同一個 taskAffinity 可以被創建出多個 Task,但它們最多隻能有一個顯示在最近任務列表。這也就是爲什麼剛纔例子裏 singleInstance 的那個 Activity 會從最近任務裏消失了:因爲它被另一個相同 taskAffinity 的 Task 搶了排面。
同理,你在一個App從首頁Activity新建一個Activity(singletask/singleInstance),如果沒有指定taskAffinity,這個Activity的taskAffinity和其他界面一樣,所以在最近的範圍列表,你也只能看到一個App的卡片,但是如果你taskAffinity設置的不一樣,就可以看到在最近列表中看到兩個了。
上面講到的任務對應的是TaskRecord(棧結構),其內部維護了一個ArrayList<ActivityRecord>
用來保存和管理ActivityRecord,ActivityRecord包含了一個Activity的所有信息。
通常我們的App都會設置啓動頁(SplashActivity通常是一張圖片),然後進入我們的主界面(MainActivity),在主界面中通常有很多邏輯會導致該界面異常龐大,佔據的內存很大,所以很多時候我們都會給該界面設置爲SingleTask棧內複用模式。
場景一:如果爲了達到快速啓動的效果,將我們的App的閃屏頁(SplashActivity顯示固定圖片)移除掉,換成MainActivity(SingleTask/SingleInstance)的背景(windowBackground),最後再替換成App的主題,給用戶快速響應的體驗;
場景二:如果給啓動頁SplashActivity設置爲SingleTask/SingleInstance模式,同時你的啓動頁沒有及時的關閉。
以上兩種場景會導致你的App無論冷啓動還是熱啓動,每次點擊圖標都是從啓動頁開始啓動的。
3.任務,任務棧,前臺任務棧,後臺任務棧,返回棧分別是什麼?
首先來看官網的說明Understand Tasks and Back Stack,(A task is a collection of activities that users interact with when performing a certain job. The activities are arranged in a stack—the back stack)—in the order in which each activity is opened. )任務是用戶在執行某項工作時與之互動的一系列 Activity 的集合。這些 Activity 按照每個 Activity 打開的順序排列在一個返回堆棧中。前面說過任務對應的是TaskRecord(棧結構),其內部維護了一個ArrayList<ActivityRecord>
用來保存和管理ActivityRecord,ActivityRecord包含了一個Activity的所有信息。所以其實任務就是任務棧(TaskRecord是棧結構)。
- 一般地,對於沒有分屏功能以及虛擬屏的情況下,ActivityStackSupervisor與ActivityDisplay都是系統唯一;
- ActivityDisplay主要有Home Stack和App Stack這兩個棧;
- 每個ActivityStack中可以有若干個TaskRecord對象,當前只會有一個獲得了焦點的ActivityStack;
- 每個TaskRecord包含如果若干個ActivityRecord對象;
- 每個ActivityRecord記錄一個Activity信息。
一個返回棧可能只包含一個任務,但在特殊情況下,可能引入多個任務。這個概念非常重要,這裏引用官方的圖
這裏先說一下操作流程,依次啓動ActivityX,ActivityY,Activity1,Activity2;ActivityY,ActivityX(這兩個都是SingleTask)在後臺任務中,Activity2,Activity1在前臺任務中,這兩個任務的taskAffinity不同,當從Activity2中啓動ActivityY的時候,返回棧如第二列所示,然後點擊返回鍵可以一個個退出。
再普及一個概念在 Android 裏,每個 Activity 都有一個 taskAffinity,它就相當於是對每個 Activity 預先進行的分組。它的值默認取自它所在的 Application 的 taskAffinity,而 Application 的 taskAffinity 默認是 App 的包名。當然也可以手動指定taskAffinity。
但是圖中並沒有指明Activity2,Activity1是什麼啓動模式,實際上我如果我們指定爲standard標準模式根本模擬不出這個場景,這一點有點坑,因爲這四個Activity分別按2,1,X,Y排列,也即是說啓動是從Y,X,1,2一個個啓動的,如果Activity1爲standard,就算你指定了Activity1的taskAffinity和ActivityY的不同也沒有用,Activity1還是會和ActivityY在同一個任務(TaskRecord)中,也就是說standard 和 singleTop 的 Activity 在哪個 TaskRecord 啓動,全憑啓動它的 Activity 在哪個 TaskRecord,taskAffinity在同時指定爲singleTask模式下才有意義(只有一種例外,standard 和 singleTop在 allowTaskReparenting 爲 true,且被其他應用以 DeepLink 的方式喚起時,纔會在指定的任務中)。
所以我們將Activity2,Activity1也設置爲singleTask,同時taskAffinity也相同,纔會模擬出上面的場景,點擊Activity2啓動ActivityY,纔會將後臺任務棧ActivityY,ActivityX都帶到前臺任務棧中,也就是都帶到返回棧中。
小結
任務就是任務棧(TaskRecord是棧結構),TaskRecord內部維護了一個ArrayList<ActivityRecord>
用來保存和管理ActivityRecord,ActivityRecord包含了一個Activity的所有信息。
一個返回棧可能只包含一個任務,但特殊情況下,可能引入多個任務。返回棧,前臺任務棧,後臺任務棧其實在源碼中並沒有明確的定義,而是在我們操作任務棧過程中提出的一些“概念”,爲了便於描述和區分。
前臺棧比如現在下圖A中的Activity2,Activity1所在的任務,後臺任務棧是ActivityY,ActivityX所在的任務。
但是問題來了,當Activity2啓動ActivityY的時候,返回棧中的內容如下圖B所示,這個時候前臺任務棧是什麼呢?
這個時候後臺的任務棧(ActivityY,ActivityX)已經返回到前臺,四個Activity都在前臺,此時返回堆棧中包含了轉到前臺任務中的所有Activity(這句話來自官網對這一場景的說明)。
問題又來了,比如我們前面說的後臺任務棧是在後臺等待恢復(比如ActivityX,ActivityY所在的棧),依次啓動ActivityX,ActivityY,Activity1,Activity2,如果你這個時候什麼都不做,不斷點擊返回鍵,這四個Activity會一個個退出,這個時候你會不會覺得返回棧包含前臺任務棧和後臺任務棧。但是一開始圖A中返回棧(Back Stack)只標明瞭Activity1,Activity2,這就出現矛盾了,但我的感覺返回棧就是字面上的含義,點擊返回鍵,能退出多少個Activity,那麼這些Activity就都在返回棧中,返回棧就是一個概念,當然你也可以理解它的大小動態變化的(點擊返回鍵的過程中可能大小可能新增)。
4.startActivityForResult導致的一系列問題?
在使用Activity的startActivityForResult啓動新界面時,在Api20以下調整時會直接返回Activity.RESULT_CANCELED
,官方覺得不應該在兩個任務之間setResult。在Api20及以上,對於非startActivity跳轉,也就是reqeusetCode>=0,singleTask和SingleInstance模式啓動的Activity都不會新建一個任務,還是在原來的棧中。
5.清除返回棧(Clearing the back stack)的一些概念
如果用戶離開任務較長時間,系統會清除任務中除根 Activity 以外的所有 Activity。當用戶再次返回到該任務時,只有根 Activity 會恢復。系統之所以採取這種行爲方式是因爲,經過一段時間後,用戶可能已經放棄了之前執行的操作,現在返回任務是爲了開始某項新的操作。
您可以使用一些 Activity 屬性來修改此行爲:
-
alwaysRetainTaskState
如果在任務的根 Activity 中將該屬性設爲
"true"
,則不會發生上述默認行爲。即使經過很長一段時間後,任務仍會在其堆棧中保留所有 Activity。 -
clearTaskOnLaunch
如果在任務的根 Activity 中將該屬性設爲
"true"
,那麼只要用戶離開任務再返回,堆棧就會被清除到只剩根 Activity。也就是說,它與alwaysRetainTaskState
正好相反。用戶始終會返回到任務的初始狀態,即便只是短暫離開任務也是如此。 -
finishOnTaskLaunch
該屬性與
clearTaskOnLaunch
類似,但它只會作用於單個 Activity 而非整個任務。它還可導致任何 Activity 消失,包括根 Activity。如果將該屬性設爲"true"
,則 Activity 僅在當前會話中歸屬於任務。如果用戶離開任務再返回,則該任務將不再存在。
6.allowTaskReparenting的使用
Activity 默認情況下只會歸屬於一個 Task,不會在多個 Task 之間跳來跳去,但你可以通過設置來改變這個邏輯。把它的 allowTaskReparenting 屬性設置爲 true。如果未設置該屬性,則由 <Activity>
元素的相應 allowTaskReparenting
屬性所設置的值。默認值爲“false
”。
正常情況下,Activity 啓動時會與啓動它的任務關聯,並在其整個生命週期中一直留在該任務處。當不再顯示現有任務時,您可以使用該屬性強制 Activity 將其父項更改爲與其有相似性的任務。該屬性通常用於將應用的 Activity 轉移至與該應用關聯的主任務。
例如,如果電子郵件消息包含網頁鏈接,則點擊該鏈接會調出可顯示該網頁的 Activity。該 Activity 由瀏覽器應用定義,但作爲電子郵件任務的一部分啓動。如果將該 Activity 的父項更改爲瀏覽器任務,則它會在瀏覽器下一次轉至前臺時顯示,在電子郵件任務再次轉至前臺時消失。
7.Activity的隱式啓動
Activity分爲顯示啓動和隱式啓動,顯示啓動就是我們平時調用的一些startActivityXXX()
方法,隱式啓動可以通過action來啓動,啓動時調用如下,同時要記得添加category爲"android.intent.category.DEFAULT"
。
Intent implicitIntent = new Intent();
implicitIntent.setAction("com.test.image");
implicitIntent.addCategory("android.intent.category.DEFAULT");
MainActivity.this.startActivity(implicitIntent);
具體界面的配置如下:
<activity android:name=".ImageActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="com.test.image" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
注意如果是其他App的Activity,需要添加android:exported="true"才能被調用。
8.Activity的啓動流程(中級問題)
對很多開發者來說,這可能都是個很沉重的問題,原因很簡單,因爲回答不好,畢竟裏面涉及到的東西很多,需要你擁有很大知識存儲量。下面來嘗試回答這個問題(基於源碼9.0)
首先先普及一些常見的概念
Instrumentation
Android Instrumentation是Android系統中的一套控制方法或者“鉤子”,這些鉤子可以在正常的生命週期(正常是由操作系統控制的)之外控制Android控件的運行,其實指的就是Instrumentation類提供的各種流程控制方法。
app->instrumentation->ams->app
,自動化測試可以通過Instrumentation來操作Activity等,這個Instrumentation相當於設計了一個統一的入口。
ActivityThread
ActivityThread不是線程類(Thread),只不過它會跑在ActivityThread.main()
方法中,安卓程序的入口就是該方法,同時在該方法中一個Looper不斷循環的在消息隊列中處理消息。管理應用程序進程中主線程的執行,根據Activity管理者的請求調度和執行activities、broadcasts及其相關的操作。
public static void main(String[] args) {
// 看源碼很重要的一個能力就是‘眼中只有你’,認不到的都忽略,看認得到的
···
// 創建主線程的Looper對象,發現和工作線程創建Looper對象調用的方法不一樣,這裏先記下,以後在詳解。
// 主線程原來也有Looper對象啊
Looper.prepareMainLooper();
//創建ActivityThread
ActivityThread thread = new ActivityThread();
thread.attach(false);
// 如果主線程的Handler爲空(可以看出,一個好的命名可讀性是多麼高),那就爲主線程創建一個Handler。
// 然後我們還可以在主線程創建Handler,說明一個線程對應多個Handler。多讀源碼,很多問題都得到了解決啊。
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
Looper.loop();
// 這裏拋了個異常,主線程loop異常退出。說明主線程loop不能退出,這裏和前面建立Looper對象的調用方法有關
throw new RuntimeException("Main thread loop unexpectedly exited");
}
ActivityManagerService
Android中最核心的服務,主要負責系統中四大組件的啓動、切換、調度及應用程序的管理和調度等工作。
ActivityManager
該類提供與Activity、Service和Process相關的信息以及交互方法, 可以被看作是ActivityManagerService的輔助類。
ActivityStackSupervisor
負責所有Activity棧的管理。內部管理了mHomeStack、mFocusedStack和mLastFocusedStack三個Activity棧。其中,mHomeStack管理的是Launcher相關的Activity棧;mFocusedStack管理的是當前顯示在前臺Activity的Activity棧;mLastFocusedStack管理的是上一次顯示在前臺Activity的Activity棧。下面是大致的關係圖,對於沒有分屏功能以及虛擬屏的情況下,ActivityStackSupervisor與ActivityDisplay都是系統唯一。
ActivityStack
ActivityStack負責“Activity棧”的狀態和管理,ActivityStack內部包含了多個任務棧(TaskRecord),TaskRecord內部維護了一個ArrayList<ActivityRecord>
用來保存和管理ActivityRecord,ActivityRecord包含了一個Activity的所有信息
如果我們從桌面點擊啓動app,桌面就是一個Activity,點擊app(按鈕)啓動我們的啓動頁Activity,從這裏分析Activity的啓動流程更加全面,而不是在app中去啓動一個普通的Activity。可以分爲如下幾個流程
-
Launcher通知AMS啓動App的啓動頁Activity,AMS記錄要啓動的Activity信息,並且通知Launcher進入pause狀態。
Launcher進入pause狀態後,通知AMS已經paused了,可以啓動App了
如果App未開啓過,AMS發送創建進程請求,Zogyte進程接受AMS請求並孵化應用進程,應用進程調用ActivityThread並調用mian()方法,並且main()方法中創建ActivityThread對象,
activityThread.attach()
方法中進行綁定(應用進程綁定到AMS),傳入applicationThread以便通訊。AMS通知App綁定Application(bindApplication)並啓動Activity,並且創建和關聯Context,最後調用onCreate等方法。
靈魂拷問:AMS,Zogyte,App進程,Launcher如何通信?
這個問題一旦問出來,能幹翻一大堆開發人員,下面來仔細講講:
App進程和AMS是如何通信的?
Zogyte去fork一個App進程,後面就是應用進程和AMS兩者的事情了,我們知道Android的跨進程通信是通過Binder服務的,AMS所在的進程和應用進程在通過Binder互相通信時,實際上都是通過兩者的代理類進行通信的。
ActivityManagerService(AMS)在手機開機後時就已經啓動了,應用進程去調用AMS的方法,比如startActivity,很容易調用,因爲AMS是一個有名稱的Binder服務,在任意地方都可以通過在ServiceManger(SM)裏面查詢拿到代理類,調用代理類的對應方法,然後再去調用AMS的真正方法。
因爲Binder通信是通過代理類來通信的,如果拿不到代理類,其他進程就不知道如何和我們的App通信,系統服務中的AMS也就不知道如何和我們App通信了,所以當App進程創建完成後,會進行設置代理,代理的設置過程如圖
就是在ActivityThread.attach(false)
方法中,AMS綁定ApplicationThread對象,即應用進程綁定到AMS,通過調用AMS的attachApplication來將ActivityThread的內部類ApplicationThread對象綁定至AMS,這樣AMS就可以通過這個代理對象來控制應用進程。
AMS和Launcher是怎麼通信的?
其實Launcher也是一個App,調用startActivity方法,然後調用的是Instrumentation的execStartActivity方法
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
...
try {
...
//獲取AMS的代理對象
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
在這個方法會調用ActivityManager的getService方法來得到AMS的代理對象,然後調用這個代理對象的startActivity方法
@UnsupportedAppUsage
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
@UnsupportedAppUsage
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
//得到activity的service引用,即IBinder類型的AMS引用
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
//轉換成IActivityManager對象
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
可以發現在Singleton中的create方法中由於b是AMS引用作爲服務端處於SystemServer進程中,與當前Launcher進程作爲客戶端與服務端不在同一個進程,所以am返回的是IActivityManager.Stub的代理對象,此時如果要實現客戶端與服務端進程間的通信,只需要在AMS繼承了IActivityManager.Stub類並實現了相應的方法,而通過下面的代碼可以發現AMS剛好是繼承了IActivityManager.Stub類的,這樣Launcher進程作爲客戶端就擁有了服務端AMS的代理對象,然後就可以調用AMS的方法來實現具體功能了,就這樣Launcher的工作就交給AMS實現了。
public class ActivityManagerService extends IActivityManager.Stub
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
}
Zygote和AMS是如何通信的?
AMS和Zygote建立Socket連接,然後發送創建應用進程的請求。具體可以參考這裏。
最後我們再來看看流程圖,看下方的App進程啓動過程和Activity.startActivity
這兩個流程
這裏還要提到一點,Hook Activity的啓動流程是一個很重要的運用場景,我們需要欺騙AMS,然後啓動真正的TargetActivity,Hook有起始點和終點。這裏需要尋找兩個地方的hook點,一個是對Intent中Activity的替換(hookIActivityTaskManager方法),一個是對Intent中Activity的還原(hookHandler)。
在回答Activity的啓動流程時,具體的方法如何調用並不重要,所以我纔會在最後放出整個流程,各個進程之間如何建立通信,如何通信很重要,同時一些Activity相關概念也很重要,熟悉這些,你就很容易把整個流程串起來了。
Activity深層次問題
1.Activity生命週期的變化對進程的優先級有什麼影響?
這裏先看一下官網上Activity生命週期上對onStart的一段描述,onStart時候Activity就對用戶可見了
同時你也可以在《Android開發藝術探索》上看到類似的描述
但是瞭解Activity啓動流程源碼的朋友都知道,ActivityThread的handleResumeActivity方法中,首先調用Activity的onResume方法,接着會調用Activity.makeVisible()在該方法中,DecorView真正完成了添加和顯示這兩個過程,到這裏Activity的視圖才能被看到。DecoreView和Window進行關聯。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
//DecoreView和WindowManager進行關聯。
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
//設置DecorView可見
mDecor.setVisibility(View.VISIBLE);
}
也就是說在onResume方法執行之後再調用Activity.makeVisible()方法,我們才能真正用肉眼看到我們的DecoreView,看到這裏你這裏不禁會產生一個疑問,那上面官網上的說法(onStart()
調用使 Activity 對用戶可見)難道是錯誤的嗎?
帶着疑問我們繼續在官網上找答案,在進程和生命週期這一章節上可以看到:
爲了確定在內存不足時應該終止哪些進程,Android 會根據每個進程中運行的組件以及這些組件的狀態,將它們放入“重要性層次結構”。這些進程類型包括(按重要性排序):
- 前臺進程是用戶目前執行操作所需的進程。在不同的情況下,進程可能會因爲其所包含的各種應用組件而被視爲前臺進程。如果以下任一條件成立,則進程會被認爲位於前臺:
- 它正在用戶的互動屏幕上運行一個
Activity
(其onResume()
方法已被調用)。 - 它有一個
BroadcastReceiver
目前正在運行(其BroadcastReceiver.onReceive()
方法正在執行)。 - 它有一個
Service
目前正在執行其某個回調(Service.onCreate()
、Service.onStart()
或Service.onDestroy()
)中的代碼。
系統中只有少數此類進程,而且除非內存過低,導致連這些進程都無法繼續運行,纔會在最後一步終止這些進程。通常,此時設備已達到內存分頁狀態,因此必須執行此操作才能使用戶界面保持響應。
-
可見進程正在進行用戶當前知曉的任務,因此終止該進程會對用戶體驗造成明顯的負面影響。在以下條件下,進程將被視爲可見:
- 它正在運行的
Activity
在屏幕上對用戶可見,但不在前臺(其onPause()
方法已被調用)。舉例來說,如果前臺 Activity 顯示爲一個對話框,而這個對話框允許在其後面看到上一個 Activity,則可能會出現這種情況。 - 它有一個
Service
正在通過Service.startForeground()
(要求系統將該服務視爲用戶知曉或基本上對用戶可見的服務)作爲前臺服務運行。 - 系統正在使用其託管的服務實現用戶知曉的特定功能,例如動態壁紙、輸入法服務等。
相比前臺進程,系統中運行的這些進程數量較不受限制,但仍相對受控。這些進程被認爲非常重要,除非系統爲了使所有前臺進程保持運行而需要終止它們,否則不會這麼做。
- 它正在運行的
-
服務進程包含一個已使用
startService()
方法啓動的Service
。雖然用戶無法直接看到這些進程,但它們通常正在執行用戶關心的任務(例如後臺網絡數據上傳或下載),因此係統會始終使此類進程保持運行,除非沒有足夠的內存來保留所有前臺和可見進程。已經運行了很長時間(例如 30 分鐘或更長時間)的服務的重要性可能會降位,以使其進程降至下文所述的緩存 LRU 列表。這有助於避免超長時間運行的服務因內存泄露或其他問題佔用大量內存,進而妨礙系統有效利用緩存進程。
-
緩存進程是目前不需要的進程,因此,如果其他地方需要內存,系統可以根據需要自由地終止該進程。在正常運行的系統中,這些是內存管理中涉及的唯一進程:運行良好的系統將始終有多個緩存進程可用(爲了更高效地切換應用),並根據需要定期終止最早的進程。只有在非常危急(且具有不良影響)的情況下,系統中的所有緩存進程纔會被終止,此時系統必須開始終止服務進程。
這些進程通常包含用戶當前不可見的一個或多個
Activity
實例(onStop()
方法已被調用並返回)。只要它們正確實現其 Activity 生命週期(詳情請見Activity
),那麼當系統終止此類流程時,就不會影響用戶返回該應用時的體驗,因爲當關聯的 Activity 在新的進程中重新創建時,它可以恢復之前保存的狀態。這些進程保存在僞 LRU 列表中,列表中的最後一個進程是爲了回收內存而終止的第一個進程。此列表的確切排序政策是平臺的實現細節,但它通常會先嚐試保留更多有用的進程(比如託管用戶的主屏幕應用、用戶最後看到的 Activity 的進程等),再保留其他類型的進程。還可以針對終止進程應用其他政策:比如對允許的進程數量的硬限制,對進程可持續保持緩存狀態的時間長短的限制等。
可以看到在屏幕上運行時一個Activity的onResume的方法已被調用,此時處於前臺進程;可見進程的一個符合條件:它正在運行的 Activity
在屏幕上對用戶可見,但不在前臺,然後再對比上面對onStart的描述(onStart()
調用使 Activity 對用戶可見,因爲應用會爲 Activity 進入前臺並支持互動做準備),這下子你就豁然開朗了,這裏的onStart的可見指的是可見進程的可見,而不是真正意義上的肉眼可見。
“onPause此方法表示 Activity 不再位於前臺(儘管在用戶處於多窗口模式時 Activity 仍然可見)”,“如果您的 Activity 不再對用戶可見,說明其已進入“已停止”狀態,因此係統將調用 onStop()
回調”,以上都是官方的描述,我們可以打印一下手機中的這些進程,使用adb shell dumpsys meminfo
命令,設備是android 10華爲手機。
可以看到分別對應我們的前臺進程,可見進程,服務進程和緩存進程,其中服務進程還分爲A Services和B Services。其實遠遠不止這麼多的進程級別區分,我自己的App打開後,然後點擊home鍵退到後臺,此時屬於Previous進程(後臺進程)級別(com.jackie.testdialog),如果我打開App後,點擊返回鍵退出,這個時候我的App進程就變成了Cached進程級別了。
講了這麼多,你可能覺得一直沒有一個量化的數字,進程的級別(oom_adj)的取值範圍是多少,在Android7.0之後,ADJ採用100,200,300等數字。下面是基於android9的區分:
ADJ級別 | 取值 | 含義 |
---|---|---|
NATIVE_ADJ | -1000 | native進程 |
SYSTEM_ADJ | -900 | 僅指system_server進程 |
PERSISTENT_PROC_ADJ | -800 | 系統persistent進程 |
PERSISTENT_SERVICE_ADJ | -700 | 關聯着系統或persistent進程 |
FOREGROUND_APP_ADJ |
0 | 前臺進程 |
VISIBLE_APP_ADJ |
100 | 可見進程 |
PERCEPTIBLE_APP_ADJ |
200 | 可感知進程,比如後臺音樂播放 |
BACKUP_APP_ADJ | 300 | 備份進程 |
HEAVY_WEIGHT_APP_ADJ | 400 | 重量級進程 |
SERVICE_ADJ |
500 | 服務進程 |
HOME_APP_ADJ | 600 | Home進程 |
PREVIOUS_APP_ADJ | 700 | 上一個進程 |
SERVICE_B_ADJ |
800 | B List中的Service |
CACHED_APP_MIN_ADJ |
900 | 不可見進程的adj最小值 |
CACHED_APP_MAX_ADJ | 906 | 不可見進程的adj最大值 |
開發者應該減少在保活上花心思,更應該在優化內存上下功夫,因爲在相同ADJ級別的情況下,系統會選擇優先殺內存佔用的進程。當然你也可以手動去測試App的進程級別,不過過程可能有點麻煩,可以參考這篇文章。
小結
當界面只有一個Activity時,它進入onStart和onPause時是可見進程,進入onResume時是前臺進程,打開後點擊Home鍵退到後臺這個時候是Previous進程(後臺進程),如果直接點擊返回鍵退出Activity,這個時候是緩存進程;如果有多個Activity(注意這個時候只有app從後臺任務進入前臺,或者點擊Home鍵退到後臺這兩種場景;因爲app在前臺運行時都是前臺進程),棧頂的的Activity進入onStart和onPause時是可見進程,進入onResume後是前臺進程,點擊Home鍵退到後臺時是Previous進程(大家常說的後臺進程)。
2.如果App還存在緩存進程,這個時候啓動App,應用Application的onCreate方法會執行嗎?
如果你點擊主界面MainActivity,點擊返回鍵後系統執行MainActivity的onDestory方法,這個時候App進程爲緩存進程,下次啓動App你會發現Application的onCreate方法並不會執行,當然MainActivity的生命週期都會正常執行,這是因爲從緩存進程啓動App,系統已經緩存了很多信息,很多數據並不會被銷燬,onCreate中初始化的那些內容還在,方便用戶下次快速啓動。利用這一特性,我們的App首次啓動速度一般爲500600ms,退出App後存在緩存進程的情況下,每次啓動的速度一般爲200300ms,算是某種程度上提升了App的啓動時間。
需要注意的是,很多App在退出主界面的時候,會手動調用如下代碼去退出App
System.exit(0);
一旦調用瞭如下代碼,就會徹底的退出並不會利用緩存進程的優勢,也失去了系統提供給我們的優化了。
3.一個Activity A啓動另一個Activity B,爲何會先走A的onPause方法,等到B執行完onResume方法後,纔會走A的onStop方法呢?
如果你看過前面兩個問題,這個問題你可能已經有答案了。手機之所以進行進程的管理,用不同的優先級對進程進行區分,首先肯定是爲了保證用戶的流暢體驗,對於優先級低且佔用內存高的進程及時清理,保證前臺進程有足夠的運行空間。前面我們講到處於前臺的(獲取焦點)界面只有一個,onPause時當前進程離開了前臺,當然可能也要進行一些數據的保存,所以肯定需要先執行當前界面的某個方法,然後再執行B界面的onCreate,onStart,onResume是爲了新的界面能夠被快速呈現(獲取焦點),然後再走舊界面A的onStop方法。
這裏也需要注意,onPause方法中儘量不要去做耗時的操作,如果過於耗時,新界面會很久才能顯示出來,儘量放在onStop方法中去做。當然onStop中也不能做過於耗時的操作中,前面我們也試過,點擊Home鍵會執行onStop方法,此時App進程處於後臺進程,此時進程的優先級的很低的,當內存不足時,onStop中保存數據的操作可能就未完成,然後App進程就被系統回收了。
關於狀態保存和恢復,在API28之前,onSaveInstanceState執行在onStop之前,但不限於在onPause之前或之後;在API28之後,onSaveInstanceState 執行時機已確定爲在 onStop
之後。而onRestoreInstanceState確定執行在onStart之後。
4.爲什麼要這麼設計Activity生命週期
假如你自己設計界面的生命週期:
界面啓動時候用需要設計一個方法
界面完全渲染完畢顯示需要一個方法
界面被部分遮蓋時/跳到其他界面/退到後臺需要一個方法
界面完全退出銷燬時需要一個方法
這麼看來,我們好像只需要onCreate,onResume,onPause,onDestroy這四個方法,但是這只是一個很粗糙的界面創建~退出流程的回調,但是你看看IOS的UIViewController的生命週期,看起來就是個精緻的豬豬女孩
這樣一對比,連Android的生命週期顯得有點粗糙了,其實不全是,Activity還有一系列的onPostXXX
方法以及onContentChanged等,但還是沒有IOS細膩。其實我覺得,這些生命週期的回調是基於一些場景設計的,從視圖的顯示到銷燬,考慮到不同的需求,我們需要不同程度級別的設計,如果Android是一個非常簡單的系統,也不會實現那麼多的特殊需求,可能只需要前面我說的那四個方法就夠了,我感覺在生命週期的設計方面,IOS做的更好一些,對開發者更加友好。
也有一些人在回答生命週期爲什麼要這麼設計時,可能會這麼回答,因爲界面需要有個創建/銷燬過程,onCreate/onDestroy肯定需要,onStart時進程爲可見進程,提升進程的優先級,或者做一些特殊場景的操作,onResume在界面啓動完成或者恢復時需要,界面在被透明Activity的覆蓋時會執行onPause(),需要有個方法在這個時候做狀態保存或特殊操作等,onStop時可以進行狀態保存。這樣想問題完全是一種結果倒推的想法,經不起仔細的推敲,一定不要從具體的方法去推場景,而是應該從需求場景開始推導,切記,這一切都是需求或可能的需求引起的!
第三方App中一些Activity的設置
今日頭條極速版-新聞界面打開的一些限制
NewDetailActivity就是我們看到的普通新聞界面,最多隻能打開四個,超過四個就會將之前最早的NewDetailActivity關閉,原因很簡單,如果無限制的話Activity會越建越多,整個應用越來越卡,影響用戶體驗。
TaskRecord{8636d7b #6564 A=com.ss.android.article.lite U=0 StackId=282 sz=5}
Run #4: ActivityRecord{8794744 u0 com.ss.android.article.lite/com.ss.android.article.base.feature.detail2.view.NewDetailActivity t6564}
Run #3: ActivityRecord{8be5248 u0 com.ss.android.article.lite/com.ss.android.article.base.feature.detail2.view.NewDetailActivity t6564}
Run #2: ActivityRecord{8bd6a09 u0 com.ss.android.article.lite/com.ss.android.article.base.feature.detail2.view.NewDetailActivity t6564}
Run #1: ActivityRecord{87cc383 u0 com.ss.android.article.lite/com.ss.android.article.base.feature.detail2.view.NewDetailActivity t6564}
Run #0: ActivityRecord{8bd6b44 u0 com.ss.android.article.lite/.activity.SplashActivity t6564}
而且還可以發現這個今日頭條極速版的主頁叫SplashActivity,真他麼牛逼~,估計是原來有個SplashActivity界面和MainActivity界面,爲了優化快速啓動,給用戶一個秒開的感覺,移除原來的SplashActivity,直接把MainActivity改名爲SplashActivity,然後做主題的替換。
然後我們看看它的啓動模式,啓動模式是standard。
~ » adb shell dumpsys activity | grep SplashActivity jackie@JackieLindeMacBook-Pro
baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ss.android.article.lite/.activity.SplashActivity }
baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.sina.weibo/.SplashActivit }
baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=cmccwm.mobilemusic/.ui.base.SplashActivity }
baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.daimajia.gold/im.juejin.android.ui.SplashActivity }
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ss.android.article.lite/.activity.SplashActivity bnds=[544,149][796,458] }
mActivityComponent=com.ss.android.article.lite/.activity.SplashActivity
mIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ss.android.article.lite/.activity.SplashActivity bnds=[544,149][796,458] }
#0 ActivityRecord{8e43505 u0 com.ss.android.article.lite/.activity.SplashActivity t6684} type=standard mode=fullscreen override-mode=undefined //啓動模式是standard
intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ss.android.article.lite/.activity.SplashActivity}
mActivityComponent=com.ss.android.article.lite/.activity.SplashActivity
Activities=[ActivityRecord{8e43505 u0 com.ss.android.article.lite/.activity.SplashActivity t6684}]
Hist #0: ActivityRecord{8e43505 u0 com.ss.android.article.lite/.activity.SplashActivity t6684}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ss.android.article.lite/.activity.SplashActivity bnds=[544,149][796,458] }
Run #0: ActivityRecord{8e43505 u0 com.ss.android.article.lite/.activity.SplashActivity t6684}
mResumedActivity: ActivityRecord{8e43505 u0 com.ss.android.article.lite/.activity.SplashActivity t6684}
intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.sina.weibo/.SplashActivity}
mActivityComponent=com.sina.weibo/.SplashActivity
intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=cmccwm.mobilemusic/.ui.base.SplashActivity}
mActivityComponent=cmccwm.mobilemusic/.ui.base.SplashActivity
ResumedActivity:ActivityRecord{8e43505 u0 com.ss.android.article.lite/.activity.SplashActivity t6684}
ResumedActivity: ActivityRecord{8e43505 u0 com.ss.android.article.lite/.activity.SplashActivity t6684}
再來看今日頭條的首頁的啓動模式,它的首頁叫MainActivity,用的也是standard。
adb shell dumpsys activity | grep MainActivity jackie@JackieLindeMacBook-
Intent { flg=0x24008000 cmp=com.ss.android.article.news/.activity.MainActivity (has extras) }
mActivityComponent=com.ss.android.article.news/.activity.MainActivity
mIntent=Intent { flg=0x24008000 cmp=com.ss.android.article.news/.activity.MainActivity (has extras) }
#0 ActivityRecord{8fadec6 u0 com.ss.android.article.news/.activity.MainActivity t6685} type=standard mode=fullscreen override-mode=undefined //standard啓動模式
#0 ActivityRecord{9130583 u0 cmccwm.mobilemusic/.ui.base.MainActivity t6681} type=standard mode=fullscreen override-mode=undefined
Activities=[ActivityRecord{8fadec6 u0 com.ss.android.article.news/.activity.MainActivity t6685}]
Hist #0: ActivityRecord{8fadec6 u0 com.ss.android.article.news/.activity.MainActivity t6685}
Intent { flg=0x24008000 cmp=com.ss.android.article.news/.activity.MainActivity (has extras) }
Run #0: ActivityRecord{8fadec6 u0 com.ss.android.article.news/.activity.MainActivity t6685}
mResumedActivity: ActivityRecord{8fadec6 u0 com.ss.android.article.news/.activity.MainActivity t6685}
Activities=[ActivityRecord{9130583 u0 cmccwm.mobilemusic/.ui.base.MainActivity t6681}, ActivityRecord{9822b05 u0 cmccwm.mobilemusic/com.migu.music.ui.local.LocalSongsActivity t6681}]
Hist #0: ActivityRecord{9130583 u0 cmccwm.mobilemusic/.ui.base.MainActivity t6681}
Intent { flg=0x10000000 cmp=cmccwm.mobilemusic/.ui.base.MainActivity (has extras) }
Run #0: ActivityRecord{9130583 u0 cmccwm.mobilemusic/.ui.base.MainActivity t6681}
ResumedActivity:ActivityRecord{8fadec6 u0 com.ss.android.article.news/.activity.MainActivity t6685}
ResumedActivity: ActivityRecord{8fadec6 u0 com.ss.android.article.news/.activity.MainActivity t6685}
我在今日頭條和今日頭條極速版的app中尋找從某個界面點擊某個按鈕返回到主頁的場景,沒有發現有這樣的場景,或者說很少(可能是我沒有發現),前面也說過使用standard標準模式只要是每次被啓動都會創建一個新的實例,如果其他界面回到主頁的場景多的話,我覺得可能會用singleTop(當要實現類似SingleTask的效果時可以配合flag實現)。場景極少或者沒有是它使用standard的原因吧。
最後,分享一份大佬收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記,高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料
這些都是我現在閒暇還會反覆翻閱的精品資料。裏面對近幾年的大廠面試高頻知識點都有詳細的講解。相信可以有效的幫助大家掌握知識、理解原理。
當然你也可以拿去查漏補缺,提升自身的競爭力。
如果你有需要的,可以點擊Github 自行領取
喜歡本文的話,不妨順手給我點個贊、評論區留言或者轉發支持一下唄~