注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS 消息调用过程

iOS
iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、...
继续阅读 »

iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:


iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、一个缓存方法链表。当对实例 son 发送消息后,会在 son 缓存方法链表中寻找;缓存中没有时,向实例方法链表寻找;再找不到,会向父类的实例方法缓存链表 -> 父类的实例方法链表寻找,直至 NSObject。在 NSObject 中会经历以下两个步骤:
1 - (BOOL)resolveInstanceMethod:(SEL)sel ; 
2 - (id)forwardingTargetForSelector:(SEL)aSelector ;

如果在步骤 2 中范围 nil, 就会触发 iOS 的崩溃。

当向 Son 发送类方法时,会首先向 Son 的元类 metaClass 中的类缓存方法链表中寻找,然后类方法链表,然后直接在 NSObject 进行缓存方法链表 -> 类方法链表的寻找路径 . 在 NSObject 中会经历如下两个步骤:


实例的 methodList 链表中寻找方法,找不到时会寻找 Son 的类方法,仍然找不到时,会寻找父类的方法链表,直到 NSObject 。


其中不同对象间的切换,通过 isa 指针完成,实例 son 的 isa 指向类 Son, 类 Son 的 isa 指向元类,元类的 isa 指向父类的元类, 父类的元类向上传递,直至 NSObject .


NSObject 的指针 isa 指向其本身,在想 NSObject 发送消息时,会经历如下步骤:

1 + (BOOL)resolveClassMethod:(SEL)sel ; 
2 - (void)doesNotRecognizeSelector:(SEL)aSelector ;
当调用方法 2 时,会触发 iOS 的崩溃。利用以上机制,可以对resolveInstanceMethod 和 resolveClassMethod 两个方法进行方法交换,拦截可能出现的 iOS 崩溃,然后自定义处理。
作者:iOS猿_员
链接:https://www.jianshu.com/p/1a76ccad4e73
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS面试--虎牙最新iOS开发面试题

iOS
关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。 一面 项目架构,项目是自己写的吗 fps是怎么计算的 除了用cadisplay,还有什么方法吗 kv...
继续阅读 »

关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。


一面



  • 项目架构,项目是自己写的吗

  • fps是怎么计算的

  • 除了用cadisplay,还有什么方法吗

  • kvo怎么实现

  • leaks怎么实现

  • 如何代码实现监听僵尸对象

  • imageWithName什么时候发生编解码,在什么线程

  • isa指针里面有什么

  • 消息发送和消息转发流程

  • 函数里面的参数怎么存储

  • oc一个空函数里面有参数吗

  • 他们存在栈还是寄存器

  • 红黑树等查找时间复杂度

  • nsdictionary的实现

  • iOS的各种锁

  • 如何实现dispatch once,要考虑什么问题

  • 同一线程里面使用两个@synconize会怎么样,是递归锁还是非递归锁

  • 如何增加按钮点击范围


二面



  • 说一下ARC

  • autoreleasepool可以用来干嘛

  • 里面的对象什么时候释放,是出来就释放吗

  • 消息转发可以用来干什么

  • runloop是干什么,你用来干什么了

  • 说一下c++多态和虚函数表

  • TCP如何保证数据传输完整性

  • TCP为什么三次握手

  • http和https,全程都是非对称加密吗

  • 开放性问题,很多乱序数据过来,你要怎么考虑排序方法的设计

  • 对RxSwift的看法,有用过吗?


三面



  • iOS对象指针大小

  • 对象分配到堆还是栈

  • http怎么区分header和body

  • 多线程可以访问同一个对象吗,多进程呢

  • 视频pts和dts

  • 视频丢帧丢哪个好点

  • iOS各种锁的性能,琐是毫秒级别还是微妙级别

  • http请求是异步还是同步

  • 怎么看待rn和flutter


作者:iOS弗森科
链接:https://www.jianshu.com/p/17849abb722c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS之iOS13适配总结

iOS
前言 随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。 新特性适配 一、新添加的Dark Mode iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具...
继续阅读 »

前言


随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。


新特性适配


一、新添加的Dark Mode


iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具体适配可见: Implementing Dark Mode on iOS


切换、修改当前 UIViewController 或 UIView的模式。只要设置了控制器为暗黑模式,那么它子view也会对应的修改。



  • 只修改当前UIViewController或UIView的模式。

  • 只要设置了控制器为暗黑模式,那么它子view也会对应的修改。


代码如下:

if (@available(iOS 13.0, *)) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;//UIUserInterfaceStyleLight
} else {
// Fallback on earlier versions
}
注意当我们在window上设置 overrideUserInterfaceStyle的时候,就会影响 window下所有的controller,view,包括后续推出的 controller。



二、使用KVC访问私有变量已发崩溃


iOS13之后就不能通过KVC访问修改私有属性,不然就会找不到这个key,从而引发崩溃。


目前搜集到的KVC访问权限引发崩溃的方法:



  1. UIApplication -> _statusBar

  2. UITextField -> _placeholderLabel

  3. UITabBarButton -> _info

  4. UISearchBar -> _searchField

  5. UISearchBar -> _cancelButton

  6. UISearchBar -> _cancelButtonText

  7. UISearchBar -> UISearchBarBackground


1、UIApplication -> _statusBar 获取状态栏崩溃


在iOS13上获取状态栏statusBar,不能直接用KVC。要使用performSelector

UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
UIView *statusBar;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {

UIView *localStatusBar= [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {

statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}



适配的时候就是iOS13和非iOS13

if(@available(iOS 13.0, *)) {

//上面获取statusBar代码
} else {

UIView *statusBar = [[UIApplication sharedApplication]
valueForKey:@"statusBar"];

}



2、UITextField -> _placeholderLabel


在iOS13 UITextField通过KVC来获取_placeholderLabel会引发崩溃。

//在ios13使用会引发崩溃
[self.textField setValue:self.placeholderColor
forKeyPath:@"_placeholderLabel.textColor"];



崩溃如下:

'Access to UITextField's _placeholderLabel ivar is prohibited. 
This is an application bug'

解决方案:UITextField有个attributedPlaceholder的属性,我们可以自定义这个富文本来达到我们需要的结果。

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder 
attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;



3、UISearchBar 黑线处理导致崩溃


iOS13之前为了处理搜索框的黑线问题,通常会遍历searchBar的 subViews,找到并删除UISearchBarBackground。


在 iOS13 中这么做会导致UI渲染失败,然后直接崩溃,崩溃信息如下:

erminating app due to uncaught exception'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'



解决方案:修改方法为:设置 UISearchBarBackground 的 layer.contents 为 nil

for (UIView *view in _searchBar.subviews.lastObject.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
view.layer.contents = nil;
break;
}
}



4、iOS UISearchBar通过kvc获取_cancelButtonText、_cancelButton、_searchField引发崩溃。


先说一下_searchField来说明的解决方案。


在iOS13之前,我们通过"_searchField"来获取UISearchTextField来修改一些属性。

UITextField *searchFiled = [self valueForKey:@"_searchField"];



但在iOS13会引发崩溃,解决方案就是在iOS13中引入了名为searchTextField的属性。

@property (nonatomic, readonly) UISearchTextField *searchTextField;

查看一下UISearchTextField

UIKIT_CLASS_AVAILABLE_IOS_ONLY(13.0)
@interface UISearchTextField : UITextField
///功能省略
@end



发现UISearchTextField继承UITextField,代码实现:

UITextField *searchField;
if(@available(iOS 13.0, *)) {
//UISearchBar的self.searchTextField属性是readonly,不能直接用
searchField = self.searchTextField;
} else {
searchField = [self valueForKey:@"_searchField"];
}

三、presentViewController 默认弹出样式



  • 苹果将 UIViewController 的 modalPresentationStyle 属性的默认值改成了新加的一个枚举值 UIModalPresentationAutomatic,对于多数 UIViewController,此值会映射成 UIModalPresentationPageSheet。

  • iOS13系统的默认样式是: UIModalPresentationAutomatic

  • iOS12及以下系统的默认样式是:UIModalPresentationFullScreen;


想要改成以前默认的样式

- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}



四、AVPlayerViewController 替换MPMoviePlayerController


在 iOS 9 之前播放视频可以使用 MediaPlayer.framework 中的MPMoviePlayerController类来完成,它支持本地视频和网络视频播放。但是在 iOS 9 开始被弃用,如果在 iOS 13 中继续使用的话会直接抛出异常:

'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'

解决方案:

既然不能再用了,那只能换掉了。替代方案就是AVKit里面的那套播放器。


五、废弃UIWebview 改用 WKWebView


iOS13 开始苹果将 UIWebview 支持的系统(iOS2.0-iOS12.0),目前提交苹果应用市场(App Store)会发送邮件提示你在下一次提交时将应用中UIWebView的api移除。


虽然暂时没有强制必须替换WKWebView,但是在iOS13开始UIWebView已是废弃的API,所以还是越早换越好。


六、iOS13 获取window适配


在iOS13通过UIWindowScene的方式获取window

UIWindow* window = nil;
if (@available(iOS 13.0, *)) {
for (UIWindowScene* windowScene in [UIApplication sharedApplication].connectedScenes) {
if (windowScene.activationState == UISceneActivationStateForegroundActive) {
window = windowScene.windows.firstObject;
break;
}
}
}else{
window = [UIApplication sharedApplication].keyWindow;
}



七、iOS13 废弃LaunchImage


从iOS8的时候,苹果就引入了LaunchScreen,我们可以设置 LaunchScreen来作为启动页。当然,现在你还可以使用LaunchImage来设置启动图。


但是从2020年4月开始,所有使⽤ iOS13 SDK的 App将必须提供 LaunchScreen,LaunchImage即将退出历史舞台。使用LaunchScreen有点:



  • 不需要单独适配种屏幕尺寸的启动图

  • LaunchScreen是支持AutoLayout+SizeClass的,所以适配各种屏幕都不在话下


七、iOS13 适配UISegmentedControl


默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。

如下图:



其次设置选中颜色的tintColor属性在iOS13已经失效,所以在iOS13新增新增了selectedSegmentTintColor 属性用以修改选中的颜色。


适配代码如下:

if ( @available(iOS 13.0, *)) {
self.segmentedControl.selectedSegmentTintColor = [UIColor yellowcolor];
} else {
self.segmentedControl.tintColor = [UIColor yellowcolor];
}
作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/acde9bc3fc97
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS推送通知及静默推送相关

iOS
    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。1. 普通推送和静默推送的区别      &...
继续阅读 »

    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。

1. 普通推送和静默推送的区别

        普通推送:收到推送后(有文字有声音),点开通知,进入APP后,才执行

- (void)application:(UIApplication didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void result))handler *)application *)userInfo (^)(UIBackgroundFetchResult


        静默推送:(Silent Push)并不是必须要“静默”(通常是没有文字没有声音),只要推送payload中aps字典里包含了"content-available": 1的键值对,都具有静默推送的特性,不用点开通知,不用打开APP,就能执行

-(void)application:(UIApplication )application)userInfo didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void (^)(UIBackgroundFetchResultresult))handler


用户完全感觉不到所以静默推送又被我们称做 Background Remote Notification(后台远程推送)。

        静默推送是在iOS7之后推出的一种推送方式。它与其他推送的区别在于允许应用收到通知后在后台(background)状态下运行一段代码,可用于从服务器获取内容更新。

PS:注册消息通知时通常的弹窗询问权限有什么用呢?其实只是请求用户允许在推送通知到来时能够有alert, badge和sound,而并不是在请求注册推送本身的权限。静默推送即使用户不允许应用的推送,静默推送依然会送达用户设备,只是不会有alert, badge和sound。这也符合静默推送的正常使用场景。



2. 远程推送时 , 应用的几种状态及对应回调方法

     (1) . 应用开启时 , 应用在前台

     (2) . 应用开启时 , 应用在后台

     (3) . 应用未启动(应用被杀死)

从苹果APNS服务器远程推送时:

不使用时(iOS10以后可用)

1 . 如果应用处于 (1) 状态 , 则不会发出声音 , 会直接调用appDelegate的代理方法didReceiveRemoteNotification(didReceiveRemoteNotification:fetchCompletionHandler:)

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法didReceiveRemoteNotification

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了application:didReceiveRemoteNotification:fetchCompletionHandler:这个方法,则还会调用这个方法

注:didReceiveRemoteNotification指以下两个方法。两个方法互斥。在两方法都实现的情况下方法2优先级高

1. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo

2. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler


iOS10使用

1 . 如果应用处于 (1) 状态 , 会发出声音 , 会直接调用appDelegate的代理方法userNotificationCenter:willPresentNotification:withCompletionHandler

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法

userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler这个方法,则还会调用这个方法



2. 静默推送及app的状态切换

        在大多数情况下,启动一个app后都是进入前台,比如我们点击应用图标或点推送通知来启动应用。其实app在某些后台事件和特定条件下是可以直接启动到后台(launch into the background)的。

    2.1 应用状态之一Suspended

        这种状态其实和Background类似,而且从用户角度讲应用现在看起来确实是在“后台”,但它和Background状态不同的是Suspended下已经不能执行代码了。应用何时会进Suspended就是玄学了,这是由iOS系统自动控制的,而且不会有任何回调,可以看到UIApplicationDelegate里并没有像applicationWillBecomeSuspended:这种东西。这种状态下的应用虽然还在内存中,但是一旦设备内存吃尽,比如开了炉石传说的游戏,那么系统就会优先干掉(文档上用的是purge这个词)处于Suspended状态的应用,而且也不会有回调。

    2.2 应用启动到前台的生命周期(以点击应用图标开始)

    AppDelegate中走的回调方法 

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidBecomeActive:


    静默推送可以使应用启动到后台

        前提是应用先被退到后台,过一段时间被系统移入Suspended状态,然后又被系统在内存吃紧时回收了内存(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),在这以后,该应用收到静默推送即会启动应用到后台。

    AppDelegate中走的回调方法变为

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidEnterBackground:


        这个过程中,系统不会显示应用的window,就是说我们不会看到手机屏幕上突然鬼畜一下应用启动,但是应用的第一屏会被加载和渲染,比如你的window.rootViewController是一个TabBarController,那么它及其默认选中的selectedViewController都会被加载和渲染。这是因为系统认为在后台执行完任务后可能会有UI上的更新,所以在applicationDidEnterBackground:方法执行结束后便会有个快速的截图,来更新用户双击Home时看到的那个应用截图。


3. 收到静默推送时的后续该如何处理。

        application:didReceiveRemoteNotification:fetchCompletionHandler:

        这是应用收到静默推送的回调方法,我们最多有30s的时间来处理数据,比如静默推送表示某个列表或资源有更新,你可以在此处下载数据,在下载处理完数据后需要尽快调用completionHandler(...)告诉系统处理完毕。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

[Downloader fetchData:^(id x){ // 处理数据,更新UI 等

completionHandler(UIBackgroundFetchResultNewData);

}];

}


        如果这次是启动到后台的情况,调用completionHandler(...)后会使应用马上进入之前的状态。那就有可能遇到这样的问题:很多时候我们需要在启动时发送一堆业务上的API请求,如果这次静默推送没有数据需要下载和处理,就会刚把启动处的API请求发出,就调用了completionHandler(...),导致发出的这些请求在下次打开应用时显示超时。这种情况下我们可以强行延时下completionHandler(...)的调用,来保证能在这次收到那些API的返回。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

completionHandler(UIBackgroundFetchResultNoData);

});



4. 静默推送


应用想收到静默推送需要满足的条件:

1.应用在前台/后台 (应用被杀死就收不到了)

2.应用实现了

application:didReceiveRemoteNotification:fetchCompletionHandler:/application:didReceiveRemoteNotification:

3. 消息定义时需设置:"content-available" = 1

流程:

  1. 移动端注册消息,向APNs服务器获取deviceToken,并提交给后台保存;

  2. 后台定义消息,并推送给APNs服务器。APNs根据deviceToken做分发。

  3. 移动端收到推送消息后的逻辑处理。

消息定义示例:

特殊说明:

1. APNS去掉alert、badge、sound字段实现静默推送,增加增加字段"content-available":1,也可以在后台做一些事情。

//静默推送消息格式

{

"aps":{

"alert":"",

"content-available":1

},

"userInfo":"test"

}


*/

小结:

1.应用在后台/前台/被杀死,都可以收到普通的远程推送

2.应用在后台/前台时,可以通过静默推送,修改一些数据

3.应用被杀死时(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),可以通过Background Fetch短时间唤醒应用



作者:Aliv丶Zz
链接:https://www.jianshu.com/p/0275d9a9592b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS获取设备的网络状态(已适配iOS13,iOS14无变化)

iOS
前言 小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。 实现 因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一...
继续阅读 »

前言


小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。


实现


因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一尺,魔高一尺。开发者总会有办法获取自己想要的东西。


1.网络状态获取


获取当前的网络类型

获取当前的网络类型是通过获取状态栏,然后遍历状态栏的视图完成的。

先导入头文件,如下:

#import "AppDelegate.h"

实现方法如下:

+ (NSString *)getNetworkType
{
UIApplication *app = [UIApplication sharedApplication];
id statusBar = nil;
// 判断是否是iOS 13
NSString *network = @"";
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop

if (statusBar) {
// UIStatusBarDataCellularEntry
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id _wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
id _cellularEntry = [currentData valueForKeyPath:@"cellularEntry"];
if (_wifiEntry && [[_wifiEntry valueForKeyPath:@"isEnabled"] boolValue]) {
// If wifiEntry is enabled, is WiFi.
network = @"WIFI";
} else if (_cellularEntry && [[_cellularEntry valueForKeyPath:@"isEnabled"] boolValue]) {
NSNumber *type = [_cellularEntry valueForKeyPath:@"type"];
if (type) {
switch (type.integerValue) {
case 0:
// 无sim卡
network = @"NONE";
break;
case 1:
network = @"1G";
break;
case 4:
network = @"3G";
break;
case 5:
network = @"4G";
break;
default:
// 默认WWAN类型
network = @"WWAN";
break;
}
}
}
}
}else {
statusBar = [app valueForKeyPath:@"statusBar"];

if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
if ([[wifiEntry valueForKey:@"_enabled"] boolValue]) {
network = @"WIFI";
}else {
// 卡1:
id cellularEntry = [currentData valueForKey:@"cellularEntry"];
// 卡2:
id secondaryCellularEntry = [currentData valueForKey:@"secondaryCellularEntry"];

if (([[cellularEntry valueForKey:@"_enabled"] boolValue]|[[secondaryCellularEntry valueForKey:@"_enabled"] boolValue]) == NO) {
// 无卡情况
network = @"NONE";
}else {
// 判断卡1还是卡2
BOOL isCardOne = [[cellularEntry valueForKey:@"_enabled"] boolValue];
int networkType = isCardOne ? [[cellularEntry valueForKey:@"type"] intValue] : [[secondaryCellularEntry valueForKey:@"type"] intValue];
switch (networkType) {
case 0://无服务
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"NONE"];
break;
case 3:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"2G/E"];
break;
case 4:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"3G"];
break;
case 5:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"4G"];
break;
default:
break;
}

}
}

}else {

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
network = @"WIFI";
}else if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarStringView")]) {
network = [subview valueForKeyPath:@"originalText"];
}
}
}

}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKeyPath:@"foregroundView"];
NSArray *subviews = [foregroundView subviews];

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"UIStatusBarDataNetworkItemView")]) {
int networkType = [[subview valueForKeyPath:@"dataNetworkType"] intValue];
switch (networkType) {
case 0:
network = @"NONE";
break;
case 1:
network = @"2G";
break;
case 2:
network = @"3G";
break;
case 3:
network = @"4G";
break;
case 5:
network = @"WIFI";
break;
default:
break;
}
}
}
}
}

if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}
获取当前的Wifi信息

获取当前的Wifi信息需要借助系统的SystemConfiguration这个库。
先导入头文件,如下:

#import <SystemConfiguration/CaptiveNetwork.h>

实现方法如下:

#pragma mark 获取Wifi信息
+ (id)fetchSSIDInfo
{
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
id info = nil;
for (NSString *ifnam in ifs) {
info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);

if (info && [info count]) {
break;
}
}
return info;
}
#pragma mark 获取WIFI名字
+ (NSString *)getWifiSSID
{
return (NSString *)[self fetchSSIDInfo][@"SSID"];
}
#pragma mark 获取WIFI的MAC地址
+ (NSString *)getWifiBSSID
{
return (NSString *)[self fetchSSIDInfo][@"BSSID"];
}
获取当前的Wifi信号强度

获取信号强度与获取网络状态有点类似,通过遍历状态栏,从而获取WIFI图标的信号强度。在获取前需注意当前状态是否为WIFI。如下:

+ (int)getWifiSignalStrength{

int signalStrength = 0;
// 判断类型是否为WIFI
if ([[self getNetworkType]isEqualToString:@"WIFI"]) {
// 判断是否为iOS 13
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

id statusBar = nil;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop
if (statusBar) {
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
if ([wifiEntry isKindOfClass:NSClassFromString(@"_UIStatusBarDataIntegerEntry")]) {
// 层级:_UIStatusBarDataNetworkEntry、_UIStatusBarDataIntegerEntry、_UIStatusBarDataEntry

signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
}
}
}else {
UIApplication *app = [UIApplication sharedApplication];
id statusBar = [app valueForKey:@"statusBar"];
if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
// dBm
// int rawValue = [[wifiEntry valueForKey:@"rawValue"] intValue];
}else {
for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
signalStrength = [[subview valueForKey:@"_numberOfActiveBars"] intValue];
}
}
}
}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKey:@"foregroundView"];

NSArray *subviews = [foregroundView subviews];
NSString *dataNetworkItemView = nil;

for (id subview in subviews) {
if([subview isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
dataNetworkItemView = subview;
break;
}
}

signalStrength = [[dataNetworkItemView valueForKey:@"_wifiStrengthBars"] intValue];

return signalStrength;
}
}
}
return signalStrength;
}

2.Reachability的使用

下载开源类Reachability,然后根据文档使用即可(该类把移动网络统称为WWAN):+ (NSString *)getNetworkTypeByReachability

{
NSString *network = @"";
switch ([[Reachability reachabilityForInternetConnection]currentReachabilityStatus]) {
case NotReachable:
network = @"NONE";
break;
case ReachableViaWiFi:
network = @"WIFI";
break;
case ReachableViaWWAN:
network = @"WWAN";
break;
default:
break;
}
if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}

上次发布了这篇文章之后,有人问我,怎么才能获取设备的IP地址呢?在这里,小编附上获取iP地址的方法。
先导入头文件,如下:

#import <ifaddrs.h>
#import <arpa/inet.h>

实现方法,如下:

#pragma mark 获取设备IP地址
+ (NSString *)getIPAddress
{
NSString *address = @"error";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
// 检索当前接口,在成功时,返回0
success = getifaddrs(&interfaces);
if (success == 0) {
// 循环链表的接口
temp_addr = interfaces;
while(temp_addr != NULL) {
// 开热点时本机的IP地址
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"bridge100"]
) {
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
if(temp_addr->ifa_addr->sa_family == AF_INET) {
// 检查接口是否en0 wifi连接在iPhone上
if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// 得到NSString从C字符串
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}
temp_addr = temp_addr->ifa_next;
}
}
// 释放内存
freeifaddrs(interfaces);
return address;
}


收起阅读 »

ios - 真机无法运行

iOS
iOS 开发小记8.10日遇见问题新接手的苹果账号无法真机运行,查询一番以为是证书的问题。登录到苹果的官网发现手机有个7天无效的问题。最终解决的方式是换了个手机 添加到真机运行中就可以了但是无法运行的手机,估计是需要等到七天之后查看结果。七天之后应该是有所变化...
继续阅读 »

iOS 开发小记

8.10日遇见问题新接手的苹果账号无法真机运行,查询一番以为是证书的问题。登录到苹果的官网发现手机有个7天无效的问题。

最终解决的方式是换了个手机 添加到真机运行中就可以了

但是无法运行的手机,估计是需要等到七天之后查看结果。七天之后应该是有所变化

产生这个问题的原因:有大佬解答是因为 苹果账号被封过。导致手机的UUID被标记。换到其他的苹果账号会有这种情况的发生

(我遇到过的是被封过开发者账号的,一个开发者账号被封,里面的测试机也会被苹果标记,再换其他的开发者账号,就可以关联到一起了)

收起阅读 »

Swift 中的热重载

前言    这一年是2040年,我们最新的 MacBook M30X 处理器可以感知到瞬间编译大型 Swift 项目,听起来很神奇,对吧?除此之外,编译代码库只是我们迭代周期的一部分。包括:    1...
继续阅读 »

前言

    这一年是2040年,我们最新的 MacBook M30X 处理器可以感知到瞬间编译大型 Swift 项目,听起来很神奇,对吧?除此之外,编译代码库只是我们迭代周期的一部分。包括:
    1、重新启动它(或将其部署到设备)
    2、导航到您在应用程序中的先前位置
    3、重新生成您需要的数据。
    如果您只需要做一次的话,听起来还不错。但是如果您和我一样,在特别的一天中,对代码库进行 200 - 500 次迭代,该怎么办呢?它增加了。
    有一种更好的方法,被其他平台所接受,并且可以在 Swift/iOS 生态系统中实现。我已经用了十多年了。
    从今天开始,您想每周节省多达 10 小时的工作时间吗?


热重载

    热重载是关于摆脱编译整个应用程序并尽可能避免部署/重新启动周期,同时允许您编辑正在运行的应用程序代码并且能立即看到更改。
    这种流程改进可以每天为您节省数小时的开发时间。我跟踪我的工作一个多月,对我来说,每天节省了 1-2 小时。
    坦白地说,如果每周节省10个小时的开发时间都不能说服您去尝试,那么我认为任何方法都不能说服你。


其他平台在做什么?

    如果您只使用 Apple 平台,您会惊讶地发现有好多平台几十年前已经采用了热重载。无论您是编写 Node 还是任何其他 JS 框架,都有一个使用热重载的设置。Go 也提供了热重载(本博客使用了该特性)
    另一个例子是谷歌的 Flutter 架构,从一开始就设计用于热重载。如果您与从事 Flutter 工作的工程师交谈,你会发现他们最喜欢 Flutter 开发者体验的一点就是能够实时编写他们的应用程序。当我为《纽约时报》写了一个拼字游戏时,我很喜欢它。
    微软最近推出了 Visual Studio 2022,并为 .NET 和 标准 C++ 应用程序提供热重载,在过去的十年中,微软在开发工具和经验方面一直在大杀四方,所以这并不令人惊讶。


苹果生态系统怎么样?

    早在 2014 年推出时,很多人都对 Swift Playgrounds 感到敬畏,因为它们允许我们快速迭代并查看代码的结果,但它们并不能很好地工作,因为它存在崩溃、挂起等问题。不能支持整个iPad环境。
    在它们发布后不久,我启动了一个名为 Objective-C Playgrounds 的开源项目,它比官方 Playgrounds 运行得更快、更可靠。我的想法是设计一个架构/工作流程,利用我已经使用了几年的 DyCI 代码注入工具,该工具已经由 Paul 制作。
    自从 Swift Playgrounds 存在以来,已经过去了八年,而且它们变得更好了,但它们可靠吗?人们是否在使用它们来推动开发?

    SwiftUI 出现了,它是一项了不起的技术(尽管仍然存在错误),它引入了与 Playgrounds 非常相似的 Swift Previews 的想法,它们有什么好处吗?
    类似的故事,当它工作的时候是很好的,但是在更大的项目中,它的工作是不可靠的,而且往往中断的次数比它们工作的次数多。如果你有任何错误,他们不会为你提供调试代码的能力,因此,采用的情况有限。


我们需要等待 Apple 吗?

    如果你关注我一段时间,你就已经知道答案了,绝对不要。毕竟,我的职业生涯是构建普通 Apple 解决方案无法解决的问题:从像 Sourcery 这样的语言扩展、像 Sourcery Pro 这样的 Xcode 改进,再到 LifetimeTracker 以及许多其他开源工具。
    我们可以利用我最初在 2014 Playgrounds 中使用的相同方法。我已经使用它十多年了,并且在数十个 Swift 项目中使用它并取得了巨大的成功!
    许多年前,我从使用 DyCI 切换到 InjectionForXcode,通过利用 LLVM 互操作而不是任何 swizzling ,它的效果更好。它是一个完全免费的开源工具,您可以在菜单栏中运行,它是由多产的工程师 John Holdsworth 创建的。你应该看看他的书 Swift Secrets。
    我意识到 Playgrounds 的方法可能过于笨重,所以今天,我开源了。一个非常专注的名为 Inject 的微型库,与 InjectionForXcode 搭配使用时,将使您的 Apple 开发更加高效和愉快!
    但不要只相信我的话。看看 Alexandra 和 Nate 的反馈,在我将这个工作流程引入 The Browser Company 设置之前,他们已经非常精通了,这使得它更加令人印象深刻。


Inject

    这个小型库是完全通用的,无论您使用 UIKit、 AppKit 还是 SwiftUI,您都可以使用它。
    您无需为生产应用程序添加条件或删除 Inject 代码。它变成了无操作内联代码,将在非调试版本中被编译过程剥离。您可以在每个视图中集成一次,并持续使用数年。
    请参考 GitHub repo中关于配置项目的说明。现在让我们来看看您有哪些工作流程选项。


工作流

    SwiftUI
        只需要两行字就可以使任何 SwiftUI 启用实时编程,而当您这样做时,您将拥有比使用 Swift Previews 更快的工作流程,同时能够使用实际的生产数据。
        这是我的 Sourcery Pro 应用程序的示例,其中加载了我所有的实际数据和逻辑,使我能够即时快速迭代整个应用程序设计,而无需任何重新启动、重新加载或类似的事情。
        看看这个开发工作流程有多快吧,告诉我你宁愿在我每次接触代码时等待Xcode的重新构建和重新部署。


    UIKit / AppKit
        我们需要一种方法来清理标准命令式UI框架的代码注入阶段之间的状态。
        我创建了 Host 的概念并且在这种情况下工作的很好。有两个:

        - Inject.ViewHost
        - Inject.ViewControllerHost

        我们如何集成它?我们把我们想迭代的类包装在父级,因此我们不修改要注入的类型,而是改变父级的调用站点。
        例如,如果你有一个 SplitViewController ,它创建了 PaneA 和 PaneB ,而你想在PaneA 中迭代布局/逻辑代码,你就修改 SplitViewController 中的调用站点。

        paneA = Inject.ViewHost(
            PaneAView(whatever: arguments, you: want)
        )

        这就是你需要做的所有改变。注入现在允许你更改 PaneAView 中的任何东西,除了它的初始化API。这些变化将立即反映在你的应用程序中。


        一个更具体的例子?
        1、我下载了 Covid19 App
        2、添加 -Xlinker -interposable 到 Other Linker Flags
        3、交换了一行 Covid19TabController.swift:L63 行

        从这句:

        let vc = TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content)

        替换为:

        let vc = Inject.ViewControllerHost(TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content))

        现在,我可以在不重新启动应用程序的情况下迭代控制器设计。


这是如何运作的呢?

    Hosts 利用了自动闭包,因此每次您注入代码时,我们都会使用与最初相同的参数创建您类型的新实例,从而允许您迭代任何代码、内存布局和其他所有内容。你唯一不能改变的是你的初始化 API。


逻辑注入如何呢?

    像 MVVM / MVC 这样的标准架构可以获得免费的逻辑注入,重新编译你的类,当方法重新执行时,你已经在使用新代码了。
    如果像我一样,你喜欢 PointFree Composable Architecture,你可能想要注入 reducer 代码。Vanilla TCA 不允许这样做,因为 reducer 代码是一个免费功能,不能直接用注入替换,但我们在 The Browser Company 的分支 支持它。
    当我最初开始咨询 TBC 时,我想要的第一件事是将 Inject 和 XcodeInjection 集成到我们的工作流程中。公司管理层非常支持。
    如果您切换到我们的 TCA 分支(我们保持最新),你可以在 UI 和 TCA 层上使用 Inject 。


它有多可靠?

    没有什么是完美的,但我已经使用它十多年了。它比 Apple 技术(Playgrounds / Previews)可靠得多。
如果您投入时间学习它,它将为您和您的团队节省数千小时!

收起阅读 »

iOS-底层原理 04:NSObject的alloc 源码分析

iOS
主要自定义类的alloc的alloc的源码实现中加一个断点,同时需要暂时关闭断点运行target,断点断在alloc源码的断点,然后继续执行,会出现以下这种现象探索Why【第一步】探索Debug --> Debug Workflow --> 勾选 ...
继续阅读 »

主要NSObject中的alloc是与自定义类的alloc源码流程的区别,以及为什么NSObject中的alloc不走源码工程。

上一篇文章中分析了alloc的源码,这篇文章是作为对上一篇文章的补充,去探索为什么NSObject的alloc方法不走源码工程。

NSObject的alloc无法进入源码的问题

首先在objc4-781可编译源码中的main函数中增加一个NSObject定义的对象,NSObject 和 LGPersong同时加上断点



alloc的源码实现中加一个断点,同时需要暂时关闭断点


运行target,断点断在NSObject部分,打开alloc源码的断点,然后继续执行,会出现以下这种现象


探索Why

【第一步】探索[NSObject alloc]走的是哪步源码

接下来,我们就来探索为什么NSObject的alloc会出现这种情况,首先,

  • 打开Debug --> Debug Workflow --> 勾选 Always Show Disassemly,开启汇编调试

    关闭源码的断点,只留main中的断点,重新运行程序,然后通过下图的汇编可以发现NSObject并没有走 alloc源码,而是走的objc_alloc


然后关闭汇编调试,在全局搜索 objc_alloc,在objc_alloc中加一个断点,先暂时关闭,


重新运行进行调试,断住,然后打开objc_alloc的断点,发现会进入objc_alloc的源码实现,此时查看 cls 是 NSObject


【第二步】探索 NSObject 为什么走 objc_alloc?

首先,我们来看看 NSObject 与 LGPerson的区别

  • NSObject 是iOS中的基类,所有自定义的类都需要继承自NSObject
  • LGPerson 是继承NSObject类的,重写NSObject中的alloc方法

然后根据第一步中汇编的显示,可以看出,NSObject 和 LGPerson 都调用了objc_alloc,所以这里就有两个疑问

  • 为什么NSObject 调用alloc方法 会走到 objc_alloc 源码?
  • 为什么LGPerson中的alloc 会走两次?即调用了alloc,进入源码,然后还要走到 objc_alloc

LGPerson中alloc 走两次 的 Why?

首先,需要在源码中调试,在mainLGPerson加断点,断在LGPerson,再在alloc 、 objc_alloc 和 calloc 源码加断点,运行demo,会断在objc_alloc源码中(重新运行前需要暂时关闭源码中的所有断点)


继续运行,发现LGPerson 第一次的alloc会走到 objc_alloc --> callAlloc方法中最下方的objc_msgSend,表示向系统发送消息



所以由上述调试过程可以得出,LGPerson两次的原因是首先需要去查找sel,以及对应的imp的关系,当前需要查找的是 alloc的方法编号,但是为什么会找到objc_alloc?这个就需要问系统了,肯定是系统在底层做了一些操作。请接着往下看

NSObject中alloc 走到 objc_alloc 的 why?

这部分需要通过 LLVM源码(即llvm-project) 来分析

准备工作:首先需要一份llvm源码

在llvm源码中搜索objc_alloc


搜索shouldUseRuntimeFunctionForCombinedAllocInit,表示版本控制


搜索tryEmitSpecializedAllocInit,非常著名的特殊消息发送,在这里也没有找到 objc_alloc


继续尝试,开启上帝视角,通过alloc字符串搜索,如果还找不到,还可以通过omf_alloc:找到tryGenerateSpecializedMessageSend,表示尝试生成特殊消息发送


然后在这个case中可以找到调用alloc,转而调用了objc_objc的逻辑,其中的关键代码是EmitObjCAlloc


跳转至EmitObjCAlloc的定义可以看到alloc 的处理是调用了 objc_alloc


由此可以得出 NSObject中的alloc 会走到 objc_alloc,其实这部分是由系统级别的消息处理逻辑,所以NSObject的初始化是由系统完成的,因此也不会走到alloc的源码工程中

总结

总结下NSObject中alloc 和自定义类中alloc的调用流程

NSObject


自定义类


作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108480971

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS 底层原理03:objc4-781 源码编译 & 调试

iOS
准备工作环境版本 & 最新objc源码mac OS 10.15Xcode 11.4objc4-781依赖文件下载需要下载以下依赖文件源码编译源码编译就是不断的调试修改源码的问题,主要有以下问题问题一:unable to find sdk 'macosx...
继续阅读 »

准备工作

环境版本 & 最新objc源码

  • mac OS 10.15
  • Xcode 11.4
  • objc4-781

依赖文件下载

需要下载以下依赖文件


源码编译

源码编译就是不断的调试修改源码的问题,主要有以下问题

问题一:unable to find sdk 'macosx.internal'


选择 target -> objc -> Build Settings -> Base SDK -> 选择 macOS 【target中的 objc 和 obc-trampolines都需要更改】


问题二:文件找不到的报错问题

【1】‘sys/reason.h’ file not found


在Apple source的 macOS10.15 --> xnu-6153.11.26/bsd/sys/reason.h 路径自行下载

在objc4-781的根目录下新建CJLCommon文件, 同时在CJLCommon文件中创建sys文件

最后将 reason.h文件拷贝到sys文件中

设置文件检索路径:选择 target -> objc -> Build Settings,在工程的 Header Serach Paths 中添加搜索路径 $(SRCROOT)/CJLCommon

【2】‘mach-o/dyld_priv.h’ file not found

  • CJLCommon文件中 创建 mach-o 文件
  • 找到文件:dyld-733.6 -- include -- mach-o -- dyld_priv.h


拷贝到 mach-o文件中



  • 拷贝到文件后,还需要修改 dyld_priv.h 文件,即在 dyld_priv.h文件顶部加入一下宏:


【3】‘os/lock_private.h’ file not found 和 ‘os/base_private.h’ file not found

  • 在CJLCommon中创建 os文件
  • 找到lock_private.h、base_private.h文件:libplatform-220 --> private --> os --> lock_private.h 、base_private.h,并将文件拷贝至 os 文件中

【4】‘pthread/tsd_private.h’ file not found 和 ‘pthread/spinlock_private.h’ file not found

在CJLPerson中创建 pthread 文件
找到tsd_private.h、spinlock_private.h文件,h文件路径为:libpthread-416.11.1 --> private --> tsd_private.h、spinlock_private.h,并拷贝到 pthread文件


【5】‘System/machine/cpu_capabilities.h’ file not found

创建 System -- machine 文件
找到 cpu_capabilities.h文件拷贝到 machine文件,h文件路径为:xnu6153.11.26 --> osfmk --> machine --> cpu_capabilities.h


【6】os/tsd.h’ file not found

找到 tsd.h文件,拷贝到os文件, h文件路径为:xnu6153.11.26 --> libsyscall --> os --> tsd.h


【7】‘System/pthread_machdep.h’ file not found

  • 这个地址下载pthread_machdep.h文件,h文件路径为:Libc-583/pthreads/pthread_machdep.h
  • 将其拷贝至system文件中


【8】‘CrashReporterClient.h’ file not found

导入下载的还是报错,可以通过以下方式解决
- 需要在 Build Settings -> Preprocessor Macros 中加入:LIBC_NO_LIBCRASHREPORTERCLIENT
- 或者下载我给大家的文件CrashReporterClient,这里面我们直接更改了里面的宏信息 #define LIBC_NO_LIBCRASHREPORTERCLIENT

【9】‘objc-shared-cache.h’ file not found

文件路径为:dyld-733.6 --> include --> objc-shared-cache.h


  • 将h文件报备制拷贝到CJLCommon

【10】Mismatch in debug-ness macros

注释掉objc-runtime.mm中的#error mismatch in debug-ness macros


【11】’_simple.h’ file not found

文件路径为:libplatform-220 --> private --> _simple.h


  • 将文件拷贝至CJLCommon
【12】‘kern/restartable.h’ file not found

  • 在CJLCommon中创建kern 文件
  • 找到 h文件,路径为xnu-6153.11.26 --> osfmk --> kern -->restartable.h


【13】‘Block_private.h’ file not found

找到 h 文件,文件路径为libclosure-74 --> Block_private.h



拷贝至CJLCommon目录

【14】libobjc.order 路径问题

问题描述为:can't open order file: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/AppleInternal/OrderFiles/libobjc.order

  • 选择 target -> objc -> Build Settings
  • 在工程的 Order File 中添加搜索路径 $(SRCROOT)/libobjc.order



【14】Xcode 脚本编译问题
问题描述为:/xcodebuild:1:1: SDK "macosx.internal" cannot be located.

选择 target -> objc -> Build Phases -> Run Script(markgc)
把脚本文本 macosx.internal 改成 macosx


编译调试

新建一个target :CJLTest



绑定二进制依赖关系



源码调试

自定义一个CJLPerson类

image

在main.m中 创建 CJLPerson的对象,进行源码调试



作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108435967

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

现今 Swift 包中的二进制目标

一、目录      1、理解二进制在 Swift 中的演变    2、命令行工具相关    3、结论二、前言    在 iOS 和...
继续阅读 »

一、目录  

    1、理解二进制在 Swift 中的演变
    2、命令行工具相关
    3、结论

二、前言

    在 iOS 和 macOS 开发中, Swift 包现在变得越来越重要。Apple 已经努力推动桥接那些缝隙,并且修复那些阻碍开发者的问题,例如阻碍开发者将他们的库和依赖由其他诸如 Carthage 或 CocoaPods依赖管理工具迁移到 Swift 包依赖管理工具的问题,例如没有能力添加构建步骤的问题。这对任何依赖一些代码生成的库来说都是破坏者,比如,协议和 Swift 生成。


    1、理解二进制在 Swift 中的演变

        为了充分理解 Apple 的 Swift 团队在二进制目标和他们引入的一些新 API 方面采取的一些步骤,我们需要理解它们从何而来。在后续的部分中,我们将调研 Apple 架构的演变,以及为什么二进制目标的 API 在过去几年中逐渐形成的,特别是自 Apple 发布了自己的硅芯片之后。


        胖二进制和 Frameworks 框架

        如果你曾必须处理二进制依赖,或者你曾创建一个属于你自己的可执行文件,你将会对 胖二进制 这个术语感到熟悉。这些被扩展(或增大)的可执行文件,是包含了为多个不同架构原生构建的切片。这允许库的所有者分发一个运行在所有预期的目标架构上的单独的二进制。
        当源码不能被暴露或当处理非常庞大的代码仓库时,预编译库成为可执行文件非常有意义,因为预编译源码以及以二进制文件分发他们,将节省构建程序在他们的应用上的构建时间。
        Pods 是一个非常好的例子,当开发者发现他们自己没必要构建那些非常少改动的依赖。这是一个很共通的问题,它激发了诸如 cocoapods-binary之类的项目,该项目预编译了 pod 依赖项以减少客户端的构建时间。


        Frameworks 框架

        嵌入静态二进制文件可能对应用程序来说已经足够了,但如果需要某些资源(如 assets 或头文件),则需要将这些资源与包含所有切片的 胖二进制文件 捆绑在一起,形成所谓的 frameworks 文件。
这就是诸如 Google Cast[5] 之类的预编译库在过渡到使用 xcframework 进行分发之前所做的事情 —— 下一节将详细介绍这种过渡的原因。
        到目前为止,一切都很好。如果我们要为分发预编译一个库,那么胖二进制文件听起来很理想,对吧?并且,如果我们需要捆绑一些其他资源,我们可以只使用一个 frameworks。一个二进制来统治他们所有!


        XCFrameworks 框架

        好吧,不完全是。胖二进制文件有一个大问题,那就是你不能有两个架构相同但命令/指令不同的切片。这曾经很好,因为设备和模拟器的架构总是不同的,但是随着 Apple Silicon 计算机 (M1) 的推出,模拟器和设备共享相同的架构 (arm64),但具有不同的加载器命令。这与面向未来的二进制目标相结合,正是 Apple 引入 XCFrameworks 的原因。
        XCFrameworks现在允许将多个二进制文件捆绑在一起,解决了 M1 Mac 引入的设备和模拟器冲突架构问题,因为我们现在可以为每个用例提供包含相关切片的二进制文件。事实上,如果我们需要,我们可以走得更远,例如,在同一个 xcframework 中捆绑一个包含 iOS 目标的 UIKit 接口的二进制文件和一个包含 macOS 的 AppKit 接口的二进制文件,然后让 Xcode 基于期望的目标架构决定使用哪一个。
        在 Swift 包中,那先能够以 binaryTarget 被包含进项目的,能够在包中被引入任意其他目标。这相同的操作同样适用于 frameworks。


     2、命令行工具相关

        由于 Swift 5.6 版本中引入了用于 Swift 包管理器的 可扩展构建工具[9] ,因此可以在构建过程中的不同时间执行命令。

        这是 iOS 社区长期以来一直强烈要求的事情,例如格式化源代码、代码生成甚至收集公制代码库的指标。Swift 5.6 中所有这些所谓的 插件最终都需要调用可执行文件来执行特定任务。这是二进制文件再次在 Swift 包中参与的地方。
        在大多数情况下,对于我们 iOS 开发人员来说,这些工具将来自同时支持 macOS 的不同架构切片 —— Apple Silicon 的 arm64 架构和 Intel Mac 的 x86_64 架构。开发者工具如, SwiftLint或 SwiftGen 正是这种案例。在这种情况下,可以使用包含可执行文件(本地或远程)的 .zip 文件的路径创建新的二进制目标。


        Artifact Bundles

        到目前为止,命令行工具所采用的方法仅适用于 macOS 架构。但我们不能忘记,Linux 机器也支持 Swift 包。这意味着如果要同时支持 M1 macs (arm64) 和 Linux arm64 机器,上面的胖二进制方法将不起作用 —— 请记住,二进制不能包含具有相同架构的多个切片。在这个阶段可能有人会想,我们可以不只使用 xcframeworks 吗?不,因为它们在 Linux 操作系统上不受支持!
        Apple 已经考虑到这一点,除了引入 可扩展构建工具[13] 之外,Artifact Bundles和对二进制目标的其他改进也作为 Swift 5.6 的一部分发布。
        工件包(Artifact Bundles) 是包含 工件 的目录。这些工件需要包含支持架构的所有不同二进制文件。二进制文件和支持的架构的路径是使用清单文件 (info.json) 指定的,该文件位于 Artifact Bundle 目录的根目录中。你可以将此清单文件视为一个地图或指南,以帮助 Swift 确定哪些可执行文件可用于哪种架构以及可以在哪里找到它们。


        以 SwiftLint 为例

        SwiftLint 在整个社区中被广泛用作 Swift 代码的静态代码分析工具。由于很多人都非常渴望让这个插件在他们的 SwiftPM 项目中运行,我认为这将是一个很好的例子来展示我们如何将分发的可执行文件从他们的发布页面变成一个与 macOS 架构和 Linux arm64 兼容的工件包。
        让我们从下载两个可执行文件(macOS 和 Linux)开始。
        至此,bundle的结构就可以创建好了。为此,创建一个名为 swiftlint.artifactbundle 的目录并在其根目录添加一个空的 info.json:

        mkdir swiftlint.artifactbundle

        touch swiftlint.artifactbundle/info.json

        现在可以使用 schemaVersion 填充清单文件,这可能会在未来版本的工件包和具有两个变体的工件中发生变化,这将很快定义:

        {

            "schemaVersion": "1.0",
            "artifacts": {
                "swiftlint": {
                    "version": "0.47.0", # The version of SwiftLint being used
                    "type": "executable",
                    "variants": [
                    ]
                },
            }
        }

        需要做的最后一件事是将二进制文件添加到包中,然后将它们作为变体添加到 info.json 文件中。让我们首先创建目录并将二进制文件放入其中(macOS 的一个在 swiftlint-macos/swiftlint,Linux 的一个在 swiftlint-linux/swiftlint)。
        添加这些之后,可以在清单文件中变量:

        {
            "schemaVersion": "1.0",
            "artifacts": {
                "swiftlint": {
                    "version": "0.47.0", # The version of SwiftLint being used
                    "type": "executable",
                    "variants": [
                    {
                        "path": "swiftlint-macos/swiftlint",
                        "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"]
                    },
                    {
                        "path": "swiftlint-linux/swiftlint",
                        "supportedTriples": ["x86_64-unknown-linux-gnu"]
                    },
                    ]
                },
            }
        }

        为此,需要为每个变量指定二进制文件的相对路径(从工件包目录的根目录)和支持的三元组。如果您不熟悉 目标三元组,它们是一种选择构建二进制文件的架构的方法。请注意,这不是 主机(构建可执行文件的机器)的体系结构,而是 目标 机器(应该运行所述可执行文件的机器)。

        这些三元组具有以下格式: ---- 并非所有字段都是必需的,如果其中一个字段未知并且要使用默认值,则可以省略或替换为 unknown 关键字。
        可执行文件的架构切片可以通过运行 file 找到,这将打印捆绑的任何切片的供应商、系统和架构。在这种情况下,为这两个命令运行它会显示:


        swiftlint-macos/swiftlint


        swiftlint: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]

        swiftlint (for architecture x86_64): Mach-O 64-bit executable x86_64

        swiftlint (for architecture arm64): Mach-O 64-bit executable arm64


        swiftlint-linux/swiftlint


        -> file swiftlint
        swiftlint: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped


       这带来了上面显示的 macOS 支持的两个三元组(x86_64-apple-macosx、arm64-apple-macosx)和 Linux 支持的一个三元组(x86_64-unknown-linux-gnu)。

        与 XCFrameworks 类似,工件包也可以通过使用 binaryTarget 包含在 Swift 包中。


    3、结论

        简而言之,我们可以总结 2022 年如何在 Swift 包中使用二进制文件的最佳实践,如下所示:

        1、如果你需要为你的 iOS/macOS 项目添加预编译库或可执行文件,您应该使用 XCFramework,并为每个用例(iOS 设备、macOS 设备和 iOS 模拟器)包含单独的二进制文件。
        2、如果你需要创建一个插件并运行一个可执行文件,你应该将其嵌入为一个工件包,其中包含适用于不同支持架构的二进制文件。

收起阅读 »

AFNetworking源码探究 —— UIKit相关之UIProgressView+AFNetworking分类

iOS
下面我们先看一下接口的API/** This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide...
继续阅读 »

接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end


该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。

runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。

接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

转载自:https://cloud.tencent.com/developer/article/1872398





收起阅读 »

AFNetworking源码探究(二十五) —— UIKit相关之UIRefreshControl+AFNetworking分类

iOS
上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。接口API下面我们先看一下接口的API/** This ...
继续阅读 »

回顾

上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。


接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end

该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。


runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。


接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

本文转载自腾讯社区:作者conanma  https://cloud.tencent.com/developer/article/1872401



收起阅读 »

AFNetworking源码探究 —— UIKit相关之AFAutoPurgingImageCache缓存

iOS
回顾上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。接口API按照老惯例,我们还是先看一下接口API文档。这个接口...
继续阅读 »

回顾

上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。


接口API

按照老惯例,我们还是先看一下接口API文档。这个接口文档包括三个部分,两个协议一个类。

  • 协议AFImageCache
  • 协议AFImageRequestCache
  • AFAutoPurgingImageCache

1. AFImageCache

这个协议包括四个方法

/**
Adds the image to the cache with the given identifier.

@param image The image to cache.
@param identifier The unique identifier for the image in the cache.
*/
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;

/**
Removes the image from the cache matching the given identifier.

@param identifier The unique identifier for the image in the cache.

@return A BOOL indicating whether or not the image was removed from the cache.
*/
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;

/**
Removes all images from the cache.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeAllImages;

/**
Returns the image in the cache associated with the given identifier.

@param identifier The unique identifier for the image in the cache.

@return An image for the matching identifier, or nil.
*/
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;

该协议定义了包括加入、移除、获取缓存中的图片。

2. AFImageRequestCache

该协议包含下面几个方法,这里注意这个协议继承自协议AFImageCache

@protocol AFImageRequestCache <AFImageCache>

/**
Asks if the image should be cached using an identifier created from the request and additional identifier.

@param image The image to be cached.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not the image should be added to the cache. YES will cache, NO will prevent caching.
*/
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Adds the image to the cache using an identifier created from the request and additional identifier.

@param image The image to cache.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.
*/
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Removes the image from the cache using an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Returns the image from the cache associated with an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return An image for the matching request and identifier, or nil.
*/
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

@end

根据请求和标识对图像进行是否需要缓存、加入到缓存或者移除缓存等进行操作。

3. AFAutoPurgingImageCache

这个是这个类的接口,大家注意下这个类遵循协议AFImageRequestCache

/**
The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the internal access date of the image is updated.
*/
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>

/**
The total memory capacity of the cache in bytes.
*/
// 内存缓存总的字节数
@property (nonatomic, assign) UInt64 memoryCapacity;

/**
The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory capacity drops below this limit.
*/
// 以字节为单位清除后的首选内存使用情况。 在清除过程中,图像将被清除,直到内存容量降至此限制以下。
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;

/**
The current total memory usage in bytes of all images stored within the cache.
*/
// 当前所有图像内存缓存使用的总的字节数
@property (nonatomic, assign, readonly) UInt64 memoryUsage;

/**
Initialies the `AutoPurgingImageCache` instance with default values for memory capacity and preferred memory usage after purge limit. `memoryCapcity` defaults to `100 MB`. `preferredMemoryUsageAfterPurge` defaults to `60 MB`.
// 初始化,memoryCapcity为100M,preferredMemoryUsageAfterPurge为60M

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)init;

/**
Initialies the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage
after purge limit.

@param memoryCapacity The total memory capacity of the cache in bytes.
@param preferredMemoryCapacity The preferred memory usage after purge in bytes.

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;

@end

内存中图像缓存中的AutoPurgingImageCache用于存储图像到给定内存容量。 达到内存容量时,图像缓存按上次访问日期排序,然后最旧的图像不断清除,直到满足清除后的首选内存使用量。 每次通过缓存访问图像时,图像的内部访问日期都会更新。


AFAutoPurgingImageCache接口及初始化

从接口描述中我们可以看出来,类的初始化规定了内存总的使用量以及清楚以后的内存最优大小。

- (instancetype)init {
return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
if (self = [super init]) {
self.memoryCapacity = memoryCapacity;
self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
self.cachedImages = [[NSMutableDictionary alloc] init];

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];

}
return self;
}

我们看一下这个初始化方法中都做了什么事情

设置置缓存图像的字典

self.cachedImages = [[NSMutableDictionary alloc] init];

常见和UUID关联的并发队列

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

增加移除所有图像的通知

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

这里就是在上面生成的队列中,清空数组,重置一些属性值。


AFCachedImage接口及初始化

这里我们就看一下AFCachedImage的接口及初始化。

@interface AFCachedImage : NSObject

@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, assign) UInt64 totalBytes;
@property (nonatomic, strong) NSDate *lastAccessDate;
@property (nonatomic, assign) UInt64 currentMemoryUsage;

@end

- (instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
if (self = [self init]) {
self.image = image;
self.identifier = identifier;

CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
CGFloat bytesPerPixel = 4.0;
CGFloat bytesPerSize = imageSize.width * imageSize.height;
self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
self.lastAccessDate = [NSDate date];
}
return self;
}

这个初始化方法里面初始化图像的字节数,并更新上次获取数据的时间。


协议方法的实现

1. AFImageCache协议的实现

将图像根据标识添加到内存

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}

这里用到了两个阻塞

第一个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

这里的作用其实很清楚,就是先根据image和identify实例化AFCachedImage对象。然后在字典中根据identifier查看是否有AFCachedImage对象,如果有的话,那么就减小当前使用内存的值。并将前面实例化的AFCachedImage对象存入字典中,并增加当前使用内存的值。

第二个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});

这里完成的功能是,首先判断如果当前内存使用量大于内存总量,那么就需要清理了,这里需要计算需要清理多少内存,就是当前内存值 - 最优内存值。然后sortedImages实例化字典中所有的图片,并对这些图片进行按照时间的排序,遍历这个排序后的数组,逐一从字典中移除,终止条件就是移除的字节数大于上面计算的要清除的字节数值。最后,更新下当前内存使用的值。

根据指定标识将图像移出内存

- (BOOL)removeImageWithIdentifier:(NSString *)identifier;
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
if (cachedImage != nil) {
[self.cachedImages removeObjectForKey:identifier];
self.currentMemoryUsage -= cachedImage.totalBytes;
removed = YES;
}
});
return removed;
}

这个还是很好理解的,在定义的并行队列中,取出identifier对应的AFCachedImage对象,然后从字典中移除,并更新当前内存的值。

从内存中移除所有的图像

- (BOOL)removeAllImages;
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

其实就是一句话,清空字典,更新当前内存使用值。

根据指定的标识符从内存中获取图像

- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
__block UIImage *image = nil;
dispatch_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
image = [cachedImage accessImage];
});
return image;
}
- (UIImage*)accessImage {
self.lastAccessDate = [NSDate date];
return self.image;
}

其实就是从字典中取值,并更新上次获取图像的时间。

2. AFImageRequestCache协议的实现

根据请求和标识符将图像加入到内存

- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
[self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
NSString *key = request.URL.absoluteString;
if (additionalIdentifier != nil) {
key = [key stringByAppendingString:additionalIdentifier];
}
return key;
}

这里其实是调用上面我们讲过的那个根据identifier取出AFCachedImage对象的那个方法。不过下面这个identifier是通过调用下面这个方法生成的。、

根据请求和标识符将图像移出内存

- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法移除对应的图像。

根据请求和标识符获取内存中图像

- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法获取对应的图像。

是否将图像缓存到内存

- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier {
return YES;
}

这里就是写死的,默认就是需要进行缓存。

后记

本篇主要讲述了关于图像缓存方面的内容,包括使用标识符或者请求进行图像相关的缓存操作

本文转载自:https://cloud.tencent.com/developer/article/1872403

收起阅读 »

React Native ART

Android自带ART,不用导入。iOS要使用需要使用xcode打开react native 的ios目录,

1、使用xcode中打开react-native中的ios项目,选中‘Libraries’目录 ——> 右键选择‘Add Files to 项目名称’ ——> 'node_modules/react-native/Libraries/ART/ART.xcodeproj' 添加;


2、选中项目根目录 ——> 点击’Build Phases‘ ——> 点击‘Link Binary With Libraries’ ——> 点击左下方‘+’ ——> 选中‘libART.a’添加。


感谢奋斗的orange 提供,转载原文http://blog.csdn.net/u010940770/article/details/71126700

如果要使用svg作为渲染,使用react-native-art-svg

以下是个人记录:

1. svg的设计要使用局中描边;

2. 画扇形

import React from 'react'

import {

View,

ART

} from 'react-native'

const {Surface} = ART;

import Wedge from './Wedge'

export default class Fan extends React.Component{

render(){

return(

outerRadius={50}

startAngle={0}

endAngle={60}

originX={50}

originY={50}

fill="blue"/>

)

}

}



iOS block与__block、weak、__weak、__strong

iOS

首先需要知道:

block,本质是OC对象,对象的内容,是代码块。
封装了函数调用以及函数调用环境。

block也有自己的isa指针,依据block的类别不同,分别指向
__NSGlobalBlock __ ( _NSConcreteGlobalBlock )
__NSStackBlock __ ( _NSConcreteStackBlock )
__NSMallocBlock __ ( _NSConcreteMallocBlock )
需要注意是,ARC下只存在__NSGlobalBlock和__NSMallocBlock。
通常作为参数时,才可能是栈区block,但是由于ARC的copy作用,会将栈区block拷贝到堆上。
通常不管作为属性、参数、局部变量的block,都是__NSGlobalBlock,即使block内部出现了常量、静态变量、全局变量,也是__NSGlobalBlock,
除非block内部出现其他变量,auto变量或者对象属性变量等,就是__NSMallocBlock

为什么block要被拷贝到堆区,变成__NSMallocBlock,可以看如下链接解释:Ios开发-block为什么要用copy修饰

对于基础数据类型,是值传递,修改变量的值,修改的是a所指向的内存空间的值,不会改变a指向的地址。

对于指针(对象)数据类型,修改变量的值,是修改指针变量所指向的对象内存空间的地址,不会改变指针变量本身的地址
简单来说,基础数据类型,只需要考虑值的地址,而指针类型,则需要考虑有指针变量的地址和指针变量指向的对象的地址

以变量a为例

1、基础数据类型,都是指值的地址

1.1无__block修饰,

a=12,地址为A
block内部,a地址变B,不能修改a的值
block外部,a的地址依旧是A,可以修改a的值,与block内部的a互不影响
内外a的地址不一致

1.2有__block修饰

a=12,地址为A
block内部,地址变为B,可以修改a的值,修改后a的地址依旧是B
block外部,地址保持为B,可以修改a的值,修改后a的地址依旧是B

2、指针数据类型

2.1无__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,不能修改a指向的对象地址
block外部,a指针变量的地址为A,指向的对象地址为B,可以修改a指向的对象地址,
block外部修改后,
外部a指针变量的地址依旧是A,指向的对象地址变为D
内部a指针变量的地址依旧是C,指向的对象地址依旧是B

2.1有__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block外部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block内外,或者另一个block中,无论哪里修改,a指针变量地址都保持为C,指向的对象地址保持为修改后的一致

block内修改变量的实质(有__block修饰):

block内部能够修改的值,必须都是存放在堆区的。
1、基础数据类型,__block修饰后,调用block时,会在堆区开辟新的值的存储空间,
指针数据类型,__block修饰后,调用block时,会在堆区开辟新的指针变量地址的存储空间

2、并且无论是基础数据类型还是指针类型,block内和使用block之后,变量的地址所有地址(包括基础数据类型的值的地址,指针类型的指针变量地址,指针指向的对象的地址),都是保持一致的
当然,只有block进行了真实的调用,才会在调用后发生这些地址的变化

另外需要注意的是,如果对一个已存在的对象(变量a),进行__block声明另一个变量b去指向它,
a的指针变量地址为A,b的指针变量会是B,而不是A,
原因很简单,不管有没__block修饰,不同变量名指向即使指向同一个对象,他们的指针变量地址都是不同的。

__weak,__strong

两者本身也都会增加引用计数。
区别在于,__strong声明,会在作用域区间范围增加引用计数1,超过其作用域然后引用计数-1
而__weak声明的变量,只会在其使用的时候(这里使用的时候,指的是一句代码里最终并行使用的次数),临时生成一个__strong引用,引用+次数,一旦使用使用完毕,马上-次数,而不是超出其作用域再-次数

    NSObject *obj = [NSObject new];
NSLog(@"声明时obj:%p, %@, 引用计数:%ld",&obj, obj, CFGetRetainCount((__bridge CFTypeRef)(obj)));
__weak NSObject *weakObj = obj;
NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));
NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

声明时obj:0x16daa3968, , 引用计数:1
声明时weakObj:0x16daa3960, ,, , 引用计数:5
声明后weakObj引用计数:2

这个5,是因为obj本来计数是1,

    NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句代码打印5,是因为除去&weakObj(&这个不是使用weakObj指向的对象,而只是取weakObj的指针变量地址,所以不会引起计数+1),另外还使用了4次weakObj,导致引用计数+4

   NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句打印2,说明上一句使用完毕后,weakObj引用增加的次数会马上清楚,重新变回1,而这句使用了一次weakObj,加上obj的一次引用,就是2了

__weak 与 weak

通常,__weak是单独为某个对象,添加一条弱引用变量的。
weak则是property属性里修饰符。

LGTestBlockObj *testObj = [LGTestBlockObj new];
self.prpertyObj = testObj;
__weak LGTestBlockObj *weakTestObj = testObj;
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), self.prpertyObj,self.prpertyObj,self.prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(self.prpertyObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), _prpertyObj,_prpertyObj,_prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"prpertyObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"weakTestObj:%p, %@,%@, %@, 引用计数:%ld",&weakTestObj, weakTestObj,weakTestObj,weakTestObj, CFGetRetainCount((__bridge CFTypeRef)(weakTestObj)));

prpertyObj:0x1088017b0, ,, , 引用计数:2
prpertyObj:, 引用计数:2
testObj:, 引用计数:2
weakTestObj:0x16b387958, ,, , 引用计数:6

待补充...

Block常见疑问收录

1、block循环引用

通常,block作为属性,并且block内部直接引用了self,就会出现循环引用,这时就需要__weak来打破循环。

2、__weak为什么能打破循环引用?

一个变量一旦被__weak声明后,这个变量本身就是一个弱引用,只有在使用的那行代码里,才会临时增加引用结束,一旦那句代码执行完毕,引用计数马上-1,所以看起来的效果是,不会增加引用计数,block中也就不会真正持有这个变量了

3、为什么有时候又需要使用__strong来修饰__weak声明的变量?

在block中使用__weak声明的变量,由于block没有对该变量的强引用,block执行的过程中,一旦对象被销毁,该变量就是nil了,会导致block无法继续正常向后执行。
使用__strong,会使得block作用区间,保存一份对该对象的强引用,引用计数+1,一旦block执行完毕,__strong变量就会销毁,引用计数-1
比如block中,代码执行分7步,在执行第二步时,weak变量销毁了,而第五步要用到weak变量。
而在block第一步,可先判断weak变量是否存在,如果存在,加一个__strong引用,这样block执行过程中,就始终存在对weak变量的强引用了,直到block执行完毕

4、看以下代码,obj对象最后打印的引用计数是多少,为什么?

    NSObject *obj = [NSObject new];
void (^testBlock)(void) = ^{
NSLog(@"%@",obj);
};
NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

最后的打印的是3
作为一个局部变量的block,由于引用了外部变量(非静态、常量、全局),定义的时候其实是栈区block,但由于ARC机制,使其拷贝到堆上,变成堆block,所以整个函数执行的过程中,实际上该block,存在两份,一个栈区,一个堆区,这就是使得obj引用计数+2了,加上创建obj的引用,就是3了

5、为什么栈区block要copy到堆上

block:我们称代码块,他类似一个方法。而每一个方法都是在被调用的时候从硬盘到内存,然后去执行,执行完就消失,所以,方法的内存不需要我们管理,也就是说,方法是在内存的栈区。所以,block不像OC中的类对象(在堆区),他也是在栈区的。如果我们使用block作为一个对象的属性,我们会使用关键字copy修饰他,因为他在栈区,我们没办法控制他的消亡,当我们用copy修饰的时候,系统会把该 block的实现拷贝一份到堆区,这样我们对应的属性,就拥有的该block的所有权。就可以保证block代码块不会提前消亡。

iOS安全–浅谈关于iOS加固的几种方法

iOS
关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。 只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破...
继续阅读 »

关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。
只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破解方法。
但是尽管这样,对IOS保护这方面来说,需求还不是很乏,所有基于IOS平台的加固产品也不是很多,目前看到几种关于IOS加固的产品也有做的比较好的。
最开始关于爱加密首创的IOS加密,http://www.ijiami.cn/ios 个人感觉这只是一个噱头而已,因为没有看到具体的工具以及加固应用,所以也不知道它的效果怎么样了。
后来在看雪上面看到一个http://www.safengine.com/mobile/ 有关于IOS加密的工具,但是感觉用起来太麻烦了,而且让产品方也不是很放心,要替换xcode默认的编译器。
不久前看到偶然看到一个白盒加密的应用http://kiwisec.com/ 也下下来试用了一下,感觉要比上面两个从使用上方面了许多,而且考虑的东西也是比较多的。
好了,看了别人做的一些工具,这里大概说下都有哪些加固方法以及大概的实现吧,本人也是刚接触这个方面不就,可能分析的深度没有那么深入,大家就随便听听吧。
现在的加固工具总的来说都是从以下几个方面来做的:
一、字符串加密:
现状:对于字符串来说,程序里面的明文字符串给静态分析提供了极大的帮助,比如说根据界面特殊字符串提示信息,从而定义到程序代码块,或者获取程序使用的一些网络接口等等。
加固:对程序中使用到字符串的地方,首先获取到使用到的字符串,当然要注意哪些是能加密,哪些不能加密的,然后对字符串进行加密,并保存加密后的数据,再在使用字符串的地方插入解密算法,这样就很好的保护了明文字符串。
二、类名方法名混淆
现状:目前市面上的IOS应用基本上是没有使用类名方法名混淆的,所以只要我们使用class-dump把应用的类和方法定义dump下来,然后根据方法名就能够判断很多程序的处理函数是在哪。从而进行hook等操作。
加固:对于程序中的类名方法名,自己产生一个随机的字符串来替换这些定义的类名和方法名,但是不是所有类名,方法名都能替换的,要过滤到系统有关的函数以及类,可以参考下开源项目:https://github.com/Polidea/ios-class-guard
三、程序代码混淆
现状:目前的IOS应用找到可执行文件然后拖到Hopper Disassembler或者IDA里面程序的逻辑基本一目了然。
加固:可以基于Xcode使用的编译器clang,然后在中间层也就是IR实现自己的一些混淆处理,比如加入一些无用的逻辑块啊,代码块啊,以及加入各种跳转但是又不影响程序原有的逻辑。可以参考下开源项目:https://github.com/obfuscator-llvm/obfuscator/ 当然开源项目中也是存在一些问题的,还需自己再去做一些优化工作。
四、加入安全SDK
现状:目前大多数IOS应用对于简单的反调试功能都没有,更别说注入检测,以及其它的一些检测了。
加固:加入SDK,包括多处调试检测,注入检测,越狱检测,关键代码加密,防篡改等等功能。并提供接口给开发者处理检测结果。

当然除了这些外,还有很多方面可以做加固保护的,相信大家会慢慢增加对IOS应用安全的意识,保护好自己的APP。

收起阅读 »

iOS10-iOS15主要适配回顾

iOS
ios15适配1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAp...
继续阅读 »

ios15适配

  • 1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAppearance
  • 2、tableView 增加sectionHeaderTopPadding属性,默认值是UITableViewAutomaticDimension,可能会使tableView sectionHeader多处一段距离,需要设置 为
  • 3、IDFA 请求权限不弹框问题,解决参考iOS15 ATTrackingManager请求权限不弹框
  • 4、iOS15终于迎来了UIButton的这个改动

ios14适配

  • 1、更改了cell布局视图,之前将视图加载在cell上,将会出现contentView遮罩,导致事件无法响应,必须将customView 放在 contentView 上
  • 2、UIDatePicker默认样式不再是以前的,需要设置preferredDatePickerStyle为 UIDatePickerStyleWheels。
  • 3、IDFA必须要用户用户授权处理,否则获取不到IDFA
  • 4、 UIPageControl的变化 具体参考iOS 14 UIPageControl对比、升级与适配

ios13适配

-1、 iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整

  • 2、支持第三方登录必须,就必须Sign In with Apple
  • 3、MPMoviePlayerController 废弃
  • 4、iOS 13 DeviceToken有变化
  • 5、模态弹出默认不再是全屏。
  • 6、私有方法 KVC 不允许使用
  • 7、蓝牙权限需要申请
  • 8、LaunchImage 被弃用
  • 9、新出UIBarAppearance统一配置navigation bars、tab bars、 toolbars等bars的外观。之前设置na bar和tab bar外观的方法可能会无效

ios12适配

  • 1、C++ 标准库libstdc++相关的3个库(libstdc++、libstdc++.6、libstdc++6.0.9 )废弃,使用libc++代替
  • 2、短信 验证码自动填充api
if (@available(iOS 12.0, *)) {
codeTextFiled.textContentType = UITextContentTypeOneTimeCode;
}

ios11适配

  • 1、ViewController的automaticallyAdjustsScrollViewInsets属性被废弃,用scrollView的contentInsetAdjustmentBehavior代替。
  • 2、safeAreaLayoutGuide的引入
  • 3、tableView默认开启了Size-self
  • 4、新增的prefersLargeTitles属性
  • 5、改善圆角,layer新增了maskedCorners属性
  • 6、tableView右滑删除新增api
  • 7、导航条的层级发生了变化。
    ios11适配相关

ios10适配

  • 1、通知统一使用UserNotifications.framework框架
  • 2、UICollectionViewCell的的优化,新增加Pre-Fetching预加载机制
  • 3、苹果加强了对隐私数据的保护,要对隐私数据权限做一个适配,iOS10调用相机,访问通讯录,访问相册等都要在info.plist中加入权限访问描述,不然之前你们的项目涉及到这些权限的地方就会直接crash掉。
  • 4、AVPlayer增加了多个属性,timeControlStatus、
    automaticallyWaitsToMinimizeStalling
  • 5、tabar未选中颜色设置 用 unselectedItemTintColor代替
收起阅读 »

大家好啊,新手一枚,请多关照哈

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

React-Native iOS 列表(ListView)优化方案

在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现...
继续阅读 »

在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现在提供几套可行性方案:

1.利用 Facebook 提供的建议对 ListView 进行优化

Facebook 官方对 ListView 的性能优化做了简单介绍,并提供了以下几个方法:

  • initialListSize
  • 这个属性用来指定我们第一次渲染时,要读取的行数。如果我们想尽可能的快,我们可以设置它为1, 然后可以在后续的帧中,填弃其它的行。每一次读取的行数,由 pageSize 决定.
  • pageSize
  • 在使用了 initialListSize 之后,ListView 根据 pageSize 来决定每一帧读取的行数,默认值为1, 但如果你的的 views 非常的小,并且读取时占的资源很少, 你可以调整这个值,在找到适合你的值。
  • scrollRenderAheadDistance
  • 如果我们的列表有2000个项,而让它一次性读取,它会导致内存和计算资源的耗尽。所以 scrollRenderAhead distance 可以指定,超出当前视图多少,继续宣染。
  • removeClippedSubviews
  • “当它设置为true时,当本地端的superview为offscreen时 ,不在屏幕上显示的子视图offscreen(它的overflow的值为hidden) 会被删除。它可以改善长列表的滚动的性能,默认值为true.
    这对于大的ListViews来说是一个非常重要。在Android, overflow的值通常为hidden. 所以我们并不需要担心它的设置,但是对于iOS来说,你需要设置row Container的样式为overflow: hidden。

在使用了上述方法后,我们可以看到app的内存占用有了一定的下降(加载100张图片时的效果):

使用前


使用后


3.桥接 Native tableview

第二种方法里面,能够比较好的解决屏幕外的 cell 内存问题,但是和 native tableview 相比,并没有 native 的 cell 重用机制完美,那么,我们可以讲 native 的 tableview 桥接到 React-native 中来,让我们可以在 React-Native 中也可以重用 cell

我们创建一些 VirtualView,他只是遵从了 RCTComponent 协议,其实并不是一个真正的 View,我把它形成一个组件,把它 Bridge 到 JS,这就使得,你在写 JSX 的时候,就可以直接用 VirtualView 来去做布局了。在RN里面做布局的时候我们用VirtualView来做布局。但是最终在 insertReactSubview 时,我们把这些 VirtualView 当做数据去处理,通过 VirtualView 和RealView 的对应关系,把它转化成一个真实的 View 对象添加到 TableView 中去。


但是使用这种方法,我们需要将 tableview 的所有常用数据源方法和代理方法都桥接到 React-Native 中来,甚至对于一些 cell 组件,我们也需要自己桥接,并不能像 React-Native 那样使用自己的组件。当我们的需求比较复杂或者需求发生变化时,就需要重新桥接我们的自定义 cell,这样工作量就会比较大。大多数的 cell 里面如果做展示来用的话,Label 和 Image 基本上能够满足大多数的需求了。所以我们现在只是做了 Label 和 Image 的对应工作,但在 RN 的一些官方控件,在这个 view 里面都是没法直接使用的。

4.用 JS 实现一套 cell 重用的逻辑

基于 RN 的 ScrollView,我们也监听 OnScroll(),他往上滑的时候,我们需要把上面的 cellComponent 挪下来,挪到上面去用。但是这个方式最终的效果并不是特别好。

问题在于,如果我们所有的 Cell 都是一样高的,里面的元素不是很多的情况下,性能还相对好一些,我们每次 OnScroll 的时候,他处理的Cell比较少。如果你希望有一个界面滚动能够达到流畅的话,所有的处理都需要在 16ms 内完成,但是这又造成了 onScroll 都要去刷新页面,导致这样的交互会非常非常多,导致你从 JS,到 native 的 bridge 要频繁的通讯,JS 中的很多处理方式都是异步的,使得这个方案的效果没有达到很好的预期。

总结

从上面的几种方案可以看出,方案1、2、3、4都能够比较好的解决列表的性能问题 ,而且各有优缺点,那么,我们在项目开发中该如何应用呢?

  • 当我们在进行列表展示的时候,如果数据量不是特别的庞大(不是无限滚动的),且界面比较复杂的时候,方案1能够比较好的解决性能问题,而且操作起来比较简单,只需要对 listview 的一些属性进行基本设置。
  • 当我们需要展示很多数据的时候(不是无限滚动的),我们可以使用方案2,对那些超出屏幕外的部分,对他进行组件最小化
  • 当我们需要展示大量数据(可以无限滚动的),我们可以通过方案3/4,来达到重用的目的


收起阅读 »

iOS-底层原理 02:alloc & init & new 源码分析

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:分别输出3个对象的内容、内存地址、指针地址,下图是打印结果结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址是相同的,但是对象的指针...
继续阅读 »

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:


分别输出3个对象的内容、内存地址、指针地址,下图是打印结果


结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址相同的,但是对象的指针地址是不同的

%p -> &p1:是对象的指针地址,
%p -> p1: 是对象指针指向的的内存地址

这就是本文需要探索的内容,alloc做了什么?init做了什么?

准备工作

alloc 源码探索

alloc + init 整体源码的探索流程如下


  • 【第一步】首先根据main函数中的LGPerson类的alloc方法进入alloc方法的源码实现(即源码分析开始),

  • 【第二步】跳转至_objc_rootAlloc的源码实现

  • 【第三步】跳转至callAlloc的源码实现

如上所示,在calloc方法中,当我们无法确定实现走到哪步时,可以通过断点调试,判断执行走哪部分逻辑。这里是执行到_objc_rootAllocWithZone

slowpath & fastpath

其中关于slowpathfastpath这里需要简要说明下,这两个都是objc源码中定义的,其定义如下



其中的__builtin_expect指令是由gcc引入的,
1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
2、作用:允许程序员将最有可能执行的分支告诉编译器。
3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。
4、fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大
5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大
6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest

cls->ISA()->hasCustomAWZ()

其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone


【第四步】跳转至_objc_rootAllocWithZone的源码实现


【第五步】跳转至_class_createInstanceFromZone的源码实现,这部分是alloc源码的核心操作,由下面的流程图及源码可知,该方法的实现主要分为三部分
cls->instanceSize:计算需要开辟的内存空间大小
calloc:申请内存,返回地址指针
obj->initInstanceIsa:将 类 与 isa 关联


根据源码分析,得出其实现流程图如下所示:


alloc 核心操作

核心操作都位于calloc方法中

cls->instanceSize:计算所需内存大小

计算需要开辟内存的大小的执行流程如下所示


  • 1、跳转至instanceSize的源码实现

通过断点调试,会执行到cache.fastInstanceSize方法,快速计算内存大小

  • 2、跳转至fastInstanceSize的源码实现,通过断点调试,会执行到align16

  • 3、跳转至align16的源码实现,这个方法是16字节对齐算法

内存字节对齐原则

在解释为什么需要16字节对齐之前,首先需要了解内存字节对齐的原则,主要有以下三点

数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐
为什么需要16字节对齐

需要字节对齐的原因,有以下几点:

通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况
字节对齐-总结

在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体,
结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转。
苹果早期是8字节对齐,现在是16字节对齐
下面以align(8) 为例,图解16字节对齐算法的计算过程,如下所示

首先将原始的内存 8 与 size_t(15)相加,得到 8 + 15 = 23
将 size_t(15) 即 15进行~(取反)操作,~(取反)的规则是:1变为0,0变为1
最后将 23 与 15的取反结果 进行 &(与)操作,&(与)的规则是:都是1为1,反之为0,最后的结果为 16,即内存的大小是以16的倍数增加的

calloc:申请内存,返回地址指针

通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针


这里我们可以通过断点来印证上述的说法,在未执行calloc时,po objnil,执行后,再po obj法线,返回了一个16进制的地址


在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x01111111f>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc 地址 还没有与传入 的 cls进行关联,
  • 同时印证了 alloc的根本作用就是 开辟内存
obj->initInstanceIsa:类与isa关联

经过calloc可知,内存已经申请好了,类也已经出入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示


主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联

同样也可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针


总结

  • 通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍
  • 开辟内存的核心步骤有3步:计算 -- 申请 -- 关联

init 源码探索

alloc源码探索完了,接下来探索init源码,通过源码可知,inti的源码实现有以下两种

类方法 init


这里的init是一个构造方法 ,是通过工厂设计(工厂方法模式),主要是用于给用户提供构造方法入口。这里能使用id强转的原因,主要还是因为 内存字节对齐后,可以使用类型强转为你所需的类型

实例方法 init

  • 通过以下代码进行探索实例方法 init

  • 通过main中的init跳转至init的源码实现

  • 跳转至_objc_rootInit的源码实现

有上述代码可以,返回的是传入的self本身。

new 源码探索

一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实现,通过源码可以得知,new函数中直接调用了callAlloc函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [alloc init]的结论


但是一般开发中并不建议使用new,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX,会在这个方法中调用[super init],用new初始化可能会无法走到自定义的initWithXXX部分。

例如,在CJLPerson中有两个初始化方法,一个是重写的父类的init,一个是自定义的initWithXXX方法,如下图所示


使用 alloc + init 初始化时,打印的情况如下


使用new 初始化时,打印的情况如下

总结

如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。

补充

【问题】为什么无法断点到obj->initInstanceIsa(cls, hasCxxDtor);

主要是因为断点断住的不是 自定义类的流程,而是系统级别的


作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108427260

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS底层原理01:源码探索的三种方式

iOS
本文主要介绍下源码探索的三种方法1、符号断点直接跟流程2、通过按住control+step into3、汇编跟流程下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例1、符号断点直接跟流程通过下alloc的符号断点选择断点Symbolic Br...
继续阅读 »

本文主要介绍下源码探索的三种方法

  • 1、符号断点直接跟流程
  • 2、通过按住control+step into
  • 3、汇编跟流程

下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例

1、符号断点直接跟流程

  • 通过下alloc的符号断点

    • 选择断点Symbolic Breakpoint


符号断点中输入 alloc


main中的CJLPerson处 加一个断点
在走到这部分断点之前,需要关闭上面新增的符号断点,原因是因为alloc的调用有很多,如果开启了就不能准确的定位到CJLPerson的alloc方法


以下为符号断点的关闭状态


运行程序, 断在CJLPerson部分

  • 打开 alloc符号断点 ,断点状态为


    继续执行


    以下为alloc符号断点断住的堆栈调用情况,从下图可以看出 alloc 的源码位于libobjc.A.dylib库(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


    2、通过按住control+step into

    • main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


  • 按住 control键,选择 step into ⬇️键


进去后,显示为以下内容


再下一个objc_alloc符号断点,符号断点后显示了 objc_alloc所在的源码库
(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


3、汇编跟流程

main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


xcode 工具栏 选择 Debug --> Debug Workflow --> Always Show Disassembly,这个 选项表示 始终显示反汇编 ,即 通过汇编 跟流程


按住control,点击 step into ⬇️键,执行到下图的callq ,对应 objc_alloc


  • 按住control,点击 step into ⬇️键进入,看到断点断在objc_alloc部分


  • 同样通过objc_alloc的符号断点,得知源码所在库
    (需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)



作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108425742
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

苹果:App自6月30日起支持删除账号,开发者相关问题都在这里了

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。6 月 30 日起,App 必须允许用户删除账号从 2022 年 6 月 30 日开始,App Store 内支持账号创建的...
继续阅读 »

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。

6 月 30 日起,App 必须允许用户删除账号

从 2022 年 6 月 30 日开始,App Store 内支持账号创建的应用,必须提供删除账号的功能。

1653473320(1).jpg

出海痛点很多?点击这里解决



开发者如需更新应用程序以完善删除账号功能,需要注意以下几点:

1)用户能在应用中快速找到删除账号的入口,一般可在账户设置中找到;

2)如果用户是通过 Apple ID 登录,需要在删除账号时使用 Sign in with Apple REST API 来撤销用户令牌;

发.png

3)用户删除账号不仅是暂时停用或禁用账号,苹果要求在应用内,所有与该账号相关的个人数据都可以被删除,以帮助用户更好地管理隐私数据;

4)受高度监管的应用可能需要提供额外的客户服务流程,以跟进账号删除过程;

5)遵守有关存储和保留用户账号信息以及处理账号删除的适用法律要求,包括遵守不同国家或地区的当地法律。

此外,如果用户需要访问网站以指引如何删除账号,开发者也需提供相关链接。

若删除账号需要额外的时间,或删除时应用购买问题需要另外解决,开发者也应告知用户。

App 删除账号功能相关问题

Q:开发者可以将用户引导到客户服务流程以完成账号删除吗?

A:受高度监管的应用,如中应用商店审查指南 5.1.1(ix)所述,可能会使用额外的客户服务流程来确认和促进账号删除过程。

不在高度监管的行业中运行的应用程序不应要求用户拨打电话、发送电子邮件或通过其他支持流程完成账号删除。

Q:开发者是否可以要求重新认证,或添加确认步骤以确保账号不会被意外删除或被账号持有人以外的人删除?

A:可以,确保删除动作是用户期望进行的。

开发者可以添加步骤来验证用户身份,并确认他们想要删除该账号(如通过输入已与该账号关联的电子邮件或电话号码)。

但是,给用户删除账号增加不必要的困难将不会通过审核。

Q:如应用使用 Sign in with Apple 为用户提供账号创建和身份验证,需要进行哪些更改?

A:支持 Sign in with Apple 的应用需要使用 Sign in with Apple REST API 来撤销用户令牌。更多信息,请查看苹果官方文档和设计建议。

Q:如果开发者的应用链接到默认网络浏览器以创建账号,是否仍需要在应用内提供账号删除功能?

A:是的。但请注意链接到默认 Web 浏览器进行登录或注册账号,会影响用户体验,具体可查看应用商店审查指南 4。

Q:应用会自动为用户创建一个账号,是否需要提供进行账号删除的选项?

A:是的。用户应该可以选择删除自动生成的账号(包括访客账号)以及与这些账号关联的数据。

同时,开发者需要确保应用中的任何账号创建都符合当地法律。

Q:账号删除是否必须立即自动完成?

A:不是,可以接受手动删除账号,并花费一些时间。

开发者需要通知用户删除账号需要多长时间,并在删除完成后提供确认,并确保删除账号所用的时间。

Q:删除账号后,用户产生的内容是否需要在共享的应用中删除?

A:是的。用户删除账号时,将删除与其账号关联的所有数据,包括与他人一起生成的内容,如照片、视频、文字帖子和评论等。

如果当地法律要求开发者维护某些数据,请另外告知用户。

Q:是否允许应用只在某些地方根据 CCPA、GDPR 或其他当地法律删除账号?

A:不可以。应该允许所有用户删除他们的账号,无论他们身在何处,开发者的账号删除流程也需要提供给所有用户。

Q:如何管理自动续订的用户,以免在用户删除账号后意外收费?

A:告知用户管理订阅,后续计费将通过 Apple 继续,并提醒用户在下一次收费前取消订阅。

开发者使用 App Store 自动续订的 Server Notifications,可以实时查看用户的订阅状态,或者使用订阅状态 API 进行识别。

同时,开发者可以提供 Apple 支持链接(https: //support.apple.com/en-us/HT204084),帮助用户提交退款请求。

此外,开发者还可以提供一个选项,即设置账号删除日期与订阅到期时间一致,但仍需提供可立即删除账号的选项。

应用更新过程中的更多常见问题,可访问以下网站了解:

https://developer.apple.com/support/offering-account-deletion-in-your-app

据悉,苹果去年就已宣布调整 App Store 的指导方针,要求应用允许用户删除自己的账户,但由于功能实现较复杂,苹果两度推迟实行。如今正式推行,预计未来一段时间内或将有大量应用进行更新。

收起阅读 »

KTV歌词解析, 音准评分组件

iOS
KTV歌词解析, 音准评分组件介绍支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分欢迎各位大佬提交PR, 有问题提issue, 我会不定时fixGithub使用方法初始化    private...
继续阅读 »

KTV歌词解析, 音准评分组件

KTV歌词解析, 音准评分组件

介绍

支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分

欢迎各位大佬提交PR, 有问题提issue, 我会不定时fix

Github

使用方法

初始化

    private lazy var lrcScoreView: AgoraLrcScoreView = {
       let lrcScoreView = AgoraLrcScoreView(delegate: self)
       lrcScoreView.config.scoreConfig.scoreViewHeight = 100
       lrcScoreView.config.scoreConfig.emitterColors = [.systemPink]
       lrcScoreView.config.lrcConfig.lrcFontSize = .systemFont(ofSize: 15)
       return lrcScoreView
   }()

配置属性

组件base配置
    /// 评分组件配置
   public var scoreConfig: AgoraScoreItemConfigModel = .init()
   /// 歌词组件配置
   public var lrcConfig: AgoraLrcConfigModel = .init()
   /// 是否隐藏评分组件
   public var isHiddenScoreView: Bool = false
   /// 背景图
   public var backgroundImageView: UIImageView?
   /// 评分组件和歌词组件之间的间距 默认: 0
   public var spacing: CGFloat = 0
歌词配置
    /// 无歌词提示文案
   public var tipsString: String = "纯音乐,无歌词"
   /// 提示文字颜色
   public var tipsColor: UIColor = .black
   /// 提示文字大小
   public var tipsFont: UIFont = .systemFont(ofSize: 17)
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .lightGray
   /// 是否隐藏分割线
   public var isHiddenSeparator: Bool = false
   /// 默认歌词背景色
   public var lrcNormalColor: UIColor = .gray
   /// 高亮歌词背景色
   public var lrcHighlightColor: UIColor = .white
   /// 实时绘制的歌词颜色
   public var lrcDrawingColor: UIColor = .orange
   /// 歌词文字大小 默认: 15
   public var lrcFontSize: UIFont = .systemFont(ofSize: 15)
   /// 歌词高亮文字缩放大小 默认: 1.1
   public var lrcHighlightScaleSize: Double = 1.1
   /// 歌词左右两边间距
   public var lrcLeftAndRightMargin: CGFloat = 15
   /// 等待开始圆点背景色 默认: 灰色
   public var waitingViewBgColor: UIColor? = .gray
   /// 等待开始圆点大小 默认: 10
   public var waitingViewSize: CGFloat = 10
   /// 是否可以拖动歌词 默认: true
   public var isDrag: Bool = true
评分配置
    /// 评分视图高度 默认:100
   public var scoreViewHeight: CGFloat = 100
   /// 圆的起始位置: 默认: 100
   public var innerMargin: CGFloat = 100
   /// 线的高度 默认:10
   public var lineHeight: CGFloat = 10
   /// 线的宽度 默认: 120
   public var lineWidht: CGFloat = 120
   /// 默认线的背景色
   public var normalColor: UIColor = .gray
   /// 匹配后线的背景色
   public var highlightColor: UIColor = .orange
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .systemPink
   /// 是否隐藏垂直分割线
   public var isHiddenVerticalSeparatorLine: Bool = false
   /// 是否隐藏上下分割线
   public var isHiddenSeparatorLine: Bool = false
   /// 游标背景色
   public var cursorColor: UIColor = .systemPink
   /// 游标的宽
   public var cursorWidth: CGFloat = 20
   /// 游标的高
   public var cursorHeight: CGFloat = 20
   /// 是否隐藏粒子动画效果
   public var isHiddenEmitterView: Bool = false
   /// 使用图片创建粒子动画
   public var emitterImages: [UIImage]?
   /// emitterImages为空默认使用颜色创建粒子动画
   public var emitterColors: [UIColor] = [.red]
   /// 尾部动画图片
   public var tailAnimateImage: UIImage?
   /// 尾部动画颜色
   public var tailAnimateColor: UIColor? = .yellow
   /// 评分默认分数: 50
   public var defaultScore: Double = 50

事件回调

歌词Delegate
weak var delegate: AgoraLrcViewDelegate?

protocol AgoraLrcViewDelegate {
   /// 当前播放器的时间 单位: 秒
   func getPlayerCurrentTime() -> TimeInterval
   /// 获取歌曲总时长
   func getTotalTime() -> TimeInterval

   /// 设置播放器时间
   @objc
   optional func seekToTime(time: TimeInterval)
   /// 当前正在播放的歌词和进度
   @objc
   optional func currentPlayerLrc(lrc: String, progress: CGFloat)
}
歌词下载Delegate
weak var downloadDelegate: AgoraLrcDownloadDelegate?

protocol AgoraLrcDownloadDelegate {
   /// 开始下载
   @objc
   optional func beginDownloadLrc(url: String)
   /// 下载完成
   @objc
   optional func downloadLrcFinished(url: String)
   /// 下载进度
   @objc
   optional func downloadLrcProgress(url: String, progress: Double)
   /// 下载失败
   @objc
   optional func downloadLrcError(url: String, error: Error?)
   /// 下载取消
   @objc
   optional func downloadLrcCanceld(url: String)
   /// 开始解析歌词
   @objc
   optional func beginParseLrc()
   /// 解析歌词结束
   @objc
   optional func parseLrcFinished()
}
评分Delegate
weak var scoreDelegate: AgoraKaraokeScoreDelegate?

protocol AgoraKaraokeScoreDelegate {
   /// 分数实时回调
   /// score: 每次增加的分数
   /// cumulativeScore: 累加分数
   /// totalScore: 总分
   @objc optional func agoraKaraokeScore(score: Double, cumulativeScore: Double, totalScore: Double)
}

集成方式

本地pod引入,暂时使用本地pod 待后续发布cocoapods

把 'AgoraLrcScoreView' 复制到根目录, 执行pod

pod 'AgoraLrcScore', :path => "AgoraLrcScoreView"

作者:莫烦恼
来源:https://juejin.cn/post/7054443857928257550

收起阅读 »

swift 苹果登录

iOS
- 苹果登录的前期工作: - 1.开发者账号中增加苹果登录的选项- 2.xcode中配置苹果登录 //swift版本的代码逻辑 //头文件 import AuthenticationServices //按钮加载 苹果登录 对于按钮有一定的要求,具体查看...
继续阅读 »
苹果登录
项目中继承第三方登录时,需增加上苹果登录即可上架
苹果登录需要iOS系统 13以上支持
详细的内容阅读苹果官方的网址
url:https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple
- 苹果登录的前期工作:
- 1.开发者账号中增加苹果登录的选项


1.1  可能会造成证书无法使用,重新编辑一下保存下载即可!


- 2.xcode中配置苹果登录


前期的配置基本上完成
剩下的就是代码逻辑
- 3.代码中增加苹果登录的逻辑
//swift版本的代码逻辑
//头文件
import AuthenticationServices

//按钮加载 苹果登录 对于按钮有一定的要求,具体查看上方的连接
// 此处使用了一个临时的
if #available(iOS 13.0, *) {
let authorizationButton = ASAuthorizationAppleIDButton()
authorizationButton.frame = CGRect(x: (KScreenWidth - 300) / 2, y: kScreenHeight - 50, width: 300, height: 30)
authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
self.view.addSubview(authorizationButton)
} else {
// Fallback on earlier versions
}


//MARK: 点击苹果登陆按钮
@objc
func handleAuthorizationAppleIDButtonPress() {

if #available(iOS 13.0, *) {
/**
- 点击 苹果登录的按钮跳出苹果登录的界面
- 跳转出系统界面
*/

let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]

let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self as? ASAuthorizationControllerPresentationContextProviding
authorizationController.performRequests()

} else {
// Fallback on earlier versions
}

}

//MARK: - 授权成功
@available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if #available(iOS 13.0, *) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
/**
- 首次注册 能够那去到的参数分别是:
1. user
2.state
3.authorizedScopes
4.authorizationCode
5.identityToken
6.email
7.fullName
8.realUserStatus
*/

// Create an account in your system.
let userIdentifier = appleIDCredential.user
let fullName = appleIDCredential.fullName
let email = appleIDCredential.email
let code = appleIDCredential.authorizationCode
// For the purpose of this demo app, store the `userIdentifier` in the keychain.
self.saveUserInKeychain(userIdentifier)

// For the purpose of this demo app, show the Apple ID credential information in the `ResultViewController`.
self.showResultViewController(userIdentifier: userIdentifier, fullName: fullName, email: email)
BPLog.lmhInfo("userID:\(userIdentifier),fullName:\(fullName),userEmail:\(email),code:\(code)")
case let passwordCredential as ASPasswordCredential:

// Sign in using an existing iCloud Keychain credential.
let username = passwordCredential.user
let password = passwordCredential.password

// For the purpose of this demo app, show the password credential as an alert.
DispatchQueue.main.async {
self.showPasswordCredentialAlert(username: username, password: password)
}

default:
break
}
} else {
// Fallback on earlier versions
}
}
收起阅读 »

SwiftUI版通知栏应用开发(4) ——多语言本地化适配

iOS
开发多语言版本的 APP,估计是大家希望的,尤其对于 iOS/Mac APP 的开发,上线 App Store 多希望在其它地区也能使用,所以今天主要想学习怎么基于 SwiftUI 做一些文本和字符串文字多语言化。相信市面上不少这样的文章可供参考Project...
继续阅读 »

开发多语言版本的 APP,估计是大家希望的,尤其对于 iOS/Mac APP 的开发,上线 App Store 多希望在其它地区也能使用,所以今天主要想学习怎么基于 SwiftUI 做一些文本和字符串文字多语言化。

相信市面上不少这样的文章可供参考

Project 配置

首先,在 Project Info 选项中,选择 Localization 增加一个「中文」本地化配置,如果你需要其他语言,可以相对应的添加:

接下来,新建对应的 Group 文件夹,如本文的两个 Groupen.lprojzh-Hans.lproj

在这两个 Group 里,同时创建同名的 Strings 文件:Localizable

代码编写

创建完成之后,我们分别创建一个 Preferences demo:

在我们的主 View 上引入变量 locale

popOver.contentViewController?.view = NSHostingView(rootView: MainView().environment(\.locale, .init(identifier: "en")))

然后创建一个 Text

Text("Preferences")
.font(.customf(22))
.padding(.bottom, 10.0)

刚开始我们定义的是 en,执行看看效果:

如果我们改为 zh-Hans

MainView().environment(\.locale, .init(identifier: "zh-Hans"))

结果也就不一样了:

Locale 变化功能

基本功能实现了,接下来就是设置一个开关来变化 Locale 了。

首先,创建一个 Picker

Section(header: Text("localization")) {
Picker("", selection: $localeViewModel.localeString) {
ForEach(LocaleStrings.allCases) { localeString in
Text(localeString.rawValue)
.tag(localeString.suggestedLocalication)
}
}
.pickerStyle(SegmentedPickerStyle())
}

其中,我定义两个 enum 来做选择的类型:

enum Localication: String, CaseIterable, Identifiable {
case zh_Hans = "zh-Hans"
case en = "en"
var id: String { self.rawValue }
}

enum LocaleStrings: String, CaseIterable, Identifiable {
case zh_Hans = "中文"
case en = "English"
var id: String { self.rawValue }
}

extension LocaleStrings {
var suggestedLocalication: Localication {
switch self {
case .zh_Hans: return .zh_Hans
case .en: return .en
}
}
}

这个好理解,因为显示的 String 和提供给 locale 的字符串不一致,如显示的是「中文」,提供给 locale 的是 zh-Hans,这里我借助 suggestedLocalication 做桥梁转换。

最后,我们创建一个 Combine ViewModel 变量 localeString,以供实时变化改变本地化字符串内容。

class LocaleViewModel: ObservableObject {
@Published var localeString: Localication
}

最后,只需要在具体的 View 里加入 ViewModel 订阅变量 localeString 的更新:

// ContentView.swift

import SwiftUI

struct ContentView: View {
@ObservedObject private var timerViewModel: TimerViewModel
@ObservedObject private var localeViewModel: LocaleViewModel
init(timerViewModel: TimerViewModel, localeViewModel: LocaleViewModel) {
self.timerViewModel = timerViewModel
self.localeViewModel = localeViewModel
}

var body: some View {
Text("localization")
.font(.customf(14))
.padding()
.environment(\.locale, .init(identifier: localeViewModel.localeString.rawValue))
}
}

好了,代码编写完毕,我们运行看效果:

localechange2

小结

基本跑通本地化多语言适配流程,接下来就是不断增加新的语言和翻译工作。

未完待续

收起阅读 »

[译] SwiftUI 2 应用生命周期的终极指导

原文地址:The Ultimate Guide to the SwiftUI 2 Application Life Cycle原文作者:Peter Friese译文出自:掘金翻译计划本文永久链接:github.com/xitu/gold-m…译者:zhuzil...
继续阅读 »

在很长一段时间里,iOS 开发者们都是使用 AppDelegate 作为应用的主要入口。随着 SwiftUI 2 在 WWDC 2020 上发布,苹果公司引入了一个新的应用生命周期。新的生命周期几乎(几乎)完全与 AppDelegate 无关,为类 DSL 方法铺平了道路。

在本文中,我会讨论引入新的生命周期的原因,以及你该如何在已有的应用或新的应用中使用它。

指定应用入口

我们的第一个问题是,该如何告诉编译器哪里是应用的入口呢?SE-0281 详述了**基于类型的程序入口(Type-Based Program Entry Points)**的工作方式:

Swift 编译器将识别标注了 @main 属性的类型为程序的入口。标有 @main 的类型有一个隐式要求:类型内部需要声明一个静态 main() 方法。

创建新的 SwiftUI 应用时,应用的主类(main class)如下所示:

import SwiftUI

@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

那么 SE-0281 提到的静态 main() 函数在哪儿呢?

实际上,框架可以(并且应该)为用户提供方便的默认实现。你会从上面的代码片段注意到 SwiftUIAppLifeCycleApp 遵循 App 协议。对于 App 协议,苹果提供了如下协议扩展:

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension App {

/// 初始化并运行应用。
///
/// 如果你在你的 ``SwiftUI/App`` 的实现类(conformer)的声明前加上了
/// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626)
/// 属性,系统会调用这个实现类的 `main()` 方法来启动应用。
/// SwiftUI 提供了该方法的默认实现,从而能以适合平台的方式处理应用启动流程。
public static func main()
}

这下你就懂了吧 —— 这个协议扩展提供了处理应用启动的默认的实现。

由于 SwiftUI 框架不是开源的,所以我们看不到苹果是如何实现此功能的,但是 Swift Argument Parser 是开源的,并且也用了这个办法。查看 ParsableCommand 的源码,就能了解它是如何用协议扩展来提供静态 main 函数的默认实现,并将其用作程序入口的:

extension ParsableCommand {
...
public static func main(_ arguments: [String]?) {
do {
var command = try parseAsRoot(arguments)
try command.run()
} catch {
exit(withError: error)
}
}

public static func main() {
self.main(nil)
}
}

如果上述这些听起来有点复杂,好消息是实际上在创建新的 SwiftUI 应用程序时你不必关心它:只需确保在 Life Cycle 下拉菜单中选择 SwiftUI App 来创建你的应用程序就行了:

创建一个新的 SwiftUI 项目

让我们来看一些常见的情况。

初始化资源 / 你最喜欢的 SDK 或框架

大多数应用程序需要在启动时执行这些步骤:获取一些配置值,连接数据库或者初始化框架或第三方 SDK。

通常,您可以在 ApplicationDelegate 的 application(_:didFinishLaunchingWithOptions:) 方法中进行这些操作。由于已经没有应用委托了,我们需要找到其他方法来初始化我们的应用程序。根据您的特定需求,有以下策略:

  • 为你的主类实现一个构造函数(initializer)(详见文档
  • 为存储属性设置初始值(详见文档
  • 用闭包设置属性的默认值(详见文档
@main
struct ColorsApp: App {
init() {
print("Colors application is starting up. App initialiser.")
}

var body: some Scene {
WindowGroup {
ContentView()
}
}

如果上述几种策略都无法满足你的需求,你可能还是需要一个 AppDelegate。后文会介绍如果能在应用中加入一个 AppDelegate。

处理你的应用的生命周期

了解你的应用程序处于哪种状态有时很有用。例如,你可能希望应用处于活动状态时立即获取新数据,或者在应用程序变为非活动状态并转换到后台后清除所有缓存。

通常,您可以在你的 ApplicationDelegate 上实现 applicationDidBecomeActiveapplicationWillResignActive 或 applicationDidEnterBackground

从 iOS 14.0 起,苹果提供了新的 API,该 API 允许以更优雅,更易维护的方式跟踪应用程序状态:[ScenePhase](https://developer.apple.com/documentation/swiftui/scenephase)。你的项目可以有多个场景(scene),不过有时只有一个场景。这些场景将由 [WindowGroup](https://developer.apple.com/documentation/swiftui/windowgroup) 展示。

SwiftUI 追踪环境中场景的状态,你可以使用 @Environment 属性包装器来获取 scenePhase 的值,然后使用 onChange(of:) modifier 来监听该值的变化:

@main
struct SwiftUIAppLifeCycleApp: App {
@Environment(\.scenePhase) var scenePhase

var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
print("App is active")
case .inactive:
print("App is inactive")
case .background:
print("App is in background")
@unknown default:
print("Oh - interesting: I received an unexpected new value.")
}
}
}
}

值得注意的是,你可以从应用中的其他位置读取该值。当在应用的顶层读取该值时(如上面的代码片段所示),你将获得应用程序中所有阶段(phase)的汇总。.inactive 表示你应用中的所有场景均未激活。当在视图中读取 scenePhase 时,你将收到包含该视图的阶段值。请记住,你的应用程序在在同一时刻可能包含在不同阶段的多个场景。想了解有关场景阶段的更多详细信息,请阅读苹果的[文档](developer.apple.com/documentati…

处理深层链接(Deeplink)

之前,在处理深层链接时,你需要实现 application(_:open:options:),并将传入的 URL 转给最合适的处理程序。

新的应用生命周期模型可以更容易地处理深层链接。在最顶层的场景上添加 onOpenURL 就可以处理传入的 URL 了:

@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
print("Received URL: \(url)")
}
}
}
}

真正酷的是:你可以在整个应用程序中装上多个 URL 处理程序 —— 让进行深层链接变得很轻松,因为你可以在最合适的位置处理传入的链接。

可能的话,你应该使用 universal links(或者 Firebase Dynamic Links,它使用了 universal links for iOS apps),因为它们使用了关联域(associated domain)来创建网站和你的应用之间的链接 —— 这会让你可以安全地共享数据。

不过,你仍可以使用自定义 URL scheme 来链接应用内部的内容。

无论哪种方式,触发应用中的深层链接的一种简单方法是在开发计算机上使用以下命令:

xcrun simctl openurl booted <your url>

Demo: Opening deep links and continuing user activities

继续用户 activity

如果你的应用使用 NSUserActivity 来集成 Siri、Handoff 或 Spotlight,你需要处理用户继续进行的 activity。

同样,新的应用生命周期模型通过提供两个 modifier 使你更容易实现这一点。这些 modifier 使你可以声明 activity 并让用户可以继续进行它们。

下面是一个展现如何声明 activity 的代码片段。在一个具体的视图里:

struct ColorDetailsView: View {
var color: String

var body: some View {
Image(color)
// ...
.userActivity("showColor") { activity in
activity.title = color
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// ...
}
}
}

为了允许继续进行这个 activity,你可以在最顶层的导航视图中注册 onContinueUserActivity 闭包,如下所示:

import SwiftUI

struct ContentView: View {
var colors = ["Red", "Green", "Yellow", "Blue", "Pink", "Purple"]

@State var selectedColor: String? = nil

var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(colors, id: \.self) { color in
NavigationLink(destination: ColorDetailsView(color: color),
tag: color,
selection: $selectedColor) {
Image(color)
}
}
}
.onContinueUserActivity("showColor") { userActivity in
if let color = userActivity.userInfo?["colorName"] as? String {
selectedColor = color
}
}
}
}
}
}

请帮帮我 —— 上述的那些对我都不管用!

新的应用声明周期(截止当前)并非支持 AppDelegate 的所有回调函数。如果上述这些都不满足你的需求,你可能还是需要一个 AppDelegate

另一个需要 AppDelegate 的原因是你使用的第三方 SDK 会使用 method swizzling 来把它们注入应用生命周期。Firebase 就是一个典型的例子

为了帮助上述情况中的你摆脱困境,Swift 提供了一种将 AppDelegate 的一个实现类与你的 App 实现相连接的方法:@UIApplicationDelegateAdaptor。使用方法如下:

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Colors application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.")
return true
}
}

@main
struct ColorsApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

如果你是在复制现有的 AppDelegate 实现,不要忘记删除 @main 属性 —— 不然,编译器该向你抱怨存在多个应用入口了。

总结

至此,让我们讨论一下苹果为什么要进行这些改变。我觉得有以下的几个原因:

SE-0281 explicitly states that one of the design goals was “to offer a more general purpose and lightweight mechanism for delegating a program’s entry point to a designated type.”

苹果选择的基于 DSL 来处理应用生命周期的方法和 SwiftUI 的声明式 UI 搭建方法相契合。两者采用相同的概念可以更方便新加入的开发者们理解。

声明式方法的主要好处是:框架/平台将替代开发者承受实现特定功能的负担。如果需要进行任何更改,这种模式可以在不破坏许多开发人员的应用的情况下进行发布,这也使发布更改变得更容易 —— 理想情况下,开发人员无需更改其实现,因为框架将把一切都搞定。

总体而言,新的应用生命周期模型使实现应用程序的启动更加简单。你的代码将变得更加简洁,更易于维护 —— 要我说,这总是一件好事。

我希望本文能帮你了解新的应用生命周期的来龙去脉。如果你有关于本文的任何疑问或评论,欢迎在 Twitter 上关注并私信我,或者在 GitHub 上的样例项目中提 issue。

感谢你的阅读!

扩展阅读

想了解更多,请查看下面的这些资料:

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

收起阅读 »

SwiftUI 实现侧滑菜单 Side Menu

SwiftUI 实现侧滑菜单 Side Menu 效果 代码 代码里都有相关注释 源码 github 链接:gist.github.com/RandyWei/05… // // ContentView.swift // SiderMenuDemo01 ...
继续阅读 »

SwiftUI 实现侧滑菜单 Side Menu


效果


iShot2021-09-08 09.43.45.gif


代码


代码里都有相关注释


源码 github 链接:gist.github.com/RandyWei/05…



//
// ContentView.swift
// SiderMenuDemo01
//
// Created by RandyWei on 2021/9/7.
//

import SwiftUI

struct ContentView: View {

//划动偏移量
@GestureState var offset:CGFloat = 0

//滑动应该停留在某个点
//停留点: 屏幕宽度的3/5
let maxOffset:CGFloat = UIScreen.main.bounds.width * 3 / 5

//滑动展开之后的 offset
@State var expandOffset:CGFloat = 0

//回弹点:最大停留点/2
private var springOffset:CGFloat{
maxOffset / 2
}
//缩放比例,默认是1
@State private var scaleRatio:CGFloat = 1

//最小 可缩放值
let minScale:CGFloat = 0.9


private var dragGesture: some Gesture {
DragGesture()
.updating($offset, body: { value, out, _ in
//判断是否反向滑动,如果是展开状态需要反向滑动
if value.translation.width >= 0 || expandOffset != 0 {
out = value.translation.width
}
})
.onChanged { value in
//为了顺畅给缩放增加过渡
if value.translation.width >= 0 {
//对缩放比例进行计算:缩放值 = 划动比例 * 可缩放值(1-minScale)
//因为是往小了缩,所以是1-缩放值
scaleRatio = 1 - (value.translation.width / maxOffset) * (1 - minScale)
} else {
//反向value.translation.width是负数 ,所以+maxOffset变为正值
scaleRatio = 1 - ((maxOffset + value.translation.width) / maxOffset) * (1 - minScale)
}
}
.onEnded { value in
//需要判断滑动是否超过某个点来决定是重置还是停留
if value.translation.width >= springOffset {
expandOffset = maxOffset
//停止后,缩小 到0.9
scaleRatio = minScale
} else {
expandOffset = 0
scaleRatio = 1
}
}
}

var body: some View {

ZStack{

//侧边菜单层
SideMenuView()

//功能区域
FeatureView()
.offset(x: offset + expandOffset)
.scaleEffect(scaleRatio)
.animation(.easeInOut(duration: 0.05))
.gesture(dragGesture)


}

}
}

struct FeatureView:View {

var body: some View{

GeometryReader{proxy in
VStack{
HStack{
Image(systemName: "list.dash")
.resizable()
.frame(width: 20, height: 20, alignment: .center)

Text("功能区域")
.font(.title)

Spacer()
}

ScrollView(.vertical, showsIndicators: false, content: {

VStack{

ForEach(0..<50){_ in

HStack{

Image(systemName: "person")
.resizable()
.frame(width: 80, height: 80, alignment: .center)

VStack(alignment: .leading){
Text("titletitletitletitletitle")
.font(.title)

Spacer()

Text("bodybodybodybodybodybody")
.font(.body)
}

}

}.redacted(reason: .placeholder)
}

})
}
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.white)
.cornerRadius(30)
.shadow(radius: 10)
.ignoresSafeArea()
}

}
}

struct SideMenuView:View {
var body: some View{

GeometryReader{proxy in
VStack(alignment:.leading){
//祖传头像
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100, alignment: .center)
.clipShape(Circle())

Text("韦爵爷")
.font(.title)

Text("这个人很懒,什么都没留下")

//菜单

HStack{
Image(systemName: "archivebox")
Text("菜单一")
}
.padding(.top)

HStack{
Image(systemName: "note.text")
Text("菜单二")
}
.padding(.top)


HStack{
Image(systemName: "gearshape")
Text("个人设置")
}
.padding(.top)

Spacer()

HStack{
Image(systemName: "signature")
Text("退出登录")
}
.padding(.top)

}
.foregroundColor(.white)
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.padding(.bottom, 8 + proxy.safeAreaInsets.bottom)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.orange)
.ignoresSafeArea()
}

}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

相关视频

Swift UI侧滑菜单Side Menu-哔哩哔哩


作者:RandyWei
链接:https://juejin.cn/post/7005374220360220702

收起阅读 »

聊聊 Combine 和 async/await 之间的合作

iOS
在 Xcode 13.2 中,苹果完成了 async/await 的向前部署(Back-deploying)工作,将最低的系统要求降低到了 iOS 13(macOS Catalina),这一举动鼓舞了越来越多的人开始尝试使用 async/await 进行开发。...
继续阅读 »

在 Xcode 13.2 中,苹果完成了 async/await 的向前部署(Back-deploying)工作,将最低的系统要求降低到了 iOS 13(macOS Catalina),这一举动鼓舞了越来越多的人开始尝试使用 async/await 进行开发。当大家在接触了异步序列(AsyncSequence)后,会发现它同 Combine 的表现有些接近,尤其结合近两年 Combine 框架几乎没有什么变化,不少人都提出了疑问:苹果是否打算使用 AsyncSequence 和 AsyncStream 替代 Combine。

恰巧我在最近的开发中碰到了一个可能需要结合 Combine 和 async/await 的使用场景,通过本文来聊聊 Combine 和 async/await 它们之间各自的优势、是否可以合作以及如何合作等问题。

原文发表在我的博客 wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

需要解决的问题

在最近的开发中,我碰到了这样一个需求:

  • 在 app 的生命周期中,会不定期的产生一系列事件,事件的发生频率不定、产生的途径不定
  • 对每个事件的处理都需要消耗不小的系统资源,且需要调用系统提供的 async/await 版本的 API
  • app 对事件的处理结果时效性要求不高
  • 需要限制事件处理的系统消耗,避免同时处理多个事件
  • 不考虑使用 GCD 或 OperationQueue

对上述的需求稍加分析,很快就可以确立解决问题的方向:

  • Combine 在观察和接收事件方面表现的非常出色,应该是解决需求第一点的不二人选
  • 在解决方案中必然会使用到 async/await 的编程模式

需要解决的问题就只剩下两个:

  • 如何将事件处理串行化(必须处理完一个事件后才能处理下一个事件)
  • 如何将 Combine 和 async/await 结合使用

Combine 和 AsyncSequence 之间的比较

由于 Combine 同 AsyncSequence 之间存在不少相似之处,有不少开发者会认为 AsyncSequence 可能取代 Combine,例如:

  • 两者都允许通过异步的方式处理未来的值
  • 两者都允许开发者使用例如 map、flatMap 等函数对值进行操作
  • 当发生错误时,两者都会结束数据流

但事实上,它们之间还是有相当的区别。

事件的观察与接收

Combine 是为响应式编程而生的工具,从名称上就可以看出,它非常擅长将不同的事件流进行变形和合并,生成新的事件流。Combine 关注于对变化的响应。当一个属性发生变化,一个用户点击了按钮,或者通过 NotificationCenter 发送了一个通知,开发者都可以通过 Combine 提供了的内置工具做出及时处理。

通过 Combine 提供的 Subject(PassthroughSubject、CurrentValueSubject),开发者可以非常方便的向数据流中注入值,当你的代码是以命令式风格编写的时候,Subject 就尤为显得有价值。

在 async/await 中,通过 AsyncSequence,我们可以观察并接收网络流、文件、Notification 等方面的数据,但相较于 Combine,仍缺乏数据绑定以及类似 Subject 的数据注入能力。

在对事件的观察与接收方面,Combine 占有较大优势。

关于数据处理、变形的能力

仅从用于数据处理、变形的方法数量上来看,AsyncSequence 相较 Combine 还是有不小的差距。但 AsyncSequence 也提供了一些 Combine 尚未提供,且非常实用的方法和变量,例如:characters、lines 等。

由于侧重点不同,即使随着时间的推移两者增加了更多的内置方法,在数据处理和变形方面也不会趋于一致,更大的可能性是不断地在各自擅长的领域进行扩展。

错误处理方式

在 Combine 中,明确地规定了错误值 Failure 的类型,在数据处理链条中,除了要求 Output 数据值类型一致外,还要求错误值的类型也要相互匹配。为了实现这一目标,Combine 提供了大量的用于处理错误类型的操作方法,例如:mapError、setFailureType、retry 等。

使用上述方法处理错误,可以获得编译器级别的保证优势,但在另一方面,对于一个逻辑复杂的数据处理链,上述的错误处理方式也将导致代码的可读性显著下降,对开发者在错误处理方面的掌握要求也比较高。

async/await 则采用了开发者最为熟悉的 throw-catch 方式来进行错误处理。基本没有学习难度,代码也更符合大多数人的阅读习惯。

两者在错误处理上功能没有太大区别,主要体现在处理风格不同。

生命周期的管理

在 Combine 中,从订阅开始,到取消订阅,开发者通过代码可以对数据链的生命周期做清晰的定义。当使用 AsyncSequence 时,异步序列生命周期的表述则没有那么的明确。

调度与组织

在 Combine 中,开发者不仅可以通过指定调度器(scheduler),显式地组织异步事件的行为和地点,而且 Combine 还提供了控制管道数量、调整处理频率等多维度的处理手段。

AsyncSequence 则缺乏对于数据流的处理地点、频率、并发数量等控制能力。

下文中,我们将尝试解决前文中提出的需求,每个解决方案均采用了 Combine + async/await 融合的方式。

方案一

在 Combine 中,可以使用两种手段来限制数据的并发处理能力,一种是通过设定 flatMap 的 maxPublishers,另一种则是通过自定义 Subscriber。本方案中,我们将采用 flatMap 的方式来将事件的处理串行化。

在 Combine 中调用异步 API,目前官方提供的方法是将上游数据包装成 Future Publisher,并通过 flatMap 进行切换。

在方案一中,通过将 flatMap、Deferred(确保只有在订阅后 Future 才执行)、Future 结合到一起,创建一个新的 Operator,以实现我们的需求。

public extension Publisher {
func task<T>(maxPublishers: Subscribers.Demand = .unlimited,
_ transform: @escaping (Output) async -> T) -> Publishers.FlatMap<Deferred<Future<T, Never>>, Self> {
flatMap(maxPublishers: maxPublishers) { value in
Deferred {
Future { promise in
Task {
let output = await transform(value)
promise(.success(output))
}
}
}
}
}
}

public extension Publisher where Self.Failure == Never {
func emptySink() -> AnyCancellable {
sink(receiveValue: { _ in })
}
}

鉴于篇幅,完整的代码(支持 Error、SetFailureType)版本,请访问 Gist,本方案的代码参考了 Sundell 的 文章

使用方法如下:

var cancellables = Set<AnyCancellable>()

func asyncPrint(value: String) async {
print("hello \(value)")
try? await Task.sleep(nanoseconds: 1000000000)
}

["abc","sdg","353"].publisher
.task(maxPublishers:.max(1)){ value in
await asyncPrint(value:value)
}
.emptySink()
.store(in: &cancellables)
// Output
// hello abc
// 等待 1 秒
// hello sdg
// 等待 1 秒
// hello 353

假如将将上述代码中的["abc","sdg","353"].publisher更换成 PassthoughSubject 或 Notification ,会出现数据遗漏的情况。这个状况是因为我们限制了数据的并行处理数量,从而导致数据的消耗时间超过了数据的生成时间。需要在 Publisher 的后面添加 buffer,对数据进行缓冲。

let publisher = PassthroughSubject<String, Never>()
publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest) // 缓存数量和策略根据业务的具体情况确定
.task(maxPublishers: .max(1)) { value in
await asyncPrint(value:value)
}
.emptySink()
.store(in: &cancellables)

publisher.send("fat")
publisher.send("bob")
publisher.send("man")

方案二

在方案二中,我们将采用的自定义 Subscriber 的方式来限制并行处理的数量,并尝试在 Subscriber 中调用 async/await 方法。

创建自定义 Subscriber:

extension Subscribers {
public class OneByOneSink<Input, Failure: Error>: Subscriber, Cancellable {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void

var subscription: Subscription?

public init(receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
}

public func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(1)) // 订阅时申请数据量
}

public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .max(1) // 数据处理结束后,再此申请的数据量
}

public func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
}

public func cancel() {
subscription?.cancel()
subscription = nil
}
}
}

receive(subscription: Subscription)中,使用subscription.request(.max(1))设定了订阅者订阅时请求的数据量,在receive(_ input: Input)中,使用return .max(1)设定了每次执行完receiveValue方法后请求的数据量。通过上述方式,我们创建了一个每次申请一个值,逐个处理的订阅者。

但当我们在receiveValue方法中使用 Task 调用 async/await 代码时会发现,由于没有提供回调机制,订阅者将无视异步代码执行完成与否,调用后直接会申请下一个值,这与我们的需求不符。

在 Subscriber 中可以通过多种方式来实现回调机制,例如回调方法、Notification、@Published 等。下面的代码中我们使用 Notification 进行回调通知。

public extension Subscribers {
class OneByOneSink<Input, Failure: Error>: Subscriber, Cancellable {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void

var subscription: Subscription?
var cancellable: AnyCancellable?

public init(notificationName: Notification.Name,
receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
cancellable = NotificationCenter.default.publisher(for: notificationName, object: nil)
.sink(receiveValue: { [weak self] _ in self?.resume() })
// 在收到回调通知后,继续向 Publisher 申请新值
}

public func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(1))
}

public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none // 调用函数后不继续申请新值
}

public func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
}

public func cancel() {
subscription?.cancel()
subscription = nil
}

private func resume() {
subscription?.request(.max(1))
}
}
}

public extension Publisher {
func oneByOneSink(
_ notificationName: Notification.Name,
receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Output) -> Void
) -> Cancellable {
let sink = Subscribers.OneByOneSink<Output, Failure>(
notificationName: notificationName,
receiveCompletion: receiveCompletion,
receiveValue: receiveValue
)
self.subscribe(sink)
return sink
}
}

public extension Publisher where Failure == Never {
func oneByOneSink(
_ notificationName: Notification.Name,
receiveValue: @escaping (Output) -> Void
) -> Cancellable where Failure == Never {
let sink = Subscribers.OneByOneSink<Output, Failure>(
notificationName: notificationName,
receiveCompletion: { _ in },
receiveValue: receiveValue
)
self.subscribe(sink)
return sink
}
}

调用:

let resumeNotification = Notification.Name("resume")

publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
.oneByOneSink(
resumeNotification,
receiveValue: { value in
Task {
await asyncPrint(value: value)
NotificationCenter.default.post(name: resumeNotification, object: nil)
}
}
)
.store(in: &cancellables)

由于需要回调才能完成整个处理逻辑,针对本文需求,方案一相较方案二明显更优雅。

方案二中,数据处理链是可暂停的,很适合用于需要触发某种条件才可继续执行的场景。

方案三

在前文中提到过,苹果已经为 Notification 提供了 AsyncSequence 的支持。如果我们只通过 NotificationCenter 来发送事件,下面的代码就直接可以满足我们的需求:

let n = Notification.Name("event")
Task {
for await value in NotificationCenter.default.notifications(named: n, object: nil) {
if let str = value.object as? String {
await asyncPrint(value: str)
}
}
}

NotificationCenter.default.post(name: n, object: "event1")
NotificationCenter.default.post(name: n, object: "event2")
NotificationCenter.default.post(name: n, object: "event3")

简单的难以想象是吗?

遗憾的是,Combine 的 Subject 和其他的 Publishe 并没有直接遵循 AsyncSequence 协议。

但今年的 Combine 为 Publisher 增加了一个非常小但非常重要的功能——values。

values 的类型为 AsyncPublisher,其符合 AsyncSequence 协议。设计的目的就是将 Publisher 转换成 AsyncSequence。使用下面的代码便可以满足各种 Publisher 类型的需求:

let publisher = PassthroughSubject<String, Never>()
let p = publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
Task {
for await value in p.values {
await asyncPrint(value: value)
}
}

因为 AsyncSequence 只能对数据逐个处理,因此我们无需再考虑数据的串行问题。

将 Publisher 转换成 AsyncSequence 的原理并不复杂,创建一个符合 AsyncSequence 的结构,将从 Publihser 中获取的数据通过 AsyncStream 转送出去,并将迭代器指向 AsyncStream 的迭代器即可。

我们可以用代码自己实现上面的 values 功能。下面我们创建了一个 sequence,功能表现同 values 类似。

public struct CombineAsyncPublsiher<P>: AsyncSequence, AsyncIteratorProtocol where P: Publisher, P.Failure == Never {
public typealias Element = P.Output
public typealias AsyncIterator = CombineAsyncPublsiher<P>

public func makeAsyncIterator() -> Self {
return self
}

private let stream: AsyncStream<P.Output>
private var iterator: AsyncStream<P.Output>.Iterator
private var cancellable: AnyCancellable?

public init(_ upstream: P, bufferingPolicy limit: AsyncStream<Element>.Continuation.BufferingPolicy = .unbounded) {
var subscription: AnyCancellable?
stream = AsyncStream<P.Output>(P.Output.self, bufferingPolicy: limit) { continuation in
subscription = upstream
.sink(receiveValue: { value in
continuation.yield(value)
})
}
cancellable = subscription
iterator = stream.makeAsyncIterator()
}

public mutating func next() async -> P.Output? {
await iterator.next()
}
}

public extension Publisher where Self.Failure == Never {
var sequence: CombineAsyncPublsiher<Self> {
CombineAsyncPublsiher(self)
}
}

完整代码,请参阅 Gist,本例的代码参考了 Marin Todorov 的 文章

sequence 在实现上和 values 还是有微小的不同的,如果感兴趣的朋友可以使用下面的代码,分析一下它们的不同点。

let p = publisher
.print() // 观察订阅器的请求情况。 values 的实现同方案二一样。
// sequence 使用了 AsyncStream 的 buffer,因此无需再设定 buffer

for await value in p.sequence {
await asyncPrint(value: value)
}

总结

在可以预见的未来,苹果一定会为 Combine 和 async/await 提供更多的预置融合手段。或许明后年,前两种方案就可以直接使用官方提供的 API 了。

希望本文能够对你有所帮助。

原文发表在我的博客 wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

收起阅读 »

[翻译]你不可错过的 10 个 Xcode 技巧和快捷键

iOS
原文地址:10 Tips and Shortcuts You Should Be Using Right Now in Xcode 原文作者:Mike Pesate 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m… 译者:F...
继续阅读 »


你不可错过的 10 个 Xcode 技巧和快捷键


Image source: Author


在我作为 iOS 开发人员的职业生涯中,养成了一些使得工作变得更加轻松快捷的 Xcode 习惯。很多好用的快捷键一直都存在,只是我们没有发现而已。


所以我收集了一些我最喜欢的,在这里和大家分享。


我们开始吧!


1. 快速自动缩进


当你的代码没有对齐时,这个快捷键非常有用。



control + i / ⌃ + i



它会自动缩进光标所在的行。如果你选中了一些代码,甚至整个文件,这个快捷键就会调整选中部分的缩进。


Demo of ⌃ + i


这对及时保持代码整洁非常有帮助。


2. 在所有作用域中修改


假设你发现某个方法或变量名有错误,你想要修复它。当然你不会一个个去修改,因为你知道有重构(Refactor)功能可以批量重命名,但有时候 Xcode 的重构功能可能不太靠谱。


此时你可以使用以下快捷键,选中当前文件中所有用到该变量的位置。



command + control + e / ⌘ + ⌃ + e



这将选中所有用到这个变量的位置,让你可以非常方便地更改变量名。


Demo of ⌘ + ⌃ + e


3. 查找下一个


现在,假设你不想在所有作用域中修改变量名称,而只想找到下一处;或者只想在一个函数中重命名,而不是整个类中,或者其他类似情况。有一个(和上面)非常相似的快捷键。



option + control + e / ⌥ + ⌃ + e



Demo of ⌥ + ⌃ + e


当你选中某个字符串,按下这个快捷键,Xcode 将选中下一个出现该字符串的位置。但这意味着,如果某些变量和函数同名,则下一个选中的,也许和你预期的不一样。(译注:这里指的是,并不判断是否真的是同一个变量,只是单纯的字符串匹配)。


4. 查找上一个


上面我们介绍了“查找下一个”,再多按一个键,则变成了“查找上一个”。



shift + option + control + e / ⇧ + ⌥ + ⌃ + e



Demo of ⇧ + ⌥ + ⌃ + e


5. 整行向上或向下移动


我们可能会对代码进行一些顺序调整,当然可以用经典的“剪切粘贴”,但如果我们只想将代码向上移动一行或向下移动一行,那么以下快捷键肯定会对你有所帮助。


向上移动:



option + command + [ / ⌥ + ⌘ + [



向下移动:



option + command + ] / ⌥ + ⌘ + ]



Demo of ⌥ + ⌘ + [ and ⌥ + ⌘ + ]


额外提示!你可以移动多行


如果选中多行之后再使用前面的快捷键,那么这些行将作为一个整体进行移动。


Demo of previous shortcut moving several lines as block


6. 多行光标(使用鼠标)


有时你需要在文件的不同部分中写入相同的内容,你很烦恼,因为你必须编写一次并复制粘贴几次。好吧,别再烦了。你可以使用一个快捷键同时写入多行。



shift + control + click / ⇧ + ⌃ + click



Demo of ⇧ + ⌃ + click


7. 多行光标(使用键盘)


此快捷键与上一个基本相同,但是我们不是使用鼠标来选择光标的位置,而是使用箭头向上或向下来移动光标。



shift + control + up or down /⇧ + ⌃ + ↑ or ↓




8. 快速创建带有多个参数的初始化(init)函数


上面的快捷键,我最喜欢用法之一,就是快速创建一个初始化函数,比之前的任何方法都快。



通过使用多行光标,配合其他一些快捷键,例如复制粘贴或选中整行,我们可以快速创建初始化函数。这只是这个按键的几种用途之一。


8.1 另一种方式


还有一个编辑功能,可以让你轻松地生成 “成员初始化器”(Memberwise Initializer)。你可以将光标放在类的名称上,然后找到 Editor > Refactor > Generate Memberwise Initializer。


但是,由于本文介绍快捷键,所以这里给一个小提示:可以进入 Preferences > Key Bindings,再查找对应命令,并添加快捷键。


这是操作示例:


How to add a key binding


9. 返回光标之前所在的位置


有时候你需要处理很大的文件,向上滚动查看某些内容之后,可能很难找到原来位置。有了这个快捷键,只要我们没有将光标移开,我们就可以快速跳回之前的位置。



option + command + L / ⌥ + ⌘ + L



Demo of ⌥ + ⌘ + L


10. 跳到某一行


和上一条相关,如果我们知道要跳转的那一行的行号,那么使用此快捷键,我们可以直接跳到该行。



command + L / ⌘ + L



Demo of ⌘ + L


最后的想法


这些就是我每天用来高效使用 Xcode 的十个快捷键和技巧。他们经常会派上用场。


我希望他们对你也一样有用。


如果你已经知道了这些快捷键,或者还不知道,都可以与我交流,我会很高兴。也欢迎和我分享你用到的其他有用的快捷键。


小贴士


理想情况下,你可以使用同样的快捷键,来实现前面提到的所有技巧。但是也可能取决于你的操作系统语言设置,其中一些可能略有不同。


你可以在 Xcode > Preferences… > Key Bindings 中查看特定快捷键的按键组合。


额外提示! 快速打开偏好设置(Preferences)



command + , / ⌘ + ,




如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。







作者:FranzWang
收起阅读 »

Xcode 13 更新了哪些内容

iOS
直接进入主题。外观对比 Xcode 12,风格和显示都发生了变化:去掉了文件拓展名图标也可以识别文件类型自动调整了导航栏布局重新进行了分布和调整右下角增加了光标所在行列数文件拓展名设置:打开 设置 - 通用 选择 Fil...
继续阅读 »

直接进入主题。

外观

005XGWPvly1gun3djnsn4j627y1cku0x02

对比 Xcode 12,风格和显示都发生了变化:

  • 去掉了文件拓展名
  • 图标也可以识别文件类型自动调整了
  • 导航栏布局重新进行了分布和调整
  • 右下角增加了光标所在行列数
文件拓展名设置:

打开 设置 - 通用 选择 File Extensions

image-20210920152905766

文件拓展名的显示隐藏控制,选项有三种:

image-20210920153816172

  • Hide All:隐藏全部拓展名

  • Show All:显示全部拓展名

  • Show Only:自定义显示拓展名 ↓↓↓↓

    QQ20210921-024658-HD

    问题提醒设置:

    在 设置 - 通用 里还多了一个 Xcode 12没有的选项:Issues,对应的子选项为:Show InlineShow Minimized

    Show InlineShow Minimized
    image-20210920155859979image-20210920155918641

    对比 Show InlineShow Minimized 把问题提醒最小化到了右侧,当开发者点击对应的问题时,会显示出来。

    优点一目了然,界面整洁,没有一堆提示文字和红蓝色。

    缺点则是无法直接的查看问题原因,即使是点击出来,也没有像前者那样直接的把问题精准的定位具体代码中。

    image-20210920160709412

    不过这个也不能够算是缺点,只是说提示的没有那么的明显,这点根据个人喜好选择就行。

    info.plist

    info.plist 文件内容减少,甚至使用 SwiftUI 创建项目,已经移除了 info.plist 文件,真是把简洁做到了极致

    Storyboard 创建SwiftUI 创建
    image-20210921172421609image-20210921173002487

    当然,只是当前 info.plist 文件没有显示之前的内容,在 Project - Target - Info 下,对应的信息还是存在的,且如果你在 info.plist 文件内新增了的话,依旧会在 Project - Target - Info 下显示出来的。

    至于 SwiftUI 下没有 info.plist 文件,开发者可以自行创建,具体方式可以看这里

    我不能接受

    有一个地方的改变我不能接受,那就是:编译成功失败的提醒框没有啦

    Xcode 12Xcode13
    005XGWPvly1gun4lqd89kg60ig0b80vi02005XGWPvly1gun4lsjs0jg60ig0b841m02

    Xcode 13 中,不管是编译还是运行,都没有了最后的提示框。在设置中也没有找到对应的选项。

    对于我这种经常写 Bug 的人来说,看不到弹出来的 Build Succeeeded,简直是要命。苹果你赶紧给我改回来...

更新:

评论区小伙伴给出解决方案:通知栏会提示编译成功或者失败的提示。

感谢指出,Xcode 13 版本之前也有这个提示, 我一直都忽略了这个地方,平时都把大多数应用的通知都给关了。

让我意外的是:我自己的笔记本 设置 - 通知 里面竟然没有找到 Xcode 这个应用。。。我又不会玩了~

自动补全

import

在开发过程中,经常会出现没有导入头文件就开始直接调用文件,这个时候就会比较尴尬,特别是当代码行数比较多的时候,要先回到顶部导入头文件,再回来继续写,有时候甚至都找不到刚才的位置在哪了...

Xcode 13 解决了这个问题,当你使用一个没有导入头文件的库时,会智能帮助你导入对应的头文件,非常 nice

QQ20210920-163257-HD

switch

Xcode 13 以前,使用 switch 调用枚举的时候,如果想快速调出全部的 case,就只能输入代码后等着 Xcode 给你提示 Switch must be exhaustive 然后 Fix 加载全部的 case

QQ20210920-172130-HD

Xcode 13 中,你是需要正常输入代码,就会自动的显示出来了

QQ20210920-171833-HD

摸鱼的时间又增加了

不过并不是所有的情况都支持,在使用接口请求的时候,回调的Result 类型目前就无法自动补全,只能手动输入。不知道是苹果故意为之还是。。。。

QQ20210920-172656-HD

if / guard let

Xcode 13 中,使用 if 、guard 判断一个 Optional 参数的时候,也会同名自动补全。就很舒服

QQ20210920-173543-HD

for

使用 for...in 循环语句遍历一个数组的时候,Xcode 13 会根据数组名自动生成子元素名自动补全循环

QQ20210920-175028-HD

当然,即使你输入的数组名不是那么的标准,Xcode 也还是会根据它自动识别的进行补全,比如:如果你的数组名是 number 而不是 numbers 的时候,Xcode 的自动补全依旧是 for number in number。所以,还是尽量保证代码命名的正确性吧。

列断点

Swift 链式语法在开发过程中会使代码变得非常美观和整洁,与之带来的部分问题也会出现,就是无法直观的看到每块代码的具体值,每次想查看的时候只能通过声明一个新的变量来赋值查看,这很不Swift

Xcode 13 可以使用给每行代码的任意位置设置断点,通过打印日志来查看详细内容。

可能对于这个 列断点 描述的不是太清楚。可以通过具体操作来了解。

首先创建 列断点:再所选代码位置右键 - Show Code Actions - Create Column Breakpoint

QQ20210920-182540-HD

列断点 跟之前的 行断点 一样,可以 单击、双击、和拖拽。对应的功能也一样。

运行代码,在断点位置处,通过打印日志查看:

QQ20210920-183129-HD

这个功能增加的蛮不错的。

vim

现在你可以从 Xcode 13 中使用 vim 模式来编写代码了。

beta 5 版本中,通过 Editor - Vim Mode 来开启和关闭 vim 模式了。

开启后,Xcode 底部会有对应的快捷键提示。非常友好

image-20210921022842159

其他

除了以上这些之外,Xcode 13 还增加和完善了很多的功能,比如:优化了版本控制功能、新增了 Xcode Cloud 和可以直接在 Xcode 中构件展示官方文档了等等..

image-20210921024217111

更多更详细的内容就需要各位开发者自己亲自去研究和探索了。

总结

相对于之前的版本来说,Xcode 13 看起来让人感觉更加的舒服了,不管是文件风格还是展示形式都显得干净简洁。

当然安装包也还是那么大、还是那么的吃内存。无解~

目前使用起来还是比较顺手的,就是赶紧把编译提醒框退回来,不然每次 command + B 后都要网上看,多别扭。

收起阅读 »

升级到xcode13碰到的问题

iOS
经过了半个月的时间, xcode 没有暴露出来大的 BUG , 可以安心的升级了 然后问题来了, 各种适配问题, 开始撸起来 问题 : The Legacy Build System will be removed in a future release...
继续阅读 »

经过了半个月的时间, xcode 没有暴露出来大的 BUG , 可以安心的升级了


Xcode版本


然后问题来了, 各种适配问题, 开始撸起来



  1. 问题


: The Legacy Build System will be removed in a future release. You can configure the selected build system and this deprecation message in File > Workspace Settings.


错误详情


解决方案:



菜单栏 File->Workspace Settings-> BuildSystem

选择使用 New Buile System(Default)


错误详情



  1. 问题, 文件引入问题(Xcode12之前没有问题)



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/PEDat_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/PEDat_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/PEDat_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/WBCloudReflectionFaceVerify.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/WBCloudReflectionFaceVerify.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/WBCloudReflectionFaceVerify.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/detector_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/detector_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/detector_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/ufa_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/ufa_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/ufa_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”



解决方案:



target->Build Phases

具体如图, 报错的文件就行了



错误详情



  1. 问题: info.plist 文件问题, 由于项目中使用的库手动拉进来的,



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/AliyunOSSiOS/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

2) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/SupportingFiles/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

3) Target 'dudu' (project 'dudu') has process command with output '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'



解决方案:



直接删除手动拉进来的库里面的 `info.plist` 文件




  1. 项目里面有个 VERSION 的文件, 想不明白这个为啥



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/ST_Mobile/SenseArSourceService/VERSION' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION'

2) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/ST_Mobile/VERSION' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION'



解决方案:



修改一下 `VERSION` 的文件名称: 或者删除文件




  1. 处理了上面的问题, 依旧还是有问题, 接着修改



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/SupportingFiles/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

2) Target 'dudu' (project 'dudu') has process command with output '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'



解决方案:



1. 找到 Products 文件夹下的 项目, 邮件 show in finder, 然后向上找, 找到 DerivedData 文件下, 删除对用的文件

到这里这个问题应该已经修复了, 如果还有问题, 继续

2. Build Settings 里面 找到 info.plist 文件夹的位置, (先复制一下路径) 删除, 编译一下, 然后再添加上路径

如果还是不行:

3 Build Phases 里面 Copy Bundle Resources 删除 info.plist 文件



错误详情


好了, 到现在, 我的项目已经能正常运行了,


在翻阅的时候发现 JWAutumn 同学的文章 Xcode 13 更新了哪些内容 这个文章已经更新的比较全了, 可以参考一下


不知道有没有碰到其他问题的, 可以私信我一下, 给加在上面, 供大家参考




  1. Xcode 编译结果的提示, 中间的 Build Successed 和 小锤子不出来了, 没找到在哪里能修改的, 只能在软件上面提示, 感觉习惯了小锤子的提示, 这个修改感觉很不友好




  2. XcodeSnippets (就是自定义的代码块) 不自动提示了, 需要将自定义的名称打全才出来提示 , 比如: 设置的 mark 只有全部打出来才提示代码块, 不然不提示, 也是不友好, 目前没发现在哪里可以提示的




  3. 以前修改的文件, 当前文件的名字变灰, 知道编译后有哪些文件修改过了, 现在直接没了, 没找到哪里设置的 (这个修改很不友好)







作者:Keya
链接:https://juejin.cn/post/7016195057266999304

收起阅读 »

Xcode调试技巧总结

iOS
前言 本来觉得调试是一件很简单的事情,但是看了很多介绍调试方法的文章,发现有些技巧并不知道,有必要对常用的Xcode调试技巧做一个总结,提高工作效率。 一、调试面板 上方:断点开关、继续执行、单步执行、单步步入、单步步过等命令; 左边:watch窗口,负责变...
继续阅读 »

前言


本来觉得调试是一件很简单的事情,但是看了很多介绍调试方法的文章,发现有些技巧并不知道,有必要对常用的Xcode调试技巧做一个总结,提高工作效率。


一、调试面板


image.png


上方:断点开关、继续执行、单步执行、单步步入、单步步过等命令;

左边:watch窗口,负责变量信息显示,如果想查看寄存器的内容,可以将左下角的Auto切换为All

右边:日志窗口,接受和显示程序日志,左下角可以选择All/Debugger/Target output


二、断点


1- 普通断点


找到下断点的代码行,可以通过下面3种方式下断点:

(1)导航栏:Debug->Breakpoint->Add Breakpoint at Current Line

(2)快捷键:command +

(3)鼠标:直接在编辑区域左边行号的地方左键


大部分情况普通断点就满足需求了,但是对于一些特殊的调试情况,还需要掌握一些其他类型的断点。


2- 条件断点


适用场景

(1)一个函数重复多次被调用,但是只需要调试其中某一次的情况时;

(2)对于一些因为异常数据导致的bug调试也很实用;

下断点: 右键普通断点 -> Edit Breakpoint,条件断点和普通断点相比只是多了一个条件判断而已,和我们手动在断点代码加一个if条件判断效果一样,只有满足条件的情况才会断下来;


image.png


3- 符号断点


符号断点: 其实就是对一个特定的函数名下断点,这里的方法可以是OC方法或者C++函数名;

适用场景: 调试一些没有源码的模块时比较有用,比如调试一个第三方提供的Lib库,或者系统模块,可以给相应函数下断点,调试程序的运行流程,查看一些参数信息;

下断点 :断点Tab页 -> 点击下面+号 -> Symbolic Breakpoint


image.png


image.png


设置符号断点可以输入类名+函数名,也可以只输入函数名,xcode会自动匹配不同类中同名的方法进行断点,如下DJTPerson和DJTAnimal都有-(void)djt_run方法,会自动生成两个断点,一旦被调用就会命中断点:


image.png


4- 异常断点


适用场景: 异常断点用来调试程序抛出异常而导致退出,下个异常断点很快就能定位运行到那行代码出了问题;

下断点: 断点Tab -> 左下角+号 -> Exception Breakpoint

Exception Breakpoint也是可以编辑的,可以选择Exception类型,也可以选择在抛出异常或者捕获异常的时候断点等;


image.png


注:有的程序会使用异常来组织程序逻辑,比如微信扫一扫,所以如果Exception选了All,那么异常断点会频繁触发,所以这种情况可以只选择Objective-C异常。


下面是一个异常断点,在DJTPerson类中只有djt_run方法的声明没有实现,触发断点:


image.png


5- watch断点


顾名思义:watch断点就是当某个变量发生改变时触发的断点;

适用场景: watch断点对于要跟踪某个变量或者某个状态的变化时非常有用的,可以方便的跟踪到哪些地方改变了变量的值。

下断点: 在xcode的watch窗口 -> 右键需要watch的变量 -> watch "Xxx":


image.png


如例子中,当_name变量发生变化时调试器会自动断下来,同时输出变化信息:
image.png


6- 线程断点


线程断点: 线程断点适用在调试多线程代码的时候,一段代码可能会被多个线程同时执行,如果下普通断点,那么你会在不同线程之间切来切去,最后自己都迷糊了,这个时候可以使用多线程断点。

下断点: 调试区域右边控制台输出 -> breakpoint set –f 文件名 –l 行号 –t 线程id


下面例子在28行设置普通断点,就可以在控制台打印 thread-id,控制台输入:


thread info

获取当前线程id,在控制台通过命令行给32行设置线程断点:


breakpoint set -f ViewController.m -l 32 -t 0x331854

image.png


7- 断点后的Action


断点后的Action: 当断点被触发可以执行的一些操作;

下断点: 右键断点 -> Edit Breakpoint -> Add action


action类型很多,有调试命令、apple script、shell script等:

image.png


下边是在运行到断点后po一下person.name,直接打印了name的值:
image.png


如果觉得仅仅输出对象信息不够,还想加一些自己指定的内容,可以使用Log Message。


image.png


三、常用命令


1- p命令


p命令:查看基本数据类型的值

po命令:查看oc对象

简单查看一个变量或者OC对象的值在watch窗口完全可以满足,但是如果需要查看一个oc对象的属性,或者一个oc对象方法的返回值怎么办呢?p和po命令后面都可以接相应的表达式,如:


image.png


2- expr命令


expr命令:全称expression,可以在调试时动态修改变量的值,同时打印出结果。使用expr命令动态修改变量的值,可以在调试的时候覆盖一些异常路径,对调试异常处理的代码很有用。


image.png


3- call命令


call命令用来动态调用函数,可以在不增加代码不重新编译的情况下动态调用一个方法。下例动态将view1从父view移除:


image.png


4- image命令


image命令可以列出当前app中的所有模块,可以查找一个地址对应的代码位置,在调试越狱插件时,可以用image list命令查看越狱插件是否注入自己的App。当遇到crash时,查看线程栈只能看到栈帧的地址,使用image lookup -address 地址命令可以方便的定位这个地址对应的代码行。


5- bt命令


bt命令可以查看线程的堆栈信息,该信息也可以在导航区的Debug Navigator看到;

bt:打印当前线程栈
bt all:打印所有线程栈


image.png


分割线:上边介绍了基本的调试技巧,下面是一些不同场景下的调试经验


四、多线程


场景:在调试的时候bug不出现,一旦关闭调试直接运行bug就出现:这种问题大部分是因为多线程bug,而调试影响了多线程的执行顺序。

技巧:这种问题可以在关键点输出log,之前介绍的断点action中的Log Message就派上用场,这样的好处是不需要在代码中添加冗余的log即可调试;在调试多线程问题时,合理使用线程断点和条件断点也是很有帮助的;


五、UI调试


1-控件信息


查看控件信息除了使用p和po命令,还可以使用expr命令修改控件属性,如内容、坐标、大小等,这样可以不重启程序看到界面的变化;


2-界面结构


查看界面结构:po [view recursiveDescription],该命令可以打印出view的所有子view的结构关系,对于调试界面层级关系很有用;


3-快速预览


xcode支持在调试时对变量进行快速预览,调试时将鼠标放在变量上,然后点击快速预览按钮即可看到控件的显示。


image.png


4-符号断点跟踪UI变化


对于一些系统控件的信息,如果发现最终显示和自己设置的不一样,可以使用符号断点,在一些设置函数下断点,这样就可以很清晰的看到是从哪里改变了这个属性的值。比如一个UIButton的title在显示的时候和设置的不一样,只需要符号断点设置setTitle就可以跟踪哪里改了值;



作者:D___
链接:https://juejin.cn/post/6950852311346315271

收起阅读 »

黑科技- iOS静态cell和动态cell结合使用

iOS
1. 什么是静态Cell。 静态cell,可以直接布局cell样式的、group、insert group等直接拖@IBOutlet 布局简单,实用,比如我们同一类型的登陆、密码、设置、WIFI等页面 2. 怎么使用静态Cell。 必须使用StoryBo...
继续阅读 »

1. 什么是静态Cell。



  • 静态cell,可以直接布局cell样式的、group、insert group等直接拖@IBOutlet

  • 布局简单,实用,比如我们同一类型的登陆、密码、设置、WIFI等页面


2. 怎么使用静态Cell。



  • 必须使用StoryBoard来创建UITableViewController

  • image-20210823133103767.png

  • 然后你就可以直接使用cell的布局,运行出来就是StoryBoard的布局

  • image-20210823133226364.png

  • 运行以后的效果

  • image-20210823133337424.png


3. 和动态Cell结合。



  • 如例子:Wi-Fi的截图,我的网络和其他网络可以用动态cell来创建、其他的都可以直接用静态cell来创建


image-20210823132756571.png


使用步骤:




  1. 先在StoryBoard创建静态的cell,需要复用的cell留出一个位置即可

  2. 复用的cell必须单独创建(或者使用单独的xib文件)

  3. 这使用的时候,必须注册cell

  4. 动态的cell必须实现UITableViewDelegate的indentationLevelForRowAt这个方法

  5. 在这个方法里indentationLevelForRowAt返回第一个这StoryBoard留出位置的cell的indexPath


详情请看下列代码实现;



image-20210823133226364.png


第二个section 留白的就是给动态cell实现的


image-20210823134208316.png


创建一个xib的动态cell实现(可复用)


// 注册Cell
tableView.register(UINib(nibName: "CostomTableViewCell", bundle: nil), forCellReuseIdentifier: "CostomTableViewCell")


override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
   if indexPath.section == 1 {
       return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
   }
   return super.tableView(tableView, indentationLevelForRowAt: indexPath)
}


实现indentationLevelForRowAt方法,返回IndexPath(row: 0, section: 1)第一section 的第一个row,其他不需要复用的自己返回父类即可


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   if indexPath.section == 1 {
       let cell = tableView.dequeueReusableCell(withIdentifier: "CostomTableViewCell", for: indexPath) as! CostomTableViewCell
       cell.dynamic.text = "dynamicRow:(dynamicRowArray[indexPath.row])"
       return cell
   }
   return  super.tableView(tableView, cellForRowAt: indexPath)
}

在cellForRowAt复用里写已经要实现的复用的cell,其他静态cell直接返回父类即可


最终实现的效果


image-20210823134654515.png


4. Row、Section使用的技巧,以及常出现的问题。



  • 如果复用的是row,直接实现indentationLevelForRowAt这个方法和cellForRowAt方法即可 必须实现,不然会崩溃

  • 但是如果是复用的section,就必须实现UITableViewDataSource的和数据源相关的方法以下的几个方法


// 自定义动态section 的时候,以下方法必须实现,否则会崩溃
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
   nil
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
   nil
}

override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
   nil
}

override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
   nil
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
   5
}

override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
   .leastNonzeroMagnitude
}



  • 默认情况下会调用父类的数据,静态cell是不实现这些方法是越界的


注意事项:



  1. indentationLevelForRowAt 这个方法是动态和静态结合必须实现的方法

  2. Row、Section所需要实现的方法有差别,当是Section的时候需要实现与section相关的代理和数据源,例如sectionHeaderView、Footer等

  3. 动态cell一定要在storyboard里留白,自定义需要复用的cell必须使用xib、或者自定义,不能在原有的storyboard里创建

  4. 如果遇到崩溃,大多数是因为数据越界,数据源的问题,如果以上都实现,基本是没有问题的


Demo地址


作者:芭菲猫
链接:https://juejin.cn/post/6999504422065831943

收起阅读 »

std::out_of_range异常

iOS
使用C++容器类访问成员时由于使用问题可能会遇到"terminate called after throwing an instance of 'std::out_of_range'"或者"Abort message: 'terminating with un...
继续阅读 »

使用C++容器类访问成员时由于使用问题可能会遇到"terminate called after throwing an instance of 'std::out_of_range'"或者"Abort message: 'terminating with uncaught exception of type std::out_of_range"。问题的大概意思是:访问越界了。没有捕获std::out_of_range类型的异常终止。

通常在使用vector、map这样的C++容器类型时会遇到,这里我们以map类型为例,加以说明。

std::out_of_range异常的描述

假设我们定义了一个map类型的变量g_mapIsDestroyedRefCount,要访问容器中的数据项有多种方式。例如,获取g_mapIsDestroyedRefCount中key值为cameraId的值,可以这样:

  1. g_mapIsDestroyedRefCount[cameraId]
  2. g_mapIsDestroyedRefCount.at(cameraId)

两种写法都可以获取key为cameraId的value,一般效果看不出来差别,但是当g_mapIsDestroyedRefCount中不存在key为cameraId的<key, value>时就会出现“std::out_of_range”访问越界问题。

导致std::out_of_range的原因

容器类型访问方法使用有问题

对于std::map::at官方声明:

  mapped_type& at (const key_type& k);
const mapped_type& at (const key_type& k) const;

对于std::map::at使用有如下说明: Access element        访问元素

Returns a reference to the mapped value of the element identified with key k.      返回元素键为k的映射值的引用,即Key为k的元素的对应value值。
If k does not match the key of any element in the container, the function throws an out_of_range exception.   如果容器中没有匹配的k键,该函数将抛出一个out_of_range异常

 

std::map::at的使用

  • 正确使用
  • 错误使用

1.std::map::at的正确使用

 

#include <iostream>
#include <string>
#include <map>

std
::map<int, int> g_mapIsDestroyedRefCount;

int main()
{
int cameraId = 1;
cout
<< "Let's try"<< endl;

//向map中添加测试数据
g_mapIsDestroyedRefCount
.insert(std::pair<int, int>(0, 2))'
cout << "cameraId:"<< cameraId<< "count:";
try {
cout<< g_mapIsDestroyedRefCount.at(cameraId) <<endl;
} catch (const std::out_of_range& oor) {
std::cerr << "\nOut of range error:" << oor.what()<< endl;
}
cout << "try done"<< endl;
return 0;
}

 

运行结果:

 

2.std::map::at错误使用

#include <iostream>
#include <string>
#include <map>
using namespace std;

std
::map<int, int> g_mapIsDestroyedRefCount;

int main()
{
int cameraId = 2;

cout
<< "Let's try"<< endl;
g_mapIsDestroyedRefCount
.insert(std::pair<int, int>(0, 2));
cout
<< "cameraId:"<< cameraId<< "count:";

//介绍中说的方法一,可以访问
cout
<< g_mapIsDestroyedRefCount[cameraId]<< endl;
//方法二,异常
cameraId = 2;
count
<< g_mapIsDestroyedRefCount.at(cameraId)<< endl;
cout<< "try done"<< endl;
}

运行结果:(程序异常退出)


收起阅读 »

SwiftUI开发小技巧总结(不定期更新)

iOS
目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。 (注:本文主要面向对SwiftUI有一定基础的读者。) 调整状态栏样式 StatusBarStyle 尝试In...
继续阅读 »

目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。


(注:本文主要面向对SwiftUI有一定基础的读者。)


调整状态栏样式 StatusBarStyle


尝试Info.plistUIApplication.statusBarStyle方法无效。如果有UIViewController作为根视图,重写方法preferredStatusBarStyle,这样可以控制全局;如果要设置单个页面的样式用preferredColorScheme(.light),但测试似乎设置无效。还有另一个方法:stackoverflow.com/questions/5…


调整导航栏样式 NavigationBar


let naviAppearance = UINavigationBarAppearance()
naviAppearance.configureWithOpaqueBackground() // 不透明背景样式
naviAppearance.backgroundColor = UIColor.whiteColor // 背景色
naviAppearance.shadowColor = UIColor.whiteColor // 阴影色
naviAppearance.titleTextAttributes = [:] // 标题样式
naviAppearance.largeTitleTextAttributes = [:] // 大标题样式
UINavigationBar.appearance().standardAppearance = naviAppearance
UINavigationBar.appearance().compactAppearance = naviAppearance
UINavigationBar.appearance().scrollEdgeAppearance = naviAppearance
UINavigationBar.appearance().tintColor = UIColor.blackColor // 导航栏按钮颜色


注意configureWithOpaqueBackground()需要在其它属性设置之前调用,除此之外还有透明背景configureWithTransparentBackground(),设置背景模糊效果backgroundEffect(),背景和阴影图片等,以及导航栏按钮样式也可修改。


调整标签栏样式 TabBar


let itemAppearance = UITabBarItemAppearance()
itemAppearance.normal.iconColor = UIColor.whiteColor // 正常状态的图标颜色
itemAppearance.normal.titleTextAttributes = [:] // 正常状态的文字样式
itemAppearance.selected.iconColor = UIColor.whiteColor // 选中状态的图标颜色
itemAppearance.selected.titleTextAttributes = [:] // 选中状态的文字样式
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground() // 不透明背景样式
tabBarAppearance.stackedLayoutAppearance = itemAppearance
tabBarAppearance.backgroundColor = UIColor.whiteColor // 背景色
tabBarAppearance.shadowColor = UIColor.clear // 阴影色
UITabBar.appearance().standardAppearance = tabBarAppearance


注意configureWithOpaqueBackground()同样需要在其它属性设置之前调用,和UINavigationBarAppearance一样有同样的设置,除此之外还可以为每个标签项设置指示器外观。


标签视图 TabView


设置默认选中页面:方法如下,同时每个标签项需要设置索引值tag()


TabView(selection: $selectIndex, content: {})
复制代码

控制底部标签栏显示和隐藏:


UITabBar.appearance().isHidden = true


NavigationView与TabView结合使用时,进入子页面TabBar不消失问题:不用默认的TabBar,将其隐藏,自己手动实现一个TabBar,放在根视图中。


键盘


输入框获得焦点(弹出键盘):在iOS15上增加了方法focused(),注意这个方法在视图初始化时是无效的,需要在onAppear()中延迟一定时间调用才可以。在此之前的系统只能自定义控件的方法实现参考这个:stackoverflow.com/questions/5…


关闭键盘,两种方法都可以:


UIApplication.shared.keyWindow?.endEditing(true)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)


添加键盘工具栏:


.toolbar()


手势


获得按下和松开的状态:


.simultaneousGesture(
DragGesture(minimumDistance: 0)
    .onChanged({ _ in })
    .onEnded({ _ in })
)


通过代码滚动ScrollView到指定位置:借助ScrollViewReader可以获取位置,在onAppear()中设置位置scrollTo(),我们实际使用发现,需要做个延迟执行才会有效,可以把执行放在DispatchQueue.main.async()中执行。


TextEditor


修改背景色:


UITextView.appearance().backgroundColor


处理Return键结束编辑:


.onChange(of: text) { value in
if value.last == "\n" {
UIApplication.shared.keyWindow?.endEditing(true)
}
}


Text文本内部对齐方式


multilineTextAlignment(.center)


页面跳转


容易出错的情况:开发中会经常遇到这样的需求,列表中选择一项,进入子页面。点击按钮返回上一页。此时再次点击列表中的某一项,会发现显示的页面内容是错误的。如果你是用NavigationLink做页面跳转,并传递了isActive参数,那么是会遇到这样的问题。原因在于多个页面的使用的是同一个isActive参数。解决办法是,列表中每一项都用独立的变量控制。NavigationView也尽量不要写在TabView外面,可能会导致莫名其妙的问题。


属性包装器在init中的初始化


init方法中直接赋值会发现无法成功,应该用属性包装器自身的方法包装起来,赋值的属性名前加_,例如:


_value = State<Int>(initialValue: 1)
_value = Binding<Bool>.constant(true) // 也可以使用Swift语法特性直接写成.constant(true)


View如何忽略触摸事件


allowsHitTesting(false)

作者:iOS技术小组
链接:https://juejin.cn/post/7037780197076123685

收起阅读 »

设计一套完整的日志系统

iOS
需求日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关...
继续阅读 »

需求

日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关键点记录日志可以快速定位问题。

假设我们的用户量是一百万日活,其中有1%的用户使用出现问题,即使这个问题并不是崩溃,就是业务上或播放出现问题。那这部分用户就是一万的用户,一万的用户数量是很庞大的。而且大多数用户在遇到问题后,并不会主动去联系客服,而是转到其他平台上。

虽然我们现在有Kibana网络监控,但是只能排查网络请求是否有问题,用户是否在某个时间请求了服务器,服务器下发的数据是否正确,但是如果定位业务逻辑的问题,还是要客户端记录日志。

现状

我们项目中之前有日志系统,但是从业务和技术的角度来说,存在两个问题。现有的日志系统从业务层角度,需要用户手动导出并发给客服,对用户有不必要的打扰。而且大多数用户并不会答应客服的请求,不会导出日志给客服。从技术的角度,现有的日志系统代码很乱,而且性能很差,导致线上不敢持续记录日志,会导致播放器卡顿。

而且现有的日志系统仅限于debug环境开启主动记录,线上是不开启的,线上出问题后需要用户手动打开,并且记录时长只有三分钟。正是由于现在存在的诸多问题,所以大家对日志的使用并不是很积极,线上排查问题就比较困难。

方案设计

思路

正是针对现在存在的问题,我准备做一套新的日志系统,来替代现有的日志系统。新的日志系统定位很简单,就是纯粹的记录业务日志。Crash、埋点这些,我们都不记录在里面,这些可以当做以后的扩展。日志系统就记录三种日志,业务日志、网络日志、播放器日志。

日志收集我们采用的主动回捞策略,在日志平台上填写用户的uid,通过uid对指定设备下发回捞指令,回捞指令通过长连接的方式下发。客户端收到回捞指令后,根据筛选条件对日志进行筛选,随后以天为单位写入到不同的文件中,压缩后上传到后端。

在日志平台可以根据指定的条件进行搜索,并下载文件查看日志。为了便于开发者查看日志,从数据库取出的日志都会写成.txt形式,并上传此文件。

API设计

对于调用的API设计,应该足够简单,业务层使用时就像调用NSLog一样。所以对于API的设计方案,我采用的是宏定义的方式,调用方法和NSLog一样,调用很简单。

#if DEBUG
#define SVLogDebug(frmt, ...) [[SVLogManager sharedInstance] mobileLogContent:(frmt), ##__VA_ARGS__]
#else
#define SVLogDebug(frmt, ...) NSLog(frmt, ...)
#endif

日志总共分为三种类型,业务日志、播放器日志、网络日志,对于三种日志分别对应着不同的宏定义。不同的宏定义,写入数据库的类型也不一样,可以用户日志筛选。

  • 业务日志:SVLogDebug
  • 播放器日志:SVLogDebugPlayer
  • 网络日子:SVLogDebugQUIC

淘汰策略

不光是要往数据库里写,还需要考虑淘汰策略。淘汰策略需要平衡记录的日志数量,以及时效性的问题,日志数量尽量够排查问题,并且还不会占用过多的磁盘空间。所以,在日志上传之后会将已上传日志删除掉,除此之外日志淘汰策略有以下两种。

  1. 日志最多只保存三天,三天以前的日志都会被删掉。在应用启动后进行检查,并后台线程执行这个过程。
  2. 日志增加一个最大阈值,超过阈值的日志部分,以时间为序,从前往后删除。我们定义的阈值大小为200MB,一般不会超过这个大小。

记录基础信息

在排查问题时一些关键信息也很重要,例如用户当时的网络环境,以及一些配置项,这些因素对代码的执行都会有一些影响。对于这个问题,我们也会记录一些用户的配置信息及网络环境,方便排查问题,但不会涉及用户经纬度等隐私信息。

数据库

旧方案

之前的日志方案是通过DDLog实现的,这种方案有很严重的性能问题。其写入日志的方式,是通过NSData来实现的,在沙盒创建一个txt文件,通过一个句柄来向本地写文件,每次写完之后把句柄seek到文件末尾,下次直接在文件末尾继续写入日志。日志是以NSData的方式进行处理的,相当于一直在频繁的进行本地文件写入操作,还要在内存中维持一个或者多个句柄对象。

这种方式还有个问题在于,因为是直接进行二进制写入,在本地存储的是txt文件。这种方式是没有办法做筛选之类的操作的,扩展性很差,所以新的日志方案我们打算采用数据库来实现。

方案选择

我对比了一下iOS平台主流的数据库,发现WCDB是综合性能最好的,某些方面比FMDB都要好,而且由于是C++实现的代码,所以从代码执行的层面来讲,也不会有OC的消息发送和转发的额外消耗。

根据WCDB官网的统计数据,WCDBFMDB进行对比,FMDB是对SQLite进行简单封装的框架,和直接用SQLite差别不是很大。而WCDB则在sqlcipher的基础上进行的深度优化,综合性能比FMDB要高,以下是性能对比,数据来自WCDB官方文档。

单次读操作WCDB要比FMDB5%左右,在for循环内一直读。

15906481049447.jpg

单次写操作WCDB要比FMDB28%,一个for循环一直写。

15906481114970.jpg

批量写操作比较明显,WCDB要比FMDB180%,一个批量任务写入一批数据。

15906481277664.jpg

从数据可以看出,WCDB在写操作这块性能要比FMDB要快很多,而本地日志最频繁的就是写操作,所以这正好符合我们的需求,所以选择WCDB作为新的数据库方案是最合适的。而且项目中曝光模块已经用过WCDB,证明这个方案是可行并且性能很好的。

表设计

我们数据库的表设计很简单,就下面四个字段,不同类型的日志用type做区分。如果想增加新的日志类型,也可以在项目中扩展。因为使用的是数据库,所以扩展性很好。

  • index:主键,用来做索引。
  • content:日志内容,记录日志内容。
  • createTime:创建时间,日志入库的时间。
  • type:日志类型,用来区分三种类型。

数据库优化

我们是视频类应用,会涉及播放、下载、上传等主要功能,这些功能都会大量记录日志,来方便排查线上问题。所以,避免数据库太大就成了我在设计日志系统时,比较看重的一点。

根据日志规模,我对播放、下载、上传三个模块进行了大量测试,播放一天两夜、下载40集电视剧、上传多个高清视频,累计记录的日志数量大概五万多条。我发现数据库文件夹已经到200MB+的大小,这个大小已经是比较大的,所以需要对数据库进行优化。

我观察了一下数据库文件夹,有三个文件,dbshmwal,主要是数据库的日志文件太大,db文件反而并不大。所以需要调用sqlite3_wal_checkpointwal内容写入到数据库中,这样可以减少walshm文件的大小。但WCDB并没有提供直接checkpoint的方法,所以经过调研发现,执行database的关闭操作时,可以触发checkpoint

我在应用程序退出时,监听了terminal通知,并且把处理实际尽量靠后。这样可以保证日志不被遗漏,而且还可以在程序退出时关闭数据库。经过验证,优化后的数据库磁盘占用很小。143,987条数据库,数据库文件大小为34.8MB,压缩后的日志大小为1.4MB,解压后的日志大小为13.6MB

wal模式

这里顺带讲一下wal模式,以方便对数据库有更深入的了解。SQLite3.7版本加入了wal模式,但默认是不开启的,iOS版的WCDBwal模式自动开启,并且做了一些优化。

wal文件负责优化多线程下的并发操作,如果没有wal文件,在传统的delete模式下,数据库的读写操作是互斥的,为了防止写到一半的数据被读到,会等到写操作执行完成后,再执行读操作。而wal文件就是为了解决并发读写的情况,shm文件是对wal文件进行索引的。

SQLite比较常用的deletewal两种模式,这两种模式各有优势。delete是直接读写db-page,读写操作的都是同一份文件,所以读写是互斥的,不支持并发操作。而walappend新的db-page,这样写入速度比较快,而且可以支持并发操作,在写入的同时不读取正在操作的db-page即可。

由于delete模式操作的db-page是离散的,所以在执行批量写操作时,delete模式的性能会差很多,这也就是为什么WCDB的批量写入性能比较好的原因。而wal模式读操作会读取dbwal两个文件,这样会一定程度影响读数据的性能,所以wal的查询性能相对delete模式要差。

使用wal模式需要控制wal文件的db-page数量,如果page数量太大,会导致文件大小不受控制。wal文件并不是一直增加的,根据SQLite的设计,通过checkpoint操作可以将wal文件合并到db文件中。但同步的时机会导致查询操作被阻塞,所以不能频繁执行checkpoint。在WCDB中设置了一个1000的阈值,当page达到1000后才会执行一次checkpoint

这个1000是微信团队的一个经验值,太大会影响读写性能,而且占用过多的磁盘空间。太小会频繁执行checkpoint,导致读写受阻。

# define SQLITE_DEFAULT_WAL_AUTOCHECKPOINT  1000

sqlite3_wal_autocheckpoint(db, SQLITE_DEFAULT_WAL_AUTOCHECKPOINT);

int sqlite3_wal_autocheckpoint(sqlite3 *db, int nFrame){
#ifdef SQLITE_OMIT_WAL
UNUSED_PARAMETER(db);
UNUSED_PARAMETER(nFrame);
#else
#ifdef SQLITE_ENABLE_API_ARMOR
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if( nFrame>0 ){
sqlite3_wal_hook(db, sqlite3WalDefaultHook, SQLITE_INT_TO_PTR(nFrame));
}else{
sqlite3_wal_hook(db, 0, 0);
}
#endif
return SQLITE_OK;
}

也可以设置日志文件的大小限制,默认是-1,也就是没限制,journalSizeLimit的意思是,超出的部分会被覆写。尽量不要修改这个文件,可能会导致wal文件损坏。

i64 sqlite3PagerJournalSizeLimit(Pager *pPager, i64 iLimit){
if( iLimit>=-1 ){
pPager->journalSizeLimit = iLimit;
sqlite3WalLimit(pPager->pWal, iLimit);
}
return pPager->journalSizeLimit;
}

下发指令

日志平台

日志上报应该做到用户无感知,不需要用户主动配合即可进行日志的自动上传。而且并不是所有的用户日志都需要上报,只有出问题的用户日志才是我们需要的,这样也可以避免服务端的存储资源浪费。对于这些问题,我们开发了日志平台,通过下发上传指令的方式告知客户端上传日志。

037C8667-914E-43A7-8B6D-7B6EDD80E3A5.png

我们的日志平台做的比较简单,输入uid对指定的用户下发上传指令,客户端上传日志之后,也可以通过uid进行查询。如上图,下发指令时可以选择下面的日志类型和时间区间,客户端收到指令后会根据这些参数做筛选,如果没选择则是默认参数。搜索时也可以使用这三个参数。

日志平台对应一个服务,点击按钮下发上传指令时,服务会给长连接通道下发一个jsonjson中包含上面的参数,以后也可以用来扩展其他字段。上传日志是以天为单位的,所以在这里可以根据天为单位进行搜索,点击下载可以直接预览日志内容。

长连接通道

指令下发这块我们利用了现有的长连接,当用户反馈问题后,我们会记录下用户的uid,如果技术需要日志进行排查问题时,我们会通过日志平台下发指令。

指令会发送到公共的长连接服务后台,服务会通过长连接通道下发指令,如果指令下发到客户端之后,客户端会回复一个ack消息回复,告知通道已经收到指令,通道会将这条指令从队列中移除。如果此时用户未打开App,则这条指令会在下次用户打开App,和长连接通道建立连接时重新下发。

未完成的上传指令会在队列中,但最多不超过三天,因为超过三天的指令就已经失去其时效性,问题当时可能已经通过其他途径解决。

静默push

用户如果打开App时,日志指令的下发可以通过长连接通道下发。还有一种场景,也是最多的一种场景,用户未打开App怎么解决日志上报的问题,这块我们还在探索中。

当时也调研了美团的日志回捞,美团的方案中包含了静默push的策略,但是经过我们调研之后,发现静默push基本意义不大,只能覆盖一些很小的场景。例如用户App被系统kill掉,或者在后台被挂起等,这种场景对于我们来说并不多见。另外和push组的人也沟通了一下,push组反馈说静默push的到达率有些问题,所以就没采用静默push的策略。

日志上传

分片上传

进行方案设计的时候,由于后端不支持直接展示日志,只能以文件的方式下载下来。所以当时和后端约定的是以天为单位上传日志文件,例如回捞的时间点是,开始时间4月21日19:00,结束时间4月23日21:00。对于这种情况会被分割为三个文件,即每天为一个文件,第一天的文件只包含19:00开始的日志,最后一天的文件只包含到21:00的日志。

这种方案也是分片上传的一种策略,上传时以每个日志文件压缩一个zip文件后上传。这样一方面是保证上传成功率,文件太大会导致成功率下降,另一方面是为了做文件分割。经过观察,每个文件压缩成zip后,文件大小可以控制在500kb以内,500kb这个是我们之前做视频切片上传时的一个经验值,这个经验值是上传成功率和分片数量的一个平衡点。

日志命名使用时间戳为组合,时间单位应该精确到分钟,以方便服务端做时间筛选操作。上传以表单的方式进行提交,上传完成后会删除对应的本地日志。如果上传失败则使用重试策略,每个分片最多上传三次,如果三次都上传失败,则这次上传失败,在其他时机再重新上传。

安全性

为了保证日志的数据安全性,日志上传的请求我们通过https进行传输,但这还是不够的,https依然可以通过其他方式获取到SSL管道的明文信息。所以对于传输的内容,也需要进行加密,选择加密策略时,考虑到性能问题,加密方式采用的对称加密策略。

但对称加密的秘钥是通过非对称加密的方式下发的,并且每个上传指令对应一个唯一的秘钥。客户端先对文件进行压缩,对压缩后的文件进行加密,加密后分片再上传。服务端收到加密文件后,通过秘钥解密得到zip并解压缩。

主动上报

新的日志系统上线后,我们发现回捞成功率只有40%,因为有的用户反馈问题后就失去联系,或者反馈问题后一直没有打开App。对于这个问题,我分析用户反馈问题的途径主要有两大类,一种是用户从系统设置里进去反馈问题,并且和客服沟通后,技术介入排查问题。另一种是用户发生问题后,通过反馈群、App Store评论区、运营等渠道反馈的问题。

这两种方式都适用于日志回捞,但第一种由于有特定的触发条件,也就是用户点击进入反馈界面。所以,对于这种场景反馈问题的用户,我们增加了主动上报的方式。即用户点击反馈时,主动上报以当前时间为结束点,三天内的日志。这样可以把日志上报的成功率提升到90%左右,成功率上来后也会推动更多人接入日志模块,方便排查线上问题。

手动导出

日志上传的方式还包含手动导出,手动导出就是通过某种方式进入调试页面,在调试页面选择对应的日志分享,并且调起系统分享面板,通过对应的渠道分享出去。在新的日志系统之前,就是通过这种方式让用户手动导出日志给客服的,可想而知对用户的打扰有多严重吧。

现在手动导出的方式依然存在,但只用于debug阶段测试和开发同学,手动导出日志来排查问题,线上是不需要用户手动操作的。

dsasdlalfjsdafas.png


作者:刘小壮
链接:https://juejin.cn/post/7028229305050071071

收起阅读 »

iOS-组件化

iOS
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动 通过问题看本质!!! 组件化目的: 组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。 那什么时候要做组...
继续阅读 »

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动


通过问题看本质!!!


组件化目的:


组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。


那什么时候要做组件化呢?随着项目功能的复杂提升,各个业务代码耦合越来越多。这个时候就可以开始考虑组件化了。


通俗的讲,就好比你去宿舍附近的便利店买东西,直接走过去就到了。就没有必要打车了,打车效率还更低了呢。


如果你去公司(车程半小时),就有必要打车或者公交车了,走路那得多慢啊,等你走到了,估计都矿工几小时了。


组件化方案


1. URL路由方案;


2. runtime反射调用(简单反射及二次封装Target-action)


3. Target-action(category及动态调度);


4. protocol方案;


5. notification方案;


组件化的方案有很多种,没有哪种最好,只有哪种最合适。常见的是url-block、protocol-class、target-action方案。所以参考了网上的一些文章,对这3种方案做了一下简单的对比。


url-block 蘑菇街


路由中心维护一张路由表,url为key,block为value。


优点:

1、统一iOS、安卓的平台差异性


缺点:

1、url参数收到限制,只能传常规的字符串参数,无法传递data、image参数;


2、无法区分本地和远程情况;


3、组件本身依赖中间件,且分散注册的耦合较多;


4、启动时提供注册服务,保存在内存中。


protocol-class


优点:

1、扩展了本地调用的功能;


2、通过实现接口来提供服务,只是中间加了一层wrapper;


3、通过protocol-class做一个映射,在内存中保存一张映射表;


缺点:

还是存在内存中维护注册表的问题


target-action


使用target-action方式实现组件间的解耦,本身功能完全独立,不依赖中间件。


1、通过runtime进行反射,直接调用。


2、生成方法签名,通过invocation对象,直接执行invoke方法。


3、通过组件包装一层wrapper来给外界提供服务,不会对原组件代码造成入侵。


4、中间件是通过runtime来调用组件服务的,中间件的catergory提供服务给调度者。


5、使用者只需要依赖中间件,中间件又不需要依赖组件。


作者:龙在掘金62077
链接:https://juejin.cn/post/7023972006957678599
收起阅读 »

Swift 重构:通过预设视图样式,缩减代码量

iOS
通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高; 🌰🌰: { func application(_ application: UIApplication, didFinishLaunchin...
继续阅读 »

通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高;



🌰🌰:


{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

UIApplication.setupAppearance(.white, barTintColor: .systemBlue)
}


源码:


@objc public extension UIApplication{

/// 配置 app 外观主题色
static func setupAppearance(_ tintColor: UIColor, barTintColor: UIColor) {
_ = {
$0.barTintColor = barTintColor
$0.tintColor = tintColor
$0.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor,]
}(UINavigationBar.appearance())


_ = {
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.black], for: .normal)
}(UIBarButtonItem.appearance(whenContainedInInstancesOf: [UIImagePickerController.self]))


_ = {
$0.setTitleColor(tintColor, for: .normal)
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor

$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: tintColor,
], for: .normal)
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: barTintColor,
], for: .selected)
}(UISegmentedControl.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor
}(UISegmentedControl.appearance())


_ = {
$0.autoresizingMask = [.flexibleWidth, .flexibleHeight]
$0.showsHorizontalScrollIndicator = false
$0.keyboardDismissMode = .onDrag;
if #available(iOS 11.0, *) {
$0.contentInsetAdjustmentBehavior = .never;
}
}(UIScrollView.appearance())


_ = {
$0.separatorInset = .zero
$0.separatorStyle = .singleLine
$0.rowHeight = 60
$0.backgroundColor = .groupTableViewBackground
if #available(iOS 11.0, *) {
$0.estimatedRowHeight = 0.0;
$0.estimatedSectionHeaderHeight = 0.0;
$0.estimatedSectionFooterHeight = 0.0;
}
}(UITableView.appearance())


_ = {
$0.layoutMargins = .zero
$0.separatorInset = .zero
$0.selectionStyle = .none
$0.backgroundColor = .white
}(UITableViewCell.appearance())


_ = {
$0.scrollsToTop = false
$0.isPagingEnabled = true
$0.bounces = false
}(UICollectionView.appearance())


_ = {
$0.layoutMargins = .zero
$0.backgroundColor = .white
}(UICollectionViewCell.appearance())


_ = {
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UIImageView.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UILabel.appearance())


_ = {
$0.pageIndicatorTintColor = barTintColor
$0.currentPageIndicatorTintColor = tintColor
$0.isUserInteractionEnabled = true;
$0.hidesForSinglePage = true;
}(UIPageControl.appearance())


_ = {
$0.progressTintColor = barTintColor
$0.trackTintColor = .clear
}(UIProgressView.appearance())


_ = {
$0.datePickerMode = .date;
$0.locale = Locale(identifier: "zh_CN");
$0.backgroundColor = .white;
if #available(iOS 13.4, *) {
$0.preferredDatePickerStyle = .wheels
}
}(UIDatePicker.appearance())


_ = {
$0.minimumTrackTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISlider.appearance())


_ = {
$0.onTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISwitch.appearance())

}
}

作者:SoaringHeart
链接:https://juejin.cn/post/6974338640784654350

收起阅读 »

iOS Reachability

iOS
大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachabil...
继续阅读 »

大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。

为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachability 框架,比较著名的有Github上的 tonymillion/Reachability 以及 AFNetworking 中的 AFNetworkReachabilityManager 模块,它们的实现原理基本上都是对苹果公司的SCNetworkReachability API进行的封装。

1、SCNetworkReachability (SystemConfiguration.framework)

0.png

获取网络状态:

@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, assign) SCNetworkReachabilityRef reachability;
@property (nonatomic, strong) dispatch_queue_t serialQueue;

-(void)dealloc {
if (_reachability != NULL) {
CFRelease(_reachability);
_reachability = NULL;
}
}

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

//创建零地址,0.0.0.0地址表示查询本机的网络连接状态
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;

_reachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
_serialQueue = dispatch_queue_create("com.xmy.serialQueue", DISPATCH_QUEUE_SERIAL);

__weak __typeof(self) weakSelf = self;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"连接状态: %d", [strongSelf isConnectionAvailable]);
});
dispatch_resume(_timer);

[self startMonitor];
}

- (BOOL)isConnectionAvailable
{
SCNetworkReachabilityFlags flags;
//获取连接的标志
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(_reachability, &flags);

//如果不能获取连接标志,则不能进行网络连接,直接返回
if (!didRetrieveFlags) {
NSLog(@"Error. Could not recover network reachability flags");
return NO;
}

//根据连接标志进行判断
BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
BOOL needConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);

return (isReachable && !needConnection) ? YES : NO;
}

- (void)startMonitor
{
SCNetworkReachabilityContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
if (SCNetworkReachabilitySetCallback(_reachability, ReachabilityCallback, &context)) {
// Schedules the given target with the given run loop and mode.
// SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

// Schedule or unschedule callbacks for the given target on the given dispatch queue.
SCNetworkReachabilitySetDispatchQueue(_reachability, _serialQueue);
}
}

- (void)stopMonitor
{
SCNetworkReachabilitySetCallback(_reachability, NULL, NULL);

// Unschedules the given target from the given run loop and mode.
// SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
SCNetworkReachabilitySetDispatchQueue(_reachability, NULL);
}

static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
NSLog(@"%@, %d, %@", target, flags, info);
}

优点:

  • 使用简单,只有一个类,官方还有Demo,容易上手
  • 灵敏度高,基本网络一有变化,基本马上就能判断出来

缺点:

  • 现在很流行的公用wifi,需要网页鉴权,鉴权之前无法上网,但本地连接已经建立
  • 存在本地网络连接,但信号很差,实际无法连接到服务器情况
  • 能否连接到指定服务器,比如国内访问墙外的服务器

苹果的Reachability有如下说明,告诉我们其能力受限于此:
The SCNetworkReachability programming interface allows an application to determine the status of a system's current network configuration and the reachability of a target host.
A remote host is considered reachable when a data packet, sent by an application into the network stack, can leave the local device. Reachability does not guarantee that the data packet will actually be received by the host.
当应用程序发送到网络堆栈的数据包可以离开本地设备时,就可以认为远程主机是可访问的,不能保证主机是否实际接收到数据包。

2、SimplePing

ping 是 Windows、Unix 、Linux和macOS 等系统下一个常用的命令,利用 ping 命令可以用来测试数据包能否通过IP 协议到达特定主机,并收到主机的应答,以检查网络是否连通和网络连接速度,帮助我们分析和判定网络故障。

SimplePing是苹果封装好的ping的功能,它利用resolve host,create socket(send&recv data),解析ICMP 包验证 checksum 等实现了 ping功能。并且支持iPv4 和 iPv6。

ping 功能使用是 ICMP 协议(Internet Control Message Protocol),ICMP 协议定义了一组错误信息,当路由器或者主机无法成功处理一个IP 封包的时候,能够将错误信息回送给来源主机:

1.png ICMP用途:差错通知、信息查询、重定向等

2.png [1]给送信者的错误通知;[2]送信者的信息查询。

[1]是到IP 数据包被对方的计算机处理的过程中,发生了什么错误时被使用。不仅传送发生了错误这个事实,也传送错误原因等消息。

[2]的信息询问是在送信方的计算机向对方计算机询问信息时被使用。被询问内容的种类非常丰富,他们有目标IP 地址的机器是否存在这种基本确认,调查自己网络的子网掩码,取得对方机器的时间信息等。

Ping实现:

3.png Ping超时原因:

  • 目标服务器不存在
  • 花在数据包交流上的时间太长ping命令认为超时
  • 目标服务器不回答ping命令

SimplePing实现:

4.png SimplePing初始化:

let hostName = "www.baidu.com"
var pinger: SimplePing?
var sendTimer: NSTimer?

/// Called by the table view selection delegate callback to start the ping.
func start(forceIPv4 forceIPv4: Bool, forceIPv6: Bool) {
let pinger = SimplePing(hostName: self.hostName)
self.pinger = pinger

// By default we use the first IP address we get back from host resolution (.Any)
// but these flags let the user override that.
if (forceIPv4 && !forceIPv6) {
pinger.addressStyle = .ICMPv4
} else if (forceIPv6 && !forceIPv4) {
pinger.addressStyle = .ICMPv6
}

pinger.delegate = self
pinger.start()
}

/// Called by the table view selection delegate callback to stop the ping.
func stop() {
self.pinger?.stop()
self.pinger = nil

self.sendTimer?.invalidate()
self.sendTimer = nil

self.pingerDidStop()
}

/// Sends a ping.
/// Called to send a ping, both directly (as soon as the SimplePing object starts up) and
/// via a timer (to continue sending pings periodically).
func sendPing() {
self.pinger!.sendPingWithData(nil)
}

代理方法:

/// pinger.start()成功之后,解析HostName拿到ip地址后的回调
func simplePing(pinger: SimplePing, didStartWithAddress address: NSData) {
NSLog("pinging %@", MainViewController.displayAddressForAddress(address))

// Send the first ping straight away.
self.sendPing()

// And start a timer to send the subsequent pings.
assert(self.sendTimer == nil)
self.sendTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(MainViewController.sendPing), userInfo: nil, repeats: true)
}

/// pinger.start()功能启动失败的回调
func simplePing(pinger: SimplePing, didFailWithError error: NSError) {
NSLog("failed: %@", MainViewController.shortErrorFromError(error))

self.stop()
}

/// sendPingWithData发送数据成功
func simplePing(pinger: SimplePing, didSendPacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u sent", sequenceNumber)
}

/// sendPingWithData发送数据失败,并返回错误信息
func simplePing(pinger: SimplePing, didFailToSendPacket packet: NSData, sequenceNumber: UInt16, error: NSError) {
NSLog("#%u send failed: %@", sequenceNumber, MainViewController.shortErrorFromError(error))
}

/// ping发送后收到响应
func simplePing(pinger: SimplePing, didReceivePingResponsePacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u received, size=%zu", sequenceNumber, packet.length)
}

/// ping接收响应封包发生异常
func simplePing(pinger: SimplePing, didReceiveUnexpectedPacket packet: NSData) {
NSLog("unexpected packet, size=%zu", packet.length)
}

如代码所示,每隔一段时间就ping下host,看看是否畅通无阻,因此ping不可能做到及时判断网络变化,会有一定的延迟:
利用Reachability判断当前设备是否联网,利用SimplePing来检查服务器是否连通。

3、RealReachability (Star: 3k)

5.png

4、扩展:traceroute

由于ping命令不一定能判断对方是否存在,为了查看主机及目标主机之间的路由路径,我们使用traceroute 命令。它与ping 并列,也是ICMP 的典型实现之一。

traceroute是利用增加存活时间(TTL)值来实现功能的。每当一个icmp包经过一个路由器时,其存活时间值就会减1,当其存活时间为0时,路由器便会取消包发送,并发送一个ICMP TTL超时封包给原封包发出者。

6.png

7.png 命令行测试:

测试1
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 220.181.38.148
traceroute to baidu.com (220.181.38.148), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 2.198 ms 1.690 ms 1.437 ms
2 172.25.100.17 (172.25.100.17) 2.175 ms 1.795 ms 1.769 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 220.181.38.148 (220.181.38.148) 29.700 ms 29.135 ms 29.127 ms

测试2
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 39.156.69.79
traceroute to baidu.com (39.156.69.79), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 3.339 ms 1.993 ms 4.845 ms
2 172.25.100.17 (172.25.100.17) 2.146 ms 1.792 ms 1.971 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 * * *
17 * * *
18 39.156.69.79 (39.156.69.79) 29.015 ms 27.569 ms 28.232 ms

net-diagnosis (Star: 0.3k)
通过集成net-diagnosis,您可以轻松地在iOS上实现ping / traceroute /移动公共网络信息/端口扫描等网络诊断相关的功能。

8.png

9.png


收起阅读 »

iOS开发Crash之内存暴涨

iOS
今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全m...
继续阅读 »

今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全master指定版本真机测试.


全master真机运行出现的结果为


image.png


Message from debugger:Terminated due to memory issue
复制代码

看见这个错误大概就知道是内存暴涨被看门狗杀死了


关键是如何排查


使用instrument中的leaks工具来查看,但是我们并没有这样排查,我们发现一进APP过了main以后就被杀死了,那么可以初步确定是工作台发生的错误.


那么工作台的主要功能就是加载相应权限,然后配置菜单等功能,想到会不会是因为这个客户的数据异常,我们把网络关了,然后进入工作台,不会Crash了。


然后我们把工作台所用到的几个请求一一排查下来,发现在工作台有一个功能卡片组件,组件里面使用了富文本,而富文本是根据后端来的数据来进行对应的加载。


后端接口返回有一个字段,是否采用富文本,然后哪一段启用富文本,字体,颜色,样式都是由后端决定。


因为是一个tableView的Cell,里面又嵌套了for循环,for循环里面又嵌套了for来展示item,item又记录的一条条的富文本还有图片,犹豫cell每次数据源都会addSubviews,没有remove掉,再加上后端返回了N条,同事写的时候没有限制,我们这边只显示4条,显示的图片占用内存很大,从而导致内存暴涨,这一块因为是很老的代码了,需要重构一下,为了线上先不崩溃,跟后端商量图片压缩以及返回条数限制


最后记录一下crash日志
image.png


作者:小肥肥
链接:https://juejin.cn/post/7039598306267234318
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS整体框架介绍

iOS
这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战iOS整体框架通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:简单解释一下:Cocoa (Application) Layer(触摸层)Media Layer ...
继续阅读 »

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

iOS整体框架

通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:

image.png

简单解释一下:

  • Cocoa (Application) Layer(触摸层)
  • Media Layer (媒体层)
  • Core Services Layer(核心服务层)
  • Core OS Layer (核心系统操作层)
  • The Kernel and Device Drivers layer(内核和驱动层)

注:Cocoa (Application) Layer(触摸层)其实包含cocoa Touch layer(触摸层) 和Application Layer (应用层).应用层原本在触摸层上面,因为应用层是开发者自己实现,所以和触摸层合在一起.

其实每一层都包含多个子框架, 如下图:

image.png

简单解释下(瞄一眼就得了):

  • Cocoa Touch Layer:触摸层提供应用基础的关键技术支持和应用的外观。如NotificationCenter的本地通知和远程推送服务,iAd广告框架,GameKit游戏工具框架,消息UI框架,图片UI框架,地图框架,连接手表框架,UIKit框架、自动适配等等

  • Media Layer:媒体层提供应用中视听方面的技术,如图形图像相关的CoreGraphics,CoreImage,GLKit,OpenGL ES,CoreText,ImageIO等等。声音技术相关的CoreAudio,OpenAL,AVFoundation,视频相关的CoreMedia,Media Player框架,音视频传输的AirPlay框架等等

  • Core Services Layer:系统服务层提供给应用所需要的基础的系统服务。如Accounts账户框架,广告框架,数据存储框架,网络连接框架,地理位置框架,运动框架等等。这些服务中的最核心的是CoreFoundationFoundation框架,定义了所有应用使用的数据类型。CoreFoundation是基于C的一组接口,Foundation是对CoreFoundation的OC封装

  • Core OS Layer:系统核心层包含大多数低级别接近硬件的功能,它所包含的框架常常被其它框架所使用。Accelerate框架包含数字信号,线性代数,图像处理的接口。针对所有的iOS设备硬件之间的差异做优化,保证写一次代码在所有iOS设备上高效运行。CoreBluetooth框架利用蓝牙和外设交互,包括扫描连接蓝牙设备,保存连接状态,断开连接,获取外设的数据或者给外设传输数据等等。Security框架提供管理证书,公钥和私钥信任策略,keychain,hash认证数字签名等等与安全相关的解决方案。

想看更详细的可以移步:iOS总体框架介绍和详尽说明

我们只需要知道其中重要的框架就是UIKit和Function框架.下面说说这两个框架.

Function框架

Foundation框架为所有应用程序提供基本的系统服务。应用程序以及 UIKit和其他框架,都是建立在 Foundation 框架的基础结构之上。 Foundation框架提供许多基本的对象类和数据类型,使其成为应用程序开发的基础。

话不多说,我们先来看看Foundation框架,三个图,包括了Foundation所以的类,图中灰色的是iOS不支持的,灰色部分是OS X系统的。

image.png

image.png

image.png

这里只需要知道绝大部分Function框架的类都继承NSObject, 小部分继承NSProxy

对于Foundation框架中的一些基本类的使用方法详情参见:iOS开发系列—Objective-C之Foundation框架

UIKit框架

UIKit框架提供一系列的Class(类)来建立和管理iOS应用程序的用户界面( UI )接口、应用程序对象、事件控制、绘图模型、窗口、视图和用于控制触摸屏等的接口。

UIKit框架的类继承体系图如下图所示:

image.png

在图中可以看出,responder 类是图中最大分支的根类,UIResponder为处理响应事件和响应链定义了界面和默认行为。当用户用手指滚动列表或者在虚拟键盘上输入时,UIKit就生成事件传送给UIResponder响应链,直到链中有对象处理这个事件。相应的核心对象,比如:UIApplication ,UIWindowUIView都直接或间接的从UIResponder继承 。

这里需要知道一点:UIKit框架所有的类都继承NSObject

UIKit框架的各个类的简单介绍戳后面的链接:UIKit框架各个类的简要说明 

收起阅读 »

阿里二面:什么是mmap?

iOS
平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。传统IO在开始谈零拷贝之前,首先要对传统的IO方式有一个概念...
继续阅读 »

平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?

这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。

传统IO

在开始谈零拷贝之前,首先要对传统的IO方式有一个概念。

基于传统的IO方式,底层实际上通过调用read()write()来实现。

通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。

整个过程发生了4次用户态和内核态的上下文切换4次拷贝,具体流程如下:

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

  1. 用户进程通过read()方法向操作系统发起调用,此时上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,read()返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将应用缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

那么,这里指的用户态内核态指的是什么?上下文切换又是什么?

简单来说,用户空间指的就是用户进程的运行空间,内核空间就是内核的运行空间。

如果进程运行在内核空间就是内核态,运行在用户空间就是用户态。

为了安全起见,他们之间是互相隔离的,而在用户态和内核态之间的上下文切换也是比较耗时的。

从上面我们可以看到,一次简单的IO过程产生了4次上下文切换,这个无疑在高并发场景下会对性能产生较大的影响。

那么什么又是DMA拷贝呢?

因为对于一个IO操作而言,都是通过CPU发出对应的指令来完成,但是相比CPU来说,IO的速度太慢了,CPU有大量的时间处于等待IO的状态。

因此就产生了DMA(Direct Memory Access)直接内存访问技术,本质上来说他就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少CPU的等待时间。

但是无论谁来拷贝,频繁的拷贝耗时也是对性能的影响。

零拷贝

零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

那么对于零拷贝而言,并非真的是完全没有数据拷贝的过程,只不过是减少用户态和内核态的切换次数以及CPU拷贝的次数。

这里,仅仅有针对性的来谈谈几种常见的零拷贝技术。

mmap+write

mmap+write简单来说就是使用mmap替换了read+write中的read操作,减少了一次CPU的拷贝。

mmap主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。

整个过程发生了4次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过mmap()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. 上下文从内核态转为用户态,mmap调用返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将读缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。

sendfile

相比mmap来说,sendfile同样减少了一次CPU拷贝,而且还减少了2次上下文切换。

sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于使用sendfile替代了read+write从而节省了一次系统调用,也就是2次上下文切换。

整个过程发生了2次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU将读缓冲区中数据拷贝到socket缓冲区
  4. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile调用返回

sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。

sendfile+DMA Scatter/Gather

Linux2.4内核版本之后对sendfile做了进一步优化,通过引入新的硬件支持,这个方式叫做DMA Scatter/Gather 分散/收集功能。

它将读缓冲区中的数据描述信息--内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程

整个过程发生了2次用户态和内核态的上下文切换2次拷贝,其中更重要的是完全没有CPU拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器利用scatter把数据从硬盘中拷贝到读缓冲区离散存储
  3. CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
  4. DMA控制器根据文件描述符和数据长度,使用scatter/gather把数据从内核缓冲区拷贝到网卡
  5. sendfile()调用返回,上下文从内核态切换回用户态

DMA gathersendfile一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有CPU拷贝过程,极大提升了性能。

应用场景

对于文章开头说的两个场景:RocketMQ和Kafka都使用到了零拷贝的技术。

对于MQ而言,无非就是生产者发送数据到MQ然后持久化到磁盘,之后消费者从MQ读取数据。

对于RocketMQ来说这两个步骤使用的是mmap+write,而Kafka则是使用mmap+write持久化数据,发送数据使用sendfile

总结

由于CPU和IO速度的差异问题,产生了DMA技术,通过DMA搬运来减少CPU的等待时间。

传统的IOread+write方式会产生2次DMA拷贝+2次CPU拷贝,同时有4次上下文切换。

而通过mmap+write方式则产生2次DMA拷贝+1次CPU拷贝,4次上下文切换,通过内存映射减少了一次CPU拷贝,可以减少内存使用,适合大文件的传输。

sendfile方式是新增的一个系统调用函数,产生2次DMA拷贝+1次CPU拷贝,但是只有2次上下文切换。因为只有一次调用,减少了上下文的切换,但是用户空间对IO数据不可见,适用于静态文件服务器。

sendfile+DMA gather方式产生2次DMA拷贝,没有CPU拷贝,而且也只有2次上下文切换。虽然极大地提升了性能,但是需要依赖新的硬件设备支持。

收起阅读 »

Swift接入例子-适合多人协作

iOS
在「 Swift接入例子 」中介绍了Swift项目如何接入SOT。但是要求SDK解压到特定目录中,编译配置的路径也是绝对路径,不适合多人协作合开。文本介绍适合多人开发协作的接入方法。 还是以开源的「 SwiftMessages 」Demo为例,该工程全部用Sw...
继续阅读 »

「 Swift接入例子 」中介绍了Swift项目如何接入SOT。但是要求SDK解压到特定目录中,编译配置的路径也是绝对路径,不适合多人协作合开。文本介绍适合多人开发协作的接入方法。


还是以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言开发。这里把修改好的版本上传到了git上,分支为 「 sotcollaboration 」SotDebug接入免费版,SotRelease接入网站版,读者只需要进行下面的 Step1.配置编译环境 就可以直接用该分支测试。


现在开始从头讲解,git clone原本的工程后(我的路径为/Applications/SwiftMessages),命令行cd /Applications/SwiftMessages进入根目录,使用版本切换命令:git checkout 1e49de7b3780b699(因本文档制作于21年10月21号,以当日版本为准)。首先进入Demo目录,打开Demo.xcodeproj工程,scheme默认就已经选中了Demo:...


我使用的是Xcode12.4,可以直接编译成功,启动APP能看到画面(模拟器):


......


点击最上面的MESSAGE VIEW控件,会弹出一个错误提示窗口,今天我们就来用SOT热更的方式修改错误提示的文案:...


Step1: 配置编译环境


「 下载SOT的SDK 」,解压到项目目录下 /Applications/SwiftMessages/Demo/sotsdk...


在terminal运行命令:sh /Applications/SwiftMessages/Demo/sotsdk/compile-script/install.sh安装SOT编译工具链,需要输入密码。


用文本编辑器打开 /Applications/SwiftMessages/Demo/sotsdk/project-script/sotconfig.sh,修改EnableSot=1:...新版SDK已经不会再使用sotconfig.sh里的sdkdir,sotbuilder和objbuilder路径了,所以不用修改这些配置了,删掉也可以。


Step2: 增加Configuration


增加两个Configuration,只有切换到这两个Configuration才使用SOT编译模式,平时还是用原来的Configuration做开发,步骤如下:



  1. 选中Demo Project,然后选择Info面板,点击Configurations的下面加号,复制Debug的编译配置,并且命名为SotDebug,用来接入免费版的SOT。再选择复制Release编译配置,命名为SotRelease,用来配置网站版的SOT,注意名字都不要留有空格:...加完就是:...

  2. SwiftMessages也加上这两个Configuration:...


注意:读者应用到自己项目中时,需要把所有的工程都加上这两个Configuration,否则编译会报找不到文件等等的错误。所以加完这两个Configuration的之后,就马上切换到它们去Build和Run一下,看是否有编译错误,如果没有再进行下面的操作,如果有,请检查是否漏了一些工程没有添加上。


Step3: 修改编译选项


添加热更需要的编译选项,添加SOT虚拟机静态库等,步骤如下:




  1. 选中Demo工程,然后选择Demo这个Target,再选择Build Settings:...




  2. Other Linker FlagsSotDebug中添加-sotmodule $(PRODUCT_NAME) sotsdk/libs/libsot_free.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh


    SotRelease中添加-sotmodule $(PRODUCT_NAME) sotsdk/libs/libsot_web.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh


    每个选项的意义如下:



    • -sotmodule是module的名字,可以直接用$(PRODUCT_NAME),也可以自定义名字,名字不要有空格

    • -sotsaved是编译中间产物保存的目录,补丁自动化生成需要对比前后编译的产物来生成补丁

    • -sotconfig指定了项目sotconfig.sh的路径,该脚本控制sot编译器的工作

    • sotsdk/libs/libsot_free.a是SOT虚拟机静态库的路径,链接的是免费版的虚拟机

    • sotsdk/libs/libsot_web.a是SOT虚拟机静态库的路径,链接的是网站版的虚拟机




  3. Other C Flags以及Other Swift Flags的SotDebug和SotRelease下添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...




  4. Preprocessor Macros添加USE_SOT=1,后面用来控制是否编译调用SDK的代码...




  5. 因为SOT SDK库文件编译时不带Bitcode,所以也需要把Enable Bitcode设为No...




  6. 为了模拟器架构时不编译arm64,给SotRelease增加如下配置...或者把Build Active Architecture Only设为Yes




Step4: 增加拷贝补丁脚本


SDK里提供了一个便利脚本,路径在sdk目录的project-script/sot_package.sh,它会把生成的补丁拷贝到Bundle文件夹下,在每次项目编译成功时调用该脚本,添加步骤如下:


...


脚本内容为:



if [[ "$CONFIGURATION" == "SotDebug" || "$CONFIGURATION" == "SotRelease" ]];then
sh "$SOURCE_ROOT/sotsdk/project-script/sot_package.sh" "$SOURCE_ROOT/sotsdk/project-script/sotconfig.sh" "$SOURCE_ROOT/sotsaved/$CONFIGURATION" Demo
fi

复制代码

...


Based on dependency analysis的勾去掉。




Step5: 链接C++库


SOT需要压缩库和c++标准库的支持,还是在这个页面下,打开Link Binary With Libraries页...


点击加号,分别加入这两,libz.tbdlibc++.tbd...




Step6: 调用SDK API


需要用Swift代码调用OC代码,已经提供了一个样例代码在SDK的swift-call-objc目录中,先把callsot.h和callsot.m拷贝到Demo目录下...


再添加到Demo工程中。点击Xcode软件的File按钮,找到Demo目录下的callsot.h和callsot.m,接着点击Add Files to "Demo",如下图所示:...


点击Add按钮,添加后会弹出询问:是否创建桥接文件。点击按钮Create Bridging Header...


然后可以看到项目中多了3个文件,分别是callsot.h,callsot.m和Demo-Bridging-Header.h:...


打开Demo-Bridging-Header.h,加入一行代码#import "callsot.h"...


打开callsot.m,修改代码为


#import <Foundation/Foundation.h>
#import "callsot.h"
#import "../sotsdk/libs/SotWebService.h"
@implementation CallSot:NSObject
-(void) InitSot
{
#ifdef USE_SOT
#ifdef DEBUG
[SotWebService ApplyBundleShip];
#else

[SotWebService Sync:@"1234567" is_dev:false cb:^(SotDownloadScriptStatus status)
{
if(status == SotScriptStatusSuccess)
{
NSLog(@"SotScriptStatusSuccess");
}
else
{
NSLog(@"SotScriptStatusFailure");
}
}];

#endif
#endif
}
@end
复制代码

注意SotWebService.h的头文件路径不再依赖于绝对路径,并且代码里用了#ifdef USE_SOT宏来隔开API调用代码,不影响正常编译:...


打开AppDelegate.swift,加入两行代码let sot = CallSot()sot.initSot()...


注意:读者在应用到自己项目中时,以上这些配置的路径不要生搬硬套。例如找不到SotWebService.h文件,找不到sotconfig.sh文件等等,读者自己要清楚SDK的目录与自己工程目录的相对关系,灵活调整这些配置的路径。


测试热更-免费版


按上面配置完之后,先测试免费版热更功能


Step1: 热更注入



  1. Build Configuration切换到SotDebug...

  2. 确保sotconfig.sh的配置是,EnableSot=1以及GenerateSotShip=0,先Clean Build Folder一下,然后再Build:...


然后看编译日志的输出,Link日志可以看到run sot link等输出,会告诉你每个文件里哪些函数可以被热更等信息:......


项目编译成功了,该APP可以正常启动。同时它具备了热更能力,可以加载补丁改变程序的代码逻辑,下面介绍如何生成补丁来测试它。




Step2: 生成补丁


上一步进行了热更注入的编译,当时的代码保存到了Demo/sotsaved这个文件夹下,用来和新代码比较生成补丁。生成补丁步骤如下:



  1. 首先启动SOT生成补丁模式,修改sotconfig.shEnableSot=1GenerateSotShip=1

  2. ...

  3. 接下来直接在Xcode里修改源代码,把ViewController.swift文件的”Something is horribly wrong!“改成了”SOT is great“,修改前:...修改后:...

  4. Swift项目生成补丁,每次都需要先Clean项目,再Build项目。然后查看编译日志输出,可以看到生成了补丁并且被脚本拷贝到了Bundle目录下,可以展开Link Demo(x86_64)的编译日志:...点击展开后,可看到生成补丁的Link日志,日志里显示了函数demoBasics被修改了:...

  5. 生成出来的补丁原始文件保存到了Demo/sotsaved/SotDebug/x86_64/ship/ship.sot,还记得之前加了一个script到Build Phase中吗?它会每次编译结束时,会把这个补丁拷贝到了Bundle目录中,并且添加CPU架构到文件名中。可以在Bundle中看到这个补丁,至此完毕。




Step3: 加载补丁


启动APP,API会判断Bundle内是否有补丁,有则加载,加载成功的日志大概如下,提示有一个模块加载了热更补丁:...之后点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...


如果去Xcode断点调试demoBasics,会发现无法断住了,因为实际执行补丁代码的是SOT虚拟机。


顺便提一嘴,GenerateSotShip=1时,编译APP用的是保存在sotsaved目录下的代码,所以无论怎么修改Xcode里的代码,如果没有把补丁拷贝到Bundle目录里,那么APP都是最后一次GenerateSotShip=0热更注入时的样子。


如果怀疑,可以把拷贝补丁的Script脚本从Build Phases删除,可以发现GenerateSotShip=1怎么改代码都不会生效了。


注意:如果读者接入自己的一个很简单项目进行测试,例如设置某个控件的颜色,热更前是红色,修改后是绿色,发现无法生效。那是因为这样的项目太过于简单,寥寥几行代码。热更前没有访问过绿色的这个全局变量,在热更时也无法访问到了,SOT只能利用原有的能力,无法无中生有。所以不要这样测试,更具体的原因在「 热更能力-语言特性 」说明。通常完整的项目代码比较多,所以就不会有这样的缺陷。


接入网站版


按上面的教程,已经对APP实现了免费版和网站版的接入。它俩区别只是链接的库不一样,具体就是Other Linker Flags根据Configuration区别配置,SotRelease下接入了网站版。但除了APP接入了网站版SDK,还需要用配合网站来管理补丁的发布。


Step1: 注册网站



  1. 第一步当然是注册网站,成为会员。点击跳转注册页面,免费注册,注册需要验证邮箱,然后登录。

  2. 从导航栏进入我的APP:...

  3. 点击创建APP,弹出弹窗填写APP的名字:...

  4. 进入APP页面,点击右上角的创建新版本按钮,会弹出弹窗,需要选择网站版,SDK版本选择1.0,目前只有1.0版本,然后输入版本号,版本号可以是随意字符串,方便区分就行。...

  5. 创建版本成功后,点击版本,进入版本页面,左上角是唯一标识该版本的VersionKey,后面API接口需要这个Key。...


Step2: 修改VersionKey


打开callsot.m,修改网站版的Sync接口,第一个参数填入你在网站创建的版本的VersionKey。...至此,网站版热更就算接入完成了。


Step3: 测试网站热更




  • 网站版生成补丁的步骤免费版是一样的,需要经历热更注入->出包->修改代码->生成补丁,这里不再赘述。


    唯一不同的是,生成出来的补丁要上传到网站上,然后才能通过网络同步到手机上实现热更。通过之前的免费版教程,知道生成的补丁会被拷贝到Bundle目录下,所以去Bundle目录里就能找它,在Xcode导航栏里右键选择Products下的Demo.app,选择Show in Finder:...




  • 右键Demo文件,选择Show Package Contents:...




  • 找到目录下的sotship_arm64.sot,这里用手机测试,cpu是arm64类型,补丁名字带有cpu后缀,这就是补丁了:...




  • 回到网站的版本页面,点击右侧上传补丁按钮:...




  • 弹出页面里,真机的架构一般选择arm64,除非是老的armv7的机器,并把补丁文件拖到框里,点击上传:...




  • 上传成功并且补丁文件无异常(补丁最大支持5MB),则会添加成功,补丁默认是停用状态,需要点击编辑来启用它:...




  • 这里选择全量启用,点击下面的提交按钮,然后补丁就会成功启用了:...




  • 上一步更新了补丁状态,通常很快生效,但CDN有时也需要1到2分钟才能生效。之后手机打开APP,如果成功下载补丁和加载的话,就能看到下面的日志:...这里输出的md5也跟网站上的补丁md5是一致的。




  • 打开APP后,点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...




注意:使用网站版,需要考虑到网络传输延迟的问题,只有看到了下载补丁和成功加载补丁的日志之后,调用的函数才会使用热修后的函数。例如有的开发问我,首屏代码怎么无法热更生效?那是因为首屏代码调用的时机太早了,SOT去网站上拿补丁,是异步的,不会一直卡住等着,而且在异步线程中等待结果。在补丁没传输回来之前,首屏的代码都已经调用结束了,这种情况下当然调用的还是老的代码了。而免费版没有这个问题,因为免费版是同步加载补丁的,直接去Bundle里加载,不是异步的。


构建热更注入版本和构建补丁必须是同一台机器,同一个Xcode版本。例如上架前APP用Xcode12进行了热更注入,而之后用Xcode13来构建补丁,那么将得到无效甚至错误的补丁。请使用同一个版本Xcode。


Step4: 几点提示



  • 网站版跟免费版主要接入流程差不多,可以用免费版测试,功能通过测试之后再接入网站版。

  • 网站版需要有网络的情况下才能生效,如果手机没有网,即使之前已经下载过了补丁,也无法加载。

  • 网站版费用很低,日活10万的APP,一个月几百块就够了。

  • 网站版补丁和配置都放在CDN上,支持高并发。




非主Target接入热更


上面的教程都是针对主Target,也就是Demo。这个工程还有一个名为SwiftMessages的Framework,也可以热更,下面介绍如何配置。


可以看到SwiftMessages的Mach-O Type是Dynamic Library,通过下图方式查看得到:...


这种类型的话,配置相对麻烦些。还有一种是Static Library,配置起来会简单得多。但本例改成Static Library启动会崩溃,所以按Dynamic Library的方式来介绍。


Step1: 修改编译选项



  1. 选中SwiftMessages.xcodeproject工程,然后选择SwiftMessages这个Target,再选择Build Settings:...

  2. Other Linker FlagsSotDebug中添加-sotmodule $(PRODUCT_NAME) $(SRCROOT)/Demo/sotsdk/libs/libsot_free.a -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh

  3. Other Linker FlagsSotRelease中添加-sotmodule $(PRODUCT_NAME) $(SRCROOT)/Demo/sotsdk/libs/libsot_web.a -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh

  4. Other C Flags以及Other Swift FlagsSotDebugSotRelease中,添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...这一步跟Demo的配置差不多,区别在于有些路径写法不一样,以达到复用Demo配置的目的,读者可以仔细比较一下。

  5. Preprocessor Macros添加USE_SOT=1,后面用来控制是否编译调用SDK的代码...

  6. 需要把Target的Enable Bitcode设为No...

  7. 为了模拟器架构时不编译arm64,给SotRelease增加如下配置...


Step2: 链接C++库


点击Build Phases页面,打开Link Binary With Libraries页,点击加号,分别加入这两,libz.tbdlibc++.tbd...


Step3: 调用SDK API


因为SwiftMessages是动态库,所以需要在它的编译文件中调用SDK的热更初始化接口。跟Demo一样,添加OC文件。先从Demo文件夹中复制callsot.h和callsot.m文件到SwiftMessages文件夹中...


选中SwiftMessages工程,点击Xcode软件的File按钮,接着点击Add Files to "SwiftMessages.xcodeproject",如下图所示:...


选择到SwiftMessages目录,同时选中callsot.h和callsot.m两个文件,勾选下面的Copy items if needed,勾选Add to targets:中的SwiftMessages target,如下图所示:...


点击Add按钮,然后可以看到项目中多了2个文件,分别是callsot.h,callsot.m,修改CallSot类名为CallSotMessage:...


去到右边面板,把文件属性改成public:...


打开callsot.m,做相应路径和类名的修改:...


打开Demo-Bridging-Header.h,加入一行代码#import "SwiftMessages/callsot.h"...


打开AppDelegate.swift,加入两行代码let sot1 = CallSotMessage()sot1.initSot()...


因为这时候有两个Target都可以生成补丁,Demo和SwiftMessages,需要修改拷贝补丁的脚本,加入SwiftMessages:...


Step4: 测试热更




  1. 测试热更的流程跟之前是一模一样的,只是输出的日志可能会有所区别,我们过一遍。EnableSot=1和GenerateSotShip=0热更注入,先Clean后Build,如果去看编译日志的Link SwiftMessages,也可以看到热更注入的信息。




  2. 然后修改MessageView.swift的代码,错误提示的文案会加上“SOT is great”:...




  3. GenerateSotShip=1开启生成补丁模式,Clean后Build,查看Link SwiftMessages日志,有提示该函数被热更:...




  4. 接下来可以看到补丁拷贝脚本日志输出的信息,这里它检测到有两个Target都生成了补丁文件,会把它们两个合成一个,拷贝到Bundle目录下:...




  5. 启动APP,会看到两条加载补丁的日志,因为我们Demo Target和SwiftMessages Target都调用了API接口:...




  6. 点击MESSAGE VIEW控件,可以看到错误提示文案后面多了“SOT is great”,热更成功:


    ...网站版的测试跟以前也是一样的,这里不再重复了。




Step5: 几点提示



  1. Dynamic Library的热更编译改法其实跟主Target,也就是Mach-O Type为Executable的改法是一样,只是这里复用了主Target的一些配置,例如sotsaved目录和sotconfig.sh的路径。增加再多的Target也可以按同样的改法修改它们。

  2. 补丁拷贝脚本只需要主Target有就行了,把要热更的sotmodule对应的名字加上即可,条件就是sotsaved目录必须是同一个。

  3. 如果需要接入网站版,那么每个需要热更的Target都需要调用API跟网站同步,它们的消耗是独立计费的。


Static Library的改法


上面说到Dynamic Library的改法步骤比较多,而且有诸多缺点,如果能把Framework的Mach-O Type改成Static Library是最好的,会少很多步骤和配置。由于本例无法修改,这里简单说一下步骤:



  1. Other Libraian Flags添加-sotmodule $(PRODUCT_NAME) -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh ,注意是Other Libraian Flags而不是Other Linker Flags了。还有这里比Dynamic Library加的配置少一个,即没有链接SDK的.a库文件了。

  2. Other C Flags以及Other Swift Flags添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh,这步跟之前是一模一样的。

  3. 需要把Target的Enable Bitcode设为No

  4. 修改拷贝补丁的脚本,加入该Target的名字,例如本例加入SwiftMessages,跟之前也是一模一样的:...


然后就配置完了,如果是使用网站版,同步一次消耗,就能实现所有Target的热更,修改简单,对包体影响最小。




总结


本文完整介绍Swift项目如何接入免费版和网站版。


本文的方式是把SDK拷贝到了工程文件夹里,让它可以跟随项目一起进行版本管理,路径也配置成了相对路径,更加灵活。


通过新增Configuration的方式,也做到了不影响原来的开发,Debug和Release相当于没有接入SOT,适合大多数开发平时使用。只需上线前改成SotRelease出包,就能让APP就得到热更能力。


作者:忒修斯科技
链接:https://juejin.cn/post/7033403091550470180
收起阅读 »

一个录音项目的开发总结(一)

iOS
最近,工作之余,自己做了一个项目,项目的一期主要功能是音频录制和播放,音频格式包含m4a、mp3、wav三种格式,录制过程中要支持变音,还要能获取到metering以绘制录音过程的声音强弱变化图,播放功能包括音频波形图的绘制以及音频播放。 在做之前,我对iOS...
继续阅读 »

最近,工作之余,自己做了一个项目,项目的一期主要功能是音频录制和播放,音频格式包含m4a、mp3、wav三种格式,录制过程中要支持变音,还要能获取到metering以绘制录音过程的声音强弱变化图,播放功能包括音频波形图的绘制以及音频播放。


在做之前,我对iOS中的录音方面的知识了解甚少, 以至于走了很多弯路,虽然浪费了很多时间,但是也从中学到了很多知识,最终完成了项目的编码。


以下,主要介绍录音方面开发总结,播放方面后续记录。


在iOS中如果想实现录音功能,那么有四种方式可以实现:



  1. AVAudioRecorder :这是最简单的录音方式,只需要配置好录音格式就能得到相应的文件,但是相应的,这种方式无法得到录音过程中的音频源数据,无法实现变音功能和录制mp3文件。

  2. AVAudioEngine:AVAudioEngine功能强大,能实现录音、播放、混响、变音等功能,当我发现我这个类时,我高兴坏了,看了很多文档,结果做demo时,发现这个类外强中干,譬如你不能改变AudioEngine默认的inputNode、outputNode、mainMixNode的数据format,即使你千辛万苦找方法成功改变了数据格式,输出的数据也会狠狠地扇你一巴掌,告诉你高兴太早了,而且inputNode、outputNode、mainMixNode不会自动转换数据格式......如果想录制m4a、mp3还是需要其他方式才能实现。

  3. AudioQueue & AudioFile:AudioQueue在录音过程中有个回调方法,抛出编码后的音频数据,如果想处理音频数据,譬如变音、转码,可以在这个回调中实现,而且能获取到metering数据; AudioFile用于存储回调方法中抛出的音频数据,这个音频数据要与AudioFile创建时配置的inFormat一致。如果想了解audioqueue,可以看看这个文档

  4. AudioUnit &  ExtAudioFile:AudioUnit在录音过程中也会有回调,AudioUnit只支持pcm录制,所以要辅以ExtAudioFile来实现其他音频格式的录制,ExtAudioFile是个宝藏类,这个类可以实现音频格式的自动转换。


如果只是单纯的录用,那么使用AVAudioRecorder就可以非常简单的实现了,如果想对音频数据做处理,那么对于录音过程中抛出的数据请务必是pcm,只要能拿到pcm源数据,任何能做到的音频处理只要你想,都能实现。


iOS中,系统支持的录音编码格式为:


官方参考文档点击查看



wav文件是无损编码格式pcm,m4a文件编码格式AAC,mp3文件iOS系统不支持录制,系统支持mp3文件播放,有解码器但是无相应编码器,如果想录制mp3 文件只能依托于第三方的编码库,我采用的是lame库。


变音


iOS系统中本身是支持变音功能的,AudioUnit中有一个属性kNewTimePitchParam_Pitch,可以改变声音的音色,但是这个属性是MixerUnit的属性,录音的OutputUnit不支持这个属性,要想将 混音MixerUnit和录音OutputUnit连接到一块,需要AUGraph类去连接,但是AUGraph已弃用。。。系统推荐去使用AVAudioEngine类,这个类也是个坑,我在做demo时无法实现m4a文件的录制,就放弃了。


所以变音我采用的也是三方类SoundTouch,SoundTouch支持pcm编码、音频数据是lettle-endian,所以录音过程中抛出的音频buffer的format必须是pcm的,不然无法实现边录音边变音。


这样看起来AudioUnit貌似更适合这个项目一些,但是AudioUnit无法获取到录音过程中的metering数据,真是个令人悲伤的事情,AudioUnit很强大,如果AUGraph不被弃用,可能我会用它来录音。


最终我的实现方案是:


1、wav文件录制:AudioQueue录音、 AudioFile存储文件,AudioQueue和AudioFile的dataFormat为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;


AudioFile的fileType为kAudioFileCAFType


2、mp3文件:AudioQueue录音, lame转码, FILE文件存储, AudioQueue的编码格式为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger
| kLinearPCMFormatFlagIsPacked
|kAudioFormatFlagIsNonInterleaved


3、m4a文件:AudioQueue录音,  ExtAudioFile转码加存储,AudioQueue和ExtAudioFile的kExtAudioFileProperty_ClientDataFormat属性的编码格式为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;


ExtAudioFile文件的音频编码格式为:


AudioStreamBasicDescription outputFormat;outputFormat.mSampleRate = 44100;

outputFormat.mFormatID = kAudioFormatMPEG4AAC;outputFormat.mFormatFlags = 0;outputFormat.mBytesPerPacket = 0;outputFormat.mFramesPerPacket = 1024;

outputFormat.mBytesPerFrame = 0;outputFormat.mChannelsPerFrame = 1;outputFormat.mBitsPerChannel = 0;outputFormat.mReserved = 0;


关于四种录音方式的代码实现后续会更新,音频编辑功能,二期可能会上,到时也会研究、记录,希望到时自己不会太懒......


作者:阿喵同学
链接:https://juejin.cn/post/6936869349546426382

收起阅读 »

“杀死” App 上的疑难崩溃!

iOS
问题与背景在移动应用性能方面,崩溃带来的影响是最为严重的,程序崩溃可以打断用户正在进行的操作体验,造成关键业务中断、用户留存率下降、品牌口碑变差、生命周期价值下降等影响。很多公司将崩溃率作为优先级最高的技术指标,因此程序崩溃的监控与收集就成为了一项必不可少的工...
继续阅读 »

问题与背景

在移动应用性能方面,崩溃带来的影响是最为严重的,程序崩溃可以打断用户正在进行的操作体验,造成关键业务中断、用户留存率下降、品牌口碑变差、生命周期价值下降等影响。很多公司将崩溃率作为优先级最高的技术指标,因此程序崩溃的监控与收集就成为了一项必不可少的工作, 目前58同城App使用腾讯Bugly作为发布环境下App异常数据的收集工具。

我们的崩溃率一直在优化,每个版本都有专门负责监控线上崩溃以及解决问题的同学,经过我们不断的优化,目前 58同城iOS App的崩溃率维持在一个比较优秀的水准, Bugly上收集的崩溃大部分都是野指针崩溃和疑难崩溃。但是遗留的疑难崩溃优化手段比较有限,一个主要的原因是Bugly上的崩溃不能正常解析,定位不到真正原因。我们拿一个简单的例子来说明一下。

RN的HOOK函数问题

0 CoreFoundation  0x00000001804f504c 	0x000000018045c000 + 626764
1 Foundation 0x0000000181dae6cc 0x0000000181c7e000 + 1246924
2 UIKit 0x0000000198e5cf30 0x0000000198e57000 + 24368
3 AppName 0xe622388106d79fcc RCTFBQuickPerformanceLoggerConfigureHooks + 16244
4 CoreTelephony 0x0000000198e5e628 0x0000000198e57000 + 30248
5 CoreTelephony 0x0000000108f68fe4 0x0000000198e57000 + 78455260
6 CoreTelephony 0x00000001061ed870 0x0000000198e57000 + 30763624
7 CoreTelephony 0x0000000108f657ec 0x0000000198e57000 + 78440932
8 AppName 0x0000000108f67024 _ZN6tflite19AcquireFlexDelegateEv + 78447132
9 Foundation 0x0000000108f67024 _NSGetUsingKeyValueGetter + 88

在近几个版本中,我们发现Bugly上有大量的崩溃日志都会携带一个来自RN的函数调用栈: RCTFBQuickPerformanceLoggerConfigureHooks,这是一个RNHOOK函数。多条崩溃日志的堆栈都指向这个函数,且这个函数是一个空函数,没有任何实现,这让我们比较困扰。用过Bugly的同学都知道,Bugly每条崩溃日志都有个跟踪数据,记录着这个崩溃发生之前页面的跟踪日志,通过页面的跟踪日志我们发现这些崩溃中用户浏览的页面大多数都不涉及RN业务,与RN没有任何关系。而且每条崩溃的页面跟踪日志也不相同。既然程序崩溃之前浏览的业务不涉及RN但Bugly上的堆栈确指向RN,因此我们怀疑这种崩溃不是崩溃在RNHOOK函数上并且它们是不同错误导致的崩溃。带着这种疑问,我们开始验证这个猜想,来看一看我们的怀疑是否准确。

如何验证Bugly解析错误

因为Bugly无法拿到应用崩溃后所产生的ips文件,无法利用symbolicatecrash等工具符号化日志。因此我们采用atos命令来验证我们的怀疑是否正确。

1. atos验证

atos工具会输出崩溃的代码语句和它所在的文件以及行数,前置条件是需要拿到dSYM文件,确定手机架构是arm64还是armv7,还需要拿到atos需要的load-addressaddress,根据这些信息就能够找到问题所在。aots命令格式如下:

atos -o yourAppName.app.dSYM/Contents/Resources/DWARF/yourAppName -arch arm64/armv7 -l <load-address> <address>

怎么获取dSYM文件与架构这里就不做详细介绍了,我们来看一下怎么在Bugly的崩溃日志中拿到load-addressaddress

一般以app命名的地方就是崩溃的位置,例如:正常的一个崩溃日志格式为:

0x0000000103ef6970 0x0000000102728000 + 30252

其中0x0000000103ef6970为运行地址,就是atos需要的address0x0000000102728000为运行起始地址,就是atos需要的load address302522为偏移量,一般来说,偏移量 + 运行起始地址 = 运行地址。

介绍完atos需要的load address(运行起始地址)与address(运行地址)之后,再来看一下RCTFBQuickPerformanceLoggerConfigureHooks这个函数的崩溃,根据图中示例我们看到这个崩溃的运行地址为0xe622388106d79fcc,但是这个崩溃地址是错误的,一般地址小于0xFFFFFFFFFF,示例中明显大很多。因此我需要将高位地址清洗,清洗后此地址为0x106d79fcc。因此address0x106d79fcc

接下来我们打开Bugly其他信息一栏,看到App base addr(基地址):0x0000000102604000,这个就是atos需要需要的load address。 image20211116110000581.png 通过上述信息,我们以RCTFBQuickPerformanceLoggerConfigureHooks这个函数为例验证一下Bugly的解析结果是否正确

➜ atos -o AppName.app.dSYM/Contents/Resources/DWARF/AppName -arch arm64 -l 0x0000000102604000 0x0000000106d79fcc
-[NSMutableDictionary(YJKit) yjKit_setObject:forKey:] (in AppName) (YJKit.m:432)

结果发现atos符号化后的结果与Bugly给我们的结果确实不一致。再根据Bugly的页面跟踪数据我们确认atos符号化后的结果是正确的,这与我们的怀疑是一致的。

既然Bugly的堆栈错误的指向了这个RN的空函数,那么我们就来看一看源码中RCTFBQuickPerformanceLoggerConfigureHooks是怎样的存在。

**2. 源码中的RCTFBQuickPerformanceLoggerConfigureHooks**函数

RCTFBQuickPerformanceLoggerConfigureHooks函数在源码中的声明如下:

image-20211116110100992.png 源码中,这个函数没有任何实现,完全是一个空函数。将RCT__EXTERN 展开后为__attribute__((visibility("default"))),其作用为将RCTFBQuickPerformanceLoggerConfigureHooks向外界暴露,如果外界存在同名函数,那么RCTFBQuickPerformanceLoggerConfigureHooks会报符号冲突的错误。这里利用__attribute__((weak))RCTFBQuickPerformanceLoggerConfigureHooks声明为弱符号,当外界有同名函数时,SDK内部调用外届的函数,否则调用内部空函数,这个弱符号在RN里起到了HOOK的作用 ,接下来我们就详细的了解一下弱符号。

3. 弱符号__attribute__ ((weak))

在一个程序中,无论是变量名,还是函数名,在编译器的眼里,就是一个符号而已,符号可以分为强符号和弱符号。对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量是弱符号,强符号和弱符号在程序编译连接过程中一般遵循下面三个规则:

  1. 不允许强符号被多次定义。如果有多个强符号,会报符号重定义错误

  2. 如果有一个强符号,其他定义都是弱符号,则选择强符号

  3. 如果一个符号在所有文件中都是弱符号,则选择其中一个占用空间最大的

强弱符号规则定义摘选自:强符号和弱符号,强引用和弱引用

duplicate symbol '_OBJC_CLASS_$_XXX'这个错误大家应该都比较熟悉,通过错误的描述我们很容易就可以知道这是因为在链接的时候有重复的符号。在编译时,编译器向汇编器输出每个全局符号,若两个或两个以上全局符号(函数或变量名)名字一样,且都是强符号就会出现符号重定义错误,如果有一个是弱符号(weak symbol),则不会出现问题。

一个程序内同时存在强符号与弱符号时,链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。当有函数或变量名可能被用户覆盖时,该函数或变量名可以声明为一个弱符号。可以通过__attribute__((weak))来定义弱符号。

4. 弱符号的使用

在开发中,假如我们不确定外部模块是否提供一个函数func,但是我们不得不用这个函数,即自己模块的代码必须用到func函数:

extern int func(void);
...int a = func;
...

我们不知道func函数是否被定义了,这会导致2个结果:

  1. 外部存在这个函数func,那么在我自己的模块使用这个函数func,正确。
  2. 外部如果不存在这个函数,那么我们使用func,程序直接崩溃。

所以这个时候,__attribute__((weak)) 派上了用场,在自己的模块中定义:

int __attribute__((weak)) func(......)
{
return 0;
}

将本模块的func转成弱符号类型,如果遇到强符号类型(即外部模块定义了func),那么我们在本模块执行的func将会是外部模块定义的func。如果外部模块没有定义,那么,将会调用这个弱符号。

我们发现Bugly对某些没有解析正确的崩溃,堆栈都会定位到项目中的弱符号上,同时我们还发现在58同城App中,Bugly不单单定位到RCTFBQuickPerformanceLoggerConfigureHooks这一个弱符号上,还有大量的崩溃定位到了其他的弱符号上。

上面我们通过atos还原了正确的日志,并定位到了是弱符号的问题,下面我们结合符号表来看一下日志符号化的原理。

如何处理bugly解析异常的数据

Crash 日志在被符号化之前是不可读的,所谓符号化就是把堆栈信息解释成源码里可读的函数名或方法名,也就是所谓的符号。只有符号化成功后,Crash 日志才能更好的帮助开发者定位问题。日志的解析需要用到dSYM文件,dSYM指的是 Debug Symbols, 也就是调试符号。

DWARF是一种被众多编译器和调试器使用的用于支持源码级别调试的调试文件格式,该格式是一个固定的数据格式,dSYM就是按照DWARF格式保存调试信息的文件,我们常常称为符号表文件。

日志的符号化有很多种方式,例如xcode分析、symbolicatecrashatosdwarfdump等,本质其实就是查找崩溃指令在符号表哪个函数的指令区间。今天我们主要讲一下Bugly解析不准的日志怎么在符号表里查找出正确的堆栈。

1. Bugly还原正确堆栈的原理

以弱符号RCTFBQuickPerformanceLoggerConfigureHooks函数为例,还原一下日志的解析原理。

0 CoreFoundation  0x00000001804f504c 	0x000000018045c000 + 626764
1 Foundation 0x0000000181dae6cc 0x0000000181c7e000 + 1246924
2 UIKit 0x0000000198e5cf30 0x0000000198e57000 + 24368
3 AppName 0xe622388106d79fcc RCTFBQuickPerformanceLoggerConfigureHooks + 16244
4 CoreTelephony 0x0000000198e5e628 0x0000000198e57000 + 30248
5 CoreTelephony 0x0000000108f68fe4 0x0000000198e57000 + 78455260
6 CoreTelephony 0x00000001061ed870 0x0000000198e57000 + 30763624
7 CoreTelephony 0x0000000108f657ec 0x0000000198e57000 + 78440932
8 AppName 0x0000000108f67024 _ZN6tflite19AcquireFlexDelegateEv + 78447132
9 Foundation 0x0000000108f67024 _NSGetUsingKeyValueGetter + 88

  • 上图中我们看到 RCTFBQuickPerformanceLoggerConfigureHooks 这行调用栈的虚拟内存地址存在异常,一般地址地址小于0xFFFFFFFFFF ,示例中明显大很多。我们将高位地址清洗后来保证堆栈正常。调整后,地址为 0x106d79fcc,但当然不是每个Bugly解析错误的日志虚拟内存地址都异常,如果是正常的,则不用改变
  • 查看其他信息,找到基地址App base addr,此处为 0x102604000。如果崩溃发生在其他动态库,那么查找下方对应动态库的地址。
  • 经过第一步和第二步,我们获取到了 0x106d79fcc 和 0x102604000
  • 指令偏移地址为:0x4775FCC = (步骤1)0x106d79fcc - (步骤2)0x102604000
  • 找到此次打包对应的Bugly符号表,并以文本的方式打开
  • 查找0x4775FCC在哪一行符号区间内
  • 最终查找到其在 0x4775fb4 ≤ 0x4775FCC < 0x4775fd0,即3997407行的符号,符号区间遵循前闭后开原则

image20211112210415260.png 通过以上步骤我们找到了RCTFBQuickPerformanceLoggerConfigureHooks函数的实际崩溃位置,并且与我们用atos工具验证后的结果一致,说明这个结果是正确的。

上面我们在符号表里查找到Bugly解析错误的日志的正确堆栈,那如果没有符号表怎么呢,这就涉及到了提取符号表。

2. 如何提取符号表

如果符号表丢失了,但是代码没有改动,那么可以尝试在相同的环境下重新编译和提取符号表,这个步骤有两个前提 1. 代码要与之前保持一致 2. 编译和链接环境都相同,防止由于Debug/Relase对最终包有影响。如果是Debug包,可以用过dsymutil xxx.app/xxx -o xxx.dSYM 来提取符号表

有了以上两个前提就可以通过dSYM文件来提取符号表了,目前我们实现了Bugly轻量符号表的提取,并且文件体积相对于Bugly符号表体积减少到60%。推动ICI(58项目管理平台)按照一定规则输出符号表,目前可以做到根据崩溃日志的UUID直接下载对应的符号表,日志解析和问题排查效率极大提高。

3. 无符号表符号化日志

如果既找不到符号表(dSYM文件或symbol文件),也无法恢复到原先的代码重新生成符号表,那么可以考虑借助无符号表符号化工具 WBBlades 来还原日志:github.com/wuba/WBBlad…

WBBlades是基于Mach-O文件解析的工具集,包括未使用代码检测(支持ObjCSwift)、应用程序大小分析、不需要dSYM文件的日志恢复。

由于方案自身的限制,目前还不能解析除了OC方法以外的崩溃日志,如:block的崩溃、自定义C函数的崩溃。后续需要考虑如何将block的崩溃日志进行符号化。

优化成果与收益

现在我们知道了当Bugly解析不准的时候,我们可以利用Bugly给我们提供的其他信息在符号表里找到正确的答案。通过以上研究,我们通过自研解析工具重新对Bugly的日志进行符号化,通过工具我们在集团内部解决了除RNHOOK函数问题以外还解决了多个遗留已久的历史版本崩溃问题,这里简单的介绍几个比较有代表性的。

1. 拿不到基地址的问题

通过RCTFBQuickPerformanceLoggerConfigureHooks函数的崩溃的介绍,我们可以在Bugly的其他信息里获取到日志的基地址,通过这个地址我们不论是用atos验证还是手动在符号表里查找都可以还原正确的堆栈,但是如果Bugly的其他信息里没有基地址怎么办,我们来看一下下面的这种崩溃日志。

0 CoreFoundation 0x00000001835891b8 0x0000000183459000 + 1245624
5 UIKit 0x000000018963a660 0x000000018942f000 + 2143840
6 AppName 0x00000001075c9904 str_to_integral_8ExpectedIT_NS_14Conversion + 1950052
7 AppName 0x000000010627f94c RCTFBQuickPerformanceLoggerConfigureHooks + 3098344
8 AppName 0x00000001062015a0 RCTFBQuickPerformanceLoggerConfigureHooks + 2581308
9 AppName 0x00000001061fe498 RCTFBQuickPerformanceLoggerConfigureHooks + 2568756
10 AppName 0x00000001061fed38 RCTFBQuickPerformanceLoggerConfigureHooks + 2570964
11 AppName 0x00000001061ed900 RCTFBQuickPerformanceLoggerConfigureHooks + 2500252
12 AppName 0x0000000105231bd8 _ZZGetAppIdTableEvE12arAppIdTable + 57325128
13 libdispatch.dylib 0x00000001824121fc 0x0000000182411000 + 4604
21 UIKit 0x00000001894a4534 UIApplicationMain + 208
22 AppName 0x00000001085b73e8 _ZN15CTXAppidConvert13GetAppIdTableEv + 8521236
23 libdyld.dylib 0x00000001824455b8 0x0000000182441000 + 17848

通过上面的堆栈信息我们看到崩溃的调用栈也停留在了弱符号RCTFBQuickPerformanceLoggerConfigureHooks上,但与我们上面举的例子的不同点是这个崩溃在Bugly上的其他信息一栏里是空的,也就是拿不到基地址,因此我们使用atos命令是不可行的,所以只能在符号表里查找,但是我们要首先要拿到基地址。下面我们来看一下遇到这种情况该怎样拿到基地址。

  1. 首先我们看到 22 AppName 0x00000001085b73e8 str_to_integral_8ExpectedIT_NS_14Conversion + 8521236 这一行信息,熟悉Bugly与crash 日志的同学一定知道, 这一行大概率是main函数,那么我们就在这里找到突破口。
  2. 我们看到main函数的调用栈符号是_ZN15CTXAppidConvert13GetAppIdTableEv,这个函数运行地址是0x00000001085b73e8
  3. 那这个函数的运行起始地址为 0x00000001085b73e8 - 0x8521236 = 0x107D96DD4
  4. 打开符号表,找到_ZN15CTXAppidConvert13GetAppIdTableEv这个符号的偏移地址为 0x7d86dd4
  5. App base addr(基地址): 0x107D96DD4 - 0x7d86dd4 = 0x100010000

这样我们就拿到了这个日志的基地址,然后利用上面的方式在符号表里找到正确的堆栈,因此也就能将这个日志正确的解析了。

2. 百度地图SDK的崩溃问题

除了RNHOOK函数问题,我们还发现有大量的崩溃日志调用栈都指向了百度地图SDK。 image20211116110252571.png 首先我们通过Bugly显示的堆栈信息以为是百度地图SDK的崩溃,这个崩溃在某几个版本中占58同城App总崩溃率的40%左右,是58App内崩溃率最高的一个模块,在更换了新的SDK后崩溃率也并没有下降,而这么高的崩溃率,我们在开发与测试中却从未遇到过。通过Bugly上的跟踪数据我们看到最后的页面记录停留在了金融业务内,而金融业务与百度地图没有任何关系。因此这个崩溃应该与上面描述的一样,解析错误。拿到基地址与运行地址,通过我们自研的工具拿到了正确的堆栈。结果为金融业务使用的一个人脸识别SDK的崩溃,文件名称与Bugly上的跟踪日志也相同。

3. 安居客IM登录问题

我们编写了脚本文件,利用脚本文件定位了一个安居客存在很久的问题。在Bugly上排名比较靠前,崩溃占比很高,Bugly上的堆栈显示异常,因此这个崩溃之前并没有定位到具体原因。脚本协助安居客定位是IMSDK的原因。 image20211116110315205.png

以上是几个比较具有代表性的Bugly解析错误的日志,我们通过研究分析将这些错误的堆栈还原正确并解决了问题。

目前我们支持按版本自动排查出Bugly上前200名崩溃中解析异常的日志,并且可以将异常日志自动符号化成正确的日志。整个过程在符号表已经提前下载并解析好的前提下,只有10秒左右,大大提升了我们日常研发以及解决问题的效率。通过对Bugly上的疑难崩溃的治理,目前为止我们修复了Bugly上70%左右的疑难崩溃,大大降低了58 App的崩溃率。 image20211125150625544.png 除了上述我们研究的Bugly解析异常的日志可以正确解析外,58同城还支持其他异常日志的解析。 例如App内存在段迁移发生崩溃后的日志,段迁移崩溃日志中的库名变成了异常字符、丢失了进程的起始地址,获取到错误的偏移地址,这种情况下我们可以进行自动修正并解析出正确的堆栈信息。

总结与展望

文本首先介绍了我们使用Bugly遇到的RNHOOK函数问题,通过这个问题我们提出Bugly可能解析存在错误的疑问,后续用atos命令以及符号表排查找到了正确的答案,过程中又发现了弱符号的问题。按照这个研究方向我们在集团内做了一系列工具并解决了多个版本的历史遗留问题,大大的降低了58同城iOS App的崩溃率,也提高了日常工作研发效率。

App的性能优化对用户的体验十分重要,而崩溃作为其中最重要的一个环节需要我们持续的钻研与探索。后续我们将持续优化App的性能给用户带来最好的体验。

首发自CSDN:“杀死” App 上的疑难崩溃!


作者:ZYJ
链接:https://juejin.cn/post/7037308047382806565

收起阅读 »

iOS 实现类似探探、陌陌的卡片左滑右滑效果

iOS
本文章分析怎么实现这种卡片效果以及都有哪些功能,基于这些功能是否可以完善,让框架更加灵活,可拓展等。现已封装成通用框架。效果图如下: 代码地址 1、功能分析 不管是探探还是陌陌的点点匹配模块,都是对卡片的左滑右滑进行的操作,那么以陌陌的点点匹配模块分析,所涉...
继续阅读 »

本文章分析怎么实现这种卡片效果以及都有哪些功能,基于这些功能是否可以完善,让框架更加灵活,可拓展等。现已封装成通用框架。效果图如下:


效果图


代码地址


1、功能分析


不管是探探还是陌陌的点点匹配模块,都是对卡片的左滑右滑进行的操作,那么以陌陌的点点匹配模块分析,所涉及的功能有:



  • 卡片复用机制

  • 拖拽卡片时的动画

    • 卡片拖拽左上方、左下方、右上方、右下方都会有相应的旋转动画



  • 喜欢功能

    • 喜欢功能又分为拖拽卡片向右侧滑出、点击下方的喜欢按钮控制卡片滑出。



  • 不喜欢功能

    • 不喜欢功能也是和喜欢功能逻辑一样,只是方向不同。



  • 超级喜欢功能

    • 超级喜欢功能跟喜欢功能一样,只不过是针对的vip用户,普通用户需要充值vip才能使用,vip用户触发超级喜欢,会有一个炫丽的动画特效,并进行其它业务处理



  • 回退功能

    • 回退功能也是针对vip用户设计的,vip用户单次只能回退一张卡片。我们实现回退多张功能,让外界控制是否可以回退功能,更加灵活。



  • 预加载功能

    • 卡片可以无限的滑走,那么数据源获取就得支持加载更多数据。



  • 无数据的处理功能

    • 卡片数据操作完了,就得处理无数据的情况,显示占位图等等。



  • 触发卡片喜欢功能时,需要检测是否允许此次操作

    • 这个功能也是根据业务去做,探探里面,好像对喜欢的操作是有限制的,如果超出了这个限制,再次触发喜欢功能就会提示充值vip同时拖拽的卡片也会恢复原位。






2、功能实现


完成了上面的功能分析之后,接下来就可以一个一个的去实现了。




  • 卡片复用机制


    这里用了4张卡片,最上面的卡片划走之后,会被放在最下面一层,达到复用。




  • 拖拽卡片时的动画


    卡片拖拽左上方、左下方、右上方、右下方都会有相应的旋转动画。我们可以确定用户手势触发的点的位置,根据方位进行设置相关的旋转角度,左右是相反的。




  • 喜欢、不喜欢功能


    1、通过拖拽手势划走卡片
    给每个卡片添加一个拖拽手势,当拖拽卡片的时候,根据拖拽的距离和卡片原始的中心点X值进行判断卡片是向左还是向右,拖拽结束的时候,通过改变卡片的位置并加上动画,达到卡片划走的效果。


    2、通过按钮触发划走卡片
    按钮触发的时候,指定卡片的x位置。然后内部统一走手势结束的方法。




  • 超级喜欢功能
    超级喜欢功能,其实也是喜欢的一种,通过按钮触发喜欢操作,之后加上自己的炫丽动画以及业务逻辑.




  • 回退功能


    卡片回退的实现,将最底下的卡片放到最上面的卡片上面并加上入场动画,同时更新对应的索引数据。
    陌陌vip用户只支持回退一张,我们可以设计支持多张,有多少张不喜欢卡片,默认回退多少张,如果想要实现一张也可以,外界可以控制。相关的方法如下:

    这里回退操作的场景很多,比如:左滑10次,然后右滑4次,回退5次,在一次次的滑动,那么怎么保证卡片是按照正常的顺序显示的呢。这里用了2个数组处理的,第一个数组保存左滑的数据index,第二个数组保存回退的index。具体的思路看下截图:




  • 预加载功能
    每次划走一个卡片,都会代理回调对应的数据源Index供上层更新底部的卡片显示内容, 卡片划走的时候,也会做校验,看看当前的index相对于数据源总数是否小于一个值,这个值我们称为阀值。小于这个阀值会触发加载更多的代理回调。




  • 无数据的处理功能
    每次划走一个卡片,都会更新底部卡片显示内容,如果内部卡片的数据index超出了外界的数据源总数,则将卡片内容隐藏,也会做无数据的检测。




  • 触发卡片喜欢功能时,需要检测是否允许此次操作
    可以在拖拽手势结束的时候,通过代理去询问是否允许滑走,如果不允许则内部更改拖动的距离x值,走复位逻辑。






3、总结


卡片交互的细节很多,很多控制的地方也很多,封装的框架现已支持上面的所有功能, 使用的时候,可以自定义卡片cell实现自己的样式。提供的有示例demo. 欢迎预览。


作者:大大的太阳
链接:https://juejin.cn/post/7036769362652430372
收起阅读 »

cocoapods-binary工作原理及改进

iOS
「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」 在iOS开发中,如果能够对一些稳定的组件能够二进制化,那么将大大的缩减我们在开发过程中的编译时间。在基于Cocaopods工程,快速实现Swift组件二进制一文中,我们讲述了,借助P...
继续阅读 »

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战


iOS开发中,如果能够对一些稳定的组件能够二进制化,那么将大大的缩减我们在开发过程中的编译时间。在基于Cocaopods工程,快速实现Swift组件二进制一文中,我们讲述了,借助Pods工程和Shell脚本,一步实现二进制打包,但需要我们手动更改podspec文件,采用这种方式,如果作为依赖项加入到其他工程中,还会出现二进制源码共存的情况,今天介绍一个cocoapods插件 cocoapods-binary,可以实现组件预编译,该工程已经两年多没维护了,随着pods更新,有了一些小bug。基于源码,我对该插件做了几点更改,又可以开心的玩耍了.在了解该插件之前,我们先大概了解下,输入pod install之后发生了什么?


Pod install


如果你想调试cocoapods工程,可以查看之前的文章Ruby和Cocoapods文章合集


一图胜千言,当输入Pod install


Pod install.png



  • 1,首先校验是否有Podfile文件,并解析为Podfile对象

  • 2,准备阶段,安装插件,调用pre_install阶段的插件

  • 3,解析依赖,通过 当前的Podfile文件 和 上一次的Pofile.Lock,Manifest.Lock文件进行比对,确认哪些文件需要更新。

  • 4,下载依赖,更新需要更改的文件,并执行Podfile里面定义的pre_installhook函数。

  • 5,集成阶段,生成新的Pods.xcodeproj工程文件,并执行Podfile里面的post_installhook函数。

  • 6,写入新的Lockfile信息。

  • 7,执行 post_install阶段的插件,并输出安装信息。


cocoapods-binary是以插件的形式,在Pod工程的pre_install阶段进行预编译的,


cocoapods-binary工作流


pre_install 插件


cocoapods-binary中的,通过HookManager来注册插件的执行时机


Pod::HooksManager.register('cocoapods-binary', :pre_install) do |installer_context|

end


主流程


主要流程如下图所示


截屏2021-11-28 下午7.20.16.png



  • 1,必须使用framework的形式,也就是use_frameworks

  • 2,在Pods文件夹下,创建一个名为_Prebuild的文件夹,作为预编译沙箱

  • 3,在当前环境下,读取Podfile文件,并创建Podfile对象。

  • 4,读取Podflie.lock文件,创建 Lockfile对象。

  • 5,创建预编译安装器,沙箱地址为 预编译沙箱

  • 6,对比PodfilePodfile.lock,得到已更改的pod_name

  • 7,使用预编译安装器pod_name的源代码下载到预编译沙箱中,并生成新的Pods.xcodeproj文件。开始编译需要更新的framework

  • 8,回到主工程,继续执行 pod install的后续流程,在这一步修该需要二进制文件的podspec文件。


解析自定义参数


在插件中,有两个自定义的参数 :binaryall_binary!,是通过自定义DSL来实现的,有对这一块不熟悉的,可以参考我的这篇文章Cocoapods之 Podfile文件


Podfile.png
创建Podfile对象时,通过 method swizzling来hook:parse_inhibit_warnings方法,拿到我们在Podfile文件中写入的配置选项。将需要预编译pod,保存到数组中。


old_method = instance_method(:parse_inhibit_warnings)
define_method(:parse_inhibit_warnings) do |name, requirements|
variables = requirements
parse_prebuild_framework(name, requirements)
old_method.bind(self).(name, variables)
end
复制代码

Ruby中,Method Swizzling主要分为三步:



  • 1,获取parse_inhibit_warnings实例方法。

  • 2,定义一个相同名字的方法。

  • 3,调用原来的方法。


对比Lockfile


lockfile.png
在这里 Podfle.lock预编译沙箱Manifest.lock是一样的,通过对比可以一个Hash对象


<Pod::Installer::Analyzer::SpecsState:0x00007f83370c61a8 @added=#<Set: {}>, @deleted=#<Set: {}>, @changed=#<Set: {}>, @unchanged=#<Set: {"Alamofire", "SnapKit"}>>


可以很清楚的知道哪些pod库发生了更改。如果有改动,则就在预编译沙箱进行install


binary_installer.install!


pre_install.png
在这一阶段,主要是在预编译沙箱中拉取framework源码和修改Pods.xcodeproj文件,在 Manifest.lock成功写入预编译沙箱,通过hook run_plugins_post_install_hooks函数,在预编译沙箱中,使用 xcodebuild命令,编译每一个需要更新的pod_target,并将编译好的framework放至GeneratedFrameworks目录下。


回到主工程执行pod install


截屏2021-11-28 下午8.22.44.png


编译完成后,就回到了主工程里面的 Pod install流程中。对:resolve_dependencies方法进行Method Swizzling,对需要更改的pod_target进行修改。通过修改内存中的Pod::Specification对象的vendored_frameworkssource_filesresource_bundlesresources属性,来引用已经编译好的framework


工作流总结


通过对每一个阶段的了解,我们了解了作者的思路是这样的:
1,先将源码和Pods.project安装到预编译沙箱中 。
2,借助于Pods.project工程,使用xcodebuild编译需要预编译的scheme
3,巧妙的利用Method Swizzling,在分析依赖阶段,修改Pod::Specification对象,完成二进制的引用等工作。


现有问题


1,:binary => true 无效


ruby 2.6.8p205 (2021-07-07 revision 67951) [universal.x86_64-darwin21]版本中,使用:binary => true无效,无法编译为framework。在D ebug模式下生效,在发布后就失效了。


def set_prebuild_for_pod(pod_name, should_prebuild)
Pod::UI.puts("pod_name: #{pod_name} prebuild:#{should_prebuild}")
if should_prebuild == true
@prebuild_framework_pod_names ||= []
@prebuild_framework_pod_names.push pod_name
else
@should_not_prebuild_framework_pod_names ||= []
@should_not_prebuild_framework_pod_names.push pod_name
end
end


ruby 2.6.8中,release模式下,执行了两次,参数还不一致,导致 @prebuild_framework_pod_names@should_not_prebuild_framework_pod_names相等,最终需要预编译的数组为[]


pod_name: SnapKit, should_prebuild:true
pod_name: SnapKit, should_prebuild:


2,pod update没有及时更新最新的framework


frameworkA依赖frameworkB,在 Podfile中,只引入了 frameworkA


target xxx do 
pod "frameworkA", :binary => true
end


frameworkB有新版本时,没有更新最新的frameworkB,对于我们自己的组件,我们期望有新版本发布时,能及时更新。


解决办法:
在检测更新的方法中,读取最新的Manifest.lock文件,读取已编译的frameworkplist文件,比较两个版本号是否一致,不一致则重新编译。


cocoapods-binary-bel


为了能充分利用该插件,我根据实际产生的问题,对源码进行了一些修改,增加了 plist版本校验。使用方式和cocoapods-binary一致。
源码github链接cocoapods-binary-bel


安装


sudo gem install cocoapods-binary-bel


流程图


cocoapods-binary的基础上增加了版本校验工程,总的流程图如下所示:


cocoapods-binary-bel.png


一键转源码


1,:binary => false,指定某一个framework为源码


2, 新增 --hsource选项,输入 pod install --hsource,可以将所有的framework转为源码。


去除了依赖分析


在实际运用的工程中,如果 A 依赖 BC, B又依赖D,如果 A需要预编译,那么 BCD都需要重新编译,实际上此时BCD已经有了已经编译好的版本,无需重新编译。在Podfile指明 BCD即可。


pod "A"
pod "B"
pod "C"
pod "D"


增加静态库的resource处理


在 cocoapods中,如果使用 resource_bundle 处理资源文件,会生成一个相对应的target来处理资源文件,如果是动态库,会在动态库Target,添加资源工程依赖,使用 xcodebuild命令制作二进制会将bundle文件编译至xxx.framework目录下,使用 resources同样也会将资源文件编译至xxx.framework目录下。


对于静态库而言,如果是使用resource_bundle,也同样生成一个会生成一个相对应的target来处理资源文件,但对资源文件的拷贝,是由主工程做的,无需静态库工程处理, 如果使用 resources,则需要将资源文件从源码中,拷贝到 xxx.framework下,在主工程编译时,由主工程处理即可。


作者:Bel李玉
链接:https://juejin.cn/post/7035628418972516360

收起阅读 »

iOS 简单封装一个新用户功能模块引导工具类小玩儿意

iOS
废话开篇:新手引导功能就是简单的告诉用户某一模块下能够进行什么样的操作,起到指引用户的作用,那么就简单的实现一下这样的功能模块。一、实现效果展示可以从效果图中看到,对新用户的必要模块都会进行简单的功能解释。二、调用代码添加待引导功能视图到管理类管理类进行展示三...
继续阅读 »

废话开篇:新手引导功能就是简单的告诉用户某一模块下能够进行什么样的操作,起到指引用户的作用,那么就简单的实现一下这样的功能模块。

一、实现效果展示

屏幕录制2021-12-08 上午11.04.57.gif

可以从效果图中看到,对新用户的必要模块都会进行简单的功能解释。

二、调用代码

添加待引导功能视图到管理类

image.png

管理类进行展示

image.png

三、工具类解析

image.png

1、KDSGuideMannager 类

(1)统一管理全局下需要进行 “引导” 的功能区域(UIView)的保存。

(2)控制引导界面的显示与消失。

(3)控制下一个功能区域(UIView)圈定及描述展示。

2、KDSGuideView 类

(1)整体的蒙板视图层。

(2)对当前所选引导功能区域(UIView)进行镂空标注

(3)调整气泡(KDSGuideBubbleView)位置。

3、KDSGuideBubbleView 类

(1)气泡标注视图。

4、KDSGuideModel 类

(1)保存功能区域(UIView)视图及功能描述文字

四、实现代码

1、KDSGuideMannager 类

KDSGuideMannager.h

image.png

KDSGuideMannager.m

image.png

image.png

image.png

2、KDSGuideView 类

KDSGuideView.h

image.png

KDSGuideView.m

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

3、KDSGuideBubbleView 类

KDSGuideBubbleView.h

image.png

KDSGuideBubbleView.m

image.png

image.png

4、KDSGuideModel 类

KDSGuideModel.h

image.png

KDSGuideModel.m

image.png

五、其他效果展示

屏幕录制2021-12-08 下午1.54.31.gif

个人总结,代码拙劣,大神勿笑。

收起阅读 »

SDWebImage从小白到大师蜕变

iOS
简介SDWebImage提供的简洁的获取远程URL图片的API;平时开发中使用最多场景就是列表中的cell中要显示远程图片的需求,在具体的实现中要避免加载图片造成的界面卡顿,列表卡顿等现象的出现;所以需要编码实现如下功能:使用占位图片显示UI界面,异步线程加载...
继续阅读 »

简介

SDWebImage提供的简洁的获取远程URL图片的API;平时开发中使用最多场景就是列表中的cell中要显示远程图片的需求,在具体的实现中要避免加载图片造成的界面卡顿,列表卡顿等现象的出现;所以需要编码实现如下功能:

  • 使用占位图片显示UI界面,异步线程加载图片成功后刷新控件
  • 缓存机制,下载过的图片做内存缓存和磁盘缓存
  • app内存吃紧的状态下移除缓存的内容

SDWebImage的框架结构

SDWebImage的框架结构

SDWebImage的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。

功能简介

  1. 一个添加了web图片加载和缓存管理的UIImageView分类
  2. 一个异步图片下载器
  3. 一个异步的内存加磁盘综合存储图片并且自动处理过期图片
  4. 支持动态gif图
    • 4.0 之前的动图效果并不是太好
    • 4.0 以后基于 FLAnimatedImage加载动图
  5. 支持webP格式的图片
  6. 后台图片解压处理
  7. 确保同样的图片url不会下载多次
  8. 确保伪造的图片url不会重复尝试下载
  9. 确保主线程不会阻塞

实现原理

  1. 架构图(UML 类图)

架构图(UML 类图)

  1. 流程图(方法调用顺序图)

1559217862563-364c0d60-3f2a-4db9-b5c5-e81f01cd125e.png

目录结构

  • Downloader
    ○ SDWebImageDownloader
    ○ SDWebImageDownloaderOperation

  • Cache
    ○ SDImageCache

  • Utils
    ○ SDWebImageManager
    ○ SDWebImageDecoder
    ○ SDWebImagePrefetcher

  • Categories
    ○ UIView+WebCacheOperation
    ○ UIImageView+WebCache
    ○ UIImageView+HighlightedWebCache
    ○ UIButton+WebCache
    ○ MKAnnotationView+WebCache
    ○ NSData+ImageContentType
    ○ UIImage+GIF
    ○ UIImage+MultiFormat
    ○ UIImage+WebP

  • Other
    ○ SDWebImageOperation(协议)
    ○ SDWebImageCompat(宏定义、常量、通用函数)

相关类名与功能描述

SDWebImageDownloader:是专门用来下载图片和优化图片加载的,跟缓存没有关系

SDWebImageDownloaderOperation:继承于 NSOperation,用来处理下载任务的

SDImageCache:用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程

SDWebImageManager:作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来

SDWebImageDecoder:图片解码器,用于图片下载完成后进行解码

SDWebImagePrefetcher:预下载图片,方便后续使用,图片下载的优先级低,其内部由

SDWebImageManager :来处理图片下载和缓存

UIView+WebCacheOperation:用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation

UIImageView+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用

UIImageView+HighlightedWebCache:跟 UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片

UIButton+WebCache:跟 UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用

MKAnnotationView+WebCache:跟 UIImageView+WebCache 类似

NSData+ImageContentType:用于获取图片数据的格式(JPEG、PNG等)

UIImage+GIF:用于加载 GIF 动图

UIImage+MultiFormat:根据不同格式的二进制数据转成 UIImage 对象

UIImage+WebP用于解码并加载 WebP 图片

工作流程

工作流程

  • 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

  • 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:。

  • 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

  • SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

  • 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

  • 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

  • 如果从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:进而回调展示图片。

  • 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

  • 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

  • 图片下载由 NSURLConnection(3.8.0之后使用了NSURLSession),实现相关 delegate 来判断图片下载中、下载完成和下载失败。

  • connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

  • 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

  • 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

  • imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

  • 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

  • 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

  • SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

  • SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

常见面试题

  1. 图片文件缓存的时间有多长:1周

_maxCacheAge = kDefaultCacheMaxCacheAge

  1. SDWebImage 的内存缓存是用什么实现的?

NSCache

  1. SDWebImage 的最大并发数是多少?

maxConcurrentDownloads = 6

  • 是程序固定死了,可以通过属性进行调整!
  1. SDWebImage 支持动图吗?GIF
1. #import <ImageIO/ImageIO.h>
2. [UIImage animatedImageWithImages:images duration:duration];
复制代码
  1. SDWebImage是如何区分不同格式的图像的

    • 根据图像数据第一个字节来判断的!
    • PNG:压缩比没有JPG高,但是无损压缩,解压缩性能高,苹果推荐的图像格式!
    • JPG:压缩比最高的一种图片格式,有损压缩!最多使用的场景,照相机!解压缩的性能不好!
    • GIF:序列桢动图,特点:只支持256种颜色!最流行的时候在1998~1999,有专利的!

6.SDWebImage 缓存图片的名称是怎么确定的!

  • md5

  • 如果单纯使用 文件名保存,重名的几率很高!

  • 使用 MD5 的散列函数!对完整的 URL 进行 md5,结果是一个 32 个字符长度的字符串!

  1. SDWebImage 的内存警告是如何处理的!
    • 利用通知中心观察
    • - UIApplicationDidReceiveMemoryWarningNotification 接收到内存警告的通知
    • 执行 clearMemory 方法,清理内存缓存!
    • - UIApplicationWillTerminateNotification 接收到应用程序将要终止通知
    • 执行 cleanDisk 方法,清理磁盘缓存!
    • - UIApplicationDidEnterBackgroundNotification 接收到应用程序进入后台通知
    • 执行 backgroundCleanDisk 方法,后台清理磁盘!
    • 通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内!
    • clearDisk 清空磁盘缓存,将所有缓存目录中的文件,全部删除!

实际工作,将缓存目录直接删除,再次创建一个同名空目录!

青山不改,绿水长流,后会有期,感谢每一位佳人的支持!

收起阅读 »

闲鱼正在悄悄放弃 Flutter 吗?

iOS
闲鱼技术阿里巴巴集团采访嘉宾 | 于佳(宗心)编辑 | Tina闲鱼在 2017 年引入 Flutter,当时的 Flutter 还远未成熟,行业内也没有把 Flutter 放入已有工程体系进行开发的先例。之后这支不到 15 人的闲鱼团队从工程架构、混合栈调用...
继续阅读 »

闲鱼技术lv-4阿里巴巴集团

采访嘉宾 | 于佳(宗心)

编辑 | Tina

闲鱼在 2017 年引入 Flutter,当时的 Flutter 还远未成熟,行业内也没有把 Flutter 放入已有工程体系进行开发的先例。

之后这支不到 15 人的闲鱼团队从工程架构、混合栈调用、打包构建、协同模式上都做了一些创新,保证了 Flutter 能融入到闲鱼已有的客户端工程体系内。在 2017 年到 2019 年期间,闲鱼也不断的修正 Bug 提高 Flutter 的稳定性并同步给 Google,并在实践中沉淀出一套自己的混合技术方案,开源了 Flutter Boost 引擎。

2019 年,闲鱼开始大规模落地,推进 Flutter 在闲鱼的应用。2020 年,闲鱼线上的主链路几乎已经完全拥抱 Flutter。这两年,Flutter 也逐渐在其他企业里落地,但同时也不断有质疑的声音发出。甚至有传言表示“闲鱼的新业务已经放弃 Flutter”、“相信闲鱼遇到了很大的难题”......

那么,作为 Flutter 先驱和探路者,闲鱼在过去几年的摸索过程中是否有走弯路?闲鱼现在到底面临着什么样的挑战?是否会放弃 Flutter?新业务选择了什么技术?对应的技术选型原则是什么?针对这些疑问,闲鱼技术团队客户端负责人于佳(宗心)逐一给了我们解答。

国内第一个引进 Flutter 的团队

InfoQ:闲鱼当时引进 Flutter 时主要是为了解决什么问题?

于佳(宗心):闲鱼在 17 年调研的时候,客户端团队只有不到 15 人,而闲鱼的业务场景可以称得上是一个 “小淘宝”,相对比较复杂。这种场景下我们首先需要解决的是多端人力共享的问题。多端人力带来的好处不只是可以一人开发双端,也代表着更好的研发资源调配灵活性(这意味着团队的 iOS:Android 的比例不再需要 1:1,而市面上 Android 的工程师基数远大于 iOS)。

另外我们希望这个技术是贴合移动端研发技术栈的,而非前端技术栈,本身对于 RN 和 Weex 来说,工具链和研发习惯还是有比较大的差异的。最后我们希望这个技术的体验可以做到接近原生,AOT 下的 Flutter 基本满足我们当时的要求,在实际测试过程中,同样未深度优化的详情页面,Flutter 在低端机的表现比 Native 更好。因此当时基于这三个条件选择了 Flutter。

2018 年的尝试投入过程中,整个基建和探索带来了一定的成本。2019 年,团队开始正式大量使用 Flutter 进行研发,目前整个团队 70% 的 commit 来自 Dart,可以说基本完成了我们当初的期望。在实际的研发过程中,基本可以完成一个需求一个客户端投入的目标。

InfoQ:很多人质疑 Dart 语言,认为这个语言独特小众,还存在比如说多层嵌套的问题,您们怎么看待新语言的应用?

于佳(宗心):语言是我们选择技术方案的其中一个因素,但是相对比较弱的因素。

我们会从几个角度去看:

  • 语言的背景,从我们的角度来看 Dart 是大厂研发的,也有比较久的历史。

  • 语言的学习成本,从语法糖和学习曲线上来看,Dart 成本都比较低,首先 Android 同学的上手率很快。另外熟悉 swift 的 iOS 同学,上手也很快。现代语言的特性有很多是相通的。这部分是它的优势。

  • 语言带来的其他优势,如编译产物支持 AOT 和 JIT,比较灵活。AOT 有明显的性能优势。

  • 语言的未来的趋势。Dart 在 2020 年第四季度 Github Pull Request 的排名已经到了全网第 13 位,超过了 Kotlin(15 位),Swift(16 位),Objective-C(18 位)。作为移动技术领域的新语言成长性还是非常不错的。

对于像多层嵌套的问题,可以通过进一步抽象一些控件类或方法解决,并不是特别大的问题。

InfoQ:闲鱼引入 Flutter 之后做了哪些关键创新?在使用 Flutter 上有哪些收益?

于佳(宗心):闲鱼在这部分创新非常多,并在内部申请了非常多专利。

  • 我们的开源项目 Flutter Boost 彻底改变了 Flutter 官方的一些 RoadMap。目前 Add2ExistApp 是行业最主流的研发方式。混合开发一方面帮助了业务更平滑的迁移到了新的技术栈,另一方面可以更好的利用已有的 Native 能力,大幅减少了重复开发的工作。

  • 针对音视频的外接纹理方案,也是目前行业大厂常见的解决方案,在外接纹理方案下,Native 和 Flutter 侧的缓存管理得到了统一,在性能上也有一定的提升。

  • Flutter APM,基于 Flutter 技术栈的性能稳定性数据采集和加工方案,目前在集团内部也是跟多个 BU 一起共建,为大的 AliFlutter 组织提供服务。

  • Flutter 相关的动态模版方案,Flutter DX,兼容集团的已有的 Native 模版,保证了业务的平滑迁移,并为 Flutter 提供了部分业务动态性。

  • 其他还有很多,包括内部的高性能长列表容器 PowerScrollView,动画框架 Fish-Lottie,游戏引擎 Candy,我们现在还有一些新的方向在沉淀,在基于 Flutter 的研发流程和研发工具上也有投入,未来大家如果感兴趣可以去 InfoQ 组织的行业大会与我们交流。

闲鱼有想过放弃 Flutter 吗?

InfoQ:最近一两年,您们在 Flutter 开发上,遇到的最大挑战是什么?跟最初使用 Flutter 时的挑战一样吗?

于佳(宗心):早先几年闲鱼作为整个行业的先驱,主要的挑战是整个技术生态太差,都需要自己做。另外就是前期引擎的稳定性有比较大的问题。

最近几年随着整个技术的深度使用,以及闲鱼这两年业务快速发展背后,越来越多的体验问题被大家提及,因此我们从去年开始进行了整个产品的大改版,同时客户端的目标就是全面优化,打造更好的用户端产品体验。

因此在生态逐渐完善后,我们的挑战是,怎么通过 Flutter 来实现更加精细化的用户体验。去年,这部分确实花了我们比较多的精力。基于这个命题,我们在内存和卡顿上内部也开发了较多的基于 Flutter 的检测工具,在内存优化和卡顿优化上也有一些比较具体的方法,但不得不说,所有的细节优化都是比较耗人力的,不管是 Native 还是 Flutter 都要投入相当的精力,所以我们目前也面向全行业进行客户端的招聘,希望有志在 Flutter 领域进行探索的同学联系我。

InfoQ:在混合研发体系下,闲鱼还进行了引擎定制,那么官方提供的方案主要问题是什么?对于一般小企业来说,混合开发复杂度会不会太高?

于佳(宗心):闲鱼在前期有不少修改引擎的动作,我针对当时有一些 自己的反思,一方面是确实因为 Flutter 不太完善,另一方面在 18 年左右,我们自己引擎的理解也不够深刻,很多时候可以通过更上层的方案解决,这也间接导致了我们的很多引擎定制修改难以合入主干。

所以这部分我想说的是,目前官方的方案可以解决 90% 的问题,如果一定要说定制,目前在性能侧还是有一些问题的。比如闲鱼目前首页还是 native 没有使用 Flutter,就是因为替换以后启动加载体验不佳,另外在长列表侧大家一直诟病的卡顿问题,我们有尝试通过上层框架解决了一部分,接下来可能还需要底层引擎帮忙优化。另外一些包括双端字体不一致的问题,还有输入框体验不一致的问题,都需要官方进行长期的优化。

目前我们主要还是希望跟随主干分支,尽量不修改 Flutter 的代码,闲鱼团队会储备一些引擎侧的专家,同时也会依靠集团 AliFlutter 的生态做事情。在整个 AliFlutter 的组织里不同的 BU 擅长的也不同,如 UC 同学更擅长引擎定制,闲鱼团队有大量的上层应用框架,淘宝团队提供基于构建相关的基础设施。这样在大型公司中通过内部开源社区的方式就可以解决大部分的问题,放心开发了。

对于中小企业来说,要明确下大家面临的场景,如果前期快速迭代跑起来,对细节问题可以有一部分妥协,选择 Flutter 是一个比较明确的路径。今天大家所处的环境比闲鱼当年所处的环境要完善的多。推荐使用 Flutter Boost 进行混合开发,在部分场景下遇到问题无法快速响应时,也可以通过混合工程使用 native 进行兜底。复杂度方面,单纯引入混合栈能力,整体复杂度一般。

InfoQ:有传言,闲鱼有新业务没采用 Flutter,这给很多人造成了闲鱼放弃 Flutter 的观念,那么您们在新业务的技术选型上,考虑了哪些因素?

于佳(宗心):作为技术决策者,是应该避免自己被某一个技术绑架而在落地过程中产生谬误的。Flutter 和其他技术一样,最终是为了帮助团队实现业务价值,同时它也只是移动端的一种技术,捧杀和谩骂都是不合适的。这也是我特别不想在公众面前回应这个事情的原因,因为 技术本身要看适用场景。

从目前闲鱼的人员规模和业务规模来看。对于架构设计,我的理念是尽量追求一致性和架构的简洁。

整个客户端组织未来从语言的方向来看是 Dart First,尽量减少双端的研发投入。而对其他容器的选择,主要以 H5 为主,在未来的路径上尽量减少其他容器的接入,让前端开发也回归到标准路线来。

这里有两个好处:

  1. 组织成本最低,组织成本包括了同学们的学习成本、协同成本等等,多技术栈和容器多会带来额外的成本,这是我不愿意看到的。

  2. 架构的一致性对研发效能和质量都有帮助。举个例子,随着业务复杂性加大,多容器带来的内存飙升和包大小的问题是非常致命的,而且几乎是无解的,这就需要架构师作出决策,干掉什么留下什么。回到研发效能上,配套的工具,流程一定是围绕一类容器和语言来扩展的,如果方案特别多,每个方向都需要做额外的配套设施,成本收益很低,研发的幸福感也很低。

从这个设计的角度出发,我们会有几个明确的选择

  • 在默认场景下使用 Flutter 作为首选的方案;

  • 在投放活动、前台导购、非常不确定的新业务、以及管理后台等使用 H5 作为首选实现方案;

  • 在极少场景下,比如已有完整的 SDK 附带 UI 的支持如直播,以及未来中台的拍摄功能 SDK 也是自带 UI 的部分,如要切换,Native 成本最低,选择 Native。另外目前 Flutter 在首页加载还有一定的性能问题,因此还在使用 Native。从长远发展来看,未来到一定程度可能随改版直接改为 Flutter。

关于未来发展

InfoQ:使用 Flutter 多年后,现在回过头去看,您认为哪些公司哪些场景适合 Flutter?

于佳(宗心):目前看起来有几个典型场景比较适合:

  • 中台战略下的小前台产品,从大公司的组织里看阿里、头条、美团都有相对完善的 Flutter 组织或内部技术社区可以提供一些基础服务,保证了基于 Flutter 基础设施在前期投入过程中的成本均摊,在未来落地过程中,业务团队可以更加专注于业务研发,而更少的担心过程中填坑的成本。

  • 中小型企业的初创 App,在人力成本资源都不够的情况下,希望先跑通流程上线验证的团队,可以尝试使用 Flutter 进行研发,在我自己实际的面试和行业交流过程中,这一类情况也比较典型。这种方式可以避免前期成本过度投入,在人员调配上也更加灵活。

  • 另外这个观点还没有验证,但是逻辑上应该可行。未来面向企业内部流程工具,政府部门的部分工具属性较强的 App,可以尝试使用 Flutter。因为目前我了解的情况来看,在企业这边的应用来看,整体 ToB(美团商家端)和 ToD(比如饿了么骑手端)的场景的 App 特别多。横向比较来看,场景比较类似,也就是说更多中长尾应用有可能是 Flutter 技术的主要场景。

InfoQ:您认为未来 Flutter 急需改善的地方是什么?

于佳(宗心):从 Flutter 2.0 发布后我跟一些一线开发者交流的感受来看,Flutter 还是需要推进跨端性能和细节体验的优化。去年一年在大的战略方向上(跨终端),Flutter 做的不错,在 PC 和 Web 侧都有建树,跟车企以及操作系统厂商合作都有一定进展。但回归到产品体验和开发者体验上,还有不少路要走,很多时候对于一个严苛的业务方来说,小到字体和控件的体验都会成为最后不选择这门技术的原因。这部分希望整个开源社区在新的一年能有一些进步。我们 AliFlutter 组织内部,以 UC 内核团队为首的同学们,在这方面就有非常多的沉淀以及 PR,在内部引擎制定上有很多体验的提升。未来在 AliFlutter 组织内,我们也会除了完善整个公司的基建外,进一步关注细节体验,沉淀一些最佳实践给到其他的开发同学。大家会在2个月内看到我们最新出版的书籍,欢迎交流。

InfoQ:Flutter2.0 来了,那么 Flutter 会成为主流选择吗?

于佳(宗心):可以讲一下我对 Flutter 未来的判断。一方面在未来操作系统有可能走向分裂,多终端的场景下,Flutter 会有比较不错的发展,跨平台本身的对企业来说在成本侧是有很大的诉求的,尤其是互联网公司。但是从历史的经验来看,Flutter 只是渲染引擎,即使今天的游戏开发,在游戏引擎和配套工具完善的情况下,有部分的功能模块(比如社区 / 直播的功能)依然还是混合的框架,所以混合开发最后一定是一直存在的。能不能成为未来整个移动研发的主流这件事情上看,我无法给出答案,但可以肯定的是,在生态更加完善后,会在一定的历史阶段成为客户端研发的另一种常见的技术选择。

嘉宾介绍:

于佳,花名 宗心,闲鱼技术团队客户端负责人。2012 年应届毕业加入阿里巴巴,经历集团无线化转型的重要时期,参与过集团多款重量级 App 以及移动中间件的设计与开发,多年客户端老兵。2014 年参与了手机淘宝的 iOS 客户端的架构升级,该架构首次完成了对百人团队并行开发的支持,同年主导了手机天猫客户端基础架构以及交易链路向手淘架构的归一,为手机淘宝作为未来集团无线中台奠定了坚实的基础。2015 年加入闲鱼客户端团队负责端架构和团队建设,工作期间完成了基于 Flutter 混合架构的闲鱼客户端的整体架构设计,在工程体系上完善了针对 Flutter 的持续集成以及高可用体系的支撑,同时推进了闲鱼主链路业务的 Flutter 化。未来将持续关注终端技术的演变及发展趋势。

收起阅读 »