内存管理一

全局静态变量考察

.h文件

static int personNum = 100;

@interface LGPerson : NSObject

- (void)run;
+ (void)eat;
@end

.m文件

@implementation LGPerson
- (void)run{
    personNum ++;
    NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}

+ (void)eat{
    personNum ++;
    NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}

@end

然后在ViewController中执行下面代码的打印结果是什么?

NSLog(@"vc:%p--%d",&personNum,personNum); // 100
personNum = 10000;
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LGPerson new] run]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[LGPerson eat]; // 102
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000

结果已经在上面代码的注释中,全局静态变量只对文件内有效。一个文件内是同一个变量。如果在其他文件访问这个全局静态变量,那么访问到的是另外copy的变量,地址和源文件的全局静态变量不一样。

taggedPointer考察

已知ViewController中有以下的nameStr属性。

@interface ViewController ()

@property (nonatomic, strong) dispatch_queue_t  queue;
@property (nonatomic, strong) NSString *nameStr;

@end

下面的代码可以正常执行吗?

- (void)taggedPointerDemo {
    self.queue = dispatch_queue_create("com.zf.cn", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"zf我是谁的谁是我的我是谁的谁"];
             NSLog(@"%@",self.nameStr);
        });
    }
}

答案是不能正常执行,会出现野指针崩溃。原因nameStr属性是通过nonatomic修饰的,是线程不安全的。

setter方法实现

setter方法的实现中存在旧值得释放和新值的retain或者copy。如果不加锁进行保护的话,多线程可能会对旧值release多次,从而出现野指针。我们只需要将nonatomic换成atomic就可以解决。

如果将以上代码中的self.name的赋值改为下面这样

self.nameStr = [NSString stringWithFormat:@"zf"];

然后再次运行就不会崩溃了。为什么换了一个值就会不一样呢?我们通过断点来看下具体的区别

taggedPointer

CFString

可以看出区别就是,如果字符比较短,那么nameStr就是NSTaggedPointerString,否则就是_NSCFString
我们查看objc的源码找到objc_release函数实现。

void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

可以看到release方法会判断是否为taggedPointer,如果为true直接就返回了,不会对taggedPointer对象进行发送release消息。所以上面代码中 nameStr为taggedPointer时不会崩溃。

TaggedPointer是系统对地址的一个优化,此时TaggedPointer地址已经不再是纯粹的地址,而是表示了变量的值以及类型。下面我们来打印一下TaggedPointer变量的地址。

NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"b"];

NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);

NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;

NSLog(@"%@-%p-%@", object_getClass(number1), number1, number1);
NSLog(@"%@-%p-%@", object_getClass(number2), number2, number2);
NSLog(@"%@-%p-%@", object_getClass(number3), number3, number3);
NSLog(@"%@-%p-%@", object_getClass(number4), number4, number4);

打印的结果如下:

taggedPointer[62681:1674928] 0xef3a6d66db722165-a
taggedPointer[62681:1674928] 0xef3a6d66db722155-b
taggedPointer[62681:1674928] __NSCFNumber-0xff3a6d66db722766-1
taggedPointer[62681:1674928] __NSCFNumber-0xff3a6d66db722766-1
taggedPointer[62681:1674928] __NSCFNumber-0xff3a6d66db722751-2
taggedPointer[62681:1674928] __NSCFNumber-0x6000038d8760-3.2

但是打印结果的地址有点看不懂,看不到地址中能包含变量的值。我们去objc源码中追究一下:
_read_images中调用了下面的TaggedPointer的初始化方法

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

我们看到在新的系统中objc_debug_taggedpointer_obfuscator通过mask被赋予了值。在_objc_encodeTaggedPointer_objc_decodeTaggedPointer方法有使用到了这个值,和TaggedPoint对象的地址进行了与操作。

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

这下我们应该明白了,TaggedPoint变量的地址通过objc_debug_taggedpointer_obfuscator进行了encode,所以才无法通过地址直接看到值。我们只需要将变量的地址decode一下。
首先模仿系统的decode方法:

uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

然后调用decode方法对变量指针decode:

NSString *str2 = [NSString stringWithFormat:@"b"];

NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));

NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;

NSLog(@"%p-%@ - 0x%lx",number1,number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));

打印结果如下:

taggedPointer[62961:1688303] 0xa000000000000621
taggedPointer[62961:1688303] 0xb000ee1dc1cb13c7-1 - 0xb000000000000012
taggedPointer[62961:1688303] 0xb000000000000012
taggedPointer[62961:1688303] 0xb000000000000025
taggedPointer[62961:1688303] 0x8e1dc1aca9f5

可以看到str的指针为 0xa000000000000621。0xa代表字符串
number1的指针为0xb000000000000012。0xb代表number类型,其中右边第二位代表值。

retain()

retain操作会走下面的
objc_object::rootRetain(bool tryRetain, bool handleOverflow)方法。
在此方法中首先判断是否为taggedPointer

if (isTaggedPointer()) return (id)this;

如果不是taggedPointer的话,引用计数存在散列表中,然后通过sidetable_retain进行retain操作。

// 散列表的引用计数表 进行处理 ++
if (slowpath(!newisa.nonpointer)) {
    ClearExclusive(&isa.bits);
    if (rawISA()->isMetaClass()) return (id)this;
    if (!tryRetain && sideTableLocked) sidetable_unlock();
    if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
    else return sidetable_retain();
}

sidetable_retain()的定义如下:

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

通过SideTables()[this]获取到一个SideTables,可见,散列表是个集合,这是因为在进行散列表的操作的时候,需要保证效率和安全,如果全部对象都用一个散列表,那么查询和操作的速度肯定比较低。如果分步在多个散列表中,就可以提高效率,以及安全度。
引用计数加上SIDE_TABLE_RC_ONE,我们可以找到SIDE_TABLE_RC_ONE的定义:

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit

可见,SIDE_TABLE_RC_ONE是左移了两位,为什么左移两位呢?因为左边的第一位表示的是SIDE_TABLE_WEAKLY_REFERENCED,也就是weak表;左边第二位是SIDE_TABLE_DEALLOCATING,也就是析构表。

SideTable的结构如下,主要包含锁、引用计数表、weak表。

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
.......
}

我们继续回到rootRetain方法,看下如果是nonpointer的情况:

uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

nonponter的引用计数首先选择存在isa.bits中,那么其中的carry是干什么的呢?
carry的作用是:如果因为引用计数太大,isa中装不下了,那么会将引用计数的一半放到isa中,另外一个放到散列表中。

if (slowpath(carry)) {
    // newisa.extra_rc++ overflowed
    if (!handleOverflow) {
        ClearExclusive(&isa.bits);
        return rootRetain_overflow(tryRetain);
    }
    // Leave half of the retain counts inline and 
    // prepare to copy the other half to the side table.
    if (!tryRetain && !sideTableLocked) sidetable_lock();
    sideTableLocked = true;
    transcribeToSideTable = true;
    newisa.extra_rc = RC_HALF;
    newisa.has_sidetable_rc = true;
}
if (slowpath(transcribeToSideTable)) {
    // Copy the other half of the retain counts to the side table.
    sidetable_addExtraRC_nolock(RC_HALF);
}

release()的过程相当于将retain()的反过来。

retainCount

我们都知道下面代码的打印结果是1,那么objc对象的存储的真正的引用计数是多少呢?

NSObject *objc = [NSObject alloc]; 
NSLog(@"%ld",objc.retainCount); 

我们看下objc的源码,retainCount调用了_objc_rootRetainCount。

- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}

_objc_rootRetainCount又调用了objc的rootRetainCount。

uintptr_t
_objc_rootRetainCount(id obj)
{
    ASSERT(obj);

    return obj->rootRetainCount();
}

然后来到rootRetainCount函数:

inline uintptr_t 
objc_object::rootRetainCount() // 1
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        // bits.extra_rc = 0;
        // 
        uintptr_t rc = 1 + bits.extra_rc; // isa
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock(); // 散列表
        }
        sidetable_unlock();
        return rc; // 1
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

如果是nonpointer,就会返回1+bits.extra_rc。所以虽然外面打印了1,但是bits.extra_rc实际上为0,只不过返回retainCount的时候默认加了1。所以我们说alloc出来的对象实际上的引用计数为0,我们外面打印的为1,是因为默认加1。

dealloc方法

我们来探究下dealloc方法里面做了些什么呢?
dealloc方法调用了_objc_rootDealloc_objc_rootDealloc调用了rootDealloc

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

里面又调用了object_dispose函数。

static id 
_object_dispose(id anObject) 
{
    if (anObject==nil) return nil;

    objc_destructInstance(anObject);
    
    anObject->initIsa(_objc_getFreedObjectClass ()); 

    free(anObject);
    return nil;
}

函数_object_dispose在free之前调用了objc_destructInstance。我们看下函数objc_destructInstance的实现:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

1、移除cxx;
2、移除关联对象;
3、调用了clearDeallocating;

下面我们看下clearDeallocating的实现:

objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

如果不是nonpointer,清理散列表,也就是清理引用计数表和weak表;否则的话执行clearDeallocating_slow

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) { // 清理引用计数表
        table.refcnts.erase(this);
    }
    table.unlock();
}

同样是清理引用计数表和weak表。

总结:dealloc主要实现的内容:
1、移除cxx;
2、移除关联对象;
3、清理引用计数表和weak表;
4、free对象;

Timer的引用无法释放问题

ViewController中的下面代码会导致什么问题?

@interface TimerViewController ()
@property (nonatomic, strong) NSTimer       *timer;
@end

@implementation TimerViewController
- (void)viewDidLoad {
     self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
    // 加runloop
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
@end

当然是viewController无法释放。是怎么样的一种持有关系导致ViewController无法释放的呢?
因为timer依赖于RunLoop,需要加入到RunLoop中才能执行,而RunLoop又是全局的。所以RunLoop持有timer。通过timer的文档我们知道timer是强持有了target,也就是timer持有了self。所以timer释放不了,self也无法释放。
持有关系是RunLoop-->timer-->self。
我们再block的时候为了打断引用关系,使用了weakSelf的方式。那么这里能不能也使用weakSelf呢?

__weak typeof(self) weakSelf = self; // weak
    
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
// 加runloop
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

答案是依旧会无法释放。首先我们看解释一下weakSelf的作用:
weakSelf和self都指向同一个对象的两个不同的指针,也就是说它们指向的对象是同一个,但是它们的指针地址不一样。weakSelf虽然不会引起指向对象引用计数的增加,但是如果你强持有weakSelf,就相当于强持有了weakSelf所指向的对象。也就是虽然weakSelf不会导致self的引用计数加1,但是如果其他的对象强持有weakSelf,也就相当于强持有了self,从而会导致self的引用计数加1。例如下面的打印结果是什么呢?

NSObject *obj = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)obj));

__weak NSObject *weakObjc = obj;

NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)obj));

NSObject *newWObjc = weakObjc;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)obj));

打印结果为"1 1 2"。

但是为什么block那么传weakSelf就不会导致循环应用了呢?因为block的实现方法_Block_object_assign

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        //里面什么都没做
        _Block_retain_object(object);
        *dest = object;
        break;
......

我们看到指向object是*dest。也就是说dest保存的是object的指针。如果此处传进来的是weakSelf的话,dest指向的就是weakSelf的指针,而不是self对象。所以此处不会对weakSelf所指向的对象产生影响。也就是说weakSelf指向的对象是self,dest指向的对象是weakSelf的指针。如果此处是dest=object,那么weakSelf也不能解决循环引用问题了。

解决Timer无法释放的问题

我们要解决上面提交的Timer无法释放的问题,那么就需要在合适的时机将timer调用invalidate并且置为nil。

方法一、didMoveToParentViewController

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
    }
}

在上面方法中判断parent == nil,也就是当pop的时候parent就为nil。所以可以在此处将timer销毁,那么ViewController的dealloc方法也就能正常执行了。

方法二、
使用中介者模,创建一个其他的对象,然后将timer的target指向这个中间者对象,给中间者对象添加方法,在方法的实现函数中处理timer的事件。

self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];

然后timer会一直调用下面的fireHomeObjc函数。

void fireHomeObjc(id obj){
    NSLog(@"%s -- %@",__func__,obj);
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

然后ViewController 返回的时候就会正常的调用dealloc方法了,只需要在dealloc方法中将timer销毁就可以了。

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
}

方式三、
方法三和方法二类似,都是使用中介者模式,只不过将方法二进行封装成了一个工具类TimerWapper,该工具类的初始化方法如下:

- (instancetype)zf_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    if (self == [super init]) {
        self.target     = aTarget; // vc
        self.aSelector  = aSelector; // 方法 -- vc 释放
        
        if ([self.target respondsToSelector:self.aSelector]) {
            self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(timerFire) userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

注意target需要使用weak来修饰,防止出现循环引用

@property (nonatomic, weak) id target;

然后在timerFire中使用发送消息的方式调用ViewController的方法,判断当target不存在的时候,要及时销毁到timer。这样的在ViewController中也不需要处理timer的销毁了,全部都封装到工具类了,外面只需要调用工具类的方法就可。

- (void)timerFire {
    if (self.target) {
        void (*zf_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
        zf_msgSend((__bridge void *)(self.target), self.aSelector, self.timer);
    } else {
        [self.timer invalidate];
        self.timer = nil;
    }
}

方法四、使用NSProxy
NSProxy是个虚拟类,我们需要写个子类集成于它,他的特点是可以将自己未实现的方法转给其他对象去实现。自定义Proxy的实现如下:

#import "ZFProxy.h"

@interface ZFProxy()
@property (nonatomic, weak) id object;
@end

@implementation ZFProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    ZFProxy *proxy = [ZFProxy alloc];
    proxy.object = object;
    return proxy;
}

// 转移
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}

@end

我们在外面使用:

self.proxy = [ZFProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];

在ViewController中创建proxy,然后创建timer,将timer的target设置为proxy,但是proxy没有实现对象的方法,然后将方法的实现者重新指向self,最终会调用self的firHome方法。这样同样也不会导致self释放不了的问题了,因为timer强持有的是proxy。注意这个方法依然需要在self的dealloc方法中移除timer。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容

  • # 前言 反复地复习iOS基础知识和原理,打磨知识体系是非常重要的,本篇就是重新温习iOS的内存管理。 内存管理是...
    Vein_阅读 799评论 0 2
  • 一、抛出一个问题:使用CADisplayLink、NSTimer 有什么注意点?1.1-1.6的demo1.1、分...
    IIronMan阅读 1,372评论 1 3
  • 内存管理是程序在运行时分配内存、使用内存,并在程序完成时释放内存的过程。在Objective-C中,也被看作是在众...
    蹲瓜阅读 3,095评论 1 8
  • 内存管理ARC处理原理ARC是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制,ARC所做的只...
    阳明先生_X自主阅读 339评论 0 3
  • 内存管理ARC处理原理ARC是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制,ARC所做的只...
    阳明先生_X自主阅读 484评论 1 3