#分享 把 Gemini 塞進 AR 眼鏡:從卡成狗到神經外科級別的除錯血淚史

大家好,我又來了。距離上次把小米滑板車藍牙協議扒光之後,最近我又自找麻煩開了一個新副本: 「用 Rokid AR 眼鏡當攝像頭,手機藍牙接收照片,再串接 Google Gemini API 做 AI 分析。」 聽起來邏輯很通順對吧? 1.眼鏡拍照 2.傳給手機 3.手機問 AI 4.AI 告訴我不這什麼 實際上,我在這個過程中經歷了三次全網 StackOverflow 都救不了的詭異 Bug。而且最氣人的是,這三次都不是什麼高深的演算法問題,純粹是**「我弄反了」、「我想太少」**這種讓人想穿越回去掐死自己的工程細節。 這是一篇關於「如何不讓你的 App 變磚頭」的踩坑紀錄。 坑一:Frame Skipping 168 —— 你的 App 正在跑馬拉松,但你不知道 症狀: App 介面明顯卡頓,按鈕按下去要等三秒才有反應,彷彿回到了 Android 2.3 的年代。 Logcat 的尖叫: Skipped 52/168/57 frames in the last second 在 30fps 的世界裡,Skip 掉 168 幀意味著你的 App 在過去 5 秒內基本上是植物人狀態。我一開始以為是 Coroutine 卡住主執行緒,檢查 CPU、記憶體、I/O 全部綠燈,見鬼了。 真相: 兇手是相機的 ImageReader。 我在 Code 裡寫了一個監聽器(Listener),只要相機有畫面進來(Preview),它就無條件接收。 問題是,相機為了對焦和測光,啟動前 1.5 秒會有大量的 Frame 進來。而我原本的寫法是:「只要有 Frame,就轉成 JPEG。」 你知道 YUV 轉 JPEG 有多吃資源嗎?一幀大約 20ms。 相機熱身送來 45 幀 × 20ms = 900ms 的 CPU 滿載。 簡單說,我讓手機在還沒開始拍照前,就先瘋狂處理它根本不需要的預覽畫面,直接把 UI Thread 鎖死。 解法(觀念): 加一個「保全」(Atomic Flag)。 邏輯改成:預設門是關的。只有當使用者真的按下「拍照」按鈕,且相機熱身完畢(我設了 1.5 秒延遲)後,才把那個 Flag 打開。拍完一張立刻關門。 結果:Skipped frames 瞬間降回正常值,手指終於跟得上螢幕了。 坑二:JSON Parser 吃到類似 Emoji 的垃圾 症狀: App 跑得順順的,但 Log 狂跳 Failed to parse message,後面跟著一串亂碼加上原本正常的 JSON。 真相: 這是一個經典的通訊協議(Protocol)設計失誤。 我在藍牙傳輸時,混用了兩種資料: JSON 字串: 用來傳指令(如:拍照、開燈)。 Binary ACK: 用來確認封包收到沒(如:0x00, 0xFF 這種二進位數據)。 當眼鏡端回傳一個「收到!」的訊號(Binary)時,我的 App 居然天真地把它當成文字來解讀。 結果就是:亂碼(Binary) + JSON = JSON Parser 崩潰。 解法(觀念): 不要相信你的 Socket 永遠只會吐乾淨的 JSON。 我在接收端加了一個濾網:「只找大括號 {」。 如果是 Binary ACK,通常不會有 {,直接無視;如果有 {,就從那裡開始切字串。 這招有點暴力,但在混雜流的藍牙通訊裡,意外地好用。Log 瞬間乾淨了。 坑三:記憶體蒸發事件(The Silent Crash) 症狀: 照片傳輸需要切成 41 個小塊(Chunk)傳送。每次傳到第 33 塊左右,App 直接閃退。 沒有 Exception,沒有 Crash Log,只有系統冷冷的一句:PROCESS ENDED。 這通常代表 OOM (Out Of Memory) 被系統殺掉,或是 ANR。 真相: 這是最羞恥的一個坑。 我在處理藍牙接收的 bytes 時,用了一個很低效的方法:List<Byte>。 然後我寫了一個迴圈,把收到的幾千個 bytes,一個一個 add() 進去。 各位,ArrayList 的 add() 當容量不夠時,是會觸發 Array Copy 的。 傳一張照片(41 個 Chunk),我可能觸發了幾千次的 Resize 和記憶體重新分配。這在現代手機上可能還好,但在高頻率的藍牙傳輸下,這造成了大量的記憶體碎片(Fragmentation)和 GC 壓力。 系統覺得這 App 吃相太難看,直接把它殺了。 解法(觀念): 換工具。別用湯匙(List.add)搬沙子,用鏟子(Stream)。 我把資料結構換成了 ByteArrayOutputStream。它支援 write(byte[], offset, length),可以一次把一大塊資料倒進去,不用一個一個 byte 處理。 結果:從第 33 幀必死,變成 100% 傳輸成功。 總結:給同樣在玩硬體串接的人 回頭看這三個 Bug,其實都跟演算法無關,而是對「資料流動」的想像不夠精確: 1.Frame Skipping 是因為沒擋住不該處理的流。 2.JSON Error 是因為沒分清楚文字流和二進位流。 3.Memory Crash 是因為處理流的容器選錯了。 現在這套「眼鏡 -> 藍牙 -> 手機 -> Gemini API」的系統終於穩定了。雖然現在看來都是些低級錯誤,但在 Debug 的當下,真的會讓人懷疑人生。 如果你也在做類似的 AR 或 AI 硬體專案,記得檢查一下你的 Buffer 和 Listener,別跟我一樣犯傻了。 祝大家的 Console 永遠沒有 Error! #Android #Rokid #血淚史
愛心
13
3
全部留言