Glow Android 優化實踐

szuan621 8年前發布 | 5K 次閱讀 安卓開發 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

 

 本文由用戶 szuan621 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!