iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Mobile Development

Android 性能戰爭:從 Profiler 開始的 30 天優化實錄系列 第 10

# Day 10:【啟動戰役】第一幀的藝術:延遲載入你的主畫面

  • 分享至 

  • xImage
  •  

各位戰士,歡迎來到第十天的戰場。昨天我們成功地為 Application 進行了瘦身,為應用的快速啟動掃清了第一道障礙。然而,真正的決戰發生在 Activity 層級。使用者不會因為你的 Application 初始化得快而給你按讚,他們只關心什麼時候能看到第一個有意義的畫面

這個畫面,我們稱之為「第一幀」。而從 Activity 啟動到第一幀繪製完成的這段時間,是使用者體感上最敏感的區域。ActivityonCreate()onResume() 方法就是這段關鍵路徑上的核心。任何發生在這裡的延遲,都會被使用者看得一清二楚。


主執行緒的「塞車」現場

Activity 的生命週期方法,尤其是 onCreate(),都運行在主執行緒 (UI Thread) 上。這意味著,如果你在這裡做了任何耗時的操作,整個 UI 渲染流程都會被阻塞,畫面將停留在白屏或閃屏頁,直到你的程式碼執行完畢。

檢查一下你的 MainActivity,是否存在以下常見的「塞車」行為:

  • 複雜的佈局渲染:一個嵌套過深、過於複雜的 View 層級會增加測量 (Measure) 和佈局 (Layout) 的時間。
  • 同步的 I/O 操作:在 onCreate() 裡直接讀取大型檔案或查詢資料庫。
  • 大量的資料處理:從 Intent 或資料庫中讀取一個大列表,然後在主執行緒上對其進行排序、篩選或轉換。
  • 初始化重量級物件:建立一些需要大量計算或設定的複雜物件。

我們的核心戰術是:盡快顯示一個骨架,然後再填充血肉。


延遲載入的兩把利刃

要實現這個戰術,我們需要將非必要的任務從 onCreate() 這個關鍵路徑上移開。有兩種簡單而強大的武器可以幫助我們。

1. View.post():簡單的延遲器

這是一個非常經典的技巧。View.post(Runnable) 會將一個 Runnable 任務投遞到主執行緒的消息佇列 (Message Queue) 中,但這個任務會等到下一次的繪製週期,在佈局和繪製都完成之後才執行。

這非常適合用來延遲那些「錦上添花」的 UI 更新。

2. 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
    }
}

在這個例子中,從 setContentViewprogressBar.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 (基準設定檔),看看它是如何透過預編譯來為你的應用加速的。

我們明天見!


上一篇
# Day 9:【啟動戰役】Application 的瘦身計畫
下一篇
# Day 11:【啟動戰役】Baseline Profiles (基準設定檔) 的威力
系列文
Android 性能戰爭:從 Profiler 開始的 30 天優化實錄12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言