Thursday, June 30, 2011

無法讀到指定解析度的目錄的問題

今天在開發時遇到當我希望程式去依手機規格︰hdpi去跑drawable-hdpi的目錄時,
程式卻不理我們而老是讀取drawable-mdpi目錄圖檔的問題。

其實這個問題發生在這裡︰
請參閱Android SDK文件<supports-screens>
android:anyDensity
Indicates whether the application includes resources to accommodate any screen density. Older applications (before API Level 4) are assumed unable to accomodate all densities and this is "false" by default. If the application has set either minSdkVersion or targetSdkVersion to "4" or higher, the default value for this is "true". Otherwise, it is "false". You can explicitly supply your abilities here. Based on the "standard" device screen density (medium dpi), the Android framework will scale down application assets by a factor of 0.75 (low dpi screens) or scale them up by a factor of 1.5 (high dpi screens), when you don't provide alternative resources for a specifc screen density. The screen density is expressed as dots-per-inch (dpi). Android在新開一個專案時, Manifest裡的minSdkVersion預設為2, 難怪我當時一直無法讓程式去跑drawable-hdpi解析度的drawable目錄

Friday, May 27, 2011

運行CTS時的問題收集

韌體在撰寫後,
為了檢測是不是符合Google的要求,
通常都會在交給Google認證前,
做CTS(Compatibility Test Suite︰兼容度測試套件)測試。

在CTS測試前,硬體製造商須先遵從CDD(Compatibility Definition Documents︰兼容度定義文件),
然後再做CTS測試。
如果根本沒有符合CDD,那麼做再多的CTS,也完全沒用的(過不了Google認證)!
詳細的內容都可以在這裡看到。

目前我遇過的問題有︰
1.在cts_host >start --Plan Android 時,
遇到了ShellCommandUnresponsiveException。
詳細內容如下︰
Exception in thread "Thread-5" com.android.ddmlib.ShellCommandUnresponsiveException
    at com.android.ddmlib.AdbHelper.executeRemoteCommand(AdbHelper.java:408)
    at com.android.ddmlib.Device.executeShellCommand(Device.java:276)
    at com.android.cts.TestDevice$1.run(TestDevice.java:1718)

這個原因是SDK版本本身的BUG造成的,換個SDK版本即可。

Tuesday, May 24, 2011

Build Android OS

Android的骨子裡是Linux Kernel(以Linux做為核心的系統),
最近因為在研究Android的Launcher,
左右滑動、軟體清單、Widget...
這些都是Launcher底下的內容

開始要學會make(製作)自己的Android System。

主要原因是Launcher也是APK - 放在預設系統內的一隻APK,
而Google釋出的Source Code裡,Launcher用了很多Android Library以外的系統原生類別(Class),
所以必須make出一個Android系統來搭配Launcher,才看的出執行結果。

在製作自己的Android OS時,
如果遇到了︰
You are attempting to build with the incorrect version
of java.

Your version is: java version "1.6.0_22".
The correct version is: 1.5.



原來是Android Froyo(含)以下在make時,要求使用JDK5,
而非你現在可能在用的JDK6。
所以必須將系統的OS切換成JDK5。

指令為︰

$ sudo update-java-alternatives -s java-1.5.0-sun


剩下的文章等手邊案子差不多再繼續吧...XD

Wednesday, April 13, 2011

在你的Apk裡使用GoogleMap

使用GoogleMap有幾個Google要我們遵守的,一旦沒遵守會發生超多問題︰

1.SDK需使用含Google APIs [Android x.x]的,而非單純的 [Android x.x]

因為我們使用Google API s[Android 2.2]  (而非只是[Android 2.2]) ,maps.jar會直接被匯入到APIs裡,而不用自己另外匯入maps.jar

2.

 
這行要宣告在AndroidManifest.xml的區塊裡,放在別的區塊都不行



3.必須使用含有Google APIs的模擬器


4.跟Google申請APIkey
將手邊正在用的KeyStore取出MD5,跟Google申請專屬的Google Map金鑰。
並且在你的layout.xml裡宣告。
如︰

Tuesday, April 5, 2011

客製化你專屬的View - View與OnDraw()應用

文章更新時間︰2013/06/04
文章更新次數︰3

一、前提

話說人是貪婪的,
這句話用在沉浸於3C的使用者來說,
真是滿恰當的。

一開始就以這段文字做開頭,
跟今天的主題很有關係,
一般初學者剛開始學的,
大部份都是如何建立一個TextView、EditText、Button等等的元件。

想要在我們的畫面上叫出一個TextView文字框,我們有以下2種方式︰
1.在程式碼裡
TextView text = new TextView(this);
text.setText("test");
我們的Layout.addView(text);  //將TextView新增至我們的Layout
2.在xml裡宣告
<TextView
    android:layout_width="fill_parent"
    android:layout_height="warp_content"
    android:text="test"/>
於是,我們可以很順利的得到這個畫面

但是這些基礎元件用久了,我們或使用者,
開始想要搞東搞西,想要客製化

以下是這篇分享文最後會呈現的樣子︰

ㄜ...這不就是再加一行字,並且把字的大小和顏色改一改而已嗎?
有什麼好說的呢?

二、文章開始

如果你這麼想,可就大錯特錯囉!
快來看看我在layout資料夾裡面的main.xml放了些什麼︰
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
<com.test.testcustomize.LabelView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    app:text="Reddd" />
</LinearLayout>
有注意到嗎?多了
xmlns:app="http://schemas.android.com/apk/res/com.test.testcustomize"

app:text="Reddd" 
這時候你會問︰
app:text="Reddd"
這個標籤怎麼來的?

從這裡︰

只要在res/values/裡新增attrs.xml這份文件,宣告我們自訂的屬性,
然後在main.xml裡導入這份文件,
再加上之後會提到LabelView.class的一些設定,
就可以達到藍色字的效果了。

也許你會說︰
「天啊!
為什麼我們要把一個原本可以很簡單設定大小和顏色就能做到的事,
搞得那麼複雜?」
這就是我今天要說的「客製化」。

不知道你有沒有注意到,畫面中藍色的那行字,
只在main.xml裡很簡單的宣告文字內容,
app:text="Reddd"
卻呈現了藍色、而且更大的字體。

Android允許我們自訂View,
呈現我們自己想呈現的基本樣式。

技術文件說︰
如果我們想要有自己的View,
那我們就自己寫一個類並繼承View。
所以在這份文件裡,我多寫了一個LabelView來繼承View。

該類別Code如下(可快速跳過這段)︰
package com.test.testcustomize;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class LabelView extends View {
    private Paint mTextPaint;
    private String mText;
    private int mAscent;
    
    public LabelView(Context context) {
        super(context);
        initLabelView();
    }
    
    public LabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
        CharSequence s = a.getString(R.styleable.LabelView_text);
        if(s!=null){
            setText(s.toString());
        }
        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));

            int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
            if (textSize > 0) {
                setTextSize(textSize);
            }
        a.recycle();
    }

    private void initLabelView(){
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(24);
        mTextPaint.setColor(0xFF000000);
        setPadding(3,3,3,3);
    }

    private void setText(String str){
        mText = str;
        requestLayout();
        invalidate();  //每呼叫一次就會重新繪圖onDraw()
    }

      /**
     * Sets the text size for this label
     * @param size Font size
     */
    public void setTextSize(int size) {
        mTextPaint.setTextSize(size);
        requestLayout();
        invalidate();  //每呼叫一次就會重新繪圖onDraw()
    }

    /**
     * Sets the text color for this label.
     * @param color ARGB value for the text
     */
    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        invalidate(); //每呼叫一次就會重新繪圖onDraw()
    }

    /**
     * @see android.view.View#measure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * Determines the width of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        
        int specSize = MeasureSpec.getSize(measureSpec);
        
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    /**
     * Determines the height of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        Log.i("tag", "Height Ascent: "+mAscent);
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent());
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);        
        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
    }
    
}

Code很長,請聽我一一道來。

這段Code的基本骨架是這樣的︰
1.一開始,宣告2個建構式︰
//建構式1
    public LabelView(Context context) {
        super(context);
        initLabelView();  //設定文字的最初樣式
    }

    //建構式2    
    public LabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();  //設定文字的最初樣式

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
        CharSequence s = a.getString(R.styleable.LabelView_text);
        if(s!=null){
            setText(s.toString());
        }
        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));

            int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
            if (textSize > 0) {
                setTextSize(textSize);
            }

        a.recycle();
    }
技術文件裡提到,
如果我們想對我們想呈現的View初始化,
那麼我們就應該在建構式裡做基本的設定

先不看第2段建構式的龐大內容,
這2個建構式都有一個共通點︰
都呼叫了
initLabelView();
該函式的內容如下︰
private void initLabelView(){
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true); //設定反鉅齒
        mTextPaint.setTextSize(24);    //設定預設大小為24
        mTextPaint.setColor(0xFF000000); //設定預設的顏色為黑字體
        setPadding(3,3,3,3);   //設定在版面上下左右各距離3
    }
我們在輸入文字的一開始,
馬上呼叫了Paint,
並對這個我們自訂的View做了初始化.

回到第2個建構式︰
//建構式2    
    public LabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
        CharSequence s = a.getString(R.styleable.LabelView_text);
        if(s!=null){
            setText(s.toString());
        }
        setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF0000FF));

        int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
        if (textSize > 0) {
             setTextSize(textSize);
        }
      
        a.recycle();
    }
第6行︰將我們自己做的attrs.xml呼叫進來
第7行︰取出我們在main.xml裡設定的字
第8行︰如果有取到字,則呼叫setText()函式
第11行︰設定文字最後會呈現的顏色,如果在main.xml裡面沒有設定的話,
就用這裡的預設值︰0xFF0000FF,設為藍色
第13行︰設定文字大小
第18行︰讓我們自訂的attrs可以重覆被使用(因為以後也可能被用到啊!)

Android在畫出一個View時,
原理是這樣子的︰
1.不斷的呼叫我們程式裡的onMeasure()函式去量測現在要畫的View的尺寸會有多大︰
/**
     * @see android.view.View#measure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }
2.最後呼叫onDraw()繪圖

當我在試驗時,程式為了畫出藍色字體,
至少呼叫了3次的onMeasure()才呼叫onDraw()。

所以我們得知︰
系統在開始onDraw()前,
會不斷的呼叫onMeasure()去跟Layout問現在要畫的藍色字體,
範圍是多大。

所以,我們就要將尺寸的算法在這裡交待一下了︰
/**
     * 量測這個View的寬
     * @param measureSpec A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);       
        int specSize = MeasureSpec.getSize(measureSpec);
        
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }
/**
     * 量測這個View的高
     * @param measureSpec A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {

        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        Log.i("tag", "Height Ascent: "+mAscent);
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            Log.i("tag", "Height mTextPaint.descent(): "+mTextPaint.descent());
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
第6行︰一開始系統傳進來的引數measureSpec是從main.xml裡抓過來的
第9行︰在MeasureSpec.getMode()裡會抓到的尺寸模式,有以下3種
 Mode
 Value
 備註
main.xml裡相對應的設定
AT_MOST
 -2147483648
 子視圖可自行調整尺寸大小
android:layout_width或height 設為"wrap_content"時
EXACTLY     
 1073741824
 父Layout已經很明確的定義該子視圖的大小
android:layout_width或height有自訂大小或為"fill_parent"時
UNSPECIFIED
 0
 父Layout未指定大小,子視圖可自行調整。

第10行︰你在main.xml裡真實設定的尺寸。

幾次的onMeasure()呼叫完後,就跑去呼叫onDraw()開始繪圖了。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);        
        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
    }
挪,結束了。

三、結論

1.在res/values/新增一個自訂的attrs.xml
2.在main.xml裡使用他們
3.繼承View,並在
(1)建構式 裡宣告初始值
(2)在onMeasure()裡交待清楚這個自創的View應該有多大
(3)在onDraw()裡開始繪圖

這樣子,你就能為所欲為,畫出任何你預設想呈現出來的View了。

有點複雜,但搞懂了就簡單了。

備註︰
來源︰百度知道





1.基準點是baseline
2.ascent:是baseline之上至字符最高處的距離
3.descent:是baseline之下至字符最低處的距離
4.leading:是上一行字符的descent到下一行的ascent之間的距離,也就是相鄰行間的空白距離
5.top:指的是最高字符到baseline的值,即ascent的最大值
6.bottom:指最低字符到baseline的值,即descent的最大值

Sunday, April 3, 2011

Android從不會到會 - 一位從餐飲業跳到資訊業的心路歷程

相信很多想要跳進Android開發世界的初學者在入門的階段都無從學起,而這條路我也是走的挺辛苦的。現在有了一點小小的心得,希望在這裡分享給剛入門的朋友們。

我本來的職業是餐飲業的煮麵學徒兼副店,之所以會跳來這個領域
1.臺灣餐飲業人員逐漸大陸化
2.餐飲業越來越看不出我存在的優勢

後來想一想,手上那臺Windows Mobile手機真難用,
可是手機不應該僅止於此,於是跑去聯成學JAVA,也很確定要開發手機。

在聯成遇到貴人好老師(Thanks for 顧老師Piano Man),辛苦了半年左右得了一張基本的SCJP證照,
也很辛苦的進了一家公司開始,
對我而言,才是戰場的開始。

Android設計不用太深的JAVA理論,
而Google在讓開發人員跳槽進來,也設定了很低的門檻。

但對我而言,有一個很重要的障礙︰
「英文很爛。」

不知為何,對那隻Android機器人就是情有獨鍾,
就是想開發Android。

於是我開始從Gasolin的網路電子書下手。
Gasolin的這個網站,讓我從英文很爛 + 對Android什麼都不懂,變成了開始懂什麼叫Android Activity的生命週期等等的Android基本概念。經由這個網站製作BMI量測的專案的教學,建立了我對Android的基本認識。

註︰
2011-09-23
Gasolin其實有將這個電子書出成實體書,
大家還是可以多多買實體書,
最近他的寶寶剛誕生,
養家不容易的!

我跟你們說,在程式開發初期,你會遇到一堆超級奇怪的現象︰
1.Eclipse怎麼用?我在補習班學的是NetBeans。


2.什麼叫外掛?Eclipse怎麼掛Android都掛不起來?
3.為什麼專案一匯入Eclipse就是出現紅色警嘆號或XX
4.為什麼叫不出顏色
5.為什麼就是不能安裝Android SDK

總之,你會在剛踏入Android的初期遇到超多超多的問題,
但是你都得努力的爬文、不計任何代價、用任何方法,
都要去克服他們。
我覺得因為有這些過程,才能磨練出一位程設師之後在開發過程遇到困難時的耐力。

讀完了Gasolin的電子書後,接下來要如何下一步?
我遇到了盧育聖Sam的網站
Sam在帶領新人的一開始,
希望我們打好JAVA的基礎,
希望我們讀熟Android技術文件再來做開發。

這種建議不否認絕對是個好建議,
但對我這種英文很爛的人來說,
簡直是災難一場

這時候心裡想,
真的該堅持下去嗎?
這條路超辛苦的。

我相信當一個人有心的時候,
老天爺絕對不會給他絕路。
此時,我發現到,
有一個東西可以幫助英文很爛 + 對Android什麼都不懂的我,
那就是Mars的視頻

Mars的視頻是一個很棒的東西,
他幫你把你本來應該從技術文件看到的基礎知識,
用影像的方式一步一步帶你實作和講解,
他講的內容對我來說都不會很難。
(可能我當時手上已經買了6本左右的Android書了)

我覺得在學習的過程中,
有一點很重要,
就是要有一個練習的環境 + 好的工具書。

在一家Android環境的公司磨練 + 工具書,
幫我慢慢的建立起"模仿"的能力。

工具書我推薦一定要買SDK範例大全2

這是一本幫什麼都不懂的你,
開始學會模仿的書。

註︰
2011-09-23
現在又有新版了︰SDK開發範例大全3

所以,邊看書、邊打成Code、邊查英文、邊做專案、參與論譠(大陸最大的Android網站eoe),
就成了我程式學習的起初所做的事。

我從Mars的視頻裡,
真的是驚嘆他能一看技術文件變馬上翻譯成中文的能力,
我理解到如果Android技術如果要更上一層,
你的英文底子一定要有。

於是,下班後跑去補習班學英文、聽廣播、跟外國人聊天、看電影學英文…

但是,這麼做並沒有幫我看懂技術文件,
因為技術文件裡的字彙,
只是英文的其中一個領域。
在我學英文的範圍裡,是沒有學到這個領域的。

苦了我,拚了那麼久英文文件仍看不懂。

我又去找為什麼我看不懂技術文件的原因。

1.我完全沒有文法概念,所以一個句子遇到倒裝或子句時,我看不懂其意。
2.一堆生字,造成我一個字也看不下去。
3.句子與句子之間的介系詞片語,如︰for a while, in particular,..我看不懂。

於是又再報了一家補習班,專攻閱讀與文法這一塊。
補了1、2個月,效果也慢慢出來了。

但真正讓我理解我應該要怎麼面對外來語言的文章的,
是呂英沖的書籍︰你用對方法學英語了嗎。


我是在國家圖書總館無意間看到這本書的,
書裡提倡我們一般在聽英文MP3時,
都是先唸英文,再聽中文解釋。
但這個方式錯了。
因為英文是我們不熟的語言,
當我們聽到  Apple 蘋果  時,
對於外文完全不熟的我們,聽到Apple腦袋馬上打了個問號,
後來聽到蘋果的中文解釋,才知道原來剛剛那個字是蘋果,
此時,剛剛那個字怎麼發音你也忘了。
所以呂先生說,先唸英文再唸中文的教材,是FOR老外閱讀的。
我們應該先聽蘋果,再聽到Apple。否則會產生記憶斷點

他又提到語言一定是先聽,後講,後來產生文字,最後才寫。
所以,在我所要求的讀懂Android技術文件裡,
我得先會聽和講英文。

直覺告訴我呂先生心理+記憶學的經歷,能讓我在學英文的過程中,
得到更多的啟發,
於是又繼續往他的其他書籍追相關資料。

我追到外全外文學習法。

這本書提到我們是怎麼看懂一個句子的。

我看到小明在沙發睡覺。

1.看到 「我」這個字時,我們心裡會開始默唸「ㄨㄛˇ」,並跟大腦索取什麼叫「我」,
於是你得到我就是「自己」的意思。
然後,你將「我」這個字,存在你腦中的短暫記憶裡。
2.依此類推,你一一的將「看到」、「小明」、「在」、「沙發」、「睡覺」這些字放進了短暫記憶。
3.最後你看到了句點。
4.大腦開始將你短暫記憶裡儲存的「我」、「看到」、「小明」、「在」、「沙發」、「睡覺」組出一個句意。於是你知道什麼叫「我看到小明在沙發睡覺」。

在這個理解的過程中,如果你不知道什麼叫「看到」,
或者如果你的短暫記憶裡,漏記了「看到」,
而只記得「我」、「小明」、「在」、「沙發」、「睡覺」,
那這個句意是出不來的。

原來我們要看懂一個句子,我們經過了一個
心裡默唸→懂詞意→記憶→組詞成句
的過程。

英文也是如此。
An apple a day, keeps the doctor away.

你是不是也用一樣的原理,在理解這個句子呢?

搞懂了人類理解句子的原理,
長期的廣播 + 補習 + 背Android技術字 + 真正去寫過程式碼產生了開發經驗
我才終於慢慢看懂技術文件。

謝謝你看完這篇文章,
我相信你絕對是一位有耐心的人。

為了得到一個目的,
我付出了相當大的代價。

這個過程中,
我發現我不斷在做的事情是︰
1.我缺什麼?
2.該如何補足?
3.方法對嗎?
4.不斷檢視這3點。

寫到這邊,我覺得這不僅用在學Android,
也能用在學任何的東西上面。

如果你真的有心,我相信沒有事能難的倒你的。
一起加油!

下一篇︰Android開發者應有的認知

Monday, February 28, 2011

記憶體超出OutOfMemoryError

最近在開發上常常遇到
java.lang.OutOfMemoryError: bitmap size exceeds VM budget,
甚至有時候程式執行到一半時,
程式會突然重開,甚至關閉電源。

後來發現會出現這些狀況,大部份原因是因為記憶體用太兇了。
在Android裡,每new出一個實體時、做了太多的指派(Reference),
在heap裡都會占用記憶體。

據我目前所知,大部份的Android硬體裝置,
native heap都分配了16MB左右的空間,
但是隨著每臺硬體配備的不同,真正能用的記憶體大小,
又會低於16MB。
一占超出了這個使用空間,就會很容易丟出OutOfMemoryError這個錯誤訊息。

我目前遇到最常發生的時候就是在new一個圖檔Bitmap時,
Android在載入Bitmap時,每個圖點的換算是很耗記憶體的,
圖檔尺寸越大,所占的記憶體也越大
Android在繪圖時,每一個圖檔的算法是︰

圖檔的長*寬*圖檔類型

假設我今天有一張圖

該圖長︰177像素
寬︰111像素
圖檔類型︰RGB8888(每個8代表8bit,共有32bits=4Bytes)



那麼,這張圖記憶體的占用大小就會是
(177*111*4)/1024 = 76KB

我真實去試過,把原圖縮小後,記憶體占用量也真的降低了,
所以,去對圖檔做壓縮或者事先調整尺寸、甚至靈活的運用Bitmap類裡的recycle()這個回收函式,都是不錯的解決辦法。
Bitmap的使用要小心。

另外,我們常常會看到很多範例書裡,在寫ListView的Adapter時,
Adapter裡的getView()函式中,
常會有類似的以下片斷

@Override
public View getView(int position,View convertView, ViewGroup parent){
    ViewHolder holder;
    if(convertView == null){
     convertView = mInflater.inflate(R.layout.file_row, null);//告訴系統我們有自己畫的View
     holder = new ViewHolder();
     holder.text = (TextView) convertView.findViewById(R.id.text);
     holder.icon = (ImageView) convertView.findViewById(R.id.icon);

      convertView.setTag(holder);
    }else{
            holder = (ViewHolder) convertView.getTag();
    }

         holder.text.setText("test");
            .
            .
            .
      return convertView;
   }
//用來存放每個格子的資料的實體類別
private class ViewHolder{
         TextView text;
         ImageView icon;
}
      

ListView的每一個格子,都是Android系統呼叫getView()這個函式後,
去畫出來的。
getView()這個函式決定你每一格格子要畫成什麼鬼樣子(View)。

當我們要開始畫ListView時,
系統就先去看看convertView裡有沒有之前畫好的View,
想當然,一開始畫時怎麼可能會有畫好的View呢?
convertView當然會回傳null,
於是程式開始跑convertView == null 這個判斷式
告訴Android我們每一格ListView要畫成我們自訂的View: file_row(第5行)
(file_row裡我畫了ImageView、TextView等等的組合。你要讓每一個格子變成什麼樣子,就寫成什麼樣子)

而holder則是幫我們記住每個格子裡的資訊內容。

像我手上這臺Tablet一次能畫出5個View,
像剛才說的,系統會從convertView裡去找有沒有之前畫好的View,
ok,程式一次抓了5個convertView,都呈現成null,也就是之前並沒有畫好並產生出來的實體View。

於是,程式開始一口氣畫了5個View(並建立了5個holder實體,程式第6行)。
當我們看到系統將ListView畫出了5個格子並呈現給你看後,
使用者此時將手指往上滑動,ok,第1格、第2格、第3格格子陸續消失,
第6格、第7格、第8格也準備要出現了。

於是,Android又回去找convertView,
找到之前畫好的View,也new出來的holder實體,
於是,reuse,然後只是把holder裡面的內容改一改。

我們就看到第6格、第7格、第8格...又是新的資料。
但其實,那些實體是一直被reuse的。

=================================
好,有沒有想過這些View和實體為什麼要reuse?
我曾經沒有寫這個判斷式
if(convertView == null){
       .
       .
       .
      }else{
       .
       }
而把每一個getView,都寫成新的View
convertView = mInflater.inflate(R.layout.file_row, null);
     holder = new ViewHolder();
     holder.text = (TextView) convertView.findViewById(R.id.text);
     holder.icon = (ImageView) convertView.findViewById(R.id.icon);

      convertView.setTag(holder);
也就是不斷new出實體和畫新的View。
問題來了,
這就是為什麼我會遇到記憶體超出的原因。

原來Android系統,擁有著這種reuse機制。
利用有限的資源,去做無限的事。

相關文章︰
1.使用MAT(記憶體分析)工具檢查Memory Leak
2.Memory Leak經驗分享-Drawable篇
3.何謂Memory Leak(記憶體洩漏)

Sunday, January 23, 2011

Android 電量控管和效能之路

在android SDK的Dev guide裡︰

Designing for Performance單元

提到了很多攢寫程式碼需要考量到手機電量的問題
其中有幾點是需要注意的。

1.Avoid Creating Objects

  • When extracting strings from a set of input data, try to return a substring of the original data, instead of creating a copy. You will create a new String object, but it will share the char[] with the data. 叫我們少用New String而用StringBuilder
  • An array of ints is a much better than an array of Integers, but this also generalizes to the fact that two parallel arrays of ints are also a lot more efficient than an array of (int,int) objects. The same goes for any combination of primitive types.  最好避免二維陣列的使用,而使用多個一維陣列

2.Prefer Static Over Virtual

If you don't need to access an object's fields, make your method static. Invocations will be about 15%-20% faster. It's also good practice, because you can tell from the method signature that calling the method can't alter the object's state. 盡量使用Static method


3.Avoid Internal Getters/Setters

On Android, this is a bad idea. Virtual method calls are expensive, much more so than instance field lookups. It's reasonable to follow common object-oriented programming practices and have getters and setters in the public interface, but within a class you should always access fields directly.  有時候我們做Getter/Setter是為了OO,但是若在程式內部,就免了吧!

4.Use Static Final For Constants

如果有些field不會變,加上Final增加存取的速度!


5.Use Enhanced For Loop Syntax

  public void two() {
        int sum = 0;
        for (Foo a : mArray) {
            sum += a.mSplat;
        }
使用超迴圈,能產生最大的效能。

6.Avoid Enums Where You Only Need Ints

you should consider static final int constants instead.   避免使用Enum。


7.Use Floating-Point Judiciously

As a rule of thumb, floating-point is about 2x slower than integer on Android devices. This is true on a FPU-less, JIT-less G1 and a Nexus One with an FPU and the JIT. (Of course, absolute speed difference between those two devices is about 10x for arithmetic operations.)  避免使用Float。


8.Know And Use The Libraries


9.Use Native Methods Judiciously



Tuesday, January 4, 2011

AlertDialog的ListView全白問題


new AlertDialog.Builder(this)
.setAdapter(new ArrayAdapter(getApplicationContext(), android.R.layout.select_dialog_item,
new String[]{"1","2","3"}),
new DialogInterface.OnClickListener(){}
.show();
中,若介面使用android.R.layout.simple_list_item_1,會出現螢幕全白
所以必須使用紅字的介面

Wednesday, December 22, 2010

開發之氣死人之路II - Second issue in Android

繼上一篇︰Android開發之氣死人之路I後,
這是我第2起案例,一模一樣的程式碼
跑在Android手機上的結果不同
This is my second case , programming is the same, but result in two different status.


Difficult way to develop Android program.

狀況︰
我希望將訊息通知設在上方的"進行中",而非下方的通知欄位。
用了相同method函式請求,卻產出不同結果。

下一篇(完)︰Android開發者應有的認知

Friday, December 17, 2010

Android開發之氣死人之路I

一模一樣的程式碼,在手機裡執行反應居然不一樣



狀況︰
長按後的結果不同。

原因︰
每個按鈕我都有做setOnClickListener和setOnLongClickListener,
後來發現由於電容面板的問題,
有些硬體執行完onLongClickListener後,
會雞婆的幫你執行onClickListener。

實測手機︰
Nexus One VS. Vibo A688

下一篇︰開發之氣死人之路II

Monday, December 13, 2010

進程與生命週期(Process and lifecycles)

以下文章來至Application Fundamentals :
The Android system tries to maintain an application process for as long as possible, but eventually

it will need to remove old processes when memory runs low.
只要環境允許,Android系統都會嚐試著保留著應用程式的進程。但是,如果記憶體很少時,它們還是會被砍殺的。
To determine which processes to keep and which to kill, Android places each process

into an "importance hierarchy" based on the components running in it and the state of those components.
為了能夠決定哪些進程要保留、哪些進程又要砍殺,Android將每個進程放入一個叫「層級優先權」的地方做管理了。這裡面記錄著哪些元件是執行中、哪些元件是在背景執行


Processes with the lowest importance are eliminated first, then those with the next lowest, and so on. 
如果是那種低重要性的(非被使用),會被第1個踢出去,系統會繼續找誰第二、誰第三、以此類推。

There are

five levels
 in the hierarchy. The following list presents them in order of importance:

1.A foreground process
2.A visible process
3.A service process
4.A background process
5.An empty process
以上這五種進程,由高至低,排序了其進程在系統裡的優先權。

底下詳細說明這5種進程。
什麼是前景進程 (foreground process)?

  • It is running an activity that the user is interacting with (the Activity object's onResume() method has been called). - Activity正在被使用者使用的當下(onResume()事件發生時)
  • It hosts a service that's bound to the activity that the user is interacting with. - 一個activity有互動的Service
  • It has a Service object that's executing one of its lifecycle callbacks (onCreate(), onStart(), or onDestroy()). - 有一個Service物件,而且這個Service物件還執行callback回呼函式(像Service裡的onCreate()、onStart()或onDestroy()之類的)
  • It has a BroadcastReceiver object that's executing its onReceive() method. - 有一個廣播監聽物件,而且該廣播還執行了onReceive這個callback回呼函式。
Only a few foreground processes will exist at any given time. They are killed only as a last resort.
因為在同樣的時間裡,只會有少數的前景進程存在,所以它們被排到最最最最最後面,才有機會被系統砍殺。





什麼是可用進程 (visible process)?

A visible process is one that

doesn't have any foreground components, but still can affect what the user sees on screen.
可用進程沒會有任何的前景元件執行(像Activity),但是呢,它卻仍會影響使用者所看的螢幕畫面。

  • It hosts an activity that is not in the foreground, but is still visible to the user (its onPause() method has been called). This may occur, for example, if the foreground activity is a dialog that allows the previous activity to be seen behind it. - 像是我們叫出了一個畫面裡的訊息對話視窗,此時原程式的onPause()被呼叫,我們稱此時該進程為可用進程
  • It hosts a service that's bound to a visible activity. - 一個綁到可用activity中的Service

A visible process is considered extremely important and will not be killed unless doing so is required to keep all foreground processes running.
可用進程也是非常重要的,系統會盡量的讓所有這些前景進程保持運作中,除非非不得己,才會砍掉。


什麼是Service進程 (service process)?
  Although service processes are not directly tied to anything the user sees, they are generally doing things that

the user cares about (such as playing an mp3 in the background or downloading data on the network), so the system keeps them running unless there's not enough memory to retain them along with all foreground and visible processes.
雖然Service進程不是對使用者所觀看到的畫面產生直接影響的元件,但是它們一般都被拿來做一些使用者在乎的事,像是在背景播mp3啦、從網路下載檔案啦…,所以,系統在記憶體不足以前,也會如同前景進程和可用進程一樣努力的保持著它的作業。


什麼是背景進程 (background process)?
A background process is

one holding an activity that's not currently visible to the user (the Activity object's
onStop() method has been called).
背景進程是一個使用者沒有在使用狀態的activity(此時Activity的onStop()會被系統呼叫)
因為通常系統如果呼叫到程式的onStop時,幾乎可以說該應用程式已經不是使用者當下想執行了。所以Android可以很大膽的先處理掉這種進程,這麼做,一點都不影響使用者的使用體驗。


什麼是空進程 (empty process)?
An empty process is one that

doesn't hold any active application components.
The only reason to keep such a process around is
as a cache to improve startup time the next time a component needs to run in it.
 
空進程是一個沒有執行任何啟動中的應用程式元件的進程。它存在唯一的理由就是保存cache,好讓下一次啟動元件時需要的元件,可以被快速的被找到和執行。
The system often kills these processes in order to balance overall system resources between process caches and the underlying kernel caches.
這種進程最容易被砍殺,為了讓系統保持良好的循環平衡。

何謂Service

Service文件裡提到︰
A Service is an application component representing either an application's desire to perform a longer-running operation while not interacting with the user or to supply functionality for other applications to use.
Service是應用程式的其中一種元件,它意謂著當一隻APK在不需要與使用者互動時還要運作,因而存在。

Each service class must have a corresponding <service> declaration in its package's AndroidManifest.xml. Services can be started with Context.startService() and Context.bindService().
Service在使用時,必須在AndroidManifest.xml裡宣告

使用Service的方式有2:
1.Context.startService()
A facility for the application to tell the system about something it wants to be doing in the background.
Service是一個讓應用程式告訴系統想在背景做些什麼事的工具。
2.Context.bindService()
A facility for an application to expose some of its functionality to other applications.
Service是一個讓應用程式展示它內部函式功能給其它應用程式的媒介。


Note that services, like other application objects, run in the main thread of their hosting process. This means that, if your service is going to do any CPU intensive (such as MP3 playback) or blocking (such as networking) operations, it should spawn its own thread in which to do that work.
Service就像其它應用程式的物件一樣,在該程序Process的主執行緒運作著。
所以,當我們要用到像高度的CPU運算(像是播放MP3)或是一些會卡住的運算(像網路運算),那麼就應該建立出一個自己的執行緒,好讓工作能順利完成。

The IntentService class is available as a standard implementation of Service that has its own thread where it schedules its work to be done.
IntentService是一個標準實作Service的類別,它呢,擁有一個獨立的執行緒,靠這個類別,就能完成上述所提到的「獨立完成作業」。


Processes and Threads一文中提到︰

 When the first of an application's components needs to be run, Android starts a Linux process for it with a single thread of execution.



By default, all components of the application run in that process and thread.
However, you can arrange for components to run
in other processes
, and you can spawn additional threads for any process.

當應用程式的元件(指Activity、Service、BroadcastReceiver和ContentProvider)要啟動的時候,Android會去開啟Linux裡進程的單一執行緒,開始做運算處理。
預設來說,應用程式中的所有元件,都會在"那個"進程和執行緒運作。
所以,如果要制定元件在某個特定進程運作時,程式設計師可以自行決定要在哪個另外的進程或新增的執行緒下去執行應用程式。

參考網站
1.書生雜記
2.程式搖滾

Process與Thread

一個應用程式,會開啟一個Process
一個Process,可以被程式設計師開出多個Thread

同一個process內的Threads可以共享Code、Data Section及OS Resources

一個Thread 就是一個小型的 Process ,
若我們把 Process 分為兩個部份-----Threads 和 Resources,
Threads 就是這個 Process 的動態執行者(Dynamic Object)

附上Process和Thread的差別
           
The major difference between threads and processes is 
1.Threads share the address space of the process that created it; 
processes have their own address.

2.Threads have direct access to the data segment of its process; 
processes have their own copy of the data segment 
of the parent process. 

3.Threads can directly communicate with other threads of its process; 
processes must use interprocess communication to communicate with sibling processes. 

4.Threads have almost no overhead; processes have considerable overhead.

5.New threads are easily created; new processes require duplication of the parent process.

6.Threads can exercise considerable control over threads of the same process; 
processes can only exercise control over child processes. 

7.Changes to the main thread (cancellation, priority change, etc.) 
may affect the behavior of the other threads of the process; 
changes to the parent process does not affect child processes. 

何謂Task和Back Stack

撰寫時間︰2010/12/13 12:38
修改時間︰2012/07/10 10:38
修改次數︰2

一、前言
假設今天我們的APP裡要寫一個新功能︰「撰寫e-mail連絡開發者」。
該怎麼做?
我們開發人員要寫一個全新的頁面才能做到嗎?

這個答案是否定的。

Android提供了好用的Intent,
我們只要簡單的使用它,
就可以叫出系統發送e-mail的畫面,
當使用者撰寫好並發送出去後,
畫面又回到原本觸發發送e-mail的畫面。

這件事很奇怪,
發送e-mail的頁面不是我們程式撰寫的,
但是為什麼使用體驗就好像是APP裡寫出來的東西?
原來,
這一切是因為Android系統背後,藏了一個Stack(堆疊)機制。

二、文章開始
一個APP通常來說,都擁有數個Activity。
每個Activity都能夠執行個自的任務,
甚至是啟動另一個Activity。
舉例來說,當使用者開啟了e-mail軟體,
他們看到條列式的e-mail列表清單,
當使用者點擊了列表清單,
一個新的Activity又會開出來,
顯示那些e-mail的內容。

使用者在看e-mail清單列表,
又看了裡面的e-mail內容,
一次一次的舉動,
每個Activity頁面都被存進了任務容器(collection of activities),
我們稱這個容器為背景堆疊(back stack)。

當一個Activity啟動了另一個Activity之後,
新的Activity就被放進堆疊的最頂層,
而之前的Activity則被保留在堆疊中,
但是屬於暫停的狀態。

一旦Activity遭暫停(stop),
系統就會自動將該頁面的使用狀態儲存起來。
當使用者從新頁面按下返回按鈕後,
新的頁面被消毀(destroy),
而舊的頁面在此時又會被恢復(resume)。
也因此,堆疊的特性是「後進先出」。
圖1. 這張圖表顯示出每一個新的Activity是如何被放進背景堆疊中。當使用者按下返回鍵,當下的Activity就會被消毀(destroy),然後會將前一個Activity恢復(resume)給使用者。

======底下文章為2010/12/13撰寫======
To the user, it will seem as if the map viewer is part of the same application as your activity, even though it's defined in another application and runs in that application's process. Android maintains this user experience by

keeping both activities in the same task. Simply put, a task is what the user experiences as an "application." It's a group of related activities, arranged in a stack.
Task是使用者在使用Application時的User Experiences。如果今天我們的APK功能要開啟Google map,也許我們程式會做連結直接開啟MAP。但這個MAP卻不是我們寫的。但從我們的程式到展開MAP卻感覺是一體的。那是因為Google想要照顧這部份的使用者經驗。
Task寫在Stack,也就是堆疊裡。

The root activity in the stack is the one that began the task — typically, it's an activity the user selected in the application launcher.

The activity at the top of the stack is one that's currently running — the one that is the focus for user actions. When one activity starts another, the new activity is pushed on the stack; it becomes the running activity. The previous activity remains in the stack. When the user presses the BACK key, the current activity is popped from the stack, and the previous one resumes as the running activity.
Task裡放的,就是我們在使用一隻應用程式時,所有和這隻應用程式相關的Activity的存放空間。
程式會依我們最先開啟的Activity,在堆疊裡,開出一個Task(任務)空間,然後呢,它被放在Task的最頂層。如果此時又開了一個新的Activity,就會把前一個Activity往下推,而成為你眼前看到的畫面。之前那個Activity還是在堆疊裡,所以如果此時你又按了[返回]鍵,目前的新畫面,會"彈出"這個堆疊,然後把之前的Activity,利用onResume()的方式,呼叫回來你面前。

A task is a stack of activities, not a class or an element in the manifest file.
Task是一個Activity的堆疊,而非類別或任何manifest檔裡你看的見的物件。

Sunday, December 12, 2010

IntentService的功用

盧育聖的網站中,教了大家很清楚的IntentService的概念,但看了還不是很懂。
因為,這要從Thread講起(我接下來要說的,大家可以參考高煥堂-物件導向技術與執行緒模式一書找到更完整的解說)。
Android在傳遞訊息時,主要用到Looper和Message,每一個執行緒,都有一個Looper。
主畫面(UI)的Thread本身就有Looper去Handler(處理)各種訊息,但是,程式裡new出來的副Thread,當中的Looper必須被new出來,因為android沒有替副Thread做Looper。
想像一下,Looper是運轉中的果汁機,Message是柳丁,Android傳遞訊息的原理就是將Message丟進Looper,再由Handler(老闆娘)去處理看是要賣還是要自己喝。
在知道上面Thread、Looper、Message和Handler的概念後,我們再回到盧育聖的網站裡看。
IntentService繼承Service,我們先前知道,Service的幾個良好的特性︰
1.系統當發現資源不足,而砍殺現有程序時,Service是最後幾名被砍的,不會馬上被砍。
2.Service不用畫面就能做事情

所以,當我們用Intent去開啟IntentService時,就可以很放心的將要在背後做的事,放在IntentService去做,不用再擔心會被系統砍掉!

這裡還有asynctask和IntentService的差別。倒也可以順便看看。

總之,IntentService幫我們處理掉煩人的Looper、Message和Handler啦!

Service一些手札記錄

以下文章來至於Android guide

it's started by calling Context.startService() and stopped by calling Context.stopService(). It can stop itself by calling Service.stopSelf() or Service.stopSelfResult()
靠Context的method:startService()來啟動,
並靠stopService()關閉
另外,Service也能自行自殺:stopSelf()(果然具有服務精神啊!)


Only one stopService() call is needed to stop the service, no matter how many times startService() was called.
只需要呼叫一次stopService()就能關閉Service了。但是startService()愛叫幾次都行!(服務生真是周到!)

The two modes are not entirely separate. You can bind to a service that was started with startService().
開起服務生後,還能綁架他。
For example, a background music service could be started by calling startService() with an Intent object that identifies the music to play.
當叫服務生播音樂後,
Only later, possibly when the user wants to exercise some control over the player or get information about the current song, would an activity establish a connection to the service by calling bindService().
客人想要看歌曲資訊,也許這時後我們能呼叫綁架服務生。
In cases like this, stopService() will not actually stop the service until the last binding is closed. 
這時候,就算要關閉服務生,也因為被我們綁架了,他也逃不了了,除非我們解繩。

另外,服務生因為不是最重要的,所以生命短暫︰
他只有
void onCreate()  活吧!
void onStart(Intent intent)  做事了!
void onDestroy() 去死吧!
However, onStart() is called only for services started by startService().

當然,如果服務生被綁架了,事情就沒有那麼單純了!
快來看看會發生什麼事!?
 會產生3個Callback
IBinder onBind(Intent intent)
boolean onUnbind(Intent intent)
void onRebind(Intent intent)

The onBind() callback is passed the Intent object that was passed to bindService and onUnbind() is handed the intent that was passed to unbindService().
onBind()函式被拿來傳送Intent(意圖),
If the service permits the binding, onBind() returns the communications channel that clients use to interact with the service.
onBind()會回傳管道上和Service互動的資訊

keep in mind that any service, no matter how it's started, can potentially allow clients to bind to it, so any service may receive onBind() and onUnbind() calls.

因為任何的Service都有可能被綁架,所以你會看到實作Service時,都會有onBind()的出現
不管你是用bindService還是startService,總之,Service都有可能接到onBind()呼叫啦!

Tuesday, December 7, 2010

在Ubuntu10.04增加Android2.3的adb環境變數

1.終端機裡打上︰
sudo gedit '/etc/environment'
2.最後一行加上
:/media/Data/android-sdk-linux_86/platform-tools
3.重開機

adb工具在android2.3好像移到了platform-tools目錄(待查證)
(查證結果︰沒錯,果真被移至此
所以環境變數必須重新設定,才能繼續在任何目錄使用adb工具)

Monday, November 29, 2010

[作品集1]統一發票愛我

這隻是我第1隻上傳至Android Market的APK
但不是我第1隻APK
好難懂~~哈哈

v1.0.8版功能有
3種語音模式 - 美女主播 / 大奶搖搖 / 開心大媽,減少你枯燥的對獎心情
大鍵盤 - 連男生大拇指都能用(因為我量過)
懶人輸入法 - 直接打XX告訴你發票最後一碼沒中,節省你的時間
3種對獎邏輯 - 從右到左對,末三碼對,不管怎麼對,你都能對

感謝mobile01網友的熱列支持!

Wednesday, November 24, 2010

將Log輸出成txt的方法

Linux下,將檔案產生到根目錄︰
$adb logcat -d > ~/logcat.txt

如果是要顯示當前程式︰
$adb shell logcat -d > ~/logcat

如果還要顯示特定標籤
$adb shell logcat -d -s tag > ~/logcat
▲tag是我自訂的tag的標籤名稱

Windows下︰
adb logcat > %userprofile%\Desktop\logcat.txt

▲ 「>」是指令,不是終端機的目錄符號!