慢慢來之生成函式


最近在改良我的電子佛典 app – cbetar2,其中新增一項大功能就是支援匯入 CBETA 經文檔 zip 作離線瀏覽。起初的版本我只在我的 hTC U19e 6 GB RAM Android 手機與 iPad 模擬器測過。但後來發現 iPad 模擬器與實機有一重大差異 – 硬體資源,主要是 CPU, RAM, 儲存空間這些部分基本上模擬器的資源就是開發機(我的 MacBook Air M1 2020)的資源,而不是 iPad 實機的資源。而往往開發機的資源會比實機好上很多,因此這個部分模擬器的表現與實機不同。的確我的 app 在模擬器可以順利運行,但在實機 iPad 6 2 GB RAM 就會當掉。

我的 app 在匯入小檔 zip 時都可以順利運作,但匯入完整 CBETA 經文檔 zip 才會在 iPad 當掉。因此這顯然是匯入時作 zip 解壓縮的部分耗掉太多 RAM 所致。經研究我原本使用的解壓縮程式 adm-zip 只支援一次將 zip 讀入 RAM 後的使用,可想而至少要耗掉 1 倍經文檔 zip 大小 – 1.2 GB 的 RAM,因此要找其它解決方案。

我後來認為要解決這種一次把大檔 zip 載入 RAM 作處理而造成 RAM 不足的問題,要改為多次慢慢讀取 zip,最好是每次只讀出 zip 中的一筆項目,待我的 app 處理完再讀下一筆。後來知道真的有這種程式處理概念串流 – stream。雖然這名詞我很早就聽過,但一直對它沒很清楚,不知道用在哪裡。stream 的概念就是為了達成不須取得完整檔案,就能作處理的一種技術。stream 其實很常見,例如 YouTube 影片就是,我們不必等到把整部影片下載到電腦後才能看,YouTube server 只要傳影片一部分資料給我們,就能開始看。

而 JavaScript (JS) 中,也有針對檔案的 stream 功能 – Blob,所以代表 JS 也能一點一點的讀檔。除了 JS 要支援 File stream,還須要支援 zip stream 的程式才行,但這已超出我能力所及,不可能自己寫。我找了一段時間,幸好找到現成的 zip.js!它的演算法確實使用 stream 技術,因此適合讀大檔。把它整合到我的 app 後,成功讓 iPad 6 2 GB RAM 匯入 1.2 GB 的 CBETA 經文檔 zip!

但至此與此文標題有何關係?我都在說 stream 技術啊!?因為我仍不滿意😅雖然用 zip.js 後解決了 iPad 的問題,但仍有一個問題未解,就是 Android 在匯入時仍無法讀入官方 1.2 GB zip,只能讀入我刪檔後 639 MB zip. 我研究後認為這是因為 zip.js 只提供一個 getEntries 函式,一次取回 zip 檔中所有項目(不含項目解壓後的資料,以節省 RAM 使用),但這些項目太多仍吃掉不少 RAM. 所以我就在想能不能改良 zip.js 讓它一個一個讀取 zip 項目?

研究 zip.js  原始碼後,發現 getEntries 一開始是作一些 zip 標頭資訊的讀取,最後有一個迴圈迭代每一個 zip entry 並附加到一 entries 陣列,迴圈結束後才回傳完整 entries. 如果這迴圈每個迭代沒有相依變數,那麼此迴圈的程式區塊可以簡單獨立為一個函式,就能達到一次讀一項的目的,但可惜它們是有相依的。因此我就在想這迴圈能不能每迭代一次,就回傳一個結果?這想法喚醒我以前看過的 generator function (生成函式)! Generator function 剛好就能達成此目的。因此我輕鬆的就把 getEntries 用 generator function 改寫,而整個改寫僅動了 13 行程式碼,改出一個可以一次一次慢慢迭代 zip entry 的 getEntriesGenerator generator function. 詳細改寫在這

改寫 zip.js 後,使用它確實解決我的 app 不能在 Android 匯入 1.2 GB 官方 CBETA 經文檔 zip 的問題😁後來我把改寫後的版本送出一個 GitHub pull request 給官方 zip.js,很快的被接受、合併程式碼😁我才知道生成函式的妙用,可用來一次一次慢慢的迭代具有相依的一段程式碼並將輸出一次一次處理。而不必一次跑完整個迴圈消耗大量資源後,才能處理輸出。


Leave a Reply