[上課筆記] Learning How to Learn


時間就這麼多,除了在有限的時間內安排時間學習外,我還想要有效率地學習(學得快又記得久)。

上個月花了四週上完這堂課,把四週的重點寫下來,讓沒時間去上課或者是有興趣的人先快速瞭解一下。以下是我用到的教材,上課前可以先看一下講者去 TED 的快講,不合拍就不用去上課了。書的部分基本上跟上課內容完全一致,買或不買都沒差,我比較喜歡紙本所以就買了。

之前對學習技巧的書都很有興趣,這四週課程有把以前看過的技巧建構成一個系統流程,試著寫一下我的理解,下面的部分項目“學習”換成“工作“也適用。
  1. 制定學習計劃 // 有目標,執行,無論如何就是有進步
  2. 安排時間學習 // 養成習慣,就不容易停滯下來
  3. 營造好的學習環境 // 專心效果才會好
  4. 配合蕃茄鐘專注學習 // 人一天的專注力是有限的,跑一段休息一段,才跑的遠。
  5. 針對學習內容先鳥瞰再詳讀 // 先建構輪廓,有助於拼湊章節間的前因後果。
  6. 做整理 // 讀過就只是讀過,用筆寫能幫助記憶。
  7. 隔一段時間複習 // 一次念多次沒有比過一段時間重唸一次的效果好。
  8. 與人分享學習成果 // 能說給別人聽才代表自己有讀懂了
  9. 回到第一項...
四週筆記如下:

第一週
  • 專注工作一陣子(大腦進入專注模式),然後休息一下子(大腦進入發散模式),讓腦袋消化後學習效率更好。工具:蕃茄鐘。
  • 一天重念多次,不如每隔幾天重念一次。間隔一段時間重複練習,可以將知識深刻地記在大腦的長期記憶中。
  • 遇到難解問題,先放下做些別的事情再回來看,讓大腦有時間消化。
  • 睡眠充足有助於學習。
  • 不拖延的最好作法就是趕快動手做。
  • 邊做邊學效果最好。
  • 運動/放空有助於大腦產生新想法。
  • 有熱情就能堅持。

第二週
  • chunk 像是建構知識的磚塊,基礎打穩學的知識才牢靠(記得久、內化成常識)
  • 建立 chunk 的三步驟:專注 > 理解基本概念 > 多練習。
  • 不要過早進入細節,先知道輪廓再進入細節(ex:看書前先看大綱)。
  • 回想的效果大於反覆閱讀。複習前不看課本想想自己還記得多少,然後再重新看一次課本。
  • 畫線不等於真正懂了,要做重點筆記、測驗。

    第三週
    • 持續有規律地學習。
    • 熟能生巧。
    • 養成“好”的習慣(ex:固定時間就去把功課做完、下班前整理好隔天要做的項目)。
    • 注意力放在過程而非結果,不要去想還要多久才能做完,在規定的時間內專注做就對了,這可以減少心理上的壓力。
    • 如何對付拖延症:
      • The cue > 了解自己何時開始拖延的訊號 > 移除這些訊號 > 營造不分心的環境
      • The routine > 制定計畫營造自己不分心的策略
      • The reward > 達成後獎賞自己
      • The belief > 相信自己可以做到 + 找夥伴彼此互相督促
    • 制定每週計畫,列出達成計畫需要完成的項目。
    • 每日待做清單,寫下來就不會一直想,完成了就畫線,這種回饋機制有助於建立成就感。
    • 用影像圖片加強記憶。
    • 用自己熟悉的事物做關聯(ex:要記1999,可以與1999末日預言關聯)。
    • 記憶宮殿:人的大腦對於空間的記憶力特別好,所以可以利用自己的熟悉環境作為場景加入要記憶的事物。

    第四週
    • 多運動,運動也會產生新的神經元,能幫助大腦學習。
    • 用比喻的方式來學習新概念。
    • 主動學習比被動學習的效果好(自己想學而非別人逼你)。
    • 改變對考試的態度,不是懼怕到而是興奮(驗證自己到底學了多少),有助於提升考試表現。
    • 考試時先看難題對其有個印象,然後馬上切換去做簡單的題目,此時雖然專注在簡單的問題,但大腦已經在背景處理難題。

    JCCONF 2016 筆記


    會場有提供幾個黑板讓大家回答問題,還蠻有趣的。

    • IDE:以 Eclipse 和 Intellij 為大宗,Intellij 還不是寫成 Android Studio。
    • 第二語言:以 Python 最多,Groovy 也不少。
    • Server:什麼都有看到但就是沒有 JBoss Server。
    這次參加心得是
    1) 一個團隊就是要重頭包到尾 // 不分開發測試DB維運
    2) 自動化要做的好,才有時間精力從頭包到尾
    3) Spring Boot 看來能快速開發後台,直接包好 Web Server 一個指令執行,不再走傳統部屬至 Server 的流程 // 我想要繼續走後台的話要來學一下
    4) 覺得現場人頭廠商徵才的方式蠻鳥的,覺得最好的方法就是好好把內部的 API 開源出來,派開發者出來演講,想用的人就會上去貢獻程式碼,這些人就是最有潛力的人才。 // 我絕對沒說是東森
    5) 新竹台北通勤好累,來回大概就4小時去了,回來安全帽還被幹走。 // 幹
    6) 回來想說隨便寫,沒想到大概花了又4小時才整理出來。 // 一場鬥陣都還沒開始玩
    7) 沒帶筆電去是對的,沒事做只能認真寫筆記。

    下面是準備回公司分享的投影片, 礙於內部分享不寫太多字太深入,所以一個議題都只有一張投影片,眼睛掃過去有興趣的話大會有提供 hackpad 可以看 ,會後也應該會放上影片,到時我也會更新至這份投影片中。

    用程式碼介紹 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 的概念外,也針對各種運算子有很詳細的說明。

    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 的時間大概比寫扣的時間還要多,只能說免費的最貴。

    解譯 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 或文字編輯器來看。