即刻APP怎样在苹果APP下载ipa

Nudifier iphone版下载 1.1(附ipa下载/攻略) - 跑跑车手游网
更多历史版本&figure&&img src=&https://pic1.zhimg.com/v2-6bc244a3a76d36b_b.jpg& data-rawwidth=&378& data-rawheight=&316& class=&content_image& width=&378&&&/figure&&p&本文为[腾讯Bugly公众号](&a href=&http://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/s/yYCaPMxHGT9LyRyAPewVWQ& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&http://&/span&&span class=&visible&&mp.weixin.qq.com/s/yYCa&/span&&span class=&invisible&&PMxHGT9LyRyAPewVWQ&/span&&span class=&ellipsis&&&/span&&/a&)投稿,作者:littleliang,未经作者同意,请勿转载。&/p&&h2&一、背景&/h2&&p&为了解决小商户老板们在频繁交易中不方便核对、确认到账的痛点,产品MM提出了新版本需要支持收款到账语音提醒功能。这篇文章总结了开发过程中遇到的坑和一些小技巧。&/p&&h2&二、技术方案&/h2&&p&&b&后台唤醒App&/b&&/p&&p&收款到账语音提醒需要收款方在收到款后,播放一段TTS合成语音播报金额,微信在前台时可以通过模板消息将需要播报的金额带下来,再请求TTS数据并播放,但是app在挂起或者被kill掉的情况下要如何请求语音数据并播放呢?&/p&&p&iOS提供了两种方式唤醒处于挂起或已经被kill掉的app。分别是Silent Notification和VoIP Push Notification,客户端在被唤醒之后将获得30s的后台运行时间,这段运行时间足以请求合成语音数据并播放。&/p&&p&&b&1.Silent Notification:&/b&&/p&&p&&a href=&http://link.zhihu.com/?target=https%3A//developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html%23//apple_ref/doc/uid/TP-CH10-SW8& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Silent Notification&/a&在iOS7以上便可以支持,但是每小时能推送的Silent Notification次数有限制。&/p&&p&&b&2.VoIP Push Notification&/b&&/p&&p&&a href=&http://link.zhihu.com/?target=https%3A//developer.apple.com/library/content/documentation/Performance/Conceptual/EnergyGuide-iOS/OptimizeVoIP.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&VoIP Push Notification&/a&则是在iOS8以上才支持的新Push类型,相比于Silent Notification,VoIP Push具有高优先级、低延迟的优势,并且没有次数限制。&/p&&p&对比这两种技术方案,VoIP Push Notification明显更适合用于收款到账语音提醒的唤醒方案。&/p&&p&&b&TTS合成语音&/b&&/p&&p&TTS语音合成方案分为离线合成方案和在线合成方案,离线合成方案省去网络请求,合成速度更快,节省网络流量,但是合成音的听起来比较机械,语速和停顿的处理较差一些。如果对合成音的效果要求不是特别高,可以考虑采用iOS自带的AVSpeechSynthesis框架,免去语音库的合入,减少安装包大小。&/p&&p&在线合成方案的效果则相对更像人声,富有感情。考虑到产品体验,我们采用了搜索产品部提供的在线语音合成方案,接入方式可以看&a href=&http://link.zhihu.com/?target=https%3A//open.weixin.qq.com/cgi-bin/showdocument%3Faction%3Ddir_list%26t%3Dresource/res_list%26verify%3D1%26id%3Dopen%26token%3D%26lang%3Dzh_CN& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&这篇文章&/a&。合成音格式支持wav,mp3,silk,amr,speex,对比后发现,在合成相同文本的情况下,amr的压缩率最高,但是能听到音质下降明显。silk格式压缩率次高,且能保持相对清晰的音质,单条合成语音大小在2KB左右。&/p&&p&&b&唤醒后播放音频文件&/b&&/p&&p&在请求到合成语音后,要在后台或者锁屏状态下播放音频文件,AVAudio Session的Category值需要使用AVAudioSessionCategoryPlayback或是AVAudioSessionCategoryPlayAndRecord,CategoryOptions根据实际需要可选择MixWithOthers(与其他声音混音)或是DuckOthers(调低其他声音的音量)。&/p&&p&&br&&/p&&figure&&img src=&https://pic2.zhimg.com/v2-fd72a7c649_b.png& data-rawwidth=&666& data-rawheight=&558& class=&origin_image zh-lightbox-thumb& width=&666& data-original=&https://pic2.zhimg.com/v2-fd72a7c649_r.jpg&&&/figure&&p&&br&&/p&&p&需要注意的是,只有iOS10以上才支持app被唤醒后在后台/锁屏状态下播放音频。所以iOS10以下的设备,在收到VoIP Push后只能在local push上设定一段固定铃声,这也是为什么iOS10以下只有“微信支付收款到账”,而没有后面具体的金额数值。&/p&&h2&三、静音开关检测&/h2&&p&不幸的是,在产品发布后没多久就受到了某互联网大佬的吐槽。&/p&&p&&br&&/p&&figure&&img src=&https://pic4.zhimg.com/v2-379cafa20f948b036c603_b.jpg& data-rawwidth=&674& data-rawheight=&580& class=&origin_image zh-lightbox-thumb& width=&674& data-original=&https://pic4.zhimg.com/v2-379cafa20f948b036c603_r.jpg&&&/figure&&p&&br&&/p&&p&从产品体验上来说,收款到账的金额播报是随着local push的弹出一起播放的,更像是一种特殊的push铃声,而苹果对push铃声的处理是受到静音开关控制的,所以讲道理,这个吐槽是合理的。然而前面提到App在被VoIP Push唤醒之后,需要将AudioSessionCategory设置为AVAudioSessionCategoryPlayback或AVAudioSessionCategoryPlayAndRecord才可以在后台播放音频文件,这两种模式是不受静音开关控制的。要实现这个需求,就必须获取当前静音开关的状态。而苹果在iOS5之后并没有明确地提供一种方式让开发获取静音开关的状态,这就陷入了一个尴尬的局面。&/p&&p&苹果在iOS5之前可以使用以下方式监听静音键开关&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&- (BOOL)isMuted
UInt32 routeSize = sizeof(CFStringRef);
OSStatus status = AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &routeSize, &route);
if (status == kAudioSessionNoError)
if (route == NULL || !CFStringGetLength(route))
return YES;
return NO;
&/code&&/pre&&/div&&p&&br&&/p&&p&苹果在iOS5之后便禁止了使用这种方式监听静音按键,背后的原因应该是苹果希望开发者使用AVAudioSession来提供统一的音频播放效果。&/p&&p&最后我在Reddit上找到了一种曲线救国的方式,实现起来也不复杂:使用AudioServicesPlaySystemSound播放一段0.2s的空白音频,并监听音频播放完成事件,如果从开始播放到回调完成方法的间隔时间小于0.1s,则意味当前静音开关为开启状态。&/p&&p&&br&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void SoundMuteNotificationCompletionProc(SystemSoundID
ssID,void* clientData){
MMSoundSwitchDetector* detecotr = (__bridge MMSoundSwitchDetector*)clientD
[detecotr complete];
- (instancetype)init {
self = [super init];
if (self) {
NSURL *pathURL = [[NSBundle mainBundle] URLForResource:@&mute& withExtension:@&caf&];
if (AudioServicesCreateSystemSoundID((__bridge CFURLRef)pathURL, &_soundId) == kAudioServicesNoError){
AudioServicesAddSystemSoundCompletion(self.soundId, CFRunLoopGetMain(), kCFRunLoopDefaultMode, SoundMuteNotificationCompletionProc,(__bridge void *)(self));
UInt32 yes = 1;
AudioServicesSetProperty(kAudioServicesPropertyIsUISound, sizeof(_soundId),&_soundId,sizeof(yes), &yes);
MMErrorWithModule(LOGMODULE, @&Create Sound Error.&);
_soundId = 0;
- (void)checkSoundSwitchStatus:(CheckSwitchStatusCompleteBlk)completHandler {
if (self.soundId == 0) {
completHandler(YES);
self.completeHandler = completH
self.beginTime = CACurrentMediaTime();
AudioServicesPlaySystemSound(self.soundId);
- (void)complete {
CFTimeInterval elapsed = CACurrentMediaTime() - self.beginT
BOOL isSwitchOn = elapsed & 0.1;
if (self.completeHandler) {
self.completeHandler(isSwitchOn);
&/code&&/pre&&/div&&p&&br&&/p&&h2&四、设置声音阈值&/h2&&p&另外一个用户反馈较多的问题是听不到播报声音,通过查看日志发现是触发语音播报时,用户设置的系统音量过小所导致。首先想到的解决方案是直接设置AVAudioPlayer的volume(或者是AudioQueue中的kAudioQueueParam_Volume),然而实验过后发现这样行不通,volume属性受制于系统音量(比如系统volume是0.5,AVAudioPlayer的音量是0.6,则最终的音量为0.5*0.6 =0.3)。要解决音量过小的问题,还是需要通过调节系统音量。最终的解决方案借鉴了进入收付款展示二维码时自动调节屏幕亮度的方案:如果屏幕亮度未达到阈值,则调高屏幕亮度到阈值,离开页面时,将亮度设回原亮度。同理,播放提示音时,若用户设置的系统音量小于阈值,则调节到阈值。提示音播放完毕后,将提示音调回原音量。&/p&&p&&b&控制系统音量有两种方式:&/b&&/p&&p&&b&方式一:通过MPMusicPlayerController设置音量&/b&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&MPMusicPlayerController *mpc = [MPMusicPlayerController applicationMusicPlayer];
//This property is deprecated -- use MPVolumeView for volume control instead.
mpc.volume = 0;
&/code&&/pre&&/div&&p&&br&&/p&&p&第一种方式简单粗暴,在设置的时候会弹出系统音量提示框,如果用户在使用app的过程突然弹出音量框,会对用户造成困扰,不建议使用这种方式,并且苹果在iOS7.0以后已将该属性标为deprecated。&/p&&p&&b&方式二:通过MPVolumeView设置音量&/b&&/p&&p&第二种方式则是将一个看不见的MPVolumeView添加到当前视图上,系统音量提示框就不会显示了&/p&&p&需要注意的是,在调节完系统音量需要将MPVolumeView移除,否则后续用户手动调节音量会出现系统音量提示框不显示的情况。&/p&&p&调节音量的方式,则是先取到MPVolumeView中名为MPVolumeSlider的子View,并对其发送模拟用户操作的事件。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&- (void)setSystemVolume:(float)volume {
UISlider* volumeViewSlider =
for (UIView *view in [self.m_privateVoulmeView subviews]){
if ([view.class.description isEqualToString:@&MPVolumeSlider&]){
volumeViewSlider = (UISlider*)
if (volumeViewSlider != nil) {
[volumeViewSlider setValue:volume animated:NO];
//通过send
[volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
&/code&&/pre&&/div&&p&&br&&/p&&p&更多精彩内容欢迎关注&a href=&http://link.zhihu.com/?target=https%3A//bugly.qq.com/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&bugly&/a&的微信公众账号:&/p&&figure&&img src=&https://pic3.zhimg.com/3f2c1b1ff77fcedf3fb54616_b.jpg& class=&content_image&&&/figure&&p&&a href=&http://link.zhihu.com/?target=https%3A//bugly.qq.com/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&腾讯 Bugly&/a&是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 &a href=&http://link.zhihu.com/?target=https%3A//bugly.qq.com/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Crash&/a& 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!&/p&&p&&/p&
本文为[腾讯Bugly公众号]()投稿,作者:littleliang,未经作者同意,请勿转载。一、背景为了解决小商户老板们在频繁交易中不方便核对、确认到账的痛点,产品MM提出了新版本需要支持收款到账语音提醒功能。这篇文章总结了开发过程中遇到…
&figure&&img src=&https://pic2.zhimg.com/e9454ecbab11e_b.jpg& data-rawwidth=&900& data-rawheight=&500& class=&origin_image zh-lightbox-thumb& width=&900& data-original=&https://pic2.zhimg.com/e9454ecbab11e_r.jpg&&&/figure&&p&如果您有耐心看完这篇文章,您将懂得如何着手进行app的分析、追踪、注入等实用的破解技术,另外,通过“入侵”,将帮助您理解如何规避常见的安全漏洞,文章大纲:&/p&&ul&&li&&p&简单介绍ios二进制文件结构与入侵的原理&/p&&/li&&li&&p&介绍入侵常用的工具和方法,包括pc端和手机端&/p&&/li&&li&&p&讲解黑客技术中的静态分析和动态分析法&/p&&/li&&li&&p&通过一个简单的实例,来介绍如何综合运用砸壳、寻找注入点、lldb远程调试、追踪、反汇编技术来进行黑客实战&/p&&/li&&li&&p&讲解越狱破解补丁和不需越狱的破解补丁制作方法和差别&/p&&/li&&/ul&&h1&&strong&黑客的素养&/strong&&/h1&&ul&&li&&p&敏锐的嗅觉&br&有时候通过一个函数名,一个类名,就能大致的判断出它的作用,这就是嗅觉;功力已臻化境时,甚至可以使用第六感判断出一些注入点&/p&&/li&&li&&p&面对失败的勇气&br&破解有时候很耗时,和程序开发正好相反,它耗时不是耗在写代码上,而是耗在寻找注入点和逆向工程上,有可能你花了3天时间去找程序的破绽,但是最终的破解代码可能就2行,不到一分钟就搞定了;但是你也需要做好面对失败的准备,如果路选错了,有可能你这3天完全是在浪费脑细胞&/p&&/li&&li&&p&洪荒之力&br&洪荒之力-即入侵过程中需要借助的各种工具,工欲善其事,必先利其器,工具都是前人智慧的结晶,能用工具解决的,绝不要手动去搞&/p&&/li&&/ul&&br&&h1&&strong&iOS黑客关键字&/strong&&/h1&&br&&p&iOS的入侵离不开越狱开发,一切的破解、入侵都是建立在越狱的基础上的,如果没有拿到系统级权限,一切的想法都是空谈了,当然,市面上存在免越狱的破解补丁,但是它的开发过程,也是基于越狱环境的&/p&&h3&&strong&tweak&/strong&&/h3&&p&在iOS的黑客界,要做破解或越狱开发,就必须了解tweak,它是各种破解补丁的统称,在google上,如果你想搜索一些越狱开发资料或者开源的破解补丁代码,它是最好的关键字。&/p&&p&iOS的tweak大致分为两种:&/p&&ul&&li&&p&第一种是在cydia上发布的,需要越狱才能安装,大部分是deb格式的安装包,iOS在越狱后,会默认安装一个名叫mobilesubstrate的动态库,它的作用是提供一个系统级的入侵管道,所有的tweak都可以依赖它来进行开发,目前主流的开发工具有theos和iOSOpenDev,前者是采用makefile的一个编译框架,后者提供了一套xcode项目模版,可以直接使用xcode开发可调试,但是这个项目已经停止更新了,对高版本的xcode支持不好,大家酌情选择(本文中的例子全部采用theos)&/p&&/li&&li&&p&第二种是直接打包成ipa安装包,并使用自己的开发证书或者企业证书签名,不需越狱也可以安装,可直接放到自己的网站上,可实现在线安装;对于没有越狱的手机,由于权限的限制,我们是没有办法写系统级的tweak的,例如springboard的补丁是没法运行的,这种tweak大多是针对某个app,把目标app进行修改注入处理,再重新签名和发布,有点类似于windows软件的xxx破解版、xxx免注册版&/p&&/li&&/ul&&p&没有越狱的机器由于系统中没有mobilesubstrate这个库,我们有二个选择,第一个是直接把这个库打包进ipa当中,使用它的api实现注入,第二个是直接修改汇编代码;第一个适用于较为复杂的破解行为,而且越狱tweak代码可以复用,第二种适用于破解一些if…else…之类的条件语句&/p&&h3&&strong&Mobilesubstrate&/strong&&/h3&&p&下面的图展示的就是oc届著名的method swizzling技术,他就是iOS的注入原理,类似于windows的钩子,所以我们注入也称为hook&/p&&p&&figure&&img src=&https://pic3.zhimg.com/c23f21268fcf_b.jpg& data-rawwidth=&629& data-rawheight=&321& class=&origin_image zh-lightbox-thumb& width=&629& data-original=&https://pic3.zhimg.com/c23f21268fcf_r.jpg&&&/figure&&br&Mobilesubstrate为了方便tweak开发,提供了三个重要的模块:&br&&/p&&ul&&li&&p&&strong&MobileHooker&/strong& 就是用来做上面所说的这件事的,它定义一系列的宏和函数,底层调用objc-runtime和fishhook来替换系统或者目标应用的函数&/p&&/li&&li&&p&&strong&MobileLoader&/strong& 用来在目标程序启动时根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序,他的原理下面会简单讲解一下&/p&&/li&&li&&p&&strong&Safe mode &/strong&类似于windows的安全模式,比如我们写的一些系统级的hook代码发生crash时,mobilesubstrate会自动进入安全模式,安全模式下,会禁用所有的第三方动态库&/p&&/li&&/ul&&h3&&strong&app注入原理&/strong&&/h3&&p&上面讲到了mobileloader,他是怎么做到把第三方的lib注入进目标程序的呢?这个我们要从二进制文件的结构说起,从下面的图来看,Mach-O文件的数据主体可分为三大部分,分别是头部(Header)、加载命令(Load commands)、和最终的数据(Data)。mobileloader会在目标程序启动时,会根据指定的规则检查指定目录是否存在第三方库,如果有,则会通过修改二进制的loadCommands,来把自己注入进所有的app当中,然后加载第三方库。&/p&&p&&figure&&img src=&https://pic2.zhimg.com/a987a2ad175_b.jpg& data-rawwidth=&523& data-rawheight=&570& class=&origin_image zh-lightbox-thumb& width=&523& data-original=&https://pic2.zhimg.com/a987a2ad175_r.jpg&&&/figure&&br&为了让大家看的更清楚,下面我用machoview来打开一个真实的二进制文件给大家看看,可以看出,二进制当中所有引用到的动态库都放在Load commands段当中,所以,通过给这个段增加记录,就可以注入我们自己写的动态库了&br&&/p&&p&&figure&&img src=&https://pic4.zhimg.com/98ea065adba7cb4d0de733d19a42170b_b.jpg& data-rawwidth=&640& data-rawheight=&436& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic4.zhimg.com/98ea065adba7cb4d0de733d19a42170b_r.jpg&&&/figure&&br&那么问题来了,在这里插入我们自己的动态库有什么用?我们自己写的代码没有执行的入口,我们一样没发干坏事,嗯,恭喜你问到点子上了,我们还需要一个”main”函数来执行我们自己的代码,这个”main”函数在oc里面称为构造函数,只要在函数前声明 “&strong&attribute&/strong&((constructor)) static” 即可,有了它我们就可以发挥想象力,进行偷天换日干点坏事了:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&#import &CaptainHook/CaptainHook.h&CHDeclareClass(AnAppClass);
CHMethod(1, void, AnAppClass, say, id, arg1)
NSString* tmp=@&Hello, iOS!&;
CHSuper(1, AnAppClass, say, tmp);
__attribute__((constructor)) static void entry()
NSLog(@&Hello, Ice And Fire!&);
CHLoadLateClass(AnAppClass);
CHClassHook(1, AnAppClass,say);
&/code&&/pre&&/div&&/blockquote&&p&到这里为止,我们已经知道了怎么在目标程序注入自己的代码,那么我们怎么知道需要hook哪些方法?怎么找到关键点进行实际的破解呢?下面讲一下常见的app入侵分析方法&/p&&h3&&strong&iOS逆向分析方法&/strong&&/h3&&p&逆向分析最常用的有三种方法:&/p&&ol&&li&&p&&strong&网络分析&/strong&&br&通过分析和篡改接口数据,可以有效的破解通过接口数据来控制客户端行为的app,常用的抓包工具有Tcpdump, WireShark, Charles等,windows平台有fidller&/p&&/li&&li&&p&&strong&静态分析&/strong&&br&通过砸壳、反汇编、classdump头文件等技术来分析app行为,通过这种方式可以有效的分析出app实用的一些第三方库,甚至分析出app的架构等内容,常用的工具有dumpdecrypted(砸壳)、hopper disassembler(反汇编)、class_dump(导头文件)&/p&&/li&&li&&p&&strong&动态分析&/strong&&br&有静就有动,万物都是相生相克的,动态分析指的是通过分析app的运行时数据,来定位注入点或者获取关键数据,常用的工具有cycript(运行时控制台)、 lldb+debugserver(远程断点调试)、logify(追踪)&/p&&/li&&/ol&&br&&h1&&strong&demo:微信抢红包插件&/strong&&/h1&&br&&p&上面讲了很多原理性的东西,相信大家已经看的不耐烦了,下面我们一起动点真格的,我们从头开始,一步一步的做一个微信的自动抢红包插件,当然,网上可能已经有相关的开源代码了,但是我这里要讲的是,这些代码是怎么得出来的,我么重点讲一讲分析过程&/p&&h3&&strong&工欲善其事,必先利其器&/strong&&/h3&&p&一台越狱的手机,并装有以下软件&/p&&ul&&li&&p&cycript&/p&&/li&&li&&p&dumpdecrypted&/p&&/li&&li&&p&debug server&/p&&/li&&li&&p&openssh&/p&&/li&&/ul&&p&一台苹果电脑,并装有以下软件&/p&&ul&&li&&p&class_dump&/p&&/li&&li&&p&Theos&/p&&/li&&li&&p&Hopper Disassembler v3&/p&&/li&&li&&p&xcode&/p&&/li&&li&&p&insert_dylib&/p&&/li&&li&&p&pp助手&/p&&/li&&/ul&&h3&&strong&寻找注入点&/strong&&/h3&&h4&&strong&砸壳&/strong&&/h4&&p&首先我们要做的就是把微信的壳砸掉,砸壳其实是为了把它的头文件classdump出来,因为从appstore下载的app二进制都是经过加密的,直接进行classdump操作是啥也看不出来的&/p&&ul&&li&&p&用pp助手把dumpdecrypted.dylib文件copy到微信的documents目录&/p&&/li&&li&&p&ssh到手机的终端,cd到documents目录中,执行下面的命令进行砸壳操作&/p&&/li&&/ul&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&xxx$ cp /usr/lib/dumpdecrypted.dylib /path/to/app/document
xxx$ DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /path/to/WeChat
&/code&&/pre&&/div&&/blockquote&&ul&&li&&p&最后砸壳完成后会在documents目录生成砸了壳后的二进制文件,用pp助手copy出来并class-dump他的头文件备用&/p&&/li&&/ul&&p&执行完这几行命令后,会在微信的documents目录生成一个WeChat.decrypted文件,这就是砸壳后的二进制文件;当然了,这一步不是必须的,我们可以直接从91或者pp助手下载一个已经砸过壳的版本&/p&&h4&&strong&动态分析-cycript&/strong&&/h4&&p&要想实现自动抢红包,我们必须找到收到红包消息的handler方法,怎么入手呢?我们先从界面出发,进入微信的消息首发窗口:&/p&&figure&&img src=&https://pic4.zhimg.com/dc308d76a3a7ca105d3f_b.jpg& data-rawwidth=&322& data-rawheight=&496& class=&content_image& width=&322&&&/figure&&br&&ul&&li&&p&ssh进手机的终端,输入ps命令,查找到微信的进程id&/p&&/li&&/ul&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&ps aux | grep WeChat
&/code&&/pre&&/div&&/blockquote&&ul&&li&&p&祭起神器cycript,根据上一步找到的pid注入到微信的进程&/p&&/li&&/ul&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&cycript -p pidxxx
&/code&&/pre&&/div&&/blockquote&&ul&&li&&p&在cycript的终端输入这一串方法,作用就是打印出当前界面的view层级,(cycript还有很多妙用,大家可以上官网看文档,这里不详细介绍)&/p&&/li&&/ul&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&UIApp.keyWindow.recursiveDescription().toString()
&/code&&/pre&&/div&&/blockquote&&p&最终的输出如下,内容太多,大家肯定看不清楚,不过没关系,这个不是重点,这里只是展示一下打印的结果形式:&br&&/p&&p&&figure&&img src=&https://pic3.zhimg.com/c7059554ecbcf22a35d6_b.jpg& data-rawwidth=&635& data-rawheight=&324& class=&origin_image zh-lightbox-thumb& width=&635& data-original=&https://pic3.zhimg.com/c7059554ecbcf22a35d6_r.jpg&&&/figure&我们可以随机的选取一个节点不要太靠树叶,也不要太靠树根,例如我选的是标红的部分,把这个节点的内存地址copy出来,这个内存地址,就代表了这个节点的view对象,ios开发的老油条们都知道,通过view的nextResponder方法,可以找出它所属的视图控制器ViewController,所以我么在cycript的控制台中持续输入如下的命令:&br&&/p&&p&&figure&&img src=&https://pic4.zhimg.com/91e7aeddcce7_b.jpg& data-rawwidth=&640& data-rawheight=&223& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic4.zhimg.com/91e7aeddcce7_r.jpg&&&/figure&看到没有,通过四个nextResponder方法调用,我么找到了当前聊天窗口的ViewController类名,他就是&strong&BaseMsgContentViewController&/strong&,现在我们缩小了目标范围,下面我们还需要继续缩小范围,要找到具体的消息处理函数才行。&br&&/p&&h4&&strong&动态分析-Logify&/strong&&/h4&&p&要继续缩小范围,就得祭起神器Logify了,它是theos的一个模块,作用就是根据头文件自动生成tweak,生成的tweak会在头文件的所有方法中注入NSLog来打印方法的入参和出参,非常适合追踪方法的调用和数据传递&/p&&p&现在我们根据此前砸壳后class_dump出来的头文件,找到&strong&BaseMsgContentViewController&/strong&在pc终端执行如下命令:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&logify.pl /path/to/BaseMsgContentViewController.h & /out/to/Tweak.xm
&/code&&/pre&&/div&&/blockquote&&p&输出的tweak文件大概是这个样子的:&br&&/p&&p&&figure&&img src=&https://pic4.zhimg.com/cf733bfb261bc00ab296e3_b.jpg& data-rawwidth=&640& data-rawheight=&237& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic4.zhimg.com/cf733bfb261bc00ab296e3_r.jpg&&&/figure&这里带百分号的关键字,例如 %hook、%log、%orig 都是mobilesubstrate的MobileHooker模块提供的宏,其实也就是把method swizzling相关的方法封装成了各种宏标记,使用起来更简单,大家想要更深入了解各种标记,可以google一下logos语言&br&&/p&&h4&&strong&theos创建tweak&/strong&&/h4&&p&上面我们用logify生成了一个tweak代码,我们要把它安装到手机上,首先需要使用theos进行编译,安装了theos之后,在pc终端输入nic.pl:&/p&&p&&figure&&img src=&https://pic2.zhimg.com/74f65e22b9158a65adc78b08d91407bd_b.jpg& data-rawwidth=&640& data-rawheight=&259& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic2.zhimg.com/74f65e22b9158a65adc78b08d91407bd_r.jpg&&&/figure&首先选择项目模版当然是tweak啦,然后是项目名称、作者,后面两个选项要注意:&br&&/p&&ul&&li&&p&首先是bundle filter,这个需要填你需要注入的目标app的bundle id,MobileLoader模块会根据它来寻找你的tweak的注入目标&/p&&/li&&li&&p&最后是list id applications to terminate upon installation,这里指定当tweak安装成功之后,需要kill的进程,我们要hook微信,这里就填微信的二进制文件名就可以了,为什么要kill? 因为我么的插件是需要在app启动时加载进去的,如果不重启app,插件是不会生效的&/p&&/li&&/ul&&p&最后一切都完成后,在当前目录会生成下列文件:&/p&&p&&figure&&img src=&https://pic1.zhimg.com/d39e352a9fa58dd4afe2ee308b333210_b.jpg& data-rawwidth=&592& data-rawheight=&130& class=&origin_image zh-lightbox-thumb& width=&592& data-original=&https://pic1.zhimg.com/d39e352a9fa58dd4afe2ee308b333210_r.jpg&&&/figure&把上面logify生成的tweak文件覆盖到当前目录,并用文本编辑器打开makefile文件,在文件的开头增加你的ios设备的ip地址和ssh端口:&br&&/p&&p&&figure&&img src=&https://pic1.zhimg.com/6c6fd93fd0e77a06f368_b.jpg& data-rawwidth=&485& data-rawheight=&349& class=&origin_image zh-lightbox-thumb& width=&485& data-original=&https://pic1.zhimg.com/6c6fd93fd0e77a06f368_r.jpg&&&/figure&最后在pc终端进入项目目录,输入 make package install 命令:&br&&/p&&p&&figure&&img src=&https://pic1.zhimg.com/bad9c7ffd54_b.jpg& data-rawwidth=&640& data-rawheight=&168& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic1.zhimg.com/bad9c7ffd54_r.jpg&&&/figure&期间会让你输入设备的ssh密码,越狱机器的默认ssh密码是alpine,make命令会生成deb安装包,放在debs目录,我们如果想对外发布自己的插件,可以把生成的安装包上传到cydia即可&br&&/p&&p&安装成功后再次进入微信的聊天界面,并使用另外一个微信在群里发个普通消息,连接xcode打开越狱机器控制台,查看输出,会发现有类似下面的输出:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&Jun
7 09:56:13 Administratorde-iPhone WeChat[85972] &Notice&: [1;36m[WxMsgPreview] [m[0;36mTweak.xm:308[m [0;30;46mDEBUG:[m -[&BaseMsgContentViewController: 0x15e0c9a00& addMessageNode:{m_uiMesLocalID=2, m_ui64MesSvrID=0, m_nsFromUsr=ccg*675~9, m_nsToUsr=@chatroom, m_uiStatus=1, type=1, msgSource=&(null)&}
layout:1 addMoreMsg:0]
&/code&&/pre&&/div&&/blockquote&&p&看出来了吧,消息处理函数是BaseMsgContentViewController的addMessageNode:layout:addMoreMsg:方法,大家可以看出,方法的参数内容也打印出来了&br&&/p&&h4&&strong&动态分析-lldb&/strong&&/h4&&p&到目前为止,我么已经把范围缩小到了具体的函数,看起来注入点已经找到了,但是请大家思考一下,如果我们在这个函数中注入抢红包逻辑,那我们的tweak会不会有什么致命的缺陷?&/p&&p&是的,因为BaseMsgContentViewController这个类是微信群聊天窗口对应的controller,我么必须进入到群的聊天界面,这个类才会创建,如果不进入聊天窗口,我们的插件就不生效了,而且,即使进入聊天窗口,也只是能自动枪当前群的红包而已,其他群就无能为力了,是不是有点low?&/p&&p&所以为了使我们的插件显得上流一些,我么还要继续追根溯源,寻找消息的源头,这里就用到了lldb远程调试,使用lldb打断点的方式,通过调用栈,我们可以就可以看到当消息来到时,方法的调用顺序,找到最先执行的消息处理函数。&/p&&p&要在刚刚追踪到的&strong&addMessageNode:layout:addMoreMsg:&/strong&方法中打断点,首先我们得知道它在运行时的内存地址,那么内存地址怎么来呢?有这么一个公式:&/p&&ul&&li&&p&内存地址=进程内存基地址+函数在二进制中的偏移量&/p&&/li&&/ul&&p&首先偏移量我们可以通过反汇编工具hooper来查,在pc上用hooper打开微信的二进制文件(注意,打开时会让你选择armv7或者arm64,这需要根据你越狱手机的cpu类型来选,一定要和你的手机一致),hooper的界面非常简洁,左侧有个搜索框,可以输入函数名,直接找到函数在二进制中的位置&/p&&p&&figure&&img src=&https://pic1.zhimg.com/6ee67d074a774dbfddc2e4dd2dd4c348_b.jpg& data-rawwidth=&640& data-rawheight=&514& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic1.zhimg.com/6ee67d074a774dbfddc2e4dd2dd4c348_r.jpg&&&/figure&通过左侧的搜索框搜addMessageNode关键字,找到它的偏移量是0xd7c6c:&br&&/p&&p&&figure&&img src=&https://pic2.zhimg.com/35deab7a114e6aefd940d9d_b.jpg& data-rawwidth=&640& data-rawheight=&61& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic2.zhimg.com/35deab7a114e6aefd940d9d_r.jpg&&&/figure&找到了偏移量,还需要进程的基地址,这个地址需要连lldb,所以下面讲一下如何连接lldb进行远程调试,先ssh进越狱手机的终端,在终端输入如下命令(注意,你的手机必须连xcode调试过才会有这个命令):&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&debugserver *:19999 -a WeChat
&/code&&/pre&&/div&&/blockquote&&p&然后在pc端新起一个终端窗口,输入如下命令来连接手机端进行调试:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&lldb
process connect connect://deviceIP:19999
&/code&&/pre&&/div&&/blockquote&&p&如果连接成功,会进入lldb的控制台,我们在lldb的控制台输入如下命令来获取微信进程的基地址:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&image list -o -f
&/code&&/pre&&/div&&/blockquote&&p&执行这个命令会打印很多行数据,像下面图中这样,我么要找到微信的二进制文件所在的行,记录它的内存地址0XE800:&br&&/p&&p&&figure&&img src=&https://pic1.zhimg.com/ed6abe7bcba4b11dfd174_b.jpg& data-rawwidth=&640& data-rawheight=&39& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic1.zhimg.com/ed6abe7bcba4b11dfd174_r.jpg&&&/figure&到这里我们两个地址都找到了,再通过br命令打断点:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&br s -a '0XE800+0xd7c6c'
&/code&&/pre&&/div&&/blockquote&&p&打好断点后继续向群里面发消息,我们会发现进程被断掉了,这时输入bt指令,就可以看到当前的调用栈,就像下图这样:&br&&/p&&p&&figure&&img src=&https://pic1.zhimg.com/660fe790d34a0f809a2a58_b.jpg& data-rawwidth=&640& data-rawheight=&361& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic1.zhimg.com/660fe790d34a0f809a2a58_r.jpg&&&/figure&分析堆栈的时候,重点找出模块时WeChat的项,这些都是微信模块的方法调用,有了堆栈,我们需要根据堆栈的内存地址找出它的具体函数名,思路还是先根据上面讲到的公式来计算出栈地址在二进制中的偏移量,然后用hooper找到偏移量对应的函数名&br&&/p&&ul&&li&&p&函数在二进制中的偏移量=内存地址 - 进程内存基地址&/p&&/li&&/ul&&p&例如根据箭头所指的内存地址和刚刚得到的进程基地址,计算偏移量:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&0xad02f4 – 0xe8000 =
&/code&&/pre&&/div&&/blockquote&&p&然后在hooper中搜索这个地址,得到结果如下:&br&&/p&&p&&figure&&img src=&https://pic1.zhimg.com/5bbeb02b689a1a815adfc_b.jpg& data-rawwidth=&640& data-rawheight=&210& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic1.zhimg.com/5bbeb02b689a1a815adfc_r.jpg&&&/figure&最终把所有的栈都进行还原,得出调用栈是这个样子的:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&-[CMessageMgr MainThreadNotifyToExt:]:
-[BaseMsgContentLogicController OnAddMsg:MsgWrap:]:
-[RoomContentLogicController DidAddMsg:]
-[BaseMsgContentLogicController DidAddMsg:]
—————-&
-[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:]:
&/code&&/pre&&/div&&/blockquote&&p&CMessageMgr这个类浮出水面了,是时候发挥黑客的嗅觉了,根据方法名我们能判断出MainThreadNotifyToExt:这个方法仅仅是用来发送通知的,如果hook这个方法,我们是拿不到消息内容的&/p&&p&由于这里可能是一个异步调用,用断点的方式,可能已经打印不出来栈信息了,所以还得使用logify来继续追踪CMessageMgr这个类,讲过的内容我就不重复了,直接得到最终的消息处理函数:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&-(void)AsyncOnAddMsg:(id)message MsgWrap:(CMessageWrap* )msgWrap
&/code&&/pre&&/div&&/blockquote&&h3&&strong&实现“抢”的动作&/strong&&/h3&&p&上一节我们已经找到了hook的关键点,那么该如何去实现抢的动作?同样我们需要结合动态分析和静态分析,首先得到红包消息体的数据特征,然后再分析处理消息的关键点&/p&&h4&&strong&数据包分析&/strong&&/h4&&p&首先我们的代码需要分辨哪些才是红包消息,方法很简单,用logify追踪BaseMsgContentViewController,然后向微信群发一个红包,观察手机日志输出,我们可以看出消息的数据结构中有个type字段,值是49,这个type应该就是标记消息类型的,如果不确定,可以再发个图片或者文本之类的消息,这个值是不同的:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&Administratorde-iPhone WeChat[47410] &Notice&: [1;36m[WxMsgPreview] [m[0;36mTweak.xm:308[m [0;30;46mDEBUG:[m -[&BaseMsgContentViewController: 0x15e0c9a00& addMessageNode:{m_uiMesLocalID=16, m_ui64MesSvrID=0425509, m_nsFromUsr=@chatroom, m_nsToUsr=ccg*675~9, m_uiStatus=4, type=49, msgSource=&&msgsource&
&silence&0&/silence&
&membercount&3&/membercount&
&/msgsource&
layout:1 addMoreMsg:0]
&/code&&/pre&&/div&&/blockquote&&p&现在我们能分辨消息类型了,重点来了,怎么实现抢这个事呢,可能聪明人已经猜到了,从ui入手,先找到微信本身的抢红包函数,我们自己来给它构造参数并调用他不就行了?&br&&/p&&p&&figure&&img src=&https://pic4.zhimg.com/410027edcd4f183d66d7137abfe1c5df_b.jpg& data-rawwidth=&306& data-rawheight=&528& class=&content_image& width=&306&&&/figure&把红包点开后,用cycript打印出当前view的层次,就像下面这个,一眼就可以看到重点,WCRedEnvelopesReceiveHomeView就是开红包弹框的类名&br&&/p&&figure&&img src=&https://pic3.zhimg.com/f45f5c37ad39aefdbbe902_b.jpg& data-rawwidth=&640& data-rawheight=&274& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic3.zhimg.com/f45f5c37ad39aefdbbe902_r.jpg&&&/figure&&p&知道类名后,用cycript追踪它,点击开红包,在日志中找到了下图中的内容,从名字来看,这是一个事件处理函数,我们现在要做的,就是把他还原成oc代码,真正实现抢红包功能&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&Administratorde-iPhone WeChat[91173] &Notice&: [1;36m[WxMsgPreview] [m[0;36mTweak.xm:8[m [0;30;46mDEBUG:[m -[&WCRedEnvelopesReceiveHomeView: 0x13cdda8c0& OnOpenRedEnvelopes]
&/code&&/pre&&/div&&/blockquote&&h4&&strong&静态分析法&/strong&&/h4&&p&怎么把他还原成oc代码,真正实现抢红包功能呢?还得借助一点点汇编技能,只是一点点而已,因为现在的反汇编工具已经很强大了,我们不需要挨个去看寄存器了&/p&&p&在pc上用hooper打开微信的二进制文件,搜索OnOpenRedEnvelopes,查看汇编代码,注意在图片中最后一行调用了一个WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes函数&/p&&figure&&img src=&https://pic4.zhimg.com/9d63c0cc5c0bdb83f7aaf_b.jpg& data-rawwidth=&640& data-rawheight=&46& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic4.zhimg.com/9d63c0cc5c0bdb83f7aaf_r.jpg&&&/figure&&figure&&img src=&https://pic4.zhimg.com/bc07b7e1730ced9e14a3fe65a4de384b_b.jpg& data-rawwidth=&640& data-rawheight=&294& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic4.zhimg.com/bc07b7e1730ced9e14a3fe65a4de384b_r.jpg&&&/figure&&p&继续搜索WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes这个方法,找到它的汇编代码&/p&&ul&&li&&p&首先他不知道从哪里获取了一个payinfoitem&/p&&/li&&li&&p&然后又获取了payinfo的m_c2cNativeUrl属性&/p&&/li&&li&&p&然后调用substringfromindex吧navtiveurl的前缀截断,并调用bizutil的一个方法把url参数转换成了一个字典&/p&&/li&&/ul&&figure&&img src=&https://pic4.zhimg.com/c5ecd2a34bca9ed5028eb_b.jpg& data-rawwidth=&640& data-rawheight=&386& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic4.zhimg.com/c5ecd2a34bca9ed5028eb_r.jpg&&&/figure&&p&最终反解出的代码如下,是不是很简单?&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&NSString *nativeUrl = [[msgWrap m_oWCPayInfoItem] m_c2cNativeUrl];
nativeUrl = [nativeUrl substringFromIndex:[@&wxpay://c2cbizmessagehandler/hongbao/receivehongbao?& length]];NSDictionary *nativeUrlDict = [%c(WCBizUtil) dictionaryWithDecodedComponets:nativeUrl separator:@&&&];
&/code&&/pre&&/div&&/blockquote&&p&继续往下看, 在这里前面三行创建了一个mutable dictionary:&/p&&ul&&li&&p&紧接着下面三个框框处都是调用了setobject:forkey:向里面填东西,那填的东西是啥呢?&/p&&/li&&li&&p&其实这里已经可以看的很清楚了,第一个key是msgtype,值是字符串1,第二个sendid,值是调用了一个objectforkey从另一个字典中取出来的,很显然,另一个字典就是上面从url解析得到的,后面的channelid也是同样的道理&/p&&/li&&/ul&&p&&figure&&img src=&https://pic3.zhimg.com/a106c02c5ad85dbdbc0fa2_b.jpg& data-rawwidth=&640& data-rawheight=&496& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic3.zhimg.com/a106c02c5ad85dbdbc0fa2_r.jpg&&&/figure&最终得到的代码如下:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&NSMutableDictionary *args = [[%c(NSMutableDictionary) alloc] init];
[args setObject:nativeUrlDict[@&msgtype&] forKey:@&msgType&];
[args setObject:nativeUrlDict[@&sendid&] forKey:@&sendId&];
[args setObject:nativeUrlDict[@&channelid&] forKey:@&channelId&];
&/code&&/pre&&/div&&/blockquote&&p&继续往下看从箭头所指的几处,我们可以看见,它的代码是这样的,共分为四步&/p&&ul&&li&&p&第一个箭头调用了mmservicecenter的defaultcenter方法来获取mmservicecenter实例&/p&&/li&&li&&p&第二个箭头调用了CContactMgr的class方法&/p&&/li&&li&&p&第三个箭头调用了第一步获取的mmservicecenter实例的getservice方法,而这个方法是把第二步得到的class作为参数&/p&&/li&&li&&p&第四个箭头很明白了吧,第三步得到了CContactMgr实例,这里就是调用CContactMgr实例的getselfcontact方法获取自己的账户资料&/p&&/li&&/ul&&p&&figure&&img src=&https://pic2.zhimg.com/76877dfeba4fc2d789c9cd_b.jpg& data-rawwidth=&640& data-rawheight=&317& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic2.zhimg.com/76877dfeba4fc2d789c9cd_r.jpg&&&/figure&最终还原的到的代码如下:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&CContactMgr *contactManager = [[%c(MMServiceCenter) defaultCenter] getService:[%c(CContactMgr) class]];
CContact *selfContact = [contactManager getSelfContact];
&/code&&/pre&&/div&&/blockquote&&p&继续往下看,这里使用刚刚得到的selfcontact来获取displayname和headimgurl,并把它们设置到刚刚的字典里面了,key分别是nickname和headimg&br&&/p&&p&&figure&&img src=&https://pic2.zhimg.com/e14aa0ec85b8d9e846f549_b.jpg& data-rawwidth=&640& data-rawheight=&279& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic2.zhimg.com/e14aa0ec85b8d9e846f549_r.jpg&&&/figure&最终的代码:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[args setObject:[selfContact getContactDisplayName] forKey:@&nickName&];
[args setObject:[selfContact m_nsHeadImgUrl] forKey:@&headImg&];
&/code&&/pre&&/div&&/blockquote&&p&接着看,接下来这两段就比较蛋疼了,完全是从内存地址里面取的值,我也不知道他从哪里来,怎么办呢?有没有不懂汇编就能搞定它的捷径呢,答案是有!&/p&&ul&&li&&p&对于第一个,我可以通过它的key猜出来,还记得最开始的时候我们取过payinfo的一个nativeurl属性吧,我们姑且把他传进去&/p&&/li&&li&&p&对于第二个,我们可以猜测sessionUserName大概是会话名称,也就是群名称的意思,从哪里取这个值呢?我们先把也设置成伪代码&/p&&/li&&/ul&&figure&&img src=&https://pic1.zhimg.com/d0dacd366ee8c5e98d34e8_b.jpg& data-rawwidth=&640& data-rawheight=&290& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic1.zhimg.com/d0dacd366ee8c5e98d34e8_r.jpg&&&/figure&&p&最终的结果如下:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[args setObject:nativeUrl forKey:@&nativeUrl&];
[args setObject:xxx forKey:@&sessionUserName&];
&/code&&/pre&&/div&&/blockquote&&p&继续往下看,接下来这一段还是用mmservicecenter来获取WCRedLogicMgr对象,然后调用WCRedLogicMgr的open方法来拆红包,可以想象open方法的参数就是上面我们辛苦组装的字典&br&&/p&&p&&figure&&img src=&https://pic3.zhimg.com/a337db90dbe7e_b.jpg& data-rawwidth=&640& data-rawheight=&364& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic3.zhimg.com/a337db90dbe7e_r.jpg&&&/figure&代码如下:&br&&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[[[%c(MMServiceCenter) defaultCenter] getService:[%c(WCRedEnvelopesLogicMgr) class]] OpenRedEnvelopesRequest:args];
&/code&&/pre&&/div&&/blockquote&&h4&&strong&领红包逻辑&/strong&&/h4&&p&到这里,我们再总结一下我们上面分析的过程……&/p&&ul&&li&&p&得到m_oWCPayInfoItem属性&/p&&/li&&li&&p&解析m_oWCPayInfoItem的m_c2cNativeUrl属性&/p&&/li&&li&&p&得到selfcontact&/p&&/li&&li&&p&组装相关参数&/p&&/li&&li&&p&调用OpenRedEnvelopesRequest:领取红包&/p&&/li&&/ul&&p&最终的抢红包代码合并起来如下:&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&#import &WxMsgPreview.h&%hook CMessageMgr
-(void)AsyncOnAddMsg:(id)message MsgWrap:(CMessageWrap* )msgWrap {
if(msgWrap.m_uiMessageType == 49){
CContactMgr *contactManager = [[%c(MMServiceCenter) defaultCenter] getService:[%c(CContactMgr) class]];
CContact *selfContact = [contactManager getSelfContact];
if ([msgWrap.m_nsContent rangeOfString:@&wxpay://c2cbizmessagehandler/hongbao/receivehongbao&].location != NSNotFound) { // 红包
NSString *nativeUrl = [[msgWrap m_oWCPayInfoItem] m_c2cNativeUrl];
nativeUrl = [nativeUrl substringFromIndex:[@&wxpay://c2cbizmessagehandler/hongbao/receivehongbao?& length]];
NSDictionary *nativeUrlDict = [%c(WCBizUtil) dictionaryWithDecodedComponets:nativeUrl separator:@&&&];
NSMutableDictionary *args = [[%c(NSMutableDictionary) alloc] init];
[args setObject:nativeUrlDict[@&msgtype&] forKey:@&msgType&];
[args setObject:nativeUrlDict[@&sendid&] forKey:@&sendId&];
[args setObject:nativeUrlDict[@&channelid&] forKey:@&channelId&];
[args setObject:[selfContact getContactDisplayName] forKey:@&nickName&];
[args setObject:[selfContact m_nsHeadImgUrl] forKey:@&headImg&];
[args setObject:nativeUrl forKey:@&nativeUrl&];
[args setObject:msgWrap.m_nsFromUsr forKey:@&sessionUserName&];
[[[%c(MMServiceCenter) defaultCenter] getService:[%c(WCRedEnvelopesLogicMgr) class]] OpenRedEnvelopesRequest:args];
&/code&&/pre&&/div&&/blockquote&&p&刚才说了,有两个疑难点没有解决:&/p&&ul&&li&&p&第一:我们不知道payinfo是哪里来的,&/p&&/li&&li&&p&第二:sessionusername我们也不知道是哪里来的&/p&&/li&&/ul&&p&第二个我们已经猜测到它代表群名称,所以我们从修改几次群名称,然后再观察logify打印出的参数值的变化,就可以确认出从哪里取了&/p&&p&这时候我们可以从我们注入点的参数入手,首先用logify打印出addmsg方法的参数信息,会发现,它的第二个参数刚好有一个payinfo的属性,这样第一个问题迎刃而解了&/p&&p&通过一番折腾,得出了抢红包的核心代码,再结合上面章节所讲的theos制作tweak包的方法,打包并安装到手机,发个红包试试,是不是秒抢?&/p&&br&&h1&&strong&免越狱插件&/strong&&/h1&&br&&h4&&strong&检查依赖项&/strong&&/h4&&p&如果设备没有越狱,是没有mobilesubstrate等环境的,而且一些系统目录是没有读写权限的,这时我么只能从目标app的二进制文件入手,通过手动修改load commands来加载自己的dylib,那么上面我们的插件又是使用theos基于mobilesubstrate编译的,有没有办法确定我们的dylib有没有依赖其他的库呢?&/p&&p&使用osx自带的otool工具即可,可以看出,我们的lib是依赖于substrate库的,其他的都是系统库,所以我们从越狱设备中把cydiasubstrate文件copy出来重命名为libsunstrate.dylib,和我们的dylib一起放入wechat.app目录中&/p&&p&最后使用install_name_tool命令修改动态库的路径把它指向app二进制文件的同级目录&/p&&figure&&img src=&https://pic1.zhimg.com/3c4cc6f7ba058ca9631f40_b.jpg& data-rawwidth=&640& data-rawheight=&393& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&https://pic1.zhimg.com/3c4cc6f7ba058ca9631f40_r.jpg&&&/figure&&h4&&strong&制作安装包&/strong&&/h4&&p&解决了依赖问题,然后要把我们的库注入到二进制weixin的二进制文件,这一步使用开源的insert_dylib即可 (@executable_path是一个环境变量,指的是二进制文件所在的路径)&/p&&p&insert_dylib命令格式:./insert_dylib 动态库路径 目标二进制文件&/p&&blockquote&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&//注入动态库./insert_dylib @executable_path/wxmsgpreview.dylib WeChat//打包成ipaxcrun -sdk iphoneos PackageApplication -v WeChat.app -o ~/WeChat.ipa
&/code&&/pre&&/div&&/blockquote&&p&最后使用用企业证书或者开发证书签名对ipa重新签名,就可以放到自己的渠道进行发布了!&br&&/p&&br&&h1&&strong&结语&/strong&&/h1&&br&&p&通过综合运用各种工具,进行静态和动态分析,我们通过实战破解了微信的抢红包逻辑,明白了入侵常用的工具,上面的抢红包代码还有很多改进之处,比如没有判断红包的发送者是不是自己、也没有判断红包里面的文字是不是抢错三倍,有兴趣的童鞋可以尝试优化一下!&/p&&br&&p&&a href=&http://link.zhihu.com/?target=http%3A//weixin.qq.com/r/VjuLk3DE1C2rrTTw925E& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&http://&/span&&span class=&visible&&weixin.qq.com/r/VjuLk3D&/span&&span class=&invisible&&E1C2rrTTw925E&/span&&span class=&ellipsis&&&/span&&/a& (二维码自动识别)&/p&&p&&a href=&http://link.zhihu.com/?target=http%3A//blog.csdn.net/heiby/article/details/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&阅读原文&/a&
作者:&a href=&http://link.zhihu.com/?target=http%3A//my.csdn.net/heiby& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&唐胜云&/a&&/p&
如果您有耐心看完这篇文章,您将懂得如何着手进行app的分析、追踪、注入等实用的破解技术,另外,通过“入侵”,将帮助您理解如何规避常见的安全漏洞,文章大纲:简单介绍ios二进制文件结构与入侵的原理介绍入侵常用的工具和方法,包括pc端和手机端讲解黑客…
个人认为数据结构是编程最重要的基本功没有之一!&br&&br&学了顺序表和链表,你就知道,在查询操作更多的程序中,你应该用顺序表;而修改操作更多的程序中,你要使用链表;而单向链表不方便怎么办,每次都从头到尾好麻烦啊,怎么办?你这时就会想到双向链表or循环链表。&br&&br&学了栈之后,你就知道,很多涉及后入先出的问题,例如函数递归就是个栈模型、Android的屏幕跳转就用到栈,很多类似的东西,你就会第一时间想到:我会用这东西来去写算法实现这个功能。&br&&br&学了队列之后,你就知道,对于先入先出要排队的问题,你就要用到队列,例如多个网络下载任务,我该怎么去调度它们去获得网络资源呢?再例如操作系统的进程(or线程)调度,我该怎么去分配资源(像CPU)给多个任务呢?肯定不能全部一起拥有的,资源只有一个,那就要排队!那么怎么排队呢?用普通的队列?但是对于那些优先级高的线程怎么办?那也太共产主义了吧,这时,你就会想到了优先队列,优先队列怎么实现?用堆,然后你就有疑问了,堆是啥玩意?自己查吧,敲累了。&br&&br&总之好好学数据结构就对了。我觉得数据结构就相当于:我塞牙了,那么就要用到牙签这“数据结构”,当然你用指甲也行,只不过“性能”没那么好;我要拧螺母,肯定用扳手这个“数据结构”,当然你用钳子也行,只不过也没那么好用。学习数据结构,就是为了了解以后在IT行业里搬砖需要用到什么工具,这些工具有什么利弊,应用于什么场景。以后用的过程中,你会发现这些基础的“工具”也存在着一些缺陷,你不满足于此工具,此时,你就开始自己在这些数据结构的基础上加以改造,这就叫做自定义数据结构。而且,你以后还会造出很多其他应用于实际场景的数据结构。。你用这些数据结构去造轮子,不知不觉,你成了又一个轮子哥。
个人认为数据结构是编程最重要的基本功没有之一! 学了顺序表和链表,你就知道,在查询操作更多的程序中,你应该用顺序表;而修改操作更多的程序中,你要使用链表;而单向链表不方便怎么办,每次都从头到尾好麻烦啊,怎么办?你这时就会想到双向链表or循环…
&h2&&b&从0打造一个GPUImage(1)&/b&&/h2&&blockquote&&i&做业务是没有任何前途的,所以,让我们来学习GL ES&/i&&/blockquote&&h2&&b&GPUImage&/b&&/h2&&p&GPUImage是什么,如果有不知道的iOS开发,那就要面壁思过了。&br&基本上国内99%的视频摄像类app都是基于这个开发的(搞不好这个99%还要包括国外的app)。&br&我们的目的就是打造一个拥有GPUImage大部分功能的iOS库。&/p&&h2&&b&OpenGL ES 2.0&/b&&/h2&&p&GPUImage本质上是一个使用iOS方便操作OpenGL ES的第三方库。除非你要写shader,否则基本上接触不到gl es的东西,它帮你把CPU -& GPU这一层全部封装好了。这也是GPUImage厉害的地方。&/p&&h2&&b&GL ES的基础知识&/b&&/h2&&h2&&b&画一个三角形&/b&&/h2&&p&学一门语言一般是从hello world开始的。不过GL ES一般是从绘制一个三角形开始的。&/p&&p&GL ES可以绘制三种基本元素,点,线, 三角。&/p&&p&一个三角形,理论上知道3个点的位置,我们就能绘制出一个三角形了。那么放在程序里,无非就是,我们如何告诉程序,我们这3个点的位置。&/p&&p&绘制之前,我们先看一下GL ES的坐标系。&/p&&p&&br&&/p&&figure&&img data-rawheight=&1380& src=&https://pic3.zhimg.com/v2-85fa5e1fd1e8c2d8a0a560_b.jpg& data-rawwidth=&768& class=&origin_image zh-lightbox-thumb& width=&768& data-original=&https://pic3.zhimg.com/v2-85fa5e1fd1e8c2d8a0a560_r.jpg&&&/figure&&p&&br&&/p&&p&我们的目标是绘制一个这样的三角形。&br&&/p&&figure&&img data-rawheight=&1390& src=&https://pic1.zhimg.com/v2-4bdea732d4dd8acd5a3ca95be8bd649e_b.jpg& data-rawwidth=&800& class=&origin_image zh-lightbox-thumb& width=&800& data-original=&https://pic1.zhimg.com/v2-4bdea732d4dd8acd5a3ca95be8bd649e_r.jpg&&&/figure&&p&&br&&/p&&h2&&b&步骤&/b&&/h2&&p&用GL ES画一个三角形从程序的角度需要以下几个步骤。&br&1. 写一个fragment Shader和vertex shader&br&2. 创建一个glProgram,链接两个shader。&br&3. 设置viewport&br&4. 设置framebuffer和renderbuffer用来显示三角形&br&5. 传递顶点坐标&br&6. 绘制&/p&&p&如果把所有的都详细介绍一遍的话,估计没有几万字是说不清楚的。这里我们只做5,6步。前面4步我已经写好了放在github上。&/p&&p&&b&第一个问题, 如何向GPU传递顶点信息&/b&&/p&&p&顶点信息是什么?&br&简单来说,为了绘制三角形,我们必须告诉GL ES三角形的三个点具体是什么。在GL ES中,我们使用(x, y, z)这种形式的数据来表示一个顶点的xyz坐标。&/p&&p&那么放在程序里,我们使用的是&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&const GLfloat vertices[] = {
-1, -1, 0,
&/code&&/pre&&/div&&p&这种数据格式来传递顶点信息。典型的C语言的一维数组。每个顶点由3个数值确定坐标。那么3个顶点,刚好是含有9个GLFloat数值的数组了。&/p&&p&那么怎么向GPU传递数据呢?&/p&&p&看一下代码。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&glEnableVertexAttribArray(_positionSlot);
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
&/code&&/pre&&/div&&p&在学习一般计算机语言的过程中,我们已经习惯了赋值就是简单的x = 10这种形式。&br&但是OpenGL ES不一样的地方在于,OpenGL ES内部是一个巨大的状态机。&br&至于为什么OpenGL被设置成状态机模式的原因很简单,因为GPU就是这么工作的。&br&再给大家举一个形象的例子。&br&清晨起床的你需要去用咖啡机做一杯咖啡。&br&首先,你需要&br&1. 放入咖啡豆&br&2. 将旋钮转动,选择制作咖啡的杯数&br&3. 灌入水&br&4. 在咖啡机的出水口放置咖啡壶。&br&5. 点击制作按钮&/p&&p&也就是说,任何一步出错都会导致你制作咖啡失败。但是,如果你把前4步做的没问题,那么只要按按钮,就肯定可以做出咖啡。&/p&&p&OpenGL可以看做是一个拥有好几百个步骤的咖啡机。但是,这个咖啡机不需要你调配基本的设置就可以做出咖啡,需要你做的仅仅是,配置其中几个步骤,做出不同风味的咖啡。比如,不加奶就是美式,加少量奶就是拿铁,多奶就是卡布奇诺。&/p&&p&那么如何利用OpenGL来绘制呢?&br&&/p&&figure&&img data-rawheight=&1170& src=&https://pic2.zhimg.com/v2-ad65b99a9ebbe4aa4c0bb9_b.jpg& data-rawwidth=&1592& class=&origin_image zh-lightbox-thumb& width=&1592& data-original=&https://pic2.zhimg.com/v2-ad65b99a9ebbe4aa4c0bb9_r.jpg&&&/figure&&p&&br&&/p&&p&上面这张图就是OpenGL绘制的基本过程。&/p&&p&这里不详细阐述每个过程的意义了。&br&需要注意的是,我们需要控制的步骤只有2个。&br&1. 传顶点数据&br&2. 写vertex shader 和fragment shader&/p&&p&我们先看一下vertex shader里面到底是什么东西.&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&attribute vec4 a_P
void main(void) {
gl_Position = a_P
&/code&&/pre&&/div&&p&首先,第一行是一个变量的声明。这里声明的变量的值是从外部传入的。&br&shader中一共有三种变量,attribute, uniform, varying。&/p&&p&至于3种变量的区别,大家可以看这篇文章。&a href=&https://link.zhihu.com/?target=http%3A//www.jianshu.com/p/989cad48e01a& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&三种变量的区别&/a&&/p&&p&下面是一个形如C语言的main函数。但是不同的地方在于,这个main函数里面有一个莫名其妙的赋值,是把我们的外部变量a_Position赋值给一个叫做gl_Position的东西。&/p&&p&其实,就像之前所说的,OpenGL是一个状态机,那么gl_Position可以理解为OpenGL的其中一个状态,这里你就是把OpenGL的一个状态确定了。&/p&&p&既然vertex shader又叫做顶点着色器,所以很理所当然的,这里的gl_Position就是代表我们需要处理的顶点。也就是说,这里gl_Position接受的数据就是我们之前传入的vertices数组。&/p&&p&几个问题。&br&1.vec4是什么东西?&br&vec4你可以类比为swift中的元组。他是一组包含了4个参数的结构。比如,一个RGBA的颜色用4个0-1的float类型表示一个颜色。刚好用vec4就可以表示一个RGBA颜色。比如vec4(1.0, 0.0, 0.0, 1.0)就表示一个透明度为1.0的红色。&/p&&p&2.我们的最终绘制结果是一个三角形,当然,三角形只需要3个点就可以绘制,但是,当这个三角形放到屏幕上的时候,这个三角形区域就包含了无数个像素,这个vertex shader到底是处理三个点,还是区域内的所有点?&br&那么在这里解释一下。&b&vertex shader,只对传入的顶点操作。比例如,你绘一个三角形,只传入三个点顶点数据。vertex shader只会对这个三点进行操作,而且是并行计算的&br&至于点所包围的区域。这个是vertex shader之后有个格栅化(就是上图中的Rasterization)的操作。就是计算你在缓冲区占了多少个像素点。然后在frament shader里利用插值计算每个像素的颜色。&/b&&/p&&p&再来看看frament shader。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&pre
void main(void) {
gl_FragColor = vec4(1.0, 0.0, 0.0 , 1.0);
&/code&&/pre&&/div&&p&第一句话是指明float类型的精度。&br&大家可能会很奇怪。在普通的语言中,float类型有一个固定的取值范围。&br&但是在glsl中,shader将float类型分为三种精度,lowp, mediump, highp.分别表示着,低精度,中精度,高精度。越高的精度耗费的资源和时间越多。我们需要在shader中指明我们需要的精度。这里使用mediump就可以了。&/p&&p&下面也是个类似于vertex shader中的main函数。&br&区别在于,这里返回的是gl_FragColor而不是gl_Position.因为顶点位置的处理我们已经在veretx shader中做完了。所以这里只需要处理三角形区域内的每个像素的颜色是多少。这里我们赋值vec4(1.0, 0.0, 0.0, 1.0);代表每个像素点的颜色都是红色。&/p&&h2&&b&在iOS中将三角形绘制出来&/b&&/h2&&p&先来看一下绘制的代码。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&- (void)drawTrangle {
static const GLfloat vertices[] = {
-1, -1, 0,
glEnableVertexAttribArray(_positionSlot);
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glDrawArrays(GL_TRIANGLES, 0, 3);
[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
&/code&&/pre&&/div&&p&在这段代码中,我们根据OpenGL ES的坐标系,确定了三角形的三个顶点,然后生成了一个一维数组,用来存放三个顶点的坐标。&/p&&p&然后我们调用glEnableVertexArrribArray(_positionSlo)这句话,激活了在vertex shader中声明的a_Position这个变量。&br&怎么激活的呢?&br&你们需要把示例代码下载下来然后看step7.&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// step7
- (void)setupShader {
shaderCompiler = [[ZQLShaderCompiler alloc] initWithVertexShader:@&vertexShader.vsh& fragmentShader:@&fragmentShader.fsh&];
[shaderCompiler prepareToDraw];
_positionSlot = [shaderCompiler attributeIndex:@&a_Position&];
&/code&&/pre&&/div&&p&里面执行的原理我们下一章细讲。&/p&&p&最后,调用了glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);&br&这个方法,将我们的vertices数组赋值给vertex shader中的a_Position.&/p&&p&这个方法的原型如下。&/p&&p&void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer);&/p&&p&有茫茫多参数。&/p&&p&第一个,index.&br&这个值是索引。哪里的索引呢?其实,索引,指的就是shader中,某个变量的位置。这里,自然传入的就是a_Position这个变量的位置了。&/p&&p&第二个,size。&br&是指,传入某个变量的数量,更简单地说,就是vec多少, 比如,位置,就是3,xyz,颜色就是4,rgba.这里你可能会有疑问,为啥shader里声明的a_Position是vec4,但是在vertices中,每个位置我们只用了3个数值来表示,还有一个呢?你疑惑的没错,其实位置是vec4(x,y,z,w),但是最后一个w只有在矩阵变换的时候才有用,默认情况下是1.在这里我们不需要传入w。所以位置信息是3.&/p&&p&第三个,type&br&指定数组中每个组件的数据类型。我们用的是float类型,所以是GL_Float。&/p&&p&第四个, normalized&br&指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)&/p&&p&第五个, stride&br&指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。&br&在这里,我们传递的是数组不是一个struct类型,所以是0,这块内容后面还会详细介绍。&/p&&p&第六个,pointer&br&指定第一个组件在数组的第一个顶点属性中的偏移量。该数组与GL_ARRAY_BUFFER绑定,储存于缓冲区中。初始值为0,也可以直接传递顶点数组,也就是我们的vertices&/p&&p&最后跑一下我们的程序。看看效果。&br&&/p&&figure&&img data-rawheight=&1866& src=&https://pic2.zhimg.com/v2-f67d7e90e0f91e276c4ae8b_b.jpg& data-rawwidth=&926& class=&origin_image zh-lightbox-thumb& width=&926& data-original=&https://pic2.zhimg.com/v2-f67d7e90e0f91e276c4ae8b_r.jpg&&&/figure&&p&&br&&/p&&p&一定要下载demo看代码。&br&demo地址如下。&br&&a href=&https://link.zhihu.com/?target=https%3A//github.com/zangqilong198812/OpenGLESTutorial/tree/master& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&https://&/span&&span class=&visible&&github.com/zangqilong19&/span&&span class=&invisible&&8812/OpenGLESTutorial/tree/master&/span&&span class=&ellipsis&&&/span&&/a&&/p&
从0打造一个GPUImage(1)做业务是没有任何前途的,所以,让我们来学习GL ESGPUImageGPUImage是什么,如果有不知道的iOS开发,那就要面壁思过了。 基本上国内99%的视频摄像类app都是基于这个开发的(搞不好这个99%还要包括国外的app)。 我们的目的就是打造一…
&figure&&img src=&https://pic1.zhimg.com/v2-30f911ef00a2ead1cf90d1_b.jpg& data-rawwidth=&1920& data-rawheight=&584& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&https://pic1.zhimg.com/v2-30f911ef00a2ead1cf90d1_r.jpg&&&/figure&&ul&&li&&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&AsyncDisplayKit系列之一:原理和思路&/a&&/li&&li&&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&AsyncDisplayKit系列之二:布局系统&/a&&/li&&li&&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&AsyncDisplayKit系列之三:深度优化列表性能&/a&&/li&&/ul&&p&对于iOS开发而言,UITableView/UICollectionView的优化一直是iOS应用性能优化重要的一块。即使是iOS10+iPhone7这样的最新软硬件配置,在系统的信息app中滚动,仔细观察的话仍然能感到一定的掉帧现象。对于UI要求苛刻的苹果,竟然在如此简单的tableView上无法达到60fps的帧率,可见优化滚动性能的背后并不简单。&/p&&h2&为什么?&/h2&&p&理想状态下,iOS的帧率应该保持在60fps。然而很多情况下用户操作时会感觉到掉帧或者『不跟手』。原因可能有很多,这里只简单列举几个,网上可以找到许多相应分析:&/p&&ol&&li&&b&CPU(主要是主线程)/GPU负担过重或者不均衡&/b&(诸如mask/cornerRadius/drawRect/opaque带来offscreen rendering/blending等等)。由于所有的UIView都是由CALayer来负责显示,因此对Core Animation的了解就变得尤为重要。这里推荐Nick Lockwood的*Core Animation: Advanced Techniques*一书,其中有对Core Animation的性能有着非常详尽的梳理和剖析。&/li&&li&&b&Autolayout布局性能瓶颈&/b&,约束计算时间会随着数量呈指数级增长,并且必须在主线程执行。具体分析可以参考这篇文章:&a href=&https://link.zhihu.com/?target=http%3A//floriankugler.com//auto-layout-performance-on-ios/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Auto Layout Performance on iOS&/a&。这也是为何ASDK抛弃了Autolayout而设计了自己的布局系统的重要原因之一(&a href=&https://link.zhihu.com/?target=https%3A//github.com/facebook/AsyncDisplayKit/issues/196& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&auto layout · Issue #196 · facebook/AsyncDisplayKit · GitHub&/a&)。Autolayout在单个View开发时能带来很多便利,而在一些需要高性能的场景下需要谨慎使用。&br&&/li&&li&尽管从iPhone4S(A5)开始CPU已经采用多核,然而&b&对于大多数app来说,多线程协作并没有被充分利用。&/b&换句话说,在app卡顿(主线程所占用的核心满负荷)时,往往CPU的其他核心几乎无事可做。一般情况下,由于主线程承担了绝大部分的工作,如果能把主线程的任务转移一部给其他线程进行异步处理,就可以马上享受到并发带来的性能提升。这应该也是AsyncDisplayKit得名的原因之一。&/li&&/ol&&p&UIKit的单线程设计也有一定的历史原因。早在十年前iOS SDK刚问世的时候,mobile SDK还是一个非常新的概念,更没有移动多核CPU的存在,因此当时的重点是简单可靠,大多数API都没有支持相对复杂的异步操作。时至今日,如果要完全重构UIKit使之支持异步绘制和布局,对于兼容已有海量的app,难度可想而知。在iOS10中虽然对UICollectionView/UITableView做了一定的预加载优化(WWDC2016 Session219),然而并没有从根本上解决主线程布局和渲染的问题。&/p&&h2&优化思路&/h2&&p&我们知道,当用户开始滚动或点击一个View,所有的事件都会被送到主线程等待处理。此时主线程能否抽出足够充裕的时间来处理变得极为重要,尤其是在连续操作(如UIGestureRecognizer)时,每次touchMoved事件处理都会占用主线程一定的时间(如新的UIImageView进入视图,主线程开始处理布局或者图片解码,而这些需要连续占用大量CPU时间)。如果一个操作耗时超过16ms(1000ms/60fps),那就意味着下一帧无法及时得到处理,引起丢帧。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-862d7be56df983f9cd51b_b.jpg& data-rawwidth=&1542& data-rawheight=&852& class=&origin_image zh-lightbox-thumb& width=&1542& data-original=&https://pic4.zhimg.com/v2-862d7be56df983f9cd51b_r.jpg&&&/figure&&p&(图片截取自WWDC2016 session219)&/p&&p&因此如何能将主线程的压力尽可能减轻成为优化的首要目标。&/p&&p&对列表滚动卡顿的常用解决方案有:&/p&&ol&&li&针对Autolayout性能优化:提前计算并缓存cell的layout: &a href=&https://link.zhihu.com/?target=https%3A//github.com/forkingdog/UITableView-FDTemplateLayoutCell& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&forkingdog/UITableView-FDTemplateLayoutCell: Template auto layout cell for automatically UITableViewCell height calculating&/a&&/li&&li&省去中间滑动过程中的计算,直接计算目标区域cell: &a href=&https://link.zhihu.com/?target=https%3A//github.com/johnil/VVeboTableViewDemo& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&VVebo剥离的TableView绘制&/a&&/li&&li&弃用Autolayout,采用手动布局计算。这样虽然可以换来最高的性能,但是代价是编写和维护的不便,对于经常改动或者性能要求不高的场景并不一定值得。&br&&/li&&li&自行异步渲染Layer,如:&a href=&https://link.zhihu.com/?target=https%3A//github.com/ibireme/YYAsyncLayer& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&ibireme/YYAsyncLayer: iOS utility classes for asynchronous rendering and display.&/a&&/li&&li&iOS10列表的prefetch API,只是并没有解决Autolayout的性能问题,同时也受到系统版本限制。&/li&&/ol&&p&&br&&/p&&p&推荐Yaoyuan的博客,也有比较深入的介绍: &a href=&https://link.zhihu.com/?target=http%3A//blog.ibireme.com//smooth_user_interfaces_for_ios/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&iOS 保持界面流畅的技巧 | Garan no dou&/a& &/p&&h2&ASDK的基本思路:异步&/h2&&p&对于一般的开发者,自己重新实现一整套异步布局和渲染机制是非常困难的。幸运的是,ASDK做到了。&/p&&p&AsyncDisplayKit(ASDK)是2012年由Facebook开始着手开发,并于2014年出品的高性能显示类库,主要作者是Scott Goodson。Scott(github: &a href=&https://link.zhihu.com/?target=https%3A//github.com/appleguy& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&appleguy · GitHub&/a&)曾经参与了多个iOS版本系统的开发,包括UIKit以及一些系统原生app,后来加入Facebook并参与了ASDK的开发并应用到Paper,因此该库有机会从相对底层的角度来进行一系列的优化。&/p&&p&现在最新的版本是2.0,除了拥有1.0系列版本核心的异步布局渲染功能,还增加了类似ComponentKit的基于flexbox的布局功能。源文件一共近300个,3万多行代码,是一个非常庞大而精密的显示和布局系统。使用上&b&如果不考虑工程成本,完全可以在一定程度上代替UIKit的大部分功能。&/b&同时由于和Instagram同处于FB家族,因此也迅速在最近的更新中加入了IGListKit的支持。&/p&&p&在Scott介绍ASDK的视频中,总结了一下三点占用大量CPU时间的『元凶』(虽然仍然可能有以上提到的其他原因,但ASDK最主要集中于这三点进行优化):&/p&&ol&&li&&b&渲染&/b&,对于大量图片,或者大量文字(尤其是CJK字符)混合在一起时。而文字区域的大小和布局,恰恰依赖着渲染的结果。ASDK尽可能后台线程进行渲染,完成后再同步回主线程相应的UIView。&br&&/li&&li&&b&布局&/b&。ASDK完全弃用了Autolayout,另辟蹊径实现了自己的布局和缓存机制。关于布局的问题会在下一篇讲到。&br&&/li&&li&系统objects的&b&创建与销毁&/b&。由于UIKit封装了CALayer以支持触摸等显示以外的操作,耗时也相应增加。而这些同样也需要在主线程上操作。ASDK基于Node的设计,突破了UIKit线程的限制。&br&&/li&&/ol&&p&&br&&/p&&p&既然同步就意味着阻塞,那就异步放到其他线程去做,在需要主线程时再同步回来。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-56b49c627fd_b.jpg& data-rawwidth=&1370& data-rawheight=&914& class=&origin_image zh-lightbox-thumb& width=&1370& data-original=&https://pic3.zhimg.com/v2-56b49c627fd_r.jpg&&&/figure&&p&我们知道对于一般UIView和CALayer来说,因为他们不是线程安全的,任何相关操作都需要在主线程进行。正如UIView可以弥补CALayer无法处理用户事件的不足一样,ASDK引入了Node的概念来解决UIView/CALayer只能在主线程上操作的限制(不由让人想起『Abstract layer can solve many problems, except problem of having too many abstract layers.』)。&/p&&h2&ASDisplayNode主要特点:&/h2&&ol&&li&每个Node对应相应的UIView或者CALayer,从开发者的角度而言,只需要将初始化UIView的代码稍作修改,替换为创建ASDisplayNode即可。在不需要接受用户操作的Node上可以开启isLayerBacked,直接使用CALayer进一步降低开销。根据Scott的研究UIView的开销大约是CALayer的5倍。&br&&/li&&li&Node&b&默认是异步布局/渲染&/b&,只有在需要将frame/contents等同步到UIView上才会回到主线程,使其空出更多的时间处理其他事件。&br&&/li&&li&ASDK只有在认为需要的时候才会异步地为Node加载相应的View,因此创建Node的开销变得非常低。同时Node是&b&线程安全&/b&的,可以在任意queue上创建和设置属性。&br&&/li&&li&ASDK不仅有与UIView对应的大部分控件(如ASButtonNode、ASTextNode、ASImageNode、ASTableNode等等),同时也bridge了大多数UIView的方法和属性,可以非常方便的操作frame/backgroundColor/addSubnode等,因此一般情况下只要对Node进行操作,ASDK就会在适当的时候同步到其View。如果需要的话,当相应的View加载之后(或访问node.view手动触发加载),也可以通过node.view的方式直接访问,回到我们熟悉的UIKit。&br&&/li&&li&当实现自定义View的时候,ASDisplayNode提供了一个初始化方法initWithViewBlock/initWithLayerBlock,就可以将任意UIView/CALayer用Node包裹起来(被包裹的view可以使用autolayout),从而与ASDK的其他组件相结合。虽然这样创建的Node与一般view在布局和渲染上的差异不大,但是由于Node管理着何时何地加载view,我们仍然能得到一定的性能提升。&/li&&/ol&&p&举例来说,当使用UIKit创建一个UIImageView:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span& _imageView = [[UIImageView alloc] init];
_imageView.image = [UIImage imageNamed:@&hello&];
_imageView.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
[self.view addSubview:_imageView];
&/code&&/pre&&/div&&p&使用ASDK后只要稍加改动:&/p&&div class=&highlight&&&pre&&code class=&language-objective-c&&&span&&/span& &span class=&n&&_imageNode&/span& &span class=&o&&=&/span& &span class=&p&&[[&/span&&span class=&n&&ASImageNode&/span& &span class=&n&&alloc&/span&&span class=&p&&]&/span& &span class=&n&&init&/span&&span class=&p&&];&/span&
&span class=&n&&_imageNode&/span&&span class=&p&&.&/span&&span class=&n&&image&/span& &span class=&o&&=&/span& &span class=&p&&[&/span&&span class=&bp&&UIImage&/span& &span class=&nl&&imageNamed&/span&&span class=&p&&:&/span&&span class=&s&&@&hello&&/span&&span class=&p&&];&/span&
&span class=&n&&_imageNode&/span&&span class=&p&&.&/span&&span class=&n&&frame&/span& &span class=&o&&=&/span& &span class=&n&&CGRectMake&/span&&span class=&p&&(&/span&&span class=&mf&&10.0f&/span&&span class=&p&&,&/span& &span class=&mf&&10.0f&/span&&span class=&p&&,&/span& &span class=&mf&&40.0f&/span&&span class=&p&&,&/span& &span class=&mf&&40.0f&/span&&span class=&p&&);&/span&
&span class=&p&&[&/span&&span class=&nb&&self&/span&&span class=&p&&.&/span&&span class=&n&&view&/span& &span class=&nl&&addSubview&/span&&span class=&p&&:&/span&&span class=&n&&_imageNode&/span&&span class=&p&&.&/span&&span class=&n&&view&/span&&span class=&p&&];&/span&
&/code&&/pre&&/div&&p&虽然只是简单的把View替换成了Node,然而和UIImageView不同的是,此时ASDK已经在悄悄使用另一个线程进行图片解码,从而大大降低新的用户操作到来时主线程被阻塞的概率,使每一个回调都能得到及时的处理。实践中将会有更加复杂的情况,有兴趣的话可以参考项目中的Example目录,有20多个不同场景下的示例项目。&/p&&h2&一些细节&/h2&&ol&&li&在ASDisplayNode.h中有相当多的注释,其中displaysAsynchronously属性大致描述了异步渲染的步骤:&br&&/li&&/ol&&p&&br&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&& * Asynchronous rendering proceeds as follows:
& * When the view is initially added to the hierarchy, it has -needsDisplay true.
& * After layout, Core Animation will call -display on the _ASDisplayLayer
& * -display enqueues a rendering operation on the displayQueue
& * When the

我要回帖

更多关于 苹果app退款 的文章

 

随机推荐