Skip to content

Latest commit

 

History

History
721 lines (410 loc) · 43.1 KB

07.md

File metadata and controls

721 lines (410 loc) · 43.1 KB

2018.07

WWDC 2018 苹果推荐的大图加载方式

作者: halohily

在 iOS 开发中,图片载入到内存中占用的空间和它的二进制文件大小无关,而是基于图片的尺寸。在 WWDC 2018 中,苹果为我们建议了一种大家平时使用较少的大图加载方式,它的实际占用内存与理论值最为接近。下面是示例代码:

func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage
{
let sourceOpt = [kCGImageSourceShouldCache : false] as CFDictionary
// 其他场景可以用createwithdata (data并未decode,所占内存没那么大),
let source = CGImageSourceCreateWithURL(imageURL as CFURL, sourceOpt)!

let maxDimension = max(pointSize.width, pointSize.height) * scale
let downsampleOpt = [kCGImageSourceCreateThumbnailFromImageAlways : true,
kCGImageSourceShouldCacheImmediately : true ,
kCGImageSourceCreateThumbnailWithTransform : true,
kCGImageSourceThumbnailMaxPixelSize : maxDimension] as CFDictionary
let downsampleImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOpt)!
return UIImage(cgImage: downsampleImage)
}

参考资料:https://juejin.im/post/5b2ddfa7e51d4553156be305

让人怀疑人生的一段代码

作者: 高老师很忙

大家可以看一下下面两段代码(图1和图2)

如果已经显示了一张沙盒里的图片,这个时候对相同路径的文件进行修改和删除,通常我们认为_imageView应该不受到影响,因为图片已经完成渲染,但事实并非如此,_imageView竟然会跟着发生变化,并且变化的结果也是不可预期的,比如说删除对应的路径的文件,_imageView可能全部黑屏或者一些黑屏,如果不想因为后续操作而影响_imageView的显示,那么就需要用NSData中转一下(图3)。

对于不需要及时删除图片或者修改图片路径的情况,建议在dispath_asyn到main queue里执行图片删除或者文件更改工作,因为此时图片已经完成渲染,再进行删除或者修改操作就没问题了

是不是感觉有点怀疑人生?在WWDC 2018 Lab中我咨询了相关的苹果工程师,结果是为了性能,内部会有一些同步机制。以后大家遇到这样的问题可以参考一下。

git 恢复误删的 stash

作者: Vong_HUST

日常开发过程中,相信大家都使用 git,团队协作使用 git-flow。也经常会遇到需求做到一半,产品或者测试反馈一个线上问题,不得不 stash 当前已经写了一半的代码,然后切回到 master 查看问题,然后又恢复回原来的 stash。但是这样操作有时候可能会把之前 stash 的代码误删,辛辛苦苦写的代码说没就没了。那么问题来了,stash 能否像 commit 那样可以随时恢复?

答案当然是肯定的。我们知道只要有提交记录,git 肯定能恢复。其实 stash 某种程度上也可以看做一种 commit,如果还记得当时 stash 的名称,就更加容易恢复了。可以使用如下命令行来恢复,其中 'your stash name' 处填入 stash 操作时留的名称

$ git fsck 2> /dev/null | awk '/commit/{print $3}' | git show --stdin --grep 'your stash name'

最终可以在终端中看到一些 commit 信息和日期,找到对应想恢复的 SHA,然后执行

$ git stash apply your-commit-sha

关于第一处代码的解释:

  1. The funny 2> /dev/null part ignores all error messages (they are thrown to /dev/null a dark hole in every UNIX system).
  2. git fsck checks your repo for orphaned commits.
  3. This prints a list of information, containing the id of the commit and it’s type, for example:
   dangling commit 6108663eaaac4b7e850f6d492cf83e7b65db2c97
   dangling commit b526a825c7730075eb5938917c8b8b7a98f63cdf
   dangling commit 04479ae959fc7470d04e1743f1c7149414c366fa
   dangling blob c6609e5099056da80ea1cdf5bea302225bd6b7ed
   dangling commit 9d65fa867f23d28ce618fcb5d7988180efb67f9c
  1. We’re after commit ids, which is the third part of each line, so we run: awk '/commit/{print $3}’ to obtain the third part of each line.
  2. git show shows information about that particular commit. So if we filter and print those containing the bug number… voilà!

参考

How to recover a deleted git stash

Can we recover deleted commits?

Objective-C import 第三方库头文件总结

作者: KANGZUBIN

当我们的 iOS 工程要引用其它第三方开源库时,一般有以下几种方式:

(1)下载源代码直接拖拽到工程中;

(2)使用 CocoaPods 管理,当开启 use_frameworks! 标记时,第三方库会被编译成 .framework 引入工程,否则就会编译成 .a 静态库;

(3)使用 Carthage 管理,第三方库会被编译成 .framework 然后导入工程;

(4)直接下载作者编译好的 .framework 导入工程。

但当我们在代码中要 import 第三方库的头文件时,对于这几种情况,写法都不太一样,以 AFNetworking 为例,总结如下:

  • 对于(1)拖拽源码,只能以 "" 引号的方式 import,
#import "AFNetworking.h"
  • 对于(2)CocoaPods,如果开启 use_frameworks!,则将编译成 .framework 库,只能以 <> 尖括号的方式 import,此外,对于(3)和(4)也是这样
#import <AFNetworking/AFNetworking.h>
  • 而对于 CocoaPods,如果不开启 use_frameworks!,则将编译成 .a 库,此时有如下 3 种方式 import,
#import "AFNetworking.h"
// 或者
#import <AFNetworking.h>
// 或者
#import <AFNetworking/AFNetworking.h>

那么问题来了,如果我们在写一个 SDK 或者私有的 Pods 库,需要宿主 App 工程引用某一个第三方库,如上所述,宿主工程有很多方式引用第三方库,这样我们就无法确定应该以哪种方式 import 头文件,怎么办呢?这时候我们就可以使用 __has_include() 宏来判断。

__has_include() 宏接收一个预引入的头文件名称(引号或者尖括号都可以)作为参数,如果该头文件能够被引入则返回 1,否则返回 0,使用起来如下:

#if __has_include(<AFNetworking/AFNetworking.h>)
#import <AFNetworking/AFNetworking.h>
#else
#import "AFNetworking.h"
#endif

获取 ipa 包三种姿势

作者: Lefe_x

以前获取一个应用的 ipa 包轻而易举,然而今天想获取一个 ipa 包,如果没有适当的方法,非常费劲。今天我们就聊聊如何获取 ipa 包,下面这三种方式都经过亲自验证,可能由于不同环境会出现异常,若遇到问题可以到【知识小集】gong-Zhong-Hao 留言。如果你有更好的方式,不妨分享出来。

方式一:iTunes

苹果既然在高版本的 iTunes 取消了获取 ipa 包的入口,那我们就想办法降级处理。需要下载低版本的 iTunes。 下载

下载完后,安装,第一次启动的时候按住 option 键,这样才不会报错,安装完成后,即可下载应用的 ipa 包。下载完成后,在应用的图标上按右键,show in finder 即可找到 ipa 包。

方式二:pp助手

电脑安装一个 pp助手客户端,直接下载越狱应用,下载完成后,即可在“本地应用”中找打 APP 的 ipa 包。需要强调一点,这种方式下载的应用是解密后的 ipa。

方式三:抓包

在 Mac 中的 iTunes 中下载应用,通过 Charles 抓包获取到 ipa 包的下载地址,直接在浏览器中下载,下载地址是在 p52-buy.itunes 这个域名下。

一些 UI 性能优化的 tips

作者: halohily

  • 圆角效果:圆角效果的优化老生常谈,产生性能问题的根源在于原生圆角效果带来的离屏渲染开销。通常我们推荐直接使用圆角的素材,或者提前在子线程将图片进行圆角裁剪,这两者原理相同。除此之外,还有一种思路是在需要圆角的视图最上层添加一个中空的圆角遮罩层,以此来做出圆角效果。这个遮罩层和被盖在下面的视图在显示时会由 GPU 进行图层混合,而图层混合的开销远小于离屏渲染。值得一提的是,由于圆角效果通常在一屏中频繁出现,所以这个遮罩的图片素材可以只加载一次,并且应用于每一个圆角视图,避免重复加载。
  • 阴影效果:值得注意的是系统原生的阴影实现要求 layer 的 masksToBounds 值为 YES,所以原生的阴影效果和圆角是不兼容的。高效的阴影实现是为阴影指定 shadowPath,如果你还没用的话,不妨试一下。
  • 适时替换轻量控件:@ibireme 在他的性能优化文章中提出在合适的时候用 CALayer 替换 UIView,这确实有效,不过盲目替换往往会造成代码维护的困难。这里举两个适合的场景:绘制线条时,完全可以替换。以及静态展示图片时,将图片对象赋值给 layer 的 content 属性,也完全可以达到效果。
  • 图片解码:图片解码的知识不再赘述,值得一提的是,对于不同的图片格式,不同的解码算法,或者使用系统解码方法时的不同参数设置,都会影响解码性能,如果有这方面瓶颈的,不妨做多种尝试。

再说一个经典的例子:为了实现一个简单的画板需求,有人会在 UIView 上频繁调用 drawRect 方法进行新笔划的绘制,殊不知有一个天生的专用图层对象 CAShapeLayer 是很适合做这件事的。CAShapeLayer 不需要像普通 CALayer 一样创建寄宿图,不会造成巨量内存的使用,并且它使用了硬件加速。

UI 性能优化时,我们常常需要实时监测帧率。这里讲一下 @ibireme 的帧率监测工具 YYFPSLabel 的实现原理:使用 CADisplayLink,在每帧的回调事件中,计数器 c 加一,并且累计时间间隔 t 也进行更新。当时间间隔够 1 秒后,使用 c/t 计算出过去 1 秒的帧率,而后计数器清零,时间戳更新为当前时间戳,再重复之前步骤。因此 YYFPSLabel 的帧率更新周期在 1 秒左右。

Assets的几个方便用法

作者: 高老师很忙

Assets想必大家都使用过,今天聊几个Assets比较方便的用法。

  • 在工程中,某个通用的颜色,我们可能会用宏或者全局变量来表示,这样可以方便大家的使用,但有一个弊端,在storyboard或者xib布局的时候,设置颜色依旧要去设置具体的RGB值;而Assets给我们提供了一个很方便的功能,可以创建New Color Set,就弥补了刚才方案的缺陷(如图1,图2),并且代码中使用也很方便。

  • 在需要拉伸图片的时候,通常会使用UIImage的API的-[UIImage resizableImageWithCapInsets:resizingMode:]这个方法;而Assets为我们提供了Slicing的功能(如图3),在Assets中直接设置后,在storyboard和xib中就可以直接显示拉伸后的图片,在代码中使用也及其方便,直接用-[UIImage imageNamed:]方法即可。

  • 如果是Universal的工程,同一个UIImageView,在iPhone中显示图片A,在iPad中显示图片B,Assets可以很方便的通过Devices设置,会让代码看着很清爽,不会存在判断机型再去设置图片的恶心代码。在设置横竖屏的时候也可以充分利用Width ClassHeight Class两个参数(如图4)。

我觉得这3个用法在工作中还是很实用的,当然Assets还有其他很好用的功能,欢迎大家一起交流。

两种 App 启动连续闪退检测策略

作者: KANGZUBIN

当我们要做 App 日志上报时,需要考虑到一种行为:App 在启动时就崩溃闪退了,而且当遇到连续启动闪退(也就是每次打开 App 必崩)时,那几乎是灾难,但更可怕是,如果没有有效的监测手段,我们可能对已发生的这种线上严重问题毫不知情。

WeRead 团队博客的《iOS 启动连续闪退保护方案》和 MrPeak 老师的《iOS App 连续闪退时如何上报 crash 日志》分别介绍了两种简易的如何检测连续闪退的策略,在这里跟大家分享一下。

  • 计时器方法

1)App 本地缓存维护一个计数变量,用于表示连续闪退的次数;

2)在启动入口方法 application:didFinishLaunchingWithOptions: 里判断 App 之前是否发生过连续闪退,如果有,则启动保护流程,自我修复,日志上报等,否则正常启动。判断的逻辑如下:

3)先取出缓存中的启动闪退计数 crashCount,然后把 crashCount 加 1 并保存;

4)接着使用 dispatch_after 方法在 5s 后清零计数,如果 App 活不过 5 秒计数就不会被清零,下次启动就可以读取到;

5)如果发现计数变量 > maxCount,表明 App 连续 maxCount 次连续闪退,启动保护流程,重置计数。

具体的代码如下图所示:

这种计数器方法逻辑简单,与原有的代码耦合小。但存在误报可能(用户在启动 App 后又立即 kill 掉,会被误认为是 crash),不过可以通过设置时间阈值或者在 applicationWillTerminate: 里标记 App 是被手动 kill 来减少误报。

  • 时间数组比对

我们可以在本地保存一个 App 每次启动时间、闪退时间、手动关闭时间的时间数组,然后在 App 启动时根据分析各个时间戳判断是否存在连续闪退(当闪退时间减去启动时间小于阈值 5 秒时,则认为是启动闪退),具体如下:

1)App 每次启动时,记录当前时间 launchTs,写入时间数组;

2)App 每次启动时,通过 crash 采集库,获取上次 crash report 的时间戳 crashTs,写入时间数组;

3)App 在接收到 UIApplicationWillTerminateNotification 通知时,记录当前时间戳 terminateTs,写入时间数组。注意,之所以要记录 terminateTs,是为了排除一种特殊情况,即用户启动 App 之后立即手动 kill app。

如果我们正确记录了上面三个时间戳,那么我们可以得到一个与 App crash 行为相关的时间线,如下图:

根据各种时间线的行为特征,我们只需要加上时间间隔判断,就能得知是否为连续两次闪退了。注意,如果两个 crashTs 之间如果存在 terminateTs,则不能被认为是连续闪退。

以上,介绍了两种检测 App 是否存在启动连续闪退的策略。

此外,对于连续闪退的保护方案以及连续闪退如何上报日志,请详细阅读开头提到的两篇博文。

你的项目中还用热修复吗?

作者: Lefe_x

前两天知识小集群里有人讨论关于热修复的问题,对此我非常感兴趣,今天作为一个小集和大家探讨一下。虽然目前苹果严禁带有热修复功能的 APP 上线,一旦发现,将增加审核时间(大约是一周的时间)。苹果主要考虑到了安全问题,避免给自己找事,所以干脆禁用了 JSPatch。但是 JSPatch 使用的 API 并没有违反苹果的规定,他也就没有一个十足的理由拒绝你的 APP 上线。这样就导致还有很多公司在悄悄地用 JSPatch。不过原理基本都是对 JSPatch 进行混淆后使用,当然如果你有能力自己实现一个 JSPatch 也可以。

被拒苹果的拒绝理由大概是这样的:

目前我了解到市面上主要通过以下几种方式进行混淆(如果对这个话题感兴趣,后续我们会在【知识小集】gong-zhong-hao 进一步探讨):

方式一:使用官方提供的混淆方式

目前使用官方提供的 JSPatch 服务任然可以过审,据说也是通过静态混淆-宏定义 这中方式。

方式二:Bugly(静态混淆-宏定义)

Bugly 提供了热修复功能,它提供了一种对 JSPatch 混淆的方式。在 BuglyHotfixConfuse_pch.h 文件中把需要混淆的类名方法名替换掉。有兴趣的读者可以 下载 查看详细代码。

方式三:自己混淆

自己混淆当然是最保守的,苹果很难察觉。某天网上爆出一个 ZipArchive 安全漏洞,而这个漏洞的一个条件就是使用了类似 JSPatch 这种可以动态执行脚本的功能,而被爆出的 APP 经查确实使用混淆后 JSPatch,而他们采用的混淆方式也就是自己混淆。所以自己混淆 JSPatch 这条路是通的。自己混淆主要是理解 JSPatch 的原理,换一种方式来实现。

一个关于 GCD group 使用的小 tip

作者: halohily

在项目中看到一段使用 GCD group 处理的代码,简化下来大概如图1,dispatch_group_notify 的调用放在了 dispatch_group_async 的 block 中,乍一看会有是否产生永久阻塞的疑问,因为子任务完成后的派发任务被放在了一个子任务中。然而其实这是不会阻塞的,代码会按编写人的预期进行执行,即 log1 输出之后,输出 log2。这是因为 dispatch_group_notify 的 block 是异步执行的。

再举个例子,如图2,执行结果依次会是:log 1,log 2 ,log 4 ,log 5 ,log 3。

虽然此处结果正确,但这种将 dispatch_group_notify 的调用放在某一个子任务的执行块中的写法是不被推荐的,它不但反逻辑,而且并不总能保证结果正确。比如此例中,在调用了 dispatch_group_notify 的子任务之后,又为该任务组使用 dispatch_group_async 语句添加后续子任务,这时代码的执行结果是不确定的。

既然最开始的例子中执行结果是正确的,有的同学会问,如果把 dispatch_group_notify 的调用放在所有子任务的最前面,如图3,是否也能获得预期的结果呢?答案是否定的,因为在最开始调用 dispatch_group_notify 时,子任务数量为0,它的代码块会立即执行。而后为该组派发了多个子任务,当这些子任务都执行完毕后,也并不会再次触发 dispatch_group_notify 的代码块。

UIAlertView 与输入框结合使用时的一个坑

作者: Vong_HUST

相信 UIAlertView 大家应该都很熟悉,但是最近遇到一个坑。

由于历史原因,项目中还在大量使用 UIAlertView。某天测试过来反馈说,评论框字符长度超过最大长度时,点击发送,弹出一个 alert 提示,点击确定后,评论框无法在被激活,也就是没法弹出键盘了。很是怪异,debug 无果,搜了一下 stackoverflow,发现有人遇到过类似的问题,可以点击末尾的参考链接来查看具体详情。

他给出的解决方案就是把这种情况下的 UIAlertView 换成 UIAlertController。试了下这种方式,果然是可行的,由于之前 UIAlertView 是不依赖其它视图层级的,创建后直接 show 就可以了,所以很多地方直接写在了非视图控制器类中。在换成 UIAlertController 之后,由于它是继承自 UIViewController 的,所以必须要有 VC 把它 present 起来。解决方案也很简单,写一个 UIViewController 的分类获取当前顶部可见的 ViewController,然后在上面 presentUIAlertController 即可,获取顶部可见 ViewController 的代码随便一搜就可以找到,这边就不贴了。

PS:UIAlertControlleriOS8 以后才提供的,不过相信大家也不用适配 iOS8 之前的系统了吧😂。如果还要适配,那就只能做版本区分了。。。

参考

iOS 9 - Keyboard pops up after UIAlertView dismissed

提高Shortcuts调试效率的小技巧

作者: 高老师很忙

iOS12提供了Shortcuts的功能,今天给大家介绍2个苹果提供的提高Shortcuts调试效率的小技巧。

  • Shortcuts是可以通过Siri唤起的,如果每次调试的时候都要和Siri说一次短语,既浪费时间又打扰旁边正在工作的同事,Xcode提供了Siri Intent Query功能,在调试Intents Extension或者Intens UI Extension的target时,直接在里面输入你要说的短语,就可去省去调戏Siri的时间啦。

  • iPhone设置->开发者里面提供了Display Recent ShortsDisplay Donations on Lock Screen的开关,可以忽略系统当前的建议和预测,显示我们需要调试的Shortcuts;同时还支持Force Sync Shortcuts to Watch的功能,手动去强制同步到Watch,会节省很多时间哦!

我觉得这2个小技巧还是很实用的,如果有其它小技巧欢迎一起交流!

Xcode 断点调试时打印变量值报错的问题(编译优化相关)

作者: KANGZUBIN

在日常开发中,我们经常会在 Debug 模式下打断点进行调试,并通过 LLDB 的 po 命令在控制台打印一些变量的值,以方便排查问题。

今天在 Release 模式下编译运行项目,发现要打印某一变量的值时(po xxx),报如下错误:

error: Couldn't materialize: couldn't get the value of variable xxx: no location, value may have been optimized out
error: errored out in DoExecute, couldn't PrepareToExecuteJITExpression

大致意思是说,xxx 的值不存在,可能已经被编译优化了。而且在断点模式下当我们把鼠标的箭头移到某一变量上要进行快速浏览时,发现它们的值都是 nil

查了一下才发现,原来这与 Xcode 工程的编译选项 Optimization Level 设置有关,它是指编译器的优化级别,优化后的代码效率比较高,但是可读性比较差,且编译时间更长,它有 6 个选项值如下图:

上述每选项值的详细说明可以参考《Xcode 中 Optimization Level 的设置》《如何加快编译速度》两篇文章,我们这里不再赘述。

Xcode 工程的 Optimization Level 值在 Debug 模式下默认为 None [-O0],表示编译器不会尝试优化代码,保证调试时输出期望的结果;而在 Release 模式下默认为 Fastest, Smallest[-Os],表示编译器将执行所有优化,且不会增加代码的长度,它是可执行文件占用更少内存的首选方案。

这也是为什么我们在 Release 模式下断点打印变量会报错,因为编译器已经给代码做了优化,它将不在调试时记录变量的值了。

此外,有时候遇到一些线上 Bug 但是在 Debug 调试时却无法复现,我猜有可能会跟编译优化有关,你觉得呢?欢迎留言讨论。

一次内存泄漏后的思考

作者: Lefe_x

最近项目中遇到一个内存泄漏的问题,SecondViewController 这个类在 pop 后并没有执行 dealloc 方法,也就没有被正常被释放。使用内存泄漏工具排查,并没有发现有循环引用的地方,手动查了一下也没发现异常。正在迷茫的时候,突然看到了一个注册监听的地方。实现方式类似下面这样:

- (void)dealloc {
    [[Manager sharedInstance] removeObserver:self];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [[Manager sharedInstance] addObserver:self];
}

看到这里你应该已经猜到 SecondViewController 为什么没被释放,它被 Manager 持有了,而 Manager 是一个单例,自然 SecondViewController 也不会被释放,dealloc 方法也不会执行。

这种设计很常见,往往给某个服务注册监听,达到类似通知的效果。如果使用数组保存监听者,监听者将会被数组持有。有同学可能说,可以在 viewDidAppear 注册,在 viewWillDisappear 移除,这样 SecondViewController 就会被释放。但是,这样设计很糟糕,我们尽量不去约束调用者如何调用某个 API。

其实正确的做法是使用一个弱引用容器,我们可以使用 NSHashTable 来保存监听者,这样当监听者释放后,将自动从 NSHashTable 中移除,也不需要主动调用移除监听者的方法(也可以调用,视情况而定)。下面是一个简单的实现,你也可以参考 YYTextKeyboardManager 的实现:

_listenerTable = [NSHashTable weakObjectsHashTable];

- (void)addObserver:(NSObject *)obj {
    [self.listenerTable addObject:obj];
}

- (void)removeObserver:(NSObject *)obj {
    [self.listenerTable removeObject:obj];
}

怎么解决网络请求的依赖关系

作者: 蒋匿

怎么解决网络请求的依赖关系:当一个接口的请求需要依赖于另一个网络请求的结果?

  1. 思路 1:操作依赖:NSOperation 操作依赖和优先级。

例如 [operationB addDependency:operationA]; 虽然这个想法很好,但不适用异步,异步网络请求并不是立刻返回,无法保证回调时再开启下一个网络请求。

  1. 思路 2:逻辑判断:在上一个网络请求的响应回调中进行下一网络请求的激活。

这是最原始的想法,但还是有 BUG:可能拿不到回调无法执行到 block 块里面的代码。

  1. 思路 3:线程同步 -- 组队列(dispatch_group)。

先建一个全局队列 queue,并新建一个 group(用 dispatch_group_create()),然后向 Group Queue 依次追加 block,最后用 dispatch_group_notify 添加 block。当前面的 block 全部执行完,就会执行最后的 block。例如下图。

  1. 思路4:线程同步 --任务阻塞(dispatch_barrier)。

通过 dispatch_barrier_async 添加的操作会暂时阻塞当前队列,即等待前面的并发操作都完成后执行该阻塞操作,待其完成后后面的并发操作才可继续。使用 dispatch_barrier_async 可以实现类似组队列的效果。例如图2。

  1. 思路5:线程同步 -- 信号量机制(dispatch_semaphore)。

除了任务阻塞,还可以利用信号量实现这种阻塞效果:在异步开启任务 1 和任务 2 之前,初始化一个信号量并设置为 0,然后在任务 1 的 block 中写好请求操作,操作执行完后对前面的信号量加 1,在任务 2 的 block 中,需要在开始请求之前加上等待信号量的操作。这样一来,只有任务 1 中的请求执行完后,任务 2 等到了信号量加 1 才接着执行它的请求。例如图 3。

对AppStore在蜂窝网150MB的下载限制的理解

作者: 高老师很忙

大家应该都有这样的一个印象:在蜂窝网下,150MB的包在AppStore是不能下载的,每当看到AppStore下面展示的Size接近150MB的时候就会很紧张,这意味着又要来一波艰难的减包操作了。

那么这个150MB指的是什么呢?是AppStore展示的Size么?我做了一个小小的调研,发现并非如此,150MB是包的下载大小,而AppStore里展示的Size是安装后(有解压操作)的大小,对几个App进行了抓包操作:(这个是前段时间的调研,因为版本迭代可能数据会有出入,但足以说明问题)

从苹果之前的官方新闻也能看出端倪:

所以说AppStore蜂窝下载限制指的是下载的实际大小而非AppStore展示的Size。

在ITC构建版本处理完成后,可以看到详细信息:在各个机型上的下载大小以及安装大小,同时也会提示你是150MB下载限制,超过限制还会给你一个黄色警告哦。

减包是一个任重而道远的事情,不要等到在150MB的危险边缘再去处理哦!

静态 UITableView 两种 style 的差异

作者: Vong_HUST

想必设置页应该是各大应用所必备的,相信大部分还是采用静态 UITableView 的方式在构建,我们项目中也用到了。最近测试反馈一个问题就是一些配置项的描述文案会盖住单元格内容,如图所示。由于之前配置项比较少,所以没有发现,最近新增了好几个配置,所以问题暴露出来了。

图中【接收哪些人】的私信是一个 SectionFooter,由于 SectionFooter是悬停的,内容超过一屏的情况下,SectionFooter 会将单元格挡住,由于 footer 背景是透明的,所以看起来是重叠的。由于 tableView 设置的 stylePlain 的,这种情况下 SectionFooterSectionHeader 都是悬停的。如果要想他们不悬停,只需要把 tableViewstyle 设置成 Grouped 即可。

但是需要注意的是 Grouped 样式的 SectionFooter 是自带间隔的,会比 Plain 样式下的 SectionFooter 高 18pt,所以改成 Grouped 样式之后如果要同步 Plain 样式的间隔,这个 tableView:heightForFooterInSection: 代理方法返回的高度要减小18。

配置 xcodebuild 命令打包支持 Bitcode

作者: KANGZUBIN

我们通常会把一些公用的模块抽离出来打成一个 .a 静态库或者 .framework 动态库,然后再嵌入到宿主工程中。

最近我们的 App 工程开启 Bitcode 编译选项后(Enable Bitcode = YES),发现在进行 Archive 归档打 Release 包时,报如下错误,提示说工程使用的 libTestStaticSDK.a 静态库不支持 Bitcode:

ld: bitcode bundle could not be generated because '/.../TestApp/TestStaticSDKLib/libTestStaticSDK.a(TestStaticSDK.o)' was built without full bitcode. All object files and libraries for bitcode must be generated from Xcode Archive or Install build for architecture armv7

但是我们的 libTestStaticSDK 静态库工程的 Build Settings 中同样是有配置开启 Bitcode 的,为什么打出来的 .a 包却不支持 Bitcode 呢?

通过查阅 StackOverflow 我们发现,原来开启 Bitcode 后,在 Xcode 中进行 "Build" 或 "Archive" 时,Xcode 会自动在编译命令后面添加 -fembed-bitcode 标识,而如果使用 xcodebuild 命令进行打包,则需要手动添加一个 OTHER_CFLAGS,如下:

xcodebuild build OTHER_CFLAGS="-fembed-bitcode" -target libTestStaticSDK ...

另外一种解决方案是,在静态库 Xcode 工程的 Build Settings 中,添加一个 "User-Define Setting",内容为:'BITCODE_GENERATION_MODE' => 'bitcode',如下图所示:

这样在使用 xcodebuild 命令时就不用添加 OTHER_CFLAGS="-fembed-bitcode" 了。

综上,为了通用,我们可以在 xcodebuild 命令后同时添加上述两种标识,因此一个完整的静态库打包脚本大致如下(同样适用于 Framework 的打包):

参考链接

iOS 内存泄露工具

作者: Lefe_x

在日常开发中总会遇到内存泄漏的的问题,而排除内存泄漏一般会依靠以下这些工具:

这个 WeRead 团队开发的一个内存泄漏检测工具,主要用来检测 UIViewController 和 UIView 中存在的内存泄漏。如果检查到内存泄漏,会弹出 Alert 提示存在内存泄漏。当然,如果某个 UIViewController 是单例,将会误检。

如果检查出内存泄漏,点击 Alert 上的 Retain Cycle 将使用 FBRetainCycleDetector 检查存在循环引用的对象。比如:

-> DownloadAudioListViewController ,
-> _callblock -> __NSMallocBlock__ 

这是 facebook 开源的一个内存泄漏检测工具,它可以检测出循环引用:

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:myObject];
NSSet *retainCycles = [detector findRetainCycles];

检查出的内存泄漏将打印出来:

-> DownloadAudioListViewController ,
-> _callblock -> __NSMallocBlock__ 

Instrument 中的 Leak 工具主要用来“突袭”,开发者定期地使用它来检测内存泄漏。而上面介绍的工具主要在开发过程中即可发现内存问题,提前暴露给开发者。

  • [Xcode 中的 Debug Memory Graph]

这个工具主要以图表的形式显示了当前内存的使用情况,可以查看循环引用,如果有内存问题会显示一个叹号。

Swift 版本建私有库时需要注意的地方

作者: 这个汤圆没有馅

利用 cocoapodsswift 版本私有库步骤和 OC 版本一样,只要把语言 Objc 切换成 Swift 即可。一般情况下,pod lib lint验证会报警告,如下图,加 --allow-warnings 直接忽略即可。

但是如果私有库里依赖了其他三方库,且该三方库的 swift 版本不一致,则 pod lib lint 会报一堆 error,如下图。

这个时候就需要根据警告里的提示配置 .swift-version。该文件默认情况是不会有的,需要手动添加,如下图。这个时候再次执行 pod lib lint --allow-warnings 验证就能通过。

用 NSDecimalNumber 处理 iOS 中的货币金额

作者: halohily

在iOS开发中,经常遇到货币金额的表示与计算,你可能会使用 double 或 float 这样的浮点数,也可能使用 NSString 。无论用哪个,都需要再编写繁琐的精度控制、小数位数控制等代码。其实,苹果为我们提供了一个标准类 NSDecimalNumber 来处理这样的需求。

NSDecimalNumber 是 NSNumber 的子类,它提供了完善的初始化方法。对于令人头疼的金额计算,它还提供了贴心的加、减、乘、除运算方法。在进行这些运算的时候,你还可以通过 NSDecimalNumberHandler 对象来对运算的处理策略进行设置,比如舍入模式的选择,数据溢出、除零等异常情况的处理等。

下次遇到货币金额的需求,不妨了解一下 NSDecimalNumber。

参考资料:

Storyboard/Xib 颜色空间的坑

作者: Vong_HUST

今天分享一下 Xcode Interface Builder 设置背景色的一个坑。从 Xcode8 起,Xib/Storyboard 里的颜色空间默认从 Generic RGB 换成了 sRGB,但又不是所有的都会转换,很奇怪。所以当时在适配 Xcode8 的时候,颜色空间都统一全局替换了一遍。最近又遇到一次这个坑,在 Storyboard 把某个视图背景色从白色更改为 0xf0f1f2,然后 run 起来,和其 superview (superview 的背景色是用代码设置的 0xf0f1f2)竟然有一个明显的分割线,所以回想起当时适配时的这个问题,然后取到 Storyboard 里面一看,果然是颜色空间被莫名改为了 Generic RGB,如图所示。所以手动改变其颜色空间为 sRGB 即可。

参考链接:http://t.cn/RgpFOPg、http://t.cn/ReADdu7

添加沙箱技术测试员报错的问题

作者: 高老师很忙

在测试IAP的时候需要添加沙箱技术测试员,相当于创建一个虚拟的AppleID,填写的邮箱是不能写真实AppleID邮箱的,要不然就会报错,可以随意写一个不存在的邮箱,为了方便,创建一个简短的密码就会报错误:

按照提示改好后,依然报错:

并且没有任何的提示,感觉这点好坑,尝试了N遍之后发现是密码的复杂度不够,要包含大小写和数字,虽然是虚拟的AppleID,但是密码规则要和正式的AppleID一样,希望以后苹果把错误提示做的更好吧!😂

UIViewController 设置导航栏和标签栏不同 title 的问题

作者: KANGZUBIN

我们通常会在一个 UIViewControllerviewDidLoad 方法中通过 self.title = xxx 的方式给一个页面设置其导航栏标题,相信大家对这再熟悉不过了。

如果一个 VC 页面中同时具有 NavigationBar(导航栏)和 TabBar(标签栏),而且我们又想让这两个地方的标题显示不一致,如下图所示,在首页顶部导航栏标题中显示“知识小集”,而在底部标签栏标题中显示“首页”:

但是,当我们在 UITabBarController 中初始化好上述页面结构后,且设置首页 VC 的 tabBarItem.title 为 “首页”,然后在首页 VC 的 viewDidLoad 方法中设置 self.title 为 “知识小集”,编译运行后我们发现首页底部标签栏的标题也变成“知识小集”了,而不是刚设置的“首页”。

查了苹果文档中关于 UIViewControllertitle 属性的定义,有如下一段描述:

If the view controller has a valid navigation item or tab-bar item, assigning a value to this property updates the title text of those objects.

也就是说,如果一个 VC 同时有导航栏和标签栏,那么当给 title 赋值时,会同时修改这两个地方的标题。所以如果我们只想设置导航栏的标题,可以通过 self.navigationItem.title = xxx 的方式来实现。

因此,在一个 VC 中设置相关标题简单总结如下:

  • self.navigationItem.title: 设置 VC 顶部导航栏的标题

  • self.tabBarItem.title: 设置 VC 底部标签栏的标题

  • self.title: 同时修改上述两处的标题

参考文档:UIViewController.title

如何更容易看懂宏

作者: Lefe_x

相信你和我一样,也遇到过特别难理解的宏定义,比如宏与宏之间嵌套、带参数的宏。我们看个例子(这个宏并不是特别难,但也很绕):

#define JPBOXING_GEN(_name, _prop, _type) \
+ (instancetype)_name:(_type)obj  \
{   \
    JPBoxing *boxing = [[JPBoxing alloc] init]; \
    boxing._prop = obj;   \
    return boxing;  \
}

JPBOXING_GEN(boxObj, obj, id)

这个例子看着总是怪怪的,如果把上面的宏转换成实际代码,相信你会很容易看懂。

+ (instancetype)boxObj:(id)obj
{
    JPBoxing *boxing = [[JPBoxing alloc] init];
    boxing.obj = obj;
    return boxing;
}

其实就是各种参数的替换导致阅读起来比较困难。我们都知道程序经过预处理后就会把宏转换为实际的代码,而 Xcode 为我们提供了对单个文件进行预处理(Produce -> Perform Action -> Preprocess 'xxxx.m'),这样处理后,上面的宏就变成了:

+ (instancetype)boxObj:(id)obj { 
   JPBoxing *boxing = [[JPBoxing alloc] init]; 
   boxing.obj = obj; 
   return boxing; 
}

经过预处理后和我们手动翻译的结果一样。

vc多层push后回到指定页面的几种方法

作者: 这个汤圆没有馅

场景如下: RootVC -- > A -- > B -- > C,然后现在要求C直接pop回到A。

方法一:C返回到B的时候写个回调,B接收到回调再自己pop到A,但是这个方法B的页面会闪现一下,用户体验不好,不推荐。

方法二:在B push 到C的时候,直接把B从导航控制器的堆栈中移除,如图一。

方法三:写一个UIViewController的catrgory,方法实现如图二。在C的backAct方法中使用,如图三。有的同学可能会怀疑B会不会内存泄露,可以在B中打印dealloc。

这里比较推荐方法三。不论有多少级的push,只要传入指定页面的类名,都能回到该页面。