顯示具有 Android 標籤的文章。 顯示所有文章
顯示具有 Android 標籤的文章。 顯示所有文章

2015年11月5日 星期四

Android StackTrace

2015/11/05 15:10~15:31

在C語言中,Log時可以使用 __func__ 及 __LINE__ 取得目前程式所執行的函式和行數,在Android中的StackTraceElement物件也提供了類似的功能,它記錄了Call stack的許多資訊,包括執行檔案名稱、程式碼行號、方法(或函數)名稱、類別名稱。

程式範例:

package com.example.getstacktrace;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         trace("test");
    }

    private void trace(String msg) {
         StackTraceElement[] stack = Thread.currentThread().getStackTrace();        
         for(int i = 0; i < stack.length; i++) {
             StackTraceElement s = stack[i];
             Log.d("Brian", "i=" + i);
             Log.d("Brian", "ClassName="+s.getClassName());
             Log.d("Brian", "FileName="+s.getFileName());
             Log.d("Brian", "MethodName="+s.getMethodName());
             Log.d("Brian", "LineName="+s.getLineNumber());      
             i ++;
         }
         Log.d("Brian ", msg);
    }

}

輸出結果:
11-05 15:25:49.748: D/Brian(6662): i=0
11-05 15:25:49.748: D/Brian(6662): ClassName=dalvik.system.VMStack
11-05 15:25:49.748: D/Brian(6662): FileName=VMStack.java
11-05 15:25:49.748: D/Brian(6662): MethodName=getThreadStackTrace
11-05 15:25:49.748: D/Brian(6662): LineNumber=-2
11-05 15:25:49.748: D/Brian(6662): i=1
11-05 15:25:49.748: D/Brian(6662): ClassName=java.lang.Thread
11-05 15:25:49.748: D/Brian(6662): FileName=Thread.java
11-05 15:25:49.748: D/Brian(6662): MethodName=getStackTrace
11-05 15:25:49.748: D/Brian(6662): LineNumber=599
11-05 15:25:49.748: D/Brian(6662): i=2
11-05 15:25:49.748: D/Brian(6662): ClassName=com.example.getstacktrace.MainActivity
11-05 15:25:49.748: D/Brian(6662): FileName=MainActivity.java
11-05 15:25:49.748: D/Brian(6662): MethodName=trace
11-05 15:25:49.748: D/Brian(6662): LineNumber=16
11-05 15:25:49.748: D/Brian(6662): i=3
11-05 15:25:49.748: D/Brian(6662): ClassName=com.example.getstacktrace.MainActivity
11-05 15:25:49.748: D/Brian(6662): FileName=MainActivity.java
11-05 15:25:49.748: D/Brian(6662): MethodName=onCreate
11-05 15:25:49.748: D/Brian(6662): LineNumber=12
11-05 15:25:49.748: D/Brian(6662): i=4
11-05 15:25:49.748: D/Brian(6662): ClassName=android.app.Activity
11-05 15:25:49.748: D/Brian(6662): FileName=Activity.java
11-05 15:25:49.748: D/Brian(6662): MethodName=performCreate
11-05 15:25:49.748: D/Brian(6662): LineNumber=5165
11-05 15:25:49.748: D/Brian(6662): i=5
11-05 15:25:49.748: D/Brian(6662): ClassName=android.app.Instrumentation
11-05 15:25:49.748: D/Brian(6662): FileName=Instrumentation.java
11-05 15:25:49.748: D/Brian(6662): MethodName=callActivityOnCreate
11-05 15:25:49.748: D/Brian(6662): LineNumber=1103
11-05 15:25:49.758: D/Brian(6662): i=6
11-05 15:25:49.758: D/Brian(6662): ClassName=android.app.ActivityThread
11-05 15:25:49.758: D/Brian(6662): FileName=ActivityThread.java
11-05 15:25:49.758: D/Brian(6662): MethodName=performLaunchActivity
11-05 15:25:49.758: D/Brian(6662): LineNumber=2416
11-05 15:25:49.758: D/Brian(6662): i=7
11-05 15:25:49.758: D/Brian(6662): ClassName=android.app.ActivityThread
11-05 15:25:49.758: D/Brian(6662): FileName=ActivityThread.java
11-05 15:25:49.758: D/Brian(6662): MethodName=handleLaunchActivity
11-05 15:25:49.758: D/Brian(6662): LineNumber=2521
11-05 15:25:49.758: D/Brian(6662): i=8
11-05 15:25:49.758: D/Brian(6662): ClassName=android.app.ActivityThread
11-05 15:25:49.758: D/Brian(6662): FileName=ActivityThread.java
11-05 15:25:49.758: D/Brian(6662): MethodName=access$600
11-05 15:25:49.758: D/Brian(6662): LineNumber=162
11-05 15:25:49.758: D/Brian(6662): i=9
11-05 15:25:49.758: D/Brian(6662): ClassName=android.app.ActivityThread$H
11-05 15:25:49.758: D/Brian(6662): FileName=ActivityThread.java
11-05 15:25:49.758: D/Brian(6662): MethodName=handleMessage
11-05 15:25:49.758: D/Brian(6662): LineNumber=1370
11-05 15:25:49.758: D/Brian(6662): i=10
11-05 15:25:49.758: D/Brian(6662): ClassName=android.os.Handler
11-05 15:25:49.758: D/Brian(6662): FileName=Handler.java
11-05 15:25:49.758: D/Brian(6662): MethodName=dispatchMessage
11-05 15:25:49.758: D/Brian(6662): LineNumber=99
11-05 15:25:49.758: D/Brian(6662): i=11
11-05 15:25:49.758: D/Brian(6662): ClassName=android.os.Looper
11-05 15:25:49.758: D/Brian(6662): FileName=Looper.java
11-05 15:25:49.758: D/Brian(6662): MethodName=loop
11-05 15:25:49.758: D/Brian(6662): LineNumber=158
11-05 15:25:49.758: D/Brian(6662): i=12
11-05 15:25:49.758: D/Brian(6662): ClassName=android.app.ActivityThread
11-05 15:25:49.758: D/Brian(6662): FileName=ActivityThread.java
11-05 15:25:49.758: D/Brian(6662): MethodName=main
11-05 15:25:49.758: D/Brian(6662): LineNumber=5777
11-05 15:25:49.758: D/Brian(6662): i=13
11-05 15:25:49.758: D/Brian(6662): ClassName=java.lang.reflect.Method
11-05 15:25:49.758: D/Brian(6662): FileName=Method.java
11-05 15:25:49.758: D/Brian(6662): MethodName=invokeNative
11-05 15:25:49.758: D/Brian(6662): LineNumber=-2
11-05 15:25:49.758: D/Brian(6662): i=14
11-05 15:25:49.758: D/Brian(6662): ClassName=java.lang.reflect.Method
11-05 15:25:49.758: D/Brian(6662): FileName=Method.java
11-05 15:25:49.758: D/Brian(6662): MethodName=invoke
11-05 15:25:49.758: D/Brian(6662): LineNumber=511
11-05 15:25:49.758: D/Brian(6662): i=15
11-05 15:25:49.758: D/Brian(6662): ClassName=com.android.internal.os.ZygoteInit$MethodAndArgsCaller
11-05 15:25:49.758: D/Brian(6662): FileName=ZygoteInit.java
11-05 15:25:49.758: D/Brian(6662): MethodName=run
11-05 15:25:49.758: D/Brian(6662): LineNumber=1083
11-05 15:25:49.758: D/Brian(6662): i=16
11-05 15:25:49.758: D/Brian(6662): ClassName=com.android.internal.os.ZygoteInit
11-05 15:25:49.758: D/Brian(6662): FileName=ZygoteInit.java
11-05 15:25:49.758: D/Brian(6662): MethodName=main
11-05 15:25:49.758: D/Brian(6662): LineNumber=850
11-05 15:25:49.758: D/Brian(6662): i=17
11-05 15:25:49.758: D/Brian(6662): ClassName=dalvik.system.NativeStart
11-05 15:25:49.758: D/Brian(6662): FileName=NativeStart.java
11-05 15:25:49.758: D/Brian(6662): MethodName=main
11-05 15:25:49.758: D/Brian(6662): LineNumber=-2
11-05 15:25:49.758: D/Brian(6662): test



挑自己需要的StackTraceElement包成Method就很好Debug了!

2015年1月3日 星期六

玩一玩Android外部記憶體

2015/1/2 14:41-15:15, 22:32-24:04

年假在家裡休息無所事事,昨天花一個下午的時間稍微試一下Android的外部記憶體,也就是在裝置中的/sdcard/目錄的使用方式。

在Android 4.4之前,只要在AndroidManifest.xml中加入android.permission.WRITE_EXTERNAL_STORAGE,就可以自由地使用外部路徑,使用API Environment.getExternalStorageDirectory()可以取得外部記憶體的路徑。約在一年前,Android作業系統大部分都使用在手機上,那時的HTC最高階的手機是HTC Sensation,同一時期的各家廠牌手機,外部記憶體通常指的就是Micro SD卡,開發App時,將檔案寫在外部記憶體很方便,能透過ADB或USB隨身碟的方式將檔案拉到PC上。

一直到了HTC機皇是HTC One X的時期,原本是外部記憶體路徑的/sdcard/,變成虛擬外部記憶體,也就是說,這個/sdcard/目錄,並不是實體Micro SD卡的路徑;而是手機內部空間虛擬成的外部記憶體,這個虛擬而成的外部記憶體一樣須加上permission後才能正常讀寫檔案,在開發階段很方便,不需要真的取準備一張實體的Micro SD卡,就可以自由地將檔案從手機拉到電腦上。

但也衍生一個問題,如果真的想把檔案存到實體的Micro SD卡中,該怎麼辦呢?目前Android也沒有提供相關的API取得實體外部路徑,但在網路上有一些非官方的方法可以取得真正SD卡路徑的,但不是每隻手機都適用。

在Android 4.4剛出來的時候,很興奮的把我的Asus Nexus 7升級,升級之後原本的程式一直當機,追查後發現是Android 4.4以後的版本,sdcard有做一層保護,App不能直接使用外部記憶體(不管是虛擬或實體外部記憶體),需透過手機的設定,才能把整隻App移到sdcard上,但還是有一個缺點,把App移動sdcard上後,只要移除該App,所有和該App關聯的檔案(包含使用該App所產生出來的所有檔案)全部都會被刪除。天殺的Google這樣改,超級麻煩,在開發階段的App,動不動就須移除程式重新安裝,產生好的假資料每安裝一次就要再建立一次,怕麻煩的我決定重回Android 4.2的懷抱,把Nexus 7先丟一旁,拿實驗室的其他手機來開發。

最近使用ES File Browser (最初它也不能再Android 4.4上正常運作),突然發現它在我的Nexus 7上竟然可以正常存取外部記憶體中的檔案,於是我好奇地重回Android 4.4的懷抱,參考這裡寫了一隻在外部記憶體上寫檔案和讀檔案的程式,檔案竟然可以操作了!操作方法跟以前一模一樣。

在Android 4.4多了一個permission:android.permission.READ_EXTERNAL_STORAGE,顧名思義就是讀檔案的權限。
寫檔案的權限已經包含讀檔案的權限,也就是說如果加入android.permission.WRITE_EXTERNAL_STORAGE,就可以不用再加入android.permission.READ_EXTERNAL_STORAGE,Google也很用心地提供了isExternalStorageWritable()和isExternalStorageReadable()兩個方法,用來判斷外部記憶體是否能讀寫。

於是我做了小實驗,觀察isExternalStorageWritable()和isExternalStorageReadable()的回傳值:

在沒有加入任何權限的情況下,
isWritable()回傳true
isReadable()回傳true
但當我執行檔案讀取或檔案寫入都會失敗,
蝦咪!?明明不能讀寫檔案還給我回傳true。

追了一下程式發現這兩個方法都是藉由Environment.getExternalStorageState()的回傳字串判斷外部記憶體的狀態是否可讀可寫,於是我又在HTC Butterfly測試,HTC Butterfly可以插實體外部記憶體(Micro SD卡),觀察實體外部記憶體在插上或拔除的時候Environment.getExternalStorageState()會不會取得其他不同的狀態,結果當然沒有這麼順利,它竟然永遠都是回傳Environment.MEDIA_MOUNTED。
PS: HTC Butterfly的"/sdcard/"目錄是屬於虛擬外部記憶體而非實體外部記憶體。

接著我決定實作看看『正確地判斷/sdcard/可讀或可寫』的方法,花了約六個小時的時間(因為嚐鮮透過Android Studio加上最近剛學的TDD實作),這邊直接給大家我完成的程式碼。

先定義一下我所個人認為的isExternalStorageReadable()和isExternalStorageWritable(),兩個方法的理想行為

isExternalStorageReadable():

在沒有加入android.permission.READ_EXTERNAL_STORAGE或android.permission.WRITE_EXTERNAL_STORAGE時,回傳false;
反之則回傳true,代表可以讀取在外部記憶體的檔案。

isExternalStorageWritable():

在沒有加入android.permission.WRITE_EXTERNAL_STORAGE時,回傳false;
反之則回傳true,代表可以寫資料在外部記憶體內。




實作內容如下(黃色部分):

----------------------------------------------------------------------------------------------------
boolean isExternalStorageReadable() {
        return Environment.getExternalStorageDirectory().canRead();
}

boolean isExternalStorageWritable() {
        boolean writable;
        final String filePath = Environment.getExternalStorageDirectory() + 
                                       "/" + "testExternalStorageWritableFile";
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(filePath);
            writable = true;
            // delete test file
            File file = new File(filePath);
            file.delete();
        } catch (FileNotFoundException e) {
            writable = false;
        } finally {
            if(fos != null)
                try {
                    fos.close();
                } catch(IOException e) {
                    e.printStackTrace();
                }
        }
        return writable;
}
----------------------------------------------------------------------------------------------------


最初的isExternalStorageWritable()使用Environment.getExternalStorageDirectory().canWrite();
但我加入android.permission.READ_EXTERNAL_STORAGE後它就回傳true了,在這個情境下要回傳false才對。

所以最後用了比較麻煩的方法:

如果沒有加入android.permission.WRITE_EXTERNAL_STORAGE,
執行new FileOutputStream(...)就會捕捉到FileNotFoundException,此時代表外部記憶體不可寫。

加入android.permission.WRITE_EXTERNAL_STORAGE後new FileOutputStream(...)不會發生例外且產生檔案,代表外部記憶體可寫,但最後必須將產生的檔案刪除。


以上是我小小的分享,祝各位新年快樂!



有疑問就去挖(一直"那些事"好煩喔,暫時讓它消失,有需要再讓它出現)