2014/03/04

在iOS應用程式中使用sqlite

SQLite是輕量級的資料庫管理系統,多半用於手機應用程式中。SQLite支援大多數常見的SQL語句,對於已經熟悉在大型伺服器上使用SQL語言的人來說,是相當容易上手的。

以下,將介紹如何在iOS應用程式中使用SQLite。

建立sqlite檔案

sqlite檔案是所有SQLite資料實體所存放的位置。 網路上已有諸多SQLite工具程式可以建立sqlite檔案。

如Firefox的附加元件SQLite Manager:

建立好Table之後,將sqlite檔案加入專案中即可。

拷貝sqlite檔案至documents目錄底下

sqlite檔案應存放在應用程式的documents目錄底下,假設我們的sqlite檔案的檔案名稱為myproject.sqlite,則取得該路徑的方式如下:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentsDirectory = [paths objectAtIndex:0];

//sqlite檔案在documents目錄底下的路徑
NSString *pathInDoc = [documentsDirectory stringByAppendingPathComponent:@"myproject.sqlite"]; 

iOS應用程式應該在第一次執行時,將sqlite檔案由專案中拷貝到至documents目錄底下。

因此,我們在程式每一次啟動時,檢查documents目錄底下的sqlite檔案是否存在:

//檢查sqlite檔案是否存在
if([fileManager fileExistsAtPath:pathInDoc]) {

    //sqlite檔案不存在,由專案中拷貝至documents目錄底下
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"myproject.sqlite"];
    int result = [fileManager copyItemAtPath:defaultDBPath toPath:pathInDoc error:&error];
}

使用C語言API

欲使用C語言API存取SQLite資料庫,必須在專案中加入libsqlite3.0.dylib

並在程式中引入標頭檔:

#import <sqlite3.h>

開啟資料庫,建立sqlite3物件

在存取資料庫前,必須先開啟資料庫,建立sqlite3物件。 下方範例中,pathInDoc為上方範例中取得的sqlite檔案路徑。 由於使用的是C語言API,因此,需要使用NSString的UTF8String方法,來取得C式字串(char*);

sqlite3 *database;
int result = sqlite3_open([pathInDoc UTF8String], &database);
if (result != SQLITE_OK){
    //錯誤處理
}

在以下存取資料庫的C API中,其參數都需要用到sqlite3物件。

寫入資料庫(C API)

首先,使用sqlite3_prepare_v2函式建立sqlite3_stmt物件,並傳入SQL語句參數。SQL中可以使用問號,於稍後程式中輸入參數。

在sqlite3_stmt建立後,再使用sqlite3_bind_int函式設定各個問號所代表的參數內容。

參數設定完成後,最後使用sqlite3_step函式來執行。完整範例如下:

char *sqlCString = "UPDATE Content SET isFavorite = ? WHERE id = ? ;";
sqlite3_stmt *stmt;
int result = sqlite3_prepare_v2(database, sqlCString, -1, &stmt, nil);
if(result != SQLITE_OK) {
    //錯誤處理
    return;
}

//設定isFavorite(第1個問號)值為1
sqlite3_bind_int(stmt, 1, 1);   

//設定id(第2個問號)值為5
sqlite3_bind_int(stmt, 2, 5);   //設定id值為5

result = sqlite3_step(stmt);
if(result != SQLITE_DONE) {
    //錯誤處理 NSLog(@"An error occurs when executing sql! (%i)",result);
}
sqlite3_finalize(stmt);

執行完寫入後,務必執行sqlite3_finalize,釋放相關資源。

讀取資料庫(C API)

讀取資料庫時,同樣使用sqlite3_prepare_v2函式建立sqlite3_stmt物件、傳入SQL語句參數。

取回結果時,可以使用sqlite3_column_int、sqlite3_column_text函式,以取得對應資料形態的結果。

char *sqlCString = "SELECT c.subject_id, c.title FROM Content c WHERE c.id = ?;";
sqlite3_stmt *stmt;
int result = sqlite3_prepare_v2(database, sqlCString, -1, &stmt, nil);

//設定id(第1個問號)值為1
sqlite3_bind_int(stmt, 1, 5);

while(sqlite3_step(statement) == SQLITE_ROW){
    //取得第1個參數 (整數形態)
    int subectID = sqlite3_column_int(statement, 1);

    //取得第2個參數 (字串形態)
    NSString* title = [[NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 2)] copy];

}
sqlite3_finalize(stmt);

執行完讀取後,同樣需執行sqlite3_finalize,釋放相關資源。

使用FMDB API

雖然C語言API並不算是太難使用,但畢竟是純C語言界面,與Objective-C之間往往還需要轉換資料形態才能使用(如:NSString與char陣列)。為了能讓程式碼更為簡潔、更易於管理,可以使用FMDB。

FMDB是一套以Objective-C寫成的wrapper,將C語言API包起來藏在內部,讓其他開發者能直接使用Objective-C界面來存取資料庫,無需考慮Objective-C與C語言介接的細節。

欲使用FMDB,只要下載FMDB專案,在我們自己的專案中加入FMDB資料夾。

並在程式中引入FMDB標頭檔:

#import "FMDatabase.h"

開啟資料庫(FMDB API)

使用FMDB存取資料庫前,需要建立建立FMDatabase物件。

FMDatabase *fmdb = [FMDatabase databaseWithPath:pathInDoc];
if (![fmdb open]) {
    //錯誤處理 NSLog(@"fmdb open error");
}

寫入資料庫(FMDB API)

使用FMDB寫入資料庫的範例,比使用C API簡潔很多。範例如下:

NSString *sql = "UPDATE Content SET isFavorite = ? WHERE id = ? ;";
int updateCount = 0;
if([fmdb executeUpdate:sql,@1,@5]) {
     updateCount = [fmdb changes];
}
else {
    //錯誤處理
}
[fmdb close];

讀取資料庫(FMDB API)

使用FMDB讀取資料庫也同樣簡潔:

FMResultSet *rs = [fmdb executeQuery:@"SELECT c.subject_id, c.title FROM Content c WHERE c.id = %i;", @5];

while ([rs next]) {
    //取得第1個參數 (整數形態)
    int subectID = [rs intForColumn:@"subject_id"];

    //取得第2個參數 (字串形態)
    NSString* title = [rs stringForColumn:@"title"];
}
[fmdb close];

快速讀取

此外,FMDB還提供了便捷的API。

像是,如果我們只需要取得一個SQL語句回傳的某個結果:

int score = [fmdb intForQuery:@"SELECT score FROM Student WHERE Name = ?",@"Michael”];

一行就可以解決。

結語

撰寫手機應用程式石,存放資料到手機上,以及讀取先前儲存的資料,幾乎是一定會遇到的需求。而SQLite則提供了大家所熟悉的解決方案。對於熟悉SQ語法的人,在撰寫手機應用程式時,更應了解SQLite的使用方式。

erlang basics - other issues

apply

apply(Mod, Func, [Arg1, Arg2, ..., ArgN]) 是 BIF,可讓我們呼叫一個模組裡的一個函數。相當於呼叫
Mod:Func(Arg1, Arg2, ..., ArgN)

apply 用在當程式裡,Mod 與 Func 都是動態計算出來的時候。

1> apply(erlang, atom_to_list, [hello]).
"hello"

但程式要盡量避免使用 apply,因為使用 apply 時,分析工具無法判斷程式碼,編譯器也無法進行最佳化。

attribute

語法為 -AtomTag(...) ,用來定義一個檔案的某些特性。

predefined attributes

  • -module(modname).
    模組宣告。modname 必須要是 atom,檔案名稱習慣是 modname.erl

  • -import(Mod, [Name1/Arity1, Name2/Arity2, ...]).
    匯入指定 Mod 模組,將具有 Arity1 引數的 Name1 函數,匯入後,呼叫函數就不需要加上模組名稱。

  • -export([Name1/Arity1, Name2/Arity2, ...]).
    匯出 Name1/Arity1, Name2/Arity2 函數,只有被匯出的函數,可以從模組外被呼叫。

  • -compile(Options)
    加入 Options 到編譯器選項中,例如 -compile(export_all) 可將模組中所有函數都匯出

  • -vsn(Version)
    指定模組版本

user-defined attributes

語法如下
-SomeTag(Value).

模組屬性的值,會被編譯到模組中,可以在執行期被取出。每次編譯時,module_info/0 與 module_info/1 都會自動被建立。

範例

-module(attrs).
-vsn(1234).
-author({joe,armstrong}).
-purpose("example of attributes").
-export([fac/1]).

fac(1) -> 1;
fac(N) -> N * fac(N-1).

測試

(erlangotp@yaoclNB)1> attrs:module_info().
[{exports,[{fac,1},{module_info,0},{module_info,1}]},
 {imports,[]},
 {attributes,[{vsn,[1234]},
              {author,[{joe,armstrong}]},
              {purpose,"example of attributes"}]},
 {compile,[{options,[{i,"D:/projectcase/erlang/erlangotp/include"},
                     debug_info,nowarn_export_all,nowarn_export_vars,
                     nowarn_shadow_vars,warn_unused_function,
                     warn_deprecated_function,nowarn_obsolete_guard,
                     nowarn_unused_import,warn_unused_vars,
                     warn_unused_record]},
           {version,"4.9.4"},
           {time,{2014,1,16,9,11,33}},
           {source,"d:/projectcase/erlang/erlangotp/src/attrs.erl"}]}]
(erlangotp@yaoclNB)2> attrs:module_info(attributes).
[{vsn,[1234]},
 {author,[{joe,armstrong}]},
 {purpose,"example of attributes"}]

可以使用 beam_lib 的函數,在不載入模組的條件下,分析模組的內容。

-module(extract).
-export([attribute/2]).

attribute(File, Key) ->
    case beam_lib:chunks(File,[attributes]) of
        {ok, {_Module, [{attributes,L}]}} ->
            case lookup(Key, L) of
                {ok, Val} ->  
                    Val;
                error ->
                    exit(badAttribute)
            end;
        _ -> 
            exit(badFile)
    end.

lookup(Key, [{Key,Val}|_]) -> {ok, Val};
lookup(Key, [_|T])         -> lookup(Key, T);
lookup(_, [])              -> error.

測試

(erlangotp@yaoclNB)8> extract:attribute("D:/projectcase/erlang/erlangotp/ebin/attrs.beam", author).
[{joe,armstrong}]

block expression

使用 block expression 將一連串的 expression 接在一起,此區塊的值,是最後一個 ExprN 的值。

begin
    Expr1,
    ...,
    ExprN
end

boolean expression

有四種可能的 boolean expression

  1. not B1
  2. B1 and B2
  3. B1 or B2
  4. B1 xor B2
1> not true.
false
2> true or false.
true
3> (2>1) or (3>4).
true

character set

erlang R16 就支援 source code 裡面可以寫 unicode 的文字,但是預設還是 ISO-Latin-1,而預定 R17 將會把預設值改為 UTF-8。

epp

erlang 的模組在編譯前,會先由 epp 將 巨集展開,並 include 需要的函數檔。

escape

在 "" 裡面可以使用 \ 用以輸入不能列印的字元。

escape char description 整數碼
\b 後退 8
\d 刪除 127
\e Esc 27
\f 換頁 12
\n 換行 10
\r CR 13
\s space 32
\t tab 9
\v 垂直跳格 11
\NNN \NN \N 八進位字元(N是0..7)
\^a..\^z or \^A..\^Z Ctrl+A to Ctrl+Z 1 to 26
\' 單引號 39
\" 雙引號 34
\ 反斜線 92
\C C 字元的 ASCII code (整數)

測試

1> io:format("~w~n", ["\b\d\e\f\n\r\s\t\v"]).
[8,127,27,12,10,13,32,9,11]
ok
2> io:format("~w~n", ["\123\12\1"]).
[83,10,1]
ok
3> io:format("~w~n", ["\'\"\\"]).
[39,34,92]
ok
4> io:format("~w~n", ["\a\z\A\Z"]).
[97,122,65,90]
ok

expression 表示式 與 expression sequence 表示式序列

「估算後會產生值」的就是表示式。catch、if、try...catch 都是表示式,而 record、module 無法被估算,就不是表示式。

表示式序列 E1,E2,...,En 中間用逗號隔開,通常出現在 -> 後面,整個序列的值是最後一個 En 的值。

函數參考

fun LocalFunc/Arity
fun Mod:RemoteFunc/Arity

範例

%% x1.erl
-module (x1).
-export([square/1]).
square(X) -> X*X.

double(L) -> lists:map(fun square/1, L).

%% x2.erl
-module (x2).

double(L) -> lists:map(fun x1:square/1, L).

include file

習慣上 include file的副檔名為 .hrl。檔名中要包含絕對或相對路徑。

通常用在共用的 record 定義。

範例

-include (FileName).

-include ("include/file.hrl").

在編譯時,可以用
erlc 的 -I 或是 c("src/source.erl", [{i, "include/"} ] ). 的方式,增加搜尋檔案的路徑。

引用含有版本號碼的 library

如果程式依賴別的 erlang lib,必須要知道安裝路徑,把目錄納入 include path,此外,安裝路徑可能有版本號碼,例如 kernel 可能會安裝在 c:\Program files\erl5.10.4\lib\kernel-2.16.4 ,於是 include_lib 會自動將檔案名稱起始處去除版本號碼,成為匹配路徑 kernel/ ,在該路徑下面,尋找 include/file.hrl。

-include_lib (FileName).

-include_lib ("kernel/include/file.hrl").

list operation: ++ and --

++ 與 -- 是 infix operator,用來進行 list 的加法與減法。

A -- B 會將 A 中所有 B 的元素移除,如果 X 在 B 只出現 K 次,則只有 A 裡面的前 K 次 X 會被移除。

範例

1> [1,2,3] ++ [4,5,6].
[1,2,3,4,5,6]
2> A=[a,b,c,1,d,e,1,x,y,1].
[a,b,c,1,d,e,1,x,y,1]
3> A -- [1].
[a,b,c,d,e,1,x,y,1]
4> A -- [1,1].
[a,b,c,d,e,x,y,1]
5> A -- [1,1,1].
[a,b,c,d,e,x,y]
6> A -- [1,1,1,1].
[a,b,c,d,e,x,y]

在 pattern 中也可以使用 ++

f("begin" ++ T) -> ...
f([$b,$e,$g,$i,$n | T]) -> ...

macro 巨集

macro 的定義方式如下

-define (Constant, Replacement).
-define (Func(Var1, Var2, ..., VarN), Replacement).

當 epp 遇到 ?MacroName 時,會進行巨集展開

範例

-define (macro1(X,Y), {a,X,Y}).

foo(A) ->
    ?macro1(A+10, b).

% 會展開成
foo(A) ->
    {a, A+10, b}.

有一些事先定義的巨集:

  1. ?FILE 展開為目前的檔案名稱
  2. ?MODULE 展開為目前的模組名稱
  3. ?LINE 展開為目前的行號

巨集中的控制

-undef(Macro):取消巨集的定義
-ifdef(Macro):只有當巨集定義時,才估算下面的表示式
-ifndef(Macro):只有當巨集沒有被定義時,才估算下面的表示式
-else:可在 ifdef 與 ifndef 後面使用
-endif:作為 ifdef 與 ifndef 的結尾

範例

%% m1.erl
-module(m1).
-export([start/0]).

-ifdef(debug).
-define(TRACE(X), io:format("TRACE ~p:~p ~p~n",[?MODULE, ?LINE, X])).
-else.
-define(TRACE(X), void).
-endif.

start() ->  loop(5).

loop(0) -> 
    void;
loop(N) ->
    ?TRACE(N),
    loop(N-1).

測試

(erlangotp@yaoclNB)6> c("m1",{d,debug}).
{ok,m1}
(erlangotp@yaoclNB)7> m1:start().
TRACE m1:15 5
TRACE m1:15 4
TRACE m1:15 3
TRACE m1:15 2
TRACE m1:15 1
void
(erlangotp@yaoclNB)8> c("m1").
{ok,m1}
(erlangotp@yaoclNB)9> m1:start().
void

pattern 中的巨集運算子

在第三行呼叫 {tag1,A,B} 時,系統會重新建立 {tag1,A,B}

func1([{tag1,A,B}|T]) ->
    ...
    ... f(..., {tag1,A,B}, ...)
    ...

更有效且不會出錯的方法為,把 {tag1,A,B} 指定為暫時變數 Z,然後傳遞給 f

func1([{tag1,A,B}=Z|T]) ->
    ...
    ... f(..., Z, ...)
    ...

兩個 term 也可以用同樣的方式改寫

func1([{tag,{one,A}=Z1,B}=Z2|T]) ->
    ...
    ... f(..., Z2, ...)
    ... g(..., Z1, ...)
    ...

運算子的優先順序

不確定優先順序時,就加上括號。

運算子 組合性
:
#
+, - , bnot, not
/, *, div, rem, band, and 左組合
+, -, bor, bxor, bsl, bsr, or, xor 左組合
++, -- 右組合
==, /=, =<, <, >=, >, =:=, =/=
andalso
orelse

行程字典 process dictionary

每個 process 都有自己的私用資料儲存空間,稱為 process dictionary,這是一個 associative array,也就是 map、hashmap或hashtable。

此字典可以用以下 BIF 處理

  1. @spec put(Key, Value) -> OldValue.
    加入一個 key, value 到 process dictionary,會傳出該 Key 對應的舊 OldValue,如果沒有舊值,就會傳出 undefined.
  2. @spec get(Key) -> Value.
    如果沒有Key,就會傳出 undefined.
  3. @spec get() -> [{Key, Value}].
    取得整個 list
  4. @spec get_keys(Value) -> [Key].
    取得具有某個值的所有 keys 組成的 list
  5. @spec erase(Key) -> Value.
    如果沒有Key,就會傳出 undefined.
  6. @spec erase() -> [{Key, Value}].
    移除整個 process dictionary
1> get().
[]
2> put(x,20).
undefined
3> get(x).
20
4> get(y).
undefined
5> put(y,40).
undefined
6> get(y).
40
7> get().
[{y,40},{x,20}]
8> erase(x).
20
9> get().
[{y,40}]
10> erase().
[{y,40}]
11> get().
[]

注意:盡量不要使用 process dictionary

使用 process dictionary 可能會導致一些 bug,難以除錯,只有一個地方可以使用,就是拿來儲存「只寫入一次」的變數,如果一個 key 得到的一個值僅此一次,且不會再變動,那麼就可以放入 process dictionary。

Ref

Ref 是全域唯一的 erlang term,他是透過 erlang:make_ref() 所建立的。

可用來當作獨特唯一的標籤,包含在資料庫中,之後可以比對是否相等 equality。例如在錯誤追蹤系統終,可為每個錯誤報告產生新的 Ref。

boolean expression shortcut

有兩種

  1. Expr1 orelse Expr2
    先計算 Expr1。如果 Expr1 結果為 true,則 Expr2 就不會被計算。如果 Expr1 結果為 false,則會繼續計算 Expr2。
  2. Expr1 andalso Expr2
    先計算 Expr1。如果 Expr1 結果為 true,則會繼續計算 Expr2。如果 Expr1 結果為 false,則 Expr2 就不會被計算。

注意:在 A or B 與 A and B 中,A 與 B 都一定會被計算。

參考

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

2014/03/03

DexClassLoader 應用 - 整合服務平台


原理:使用 DexClassLoader 動態載入 apk 中 dex file,並產生 classes 然後執行其 method
※ 此方法必須先知道 apk 中 dex file 有哪些 classes 與 method

Code 解說

目的:將編譯好的 classB 之 apk 放到 SD 卡目錄下,在使用 classA 動態啟動(載入) classB
class A

String path = Environment.getExternalStorageDirectory() + File.separator;  //File.separator 表示"/"符號,系統自動產生對應的 "\" 或 "/"
String filename = "TestClassLoadBDex.apk";

String optimizedDirectory = path + File.separator + "dex_temp"  ;
//為解決 .dex文件在 4.1 之後不能存在 SD 卡下,否則會出現 IllegalArgumentException 問題,這裡透過 getDir 來取得 file
File file = getDir("dex", 0) ;
DexClassLoader cl = new DexClassLoader(path + filename, file.getAbsolutePath(),null, getClassLoader());

Class libProviderClazz = null;

try {
 libProviderClazz = cl.loadClass("com.example.testclassloadb.MainActivity");  //class 完整名稱

 Method onCreateMethod = libProviderClazz.getDeclaredMethod("onCreate", Bundle.class);  //呼叫 class 中 method
 onCreateMethod.setAccessible(true);  //設定 method 是否可訪問
 onCreateMethod.invoke(this, savedInstanceState); //第一個参数為,一般 method 傳入 context,靜態 method 傳入 null
    
     Method changeTitle = libProviderClazz.getDeclaredMethod("changeTitle", String.class);  //呼叫 class 中 method
     changeTitle.setAccessible(true);  //設定 method 是否可訪問
 changeTitle.invoke(this, "class A call change title"); //第一個参数為,一般 method 傳入 context,靜態 method 傳入 null
    

} catch (Exception exception) {
 exception.printStackTrace();
}
class B (此部分代碼為錯誤示範)

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 
 tv_title = (TextView) findViewById(R.id.tv_title);
 btn_showToast = (Button) findViewById(R.id.btn_showToast);
 
 Log.i("ruby", "path="+this.getFilesDir());
 
 btn_showToast.setOnClickListener(new OnClickListener() {
  
  @Override
  public void onClick(View v) {
   Toast.makeText(MainActivity.this, "in ClassLoadB", Toast.LENGTH_SHORT).show();
  }
 });
 
}

private void changeTitle(String str){
 tv_title.setText(str);
}

執行步驟:

  1. 將 classB 編譯成 apk 檔案
  2. 將 apk 放置手機 SD 卡跟目錄下 (這裡示範位置在根目錄,要自訂的請自行修改路徑)
  3. 執行 classA

執行結果:

輸入完以上代碼後執行,發現雖然成功執行了,但是 classA 並沒有載入 classB 的 apk,也沒有執行預定的 method (onCreate 與 changeTitle)
其實不安装apk,直接動態加載 dex 中的類別,是屬於透過反射的方式手動的創建 Activity
這種方式跟系统創建的 Activity 是不一樣的,没有系统創建之 Activity 的特性,而且 R 也找不到(無法透過 layout 來畫 UI),其實他並不是啟動 Apk 中的 Activity,而是在 classA Activity 上劃出 Apk 中 Activity 的 View

解決辦法:

由上述內容可知,其實只要手動產生一個 activity 並傳入 classB 即可正常執行 classB 及其 method
首先,增加一個 setActivity 的 method
class B

public void setActivity(Activity paramActivity) {
    Log.i("ruby", "setActivity..." + paramActivity);
    this.activity = paramActivity;
}
接著,在 classB OnCreate method 中修改 view 的呈現
※ classB 載入的 view 不可使用 resources 只能動態產生,因為載入 classB 時無法做 resources 連結
classB

boolean b = false;
TextView t = new TextView(this.activity);  
t.setText("class B"); 

if (savedInstanceState != null) {
 b = savedInstanceState.getBoolean("KEY_START_FROM_OTHER_ACTIVITY", false);
 if (b) {
  this.activity.setContentView(t);
 }
}
if (!b) {
 super.onCreate(savedInstanceState);
 setContentView(t);
}
最後,在 classA 呼叫 classB 的 onCreate method 時,先呼叫 setActivity method
classA

Constructor localConstructor = libProviderClazz.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
Method localMethodSetActivity = libProviderClazz.getDeclaredMethod(
  "setActivity", new Class[] { Activity.class });
localMethodSetActivity.setAccessible(true);
localMethodSetActivity.invoke(instance, new Object[] { this });

應用範圍:

app 整合平台,例如:game center classB 就像是遊戲大廳,classA 就像是裡面的各個小遊戲,讓不同公司開發出的小遊戲可以直接存取底層的遊戲大廳(classB)

getDeclaredMethod 與 getMethod 差異

  1. getDeclaredMethod:取得本身 class 中的所有方法,包含 public、protected 和 private 方法
  2. getMethod:取得 class 中的所有共有方法,包括自己的 public 方法、繼承來的方法、實作介面的 method 

References