0%

前言


在Flutter开发中,对于一些简单的数据传递,我们可以使用Widget constructor直接传递进去,但是当某个后代Widget依赖上层的祖先Widget或者多个Widget同时依赖祖先Widget的情形时,直接传递的方式就会暴露出很多的问题:需要连续传递很难维护以及依赖的祖先Widget数据改变时,所有子Widget都需要rebuild。

Flutter提供了另外一个机制来解决这个问题,InheritedWidget,当InheritedWidget数据发生变化时,只通知依赖其数据变化的Widgets,接下来将按步骤分析实现细节。( Provider也是基于InheritedWidget进行的封装)

实现细节


我们通过一个小demo来展示使用方法,假设数据Color需要被后代Widget访问,且当Color变化,后代Widget能监听到变化。步骤比较简单,先子类InheritedWidget,如下代码示例,FrogColor定义了一个名为of的静态方法,该方法由后代Widget进行调用,调用时将后代Widget的Element作为context传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FrogColor extends InheritedWidget {
const FrogColor({
Key key,
@required this.color,
@required Widget child,
}) : assert(color != null),
assert(child != null),
super(key: key, child: child);

final Color color;

static FrogColor of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<FrogColor>();
}

@override
bool updateShouldNotify(FrogColor old) => color != old.color;
}

我们看下of静态方法的实现,实际调用的是后代Widget对应Element的 dependOnInheritedWidgetOfExactType,该方法首先会去_inheritedWidgets里找是否有需要的InheritedWidget,如果找到,返回找到的InheritedWidget,并将自己作为依赖注册到InheritedWidget里,这样InheritedWidget就能知道哪些Widget依赖它,如果InheritedWidget发生了变化,会通知依赖方。_inheritedWidgets会在InheritedWidget mount的时候从父Element获取当前所有的InheritedWidgets,并且把自己也加到里边,也就是说,所有的Element都有一个Map来收集当前祖先所有的InheritedWidget。

1
2
3
4
5
6
7
8
9
10
11
@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

重新回到FrogColor类,另一个方法updateShouldNotify,当FrogColor widget rebuild时,FrogColor的Element会通过updated方法来判断是否需要通知所有依赖它的后代Widgets,super.updated(oldWidget)用来通知所有的dependencies 依赖方。

1
2
3
4
5
@override
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget);
}

当需要通知依赖方的时候,调用依赖Widget对应Element的didChangeDependencies方法,Element将自己标记为dirty,并加到BuildOwner的dirty列表中,当下一帧绘制时,会重新build Widget。

1
2
3
4
5
6
@mustCallSuper
void didChangeDependencies() {
assert(_active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}

总结


综上,我们可以看到,InheritedWidget不仅能解决多层传递带来的业务复杂度,也能非常高效的进行rebuild操作。


Flutter使用Dart作为开发语言以及运行时,Dart 运行时存在于Debug和Release模式,不过两个模式中的运行时有很大的区别。

Debug模式下,Dart运行时,JIT编译器/解释器(Android是JIT,iOS是解释器),Debug和Profile工具都会加载到设备上。而Release模式下,JIT编译器/解释器和Debug服务被移除,只包括运行时。

Dart运行时包含垃圾回收器,在对象创建和销毁时分配和释放内存。Flutter在运行中,会创建大量的对象,比如,伴随着在界面展示,不可见,app 状态变化等,会创建很多Staleless Widgets,其中很多生命周期都很短。一个比较复杂的UI界面,widgets数量能上千。

那么Flutter 开发者是否需要关心垃圾回收?Flutter在高频次创建和销毁对象的情况下,开发者是否需要采取措施来限制这种行为?很多人会对不会变的widgets创建引用,保存在state中,来避免重建和销毁。其实这些是没有必要的,因为Dart垃圾回收器基于分代架构,且针对对象的快速创建和销毁做了优化,大多数情况下,让引擎自己管理所有widgets创建和销毁即可。

Dart垃圾回收器


Dart的垃圾回收器由两部分组成:新生代scavenger和并发标记清除回收器。

调度

为了降低垃圾回收对UI性能的影响,垃圾回收器提供了hooks给flutter engine,当没有用户交互或app处于闲时状态时,flutter engine会通知垃圾回收器来进行收集,而不会影响想能。

当处于闲时状态时,垃圾回收器也能进行内存碎片整理,减少内存碎片。

年轻代Scavenger

该阶段主要用来清理生命周期短的临时对象,如stateless widgets,会比次代标记/清除阶段更快,极大减小app运行过程中可能带来的卡顿。

对象创建时会在一片连续的内存空间进行分配,每次创建对象,都会再这片空间区域查找是否有可用的空间,当可用空间被占满,Dart会使用bump pointer(指针碰撞)的方式来快速分配新的空间。

新的空间分为两部分,称为半空间,任何时候,只有一半是处于激活的,另一半则处于非激活状态。新创建的对象会在激活的那部分空间进行分配,当激活部分被占满,引用不可释放的对象会从激活空间移到非激活空间,非激活空间将变为激活空间,以此反复。

为了确定当前的对象是否可释放,收集器会从根对象,如栈变量,来检测是否是否还有引用,引用的对象会被移动到非激活空间,最后,剩下的对象就是可释放对象,在后续的垃圾回收时,引用对象会直接覆盖之前的可释放对象占用的空间。更多细节,参看Cheney’s 算法

并行标记和并发清除

当对象到达一定的生命周期,他们会被移入新的内存空间(老年代),由次代收集器(标记/清除)进行内存管理,收集器同样分两部分:首先会遍历对象图,标记还在使用的对象。遍历完成后,未被标记的对象会被回收。

注意,这种形式的垃圾回收会在标记阶段阻塞UI线程,且不能进行内存修改操作。不过,这个阶段发生的频率很低,因为生命周期短的对象都被年轻代scavenger处理了。

当然,如果开发的app不满足弱分代假说(即大多数对象会在年轻时死亡,生命周期短)的情况,那么该形式的垃圾回收会更容易发生。

Heap Map

Observatory下可以通过heap map来观察某一时刻特定Isolate老年代的内存分配,如下图,不同的颜色代表不同的内存块,白色代表空闲空间,如果我们看到很多小的白色块,说明产生了内存碎片,可能考虑有内存泄漏了。

Isolates


Dart的每个Isolate有自己的堆空间,每个Isolate也都运行在单独的线程中,垃圾回收时,互不影响性能,所以Isolate可以避免阻塞UI,进行CPU密集型、IO、网络等操作。

附录


  1. https://mrale.ph/dartvm/
  2. https://medium.com/flutter/flutter-dont-fear-the-garbage-collector-d69b3ff1ca30
  3. https://dart-lang.github.io/observatory/heap-map.html
  4. https://dart-lang.github.io/observatory/glossary.html?spm=ata.13261165.0.0.1f0058e56w9Oaq#memory-leak

Block管理Objective-C对象实例的原理


Block的实现结构体见如下图所示,Block对于捕获的Objective-C对象实例,会在Block实现的结构体中创建一个变量指向Objective-C对象实例,当Block将要释放时,会调用dispose_helper函数,该函数会调用所有需要进行内存管理的所捕获的对象,如Block__block变量、__attribute__((NSObject))变量或有constructor/destructorC++ const对象。

获取Block retain的Objective-C对象实例


所以,怎么能够拿到retainObjective-C对象实例呢,方法为创建fake对象,来模拟捕获的Objective-C对象实例,然后我们程序来调用Blockdispose_helper方法(dispose_helper方法接收一个参数,值为Block的指针),该方法会调用对象实例的release方法,我们只需要在fake对象中实现release方法,如果release方法被调用,所以该fake对象对应的真实变量为Objective-C对象实例。

那么需要创建多少个fake对象呢,首先,Objective-C对象实例在Blockstruct中的位置是指针对齐的,所以我们可以获取Block结构体的大小,除以指针的大小取上即为需要创建的fake对象的数量,Block结构体的size可以通过block->descriptor->size来获取。

得到需要创建fake对象的数量count后,创建一个数组,再创建countfake对象,将数组指针传入dispose_helper即可。对于调用了release方法的对象,记录其索引值,通过访问block[index]即可拿到真正的Objective-C对象实例。

注意


如上所说的捕获的Objective-C对象实例,不包括使用__block创建的实例,因为使用__block创建的变量,会加一层间接层,并不会在Block结构体中创建指向实例对象的变量,而是指向另一个间接层结构体。

附录


  1. https://clang.llvm.org/docs/Block-ABI-Apple.html
  2. https://github.com/mikeash/Circle/blob/master/Circle/CircleIVarLayout.m

Swap Space


LinuxMacos等系统有一个Swap space的概念,当物理内存紧张时,系统会将inactivepages放到Swap SpaceSwap Space为磁盘上的某个区域,一般是文件形式,这样能节省出来一部分的物理内存,不过,当我们需要访问已经放到磁盘中的内存时,由于已经不在物理内存中,会引发缺页中断,需要再次从磁盘中重新读取,所以会比直接从内存获取要慢。

不过iOS系统并没有Swap Space,原因可能有二,其一是iPhoneFlash闪存空间受限;其二是CPU受限,因为手机的CPU相比电脑还是有一定差距。

Swapped Size


iOS中,内存分为两种,一种为Clean memory,另一种为Dirty memory
Clean memorypage可以换出,既磁盘中有其对应内容,系统可以在内存紧张时将Clean memorypage换出,当再次访问时,可以重新从磁盘中读取,我们使用的图片、mapped filesFramework的数据段常量以及代码段等,这些都是Clean memory
Dirty memory是无法换出的,我们所有的堆上的分配等都是属于Dirty memory,所以我们一定要尽可能的减少Dirty memory的使用。

iOS7开始,iOS引入了Compression的概念,如下图为InstrumentsAllocations template,我们可以从标红的地方看到,有一个Swapped Size的指标,从WWDC 2018 416得知,该指标的含义为compression size,即系统可以把最近最少使用的Dirty memory进行压缩,这样可以腾出一些pages供使用,当再次需要访问内容时,系统将其解压,这时,原来内容占多少pages,解压后同样会是相同数量的pages

上图为JetsamEvent的log,展示了compression size等指标,jetsam_thread线程运行在阻塞的循环中,当唤起时,根据内存列表来kill掉处于top的进程,当内存足够时,再次进入休眠。

参考


  1. https://devstreaming-cdn.apple.com/videos/wwdc/2018/416n2fmzz0fz88f/416/416_ios_memory_deep_dive.pdf
  2. http://newosxbook.com/articles/MemoryPressure.html

GCD main queuemain thread的关系


dispatch_get_main_queue()返回main queue,该队列会被绑定到main thread,所以我们如果我们将block提交到main queue,那么该block将会在主线程中执行。

dispatch_sync(queue, block)main queue提交block


1
2
3
4
5
6
7
/// Current thread is main thread
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"Is main thread? %d", [NSThread isMainThread]);
});

/// output:
/// "Is main thread? 1"

大家有没有注意到,Log输出表明执行Block时的线程为主线程,根据我们以往的经验,dispatch_get_global_queue获取到的队列,队列中的Block应该是在Secondly thread中执行,为什么这里会是在主线程中呢。

答案就是在dispatch_sync语句,libdispatchcommit中进行了优化,只要目标queue不是main queue,那么提交的Block就会直接在原线程中执行,这就能解释为什么上面的demo程序中输出的Log显示是主线程。

那么,这种会不会有潜在的问题呢?

isMainThread带来的潜在问题


首先,我们看一个Radar,大概意思是,即使MapKit框架的addOverlay方法在主线程执行,但是由于其不是在main queue中执行的,会导致Crash,这是因为MapKit内部在main queue中使用dispatch_queue_set_specific设置了数据,当你在其它队列中执行时,会由于没有该数据从而导致Crash

避免使用isMainThread


所以,我们需要避免使用isMainThread,而是判断是不是main queue,我们可以使用两种方法来判断:

  1. 使用dispatch_queue_set_specificmain queue上设置flag,然后做判断,有该flag即为main queue.
  2. 判断dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())main queue设置了label,可以通过其来判断。

总结


  1. main queue中的Block一定在主线程中执行。
  2. 主线程可以对应多个queue,既可以有任意的队列,其Block可以在主线程中执行。

参考


  1. https://github.com/ReactiveCocoa/ReactiveCocoa/issues/2635#issuecomment-170215083
  2. http://www.openradar.me/24025596

Swift闭包作为Objective-C方法参数


Swift开发时,有时我们需要调用带有Block参数的Objective-C方法,比如,我们通过perform(_:with:afterDelay:inModes:)来在特定的Runloop模式下运行某selector,如下所示,doAnimation方法接收一个OptionalBlock,注意,该方法标注了@objc,所以是Objective-C方法,animationBlock参数为Objective-CBlock,那么我们怎么将SwiftClosure转化为Block呢,方法就是使用@convention(block)来声明兼容Objective-C BlockClosure,如下[1]所示:

1
2
3
4
5
6
7
8
9
10
11
12
class LocationMessageCell: UICollectionViewCell {
func callSEL() {
let block: @convention(block) (UIImageView) -> Void = { _ in // [1]
....
}
self.perform(#selector(LocationMessageCell.doAnimation(with:)), with: block, afterDelay: 0, inModes: [.defaultRunLoopMode])
}

@objc private func doAnimation(with animationBlock: ((UIImageView) -> Void)!) {
....
}
}

除了显式的用@convention(block)来声明,我们也可以直接使用闭包,如:

1
2
3
UIView.animate(withDuration: 0.1, animations: {
...
})

注意,之所以我没有在之前的例子进行修改,是因为func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval, inModes modes: [RunLoopMode])anArgument参数类型为Any?,编译器没有进行自动转换,这种情况只能显式的声明@convention(block)

参考


  1. https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html

消息发送


在使用Objective-C调用方法时,我们将其称之为消息发送,这与我们用的CC++等调用函数的说法不一样,原因就是Objective-C调用方法时,并不是简单的会在编译时得到函数指针,调用时直接使用该函数指针调用就行(C++有虚函数,包含一个v-table,可以实现简单的多态),而是会在调用的时候,运行时的去查找函数实现,比如,当我们发送[objc foo]时,编译器会将其转化为objc_msgSend(objc, @selector(foo))(注意,不一定都是转化为objc_msgSend,根据发送对象和返回类型,可转化为objc_msgSendSuper,objc_msgSendSuper_stret等),objc_msgSend方法负责查找函数实现并调用返回结果,我们知道,Objc Runtime开源的,所以我们可以看一下源代码objc_msgSend的实现逻辑。

objc_msgSend()使用汇编实现


源码可参见objc_msgSend源码,我们发现,竟然不是用C实现的,而是使用的汇编语言,总结来说,原因有二:

  1. 我们无法定义一个C函数,可以有可变的参数(可变参数是可以实现的,参考printf函数)并且可以调用任意的C函数指针,因为函数指针类型是在是无穷无尽的,根本就无法预先全部定义出来。
    阅读全文 »

前言


Golang是由Google开发,天生支持并发的语言。Go有一个goroutine的机制,当我们在调用函数前加上go关键字,那么就会创建一个goroutine来异步执行该函数,如:go foo(),以此来实现并发的功能。本文,我们将讨论一下goroutine和线程的区别。

大小可调整的栈


线程可以在启动前设置栈的大小,启动后,线程的栈大小就固定了,所以带来的弊端就是浪费内存空间,因为很多时候并不需要这么多内存。
相比于线程,goroutine的栈空间是可调整的,goroutine创建时,通常只会分配2KB大小的栈,随着goroutine的运行,比如不断的递归,创建变量等,相应的栈空间也会增大,反之也会按需减小,一个goroutine最大可分配栈空间通常为1GB

goroutine 的调度


线程由系统内核进行调度,系统为了实现并发,会不断的切换线程的执行,由此会带来线程的上下文切换。
Go的运行时有一套自己的调度系统,使用m:n策略,既复用mgoroutinen个线程,Go的调度器类似于内核调度器,区别是它仅管理单个Go程序创建的goroutine
相比于线程,Go的调度器并不会周期性的被硬件定时器调用,而是Go自己来处理,比如,当一个goroutine调用time.Sleep或阻塞与通道、互斥操作时,调度器会将goroutine休眠,然后运行另外的goroutine,这些并不需要内核的上下文切换,所以重新调度一个goroutine的开销会小于重新调度线程的开销。

GOMAXPROCS


Go的调度器使用一个名为GOMAXPROCS的参数来确定真正的线程数,其默认值为机器的CPU的数量,如一台机器有8个CPU,那么调度器会同时创建8个线程(GOMAXPROCS就是如上所说m:n中的n)。休眠或阻塞中goroutine不需要线程,但是对于阻塞在I/O或系统调用、正在调用非Go函数的goroutine来说,需要一个系统线程,不过不占用GOMAXPROCS

附录


  1. https://golang.google.cn
  2. http://www.gopl.io

Objective-C++内存管理


在项目开发时,有时会使用C++来进行混合开发,因为C++有很好的跨平台及性能优势。我们可以将C++对象作为Objective-C的属性或者反过来Objective-C作为C++对象的成员,当我们需要在App中使用C++库时,会很有用。

当我们的文件同时使用了Objective-CC++时,需要告知编译器来进行处理,方法为将该文件的后缀从.m改为.mm

如下示例展示了Objective-CC++互相作为成员变量来关联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// Forward declare so that everything works below
@class ObjcClass;
class CppClass;

// C++ class with an Objective-C member variable
class CppClass {
public:
ObjcClass *objcClass;
};

// Objective-C class with a C++ object as a property
@interface ObjcClass : NSObject
@property (nonatomic, assign) std::shared_ptr<CppClass> cppClass;
@end

@implementation ObjcClass
@end

// Using the two classes above
std::shared_ptr<CppClass> cppClass(new CppClass());
ObjcClass *objcClass = [[ObjcClass alloc] init];

cppClass->objcClass = objcClass;
objcClass.cppClass = cppClass;

如上,我们注意到,属性声明成了assign,而不是我们通常使用的strongweak,原因就是对于非Objective-C类型对象来说是没有意义的,编译器无法retainrelease一个C++对象,因为它不是一个Objective-C对象类型。

尽管声明属性为assign,但是内存管理依然能正确处理;不过这里要注意的是,如果我们使用原始指针来保存,这时候就需要自己进行内存管理了。

Objective-C对象实例总是在堆上分配,但是C++实例可以在栈或堆上。所以我们把分配在栈上的C++实例赋值给Objective-C的成员变量时,就有点奇怪,不过不用担心,它其实会放到堆上,因为整个Objective-C对象都是在堆上的。编译器实现这个转化的方式为:在allocdealloc方法中构建和析构C++对象,分别调用object_cxxConstructobject_cxxDestruct方法,来处理C++对象。

所以,对于所有基于栈的C++对象,不需要担心内存的问题,编译器自动接管。但是再强调一下,基于堆的C++对象,需要手动管理内存,如在deallocdelete``C++对象。

Game Over!!!

前言


所有代码注释可在Objc-Runtime中查看

iOS开发中,我们经常会通过dealloc来判断对象实例是否被释放,依据是当对象实例的引用计数变为0时,运行时会调用对象实例的dealloc方法,我们可以利用该方法做一些扫尾的工作。

dealloc调用时机


Objective-C的引用计数管理使用两种方式相结合,sidetableisa指针(指针并不是对象的真正内存地址,而是某些位用来进行了一些标志位的存放);接下来,我将以sidetable进行release来讨论dealloc的调用,直接上代码,如下sidetable_release(下文所有都会用sidetable_release来讨论)函数会在给对象发送release消息的时候调用,sidetable_release方法首先获取对象的引用计数,对引用计数相关标志位做操作,若对象实例可以被释放,将通过objc_msgSend发送SEL_dealloc消息,既调用对象的dealloc方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

bool do_dealloc = false;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
// 进行释放操作,调用dealloc
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
阅读全文 »