2014年5月13日 星期二

Android Service之IntentService

前言:

對於Android的Service,一直以來都是一知半解的狀況,因此前陣子有小段空擋時,花點時間在Android Develop Guide看看Service的說明,意外看到了一個玩意,IntentService,對它有點好奇,有Service就好了為何還會有它?因此對這個類別研究並測試了一下。

淺談Service

Android Service其實有點複雜,要談的話可能需要多點篇幅,因此這邊稍微講一下它的大概就好。
在我的認知裡,其實就把它想成一個沒有畫面的Activity就好了,因為它沒有畫面,所以它也不會因為User切換Activity而被中斷掉,因此是一個很適合做背景工作的App Component,如放音樂、下載檔案等工作,畢竟你不可能讓User只能在某個特定頁面才能正常下載,而且還跟User說,不准動手機,等我抓完你才可以動這類的話吧。
而Android提供了方便的框架,讓開發者去使用自己寫的Service,也就是透過context.startService(Intent service),就可以對Service送出request,讓它做某些工作。
只是因為要讓Service能完成更全面的工作,因此Android官方在設計此Components時將此它弄的很彈性,相對的複雜度也提高。在看官方API時可能會對於context.bindService()和context.startService()差別在哪為何要用有所困惑,又或者對為什麼service.onStartCommand()的回傳值要傳特定的參數回去,每個參數有何意義,在什麼狀況下我的參數會發揮什麼作用諸如此類的。
所以在看IntentService時,只需知道幾件和Service有關的事情:
  1. Service並不會另外開一條process出來執行你的code,除非你另外指定,否則它會和application執行在同一條process內,所以你如果在這邊執行大量運算的工作,還是會出現ANR的訊息。
  2. Service並不是另開一條新的thread來執行你的工作,它還是在main thread底下,所以不能直接透過他執行網路存取的工作。

Why IntentService

由於上面提到的Service特性以及它的複雜度的關係,因此官方提供了一個簡單使用的Service來給開發者使用,也就是今天要提到的IntentService,IntentService跟Service的差別在於,系統會給IntentService獨立的一條worker thread,讓它不會和activity共用thread,然後它預設會有個queue的機制,會保證一次只有一個request會被執行而已,也就是說不管你呼叫幾次context.startService(intent),在同一時間內只有單一個request會在service.onHandleIntent(intent)執行,而當沒有request在queue內時,該IntentService會自動銷毀,讓你不需要管理它的命週期。這邊整理了IntentService的一些特點:
  1. Easy to use,一樣透過context.startService(Intent service)就可以送出request。
  2. 不需要管Service的生命週期,只要把主要工作的code寫在onHandleIntent(Intent intent)就好了,code執行完畢如果沒有其他request則它會自己銷毀。
  3. 保證一次只有一個request會被處理,其餘的request會被block住。
  4. 由於他是另外開一條worker thread的關係,因此request被block住也不會影響main thread,簡單來說就是不會出現ANR的訊息。
  5. 也由於是另一條thread的關係,因此你可以直接在onHandleIntent()內寫網路相關的程式。

測試範例:

先在resource裡面定一個Button:
<Button 
    android:id="@+id/btn_is"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="intent service go"/>
接著在Activity內寫他的事件,只寫了這段code,發送request出去:
btn_is = (Button) findViewById(R.id.btn_is);
btn_is.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent();
        intent.setClass(MainActivity.this, 
                    IntentServiceImpl.class);
        startService(intent);
    }
});
而主要IntentService code,在onHandleIntent()裡面會故意將Thread sleep 2秒,用以觀察request bolcking的狀況,這邊需要注意的是,要覆寫建構子回傳一個IntentServiceImpl Service的名稱給worker thread,如下::
public class IntentServiceImpl extends IntentService {
    public static final String TAG = "mayer";
    private int i = 0;

    public IntentServiceImpl() {
        super("IntentServiceImpl");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate()");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent(), start work.");
        Log.d(TAG, "print start, init i = " + i);
        for(; i< 5; i++) {
            Log.d(TAG, "i = " + i);
        }
        try {
            TimeUnit.SECONDS.sleep(2l);
        } catch (InterruptedException e) {
            Log.e(TAG, "Error:", e);
        }
        Log.d(TAG, "onHandleIntent(), end work.");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy()");
    }
}
最後,跟一般Service一樣,需要在AndroidManifest.xml加上:
<service android:name="com.test.servicetest.IntentServiceImpl" />

範例結果:

測試時對按鈕連點三下,會輸出以下結果:
01-14 11:02:43.746: onCreate()
01-14 11:02:43.761: onHandleIntent(), start work.
01-14 11:02:43.761: print start, init i = 0
01-14 11:02:43.761: i = 0
01-14 11:02:43.761: i = 1
01-14 11:02:43.761: i = 2
01-14 11:02:43.761: i = 3
01-14 11:02:43.765: i = 4
01-14 11:02:45.765: onHandleIntent(), end work.
01-14 11:02:45.769: onHandleIntent(), start work.
01-14 11:02:45.773: print start, init i = 5
01-14 11:02:47.773: onHandleIntent(), end work.
01-14 11:02:47.777: onHandleIntent(), start work.
01-14 11:02:47.781: print start, init i = 5
01-14 11:02:49.785: onHandleIntent(), end work.
01-14 11:02:49.793: onDestroy()
由於點了按鈕三次,因此是送出了三個context.startService()的request出去。可以看到測試的Intent Service只會onCreate()一次,也就代表只會有一個Intent Service的實例被建立,接著他執行onHandleIntent()裡面的程式,由於會自己幫你block住其他的request,因此可以確保一次只有一個request會在這邊被執行,第一個request執行完之後第二個在進來執行,以此類推。當第三個request執行完時,由於沒有request了,因此這個Intent Service會自己呼叫onDestroy(),將自己銷毀。

結論:

所以這能幹麻呢?舉個例子,譬如你的Client要做圖片下載,而你又想限制一次只從Server上抓取一張圖片,一張抓完之後在抓下一張,這時你就可以用IntentService來做,當要抓圖片時把intent包一包,就直接呼叫startService(intent),然後就可以達成你的需求了!

Java 8 Lambda Expressions

Java 8從 2014/3/18 正式推出至今,也一個多月了,而Eclipse也在Java 8推出後宣布Eclipse 4.3.2將會支援Java8,因此花點時間測試了一下最有興趣的新特性,也就是本篇的標題,Lambda Expressions。

環境準備:

要用Eclipse來測試Java 8,需要先準備好Java 8相關的開發工具。
  1. Java Platform (JDK) 8u5,此為Java的開發工具,目前版本為8u5。
  2. eclipse 4.3.2(kepler SR2),Eclipse要4.3.2版才會支援Java 8,由於我是單純要測試而已,因此只抓標準版的Eclipse即可。
  3. Eclipse JDT升級,才能將專案的compiler設為1.8。安裝完Eclipse 4.3.2之後,再到:
    Help -> Eclipse Marketplace,搜尋Java 8,安裝Java 8 support for Eclipse Kepler SR2

  4. 開一個新的project,並將project設定為 compiler 1.8。

做完以上的準備,可以開始測試Java 8了。

Lambda語法(Syntax)介紹:

Lambda語法架構為:
Argument ListArrow TokenBody
(int x, int y)->x+y
下面有幾個範例,來對Lambda有基本的認識:
輸入沒有任何參數,輸出2:
() -> 2;  
輸入兩個int參數,輸出兩個參數相加的結果:
(int x, int y) -> x+y;
輸入字串,在console顯示輸入字串:
(String s) -> System.out.println(s);

Functional Interfaces:

對於Lambda的語法有了了解之後,接著來看看Java 8如何使用它。Java 8引進的一個新的詞彙,Functional Interfaces,指的是只擁有單一抽象方法的介面。這詞彙並不是新概念,我們之前在開發時也常常會使用到類似的介面,如比較資料時會用到的Comparator介面,或是在跑執行緒時會用到的Runnable介面。
而只要是Functional Interfaces,就可以用Lambda的方式來實作!比如說Runnable介面,原本可以透過匿名函式的方式實作,現在可改用Lambda的方式來實作,如下:
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("run!");
    }
};

Runnable r2 = () -> System.out.println("run!"); 
再來看看Comparator介面,用匿名函式與用Lambda的實作方式:
Comparator<Integer> c1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1.compareTo(o2);
        }
    };

Comparator<Integer> c2 = (o1, o2) -> o1.compareTo(o2);
透過Lambda來實作可以讓程式可以精簡很多,並增加程式的可讀性。
另外,Java 8也引進了新的標注(Annotation),來標記介面為Functional Interface,只要該介面有兩個以上的自定義方法,則編譯器會報錯,此標注讓團隊的其他開發人員不會在此介面上加上別的方法。
@FunctionalInterface
interface Ti {
    public void test(String s);
}

java.util.function:

Java的型別需要事先定義好,程式才能進行編譯。因此為了搭配Lambda語法,Java 8提供新的介面讓人使用,這些介面統一放在java.util.function底下,每個介面都代表一個方法,根據輸入參數與輸出參數,定了多個介面出來。以下介紹幾個基本類型的介面:
  • Consumer< T >:輸入參數類型為T,沒有任何輸出。
  • Function< T, R >:輸入參數類型為T,輸出參數類型為R。
  • Predicate < T >:輸入參數類型為T,輸出一個布林值。
  • Supplier< T >:沒有任何輸入,輸出參數類型為T。
  • UnaryOperator< T >:輸入參數類型為T,輸出參數類型為T。
舉例來說,今天我需要一個接受字串作為輸入而沒有任何輸出的方法,則我可以使用Consumer這個介面來操作,如下:
Consumer<String> c = (s) -> System.out.println(s);
c.accept("hello world!"); // output hello world!

ArrayList與java.util.function:

接著要談到於Java 8的Collections中,新增了幾個方法可以搭配Lambda使用。這邊會說明ArrayList的三個方法:
  • forEach(Consumer<? super E> action)
  • removeIf(Predicate<? super E> filter)
  • replaceAll(UnaryOperator operator)
首先介紹forEach方法,就跟for each語法一樣,只是這邊更直覺。傳入參數為Consumer介面,Consumer介面為單一輸入參數,沒有任何輸出參數,下面為範例:
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);

// for each語法
for(Integer t : numbers) {
    System.out.println(t);
}

// Lambda語法
numbers.forEach((t) -> {
    System.out.println(t);
});
接著介紹removeIf方法,傳入參數為Predicate介面,此介面輸入參數類型為T,然後輸出一個布林值。也就是說,他會幫你繞行陣列的每個元素,元素會被丟到Predicate內執行,當執行結果為true,則該元素會被刪除,範例如下:
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);
System.out.println(list); // output:[7, 6, 2, 3]
list.removeIf((n) -> {
    if (n > 3) {
        return true;
    } else {
        return false;
    }
});
System.out.println(list); // output:[2, 3]
最後是replaceAll方法,傳入參數為UnaryOperator介面,此介面輸入參數類型和輸出參數類型是一樣的,這邊運作的方法是,回傳的參數會取代掉輸入的參數,範例如下:
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);
System.out.println(list); // output:[7, 6, 2, 3]
list.replaceAll((t) -> t + 1);
System.out.println(list); // output:[8, 7, 3, 4]

Method and Constructor References:

在Java 8,你可以將將方法的參考傳給變數,摘錄JSR335的內容:
Examples of method and constructor references:
System::getProperty
"abc"::length
String::length
super::toString
ArrayList::new
int[]::new
說明一下上面的例子:
  1. 如果你要需要靜態方法的參考,則可以使用 類別名稱::靜態方法名稱 來得到參考。
  2. 如果你是需要某個實例物件的方法他的參考,則可以用 實例變數::方法名稱 來得到參考。
  3. 如果你需要得到的是建構式,則可以使用 類別名稱::new 來得到參考。
下面為一個簡單的範例,透過Lambda實作一個介面,之後將介面的方法傳給變數,在用此變數呼叫該方法。
public static void main(String[] args) {
    TestInterface ti = (i) -> (i > 5) ? true : false;
    Predicate<Integer> p = ti::isGreaterThenFive;

    System.out.println(p.test(6));
}

@FunctionalInterface
interface TestInterface {
    public boolean isGreaterThenFive(Integer i);
}

參考:

Singleton的雙重檢查鎖與volatile

前言:

幾年前從踏進java開始,熟悉了物件導向之後,偶然從朋友那邊聽到了一個詞,Design Pattern,因此買了本歐萊禮的Headfirst Design Pattern來看,其中對於Singleton這Pattern一直有特別印象,因為這Pattern在系統設計時,放置系統設定檔案資料時很常用,也是一個好入門的Pattern,只是對於書上的雙重檢查鎖範例,一直沒有去理解它,趁最近比較有空時,花點時間去搞懂它。

Singleton雙重檢查鎖技巧:

先看一下書上的範例:
public class Singleton {
    private volatile static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) {
            synchronized(Singleton.class) {
                if(uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
這毫無疑問的是個Singleton的Pattern,建構子設為private,因此不能用new Singleton()的方式來建立instance,而其他物件要來取此物件的instance,也只能透過它對外開放的getInstance()來取得;為了講求效能,因此只有在uniqueInstance尚未初始化,也就是為null時,才會用synchronized來避免多執行緒造成new過多物件的問題發生,而如果它已經初始化過了,則直接回傳物件回去。
看起來是個很簡單很好懂的範例,但這範例裡面有個讓我困惑很久的地方就是,為什麼要在uniqueInstance這個field上加個volatile的關鍵字?

關鍵字volatile:

一開始我對valotitle的理解大概是這樣。
如果我今天宣告了一個宣告了一個long變數為volatile,如下
public volatile long value = 0l;
則其語意會相等於(注意是語意,不是底層真正的實作方式):
public long value = 0l;
public synchronized void set(long v) {
    value = v;
}

public synchronized long get() {
    return value;
}
也就是說,把一個有可能會被中斷的操作,變為不可中斷的操作,看起來似乎不加關鍵字volatile是對Singleton沒啥影響,那為何還要加呢?於是我花了一些時間尋找了幾篇相關討論的文章。

問題1,物件可能尚未初始化完成:

在這個網站The "Double-Checked Locking is Broken" Declaration提到,在不同系統下的compiles,可能會對於指令的處理有所不同,以下是網站裡提到的測試程式片段:
singletons[i].reference = new Singleton();
網站裡提到,Paul Jakubik發現把這程式碼片段程式在Symantec JIT執行過後,它會先對物件做memory allocate,然後才去執行建構子
這會造成什麼問題呢?讓我們在回過頭來看Singleton的程式碼:
public static Singleton getInstance() {
    if(uniqueInstance == null) {
        synchronized(Singleton.class) {
            if(uniqueInstance == null) {
                uniqueInstance = new Singleton();
            }
        }
    }
    return uniqueInstance;
}
上面問題在於,一開始我們判斷物件是不是為null,null代表物件尚未初始化過,接著我們而在Symantec JIT的情況下,會先做memory allocate,在去執行建構子,因此可能有某個Thread正在對物件做初始化,做到一半被中斷了,而第二個物件呼叫了getInstance(),然後發現該物件不為null,因此馬上就回傳回去,因此它就得到了一個尚未初始化完成的物件,這有可能會造成系統錯誤。

問題2,編譯器優化:

接著另一本書提到的Effective Java (2nd Edition)裡面的 Item66 Synchronize access to shared mutable data 提到的範例,編譯器可能會對你的程式碼進行優化,如以下的程式碼:
public class HoistingTestClass {
    private static boolean stopRequested;

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!stopRequested)
                    i++;
            }
        });

        t.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            stopRequested = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
程式裡面,宣告一個Thread,並且先讓它執行,Thread裡面只做一件事,就是判斷stopRequested這個flag,如果為false,就一直執行下去,而主thread會在停止1秒之後,把stopRequested這個flag設為true。預期中,這程式應該是會跑一秒之後就停止了,不過實際卻是無窮迴圈。原因在於jvm對它做了優化處理。所以原本code是:
while(!stopRequested)
    i++;
在jvm執行時會被處理成:
if(!stopRequested)
    while(true)
        i++;
因此會和預期中的不一樣。而這問題要怎麼處理,只要將變數加上volatile則一切就正常了。
private static volatile boolean stopRequested;
為何用volatile可以解決呢?因為用了volatile,編譯器會已確保這個值的正確做為主要策略,而不會已效能為主來優化程式,因此它可以讓被設為volatile的field在任何Thread去讀取時,都會看到最新的狀態。

問題3,指令的重新排序:

在這篇文章 JSR 133 (Java Memory Model) FAQ 提到,執行程式時為了提高效率,因此compiler和processor可以對指令做重新排序的動作,比如說這段code:
class VolatileExample {
    int x = 0;
    boolean v = false;

    public void writer() {
        x = 42;
        v = true;
      }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}
其中compile會認為在writer()內的這段code
public void writer() {
    x = 42;
    v = true;
}
和以下這段code,就語意上來說執行起來是一樣的,而為了效能考量,它有可能會對這段code進行重新排序然後執行,就會變成以下這段code:
public void writer() {
    v = true;
    x = 42;
}
而這如果發生在multithread的情況下,當有一條thread執行writer(),而另一條thread執行reader(),就會發生問題了,問題在於reader()內的程式:
public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
}
這邊會先判斷v是否為true,是true的話,根據原始碼的流程來看,如果這邊的v為true了,則代表writer()已經將x設為42了,但是由於編譯器對writer()做了重新排序,因此當reader()判斷v== ture之後繼續往下執行時,x的值有可能還是為0,這會造成問題。
所以這邊的code只要boolean v設為volatile,就能避免編譯器對writer ()這段code重新排序,如下:
volatile boolean v = false;

結論:

所以回到一開始的疑問,為何要在這Pattern的instance加上volatile關鍵字。
  • 為了讓Singleton instance初始化正常,不會初始化到一半就被別的thread取走了。
  • 確保編譯器不會幫你對Singleton的code進行優化,讓一切判斷如你預期般的執行。
  • 確保指令在執行時不會被重新排序,造成Singleton的值有異常。
值得注意的是,在JAVA 5以上才能用這種雙重鎖的技巧,原因volatile在JAVA 5之後變得比較完善(JSR-133實現)。。

參考:

  1. 深入理解Java内存模型(一)——基础
  2. 深入理解Java内存模型(四)——volatile
  3. The "Double-Checked Locking is Broken" Declaration
  4. Double-checked locking
  5. JSR 133 (Java Memory Model) FAQ