2016年6月27日 星期一

用程式碼介紹 RxJava

前言

RxJava 也強迫自己用兩個月了,除了自己的專案外,工作的專案也拿來改寫原本很難看懂的邏輯。這篇建議就當作一篇引導文或是心得文看看,有興趣繼續研究的朋友,網路上已經有很多中/英文資源,不怕沒地方學只怕你不學。

安裝

要導入 RxJava 很簡單,gradle 下一行即可,沒用 gradle 也只要抓一個 rxjava.jar 檔案,再引入專案就可以了。
compile 'io.reactivex:rxjava:x.y.z'

不用綁定 JVM 版本,不限定是 Android 專案還是純 Java 專案,只要一個 jar 檔案就能使用 RxJava。

什麼是 RxJava

直接舉幾個例子,來說明什麼是 RxJava,又為什麼我要用 RxJava,看了程式碼應該比用文字描述來的有感覺。這些例子也是我在專案中反覆用到的程式碼來改寫的。

Android 中需要更新 GUI 的耗時工作

取代 AsyncTask 在 Android 上呼叫 耗時工作,呼叫前更新 GUI,呼叫完再更新 GUI。

對於耗時工作回傳的結果,在 RxJava 中還能做進一步處理,就像工廠的流水線,從頭到尾能看到做哪些處理。想想如果要用第一個耗時工作的結果,去呼叫第二的耗時工作,在 RxJava 就是單純再多一個站點(下面程式碼中的 map()),如果沒有 RxJava 這段程式碼要怎麼寫?寫出來的程式碼會不會很難看?
Observable.just("input")
    .doOnSubscribe(new Action0()
    {
        @Override
        public void call()
        {
            // 在觸發耗時工作前更新 GUI
            // 比如顯示進度條
        }
    })
    .subscribeOn(Schedulers.newThread()) // 設定耗時工作在新的 Thread 上執行
    .map(new Func1<String, String>()
    {
        @Override
        public String call(String input) 
        {
            // 執行耗時工作
            // 比如是一個 Web API 呼叫
            return new WebApi(input);
       }
    })
    .map(new Func1<String, User>()
    {
        @Override
        public User call(String input) 
        {
            // 處理耗時工作回傳的結果
            // 比如 Web API 回傳的 Json 字串轉成 POJO
            return new Gson().fromJson(input, User.class);
       }
    })
    .observeOn(AndroidSchedulers.mainThread()) // 設定非同步工作完成後回到 Android GUI Thread 上更新 GUI
    .subscribe(new Observer<User>() 
    {
        @Override
        public void onNext(User user) 
        {
            // 更新 GUI
            // 比如在 TextView 上顯示 User 的名稱
        }

        @Override
        public void onCompleted()
        {
           // 如果沒有發生例外
           // 比如關掉進度條
        }

        @Override
        public void onError(Throwable error)
        {
            // 如果發生例外
            // 比如關掉進度條後跳出警告視窗
        }
    }); 

移除會造成 Callback Hell 的程式風格

延續上個範例提到的,如果要用第一個耗時工作的結果(用帳號密碼呼叫 Web API 得到 token),去呼叫第二的耗時工作(用 token 呼叫得到使用者的訊息清單),最後印出訊息清單。

在 Java 中考慮到呼叫 Web API 是一個耗時工作,所以大部分設計的呼叫參數常會多帶一個 Handler(Observer Pattern),被呼叫端做完耗時工再後,再利用 Handler 讓呼叫端知道該做什麼,程式碼大致會長成這樣:
webApi.getToken("user", "password", new DefaultHandler()
{
    // 第一個 Web API 呼叫成功
    @Override
    public void onSuccess(String response)
    {
        webApi.getMessageList(response, new DefaultHandler()
        {   
            // 第二個 Web API 呼叫成功
            @Override
            public void onSuccess(List<String> response)
            {
                // 呼叫成功
                for (String message : response)
                {
                    // 印出每個訊息
                }
            }

            // 第二個 Web API 呼叫失敗
            @Override
            public void onFailure(Exception exception)
            {
                // 印出失敗訊息
            }
        });
    }

    // 第一個 Web API 呼叫失敗
    @Override
    public void onFailure(Exception exception)
    {
        // 印出失敗訊息
    }
});

有沒有看到程式碼一直往內凹,這個就是 Callback Hell,雖然程式碼還是可以動,但就是不夠簡單不夠整齊。如果今天採用 RxJava 來設計,RxJava 本來就是 Observer Pattern,前一個方法呼叫完成,才會進入下一個方法,程式碼大致會長成這樣:
Observable.just(webApi.getToken("user", "password"))
    .flatMap(new Func1<String, Observable<String>>()
    {
        // 第一個 Web API 呼叫成功
        @Override
        public Observable<String> call(String token)
        {
            try
            {
                // 第二個 Web API 呼叫成功
                return Observable.from(webApi.getMessageList(token));
            }
            catch (Exception e)
            {
                // 第二個 Web API 呼叫失敗
                // 將錯誤丟出來
                throw Exceptions.propagate(e);
            }
        }
    })
    .subscribe(new Observer<String>() 
    {
        @Override
        public void onNext(String message) 
        {
            // 印出每個訊息
        }

        @Override
        public void onCompleted()
        {
            // 如果沒有發生例外
        }

        @Override
        public void onError(Throwable error)
        {
            // 第一個 Web API 呼叫失敗
            // 第二個 Web API 呼叫失敗
        }
    });

當要串的 Web API 越多的時候,帶 Handler 產生的巢狀架構,會讓程式碼越來越複雜不容易看懂,而用 RxJava 寫法的程式碼只會變長。

執行固定次數的動作

有時想要執行某個動作10次,一般都是要寫個 for 迴圈,現在不用羨慕 Ruby/Python 有很簡單的寫法,現在我們也有了。
Observable.range(0, 10).subscribe(new Action1<Integer>() // 0, 1, 2, 3...9
{
    @Override
    public void call(Integer item)
    {
        // 執行動作
        System.out.println(string);
    }
});

幹,好像也沒有比 for 迴圈少打幾個字,這個就必須要你有學好 lamda 了。
Observable.range(0, 10).subscribe(item -> System.out.println(item)); // 0, 1, 2, 3...9

資料集合處理

寫後台寫邏輯常常會需要處理資料集合,今天如果我們有個字串集合,要做以下處理(依序):
  • 只取出有帶 "boy" 的字串
  • 取出字串中的數字
  • 將數字轉成英文(1變成 "one")
  • 只取最前面兩個符合條件的英文
廢話不多說,直接上程式碼最有感覺。
public class HelloRxJava
{
    public static void main(String[] args)
    {
        List<String> list = new ArrayList<String>(Arrays.asList("boy_4", "boy_1", "girl_1", "boy_2", "boy_3", "girl_2"));


        // 傳統寫法:
        int take = 2;
        int i = 1;
        for (String string : list)
        {
            if (isBoy(string))
            {
                if (take < i)
                {
                    return; // Jump!
                }

                System.out.println(numberToEnglishNumber(getNumber(string)));
                i++;
            }
        }


        // RxJava 沒有 lamda 的寫法:
        Observable.from(list)
            .filter(new Func1<String, Boolean>()
            {
                @Override
                public Boolean call(String string) 
                {
                    return isBoy(string);
                }
            })
            .map(new Func1<String, String>()
            {
                @Override
                public String call(String string) 
                {
                    return getNumber(string);
                }
            })
            .map(new Func1<String, String>()
            {
                @Override
                public String call(String string) 
                {
                    return numberToEnglishNumber(string);
                }
            })
            .take(2)
            .subscribe(new Action1<String>()
            {
                @Override
                public void call(String string) 
                {
                    System.out.println(string);
                }
            });


        // RxJava 有 lamda 的寫法:
        Observable.from(list)
            .filter(string -> isBoy(string) == true)
            .map(string -> getNumber(string))
            .map(string -> numberToEnglishNumber(string))
            .take(2)
            .subscribe(string -> System.out.println(string));
    }




    /**
     * 將阿拉伯數字轉為英文字
     */
    private static String numberToEnglishNumber(String string)
    {
        if (string.equals("1")) return "one";

        if (string.equals("2")) return "two";

        if (string.equals("3")) return "three";

        return "oops!";
    }

    /**
     * 取得字串裡的數字
     */
    private static String getNumber(String string)
    {
        return string.substring(string.indexOf("_") + 1);
    }

    /**
     * 字串是否包含 boy
     */
    private static boolean isBoy(String string)
    {
        return string.startsWith("boy");
    }
}

為求範例簡潔,一些邏輯都抽取成 private 方法。可以發現傳統寫法雖然不長,但有巢狀 if 的關係,需要花一點時間才能了解程式碼的意圖,而用 RxJava 的寫法,基本上根本就是把剛剛條列的動作依序執行,而用 lamda 的方式寫的程式碼更是很容易就能看出程式碼的意圖。

小結

本文說明了:
  • 無論是單純的 Java 環境,還是在 Android 上,只要一個 jar 就能開始用 RxJava。
  • RxJava 雖然讓程式碼變長了,但讓程式碼的意圖變得比較好理解。
  • 如果你會用 lamda,你的程式碼就不會那麼長。
  • RxJava 提供的多種運算子,讓我們很容易就能做出"取出最前面兩個元素"、"Retry 10次"之類的邏輯。
我有把我學習過程中寫的範例碼都放上 github,有興趣的人可以拉下來跑,基本上都是一些釐清觀念的 Code。另外推薦這篇 RxJava 文檔中文版,除了介紹 RxJava 的概念外,也針對各種運算子有很詳細的說明。

2016年2月23日 星期二

JBoss 的推播方案 - Unified Push Server(現在叫 AeroGearPush)

前言

2016年獨立開發者最慘痛的第一個消息,大概就是臉書併購的 Parse 要收起來不玩了,我手上的專案用到 Parse 的推播功能也中鏢了,想到要自己把 GCM 接起來真的是一個頭兩個大。

簡單說明一下 Parse 是一個 BaaS(Back end as Service) 服務,可以想成臉書幫你養了一台提供資料庫/會員註冊/排程工作/推播的雲端主機,對開發者來說省去了架設/部署的工作,能夠專注於開發。

Parse 預計明年一月要收掉,整個 Parse 也已經開源出來,如果你本身也熟悉 Node.js 的開發生態,基本上可以整包拿下來架站。我最終選擇是改用 JBoss 提供的推播方案,下文大概介紹一下架構、功能,詳細實作的程式碼因為會隨著改版而失效,這邊就不會寫上程式碼。

JBoss Unified Push 架構

JBoss 的推播方案一開始叫做 Unified Push,現在就整進 AeroGearPush 裡頭(算是其中一個子專案),整個程式碼也有開源出來,其實是一個跑在 JAVA EE Server 上的 war 檔。

因為我只有實作 Android Push 這一塊,以下就用 GCM 為例說明。Push Server 封裝了 GCM,如果沒有 Push Server,我們就必須自己去管理手機從 GCM 取得的註冊資訊。當有跨手機平台的推播需求的話,我們也只需與 Push Server 介接,Push Server 負責實作推播至不同手機平台的功能。

以下說明上圖每個元件的作用:

  • Push Server:收集所有手機傳回的註冊資訊。發送訊息給各平台的推播系統,再推播至手機上。
  • Application Backend Server:實作 APP 的後台邏輯,當有推播需求(ex:商品上架、有新事件)時透過 SDK 將訊息傳給 Push Server。
  • Push Networks:就是指 Android / iOS 原生的推播系統。
  • 手機:除了接收訊息外,最重要的將從原生推播系統取得註冊資訊傳給 Push Server。以 Android 來說,透過 SDK 將從 GCM 取得的 token 傳給 Push Server。


環境架設

除了把 Push Server 架起來外,上面可以看到手機 APP 和後台 Server 還會需要用到以下 SDK:
  • aerogear-android-push (用來跟 PushServer 註冊裝置)
  • unifiedpush-java-client (包裹 Web API 用來將要傳送的訊息交給 PushServer 推播)。
很多程式碼,名字變來變去,版號很多,這就代表有很多雷可以踩,建議先裝好 Push Server,裝好以後在 Web GUI 把推播相關的參數設定好,在 Web GUI 上有自動產生樣本程式碼的功能,這時候再來去抓最新 SDK,如果找不到類別或錯誤就降一版 SDK 試看看。我也是花了一點時間才找出來可以動的組合。

建議的步驟如下:

  1. 安裝 Push Server。
  2. 在 Push Server 上建立 Application。
  3. Application 下設定 variant,variant 主要用來介接 iOS/Android 推播系統,介接的資料欄位需要從 iOS/Android 推播系統後台得到。
  4. 此時,你 Push Server 上應該有一個 Application,一個以上的 variant。
  5. 利用 Push Server > Application > variant 產生回傳註冊資訊至 Push Server 的範例程式碼,整合至手機 APP 程式碼,看看與下載的 aerogear-android-push 相不相容,不相容就找別的版本。
    1. 開啟手機 APP,利用 Push Server 的 WEB GUI 觀察手機有無註冊成功。
    2. 利用 Push Server 的 WEB GUI 發送訊息,驗證 Push Server 可以將訊息推到手機上。
  6. 利用 Push Server > Application 產生推播訊息至 Push Server 的範例程式碼,整合 APP 後台程式碼,看看與下載的 unifiedpush-java-client 相不相容,不相容就找別的版本。
    1. APP 後台發訊息給 Push Server,驗證可以透過 Push Server 將訊息推到手機上。


其他

關於 Push Server 的註冊部分,看到的寫法是每次打開 APP 或是某個頁面就重新註冊,這部分觀察資料庫,除非重新安裝 APP 否則上傳的註冊資料(GCM token)都是一樣的。解除註冊部分,看到的寫法可能寫在使用者勾選的開關上(讓使用者決定要不要收到推播),或是離開某個頁面就解除註冊。以 GCM 來說,唯一知道這裝置已經解除 GCM 註冊的時機點是在推播的時候,然後如果是自己寫的 Push Server 就可以將這些裝置清掉,但 JBoss (我用的版本是1.0.0 beta 2)的做法是沒寫這塊邏輯,所以必須自己處理,我自己的做法是定時下 SQL 去清除一個月以上沒有向 Push Server 回傳註冊資訊的裝置。

最後還是想唸一下,紅帽的文件不是寫不好寫不夠多,是整理的很沒系統,這次找文件找 SDK 的時間大概比寫扣的時間還要多,只能說免費的最貴。

2016年2月11日 星期四

解譯 Android APK

前言

最近有個需求需要看一下別的 APK 是怎麼做的,原本以為有現成的工具應該蠻簡單的,沒想到命令列苦手跟一些不熟悉的技術問題還是把我弄的很慘,寫個筆記記錄一下。

透過本文,可以知道我用了哪些工具跟這些工具的功用,最後我把所有步驟寫成一個 Groovy Script ,我會簡單介紹用法,透過這個 Script 最終可以得到 APK 所有的資源檔案、java 檔案、或是一包 jar 檔案。

工具

透過我走過的流程順便帶出我使用的工具,APK 轉換的結果,對我來說重要的就是資源檔案(layout、語系...)和程式碼,我嘗試過的三個流程如下(最後當然是用最後一個流程):

APK -> 透過 Apktool 解出來 -> 可以取得資源檔案,但只有 Smali(VM 上的虛擬語言,語法介於 Java 和 組合語言之間) 可以看但要花很多時間看。

APK -> 透過 dex2jar 直接把 APK 轉成 jar -> 沒有資源檔案,當 APK 裡頭有不只一個 dex 檔案時它只會解第一個 dex 出來,代表原始碼會缺少。

APK -> 改副檔名為 zip 解壓縮 -> 可以取得資源檔案和 dex -> 透過 dex2jar 把所有 dex 檔轉成 jar -> 解壓縮所有 jar 檔會得到所有的 .class 檔案 -> 把所有 .class 組成一個單一 jar 檔就可以用 JD-GUI 來看原始碼,或是透過 JAD 將所有 .class 轉成 .java。

怎麼使用我的 Script 

問我為何不寫 linux script 就好,我是有寫一版 linux script,但條件判斷和檔案搜尋結果再利用在 linux script 下真的是很難拼出來,所以我讓 groovy 做他擅長的部分,但執行指令還是會用下指令的方式。為什麼不用 python,因為我主力寫 Java,Groovy 跟 Java 語法相容。

Script 放在 github,所有會用到的工具就直接包含在專案裡了,使用環境是在 MAC 上,當然要先裝好 groovy 環境,然後修改 script 中 APK 的指定路徑,執行完後所有結果都會放在桌面上的 output 資料夾。

資源檔可以去 output/apktool 下找到。最終完成的單一 jar 檔可以去 output/onejar 下找到,用 JD-GUI 直接開這個 jar 就能看到原始碼。所有轉出來的 .java 放在 output/jad 下,這樣就能用 IDE 或文字編輯器來看。

Related Posts Plugin for WordPress, Blogger...