LS's DevLog


  • 首页

  • 归档

从一次性能优化到 runloop 和 Allocations 的学习

发表于 2016-10-18   |     |   阅读次数

之前在项目中做过一次收集手机电池信息然后上传的需求,通过公开的api只能获取batteryState和batteryLevel这两个属性,很多其他的更详细的信息获取不到。当时引用了一个第三方的代码,大概实现是单开一个线程,在这个线程上通过kvo监听上面两个属性值的修改,在监听触发的时刻能劫持获取到底层传输的关于系统硬件的信息,这里面就能取出我们需要的关于电池的详细信息。

该实现见UIDeviceListener

遇到的问题

具体需求是应用启动时在一个合适的时间把这些信息收集起来传给后台,而且在应用的整个生命周期中只需要传这一次即可。我一开始的实现是做了一个单例Manager负责上面获取硬件信息的过程。

因为收集完成后就不再有用,理想的情况是这个线程在完成了自己的使命后自动退出,这个Manager实例也需要销毁以避免占用内存。但现实是残酷的,事实上我发现,在完成了信息的上传之后,那个专门命名的线程(我专门设置了name为XXDeviceListener)压根没有退出,每次调试查看Xcode左边的堆栈信息,在存活的线程列表中,XXDeviceListener都扎眼的躺在那里~,而在Instruments的Allocation工具里,也可发现这个Manager实例一直还占用着内存。所以需要优化。

线程与runloop

我们开启的线程使用kvo,这是个异步的过程,需要等待回调,而子线程默认不开启runloop,这样线程就会运行完代码后立即退出。因此我们代码的实现里是使用了一个runloop来保证线程不死。

The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

大概代码如下所示

- (instancetype)init{
    ...
    listenerThread = [[NSThread alloc] initWithTarget: self selector: @selector(listenerThreadMain) object: nil];
    listenerThread.name = @"XXDeviceListener";

    [listenerThread start];
}

- (void)listenerThreadMain{
    ...
    CFRunLoopRef mainLoop = CFRunLoopGetCurrent();
    CFRunLoopAddObserver(mainLoop, observer, kCFRunLoopCommonModes);
    [[NSRunLoop currentRunLoop] run];
}

注意上面的runloop进入方式是走run方法,这相当于走入while{}函数里面,是一个死循环,如果在上面的方法里run后面再加上一些代码,调试发现这些代码是一直不会被执行的。

现在我们的问题浮出水面了,为了异步操作的进行我们需要开runloop做线程保活,而又根据需求需要,我们只想让线程保活一段时间,这段时间之后我们希望runloop或者线程自动退出。
这就要说到runloop的启动方式了,搬出文档

There are several ways to start the run loop, including the following:
1.Unconditionally
2.With a set time limit
3.In a particular mode

Entering your run loop unconditionally is the simplest option, but it is also the least desirable. Running your run loop unconditionally puts the thread into a permanent loop, which gives you very little control over the run loop itself. You can add and remove input sources and timers, but the only way to stop the run loop is to kill it. There is also no way to run the run loop in a custom mode.

Instead of running a run loop unconditionally, it is better to run the run loop with a timeout value. When you use a timeout value, the run loop runs until an event arrives or the allotted time expires. If an event arrives, that event is dispatched to a handler for processing and then the run loop exits. Your code can then restart the run loop to handle the next event. If the allotted time expires instead, you can simply restart the run loop or use the time to do any needed housekeeping.

In addition to a timeout value, you can also run your run loop using a specific mode. Modes and timeout values are not mutually exclusive and can both be used when starting a run loop. Modes limit the types of sources that deliver events to the run loop.

分析文档。第一种无条件启动,这种方式最简单但最不推荐,因为结束 runloop 的唯一方式是 kill it,并且不能选择自定义的 runloop mode 启动它;第二种是设置超时,这种方式 is better ,处理完 event 、或者超时时间结束后,runloop 退出;第三种是设置 runloop 跑起的 mode ,并且在启动时,timeout 和 mode 都是可以设置的。

CFRunloop的API中,CFRunloopRun() 方法以默认 mode 启动,在遇到 CFRunLoopStop 方法时退出,(或者在该mode里的所有sources和timers被移除时,runloop也会退出,但这种方式可能被系统里其他的sources干扰,因此不被保证,也不推荐这种方式,下同)。CFRunLoopRunInMode (CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)方法可以以特定 mode 启动,通过这个方法启动的退出场景除了上面所述,还有当 timeout 到时。另外,需要注意的是,这里的 mode 不能选 kCFRunLoopCommonModes ,因为 runloop 需要知道它跑的是哪一个具体的 mode 。最后是 CFRunLoopStop (CFRunLoopRef rl)方法,这个方法让当前的 runloop 退出,使 runloop 可以继续响应 CFRunloopRun 或者 CFRunLoopRunInMode 方法重新启动。

NSRunloop的API中,首先是
-runMode:beforeDate:,接收特定 mode 和 timeout ,并且更重要的是,它相当于运行一次 runloop ,处理的第一个 input source 完成或者 timeout到时,该方法就会退出。另外,调用 CFRunLoopStop 方法当然也能使这个方法启动的 runloop 退出。
-runUntilDate: 方法则运行循环直到 timeout 到时,实际上它的内部是不断的调用 runMode:beforeDate: 方法处理该 runloop 绑定的 input sources 触发的一个个事件。最后是我们熟知的 -run 方法,它也是不断调用 runMode:beforeDate: 方法,并且恐怖的是,这个方法没有超时!于是它不像 runUntilDate: 方法知道适可而止,而是不断跑(infinite loop)。。。文档中明确表示,如果你希望这个 runloop 可以 terminate ,你不能使用这个方法!事实上,我在优化之前就是使用这个run方法开启的runloop(囧)~

对于我们知道明确时间点结束 runloop 的情况,我们可以用 runUntilDate 方法,而如果我们事先不能确定什么时候结束,或者说我们希望 runloop 结束的触发不是由超时决定,而是由其他什么条件决定时,我们可以用文档推荐的方法:

BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
//where shouldKeepRunning is set to NO somewhere else in the program.

学习到这里,我们前面遇到的问题就迎刃而解了,而且由于可以确定明确的结束时间点,我们只需要使用 -runUntilDate 方法,就可以让runloop结束,从而让收集硬件信息的线程结束。通过调试,发现过了指定时间后,线程列表里不再出现 XXDeviceListener 。 算是告一段落哈!

上面对API中方法的研究,我准备再写篇实验的文章,后续推出~

Allocations的使用

关于内存优化,除了避免内存泄露,其实还需要尽量避免内存不合理使用的问题。这里明确一下这两者的区别:

  • 内存泄露:是指内存被分配了,但程序中已经没有指向该内存的指针,导致该内存无法被释放,这叫内存泄露。
  • 内存不合理使用:官方称之为 Abandoned Memory, 顾名思义,这块内存分配了,也有相应指向这块内存的引用 ,但实际上程序已不再使用。比如图片等对象加入了缓存,但缓存中的对象一直没有被使用。再比如本文中的情况,创建了单例对象管理事务,事务很快就能完成,但该对象会一直存在于内存中而无法释放掉。

Instruments 工具集里的 Allocations 可以用来追踪对象的生命周期,这里有几个使用 Allocations 中的小 tips 可以方便我们更好的了解内存的分配情况。

1.在 Allocations 的默认设置里,只是记录了对象 malloc 和 free 这两个事件的时刻,如果还需要追踪对象 retain 、 release 、 autorelease 这些事件,我们可以在设置面板选中 “Record reference counts” 的选项。
2.在Allocations 下面的 Statistics 面板里会显示内存中的所有对象,我们可以搜索查询我们关注的部分。这里默认只显示当前还存活的对象,但设置面板里有三种选项:“All Objects Created”、“Created & Still Living”(default)和“Created & Destroyed”,这里我们可以选中“All Objects Created”就可以查看之前已经被销毁的对象了。
3.我们继续看Statistics面板里的对象列表,如下面图1所示,在最右边显示着当前的堆栈调用信息,可此时看到的类似于崩溃日志里的符号表,并不能看到自己程序里的函数。解决方法是在工程对应Target的Build Settings中,找到Debug Information Format这一项,将Debug时的DWARF改为DWARF with dSYM file,再重新编译后的显示效果就如图2所示。
1

2
DWARF与dSYM的关系是,DWARF是文件格式,而dSYM往往指一个单独的文件。官方解释是

DWARF - Object files and linked products will use DWARF as the debug information format. [dwarf]
DWARF with dSYM File - Object files and linked products will use DWARF as the debug information format, and Xcode will also produce a dSYM file containing the debug information from the individual object files (except that a dSYM file is not needed and will not be created for static library or object file products). [dwarf-with-dsym]

当Debug Information Format为DWARF with dSYM File的时候,构建过程中多了一步Generate dSYM File 。 最终产出的文件也多了一个dSYM文件。不过,既然这个设置叫做Debug Information Format,所以首先得有调试信息。如果此时Generate Debug Symbols选择的是NO的话,是没法产出dSYM文件的。
4.我们往往要关注某段时间或操作过程中内存的分配和使用情况,比如在进入一个视图前或操作前,我们在Allocation设置面板点击Mark Generation,这时候会产生Generation A节点,显示内存当前的情况;我们可以在进入视图后再点一次Mark Generation,在视图退出后再点一次Mark,这样三次产生的 Generation分别记录了进入前、进入后、关闭后,在最后一个Generation应该内存被合理释放,否则就代表了在这个视图或操作中有泄漏或不合理的地方。

回到项目里内存的优化,我取消了单例的使用,而是在上下文中创建了一个普通的对象来管理事务。这个对象里开启一条子线程,在这个线程上添加kvo监听 batteryState 和 batteryLevel 这两个属性值的变化。到了指定时间后,通过 -performSelector:onThread:withObject:waitUntilDone 在该线程上清理工作并移除kvo。

可通过 Allocations 追踪发现,在程序走出该对象的作用域之后,对象仍然没有被销毁。经过调试,发现了没有正常销毁的原因:这个对象注册成为了kvo的 observer,当程序运行到指定时间时,我设置的自线程的runloop也走到了 timeout 而挂起,这时这个线程上就没有活跃的 runloop ,从而 performSelector 方法不会正常得到调用。因而这个kvo的绑定关系一直没有被移除,导致了该对象一直被持有不能释放。找到原因之后就好办了,将子线程的 runloop 的 timeout 设置的比前面的“指定时间”稍长一些,这样在调用 performSelector 的时候,runloop 还处于活跃状态,从而使移除 observer 的代码得以正常调用,再看 Allocations 追踪的结果,这段内存终于在指定时间后正常销毁了!

我后面查文档发现kvo不会持有observer, 如果不改runloop的timeout,只是把kvo部分的代码注释掉,再跑一遍发现这个对象的内存仍然没有被释放,可见原因确实不在于kvo没有被移除。而如果不调用 performSelector 方法,则能正常释放,哈哈😄,原来问题出在这个方法中。查询资料,文档里没有说 performSelector 方法会持有对象,但在 这篇回答里 里有提到

The old NSObject documentation I’ve found says (about performSelector:onThread:withObject:waitUntilDone:)
This method retains the receiver and the arg parameter until after the selector is performed

猜测调用 -performSelector:onThread:withObject:waitUntilDone 时,确实持有了调用对象,而由于 runloop 挂起,selector 方法一直不能执行,从而引用一直不能释放,这应该就是内存没有正常销毁的原因!解决办法跟上面一样,将 runloop 的 timeout 加长,使 performSelector 能正常执行即可。

#####参考资料
深入研究 Runloop 与线程保活
Instruments Allocations track alloc and dealloc of objects of user defined classes
IOS性能调优系列:使用Allocation动态分析内存使用情况

从RSA加密说起

发表于 2016-07-30   |     |   阅读次数

今天在项目里调登录注册接口的时候,发现一个问题,由这个问题引出了下面这篇blog。

背景是这样的:手机号和密码做为接口的请求参数时,密码需要进行RSA加密,而我在charles抓包时发现,即使是同一个明文密码,经过加密后得到的字串(密文)每次都是完全不一样的。一开始我还以为是请求接口的地方出了问题,后来经过验证确实正常情况就是每次加密的密文都不一样,而传到服务器那边校验也能正常工作。

出于对于这样一个校验流程的好奇,我查找了一些资料,了解了下RSA的相关内容。

非对称加密之RSA

为保障传输数据的安全,需要使用加密算法。加密算法一般分为两类:“对称”加密算法和“非对称”加密算法,常用的分别为AES和RSA。“对称”加密算法的安全性完全取决于对加密密钥的管理,不在本文的关注范围内。

AES加密算法的原理可参考App安全之网络传输安全

不同于“对称”加密算法,“非对称”加密算法实现了在不直接传递密钥的情况下完成解密。具体流程如下:

  1. 乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的。
  2. 甲方获取乙方的公钥,然后用它对信息加密。
  3. 乙方得到加密后的信息,用私钥解密。

如果公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。这种算法的破解难度与密钥的长度正相关。一般来说,1024位的RSA密钥基本安全,2048位的密钥极其安全。

RSA算法的原理可参考RSA算法原理

关于RSA这种非对称加密算法,在App的使用当中,需要明白其主要作用有2个:

  • 信息加密:通信双方可以在公开的网络环境下,“安全”的商量对称加密算法所使用的密钥。
  • 电子签名:为了防止中间人攻击,通信双方在商量密钥之前可以通过签名算法确认对方的身份。

非对称加密算法本身是一种加密算法,但由于RSA本身加解密的性能在现在的计算机硬件条件下存在一定瓶颈,同时对加密数据的“安全长度”也有限制,被加密数据的长度一般要求不超过公钥的长度。所以RSA更多的是被用来商量一个密钥,如果密钥是安全的,那么后续的通信都可以使用上面提到的AES来完成,AES在性能上不存在瓶颈。

RSA加密中的padding

padding即填充方式,由于RSA加密算法中要加密的明文是要比模数小的,padding就是通过一些填充方式来限制明文的长度。

  • RSA_PKCS1_PADDING 填充模式,最常用的模式
    输入:必须 比 RSA 钥模长(modulus) 短至少11个字节, 也就是 RSA_size(rsa) – 11 如果输入的明文过长,必须切割,然后填充。
    输出:和modulus一样长
    根据这个要求,对于1024bit的密钥,block length = 1024/8 – 11 = 117 字节

  • RSA_PKCS1_OAEP_PADDING
    输入:RSA_size(rsa) – 41
    输出:和modulus一样长

  • RSA_NO_PADDING  不填充
    输入:可以和RSA钥模长一样长,如果输入的明文过长,必须切割, 然后填充
    输出:和modulus一样长

研究到这里,再一看项目里加密的代码,终于明白了,我们用了RSA_PKCS1_PADDING模式!其中切割出来的11字节用随机数填充,从而每次加密后的密文都完全不一样!

加密算法如下:

- (NSData *)encrypt:(NSString *)plainText usingKey:(SecKeyRef)key error:(NSError **)err
{

    size_t cipherBufferSize = SecKeyGetBlockSize(key);

    uint8_t *cipherBuffer = NULL;

    cipherBuffer = malloc(cipherBufferSize * sizeof(uint8_t));

    memset((void *)cipherBuffer, 0*0, cipherBufferSize);

    NSData *plainTextBytes = [plainText dataUsingEncoding:NSUTF8StringEncoding];

    unsigned long blockSize = cipherBufferSize - 12;

    int numBlock = (int)ceil([plainTextBytes length] / (double)blockSize);

    NSMutableData *encryptedData = [[NSMutableData alloc] init];

    for (int i=0; i<numBlock; i++) {

        unsigned long bufferSize = MIN(blockSize,[plainTextBytes length] - i * blockSize);

        NSData *buffer = [plainTextBytes subdataWithRange:NSMakeRange(i * blockSize, bufferSize)];

        OSStatus status = SecKeyEncrypt(key, kSecPaddingPKCS1,
                                    (const uint8_t *)[buffer bytes],
                                    [buffer length], cipherBuffer,
                                    &cipherBufferSize);

        if (status == noErr)
        {
            NSData *encryptedBytes = [[NSData alloc]
                                   initWithBytes:(const void *)cipherBuffer
                                   length:cipherBufferSize];
            [encryptedData appendData:encryptedBytes];
        }
        else
        {
            if (err)
            {
                *err = [NSError errorWithDomain:@"errorDomain" code:status userInfo:nil];
            }
            free(cipherBuffer);
            return nil;
        }
    }

    if (cipherBuffer)
    {
        free(cipherBuffer);
    }

    return encryptedData;
}

RSA签名验证

在网络中传输数据时,为了防止中间人篡改信息,通讯双方可以通过非对称签名算法确认对方的身份,这也即RSA签名验证。

JSPatch在传js脚本时就用了这个方法保证传输的安全。校验过程如下:

第一步在服务端计算脚本文件的MD值,做为这个文件的数字签名,用存在服务端的私钥对这个MD5值进行加密。然后把这个MD5值和脚本一起打包下发给客户端。客户端拿到脚本和加密后的MD5值,用存在客户端的公钥进行解密,拿到服务端计算出来的MD5值,本地再对脚本文件计算一遍MD5值,对比这两个值是否一致,若一致,则说明传输过程中数据没有被篡改。

需要注意的是,这个校验的目的,是防止数据传输时被篡改,对于数据内容泄露不是太在意。第三方截获请求想要篡改下发恶意脚本时,需要用私钥加密这个脚本的MD5值一起下发,才能最终在客户端通过验证,只要第三方没有私钥,就不能达到目的。

校验算法如下:

-(BOOL)verifyTheDataSHA1WithRSA:(NSData *)signature andplainText:(NSString*)plainText
{
    NSData *sigdata = signature;
    SecKeyRef publicKeyRef= [self getPublicKey];
    size_t signedBytesSize = SecKeyGetBlockSize(publicKeyRef);

    OSStatus status = SecKeyRawVerify(publicKeyRef, kSecPaddingPKCS1SHA1,
                                      (const uint8_t *)[[self getHashBytes:[plainText dataUsingEncoding:NSUTF8StringEncoding]] bytes],
                                      kChosenDigestLength,
                                      (const unsigned char *)[sigdata bytes],
                                      signedBytesSize);
    return status == noErr;
}  

注:openssl 中
公钥加密 = 加密
私钥解密 = 解密
私钥加密 = 签名
公钥解密 = 验证

关于HTTPS

HTTPS应该是RSA应用的最重要的场景,里面同时涉及了“加密”和“签名”的内容。原本以为在这篇里一并把https也总结一下,看着看着发现https涉及的东西实在是太多了,想要顺便提一下实在是不自量力,还是以后专门学习一下再写一篇吧~

HTTPS相关的文章先贴一下,后面再看:
iOS安全系列之一:HTTPS
iOS安全系列之二:HTTPS进阶
HTTPS到底是个啥玩意儿?
HTTPS科普扫盲帖

本文学习参考了以下文章:
App安全之网络传输安全
RSA算法原理
一篇搞定RSA加密与SHA签名
what-is-the-difference-between-the-different-padding-types-on-ios
JSPatch 部署安全策略

锁定横屏的实现与遇到的坑

发表于 2016-07-24   |     |   阅读次数

最近项目中有个锁定横屏的需求。具体是通过url进入h5页面时,url中有参数控制横屏进入webview页面,并能锁定横屏显示。于是研究了下横竖屏切换控制,在进入横屏的要求上也遇到了些坑,最终得到比较好的解决,记录如下:

presentViewController

supportedInterfaceOrientations 和 preferredInterfaceOrientationForPresentation方法

如果当前viewcontroller是通过presentViewController的方式切换出来的,我们可以通过preferredInterfaceOrientationForPresentation方法设置视图默认显示的方向。同时通过supportedInterfaceOrientations方法设置视图支持的显示方向。

//不支持自动旋转,iOS6以后支持的方法
- (BOOL)shouldAutorotate  
{  
    return NO;  
}  

//支持横向  
-(NSUInteger)supportedInterfaceOrientations  
{  
    return UIInterfaceOrientationMaskLandscapeRight;  
}  

//当视图是通过 presentViewController 出来的时候,设定视图默认显示的方向为横向  
-(UIInterfaceOrientation)preferredInterfaceOrientationForPresentation  
{  
    return UIInterfaceOrientationLandscapeRight;  
}  

//IOS6以前的旋转控制方法,这里加上适配IOS5  
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation  
{  
    return toInterfaceOrientation == UIInterfaceOrientationLandscapeRight;  
}  

pushViewController

如果viewcontroller是通过push的方式切换进来的(实际上我们项目中遇到的大多数场景都是这种),只是设置当前VC的横竖屏控制权限是不够的。因为push栈里各个视图控制器的横竖屏控制权限是由根VC决定的,所以在通常的TNV架构(TabbarController->NavigationController->ViewController)里,横竖屏控制的属性我们要从TabbarController一路获取到当前ViewController。

@interface LSTabBarController : UITabbarController

@implementation LSTabBarController
- (BOOL)shouldAutorotate
{
    return [self.selectedViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    return [self.selectedViewController supportedInterfaceOrientations];
}
@end

@interface LSNavigationRotateController : UINavigationController

@implementation LSNavigationRotateController
- (BOOL)shouldAutorotate
{
    return [self.topViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    return [self.topViewController supportedInterfaceOrientations];
}
@end

再在需要设置横屏的VC上设置好相应方法即可。

解决了吗?并没有!

看似大功告成,当我调试时,立刻发现一个问题:当我们从视图A(竖屏)push切换到视图B(横屏)的时候,视图不会自动发生横竖屏切换,而是当设备改变方向的时候才会根据代码中对应的方法设置去改变当前视图的横竖屏方向。
原因是shouldAutorotate和supportedInterfaceOrientations方法只有设备改变方向的时候才会被调用,而进入视图的时候是不会被触发调用的。更悲伤的是上面presentVC里的preferredInterfaceOrientationForPresentation方法在pushVC里也不起作用了,事实上这个方法系统只在模态动画(modal)展示视图时才会调用。

既然进入视图时系统不能自动触发转屏,那我们就手动触发。这里currentDevice的orientation属性没有公开的设置方法,我们用KVC的方式修改。

- (void)viewWillAppear:(BOOL)animated{
    [self forceToOrientation:UIInterfaceOrientationLandscapeLeft];
}

- (void)viewWillDisappear:(BOOL)animated{
    [self forceToOrientation:UIInterfaceOrientationPortrait];
}

- (void)forceToOrientation:(UIDeviceOrientation)orientation{
    NSNumber *orientationUnknown = [NSNumber numberWithInt:UIInterfaceOrientationUnknown];
    [[UIDevice currentDevice] setValue:orientationUnknown forKey:@"orientation"];

    NSNumber *orientationTarget = [NSNumber numberWithInt:orientation];
    [[UIDevice currentDevice] setValue:orientationTarget forKey:@"orientation"];
}  

为什么每次设置orientation的时候都先设置为UnKnown?因为在视图B回到视图A时,如果当时设备方向已经是Portrait,再设置成Portrait会不起作用(直接return)。

好了吧?还是没有~

加上手动触发转屏后,我欣喜的看到页面在进入后自动转为横屏展示,可是当我返回时,又发现了一个蛋疼的问题:

我测试进入h5页面的场景是通过点击feed流页面的cardview里的网页链接,点击~push进h5页面~转横屏~一切都很好,但点返回后,发现原来链接所在的card里的图文排版乱了~~

像下面这样
image

我再三检查basicWebViewController里的-(void)viewWillDisappear方法,确实是调用了将屏幕转回Portrait的方法,上层页面的刷新布局的方法在返回时也正常调用,一切都感觉没啥问题。

折腾了好一阵,我发现一个现象,就是从h5页面返回后,只有点击链接所在的那个card里图文排版错乱,它上下的card都是正常显示的(那些card也都在屏幕里)。于是我找到处理card点击链接的代码,发现了这个:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.pressingActiveRange) {

        id<WBTextActiveRange> activeRange = self.pressingActiveRange;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self eventDelegateDidPressActiveRange:activeRange];
        });

        _touchesBeginPoint = CGPointZero;

            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 若用户点击速度过快,hitRange高亮状态还未绘制又取消高亮会导致没有高亮效果
            // 故延迟执行
            [self setPressingActiveRange:nil];
            [[self eventDelegateContextView] setNeedsDisplay];
        });
    }
}

哈!在调用delegate响应点击事件后,代码里还做了这么一件事:延迟执行取消高亮(链接点击有高亮效果)的重新绘制!可以猜想执行绘制的时候屏幕也处在旋转的过程中,这中间的状态很难确定,自然绘制出来的排版是乱的,而在h5页面返回时又没有相应的重新绘制的操作,导致了返回后这个响应链接的card排版错乱,这也解释了其他card能正常显示。

找到了原因,解决方法就很简单。在进入页面的viewWillAppear方法里,把旋转屏幕的操作改成延迟执行:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 *     NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self forceToOrientation:UIInterfaceOrientationLandscapeLeft];
    });

这样保证绘制工作结束后(下一个runloop)再执行转屏,从而避免了排版错乱的问题。

一些感想

因为选择锁定横屏这是浏览器做为一个通用的模块给其他业务提供的服务,在出现问题时,自然不能改动外面业务的逻辑,而只能从浏览器模块本身找解决办法。上面的方法比较好的解决了我遇到的问题,也可以说是应对潜在这一类问题(排版)的通用解决办法。

Hexo 使用手册

发表于 2016-07-24   |     |   阅读次数

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

#启动本地服务,进行文章预览调试。
浏览器输入 http://localhost:4000 就可以看到效果。

More info: Server

Generate static files

$ hexo generate

#hexo g
生成静态页面
命令必须在init目录下执行,否则不成功,但是也不报错。
当你修改文章Tag或内容,不能正确重新生成内容,可以删除 hexo\db.json 后重试,还不行就到 public 目录删除对应的文件,重新生成。

More info: Generating

Deploy to remote sites

$ hexo deploy

#hexo d

More info: Deployment

ls

ls

4 日志
3 标签
GitHub
© 2016 ls
由 Hexo 强力驱动
主题 - NexT.Mist