二十一、React Nactive与原生的交互
一、RN调用原生方法
1、编写原生的功能类,需要实现RCTBridgeModule协议,类中包含包含RCT_EXPORT_MODULE()宏用来指定React Native中可以访问的类名。另外通过RCT_EXPORT_METHOD()宏来声明哪些方法是在ReactNative中可以调用的
定义模型
#import <React/RCTBridgeModule.h>
@interface ZHJSManager : NSObject <RCTBridgeModule>
@end
#import "ZHJSManager.h"
@implementation ZHJSManager
// 默认导出名为 ZHJSManager 的module暴露给JS
RCT_EXPORT_MODULE();
// 也可以传值自定义moduleName 比如 RCT_EXPORT_MODULE(MyCustomJSManager);
@end
2、直接在React Native中调用就可以了。
暴露方法
模型定义好之后就可以写方法,为了让RN发现原生方法需要在.m中使用另外一个宏定义RCT_EXPORT_METHOD()来导出(包裹)你的方法:
@implementation ZHJSManager
RCT_EXPORT_MODULE();RCT_EXPORT_METHOD(methodNameWithFirst:(NSString *)paramStr second:(NSString *)second){
//OC代码随便写
}
// 你还可以使用RN向原生端传递整数RCT_EXPORT_METHOD(getIntFromReactNative:(NSInteger)count) {
NSString *msg = [NSString stringWithFormat:@"我收到来自RN的整数:%zd", count]; [self showAlert:msg];}
// 你甚至还可以使用RN向原生传递NSDictionary和NSArray等复杂数据RCT_EXPORT_METHOD(getDictionaryFromRN:(NSDictionary *)dict) { NSLog(@"RN传递过来的字典:%@", dict);
// 使用字典参数做你想做的,原生iOS攻城狮}
RCT_EXPORT_METHOD(getArrayFromRN:(NSArray *)array) { NSLog(@"RN传递过来的数组:%@", array); // 使用数组参数做你想做的,原生iOS攻城狮}
//还可以使用RN提供的block
RCT_EXPORT_METHOD(giveMyRNSomeStringWithBlock:(RCTResponseSenderBlock)callbackBlock) { if (callbackBlock) { callbackBlock(@[@"包裹在数组中的字符串"]); }}
/// 使用block回传字典或者数组(包裹成二维数组了)到RN端RCT_EXPORT_METHOD(giveMyRNSomeDicDataWithBlock:(RCTResponseSenderBlock)callbackBlock) { if (callbackBlock) { NSDictionary *dict = @{@"key1": @"strValue", @"key2" : @(20), @"key3": @(YES)};
callbackBlock(@[dict]); }}
//同步方法的宏,两个参数第一个是返回值的类型,第二个是方法名RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString*, canReturnSomething){ return @"SSSSS";
}
// 使用Promise回传数据到RN端
RCT_REMAP_METHOD(findEvents,
findEventsWithResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *events = ...
if (events) {
resolve(events);
} else {
NSError *error = ...
reject(@"no_events", @"There were no events", error);
}
}
//或者//RCT_EXPORT_METHOD(usePromisePassToRN:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {// if (![key isEqualToString:@""]) {// resolve(@(YES));// } else {// reject(@"warning", @"key 不能为空!", nil);// }//}@end
相关JS代码实现如下:
//Promise方法调用原生
async function updateEvents() {
try {
const events = await ZHJSManager.findEvents(); this.setState({ events });
} catch (e) {
console.error(e);
}
}
function methodNameWithFirst(param1,param2){
//其他调用类似
ZHJSManager.methodNameWithFirst(param1,param2);}
三种调用方法
1、通过Callback的方式(同步调用)
callback就是在RN中写一个回调方法,等着原生代码执行完成之后回调。
2、通过Promise的方式(同步调用)
3、通过通知的方式(异步调用)
上述的代码给React Native发送了一个名为"testEventName"的通知事件,并且携带了map作为参数。
React Native的代码: React Native 对原生模块名为“testEventName”的事件进行监听。
我们要创建一个 Bridge。在 React Native 中,通过 Bridge 实现了 JavaScript 与原生框架之间的通信
RCTBridge *carrierBridge = [[RCTBridge alloc] initWithDelegate:self
launchOptions:nil];
接下来,我们需要创建一个 RCTRootView,用于展示 React Native 视图的组件RCTRootView,在 JavaScript 代码中 render() 部分的 UI 组件均会渲染到该 View 中,创建方式如下:
RCTRootView *rctView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:moduleName
initialProperties:nil];
[self.view addSubview:rctView];
整体的流程是这样的:
* 在初始化Bridge时,在 setup 的过程中,首先会调用 bridge 的代理方法 (NSURL \*)sourceURLForBridge : (RCTBridge \*)bridge 方法,指定获取 JS bundle 的路径:
-(NSURL *)sourceURLForBridge:(RCTBridge *)bridge{
NSString *bundlePath = [self getCurrentBundlePath:bundleid];
return bundlePath;
}
* 确定 URL 之后,bridge 会调用 start 方法,开始加载 JS bundle 并调用以下方法:
[self loadSource:^(NSError *error, RCTSource *source) {
if (error) {
[weakSelf handleError:error];
}
...
]
* 接下来会调用 bridge 的代理方法,我们可以在该方法中手动注入一些业务参数:
- (void)loadSourceForBridge:(RCTBridge *)bridge
onProgress:(RCTSourceLoadProgressBlock)onProgress
onComplete:(RCTSourceLoadBlock)loadCallback{
[RCTJavaScriptLoader loadBundleAtURL:bridge.bundleURL onProgress:onProgress onComplete:^(NSError *error, RCTSource *source) {
//手动注入一些业务参数
NSString *string = ";this.__xxx___ = 'yyy';"
NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *newData = [NSMutableData dataWithData:stringData];
[newData appendData:source.data];
//生成新的Source去加载
RCTSource * newSource = [RCTJavaScriptLoader getNewRCTSourceURL:source.url data:newData];
loadCallback(error,newSource);
}];
}
之后,bridge 会负责执行该 newSource,执行 JavaScript 代码并渲染出页面。
二、原生调用JS方法(发送事件)
即使没有被 JavaScript 调用,原生模块也可以给 JavaScript 发送事件通知。最好的方法是继承RCTEventEmitter,实现suppportEvents方法并调用self sendEventWithName:。
/// 接收通知的方法,接收到通知后发送事件到RN端。RN端接收到事件后可以进行相应的逻辑处理或界面跳转
- (void)sendCustomEvent:(NSNotification *)notification {
[self sendEventWithName:kCustomEventName body:@"这是发给RN的字符串"];
}
/// 重写方法,定义支持的事件(方法名)集合
- (NSArray<NSString *> *)supportedEvents {
return @[kCustomEventName,kHaveImportListNotification,kHaveSelectedExpressImage,kHaveSelectedRbrsImages];
}
三、调用原生UI组件
你甚至可以自己在iOS编写一个组件然后提供给RN来作为render中的一部分来渲染页面
首先创建一个RCTViewManager的子类。
添加RCT_EXPORT_MODULE()宏标记。
实现-(UIView *)view方法。
// ZHMapManager.m
// 命名规则前缀自定义,避免和RNT 或者UI这种框架自己的命名规则冲突
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface ZHMapManager : RCTViewManager
@end
@implementation ZHMapManager
RCT_EXPORT_MODULE(ZHCustomMap)
(UIView *)view
{
//这里不能设置frame和backgroundcolor之类的属性,会被ReactNative所覆盖,因为他要保证整个页面的一致性,
//你可以直接在jsx中设置style
return [[MKMapView alloc] init];
}
@end
你在原生种完成了上面的代码,为了能让js可以直接使用,还需要作如下操作,添加MapView.js文件,然后使用RN提供的HOC方法requireNativeComponent
// MapView.js
import { requireNativeComponent } from 'react-native';/*
requireNativeComponent 自动把'ZHCustomMap'解析为'ZHCustomMapManager'
第二个参数MapView可选,但建议加上
这使得 React Native 的底层框架可以检查原生属性和包装类的属性是否一致,来减少出现问题的可能。
*/export default requireNativeComponent('ZHCustomMap',MapView);//
然后你就可以在其他组件中使用这个MapView组件了。so easy,so nice
// MyApp.jsimport MapView from './MapView.js';
...render() {//需要加上style,否则看不到哦 return <MapView style={{ flex: 1 }} />;}
这里你应该会有疑问,每个组件都有自己的属性,原生组件怎么设置某些属性?直接在我们自定义的ZHMapManager.m文件中添加导出属性
// ZHMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
然后我们就可以在js代码中愉快的使用我们原生属性了
// MyApp.js
<MapView zoomEnabled={false} style={{ flex: 1 }} />
当然,你应该加一些文档以方便你组件的使用者,或者如果你使用的是ts,记得写个interface来约束下。
更复杂的属性如下
// ZHMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:(json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region) animated:YES];
}
总结
先创建一个 Bridge。在 React Native 中,通过 Bridge 实现了 JavaScript 与原生框架之间的通信
RCTBridge *carrierBridge = [[RCTBridge alloc] initWithDelegate:self
launchOptions:nil];
接下来,我们需要创建一个 RCTRootView,用于展示 React Native 视图的组件RCTRootView,在 JavaScript 代码中 render() 部分的 UI 组件均会渲染到该 View 中,创建方式如下:
RCTRootView *rctView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:moduleName
initialProperties:nil];
RN调用原生端:RCT_EXPORT_MODULE()宏用来指定React Native中可以访问的类名。另外通过RCT_EXPORT_METHOD()宏定义方法,RCT_EXPORT_VIEW_PROPERTY定义属性。
原生模块也可以给 JavaScript 发送事件通知。最好的方法是继承RCTEventEmitter,实现suppportEvents方法并调用self sendEventWithName:。
二十二、podspec作用
podsepc文件的全称我们可以叫做 pod specification,specification是规格说明书的意思,所以顾名思义,podspec就是pod 库的规格说明书(配置文件),这个说明书描述了pod库的版本,包括了源文件的需要的地址,用什么样的文件,需要什么样的构建配置,还有许多普通的元数据像是库的名称,版本号以及描述。
二十三、load方法跟main方法哪个先调用
1、load方法是在main函数执行前执行的;
2、+load方法是在加载类和分类时系统调用,一般不手动调用,如果想要在类或分类加载时做一些事情,可以重写类或分类的+load方法。
3、类、分类的+load方法,在程序运行过程只调用一次。
二十四、iOS 有什么方案让wkwebView加载更快一些
二十五、即时通讯怎么处理心跳问题?怎么处理丢包问题呢?
TCP KeepAlive用于检测连接的死活,而心跳机制则附带一个额外的功能:检测通讯双方的存活状态。
最简单粗暴的方法是定时心跳,如每隔30秒心跳一次,15秒内没有收到心跳包则认为当前连接已失效,断开连接并进行重连
连接可靠性的判断也可以放宽,避免一次心跳超时就认为连接无效的情况,使用错误积累,只在心跳超时n次后才判定当前连接不可用
处理丢包问题:发送一个包 包首,包中,包尾都做了标记,并且有字节大小
二十六、怎么销毁单例
static NGUser *sharedInstance = nil;
static dispatch_once_t onceToken;
+ (instancetype)sharedInstance{
dispatch_once(&onceToken, ^{
//调用父类的allocWithZone,不能使用self,避免循环引用
sharedInstance = [[super allocWithZone:NULL] init];
});
return sharedInstance;
}
//必须要实现的,当我们创建一个对象,alloc会给对象分配内存,init初始化数据
//alloc会调用allocWithZone,如果创建对象没有使用sharedInstance,而是使用alloc
//那么alloc就会调用allocWithZone,重写类方法,调用sharedInstance使得alloc时创建的也是单例对象
+(instancetype)allocWithZone:(struct _NSZone *)zone{
return [self sharedInstance];
}
//单例对象被copy
-(id)copyWithZone:(nullable NSZone *)zone{
return self;
}
+(void)attempDealloc{
onceToken = 0; // 只有置成0,GCD才会认为它从未执行过.它默认为0.这样才能保证下次再次调用shareInstance的时候,再次创建对象.
sharedInstance = nil;
}
二十七、项目架构,MVVM有什么优势?RAC有什么优势?两者结合起来有什么优势?
MVVM(Model - View/ViewController - ViewModel)是对MVC的一种变形设计模式,解决ViewController代码臃肿、View和Model模块耦合严重两个主要问题。抽出ViewModel来处理ViewController的业务逻辑,分离了UI代码和业务逻辑,并且ViewModel监听model事件,一旦发生变化更新视图,很好的解决了视图与模型的依赖性。看下图
MVVM优势:
低耦合:View 可以独立于Model变化和修改。
可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑。
独立开发:开发人员可以专注于业务逻辑和数据的 viewModel开发。
可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试。
兼容性:MVVM 可以兼容当下使用的MVC架构, 增加应用的可测试性。
ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架。结合了几种编程风格:函数式编程 和 响应式编程 ,使用RAC解决问题,就不需要考虑调用顺序,直接考虑结果,把每一次操作都写成一系列嵌套的方法中,使代码高聚合,方便管理。
函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用。
响应式编程思想:不需要考虑调用顺序,只需要考虑结果,产生一个事件,事件传播出去,然后影响结果。
RAC与MVVM架构设计的优点
1、从上面RAC的用法可以看出,RAC的绑定,这是常用的。比如:用户信息展示界面->登录界面->登录成功->回到用户信息展示界面->展示用户信息,以往我们的做法通常是通知,也可以用协议、block什么的,一旦代码量多了过后,耦合度高,维护成本就会增加,而使用RAC的属性绑定、属性联合等一系列方法,将会有事半功倍的效果,这样就可以解决相当多的需求了。
2、MVVM搭建思路里面会涉及大量的属性绑定、事件传递来实现不同功能,运用RAC能大量简化代码,充分的降低了代码的耦合度,降低维护成本,思路更清晰。
在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM。然后,为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,因为苹果本身并没有提供一个比较适合这种情况的绑定方法。iOS领域里KVO,Notification,block,delegate和target-action都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。
二十八、图形绘制
第一种绘图形式UIBezierPath:在UIView的子类方法drawRect:中绘制一个蓝色圆,使用UIKit在Cocoa为我们提供的当前上下文中完成绘图任务。
-(void)drawRect:(CGRect)rect{
UIBezierPath*p=[UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColorblueColor]setFill];
[pfill];
}
第二种绘图形式:使用Core Graphics实现绘制蓝色圆。
-(void)drawRect:(CGRect)rect{
CGContextRefcon=UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con,CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con,[UIColorblueColor].CGColor);
CGContextFillPath(con);
}
第三种绘图形式:我将在UIView子类的drawLayer:inContext:方法中实现绘图任务。drawLayer:inContext:方法是一个绘制图层内容的代理方法。为了能够调用drawLayer:inContext:方法,我们需要设定图层的代理对象。但要注意,不应该将UIView对象设置为显示层的委托对象,这是因为UIView对象已经是隐式层的代理对象,再将它设置为另一个层的委托对象就会出问题。轻量级的做法是:编写负责绘图形的代理类。在MyView.h文件中声明如下代码:
@interface MyLayerDelegate:NSObject
@end
然后MyView.m文件中实现接口代码:
@implementation MyLayerDelegate
-(void)drawLayer:(CALayer*)layerinContext:(CGContextRef)ctx{
UIGraphicsPushContext(ctx);
UIBezierPath*p=[UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColorblueColor]setFill];
[pfill];
UIGraphicsPopContext();
}
@end
@interface MyView(){
MyLayerDelegate*_layerDeleagete;
}
@end
使用该图层代理:
MyView*myView=[[MyViewalloc]initWithFrame:CGRectMake(0,0,320,480)];
CALayer*myLayer=[CALayerlayer];
_layerDelegate=[[MyLayerDelegatealloc]init];
myLayer.delegate=_layerDelegate;
[myView.layeraddSublayer:myLayer];
[myViewsetNeedsDisplay];//调用此方法,drawLayer:inContext:方法才会被调用。
第四种绘图形式:使用Core Graphics在drawLayer:inContext:方法中实现同样操作,代码如下:
-(void)drawLayer:(CALayer*)layinContext:(CGContextRef)con{
CGContextAddEllipseInRect(con,CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con,[UIColorblueColor].CGColor);
CGContextFillPath(con);
}
第五种绘图形式:使用UIKit实现:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100),NO,0);
UIBezierPath*p=[UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColorblueColor]setFill];
[pfill];
UIImage*im=UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
第六种绘图形式:使用Core Graphics实现:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100),NO,0);
CGContextRefcon=UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con,CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con,[UIColorblueColor].CGColor);
CGContextFillPath(con);
UIImage*im=UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
二十九、自动布局,通过布局优先级来适配按钮离底部的距离10-35?
在Autolayout中每个约束都有一个优先级,优先级的范围是1 ~ 1000,默认创建的约束优先级是最高的1000。
三十、怎么实现同步修改struct原数据的值
#include <stdio.h>
struct person {
char *name;
int age;
int weight;
};
void changePersonl(struct person p);
void showPersonl(struct person p);
void showPersonl(struct person p) {
printf("name:%s\n", p.name);
printf("age:%d\ n", p.age);
printf("weight:%d\n", p.weight);
}
void changePersonl(struct person p) {
p.age = p.age - 2;
p.weight = p.weight - 5;
printf("in changePersonl()\n");
showPersonl(p);
return;
}
int main() {
struct person bob, sue;
sue.name = "sue";
sue.age = 26;
sue.weight = 50;
bob.name = "bob";
bob.age = 20;
bob.weight = 60;
showPersonl(sue);
printf("********before changeperson*************\n");
showPersonl(bob);
printf("*************after changepersonl*************\n");
changePersonl(bob);
showPersonl(bob);
return 0;
}
if 想要通过调用函数修改这个结构体中原始数据的值,需要定义指针,这个指针指向结构体
每次声明person结构体的时候,需要写struct person 太繁琐了,所以可以用 typedef,typedef可以定义一个等价的量,
typedef struct person Person;
struct person *sue,*bob;//两个结构体指针
Person *sue,*bob;//两个结构体指针
//两句话等价
//一开始这两个指针并没有指向任何一个结构体,因为结构体还没分配内存空间,
//先给结构体分配内存空间,并确定指针指向了它中的元素
//以下可以这么写
Alloc_1D(sue,1,Person);
//它为结构体person分配内存,并把指针sue与内存联系到一起
通过指针访问Person中的元素,和直接访问Person中的元素不同,
为了设定sue中的age元素可以有以下两种写法
(*sue).age=21;
sue->age=21;
三十一、 block 底层原理
一、Block定义
返回值类型 (^block变量名)(形参列表) = ^(形参列表) {
};
// 调用Block保存的代码
block变量名(实参);
二、Block底层实现
block的底层实现是结构体,和类的底层实现类似,都有isa指针,可以把block当成是一个对象。下面通过创建一个控制台程序,来窥探block的底层实现
block 的内存结构图:
三、Block变量截获
Block如何捕获外部变量一:基本数据类型
局部变量截获 是值截获
一:auto变量
auto变量:自动变量,离开作用域就会销毁,一般我们创建的局部变量都是auto变量 ,比如 int age = 10,系统会在默认在前面加上auto int age = 10
首先我们要搞清楚,什么是捕获,所谓捕获外部变量,意思就是在block内部,创建一个变量来存放外部变量,这就叫做捕获.先做一个小小的Test:
{
int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 20;
block();
}
输出的age是10
二:static变量 局部静态变量截获 是指针截获。
auto int age = 10;
static int height = 20;
void (^block)(void) = ^{
NSLog(@"age is %d, height is %d",age,height);
};
age = 20;
height = 30;
block();
打印的结果是age is 10, height is 30
我们看到,对于age是捕获到内部,把外部age的值存起来,而对于height,是把外部变量的指针保存起来,所以, 我们在修改height时,会影响到block内部的值
总结:block捕获外部基本数据类型变量: auto变量是值传递;static变量是指针传递
三:全局变量
为什么全局变量不需要捕获?
因为全局变量无论哪个函数都可以访问,block内部当然也可以正常访问,所以根本无需捕获
为什么局部变量就需要捕获呢?
因为作用域的问题,我们在一个函数中定义变量,在block内部访问,本质上跨函数访问,所以需要捕获起来.
@implementation Person
- (void)test{
void(^block)(void) = ^{
NSLog(@"会不会捕获self--%@",self);
};
block();
}
@end
答案是会捕获self,我们看看底层代码:
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
很显然block内部的确声明了一个Person *self用于保存self,既然block内部捕获了self,那就说明self肯定是一个局部变量.那问题就来了,
为什么self会是一个局部变量?它应该是一个全局变量呀?我们看一下转换后的test()方法:
static void _I_Person_test(Person * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
我们 OC 中的test()方法时没有参数的,但是转换成 C++ 后就多了两个参数self,_cmd,其实我们每个 OC 方法都会默认有这两个参数,这也是为什么我们在每个方法中都能访问self和_cmd,而参数就是局部变量,所以block就自然而然的捕获了self.
总结:
局部变量截获 是值截获;局部静态变量截获 是指针截获;全局变量不需要捕获。
一:只要是局部变量,不管是auto 变量
,还是static 变量
,block
都会捕获. 不同的是,对于auto 变量
,block
是保存值,而static 变量
是保存的指针.
二:如果是全局变量,根本不需要捕获,直接访问
本篇只是讲解了block
捕获基本数据类型的auto
变量
[block如何捕获外部变量(对象类型变量)
结论:
不管是 ARC 环境还是 MRC 环境,栈空间的block是不会拥有外部对象的 ; 堆空间的block会拥有外部对象,在 ARC 环境下就是强引用,在 MRC 环境下就是 retain.
结论:
当block内部访问了对象类型的auto变量时:
1:如果block是在栈上,将不会对auto变量产生强引用(不管是 MRC 或者 ARC)
2:如果block是在堆上,就说明block进行过copy操作,进行copy操作的block会自动调用block内部的__main_block_copy_0函数,__main_block_copy_0函数内部会根据auto变量的修饰符形成相应的强引用(retain)或者弱引用.
3:当block销毁时,block会自动调用内部的dispose函数,dispose函数会自动调用内部的__main_block_dispose_0释放引用的auto变量
__block 修饰的外部变量
对于用 block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改block 修饰的外部变量的值。
__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
输出为:
age = 18
四、Block的几种形式
block有三种类型:
全局块(_NSConcreteGlobalBlock)
栈块(_NSConcreteStackBlock)
堆块(_NSConcreteMallocBlock)
全局块存在于全局内存中, 相当于单例.
栈块存在于栈内存中, 超出其作用域则马上被销毁
堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存
1、不使用外部变量的block是全局block
NSLog(@"%@",[^{
NSLog(@"globalBlock");
} class]);
输出:NSGlobalBlock
2、使用外部变量并且未进行copy操作的block是栈block
NSInteger num = 10;
NSLog(@"%@",[^{
NSLog(@"stackBlock:%zd",num);
} class]);
输出:NSStackBlock
日常开发时是将block当做参数传递给方法:
- (void)testWithBlock:(void(^)(NSString *string))callback{
NSString *string = @"string";
callback(string);
NSLog(@"%@",[callback class]);
}
NSInteger num = 10;
[self testWithBlock:^(NSString *string) {
NSLog(@"stackBlock:%zd",num);
}];
输出:NSGlobalBlock
3、对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block
比如堆1中的全局进行copy操作,即赋值:
void (^globalBlock)(void) = ^{
NSLog(@"globalBlock");
};
NSLog(@"%@",[globalBlock class]);
输出:NSGlobalBlock 仍是全局block
而对2中的栈block进行赋值操作:
NSInteger num = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"stackBlock:%zd",num);
};
NSLog(@"%@",[mallocBlock class]);
输出:NSMallocBlock
对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block
c、block变量与forwarding
在copy操作之后,既然block变量也被copy到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?forwarding 终于要闪亮登场了,如下图:
通过forwarding, 无论是在block中还是 block外访问block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。