# Group3 Discussion I. Architecture Components and Lifecycle --- 主要包含四組 Library: * Lifecycle:Lifecycle-ware components(生命週期感知元件)包含在 android.arch.lifecycle 套件中,基本上就是當元件(Activity or Service)的生命週期發生變化時,生命週期元件即會自動收到通知並觸發相對應的動作,之前若開發者需要根據元件的生命週期執行相對應的動作時都必須寫在元件的對應方法中。最常見的例子為旋轉螢幕時需要儲存資料,不是寫在 onPause 方法就是寫在 onSaveInstanceState 方法。 但這種寫法最直接的缺點就是增加 Activity 的大小,讓 Activity 變得越來越大。 生命週期感知元件可以根據 Activity 或 Fragment 的當前生命週期狀態自動呼叫其行為。 https://blog.csdn.net/hyc1988107/article/details/81129744#lifecycle%E7%B0%A1%E4%BB%8B https://medium.com/jastzeonic/i-o-2018-android-architecture-component-%E9%82%A3%E4%B8%80%E4%BA%9B%E6%96%B0%E6%9D%B1%E8%A5%BF-4cb4d8b04960 * ViewModel:綁定畫面與資料 * LiveData:即時且自動更新取得的資料 * Room:更乾淨的 SQLite database 使用方式 ==LiveData== 是一種可以被觀察資料的載體(an observable data holder class)。和其他可被觀察的類不同的是,LiveData是有生命週期感知能力的(lifecycle-aware),它可以在 activities, fragments, 或者 services 生命週期是活躍狀態(onStart 和 onResume)更新這些組件,當 LiveData 的資料被更新時,所有對這個LiveData 變化感興趣的 Class 或者是 View 都會收到資料更新的通知,感知 Android 元件(例如 activities / fragments /services)的生命週期,防止 Memory Leak。 除了傳統的觀察者模式中的功能之外,LiveData 還有很多優點: * **UI 與數據保持一致**:觀察者模式最基本的功能。 * **節省資源**:當 Activity 不在活躍時期時是不會收到 LiveData 的任何事件的,能讓系統省去不必要的接受事件。 * **避免內存泄露**:因為 LiveData 能夠感知生命週期,所以當 Activity 被銷毀時,LiveData 也會隨之銷毀,避免不必要的引用而造成的內存泄露。 * **更好的使用者體驗**:如果今天有一個功能是:數據更新完成後跳出 Toast 告訴使用者更新完成,但如果這時候應用是處在後台,也就是非活躍狀態,突然跳出一個 Toast,反而會讓使用者覺得奇怪。若是使用 LiveData 則會讓應用處在 活躍狀態時 才跳 Toast,能帶來更好的使用者體驗。 LiveData最強大的地方在於lifecycle-aware特性,當LiveData的value發生改變時,若View在前景便會直接發送,而View在背景的話,value將會被保留(hold)住,直到回到前景時才發送。此外,當View被destroy時,LiveData也會自動停止observe行為,避免造成memory-leak。 ==viewModel== 為特定的 UI(例如 Activity/Fragment)提供資料的 Class,同時它也承擔和資料相關的邏輯處理功能。因為 ViewModel 是獨立於View,所以並不會被View的事件影響,比如 Activity 被回收或者是螢幕旋轉等並不會造成 ViewModel 的改變。 ViewModel 的生命週期要比 Views 長,所以不能在 ViewModel 中持有 Views 的 reference,否則會造成 Memory Leak 等問題 ==MVVM + LiveData== ![](https://i.imgur.com/D9rUNDd.png) 以上圖為例,我們將 LiveData 掛載於 ViewModel 中,當 View 向 ViewModel發出請求,這時候 ViewModel 會請 Model 去跟 API 取得資料後更新身上的LiveData。這時候當有對這個 ViewModel 上的 LiveData 做觀察的View就會收到 DataSet 被更新通知,View 就可以取得新的 DataSet 去更新 UI LiveData 與 ViewModel 一樣,是一個可以感知 Activity / Fragment 生命週期的 Data Holder,因此他可以確保只在元件處在 "Active" 才會更新 UI , View 在背景時則會保存此狀態,並在下一次元件甦醒時更新畫面,而 View 被摧毀時則會一併被回收,從而避免了 memory leak。 想想以前在把資料顯示在 Activity 上時,動不動就要檢查 Activity 是否存活,如今有了 LiveData 後就可以省略這些步驟了。 通常我們會在 onCreate() 開始觀察 LiveData ,有以下的原因: 確保系統不會因為 onResume() 而有多餘的調用。 確保 Activity / Fragment 在 Active 狀態時擁有可顯示的資料。 一般而言 LiveData 只會在資料改變時傳遞給觀察者,但是有例外狀況如昨天在 ViewModel 提到類似的例子:如果旋轉螢幕則因為 View 重新初始化,所以又會收到 LiveData 的資料,造成 Toast UI 重複顯示等,需搭配liveData 實現 ViewModel 由於有著 Lifecycle-Aware Components 的幫助, ViewModel 可以在 App配置改變時自動保存 instance ,以便其資料可以在接下來的 Activity 或是 Fragment 使用。 ![](https://i.imgur.com/bQq14xT.jpg) 上圖可以看到 ViewModel 在 Activity 第一次時建立也一起建立,在螢幕旋轉時依舊存在,直到 Activity finish() 並且 destroy 時才一起消滅。 ==Room== Android從一開始就支援SQLite資料庫;然而,為了讓SQLite工作,開發者一般需要編寫大量樣板代碼。另外,SQLite沒有保存POJO(純Java物件),並且在編譯時沒有檢查查詢。 Room元件就是用來解決這些問題的!它是一個SQLite映射庫,能夠持久化Java POJO,直接將查詢轉換為物件,在編譯時檢查錯誤,並從查詢結果生成LiveData可觀察者。 * Room 把一些 SQLite 底層實作封裝起來讓我們能更方便存取資料庫,不需要再寫冗長的程式碼才能將 SQL 和 Kotlin程式資料類別轉換 * Room支援編譯時期的 SQL 語法檢查,不需要等到執行後才能發現錯誤。 容易整合且語法簡單需多,少掉很多囉唆的程式碼。 支援LiveData / RxJava,可以使用觀察者模式來訂閱資料變更。 Room主要分為三個部分: * Database 使用註解申明一個類,註解中包含若干個Entity類,這個Database類主要負責建立資料庫以及獲取資料物件的。 * Entity 表示每個資料庫的總的一個表結構,同樣也是使用註解表示,類中的每個欄位都對應表中的一列。 ```kotlin= @Entity(tableName = "new_post") data class NewPost(@PrimaryKey val id: String = UUID.randomUUID().toString(), @ColumnInfo val caption: String? = null, @ColumnInfo(name = "media_file") val mediaFile: File? = null, @ColumnInfo val type: PostType? = null, val location: Location? = null ) ``` * DAO DAO是 Data Access Object的縮寫,表示從從程式碼中直接訪問資料庫,遮蔽sql語句。 ![](https://i.imgur.com/GWiJ754.png) http://jacob-yo.net/viewlifecycleowner-vs-lifecycleowner/ https://willy2016.pixnet.net/blog/post/217587468-android-kotlin-fragment-observes-livedata-%E7%9A%84%E9%99%B7%E9%98%B1%28memory-l http://blog.cgsdream.org/2018/10/28/android-arch-viewmodel/ https://ithelp.ithome.com.tw/articles/10193296 什麼是 Lifecycle-aware Components http://vulpesadn.blogspot.com/2018/10/lifecycle-aware-components.html II. Reference Type v.s. Value Type --- Java資料型別可分為==實值型別==(Value Type)與==參考型別==(Reference Type) ==實值型別==變數會儲存資料的值,如數值的1、3.51、10、149….等。指派一個實值型別變數給其他實值型別變數,會複製所包含的值。Java語言的基本資料型別(Primitive Types)如byte、short、int、long、float、double、boolean和char為實值型別。 ==參考型別==變數是儲存資料的參考(資料所在位址)。參考型別變數的指派會複製物件的參考,但不會複製物件的值。除了Java基本資料型別之外其他型別均為參考型別(Reference Types),如類別(Class)、介面(Interface)、字串(String)、矩陣(Array)等。 ** Reference Type 參考型態: 內建物件之陣列(矩陣)、字串,與自行設計之建構物件 這三種變數的值存放的是參考而非實值,也就是存放指向物件的位址。 宣告變數時配置記憶體位置,值為一段位置,要到heap區找實際值(實例資料會儲存在 Heap 中,Stack 內的變數值為實例在 Heap 中的記憶體位址) https://blog.marksylee.com/2016/09/14/java-interview-02-jvm-stack-heap/ Value Type 基本型態: byte、short、int、long、float、double、boolean和char 宣告變數時配置記憶體位置,同時將實際值賦予變數,整數初始值為0 https://tianchyi1955.pixnet.net/blog/post/101762587?pixfrom=related ![](https://i.imgur.com/yzA8ypT.png) pass by value vs pass by reference???? Stack vs Heap???? **Stack (棧) :** 用來儲存 Value Types (Primitives)的地方 Stack frame 存活時間是規律可預測的,只存在於 function 的執行期間,一旦 function 執行完畢,系統會自動回收空間,不需要擔心 Memory Leak 在這裡發生。 **Heap (堆)** : 用來儲存 Reference Types,new 一個物件即是存在 Heap 裡面,由於是動態配置記憶體空間,其存活時間不規律不可預測的,即使已經執行完動態配置的 function ,物件仍可能存在 Heap 中,這邊會因程式語言有沒有 GC 功能而有所不同: 沒有 GC :像 C++ 就需要用 delete 語法來清除物件 有 GC:Java 的 Garbage collector 為了防止 memory leak 會自動釋放 heap 上的記憶體空間 https://medium.com/joe-tsai/stack-vs-heap-b4bd500667cd --- * Reference Type : variable指向instance的記憶位置 ![](https://i.imgur.com/dFLNBOT.png) Reference types consist of shared instances that can be passed around and referenced by multiple variables. This is best illustrated with an example. Dog and puppy both points to ``` class Dog { var wasFed = false } let dog = Dog() let puppy = dog puppy.wasFed = true ``` ![](https://i.imgur.com/2p2OpEh.png) * Value Type : Variable 創立一個新的 Value 的 Copy ![](https://i.imgur.com/xNCWrWn.png) ``` var a = 42 var b = a b += 1 a // 42 b // 43 ``` ```swift= struct Cat { var wasFed = false } var cat = Cat() var kitty = cat kitty.wasFed = true cat.wasFed // false kitty.wasFed // true ``` ![](https://i.imgur.com/y7CPJEP.png) > The quick and dirty explanation is that reference types share a single copy of their data while value types keep a unique copy of their data. > #### - A better way of understanding this is that it passes the value, and for Objects the value is a reference 1. Kotlin never implicitly copies objects on assignment. **Variables always hold references to objects, and assigning an expression to a variable only copies a reference to the object, not the object itself**. Under the hood, each value is either a primitive (Int, Boolean, Char etc.) or a reference. A better way of understanding this is that it passes the value, and for Objects the value is a reference. 2. 定義 * 數值型別 ── A Value Type holds the data within its own memory allocation. **When you created a Value Type, a single space in memory is allocated to store the value and that variable directly holds a value. If you assign it to another variable, the value is copied directly and both variables work independently.** **Predefined datatypes, structures, enums are also value types, and work in the same way. Value types can be created at compile time and Stored in stack memory, because of this, Garbage collector can't access the stack.** 每個實例保存資料一份獨立的備份。當這類型別被指派給一個變數或常數、或是被傳送到函式時,就會創建一個新的實例(備份)。 * 參考型別 ── Reference Type contains a pointer to another memory location that holds the real data. 每個實例共享資料的單一備份。當這類型別被初始化、被指派給一個變數或常數、或者是被傳送到函式時,就會回傳參考到相同的實例。 3. 在以下情況,我們可以使用數值型別: * 以 == 來比較實例的資料較為合理(一個雙等於運算符(==)是用來比較 數值的)。 * 你希望副本有獨立狀態。 * 資料將會被跨越多個執行序的程式碼使用,而你擔心資料會在其他執行序中被變更。 4. 在以下情況,我們可以使用參考型別: * 以 === 來比較實例較為合理( === 是用來確認兩個物件是否完全相同的,包括儲存資料的記憶體位置。) * 你希望創建一個共享、可變動的狀態。 5. 範例 ```kotlin= class Home() {var people = 4} var myHome = Home() var theirHome = myHome ``` ![](https://i.imgur.com/UbwDgFS.gif) ![](https://i.imgur.com/BA5NUK0.png) | 分類 | Value type | Reference type | | -------- | -------- | -------- | | Definition | A Value Type stores its contents in memory allocated on the stack. | holds a reference (address) to the object but not the object itself. | |Example| var x: Int = 10| val y = AClass()| |Storage|Stack?動態記憶體位置|Heap?靜態記憶體位置| |Garbage collector|Value types can be created at compile time and Stored in stack memory, because of this, Garbage collector can't access the stack.|When a reference type variable is no longer used, it can be marked for garbage collection. Examples of reference types are Classes, Objects, Arrays, Indexers, Interfaces etc. | notes: 1. You can assign a pointer, pass the pointer to a method, follow the pointer in the method and change the data that was pointed to. However, you cannot change where that pointer points. 2. Stack and Heap ![](https://i.imgur.com/qZQiMQm.jpg) * value type 的變數, 包括指標變數會放在 stack * reference type 的變數 (如 string, object) 本身也會放 stack, 然而他的值 (value) 則是放 heap * box 就是 value types to reference types 的過程, 所以 value 會被放到 heap 中, 而產生一個 object 變數來指向這個 value, 變數指標則是在 stack * unbox 是 reference types to value types 的過程, 所以原本 object 所指向的值 (heap 中) 會被複製到 stack 中並賦予明確 value type 型別 ref: 1. https://appcoda.com.tw/value-type-and-reference-type/ 2. http://net-informations.com/faq/general/valuetype-referencetype.htm 3. https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value (with nice examples explain the arguments passing) ---2 III. OOM & GC --- ## 何謂 Memory Leak? 記憶體洩漏 ( Memory Leak ) 可說是每個 Android 開發者都很常遇到的問題,顧名思義就是指因為疏誤或錯誤導致程式無法釋放不再使用的記憶體。更明確地來說,是指應用程式所分配到的某段記憶體空間在釋放前就失去了控制權,導致該段記憶體無法回收。而在探討可能發生 Memory Leak 的幾種情況前,我們須先了解一下 ART 的 Garbage Collection 機制。 ![](https://i.imgur.com/lTMDVmg.png) 所謂的 Garbage Collection,是指在大部分的虛擬機器 (不論是 ART 或 Dalvik) 中,會由一種稱作可達性分析的演算法追蹤記憶體的配置情況,用來決定哪些是未來可能不再被使用的對象 (物件),回收對象所占用的資源。原理則是:有幾個對象作為 root 節點,從這些起始點開始向下搜尋,搜尋的路徑就是各個對象的參照鍊。 例如從下圖來看:從 GC Roots 開始,每個 root 參照到某些對象,而那些對象也同樣參照了其他的對象。由此便能得知目前正在使用及可能不再被使用的物件,進而決定要回收哪些物件所占用的記憶體資源。 Android 中的 Memory Leak 就是指: 對象雖然已不再被使用,卻因為一直被 roots 所參照到,導致無法回收對象占用的記憶體空間,因而稱作洩漏 ( Leak )。 ## 何時會發生 Memory Leak? 再來我們看看以下幾種常見記憶體洩漏的情景: **static** 當一個變數指定為 static (靜態) 時,代表它從程式開始執行時便產生了,而且它的壽命直到程式結束時才終止。因此當某個對象被靜態變數參照時是無法回收的。 **inner class** 內部類別的實例會參照到外部類別的實例,因此當內部類別的實例一直存活時,外部類別的實例也就永遠不會被回收。 ```java= class OuterClass { static InnerClass innerObject; class InnerClass { ... } } ``` ## 該如何檢查 Memory Leak? 目前最容易檢查記憶體洩漏的工具應該是 LeakCanary,首先建立兩個 Activity,分別是 MainActivity 和 LeakActivity,MainActivity 有個 Button,按下去後會進入 LeakActivity 的畫面。而在建立 LeakActivity 時,指定一個靜態變數的對象為 LeakActivity 本身,再來只要按下手機的返回鍵後,就會照預期的發生記憶體洩漏了。 ![](https://i.imgur.com/XKvZjRe.png) 發生洩漏時, LeakCanary 會發出洩漏的通知,另外我們可以開啟 LeakCanary 的 APP 來看看洩漏的詳細資訊: ![](https://i.imgur.com/y6PNYi5.png) ## 總結 避免 Memory Leak 的方法便是平常開發時,盡量避免用靜態變數存 Activity, View 等元件,另外盡量少用匿名類別變數來實作非同步工作或是耗時程序。 --- * OOM 1. Cause of OOM * 1. You are doing some **operation that continuously demands a lot of memory and at some point it goes beyond the max heap memory limit** of a process. * 2. You are **leaking some memory** i.e you didn’t make the previous objects you allocated eligible for Garbage Collection (GC). This is called Memory leak. * 3. **You are dealing with large bitmaps and loading all of them at run time.** You have to deal very carefully with large bitmaps by loading the size that you need not the whole bitmap at once and then do scaling. 2. Stack Memory Space!在記憶體中存放變數、函式相關資訊等資料,使運作過程可以順利取得所需的變數或函式所在地。要讓系統可以全自動化管理,代表需可被預期此變數或函數資訊的生命週期,一旦完全可預測代表可以安心的交由系統管理,這些資訊也將在執行過程中被存放在stack空間。 3. Heap的區塊專收執行期間動態產生的資料,由於為動態產生故結束點無法由系統來掌握,故需使用者自行回收空間。 4. 許多時候執行的程式都沒有改變,但卻常出現隨時間執行越久程式所耗用的空間將越多,最後造成out of memory。Heap中的資料如果沒有正常的回收,將會逐步成長到將記憶體消耗殆盡。 5. Java中會採用Garbage Collection(垃圾回收)的機制自動檢查Heap中哪些資料已經沒有被使用,當確認資料已經沒有使用會自動將空間回收,如此工程師就專注撰寫程式即可,不用擔心記憶體回收不當等問題。 notes: 1. 記憶體占用: 第一行:物件a的變數v指向了物件b 第二行:物件b的變數v指向了物件c 第六行:物件a的變數v指向了變數d。 這個時候,雖然變數c指向的物件有c 以及b.v指向它,但是它們都已經不可達了,為什麼?因為唯一可以找到它們的是a.v,但是現在a.v指向了d,所以他們就是不可達的了。 理由也很直觀:沒有任何可達變數指向你,你還有活下去的理由嗎?你就算活下去誰能找得到你呢? 所以說,C 中將釋放了的指標置為null的習慣要保留到Java中,因為這有可能是你釋放記憶體的唯一途徑。 > avoid memory leaks after closing activity I need to unbind those Drawables: set their callback to null. ## 整個Java堆可以切割成為三個部分: * Young(年輕代):又分以下兩區 Eden(伊利園):存放新生物件。 Survivor(倖存者):存放經過垃圾回收沒有被清除的物件。 * Tenured(老年代):物件多次回收沒有被清除,則移到該區塊。 Perm:存放載入的類別還有方法物件 當垃圾回收開始清理資源時,其餘的所有執行緒都會被停止。所以,我們要做的就是儘可能的讓它執行的時間變短。如果清理的時間過長,在我們的應用程式中就能感覺到明顯的卡頓。 1. 首先,所有新生成的物件都是放在年輕代的Eden分割槽的,初始狀態下兩個Survivor分割槽都是空的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件。 ![](https://i.imgur.com/BEunGJf.jpg) 2. 當Eden區滿的的時候,小垃圾收集就會被觸發。 ![](https://i.imgur.com/ZJ8UY4j.jpg) 3. 當Eden分割槽進行清理的時候,會把引用物件移動到第一個Survivor分割槽,無引用的物件刪除。 ![](https://i.imgur.com/rvKi3ll.jpg) 4. 在下一個小垃圾收集的時候,在Eden分割槽中會發生同樣的事情:無引用的物件被刪除,引用物件被移動到另外一個Survivor分割槽(S1)。此外,從上次小垃圾收集過程中第一個Survivor分割槽(S0)移動過來的物件年齡增加,然後被移動到S1。當所有的倖存物件移動到S1以後,S0和Eden區都會被清理。注意到,此時的Survivor分割槽儲存有不同年齡的物件。 ![](https://i.imgur.com/MrhTYZK.jpg) 5. 在下一個小垃圾收集,同樣的過程反覆進行。然而,此時Survivor分割槽的角色發生了互換,引用物件被移動到S0,倖存物件年齡增大。Eden和S1被清理。 ![](https://i.imgur.com/dPyCR4Z.jpg) 6. 這幅圖展示了從年輕代到老年代的提升。當進行一個小垃圾收集之後,如果此時年老物件此時到達了某一個個年齡閾值(例子中使用的是8),JVM會把他們從年輕代提升到老年代。 ![](https://i.imgur.com/5mZVIgw.jpg) 7. 隨著小垃圾收集的持續進行,物件將會被持續提升到老年代。 ![](https://i.imgur.com/FM2rZWk.jpg) 8. 這樣幾乎涵蓋了年輕一代的整個過程。最終,在老年代將會進行大垃圾收集,這種收集方式會清理-壓縮老年代空間。 ![](https://i.imgur.com/UMkpppE.jpg) 也就是說,剛開始會先在新生代內部反覆的清理,頑強不死的移到老生代清理,最後都清不出空間,就爆炸了。 總的來說,有兩個條件會觸發主GC: 當應用程式空閒時,即沒有應用執行緒在執行時,GC會被呼叫。因為GC在優先順序最低的執行緒中進行,所以當應用忙時,GC執行緒就不會被呼叫,但以下條件除外。 Java堆記憶體不足時,GC會被呼叫。當應用執行緒在執行,並在執行過程中建立新物件,若這時記憶體空間不足,JVM就會強制地呼叫GC執行緒,以便回收記憶體用於新的分配。若GC一次之後仍不能滿足記憶體分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。 ref: 1. https://proandroiddev.com/collecting-the-garbage-a-brief-history-of-gc-over-android-versions-f7f5583e433c https://www.alexleo.click/java-%E5%96%9D%E6%9D%AF%E5%92%96%E5%95%A1%EF%BC%8C%E8%81%8A%E9%BB%9E-gc%EF%BC%88%E4%B8%80%EF%BC%89-%E5%9F%BA%E7%A4%8E%E6%A6%82%E5%BF%B5/ https://iter01.com/50376.html Android gc與記憶體洩漏,溢位的理解: https://www.itread01.com/content/1546572089.html 官網Overview of memory management,內容概述GC: https://developer.android.com/topic/performance/memory-overview IV. Thread & Handler --- ### Why thread Android 處理程式常須執行在多執行緒環境中,UI Thread被lock超過5秒?會ANR ** **Runnable** = 工作,任務,run() **Handler**:提供 callback function 給其它 Thread 作執行 **Message**:將 Handler 包裝起來,傳送給其它 Thread **MessageQueue**: 緩衝,讓 Message 暫存 **Looper**:Looper取出訊息,處理 Message,一個線呈一個 Looper,處理完將 Message發送給 Handler -> 共同目標 : **讓程式能夠丟到其他 Thread 執行** ** Android 環境內,Thread 分兩種: **單次Thread** : 做完即關閉 ```java= Thread t1 = new Thread(r1); t1.start(); ``` start即開始工作~ **常駐Thread** : Handle Thread,做完idle(閒置) ```java= HandlerThread handlerThread = new HandlerThread("HandlerThread"); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper()) handler.post(r1) ``` start待命,post給工作即開始工作,做完idle,直到quit() HandlerThread run 方法內建立looper,loopr建立handler,handler發、處理信息 Handler 非同步更新UI,做執行續之間的通訊(子與Main) HandlerThread 使用方式: //參數:線呈名稱 mHandlerThread = new HandlerThread("HandlerThread"); handlerThread.start(); 結束:釋放記憶體 mHandlerThread.quit() HT vs T 多looper https://milochen.wordpress.com/2011/03/25/understanding-android-os-src-looperhandler-message-messagequeue/ https://hikaru79109.wordpress.com/2017/08/07/android-%E5%A4%9A%E5%9F%B7%E8%A1%8C%E7%B7%92-handler%E5%92%8Cthread%E7%9A%84%E9%97%9C%E4%BF%82/ ![](https://i.imgur.com/4piAAaM.png) ref: https://www.youtube.com/watch?v=LJ_pUlWzGsc https://dotblogs.com.tw/hanktom/2016/01/26/191154