iOS本地數(shù)據(jù)存儲方案

移動開發(fā)何乎,數(shù)據(jù)存儲是一定而且經常遇到的昔搂,存儲的數(shù)據(jù)有大有小玲销,大數(shù)據(jù)和小數(shù)據(jù)的存儲方式也不一樣,存儲數(shù)據(jù)也要考慮性能摘符,針對iOS的數(shù)據(jù)存儲贤斜,有以下幾種存儲方式。

常見的存儲方式

  • plist 格式文件存儲
  • NSUserDefaults 沙盒存儲(個人偏好設置)
  • 文件讀寫存儲
  • 解歸檔存儲
  • 數(shù)據(jù)庫存儲

了解緩存逛裤,先要了解iOS中沙盒機制這個概念

沙盒其實質就是在iOS系統(tǒng)下瘩绒,每個應用在內存中對應的存儲空間。
每個iOS應用都有自己的應用沙盒(文件系統(tǒng)目錄)别凹,與其他文件系統(tǒng)隔離草讶,各個沙盒之間相互獨立,而且不能相互訪問(手機沒有越獄情況下)炉菲,各個應用程序的沙盒相互獨立的堕战,在系統(tǒng)內存消耗過高時坤溃,系統(tǒng)會收到內存警告并自動退出軟件。這就保證了系統(tǒng)的數(shù)據(jù)的安全性及系統(tǒng)的穩(wěn)定性嘱丢。

一個沙盒目錄如下:


屏幕快照 2018-08-01 下午2.35.24.png
屏幕快照 2018-08-01 下午2.35.55.png
屏幕快照 2018-08-01 下午2.36.06.png
屏幕快照 2018-08-01 下午2.36.15.png

Documents 應用程序在運行時生成的一些需要長久保存的數(shù)據(jù)薪介。
Library/Caches 儲存應用程序網絡請求的數(shù)據(jù)信息(音視頻與圖片等的緩存)。此目錄下的數(shù)據(jù)不會自動刪除越驻,需要程序員手動清除該目錄下的數(shù)據(jù)汁政。主要用于保存應用在運行時生成的需要長期使用的數(shù)據(jù).一般用于存儲體積較大數(shù)據(jù)。
Library/Preferences 設置應用的一些功能會在該目錄中查找相應設置的信息,該目錄由系統(tǒng)自動管理,通常用來儲存一些基本的應用配置信息,比如賬號密碼,自動登錄等缀旁。
tmp 保存應用運行時產生的一些臨時數(shù)據(jù);應用程序退出记劈、系統(tǒng)空間不夠、手機重啟等情況下都會自動清除該目錄的數(shù)據(jù)并巍。無需程序員手動清除該目錄中的數(shù)據(jù)目木。

plist 格式文件存儲

  • plist文件 即為屬性列表文件
  • 可以存儲的類型有NSString,NSDictionary,NSArray,NSNumber,Boolean,NSDate,NSData等基本類型
  • 常用于存儲用戶的設置,或存儲項目中經常用到又不經常改變的數(shù)據(jù)
  • 創(chuàng)建.plist可以用xcode工具懊渡,也可以用代碼
  • 不適合存儲大量數(shù)據(jù)刽射,而且只能存儲基本類型
  • 可以實現(xiàn):增,刪剃执,改誓禁,查等操作,但數(shù)據(jù)的存取是一次性的全部操作肾档,所以性能方向表現(xiàn)并不好
  • NSDate,BOOL,Int,Float,NSNumber,NSData的數(shù)據(jù)存儲都是轉換成NSDictionary的Key-Value形式之后摹恰,通過NSDictionary存儲方式存儲的。
字符串存儲:
NSArray *paths = NSSearchPathForDirectoriesInDomains(searchPath, NSUserDomainMask, YES);
NSString *filePath = [[paths firstObject] stringByAppendingPathComponent:@"test.plist"];
BOOL isSave = [string writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
NSLog(@"存儲是否成功---%d",isSave);
    
NSString *testString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"存儲的字符串----%@",testString);
NSArray存儲
NSArray *paths = NSSearchPathForDirectoriesInDomains(searchPath, NSUserDomainMask, YES);
NSString *filePath = [[paths firstObject] stringByAppendingPathComponent:@"test.plist"];
[array writeToFile:filePath atomically:YES];
NSArray *mArray = [NSArray arrayWithContentsOfFile:filePath];
NSLog(@"存儲的Array-----%@",mArray);
NSDictionary存儲
NSArray *paths = NSSearchPathForDirectoriesInDomains(searchPath, NSUserDomainMask, YES);
NSString *filePath = [[paths firstObject] stringByAppendingPathComponent:@"test.plist"];
[dic writeToFile:filePath atomically:YES];   
NSDictionary *mDic = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(@"存儲的Dic------%@",mDic);

NSUserDefaults 沙盒存儲(個人偏好設置)

  • NSUserDefaults 沙盒存儲(個人偏好存儲) 是個單例類阁最,用于存儲少量數(shù)據(jù)戒祠,例如登錄后的用戶名骇两,密碼等速种。
  • 應用程序啟動后,會在沙盒路徑Library -> Preferences 下默認生成以工程bundle為名的.plist文件低千,用NSUserDefaults存儲的數(shù)據(jù)都是存儲在該.plist文件中配阵。
  • 這種方式本質是操作plist文件,所以性能方面的考慮同plist文件數(shù)據(jù)儲存
[[NSUserDefaults standardUserDefaults] setBool:bol forKey:key];  //BOOL存儲
[[NSUserDefaults standardUserDefaults] setInteger:i forKey:key];  //NSInteger存儲
[[NSUserDefaults standardUserDefaults] setObject:obj forKey:key];  //Object存儲
[[NSUserDefaults standardUserDefaults] setURL:url forKey:key];  //NSURL存儲
[[NSUserDefaults standardUserDefaults] setFloat:f forKey:key];  //Float存儲
[[NSUserDefaults standardUserDefaults] setDouble:d forKey:key];  //Double存儲
[[NSUserDefaults standardUserDefaults] URLForKey:key];  //URL的數(shù)據(jù)獲取
[[NSUserDefaults standardUserDefaults] objectForKey:key];  //Object或者基本類型的獲取

文件讀寫存儲(NSFileManager)

  • 文件操作可通過單例 NSFileManager 處理示血。文件存儲的路徑可以代碼設置棋傍。
  • 可以存儲大量數(shù)據(jù),對數(shù)據(jù)格式沒有限制难审。
  • 但由于數(shù)據(jù)的存取必須是一次性全部操作瘫拣,所以在頻繁操作數(shù)據(jù)方面性能欠缺。
創(chuàng)建文件夾
/**
 創(chuàng)建文件夾

 @param dirName 文件夾名稱
 @param dirPath 文件夾所在路徑
 @return 創(chuàng)建結果YES/NO
 */
+ (BOOL)creatDir:(NSString *)dirName dirPath:(NSString *)dirPath {
    
    dirPath = [dirPath stringByAppendingPathComponent:dirName];
    if ([FileManager fileExistsAtPath:dirPath]) {
        NSLog(@"創(chuàng)建失敗告喊,目錄已存在");
    } else {
        BOOL isCreat = [FileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil];
        if (isCreat) {
            NSLog(@"創(chuàng)建成功");
            return YES;
        } else {
            NSLog(@"創(chuàng)建失敗,請檢查路徑");
            return NO;
        }
    }
    return NO;
}
創(chuàng)建文件
/**
 創(chuàng)建文件

 @param fileName 文件名稱
 @param dirPath 文件所在的文件夾路徑
 @return 創(chuàng)建結果YES/NO
 */
+ (BOOL)creatFile:(NSString *)fileName dirPath:(NSString *)dirPath {

    NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
    BOOL isDir = NO;
    BOOL isFileExist = [FileManager fileExistsAtPath:filePath isDirectory:&isDir];
    //目錄是否存在
    if (!(isFileExist && isDir)) {
        BOOL isCreat = [FileManager createFileAtPath:filePath contents:nil attributes:nil];
        if (isCreat) {
            NSLog(@"創(chuàng)建成功");
            return YES;
        } else {
            NSLog(@"創(chuàng)建失敗");
            return NO;
        }
    } else {
        NSLog(@"創(chuàng)建失敗麸拄,文件已存在");
        return NO;
    }
    return NO;
}
String寫入
/**
 String寫入

 @param content 寫入的字符串內容
 @param filePath 寫入文件的路徑
 @return 寫入結果YES/NO
 */
+ (BOOL)writeString:(NSString *)content filePath:(NSString *)filePath {
    
    BOOL isFileExist = [FileManager fileExistsAtPath:filePath];
    if (isFileExist) {
        BOOL isWrite = [content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
        if (isWrite) {
            NSLog(@"寫入成功");
            return YES;
        } else {
            NSLog(@"寫入失敗");
            return NO;
        }
    } else {
        NSLog(@"寫入失敗,文件不存在");
        return NO;
    }
    return NO;
}
Array寫入
/**
 Array寫入

 @param array 要寫入的array
 @param filePath 文件路徑
 @return 寫入結果YES/NO
 */
+ (BOOL)writeArray:(NSArray *)array filePath:(NSString *)filePath {

    BOOL isFileExist = [FileManager fileExistsAtPath:filePath];
    if (isFileExist) {
        BOOL isCreat = [array writeToFile:filePath atomically:YES];
        if (isCreat) {
            NSLog(@"寫入成功");
            return YES;
        } else {
            NSLog(@"寫入失敗");
            return NO;
        }
    } else {
        NSLog(@"寫入失敗,文件不存在");
        return NO;
    }
    return NO;
}
Dictionary寫入
/**
 Dictionary寫入

 @param dic 要寫入的dictionary
 @param filePath 文件路徑
 @return 寫入結果YES/NO
 */
+ (BOOL)writeDictionary:(NSDictionary *)dic filePath:(NSString *)filePath {

    BOOL isFileExist = [FileManager fileExistsAtPath:filePath];
    if (isFileExist) {
        BOOL isCreat = [dic writeToFile:filePath atomically:YES];
        if (isCreat) {
            NSLog(@"寫入成功");
            return YES;
        } else {
            NSLog(@"寫入失敗");
            return NO;
        }
    } else {
        NSLog(@"寫入失敗,文件不存在");
        return NO;
    }
    return NO;
}
讀取存儲的String
/**
 讀取存儲的String

 @param filePath 文件路徑
 @return 存儲的字符串
 */
+ (NSString *)readFileWithFilePath:(NSString *)filePath {

    NSString *str = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    return str;
}
讀取存儲的array
/**
 讀取存儲的array

 @param filePath 文件路徑
 @return 存儲的array
 */
+ (NSArray *)readArrayWithFilePath:(NSString *)filePath {
    
    NSArray *array = [NSArray arrayWithContentsOfFile:filePath];
    return array;
}
讀取存儲的dictionary
/**
 讀取存儲的dictionary

 @param filePath 文件路徑
 @return 存儲的dictionary
 */
+ (NSDictionary *)readDictionaryWithFilePath:(NSString *)filePath {

    NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:filePath];
    return dic;
}
讀取文件夾中所有文件
/**
 讀取文件夾中所有文件

 @param dirPath 文件夾路徑
 @return 文件夾中所有的文件
 */
+ (NSArray *)readAllFileWithDirPath:(NSString *)dirPath {

    NSArray *array = [FileManager contentsOfDirectoryAtPath:dirPath error:nil];
    return array;
}
判斷文件是否存在
/**
 判斷文件是否存在

 @param filePath 文件路徑
 @return 文件是否存在
 */
+ (BOOL)fileIsExistFilePath:(NSString *)filePath {

    BOOL isFileExist = [FileManager fileExistsAtPath:filePath];
    return isFileExist;
}
文件大小
/**
 文件大小

 @param filePath 文件路徑
 @return 文件大小
 */
+ (unsigned long long)computerFileSizeWithFilePath:(NSString *)filePath {

    BOOL isDir;
    BOOL isFile = [FileManager fileExistsAtPath:filePath isDirectory:&isDir];
    if (!isDir) {
        if (!isFile) {
            NSLog(@"文件不存在");
            return 0;
            
        }else {
            unsigned long long fileSize = [FileManager attributesOfItemAtPath:filePath error:nil].fileSize;
            return fileSize;
        }
    }  else {
        NSLog( @"該文件是一個目錄");
        return 0;
    }
    return 0;
}
文件夾中的所有文件大小
/**
 文件夾中的所有文件大小

 @param dirPath 文件夾路徑
 @return 所有文件大小
 */
+ (unsigned long long)computerDirSizeWithDirPath:(NSString *)dirPath {

    BOOL isExist = [FileManager fileExistsAtPath:dirPath];
    if (isExist) {
        
        NSEnumerator *childFilesEnumerator = [[FileManager subpathsAtPath:dirPath] objectEnumerator];
        NSString* fileName;
        long long folderSize = 0;
        while ((fileName = [childFilesEnumerator nextObject]) != nil){
            NSString* fileAbsolutePath = [dirPath stringByAppendingPathComponent:fileName];
            folderSize += [FileManager attributesOfItemAtPath:fileAbsolutePath error:nil].fileSize;
        }
        return folderSize;
        
    } else {
        NSLog(@"目錄不存在");
        return 0;
    }
    return 0;
}
移除文件
/**
 移除文件

 @param filePath 文件路徑
 @return 移除結果YES/NO
 */
+ (BOOL)removeFileWithFilePath:(NSString *)filePath {

    if ([FileManager fileExistsAtPath:filePath]) {
        BOOL isRemove = [FileManager removeItemAtPath:filePath error:nil];
        if (!isRemove) {
            NSLog(@"移除失敗");
            return NO;
        } else {
            NSLog(@"移除成功");
            return YES;
        }
    } else {
        NSLog(@"文件不存在");
        return NO;
    }
    return NO;
}
移動文件
/**
 移動文件

 @param filePath 要移動的文件路徑
 @param toDirPath 文件要移動到的文件夾路徑
 @param newFileName 移動文件的新名稱
 @return 是否移動成功
 */
+ (BOOL)moveFileWithFilePath:(NSString *)filePath toDirPath:(NSString *)toDirPath newFileName:(NSString *)newFileName {

    NSString *des = [toDirPath stringByAppendingPathComponent:newFileName];

    if (![FileManager fileExistsAtPath:filePath]) {
        NSLog(@"文件不存在");
        return NO;
    }else {
        if (![FileManager fileExistsAtPath:toDirPath]) {
            NSLog(@"目標路徑不存在");
            return NO;
        } else {
            BOOL move = [FileManager moveItemAtPath:filePath toPath:des error:nil];
            if (move) {
                NSLog( @"移動成功");
                return YES;
            } else {
                NSLog(@"移動失敗");
                return NO;
            }
        }
    }
    return NO;
}

解歸檔存儲

  • plist 與 NSUserDefaults(個人偏好設置)兩種類型的儲存只適用于系統(tǒng)自帶的一些常用類型派昧,而且前者必須拿到文件路徑,后者也只能儲存應用的主要信息拢切。
  • 對于開發(fā)中自定義的數(shù)據(jù)模型的儲存蒂萎,我們可以考慮使用歸檔儲存方案。
  • 歸檔保存數(shù)據(jù)淮椰,文件格式自己可以任意五慈,沒有要求 ; 即便設置為常用的數(shù)據(jù)格式(如:.c .txt .plist 等)要么不能打開,要么打開之后亂碼顯示主穗。
  • 值得注意的是使用歸檔保存的自定義模型需要實現(xiàn)NSCoding協(xié)議下的兩個方法泻拦。
  • 不適合存儲大量數(shù)據(jù),可以存儲自定義的數(shù)據(jù)模型忽媒。
  • 雖然歸檔可以存儲自定義的數(shù)據(jù)結構聪轿,但在大批量處理數(shù)據(jù)時,性能上仍有所欠缺猾浦。
YJArchiveModel.h
@interface YJArchiveModel : NSObject <NSCoding>

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) double price;

@end
YJArchiveModel.m
@implementation YJArchiveModel

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:_name forKey:@"name"];
    [aCoder encodeInteger:_age forKey:@"age"];
    [aCoder encodeBool:_sex forKey:@"sex"];
    [aCoder encodeDouble:_price forKey:@"price"];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        _name = [aDecoder decodeObjectForKey:@"name"];
        _age = [aDecoder decodeIntegerForKey:@"age"];
        _sex = [aDecoder decodeBoolForKey:@"sex"];
        _price = [aDecoder decodeDoubleForKey:@"price"];
    }
    return self;
}

@end

數(shù)據(jù)庫存儲

  • SQLite : 它是一款輕型的嵌入式數(shù)據(jù)庫陆错,安卓和ios開發(fā)使用的都是SQLite數(shù)據(jù)庫;占用資源非常的低金赦,在嵌入式設備中音瓷,可能只需要幾百K的內存就夠了;而且它的處理速度比Mysql夹抗、PostgreSQL這兩款著名的數(shù)據(jù)庫都還快绳慎。
  • FMDB 正是基于 SQLite 開發(fā)的一套開源庫。使用時漠烧,需要自己寫一些簡單的SQLite語句
  • CoreData 是蘋果給出的一套基于 SQLite 的數(shù)據(jù)存儲方案杏愤;而且不需要自己寫任何SQLite語句。該功能依賴于 CoreData.framework 框架已脓,該框架已經很好地將數(shù)據(jù)庫表和字段封裝成了對象和屬性珊楼,表之間的一對多、多對多關系則封裝成了對象之間的包含關系
  • Core Data的強大之處就在于這種關系可以在一個對象更新時度液,其關聯(lián)的對象也會隨著更新厕宗,相當于你更新一張表的時候,其關聯(lián)的其他表也會隨著更新堕担。Core Data的另外一個特點就是提供了更簡單的性能管理機制已慢,僅提供幾個類就可以管理整個數(shù)據(jù)庫。由于直接使用蘋果提供的CoreData容易出錯霹购,這里提供一個很好的三方庫 MagicalRecord

緩存系統(tǒng)

對大多數(shù) APP 而言佑惠,都是 Hybrid 開發(fā),Web 頁與原生同時存在,其中 Web 頁可能是 UIWeb 也可能是 WKWeb 膜楷。所以與之相應的緩存系統(tǒng)乍丈,應該包括 Web 緩存與 原生接口數(shù)據(jù)緩存兩部分。

原生接口部分的數(shù)據(jù)緩存

存儲方式:主要采用文件讀寫把将、歸檔轻专、個人偏好設置(NSUserDefaults) 。

具體說明:大部分接口數(shù)據(jù)解析之后寫入文件保存(讀寫操作最好 GCD 子線程操作)察蹲;整個應用需要用到的重要數(shù)據(jù)模型可以考慮采用歸檔方式(標記狀態(tài)的數(shù)據(jù)模型);與用戶相關的信息请垛、單個標記標識等采用個人偏好設置。

補充: 原生接口數(shù)據(jù)存儲方式以上三種方式就已夠用洽议;當然對于一些涉及查詢宗收、刪除、更新等操作的數(shù)據(jù)模型亚兄,就需要使用數(shù)據(jù)庫操作混稽。這里推薦使用 CoreData 的封裝庫 MagicalRecord 。

關于存儲的使用审胚,我寫了一個簡單的demol匈勋,除了數(shù)據(jù)庫的,其他的存儲基本上都有膳叨,有興趣的可以看一看:YJDataStore

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末洽洁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子菲嘴,更是在濱河造成了極大的恐慌饿自,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件龄坪,死亡現(xiàn)場離奇詭異昭雌,居然都是意外死亡,警方通過查閱死者的電腦和手機健田,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門烛卧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抄课,你說我怎么就攤上這事唱星■茫” “怎么了跟磨?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長攒盈。 經常有香客問我抵拘,道長,這世上最難降的妖魔是什么型豁? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任僵蛛,我火速辦了婚禮尚蝌,結果婚禮上,老公的妹妹穿的比我還像新娘充尉。我一直安慰自己飘言,他們只是感情好,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布驼侠。 她就那樣靜靜地躺著姿鸿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪倒源。 梳的紋絲不亂的頭發(fā)上苛预,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音笋熬,去河邊找鬼热某。 笑死,一個胖子當著我的面吹牛胳螟,可吹牛的內容都是我干的昔馋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼糖耸,長吁一口氣:“原來是場噩夢啊……” “哼绒极!你這毒婦竟也來了?” 一聲冷哼從身側響起蔬捷,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤垄提,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后周拐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铡俐,經...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年妥粟,在試婚紗的時候發(fā)現(xiàn)自己被綠了审丘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡勾给,死狀恐怖滩报,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情播急,我是刑警寧澤脓钾,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站桩警,受9級特大地震影響可训,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一握截、第九天 我趴在偏房一處隱蔽的房頂上張望飞崖。 院中可真熱鬧,春花似錦谨胞、人聲如沸固歪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昼牛。三九已至,卻和暖如春康聂,著一層夾襖步出監(jiān)牢的瞬間贰健,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工恬汁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伶椿,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓氓侧,卻偏偏與公主長得像脊另,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子约巷,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內容