資深谷歌安卓工程師對安卓應用開發的建議
with Romain Guy and Chet Haase
擅長Java語言的資深開發者們,多年以來多是工作在網頁,服務器,和桌面系統等開發領域。這些領域的經驗幫助他們建立起來了自己使用Java語言的模式和自己的Java庫的生態系統。但是移動應用的開發卻和這些領域的java開發有著天壤之別。優秀的安卓應用開發者需要考慮到移動設備的限制,重新學習怎么樣去使用java語言,怎么樣去有效地使用實時環境和安卓平臺,然后寫出更好的安卓應用程序。
See the discussion on Hacker News .
Transcription below provided by Realm: a replacement for SQLite that you can use in Java or Kotlin. Check out the docs!
</div>Sign up to be notified of new videos — we won’t email you for any other reason, ever.
About the Speaker: Romain Guy
Romain是谷歌的安卓工程師。在加入Robotics之前,他在安卓Framwork組參與了安卓1.0到5.0的開發工作。他現在又重新加入了安卓的新UI和圖形圖像相關的項目。
</div> </div>About the Speaker: Chet Haase
Chet 也是谷歌的工程師。 他現在是安卓UI Toolkit 組的組長,他擅長于動畫,圖像, UI控件和其他能帶來安卓更好的用戶體驗的UI組件的開發。他還擅長撰寫和表演喜劇。
</div> </div>介紹(0:00)
本次演講是以安卓平臺組寫的近10篇文章為基礎的。所有的文章都能夠在Medium網站上看到,文章的第一部分請看 這里 . 今天我們會講到這些文章里面的一些東西,如果你對特定的話題感興趣或者想深入了解它們,請去閱讀原文。
為什么移動開發如此艱難?(1:47)
有限的內存(1:47)
我們發現谷歌公司里面的應用開發者有一個大問題,他們對口袋里每天都攜帶著的安卓手機的本質都有些誤解。這些設備內存,CPU的處理能力和電池的待機能力都非常有限。開發者們必須理解你們的應用不是在設備上唯一運行的應用。內存是非常有限的資源,并且被整個系統共享著。我所在的Android平臺組非常小心地對待這個問題,這也是為什么有的時候我們建議的一些規則看起來會有一點極端的原因。我們需要有全局的眼光來看待這個問題,因為系統會同時運行20、30、40個的進程。當你只為單一應用開發的時候,牢記這些限制是比較困難的。
今天我們手上的設備往往會有1-3GB的內存,但不是所有的安卓設備都有這么多的內存的。安卓陣營中有14億的設備,其中許多是兩三年前的產品。事實上,他們才是著14億設備的主力軍。大部分的設備不是在美國和西歐,而是在中國和印度這樣的國家。在這些國家里,安卓設備必須便宜才能吸引更多的消費者。
安卓在從4.3到4.4的演進過程中,我們使出渾身解數才使安卓系統能夠成功地在內存受限的系統上運行起來。很長時間以來,因為人們想在便宜的500MB內存的設備上使用安卓系統,姜餅系統成了他們唯一的選擇。當然,現在的情況已經大不相同,姜餅設備已經差不多消失殆盡了。但是,因為現在的安卓版本有著龐大的大內存消耗的應用群,這就需要更強大的框架和平臺。
這是一件你必須持續思考的事情,但是這很難,因為它不總在你優先考慮的事情的范疇內。你可能知道Knuth的名言“過早的優化是一些罪惡的根源”。雖然我同意他的觀點,可是當你寫完你的應用,然后打算從應用中再減少50MB的內存的使用,也是一件非常困難的事情。我們不是說你需要毀掉你的應用系統架構或者犧牲你的測試,但是每一次你能做些改進來改善你的內存使用情況的時候,請馬上動手。
為了整個設備有更好的整體用戶體驗而開發。當你的應用很大的時候,你會導致系統殺掉別的應用,這樣你就影響到了別人應用的用戶體驗。當別人的應用不注意內存使用情況的時候,你的應用體驗也不會好。你們需要互相交流然后找到和諧共存的方法,請牢記這條建議。
如果系統需要殺掉除了你的應用之外的所有應用,那么當你的應用退出以后,其他應用需要重新建立他們可運行的環境。這樣你的應用會看起來像一個惡意的應用。如果你想要讓用戶重入你的應用時有一個良好的體驗的話,請盡你所能將你的應用保持在后臺運行并且消耗最小的內存。
在有些應用當中,實現內存回調接口是非常管用的。你能在API文檔中非常容易的找到 onTrim 的內存回調函數,并且能實現多個級別的Trim操作。基于你的Trim操作的級別,你可以釋放一些緩存和bitmaps,也可以退出一些Activity,或者其他的任何能幫助你留在后臺運行的措施。
CPU 處理器(8:07)
移動處理器顯然比桌面和服務器的處理器能力要慢很多。雖然從外界不容易察覺,但是移動處理器大部分時候都是處在過熱降頻保護的狀態。這意味著盡管CPU的頻率很高,但是仍然不能像桌面和服務器的處理器一樣快。當諸如用戶在屏幕上拖拽的時候,CPU可以達到它的最快處理速度。可是在其他的情況下,CPU處理器是跑在待機頻率上的,不然的話,電池不能保證用戶的基本待機時間。
如果你買的是2GHz的臺式機或者筆記本,CPU大部分時候是能跑在那個參數指標上的。可是你如果買的是一個2GHz的手機,CPU只能有時候跑在2GHz上。如果你買的是八核的手機,硬件上你是有八個處理核,但是他們大部分時候不會一起工作。基本上,手機盒子上的參數表和你實際上能達到的參數是有不一致的地方的。這主要是為了降頻保護CPU,提高電池待機時間和減少發熱量。
GPU 圖像處理器(10:10)
和CPU一樣,GPU也有保護降頻的功能。紋理加載(Texture uploads)的開銷特別大。所有bitmap的操作或者結果是bitmap的操作都會被加載到GPU。舉個例子說,當你在繪制路徑時,這些東西會轉成bitmap并且作為紋理加載到GPU,這時系統的開銷就會特別大。你的性能瓶頸就可能在這。
填充率和分辨率之間也是相關聯的. Nexus 6P的分辨率非常高,換句話說,屏幕上有許多的像素點要填充。但是GPU的性能帶寬卻跟不上。系統為了填充所有的像素點而超負荷的工作。你的要求越高,系統需要的時間就越長。
一個提升UI 性能的技巧就是避免過度刷屏。你需要系統填充屏幕上每次像素的次數越多,當屏幕越來越大和分辨率越來越高的時候,情況就會越來越糟。如果你有一個Playstation 4或者Xbox One的話,他們在運行那些1080p的游戲(每秒30幀)時十分卡頓。在我們的手機上,我們有更高的分辨率,而且我們以每秒60幀的速度完成所有的事情;我們盡可能的完成更多的事情并且消耗更少的電量。這就是為什么我們對圖片的處理性能有著諸多的要求以及對應用優化的建議,因為我們沒有你想象的那么強的處理能力和電池電量。
內存 = 性能(12:29)
如果你有大的應用,那么就會發生更多的內存頁的置換,更慢的內存分配。實際運行中,系統需要遍歷來決定新的對象可以放到內存何處,這需要花費更多的時間。回收也會需要花費更多的時間;每次需要內存的時候都要遍歷更多的東西。
整體上內存回收機制也被更多地觸發。在我們的演講中,我們詳細闡述了內存回收是如何工作的,而且討論了ART和Dalvik的改進。詳情請看上面的視頻 at 13:06
低端設備(19:40)
你口袋里面的設備保證比你用戶的設備要快很多。你認為的那些老舊設備并沒有消失。 A 它們依舊在用戶的口袋中,而且用戶打算盡可能長地使用它們。B 那些老舊的設備雖然慢但是便宜。這意味著它們能吸引哪些不能夠購買最快設備的人們。
流暢的幀頻率(21:01)
找到一個流暢的幀頻率是十分困難的。你只有16毫秒的時間去完成所有的事情。這些事情包括觸屏事件的處理,計算,布局,繪制幀,然后交換緩存。16毫秒意味著每秒刷新60幀,這是我們在安卓系統上要求的,因為我們是V-Sync。我們不希望屏幕花屏。花屏在有些游戲中你能看到,因為它們在一秒內同時有兩個buffer。那個看起來太可怕了,我們不想讓你這么做。這就意味著如果你僅僅多花了一點時間,哪怕17毫秒,我們就會跳過一幀。然后跳過一次V-sync,這樣系統就不是60fps了,而是30fps。我們叫這種現象為Jank。
系統在60fps和30fps中來回跳躍是件非常糟糕的事情。這會讓你的用戶覺得你的應用非常janky和不一致。也有可能你的應用會一直表現糟糕,一直都是30fps。這就是許多游戲當它們認為做不到60fps的時候,它們就一直都是30fps。
實時運行: 語言 ≠ 平臺(22:48)
幾周前,有人問我 “當有新版本的JDK發布的時候,你一般做什么?”。 然后我意識到他們并不理解語言和實時運行環境是不一樣的。當有新的JDK發布的時候,我基本不關心。它和安卓運行時一點關系都沒有。我們使用同一種語言,但那不意味著我們在運行的時候是一樣的。
當人們使用Java語言的時候,有三個方面的因素構成了整個java體驗。Java編程語言本身,實時運行環境(在有些server環境中叫HotSpot)然后是硬件設備。
對于那些擅長服務器的人來說,服務器的實時運行環境提供諸如移動,壓縮收集器的功能,這些都意味著臨時的內存分配帶來的系統開銷非常小。這些事情和移動指針一樣快。在服務器環境中,的確如此。然后你會理所當然的認為移動設備有一樣快的處理器,一樣大的內存,所以處理能力就應該和服務器一樣無限制。
在安卓系統中,情況是非常不一樣的,特別是你習慣于無限的系統資源和完全不同的實時運行環境的時候。我們有Dalvik和ART。我們沒有壓縮,這意味著當你分配一個對象的時候,這個對象就會存在堆里面。堆會碎片化,也會使得尋找空余的內存空間變得開銷更大和更困難。在ART環境中,我們有空閑時的壓縮。ART從來不能在應用是前臺運行的時候壓縮堆空間,但是當它在后臺運行的時候是可以壓縮堆空間的。在進程的生存周期里面,有的時候壓縮是會發生的,這會幫助系統尋找空閑內存。
壓縮在本地代碼上的作用更為關鍵。如果你的應用使用JNI,并且分配一個指針給一個java對象的時候,那個指針通常會被系統認為一直有效。在ART環境中,如果你的堆被壓縮了,那指針就會變成野指針。如果你使用JNI,在開始的時候你就必須對這種情況特別小心。
UI 線程(26:56)
安卓是一個單線程的UI系統。技術上,你可以有多個UI線程,但這會帶來很多麻煩。正如你擔心的那樣,所有的UI控件都是在同一個線程里。因為是一個UI線程,所以你任何阻塞UI線程的操作都會帶來性能上的影響,jankiness(閃屏)和不一致性。在UI線程上分配內存就更糟了。如果你有后臺運行的線程分配內存,當虛擬機VM阻塞所有的線程(包括UI線程)十幾毫秒的時候,你就會跳過一幀。這樣,你就只有30fps,這會非常糟糕。
存儲(28:53)
存儲的性能沒有一個定論。有時候人們把數據存在SD卡上,這就會有各種各樣的性能標準。當存儲設備快要滿的時候,即使在同一個存儲設備上也是有不同的性能體現的。但是如果你的應用是依賴每個測試設備的存儲速度的話,那總歸是不好的。寫flash存儲的時候也意味著控制器需要做很多事情,因為它需要收集信息,記錄哪些空間已用和哪些空間沒用。換句話說,如果你的應用在后臺做大量的IO操作的話,你會把這個系統拖慢。你應該有這樣的經驗,當Play Stoer 在后臺安裝應用更新的時候,你的設備會突然間感覺慢了起來。因為它在后臺做磁盤寫操作,所以前臺的應用讀它們自己資源也會變得很慢。
存儲的大小也是各式各樣的,所以不要讓你的應用對特定的存儲大小有依賴。APK的大小也很重要。你應用中資源的大小會影響到你的啟動時間。
優化你的應用資源的辦法有很多。現在在老的版本的安卓上,也有了矢量圖。如果可以的話,請使用安卓的SVG庫。雖然對于圖標來說不是太合適,但的確能對你的應用起好的作用。你也可以使用WebP格式,這會比PNG省下20%-30%的存儲空間。 PNG Crush 也是一個很好地離線工具。APT做了一部分PNG crush的工作,但是還有很多工具會做的更好,更有效。
網絡(28:53)
你使用的網絡肯定比大多數用戶的網絡要快。他們可能還在用著2G網絡,并且流量很貴。所以你的應用不應該依賴持續的網絡連接。也許你應該讓你的應用能夠智能地下載它需要的內容或者你的應用不需要依賴網絡下載的內容就能使用,真正的內容可以晚點再去下載。
開車去Utah的路上,你可以測試在糟糕的網絡環境下,你的應用的表現如何。或者你可以用一些模擬糟糕網絡的工具。所有這些應用的測試都是手工進行的。
每一臺設備都是一個村莊(33:31)
每一臺設備都是一個村莊,這意味著每個在村莊里的人都需要共同努力來維護一個好的用戶體驗。你可以讓這個體驗特別差,也可以特別好。如果你的應用想成為好的體驗的支持者,你可以試著在你的manifest里面關閉Service和broadcast receivers。在代碼里面,當你的應用發現用戶打算使用你的應用的時候,你才動態的開啟你的services和receivers。郵件客戶端就是一個好的例子,當用戶安裝郵件客戶端的時候,所有的一切都應該處于關閉的狀態。一旦用戶點擊了該郵件客戶端并且加入了賬戶的時候,你才開啟你的services和receivers。然后下次用戶重啟設備的時候,Framework就會記錄下來,你的服務就真正的運行起來了。這個方法很簡單,但也很重要。因為你的services雖然運行起來了,但是沒有人用,你會對別人帶來不必要的壞的影響。
另外一個現象叫做”公共地帶的悲劇“: 每個人都認為他自己的應用是最重要的。如果每個人都是這樣的觀點,那么每個應用都會非常大而且盡可能被激活,這樣設備會承受不了。勿以惡小而為之,勿以善小而不為。
技巧與建議(35:53)
了解你使用的語言(35:55)
開發者們可能有了很多年Java的經驗,但是卻不能最好地利用語言來發揮出移動設備的最大性能。
不要使用Java的序列化
序列化本身是非常有用的,這不是我們現在討論的話題。如果你用過序列化的話,你會發現不是太好用,因為你需要產生UUID,而且它還有限制。序列化也會慢,因為它用到了Reflection。
使用安卓的數據結構
其他場合Java開發者常用到的一些集合類和方法在Android上也許就不合適了。Autoboxing 和 Primitive Java 類型的替代品在Java領域里使用的非常廣泛。集合的迭代器也是非常常用的。在Android平臺上,我們特別創建了一些集合類來替代這些模式。對于Key來說,我們使用基本類型,所以如果hash表里面是一個整形作為key的時候,我們沒有autoboxing。你可以以使用SparseArray類,同樣的,ArrayMap和SimpleArrayMap也是HashMap的替代者。
為了避免java package中基礎類型的額外開銷,使用Android的數據結構類型是個不錯的選擇。HashMap里面的每個條目都會多用4倍的內存空間,像你的int類型一樣。你放在hashmap里面的內容越多,你浪費的內存資源也就越多。看看你現在已經使用的數據結構,如果你打算在某些情況下重寫你自己的數據類的話,不要猶豫。
小心使用XML和JSON
他們相對來說太大了,對于你的某些應用來說,他們或許太結構化了。如果你使用有線設備上的數據格式會更加簡潔,但是至少你能在你做網絡傳輸的時候gzip這些數據。當然,如果你打算序列化你的對象到磁盤上,你可能需要找找其他的替代方案了。
避免使用JNI
有些時候你需要JNI,但是如果不方便就不要使用它了。JNI有些有趣的事情:每一次你越過JAVA和Native的邊界的時候,我們都需要檢查參數的有效性,這會對GC的行為有影響,而且帶來額外的消耗。有的時候這些消耗是非常昂貴的,所以當你使用了很多JNI的調用的時候,你可能在JNI調用本身上花的時間比在你native代碼執行的時間都要多。如果你有些老的JNI代碼,請仔細檢查他們。
有一件你能提高JNI效率的事情就是盡可能的把多次調用集中到一次。避免在JAVA和Native中間來回調用,一次搞定。例如,在Android的Graphic Piplline中,我們在每次調用JNI的時候傳入了盡可能多的參數,從而避免了調用JNI的時候,JNI從JAVA對象中抽取參數的開銷。我們使用了基礎類型,避免了使用奇怪的對象,我們傳入的是Left,bottom和right參數,所以我們不需要又返回到JAVA層了。
基礎類型 vs. 封裝后的基礎類型
請使用基礎類型來代替封裝后的基礎類型。一個不那么明顯的事實是:如果你使用那些集合類并且比較它們是否相等的時候,我們每次都會做autobox。每次都需要分配一個對象去使用,因為它們會被強制轉換成boxed equivalent。這里有個例外,你可以使用big boolean,因為它們只有兩個。
避免使用反射(Reflection)
比避免使用JNI更進一步,我需要如我建議避免使用JNI一樣指出來,animation framework使用了JNI來避免使用反射。這樣當然多了雙重內存分配的開銷,因為我們需要分配這些對象并且每處都要autobox他們,同時從Java運行環境發起的函數調用才用的內部機制的開銷特別大。
小心使用 finalizers
內部我們只在很有限的情況下才用。關于finalizers有個事實不太明顯的是:它需要兩次完整的GC才能收集完所有的事情。如果你在一個finalizer里面放置某些assert來收集信息的話,我們需要運行兩次完整的GC才能回來處理。有些時候這是必要的,但是在其他的一些情況下,把這些處理放在離finalizer之外的近點的地方會更方便。不然的話開銷太大了。
網絡(47:19)
不要過度同步
正如前面描述的那樣,你的用戶的網絡可能不那么好,或者他們的網絡會很貴。而且,你會給系統帶來負擔。也許你認為你的應用需要盡可能快地得到那些數據,但是事實上是你會給系統帶來很多的負擔,僅僅為了保持你的應用處于激活狀態。
允許延時下載
這在信號不好的網絡和收費很貴的網絡下十分重要。你可以把數據打包起來。把所有需要下載的東西收集在一起,然后做一次同步。
谷歌云消息 Google Cloud Messaging
GCM收集了很多東西。他會使用傳送層來和后臺服務器傳送數據,所以你也許可以重用這個系統來避免自己重復工作,也避免了每個應用都創建自己的socket。
GCM 網絡管理(Job 調度)
這也是個很好的東西可以利用起來。Job 調度可以讓你把你的東西打包起來。你可以說 “我想在空閑的時候才使用這些事務,或者當我被激活的,或者當我在wifi連上的時候”,然后在普通的時間間隙里面收集這些事情。這會更有效,對用戶也不會太突兀。
不要輪詢
永遠不要輪詢
只同步你需要的
你知道你的應用里面發生了什么,所以僅僅只要同步當前應用需要的數據,而不是把所有可能需要的數據都同步下來。
網絡質量(50:05)
不要對網絡有任何的假設。為低端網絡開發,然后保證你的應用在低端網絡下測試過。即使是你在使用模擬器,你也需要保證你在我們稱為“糟糕的網絡”的環境下測試過。在這些情況下,你應用的表現會讓你大吃一驚的。
數據 & 協議(51:13)
如果你擁有服務器,做所有能幫助到設備的事情。比如改變數據格式,改變發送數據的類型。設備來告訴服務器它打算瀏覽圖片的大小,也是個好方法。我們看到過內部的應用接收到了比屏幕大小大4倍的圖片,然后在設備端重新處理圖片的大小。這個工作明顯服務器來做比較合適。
使用縮小和壓縮的算法。gzip是個好的選擇。如果你能在HTT上傳送GZIP的數據,請這樣做。這會很有用,特別是在糟糕的網絡上。GCM也可以幫忙。它會幫助你保持連接,所以,一個好處就是使用它會有更短的延時,因為不用在每次連接的時候都做一次握手而且在延時方面,移動網絡是出了名的糟糕的。
存儲(52:21)
不要寫死你的文件路徑
路徑會改變的,如果用戶想把它們存到別處呢?
僅僅固定相對路徑
你知道你的數據相對你的APK的路徑,這是正確的,安卓有APIs得到那些路徑。你不需要做hard code的事情,你只需要堅守和你APK實際安裝位置的相對路徑。
使用存儲緩存來處理臨時文件
有APIs可以調用,請使用它們。
簡單情況下不要使用SQLite
SQlite的消耗在某種程度上也是很大的,所以如果你只需要些簡單的方式,比如 key-value存儲會更加合適些。
避免使用太多的數據庫
數據庫整體上開銷是很大的。也許你可以試試只用一個數據庫,然后服務于多個不同的用戶。
讓用戶選擇內容存儲位置
當用戶有可移除的存儲設備的時候這些尤為重要。或者他們可以使用adoptable storage,這是在棉花糖版本上的新的方法。如果用戶打算采用新的存儲設備,然后把數據都遷移到它上面,你的應用應該允許他們這樣做,這樣你的應用就不會亂掉了。
問和答(54:00)
問:你提到了一個平滑的策略是降低你的幀頻率,有可能一個應用說: “我需要30fps而不是60fps嗎?”
Romain:某種程度上。有一些你可以使用的API來達到和屏幕同步的目的。你可以每隔一幀同步一次。在普通的應用中,這會是很困難的,除非你知道你會因為外面各種各樣的設備而有許多的工作量。但是,如果你使用的是OpenGL或者Canvas,你又有自己的線程做渲染,并且你對渲染線程有完整的控制權的時候,情況就不一樣了。在這種情況下,你可以控制你的幀,你可以等待,然后你記錄V-Sync,諸如此類的動作。如果你想深入的了解這些高級的技術細節,你可以看看那些游戲開發者的文章,他們在做類似的事情。
Chet:一般情況下,你可以嘗試60fps,然后你可以使用一個叫GPU Profile的工具,這個工具在Android Studio上就有。工具里有一個彩色的顯示條,你需要保證你持續的呆在綠線以下。你可以在 developers.android.com 查到工具的詳細信息。
Romain: 還有,我們經常用 Systrace。Systrace是一個底層的,輕量級的,系統層面的監測工具。如果你的應用在任何地方有性能問題,它不需要嵌入在你的代碼中。Systrace不但能顯示你在那你花費了時間,而且還能顯示你的線程是否被調度,CPU的運行頻率,你是不是被從一個CPU轉移到另一個CPU上(這也會影響到你的性能),是否后臺線程導致了鎖定,或者優先級倒置。文檔在developer.android.com上也能找到。這也是個很有用的工具。
問:有一個類叫‘android.os.memfile’。 它和linux 共享內存聯系在一起。當我使用它的時候,我能從linux 內核中得到300-400MB的內存來共享。所以,我使用它來分享拍攝的視頻并且存到了臨時區域。同時我把它設為 non-purgable. 因為使用了400MB的內存,Android總會參與進來刷新內存。你對使用這個技術來存儲奇怪的內存有什么建議嗎,或者不要使用共享內存?
Romain:這是個非常有趣的問題。我沒有什么想法。我也不知道系統有這樣的行為,也許你應該問問內核組來理解這里的行為。如果是如設計一樣,那么后果是什么。對不起,我沒有一個更好的答案。
問:我想指出當Android在切換Activity狀態的時候,會使用共享內存。他們發現如果你一直監視著你的Android設備中共享內存的使用狀態,你可以準確地猜出Activity是處于什么狀態,如:是從onResume到onStop,所以這看起來像是個安全漏洞。顯然,安卓再每次做Activity的狀態轉換的時候都使用了共享內存,有時候是12bytes。所以實時監測共享內存的狀態,你就可以知道Activity是在什么狀態。
Romain: 如果你一直監視著內存狀態,你可能還可以知道其他的發生在設備上的事情。比如看著屏幕。但是,很高興你能分享這點。
問:你之前討論到應該避免使用JNI,也需要避免使用Reflection。然后你提到你在動畫實現中忍受了許多開銷來避免使用Reflection。我想知道如果使用了Reflection,會在動畫對象中發生些什么呢?
Chet: 我們沒有用Reflection,我們用的是JNI。JNI里面有Reflection的代碼。它是這樣工作的:他會調用到JNI說,“我需要一個方法”。所以,當你想使用一個動畫的屬性叫做“foo”。你傳入了一個串,然后說“好吧,我想找到一個設置的方法叫做setFoo”。進入JNI層后,看起來JNI層會說:“有這樣的方法的簽名嗎?”。如果JNI找不到,就會返回false然后就會使用Reflection。我認為Reflection的代碼應該永遠都不被調用到。所以JNI需要返回false。我以前也使用過Reflection,只要是我寫的代碼,我會使用已經存在的接口來保證沒有其他奇怪的事情發生。然后,在使用Reflection的時候我還會有些backup的機制。但是我不認為Reflection需要這樣用。這種情況下可以使用JNI。在有些代碼中,也許不太明顯,但是如果你找到代碼正確的地方,你會發現一個情況是“JNI夠用嗎?如果他夠用,就使用JNI好了。”
Romain: 當你使用JNI的時候,你其實可以有效地訪問到所有的成員和方法,這就是Reflection做的事情。我們發現使用JNI會比Reflection快上很多。
Chet: 而且部分原因是內存的消耗。因為至少算上運行的開銷,JNI是不會額外再分配內存的。通過任何機制實現方法查找都不簡單。但是,我們只會在你第一次調用的時候做方法查找。所以,當你創建動畫對象的時候,第一次你運行的時候,就開始尋找setter或者getter,然后緩存它們。這樣同樣的情況不會再次發生。
Romain: 另一個事情是當你通過Reflection調用一個set alpha的函數的時候,你會在方法對象上調用該方法,這需要一個對象。所以當你需要傳入一個Float時,你需要分配內存來封裝(boxing)你的Float。但是如果你用的是JNI,你就不需要boxing。
Chet: 當然還有第三種機制,如果你打算習慣它的話,就是屬性(properties)。這就是我們為什么很早前就加入屬性(properties)。為一些特殊的視圖我們創建了屬性,alpha屬性,translation X屬性, rotation X屬性等等。他們直接使用setters。所以當你使用屬性的時候,他直接調用了靜態的屬性對象。你在視圖的實例中傳遞它們,它會調用視圖的seter,這樣是開銷最小的方法。
問:你剛才說你們不太關心Java的版本發布,因為Java語言和實時運行是分開的。但是我還是很好奇你們如何看待 retrolambda,它讓你使用lambdas,這是Java 8 的功能,但是它把Lambdas編譯成二進制代碼。而且作為dexing的一部分,它把他們重編譯成匿名類。你有什么建議嗎?
Romain: 我知道retrolambda,而且我喜歡它。我覺得它太棒了。使用它之前,我會反編譯retrolambda的結果看看發生了什么。因為使用lambda,我敢肯定你會有些討厭的驚喜。我非常肯定它們在某些地方會變成匿名類,所以在你會遇到大量的內存分配,分配情況取決于它們是如何介入的。例如,如果我在一個循環中傳入一個lambda,內存分配會發生什么呢?所以,我會去看看反匯編代碼,然后在我決定前看看發生了什么。你知道,匿名類可以被緩存,或者它會在每次使用的時候分配,我不知道具體會發生那種情況。我需要看看。作為一個軟件工程師來說,在決定如何使用前,盡量的去理解背后到底發生了什么是非常重要的。因為這樣做,就不會在之后的開發中遇到討厭的驚喜。當然,在某些情況下,如果你是在用戶點擊一個按鈕時使用了lambda,并不重要,對嗎?當你點擊一個按鈕時,性能不是個特別大的問題。
問:我想知道的是什么時候你們會支持Java 8, 然后你不需要把他們編譯成匿名類。
Romain: 不知道,實話實說,這幾個月我們確實討論了不少關于桌面系統的JAVA 8的事情。我使用過streams,parallel streams和lambdas。這些都太棒了。代碼看起來特別棒,寫起來都特別有趣。但是當你意識到在某些情況下,他們確實會比老的循環更慢些的時候,感覺就不一樣了。再說一遍,小心一些,盡量去理解你現在正在做的取舍。關于Java 8語言什么時候會被Android支持,我不知道,因為我們沒有討論過這個事情。而且即使我知道,我也不能告訴你。
Chet: 我有必要提一下,我最喜歡的一個工具就是DDMS里面的Allocation Tracker,它現在可能是在Monitor的什么地方。反編譯二進制代碼是一個好的方法,這樣可以看看到底編譯器干了什么,但是運行的時候看看發生了什么也是個好方法。當我們剛開始開發動畫框架的時候,我們使用Allocation Tracker去找到我們在每一幀的時候分配了些什么,然后消除它們。所以,如果你用retralambda,你需要確定在你應用的關鍵點,你不要分配那些從外部看不需要的內存。
問:我們日常的工作都是feature驅動的,對嗎?內部,我們如何去維護一個性能的標桿?你們有專職的QA來跟蹤你們的代碼,然后你可以看棧的trace,然后時刻提醒你,或者作為一個feature的開發者在你們提交給QA前你們有什么標準嗎?
Romain: 這是個混合的問題。對于那些非常關心他們應用的性能的工程師來說,這是個他們開發過程中非常小心的問題。也有QA會關心這個話題,但是這幾年來,Android組寫了很多自動化的測試用例來測試性能。例如,拿Butter項目來說,圖像組為每一個CL做了一個jank的dashboard。他會運行一些像在Gmail中滾動滾動條的用例,然后計算我們錯過的幀。每一次數字的變動,我們都會收到一封郵件,然后有人就會受到批評。當然應用的開發者會先收到批評,然后他們會發給我們框架組說:“你們太爛,你們的東西太慢”。然后我們會回復它,這樣會來來回回討論好長時間。
Chet: 我們也會寫出許多工具并且改進它們。Systrace就是一個我們內部正在使用并且持續改進的一個很好的工具,之后我們提供給所有人使用。并且我們持續迭代地開發它。這樣我們就像最近做的事情一樣,在一些不明顯的情況下,給你一些關于你的問題的提醒和信息。
Romain: 事實上,大部分的工具你可以通過工程的UI看到很多信息。所以諸如Hierarchy Viewer, devel-draw,debug tool 和 GPU profiler都在設備上可以運行。所有的這些工具都是我們內部需要的,所以我們開發出來并且提供給所有人使用。當然,還有很多的事情可以做,但是關于性能最重要的事情就是你們能寫出自動的測試用例來捕捉它們。這是非常困難的一件事情。我們暴露了一些內部的計數器,我想我們有文檔描述它們。在adb shell的dumpsys幫助下,你可以獲取其中的一些信息,幀的時間戳,janks的次數,特別是在棉花糖和棒棒糖中我們加入了更多的方法。你可以做的事情就像UI automater一樣,你可以創建一個用例,驅動你的應用,然后輸出這些計數器看看能否告訴你些什么有用的信息。更有效的方式是有個dashboard。它抓取了這些命令的輸出,然后給你畫一個特別好看的圖表。另一件事情是,當你做些事情的時候想想你的性能。說起來簡單,做起來難,評判起來更難,理解你在量化什么還要困難一些。但是,這是唯一的途徑。
問:你提到不要使用序列化。你能再詳細的描述一下嗎?在Activity和Fragment間使用Serializable和Parcelable傳輸數據包含在里面嗎?
Romain: 我們是在討論Serializable的接口。Parcelable是Android提供的Serialization的一個變種,是非常有效的,但是僅僅用來進程間的數據傳輸。現在我們有新的方式了,搜索“persistent parcel”,你就可以找到它了。但是對了,我們討論的是Serializable接口。
問:你之前提到flat buffers。有一種說法是如果網絡請求需要很長時間的話,節省100多毫秒不太重要。
Romain: 我之前給的例子比較有意思是因為他用了JSON作為磁盤上文件存儲格式。這里的想法是你已經緩存了數據了,所以你已經承受了網絡的開銷,所以可以優化的地方在你多次讀取這些數據的時候。在這種情況下,使用更快的方式是有意義的。但是你是對的,如果解析JSON需要10毫秒,解析你的flat buffer只需要5毫秒,但是網絡傳輸需要200多毫秒,這樣的優化還有作用嗎?顯然,我希望你的應用在后臺線程處理所有的網絡操作。所以這里討論的更多的是UI的延時,而不是別的。如果你有什么東西是經常訪問的,你當然需要在flat buffer 這樣的地方里面尋找它。
還有一個Cap’n Proto,也是protobuf v2的開發者開發的。他寫了一些類似flat buffer的東西,但是更加深入。因為它可以被作為有線傳輸格式,可以被用作RPC機制,而且我相信它有Java 封裝層。這就回到我們之前的堅持,當你考慮性能的時候,你需要量化標準。確定你修改的是正確的問題。如果你修改的地方不是個問題可能還無所謂。作為我們來說,在你的應用里面到底什么會是個問題更難。因為這完全依賴于你的應用。我們只能分享我們在應用中一次又一次看到的問題,再一次強調,這不意味著你的應用也有同樣的問題。但是真的,如果你在磁盤上做serialization,看看其他的格式吧。總而言之,找到你最好的方式。
See the discussion on Hacker News .
Sign up to be notified of new videos — we won’t email you for any other reason, ever.
</div> </div>來自: https://realm.io/cn/news/romain-guy-chet-haase-developing-for-android/
</span></span>