[乾貨] Glow Android 優化實踐 | wingjay

版權聲明:本文原創發佈於公衆號 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 端的大體結構:

281665-6a05f7e014dc6234.png

我們有四個 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) 之間,如404500
  • 網絡正常,http 請求成功,但是 server 在處理請求時出了問題,使得返回結果的 rc != 0

不同的錯誤,我們希望給用戶不同的提示,並且統計這些錯誤。

目前我們的網絡請求裏已經能夠處理第三種情況,另外兩種都在 throwable 裏面,我們可以通過判斷 throwableIOException 還是 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 MonitorHierarchy 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 相對比較簡單,不過,我們還需要在 CommunityBase-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 已經集成好了,那麼我們應該可以按如下方式在 CommunityActivityinject 一個 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 就行,我們不應該把屬於 Communityinject 放到App 裏,這樣的代碼太ugly,而且更重要的是,隨着 Community 越來越多 Activity 需要 inject ,每個 inject 都要在各個 App 裏重複加,這太煩了,也太笨了。

因此,我們採用了一個簡潔有效的方法來改進。

Community 裏創建一個 CommunityComponent,所有屬於 Communityinject 直接寫在 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);
  }
}
281665-483c8a864c09503d.png
dagger

Bingo! 至此我們已經能夠優雅簡潔地在 App 和 Library 裏同時應用依賴注入了。

關於demo

很多讀者提到想要demo,有需要的小夥伴可以先關注我的Github:https://github.com/wingjay 之後會抽空把demo上傳到Github上的。

小結

由於篇幅有限,本文暫時先從網絡層、內存優化和依賴注入方面進行講解,之後會再考慮從 Logging模塊、數據同步模塊、Deep Linking模塊、多Library的Gradle發佈管理、持續集成和崩潰監測模塊等進行講解。

謝謝!

wingjay

https://github.com/wingjay

281665-3c3f156904b69f11
wingjay

版權聲明:轉載必須得到本人授權。謝謝。

發佈了45 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章