iOS 儲存資料與 SQLite 使用範例
資料儲存至硬碟的必要性
在 iOS App 開發時,有許多情況會需要存些東西在硬碟裡:
登入資訊
對於需要登入的App,為避免每次使用都要重新登入一次,就會需要把 Access Token 甚至是帳密存進硬碟裡。當然為了安全性考量,系統應該避免直接將密碼儲存,而應該設計 Access Token 的作法比較理想。
檔案暫存
已經下載過的圖檔或是資料檔,避免重覆下載浪費頻寬,就需要存在硬碟裡。對於使用非吃到飽的資費方案的使用者,這是很實用的功能。
資料庫暫存
對於許多需要作多面向的搜尋的 App 而言,每一個搜尋都要跟 Server 尋問會導致執行速度低落,以及頻寬的浪費。此時即可將問到的資料都先行存在硬碟裡,搜尋時先搜尋硬碟裡的資料,同時向 Server 尋問是否資料有更新。
多步驟程序
有許多時候,有些任務會需要依序執行數個步驟,比如依順序向伺服器發出多次命令,如果能將已執行完成的命令結果存在硬碟裡,可以避免重複命令,因此能減少頻寬,增進執行效率。
頁面位置暫存
記錄使用者上一次結束 App 時的頁面,而在 App 一開始時即跳至該頁面,會是一個不錯的貼心小功能。
背景App記憶體被釋放
當使用者切換至另一個很吃記憶體的App而導致App 進入背景時,不保證記憶體不會被適放。若沒有儲存至硬碟,將會導致重要的資料消失。
App 閃退
理論上,一個測試完整的 App 不應該存在閃退的情況,然而實際上一方面在測試完整之前 App 還是要能給內部外部使用者使用(這也是測試的一環),而另一方面,總是難免會有許多意外的情況出現;而身為一個 App 的設計者,會希望在即始偶爾閃退的情況下,App 還能夠維持它能夠使用的程度。比如說,如果輸入帳密之後,若不幸的會有10%的情況下會閃退,若 App 有將資訊存至硬碟,這10%的人再重開 App 時,並不需要再次輸入帳密,就可以將不適的感覺降至最低,也多少爭取了修正的時間。
iOS App 儲存資料方案
儲存的方案主要有下列幾種:
1. NSUserDefault
適合用來存輕量的 key-value 資料,比如說 token, user account info, app status 等等。
Write 範例
[[NSUserDefaults standardUserDefaults] setObject:valueString forKey:keyString]; [[NSUserDefaults standardUserDefaults] synchronize]; // save to disk
Read 範例
NSString *value=[[NSUserDefaults standardUserDefaults] objectForKey:keyString];
2. 寫入 local file
適合放圖檔或是其它比較大的檔案。要使用這個方法,要先對於 App 的檔案存取限制路徑要有基本的了解,對於本機來說,常會使用到的檔案主要為 NSBundle 裡的檔案,以及 Document folder 裡檔案:
NSBundle
NSBundle 裡的檔案,也就是在 Project 裡有匯入的檔案,App 可以讀取,但是無法寫入。這點在後面要使用 SQLite 資料庫時也需要注意。
Document folder
另一個可以利用的檔案目錄,則是NSDocumentDirectory。App可以對這個目錄底下的檔案作完全的控制。
每個 App 都會有其配置的NSDocumentDirectory,不同 App 之間無法互通。電腦透過 iTunes 程式,可以將檔案放至 App 的 NSDocumentDirectory底下。這個目錄在 App 被移除時也會一併消滅。
一般的作法,是將設定檔案先放在 NSBundle 裡,而在 App 初始化時,檢查 Document folder 裡是否有同名的檔案,若沒有就將 NSBundle 裡的檔案複製至 Document folder 下,接下來就可以存取這個檔案了。
Read 範例
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; NSString *filePath= [documentsDirectory stringByAppendingPathComponent:fileName]; UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];
Write 範例
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; NSString *filePath= [documentsDirectory stringByAppendingPathComponent:fileName]; NSData *imageData=UIImagePNGRepresentation(crayImage); if(imageData){ [imageData writeToFile:filePath atomically:YES]; }
3. Core Data
iOS 自定的一個資料服務,近似物件資料庫不過不完全相同。使用上直接以 Objective-C 物件方式來進行,對新手來說一開始會比較難上手,若能夠熟悉的話,也會有相當大的幫助。
Read 範例
-(NSArray *) getMemberNamed:(NSString *) name { AppDelegate *appDelegate=(AppDelegate *)[[UIApplication sharedApplication] delegate]; NSManagedObjectContext *managedObjectContext=[appDelegate managedObjectContext]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"Member" inManagedObjectContext: managedObjectContext]; [fetchRequest setEntity:entity]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name=%@", name]; [fetchRequest setPredicate:predicate]; NSError *error = nil; NSArray *fetchedObjects = [ managedObjectContext executeFetchRequest:fetchRequest error:&error]; if (fetchedObjects == nil) { NSLog(@"Fetch error, something's wrong. %@",error); } return fetchedObjects; }
Write 範例
NSArray *orangeMemberList=[self getMemberNamed:@"Orange"]; if(orangeMemberList){ for (Member *member in orangeMemberList) { member.name=@"Apple"; } } NSError *error = nil; if (![managedObjectContext save:&error]) { NSLog(@"[ERROR] Saving managed context error: %@",error); }else{ NSLog(@"Managed context saved."); }
4. SQLite 資料庫
一個輕量方便好用的關連式資料庫。對於熟悉關聯式資料庫與SQL語法的開發者們來說,這是個不錯的選擇。
本文中介紹 SQLite 在 iOS App 中的用法與範例。
SQLite簡介
SQLite是一個簡易的關聯式資料庫(Relational Database)。在這波 App 開發的浪潮中,SQLite受到廣大的歡迎。它使用單一的檔案來運作,以函式庫的方式直在內建在 App 裡,所需增加的空間不大,由於不需開連線所以運作快速。在iOS, Android, Symbian, WebOS 等都可以使用 SQLite。SQLite 在 iOS 中已有內建函式庫,使用上十分方便。
使用步驟
1. 加入 Framework
Framework 裡加入 libsqlite3.tbd
疑問一:為何這裡有 libsqlite3.tbd 與 libsqlite3.0.tbd?二者有何不同?
答:libsqlite3.tbd是連結至「最新的sqlite library」,而目前最新的是libsqlite3.0.tbd,所以雖然加二者的效果相同,但是為了維護性,應該加 libsqlite3.tbd
疑問二:為何早先的 XCode 版本是使用 libsqlite3.dylib 而現在則是 libsqlite3.tbd?
答:新版的 XCode 改使用 tbd 檔來減少檔案大小
2. 匯入header 檔
法一:在要使用的.h檔裡加上 #import <sqlite3.h>
法二:在<你的專案名稱>-prefix.pch檔裡加上#import <sqlite3.h>
若只有一二個檔案會用到 SQLite,則使用法一;否則就使用法二
3. 建立sqlite檔案法一:預先建立再複製法
3.1 先建立好 sqlite 檔案
打開終端機,然後輸入
sqlite3 member.db
在 sqlite3> 下,輸入
CREATE TABLE IF NOT EXISTS MEMBER (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, phone TEXT);
然後打下面的指令離開
.quit
然後就可以看到 member.db 這個檔案
3.2 將它加入 project Bundle 裡
3.3 在一APP開啟時,將這個檔案複製到 App 目錄下
NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *bundlePath = [[NSBundle mainBundle] pathForResource:databaseName ofType:NULL]; [fileManager copyItemAtPath:bundlePath toPath:databasePath error:&error];
3. 建立sqlite檔案法二:程式從頭建立法
在程式一開始檢查有沒有 sqlite 檔案,若沒有的話進行初始化(Create Database Tables…)
-(BOOL) createDatabase:(NSString *)databaseName{ NSString *docsDir; NSArray *dirPath; sqlite3 *db; // Get the documents directory dirPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); docsDir = [dirPath objectAtIndex:0]; // Build the path to the database file NSString *databasePath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent: databaseName]]; NSFileManager *filemgr = [NSFileManager defaultManager]; if ([filemgr fileExistsAtPath: databasePath ] == NO) { const char *dbpath = [databasePath UTF8String]; if (sqlite3_open(dbpath, &db) == SQLITE_OK) { char *errMsg; // create SQL statements const char *sql = "CREATE TABLE IF NOT EXISTS MEMBER (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT, phone TEXT)"; if (sqlite3_exec(db, sql, NULL, NULL, &errMsg) != SQLITE_OK) { NSLog( @"Failed to create table"); return NO; } sqlite3_close(db); return YES; } else { NSLog( @"Failed to open/create database"); return NO; } }else{ NSLog(@"Database already created."); return YES; } }
4. 使用範例
4.1 新增
char *errorMsg; const char *insertSql="insert into member(name,company) values('Orange','iOTEC Systems')"; if (sqlite3_exec(db, insertSql, NULL, NULL, &errorMsg)==SQLITE_OK) { NSLog(@"INSERT OK"); }else{ NSLog(@"Insert error: %s",errorMsg); }
4.2 查詢
const char *sql = "select * from member"; sqlite3_stmt *statement =nil; if (sqlite3_prepare_v2(db, sql, -1, &statement, NULL) == SQLITE_OK) { while (sqlite3_step(statement) == SQLITE_ROW) { NSString *_id,*name, *company; _id = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; name = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 1)]; company = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 2)]; NSLog(@"Record: %@> %@ , %@",_id, name, company); } sqlite3_finalize(statement); }
4.3 修改
char *errorMsg; const char *sql = "UPDATE member SET name='Apple' WHERE name='Orange'"; if (sqlite3_exec(db, sql, NULL, NULL, &errorMsg)==SQLITE_OK) { NSLog(@"UPDATE OK"); }else{ NSLog(@"UPDATE error: %s",errorMsg); }
4.4 刪除
char *errorMsg; const char *sql = "DELETE FROM member WHERE name='Apple'"; if (sqlite3_exec(db, sql, NULL, NULL, &errorMsg)==SQLITE_OK) { NSLog(@"DELETE OK"); }else{ NSLog(@"DELETE error: %s",errorMsg); }
範例程式請參考SQLite example on GitHub
I am very happy to find & read your this blog. thanks for sharing this syntax, very useful for me. Regards