魔窗研發副總裁沈哲:移動端SDK的優化之路

作者簡介:

沈哲,擅長移動端、互聯網後端技術,曾經在安碩信息、decarta(已被uber收購)、京東商城等國內外知名軟件公司、互聯網公司工作。開發過decarta第一款地圖導航app,今夜酒店特價app,負責過京東到家上海的移動端團隊。現負責魔窗移動端團隊,負責研發魔窗的sdk以及移動端相關產品。

1

本人自2015年9月底加入魔窗,開始着手優化魔窗移動端sdk的工作。

魔窗是基於Deep Link技術的開放平臺,通過提供生態落地最後一公里的deep link、跨App store渠道的歸因分析以及場景還原(deferred deep link)等解決方案爲App開發者構建一個去中心化的高效連接時代。最重要的產品就是iOS和Android端的SDK。

sdk優化過程,是一段血淚史,可以吐槽的地方無數。移動端sdk不像app一樣方便,sdk發佈後出現任何問題,都會影響到很多家的app。不能像一家app一樣,可以及時發佈一個hotfix,或者強制升級app,又或者熱更新app。所以sdk發版之前,必須經過嚴格的測試,每一次sdkhotfix的發佈都會對我們的用戶造成嚴重的影響。

sdk的優化,最大的痛點是它的大小。每次對接客戶,他們都會問我們sdk的大小是多少?每當提到iOSsdk時,他們都會說還蠻大的,他們自己家的app都已經幾十M了,接入我們的sdk會增加他們app的大小。所以,不得不開始痛苦的sdk優化之路。

我們主要從以下幾個方面進行優化sdk:

1. 腳本構建

2. 極限優化(網絡、日誌上報、圖片格式等方面優化)

3. 第三方組件替換

4. 小版本穩步迭代

腳本構建

我們從開始開發sdk到目前正在開發中的3.8版本,一直推崇藉助腳本進行自動化打包,例如android使用gradle。藉助腳本的好處在於:

1)androidsdk混淆

2)自動生成文檔,便於開發者查閱,例如android可以很方便的生成javadoc文檔

3)androidsdk上傳aar包,iOSsdk發佈到cocoa-pods,便於開發者集成

4)節省人工時間,減少出錯

腳本通常能幫助我們實現很多自動化的事情,能提高工作效率的方法是一定會被採納的。

接下來我們來看看藉助gradle如何實現sdk混淆,核心的task是proguardJar這個task。

2

極限優化 

所謂極限優化,是指從多個角度、維度對sdk進行優化,重點是考慮網絡優化以及電量消耗優化。能夠做到代碼精簡,低網絡流量,微能耗而不僅僅是低能耗。

香農定理是所有通信制式最基本的原理,我們知道C=B lb(1+S/N)

其中:C是信道支持的最大速度或者叫信道容量,B是信道的帶寬,S是平均信號功率,N是平均噪聲功率,S/N即信噪比。

從最初的1G網絡到現在的4G網絡,都是在利用這個公式提高速度。要麼充分利用頻道資源,要麼提高整體帶寬。但是頻段資源都是有限的,所以不得不制定出更優秀的策略來提高資源的利用率。結合網絡情況、手機電量等因素,我們採取以下幾種方式進行優化:

1)合併網絡請求,減少服務器壓力和dns請求時間,減少手機的網絡流量。

2)數據緩存到本地,最省電的方式就是不使用移動網絡,數據緩存能大大減少網絡請求的次數。

3)日誌上報策略,批量非實時上報。日誌生成後,首先存儲在RAM中,基礎策略是滿30條發送,每隔一分鐘輪詢一次。爲了滿足客戶定製需求,發送策略可通過後臺配置。如果遇到異常情況,比如網絡異常或者crash等,我們會將日誌存儲在本地sqlite中,在程序下次啓動後,根據發送策略再次發送。

3

爲了減少app的網絡流量消耗,我們還將活動的圖片新增了WebP的格式。 

4

WebP格式的圖片好處是什麼?舉個例子,做一個簡單的測試對比PNG 原圖、PNG 無損壓縮、PNG 轉WebP(無損)、PNG 轉WebP(有損)的壓縮效果。

可以得出結論:PNG 轉WebP 的壓縮率要高於PNG 原圖壓縮率,同樣支持有損與無損壓縮。

轉換後的WebP 體積大幅減少,圖片質量也得到保障(同時肉眼幾乎無法看出差異)。轉換後的WebP 支持Alpha 透明和 24-bit 顏色數,不存在 PNG8 色彩不夠豐富和在瀏覽器中可能會出現毛邊的問題。

WebP 的優勢體現在它具有更優的圖像數據壓縮算法,能帶來更小的圖片體積,而且擁有肉眼識別無差異的圖像質量。除此之外,國內外很多知名的應用已經使用了WebP格式,這也是我們使用它的原因之一。

在3.8版本的sdk中,用於活動的Marketing接口會返回PNG和WebP兩種格式的圖片。對於Android而言,如果操作系統版本在4.0以及4.0之後,它天生支持WebP格式,sdk會優先加載這種格式,加載不成功纔會去加載PNG的圖片。如果是Android 4.0以下,sdk只加載PNG圖片。

對於iOS而言,目前iOS本身不支持WebP格式(但願iOS10會支持它:(),要藉助第三方庫才能支持,比如SDWebImage。但是iOS sdk已經足夠大了,不可能把SDWebImage集成到sdk。所以,目前iOSsdk不會像androidsdk一樣存在imageloader,iOSsdk把圖片加載的權利交給開發者。當然以後,我們肯定會給iOSsdk提供類似android的imageloader的功能。

藉助Webp,我們替用戶節省了流量,節省了手機內存和CPU資源。

未來,網絡請求還會進一步優化。會考慮使用protobuf協議替換現在的返回json格式。protobuf返回的數據更小,而且是二進制的格式。從安全性的角度上說,在一定程度上能夠防止被惡意抓取數據包進行分析。

第三方組件替換

對於移動端sdk的開發者來說,移動端其餘的開發人員都是幸福的。他們可以嘗試使用無數的第三方庫,在github上每天都會誕生很多優秀的第三方庫。sdk的開發者不得不自己去實現很多功能,因爲考慮到sdk大小的問題。

對於sdk的開發者來說“這是一個最好的時代,也是一個最壞的時代”。他們必須自己去“造輪子”,但是會給他們帶來更多收穫,無論是接觸到os的底層還是設計模式,都會比普通的開發者瞭解更多。

我們魔窗的sdk包括Androd、iOS版本在不斷迭代的過程中,都經歷過第三方組件的替換。以android爲例,我們替換了json解析器和網絡框架等等。

最初,我們使用fastjson,它是由阿里巴巴的工程師編寫的,性能和穩定性都很好。我自己寫app時,也會首選它作爲json的解析器。但是它明顯增大了sdk的體積,於是我們使用gson替換了fastjson。用了一段時間後,覺得gson還是很大。

最終,我們考慮重寫jsonparser。重寫的jsonparser,必須能兼容原先gson的一些api,避免sdk工程做太大的改動,這是我們重寫的一個目標。

重寫jsonparser之前,我們先對反射做了一次封裝。傳統的反射是這樣寫的:

5

封裝之後的寫法是這樣的,基於流式API:

6

依託於簡潔的反射,實現了自己的jsonparser。除此之外,還需要將http請求返回的結果藉助自己的json工具類轉換成對象、對象數組。類似於這樣:

7

藉助這個反射我們還獲得的額外好處是,在android4.0以後的版本能夠隨時獲取到App的ApplicationContext,以前還擔心獲取不到ApplicationContext,這樣一來還能防止memory leak。因爲,Activity的Context使用不當經常會引起內存泄露。

8

另一個被替換的第三方組件是volley。它是google開發的網絡框架,便於android應用操作網絡。替換volley的原因,是它功能太強大了,簡直就是一個“全家桶”。我們用不到那麼多功能,sdk需要的是一個符合自身業務需求的網絡框架。同樣,替換的準則是能夠兼容原先volley的大部分api。於是我們做了一個簡化版本的volley,它大致的流程如下圖所示:

9

它最主要的四個部分是:Request、RequestQueue、NetworkExecutor和ResponseDelivery。

Request,即各種請求類型。包括StringRequest和ImageRequest,分別表示返回的數據是字符串和網絡圖片的請求。Request支持Get、Post請求,支持header、支持請求緩存、支持postbody、支持請求的重試機制。Request類還包含了一個回調處理的接口ResponseListener。

第二部分爲消息隊列RequestQueue,消息隊列維護了提交給網絡框架的請求列表,並且根據相應的規則進行排序。默認情況下更具優先級和進入隊列的順序來執行,該隊列使用的是線程安全的PriorityBlockingQueue,因爲我們的隊列會被併發的訪問,因此需要保證訪問的原子性。

第三部分是NetworkExecutor,它是網絡的執行者。該Executor繼承自Thread,在run方法中循環訪問第二部分的請求隊列,請求完成之後將結果投遞給UI線程。爲了更好的控制請求隊列,例如請求排序、取消等操作,這裏我們並沒有使用線程池來操作,而是自行管理隊列和Thread的形式,這樣整個結構也變得更爲靈活。它的主要代碼是這樣的:

10

其中,doRequest()方法用於真正的網絡請求和分發網絡請求返回的Response。doRequest()支持重試機制,它的大致流程如下圖所示:

11

第四部分是ResponseDelivery,在第三部分的Executor中執行網絡請求,Executor是Thread,但是我們並不能在主線程中更新UI,因此我們使用ResponseDelivery來封裝Response的投遞,保證Response執行在UI線程。

總之,每個部分都符合單一職責的原則,便於日後的獨立維護。

我們再看看怎麼藉助這個網絡框架如何調用httppost請求。 

12

一. NeteaseAPM是什麼

對於普通的app開發來說,小版本快速迭代幾乎是不可或缺的方法論。而對於sdk開發而言,“小步快跑,快速迭代”的策略不再適用。我們必須採取相對穩健的更新策略。

sdk是面向所有的開發者使用的,高版本必須向下兼容api。如果某個api確實需要過期的時候,至少保留幾個版本後再刪除過期的api,並附有詳細的說明文檔。

對於sdk而言,版本發佈也不宜頻繁,否則會讓開發者會感覺自己是“小白鼠”。這樣的體驗,對於開發者是相當不友好的。

13

對於每一個小版本除了新增的功能之外,我們都會集中精力優化好某一塊地方。每一個小版本都是“小步迭代”,但是經過幾個版本的迭代之後,還是能夠實現量變。下面的表格是我開始接手魔窗sdk之後,androidsdk體積的大小的變化。

從3.0到3.7版本,android sdk的大小,總體趨勢是不斷減少的。其實功能不斷增加的,sdk的穩定性也得到提升,這就是我們採用小版本不斷迭代帶來的好處。

未來,sdk拆分

關於未來,我們追求的是在保證sdk穩定的前提下,繼續努力減少sdk的大小。將我們的sdk拆分成多個組件,供用戶挑選自己想要的各個組件。我們目前sdk的模塊如下圖所示。

14

sdk最核心的部分是sdkcore,它是sdk必不可少的組成部分。它有以下幾部分組成:

1)http組件,是我們自己開發的http模塊,符合自己的業務需求。

2)imageloader組件,在sdk中顯示活動圖片的組件,是自己開發的模塊。

3)domain,是sdk所需要的對象,包括http返回的對象以及業務模型。

4)config組件,是sdk必須的配置組件。

5)jsonparser組件,json解析器,是我們自己開發的模塊。

6)utils,sdk中各種幫助工具類。

7)sqlite組件,操作數據庫的相關類,把一些數據緩存到sqlite數據庫。

其餘的組件雖然沒那麼重要,但是可以通過自由組合的方式,組成開發者想要的功能。這是我們未來1-2月的努力方向——sdk的拆分。將sdk拆成更小更細粒度的模塊,開發者也能更好地選擇他們想要的模塊。

比如一個開發者只想要tracking功能,那麼他只需使用sdkcore包和tracking包。再比如一個開發者只想要mLink(基於deeplink深度改造)的功能,那麼他會需要sdkcore包、tracking包、magicwindowview包和mLink包這幾個包。      

Ending

sdk無論怎麼拆分,穩定性是最最重要的。它涉及到使用sdk的所有app,以及app背後的無數用戶。作爲sdk的開發者,必須對用戶負責,要抱有一顆敬畏之心。

經歷sdk的拆分之後,我們會逐步開源sdk的功能到github社區,接受所有開發者的監督。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章