效能最佳化

上一篇講了怎麼把壓縮位元組快速解成像素。這篇接著講:解出來的像素,怎麼做到一張幾億像素的大圖也能流暢瀏覽——拖曳、縮放都跟手不卡。

一張圖能有多大

手機隨手拍是 1200 萬像素,單眼 2400 萬,而一張衛星圖可以達到 18641 × 18641 ≈ 3.5 億像素。如果老老實實把它全部解碼進記憶體:

全部解碼進記憶體 ≈ 1.4 GB 3.5 億像素 × 4 位元組 螢幕真正能顯示 ≈ 800 萬像素 4K = 3840 × 2160 ≫

1.4 GB 記憶體、好幾秒的等待——只為了顯示一張縮到螢幕裡、肉眼看不清每個像素的圖。絕大多數像素根本沒有機會被看到。 果核看圖的思路是:只在需要的清晰度、需要的區域上花力氣。這靠三件事配合:金字塔、圖塊、按需載入。

金字塔:依需要的清晰度解碼

把原圖反覆縮小一半,存成一疊由大到小的層——這就是影像金字塔。最底層是原始尺寸,往上每層邊長減半,直到縮成一張 256×256 的小圖。一張 18641² 的圖大約有 8 層。

Level 0 — 18641² 原始 Level 1 — 9320² Level 2 … 最頂層 ≤ 256² 縮得越小 → 用越上面的層 放得越大 → 用越下面的層

以「符合視窗」開啟圖片時,整張圖縮進螢幕,根本不需要 18641² 的原始資料——直接拿某一個中間層來顯示,又快又省。等你放大查看局部細節,再切到更下面、更清晰的層。永遠使用剛好夠清晰的那一層,這就是金字塔的意義。多出來的層因為每層只有下一層的四分之一大,全部加起來也只比原圖多三分之一,代價很小。

圖塊:只解看得見的那部分

光有金字塔還不夠——放大看細節時用的是底層大圖,它依然有幾億像素。所以每一層都再切成 256×256 的小方塊(圖塊),顯示時只處理落在螢幕內的那些。

螢幕可視區域 可視區域內的圖塊:解碼 可視區域外的圖塊:先不管 每塊 256×256;一個 4K 可視區域 也只有約 135 塊

一個 4K 可視區域鋪滿,也只有約 135 個圖塊的工作量——和「全圖幾億像素」完全不是同一個量級。圖塊還能平行解碼:把這一百多塊分給多個 CPU 核心同時處理,呈現更快。

按需載入:拖到哪,解到哪

把金字塔和圖塊合起來,就有了一套「可視區域驅動」的載入機制。每次縮放或平移,畫布會算出目前可視區域涵蓋了哪一層、哪些圖塊,把還沒解碼的那幾塊交給背景執行緒去解,解好一塊就貼上一塊。

縮放 / 平移 可視區域改變 算出可見圖塊 選層 + 求交集 背景解碼 多核心平行 解好就貼上 逐塊更新 繼續操作 → 循環

解碼全在背景執行緒進行,不會卡住介面;已經解過的圖塊會快取起來,拖回去不用重新解碼。於是不論圖多大,每一刻真正在解碼的,永遠只是螢幕上這一小片。

先看見,再變清晰

還有最後一步體驗最佳化。切換到一張大圖的瞬間,可見圖塊還沒解完,畫布會先鋪上一張整張圖的低解析度預覽兜底——讓你立刻看到完整畫面,然後清晰的圖塊再一塊塊蓋上來,清晰度只增不減。

這裡有個容易出錯的細節:預覽圖和圖塊是兩層獨立繪製,如果切換時機沒處理好,會出現「清晰預覽 → 閃一下變更模糊 → 再變清晰」的倒退。我們特別確保了這條單調變清晰的路徑,整個過程不會出現回退閃爍。

把這套組合起來

  • 金字塔決定「用多清晰的資料」——依顯示縮放選層,不浪費在看不清的細節上;
  • 圖塊決定「解多大範圍」——只處理螢幕上的那一小片;
  • 按需載入 + 背景平行決定「什麼時候解」——拖到哪解到哪,不卡介面;
  • 低解析度預覽兜底決定「先看到什麼」——立刻有畫面,再逐步清晰。

四者合起來,記憶體和時間就只花在你真正看得見的像素上。這就是幾億像素的大圖也能隨手拖曳、縮放都跟手不卡的原因。