歡迎光臨
每天分享高質量文章

iOSer:淺談開發中提升工作效率的姿勢

作者:神經騷棟
連結:https://www.jianshu.com/p/a97a1d672471

 

 

簡介

 

回想起來,從畢業到現在在iOS這個行業也努(hua)力(shui)了好幾年,每每看到同事加班到深夜,於心不忍,故寫這篇部落格,總結自己這幾年寫程式碼的感悟,希望能幫助到那些加班到深夜的程式猿們.這篇部落格主要有兩個主題,一是程式碼規範,而是提升效率.雖然兩者看似風牛馬不相及,但其中的聯絡可是大大的存在,當你註重了程式碼規範,那麼你的程式碼質量對應的提升,反正,最終你加班的次數減少就對了~

 

文章也是隨意寫的,沒有什麼順序,也就想到哪寫到哪,各位大佬就當做飯後茶資來看吧~

 

正確的理解什麼叫做寫程式碼,理解業務邏輯的重要性

 

對於什麼叫寫程式碼,什麼叫程式猿?首先來談談我自己對寫程式碼的觀點,開發一個專案其中 50%-70%的工作量為理解業務邏輯,剩餘的部分為編寫程式碼,而在編寫程式碼部分70%的工作量為處理異常情況,只有30%的工作量是開發程式.所以理解專案的業務邏輯是非常有必要的,因為業務邏輯決定UI和UE.例如,某個新增資料的Button 當使用者沒有許可權的時候,是不允許點選的,當你不管這些,允許沒有許可權的使用者點選操作該Button,那麼就很有可能出現Bug,這是最常見的例子.所以,對於專案中業務邏輯,雖然不用做到倒背如流,但是最少要做到熟讀於心.

 

合理架構程式碼,提高工作效率

 

在寫程式碼之前,一定要先去架構自己的程式碼結構,讓它儘量變的合理起來,靈活合理的程式碼結構會讓你更高效的工作,切忌先實現,後最佳化的理念去架構程式碼,有些程式猿(這幾年遇到不少)就是喜歡使用先實現,後最佳化的理念去架構程式碼,或者是連最佳化都沒有,想到一種實現方式就立馬開始碼程式碼,結果一堆Bug存在了自己寫的程式碼裡面.反正各種隱患,日積月累,Bug越來越多.到最後自己都不想去處理了,而且很多時候還是拆了東牆補西牆的情況,反正種種情況不斷. 下麵我就分享一下我寫程式碼的兩種架構方式.

 

  • 正所謂業務邏輯決定程式碼邏輯,所以我們可以透過業務邏輯來架構我們的程式碼結構.例如,現在業務邏輯中的帖子串列,只有展示和新增的邏輯,你就要立馬去想會不會在後面的版本有刪除或者修改的功能,或者還有分享的功能呢?是否需要給這些功能預留介面或者位置?使用者會不會有其他想法或者操作?每一種業務情景都可能對應著Bug,架構程式碼之前多考慮業務邏輯是很有必要的.

  • 當你需要修改某個程式碼模組的時候,這時候你也要先去思考當你修改這個業務邏輯會不會對其他模組造成影響,這裡主要可以透過耦合性來去聯想其他模組,然後去思考如何架構程式碼才能讓相容性更好.這樣修改程式碼是否會對後面的程式碼迭代造成影響?

 

當然了上面的只是簡單的舉例而已,有自己認為合理的架構方式歡迎評論…..?

 

合理復用程式碼,業務邏輯程式碼儘量復用,UI邏輯程式碼少復用.

 

復用程式碼,在很大程度上可以減少程式碼的重覆率,一個重覆率很高的程式碼工程不是一個合格的工程,所以,復用程式碼是非常有必要的.

 

但是我們一定要去合理的復用程式碼,不合理的復用程式碼會造成的最常見問題就是程式碼臃腫,耦合度高.例如,我們一個ViewController檢視控制器在UI的展現形式上在每一個地方都是一致的,但是每一個地方都需要不同的邏輯,有的是隻展示,有的是既展示有可以跳轉,有的是隻跳轉不展示種種邏輯.如果我們都復用這個檢視控制器的話,那麼這個檢視控制器的邏輯程式碼會非常的多,各個使用這個控制器的模組也會因此變得耦合度高了起來.

 

那麼我們應該遵循一個怎樣的復用規律呢?那就是業務邏輯程式碼儘量復用,UI邏輯程式碼少復用(PS:安卓的佈局檔案儘量復用,不涉及邏輯程式碼,儘量復用).為什麼這麼說呢?這是因為業務邏輯決定著UI的展現,業務邏輯發生改變,UI一般就發生了改變,相反,只要業務邏輯不發生改變,業務邏輯程式碼也不需要發生改變.所以,業務邏輯程式碼儘量多復用,例如網路請求方法,我們寫在一個統一的檔案中,誰用誰呼叫即可.只有當後臺發生變化的時候,我們才需要修改程式碼,大大的提高效率.

 

合理理解’閉環’現象,任何入口程式碼在使用者使用過程中都需要出口程式碼.

 

這裡我稱之為’閉環’現象,也就是說任何入口程式碼都需要出口程式碼.當然了,這是我的個人感覺,與其說這是程式碼習慣,不如說它是我的一種思考習慣.而且我常常透過這種形式來完善我的程式碼,比如我Push一個介面,我就會想到底有多少種方式Pop到上一個介面?每一種方式會不會有其他的分支情況等等,再例如使用者進入了某個狀態,怎麼樣才能回到初始狀態?需不需要回到初始狀態(當然,在想這種問題都是假設能回到初始狀態,完成一個’閉環’現象.)等等, 還有就是下麵寫到的if 和switch 的完整性問題,我也是常常用到這種思考方式,來驗證我的程式碼是否完整,一個不是’閉環’的程式碼多多少少都會有點Bug.太深層次的我還沒有體驗,比如一個物件的建立必然會有對應的銷毀過程,等等.

 

利用百度和Google解決日常問題和Bug.

 


 

程式猿日常開發過程中不免遇到這樣或者那樣的問題或者Bug,那麼正確解決問題的姿勢是什麼呢?

 

一般情況下,我會分下麵幾步步驟操作.

 

  • 一、回想自己以前是否遇到過類似問題或者Bug,自己的部落格是否有記錄過這種問題(部落格是程式猿很好的解決問題途徑).有沒有聽說過類似的問題.

  • 二、回想發現沒有該類似問題,那就思考問題可能出現的原因,仔細檢查自己的程式碼邏輯,尋找問題可能出現的位置,打斷點驗證正確性.

  • 三、還是沒有發現問題,這時候,我們就要百度或者Google了,我們要把具體的問題儘量提取出關鍵字來查詢,提高查詢效率.比如,日誌的錯誤碼或者錯誤資訊等等,都是關鍵資訊.

  • 四、其實上面的三步就已經差不多把問題給解決了,但是還是有一些很具體的問題,怎麼辦?我們要去回想我們身邊的大佬有沒有談及這塊的內容,如果有,我們去詢問,儘量去詢問解決思路,而不是解決方法.比如,當時我學習Java的時候,我就問當時我們老大,我說’老大,有沒有相關的書籍或者學習網站呢?’,而不是去問’老大,你教教我Java吧’ 爾爾之語.最後想別人請教的時候,最好是有償的,比如發個紅包什麼的,數量不用太大,這樣做有兩個原因,一,讓別人知道你願意為知識付費,這樣別人以後更喜歡幫助你.二,提醒自己,都TM是錢吶,別隨便去請教別人問題,自己動手,豐衣足食…..

 

說一下反面教材,我曾經碰到不止一個人問我問題,”你好,大佬,我這裡有個問題,我把程式碼發你,你給我看看吧”,”大佬,可不可給我解決這個問題?(其實連文章都沒看,就讓我解決)”如此爾爾,還有很多的人覺得在工作中向別人提問問題是一種好學的體現,但是我要說的是醒醒吧,你已經不在學生時代了,醒醒吧你的老師已經不在你身邊了,你去向別人提問問題,讓別人給你解決,就有可能是浪費他的工作時間,來幫助你,那他的工作可能就完成不了,被老闆罵是他,被老闆噴是他.當然了,對於騷棟自己而言,我還是很喜歡幫助別人的,只是不喜歡伸手黨而已.

 

善用 return 和 break 關鍵詞

 

returnbreak 程式碼中常用的關鍵詞,其實還有一個關鍵詞continue,這裡簡單的說明一下三者的作用以及不同之處.return是用來結束一個方法,break是來結束一個迴圈體,continue是來結束某個迴圈體中的一次迴圈.

 

那麼為什麼要善於運用 return break 呢? 這主要是當陣列遍歷的時候,我們已經尋找到了我們想要的資料的時候,我們就可以停止迴圈體,或者停止函式了,具體是選擇return 還是break ,要根據獲取到我們想要的資料後續是否還有操作來作為依據.下麵我們就舉例來說明.

 

例: 傳回陣列中元素值為”test”的下標(有且只有一個),並且組成”第x個元素為test”傳回,沒有則傳回nil

 

  • 在未做最佳化程式碼之前, 我們一般會想到我們要在迴圈體的外部建立一個字串空物件,然後遍歷陣列,找到符合條件的下標,組裝字串,然後在迴圈體外傳回.但是這樣做就會可能造成效能的浪費,比如要是陣列元素個數為10個,符合下標的元素是在第一位,也就是說後面九次的迴圈都是毫無意義的,從而造成資源的浪費.

 

- (NSString *)returnThirdItemWithArray:(NSArray *)array {

    NSString *thirdItem = nil;
    for (int i = 0; i         NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            thirdItem = [NSString stringWithFormat:@"第%d個元素為test",i + 1];
        }
    }
    return thirdItem;
}

  • 下麵為最佳化過後的程式碼, 我們直接把return放在了if當中,這樣當在陣列中找到合適的元素的時候就會立馬跳出函式.不會有過多的效能浪費,我們要把握的時機就是隻要當函式滿足我們的需求時就停止函式的進行即可.

 

- (NSString *)returnThirdItemWithArray:(NSArray *)array {

    for (int i = 0; i         NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            return [NSString stringWithFormat:@"第%d個元素為test",i + 1];
        }
    }
    return nil;
}

 

break關鍵詞和上面的基本一致,主要是用於在當前函式當中跳出迴圈體時還需要做其他操作.這裡就不多細說了.看例子吧~

 

//傳回陣列中元素值為”test”的下標(有且只有一個),並且組成”內容為test的元素的下一個下標為xxx”.

 

  • 程式碼最佳化之前

- (void)findItemWithArray:(NSArray *)array {

    int index = 0;
    for (int i = 0; i array.count; i++) {
        NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            index = i + 1;
        }
    }
    NSLog(@"內容為test的元素的下一個下標為%d",index);
}

  • 程式碼最佳化之後

//傳回陣列中元素值為"test"的第一個下標,並且組成"第x個元素為test"傳回,沒有則傳回nil
- (void)findItemWithArray:(NSArray *)array {

    int index = 0;
    for (int i = 0; i array.count; i++) {
        NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            index = i + 1;
            break;
        }
    }
    NSLog(@"內容為test的元素的下一個下標為%d",index);
}

關於 if 和 switch 產生 Bug 的思考

 

我可說在很多的初級小白百分之五十的Bug都是由於情況考慮不全導致的,那麼在體現在程式碼上是什麼樣呢?在程式碼上主要就是if和switch寫的不完整造成情況考慮不全從而產生各種Bug.

我們先說一下if判斷陳述句,什麼叫完整的if,什麼叫不完整的if,如下程式碼所示.

 

  • 不完整的if陳述句寫法

    if (條件) {
        操作
    }

    或者

    if (條件) {
        操作
    } else if (條件) {
        操作
    }

  • 完整的if陳述句寫法

    if (條件) {
        操作
    } else {
        操作
    }

    或者

    if (條件) {
        操作
    } else if (條件) {
        操作
    } else {
        操作
    }

關於完整性的if陳述句這種做法,很多書很多文章都稱之為if陳述句的窮舉法(自己看過<>中就有說到),也就是把if所有的情況都列舉出啦,哪怕它不需要任何的程式碼操作.

 

<>PDF版傳送門

 

對比上面的兩種if,很多看官又會說到,臥槽,你這是侮辱我智商呢?我剛剛學習程式設計就會了,只是後面為了方便,所以就不寫完整了,其實我工作以來也是基本上很少寫完整的if陳述句,能少些就少些,程式碼同時整潔易懂.何樂而不為?但是要註意的是,在程式碼層面上你可以不寫完整,但是你在心中一定要去把if陳述句的所有情況進行窮舉,因為每一個if分支情況都可能是一個隱藏的Bug,這可能是業務邏輯方面的,也可能是程式碼邏輯方面的.所以對if陳述句進行窮舉操作是很有必要的.

 

那麼對於switch是一樣的情況,switch中有default關鍵詞,很多時候,我們並不寫default部分,但是default部分也算是一個情況分支,這是我們所需要註意.但是有一種情況例外,那麼就是switch的判斷條件為列舉值的時候,這時候,情況總體個數已經根據列舉值的多少而定下了,所以不需要寫default部分了.

 

串列檢視能區域性掃清絕對不全部掃清.

 

對於串列掃清是我們日常開發中最常見的一個操作,例如資料的刪除,新增,變動,修改等等都需要我們去掃清串列,很多時候我們都是直接使用[self.mainTableView reloadData];來掃清資料,但是我們仔細想想假定就只有一個或者有限的Cell需要掃清,你使用上面的那句話,那不是白白造成了許多的記憶體資源浪費嗎?所以我們能使用區域性掃清絕對不使用全部掃清.

 

舉例子說明,iOS這邊我們能使用下麵方法就使用下麵方法進行區域性掃清,雖然在程式碼量會有所提升,但是不會造成大量的資源浪費.

 

- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;

- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;

 

善用宏定義和列舉,減少魔法數字的使用.

 

何為魔法數字?就是根本沒有任何的解釋,隨心所欲的寫在程式碼之中的數字,反正就是讓人不明覺厲的那種就對了~ 魔法數字的危害性主要會體現在專案後期的維護上,在開發階段的時候,你根據隨手寫上了一個魔法數字,可能是寬高資訊,可能是邊距資訊,但是你沒有寫任何的註釋來表明這個數字是怎麼來的,是做什麼用的,我相信不出三個月,連你去看這個你當前的魔法數字都會覺得很神秘.所以,在你的程式碼中減少魔法數字扥出現是很有必要的.

 

那麼如何去消除魔法數字這種危害性呢?

 

一,增加合理註釋,解釋這個魔法數字是如何產生的,在程式碼當中有著怎麼樣的作用,雖然這樣可以在一定程度上解決了魔法數字的危害,註釋卻多的一皮,還有就是後期維護非常麻煩,假設很多的魔法數字分佈在你的專案各個角落中,後期你要改的話,需要先去找到這個魔法數字的位置,然後再去修改,是你自己寫的程式碼還好,如果是別人的程式碼,光這個找的時間,就夠自己喝一壺的了~

 

二,既然使用註釋的方式不能完全解決魔法數字問題,我們就看一下使用宏定義和列舉如何解決魔法數字問題.(其實當某一種魔法數字少量的時候,使用註釋是完全可行的~,酌情而定)

 

  • 宏定義方式來定義魔法數字,全域性都可能用到的魔法數字,我們就放在pch檔案中,如果只是某幾個類可能用到的檔案,我們就直接建立一個.h檔案,然後需要的匯入即可,對於單個類使用的魔法數字宏定義,我們直接放在頭部即可.這樣的方式不但方便管理魔法數字,而且簡介明瞭,後期維護起來也是非常的方便.具體程式碼示例如下所示.

 

//pch 檔案中的全域性宏定義
#define NavigationBarHeight  (44.0f)

#define TabBarHeight  (49.0f)

#define KNormalEdgeDistance  (16.0f)

#define KNormalViewDistance  (10.0f)

 

#ifndef HomeHeader_h
#define HomeHeader_h

//HomeHeader是所有帖子串列的所需資訊主要包含內容高度,Cell圖片部分的尺寸

//Cell左右邊距
#define EdgeDistance (15.0f * 4)

//Cell頂部邊距
#define TopEdgeDistance (15.0f)

//頭部分組資訊高度
#define HeaderInfoHeight (27.0f)

#endif /* HomeHeader_h */

 

  • 再來說一下列舉的問題.列舉值也是很好的解決魔法數字的方式,註釋是用於狀態的展示,如果有兩種狀態,我們一個布林值就可以解決了,如果是多種狀態,如果不用列舉的話,到時候程式碼中各種if (style == 1) {}等等魔法數字,完全讓人摸不到頭腦,各種翻檔案,各種翻介面找到對應的業務意義.大大浪費了時間.但是我們如果定了列舉型別了呢? 我們就可以快速的透過字面的意思推測出型別的意義,例如if (style == DrawStyleLine) {},我們可以清楚的明白我們的繪製的樣式為線性,定義的列舉型別如下所示.

typedef enum : NSUInteger {
    DrawStyleLine,
    DrawStyleSquare,
    DrawStyleCircle,
    DrawStyleArrow,
    DrawStyleHand,
} DrawStyle;

當然,宏定義和列舉除了能解決魔法數字問題,還能解決書寫錯誤問題,比如我們因為不小心把if (style == 1) {}寫成if (style == 10) {}在編譯過程中是沒有任何錯誤的,只有在執行過程中才可能暴露出其對應的Bug來,但是我們如果使用宏定義或者列舉,我們書寫不全,在編譯過程中就直接顯示錯誤,例如把DrawStyleLine寫成DrawStyleLina,編譯器會直接提示我們書寫錯誤,這樣也會有助於避免我們在這些小問題上翻車.

 

多利用 位移列舉 的位運算實現業務邏輯中多選操作

 

我們經常會在iOS中的.h看到這樣的列舉,例如對於貝塞爾曲線的指定角進行切邊操作,用到的列舉型別,如下所示.

 

typedef NS_OPTIONS(NSUIntegerUIRectCorner) {
    UIRectCornerTopLeft     = 1 <0,
    UIRectCornerTopRight    = 1 <1,
    UIRectCornerBottomLeft  = 1 <2,
    UIRectCornerBottomRight = 1 <3,
    UIRectCornerAllCorners  = ~0UL
};

這時候我們會發現列舉型別的值並不是我們常見的0,1,2,3等等,而是1 << 0,1 << 2,1 << 3等等,如下圖所示,這代表著位執行的表示形式, 示例解釋如下所示.

 

1 << 0 代表著 十進位制的 1 左移 0 位 那麼就是 0001 (十進製為1,具體運算為1(2^0)),
1 << 1 代表著 十進位制的 1 左移 1 位 那麼就是 0010 (十進製為2,具體運算為1(2^1)),
1 << 2 代表著 十進位制的 1 左移 2 位 那麼就是 0100(十進製為4,具體運算為1*(2^2)),

 

再來給各位小白惡補一下位運算的幾種運運算元號的意義

位運算的幾種常用運運算元號的意義
  • <<  左移運運算元,就是將某一個整數的二進位制整體左移n位,例如 整數5(二進製表示為0101)的位運算 5 << 1,那麼結果就是整數10 (二進製為1010);

  • >>  右移運運算元,就是將某一個整數的二進位制整體右移n位,和左移運運算元類似.

  • &  按位與運運算元,只有對應的兩個二進位均為1時,結果位才為1,否則為0,  例如5&9=1,解釋為0101&1001=0001,轉化成整數就是1.

  • |  按位或運運算元,只要對應的二個二進位有一個為1時,結果位就為1,否則為0,   例如5|9=13,解釋為0101|1001=1101,轉化成整數就是13.

那麼說了這麼多,位移列舉的位運算到底有什麼的用途呢?其實,這樣的列舉任意幾個列舉值相加的值(用其 按位或運算即可~) 都是不一樣的,不信可以試驗一下~我們也就是說可以對列舉值的任意組合進行判斷,我們就用UIRectCorner來說明一下,假設我們當我們選擇的是UIRectCornerTopLeft和UIRectCornerTopRight的時候,我們就讓view的背景色為紅色,當我們選擇的是UIRectCornerTopLeft和UIRectCornerBottomLeft我們就為黑色,其他的都為白色,示例如下.

 

if (value == UIRectCornerTopLeft|UIRectCornerTopRight) {
    view.backgroundColor = [UIColor redColor];
} else if (value == UIRectCornerTopLeft|UIRectCornerBottomLeft) {
    view.backgroundColor = [UIColor blackColor];
} else {
    view.backgroundColor = [UIColor whiteColor];
}

 

有人就會問我們用普通的列舉來做多選會有什麼問題,下麵我來定義一個列舉型別,大家來看一下,仍然用UIRectCorner來做說明.

 

// 錯誤演示
typedef NS_OPTIONS(NSUIntegerUIRectCorner) {
    UIRectCornerTopLeft     = 1 ,
    UIRectCornerTopRight    = 2,
    UIRectCornerBottomLeft  = 3,
    UIRectCornerBottomRight = 4,
    UIRectCornerAllCorners  = 5
};

當我們選擇 UIRectCornerTopLeft|UIRectCornerTopRight的時候計算出來的值為3,也就是說選擇 UIRectCornerTopLeft|UIRectCornerTopRight和選擇UIRectCornerBottomLeft是沒有任何區別的.因為我們的判斷依據只能是列舉所代表的值.這樣就會出現了問題,做不成多選操作,這種型別的列舉只能來做單選操作.

 

當然了,還是會有人比比用下麵的例子說,這樣不是也能多選嗎?但是 1 就是 1 <

 

typedef NS_OPTIONS(NSUIntegerUIRectCorner) {
    UIRectCornerTopLeft     = 1 ,
    UIRectCornerTopRight    = 2,
    UIRectCornerBottomLeft  = 4,
    UIRectCornerBottomRight = 8,
    UIRectCornerAllCorners  = ~0UL
};

合理理解高內聚,低耦合 控制單個檔案的程式碼量

 

在上一家公司的時候,那時候的我還是那麼天真單純,當我接手iOS專案時,再一次掃清了我的三觀,這是為什麼呢?因為這個專案被上一夥人解耦解到支離破碎的,邏輯分散的各個角落中了,簡直是慘不忍睹.最後一問原來是有後臺開發大佬參與了開發~ 後來我接觸了Java後臺,我才明白為什麼會寫的支離破碎,在Java的前後端不分離的webApp中,View和Controller就是完全分離的~但是在iOS中,View和Controller的邏輯在一定程度上是內聚的.當然了,埋怨當時的後臺開發人員,畢竟每一種程式語言都有一定的規則.

 

好了,言歸正傳,我們來說說高內聚,低耦合的問題,高內聚,低耦合這個概念我相信在學程式設計之初,你的老師就一定提過,高內聚就是讓我們要把相關度比較高的部分盡可能的集中,不要分散.但是一旦過分高內聚,就會造成程式碼臃腫不堪,業務邏輯混亂複雜的情況,而低耦合就是讓我們把兩個相關的模組盡可以能把依賴的部分降低到最小,不要讓兩個系統產生強依賴.但是如果過度低耦合,那麼就會造成上面的那種情況,程式碼邏輯支離破碎,程式碼可讀性非常差.所以具體的高內聚,低耦合的概念如何在你的程式碼中體現,是需要一定的程式設計經驗的~ 當然,高內聚低耦合這個概念的標準,什麼時候該內聚,什麼該解耦,在每一個程式猿眼裡,我相信都是不一樣,有的人認為這個部分應該解耦,認為邏輯堆在這裡過於臃腫,但是有的人卻認為這裡的程式碼根據業務邏輯就應該堆在這裡,可以提高程式碼的可讀性,所以這個標準是隻可意會不可言傳的,哈哈.只要心中有這個概念,不用刻意去追求,水到渠成即可~

 

透過內聚和解耦,我們可以合理的控制單個程式碼檔案的程式碼量,其實我不建議一個程式碼檔案中的程式碼量太多.這樣會造成程式碼非常的臃腫,可讀性也是很差的.比如我以前寫過一個串列的九宮格Cell(每一種情況都是一個新的UI),裡面的程式碼超過兩千多行,著實是臃腫不堪~維護起來非常的麻煩.這時候,我們就可以把部分的程式碼抽出來,寫在一個新的檔案中.當然了,如果實在是解耦不了,我們一定要去加註釋,註明這裡的程式碼是乾什麼用的,為什麼要這麼做等等,為後期維護或者二次開發做好鋪墊工作.

 

合適使用註釋 ,相對於“做了什麼”,更應該說明“為什麼這麼做”

 

程式碼千萬行,註釋第一行;程式設計不規範,同事兩行淚
透過上面的詩句,我們就可以深刻的體會到註釋的重要性,從我們開始寫第一行的程式碼時候,很多的大佬就會教導我們一定要把註釋寫好,寫明白,我想大多數程式猿會有這種感覺,如果不寫註釋,當我們自己會看自己三個月前的程式碼時,我們都會大聲的說一句,”我擦,這是寫的什麼鬼~ ,這肯定不是我寫的”,所以寫註釋,寫好註釋,這是一種利人利己的行為,何樂為不為?網上很多有很多寫註釋的重要性的部落格或者文章,我想都是深受其害的程式猿~

 

在寫註釋的時候,既不能像大姨媽一樣拖拖拉拉的寫一堆,也不能為了成為衛生標兵就一點也不做註釋,合理的註釋會讓你的程式碼可讀性更高(其實,就算你把註釋註的再詳細,我想別人也不願意去看你寫的程式碼,通病而已,?),我們在寫註釋的時候,不但要寫明這程式碼是乾什麼用的,有時候更應該寫為什麼要去這麼寫,你當時所想的想法是什麼,比如有個計算Cell的高度的時候,由於裡面可能有固定高度也可能有可變高度,我一般會在Cell的.h檔案中如下圖進行類似註釋(雖然這個類是用來做tableViewHeaderView的,但是是一樣的.).詳細的註明,Cell的高度是怎麼來的,哪怕是日後再也看這些程式碼也是很輕鬆的.要不然,真的就成了”程式設計不規範,同事兩行淚”了.

 

 

利用 狀態機 和 列舉 完成一個控制器多種狀態UI展示效果

 

狀態機的這個概念我第一次接觸是在Untiy 3D 做遊戲用到的,其實就是一個監控狀態流轉的模組,例如,一個人有三種操作,一個是停止吃喝,一個是吃飯,一個是喝水.我們可以讓一個人從吃飯到停止吃喝,或者從停止吃喝到吃飯,但是我們不能讓一個人吃飯直接切換到喝水.因為我們要停止了吃飯,再去喝水,當然了,你非要一手吃飯一手喝水也行~ 請出門右轉不送.狀態的流轉,什麼樣的狀態可以流轉到什麼狀態,不可以流轉到什麼樣的狀態,我們做一總結,這就是初步的狀態機.

 

當我們在一個頁面中我們有多種UI要展示,而我們要放在同一個控制器中,那麼我們就要考慮狀態的流轉了,其實也就是狀態機的問題.如果不做好狀態的流轉,可能會導致邏輯程式碼分散到各個位置,反正就是一個字,亂.

 

這時候,我們要先去理清UI的狀態是如何流轉的,然後我們要定義列舉,有多少種狀態就定義多少種列舉值.例如我做過的一個關於Socket的專案中就有一個介面根據socket的不同狀態需要展示不同的UI,列舉程式碼如下所示.

 

typedef enum : int {
    ConnectedStateWIFINotConnect,//wifi還未連線
    ConnectedStateWIFIContented,//wifi已連線
    ConnectedStateSocketConnecting,//socket連線中
    ConnectedStateSocketNotConnect,//socket連線失敗
    ConnectedStateSocketContented,//socket已經連線
    ConnectedStateSocketDisconnect//socket斷開連線
} ConnectedState;

接著,我們需要用switch 來做狀態的流轉之後的邏輯程式碼, 有人說為什麼不用if else做,我不多說,自行體會去.部分程式碼(已經做了刪減了)如下所示.

 

- (void)loadSubViewStateAction {

    // 刪除所有的子控制元件,然後重新新增,我用的是懶載入的形式,所以不太用考慮效能問題.
    [self.view.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];

    switch ([SocketClinetManager defaultManager].connectedState) {

        case ConnectedStateWIFINotConnect:{
            // 當前WIfI未連線  分為未系結盒子和已係結盒子
            if ([UserManager defaultManager].connectBoxName == nil) {
                [self.view addSubview:self.bindButton];
            } else {
                [self.view addSubview:self.reloadConnectButton];
                [self.view addSubview:self.boxConnectWifiView];
            }
        }break;

        case ConnectedStateWIFIContented:{
            // 當前WIFI已連線
            _socketContentInfoLabel.text = @"裝置未連線";
            [self.view addSubview:self.connectBoxButton];
            [self.view addSubview:self.reloadBindButton];
        }break;

        case ConnectedStateSocketConnecting:{
            _socketContentInfoLabel.text = @"連線中...";
            [self.view addSubview:self.reloadBindButton];
            [self.view addSubview:self.reloadConnectButton];
            [self socketStateImageViewStartAnimationAction];
        }break;

        case ConnectedStateSocketNotConnect:{
            // socket未連線
            _socketContentInfoLabel.text = @"裝置連線失敗";
            [self.view addSubview:self.reloadBindButton];
            [self.view addSubview:self.reloadConnectButton];
        } break;
        case ConnectedStateSocketContented:{
            // scoket連線成功
            _socketContentInfoLabel.text = @"連線狀態正常";
            [self.view addSubview:self.reloadBindButton];
            [self.view addSubview:self.disconnectButton];
        }break;

        case ConnectedStateSocketDisconnect:{
            _socketContentInfoLabel.text = @"裝置未連線";
            [self.view addSubview:self.connectBoxButton];
        }break;
    }
}

然後我們只需要修改[SocketClinetManager defaultManager].connectedState的值,然後呼叫loadSubViewStateAction這個方法就可以得到我們所需要的UI了.其實,這個模組算的上是一個開發經驗吧,如果有這種需要可以用上這種樣式,這樣寫我個人感覺把UI狀態流轉放到一個地方更方便去管理,在程式碼的可讀性上也會有更大的提高.

 

合理使用懶載入.尤其是在 removeFromSuperView時候,就是兩字,真香.

 

按需載入,是最佳化程式碼的一個重要的途徑.先說說我自己吧,寫了這麼多年,一直在使用懶載入的形式建立控制元件,其實在那些檢視要出現的就需要載入完成的控制元件身上,懶載入並沒有提高什麼效率,反而讓程式碼量上升了,這種情形最好的好處也就是程式碼規範整潔,不用所以的控制元件初始化都擠在一個方法裡面,其他別無用途.但是當我們有彈窗這種使用者主觀調出的檢視的時候,我們就可以用懶載入的形式,這樣當使用者需要的時候,我們才去分配記憶體,初始化控制元件,不用在父類初始過程中就要分配記憶體空間,降低了程式記憶體峰值,提高了效率.

 

但是我要說的時候,什麼時候使用懶載入是最爽的?那就是當Cell中有個控制元件,有的資料需要展示,有的資料則不需要展示,我們在配合上下劃線直接訪問屬性的形式,就是兩字,真香.

 

說的再多,也可能是白扯,我們看一下例子~ 這裡有一個班級選擇串列,當使用者選擇某個班級的時候,後面才會會出現選中按鈕,否則不會出現選中按鈕,如下圖所示.

 

Cell中部分程式碼如下所示.初始化過程中不做任何操作,懶載入還是平常的懶載入.

 

// 初始化過程中不做任何操作
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {

    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        self.selectionStyle = UITableViewCellSeparatorStyleNone;
    }
    return self;
}

//選中圖片的懶載入
- (UIImageView *)selectImageView {

    if (_selectImageView == nil) {
        _selectImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"common_class_select_icon"]];
        _selectImageView.frame = CGRectMake(KmainWidth - 48.0f, 048.0f, 48.0f);
        _selectImageView.contentMode = UIViewContentModeCenter;
    }
    return _selectImageView;
}

但是在賦值的時候就是提高效能的時候,我們的想法是Cell上是否含有圖片控制元件,我們都刪除.然後重新新增圖片控制元件,這就是我們為什麼在這裡使用的是_selectImageView的原因,_selectImageView可以不透過set方法直接訪問成員屬性,所以假設圖片控制元件沒有被建立,那麼就是[nil removeFromSuperview]了,為什麼不用[self.selectImageView removeFromSuperview];呢?因為一旦self.selectImageView就會呼叫get方法從而建立控制元件,懶載入也就失去了意義.通俗講法就是,刪除控制元件的時候,控制元件沒有初始化我就不進行刪除,如果有,那麼我就進行刪除控制元件操作.從而提高程式碼效率.

 

//賦值資料時
- (void)setDataModel:(ClassModel *)dataModel {

    _dataModel = dataModel;

    [_selectImageView removeFromSuperview];

    if (dataModel.isSelect) {
        [self.contentView addSubview:self.selectImageView];
    }
}

 

及時解決程式碼冗餘問題

 

程式碼冗餘這種問題說大不大,說小不小,程式碼冗餘每一個工程都或多或少有這樣的問題,其實冗餘的程式碼在業務邏輯上並不會有太大的影響,但是在後期程式碼維護上是存在著一定的問題,一定程度上增加了閱讀難度,所以當你發現自己的程式碼冗餘的時候,一定要及時刪除冗餘的程式碼.

 

學習並使用優秀的三方元件,嘗試自己封裝一些侵入性低的元件

 

首先說明一點,雖然以前的我造過不少的輪子,我不太提倡在工作中重覆的去造輪子,這主要是因為自己造的輪子可能由於開發時間過短會存在各種問題或者Bug.而且還浪費你的工作時間,降低了工作效率.很多的優秀的三方迭代的較多,所以穩定性很好,如果你在工作中需要某一個三方,我建議先去網上找找看,如果有合適的,儘量使用那麼穩定的三方來解決工作中的問題.提高自己的工作效率,這樣就不用天天加班到深夜了.

 

很多優秀的三方元件都是值得我們去學習的,我們可以透過檢視元件原始碼的形式學習開發者當時的開發邏輯,看多了,自然也就懂了.

 

當然了,在我們業餘的時間,我們可以去嘗試封裝一些入侵性較低的元件,下麵有侵入性的解釋,侵入性就伴隨著耦合問題,所以在封裝元件的時候,元件的侵入性是一個很好的衡量元件優劣的方式.

 

當你的程式碼引入了一個元件,導致其它程式碼或者設計,要做相應的更改以適應新元件.這樣的情況我們就認為這個新元件具有侵入性.

 

做好釋放工作,完成”閉環”現象

 

當一塊記憶體被分配的時候,你就需要想這塊記憶體需不需要你自己來釋放它(也就是上面提到的”閉環”現象).當你確定這塊記憶體需要你來釋放,不妨提前寫好釋放程式碼,防止自己遺忘.大大減少因為記憶體釋放問題所造成的Bug或者問題的數量.反正牢記”建立就要銷毀”的理念就行了.

 

不炫技,簡單易懂才是最屌的程式碼

 

自己學到了某一個新的框架或者元件,總是想著把它使用到我們的專案當中,雖然這樣是沒有問題的,但是我們忽略了它的穩定性和可讀性,一個新的框架可能會存在很多的問題或者Bug,所以程式碼的穩定性是一個很大的問題,再加上當別人來接手你的程式碼時,很有可能因為這些新的框架而需要額外的學習時間,從而造成了工作效率的降低,這都是一些潛在的風險.

 

保持函式的功能單一性,控制每個函式的程式碼量

 

我們在構建一個函式之前,我們要思考這個函式到底在我們程式中扮演著什麼樣的功能模組,從而保持函式的功能單一性,從程式碼結構上來說,一個功能單一的函式更利於閱讀,同時,由於我們需要保持每個函式的功能單一性就必然會去抽離程式碼,重新組裝新的函式,這樣每一個函式的程式碼量都不會有太多.閱讀起來相當的輕鬆.

 

例如,我寫的自定義UITableViewCell都是透過懶載入的方式抽離出程式碼,如下圖所示,這樣的程式碼層次感就出現了,使人更容易理解程式碼邏輯.而不是把所有的控制元件初始化都放在init方法中,如果這樣做的話,雖然沒有任何的問題,但是到底是什麼控制元件添加了什麼控制元件就需要仔細閱讀程式碼了,增加了閱讀的難度.

 

 

淺談NSTimer的釋放問題

 

很多iOS初級開發者在使用NSTimer做定時器功能的時候,往往一個不訊息就會造成了記憶體洩露問題,當然了,這也包括我在內,我們來舉例說明一下NSTimer的釋放問題.

首先,我們來舉例子說明一下NSTimer的迴圈取用.首先我們在ViewController分類中定義一個NSTimer的成員變數.如下所示.

 

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic,strong)NSTimer *timer;

@end

 

然後我們在下麵的delloc中釋放該NSTimer物件.

 

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

- (void)timerAction {

}

- (void)dealloc {

    if (_timer != nil) {
        [_timer invalidate];
        _timer = nil;
    }
}

@end

然後我們就會發現dealloc方法根本不走,也就是說我們釋放不了這個NSTimer物件,這樣就迴圈取用了 ,然後有人就說,你用strong強取用NSTimer物件了.所以釋放不了,但是我要告訴就算我改成下麵哪種方式,迴圈取用依然是存在的.

 


@interface ViewController ()

@property(nonatomic,weak)NSTimer *timer;

@end

 

@interface ViewController ()
{
    NSTimer *timer;
}

@end

 

那麼NSTimer迴圈問題到底出現在哪裡呢?這個迴圈取用的根源是在Target上,其實NSTimer的target引數會被RunLoop所持有(此時ViewController物件取用計數為2),也就是說銷毀介面的時候,ViewController物件取用計數依然是1,故不能被釋放,也就不能走dealloc方法.所以造成了迴圈取用.

 

 

既然知道了問題所在,我們只要打破環中一個位置即可,這裡常見的方式就是在使用者主動呼叫的方法中釋放NSTimer,先釋放NSTimer,然後RunLoop釋放了對ViewController物件的持有,ViewController物件的取用計數變為1,然後銷毀介面ViewController物件的取用計數變成0,物件被成功銷毀,如下所示.

 

//假設是導航控制器 Push的介面
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    if (_timer == nil) {
        [_timer invalidate];
        _timer = nil;
    }
    [self.navigationController popViewControllerAnimated:YES];
}

// dealloc不做操作
- (void)dealloc {

}

 

在iOS 10 出現了一個新的NSTimer構建方法,那就是使用block的形式,雖然能解決上的問題,但是依然需要註意block中self的迴圈取用問題,具體方法如下圖所示.

 

 

總結NSTimer釋放秘訣就是下麵的這句話.

 

透過使用者主動操作呼叫的方法中來釋放NSTimer,任何時候都不要在dealloc中釋放NSTimer.

 

不建議在if中新增過長的判斷陳述句,如果需要,那麼就分行顯示,提高程式碼的可讀性

 

if分支陳述句中的判斷條件有時候很多,如下所示.如果我們順著寫,程式碼不但臃腫了~ 閱讀起來及其的不方便.這時候我們就可以使用分行顯示的形式,來展示我們的判斷條件.提高閱讀效率.如下所示.

 

        // 未最佳化之前
        if ([test isEqualToString:@"條件1"] || [test isEqualToString:@"條件2"] || [test isEqualToString:@"條件3"] ) {

        }

        // 最佳化之後
        if ([test isEqualToString:@"條件1"] ||
            [test isEqualToString:@"條件2"] ||
            [test isEqualToString:@"條件3"]) {

        }

 

當然假設某一個條件過長的時候,我們也可以利用抽離的方式,讓程式碼看起來整潔大方.具體例子如下所示.

 

        // 未最佳化之前
        if ([test isEqualToString:@"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] ||
            [test isEqualToString:@"條件2"] ||
            [test isEqualToString:@"條件3"]) {

        }

 

        // 最佳化之後

        BOOL firstCondition = [test isEqualToString:@"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"];
        if (firstCondition ||
            [test isEqualToString:@"條件2"] ||
            [test isEqualToString:@"條件3"]) {

        }

 

程式碼風格要規範統一,做到自己現有水平的最好標準.

 

自己的程式碼一定要有統一的風格,一個良好的程式碼風格是一個程式猿最基本的要求,一個良好的程式碼風格可以讓別人在閱讀自己寫的程式碼時候更加輕鬆.萬萬不可在每一個程式碼檔案中都有著不同的風格,這樣在閱讀程式碼的時候肯定是非常難受的.寫程式碼的時候我們要做到自己現在水平的最好,要像對待自己的孩子一樣對待自己的程式碼,你只有呵護它,它更好的才能回報於你.下麵我們就談一下幾種常見的程式碼規範.可以稍微參考.

 

參考於<>

 

  • 命名規範問題


命名儘量使用駝峰命名法,命名的時候不可隨意取名,例如 action1 ,儘量要做到見名知意.

 


我們知道駝峰命名可以很清晰地體現變數的含義,但是當駝峰命名中的單元超過了3個之後,就會很影響閱讀體驗:
userFriendsInfoModel
memoryCacheCalculateTool
是不是看上去很吃力?因為我們大腦同時可以記住的資訊非常有限,尤其是在看程式碼的時候,這種短期記憶的侷限性是無法讓我們同時記住或者瞬間理解幾個具有3~4個單元的變數名的。所以我們需要在變數名裡面去除一些不必要的單元.(PS:這一點我還真沒做到….?)

 


不能使用大家不熟悉的縮寫
有些縮寫是大家熟知的:
doc 可以代替document
str 可以代替string
但是如果你想用BEManager來代替BackEndManager就比較不合適了。因為不瞭解的人幾乎是無法猜到這個名稱的意義的。
所以類似這種情況不能偷懶,該是什麼就是什麼,否則會起到相反的效果。因為它看起來非常陌生,跟我們熟知的一些縮寫規則相去甚遠。

 

  • 提高程式碼的美觀性


在宣告一組變數的時候,由於每個變數名的長度不同,導致了在變數名左側對齊的情況下,等號以及右側的內容沒有對齊:

 

NSString *name = userInfo[@"name"];
NSString *sex = userInfo[@"sex"];
NSString *address = userInfo[@"address"];

而如果使用了列對齊的方法,讓等號以及右側的部分對齊的方式會使程式碼看上去更加整潔:

 

NSString *name    = userInfo[@"name"];
NSString *sex     = userInfo[@"sex"];
NSString *address = userInfo[@"address"];

 

這二者的區別在條目數比較多以及變數名稱長度相差較大的時候會更加明顯。

 


當涉及到相同變數(屬性)組合的存取都存在的時候,最好以一個有意義的順序來排列它們:

 

  • 讓變數的順序與對應的HTML表單中欄位的順序相匹配

  • 從最重要到最不重要排序

  • 按照字母排序

 

舉個例子:相同集合裡的元素同時出現的時候最好保證每個元素出現順序是一致的。除了便於閱讀這個好處以外,也有助於能發現漏掉的部分,尤其當元素很多的時候:

 

//給model賦值
model.name    = dict["name"];
model.sex     = dict["sex"];
model.address = dict["address"];

 ...

//拿到model來繪製UI
nameLabel.text    = model.name;
sexLabel.text     = model.sex;
addressLabel.text = model.address;

 


有些時候,你的某些程式碼風格可能與大眾比較容易接受的風格不太一樣。但是如果你在你自己所寫的程式碼各處能夠保持你這種獨有的風格,也是可以對程式碼的可讀性有積極的幫助的。

比如一個比較經典的程式碼風格問題:

 


if(condition){

}

 

if(condition)
{

}

 

對於上面的兩種寫法,每個人對條件判斷右側的大括號的位置會有不同的看法。但是無論你堅持的是哪一個,請在你的程式碼裡做到始終如一。因為如果有某幾個特例的話,是非常影響程式碼的閱讀體驗的。

 

熟悉常用的顏色以及顏色的表示方式

 


 

作為一個程式猿,日常開發中在UI方面說的最多的可能就是”RGB值”,”#f5f5f5″,”RGBA”等等,每一天都和各種各樣的顏色打交道,但是我見過很多的程式猿對顏色這塊的知識實在太少了,今天,我們就簡單地聊聊關於顏色一些常識.

 

而我們在日常中UI給我們最多的就是RGB值表示顏色,例如下下圖所表示的 “#F500CC”,那麼其中F5代表著紅色的十六進位制值,00代表綠色的十六進位制值,CC代表著藍色的十六進位制值,我們知道十六進位制是從0到F,所以兩位的十六進位制最大值為16*16 = 256 (十進位制),這也就是256顏色值的來由.三者全都是256的值那就是#FFFFFF(白色),三者都是0的值那就是#000000(黑色).

 

那麼,現在我們就可以建立一個簡單的顏色,比如我只想要紅色,那麼我們就讓紅色的值不為0,然後綠色和藍色的值都是0即可,比如#FF0000,就是最滿的紅色,當紅色的值變小時,顏色逐漸趨於黑色,我們可以透過下麵來來瞭解這種變化.

 

再例如,我們可以透過本模組的第一個圖來調出黃色,黃色就是紅色加上綠色,那麼RGB十六進製表示方式就為#FFFF00.其他的顏色以此類似.

 

我們接下來說一個比較有意思的顏色,那就是灰色,很多專業屬於稱之為中性灰,灰色是怎麼來的呢?灰色其實就是RGB三個值是一樣的即可. 例如 #C0C0C0 , #F5F5F5等等,只有是#ABABAB或者#AAAAAA的形式都是中性灰.這裡還要說一個中性灰叫做 #808080 ,很多人稱之為絕對中性灰.

 

寫部落格是一個快速提高的姿勢

 

從我剛開始工作開始,我就一直在寫部落格,雖然也是中間也是斷斷續續,但是我絕對要說寫部落格是讓一個程式猿快速成長的良好途徑.當然了,我說的寫部落格並不是讓你去抄網上的部落格,一頓CV完了之後就沒事了,寫部落格主要是寫自己的學習程式碼和記錄問題,而不是去抄襲,就算是抄襲同一個問題,你也要加上自己的觀點,談談你對這個問題的理解.而不是抄一頓就以為萬事大吉了,這種想法是萬萬要不得的.

 

可能一開始學部落格會覺得很困難,陳述句組織不行,沒有什麼要寫的,這時候你可以分析一下當時你對這個問題或者Bug的理解,你是怎麼想的,怎麼思考的,怎麼解決的,然後有什麼感悟,這些都是可以寫上去.完全可以用大白話來說這些問題,我敢保證,你這樣寫十篇部落格之後,你就知道你該如何組織你的語言了~

 

有人會問你比比這麼多,那麼寫部落格到底有什麼好處?其實我在以前的部落格中有寫到過,這裡我再總結一下,主要有以下的三點好處.

 

  • 部落格是日常開發的筆記,一個程式猿是不能記太多的程式碼的,這從我們成為程式猿的第一天就知道,所以我們要做的就是知道這個問題如何解決,當我們發現了一個新的問題或者Bug,可能第一次花了我們很長時間才解決這個問題,如果我們不做記錄的話,那麼下一次我們雖然知道思路,但是依然需要做很長時間的修正,可能比第一次提高20%-30%的效率,但是當我們寫部落格做筆記呢?下次遇到這樣的問題我們就會立馬知道我們的部落格中對應的解決方案,然後我們可以快速的找到解決方案,這樣工作效率最少提升50%-60%,這就是小時候我們常說的”好記性不如爛筆頭”的道理~~~~~

  • 如果出去面試,如何看出一個人的能力?這裡有兩個程式猿,水平差不多,一個有著寫部落格習慣,另外一個不寫部落格習慣,我相信很多人會選擇前者,部落格雖然不能概括出你所有的能力,但是最少能從其中窺其一二,所以,堅持寫部落格是很有必要的.

  • 寫部落格還有個好處就是幫助別的程式猿,當你發表了一篇關於某個問題解決方案的部落格,可能更多的程式猿因此而解決問題,這是一個很好的過程,因為我的部落格幫助很多人,雖然這是無償的,但是我依然非常高興.再就是可能有人可能對你的部落格有著不同的理解,他們可能提出一些更好的方案或者解決辦法.透過這樣的方式你可瞭解更多更優的解決方案,助人即助己…

 

 

結語

 

這篇部落格寫了好幾天,算的上是技術雜談吧,如果有任何問題,歡迎隨時在下麵評論,謝謝~

已同步到看一看
贊(0)

分享創造快樂