Runtime那些事

来源:https://www.cnblogs.com/zhanggui/archive/2018/01/08/8243316.html
-Advertisement-
Play Games

Runtime 前言 從字面意思看,就是運行時。但是這個運行時究竟什麼意思?可以把它理解成:不是在編譯期也不是在鏈接期,而是在運行時。那究竟在運行期間做了什麼呢?按照蘋果官方的說法,就是把一些決策(方法的調用,類的添加等)推遲,推遲到運行期間。只要有可能,程式就可以動態的完成任務,而不是我們在編譯期 ...


Runtime

前言

從字面意思看,就是運行時。但是這個運行時究竟什麼意思?可以把它理解成:不是在編譯期也不是在鏈接期,而是在運行時。那究竟在運行期間做了什麼呢?按照蘋果官方的說法,就是把一些決策(方法的調用,類的添加等)推遲,推遲到運行期間。只要有可能,程式就可以動態的完成任務,而不是我們在編譯期已經決定它要完成什麼任務。這就意味了OC不僅僅需要編譯器,還需要一個運行時的系統來支撐。

目錄

接下來就對Runtime做一個系統的介紹,主要內容包括:

  1. 簡介
  2. 涉及到的數據結構
  3. runtime.h解析
  4. 如何可以觸及到RunTime?
  5. 消息
  6. 動態消息解析
  7. 消息轉發
  8. Runtime的使用場景

1.簡介

根據前言,你已經瞭解了Runtime大概是個什麼鬼,在OC發展歷程中,它主要有兩個版本:Legacy和Modern。Legacy版本採用的是OC1.0版本;Modern版本採用的OC2.0版本,而且相比Legacy也添加了一些新特性。最明顯的區別在於:

  • 在legacy版本,如果你改變了類的佈局,那麼你必須重新編譯繼承自它的類。
  • 在modern版本,如果你改變了類的佈局,你不必重新編輯繼承自它的類。
平臺

iPhone的應用程式以及OS X v10.5版本的64位機器使用的是modern版本的runtime。
其他(OS X桌面應用32位程式)使用的是legacy版本的runtime。

2.涉及到的數據結構

這裡主要介紹一下在runtime.h裡面涉及到的一些數據結構。

Ivar

Ivar從字面意思來講,它就是代表的實例變數,它也是一個結構體指針,包含了變數的名稱、類型、偏移量以及所占空間。

SEL

選擇器,每個方法都有自己的選擇器,其實就是方法的名字,但是不僅僅是方法的名字,在objc.h中,我們可以看到它的定義:

/// An opaque type that represents a method selector.一個不透明類型,用來代表一個方法選擇器
typedef struct objc_selector *SEL;

由定義可知它是一個objc_selector的結構體指針,尷尬的是在runtime源碼中並沒有找到該結構體。猜想它內部應該就是一個char 的字元串。
你可以使用:

 NSLog(@"%s",@selector(description));  //%s用來輸出一個字元串

列印出來description。
在這裡你可以把它理解成一個選擇器,可以標識某個方法。

IMP

它是一個函數指針,指向方法的實現,在objc.h裡面它的定義是這樣的:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
id

id是一個我們經常使用的類型,可用於作為類型轉換的中介者。它類似於Java裡面的Object,可以轉換為任何的數據類型。它在objc.h裡面是這樣定義的:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

它其實是一個objc _ object的結構體指針,而在後面將要提到的Class其實是個objc _ class的指針,而objc _ class是繼承自objc _o bject的,因此可以相互轉換,這也是為什麼id可以轉換為其他任何的數據類型的原因。

Method

方法,它其實是一個objc_method的結構體指針,其定義如下:

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}  

這個就比較好理解了,該結構體包含了方法的名稱(SEL),方法的類型以及方法的IMP。

Class

它是一個objc_class的結構體指針,在runtime.h中的定義如下:

/// An opaque type that represents an Objective-C class.一個不透明類型,代表OC的類
typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

該結構體中各部分介紹如下:

  • isa:是一個Class類型的指針,每個對象的實例都有isa指針,他指向對象的類。而Class裡面也有個isa指針,它指向meteClass(元類),元類保存了類方法的列表。
  • name:對象的名字
  • version:類的版本號,必須是0
  • info:供運行期間使用的位標識
  • instance_size:該類的實例大小
  • ivars:成員變數數組,包含了該類包含的成員變數
  • methodLists:包含方法的數組列表,也是一個結構體,該結構體裡面還包含了一個obsolete的指針,表示廢棄的方法的列表
  • cache:緩存。這個比較複雜,在後面會提到,這裡先忽略。
  • protocols:協議列表,也是一個數組

而在objc-runtime-new.h中,你會發現這樣的定義(在runtime中並沒有完全暴露objc_class的實現):

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    //其他的省略

其實objc _ class繼承自objc _ object。所以這也說明瞭為什麼id能夠轉換為其他的類型。

3.runtime.h解析

我們先看一下在usr/include/objc/runtime.h,這個是任何一個工程都可以直接找到的,它是SDK的一部分。主要定義了以下內容:

  1. 定義了一些類型,例如Method/Ivar/Category等,還有一些結構體。
  2. 函數。函數裡面有分了幾大類:
    • 關於對象實例的方法,例如object _ getClass、object _ setClass以及object _ getIvar等。這些函數大多以object開頭。 用來獲取屬性或者對對象進行操作。
    • 獲得類定義的方法,例如objc _ getClass/objc _ getMetaClass等,這些方法更多的是獲取Class或者在Class級別上進行操作。 多以objc開頭
    • 和類相關的方法。例如class _ getName/class _ isMetaClass等,這些更多的是獲取Class的一些屬性。比如該類的屬性列表、方法列表、協議列表等。傳參大多為Class。 多以class開頭
    • 實例化類的一些方法。例如class _ createInstance方法,就是相當於平時的alloc init。
    • 添加類的方法。例如你可以使用這些方法冬天的註冊一個類。使用objc _ allocateClassPair創建一個新類,使用 objc _ registerClassPair對類進行註冊
    • 等等。。。
  3. 另外就是一些廢棄的方法和類型。

4. 如何可以觸及到RunTime?

有三種不同的方式可以讓OC編程和runtime系統交互。

OC源代碼

大多數情況下,我們寫的OC代碼,其實它底層的實現就是runtime。runtime系統在背後自動幫我們處理了操作。例如我們編譯一個類,編譯器器會創建一個結構體,然後這個結構體會從類中捕獲信息,包括方法、屬性、Protocol等。

NSObject的一些方法

在Foundation框架裡面有個NSObject.h,在usr/include/objc裡面也有一個NSObject.h。而我們平時用到的類的基類是/usr/include/objc裡面的這個NSObject.h,Foundation裡面的NSObject.h只是NSObject的一個Category。所以這裡我們更關註一下/usr/include/objc裡面的NSObject.h。
由於大多數對象都是NSObject的子類,所以在NSObject.h裡面定義的方法都可以使用。
在這些方法裡面,有一些方法能夠查詢runtime系統的信息,例如:

- (BOOL)isKindOfClass:(Class)aClass;   //用來檢測一個對象是否是某各類的實例對象,aClass也有可能是父類,同樣可以檢測出來。
- (BOOL)isMemberOfClass:(Class)aClass;   //而該方法只能檢測一個對象是否是某各類的實例對象。但如果aClass不能為該類的父類,如果是父類則該方法返回NO

- (BOOL)respondsToSelector:(SEL)aSelector;

- (BOOL)conformsToProtocol:(Protocol *)aProtocol;

- (IMP)methodForSelector:(SEL)aSelector;

這裡用代碼對isKindOfClass和isMemberOfClass做個簡單介紹:

//stu是Student的實例對象,Student的父類為Person,Person的父類為NSObject。
[stu isKindOfClass:[Student class]];    //YES
[stu isKindOfClass:[Person class]];    //YES
[stu isKindOfClass:[NSObject class]];   //YES

[stu isMemberOfClass:[Student class]];    //YES
[stu isMemberOfClass:[Person class]];    //NO
[stu isMemberOfClass:[NSObject class]];   //NO

我們可以在objc源代碼中的NSObject.mm中看到相應的實現:

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

從具體實現可知,為什麼isKindOfClass能夠檢測出superclass。另外,在NSObject.h中,並沒有看到兩個方法的類方法聲明,但是在實現裡面卻包含了類方法的實現。這裡有個疑問:為什麼沒有對外聲明的兩個類方法依然可以在外部調用呢?(比如我可以直接使用[Student isMemberOfClass:[NSObject class]])
這裡還用到了class方法,這個方法聲明如下:

+ (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");

- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'type(of: anObject)' instead");

+ (Class)class {   //返回當前的self
    return self;
}

- (Class)class {
    return object_getClass(self);
}

這裡重要的是理解self究竟代表著什麼:

  1. 當self為實例對象的時候,[self class] 和 object_getClass(self)是等價的。object_getClass([self class])得到的是元類。
  2. 當self為類對象的時候,[self class]返回的是自身,還是self。object_getClass(self) 與object_getClass([self class])等價。拿到的是元類。
Runtime函數

runtime系統其實就是一個動態共用的Library,它是由在/usr/include/objc目錄的公共介面中的函數和數據結構組成。
runtime

5. 消息

在Objective-C中,消息直到運行時才將其與消息的實現綁定,編譯器會將

[receiver message];

轉換成

objc_msgSend(receiver,selector);   //1

objc_msgSend(receiver,selector,arg1,arg2,...);  //2

如果包含參數,那麼就會執行2方法。其實除了該方法,還有以下幾個方法:

objc_msgSend_stret    
objc_msgSendSuper
objc_msgSendSuper_stret

當想一個對象的父類發送message時,會使用

objc_msgSendSuper

如果方法的返回值是一個結構體,那麼就會使用

objc_msgSend_stret
objc_msgSendSuper_stret

這裡我們可以打開objc源碼,然後你會發現裡面有多個.s文件:
objc_msg_diff
這裡之所以有objc-msg-類的不同文件,我猜想應該是對不同的CPU指令集(指令不一樣)做了分別處理。因為這些.s文件名稱中包含的是不同的arm指令集。而且打開.s文件你會發現裡面的實現是彙編語言,所以蘋果為了效率還是蠻拼的,直接用彙編語言實現。
其中就能找到objc _ msgSend的實現(objc-msg-i386.s中):
objc_msgSend
雖然對彙編瞭解不是太多,但是這個文件中的註釋很詳細,從註釋可以看出objc_msgSend方法的執行過程:

  1. 先載入receiver和selector到寄存器,然後判斷receiver是否為空,如果為空,則函數執行結束;
  2. 如果receiver不為空,開始搜索緩存,查看方法緩存列表裡面是否有改selector,如果有則執行;
  3. 如果沒有緩存,則搜索方法列表,如果在方法列表中找到,則跳轉到具體的imp實現。沒有則執行結束。
使用了隱藏參數

在發送一個消息的時候,會被編譯成objc_msgSend,此時該消息的參數將會傳入objc_msgSend方法裡面。除此之外,還會包含兩個隱藏的參數:

  1. receiver
  2. method的selector

這兩個參數在上面也有提到。其中的receiver就是消息的發送方,而selector就是選擇器,也可以直接用 _ cmd來指代( _ cmd用來代表當前所在方法的SEL)。之所以隱蔽是因為在方法聲明中並沒有被明確聲明,在源代碼中我們仍然可以引用它們。

獲取方法地址

我們每次發送消息都會走objc_msgSend()方法,那麼有沒有辦法避開消息綁定直接獲取方法的地址並調用方法呢?答案當然是有的。我們上面簡單介紹了IMP,其實我們可以使用NSObject的

- (IMP)methodForSelector:(SEL)aSelector;

方法,通過該方法獲得IMP,然後調用該方法。但是避開消息綁定而直接調用的使用並不常見,但是如果你要多次迴圈調用的話,直接獲取方法地址並調用不失為一個省時操作。看下麵的代碼:

 void (*setter)(id,SEL,BOOL);
    setter = (void(*)(id,SEL,BOOL))[stu2 methodForSelector:@selector(learning)];
    NSDate *startDate = [NSDate date];
    for (int i = 0;i<100000;i++) {
        setter(stu2,@selector(learning),YES);
    }
    double deltaTime = [[NSDate date] timeIntervalSinceDate:startDate];
    NSLog(@"----%f",deltaTime);
    
    NSDate *startDate1 = [NSDate date];
    for (int i = 0;i<100000;i++) {
        [stu2 learning];
    }
    double deltaTime1 = [[NSDate date] timeIntervalSinceDate:startDate1];
    NSLog(@"----%f",deltaTime1);

你可以自行跑一下,看一下時間差異。你會發現:獲取方法地址直接調用更省時間,但請註意使用場景。

6. 動態消息解析

這裡介紹一下如果動態地提供方法的實現。

動態方法解析

在開發過程中,你可能想動態地提供一個方法的實現。比如我們對一個對象聲明瞭一個屬性,然後我們使用了 @dynamic 標識符:

@dynamic propertyName;

該標識符的目的就是告訴編譯器:和這個屬性相關的getter和setter方法會動態地提供(當然你也可以直接手動在代碼裡面實現)。這個時候你就會用到NSObject.h裡面的兩個方法

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

來提供方法的實現。
其實OC方法就是一個簡單的C函數,它至少包含了兩個參數self和 _ cmd,你可以自己聲明一個方法:

void dynamicMethodIMP(id self, SEL _cmd) {
    //這裡是方法的具體實現
}

此時我們可以在聲明屬性的類中實現上面提到的兩個方法(一個是解析類方法,一個是解析實例方法),例如我在Person裡面這樣寫:

@dynamic address;   //也就意味著我們需要手動/動態實現該屬性的getter和setter方法。

你會發現當我們運行下麵的代碼時,程式會crash:

   Person *zhangsan = [[Person alloc] init];
    zhangsan.address = @"he nan xinxiang ";
    
    NSLog(@"%@",zhangsan.address);
    
//    crash reason
// -[Person setAddress:]: unrecognized selector sent to instance 0x1d4449630

這裡簡單的做一個動態方法解析:

void setter(id self,SEL _cmd) {
    NSLog(@"set address");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selStr = NSStringFromSelector(sel);
    if ([selStr hasPrefix:@"set"]) {
        class_addMethod([self class], sel, (IMP)setter, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

所以我們需要自己去實現setAddress: 方法。(這裡判斷用hasPrefix不太準確,開發者可以自行根據需求調整)。轉發消息(下麵會講到)和動態解析是正交的。也就是說一個class有機會再消息轉發機制前去動態解析此方法,也可以將動態解析方法返回NO,然後將操作轉發給消息轉發。

動態載入

OC編程也允許我們在程式運行的時候動態去創建和鏈接一個類或者分類。這些創建的類或者分類將會和運行app前創建的類一樣,沒有差別。
動態載入在開發的過程中可以做好多事情,例如系統設置中的不同模塊就是動態載入的。
在Cocoa環境中,最經典的就是Xcode,它可以安裝不同的插件,這個也是動態載入的方式實現的。

7. 消息轉發

發送一個消息給對象,如果對象不能處理,那麼就會產生錯誤。然而,在產生錯誤之前,runtime 系統會給對象第二次機會去處理該消息。這裡詳細已經在深入淺出理解消息的傳遞和轉發文章中做了介紹,這裡就不再介紹了。

8. Runtime的使用場景

Runtime的使用幾乎無處不在,OC本身就是一門運行時語言,Class的生成、方法的調用等等,都是Runtime。另外,我們可以用Runtime做一些其他的事情。

字典轉換Model

平時我們從服務端拿到的數據是json字元串,我們可以將其轉換成成NSDictionary,然後通過runtime中的一些方法做一個轉換:
先拿到model的所有屬性或者成員變數,然後將其和字典中的key做映射,然後通過KVC對屬性賦值即可。更多可參見class_copyIvarList方法獲取實例變數問題引發的思考中的例子。

熱更新(JSPatch的實現)

JSPatch能做到JS調用和改寫OC方法的根本原因就是OC是動態語言,OC上的所有方法的調用/類的生成都通過OC Runtime在運行時進行,我們可以根據名稱/方法名反射得到相應的類和方法。例如

Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

也正是鑒於此,才實現了熱更新。

給Category添加屬性

我們可以使用runtime在Category中給類添加屬性,這個主要使用了兩個runtime鐘的方法:

OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy);

OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);

具體使用可參見:給分類(Category)添加屬性

Method Swizzling

它是改變一個已存在的selector的實現的技術,比如你想將viewDidload方法替換為我們自定義的方法,給系統的方法添加一些需要的功能,來實現某些需求。比如你想跟蹤每個ViewController展示的次數,你可以使用該技術重寫ViewDidAppear方法,然後做一些自己的處理。可以參見Method Swizzling裡面的講解。

總結

Objective-c本身就是一門冬天語言,所以瞭解runtime有助於我們更加深入地瞭解其內部的實現原理。也會把一些看似很難的問題通過runtime很快解決。

參考鏈接:

1.Objective-C Runtime Programming Guide
2.Objective-C Runtime
3.objc4
4.深入淺出理解消息的傳遞和轉發
5.class_copyIvarList方法獲取實例變數問題引發的思考
6.JSPatch 實現原理詳解
7.給分類(Category)添加屬性
8.Method Swizzling

轉載請註明來源:http://www.cnblogs.com/zhanggui/p/8243316.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 應儘量避免在where中使用!=或<>操作符。否則會進行全表查詢 對於查詢,避免全盤掃描,考慮在where或order by涉及到的列上建立索引 避免在where中進行null值判斷,否則會進行全表掃描 查詢時,避免*查詢全部,按要求指定的查 In和not in也要慎用,否則會導致全表掃描 不要寫一 ...
  • 針對mysql的連接參數和狀態值,本文做些介紹和對比 一、MYSQL連接參數變數 1、常用連接數限制參數 show variables like '%connect%'; 2、超時參數 mysql -e "show variables like '%timeout%'" 二、MySQL連接狀態變數 ...
  • 需求說明: 1、在項目中需要計算某一個環節的持續時間及該環節進行的次數。 2、要求持續時間以分鐘進行顯示,並統計進行次數。 解決方式: 通過計算某一環節的開始時間與結束時間的秒數差值進行判斷。 代碼部分: 說明:資料庫使用的是Mysql,持久層框架使用的是Mybatis。 代碼如下: FLOOR(( ...
  • 在windows安裝好了windows,首先記得要把mongodb bin目錄路徑放在 系統環境變數的path中,確定之後即配置好了mongo的環境變數,在dos命令框中輸入mongo會出現如下 版本信息: 想要啟動本地mongo 服務,直接在命令框中輸入 mongod.exe 即可啟動 mongo ...
  • dict是一種用於保存鍵值對的抽象數據結構,在redis中使用非常廣泛,比如資料庫、哈希結構的底層。 ...
  • 1,被測試的應用程式必須是Developer簽名的應用程式或者是運行在模擬器裡面的應用程式。 2,在被測試的應用程式開發的過程中需要處理UI控制項的可訪問性。使用IB的開發工程師需要在XIB中加入一個Accessibility屬性設置。該屬性直接控制在執行UI Automation時UI控制項的可操作性 ...
  • Twitter曾經舉行了自己四年以來的第一場開發者大會。而這場名為“Flight”的大會,也是以後它的年度慣例。 這次大會的主題也完全圍繞開發者進行。大會的焦點是一個名叫Fabric的新SDK,裡面包括三個開發者工具包:面向Twitter本身的 Twitter Kit、面向Twitter廣告網路的M ...
  • 在處理完框架記憶體泄漏的問題後,發現業務代碼有一個地方的記憶體沒釋放,原因很也簡單:block和self互相強引用了,接下來。。。。。。 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...