簡單VC記憶體檢測

愛玩愛鬧發表於2019-03-04

iOS tip

  • class_copyIvarList: 只是返回本類的例項變數,父類的例項變數不會返回。

  • NSArrayenumeration block中,return並不能阻止其迴圈,只有*stop = YES可以保證退出迴圈遍歷

NSArray *array = @[@"1", @"2"];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"%@", obj);
        return;
}];
NSLog(@"hahahha");
///// 依然會輸出每個元素,在列印hahaha
複製程式碼

需要檢測的項

1、ivar list

現在只檢測OC物件

2、timer (NSTimer, Dispatch_Source, displayLink)

應該查詢

利用clang來進行前端編譯,看是否可以知道一些端倪

1、根據clang --help 命令來檢視clang的用法,但是命令太多我們可以使用
clang --help | grep Object 來縮小我們檢視的範圍,這樣就可以一目瞭然的檢視應該需要哪個命令對原始檔進行轉變

2、-rewrite-objc           Rewrite Objective-C source to C++
根據查詢到線索我們開始對main.m檔案進行編譯

clang -rewrite-objc main.m

很遺憾的是報錯了:

warning: include path for stdlibc++ headers not found; pass `-std=libc++` on the
      command line to use the libc++ standard library instead
      [-Wstdlibcxx-not-found]
main.m:9:9: fatal error: `UIKit/UIKit.h` file not found
#import <UIKit/UIKit.h>

解決:
經過網上查詢使用一下命令:clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

-x 接下來輸入的檔案是什麼型別的語言
-isysroot  指定系統路徑

如果覺命令長可以使用 別名 (alias),,在~/.bash_profile檔案中宣告:

alias rewriteoc=clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

source ~/.bash_profile


很遺憾的是當我們執行的時候並沒有我們想要的檔案,在從網上查詢

//指定真機的sdk
xcrun -sdk iphoneos clang -rewrite-objc main.m

指定模擬器sdk
xcrun -sdk iphonesimulator clang -rewrite-objc main.m

指定模擬器具體的sdk
xcrun -sdk iphonesimulator10.3 clang -rewrite-objc main.m

複製程式碼

xcrun命令講解

clang命令之後生成的檔案

檢視CF原始碼

當一個timer新增到runloop中的時候,timer就會被runloop強引用, 而timertarget會被timer所強引用,那麼現在問題我們怎麼樣找見這個強引用在什麼地方。我們通過檢視CF原始碼,很大的機率是存放在 info中,但是info是一個void *指標。所以我們應該檢視一下這個info存放了什麼?

CFRunLoopTimerRef timerRef = (__bridge CFRunLoopTimerRef)timer;
        CFRunLoopTimerContext cxt;
        CFRunLoopTimerGetContext(timerRef, &cxt);
        void * info = cxt.info;
        //列印的是每個位元組存放的數字
        //我們會發現從第二個位元組開始,後8個位元組是target的地址
        for (int i = 1;i < 100; i ++) {
            char a = *((char *)(info + i -1));
            printf("%x ", a);
            if (i != 0 && i % 8 == 0) {
                printf("
");
            }
        }

複製程式碼

根據上面程式碼我們把info轉換成一個struct

typedef struct mc_info {
    char a;
    void * objc; //表示target
}mc_info;

複製程式碼

3、block

  • 簡單介紹block的在程式碼中形式:
    void(^block)(void) = ^(void){NSLog(@"%d", a);};其實變數block是一個物件指標,我們可以通過objc_getClass()或者[block class]的方法來檢視他是否一個物件。
// 這段程式碼沒有crash,並且返回了值。這就表明了block是一個物件指標。而且系統是吧block包裝成了一個物件
void(^block)(void) = ^(void){NSLog(@"%d", a);};
Class cls = [block class];
複製程式碼
  • 請看接下來一個問題:那麼我們怎麼判斷一個指標是一個BLOCK物件指標呢?那可以很自然的想到我們在判斷一個物件是不是NSObject的方法:isKindOfClass:。這方法的需要一個引數Class,那麼我們怎麼樣找BLOCK對應的Class,那我們可以不可以就用上面的[block class]來作為引數呢?答應是否定的。為什麼呢?因為OC中有類簇的概念。我們也知道block有三種型別malloc blockglobal block stack block。這三個類是兄弟關係,他們是繼承與一個根類,那麼我們現在就是要找到這個根類來作為isKindOfClass的引數。
void(^block)(void) = ^(void){NSLog(@"%d", a);};
Class cls = [block class];
while( cls != NULL && class_getSuperclass(cls) != [NSObject class]) {
    cls = class_getSuperclass(cls);
}
NSLog(@"%@", cls);
複製程式碼

如果說你是一個指標物件,那麼最後的super class一定是[NSObject class],你可以通過runtime的那張表可以看出來。所以繼承與NSOject的那個類一定是BLOCK的根類。

rutime
  • 如何查詢block是否引用我們檢測的物件?

檢視circle程式碼,太難了。 學習的點:

.cxx_destruct這是編譯器幫忙加上的程式碼,這個程式碼就是幫助釋放例項變數的。例如我們MRC的環境下

- (void)dealloc {
    release(_name);
}

複製程式碼

.cxx_destruct就相當於執行上面的程式碼。stack overflow, stack overflow網站中有點錯誤,就是利用clang生成MRC程式碼引數錯誤,再次糾正一下clang -fno-objc-arc main.m -o test -framework foundation

class—dump用來查詢反編譯檔案
下載地址,可以把class-dump檔案放在~/bin檔案下,並且把~/bin檔案放在$PATH環境變數裡面。

vim ~/.bash_profile
#貼上下面語句到bash_profile文中
export PATH=`~/bin:$PATH`
複製程式碼

通過一個類檔案作為實驗

發現:當類沒有strong修飾的例項變數的時候,這個函式是沒有的。

lldb 的命令,通過watchpoint 來查詢例項變數是否會被修改

watchpoint set variable self->_b // self->_b,代表某個例項變數,我們可以不通過KVO進行觀察了
複製程式碼

當block結構中含有含有指標例項變數(__block修飾的),非基本型別(int , bool)。flag 是 570425344, 而其他是0,當非0的時,block中desc結構中是含有copydispose函式的。

我們拿到了一個物件,在OC中物件都是用指標來表示的。明白一個概念當指標進行加減法操作的時候鎖增加或者減少的位元組數是被加數(被減數)乘以 當前指標所指向的位元組數,在Circle這個程式中,首先把一個存放指標的陣列偽裝成一個物件,為什麼是存放指標的陣列呢?因為我們的物件都是指標應用的,而且也有記憶體對齊的原則。這個陣列進行釋放,如果其中某個元素被釋放了,那麼這個元素所在的idx,就是這個物件中強引用例項變數的相對物件地址的偏移量。之後我們在把這個物件偽裝成陣列,用idx進行指標偏移。如果對一個指向物件的指標進行偏移呢?因為開始我們用的是一個存放指標的陣列,那麼我們在進行偏移的時候,也要把物件指標轉成成一個存放指標的陣列,也就是void **p,讓後我們用p+ idx可以獲取到強引用的例項變數指標,之後我們進行取值*(p + idx),這樣我們就可以得到例項變數了。

如何向一個物件的例項變數賦值,通知指標的方式:


@interface Person : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int a;

@end



 {
        Person *person = [[Person alloc] init];
        
        unsigned int count;
        Ivar *ivar = class_copyIvarList(Person.class, &count);
        for (int i = 0 ; i < count; i ++) {
            Ivar currentIvar = ivar[i];
            // 獲取改例項變數相對於物件指標的偏移量,這個偏移量表示的幾個位元組
            ptrdiff_t offset = ivar_getOffset(currentIvar);
            NSLog(@"ivar name - %@, offset-- %d", [NSString stringWithUTF8String:ivar_getName(currentIvar)], offset);
            // ivar name - _a, offset-- 8 偏移8個位元組
               ivar name - _name, offset-- 16 偏移16個位元組
        }
        
        id person_void = (id)person;
        char *a = (__bridge void *)person_void + 8;
        *a = 10;
        
        // 指明不進行強引用,不進行記憶體管理
        __unsafe_unretained id * b = (__unsafe_unretained id *)((__bridge void *)person + 16);
        NSObject *name = [NSString stringWithUTF8String:"123"];
        *b = name;
        
        
        NSLog(@"person : %@---- ", person.name);
}
複製程式碼

1、當我們相對一個指標進行偏移的,這時候我們應該知曉我們想要偏移多少個位元組,這樣我們就把這個指標轉化什麼型別的指標

2、當用malloc申請一片記憶體,而非使用new alloc 這種方式生成的時候,我們在把這個void * 指標轉向 OC物件指標的時候,我們一定加入unsafe_unreatined許可權修飾符

4、關聯 association

相關文章