1. 程式人生 > >Android Weekly Notes Issue #223

Android Weekly Notes Issue #223

Android Weekly Issue #223

本期內容包括:
Offline時間戳處理; Accessibility的安全問題可能並不是個問題; 如何在單元測試和UI測試之間共享程式碼; Android中的指紋認證; 編譯時間Kotlin vs Java; MVP結合RxJava, 讓View來處理生命週期; RxJava2預覽; 記憶體洩露處理; Gradle相關等等.

ARTICLES & TUTORIALS

TrueTime是一個NTP library for Swift and Android.

其中NTP是Network Time Protocol.

作者他們有一個購物app, 但是時斷時續的網路降低了使用者體驗, 所以他們進行了離線遷移, 準備出一系列文章分享相關的想法和在此過程中學到的東西.

本文是第一篇, 關於時間.

由於在設定裡可以設定裝置的日期和時間, 所以裝置的時間並不一定是真實的時間, 我們在程式裡new Date()得到的其實是裝置時間.

關於真實時間的計算, 他們開源了TrueTime庫, Android和iOS都能用.

TrueTime如何計算真實時間的呢? 它其實是向NTP的server發了請求, 然後計算出的.

文中和庫都說明了用法.

之前有一個文章說Accessiblity存在安全隱患, 這個服務可能可以訪問到一些隱私資訊, 比如密碼.

但是這篇文章的作者覺得前一篇文章作者的解決方案不是很好.

因為當用戶開啟Accessibility許可權的時候, Android就已經給出了警告, 說明敏感資訊可能會被觀察到. 第三方的keyboard也可以訪問這些資訊, Android也是在開啟的時候給出了警告.

另外對於前一篇文章作者提出的解決方案: View.IMPORTANT_FOR_ACCESSIBILITY_NO
這樣真正有視覺障礙的那部分使用者也無法看到密碼, 可能就無法登陸了.

所以本文作者建議的解決方案是, 可以彈一個對話方塊來提醒使用者, 如果使用者允許了, 再繼續輸入.

Android的測試分兩種:

一種是Unit tests. 單元測試, 在JVM上跑.

另一種是UI測試, 需要Android裝置.

在Android Studio中對應testandroidTest資料夾.

這兩個測試資料夾之間是不共享程式碼的, 即一個資料夾裡不能訪問另一個裡面的程式碼.

但是如果我們想要共用一些程式碼, 是有辦法解決的.

首先在app/src下新建一個資料夾, 比如叫testShared. 裡面新增要共享的程式碼.

然後在app/build.gradle裡面新增這個:

android.sourceSets {
    test {
        java.srcDirs += "$projectDir/src/testShared"
    }

    androidTest {
        java.srcDirs += "$projectDir/src/testShared"
    }
}

就可以在UI測試和單元測試中共享同一份程式碼了.

作者想做的一個效果是, 在切換tab的時候, 把Toolbar, TabLayout, FloatingActionButton還有StatusBar的顏色都動畫地改變到另一個顏色.

實現很簡單, 首先用當前顏色和目標顏色建立一個ValueAnimator, 然後addUpdateListener()在更新的過程中把值set給相應的控制元件:

colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animator) {
        int color = (int) animator.getAnimatedValue();

        toolbar.setBackgroundColor(color);
        tabLayout.setBackgroundColor(color);
        floatingActionButton.setBackgroundTintList(ColorStateList.valueOf(color));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setStatusBarColor(color);
        }
    }

});
colorAnimation.start();

其中FloatingActionButton要用setBackgroundTintList().

StatusBar在21及以上才支援getWindow().setStatusBarColor(color);

其實使用者都不喜歡驗證, 因為使用者都比較懶, 不喜歡一次又一次地輸入密碼或者手勢pattern, 但是不鎖屏又不安全.

指紋驗證Fingerprint Authentication是Android M (Android 6.0, API 23)引入的. 它就是為了解決這個問題, 提升使用者體驗. 這種non-disturbing和easy的方式, 讓我們不用在安全和使用者體驗之間做出妥協.

如果你的應用需要做一些關鍵操作, 比如支付, 你需要使用者在操作前授權, 那麼指紋驗證會很有幫助.

然後作者介紹了實現的細節.

最後作者附上了自己的相關庫: fingerlock.

這是作者關於Kotlin的第三篇文章, 作者在這篇文章裡測試了Kotlin和Java的編譯時間.

Clean build with No Gradle daemon
Java編譯比Kotlin快17%.

Clean build + Gradle daemon
org.gradle.daemon=true

Java編譯比Kotlin快13%.

Incremental builds
kotlin.incremental=true

在clean build的時候, Java可能快10-15%, 但是在增量build + gradle daemon時, kotlin和Java一樣快, 甚至可能比Java更快一些.

問題:
作者舉了一個例子, 在Fragment作為View的MVP中, 如果P從service取一些資料, 然後呼叫View的顯示方法, 則還需要知道onViewCreated()是不是已經呼叫過了.

解決方案:

首先建立一個Lifecycle的BehaviorSubject, 在onViewCreated()的時候呼叫onNext(null).

把View的方法改成返回一個Observable, presenter的方法呼叫View的方法時實際上是subscribe了一下:

class ProductsFragment implements ProductsView {
  private ProductsPresenter presenter;
  //Lifecycle subject. It is BehaviourSubject because it can be subscribed after onViewCreated call.
  private final BehaviorSubject<Void> onViewCreatedSubject = BehaviorSubject.create();

  @Override
  public Observable<Void> showProducts(List<Product> productList) {
    return onViewCreatedSubject. // Wait for onViewCreated
        doOnNext(new Action1<Object>() {
          @Override
          public void call(Object o) {
            //Updates recyclerview adapter items
          }
        });
  }

  @Override
  public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    onViewCreatedSubject.onNext(null);
  }
}

Presenter:

class ProductsFragmentPresenter implements ProductsPresenter {
  private ProductsView view;

  public void loadProducts(){
    productsService.getProducts()
      .flatMap(new Func1<Object, Observable<Void>>() {
          @Override
          public Observable<Void> call(List<Product> productList) {
            //Return the view's observable to show products.
            //No need to check if the view is created!
            return view.showProducts(productList);
          }
        })
      .subscribe();
  }
}

當然這並不是一個完整的例子, 完整的例子還需要考慮onDestroyView()還有登出等情況的處理.

作者搞了一個message app來研究Android 7的新特性.

他們的應用首先需要週期性地生產一些訊息, 關於生產訊息的實現, 作者沒有用AlarmManager, 也沒有用JobScheduler(因為只支援API 21及以上), 而是選用了GCMNetworkManager.

具體實現見原文, 有詳細說明.
另: 程式碼

這只是系列文章的第一篇, 後續應該會寫更多.

作者自己的應用在Activity轉換的時候遇到了一個crash: java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 700848 bytes.

之前應用裡有相關的Warning log, 但是
Android 7 Nougat (API 24)把它作為異常丟擲來了.

產生這個問題的原因是在onSaveInstanceState()裡面存了太多資料. 作者做了一個測試, 想看看這個限制大概是多少, 大概是500K左右.

所以這裡是不應該用來儲存太多資料的, 應該只存狀態.

底下回復說每個程序都有1M的buffer來接收transactions, 但是是在沒有任何其他IPC的情況下. 所以建議儲存的狀態資料少於100K或者50K, 當然越少越好.

作者是Google的, 以前做遊戲的, 所以致力於Performances, GPU, 資料壓縮等內容.

作者關注VR, 但是VR中要提升體驗, 必定會增加影象的大小和質量.

ETC textures 是OpenGLES 3.0的一種標準格式.

編碼一個高質量的ETC2 texture會花費很多時間.
以在遊戲界最流行的壓縮工具Mali GPU Texture Compression tool為例, 作者做了實驗, 證明確實要花費很多時間(平均10分鐘)來encode一個圖.

所以作者他們開發了一個新的庫: etc2comp, 一個很快的texture encoder.

然後和之前的工具做了比較, 平均時間提高到了10秒.

後來他說的技術細節我就看不懂了. 文後還有其他影象格式(JPG, PNG, WebP)相關的文章連結.

作者舉例展示Android程式的解耦.

首先, 他展示一個高度耦合的Android程式, 然後加入Rx, 最後加入Dagger2, 從而一步一步地解耦這個專案.

專案的內容是發現Network中的Services. 這裡有官方的Training: Network Service Discovery.

最近RxJava2有了第一個Release Candidate. 所以作者在這裡先預覽一下有哪些有趣的更新和新加的功能:

Imports:
RxJava2放在了一個不同的package下:

RxJava:

compile ‘io.reactivex:rxjava:1.0.y-SNAPSHOT’

RxJava2:

compile ‘io.reactivex.rxjava2:rxjava:x.y.z’

這意味著, 你可以同時用兩個版本的庫. 如果你要完全遷移的話, 你需要把所有的import都改到新包.

Null Emissions No Longer Permitted:
不允許再發送null值了, 會直接丟擲空指標異常.

Observable.just(null); //don’t do this
subject.onNext(null); //don’t do this either

Under(Back)Pressure:

Backpressure是當Observable發射值的速度比Observer能處理的速度快時發生的.

RxJava2引入了一個新的Observable類Flowable, with backpressure support.

Single Old and New:
訂閱一個Single現在可以用這個:
SingleObserver<T>.

Hit Me Maybe One More Type:
一個新的型別叫Maybe, 它是SingleCompletable的混合體. 用來發射0或1個值.

New BackPressured Subject: Processor:
引入了一個新型別, Processor, 它是一個有backpressure support的Subject.

New Names for Function and Action:

  • Func1 -> Function
  • Func2 -> BiFunction
  • FuncN -> Function<Object[], R>
  • Func1<T, Boolean> -> Predicate<T>
  • Action0 -> Consumer
  • Action1 -> BiConsumer
  • ActionN -> Consumer<Object[]>

Subscriber is Now Disposable:

因為和Reactive-Streams的命名衝突, 所以Subscriber改名為Disposable. 它有一個.dispose()方法, 類似於Subscription.unsubscribe()方法.

onCompleted()也將變為onComplete().

Composite Subscriptions Changes:

CompositeSubscription + subscribe()-> CompositeDisposable + subscribeWith()

Blocking Calls:
RxJava2加了一些新的操作符來變非同步為同步.
.toBlocking.first() -> .blockingFirst()

Better Hooks for Plugins:
plugin系統被重寫了. 現在你可以覆寫內建schedulers返回的值了. 這樣你就可以在做單元測試的時候覆寫Schedulers.io()來返回同步的值, 甚至debug Schedulers.

Summary

目標Release日期: October 29.

這裡還有一個Library用來把RxJava1轉換到RxJava2: RxJava2Interop

這篇文章主要講解決方法:

Static Activities

錯誤原因: 把Activity存在一個靜態引用裡, Activity生命週期結束後仍然持有.

Static Views

錯誤原因: 靜態引用了View, 因為attached View引用了Activity, 所以等於間接引用了Activity.

解決方法:

  1. 使用WeakReference;
  2. 在onDestroy()裡面把引用置為null.

Inner Classes

內部類分兩種, 靜態內部類和非靜態內部類: Nested Class

錯誤原因: 在Activity裡有一個內部類(非靜態), 建立內部類的物件, 然後靜態引用之. 因為內部類持有外部類的應用, 所以會造成記憶體洩露.

解決方法:
儘量不要存static引用.

匿名內部類 AsyncTask, Handler, Thread, TimerTask

錯誤原因:

如果你不在超出生命週期的地方引用它, 匿名內部類的物件是無害的.

但是上面的這些內部類物件全都是用來產生一些執行緒的, 這些執行緒是app全域性的, 而且會引用建立它們的物件.

解決方法:

  1. 把上面的這些類改成靜態內部類, 靜態的內部類物件不會引用外部類的物件.
  2. 如果你堅持使用匿名內部類, 可以在Activity的onDestroy()裡面終止執行緒.

Sensor Manager

錯誤原因:

把Activity作為listener註冊給了系統服務, 但是在Activity生命週期結束之前沒有登出listener.

解決方法: 在生命週期結束前登出listener.

在應用release的時候, 版本號是確定的, 這沒問題. 在應用開發的時候, 如果每一個apk也有一個特定的版本號, 將會非常有幫助.

自定義Gradle Plugin:
com.android.application就是一個gradle plugin.

有三種方式可以建立gradle plugin: doc.

本文作者選擇了buildSrc的方式, 因為這很容易, 而且可以被加到repo裡, 但是這樣將依附於你的project, 不能複用.

具體程式碼見原文.

這麼做了之後, 每一次build的apk都自帶了分支資訊, Jira卡號, 或者任何你想帶的資訊.

什麼是Interactive View?
當View是可見的, 即可以和使用者互動, 即為interactive.

當你的自定義View做一些很重的工作, 比如迴圈的動畫或者loading, 或者依賴於感測器, 當這種View變為不可見時,你需要做一些工作來節約電量.

具體利用了View的這幾個回撥:

void View::onVisibilityChanged(View, int)
void View::onWindowVisibilityChanged()
void View::onAttachedToWindow()
void View::onDetachedFromWindow()

還有兩個ACTION:

Intent.ACTION_SCREEN_ON
Intent.ACTION_SCREEN_OFF

講了如何用Build Variants, 新增不同的Flavors.

1. 把你的build.gradle分成小份, 更加模組化, 用apply應用.

2. 在build file裡指明application id.

applicationId是apk最終會用的包名.
packageName是用來找程式碼中的R, 和activity/service元件的相對路徑.
如果不在build檔案裡指明applicationId可能會有一些問題.

3. 給debug版使用一個不同的applicationId.

buildTypes {
    debug {
        applicationIdSuffix ".debug"
    }
    // ...
}

好處是同一個機器上可以同時安裝debug和release版.

4. 統計build時間.

5. 配置release.

Proguard在Java層面工作, 對於資源是不管的, 只把R中的id刪了.
如果想進一步處理不用的資源, 需要加:
shrinkResources true.

6. 發現一些有用的tasks, 或者自己開發. Reddit page.

7. 把依賴的版本號抽出來.

8. 使用jcenter, 響應更快.

9. 在開發時把最小sdk設為21或以上, 會build得更快.

LIBRARIES & CODE

一個自定義View, 顯示門牌. AnimatedDoorSignView可以根據感測器進行動畫.

一個統一的錯誤處理器. 為每一種錯誤建立全域性預設的處理方式.