作者: xiao, yanzi

交互设计:iOS原型尺寸规范

*近公司安排我带一下新来的交互设计师,我想给他制定一份交互设计规范。这样一来,即使新来的交互设计师没什么基础,也可以根据这一份规范,做出大致标准的原型图。

因为Ui设计稿是先做iPhone6的,方便向上适配iPhone6Plus,也方便向下适配iPhone5和iPhone4的尺寸。所以,交互设计稿的尺寸,就按照iPhone6的尺寸来做。

 

在这里我还是要推荐下我自己建的iOS开发学习群:680565220,群里都是学ios开发的,如果你正在学习ios ,小编欢迎你加入,今天分享的这个案例已经上传到群文件,大家都是软件开发党,不定期分享干货(只有iOS软件开发相关的),包括我自己整理的一份2018*新的iOS进阶资料和高级开发教程

1、 iPhone6的界面布局是:屏幕是4.7英寸的,设计稿的大小为750x1334px。
状态栏(status bar):就是电量条,其高度为:40px;
导航栏(navigation):就是顶部条,其高度为:88px;
主菜单栏(submenu,tab):就是标签栏,底部条,其高度为:98px;
内容区域(content):就是屏幕中间的区域,其高度为:1334px-40px-88px-98px=1108px
截图如下:

推荐3款测量工具为:MarkMan马克鳗,Dorado标注,PXcook像素大厨。

 

2、关于iPhone6的图标的尺寸:
导航栏的图标高度为44px左右,标签栏的图标尺寸为50x50px左右,*大为96x64px。
关于iPhone6的文字的尺寸:

导航栏的文字大小*大值是34px,标签栏的图标下方的文字大小为20px。内容区域的文字大小是:24px,26px,28px,30px,32px,34px。
3、(iPhone6设计稿尺寸是@2x),做原型图的时候,可以做成@2x的,即750x1334px;也可以做成@1x的,即375*667px。
4、设置界面的图标高度和开关滑动按钮的图标高度:58px。

 

5、关于颜色,自从我做了3套高保真的交互原型图之后,发现后面还是改成画线框图来的快。
线框图的黑白灰之纯美,有利于专心布局界面,而不用在意界面的颜色搭配。

黑白灰颜色常用的数值是:

文字黑色#282828
文字深灰色#656565
文字浅灰色#98989
边框浅灰色#C3C3C3
背景淡灰色#f2f2f2
按钮背景纯白色#ffffff
6、常用的可点击高度,在iPhone6的原型图上,统一成88px。在iPhone6设计稿中,88px是一个常用的设计尺寸。

 

7、搜索栏的高度,在iPhone6的原型图上,统一成58px。在iPhone6设计稿中,58px是一个常用的设计尺寸。

 

8、在iPhone6设计稿中,界面元素之间的常用距离,亲密距离:20px;疏远距离:30px。
疏远距离:比如,所有元素距离手机屏幕*左边的距离。
亲密距离:比如,左边图标与右边文字之间的距离。

9、原型设计中,需要考虑不同屏幕尺寸的苹果手机,在@1x的情况下的适配情况。比如:
iPhone5在@2x屏幕尺寸是,640x1136px;对应的@1x,屏幕尺寸就是320x568px;
iPhone6在@2x屏幕尺寸是:750x1334px;对应的@1x,屏幕尺寸就是375x667px;
iPhone6Plus在@3x屏幕尺寸是:750x1334px;对应的@1x,屏幕尺寸就是414x736px;

 

10、iPhone6Plus的@3x,iPhone6的@2x, iPhone5的@2x的介绍
iPhone6Plus是5.5英寸屏幕,1242x2208px-@3x的像素分辨率,逻辑分辨率是414x736pt-@1x。物理尺寸是1080x1920px。这个物理尺寸,也是安卓机目前*流行的大屏设计稿尺寸。
iPhone6是4.7英寸屏幕,750x1334px-@2x的像素分辨率,逻辑分辨率是375x667pt-@1x。
iPhone5是4英寸屏幕,640x1135px-@2x的像素分辨率,逻辑分辨率是320x568pt-@1x。

 

【综上所述】iPhone6的原型规范如下:
界面尺寸布局:满屏尺寸750x1334px
高度电量条高度40px,导航栏高度88px,标签栏高度98px;
各区域图标大小导航栏图标44px,标签栏图标50px;
各区域文字大小电量条文字22px,导航栏-文字32px,标签栏字20px;
常用的文字大小:32px,30px,28px,26px,24px,22px,20px;
常用的颜色:背景浅灰色#f2f2f2,文字深黑色#323232,边框色深灰#CCCCCC;
常用可点击区域的高度:88px;
单行文字的背景框的高度:88px,双行则为:176px,三行则为:264px;
常用间距:亲密距离:20px;疏远距离:30px,其它距离:10px,44px等;
按钮和文本框,原型图做成直角的,圆角半径是多少,由Ui来设计;
这种情况,需要修改原型。单个页面的逻辑流程图或用户学习使用时间,超过其它页面平均数的3倍以上;
以上规范具备如下的一个条件,即可修改:
准备有已经上线的截屏参考页面;
产品和开发协商修改。

不用自学APP开发:零基础也能制作APP软件,兼容iOS

现在,我们总能在手机上发现很多有趣好玩的手机APP,自己也有一些不错的想法,想通过手机APP实现。网站、论坛的访问量越来越小,APP越来越重要。那么如何不用学习安卓APP开发入门教程就能开发一款APP呢?

%title插图%num

通过咨询APP开发公司,我们知道,一款简单的APP开发就需要花费数十万。先不谈APP的推广运营,光是Android APP开发教程就很让人头疼了。通过研究会发现需要掌握的太多,很多人感到很沮丧。

%title插图%num

在这里我还是要推荐下我自己建的iOS开发学习群:680565220,群里都是学ios开发的,如果你正在学习ios
,小编欢迎你加入,今天分享的这个案例已经上传到群文件,大家都是软件开发党,不定期分享干货(只有
iOS软件开发相关的),包括我自己整理的一份2018*新的iOS进阶资料和高级开发教程

我这个和计算机专业不沾边的人,也能开发出一款App,关键是要找对方法。

大学时,我学习的专业是中文,想开发一个校园类的APP,把学校的社团、商家、兼职、家教整合起来,顺便可以共享课程表等。然后去请教计科学校的同学,结果当然被拒,但是给我推荐了一个平台:应用公园APP在线制作平台。零基础也能制作APP软件,兼容iOS,完全不需要自己敲代码。

登录应用公园的网站看了一下APP开发制作原理,才发现这种商业模式太创新了。

应用公园平台有上百个独立的APP的功能,比如图片、文字、聊天、交易、论坛、信息发布、商品交易等等。利用这些开发好的功能,自己就能搭配出手机APP,完全不需要任何的编程,网站有详细的操作教程,只需要对图片文字进行填充就可以了。

%title插图%num

有些小伙伴可能会问,这个功能的搭配难么?刚开始确实一头雾水,不过研究半天,就清晰了,不懂的地方也可以请教他们的客服。

%title插图%num

而且,更为关键是,应用公园提供了上百个制作好的APP模板,可以直接套用,一键完成制作,后期对内容进行填充就可以了。常见的APP比如类似今日头条、贴吧、58同城、美团、淘宝、天猫、得到类的都可以找到多个APP模板。

%title插图%num

在APP的其他方面,比如后台、服务器、发布、上架、下载站点、下载链接等等应用公园提供都提供相关的服务帮助,而且还有很多APP运营的实用小工具推荐。比如制作宣传海报、小视频编辑等等。

据了解,应用公园已经荣获了荣获中国(深圳)科技创投创新大赛一等*,而且被列入九年级的教材,中学生都会制作手机APP了。

万事开头难,APP的制作*步走好了,后面的推广宣传也是水到渠成的事情,希望对大家有帮助。

iOS开发者必备:五大编程类工具

作为一个开发者应该学会去整理收集开发常用的工具类,这些复用的工具可以在项目开发中给你很大程度提高你的工作效率。难道你不想早点完成工作,然后出去撩妹、陪女朋友或者回家陪老婆孩子吗?反正我想早点回家。

iOS10正式版的发布,无疑又掀起一场开发热潮。iOS开发工具多如牛毛,开发者在编写大段代码时,难免会出现结构或语法错误。所以选择一款功能强劲且实用的编程工具对开发者来说可谓是大有裨益。小编收录和整理了对开发者有帮助的5大iOS编程工具,旨在帮助更多的“独立开发者”徜徉iOS 10开发世界。

1. CodeRunner
在这里我还是要推荐下我自己建的iOS开发学习群:680565220,群里都是学ios开发的,如果你正在学习ios ,小编欢迎你加入,今天分享的这个案例已经上传到群文件,大家都是软件开发党,不定期分享干货(只有iOS软件开发相关的),包括我自己整理的一份2018*新的iOS进阶资料和高级开发教程

CodeRunner 是一款轻量级,可以编写和运行多种语言的编辑器,它不需要安装额外的语言环境就可以执行多种语言代码。如果开发者想要测试一段代码或者一个API具体的功能,用Xcode未免过于麻烦,而CodeRunner却恰巧弥补了Xcode在这方面的欠缺。开发者只需要在CodeRunner中编写一个短小的代码片段,即可测试代码和API的具体功能。

此外,CodeRunner能加快开发者的编程速度,所以开发者可以在很短的时间内完成代码编写工作,通过CodeRunner测试无误后,就可以把它拷贝回Xcode的项目中,*大地提高了开发者的工作效率。

CodeRunner的主要功能:

支持多种语言:CodeRunner默认支持AppleScript、C、C++、Java、JavaScript(Node.js)、Lua、Objective-C、Perl、PHP、Python、Ruby和Shell等多种语言

自定义模块:无需写出完整代码即可快速的测试和执行代码片段和API。

强大的的代码编写能力:CodeRunner具有自动补全、句法高亮、多标签页、自定义主题等功能。

%title插图%num

内置控制台

支持Retina显示

支持运行各种类型代码

完全编码支持,自定义运行编译器

2. AppCode
AppCode是全新的Objective-C的IDE集成开发环境,旨在帮助开发者开发Mac OS X和iOS系统的相关应用程序。

AppCode主要功能:

代码补全:AppCode的代码补全并不局限于类、方法或者变量,它甚至还具备了十分可靠的上下文推测能力,假如开发者编写一个从未出现过的变量,AppCode都会询问是否要添加这个变量。这样开发者将有机会避免一些可能无意义的来回跳转,而更加专注于有效代码的编写。

代码检查和修改:AppCode的代码分析是实时进行的,在编写代码的过程中,开发者就能看到存在的问题。另外,AppCode提供了高度可定制的代码风格模版,并可以很简单地将其套用到任何代码上。这样,不论开发者写多少代码,*后产生的代码都是漂亮优雅的。

快速跳转:Xcode的*大问题就是难以定位文件和类,想要寻找一个文件的话,基本上不可能完全用键盘来实现。但是AppCode中添加了高效的导航功能,配合类似于微博的特定符号的搜索,可以完成从文件到类的快速跳转,避免了可能出现的鼠标操作,提高了开发者的工作效率。

AppCode的不足:AppCode同样也有一些不足之处。AppCode在某些情况下需要依赖Xcode,它没有集成Nib编辑器,在打开Nib文件时会自动去开Xcode,Instrument工具也要调用Xcode。

%title插图%num
完整的Xcode的兼容性

即时的代码转换,支持i18n

及时的代码分析、搜索、转换功能

可直接在iOS设备和模拟器上运行

3. Chocolat
Chocolat是Mac系统上*新出现的一款强大的文本编辑器,支持vim模式、多种编程语言、窗口分割、标签页、色彩主题等功能。另外Chocolat具有实时的代码检错功能,它可以在编写代码时检查代码的正确性(目前只支持Ruby、Python、PHP和JavaScript)。

%title插图%num

关键词高亮显示

兼具了Cocoa强大的文本编辑功能

4. Alcatraz
Alcatraz是一个开源的Xcode管理器,可让你更快捷地发现和安装插件、模版和颜色方案。只需要简单地点击或者勾选,而无需手工克隆或拷贝文件。

%title插图%num

5. Mou
对于熟悉Markdown的人来说,Mou是一款不错的文本编辑器(Mac平台)。相比同类型产品,Mou 在功能性和易用性上都有众多特色,在其简单的界面之后,不仅拥有自定义样式、增量搜索、中文字体“漂移”优化,还支持终端命令行启动、内容发布、竖版文字布局,甚至还可以作为任务列表使用,其Markdown双栏布局的实时预览和同步滚动功能更是被众多应用借鉴和采用。

Mou的开发者——罗晨,一个中国开发者可以在国外流行的标准之上做出一款更加优秀的产品,特别是考虑到其并非科班出身,这无疑更加给了国内众多“野生程序员”们莫大的动力。

Mou的主要功能:

实用的代码编写:Mou为开发者提供一系列的快速输入Markdown既定语法元素,减少了开发者在编写代码上所消耗的时间。

实时的代码修正:开发者在Mou的左栏写Markdown代码,右侧就能实时显示输出效果,从而能实时地为开发者检查代码的正确性。

%title插图%num

我们希望在iOS 10中看到什么

腾讯科技讯 6月7日消息,据外电报道,新款iOS是苹果每届WWDC(全球开发者)大会上*令人激动的看点之一。它通常都会推出一些针对开发者和用户的新功能。这种免费的升级程序让我们所有人都可以享受到它的好处。现在,它已推出了公开测试版,它的*终版本要到9月才会推出。下面是我们希望在下一代iOS 10中看到的东西。

1. 改善的键盘导航功能

谷歌(微博)和微软均推出了针对iPhone的键盘。但是,iOS*不灵活的功能就是在这些键盘之间进行切换。我们不指望苹果自己推出像这些键盘一样功能齐全的键盘,但是iOS 10至少可以让我们更方便地在所有这些第三方键盘之间进行切换。

在这里我还是要推荐下我自己建的iOS开发学习群:680565220,群里都是学ios开发的,如果你正在学习ios ,小编欢迎你加入,今天分享的这个案例已经上传到群文件,大家都是软件开发党,不定期分享干货(只有iOS软件开发相关的),包括我自己整理的一份2018*新的iOS进阶资料和高级开发教程

2. Apple Music与iTunes音乐商店分开

有传闻称,苹果将会在WWDC大会上推出全新改版的流媒体音乐服务Apple Music。它的音乐社交功能Connect将会被降级,它的显示音乐专辑封面的功能Album Art将会得到强化。这款应用程序将会选择黑白色的用户界面。但是,Apple Music的*大问题是它与iTunes音乐商店之间的关系太复杂。因此,我们认为,iOS 10应该将这款流媒体音乐服务与iTunes分开。

3. Siri语音邮箱转录文字功能

苹果宣称,在 iOS 9上,智能语音助手Siri已变得更积*了。但是,这款语音助手应用还远不够完善。其中人们*期待它具有的一项功能就是将语音邮箱转录为文字信息。Siri有望*终在iOS 10中推出这项功能,因为如果Siri要与谷歌Assistant和亚马逊Alexa竞争,那么它必须有实质性的改进。

4. 针对应急按钮的Touch ID指纹识别技术

在苹果与FBI摊牌后,苹果就发誓要让iOS 10变得更安全。而*近,又有报道称,苹果可能会利用Touch ID指纹识别技术来激活处于“应急模式”中的iPhone。由于人们*近一直在乐此不疲地谈论入侵iPhone的事情,因此苹果在iOS 10中推出这项更高级的安全功能正当其时。

5. 移动网页上的Apple Pay

另有传闻称,在WWDC大会上可能会出现的一幕是:苹果支付服务Apple Pay将会被植入iOS操作系统的Safari浏览器中。*近有报道称,支持Apple Pay的移动网站将能够让用户使用Touch ID指纹识别技术来验证身份,而不需要输入信用卡信息。这项功能将会让Apple Pay与支付服务PayPal形成直接竞争。PayPal现在也在移动网页上提供这种快捷的认证功能。

6. Instagram支持动态照片功能Live Photos

在苹果推出神奇的动态照片功能Live Photos后不到一年的时间内,它就得到了Facebook、Google Photos和轻微博网站Tumblr的支持。但是,*出名的照片分享服务Instagram却迟迟按兵不动。在WWDC大会上,如果苹果和Instagram宣布合作,那么用户就再也不用通过其他变通的方法将Live Photos动态照片分享到Instagram服务上了。

7. 控制中心定制化

从计算器到照相机,iOS控制中心是用户访问iPhone的*常用功能的地方。但是,随着新的功能,如AirDrop文件传输功能和Night Shift夜光功能,不断被整合到iOS中,控制中心变得越来越拥挤了。因此,苹果应该向Android N学习,让iOS用户根据自己的意愿来定制自己的控制中心,重新安排他们*常用的快捷键,或删除他们从不使用的快捷键。

8. HomeKit控制器应用程序

苹果在发布新款iOS时通常会推出全新的本地应用程序。因此,iOS 10可能会推出正式的HomeKit控制器应用程序。这款新的应用程序将成为你的所有支持HomeKit的设备的中央控制系统。

9. 全新改版的Mail应用

苹果往往还会借推出新款iOS之际对其本地应用程序进行重大升级。例如,iOS 9给我们带来了重大升级的Notes应用。现在,*陈旧的iOS应用程序要数Mail。iOS 10可能会对Mail进行全新改版,植入智能电子邮件技术,如日历整合功能、清空收件箱功能以及智能助手功能。

10. 隐藏不想要的本地应用程序

不管苹果的本地应用程序如何改进,其中总有一些应用程序我们从不会用到,例如有些人就从来不用它的股票应用。事实上,人们*想要的iOS功能就是隐藏不想要的本地应用程序。iOS 10*终可能会推出这种功能。iOS应用程序资源网站AppAdvice刚刚在iTunes元数据中宣布了新的代码,它表明苹果可能很快会让iOS用户隐藏其iPhone上的某些本地应用程序。

11. 苹果新闻应用Apple News的付费订阅服务

在推出一年后,苹果的新闻应用Apple News并未能抗衡竞争对手如Flipboard和其他RSS阅读器。现在,苹果正考虑让iOS 10中的Apple News具备更独特的东西。据*近的一篇报道称,苹果正考虑将Apple News变成出版商如《华尔街日报》的付费订阅中心。

12. iPhone的分屏功能Split View

苹果在iOS 9中推出了针对iPad的很多功能,例如分屏功能Split View,从而大大提高了它的使用效率。现在,人们希望这些功能,如Split View,也能够出现在其他兼容iOS 10的设备中,例如iPhone。尽管iPad的大屏幕可能更适合Split View功能,但是在iPhone上,尤其是在 6 Plus 和6s Plus上,同时使用两款应用程序的功能将会很受欢迎。(乐学)

移动App入侵与逆向破解技术-iOS篇

如果您有耐心看完这篇文章,您将懂得如何着手进行app的分析、追踪、注入等实用的破解技术,另外,通过“入侵”,将帮助您理解如何规避常见的安全漏洞,文章大纲:

  • 简单介绍ios二进制文件结构与入侵的原理
  • 介绍入侵常用的工具和方法,包括pc端和手机端
  • 讲解黑客技术中的静态分析和动态分析法
  • 通过一个简单的实例,来介绍如何综合运用砸壳、寻找注入点、lldb远程调试、追踪、反汇编技术来进行黑客实战
  • 讲解越狱破解补丁和不需越狱的破解补丁制作方法和差别

黑客的素养

  • 敏锐的嗅觉有时候通过一个函数名,一个类名,就能大致的判断出它的作用,这就是嗅觉;功力已臻化境时,甚至可以使用第六感判断出一些注入点
  • 面对失败的勇气破解有时候很耗时,和程序开发正好相反,它耗时不是耗在写代码上,而是耗在寻找注入点和逆向工程上,有可能你花了3天时间去找程序的破绽,但是*终的破解代码可能就2行,不到一分钟就搞定了;但是你也需要做好面对失败的准备,如果路选错了,有可能你这3天完全是在浪费脑细胞
  • 洪荒之力洪荒之力-即入侵过程中需要借助的各种工具,工欲善其事,必先利其器,工具都是前人智慧的结晶,能用工具解决的,*不要手动去搞

iOS黑客关键字

iOS的入侵离不开越狱开发,一切的破解、入侵都是建立在越狱的基础上的,如果没有拿到系统级权限,一切的想法都是空谈了,当然,市面上存在免越狱的破解补丁,但是它的开发过程,也是基于越狱环境的

tweak

在iOS的黑客界,要做破解或越狱开发,就必须了解tweak,它是各种破解补丁的统称,在google上,如果你想搜索一些越狱开发资料或者开源的破解补丁代码,它是*好的关键字。

iOS的tweak大致分为两种:

  • *种是在cydia上发布的,需要越狱才能安装,大部分是deb格式的安装包,iOS在越狱后,会默认安装一个名叫mobilesubstrate的动态库,它的作用是提供一个系统级的入侵管道,所有的tweak都可以依赖它来进行开发,目前主流的开发工具有theos和iOSOpenDev,前者是采用makefile的一个编译框架,后者提供了一套xcode项目模版,可以直接使用xcode开发可调试,但是这个项目已经停止更新了,对高版本的xcode支持不好,大家酌情选择(本文中的例子全部采用theos)
  • 第二种是直接打包成ipa安装包,并使用自己的开发证书或者企业证书签名,不需越狱也可以安装,可直接放到自己的网站上,可实现在线安装;对于没有越狱的手机,由于权限的限制,我们是没有办法写系统级的tweak的,例如springboard的补丁是没法运行的,这种tweak大多是针对某个app,把目标app进行修改注入处理,再重新签名和发布,有点类似于windows软件的xxx破解版、xxx免注册版

没有越狱的机器由于系统中没有mobilesubstrate这个库,我们有二个选择,*个是直接把这个库打包进ipa当中,使用它的api实现注入,第二个是直接修改汇编代码;*个适用于较为复杂的破解行为,而且越狱tweak代码可以复用,第二种适用于破解一些if…else…之类的条件语句

Mobilesubstrate

下面的图展示的就是oc届著名的method swizzling技术,他就是iOS的注入原理,类似于windows的钩子,所以我们注入也称为hook

%title插图%num

Mobilesubstrate为了方便tweak开发,提供了三个重要的模块:

  • MobileHooker 就是用来做上面所说的这件事的,它定义一系列的宏和函数,底层调用objc-runtime和fishhook来替换系统或者目标应用的函数
  • MobileLoader 用来在目标程序启动时根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序,他的原理下面会简单讲解一下
  • Safe mode 类似于windows的安全模式,比如我们写的一些系统级的hook代码发生crash时,mobilesubstrate会自动进入安全模式,安全模式下,会禁用所有的第三方动态库

app注入原理

上面讲到了mobileloader,他是怎么做到把第三方的lib注入进目标程序的呢?这个我们要从二进制文件的结构说起,从下面的图来看,Mach-O文件的数据主体可分为三大部分,分别是头部(Header)、加载命令(Load commands)、和*终的数据(Data)。mobileloader会在目标程序启动时,会根据指定的规则检查指定目录是否存在第三方库,如果有,则会通过修改二进制的loadCommands,来把自己注入进所有的app当中,然后加载第三方库。

%title插图%num

为了让大家看的更清楚,下面我用machoview来打开一个真实的二进制文件给大家看看,可以看出,二进制当中所有引用到的动态库都放在Load commands段当中,所以,通过给这个段增加记录,就可以注入我们自己写的动态库了

%title插图%num

那么问题来了,在这里插入我们自己的动态库有什么用?我们自己写的代码没有执行的入口,我们一样没发干坏事,嗯,恭喜你问到点子上了,我们还需要一个”main”函数来执行我们自己的代码,这个”main”函数在oc里面称为构造函数,只要在函数前声明 “attribute((constructor)) static” 即可,有了它我们就可以发挥想象力,进行偷天换日干点坏事了:

  1. #import <CaptainHook/CaptainHook.h>
  2. CHDeclareClass(AnAppClass);
  3. CHMethod(1, void, AnAppClass, say, id, arg1)
  4. {
  5. NSString* tmp=@“Hello, iOS!”;
  6. CHSuper(1, AnAppClass, say, tmp);
  7. }
  8. __attribute__((constructor)) static void entry()
  9. {
  10. NSLog(@“Hello, Ice And Fire!”);
  11. CHLoadLateClass(AnAppClass);
  12. CHClassHook(1, AnAppClass,say);
  13. }

到这里为止,我们已经知道了怎么在目标程序注入自己的代码,那么我们怎么知道需要hook哪些方法?怎么找到关键点进行实际的破解呢?下面讲一下常见的app入侵分析方法

###iOS逆向分析方法

逆向分析*常用的有三种方法:

  1. 网络分析通过分析和篡改接口数据,可以有效的破解通过接口数据来控制客户端行为的app,常用的抓包工具有Tcpdump, WireShark, Charles等,windows平台有fidller
  2. 静态分析通过砸壳、反汇编、classdump头文件等技术来分析app行为,通过这种方式可以有效的分析出app实用的一些第三方库,甚至分析出app的架构等内容,常用的工具有dumpdecrypted(砸壳)、hopper disassembler(反汇编)、class_dump(导头文件)
  3. 动态分析有静就有动,万物都是相生相克的,动态分析指的是通过分析app的运行时数据,来定位注入点或者获取关键数据,常用的工具有cycript(运行时控制台)、 lldb+debugserver(远程断点调试)、logify(追踪)

demo:微信抢红包插件

上面讲了很多原理性的东西,相信大家已经看的不耐烦了,下面我们一起动点真格的,我们从头开始,一步一步的做一个微信的自动抢红包插件,当然,网上可能已经有相关的开源代码了,但是我这里要讲的是,这些代码是怎么得出来的,我么重点讲一讲分析过程

工欲善其事,必先利其器

一台越狱的手机,并装有以下软件

  • cycript
  • dumpdecrypted
  • debug server
  • openssh

一台苹果电脑,并装有以下软件

  • class_dump
  • Theos
  • Hopper Disassembler v3
  • xcode
  • insert_dylib
  • pp助手

###寻找注入点

砸壳

首先我们要做的就是把微信的壳砸掉,砸壳其实是为了把它的头文件classdump出来,因为从appstore下载的app二进制都是经过加密的,直接进行classdump操作是啥也看不出来的

  • 用pp助手把dumpdecrypted.dylib文件copy到微信的documents目录
  • ssh到手机的终端,cd到documents目录中,执行下面的命令进行砸壳操作
  1. xxx$ cp /usr/lib/dumpdecrypted.dylib /path/to/app/document
  2. xxx$ DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /path/to/WeChat
  • *后砸壳完成后会在documents目录生成砸了壳后的二进制文件,用pp助手copy出来并class-dump他的头文件备用

执行完这几行命令后,会在微信的documents目录生成一个WeChat.decrypted文件,这就是砸壳后的二进制文件;当然了,这一步不是必须的,我们可以直接从91或者pp助手下载一个已经砸过壳的版本

动态分析-cycript

要想实现自动抢红包,我们必须找到收到红包消息的handler方法,怎么入手呢?我们先从界面出发,进入微信的消息首发窗口:

%title插图%num

  • ssh进手机的终端,输入ps命令,查找到微信的进程id
ps aux | grep WeChat
  • 祭起神器cycript,根据上一步找到的pid注入到微信的进程
cycript -p pidxxx
  • 在cycript的终端输入这一串方法,作用就是打印出当前界面的view层级,(cycript还有很多妙用,大家可以上官网看文档,这里不详细介绍)
UIApp.keyWindow.recursiveDescription().toString()

*终的输出如下,内容太多,大家肯定看不清楚,不过没关系,这个不是重点,这里只是展示一下打印的结果形式:

%title插图%num

我们可以随机的选取一个节点不要太靠树叶,也不要太靠树根,例如我选的是标红的部分,把这个节点的内存地址copy出来,这个内存地址,就代表了这个节点的view对象,ios开发的老油条们都知道,通过view的nextResponder方法,可以找出它所属的视图控制器ViewController,所以我么在cycript的控制台中持续输入如下的命令:

%title插图%num

看到没有,通过四个nextResponder方法调用,我么找到了当前聊天窗口的ViewController类名,他就是BaseMsgContentViewController,现在我们缩小了目标范围,下面我们还需要继续缩小范围,要找到具体的消息处理函数才行。

动态分析-Logify

要继续缩小范围,就得祭起神器Logify了,它是theos的一个模块,作用就是根据头文件自动生成tweak,生成的tweak会在头文件的所有方法中注入NSLog来打印方法的入参和出参,非常适合追踪方法的调用和数据传递

现在我们根据此前砸壳后class_dump出来的头文件,找到BaseMsgContentViewController在pc终端执行如下命令:

logify.pl /path/to/BaseMsgContentViewController.h > /out/to/Tweak.xm

输出的tweak文件大概是这个样子的:

%title插图%num

这里带百分号的关键字,例如 %hook、%log、%orig 都是mobilesubstrate的MobileHooker模块提供的宏,其实也就是把method swizzling相关的方法封装成了各种宏标记,使用起来更简单,大家想要更深入了解各种标记,可以google一下logos语言

theos创建tweak

上面我们用logify生成了一个tweak代码,我们要把它安装到手机上,首先需要使用theos进行编译,安装了theos之后,在pc终端输入nic.pl:

%title插图%num

首先选择项目模版当然是tweak啦,然后是项目名称、作者,后面两个选项要注意:

  • 首先是bundle filter,这个需要填你需要注入的目标app的bundle id,MobileLoader模块会根据它来寻找你的tweak的注入目标
  • *后是list id applications to terminate upon installation,这里指定当tweak安装成功之后,需要kill的进程,我们要hook微信,这里就填微信的二进制文件名就可以了,为什么要kill? 因为我么的插件是需要在app启动时加载进去的,如果不重启app,插件是不会生效的

*后一切都完成后,在当前目录会生成下列文件:

%title插图%num

把上面logify生成的tweak文件覆盖到当前目录,并用文本编辑器打开makefile文件,在文件的开头增加你的ios设备的ip地址和ssh端口:

%title插图%num

*后在pc终端进入项目目录,输入 make package install 命令:

%title插图%num

期间会让你输入设备的ssh密码,越狱机器的默认ssh密码是alpine,make命令会生成deb安装包,放在debs目录,我们如果想对外发布自己的插件,可以把生成的安装包上传到cydia即可

安装成功后再次进入微信的聊天界面,并使用另外一个微信在群里发个普通消息,连接xcode打开越狱机器控制台,查看输出,会发现有类似下面的输出:

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=1037957572@chatroom, m_uiStatus=1, type=1, msgSource="(null)"}  layout:1 addMoreMsg:0]

看出来了吧,消息处理函数是BaseMsgContentViewController的**addMessageNode:layout:addMoreMsg:**方法,大家可以看出,方法的参数内容也打印出来了


动态分析-lldb

到目前为止,我么已经把范围缩小到了具体的函数,看起来注入点已经找到了,但是请大家思考一下,如果我们在这个函数中注入抢红包逻辑,那我们的tweak会不会有什么致命的缺陷?

是的,因为BaseMsgContentViewController这个类是微信群聊天窗口对应的controller,我么必须进入到群的聊天界面,这个类才会创建,如果不进入聊天窗口,我们的插件就不生效了,而且,即使进入聊天窗口,也只是能自动枪当前群的红包而已,其他群就无能为力了,是不是有点low?

所以为了使我们的插件显得上流一些,我么还要继续追根溯源,寻找消息的源头,这里就用到了lldb远程调试,使用lldb打断点的方式,通过调用栈,我们可以就可以看到当消息来到时,方法的调用顺序,找到*先执行的消息处理函数。

要在刚刚追踪到的**addMessageNode:layout:addMoreMsg:**方法中打断点,首先我们得知道它在运行时的内存地址,那么内存地址怎么来呢?有这么一个公式:

  • 内存地址=进程内存基地址+函数在二进制中的偏移量

首先偏移量我们可以通过反汇编工具hooper来查,在pc上用hooper打开微信的二进制文件(注意,打开时会让你选择armv7或者arm64,这需要根据你越狱手机的cpu类型来选,一定要和你的手机一致),hooper的界面非常简洁,左侧有个搜索框,可以输入函数名,直接找到函数在二进制中的位置

%title插图%num

通过左侧的搜索框搜addMessageNode关键字,找到它的偏移量是0x00000001017d7c6c:

%title插图%num

找到了偏移量,还需要进程的基地址,这个地址需要连lldb,所以下面讲一下如何连接lldb进行远程调试,先ssh进越狱手机的终端,在终端输入如下命令(注意,你的手机必须连xcode调试过才会有这个命令):

debugserver *:19999 -a WeChat

然后在pc端新起一个终端窗口,输入如下命令来连接手机端进行调试:

lldb  ->  process connect connect://deviceIP:19999

如果连接成功,会进入lldb的控制台,我们在lldb的控制台输入如下命令来获取微信进程的基地址:

image list -o -f

执行这个命令会打印很多行数据,像下面图中这样,我么要找到微信的二进制文件所在的行,记录它的内存地址0X00000000000E800:

%title插图%num

到这里我们两个地址都找到了,再通过br命令打断点:

br s -a '0X00000000000E800+0x00000001017d7c6c'

打好断点后继续向群里面发消息,我们会发现进程被断掉了,这时输入bt指令,就可以看到当前的调用栈,就像下图这样:

%title插图%num

分析堆栈的时候,重点找出模块时WeChat的项,这些都是微信模块的方法调用,有了堆栈,我们需要根据堆栈的内存地址找出它的具体函数名,思路还是先根据上面讲到的公式来计算出栈地址在二进制中的偏移量,然后用hooper找到偏移量对应的函数名

  • 函数在二进制中的偏移量=内存地址 – 进程内存基地址

例如根据箭头所指的内存地址和刚刚得到的进程基地址,计算偏移量:

0x0000000101ad02f4  0x00000000000e8000 = 1019E82F4

然后在hooper中搜索这个地址,得到结果如下:

%title插图%num

*终把所有的栈都进行还原,得出调用栈是这个样子的:

  1. -[CMessageMgr MainThreadNotifyToExt:]:
  2. –>
  3. -[BaseMsgContentLogicController OnAddMsg:MsgWrap:]:
  4. ——>
  5. -[RoomContentLogicController DidAddMsg:]
  6. ———->
  7. -[BaseMsgContentLogicController DidAddMsg:]
  8. —————->
  9. -[BaseMsgContentViewController addMessageNode:layout:addMoreMsg:]:

CMessageMgr这个类浮出水面了,是时候发挥黑客的嗅觉了,根据方法名我们能判断出MainThreadNotifyToExt:这个方法仅仅是用来发送通知的,如果hook这个方法,我们是拿不到消息内容的

由于这里可能是一个异步调用,用断点的方式,可能已经打印不出来栈信息了,所以还得使用logify来继续追踪CMessageMgr这个类,讲过的内容我就不重复了,直接得到*终的消息处理函数:

-(void)AsyncOnAddMsg:(id)message MsgWrap:(CMessageWrap* )msgWrap 

实现“抢”的动作

上一节我们已经找到了hook的关键点,那么该如何去实现抢的动作?同样我们需要结合动态分析和静态分析,首先得到红包消息体的数据特征,然后再分析处理消息的关键点

数据包分析

首先我们的代码需要分辨哪些才是红包消息,方法很简单,用logify追踪BaseMsgContentViewController,然后向微信群发一个红包,观察手机日志输出,我们可以看出消息的数据结构中有个type字段,值是49,这个type应该就是标记消息类型的,如果不确定,可以再发个图片或者文本之类的消息,这个值是不同的:

  1. AdministratordeiPhone WeChat[47410] <Notice>: [1;36m[WxMsgPreview] [m[0;36mTweak.xm:308[m [0;30;46mDEBUG:[m -[<BaseMsgContentViewController: 0x15e0c9a00> addMessageNode:{m_uiMesLocalID=16, m_ui64MesSvrID=1452438635530425509, m_nsFromUsr=1037957572@chatroom, m_nsToUsr=ccg*675~9, m_uiStatus=4, type=49, msgSource=<msgsource>
  2. <silence>0</silence>
  3. <membercount>3</membercount>
  4. </msgsource>
  5. } layout:1 addMoreMsg:0]

现在我们能分辨消息类型了,重点来了,怎么实现抢这个事呢,可能聪明人已经猜到了,从ui入手,先找到微信本身的抢红包函数,我们自己来给它构造参数并调用他不就行了?

%title插图%num

把红包点开后,用cycript打印出当前view的层次,就像下面这个,一眼就可以看到重点,WCRedEnvelopesReceiveHomeView就是开红包弹框的类名

%title插图%num

知道类名后,用cycript追踪它,点击开红包,在日志中找到了下图中的内容,从名字来看,这是一个事件处理函数,我们现在要做的,就是把他还原成oc代码,真正实现抢红包功能

Administratorde-iPhone WeChat[91173] <Notice>: [1;36m[WxMsgPreview] [m[0;36mTweak.xm:8[m [0;30;46mDEBUG:[m -[<WCRedEnvelopesReceiveHomeView: 0x13cdda8c0> OnOpenRedEnvelopes]

静态分析法

怎么把他还原成oc代码,真正实现抢红包功能呢?还得借助一点点汇编技能,只是一点点而已,因为现在的反汇编工具已经很强大了,我们不需要挨个去看寄存器了

在pc上用hooper打开微信的二进制文件,搜索OnOpenRedEnvelopes,查看汇编代码,注意在图片中*后一行调用了一个WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes函数

%title插图%num%title插图%num


继续搜索WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes这个方法,找到它的汇编代码

  • 首先他不知道从哪里获取了一个payinfoitem
  • 然后又获取了payinfo的m_c2cNativeUrl属性
  • 然后调用substringfromindex吧navtiveurl的前缀截断,并调用bizutil的一个方法把url参数转换成了一个字典

%title插图%num

*终反解出的代码如下,是不是很简单?

  1. NSString *nativeUrl = [[msgWrap m_oWCPayInfoItem] m_c2cNativeUrl];
  2. nativeUrl = [nativeUrl substringFromIndex:[@"wxpay://c2cbizmessagehandler/hongbao/receivehongbao?" length]];
  3. NSDictionary *nativeUrlDict = [%c(WCBizUtil) dictionaryWithDecodedComponets:nativeUrl separator:@"&"];

继续往下看, 在这里前面三行创建了一个mutable dictionary:

  • 紧接着下面三个框框处都是调用了setobject:forkey:向里面填东西,那填的东西是啥呢?
  • 其实这里已经可以看的很清楚了,*个key是msgtype,值是字符串1,第二个sendid,值是调用了一个objectforkey从另一个字典中取出来的,很显然,另一个字典就是上面从url解析得到的,后面的channelid也是同样的道理

%title插图%num

*终得到的代码如下:

  1. NSMutableDictionary *args = [[%c(NSMutableDictionary) alloc] init];
  2. [args setObject:nativeUrlDict[@"msgtype"] forKey:@"msgType"];
  3. [args setObject:nativeUrlDict[@"sendid"] forKey:@"sendId"];
  4. [args setObject:nativeUrlDict[@"channelid"] forKey:@"channelId"];

继续往下看从箭头所指的几处,我们可以看见,它的代码是这样的,共分为四步

  • *个箭头调用了mmservicecenter的defaultcenter方法来获取mmservicecenter实例
  • 第二个箭头调用了CContactMgr的class方法
  • 第三个箭头调用了*步获取的mmservicecenter实例的getservice方法,而这个方法是把第二步得到的class作为参数
  • 第四个箭头很明白了吧,第三步得到了CContactMgr实例,这里就是调用CContactMgr实例的getselfcontact方法获取自己的账户资料

%title插图%num

*终还原的到的代码如下:

  1. CContactMgr *contactManager = [[%c(MMServiceCenter) defaultCenter] getService:[%c(CContactMgr) class]];
  2. CContact *selfContact = [contactManager getSelfContact];

继续往下看,这里使用刚刚得到的selfcontact来获取displayname和headimgurl,并把它们设置到刚刚的字典里面了,key分别是nickname和headimg

%title插图%num

*终的代码:

  1. [args setObject:[selfContact getContactDisplayName] forKey:@"nickName"];
  2. [args setObject:[selfContact m_nsHeadImgUrl] forKey:@"headImg"];

接着看,接下来这两段就比较蛋疼了,完全是从内存地址里面取的值,我也不知道他从哪里来,怎么办呢?有没有不懂汇编就能搞定它的捷径呢,答案是有!

  • 对于*个,我可以通过它的key猜出来,还记得*开始的时候我们取过payinfo的一个nativeurl属性吧,我们姑且把他传进去
  • 对于第二个,我们可以猜测sessionUserName大概是会话名称,也就是群名称的意思,从哪里取这个值呢?我们先把也设置成伪代码

%title插图%num

*终的结果如下:

  1. [args setObject:nativeUrl forKey:@“nativeUrl”];
  2. [args setObject:xxx forKey:@“sessionUserName”];

继续往下看,接下来这一段还是用mmservicecenter来获取WCRedLogicMgr对象,然后调用WCRedLogicMgr的open方法来拆红包,可以想象open方法的参数就是上面我们辛苦组装的字典

%title插图%num

代码如下:

[[[%c(MMServiceCenter) defaultCenter] getService:[%c(WCRedEnvelopesLogicMgr) class]] OpenRedEnvelopesRequest:args];

领红包逻辑

到这里,我们再总结一下我们上面分析的过程…

  • 得到m_oWCPayInfoItem属性
  • 解析m_oWCPayInfoItem的m_c2cNativeUrl属性
  • 得到selfcontact
  • 组装相关参数
  • 调用OpenRedEnvelopesRequest:领取红包

*终的抢红包代码合并起来如下:

  1. #import “WxMsgPreview.h”
  2. %hook CMessageMgr
  3. -(void)AsyncOnAddMsg🙁id)message MsgWrap🙁CMessageWrap* )msgWrap {
  4. %log;
  5. %orig;
  6. if(msgWrap.m_uiMessageType == 49){
  7. CContactMgr *contactManager = [[%c(MMServiceCenter) defaultCenter] getService:[%c(CContactMgr) class]];
  8. CContact *selfContact = [contactManager getSelfContact];
  9. if ([msgWrap.m_nsContent rangeOfString:@“wxpay://c2cbizmessagehandler/hongbao/receivehongbao”].location != NSNotFound) { // 红包
  10. NSString *nativeUrl = [[msgWrap m_oWCPayInfoItem] m_c2cNativeUrl];
  11. nativeUrl = [nativeUrl substringFromIndex:[@“wxpay://c2cbizmessagehandler/hongbao/receivehongbao?” length]];
  12. NSDictionary *nativeUrlDict = [%c(WCBizUtil) dictionaryWithDecodedComponets:nativeUrl separator:@“&”];
  13. NSMutableDictionary *args = [[%c(NSMutableDictionary) alloc] init];
  14. [args setObject:nativeUrlDict[@“msgtype”] forKey:@“msgType”];
  15. [args setObject:nativeUrlDict[@“sendid”] forKey:@“sendId”];
  16. [args setObject:nativeUrlDict[@“channelid”] forKey:@“channelId”];
  17. [args setObject:[selfContact getContactDisplayName] forKey:@“nickName”];
  18. [args setObject:[selfContact m_nsHeadImgUrl] forKey:@“headImg”];
  19. [args setObject:nativeUrl forKey:@“nativeUrl”];
  20. [args setObject:msgWrap.m_nsFromUsr forKey:@“sessionUserName”];
  21. [[[%c(MMServiceCenter) defaultCenter] getService:[%c(WCRedEnvelopesLogicMgr) class]] OpenRedEnvelopesRequest:args];
  22. }
  23. }
  24. }
  25. %end

刚才说了,有两个疑难点没有解决:

  • *:我们不知道payinfo是哪里来的,
  • 第二:sessionusername我们也不知道是哪里来的

这时候我们可以从我们注入点的参数入手,首先用logify打印出addmsg方法的参数信息,会发现,它的第二个参数刚好有一个payinfo的属性,这样*个问题迎刃而解了

第二个我们已经猜测到它代表群名称,所以我们从修改几次群名称,然后再观察logify打印出的参数值的变化,就可以确认出从哪里取了

通过一番折腾,得出了抢红包的核心代码,再结合上面章节所讲的theos制作tweak包的方法,打包并安装到手机,发个红包试试,是不是秒抢?


免越狱插件

检查依赖项

如果设备没有越狱,是没有mobilesubstrate等环境的,而且一些系统目录是没有读写权限的,这时我么只能从目标app的二进制文件入手,通过手动修改load commands来加载自己的dylib,那么上面我们的插件又是使用theos基于mobilesubstrate编译的,有没有办法确定我们的dylib有没有依赖其他的库呢?

使用osx自带的otool工具即可,可以看出,我们的lib是依赖于substrate库的,其他的都是系统库,所以我们从越狱设备中把cydiasubstrate文件copy出来重命名为libsunstrate.dylib,和我们的dylib一起放入wechat.app目录中

*后使用install_name_tool命令修改动态库的路径把它指向app二进制文件的同级目录

%title插图%num

制作安装包

解决了依赖问题,然后要把我们的库注入到二进制weixin的二进制文件,这一步使用开源的insert_dylib即可 (@executable_path是一个环境变量,指的是二进制文件所在的路径)

insert_dylib命令格式:./insert_dylib 动态库路径 目标二进制文件

  1. //注入动态库
  2. ./insert_dylib [@executable_path](/user/name/executable_path)/wxmsgpreview.dylib WeChat
  3. //打包成ipa
  4. xcrun sdk iphoneos PackageApplication v WeChat.app o ~/WeChat.ipa

*后使用用企业证书或者开发证书签名对ipa重新签名,就可以放到自己的渠道进行发布了!

 

蓝牙4.0和蓝牙多通道

蓝牙4.0和蓝牙多通道
1.首先连接蓝牙的时候需要在配置文件manifest中增加权限

1 <uses-permission android:name=”android.permission.BLUETOOTH” />
2 <uses-permission android:name=”android.permission.BLUETOOTH_ADMIN” />

这个时候我们才算正式开始进入主题

2.判断你的移动设备是否支持蓝牙
如果你想声明你的应用程序只能在支持BLE的设备上运行,可以将下面声明包含进你的应用程序manifest文<uses-feature android:name=”android.hardware.bluetooth_le” android:required=”true”/>
想让你的应用程序也能够在不支持BLE的设备上运行,你就应该将上面标签中的属性设置为required=”false”。然后在运行的过程中使用PackageManager.hasSystemFeature()方法来判断设备是否支持BLE:

PackageManager.hasSystemFeature()方法来判断设备是否支持BLE:

1  if(!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)){
2  Toast.makeText(this, “没有可提供的蓝牙设备”, Toast.LENGTH_SHORT).show();
3  finish();
4  }

3.获取蓝牙适配器 BluetoothAdapter
先声明 private BluetoothAdapter mBluetoothAdapter;
然后

1  final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
2  mBluetoothAdapter = bluetoothManager.getAdapter();

1  //如果蓝牙设配器里面没有数据的话就让它返回if(mBluetoothAdapter == null){
2  Toast.makeText(this, “没有可提供的蓝牙设备”, Toast.LENGTH_SHORT).show();
3  finish();
4  return;
5  }

4.搜索蓝牙

/**
* 1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
*/
//我这里使用了需要连接多台蓝牙设备 所有放在list里面
private LinkedList mDeviceContainer = new LinkedList();

private ArrayList mDeviceList = new ArrayList();

1 private void scanLeDevice(final boolean enable){
2         if(enable){
3             new Thread(new Runnable() {
4
5                 @Override
6                 public void run() {
7                     try {
8                         Thread.sleep(SCAN_PERIOD);
9                         if(mScanning){   //10秒钟后停止搜索
10                             mScanning = false;  //状态标记为false
11
12                             mBluetoothAdapter.stopLeScan(mLeScanCallback);
13                             invalidateOptionsMenu();
14                         }
15
16                     } catch (InterruptedException e) {
17
18                         e.printStackTrace();
19                     }
20
21                 }
22             }).start();
23
24             mScanning = true;
25             mBluetoothAdapter.startLeScan(mLeScanCallback); //开始搜索
26         }else {
27             mScanning = false;
28             mBluetoothAdapter.stopLeScan(mLeScanCallback);  //停止搜索
29         }
30
31         invalidateOptionsMenu();
32 }

5.搜到设备的时候有一个回调接口

1 private BluetoothAdapter.LeScanCallback mLeScanCallback = new LeScanCallback() {
2
3         @Override
4         public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
5             runOnUiThread(new Runnable() {
6
7                 @Override
8                 public void run() {
9                     if(!mDeviceContainer.isEmpty()){
10                         if(!isEquals(device)){
11                             connectBle(device);
12                         }
13                     }else{
14                         connectBle(device);
15                     }
16
17                 }
18             });
19
20         }
21     };

6.启用蓝牙模块

1 private final ServiceConnection mServiceConnection = new ServiceConnection() {
2
3     @Override
4     public void onServiceDisconnected(ComponentName name) {
5
6         mBluetoothLeService = null;
7     }
8
9     @Override
10     public void onServiceConnected(ComponentName name, IBinder service) {
11         mBluetoothLeService = ((TemperatureServices.LocalBinder)service).getService();
12         if(!mBluetoothLeService.initialize()){
13             Log.e(TAG, “Unable to initialize Bluetooth”);
14             finish();
15         }
16         Log.e(TAG, “mBluetoothLeService is okay”);
17     }
18 };
19
20 /**
21 *接收从BlutoothLeService发送过来的广播
22 包扣 连接 未连接 以及发送过来的数据
23 */
24 private final BroadcastReceiver mGattUpdateRecevicer = new BroadcastReceiver() {
25
26     @Override
27     public void onReceive(Context context, Intent intent) {
28         final String action = intent.getAction();
29         Bundle extras = intent.getExtras();
30
31         if(TemperatureServices.ACTION_GATT_CONNECTED.equals(action)){
32             Log.i(TAG, “Only gatt, just wait”);
33         }else if(TemperatureServices.ACTION_GATT_DISCONNECTED.equals(action)){
34             if(!mDeviceList.isEmpty()){
35                 String strAddress = intent.getStringExtra(“DEVICE_ADDRESS”);
36                 if(removeDevice(strAddress)){
37                     int deviceNum = mDeviceList.size() – 1;
38                     numDevice.setText(deviceText+deviceNum);
39                 }
40             }
41
42             invalidateOptionsMenu();
43         }else if(TemperatureServices.ACTION_GATT_SERVICES_DISCOVERED.equals(action)){
44             if(!mDeviceContainer.isEmpty()){
45                 String strAddress =intent.getStringExtra(“DEVICE_ADDRESS”);
46
47                 for(BluetoothDevice bluetoothDevice:mDeviceContainer){
48                     if(bluetoothDevice.getAddress().equals(strAddress)){
49                         mDeviceList.add(bluetoothDevice);
50                     }
51                 }
52             }
53             numDevice.setText(deviceText+mDeviceList.size());
54
55             Log.e(TAG, “Discover GATT Services”);
56             invalidateOptionsMenu();
57
58         }else if(TemperatureServices.ACTION_DATA_AVAILABLE.equals(action)){
59             Log.i(TAG, “ACTION_DATA_AVAILABLE”);
60             String data = intent.getStringExtra(TemperatureServices.EXTRA_DATA_TEMP);
61             if(extras.containsKey(TemperatureServices.EXTRA_DATA_TEMP)){
62
63                 if (data != null) {
64                     if (mDataField.length() > 500) {
65                         mDataField.setText(“”);
66                     }
67                     mDataField.append(data);
68                     Log.i(“==temp_data==”, data); // 打印温度
69                 }
70             }else if (extras.containsKey(TemperatureService.EXTRA_DATA)) {
71                 // 如果是蓝牙的读取通知到显示
72
73             }
74         }
75
76
77
78     }
79 };
1 /**
2      * 连接蓝牙
3      * @param device
4      */
5     private void connectBle(BluetoothDevice device){
6         mDeviceContainer.add(device);
7         while (true) {
8             if(mBluetoothLeService!=null){
9                 mBluetoothLeService.connect(device.getAddress(), this);
10                 break;
11             }else{
12                 try{
13                     Thread.sleep(250);
14
15                 }catch(InterruptedException e){
16                     e.printStackTrace();
17                 }
18             }
19
20         }
21     }

7.*主要的 服务
从上面代码中我们可以看到 有一个mBluetoothLeSerivice 它正是我们创建的BluetoothLeService他继承了Service。接下来这个类我详细讲解。

1 public class BluetoothLeService extends Service{
2     private static final String TAG=”BluetoothLeService”;  //这里是用来打印的
3     private String mBluetoothDeviceAddress;  //蓝牙地址
4     private BluetoothManager mBluetoothManager; //通过BluetoothManager来获取BluetoothAdapter
5     private BluetoothGatt mBluetoothGatt; //通过BluetoothGatt可以连接设备(connect),发现服务(discoverServices),并把相应地属性返回到BluetoothGattCallback
6
7 //常用的广播
8     public final static String ACTION_GATT_CONNECTED           = “com.example.bluetooth.le.ACTION_GATT_CONNECTED”;   //连接
9     public final static String ACTION_GATT_DISCONNECTED        = “com.example.bluetooth.le.ACTION_GATT_DISCONNECTED”;  //断开链接
10     public final static String ACTION_GATT_SERVICES_DISCOVERED = “com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED”; //发现设备
11     public final static String ACTION_DATA_AVAILABLE           = “com.example.bluetooth.le.ACTION_DATA_AVAILABLE”;    //官方的demo里面有这四个
12     //这2个是我自己需要读取的所以增加了
13     public final static String EXTRA_DATA                      = “com.example.bluetooth.le.EXTRA_DATA”;    //有数据的时候传输数据
14
15     public final static String EXTRA_DATA_TEMP            = “com.example.bluetooth.le.EXTRA_DATA_TEMP”;  //温度类型变量
16
17     private BluetoothAdapter mBluetoothAdapter = null;
18
19     private ArrayList<BluetoothGatt> connectionQueue = new ArrayList<BluetoothGatt>();  //创建一个BluetoothGatt的队列
20     //相当于一个数据类型,它包括一个value和0~n个value的描述(BluetoothGattDescriptor)
21     public BluetoothGattCharacteristic mNotifyCharacteristic;
22
23     private Context mContext; //上下文
24
25     //查找服务
26     public void findService(BluetoothGatt gatt){
27         List<BluetoothGattService> gattservices = gatt.getServices();
28         Log.i(TAG,”Count is:”+gattservices.size());
29         for(BluetoothGattService gattservice : gattservices){
30             Log.i(TAG,gattservice.getUuid().toString());
31             //如果服务要等于温度服务
32             if(gattservice.getUuid().toString().equalsIgnoreCase(GattAttributes.HEALTH_THERMO_SERVICE)){
33                 List<BluetoothGattCharacteristic> gattCharacteristics = gattservice.getCharacteristics();
34                 Log.i(TAG,”Count is:”+gattCharacteristics.size());
35
36
37                 for(BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics){
38                     String uuidchara = gattCharacteristic.getUuid().toString();
39                     //如果是温度计的类型的话
40                     /**
41                     注意:这里的GattAttributes 这个值相当于
42                     //实际用你们自己的
43                     public static final String HEALTH_TEMP_MEASUREMENT = “0000xxxx-0000-1000-8000-00805f9b34fb”;
44                     */
45                     if(uuidchara.equalsIgnoreCase(GattAttributes.TEMPERATURE_TYPE)){
46                         prepareBroadcastDataRead(gattCharacteristic);
47                     }
48
49                     //如果是温度计的度数的时候
50                 else if(uuidchara.equalsIgnoreCase(GattAttributes.HEALTH_TEMP_MEASUREMENT)){
51                         prepareBroadcastDataIndicate(gattCharacteristic);
52
53                         //发现服务获取设备的地址
54                         brocastUpdate(ACTION_GATT_SERVICES_DISCOVERED, gatt.getDevice().getAddress());
55                     }
56                 }
57             }
58         }
59
60
61     }
62
63     /**
64      * *重要的回调
65      */
66     private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
67
68         @Override
69         public void onConnectionStateChange(BluetoothGatt gatt,int status,int newState){
70             String initAction;
71             Log.i(TAG,”oldStatus = “+status+”NewStates”+newState); //打印出来
72             if(status == BluetoothGatt.GATT_SUCCESS){ //这个值一般返回的为0
73                 //如果连接成功
74                 if(newState == BluetoothProfile.STATE_CONNECTED){
75                     initAction = ACTION_GATT_CONNECTED;
76
77                     brocastUpdate(initAction);
78
79                     //连接之后马上去发现服务
80                     gatt.discoverServices();
81                 }
82             }else if(newState == BluetoothProfile.STATE_DISCONNECTED){ //断开连接
83                 initAction = ACTION_GATT_DISCONNECTED;
84
85                 brocastUpdate(initAction, gatt.getDevice().getAddress());
86             }
87
88         }
89
90         @Override
91         public void onServicesDiscovered(BluetoothGatt gatt, int status) {
92             if(status == BluetoothGatt.GATT_SUCCESS){
93                 Log.w(TAG, “onServicesDiscovered received: ” + status);
94                 findService(gatt);
95             }else{
96                 if(gatt.getDevice().getUuids() == null){
97                     Log.w(TAG, “onServicesDiscovered received: ” + status);
98                 }
99             }
100         };
101         //这个是读取数据
102         @Override
103         public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
104             if(status == BluetoothGatt.GATT_SUCCESS){
105                 brocastUpdate(ACTION_DATA_AVAILABLE, characteristic);
106             }
107         };
108
109         //当数据发生改变的时候
110         @Override
111         public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
112
113             brocastUpdate(ACTION_DATA_AVAILABLE, characteristic);
114
115         };
116         //写入数据
117         @Override
118          public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
119
120             if (status == BluetoothGatt.GATT_SUCCESS) {
121              System.out.println(“onDescriptorWriteonDescriptorWrite = ” + status + “, descriptor =” + descriptor.getUuid().toString());
122             }
123           };
124     };
125
126
127
128
129     /**
130      * 接收广播可读的特征
131      */
132     public void prepareBroadcastDataRead(BluetoothGattCharacteristic gattCharacteristic){
133         if((gattCharacteristic.getProperties()|BluetoothGattCharacteristic.PROPERTY_READ)>0){
134             readCharacteristic(gattCharacteristic); //当特征为可读的特征的时候就直接可以读取
135         }
136     }
137
138     /**
139      * 接收广播通知特性
140      */
141     public void prepareBroadcastDataIndicate(BluetoothGattCharacteristic gattCharacteristic){
142         if((gattCharacteristic.getProperties()|BluetoothGattCharacteristic.PROPERTY_INDICATE)>0){
143             setCharacteristicIndication(gattCharacteristic,
144                     true);  //当特征为不可读的时候哪么就是为通知
145         }
146     }
147
148     /**
149      * 处理连接蓝牙后进入这里面操作
150      */
151     private final IBinder mBinder = new LocalBinder();
152
153     public class LocalBinder extends Binder{
154         public TemperatureServices getService(){
155             return TemperatureServices.this;
156         }
157     }
158
159
160     @Override
161     public IBinder onBind(Intent intent) {
162         // TODO Auto-generated method stub
163         return mBinder;
164     }
165
166     @Override
167     public boolean onUnbind(Intent intent) {
168         // After using a given device, you should make sure that BluetoothGatt.close() is called
169         // such that resources are cleaned up properly.  In this particular example, close() is
170         // invoked when the UI is disconnected from the Service.
171         close();
172         return super.onUnbind(intent);
173     }
174
175
176     /**
177      * 对外事件处理类
178      * 更新广播事件
179      */
180     private void brocastUpdate(final String action){
181         final Intent intent = new Intent(action);
182         sendBroadcast(intent);
183     }
184
185     /**
186      * 传地址广播
187      * @param action
188      * @param strAddress
189      */
190     private void brocastUpdate(final String action,final String strAddress){
191         final Intent intent = new Intent(action);
192         intent.putExtra(“DEVICE_ADDRESS”, strAddress);
193         sendBroadcast(intent);
194     }
195
196     private void brocastUpdate(final String action,final BluetoothGattCharacteristic characteristic){
197         final Intent intent = new Intent(action);
198
199         //当温度计为温度类型的时候
200         if(characteristic.getUuid().equals(UUIDDatabase.UUID_HEALTH_THERMOMETER_SENSOR_LOCATION)){
201             String a = HTMParser.getHealthThermoSensorLocation(characteristic, mContext);
202             intent.putExtra(EXTRA_DATA, a);
203         }
204
205         else if(characteristic.getUuid().equals(UUIDDatabase.UUID_HEALTH_THERMOMETER)){
206             String health_temp = HTMParser.getHealthThermo(characteristic, mContext);
207             intent.putExtra(EXTRA_DATA_TEMP, health_temp);
208         }
209
210         sendBroadcast(intent);
211     }
212
213     /**
214      * 初始化一个参考本地蓝牙适配器
215      * @return
216      */
217     public boolean initialize(){
218         if(mBluetoothManager == null){
219             mBluetoothManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
220             if(mBluetoothManager == null){
221                 Log.e(TAG,”Unable to initialize BluetoothManager.”);
222                 return false;
223             }
224         }
225
226         mBluetoothAdapter = mBluetoothManager.getAdapter();
227         if(mBluetoothAdapter == null){
228             Log.e(TAG, “Unable to obtain a BluetoothAdapter.”);
229             return false;
230         }
231
232         return true;
233     }
234
235
236     /**
237      * 连接代码
238      * @param address
239      * @param context
240      * @return
241      */
242     public boolean connect(final String address,Context context){
243         mContext = context;
244         if(mBluetoothAdapter == null && address == null){
245              Log.w(TAG, “BluetoothAdapter not initialized or unspecified address.”);
246              return false;
247         }
248
249         BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
250
251         if(device == null){
252             Log.w(TAG,”Device not found.  Unable to connect.”);
253             return false;
254         }
255
256         BluetoothGatt bluetoothGatt;
257
258         bluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
259         if(checkGatt(bluetoothGatt)){
260             connectionQueue.add(bluetoothGatt);
261         }
262
263         Log.d(TAG, “Trying to create a new connection.”);
264
265         return true;
266
267
268     }
269
270     /**
271      * 检查是否加入了相同的特征 如果相同就移除掉
272      * @param bluetoothGatt
273      * @return
274      */
275     private boolean checkGatt(BluetoothGatt bluetoothGatt){
276         if(!connectionQueue.isEmpty()){
277             for(BluetoothGatt btg:connectionQueue){
278                 if(btg.equals(bluetoothGatt)){
279                     return false;
280                 }
281             }
282         }
283
284         return true;
285     }
286
287     /**
288      * 断开蓝牙连接 一按断开是全部都断开
289      */
290     public void disConnect(){
291         if(mBluetoothAdapter == null && connectionQueue.isEmpty()){
292             Log.w(TAG, “BluetoothAdapter not initialized”);
293             return;
294         }
295
296         for(BluetoothGatt bluetoothGatt:connectionQueue){
297             bluetoothGatt.disconnect();
298         }
299     }
300
301     public void close(){
302         if(connectionQueue.isEmpty()){
303             return;
304         }
305
306         listClose(null);
307     }
308
309     /**
310      * 清除连接蓝牙的数据
311      * 我这里用的是连接多台的设备,所以清除的时候需要一个一个的移除掉
312      * @param gatt
313      */
314     private synchronized void listClose(BluetoothGatt gatt){
315         if(!connectionQueue.isEmpty()){
316             if(gatt!=null){
317                 for(final BluetoothGatt bluetoothGatt:connectionQueue){
318                     if(bluetoothGatt.equals(gatt)){
319                         bluetoothGatt.close();
320
321                         new Thread(new Runnable() {
322
323                             @Override
324                             public void run() {
325                                 try{
326                                     Thread.sleep(250);
327                                     connectionQueue.remove(bluetoothGatt);
328
329                                 }catch(Exception ex){
330                                     ex.printStackTrace();
331                                 }
332
333                             }
334                         }).start();
335                     }
336                 }
337             }else{
338                 for(BluetoothGatt bluetoothGatt:connectionQueue){
339                     bluetoothGatt.close();
340                 }
341
342                 connectionQueue.clear();
343             }
344         }
345     }
346
347     /**
348      * 读取蓝牙数据
349      */
350     public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
351         if (mBluetoothAdapter == null || connectionQueue.isEmpty()) {
352             Log.w(TAG, “BluetoothAdapter not initialized”);
353             return;
354         }
355         for (BluetoothGatt bluetoothGatt : connectionQueue) {
356             bluetoothGatt.readCharacteristic(characteristic);
357         }
358     }
359
360      /**
361      * Enables or disables notification on a give characteristic.
362      *
363      * @param characteristic Characteristic to act on.
364      * @param enabled If true, enable notification.  False otherwise.
365      */
366     public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
367                                               boolean enabled) {
368         if (mBluetoothAdapter == null || connectionQueue.isEmpty()) {
369             Log.w(TAG, “BluetoothAdapter not initialized”);
370             return;
371         }
372
373         for(BluetoothGatt bluetoothGatt:connectionQueue){
374             bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
375         }
376     }
377
378     /**
379      * 蓝牙温度计*关键的一步 有些特征只能通过这个 读取  并启用通知更新数据
380      * Enables or disables indications on a give characteristic.
381      * 启用或禁用一个给定特性的指示
382      * @param characteristic Characteristic to act on.
383      * @param enabled        If true, enable indications. False otherwise.
384      */
385     public void setCharacteristicIndication(
386             BluetoothGattCharacteristic characteristic, boolean enabled) {
387         String serviceUUID = characteristic.getService().getUuid().toString();
388
389         String characteristicUUID = characteristic.getUuid().toString();
390
391         Log.i(“==TAG==”,serviceUUID+”   “+characteristicUUID);
392
393         for(BluetoothGatt mBluetoothGatt:connectionQueue){
394
395             if (mBluetoothAdapter == null || mBluetoothGatt == null) {
396                 return;
397             }
398
399             if (characteristic.getDescriptor(UUID
400                     .fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG)) != null) {
401                 if (enabled == true) {
402                     BluetoothGattDescriptor descriptor = characteristic
403                             .getDescriptor(UUID
404                                     .fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
405                     descriptor
406                             .setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
407                     mBluetoothGatt.writeDescriptor(descriptor);
408
409                 } else {
410                     BluetoothGattDescriptor descriptor = characteristic
411                             .getDescriptor(UUID
412                                     .fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
413                     descriptor
414                             .setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
415                     mBluetoothGatt.writeDescriptor(descriptor);
416
417                 }
418             }
419             mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
420         }
421     }
422
423
424 }

其中我里面用了一些外部的方法代码如下

/**
* Class used for parsing Health temperature related information
* 用于解析温度相关信息的类
*/

1 public class HTMParser {
2
3     private static ArrayList<String> mTempInfo = new ArrayList<String>();
4
5     //Byte character format
6     private static final String BYTE_CHAR_FORMAT = “%02X “;
7
8     //Switch case Constants
9     private static final int CASE_ARMPIT = 1;
10     private static final int CASE_BODY = 2;
11     private static final int CASE_EAR_LOBE = 3;
12     private static final int CASE_FINGER = 4;
13     private static final int CASE_GIT = 5;
14     private static final int CASE_MOUTH = 6;
15     private static final int CASE_RECTUM = 7;
16     private static final int CASE_TYMPANUM = 8;
17     private static final int CASE_TOE = 9;
18     private static final int CASE_TOE_REP = 10;
19
20     /**
21      * Get the thermometer reading
22      * 温度计的读数
23      *
24      * @param characteristic
25      * @return
26      */
27     public static String getHealthThermo(
28             BluetoothGattCharacteristic characteristic, Context context) {
29         String tempUnit = “”;
30         // For all other profiles, writes the data formatted in HEX.
31         final byte[] data = characteristic.getValue();
32         if (data != null && data.length > 0) {
33             final StringBuilder stringBuilder = new StringBuilder(data.length);
34             byte flagByte = data[0];
35             if ((flagByte & 0x01) != 0) {
36                 tempUnit = context.getString(R.string.tt_fahren_heit);
37             } else {
38                 tempUnit = context.getString(R.string.tt_celcius);
39             }
40             for (byte byteChar : data)
41                 stringBuilder.append(String.format(BYTE_CHAR_FORMAT, byteChar));
42         }
43         final float temperature = characteristic.getFloatValue(BluetoothGattCharacteristic.FORMAT_FLOAT, 1);
44
45         //Logger.i(“tempRate ” + temperature);
46
47         String ss = temperature+”,”+tempUnit;
48         //mTempInfo.add(1, tempUnit);
49         return ss;
50     }
51
52     /**
53      * Get the thermometer sensor location
54      *
55      * @param characteristic
56      * @return
57      */
58     public static String getHealthThermoSensorLocation(
59             BluetoothGattCharacteristic characteristic, Context context) {
60         String healthTherSensorLocation = “”;
61         final byte[] data = characteristic.getValue();
62         if (data != null && data.length > 0) {
63             final StringBuilder stringBuilder = new StringBuilder(data.length);
64             for (byte byteChar : data)
65                 stringBuilder.append(String.format(BYTE_CHAR_FORMAT, byteChar));
66             int healthBodySensor = Integer.valueOf(stringBuilder.toString()
67                     .trim());
68             switch (healthBodySensor) {
69                 case CASE_ARMPIT:
70                 //这个R.string.armpit 里面就是这个airpit的英文意思
71                     healthTherSensorLocation = context.getString(R.string.armpit);
72                     break;
73                 case CASE_BODY:
74                     healthTherSensorLocation = context.getString(R.string.body);
75                     break;
76                 case CASE_EAR_LOBE:
77                     healthTherSensorLocation = context.getString(R.string.ear);
78                     break;
79                 case CASE_FINGER:
80                     healthTherSensorLocation = context.getString(R.string.finger);
81                     break;
82                 case CASE_GIT:
83                     healthTherSensorLocation = context.getString(R.string.intestine);
84                     break;
85                 case CASE_MOUTH:
86                     healthTherSensorLocation = context.getString(R.string.mouth);
87                     break;
88                 case CASE_RECTUM:
89                     healthTherSensorLocation = context.getString(R.string.rectum);
90                     break;
91                 case CASE_TYMPANUM:
92                     healthTherSensorLocation = context.getString(R.string.tympanum);
93                     break;
94                 case CASE_TOE:
95                     healthTherSensorLocation = context.getString(R.string.toe_1);
96                     break;
97                 case CASE_TOE_REP:
98                     healthTherSensorLocation = context.getString(R.string.toe_2);
99                     break;
100                 default:
101                     healthTherSensorLocation = context.getString(R.string.reserverd);
102                     break;
103             }
104
105         }
106         return healthTherSensorLocation;
107     }
108 }

 

8.总结:
蓝牙连接*主要的是要掌握 BluetoothLeService这个类里面的内容,主要包扣BluetoothGattCallback 回调
这里面是处理蓝牙数据的核心,包扣连接设备回调 断开设备回调 发现服务回调 发现数据回调。多台连接蓝牙设备的时候 把BluetoothGatt 放到一个List里面去。断开的时候也要移次的移除掉 list里面的BluetoothGatt数据。

IOS蓝牙连接和发送数据

首先要确定蓝牙是否打开
本文使用的是 Objective-C语言
1.新建一个蓝牙帮助类BlueHelp
并导入

pragma mark – CBPeripheralDelegate
//只要扫描到服务就会调用,其中的外设就是服务所在的外设
– (void)peripheral:(CBPeripheral )peripheral didDiscoverServices:(NSError )error{
if (error){
NSLog(@”扫描服务出现错误,错误原因:%@”,error);
}else{
//获取外设中所扫描到的服务
NSArray *services = peripheral.services;
for (CBService *service in services){
//拿到需要扫描的服务,例如打印一些服务数据
//把所有的service打印出来
NSLog(@”service is :%@”,service);
//从需要的服务中查找需要的特征
//从peripheral的services中扫描特征
[peripheral discoverCharacteristics:nil forService:service];
}
}
}

//只要扫描到特征就会调用,其中的外设和服务就是特征所在的外设和服务
– (void)peripheral:(CBPeripheral )peripheral didDiscoverCharacteristicsForService:(CBService )service error:(NSError *)error{
if (error){
NSLog(@”扫描特征出现错误,错误原因:%@”,error);
}else{
//拿到服务中所有的特征
NSArray *characteristics = service.characteristics;
//遍历特征,拿到需要的特征进行处理
for (CBCharacteristic *characteristic in characteristics){
NSLog(@”所有的特征为:%@”,characteristic.UUID.UUIDString);
//如果是温度数据处理
if([characteristic.UUID.UUIDString isEqualToString:@”你自己的特征值”]){
//将全部的特征信息打印出来
_peripheral = peripheral;
scperipheral = peripheral;
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
NSLog(@”%@”,characteristic);
}
//这里是发送 设备设置蓝牙发送温度数据的值
else if([characteristic.UUID.UUIDString isEqualToString:@”你自己的特征值”]){
writecharacteristic = characteristic;
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
NSLog(@”写入的值为:%@”,characteristic);
}
//如果是ota进行ota升级
else if([characteristic.UUID.UUIDString isEqualToString:@”你需要的特征值”]){
_peripheral = peripheral;
scperipheral = peripheral;
writeOtacharacteristic = characteristic;
//设置通知
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
//NSLog(@”OTA升级了”);
isOTA = TRUE;
NSString *s = @”OTA”;
NSDictionary *tempOta = [NSDictionary dictionaryWithObject:s forKey:@”ota”];
[[NSNotificationCenter defaultCenter] postNotificationName:@”tempOTA” object:nil userInfo:tempOta];
}
//无键按钮DFU
else if([characteristic.UUID.UUIDString isEqualToString:@”你自己需要的特征值”]){
writeDfucharacteristic = characteristic;
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
NSLog(@”写入的OTA值为:%@”,characteristic);
}
}
}
}

/*
* 获取所有的温度更新数据
* 接收蓝牙发送过来的温度数据 并经过解析和转换
*/
– (void)peripheral:(CBPeripheral )peripheral didUpdateValueForCharacteristic:(CBCharacteristic )characteristic error:(NSError *)error
{
if ([characteristic.UUID isEqual:BOOT_LOADER_CHARACTERISTIC_UUID])
{
uint8_t dataPointer = (uint8_t ) [characteristic.value bytes];
NSString *errorCode = [NSString stringWithFormat:@”0x%2x”,dataPointer[1]];
errorCode = [errorCode stringByReplacingOccurrencesOfString:@” ” withString:@”0”];

 

1     // Checking the error code from the response
2     if ([errorCode isEqualToString:SUCCESS]&&commandArray.count>0)
3     {
4         if ([[commandArray objectAtIndex:0] isEqual:@(ENTER_BOOTLOADER)])
5         {
6             [self getBootLoaderDataFromCharacteristic:characteristic];
7         }
8         else if ([[commandArray objectAtIndex:0] isEqual:@(GET_FLASH_SIZE)])
9         {
10             [self getFlashDataFromCharacteristic:characteristic];
11         }
12         else if ([[commandArray objectAtIndex:0] isEqual:@(SEND_DATA)])
13         {
14             _isWritePacketDataSuccess = YES;
15             [_userDefaults setBool:_isWritePacketDataSuccess forKey:@”WriteSuccess”];
16         }
17         else if ([[commandArray objectAtIndex:0] isEqual:@(PROGRAM_ROW)])
18         {
19             _isWritePacketDataSuccess = YES;
20             [_userDefaults setBool:_isWritePacketDataSuccess forKey:@”WriteSuccess”];
21         }
22
23         else if ([[commandArray objectAtIndex:0] isEqual:@(VERIFY_ROW)])
24         {
25             [self getRowCheckSumFromCharacteristic:characteristic];
26         }
27         else if([[commandArray objectAtIndex:0] isEqual:@(VERIFY_CHECKSUM)])
28         {
29             [self checkApplicationCheckSumFromCharacteristic:characteristic];
30         }
31
32         if (cbCharacteristicUpdationHandler != nil)
33         {
34             cbCharacteristicUpdationHandler(YES,[commandArray objectAtIndex:0],nil);
35             [commandArray removeObjectAtIndex:0];
36         }
37
38     }
39     else
40     {
41         if(commandArray.count>0){
42             if ([[commandArray objectAtIndex:0] isEqual:@(PROGRAM_ROW)])
43             {
44                 _isWritePacketDataSuccess = NO;
45             }
46             else if ([[commandArray objectAtIndex:0] isEqual:@(SEND_DATA)])
47             {
48                 _isWritePacketDataSuccess = NO;
49             }
50             if (cbCharacteristicUpdationHandler != nil)
51             {
52                 cbCharacteristicUpdationHandler(YES,[commandArray objectAtIndex:0],nil);
53                 [commandArray removeObjectAtIndex:0];
54             }
55         }
56     }
57 }
58 if(!isOTA&&!_isSendData){
59     NSString *temps=[self getTempData:characteristic];
60     NSArray *ss = [temps componentsSeparatedByString:@”,”];
61     NSString *tempWendu =ss[0];
62     NSString *tempSymbol = ss[1];
63
64     self.currentDate = [NSDate date];
65
66     NSString *dateString = [_dateformatter stringFromDate:self.currentDate];
67
68     NSLog(@” ==save is time=== :%@”,dateString);
69
70     NSDate *logDate = [_dateformatter dateFromString:dateString];
71     long logTime= (long)[logDate timeIntervalSince1970];
72     if(temps!=nil&&![_bluename isEqualToString:@””]&&_bluename!=nil&&_bluename!=NULL){ //把数据存入到数据库
73     NSString *a = [_userDefaults objectForKey:@”tempUnit”];
74     //如果是华氏度的时候一定要把它转为摄氏度之后再来保存数据 以便数据的统一性
75     if([a isEqualToString:@”°F”]){
76         float floatString = [tempWendu floatValue];
77
78         float aa = [_normalUtil convertFahrenheitToCelcius:floatString];
79
80         NSString *ftempWendu =  [NSString stringWithFormat:@”%.1f”,aa];
81         NSString *ftempSymbol = @”°C”;
82
83         [_tempDao saveTempname:_bluename saveTempaddre:_blueaddre saveTempwenduname:ftempSymbol saveTempwendu:ftempWendu saveTemptime:logTime];
84         }else{
85
86             [_tempDao saveTempname:_bluename saveTempaddre:_blueaddre saveTempwenduname:tempSymbol saveTempwendu:tempWendu saveTemptime:logTime];
87         }
88
89         NSLog(@” ==save is success=== :%ld”,logTime);
90     }
91
92     //当读取到数据的时候就要存入数据 以后用来蓝牙自动的去连接
93     if(_saveAutoOnce){
94         [_userDefaults setObject:_bluename forKey:@”autoBlueName”];
95         [_userDefaults setObject:_blueaddre forKey:@”autoBlueAddre”];
96         _saveAutoOnce = false;
97     }
98
99     NSString * tp = [temps stringByAppendingString:@”,”];
100
101     NSString * tempp = [tp stringByAppendingString:_blueaddre];
102
103     NSDictionary *tempDict = [NSDictionary dictionaryWithObject:tempp forKey:@”temp”];
104
105     [[NSNotificationCenter defaultCenter] postNotificationName:@”tempNofiction” object:nil userInfo:tempDict];
106 }

}

上面这些代码可以不看 我写的是一些数据的转换
下面我们可以看下蓝牙写入数据
//也可以自定义主要是给外部调用
//发送数据

-(void)writeBlueValue:(NSString *)value{
_isSendData = TRUE;
[self writeValue:value forCharacteristic:writecharacteristic];
}

/*
这个是给蓝牙发送数据用的
*/
-(void)writeValue:(NSString )value forCharacteristic:(CBCharacteristic )characteristic
{
// NSData *data = [value dataUsingEncoding:4];
NSData *data = [_normalUtil stringToBytes:value];
NSLog(@”send data is:%@:characteristic:%@”,data,writecharacteristic);
//is no write bluetooth data
if(writecharacteristic.properties & CBCharacteristicPropertyWriteWithoutResponse)
{
//send phone on bluetooth data
[selectPeriperal writeValue:data forCharacteristic:writecharacteristic type:CBCharacteristicWriteWithoutResponse];
}else
{
[selectPeriperal writeValue:data forCharacteristic:writecharacteristic type:CBCharacteristicWriteWithResponse];

1 }
2 //发送完成后 又要它归位
3 _isSendData = FALSE;
4 NSLog(@”已经向外设%@的特征值%@写入数据”,selectPeriperal.name,writecharacteristic.description);

}

一般发送数据都需要用到 NSData数据类型格式下面我有几个方法
/**
* Method to convert hex to byteArray。这里是将16进制转换为NSData
*/

-(NSMutableData )dataFromHexString:(NSString )string
{
NSMutableData *data = [NSMutableData new];
NSCharacterSet *hexSet = [[NSCharacterSet characterSetWithCharactersInString:@”0123456789ABCDEF “] invertedSet];

 

1 // Check whether the string is a valid hex string. Otherwise return empty data
2 if ([string rangeOfCharacterFromSet:hexSet].location == NSNotFound) {
3
4     string = [string lowercaseString];
5     unsigned char whole_byte;
6     char byte_chars[3] = {‘\0′,’\0′,’\0’};
7     int i = 0;
8     int length = (int)string.length;
9
10     while (i < length-1)
11     {
12         char c = [string characterAtIndex:i++];
13
14         if (c < ‘0’ || (c > ‘9’ && c < ‘a’) || c > ‘f’)
15             continue;
16         byte_chars[0] = c;
17         byte_chars[1] = [string characterAtIndex:i++];
18         whole_byte = strtol(byte_chars, NULL, 16);
19         [data appendBytes:&whole_byte length:1];
20     }
21 }
22 return data;

}

还有一种是
//字符串转为数组
-(NSData )stringToBytes:(NSString )string{
int a = [string intValue];
NSData *aData = [string dataUsingEncoding: NSUTF8StringEncoding];
Byte testByte = (Byte )[aData bytes];
testByte[0] = (Byte)(a>>24&0xff);
testByte[1] = (Byte) (a>>16&0xff);
testByte[2] = (Byte)(a>>8&0xff);
testByte[3] = (Byte)(a&0xff);

1 NSData *adata = [[NSData alloc] initWithBytes:testByte length:4];
2
3 return adata;

这些方法比较常用的
好了基本思路就这样 源代码 过段时间放上来 。

iOS蓝牙开发数据实时传输

随着iOS项目开发  很多app需要通过蓝牙与设备连接

蓝牙开发注意:

先定义中心设备和外围设备以及遵守蓝牙协议

@interface ViewController()<CBCentralManagerDelegate,CBPeripheralDelegate>
@property (strong, nonatomic) CBCentralManager *manager;
@property (nonatomic, strong) CBPeripheral *peripheral;

@property (nonatomic, weak)NSTimer * connentTimer;

@end

8313

 

再实现delegate方法

  1. 判断蓝牙状态,如成功则扫描指定UUID设备(如不指定UUID,则无法后台持续连接)
  2. 当发现指定设备后,连接该设备
  3. 当连接指定外围设备成功,编写定时器,每秒读取1次RSSI
  4. 当监听到失去和外围设备连接,重新建立连接
  5. 当读取到RSSI值,打印出它的值

 

  1. 1 //蓝牙状态
    2 – (void)centralManagerDidUpdateState:(CBCentralManager *)central
    3 {
    4     NSString * state = nil;
    5     switch ([central state])
    6     {
    7         case CBCentralManagerStateUnsupported:
    8             state = @”The platform/hardware doesn’t support Bluetooth Low Energy.”;
    9             break;
    10
    11         case CBCentralManagerStateUnauthorized:
    12             state = @”The app is not authorized to use Bluetooth Low Energy.”;
    13             break;
    14              //尚未打开蓝牙
    15         case CBCentralManagerStatePoweredOff:
    16             state = @”Bluetooth is currently powered off.”;
    17             break;
    18              //连接成功
    19         case CBCentralManagerStatePoweredOn:
    20             [self.manager scanForPeripheralsWithServices:nil options:nil];
    21             state = @”work”;
    22             break;
    23         case CBCentralManagerStateUnknown:
    24         default:
    25             ;
    26     }
    27
    28     NSLog(@”Central manager state: %@”, state);
    29 }
    30 //查找设备
    31 – (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
    32 {
    33       //每个蓝牙设备有自己唯一的标识符,根据标识符确认自己要连接的设备
    34     if ([peripheral.identifier isEqual:self.peripheral.identifier])
    35     {
    36         self.peripheral = peripheral;
    37         //数据连接定时器
    38         self.connentTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(connentPeripheral) userInfo:@”timer” repeats:YES];
    39         [self.connentTimer fire];
    40     }
    41 }
    42
    43 – (void)connentPeripheral {
    44     //连接外设
    45     self.manager.delegate = self;
    46     [self.manager connectPeripheral:_peripheral options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:CBConnectPeripheralOptionNotifyOnDisconnectionKey]];
    47
    48 }
    49
    50 //连接成功后调用
    51 – (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
    52 {
    53     NSLog(@”Did connect to peripheral: %@,%@”, peripheral,peripheral.name);
    54     [peripheral setDelegate:self];  //查找服务
    55     [peripheral discoverServices:nil];
    56     [self.connentTimer invalidate];
    57     //监测设备是否断开了
    58 //    [self createWorkDataSourceWithTimeInterval:1];
    59 }
    60 //当监听到失去和外围设备连接,重新建立连接
    61 //这个方法是必须实现的,因为蓝牙会中断连接,正好触发这个方法重建连接。重建连接可能造成数秒后才能读取到RSSI。
    62
    63 – (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
    64 {
    65     [self.manager connectPeripheral:peripheral options:nil];
    66 }
    67
    68 – (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
    69 {
    70     NSLog(@”%@”,error.description);
    71 }
    72
    73 //返回的蓝牙服务通知通过代理实现
    74 – (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
    75 {
    76     if (error)
    77     {
    78         NSLog(@”Discovered services for %@ with error: %@”, peripheral.name, [error localizedDescription]);
    79         return;
    80     }
    81     for (CBService *service in peripheral.services)
    82     {
    83 //        NSLog(@”Service found with UUID: %@”, service.UUID.UUIDString);
    84         //发现服务
    85         if ([service.UUID isEqual:[CBUUID UUIDWithString:@”180D”]])//heart rate
    86         {
    87             //在一个服务中寻找特征值
    88             [peripheral discoverCharacteristics:nil forService:service];
    89         }
    90     }
    91 }
    92
    93 //返回的蓝牙特征值通知通过代理实现
    94 – (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
    95 {
    96     if (error)
    97     {
    98         NSLog(@”Discovered characteristics for %@ with error: %@”, service.UUID, [error localizedDescription]);
    99         return;
    100     }
    101     for (CBCharacteristic * characteristic in service.characteristics)
    102     {
    103         NSLog(@”characteristic:%@”,characteristic);
    104         if( [characteristic.UUID isEqual:[CBUUID UUIDWithString:@”2A37″]])
    105         {
    106
    107             [self notification:service.UUID characteristicUUID:characteristic.UUID peripheral:peripheral on:YES];
    108 //            [self.peripheral setNotifyValue:YES forCharacteristic:characteristic];
    109         }
    110     }
    111 }
    112
    113 //处理蓝牙发过来的数据
    114 – (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
    115 {
    116
    117 }
    118
    119 -(void) notification:(CBUUID *) serviceUUID characteristicUUID:(CBUUID *)characteristicUUID peripheral:(CBPeripheral *)p on:(BOOL)on
    120 {
    121     CBService *service = [self getServiceFromUUID:serviceUUID p:p];
    122     if (!service)
    123     {
    124         //        if (p.UUID == NULL) return; // zach ios6 addedche
    125         //        NSLog(@”Could not find service with UUID on peripheral with UUID \n”);
    126         return;
    127     }
    128     CBCharacteristic *characteristic = [self getCharacteristicFromUUID:characteristicUUID service:service];
    129     if (!characteristic)
    130     {
    131         //        if (p.UUID == NULL) return; // zach ios6 added
    132         //        NSLog(@”Could not find characteristic with UUID  on service with UUID  on peripheral with UUID\n”);
    133         return;
    134     }
    135     [p setNotifyValue:on forCharacteristic:characteristic];
    136
    137 }
    138
    139 -(CBService *) getServiceFromUUID:(CBUUID *)UUID p:(CBPeripheral *)p
    140 {
    141
    142     for (CBService* s in p.services)
    143     {
    144         if ([s.UUID isEqual:UUID]) return s;
    145     }
    146     return nil; //Service not found on this peripheral
    147 }
    148 -(CBCharacteristic *) getCharacteristicFromUUID:(CBUUID *)UUID service:(CBService*)service {
    149
    150     for (CBCharacteristic* c in service.characteristics)
    151     {
    152         if ([c.UUID isEqual:UUID]) return c;
    153     }
    154     return nil; //Characteristic not found on this service
    155 }

iOS蓝牙开发总结及Demo

1. 概念

BLE ,buletouch low energy,蓝牙4.0设备因为低耗电,所以也叫做BLE;
中心设备 ,用于扫描周边蓝牙外设的设备,比如我们上面所说的中心者模式,此时我们的手机就是中心设备;
外设 ,被扫描的蓝牙设备,比如我们上面所说的用我们的手机连接小米手环,这时候小米手环就是外设;
广播 ,外部设备不停的散播的蓝牙信号,让中心设备可以扫描到,也是我们开发中接收数据的入口;
服务(Service) ,外部设备在与中心设备连接后会有服务,可以理解成一个功能模块,中心设备可以读取服务,筛选我们想要的服务,并从中获取出我们想要特征。(外设可以有多个服务);
特征(Characteristic) ,服务中的一个单位,一个服务可以多个特征,而特征会有一个value,一般我们向蓝牙设备写入数据、从蓝牙设备读取数据就是这个value;
UUID ,区分不同服务和特征的唯一标识,使用该字端我们可以获取我们想要的服务或者特征(ps: 不同的中心设备(也可以说是不同的手机)对于同一台蓝牙设备,获取到的UUIDString可能是不一样的)。

2. CoreBluetooth框架

CoreBluetooth框架的核心其实是两个东西,peripheral和central, 可以理解成外设和中心。
图中两组api分别对应不同的业务场景,左侧叫做中心模式,就是以你的app作为中心,连接其他的外设的场景,而右侧称为外设模式,使用手机作为外设别其他中心设备操作的场景

蓝牙中心模式流程

1. 建立中心角色
2. 扫描外设(discover)
3. 连接外设(connect)
4. 扫描外设中的服务和特征(discover)
– 4.1 获取外设的services
– 4.2 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值
5. 与外设做数据交互(explore and interact)
6. 订阅Characteristic的通知
7. 断开连接(disconnect)

 

蓝牙外设模式流程

1. 启动一个Peripheral管理对象
2. 本地Peripheral设置服务,特性,描述,权限等等
3. Peripheral发送广告
4. 设置处理订阅、取消订阅、读characteristic、写characteristic的委托方法

3. Demo(这里只写了中心者模式)

支持蓝牙名称搜索过滤、连接多台蓝牙设备、连续写入多条命令@class LSBluetoothManager;

@protocol LSBluetoothManagerDelegate <NSObject>

@optional
// 获取设备,会调用多次,需要先调用- (void)startScanDevices;
– (void)manager:(LSBluetoothManager *_Nullable)manager didDiscoverDeveice:(nonnull LSBluetoothModel *)peripheral error:(nullable NSError *)error;

// 连接某一台设备是否成功的结果,需要先调用- (void)conect:(CBPeripheral *)peripheral;
– (void)manager:(LSBluetoothManager *_Nonnull)manager connectedDevice:(nonnull CBPeripheral *)peripheral state:(BOOL)state;

// 写入数据结果,需要先调用writeWithPeripheral:(CBPeripheral *_Nonnull)peripheral ServiceUUID:(NSString * _Nonnull )ServiceUUID CharacteristicWriteUUID:(NSString *_Nonnull)characteristicWriteUUID CharacteristicNotifyUUID:(NSString *_Nonnull)characteristicNotifyUUID CMD:(NSString *_Nonnull)CMDString;
– (void)manager:(LSBluetoothManager *_Nullable)manager didUpdateValueForCharacteristic:(nonnull CBCharacteristic *)characteristic receiveData:(NSData *_Nullable)receiveData error:(nullable NSError *)error;

@end

@interface LSBluetoothManager : NSObject

@property (nonatomic, weak, nullable) id <LSBluetoothManagerDelegate> delegate;

// 初始化蓝牙
+ (instancetype _Nonnull )shareManager;

// 蓝牙是否打开,需要设置代理
– (BOOL)isAuthorizationOpen;

// 开始扫描,prefix: 只查找某一个前缀开头的设备,传nil默认扫描所有
– (void)startScanDevicesHasNamePrefix:(NSString *_Nullable)nameprefix;

// 结束扫描
– (void)stopScanDevices;

// 连接某一台设备
– (void)conect:(CBPeripheral *_Nonnull)peripheral ServiceUUID:(NSString * _Nonnull )ServiceUUID CharacteristicWriteUUID:(NSString *_Nonnull)characteristicWriteUUID CharacteristicNotifyUUID:(NSString *_Nonnull)characteristicNotifyUUID;

// 判断获取某一台设备是否在线
– (BOOL)isOnLine:(CBPeripheral *_Nonnull)peripheral ServiceUUID:(NSString *_Nonnull)ServiceUUID;

// 断开某一台设备
– (void)disconect:(CBPeripheral *_Nullable)peripheral;

// 写入数据
– (void)writeWithPeripheral:(CBPeripheral *_Nonnull)peripheral ServiceUUID:(NSString * _Nonnull )ServiceUUID CharacteristicWriteUUID:(NSString *_Nonnull)characteristicWriteUUID CharacteristicNotifyUUID:(NSString *_Nonnull)characteristicNotifyUUID CMD:(NSString *_Nonnull)CMDString;

@end

MAC os 安装 wine

方式一:下载 pkg 安装
https://www.winehq.org/

%title插图%num
安装成功后显示图标

%title插图%num
方式二:命令行用brew
brew search wine

%title插图%num

brew cask install wine-stable

%title插图%num

安装成功,安装过程中会提示输入密码。
接下来就可以直接在终端中使用 wine xxx.exe 。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速