iOS大解密:玄之又玄的KVO(2)

0x10e5b1088 <+198>: leaq   0x2b7929(%rip), %rax      ; __block_descriptor_tmp.77

0x10e5b108f <+205>: movq   %rax, 0x18(%r9)

0x10e5b1093 <+209>: movq   %rbx, 0x28(%r9)

0x10e5b1097 <+213>: movq   %r15, 0x30(%r9)

0x10e5b109b <+217>: movq   %r13, 0x20(%r9)

0x10e5b109f <+221>: movl   -0x2c(%rbp), %eax

0x10e5b10a2 <+224>: movl   %eax, 0x38(%r9)

0x10e5b10a6 <+228>: movq   0x364fab(%rip), %rsi      ; “_changeValueForKey🔑key:usingBlock:”

0x10e5b10ad <+235>: xorl   %ecx, %ecx

0x10e5b10af <+237>: xorl   %r8d, %r8d

0x10e5b10b2 <+240>: movq   %r13, %rdi

0x10e5b10b5 <+243>: movq   %r12, %rdx

0x10e5b10b8 <+246>: callq  *0x2b27ba(%rip)           ; (void *)0x000000010eb89d80: objc_msgSend

0x10e5b10be <+252>: movq   0x362f73(%rip), %rsi      ; “release”

0x10e5b10c5 <+259>: movq   %r12, %rdi

0x10e5b10c8 <+262>: callq  *0x2b27aa(%rip)           ; (void *)0x000000010eb89d80: objc_msgSend

0x10e5b10ce <+268>: addq   $0x48, %rsp

0x10e5b10d2 <+272>: popq   %rbx

0x10e5b10d3 <+273>: popq   %r12

0x10e5b10d5 <+275>: popq   %r13

0x10e5b10d7 <+277>: popq   %r14

0x10e5b10d9 <+279>: popq   %r15

0x10e5b10db <+281>: popq   %rbp

0x10e5b10dc <+282>: retq

上面这段汇编代码翻译为伪代码大致如下:

typedef struct {

Class originalClass;                // offset 0x0

Class KVOClass;                     // offset 0x8

CFMutableSetRef mset;               // offset 0x10

CFMutableDictionaryRef mdict;       // offset 0x18

pthread_mutex_t *lock;              // offset 0x20

void *sth1;                         // offset 0x28

void *sth2;                         // offset 0x30

void *sth3;                         // offset 0x38

void *sth4;                         // offset 0x40

void *sth5;                         // offset 0x48

void *sth6;                         // offset 0x50

void *sth7;                         // offset 0x58

bool flag;                          // offset 0x60

} SDTestKVOClassIndexedIvars;

typedef struct {

Class isa;                          // offset 0x0

int flags;                          // offset 0x8

int reserved;

IMP invoke;                         // offset 0x10

void *descriptor;                   // offset 0x18

void *captureVar1;                  // offset 0x20

void *captureVar2;                  // offset 0x28

void *captureVar3;                  // offset 0x30

int captureVar4;                    // offset 0x38

} SDTestStackBlock;

void _NSSetIntValueAndNotify(id obj, SEL sel, int number) {

Class cls = object_getClass(obj);

// 获取类实例关联的信息

SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(cls);

pthread_mutex_lock(indexedIvars->lock);

NSString *str = (NSString *)CFDictionaryGetValue(indexedIvars->mdict, sel);

str = [str copyWithZone:nil];

pthread_mutex_unlock(indexedIvars->lock);

if (indexedIvars->flag) {

[obj willChangeValueForKey:str];

((void(*)(id obj, SEL sel, int number))class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number);

[obj didChangeValueForKey:str];

} else {

// 生成block

SDTestStackBlock block = {};

block.isa = _NSConcreteStackBlock;

block.flags = 0xC2000000;

block.invoke = ___NSSetIntValueAndNotify_block_invoke;

block.descriptor = __block_descriptor_tmp;

block.captureVar2 = indexedIvars;

block.captureVar3 = sel;

block.captureVar1 = obj;

block.captureVar4 = number;

[obj _changeValueForKey:str key:nil key:nil usingBlock:&SDTestStackBlock];

}

}

这段代码的大致意思是说首先通过 object_getIndexedIvars(cls)获取到 KVO 类的 indexedIvars,如果 indexedIvars->flag 为 true 即开发者自己重写实现过 willChangeValueForKey:或者 didChangeValueForKey:方法的话就直接以 class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number)的方式实现对被观察的原方法的调用,否则就用默认实现为 NSSetIntValueAndNotify_block_invoke 的栈 block 并捕获 indexedIvars、被 KVO 观察的实例、被观察属性对应的 SEL、赋值参数等所有必要参数并将这个 block 作为参数传递给 [obj _changeValueForKey:str key:nil key:nil usingBlock:&SDTestStackBlock]调用。看到这里你或许会有个疑问:伪代码中通过 object_getIndexedIvars(cls)获取到的 indexedIvars 是什么信息呢?block.invoke = ___ NSSetIntValueAndNotify_block_invoke 又是如何实现的呢?首先我们看下 NSSetIntValueAndNotify_block_invoke 的汇编实现:

Foundation`___NSSetIntValueAndNotify_block_invoke:

->  0x10bf27fe1 <+0>:  pushq  %rbp

0x10bf27fe2 <+1>:  movq   %rsp, %rbp

0x10bf27fe5 <+4>:  pushq  %rbx

0x10bf27fe6 <+5>:  pushq  %rax

0x10bf27fe7 <+6>:  movq   %rdi, %rbx

0x10bf27fea <+9>:  movq   0x28(%rbx), %rax

0x10bf27fee <+13>: movq   0x30(%rbx), %rsi

0x10bf27ff2 <+17>: movq   (%rax), %rdi

0x10bf27ff5 <+20>: callq  0x10c1422b2               ; symbol stub for: class_getMethodImplementation

0x10bf27ffa <+25>: movq   0x20(%rbx), %rdi

0x10bf27ffe <+29>: movq   0x30(%rbx), %rsi

0x10bf28002 <+33>: movl   0x38(%rbx), %edx

0x10bf28005 <+36>: addq   $0x8, %rsp

0x10bf28009 <+40>: popq   %rbx

0x10bf2800a <+41>: popq   %rbp

0x10bf2800b <+42>: jmpq   *%rax

___NSSetIntValueAndNotify_block_invoke 翻译成伪代码如下:

void ___NSSetIntValueAndNotify_block_invoke(SDTestStackBlock *block) {

SDTestKVOClassIndexedIvars *indexedIvars = block->captureVar2;

SEL methodSel =  block->captureVar3;

IMP imp = class_getMethodImplementation(indexedIvars->originalClass);

id obj = block->captureVar1;

SEL sel = block->captureVar3;

int num = block->captureVar4;

imp(obj, sel, num);

}

这个 block 的内部实现其实就是从 KVO 类的 indexedIvars 里取到原始类,然后根据 sel 从原始类中取出原始的方法实现来执行并最终完成了一次 KVO 调用。我们发现整个 KVO 运作过程中 KVO 类的 indexedIvars 是一个贯穿 KVO 流程始末的关键数据,那么这个 indexedIvars 是何时生成的呢?indexedIvars 里又包含哪些数据呢?想要弄清楚这个问题,我们就必须从 KVO 的源头看起,我们知道既然 KVO 要用到 isa 交换那么最终肯定要调用到 object_setClass 方法,这里我们不妨以 object_setClass 函数为线索,通过设置条件符号断点来追踪 object_setClass 的调用,lldb 调试截图如下:

断点到 object_setClass 之后,我们再验证看下寄存器 rdi、rsi 里面的参数打印出来分别是<Test: 0x600003df01b0>、NSKVONotifying_Test

不错,我们现在已经成功定位到 KVO 的 isa 交换现场了,然而为了找到 KVO 类的生成的地方我们还需要沿着调用栈向前回溯,最终我们定位到 KVO 类的生成函数_NSKVONotifyingCreateInfoWithOriginalClass,其汇编代码如下:

Foundation`_NSKVONotifyingCreateInfoWithOriginalClass:

->  0x10c557d79 <+0>:   pushq  %rbp

0x10c557d7a <+1>:   movq   %rsp, %rbp

0x10c557d7d <+4>:   pushq  %r15

0x10c557d7f <+6>:   pushq  %r14

0x10c557d81 <+8>:   pushq  %r12

0x10c557d83 <+10>:  pushq  %rbx

0x10c557d84 <+11>:  subq   $0x20, %rsp

0x10c557d88 <+15>:  movq   %rdi, %r14

0x10c557d8b <+18>:  movq   0x2b463e(%rip), %rax      ; (void *)0x000000011012d070: __stack_chk_guard

0x10c557d92 <+25>:  movq   (%rax), %rax

0x10c557d95 <+28>:  movq   %rax, -0x28(%rbp)

0x10c557d99 <+32>:  xorl   %eax, %eax

0x10c557d9b <+34>:  callq  0x10c55b452               ; NSKeyValueObservingAssertRegistrationLockHeld

0x10c557da0 <+39>:  movq   %r14, %rdi

0x10c557da3 <+42>:  callq  0x10c7752b8               ; symbol stub for: class_getName

0x10c557da8 <+47>:  movq   %rax, %r12

0x10c557dab <+50>:  movq   %r12, %rdi

0x10c557dae <+53>:  callq  0x10c775ba0               ; symbol stub for: strlen

0x10c557db3 <+58>:  movq   %rax, %rbx

0x10c557db6 <+61>:  addq   $0x10, %rbx

0x10c557dba <+65>:  movq   %rbx, %rdi

0x10c557dbd <+68>:  callq  0x10c775666               ; symbol stub for: malloc

0x10c557dc2 <+73>:  movq   %rax, %r15

0x10c557dc5 <+76>:  leaq   0x29d604(%rip), %rsi      ; _NSKVONotifyingCreateInfoWithOriginalClass.notifyingClassNamePrefix

0x10c557dcc <+83>:  movq   $-0x1, %rcx

0x10c557dd3 <+90>:  movq   %r15, %rdi

0x10c557dd6 <+93>:  movq   %rbx, %rdx

0x10c557dd9 <+96>:  callq  0x10c77510e               ; symbol stub for: __strlcpy_chk

0x10c557dde <+101>: movq   $-0x1, %rcx

0x10c557de5 <+108>: movq   %r15, %rdi

0x10c557de8 <+111>: movq   %r12, %rsi

0x10c557deb <+114>: movq   %rbx, %rdx

0x10c557dee <+117>: callq  0x10c775108               ; symbol stub for: __strlcat_chk

0x10c557df3 <+122>: movl   $0x68, %edx

0x10c557df8 <+127>: movq   %r14, %rdi

0x10c557dfb <+130>: movq   %r15, %rsi

0x10c557dfe <+133>: callq  0x10c775762               ; symbol stub for: objc_allocateClassPair

0x10c557e03 <+138>: movq   %rax, %rbx

0x10c557e06 <+141>: testq  %rbx, %rbx

0x10c557e09 <+144>: je     0x10c557f17               ; <+414>

0x10c557e0f <+150>: movq   %rbx, %rdi

0x10c557e12 <+153>: callq  0x10c775816               ; symbol stub for: objc_registerClassPair

0x10c557e17 <+158>: movq   %r15, %rdi

0x10c557e1a <+161>: callq  0x10c7754ec               ; symbol stub for: free

0x10c557e1f <+166>: movq   %rbx, %rdi

0x10c557e22 <+169>: callq  0x10c77588e               ; symbol stub for: object_getIndexedIvars

0x10c557e27 <+174>: movq   %rax, %r15

0x10c557e2a <+177>: movq   %r14, (%r15)

0x10c557e2d <+180>: movq   %rbx, 0x8(%r15)

0x10c557e31 <+184>: movq   0x2b4748(%rip), %rdx      ; (void *)0x000000010d7fd1f8: kCFCopyStringSetCallBacks

0x10c557e38 <+191>: xorl   %edi, %edi

0x10c557e3a <+193>: xorl   %esi, %esi

0x10c557e3c <+195>: callq  0x10c774778               ; symbol stub for: CFSetCreateMutable

0x10c557e41 <+200>: movq   %rax, 0x10(%r15)

0x10c557e45 <+204>: movq   0x2b49e4(%rip), %rcx      ; (void *)0x000000010d7f6bb8: kCFTypeDictionaryValueCallBacks

0x10c557e4c <+211>: xorl   %edi, %edi

0x10c557e4e <+213>: xorl   %esi, %esi

0x10c557e50 <+215>: xorl   %edx, %edx

0x10c557e52 <+217>: callq  0x10c774454               ; symbol stub for: CFDictionaryCreateMutable

0x10c557e57 <+222>: movq   %rax, 0x18(%r15)

0x10c557e5b <+226>: leaq   -0x38(%rbp), %rbx

0x10c557e5f <+230>: movq   %rbx, %rdi

0x10c557e62 <+233>: callq  0x10c775a3e               ; symbol stub for: pthread_mutexattr_init

0x10c557e67 <+238>: movl   $0x2, %esi

0x10c557e6c <+243>: movq   %rbx, %rdi

0x10c557e6f <+246>: callq  0x10c775a44               ; symbol stub for: pthread_mutexattr_settype

0x10c557e74 <+251>: leaq   0x20(%r15), %rdi

0x10c557e78 <+255>: movq   %rbx, %rsi

0x10c557e7b <+258>: callq  0x10c775a20               ; symbol stub for: pthread_mutex_init

0x10c557e80 <+263>: movq   %rbx, %rdi

0x10c557e83 <+266>: callq  0x10c775a38               ; symbol stub for: pthread_mutexattr_destroy

0x10c557e88 <+271>: cmpq   $-0x1, 0x3824a0(%rip)     ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken + 7

0x10c557e90 <+279>: jne    0x10c557fa4               ; <+555>

0x10c557e96 <+285>: movq   (%r15), %rdi

0x10c557e99 <+288>: movq   0x366528(%rip), %rsi      ; “willChangeValueForKey:”

0x10c557ea0 <+295>: callq  0x10c7752b2               ; symbol stub for: class_getMethodImplementation

0x10c557ea5 <+300>: movb   $0x1, %cl

0x10c557ea7 <+302>: cmpq   0x38248a(%rip), %rax      ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange

0x10c557eae <+309>: jne    0x10c557ec9               ; <+336>

0x10c557eb0 <+311>: movq   (%r15), %rdi

0x10c557eb3 <+314>: movq   0x366526(%rip), %rsi      ; “didChangeValueForKey:”

0x10c557eba <+321>: callq  0x10c7752b2               ; symbol stub for: class_getMethodImplementation

0x10c557ebf <+326>: cmpq   0x38247a(%rip), %rax      ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange

0x10c557ec6 <+333>: setne  %cl

0x10c557ec9 <+336>: movb   %cl, 0x60(%r15)

0x10c557ecd <+340>: movq   0x36715c(%rip), %rsi      ; “_isKVOA”

0x10c557ed4 <+347>: leaq   0x1ff(%rip), %rdx         ; NSKVOIsAutonotifying

0x10c557edb <+354>: xorl   %ecx, %ecx

0x10c557edd <+356>: movq   %r15, %rdi

0x10c557ee0 <+359>: callq  0x10c558057               ; NSKVONotifyingSetMethodImplementation

0x10c557ee5 <+364>: movq   0x365154(%rip), %rsi      ; “dealloc”

0x10c557eec <+371>: leaq   0x1ef(%rip), %rdx         ; NSKVODeallocate

0x10c557ef3 <+378>: xorl   %ecx, %ecx

0x10c557ef5 <+380>: movq   %r15, %rdi

0x10c557ef8 <+383>: callq  0x10c558057               ; NSKVONotifyingSetMethodImplementation

0x10c557efd <+388>: movq   0x36519c(%rip), %rsi      ; “class”

0x10c557f04 <+395>: leaq   0x433(%rip), %rdx         ; NSKVOClass

0x10c557f0b <+402>: xorl   %ecx, %ecx

0x10c557f0d <+404>: movq   %r15, %rdi

0x10c557f10 <+407>: callq  0x10c558057               ; NSKVONotifyingSetMethodImplementation

0x10c557f15 <+412>: jmp    0x10c557f84               ; <+523>

0x10c557f17 <+414>: cmpq   $-0x1, 0x382409(%rip)     ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog + 7

0x10c557f1f <+422>: jne    0x10c557fbc               ; <+579>

0x10c557f25 <+428>: movq   0x3823f4(%rip), %r14      ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog

0x10c557f2c <+435>: movl   $0x10, %esi

0x10c557f31 <+440>: movq   %r14, %rdi

0x10c557f34 <+443>: callq  0x10c7758e2               ; symbol stub for: os_log_type_enabled

0x10c557f39 <+448>: testb  %al, %al

0x10c557f3b <+450>: je     0x10c557f79               ; <+512>

0x10c557f3d <+452>: movq   %rsp, %rbx

0x10c557f40 <+455>: movq   %rsp, %rax

0x10c557f43 <+458>: leaq   -0x10(%rax), %r8

0x10c557f47 <+462>: movq   %r8, %rsp

0x10c557f4a <+465>: movl   $0x8200102, -0x10(%rax)   ; imm = 0x8200102

0x10c557f51 <+472>: movq   %r15, -0xc(%rax)

0x10c557f55 <+476>: leaq   -0x63f5c(%rip), %rdi

0x10c557f5c <+483>: leaq   0x296c1d(%rip), %rcx      ; “KVO failed to allocate class pair for name %s, automatic key-value observing will not work for this class”

0x10c557f63 <+490>: movl   $0x10, %edx

0x10c557f68 <+495>: movl   $0xc, %r9d

0x10c557f6e <+501>: movq   %r14, %rsi

0x10c557f71 <+504>: callq  0x10c7751aa               ; symbol stub for: _os_log_error_impl

0x10c557f76 <+509>: movq   %rbx, %rsp

0x10c557f79 <+512>: movq   %r15, %rdi

0x10c557f7c <+515>: callq  0x10c7754ec               ; symbol stub for: free

0x10c557f81 <+520>: xorl   %r15d, %r15d

0x10c557f84 <+523>: movq   0x2b4445(%rip), %rax      ; (void *)0x000000011012d070: __stack_chk_guard

0x10c557f8b <+530>: movq   (%rax), %rax

0x10c557f8e <+533>: cmpq   -0x28(%rbp), %rax

0x10c557f92 <+537>: jne    0x10c557fd4               ; <+603>

0x10c557f94 <+539>: movq   %r15, %rax

0x10c557f97 <+542>: leaq   -0x20(%rbp), %rsp

0x10c557f9b <+546>: popq   %rbx

0x10c557f9c <+547>: popq   %r12

0x10c557f9e <+549>: popq   %r14

0x10c557fa0 <+551>: popq   %r15

0x10c557fa2 <+553>: popq   %rbp

0x10c557fa3 <+554>: retq

0x10c557fa4 <+555>: leaq   0x382385(%rip), %rdi      ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce

0x10c557fab <+562>: leaq   0x2b9886(%rip), %rsi      ; __block_literal_global.8

0x10c557fb2 <+569>: callq  0x10c7753d8               ; symbol stub for: dispatch_once

0x10c557fb7 <+574>: jmp    0x10c557e96               ; <+285>

0x10c557fbc <+579>: leaq   0x382365(%rip), %rdi      ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken

0x10c557fc3 <+586>: leaq   0x2b982e(%rip), %rsi      ; __block_literal_global

0x10c557fca <+593>: callq  0x10c7753d8               ; symbol stub for: dispatch_once

0x10c557fcf <+598>: jmp    0x10c557f25               ; <+428>

0x10c557fd4 <+603>: callq  0x10c775102               ; symbol stub for: __stack_chk_fail

翻译成伪代码如下:

typedef struct {

Class originalClass;                // offset 0x0

Class KVOClass;                     // offset 0x8

CFMutableSetRef mset;               // offset 0x10

CFMutableDictionaryRef mdict;       // offset 0x18

pthread_mutex_t *lock;              // offset 0x20

void *sth1;                         // offset 0x28

void *sth2;                         // offset 0x30

void *sth3;                         // offset 0x38

void *sth4;                         // offset 0x40

void *sth5;                         // offset 0x48

void *sth6;                         // offset 0x50

void *sth7;                         // offset 0x58

bool flag;                          // offset 0x60

} SDTestKVOClassIndexedIvars;

Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) {

const char *clsName = class_getName(originalClass);

size_t len = strlen(clsName);

len += 0x10;

char *newClsName = malloc(len);

const char *prefix = “NSKVONotifying_”;

__strlcpy_chk(newClsName, prefix, len);

__strlcat_chk(newClsName, clsName, len, -1);

Class newCls = objc_allocateClassPair(originalClass, newClsName, 0x68);

if (newCls) {

objc_registerClassPair(newCls);

SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(newCls);

indexedIvars->originalClass = originalClass;

indexedIvars->KVOClass = newCls;

CFMutableSetRef mset = CFSetCreateMutable(nil, 0, kCFCopyStringSetCallBacks);

indexedIvars->mset = mset;

CFMutableDictionaryRef mdict = CFDictionaryCreateMutable(nil, 0, nil, kCFTypeDictionaryValueCallBacks);

indexedIvars->mdict = mdict;

pthread_mutex_init(indexedIvars->lock);

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

bool flag = true;

IMP willChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(willChangeValueForKey:));

IMP didChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(didChangeValueForKey:));

if (willChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange && didChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange) {

flag = false;

}

indexedIvars->flag = flag;

NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA), NSKVOIsAutonotifying, nil)

NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc), NSKVODeallocate, nil)

NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class), NSKVOClass, nil)

});

} else {

// 错误处理过程省略…

return nil

}

return newCls;

}

通过_NSKVONotifyingCreateInfoWithOriginalClass 的这段伪代码你会发现我们之前频繁提到 indexedIvars 原来就是在这里初始化生成的。objc_allocateClassPair 在 runtime.h 中的声明为 Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) ,苹果对 extraBytes 参数的解释为“The number of bytes to allocate for indexed ivars at the end of the class and metaclass objects.”,这就是说当我们在通过 objc_allocateClassPair 来生成一个新的类时可以通过指定 extraBytes 来为此类开辟额外的空间用于存储一些数据。系统在生成 KVO 类时会额外分配 0x68 字节的空间,其具体内存布局和用途我用一个结构体描述如下:

typedef struct {

Class originalClass;                // offset 0x0

Class KVOClass;                     // offset 0x8

CFMutableSetRef mset;               // offset 0x10

CFMutableDictionaryRef mdict;       // offset 0x18

pthread_mutex_t *lock;              // offset 0x20

void *sth1;                         // offset 0x28

void *sth2;                         // offset 0x30

void *sth3;                         // offset 0x38

void *sth4;                         // offset 0x40

void *sth5;                         // offset 0x48

void *sth6;                         // offset 0x50

void *sth7;                         // offset 0x58

bool flag;                          // offset 0x60

} SDTestKVOClassIndexedIvars;

3. 如何解决 custom-KVO 导致的 native-KVO Crash

最后

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

以下是今天给大家分享的一些独家干货:

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
         // offset 0x50

void *sth7;                         // offset 0x58

bool flag;                          // offset 0x60

} SDTestKVOClassIndexedIvars;

3. 如何解决 custom-KVO 导致的 native-KVO Crash

最后

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

以下是今天给大家分享的一些独家干货:

[外链图片转存中…(img-GUWebYto-1715838338226)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值