版權聲明:本文原創發佈於公衆號 wingjay,轉載請務必註明出處! https://www.jianshu.com/p/a8b5278cdbcd
瞭解 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);
}
}
Bingo! 至此我們已經能夠優雅簡潔地在 App 和 Library 裏同時應用依賴注入了。
關於demo
很多讀者提到想要demo,有需要的小夥伴可以先關注我的Github:https://github.com/wingjay 之後會抽空把demo上傳到Github上的。
小結
由於篇幅有限,本文暫時先從網絡層、內存優化和依賴注入方面進行講解,之後會再考慮從 Logging模塊、數據同步模塊、Deep Linking模塊、多Library的Gradle發佈管理、持續集成和崩潰監測模塊等進行講解。
謝謝!
wingjay
版權聲明:轉載必須得到本人授權。謝謝。