Glow Android 优化实践

szuan621 7年前
   <p>了解 Glow 的朋友应该知道,我们主营四款 App,分别是 Eve、Glow、Nuture和Baby。作为创业公司,我们的四款 App 都处于高速开发中,平均每个 Android App 由两人负责开发,同时负责 Android 和 Server 开发,在满足 PM 各种需求的同时,我们的 session crash free 率保持不低于 99.8%,其中两款 App 接近 100%。</p>    <p>本文将对 Glow 当前 Android App 中对现有工具的探索及优化进行讲解,希望对读者有所启发。</p>    <h2><strong>整体结构概览</strong></h2>    <p>下面是 Glow Android 端的大体结构:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9772d186e03607f0c5a4b5d005fd5b6c.png"></p>    <p>我们有四个 Android App,它们共用同一个 Community 社区,最底层是 Base-Library,存放公用的模块组件,如支付模块,Logging模块等等。</p>    <p>下面,我将依次从以下几个方面进行讲解:</p>    <ul>     <li> <p>网络层优化</p> </li>     <li> <p>内存优化实践</p> </li>     <li> <p>在 App 和 Library 中集成依赖注入</p> </li>     <li> <p>etc.</p> </li>    </ul>    <h2><strong>网络层优化</strong></h2>    <p><strong>1. Retrofit2 + OkHttp3 + RxJava</strong></p>    <p>上面这套结构是目前最为流行的网络层架构,可以帮我们写出简洁而稳定的网络请求代码,比起以前复杂的异步回调、主次线程切换等代码更为易用,而且能支持 https 请求。</p>    <p>基本用法如下:</p>    <pre>  <code class="language-java">UserApi userApi = retrofit.create(UserApi.class);</code></pre>    <pre>  <code class="language-java">@Get("/{id}")  Observable<User> getUser(@Path("id") long id);</code></pre>    <pre>  <code class="language-java">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      }    });</code></pre>    <p>这只是通用做法。下面我们要根据实际情况进行优化。</p>    <p><strong>2. 封装线程切换代码</strong></p>    <p>上面的代码中可以看到,为了执行网络请求,我们会利用 RxJava 提供的 Schedulers 工具来方便切换线程。</p>    <pre>  <code class="language-java">.subscribeOn(Schedulers.io())    .observeOn(AndroidSchedulers.mainThread())</code></pre>    <p>上面的代码的作用是:让网络请求进入 io线程 执行,并将返回结果转入 UI线程 去进行渲染。</p>    <p>不过,我们 app 有非常多的网络请求,而且除了 网络请求 ,其他的 数据库操作 或者 文件读写操作 都需要一样的线程切换。因此,为了代码复用,我们利用 RxJava 提供的 Transformer 来进行封装。</p>    <pre>  <code class="language-java">// 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());      }    };  }</code></pre>    <p>然后,我们可以把网络请求代码转化为</p>    <pre>  <code class="language-java">userApi.getUser(1)    .compose(RxUtil.normalSchedulers())    .subscribe(...)</code></pre>    <p>这虽然只是很简单的改进,但能让我们的代码更简洁,更不易出错。</p>    <p><strong>3. 封装响应结果 JsonDataResponse</strong></p>    <p>我们 server 的所有返回结果都符合如下格式:</p>    <pre>  <code class="language-java">{    'rc': 0,    'data': {...},    'msg': "Successful Call"  }</code></pre>    <p>其中 rc 是自定义的结果标志,server 用来告诉我们该请求的逻辑处理是否成功(此时 rc = 0 )。 data 是这个请求需要的 json 数据。 msg 一般用来存放错误提示信息。</p>    <p>于是我们创建了一个通用类来封装所有的 Response 。</p>    <pre>  <code class="language-java">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; }  }</code></pre>    <p>于是,我们的请求变成如下:</p>    <pre>  <code class="language-java">@Get("/{id}")  Observable<JsonDataResponse<User>> getUser(@Path("id") long id);</code></pre>    <pre>  <code class="language-java">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      }    });</code></pre>    <p><strong>4. 异常处理</strong></p>    <p>上面已经能完成正常的网络请求了,但是,却还没有对错误进行处理。</p>    <p>一次网络请求中,可能发生以下几种错误:</p>    <ul>     <li> <p>没有网络</p> </li>     <li> <p>网络正常,但 http 请求失败,即 http 状态码不在 [200, 300) 之间,如 404 、 500 等</p> </li>     <li> <p>网络正常,http 请求成功,但是 server 在处理请求时出了问题,使得返回结果的 rc != 0</p> </li>    </ul>    <p>不同的错误,我们希望给用户不同的提示,并且统计这些错误。</p>    <p>目前我们的网络请求里已经能够处理第三种情况,另外两种都在 throwable 里面,我们可以通过判断 throwable 是 IOException 还是 retrofit2.HttpException 来区分这两种情况。</p>    <p>因此,我们可得到如下异常处理代码:</p>    <pre>  <code class="language-java">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(...);      }    });</code></pre>    <p><strong>5. 封装异常处理代码</strong></p>    <p>当然,我们并不想在每一个网络请求里都写上面一大段代码来处理 error ,那样太傻了。比如上面 getUser() 请求,我希望只要写 handleUser() 这个方法,至于是网络问题还是 server 自己问题我都不想每次去 handle。</p>    <p>接下来我们来封装上面两个 Action 。我们可以自定义两个 Action :</p>    <pre>  <code class="language-java">WebSuccessAction<T extends JsonDataResponse> implements Action1<T></code></pre>    <pre>  <code class="language-java">WebFailureAction implements Action1<Throwable></code></pre>    <p>其中, WebSuccessAction 用来处理一切正常(网络正常,请求正常, rc=0 )后的处理, WebFailureAction 用来统一处理上面 三种 error 。</p>    <p>实现如下:</p>    <pre>  <code class="language-java">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);  }</code></pre>    <pre>  <code class="language-java">// (rc != 0) Error  class ResponseCodeError extends RuntimeException {    public ResponseCodeError(String detailMessage) {      super(detailMessage);    }  }</code></pre>    <p>在 WebSuccessAction 里,我们把 rc != 0 这种情况转化成 ResponseCodeError 并抛出给 WebFailureAction 去统一处理。</p>    <pre>  <code class="language-java">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(...);    }  }</code></pre>    <p>有了上面两个自定义 Action 后,我们就可以把前面 getUser() 请求转化如下:</p>    <pre>  <code class="language-java">userApi.getUser(1)    .compose(RxUtil.normalSchedulers())    .subscribe(new WebSuccessAction<JsonDataResponse<User>>() {        @Override        public void onSuccess(JsonDataResponse<User> response) {          handleUser(response.getUser());        }      }, new WebFailAction())</code></pre>    <p>Bingo! 至此我们能够用非常简洁的方式来执行网络操作,而且完全不用担心异常处理。</p>    <h2><strong>内存优化实践</strong></h2>    <p>在内存优化方面,Google 官方文档里能找到非常多的学习资料,例如常见的内存泄漏、 <a href="/misc/goto?guid=4959723546268394228" rel="nofollow,noindex">b</a>itmap官方最佳实践 。而且 Android studio 里也集成了很多有效的工具如 Heap Viewer , Memory Monitor 和 Hierarchy Viewer 等等。</p>    <p>下面,本文将从其它角度出发,来对内存作进一步优化。</p>    <p>1. 当Activity关闭时,立即取消掉网络请求结果处理。</p>    <p>这一点很容易被忽略掉。大家最常用的做法是在 Activity 执行网络操作,当 Http Response 回来后直接进行UI渲染,却并不会去判断此时 Activity 是否仍然存在,即用户是否已经离开了当时的页面。</p>    <p>那么,有什么方法能够让每个网络请求都自动监听 Activity(Fragment) 的 lifecycle 事件并且当特定 lifecycle 事件发生时, 自动中断 掉网络请求的继续执行呢?</p>    <p>首先来看下我们的网络请求代码:</p>    <pre>  <code class="language-java">userApi.getUser(1)    .compose(RxUtil.normalSchedulers())    .subscribe(new WebSuccessAction<JsonDataResponse<User>>() {        @Override        public void onSuccess(JsonDataResponse<User> response) {          handleUser(response.getUser());        }      }, new WebFailAction())</code></pre>    <p>我们希望达到的是,当 Activity 进入 onStop 时立即停掉网络请求的后续处理。</p>    <p>这里我们参考了 RxLifecycle 的实现方式,之所以没有直接使用 RxLifecycle 是因为当时集成时它必须我们的 BaseActivity 继承其提供的 RxActivity ,而 RxActivity 并未继承我们需要的 AppCompatActivity (不过现在已经提供了)。因此本人只能在学习其源码后,自己重新实现一套,并做了一些改动以更符合我们自己的应用场景。</p>    <p>具体实现如下:</p>    <ul>     <li> <p>首先,我们在 BaseActivity 里,利用 RxJava 提供的 PublishSubject 把所有 lifecycle event 发送出来。</p> <pre>  <code class="language-java">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();    }  }</code></pre> </li>     <li> <p>然后,在 BaseActivity 里,提供 bindUntilEvent(LifeCycleEvent) 方法</p> <pre>  <code class="language-java">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);        }      };    }  }</code></pre> <p>这个方法可以用于每一个网络请求 Observable 中,当它监听到特定的 lifecycle event 时,就会自动让网络请求 Observable 终止掉,不会再去监听网络请求结果。</p> </li>     <li> <p>具体使用如下:</p> </li>    </ul>    <pre>  <code class="language-java">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())</code></pre>    <p>利用 .compose(bindUntilEvent(ActivityLifeCycleEvent.STOP)) 来监听 Activity 的 Stop 事件并终止 userApi.getUser(1) 的 subscription ,从而防止内存泄漏。</p>    <p>2. 图片优化实践</p>    <p>Android开发者都知道,每个app的可用内存时有限的,一旦内存占用太多或者在主线程突然请求较大内存,很有可能发生 OOM 问题。而其中,图片又是占用内存的大头,因此我们必须采取多种方法来进行优化。</p>    <p>多数情况下我们是从 server 获取一张高清图片下来,然后在内存里进行裁剪成需要的大小来进行显示。这里面存在两个问题,</p>    <p>1:假设我们只需要一张小图,而server取回来的图如果比较大,那就会浪费带宽和内存。</p>    <p>2:如果直接在主线程去为图片请求大块空间,很容易由于系统难于快速分配而 OOM;</p>    <p>比较理想的情况是:需要显示多大的图片,就向server请求多大的图片,既节省用户带宽流量,更减少内存的占用,减小 OOM 的机率。</p>    <p>为了实现 server 端的图片Resize,我们采用了 Thumbor 来提供图片 Resize 的功能。android端只需要提供一个原图片 URL 和需要的 size 信息,就可以得到一张 Resize 好的图片资源文件。具体server端实现这里就不细讲了,感兴趣的读者可以阅读官方文档。</p>    <p>这里介绍下我们在 Android 端的实现,以 Picasso 为栗子。</p>    <ul>     <li> <p>首先要引入 Square 提供的 pollexor 工具,它可以让我们更简便的创建 thumbor 的规范 URI,参考如下:</p> </li>    </ul>    <pre>  <code class="language-java">thumbor.buildImage("http://example.com/image.png")        .resize(48, 48)        .toUrl()</code></pre>    <ul>     <li> <p>然后,利用 Picasso 提供的 requestTransformer 来实时获取当前需要显示的图片的真实尺寸,同时设置图片格式为 WebP,这种格式的图片可以保持图片质量的同时具有更小的体积:</p> </li>    </ul>    <pre>  <code class="language-java">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();</code></pre>    <ul>     <li> <p>利用修改后的 picasso 对象来请求图片</p> </li>    </ul>    <pre>  <code class="language-java">picasso.load(originUrl).fit().centerCrop().into(imageView);</code></pre>    <p>利用上面这种方法,我们可以为不同的 ImageView 计算显示需要的真实尺寸,然后去请求一张尺寸匹配的图片下来,节约带宽,减小内存开销。</p>    <p>当然,在应用这种方法的时候,不要忘记考虑服务器的负载情况,毕竟这种方案意味着每张图片会被生成各种尺寸的小图缓存起来,而且Android设备分辨率不同,即使是同一个 ImageView,真实的宽高 Pixel 值也会不同,从而生成不同的小图。</p>    <h2><strong>在App和Library中集成依赖注入</strong></h2>    <p>依赖注入框架 Dagger 我们很早就开始用了,从早期的 Dagger1 到现在的 Dagger2。虽然 Dagger 本身较为陡峭的学习曲线使得不少人止步,不过一旦用过,根本停不下来。</p>    <p>如果只是在 App 里使用 Dagger 相对比较简单,不过,我们还需要在 Community 和 Base-Android 两个公用 Library 里也集成 Dagger,这就需要费点功夫了。</p>    <p>下面我来逐步讲解下我们是如何将 Dagger 同时集成进 App 和 Library 中。</p>    <p><strong>1. 在App里集成Dagger</strong></p>    <p>首先需要在 GlowApplication 里生成一个全局的 AppComponent</p>    <pre>  <code class="language-java">@Singleton  @Component(modules = AppModule.class)  public interface AppComponent {    void inject(MainActivity mainActivity);  }</code></pre>    <p>创建 AppModule</p>    <pre>  <code class="language-java">@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();    }  }</code></pre>    <p>集成进 Application</p>    <pre>  <code class="language-java">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;    }  }</code></pre>    <p>在 MainActivity 中使用 inject 一个 tool 对象</p>    <pre>  <code class="language-java">class MainActivity extends Activity {    @Inject Tool tool;      @Override    public void onCreate() {      GlowApplication.getAppComponent().inject(this);    }  }</code></pre>    <p><strong>2. 在 Library 中集成 Dagger</strong></p>    <p>(下面以公用Library:Community为例子)</p>    <p>逆向思维下,先设想应用场景:即 Dagger 已经集成好了,那么我们应该可以按如下方式在 CommunityActivity 里 inject 一个 tool 对象。</p>    <pre>  <code class="language-java">class CommunityActivity extends Activity {    @Inject Tool tool;      @Override    public void onCreate() {      GlowApplication.getAppComponent().inject(this);    }  }</code></pre>    <p>关键在于: GlowApplication.getAppComponent().inject(this); 这一句。</p>    <p>那么问题来了:</p>    <p>对于一个 Library 而言,它是无法拿到 GlowApplication 对象的,因为作为一个被别人调用的 Library,它甚至不知道这个上层 class 的存在</p>    <p>为了解决这个问题,我们在 community 里定义一个公用接口作为 中间桥梁 ,让 GlowApplication 实现这个公共接口即可。</p>    <pre>  <code class="language-java">// 在Community定义接口CommunityComponentProvider  public interface CommunityComponentProvider {    AppComponent getAppComponent();  }</code></pre>    <pre>  <code class="language-java">// 每个app的Application类都实现这个接口来提供AppComponent  class GlowApplication implements CommunityComponentProvider {    AppComponent getAppComponent() {      return appComponent;    }  }</code></pre>    <p>然后 CommunityActivity 就可以实现如下:</p>    <pre>  <code class="language-java">class CommunityActivity extends Activity {    @Inject Tool tool;      @Override    public void onCreate() {      Context applicationContext = getApplicationContext();      CommunityComponentProvider provider = (CommunityComponentProvider) applicationContext;      provider.getAppComponent().inject(this);    }  }</code></pre>    <p><strong>3. 从 AppComponent 抽离 CommunityComponent</strong></p>    <pre>  <code class="language-java">provider.getAppComponent().inject(this);</code></pre>    <p>这一句里我们已经实现前半句 provider.getAppComponent() 了,但后半句的实现呢?</p>    <p>正常情况下,我们要把</p>    <pre>  <code class="language-java">void inject(CommunityActivity communityActivity);</code></pre>    <p>放入 AppComponent 中,如下:</p>    <pre>  <code class="language-java">@Singleton  @Component(modules = AppModule.class)  public interface AppComponent {    void inject(MainActivity mainActivity);      // 加在这里    void inject(CommunityActivity communityActivity);  }</code></pre>    <p>其实这样我们就已经几乎完成了整个 Library 和 App 的依赖注入了。</p>    <p>但细心的朋友应该发现里面存在一个小问题,那就是</p>    <pre>  <code class="language-java">void inject(CommunityActivity communityActivity);</code></pre>    <p>这句代码如果放入了 App 里的 AppComponent 里,那就意味着我们也需要在另外三个 App 里的 AppComponent 都加上一句相同的代码?这样可以吗?</p>    <p>理论上当然是可行的。但是,从单一职责的角度来考虑, AppComponent 只需要负责 App 层的 inject 就行,我们不应该把属于 Community 的 inject 放到 App 里,这样的代码太ugly,而且更重要的是,随着 Community 越来越多 Activity 需要 inject ,每个 inject 都要在各个 App 里重复加,这太烦了,也太笨了。</p>    <p>因此,我们采用了一个简洁有效的方法来改进。</p>    <p>在 Community 里创建一个 CommunityComponent ,所有属于 Community 的 inject 直接写在 CommunityComponent 里,不需要 App 再去关心。与此同时,为了保持前面 provider.getAppComponent() 仍然有效,我们让 AppComponent 继承 CommunityComponent 。</p>    <p>实现代码如下:</p>    <pre>  <code class="language-java">class AppComponent extends CommunityComponent {...}</code></pre>    <p>在 Community 里</p>    <pre>  <code class="language-java">class CommunityComponent {    void inject(CommunityActivity communityActivity);  }</code></pre>    <pre>  <code class="language-java">class CommunityActivity extends Activity {    @Inject Tool tool;      @Override    public void onCreate() {      Context applicationContext = getApplicationContext();      CommunityComponentProvider provider = (CommunityComponentProvider) applicationContext;      provider.getAppComponent().inject(this);    }  }</code></pre>    <p><img src="https://simg.open-open.com/show/62a9b08447a473df3c7ea01d8c6b77f1.png"></p>    <p style="text-align:center">dagger</p>    <p>Bingo! 至此我们已经能够优雅简洁地在 App 和 Library 里同时应用依赖注入了。</p>    <h2><strong>小结</strong></h2>    <p>由于篇幅有限,本文暂时先从网络层、内存优化和依赖注入方面进行讲解,之后会再考虑从 Logging模块、数据同步模块、Deep Linking模块、多Library的Gradle发布管理、持续集成和崩溃监测模块等进行讲解。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/a8b5278cdbcd</p>    <p> </p>