[Android]使用Dagger 2依賴注入 - 圖表創建的性能(翻譯)
使用Dagger 2依賴注入 - 圖表創建的性能
#PerfMatters - 最近非常流行標簽,尤其在Android世界中。不管怎樣,apps只需要正常工作就可以的時代已經過去了。現在所有的一切都應該是令人愉悅的,流暢并且快速。舉個例子,Instagram 花費了半年的時間 只是讓app更加快速,更加美觀,和更好的屏幕適配性。
這就是為什么今天我想去分享給你一些小的建議,它會在你app啟動時間上有很大的影響(尤其是當app使用了一些額外庫的時候)。
對象圖表的創建
大多情況下,在app開發過程中,它的啟動時間或多或少會增加。有時隨著一天天地開發它是很難被注意到的,但是當你把第一個版本和你能找到的最近的版本比較時區別就會相對比較大了。
原因很可能就在于dagger對象圖表的創建過程。
Dagger 2?你可能會問,確切地說 - 就算你移除了那些基于反射的實現方案,并且你的代碼是在編譯時期生成的,但是別忘了對象的創建仍然發生是在運行時。
對象(還有它的依賴)會在第一次被注入時創建。Jake Wharton 在Dagger 2演示中的一些幻燈片很清楚地展示了這一點:
以下表示在我們的 GithubClient 例子app中它是怎樣的:
- App第一次(被kill之后)被啟動。Application對象并沒有 @Inject 屬性,所以只有 AppComponent 對象被創建。
- App創建了 SplashActivity - 它有兩個 @Inject 屬性: AnalyticsManager 和 SplashActivityPresenter 。
- AnalyticsManager 依賴已被創建的 Application 對象。所以只有 AnalyticsManager 構造方法被調用。
- SplashSctivityPresenter 依賴: SplashActivity , Validator 和 UserManager 。 SplashActivity 已被提供, Validator 和 UserManager 應該被創建。
- UserManager 依賴需要被創建的 GithubApiService 。之后 UserManager 被創建。
- 現在我們擁有了所有依賴, SplashActivityPresenter 被創建。
有點混亂,但是就結果來說,在 SplashActivity 被創建之前(我們假設對象注入的操作只會在 onCreate() 方法中執行)我們必須要等待以下構造方法(可選配置):
- GithubApiService (它也使用了一些依賴,如 OkHttpClient ,一個 RestAdapter )
- UserManager
- Validator
- SplashActivityPresenter
- AnalyticsManager
一個接一個地被創建。
嘿,別擔心,更復雜地圖表也幾乎被立即創建。
問題
現在讓我們想象下,我們有兩個外部的庫需要在app啟動時被初始化(比如,Crashlytics, Mixpanel, Google Analytics, Parse等等)。想象下我們的 HeavyExternalLibrary 看起來如下:
public class HeavyExternalLibrary { private boolean initialized = false; public HeavyExternalLibrary() { } public void init() { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } initialized = true; } public void callMethod() { if (!initialized) throw new RuntimeException("Call init() before you use this library"); } }
簡單說 - 構造方法是空的,并且調用幾乎不花費任何東西。但是有一個 init() 方法,它耗時500ms并且在我們使用這個庫之前必須要被調用。確保在我們module的某處的某一時刻調用了 init() :
//AppModule @Provides @Singleton HeavyExternalLibrary provideHeavyExternalLibrary() { HeavyExternalLibrary heavyExternalLibrary = new HeavyExternalLibrary(); heavyExternalLibrary.init(); return heavyExternalLibrary; }
現在我們的 HeavyExternalLibrary 成為了 SplashActivityPresenter 的一部分:
@Provides @ActivityScope SplashActivityPresenter provideSplashActivityPresenter(Validator validator, UserManager userManager, HeavyExternalLibrary heavyExternalLibrary) { return new SplashActivityPresenter(splashActivity, validator, userManager, heavyExternalLibrary); }
然后會發生什么?我們app啟動時間需要500ms還多,只是因為 HeavyExternalLibrary 的初始化,這過程會在SplashActivityPresenter依賴圖表創建中執行。
測量
Android SDK(Android Studio本身)給我們提供了一個隨著應用執行的時間的可視化的工具 - Traceview 。多虧這個我們可以看見每個方法花了多少時間,并且找出注入過程中的瓶頸。
順便說一下,如果你以前沒有見過它,可以在 Udi Cohen的博客 看下這篇Android性能優化相關的文章。
Traceview可以直接從Android Studio(Android Monitor tab -> CPU -> Start/Stop Method Tracing)啟動,它有時并不是那么精確的,尤其是當我們嘗試在app啟動時點擊 Start 。
對于我們而言,幸運的是當我們知道確切的需要被測量的代碼位置時,有一個可以使用的方法。 Debug.startMethodTracing() 可以用來指定我們代碼中需要被啟動測量的位置。 Debug.stopMethodTracing() 停止追蹤并且創建一個新的文件。
為了實踐,我們測量了SplashActivity的注入過程:
@Override protected void setupActivityComponent() { Debug.startMethodTracing("SplashTrace"); GithubClientApplication.get(this) .getAppComponent() .plus(new SplashActivityModule(this)) .inject(this); Debug.stopMethodTracing(); }
setupActivityComponent() 是在 onCreate() 中調用的。
文檔結果被保存在 /sdcard/SplashTrace.trace 中。
在你的terminal中把它pull出來:
$ adb pull /sdcard/SplashTrace.trace
現在閱讀這個文件所要做的全部事情只是把它拖拽到Android Studio:
你應該會看到類似以下的東西:
當然,我們這個例子中的結果是非常清晰的: AppModule_ProvideHeavyExternalLibraryFactory.get() (HeavyExternalLibrary被創建的地方)花費了500ms。
真正好玩的地方是,縮放trace尾部的那一小塊地方:
看到不同之處了嗎?比如構建類: AnalyticsManager 花了小于1ms。
如果你想看到它,這里有這個例子中的 SplashTrace.trace 文件。
解決方案
不幸的是,對于這類性能問題,有時并沒有明確的回答。這里有兩種方式會給我們很大的幫助。
懶加載(臨時的解決方案)
首先,我們要思考是否你需要所有的注入依賴。也許其中一部分可以延遲一定時間后再加載?當然這并不解決真正的問題(UI線程將會在第一次調用Lazy<>.get()方法的時候阻塞)。但是在某些情況下對啟動耗時有幫助(尤其是很少地方會使用到的一些對象)。查看 Lazy<> 接口文檔獲取更多的信息和例子代碼。
簡單說,每一個你使用 @Inject SomeClass someClass 的地方都可以替換成 @Inject Lazy<SomeClass> someClassLazy (構造方法注入也是)。然后獲取某個類的實例時必須要調用 someClassLazy.get() 。
異步對象創建
第二種選擇(它仍然只是更多的想法而不是最終的解決方案)是在后臺線程中的某處進行對象的初始化,緩存所有方法的調用并在初始化之后再回調它們。
這種方案的缺點是它必須要單獨地準備我們要包含的所有類。并且它只有在方法調用可以被執行的將來(就像任何的analytics - 在一些事件被發生之后才可以),這些對象才可能正常工作。
以下就是我們的 HeavyExternalLibrary 使用這種解決方案后的樣子:
public class HeavyLibraryWrapper { private HeavyExternalLibrary heavyExternalLibrary; private boolean isInitialized = false; ConnectableObservable<HeavyExternalLibrary> initObservable; public HeavyLibraryWrapper() { initObservable = Observable.create(new Observable.OnSubscribe<HeavyExternalLibrary>() { @Override public void call(Subscriber<? super HeavyExternalLibrary> subscriber) { HeavyLibraryWrapper.this.heavyExternalLibrary = new HeavyExternalLibrary(); HeavyLibraryWrapper.this.heavyExternalLibrary.init(); subscriber.onNext(heavyExternalLibrary); subscriber.onCompleted(); } }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).publish(); initObservable.subscribe(new SimpleObserver<HeavyExternalLibrary>() { @Override public void onNext(HeavyExternalLibrary heavyExternalLibrary) { isInitialized = true; } }); initObservable.connect(); } public void callMethod() { if (isInitialized) { HeavyExternalLibrary.callMethod(); } else { initObservable.subscribe(new SimpleObserver<HeavyExternalLibrary>() { @Override public void onNext(HeavyExternalLibrary heavyExternalLibrary) { heavyExternalLibrary.callMethod(); } }); } } }
當 HeavyLibraryWrapper 構造方法被調用,庫的初始化會在后臺線程(這里的 Schedulers.io() )中執行。在此期間,當用戶調用 callMethod() ,它會增加一個新的subscription到我們的初始化過程中。當它完成時(onNext()方法返回一個已初始化的HeavyExternalLibrary對象)被緩存的回調會被傳送到這個對象。
目前為止,這個想法還是非常簡單并且仍然是在開發之中。這里可能會引起內存泄漏(比如,我們不得不在callMethod()方法中傳入一些參數),但一般還是適用于簡單的情況下的。
還有其它方案?
性能優化的過程是非常孤獨的。但是如果你想要分享你的ideas,請在這里分享吧。
感謝你的閱讀!
代碼:
以上描述的完整代碼可見Github [repository]。
作者
Head of Mobile Development @ Azimo
[Android]使用Dagger 2依賴注入 - DI介紹(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5092083.html[Android]使用Dagger 2依賴注入 - API(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5092525.html[Android]使用Dagger 2依賴注入 - 自定義Scope(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5095426.html