總結了Effective Objective-C以後,還想讀一本進階的iOS書,絕不猶豫選中了《Objective-C 高級編程》。html
這本書有三個章節,我針對每一章節進行總結並加上適當的擴展分享給你們。能夠從下面這張圖來看一下這三篇的總體結構:ios
注意,這個結構並不和書中的結構一致,而是以書中的結構爲參考,稍做了調整。git
本篇是第一篇:引用計數,簡單說兩句: Objective-C經過 retainCount 的機制來決定對象是否須要釋放。 每次runloop迭代結束後,都會檢查對象的 retainCount,若是retainCount等於0,就說明該對象沒有地方須要繼續使用它,能夠被釋放掉了。不管是手動管理內存,仍是ARC機制,都是經過對retainCount來進行內存管理的。程序員
先看一下手動內存管理:github
我我的以爲,學習一項新的技術以前,須要先了解一下它的核心思想。理解了核心思想以後,對技術點的把握就會更快一些:面試
從上面的思想來看,咱們對對象的操做能夠分爲三種:生成,持有,釋放,再加上廢棄,一共有四種。它們所對應的Objective-C的方法和引用計數的變化是:編程
對象操做 | Objecctive-C方法 | 引用計數的變化 |
---|---|---|
生成並持有對象 | alloc/new/copy/mutableCopy等方法 | +1 |
持有對象 | retain方法 | +1 |
釋放對象 | release方法 | -1 |
廢棄對象 | dealloc方法 | 無 |
用書中的圖來直觀感覺一下這四種操做:數組
下面開始逐一解釋上面的四條思想:多線程
在生成對象時,使用如下面名稱開頭的方法生成對象之後,就會持有該對象:app
舉個🌰:
id obj = [[NSObject alloc] init];//持有新生成的對象
複製代碼
這行代碼事後,指向生成並持有[[NSObject alloc] init]的指針被賦給了obj,也就是說obj這個指針強引用[[NSObject alloc] init]這個對象。
一樣適用於new方法:
id obj = [NSObject new];//持有新生成的對象
複製代碼
注意: 這種將持有對象的指針賦給指針變量的狀況不僅侷限於上面這四種方法名稱,還包括以他們開頭的全部方法名稱:
舉個🌰:
id obj1 = [obj0 allocObject];//符合上述命名規則,生成並持有對象
複製代碼
它的內部實現:
- (id)allocObject
{
id obj = [[NSObject alloc] init];//持有新生成的對象
return obj;
}
複製代碼
反過來,若是不符合上述的命名規則,那麼就不會持有生成的對象, 看一個不符合上述命名規則的返回對象的createObject方法的內部實現🌰:
- (id)createObject
{
id obj = [[NSObject alloc] init];//持有新生成的對象
[obj autorelease];//取得對象,但本身不持有
return obj;
}
複製代碼
經由這個方法返回之後,沒法持有這個返回的對象。由於這裏使用了autorelease。autorelease提供了這樣一個功能:在對象超出其指定的生存範圍時可以自動並正確地釋放(詳細會在後面介紹)。
也就是說,生成一個調用方不持有的對象是能夠經過autorelease來實現的(例如NSMutableArray的array類方法)。
個人我的理解是:經過autorelease方法,使對象的持有權轉移給了自動釋放池。因此實現了:調用方拿到了對象,但這個對象還不被調用方所持有。
由這個不符合命名規則的例子來引出思想二:
咱們如今知道,僅僅經過上面那個不符合命名規則的返回對象實例的方法是沒法持有對象的。可是咱們能夠經過某個操做來持有這個返回的對象:這個方法就是經過retain方法來讓指針變量持有這個新生成的對象:
id obj = [NSMutableArray array];//非本身生成並持有的對象
[obj retain];//持有新生成的對象
複製代碼
注意,這裏[NSMutableArray array]返回的非本身持有的對象正是經過上文介紹過的autorelease方法實現的。因此若是想持有這個對象,須要執行retain方法才能夠。
對象的持有者有義務在再也不須要這個對象的時候主動將這個對象釋放。注意,是有義務,而不是有權利,注意兩個詞的不一樣。
來看一下釋放對象的例子:
id obj = [[NSObject alloc] init];//持有新生成的對象
[obj doSomething];//使用該對象作一些事情
[obj release];//事情作完了,釋放該對象
複製代碼
一樣適用於非本身生成並持有的對象(參考思想二):
id obj = [NSMutableArray array];//非本身生成並持有的對象
[obj retain];//持有新生成的對象
[obj soSomething];//使用該對象作一些事情
[obj release];//事情作完了,釋放該對象
複製代碼
可能遇到的面試題:調用對象的release方法會銷燬對象嗎? 答案是不會:調用對象的release方法只是將對象的引用計數器-1,當對象的引用計數器爲0的時候會調用了對象的dealloc 方法才能進行釋放對象的內存。
在釋放對象的時候,咱們只能釋放已經持有的對象,非本身持有的對象是不能被本身釋放的。這很符合常識:就比如你本身才能從你本身的銀行卡里取錢,取別人的卡里的錢是不對的(除非他的錢歸你管。。。只是隨便舉個例子)。
id obj = [[NSObject alloc] init];//持有新生成的對象
[obj doSomething];//使用該對象
[obj release];//釋放該對象,再也不持有了
[obj release];//釋放已經廢棄了的對象,崩潰
複製代碼
id obj = [NSMutableArray array];//非本身生成並持有的對象
[obj release];//釋放了非本身持有的對象
複製代碼
思考:哪些狀況會使對象失去擁有者呢?
如今知道了引用計數式內存管理的四個思想,咱們再來看一下四個操做引用計數的方法:
某種意義上,GNUstep 和 Foundation 框架的實現是類似的。因此這本書的做者經過GNUstep的源碼來推測了蘋果Cocoa框架的實現。
下面開始針對每個方法,同時用GNUstep和蘋果的實現方式(追蹤程序的執行和做者的猜想)來對比一下各自的實現。
//GNUstep/modules/core/base/Source/NSObject.m alloc:
+ (id) alloc
{
return [self allocWithZone: NSDefaultMallocZone()];
}
+ (id) allocWithZone: (NSZone*)z
{
return NSAllocateObject(self, 0, z);
}
複製代碼
這裏NSAllocateObject方法分配了對象,看一下它的內部實現:
//GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:
struct obj_layout {
NSUInteger retained;
};
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
{
int size = 計算容納對象所需內存大小;
id new = NSZoneMalloc(zone, 1, size);//返回新的實例
memset (new, 0, size);
new = (id)&((obj)new)[1];
}
複製代碼
- NSAllocateObject函數經過NSZoneMalloc函數來分配存放對象所須要的內存空間。
- obj_layout是用來保存引用計數,並將其寫入對象內存頭部。
對象的引用計數能夠經過retainCount方法來取得:
GNUstep/modules/core/base/Source/NSObject.m retainCount:
- (NSUInteger) retainCount
{
return NSExtraRefCount(self) + 1;
}
inline NSUInteger
NSExtraRefCount(id anObject)
{
return ((obj_layout)anObject)[-1].retained;
}
複製代碼
咱們能夠看到,給NSExtraRefCount傳入anObject之後,經過訪問對象內存頭部的.retained變量,來獲取引用計數。
//GNUstep/modules/core/base/Source/NSObject.m retain:
- (id)retain
{
NSIncrementExtraRefCount(self);
return self;
}
inline void NSIncrementExtraRefCount(id anObject)
{
//retained變量超出最大值,拋出異常
if (((obj)anObject)[-1].retained == UINT_MAX - 1){
[NSException raise: NSInternalInconsistencyException
format: @"NSIncrementExtraRefCount() asked to increment too far」];
}
((obj_layout)anObject)[-1].retained++;//retained變量+1
}
複製代碼
//GNUstep/modules/core/base/Source/NSObject.m release
- (void)release
{
//若是當前的引用計數 = 0,調用dealloc函數
if (NSDecrementExtraRefCountWasZero(self))
{
[self dealloc];
}
}
BOOL NSDecrementExtraRefCountWasZero(id anObject)
{
//若是當前的retained值 = 0.則返回yes
if (((obj)anObject)[-1].retained == 0){
return YES;
}
//若是大於0,則-1,並返回NO
((obj)anObject)[-1].retained--;
return NO;
}
複製代碼
//GNUstep/modules/core/base/Source/NSObject.m dealloc
- (void) dealloc
{
NSDeallocateObject (self);
}
inline void NSDeallocateObject(id anObject)
{
obj_layout o = &((obj_layout)anObject)[-1];
free(o);//釋放
}
複製代碼
總結一下上面的幾個方法:
下面看一下蘋果的實現:
經過在NSObject類的alloc類方法上設置斷點,咱們能夠看到執行所調用的函數:
retainCount:
咱們能夠看到他們都調用了一個共同的 __CFdoExternRefOperation 方法。
看一下它的實現:
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得對象的散列表(obj);
int count;
switch (op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
break;
case OPERATION_retain:
count = CFBasicHashAddValue(table, obj);
return obj;
case OPERATION_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}
複製代碼
能夠看出,__CFDoExternRefOperation經過switch語句 針對不一樣的操做來進行具體的方法調用,若是 op 是 OPERATION_retain,就去掉用具體實現 retain 的方法,以此類推。
能夠猜測上層的retainCount,retain,release方法的實現:
- (NSUInteger)retainCount
{
return (NSUInteger)____CFDoExternRefOperation(OPERATION_retainCount,self);
}
- (id)retain
{
return (id)____CFDoExternRefOperation(OPERATION_retain,self);
}
//這裏返回值應該是id,原書這裏應該是錯了
- (id)release
{
return (id)____CFDoExternRefOperation(OPERATION_release,self);
}
複製代碼
咱們觀察一下switch裏面每一個語句裏的執行函數名稱,彷佛和散列表(Hash)有關,這說明蘋果對引用計數的管理應該是經過散列表來執行的。
在這張表裏,key爲內存塊地址,而對應的值爲引用計數。也就是說,它保存了這樣的信息:一些被引用的內存塊各自對應的引用計數。
那麼使用散列表來管理內存有什麼好處呢?
由於計數表保存內存塊地址,咱們就能夠經過這張表來:
當對象超出其做用域時,對象實例的release方法就會被調用,autorelease的具體使用方法以下:
全部調用過autorelease方法的對象,在廢棄NSAutoreleasePool對象時,都將調用release方法(引用計數-1):
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];//至關於obj調用release方法
複製代碼
NSRunLoop在每次循環過程當中,NSAutoreleasePool對象都會被生成或廢棄。 也就是說,若是有大量的autorelease變量,在NSAutoreleasePool對象廢棄以前(一旦監聽到RunLoop即將進入睡眠等待狀態,就釋放NSAutoreleasePool),都不會被銷燬,容易致使內存激增的問題:
for (int i = 0; i < imageArray.count; i++)
{
UIImage *image = imageArray[i];
[image doSomething];
}
複製代碼
所以,咱們有必要在適當的時候再嵌套一個自動釋放池來管理臨時生成的autorelease變量:
for (int i = 0; i < imageArray.count; i++)
{
//臨時pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *image = imageArray[i];
[image doSomething];
[pool drain];
}
複製代碼
可能會出的面試題:何時會建立自動釋放池? 答:運行循環檢測到事件並啓動後,就會建立自動釋放池,並且子線程的 runloop 默認是不工做的,沒法主動建立,必須手動建立。 舉個🌰: 自定義的 NSOperation 類中的 main 方法裏就必須添加自動釋放池。不然在出了做用域之後,自動釋放對象會由於沒有自動釋放池去處理本身而形成內存泄露。
和上文同樣,咱們仍是經過GNUstep和蘋果的實現來分別看一下。
//GNUstep/modules/core/base/Source/NSObject.m autorelease
- (id)autorelease
{
[NSAutoreleasePool addObject:self];
}
複製代碼
若是調用NSObject類的autorelease方法,則該對象就會被追加到正在使用的NSAutoreleasePool對象中的數組裏(做者假想了一個簡化的源代碼):
//GNUstep/modules/core/base/Source/NSAutoreleasePool.m addObject
+ (void)addObject:(id)anObj
{
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool對象
if (pool != nil){
[pool addObject:anObj];
}else{
NSLog(@"NSAutoreleasePool對象不存在");
}
}
- (void)addObject:(id)anObj
{
[pool.array addObject:anObj];
}
複製代碼
也就是說,autorelease實例方法的本質就是調用NSAutoreleasePool對象的addObject類方法,而後這個對象就被追加到正在使用的NSAutoreleasePool對象中的數組裏。
再來看一下NSAutoreleasePool的drain方法:
- (void)drain
{
[self dealloc];
}
- (void)dealloc
{
[self emptyPool];
[array release];
}
- (void)emptyPool
{
for(id obj in array){
[obj release];
}
}
複製代碼
咱們能夠看到,在emptyPool方法裏,確實是對數組裏每個對象進行了release操做。
咱們能夠經過objc4/NSObject.mm來確認蘋果中autorelease的實現:
objc4/NSObject.mm AutoreleasePoolPage
class AutoreleasePoolPage
{
static inline void *push()
{
//生成或者持有 NSAutoreleasePool 類對象
}
static inline void pop(void *token)
{
//廢棄 NSAutoreleasePool 類對象
releaseAll();
}
static inline id autorelease(id obj)
{
//至關於 NSAutoreleasePool 類的 addObject 類方法
AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 實例;
autoreleaesPoolPage->add(obj)
}
id *add(id obj)
{
//將對象追加到內部數組中
}
void releaseAll()
{
//調用內部數組中對象的 release 方法
}
};
//壓棧
void *objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}
//出棧
void objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
AutoreleasePoolPage::pop(ctxt);
}
複製代碼
來看一下外部的調用:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同於 objc_autoreleasePoolPush
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同於 objc_autorelease(obj)
[NSAutoreleasePool showPools];
// 查看 NSAutoreleasePool 情況
[pool drain];
// 等同於 objc_autoreleasePoolPop(pool)
複製代碼
看函數名就能夠知道,對autorelease分別執行push、pop操做。銷燬對象時執行release操做。
可能出現的面試題:蘋果是如何實現autoreleasepool的? autoreleasepool以一個隊列數組的形式實現,主要經過下列三個函數完成. • objc_autoreleasepoolPush(壓入) • objc_autoreleasepoolPop(彈出) • objc_autorelease(釋放內部)
上面學習了非ARC機制下的手動管理內存思想,針對引用計數的操做和自動釋放池的相關內容。如今學習一下在ARC機制下的相關知識。
ARC和非ARC機制下的內存管理思想是一致的:
在ARC機制下,編譯器就能夠自動進行內存管理,減小了開發的工做量。但咱們有時仍須要四種全部權修飾符來配合ARC來進行內存管理
可是,在ARC機制下咱們有的時候須要追加全部權聲明(如下內容摘自官方文檔):
下面分別講解一下這幾個修飾符:
__strong修飾符 是id類型和對象類型默認的全部權修飾符:
id obj = [NSObject alloc] init];
複製代碼
等同於:
id __strong obj = [NSObject alloc] init];
複製代碼
看一下內存管理的過程:
{
id __strong obj = [NSObject alloc] init];//obj持有對象
}
//obj超出其做用域,強引用失效
複製代碼
__strong修飾符表示對對象的強引用。持有強引用的變量在超出其做用域時被廢棄。
在__strong修飾符修飾的變量之間相互賦值的狀況:
id __strong obj0 = [[NSObject alloc] init];//obj0 持有對象A
id __strong obj1 = [[NSObject alloc] init];//obj1 持有對象B
id __strong obj2 = nil;//ojb2不持有任何對象
obj0 = obj1;//obj0強引用對象B;而對象A再也不被ojb0引用,被廢棄
obj2 = obj0;//obj2強引用對象B(如今obj0,ojb1,obj2都強引用對象B)
obj1 = nil;//obj1再也不強引用對象B
obj0 = nil;//obj0再也不強引用對象B
obj2 = nil;//obj2再也不強引用對象B,再也不有任何強引用引用對象B,對象B被廢棄
複製代碼
並且,__strong可使一個變量初始化爲nil:id __strong obj0; 一樣適用於:id __weak obj1; id __autoreleasing obj2;
作個總結:被__strong修飾後,至關於強引用某個對象。對象一旦有一個強引用引用本身,引用計數就會+1,就不會被系統廢棄。而這個對象若是再也不被強引用的話,就會被系統廢棄。
生成並持有對象:
{
id __strong obj = [NSObject alloc] init];//obj持有對象
}
複製代碼
編譯器的模擬代碼:
id obj = objc_mesgSend(NSObject, @selector(alloc));
objc_msgSend(obj,@selector(init));
objc_release(obj);//超出做用域,釋放對象
複製代碼
再看一下使用命名規則之外的構造方法:
{
id __strong obj = [NSMutableArray array];
}
複製代碼
編譯器的模擬代碼:
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
複製代碼
objc_retainAutoreleasedReturnValue的做用:持有對象,將對象註冊到autoreleasepool並返回。
一樣也有objc_autoreleaseReturnValue,來看一下它的使用:
+ (id)array
{
return [[NSMutableArray alloc] init];
}
複製代碼
編譯器的模擬代碼:
+ (id)array
{
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj,, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
複製代碼
objc_autoreleaseReturnValue:返回註冊到autoreleasepool的對象。
__weak修飾符大多解決的是循環引用的問題:若是兩個對象都互相強引用對方,同時都失去了外部對本身的引用,那麼就會造成「孤島」,這個孤島將永遠沒法被釋放,舉個🌰:
@interface Test:NSObject
{
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (id)init
{
self = [super init];
return self;
}
- (void)setObject:(id __strong)obj
{
obj_ = obj;
}
@end
複製代碼
{
id test0 = [[Test alloc] init];//test0強引用對象A
id test1 = [[Test alloc] init];//test1強引用對象B
[test0 setObject:test1];//test0強引用對象B
[test1 setObject:test0];//test1強引用對象A
}
複製代碼
由於生成對象(第一,第二行)和set方法(第三,第四行)都是強引用,因此會形成兩個對象互相強引用對方的狀況:
因此,咱們須要打破其中一種強引用:
@interface Test:NSObject
{
id __weak obj_;//由__strong變成了__weak
}
- (void)setObject:(id __strong)obj;
@end
複製代碼
這樣一來,兩者就只是弱引用對方了:
{
id __weak obj1 = obj;
}
複製代碼
編譯器的模擬代碼:
id obj1;
objc_initWeak(&obj1,obj);//初始化附有__weak的變量
id tmp = objc_loadWeakRetained(&obj1);//取出附有__weak修飾符變量所引用的對象並retain
objc_autorelease(tmp);//將對象註冊到autoreleasepool中
objc_destroyWeak(&obj1);//釋放附有__weak的變量
複製代碼
這確認了__weak的一個功能:使用附有__weak修飾符的變量,便是使用註冊到autoreleasepool中的對象。
這裏須要着重講解一下objc_initWeak方法和objc_destroyWeak方法:
注意:由於同一個對象能夠賦值給多個附有__weak的變量中,因此對於同一個鍵值,能夠註冊多個變量的地址。
當一個對象再也不被任何人持有,則須要釋放它,過程爲:
ARC下,能夠用@autoreleasepool來替代NSAutoreleasePool類對象,用__autoreleasing修飾符修飾變量來替代ARC無效時調用對象的autorelease方法(對象被註冊到autoreleasepool)。
說到__autoreleasing修飾符,就不得不提__weak:
id __weak obj1 = obj0;
NSLog(@"class = %@",[obj1 class]);
複製代碼
等同於:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@",[tmp class]);//實際訪問的是註冊到自動個釋放池的對象
複製代碼
注意一下兩段等效的代碼裏,NSLog語句裏面訪問的對象是不同的,它說明:在訪問__weak修飾符的變量(obj1)時必須訪問註冊到autoreleasepool的對象(tmp)。爲何呢?
由於__weak修飾符只持有對象的弱引用,也就是說在未來訪問這個對象的時候,沒法保證它是否尚未被廢棄。所以,若是把這個對象註冊到autoreleasepool中,那麼在@autoreleasepool塊結束以前都能確保該對象存在。
將對象賦值給附有__autoreleasing修飾符的變量等同於ARC無效時調用對象的autorelease方法。
@autoreleasepool{
id __autoreleasing obj = [[NSObject alloc] init];
}
複製代碼
編譯器的模擬代碼:
id pool = objc_autoreleasePoolPush();//pool入棧
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);//pool出棧
複製代碼
在這裏咱們能夠看到pool入棧,執行autorelease,出棧的三個方法。
咱們知道了在ARC機制下編譯器會幫助咱們管理內存,可是在編譯期,咱們仍是要遵照一些規則,做者爲咱們列出瞭如下的規則:
在ARC機制下使用retain/release/retainCount/autorelease方法,會致使編譯器報錯。
在ARC機制下使用NSAllocateObject/NSDeallocateObject方法,會致使編譯器報錯。
對象的生成/持有的方法必須遵循如下命名規則:
前四種方法已經介紹完。而關於init方法的要求則更爲嚴格:
對象被廢棄時,不管ARC是否有效,系統都會調用對象的dealloc方法。
咱們只能在dealloc方法裏寫一些對象被廢棄時須要進行的操做(例如移除已經註冊的觀察者對象)可是不能手動調用dealloc方法。
注意在ARC無效的時候,還須要調用[super dealloc]:
- (void)dealloc
{
//該對象的處理
[super dealloc];
}
複製代碼
ARC下須使用使用@autorelease塊代替NSAutoreleasePool。
NSZone已經在目前的運行時系統(__OBC2__被設定的環境)被忽略了。
C語言的結構體若是存在Objective-C對象型變量,便會引發錯誤,由於C語言在規約上沒有方法來管理結構體成員的生存週期 。
非ARC下,這兩個類型是能夠直接賦值的
id obj = [NSObject alloc] init];
void *p = obj;
id o = p;
複製代碼
可是在ARC下就會引發編譯錯誤。爲了不錯誤,咱們須要經過__bridege來轉換。
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//顯式轉換
id o = (__bridge id)p;//顯式轉換
複製代碼
來看一下屬性的聲明與全部權修飾符的關係
屬性關鍵字 | 全部權 修飾符 |
---|---|
assign | __unsafe_unretained |
copy | __strong |
retain | __strong |
strong | __strong |
__unsafe_unretained | __unsafe_unretained |
weak | __weak |
說一下__unsafe_unretained: __unsafe_unretained表示存取方法會直接爲實例變量賦值。
這裏的「unsafe」是相對於weak而言的。咱們知道weak指向的對象被銷燬時,指針會自動設置爲nil。而__unsafe_unretained卻不會,而是成爲空指針。須要注意的是:當處理非對象屬性的時候就不會出現空指針的問題。
這樣第一章就介紹完了,第二篇會在下週一發布^^
本文已經同步到我的博客:傳送門
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。
由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。
並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~
掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~