卡洛斯的博客


  • 首页

  • 归档

  • 标签
卡洛斯的博客

nonatomic 和 atomic

发表于 2020-11-20 |

nonatomic 和 atomic

atomic 并不是绝对线程安全,它能保证代码进入 getter 和 setter 方法的时候是安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 getter 和 setter 方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系。

nonatomic 特点

  • 不会加锁
  • 多线程给nonatomic属性赋值,是可能重复release而崩溃的
@interface NonatomicTest : NSObject
@property (nonatomic, strong) id obj;
@end
@implementation
+ (void)main {
// 多线程给 nonatomic 属性赋值,是可能重复 release 而崩溃的
// ivar 所指向的内存区域被其他线程修改了,产生了野指针. Thread 62: EXC_BAD_ACCESS (code=1, address=0x43308abb42f0)
for (int i = 0; i < 100000; ++i) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
id o = [[NSObject alloc] init];
self.obj = o;
});
}
}
@end

atomic 特点

  • 无差别加锁,及时是在非多线程环境下,加锁会损耗性能。
  • 由于是原子操作,可以在多线程情况下不崩溃,但是无法保证业务正确。

为什么 atomic 可以在多线程环境下可以不崩溃而 nonatomic 会崩溃?

atomic 的底层实现是有加锁的,老版本是自旋锁,iOS10 开始是互斥锁,spinlock 底层实现改变了。
nonatomic 底层是没有加锁的, 多线程读写,资源抢夺就会崩,比如多线程给属性赋值,是可能重复 release 而崩溃的。

// objc4-779.1 
// @property (strong) strong 属性赋值底层实现
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {// 偏移 0 就是修改 isa 指针
object_setClass(self, newValue);
return;
}

id oldValue;
id *slot = (id*) ((char*)self + offset);// 根据偏移找到变量地址

if (*slot == newValue) return;// 1、值不相等,才做赋值操作
newValue = objc_retain(newValue);// 2、先对新值 retain

if (!atomic) {// nonatomic
oldValue = *slot;// 3、保存变量旧值
*slot = newValue;// 4、给变量赋值新值
} else {// atomic
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();// 加锁 由于这里有加锁逻辑,那么多线程情况下是不会多次获取到同一个旧值的,所以也不会引起重复释放的问题
oldValue = *slot;// 3、保存变量旧值
*slot = newValue;// 4、给变量赋值新值
slotlock.unlock();// 解锁
}

objc_release(oldValue);// 5、release 旧值
}
卡洛斯的博客

记一个听云导致GIO不能圈选H5的问题

发表于 2020-11-13 |

记一个听云导致GIO不能圈选H5的问题

产品侧是一直是使用 GIO 在 APP 内圈选 H5 页面来进行埋点的。然后有一天产品跑过来跟我说,GIO 在圈选 H5 元素时,不能弹出圈选操作的界面了,于是就走上了排查的不归路。

联系 GIO 技术客服

联系了 GIO 技术客服,给他们提供了不能圈选 H5 的视频和 SDK 版本号。另外也告知了 GIO,我们最近是把 UIWebView 升级到了 WKWebView。

- Growing (2.7.8)
- GrowingAutoTrackKit (2.7.7):
- GrowingCoreKit (~> 2.7.7)
- GrowingCoreKit (2.7.8):
- Growing (~> 2.7.8)
- GrowingTouchKit (0.1.5):
- GrowingAutoTrackKit (>= 2.6.7)

不久就收到了 GIO 的回复,他们说最新版本的 2.8.7 是没有问题的,于是我就升级到了 2.8.7 去试下,试完后还是有不能圈选的问题。

然后我就去看 GIO 是怎么做圈选 H5 的,发现他们是 hook 了 didFinishNavigation,在 H5 加载后就会注入一段 JS 脚本。

try {
(function() {
try {
var p = document.createElement('script');
p.src = 'https://assets.giocdn.com/sdk/hybrid/2.0/gio_hybrid.min.js?sdkVer=2.8.7&platform=iOS';
document.head.appendChild(p);
} catch(e) {}
})()
} catch(e) {}

try {
(function() {
try {
var p = document.createElement('script');
p.src = 'https://assets.giocdn.com/sdk/hybrid/2.0/vds_hybrid_circle_plugin.min.js?sdkVer=2.8.7&platform=iOS';
document.head.appendChild(p);
} catch(e) {}
})()
} catch(e) {}

然后我 hook evaluateJavaScript:completionHandler: 方法,打开圈选,打开一个百度链接。准备去圈选百度按钮,可以发现执行的下面的脚本:

2019-12-25 15:58:27.030537+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(362.333328, 170.666656); } catch (e) { }

2019-12-25 15:58:27.047863+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(362.333328, 170.666656); } catch (e) { }

2019-12-25 15:58:27.062563+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(362.666656, 170.666656); } catch (e) { }

2019-12-25 15:58:27.080025+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(362.666656, 170.666656); } catch (e) { }

2019-12-25 15:58:27.095867+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(362.666656, 170.666656); } catch (e) { }

2019-12-25 15:58:27.112504+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(362.666656, 170.666656); } catch (e) { }

2019-12-25 15:58:27.129417+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(363.000000, 170.666656); } catch (e) { }

2019-12-25 15:58:27.196647+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(363.333328, 170.666656); } catch (e) { }

2019-12-25 15:58:27.833718+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(363.666656, 170.666656); } catch (e) { }

2019-12-25 15:58:29.405819+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(364.000000, 170.666656); } catch (e) { }

2019-12-25 15:58:29.420993+0800 App[8816:2927073] ssssss try { _vds_hybrid.hoverOn(364.000000, 170.666656); } catch (e) { }

2019-12-25 15:58:29.446618+0800 App[8816:2927073] ssssss try { _vds_hybrid.cancelHover(); } catch (e) { }

2019-12-25 15:58:29.463312+0800 App[8816:2927073] ssssss try { _vds_hybrid.findElementAtPoint("seq5"); } catch (e) { }

2019-12-25 15:58:29.480739+0800 App[8816:2927073] ssssss try { _vds_hybrid.pollEvents(); } catch (e) { }

2019-12-25 15:58:29.482341+0800 App[8816:2927073] ssssss try { _vds_hybrid.pollEvents(); } catch (e) { }

说明他这是通过 Native 拿到的圈选坐标,然后把坐标传给 JS 去定位到 H5 元素的。这段打印也另外说明了,GIO 注入的脚本是成功的, 既然注入是成功的,那为什么没有弹出圈选界面呢?

阅读全文 »
卡洛斯的博客

H5 与 Native 交换图片技术方案

发表于 2020-11-13 |

H5 与 Native 交换图片技术方案

背景

目前在APP内的H5分享图片流程如下:

H5 -> canvas 绘制界面获取要分享的图片 -> 上传图片到 CDN -> 调用 JSSDK 的分享 API,入参是图片的 CDN 链接 -> App 收到分享请求后,会根据图片的 CDN 链接下载图片 -> 然后调用第三方 SDK 分享图片。

从这个过程中,实际上 H5 和 App 一共会进行两次的 CDN 操作,这样的操作是非常耗时,网络差的时候,在 H5 内的分享图片操作会非常卡,体验很差。

方案选型

Base64 方案:

此方案是把图片二进制的一个字节转成一个字符,最后会生成一个纯字符串,通过 JSBridge 把改字符串传给 Native,但是如果图片过大,产生的字符串很长,会导致 H5 和 Natvie 传输很慢,会让页面有卡死的现象。

Local Web Server 方案:

逆向了微信,看了下微信里 log 日志,会发现

[INFO] WCProxyServer started on port 48765 and reachable at http: //localhost:48765/
2019 - 05 - 30 22 : 17 : 52.821043 + 0800 WeChat[625 : 18089] evaluateJavaScript: if (window.WeixinJSBridge) {
WeixinJSBridge._handleMessageFromWeixin({
“sha_key”: “e917c46a4fec7f116c7a3a9b325ab946e94bb15b”,
“
json_message”: “{"params":{"err_msg":"imageProxyInit:ok","serverUrl":"http://localhost:48765/\",\"port\":48765},\"callback_id":"1018","__msg_type":"callback"}”
});
}

这段日志,说明微信在本地也是开启了一个本地web服务,来进行 H5 和 Native 的图片交换。

阅读全文 »
卡洛斯的博客

主线程和主队列的关系

发表于 2020-10-20 |

主线程和主队列的关系

先说结论

主队列只在主线程中被执行的,而主线程运行的是一个 runloop,不仅仅只有主队列的中的任务,还会处理 UI 的布局和绘制任务。

几个例子

一、自定义串行队列,同步执行。

- (void)someMethod {
dispatch_queue_t queue = dispatch_queue_create("com.kk", nil);
dispatch_sync(queue, ^{
NSLog(@"current thread = %@, curren queue = %@, main queue = %@", [NSThread currentThread], [NSString stringWithCString:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) encoding:NSUTF8StringEncoding], [NSString stringWithCString:dispatch_queue_get_label(dispatch_get_main_queue()) encoding:NSUTF8StringEncoding]);

});
}

------------------
current thread = <NSThread: 0x600002c0cf00>{number = 1, name = main}, curren queue = com.kk, main queue = com.apple.main-thread
这里是主线程但不是主队列。

二、主线程判断的几个方法

//下面这几种切换到主线程执行的方法, 你更喜欢哪种?有什么优缺点?
//方法1
if ([NSThread isMainThread]) {
//xxx
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
//xxx
});
}
// 由于主队列里的任务只能在主线程中被执行的,如果是子线程执行这段代码,那子线程会等着主线程执行玩任务,才能往下。
// 如果是主线程执行这段代码,那就走 if 的分支了,所以这段代码既不会引起UI的崩溃,也不会造成死锁。
// 只不过这种写法就必须要等任务执行完,才能玩下走。

//方法2
if ([NSThread isMainThread]) {
//xxx
} else {
dispatch_async(dispatch_get_main_queue(), ^{
//xxx
});
}
// 大部分都是这么用的,也没有什么问题
// 但是主线程不一定能过只跑主队列的任务。当主线程在同步执行自定义串行队列的任务时,那此方法的判断就不对了,因为此时是主线程且是非主队列

//方法3
dispatch_async(dispatch_get_main_queue(), ^{
//xxx
});
// 始终异步放在下一个loop使用,会延迟执行时机

//方法4
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {
//xxx
} else {
dispatch_async(dispatch_get_main_queue(), ^{
//xxx
});
}
// 方法二已经解释了,这里可以通过判断队列label的形式,可以保证任务一定是放在主队列中的。虽然方法二,也不会造成什么问题,但是方法四会更加符合预期(任务一定要在主队列中)

综合来说,方法 4 是更好的判断是否是主线程的方式。

阅读全文 »
卡洛斯的博客

WKWebView 离线包方案比较

发表于 2020-04-02 |

WKWebView 离线包方案比较

方案列表

  • 沙盒方案:通过沙盒路径直接加载本地文件
  • NSURLProtocol 方案:基于 NSURLProtocol 进行全局请求拦截
  • LocalServer 方案:搭建本地服务器加载本地资源
  • WKURLSchemeHandler 方案:基于 WKURLSchemeHandler 进行自定义Scheme 注册拦截

方案图表比较

问题 沙盒 NSURLProtocol LocalServer WKURLSchemeHandler
是否可以同步 Cookie NO YES NO NO(Session Cookie无效)
是否有跨域问题 YES NO YES YES
Ajax 是否完全可用 NO NO YES NO
Fetch 是否完全可用 NO NO YES NO
是否丢失 POST Body NO YES NO YES
是否可以提交表单 NO NO YES NO
是否必须使用 Ajax Hook NO YES NO NO
是否必须使用 JSBridge 发送 API YES NO NO NO
是否需要后台配置 CORS(跨域) NO NO YES YES
是否可以获取期望的 window.location NO YES NO YES
是否使用私有 API NO YES NO NO
是否有系统限制 NO NO NO YES(iOS11以上)
是否需要 H5 改造支持 YES NO YES YES
是否需要请求是绝对路径 NO NO YES YES
是否需要资源是相对路径 YES NO YES YES
是否有后期维护成本 NO YES(Fetch Hook) NO NO
方案成本 低 高 中 中
(H5不依赖Session Cookie)方案可行性 低 高 中 高
(H5不依赖window.location)方案可行性 低 高 高 中
全场景方案可行性 低 高 中 中
阅读全文 »
卡洛斯的博客

一站式解决WKWebView各类问题

发表于 2019-08-30 |

一站式解决WKWebView各类问题

为什么要使用 WKWebView

  • UIWebView 在 iOS 12 后被标记为过期了。

  • H5 在 UIWebView 和 WKWebView 上的行为不一致,特别在滚动监听上了。另外像微信这样的分享渠道都是使用的 WKWebView,如果还在使用 UIWebView 的话,会导致 H5 的显示效果跟微信上的不一致。

  • WKWebView 可以带来执行效率和渲染效率的提升。

为什么要重新造轮子

我们知道 WKWebView 有典型的两个问题:

  1. 支持离线包时,ajax body 就会丢失

  2. cookie 同步问题

关于 body 丢失的问题

我了解到的解决 body 丢失问题的方式如下:

  • H5 在 进行 ajax 请求时,把 body 体放入请求头里,在拦截的时候,再从请求头里拿出 body,然后重新构建新的请求,通过 NSURLSession 发送。但是请求头不适合放入太多数据,而且也需要 H5 侧配合才行。

  • H5 ajax 请求是通过 JSAPI 转发到 Native 来发送,这个确实可以解决 body 丢失的问题,但是会让 H5 侧有集成的成本。

  • 在请求之前把 url 上的 http/https scheme 替换成自定义的 customscheme,然后返回的 Html 中的子资源链接,使用 src=’//xxx.com/a.js’ 的形式去加载,这样也会让子资源带上 customscheme,通过这种方式 NSURLProtocol 只用注册和拦截 customscheme,针对 customscheme 丢失 body 信息没有任何影响,然后 H5 ajax 请求,还是可以走 http/https 来请求。这种也可以解决 body 丢失的问题,但是必须要求 H5 侧能够按照规范来,如果 H5 没有历史包袱,这种也是可行的,如果有历史包袱的话,也会有改动的成本。

  • 注入 ajax hook js 代码,对所有 XMLHTTPRequest 对象进行 hook,在 open,send 等方法处进行拦截,通过 JSAPI 把 url,数据转发到 Native 去发送请求。这种可以解决 body 丢失的问题,而且对 H5 ajax 请求是无侵入式的,但是这种方式相关开源实践很少。

这里的话,只有 ajax hook 是无侵入式的,H5 侧没有任何改动成本,我自己的原则也是最小化改动 H5,让 H5 无感知的使用 WKWebView 的能力,所以 KKJSBridge 也是基于 ajax hook 来实现的。

关于 cookie 同步的问题

我们都知道 WKWebView 的 cookie 是单独保存在 WKWebView cookie 里的,而不是我们平时用的 NSHTTPCookieStorage,所以这就会导致 cookie 不同步的问题,一些文章也介绍了很多但是都没有说处理的很全面。这里我列下所有情况下 cookie 同步的问题:

  • 首次同步请求的 cookie 同步(NSHTTPCookieStorage 同步到 WKWebView cookie)
  • 异步 ajax 的 cookie 同步(NSHTTPCookieStorage 同步到 WKWebView cookie)
  • 服务器端(302)重定向/浏览器重定向的 cookie 同步(NSHTTPCookieStorage 同步到 WKWebView cookie)
  • 服务器端响应头里的 Set-Cookie 同步(WKWebView cookie 同步到 NSHTTPCookieStorage)
  • H5 侧的 document.cookie 同步(WKWebView cookie 同步到 NSHTTPCookieStorage)
  • 异步 ajax 响应头里的 Set-Cookie 同步(WKWebView cookie 同步到 NSHTTPCookieStorage)
  • cookie HTTPOnly 同步 (NSHTTPCookieStorage 同步到 WKWebView cookie 和 WKWebView cookie 同步到 NSHTTPCookieStorage)

基本上开源的处理方式都只处理到了前四步,后面几个是没有涉及到的,针对剩下的 cookie 同步问题,我们也是需要处理。

所以 KKJSBridge 是为了完全解决上面两个问题而出的新轮子,同时 KKJSBridge 也会提供其他的特性。

KKJSBridge 支持的功能

  • 基于 MessageHandler 搭建通信层

  • 支持模块化的管理 JSAPI

  • 支持模块共享上下文信息

  • 支持模块消息转发

  • 支持离线资源

  • 支持 ajax hook 避免 body 丢失

  • Native 和 H5 侧都可以控制 ajax hook 开关

  • Cookie 统一管理

  • WKWebView 复用

  • 兼容 WebViewJavascriptBridge

阅读全文 »
卡洛斯的博客

Mac上使用Privoxy将socks5转换为http代理

发表于 2018-03-05 | 分类于 tool |

Mac上使用Privoxy将socks5转换为http

shadowsocks 挺不错的,但是有些时候需要使用http代理来爬墙。这时候可以使用privoxy来将 socks5 代理转换为 http 代理。

配置

首先,确保 shadowsocks 已经正常起来的,默认的本地socks5端口号为 1080,可以使用 netstat 和 lsof 命令查看端口情况。

安装 privoxy, mac 使用 brew install privoxy 即可,安装完成后,修改 privoxy 配置文件。

如果安装过程报如下错误,先手动创建这个目录 sudo mkdir -p /usr/local/sbin, 再添加修改权限 chmod 777 /usr/local/sbin。

Error: The `brew link` step did not complete successfully
The formula built, but is not symlinked into /usr/local
Could not symlink sbin/privoxy
/usr/local/sbin is not writable.

编辑 config 文件:

vim /usr/local/etc/privoxy/config

修改内容如下:

forward-socks5 / 127.0.0.1:1080 .listen-address 127.0.0.1:8118

listen-address 默认是监听本地8118端口,如果端口没有被占用,可以不用修改

启动 privoxy

/usr/local/sbin/privoxy /usr/local/etc/privoxy/config

可以使用 ps aux|grep privoxy和 lsof -i:8118来检查是否成功启动

阅读全文 »
卡洛斯的博客

YYWebImage,SDWebImage和PINRemoteImage比较

发表于 2018-02-27 | 分类于 WebImage |

YYWebImage,SDWebImage和PINRemoteImage比较

共同的特性

  1. 以类别 api 下载远程图片。
  2. 图片缓存
  3. 图片提前解码
  4. 其他

图片框架比较

图片后处理

根据下面的比较,可以看出图片后处理方面,PINRemoteImage > YYWebImage > SDWebImage

  • YYWebImage:

    • 支持不带标记的后处理。

      /**
      Set the view's `image` with a specified URL.

      @param imageURL The image url (remote or local file path).
      @param placeholder he image to be set initially, until the image request finishes.
      @param options The options to use when request the image.
      @param manager The manager to create image request operation.
      @param progress The block invoked (on main thread) during image request.
      @param transform The block invoked (on background thread) to do additional image process.
      @param completion The block invoked (on main thread) when image request completed.
      */
      - (void)yy_setImageWithURL:(nullable NSURL *)imageURL
      placeholder:(nullable UIImage *)placeholder
      options:(YYWebImageOptions)options
      manager:(nullable YYWebImageManager *)manager
      progress:(nullable YYWebImageProgressBlock)progress
      transform:(nullable YYWebImageTransformBlock)transform
      completion:(nullable YYWebImageCompletionBlock)completion;
  • SDWebImage: 不支持图片后处理。

  • PINRemoteImage:

    • 支持带标记的图片后处理。对于同一张图片,当需要不同的后处理方式时(a 界面需要正圆角,b 界面需要小幅度的圆角),尤为有用。

          /**
      Set placeholder on view and retrieve the image from the given URL, process it using the passed in processor block and set result on view. Call completion after image has been fetched, processed and set on view.

      @param url NSURL to fetch from.
      @param placeholderImage PINImage to set on the view while the image at URL is being retrieved.
      @param processorKey NSString key to uniquely identify processor. Used in caching.
      @param processor PINRemoteImageManagerImageProcessor processor block which should return the processed image.
      @param completion Called when url has been retrieved and set on view.
      */
      - (void)pin_setImageFromURL:(nullable NSURL *)url placeholderImage:(nullable PINImage *)placeholderImage processorKey:(nullable NSString *)processorKey processor:(nullable PINRemoteImageManagerImageProcessor)processor completion:(nullable PINRemoteImageManagerImageCompletion)completion;
阅读全文 »
卡洛斯的博客

仿微信图片压缩方式

发表于 2018-02-10 | 分类于 图片 |

仿微信图片压缩方式

现状

目前公司 app 压缩图片,是压缩成一个固定大小的,比如把所有图片压缩成一个 120k 的图片,或者压缩成一个 300k的图片,这两种方式都不好,压缩成 120 会让原始图片质量太低,而显示模糊。

图片上传限定到 300k,也不是很对,这样会造成所有的图片都会以 300k 上传,这样会造成两个问题:

  1. 上传速度会很慢,也比较耗流量。9 张图一起上传会更慢。
  2. 300k 图片质量其实和 150k 左右的图片看起来差不多,过多的图片空间占用意义不大。

参考微信

因为发现微信的图片压缩后质量还不错,于是选择了 12 张不同大小不同尺寸的图片进行测试。

发现微信客户端上的图片压缩后,基本是 90k~200k 这个范围。

代码实现

本来一开始想试下鲁班算法,但是发现鲁班算法压缩后的图片大小跟微信的还是有些区别了,所以就放弃了。

于是参考微信,简单实现 iOS 压缩方式如下:

阅读全文 »
卡洛斯的博客

iOS界面开发需要用Point乘以屏幕比例系数吗

发表于 2018-02-10 | 分类于 layout |

iOS界面开发需要用Point乘以屏幕比例系数吗

现状

由于公司给 app 设计的效果图是基于 iPhone 6 的,效果图的宽度是 375 point。为了适应大小屏幕,我们专门封装了一个函数来获取一个适配的点。

inline CGFloat Point(void)
{
static CGFloat point;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
point = ([UIScreen mainScreen].bounds.size.width / 375.0f);//以iPhone6为标准的比例值
});

return point;
}

所以在使用的时候,就会是效果图上里面的点乘以适配点。比如一张 140 x 140 图片距离屏幕左边 20 点。

UIImageView *imageView = [UIImageView new];
imageView.frame = CGRectMake(20 * Point(), 0, 140 * Point(), 140 * Point());

这样的话,在大屏幕里面,间距和图片大小都会比效果图更大,

在小屏幕里面,间距和图片大小都会比效果图更小,这样就可以适配小屏幕了。

虽然这样很好的适配小屏幕,但是会让大屏幕的间距看起来很大,总体看起来没有很好的利用空间。比如距离左边 20 点,在 7p 上就是 22.08,会多出 2.08 的点,这样就会距离左边更大了。

20 * Point() = 20 * 1.104 = 22.08
阅读全文 »
123
KarosLi (卡洛斯)

KarosLi (卡洛斯)

正在修行的路上

24 日志
13 分类
18 标签
友情链接
  • 晓飞的博客
© 2020 KarosLi (卡洛斯)
由 Hexo 强力驱动
主题 - NexT.Mist
| 本站总访问量次 本文总阅读量次