commit 8e5d2c9e7f72596384b5771b808cf29456b23ea6 Author: 杭城小刘 <704568245@qq.com> Date: Tue Sep 11 10:11:39 2018 +0800 feat: 初始化 diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..055f4bc Binary files /dev/null and b/.DS_Store differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2ec1d6f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 杭城小刘 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7008b6b --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ + + + +iOS、Web前端、后端、Swift、计算机网络踩坑以及学习记录 diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..cdb32e4 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,97 @@ +# Summary + + +* [Introduction](README.md) +* [第一部分 iOS](第一部分 iOS/chapter1.md) + * [1、工程大小优化之图片资源](第一部分 iOS/1.1.md ) + * [2、看透构造方法](第一部分 iOS/1.2.md) + * [3、控制器加载的玄机](第一部分 iOS/1.3.md) + * [4、如何优雅地调试手机网页?](第一部分 iOS/1.4.md) + * [5、事件响应者链](第一部分 iOS/1.5.md) + * [6、外卖App双列表联动](第一部分 iOS/1.6.md) + * [7、在内存剖析对象](第一部分 iOS/1.7.md) + * [8、长按UIWebView上的图片保存到相册](第一部分 iOS/1.8.md) + * [9、hitTest和pointInside方法你真的熟吗?](第一部分 iOS/1.9.md) + * [10、HyBrid探索(一)](第一部分 iOS/1.10.md) + * [11、iOS中的事件](第一部分 iOS/1.11.md) + * [12、NSFileManager终极杀手](第一部分 iOS/1.12.md) + * [13、UINavigationController的妙用](第一部分 iOS/1.13.md) + * [14、URL-Schemes深度剖析(上)](第一部分 iOS/1.14.md) + * [15、URL Schemes 的发展](第一部分 iOS/1.15.md) + * [16、CocoaPods的使用](第一部分 iOS/1.16.md) + * [17、OC与Swift混编](第一部分 iOS/1.17.md) + * [18、对于不可调节高度的UI控件进行改变frame](第一部分 iOS/1.18.md) + * [19、YYModel 的使用](第一部分 iOS/1.19.md) + * [20、模块化](第一部分 iOS/1.20.md) + * [21、实现波浪动画](第一部分 iOS/1.21.md) + * [22、底层原理探究](第一部分 iOS/1.22.md) + * [23、禅与 Objective-C 编程艺术](第一部分 iOS/1.23.md) + * [24、修改 UITextField placeholder 样式](第一部分 iOS/1.24.md) + * [25、UIScrollView拖拽时回收键盘](第一部分 iOS/1.25.md) + * [26、读 Apple 源码看看 NSRange](第一部分 iOS/1.26.md) + * [27、复制层(CAReplicatorLayer)](第一部分 iOS/2.17.md) + * [28、CAShapeLayer](第一部分 iOS/1.28.md) + * [29、仿微博动画](第一部分 iOS/1.29.md) + * [30、UILabel 全局匹配并高亮](第一部分 iOS/1.30.md) + * [31、JavascriptCore](第一部分 iOS/1.31.md) + * [32、Xcode 小技巧](第一部分 iOS/1.32.md) + * [33、终端效率](第一部分 iOS/1.33.md) + * [34、终极截屏](第一部分 iOS/1.34.md) + * [35、推送](第一部分 iOS/1.35.md) + * [36、App 评分](第一部分 iOS/1.36.md) + * [37、一些布局小知识](第一部分 iOS/1.37.md) + * [38、iOS数值计算精度丢失问题](第一部分 iOS/1.38.md) + * [39、一些看到但未尝试的知识](第一部分 iOS/1.39.md) + * [40、RunLoop上](第一部分 iOS/1.40.md) + * [41、RunLoop下](第一部分 iOS/1.41.md) + * [42、RunLoop的应用](第一部分 iOS/1.42.md) + * [43、iOS 应用启动性能优化资料汇总](第一部分 iOS/1.43.md) + * [44、App security](第一部分 iOS/1.44.md) + * [45、调试技巧](第一部分 iOS/1.45.md) + * [46、Awesome Hybrid - 1](第一部分 iOS/1.46.md) + * [46、Awesome Hybrid - 1](第一部分 iOS/1.46.md) + * [47、NSTimer 的内存泄漏](第一部分 iOS/1.47.md) + * [48、KVC && KVO](第一部分 iOS/1.48.md) + * [48、金额格式化](第一部分 iOS/1.49.md) +* [第二部分 Web前端](第二部分 Web 前端/chapter2.md) + * [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](第二部分 Web 前端/2.1.md) + * [2、正则表达式](第二部分 Web 前端/2.2.md) + * [3、CSS-埋点统计](第二部分 Web 前端/2.3.md) + * [4、generator函数](第二部分 Web 前端/2.4.md) + * [5、H5性能优化方面的探索](第二部分 Web 前端/2.5.md) + * [6、h5自定义对象](第二部分 Web 前端/2.6.md) + * [7、Javascript-prototype](第二部分 Web 前端/2.7.md) + * [8、JSON的一些骚操作](第二部分 Web 前端/2.8.md) + * [9、vue-cli](第二部分 Web 前端/2.9.md) + * [10、Vue-devtools](第二部分 Web 前端/2.10.md) + * [11、H5页面保存页面为图片](第二部分 Web 前端/2.11.md) + * [12、Promise](第二部分 Web 前端/2.12.md) + * [13、webpack-dev-server 的配置和使用](第二部分 Web 前端/2.13.md) + * [14、Web 与 H5 交互的坑](第二部分 Web 前端/2.14.md) + * [15、前端持久化](第二部分 Web 前端/2.15.md) + * [16、VS-Code](第二部分 Web 前端/2.16md) + * [17、Vue tips](第二部分 Web 前端/2.17.md) + * [18、Web反爬虫技术](第二部分 Web 前端/2.18.md) + * [19、webpack小集](第二部分 Web 前端/2.19.md) + * [20、大前端](第二部分 Web 前端/2.20.md) + * [21、canvas](第二部分 Web 前端/2.21.md) + * [21、动画控制](第二部分 Web 前端/2.22.md) + * [23、打造自己的图标字体文件](第二部分 Web 前端/2.23.md) + * [24、Chrome 调试技巧](第二部分 Web 前端/2.24.md) +* [第三部分 后端技术](第三部分 后端技术/chapter3.md) + * [1、利用分页和模糊查询技术实现一个App接口](第三部分 后端技术/3.1.md) + * [2、网页端扫码登录实现原理](第三部分 后端技术/3.2.md) +* [第四部分 数据库](第四部分 数据库/chapter4.md) + + * [1、Mysql JSON 类型的字段](第四部分 数据库/4.1.md) +* [第五部分 开发杂谈](第五部分 开发杂谈/chapter5.md) + * [1、开始hexo博客之旅](第四部分 开发杂谈/4.1.md) + * [2、命令行文件查找](第四部分 开发杂谈/4.2.md) + * [3、Charles 从入门到精通](第四部分 开发杂谈/4.3.md) + * [4、git常见使用](第四部分 开发杂谈/4.4.md) + * [5、Chrome 正确调试姿势](第四部分 开发杂谈/4.5.md) + * [6、Git 实用操作](第四部分 开发杂谈/4.6.md) + * [7、短链接](第四部分 开发杂谈/4.7.md) + * [8、移动端推送杂谈-part one](第四部分 开发杂谈/4.8.md) + * [9、iOS 隔了较久时间推送变得缓慢](第四部分 开发杂谈/4.9.md) + * [10、抓包工具的原理](第四部分 开发杂谈/4.10.md) diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/11532309922_.pic.jpg b/assets/11532309922_.pic.jpg new file mode 100644 index 0000000..443483d Binary files /dev/null and b/assets/11532309922_.pic.jpg differ diff --git a/assets/164ac9a99ea05b2e.png b/assets/164ac9a99ea05b2e.png new file mode 100644 index 0000000..9c1db74 Binary files /dev/null and b/assets/164ac9a99ea05b2e.png differ diff --git a/assets/164ac9a9a966fafe.png b/assets/164ac9a9a966fafe.png new file mode 100644 index 0000000..51d3ca8 Binary files /dev/null and b/assets/164ac9a9a966fafe.png differ diff --git a/assets/164ac9a9ca26c907.png b/assets/164ac9a9ca26c907.png new file mode 100644 index 0000000..ffb9838 Binary files /dev/null and b/assets/164ac9a9ca26c907.png differ diff --git a/assets/164ac9aa40822368.png b/assets/164ac9aa40822368.png new file mode 100644 index 0000000..54a72d4 Binary files /dev/null and b/assets/164ac9aa40822368.png differ diff --git a/assets/164ac9aaad2c0ff8.png b/assets/164ac9aaad2c0ff8.png new file mode 100644 index 0000000..54cc75a Binary files /dev/null and b/assets/164ac9aaad2c0ff8.png differ diff --git a/assets/164ac9aab5332151.png b/assets/164ac9aab5332151.png new file mode 100644 index 0000000..a7368ac Binary files /dev/null and b/assets/164ac9aab5332151.png differ diff --git a/assets/1785352-087fd4b664e0e387.png b/assets/1785352-087fd4b664e0e387.png new file mode 100644 index 0000000..9089c71 Binary files /dev/null and b/assets/1785352-087fd4b664e0e387.png differ diff --git a/assets/2017-09-24 15_35_52.gif b/assets/2017-09-24 15_35_52.gif new file mode 100644 index 0000000..4e74fed Binary files /dev/null and b/assets/2017-09-24 15_35_52.gif differ diff --git a/assets/224420fhxsp8e00v0s0b0s.png b/assets/224420fhxsp8e00v0s0b0s.png new file mode 100644 index 0000000..61c8e1f Binary files /dev/null and b/assets/224420fhxsp8e00v0s0b0s.png differ diff --git a/assets/224819kyaynn1zzoyoulzn.jpg.png b/assets/224819kyaynn1zzoyoulzn.jpg.png new file mode 100644 index 0000000..2e63819 Binary files /dev/null and b/assets/224819kyaynn1zzoyoulzn.jpg.png differ diff --git a/assets/2287777-16e40aa2ce7d0366.png b/assets/2287777-16e40aa2ce7d0366.png new file mode 100644 index 0000000..4f4b844 Binary files /dev/null and b/assets/2287777-16e40aa2ce7d0366.png differ diff --git a/assets/2287777-4d55fd205fa81cc8.png b/assets/2287777-4d55fd205fa81cc8.png new file mode 100644 index 0000000..26fc35d Binary files /dev/null and b/assets/2287777-4d55fd205fa81cc8.png differ diff --git a/assets/2287777-5691a767f7f4c5b6.png b/assets/2287777-5691a767f7f4c5b6.png new file mode 100644 index 0000000..3396b4d Binary files /dev/null and b/assets/2287777-5691a767f7f4c5b6.png differ diff --git a/assets/2287777-5ddf86e7d30b1c05.png b/assets/2287777-5ddf86e7d30b1c05.png new file mode 100644 index 0000000..816fc48 Binary files /dev/null and b/assets/2287777-5ddf86e7d30b1c05.png differ diff --git a/assets/2287777-67f09fb472c6b87d.png b/assets/2287777-67f09fb472c6b87d.png new file mode 100644 index 0000000..e7587d4 Binary files /dev/null and b/assets/2287777-67f09fb472c6b87d.png differ diff --git a/assets/2287777-6b827146b1c1cd36.png b/assets/2287777-6b827146b1c1cd36.png new file mode 100644 index 0000000..7a16cf5 Binary files /dev/null and b/assets/2287777-6b827146b1c1cd36.png differ diff --git a/assets/2287777-8ff7c3b976ffb29a.png b/assets/2287777-8ff7c3b976ffb29a.png new file mode 100644 index 0000000..d2accd1 Binary files /dev/null and b/assets/2287777-8ff7c3b976ffb29a.png differ diff --git a/assets/2287777-93cc952da314d7bf.PNG b/assets/2287777-93cc952da314d7bf.PNG new file mode 100644 index 0000000..d441e79 Binary files /dev/null and b/assets/2287777-93cc952da314d7bf.PNG differ diff --git a/assets/2287777-a50bd17a33290204.png b/assets/2287777-a50bd17a33290204.png new file mode 100644 index 0000000..673d18c Binary files /dev/null and b/assets/2287777-a50bd17a33290204.png differ diff --git a/assets/2287777-b2c552dfef180bd0.png b/assets/2287777-b2c552dfef180bd0.png new file mode 100644 index 0000000..f6846de Binary files /dev/null and b/assets/2287777-b2c552dfef180bd0.png differ diff --git a/assets/2287777-b6128646373dfffb.png b/assets/2287777-b6128646373dfffb.png new file mode 100644 index 0000000..df71af9 Binary files /dev/null and b/assets/2287777-b6128646373dfffb.png differ diff --git a/assets/2287777-b9c1d5245529fa1b.png b/assets/2287777-b9c1d5245529fa1b.png new file mode 100644 index 0000000..510337c Binary files /dev/null and b/assets/2287777-b9c1d5245529fa1b.png differ diff --git a/assets/2287777-c12eb2da00e79f34.png b/assets/2287777-c12eb2da00e79f34.png new file mode 100644 index 0000000..05356e9 Binary files /dev/null and b/assets/2287777-c12eb2da00e79f34.png differ diff --git a/assets/2287777-c1d479c5171de2d0.png b/assets/2287777-c1d479c5171de2d0.png new file mode 100644 index 0000000..b33b8ba Binary files /dev/null and b/assets/2287777-c1d479c5171de2d0.png differ diff --git a/assets/2287777-e22f24acf7823cfa.png b/assets/2287777-e22f24acf7823cfa.png new file mode 100644 index 0000000..875b5d3 Binary files /dev/null and b/assets/2287777-e22f24acf7823cfa.png differ diff --git a/assets/2287777-e937adb9c77a3768.png b/assets/2287777-e937adb9c77a3768.png new file mode 100644 index 0000000..9331266 Binary files /dev/null and b/assets/2287777-e937adb9c77a3768.png differ diff --git a/assets/3.png b/assets/3.png new file mode 100644 index 0000000..4a570f0 Binary files /dev/null and b/assets/3.png differ diff --git a/assets/4.png b/assets/4.png new file mode 100644 index 0000000..4bfa3f2 Binary files /dev/null and b/assets/4.png differ diff --git a/assets/4316713-49ef454cca917acd.jpg b/assets/4316713-49ef454cca917acd.jpg new file mode 100644 index 0000000..b630751 Binary files /dev/null and b/assets/4316713-49ef454cca917acd.jpg differ diff --git a/assets/442e4085cf4b8f62c7ad359343c5f155_hd.png b/assets/442e4085cf4b8f62c7ad359343c5f155_hd.png new file mode 100644 index 0000000..97e11b0 Binary files /dev/null and b/assets/442e4085cf4b8f62c7ad359343c5f155_hd.png differ diff --git a/assets/4673_140117110629_1.png b/assets/4673_140117110629_1.png new file mode 100644 index 0000000..4b37d13 Binary files /dev/null and b/assets/4673_140117110629_1.png differ diff --git a/assets/61530453779_.pic.jpg b/assets/61530453779_.pic.jpg new file mode 100644 index 0000000..75976c6 Binary files /dev/null and b/assets/61530453779_.pic.jpg differ diff --git a/assets/Andoid_Webview_Localstroage_erroe.jpg b/assets/Andoid_Webview_Localstroage_erroe.jpg new file mode 100644 index 0000000..e547889 Binary files /dev/null and b/assets/Andoid_Webview_Localstroage_erroe.jpg differ diff --git a/assets/Anti-WebSpider.gif b/assets/Anti-WebSpider.gif new file mode 100644 index 0000000..abdf83d Binary files /dev/null and b/assets/Anti-WebSpider.gif differ diff --git a/assets/App-Server.png b/assets/App-Server.png new file mode 100644 index 0000000..872b194 Binary files /dev/null and b/assets/App-Server.png differ diff --git a/assets/Charles-method.png b/assets/Charles-method.png new file mode 100644 index 0000000..1a94039 Binary files /dev/null and b/assets/Charles-method.png differ diff --git a/assets/Chrome-Vue-tools1.png b/assets/Chrome-Vue-tools1.png new file mode 100644 index 0000000..95942b7 Binary files /dev/null and b/assets/Chrome-Vue-tools1.png differ diff --git a/assets/Chrome-Vue-tools3.png b/assets/Chrome-Vue-tools3.png new file mode 100644 index 0000000..915ea4e Binary files /dev/null and b/assets/Chrome-Vue-tools3.png differ diff --git a/assets/Chrome-Vue-tools4.png b/assets/Chrome-Vue-tools4.png new file mode 100644 index 0000000..b418800 Binary files /dev/null and b/assets/Chrome-Vue-tools4.png differ diff --git a/assets/Chrome-Vue-tools5.png b/assets/Chrome-Vue-tools5.png new file mode 100644 index 0000000..64090b4 Binary files /dev/null and b/assets/Chrome-Vue-tools5.png differ diff --git a/assets/Chrome-Vue-tools6.png b/assets/Chrome-Vue-tools6.png new file mode 100644 index 0000000..1454fb0 Binary files /dev/null and b/assets/Chrome-Vue-tools6.png differ diff --git a/assets/IMG_3714.PNG b/assets/IMG_3714.PNG new file mode 100644 index 0000000..9c1db74 Binary files /dev/null and b/assets/IMG_3714.PNG differ diff --git a/assets/IMG_3715.PNG b/assets/IMG_3715.PNG new file mode 100644 index 0000000..ffb9838 Binary files /dev/null and b/assets/IMG_3715.PNG differ diff --git a/assets/QQ20180525-144755@2x.png b/assets/QQ20180525-144755@2x.png new file mode 100644 index 0000000..674efde Binary files /dev/null and b/assets/QQ20180525-144755@2x.png differ diff --git a/assets/QQ20180525-144824@2x.png b/assets/QQ20180525-144824@2x.png new file mode 100644 index 0000000..5cebe0e Binary files /dev/null and b/assets/QQ20180525-144824@2x.png differ diff --git a/assets/QQ20180602-210826@2x.png b/assets/QQ20180602-210826@2x.png new file mode 100644 index 0000000..42e02bc Binary files /dev/null and b/assets/QQ20180602-210826@2x.png differ diff --git a/assets/QQ20180602-211328@2x.png b/assets/QQ20180602-211328@2x.png new file mode 100644 index 0000000..5714323 Binary files /dev/null and b/assets/QQ20180602-211328@2x.png differ diff --git a/assets/QQ20180610-225937-HD.gif b/assets/QQ20180610-225937-HD.gif new file mode 100644 index 0000000..ee7ece9 Binary files /dev/null and b/assets/QQ20180610-225937-HD.gif differ diff --git a/assets/QQ20180610-235637-HD.gif b/assets/QQ20180610-235637-HD.gif new file mode 100644 index 0000000..a4f6f66 Binary files /dev/null and b/assets/QQ20180610-235637-HD.gif differ diff --git a/assets/QQ20180704-004941@2x.png b/assets/QQ20180704-004941@2x.png new file mode 100644 index 0000000..54cc75a Binary files /dev/null and b/assets/QQ20180704-004941@2x.png differ diff --git a/assets/QQ20180704-005101@2x.png b/assets/QQ20180704-005101@2x.png new file mode 100644 index 0000000..54a72d4 Binary files /dev/null and b/assets/QQ20180704-005101@2x.png differ diff --git a/assets/QQ20180704-005145@2x.png b/assets/QQ20180704-005145@2x.png new file mode 100644 index 0000000..a7368ac Binary files /dev/null and b/assets/QQ20180704-005145@2x.png differ diff --git a/assets/QQ20180704-010542@2x.png b/assets/QQ20180704-010542@2x.png new file mode 100644 index 0000000..51d3ca8 Binary files /dev/null and b/assets/QQ20180704-010542@2x.png differ diff --git a/assets/QQ20180802-110653@2x.png b/assets/QQ20180802-110653@2x.png new file mode 100644 index 0000000..e91402c Binary files /dev/null and b/assets/QQ20180802-110653@2x.png differ diff --git a/assets/QQ20180802-110938@2x.png b/assets/QQ20180802-110938@2x.png new file mode 100644 index 0000000..87d7d31 Binary files /dev/null and b/assets/QQ20180802-110938@2x.png differ diff --git a/assets/QQ20180802-111011@2x.png b/assets/QQ20180802-111011@2x.png new file mode 100644 index 0000000..ee2294b Binary files /dev/null and b/assets/QQ20180802-111011@2x.png differ diff --git a/assets/QQ20180802-111143@2x.png b/assets/QQ20180802-111143@2x.png new file mode 100644 index 0000000..def7ef6 Binary files /dev/null and b/assets/QQ20180802-111143@2x.png differ diff --git a/assets/QQ20180802-113022@2x.png b/assets/QQ20180802-113022@2x.png new file mode 100644 index 0000000..ed9e7f2 Binary files /dev/null and b/assets/QQ20180802-113022@2x.png differ diff --git a/assets/QQ20180802-120033@2x.png b/assets/QQ20180802-120033@2x.png new file mode 100644 index 0000000..0e546c4 Binary files /dev/null and b/assets/QQ20180802-120033@2x.png differ diff --git a/assets/QQ20180802-120113@2x.png b/assets/QQ20180802-120113@2x.png new file mode 100644 index 0000000..7b79357 Binary files /dev/null and b/assets/QQ20180802-120113@2x.png differ diff --git a/assets/QmQUyUSLYB3VGs4juzfsEdncyWetz7BTN2GFtURbmEYbEY.png b/assets/QmQUyUSLYB3VGs4juzfsEdncyWetz7BTN2GFtURbmEYbEY.png new file mode 100644 index 0000000..91d4a77 Binary files /dev/null and b/assets/QmQUyUSLYB3VGs4juzfsEdncyWetz7BTN2GFtURbmEYbEY.png differ diff --git a/assets/QmQrU8UxSytnKbWcDVpY5mdy6kmiSHpzyqwt8GykWKNEY2.png b/assets/QmQrU8UxSytnKbWcDVpY5mdy6kmiSHpzyqwt8GykWKNEY2.png new file mode 100644 index 0000000..b8dc8c1 Binary files /dev/null and b/assets/QmQrU8UxSytnKbWcDVpY5mdy6kmiSHpzyqwt8GykWKNEY2.png differ diff --git a/assets/QmUhGFJgxj6ofpvZp6MK3bqaH2hLgq9vfKsnwDmMisahGu.gif b/assets/QmUhGFJgxj6ofpvZp6MK3bqaH2hLgq9vfKsnwDmMisahGu.gif new file mode 100644 index 0000000..6884d85 Binary files /dev/null and b/assets/QmUhGFJgxj6ofpvZp6MK3bqaH2hLgq9vfKsnwDmMisahGu.gif differ diff --git a/assets/QmW9ACfS9P5orau43H7gxuxsU4RVMDPD7mPnDKq4pgLmzr.gif b/assets/QmW9ACfS9P5orau43H7gxuxsU4RVMDPD7mPnDKq4pgLmzr.gif new file mode 100644 index 0000000..22de76c Binary files /dev/null and b/assets/QmW9ACfS9P5orau43H7gxuxsU4RVMDPD7mPnDKq4pgLmzr.gif differ diff --git a/assets/Simulator Screen Shot - iPhone 6s Plus - 2017-10-11 at 10.14.37.png b/assets/Simulator Screen Shot - iPhone 6s Plus - 2017-10-11 at 10.14.37.png new file mode 100644 index 0000000..33964c8 Binary files /dev/null and b/assets/Simulator Screen Shot - iPhone 6s Plus - 2017-10-11 at 10.14.37.png differ diff --git a/assets/Untitled Diagram-2.png b/assets/Untitled Diagram-2.png new file mode 100644 index 0000000..33d923f Binary files /dev/null and b/assets/Untitled Diagram-2.png differ diff --git a/assets/Vue-20180225-1.png b/assets/Vue-20180225-1.png new file mode 100644 index 0000000..83e3ca6 Binary files /dev/null and b/assets/Vue-20180225-1.png differ diff --git a/assets/Vue-20180225-2.png b/assets/Vue-20180225-2.png new file mode 100644 index 0000000..d8419c7 Binary files /dev/null and b/assets/Vue-20180225-2.png differ diff --git a/assets/WX20180507-091957@2x.png b/assets/WX20180507-091957@2x.png new file mode 100644 index 0000000..c6821bf Binary files /dev/null and b/assets/WX20180507-091957@2x.png differ diff --git a/assets/WX20180507-092046@2x.png b/assets/WX20180507-092046@2x.png new file mode 100644 index 0000000..8b6c2d3 Binary files /dev/null and b/assets/WX20180507-092046@2x.png differ diff --git a/assets/WX20180507-092145@2x.png b/assets/WX20180507-092145@2x.png new file mode 100644 index 0000000..26896c6 Binary files /dev/null and b/assets/WX20180507-092145@2x.png differ diff --git a/assets/WX20180507-092232@2x.png b/assets/WX20180507-092232@2x.png new file mode 100644 index 0000000..a70bb93 Binary files /dev/null and b/assets/WX20180507-092232@2x.png differ diff --git a/assets/WX20180507-092358@2x.png b/assets/WX20180507-092358@2x.png new file mode 100644 index 0000000..f914bf7 Binary files /dev/null and b/assets/WX20180507-092358@2x.png differ diff --git a/assets/WX20180516-235614@2x.png b/assets/WX20180516-235614@2x.png new file mode 100644 index 0000000..a58e084 Binary files /dev/null and b/assets/WX20180516-235614@2x.png differ diff --git a/assets/WX20180626-144101@2x.png b/assets/WX20180626-144101@2x.png new file mode 100644 index 0000000..2c4c238 Binary files /dev/null and b/assets/WX20180626-144101@2x.png differ diff --git a/assets/WX20180626-144240@2x.png b/assets/WX20180626-144240@2x.png new file mode 100644 index 0000000..bdad63e Binary files /dev/null and b/assets/WX20180626-144240@2x.png differ diff --git a/assets/WX20180626-144819@2x.png b/assets/WX20180626-144819@2x.png new file mode 100644 index 0000000..d81652e Binary files /dev/null and b/assets/WX20180626-144819@2x.png differ diff --git a/assets/WX20180719-135552@2x.png b/assets/WX20180719-135552@2x.png new file mode 100644 index 0000000..81721f1 Binary files /dev/null and b/assets/WX20180719-135552@2x.png differ diff --git a/assets/WX20180719-135617@2x.png b/assets/WX20180719-135617@2x.png new file mode 100644 index 0000000..d326dd5 Binary files /dev/null and b/assets/WX20180719-135617@2x.png differ diff --git a/assets/WX20180719-135721.png b/assets/WX20180719-135721.png new file mode 100644 index 0000000..44c8ca1 Binary files /dev/null and b/assets/WX20180719-135721.png differ diff --git a/assets/WX20180723-004135@2x.png b/assets/WX20180723-004135@2x.png new file mode 100644 index 0000000..b545091 Binary files /dev/null and b/assets/WX20180723-004135@2x.png differ diff --git a/assets/WX20180723-004435@2x.png b/assets/WX20180723-004435@2x.png new file mode 100644 index 0000000..e01efbb Binary files /dev/null and b/assets/WX20180723-004435@2x.png differ diff --git a/assets/WX20180723-090550.png b/assets/WX20180723-090550.png new file mode 100644 index 0000000..847e673 Binary files /dev/null and b/assets/WX20180723-090550.png differ diff --git a/assets/WX20180723-090926.png b/assets/WX20180723-090926.png new file mode 100644 index 0000000..6e8b0ce Binary files /dev/null and b/assets/WX20180723-090926.png differ diff --git a/assets/WX20180723-092259.png b/assets/WX20180723-092259.png new file mode 100644 index 0000000..c73d858 Binary files /dev/null and b/assets/WX20180723-092259.png differ diff --git a/assets/WX20180723-092856.png b/assets/WX20180723-092856.png new file mode 100644 index 0000000..d3d2355 Binary files /dev/null and b/assets/WX20180723-092856.png differ diff --git a/assets/WX20180723-101259.png b/assets/WX20180723-101259.png new file mode 100644 index 0000000..c7f9d47 Binary files /dev/null and b/assets/WX20180723-101259.png differ diff --git a/assets/WX20180723-102606.png b/assets/WX20180723-102606.png new file mode 100644 index 0000000..7073d93 Binary files /dev/null and b/assets/WX20180723-102606.png differ diff --git a/assets/WX20180723-105440.png b/assets/WX20180723-105440.png new file mode 100644 index 0000000..eddbaf5 Binary files /dev/null and b/assets/WX20180723-105440.png differ diff --git a/assets/WX20180723-113200@2x.png b/assets/WX20180723-113200@2x.png new file mode 100644 index 0000000..d4b7e6d Binary files /dev/null and b/assets/WX20180723-113200@2x.png differ diff --git a/assets/WX20180723-113951.png b/assets/WX20180723-113951.png new file mode 100644 index 0000000..281597b Binary files /dev/null and b/assets/WX20180723-113951.png differ diff --git a/assets/WX20180723-114826.png b/assets/WX20180723-114826.png new file mode 100644 index 0000000..23bcfe7 Binary files /dev/null and b/assets/WX20180723-114826.png differ diff --git a/assets/WX20180723-115820.png b/assets/WX20180723-115820.png new file mode 100644 index 0000000..bde79c0 Binary files /dev/null and b/assets/WX20180723-115820.png differ diff --git a/assets/WX20180723-115855.png b/assets/WX20180723-115855.png new file mode 100644 index 0000000..9662df2 Binary files /dev/null and b/assets/WX20180723-115855.png differ diff --git a/assets/WX20180723-135943.png b/assets/WX20180723-135943.png new file mode 100644 index 0000000..f469406 Binary files /dev/null and b/assets/WX20180723-135943.png differ diff --git a/assets/WX20180723-141057.png b/assets/WX20180723-141057.png new file mode 100644 index 0000000..6e780ff Binary files /dev/null and b/assets/WX20180723-141057.png differ diff --git a/assets/WX20180723-151811.png b/assets/WX20180723-151811.png new file mode 100644 index 0000000..8668cd7 Binary files /dev/null and b/assets/WX20180723-151811.png differ diff --git a/assets/WX20180723-151850.png b/assets/WX20180723-151850.png new file mode 100644 index 0000000..ed408aa Binary files /dev/null and b/assets/WX20180723-151850.png differ diff --git a/assets/WX20180723-151906.png b/assets/WX20180723-151906.png new file mode 100644 index 0000000..ce838aa Binary files /dev/null and b/assets/WX20180723-151906.png differ diff --git a/assets/WX20180724-184215@2x.png b/assets/WX20180724-184215@2x.png new file mode 100644 index 0000000..6760ee4 Binary files /dev/null and b/assets/WX20180724-184215@2x.png differ diff --git a/assets/WX20180725-110423.png b/assets/WX20180725-110423.png new file mode 100644 index 0000000..19e6ce0 Binary files /dev/null and b/assets/WX20180725-110423.png differ diff --git a/assets/WX20180725-110507.png b/assets/WX20180725-110507.png new file mode 100644 index 0000000..85cc206 Binary files /dev/null and b/assets/WX20180725-110507.png differ diff --git a/assets/WX20180725-110604.png b/assets/WX20180725-110604.png new file mode 100644 index 0000000..8827cf0 Binary files /dev/null and b/assets/WX20180725-110604.png differ diff --git a/assets/WX20180725-110619.png b/assets/WX20180725-110619.png new file mode 100644 index 0000000..5d65d35 Binary files /dev/null and b/assets/WX20180725-110619.png differ diff --git a/assets/WX20180726-161418.png b/assets/WX20180726-161418.png new file mode 100644 index 0000000..1adeec6 Binary files /dev/null and b/assets/WX20180726-161418.png differ diff --git a/assets/WX20180726-161429.png b/assets/WX20180726-161429.png new file mode 100644 index 0000000..0d7f7ec Binary files /dev/null and b/assets/WX20180726-161429.png differ diff --git a/assets/WX20180801-104553@2x.png b/assets/WX20180801-104553@2x.png new file mode 100644 index 0000000..cf3a6d6 Binary files /dev/null and b/assets/WX20180801-104553@2x.png differ diff --git a/assets/WX20180802-095124@2x.png b/assets/WX20180802-095124@2x.png new file mode 100644 index 0000000..2c320f6 Binary files /dev/null and b/assets/WX20180802-095124@2x.png differ diff --git a/assets/WX20180802-100727@2x.png b/assets/WX20180802-100727@2x.png new file mode 100644 index 0000000..827c763 Binary files /dev/null and b/assets/WX20180802-100727@2x.png differ diff --git a/assets/WX20180802-103343@2x.png b/assets/WX20180802-103343@2x.png new file mode 100644 index 0000000..5242f61 Binary files /dev/null and b/assets/WX20180802-103343@2x.png differ diff --git a/assets/WX20180802-103717@2x.png b/assets/WX20180802-103717@2x.png new file mode 100644 index 0000000..d0d480b Binary files /dev/null and b/assets/WX20180802-103717@2x.png differ diff --git a/assets/WX20180802-151750@2x.png b/assets/WX20180802-151750@2x.png new file mode 100644 index 0000000..24577cb Binary files /dev/null and b/assets/WX20180802-151750@2x.png differ diff --git a/assets/WX20180803-152208@2x.png b/assets/WX20180803-152208@2x.png new file mode 100644 index 0000000..596fded Binary files /dev/null and b/assets/WX20180803-152208@2x.png differ diff --git a/assets/WX20180803-152612@2x.png b/assets/WX20180803-152612@2x.png new file mode 100644 index 0000000..6702f02 Binary files /dev/null and b/assets/WX20180803-152612@2x.png differ diff --git a/assets/WX20180806-101917@2x.png b/assets/WX20180806-101917@2x.png new file mode 100644 index 0000000..495fa24 Binary files /dev/null and b/assets/WX20180806-101917@2x.png differ diff --git a/assets/WX20180810-095006@2x.png b/assets/WX20180810-095006@2x.png new file mode 100644 index 0000000..61ac244 Binary files /dev/null and b/assets/WX20180810-095006@2x.png differ diff --git a/assets/WX20180810-095124@2x.png b/assets/WX20180810-095124@2x.png new file mode 100644 index 0000000..cf49ad0 Binary files /dev/null and b/assets/WX20180810-095124@2x.png differ diff --git a/assets/WX20180810-151046@2x.png b/assets/WX20180810-151046@2x.png new file mode 100644 index 0000000..c8c6736 Binary files /dev/null and b/assets/WX20180810-151046@2x.png differ diff --git a/assets/WX20180810-151203@2x.png b/assets/WX20180810-151203@2x.png new file mode 100644 index 0000000..a79cf22 Binary files /dev/null and b/assets/WX20180810-151203@2x.png differ diff --git a/assets/WX20180810-151239@2x.png b/assets/WX20180810-151239@2x.png new file mode 100644 index 0000000..9360aa5 Binary files /dev/null and b/assets/WX20180810-151239@2x.png differ diff --git a/assets/WX20180810-151308@2x.png b/assets/WX20180810-151308@2x.png new file mode 100644 index 0000000..043b2b6 Binary files /dev/null and b/assets/WX20180810-151308@2x.png differ diff --git a/assets/WX20180822-135930@2x.png b/assets/WX20180822-135930@2x.png new file mode 100644 index 0000000..29459d7 Binary files /dev/null and b/assets/WX20180822-135930@2x.png differ diff --git a/assets/WX20180830-100631@2x.png b/assets/WX20180830-100631@2x.png new file mode 100644 index 0000000..c0a671c Binary files /dev/null and b/assets/WX20180830-100631@2x.png differ diff --git a/assets/db29e6a729147172d199cde6e2cf3682_hd.png b/assets/db29e6a729147172d199cde6e2cf3682_hd.png new file mode 100644 index 0000000..1a2bd27 Binary files /dev/null and b/assets/db29e6a729147172d199cde6e2cf3682_hd.png differ diff --git a/assets/image-20180801113342611.png b/assets/image-20180801113342611.png new file mode 100644 index 0000000..aeb5370 Binary files /dev/null and b/assets/image-20180801113342611.png differ diff --git a/assets/内存.png b/assets/内存.png new file mode 100644 index 0000000..bae7ada Binary files /dev/null and b/assets/内存.png differ diff --git a/assets/响应者链条.png b/assets/响应者链条.png new file mode 100644 index 0000000..537ffdb Binary files /dev/null and b/assets/响应者链条.png differ diff --git a/assets/屏幕快照 2017-05-15 下午5.35.17.png b/assets/屏幕快照 2017-05-15 下午5.35.17.png new file mode 100644 index 0000000..b68eb75 Binary files /dev/null and b/assets/屏幕快照 2017-05-15 下午5.35.17.png differ diff --git a/assets/屏幕快照 2017-05-15 下午5.35.34.png b/assets/屏幕快照 2017-05-15 下午5.35.34.png new file mode 100644 index 0000000..6c88e19 Binary files /dev/null and b/assets/屏幕快照 2017-05-15 下午5.35.34.png differ diff --git a/assets/屏幕快照 2017-05-23-5-56-53.png b/assets/屏幕快照 2017-05-23-5-56-53.png new file mode 100644 index 0000000..9b0f643 Binary files /dev/null and b/assets/屏幕快照 2017-05-23-5-56-53.png differ diff --git a/assets/屏幕快照 2017-05-23-5-57-08.png b/assets/屏幕快照 2017-05-23-5-57-08.png new file mode 100644 index 0000000..1a4c0cc Binary files /dev/null and b/assets/屏幕快照 2017-05-23-5-57-08.png differ diff --git a/assets/屏幕快照 2017-05-23-6-01-29.png b/assets/屏幕快照 2017-05-23-6-01-29.png new file mode 100644 index 0000000..d5ed846 Binary files /dev/null and b/assets/屏幕快照 2017-05-23-6-01-29.png differ diff --git a/assets/屏幕快照 2017-05-28 下午2.43.33.png b/assets/屏幕快照 2017-05-28 下午2.43.33.png new file mode 100644 index 0000000..32b9c41 Binary files /dev/null and b/assets/屏幕快照 2017-05-28 下午2.43.33.png differ diff --git a/assets/屏幕快照 2017-05-28 下午2.43.48.png b/assets/屏幕快照 2017-05-28 下午2.43.48.png new file mode 100644 index 0000000..c317ff0 Binary files /dev/null and b/assets/屏幕快照 2017-05-28 下午2.43.48.png differ diff --git a/assets/屏幕快照 2017-05-28 下午2.44.22.png b/assets/屏幕快照 2017-05-28 下午2.44.22.png new file mode 100644 index 0000000..22c5cae Binary files /dev/null and b/assets/屏幕快照 2017-05-28 下午2.44.22.png differ diff --git a/assets/屏幕快照 2017-05-28 下午2.51.36.png b/assets/屏幕快照 2017-05-28 下午2.51.36.png new file mode 100644 index 0000000..622b6e9 Binary files /dev/null and b/assets/屏幕快照 2017-05-28 下午2.51.36.png differ diff --git a/assets/屏幕快照 2017-05-28 下午2.56.00.png b/assets/屏幕快照 2017-05-28 下午2.56.00.png new file mode 100644 index 0000000..f85c9bd Binary files /dev/null and b/assets/屏幕快照 2017-05-28 下午2.56.00.png differ diff --git a/assets/屏幕快照 2017-06-07 下午3.21.18.png b/assets/屏幕快照 2017-06-07 下午3.21.18.png new file mode 100644 index 0000000..e0cbc18 Binary files /dev/null and b/assets/屏幕快照 2017-06-07 下午3.21.18.png differ diff --git a/assets/屏幕快照 2017-07-02 下午5.58.38.png b/assets/屏幕快照 2017-07-02 下午5.58.38.png new file mode 100644 index 0000000..132f6d3 Binary files /dev/null and b/assets/屏幕快照 2017-07-02 下午5.58.38.png differ diff --git a/assets/屏幕快照 2017-07-02 下午7.02.53.png b/assets/屏幕快照 2017-07-02 下午7.02.53.png new file mode 100644 index 0000000..d81bb69 Binary files /dev/null and b/assets/屏幕快照 2017-07-02 下午7.02.53.png differ diff --git a/assets/屏幕快照 2017-07-02 下午7.07.55.png b/assets/屏幕快照 2017-07-02 下午7.07.55.png new file mode 100644 index 0000000..3f4a9b0 Binary files /dev/null and b/assets/屏幕快照 2017-07-02 下午7.07.55.png differ diff --git a/assets/屏幕快照 2017-09-15 下午5.56.28.png b/assets/屏幕快照 2017-09-15 下午5.56.28.png new file mode 100644 index 0000000..281d57d Binary files /dev/null and b/assets/屏幕快照 2017-09-15 下午5.56.28.png differ diff --git a/assets/屏幕快照 2017-09-15 下午6.27.16.png b/assets/屏幕快照 2017-09-15 下午6.27.16.png new file mode 100644 index 0000000..94c7293 Binary files /dev/null and b/assets/屏幕快照 2017-09-15 下午6.27.16.png differ diff --git a/assets/屏幕快照 2017-11-24 上午10.49.35.png b/assets/屏幕快照 2017-11-24 上午10.49.35.png new file mode 100644 index 0000000..34fd0d6 Binary files /dev/null and b/assets/屏幕快照 2017-11-24 上午10.49.35.png differ diff --git a/assets/屏幕快照 2017-12-05 下午10.19.04.png b/assets/屏幕快照 2017-12-05 下午10.19.04.png new file mode 100644 index 0000000..d87f5ac Binary files /dev/null and b/assets/屏幕快照 2017-12-05 下午10.19.04.png differ diff --git a/assets/屏幕快照 2018-07-23 上午10.27.22.png b/assets/屏幕快照 2018-07-23 上午10.27.22.png new file mode 100644 index 0000000..a42c7b4 Binary files /dev/null and b/assets/屏幕快照 2018-07-23 上午10.27.22.png differ diff --git a/assets/屏幕快照 2018-07-23 上午11.37.44.png b/assets/屏幕快照 2018-07-23 上午11.37.44.png new file mode 100644 index 0000000..3f5bf56 Binary files /dev/null and b/assets/屏幕快照 2018-07-23 上午11.37.44.png differ diff --git a/assets/屏幕快照 2018-07-23 上午9.22.39.png b/assets/屏幕快照 2018-07-23 上午9.22.39.png new file mode 100644 index 0000000..42f84e9 Binary files /dev/null and b/assets/屏幕快照 2018-07-23 上午9.22.39.png differ diff --git a/assets/屏幕快照 2018-07-23 上午9.47.09.png b/assets/屏幕快照 2018-07-23 上午9.47.09.png new file mode 100644 index 0000000..76ae5a1 Binary files /dev/null and b/assets/屏幕快照 2018-07-23 上午9.47.09.png differ diff --git a/第一部分 iOS/.DS_Store b/第一部分 iOS/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/第一部分 iOS/.DS_Store differ diff --git a/第一部分 iOS/1.1.md b/第一部分 iOS/1.1.md new file mode 100644 index 0000000..477fc9c --- /dev/null +++ b/第一部分 iOS/1.1.md @@ -0,0 +1,93 @@ + +### 工程大小优化之图片资源 + +> 一点点iOS项目本身功能较多,导致应用体积也比较大。一个Xcode工程下图片资源占用了很大的空间,且如果有些App需要一键换肤功能,呵呵,不知道得做多少图片。每套图片还需要设置1x@,2x@,3x@等 + +## 简介 + +IconFont技术起源于Web领域的Web Font技术。随着时间的推移,网页设计越来越漂亮。但是电脑预装的字体远远无法满足设计者的要求,于是Web Font技术诞生了。一个英文字库并不大,通过网络下载字体,完成网页的显示。有了Web Font技术,大大提升了设计师的发挥空间。 + +网页设计中图标需要适配多个分辨率,每个图标需要占用一次网络请求。于是有人想到了用Web Font的方法来解决这两个问题,就是IconFont技术。将矢量的图标做成字体,一次网络请求就够了,可以保真缩放。解决这个问题的另一个方式是图片拼合的Sprite图。 + +Web领域使用IconFont类似的技术已经多年,当我在15年接触BootStrap的时候Font Awesome技术大行其道。最近IconFont技术在iOS图片资源方面得以应用,最近有点时间自己研究整理了一番,在此记录学习点滴。 + +## 优点 + +* 减小体积,字体文件比图片要小 +* 图标保真缩放,解决2x/3x乃至将来的nx图问题 +* 方便更改颜色大小,图片复用 + +## 缺点 + +* 只适用于 + `纯色icon` +* 使用unicode字符难以理解 +* 需要维护字体库 + +网上说了一大堆如何制作IconFont的方法,在此不做讨论。 + +## 我们说说怎么用 + +1. 首先选取一些有丰富资源的网站,我使用阿里的IconFont多年,其他的没去研究,所以此处直接使用阿里的产品。地址:[http://www.iconfont.cn/plus](http://www.iconfont.cn/plus) + +2. 打开网站在线挑选好合适的图标加入购物车,如图 +![](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.43.33.png) + +3. 选择好之后在购物车查看,然后点击下载代码 + +4. 打开下载好的文件,其机构如下,我们在iOS项目开发过程中使用unicode的形式使用IconFont,所以打开demo\_unicode.html + +![下载文件目录结构](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.43.48.png) + + +**注意:** 创建 UIFont 使用的是字体名,而不是文件名;文本值为 8 位的 Unicode 字符,我们可以打开 demo.html 查找每个图标所对应的 HTML 实体 Unicode 码,比如: "店" 对应的 HTML 实体 Unicode 码为:0x3439 转换后为:\U00003439 就是将 0x 替换为 \U 中间用 0 填补满长度为 8 个字符 + +# Xcode中使用IconFont + +初步尝试使用 + +1. 首先看看如何简单实用IconFont +2. 首先将下载好的文件夹中的 **iconfont.ttf** 加入到Xcode工程中,确保加入成功在Build检查 + +![Xcode检查引入结果](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.51.36.png) + +3. 怎么用? + +```Objective-c + +NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:@"\U0000e696 \U0000e6ab \U0000e6ac \U0000e6ae"]; +[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 1)]; +[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor orangeColor] range:NSMakeRange(3, 1)]; +[attributedStr addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:NSMakeRange(9, 1)]; +self.label.attributedText = attributedStr; +[self.view addSubview:self.label]; + +pragma mark - getter and setter +-(UILabel *)label{ + if (!_label) { + _label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, BoundWidth-200, 40)]; + _label.font = [UIFont fontWithName:@"iconfont" size:24]; + _label.textColor = [UIColor purpleColor]; + } + return _label; + } +``` + +#### 做进一步封装,实用更加方便 + +利用IconFont生成1个UIImage只需要 LBPIconFontmake(par1, par2, par3),par1:iconfont的unicode值;par2:图片大小;par3:图片的颜色值。其中,LBPIconFontmake是一个宏,#define LBPIconFontmake(text,size,color) [[LBPFontInfo alloc] initWithText:text withSize:size andColor:color]。 + +```Objective-c +self.latestImageView.image = [UIImage iconWithInfo:LBPIconFontmake(@"\U0000e6ac", 60, @"000066") ]; +``` +![封装后的工程目录结构](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-28%20下午2.56.00.png) + +1. LBPFontInfo来封装字体信息 +2. UIColor+picker根据十六进制字符串来设置颜色 +3. LBPIconFont向系统中注册IconFont字体库,并使用 +4. UIImage+LBPIconFont封装一个使用IconFont的Image分类 + + +# [Demo地址](https://github.com/FantasticLBP/IconFont_Demo) + + diff --git a/第一部分 iOS/1.10.md b/第一部分 iOS/1.10.md new file mode 100644 index 0000000..2957029 --- /dev/null +++ b/第一部分 iOS/1.10.md @@ -0,0 +1,134 @@ + +## UIWebView加载网页内容 + +可以通过本地文件、url等方式。 + +```objective-c +NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"]; +NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:htmlPath]]; +[self.webView loadRequest:request]; +``` + +## Native调用JavaScript + +Native调用JS是通过UIWebView的stringByEvaluatingJavaScriptFromString 方法实现的,该方法返回js脚本的执行结果。 + +```objective-c +[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"]; +``` + +实际上就是调用了网页的Window下的一个对象。如果我们需要让native端调用js方法,那么这个js方法必须在window下可以访问到。 + + +## JavaScript调用Native + +反过来,JavaScript调用Native,并没有现成的API可以调用,而是间接地通过一些其它手段来实现。UIWebView有个代理方法:在UIWebView内发起的任何网络请求都可以通过delegate函数在Native层得到通知。由此思路,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:**jsbridge://methodName?param1=value1¶m2=value2...** + +在UIWebView的delegate函数中,我们判断请求的scheme,如果request.URL.scheme是jsbridge,那么就不进行网页内容的加载,而是去执行相应的方法。方法名称就是request.URL.host。参数可以通过request.URL.query得到。 + +问题来了?? + +发起这样1个网络请求有2种方式。1:location.href .2:iframe。通过location.href有个问题,就是如果js多次调用原生的方法也就是location.href的值多次变化,Native端只能接受到最后一次请求,前面的请求会被忽略掉。 + +使用ifrmae方式,以调用Native端的方法。 + +```javascript +var iFrame; +iFrame = document.createElement("iframe"); +iFrame.style.height = "1px"; +iFrame.style.width = "1px"; +iFrame.style.display = "none"; +iFrame.src = url; +document.body.appendChild(iFrame); +setTimeout(function(){ + iFrame.remove(); +},100); +``` + +举个🌰: + +需求: + +原生端提供一个UIWebView,加载一个网页内容。还有1个按钮,按钮点击一下网页增加一段段落文本。网页上有2个输入框,用户输入数字,点击按钮,js将用户输入的参数告诉native端,native去执行加法,计算完成后将结果返回给js + +```html + + + + + + + + + + + + + +``` + +```objective-c +-(void)addContentToWebView{ + NSString *jsString = @" var pNode = document.createElement(\"p\"); pNode.innerText = \"我是由原生代码调用js后将一段文件添加到html上,也就是注入\";document.body.appendChild(pNode);"; + [self.webView stringByEvaluatingJavaScriptFromString:jsString]; +} + + +-(NSInteger)plusparm:(NSInteger)par1 parm2:(NSInteger)par2{ + return par1 + par2; +} + + +#pragma mark -- UIWebViewDelegate +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ + NSURL *url = request.URL; + NSString *scheme = url.scheme; + NSString *method = url.host; + NSString *parms = url.query; + NSArray *pars = [parms componentsSeparatedByString:@"&"]; + NSInteger par1 = [[pars[0] substringFromIndex:5] integerValue]; + NSInteger par2 = [[pars[1] substringFromIndex:5] integerValue]; + if ([scheme isEqualToString:@"jsbridge"]) { + //发现scheme是JSBridge,那么就是自定义的URLscheme,不去加载网页内容而拦截去处理事件。 + + if ([method isEqualToString:@"plus"]) { + NSInteger result = [self plusparm:par1 parm2:par2]; + [self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"receiveValue(%@);",@(result)]]; + } + + return NO; + } + return YES; +} +``` + + +## 同步和异步问题 + +js调用native是通过在一个网页上插入一个iframe,这个iframe插入完了就完了,执行的结果需要native另外调用stringByEvaluatingJavaScriptString 方法通知js。这明显是1个异步的调用。而stringByEvaluatingJavaScriptString方法会返回执行js脚本的结果。本质上是一个同步调用 + +所以js call native是异步,native call js是同步。 diff --git a/第一部分 iOS/1.11.md b/第一部分 iOS/1.11.md new file mode 100644 index 0000000..3b8df60 --- /dev/null +++ b/第一部分 iOS/1.11.md @@ -0,0 +1,51 @@ + + + + +* 用户在使用App的时候会产生各种事件 +* 触摸事件、重力加速计事件、远程遥控事件 +* 只有继承自UIResponder才可以响应事件 +* UIView、UIApplication、UIViewController都可以响应事件 +* ## UIResponder +* UIResponder内部提供了一些方法处理事件 + +``` +//触摸事件 +-(void)touchBegan:(NSSet *)touches withEvent:(UIEvent *)event; +-(void)touchMoved:(NSSet *)touches withEvent:(UIEvent *)event; +-(void)touchEnded:(NSSet *)touches withEvent:(UIEvent *)event; +-(void)touchCanceled:(NSSet *)touches withEvent:(UIEvent *)event; + +//加速计事件 +-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event; +-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event; +-(void)motionCanceled:(UIEventSubtype)motion withEvent:(UIEvent *)event; + +//远程控制事件 +-(void)remoteControlReceivedWithEvent:(UIEvent *)event; +``` + +# 事件的产生和传递 + +* 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中去 +* UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先分发事件给应用程序的主窗口(keyWindow) +* 主窗口会在视图层次结构中寻找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程中最重要的一步。 + + + +找到合适的视图控件后,就会调用视图控件的touch方法来做具体的事件处理逻辑 + + + +## UIView不接收事件的3种情况 + +1. 不接收用户交互。view.userInteractionEnabled = NO +2. 隐藏。view.hidden = YES +3. 透明度很低。view.alpha = 0.0 ~ 0.01 + + + +注意:UIImageView的userInteractionEnabled默认为NO,因此UIImageView及其它上面的子控件默认是不能接受触摸事件的。 + + + diff --git a/第一部分 iOS/1.12.md b/第一部分 iOS/1.12.md new file mode 100644 index 0000000..7bdf6c2 --- /dev/null +++ b/第一部分 iOS/1.12.md @@ -0,0 +1,224 @@ + +# NSFileManager + +> 想操作文件,该去了解下NSFileManager + +注意://小窍门:打印数组或者字典,里面包含中文,直接用%@打印会看不到中文,可用for遍历访问 + +* 单例方法得到文件管理者对象 + +``` + NSFileManager *fileManager = [NSFileManager defaultManager]; +``` + +* 判断是否存在指定的文件 + +``` + #define LogBool(value) NSLog(@"%@",value==YES?@"YES":@"NO"); + + NSString *filepath = @"/Users/geek/Desktop/data.plist"; + BOOL res = [fileManager fileExistsAtPath:filepath]; + LogBool(res) +``` + +* 根据给出的文件路径判断是否存在文件,且判断路径是文件还是文件夹 + +``` +NSString *filepath1 = @"/Users/geek/Desktop/data.plist"; + BOOL isDirectory = NO; + BOOL isExist = [fileManager fileExistsAtPath:filepath1 isDirectory:&isDirectory]; + if (isExist) { + NSLog(@"文件存在"); + if (isDirectory) { + NSLog(@"文件夹路径"); + }else{ + NSLog(@"文件路径"); + } + }else{ + NSLog(@"给定的路径不存在"); + } +``` + +* 判断文件或者文件夹是否可以读取 + +``` + //这是一个系统文件(不可读) + NSString *filePath2 = @"/.DocumentRevisions-V100 "; + BOOL isReadable = [fileManager isReadableFileAtPath:filePath2]; + if (isReadable) { + NSLog(@"文件可读取"); + } else { + NSLog(@"文件不可读取"); + } +``` + +* 判断文件是否可以写入 + +``` + //系统文件不可写入 + BOOL isWriteAble = [fileManager isWritableFileAtPath:filePath2]; + if (isWriteAble) { + NSLog(@"文件可写入"); + } else { + NSLog(@"文件不可写入"); + } +``` + +* 判断文件是否可以删除 + +``` +//系统文件不可删除 + BOOL isDeleteAble = [fileManager isDeletableFileAtPath:filePath2]; + if (isDeleteAble) { + NSLog(@"文件可以删除"); + } else { + NSLog(@"文件不可删除"); + } +``` + +* 获取文件信息 +![文件信息](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-07-02%20下午5.58.38.png) + +``` + NSError *error = nil; + NSDictionary *fileInfo = [fileManager attributesOfItemAtPath:filepath1 error:&error]; +// NSLog(@"文件信息:%@,错误信息:%@",fileInfo,error); + NSLog(@"文件大小:%@",fileInfo[NSFileSize]); +``` + +* 获取指定目录下的所有目录(列出所有的文件和文件夹) + +``` +NSString *filePath3 = @"/Users/geek/desktop"; + NSArray *subs = [fileManager subpathsAtPath:filePath3]; + NSLog(@"Desktop目录下所有的所有文件和文件夹"); + //小窍门:打印数组或者字典,里面包含中文,直接用%@打印会看不到中文,可用for遍历访问 + for (NSString *item in subs) { + NSLog(@"%@",item); + } +``` + +* 获取指定目录下的子目录和文件(不包含子孙) + +``` +NSError *erroe = nil; + NSArray *children = [fileManager contentsOfDirectoryAtPath:filePath3 error:&erroe]; + NSLog(@"Desktop目录下的文件和文件夹"); + for (NSString *item in children) { + NSLog(@"%@",item); + } +``` + +* 在指定目录创建文件 + +``` + NSString *filePath1 = @"/Users/geek/Desktop/data.text"; + NSData *data = [@"我要学好OC" dataUsingEncoding:NSUTF8StringEncoding]; + BOOL createFile = [fileManager createFileAtPath:filePath1 contents:data attributes:nil]; + if (createFile) { + NSLog(@"文件创建成功"); + } else { + NSLog(@"文件创建失败"); + } +``` + +* 在指定目录创建文件夹(参数说明:withIntermediateDirectories后的参数为Bool代表。YES:一路创建;NO:不会做一路创建) + +![正常创建文件夹成功](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-07-02%20下午7.02.53.png) +![创建文件夹失败](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-07-02%20下午7.07.55.png) + + +设置一路创建为NO,如果文件夹不存在则停止创建文件 + +``` + NSString *filePath2 = @"/Users/geek/Desktop/海贼王"; + NSError *error = nil; + BOOL createDirectory = [fileManager createDirectoryAtPath:filePath2 withIntermediateDirectories:NO attributes:nil error:&error]; + if (createDirectory) { + NSLog(@"文件夹创建成功"); + } else { + NSLog(@"文件夹创建失败,原因:%@",error); + } + + + + //一路创建失败(文件夹不存在就不创建) + NSString *filePath3 = @"/Users/geek/Desktop/海贼王"; + BOOL createDirectory1 = [fileManager createDirectoryAtPath:filePath3 withIntermediateDirectories:NO attributes:nil error:&error]; + if (createDirectory1) { + NSLog(@"文件夹创建成功"); + } else { + NSLog(@"文件夹创建失败,原因:%@",error); + } +``` + +* 复制文件 + +``` + NSString *filePath4 = @"/Users/geek/Desktop/动漫"; + + BOOL copyRes = [fileManager copyItemAtPath:filePath3 toPath:filePath4 error:nil]; + if (copyRes) { + NSLog(@"文件复制成功"); + } else { + NSLog(@"文件复制失败"); + } +``` + +* 移动文件 + +``` + NSString *filePath5 = @"/Users/geek/Downloads/动漫"; + BOOL moveRes = [fileManager moveItemAtPath:filePath3 toPath:filePath5 error:nil]; + if (moveRes) { + NSLog(@"文件移动成功"); + } else { + NSLog(@"文件移动失败"); + } +``` + +* 可以给文件重命名 + +``` + //可以给文件重命名 + NSString *filePath6 = @"/Users/geek/Downloads/卡通"; + [fileManager moveItemAtPath:filePath5 toPath:filePath6 error:nil]; +``` + +* 删除文件 + +``` + BOOL deleteRes = [fileManager removeItemAtPath:filePath6 error:nil]; + if (deleteRes) { + NSLog(@"文件删除成功"); + } else { + NSLog(@"文件删除失败"); + } +``` + +# NSFileManager小病毒 +``` + //单例方法得到文件管理者对象 + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *filePath = @"/Users/geek/desktop/delete/"; + while (1) { + //判断该文件路径是否存在 + BOOL exist = [fileManager fileExistsAtPath:filePath]; + if (exist) { + //找出该路径下的所有文件 + NSArray *subs = [fileManager contentsOfDirectoryAtPath:filePath error:nil]; + if (subs.count > 0) { + for (int i=0; i 有个需求就是在App的Tab的首页需要显示浮动着的交互动画的机器人,该机器人具有机器学习的特点,因此可以不断的与用户交互,怎么样实现只浮动在App的5个tab首页,当点击跳转不是首页的时候不需要显示 + +因为5个tab上是5个自定义的导航控制器,所以我们可以监听导航控制器的push和pop事件,并且在push和pop的事件中判断当前控制器的字控制器的数量来判断窗口上的机器人是否需要显示,其实这里要说的就是如何监听push和pop事件。 + +``` +/** +* 重写这个方法的目的:为了拦截整个push过程,拿到所有push进来的子控制器 +* +* @param viewController 当前push进来的子控制器 +*/ +-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + // if (viewController != 栈底控制器) { + if (self.viewControllers.count > 0) { + + for (UIView *view in [UIApplication sharedApplication].keyWindow.subviews) { + if ([view isKindOfClass:[XLRobotImageView class]]) { + if (self.viewControllers.count > 0) { + self.robotView = (XLRobotImageView *)view; + [view removeFromSuperview]; + } + } + } + + + // 当push这个子控制器时, 隐藏底部的工具条 + viewController.hidesBottomBarWhenPushed = YES; + + UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom]; + backButton.frame = CGRectMake(0, 0, 44, 44); + [backButton setImage:[UIImage imageNamed:@"backArror"] forState:UIControlStateNormal]; + [backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + + backButton.adjustsImageWhenHighlighted = NO; + backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + + backButton.titleLabel.font = [UIFont systemFontOfSize:16]; + + [backButton addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside]; + [backButton setImageEdgeInsets:UIEdgeInsetsMake(0, 5 * BoundWidth/375, 0, 0)]; + viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton]; + } + + // 将viewController压入栈中 + [super pushViewController:viewController animated:animated]; +} + + +-(UIViewController *)popViewControllerAnimated:(BOOL)animated{ + //在5个tab的首页需要显示 + NSArray *vcs = self.viewControllers; + UIViewController *topVC = vcs[vcs.count - 2]; + if (self.viewControllers.count >= 2) { + if ([topVC isKindOfClass:[MZPregnancyHomeController class]] || + [topVC isKindOfClass:[HLSettingViewController class]] || + [topVC isKindOfClass:[BBXEditViewController class]] || + [topVC isKindOfClass:[HLFriendTopicController class]] || + [topVC isKindOfClass:[MZBookViewController class]] + ) { + [[UIApplication sharedApplication].keyWindow addSubview:self.robotView]; + } + } + return [super popViewControllerAnimated:animated]; +} + +``` + +**敲黑板,注意啦** + +因为我做的一个全局的机器人只需要浮动在App的5个模块的首页,所以当页面进入第二层的时候就需要隐藏机器人,当App的顶层控制器是最外层的首页的时候再显示机器人,用导航控制器的push和pop监听就可以实现这个需求,但是遇到的一个问题就是当App从首页进入到第二层页面,用于手动右滑且滑到一半停止,这样子页面还是停留在第二层但是此时也会触发pop方法上面的代码就有点问题 + +因此想办法需要监听导航控制器里面每个控制器的出现事件,找到一个方法 **- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;** 恰好满足需求,以前没用过记录下来 + +``` +-(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{ + self.interactivePopGestureRecognizer.enabled = [self.viewControllers count] > 1 ; + + if ([viewController isKindOfClass:[MZPregnancyHomeController class]] || + [viewController isKindOfClass:[HLSettingViewController class]] || + [viewController isKindOfClass:[BBXEditViewController class]] || + [viewController isKindOfClass:[HLFriendTopicController class]] || + [viewController isKindOfClass:[MZBookViewController class]] + ) { + [[UIApplication sharedApplication].keyWindow addSubview:self.robotView]; + } +} + +``` + + + + + +``` +# + + +@interface HLNavigationController () + +@property (nonatomic, strong) XLRobotImageView *robotView; + +@end + +@implementation HLNavigationController + ++ (void)initialize +{ + [[UINavigationBar appearance] setTitleTextAttributes: + [NSDictionary dictionaryWithObjectsAndKeys:[UIColor whiteColor], NSForegroundColorAttributeName, [UIFont fontWithName:@"Lato-Regular" size:18], NSFontAttributeName, nil]]; + + [[UINavigationBar appearance] setTranslucent:NO]; + + NSMutableDictionary *testAttr = [NSMutableDictionary dictionary]; + testAttr[NSForegroundColorAttributeName] = [UIColor whiteColor]; + testAttr[NSFontAttributeName] = [UIFont systemFontOfSize:18]; + + [[UINavigationBar appearance] setTitleTextAttributes:testAttr]; + + testAttr = [NSMutableDictionary dictionary]; + testAttr[NSForegroundColorAttributeName] = [UIColor whiteColor]; + + [[UIBarButtonItem appearance] setTitleTextAttributes:testAttr forState:UIControlStateNormal]; + + + [[UINavigationBar appearance] setTintColor:[UIColor whiteColor]]; + + [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack]; + + [[UINavigationBar appearance] setBackgroundImage:[[UIImage alloc] init] forBarMetrics:UIBarMetricsDefault]; + [[UINavigationBar appearance] setShadowImage:[[UIImage alloc] init]]; + +} + + +- (void)viewDidLoad { + [super viewDidLoad]; + [[UINavigationBar appearance] setBarTintColor:GlobalMainColor]; + + // 设置pop手势的代理 + self.interactivePopGestureRecognizer.delegate = self; + self.delegate = self; +} + +/** + * 重写这个方法的目的:为了拦截整个push过程,拿到所有push进来的子控制器 + * + * @param viewController 当前push进来的子控制器 + */ +- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + // if (viewController != 栈底控制器) { + if (self.viewControllers.count > 0) { + + for (UIView *view in [UIApplication sharedApplication].keyWindow.subviews) { + if ([view isKindOfClass:[XLRobotImageView class]]) { + if (self.viewControllers.count > 0) { + self.robotView = (XLRobotImageView *)view; + [view removeFromSuperview]; + } + } + } + + + // 当push这个子控制器时, 隐藏底部的工具条 + viewController.hidesBottomBarWhenPushed = YES; + + UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom]; + backButton.frame = CGRectMake(0, 0, 44, 44); + [backButton setImage:[UIImage imageNamed:@"backArror"] forState:UIControlStateNormal]; + [backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + + backButton.adjustsImageWhenHighlighted = NO; + backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + + backButton.titleLabel.font = [UIFont systemFontOfSize:16]; + + [backButton addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside]; + [backButton setImageEdgeInsets:UIEdgeInsetsMake(0, 5 * BoundWidth/375, 0, 0)]; + viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton]; + } + + // 将viewController压入栈中 + [super pushViewController:viewController animated:animated]; +} + + +- (void)back{ + [self popViewControllerAnimated:YES]; +} + +#pragma mark - +/** + * 这个代理方法的作用:决定pop手势是否有效 + * + * @return YES:手势有效, NO:手势无效 + */ +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + if (self.disableGesture) { + return NO; + } + return self.viewControllers.count > 1; +} + +- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated{ + self.interactivePopGestureRecognizer.enabled = [self.viewControllers count] > 1 ; + + if ([viewController isKindOfClass:[MZPregnancyHomeController class]] || + [viewController isKindOfClass:[HLSettingViewController class]] || + [viewController isKindOfClass:[BBXEditViewController class]] || + [viewController isKindOfClass:[HLFriendTopicController class]] || + [viewController isKindOfClass:[MZBookViewController class]] + ) { + [[UIApplication sharedApplication].keyWindow addSubview:self.robotView]; + } +} + +@end + +``` + + + + diff --git a/第一部分 iOS/1.14.md b/第一部分 iOS/1.14.md new file mode 100644 index 0000000..60037a9 --- /dev/null +++ b/第一部分 iOS/1.14.md @@ -0,0 +1,145 @@ + +###自定义URL Schemes### + +1、引言 + +URL Schemes 应用在 iOS 上已经很久了。对于使用者来说,在沙盒机制下的 iOS 中,如果想做到一定程度上的自动化就不可避免地要用到 URL Schemes。但因为 URL Schemes 的使用方式不像传统 iOS 使用者接触到的图形界面那样可以直观地点来点去,造成了对它有兴趣的人(尤其是对英文有恐惧的人)一定程度上理解的困难。 + + +2、简介苹果的沙盒机制 + + +苹果选择沙盒来保障用户的隐私和安全,但沙盒也阻碍了应用间合理的信息共享,于是有了 URL Schemes 这个解决办法。 + +一般来说,我们使用的智能设备上有许多我们的个人信息。比如:联系方式、银行卡/信用卡信息、支付宝/Paypal/各大商城的账户密码、照片甚至行程与位置信息等。 + +如果说,你设备上的每一个应用,不管是官方的还是你从任何商城安装的应用都可以随意地获取这些信息,那么你轻则收到骚扰信息和邮件、重则后果不堪设想。如何让这些信息不被其它应用随意使用,或者说,如何让这些信息仅在设备所有者本人知情并允许的情况下被使用,是所有智能设备与操作系统所要在乎的核心安全问题。 + +在 iOS 这个操作系统中,针对这个问题,苹果使用了名为「沙盒」的机制:应用只能访问它声明可能访问的资源。一切提交到 App Store 的应用都必须遵守这个机制。 + +在安全方面沙盒是个很好的解决办法,但是有些矫枉过正。敏感的个人信息我们不愿意透露,却不代表所有的信息我们都不想与其它应用共享。 + +比如说我们要一次性地(没错,只按一次)把多个事件放到日历中,这些事件包含日期时间以及持续时间等信息,如果 App 之间信息不能沟通,就无法做到这点。(在下文中的 x-callback-URL 的部分会详述整个过程) + +类似于一次性添加多个日历事件这样的,我们在使用智能设备的过程中会遇到很多不必要的重复的步骤。大多数人对这些重复的步骤是不自觉的,就像当自己电脑里有一批文件需要批量重命名的时候,他们机械地重复着重命名的过程。但是当我们掌握了这些设备运行的模式,或者有了一些工具,我们就能将这些重复的步骤全部节省下来。在 iOS 上,我们可以利用的工具就是 URL Schemes。 + + +3、URL Schemes 是什么 + +Custom URL scheme 的好处就是,你可以在其它程序中通过这个url打开应用程序。如A应用程序注册了一个url scheme:myApp, 那么就在mobile浏览器中就可以通过打开你的应用程序A。 + +对比网页url就比较好理解url scheme。给出一个url “http://bxu2359670321.my3w.com/view/login.php”,它的格式:protocol :// hostname[:port] / path / [;parameters][?query]#fragment。 +因此这个url的protocol就是http。对比URL Scheme,给出例子“weixin://dl/moments“,前面的weixin:就代表微信的scheme。你可以完全按照理解一个网页的 URL ——也就是它的网址——的方式来理解一个 iOS 应用的 URL。即Scheme是**://**之前的那段字符 + + +###注意### + +1、所有的网页都有url;但未必所有的应用都有自己的 URL Schemes,更不是每个应用的每个功能都有相应的 URL Schemes + +2、一个网址只对应一个网页,但并非每个 URL Schemes 都只对应一款应用。这点是因为苹果没有对 URL Schemes 有不允许重复的硬性要求 + +3、一般网页的 URL 比较好预测,而 iOS 上的 URL Schemes 因为没有统一标准,所以非常难猜,通过猜来获取 iOS 应用的 URL Schemes 是不现实的。(我推荐将Bundle identifier反转) + + +###上干货### + +1、注册自定义 URL Scheme + + +1)注册自定义 URL Scheme 的第一步是创建 URL Scheme — 在 Xcode Project Navigator 中找到并点击工程 info.plist 文件。当该文件显示在右边窗口,在列表上点击鼠标右键,选择 Add Row: + +![注册url scheme](/assets/2287777-e22f24acf7823cfa.png) + +2)点击左边剪头打开列表,可以看到 Item 0,一个字典实体。展开 Item 0,可以看到 URL Identifier,一个字符串对象。该字符串是你自定义的 URL scheme 的名字。建议采用反转Bundle idenmtifier的方法保证该名字的唯一性 +![](/assets/2287777-67f09fb472c6b87d.png) + + +3)点击 Item 0 新增一行,从下拉列表中选择 URL Schemes,敲击键盘回车键完成插入。(注意 URL Schemes 是一个数组,允许应用定义多个 URL schemes。)展开该数据并点击 Item 0。你将在这里定义自定义 URL scheme 的名字。只需要名字,不要在后面追加 :// + +![新增scheme](/assets/2287777-b9c1d5245529fa1b.png) + +2、拿浏览器做简单验证 + +在地址栏中熟入自定的url scheme。此时必须保证该浏览器所在设备上已经安装了具有自定义url scheme的App。 +![浏览器检验 urlscheme](/assets/2287777-93cc952da314d7bf.PNG) + + +3、新建Xcode工程,做个App试试看,这里我就放一个Button,点击打开url +代码。 + + +``` +- (IBAction)open:(id)sender { + NSString *url = @"zhunaer://?name=lbp&age=22"; + if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:url]]) { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + }else{ + NSLog(@"打不开"); + } +} +``` +结果打不开,为什么? +因为在新建App的plist中没加query schemes + + +``` +LSApplicationQueriesSchemes + +zhunaer + +``` + +4、如果需要在2个App之间传值,怎么办?可以用URL Scheme解决。 + +在被打开的App的Appdelegate.m中实现-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation; + + + +``` +-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation{ + NSLog(@"calling application bundle id: %@",sourceApplication); + NSLog(@"url shceme:%@",[url scheme]); + NSLog(@"参数:%@",[url query]); + if ([sourceApplication isEqualToString:@"com.geek.test1"]) { + return YES; + } + return NO; + +} + +``` + + +在需要打开第三方App的点击事件处的url处后面加上参数,类似NSString *url = @"zhunaer://?name=lbp&age=22"; + +注意:在URL Scheme后加?然后跟网页的url的参数一样写法。 + +5、如何判断是指定App打开,或者某些App不让打开我们的App? +做了实验。 + +A:在需要打开第三方App的工程中将Bundle identifier改为“com.geek.test2”,其余不变 + +B:在被打开的App的AppDelegate.m中 + + +``` +-(BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nonnull id)annotation{ + NSLog(@"calling application bundle id: %@",sourceApplication); + NSLog(@"url shceme:%@",[url scheme]); + NSLog(@"参数:%@",[url query]); + if ([sourceApplication isEqualToString:@"com.geek.test1"]) { + return YES; + } + return NO; +} +``` +![测序](/assets/2287777-5ddf86e7d30b1c05.png) + +###实验结果### + +依旧可以打开App,即使断点走入Return NO + +结论:如果你想阻止其它应用调用你的应用,**创建一个与众不同的 URL scheme**。尽管这不能保证你的应用不会被调用,但至少大大降低了这种可能性。 + + +参考:https://sspai.com/post/31500#01 diff --git a/第一部分 iOS/1.15.md b/第一部分 iOS/1.15.md new file mode 100644 index 0000000..dc31747 --- /dev/null +++ b/第一部分 iOS/1.15.md @@ -0,0 +1,54 @@ + +###URL Schemes 的发展### + + + +URL Schemes 的发展过程可以说就是 iOS 效率工具类 App 的发展过程。 + +起初的苹果建立的 Apple URL Schemes 只是用于自用,里面只有邮件、电话、iTunes 搜索、Youtube 视频等一些内置服务的 URL。 + +个人认为 URL Schemes 第一次大火是在 2011 年末(如有异议欢迎指正),那个时期也是越狱的鼎盛时期,那个时期越狱后大家都会装的一个插件是 SBSettings[1]。越狱的人都知道每当新系统发布的时候,等待新系统的越狱发布是最撩人的,而这段时期那些「不越狱就能做到某种越狱功能」的应用经常一时间风头无两。 + +2011年 iOS 5 发布带来了通知中心,没过多久,出现了一大批使用 iOS 系统设置的 URL Schemes 的 App 神奇地完成了接近 SBSettings 的功能——它们可以让我们从通知中心直接跳转到某些 App 的特定界面,比如 Twitter 的发推界面。它们甚至还可以直接跳转到系统设置里的 Wi-Fi 选项。在这一批 App 中,就有如今效率软件霸主之一 Launch Center Pro 的前身——Launch Center。 + + + +###基本 URL Schemes### + +基本 URL Schemes 的能力虽然简单有限,但使用情境却是最普遍的。 + +我所谓的基本 URL Schemes,是指一个 URL 的 Schemes 部分,比如上文提到的微信的 weixin:。这个部分的唯一功能,就是打开相应应用,而不能够跳转到任何功能。 + +绝大多数所谓支持 URL Schemes 的应用,一般都是只有这么一个部分,它一般是这个应用的名称,比如 OmniFocus 这款应用,它的基本 URL Schemes 就是 Omnifocus:。如果应用的主名称是个中文名的话,它的 URL Schemes 也许会是应用名的拼音,比如 墨客 这款应用,它的基本 URL Schemes 是 moke:。 + +但,我前面提过了网页 URL 和 iOS 应用的 URL 的三个重要区别,其中第三项,就是 iOS 上的 URL Schemes 并不规范,一个应用的 URL 可以是各种各样的: +
    +
  • Coursera 的 URL 是:coursera-mobile:
  • +
  • Duet 这款游戏的 URL 是:x-kumo-duet:
  • +
  • Monument 这款游戏的 URL 是:fb690517270143345:
  • +
  • Feedly 的 URL 是:fb129765800430121:
  • +
  • 扇贝新闻的 URL 是:wx95962d02b9c3e2f7:
  • +
+ +它们目前并没有统一的规则,所以猜测一个应用的意义并不太大,你可以试试,但不要过于指望这种方式。如何查找一个应用的基本 URL Schemes,只要那个应用支持 URL Schemes 就能找到。 + + +步骤 +
    +
  • 首先,在 iTunes 找到你想用 URL 打开的 App,右键选择在文件夹中显示:
  • +
  • 然后解压该文件:
  • +
  • 解压完毕后,在解压出的文件夹中,找到 .app 文件:
  • +
  • 然后选择显示包内容:
  • +
  • 找到 info.plist 这个文件,用你电脑里能打开它的 App 打开它(Xcode没得说)。
  • +
  • 然后查找 URLSchemes:
  • +
  • 在 CFBundleURLSchemes 下的那两行就是该 App 的基本 URL Schemes 了。
  • +
+ +###复杂 URL Schemes### + + +参考链接:[URL Scheme](https://sspai.com/post/31500#fnref:2) + + + + diff --git a/第一部分 iOS/1.16.md b/第一部分 iOS/1.16.md new file mode 100644 index 0000000..939170b --- /dev/null +++ b/第一部分 iOS/1.16.md @@ -0,0 +1,137 @@ +# CocoaPods最新安装步骤 + +## 一、简介 + +* ### 什么是CocoaPods + + * CocoaPods是OS X和iOS下的一个第三方库管理工具,通过CocoaPods我们可以为项目添加所依赖的库。(这种库必须是CocoaPods本身可以找到且支持的),且可以方便管理这些库的版本(更新、回退) +* ### 好处 + + * 在引入第三方库的时候它自动为我们完成各种各样的配置,包括配置编译阶段、连接器选项、甚至是ARC环境下的-fno-objc-arc配置等 + + * 使用CocoaPods可以方便查找第三方库(pod search AFNetworking\),这些库是比较标准的,因为在CocoaPods上可以找得到所以符合CocoaPods的规范比较标准 + +## 二、CocoaPods的安装步骤 + +### 1、升级机器的Ruby环境 + +``` +终端输入:$ gem update --system +``` + +此时有可能出现You do not have write permissions for the /Library/Ruby/Gems/2.0.0 directory + +明显告诉我们没有权限,用过linux的人知道需要加上sudo接下来会让你输入密码。不出意外接下来会提示 + +``` +RubyGems system software updated +``` + +### 2、更换Ruby镜像 + +* 首先移除现有的Ruby镜像 + +``` +终端输入:$ gem sources --remove https://rubygems.org/ +``` + +* 然后添加国内镜像 + +``` +终端输入:$ gem sources -a https://gems.ruby-china.org/ +``` + +* 执行完毕之后输入gem sources -l来查看当前镜像 + +``` +终端输入:$ gem sources -l +``` + +如果结果是 + +`*** CURRENT SOURCES ***https://gems.ruby-china.org/` + +### 3、安装Cocopods + +输入 + +``` +终端输入:$ sudo gem install cocoapods +``` + +在我的MBP上出现了提示说权限不足,接下来再次输入 + +``` +终端输入:$ sudo gem install -n /usr/local/bin cocoapods +``` + +安装成功 + +``` +终端显示 21 gems installed +``` + +继续 + +``` +pod setup +``` + +这个过程比较长 + + + +## 三、Cocopods的使用 + +### 1、查找 + +``` +终端输入:pod search ReactiveObjc +``` + +会显示找到的最新库及其历史版本 +![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/屏幕快照%202017-11-24%20上午10.49.35.png) + + + +### 2、用法 + +* 新建工程 +* 命令行切换到工程目录下 +* 新建一个Podfile + +``` +终端:touch Podfile +``` + +* 编辑Podfile + +``` +终端:vim Podfile +``` + +输入以下内容 + +``` +target 'RAC' do + +pod 'ReactiveObjC', '~> 3.0.0' + +end +``` + +* 终端安装库 + +``` +终端:pod install +``` + +* 完成之后会看到一个在项目目录下多出一个RAC.xcworkspace文件 +* 终端输入 + +``` +终端:open RAC.xcworkspace +``` + + + diff --git a/第一部分 iOS/1.17.md b/第一部分 iOS/1.17.md new file mode 100644 index 0000000..d3ad5a0 --- /dev/null +++ b/第一部分 iOS/1.17.md @@ -0,0 +1,10 @@ +### Swift、OC混编 + +``` +1、在oc文件中使用swift文件。 +选中项目TARGETS->Building Settings->搜索“Objective-C Genereated Interface Header Name”对应的名字。 +在oc文件中需要使用swift的地方,头文件导入上一步对应的名字。 +``` + + + diff --git a/第一部分 iOS/1.18.md b/第一部分 iOS/1.18.md new file mode 100644 index 0000000..e7ba4b3 --- /dev/null +++ b/第一部分 iOS/1.18.md @@ -0,0 +1,8 @@ +* 对于不能调节高度的控件比如 UISlider、UISwitch、UIProgressView 等控件的宽高可以用 \(仿射变化\)transform 属性控制高度。 + +``` +myswitch.transform = CGAffineTransformMakeScale(1,5); +``` + + + diff --git a/第一部分 iOS/1.19.md b/第一部分 iOS/1.19.md new file mode 100644 index 0000000..1f01186 --- /dev/null +++ b/第一部分 iOS/1.19.md @@ -0,0 +1,193 @@ +### 简单的 Model 与 JSON 相互转换 + +``` +// JSON: +{ +"uid":123456, +"name":"Harry", +"created":"1965-07-31T00:00:00+0000" +} + +// Model: +@interface User : NSObject +@property UInt64 uid; +@property NSString *name; +@property NSDate *created; +@end +@implementation User +@end + +// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model: +User *user = [User yy_modelWithJSON:json]; +// 将 Model 转换为 JSON 对象: +NSDictionary *json = [user yy_modelToJSONObject]; + +``` + + +### Model 属性名和 JSON 中的 Key 不相同 + +``` +// JSON: +{ + "n":"Harry Pottery", + "p": 256, + "ext" : { + "desc" : "A book written by J.K.Rowing." + }, + "ID" : 100010 +} + +// Model: +@interface Book : NSObject +@property NSString *name; +@property NSInteger page; +@property NSString *desc; +@property NSString *bookID; +@end +@implementation Book +//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。 ++ (NSDictionary *)modelCustomPropertyMapper { + return @{@"name" : @"n", + @"page" : @"p", + @"desc" : @"ext.desc", + @"bookID" : @[@"id",@"ID",@"book_id"]}; +} +@end +``` + +你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。 + +在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。 + +在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。 + + +### Model 包含其他 Model +``` +// JSON +{ + "author":{ + "name":"J.K.Rowling", + "birthday":"1965-07-31T00:00:00+0000" + }, + "name":"Harry Potter", + "pages":256 +} + +// Model: 什么都不用做,转换会自动完成 +@interface Author : NSObject +@property NSString *name; +@property NSDate *birthday; +@end +@implementation Author +@end + +@interface Book : NSObject +@property NSString *name; +@property NSUInteger pages; +@property Author *author; //Book 包含 Author 属性 +@end +@implementation Book +@end +``` + + +### 容器类属性 +``` +@class Shadow, Border, Attachment; + +@interface Attributes +@property NSString *name; +@property NSArray *shadows; //Array +@property NSSet *borders; //Set +@property NSMutableDictionary *attachments; //Dict +@end + +@implementation Attributes +// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。 ++ (NSDictionary *)modelContainerPropertyGenericClass { + return @{@"shadows" : [Shadow class], + @"borders" : Border.class, + @"attachments" : @"Attachment" }; +} +@end + +``` + +### 黑名单与白名单 +``` +@interface User +@property NSString *name; +@property NSUInteger age; +@end + +@implementation Attributes +// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性 ++ (NSArray *)modelPropertyBlacklist { + return @[@"test1", @"test2"]; +} +// 如果实现了该方法,则处理过程中不会处理该列表外的属性。 ++ (NSArray *)modelPropertyWhitelist { + return @[@"name"]; +} +@end +``` + + +### 数据校验与自定义转换 + +``` +// JSON: +{ + "name":"Harry", + "timestamp" : 1445534567 +} + +// Model: +@interface User +@property NSString *name; +@property NSDate *createdAt; +@end + +@implementation User +// 当 JSON 转为 Model 完成后,该方法会被调用。 +// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。 +// 你也可以在这里做一些自动转换不能完成的工作。 +- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic { + NSNumber *timestamp = dic[@"timestamp"]; + if (![timestamp isKindOfClass:[NSNumber class]]) return NO; + _createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue]; + return YES; +} + +// 当 Model 转为 JSON 完成后,该方法会被调用。 +// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。 +// 你也可以在这里做一些自动转换不能完成的工作。 +- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic { + if (!_createdAt) return NO; + dic[@"timestamp"] = @(n.timeIntervalSince1970); + return YES; +} +@end +``` + +### Coding/Copying/hash/equal/description + +``` +@interface YYShadow :NSObject +@property (nonatomic, copy) NSString *name; +@property (nonatomic, assign) CGSize size; +@end + +@implementation YYShadow +// 直接添加以下代码即可自动完成 +- (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; } +- (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; } +- (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; } +- (NSUInteger)hash { return [self yy_modelHash]; } +- (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; } +- (NSString *)description { return [self yy_modelDescription]; } +@end + +``` \ No newline at end of file diff --git a/第一部分 iOS/1.2.md b/第一部分 iOS/1.2.md new file mode 100644 index 0000000..607cbc6 --- /dev/null +++ b/第一部分 iOS/1.2.md @@ -0,0 +1,183 @@ + +# 构造方法 + +* new方法的内部就是先调用alloc方法,再调用init方法 + + * alloc方法:那个类接受alloc消息,那么该方法返回该接受类的对象,并把对象返回 + + * init方法:是1个对象方法,作用:初始化对象 + +* 创建对象的步骤:先使用alloc创建1个对象,再使用init初始化这个对象,才可以使用这个对象 + + * 使用1个未被初始化的对象是很危险的 + +* init方法:作用:初始化对象,为对象赋初始值,叫做构造方法 + +# 重写init构造方法 + +* 如果想创建出来的对象的属性值不是默认的初始化值,则需要重写init方法 + +* 重写init方法的规范: + + * 必须要先调用父类的init方法(因为当前类初始化就是通过\`\[父类init\]\`去初始化),然后将返回值赋值给self + + * 调用init方法有可能会失败,如果失败直接返回nil + + * 判断父类是否初始化成功。如果self != nil,则代表初始化成功 + + * 如果初始化成功就去初始化当前对象的属性 + + * 最后返回self + +#### 解惑: + +1. 为什么要调用父类的init方法? + 1. 当前类有isa指针,当前类的isa指针赋值是通过父类的init方法赋值的。 + 2. 需要保证当前对象的父类属性同时被初始化 +2. 重写init方法的规范: + +``` +-(instancetype)init{ + if (self = [super init]) { + //todo:自定义属性的初始化 + } + return self; +} +``` + +``` +//Person + +#import + +@interface Person : NSObject +@property NSString* name; +@property int age; + +-(void)sayHi; + +@end + +#import "Person.h" +@implementation Person + +-(void)sayHi{ + NSLog(@"Hi"); +} + +-(instancetype)init{ + self = [super init]; + if (self) { + self.name = @"杭城小刘"; + self.age = 22; + } + return self; +} +@end + + +//测试 + + Person *p1 = [[Person alloc] init]; //p1.name = "杭城小刘",p1.age =22; + Person *p2 = [Person new]; //p2.name = "杭城小刘",p2.age =22; +``` + +如果2个类的关系为组合关系,且它的一个属性是另一个类的对象,那么当该类初始化的时候默认它的属性为nil,那么如何初始化? + +``` +-(instancetype)init{ + self = [super init]; + if (self) { + self.name = @"lbp"; + self.age = 22; + self.pig = [[Pig alloc] init]; + } + return self; +} + +//测试 + Person *p1 = [[Person alloc] init]; //p1.dog != nil +``` + +# 自定义构造方法 + +* 现状:虽然每次双肩的对象的属性值不是默认的,但是每次初始化的对象的值都是一样的。 + +* 需求:每次实例化的对象的属性值由调用者决定 + +* 解决办法:自定义构造方法 + +* 自定义构造方法规范: + + * 自定义构造方法的返回值为instancetype + + * 方法的命名必须以initWith开头 + + * 方法的实现类似init的实现 + +**注意:此时不能使用new来调用。(因为new的实现是先alloc再init,默认init的实现是给属性赋默认值)** + +``` +-(instancetype)initWithName:(NSString *)name andAge:(int)age{ + if (self = [super init]) { + self.name = name; + self.age = age; + } + return self; +} +``` + +``` +//Person +#import +@interface Person : NSObject +@property NSString* name; +@property int age; + +-(instancetype)initWithName:(NSString *)name andAge:(int)age; +@end + +#import "Person.h" +@implementation Person + +-(instancetype)init{ + self = [super init]; + if (self) { + self.name = @"lbp"; + self.age = 22; + } + return self; +} + +//不能在构造方法之外给self赋值 +//编译器认为只有以initWith开头的方法是构造方法 + +-(instancetype)initWithName:(NSString *)name andAge:(int)age{ + if (self = [super init]) { + self.name = name; + self.age = age; + } + return self; +} + +@end + + +//测试 +Person *p1 = [[Person alloc] init]; +Person *p2 = [Person new]; +Person *p3 = [[Person alloc] initWithName:@"杭城小刘2号" andAge:23]; +``` + + +![init](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-23-5-56-53.png) +![init](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-23-5-57-08.png) + + +关于“自定义构造方法必须以initWith开头”做个实验 + +![initwith](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-23-6-01-29.png) + +报错信息很明显:不能在构造方法之外给self赋值 + +因为,编译器认为只有以initWith开头的方法是构造方法 diff --git a/第一部分 iOS/1.20.md b/第一部分 iOS/1.20.md new file mode 100644 index 0000000..9ba27ce --- /dev/null +++ b/第一部分 iOS/1.20.md @@ -0,0 +1,4 @@ +1、豆瓣模块化解决方案:https://github.com/lincode/FRDModuleManager(主要解决Appdelegate臃肿) + + + diff --git a/第一部分 iOS/1.21.md b/第一部分 iOS/1.21.md new file mode 100644 index 0000000..f9d8526 --- /dev/null +++ b/第一部分 iOS/1.21.md @@ -0,0 +1,56 @@ +## 实现原理 {#实现原理} + +波浪的形状绘制在 CAShapeLayer 上。通过 CADisplayLink 与屏幕刷新频率同步,每次刷新都绘制新的波浪,并改变小船的位置和角度。另外,水和天空的颜色是渐变的,由 CAGradientLayer 实现,其中,显示水的 CAGradientLayer 需要有波浪形状的 CAShapeLayer 的遮罩\(mask\)。 + +### CAShapeLayer {#cashapelayer} + +CAShapeLayer 的属性 path \(CGPath\)就是图层要显示的形状。把波浪的形状绘制出来,赋值给此属性即可。 + +### CADisplayLink {#cadisplaylink} + +创建 CADisplayLink,相应的 target 实现屏幕刷新时要调用的方法。把 CADisplayLink 加入 RunLoop 中。通过 isPaused 属性控制 CADisplayLink 是否暂停\(target 是否调用方法\) + +``` +private var waveLink: CADisplayLink?waveLink = CADisplayLink(target: self, selector: #selector(waveLinkRefresh)) +waveLink?.isPaused = true +waveLink?.add(to: .current, forMode: .defaultRunLoopMode) +``` + + + +### 绘制波浪 {#绘制波浪} + +波浪的形状关键是正弦函数曲线 + +``` +y = A*sin(x+B) +``` + +参数 A 决定了波浪的高度;参数 B 决定了波浪在 x 轴的位置。 + +用一个属性 currentPhase 表示参数 B。每次屏幕刷新的时候用 currentPhase 绘制,然后更新此属性,加上一个固定的数。这样波浪就会朝左或右匀速移动。 + +为了使波浪高度逐渐变化,用一个属性表示参数 A,然后每次绘制后更新此属性,加上一个固定的数,直到波浪高度达到目标值。 + +### 小船的位置和旋转角度 {#小船的位置和旋转角度} + +已知小船 x 轴坐标,通过正弦函数可以直接计算出小船的 y 轴坐标。此外,小船需要随着波浪旋转,旋转至船底与波浪表面相切。这就要对正弦函数进行求导 + +``` +y' = A * cos(x + B) +``` + +用以上式子计算出小船所在位置的 y',表示正弦函数在此处的切线斜率,几何意义是切线与 x 轴的夹角的正切值。反正切即可求出切线与 x 轴的夹角,也就是小船需要旋转的角度 + +``` +angle = atan(y') +``` + +用以上旋转角度,改变小船视图\(UIView\)的 transform,调用 CGAffineTransformRotate 方法,实现小船的旋转。 + +### CAGradientLayer {#cagradientlayer} + +CAGradientLayer 默认的颜色渐变方向是由上至下。给 colors 属性赋值一个包含 CGColor 的数组,则图层颜色由上至下,从数组第一个值经中间值渐变至最后一个值。 + +显示水的 CAGradientLayer 需要呈现波浪形状,需要 CAShapeLayer 的遮罩。把绘制好波浪形状的 CAShapeLayer 赋值给 CAGradientLayer 的 mask 属性即可。 + diff --git a/第一部分 iOS/1.22.md b/第一部分 iOS/1.22.md new file mode 100644 index 0000000..2a644e1 --- /dev/null +++ b/第一部分 iOS/1.22.md @@ -0,0 +1,88 @@ +# Block探究 + + +### 1、Block作为函数参数可以应用到函数式编程 +``` +self.prepare.play(@"女人"); +- (ViewController *(^)(NSString *))play{ + NSLog(@"即将吃喝玩乐"); + ViewController *(^block)(NSString *) = ^ViewController *(NSString *fun){ + NSLog(@"接下来玩%@,好不好?",fun); + return self; + }; + return block; +} + +- (ViewController *)prepare{ + NSLog(@"我们先好好休息一下。😂\n"); + return self; +} + + + +``` + + +###2、Block作为函数的返回值可以作为链式编程 + + +``` +[self blockAsFunctionalProgramming]; + +- (void)blockAsFunctionalProgramming{ + [self reprepare:^{ + NSLog(@"接下来玩女人,好不好?😊"); + }]; +} + +- (void)reprepare:(void(^)(void))replay{ + NSLog(@"我们先好好休息一下。😂\n"); + replay(); +} +@end +``` + +###3、Block 访问、修改外部变量 + +* 打开 Terminal.app,编写一段c代码 + +``` +#include "stdio.h" + +int main(){ + + printf("Coming\n"); + __block int a = 10; + + printf("开始->%p %d\n",&a,a); + + void(^block)(int a) = ^void(int a){ + a += 10; + printf("中间->%p %d\n",&a,a); + }; + block(a); + printf("结束->%p %d\n",&a,a); + + return 0; +} + +``` + +* 之后用 **gcc** 编译一下。在同层目录下得到一个 **a.out** 的可执行文件。 + +``` +gcc index.c + +``` + +* 之后用 **clang** 编译成 C++ 文件,可以看到系统底层是如何处理 block 外部的变量、以及如何在 block 里面处理变量的。 + +``` +clang -rewrite-objc index.c +``` +![clang结果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180516-235614@2x.png) + + +###4、Block 经常造成循环引用 +* 如果 block 作为函数参数的话,且这个函数是在对象的层级,那么可能会造成循环应用。 self -> func -> block -> self. +此时需要在 block 里面访问 self 的时候将 self 修饰为 __weak \ No newline at end of file diff --git a/第一部分 iOS/1.23.md b/第一部分 iOS/1.23.md new file mode 100644 index 0000000..6c7f81a --- /dev/null +++ b/第一部分 iOS/1.23.md @@ -0,0 +1,80 @@ +#禅与 Objective-C 编程艺术 + +#### 警告和错误 + +* 警告 +``` +#warning Dude, don't compare floating point numbers like this! +``` + +* 错误 +``` +#warning Dude, don't compare floating point numbers like this! +``` + +* 让编译器忽略忽略你这段代码的警告 + + 大多数 iOS 开发者平时并没有和很多编译器选项打交道。一些选项是对控制严格检查(或者不检查)你的代码或者错误的。有时候,你想要用 pragma 直接产生一个异常,临时打断编译器的行为。 + + 当你使用ARC的时候,编译器帮你插入了内存管理相关的调用。但是这样可能产生一些烦人的事情。比如你使用 NSSelectorFromString 来动态地产生一个 selector 调用的时候,ARC不知道这个方法是哪个并且不知道应该用那种内存管理方法,你会被提示 performSelector may cause a leak because its selector is unknown(执行 selector 可能导致泄漏,因为这个 selector 是未知的). + + 如果你知道你的代码不会导致内存泄露,你可以通过加入这些代码忽略这些警告 + +``` +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + +[myObj performSelector:mySelector withObject:name]; + +#pragma clang diagnostic pop +``` + + +* 忽略没用使用变量的编译警告 + +``` +#pragma used(foo) +``` + +``` +- (NSInteger)giveMeFive +{ + NSString *foo; + #pragma unused (foo) + return 5; +} +``` + + +* 善用代码块 + +一个 GCC 非常模糊的特性、以及 Clang 也有的特性:代码块如果在闭合的括号内,会返回最后语句的值。 + +``` +self. = ({ + NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint]; + [NSURL URLWithString:urlString]; +}); +``` + +这个特性非常适合组织小块的代码,给代码阅读者一个重要的入口且减小相关干扰,能让读者聚焦于关键的变量和函数中,此外这个方法有个优点:变量在代码块的区域内有效,可以减小对其他作用域的命名污染。 + + +* 方法参数断言 + +你的方法可能需要一些参数来满足特定的条件(比如不能为 nil),在这种情况下最好使用 **NSParameterAssert()**  来断言条件是否成立 + +括号内的条件为 false 的时候则断言抛出异常 + +``` +NSParameterAssert(message.length > 0); +``` + +``` +[self testAssert:nil]; //*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: message.length > 0' +- (void)testAssert:(NSString *)message{ + NSParameterAssert(!(message.length < 1)); + NSLog(@"%@",message); +} + +``` diff --git a/第一部分 iOS/1.24.md b/第一部分 iOS/1.24.md new file mode 100644 index 0000000..67fdfb7 --- /dev/null +++ b/第一部分 iOS/1.24.md @@ -0,0 +1,35 @@ +# 修改 UITextField 的 placeholder样式 + +> 对于 UITextField 的 placeholder 私有属性来说 Apple 不允许我们直接修改,但是按照经验我们有2种方式可以实现自定义 placeholder 的样式 + + +### 1、利用 KVC 对 UITextField 的私有属性修改 + +``` + + [self.invitecodeTextfield setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"]; + [self.invitecodeTextfield setValue:[UIFont systemFontOfSize:35] forKeyPath:@"_placeholderLabel.font"]; + +``` + + +### 2、利用 Apple 提供的 API 进行修改 + +UITextField 有个属性 attributedPlaceholder,利用它我们可以修改 placeholder 的样式 + + +``` + +self.invitecodeTextfield.attributedPlaceholder = [LBPHightedAttributedString setAllText:@"我要Testing" andSpcifiStr:@"Testing" withColor:[UIColor redColor] specifiStrFont:[UIFont systemFontOfSize:17]]; + +``` + + +其中 **LBPHightedAttributedString** 是我封装的一个关于 NSMutableAttributedString 的工具,可以对一个指定的字符串内部的字符串进行全局查找并高亮设置的小工具,具体可以查看地址 + + +[LBPHightedAttributedString](https://github.com/FantasticLBP/BlogDemos/tree/master/LBPAttributedStringTools/LBPHightedAttributedString "LBPHightedAttributedString") + + + + diff --git a/第一部分 iOS/1.25.md b/第一部分 iOS/1.25.md new file mode 100644 index 0000000..d963128 --- /dev/null +++ b/第一部分 iOS/1.25.md @@ -0,0 +1,27 @@ +## UIScrollView 拖拽滑动时收起键盘 + + +> 当一个页面的 UIScrollView/UITableView 上有输入框时,为了较好的体验,就是当滑动的时候需要回收键盘 + +* 最开始的做法是设置 UIScrollView 的代理位当前控制器,监听 scrollViewWillBeginDragging 方法,找到 keyWindow 并且 endEditing + + +``` +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ + [[UIApplication sharedApplication].keyWindow endEditing:YES]; +} +``` + +* 之后偶然有幸看到一个 UIScrollView 的属性"keyboardDismissModel"。实现上述需求只需要一行代码 + +``` +self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; +``` + +* keyboardDismissMode 有3个枚举值 + + * UIScrollViewKeyboardDismissModeNone:默认值,也就是拖拽时对于键盘没有任何影响。 + + * UIScrollViewKeyboardDismissModeOnDrag:(dismisses the keyboard when a drag begins)当刚拖拽的时候就会回收键盘 + + * UIScrollViewKeyboardDismissModeInteractive:(the keyboard follows the dragging touch off screen, and may be pulled upward again to cancel the dismiss)当向下滑动的时候键盘会跟随手势一起下滑,当向上滑动的时候键盘也会跟随手势向上滑动而出现。 \ No newline at end of file diff --git a/第一部分 iOS/1.26.md b/第一部分 iOS/1.26.md new file mode 100644 index 0000000..a7d6250 --- /dev/null +++ b/第一部分 iOS/1.26.md @@ -0,0 +1,86 @@ +# NSRange 设计之美 + + + +> typedef struct _NSRange { + NSUInteger location; + NSUInteger length; +} NSRange; + +1、看到官方文档的源代码就知道 NSRange 是个结构体,但是如果是你设计一个这样的数据类型你会怎么办?? + +设计成结构体,然后有些属性怎么办?比如为了开发者方便,让你设计出一个办法,让开发者可以很快知道这个结构体的上限是什么? + +苹果就很机智,设计了一个内联函数 + + ``` + NS_INLINE NSUInteger NSMaxRange(NSRange range) { + return (range.location + range.length); +} + ``` + + 2、什么是内联函数? + + ``` +NS_INLINE 返回值类型 函数名(参数列表) { + //函数实现 + //return ; +} + ``` + +3、内联函数的应用 + +比如自定义一个弹窗 + +``` +NS_INLINE void tipWithMessage(NSString *message){ + + dispatch_async(dispatch_get_main_queue(), ^{ + + UIAlertView *alerView = [[UIAlertView alloc] initWithTitle:@"提示" message:message delegate:nil cancelButtonTitle:nil otherButtonTitles:nil, nil]; + + [alerView show]; + + [alerView performSelector:@selector(dismissWithClickedButtonIndex:animated:) withObject:@[@0, @1] afterDelay:0.9]; + + }); + +} + +``` + + +4、内联函数的注意事项 + +内联函数是以代码膨胀为代价, 仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数内代码的时间相比于函数调用的开销大,那么效率收获会很小。 +此外每一处调用内联函数的地方都会复制一遍代码,所以会使得程序的体积变大,消耗更多的代码段区域。 +所以下面的情况不适合使用内联函数: + + * 如果函数体内的代码比较长,使用内联将导致内存消耗比较大 + * 如果函数体内出现循环,那么执行函数体内代码的时间要比函数的调用开销大 + + +5、FOUNDATION_EXPORT + +查看 NSRange 的代码还会发现一个关键词 **FOUNDATION_EXPORT**,它可以用作定义常量。 + + +FOUNDATION_EXPORT 和 #define 都可用来定义常量。 +用法 + +``` +//.h +FOUNDATION_EXPORT NSString *const NickName; + +//.m +NSString *const NickName = @"杭城小刘"; +``` + +那么它和 **#define**  有何区别? + +FOUNDATION_EXPORT 在检测字符串的值是否相等的时候效率更高 +使用** NickName == MyName** 来判断,而 #define 是用 **[NickName isEqualToString:MyName]** 来判断。 + + + * 本质上 FOUNDATION_EXPORT 是比较指针的自己 + * \#define 是比较每个字符串是否相等 \ No newline at end of file diff --git a/第一部分 iOS/1.27.md b/第一部分 iOS/1.27.md new file mode 100644 index 0000000..b080254 --- /dev/null +++ b/第一部分 iOS/1.27.md @@ -0,0 +1,210 @@ +# 复制层(CAReplicatorLayer) + +> 对于下面的效果大家是否有实现思路? +> +> 有些人可能要说:老夫撸起袖子,敲键盘就是干,不需要手势交互,那么直接用5个**CALayer**,处理不同的位置以及定时器、透明度等等,貌似很简单。 +> +> 不不不,今天要带出来的主题是 **CAReplicatorLayer** + +![音量柱动画效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmW9ACfS9P5orau43H7gxuxsU4RVMDPD7mPnDKq4pgLmzr.gif) + + + + + + +## 1、CAReplicatorLayer + +> /* The replicator layer creates a specified number of copies of its +> +> - sublayers, each copy potentially having geometric, temporal and +> - color transformations applied to it. +> +> * +> +> - Note: the CALayer -hitTest: method currently only tests the first +> - instance of z replicator layer's sublayers. This may change in the +> - future. */ + +官方给出的意思就不翻译了,使用场景大致是一个形状、特性差不多的 layer,我们不需要重复创建,可以利用它来实现复制多个 layer ,然后通过 CAReplicatorLayer 的一些属性实现我们的需求。 + + + +上述效果的代码 + +```objective-c +//创建复制层,因为我们做的多个音量柱变化的动画都是一样的,所以创建了一个复制层,这个复制层可以对里面的 sublayer 进行复制,所以我们不需要重复创建了 + + CAReplicatorLayer *replicatorrLayer = [CAReplicatorLayer layer]; + replicatorrLayer.frame = CGRectMake(0, 0, self.contentView.frame.size.width, self.contentView.frame.size.height); + replicatorrLayer.backgroundColor = [UIColor blackColor].CGColor; + self.replicatorrLayer = replicatorrLayer; + [self.contentView.layer addSublayer:replicatorrLayer]; + + + //创建音量震动条 + CALayer *layer = [CALayer layer]; + layer.backgroundColor = [UIColor whiteColor].CGColor; + CGFloat width = 30; + CGFloat height = 100; + layer.bounds = CGRectMake(0, self.contentView.frame.size.height - height, width, height); + layer.anchorPoint = CGPointMake(0, 1); + layer.position = CGPointMake(0, self.contentView.frame.size.height); + [self.contentView.layer addSublayer:layer]; + + //创建音量震动动画 + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale.y"]; + animation.toValue = @0; + animation.duration = 1; + animation.repeatCount = MAXFLOAT; + animation.autoreverses = YES; + [layer addAnimation:animation forKey:nil]; + + + [replicatorrLayer addSublayer:layer]; + + //* The number of copies to create, including the source object. + replicatorrLayer.instanceCount = 6; //复制 sublayer 的个数,包括创建的第一个sublayer 在内的个数 + replicatorrLayer.instanceDelay = 0.4; //设置动画延迟执行的时间 + replicatorrLayer.instanceAlphaOffset = -0.15; //设置透明度递减 + replicatorrLayer.instanceTransform = CATransform3DMakeTranslation(50, 0, 0); +``` +[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用1-音量柱动画) + + + + +## 例子1 + +![倒影效果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmQrU8UxSytnKbWcDVpY5mdy6kmiSHpzyqwt8GykWKNEY2.png) + +这里比较简单了,关键代码 + +```objective-c + CAReplicatorLayer *replicatorLayer = (CAReplicatorLayer *)self.view.layer; + replicatorLayer.instanceCount = 2; + replicatorLayer.instanceTransform = CATransform3DMakeRotation(M_PI, 1, 0, 0); + replicatorLayer.instanceRedOffset -= 0.1; + replicatorLayer.instanceGreenOffset -= 0.1; + replicatorLayer.instanceBlueOffset -= 0.1; + replicatorLayer.instanceAlphaOffset -= 0.3; +``` + +- 需要说明是这里我用 storyboard 处理的,因为已经拉好了控件,所以我们没办法将图片直接加到复制层上去。间接做法是将 UIViewController 的 view 的 layer 类型改变为 复制层 + + ``` + //该方法返回 UIView 的层 + //改写 UIView 的层:重写 layerClass 方法 + + (Class)layerClass{ + return [CAReplicatorLayer class]; + } + ``` + [源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用2-倒影效果) + + +## 例子2 + +![复制层动画综合应用](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/QQ20180610-235637-HD.gif) + + +需求分析: + +- 先画图。也就是添加一个滑动手势并监听它。然后强制绘图(self setNeedsDisplay) + +- 添加一个 layer 到 self.layer 上 + +- 改变当前 view 的 layer 类型。 + + ``` + + (Class)layerClass{ + return [CAReplicatorLayer class]; + } + ``` + +- 设置 CAReplicatorLayer 的 instanceCount 和 instanceDelay 属性 + +- 添加了小点,并为小点设置关键帧动画。 + +- 重置功能实现靠的是清除 path 上面的 points ,并移除 小点上面的动画 + +``` +#import "ViewControllerView.h" + +@interface ViewControllerView() + +@property (nonatomic, strong) UIBezierPath *path; +@property (nonatomic, weak) CALayer *dotLayer; +@end + +@implementation ViewControllerView + ++ (Class)layerClass{ + return [CAReplicatorLayer class]; +} + +- (void)awakeFromNib{ + [super awakeFromNib]; + + UIPanGestureRecognizer *tapGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(draw:)]; + [self addGestureRecognizer:tapGesture]; + self.path = [UIBezierPath bezierPath]; + + CALayer *layer = [CALayer layer]; + layer.frame = CGRectMake(-UIScreen.mainScreen.bounds.size.width, 0, 15, 15); + layer.backgroundColor = [UIColor orangeColor].CGColor; + layer.cornerRadius = 7.5; + self.dotLayer = layer; + [self.layer addSublayer:layer]; + + CAReplicatorLayer *replicatorLayer = (CAReplicatorLayer *)self.layer; + replicatorLayer.instanceCount = 20; + replicatorLayer.instanceDelay = 0.25; +} + + +- (void)draw:(UIPanGestureRecognizer *)tap{ + CGPoint currentPoint = [tap locationInView:self]; + if (tap.state == UIGestureRecognizerStateBegan) { + [self.path moveToPoint:currentPoint]; + } + else if(tap.state == UIGestureRecognizerStateChanged){ + [self.path addLineToPoint:currentPoint]; + [self setNeedsDisplay]; + } +} + +- (void)startAnimation{ + //要实现动画围绕着给定的形状执行,那么需要关键帧动画(类比于Flash概念中的关键帧动画,只需要给定指定的关键帧,其余的帧系统会创建出来。)。关键帧动画的 path 和 values 是互斥的,也就是说如果设置了 values 还设置了 path 那么 path 属性会覆盖 values 属性。 + + CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; + animation.keyPath = @"position"; + animation.path = self.path.CGPath; + animation.duration = 5; + animation.repeatCount = MAXFLOAT; + [self.dotLayer addAnimation:animation forKey:nil]; +} + +- (void)redraw{ + //清空路径:移除 path 上面所有的点,然后重绘 + [self.path removeAllPoints]; + [self setNeedsDisplay]; + //移除动画 + [self.dotLayer removeAllAnimations]; +} + +- (void)drawRect:(CGRect)rect{ + [self.path stroke]; +} + +@end +``` +[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/复制层应用3-粒子闪烁效果) + +### CALayer 层的动画有2个概念非常重要:AnchorPoint 和 position + +- postion 用来确定 layer 层在父层中的位置 + +- anchorPoint 用来确定 layer 身上哪个点会在 position 所指的位置。 + + + diff --git a/第一部分 iOS/1.28.md b/第一部分 iOS/1.28.md new file mode 100644 index 0000000..abebcc3 --- /dev/null +++ b/第一部分 iOS/1.28.md @@ -0,0 +1,172 @@ +# CAShapeLayer + +> 一言以蔽之:CAShapeLayer 可以根据贝塞尔曲线描绘出的路径而生成对应的图形 + + + +## 综合例子 + +- 效果图 + +![QQ粘性动画](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmUhGFJgxj6ofpvZp6MK3bqaH2hLgq9vfKsnwDmMisahGu.gif) + + +- 关键技术点剖析 + + - 分析 QQ 粘性动画的关键点就是当手势拖动时候2个圆之间那个形状怎么绘制 + + 答案:将2个圆的某一时刻之间形成的形状用数学抽象来计算。 +![轨迹分解](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QmQUyUSLYB3VGs4juzfsEdncyWetz7BTN2GFtURbmEYbEY.png) + + + - 拖动到超过某个范围的时候怎么执行爆炸动画 + + UIImageView 可以执行帧动画,类似于 Flash 效果 + + + + + + + + + + ### 关键代码 + + ``` + - (void)pan:(UIPanGestureRecognizer *)pan{ + //当前移动的偏移量 + CGPoint transP = [pan translationInView:self]; + //改变红点的位置 + //transform并没有修改自身的 center(center 是 layer 的position),只是修改了 frame + NSLog(@"偏移量:%@",NSStringFromCGPoint(transP)); + CGPoint center = self.center; + center.x += transP.x; + center.y += transP.y; + self.center = center; + + //self.transform = CGAffineTransformTranslate(self.transform, transP.x, transP.y); + //手势复位:设置坐标原点位上次的坐标 + [pan setTranslation:CGPointZero inView:self]; + + CGFloat distance = [self distanceWith:self.smallCircle bigCircle:self]; + NSLog(@"%f",distance); + + + CGFloat smallCircleRadius = self.bounds.size.width * 0.5; + smallCircleRadius = smallCircleRadius - distance/10; + + if (smallCircleRadius < 3) { + smallCircleRadius = 3; + } + self.smallCircle.bounds = CGRectMake(0, 0, smallCircleRadius*2, smallCircleRadius*2); + self.smallCircle.layer.cornerRadius = smallCircleRadius; + + if (self.smallCircle.hidden == NO) { + //返回一个不规则的路径 + UIBezierPath *path = [self drawTracertWithSmallCircle:self.smallCircle bigCircle:self]; + //将形状转换为一个形状图层 + self.shapeLayer.path = path.CGPath;//根据路径生成形状 + } + //创建形状图层 + [self.superview.layer insertSublayer:self.shapeLayer atIndex:0]; + + if (distance > 60) { + self.smallCircle.hidden = YES; + [self.shapeLayer removeFromSuperlayer]; + } + + if (pan.state == UIGestureRecognizerStateEnded) { + //结束手势 + if (distance < 60) { + [self.shapeLayer removeFromSuperlayer]; + self.center = self.smallCircle.center; + self.smallCircle.hidden = NO; + } + else{ + //手势拖拽超过60则播放一个动画 + UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds]; + + NSMutableArray *images = [NSMutableArray array]; + + for (int i=0; i<8; i++) { + NSString *imageName = [NSString stringWithFormat:@"%d",i+1]; + UIImage *image = [UIImage imageNamed:imageName]; + [images addObject:image]; + } + imageView.animationImages = images; + [imageView setAnimationDuration:1]; + [imageView startAnimating]; + [self addSubview:imageView]; + //动画结束移除本身 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self removeFromSuperview]; + }); + } + } + + } + + - (CGFloat )distanceWith:(UIView *)smallCircle bigCircle:(UIView *)bigScirle{ + CGFloat offsetX = bigScirle.frame.origin.x - smallCircle.frame.origin.x; + CGFloat offsetY = bigScirle.frame.origin.y - smallCircle.frame.origin.y; + return sqrt( pow(offsetX, 2) + pow(offsetY, 2)); + } + + //将2个圆运行的变化轨迹用代码模拟 + - (UIBezierPath *)drawTracertWithSmallCircle:(UIView *)smallCircle bigCircle:(UIView *)bigCircle{ + + CGFloat X1 = smallCircle.center.x; + CGFloat X2 = bigCircle.center.x; + CGFloat Y1 = smallCircle.center.y; + CGFloat Y2 = bigCircle.center.y; + + CGFloat r1 = smallCircle.bounds.size.width/2; + CGFloat r2 = bigCircle.bounds.size.width/2; + + CGFloat d = [self distanceWith:smallCircle bigCircle:bigCircle]; + //Ø 代表角度 + CGFloat SinØ = (X2 - X1)/d; + CGFloat CosØ = (Y2 - Y1)/d; + + CGPoint pointA = CGPointMake(X1 - r1*CosØ, Y1 + r1*SinØ); + + CGPoint pointB = CGPointMake(X1 + r1*CosØ, Y1 - r1*SinØ); + + CGPoint pointC = CGPointMake(X2 + r2*CosØ, Y2 - r2*SinØ); + + CGPoint pointD = CGPointMake(X2 - r2*CosØ, Y2 + r2*SinØ); + + CGPoint pointO = CGPointMake(X1 + SinØ *d/2, Y1 + CosØ*d/2); + + CGPoint pointP = CGPointMake(X1 + SinØ *d/2,Y1 + CosØ*d/2 ); + + //描述路径 + UIBezierPath *path = [UIBezierPath bezierPath]; + + //AB + [path moveToPoint:pointA]; + [path addLineToPoint:pointB]; + + //BC(曲线) + [path addQuadCurveToPoint:pointC controlPoint:pointP]; + + //CD + [path addLineToPoint:pointD]; + + //DA(曲线) + [path addQuadCurveToPoint:pointA controlPoint:pointO]; + + return path; + } + ``` + + + +完整的代码,[Github地址](https://github.com/FantasticLBP/BlogDemos/tree/master/QQ粘性动画) + + + + + + \ No newline at end of file diff --git a/第一部分 iOS/1.29.md b/第一部分 iOS/1.29.md new file mode 100644 index 0000000..2474f26 --- /dev/null +++ b/第一部分 iOS/1.29.md @@ -0,0 +1,65 @@ +# 仿微博弹簧动画 + +> 老玩微博,最近在研究动画,周末抽空写了个发微博的动画 + + + +# 实现步骤 + +- 首先模打出一个控制器 +- 这个控制器用来显示多个按钮。(按钮是图文上下排列的,所以我们需要自定义按钮的布局样式) +- 动画思路:先在界面添加好几个 UIButton,之后给每个 button 添加**y**方向的平移动画 -> 设置一个定时器,每次执行的时候依次取出按钮,将按钮添加一个弹簧动画(**usingSpringWithDamping **)将形变动画恢复原位 +- 给按钮添加2种事件(按下的事件、点击后抬起的事件) + +## 关键代码 + +``` +//开始时让所有按钮都移动到最底部 +btn.transform = CGAffineTransformMakeTranslation(0, self.view.bounds.size.height); + +//添加定时器 +self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(update) userInfo:nil repeats:YES]; + +- (void)update{ + if (self.btnIndex == self.btnArray.count) { + [self.timer invalidate]; + return ; + } + + VerticalStyleButton *button = self.btnArray[self.btnIndex]; + //弹簧动画 + [UIView animateWithDuration:0.3 delay:0.2 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{ + button.transform = CGAffineTransformIdentity; + } completion:^(BOOL finished) { + + }]; + self.btnIndex++; +} + +- (void)btnClick:(UIButton *)button{ + [UIView animateWithDuration:0.25 animations:^{ + button.transform = CGAffineTransformMakeScale(1.2, 1.2); + }]; +} + +- (void)btnClick1:(UIButton *)button{ + [UIView animateWithDuration:0.25 animations:^{ + button.alpha = 0; + button.transform = CGAffineTransformMakeScale(2, 2); + }]; +} +``` + + + +# 效果图 + + +![发微博动画效果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180610-225937-HD.gif) + + + + + +[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/微博发帖动画) + diff --git a/第一部分 iOS/1.3.md b/第一部分 iOS/1.3.md new file mode 100644 index 0000000..2d67921 --- /dev/null +++ b/第一部分 iOS/1.3.md @@ -0,0 +1,99 @@ + + +# loadView + +1. 作用:加载控制器的view + +2. 何时调用:当控制器的view第一次使用的时候就会调用 + +3. 使用场景:只要想自定义控制器的view就调用此方法 + +访问控制器的View就相当于调用控制器中的view get方法 + +``` + +-(UIView *)view{ + if(_view == nil){ + [self loadView]; + [self viewDidload]; + + } + return _view; +} + +``` + +# 控制器加载view的流程 +![控制器加载view的流程](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2287777-b6128646373dfffb.png) + + +* 控制器的init方法底层会调用initWithNibName方法 + +MyViewController *vc = [[MyViewController alloc] init]; + +注意点: + +* 系统做判断的前提提条件:没有指定nibName;没有自定义loadView方法;控制器以...Controller命名 + +* 判断原则: + +* 1、判断下有没有指定nibName,如果指定了就去加载nib + +* 2、判断有没有跟控制器同名的xib,但是xib的名称不带Controller的xib,如果有就去加载 + +* 3、如果第二步没有指定,就判断有没有跟控制器类名同名的xib,如果有就去加载 + +* 4、如果没有任何xib描述控制器的view,就不加载xib + +## MyViewController加载view的处理 + +* 判断有没有指定xibName,如果有就去加载指定的xib + +* 判断有没有跟控制器类名同名的xib,但是名字不带controller + +* 判断有没有跟控制器类名同名的xib,有就去加载 + +* 直接创建一个空的xib + +例子 + +``` +//在Appdelegate中 +ViewController *vc = [[ViewController alloc] init]; +vc.view.backgroundColkor = [UIColor redColor]; +self.window.rootViewController = vc; +[pself.window makeKeyAndVisable]; + +//ViewController +-(UIView *)view{ + if(!_view){ + [self loadView]; + [self viewDidLoad]; + } +} + +-(void)loadView{ + UIView*view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds]; + view.backgroundColor = [UIColor greenColor]; self.view = view; + +} + +-(void)viewDidload{ + [super viewDidload]; + self.view.backgroundColor = [UIColor brownColor]; + +} + +``` + +### 请问此时界面颜色是什么? + +可能很多人会回到绿色。其实答案是 红色 + +why?在AppDelegate中vc.view.backgroundColor就是调用vc的view的getter方法,在getter方法内部判断_view是否存在,不存在则新建一个UIView,新建view是通过[self loadView]方法创建,创建成功直接调用viewdidload方法;存在则直接返回,所以界面先是绿色,再是棕色最后是红色 + +#### 来一个官方解释 + +![Apple 文档](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2287777-8ff7c3b976ffb29a.png) + + diff --git a/第一部分 iOS/1.30.md b/第一部分 iOS/1.30.md new file mode 100644 index 0000000..20d1d50 --- /dev/null +++ b/第一部分 iOS/1.30.md @@ -0,0 +1,39 @@ +# UILabel 给关键字模糊匹配并高亮 + +> 有些情况就是需要查找某个字符串并高亮,但有些需求就是需要全局模糊查找,找到符合的字符串并高亮。造了个小轮子 + +###效果图 +![模糊匹配文字并高亮 +](https://raw.githubusercontent.com/FantasticLBP/BlogDemos/master/image/QQ20180610-235439%402x.png) + +``` +#pragma mark -- 设置在一个文本中所有特殊字符的特殊颜色 ++ (NSMutableAttributedString *)setAllText:(NSString *)allStr andSpcifiStr:(NSString *)keyWords withColor:(UIColor *)color specifiStrFont:(UIFont *)font{ + NSMutableAttributedString *mutableAttributedStr = [[NSMutableAttributedString alloc] initWithString:allStr]; + if (color == nil) { + color = [UIColor redColor]; + } + if (font == nil) { + font = [UIFont systemFontOfSize:17]; + } + + + for (NSInteger j=0; j<=keyWords.length-1; j++) { + + NSRange searchRange = NSMakeRange(0, [allStr length]); + NSRange range; + NSString *singleStr = [keyWords substringWithRange:NSMakeRange(j, 1)]; + while + ((range = [allStr rangeOfString:singleStr options:NSLiteralSearch range:searchRange]).location != NSNotFound) { + //改变多次搜索时searchRange的位置 + searchRange = NSMakeRange(NSMaxRange(range), [allStr length] - NSMaxRange(range)); + //设置富文本 + [mutableAttributedStr addAttribute:NSForegroundColorAttributeName value:color range:range]; + [mutableAttributedStr addAttribute:NSFontAttributeName value:font range:range]; + } + } + return mutableAttributedStr; +} +``` + +[源码地址](https://github.com/FantasticLBP/BlogDemos/tree/master/LBPAttributedStringTools) \ No newline at end of file diff --git a/第一部分 iOS/1.31.md b/第一部分 iOS/1.31.md new file mode 100644 index 0000000..e7be9ad --- /dev/null +++ b/第一部分 iOS/1.31.md @@ -0,0 +1,155 @@ +JavascriptCore + + + +1、JSCore 是基于 webkit 以 C/C++ 实现的一个 js 包装,让 js 和 Native 交互变得更加简单。 + +- JScontext + JSContext 代表一个 JavaScript 的执行环境的一个实例。所有JavaScript执行都是在上下文内进行。JSContext还用于管理对象的生命周期内 JavaScript 的虚拟机 +- JSValue + JSValue 是用来接收 JSContext 执行后的返回结果。JSValue 可以是 JS 的任意类型(变量、对象、函数...) +- JSManagedValue + JSManagedValue 是对 JSValue 的封装,可以解决 JS 和 OC 之间循环引用的问题。JSManagedValue 最常用的用法就是安全的从内存堆区里面引用 JSValue 对象.如果 JSValue 存储在内存的堆区的方式是不正确的,很容易造成循环引用,然后导致 JSContext 对象不能正确的释放掉. +- JSExport + 是一个协议,用来将 Native 对象暴露给 JS,这个对象可以指向给自身和别的对象。 +- JSVirtualMachine + 管理 JS 对象空间和所需的资源 + +2、Native 调用 JS + +- 加载 JS 代码 + (JSValue *)evaluateScript:(NSString *)script; + +- 调用 JS 方法 + JSvalue *callBack = self.context[@"sayHi"]; + [callback callWithArguments:@[@"杭城小刘"]]; + + +3、JS 调用 Native + +- 通过 Block 实现。然后在 JS 中直接调用方法即可。需要注意的是在 Block 内部不要直接使用外部定义的 JScontext 对象或 JSValue ,应该作为参数传递进来,或者通过 + (JSContext *)currentContext; 来获取。否则会造成循环引用、内存无法被正确回收 + self.context[@"showMessage"] = ^(NSString *message){ + UIAlertController *alertCtr = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]; + [alertCtr addAction:cancel]; + //注意:方法是在子线程中执行的,需要跟新UI的话,需要切入主线程。 + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSealf presentViewController:alertCtr animated:YES completion:nil]; + }); + }; +- 通过 JSExport 协议实现; JS 需要通过 OC 中注入的对象来调方法,那么方法需要在协议中声明,并且在注入的对象中实现;在 webview 加载完成的时候注入实现协议的 Native 对象 + //声明协议 + + @proptocol JSInject + - (void)showMessage:(NSString *)message; + @end + + //实现相应的协议 + + - (void)showMessage:(NSString *)message{ + UIAlertController *alertCtr = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]; + [alertCtr addAction:cancel]; + //注意:方法是在子线程中执行的,需要跟新UI的话,需要切入主线程。 + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSealf presentViewController:alertCtr animated:YES completion:nil]; + }); + } + + //注入 + + - (void)webViewDidFinishLoad:(UIWebView *)webView + { + //从webview上获取相应的JSContext。 + self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; + + //注入JS需要的“OC”对象 + self.context[@"Bridge"] = [JSInject new]; + } + + +举个🌰 + +JS call Native + + //对外要暴露的 native 对象(其中挂载了一些属性和方法) + #import + #import + + @protocol PersonInjectExport + + @property (nonatomic, strong) NSString *name; + + @property (nonatomic, strong) NSString *hobby; + + - (id)sayHi; + + @end + + + @interface PersonInject : NSObject + + @property (nonatomic, strong) NSString *name; + + @property (nonatomic, strong) NSString *hobby; + + - (id)sayHi; + + @end + + + // viewcontroller + + - (void)webViewDidFinishLoad:(UIWebView *)webView{ + self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; + PersonInject *person = [[PersonInject alloc] init]; + person.name = @"杭城小刘"; + person.hobby = @"Coding、Movie、Music、Table tennis、Fit"; + self.jsContext[@"lbp"] = person; + } + + //JS + + + 嗨。大家好我是 +

***

+ + + + + + + + +Native call JS + + //Native + - (void)callJS{ + JSValue *functionName = self.jsContext[@"sum"]; + NSInteger sum = [[functionName callWithArguments:@[@"2",@"18"]] toInt32];; + + UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"来自JS 的计算" message:[NSString stringWithFormat:@"%zd",sum] preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]; + [alertVC addAction:okAction]; + [self presentViewController:alertVC animated:YES completion:nil]; + } + + //JS + function sum(a ,b){ + return parseInt(a) + parseInt(b); + } + + diff --git a/第一部分 iOS/1.32.md b/第一部分 iOS/1.32.md new file mode 100644 index 0000000..dd5e752 --- /dev/null +++ b/第一部分 iOS/1.32.md @@ -0,0 +1,39 @@ +# Xcode 小技巧 + +1、快速打开:**Command+Shift+O** 。这个命令可以开启一个小窗格用来快速搜索浏览文件、类、算法以及函数等,且支持模糊搜索,这个命令可以说是日常开发中最常用的一个命令了。 + +2、显示项目导航器:**Command+Shift+J**。使用快速打开命令跳转到对应文件后,如果需要在左侧显示出该文件在项目中的目录结构,只需要键入这个命令,非常方便 + +3、显示编辑历史。如果一行代码写的很好或者很糟糕,不需要专门跑到 diff 工具去查看代码历史。在该行代码处右击,选择**Show Last Change For Line** + +4、跳转到方法。在使用类或者结构时,我们经常需要快速的跳转到类的某个特定方法。通过快捷键**control+6**再输入方法的头几个字母就可以非常方便的做到这点。 + +5、范围编辑。多光标是个很棒的并且每个高级的编辑器都该有的特训过,快捷键为**Command+Control+E**。将光标移动刀需要编辑的符号,输入快捷键,然后就可以在当前页面全局编辑了。 + +6、Xcode 设置代码只在 Debug 下起效的几种方式 +在日常开发中 Xcode 在 Debug 模式下写很多测试代码,或者引入一些第三方测试用的 .a 和 .framework 动态库,也会通过 CocoaPods 引入一些第三方测试工具或者库;但是不希望这些库在**Release**正式包中被引入,如何做到呢? + +* .h/.m 文件中的测试代码 + + Xcode 在 Debug 模式下定义了宏 DEBUG=1 ,所以我们可以在代码中把相关的测试代码写在预编译处理命令 **\#ifdef DEBUG... \#endif** 中间即可,如图所示 + +![DEBUG宏在头文件](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180626-144101@2x.png) + +![DEBUG宏在代码块](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180626-144240@2x.png?raw=true) + +* 测试用的 .a 和 .framework + + 对于拖拽到工程中的 .a .framework 静态库,可以在 **target->Build Settings->Search Paths**这2个选项,分别设置 **Library Search Paths**和**Framework Search Paths**这2个选项。如果我们需要在测试的时候会用到,那么我们可以将 **Debug** 对应的值留下,删掉**Release** 对应的值。这样我们打包 Release 包的时候就不会包含不需要的包。 + +![不需要的包删除即可](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180626-144819@2x.png?raw=true) + +* CocoPods 引入的库 + 对于 CocoPods 方式引入的库,在配置的时候就可以处理掉,比如下面的方式 + ``` + platform: iOS, '8.0' + ... + pod 'PonyDebugger', :configurations => ['Debug'] + ``` + +\(参考来自:「 iOS知识小集 」2018 · 第 18 期\) + diff --git a/第一部分 iOS/1.33.md b/第一部分 iOS/1.33.md new file mode 100644 index 0000000..936788f --- /dev/null +++ b/第一部分 iOS/1.33.md @@ -0,0 +1,30 @@ +# 终端效率 + + +## tree + +如果要在终端查看当前目录的层级结构,不妨了解下**tree**。它可以以树状的形式展示当前的目录结构。 + +安装: +在终端输入**brew install tree** + +使用: + +在当前目录下,显示树状目录结构。**tree -L 2 -d**。其中 -L 表示遍历的深度,-d 表示只显示目录。 + + +## 合并写法 + +之前在终端操作的时候都是老老实实一行行的写代码,最近发现可以合并起来写。比如 + +``` +//写法一 +cd Desktop +mkdir awesome-project +cd awesome-project +//写法二 +cd Desktop && mkdir awesome-project && cd awesome-project +``` + + + diff --git a/第一部分 iOS/1.34.md b/第一部分 iOS/1.34.md new file mode 100644 index 0000000..33b77ec --- /dev/null +++ b/第一部分 iOS/1.34.md @@ -0,0 +1,303 @@ +# 终极截屏 + +- **-(void)snapshotForView:(__kindof UIView *)view;** + + 今天新学到这种写法,__kindof 是苹果声明的一个特性,是 Xcode7 出现的新特性。 + + - 假如我们想声明一个方法,这个方法的参数必须是一个 UIView 类型的对象,那么我们应该可以写成下面这个样子 + + ``` + -(void)snapshotForView:(UIView *)view; + ``` + + + + - 那么我们想声明一个方法,这个方法的参数必须是 UIView 及其 UIView 的子类,那么前一种写法就满足不了我们的需求了,这时候引入了 __kindof 方法 + + ``` + -(void)snapshotForView:(__kindof UIView *)view; + ``` + + + + + +**UIWebView 截图** + +对 UIWebView 截图比较简单,renderInContext 这个方法相信大家都不会陌生,这个方法是 CALayer 的一个实例方法,可以用来对大部分 View 进行截图。我们知道,UIWebView 承载内容的其实是作为其子 View 的 UIScrollView,所以对 UIWebView 截图应该对其 scrollView 进行截图。具体的截图方法如下: + +``` +- (void)snapshotForScrollView:(UIScrollView *)scrollView +{ + // 1. 记录当前 scrollView 的偏移和位置 + CGPoint currentOffset = scrollView.contentOffset; + CGRect currentFrame = scrollView.frame; + + scrollView.contentOffset = CGPointZero; + // 2. 将 scrollView 展开为其实际内容的大小 + scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); + + // 3. 第三个参数设置为 0 表示设置为屏幕的默认缩放因子 + UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, 0); + [scrollView.layer renderInContext:UIGraphicsGetCurrentContext()]; + UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + // 4. 重新设置 scrollView 的偏移和位置,还原现场 + scrollView.contentOffset = currentOffset; + scrollView.frame = currentFrame; +} +``` + +**WKWebView 截图** + +虽然 WKWebView 里也有 scrollView,但是直接对这个 scrollView 截图得到的是一片空白的,具体原因不明。一番 Google 之后可以看到好些人提到 drawViewHierarchyInRect 方法, 可以看到这个方法是 iOS 7.0 开始引入的。官方文档中描述为: + +> Renders a snapshot of the complete view hierarchy as visible onscreen into the current context. + +注意其中的 **visible onscreen**,也就是将屏幕中可见部分渲染到上下文中,这也解释了为什么对 WKWebView 中的 scrollView 展开为实际内容大小,再调用 drawViewHierarchyInRect 方法总是得到一张不完整的截图(只有屏幕可见区域被正确截到,其他区域为空白)。 + +不过,这样倒是给我们提供了一个思路,可以将 WKWebView 按屏幕高度裁成 n 页,然后将 WKWebView 一页一页的往上推,每推一页就调用一次 drawViewHierarchyInRect 将当前屏幕的截图渲染到上下文中,最后调用 UIGraphicsGetImageFromCurrentImageContext 从上下文中获取的图片即为完整截图。 + +核心代码如下: + +``` +- (void)snapshotForWKWebView:(WKWebView *)webView +{ + // 1 + UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES]; + [webView.superview addSubview:snapshotView]; + + // 2 + CGPoint currentOffset = webView.scrollView.contentOffset; + ... + + // 3 + UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds]; + [webView removeFromSuperview]; + [containerView addSubview:webView]; + + // 4 + CGSize totalSize = webView.scrollView.contentSize; + NSInteger page = ceil(totalSize.height / containerView.bounds.size.height); + + webView.scrollView.contentOffset = CGPointZero; + webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height); + + UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale); + [self drawContentPage:0 maxIndex:page completion:^{ + UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + // 8 + [webView removeFromSuperview]; + ... + }]; +} + +- (void)drawContentPage(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion +{ + // 5 + CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(containerView.bounds), containerView.bounds.size.width, containerView.frame.size.height); + CGRect myFrame = webView.frame; + myFrame.origin.y = -(index * containerView.frame.size.height); + webView.frame = myFrame; + + // 6 + [targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES]; + + // 7 + if (index < maxIndex) { + [self drawContentPage:index + 1 maxIndex:maxIndex completion:completion]; + } else { + completion(); + } +} +``` + +代码注意项如下(对应代码注释中的序号): + +1. 为了截图时对 frame 进行操作不会出现闪屏等现象,我们需要盖一个“假”的 webView 到现在的位置上,并将真正的 webView “摘下来”。调用 snapshotViewAfterScreenUpdates 即可得到这样一个“假”的 webView + + + +2. 保存真正的 webView 的偏移、位置等信息,以便截图完成之后“还原现场” + +3. 用一个新的视图承载“真正的” webView,这个视图也是绘图所用到的上下文 + +4. 将 webView 按照实际内容高度和屏幕高度分成 page 页 + +5. 得到每一页的实际位置,并将 webView 往上推到该位置 + +6. 调用 drawViewHierarchyInRect 将当前位置的 webView 渲染到上下文中 + +7. 如果还未到达最后一页,则递归调用 drawViewHierarchyInRect 方法进行渲染;如果已经渲染完了全部页,则回调通知截图完成 + +8. 调用 UIGraphicsGetImageFromCurrentImageContext 方法从当前上下文中获取到完整截图,将第 2 步中保存的信息重新赋予到 webView 上,“还原现场” + +注意:我们的截图方法中有对 webView 的 frame 进行操作,如果其他地方如果有对 frame 进行操作的话,是会影响我们截图的。所以在截图时应该禁用掉其他地方对 frame 的改变,就像这样: + +``` +- (void)layoutWebView +{ + if (!_isCapturing) { + self.wkWebView.frame = [self frameForWebView]; + } +} +``` + +``` +#import +@class PPSnapshotHandler; + +@protocol PPSnapshotHandlerDelegate + +@optional + +- (void)snapshotHandler:(PPSnapshotHandler *)snapshotHandler didFinish:(UIImage *)captureImage forView:(UIView *)view; + +@end + +@interface PPSnapshotHandler : NSObject + ++ (instancetype)defaultHandler; + +@property (nonatomic, weak) id delegate; + +- (void)snapshotForView:(__kindof UIView *)view; + +@end + + +#import "PPSnapshotHandler.h" +#import + +#define DELAY_TIME_DRAW 0.1 + +@interface PPSnapshotHandler () { + BOOL _isCapturing; + UIView *_captureView; +} +@end + +@implementation PPSnapshotHandler + ++ (instancetype)defaultHandler +{ + static dispatch_once_t onceToken; + static PPSnapshotHandler *defaultHandler = nil; + dispatch_once(&onceToken, ^{ + defaultHandler = [[PPSnapshotHandler alloc] init]; + }); + return defaultHandler; +} + +#pragma mark - public method + +- (void)snapshotForView:(__kindof UIView *)view +{ + if (!view || _isCapturing) { + return; + } + + _captureView = view; + + if ([view isKindOfClass:[UIScrollView class]]) { + [self snapshotForScrollView:(UIScrollView *)view]; + } else if ([view isKindOfClass:[UIWebView class]]) { + UIWebView *webView = (UIWebView *)view; + [self snapshotForScrollView:webView.scrollView]; + } else if ([view isKindOfClass:[WKWebView class]]) { + [self snapshotForWKWebView:(WKWebView *)view]; + } +} + +#pragma mark - WKWebView + +- (void)snapshotForWKWebView:(WKWebView *)webView +{ + UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES]; + snapshotView.frame = webView.frame; + [webView.superview addSubview:snapshotView]; + + CGPoint currentOffset = webView.scrollView.contentOffset; + CGRect currentFrame = webView.frame; + UIView *currentSuperView = webView.superview; + NSUInteger currentIndex = [webView.superview.subviews indexOfObject:webView]; + + UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds]; + [webView removeFromSuperview]; + [containerView addSubview:webView]; + + CGSize totalSize = webView.scrollView.contentSize; + NSInteger page = ceil(totalSize.height / containerView.bounds.size.height); + + webView.scrollView.contentOffset = CGPointZero; + webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height); + + UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale); + [self drawContentPage:containerView webView:webView index:0 maxIndex:page completion:^{ + UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + [webView removeFromSuperview]; + [currentSuperView insertSubview:webView atIndex:currentIndex]; + webView.frame = currentFrame; + webView.scrollView.contentOffset = currentOffset; + + [snapshotView removeFromSuperview]; + + self->_isCapturing = NO; + + if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) { + [self.delegate snapshotHandler:self didFinish:snapshotImage forView:self->_captureView]; + } + }]; +} + +- (void)drawContentPage:(UIView *)targetView webView:(WKWebView *)webView index:(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion +{ + CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(targetView.bounds), targetView.bounds.size.width, targetView.frame.size.height); + CGRect myFrame = webView.frame; + myFrame.origin.y = -(index * targetView.frame.size.height); + webView.frame = myFrame; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(DELAY_TIME_DRAW * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES]; + + if (index < maxIndex) { + [self drawContentPage:targetView webView:webView index:index + 1 maxIndex:maxIndex completion:completion]; + } else { + completion(); + } + }); +} + +#pragma mark - UIScrollView + +- (void)snapshotForScrollView:(UIScrollView *)scrollView +{ + CGPoint currentOffset = scrollView.contentOffset; + CGRect currentFrame = scrollView.frame; + + scrollView.contentOffset = CGPointZero; + scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); + + UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, UIScreen.mainScreen.scale); + [scrollView.layer renderInContext:UIGraphicsGetCurrentContext()]; + UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + scrollView.contentOffset = currentOffset; + scrollView.frame = currentFrame; + + if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) { + [self.delegate snapshotHandler:self didFinish:snapshotImage forView:_captureView]; + } +} + +@end +``` + + + diff --git a/第一部分 iOS/1.35.md b/第一部分 iOS/1.35.md new file mode 100644 index 0000000..a4140ef --- /dev/null +++ b/第一部分 iOS/1.35.md @@ -0,0 +1,28 @@ +# 推送 + +> 1、现在 App 开发推送功能,一般都是接入极光推送,那么为什么极光推送就可以实现推送呢? +2、极光推送做了哪些事情?与 APNS 怎么交互的? +带着这2个问题来看看推送吧 + +## 一、推送原理 +![推送原理](/assets/4316713-49ef454cca917acd.jpg) + +(这张图转载于网络) + + + +说说推送的步骤: +1、你的 App 需要推送服务,要向苹果的 APNS 注册推送功能 +2、当苹果 APNS 推送服务器收到你的注册请求后会返回给你一串 device token +3、当应用收到 device token 后,需要将 device token 传给自己的应用服务器(自己公司的服务端) +4、当你需要为你的应用推送消息的时候,自己的应用服务器会将消息,以及 device token 打包发送给苹果的 APNS。 +5、APNS 再将消息推送给你的 手机 App + + +## 不接入极光推送的话,自己怎么做推送功能 + +参考这篇文章:[自己做推送](https://blog.csdn.net/shenjie12345678/article/details/41120637) + +## 所以极光推送逗帮我们做了什么? + +简化了获取 device token 的步骤,我们将申请号的证书上传到极光服务器,程序运行接入极光 SDK ,手机获取 device token, 然后将 device token 上传给极光推送服务器,。 \ No newline at end of file diff --git a/第一部分 iOS/1.36.md b/第一部分 iOS/1.36.md new file mode 100644 index 0000000..9d0d687 --- /dev/null +++ b/第一部分 iOS/1.36.md @@ -0,0 +1,55 @@ +# App 评分 + +> 经常有这样的需求-引导用户在合适的时机对 App 做出好评。本文就尝试谈一谈这一块的一些知识 + +1. 评分的方式 +可以跳出应用对 App 进行评分,也可以在应用内进行评分(>= iOS 10.3)。 + +2. 跳出 App 评分 +利用系统方法打开 URL(跳到 App store 后跳转到自己 App 的评价页面) +``` +NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"]; +[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]]; + ``` + +3. 应用内评分 +iOS 10.3 之后系统为我们评分这个需求引入 **StoreKit**。利用它,我们可以很方便地在应用内对 App 进行快速评分,而不用跳出去。 + + 在 App 内部打开 App store并跳转到App 评价页面 + ``` + #import + SKStoreProductViewController *storeVC = [[SKStoreProductViewController alloc] init]; +storeVC.delegate = self; +[storeVC loadProductWithParameters:@{SKStoreProductParameterITunesItemIdentifier:@"1401834682"} completionBlock:^(BOOL result, NSError * _Nullable error) { + if (error) { + + }else{ + [self presentViewController:storeVC animated:YES completion:nil]; + } +}]; + ``` + + 在 App 内弹出评分对话框,用户星级评分后可以继续输入文字 + ``` + if (@available(iOS 10.3, *)) { + if([SKStoreReviewController respondsToSelector:@selector(requestReview)]){ + [SKStoreReviewController requestReview]; + else{ + NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]]; + } +}else { + // Fallback on earlier versions + NSString *urlString = [NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@?action=write-review", @"你的App ID"]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]]; +} + ``` + +4. 注意时机哦 + 我们的目的是能得到用户的正反馈,如果在用户刚使用APP时就弹出评分框,可能会给某些用户带来反感,因此,选择一个合适的时机弹出评分很重要,不然适得其反。 + 今天在使用爱奇艺的时候发现他们的弹出场景是这样的。我因为要出门所以下载了一部电影。在会员模式下高速缓存成功后(我很满意)弹出评分按钮。 + ![爱奇艺评分](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/61530453779_.pic.jpg) + + + + + + \ No newline at end of file diff --git a/第一部分 iOS/1.37.md b/第一部分 iOS/1.37.md new file mode 100644 index 0000000..fedb034 --- /dev/null +++ b/第一部分 iOS/1.37.md @@ -0,0 +1,163 @@ +## 一些布局小知识 + +1. LaunchScreen 会根据设备大小设置屏幕的显示范围;LaunchImage 则根据提供的启动图片设置App的可见范围 +2. UITextView 可以设置显示范围 + textView.textContainerInset = UIEdgeInsetsMake(40, 0, 0, 0); +3. UITextView 可以设置像 Word 一样文字环绕在图片四周的效果。其中用到的属性就是exclusionPaths + // Default value : empty array An array of UIBezierPath representing the exclusion paths inside the receiver's bounding rect. + @property (copy, NS_NONATOMIC_IOSONLY) NSArray *exclusionPaths NS_AVAILABLE(10_11, 7_0); + NSString *str = @“xxx”;//xxx为文字内容 + textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 20, self.view.frame.size.width-20, self.view.frame.size.height-30)]; + textView.text = str; + [self.view addSubview:textView]; + + imageView = [[UIImageView alloc] initWithFrame:CGRectMake(140, 280, 160, 100)]; imageView.backgroundColor = [UIColor orangeColor]; + imageView.image = [UIImage imageNamed:@"mao.jpg"]; + [self.view addSubview:imageView]; + textView.textContainer.exclusionPaths = @[[self translatedBezierPath]]; + + - (UIBezierPath *)translatedBezierPath{ + CGRect imageRect = [textView convertRect:imageView.frame fromView:self.view]; + UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(imageRect.origin.x+5, imageRect.origin.y, imageRect.size.width-5, imageRect.size.height-5)]; + return bezierPath; + } + 1. 引申学习 CoreText + 2. 在读很多第方库的时候,经常会看到2个关键词:“IB_DESIGNABLE”和”IBInspectable“。如果你想让你纯代码写的 View 具有可以在 StoryBoard 和 xib 文件可预览,就要在自定义的 UIView 头文件加上 IB_DESIGNABLE + 3. 如果想让你自定义的 View 的参数可以在 xib 或者 storyboard 上 Attributes inspector 栏目中被看到且可以被修改,那么你需要在每个 property 前面加上 IBInspectable + #import + + IB_DESIGNABLE + @interface DashView : UIView + + @property (nonatomic, copy) void(^TimerBlock)(NSInteger); + + @property (nonatomic, strong) IBInspectable UIColor *color; + + //跃动数字刷新 + - (void)refreshJumpNOFromNO:(NSString *)startNO toNO:(NSString *)toNO andTime:(NSString *)time; + + @end + + +4.UITabBarController 设置图片不能过大,不然不能显示 + +5.设置导航控制器的 NavigationBar 的 BackgroundImage 且 使用了 UIBarMetericsDefault 会导航控制器的子控制器的 view 的高度会减小 64。只有设置为 UIBarMetricsDefault 的时候给 NavigationBar 设置背景图片才会显示。UIBarMetricsCompact 意味着导航条是透明的 + + [self.navigationBar setBackgroundImage:[UIImage imageNamed:@"Report_customreport"] forBarMetrics:UIBarMetricsDefault]; + +1. 在 iOS 6及之前的系统上默认都是 NO,在 iOS 7及其以后都是默认为 YES。效果表现为顶部的 NavigationBar 都是有透明度的效果 + @property(nonatomic,assign,getter=isTranslucent) BOOL translucent NS_AVAILABLE_IOS(3_0) UI_APPEARANCE_SELECTOR; // Default is NO on iOS 6 and earlier. Always YES if barStyle is set to UIBarStyleBlackTranslucent + translucent 设置为 YES ,则布局 view 从屏幕的左上角开始计算,如果设置为 NO,那么布局从 NavigationBar 的下面开始布局。 +2. 总结:需要让导航控制器里面的控制器的 view 从导航栏以下开始布局,有2种方法可以实现。 + - 设置导航控制器 setTranslucent = NO + - 给导航控制器的 NavigationBar 设置背景图片,且 BarMetrics 需要设置为 UIBarMetricsDefault +3. + (void)load 和 + (void)initialize 的使用分析 + - initialize:第一次使用这个类或者它的子类的时候调用 + - load :这个方法在类加载的时候调用一次。 + //window 下有一个导航控制器,导航控制器的根控制器是 ViewController ,点击屏幕跳转到 SubViewController(继承自 ViewController) + + //ViewController + + (void)initialize{ + NSLog(@"%s",__func__); + } + + (void)load{ + NSLog(@"%s",__func__); + } + + - (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"test"; + self.view.backgroundColor = [UIColor whiteColor]; + } + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + SubViewController *vc = [[SubViewController alloc] init]; + [self.navigationController pushViewController:vc animated:YES]; + } + //SubViewController + + (void)initialize{ + [super initialize]; + NSLog(@"%s",__func__); + } + + //这个方法在类加载的时候调用一次。 + + (void)load{ + NSLog(@"%s",__func__); + } + + 2018-07-19 11:26:04.621740+0800 Test[14617:1049502] +[ViewController load] + 2018-07-19 11:26:04.622463+0800 Test[14617:1049502] +[SubViewController load] + 2018-07-19 11:26:04.743541+0800 Test[14617:1049502] +[ViewController initialize] + 2018-07-19 11:26:07.648425+0800 Test[14617:1049502] +[ViewController initialize] + 2018-07-19 11:26:07.648610+0800 Test[14617:1049502] +[SubViewController initialize] + + + 结果分析来看,类都被加载了(调用了 load 方法,其中页面显示的是 ViewController 所以它的 initialize 被调用,点击屏幕跳转到 SubViewController,所以 SubViewController 的 initialize 方法会被调用,在调用的时候调用了 super 关键字,调用父类的 initialize 方法) +4. UIAppearance appearanceWhenContainedInInstancesOfClasses : 这个方法可以控制让自定义的导航控制器的 appearance 只修改自己需要修改的样式,不至于对于全部的导航控制器的 navigationBar 全部修改。 +5. UIImage 与 UIImageRenderingMode + 在 iOS 系统中经常会用到 UIImage 来渲染一些控件,比如 UITabBar 和 UIBarButtonItem + 在日常开发的时候我们可以为 UITabBar 设置 items 属性。其中可以指定 UITabBar 的 image 和 selectedImage。此时你可以提供2张图片,比如下面的代码 + [homeBar setImage:[UIImage imageNamed:@"Tab_home"]]; + [homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home_selected"]]; + 你也可以按照下面的写法 + [homeBar setImage:[UIImage imageNamed:@"Tab_home"]]; + [homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home"]]; + 这是因为系统会渲染。如果不为 UIImage 设置渲染模式,系统会在合适的地方根据上下文渲染,比如在这个地方的 UITabBar 就会根据上下文渲染出选中的效果。我们不必要必须设置选中的颜色。 + 当然你也可以指定 UIImage 的渲染模式。下面看看官方文档讲的 UIImage 的渲染模式 + // Create a version of this image with the specified rendering mode. By default, images have a rendering mode of UIImageRenderingModeAutomatic. + - (UIImage *)imageWithRenderingMode:(UIImageRenderingMode)renderingMode NS_AVAILABLE_IOS(7_0); + @property(nonatomic, readonly) UIImageRenderingMode renderingMode NS_AVAILABLE_IOS(7_0); + + + + /* Images are created with UIImageRenderingModeAutomatic by default. An image with this mode is interpreted as a template image or an original image based on the context in which it is rendered. For example, navigation bars, tab bars, toolbars, and segmented controls automatically treat their foreground images as templates, while image views and web views treat their images as originals. You can use UIImageRenderingModeAlwaysTemplate to force your image to always be rendered as a template or UIImageRenderingModeAlwaysOriginal to force your image to always be rendered as an original. + */ + typedef NS_ENUM(NSInteger, UIImageRenderingMode) { + UIImageRenderingModeAutomatic, // Use the default rendering mode for the context where the image is used + + UIImageRenderingModeAlwaysOriginal, // Always draw the original image, without treating it as a template + UIImageRenderingModeAlwaysTemplate, // Always draw the image as a template image, ignoring its color information + } NS_ENUM_AVAILABLE_IOS(7_0); + UIImage 的渲染模式共有3种值可以选择 + - UIImageRenderingModeAutomatic:根据所使用的环境和绘图上下文自动调整渲染模式 + - UIImageRenderingModeAlwaysOriginal:始终绘制图片原始状态,不使用 tintColor + - UIImageRenderingModeAlwaysTemplate:始终根据tintColor绘制图片,不管图片本身的颜色状态 + +6.下面举个例子。在 UITabBarController 设置 tabBar + + [homeBar setImage:[UIImage imageNamed:@"Tab_home"]]; + [homeBar setSelectedImage:[UIImage imageNamed:@"Tab_home_selected"]]; + + + 从 iconfont 网站上面随便选择1个彩色 icon 用来做对比实验 + + ![iconfont小图标](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135721.png?raw=true) + + + - 实验1 + + [homeBar setImage:[UIImage imageNamed:@"Tab_home"]]; + [homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAutomatic]]; + + ![UIImageRenderingModeAutomatic模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135617@2x.png?raw=true) + + +- 实验2 + + [homeBar setImage:[UIImage imageNamed:@"Tab_home"]]; + [homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]]; + + ![UIImageRenderingModeAlwaysOriginal模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135552@2x.png?raw=true) + + +- 实验3 + [homeBar setImage:[UIImage imageNamed:@"Tab_home"]]; + [homeBar setSelectedImage:[[UIImage imageNamed:@"Tab_home_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; + + ![UIImageRenderingModeAlwaysTemplate模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180719-135552@2x.png?raw=true) + +结论:对于 UIImage 来说如果不指定渲染模式的话则默认使用**UIImageRenderingModeAutomatic**,则会根据渲染的环境和上下文进行渲染。如果指定了模式,则根据具体的模式开启渲染。**UIImageRenderingModeAlwaysOriginal:**则绘制图片的原始信息,不使用**tintColor**。**UIImageRenderingModeAlwaysTemplate:**则始终根据**tintColor**绘制图片,忽略图片本身的信息。 + + + +
+![引用自网络的图片](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/4673_140117110629_1.png?raw=true) \ No newline at end of file diff --git a/第一部分 iOS/1.38.md b/第一部分 iOS/1.38.md new file mode 100644 index 0000000..73fecec --- /dev/null +++ b/第一部分 iOS/1.38.md @@ -0,0 +1,72 @@ +# iOS 数值计算精度丢失问题 + +> 在 iOS 中经常会计算金额和价格,我们有时会定义数据类型为 double 或者 float,这样在做过一些运算后会发现精度丢失了,这显然不是我们想要的结果。今日偶然间看到一篇技术博文,为了记忆,顺道解决我的这个问题,所以记录了下来。 + + + +### 存在的问题(精度丢失) + +```objective-c +float a = 0.01; +int b = 99999999; +double c = 0.0; +c = a*b; +NSLog(@"c-%f",c); // c-1000000.000000 +NSLog(@"c-%.2f",c); // c-1000000.00 +``` + +通过上面的代码我们看到简单的数学运算后对于数据类型不合理的数值进行过运算后精度丢失了,这如果是在我们的 App 中,用户看到自己的金额不正确,那还不吓一跳?? + +接下来看看如何做简单的改进 + +```objective-c +NSString *aString = [NSString stringWithFormat:@"%f",a]; +NSString *bString = [NSString stringWithFormat:@"%.2f",(double)b]; +c = [aString doubleValue]*[bString doubleValue]; +NSLog(@"%f",c); //999999.990000 +NSLog(@"%.2f",c); //999999.99 +``` + +这样虽然可以达成目的,但是计算的过程比较麻烦,并不是我们想要的解决方案。通过查阅资料得知苹果推出了一个类,专门解决数据计算的精度问题NSDecimalNumber 。 + + + +### NSDecimalNumber 为数据精度应用而生 + + + +NSDecimalNumber 是 NSNumber 的子类,专门负责精度计算。提供了完善的初始化方案,对于头疼的精度计算问题(金额)它提供了便利的解决方案(加、减、乘、除、次方运算并且可以给计算出的结果设置明显的精度方案(四舍五入、取上、取下等等))。NSDecimalNumberHandler 可以对计算出的结果做一些策略,比如舍入的模式、数据溢出、除0等异常情况的处理规则。 + +我们来说说上面的问题吧,引入了 NSDecimalNumber,解决上面的问题就不费吹灰之力了。 + +```objective-c +NSString *decimalNumberMutiplyWithString(NSString *multiplierValue, NSString *multiplicandvalue){ + NSDecimalNumber *multiplierNumber = [NSDecimalNumber decimalNumberWithString:multiplierValue]; + + NSDecimalNumber *multiplicandNumber = [NSDecimalNumber decimalNumberWithString:multiplicandvalue]; + NSDecimalNumber *result = [multiplierNumber decimalNumberByMultiplyingBy:multiplicandNumber]; + return [result stringValue]; +} + +NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithMantissa:18992 exponent:-3 isNegative:NO]; +NSDecimalNumber *price2 = [NSDecimalNumber decimalNumberWithString:@"18.992"]; +NSLog(@"price:%@",[price stringValue]); //999999.990000 +NSLog(@"price2:%@",[price2 stringValue]); //999999.99 + + +//设置计算的精度(小数点位数、舍入规则) +NSDecimalNumberHandler *roundPlain = [NSDecimalNumberHandler + decimalNumberHandlerWithRoundingMode:NSRoundPlain + scale:1 + raiseOnExactness:NO + raiseOnOverflow:NO + raiseOnUnderflow:NO + raiseOnDivideByZero:YES]; + +NSDecimalNumber *resultDecimal = [multiplierNumber decimalNumberByMultiplyingBy:multiplicandNumber withBehavior:roundPlain]; +``` + +### 参考文章 + +[文章1](https://www.jianshu.com/p/25d24a184016)、[文章2](https://www.jianshu.com/p/ea4da259a062) + diff --git a/第一部分 iOS/1.39.md b/第一部分 iOS/1.39.md new file mode 100644 index 0000000..b46ad3b --- /dev/null +++ b/第一部分 iOS/1.39.md @@ -0,0 +1,34 @@ +### 数组、集合、字典与 hash、isEqual 方法的关联 + +1. NSArray 允许重复添加元素,添加元素的时候不查重,所以不会调用上面2个方法。在移出元素的时候会依次遍历数组内的元素,每个元素调用 **isEqual** 方法(remove 方法传入的元素作为参数),所有返回真值的元素都会被移除。在字典中不涉及 hash 方法。 + +2. NSSet 不允许重复添加元素,所以在添加新元素的时候,该元素的 **hash** 方法会被调用,若集合中不存在与此元素 hash 相同的元素,则它会被直接加入集合,不调用 **isEqual** 方法;若存在,则依次调用该集合每个元素的 **isEqual** 方法,返回真值则判等,不加入,处理结合,若返回 false, 则判定集合内不存在该元素,将其加入。 + +3. 集合中移除元素时,首先调用它的 **hash方法**,若集合中存在与其 **hash值** 相等的元素,则调用该元素的 **isEqual方法**,若返回真值则判断,进行移除;若不存在,则会依次调用集合中每个元素的 **isEqual方法**,只要找到一个返回真值的元素,就进行移除,并结束整个过程(所有这样会有其它满足 isEqual 方法但却漏掉未被删除的元素)。调用 contains 方法时类似 + +4. 因此如果自定义对象被加入到集合或作为字典的 key 时,需要同时重写 isEqual 方法和 hash 方法,这样,若集合存在某元素,则调用它的 contains 和 remove 方法时,可以在 O(1) 完成查询,否则需要 O(n) 完成。 +  +5. 需要注意的是,NSDictionary 的 key、value 都说对象类型即可,但是被设为 key 的对象需要遵循 NSCopying 协议。 + +6. hash 方法出现的目的是:当我们从数组中查找元素时,需要依次遍历数组中的元素,时间复杂度为 O(n),为了解决效率问题,引入了 Hash Table 方法。当添加元素的时候,为每个元素设置了 hash 值,这样当下次查找的时候就直接通过 hash 值找到对应的位置,时间复杂度为 O(1)。设计一个合理的 hash 算法的指标是对于每个参数,其返回的 hash 值唯一。 + +7. 查阅资料可知,一个推荐的自定义对象的 **hash** 算法是将关键属性的 hash 值,按照位或运算 + +``` +@interface Person : NSObject +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSDate *birthday; +@end + + +- (NSUInteger)hash{ + return [self.name hash] ^ [self.birthday hash]; +} +``` + + + +8. NSObject 类中的 equal 方法的判断是包括内存地址的,也就是说,NSObject 若想判断2个对象相等,那么这2个对象的内存地址必须相等 + + + diff --git a/第一部分 iOS/1.4.md b/第一部分 iOS/1.4.md new file mode 100644 index 0000000..49e1383 --- /dev/null +++ b/第一部分 iOS/1.4.md @@ -0,0 +1,17 @@ + +> 在web开发的过程中,抓包、调试页面样式、查看请求头是很常用的技巧。其实在iOS开发中,这些技巧也能用(无论是模拟器还是真机),不过我们需要用到mac自带的浏览器Safari。所以,本文将讲解如何使用Safari对iOS程序中的webview进行调试。 + +* 1、打开真机(模拟器)的开发者模式 +【设置】-> 【Safari】 -> 【高级】 -> 【Web检查器】打开 +![打开手机的调试模式](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2287777-e937adb9c77a3768.png) + +* 2、打开MBP上的Safari的开发者模式: +【Safari】->【偏好设置】->【高级】-> 【在菜单栏中显示“开发”菜单】勾选。 + +* 3、调试你的WebView页面。 + +* 4、在MBP的Safari选项中的开发,看到手机,右击可以看到正在调试的WebView的url +![选择需要调试等页面](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2287777-c12eb2da00e79f34.png) + +* 5、在弹出的这个框里面可以查看网页源代码以及可以调试样样式、查看localStorage、sessionStorage、Cookie的值等等,给原生端调试带来很大方便,不过这样前端调试更加方便啊,谷歌的模拟器不能完全模真实环境下的iphone使用效果啊。 +![调试手机页面](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2287777-4d55fd205fa81cc8.png) diff --git a/第一部分 iOS/1.40.md b/第一部分 iOS/1.40.md new file mode 100644 index 0000000..f900241 --- /dev/null +++ b/第一部分 iOS/1.40.md @@ -0,0 +1,938 @@ +## RunLoop 对象 + +iOS 中有2套 API 可以访问和使用 RunLoop。分别是 + +- Foundation:NSRunLoop + +- CoreFoundation:CFRunLoopRef + +``` +//Foundation +[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象 +[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象 + +//Core Foundation +CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象 +CFRunLoopGetMain(); // 获得主线程的RunLoop对象 +``` + + +NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的内部结果,就需要了解 [CFRunLoopRef](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。 + +- 每条线程都有与之一一对应的 RunLoop 对象 +- 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建 +- RunLoop 在第一次获取时创建,在线程结束时消失 + +### RunLoop 相关的5个类 + +- CFRunLoopRef +- CFRunLoopModeRef +- CFRunLoopSourceRef +- CFRunLoopTimerRef +- CFRunLoopObserverRef + + +### CFRunLoopModeRef 代表 RunLoop 的运行模式 + +- 一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个 Source/Timer/Observer +- 每次 RunLoop 启动,只能指定一个 Mode,这个 Mode 被叫做 CurrentMode +- 如果需要切换 Mode,只能退出 RunLoop,则以一个 Mode 进入 +- 这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响 + +系统默认注册了5个Mode + +- kCFRunLoopDefaultMode:App 的默认 Mode,通常主线程是在这个 Mode 下运行 +- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 +- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用 +- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 +- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode + + +## CFRunLoopSourceRef 事件源(输入源) + +- 早期的分法: + - Ported-Based Source + - Custom Input Source + - Cocoa Perform Selector Source +- 现在的分法 + - Source0:非基于 port 的,用户主动触发的事件 + - Source1: 基于 port的,通过内核在线程间相互发送消息 + + +## CFRunLoopTimerRef 是基于时间的触发器 + +- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响 +- GCD 的 timer 不受 RunLoopMode 的影响 + +## - CFRunLoopObserverRef 观察者,监听 RunLoop 状态的变化 + + ```objective-c + /* Run Loop Observer Activities */ + typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { + kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop + kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 NSTimer + kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source + kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 + kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 + kCFRunLoopExit = (1UL << 7), // 退出 RunLoop + kCFRunLoopAllActivities = 0x0FFFFFFFU + }; + ``` + + 添加 Observer + + ```objective-c + //1、获得当前线程下的 RunLoop + CFRunLoopRef runloop = CFRunLoopGetCurrent(); + //2、为 RunLoop 创建观察者 + CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + + + }); + //3、为当前的 RunLoop 添加观察者 + CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode); + //4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release + CFRelease(obersver); + ``` + + + +## NSTimer 经常会不准确,原因是什么? + +NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes + +```objective-c +NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; +[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; +``` + +NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。 + +```objective-c +#import "ViewController.h" + +@interface ViewController () +@property (nonatomic, strong) dispatch_source_t timer; +@end + + +@implementation ViewController + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + /* + 只在默认状态下执行的 NSTimer + [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { + NSLog(@"我在执行了"); + }]; + */ + + /* + 指定 NSRunLoopMode 的 NSTimer + NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; + */ + + /* + GCD 的单位是 纳秒. + 使用 GCD 创建的 timer 正常创建后不会执行,因为创建后设置了指定的时间后触发,所以当代码运行到最后一行的时候,Timer 还没执行,就被销毁了。所以我们必须设置一个属性去保存它。 + */ + //1、创建队列 + dispatch_queue_t queue = dispatch_get_main_queue(); + //2、创建 timer + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + self.timer = timer; + //3、设置 timer 的参数:精准度、时间间隔 + //第三个参数为 GCD timer 的精准度 + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); + //4、为 Timer 设置任务 + dispatch_source_set_event_handler(timer, ^{ + NSLog(@"%@",[NSRunLoop currentRunLoop]); + }); + //5、执行任务 + dispatch_resume(timer); +} + +- (void)show{ + NSLog(@"shw-%@",[NSThread currentThread]); + NSLog(@"%@",[NSRunLoop currentRunLoop]); +} +@end +``` + + +## 监听 RunLoop + +```objective-c +//给 RunLoop 添加监听者 +- (void)testRunLoopObserver{ + + //创建监听者 +// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>) + /* + 创建监听对象 + 参数1:分配内存空间 + 参数2:要监听的状态 kCFRunLoopAllActivities :所有状态 + 参数3:是否要持续监听 + 参数4:优先级 + 参数5:回调 + */ + CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + switch (activity) { + case kCFRunLoopEntry: + NSLog(@"RunLoop 闪亮登场"); + break; + case kCFRunLoopBeforeTimers: + NSLog(@"RunLoop 大哥要处理 Timer 了"); + break; + case kCFRunLoopBeforeSources: + //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息 + NSLog(@"RunLoop 大哥要处理 Source 了"); + break; + case kCFRunLoopBeforeWaiting: + NSLog(@"RunLoop 大哥没事干要睡觉了"); + break; + case kCFRunLoopAfterWaiting: + NSLog(@""); + NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了"); + break; + case kCFRunLoopExit: + NSLog(@"RunLoop 大哥要退出离开了"); + break; + default: + break; + } + }); + /* + 参数1:要监听哪个RunLoop + 参数2:监听者 + 参数3:要监听 RunLoop 在哪种运行模式下的状态 + */ + CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode); + //CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release + CFRelease(oberver); + [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES]; +} + + +//等到 RunLoop 休眠后,5秒钟叫醒 RunLoop +- (void)wakeupRunLoop{ + NSLog(@"%s",__func__); +} +/* +2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 +2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974] +2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 +2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 +2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974] +2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 +2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 +2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974] +2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 +2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop] +2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 +2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974] +2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 +2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop] +2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 +2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 +2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 +*/ +``` + +![触摸屏幕事件在 RunLoop 下的 source0](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180801-104553@2x.png?raw=true) + + +上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟 + +```objective-c +- (void)testRunLoopObserverOnSubThread{ + + //创建并发队列 + dispatch_queue_t queue = dispatch_queue_create("com.lbp.testRunLoopOnSubThread", DISPATCH_QUEUE_CONCURRENT); + //开启子线程 + dispatch_async(queue, ^{ + + //1、获得当前线程下的 RunLoop + CFRunLoopRef runloop = CFRunLoopGetCurrent(); + + + //2、为 RunLoop 创建观察者 + CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + switch (activity) { + case kCFRunLoopEntry: + NSLog(@"RunLoop 闪亮登场"); + break; + case kCFRunLoopBeforeTimers: + NSLog(@"RunLoop 大哥要处理 Timer 了"); + break; + case kCFRunLoopBeforeSources: + //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息 + NSLog(@"RunLoop 大哥要处理 Source 了"); + break; + case kCFRunLoopBeforeWaiting: + NSLog(@"RunLoop 大哥没事干要睡觉了"); + break; + case kCFRunLoopAfterWaiting: + NSLog(@""); + NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了"); + break; + case kCFRunLoopExit: + NSLog(@"RunLoop 大哥要退出离开了"); + break; + default: + break; + } + }); + //为了运行 RunLoop 必须触发事件 + [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(wakeUpRunLoopOnSubThread) userInfo:nil repeats:NO]; + //3、为当前的 RunLoop 添加观察者 + CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode); + //4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release + CFRelease(obersver); + //5、在非主线程创建的 RunLoop 必须触发运行 + [[NSRunLoop currentRunLoop] run]; + }); +} + + +- (void)wakeUpRunLoopOnSubThread{ + NSLog(@"%s",__func__); +} +/* +2018-08-01 14:23:06.453282+0800 RunLoop[2376:115968] RunLoop 闪亮登场 +2018-08-01 14:23:06.453608+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Timer 了 +2018-08-01 14:23:06.453781+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Source 了 +2018-08-01 14:23:06.453982+0800 RunLoop[2376:115968] RunLoop 大哥没事干要睡觉了 +2018-08-01 14:23:08.458237+0800 RunLoop[2376:115968] +2018-08-01 14:23:08.458658+0800 RunLoop[2376:115968] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 +2018-08-01 14:23:08.458894+0800 RunLoop[2376:115968] -[ViewController wakeUpRunLoopOnSubThread] +2018-08-01 14:23:08.459082+0800 RunLoop[2376:115968] RunLoop 大哥要退出离开了 +*/ +``` + + + + + +## RunLoop 内部运行原理 + +![RunLoop 运行原理图1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/image-20180801113342611.png) + + + + + +- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。 + - Source0:非基于 port 的,用户主动触发的事件。 + - Source1:基于 port,通过内核和其它线程互相发送消息 +- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop) + +## RunLoopMode 的概念 + +![转载于网络](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/1785352-087fd4b664e0e387.png) + + + + + + + +## 底层实现 + + + +内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer) + +我们来看看苹果官方开源的 [CFRunLoop.c 文件](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。看几个关键函数的实现猜测下 RunLoop 的内部原理 + +以下的代码都有注释说明 + +**__CFRunLoopModeIsEmpty** + + 此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop + + + +```objective-c +// expects rl and rlm locked + static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { + CHECK_FOR_FORK(); + if (NULL == rlm) return true; + #if DEPLOYMENT_TARGET_WINDOWS + if (0 != rlm->_msgQMask) return false; + #endif + Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); + if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue + // 判断时候有没有_sources0 + if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; + // 判断时候有没有_sources1 + if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; + // 判断时候有没有_timers + if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; + + + struct _block_item *item = rl->_blocks_head; + while (item) { + struct _block_item *curr = item; + item = item->_next; + Boolean doit = false; + if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { + doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } else { + doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } + if (doit) return false; + } + return true; + } +``` + + + +**CFRunLoopRun、CFRunLoopRunInMode** + + 1、2个函数的作用分别是让 RunLoop 跑在 KCFRunLoopDefaultMode 下和特定的 Mode 下 + + 2、2个函数本质上都是调用 CFRunLoopRunSpecific + +```objective-c + // 用DefaultMode启动 + void CFRunLoopRun(void) { /* DOES CALLOUT */ + int32_t result; + do { + result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); + CHECK_FOR_FORK(); + } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); + } + + + // 用指定的Mode启动,允许设置RunLoop超时时间 + SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); + } +``` + + + +**CFRunLoopRunSpecific** + + 参数1: RunLoop 对象。参数2:运行 Mode 名称。参数3:超时时间。参数4:主_CFRunLoopRun 会用到 + +```objective-c + // RunLoop的实现 + SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + if (__CFRunLoopIsDeallocating(rl)) + return kCFRunLoopRunFinished; + __CFRunLoopLock(rl); + + + // 根据modeName找到对应mode + CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); + + + // 判断mode里没有source/timer, 没有直接返回。 + if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { + Boolean did = false; + if (currentMode) + __CFRunLoopModeUnlock(currentMode); + __CFRunLoopUnlock(rl); + return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; + } + + + volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); + + + CFRunLoopModeRef previousMode = rl->_currentMode; + rl->_currentMode = currentMode; + int32_t result = kCFRunLoopRunFinished; + + + if (currentMode->_observerMask & kCFRunLoopEntry ) + // 1. 通知 Observers: RunLoop 即将进入 loop + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); + // 进入loop + result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); + + + if (currentMode->_observerMask & kCFRunLoopExit ) + // 10.通知 Observers: RunLoop 即将退出。 + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); + + + __CFRunLoopModeUnlock(currentMode); + __CFRunLoopPopPerRunData(rl, previousPerRun); + rl->_currentMode = previousMode; + __CFRunLoopUnlock(rl); + return result; + } +``` + + + +**__CFRunLoopDoObserver** + + + + 调用 Observer 回调 + + 联想给 RunLoop 添加观察者,监听 RunLoop 状态。 + +```objective-c + static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + + + CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0; + if (cnt < 1) return; + + + /* Fire the observers */ + STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1); + CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef)); + CFIndex obs_cnt = 0; + //遍历 rlm-> _observers,将元素放到 collectedObservers 数组中 + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx); + if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) { + collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); + } + } + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + for (CFIndex idx = 0; idx < obs_cnt; idx++) { + CFRunLoopObserverRef rlo = collectedObservers[idx]; + __CFRunLoopObserverLock(rlo); + if (__CFIsValid(rlo)) { + Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo); + __CFRunLoopObserverSetFiring(rlo); + __CFRunLoopObserverUnlock(rlo); + __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info); + if (doInvalidate) { + CFRunLoopObserverInvalidate(rlo); + } + __CFRunLoopObserverUnsetFiring(rlo); + } else { + __CFRunLoopObserverUnlock(rlo); + } + CFRelease(rlo); + } + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + + + if (collectedObservers != buffer) free(collectedObservers); + } +``` + + + +**__CFRunLoopRun** + +```objective-c + /* rl, rlm are locked on entrance and exit */ + static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { + + + uint64_t startTSR = mach_absolute_time(); + + + if (__CFRunLoopIsStopped(rl)) { + __CFRunLoopUnsetStopped(rl); + return kCFRunLoopRunStopped; + } else if (rlm->_stopped) { + rlm->_stopped = false; + return kCFRunLoopRunStopped; + } + + + mach_port_name_t dispatchPort = MACH_PORT_NULL; + Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); + if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); + + + #if USE_DISPATCH_SOURCE_FOR_TIMERS + mach_port_name_t modeQueuePort = MACH_PORT_NULL; + if (rlm->_queue) { + modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue); + if (!modeQueuePort) { + CRASH("Unable to get port for run loop mode queue (%d)", -1); + } + } + #endif + + + dispatch_source_t timeout_timer = NULL; + struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context)); + if (seconds <= 0.0) { // instant timeout + seconds = 0.0; + timeout_context->termTSR = 0ULL; + } else if (seconds <= TIMER_INTERVAL_LIMIT) { + dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground(); + timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_retain(timeout_timer); + timeout_context->ds = timeout_timer; + timeout_context->rl = (CFRunLoopRef)CFRetain(rl); + timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds); + dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context + dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout); + dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel); + uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL); + dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL); + dispatch_resume(timeout_timer); + } else { + // 设置RunLoop超时时间 + seconds = 9999999999.0; + timeout_context->termTSR = UINT64_MAX; + } + + + Boolean didDispatchPortLastTime = true; + int32_t retVal = 0; + do { + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED; + voucher_t voucherCopy = NULL; + #endif + uint8_t msg_buffer[3 * 1024]; + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + mach_msg_header_t *msg = NULL; + mach_port_t livePort = MACH_PORT_NULL; + #elif DEPLOYMENT_TARGET_WINDOWS + HANDLE livePort = NULL; + Boolean windowsMessageReceived = false; + #endif + __CFPortSet waitSet = rlm->_portSet; + + + __CFRunLoopUnsetIgnoreWakeUps(rl); + + + if (rlm->_observerMask & kCFRunLoopBeforeTimers) + // 2. 通知 Observers: RunLoop 即将触发 Timer 回调 + __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); + if (rlm->_observerMask & kCFRunLoopBeforeSources) + // 3. 通知 Observers: RunLoop 即将触发 Source 回调 + __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); + // 执行被加入的block + __CFRunLoopDoBlocks(rl, rlm); + + + // 4. RunLoop 触发 Source0 (非port) 回调 + Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); + if (sourceHandledThisLoop) { + // 执行被加入的block + __CFRunLoopDoBlocks(rl, rlm); + } + + + Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); + + + // 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息 + if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + msg = (mach_msg_header_t *)msg_buffer; + + + if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { + goto handle_msg; + } + #elif DEPLOYMENT_TARGET_WINDOWS + if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { + goto handle_msg; + } + #endif + } + + + didDispatchPortLastTime = false; + + + // 通知 Observers: RunLoop 的线程即将进入休眠(sleep) + if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); + __CFRunLoopSetSleeping(rl); + // do not do any user callouts after this point (after notifying of sleeping) + + + // Must push the local-to-this-activation ports in on every loop + // iteration, as this mode could be run re-entrantly and we don't + // want these ports to get serviced. + + + __CFPortSetInsert(dispatchPort, waitSet); + + + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + + CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent(); + + + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + #if USE_DISPATCH_SOURCE_FOR_TIMERS + do { + if (kCFUseCollectableAllocator) { + // objc_clear_stack(0); + // + memset(msg_buffer, 0, sizeof(msg_buffer)); + } + msg = (mach_msg_header_t *)msg_buffer; + + + __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); + + + if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { + // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer. + while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue)); + if (rlm->_timerFired) { + // Leave livePort as the queue port, and service timers below + rlm->_timerFired = false; + break; + } else { + if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); + } + } else { + // Go ahead and leave the inner loop. + break; + } + } while (1); + #else + if (kCFUseCollectableAllocator) { + // objc_clear_stack(0); + // + memset(msg_buffer, 0, sizeof(msg_buffer)); + } + msg = (mach_msg_header_t *)msg_buffer; + __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); + #endif + + + + + #elif DEPLOYMENT_TARGET_WINDOWS + // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages. + __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived); + #endif + + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + + + rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart)); + + + // Must remove the local-to-this-activation ports in on every loop + // iteration, as this mode could be run re-entrantly and we don't + // want these ports to get serviced. Also, we don't want them left + // in there if this function returns. + + + __CFPortSetRemove(dispatchPort, waitSet); + + + __CFRunLoopSetIgnoreWakeUps(rl); + + + // user callouts now OK again + __CFRunLoopUnsetSleeping(rl); + + + // 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了 + if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); + // 处理消息 + handle_msg:; + __CFRunLoopSetIgnoreWakeUps(rl); + + + #if DEPLOYMENT_TARGET_WINDOWS + if (windowsMessageReceived) { + // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + + if (rlm->_msgPump) { + rlm->_msgPump(); + } else { + MSG msg; + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + sourceHandledThisLoop = true; + + + // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced + // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later. + // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling. + __CFRunLoopSetSleeping(rl); + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + + __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL); + + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + __CFRunLoopUnsetSleeping(rl); + // If we have a new live port then it will be handled below as normal + } + + + + + #endif + if (MACH_PORT_NULL == livePort) { + CFRUNLOOP_WAKEUP_FOR_NOTHING(); + // handle nothing + } else if (livePort == rl->_wakeUpPort) { + CFRUNLOOP_WAKEUP_FOR_WAKEUP(); + // do nothing on Mac OS + #if DEPLOYMENT_TARGET_WINDOWS + // Always reset the wake up port, or risk spinning forever + ResetEvent(rl->_wakeUpPort); + #endif + } + #if USE_DISPATCH_SOURCE_FOR_TIMERS + else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { + CFRUNLOOP_WAKEUP_FOR_TIMER(); + + + if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { + // Re-arm the next timer, because we apparently fired early + __CFArmNextTimerInMode(rlm, rl); + } + } + #endif + #if USE_MK_TIMER_TOO + // 9.1 如果一个 Timer 到时间了,触发这个Timer的回调 + else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { + CFRUNLOOP_WAKEUP_FOR_TIMER(); + // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled. + // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754 + if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { + // Re-arm the next timer + __CFArmNextTimerInMode(rlm, rl); + } + } + #endif + // 9.2 如果有dispatch到main_queue的block,执行block + else if (livePort == dispatchPort) { + CFRUNLOOP_WAKEUP_FOR_DISPATCH(); + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL); + #if DEPLOYMENT_TARGET_WINDOWS + void *msg = 0; + #endif + /**/ + __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); + _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + sourceHandledThisLoop = true; + didDispatchPortLastTime = true; + } + // 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件 + else { + CFRUNLOOP_WAKEUP_FOR_SOURCE(); + + + // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again. + voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release); + + + /**/ + CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); + if (rls) { + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + mach_msg_header_t *reply = NULL; + sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; + if (NULL != reply) { + (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); + CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); + } + #elif DEPLOYMENT_TARGET_WINDOWS + sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop; + #endif + } + + + // Restore the previous voucher + _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release); + + + } + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); + #endif + // 执行加入到Loop的block + __CFRunLoopDoBlocks(rl, rlm); + + + + + if (sourceHandledThisLoop && stopAfterHandle) { + // 进入loop时参数说处理完事件就返回 + retVal = kCFRunLoopRunHandledSource; + } else if (timeout_context->termTSR < mach_absolute_time()) { + // 超出传入参数标记的超时时间了 + retVal = kCFRunLoopRunTimedOut; + } else if (__CFRunLoopIsStopped(rl)) { + __CFRunLoopUnsetStopped(rl); + // 被外部调用者强制停止了 + retVal = kCFRunLoopRunStopped; + } else if (rlm->_stopped) { + rlm->_stopped = false; + retVal = kCFRunLoopRunStopped; + } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { + // source/timer一个都没有 + retVal = kCFRunLoopRunFinished; + } + + + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + voucher_mach_msg_revert(voucherState); + os_release(voucherCopy); + #endif + // 如果没超时,mode里没空,loop也没被停止,那继续loop + } while (0 == retVal); + + + if (timeout_timer) { + dispatch_source_cancel(timeout_timer); + dispatch_release(timeout_timer); + } else { + free(timeout_context); + } + + return retVal; + } +``` + diff --git a/第一部分 iOS/1.41.md b/第一部分 iOS/1.41.md new file mode 100644 index 0000000..b4e3d0b --- /dev/null +++ b/第一部分 iOS/1.41.md @@ -0,0 +1,771 @@ +## 监听 RunLoop + + //给 RunLoop 添加监听者 + - (void)testRunLoopObserver{ + + //创建监听者 + // CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>) + /* + 创建监听对象 + 参数1:分配内存空间 + 参数2:要监听的状态 kCFRunLoopAllActivities :所有状态 + 参数3:是否要持续监听 + 参数4:优先级 + 参数5:回调 + */ + CFRunLoopObserverRef oberver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + switch (activity) { + case kCFRunLoopEntry: + NSLog(@"RunLoop 闪亮登场"); + break; + case kCFRunLoopBeforeTimers: + NSLog(@"RunLoop 大哥要处理 Timer 了"); + break; + case kCFRunLoopBeforeSources: + //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息 + NSLog(@"RunLoop 大哥要处理 Source 了"); + break; + case kCFRunLoopBeforeWaiting: + NSLog(@"RunLoop 大哥没事干要睡觉了"); + break; + case kCFRunLoopAfterWaiting: + NSLog(@""); + NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了"); + break; + case kCFRunLoopExit: + NSLog(@"RunLoop 大哥要退出离开了"); + break; + default: + break; + } + }); + /* + 参数1:要监听哪个RunLoop + 参数2:监听者 + 参数3:要监听 RunLoop 在哪种运行模式下的状态 + */ + CFRunLoopAddObserver(CFRunLoopGetCurrent(), oberver, kCFRunLoopDefaultMode); + //CoreFoundation 的内存管理:凡是带有 Create、Copy、Retain等字眼的函数创建出来的对象都需要在最后调用 release + CFRelease(oberver); + [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(wakeupRunLoop) userInfo:nil repeats:YES]; + } + + + //等到 RunLoop 休眠后,5秒钟叫醒 RunLoop + - (void)wakeupRunLoop{ + NSLog(@"%s",__func__); + } + /* + 2018-08-01 11:23:49.401626+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:49.401950+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:49.402326+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:49.402509+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:49.402721+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:49.402855+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:49.403080+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 + 2018-08-01 11:23:49.459238+0800 RunLoop[38148:1994974] + 2018-08-01 11:23:49.459512+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 + 2018-08-01 11:23:49.459740+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:49.459932+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:49.460431+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:49.460607+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:49.460775+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 + 2018-08-01 11:23:49.880631+0800 RunLoop[38148:1994974] + 2018-08-01 11:23:49.880867+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 + 2018-08-01 11:23:49.881530+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:49.881699+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:49.881870+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 + 2018-08-01 11:23:54.402263+0800 RunLoop[38148:1994974] + 2018-08-01 11:23:54.402562+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 + 2018-08-01 11:23:54.402773+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop] + 2018-08-01 11:23:54.403081+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:54.403245+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:54.403476+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 + 2018-08-01 11:23:59.402151+0800 RunLoop[38148:1994974] + 2018-08-01 11:23:59.402511+0800 RunLoop[38148:1994974] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 + 2018-08-01 11:23:59.402687+0800 RunLoop[38148:1994974] -[ViewController wakeupRunLoop] + 2018-08-01 11:23:59.402913+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Timer 了 + 2018-08-01 11:23:59.403037+0800 RunLoop[38148:1994974] RunLoop 大哥要处理 Source 了 + 2018-08-01 11:23:59.403156+0800 RunLoop[38148:1994974] RunLoop 大哥没事干要睡觉了 + */ + + +![RunLoop 运行原理图1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180801-104553@2x.png) + + + +上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟 + + + +```objective-c +- (void)testRunLoopObserverOnSubThread{ + + //创建并发队列 + dispatch_queue_t queue = dispatch_queue_create("com.lbp.testRunLoopOnSubThread", DISPATCH_QUEUE_CONCURRENT); + //开启子线程 + dispatch_async(queue, ^{ + + //1、获得当前线程下的 RunLoop + CFRunLoopRef runloop = CFRunLoopGetCurrent(); + + + //2、为 RunLoop 创建观察者 + CFRunLoopObserverRef obersver = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + switch (activity) { + case kCFRunLoopEntry: + NSLog(@"RunLoop 闪亮登场"); + break; + case kCFRunLoopBeforeTimers: + NSLog(@"RunLoop 大哥要处理 Timer 了"); + break; + case kCFRunLoopBeforeSources: + //Source 有2种。Source0:非基于 port 的,用户主动触发的事件。Source1:基于 port,通过内核和其它线程互相发送消息 + NSLog(@"RunLoop 大哥要处理 Source 了"); + break; + case kCFRunLoopBeforeWaiting: + NSLog(@"RunLoop 大哥没事干要睡觉了"); + break; + case kCFRunLoopAfterWaiting: + NSLog(@""); + NSLog(@"RunLoop 大哥终于等到有缘人了,要醒来开始干活了"); + break; + case kCFRunLoopExit: + NSLog(@"RunLoop 大哥要退出离开了"); + break; + default: + break; + } + }); + //为了运行 RunLoop 必须触发事件 + [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(wakeUpRunLoopOnSubThread) userInfo:nil repeats:NO]; + //3、为当前的 RunLoop 添加观察者 + CFRunLoopAddObserver(runloop, obersver, kCFRunLoopDefaultMode); + //4、在 CoreFoundation 框架中, create、copy、retain 过的对象都必须在最后 release + CFRelease(obersver); + //5、在非主线程创建的 RunLoop 必须触发运行 + [[NSRunLoop currentRunLoop] run]; + }); +} + + +- (void)wakeUpRunLoopOnSubThread{ + NSLog(@"%s",__func__); +} +/* +2018-08-01 14:23:06.453282+0800 RunLoop[2376:115968] RunLoop 闪亮登场 +2018-08-01 14:23:06.453608+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Timer 了 +2018-08-01 14:23:06.453781+0800 RunLoop[2376:115968] RunLoop 大哥要处理 Source 了 +2018-08-01 14:23:06.453982+0800 RunLoop[2376:115968] RunLoop 大哥没事干要睡觉了 +2018-08-01 14:23:08.458237+0800 RunLoop[2376:115968] +2018-08-01 14:23:08.458658+0800 RunLoop[2376:115968] RunLoop 大哥终于等到有缘人了,要醒来开始干活了 +2018-08-01 14:23:08.458894+0800 RunLoop[2376:115968] -[ViewController wakeUpRunLoopOnSubThread] +2018-08-01 14:23:08.459082+0800 RunLoop[2376:115968] RunLoop 大哥要退出离开了 +*/ +``` + + + + + +## RunLoop 内部运行原理 + +![RunLoop 运行原理图1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/image-20180801113342611.png) + +- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。 + - Source0:非基于 port 的,用户主动触发的事件。 + - Source1:基于 port,通过内核和其它线程互相发送消息 +- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop) + + +运行流程图 + +![运行流程](/assets/4.png) + +运行流程说明 + +![流程说明](/assets/3.png) + +## RunLoopMode 的概念 +![RunLoop 运行原理图1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/1785352-087fd4b664e0e387.png) + + + +## 底层实现 +内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer) + +我们来看看苹果官方开源的 [CFRunLoop.c 文件](https://legacy.gitbook.com/book/fantasticlbp/knowledge-kit/edit#)。看几个关键函数的实现猜测下 RunLoop 的内部原理 + +以下的代码都有注释说明 + +**__CFRunLoopModeIsEmpty** + + 此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop + + + +```objective-c +// expects rl and rlm locked + static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) { + CHECK_FOR_FORK(); + if (NULL == rlm) return true; + #if DEPLOYMENT_TARGET_WINDOWS + if (0 != rlm->_msgQMask) return false; + #endif + Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); + if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue + // 判断时候有没有_sources0 + if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; + // 判断时候有没有_sources1 + if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; + // 判断时候有没有_timers + if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; + + + struct _block_item *item = rl->_blocks_head; + while (item) { + struct _block_item *curr = item; + item = item->_next; + Boolean doit = false; + if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { + doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } else { + doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name)); + } + if (doit) return false; + } + return true; + } +``` + + + +**CFRunLoopRun、CFRunLoopRunInMode** + + 1、2个函数的作用分别是让 RunLoop 跑在 KCFRunLoopDefaultMode 下和特定的 Mode 下 + + 2、2个函数本质上都是调用 CFRunLoopRunSpecific + +```objective-c + // 用DefaultMode启动 + void CFRunLoopRun(void) { /* DOES CALLOUT */ + int32_t result; + do { + result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); + CHECK_FOR_FORK(); + } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); + } + + + // 用指定的Mode启动,允许设置RunLoop超时时间 + SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); + } +``` + + + +**CFRunLoopRunSpecific** + + 参数1: RunLoop 对象。参数2:运行 Mode 名称。参数3:超时时间。参数4:主_CFRunLoopRun 会用到 + +```objective-c + // RunLoop的实现 + SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + if (__CFRunLoopIsDeallocating(rl)) + return kCFRunLoopRunFinished; + __CFRunLoopLock(rl); + + + // 根据modeName找到对应mode + CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); + + + // 判断mode里没有source/timer, 没有直接返回。 + if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { + Boolean did = false; + if (currentMode) + __CFRunLoopModeUnlock(currentMode); + __CFRunLoopUnlock(rl); + return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; + } + + + volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); + + + CFRunLoopModeRef previousMode = rl->_currentMode; + rl->_currentMode = currentMode; + int32_t result = kCFRunLoopRunFinished; + + + if (currentMode->_observerMask & kCFRunLoopEntry ) + // 1. 通知 Observers: RunLoop 即将进入 loop + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); + // 进入loop + result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); + + + if (currentMode->_observerMask & kCFRunLoopExit ) + // 10.通知 Observers: RunLoop 即将退出。 + __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); + + + __CFRunLoopModeUnlock(currentMode); + __CFRunLoopPopPerRunData(rl, previousPerRun); + rl->_currentMode = previousMode; + __CFRunLoopUnlock(rl); + return result; + } +``` + + + +**__CFRunLoopDoObserver** + + + + 调用 Observer 回调 + + 联想给 RunLoop 添加观察者,监听 RunLoop 状态。 + +```objective-c + static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + + + CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0; + if (cnt < 1) return; + + + /* Fire the observers */ + STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1); + CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef)); + CFIndex obs_cnt = 0; + //遍历 rlm-> _observers,将元素放到 collectedObservers 数组中 + for (CFIndex idx = 0; idx < cnt; idx++) { + CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx); + if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) { + collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); + } + } + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + for (CFIndex idx = 0; idx < obs_cnt; idx++) { + CFRunLoopObserverRef rlo = collectedObservers[idx]; + __CFRunLoopObserverLock(rlo); + if (__CFIsValid(rlo)) { + Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo); + __CFRunLoopObserverSetFiring(rlo); + __CFRunLoopObserverUnlock(rlo); + __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info); + if (doInvalidate) { + CFRunLoopObserverInvalidate(rlo); + } + __CFRunLoopObserverUnsetFiring(rlo); + } else { + __CFRunLoopObserverUnlock(rlo); + } + CFRelease(rlo); + } + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + + + if (collectedObservers != buffer) free(collectedObservers); + } +``` + + + +**__CFRunLoopRun** + +```objective-c + /* rl, rlm are locked on entrance and exit */ + static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { + + + uint64_t startTSR = mach_absolute_time(); + + + if (__CFRunLoopIsStopped(rl)) { + __CFRunLoopUnsetStopped(rl); + return kCFRunLoopRunStopped; + } else if (rlm->_stopped) { + rlm->_stopped = false; + return kCFRunLoopRunStopped; + } + + + mach_port_name_t dispatchPort = MACH_PORT_NULL; + Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); + if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); + + + #if USE_DISPATCH_SOURCE_FOR_TIMERS + mach_port_name_t modeQueuePort = MACH_PORT_NULL; + if (rlm->_queue) { + modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue); + if (!modeQueuePort) { + CRASH("Unable to get port for run loop mode queue (%d)", -1); + } + } + #endif + + + dispatch_source_t timeout_timer = NULL; + struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context)); + if (seconds <= 0.0) { // instant timeout + seconds = 0.0; + timeout_context->termTSR = 0ULL; + } else if (seconds <= TIMER_INTERVAL_LIMIT) { + dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground(); + timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_retain(timeout_timer); + timeout_context->ds = timeout_timer; + timeout_context->rl = (CFRunLoopRef)CFRetain(rl); + timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds); + dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context + dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout); + dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel); + uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL); + dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL); + dispatch_resume(timeout_timer); + } else { + // 设置RunLoop超时时间 + seconds = 9999999999.0; + timeout_context->termTSR = UINT64_MAX; + } + + + Boolean didDispatchPortLastTime = true; + int32_t retVal = 0; + do { + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED; + voucher_t voucherCopy = NULL; + #endif + uint8_t msg_buffer[3 * 1024]; + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + mach_msg_header_t *msg = NULL; + mach_port_t livePort = MACH_PORT_NULL; + #elif DEPLOYMENT_TARGET_WINDOWS + HANDLE livePort = NULL; + Boolean windowsMessageReceived = false; + #endif + __CFPortSet waitSet = rlm->_portSet; + + + __CFRunLoopUnsetIgnoreWakeUps(rl); + + + if (rlm->_observerMask & kCFRunLoopBeforeTimers) + // 2. 通知 Observers: RunLoop 即将触发 Timer 回调 + __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); + if (rlm->_observerMask & kCFRunLoopBeforeSources) + // 3. 通知 Observers: RunLoop 即将触发 Source 回调 + __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); + // 执行被加入的block + __CFRunLoopDoBlocks(rl, rlm); + + + // 4. RunLoop 触发 Source0 (非port) 回调 + Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); + if (sourceHandledThisLoop) { + // 执行被加入的block + __CFRunLoopDoBlocks(rl, rlm); + } + + + Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); + + + // 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息 + if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + msg = (mach_msg_header_t *)msg_buffer; + + + if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { + goto handle_msg; + } + #elif DEPLOYMENT_TARGET_WINDOWS + if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { + goto handle_msg; + } + #endif + } + + + didDispatchPortLastTime = false; + + + // 通知 Observers: RunLoop 的线程即将进入休眠(sleep) + if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); + __CFRunLoopSetSleeping(rl); + // do not do any user callouts after this point (after notifying of sleeping) + + + // Must push the local-to-this-activation ports in on every loop + // iteration, as this mode could be run re-entrantly and we don't + // want these ports to get serviced. + + + __CFPortSetInsert(dispatchPort, waitSet); + + + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + + CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent(); + + + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + #if USE_DISPATCH_SOURCE_FOR_TIMERS + do { + if (kCFUseCollectableAllocator) { + // objc_clear_stack(0); + // + memset(msg_buffer, 0, sizeof(msg_buffer)); + } + msg = (mach_msg_header_t *)msg_buffer; + + + __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); + + + if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { + // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer. + while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue)); + if (rlm->_timerFired) { + // Leave livePort as the queue port, and service timers below + rlm->_timerFired = false; + break; + } else { + if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); + } + } else { + // Go ahead and leave the inner loop. + break; + } + } while (1); + #else + if (kCFUseCollectableAllocator) { + // objc_clear_stack(0); + // + memset(msg_buffer, 0, sizeof(msg_buffer)); + } + msg = (mach_msg_header_t *)msg_buffer; + __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); + #endif + + + + + #elif DEPLOYMENT_TARGET_WINDOWS + // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages. + __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived); + #endif + + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + + + rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart)); + + + // Must remove the local-to-this-activation ports in on every loop + // iteration, as this mode could be run re-entrantly and we don't + // want these ports to get serviced. Also, we don't want them left + // in there if this function returns. + + + __CFPortSetRemove(dispatchPort, waitSet); + + + __CFRunLoopSetIgnoreWakeUps(rl); + + + // user callouts now OK again + __CFRunLoopUnsetSleeping(rl); + + + // 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了 + if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); + // 处理消息 + handle_msg:; + __CFRunLoopSetIgnoreWakeUps(rl); + + + #if DEPLOYMENT_TARGET_WINDOWS + if (windowsMessageReceived) { + // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + + if (rlm->_msgPump) { + rlm->_msgPump(); + } else { + MSG msg; + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + sourceHandledThisLoop = true; + + + // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced + // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later. + // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling. + __CFRunLoopSetSleeping(rl); + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + + + __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL); + + + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + __CFRunLoopUnsetSleeping(rl); + // If we have a new live port then it will be handled below as normal + } + + + + + #endif + if (MACH_PORT_NULL == livePort) { + CFRUNLOOP_WAKEUP_FOR_NOTHING(); + // handle nothing + } else if (livePort == rl->_wakeUpPort) { + CFRUNLOOP_WAKEUP_FOR_WAKEUP(); + // do nothing on Mac OS + #if DEPLOYMENT_TARGET_WINDOWS + // Always reset the wake up port, or risk spinning forever + ResetEvent(rl->_wakeUpPort); + #endif + } + #if USE_DISPATCH_SOURCE_FOR_TIMERS + else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { + CFRUNLOOP_WAKEUP_FOR_TIMER(); + + + if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { + // Re-arm the next timer, because we apparently fired early + __CFArmNextTimerInMode(rlm, rl); + } + } + #endif + #if USE_MK_TIMER_TOO + // 9.1 如果一个 Timer 到时间了,触发这个Timer的回调 + else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { + CFRUNLOOP_WAKEUP_FOR_TIMER(); + // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled. + // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754 + if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { + // Re-arm the next timer + __CFArmNextTimerInMode(rlm, rl); + } + } + #endif + // 9.2 如果有dispatch到main_queue的block,执行block + else if (livePort == dispatchPort) { + CFRUNLOOP_WAKEUP_FOR_DISPATCH(); + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); + _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL); + #if DEPLOYMENT_TARGET_WINDOWS + void *msg = 0; + #endif + /**/ + __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); + _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); + __CFRunLoopLock(rl); + __CFRunLoopModeLock(rlm); + sourceHandledThisLoop = true; + didDispatchPortLastTime = true; + } + // 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件 + else { + CFRUNLOOP_WAKEUP_FOR_SOURCE(); + + + // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again. + voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release); + + + /**/ + CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); + if (rls) { + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + mach_msg_header_t *reply = NULL; + sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; + if (NULL != reply) { + (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); + CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply); + } + #elif DEPLOYMENT_TARGET_WINDOWS + sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop; + #endif + } + + + // Restore the previous voucher + _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release); + + + } + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg); + #endif + // 执行加入到Loop的block + __CFRunLoopDoBlocks(rl, rlm); + + if (sourceHandledThisLoop && stopAfterHandle) { + // 进入loop时参数说处理完事件就返回 + retVal = kCFRunLoopRunHandledSource; + } else if (timeout_context->termTSR < mach_absolute_time()) { + // 超出传入参数标记的超时时间了 + retVal = kCFRunLoopRunTimedOut; + } else if (__CFRunLoopIsStopped(rl)) { + __CFRunLoopUnsetStopped(rl); + // 被外部调用者强制停止了 + retVal = kCFRunLoopRunStopped; + } else if (rlm->_stopped) { + rlm->_stopped = false; + retVal = kCFRunLoopRunStopped; + } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { + // source/timer一个都没有 + retVal = kCFRunLoopRunFinished; + } + + + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + voucher_mach_msg_revert(voucherState); + os_release(voucherCopy); + #endif + // 如果没超时,mode里没空,loop也没被停止,那继续loop + } while (0 == retVal); + + + if (timeout_timer) { + dispatch_source_cancel(timeout_timer); + dispatch_release(timeout_timer); + } else { + free(timeout_context); + } + return retVal; + } +``` + diff --git a/第一部分 iOS/1.42.md b/第一部分 iOS/1.42.md new file mode 100644 index 0000000..d63a7c4 --- /dev/null +++ b/第一部分 iOS/1.42.md @@ -0,0 +1,143 @@ +# RunLoop 的应用 + +> 了解了 RunLoop 的底层原理以及特点后我们有必要想一想它可以应用在什么地方?现在归纳下常见的应用场景 + +## NSTimer + +我们常常写的 **NSTimer** 都是在的默认的运行状态下执行的(**NSDefaultRunLoopMode**)。所以我们会经常遇到 NSTimer 执行不准去的问题。因为 RunLoop 是有不同的运行状态的,当我们 UI 滚动的时候从 **NSDefaultRunLoopMode** 切换到 **UITrackingRunLoopMode**,所以添加到 **NSDefaultRunLoopMode** 状态下的事件是不会执行的,为了达到定时器准确的目的有2种方法。方法一:必须根据具体需求给 NSTimer 指定具体的 **CFRunLoopModeRef**。方法二:利用 GCD 的 timer 不会受 **NSDefaultRunLoopMode** 影响的特点。 + +```objective-c +//默认状态下的 NSTimer +[NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) { + NSLog(@"我在执行了"); +}]; +``` + +```objective-c +//方法1:给 NSTimer 指定运行状态 +NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(show) userInfo:nil repeats:YES]; +[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; +``` + +```objective-c + //方法2:GCD 的单位是纳秒。使用 GCD 创建的 timer 正常创建后不会执行,因为创建后设置了指定的时间后触发,所以当代码运行到最后一行的时候,Timer 还没执行,就被销毁了。所以我们必须设置一个属性去保存它。 + + //1、创建队列 + dispatch_queue_t queue = dispatch_get_main_queue(); + //2、创建 timer + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + // self.timer = timer; + //3、设置 timer 的参数:精准度、时间间隔 + //第三个参数为 GCD timer 的精准度 + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); + //4、为 Timer 设置任务 + dispatch_source_set_event_handler(timer, ^{ + NSLog(@"%@",[NSRunLoop currentRunLoop]); + }); + //5、执行任务 + dispatch_resume(timer); +``` + +## ImageView显示\(PerformSelector\) + +UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,**FPS** 达不到60。 + +利用 RunLoop 可以实现这个效果,就是给下载并显示图片的方法指定 **NSRunLoopMode**。 + +```objective-c +- (IBAction)clickLoadIMage:(id)sender { + //[self.imageview performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"test"] afterDelay:2]; + [self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]]; +} + +- (void)downloadAndShowImage{ + self.imageview.image = [UIImage imageNamed:@"test"]; +} +``` + +## 常驻线程 + +我们需要常驻线程,那么就需要线程不要销毁,那么一些做法比如设置任务死循环,那么线程就不会销毁;将当前线程强引用不要销毁等都存在问题。最好的方法是为当前线程设置合理的 RunLoop + +```objective-c +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + LBPThread *thred = [[LBPThread alloc] initWithTarget:self selector:@selector(showThreadLife) object:nil]; + self.lbpThread = thred; + [thred start]; +} + +- (void)showThreadLife{ + NSLog(@"---show----"); +} + +- (IBAction)clickLoadIMage:(id)sender { + NSLog(@"%s",__func__); + [self performSelector:@selector(contactWithTwoThread) onThread:self.lbpThread withObject:nil waitUntilDone:YES]; +} + +- (void)contactWithTwoThread{ + NSLog(@"----contactWithTwoThread----"); +} +``` + +![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180806-101917@2x.png) + +我们都知道 RunLoop 存在必须要有一个 Timer 或者 Source。 + +```objective-c +//方法1:添加 Port 的 Source。Sourcr1 +- (void)showThreadLife{ + NSLog(@"---show----"); + //子线程的 RunLoop 是需要自己手动创建并添加;RunLoop 如果不要销毁那么至少存在一个 Timer 或 Source + [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; + [[NSRunLoop currentRunLoop] run]; +} +//方法2 +- (void)showThreadLife{ + NSLog(@"---show----"); + //子线程的 RunLoop 是需要自己手动创建并添加;RunLoop 如果不要销毁那么至少存在一个 Timer 或 Source + + NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(test) userInfo:nil repeats:YES]; + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; + [[NSRunLoop currentRunLoop] run]; +} +``` + +注意:添加 Observer 是没有效果的。 + +## 自动释放池 + +自动释放池什么时候创建和释放 + +创建时间:第一次进入 RunLoop 的时候 + +释放时间:RunLoop 退出的时候 + +其他情况:当 RunLoop 将要休眠的时候释放,然后创建一个新的 + +**\_wrapRunLoopWithAutoreleasePoolHandler** **0x1** + +**\_wrapRunLoopWithAutoreleasePoolHandler** **0xa0** + +0x1 和 0xa0 是十六进制的数,对应十进制为1和160。 + + + + + +参考文章: + +http://www.cocoachina.com/ios/20180515/23380.html + +http://www.cocoachina.com/ios/20170417/19075.html + +https://www.jianshu.com/p/4c38d16a29f1 + +https://www.cnblogs.com/kenshincui/p/6823841.html + +https://juejin.im/entry/599c13bc6fb9a0248926a77d + +https://blog.ibireme.com/2015/05/18/runloop/ + + + diff --git a/第一部分 iOS/1.43.md b/第一部分 iOS/1.43.md new file mode 100644 index 0000000..1a820ae --- /dev/null +++ b/第一部分 iOS/1.43.md @@ -0,0 +1,154 @@ +# iOS应用启动性能优化资料汇总 + +[WWDC](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#wwdc) + +[文章](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E6%96%87%E7%AB%A0) + +[工具](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E5%B7%A5%E5%85%B7) + +[代码](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E4%BB%A3%E7%A0%81) + +[偏门古董](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E5%81%8F%E9%97%A8%E5%8F%A4%E8%91%A3) + +[书籍](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E4%B9%A6%E7%B1%8D) + +[总结](https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/#%E6%80%BB%E7%BB%93) + +发现好资料就整理到这里,_随时更新,最后一次更新2018年8月6日_ + +# WWDC {#wwdc} + +1. Optimizing App Startup Time + + 必看官方资料,从底层到上层[https://developer.apple.com/videos/play/wwdc2016/406/](https://developer.apple.com/videos/play/wwdc2016/406/) + +2. App Startup Time: Past, Present, and Future + + dyld层面的优化[https://developer.apple.com/videos/play/wwdc2017/413/](https://developer.apple.com/videos/play/wwdc2017/413/) + +3. Optimizing I/O for Performance and Battery Life + + IO是启动性能的重要影响部分[https://developer.apple.com/videos/play/wwdc2016/719/](https://developer.apple.com/videos/play/wwdc2016/719/) + +4. Practical Approaches to Great App Performance + + 现场一步一步解决性能问题[https://developer.apple.com/videos/play/wwdc2018/407/](https://developer.apple.com/videos/play/wwdc2018/407/) + +5. Using Time Profiler in Instruments + + TimeProfiler是必备好帮手[https://developer.apple.com/videos/play/wwdc2016/418/](https://developer.apple.com/videos/play/wwdc2016/418/) + +6. High Performance Auto Layout + + App首页如果是AutoLayout的,那么以后看来不是问题了[https://developer.apple.com/videos/play/wwdc2018/220/](https://developer.apple.com/videos/play/wwdc2018/220/) + +7. Core Image: Performance, Prototyping, and Python + + 首页当然也有大量的图片,了解Core Image[https://developer.apple.com/videos/play/wwdc2018/719/](https://developer.apple.com/videos/play/wwdc2018/719/) + +# 文章 {#文章} + +**以下文章仅仅是收集,各家之谈,不要全信,也不要反对,各有道理,学习思路即可。** + +1. 即刻技术团队:iOS app 启动速度研究实践 + + 地址[https://zhuanlan.zhihu.com/p/38183046?from=1086193010&wm=3333\_2001&weiboauthoruid=1690182120](https://zhuanlan.zhihu.com/p/38183046?from=1086193010&wm=3333_2001&weiboauthoruid=1690182120)学习思路。 + +2. iOS Dynamic Framework 对App启动时间影响实测 + + [https://www.jianshu.com/p/3263009e9228](https://www.jianshu.com/p/3263009e9228)动态库的测试。可知:启动过程中尽量不要加载动态库了。 + +3. Optimizing Facebook for iOS start time + + [https://code.fb.com/ios/optimizing-facebook-for-ios-start-time/](https://code.fb.com/ios/optimizing-facebook-for-ios-start-time/)Facebook的思路。虽然Facebook的启动很慢。 + +4. Bugly: iOS App 启动性能优化 + + [https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA](https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA)这篇文章最后透露了一个很给力的思路。强烈推荐仔细看文章最后。 + +5. 今日头条iOS客户端启动速度优化 + + [https://techblog.toutiao.com/2017/01/17/iosspeed/](https://techblog.toutiao.com/2017/01/17/iosspeed/)文章开头的信息很多,但减少代码量,貌似很难行得通。 + +6. 如何精确度量 iOS App 的启动时间 + + [https://www.jianshu.com/p/c14987eee107](https://www.jianshu.com/p/c14987eee107)文章的思路可参考。 + +7. 优化 App 的启动时间 + + [http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/](http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/)主要是对WWDC的笔记,但仍然很给力。南萧玉,北子棋。这篇文章就是南萧玉所作。 + +8. 手淘iOS性能优化探索 + + [https://github.com/izhangxb/GMTC/blob/master/%E5%85%A8%E7%90%83%E7%A7%BB%E5%8A%A8%E6%8A%80%E6%9C%AF%E5%A4%A7%E4%BC%9AGMTC%202017%20PPT/%E6%89%8B%E6%B7%98iOS%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8E%A2%E7%B4%A2%20.pdf](https://github.com/izhangxb/GMTC/blob/master/%E5%85%A8%E7%90%83%E7%A7%BB%E5%8A%A8%E6%8A%80%E6%9C%AF%E5%A4%A7%E4%BC%9AGMTC%202017%20PPT/%E6%89%8B%E6%B7%98iOS%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E6%8E%A2%E7%B4%A2%20.pdf)这是GMTC 2017手机淘宝专家的技术分享,可以参考。 + +9. iOS应用启动性能优化\(1\)-premain + + [https://everettjf.github.io/2018/05/26/ios-app-launch-performance-part1/](https://everettjf.github.io/2018/05/26/ios-app-launch-performance-part1/)仅仅是pre-main阶段的思路。作者说有后续的文章,但很久没动静了,不知道在搞什么。 + +10. 一种 hook objective c +load 的方法 + + [https://everettjf.github.io/2017/01/06/a-method-of-hook-objective-c-load/](https://everettjf.github.io/2017/01/06/a-method-of-hook-objective-c-load/)这篇文章的hook比较麻烦,其实还可以参考上面的一篇文章[https://www.jianshu.com/p/c14987eee107](https://www.jianshu.com/p/c14987eee107),这里有批量hook +load的代码。(未来我也有计划会把这些相关代码整理到一个repo中) + +11. 一种 hook C++ static initializers 的方法 + + [https://everettjf.github.io/2017/02/06/a-method-of-hook-static-initializers/](https://everettjf.github.io/2017/02/06/a-method-of-hook-static-initializers/)这篇文章的hook方法,有较大的可能是我首创,强烈推荐。手淘的分享中也提了这个方法。 + +12. 一种延迟 premain code 的方法 + + [https://everettjf.github.io/2017/03/06/a-method-of-delay-premain-code/](https://everettjf.github.io/2017/03/06/a-method-of-delay-premain-code/)通过学习Facebook的App中特有的section(参考文章[https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable/](https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable/)),发现的一种思路。 + +# 工具 {#工具} + +1. TimeProfiler + + 都知道是啥。 + +2. AppleTrace + + [https://github.com/everettjf/AppleTrace](https://github.com/everettjf/AppleTrace)使用 HookZz hook了objc\_msgSend,会有较大性能损耗,但可根据相对比例来知道大概的耗时占比。另外也手动定义开始结尾生成chrome tracing。 + +3. DTrace + + 只能用于模拟器。使用方法可参考这本书:Advanced Apple Debugging & Reverse Engineering[https://store.raywenderlich.com/products/advanced-apple-debugging-and-reverse-engineering](https://store.raywenderlich.com/products/advanced-apple-debugging-and-reverse-engineering) + +4. Xcode 环境变量 + + DYLD\_PRINT\_STATISTIC 及其他类似环境变量[https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/LoggingDynamicLoaderEvents.html](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/LoggingDynamicLoaderEvents.html) + +# 代码 {#代码} + +1. FastImageCache + + [https://github.com/path/FastImageCache](https://github.com/path/FastImageCache)优化图片加载的速度。空间换时间。 + +# 偏门古董 {#偏门古董} + +1. Code Size Performance Guidelines + + [https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html)页面最下面提出的思路很好,但文章是gcc时代的了。有没有clang时代对应的呢。 Improving Locality of Reference[https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/ImprovingLocality.html\#//apple\_ref/doc/uid/20001862-CJBJFIDD](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/ImprovingLocality.html#//apple_ref/doc/uid/20001862-CJBJFIDD) + +# 书籍 {#书籍} + +1. Pro iOS Apps Performance Optimization + + 貌似比较古老,仅参考。 + +2. iOS and macOS Performance Tuning + + 很细致,我正在看。有中文翻译版。 + +3. High Performance iOS Apps + + 有中文翻译版。 + +# 总结 {#总结} + +上面的文章我都看过,或者至少是正在看,总结下来,辅助大家优化启动性能。 + + + +--- + +转载于:https://everettjf.github.io/2018/08/06/ios-launch-performance-collection/ + diff --git a/第一部分 iOS/1.44.md b/第一部分 iOS/1.44.md new file mode 100644 index 0000000..0d07412 --- /dev/null +++ b/第一部分 iOS/1.44.md @@ -0,0 +1,84 @@ +# App 数据安全篇 + +> 之前的研究了 web 站点的数据安全,同时也用[文章](https://github.com/FantasticLBP/Anti-WebSpider)记录下来分享给大家。接着又研究了下 App 的安全,同样写文章记录下来 + + + +## 现状 + +目前 App 的安全比较低,体现在哪?很多人在想用了 HTTPS 不是就很安全吗?其实并不是,专业的抓包工具还是可以抓 HTTPS 包。根据接口规律,做自动化请求接口,将数据保存窃取是我们不想看到的结果。所以如果只用了 HTTPS 还是不安全。 + +所以需要实现的安全表现在:1. App 数据防止抓包;2. 防止中间人攻击;3. 下下策。即使抓包成功拿到的数据也是密文。如果想解密,是不可逆的。 + + + +## 解决方案 + +1. App 数据防止抓包 + + 原理:抓包工具工作原理见[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/第四部分%20开发杂谈/4.10.md) + + ![App-Server](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/App-Server.png) + + **验证证书的真伪**其实一般来说这个过程应该是安全的,因为一般的证书都是由操作系统来管理。所以只要操作系统没有证书链验证等方面的 bug 是没有什么问题的,但是为了抓包其实我们是在操作系统中导入了中间人的 CA,这样中间人下发的公钥证书就可以被认为是合法的,可以通过验证的(中间人既承担了颁发证书,又承担了验证证书,通过验证)。 + + + + 措施: 客户端为了解决这个问题,最好的方式其实就是内嵌证书,比对一下这个证书到底是不是自己真正的“服务端”发来的,而不是中间被替换了。 + + - 跟服务端人员拿到 https 证书,导入 Xcode 工程项目中 + + - AFNetworking设置以下代码 + + ```objective-c + AFSecurityPolicy * policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate]; + manager.securityPolicy = policy; + ``` + + AF的安全策略会自动的在bundle里面查找公钥证书,建立https的时候进行比对。不一样直接就失败了。 + + AFNetworking 的 AFSSLPinningMode 的三个级别 + +   AFSSLPinningModeNone: (默认级别),客户端无条件信任任何下发的公钥证书 + +   AFSSLPinningModePublicKey: 客户端本地去验证服务端下发的公钥证书的 public keys部分。如果正确才通过 + +   AFSSLPinningModeCertificate: 客户端本地去验证服务端下发的公钥证书的所有部分。如果正确才通过 + + 这样做了之后,就可以即使手机上安装了抓包工具的CA,抓包工具也不能抓到包了。因为你的客户端在验证“服务端”下发的公钥证书的真伪的时候就不会通过“中间人”下发的公钥证书,也就不会建立起来https的连接了。 + +2. 防止中间人攻击 + + 即使我们的 App 被大神逆向了(iOS + 网络精通),抓到网络请求,然后原封不动去向服务器发起请求,但是服务端做了防重放,也是很安全的。所以防重放机制是 Server 端的安全措施 + + [防重放](https://www.cnblogs.com/yjf512/p/6590890.html) + +3. 密文,反向解密不可逆 + + 采用 RSA 非对称加密算法。 + + - iOS 端和 Server 端各生成自己的公钥和私钥 + + 使用 **openssl** 生成所需秘钥 + + - iOS 端生成的公钥和私钥定义为 **iOSPublicKey、iOSPrivateKey**,Server 端生成的公钥私钥定义为**ServerPublicKey、ServerPrivateKey**。将 **iOSPublicKey ** 给 Server 端使用,让它用 **iOSPublicKey ** 加密数据传给 iOS 端,iOS 端用 **iOSPrivateKey** 解密;Server 端将 **ServerPublicKey** 给 iOS 端,iOS 端用 **ServerPublicKey** 加密数据后上传给 Server 端,Server 端利用 **ServerPrivateKey** 去解密,这样就实现了数据传输过程中的加密与解密 + + + + ## 资料 + + [RSA算法原理(一)](http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html) + + [RSA算法原理(二)](http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.htmll) + + [素数](https://zh.wikipedia.org/zh-cn/互質) + + [防重放](https://www.cnblogs.com/yjf512/p/6590890.html) + + + + + + + + diff --git a/第一部分 iOS/1.45.md b/第一部分 iOS/1.45.md new file mode 100644 index 0000000..393ba16 --- /dev/null +++ b/第一部分 iOS/1.45.md @@ -0,0 +1,11 @@ +# 调试小技巧 + +1. 在日常开发中我们经常会封装某个功能模块然后暴露某个方法给外部。但是很多时候调用我们封装功能的人可能会不按照约定的方法传递参数。所以我们会使用断言。但是在线上的时候如果使用了断言,那么程序肯定会 **Crash** ,Xcode 提供了一个小功能可以解决这个问题。 + + + + NS_BLOCK_ASSERTIONS: 表明在 Release 状态下过滤 NSAssert,只需要这一个条件就可以过滤掉 NSAssert。 + + 方法:在 “Build Settings” 下搜索 **Preprocessor Macros** ,然后在 Release 下面添加 NS_BLOCK_ASSERTIONS + +![](/Users/liubinpeng/Desktop/Github/knowledge-kit/assets/WX20180830-100631@2x.png) \ No newline at end of file diff --git a/第一部分 iOS/1.46.md b/第一部分 iOS/1.46.md new file mode 100644 index 0000000..c17490f --- /dev/null +++ b/第一部分 iOS/1.46.md @@ -0,0 +1,75 @@ +# Hybrid + + + +### Android 端如何与 JS 通信(2种方法) + +- webview.loadUrl() +- Webview.evaluateJavascript() + +> 2者区别: +> +> 1. loadUrl() 会刷新页面,evaluateJavascript() 则不会刷新页面,效率高 +> 2. loadUrl() 得不到 JS 的返回值;evaluateJavascrip() 则可以获取返回值 +> 3. evaluateJavascrip() 在 Android 4.4 之后才可以使用 + +注意:Android 可以直接调用 JS 的 alert() 方法是因为 alert 方法直接挂载在 window 对象上。但是 Native 与 JS 可能不止一个方法、多个方法多个属性去访问,这样都直接挂载在 window 对象上不是明智之举。因为后期维护很不方便。所以我们在 Native 和 JS 之间会设置一个桥接对象,像一个中间层一样,让2端互调。 + +Android 需要在页面加载完,也就是 webview 的 onPageFinished 方法中写调用逻辑,否则不会执行 + +```java +webView.loadUrl("javascript:callJsFunction('soloname')") +webView.evaluateJavascript("javascript:callJsFunction('soloname')" +``` + + + +### JS 如何与 Android 通信 + +- 通过 Webview 的 addJavascriptInterface() 进行对象映射 +- 通过 WebviewClient 的 shouldOverrideUrlLoading() 方法回调拦截 Url +- 通过 webChromeClient 的 onJsAlert()、onJSPrompt() 方法回调拦截 JS 对话框 alert()、confirm()、prompt() 等消息 + +第一种最简洁,但是在 Android 4.2 以下存在漏洞。 + +实验:Android webview 上跑一个网页,点击网页的按钮,让 Native 弹出一个字符串。 + +```vue +methods: { + showAndroidToast() { + $App.showToast("哈哈,我是js调用的") + } +} +``` + +``` +public class JsJavaBridge { + + private Activity activity; + private WebView webView; + + public JsJavaBridge(Activity activity, WebView webView) { + this.activity = activity; + this.webView = webView; + } + + @JavascriptInterface + public void onFinishActivity() { + activity.finish(); + } + + @JavascriptInterface + public void showToast(String msg) { + ToastUtils.show(msg); + } +} + +``` + +然后通过 webview 设置 Android 类与 JS 代码的映射 + +``` +webView.addJavascriptInterface(new JsJavaBridge(this, tbsWebView), "$App"); +``` + +这里将类 JsJavaBridge 在 JS 中映射为了 $App,所以在 Vue 中可以这样调用 `$App.showToast("哈哈,我是js调用的")`。 \ No newline at end of file diff --git a/第一部分 iOS/1.47.md b/第一部分 iOS/1.47.md new file mode 100644 index 0000000..31cd56f --- /dev/null +++ b/第一部分 iOS/1.47.md @@ -0,0 +1,171 @@ +# NSTimer 中的内存泄露 + +- GCD 的 timer +- NSProxy +- 采用 Block 的形式为 NSTimer 增加分类 + + + +```objective-c +@interface ViewController() +@property (nonatomic, strong) NSTimer *timer; +@end + +@implementation ViewController +- (void)viewDidLoad { + [super viewDidLoad]; + + self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 + target:self + selector:@selector(p_doSomeThing) + userInfo:nil + repeats:YES]; + +} + +- (void)p_doSomeThing { + // doSomeThing +} + +- (void)p_stopDoSomeThing { + [self.timer invalidate]; + self.timer = nil; +} + +- (void)dealloc { + [self.timer invalidate]; +} + +@end +``` + +上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。 + +能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时,NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。 + +当前的 VC 和 定时器互相引用,造成循环引用。 + +如何能在何时的时候打破循环引用就不会有问题了 + +1. 控制器不再强引用定时器 +2. 定时器不再保留当前的控制器 + +```objective-c +//.h文件 +#import + +@interface NSTimer (SGLUnRetain) ++ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval + repeats:(BOOL)repeats + block:(void(^)(NSTimer *timer))block; +@end + +//.m文件 +#import "NSTimer+SGLUnRetain.h" + +@implementation NSTimer (SGLUnRetain) + ++ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{ + + return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats]; +} + ++ (void)sgl_blcokInvoke:(NSTimer *)timer { + + void (^block)(NSTimer *timer) = timer.userInfo; + + if (block) { + block(timer); + } +} +@end + +//控制器.m + +#import "ViewController.h" +#import "NSTimer+SGLUnRetain.h" + +//定义了一个__weak的self_weak_变量 +#define weakifySelf \ +__weak __typeof(&*self)weakSelf = self; + +//局域定义了一个__strong的self指针指向self_weak +#define strongifySelf \ +__strong __typeof(&*weakSelf)self = weakSelf; + +@interface ViewController () + +@property(nonatomic, strong) NSTimer *timer; + +@end + +@implementation ViewController +- (void)viewDidLoad { + [super viewDidLoad]; + + __block NSInteger i = 0; + weakifySelf + self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) { + strongifySelf + [self p_doSomething]; + NSLog(@"----------------"); + if (i++ > 10) { + [timer invalidate]; + } + }]; +} + +- (void)p_doSomething { + +} + +- (void)dealloc { + // 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档) + [self.timer invalidate]; +} +@end +``` + +上面的方法之所以能解决内存泄漏的问题,关键在于把保留转移到了定时器的类对象身上,这样就避免了实例对象被保留。 + +当我们谈到循环引用时,其实是指实例对象间的引用关系。类对象在 App 杀死时才会释放,在实际开发中几乎不用关注类对象的内存管理。下面的代码摘自苹果开源的 NSObject.mm 文件,从中可以看出,对于类对象,并不需要像实例对象那样进行内存管理。 + +```objective-c ++ (id)retain { + return (id)self; +} + +// Replaced by ObjectAlloc +- (id)retain { + return ((id)self)->rootRetain(); +} + ++ (oneway void)release { +} + +// Replaced by ObjectAlloc +- (oneway void)release { + ((id)self)->rootRelease(); +} + ++ (id)autorelease { + return (id)self; +} + +// Replaced by ObjectAlloc +- (id)autorelease { + return ((id)self)->rootAutorelease(); +} + ++ (NSUInteger)retainCount { + return ULONG_MAX; +} + +- (NSUInteger)retainCount { + return ((id)self)->rootRetainCount(); +} +``` + + + +iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致 \ No newline at end of file diff --git a/第一部分 iOS/1.48.md b/第一部分 iOS/1.48.md new file mode 100644 index 0000000..0314054 --- /dev/null +++ b/第一部分 iOS/1.48.md @@ -0,0 +1,57 @@ +### KVC && KVO + +### 字典快速赋值 + +KVC 可以将字典里面和 model 同名的 property 进行快速赋值 **setValuesForKeysWithDictionary** + +```objective-c +//前提:model 中的各个 property 必须和 NSDictionary 中的属性一致 +- (instancetype)initWithDic:(NSDictionary *)dic{ + BannerModel *model = [BannerModel new]; + [model setValuesForKeysWithDictionary:dic]; + return model; +} +``` + +但是这里会有2种特殊情况。 + +情况一:在 model 里面有 property 但是在 NSDictionary 里面没有这个值 + +运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null + +情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值 + +运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分 + +``` +- (void)setValue:(id)value forUndefinedKey:(NSString *)key{ + +} +``` + +情况三:如果 Dictionary 和 Model 中的 property 不同名 + +我们照样可以利用 **setValue:forUndefinedKey:** 去处理 + +比如 + +```objective-c +//model +@property (nonatomic,copy)NSString *name; +@property (nonatomic,copy)NSString *sex; +@property (nonatomic,copy) NSString* age; +//NSDictionary +NSDictionary *dic = @{@"username":@"张三",@"sex":@"男",@"id":@"22"}; + +-(void)setValue:(id)value forUndefinedKey:(NSString *)key{ + if([key isEqualToString:@"id"]){ + self.age=value; + } + if([key isEqualToString:@"username"]){ + self.name=value; + } +} +``` + + + diff --git a/第一部分 iOS/1.49.md b/第一部分 iOS/1.49.md new file mode 100644 index 0000000..d50ec95 --- /dev/null +++ b/第一部分 iOS/1.49.md @@ -0,0 +1,128 @@ +# 金融 App 金额格式化 + +> 在一些金融类的 App 中,对于表示金额类的字符串,通常需要进行格式化后再显示出来。例如: +> +> 0 显示为:0.00 +> +> 123 显示为:123.00 +> +> 123.456 显示为:123.46 +> +> ​ 102000 显示为:102,000.00 +> +> ​ 10204500 显示为:10,204,500.00 +> +> ​ 它的规则为:个位数起每隔三位数字添加一个逗号,同时保留两位小数,也称为“千分位格式”。 + + + + + +### 方法一 + +​ 首先根据小数点 `.` 将传入的字符串分割为两部分,整数部分和小数部分(如果没有小数点,则补 `.00`,如果有多个小数点则报金额格式错误)。对于小数部分,只取前两位;然后对整数部分字符串进行遍历,从右到左,每三位数前插入一个逗号 `,`,最后再把两部分拼接起来,代码大致如图 1 和图 2 所示。 + + + +``` +- (void)method1{ + NSArray *temps = @[@"0",@"123",@"123.456",@"102000",@"10204500"]; + self.pricelabel.text = [self moneyFromat:temps[arc4random()%5]]; +} + +- (NSString *)moneyFromat:(NSString *)money{ + if (!money || money.length == 0) { + return money; + } + + BOOL hasPoint = NO; + if ([money rangeOfString:@"."].length > 0) { + hasPoint = YES; + } + + + NSMutableString *pointMoney = [NSMutableString stringWithString:money]; + if (hasPoint == NO) { + [pointMoney appendFormat:@".00"]; + } + + + NSArray *moneys = [pointMoney componentsSeparatedByString:@"."]; + if (moneys.count > 2) { + return pointMoney; + } + else if (moneys.count == 1) { + return [NSString stringWithFormat:@"%@.00",moneys[0]]; + } + else { + //整数部分:每隔3位插入一个“,” + NSString *frontMoney = [self stringFromToThreeBit:moneys[0]]; + if ([frontMoney isEqualToString:@""]) { + frontMoney = @"0"; + } + //拼接整数部分和消暑部分 + NSString *backMoney = moneys[1]; + if (backMoney.length == 1) { + return [NSString stringWithFormat:@"%@.%@",frontMoney,backMoney]; + } + else if (backMoney.length > 2) { + return [NSString stringWithFormat:@"%@.%@",frontMoney,[backMoney substringToIndex:2]]; + } + else { + return [NSString stringWithFormat:@"%@.%@",frontMoney,backMoney]; + } + } +} + +- (NSString *)stringFromToThreeBit:(NSString *)string{ + NSString *tempString = [string stringByReplacingOccurrencesOfString:@"," withString:@""]; + NSMutableString *mutableString = [NSMutableString stringWithString:tempString]; + NSInteger n = 2; + if (mutableString.length > 3) { + for (NSUInteger i = mutableString.length - 3; i > 0; i--) { + n++; + + if (n == 3) { + [mutableString insertString:@"," atIndex:i]; + n = 0; + } + } + } + return mutableString; +} +``` + + + +### 方法二 + +其实,苹果提供了 NSNumberFormatter 用来处理 NSString 和 NSNumber 之间的转化,可以满足基本的数字形式的格式化。我们通过设置 NSNumberFormatter 的 `numberStyle` 和 `positiveFormat` 属性,即可实现上述功能,非常简洁。 + +​ + +```objective-c +- (void)method2{ + NSArray *temps = @[@"0",@"123",@"123.456",@"102000",@"10204500"]; + self.pricelabel.text = [self formatDecimalNumber:temps[arc4random()%5]]; +} + +- (NSString *)formatDecimalNumber:(NSString *)string{ + if (!string || string.length == 0) { + return string; + } + NSNumber *number = @([string doubleValue]); + NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + formatter.numberStyle = kCFNumberFormatterDecimalStyle; + formatter.positiveFormat = @"###,##0.00"; + formatter.positiveFormat = @"###,##0.00"; + NSString *amountString = [formatter stringFromNumber:number]; + return amountString; +} +``` + + + +### [参考资料](https://www.jianshu.com/p/817029422a72) + + + diff --git a/第一部分 iOS/1.5.md b/第一部分 iOS/1.5.md new file mode 100644 index 0000000..66bf795 --- /dev/null +++ b/第一部分 iOS/1.5.md @@ -0,0 +1,68 @@ + +实验1: + +定义 BaseView,在里面实现方法touchBegan,监听当前哪个类调用了该方法。 + +在控制器的界面上加5个颜色不同的view,每个view自定义view去实现,因此在不同的view上的手势就可以由不同的view拦截到。 + + + +![UI效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Simulator%20Screen%20Shot%20-%20iPhone%206s%20Plus%20-%202017-10-11%20at%2010.14.37.png) + +``` +//BaseView +#import "BaseView.h" + +@implementation BaseView +-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ +NSLog(@"%@",[self class]); +} +``` + +结果:点击不同的View打印出不同的类名。 + +结论: + +* 触摸事件是从父控件传递到子控件的。 +* 点击了绿色(图上的2级)的view:UIApplication-> UIWindow -> UIViewController的view -> 绿色的view +* 点击了蓝色(图上的3级)的view:UIApplication-> UIWindow -> UIViewController的view -> 红棕色的view -> 蓝色的view +* 点击了黄色(图上的4级)的view:UIApplication -> UIWindow -> UIViewController的view -> 红棕色的view -> 蓝色的view -> 黄色的view + +注意:如果父控件不能接收触摸事件,那么这个父控件的子控件也不能接收触摸事件 + +#### 如何找到最合适的控件来接收触摸事件? + +* 自己能否接收触摸事件? +* 触摸点是否在自己身上? +* 从后往前遍历子控件,重复前面2个步骤 +* 如果没有符合条件的子控件,那么就自己最适合处理 + + +# 事件响应原理 + +产生的touch方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者处理。 + +#### 响应者链条 + +![响应者链条](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/响应者链条.png) + +#### 事件传递的完整过程 + +1. 先将事件对象由上往下传递(父控件传递给子控件),找到最合适的控件来处理 +2. 调用最合适控件的touch方法 +3. 如果调用了\[super touches...\]方法就会将事件顺着响应者链条向上传递,传递给上一个响应者 +4. 接着就会调用上一个响应者的touches...方法 + +#### 事件响应者 + +##### 如何判断该控件的上一个响应者? + +1. 如果当前这个view是控制器的view,那么上一个响应者就是控制器 +2. 如果当前这个view不是控制器的view,那么上一个响应者就是父控件。 + +事件传递给UIApplication后如果不处理的话,该事件会销毁掉。 + +控制器view上的子控件的touch...方法如果子控件不处理那么都会顺着响应者链条向上传递给上一层响应者对象,比如可以交给控制器处理。 + + + diff --git a/第一部分 iOS/1.6.md b/第一部分 iOS/1.6.md new file mode 100644 index 0000000..57500ed --- /dev/null +++ b/第一部分 iOS/1.6.md @@ -0,0 +1,148 @@ + + +### 双列表联动 + + +> 用过了那么多的外卖App,总结出一个规律,那就是“所有的外卖App都有双列表联动功能”。哈哈哈哈,这是一个玩笑。 +> +> 这次我也需要开发具有联动效果的双列表。也是首次开发这种类型的UI,记录下步骤与心得 + +#### 一、关键思路 + +* 懒加载左右2个UITableView +* 根据需要自定义Cell +* 2个UITableView加载到界面上的时候注意下部剧就好 +* 因为需要联动效果,所有左侧的UITableView一般是大的分类,右边的UITableView一般是大分类小的小分类,所以有了这样的特点 + * 左边的UITableView是只有1个section和n个row + * 右边的UITableView具有n个section(这里的section 个数恰好是左边UITableView的row数量),且每个section下的row由对应的数据源控制 + +#### 二、第一版代码 + +``` +#pragma mark -- UITableViewDelegate +-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ + if (tableView == self.leftTablview) { + return 1; + } + return self.datas.count; +} + +-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ + if (tableView == self.leftTablview) { + return self.datas.count; + } + QuestionCollectionModel *model = self.datas[section]; + NSArray *questions =model.questions; + return questions.count; +} + +-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ + if (tableView == self.leftTablview) { + return LeftCellHeight; + } + return RightCellHeight; +} + +-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ + if (tableView == self.leftTablview) { + PregnancyPeriodCell *cell = [tableView dequeueReusableCellWithIdentifier:PregnancyPeriodCellID forIndexPath:indexPath]; + if (self.collectionType == CollectionType_Wrong || self.collectionType == CollectionType_Miss) { + QuestionCollectionModel *model = self.datas[indexPath.row]; + cell.week = model.tag; + } + + return cell; + } + QuestionCell *cell = [tableView dequeueReusableCellWithIdentifier:QuestionCellID forIndexPath:indexPath]; + QuestionCollectionModel *model = self.datas[indexPath.section]; + NSArray *questions =model.questions; + QuestionModel *questionModel = questions[indexPath.row]; + cell.model = questionModel; + return cell; +} + + +-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ + if (tableView == self.leftTablview) { + NSIndexPath *indexpath = [NSIndexPath indexPathForRow:0 inSection:indexPath.row]; + [self.rightTableview scrollToRowAtIndexPath:indexpath atScrollPosition:UITableViewScrollPositionTop animated:YES]; + } +} + +-(void)scrollViewDidScroll:(UIScrollView *)scrollView{ + if (scrollView == self.rightTableview) { + NSIndexPath *indexpath = [self.rightTableview indexPathsForVisibleRows].firstObject; + NSIndexPath *leftScrollIndexpath = [NSIndexPath indexPathForRow:indexpath.section inSection:0]; + [self.leftTablview selectRowAtIndexPath:leftScrollIndexpath animated:YES scrollPosition:UITableViewScrollPositionMiddle]; + + } +} +``` + +缺陷:虽然实现了效果,但是有缺陷。点击左侧的UITableView,右侧的UITableViewe滚动到相应的位置,这是没问题的,但是滚动 + +右边,需要根据右边indexPath.section将选中左侧相应的indexPath。这样左侧选中的时候,又会触发右边滚动的事件,整体看上去不是很流畅。 + +#### 三、解决方案 + +观察了下,发现右侧滚动的时候左侧会上下选中,所以也就是只要让右侧滚动的时候,左侧的UITableView单方向选中,不要滚动就好,所以由于UITableView也是UIScrollview,所以在scrollViewDidScroll方法中判断右侧的UITableView是向上还是向下滚动,以此作为判断条件来让左侧的UITableView选中相应的行。 + +且之前是在scrollview代理方法中让左侧的tableview选中,这样子又会触发左侧tableview的选中事件,从而导致右侧的tablview滚动,造成不严谨的联动逻辑 + +改进后的方法: + +1. 点击左侧的UITableView,在代理方法didSelectRowAtIndexPath中拿到相应的indexPath.row,计算出右侧UITableView需要滚动的indexPath的位置。 + ``` + [self.rightTableview scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.row] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + ``` +2. 在willDisplayCell和didEndDisplayingCell代理方法中选中左侧UITableView相应的行。 + +``` +-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{ + + if (tableView == self.rightTableview && !self.isScrollDown && self.rightTableview.isDragging ) { + [self.leftTablview selectRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.section inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop]; + } +} + + + +-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{ + if (tableView == self.rightTableview && self.isScrollDown && self.rightTableview.isDragging) { + [self.leftTablview selectRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.section+1 inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop]; + + } +} + + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath +{ + if (self.leftTablview == tableView) + { + [self.rightTableview scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.row] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + }else{ + NSLog(@"嗡嗡嗡"); + } +} + + +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView{ + + static CGFloat lastOffsetY = 0; + + UITableView *tableView = (UITableView *)scrollView; + if (self.rightTableview == tableView){ + self.isScrollDown = (lastOffsetY < scrollView.contentOffset.y); + lastOffsetY = scrollView.contentOffset.y; + } + +} +``` + +##### 效果图 + +![效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/2017-09-24%2015_35_52.gif) + +附上Demo:[Demo](https://github.com/FantasticLBP/BlogDemos) diff --git a/第一部分 iOS/1.7.md b/第一部分 iOS/1.7.md new file mode 100644 index 0000000..49cd954 --- /dev/null +++ b/第一部分 iOS/1.7.md @@ -0,0 +1,101 @@ + + +# 对象在内存中的存储 + +* 栈、堆、BSS、数据段、代码段是什么? + + * 栈(stack):又称作堆栈,用来存储程序的局部变量(但不包括static声明的变量,static修饰的数据存放于数据段中)。除此之外,在函数被调用时,栈用来传递参数和返回值。 + + * 堆(heap):用于存储程序运行中被动态分配的内存段,它的大小并不固定,可动态的扩张和缩减。操作函数(malloc/free) + + * BSS段(bss segment):通常用来存储程序中未被初始化的全局变量和静态变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段输入静态内存分配 + + * 数据段(data segment):通常用来存储程序中已被初始化的全局变量和静态变量和字符串的一块内存区域 + + * 代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。 + +![内存](/assets/内存.png) + +* ##### 搞清楚上面的概念再来研究下对象在内存中如何存储? + +``` +Person *p1 = [Person new] +``` + +看这行代码,先来看几个注意点: + +* new底层做的事情: + + * 在堆内存中申请1块合适大小的空间 + + * 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做**isa**,是一个指针,指向对象所属的类在代码段中地址 + + * 初始化对象的属性。这里初始化有几个原则:a、如果属性的数据类型是基本数据类型则赋值为0;b、如果属性的数据类型是C语言的指针类型则赋值为NULL;c、如果属性的数据类型为OC的指针类型则赋值为nil。 + + * 返回堆空间上对象的地址 + +* 注意 + + * 对象只有属性,没有方法。包括类本身的属性和一个指向代码段中的类isa指针 + + * 如何访问对象的属性?指针名->属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值 + + * 如何调用方法?[指针名 方法];本质:根据指针名找到指针指向的对象,再发现对象需要调用方法,再通过对象的isa指针找到代码段中的类,再调用类里面方法 + +* 为什么不把方法存储在对象中? + + * 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段 + + * 所以一个类创建的n个对象的isa指针的地址值都相同,都指向代码段中的类地址 + +**做个小实验** + +``` +#import +@interface Person : NSObject{ + @public + int _age; + NSString *_name; + int *p; +} + +-(void)sayHi; +@end + +@implementation Person + +-(void)sayHi{ + NSLog(@"Hi, %@",_name); +} + +@end + +int main(int argc, const char * argv[]) { + Person *p1 = [Person new]; + Person *p2 = [Person new]; + Person *p3 = [Person new]; + p1->_age = 20; + p2->_age = 20; + + [p1 sayHi]; + [p2 sayHi]; + [p3 sayHi]; + + return 0; +} +``` + +``` +Person *p1 = [Person new]; +``` + +**这句代码在内存分配原理如下图所示** + +![解析图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Untitled%20Diagram-2.png) + +**结论** + +![p1](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-15%20下午5.35.17.png) +![p2](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/屏幕快照%202017-05-15%20下午5.35.34.png) + +**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。** diff --git a/第一部分 iOS/1.8.md b/第一部分 iOS/1.8.md new file mode 100644 index 0000000..9d61f12 --- /dev/null +++ b/第一部分 iOS/1.8.md @@ -0,0 +1,141 @@ + +# 长按UIWebView上的图片保存到相册 + +> 不知道各位对于这个需求要如何解决? +> +> 可能有些人会想到js与原生交互,js监听图片点击事件,然后将图片的url传递给原生App端,然后原生App将图片保存到相册,这样子麻烦吗?超麻烦。(1)、js监听图片长按事件;(2)、js将图片url传递给原生;(3)、原生通过图片的url生成UIImage;(4)、保存UIImage到系统相册,巨麻烦啊,大哥,我很懒的好不好 + +#### 那么问题跑出来了,怎么办最简单? + +* 鉴于个人道行尚浅,我就将自己的想法说出来 + +* 有个js的api:`Document.elementFromPoint()` + +> The`elementFromPoint()`method of the[`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document)interface returns the topmost element at the specified coordinates. + +所以根据这个提示,我们完全可以只在App原生端做一些代码开发,实现这个需求 + +#### 开发步骤 + +*给UIWebView添加长按手势 +*监听手势动作,拿到坐标点(x,y) +*UIWebView注入js:Document.elementFromPoint(x,y).src拿到img标签的src +*判断拿到的src是否有值,有值则代表点击的网页上的img标签,此时弹出对话框,是否保存到相册。如果src为空,则代表点击网页上的非img标签,则不需要弹出对话框。 +*拿到图片的url,生成UIImage。再将图片保存到相册 + +#### 有巨坑 + +* 长按手势事件不能每次都响应,据我猜测UIWebView本身就有很多事件,所以实现下UIGestureRecognizerDelegate代理方法。长按手势准确率100% + +``` +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ + return YES; +} +``` + + +``` +// +// ViewController.m +// WebView长按图片保存到相册 +// +// Created by 杭城小刘 on 2017/8/2. +// Copyright © 2017年 杭城小刘. All rights reserved. +// + +#import "ViewController.h" + +@interface ViewController () +@property (weak, nonatomic) IBOutlet UIWebView *webView; + +@end + +@implementation ViewController + +#pragma mark -- life cycle +- (void)viewDidLoad{ + [super viewDidLoad]; + + NSString *htmlURL = [[NSBundle mainBundle] pathForResource:@"saveImage" ofType:@"html"]; + [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:htmlURL]]]; + //给UIWebView添加手势 + UILongPressGestureRecognizer* longPressed = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressed:)]; + longPressed.delegate = self; + [self.webView addGestureRecognizer:longPressed]; +} + +#pragma mark -- UIGestureRecognizerDelegate +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ + UIActivityTypeAddToReadingList + return YES; +} + +- (void)longPressed:(UILongPressGestureRecognizer*)recognizer{ + if (recognizer.state != UIGestureRecognizerStateBegan) { + return; + } + CGPoint touchPoint = [recognizer locationInView:self.webView]; + NSString *imgURL = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", touchPoint.x, touchPoint.y]; + NSString *urlToSave = [self.webView stringByEvaluatingJavaScriptFromString:imgURL]; + if (urlToSave.length == 0) { + return; + } + + UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"大宝贝儿" message:@"你真的要保存图片到相册吗?" preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"真的啊" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + [self saveImageToDiskWithUrl:urlToSave]; + }]; + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"大哥,我点错了,不好意思" style:UIAlertActionStyleDefault handler:nil]; + [alertVC addAction:okAction]; + [alertVC addAction:cancelAction]; + [self presentViewController:alertVC animated:YES completion:nil]; +} + +#pragma mark - private method +- (void)saveImageToDiskWithUrl:(NSString *)imageUrl{ + NSURL *url = [NSURL URLWithString:imageUrl]; + + NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue new]]; + + NSURLRequest *imgRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:30.0]; + + NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:imgRequest completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (error) { + return ; + } + NSData * imageData = [NSData dataWithContentsOfURL:location]; + dispatch_async(dispatch_get_main_queue(), ^{ + + UIImage * image = [UIImage imageWithData:imageData]; + UIImageWriteToSavedPhotosAlbum(image, self, @selector(imageSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:), NULL); + }); + }]; + [task resume]; +} + +#pragma mark 保存图片后的回调 +- (void)imageSavedToPhotosAlbum:(UIImage*)image didFinishSavingWithError: (NSError*)error contextInfo:(id)contextInfo{ + NSString*message =@"嘿嘿"; + if(!error) { + UIAlertController *alertControl = [UIAlertController alertControllerWithTitle:@"提示" message:@"成功保存到相册" preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDestructive handler:nil]; + [alertControl addAction:action]; + [self presentViewController:alertControl animated:YES completion:nil]; + }else{ + message = [error description]; + UIAlertController *alertControl = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil]; + [alertControl addAction:action]; + [self presentViewController:alertControl animated:YES completion:nil]; + } +} + +@end +``` + +附上关键的js官方文档:[Document.elementFromPoint()](https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint) + +附上Demo:[Demo](https://github.com/FantasticLBP/BlogDemos) diff --git a/第一部分 iOS/1.9.md b/第一部分 iOS/1.9.md new file mode 100644 index 0000000..7a35b62 --- /dev/null +++ b/第一部分 iOS/1.9.md @@ -0,0 +1,180 @@ + +### hittest方法 + +* 就是用来寻找最合适的view +* 当一个事件传递给一个控件,就会调用这个控件的hitTest方法 +* 点击了白色的view: 触摸事件 -> UIApplication -> UIWindow 调用 \[UIWindow hitTest\] -> 白色view \[WhteView hitTest\] + +实验1: + +定义 BaseView,在里面实现方法touchBegan,监听当前哪个类调用了该方法。 + +定义KeyWindow,在里面实现hitTest方法,监听哪个类调用了该方法,用来追踪判断哪个view是最合适的view + +在控制器的界面上加5个颜色不同的view,每个view自定义view去实现,因此在不同的view上的手势就可以由不同的view拦截到。 + +![UI效果图](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Simulator%20Screen%20Shot%20-%20iPhone%206s%20Plus%20-%202017-10-11%20at%2010.14.37.png) + + +``` +//KeyWindow + +-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ + UIView *view = [super hitTest:point withEvent:event]; + NSLog(@"fittest->%@",view); + return view; +} +``` + +结果: + +点击了白色1: + +``` +2017-10-11 16:48:52.882547+0800 主流App框架[16295:358790] BrownView--hitTest withEvent +2017-10-11 16:48:59.646610+0800 主流App框架[16295:358790] GreenView--hitTest withEvent +2017-10-11 16:48:59.647145+0800 主流App框架[16295:358790] fittest->> +2017-10-11 16:48:59.647575+0800 主流App框架[16295:358790] BrownView--hitTest withEvent +2017-10-11 16:48:59.647702+0800 主流App框架[16295:358790] GreenView--hitTest withEvent +2017-10-11 16:48:59.647880+0800 主流App框架[16295:358790] fittest->> +``` + +点击了蓝色3: + +``` +2017-10-11 16:49:56.331024+0800 主流App框架[16295:358790] BrownView--hitTest withEvent +2017-10-11 16:49:56.331335+0800 主流App框架[16295:358790] BView--hitTest withEvent +2017-10-11 16:49:56.331617+0800 主流App框架[16295:358790] BlueView--hitTest withEvent +2017-10-11 16:49:56.331968+0800 主流App框架[16295:358790] YellowView--hitTest withEvent +2017-10-11 16:49:56.333206+0800 主流App框架[16295:358790] fittest->> +2017-10-11 16:49:56.333633+0800 主流App框架[16295:358790] BrownView--hitTest withEvent +2017-10-11 16:49:56.333762+0800 主流App框架[16295:358790] BView--hitTest withEvent +2017-10-11 16:49:56.333893+0800 主流App框架[16295:358790] BlueView--hitTest withEvent +2017-10-11 16:49:56.334005+0800 主流App框架[16295:358790] YellowView--hitTest withEvent +2017-10-11 16:49:56.334185+0800 主流App框架[16295:358790] fittest->> +2017-10-11 16:49:56.334644+0800 主流App框架[16295:358790] BlueView +``` + +那么看出来hitTest方法的作用就是找出最合适的view,那么我们可以指定任何事情的最合适的view为特定的view + +实验2: + +在KeyWindow中hitTest方法中返回BlueView,那么点击任何色块的view那么都会交给BlueView去处理事件。 + +``` +//KeyWindow +-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ + return self.subviews.firstObject.subviews.firstObject; +} +``` + +结果: + +``` +2017-10-11 22:48:46.102793+0800 主流App框架[21498:749663] GreenView +2017-10-11 22:48:46.668595+0800 主流App框架[21498:749663] GreenView +``` + +因为事件的响应者链条就是当用户操作屏幕会产生一个事件,该事件被系统加入到事件队列中去,UIApplication对象会将事件队列中最早加入进去的事件传递给window,然后window找到最合适的view去处理事件。因此任何事件都会先通过KeyWindow对象去判断并找到最合适的view + +## 2个重要的方法 + +* -\(BOOL\)pointInside:\(CGPoint\)point withEvent:\(UIEvent \*\)event: 用来判断触摸点是否在控件上 + +* -\(UIView \*\)hitTest:\(CGPoint\)point withEvent:\(UIEvent \*\)event: 用来判断控件是否接受事件以及找到最合适的view + +## 模仿系统实现找出最合适的view + +``` +//KeyWindow + +/** +模仿系统实现寻找最合适的view步骤 +1、控件接收事件 +2、触摸点在自己身上 +3、从后往前遍历子控件,重复前面2个步骤 +4、如果没有符合条件的子控件,那么就自己最合适 + +*/ + +-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ + if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) { + return nil; + } + + if (![self pointInside:point withEvent:event]) { + return nil; + } + + for (NSUInteger index = self.subviews.count - 1; index >= 0; index--) { + CGPoint childViewPoint = [self convertPoint:point toView:self.subviews[index]]; + UIView *fitestView = [self.subviews[index] hitTest:childViewPoint withEvent:event]; + if (fitestView) { + return fitestView; + } + return nil; + } + + return self; +} +``` + +给出 一个Demo地址:[https://github.com/FantasticLBP/BlogDemos/tree/master/模仿系统找出事件的最佳响应者](https://github.com/FantasticLBP/BlogDemos/tree/master/模仿系统找出事件的最佳响应者 "模仿系统找出事件的最佳响应者") + +实验: + +在控制器(ViewController)的view上先添加一个UIButton,再添加一个自定义的UIView\(ShelterView\),盖在button的上面。 + +需求:点击ShelterView上的点,如果点也在UIButton范围上则交给UIButton处理事件,如果不在UIButton上则交给ShelterView处理,如果点击屏幕上除了ShelterView之外的点则交给控制器的view处理。 + +``` +//ViewController +-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + NSLog(@"viewController->%s",__func__); +} + + +//ShelterView +#import "ShelterView.h" + +@implementation ShelterView + +-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ + NSLog(@"%s",__func__); +} + +-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ + NSLog(@"%s",__func__); + /** + 需求:不管点击按钮还是view都交给button处理 + 思路:在view的hitTest方法中寻找最合适的view,那么返回nil告诉系统view不是最合适的view,那么系统则认为按钮是最合适的view + return nil; + */ + + //需求,在view上点击,如果点击范围在button上则由button进行处理事件;否则交给view处理事件 + + UIView *button = nil; + for (UIView *subView in self.superview.subviews) { + //判断事件的点是否在按钮上 + if ([subView isKindOfClass:[UIButton class]]) { + button =subView; + } + + + CGPoint btnPoint = [self convertPoint:point toView:button]; + if ([button pointInside:btnPoint withEvent:event]) { + return button; + }else{ + //此时代表事件触摸点不在button上,但是也不能写nil,写nil的话点击屏幕上的其他地方系统会寻找最合适的view,此时返回nil( return nil;),则代表view不是最合适的view,那么此时点击屏幕上除了按钮之外的区域,最合适的view就是控制器上面的view + return [super hitTest:point withEvent:event]; + } + } + return nil; +} + +@end + +``` + +要看完整Demo,地址为:[https://github.com/FantasticLBP/BlogDemos/tree/master/hitTest的神奇效果(一)](https://github.com/FantasticLBP/BlogDemos/tree/master/hitTest的神奇效果(一) "hitTest的神奇效果") + diff --git a/第一部分 iOS/chapter1.md b/第一部分 iOS/chapter1.md new file mode 100644 index 0000000..981a1c2 --- /dev/null +++ b/第一部分 iOS/chapter1.md @@ -0,0 +1,6 @@ + +# 第一部分 + +第一部分主要介绍 iOS 开发中遇到的问题或者有趣的知识 + + diff --git a/第三部分 后端技术/3.1.md b/第三部分 后端技术/3.1.md new file mode 100644 index 0000000..64a443a --- /dev/null +++ b/第三部分 后端技术/3.1.md @@ -0,0 +1,155 @@ + + +摘要: 模糊查询技术、分页技术、App接口 + +1、分页问题 + + 关键技术点:php执行sql语句。示例:“SELECT * FROM hotel WHERE subject='10' and address like '%杭州%' LIMIT 0,4”。注意:LIMIT后需要跟2个数字,1个是起始位置,2是结束位置。 + + 可以对limit函数进行封装:select *from user limit (page-1)*size,page*size; + + 单例模式编写App接口的注意点: + + (1)、php后台获取参数,之后根据参数转换成SQL语句; + + (2)、php执行SQL语句,将结果转换成JSON,返回给客户端 + +2、模糊搜索 + + SQL匹配模式(开发中应用最多的一种): + + (1)、使用SQL匹配模式,不能使用操作符 = 或 != ,而是使用操作符LIKE或者NOT LIKE + + (2)、使用SQL匹配模式,MySQL提供了2种通配符。 + + %:表示任意数量的任意字符(包括0个) + + _:表示任意的单个字符 + + 例子:u_name为“张三”,“张猫三”、“三脚猫”,“唐三藏”。如果要找回包含“三”和“猫”的纪录 + + “select *from user where u_name LIKE '%三%' and u_name LIKE '%猫%'; + + 若使用“select *from user where u_name LIKE '%三%猫%';”只能找出:三脚猫 + + (3)、使用SQL匹配模式,如果匹配格式当中不包含以上2种通配符中的任意1个,其查询效果等同于= 或 != + + (4)、使用SQL匹配模式,默认情况下是不区分大小写的 + +​ + + 正则表达式匹配模式(不推荐): + + (1)、 []:表示括号内所列字符中的一个:指定一个字符、字符串或范围,要求所匹配对象为它们中的任意一个 + + 比如 SELECT * FROM [user] WHERE u_name LIKE '[张李王]三'。将找出“张三”、“李三”、“王三”(而不是“张李王三”); + + 比如[ ] 内有一系列字符(01234、abcde之类的)则可略写为“0-4”、“a-e”。SELECT * FROM [user] WHERE u_name LIKE '老[1-9]'-> 将找出“老1”、“老2”、……、“老9”; + + (2)、[^]:表示不在括号所列之内的单个字符。其取值和[]相同,要求匹配对象为指定字符以外的任意一个字符 + + 比如 SELECT * FROM [user] WHERE u_name LIKE '[^张李王]三'。将找出不姓“张”、“李”、“王”的“赵三”、“孙三”等 + + (3)、.:匹配任意的单个字符 + +   (4)、^:表示以某个字符或字符串开头。^a:以a开头 + +   (5)、$:表示以某个字符或字符串结尾。s$:以s结尾 + +   (6)、*:匹配0个或多个在它前面的字符 + +使用正则表达式匹配的操作符是:REGEXP和NOT REGEXP(RLIKE或NOT RLIKE) + +注意啊:正则表达式和SQl匹配模式工作原理不一样:正则只要匹配出符合条件就马上算成功;而SQl需要严格匹配。比如同样需要写从酒店表中拿出地址中包含“杭州”的酒店 + +SQL:SELECT *FROM hotel WHERE address LIKE '%杭州%'; + +正则:SELECT *FROM hotel WHERE address REGEXP '^[杭][州]*’; + + +需要特别注意的是:如果所查询的内容包含通配符时,导致我们查询"%","_","["的语句无法正常实现。因此我们需要特殊处理。 + +需要特别注意的是:如果所查询的内容包含通配符时,导致我们查询"%","_","["的语句无法正常实现。因此我们需要特殊处理。 + + +```php +function sql_encode($sql){ + $sql = replace($sql,"[","[[]"); + $sql = replace($sql,"%","[%]"); + $sql = replace($sql,"_","[_]"); + return $sql; +} +``` + +最后贴一个利用模糊查询实现分页查询的接口。 + +```php +telephone = $_REQUEST["telephone"]; + self.$this->subjectId = $_REQUEST["subjectId"]; + self.$this->cityName = $_REQUEST["cityName"]; + self.$this->page = $_REQUEST["page"]; + self.$this->size = $_REQUEST["size"]; + + $mysqlPdo = new PdoMySQL(); + + if($this->telephone == ""){ + Response::show(201,"fail","非安全的数据请求","json"); + } + $userRows = $mysqlPdo->find("user","telephone='$this- >telephone'"); + if($userRows[0]["telephone"] != $this->telephone){ + Response::show(201,"fail","非安全的数据请求","json"); + } + $city = str_replace("市","",$this->cityName); + $allrows = $mysqlPdo->find($this->tableName,"subject='$this->subjectId' and address like '%$city%'","","","","",[(intval($this->page)-1)*intval($this->size),intval($this->page)*intval($this->size)]); + Response::show(200,'酒店列表获取成功',$allrows,'json'); + + } +} + +$lister = HotelList::getInstance(); +$lister->getHotels(); +``` + +其中:Respone和PdoMySQL的2个类分别是接口数据展示的类和PDO操作的一个封装。 diff --git a/第三部分 后端技术/3.2.md b/第三部分 后端技术/3.2.md new file mode 100644 index 0000000..4db2fad --- /dev/null +++ b/第三部分 后端技术/3.2.md @@ -0,0 +1,46 @@ +# 网页二维码,App 扫码登录实现原理 + + + +### 需求介绍 + +首先,介绍下什么是扫码登录。现在,大部分同学手机上都装有qq和淘宝,天猫等这一类的软件。而开发这些app的企业,都有他们相对应的网站。为了让用户在使用他们的网站时,登录更加方便和安全。这些企业提供了, 使用手机,扫一扫,就可以登录的服务。网页登录时的效果如下: + + + +![淘宝扫码登录](https://github.com/FantasticLBP/knowledge-kit/blob/master/第三部分%20后端技术/assets/6693515-d566f3006b82c505.jpeg?raw=true) + + + + + +![QQ扫码登录](https://github.com/FantasticLBP/knowledge-kit/blob/master/第三部分%20后端技术/assets/6693515-3b17bdcc22e1a5a3.png?raw=true) + + + +有很多小伙伴可能会感到很神奇,网页上只是显示了个二维码,它怎么就知道是哪个手机扫到了二维码,并且进行登录的呢?而且,登录完成以后,还能直接把用户信息显示给用户,真的是很神奇啊。 + +### 原理解释 + +**网页端+服务器** + +接下来就是对于这个服务的详细实现。首先,大概说一下原理:用户打开网站的登录页面的时候,向浏览器的服务器发送获取登录二维码的请求。服务器收到请求后,随机生成一个uuid,将这个id作为key值存入redis服务器,同时设置一个过期时间,再过期后,用户登录二维码需要进行刷新重新获取。同时,将这个key值和本公司的验证字符串合在一起,通过二维码生成接口,生成一个二维码的图片(二维码生成,网上有很多现成的接口和源码,这里不再介绍。)然后,将二维码图片和uuid一起返回给用户浏览器。 + +浏览器拿到二维码和uuid后,会每隔一秒向浏览器发送一次,登录是否成功的请求。请求中携带有uuid作为当前页面的标识符。这里有的同学就会奇怪了,服务器只存了个uuid在redis中作为key值,怎么会有用户的id信息呢? + +这里确实会有用户的id信息,这个id信息是由手机服务器存入redis中的。具体操作如下: + +**手机端+服务器** + +话说,浏览器拿到二维码后,将二维码展示到网页上,并给用户一个提示:请掏出您的手机,打开扫一扫进行登录。用户拿出手机扫描二维码,就可以得到一个验证信息和一个uuid(扫描二维码获取字符串的功能在网上同样有很多demo,这里就不详细介绍了)。由于手机端已经进行过了登录,在访问手机端的服务器的时候,参数中都回携带一个用户的token,手机端服务器可以从中解析到用户的userId(这里从token中取值而不是手机端直接传userid是为了安全,直接传userid可能会被截获和修改,token是加密的,被修改的风险会小很多)。手机端将解析到的数据和用户token一起作为参数,向服务器发送验证登录请求(这里的服务器是手机服务器,手机端的服务器跟网页端服务器不是同一台服务器)。服务器收到请求后,首先对比参数中的验证信息,确定是否为用户登录请求接口。如果是,返回一个确认信息给手机端。 + +手机端收到返回后,将登录确认框显示给用户(防止用户误操作,同时使登录更加人性化)。用户确认是进行的登录操作后,手机再次发送请求。服务器拿到uuId和userId后,将用户的userid作为value值存入redis中以uuid作为key的键值对中。 + +**登录成功** + +然后,浏览器再次发送请求的时候,浏览器端的服务器就可以得到一个用户Id,并调用登录的方法,声成一个浏览器端的token,再浏览器再次发送请求的时候,将用户信息返回给浏览器,登录成功。这里存储用户id而不是直接存储用户信息是因为,手机端的用户信息,不一定是和浏览器端的用户信息完全一致。 + +**登录原理图如下:** + +![扫码登录流程图](https://github.com/FantasticLBP/knowledge-kit/blob/master/第三部分%20后端技术/assets/6693515-8e5bea48462a7904.jpeg?raw=true) + diff --git a/第三部分 后端技术/assets/6693515-3b17bdcc22e1a5a3.png b/第三部分 后端技术/assets/6693515-3b17bdcc22e1a5a3.png new file mode 100644 index 0000000..6613c41 Binary files /dev/null and b/第三部分 后端技术/assets/6693515-3b17bdcc22e1a5a3.png differ diff --git a/第三部分 后端技术/assets/6693515-8e5bea48462a7904.jpeg b/第三部分 后端技术/assets/6693515-8e5bea48462a7904.jpeg new file mode 100644 index 0000000..34bea08 Binary files /dev/null and b/第三部分 后端技术/assets/6693515-8e5bea48462a7904.jpeg differ diff --git a/第三部分 后端技术/assets/6693515-d566f3006b82c505.jpeg b/第三部分 后端技术/assets/6693515-d566f3006b82c505.jpeg new file mode 100644 index 0000000..921cc2b Binary files /dev/null and b/第三部分 后端技术/assets/6693515-d566f3006b82c505.jpeg differ diff --git a/第三部分 后端技术/chapter3.md b/第三部分 后端技术/chapter3.md new file mode 100644 index 0000000..9e633db --- /dev/null +++ b/第三部分 后端技术/chapter3.md @@ -0,0 +1,5 @@ +# 第三部分 + +第三部分主要记录在后端技术的经验或学习心得。包括 Node、PHP、Python等 + + diff --git a/第二部分 Web 前端/.DS_Store b/第二部分 Web 前端/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/第二部分 Web 前端/.DS_Store differ diff --git a/第二部分 Web 前端/2.1.md b/第二部分 Web 前端/2.1.md new file mode 100644 index 0000000..f2c5601 --- /dev/null +++ b/第二部分 Web 前端/2.1.md @@ -0,0 +1,197 @@ + +# :last-child与:last-of-type + +> 同学们遇到过给同一组元素的最后一个元素设置css失效的情况吗?我遇到过,当时使用:last-child居然不起作用,看到名字不科学啊,明明是“最后一个元素”,那为什么设置CSS失效呢?今天来一探究竟吧 + +* 先看一组`:last-child`正常工作的代码 + +``` + + + + + :last-child、:last-of-type + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • + +
+ + +``` +![效果1](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-091957@2x.png) + +* 再先看一组`:last-child`不正常工作的代码 + +``` + + + + + :last-child、:last-of-type + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +

    我是来骚扰的

    +
+ + +``` + + +![效果2](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092046@2x.png) + +问题抛出来了,那么来研究下:last-child和:last-of-type究竟是何方神圣。 + +1. :last-child:**The last-child CSS pseudo-class represents the last element among a group of sibling elements.(:last-child这个css伪类代表的一组兄弟元素当中最后一个元素)但经过代码发现,它说的一组元素应该是指其父元素的所有子元素且类型为:last-child前面指定的类型的一组元素。** + +2. :last-of-type:**The last-of-type CSS pseudo-class represents the last element of its type among a group of sibling elements.(**:last-of-type这个css伪类代表其类型的一组兄弟元素中的最后一个元素**)所以它指的是和**:last-of-type前面的元素类型一致的一组元素的最后一个元素 + +同理::nth-last-child和:nth-last-of-type的区别在于父元素的子元素中且与:nth-last-child前面的元素类型一致的最后一个元素 + +做个验证 + +* :nth-last-child可以正常工作的代码 + +``` + + + + + :last-child、:last-of-type + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • + +
+ + + +``` + + +![效果3](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092145@2x.png) + +* :nth-last-child不能正常工作的代码 + +``` + + + + + :last-child、:last-of-type + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +

    我是来骚扰的

    +
+ + +``` + +![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092232@2x.png) + +* 接下来:nth-last-of-type闪亮登场 + +``` + + + + + :last-child、:last-of-type + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +

    我是来骚扰的

    +
+ + +``` + +![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180507-092358@2x.png) \ No newline at end of file diff --git a/第二部分 Web 前端/2.10.md b/第二部分 Web 前端/2.10.md new file mode 100644 index 0000000..bf5009a --- /dev/null +++ b/第二部分 Web 前端/2.10.md @@ -0,0 +1,61 @@ + +# 调试工具安装 + +--- + +## 安装方式一 + +Vue-devtools 可以从 Chrome 商店直接下载安装,前提需要翻墙。 + +## 安装方式二 + +* 第一步:找到 Vue-devtools 的 github 地址,并将其 clone 到本地。 + +``` +git clone https://github.com/vuejs/vue-devtools.git +``` + +* 第二步:安装项目所依赖的 npm 包 + +``` +npm install +``` + +遇到的问题: + +![遇到的问题](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools1.png) + +改用命令 + +``` +npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver +``` + +![改用命令](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools3.png) + +继续 npm install + +* 第三步:编译项目文件 + +``` +npm run build +``` + +![编译项目文件](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools4.png) + +* 第四步:添加至 Chrome 浏览器的拓展 + +``` +浏览器地址栏输入:chrome://extensions/ + +点击“加载已解压的拓展程序”选择本地 clone 下来的文件夹中的 shells -> chrome 文件夹(vue-devtools-master/shells/chrome ) +``` + +![Chrome 添加拓展](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools5.png) + +* 第五步:重启浏览器 + +* 第六步:在浏览器中的调试 Vue 代码 +![Chrome 调试 Vue](https://fantasticlbp.gitbooks.io/knowledge-kit/assets/Chrome-Vue-tools6.png) + + diff --git a/第二部分 Web 前端/2.11.md b/第二部分 Web 前端/2.11.md new file mode 100644 index 0000000..fdcd4c4 --- /dev/null +++ b/第二部分 Web 前端/2.11.md @@ -0,0 +1,10 @@ +# H5页面保存页面为图片 + +方案:有个github的js库可以将html转换为canvas,然后canvas可以转换为图片,然后图片可以下载,所以基本路线就是: + +* html2canvas.js :将htmldom转为canvas ([https://github.com/niklasvh/html2canvas](https://github.com/niklasvh/html2canvas "html2canvas")) +* canvasAPI: toDataUrl\(\)将canvas转为base64格式 +* 图片下载 + +具体描述:https://juejin.im/post/5a17c5e26fb9a04527254689 + diff --git a/第二部分 Web 前端/2.12.md b/第二部分 Web 前端/2.12.md new file mode 100644 index 0000000..d374b3e --- /dev/null +++ b/第二部分 Web 前端/2.12.md @@ -0,0 +1,180 @@ +## Promise + +## 一、基础使用 + +举个例子: + +``` +//方式一 +function test(resolve, reject) { + var timeout = Math.random() * 2; + console.log("开始尝试promise"); + setTimeout(function() { + if (timeout < 1) { + resolve(timeout); + } else { + reject(timeout); + } + }, 1000); +} + +function resolve(time) { + console.log("小于1的数字" + time); +} + +function reject(time) { + console.log("不小于1的数字" + time); +} +var p1 = new Promise(test); +var p2 = p1.then(function(result) { + console.log("成功" + result); +}); +var p3 = p1.catch(function(reason) { + console.log("失败" + reason); +}); + +// 方式二 +new Promise(test).then(function(result) { + console.log("成功" + result); +}).catch(function(reason) { + console.log("失败" + reason); +}); + + +//方式三 +
+ +
+ + +var logging = document.getElementById("test-promise-log"); +while (logging.children.length > 1) { + logging.removeChild(logging.children[logging.children.length - 1]); +} + +function log(s) { + var p = document.createElement("p"); + p.innerHTML = s; + logging.appendChild(p); +} + + +new Promise(function(resolve, reject) { + log("start new Promise..."); + var timeout = Math.random() * 2; + log('set timeout to: ' + timeout + 'seconds.'); + setTimeout(function() { + if (timeout < 1) { + log('call resolve()...'); + resolve('200 OK'); + } else { + log('call reject()...'); + reject('timeout in ' + timeout + 'sconds.'); + } + }, timeout * 1000); + +}).then(function(result) { + log('Done: ' + result); +}).catch(function(reason) { + log("Failed: " + reason); +}); + + +------------ + +start new Promise... + +set timeout to: 1.6579586637257697seconds. + +call reject()... + +Failed: timeout in 1.6579586637257697sconds. +``` + +可见 Promise 的最大好处就是在异步执行的流程中,将执行代码和处理结果的代码清晰地分离了。 + +## 二、串行执行 + +比如有若干个异步任务,需要先做任务1,如果任务1成功后执行任务2...,任何一个环节中的任务失败则不再继续执行错误处理函数。 + +要完成这个需求,传统写法需要一层一层的嵌套代码有了 Promise ,就可以 + +``` +task1.then(task2).then(task3).catch(erroeHandler); +``` + +举个例子 + +``` +function add(num) { + return new Promise(function(resolve, reject) { + log(num + " + " + num + "..."); + setTimeout(resolve, 500, num + num); + }); +} + +function mul(num) { + return new Promise(function(resolve, reject) { + log(num + " x " + num + "..."); + setTimeout(resolve, 500, num * num); + }); +} + +new Promise(function(resolve, reject) { + resolve(100); +}).then(add).then(mul).then(add).then(mul).then(function(result) { + log("the result is " + result); +}); + + +--------------------- +100 + 100... + +200 x 200... + +40000 + 40000... + +80000 x 80000... + +the result is 6400000000 +``` + +## 三、并行执行 + +``` + var p1 = new Promise(function(resolve, reject) { + setTimeout(resolve, 500, "p1 success"); + }); + + var p2 = new Promise(function(resolve, reject) { + setTimeout(resolve, 500, "p2 success"); + }); + Promise.all([p1, p2]).then(function(results) { + console.log(results) + }); +``` + +多个 任务需要同时进行也就是并行执行,那么就可以使用 Promise.all\(\) 实现 + + + +## 四、容错处理,只需要拿到先返回的结果。 + +``` +var p1 = new Promise(function(resolve, reject) { + setTimeout(resolve, 500, "p1 success"); +}); + +var p2 = new Promise(function(resolve, reject) { + setTimeout(resolve, 500, "p2 success"); +}); + + +//使用第一个返回的结果 +Promise.race([p1, p2]).then(function(result) { + console.log("结果是: " + result); +}); +``` + + + diff --git a/第二部分 Web 前端/2.13.md b/第二部分 Web 前端/2.13.md new file mode 100644 index 0000000..73b3616 --- /dev/null +++ b/第二部分 Web 前端/2.13.md @@ -0,0 +1,223 @@ +# webpack-dev-server 的配置和使用 + + + +> webpack-dev-server是一个小型的`Node.js Express`服务器,它使用`webpack-dev-middleware`来服务于webpack的包,除此自外,它还有一个通过[Sock.js](http://sockjs.org/)来连接到服务器的微型运行时. +> +> 说人话就是可以极大的提高开发效率。这么好的东西那就用起来 + + + + + +1、看看 webpack.config.js + +``` +module.exports = { + target:"web", //编译平台 web + entry:path.join(__dirname,"src/index.js"), //应用程序的主入口 + output:{ + filename:"bundle.js", //输出文件名 + path: path.join(__dirname, "dist") + }, + module:{ + rules:[ + { + test:/\.vue$/, + loader:"vue-loader" + }, + { + test:/\.css$/, + use:[ + "style-loader", + "css-loader" + ] + }, + { + test:/\.(gif|jpg|jpeg|png|svg)$/, + use:[ + { + loader:"url-loader", + options:{ + limit:1024, + name:'[name]-lbp.[ext]' //甚至可以加一些名字处理规则 '[name]-lbp.[ext]' + } + } + ] + } + ] + }, + /* + 在 plugins 中写 webpack.DefinePlugin 是为了在 webpack 打包的时候选择源代码的版本。当在 dev 的时候会打包开发所需要的所有东西,比如警告信息 + */ + plugins:[ + new webpack.DefinePlugin({ + 'process.env':{ + NODE_ENV: isDev ? '"development"' : '"production"' + } + }), + new HTMLPlugin() + ] +} +``` + + + +配置文件提供1个入口和一个出口,webpack 根据这个文件来执行 js 的打包和编译。webpack-dev-server 其中的部分功能就克服了上面2个问题。 webpack-dev-server 主要启动了1个使用 express 的 HTTP 服务器(资源文件)。由于这个 HTTP 服务器和 client 使用了 websocket 通讯协议,原始文件作出了改动,webpack-dev-server 会实时编译,但是最后编译的文件并没有输出到目标文件夹 + +## 一些截图 + + + +![](/assets/todo-20180226-1.png) + + + +![](/assets/todo-20180226-2.png) + + + +![](/assets/todo-20180226-3.png) + + + + + +![](/assets/todo-20180226-4.png) + + + +![](/assets/todo-20180226-5.png) + + + +完整的 webpack.config.js + +``` +const path = require("path"); +const HTMLPlugin = require("html-webpack-plugin") +const webpack = require("webpack") + + +//process.env 是读取系统环境。比如在启动服务的时候会通过 npm run build/dev 运行,其真实入口是在 webpack.config.js ,然后我们在 webpack.config.js设置为 production 或 development。那么就可以通过 process.env.NODE_ENV 获取 +const isDev = process.env.NODE_ENV === 'developement' + + + +//webpack 会将文件打包为 bundle.js +//需要为 .vue 文件声明一个类型, 因为 webpack 只识别 .js 且支持的语法为 ES5 +//增加一个 module 模块,一个键为 rules ,值可以是多个数组。检测文件以 .vue 结尾的话就以 vue-loader 解析 +//vue-loader 为 webpack 解析 .vue + + + +/* +查看 dist 文件夹下的 bundle.js 文件 +最顶部都是 webpack 处理包的代码 +其次是 vue 代码 +webpack 做的事情就是将不同类型的静态资源打包成 JS,然后在 HTML 中引入 JS 就可以减小 HTTP 请求。 +以 css 结尾的文件使用 css-loader。 +css-loader:从css文件中将内容读了出来。 +style-loader:判断将css代码插入到html还是写到一个新的文件中 + +在写图片的读取规则的时候,用到了use,use数组里面是一个个对象,loader 可以配置一些选项 +url-loader 可以将图片转换为 base64 字符串直接写到 JS 内容里面而不用生成一个图片,这对于一些小的图片是比较有利的,这样子可以减小 http 请求。 +url-loader 封装了 file-loader:读取了文件内容并做一些简单操作,再把图片换个名称存在一个地方 + limit选项:如果图片小于1024就可以转义成 base64 + name选项: 可以处理图片的名字 + +配置完之后则需要安装相应的模块,npm install + + +配置环境变量: +1、MAC 平台:NODE_ENV=production +2、Window 平台 SET NODE_ENV=production +3、为了配置开发环境和正式环境,需要安装: cross-env包。需要修改package.json 文件中的 scripts-> build 和 dev + + +*/ +const config = { + target:"web", //编译平台 web + entry:path.join(__dirname,"src/index.js"), //应用程序的主入口 + output:{ + filename:"bundle.js", //输出文件名 + path: path.join(__dirname, "dist") + }, + module:{ + rules:[ + { + test:/\.vue$/, + loader:"vue-loader" + }, + { + test:/\.css$/, + use:[ + "style-loader", + "css-loader" + ] + }, + { + test:/\.(gif|jpg|jpeg|png|svg)$/, + use:[ + { + loader:"url-loader", + options:{ + limit:1024, + name:'[name]-lbp.[ext]' //甚至可以加一些名字处理规则 '[name]-lbp.[ext]' + } + } + ] + } + ] + }, + /* + 在 plugins 中写 webpack.DefinePlugin 是为了在 webpack 打包的时候选择源代码的版本。当在 dev 的时候会打包开发所需要的所有东西,比如警告信息 + */ + plugins:[ + new webpack.DefinePlugin({ + 'process.env':{ + NODE_ENV: isDev ? '"development"' : '"production"' + } + }), + new HTMLPlugin() + ] +} + + +//为了判断是开发环境还是生成环境,判断了 isDEV +/* +webpack 2.0 以后增加了 devSever。用来处理开发环境的配置 +0.0.0.0:可以通过 localhost 和本地电脑 ip 访问项目 +overlay:在 webpack 编译的时候如果有错误显示在网页上。errors:true +*/ +if (isDev) { + config.devtool = "#cheap-module-eval-source-map" + config.devServer = { + port:8000, + host:'0.0.0.0', + overlay:{ + errors:true + }, + hot:true + } + config.plugins.push( + new webpack.HotModuleReplacementPlugin(), + new webpack.NoEmitOnErrorsPlugin() + ) +} + +module.exports = config +``` + + + + + + + + + + + + + diff --git a/第二部分 Web 前端/2.14.md b/第二部分 Web 前端/2.14.md new file mode 100644 index 0000000..70f86c8 --- /dev/null +++ b/第二部分 Web 前端/2.14.md @@ -0,0 +1,18 @@ +# Web 与 H5 交互的坑 + + + +1、遇到一个问题 ,一个功能在 iOS 手机上正常工作,但是在 Android 上不正常,依照经验来看无非就是2个原因:(1)、URL参数少传递了;(2)、JS 在移动端的 webview 上报错了,所以我让远程对接的人员将 url 打印出来,发现没错。继续让他打印查看下 js 错误日志,发现 “Cannot read property "getItem" of null”。 代码出错行数在 259。看了下具体代码就是读取 localstorage + +![报错信息](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/Andoid_Webview_Localstroage_erroe.jpg) + +想了想,在我们的项目中 Android 原生的代码在使用 webview 的时候额外设置了代码具体如下 + + + +``` +mWebView.getSettings().setDomStorageEnabled(true); +``` + + + diff --git a/第二部分 Web 前端/2.15.md b/第二部分 Web 前端/2.15.md new file mode 100644 index 0000000..1dcfd87 --- /dev/null +++ b/第二部分 Web 前端/2.15.md @@ -0,0 +1,25 @@ +# 前端持久化 + +1. 早期的前端存储主要是 cookie,后来经过前端工程师不屑的追求以及开发,有了后来的 localStorage 和 sessionStorage 技术。 +2. 再到后来技术演进有了 indexDB 技术,也就是一个事务型的 key-value 形式的数据库 +3. 说说 cookie 有了,为什么还要有 localStorage,因为 cookie 虽然可以存储一些数据,但是大小非常有限,也就是 4k。并且 cookie 的本质工作是用来追踪浏览器用户的信息,因为 HTTP 是无状态协议,即服务器不知道用户上一次做了些什么,这严重阻碍了 Web 应用程序的时下。也就是通过 HTTP 连接无法知道用户干了些什么,比如说当用户在浏览一个商城的时候无法记录他购买了什么商品,介于此我们可以使用 cookie 技术用来绕开 HTTP 无状态的的特点,服务器可以设置或读取 cookie 中包含的信息,借此维护用户和服务器会话中的状态。 + +cookie 的另一个日常应用场景就是当用户勾选了某个网站的“下次自动登录”,那么在下次访问这个网站的时候,用户发现没有输入账号和密码就已经是登录状态。这是因为前一次登录的时候服务器发送了包含登录凭证(用户名和密,以及包含登录时长 的 token 信息)的 cookie 到用户的硬盘上,第二次登录时发现登录时长还有效,那么则自动登录 +4. 对于存储数据这个功能来说,cookie 的缺陷 + * cookie 会被附加到每个 HTTP 请求的头部去,所以如果用来存储数据,那么每次请求就加大了请求数据量 + * cookie 在 HTTP 请求中是明文传递的,所以存在安全性问题(除非 HTTPS) + * cookie 只有4k的存储空间 + +5.cookie 的另一种不好的方面表现在:当用户去访问某个网站的时候(通过搜索引擎查到的),这个网站包含一种叫做网页臭虫的图片,通常是1像素大小(以便于隐藏),它们的作用是将所有访问过此页面的计算机写入 cookie,而后,电子商务网站读取这些 cookie 信息,并寻找写入这些 cookie 的网站,随机发送包含针对这个网站的相关产品广告的垃圾邮件给这些用户 + + + + + + + + + + + + diff --git a/第二部分 Web 前端/2.16.md b/第二部分 Web 前端/2.16.md new file mode 100644 index 0000000..9e2969f --- /dev/null +++ b/第二部分 Web 前端/2.16.md @@ -0,0 +1,13 @@ +# VS-Code 的配置 + +1、Mac 下 按住 “command+shift+p”打开命令面板,输入Preferences: Open User Settings,在右边的区域输入 + +``` +{ + "window.zoomLevel": -1, + "editor.fontSize": 17, + "files.autoSave": "onFocusChange", + "terminal.integrated.fontSize": 15, + "editor.tabSize": 2 +} +``` \ No newline at end of file diff --git a/第二部分 Web 前端/2.17.md b/第二部分 Web 前端/2.17.md new file mode 100644 index 0000000..4d5b182 --- /dev/null +++ b/第二部分 Web 前端/2.17.md @@ -0,0 +1,556 @@ +# Vue + +> Vue学习过程中遇到的一些小tips + +1、组件化 + +Vue 的组件可以分为全局组件和局部组件 + +全局组件:声明好后可以在全局使用 +局部组件:只可以在当前模块使用 + +``` +//全局组件 +Vue.componetns('todo-item',{ + template:'
  • {{content}}
  • ', + prop:['content'] +}); +//局部组件 +var HobbyItem = { + template:"
  • hobby
  • " +} + +new Vue({ + el:"#test", + components:{ + 'todo-item':HobbyItem + } +}); +``` + +2、模块、实例 + +每一个模块都是 Vue 的一个实例,也就是说可以向每一个模版中像写 Vue 的实例一样进行开发。 + +3、父组件与子组件通信用 props 传递 + +4、子组件向外触发事件 this.$emit(事件名,参数列表) + +5、template 模版下最外层只可有一个根元素 +``` + + +``` + +6、单独在 .vue 文件中,data 是作为函数存在的。 + +``` + +data: function(){ + +} + +data () { + +} + +``` + +7、在 vue-cli 脚手架中,组件的声明方式 + +``` +export default{ + components : { + 'todo-item':Todo-Item + } +} + +components:['Todo-item'] + +``` + +8、style 中可以声明样式作用域 +style 同名的样式不会对其他组件有影响 + +9、在用 Vue 框架在开发的时候一般都会用 **Vue-cli** 脚手架,但是这样经过 webpack 打包后源代码会被打包成 dist 目录下的 bundle.js,此时的代码是被压缩处理过的,且经过 webpack 的处理各个代码模块的逻辑,代码可读性不是很好且不可调试,此时有些人在开发阶段就会需要调试?此时怎么办呢 + +慌不要慌,小哥哥带你 hold 住全场。 +* 脚手架已经帮你处理好了这块需求了,看下图 + +![配置图例](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180602-210826@2x.png) + +* 有些大佬不要脚手架,喜欢自己初始化项目,用 npm 挨个安装所需要的依赖。然后自己配置 webpack 的 options。需要调试的话,需要做下面的配置 + + ``` + config.devtool = '#cheap-module-eval-source-map' + + ``` + +这样你就可以在浏览器当中像写普通的 JS 一样进行调试代码了。比如 + +![调试界面](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180602-211328@2x.png) + + +10、Vue 中 **** 底层的做法是 ** render()**方法, 对 template 中的元素依次遍历创造节点 + +11、使用 stylus 写 css 的时候需要告诉 webpack如何处理代码,所以 +``` + +``` + +12、在使用 v-for 或 jsx 的map方法时候需要在生成的节点设置一个唯一的**key**作为标识,这样下次渲染的时候就可以根据 key 值判断,是否需要刷新 dom ,提高了效率 + +13、props 的另一种写法 +``` +props:[ + todos:{ + type:Array, + required:true + } +] +``` + +14、css 单独打包,缓存下来,提高程序的体验。需要使用**extrat-text-webpack-plugin** + +15、业务代码与框架代码如果打包在一起,后期维护很麻烦(因为框架代码基本不需要很快更新,但是业务代码经常更新),所以应该拆分出来,单独打包。将框架代码方便缓存。 + +16、webpack 的2个概念: + hash: 各个文件生成的**hash**值相同 + chunkhash:不同文件(模块)生成的**hash**值不同 + + +- Vue 使用官方组件(比如 vue-resource) + - 先引入 + - 再 use(必须在 main.js 中) + +``` +import VueResource from 'vue-resource' +Vue.use(VueResource); +``` + +- 第三方组件 + + - 哪里用,哪里引入(比如 axios) + +- npm install 组件名 --save + + 将安装的组件名称的依赖写入 package.json + +- 网络请求组件 + + - vue-resource + - axios + - fetch-jsonp + +- 父组件给子组件传值 + + - 父组件在调用子组件的时候传递 + + - 子组件 声明 porps + + - 同时可以在子组件接收的时候验证传值的正确性 + + ``` + props:['name','age'] + + props:{ + 'name':String, + 'age':Number + } + ``` + + + +- 父组件给子组件传递方法 + + ``` + //父 + + + + + //子 + export default{ + props:["run"], + methods:{ + test(){ + run(); + } + } + } + ``` + +- 子组件可以通过上面传递值给父组件 + +- 父组件可以将自身传递给子组件。所以可以在子组件里面访问父组件的属性和执行父组件的方法 + +- 父组件主动获取子组件的数据和方法 + + - 在调用子组件的时候给子组件定义一个 ref + + ``` + + ``` + + - 在父组件里面通过**this.$refs.header.属性** 和 **this.$refs.header.方法** + + - 子组件里面获取父组件的属性和方法 **this.$parent.属性** 和**this.$parent.方法** + +- 非父子组件间的传值 + + - 先定义1个中间的组件 + - 在需要暴露数据的一方。import 中间组件,调用 $emit(事件名称,数据) + - 在需要接受数据的一方。import 中间组件,调用 $on(事件名称,function(data){ //... }) + + ``` + //中间组件 + import Vue from 'Vue'; + var VueEvent = new Vue(); + export default = Vue; + + //Home.vue(传递数据给 News.vue) + import VueEvent from '../model/VueEvent.js'; + + VueEvent.$emit("postData",{"name":"杭城小刘"}); + + //News.vue(接收数据) + import VueEvent from '../model/VueEvent.js'; + + new Vue({ + mounted(){ + VueEvent.$on("postData",function(data){ + console.log("从Home组件接收到的数据:"+data); + }); + } + }); + ``` + +- 路由的使用规则 + + - 创建、引用组件(main.js) + + ``` + import Home form './Components/Home.vue' + import News form './Components/News.vue' + ``` + + - 导入 vue-router 并 use(main.js) + + ``` + import VueRouter from 'vue-router' + Vue.use(VueRouter); + ``` + + - 配置路由(main.js) + + ``` + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + ]; + ``` + + - 实例化 router(main.js) + + ``` + const router = new VueRouter({ + routes + //等于 routes:routes,只有当key和value一致的时候可以简写 + }); + ``` + + - 挂载 router(main.js) + + ``` + var vue = new Vue({ + el:"#app", + router, + data(){ + return { + msg:"root components" + } + } + }); + ``` + + - 在模版里面放上路由的出口。将 写到根组件上 (App.vue) + + ``` + //App.vue + + ``` + +- 要实现类似导航效果,可以使用 **** + + ``` + 首页 + 新闻 + ``` + +- 默认首页 + + ``` + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + {path:"*",redirect:"News"} //默认跳转到首页 + ]; + ``` + +- Vue 动态路由和 Get 传值 + + ``` +
      +
    • {{item}}
    • +
    + + //新增加的 router-link 需要在路由配置里面添加配置项。 + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + {path:"/content",component:"Content"}, + {path:"*",redirect:"News"} //默认跳转到首页 + ]; + ``` + + - 上面的写法是静态路由,也就是不能传递参数 + + - 那么什么是动态路由,也就是可以传递参数 + + ``` + //新增加的 router-link 需要在路由配置里面添加配置项。 + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + {path:"/content:newid",component:"Content"}, //动态路由 + {path:"*",redirect:"News"} //默认跳转到首页 + ]; + ``` + + - 子组件页面获取动态路由传递过来的值 + + ``` + this.$route.params + ``` + + - 子组件拿到父组件动态传递过来的值用动态路由 + + ``` +
      +
    • {{item}}
    • +
    + + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + {path:"/content:newid",component:"Content"}, //动态路由 + {path:"*",redirect:"News"} //默认跳转到首页 + ]; + + this.$route.params + ``` + + - Get 传值 + + ``` + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + {path:"/content",component:"Content"}, //动态路由 + {path:"/product",component:"Product"}, // + {path:"*",redirect:"News"} //默认跳转到首页 + ]; + +
      +
    • {{item}}
    • +
    + + //拿到值 + this.$route.query + ``` + +- 编程式导航(JS 跳转控) + + ``` + //直接跳转到某个组件 + this.$route.push({path:'News' }); + //跳转到某个组件并且传递值 + this.$route.push({path:'/content/495'}); + ``` + + - 命名路由跳转 + + ``` + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + {path:"/content",component:"Content",name:'news'}, //动态路由 + {path:"/product",component:"Product"}, // + {path:"*",redirect:"News"} //默认跳转到首页 + ]; + //跳转 + this.$router.push({name:'news'}); + ``` + + + +- vue-router 默认的是 hash 模式, 也就是 127.0.0.1/# 这种。所以我们在初始化 vue-router 的时候可以修改它的模式 + + ``` + const VueRouter = new VueRouter({ + mode: 'history',//hash 模式改为 history 模式 + routs + }) + ``` + +- 嵌套路由(比如顶部的菜单栏不变,点击左边的菜单栏实现页面内容的切换) + + ``` + //注册路由嵌套 + const routes = [ + {path:"/home",component:"Home"}, + {path:"/news",component:"News"}, + { + path:"/user", + components:"User", + children:[ + {path:'useradd',component:'UserAdd'}, + {path:'userlist',component:'UserList'}, + ] + }, + {path:"/content",component:"Content",name:'news'}, //动态路由 + {path:"/product",component:"Product"}, // + {path:"*",redirect:"News"} //默认跳转到首页 + ]; + + //将放到动态加载的子组件的地方 + //User.vue + + + ``` + + + +- Vuex:主要解决不同组件之间的数据共享、数据持久化(不适合与小项目,主要用于大项目) + + - 安装 vuex + + - src 目录下新建一个 vuex 的文件夹 + + - vuex 文件夹下面新建一个 store.js 文件 + + - 引入 vuex + + ``` + import Vue from 'vue' + import Vuex from 'vuex' + Vue.use(Vuex); + ``` + + - 定义数据 + + ``` + // state 在 vuex 中用于存储数据 + var state = { + count:1 + } + ``` + + - 定义方法 + + ``` + var muations = { + incCount(){ + ++state.count; + } + } + ``` + + - 暴露 vuex + + ``` + const store = new Vuex.Store({ + state, + mutations + }); + + export default store; + ``` + +- 使用 vuex(解决不同组件之间数据共享问题;数据持久化) + + - 引入 vuex + + ``` + import store from '../vuex/store.js' + ``` + + - 注册 vuex + + ``` + export default{ + data(){ + return { + msg: 'Hello' + } + }, + store, + methods:{ + + } + } + ``` + + - 使用 + + ``` + //访问属性 + this.$store.state.count + //触发方法+不带参数 + this.$store.commit('incCount'); + + //触发方法+不带参数 + this.$store.commit('方法名',参数); + + var mutation = { + addList(state,data{ + state.list = data; + } + } + ``` + + - getters 类似于计算属性,改变 state 里面的 count 的值会触发 setters 里面的方法,从而在里面可以获取新的值(做一些逻辑操作) + + ``` + var getters = { + computedCount: (state) => { + return state.count*2 + } + } + ``` + + - action 类似于 mutation ,在外面使用的时候用 this.$state.dispath('方法名') diff --git a/第二部分 Web 前端/2.18.md b/第二部分 Web 前端/2.18.md new file mode 100644 index 0000000..f21de45 --- /dev/null +++ b/第二部分 Web 前端/2.18.md @@ -0,0 +1,604 @@ +# 反爬技术研究 + +> 对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App,短期内成为你的劲敌。 + + + + +# 爬虫手段 +- 目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本 +- 有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item,将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID),这个步骤就可以拦截掉一部分的爬虫开发者 + + + +# 制定出**Web 端反爬技术方案** + +本人从这2个角度(网页所见非所得、查接口请求没用)出发,制定了下面的反爬方案。 + + + +- 使用HTTPS 协议 + +- 单位时间内限制掉请求次数过多,则封锁该账号 + +- 前端技术限制 (接下来是核心技术) + +```markdown +# 比如需要正确显示的数据为“19950220” + +1. 先按照自己需求利用相应的规则(数字乱序映射,比如正常的0对应还是0,但是乱序就是 0 <-> 1,1 <-> 9,3 <-> 8,...)制作自定义字体(ttf) +2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 17730220 +3. 对于第一步得到的字符串,依次遍历每个字符,将每个字符根据按照线性变换(y=kx+b)。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”,那么线性变换的 k 为 7,b 为 24。 +4. 然后将变换后的每个字符串用“3.1415926”拼接返回给接口调用者。(为什么是3.1415926,因为对数字伪造反爬,所以拼接的文本肯定是数字的话不太会引起研究者的注意,但是数字长度太短会误伤正常的数据,所以用所熟悉的 Π) + +​``` +1773 -> “1*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “3*7+24” -> 313.1415926733.1415926733.141592645 +02 -> "0*7+24" + "3.1415926" + "2*7+24" -> 243.141592638 +20 -> "2*7+24" + "3.1415926" + "0*7+24" -> 383.141592624 +​``` + +# 前端拿到数据后再解密,解密后根据自定义的字体 Render 页面 +1. 先将拿到的字符串按照“3.1415926”拆分为数组 +2. 对数组的每1个数据,按照“线性变换”(y=kx+b,k和b同样按照当前的日期求解得到),逆向求解到原本的值。 +3. 将步骤2的的到的数据依次拼接,再根据 ttf 文件 Render 页面上。 +``` + +- 后端需要根据上一步设计的协议将数据进行加密处理 + +下面以 **Node.js** 为例讲解后端需要做的事情 + +- 首先后端设置接口路由 + +- 获取路由后面的参数 + +- 根据业务需要根据 SQL 语句生成对应的数据。如果是数字部分,则需要按照上面约定的方法加以转换。 + +- 将生成数据转换成 JSON 返回给调用者 + + ```js + // json + var JoinOparatorSymbol = "3.1415926"; + function encode(rawData, ruleType) { + if (!isNotEmptyStr(rawData)) { + return ""; + } + var date = new Date(); + var year = date.getFullYear(); + var month = date.getMonth() + 1; + var day = date.getDate(); + + var encodeData = ""; + for (var index = 0; index < rawData.length; index++) { + var datacomponent = rawData[index]; + if (!isNaN(datacomponent)) { + if (ruleType < 3) { + var currentNumber = rawDataMap(String(datacomponent), ruleType); + encodeData += (currentNumber * month + day) + JoinOparatorSymbol; + } + else if (ruleType == 4) { + encodeData += rawDataMap(String(datacomponent), ruleType); + } + else { + encodeData += rawDataMap(String(datacomponent), ruleType) + JoinOparatorSymbol; + } + } + else if (ruleType == 4) { + encodeData += rawDataMap(String(datacomponent), ruleType); + } + + } + if (encodeData.length >= JoinOparatorSymbol.length) { + var lastTwoString = encodeData.substring(encodeData.length - JoinOparatorSymbol.length, encodeData.length); + if (lastTwoString == JoinOparatorSymbol) { + encodeData = encodeData.substring(0, encodeData.length - JoinOparatorSymbol.length); + } + } + ``` + + ```javascript + //字体映射处理 + function rawDataMap(rawData, ruleType) { + + if (!isNotEmptyStr(rawData) || !isNotEmptyStr(ruleType)) { + return; + } + var mapData; + var rawNumber = parseInt(rawData); + var ruleTypeNumber = parseInt(ruleType); + if (!isNaN(rawData)) { + lastNumberCategory = ruleTypeNumber; + //字体文件1下的数据加密规则 + if (ruleTypeNumber == 1) { + if (rawNumber == 1) { + mapData = 1; + } + else if (rawNumber == 2) { + mapData = 2; + } + else if (rawNumber == 3) { + mapData = 4; + } + else if (rawNumber == 4) { + mapData = 5; + } + else if (rawNumber == 5) { + mapData = 3; + } + else if (rawNumber == 6) { + mapData = 8; + } + else if (rawNumber == 7) { + mapData = 6; + } + else if (rawNumber == 8) { + mapData = 9; + } + else if (rawNumber == 9) { + mapData = 7; + } + else if (rawNumber == 0) { + mapData = 0; + } + } + //字体文件2下的数据加密规则 + else if (ruleTypeNumber == 0) { + + if (rawNumber == 1) { + mapData = 4; + } + else if (rawNumber == 2) { + mapData = 2; + } + else if (rawNumber == 3) { + mapData = 3; + } + else if (rawNumber == 4) { + mapData = 1; + } + else if (rawNumber == 5) { + mapData = 8; + } + else if (rawNumber == 6) { + mapData = 5; + } + else if (rawNumber == 7) { + mapData = 6; + } + else if (rawNumber == 8) { + mapData = 7; + } + else if (rawNumber == 9) { + mapData = 9; + } + else if (rawNumber == 0) { + mapData = 0; + } + } + //字体文件3下的数据加密规则 + else if (ruleTypeNumber == 2) { + + if (rawNumber == 1) { + mapData = 6; + } + else if (rawNumber == 2) { + mapData = 2; + } + else if (rawNumber == 3) { + mapData = 1; + } + else if (rawNumber == 4) { + mapData = 3; + } + else if (rawNumber == 5) { + mapData = 4; + } + else if (rawNumber == 6) { + mapData = 8; + } + else if (rawNumber == 7) { + mapData = 3; + } + else if (rawNumber == 8) { + mapData = 7; + } + else if (rawNumber == 9) { + mapData = 9; + } + else if (rawNumber == 0) { + mapData = 0; + } + } + else if (ruleTypeNumber == 3) { + + if (rawNumber == 1) { + mapData = ""; + } + else if (rawNumber == 2) { + mapData = ""; + } + else if (rawNumber == 3) { + mapData = ""; + } + else if (rawNumber == 4) { + mapData = ""; + } + else if (rawNumber == 5) { + mapData = ""; + } + else if (rawNumber == 6) { + mapData = ""; + } + else if (rawNumber == 7) { + mapData = ""; + } + else if (rawNumber == 8) { + mapData = ""; + } + else if (rawNumber == 9) { + mapData = ""; + } + else if (rawNumber == 0) { + mapData = ""; + } + } + else{ + mapData = rawNumber; + } + } else if (ruleTypeNumber == 4) { + var sources = ["年", "万", "业", "人", "信", "元", "千", "司", "州", "资", "造", "钱"]; + //判断字符串为汉字 + if (/^[\u4e00-\u9fa5]*$/.test(rawData)) { + + if (sources.indexOf(rawData) > -1) { + var currentChineseHexcod = rawData.charCodeAt(0).toString(16); + var lastCompoent; + var mapComponetnt; + var numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; + var characters = ["a", "b", "c", "d", "e", "f", "g", "h", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]; + + if (currentChineseHexcod.length == 4) { + lastCompoent = currentChineseHexcod.substr(3, 1); + var locationInComponents = 0; + if (/[0-9]/.test(lastCompoent)) { + locationInComponents = numbers.indexOf(lastCompoent); + mapComponetnt = numbers[(locationInComponents + 1) % 10]; + } + else if (/[a-z]/.test(lastCompoent)) { + locationInComponents = characters.indexOf(lastCompoent); + mapComponetnt = characters[(locationInComponents + 1) % 26]; + } + mapData = "&#x" + currentChineseHexcod.substr(0, 3) + mapComponetnt + ";"; + } + } else { + mapData = rawData; + } + + } + else if (/[0-9]/.test(rawData)) { + mapData = rawDataMap(rawData, 2); + } + else { + mapData = rawData; + } + + } + return mapData; + } + ``` + + ```javascript + //api + module.exports = { + "GET /api/products": async (ctx, next) => { + ctx.response.type = "application/json"; + ctx.response.body = { + products: products + }; + }, + + "GET /api/solution1": async (ctx, next) => { + + try { + var data = fs.readFileSync(pathname, "utf-8"); + ruleJson = JSON.parse(data); + rule = ruleJson.data.rule; + } catch (error) { + console.log("fail: " + error); + } + + var data = { + code: 200, + message: "success", + data: { + name: "@杭城小刘", + year: LBPEncode("1995", rule), + month: LBPEncode("02", rule), + day: LBPEncode("20", rule), + analysis : rule + } + } + + ctx.set("Access-Control-Allow-Origin", "*"); + ctx.response.type = "application/json"; + ctx.response.body = data; + }, + + + "GET /api/solution2": async (ctx, next) => { + try { + var data = fs.readFileSync(pathname, "utf-8"); + ruleJson = JSON.parse(data); + rule = ruleJson.data.rule; + } catch (error) { + console.log("fail: " + error); + } + + var data = { + code: 200, + message: "success", + data: { + name: LBPEncode("建造师",rule), + birthday: LBPEncode("1995年02月20日",rule), + company: LBPEncode("中天公司",rule), + address: LBPEncode("浙江省杭州市拱墅区石祥路",rule), + bidprice: LBPEncode("2万元",rule), + negative: LBPEncode("2018年办事效率太高、负面基本没有",rule), + title: LBPEncode("建造师",rule), + honor: LBPEncode("最佳奖",rule), + analysis : rule + } + } + ctx.set("Access-Control-Allow-Origin", "*"); + ctx.response.type = "application/json"; + ctx.response.body = data; + }, + + "POST /api/products": async (ctx, next) => { + var p = { + name: ctx.request.body.name, + price: ctx.request.body.price + }; + products.push(p); + ctx.response.type = "application/json"; + ctx.response.body = p; + } + }; + ``` + + ```javascript + //路由 + const fs = require("fs"); + + function addMapping(router, mapping){ + for(var url in mapping){ + if (url.startsWith("GET")) { + var path = url.substring(4); + router.get(path,mapping[url]); + console.log(`Register URL mapping: GET: ${path}`); + }else if (url.startsWith('POST ')) { + var path = url.substring(5); + router.post(path, mapping[url]); + console.log(`Register URL mapping: POST ${path}`); + } else if (url.startsWith('PUT ')) { + var path = url.substring(4); + router.put(path, mapping[url]); + console.log(`Register URL mapping: PUT ${path}`); + } else if (url.startsWith('DELETE ')) { + var path = url.substring(7); + router.del(path, mapping[url]); + console.log(`Register URL mapping: DELETE ${path}`); + } else { + console.log(`Invalid URL: ${url}`); + } + + } + } + + + function addControllers(router, dir){ + fs.readdirSync(__dirname + "/" + dir).filter( (f) => { + return f.endsWith(".js"); + }).forEach( (f) => { + console.log(`Process controllers:${f}...`); + let mapping = require(__dirname + "/" + dir + "/" + f); + addMapping(router,mapping); + }); + } + + module.exports = function(dir){ + let controllers = dir || "controller"; + let router = require("koa-router")(); + + addControllers(router,controllers); + return router.routes(); + }; + + + ``` + + + +- 前端根据服务端返回的数据逆向解密 + + ```javascript + $("#year").html(getRawData(data.year,log)); + + // util.js + var JoinOparatorSymbol = "3.1415926"; + function isNotEmptyStr($str) { + if (String($str) == "" || $str == undefined || $str == null || $str == "null") { + return false; + } + return true; + } + + function getRawData($json,analisys) { + $json = $json.toString(); + if (!isNotEmptyStr($json)) { + return; + } + + var date= new Date(); + var year = date.getFullYear(); + var month = date.getMonth() + 1; + var day = date.getDate(); + var datacomponents = $json.split(JoinOparatorSymbol); + var orginalMessage = ""; + for(var index = 0;index < datacomponents.length;index++){ + var datacomponent = datacomponents[index]; + if (!isNaN(datacomponent) && analisys < 3){ + var currentNumber = parseInt(datacomponent); + orginalMessage += (currentNumber - day)/month; + } + else if(analisys == 3){ + orginalMessage += datacomponent; + } + else{ + //其他情况待续,本 Demo 根据本人在研究反爬方面的技术并实践后持续更新 + } + } + return orginalMessage; + } + + ``` + + + +比如后端返回的是323.14743.14743.1446,根据我们约定的算法,可以的到结果为1773 + +- 根据 ttf 文件 Render 页面 + ![自定义字体文件](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180724-184215@2x.png) + 上面计算的到的1773,然后根据ttf文件,页面看到的就是1995 + +- 然后为了防止爬虫人员查看 JS 研究问题,所以对 JS 的文件进行了加密处理。如果你的技术栈是 Vue 、React 等,webpack 为你提供了 JS 加密的插件,也很方便处理 + + [JS混淆工具](http://www.javascriptobfuscator.com/Javascript-Obfuscator.aspx) + +- 个人觉得这种方式还不是很安全。于是想到了各种方案的组合拳。比如 + + + + +##  反爬升级版 + +个人觉得如果一个前端经验丰富的爬虫开发者来说,上面的方案可能还是会存在被破解的可能,所以在之前的基础上做了升级版本 + +1. 组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模,有4种值 0、1、2、3。这4种值对应不同的字体文件,所以当爬虫绞尽脑汁爬到1种情况下的字体时,没想到再次请求,字体文件的规则变掉了 😂 +2. 组合拳2: 前面的规则是字体问题乱序,但是只是数字匹配打乱掉。比如 **1** -> **4**, **5** -> **8**。接下来的套路就是每个数字对应一个 **unicode 码** ,然后制作自己需要的字体,可以是 .ttf、.woff 等等。 + +![网页检察元素得到的效果](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180726-161418.png) +![接口返回数据](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180726-161429.png) + +这几种组合拳打下来。对于一般的爬虫就放弃了。 + + + + + +## 反爬手段再升级 + +上面说的方法主要是针对**数字**做的反爬手段,如果要对汉字进行反爬怎么办?接下来提供几种方案 + +1. **方案1:** 对于你站点频率最高的词云,做一个汉字映射,也就是自定义字体文件,步骤跟数字一样。先将常用的汉字生成对应的 ttf 文件;根据下面提供的链接,将 ttf 文件转换为 svg 文件,然后在下面的“字体映射”链接点进去的网站上面选择前面生成的 svg 文件,将svg文件里面的每个汉字做个映射,也就是将汉字专为 unicode 码(注意这里的 unicode 码不要去在线直接生成,因为直接生成的东西也就是有规律的。我给的做法是先用网站生成,然后将得到的结果做个简单的变化,比如将“e342”转换为 “e231”);然后接口返回的数据按照我们的这个字体文件的规则反过去映射出来。 + +2. **方案2:** 将网站的重要字体,将 html 部分生成图片,这样子爬虫要识别到需要的内容成本就很高了,需要用到 OCR。效率也很低。所以可以拦截钓一部分的爬虫 + +3. **方案3:** 看到携程的技术分享“反爬的最高境界就是 Canvas 的指纹,原理是不同的机器不同的硬件对于 Canvas 画出的图总是存在像素级别的误差,因此我们判断当对于访问来说大量的 canvas 的指纹一致的话,则认为是爬虫,则可以封掉它”。 + + 本人将方案1实现到 Demo 中了。 + +### 关键步骤 + +1. 先根据你们的产品找到常用的关键词,生成**词云** +2. 根据词云,将每个字生成对应的 unicode 码 +3. 将词云包括的汉字做成一个字体库 +4. 将字体库 .ttf 做成 svg 格式,然后上传到 [icomoon](https://icomoon.io/app/#/select/font) 制作自定义的字体,但是有规则,比如 **“年”** 对应的 **unicode 码**是 **“\u5e74”** ,但是我们需要做一个 **恺撒加密** ,比如我们设置 **偏移量** 为1,那么经过**恺撒加密** **“年”**对应的 **unicode** 码是**“\u5e75”** 。利用这种规则制作我们需要的字体库 +5. 在每次调用接口的时候服务端做的事情是:服务端封装某个方法,将数据经过方法判断是不是在词云中,如果是词云中的字符,利用规则(找到汉字对应的 unicode 码,再根据凯撒加密,设置对应的偏移量,Demo 中为1,将每个汉字加密处理)加密处理后返回数据 +6. 客户端做的事情: + - 先引入我们前面制作好的汉字字体库 + - 调用接口拿到数据,显示到对应的 Dom 节点上 + - 如果是汉字文本,我们将对应节点的 css 类设置成汉字类,该类对应的 font-family 是我们上面引入的汉字字体库 + +```css +//style.css +@font-face { + font-family: "NumberFont"; + src: url('http://127.0.0.1:8080/Util/analysis'); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@font-face { + font-family: "CharacterFont"; + src: url('http://127.0.0.1:8080/Util/map'); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h2 { + font-family: "NumberFont"; +} + +h3,a{ + font-family: "CharacterFont"; +} +``` + + +![接口效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095006%402x.png) +![审查元素效果](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095124%402x.png) + + +### 传送门 + +[字体制作的步骤](https://blog.csdn.net/fdipzone/article/details/68166388)、[ttf转svg](https://everythingfonts.com/ttf-to-svg)、[字体映射规则](https://icomoon.io/app/#/select/font) + + +## 实现的效果 + 1. 页面上看到的数据跟审查元素看到的结果不一致 + 2. 去查看接口数据跟审核元素和界面看到的三者不一致 + 3. 页面每次刷新之前得出的结果更不一致 + 4. 对于数字和汉字的处理手段都不一致 + + 这几种组合拳打下来。对于一般的爬虫就放弃了。 + + + + ![数字反爬-网页显示效果、审查元素、接口结果情况1](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180810-151046@2x.png) + ![数字反爬-网页显示效果、审查元素、接口结果情况2](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WX20180810-151203@2x.png) + ![数字反爬-网页显示效果、审查元素、接口结果情况3](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180810-151239@2x.png?raw=true) + ![数字反爬-网页显示效果、审查元素、接口结果情况4](https://github.com/FantasticLBP/knowledge-kit/blob/master/assets/WX20180810-151308@2x.png?raw=true) + ![汉字反爬-网页显示效果、审查元素、接口结果情况1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095006%402x.png) + ![汉字反爬-网页显示效果、审查元素、接口结果情况2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180810-095124%402x.png) + +
    + +前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,下面贴出个新的链接。 + +[ttf转svg](https://convertio.co/zh/font-converter/) + + + +## [Demo 地址](https://github.com/FantasticLBP/Anti-WebSpider) + + ![效果演示](https://raw.githubusercontent.com/FantasticLBP/Anti-WebSpider/master/Anti-WebSpider.gif) + + + + 运行步骤 + +```powershell +//客户端。先查看本机 ip 在 Demo/Spider-develop/Solution/Solution1.js 和 Demo/Spider-develop/Solution/Solution2.js 里面将接口地址修改为本机 ip + +$ cd Demo +$ ls +REST Spider-release file-Server.js +Spider-develop Util rule.json +$ node file-Server.js +Server is runnig at http://127.0.0.1:8080/ + +//服务端 先安装依赖 +$ cd REST/ +$ npm install +$ node app.js +``` diff --git a/第二部分 Web 前端/2.19.md b/第二部分 Web 前端/2.19.md new file mode 100644 index 0000000..5d6de55 --- /dev/null +++ b/第二部分 Web 前端/2.19.md @@ -0,0 +1,1079 @@ +# webpack 从入门到精通 + +## 小实验 + +我们一步步打包一个小项目看看 webpack 是如何工作的。 + +1. 先写一个 hello.js + + ```javascript + function hello(messgae){ + alert(messgae); + } + ``` + +2. 然后对其打包,发现终端报错。解决后知道在 webpack 2.0 的时候,我们打包一个 js 文件可能是这样的,比如将 hello.js 打包为 hello.bundle.js 。 + + ```powershell + webpack hello.js hello.bundle.js + ``` + + 但是在现在 webpack 4.5.0 的时候就需要指定 mode 和输出路径 + + ```powershell + webpack --mode=development hello.js --output-file hello.bundle.js + ``` + + mode 有指定的3种值, development、production、none。区别在于 development 打包出来的东西是没压缩的、可读的,production 打包出来的是压缩的、不可读的。 + +3. 然后编写一个 world.js 和 一个 style.css ,然后进行打包 + + ```javascript + require('./world.js'); + require('./style.css'); + + function hello(messgae){ + alert(messgae); + } + hello("hello webpack"); + ``` + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-095124@2x.png) + + 通过报错信息知道, webpack 对于 css 文件并不是默认支持的,需要指定相应的 loader 对其打包。 + +4. 所以我们继续安装 css-loader、style-loader。然后指定 css 文件的 loader 为 css-loader + + ```javascript + require('./world.js'); + require('css-loader!./style.css'); + + function hello(messgae){ + alert(messgae); + } + + hello("hello webpack"); + ``` + +5. 接下来设置页面的背景颜色,发现网页并没有生效。这是因为 webpack 并不知道我们的样式如何作用到 html 中,所以我们需要指定 style-loader + + ```css + //style.css + body,html{ + margin: 0; + padding: 0; + } + + body{ + font-size: 17px; + background: burlywood; + } + ``` + + ```javascript + //hello.js + require('./world.js'); + require('style-loader!css-loader!./style.css'); + + function hello(messgae){ + alert(messgae); + } + + hello("hello webpack"); + ``` + +6. 查看网页效果。发现函数确实执行了,背景颜色也生效了,我们写的 css 代码新建了一个 **style标签** 被直接写入到 html 中了。 + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-100727@2x.png) + +7. 说说2个 loader 的作用。 + + * css-loader 就是 webpack 可以处理 css 文件。 + * style-loader 的作用就是将 css-loader 处理完的文件新建一个 style 标签插入到 html 中 + +8. 很多人会想 **require\('style-loader!css-loader!./style.css'\);** 我每次写一个 css 文件,那么都需要在前面加入 **style-loader、css-loader** 吗?显然不是,webpack 还为我们提供了简单写法 + + ```javascript + require('./world.js'); + // require('style-loader!css-loader!./style.css'); + require('./style.css'); + + function hello(messgae){ + alert(messgae); + } + + hello("hello webpack"); + ``` + + webpack-cli 写法为 + + ```powershell + webpack --mode=development hello.js --output-file hello.bundle.js --module-bind 'css=style-loader!css-loader' + ``` + +9. 之前的做法还存在一个弊端,就是每次修改了代码,我们都需要在终端重新运行打包命令,十分繁琐。这里强大的 webpack 为我们提供了一个 option,可以监听代码改变然后自动打包。如下 + + ```powershell + webpack --mode=development hello.js --output-file hello.bundle.js --module-bind 'css=style-loader!css-loader' --watch + ``` + + 这样,我们在源代码每次一修改,webpack 会自动打包 + +10. 如果你想看到打包过程,那么可以使用 **pregress** 参数。这样在打包的时候可以看到左下角有构件的进度 + + ```powershell + webpack --mode=development hello.js --output-file hello.bundle.js --module-bind 'css=style-loader!css-loader' --progress + ``` + +11. 如果像看到打包的模块,可以使用 **--display-modules** + + ```powershell + webpack --mode=development hello.js --output-file hello.bundle.js --module-bind 'css=style-loader!css-loader' --progress --display-modules + ``` + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-103343@2x.png) + +12. 如果想知道打包某个模块的原因,可以使用 **--display-reasons** + + ```powershell + webpack --mode=development hello.js --output-file hello.bundle.js --module-bind 'css=style-loader!css-loader' --progress --display-modules --display-reasons + ``` + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180802-103717@2x.png) + +## 用 webpack.config.js 完成上述步骤 + +1. 初始化项目,编辑 webpack.config.js + + ```powershell + var path = require('path'); + + module.exports = { + entry: './src/script/main.js', + output:{ + path: path.resolve(__dirname,'./dist/js'), + filename: 'bundle.js' + } + } + ``` + +2. 有了 webpack.config.js 文件,就不需要和上面的方式一样,指定对应的 configuration option。在终端运行 **webpack --mode=development ** + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-110653@2x.png) + +3. 注意:如果我们将 webpack.config.js 改名为 webpack.dev.config.js ,然后在命令行打包,会发现没效果。 + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-110938@2x.png) + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-111011@2x.png) + + 要将 webpack.dev.config.js 同样生效,我们需要在命令行使用下面命令。 + + ```powershell + webpack --mode=development --config webpack.dev.config.js + ``` + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-111143@2x.png) + +4. 如果想像上个实验一样,看到打包时候的一些信息,怎么办呢? + + 可以配合 npm 的 package.json 文件中的 scripts 标签,在下面添加 key 为 webpack 的项,然后将命令写到后边。然后在命令行运行 **npm run webpack** + + ``` + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "webpack": "webpack --config webpack.config.js --mode=development --progress --display-modules --display-reasons --colors" + }, + ``` + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-113022@2x.png) + +5. 对于 webpack 的 entrt 主要有3种写法,每种写法都有不同区别。 + + * 如果 webpack 只有单一入口,那么就可以是字符串。 + + ```json + entry: './src/script/main.js', + ``` + + * 如果 webpack 有多个入口,那么就可以是数组。 + + ```json + entry: ['./src/script/main.js','./src/script/a.js'], + ``` + + * 如果 webpack 有多个入口,那么可以用对象。 + + ```json + entry: { + main: './src/script/main.js', + a : './src/script/a.js' + }, + ``` + +6. 如果指定了多个入口,那么执行打包会报错,因为 webpack 文档说如果多个 entry,且只有一个 output 的 filename,那么打包的结果会覆盖。所以我们需要设置如下 + + > When combining with the [`output.library`](https://webpack.js.org/configuration/output#output-library) option: If an array is passed only the last item is exported. + + ```json + var path = require('path'); + + module.exports = { + entry: { + main: './src/script/main.js', + a : './src/script/a.js' + }, + output:{ + path: path.resolve(__dirname,'./dist/js'), + filename: '[name]-[hash].js' + } + } + ``` + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-120033@2x.png) + + 将文件修改为 **filename: '\[name\]-\[chunkhash\].js'** + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/QQ20180802-120113@2x.png) + + 会发现 hash 和 chunkhash 的输出的文件名并不一样 + + 说明: chunkhash 是根据文件的内容生成的唯一标示(类似于md5生成的唯一标示、文件版本号)。如果一个资源在打包前后文本没有变过的话,二次打包的生成的 chunkhash 是一致的。 + +## 生成项目中 html 页面文件 + +对于生成的的 js,我们 html 如何使用呢?难道每次一打包,html 中的 script 需要修改 src 吗?不是的,webpack 提供了 html-webpack-plugin + +1. 安装 + + ```powershell + npm install html-webpack-plugin --save-dev + ``` + +2. 然后运行命令,将现有的 js 打包引入到 html 文件中 + + ```json + var htmlWebpackPlugin = require('html-webpack-plugin'); + //... + plugins: [ + new htmlWebpackPlugin() + ] + ``` + + 然后生成的文件是 webpack 帮我们生成的 html 文件。当然我们可以新建一个自己的 html 作为模版。 + + ```json + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html' + }) + ] + ``` + + ```html + //模版 + + + webpack demo + + +

    我是 webpack 生成 html 的模版

    + + + + //打包生成的 + + + webpack demo + + +

    我是 webpack 生成 html 的模版

    + + + + ``` + + 说明:上面选中的 template 写了 index.html 就会找到合适的文件是因为 webpack 有个上下文参数 context,会根据上下文找到对应的 html(这里就是根目录) + +3. 上述的缺点是生成的 html 也会放在 dist/js 目录下。 + + ![](https://github.com/FantasticLBP/knowledge-kit/raw/master/WX20180802-151750@2x.png) + + 需要做到的效果就是 html 放在根目录, js 放在 dist 目录下的 js 目录下。需要对 webpack.config.js 的 output 属性做修改 + +4. ```js + output:{ + path: path.resolve(__dirname,'./dist'), + filename: 'js/[name]-[chunkhash].js' + }, + ``` +5. plugins 的参数很多可以自定义 + + ```json + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'index-[hash].html', + inject: 'head' + }) + ] + ``` + + * template: 指定生成 html 的模版 + * filename:指定生成 html 的命名规则 + * inject :指定生成 js 的 script 插入的位置。head、body + +6. 如果想通过 plugins 传值到 生成的 html,怎么办? + + * htmlWebpackPlugin.options 对象就可以拿到传递过来的值 + * <%= htmlWebpackPlugin.options.title %> 模版语法来拿值 + + ```json + //webpack.config.js + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'index.html', + inject: 'head', + title: 'Webpack is awesome', + date : new Date() + }) + ] + ``` + + ```html + // 模版 html + + + <%= htmlWebpackPlugin.options.title %> + + +

    我是 webpack 生成 html 的模版

    +

    时间:<%= htmlWebpackPlugin.options.date %>

    + + + + //生成的html + + + Webpack is awesome + + +

    我是 webpack 生成 html 的模版

    +

    时间:Thu Aug 02 2018 15:40:50 GMT+0800 (CST)

    + + + ``` + +7. 我们很好奇 html-webpack-plugin 可以传递什么参数?或者这个对象包含什么信息。做个测试就知道了 + + ```html + //模版 html + <% for (var key in htmlWebpackPlugin){ %> + <%= key %> + <% } %> + + //生成的 html + files + options + ``` + + 看到最外层的节点就2个:files、options。那么我们分别对这2个节点遍历输出。因为遍历出的 value (**htmlWebpackPlugin.files\[key\] **)可能是对象、数组。所以用 **JSON.Stringfy\(htmlWebpackPlugin.files\[key\]\)** 打印 + + ```html + + + <%= htmlWebpackPlugin.options.title %> + + +

    我是 webpack 生成 html 的模版

    +

    时间:<%= htmlWebpackPlugin.options.date %>

    + <% for (var key in htmlWebpackPlugin.files){ %> + <%= key %> <%= JSON.stringify(htmlWebpackPlugin.files[key]) %> + <% } %> +
    + <% for (var key in htmlWebpackPlugin.options){ %> + <%= key %> <%= JSON.stringify(htmlWebpackPlugin.options[key]) %> + <% } %> + + + + //生成的 html + + + Webpack is awesome + + +

    我是 webpack 生成 html 的模版

    +

    时间:Thu Aug 02 2018 15:51:27 GMT+0800 (CST)

    + + publicPath "" + + chunks {"main":{"size":28,"entry":"js/main-82c7521f0a4a776cc00b.js","hash":"82c7521f0a4a776cc00b","css":[]},"a":{"size":18,"entry":"js/a-273641522fd044fc27c7.js","hash":"273641522fd044fc27c7","css":[]}} + + js ["js/main-82c7521f0a4a776cc00b.js","js/a-273641522fd044fc27c7.js"] + + css [] + + manifest + +
    + + template "/Users/liubinpeng/Desktop/webpackdemo/Demo2/node_modules/html-webpack-plugin/lib/loader.js!/Users/liubinpeng/Desktop/webpackdemo/Demo2/index.html" + + templateParameters + + filename "index.html" + + hash false + + inject "head" + + compile true + + favicon false + + minify false + + cache true + + showErrors true + + chunks "all" + + excludeChunks [] + + chunksSortMode "auto" + + meta {} + + title "Webpack is awesome" + + xhtml false + + date "2018-08-02T07:51:27.110Z" + + + + ``` + +8. 有时候我们想把部分 js 放到 head ,部分 js 放到 body 中。单独通过 webpack.config.js 是没办法实现这个目的,结合上面的成果,我们可以拿到 **htmlWebpackPlugin.files.chunks** 属性,比如将 a.js 放到 head 标签,main.js 放到 body 标签。 + + ```json + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'index.html', + inject: false, + title: 'Webpack is awesome', + date : new Date() + }) + ] + ``` + + ```html + //模版 html + + + <%= htmlWebpackPlugin.options.title %> + + + +

    我是 webpack 生成 html 的模版

    +

    时间:<%= htmlWebpackPlugin.options.date %>

    + + + + ``` + + ```html + //生成 html + + + Webpack is awesome + + + +

    我是 webpack 生成 html 的模版

    +

    时间:Thu Aug 02 2018 15:58:56 GMT+0800 (CST)

    + + + + ``` + + 需要注意的是当自定义 js 文件的位置的时候,需要将 webpack.config.js 中 plugins 下的 inject 设置为 false + +9. 接下来看到的这种需求绝对很有料。前面我们看到的都是相对路径,但是我们的产品需要上线,所以我们的 js 文件资源路径需要改变。如下: + + ```json + //webpack.config.js + output:{ + path: path.resolve(__dirname,'./dist'), + filename: 'js/[name]-[chunkhash].js', + publicPath: 'http://test.lbp.com' + }, + ``` + + ```html + //生成的 html + + + Webpack is awesome + + + +

    我是 webpack 生成 html 的模版

    +

    时间:Thu Aug 02 2018 16:25:12 GMT+0800 (CST)

    + + + + ``` + +10. 利用 webpack 我们还可以打包好的 html 做一些优化,比如删除注释、去掉空格. + + 修改 webpack.config.js 中 plugins 节点下的 htmlWebpackPlugin 的 minify 属性 + + ```json + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'index.html', + inject: false, + title: 'Webpack is awesome', + date : new Date(), + minify:{ + removeComments: true, + collapseWhitespace: true + } + }) + ], + ``` + + 我们对 模版 html 写一些注释,运行 npm run webpack 后看到生成的页面中注释、空格都被去掉了。 + +11. 如果想打包生成多个 html 怎么办?可能使用 plugins 下的 new htmlWebpackPlugin\(\) 多来几组配置项 + + ```json + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'a.html', + title: 'this is a.html', + chunks: ['main','a'], + inject: 'body' + }), + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'b.html', + title: 'this is b.html', + chunks: ['main','b'], + inject: 'body' + }), + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'c.html', + title: 'this is c.html', + chunks: ['main','c'], + inject: 'body' + }) + ] + ``` + + 注意:这里我们可以指定每个生成的 filename 以及 title。实现上述需求关键点在于 **chunks** 这个属性。用一个数组的形式来指定需要引用的 chunk。 + +12. 上面只是实现了 a、b、c 3个页面,如果多了的话按照上面的写法要烦死人的。 webpack 为我们提供了 **excludeChunks** 这个属性,它指定了不需要包含的chunk。上面写法的另一种写法 + + ```json + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'a.html', + title: 'this is a.html', + // chunks: ['main','a'], + inject: 'body', + excludeChunks: ['b','c'] + }), + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'b.html', + title: 'this is b.html', + //chunks: ['main','b'], + inject: 'body', + excludeChunks: ['a','c'] + }), + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'c.html', + title: 'this is c.html', + // chunks: ['main','c'], + inject: 'body', + excludeChunks: ['a','b'] + }) + ], + ``` + +13. 有种需求是:当我们需要减小首页 HTTP 请求(提高首页的渲染速度),也就是将一些首页必须用到的 JS 文件用内联的方式写在首页 html 的 script 标签里面,不重要的 js 文件通过 script 的 src 引入。要怎么做呢?webpack 在设计之初没想到这种需求,很多人在 github 提了很多 issue ,官方认识到这种需求,所以在后期更新的 demo 中看到了解决方案。 + + * htmlWebpackPlugin.files.chunks 对象拿到的是我们在 webpack.config.js 设置过 publicPath 生成的完整路径 + + * 通过截取字符串子串的方式拿到文件地址。 **htmlWebpackPlugin.files.chunks.main.entry.substr\(htmlWebpackPlugin.files.publicPath.length\) ** + + * [官方的解决方案](https://github.com/jantimon/html-webpack-plugin/blob/master/examples/inline/template.jade) + + ```jade + compilation.assets[jsFile.substr(htmlWebpackPlugin.files.publicPath.length)].source() + ``` + + 在我们的项目中加以改造,为生成的每个页面的 header 里面加入 main.js。在 body 部分加入除了 main.js 之外的其他 js。 + + ```html + //模版html + + + <%= htmlWebpackPlugin.options.title %> + + + + +

    我是 webpack 生成 html 的模版

    + <% for(var key in htmlWebpackPlugin.files.chunks){ %> + <% if( key !== 'main'){ %> + + <% } %> + <% } %> + + + ``` + + ```js + //webpack.config.js + var htmlWebpackPlugin = require('html-webpack-plugin'); + var path = require('path'); + + module.exports = { + // entry: './src/script/main.js', + // entry: ['./src/script/main.js','./src/script/a.js'], + entry: { + main: './src/script/main.js', + a : './src/script/a.js', + b : './src/script/b.js', + c : './src/script/c.js' + }, + output:{ + path: path.resolve(__dirname,'./dist'), + filename: 'js/[name]-[hash].js', + publicPath: 'http://test.lbp.com' + }, + plugins: [ + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'a.html', + title: 'this is a.html', + inject: false, + excludeChunks: ['b','c'] + }), + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'b.html', + title: 'this is b.html', + inject: false, + excludeChunks: ['a','c'] + }), + new htmlWebpackPlugin({ + template: 'index.html', + filename: 'c.html', + title: 'this is c.html', + inject: false, + excludeChunks: ['a','b'] + }) + ], + } + ``` + + 为了验证生效,我将 main.js 加入了 alert + + ```js + //main.js + function helloworld(msg){ + alert(msg); + } + + helloworld("hello webpack"); + ``` + + 看看打包后生成的 a.html + + ```html + + + this is a.html + + + + +

    我是 webpack 生成 html 的模版

    + + + + ``` + + 在浏览器调试后发现页面是可以正常弹出“hello webpack” + +## loader + +新建项目,一步步认识 loader + +1. Js loader + + 我们写的项目中会用 es6,但是并不是所有的浏览器都支持 es6(虽然各个浏览器厂商每年在不断新增对 es6 的支持),所以我们需要使用 babel 将 es6 转换为浏览器都支持的 es2015 。所以使用 babel-loader 的时候需要指定 babel 转换的模式。loader 官方给出了2种方式 + + * 可以直接像 url 的 get 形式一样,将参数传递在后面。 + + ```js + //方式1 + require("babel-loader?presets=latest"); + //方式2 + { + test: /\.png$/, + loader: 'url-loader?presets=latest' + } + ``` + + * 写在 query 参数里面 + + ```js + { + test: /\.js$/, + loader: 'babel', + query: { + presets: ['latest'] + } + } + ``` + + * 其实还有一种方式:在 package.json 文件里面添加一个 key 为 babel。 + + ```json + "babel": { + presets: ['latest'] + } + ``` + + 注意:webpack 现在已经是4.5.0了。以前的版本的写法是 + + ```js + module: { + loaders: [ + { + test: /\.js$/, + loader: 'babel-loader', + query: { + presets: ['latest'] + } + } + ] + }, + ``` + + 现在的写法为 + + ```js + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader', + options: { + presets: ['latest'] + } + } + ] + }, + ``` + + 注意:我们工程如果安装的依赖非常多,node\_modules 文件非常多,babel 转换会很慢,这时候需要指定2个参数可以显著提高速度 + + ```js + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader', + options: { + presets: ['latest'] + }, + exclude: path.resolve(__dirname,'./node_modules/'), + include: path.resolve(__dirname,'./src') + } + ] + }, + ``` + +2. css loader + + 打包 css 经常会用到 css-loader、style-loader。我们经常写 flex 的时候很多浏览器兼容性不一致,所以我们需要加前缀,这时候需要使用 postcss-loader、autoprefixer + + 官网给出2种写法 + + * loader + + ```js + { + test: /\.css$/, + loader: 'style-loader!css-loader!postcss-loader' + } + ``` + + * loaders + + ```js + { + test: /\.css$/, + loaders: ['style-loader','css-loader','postcss-loader'] + } + ``` + + 如果项目中不只是使用了 css 的话,比如还使用了 less 和 sass 的话,我们需要将 css 加额外的设置 + + ```js + { + test: /\.css$/, + use:[ + 'style-loader', + {loader: 'css-loader', options: {importLoaders: 1}}, + { + loader: 'postcss-loader', + options:{ + plugins:function(){ + return [ + require('postcss-import')(), + require('autoprefixer')({browsers:['last 5 versions']}) + ] + } + } + } + ] + }, + { + test: /\.less$/, + use:[ + 'style-loader', + {loader: 'css-loader', options: {importLoaders: 1}}, + { + loader: 'postcss-loader', + options:{ + plugins:function(){ + return [ + require('postcss-import')(), + require('autoprefixer')({browsers:['last 5 versions']}) + ] + } + } + }, + 'less-loader' + ] + }, + ``` + +3. 处理模版 + + 在 webpack 经常打包处理的时候会遇到模版。有普通的 html 模版,也会有 ejs 模式下的 tpl 模版 + + - html 模版 + + ```html + + + <%= htmlWebpackPlugin.options.title %> + + + +
    + +
    + + + ``` + + ```js + //layer.js + import './layer.less' + import tpl from './layer.html' + + function layer(){ + return { + name: 'layer', + tpl: tpl + } + } + + export default layer; + + //app.js + import Layer from './components/layer/layer.js'; + import './css/common.css'; + + + const App = function(){ + var dom = document.getElementById("app"); + var layer = new Layer(); + dom.innerHTML = layer.tpl; + } + + new App() + ``` + + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180803-152208@2x.png) + + + - ejs 模版 + + ```js + //layer.tpl +
    +
    this is <%= name %> layr
    + <% for(var i=0;i < arr.length; i++){ %> + <%= arr[i] %> + <% } %> + +
    + + //layer.js + import './layer.less' + import tpl from './layer.tpl' + + function layer(){ + return { + name: 'layer', + tpl: tpl + } + } + + export default layer; + + + //app.js + import Layer from './components/layer/layer.js'; + import './css/common.css'; + + + const App = function(){ + var dom = document.getElementById("app"); + var layer = new Layer(); + dom.innerHTML = layer.tpl({ + name: 'john', + arr: ['swift','Objective-C','JS','python'] + }); + } + + new App() + ``` + ![](https://fantasticlbp.gitbooks.io/knowledge-kit/content/assets/WX20180803-152612@2x.png) + + + diff --git a/第二部分 Web 前端/2.2.md b/第二部分 Web 前端/2.2.md new file mode 100644 index 0000000..b35d7aa --- /dev/null +++ b/第二部分 Web 前端/2.2.md @@ -0,0 +1,94 @@ + + +* \d :匹配一个数字 +* \w : 匹配任意一个字母或数字 +* . : 可以匹配任意字符串 +* \* : 可以匹配任意个字符(包括0个) +* +: 至少一个字符 +* ? : 表示0个或1个字符 + +* {n} :表示n个字符 + +* {n-m} : 表示n-m个字符 + +* \[ \] :表示范围 + +* \[0-9a-zA-Z\\_\] : 可以匹配一个数字、字母或者下划线 + +* \[0-9a-zA-Z\\_\]+: 可以匹配至少由一个数字、字母或者下划线组成的字符串 + +* \[0-9a-zA-Z\_$\]\[0-9a-zA-Z\_\_$\]\* : 可以匹配由数字、字母或者下划线,后接任意个由一个数字、字母或者下划线、$组成的字符串 + +# RegExp + +JS有2种方式创建一个正则表达式。 + +第一种方式是直接通过/正则表达式/写出来。 + +第二种 是通过new RegExp\('正则表达式'\)创建一个RegExp对象。 + +#### 注意 + +因为第二种的写法问题,所以每个`\` 需要转义,也就是 `\\` + +``` + + + +``` + +# 分组 + +除了简单地判断是否匹配外,正则表达式还可以用来提取分组 ,用 `()` + +``` + + + +``` + +* 如果正则表达式中定义了组,就可以在RegExp对象上用exec\(\)方法提取出子串来。 + +* exec\(\)方法在匹配成功后会返回一个Array,第一个元素为正则表达式匹配到的整个字符串,后面的元素则表示匹配成功的子串。 +* exec\(\)方法在匹配失败后会返回null + + + +# 贪婪匹配 + + + +由于 正则表达式默认使用贪婪匹配模式,因此会造成一些问题。比如 + +``` +var res = /^(\d+)(0*)$/; +res.exec('102300'); //['102300','102300',''] +``` + +由于\d+采用贪婪匹配模式,所以会匹配到后面的0,所以加上\d+?代表使用非贪婪匹配模式 + +``` +var res = /^(\d+?)(0*)$/; +res.exec('102300'); +``` + + + + + + + diff --git a/第二部分 Web 前端/2.20.md b/第二部分 Web 前端/2.20.md new file mode 100644 index 0000000..2dcee32 --- /dev/null +++ b/第二部分 Web 前端/2.20.md @@ -0,0 +1,14 @@ +# 大前端 + +记录大前端领域较好的文章 + +1. [“观点|蚂蚁金服玉伯:我们是如何从前端技术进化到体验科技的?”](https://juejin.im/post/5b6904f1f265da0f48613ab0#comment) +2. [Pattern: Backends For Frontends](https://samnewman.io/patterns/architectural/bff/) +3. [了解 BFF 架构](https://segmentfault.com/a/1190000009558309) + + 什么情况需要 BFF? + - 比如你的后台写好了接口,App 的第一个版本上线后发现现有的数据结构不能满足第二个版本的界面需求了。这时候如果没做版本控制,直接将接口做了修改,那么对于 App 没有升级的用户来说是灾难,接口很可能会挂掉。 + - 如果之前是 iOS 和 Android App,后来加了小程序和 H5 页面,可能需求不一样,这样直接在接口上做修改的话,之前的接口会有非常多的判断代码,逻辑太乱了。一个好的设计是 **“单一原则”**。 后台做基于领域模型的 RPC 接口,前端则根据一个中间服务拿数据,常见的有 Node、PHP、Python提供这种服务, BFF 就是这样一种概念。 + + + diff --git a/第二部分 Web 前端/2.21.md b/第二部分 Web 前端/2.21.md new file mode 100644 index 0000000..c18972f --- /dev/null +++ b/第二部分 Web 前端/2.21.md @@ -0,0 +1,111 @@ +# Canvas + +## 支持性 + +由于浏览器对 Canvas 的支持标准不一致,所以通常 <canvas> 内部添加一些说明行的 HTML 代码,如果浏览器支持 Canvas,它将忽略 <canvas> 内部的 HTML,如果浏览器不支持 Canvas,它将显示 <canvas> 内部的HTML。 + +## 一、基础使用 + +使用 Canvas 前,用 canvas.getContext 来判断浏览器是否支持 Canvas + +``` +var canvas = document.getElementById("test-canvas"); +if (canvas.getContext) { + console.log("你的浏览器支持canvas"); +} else { + console.log("你的浏览器不支持canvas"); +} +``` + +getContext\('2d'\) 方法拿到一个 CanvasRenderingContext2D 对象,所有的绘图操作都需要通过这个对象完成。 + +``` +var ctx = canvas.getContext("2d"); +``` + +如果需要绘制 3D图形,我们可以通过 + +``` +var gl = canvas.getContext("webgl"); +``` + +### 1、绘制形状 + +``` +var ctx = canvas.getContext("2d"); +ctx.fillStyle = "rgb(200,0,0)"; +ctx.fillRect(10,10,50,50); + +ctx.fillStyle = "rgba(0,0,200,0.5)"; +ctx.fillRect(30,30,50,50); +``` + +![](/assets/canvas - 绘制图形1.png) + +* #### 绘制矩形 + +不同于 SVG,Canvas 只提供了一种原生的图形绘制能力:矩形。 所有的其他图形的绘制都至少需要生成一条路径。 + +1. 绘制一个填充矩形 fillRect\(x,y,width,height\) +2. 绘制一个矩形的边框 strokeRect\(x,y,width,height\) +3. 清除矩形的指定区域 clearRect\(x,y,width,height\) + +``` +ctx.fillRect(20,20,160,160); +ctx.clearRect(30,30,140,140); +ctx.strokeRect(80,80,40,40); +``` + +![](/assets/canvas - 绘制图形2.png) + +#### 2、绘制路径 + +图形的基本元素是路径,路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的步骤。 + +* 首先你需要创建路径的起始点 +* 然后使用画图命令去画出路径 +* 之后把路径封闭 +* 一旦路径生成,你就可以通过秒变或者填充路径区域来渲染图形了。 + +beginPath\(\):新建一条路径,生成之后,图形绘制命令被只想到路径上生成路径。 + +closePath\(\):闭合路径之后图形绘制命令又重新指向到上下文中。 + +stroke\(\):通过线条来绘制图形轮廓。给图形描边。 + +fill\(\): 通过填充路径的内容区域生成实心的图形。 + +``` +var ctx = canvas.getContext("2d"); +ctx.beginPath(); +ctx.moveTo(75,50); +ctx.lineTo(100,75); +ctx.lineTo(100,25); +ctx.fillStyle = "red"; +ctx.fill(); +``` + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/第二部分 Web 前端/2.22.md b/第二部分 Web 前端/2.22.md new file mode 100644 index 0000000..223d655 --- /dev/null +++ b/第二部分 Web 前端/2.22.md @@ -0,0 +1,100 @@ +# 动画控制的另一种技术 + + + +> 在 HTML5 的时代里我们可以通过 css3 的 animation 和 kerframes 配合使用动画;也可以使用 css 的 transform 控制动画;在 JS 里面我们通常用 setTimeout 和 setInterval 来控制动画时间。setTimeout 和 setInterval 对于控制动画时间不是很准确,因为它是靠电脑的刷新频率。并且当浏览器切换到其他页面或者最小化的时候动画还在执行并不会停止,显然是在做一些无用功。接下来要介绍一个新的特性,并且主流浏览器都对他进行了支持。 + + + +### requestAnimationFrame + +来看看 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame) 对它的介绍 + +> window.requestAnimationFrame() 方法告诉浏览器您希望执行动画并在浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。 +> +> 注意:若您想在下次重绘时产生另一个动画画面,您的回调例程必须调用 requestAnimationFrame() +> +> 当你需要更新屏幕画面时就可以调用此方法。在浏览器下次重绘前执行回调函数。回调的次数通常是每秒60次,但大多数浏览器通常匹配 W3C 所建议的刷新频率。在大多数浏览器里,当运行在后台标签页或者隐藏的