记录开发中碰到的一些问题,以防日后继续踩坑
2019.06
ARC 自动插入引用计数引用计数
- ARC插入retain、release、autoRelease是在编译时进行自动插入的。
- setter方法中,会先对传入的对象进行retain(+1),再对之前的对象进行release(-1),这个顺序很重要,防止传入的对象与之前是同一个对象,导致对象引用计数先减为0,直接释放掉,无法再获取到。
- autoRelease是在下一次runloop时,才去release。
- 自动插入时,编译器有进行优化,首先会删除成对的retain、release。
- 当存在函数返回值时,会给返回值添加autoRelease,以保证能够在拿到该返回值后,返回值才被释放。
// MRC 版本
- (void)sleep {
NSString *name = [self getName];
_name = [name retain];
}
- (NSString *)getName {
NSString *name = @"1111";
return [name autoRelease];
}
- 这边的autoRelease和retain看起来可以约去,但是为了考虑向后兼容性(MRC),这边不能直接删去。因此ARC在运行时,会对这部分代码进行优化,使用
objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
这两个方法(具体见下边代码)。
// ARC 版本
- (void)sleep {
_name = [self getName];
}
- (NSString *)getName {
NSString *name = @"1111";
return name;
}
// 运行时
- (void)sleep {
NSString *name = [self getName];
_name = objc_retainAutoreleasedReturnValue(name); // flag(status) == true => 不进行retain => flag(0)
}
- (NSString *)getName {
NSString *name = @"1111";
objc_autoreleaseReturnValue(name); // 通过编译器获取之后是否调用retain => flag(status)
}
2019.05
KVO中的注意点
- 观察者强引用对象,但对象(底层实现)没有强引用观察者。不会造成循环引用。
- 观察者有可能会被释放。但对象可能还是会给观察者发消息,造成奔溃。
因此在观察者dealloc方法中需要注销kvo。
2019.04
po的完整版
expr 变量|表达式 //显示变量或者表达式的值。
expr -f h -- 变量|表达式 //以16进制格式显示变量或表达式的内容
expr -f b -- 变量|表达式 //以二进制格式显示变量或者表达式的内容。
expr -o -- oc对象 //等价于po oc对象
expr -P 3 -- oc对象 //上面命令的加强版本,他还会显示出对象内数据成员的结构,具体的P后面的数字就是你要想显示的层次。
expr my_struct->a = my_array[3] //给my_struct的a成员赋值。
expr (char*)_cmd //显示某个oc方法的方法名。
expr (IMP)[self methodForSelector:_cmd] //执行某个方法调用.
真机使用的架构是arm64指令集,模拟器使用的是x64指令集。可进入汇编调试模式调试。
2019.3
微信小程序camera组件录像30s偶发超时问题
在初始化camera
上下文时,若要多次进行录像操作,必须制定id
(文档中没有相关说明)。
不指定id
,在进行多次录制时,会初始化多个上下文,stop
操作会造成上一次的录制未停止,导致录制超时。
wx.createCameraContext('myCamera')
保证当前是主线程的正确姿势
之前在开发中刷新UI时回到主线程一般都是使用:
if ([NSThread isMainThread]) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
但是在一些场合需要判断的是如果在主线程执行非主队列调度的API,而这个API需要检查是否是在主队列上调度,那么将会出现问题。也就是说需要判断的是主队列而不是主线程。
更安全的方法:
dispatch_main_async_safe(block) {
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);\
}
}
解析:
dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)
:每个queue
都有一个唯一的label
,这是获取当前队列的label
。
dispatch_queue_get_label(dispatch_get_main_queue()
:获取主队列的label
。
strcmp(A, B)== 0
: 判断当前队列是否是主队列。
这个方法将判断是否在主线程执行改为了是否在主队列执行,因为主队列无论是同步还是异步,都不会开辟新的线程,即都是在主线程执行。=> 主队列调度的任务一定在主线程执行,而主线程执行的任务则不一定是在主队列调度
NSMapTable 的使用
在翻看SDWebImage
源码时,无意中发现原本的imageCache
从NSCache
为了NSMapTable
。正好复习一下NSMapTable
。
NSMapTable
一般都是和NSDictionary
一起讲解的。NSDictionary
一般适用于key
=> object
,且key
和object
都只能是OC
对象,由于NSDictionary
是通过key
来索引object
的,key
一旦被修改就无法找到object
了,所以NSDictionary
始终会复制key
到自己的私有空间(key
必须支持 NSCopying
协议)。因此一般使用简单高效的对象来作为key(例如NSString
、NSNumber
),以至于复制的时候不会对 CPU 和内存造成负担。
如果要做对象到对象的映射,我们可以使用NSMapTable
:
- (instancetype)initWithKeyPointerFunctions:(NSPointerFunctions *)keyFunctions valuePointerFunctions:(NSPointerFunctions *)valueFunctions capacity:(NSUInteger)initialCapacity;
NSMapTableStrongMemory
: 强引用NSMapTableWeakMemory
: 弱引用NSMapTableObjectPointerPersonality
:将对象添加到集合中时是否调用对象上的isEqualTo:
和hash
方法NSMapTableCopyIn
: 复制一份,可以设置key
为该属性 =>NSDictionary
图片解压缩方案
加载图片主要有以下三种方法:
- (nullable UIImage *)imageNamed:(NSString *)name;//同时会加载图片到内存中
- (nullable UIImage *)imageWithContentsOfFile:(NSString *)path;//不会缓存到内存中,适合较大的不常用的图片
- (nullable UIImage *)imageWithData:(NSData *)data;//不缓存,适合于加载网络图片
iOS加载图片流程:
当使用imageWithContentsOfFile
从地址中获取图片时,图片并没有解压缩,此时的图片是无法显示的,在渲染到屏幕之前,必须先要得到图片的原始像素数据(位图)。
将生成的image赋值给UIImageView,此时CoreAnimation(CA)
的事务(CATrasaction)
会捕获到UIImageView
图层树的变化,在主线程的下一个runloop
到来时,CA
会提交这个事务:
- 分配内存缓冲区用于管理文件
IO
和解压缩操作。 - 从磁盘中读取图片数据到内存中。
- 在主线程中将图片数据解码成未压缩的位图形式(耗时操作)
- 最后将位图渲染到
UIImageView
的图层上
在以上操作中将图片解码成位图是一个在主线程中的耗时操作,因此很有必要将它放到子线程中提前解码成位图,再在主线程中渲染图片。而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。
SDWebImage图片解压缩
SDWebImage在从磁盘获取图片数据时,会根据图片是否包含Alpha通道以及是否图片存储时是否被缩小(iOS缓存操作60M的图片时,会缩小图片再进行缓存SDImageCacheScaleDownLargeImages
)进行解压缩。
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
if (![[self class] shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{//自动释放池,释放变量
CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();//色域
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;//kCGBitmapByteOrder32Little iPhone是小端模式,数据以32位位单位
//位图布局信息 有Alpha通道时,将A通道乘以RGB,无Alpha通道时,跳过
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,//data系统会自动分配和是否内存
width,//位图宽高,即像素数量
height,
kBitsPerComponent,//像素的每个颜色分量使用的 bit 数
0,//位图的每一行使用的字节数=》大小至少为4 * width,指定0时系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化。优化过程不了解。。。这么用就好
colorspaceRef,//色域 使用RGB
bitmapInfo);//位图布局信息
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
2018
在数据库中递归子节点并建立表关联实现快速检索
我们在开发过程中遇到大数据检索时,如果直接对多个表数据分别进行检索将很可能会导致很严重的性能问题,导致页面卡顿。
我们可以使用多张表关联的方式来优化该问题:在云办公组织组件中,由于组织树层次深、结构复杂,要计算每个初始节点包含的总人数,如果直接分别计算该节点的每个子节点的人数和,则需要执行多次的查询,势必会出现严重的性能问题。可以用表连接的方式来优化,SQLite 语句如下:
membersCount = [db intForQuery:[NSString stringWithFormat:
@"SELECT count(*) from tbl_persons\
INNER JOIN (\
WITH RECURSIVE\
cte(depId, deptName, superDeptCode) AS(\
SELECT depId,deptName,superDeptCode FROM tbl_deptments WHERE depId=%zd\
UNION ALL\
SELECT a.depId, a.deptName, a.superDeptCode FROM tbl_deptments AS a INNER JOIN\
cte ON a.superDeptCode=cte.depId\
)\
SELECT * FROM cte\
) AS a ON tbl_persons.deptCode = a.depId\
where lState=%zd and companyId=%zd",
deptId, state, [[CLOfficeUser shareInstance].companyId integerValue]]];
其中:
WITH RECURSIVE\
cte(depId, deptName, superDeptCode) AS(\
SELECT depId,deptName,superDeptCode FROM tbl_deptments WHERE depId=%zd\
UNION ALL\
SELECT a.depId, a.deptName, a.superDeptCode FROM tbl_deptments AS a INNER JOIN\
cte ON a.superDeptCode=cte.depId\
)\
SELECT * FROM cte\
表示对部门表tbl_deptments
进行递归查询superDeptCode
节点下的所有子节点depId
。
INNER JOIN
表示将关联两张表进行查询。
使用宏判断输入的路径参数是否合法,并且给出代码提示
在使用 KVO 时,通常要指定一个观察路径keyPath
,如果该路径字符串拼错或是不存在,编译时并不会报错,只有等到运行时才会发现问题。Facebook开源的FBKVOController
通过宏定义来解决了这个问题:
#define keypath(self, path) \
(((void)(NO && ((void)self.path, NO)), strchr(# PATH, '.') + 1))
其中(((void)(NO && ((void)self.path, NO)), strchr(# PATH, '.') + 1))
是一个逗号表达式,只会计算逗号后面的部分strchr(# PATH, '.') + 1
,由于前面部分没有使用,编译器会报警告,因此加上(void)
强制转换类型。
(NO && ((void)self.path, NO))
NO与运算,直接跳过忽略第一个值,去计算表达式后面的值,且这里使用(void)self.path
对path
进行点运算,只要作为表达式的一部分,Xcode会自动提示。
如果传入的path
不是self
的属性,那么self.path
就不是一个合法的表达式,所以自然编译就不会通过了。
使用:
@keypath(self.personA);//返回@"personA"
keypath(self, path)
返回的是C字符串,前面加@可转为OC对象NSString
。
NS_ENUM 与 NS_OPTIONS
使用 NS_ENUM
与 NS_OPTIONS
宏来定义枚举类型可以指明底层的数据类型。
使用:
typedef NS_OPTIONS(NSUInteger, UISwipeGestureRecognizerDirection) {
UISwipeGestureRecognizerDirectionNone = 0, // 值为0
UISwipeGestureRecognizerDirectionRight = 1 << 0, // 值为2的0次方 即左移一位
UISwipeGestureRecognizerDirectionLeft = 1 << 1, // 值为2的1次方
UISwipeGestureRecognizerDirectionUp = 1 << 2, // 值为2的2次方
UISwipeGestureRecognizerDirectionDown = 1 << 3 // 值为2的3次方
};
typedef NS_ENUM(NSInteger, NSWritingDirection) {
NSWritingDirectionNatural = 0, // 值为0
NSWritingDirectionLeftToRight, // 值为1
NSWritingDirectionRightToLeft // 值为2
};
区别:
NS_ENUM
枚举项的值为NSInteger
,NS_OPTIONS
枚举项的值为NSUInteger
NS_ENUM
定义通用枚举(默认自增步长为 1),NS_OPTIONS
定义位移枚举(可以同时存在多个)NS_OPTIONS
的枚举项的值一般使用位运算符定义:1 << 0,1 << 1
,而NS_ENUM
的值一般直接使用数值:0 1 2
在使用或运算操作两个枚举值时,C++ 默认为运算结果的数据类型是枚举的底层数据类型(NSUInteger)
,且 C++ 不允许它隐式转换为枚举类型本身,所以 C++ 模式下定义了 NS_OPTIONS
宏以保证不会出现类型转换。因此如果OC不按 C++ 模式编译,两者展开方式相同。如果按照 C++ 模式编译, 则存在区别。
结论:只要枚举值需要按位或(2个及以上枚举值可多个存在)就使用 NS_OPTIONS
,否则使用 NS_ENUM
。
UIControl拖拽移出控件问题
我们要自定义控件的互动效果时需要使用 UIControl
,但是在使用时发现,离开控件时并不会触发 UIControlEventTouchDragExit
和 UIControlEventTouchDragOutside
,实践后发现,离开控件边缘100px时,才会触发该事件。
我们可以使用以下代码来取消这个效果。
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
// 设置新的边界
CGFloat boundsExtension = 0.0f;
CGRect outerBounds = CGRectInset(self.bounds, -1 * boundsExtension, -1 * boundsExtension);
// 判断触摸位置
BOOL touchOutside = !CGRectContainsPoint(outerBounds, [touch locationInView:self]);
if(touchOutside)
{
// 判断是UIControlEventTouchDragExit/UIControlEventTouchDragOutside
BOOL previousTouchInside = CGRectContainsPoint(outerBounds, [touch previousLocationInView:self]);
if(previousTouchInside)
{
[self sendActionsForControlEvents:UIControlEventTouchDragExit];
}
else
{
[self sendActionsForControlEvents:UIControlEventTouchDragOutside];
}
} else {
[self sendActionsForControlEvents:UIControlEventTouchDown];
}
// 如果不是想要修改的control event,返回原操作
return [super continueTrackingWithTouch:touch withEvent:event];
}
判断两张UIImage全等
判断两张图片全像素相等,网络上提供的方法大多如下:
BOOL isImageEqual(UIImage *image1, UIImage *image2) {
if (image1 && image2) {
NSData *data1 = UIImagePNGRepresentation(image1);
NSData *data2 = UIImagePNGRepresentation(image2);
BOOL result = [data1 isEqual:data2];
return result;
}
return NO;
}
该方法主要实现思路:采用 UIImagePNGRepresentation
将 UIImage
转化 NSData
之后进行比较。因为:UIImage
显示的图片为 BMP 类型图片。它采用位映射存储格式,除了图像深度可选以外,不采用其他任何压缩,因此,BMP 图像所占用的空间很大。而 UIImagePNGRepresentation
会将 BMP 转化为 PNG ,其过程是是无损数据压缩的,但经过压缩转化后可能存在相同图片压缩结果不同的情况。
所以以上的判断方法存在三个问题:
- 没有判断两张图片来源是否相同,从资源文件中读出的同一张图片组成不同的
UIImage
对象,其指针是相同的,UIImage
只是引用,可能产生图片和自身比较的过程。 - 把
UIImage
转成NSData
的过程非常耗时。 - 同一张图片的压缩结果可能存在不同。
由于 UIImage
使用的图片已经是 BMP 类型的图片,所以只要获取到图片每个像素对应的存储数据进行对比,即可不经过压缩、对比结果绝对精确等。
具体代码如下:
BOOL isImageEqual(UIImage *image1, UIImage *image2) {
if (image1 == image2) {
return YES;
}
if (!CGSizeEqualToSize(image1.size, image2.size)) {
return NO;
}
if (image1 && image2) {
CGImageRef imageRef1 = [image1 CGImage];
//获取图片像素映射信息
CFDataRef data1 = CGDataProviderCopyData(CGImageGetDataProvider(imageRef1));
const unsigned char * buffer1 = CFDataGetBytePtr(data1);
CFIndex length1 = CFDataGetLength(data1);
CGImageRef imageRef2 = [image2 CGImage];
CFDataRef data2 = CGDataProviderCopyData(CGImageGetDataProvider(imageRef2));
const unsigned char * buffer2 = CFDataGetBytePtr(data2);
CFIndex length2 = CFDataGetLength(data2);
if (length1 != length2) {
return NO;
}
BOOL result = YES;
for (long i = 0; i
经过测试,最差情况下,性能是原方案的4-10倍。