Sketch 插件內存優化
背景
我們內部孵化的 Sketch 插件上線後,不斷有用戶反饋安裝插件後,Sketch 會偶現崩潰、掛起的現象,通過遠程連接這些用戶,通過報錯打點統計排查,都沒有顯式地表明出原因。直到我們打開了 MacOS 的活動監視器
非正常情況下,Sketch 的內存佔用可以一直攀升直到意外退出,此時我們正在進行插件 1.x -> 2.x 的 break 迭代,內存泄漏的問題需要在這個迭代中盡量優化、解決
開發模式
先用一張圖解釋下 Sketch 插件的作用層級:
籠統來說,Sketch Plugin 作用到 Sketch.app 本身,有兩種主要方式:
- 編寫 CocoaScript + JavaScript API 間接作用
- 編寫 CocoaScript、Objective-C 直接作用
間接使用 JS API 的最大弊端是 API 的功能覆蓋太少(截止到 Sketch 49 版本前),而我們期望實現的功能大都沒有對應的介面來完成。因此在開發時,我們選擇使用 CocoaScript + Objective-C Framework 的組合方式,這樣的好處是能夠平衡開發的敏捷性與插件的私密性
回到內存佔用的問題上來,我們將絕大部分實現邏輯放到了 CocoaScript 中,CocoaScript 屬橋接語言,實質上還是 JavaScript,而 JavaScript 會自動釋放歸還內存。
所以我們排除了 CocoaScript 的因素,列出四個疑點問題:
- 插件依賴的 Cocoa-Framework 導致
- Sketch 自身的插件機制導致
- SKPM 打包過程中引入的 polyfill 導致
- NPM 三方依賴中有不靠譜的模塊
內存漏斗排查
內存增長出現在插件的 Command 被調用時、文檔打開時、App 啟動時
Cocoa Framework ?
直接將插件內載入 Framework 的邏輯注釋掉,發現內存仍舊持續增高,單次 Command 調用導致的內存增長並未出現明顯減少,因此暫時排除該因素
Sketch Plugin Manager ?
我們的內部插件使用 Skpm 進行構建(底層基於 webpack),在編譯過程中所有的 polyfill 自動打入 bundle 中
為了求實,我們使用 skpm 創建了全新的簡單插件,單次 Command 調用導致的內存增長並未出現明顯減少,也暫時排除該因素
NPM 三方依賴?
三方依賴的排查比較煩,沒有太好的方法,所以我們使用 「二分法」 注釋我們的插件源碼來一層層測試,直到最終插件里所有的邏輯的只剩下以下代碼:
- 唯一可執行的 `document.showMessage(Hello world)`
- manifest.json 定義(actions + commands)
此時,單次 Command 調用導致的內存增長出現了明顯的減少,但仍出現有內寸無法釋放的情況,因此我們得出一套初步結論:
- NPM 的某一(些)模塊會導內存泄漏,這是第一個漏斗
- 而排除 NPM 依賴,我們的代碼中仍存在漏斗
Sketch 自身插件機制?
上面提到了NPM 三方依賴中某一(些)模塊確實導致了我們插件給 Sketch 帶來了內存泄漏,但是不是唯一的源頭,一定還存在漏斗,「Sketch 自身插件機制」 是最終的疑點
插件機制排查的切入點是 manifest.json
文件,類似 Chrome 插件,這個文件定義了 Sketch 插件的命令調用入口、Actions 監聽、插件的基本信息 --> Plugin Bundles
於是我們注釋了所有可以刪除的定義聲明,指保留了唯一可以執行命令 `context.document.showMessage(hello world)` 的代碼,這其中一個主要的減法是:刪除所有 Acitons 的監聽聲明
Action API此時,單次 Command 調用導致的內存增長几乎在 5M 之內,而且多餘佔用內存可以自動釋放,至此,我們完全復現了無內存泄漏的插件雛形。我們需要找到 Actions API 導致內存攀升的石錘。實驗方式很簡單 --- 復原 `manifest.json` 文件的 actions 監聽
再次調用插件命令後內存飆升,可以確定 actions 是我們發現的第二個內存漏斗
馬桶里撈針
總結下我們找到的漏斗
- NPM 引入的三方模塊
- `manifest.json` 中聲明的 actions 監聽
而上面的成果,不足以支撐我們後續的優化工作,因此必須要進一步挖掘 > > >
二分注釋法是碼農的好朋友
Actions 的臭魚在哪裡?
通過注釋,我們發現了結論:所有 actions 監聽都是臭魚!!!
NPM 模塊的臭魚在哪裡?
既然發現了所有 actions 都是臭魚,那我們刪掉這些臭魚,還原之前 `manifest.json` 中所有的 commands 聲明,再來分層進行 NPM 模塊的排查,套路如下:
- 查找出問題的 commands
- 在出問題的 commands 中查找第一層依賴(直接罪魁禍首模塊)
- 在罪魁禍首模塊中遞歸排查最終的葉子節點依賴(根本罪魁禍首模塊)
老母豬戴胸罩,一套又一套
我們排查發現,插件中一個創新功能 「Ant Design 組件生成器」 的邏輯代碼被注釋後,插件的內存攀升情況消失
聚焦這個功能模塊,我們岔開下話題稍微聊一下這個功能的實現。2017 年底,Airbnb 開源了 react-sketchapp,其實質上是推出了一種 DSL(React Primitives),得以使用聲明 JSX 來在 Sketch 畫布上來渲染 Layers,這為了我們的組件生成器功能實現提供了很大的便捷
?????? 我們開源了 react-sketchapp 在 Ant Design 上的實踐:antd-sketchapp
回到主題,正是這個 react-sketchapp
模塊導致了內存的佔用攀升,而這時我們對 react-sketchapp 的底層實現有了很濃厚的興趣!
react-sketchapp:我的鍋 ???
該庫的 bug 仍然很多而且實踐使用的場景寥寥無幾,之前就踩過它對 context 的場景變化了解過少導致的引用崩潰的坑,最後我們提交了 PR 得以解決。我們只能接著使用 「二分法」 對該庫的代碼進行注釋排查,發現兩個文件的引入會導致內存持續飆升:
- render.js
- symbol.js
繼續挖掘,最終鎖定了兩個模塊的共同依賴 ---> yoga-layout ??????
yoga-layout 是什麼???
用一句話總結就是:跨平台的 flex 布局解決方案
使用過 react-sketchapp 的同學應該了解,其布局實現完全依賴 flexbox,所以可以說 Facebook 家的 Yoga 才是真正的巨人,而根據 Yoga 的 "cross-platform" 特性,我們猜測故事的結尾不一定會很完美 ????♀?
最終的謎團???
通過搜索 yoga-layout 的全部 issue、pr,發現有遇到類似問題的人提過 issue,而 yoga 官方又提及了一個名詞 --> asm.js,然後把鍋甩給了它 ??????,意指是 asm 的副作
asm.js:?? ???
我們繼續挖掘,如果 asm 也是 JavaScript,為什麼會產生分配的內存無法回收的問題?
2012年,Mozilla 的工程師 Alon Zakai 在研究 LLVM 編譯器時突發奇想:許多 3D 遊戲都是用 C / C++ 語言寫的,如果能將 C / C++ 語言編譯成 JavaScript 代碼,它們不就能在瀏覽器里運行了嗎?眾所周知,JavaScript 的基本語法與 C 語言高度相似。
於是,他開始研究怎麼才能實現這個目標,為此專門做了一個編譯器項目 Emscripten。這個編譯器可以將 C / C++ 代碼編譯成 JS 代碼,但不是普通的 JS,而是一種叫做 asm.js 的 JavaScript 變體。
Asm is an extraordinarily optimizable, low-level subset of JavaScript ??
原來其存在是為了解決 「C / C++ 編譯成 JS 有困難」 的問題:
- C / C++ 是靜態類型語言,而 JS 是動態類型語言。
- C / C++ 是手動內存管理,而 JS 依靠垃圾回收機制。
因此產生了其獨有的特性:它的變數一律都是靜態類型,並且取消垃圾回收機制。
??至此,這一漏斗的依賴順序為 antd-sketchapp -> react-sketchapp -> yoga-layout -> asm.js
內存優化
??處理漏斗一:
Sketch 支持的 action 列表:http://developer.sketchapp.com/reference/action/
通過篩選可以找出我們的插件有哪些 action 是剛需且低頻觸發的,有哪些 action 是可以通過其他實現方式取代的,我們的插件就有三個繞不過:
- OpenDocument
打開新 Sketch 文檔時需要做 toolbar 的展示、插件的初始化
- CloseDocument
關閉 Sketch 文檔時需要做緩存的銷毀、toolbar的關閉、所有功能面板的關閉
- ToggleInterface
Sketch 演示模式下的 toolbar 自動收起
除開以上的監聽,其餘所有功能通過更改實現方式來繞過,例如:Objective-C 的 NSNotificationCenter
可以做原生App的監聽通知;通過 CocoaSctipt 實現輪訓來取代圖層選擇的實時統計需求等等
??處理漏斗二
Issue 傳送門:Causes a continuous increase in memory usage · Issue #284 · airbnb/react-sketchapp
三方依賴導致內存增大的源頭已經發現了,需要 yogo-layout 去手動管理編譯後的 asm 的內存,但是插件的功能的迭代不能被 block,我們只能曲線救國
我們原本的策略,是使用 Skpm 將所有模塊統一打包成一個 bundle 文件,而這種做法會使 yoga-layout 的內存泄漏影響到我們的其他功能,將 「組件生成器」 模塊與其他功能模塊的聯繫從物理上進行切斷,即單獨編譯入口文件就可以解決之前運行任何插件命令都會導致內存飆高的問題
題外話
除了以上兩個坑以外,我們發現 CoScript 的一個實例方法會導致 CocoaScript 執行佔用的內存無法釋放,調用 coscript.setShouldKeepAround(true)
是非同步 CocoaScript 得以非同步執行的基礎條件,而在非同步代碼執行結束後,再次調用 coscript.setShouldKeepAround(false)
是確保 coscript 得以 dealloc 的手段,同時也可以釋放佔用的內存
期待 Sketch 開發者社區可以逐漸繁榮,讓我們開發者可以踩更少的坑 ??????
推薦閱讀:
※Sketch measure 更新了!告別付費的 zeplin
※Sketch+principle聯動製作可交互可愛動物肖像
※sketch的對齊像素功能其實也並不精準,求各位大神支招!?
※高能提升你的 Sketch 效率:編輯與導出篇
※App演示素材那麼貴,不如來用 Sketch 插件自己做吧
TAG:Sketch |