iOS中的性能監(jiān)控方法

APP開發(fā)中性能問題無疑是很重要的一點(diǎn),有幾項(xiàng)指標(biāo)可以看出APP的性能是否存在問題褐捻,內(nèi)存使用量狸驳,F(xiàn)PS再层,以及CPU使用率。在開發(fā)階段這些數(shù)據(jù)的測(cè)試很容易秽五,有一些在Xcode編譯項(xiàng)目時(shí)就會(huì)有顯示孽查,而剩下的則可以使用Instruments來進(jìn)行監(jiān)控√勾可以說在線下的監(jiān)控Instruments為我們考慮到了方方面面盲再,但是光是線下監(jiān)控是完全不夠的,因?yàn)锳PP上線后運(yùn)行的環(huán)境是十分復(fù)雜的瓣铣,我們?cè)跍y(cè)試時(shí)不可能模擬的面面俱到答朋,因此就需要針對(duì)線上的性能問題進(jìn)行監(jiān)控。

內(nèi)存使用量

內(nèi)存使用是很重要的一點(diǎn)棠笑,如果我們的內(nèi)存使用量過大梦碗,APP就會(huì)被系統(tǒng)殺掉,給用戶的表現(xiàn)就是閃退,這是很嚴(yán)重的一個(gè)問題洪规,而我們的APP如果被系統(tǒng)強(qiáng)殺會(huì)產(chǎn)生一個(gè)叫jetsam的日志印屁,這個(gè)日志可以通過手機(jī)中設(shè)置 -> 隱私 -> 分析中看到相關(guān)日志。

現(xiàn)代的進(jìn)程在虛擬內(nèi)存中的運(yùn)行是以分頁(yè)形式存在的斩例,這樣做可以節(jié)省內(nèi)存空間雄人,因?yàn)锳PP在運(yùn)行的時(shí)候只有一部分會(huì)映射到虛擬內(nèi)存中,而不是整個(gè)APP都會(huì)被加載到虛擬內(nèi)存上念赶,只有使用到的部分才會(huì)被映射础钠,而jetsam日志就是以頁(yè)數(shù)為單位來衡量一個(gè)APP使用的內(nèi)存是否超過限制。

"rpages" : 89600,
"reason" : "per-process-limit",

像這樣晶乔,表明我們使用了89600個(gè)內(nèi)存頁(yè)珍坊,超出了單進(jìn)程的內(nèi)存限制,如果可以知道一頁(yè)的大小就可以知道系統(tǒng)對(duì)單個(gè)進(jìn)程的內(nèi)存限制是多少正罢。注意這個(gè)限制不是固定的,而是系統(tǒng)根據(jù)當(dāng)前內(nèi)存情況來決定的驻民。

jetsam日志

可以看到一頁(yè)的大小是16384翻具,這樣就可以計(jì)算出當(dāng)前 App 的內(nèi)存限制值:pageSize * rpages / 1024 /1024 =16384 * 89600 / 1024 / 1024 得到的值是 1400 MB,即 1.4G回还。

iOS系統(tǒng)會(huì)使用一個(gè)優(yōu)先級(jí)最高的線程vm_pressure_monitor來監(jiān)控系統(tǒng)內(nèi)存壓力的情況裆泳,并通過一個(gè)堆棧來維護(hù)所有的APP進(jìn)程,如果發(fā)現(xiàn)某個(gè)進(jìn)程的內(nèi)存快要超出限制了就會(huì)發(fā)出通知柠硕,內(nèi)存有壓力的APP就會(huì)執(zhí)行代理工禾,也就是熟悉的didReceiveMemoryWarning,在這里面可以寫一個(gè)釋放內(nèi)存的方法蝗柔,這是最后的機(jī)會(huì)去避免APP被強(qiáng)殺闻葵。

不過很遺憾APP在上限后我們是無法去獲取jetsam日志的,這屬于每個(gè)用戶的隱私癣丧,我們的權(quán)限是無法獲得的槽畔,但是iOS還為我們提供了其他的方法去獲取內(nèi)存的使用情況,以供我們?cè)贏PP內(nèi)存收到警告時(shí)查看當(dāng)前內(nèi)存的使用情況胁编。

我們可以寫一個(gè)文件導(dǎo)入如下頭文件

#import <mach/mach.h>

從頭文件的名稱就可以看出這個(gè)是系統(tǒng)級(jí)的方法厢钧,iOS系統(tǒng)提供了一個(gè)函數(shù)task_info可以獲得當(dāng)前進(jìn)程的使用情況

struct mach_task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kl = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
float used_mem = info.resident_size;
NSLog(@"使用了 %f MB 內(nèi)存", used_mem / 1024.0f / 1024.0f)

MACH_TASK_BASIC_INFO這個(gè)結(jié)構(gòu)體中定義了一系列和內(nèi)存有關(guān)的變量

struct mach_task_basic_info {
    mach_vm_size_t  virtual_size;       /* virtual memory size (bytes) */
    mach_vm_size_t  resident_size;      /* resident memory size (bytes) */
    mach_vm_size_t  resident_size_max;  /* maximum resident memory size (bytes) */
    time_value_t    user_time;          /* total user run time for
                                         *  terminated threads */
    time_value_t    system_time;        /* total system run time for
                                         *  terminated threads */
    policy_t        policy;             /* default policy for new threads */
    integer_t       suspend_count;      /* suspend count for task */
};

但是這樣測(cè)出來和Xcode中實(shí)際顯示的出入較大,后來蘋果的開發(fā)者大會(huì)說task_vm_info結(jié)構(gòu)體中的phys_footprint才是真正的物理內(nèi)存使用量


struct task_vm_info {
  mach_vm_size_t  virtual_size;       // 虛擬內(nèi)存大小
  integer_t region_count;             // 內(nèi)存區(qū)域的數(shù)量
  integer_t page_size;
  mach_vm_size_t  resident_size;      // 駐留內(nèi)存大小
  mach_vm_size_t  resident_size_peak; // 駐留內(nèi)存峰值

  ...

  /* added for rev1 */
  mach_vm_size_t  phys_footprint;     // 物理內(nèi)存

  ...

由此就可以寫一個(gè)簡(jiǎn)單的內(nèi)存使用量的檢測(cè)方法

- (float)getMemoryUse{
    //TASK_VM_INFO中存儲(chǔ)物理內(nèi)存使用信息
    int64_t memoryUsageInByte = 0;
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t kernReturn = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
    if (kernReturn != KERN_SUCCESS) { return NSNotFound; }
    memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
    return memoryUsageInByte/1024.0/1024.0;
}

為了測(cè)試我寫了一個(gè)方法嬉橙,每隔一秒生成1000個(gè)對(duì)象裝入類中的可變數(shù)組早直,保證其不會(huì)被釋放,測(cè)試內(nèi)存使用量市框。

- (void)startMonitor{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //創(chuàng)建一個(gè)定時(shí)器(dispatch_source_t本質(zhì)上還是一個(gè)OC對(duì)象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    //設(shè)置定時(shí)器的各種屬性
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0*NSEC_PER_SEC));
    uint64_t interval = (uint64_t)(1.0*NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    
    
    //設(shè)置回調(diào)
    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(self.timer, ^{
        //定時(shí)器需要執(zhí)行的操作
        self->usedMemory = [[BZMemoryMonitor shareInstance] getMemoryUse];
        [weakSelf increaseMemory];
        dispatch_async(dispatch_get_main_queue(), ^(void){
            //Run UI Updates
            weakSelf.useLabel.text = [NSString stringWithFormat:@"使用內(nèi)存:%f",self->usedMemory];
        });
       
    });
    //啟動(dòng)定時(shí)器(默認(rèn)是暫停)
    dispatch_resume(self.timer);
}


- (void)increaseMemory{
    
    for (int i = 0; i < 1000; i++) {
        NSObject *obj = [[NSObject alloc] init];
        [self.array addObject:obj];
    }
}

工具使用效果如下:


內(nèi)存使用獲取

和Xcode中顯示的內(nèi)存使用基本一致霞扬。

FPS監(jiān)控

提到FPS監(jiān)控很多人可能都會(huì)知道使用CADisplayLink,什么是CADisplayLink呢?

CADisplayLink是CoreAnimation提供的另一個(gè)類似于NSTimer的類祥得,它總是在屏幕完成一次更新之前啟動(dòng)兔沃,它的接口設(shè)計(jì)的和NSTimer很類似,所以它實(shí)際上就是一個(gè)內(nèi)置實(shí)現(xiàn)的替代级及,但是和timeInterval以秒為單位不同乒疏,CADisplayLink有一個(gè)整型的frameInterval屬性,指定了間隔多少幀之后才執(zhí)行饮焦。默認(rèn)值是1怕吴,意味著每次屏幕更新之前都會(huì)執(zhí)行一次。但是如果動(dòng)畫的代碼執(zhí)行起來超過了六十分之一秒县踢,你可以指定frameInterval為2转绷,就是說動(dòng)畫每隔一幀執(zhí)行一次(一秒鐘30幀)或者3,也就是一秒鐘20次硼啤,等等议经。

CADisplayLink可以以屏幕刷新的頻率調(diào)用指定selector,而且iOS系統(tǒng)中正常的屏幕刷新率為60Hz(60次每秒)谴返,那只要在這個(gè)方法里面統(tǒng)計(jì)每秒這個(gè)方法執(zhí)行的次數(shù)煞肾,通過次數(shù)/時(shí)間就可以得出當(dāng)前屏幕的刷新率了。

由此可以寫出一個(gè)FPS監(jiān)控的工具:

- (void)setupDisplayLink{
    // 初始化CADisplayLink
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsCount:)];
    // 把CADisplayLink對(duì)象加入runloop
    [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}


// 方法執(zhí)行幀率和屏幕刷新率保持一致
- (void)fpsCount:(CADisplayLink *)displayLink {
    if (_lastTimestamp == 0) {
        _lastTimestamp = self.displayLink.timestamp;
    } else {
        _performTimes++;
        // 開始渲染時(shí)間與上次渲染時(shí)間差值
        NSTimeInterval useTime = self.displayLink.timestamp - _lastTimestamp;
        if (useTime < 1) return;
        _lastTimestamp = self.displayLink.timestamp;
        // fps 計(jì)算
        float fps = _performTimes / useTime;
        NSLog(@"%f",fps);
        _performTimes = 0;
    }
}

但是我發(fā)現(xiàn)這種方式與Instrument檢測(cè)出來的差距有些大嗓袱,我的demo中滑動(dòng)已經(jīng)十分卡頓籍救,但是這個(gè)工具依舊顯示fps在57左右,后來參考這篇文章才發(fā)現(xiàn)其原因渠抹。

引用文章內(nèi)容:
iOS中每一幀畫面的生成是一個(gè)復(fù)雜的過程蝙昙,但簡(jiǎn)單來說需要經(jīng)過以下步驟:

1、系統(tǒng)根據(jù)你的代碼梧却,設(shè)置布局各個(gè)元素的位置(frame奇颠、AutoLayout)、屬性(顏色篮幢、透明度大刊、陰影等)。
2三椿、CPU對(duì)需要提前繪制的元素缺菌、圖形使用Core Graphics進(jìn)行繪制。
3搜锰、CPU將一切需要繪制到屏幕上的內(nèi)容(包括解壓后的圖片)打包發(fā)送到GPU
4伴郁、GPU對(duì)內(nèi)容進(jìn)行計(jì)算繪制,顯示到屏幕上蛋叼。

我使用的demo是一個(gè)很大的CollectionView焊傅,然后為cell添加了圓角以及陰影剂陡,并且使用了大圖片,所以我和這篇文章中出現(xiàn)的現(xiàn)象一致狐胎。

1鸭栖、滑動(dòng)列表時(shí)(即使是慢速滑動(dòng)),GPU都需要計(jì)算圖像握巢、文本的動(dòng)態(tài)陰影的位置和形狀來進(jìn)行陰影的繪制晕鹊,此時(shí)GPU將成性能瓶頸,能明顯觀察到FPS的下降暴浦。
2溅话、快速滑動(dòng)列表時(shí)Cell每次在顯示前都需要通過imageWithContentsOfFile從硬盤加載圖片并解壓,此時(shí)文件的IO歌焦,圖片的解壓讓CPU也遇到性能瓶頸飞几,使主線程無法流暢執(zhí)行,讓FPS雪上加霜独撇。

原因:

CADisplayLink運(yùn)行在主線程RunLoop之中屑墨,RunLoop中所管理的任務(wù)的調(diào)度時(shí)機(jī)受任務(wù)所處的RunLoopMode和CPU的繁忙程度所影響。
在第二個(gè)原因中受文件IO券勺、解壓圖片的影響绪钥,RunLoop 自然無法保證CADisplayLink被調(diào)用的次數(shù)達(dá)到每秒60次,這里的調(diào)用頻率正是我們的FPS指示器中所顯示FPS关炼。
而在第一個(gè)原因中主要瓶頸在于GPU,即使RunLoop能保持每秒60次調(diào)用CADisplayLink匣吊,也無法說明此時(shí)的屏幕刷新率能達(dá)到60FPS(Core Animation通過與OpenGl打交道控制GPU進(jìn)行屏幕繪制)儒拂,也正因?yàn)檫@樣FPS指示器顯示55+的FPS,但I(xiàn)nstrument中的Core Animation FPS 卻很低色鸳。

總結(jié)來說社痛,我的fps一直保持在很高,只是說明runloop保持了CADisplayLink的高頻率調(diào)用命雀,但是并不能說明屏幕的刷新率也很高蒜哀。這種方法在一些特殊場(chǎng)景下的檢測(cè)并不準(zhǔn)確,所以這種方法我不是很推薦吏砂,可以使用微信開源的matrix來進(jìn)行fps的監(jiān)控撵儿。

CPU使用率檢測(cè)

一個(gè)進(jìn)程運(yùn)行的基本單位就是線程,因此一個(gè)進(jìn)程中所有線程的使用率加起來就是CPU的使用率狐血,iOS系統(tǒng)為我們提供了這些方法淀歇,還是需要導(dǎo)入#import <mach/mach.h>頭文件,首先thread_basic_info為我們提供了單個(gè)線程的各種屬性匈织,其中一項(xiàng)就是cpu_usage浪默。

struct thread_basic_info {
    time_value_t    user_time;      /* user run time */
    time_value_t    system_time;    /* system run time */
    integer_t       cpu_usage;      /* scaled cpu usage percentage */
    policy_t        policy;         /* scheduling policy in effect */
    integer_t       run_state;      /* run state (see below) */
    integer_t       flags;          /* various flags (see below) */
    integer_t       suspend_count;  /* suspend count for thread */
    integer_t       sleep_time;     /* number of seconds that thread
                                     *  has been sleeping */
};

task_threads這個(gè)函數(shù)又為我們提供了獲取當(dāng)前所有線程的方法牡直,接下來的事情就很簡(jiǎn)單了。

- (integer_t)cpuUsage {
    thread_act_array_t threads; //int 組成的數(shù)組比如 thread[1] = 5635
    mach_msg_type_number_t threadCount = 0; //mach_msg_type_number_t 是 int 類型
    const task_t thisTask = mach_task_self();
    //根據(jù)當(dāng)前 task 獲取所有線程
    kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
    
    if (kr != KERN_SUCCESS) {
        return 0;
    }
    
    integer_t cpuUsage = 0;
    // 遍歷所有線程
    for (int i = 0; i < threadCount; i++) {
        
        thread_info_data_t threadInfo;
        thread_basic_info_t threadBaseInfo;
        mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
        
        if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
            // 獲取 CPU 使用率
            threadBaseInfo = (thread_basic_info_t)threadInfo;
            if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
                cpuUsage += threadBaseInfo->cpu_usage;
            }
        }
    }
    assert(vm_deallocate(mach_task_self(), (vm_address_t)threads, threadCount * sizeof(thread_t)) == KERN_SUCCESS);
    return cpuUsage;
}

這樣就可以獲取CPU的使用率了纳决。

個(gè)人推薦如果是想簡(jiǎn)單的進(jìn)行一個(gè)性能方面的線上監(jiān)控碰逸,使用這些簡(jiǎn)單的小方法就夠了,如果是想對(duì)APP進(jìn)行一個(gè)全量的內(nèi)存監(jiān)控阔加,那么可以使用微信的Matrix饵史。

參考文章:

APP性能監(jiān)控
iOS中基于CADisplayLink的FPS指示器詳解

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掸哑,隨后出現(xiàn)的幾起案子约急,更是在濱河造成了極大的恐慌,老刑警劉巖苗分,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厌蔽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡摔癣,警方通過查閱死者的電腦和手機(jī)奴饮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來择浊,“玉大人戴卜,你說我怎么就攤上這事∽裂遥” “怎么了投剥?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)担孔。 經(jīng)常有香客問我江锨,道長(zhǎng),這世上最難降的妖魔是什么糕篇? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任啄育,我火速辦了婚禮,結(jié)果婚禮上拌消,老公的妹妹穿的比我還像新娘挑豌。我一直安慰自己,他們只是感情好墩崩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布氓英。 她就那樣靜靜地躺著,像睡著了一般泰鸡。 火紅的嫁衣襯著肌膚如雪债蓝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天盛龄,我揣著相機(jī)與錄音饰迹,去河邊找鬼芳誓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛啊鸭,可吹牛的內(nèi)容都是我干的锹淌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赠制,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼赂摆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起钟些,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤烟号,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后政恍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汪拥,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年篙耗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了迫筑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡宗弯,死狀恐怖脯燃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蒙保,我是刑警寧澤辕棚,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站邓厕,受9級(jí)特大地震影響坟募,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邑狸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涤妒。 院中可真熱鬧单雾,春花似錦、人聲如沸她紫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)贿讹。三九已至渐逃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間民褂,已是汗流浹背茄菊。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工疯潭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人面殖。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓竖哩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親脊僚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子相叁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 作者:敖志敏本文為原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明作者及出處 為什么寫這篇文章辽幌? 隨著移動(dòng)互聯(lián)網(wǎng)向縱深發(fā)展增淹,用戶變得越來越關(guān)心...
    滬江技術(shù)學(xué)院閱讀 6,146評(píng)論 2 83
  • 前言 眾所周知,如今的用戶變得越來越關(guān)心app的體驗(yàn)乌企,開發(fā)者必須關(guān)注應(yīng)用性能所帶來的用戶流失問題虑润。目前危害較大的性...
    PerTerbin閱讀 9,653評(píng)論 5 32
  • APP的性能監(jiān)控包括: CPU 占用率、 內(nèi)存使用情況逛犹、網(wǎng)絡(luò)狀況監(jiān)控端辱、啟動(dòng)時(shí)閃退、卡頓虽画、FPS舞蔽、使用時(shí)崩潰、耗電量...
    青蘋果園閱讀 40,871評(píng)論 3 128
  • 本文源自本人的學(xué)習(xí)記錄整理與理解,其中參考閱讀了部分優(yōu)秀的博客和書籍脖岛,盡量以通俗簡(jiǎn)單的語句轉(zhuǎn)述朵栖。引用到的地方如有遺...
    水中的藍(lán)天閱讀 7,126評(píng)論 0 28
  • 黑色的海島上懸著一輪又大又圓的明月陨溅,毫不嫌棄地把溫柔的月色照在這寸草不生的小島上。一個(gè)少年白衣白發(fā)绍在,悠閑自如地倚坐...
    小水Vivian閱讀 3,108評(píng)論 1 5