2019年8月19日 星期一
用 Flutter 幾行程式搞定一個 app
1. 使用 vscode 開啟 Flutter 新專案取名為 webview
2. 在目錄結構新增一個新目錄(document root)例如 www, 把 *.html, *.js, *.css ...等等所要用到的檔案複製到該目錄, 假設 html 主檔案是 index.html
3. 修改 pubspec.yaml 檔案, 加入 flutter_webview_plugin 並啟用 flutter_assets, 把它取名為 www/
4. 修改主程式 main.dart, 不到 10 行程式碼, 讓 WebviewScaffold 去載入 html 主檔案 index.html, 一個 app 就啟動了.
5. 就這樣, 連線到手機開啟開發者模式, 允許 debug 上傳 app, 把 app 上載到手機上執行.
p.s.
1. 修改的 pubspec.yaml 內容:
name: webview
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: "> =2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
flutter_webview_plugin: ^0.3.0+2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- www/
2. 修改 main.dart 的內容, 開啟 lib/main.dart 主程式, 以下內容全數複製貼過去:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
void main() { runApp(MyApp()); }
class MyApp extends StatelessWidget {
static String assets = "file:///android_asset/flutter_assets/www";
@override build(BuildContext context) => MaterialApp( theme:ThemeData.dark(),
routes: {
'/': (_) => WebviewScaffold(url:"$assets/index.html", withZoom: true, withLocalStorage: true, withJavascript: true, allowFileURLs: true)
}
);
}
3. 修改 launch.json 內容, 在 vscode 拉下 Debug-> Start Debugging 或是用快速鍵 Ctrl-F5 上傳到手機上, 如果想將 app 改成 release 版, 順便減肥, 要拉下 Debug-> Open Configurations 修改 launch.json 檔案, 添加 flutterMode:
{
"configurations": [
{
"name": "Flutter",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}
4. 如果發生 WeView 找不到檔案 index.html, 可能是 Fullter 將 assets 包成 app 後所在目錄位置出錯, 可以用解壓縮程式 zip, 打開 webview/build/app/outputs/apk/release/app-release.apk, 看看到底是把 assets 目錄放在那個目錄底下, 藉此去修正程式類型 MyApp 中所定義的靜態字串 static String assets = "pathTo/www" 就能解決.
2019年8月18日 星期日
Dart 數列及資料流產生器, async/await/Future
在 dart 世界裡, 透過 sync* 關鍵字標註一個函式讓它可以用 yield 暫停後回傳一筆資料(實際上是包裝成一個 iterable 物件直接回傳,因此不會 block 程式),讓後續用 forEach 依序讀取, 若將 yield 放進一個 for 迴圈, 就能產生一個數列:
IntRange(int start, int end) sync* {for(int i=start; i<=end; i++) yield i;}//一個整數數列
void main() => IntRange(1,10).forEach(print); // 實際上是一個 iterable物件
或是搭配 yield * 直接呼叫遞迴函式(recursive funtion), 讓它看似沒有 loop 的序列產生器:
IntRange(int start, int end) sync* { // 打包成 Iterable 類型的物件
if (start <= end) { // 結束遞迴的條件
yield start; // 產生初始值
yield * IntRange(start+1, end); // 事件(event loop)註冊, 下次繼續用遞迴函式產生
}
}
void main() => IntRange(1,10).forEach(print); // 實際上是一個 iterable物件
當來到非同步世界後, 同樣可以用 yield 產生序列, 但要改用 async* (非同步產生器)標註函式, async * 所產生的是序列會包裝成資料流 stream, 而非 iterable:
IntStrean(int start, int end) async* {for(int i=start; i<=end; i++) yield i;}//一串整數流
void main() => IntStrean(1,10).forEach(print);//實際上是一個 Stream<int> 物件
若是 yieild Future 物件的話, 也就是非同步串, 實際上 Future 是一個抽象類型(abstract class), 它並非當下產生資料, 而是當完成後 then( )或是等待他完成 await 才現出物件的原型:
futureStream(int start, int end) async * { for(int i=start; i <=end; i++) yield Future.value(i); } // 實際是回傳 Stream<Future>
void main() => futureStream(1,10).forEach( (f) => f.then(print));// f 實際上是 Future 物件, 用 then( ) 現出物件原型
用 yield await 看似等待完成後才回傳, 實際只是註冊到 event loop , 順便將物件打包成相對應之類型,他並不會 block 程式的運行:
intStream(int start, int end) async* { // 打包成 Stream 類型的物件
for(int i=start; i <=end; i++) yield await Future.value(i); // 註冊至 event loop, 用 await 現出原型
}
void main() => intStream(1,10).forEach(print);//實際上是一個 Stream<int>物件
當然也能搭配 yield * 呼叫遞迴函式(recursive funtion)形成一個看似不用 loop 的資料流產生器,實際上也是註冊至 event loop, 待後續再執行:
intStream(int start, int end) async* {
if (start <= end) { // 結束遞迴的條件
yield await Future.value(start); // 註冊事件, 用 await 等待物件現出原型
yield * intStream(start+1, end); // 只註冊至 event loop 續用遞迴函式, 無需等待
}
}
void main() => intStream(1,10).forEach(print);//實際上是一個 Stream<int>物件
dart 程式庫的 Future 物件可類比成 Javascript 的 promise 物件, 而將 Future.value(值)類比成 promise 的 resolv(非同步傳回值), 他們運作邏輯是相同的, 同樣的,我們也可以用簡單邏輯來理解 async/await 的運作方式, async 其實只是把將來要回傳(return)的值一起打包成 Future/promise 物件, 而 await 負責註冊事件並把後續程式碼(位在相應的 async 函式碼)打包放入 then( ) 的 callback 函式內,後續當物件現出原形時, 呼叫這個 callback 方法完成後續動作, then 是 Future/promise 裏面一個註冊過的從屬函式(簡稱方法), 一旦條件滿足(物件現出原形)才會執行. 對於 sync * 產生器而言, 打包的是 iterable 物件, iterable 物件是一個在迴圈中可以一個一個取出使用的一串數列, 而 async * 所打包的是 Stream 類型的物件, Stream 則是一個可以一個一個取出來的非同步事件(a sequence of events), forEach 只是其中一個取出物件的方法, 透過註冊事件與物件方法間接或直接讀取物件的原型, 而不會 block 程式的運行,如此而已. Dart 的 async/await 語法跟 Javascript 語法長的很像:
asyncFunction( ) async => 3; // async 標註會將函式傳回值打包成 Future 類型的物件
void main( ) async => print(await asyncFunction( )); // await 只能在 async 用, 他讓物件現出原型
或是同步程式的寫法,用 Future 的 then( ) 方法現出物件原型後再處理:
void main( ) => asyncFunction( ).then(print);
同步程式也可以打包成 Future 物件, 後續沿用 then 方法取得物件原型:
syncFunction( ) => Future.value(3); // Future.value 是立刻賦值, promise resolve
void main( ) => syncFunction( ).then(print); // then 其實是一個 Future 物件裏面一個方法
2019年7月28日 星期日
理解 Kotlin 的 companion object (變身) vs instance of class (化身) 之不同
class AAA { // 類型 AAA, 預設的建構式就是 AAA( )
var b = 3 // 這是待化身的變數宣告, 透過建構式 AAA( ) 化身成物件成員(instance member)
companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
var a = 5 // 需透過 AAA 存取成員
var b = 10 // 名字同化身的成員, 直接變身成物件成員, 必需透過 AAA 存取成員
}
}
fun main(args: Array < String > ) {
val c = AAA // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
val i = AAA( ) // i 才是類型的化身, 透過類型建構式 ( ) 才能化身成物件(instance of class)
println(c.a) // a 是 AAA 共享成員
println(c.b) // b 也是 AAA 成員之一
println(i@AAA.a) // i 要透過變身標籤 @AAA 才能存取共享成員 a
println(i@AAA.b) // i 要透過變身標籤 @AAA 才能存取共享成員 b
// println(i.a) // 錯誤的用法: a 實際並非 i 的成員
// println(i.b) // 錯誤的用法: b 實際並非 i 的成員
println(i.b) // 這裡的 b 是透過建構式生成的, 並非變身(companion object)裡面的 b
執行結果:
5
10
5
10
3
變身與化身中文意義是相同的, 如果把英文的 companion object(協同物件)翻譯成"變身", 而將 instance of class (類型物件)翻譯成"化身"時, 兩者本質上都是物件, 就要區分它們的不同, 也許可以這樣理解: 一旦宣告了協同物件(簡稱變身), 類型名稱可以變身(ailias of companion object)成物件去存取共享成員, 而類型物件(instance of class)必需透過建構式去化身, 兩者物件的生成方式不同在於: 變身在是在編譯階段(compile phase)生成的靜態物件(static object),從程式開始執行到結束生命都一直維係著不會消失, 但化身是在執行階段(runtime)才生成的區域物件(scope object), 生命周期受到控制, 一旦離開所在的生命區域(scope)便消失殆盡. 變身主要目的是維繫共享資源, 實際上只有一份變身物件存在類型內, 所有類型的化身並沒有變身這個物件!如果真要在化身物件方便取用,除了用 @標籤外, 也可以用 set() 及 get() 刻意連結起來,只不過要注意的是, 因為是共享的(static object),若在多工環境下也許要用 semaphore 來加以鎖定.
class AAA { // 類型 AAA, 預設化身的建構式就是 AAA( )
companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
var a = 5 // AAA 變身的成員
var b = 10 // 若名稱與化身的成員相同時, 必需透過 AAA 存取成員
}
var b: Int get( ) { return AAA.b } // 符號讀取連結, 讓化身間接讀取變身成員 b
set(v: Int) { AAA.b = v } // 符號寫入連結, 讓化身間接寫入變身成員 b
fun a( ) { // 當化身成員名稱與變身的成員不同時, 化身可以取用到變身的成員只是因為內定(default)符號聯結的關係, 並不表示他就是化身的成員
return a // 等同取用 AAA.a , 只是因為 kotlin 內定會將符號 a 聯結到變身成員 AAA.a
}
}
fun main(args: Array < String > ) {
val c = AAA // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
val i = AAA( ) // i 是類型的化身, 透過類型建構式 ( ) 化身成物件(instance of class)
println(c.a) // a 是 AAA 共享成員
println(c.b) // b 也是 AAA 成員之一
println(i.a()) // 呼叫函式 a( )間接讀取 AAA.a
println(i.b) // 已經刻意將 b 做了符號讀取連結, 看似直接(實際是間接)讀取
i.b = 3 // 已經刻意將 b 做了符號寫入連結, 看似直接(實際是間接)寫入
println(c.b) // 因為 b 是共享成員, 所以變身的成員也跟著改變
}
結果:
5
10
5
10
3
2019年7月26日 星期五
Kotlin 的 promise
import kotlin.js.Promise
import kotlin.browser.*
fun main() {
lateinit var resolve: (Int)->Unit
val promise = Promise
resolve = Yes
}
promise.then { result -> // only execute when promise fullfill
println("I got $result")
}
window.setTimeout( {resolve(0)}, 2000) // resolve result sometime or somewhere
}
例如應用在按鍵事件驅動的不定時的異步化(async function)程式上相當實用.
promise 的原理很簡單, 只要沒呼叫 resolve( ) 去解析, 就會維持在暫停(suspend)狀態, 一旦執行過 resolve( ) 後就變成滿足(fullfill)狀態. 後續當呼叫 then { } 區塊時, 若還是在暫停狀態下,沒意外(exception)的話,就維持該狀態,但要將該區塊(指標變數)附加(累積)到 promise 物件裏面,一旦呼叫了 resolve(result), resolve 除了會解析結果外, 同時還會就將結果傳遞給之前所累積的 then { } 區塊去執行. 也就是說只有在滿足狀態當下, promise 的 then { } 區塊才會執行. 有個應用情境是:將 promise.then { } 區塊放在函式內執行, 再用像是 window.setTimeout 或是 window.window.requestAnimationFrame 定時啟動該函式, 重複啟動函式時, 若 promise 一直維持在暫停的狀態, 就會造成 then 區塊不停的累積, 一旦狀態滿足時, 就有可能造成區塊重複執行的後遺症, 例如:
import kotlin.js.Promise
import kotlin.browser.*
fun main() {
lateinit var resolve: (Int)->Unit
val promise = Promise < Int > { Yes, _ ->
resolve = Yes
}
var count = 0
fun render( ) {
val capture = count
promise.then { result -> // dupcate then if not fullfill
println("fullfill: result=${result} capture=${capture}")
}
if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
}
window.requestAnimationFrame({render() })
// promise not resolve
window.setTimeout( {resolve(0)}, 2000) // resolve result sometime or somewhere
}
執行結果:
fullfill: result=0 capture=0
fullfill: result=0 capture=1
fullfill: result=0 capture=2
fullfill: result=0 capture=3
fullfill: result=0 capture=4
fullfill: result=0 capture=5
fullfill: result=0 capture=6
fullfill: result=0 capture=7
fullfill: result=0 capture=8
fullfill: result=0 capture=9
fullfill: result=0 capture=10
為了用 kotlin 實現 async/await 的邏輯. 運用 promise 物件, 基本上就可以成為 async function, await 只能放在 async function 裏面, 直到 promise 完成解析(滿足狀態)才能進行後續動作, 因此特別用高階函式及 skip re-entry 技巧, 將 promise 包進新的類型, 並把他的 then 區塊打包放進高階函式當成 callback 函式去執行, 就能解決重複執行的困擾:
import kotlin.js.Promise
import kotlin.browser.*
class myAsync {
var once = 0
lateinit var resolve:(Int)-> Unit
val promise = Promise < Int > { Yes, _ ->
resolve = Yes
}
fun bwait(callback: (Int) -> Unit ) { // high order function with callback function
promise.then { result ->
if (once ++ == 0) callback(result ) // 只執行第一次的 then 區塊
}
}
fun await(callback: (Int) -> Unit ) { // high order function with callback function
once ++
promise.then { result ->
if (-- once == 0) callback(result ) // 只執行最後一次的 then 區塊
}
}
}
fun main() {
val async = myAsync( )
var count = 0
fun render( ) {
val capture = count
async.await { result -> // no duplicate then if fullfill
println("fullfill: result=${result} capture=${capture}")
}
if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
}
window.requestAnimationFrame( { render() } )
window.setTimeout( {async.resolve(0)}, 2000) // resolve result sometime or somewhere
}
若是用 async.await 等待執行的結果:
fullfill: result=0 capture=10
若是用 async.bwait 等待執行的結果:
fullfill: result=0 capture=0
2019年7月14日 星期日
for Kotlin
for (i = 0; i < 10; i++) // ...
在 Kotlin 的 for 迴圈語法只能用 for(variable in Range) 來實現,像上述例子就可用單端式範圍遞增數列運算子 until 來實現:
for (i in 0 until 10) // until 是單端式範圍, 包含 0, 但不含 10
或是改成雙端式範圍遞增數列運算子 .. 加以實現:
for(i in 0 .. 9 ) // .. 是雙端式範圍, 0 與 9 皆包含在內
若累進數(等差級數步階值)大於 1, 就要加上 step, 像上式用 c 或 Javascipt寫的步階改為 2:
for (i = 0; i < 10; i+=2) // ...
在 Kotlin 就直接加上 step 2:
for (i in 0 until 10 step 2) // ...
如果是遞減數列(數字大往數字小的等差級數), 像是 c 或 Javascript 所寫雙端式範圍的迴圈:
for (i=10; i>=0; i-=2) // 10, 8, 6, ..., 0
因為在 Kotlin 語法中, 步階值必須是正整數, 雙端式範圍遞減數列用 downTo 關鍵字來實現:
for (i in 10 downTo 0 step 2) // ...
參考文章: https://kotlinlang.org/docs/tutorials/kotlin-for-py/loops.html
2019年7月5日 星期五
使用 kotlinc-js 編譯程式碼快速學習 Kotlin 語言
cd
mkdir work
cd work
unzip pathToKotlinc/lib/kotlin-stdlib-js.jar
2. 在工作目錄用文字編輯器寫一個 kotlin 程式, 例如:
geany my.kt &
3. 將以下作複數運算的範例程式存成檔案 my.kt
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
val square:Double = r*r + i*i // |C|2
val length:Double = sqrt(square) // abs(C) = |C| = sqrt(|C|2)
operator fun plus(a:C):C { // +
r += a.r
i += a.i
return this
}
operator fun minus(a:C):C { // -
r -= a.r
i -= a.i
return this
}
operator fun times(a:C):C { // *
val temp = a.r * r - a.i * i // real part
i = a.i*r + a.r*i // image part
r = temp
return this
}
operator fun div(a:C):C { // ÷
if (a.length > 0f) {
val temp = (a.r*r + a.i * i) / a.square // real part
i = (a.i*r - a.r*i) / a.square // image part
r = temp
} else throw Exception("Divide by zero(length=0)")
return this
}
operator fun invoke( ):C{ // dump self complex number
val real = floor(r*100+0.5).toFloat()/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
val imag = floor(i*100+0.5).toFloat()/100 // 虛部只顯示到小數點 2 位, 4捨 5 入
if (imag < 0) println("$real - ${-imag} i") else println("$real + $imag i")
return this
}
}
fun main(){
try {
val c = C(3.0, 3.0) / C(5.0, 2.0) // c = (3 + 3i) / (5 + 2i)
c( ) // dump
} catch (e:Exception){
println(e.message)
}
}
4. 用 kotlin 的編譯器 kotlinc-js 將上述 kotlin 原始碼翻譯為 Javascript:
pathToKotlinc/bin/kotlinc-js my.kt -output my.js
或是利用 geany 的設定組建命令, 直接點選工具欄: 組建(B) - > 設定組件命令(S), 第一欄隨意輸入識別名稱(例如 kotlin2js), 第二欄輸入 kotlin2js 編譯器的位置及參數(%f 是目前編輯檔案名稱, %e 是不含副檔名的檔案名稱, -output 可以指定輸出的檔案名稱), 例如:
pathToKotlinc/bin/kotlinc-js %f -output %e.js
意思是說用 kotlinc-js 將目前的檔案翻譯成 Javascript, 輸出檔與編譯的同名稱但副檔名改為 js, 第三欄可以空白預設是目前的工作目錄, 設定好後, 用滑鼠點選: 組建 -> kotlin2js 執行看看, 應該會在工作目錄編譯出 my.js.
若要在 vscode 裏面編譯, 參考 https://code.visualstudio.com/docs/editor/variables-reference, 編寫一個 tasks.json 檔案, 點選 Terminal -> Configure Tasks帶出編輯視窗, 把以下內容寫入:
{
"tasks": [{
"label": "Kotlinc2js",
"type": "shell",
"command": "pathToKotlinc/bin/kotlinc-js",
"args": ["${file}", "-output", "${fileBasenameNoExtension}.js"],
"group": {
"kind": "build",
"isDefault": true
}
}]
}
存檔後, 點選 Terminal -> Run Build task 或是用快速按鍵 Ctrl-Shift-B, 就能將目前編輯的 Kotlin 程式檔轉換成 Javascrip
5. 在工作目錄底下用文字編輯器另外編輯一個 my.html 準備讓瀏覽器開啟, 並在開發工具底下執行用 kotlin 語言所寫的程式碼(實際上是翻譯成兩個 Javascript 檔案: kotlin.js 及 my.js). 將以下內容存檔(my.html):
<html><head><meta charset="UTF-8"></head>
<body>
<script type="text/javascript" src="kotlin.js"></script>
<script type="text/javascript" src="my.js"></script>
</body>
</html>
Kotlin 的 Delegated Properties (被代理的變數或特性)
針對類型裡的特性(propertys), 常數(val)的特性內定 getter 是用 get( ) 方法去讀取主角(field)的值, 對於可寫入的特性(var)而言, 內定 setter 則用 set(v: Type) 方法設定幕後的主角(field), 除非要另外處理 field 的運算才需改寫, 否則 kotlin 會自動加上必要程式碼:
class SomeClass {
var v: String = "?"
// get( ) { return field }
// set(s:String) { field = s }
val readonly: Int = 0
// get( ) { return field }
}
物件在引用特性時不必後綴成對小括號, 因此很簡潔. 物件引用方法( ) 時可以看做是一個運算的結果, 也就是所謂的函數. Kotlin 針對特性或變數有個特別條款可以用 by 跑龍套, 也就是說留待跑龍套代為處置, Koltin 已經為跑龍套主角(object)指定了兩個運算 operator getValue( ) 及 operator setValue( ) 來處理特性或變數的讀寫機制, 若想改寫,可以 import pacage 針對讀與寫操作分別作運算,其中對應 get( ) 方法就是 operator getValue( ), 對應 set( ) 方法就是 operator setValue( ), 詳如範例:
import kotlin.reflect.*
class InitString (str: String) { // 代理的主角類型
var bakendField: String = str // 真正被讀寫的幕後主角
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
print(property.name + " : ")
return bakendField // return value for method get( ), 讀取
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, v: String) {
println(property.name + " = " + v)
bakendField = v // set value to property for method set(v), 寫入
}
}
fun main(){
var c : String by InitString("???") // 用物件 InitString( ) 代理改寫變數的讀/寫機制
println(c) // 讀取變數 c
c = "dd" // 設定變數 c
println(c) // 重新讀取變數 c
}
執行結果:
c : ???
c = dd
c : dd
2019年7月4日 星期四
Kotlin 的 data class 搭配有趣的運算子 operator fun
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
val square:Double = r*r + i*i // |C|2
val length:Double = sqrt(square) // abs(C) = |C| = sqrt(|C|2)
operator fun plus(a:C):C { // +
r += a.r
i += a.i
return this
}
operator fun minus(a:C):C { // -
r -= a.r
i -= a.i
return this
}
operator fun times(a:C):C { // *
val temp = a.r * r - a.i * i // real part
i = a.i*r + a.r*i // image part
r = temp
return this
}
operator fun div(a:C):C { // ÷
if (a.length > 0f) {
val temp = (a.r*r + a.i * i) / a.square // real part
i = (a.i*r - a.r*i) / a.square // image part
r = temp
} else throw Exception("Divide by zero(length=0)")
return this
}
operator fun invoke( ):C{ // dump self complex number
val real = floor(r*100+0.5)/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
val imag = floor(i*100+0.5)/100 // 需部只顯示到小數點 2 位, 4 捨 5 入
if (imag < 0) println("$real - ${-imag} i") else println("$real + $imag i")
return this
}
}
fun main(){
try {
val c = ( ( C(3.0, 3.0) + C(2.0, 1.0) ) * C(9.0, 2.0) - C(10.0, 2.0) ) / C(5.0, 2.0)
c( ) // Complex number dump: c = (((3 + 3i)+(2 + i))*(9 + 2i)-(10 + 2i))÷(5 + 2i)
} catch (e:Exception){
println(e.message)
}
}
輸出結果:
7.68 - 5.72 i
2019年7月2日 星期二
理解 Kotlin 的高階函式
Kotlin 是一個 typesafe 程式語言, 每個變數都必須需確認他的資料型態(type), 函式的名稱可以看成是常數(變數), 因此函式也不例外需要加以宣告. 程式資料型態(function type)可以用成對小括號加上箭頭符號組成 ( ) -> , 用 typealias 定義函式型態後可以讓程式碼變得簡單易讀, 同時也能重複利用. 定義函式時若將函式當作輸入參數或是輸出函數,他就是所謂的高階函式 :
typealias funit = ( ) -> Unit // 函式資料型態: 無輸入參數, 也不傳回任何數值
fun g(f: funit) { // f 是一個輸入函式, 因此 g(f) 是高階函式, f 可以在函式 g 裏面執行
println("Run in HighOrderFunction")
f( ) // 輸入的函式在此開始執行
}
fun main() {
val f: funit = { println("Hello") } // f 指定成 λ 函式, 符合"無輸入參數,無傳回值"
g( f ) // 把函式丟進高階函式裏面去執行,
}
有一種有趣的高階函式:輸入參數是一個攜帶著物件類型的函式(lambda with Receiver), 透過綁定物件的手法, 讓呼叫的方法就好像侷限(scope)在物件裏面執行(context)一樣:
class Classname( ) {
fun doMethod( ) {
println("This is a method")
}
}
fun HighOrderλwithReceiver(λ: Classname.( ) -> Unit) {
val objectContext = Classname( )
objectContext.λ ( )
}
fun main( ) {
// fun doMethod { println("Global") }
HighOrderλwithReceiver {
// fun doMethod { println("Local") }
doMethod( ) // this.doMethod( )
}
}
上述當呼叫 HighOrderλwithReceiver 函式時, 因為最後一項參數是 λ 函式, 所以將它搬離出小括號, 搬離後剛好沒有參數, 小括號也跟著不用寫, 這是 Kotlin 語言的特異功能. 傳進來的 λ 區塊 { } , 在裏面的變數若沒有指定物件的範疇(scope)時, 編譯器就會在當地區域(Local scope),物件的本體區域(context 也就是 this), 及整體區域(Global scope) 推測(infer)並編譯(compile)出適當的變數或函數, 因為在高階函式 HighOrderλwithReceiver 的定義裏面, 已經預先設定進來的函式屬於 Classname 類型的一種方法, 就好像是先築起一道牆(scope), 利用類型建立起物件副本(instance)也就是物件文本(context),接著在物件的範疇下執行 λ 裏面的每一道程序, 因此 λ 區塊裏面, 呼叫的函式若是 Classname 裡面的從屬函數的話, this 就可以省略不寫了. 這是 Function literals with receiver 厲害的地方.
2019年7月1日 星期一
Kotin 的 Coroutines Channels
import kotlinx.coroutines.channels.*
之後才能使用 Kotlin 所提供的函式庫. 參考文章:
https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/08_Channels
通道是一種物件類型, 他所提供的方法 send 和 receive 是一種會依據狀況而隨時暫停(suspend)或重啟(resume)執行的函式(suspend function), 因此也只能用在 coroutine 區塊內發動. 接收通道的介面是 ReceiveChannel, 發送通道的介面是 SendChannel, 兼具接收與發送雙向類型的通道就是 Channel, 根據通道內容物的數量又可以區分幾種型式:
1. 數量為 0 時, 也就是無緩衝, 稱為 Rendezvous buffer(不見不散型通道), 收發端發現沒有東西就會在那邊等(suspend), 直到對方出現後才各自分道揚鑣(resume), 這是預設的通道型式:
val 不見不散通道 = Channel < String >
2. 大於 0 且數量有限, 稱為 Buffer channel (緩衝型通道), 一旦緩衝區滿了,發送端才會暫停(suspend), 而接收端只要發現沒東西時就會在那邊等(suspend).狀況一旦解除就會重啟(resume):
val 緩衝通道 = Channel < String >
3. 緩衝數量是 1, 且發送端不受限, 因此收發端只會收到最後一筆(前面發送的資料會被覆蓋)稱為 CONFLATED buffer (合而為一型緩衝通道),只有當接收端沒看到東西才會等待:
val 合而為一通道 = Channel < String >
4. 數量無限制時就是 UNLIMITED buffer(超級緩衝通道), 它就像是一個先進先出有秩序的隊伍(Queue), 只有當記憶體不夠時, 發送端才會產生例外(exception), 接收端只要收不到東西時就在那邊等待(suspend),狀況一旦解除就會重啟(resume):
val 超級通道 = Channel < String >
2019年6月27日 星期四
理解 Kotlin 的 Coroutines 的運作行為
import kotlinx.coroutines.*
fun main( ) {
println("main start")
val job = GlobalScope.launch {
println("Global Job 1 start")
delay(2000L) // suspend for 2 seconds
println("Global Job 1 stop")
}
println("main running")
runBlocking {
launch {
println("Job 2 start")
delay(500L) // suspend for 500ms = 0.5 sec
println("Job 2 stop")
}
launch {
println("Job 3 start")
delay(1000L) // suspend for 1 sec
println("Job 3 stop")
}
job.join() // wait job in GlobalScope.launch to finish
}
println("main stop")
}
看輸出結果可以想像: 上述程序建構了(launch) 了 3 個並行的工作, 就好像從主程序分裂出 3 條執行緒同時運行, 主程序走到 runBlocking 會一直等待直到它結束, 最後主程序執行最後一行列印指令終結任務:
main start
main running
Global Job 1 start
Job 2 start
Job 3 start
Job 2 stop
Job 3 stop
Global Job 1 stop
main stop
1. Dispatchers.Main: 通常是派任至前景執行緒
2. Dispatchers.IO: 通常是派任至背景,需長時間運作的執行緒
3. Dispatchers.Default: 通常是派任至背景, 短時間加速完成的執行緒
4. Dispatchers.Unconfined: 派任至目前呼叫的 Thread, 開始執行一直到第一個暫停點結束.
或許可以這樣理解: coutineScope { } 是 Kotlin 的一群 coroutines 區塊的生命期創建工(builder), 而 async { } 或 launch { } 僅是單一 coroutine 區塊生命期創建工.
Task, Context, Thread 都是作業系統的專有名詞, Task 是一個獨立作業的前景或背景任務簡稱任務, Context 是執行任務時的特定參數資料庫(database)簡稱文本, Thread 則是運行任務時可以走的程序路徑(program path)簡稱執行緒, 當 CPU 支援硬體的 multiThread (多工緒)或是透過軟體摹擬出 Thread pool(工緒池塘, 一整池的執行緒)時就可以將程式碼分配到不同路徑去執行,主要目地是增加運算效率並改善使用者體驗. 過多的 Threads 對記憶體需求將是一大考驗, 但對 Coroutines 卻不是問題, 這是他神奇的地方. 查了網路理解了一下 Coroutine 的運作原理: 首先透過 suspend 函式宣告後, 編譯器(compiler) 將 suspend 函式轉變成狀態機(state machine), 呼叫時再傳一個叫作 Continuation 的物件進入該函式, 之後透過 Continuation 物件去掌控函式暫停(suspend)或重啟(resume)的狀態, 有點類似Javascript yield 的運行方式, 暫停時傳回值並保留運作狀態, 後續回頭重啟時再從暫停的地方繼續執行,其壓根就不是 Thread, 因此可以透過派任的方式讓 Coroutines 在其他 Thread 上運行,最後來看一個例子:
import kotlin.coroutines.*
import kotlinx.coroutines.*
lateinit var continuation1: Continuation < Unit >
lateinit var continuation2: Continuation < Unit >
fun main(args: Array <String> ) {
println("Stat of main")
GlobalScope.launch(Dispatchers.Unconfined) {
println("Coroutine Start")
suspendCoroutine<Unit> { // it is a Continuation object
println("Coroutine seq#1 running")
continuation1 = it // save the continuation object to be resume later
println("Coroutine seq#1 suspend") // will suspend here
}
println("Coroutine running")
suspendCoroutine<Unit> { // it is a Continuation object
println("Coroutine seq#2 running")
continuation2 = it // save the continuation object to be resume later
println("Coroutine seq#2 suspend") // will suspend here
}
println("Coroutine stop")
}
println("Resume 1 in main")
continuation1.resume(Unit) // resume through continuation
println("Resume 2 in main")
continuation2.resume(Unit) // resume through continuation
println("End of main")
}
輸出結果:
Stat of main
Coroutine Start
Coroutine seq#1 running
Coroutine seq#1 suspend
Resume 1 in main
Coroutine running
Coroutine seq#2 running
Coroutine seq#2 suspend
Resume 2 in main
Coroutine stop
End of main
可以看到兩個 suspend 函式放在一個 launch 區塊內時, 他們是接續(sequential)運行, 並非想像中同時(parallel)運作, 若真要同時(運作兩個程序區塊, 那就要把他放在不同的 launch 或是 async 區塊去運行, 像是這樣就可以了:
import kotlin.coroutines.*
import kotlinx.coroutines.*
lateinit var continuation1: Continuation < Unit >
lateinit var continuation2: Continuation < Unit >
fun main(args: Array < String > ) {
println("Stat of main")
GlobalScope.launch(Dispatchers.Unconfined) {
println("Coroutine Start")
val seq1 = async {
suspendCancellableCoroutine < Unit > { // it is a Continuation object
println("Coroutine seq#1 running")
continuation1 = it // save the continuation object to be resume later
println("Coroutine seq#1 suspend") // will suspend here
}
"Coroutine seq#1 stop" // return a string
}
"Coroutine running"
val seq2 = async {
suspendCancellableCoroutine < Unit > { // it is a Continuation object
println("Coroutine seq#2 running")
continuation2 = it // save the continuation object to be resume later
println("Coroutine seq#2 suspend") // will suspend here
}
"Coroutine seq#2 stop" // return a string
}
println(seq1.await()) // will suspend until seq1 return
println(seq2.await()) // will suspend until seq2 return
println("Coroutine stop")
}
println("Resume 1 in main")
continuation1.resume(Unit) // resume through continuation
println("Resume 2 in main")
continuation2.resume(Unit) // resume through continuation
println("End of main")
}
執行結果:
Stat of main
Coroutine Start
Coroutine running
Coroutine seq#1 running
Coroutine seq#1 suspend
Coroutine seq#2 running
Coroutine seq#2 suspend
Resume 1 in main
Coroutine seq#1 stop
Resume 2 in main
Coroutine seq#2 stop
Coroutine stop
End of main
2019年6月23日 星期日
Android 的多工機制 HandlerThread , 理解 Looper , Handler 協調與分工
1. https://blog.mindorks.com/android-core-looper-handler-and-handlerthread-bd54d69fe91a
2. http://www.mallaudin.com/looper-handler-api-part-4
單純使用 Thread 可以讓程式放在背景去執行, 但 Thread 執行完就消失了. 為了要讓 Thread 重複利用, 分派複雜的工作又不會阻塞程式運行, 首先要了解 Looper 與 Handler 如何分工: Looper 稱為迴圈訊息搬運工, 他主要建構一個工作訊息串,把要工作的訊息駐列成隊, 當 Thread 裏面無任何工作訊息時, 就暫停進入休眠(剛開始仍需要發動 start( ) 啟動 Thread 開始運作), 一旦駐列有工作訊息就交給 Handler 在背景處理, Handler 可以想像是工頭(迴圈訊息處理工),負責處理程式運作之邏輯, 包含 message 的解釋(sendMessage), 或是處理像是 post 過來的片段程式碼, 若要自行定義含 Looper 的 Thread, 可以利用 Thread 裏面 run( ) 區塊, 去標示一個類似多工環境的程式迴圈, 介於 Looper.prepare( ) 與 Looper.loop( ) 之間, 從而讓 Thread 週而復始不停的運作又不阻塞(nonblock), 除非呼叫 Looper 的方法 quit( ) 打破迴圈, 才能結束 thread loop 讓系統回收資源. Android 內建的 HandlerThread 其實是一個內建了 Looper 的 Thread, 可以搭配後續的 Handler 去執行所要交代的任務:
val background = HandlerThread("Hi").run {
start() // HandlerThread.start( ) to run
object : Handler(looper) { // return Handler for this looper
override fun handleMessage(msg: Message) { //message handler
when(msg.what){ // get mesage
0 -> looper.quit() // quit the looper
//...
}
}
}
}
background.sendEmptyMessage(0) // send message 0 to HandlerThread
2019年6月18日 星期二
Android 使用 kotlin 語言讀取觸控螢幕訊息
pointerCount :螢幕觸控點累積的數量
actionIndex :觸發動作的索引鍵(index), 0是第 1 個 UP 點, pointerCount - 1 是最後一點
actionMasked :觸控動作遮罩用來判斷觸控點的動作, 共有以下 6 個工作狀態:
MotionEvent.ACTION_CANCEL : 觸控動作被取消了
MotionEvent.ACTION_DOWN :手指按壓觸控螢幕後, 當下 pointerCount==1 時觸發
MotionEvent.ACTION_POINTER_DOWN :當手指按壓螢幕後, pointerCount>1 時觸發
MotionEvent.ACTION_UP :手指離開觸控螢幕時, 當下 pointerCount==1 時觸發
MotionEvent.ACTION_POINTER_UP :當手指離開螢幕時, pointerCount>1 觸發
MotionEvent.ACTION_MOVE:只要有手指在螢幕上移動就觸發,此時actionIndex固定是 0,似乎是固定追蹤第一個 UP 的觸控點, 當pointerCount > 1 時, 額外觸控點座標就要限縮索引值後再去讀取: for(p in 1 until pointerCount) { getPointerId(p); getX(p) ; getY(p); ... } 而每個觸控點 id 透過 getPointerId(索引值) 加以區分, 其他常用的成員函數還有:
getX(index): 取得索引觸控點的X(橫)軸座標值
getY(index): 取得索引觸發點的Y(縱)軸座標值
getPointerId(index): 取得索引觸發點的唯一序號(id)
findPointerIndex(id): 取得觸發點唯一序號所對應的索引鍵
// Kotlin 程式碼: activity.kt ...
override fun onTouchEvent(event: MotionEvent): Boolean {
return with(event){
val index = actionIndex
val id = getPointerId(index)
var motion = "(" + getX(index).toInt() + "," + getY(index).toInt() + ")#" + id +"#\n"
motion += "[index=" + index + "]/"+ pointerCount+ "\n"
when(actionMasked) {
MotionEvent.ACTION_CANCEL -> motion +=":CANCEL"
MotionEvent.ACTION_DOWN -> motion +=":DOWN"
MotionEvent.ACTION_POINTER_DOWN -> motion +=":POINT DOWN"
MotionEvent.ACTION_UP -> motion +=":UP"
MotionEvent.ACTION_POINTER_UP -> motion +=":POINT UP"
MotionEvent.ACTION_MOVE -> {
motion +=":MOVE\n"
for(x in 1 until pointerCount) motion += "(" + getX(x).toInt() + "," + getY(x).toInt() + ")" +"#" + getPointerId(x) +"#\n"
}
}
Toast.makeText(applicationContext, motion, LENGTH_SHORT).show()
return true
}
}
2019年6月17日 星期一
理解 Android app 的 activity 與 context
https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html
一個 Android app 通常有兩種 context (用來承先啟後的上下文本): application context 及activity context, 只要 app 有任何(幕前或幕後的)Activity(活動)正在運作 applicationConext 都會存在, 但 activityContext 只存在當下(幕前)的活動, 一旦 app 被切換至幕後時, 例如翻轉螢幕時, 幕前的活動(activity)就會暫停(onPause), 接著 activityContext 就消失了, 系統要再重新喚醒(onResume)復出另一個幕前的 Activity, 因此活動暫停前, 一些在 Activity 的重要參數必須先保存, 當然 app 可以在 Activity 程式裏面動態分配空間來儲存資源並指定給變數參照, 一旦動態分配了資源, 後續資源回收機制(GC)若沒有適當釋放, 一旦活動結束, 參照的變數跟著消失, 就會造成記憶體洩漏危機(memory leak: 無法回收的資源), 萬一動態分配的空間夠大, 只要將手機多翻轉幾次, 很快的系統記憶體就不夠用了
2019年6月14日 星期五
Kotlin 的 try catch, if else, when( ) { else } 等區塊敘述與函數
val a: Boolean = try { TaskMayThrowException() ; true } catch (e: typeName) { false }
意思是說: 嘗試執行任務, 正常執行最後要回傳 true, 否則當產生不可預期事件時回傳 false, 詳情參考: https://kotlinlang.org/docs/reference/exceptions.html
要注意的是:
1. 必需用雙括號將程式碼分別圈出 try 與 catch 的程式區塊
2. 要接收的變數或常數, 要宣告資料型態(包含 catch 的接收參數), 主要是讓回傳資料型態一致
3. 無論有無發生例外, 都會執行 finally { } 程式區塊, 但 finally 區塊並不會回傳最後敘述值
4. try 區塊必須跟隨至少一個 catch 區塊, 或是接續 finally 區塊(可以省略 catch 區塊)處理
5. 描述宣告(statement)不會取值, 但陳述函數(expression)會取最後一行結果當函數值
把 if { } else { }, when(condition ) { else { } } 等當成陳述函, 詳情參考:
https://kotlinlang.org/docs/tutorials/kotlin-for-py/conditionals.html
Kotlin 的 run, apply, let, also, with
要分辨 run, apply, let, also 等擴展函式的用法, 首先要明瞭 λ(lambda) 的區塊表達式:
{ parameters -> retval }
或是參考之前的文章:
http://amitmason.blogspot.com/2018/08/kotlin-function.html
其中箭頭前面是: 分別用逗號分開的輸入參數,箭頭之後的最後一行是函式的傳回值(簡稱函數),當數入參數只有一個時就可以使用 it 代表唯一參數, 並且可以省略箭頭符號與參數的宣告,就只剩下:
{ ... ; retval }
物件產生後, 內建的 run { } 及 let { } 就是屬於這種型態的 λ, 兩者差別是 let 會將物件本身當唯一輸入參數也就是 it, 但 run 完全無輸入參數, 因此不能在 run 區塊裏面使用 it, 兩者相同的是: 將最後一行結果當作函數值傳回. 至於 also 同樣會將物件本身當唯一參數 it, 但不管最後一行的結果如何, also 的函數傳回值還是物件本身, apply 與 also 類似 , 函數傳回值也是物件本身, 但無輸入參數 it, 這些 run, let, apply, also 函式共通點是: 所有在區域(scope)內的從屬函式(member function)不需要引用物件句號, kotlin 自動會引用物件, 因此可以減少不少程式代碼, 下表就是根據 λ 的輸入與輸出列出函式差異, X = no, V = Yes
it=物件輸入 函數輸出=物件本身
run X X .run { member( ); retval }
let V X .let { member( ); it. ; ... ; retval }
apply X V .apply{ member( ); }.apply{ ... }
also V V .also { member( ); it. ; ... }.also{ ... }
由於 apply 與 also 函數傳回值就是物件本身, 因此可以不斷的串下去, 至於 run 與 let 傳回值是函式最後一行來指定, 因此適用於轉換不同物件時(transform)使用
說到 run 與 let 實際上的應用,以從屬函式(member function)的角度來看, let 有了 it 的加持後,就可從物件本身為起點全面展開, 但 run 只能從所屬函式為起點來展開:
val ret1 = objectTo.run { member1( ) ; member2( ); ... ; ret_val }
val ret2 = objectTo.let {
val some_ret = it.let { member1( ); member2( ); ... ; retval }
... Transform(it)
ret_val
}
同樣觀點也適用於分辨 apply 與 also (帶有 it 物件本身)運用上的差異. 他們與 run 和 let 差異只在傳回物件的不同, run 與 let 可以自訂傳回值, 但 apply 與 aslo 傳回物已經內定是物件本身, 因此程式碼裏面沒有傳回值:
objectTo.apply { member1( ) ; member2( ); ... }
objectTo.also {
it.also { member1( ); member2( ); ... }
... Transform(it)
}
而且 run, let, aslo, apply 可以互相穿插與搭配, 從單純需求(輸入參數 it , 輸出本身物件 this)的角度來看:
1. 不用再處理物件本身(it), 單純用 run 或 apply 就夠了, 之後若要傳回計算值(函數), 就用 run, 否則用 apply
2. 不用轉換物件, 最常用到的是 apply 或 also, 若又要處理物件本身(it)時就用 also
3. 若還需要處理物件本身並轉換成不同物件就只能用帶輸入參數(it)的 let
記憶口訣: let it be and run return, also it apply this. 意思是區分 let, run 和 also, apply 為兩組擴展函數, 只有 let run 可以自訂 return, 而 let 與 also 可以接 it, also apply 最後傳回自我. 最後一個 with 其實跟 run 差不多, 差異在語法的使用, with 是 kotlin 的語法, 但 run 是擴展函數 λ( ), 操作 with(obj) { ... } 的結果就如同 obj.run { ... } 同樣都會把區塊最後一行的結果當作函數值
2019年4月6日 星期六
關於 linux mint 的作業系統(軟硬體)升級
set timeout=3
set default=1
hiddenmenu
menuentry "GPT Linux mint 191 Read Only" {
set root=(hd0,1)
set iso=/boot/linux191.iso
loopback loop $iso
linux (loop)/casper/vmlinuz boot=casper iso-scan/filename=$iso locale=zh_TW.UTF-8 nomodeset
initrd (loop)/casper/initrd.lz
}
menuentry "GPT Linux mint 191 iso persistent" {
set root=(hd0,1)
set iso=/boot/linux191.iso
loopback loop $iso
linux (loop)/casper/vmlinuz boot=casper iso-scan/filename=$iso locale=zh_TW.UTF-8 persistent nomodeset
initrd (loop)/casper/initrd.lz
}
menuentry "GPT Ubuntu-18.10 iso" {
set root=(hd0,1)
set iso=/boot/ubuntu-18.10-desktop-amd64.iso
linux /boot/kernel418/vmlinuz boot=casper iso-scan/filename=$iso locale=zh_TW.UTF-8
initrd /boot/kernel418/initrd
}
menuentry "GPT Test Linux mint 19 iso" {
set root=(hd0,1)
set iso=/boot/linux191.iso
linux /boot/kernel418/vmlinuz boot=casper iso-scan/filename=$iso locale=zh_TW.UTF-8
initrd /boot/kernel418/initrd
}
Linux Mint 19 開啟終端機執行 inxi 顯示 CPU 資訊:
CPU~Quad core AMD Ryzen 3 2200G with Radeon Vega Graphics (-MCP-) speed/max~1438/3500 MHz Kernel~4.15.0-20-generic x86_64 Up~9 min Mem~1180.1/6980.1MB HDD~1370.3GB(59.9% used) Procs~186 Client~Shell inxi~2.3.56
2019年3月26日 星期二
c++ 善用 smart pointer 在物件建構法中傳回不同類型的物件並統一函式介面的作法
每個物件通常有不同的函式介面, 想要將兩種不同物件的方法統一成相同介面, 可以利用物件建構法將它封裝起來, 用這新類型物件去實例化想要的物件, 達成傳回多型物件(polymorphic class)的目的:
#include <stdio.h>
struct base { // 基礎類型
virtual void work( ){ }//擬統一的函式介面
virtual ~base(){ } // 避免繼承者,當在解構式中想刪除指標物件時,編譯器會報出錯務訊息
}; // 先建構基礎物件抽象類型, 統一函式介面
struct A: base { // 繼承 base 物件, 將物件用物件 A 封裝起來
void work( ) override { printf("A\n"); } // 將方法封裝並實現函式介面
};
struct B: base {// 繼承 base 物件, 將另一物件用物件 B 封裝起來
void work( ) override { printf("B\n"); } // 同樣將方法封裝並實現函式
};
struct C { // 一個全新多型類型(polymorphic class)
base *ptr;// 基礎物件的指標, 可以封裝物件 A 或 B
~C( ) { delete ptr; }
C(int a) { // 根據輸入值實例化不同物件, 奇數傳回 A, 偶數傳回 B, 等同於傳回不同物件
if (a%2) ptr = (base *) new A(); // 實例化物件 A 指標
else ptr = (base *) new B(); // 實例化物件 B 指標
}
void work( ){ ptr->work( ); }// 將多型物件成員用指標連結函式介面,方便使用
};
int main(){
for(int i=0; i < 10; i++) C(i).work( );
}
如果害怕指標的用法(像是造成 memory leak or double free), 可以用 smart pointer 將它封裝起來, 讓系統自動處理, 就不用擔心記憶體釋放的問題. c++ 所提供的 std::shared_ptr < > 就是 smart pointer 其中之一用來封裝類型指標的標準樣板函式(STL), 但有一點要注意的是, 用 smart pointer 封裝類型時, 該類型的建構及解構方法必須放在 public: 區, 讓 STL 可以正常呼叫, 否則會出現一堆錯誤訊息. 又或者很不喜歡用指標箭頭的方法, 還可以用左值(&alias) 換化成正常物件, 將方法從指標箭頭轉成正常物件的使用方式.
#include <stdio.h>
#include <memory> // smart pointer 要使用 memory 函式庫
typedef std::shared_ptr <base> baseptr; // 封裝成 smart pointer, 讓系統自動管理
struct base { // 基礎類型
virtual void work( ){}
virtual ~base(){ } // 避免繼承者若在解構式中想刪除指標物件時會報出錯務訊息
}; // 先建構基礎物件的抽象類型, 統一函式介面
struct A: base { // 繼承 base 類型, 可以將物件用類型 A 封裝起來
void work( ) override { printf("A\n"); } // 將方法封裝並覆蓋虛擬函式
};
struct B: base {// 繼承 base 類型 , 將另一物件用類型 B 封裝起來
void work( ) override { printf("B\n"); } // 同樣將方法封裝並覆蓋虛擬函式
};
struct C { // 一個全新多型類型(polymorphic class)
baseptr ptr;// 宣告 ptr 成為 smart pointer 可以封裝物件 A 或 B 類型的指標
// 無需解構式 ~C( ){ }
C(int a) { // 根據輸入值實例化不同物件, 奇數傳回 A, 偶數傳回 B, 等同傳回不同物件
if (a%2) ptr = baseptr(new A( )); // 實例化物件 A 指標, 同時轉換成 smart pointer
else ptr = baseptr(new B( )); // 實例化物件 B 指標, 同時轉換成 smart pointer
}
};
int main(){ // 呼叫時, 先用物件直取多型成員, 接著就能用指標 -> 呼叫函式介面
for(int i=0; i < 10; i++) C(i).ptr->work( );
}
2019年3月24日 星期日
linux mint 升級 openssl
1. 至官網下載原始碼 https://github.com/openssl/openssl
2. 解壓縮, 並進入目錄
3. 執行 ./config
4. 執行 make
5. 執行 sudo make install
預設程式庫會被安裝至 /usr/local/lib, 導致 openssl 找不到 libssl.so.3 及 libcrypto.so.3 等動態程式庫, 無法執行, 只要將符號連結過去就可以解決問題:
sudo ln -sf /usr/local/lib/libcrypto.so.3 /usr/lib
sudo ln -sf /usr/local/lib/libssl.so.3 /usr/lib
Libressl:
1. 這是另一個源出於 Oenssl 的分支, 程式碼是相容的, 號稱刪除一些不必要的程式碼, 讓它更精減, 目標也要讓程式更安全可靠, 編譯的方式大同小異, 參考文章: http://linuxg.net/how-to-install-libressl-2-1-6-on-linux-systems , 可到官網下載源碼: https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-2.9.0.tar.gz
2. 解壓縮, 並進入目錄
3. 執行 ./configure
4. 執行 make
5. 執行 sudo make install
6. 執行 sudo ldconfig
預設程式庫也是安裝至 /usr/local/lib, 如果導致 openssl 找不到 libssl.so.45 及 libcrypto.so.45 等動態程式庫, 只要將符號連結過去就可以解決問題:
sudo ln -sf /usr/local/lib/libcrypto.so.45.0.1 /usr/lib/libcrypto.so.45
sudo ln -sf /usr/local/lib/libssl.so.47.0.1 /usr/lib/libssl.so.45
編譯程式時要加上選項參數 -L/usr/local/lib -lssl -lcrypto, 例如:
g++ https.c -L/usr/local/lib -lssl -lcrypto -pthread -o https
或是編寫一個簡單的 Makefile
#Makefile to link with ssl, crypto, pthead library
https:
g++ https.c -L/usr/local/lib -lssl -lcrypto -pthread -o$@
clean:
rm -f https
之後只要下一個命令, 不用再打那麼多字
make
Boringssl:
1. 同樣源自於 Openssl 的另一分支 , 由 google 負責維護的 tls/ssl 程式庫, 首先要安裝 cmake 及 golang 兩個必要開發工具:
sudo apt-get install cmake golang
2. 到官網用 git 下載源碼, 複製整個 boringssl 目錄
cd $HOME/Downloads
git clone https://boringssl.googlesource.com/boringssl
3. 進入目錄, 建個子目錄 build, 進入子目錄 build, cmake .. , make
cd $HOME/Downloads/boringss
mkdir build
cd build
cmake ..
make
4. 例如要編譯 c 聯結 Boringssl 程式庫, 可以進到工作目錄, 把編譯好的程式庫複製過來作聯結:
cd $HOME/work/c/https
cp $HOME/Downloads/boringssl/build/ssl/libssl.a .
cp $HOME/Downloads/boringssl/build/crypto/libcrypto.a .
g++ https.c -L. -lssl -lcrypto -pthread
c++ 寫一個簡單的 https server
先安裝好 openssl 開發檔及準備自我認証的憑證
sudo apt-get install libssl–dev
openssl req -x509 -nodes -days 30 -newkey rsa:2048 -keyout mykey.pem -out mycert.pem
編輯 https server 主程式:
// server.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
int main()
{
SSL_library_init();
SSL_CTX *ctx = SSL_CTX_new(SSLv23_server_method());
SSL_CTX_set_ecdh_auto(ctx, 1);
SSL_CTX_use_certificate_file(ctx, "mycert.pem", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "mykey.pem", SSL_FILETYPE_PEM);
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int server = socket(AF_INET, SOCK_STREAM, 0);
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(5000);
addr.sin_addr.s_addr = INADDR_ANY;
int ok = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &ok, sizeof(ok));
bind(server, (struct sockaddr*)&addr, sizeof(addr));
listen(server, 10);
while(1) {
int client = accept(server, (struct sockaddr*)&addr, &len);
printf("Accept client IP= %s : port=%d\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
SSL * ssl = SSL_new(ctx);
SSL_set_fd(ssl, client);
SSL_accept(ssl);
char buf[1024];
int bytes = SSL_read(ssl, buf, sizeof(buf));
if (bytes > 0) { buf[bytes]=0; printf("Client: %s", buf); }
char msg[] = "<html><head><meta charset='UTF-8'></head><Body>歡迎光臨</Body></html >";
buf[0]=0;
sprintf(buf ,"HTTP/1.0 200 OK\n");
sprintf(buf+strlen(buf),"Server: Welcome\n");
sprintf(buf+strlen(buf),"Content-Type: text/html\n");
sprintf(buf+strlen(buf),"Content-Length: %11ld\n",strlen(msg));
sprintf(buf+strlen(buf),"Connection: close\n\n");
sprintf(buf+strlen(buf),"%s",msg);
SSL_write(ssl, buf, strlen(buf));
SSL_free(ssl);
close(client);
};
close(server);
SSL_CTX_free(ctx);
}
編譯並執行
g++ server.c -lssl -lcrypto -o server && ./server
用瀏覽器瀏覽 https://127.0.0.1:5000 , 如有警告信息, 選取並信任該憑證
c++ 寫一個簡單的 https client
設定完 https server(以下使用 local host: 127.0.0.1 TCP port: 5000). 安裝 SSL 開發檔:
sudo apt-get install libssl–dev
編輯一個簡單的 https client 文字檔:
// client.c
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <openssl/ssl.h>
struct sessionTLS {
int sid;
SSL_CTX *ctx;
SSL *ssl;
};
void freeTLS(sessionTLS &tls) {
if (tls.ssl != NULL) SSL_free(tls.ssl);
if (tls.sid > 0) close(tls.sid);
if (tls.ctx != NULL) SSL_CTX_free(tls.ctx);
}
void bindSSL(sessionTLS &tls) {
tls.ssl = SSL_new(tls.ctx);
if(tls.ssl != NULL ) {
SSL_set_fd(tls.ssl, tls.sid);
SSL_connect(tls.ssl);
printf("\nsession TLS encryption: %s\n", SSL_get_cipher(tls.ssl));
X509 * CA = SSL_get_peer_certificate(tls.ssl);
if ( CA != NULL ) {
char * subject= X509_NAME_oneline(X509_get_subject_name(CA), 0, 0);
char * issuer = X509_NAME_oneline(X509_get_issuer_name(CA) , 0, 0);
printf("CA: %s , %s\n", subject, issuer);
free(subject);
free(issuer);
X509_free(CA);
}
}
}
sessionTLS connectIP(const char *ipaddress, int tcpPort){
SSL_library_init();
sessionTLS tls;// create a new TLS object
tls.ctx = SSL_CTX_new(SSLv23_method());
tls.sid = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in host;
host.sin_family = AF_INET;
host.sin_port = htons(tcpPort);
inet_pton(AF_INET, ipaddress, &host.sin_addr) ;
connect(tls.sid, (sockaddr*)&host, sizeof(host));
bindSSL(tls);// bind tls with SSL
return tls;
}
int writeTLS(sessionTLS &tls, char *msg, int len) { return SSL_write(tls.ssl, msg, len); }
int readTLS(sessionTLS &tls, char *buf, int len) { return SSL_read(tls.ssl, buf, len); }
int main() {
char msg[]="Hello\n";
char buf[1024];
sessionTLS tls = connectIP("127.0.0.1", 5000);
writeTLS(tls, msg, strlen(msg));
int bytes = readTLS(tls, buf, sizeof(buf));
char *ptr = buf;
while (bytes-- >0) printf("%c", *ptr ++);
printf("\n");
freeTLS(tls);
}
用 g++ 編譯並執行:
g++ client.c -lssl -lcrypto && ./a.out
2019年3月23日 星期六
用 Javascript 寫一個任意位元的亂數產生器
function random(maxnum) {
// 將產生後的亂數用時間函數運算過, 傳回整數並把它限制在 {0, ..., 最大值-1} 之間:
let srand = Math.floor(Math.random( )*maxnum) ^ new Date().getMilliseconds();
return srand % maxnum ;
}
function srandBits(bits) {
// 產生任意位元數的亂數
let r = bits % 8; // 以 8 位元為單位(次), 產生一個亂數
let num = [ ]; // 初始化儲存陣列
if (r > 0) {
num.push(random(1 << r)); // 先產生餘數位元的亂數
bits -= r;
}
while (bits > 0) { // 還剩下的位元數, 一次產生一個位元組(8 bits = 1 byte)的亂數
num.push(random(256)); // 產生 8 位元的亂數, 0 <= 亂數 < 256
bits -= 8; // 減掉每次產生的 8 位元
}
return num; // 傳回陣列
}
console.log("random:" + srandBits(32));
2019年3月19日 星期二
ECDSA 數位簽章
橢圓曲線數位簽章演算法:
假設 ECC(x,y) 是橢圓曲線方程式, 座標產生器 G 將大整數映設到 ECC 曲線上, 使用質數 p 取餘數(%), 1< n < p-1, 假設 order number = n, nG = O
1. 模除數改用橢圓曲線的 order number 計算
2. 把私章 d, 1 < d < n -1 映射到橢圓曲線的座標 Q = dG = d*G(1) = G(d) , Q 就是簽章者的公鑰
3. 用 sha256 擷取文件特徵值 z, z < p,並選一亂數 k, 映設到橢圓曲線 (x, y) = kG = G(k) , 1 < k < n -1
4. 只取出 G(k)的 x 座標, r = x, 計算出 k 的模倒數 k-1 = 1/k, (k * k-1) % n = 1
5. 計算 s = (z + rd) / k % n = (z + rd)*k-1 % n, 1 < s < n - 1
6. (z, r, s , Q)就是帶數位簽章的文件
驗證方式:
1. 模除數改用橢圓曲線的 order number 計算,
2. 計算 s 的模倒數 s-1 = 1/s, (s * s-1) % n = 1
3. 計算 u = z*s-1 mod n
4. 計算 v = r*s-1 mod n
5. 用 ECC 加法運算, 計算出座標 K = uG + vQ =G(z*s-1) + G(rd*s-1) = G( (z+rd)*s-1)=G(k)
6. 取出 K 的 x 座標, 應該要等於 r
2019年3月10日 星期日
WebSocket 一些重要觀念
1. 交握協定: https://tools.ietf.org/html/rfc6455#section-1.3
WebSocket 使用的 tcp port 跟 http 相同, 當使用者用 http GET 方法時, 其中攜帶的標頭(header)就包含升級(Upgrade)至 websocket 的動作, 標籤(Sec-WebSocket-Key)後面附帶一串 base64 明文,每次開啟交握協定時都會不同,以一組 base64 明碼為例:
dGhlIHNhbXBsZSBub25jZQ==
如果將它解開,其實裏面是一組 16 位元組的密碼.假設客戶端所發送封包內容是:
GET /xxx HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
http 伺服器端收到封包偵測到 Sec-WebSocket-Key, 就將明碼串接一個共通識別碼(GUID)形成長字串:
dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
用雜湊函數 sha1 計算並轉成 base64 明文簡化成:
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
伺服器端產生一個封包把它放在 Sec-WebSocket-Accept: 標籤的後面, 回應用戶端切換通信協定,完成交握程序
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
2. 封包格式: https://tools.ietf.org/html/rfc6455#section-5.1
一旦交握完成, 雙方封包內就不再用 http 的標頭(header), 而是轉成 WebSocket 格式的標頭, WebSocket 客戶端送的資料必須使用 xor 加密, 所附帶 4 位元組(byte)遮蔽碼負責把資料每 4 個位元組循環作加密運算(xor)轉成亂碼,伺服器端則利用這 4 個遮蔽碼同樣每 4 個位元組循環作解密運算(xor)轉成明文.
標頭第 1 個位元組分上半部與下半部:
上半部最高位元(MSB) 標示該封包是否是最後一筆封包, 後 3 個位元保留未使用
下半部 4 個位元代表該封包的格式(opcode),分別如下:
0000 表示是上一封包的延續
0001 表示文字格式(ascII code)
0010 表示16進制(binary code)
1000 表示關閉 Websocket 通道
1001 表示 ping 封包
1010 表示 pong 封包
其他保留未使用
用 c++ 語言描述:
bool fin = p[0] & 0x80;
auto opcode = p[0] & 0xf ;
if( fin ) printf("final frame\n");
switch(opcode){
case 0 : printf("continue frame\n") ;break;
case 1 : printf("text frame\n") ;break;
case 2 : printf("binary frame\n") ;break;
case 8 : printf("close frame\n") ;throw "WebSocket: Connection close!";
case 9 : printf("ping frame\n") ;break;
case 10: printf("pong frame\n") ;break;
default: printf("reserved frame\n") ;break;
}
標頭第 2 個位元組指示隨後的資料, 最高位元(MSB)代表是否資料經過 xor 運算, 剩下7個位元是資料長度數值, 當數值小於 126, 就當成是資料長度(payload length), 如果等於 126, 那要用隨後的另兩個位元組當資料長度, 但如果大於 126(也就是等於 127)時, 就用隨後 8 個位元組當資料長度.客戶端送來的資料必須用 xor 運算過, 因此資料長度後面,必定先緊隨 4 個位元組的遮蔽碼(mask key), 其後才是資料(payload). 而伺服器傳給客戶端並不需要加密, 也就無需遮蔽碼, 因此長度後面緊跟的就是資料(payload)了, 簡單用 c++ 語言來解碼:
if (p[1] & 0x80) { // maskbit should be true
p[1] &= 0x7f; // data in buffer will be de-mask
auto wslen = p[1];// length value copy
p += 2; // don't need the first 2 bytes in header
long long payloadlen;// 8 bytes integer to get payload length
if (wslen < 126 ) payloadlen = wslen;
else { // 126: following 2bytes, 127: following 8 bytes
payloadlen = *p ++;
payloadlen = (payloadlen << 8) + *p ++;
if (wslen>126) for (auto i=0; i < 6; i++) payloadlen = (payloadlen << 8) + *p ++;
}
unsigned char maskkey[4]; // to store maskkey
for (auto i=0; i < 4; i++ ) maskkey = p[i]; // maskkey copy
for (auto i=0; i < payloadlen; i++) p[i] = p[i+4] ^ maskkey[i%4]; // decode & move
size -= 4; // size of buffer was reduced by 4 bytes of maskkey
auto &payload = p; // payload now point to data
}
3. 客戶端可以從 http 伺服器端接收一個 index.html 檔案, 編寫的內容像是:
<html><head><title>WebSocket 發送與接收示範</title><meta charset="UTF-8"></head>
<script>
const ws = new WebSocket('ws://127.0.0.1:5000');// port 必須與 http 相同
ws.addEventListener('open', () => {
msg = "Hello, thhis is a test message.";
console.log("Send: " + msg);
ws.send(msg);
});
ws.addEventListener('message', event => {
console.log("Get:: " + event.data);
ws.close();
});
</script>
<body>打開瀏覽器的 Console 視窗觀看結果</body>
</html>
內嵌的 Javascript 是一個用來開啟 WebSocket 通道的程式碼, 假設 http 伺服器使用 tcp port = 5000, 將上述的 index.html 放在伺服器的 www 根目錄底下, 用瀏覽器開啟 URL 的頁面 http://127.0.0.1:5000/index.html