# 跑 SQLite 查詢不及時使用 sqlite3_finalize() 的嚴重後果,望周知 本事件已被我投稿至純靠北工程師(「#純靠北工程師 7ib 」): https://www.facebook.com/init.kobeengineer/posts/pfbid02hDLMenCJsnpQw59AcsJdFeiu7nFcKqnbqT71SFuY16ZVsWF6S8VC9zz82UGsFKX3l 教訓是:用 SQLite 跑完「每一筆」查詢之後一定要「立刻」用 sqlite3_finalize(StatementPointer) 釋放記憶體,不然會產生連 Xcode Instruments 都抓不到的記憶體洩漏。 事情是這樣: 為了因應 Apple 對 8GB 記憶體的病態執著, 我最近剛剛給威注音輸入法的原廠詞庫格式由 JSON 升級至 SQLite 。 我是第一次用 SQLite ,看這篇學的(威注音是用 Swift 開發的): https://itisjoe.gitbooks.io/swiftgo/content/database/sqlite.html 現在 Swift 用 SQLite 超方便,直接 import SQLite3 就行。 ( Linux 或者舊版 macOS 還可以直接讀 SQLite3 的 almagation 。) 只是,竊以為我犯的最大錯誤就是「沒有針對相關的 SQLite API 詳讀 SQLite 官方的手冊」。 我一開始以為讓所有的 SQLite Statement 共用一個(作為靜態變數的) nullable OpaquePointer 就可以了, 於是我一開始的設計就是讓辭典模組在 deinit() 的時候才跑一次「 sqlite3_finalize()」。 因為原廠詞庫是唯讀的、在 App Bundle 內,所以我將 journal 塞在了記憶體內。 結果呢?施工完畢之後,單元測試啥的都成功通過了,剛剛開啟輸入法時的記憶體佔用是 18MB 左右。 當時我感覺很開心,因為這幾乎是直接消滅了輸入法啟動時花在原廠辭典上的載入時間。 但是,週一上午,我用這輸入法沒打六分鐘的字,輸入法碰到了我之前設定的保險絲而自盡了。 > 我早前做了這麼個保險設計:只要記憶體佔用超過 768MB ,輸入法就自盡、自動重新啟動來釋放記憶體。 然後,我用 Xcode 14.2 的 Instruments 抓記憶體洩漏, 但只抓到一個與系統輸入法選單有關的小微型洩漏(還是無法解決的那種)。 正好我週一那天公司休假,我花了一整個週一的時間排查自己做過的所有修改, 最後開始懷疑「 sqlite3_finalize()」是不是得在每次查詢之後都執行一次。 於是我補上了這句試了試: ```swift extension vChewingLM.LMInstantiator { fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) { guard Self.ptrSQL != nil else { return } defer { sqlite3_finalize(Self.ptrStatement) } // 補上了這句。 sqlite3_prepare_v2(Self.ptrSQL, sqlQuery, -1, &Self.ptrStatement, nil) while sqlite3_step(Self.ptrStatement) == SQLITE_ROW { guard let rawValue = sqlite3_column_text(Self.ptrStatement, column.id) else { continue } handler(String(cString: rawValue)) } } ... } ``` 結果發現記憶體洩漏的問題失蹤了。 這問題幸好發現得早,不然威注音 3.6.2 正式版帶著這個 Bug 發出去的話會坑死很多人。 然後我就將這次經歷投稿到靠北工程師,但沒想到反應非常空前。 居然還有人說我積陰德。還有人在轉發時提到這麼個例子: > 超市的系統在頻繁的查詢資料後都會有整個系統延遲的狀況,不知道跟這有沒關係... > 雖然資料庫跟我沒啥屁關係,也先備份再說。 於是我決定將這個故事也放在 HackMD 這邊讓各位 SQLite 用戶共勉。