Glow Android 優化實踐
了解 Glow 的朋友應該知道,我們主營四款 App,分別是 Eve、Glow、Nuture和Baby。作為創業公司,我們的四款 App 都處于高速開發中,平均每個 Android App 由兩人負責開發,同時負責 Android 和 Server 開發,在滿足 PM 各種需求的同時,我們的 session crash free 率保持不低于 99.8%,其中兩款 App 接近 100%。
本文將對 Glow 當前 Android App 中對現有工具的探索及優化進行講解,希望對讀者有所啟發。
整體結構概覽
下面是 Glow Android 端的大體結構:
我們有四個 Android App,它們共用同一個 Community 社區,最底層是 Base-Library,存放公用的模塊組件,如支付模塊,Logging模塊等等。
下面,我將依次從以下幾個方面進行講解:
-
網絡層優化
-
內存優化實踐
-
在 App 和 Library 中集成依賴注入
-
etc.
網絡層優化
1. Retrofit2 + OkHttp3 + RxJava
上面這套結構是目前最為流行的網絡層架構,可以幫我們寫出簡潔而穩定的網絡請求代碼,比起以前復雜的異步回調、主次線程切換等代碼更為易用,而且能支持 https 請求。
基本用法如下:
UserApi userApi = retrofit.create(UserApi.class);
@Get("/{id}")
Observable<User> getUser(@Path("id") long id);
userApi.getUser(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<User>() {
@Override
public void call(User user) {
// handle user
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
// handle throwable
}
});
這只是通用做法。下面我們要根據實際情況進行優化。
2. 封裝線程切換代碼
上面的代碼中可以看到,為了執行網絡請求,我們會利用 RxJava 提供的 Schedulers 工具來方便切換線程。
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
上面的代碼的作用是:讓網絡請求進入 io線程 執行,并將返回結果轉入 UI線程 去進行渲染。
不過,我們 app 有非常多的網絡請求,而且除了 網絡請求 ,其他的 數據庫操作 或者 文件讀寫操作 都需要一樣的線程切換。因此,為了代碼復用,我們利用 RxJava 提供的 Transformer 來進行封裝。
// RxUtil.java
public static <T> Observable.Transformer<T, T> normalSchedulers() {
return new Observable.Transformer<T, T>() {
@Override
public Observable<T> call(Observable<T> source) {
return source.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
};
}
然后,我們可以把網絡請求代碼轉化為
userApi.getUser(1)
.compose(RxUtil.normalSchedulers())
.subscribe(...)
這雖然只是很簡單的改進,但能讓我們的代碼更簡潔,更不易出錯。
3. 封裝響應結果 JsonDataResponse
我們 server 的所有返回結果都符合如下格式:
{
'rc': 0,
'data': {...},
'msg': "Successful Call"
}
其中 rc 是自定義的結果標志,server 用來告訴我們該請求的邏輯處理是否成功(此時 rc = 0 )。 data 是這個請求需要的 json 數據。 msg 一般用來存放錯誤提示信息。
于是我們創建了一個通用類來封裝所有的 Response 。
public class JsonDataResponse<T> {
@SerializedName("rc")
private int rc;
@SerializedName("msg")
private String msg;
@SerializedName("data")
T data;
public int getRc() { return rc; }
public T getData() { return data; }
}
于是,我們的請求變成如下:
@Get("/{id}")
Observable<JsonDataResponse<User>> getUser(@Path("id") long id);
userApi.getUser(1)
.compose(RxUtil.normalSchedulers())
.subscribe(new Action1<JsonDataResponse<User>>() {
@Override
public void call(JsonDataResponse<User> response) {
if (response.getRc() == 0) {
User user = response.getData();
// handle user
} else {
Toast.makeToast(context, response.getMsg())
}
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
// handle throwable
}
});
4. 異常處理
上面已經能完成正常的網絡請求了,但是,卻還沒有對錯誤進行處理。
一次網絡請求中,可能發生以下幾種錯誤:
-
沒有網絡
-
網絡正常,但 http 請求失敗,即 http 狀態碼不在 [200, 300) 之間,如 404 、 500 等
-
網絡正常,http 請求成功,但是 server 在處理請求時出了問題,使得返回結果的 rc != 0
不同的錯誤,我們希望給用戶不同的提示,并且統計這些錯誤。
目前我們的網絡請求里已經能夠處理第三種情況,另外兩種都在 throwable 里面,我們可以通過判斷 throwable 是 IOException 還是 retrofit2.HttpException 來區分這兩種情況。
因此,我們可得到如下異常處理代碼:
userApi.getUser(1)
.compose(RxUtil.normalSchedulers())
.subscribe(new Action1<JsonDataResponse<User>>() {
@Override
public void call(JsonDataResponse<User> response) {
if (response.getRc() == 0) {
User user = response.getData();
// handle user
handleUser();
} else {
// such as: customized errorMsg: "cannot find this user".
Toast.makeToast(context, response.getMsg(), Toast.LENGTH_SHORT).show();
}
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
String errorMsg = "";
if (throwable instanceof IOException) {
// io Exception
errorMsg = "Please check your network status";
} else if (throwable instanceof HttpException) {
HttpException httpException = (HttpException) throwable;
// http error.
errorMsg = httpException.response();
} else {
errorMsg = "unknown error";
}
Toast.makeToast(...);
}
});
5. 封裝異常處理代碼
當然,我們并不想在每一個網絡請求里都寫上面一大段代碼來處理 error ,那樣太傻了。比如上面 getUser() 請求,我希望只要寫 handleUser() 這個方法,至于是網絡問題還是 server 自己問題我都不想每次去 handle。
接下來我們來封裝上面兩個 Action 。我們可以自定義兩個 Action :
WebSuccessAction<T extends JsonDataResponse> implements Action1<T>
WebFailureAction implements Action1<Throwable>
其中, WebSuccessAction 用來處理一切正常(網絡正常,請求正常, rc=0 )后的處理, WebFailureAction 用來統一處理上面 三種 error 。
實現如下:
class WebSuccessAction<T extends JsonDataResponse> implements Action1<T> {
@Override
public void call(T response) {
int rc = response.getRc();
if (rc != 0) {
throw new ResponseCodeError(extendedResponse.getMessage());
}
onSuccess(extendedResponse);
}
public abstract void onSuccess(T extendedResponse);
}
// (rc != 0) Error
class ResponseCodeError extends RuntimeException {
public ResponseCodeError(String detailMessage) {
super(detailMessage);
}
}
在 WebSuccessAction 里,我們把 rc != 0 這種情況轉化成 ResponseCodeError 并拋出給 WebFailureAction 去統一處理。
class WebFailAction implements Action1<Throwable> {
@Override
public void call(Throwable throwable) {
String errorMsg = "";
if (throwable instanceof IOException) {
errorMsg = "Please check your network status";
} else if (throwable instanceof HttpException) {
HttpException httpException = (HttpException) throwable;
// such as: "server internal error".
errorMsg = httpException.response();
} else {
errorMsg = "unknown error";
}
Toast.makeToast(...);
}
}
有了上面兩個自定義 Action 后,我們就可以把前面 getUser() 請求轉化如下:
userApi.getUser(1)
.compose(RxUtil.normalSchedulers())
.subscribe(new WebSuccessAction<JsonDataResponse<User>>() {
@Override
public void onSuccess(JsonDataResponse<User> response) {
handleUser(response.getUser());
}
}, new WebFailAction())
Bingo! 至此我們能夠用非常簡潔的方式來執行網絡操作,而且完全不用擔心異常處理。
內存優化實踐
在內存優化方面,Google 官方文檔里能找到非常多的學習資料,例如常見的內存泄漏、 bitmap官方最佳實踐 。而且 Android studio 里也集成了很多有效的工具如 Heap Viewer , Memory Monitor 和 Hierarchy Viewer 等等。
下面,本文將從其它角度出發,來對內存作進一步優化。
1. 當Activity關閉時,立即取消掉網絡請求結果處理。
這一點很容易被忽略掉。大家最常用的做法是在 Activity 執行網絡操作,當 Http Response 回來后直接進行UI渲染,卻并不會去判斷此時 Activity 是否仍然存在,即用戶是否已經離開了當時的頁面。
那么,有什么方法能夠讓每個網絡請求都自動監聽 Activity(Fragment) 的 lifecycle 事件并且當特定 lifecycle 事件發生時, 自動中斷 掉網絡請求的繼續執行呢?
首先來看下我們的網絡請求代碼:
userApi.getUser(1)
.compose(RxUtil.normalSchedulers())
.subscribe(new WebSuccessAction<JsonDataResponse<User>>() {
@Override
public void onSuccess(JsonDataResponse<User> response) {
handleUser(response.getUser());
}
}, new WebFailAction())
我們希望達到的是,當 Activity 進入 onStop 時立即停掉網絡請求的后續處理。
這里我們參考了 RxLifecycle 的實現方式,之所以沒有直接使用 RxLifecycle 是因為當時集成時它必須我們的 BaseActivity 繼承其提供的 RxActivity ,而 RxActivity 并未繼承我們需要的 AppCompatActivity (不過現在已經提供了)。因此本人只能在學習其源碼后,自己重新實現一套,并做了一些改動以更符合我們自己的應用場景。
具體實現如下:
-
首先,我們在 BaseActivity 里,利用 RxJava 提供的 PublishSubject 把所有 lifecycle event 發送出來。
class BaseActivity extends AppCompatActivity { protected final PublishSubject<ActivityLifeCycleEvent> lifecycleSubject = PublishSubject.create(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleSubject.onNext(ActivityLifeCycleEvent.CREATE); } @Override protected void onDestroy() { lifecycleSubject.onNext(ActivityLifeCycleEvent.DESTROY); super.onDestroy(); } @Override protected void onStop() { lifecycleSubject.onNext(ActivityLifeCycleEvent.STOP); super.onStop(); } }
-
然后,在 BaseActivity 里,提供 bindUntilEvent(LifeCycleEvent) 方法
class BaseActivity extends AppCompatActivity { @NonNull @Override public <T> Observable.Transformer<T, T> bindUntilEvent(@NonNull final ActivityLifeCycleEvent event) { return new Observable.Transformer<T, T>() { @Override public Observable<T> call(Observable<T> sourceObservable) { Observable<ActivityLifeCycleEvent> o = lifecycleSubject.takeFirst(activityLifeCycleEvent -> { return activityLifeCycleEvent.equals(event); }); return sourceObservable.takeUntil(o); } }; } }
這個方法可以用于每一個網絡請求 Observable 中,當它監聽到特定的 lifecycle event 時,就會自動讓網絡請求 Observable 終止掉,不會再去監聽網絡請求結果。
-
具體使用如下:
userApi.getUser(1)
.compose(bindUntilEvent(ActivityLifeCycleEvent.PAUSE))
.compose(RxUtil.normalSchedulers())
.subscribe(new WebSuccessAction<JsonDataResponse<User>>() {
@Override
public void onSuccess(JsonDataResponse<User> response) {
handleUser(response.getUser());
}
}, new WebFailAction())
利用 .compose(bindUntilEvent(ActivityLifeCycleEvent.STOP)) 來監聽 Activity 的 Stop 事件并終止 userApi.getUser(1) 的 subscription ,從而防止內存泄漏。
2. 圖片優化實踐
Android開發者都知道,每個app的可用內存時有限的,一旦內存占用太多或者在主線程突然請求較大內存,很有可能發生 OOM 問題。而其中,圖片又是占用內存的大頭,因此我們必須采取多種方法來進行優化。
多數情況下我們是從 server 獲取一張高清圖片下來,然后在內存里進行裁剪成需要的大小來進行顯示。這里面存在兩個問題,
1:假設我們只需要一張小圖,而server取回來的圖如果比較大,那就會浪費帶寬和內存。
2:如果直接在主線程去為圖片請求大塊空間,很容易由于系統難于快速分配而 OOM;
比較理想的情況是:需要顯示多大的圖片,就向server請求多大的圖片,既節省用戶帶寬流量,更減少內存的占用,減小 OOM 的機率。
為了實現 server 端的圖片Resize,我們采用了 Thumbor 來提供圖片 Resize 的功能。android端只需要提供一個原圖片 URL 和需要的 size 信息,就可以得到一張 Resize 好的圖片資源文件。具體server端實現這里就不細講了,感興趣的讀者可以閱讀官方文檔。
這里介紹下我們在 Android 端的實現,以 Picasso 為栗子。
-
首先要引入 Square 提供的 pollexor 工具,它可以讓我們更簡便的創建 thumbor 的規范 URI,參考如下:
thumbor.buildImage("http://example.com/image.png")
.resize(48, 48)
.toUrl()
-
然后,利用 Picasso 提供的 requestTransformer 來實時獲取當前需要顯示的圖片的真實尺寸,同時設置圖片格式為 WebP,這種格式的圖片可以保持圖片質量的同時具有更小的體積:
Picasso picasso = new Picasso.Builder(context).requestTransformer(new Picasso.RequestTransformer() {
@Override
public Request transformRequest(Request request) {
String modifiedUrl = URLEncoder.encode(originUrl);
ThumborUrlBuilder thumborUrlBuilder = thumbor.buildImage(modifiedUrl);
String url = thumborUrlBuilder.resize(request.targetWidth, request.targetHeight)
.filter(ThumborUrlBuilder.format(ThumborUrlBuilder.ImageFormat.WEBP))
.toUrl();
Timber.i("SponsorAd Image Resize url to " + url);
return request.buildUpon().setUri(Uri.parse(url)).build();
}
}).build();
-
利用修改后的 picasso 對象來請求圖片
picasso.load(originUrl).fit().centerCrop().into(imageView);
利用上面這種方法,我們可以為不同的 ImageView 計算顯示需要的真實尺寸,然后去請求一張尺寸匹配的圖片下來,節約帶寬,減小內存開銷。
當然,在應用這種方法的時候,不要忘記考慮服務器的負載情況,畢竟這種方案意味著每張圖片會被生成各種尺寸的小圖緩存起來,而且Android設備分辨率不同,即使是同一個 ImageView,真實的寬高 Pixel 值也會不同,從而生成不同的小圖。
在App和Library中集成依賴注入
依賴注入框架 Dagger 我們很早就開始用了,從早期的 Dagger1 到現在的 Dagger2。雖然 Dagger 本身較為陡峭的學習曲線使得不少人止步,不過一旦用過,根本停不下來。
如果只是在 App 里使用 Dagger 相對比較簡單,不過,我們還需要在 Community 和 Base-Android 兩個公用 Library 里也集成 Dagger,這就需要費點功夫了。
下面我來逐步講解下我們是如何將 Dagger 同時集成進 App 和 Library 中。
1. 在App里集成Dagger
首先需要在 GlowApplication 里生成一個全局的 AppComponent
@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
void inject(MainActivity mainActivity);
}
創建 AppModule
@Module
public class AppModule {
private final LexieApplication lexieApplication;
public AppModule(LexieApplication lexieApplication) {
this.lexieApplication = lexieApplication;
}
@Provides Context applicationContext() {
return lexieApplication;
}
// mock tool object
@Provides Tool provideTool() {
return new Tool();
}
}
集成進 Application
class GlowApplication extends Application {
private AppComponent appComponent;
@Override
public void onCreate() {
appComponent = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();
}
public static AppComponent getAppComponent() {
return appComponent;
}
}
在 MainActivity 中使用 inject 一個 tool 對象
class MainActivity extends Activity {
@Inject Tool tool;
@Override
public void onCreate() {
GlowApplication.getAppComponent().inject(this);
}
}
2. 在 Library 中集成 Dagger
(下面以公用Library:Community為例子)
逆向思維下,先設想應用場景:即 Dagger 已經集成好了,那么我們應該可以按如下方式在 CommunityActivity 里 inject 一個 tool 對象。
class CommunityActivity extends Activity {
@Inject Tool tool;
@Override
public void onCreate() {
GlowApplication.getAppComponent().inject(this);
}
}
關鍵在于: GlowApplication.getAppComponent().inject(this); 這一句。
那么問題來了:
對于一個 Library 而言,它是無法拿到 GlowApplication 對象的,因為作為一個被別人調用的 Library,它甚至不知道這個上層 class 的存在
為了解決這個問題,我們在 community 里定義一個公用接口作為 中間橋梁 ,讓 GlowApplication 實現這個公共接口即可。
// 在Community定義接口CommunityComponentProvider
public interface CommunityComponentProvider {
AppComponent getAppComponent();
}
// 每個app的Application類都實現這個接口來提供AppComponent
class GlowApplication implements CommunityComponentProvider {
AppComponent getAppComponent() {
return appComponent;
}
}
然后 CommunityActivity 就可以實現如下:
class CommunityActivity extends Activity {
@Inject Tool tool;
@Override
public void onCreate() {
Context applicationContext = getApplicationContext();
CommunityComponentProvider provider = (CommunityComponentProvider) applicationContext;
provider.getAppComponent().inject(this);
}
}
3. 從 AppComponent 抽離 CommunityComponent
provider.getAppComponent().inject(this);
這一句里我們已經實現前半句 provider.getAppComponent() 了,但后半句的實現呢?
正常情況下,我們要把
void inject(CommunityActivity communityActivity);
放入 AppComponent 中,如下:
@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
void inject(MainActivity mainActivity);
// 加在這里
void inject(CommunityActivity communityActivity);
}
其實這樣我們就已經幾乎完成了整個 Library 和 App 的依賴注入了。
但細心的朋友應該發現里面存在一個小問題,那就是
void inject(CommunityActivity communityActivity);
這句代碼如果放入了 App 里的 AppComponent 里,那就意味著我們也需要在另外三個 App 里的 AppComponent 都加上一句相同的代碼?這樣可以嗎?
理論上當然是可行的。但是,從單一職責的角度來考慮, AppComponent 只需要負責 App 層的 inject 就行,我們不應該把屬于 Community 的 inject 放到 App 里,這樣的代碼太ugly,而且更重要的是,隨著 Community 越來越多 Activity 需要 inject ,每個 inject 都要在各個 App 里重復加,這太煩了,也太笨了。
因此,我們采用了一個簡潔有效的方法來改進。
在 Community 里創建一個 CommunityComponent ,所有屬于 Community 的 inject 直接寫在 CommunityComponent 里,不需要 App 再去關心。與此同時,為了保持前面 provider.getAppComponent() 仍然有效,我們讓 AppComponent 繼承 CommunityComponent 。
實現代碼如下:
class AppComponent extends CommunityComponent {...}
在 Community 里
class CommunityComponent {
void inject(CommunityActivity communityActivity);
}
class CommunityActivity extends Activity {
@Inject Tool tool;
@Override
public void onCreate() {
Context applicationContext = getApplicationContext();
CommunityComponentProvider provider = (CommunityComponentProvider) applicationContext;
provider.getAppComponent().inject(this);
}
}
dagger
Bingo! 至此我們已經能夠優雅簡潔地在 App 和 Library 里同時應用依賴注入了。
小結
由于篇幅有限,本文暫時先從網絡層、內存優化和依賴注入方面進行講解,之后會再考慮從 Logging模塊、數據同步模塊、Deep Linking模塊、多Library的Gradle發布管理、持續集成和崩潰監測模塊等進行講解。
來自:http://www.jianshu.com/p/a8b5278cdbcd