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
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
TestKVOClassIndexedIvars;
3. 如何解决 custom-KVO 导致的 native-KVO Crash
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-BvJTXOiu-1715627819910)]
[外链图片转存中…(img-LGUU0vis-1715627819911)]
[外链图片转存中…(img-zmCTJtm5-1715627819911)]
[外链图片转存中…(img-WHZDdOo3-1715627819912)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!