说说iOS与内存管理(下)

首先,在这里我要和各位关注小站的朋友们说声抱歉,在刚刚过去的一年当中,小站的文章产出非常少,更新不多。当然这和我过去一年当中的工作和生活状况是有很大关系的。老实说,过去一年中,在支付宝这边(至少是我所在的团队)的工作压力和强度确实比较大,很少能有较为轻松的时候,个人也有点变懒了,所以很少能拿出时间来到这里写些东西。

另外,本文这个“内存管理”的主题也是我在这边工作的第一个重要任务,所以一直未能把结论在这里写出来。不过前一阵子,阿里云的“云栖”社区已经把我在内网的一篇帖子转发了出来。所以今天,正值小站上线三周年和到支付宝工作一年的日子里,把这个系列文章的最后一篇补全。本篇重点说下“苹果的malloc源码实现”,对其源码做一个总体分析。

本文的大部分内容是转发我本人发在ATA(阿里技术协会)的文章。

0. iOS内存基本原理

在接触iOS开发的时候,我们都知道“引用计数”的概念,也知道ARC和MRR,但其实这仅仅是对堆内存上对象的内存管理。用WWDC某Session里的话说,这其实只是内存管理的冰山一角。

在内存管理方面,其实iOS和其它操作系统总体上来说是大同小异的,大的框架原理基本相似,小的细节有所创新和不同。

和其它操作系统上运行的进程类似,iOS App进程的地址空间也分为代码区、数据区、栈区和堆区等。进程开始时,会把mach-o文件中的各部分,按需加载到内存当中。

screenshot

而对于一般的iPhone,实际物理内存都在1G左右,对于超大的内存需求怎么办呢?其实这也是和其它操作系统一样的道理,都由系统内核维护一套虚拟内存系统。但这里需要注意的是iOS的虚存系统原则略有不同,最截然不同的地方就是当物理内存紧张情况时的处理。

当物理内存紧张时,iOS会把可以通过重新映射来加载的内容直接清理出内存,对于不可再生的数据,iOS需要App进程配合处理,向各进程发送内存警告要求配合释放内存。对于不能及时释放足够内存的,直接Kill掉进程,必要时时甚至是前台运行的App。

如上所述,iOS在外存没有交换区,没有内存页换出的过程。

1. malloc基本原理

在iOS App进程地址空间的各个区域中,最灵活的就要属堆区了,它为进程动态分配内存,也是我们经常和内存打交道的地方。

通常,我们会在需要新对象的时候,进行 [NSObject alloc]调用,而释放对象时需要release(ARC会自动帮你做到这些)。

而这些alloc、release方法的调用,通常最终都会走到libsystem_malloc.dylib的malloc()和free()函数这里。libsystem_malloc.dylib是iOS内核之外的一个内存库,我们App进程需要的内存,先回请求到这里,但最终libsystem_malloc.dylib也都会向iOS的系统内核发起申请,映射实际内存到App进程的地址空间上。

从苹果公开的malloc源码上来看,malloc的原理大致如下:

malloc内存分配基于malloc zone,并将内存分配按大小分为nano、tiny、small、large几种类型,申请时按需进行最适分配
malloc在首次调用时,初始化default zone,在64位情况下,会初始化default zone为nano zone,同时初始化一个scalable zone作为helper zone,nano zone负责nano大小的分配,scalable zone则负责tiny、small和large内存的分配
每次malloc时,根据传入的size参数,优先交给nano zone做分配处理,如果大小不在nano范围,则转交给helper zone处理。

screenshot
(截图自http://www.tinylab.org/memory-allocation-mystery-%C2%B7-malloc-in-os-x-ios/)

下面分别对nano zone和scalable zone上分配内存的源码做简要解读(由于苹果Open source的代码是针对OS X的特定版本,具体细节可能与iOS上有所不同,如地址空间分布)。

2. nano malloc

在支持64位的条件按下,malloc优先考虑nano malloc,负责对256B以下小内存分配,单位是16B。

nano zone分配内存的地址空间范围是0x00006nnnnnnnnnnn(OSX上64位情况),将地址空间从大到小一次分为Magazine、Band和Slot几个级别。

  • Magazine范围对应于CPU,CPU0对应Mag0,CPU1对应Mag1,依次类推;
  • Band范围为2M,连续内存分配当内存不够时以Band为单位向内核请求;
  • Slot则对应于每个Band中128K大小的范围,每个Band都分为16个Slot,分别对应于16B、32B、…256B大小,支持它们的内存分配

分配过程:

  • 确定当前cpu对应的mag和通过size参数计算出来的slot,去对应metadata的链表中取已经被释放过的内存区块缓存,如果取到检查指针地址是否有问题,没有问题就直接返回;
  • 初次进行nano malloc时,nano zone并没有缓存,会直接在nano zone范围的地址空间上直接分配连续地址内存;
  • 如当前Band中当前Slot耗尽则向系统申请新的Band(每个Band固定大小2M,容纳了16个128k的槽),连续地址分配内存的基地址、limit地址以及当前分配到的地址由meta data结构维护起来,而这些meta data则以Mag、Slot为维度(Mag个数是处理器个数,Slot是16个)的二维数组形式,放在nanozone_t的meta_data字段中。

当App通过free()释放内存时:malloc库会检查指针地址,如果没有问题,则以链表的形式将这些区块按大小存储起来。这些链表的头部放在meta_data数组中对应的[mag][slot]元素中。

其实从缓存获取空余内存和释放内存时都会对指向这篇内存区域的指针进行检查,如果有类似地址不对齐、未释放/多次释放、所属地址与预期的mag、slot不匹配等情况都会以报错结束。

下图是我根据个人理解梳理出来的一个关系图,图中标出了nanozone_t、meta_data_t等相关结构的关键字段画了出来(OSX)。

screenshot

除了分配和释放,系统内存吃紧时,nano zone需将cache的内存区块还给系统,这主要是通过对各个slot对应的meta data上挂着的空闲链表上内存区块回收来完成。

3. scalable zone上内存分配简要分析

对于超出nano大小范围或者不支持nano分配的,直接会在scalable zone(下文简称szone)上分配内存。由于szone上的内存分配比起nano分配要较为复杂,细节繁多,下面仅作简要介绍,感兴趣的同学可以直接阅读源码。

在szone上分配的内存包括tiny、small和large三大类,其中tiny和small的分配、释放过程大致相同,large类型有自己的方式管理。

而tiny、small的方式也依然遵循nano分配中的原则,新内存从系统申请并分配,free后按照大小以特定的形式缓存起来,供后续分配使用。这里的分配在region上进行,region和nano malloc里的band概念极为相似,但不同的是地址空间未必连续,而且每个region都有自己的位图等描述信息。和nano,一样每个cpu有一个magazine,除此之外还分配了一个index为-1的magazine作为后备之用。

下面是一个简图。

screenshot

以tiny的情况为例,分配时:

  • 确定当前线程所在处理器的magazine index,找到对应的magazine结构。
  • 优先查看上次最后释放的区块是否和此次请求的大小刚好相等(都是对齐之后的slot大小),如果是则直接返回。
  • 如果不是,则查找free list中当前请求大小区块的空闲缓存列表,如果有返回,并整理列表。
  • 如果没有,则在free list找比当前申请区块大的,而且最接近的缓存,如果有返回,并把剩余大小放到free list中另外的链表上。(这里需要注意的是,在一般情况下,free list分为64个槽,0-62上挂载区块的大小都是按16B为单位递增,63为所有更大的内存区块挂载的地方)
  • 上面几项都不行,就在最后一个region的尾部或者首部(如果支持内部ALSR)找空闲区域分配。
  • 如果还是不行,说明所有现有region都没空间可用了,那么从一个后备magazine中取出一个可用region,完整地拿过来放到当前magazine,再走一遍上面的步骤。
  • 如果这都不成,那只能向内核申请一块新的region区域,挂载到当前的magazine下并分配内存。
  • 要是再不行就没招了,系统也给不到内存,就报错返回。

free时:

  • 检查指针指向地址是否有问题。
  • 如果last free指针上没有挂载内存区块,则放到last free上就OK了。
  • 如果有last free,置换内存,并把last free原有内存区块挂载到free list上(在挂载的free list前,会先根据region位图检查前后区块是否能合并成更大区块,如果能会合并成一个)。
  • 合并后所在的region如果空闲字节超过一定条件,则将把此region放到后备的magazine中(-1)。
  • 如果整个region都是空的,则直接还给系统内核,一了百了。

而large的情况,malloc以页为单位申请和分配内存,不区分magazine,szone统一维护一个hash table管理已申请的内存。而且由于内存区域都比较庞大,只缓存总量2G的区块,分为16个元素,每个最大为128M。large相关的结构相对简单,就不特意画图了。

综上,iOS内存管理和malloc库的源码整理到此。如果发现分析得有纰缪的地方或者描述不完整的请不吝指出,欢迎随时交流。

附上苹果malloc源码:
http://www.opensource.apple.com/source/libmalloc/libmalloc-53.1.1/src/

结语

如上便是本篇文章的主要技术内容,下面说点题外话。

本小站已经三年多了,此前主要还是以分享技术文章为主。由于目前以iOS为代表的移动客户端基本已进入技术发展的稳定期,再加上本人所在公司对于内部信息安全的要求逐步提高,小站对技术类文章的分享可能会在数量和频次上有所减少。

接下来会有本人在其它兴趣领域的一些分享和整理,所述分类可能会五花八门,也更加丰富多彩,另外技术类文章也会更多涉及node、前端等内容,敬请期待!也欢迎大家前来,继续支持小站发展,也希望能够对诸位读者有更多帮助,建立更多交流,谢谢。

发表在 iOS, 开发, 计算机技术 | 留下评论

说说iOS与内存管理(中)

接着上文简单整理下iOS常见内存问题及排查相关的工具和方法。

上篇日志是6月份写的,由于工作的内容比较丰富,这么久一直没能更新blog,有点小遗憾,这是到这是2015年8月最后一天,趁着8月还没过,赶快把这篇补上。

0. 内存工具

针对iOS开发,我们所能使用的内存排查工具选择其实并不算特别多。最主要的调试工具就是Instruments。然而,如果仔细探查细节,Instruments还是集成了很多不错的调试模板/Library的。

本文针对如下几类应用场景,对通用的调试方法做基本介绍:

  • 最基本最常用的内存问题场景——内存泄露、过度释放
  • malloc相关的堆内存分配问题排查相关工具
  • 其它内存工具

1. 内存泄露与过度释放

我们应该都知道,iOS开发过程中,使用Objective-C分配的堆内存都是通过引用计数来做保留和释放的。一块内存初始分配,引用计数为1,此后每新增一个强引用,引用计数增加1;释放正好相反,每一次release,引用计数减1,直到为0,对象所用内存被真正free掉,以被再次复用。然而,实际开发当中,总有一些原因导致引用计数无法按正常逻辑减少到0,或者减少到0之后仍然被调用release,前者是内存泄露,后者则是过度释放。

当内存泄露发生时,运行的App不会直接第发生明显问题,但废弃内存得不到回收,在长时间持续运行后,App进程会由于可用内存不断变低而被kill或带来其它隐患。

避免内存泄露,首要是有良好的代码习惯,避免循环引用、会用weak,其次可以通过Analyze来进行静态代码检查,以发现在语法上显而易见的内存泄露问题。但更多时候,内存泄露是运行时的问题,这时可用Instruments中的Allocation和Leaks来不断重复操作App,发现和定位内存泄露点。

当运行时发生显示内存泄露时,Leaks会在时间轴上标出红色指示线,同时在Instruments的下方会列出调用细节,结合系统提供的malloc历史,其中包含引用计数变化情况,以及调用栈可以很直接地找到泄露原因。

同时对于一些“隐式”的情况,需要反复操作,同时观察Allocation中只增不减,一直创建新对象而不释放老对象的情况。

过度释放,是对同一个对象释放了过多的次数,其实当引用计数降到0时,对象占用的内存已经被释放掉,此时指向原对象的指针就成了“悬垂指针”,如若再对其进行任何方法的调用,(原则上)都会直接crash(然而由于某些特殊的情况,不会马上crash)。

对于这种问题,可以直接使用Zombie,当过度释放发生时会立即停在发生问题的位置,同时结合内存分配释放历史和调用栈,可以发现问题。

至于上文提到的不会crash的原因,其实有很多,比如:

  • 对象内存释放时,所用内存并没有完全被擦除,仍有旧对象部分数据可用
  • 原内存位置被写入同类或同样结构的数据

2. malloc库提供的相关工具

上一段提到,对象释放时,所用内存并没有完全被擦除,仍有旧对象部分数据可用,如果不使用Zombie调试,App可能不会直接crash。对付这种情况,其实很简单,可以在对象内存释放时写入无意义数据,如0×55,0xaa等,而系统已经帮我们做了这个工具,那就是Scribble,在Xcode的Edit Scheme里,Diagnostics Tab下勾选Enable Scribble。

Scribble其实是malloc库(libsystem_malloc.dylib)自身提供的调试方案,除了Scribble,malloc还提供了很多其它的调试工具/方案,在Diagnostics Tab下你应该都看到了。其实,malloc可用的工具还不止这些,通过环境变量至少还可以添加如下调试参数:

  • MallocLogFile
  • MallocGuardEdges
  • MallocDoNotProtectPrelude
  • MallocDoNotProtectPostlude
  • StackLogging
  • StackLoggingNoCompact
  • MallocCorruptionAbort
  • MallocNanoZone
  • MallocCheckHeap

其中,有几个参数是和记录分配历史的日志有关的。

除此之外,Xcode里还提供了Guard malloc,这个是等同于默认malloc库功能的另外一个调试库,为了定位大内存越界访问问题,不过只能在模拟器上使用(个人分析是因为真机根本承受不起保护页内存消耗)。

对以上参数的实现细节感兴趣的,可以参看苹果开放出来的malloc源码。

3. 其它

下面我还想对Instruments里的三样东西做简要介绍:

  • Allocation
  • Activity Monitor
  • VM Tracker

Allocation针对堆内存及匿名映射的情况提供了详细的数据,包括某类对象有多少个,对象的具体地址,占用多大内存等。通过Allocation,可以对我们开发中实际接触到的内存有非常全面的把握。

Activity Monitor则从系统的层面,对主要进程的CPU、内存、网络等数据做分析。从内存方面看,我们可以知道占用系统内存最大的5个App,它提供的数据包含了实际物理内存和虚拟内存部分。

VM Tracker则可以告诉我们哪些部分是Dirty的,Dirty的数据系统不会直接清理掉,因为这些数据是被写过的,无法通过外部存储直接生成,只能维持在内存当中。此外,通过VM Tracker我们还可以看到Region Map,看到进程地址空间各部分的映射情况。

除了上面提到的这些,对于诡异的内存问题,我们可以对Xcode7中的Address Sanitizer期待和试用下,也许它真得能帮我们解决好多问题!

发表在 iOS, 开发, 计算机技术 | 标签为 , , , , , , | 4 条评论

说说iOS与内存管理(上)

说起内存管理,看似老生常谈,而真正掌握内存管理的核心其实并不简单。ARC/MRR以及“谁分配谁就负责释放”这种基本原则是很重要的,但不是本文要讨论的重点。之前本人还没在小站发过相关的文章,本篇文章中,我本人是想结合实际开发和调试中遇到的一些细节问题,来谈谈iOS的内存管理内在机制和调试方法

上一篇文章已经是4月份的了,时间飞快又过去了好久,小站5月份没有文章更新,罪过罪过。iOS开发当中的内存管理,可深可浅,一般应用程序开发过程当中可能并不需要关注太多,如果不是最近的调试,也许就不会有这么多心得来整理此文。

关于内存,我准备分为内存管理的基本原则、原理和调试方法、实际问题几部分整理。那么接下来我就和大家一起复习和稍微深入一下iOS的内存管理的原理和原则。

0. 概述

内存,简单来说就是内部存储,复杂来说要从冯·诺依曼计算机结构说起。冯·诺依曼结构,也称做普林斯顿结构,目前和哈佛结构相对,指出了计算机由运算器、控制器、存储器、输入和输出设备几大部件组成。如今我们个人用的机器估计都是这个套路,而且运算器和控制器都合在一起,就是CPU,中央处理器。那么内存就是CPU能直接读写访问数据的地方(寄存器是在CPU内的,不算哈),有些朋友说谁谁谁的iPhone内存16G、64G,我只能说这个理解方法仅限于存储部件放在手机里(内)了,严格来讲这算“外存”,我们要讨论的不是这个。

冯·诺依曼结构还说了,内存是用来存啥的呢?指令+数据!(哈佛的恐怕就不一样了)对于我们开发者来说,指令基本就是代码逻辑,至于数据么变量常量肯定都算是的了。

内存有多大?不大,现今主流的个人机器也就几G的样子。iPhone?  统统1G。

我们操作系统都是运行在内存之上的,1G好像不算大,所以为了支持多进程,也为了支持大程序,抽象的虚拟存储的概念诞生了。

简要的概念先陈述到这,下面详细说。哦,对了,ARC和MRR我还是得提一下,这个要是真不知道还真的自己先去了解一下去。

1. 通用内存基本原理

说iOS的内存,有必要先看看一般的计算机都是怎么干的,iPhone也是计算机,通用的道理一样要遵循。这里提两方面:虚存的概念,内存内容的大致分布。

虚拟存储系统。刚刚提到了,物理内存就那么大点,但是还要跑多个程序,还要接受消耗很大内存的程序,这怎么办?凉拌。搞计算机的人都是很聪明的,在操作系统层面做了物理地址和逻辑地址之间的映射转换,当然处理器硬件上也做了支持。一个程序在运行时,实际要用到的指令和数据都是很有限的,不可能从头到尾同时用。那么对于一个程序来说,假装自己有非常大的空间,实际上只要有条理的把暂时要用到的部分放进物理内存供CPU访问就好,这样第二个问题解决了。那既然每个程序(进程)只用一小块,那整个物理内存就可以分给多个程序(进程)用了,第一个问题也迎刃而解。当然,这样做的前提是,数据和指令的动态进出,用完了的暂时不用的踢出内存,需要用的及时加载进来。这个具体的实现方式就多种多样了,很多实现方式是在外存中开了个交换区供换入换出,但iOS可略有不同。

内存的大致分布。不久以前,我发了一篇文章整理了Mach-O文件的格式分析,里面很复杂地放了好多东西,包括我们Build打包时的代码和数据。而Mach-O文件正是我们开发内容的一个静态展现形式,要想在运行的时候看样子,就得看这文件里包含的东西是怎么放进内存的。Objective-C是基于C的,不放看下C程序进程的内存分布:

一个运行时进程的典型内存分布(iOS大同小异)

一个运行时进程的典型内存分布(iOS大同小异)

最简单来说分为两大部分:指令+数据。再细分一点,五部分:代码(指令),初始化数据区,未初始化数据区,堆,栈。

  • 代码(指令,text)就不用说了,最静态的,就是只读的东西;
  • 初始化数据,简单理解就是有初始值的变量、常量;
  • 未初始化数据,只声明未给值的变量,运行前统统为0,之所以单独分出来,估计是性能考虑,因为这些东西都是0,没必要放在程序包里,也不用copy;
  • 栈,程序运行记录,每个线程,也就是每个执行序列各有一个(看crash log最容易理解),都是编译的时候能确定好的,还有一个特点就是这里面的数据可以不用指针,也不会丢;
  • 堆,最灵活的内存区,用途多多,动态分配和释放,编译时不能提前确定,我们的Objective-C对象都是这么来的,都存在这里,通常堆中的对象都是以指针来访问的,指针从线程栈中来,但不独属于某个线程,堆也是对复杂的运行时处理的基础支持,还有就是ARC还是MRR、“谁分配谁释放”说的都是堆上对象的管理;

其实,这个内存中的布局方式大部分操作系统中的大部分进程都是类似的。Objective-C的程序包对运行时有着复杂的支持和内容划分,但也都是在这个大的框架下进行的。

2. iOS的内存管理

其实,iOS的内存管理和其它操作系统大同小异。这里按照苹果文档所述,重点对堆内存分配整理下。

iOS的内存管理分为几个层面,从系统到libmalloc,ARC环境下,编译器也会帮助开发者做“力所能及”的优化处理。

首先,iOS和其它系统一样,操作系统内核会做虚拟存储到物理内存的映射管理,并做内存分页,每页4K。多个页构成一个内存区块统一管理,负责管理的对象是VM object,其中包含了pager、size、resident pages等诸多属性。所有的内存分配最终都将交由系统来处理(比如vm_allocate/mach_vm_allocate)。

而开发中,在系统内核的基础上,iOS使用libmalloc。不管是Objective-C的[NSObject alloc],还是C代码的对内存分配,重任都会落到malloc库上,释放也是如此,最终都将使用malloc库中的free()。malloc库中有很多malloc的同族函数可以动态分配内存。malloc库中定义了zone的概念,并实现了不同的zone(如nano zone和scalable zone),并根据内存需求的大小使用不通算法对nano、tiny、small、large量级的内存进行分配和释放管理。默认情况,在第一次调用malloc时,系统会生成一个default zone,后续的默认分配在此进行。比如,malloc_zone_xxx()函数最终都对特定的zone进行分配操作,执行zone->xxx()。每个zone都以链表的形式对已分配过的内存做cache处理,避免频繁对内核系统发起申请。malloc的内部实现都是开源的,感兴趣的可以去了解去看。

最后强调一下iOS特别需要注意的点:

当前的主流iPhone实际物理内存都不超过1G,可以说不算大。不过和Android机比起来,我不得不为苹果的设计称赞,1G空间利用得如此高效,性能不差,也控制了发热。

那么在这仅有的1G内存中,iOS的操作系统更是抛弃了不必要的复杂——系统层面不支持App内存页换出。当内存吃紧时,对于可以重新载入的只读数据来说,直接清理掉,而对于可写的数据,只能通过App自己去管理维护。内存紧张时,iOS会向App发起memory warning,不配合释放足够内存者,杀!

关于Instruments及内存调试,会在后续文章详细整理出来。

3. 其它

基本的原理就简要整理到此,如下是一些参考:

Memory Layout of C Programs

Anatomyof a program in memory

What and where are the stack and heap?

Memory Usage Performance Guidelines

A look at how malloc works on the Mac

Malloc库源码

发表在 iOS, 开发, 计算机技术 | 标签为 , , , | 5 条评论

定制实现NSURLProtocol

最近周末出门比较多,好久没写技术日志了,最终blog边栏上还真是少了2015.3,赶忙趁着清明节放假补上一篇。这一篇我们就来看看我们自己可以定制实现的NSURLProtocol

对于一个移动互联网时代的客户端App来说,网络通信请求基本上是不可缺少的。做过很久iOS开发的朋友们可能都记得ASIHTTPRequest,这是一个经典的http网络请求开源工具。不过遗憾的是,这个开源的请求工具目前已经停止维护。而iOS SDK自身在Foundation里也提供了一整套的URL Loading API。即使是广泛被使用的AFNetworking也是基于这套API。

说起Foundation里构建的的URL Loading System,相信大家对NSURL、NSURLRequest、NSURLConnection都不陌生,iOS7之后又增加了NSURLSession。而我们今天要提到的主角则是潜藏在这些之下的一个非常强大的类,NSURLProtocol。

0. 初识NSURLProtocol

NSURLProtocol也是随着最初的URL Loading System推出的,自iOS SDK的2.0版本开始就出现了。然而,由于苹果官方文档中对其介绍的内容非常少非常简短,并没有受到官方关注。但NSURLProtocol可以被当做是整个URL Loading System中功能最强大的API。

NSURLProtocol本身是个“抽象”类(Objective-C语法上没有真正抽象类的概念),除此之外它也是对互联网通信协议(真正的Protocol)的抽象。也就是说,如果实现了它的子类,你就可以写一套和HTTP类似的协议。

具体一些,在iOS App中使用NSURLProtocol:

  • 如果需要,可以对html页面中的图片做本地化处理
  • Mock假的response
  • 对请求头做规范化处理
  • 在上层应用不感知情况下,实现一套代理机制
  • 过滤请求、响应中敏感信息
  • 对已有协议做改进、补充处理

以上这些,说简单点,就是对上层的URLRequest请求做拦截,并根据自己的需求场景做定制化响应处理。

1. 初始化请求

不是所有请求都要经过定制的协议来走,我们自己定制实现的Protocol可以根据不同条件筛选请求,同时对请求做更完整的包装定义。

在NSURLProtocol.h的interface定义中,我们可以看到比较靠上的两个方法分别是:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;

使用canInitWithRequest:我们可以筛选出可以使用当前协议的request,其它的忽略掉,直接走其它协议或者默认实现。

而canonicalRequestForRequest:则是给了我们包装实现,给出完整版request的机会。

2. 属性设置

很多情况下,具体的通信实现最终可能还是要采用系统自身提供的默认实现,但却又需要我们做过滤包装。通常的办法是我们对request按需求场景做特定处理,最后返给系统,走默认实现。

但这样这个request很有可能会再次进入这个protocol,所以通常在对request进行完第一次处理后,打个标,下次进入canInitWithRequest时直接过滤掉。

所谓的打标就会涉及到对request的属性操作,需要如下方法:

+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;

大家可能会想,这样的方法不应该是NSURLRequest类的么?或者是Category?

是的,看起来是可以这样的。但大家注意到这是NSURLProtocol的类(+)方法,也就是说这里的API是以工具方法提供的。

3. 通信协议内容实现和与client交互

下面说实现协议最重要的部分,做一个Protocol总要有实现的。我们的定制实现就要override如下两个方法,并且给出具体的通信逻辑实现。比如你想实现SPDY,那就自己把协议的实现逻辑给出,或者使用三方库协助实现。

- (void)startLoading;
- (void)stopLoading;

在实现这两个方法时,通过URL Loading System和上层打交道是一定要有的。这就是NSURLProtocol.h中的另一部分内容——NSURLProtocolClient。NSURLProtocolClient是一个Objective-C的protocol,定义了一些方法。我们在实现startLoading/stopLoading时,我们只要在有必要和系统交互时拿到self.client对象调用就好,client会由上下文实例化好供我们使用。

4. 协议的注册

上面说了这么多,如果没有这一步都是白扯。最后的一步就是,把实现的协议注册到系统中。这样才算完美,一切都OK了。

按照苹果的Guide文档所写,在处理请求时,会按照NSURLProtocol注册的反序筛选。

5. 其它

以上,NSURLProtocol介绍完了。看,是不是Loading System中最强大的部分?

虽然目前Google已经声称支持HTTP2.0,放弃SPDY,但SPDY依然被广泛使用。SPDY的实现就可以通过NSURLProtocol来简单实现。当然拦截WebView的Request之类的,也可以找NSURLProtocol。

发表在 iOS, iOS开发基础, 开发, 计算机技术 | 标签为 , , , | 一条评论

Crash log符号化与调试信息

这篇文章主要整理了crash log的符号化解析和调试信息与配置相关的一些内容。

对于做移动App开发的来说,质量和体验都是很重要的。一个客户端应用如果经常“闪退”,是产品质量很差的一个体现,用户体验就更不用提了。所以开发一个优秀的App,首先是保证自身的技术质量,尽量杜绝“闪退”,也就是“Crash”。但客户端上线后,偶尔出现一个隐藏很深的bug也在所难免。我们所能做的就是尽可能的收集问题相关的信息,争取在将来的新版本中解决和改进。

0. Crash

一个App启动之后,用着用着就突然被iOS系统关闭,或者干脆就起不来,在打开的一瞬间关闭,这就是Crash,俗称“闪退”“崩溃”。

iOS上的App闪退有各种各样的原因,手机过热、响应超时、内存过低都是有可能的crash原因。但更多情况下是App程序自身的运行逻辑存在问题、缺陷。比如调用用了Objective-C对象根本不支持的方法(发送消息),非法内存访问,数组越界,参数不符合要求等。

这些问题在调试阶段,我们都可以很容易的通过断点和console中提供的信息快速定位并解决。

但对于已发布的App,如果想重现并利用上述办法来解决,恐怕会比较费时费事。

最有帮助最直接的办法就是根据出现问题时的闪退日志,分析和判断crash的原因,快速准确的定位和解决。

1. Crash log

在iOS上运行的App出现crash的时候,通常会生成一个crash log,记载问题发生时的具体状况。开发者可以在iTunes Connect(相当于App Store后台)中特定App下找到收集上来的crash log。不过客户端用户可以选择不发送诊断信息,这样收集上来的信息就不一定是全面的。

不过开发者可以对exception和signal设置自定义的handler做额外处理,以收集现场信息。现在也有很多第三方的工具很流行,比如Crashlytics,国内的友盟等。

闪退日志里面包含了Crash发生的App、运行软硬件环境、发生时间、错误类型、方法调用异常栈、各线程状态、寄存器和内存信息。

而其中对我们开发人员来说意义最为重大的,可能就是异常线程的调用栈,例如:

Last Exception Backtrace:
0   CoreFoundation                	0x18517e950 __exceptionPreprocess + 132
1   libobjc.A.dylib               	0x1916841fc objc_exception_throw + 60
2   CoreFoundation                	0x185085910 -[__NSDictionaryM setObject:forKey:] + 900
3   CrashDebugInfoTest            	0x1000c2b90 0x1000bc000 + 27536
4   CrashDebugInfoTest            	0x1000c28dc 0x1000bc000 + 26844
5   UIKit                         	0x1881bc55c -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 316
6   UIKit                         	0x1881bbf08 -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1564
7   UIKit                         	0x1881b59ec -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 772
8   UIKit                         	0x1881498cc -[UIApplication handleEvent:withNewEvent:] + 3316
9   UIKit                         	0x188148ad0 -[UIApplication sendEvent:] + 104
10  UIKit                         	0x1881b5044 _UIApplicationHandleEvent + 672
11  GraphicsServices              	0x18ad63504 _PurpleEventCallback + 676
12  GraphicsServices              	0x18ad63030 PurpleEventCallback + 48
13  CoreFoundation                	0x18513e890 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 56
14  CoreFoundation                	0x18513e7f0 __CFRunLoopDoSource1 + 444
15  CoreFoundation                	0x18513ca14 __CFRunLoopRun + 1620
16  CoreFoundation                	0x18507d6d0 CFRunLoopRunSpecific + 452
17  UIKit                         	0x1881b41c8 -[UIApplication _run] + 784
18  UIKit                         	0x1881aefdc UIApplicationMain + 1156
19  CrashDebugInfoTest            	0x1000c2c5c 0x1000bc000 + 27740
20  libdyld.dylib                 	0x191c77aa0 start + 4

其中从第二列来看,很多是开发库中的调用,而关键在于其间我们自己的App方法调用。可惜有些时候,这关键的信息竟然全是16进制的数据,我们很难看懂。比如:

3 CrashDebugInfoTest 0x1000c2b90 0x1000bc000 + 27536

那么要从十六进制的地址码,得到我们代码中对应的方法调用,就需要结合调试信息对crash log进行符号化。

2. 符号化的各种方法

符号化的方法多种多样,从网上社区论坛和个人经验看来,至少有如下办法:

  • 使用开发工具库中自带的symbolicatecrash
  • 使用atos
  • 使用dwarfdump

更有牛人,自己写了个复杂的脚本来解决这个问题。下面我介绍我常使用的两种方法,一个是利用atos,一个是充分利用Xcode自带的工具。其它的大家都可以到网上参看相关文章,一搜一大筐。

atos,就是address to symbol,把地址翻译成符号。上面那段我提到了,要想把十六进制的地址翻译为符号,需要调试信息。最好用的调试信息就是我们在每次给App打包时生成的dSYM文件。而atos最好用的方式就是:

atos -o XXX.app.dSYM/Contents/Resources/DWARF/XXX -l address0 targetAddress

其中:

  • XXX是AppName
  • address0是当前进程在内存中加载的起始地址,至于为什么需要这个,那就有必要去了解下ASLR
  • targetAddress就是你想要符号化的地址了,比如0x1000c2b90

除了atos外,我想介绍的另一个办法就是使用Xcode自带的crash log分析工具,在老版本的Xcode中是在Organizer里,在新版本里是在Devices中。

有的朋友可能会说,那里面显示的可还是十六进制的地址啊!那是因为它“没看到”App和dSYM文件啊。那怎么办?简单:

把App和dSYM放在一个目录中,并用mdimport把目录加入到Spotlight的索引中即可。

怎么样,这招是不是更快更好用?symbolicatecrash神马的就不需要了吧!

3. 针对framework静态库的crash定位和调试选项设置

之前本人曾经以framework(iOS Universal Framework)的方式开发了好多SDK供别人用。可当使用了framework库的App闪退了的时候,即使是SDK中的逻辑问题,异常栈中显示的也是App的名字。

更重要的是,默认情况下,异常栈的最右一列根本没法符号化。

这是因为framework实际上是一种静态库,在Build App时,它已经完全“融入”了,静态链接到App产物中。而在framework生成的时候,调试信息已经被抽取掉了。

我们打开SDK的工程文件,在Build Settings里搜索Strip,会发现有好几个选项:

  • Strip Debug Symbol During Copy
  • Strip Linked Product
  • Strip Style
  • Use Separate Strip

对于这个问题,我们只要在Strip Linked Product一项中选择No就行了。这样在Build出的SDK framework中,包的体积会变大,因为它容纳了本要去除掉的调试信息。

按我在之前的Blog的办法,我们看看在Mach-O文件中多了什么:

Debug Info

Debug Info

是的,正是DWARF格式的数据。DWARF是一种通用的调试信息格式,可以认为是Debugging With Attributed Records Format的缩写。感兴趣的可以前往:

http://www.dwarfstd.org

这样,关于Crash问题的解决方案和原理我就解释清楚了,欢迎大家拍砖!

发表在 iOS, iOS开发基础, 开发, 计算机技术 | 标签为 , , , , , | 6 条评论
第 1 页,共 23 页12345...91215...最旧 »