2018年7月29日 星期日

Android gradle dependencies 不匹配導致 build fail

最近遇到的一個 build fail 問題是這樣的,我將原本 gradle script 中的 library 都照著 Android Studio 的提示修改為最新版之後,就出現了 build fail

Manifest merger failed : Attribute meta-data#android.support.VERSION@value value=(25.4.0) from [com.android.support:preference-v14:25.4.0] AndroidManifest.xml:25:13-35
    is also present at [com.android.support:support-v4:26.1.0] AndroidManifest.xml:28:13-35 value=(26.1.0).
    Suggestion: add 'tools:replace="android:value"' to <meta-data> element at AndroidManifest.xml:23:9-25:38 to override.

編譯器給出的錯誤訊息一向是跟天書差不多,但仔細看一下覺得應該是 support library 不匹配的問題,SO 有人說 project 中用到的 support library 都應該使用同一個版本,但我的 gradle script 中宣告的 support library 都是 v25.4 版

目標轉向懷疑是別的 dependencies library 出問題了,經過一個一個排查之後,發現問題是出在 google service library,我原本是用 8.4.0 版

com.google.android.gms:play-services-appindexing:8.4.0

後來改成了 16.0.1 版

com.google.firebase:firebase-appindexing:16.0.1

就是這個改動讓編譯出錯,照編譯器的錯誤訊息來看,猜測應該是因為最新的 16.0.1 google service library 依賴於 v26.1 版 support library,而我 project 內用的是 v25.4 版,而解決方法就是將 google service library 降為 10.0.0 就可以編譯成功了

在使用 dependencies library 的時候要注意不能無腦升級,需確定所有 library 依賴的 support library 版本都要一致才行

2018年7月28日 星期六

RecyclerView 相關知識 -- ItemDecoration 分析

RecyclerView 是 Google 2014 年提出的一個用來代替 ListView 的元件,它帶來了很多優秀的特性,但同時也移除了一些在 ListView 中很方便的功能,分隔線就是其中之一

在 ListView 中要添加分隔線是很容易的,只需要在 xml 中增加設定一些 divider 屬性即可,但在 RecyclerView 中可沒辦法用 xml 就設置好分隔線,不過 Google 也有提供替代分案讓我們使用,那就是 ItemDecoration

什麼是 ItemDecoration

ItemDecoration 就如同字面意思一樣,是一個可以讓開發者對 RecyclerView 中的 item 作修飾的工具,而它的實現方式,其實就是提供一些關鍵 API 讓開發者實作,RecyclerView 在繪製內容時會呼叫這些 API,開發者在 API 中對 RecyclerView 的畫布添加想對 item 作修飾的內容

用講的可能還是太抽象,以下就先來看 ItemDecoration 提供了哪些 API

ItemDecoration 的關鍵 API

  • onDraw(Canvas c, RecyclerView parent, State state)
  • onDrawOver(Canvas c, RecyclerView parent, State state)
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

其實主要就三個重要的 API,其中 onDraw 跟 onDrawOver 很像,兩者的差別只在於 onDraw 會在 RecyclerView 繪製 item 之前呼叫,而 onDrawOver 會在繪製 item 之後呼叫,所以如果沒有計算好距離而讓內容重疊的話,在 onDraw 內繪製的內容是會被 item 給蓋掉的

Android 預先實作的好了一些 ItemDecoration 讓開發者使用,以下用官方提供的 DividerItemDecoration 當範例來解析這些 API 的用法和意義

getItemOffsets

首先是 getItemOffsets,它的作用就是讓開發者設定你的 ItemDecoration 會占用的大小,來讓系統預先幫你保留這些空間,看一下 Android 提供的 DividerItemDecoration 裡面的實作
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
        RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    } else {
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}
RecyclerView 會對每個 item 呼叫 getItemOffsets,透過 outRect 可以設置對於該 item 你希望保留的空間,可以看到 DividerItemDecoration 在縱向列表時在 bottom 設置了 mDivider 的高度,因此系統在繪製 item 時會在每個 item 的 bottom 都多保留這個距離來讓 divider 繪製在內

來看一下 getItemOffsets 是在 RecyclerView 哪裡被呼叫
Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (!lp.mInsetsDirty) {
        return lp.mDecorInsets;
    }

    if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
        // changed/invalid items should not be updated until they are rebound.
        return lp.mDecorInsets;
    }
    final Rect insets = lp.mDecorInsets;
    insets.set(0, 0, 0, 0);
    final int decorCount = mItemDecorations.size();
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    return insets;
}
在 getItemDecorInsetsForChild 中會對每個 ItemDecoration 呼叫一次 getItemOffsets,並且將得到的上下左右偏移量加總到 child LayoutParams 的 mDecorInsets 變數中

因此,每個 item 都記錄了所有 ItemDecoration 會用到的空間大小加總,在 LayoutManager 要進行 layout 時,會把這些空間保留下來以供 ItemDecoration 使用

onDraw

在 onDraw 裡面開發者需將 ItemDecoration 繪製在 RecyclerView 畫布上,來看一下 DividerItemDecoration 的實作
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (parent.getLayoutManager() == null) {
        return;
    }
    if (mOrientation == VERTICAL) {
        drawVertical(c, parent);
    } else {
        drawHorizontal(c, parent);
    }
}

private void drawVertical(Canvas canvas, RecyclerView parent) {
    canvas.save();
    final int left;
    final int right;
    if (parent.getClipToPadding()) {
        left = parent.getPaddingLeft();
        right = parent.getWidth() - parent.getPaddingRight();
        canvas.clipRect(left, parent.getPaddingTop(), right,
                parent.getHeight() - parent.getPaddingBottom());
    } else {
        left = 0;
        right = parent.getWidth();
    }

    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        parent.getDecoratedBoundsWithMargins(child, mBounds);
        final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
        final int top = bottom - mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(canvas);
    }
    canvas.restore();
}
這邊只關注縱向列表的實作方法,首先確認 divider 的左右位置,因為對縱向列表的分隔線來說左右位置是不會變的

用 getClipToPadding 查詢 RecyclerView 是否允許繪製在 padding 的範圍中,若不允許則將 canvas 的繪製範圍用 clipRect 限縮在 padding 之內

接下來開始對每個 child 繪製它的分隔線,注意這邊的 getChildCount 並不是傳回 RecyclerView 中所有的 item 數,而是在螢幕上可見的 item 數

用 getDecoratedBoundsWithMargins 得到該 child 的邊界位置,注意這個邊界位置是已經包含了 margin 跟 ItemDecoration 的,接著考慮 View 可能有平移效果,所以再加上 TranslationY,得到的值就是 divider 的 bottom 了

這邊可能會有點奇怪為什麼得到的值是 divider bottom 而不是 top 呢?別忘了之前的 getItemOffsets,RecyclerView 已經幫我們保留了 ItemDecoration 的空間了,所以這邊的值才會是 divider 的 bottom

最後,已經得到 divider 的範圍了,就在 canvas 中繪製即可

同時使用多個 ItemDecoration

瞭解了 ItemDecoration 的概念,這邊有了個衍伸議題,RecyclerView 可不可以有多個 ItemDecoration 呢?答案是可以的,回顧上面的 getItemDecorInsetsForChild 函式就可以發現,RecyclerView 將 ItemDecoration 存在 list 中,而且在計算 mDecorInsets 時是將所有的 ItemDecoration 大小加總起來

而有多個 ItemDecoration 的話,繪製的順序也會是一個需要注意的問題,ItemDecoration 繪製的順序是照它們加入到 RecyclerView 的順序,若是有需要使用多個 ItemDecoration 時,要小心別讓後繪製的 ItemDecoration 把先前的 ItemDecoration 給蓋掉了

Reference

2018年7月25日 星期三

Visual Studio 2013 UnitTest ArgumentException

在 Visual Studio 2013 中執行 UnitTest 時遇到 ArgumentException,後來發現是因為選錯了 UnitTestFramework 的緣故

無法設定類別 TestProject1.UnitTest1 的 TestContext 屬性。錯誤:  System.ArgumentException: 類型 'Microsoft.VisualStudio.TestTools.TestTypes.Unit.UnitTestAdapterContext' 的物件無法轉換成類型 'Microsoft.VisualStudio.TestTools.UnitTesting.TestContext'。

因為我的電腦裡有裝 Visual Studio 2013 跟 Visual Studio 2015,在加入 UnitTestFramework 參考的時候有多個不同版本的 UnitTestFramework 可以選,需選擇現在使用的 Visual Studio 版本內的 UnitTestFramework

Visual Studio 2013 的版本為 12,所以應該選擇下面這個目錄下的 UnitTestFramework
 
C:\Program Files\Microsoft Visual Studio 12.0

2018年7月24日 星期二

AsyncTask 的坑 -- memory leak

AsyncTask 是 Android 提供的一個讓開發者進行耗時操作的 helper class,他讓開發者很容易的將耗時操作放到 background thread 中執行,並且可以很容易的在 UI thread 中進行畫面更新

聽起來是個很有用的東西,但可惜的是它隱藏的問題其實也不少,其中一個就是它很容易造成 memroy leak

通常使用 AsyncTask 時,都會用 non-static inner class 來使用,因為這樣可以很方便的引用 outer class 裡面的欄位,例如下面一個簡單的例子
public class MainActivity extends Activity {
    private Object mMyObject;
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    // Somewhere the AsyncTask is started
    public class MyAsyncTask extends AsyncTask<Void, Void, String> {
        @Override protected String doInBackground(Void... params) {
            // Do work
            longRunningProcess();
            return result;
        }

        @Override protected void onPostExecute(String result) {
            Log.d("Object: " + mMyObject.toString());
            Log.d("MyAsyncTask", "Received result: " + result);
        }
    }
}
但是,當你用 non-static inner class 來使用 AsyncTask 的時候,你就有可能造成 memroy leak,因為 non-static inner class 會隱式的持有對 outer 的 reference,例如在這個例子中 MyAsyncTask 就會持有 MainActivity 的 reference,若是在 longRunningProcess 函式操作途中 MainActivity 被銷毀的話,MainActivity 會因為被 MyAsyncTask 持有 reference 而無法被 GC 回收,造成 memory leak

那我們要怎樣避免 AsyncTask memory leak,就是不要使用 AsyncTask 將 AsyncTask 宣告為 static inner class,然後將外部的 activity 的 reference 當成參數傳進去,然後在 AsyncTask 內部用 WeakReference 來持有,並且在使用 activity reference 前檢查是否被回收了,如下
public class MainActivity extends Activity {
    private Object mMyObject;
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    // Somewhere the AsyncTask is started
    public static class MyAsyncTask extends AsyncTask<Void, Void, String> {
        private WeakReference<MainActivity> mActivity;

        public MyAsyncTask(MainActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

        @Override protected String doInBackground(Void... params) {
            // Do work
            longRunningProcess();
            return result;
        }

        @Override protected void onPostExecute(String result) {
            MainActivity activity = mActivity.get();
            if (activity == null || activity.isFinishing())
                return;

            Log.d("Object: " + mMyObject.toString());
            Log.d("MyAsyncTask", "Received result: " + result);
        }
    }
}
memory leak 只是 AsyncTask 的一個常見的坑之一,網路上找一找會發現它的坑還有很多,限制也不少,Android 本意是要提供一個好用方便的 class,讓開發者不用煩惱 background thread 跟 UI thread 交互作用的問題,專注於開發業務邏輯,但現在反而開發者要注意更多 AsyncTask 的坑與細節,看起來也並沒有減少多少開發者的工作量

2018年7月16日 星期一

Vivado TCL support

Vivado 是 Xilinx 公司開發的一套 IDE,可以讓 HW designer 用來設計 FPGA,照官方文件說明,在 vivado GUI 中所有可以做的動作,都可以用 TCL 來完成,因為實際上 vivado 核心就是一個 TCL 的解譯器,GUI 中的動作實際上也是轉成 TCL command 餵給 TCL 解譯器而已

Vivado 提供 interactive shell 讓人可以在 shell 中測試 TCL command,用如下指令就可進入 interactive shell

vivado -mode tcl

假如想寫一個 TCL script 直接餵給 vivado 的話,需使用如下語法

vivado -mode tcl -source script.tcl

不過最好在後面加個 -notrace 參數,否則預設會把 script 內容印出來,會讓 log 變得很雜亂

Reference

Android Storage System 和 SD Card 存取

在 Android 平台開發中對 SD Card 的存取一直讓我很困惑,Android 提出的 internal storage 跟 external storage 是甚麼意思?Android 在 Context 跟 Environment class 都提供了 method 來對 external storage 的存取,這些 method 有甚麼差別,從 Android Kitkat 提出來的 secondary external storage 又是甚麼?

對 SD Card 存取的諸多混沌不清之處,讓人在開發程式時舉步維艱,因此在這邊整理一下自己的心得與了解,希望可以釐清這些問題,從而對 Android storage system 有一個比較清楚的認識

Android 想的跟你不一樣

首先最容易讓人困惑的大概就是 Android 提出的 internal/external storage 的概念了,我想大部分的人在第一次看到這個 internal/external storage 詞彙時,應該會認為所謂的 internal storage 就是在手機中內建的在主板上的 flash 區域,因為你可以看到,連系統 UI 上都是這麼顯示的

https://commonsware.com/blog/images/2014-04-07-storage-situation-internal-storage-1.png

而 external storage 理所當然的就是外接的 SD Card 了
我一開始也是這樣想的,但很可惜,Android 想的跟你不一樣,來看看 Android 是怎麼定義 internal storage 的



可以看到 Android 認為的 internal storage 跟 UI 上顯示的 internal storage 根本不是同樣的東西,官方文件是說,internal storage 是 app 的私有儲存區域,其他的 app 無法存取這個區域,而這個區域的內的資料會在 app 被移除時一併被刪除,文件裡從頭到尾沒有提到這個區域到底是在主板內建的 flash 還是在 SD Card 上,反之是用一個抽象化的概念來描述

再來看看 external storage 的定義吧



同樣的,external storage 跟 SD Card 也沒有任何關係,external storage 是使用者將裝置插入到 PC 上時被 mount 成 external storage 的區域,並且這個區域有可能是 SD Card,但也有可能不是

看完了 Android 對 internal 和 external 的定義,應該會有一種感覺,那就是 Android 根本不想讓 app 知道現在在用的儲存區域是內建的 flash 還是 SD Card,所以才用 internal/external storage 這種抽象概念把儲存區域包裝起來,實際上 internal 可以是在 SD Card 上,而 external 也可以在內建 flash 上

不清楚 Android 為什麼要這樣做,將儲存區域抽象化感覺對開發並沒有甚麼好處,絕大部分 app 應該都想知道現在實際在用的區域到底是不是 SD Card,另外雖然說 internal 也可以在 SD Card 上,但實際上應該沒有哪家製造商會做這種事情,所以可以預設 internal 都在內建 flash 上

瞭解了 internal/external storage 的觀念之後,接下來就是要講實際開發程式了,雖然 Android 想把 storage 抽象化,但還是有方法可以讓開發者判斷使用的 storage 到底是不是在 SD Card 上面的,尤其是被廣大開發者抗議之後,開始新增了對 SD Card 支援的 API...

在 Android 不同版本中對 storage 有不同的存取設計,所以以下會依照不同 Android 版本對 SD Card 的存取方法來做說明

Before Kitkat...

事實上,在 Kitkat 之前,Android 官方是不支援 SD Card 的,所以你連 SD Card 的路徑在哪都不知道,但是上有政策下有對策,可以用隱藏 API getVolumePaths 來取得系統上所有的 storage 路徑,假如製造商沒有故意修改這個 API,那 SD Card 的路徑應該會包含在其中

只要將取得的路徑跟官方 API 取得的 external storage 路徑比對,那不是 external storage 的路徑十有八九就是 SD Card 了

而因為在這個時期 Android 還沒有正式支援 SD Card,對 SD Card 的存取也就沒有甚麼限制,只要 app 持有 WRITE_EXTERNAL_STORAGE 權限而且你找的到 SD Card 的路徑,那 app 就可以對 SD Card 無限制的存取

Kitkat

Kitkat 對 SD Card 存取相關的 app 來說是一個重要的版本,在 Kitkat 中 Android 第一次對 SD Card 有了正式的支援,並且引入了 SAF 這個新的 storage 存取 API,但是 SAF 在 Kitkat 上還是相當難用的,等到了 Lolipop 才開始能用起來

Kitkat 對 SD Card 的支援主要是加入了 secondary external storage 的概念,而相對應的 primary external storage 其實跟 Kitkat 之前版本中的 external storage 講的是一樣的東西

Kitkat 還將下列這些 API 都加入了複數的版本
  • Context.getExternalCacheDir
  • Context.getExternalFilesDir
  • Context.getObbDirs

這些 API 原本在 Kitkat 之前都只會返回一個 String,也就是 primary external storage,但新加入的複數版的 API 會將 secondary external storage 一起返回,當然前提是你的系統上有 secondary external storage

就實務上來說,這個 secondary external storage 應該就是 SD Card 沒錯了,但實際上是不是 SD Card,在這時期也沒有任何官方 API 能給你答案,別忘了 Android 其實是不想讓你知道你是不是在用 SD Card 的

既然已經正式支援 SD Card 了,Android 也開始對 SD Card 的存取權限作控制,對於用 Context.getExternalFilesDirs API 返回的 secondary external storage 路徑,app 可以自由的寫入,但是這個路徑是專屬於 app 的,通常路徑會長得像這樣 /mnt/sdcard/Android/com.your.app/,而裡面的檔案在 app 移除時也會一併刪除

而 SD Card 中除了這個專屬於你 app 的路徑之外,其他路徑基本上 Android 是限制 app 直接去寫入的,除非使用 SAF 這個新的 storage 存取 API,不過 SAF雖然是可以讓你在 SD Card 的任意位置存取檔案,但是每次存取一個不同的檔案,系統就會跳出一次視窗通知使用者,使用者需同意之後,app 才有權限存取該檔案,這種可以說是擾民的使用方式,應該是沒有開發者會接受的

雖然在 XDA 有人提供了方法讓你不需經過使用者同意就可以在其他路徑寫檔和刪檔,但是這方法未經大量驗證,而且最重要的是它不能建立新資料夾,所以實用性不高

也有人說 ES explorer 可以在 Kitkat 上建立新資料夾,不過它用了甚麼神奇的魔法就不得而知了,網路上對這時期的 Android SD Card 存取絕大部分還是會推薦用一千零一招,root

Lolipop

因為在 Kitkat 上對 SD Card 的存取受到了這麼大的限制,Google 應該是收到了廣大開發者的抗議,所以在 Lolipop 上,Android 改善了對 SD Card 存取的機制

首先是 SAF,新增了一個 Intent 來讓 app 獲得 SD Card 上某個目錄以及其下所有子目錄的存取權限,app 可以透過這個 intent 詢問使用者是否要給予權限,一旦使用者同意,app 就可以對該目錄以及所有子目錄內的檔案做存取而不用重複詢問使用者

前面提到過在 Kitkat 時期沒有官方的 API 能告訴你 secondary external storage 是不是在 SD Card 上,不過 Lolipop 在 Environment class 中新增了 API isExternalStorageRemovable 可詢問任意位置的 storage 是否是 removable,若是 removable 那就是在 SD Card 上面了

基本上到了 Lolipop 時期,對 SD Card 的存取應該可以說沒有甚麼大問題了,不過後來 Android 還在持續修改對 SD Card 存取權限的機制以及又有了新的 storage API,這就之後有空再來分析了

Reference: