2014/01/28

Apache Commons Pool的使用

以下介紹Pool的概念並以Apache Commons Pool來製作rabbitmq channel Pool。

Pool的概念

當系統在運行過程中需要建立到網路連線例如資料庫連線,像這類建立連線的動作是會比較消耗系統資源。以資料庫驗証的登入為例子,一個登入的動作需要建立資料庫連線,建立完後僅執行一行sql,接著就關閉資料庫連線。建立資料庫連線會比執行一行sql所花的時間還來的久,因此連線物件一經建立後應該要拿來多加使用才有效率。好在目前已經有許多成熟的Open Source資料庫Pool library像是DBCPProxool可以讓我們重覆的使用已經建立好的資料庫Connection物件。

Pool如何運作?

我們可以先建立物件後放到Pool裡,如果有thread要使用Pool裡的物件的話則可從Pool裡面取出(borrow),物件使用完後需歸還(return)進Pool供其它thread取用。我們可以限制Pool裡的物件最大與最小的數量。如果thread向Pool borrow物件卻發現Pool裡已經無可用物件時(因為被借光了),我們可以決定要讓這個thread做等待或者是直接丟出例外。

如何自訂Pool

Apache Commons Pool library提供了object-Pooling API讓我們很輕易的開發Pool的功能,以下以Apache Commons Pool來製作rabbitmq channel Pool。

下列為將訊息傳送至rabbitmq的queue裡的程式碼,一般來說程式碼會像這樣子

//建立rabbitmq的連線
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();

//利用連線物件來建立channel
Channel channel = connection.createChannel();

//利用channel將訊息Hello World!送至名為[myqueue]的Queue裡
String message = "Hello World!";
channel.basicPublish("", "myeuque", null, message.getBytes());

建立connection, channel是會消耗resource的,rabbitmq允許在一個connection上建立多個channel,因此我們只需製作channel的Pool即可。

以下建立ChannelFactory,此Factory要負責Pool裡物件的建立與銷毀…等。

import org.apache.commons.Pool.BasePoolableObjectFactory;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class ChannelFactory extends BasePoolableObjectFactory<Channel> {

    private Connection connection = null;

    public ChannelFactory(Connection connection) {
        //connection在外部建立完後再傳進來
        this.connection = connection;
    }

    @Override
    public Channel makeObject() throws Exception {
        //此method負責建立Pool裡的物件
        try {
            //利用connection來建立channel
            return connection.createChannel();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void activateObject(Channel obj) throws Exception {
        super.activateObject(obj);
    }

    @Override
    public void destroyObject(Channel obj) throws Exception {
        //Pool裡的物件不再使用要銷毀時的處理
        super.destroyObject(obj);

        //關閉channel
        obj.close();
    }

    @Override
    public void passivateObject(Channel obj) throws Exception {
        // when an object is returned to the Pool,
        // we'll clear it out
        super.passivateObject(obj);
    }

    @Override
    public boolean validateObject(Channel obj) {
        return super.validateObject(obj);
    }
}

以下為ChannelPool, 此類別本身以singleton來實作並決定Pool裡最大建立多少物件,以及取物件時最長等多久,並提供borrowObject, returnObject method

import org.apache.commons.Pool.impl.GenericObjectPool;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class ChannelPool {

    private volatile static ChannelPool _instance;

    private GenericObjectPool<Channel> Pool;
    private Connection connection = null;

    public static ChannelPool getInstance() {
        if (_instance == null) {
            synchronized (ChannelPool.class) {
                if (_instance == null) {
                    _instance = new ChannelPool();
                }
            }
        }
        return _instance;
    }

    private ChannelPool() {
        init();
    }

    private void init() {
        try {
            //建立connection物件            
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            connection = factory.newConnection();

            //connection物件給ChannelFactory拿來建立Pool裡的channel物件使用
            ChannelFactory cf = new ChannelFactory(connection);
            Pool = new GenericObjectPool<Channel>(cf);

            //Pool裡的物件最多產生幾個
            Pool.setMaxActive(5);

            //當borrow被呼叫時且Pool裡已無可用物件時,最長可等多久再丟exception
            Pool.setMaxWait(30000);

        } catch (IOException e) {
            logger.error("error:", e);
        }
    }

    public Channel borrowObject() throws Exception {
        return Pool.borrowObject();
    }

    public void returnObject(Channel channel) throws Exception {
        Pool.returnObject(channel);
    }

    public void close() {
        try {
            //關閉 Pool
            Pool.close();

            //關閉rabbitmq connection
            this.connection.close();

        } catch (Exception e) {
            logger.error("error:", e);
        }
    }
}

當Pool相關類別寫完後,我們可以使用下列的程式碼來取用、歸還channel

//取得channel物件
Channel channel = ChannelPool.getInstance().borrowObject();

//送訊息至queue
channel.basicPublish("", "myeuque", null, message.getBytes());

//歸還channel物件
ChannelPool.getInstance().returnObject(channel);

2014/01/27

erlang basics - data type

erlang 裡的 data 通常被稱為 term。以下內容涵蓋了 numbers and arithmetic, binaries and bitstring, atoms, tuples, lists, strings, pid, port, references, fun, comparing terms, record

numbers and arithmetic

  1. integer
    整數沒有大小限制,隨便怎麼用都可以。

     1> 100.
     100
     2> 1234567890*9876543210*99999999.
     1219326298933089578736473100

    可以用 2進位 到 36進位表示法,16# 就表示是 16進位。

     3> 16#FFff.
     65535
     4> 2#101010110.
     342
     5> 36#ZZ112ko.
     78305427528

    可以用 $ 得到任意字元的數值編碼,ASCII/Latin-1/Unicode 都可以。

     6> $9.
     57
     7> $\n.
     10
     8> $測試.
     * 1: illegal character
     8> $測.
     28204
  2. floating point
    採用64位 IEEE 754 double precision,規定一定要以數字開頭,不能以 . 開頭。erlang 沒有 sigle-precision floating point。

     9> 3.14.
     3.14
     10> -0.123.
     -0.123
     11> 5.312121e23.
     5.312121e23
     12> 19029.219021e-11.
     1.9029219021e-7
  3. arithmetic
    基本的算術為 + - *,如果其中有某一個數值為 floating point,則另一個也會轉換為 floating point 進行運算。除法有兩種 / 跟 div,/ 會回傳 floating point,而 div 會將小數部份截斷,rem 則是取得餘數。

     13> 2*3.14.
     6.28
     14> 2*4.0.
     8.0
     15> 2*4.
     8
     16> 4/2.
     2.0
     17> 4/3.
     1.3333333333333333
     18> 4 div 3.
     1
     19> 4 rem 3.
     1
     20> math:sqrt(2).
     1.4142135623730951
  4. bit operation
    bsl 是將位元資料往左移動,bsr 是往右,還有 band, bor, bxor, bnot。

     21> 1 bsl 4.
     16
     22> 1 bsr 4.
     0
     24> 1 bxor 4.
     5
     25> 1 bor 4.
     5
     26> 1 band(bnot 4).
     1
     27> bnot 4.
     -5

binaries and bitstring

binaries 就是 a sequence of bytes,也就是 8位元 byte 的序列,bitstring 是廣義的 binaries,長度不必是 8 的整數倍。binaries 是一個包含在 << ... >> 內,用逗號區隔的整數序列,可填入 0~255 的數值,超過時,例如 256 會自動轉換為 0。

28> <<0,1,2,3,255>>.
<<0,1,2,3,255>>
29> <<0,1,2,3,256>>.
<<0,1,2,3,0>>
30> <<0,1,2,3,255,256>>.
<<0,1,2,3,255,0>>
31> <<"hello", 32, "world">>.
<<"hello world">>
32> <<0,1,2,3,255,-256>>.
<<0,1,2,3,255,0>>
33> <<0,1,2,3,255,-2>>.
<<0,1,2,3,255,254>>

atoms

atom 類似 java 的 enum,只要兩個 atom 的字元全部一樣,就代表他們是完全相等的。不需要宣告就可以直接使用,他是由一連串的字元組成的,通常以小寫字母開頭,後面可以使用大小寫字母、數字、底線、@,如果還要用到其他字元,就要用單引號括起來,字元的長度上限為 255,單一系統的 atom 數量上限為 1048576 個。

atom 一旦被建立,除非系統 restart,否則就永遠不會被清除,長期運作的系統,要避免動態生成 atom。

35> ok.
ok
36> error.
error
37> trap_exit.
trap_exit
38> route77.
route77
39> test_atom.
test_atom
40> test@atom.
test@atom
41> '@!#!$@%'.
'@!#!$@%'
42> true.
true
43> false.
false
44> undefined.
undefined

tuples

以 { } 表示,是固定長度,依照順序排列的元素列表,裡面可放0到多個元素,也可以再放入另一個 tuple。 tuple 的第一個元素,通常是以 atom 來標記這個 tuple 存放的資料標記,也稱為 tagged tuple。

45> {1,2,3}.
{1,2,3}
46> {one,two,three}.
{one,two,three}
47> {one,{nested,"two", {string}}}.
{one,{nested,"two",{string}}}

48> {position, 5, 2}.
{position,5,2}
49> {size, 25}.
{size,25}

lists

以 [ ] 表示,可存放任意數量的 terms。

50> [].
[]
51> [1,2,3].
[1,2,3]
52> [[1,2,3], ["test", 4]].
[[1,2,3],["test",4]]

referential transparency

erlang 的 list 必須要遵循引用透明性 referential
transparency,因為 erlang 的變數的值不能修改,X 的 reference 可以傳遞到程式的任何一處,但 X 在每一個地方所參考到的值都是一樣,不會改變。此一特性的優點是:

  1. 大幅減少程式的錯誤,因為變數不會改變的特性
  2. 將單一 process 程式重構成多個 processes 時,不需要重寫 code
  3. 不存在對現有資料結構寫入的 operation,因此可以對內部記憶體與multi-processes處理進行更好的優化

新資料要加在 list 的左邊

為了達到 referential transparency 的緣故,新元素要加在 list 的左邊,元素的加法是用 | 。

兩個 list 的加法是用 ++ ,++ 右邊的 list 並不會被破壞,而是藉由一個 pointer,成為新 list 的一部分。

53> [1|[]].
[1]
54> [2|[1]].
[2,1]
55> [ 4,3,2 | [1] ].
[4,3,2,1]
56> [4,3,2] ++ [1,0].
[4,3,2,1,0]
57> [4] ++ [3,2,1,0].
[4,3,2,1,0]

[4,3,2] ++ [1,0] 依序 會處理 [ 2 | [1,0] ] -> [ 3 | [2,1,0]] -> [ 4 | [3,2,1,0]],因此左邊 list 的長度會決定 ++ 的耗時長短,實務上使用時,左邊的 list 長度要比較短,且要盡量從左邊加入新元素到列表中

list 的結構

頭元素:head,tail會指向 list 其他的部份,nil 代表一個空 list。

list 通常用來存放臨時資料、中間結果、字串的cache。對於需要長期保存的資料,要改用 binary 來儲存。

(im)proper list

proper list 都是以 nil 作為 list 的結尾,而 [ 1 | ok ] 這種以 atom 作為結尾的 list 就是 improper list。

很多 list 處理的函數通常會以 nil 來作為結束的判斷,而這些函數如果傳入 improper list 就會造成系統 crash,因此要避免使用 improper list,如果在程式中看到了improper list,就代表可能程式寫錯了。

strings

erlang 的雙引號字串實際上就等同於 list,元素內容就是個字元的數值編碼。

erlang shell為了區分 string 與 list 會檢查 list 裡面所有的元素是不是能列印到 console 上,否則就列印為整數list,要強迫讓 shell 列印出實際的內容,可在前面加上 0 ,例如 [0|v(1)]。

1> "and".
"and"
2> "\t\r\n".
"\t\r\n"
3> [97,98,99,100].
"abcd"
4> [32,9,13,10].
" \t\r\n"
5> [$a,$b,$c,$d].
"abcd"
6> [0|v(1)].
[0,97,110,100]

pid, port, references

  1. Pid
    所有程式都需要一個 erlang process 才能運作,每一個process都有一個 Pid,shell 會以 <0.32.0> 的格式列印 Pid,在 shell 中可使用 self() 取得目前 process 的 Pid。

     8> self().
     <0.32.0>
  2. Port
    Port 跟 Pid 差不多,可跟 Erlang 外界通訊,但不具有程式碼的執行能力,格式為 #Port<0.472> 。

  3. Ref
    可用 make_ref() 產生一個 Ref,格式為 ,通常備用做各種保證資料唯一性的標籤。

     9> make_ref().
     #Ref<0.0.0.60>

fun

erlang 為 functional programming language,可將函數視為參數傳入另一個函數中,其資料型別為 fun,通常也可稱為 lambda or closure。

comparing terms

erlang 的 terms 可以透過內建的 <, >, == 比較與排序,不同資料型別的排序規則為

numbers < atom < ref < fun < port < pid < tuple < list < strings < binary

可使用 lists:sort 進行排序測試。

12> lists:sort([a,d,3,"test", [2,3],1, c, 3.2, "yy"]).
[1,3,3.2,a,c,d,[2,3],"test","yy"]

小於等於要寫成 =<
大於等於要寫成 >=
完全相等(同一個)要寫成 =:=
完全相等的否定(不是同一個) 要寫成 =/=
相等 ==
不相等 /=

不使用 30 =:= 30.0 ,改採用 30 == 30.0 算術相等來判斷。

13> 30 =:= 30.0.
false
14> 30 =:= 30.
true
15> 30 =/= 30.0.
true
16> 30 == 30.0.
true

99%的情況,要使用 =:=,只有在比較 floating point 與 integer 時,== 才有用。

在很多程式或函式庫中,會看到很多地方應該使用 =:= 但是卻是使用 ==,這不會造成程式的錯誤,因為 == 的引數,不是 floating point 而是 integer,兩者的行為是一樣的。

record

當 tuple 裡的元素變多時,我們就不容易記得,那個元素代表什麼意義,record 可以讓我們把名字跟特定的元素關聯起來,record 的本質就是有標記的tuple。

record 的定義宣告只能出現在 module 中,無法直接在 shell 裡面定義,在 shell 中,我們可以用 rr 的指令將定義讀進 shell 中。

rd(R,D) -- define a record
rf() -- remove all record information
rf(R) -- remove record information about R
rl() -- display all record information
rl(R) -- display record information about R
rr(File) -- read record information from File (wildcards allowed)
rr(F,R) -- read selected record information from file(s)
rr(F,R,O) -- read selected record information with options

record 的定義可以放在 .hrl 的檔案中,或是直接放在 .erl module 裡面,以下為定義的語法,可以直接在定義中,給予某些 key 預設值。

-record (Name, {
    key1 = DefaultValue1,
    key2 = DefaultValue2,
    key3,
    ...
})

範例

-record(customer, {name="<anonymous>", address, phone}).
-record(todos, {status=reminder, who=joe, text}).

我們可以用 rr 讀取 record 定義,然後再以 # 產生 record,因為單一賦值的限制,我們不能修改 record,但可以根據前一個 record,修改某個欄位後,產生一個新的 record。

(erlangotp@yaoclNB)12> rr("D:/projectcase/erlang/erlangotp/src/records.hrl").
[customer,todo]
(erlangotp@yaoclNB)13> X=#todo{}.
#todo{status = reminder,who = joe,text = undefined}
(erlangotp@yaoclNB)14> X1=#todo{status=urgent, text="fix errata in book"}.
#todo{status = urgent,who = joe,text = "fix errata in book"}
(erlangotp@yaoclNB)15> X2=X1#todo{status=done}.
#todo{status = done,who = joe,text = "fix errata in book"}
(erlangotp@yaoclNB)16> Y1=#customer{}.
#customer{name = "<anonymous>",address = undefined,
          phone = undefined}
(erlangotp@yaoclNB)17> Y2=#customer{name="test user", address="HomeTown", phone="987654321"}.
#customer{name = "test user",address = "HomeTown",
          phone = "987654321"}

一樣可以用 pattern matching 取出某個欄位的值

(erlangotp@yaoclNB)18> #todo{who=W, text=Text}=X2.
#todo{status = done,who = joe,text = "fix errata in book"}
(erlangotp@yaoclNB)19> W.
joe
(erlangotp@yaoclNB)20> Text.
"fix errata in book"

也可以直接用 . 的語法,取出某個欄位的值

(erlangotp@yaoclNB)21> X2#todo.text.
"fix errata in book"

當我們用 rf 將 record 定義忘記,原本的 record X2,就會變成 tuple,如果再次把 todo 的定義讀進來,X2 又會變回 record。

(erlangotp@yaoclNB)22> rf(todo).
ok
(erlangotp@yaoclNB)23> X2.
{todo,done,joe,"fix errata in book"}
(erlangotp@yaoclNB)24> rr("D:/projectcase/erlang/erlangotp/src/records.hrl").
[customer,todo]
(erlangotp@yaoclNB)25> X2.
#todo{status = done,who = joe,text = "fix errata in book"}

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

2014/01/26

MQTT(一)簡介

前言

會知道MQTT協定,要先回憶起兩年前,當初想找找Android的Push Notification的解決方案,先是找到了當時GCM的前身C2DM,以為Google已經提供了此服務,測試了一下code也蠻容易上手的,結果發現到他有quota限制,不適合拿來當成產品,因此就放棄它,改找別的解決方案,最後找到了兩種不同的實作方式,一種透過XMPP協定來完成,另一種也就是今天要提到的,透過MQTT來完成。

所以MQTT是什麼?

MQTT的全名為 Message Queuing Telemetry Transport,為IBM和Eurotech共同製定出來的protocol,在MQTT的官網可以看到一開始它對MQTT的介紹:
MQTT is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. It was designed as an extremely lightweight publish/subscribe messaging transport.
簡單來說,它是為了物聯網而設計的protocol,並且它是透過publish/subscribe的方式來做訊息傳送。由於是為了物聯網而設計的協定,因此它所需要的網路頻寬是很低的,而所需要的硬體資源也是低的。

Publish/Subscribe:

在看MQTT之前,最好要先知道Publish/Subscribe的訊息傳送機制為何,這樣之後在看其協定時,才會更快上手。Publish/Subscribe有三種主要的組成元件,分別為Publisher、Subscriber以及Topic。

Publisher為訊息的來源,它會將訊息發送給Topic,而Subscriber向Topic註冊,表示他們想要接收此Topic的訊息;因此當有某個Publisher對Topic發送訊息時,只要是有對此Topic註冊的Subscriber,都會收到此則訊息。
它們的關係如下圖:


MQTT特性:

了解了Publish/Subscribe的機制之後,讓我們來看看MQTT有哪些特性:
  1. Publish/Subscribe的訊息傳送模式,來提供一對多的訊息分配。
  2. 使用TCP/IP來提供基本的網路連結。
  3. 三種訊息傳送服務的qualities:
    • "At most once",最多一次,訊息遺失或是重複發送的狀況可能會發生;這種quality適合應用在環境感測,不在意資料是否會遺失,因為下一次的資料取樣很快就會被published出來。
    • "At least once",至少一次,這種quality保證訊息會送達,只是可能會發生重複發送訊息的狀況。
    • "Exactly once",確定一次,確認訊息只會送到一次。這種quality適合用在計費系統,系統只要有重複收到資料、或是資料遺失狀況發生,就會造成系統錯誤。
  4. 由於他的header固定長度為2byte,因此可以減少封包傳送時的額外負載,並減少所需的網路頻寬。
  5. 當異常斷線發生時,會使用最後遺囑(Last Will and Testament)的機制,通知各個感興趣的client。

MQTT現況:

MQTT現階段並不是一個標準化的Protocol,還在持續改進中,目前為MQTT V3.1。不過IBM已於2013年已經將它交給OASIS進行標準化了,並且一直以來IBM對此協定採開放、免授權費的方式讓它能夠被散佈,因此相信不久的將來會成為一個主流的Protocol。

而目前支援MQTT的Client API,有Eclipse Phno Project有對MQTT client支援,其支援C、Java、Javascript、C++等等的語言,可說是支援度很高的Project。而目已經在應用MQTT的,最知名的應該就是Facebook Message App了吧,可以參考此篇文章文章

小結:

上面提到的,低頻寬、低硬體需求的特性,訊息傳遞為Publish/Subscribe的方式,正好可以用來實現Push Notification的機制,並且能達到手持裝置省電的需求,接下來會先從其Protocol開始了解,並用Client Api跑些範例來應用此Protocol。

參考:

MQTT v3.1 specification

2014/01/24

Android 使用gridview 顯示相簿中的縮圖

Android 使用gridview 顯示相簿中的縮圖

這次我想做一個相簿功能,先把手機裝置裡的照片縮圖讀出排列,點擊縮圖可以放大顯示圖片。

main_activity.xml

< RelativeLayout xmlns:android ="http://schemas.android.com/apk/res/android"
    xmlns:tools= "http://schemas.android.com/tools"
    android:layout_width= "fill_parent"
    android:layout_height= "fill_parent"
    tools:context= ".MainActivity" >

    <GridView
        android:id= "@+id/gridView1"
        android:layout_width= "fill_parent"
        android:layout_height= "fill_parent"
        android:gravity= "center"
        android:horizontalSpacing= "1dp"
        android:numColumns= "4"
        android:verticalSpacing= "1dp"
        >
    </GridView >

    <ImageView
        android:id= "@+id/imageView1"
        android:layout_width= "fill_parent"
        android:layout_height= "fill_parent"
        android:gravity= "center"
        android:visibility= "gone"
        android:background= "#00000000"
         />

</ RelativeLayout>

在 main_activty 裡配置了一個gridview 和 一個imageView,gridview 是讓我們放縮圖用的,imageview是當我們點擊小圖顯示放大圖片用的。gridview 裡有幾個屬性是我們需要注意的:android:numColumns= "4" 這意指每一行我們要放四張縮圖,android:horizontalSpacing= "1dp" 水平間隔是 1dp,android:verticalSpacing= "1dp" 垂直間格是1dp。

再來我加了一個 item_photo.xml ,是拿來放一張一張的縮圖用。

<? xml version= "1.0" encoding= "utf-8" ?>
< RelativeLayout xmlns:android ="http://schemas.android.com/apk/res/android"
    android:id= "@+id/rl_item_photo"
    android:layout_width= "wrap_content"
    android:layout_height= "wrap_content"
    >

    <ImageView
        android:id= "@+id/imageView1"
        android:layout_width= "fill_parent"
        android:layout_height= "fill_parent" />

</ RelativeLayout>

那為什麼要特別要加一個 item_photo.xml 來放縮圖呢? 這是我們方便可以為縮圖的顯示做客製加以變化。

MainActivity.java

package com.example.gridview;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.content.ContentResolver;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivity extends Activity {

     private GridView gridView;
     private ImageView imageView;
     private List<String> thumbs;  //存放縮圖的id
     private List<String> imagePaths;  //存放圖片的路徑
     private ImageAdapter imageAdapter;  //用來顯示縮圖

     @Override
     protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);

          gridView = (GridView) findViewById(R.id.gridView1);
          imageView = (ImageView) findViewById(R.id.imageView1);

          ContentResolver cr = getContentResolver();
          String[] projection = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA };

          //查詢SD卡的圖片
          Cursor cursor = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    projection, null, null, null);

          thumbs = new ArrayList<String>();
          imagePaths = new ArrayList<String>();

          for (int i = 0; i < cursor.getCount(); i++) {

               cursor.moveToPosition(i);
               int id = cursor.getInt(cursor
                         .getColumnIndex(MediaStore.Images.Media._ID));// ID
               thumbs.add(id + "");

               String filepath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));//抓路徑

               imagePaths.add(filepath);
          }

          cursor.close();

          imageAdapter = new ImageAdapter(MainActivity.this, thumbs);
          gridView.setAdapter(imageAdapter);
          imageAdapter.notifyDataSetChanged();


          imageView.setOnClickListener(new OnClickListener() {

               @Override
               public void onClick(View v) {
                    // TODO Auto-generated method stub
                    imageView.setVisibility(View.GONE);
                    gridView.setVisibility(View.VISIBLE);
               }

          });
          imageView.setVisibility(View.GONE);

     }

     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
          // Inflate the menu; this adds items to the action bar if it is present.
          getMenuInflater().inflate(R.menu.main, menu);
          return true;
     }

     public void setImageView(int position){
          Bitmap bm = BitmapFactory.decodeFile(imagePaths.get(position));
          imageView.setImageBitmap(bm);
          imageView.setVisibility(View.VISIBLE);
          gridView.setVisibility(View.GONE);
     }
}

以上我們在 onCreate() 時,去讀取SD卡上的圖片,取出圖片的ID放入 thumbs, 取出圖片的路徑放入 imagePaths,透過ImageAdapter 將thumbs的圖片ID去取縮圖。

imageView 加入click event 以切換 gridview 及 imageview 的顯示。

setImageView() 取出路徑轉成Bitmap放入ImageView 顯示處理。

package com.example.gridview;

import java.util.List;

import android.content.Context;
import android.graphics.Bitmap;
import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.Toast;

public class ImageAdapter extends BaseAdapter {

    private ViewGroup layout;
    private Context context;
    private List coll;

    public ImageAdapter(Context context, List coll) {

        super();
        this.context = context;
        this.coll = coll;
    }

    public View getView(final int position, View convertView, ViewGroup parent) {

        LayoutInflater inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View rowview = inflater.inflate(R.layout.item_photo, parent, false);
        layout = (ViewGroup) rowview.findViewById(R.id.rl_item_photo);
        ImageView imageView = (ImageView) rowview.findViewById(R.id.imageView1);

        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        float dd = dm.density;
        float px = 25 * dd;
        float screenWidth = dm.widthPixels;
        int newWidth = (int) (screenWidth - px) / 4; // 一行顯示四個縮圖

        layout.setLayoutParams(new GridView.LayoutParams(newWidth, newWidth));
        imageView.setId(position);
        // Bitmap bm = BitmapFactory.decodeFile((String)coll.get(position));
        // Bitmap newBit = Bitmap.createScaledBitmap(bm, newWidth, newWidth,
        // true);

        Bitmap bm = MediaStore.Images.Thumbnails.getThumbnail(context
                .getApplicationContext().getContentResolver(), Long
                .parseLong((String) coll.get(position)),
                MediaStore.Images.Thumbnails.MICRO_KIND, null);

        imageView.setImageBitmap(bm);
        imageView.setScaleType(ImageView.ScaleType.FIT_XY);

        //點擊照片
        imageView.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                Toast.makeText(context, "index:" + position, Toast.LENGTH_SHORT)
                        .show();

                ((MainActivity)context).setImageView(position);
            }

        });

        return rowview;
    }

    @Override
    public int getCount() {
        // TODO Auto-generated method stub
        return coll.size();
    }

    @Override
    public Object getItem(int arg0) {
        // TODO Auto-generated method stub
        return coll.get(arg0);
    }

    @Override
    public long getItemId(int position) {
        // TODO Auto-generated method stub
        return position;
    }

}

layout.setLayoutParams(new GridView.LayoutParams(newWidth, newWidth)); 為了顯示正方形的縮圖形狀,先算出適合的寬高。

DisplayMetrics dm = context.getResources().getDisplayMetrics();
float dd = dm.density;  //取出密度
float px = 25 * dd;  //像素 = dp * 密度
float screenWidth = dm.widthPixels;  //取出螢幕寬度

這個公式用來計算一張圖片在不同裝置下不同解析度時應該縮放顯示的像素。

imageView.setScaleType(ImageView.ScaleType.FIT_XY); // 把圖片不按比例 擴大/縮小到View的大小顯示

以下 ImageView.ScaleType / android:scaleType值說明:

CENTER /center 按圖片的原來size居中顯示,當圖片長/寬超過View的長/寬,則截 取圖片的居中部分顯示

CENTER_CROP / centerCrop 按比例擴大圖片的size居中顯示,使得圖片長 (寬)等於或大於View的長(寬)

CENTER_INSIDE / centerInside 將圖片的內容完整居中顯示,通過按比例縮小 或原來的size使得圖片長/寬等於或小於View的長/寬

FIT_CENTER / fitCenter 把圖片按比例擴大/縮小到View的寬度,居中顯示

FIT_END / fitEnd 把 圖片按比例擴大/縮小到View的寬度,顯示在View的下部分位置

FIT_START / fitStart 把 圖片按比例擴大/縮小到View的寬度,顯示在View的上部分位置

FIT_XY / fitXY 把圖片 不按比例 擴大/縮小到View的大小顯示

MATRIX / matrix 用矩陣來繪製

//點擊照片
imageView.setOnClickListener(new OnClickListener() {

    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
        Toast.makeText(context, "index:" + position, Toast.LENGTH_SHORT).show();
        ((MainActivity)context).setImageView(position);
    }
});

點擊縮圖時,去呼叫MainActivity的 setImageView(position) method 顯示圖片。

2014/01/23

Markdown的使用

今天想來介紹一下 Markdown..

什麼是 Markdown? Markdown是由John Gruber跟Aaron Swartz所創造出來的輕量標記語言。 他們希望能夠讓人們可以不用HTML複雜繁長的語法就能夠標記格式,能夠易讀、易寫的簡單文件格式,可以透過工具就能格式化轉成HTML語法。 維基百科有Markdown 的更詳細介紹

由於小麥開始進行技術Blog的寫作,為了讓夥伴可以很方便的撰寫blog的文章並且將程式碼的片段也能快速的整併在內容當中,所以Markdown是很適合我們這群工程師寫文章的工具。
Markdown 語法困難嗎? 其實熟悉HTML語法的工程師,在上手Markdown應該很快,可以快速 review 一下這個語法,半小時內保證輕鬆上手。 我推薦這個 Markdown語法教學 的中文網站,內容清楚又簡單,上去看看吧!

小麥的夥伴在公司裡分別使用Mac與Windows的系統,所以在此先介紹我在Mac系統攥寫Markdown工具 -- Mou 官方網站

Mou 是對岸一位工程師羅晨的作品,有興趣的朋友可以看看INSIDE對他的專訪:獨立開發者的孤獨——Mou 作者羅晨專訪

下載Markdown之後,開啟編輯器,編輯畫面分別有兩邊,左邊是文字輸入的部份,右邊則是即時預覽畫面結果,當然也可以視個人的喜好加以設定調整,文章內容打完後,透過選項選擇輸出HTML,你就很容易將整篇文章貼到Blog上面。

這的確對於工程師在寫規格設計文件或是技術文章,有著方便易用的特性,像這篇blog文章也是用Mou所寫的,還沒Markdown的朋友們,要不要也來試試看!


Erlang 在 Mac OS X 的安裝

在 Mac OS X的作業系統裡安裝 erlang 的開發環境有幾種方式
第一種使用 Macports 方法安裝,在Mac OS console 下,打入以下指令
sudo port install erlang
第二種則是使用 Homebrew 的安裝,同樣在 console 下輸入指令
brew install erlang
以上兩種方法都可以安裝好 erlang 環境,但是會有個問題是在於安裝好後,當要使用 observer 這種 GUI tools 時會無法使用,原因在於因為以上兩種安裝方式並沒有幫你安裝 wxWidgets 套件,那麼如何確認你的 Mac OS X 是否能使用?

先進入 erlang shell 後,請啟動 observer,如果出現 exception 那麼你就得先把目前的 erlang 環境移除掉之後,再用以下方式來安裝 erlang 環境
$ erl
Erlang R16B (erts-5.10.1) [source] [64-bit] [smp:2:2]   [async-threads:10] [kernel-poll:false]

Eshell V5.10.1 (abort with ^G)
1> observer:start().
exception error: undefined function wx_object:start/3 
in function observer_wx:start/0 (observer_wx.erl, line 66)
先使用 Macports 安裝 wxWidgets 套件
sudo port install wxWidgets-3.0
然後再到 erlang 官方網站抓取目前最新版本的 source 回來,並解壓縮,如果你的Mac OS是設定 64bit 環境的話,記得下載 64bit source版。
下載網址:All versions for Mac OS X
解壓縮source後,我們開始手動編譯,先來 configure,用LLVM 來 compile,記得要加上 wxgtk參數讓它能去 link library
CFLAGS=-O0 ./configure --disable-hipe --enable-smp-support --enable-threads  --enable-kernel-poll --enable-darwin-64bit --with-ssl --with-wxgtk
configure成功之後,接著就是進行 make 與 make insatll
make;sudo make install
要如何驗證你是否已經成功安裝好在 erlang 環境裡使用 wxWidgets,請按照文章前面的方式啟用 observer ,如果你可以看到GUI tool 出現在你的螢幕畫面上,恭喜你,你已經完成基本的安裝設定了!

erlang basics - installation

安裝 erlang 開發環境並不難,以下我們分 Windows 與 CentOS 兩種作業系統說明。

Windows

erlang otp downloads 頁面下載安裝檔,32 bits: esl-erlang_16.b.3-1~windows_i386.exe 或是 64 bits: esl-erlang_16.b.3-1~windows_amd64.exe,然後直接執行就可以了,erlang會安裝到 c:\Program Files\erl5.10.4。

記得要設定環境變數,可在 電腦->內容->進階系統設定->環境變數 這裡設定:

ERLANG_HOME=c:\Program Files\erl5.10.4
PATH=%ERLANG_HOME%\bin;%PATH%

CentOS 6

CentOS 安裝有三種方式,第一種是 rpm 安裝,第二種是直接由 source code 編譯,第三種是從 CEAN 下載安裝檔。

rpm 安裝檔

erlang otp downloads 頁面下載安裝檔,首先取得 rpm 安裝檔,32 bits: esl-erlang_16.b.3-1~centos~6_i386.rpm 或是 64 bits: esl-erlang_16.b.3-1~centos~6_amd64.rpm,接下來依照下面的 sript 進行安裝。

# 因為 erlang 有些 gui 工具介面
yum install wxGTK
# 因為 rpm 相關的套件,openssl 安裝到最新版 1.0.1e-16.el6_5.1 即可
yum install openssl
# install erlang
rpm -Uvh esl-erlang_16.b.3-1~centos~6_i386.rpm
# 安裝的目錄為 /usr/lib/erlang

由 source 編譯

如果要從 source code 編譯,先到 erlang otp downloads 頁面下載原始檔 otp_src_R16B03.tar.gz

tar zxvf otp_src_R16B03.tar.gz
cd otp_src_R16B03
./configure

執行後出現以下的警告,所以我們要先安裝一些套件
odbc : ODBC library - link check failed
wx : wxWidgets not found, wx will NOT be usable
documentation :
fop is missing.
Using fakefop to generate placeholder PDF files.

yum install fop unixODBC* openssl-devel
yum install wxGTK
./configure

執行後還是一樣,出現警告,這表示 wxGTK 套件要自己編譯
wx : wxWidgets not found, wx will NOT be usable

cd ..
wget http://sourceforge.net/projects/wxwindows/files/3.0.0/wxWidgets-3.0.0.tar.bz2/download
tar jxvf wxWidgets-3.0.0.tar.bz2
# 把剛剛裝的 wxGTK 移除
yum remove wxGTK
cd wxWidgets-3.0.0
# 安裝 opengl 相關套件
yum install mesa*
./configure --with-opengl --enable-debug --enable-unicode --enable-graphics_ctx 
make
make install
export LD_LIBRARY_PATH=/usr/local/lib
ldconfig

接下來就可以正常地把 erlang otp 裝好。

cd otp_src_R16B03
./configure
make
make install

CEAN

CEAN: Comprehensive Erlang Archive Network 將主要的 Erlang Application 都集中在一起,並提供各種 OS, CPU 編譯好的 binary files,應該是依照網頁的說明去安裝就好了,這種安裝方式,我沒有測試。

Erlang shell: erl

在 Windows 可以直接從 Menu 選單中執行 werl,這會出現 erlang 的 GUI console,也可以自己在命令提示字元中,執行 erl。

查閱 erlang otp 版本資訊

> erl -version
Erlang (SMP,ASYNC_THREADS,HIPE) (BEAM) emulator version 5.10.4

啟動 erlang shell,直接執行 erl 即可,畫面上的 1> 是 erlang shell 的系統提示符號,每一次輸入一個 expression 就會累加變成 2>,可以用上、下方向鍵或是Ctrl+P/Ctrl+N,直接上下切換剛剛輸入的每一行資料。erlang expression 每一句都是以 . 句號結束,不一定要在同一行裡面就把整個 expression 寫完,erlang shell 會馬上把計算的結果列印在畫面上。

> erl
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
1> 40.
40
2> 12 +
2> 5.
17
3> h().
1: 40
-> 40
2: 12 + 5
-> 17
ok
4> v(1)+v(2).
57
5> "a test.".
"a test."

shell functions

在 shell 直接打 help(). 可以列印出 shell 可使用的函數,以下列出常用的函數 。

6> help().
** shell internal commands **
b()        -- display all variable bindings 列印所有變數連結
f()        -- forget all variable bindings  移除所有變數連結
f(X)       -- forget the binding of variable X 移除某個變數連結
h()        -- history
v(N)       -- use the value of query <N>
** commands in module c **
bt(Pid)    -- stack backtrace for a process
c(File)    -- compile and load code in <File>
cd(Dir)    -- change working directory

help()     -- help info

i()        -- information about the system
ni()       -- information about the networked system
i(X,Y,Z)   -- information about pid <X,Y,Z>

l(Module)  -- load or reload module
lc([File]) -- compile a list of Erlang modules
ls()       -- list files in the current directory
ls(Dir)    -- list files in directory <Dir>

m()        -- which modules are loaded
memory()   -- memory allocation information

nc(File)   -- compile and load code in <File> on all nodes
nl(Module) -- load module on all nodes

pid(X,Y,Z) -- convert X,Y,Z to a Pid
pwd()      -- print working directory
q()        -- quit - shorthand for init:stop()

quitting shell

要離開 shell 有幾種方式:

  1. 使用 q()init:stop()
    q() 是 init:stop() 的簡寫,這是最安全的跳出方式。

  2. Ctrl-G
    Ctrl-G 可打開使用者命令選單,鍵入 Ctrl-G 的時候,會出現 User switch command --> 的提示字元,再鍵入 h 或 ? 可以列印所有的指令。

    這個功能在 linux 裡面的 erl 有作用,在 windows 的 werl 也有作用,但 windows 的 erl 卻是直接產生一個新的 shel。我還沒找到要怎麼在 windows erl 打開這個功能的熱鍵。

     6>
     User switch command
      --> h
       c [nn]            - connect to job
       i [nn]            - interrupt job
       k [nn]            - kill job
       j                 - list all jobs
       s [shell]         - start local shell
       r [node [shell]]  - start remote shell
       q                 - quit erlang
       ? | h             - this message
  3. BREAK
    在 linux 可以按 Ctrl-C,Windows 的 werl 可以按 Ctrl-Break,這會出現 BREAK 選單,進入選單後,再以 (a) 退出系統 (c) 返回 shell (v) 列印目前 erlang 的版本 (k) 可瀏覽所有 erlang 內部活動,並強制關閉某個故障的 process。BREAK 選單位於底層,可以在 Ctrl-G 之後,再呼叫出 BREAK 選單。

     BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
            (v)ersion (k)ill (D)b-tables (d)istribution

process處理

如果有某個 process crash,或不慎進入了無窮迴圈,我們可以在 Ctrl-G User switch command 裡面,進行處理。

以 timer:sleep(infinity) 模擬無窮迴圈的狀況。

Eshell V5.10.4  (abort with ^G)
1> timer:sleep(infinity).

這時候,shell 會被鎖死,不管打什麼指令都沒有反應,按 Ctrl-G 進入 user switch command。

j 列出目前的 process
s 啟動一個新的 process
c 2 連接到新的、正常的 process

User switch command
 --> j
   1* {shell,start,[init]}
 --> s
 --> j
   1  {shell,start,[init]}
   2* {shell,start,[]}
 --> c 2

進入第二個 process 後

k 1 把第一個掛掉的 process 砍掉
c 連接到目前預設的 process(有 * 記號)

Eshell V5.10.4  (abort with ^G)
1>
User switch command
 --> j
   1  {shell,start,[init]}
   2* {shell,start,[]}
 --> k 1
 --> j
   2* {shell,start,[]}
 --> c

1>

erlide in Eclipse

在 Help -> Install new software 裡面把 http://erlide.org/update 網址貼到安裝網址上,選擇安裝 Erlang IDE 與 Erlang add-ins。在 Window -> Preference -> Erlang -> Installed runtimes 裡面,可看到 erlide 有自動找到 erlang runtimes。

erlide 是個好用的開發 ide,但當我們把 eclipse 直接關閉時,在 eclipse 的 working directory 會出現一個檔名類似 rpc_monitor-20140108_155607.dump 的檔案,這個 dump 檔,有時後會出現 erl_crash.dump。

erlide 對錯誤訊息的列印還有些問題,一直看不到結果的時候,原因應該跟 No error message in Eclipse consloe/erang shell 一樣,因為 erlide 並不是直接跟 shell 互動,而是透過一層遠端呼叫,再呼叫 shell,這個問題到現在還沒解決。

erlide 好用,但以上面的問題來看,程式測試可能還是要回到 console 自己用 erl 測試,才會得到比較確切的回應結果。

調整開發環境

設定載入程式碼的搜尋路徑

  1. @spec code:get_patha(Dir) -> true | {error, bad_directory}
    將新目錄加入到載入路徑的頭
  2. @spec code:get_pathz(Dir) -> true | {error, bad_directory}
    將新目錄加入到載入路徑的尾
  3. @spec code:get_path() -> [string()]
    得知目前載入路徑
  4. @spec code:all_loaded()
    得知目前所有載入的模組
  5. @spec code:clash()

當系統開始時,執行一組命令 .erlang

在 home 目錄,放一個 .erlang 檔案,當 erl 啟動時,會先讀入此檔案,且執行裡面的內容。

如果是在 Windows ,則把檔案放在 c:\Users{username}\ 這個目錄中。

%% .erlang
io:format("Running Erlang~n").
code:add_patha(".").
code:add_pathz("/home/bin").

啟動 erl 時會看到

>erl
Running Erlang
Eshell V5.10.4  (abort with ^G)
1> code:get_path().
[".","c:/PROGRA~1/ERL510~1.4/lib/kernel-2.16.4/ebin",
 "c:/PROGRA~1/ERL510~1.4/lib/stdlib-1.19.4/ebin",
 "c:/PROGRA~1/ERL510~1.4/lib/xmerl-1.3.5/ebin",
 "c:/PROGRA~1/ERL510~1.4/lib/wx-1.1.1/ebin",
 "c:/PROGRA~1/ERL510~1.4/lib/webtool-0.8.9.2/ebin",
 [...]|...]

另一種方式,也可以在 erl 加上參數 -pa -pz ,將搜尋路徑加入到路徑的頭或尾

erl -pa Dir1 -pa Dir2 ... -pz DirK1 -pz DirK2

crash dump

如果 erlang 當機,會產生一個 erl_crash.dump 的檔案,如果要分析此 dump,有一個網頁版的 dump 分析器

>erl
Running Erlang
Eshell V5.10.4  (abort with ^G)
1> webtool:start().
WebTool is available at http://localhost:8888/
Or  http://127.0.0.1:8888/
{ok,<0.35.0>}
2>

然後在瀏覽器瀏覽網址 http://localhost:8888/ ,就可以啟動 CrashDumpViewer 上傳並分析 erl_crash.dump。

escript

escript 可直接執行用 erlang 寫的 script file,在 linux 環境,可以直接編輯一個執行檔

>vi hello
#!/usr/bin/env escript
main(_) ->
    io:format("Hello World.~n").
>chmod 755 hello
>./hello
Hello World.

在 Windows 環境

% file: hello.escript
main(_) ->
    io:format("Hello World.~n").

可直接在命令提示字元中,執行此 script

>escript hello.escript
Hello World.

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

2014/01/22

在iOS應用程式中使用相機與存取相簿

使用行動裝置時,相機可說是最常被使用到的設備之一。因此,對開發者而言,瞭解如何藉由系統API使用相機及存取相簿,也是不可不知的基本功。以下介紹幾個在iOS上常用的方法:

透過UIImagePickerController使用相機、讀取相簿

UIImagePickerController是UIViewController的子類別。顧名思義,此類別不僅實作了常用的相機與相簿功能,還提供了基本的UI界面,讓開發者不需再勞心於UI的設計與實作。

使用UIImagePickerController,只要先建立其物件,設定相關參數,再透過presentViewController將它呈現在畫面上即可。而參數的部分說明如下:

  1. sourceType

    指定使用相機還是從相簿中挑選相片。UIImagePickerControllerSourceTypeCamera為使用相機,UIImagePickerControllerSourceTypePhotoLibrary則是從相簿中挑選相片。

  2. delegate

    此處的delegate物件指的是實作UIImagePickerControllerDelegate的物件。 此物件將接收使用者拍完照片或選好相片後的事件,以處理接收到的相片資訊。

  3. allowsEditing

    允許使用者在UIImagePickerController的畫面中編輯、修改相片。

以下為使用UIImagePickerController來使用相機的範例:

UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];

//檢查是否支援此Source Type(相機)
if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
    //設定影像來源為相機
    imagePicker.sourceType =  UIImagePickerControllerSourceTypeCamera;
    imagePicker.delegate = self;
    imagePicker.allowsEditing = YES;

    //顯示UIImagePickerController
    [self presentViewController:imagePicker animated:YES completion:^{}];
}
else {
    //提示使用者,目前設備不支援相機
}

而delegate物件必須實作以下方法,在使用者按下確定時處理得到的相片資訊,範例如下:

//使用者按下確定時
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
    //取得剛拍攝的相片(或是由相簿中所選擇的相片)
    UIImage *image=[info objectForKey:UIImagePickerControllerEditedImage];

    //設定ImageView的Image物件,例如:
    //yourImageView.image = image;

    [picker dismissViewControllerAnimated:YES completion:^{}];
}

//使用者按下取消時
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
    //一般情況下沒有什麼特別要做的事情

    [picker dismissViewControllerAnimated:YES completion:^{}];
}

由以上範例不難注意到,imagePickerController:didFinishPickingMediaWithInfo:只有接收到一張相片的資訊。在選擇使用相機直接拍攝相片的情況下,這點沒有什麼問題;但若是由相簿中挑選相片的情況下,這不就意味著一次只能挑選一張相片?

是的,這就是使用UIImagePickerController最大的不便所在。

使用UIImagePickerController由相簿中挑選相片時,是不支援多選的。一次僅能挑選一張相片。

因此,以下將再介紹另一種存取相簿的方式。

使用AssetsLibrary存取相簿

AssetsLibrary提供較高的自由度讓開發者存取相簿;當然相對地,就不像UIImagePickerController有現成UI可以直接使用。開發者必須自行將取得的UIImage呈現在畫面上。

AssetsLibrary使用列舉的方式,讓程式讀取相片資訊。每一張相片的資訊存放在一個ALAsset物件中,此物件提供thumbnaildefaultRepresentation兩個方法,分別用以取得縮圖及原圖的UIImage物件。使用AssetsLibrary的範例如下:

ALAssetsLibrary* library = [[ALAssetsLibrary alloc] init];

[library enumerateGroupsWithTypes:ALAssetsGroupAll
    usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
    [group setAssetsFilter:[ALAssetsFilter allPhotos]];
    [group enumerateAssetsUsingBlock:^(ALAsset *alAsset, NSUInteger index, BOOL *innerStop)
     {
         //讀取一張張的相片。當alAsset會變為nil時,表示已經列舉完所以
         if (alAsset) {
             //取得縮圖
             UIImage* thumbnailImage = [UIImage imageWithCGImage:[alAsset thumbnail]];

             //取得原圖
             UIImage* image = [UIImage imageWithCGImage:[[alAsset defaultRepresentation] fullResolutionImage]];

             //處理縮圖及原圖...
         }
     }];
}
 failureBlock: ^(NSError *error) {
     //依據error進行錯誤處理
 }];

ALAssetsLibrary除了能讀取相簿外,也能夠將圖片儲存到相簿中,範例如下:

UIImage* image;

//設定要儲存的image物件
//image = your image...

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:^(NSURL *assetURL, NSError *error){
    if (error) {
        //錯誤處理
    }
}];

補充:調整圖片大小

由相機、相簿取得圖片後,調整圖片大小也是常常遇到的需求。以下追加介紹調整圖片大小的方法:

    //原圖片物件
    UIImage* imageToResize;     

    //設定需要調整大小的圖片物件
    //oriImage = ...

    //設定欲調整的新大小
    CGSize destinationSize = CGSizeMake(120.0, 160.0);

    UIGraphicsBeginImageContext(destinationSize);
    [oriImage drawInRect:CGRectMake(0,0,destinationSize.width,destinationSize.height)];

    //取得調整過大小的UIImage物件
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

結語

在iOS應用程式中使用相機、讀取相片其實並不困難,卻是很常見的需求。這也是iOS開發者必備的基本功之一,請務必謹記在心。

2014/01/20

Why erlang? What can erlang do for us?

Erlang 是一種通用的平行導向程式語言,老爸是Joe Armstrong,想開發出一個共時、分散式、多核心、容錯的系統,Erlang 是個絕佳的選擇。

C10K Problem

著名的 The C10K problem 告訴我們,在高速網路與便宜的硬體的帶動下,如果一台機器能服務更多使用者,就代表著每一個客戶端消耗掉的運算資源更少,成本更低。

要解決這個問題,最重要的是要處理網路IO與硬碟IO,這些方法的區隔與不同之處如下:

  • 決定要不要在 single thread 中處理多個 I/O calls,要怎麼做?

    1. 不使用single thread處理多個 I/O 的方法,還是沿用 blocking/synchronous calls 並持續以 multiple threads or processes 來處理 concurrency 的問題

    2. 使用 nonblocking calls 啟動 I/O (e.g. write() on a socket set to O_NONBLOCK),並以有資料需要讀取的通知 readiness notification 來告訴該 channel 要啟動一輪新的 I/O (e.g. poll() or /dev/poll) 這種方法只適合 network I/O 不適合 disk I/O

    3. 使用 asynchronous calls 啟動 I/O (e.g. aio_write()),處理完成後發出通知(例如 signals or completion ports),這種方法同時適合 network 與 disk I/O。

  • 如何撰寫處理每個客戶端的程式碼?

    1. 每一個 client 使用一個 process (這是Unix 自 1980年開始,就一直採用的方法)

    2. 一個 OS-level thread 服務多個 clients,每一個 client 透過以下某個方式控制

      2.1 一個 user-level thread (例如 GNU state threads, classic Java with green threads)

      2.2 a state machine (a bit esoteric, but popular in some circles; my favorite)

      2.3 a continuation (a bit esoteric, but popular in some circles)

    3. 一個客戶端使用一個 OS-level thread (e.g. classic Java with native threads)

    4. 一個 active client 使用一個 OS-level thread (e.g. Tomcat with apache front end; NT completion ports; thread pools)

  • 不使用標準 OS services,直接將程式碼放進 kernel (e.g. in a custom driver, kernel module, or VxD)

下面的五種是最常見的組合方式

  1. 一個 thread 服務多個客戶端,使用 nonblocking I/O 和 level-triggered readiness notification
  2. 一個 thread 服務多個客戶端,使用 nonblocking I/O 和 readiness change notification
  3. 一個 thread 服務多個客戶端,使用 asynchronous I/O
  4. 一個 thread 服務一個客戶端,使用 blocking I/O
  5. 把程式碼編譯進 kernel

Currency in Erlang

共時導向編程 提到「共時」有兩種不同的作法,分別是「共享狀態共時」,與「訊息傳遞共時」。大多數的主流語言(Java、C#、C++)採用「共享狀態共時」的設計,但 Erlang 採用「訊息傳遞共時」。

在Erlang中沒有共享狀態,唯一一種交換資料的方式,是透過「非同步」訊息傳遞。Erlang的共時單位稱為行程(process),但其實是類似執行緒的地位,而不是真正的OS process。Erlang的行程之間彼此透過訊息傳遞方式來進行溝通,每個行程都可以在不同的核心,不同的CPU,甚至不同的電腦上執行。

對照到上面的 C10K 的解決方案,Erlang 的方法是一個 thread 服務一個客戶端,使用 asynchronous calls 啟動 I/O,處理完成後發出通知,從上面的分析,我們也知道這種方式,同時可以處理 網路 與 Disk I/O。

Java設計分佈式網絡架構的不足及與Erlang的比較 中提到,傳統的 RPC (Java RMI) 都是錯誤的,我們不應該想辦法把遠端呼叫包裝成跟 local call 一樣。Erlang 沒有 RPC,只有 message send/receive,receiver也能設定 timeout。

Dig into Erlang

雖然 Erlang 天生的分散式及語言本身的特性,讓他非常適合實作一個多工、容錯、分散式的系統,但並不是學會了 Erlang 的語法,了解了 OTP,就代表我們可以完美地解決 C10K Problem,甚至能處理到 C500K Problem,這牽涉到語言熟悉度,以及一些性能調整的技巧,換句話說,我們需要花時間深入 dig into the world of Erlang。

從 C10K 到 C500K 這篇文章說,現在大家開始在談 C500K,而不是 C10K 了,也提供了資訊(Linux Kernel Tuning for C500k),使用 Java+NIO 的方式,達成 C500K 的指標。

其實要達成 C500K,也不一定要用 Erlang,用 Java+NIO 也可以。那麼是不是就不要用 Erlang 呢?Erlang 還有其他優勢是 Java 欠缺的,那就是容錯與分散式。

當我們以 Erlang OTP 撰寫服務時,可以很快速,在不需要修改太多程式碼的狀況下,將程式放在多個 Erlang VM 中運作,而且這幾個 Erlang VM 可以在單一或多個實體機器上執行,Erlang OTP 也內建了monitor機制,可以設定 process 存在的條件,在發生錯誤時,自動重新啟動,或做對應的處理。這樣的優勢在 Java 開發上完全是看不見的。

世界是平行的

當我們搜尋 Erlang 時,在繁體中文的網頁中,一定會搜尋到蔡學鏞先生寫的 Erlang:世界是平行的!,還有他翻譯而且已經絕版的 Erlang程式設計 這本書。Erlang的相關資料除了英文,還有對岸 Erlang資源列表 產出了很多翻譯的資料,背後的原因,當然是經濟規模。

在台灣的軟體開發,要求要能處理 C1K 已經算是個大型的系統了,一般中小企業的系統,同時大概都是幾十到幾百個使用者使用而已,在大陸,一個城市的經濟規模的最大值就可能遠遠超過台灣的狀況。

但我們也不需要妄自菲薄,大陸的軟體公司,也不是每一個都有需要作到 C10K,也有很多中小企業需要的軟體,並不是每一個系統,都要杞人憂天,幻想會有超多的使用者同時連線進來使用。畢竟系統都是在一個進化的過程中,慢慢地因應需求,逐漸調整到最佳狀態。

erlang 的語言特色

  1. 平行導向程式設計: 以 spawn/* 函式,將函式設定為獨立的 process,後續使用訊息互相傳遞,進行跨行程通訊
  2. functional programming
  3. single assignment: 變數只能被設定一次,不能異動
  4. dynamic data type: 動態資料型別
  5. exception catching: 以 try catch 進行異常處理
  6. code hot deployment: 在不中斷系統的情況下,進行程式替換

LYME/LYCE

在 open source 軟體堆疊中,如果要建構一個支援動態網頁內容的網站,相對於常見的 LAMP:Linux, Apache HTTP Server, MySQL, PHP,在 Erlang 環境有著 LYME/LYCE:Linux, Yaws, Mnesia/CouchDB, Erlang 的軟體bundle。

要開發網頁程式,LYME 也能提供完整的開發環境,而且除了 Linux 沒什麼好挑的之外(其實可以選擇自己慣用的 distribution,我們是用 CentOS),網頁 Server Yaws也有替代選擇:Mochiweb、Misultin、Cowboy,資料庫 Mnesia 的替代選擇為:CouchDB、Riak、SimpleDB、Cloudant、Couchbase Server。

Prepare to Jump Higher

C1K 已經很夠用了,那我們選擇學習 Erlang 的用意在哪?就是為了要「準備」,公司目前雖然還是在小企業的規模中掙扎,一開始也不自我膨脹,引入外部資金來支撐一些線上服務的夢想。

我們是雙面人,一方面要激進地嘗試不同的領域與技術,另一方面又是保守的,就算是要燒錢,也要在更有把握的條件下孤注一擲。

接下來我們會嘗試從基本語法開始,認識 Erlang 的語言特性,然後是 OTP,最終目的是產出一個 open source 專案。

Erlang 的爸爸:Joe Armstrong

最後要認識一下 Erlang 的爸爸 Joe ArmstrongSICS首頁 有他的照片。1986年釋出了第一版的 Erlang,到今天已經過了28年。

Github:http://joearms.github.io/
SICS:http://www.sics.se/~joe/
Blog:http://armstrongonsoftware.blogspot.tw/ 已經很久沒有更新了)

2014/01/18

一位 pro leader 該有的條件

先前的文章我眼中的 pro programmer,討論到一位專業的程式設計師,該有什麼樣子,當時的文章最末段也想過,該找個人寫篇 pro leader 的模樣典範,而今天剛好看到InfoQ提供了一 篇文章的翻譯:優秀的領導與差勁的領導,原文是 Vlad Mihalcea 的一篇文章:Good vs Bad Leader,內容是討論一位專業的 leader 該是什麼樣子。

翻譯文章的內容雖然跟原文一樣,但是卻放棄了遵循原文表格對照的寫法,這降低了文章的可讀性。我並沒有得到原作者的同意,可以將整篇文章逐字翻譯,但我想我可以原作者的表格方式,然後嘗試用簡單幾句話做個整理。

context good leader bad leader
負責 承擔自己專案的成敗 永遠會怪別人
努力工作 以身作則,不管有趣或無聊的工作都做 不再寫程式
指導新人 注意新人的狀態,懂得在放手跟要求中間拿捏分寸 新人就是要操他才會進步
尊重 尊重每一個人 成員有人犯錯就落井下石
升遷 相信技術與專業會得到回報 沒料,只會逢迎拍馬、耍嘴皮子
信任 相信團隊成員,大家在互動中成長 不相信別人,技術差的只能做寫文件或做單元測試
任務分配 做大家不願意做的事 只會選自己喜歡的事做,困難的推給其他成員解決
問題回報 盡可能解決問題,也會及時向上回報狀態 掩飾問題,紙包不住火時就找替死鬼
代碼審查 相信 code review 的價值,重複發生的問題就寫到共享 blog 中 不做 code review,大家各自為政
挫折 吸收別人的經驗,提醒自己不重蹈覆轍 讓成員也經歷自己經歷的錯誤
新知 重視傾聽,接受 brain storming 對無聊的建言嗤之以鼻,偷取成員提出有趣的想法,往上回報。

關於挫折的部份,原作者覺得 bad leader 會讓成員也經歷自己經歷的錯誤,雖然 leader 該想辦法讓成員避開一些顯而易見的石頭,應該給新人一些必要的協助,但我認為人類有時候,就是會有「不經一事,不長一智」的狀況,沒有痛過就不懂得要如何成長,總有些時候,是其他人不管怎麼苦口婆心嘮嘮叨叨,可是當事者就是沒辦法體會別人好意的提醒的時候,這也只能讓事情發生,然後再回過頭來一起檢討。

原文把團隊成員都假設是個成熟的個體,能跟 leader 有著良好的互動,但世事並非一直都很如意,有時會遇到一些無法跟團隊融合的個體,發生這種狀況時,已經沒辦法討論誰對誰錯了,一個 leader 就該承擔起來,採取適當的行動,如果真的沒辦法解決,也只能壯士斷腕,避免問題擴散到其他成員身上,畢竟一個 team 一定會有個共用的習慣與潛規則,這是不容被質疑與打破的默契。

我認為自己針對團隊管理上需要下些決定時,心裡抱持著的最高指導原則是:「我希望大家都能發揮自己的優勢,逐日進步,以接受更多的挑戰。」挑戰不單指是更高超的技術,有時候是為了大家可能會遇到的下一個職業生涯階段需要做的準備與練習。這些動作會讓直接減輕我在一些專案上的 coding effort或是管理的 effort,感覺上像是我的工作內容減少了很多,其實我是把現在手上的工作交給適當的成員,然後設法包些其他的事情去做,有點像是一直擴大工作半徑的團隊同心圓的感覺。

因為我在專案中不是扮演實作的角色,而是在建立與定義商業邏輯與規格,分配工作,盯進度,確認成品的品質...等等。有幾次我以講笑話的方式脫口而出:「反正我也不用寫這些程式」,但其實講出來之後,卻又後悔覺得這樣講並不恰當,我不曉得有沒有人在意,「說者無心、聽者有意」。其實我有時也會想,能夠像以前一樣其實也很好,不講什麼話,專心coding,不用刻意注意每個成員的狀態,又能更深入熟悉基本的技能,不會停留在懂得皮毛的狀態。但我現在已經不能真的這樣做了,這樣反而有可能變成搶了成員表現自己的機會,也可能造成一些不信任感。

team leader 該要注意 member 的狀態,處理跟人有關的問題,也有技術上的基本條件,因為不懂技術,無法從根本上驅動團隊成員,也不知道怎麼拿捏評斷每一個人能力的標準,也無法適當地判斷並分配工作項目,否則也只是會造成團隊產出的成品,砸到自己的腳的狀況。

2014/01/17

AMR and WAV files Playback in Android, IOS and Browser


上篇 Android and IOS recording file 已介紹過如何錄製 AMR 音訊檔,接著就來介紹如何在 Android IOS and Browser 上播放 AMR 音訊檔

Android 播放 AMR 音訊檔


//使用系統播放器開啟音訊檔並播放
private void openFile(File aFile) {  
    Intent intent = new Intent();  
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
    intent.setAction(android.content.Intent.ACTION_VIEW);  
    String type = getMIMEType(aFile);  
    intent.setDataAndType(Uri.fromFile(aFile), type);  
    startActivity(intent);  
} 

//取得檔案類型
private String getMIMEType(File aFile) {  
    String end = aFile.getName().substring(aFile.getName().lastIndexOf(".") + 1, aFile.getName().length()).toLowerCase();  
    String type = "";  
    if (end.equals("mp3") || end.equals("aac") || end.equals("wav")  
            || end.equals("amr") || end.equals("mpeg") || end.equals("mp4")) {  
        type = "audio";  
    } else if (end.equals("jpg") || end.equals("gif") || end.equals("png")  
            || end.equals("jpeg")) {  
        type = "image";  
    } else {  
        type = "*";  
    }  
    type += "/*";  
    return type;  
}   

IOS 播放 WAV 音訊檔

ios 自 4.3 開始不支援 AMR 音訊格式,故播放前必須先轉檔,這裡示範轉成 WAV
AMR to WAV

//WAV轉AMR格式
int result = [VoiceConverter wavToAmr:self.wavFullName amrSavePath:amrFullName];
WAV 音訊播放

self.wavFullName = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"recorder.wav"];
    NSURL *tempUrl = [NSURL fileURLWithPath:self.wavFullName];
    NSLog(@"play tempUrl=%@",tempUrl);
    NSError *err = nil;
    //初始化播放器
    self.audioPlayer = [[AVAudioPlayer alloc]init];
    self.audioPlayer = [self.audioPlayer initWithContentsOfURL:tempUrl error:&err];
    if(err!=nil){
        NSLog(@"play err=%@",err);
    }
    //開始播放
    [self.audioPlayer play];
PS. 關於 Apple 官網說的支援 3gp 格式,我測試結果無法播放,播放器閃一下就消失了,但 mp4 測試播放正常,如果有朋友實現 IOS 3gp 檔案格式播放,也請不吝賜教,提供一下心得,謝謝! 

Browser 播放 AMR 音訊檔

這裡使用 quicktime 套件與 script 語法進行播放(若電腦未安裝 quicktime 系統會提示下載並安裝)

<script language="JavaScript" type="text/javascript">
function playAMR(filePath, width, height){
var pv='';
pv += '<object width="'+width+'" height="'+height+'" classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" codebase="http://www.apple.com/qtactivex/qtplugin.cab">';
pv += '<param name="src" value="'+filePath+'">';
pv += '<param name="controller" value="true">';
pv += '<param name="type" value="video/quicktime">';
pv += '<param name="autoplay" value="true">';
pv += '<param name="target" value="myself">';
pv += '<param name="bgcolor" value="black">';
pv += '<param name="pluginspage" value="http://www.apple.com/quicktime/download/index.html">';
pv += '<embed src="'+filePath+'" width="'+width+'" height="'+height+'" controller="true" align="middle" bgcolor="black" target="myself" type="video/quicktime" pluginspage="http://www.apple.com/quicktime/download/index.html"></embed>';
pv += '</object>';
document.write(pv);
}
</script>

References

2014/01/16

Audio Recording in Android and IOS

目前錄音在手機中是很普遍的功能,android 系統已內建錄音app,iPhone也有內建語音備忘錄,其他也有許多關於電話錄音的app出現。但如果今天您希望能將錄音檔傳給別人,尤其是跨系統的傳輸,如何讓您傳輸的檔案在對方的平台也能正常播放呢?下面就來介紹一下如何選擇錄音格式讓音訊檔可跨平台播放
Android支援的「錄音」格式:AMR、3GP...(詳情請見 Android Developer )
iPhone支援的「錄音」格式:AAC、WAV...(詳情請見 Apple Developer )
Android支援的「音訊播放」格式:AMR、3GP、WAV...(詳情請見 Android Developer )
iPhone支援的「音訊播放」格式:AAC、WAV、MP4...(詳情請見 Apple Developer )
IOS 不支援 AMR 音訊播放,必須轉成 WAV 等音訊格式才能播放,Android 支援 AMR 與 WAV 音訊格式的播放,但考慮到錄音檔案大小,故這裡選用 AMR 錄音格式

Code解說

Android AMR 錄音

//設定錄音檔名
SimpleDateFormat ff = new SimpleDateFormat("yyyy-MM-dd_hh_mm_ss");
String now = ff.format(new Date());
rfilename = "";

if (rfilename.equals("")) {
 rfilename = now + ".amr";
}
try {
 File SDCardpath = Environment.getExternalStorageDirectory();
 //路徑:/mnt/sdcard/download/2497_2013-10-12_01_19_00.amr
 File myDataPath = new File( SDCardpath.getAbsolutePath() + "/download" );
 if( !myDataPath.exists() ){
  myDataPath.mkdirs();
 }
 rfilePath = SDCardpath.getAbsolutePath() + "/download/" + rfilename;
 recodeFile = new File(rfilePath);
 
 mediaRecorder = new MediaRecorder();
 //設定音源
 mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
 //設定輸出檔案的格式
 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
 //設定編碼格式
 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
 //設定錄音檔位置
 mediaRecorder.setOutputFile(recodeFile.getAbsolutePath());
 
 mediaRecorder.prepare();
 
 //開始錄音
 mediaRecorder.start();

} catch (IOException e) {
 e.printStackTrace();
}
iPhone WAV 錄音( ios 自 4.3 開始不支援 amr 音訊格式,故選擇 WAV 檔)

//init
NSString *filename = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"recorder.wav"];
NSURL *url = [NSURL fileURLWithPath:filename];

NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
                          [NSNumber numberWithFloat: 8000.0],AVSampleRateKey, //采樣率
                          [NSNumber numberWithInt: kAudioFormatLinearPCM],AVFormatIDKey,
                          [NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,//采樣位數
                          [NSNumber numberWithInt: 1], AVNumberOfChannelsKey,//通道的數目
                          nil];
NSError *err = nil;
self.recoeder = [[AVAudioRecorder alloc] initWithURL:url settings:settings error:&err];

if(err!=nil){
    NSLog(@"err=%@",err);
}

self.recoeder.delegate = self;
self.recoeder.meteringEnabled = YES;
[self.recoeder prepareToRecord];

//啟用音訊會話
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayAndRecord error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);

//開始錄音
[self.recoeder record];
接著就可以將錄好的 AMR 或 WAV 檔案傳輸給對方瞜! 至於錄音檔的播放請見下篇 Play AMR and WAV files from Android IOS and Browser

References

2014/01/15

RabbitMQ Message Queue 使用心得

此篇是我們在開發系統過程中導入Rabbitmq的心得分享。

為何要使用Message Queue

我們可以將一個Job(例如將一則訊息存到資料庫裡、發一封email...等)以一個的訊息來描述並將訊息放到Queue裡面,再建立一個或多個Consumer來將Queue裡的訊息一一取出後做後續的處理。

這好比說將要出貨的貨物(Message)一直往倉庫(Queue)裡放,有一台或多台的貨車(Consumer)不斷的將倉庫裡的貨物取出後做出貨的動作,貨車出完貨後會再回去倉庫取貨物。如果貨車數量愈多,那麼倉庫裡的貨物愈能更快的取出處理。

假設我們的系統loading非常的大,我們可以一直增加伺服器執行Consumer來消化Queue裡面的Message

使用情境

想像一下像Line的App,假設我們要傳一則訊息到某一個群組內的所有人,當server收到這樣的request時需做下列四項事情

  1. 將此訊息log到db裡(方便之後查詢)。
  2. 從db查詢該群組下有哪些人,例如有5個人,其中有4個人已允許加入群組邀請,1個尚未回覆。
  3. 將訊息傳送給4個已經加入群組的人,假設1個人目前沒有開啟App
  4. 發送Notification(GCM or iOS notification)給這1位不在線上的人

如果我們只用一個執行緒來完成上述所有的事情,那麼使用者在App中按下送出訊息的按鈕後必需要等待server依序執行完這4件事情後才會收到回應,但這樣的回應時間會過長而不被現在的使用者可接受。

加入Message Queue設計

若搭配Message Queue的話可使用下列的設計方式來加快系統的回應時間,請參考下圖 sample

系統建立下列3個Queue(紅色方塊)與3個Consumer(灰色)。註:Consumer是獨立的執行緒在執行。

  • Log Queue負責暫存要存到db的群組訊息,Consumer A會從Log Queue取出群組訊息並做寫入db的動作
  • Chat Queue負責暫存要發送的群組訊息,Consumer B會從Chat Queue取出群組訊息後至db取出該群組的人員名單並做傳送訊息的動作,傳送過程中如發現收件人的App沒有打開的話則將訊息丟至Notification Queue
  • Notification Queue負責暫存要發送GCM或iOS的訊息,Consumer C會從Notification Queue取出群組訊息後做發送notification的動作
  • Exchange(type=fanout)自動將訊息送給所有bind到此exchange的queue(請參考此篇文章)

以下說明流程

  1. 甲送一則訊息至server,該訊息含有發訊人、收訊群組、訊息內容…等資訊。
  2. server收到這個request後會將訊息丟進Exchange,此時server就馬上就回response回去了,接下來工作都交由Exchange, Quene, Consumer處理。
  3. Type為fanout的Exchange會將訊息同時傳送給Log Queue、Chat Queue
  4. Consumer A會從Log Queue取出群組訊息並做寫入db的動作
  5. Consumer B會從Chat Queue取出群組訊息後至db取出該群組的人員名單並做傳送訊息的動作,傳送過程中發現收件人丁的app沒有打開則將要給戊的訊息丟至Notification Queue
  6. Consumer C會從Notification Queue取出群組訊息後做發送notification的動作

註:步驟4~6是各別的執行緒在處理,並沒有順序關係

結語

可能有人會想到,如果將訊息存放到db,另外再建立執行緒定時的向db query資料是否也能達到相同的功能?這跟使用Message Queue的差別會在於如何得知會有新的資料要處理?使用Message Queue架構的話當Queue有資料時會自動的通知Consumer,如果用db的話則必需自行的定時向db做查詢的動作,這可能會比較耗resource。

其實不使用Message Queue的話也是可以做的出所要的功能,但若使用的話可以讓整個系統增加效率與擴充性。

我眼中的 pro programmer

一個 pro programmer 工作者,在 team leader 的眼中,存在著某些決定性的條件,而以下是我個人認定,專業 Programmer 所該存在的幾項條件。

smart copy-and-paste coder

大部分的 programmer 都必須上網翻閱問題與資料,有時候可以直接找到其他人做的解決方案,有可能是個簡短的 hack,或是套用某個 library。

如果有製作一個完整功能經驗的programmer,不管是 library或是獨立的 APP,通常會發現,如果開發的時候,手上只有一份規格文件,以這樣的基礎去開發時,會遭遇到許多困難。這時如果已經有人把功能做出來了,直接閱讀他的範例,問題就解決一大半了。

smart copy-and-paste 並不是看到什麼就照抄,除了一些特殊的 hack codes 之外,搜尋到解決方案或 coding 方法之後,必須要先看懂別人寫什麼,然後再「適當」地套用在自己的專案上,不懂 code 的內容,閉著眼睛照抄,可能會帶來一些無法理解的副作用,還會落下一句「我不知道為什麼會這樣!」,不懂得前因後果就照抄是絕對不行的。

bug-free codes producer

基本上如果是 Java 這種 Strong Typed 語言,程式在編譯時,就已經檢查掉不少基本的語法錯誤了,而 bug 的發生,最常見的原因就是 programmer 「偷懶」,例如:method 的參數會發生 NullPointerException,就是 programmer 偷懶沒有寫檢查,因為他自己「假設」這個欄位不會是 null,或是一些參數,送入了預期之外的數值。

多寫幾行程式,就可以降低發生問題的機會,產出品質更高的 codes,如果實作時隨時都想偷懶,自己做了太多「假設」,bug-free codes 永遠都在遙遠的夢想中,尤其是business method 的界面與網頁 API 界面的參數限制。

有個概念是,越多程式碼,發生問題的機會越高,這也沒錯,但那是指業務邏輯的部份,在做程式分析時,必須想辦法釐清業務邏輯,以最精簡的判斷方式撰寫,一堆繞來繞去又重複檢查的程式邏輯,既會讓人讀不懂,又會增加發生錯誤的機會。

defect digger

每個系統都有自己的歷史包袱,有很多商業邏輯跟假設隱藏在裡面,資料庫的資料,也會有很多邏輯上的相關之處,例如,當 programmer 被分配到實作「為系統實作多國語言」這個功能時,可能只會想到,要修改界面上顯示的文字,但可能還有錯誤訊息,錯誤頁面要處理,錯誤訊息有可能是寫在 Server 直接傳送給 Client,或是系統發送給使用者的 email或通知,這些全部都得考慮多國語言。

重要的是,programmer必須要想到,因為這個功能,有可能影響到的所有其他的功能,在實作該功能的時候,要同時去檢查,有沒有影響到這些功能,或是反過來詢問 leader,這可能是一開始沒有考慮到的相關問題,該怎麼處理,把需求處理做得更完整。

考慮越周全,自然就能挖到更多系統的 defects。 如果 prgrammer 想說,這是 leader 下達的指令不夠周全,我只要照著要求的內容,去改寫程式就好了。這不是專業 programmer 該有的想法,因為 programmer 會是直接接觸程式碼的人,他可以從程式碼,去反向找到系統中所有相關的功能,熟悉系統商業邏輯的人,雖也可以做到類似的事情,但總是間接想到,也只能說「有可能」會影響到某某功能。

problem resolver/terminator

程式的問題可以分成兩種,一種是 bug,程式跑一跑,出現了奇怪的結果,一種是新功能,沒有人做過,不知道怎麼寫。

當程式遇到bug時,第一時間要做的,是懷疑自己是不是有考慮不周全的地方,也要思考,是不是自己的想法與寫法,或是程式的邏輯順序有問題,以此為前提,再往下深入去尋找,是不是有別的方法,可以解決這個問題。

要能被交代處理新功能,某方面就代表說,這個 porgrammer 是受信任,可以找到解決新問題的方法。一個能解決問題的人,總比一直製造問題、產生bug的人來得討人喜歡,且能受重用。

good communicator

當遇到問題你不知道怎麼處理時,除了問google之外,最快的方式是先問問同事,可能已經有人處理過類似的狀況。問問題,不是把問題丟出去就好了,你必須要讓其他人感受到你的責任感,問的時候,要加上自己的觀察,聽的時候,要思考並從回答中,找出模糊地帶,再繼續追問下去。

定期會議或是每天的站立會議,必須要把自己的狀態、進度,有沒有遇到特別的狀況卡住很久了,需不需要幫忙,這些重點事項言簡意賅地說明清楚。

如果有機會面對了客戶,為求專案順利在跟客戶打交道拉關係的前提下,必須跟客戶保持適當的距離,客戶的需求一定要經過 PM 才能接受。

心裡有什麼疙瘩,或是覺得團隊有什麼狀況,要直接跟 leader 或其他同事溝通,悶在那邊是無濟於事的。

aggressive learner, conservative implementater

積極地學習新的技術並了解新的工具與概念,但在團隊合作上,卻要採取比較保守的方式,而不是學了一樣新東西,就想在專案上實驗並套用上去。

新技術相對就帶來了新的風險,當專案時程游刃有餘,或是在前期的研究階段,新技術的測試與實驗是必要且有效的,但在處理舊專案時,必須適當地遷就舊有的程式碼,因為這些程式都是經過多次測試驗證的結果,沒有相當大的好處,或是有絕對的理由,不能輕易放棄既有的東西,但相對地,一旦確認有足夠要放棄的理由,也要義無反顧,適當的提出建言與舉證,強烈地建議要除舊佈新。

good job handler

明確掌握自己的工作進度與心理的狀態,如果有卡住一段時間的問題,主動去問問同事或 leader,自己悶著頭去做,只會讓團隊其他人搞不清楚你在做什麼。做的事情、研究的東西都必須讓團隊確實掌握。

除了自己的狀態,還要注意其他人的進度與狀態,整體的進度必須跟著團隊前進,如果自己落後了,要及時反應狀況,由leader決定需不需要其他人的支援,當然進度超前時,也要及時回報,由leader分配其他的工作。

industrious but agile documentator

在實作某些功能前,前期的業務分析結果,還有一些開發期間用來測試與驗證的 sql & test scripts,必須要確實地紀錄在專案的文件區裡面,而程式碼中,撰寫註解要勤快,撰寫文件與註解並不是無限上綱,規定每一個method、每一個參數、每一個邏輯判斷都要寫上註解,而是要在適當的時機去做這件事,當發覺自己花了一段時間,才釐清該怎麼撰寫時,就是個明確要寫下文件的時機點,寫文件,沒有標準的格式,要以自己認為最適當的方法記錄下來。

rush but careful stepper

積極且迅速的工作效率,但同時也要步步為營,把每一件相關的工作都做好。

如果 leader 每一次來問,都回應說做好了,但是「做好」多少的程度雙方的認知會有落差,通常 leader 認定的做好了,是指都全部測試過,都正常了,而 programmer 認定的做好了,是我把這個 task 完成了,這個 task 也測試過了,但改完這個會不會影響到其他地方,這就跟我無關了。

leader 說的都是針對整個系統,programmer 說的常都是針對某一個工作,因此當 programmer 回應給 leader 時,要清楚地告知有沒有測試,測試了多少項目等等附帶的資訊與條件,leader也會根據這些資訊,得到下一個階段工作的重點項目,或是決定某一個大範圍重新測試的時間點。

結語

當個 team member 跟 leader 的心情與想法是截然不同的,「換個位置就會換個腦袋」,這種情況是無可厚非的,只能說,不管自己在什麼位置,要時時提醒自己,換個角度去看這些事情,自然就可以轉換自己的心情與想法。

其實 team leader 掌握團隊的時候,同時也必須扮演 boss 的手下的角色,也需要一些角色轉換的技巧,才有辦法在這兩者之間,動態自由地轉換。

下次或許該找個人,談一談在他眼中一位 pro team leader 是什麼樣子。

2014/01/14

Android zip解壓縮後,讀取資料夾下的檔案清單做驗證

這兩天剛好寫到需要從網路下載壓縮檔案存在手機裡,並把zip壓縮檔解壓縮。 因為會儲到SD Card,所以先要在AndroidManifest.xml 加入 "android.permission.WRITEEXTERNALSTORAGE"權限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>

以下為解壓縮的程式碼:

String srcPath = Environment.getExternalStorageDirectory().toString() + "/download/data.zip";
String destPath = Environment.getExternalStorageDirectory().toString() + "/download/data/";

BufferedOutputStream bufferedOutputStream = null;
FileInputStream fileInputStream;
try {
    fileInputStream = new FileInputStream(srcPath);
    ZipInputStream zipInputStream = new ZipInputStream(
                new BufferedInputStream(fileInputStream));

    ZipEntry zipEntry;

     //讀出壓縮檔裡的檔案
     while ((zipEntry = zipInputStream.getNextEntry()) != null ) {

         String zipEntryName = zipEntry.getName();

         File file = new File(destPath + zipEntryName);

          if (!file.exists()) {

                if (zipEntry.isDirectory()) {
                    file.mkdirs();//如果是目錄先建立
               } else {
                    //檔案則另存

                    byte buffer[] = new byte[ 1024];
                    FileOutputStream fileOutputStream = new FileOutputStream(
                               file);
                    bufferedOutputStream = new BufferedOutputStream(
                               fileOutputStream, 1024);

                     int count;
                     while ((count = zipInputStream.read(buffer, 0,
                                1024)) != -1) {
                         bufferedOutputStream.write(buffer, 0, count);
                    }
                    bufferedOutputStream.flush();
                    bufferedOutputStream.close();
               }
         }
    }

    zipInputStream.close();

    //驗證檔存不存在
    checkFile();

} catch (FileNotFoundException e) {
     // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IOException e) {
     // TODO Auto-generated catch block
    e.printStackTrace();
}

以下的程式碼是用來讀取資料夾路徑裡的檔案清單,剛好可以拿來驗證解壓縮檔案是否成功。

private void checkFile(){
    String filepath = Environment.getExternalStorageDirectory().toString()+"/download/data/";
    Log.d("Filepath", filepath);
    File file = new File(filepath);

    File files[] = file.listFiles();
    Log.d("Files", "Size: "+ files.length);
    for (int i=0; i < files.length; i++){
        Log.d("Files", "FileName:" + files[i].getName());
    }
}

filepath 是資料夾路徑,file.listFiles()是取出檔案清單。

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),然後就可以達成你的需求了!