各位戰士,歡迎來到第十天的戰場。昨天我們成功地為 Application
進行了瘦身,為應用的快速啟動掃清了第一道障礙。然而,真正的決戰發生在 Activity
層級。使用者不會因為你的 Application
初始化得快而給你按讚,他們只關心什麼時候能看到第一個有意義的畫面。
這個畫面,我們稱之為「第一幀」。而從 Activity
啟動到第一幀繪製完成的這段時間,是使用者體感上最敏感的區域。Activity
的 onCreate()
和 onResume()
方法就是這段關鍵路徑上的核心。任何發生在這裡的延遲,都會被使用者看得一清二楚。
Activity
的生命週期方法,尤其是 onCreate()
,都運行在主執行緒 (UI Thread) 上。這意味著,如果你在這裡做了任何耗時的操作,整個 UI 渲染流程都會被阻塞,畫面將停留在白屏或閃屏頁,直到你的程式碼執行完畢。
檢查一下你的 MainActivity
,是否存在以下常見的「塞車」行為:
View
層級會增加測量 (Measure) 和佈局 (Layout) 的時間。onCreate()
裡直接讀取大型檔案或查詢資料庫。Intent
或資料庫中讀取一個大列表,然後在主執行緒上對其進行排序、篩選或轉換。我們的核心戰術是:盡快顯示一個骨架,然後再填充血肉。
要實現這個戰術,我們需要將非必要的任務從 onCreate()
這個關鍵路徑上移開。有兩種簡單而強大的武器可以幫助我們。
View.post()
:簡單的延遲器這是一個非常經典的技巧。View.post(Runnable)
會將一個 Runnable
任務投遞到主執行緒的消息佇列 (Message Queue) 中,但這個任務會等到下一次的繪製週期,在佈局和繪製都完成之後才執行。
這非常適合用來延遲那些「錦上添花」的 UI 更新。
lifecycleScope.launch
:現代化的非同步方案對於那些涉及 I/O 或複雜計算的任務,我們需要一個更強大的武器——協程 (Coroutines)。透過 lifecycleScope
,我們可以在 Activity
的生命週期內安全地啟動一個協程。
lifecycleScope.launch
會立即返回,不會阻塞主執行緒。它內部的程式碼塊可以在背景執行緒中執行耗時操作,完成後再切回主執行緒更新 UI。
onCreate
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 顯示一個 ProgressBar (但使用者可能根本看不到)
val progressBar: ProgressBar = findViewById(R.id.progress_bar)
progressBar.visibility = View.VISIBLE
// 2. 在主執行緒上執行耗時的資料庫查詢
val data = loadDataFromDatabase() // 假設這個方法耗時 500ms
// 3. 在主執行...緒上處理資料
val processedData = processData(data) // 假設這個方法耗時 300ms
// 4. 設定給 RecyclerView
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
recyclerView.adapter = MyAdapter(processedData)
// 5. 隱藏 ProgressBar
progressBar.visibility = View.GONE
}
}
在這個例子中,從 setContentView
到 progressBar.visibility = View.GONE
的所有程式碼都會阻塞 UI。使用者會盯著白屏長達 800ms,然後畫面「突然」一下全部顯示出來。
改造後:使用協程實現非同步載入
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 盡快設定佈局,顯示骨架 (包含一個可見的 ProgressBar)
setContentView(R.layout.activity_main)
val progressBar: ProgressBar = findViewById(R.id.progress_bar)
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
// 2. 啟動一個協程來處理耗時任務,不會阻塞 UI
lifecycleScope.launch {
// 3. 在背景執行緒中載入和處理資料
val data = withContext(Dispatchers.IO) { loadDataFromDatabase() }
val processedData = withContext(Dispatchers.Default) { processData(data) }
// 4. 回到主執行緒更新 UI
recyclerView.adapter = MyAdapter(processedData)
progressBar.visibility = View.GONE
}
}
}
改造後,onCreate()
的執行路徑變得極短。setContentView
會立刻執行,使用者會馬上看到一個帶有載入動畫的頁面骨架,這給了他們一個即時的反饋。同時,耗時的資料載入工作在背景悄悄進行,完成後再平滑地更新 UI。使用者的體感從「卡頓的白屏」變成了「流暢的載入」。
今日總結
今天,我們將戰火從 Application
燒到了 Activity
,並掌握了第一幀的優化藝術。
我們認識到 Activity.onCreate()
是啟動性能的關鍵路徑。
我們的核心戰術是:先顯示骨架,再非同步填充內容。
我們學會了使用 View.post()
進行簡單的延遲,以及使用 lifecycleScope
處理複雜的非同步任務。
透過今天的改造,我們極大地改善了使用者的啟動體感。但是,我們目前所做的都是執行期 (Runtime) 的優化。有沒有辦法在編譯期 (Compile time) 就讓程式碼跑得更快呢?
答案是肯定的。明天,我們將探索一個更底層、更強大的優化利器——Baseline Profiles (基準設定檔),看看它是如何透過預編譯來為你的應用加速的。
我們明天見!