<font color=" ff0000" <strong 以下內容為原創,歡迎轉載,轉載請註明來自天天博客:<http://www.cnblogs.com/tiantianbyconan/p/5098943.html </strong </font 使用Dagger 2依賴註入 圖表創建的性能 ....
以下內容為原創,歡迎轉載,轉載請註明
來自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5098943.html
使用Dagger 2依賴註入 - 圖表創建的性能
原文:http://frogermcs.github.io/dagger-graph-creation-performance/
#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
[Android]使用Dagger 2依賴註入 - 圖表創建的性能(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5098943.html
[Android]Dagger2Metrics - 測量DI圖表初始化的性能(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5193437.html