[{"content":" 前言 # 市面上业务复杂 App中近半数业务页面使用H5 页面承载，H5的优势很明显，跨平台、迭代快、开发体验好，H5的劣势同样明显，加载慢，用户体验差，为了提高页面加载速度和成功率，我们在app H5 部分业务加载 采用了离线包方式，如果有业务功能有变更，就通过我们的无线发布系统，将新的业务离线包更新到App 中，从而做到随时发布，动态更新，页面秒开，无须app 改动。\n背景 # 目前优化Web页面体验方案比较多，有Flutter、React Native、小程序、离线包，其中离线包技术对现有H5页面改造成本小，技术最为成熟和稳定。它具有快速迭代发布的天然优势。\n离线包加载方案 # 目前 成熟的方案离线包加载有本地服务和 本地资源加载 请求拦截，下面是相关方案的对比。\n1.LocalWebServer # app 启动时，在app 里面开启 WebServer ，相当于 内置服务器。资源与服务都放在app内部。\n优点 ：后端无须搭建WebServer 供 app 使用，减轻服务端压力。\n缺点： 本地服务启动，需要消耗相当大的性能，同时需要处理app 激活问题，实现起来并不容易,开源的 GCDWebServer ，CocoaHTTPServer 已不更新。\n2. WKWebView请求拦截 # wkwebView 请求目前有两种 一种是基于系统的NSURLProtocol，一种是iOS 11 引入WKURLSchemeHandler，WKURLSchemeHandler 在隔离性、稳定性、一致性上表现优于NSURLProtocol。\n优点： 不修改加载URL，没有跨域问题，且支持网页部分资源离线化，灵活性和兼容性好，腾讯的 开发 VasSonic 开源支持。\n缺点： 需要app 端对前端有深入了解，不局限与处理app的代码。\n3. 本地路径加载 # app 在请求后，去掉相应的ip ，替换为本地的资源进行加载，类似与打开html 文件。\n优点：简单可靠，无需hook和调用私有API，也有相依的公司使用。\n缺点：有跨域问题，使用的厂商比较少\n总结： WKWebView请求 拦截 基于实现成本低，方案成熟，大厂夹持等特点，可被项目采纳。\n技术实现 # 离线包基本思路都是通过 webview 统一拦截 url，将资源映射到本地离线包，更新的时候对版本资源检测，下载和维护本地缓存目录中的资源。同时对标签当有标签时，我们使用的图片第三方是Kingfisher，用Kingfisher缓存下来，这样就不需要再次用H5请求图片【改成用本地请求】提升速度。会将图片和视频等非文字内容通过原生组件的方式放在客户端进行渲染，既可以提高渲染效率，也可以减少不必要的流量消耗。\n首先实现离线包新版本获取 业务相关的id管理，离线包更新类型管理，包的下载 状态管理，同时离线包具有可选设置项包括频率，缓存策略，调用频率等。\n其次是 离线包内的网络请求，以及webView容器创建，清理。\n然后是添加离线包相关的log 日志如（离线包更新来源，离线包预加载模式，离线包下载任务状态）。\n最后是业务方打包相应的h5 HTML 配置文件，压缩为zip上传到离线包管理平台上。\n总体原理图 # app内阶段配置 # 首次加载的包 xxx.7zip 复制到项目中如下可看到资源 离线包首次加载配置\n(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { /// 离线包配置（包括激活，日志添加，加载策略等） }\n3.离线包内基础内功能如：版本更新管理，下载管理，离线容器管理，离线日志管理，可使用framework 进行封装，可在公司多个app内进行复用。 最终效果 # 统计了 APP 在不开离线包方案时，webview 平均加载时长在 1.5-2 秒的范围内(这里是计算的 webview开始加载到导航完成的时间)，在上述使用离线包的情况下，打开的时长在 0.25-0.3 秒之间。所以效果还是很显著的，用户的直观感受就是接近于秒开的体验。\n参考资料 # [1] [货拉拉H5离线包原理与实践] (https://juejin.cn/post/7103348563479887885#heading-3)\n[2] [WKWebView 请求拦截探索与实践] (https://zhuanlan.zhihu.com/p/347592487)\n[3] [VasSonic简单介绍] (https://github.com/Tencent/VasSonic)\n[4] [WKWebview秒开实践分享及问题解决方案] (https://juejin.cn/post/6887161842406260744#heading-6)\n","date":"2023年3月27日","externalUrl":"","permalink":"/%E7%A6%BB%E7%BA%BF%E5%8C%85%E5%AE%9E%E7%8E%B0app%E5%86%85h5%E7%9A%84%E7%A7%92%E5%BC%80/","section":"文章","summary":" 市面上业务复杂 App中近半数业务页面使用H5 页面承载，H5的优势很明显，跨平台、迭代快、开发体验好，H5的劣势同样明显，加载慢，用户体验差，为了提高页面加载速度和成功率，我们在app H5 部分业务加载 采用了离线包方式，如果有业务功 …","title":"离线包实现app内H5的秒开","type":"posts"},{"content":" 前言 # 今年公司引进敏捷开发工具套件 jira ，bitbuket，conference ，下面 了解了下敏捷开发的使用。\n敏捷思想 # 快速响应 相对独立，可持续，稳定运行。\n百度百科 # 敏捷开发以用户的需求进化为核心，采用迭代、循序渐进的方法进行软件开发。在敏捷开发中，软件项目在构建初期被切分成多个子项目，各个子项目的成果都经过测试，具备可视、可集成和可运行使用的特征。换言之，就是把一个大项目分为多个相互联系，但也可独立运行的小项目，并分别完成，在此过程中软件一直处于可使用状态。\n维基百科 # 是一种应对快速变化的需求的一种软件开发能力。它们的具体名称、理念、过程、术语都不尽相同，相对于“非敏捷”，更强调程序员团队与业务专家之间的紧密协作、面对面的沟通（认为比书面的文档更有效）、频繁交付新的软件版本、紧凑而自我组织型的团队、能够很好地适应需求变化的代码编写和团队组织方法，也更注重软件开发过程中人的作用。\n敏捷工具 # 已经有一些项目管理工具用于敏捷开发，可以用它们来帮助规划，跟踪，分析和集成工作。 这些工具在敏捷开发中扮演的重要的角色，也是知识管理的一种方法。通常包括：版本控制集成，进度跟踪，工作分配，集成发布和迭代规划，论坛和软件缺陷的报告和跟踪。\n公司 实现敏捷思想 的工具是 jira 的Scrum与 看板。\nScrum # Scrum 是团队用来管理其工作的框架. Scrum 将 敏捷 原则作为一组具体的项目、做法和角色来实现。\nScrum框架由角色、事件和工件组成，描述了这些实体如何相互连接并实现框架。\n三大角色（产品所有者、 scrum 主角色和 scrum 团队）\n两大工件（backlog，sprint 计划）\nScrum会议（四大会议）\n下图是它们之间的关系：\nScrum采用迭代方法，其中开发周期为一般为2—4周。在每个迭代结束时，产品/解决方案的增量版本就准备好交付了。Scrum框架中的每个事件/工件/角色都服务于一个目的，并进一步推进了敏捷项目开发的目标。接下来我们将逐一介绍。\nScrum 角色 # 包含三大角色产品产品所有者，scrum 主角色 ，实施人员 team， 三大角色联动可以达到快速与高效。\n产品所有者 # 确定产品的方向和愿景，定义产品发布的内容、优先级及交付时间，为产品ROI（profitability of product）负责。 是维护产品需求清单（ product backlog ）的人，代表利益相关者的利益。一般由 产品人员为主要负责人负责 ，（sprint 计划，以及bocklog主要维护人员）\n负责最大化产品以及开发团队工作的价值。主要职责如下：\n1、确定产品的功能；\n2、决定发布的日期和发布内容；\n3、为产品的ROI负责；\n4、根据市场价值确定功能优先级；\n5、每个sprint中，根据需要调整功能和优先级（每个sprint开始前调整）；\n6、接受或拒绝开发团队的工作成果；\n7 参与Scrum Planning Meetings（Sprint计划会议），Sprint Review Meeting（Sprint评审会）和 Sprint Retrospective Meeting（Sprint回顾会）\n一句话总结PO这个角色就是：告诉产品团队要做什么，做功能的先后顺序是怎样的，需求有变动时该如何处理。\nscrum 主角色 # 是团队的导师和组织者，与Product Owner紧密合作，及时为团队成员提供帮助。促使team按照scrum方式运行，为Scrum过程负责的人。一般为 开发负责人来负责。\nScrum Master工作职责：\n1、保证团队资源合理利用；\n2、保证各个角色及职责良好协作；\n3、解决团队开发中的障碍；\n4、作为团队和团队外部的接口，协调解决沟通中的问题；\n5、保证开发过程按计划进行，组织Scrum Planning Meetings（Sprint计划会议）, Daily Stand-up Meeting（每日站会）, Sprint Review Meeting（Sprint评审会）和 Sprint Retrospective Meeting（Sprint回顾会）\n另外，Scrum Master还需要帮助PO建立product backlog与sprint backlog，并确立其中每个story的优先级。\nScrum Master在团队中的作用 在junior团队中：主导和控制，在intermediate团队中：引导和教导 在Senior团队中：辅导和协助\n我觉得Scrum Master对Team在很多时候都应该有一种“护犊子”的精神。确保Team神圣不可侵犯。\n一句话总结SM这个角色就是：教整个团队怎么做（有效沟通，保护team），计划排定，如何估时，跟进每天进度（day meeting），风险控制（及时集成），定期总结（团队建设）。\n实施人员 team # 负责交付产品的团队。一个团队通常由5至9名具有跨职能技能的人（设计者，开发者等）组成，承担实际的开发工作。\n两大工件（backlog，sprint 计划） # 产品积压工作（backlog）是团队可以交付的优先级值列表。 产品所有者负责积压工作，并根据需要添加、更改和重新分类。 积压工作顶部的项目应始终可供团队执行。\n产品积压工作\n史诗和功能被分解为故事，并添加验收标准。待办事项被划分优先级并排序。\n产品添加需求，开发添加优化，UI添加设计计划。\nsprint 计划\n产品 先根据需求的进行排期计划。 Scrum 会议 # 冲刺计划会议\n讨论 计划迭代周期，讨论任务排期。\n每日站立会议\n早上晚上 15分钟（都要参加）\n评审会议\n在冲刺结束前给产品负责人演示并接受评价的会议，（功能演示）\n回顾会议\n每一个冲刺完成后，都会举行一次冲刺回顾会议，在会议上所有团队成员都要反思这个冲刺。举行冲刺回顾会议是为了进行持续过程改进。会议的时间限制在4小时。\n一句话总结Scrum会议就是：任务领取，时间承诺，进度跟踪，改善建议\n经验小记 # 产品开始sprint 会议时，需求已出UI已出，并审核， 主要迭代功能多少，进行优先级分类。\n需求sprint以及UI sprint 要优先开发sprint\n参考 # 微软什么是敏捷开发\nscrum的角色分类\n敏捷项目管理：最佳实践和方法论\n","date":"2022年6月10日","externalUrl":"","permalink":"/%E6%95%8F%E6%8D%B7%E5%BC%80%E5%8F%91/","section":"文章","summary":" 今年公司引进敏捷开发工具套件 jira ，bitbuket，conference ，下面 了解了下敏捷开发的使用。\n","title":"敏捷开发","type":"posts"},{"content":" 前言 # 记录下2022年度已读的书籍。\n书单 # 书名 作者 简介（个人见 解 ） 置身事内 兰小欢 中国经济模型 微尘 陈年喜 最近几年小人物的生活 思辨与立场 : 生活中无处不在的批判性思维工具 理查德·保罗 琳达·埃尔德 看待事情的真相 如何阅读一本书 [美] 莫提默·J. 艾德勒 / 查尔斯·范多伦 一本书的要点 富爸爸，穷爸爸 （美）罗伯特・T・清崎 / 莎伦・L・莱希特 思维决定赚钱的方法 影响力 [美] 罗伯特·西奥迪尼 说服别人的方式 穷查理宝典 : 查理·芒格智慧箴言录 [美]彼得·考夫曼 编] 人生笺言（更好的生活） 程序员的三门课：技术精进、架构修炼、管理探秘 于君泽 / 李伟山 / 张洪亮 / 彭首長 / 刘朋 技术架构以及管理 法治的细节 罗翔 思辨下的法制 Java 编程思想 [美] Bruce Eckel java 学习 工作消费主义新穷人 [英]齐格蒙特·鲍曼](https://book.douban.com/subject/35593780/) 现在社会的认知 人月神话 弗雷德里克·布鲁克斯 IT项目管理 丧钟为谁而鸣 海明威 战争的破坏 歌 # ♫ 在网易云音乐收听 ↗\n♫ 在网易云音乐收听 ↗\n♫ 在网易云音乐收听 ↗\n","date":"2022年5月29日","externalUrl":"","permalink":"/2022%E5%B9%B4%E5%BA%A6%E8%AF%BB%E7%9A%84%E4%B9%A6%E7%B1%8D/","section":"文章","summary":" 记录下2022年度已读的书籍。\n书名 作者 简介（个人见 解 ） 置身事内 兰小欢 中国经济模型 微尘 陈年喜 最近几年小人物的生活 思辨与立场 : 生活中无处不在的批判性思维工具 理查德·保罗 琳达·埃尔德 看待事情的真相 如何阅读一本 …","title":"2022年度读的书籍","type":"posts"},{"content":" runloop 源码 # nsrunloop 未开源，cfrunloopref 已开源。共有五个类\nCFRunLoopRef （runloop对象）\nCFRunLoopModeRef （runloop 模式）\nCFRunLoopSourceRef （源 ）\nCFRunLoopTimerRef （定时器）\nCFRunLoopObserverRef （观察者）\nCFRunLoopRef # 首先看源码结构\nstruct __CFRunLoop { CFMutableSetRef _commonModes; // Set CFMutableSetRef _commonModeItems; // Set\u0026lt;Source/Observer/Timer\u0026gt; CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set ... }; _commonModes 具有common标识的所有mode。commonModeItems 中添加的item 会自动同步到_commonModes中。\nCFRunLoopModeRef # struct __CFRunLoopMode { CFStringRef _name; // Mode Name, 例如 @\u0026#34;kCFRunLoopDefaultMode\u0026#34; CFMutableSetRef _sources0; // Set CFMutableSetRef _sources1; // Set CFMutableArrayRef _observers; // Array CFMutableArrayRef _timers; // Array ... }; ","date":"2022年3月22日","externalUrl":"","permalink":"/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90runloop/","section":"文章","summary":" nsrunloop 未开源，cfrunloopref 已开源。共有五个类\nCFRunLoopRef （runloop对象）\nCFRunLoopModeRef （runloop 模式）\n","title":"源码分析runloop","type":"posts"},{"content":" tea (image) 什么是runloop # 程序在启动的时候，一般按照代码顺序执行 ，执行完就结束。为了 维持一时刻运行等待用户输入事件的机制，程序不退出，出现了evenloop（消息/事件\u0026ndash;观察- 唤醒-等待-），比如 Node.js 的事件处理，比如 Windows 程序的消息循环，再比如 OSX/iOS 里的 RunLoop。\n为什么需要runloop # 在无事件/消息的时候 程序进行休眠，节省计算资源。\nios 中的runloop # ios 中的runloop 是一个对象，OSX/iOS 系统中，提供了两个这样的对象：NSRunLoop 和 CFRunLoopRef。\nCFRunLoopRef 是在 CoreFoundation 框架内的，它提供了纯 C 函数的 API，所有这些 API 都是线程安全的，简称cfrunloop\nNSRunLoop 是基于 CFRunLoopRef 的封装，提供了面向对象的 API，但是这些 API 不是线程安全的\nrunloop 与线程一一对应，在线程上处理输入源，同时runloop 切换状态时候会发送通知给注册在runloop 中观察者。\n​ 运行循环的结构及其来源\nrunloop 与线程关系 # Runloop是与线程相关的基础架构的一部分。Runloop是一个事件处理循环，用于安排工作和协调接收传入事件。Runloop的目的是在有工作要做时让你的线程保持忙碌，并在没有工作时让你的线程进入睡眠状态。\nIos app 不允许直接创建Runloop。它只提供了两个自动获取的函数：CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 当然，Foudation 框架也有对应的API。\nrunloop对象 # Core Foundation # CFRunLoopRef maiRunloop = CFRunLoopGetMain(); // 获得主线程对应的 runloop对象 CFRunLoopRef maiRunloop = CFRunLoopGetCurrent(); // 获得当前线程对应的runloop对象 Foundation # NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop]; // 获得主线程对应的 runloop对象 NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop]; // 获得当前线程对应的runloop对象 Run Loop Modes # 运行模式是要监视的输入源和计时器的集合以及要通知的运行循环观察者的集合。每次你运行你的运行循环，你指定（显式或隐式）一个特定的“模式”运行。在运行循环的该阶段，只有与该模式关联的源被监控并允许传递它们的事件。 （类似地，只有与该模式关联的观察者会被通知运行循环的进度。）与其他模式关联的源会保留任何新事件，直到后续以适当的模式通过循环。\nLoopMode # runloop 有相关的运行模式，运行时候进行多种切换，每个模式关联的 有【核心事件源/时间间隔，观察者】。再给线程加入相关的属于源以及观察者的时候需要给他们赋予模式。\n共有5种模式，常用的模式有NSDefaultRunLoopMode，Event tracking 两种，默认模式是用于大多数操作的模式。大多数时候，你应该使用这个模式来启动你的运行循环并配置你的输入源。Cocoa 使用此模式来限制鼠标拖动循环和其他类型的用户界面跟踪循环期间的传入事件。\nInput /Timer Source # 数据源/定时器 ，事件源：目标监测源，使 RunLoop 在需要的时候唤醒线程，没事的时候 sleep。\nInput Source # 输入源将事件异步传递到您的线程。事件的来源取决于输入源的类型，一般是两类之一。基于端口的输入源监控应用程序的 Mach 端口。自定义输入源监视自定义事件源。就你的运行循环而言，输入源是基于端口的还是自定义的都无关紧要。系统通常实现两种类型的输入源，您可以按原样使用。两个来源之间的唯一区别是它们是如何发出信号的。基于端口的源由内核自动发出信号，而自定义源必须从另一个线程手动发出信号。\nCocoa Perform Selector Sources Port-Based Sources Custom Input Sources Timer Sources # 计时器源在未来的预设时间将事件同步传递到您的线程。定时器是线程通知自己做某事的一种方式。例如，一旦用户连续击键之间经过一定时间，搜索字段就可以使用计时器启动自动搜索。使用此延迟时间使用户有机会在开始搜索之前尽可能多地键入所需的搜索字符串。\n虽然它会生成基于时间的通知，但计时器并不是一种实时机制。与输入源一样，计时器与运行循环的特定模式相关联。如果计时器不在运行循环当前正在监视的模式下，它不会触发，直到您以计时器支持的模式之一运行运行循环。类似地，如果在 run loop 正在执行处理程序例程的中间触发计时器，则计时器将等待直到下一次通过 run loop 调用其处理程序例程。如果运行循环根本没有运行，则计时器永远不会触发。\nRunloop Observer # 与在适当的异步或同步事件发生时触发的源相比，运行循环观察者在运行循环本身执行期间在特殊位置触发。您可以使用运行循环观察器来准备线程以处理给定事件或在线程进入睡眠之前准备线程。您可以将运行循环观察者与运行循环中的以下事件相关联\nThe entrance to the run loop.运行循环的入口。 When the run loop is about to process a timer.当运行循环即将处理计时器时。 When the run loop is about to process an input source.当运行循环即将处理输入源时。 When the run loop is about to go to sleep.当运行循环即将进入睡眠状态时。 When the run loop has woken up, but before it has processed the event that woke it up.当运行循环被唤醒，但在它处理唤醒它的事件之前。 The exit from the run loop.退出运行循环。 什么时候使用runloop？ # 适用于您希望与线程进行更多交互的情况下才使用runloop。\n以下任何操作，则需要启动runloop：\nUse ports or custom input sources to communicate with other threads.//使用端口或自定义输入源与其他线程进行通信。 Use timers on the thread.//在线程上使用计时器。 Use any of the performSelector… methods in a Cocoa application.//在 Cocoa 应用程序中使用任何 performSelector\u0026hellip; 方法。 Keep the thread around to perform periodic tasks.//保留线程以执行定期任务。 参考 # ","date":"2022年2月25日","externalUrl":"","permalink":"/runloop%E7%90%86%E8%AE%BA%E5%89%96%E6%9E%90/","section":"文章","summary":"tea (image) 程序在启动的时候，一般按照代码顺序执行 ，执行完就结束。为了 维持一时刻运行等待用户输入事件的机制，程序不退出，出现了evenloop（消息/事件–观察- 唤醒-等待-），比如 Node.js 的事件处理，比如 …","title":"runloop理论剖析","type":"posts"},{"content":" 前言 # ios 与h5 交互是混合开发的基石，交互方式分为三种，webview 交互，jsbrige交互，JavaScriptCore交互。\nwebview 代理 # jsbrige # JavaScriptCore # ","date":"2022年2月24日","externalUrl":"","permalink":"/ios%E5%92%8Ch5%E9%A1%B5%E9%9D%A2%E5%8F%8C%E5%90%91%E6%95%B0%E6%8D%AE%E4%BA%A4%E4%BA%92/","section":"文章","summary":" ios 与h5 交互是混合开发的基石，交互方式分为三种，webview 交互，jsbrige交互，JavaScriptCore交互。\n","title":"ios和h5双向数据交互","type":"posts"},{"content":" 前言 # 对公司最近新的千万级项目使用到的前端实现方案进行大致的总结，包括 react ，antd， antpro，less。\n重点：技术选型，组件库选型，脚手架选型，样式语法选型\nreact # React 是 Facebook 内部开源出来的一个前端 UI 开发框架，它引入了虚拟DOM， 插入在js逻辑和实际DOM之间，虚拟DOM帮助我们解决了跨浏览器问题，它为我们提供了标准化的API，甚至在IE8中都是没问题的。一切都是component，重用更加容易，代码更加模块化，重用代码更容易，可维护性高。这样当某个或某些组件出现问题是，可以方便地进行隔离。每个组件都可以进行独立的开发和测试，并且它们可以引入其它组件。这等同于提高了代码的可维护性。兼容性好，比如使用RequireJS来加载和打包，而Browserify和Webpack适用于构建大型应用。它们使得那些艰难的任务不再让人望而生畏。它采用了单向数据流，react中 Flux是一个用于在JavaScript应用中创建单向数据层的架构，它随着React视图库的开发而被Facebook概念化。减少了重复代码，这也是它为什么比传统数据绑定更简单。\n重点：大厂，虚拟dom，组件化，可维护性高，生态完善\nantd # Ant Design 是服务于企业级产品的设计体系。是基于「自然」、「确定性」、「意义感」、「生长性」四大设计价值观 设计而来。它提供了一套非常完整的 组件化设计规范 与 组件化编码规范，通过模块化解决方案，降低冗余的生产成本，让设计者专注于更好的用户体验，开发者专注功能开发。对于开发者它是组件功能库，对与设计者它提供了评价设计好坏的内在标准。\n重点：大厂，组件功能完善，用户多。\nantpro # antD-pro 基于 Ant Design 开箱即用的中台前端 解决方案。它 使用 React/dva/antd ，可针对不同屏幕大小设计进行响应。可配置的主题可用于满足多样化的品牌诉求。运用常用的设计模式，提炼自中后台应用的典型页面和场景。\n重点：Ant Design Pro 作为一个前端脚手架，可配置。\nless # Less （Leaner Style Sheets 的缩写） 是一门向后兼容的 CSS 扩展语言。它可以方便地屏蔽浏览器私有语法差异，完全兼容 CSS 代码。同时 结构清晰，便于扩展，可维护性高。\n重点：可维护性高。\n","date":"2022年2月22日","externalUrl":"","permalink":"/%E5%A4%A7%E5%9E%8Bpc%E5%B9%B3%E5%8F%B0%E5%89%8D%E7%AB%AF%E5%AE%9E%E7%8E%B0%E6%96%B9%E6%A1%88/","section":"文章","summary":" 对公司最近新的千万级项目使用到的前端实现方案进行大致的总结，包括 react ，antd， antpro，less。\n","title":"大型pc平台前端实现方案","type":"posts"},{"content":" 选项API和组合API # 什么是选项API写法：Options ApI\n咱们在vue2.x项目中使用的就是 选项API 写法\n1. 代码风格：data选项写数据，methods选项写函数...，一个功能逻辑的代码分散。 1. 优点：易于学习和使用，写代码的位置已经约定好 1. 缺点：代码组织性差，相似的逻辑（功能）代码不便于复用，逻辑复杂代码多了不好阅读。 什么是组合API：Composition API\n以功能为单位组织代码结构，后续重用功能更加方便。 总结：组合API的最大好处就是以功能为单位组织代码结构，有利于代码的复用\nvue2的选项类型 API # 选项类型API（Options API）旧的选项型API在代码里分割了不同的属性（properties）：data，computed属性，\nexport default { props: { title: String }, data () { return { username: \u0026#39;\u0026#39;, password: \u0026#39;\u0026#39; } }, mounted () { console.log(\u0026#39;组件已挂载\u0026#39;)//生命周期 }, methods: { login () { // 登陆方法 } } } why vue3 # 紧跟 react 的新特性，添加 类似于 react 16.8 升级后的 特性。吸收新的编程思想。\n组合API # 新的合成型API能让我们用方法（function）来分割，相比于旧的API使用属性来分组，这样代码会更加简便和整洁。\nexport default { props: { title: String }, setup () { const state = reactive({ username: \u0026#39;\u0026#39;, password: \u0026#39;\u0026#39; }) const login = () =\u0026gt; { // 登陆方法 } onMounted(() =\u0026gt; { console.log(\u0026#39;title: \u0026#39; + props.title) }) return { login, state } } } Vue3现状： # vue-next 2020年09月18日，正式发布vue3.0版本。但由于刚发布周边生态不支持，大多数开发者处于观望。 现在主流组件库都已经发布了支持vue3.0的版本，其他生态也在不断地完善中，这是趋势。 element-plus 基于 Vue 3.0 的桌面端组件库 vant vant3.0版本，有赞前端团队开源移动端组件库 ant-design-vue Ant Design Vue 2.0版本，社区根据蚂蚁 ant design 开发 Vue3优点： # 最火框架，它是国内最火的前端框架之一 性能提升，运行速度事vue2.x的1.5倍左右 体积更小，按需编译体积比vue2.x要更小 类型推断，更好的支持Ts（typescript）这个也是趋势（js的超集） 高级给予，暴露了更底层的API和提供更先进的内置组件 ★选项API\u0026mdash;\u0026ndash;组合API (composition api) ，能够更好的组织逻辑，封装逻辑，复用逻辑 Vue3展望： # 这是趋势，越来越多的企业将来肯定会升级到Vue3.0 大型项目，由于对Ts的友好越来越多大型项目可以用Vue3.0 1.vue3依赖的相关第三方包有针对性（包的名称发生变化）\n2.vue3性格更好，编程体验更好\n3.这是未来的趋势，未来已来。\nvue3的使用 # 从0 到1 的不同点。\n创建vue应用 # 基于Vue脚手架创建项目\n# 1. 安装脚手架 npm i @vue/cli -g # 2. 创建vue3项目（选项中选择v3版本，其他和之前一样） vue create 项目名称 # 3、切换路径 cd 项目名称 # 4、运行项目 npm run serve tip\n手工选择的时候，一定要选择v.3版本号，其他的和Vue2一样 项目代码结构分析 入口文件main.js // Api的按需导入 import { createApp } from \u0026#39;vue\u0026#39; import App from \u0026#39;./App.vue\u0026#39; import router from \u0026#39;./router\u0026#39; import store from \u0026#39;./store\u0026#39; // 入口文件的职能：把根组件渲染到单页面的div里面 // Vue2中代码风格 // const vm = new Vue({ // router, // store, // render: h =\u0026gt; h(App) // }).$mount(\u0026#39;#app\u0026#39;) // Vue3的代码风格 // createApp(App).use(store).use(router).mount(\u0026#39;#app\u0026#39;) // createApp方法负责创建Vue实例对象 const vm = createApp(App) // 把store和router插件配置到Vue实例中 vm.use(store).use(router).mount(\u0026#39;#app\u0026#39;) tip\nVue的实例化方式发生变化：基于createApp方法进行实例化\nrouter和store采用use方法进行配置\nVue3的典型API风格：按需导入，为了提升打包的性能\n根组件结构 \u0026lt;template\u0026gt; \u0026lt;!-- Vue2中组件的模板必须有唯一的根节点 --\u0026gt; \u0026lt;!-- Vue3中组件的模板可以没有根节点 --\u0026gt; \u0026lt;div id=\u0026#34;nav\u0026#34;\u0026gt; \u0026lt;router-link to=\u0026#34;/\u0026#34;\u0026gt;主页\u0026lt;/router-link\u0026gt; | \u0026lt;router-link to=\u0026#34;/about\u0026#34;\u0026gt;关于\u0026lt;/router-link\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 路由组件的填充位置 --\u0026gt; \u0026lt;router-view /\u0026gt; \u0026lt;/template\u0026gt; tip\nVue3中组件的模板可以没有根节点（Vue2中组件的模板必须有唯一的根节点） 路由模块分析 import { createRouter, createWebHashHistory } from \u0026#39;vue-router\u0026#39; import Home from \u0026#39;../views/Home.vue\u0026#39; // 路由映射 const routes = [ { path: \u0026#39;/\u0026#39;, name: \u0026#39;Home\u0026#39;, component: Home }, { path: \u0026#39;/about\u0026#39;, name: \u0026#39;About\u0026#39;, // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () =\u0026gt; import(/* webpackChunkName: \u0026#34;about\u0026#34; */ \u0026#39;../views/About.vue\u0026#39;) } ] // Vue2创建路由实例对象 // const router = new VueRouter({ // mode: \u0026#39;hash/history\u0026#39;, // routes: routes // }) // Vue3创建路由实例对象 const router = createRouter({ // 设置路由的模式：hash/history // history: createWebHistory(), history: createWebHashHistory(), routes }) export default router tip\n创建路由实例对象采用createRouter方法，Vue3的典型风格 采用hash和history的方式有变化 Vue2采用mode选项：hash/history Vue3采用方法：createWebHashHistory()/createWebHistory() Vuex模块\nimport { createStore } from \u0026#39;vuex\u0026#39; // Vue2实例化 // new Vuex.Store({}) // Vue3实例化 export default createStore({ // 状态 state: { }, // 修改状态 mutations: { }, // 异步操作 actions: { }, // 模块化拆分 modules: { }, // 类似于组件的计算属性 getters: { } }) tip\n创建store对象采用createStore方法，而不是new. 注意：Vue3代码的整体风格：API的按需导入\n组合API-setup函数 # 使用细节：\nsetup 是一个新的组件选项，作为组件中使用组合API的起点。 从组件生命周期来看，它的执行在组件实例创建之前vue2.x的beforeCreate执行。 这就意味着在setup函数中 this 还不是组件实例，this 此时是 undefined 在模版中需要使用的数据和函数，需要在 setup 返回。 演示代码：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;setup函数用法\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;div\u0026gt;{{msg}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt;{{info}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#39;App\u0026#39;, // Vue3依然可以使用data中的数据，但是不建议使用（这是选项API） data () { return { info: \u0026#39;nihao\u0026#39; } }, setup () { // 触发时机在beforCreate/created生命周期之前 // Vue3中beforCreate/created声明周期函数已经废弃了，其实已经被setup替代了 // 此时无法访问this，因为组件实例此时尚未创建 // console.log(this) // 定义事件函数 const handleClick = () =\u0026gt; { console.log(\u0026#39;click\u0026#39;) } // 这里返回的数据用于给模板使用：类似于之前data中提供的数据 return { msg: \u0026#39;hello\u0026#39;, handleClick } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; tip\nsetup选项是实现组合API的基础 触发的时机在beforeCreate之前 Vue3中beforCreate/created声明周期函数已经废弃了，其实已经被setup替代了 此时无法访问this，因为组件实例此时尚未创建 setup的返回值用于给模板提供数据和方法 模板依然可以从data获取数据，但是不建议使用data了。 组合API-生命周期 # 回顾vue2.x生命周期钩子函数： beforeCreate 创建前 created 创建后 beforeMount 挂载前 mounted 挂载后 beforeUpdate 更新前 updated 更新后 beforeDestroy 销毁前 destroyed 销毁后 认识vue3.0生命周期钩子函数 setup 创建实例前 onBeforeMount 挂载DOM前 onMounted 挂载DOM后 onBeforeUpdate 更新组件前 onUpdated 更新组件后 onBeforeUnmount 卸载销毁前 onUnmounted 卸载销毁后 演示代码：\n\u0026lt;template\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; container \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { onBeforeMount, onMounted } from \u0026#39;vue\u0026#39; export default { setup () { onBeforeMount(()=\u0026gt;{ console.log(\u0026#39;DOM渲染前\u0026#39;,document.querySelector(\u0026#39;.container\u0026#39;)) }) onMounted(()=\u0026gt;{ console.log(\u0026#39;DOM渲染后1\u0026#39;,document.querySelector(\u0026#39;.container\u0026#39;)) }) onMounted(()=\u0026gt;{ console.log(\u0026#39;DOM渲染后2\u0026#39;,document.querySelector(\u0026#39;.container\u0026#39;)) }) }, } \u0026lt;/script\u0026gt; tip\nVue3生命周期的函数发生了变化 去掉两个：beforeCreate和created，添加了setup 方法名称发生变化：方法名称前面多了个on，中间是驼峰式的 卸载组件的生命周期变化：onBeforeUnmount 、onUnmounted 同一个生命周期可以触发多次 组合API-reactive函数 # reactive是一个函数，它可以定义一个复杂数据类型，成为响应式数据。 演示代码：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;数据的响应式\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;div\u0026gt;{{obj.msg}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { reactive } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // 数据响应式：数据的变化导致视图自动变化 // let msg = \u0026#39;hello\u0026#39; // reactive方法包裹的对象中的数据都是响应式的 const obj = reactive({ msg: \u0026#39;hello\u0026#39;, info: \u0026#39;hi\u0026#39; }) const handleClick = () =\u0026gt; { // msg = \u0026#39;nihao\u0026#39; // console.log(msg) obj.msg = \u0026#39;nihao\u0026#39; } return { obj, handleClick } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; tip\nsetup默认返回的普通数据不是响应式的\n如果希望数据是响应式，有一种做法就是给数据包裹reactive方法即可\n组合API-toRef函数 # 定义响应式数据：\ntoRef是函数，转换响应式对象中某个属性为单独响应式数据，并且值是关联的。 演示代码：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;数据的响应式\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;!-- \u0026lt;div\u0026gt;{{obj.msg}}\u0026lt;/div\u0026gt; --\u0026gt; \u0026lt;div\u0026gt;{{msg}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt;{{info}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { reactive, toRef } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // 需求：模板中必须有添加对象obj前缀,而是直接获取属性 // reactive方法包裹的对象中的数据都是响应式的 const obj = reactive({ msg: \u0026#39;hello\u0026#39;, info: \u0026#39;hi\u0026#39; }) // 把对象中的单个属性取出并且保证数据的响应式 const msg = toRef(obj, \u0026#39;msg\u0026#39;) const info = toRef(obj, \u0026#39;info\u0026#39;) const handleClick = () =\u0026gt; { obj.msg = \u0026#39;nihao\u0026#39; obj.info = \u0026#39;coniqiwa\u0026#39; } // reactive中的对象属性如果重新赋值会失去响应式能力 return { msg, info, handleClick } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; tip\ntoRef方法可以把对象中的单个属性取出并且保证响应式能力 组合API-toRefs函数 # toRefs是函数，转换响应式对象中所有属性（可以是一部分）为单独响应式数据，对象成为普通对象，并且值是关联的 演示代码：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;数据的响应式\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;!-- \u0026lt;div\u0026gt;{{obj.msg}}\u0026lt;/div\u0026gt; --\u0026gt; \u0026lt;div\u0026gt;{{msg}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt;{{info}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { reactive, toRefs } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // 需求：模板中必须有添加对象obj前缀,而是直接获取属性 // reactive方法包裹的对象中的数据都是响应式的 const obj = reactive({ msg: \u0026#39;hello\u0026#39;, info: \u0026#39;hi\u0026#39;, abc: \u0026#39;abc\u0026#39; }) // 把对象中的单个属性取出并且保证数据的响应式 // const msg = toRef(obj, \u0026#39;msg\u0026#39;) // const info = toRef(obj, \u0026#39;info\u0026#39;) // 把obj对象中的属性结构出来，保证响应式能力 const { msg, info } = toRefs(obj) const handleClick = () =\u0026gt; { obj.msg = \u0026#39;nihao\u0026#39; obj.info = \u0026#39;aligotogozayimasi\u0026#39; } // reactive中的对象属性如果重新赋值会失去响应式能力 return { msg, info, handleClick } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; 总结：toRefs方法可以批量转换对象中的属性为独立的响应式数据\n组合API-ref函数 # ref函数，常用于简单数据类型定义为响应式数据 在修改值和获取值的时候，需要.value 在模板中使用ref申明的响应式数据，可以省略.value 演示代码：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;数据的响应式\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;div\u0026gt;{{count}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // ref主要（也可以是对象和数组）用于定义基本类型的数据并保证响应式能力 const count = ref(0) const obj = ref({ msg: \u0026#39;hello\u0026#39; }) const handleClick = () =\u0026gt; { // ref定义的数据，在js中操作时需要通过value属性进行 // 但是模板中访问不需要value count.value += 1 console.log(obj.value.msg) } return { count, handleClick } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; 总结\n如果是基本类型数据，可以使用ref进行定义 ref其实也可以定义对象，但是访问时需要value属性 数据响应式 setup中直接返回的普通数据不是响应式的 通过reactive包裹对象可以成为响应式数据 通过reactive包裹对象可以成为响应式数据 为了简化对象的访问（去掉前缀），可以使用toRef进行优化 为了获取对象中多个属性，可以使用toRefs进一步简化 如果是简单数据类型，其实使用ref定义更加合适 组合API-computed函数 # computed函数，是用来定义计算属性的，计算属性不能修改。 基本使用：只读 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;计算属性\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;button @click=\u0026#39;age=25\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;div\u0026gt;今年：{{age}}岁了\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt;明年：{{nextAge}}岁了\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref, computed } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // 计算属性：简化模板 // 应用场景：基于已有的数据，计算一种数据 const age = ref(18) // 计算属性基本使用 const nextAge = computed(() =\u0026gt; { // 回调函数必须return，结果就是计算的结果 // 如果计算属性依赖的数据发生变化，那么会重新计算 return age.value + 1 }) return { age, nextAge } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; Vue3中计算属性也是组合API风格\ntip\n回调函数必须return，结果就是计算的结果\n如果计算属性依赖的数据发生变化，那么会重新计算\n不要在计算中中进行异步操作\n不要在计算属性中修改依赖的数据\n高级用法：可读可写 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;计算属性\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;button @click=\u0026#39;age=25\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;button @click=\u0026#39;nextAge=28\u0026#39;\u0026gt;点击修改\u0026lt;/button\u0026gt; \u0026lt;div\u0026gt;今年：{{age}}岁了\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt;明年：{{nextAge}}岁了\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref, computed } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // 计算属性：简化模板 // 应用场景：基于已有的数据，计算一种数据 const age = ref(18) // // 计算属性基本使用 // const nextAge = computed(() =\u0026gt; { // // 回调函数必须return，结果就是计算的结果 // // 如果计算属性依赖的数据发生变化，那么会重新计算 // return age.value + 1 // }) // 修改计算属性的值 const nextAge = computed({ get () { // 如果读取计算属性的值，默认调用get方法 return age.value + 1 }, set (v) { // 如果要想修改计算属性的值，默认调用set方法 age.value = v - 1 } }) return { age, nextAge } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; 总结：\n计算属性可以直接读取或者修改 如果要实现计算属性的修改操作，那么computed的实参应该是对象 读取数据触发get方法 修改数据触发set方法，set函数的形参就是你赋给他的值 组合API-watch函数 # watch函数，是用来定义侦听器的 监听ref定义的响应式数据 // 1、侦听器-基本类型 const count = ref(10) // 侦听普通值基本使用 watch(count, (newValue, oldValue) =\u0026gt; { console.log(newValue, oldValue) }) 总结：侦听普通数据可以获取修改前后的值；被侦听的数据必须是响应式的。\n监听reactive定义的响应式数据 const obj = reactive({ msg: \u0026#39;tom\u0026#39; }) // 侦听对象 // watch(obj, (newValue, oldValue) =\u0026gt; { watch(obj, () =\u0026gt; { // console.log(newValue === oldValue) console.log(obj.msg) }) const handleClick = () =\u0026gt; { obj.msg = \u0026#39;jerry\u0026#39; } 总结：如果侦听对象，那么侦听器的回调函数的两个参数是一样的结果，表示最新的对象数据，此时，也可以直接读取被侦听的对象，那么得到的值也是最新的。\n监听多个响应式数据数据 // 3、侦听器-侦听多个数据 const n1 = ref(1) const n2 = ref(2) watch([n1, n2], (v1, v2) =\u0026gt; { // v1和v2都是数组 // v1表示被监听的所有值的最新值 // v2表示被监听的所有值的原有值 console.log(v1, v2) }) 总结：可以得到更新前后的值；侦听的结果也是数组，数据顺序一致\n监听reactive定义的响应式数据的某一个属性 // 4、侦听对象中的某个属性 // 如果侦听对象中当个属性，需要使用函数方式 watch(() =\u0026gt; obj.age, (v1, v2) =\u0026gt; { console.log(v1, v2) }) 总结：如果侦听对象中单个属性，需要使用函数方式，侦听更少的数据有利于提高性能。\n配置选项用法 // watch方法的配置选项 // immediate: true 表示，组件渲染后，立即触发一次 watch(() =\u0026gt; stuInfo.friend, () =\u0026gt; { console.log(\u0026#39;sutInfo\u0026#39;) }, { immediate: true, // 被侦听的内容需要是函数的写法 () =\u0026gt; stuInfo.friend deep: true }) 总结：\nimmediate: true,表示组件渲染时立即调用 deep:true，表示深度监听对象里面的子属性（被侦听的内容需要是函数的写法） 总结 # Vue3介绍：第三方包需要专门针对vue3进行定制；性能更好；编程体验更好（组合API）；未来的趋势 创建Vue3项目：手动选择选项（选择v3） 代码的主体风格：按需导入（打包优化） 选项API和组合API的区别 setup方法基本用法：执行时机beforeCreate之前；无法访问this; 返回值给模板使用 组件的生命周期：setup替代了beforeCreate/created:生命周期函数名称发生了变化 数据响应式：普通数据没有响应式能力 reactive toRef toRefs ref 综合案例：加强上述知识点的掌握；辅助理解组合API的开发模式 计算属性：获取；修改 侦听器 侦听基本类型数据 侦听引用类型数据 侦听多个数据 侦听对象的单个属性 侦听的配置选项：immediate；deep 组合API-ref属性 # 获取DOM或者组件实例可以使用ref属性，写法和vue2.0需要区分开\nVue2通过ref操作DOM或者组件：1.给标签添加ref属性并设置一个名字；2.通过this.$refs.起的名字\n基于Vue2的方式操作ref\u0026mdash;\u0026ndash;数组场景 \u0026lt;ul\u0026gt; \u0026lt;li v-for=\u0026#34;(item, index) in list\u0026#34; ref=\u0026#34;fruits\u0026#34; :key=\u0026#34;index\u0026#34;\u0026gt;{{ item }}\u0026lt;/li\u0026gt; \u0026lt;!-- \u0026lt;my-com :key=\u0026#39;index\u0026#39; v-for=\u0026#39;index in 8\u0026#39; ref=\u0026#39;info\u0026#39;\u0026gt;\u0026lt;/my-com\u0026gt; --\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;button @click=\u0026#34;handleClick\u0026#34;\u0026gt;点击\u0026lt;/button\u0026gt; methods: { handleClick () { const fruits = this.$refs.fruits console.log(fruits[1].innerHTML) } } // 批量绑定同名的ref （主要就是v-for场景中使用 ref）,此时通过[this.$refs.名称]访问的值应该是一个数组，数组中包含每一个DOM元素 // ref绑定组件的用法与之类似 总结：\nVue2中可以通过ref直接操作单个DOM和组件 this.$refs.info.innerHTML Vue2中可以批量通过ref操作DOM和组件 this.$refs.fruits[0].innerHTML ref操作单个DOM元素\u0026mdash;-Vue3的规则 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;ref操作DOM和组件\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;!-- 3、模板中绑定上述返回的数据 --\u0026gt; \u0026lt;div ref=\u0026#39;info\u0026#39;\u0026gt;hello\u0026lt;/div\u0026gt; \u0026lt;!-- \u0026lt;my-com ref=\u0026#39;info\u0026#39;\u0026gt;hello\u0026lt;/my-com\u0026gt; --\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li ref=\u0026#39;fruit\u0026#39; v-for=\u0026#39;item in fruits\u0026#39; :key=\u0026#39;item.id\u0026#39;\u0026gt;{{item.name}}\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // this.$refs.info.innerHTML // this.$refs.fruit 的值应该是一个数组，数组中放的DOM元素 // this.$refs.fruit[0].innerHTML ---\u0026gt; apple // ----------------------------------------- // Vue3中通过ref操作DOM // 1、定义一个响应式变量 const info = ref(null) const fruits = ref([{ id: 1, name: \u0026#39;apple\u0026#39; }, { id: 2, name: \u0026#39;orange\u0026#39; }]) const handleClick = () =\u0026gt; { // 4、此时可以通过info变量操作DOM console.log(info.value.innerHTML) } // 2、把变量返回给模板使用 return { fruits, info, handleClick } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; 总结：操作单个DOM或者组件的流程\n定义一个响应式变量 把变量返回给模板使用 模板中绑定上述返回的数据 可以通过info变量操作DOM或者组件实例 获取v-for遍历的DOM或者组件 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;ref操作DOM和组件\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;!-- 3、模板中绑定上述返回的数据 --\u0026gt; \u0026lt;div ref=\u0026#39;info\u0026#39;\u0026gt;hello\u0026lt;/div\u0026gt; \u0026lt;!-- \u0026lt;my-com ref=\u0026#39;info\u0026#39;\u0026gt;hello\u0026lt;/my-com\u0026gt; --\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li :ref=\u0026#39;setFruits\u0026#39; v-for=\u0026#39;item in fruits\u0026#39; :key=\u0026#39;item.id\u0026#39;\u0026gt;{{item.name}}\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { // this.$refs.info.innerHTML // this.$refs.fruit 的值应该是一个数组，数组中放的DOM元素 // this.$refs.fruit[0].innerHTML ---\u0026gt; apple // ----------------------------------------- // Vue3中通过ref操作DOM // 1、定义一个响应式变量 const info = ref(null) const fruits = ref([{ id: 1, name: \u0026#39;apple\u0026#39; }, { id: 2, name: \u0026#39;orange\u0026#39; }, { id: 3, name: \u0026#39;pineapple\u0026#39; }]) // 定义操作DOM的函数 const arr = [] const setFruits = (el) =\u0026gt; { // 参数el表示单个DOM元素 arr.push(el) } const handleClick = () =\u0026gt; { // 4、此时可以通过info变量操作DOM // console.log(info.value.innerHTML) console.log(arr) } // 2、把变量返回给模板使用 return { fruits, info, handleClick, setFruits } } } \u0026lt;/script\u0026gt; \u0026lt;style lang=\u0026#34;less\u0026#34;\u0026gt; \u0026lt;/style\u0026gt; 总结：ref批量操作元素的流程\n定义一个函数 把该函数绑定到ref上（必须动态绑定） 在函数中可以通过参数得到单个元素，这个元素一般可以放到数组中 通过上述数组即可操作批量的元素 防止重复添加DOM \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;h2\u0026gt;组合API-ref操作DOM或组件{{info}}\u0026lt;/h2\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;!-- 2.绑定ref属性 --\u0026gt; \u0026lt;div ref=\u0026#39;target\u0026#39;\u0026gt;你好\u0026lt;/div\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li :ref=\u0026#39;setDom\u0026#39; v-for=\u0026#39;item in fruits\u0026#39; :key=\u0026#39;item.id\u0026#39;\u0026gt;{{item.name}}\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;button @click=\u0026#39;handleClick\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { onBeforeUpdate, ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { const info = ref(\u0026#39;hello\u0026#39;) // 1. 定义一个变量 const target = ref(null) const fruits = ref([{ id: 1, name: \u0026#39;apple\u0026#39; }, { id: 2, name: \u0026#39;peach\u0026#39; }, { id: 3, name: \u0026#39;orange\u0026#39; }]) // 批量绑定DOM const list = ref([]) const setDom = (ele) =\u0026gt; { // 参数ele表示其中一个DOM元素 if (ele) { list.value.push(ele) } } // 必须在任何数据变更之前重置数组，防止重复添加 onBeforeUpdate(() =\u0026gt; { list.value = [] }) const handleClick = () =\u0026gt; { // 3.通过target操作DOM元素 // console.log(target.value.innerHTML) // console.log(list.value[1].innerHTML) info.value = \u0026#39;nihao\u0026#39; console.log(list.value.length) } return { target, handleClick, fruits, setDom, info } } } \u0026lt;/script\u0026gt; 总结：每次变更数据，需要重置数组，防止重复添加\n组合API-父子通讯 # 父组件向子组件传递数据：自定义属性 props 子组件向父组件传递数据 : 自定义事件 $emit() 父传子：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;父子组件的交互\u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;money=200\u0026#39;\u0026gt;修改\u0026lt;/button\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;Child :money=\u0026#39;money\u0026#39; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import Child from \u0026#39;./Child.vue\u0026#39; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, components: { Child }, setup () { const money = ref(100) return { money } } } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; 子组件 {{money}} \u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;getMoney\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#39;Child\u0026#39;, props: { money: { type: Number, default: 0 } }, setup (props) { // Vue3中，使用形参props获取所有父组件传递的数据 // props的数据是只读的，不可以在子组件直接修改 const getMoney = () =\u0026gt; { console.log(props.money) } return { getMoney } } } \u0026lt;/script\u0026gt; 总结：\n子组件模板中直接可以获取props中的属性值 vue代码中需要通过setup函数的第一个形参props获取属性值 子传父：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;父子组件的交互\u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;money=200\u0026#39;\u0026gt;修改\u0026lt;/button\u0026gt; \u0026lt;hr\u0026gt; + \u0026lt;Child :money=\u0026#39;money\u0026#39; @send-money=\u0026#39;getMoney\u0026#39; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import Child from \u0026#39;./Child.vue\u0026#39; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, components: { Child }, setup () { const money = ref(100) + const getMoney = (value) =\u0026gt; { + // value就是子组件传递回来的钱 + money.value = money.value - value + } + return { money, getMoney } } } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; 子组件 {{money}} \u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;getMoney\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#39;Child\u0026#39;, + // 子组件触发的自定义事件需要在emits选项中进行声明（直观的看到本组件触发了那些自定义事件） + emits: [\u0026#39;send-money\u0026#39;], props: { money: { type: Number, default: 0 } }, setup (props, context) { // Vue3中，使用形参props获取所有父组件传递的数据 // props的数据是只读的，不可以在子组件直接修改 const getMoney = () =\u0026gt; { console.log(props.money) // this.$emit(\u0026#39;send-money\u0026#39;, 50) // 向父组件传递数据50 + context.emit(\u0026#39;send-money\u0026#39;, 50) } return { getMoney } } } \u0026lt;/script\u0026gt; 总结：\n通过setup函数的参数二context.emit方法触发自定义事件 context.emit('send-money', 50) 子组件触发的自定义事件需要在emits选项中进行声明（直观的看到本组件触发了那些自定义事件） 组合API-依赖注入 # 使用场景：有一个父组件，里头有子组件，有孙组件，有很多后代组件，共享父组件数据。\n演示代码：\n\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;父子组件的交互\u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;money=200\u0026#39;\u0026gt;修改\u0026lt;/button\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;Child :money=\u0026#39;money\u0026#39; @send-money=\u0026#39;getMoney\u0026#39; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import Child from \u0026#39;./Child.vue\u0026#39; + import { ref, provide } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, components: { Child }, setup () { + // 直接把数据传递出去 + provide(\u0026#39;moneyInfo\u0026#39;, 1000) const money = ref(100) const getMoney = (value) =\u0026gt; { // value就是子组件传递回来的钱 money.value = money.value - value } return { money, getMoney } } } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; + 孙子组件:{{moneyInfo}} \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; + import { inject } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;GrandSon\u0026#39;, setup () { + const moneyInfo = inject(\u0026#39;moneyInfo\u0026#39;) + return { moneyInfo } } } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; + 子组件 {{money}} --- {{moneyInfo}} \u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;getMoney\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;GrandSon /\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import GrandSon from \u0026#39;@/GrandSon\u0026#39; import { inject } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;Child\u0026#39;, components: { GrandSon }, // 子组件触发的自定义事件需要在emits选项中进行声明（直观的看到本组件触发了那些自定义事件） emits: [\u0026#39;send-money\u0026#39;], props: { money: { type: Number, default: 0 } }, setup (props, context) { + const moneyInfo = inject(\u0026#39;moneyInfo\u0026#39;) // Vue3中，使用形参props获取所有父组件传递的数据 // props的数据是只读的，不可以在子组件直接修改 const getMoney = () =\u0026gt; { console.log(props.money) // this.$emit(\u0026#39;send-money\u0026#39;, 50) // 向父组件传递数据50 context.emit(\u0026#39;send-money\u0026#39;, 50) } + return { getMoney, moneyInfo } } } \u0026lt;/script\u0026gt; 总结：\n父传子孙数据：provide 子孙得到数据：inject 孙子把数据直接传递给爷爷 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;父子组件的交互\u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;money=200\u0026#39;\u0026gt;修改\u0026lt;/button\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;Child :money=\u0026#39;money\u0026#39; @send-money=\u0026#39;getMoney\u0026#39; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import Child from \u0026#39;./Child.vue\u0026#39; import { ref, provide } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, components: { Child }, setup () { // 直接把数据传递出去 provide(\u0026#39;moneyInfo\u0026#39;, 1000) // 把一个函数传递给孙子 + const handleMoney = (value) =\u0026gt; { + console.log(\u0026#39;孙子传递的数据：\u0026#39;, value) + } + provide(\u0026#39;handleMoney\u0026#39;, handleMoney) const money = ref(100) const getMoney = (value) =\u0026gt; { // value就是子组件传递回来的钱 money.value = money.value - value } return { money, getMoney } } } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; 孙子组件:{{moneyInfo}} \u0026lt;/div\u0026gt; + \u0026lt;button @click=\u0026#39;handleSend\u0026#39;\u0026gt;点击给爷爷数据\u0026lt;/button\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { inject } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;GrandSon\u0026#39;, setup () { const moneyInfo = inject(\u0026#39;moneyInfo\u0026#39;) + const handleMoney = inject(\u0026#39;handleMoney\u0026#39;) + const handleSend = () =\u0026gt; { + // 调用爷爷传递函数 + handleMoney(200) + } return { moneyInfo, handleSend } } } \u0026lt;/script\u0026gt; 总结：子组件传递数据给爷爷组件，需要通过provide一个函数的方式实现\n爷爷组件传递一个函数，后续通过函数的形参获取数据 孙子组获取并调用该函数传递数据 v-model语法糖 # Vue2中v-model的应用场景 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;v-model指令用法\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;div\u0026gt;{{uname}}\u0026lt;/div\u0026gt; 用户名：\u0026lt;input type=\u0026#34;text\u0026#34; v-model=\u0026#39;uname\u0026#39;\u0026gt; \u0026lt;!-- v-model的本质是属性绑定和事件绑定的结合 --\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; :value=\u0026#39;uname\u0026#39; @input=\u0026#39;uname=$event.target.value\u0026#39;\u0026gt; \u0026lt;!-- ---------------------------------------------------------------- --\u0026gt; \u0026lt;!-- v-model也可以使用到组件上 --\u0026gt; \u0026lt;my-com v-model=\u0026#39;info\u0026#39;\u0026gt;\u0026lt;/my-com\u0026gt; \u0026lt;!-- 此时 $event表示子组件传递的具体数据 this.$emit(\u0026#39;input\u0026#39;, 100) --\u0026gt; \u0026lt;my-com :value=\u0026#39;info\u0026#39; @input=\u0026#39;info=$event\u0026#39;\u0026gt;\u0026lt;/my-com\u0026gt; \u0026lt;!-- Vue2中$event有两层含义 --\u0026gt; \u0026lt;!-- 1. 如果是原始DOM的事件，那么$event表示js的原生事件对象 --\u0026gt; \u0026lt;!-- 2、如果是组件的自定义事件，那么$event是$emit传递的数据 --\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, setup () { const uname = ref(\u0026#39;tom\u0026#39;) return { uname } } } \u0026lt;/script\u0026gt; 总结：Vue2中v-model的应用场景\n用到表单元素上：$event表示事件对象 用到组件上：$event表示子组件传递的数据 Vue3中v-model新的特性 v-model的本质是 :modelValue 和 @update:modelValue 两者的绑定 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;v-model指令用法-Vue3\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;div\u0026gt;{{info}}\u0026lt;/div\u0026gt; + \u0026lt;!-- \u0026lt;TestEvent v-model=\u0026#39;info\u0026#39; /\u0026gt; --\u0026gt; + \u0026lt;TestEvent :modelValue=\u0026#39;info\u0026#39; @update:modelValue=\u0026#39;info=$event\u0026#39; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import TestEvent from \u0026#39;./TextEvent.vue\u0026#39; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, components: { TestEvent }, setup () { const info = ref(\u0026#39;hello\u0026#39;) return { info } } } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; 子组件 {{modelValue}} \u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;handleEdit\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#39;TestEvent\u0026#39;, props: { + // Vue3中，v-model默认绑定的属性名称是modelValue + modelValue: { + type: String, + default: \u0026#39;\u0026#39; } }, setup (props, context) { const handleEdit = () =\u0026gt; { // props.modelValue = \u0026#39;nihao\u0026#39; // 通知父组件修改数据 // .sync修饰符：update:绑定的属性名称 + context.emit(\u0026#39;update:modelValue\u0026#39;, \u0026#39;nihao\u0026#39;) } return { handleEdit } } } \u0026lt;/script\u0026gt; v-model可以使用多次 \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div\u0026gt;v-model指令用法-Vue3\u0026lt;/div\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;div\u0026gt;{{info}}\u0026lt;/div\u0026gt; \u0026lt;div\u0026gt;{{msg}}\u0026lt;/div\u0026gt; \u0026lt;!-- v-model提供了一种双向绑定机制（在组件上-父子之间的数据交互） --\u0026gt; \u0026lt;!-- .sync修饰符已经被废弃，替代方案是v-model --\u0026gt; \u0026lt;!-- \u0026lt;TestEvent v-model=\u0026#39;info\u0026#39; :msg.sync=\u0026#39;msg\u0026#39; /\u0026gt; --\u0026gt; + \u0026lt;TestEvent v-model:modelValue=\u0026#39;info\u0026#39; v-model:msg=\u0026#39;msg\u0026#39; /\u0026gt; + \u0026lt;!-- \u0026lt;TestEvent :modelValue=\u0026#39;info\u0026#39; @update:modelValue=\u0026#39;info=$event\u0026#39; /\u0026gt; --\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; import TestEvent from \u0026#39;./TextEvent.vue\u0026#39; import { ref } from \u0026#39;vue\u0026#39; export default { name: \u0026#39;App\u0026#39;, components: { TestEvent }, setup () { const info = ref(\u0026#39;hello\u0026#39;) const msg = ref(\u0026#39;hi\u0026#39;) return { info, msg } } } \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; 子组件 {{modelValue}} \u0026lt;/div\u0026gt; \u0026lt;button @click=\u0026#39;handleEdit\u0026#39;\u0026gt;点击\u0026lt;/button\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;script\u0026gt; export default { name: \u0026#39;TestEvent\u0026#39;, props: { // Vue3中，v-model默认绑定的属性名称是modelValue modelValue: { type: String, default: \u0026#39;\u0026#39; }, msg: { type: String, default: \u0026#39;\u0026#39; } }, setup (props, context) { const handleEdit = () =\u0026gt; { // props.modelValue = \u0026#39;nihao\u0026#39; // 通知父组件修改数据 // .sync修饰符：update:绑定的属性名称 + context.emit(\u0026#39;update:modelValue\u0026#39;, \u0026#39;nihao\u0026#39;) + context.emit(\u0026#39;update:msg\u0026#39;, \u0026#39;coniqiwa\u0026#39;) } return { handleEdit } } } \u0026lt;/script\u0026gt; 总结：\nv-model可以通过绑定多个属性的方式，向子组件传递多个值并且保证双向绑定 可以替代Vue2中sync修饰符（sync修饰符在Vue3中已经被废弃） ","date":"2022年1月6日","externalUrl":"","permalink":"/vue%E7%9A%84vue2%E5%92%8Cvue3/","section":"文章","summary":" 什么是选项API写法：Options ApI\n咱们在vue2.x项目中使用的就是 选项API 写法\n","title":"vue的vue2和vue3","type":"posts"},{"content":" 概述 # Objective-C语言主要用于iOS和Mac开发，本文结合苹果规范和Google规范，再加上自己的个人经验总结出来的一系列编码规范。 苹果开发规范 apple object-c guide Google Objective-C规范google-objc-styleguide。\n代码缩进以及行代码 # Apple是以4个空格缩进，所以Xcode里面我们这样设置。关于每行代码最大长度我们选用100个字符作为最大长度限制。编写的代码长度不可以大于这个最大值。\n头文件引入 # 当引入Objective-C或者Objective-C++头文件的时候使用#import引入，当引入C或者C++头文件的时候使用#include引入 引入系统API时应该直接引用这个根头文件，而不是其它子模块的头文件，即使是你只用到了其中的一小部分，编译器会自动完成优化的。每一个框架都会有一个和框架同名的头文件，它包含了框架内接口的所有引用。正确做法#import \u0026lt;UIKit/UIKit.h\u0026gt; 错误做法 #import \u0026lt;UIKit/UIButton.h\u0026gt; 引入系统的API时使用#import \u0026lt;\u0026gt;格式 引入第三方框架优先使用#import \u0026lt;\u0026gt;格式 引入自己定义的类使用#import \u0026ldquo;\u0026ldquo;格式 将 import和其他的文件名之间加一个空格。如果有一个以上的 import 语句，就对这些语句进行分组。每个分组的注释是可选的。 注：对于模块使用 #import语法。除了子类化或是协议之外，最好使用 @class 这种方式，避免过多的头文件引入。在引入协议的时候，如果不是连当前类也引入的情况下，将协议单独声明出来再引入。\n命名规范 # 所有的命名必须是英文规范，命名应该尽量的清晰。 尽量以描述性的方式来命名。命名需要自达意（即看到命名，即可清楚知道代表的意义）。\n一致性 # 整个工程的命名风格要保持一致性，最好和苹果 SDK 的代码保持统一。不同类中完成相似功能的方法应该叫一样的名字，比如我们总是用 count 来返回集合的个数，不能在 A 类中使用 count 而在 B 类中使用 getNumber。\n前缀 # Apple规定：Apple保留所有两个字母前缀的使用权，因此为了防止与官方API冲突，项目中所有文件的前缀必须是三个(及以上)大写字母。 可以在为类、协议、函数、常量以及 typedef 宏命名的时候使用前缀，但注意不要为成员变量或者方法使用前缀，因为他们本身就包含在类的命名空间内。\nxcode 可以对所有的类添加前缀。\nClass命名规范 # class的名称应该由两部分组成，前缀+名称。即，class的名称应该包含一个前缀和一个名词。\nProtocol命名规范 # 有时候protocol只是声明了一堆相关方法，并不关联class。这种不关联class的protocol使用ing形式以和class区分开来。比如NSLocking而非NSLock。 如果proctocol不仅声明了一堆相关方法，还关联了某个class。这种关联class的protocol的命名取决于关联的class，然后再后面再加上protocol或delegate用于显示的声明这是一份协议。 代理方法命名规范 # 当特定的事件发生时，对象会触发它注册的代理方法。代理是 Objective-C 中常用的传递消息的方式。代理有它固定的命名范式。\n一个代理方法的第一个参数是触发它的对象，第一个关键词是触发对象的类名，除非代理方法只有一个名为 sender 的参数：。\n常量命名规范 # 不要使用 #define 宏来定义常量， #define 通常用来给编译器决定是否编译某块代码。 使用 const 定义基本数据类型或字符串常量，常量的命名规范和函数相同,但是常量一般用k。\n通知命名规范 # 通知常用于在模块间传递消息，所以通知要尽可能地表示出发生的事件，通知的命名范式是\n代码示例 # //[ 触发通知的类名 ] + [Did | Will] + [ 动作 ] + Notification 例如： NSApplicationDidBecomeActiveNotification NSWindowDidMiniaturizeNotification NSTextViewDidChangeSelectionNotification NSColorPanelColorDidChangeNotification 编码规范 # 文件夹，层级结构的规范\n代码组织规范 # 分块组织，如控制器页面生命周期方法放在一起，代理方法放在一起，属性相关方法放在一起。私有方法放在一起。内存管理的方法放在一起。\n工程结构规范 # 为了避免文件杂乱，物理文件应该保持和 Xcode 项目文件同步。Xcode 创建的任何组（group）都必须在文件系统有相应的映射。为了更清晰，代码不仅应该按照类型进行分组，也可以根据业务功能进行分组。建议 遵循先业务group，后mvc group。\n合理组织工程的内的文件夹，工程中一般包括但不限于以下几个文件夹Supporting Files（全局资源文件以及引入文件）、Library(第三方)，Base(基类)。\n通知使用规范 # 当我们使用通知时，必须要思考，有没有更好的办法来代替这个通知。禁止遇到问题就想到通知，把通知作为备选项而非首选项。\n分支语句规范 # if条件判断语句后面必须要加大括号{}。不然随着业务的发展和代码迭代，极有可能引起逻辑问题。\n多于3个逻辑表达式必须用参数分割成多个有意义的bool变量。\nbool的规范 # Objective-C使用YES和NO。因为true和false应该只在CoreFoundation，C或C++代码使用。既然nil解析成NO，所以没有必要在条件语句比较。不要拿某样东西直接与YES比较。\nCGRect函数规范 # 当访问CGRect里的x, y, width, 或 height时，应该使用CGGeometry函数而不是直接通过结构体来访问。引用Apple的CGGeometry：\n在这个参考文档中所有的函数，接受CGRect结构体作为输入，在计算它们结果时隐式地标准化这些rectangles。因此，你的应用程序应该避免直接访问和修改保存在CGRect数据结构中的数据。相反，使用这些函数来操纵rectangles和获取它们的特性。\nCGRect frame = self.view.frame; CGFloat x = CGRectGetMinX(frame); CGFloat y = CGRectGetMinY(frame); CGFloat width = CGRectGetWidth(frame); CGFloat height = CGRectGetHeight(frame); CGRect frame = CGRectMake(0.0, 0.0, width, height); 多线程规范 # 对剪贴板的读取必须要放在异步线程处理，最新Mac和iOS里的剪贴板共享功能会导致有可能需要读取大量的内容，导致读取线程被长时间阻塞。 仅当必须保证顺序执行时才使用dispatch_sync，否则容易出现死锁，应避免使用，可使用dispatch_async禁止在非主线程中进行UI元素的操作。 在主线程中禁止进行同步网络资源读取，使用NSURLSession进行异步获取。当然，你可以在子线程同步获取网络资源，但还是上面的那一条建议：避免使用dispatch_sync，尽量使用dispatch_async。因为死锁不一定只发生在主线程。 2.1 code # - (void)viewDidLoad { [super viewDidLoad]; // 错误。出现死锁，报错:EXC_BAD_INSTRUCTION。原因:在主队列中同步的添加一个block到主队列中 dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_block_t block = ^() { NSLog(@\u0026#34;%@\u0026#34;, [NSThread currentThread]); }; dispatch_sync(mainQueue, block); } 其他 # 不要在对象类型前和 protocol之间添加空格。 推荐使用方式\n// 推荐： @property (nonatomic, weak) id\u0026lt;OKDelegate\u0026gt; delegate; ","date":"2021年11月26日","externalUrl":"","permalink":"/ios%E5%9B%A2%E9%98%9F%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/","section":"文章","summary":" Objective-C语言主要用于iOS和Mac开发，本文结合苹果规范和Google规范，再加上自己的个人经验总结出来的一系列编码规范。 苹果开发规范 apple object-c guide Google Objective-C规范 …","title":"ios团队代码规范","type":"posts"},{"content":" 引言 # 2016年至今个人站一直放在github上，源文档md文件一个仓库， 部署文件夹放另外一个仓库，托管到github。阿里云活动申请了3年的ECS，还好不到二百大洋，能接受，同时申请了一个域名，一个htpps 证书，以后还想干些其他的，就用了docker容器来进行构建，使用了nginx作为后端服务器程序，使用了github ci/cd进行集成和部署。\ndocker # Docker 是一个开源的应用容器引擎，让开发者可以打包他们的应用以及依赖包到一个可移植的容器中, 然后发布到任何流行的Linu x或Windows操作系统的机器上个人理解 独立的虚拟主机，与ECS 隔离出来。\n启动容器 # docker可以将容器内的文件挂载关联在宿主机器上，保持容器加载的文件在宿主机器上即可使用。 docker 使用步骤 拉取images，运行镜像。平常 常用的有启动有nginx 相应的image。 挂载文件，关联 宿主以及容器相对应的端口。\nhttp\ndocker run --name docker_nginx -d -p 8080:80 -v /root/nginx-blog/nginx/log:/var/log/nginx -v /root/nginx-blog/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v /root/nginx-blog/nginx/conf.d:/etc/nginx/conf.d -v /root/nginx-blog/nginx/html:/usr/share/nginx/html nginx https\ndocker run --name nginxHttps -d -p 80:80 -p 443:443 -v /root/nginx-blog/nginx/log:/var/log/nginx -v /root/nginx-blog/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v /root/nginx-blog/nginx/conf.d:/etc/nginx/conf.d -v /root/nginx-blog/nginx/html:/usr/share/nginx/html -v /root/nginx-blog/nginx/cert:/etc/nginx/cert nginx 进入容器 # 进入容器进行，一些操作，如配置文件拷贝。\ndocker exec -it df2887652588 \u0026#34;/bin/bash\u0026#34; 查看运行的容器 # docker ps a 停止容器以及其他 # docker stop #Name或者ID docker start #Name或者ID docker kill #Name或者ID docker restart #name或者Id nginx # nginx.conf 是一个niginx配置文件，包括全局块，event，http 块。sever 块。location 块。\n1、全局块：配置影响nginx全局的指令。一般有运行nginx服务器的用户组，nginx进程pid存放路径，日志存放路径，配置文件引入，允许生成worker process数等。\n2、events块：配置影响nginx服务器或与用户的网络连接。有每个进程的最大连接数，选取哪种事件驱动模型处理连接请求，是否允许同时接受多个网路连接，开启多个网络连接序列化等。\n3、http块：可以嵌套多个server，配置代理，缓存，日志定义等绝大多数功能和第三方模块的配置。如文件引入，mime-type定义，日志自定义，是否使用sendfile传输文件，连接超时时间，单连接请求数等。\n4、server块：配置虚拟主机的相关参数，一个http中可以有多个server。\n5、location块：配置请求的路由，以及各种页面的处理情况。\nnginx 配置 较为难以控制。主要端口，html 控制，log 的文件格式。cert 文件配置。nginx配置文件夹为conf，conf.d ,log, html。其中conf的文件夹配置文件包含conf.d 文件夹里面。conf.d文件夹里面的文件例子如下。\nhttp\nserver { listen 80; server_name localhost; #charset koi8-r; #access_log /var/log/nginx/host.access.log main; location / { root /usr/share/nginx/html; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } 添加 https\nserver { listen 80; #侦听80端口，如果强制所有的访问都必须是HTTPs的，这行需要注销掉 listen 443 ssl; server_name www.dahangda.top; # ssl on; ssl_certificate /etc/nginx/cert/x.pem; ssl_certificate_key /etc/nginx/cert/x.key; ssl_session_cache shared:SSL:1m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { proxy_set_header HOST $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 这里写的是我的阿里云内网地址,不知道为啥, proxy_pass http://x.x.x.x:x; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } conf的文件夹nginx配置\nuser nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; \u0026#39; \u0026#39;$status $body_bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#39; \u0026#39;\u0026#34;$http_user_agent\u0026#34; \u0026#34;$http_x_forwarded_for\u0026#34;\u0026#39;; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } github action # 每一个任务叫Action\nGitHub Actions 有一些自己的术语。\n（1）workflow （工作流程）：持续集成一次运行的过程，就是一个 workflow。触发条件是什么，\n（2）job （任务）：一个 workflow 由一个或多个 jobs 构成，含义是一次持续集成的运行，可以完成多个任务。\n（3）step（步骤）：每个 job 由多个 step 构成步骤里面带有名字name，一步步完成，显示在github控制台。\n（4）action （动作）：每个 step 可以依次执行一个或多个命令（action）。\n# main.yml name: deploy to aliyun on: push: branches: - master jobs: build: runs-on: ubuntu-latest steps: # 切换分支，github显示的文字 - name: Checkout # 仓库或者使用的docker uses: actions/checkout@master # 下载 git submodule - uses: srt32/git-actions@v0.0.3 with: args: git submodule update --init --recursive # 使用 node:10 - name: use Node.js 10 uses: actions/setup-node@v1 with: node-version: 10 # npm install - name: npm install and build run: | npm install npm run build env: CI: true # Deploy - name: Deploy uses: easingthemes/ssh-deploy@v2.0.7 env: SSH_PRIVATE_KEY: ${{ secrets.ACCESS_TOKEN }} ARGS: \u0026#34;-avz --delete\u0026#34; SOURCE: \u0026#34;[Current File Dir]\u0026#34; REMOTE_HOST: \u0026#34;[Domain]\u0026#34; REMOTE_USER: \u0026#34;[UserName]\u0026#34; TARGET: \u0026#34;[Server Dir]\u0026#34; 环境参数 # 读取 github的参数，可登陆阿里云服务器，切记不要写入文件内。\nhttps证书 # 阿里云 申请免费的一年，足够使用了。\n总结 # 难点包含的是 https 的配置，github yml 配置（借用github actionMarket）。主要是ssh配置，登陆使用 推送到远程 需要支持https。\n","date":"2021年10月25日","externalUrl":"","permalink":"/%E9%98%BF%E9%87%8C%E4%BA%91hugo%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E6%8C%87%E5%8D%97/","section":"文章","summary":" 2016年至今个人站一直放在github上，源文档md文件一个仓库， 部署文件夹放另外一个仓库，托管到github。阿里云活动申请了3年的ECS，还好不到二百大洋，能接受，同时申请了一个域名，一个htpps 证书，以后还想干些其他的，就用 …","title":"阿里云hugo博客搭建指南","type":"posts"},{"content":" 前言 # 解决无网络状态下无 pod 库以及库不一致问题。省去下载，安全问题。\npod 库的制作 # 暴露 文件（引用使用） 隐藏 文件 （.m 文件，静态库等） 资源 文件 podspec 文件 podspec # 为方便 pod install 运行时，能引用到相应的文件的一个配置文件，方便项目引用。常用的配置项是暴露的.h 文件。外界库文件的引用，系统库的引用（framework，.a ）。\nvendored_libraries 是引用外界静态库，配置项 Libraries 为 podspec 同层下。\ns.vendored_libraries = \u0026lsquo;Libraries/libMasonry.a\u0026rsquo;\nvendored_frameworks：配置需要引用的非系统框架（**/*的写法后面不再赘述）\nvendored_frameworks = \u0026lsquo;Frameworks/MyFramework.framework\u0026rsquo;\nsource_files 是引用所有源文件配置项，一般是.h 文件与.m 文件。\ns.source*files = \u0026lsquo;Headers/**/_.{h}\u0026rsquo; - s.frameworks 是系统的 framework 配置项。 s.frameworks = \u0026ldquo;MobileCoreServices\u0026rdquo;, \u0026ldquo;SystemConfiguration\u0026rdquo;, \u0026lsquo;AVFoundation\u0026rsquo;, \u0026lsquo;CoreTelephony\u0026rsquo;, \u0026lsquo;CoreMedia\u0026rsquo;, \u0026lsquo;AudioToolbox\u0026rsquo;, \u0026lsquo;VideoToolbox\u0026rsquo;\ns.libraries 配置系统提供的一些库，比如我们要引用 libz.tbd 等，我们需要如下配置写法。\ns.libraries = \u0026ldquo;c++\u0026rdquo;, \u0026ldquo;sqlite3.0\u0026rdquo;, \u0026ldquo;z\u0026rdquo;\nframeworks 与.a 文件 # frameworks 与 可以理解为一个框架，这个框架包括.h .a 资源文件三部分。.a 是.m 文件的封装。\npod 库地址 # 使用../ 来找到相应地址。用以引用头文件，此为离线库。\n使用pod git来修改库的下载地址为内网地址。此为内网库。\n参考 # 引用参考 简书文章 1 简书文章 2\n","date":"2020年9月17日","externalUrl":"","permalink":"/cocoapods%E7%A6%BB%E7%BA%BF%E5%BA%93%E4%BB%A5%E5%8F%8A%E5%86%85%E7%BD%91%E5%BA%93/","section":"文章","summary":" 解决无网络状态下无 pod 库以及库不一致问题。省去下载，安全问题。\n暴露 文件（引用使用） 隐藏 文件 （.m 文件，静态库等） 资源 文件 podspec 文件 为方便 pod install 运行时，能引用到相应的文件的一个配置文件 …","title":"cocoapods离线库以及内网库","type":"posts"},{"content":" 前言 # Crash 防护 分为两个方面 开始阶段拒绝出现，要不就是预测所有的情况进行处理。\n第一个是网络返回的数据类型校验，可以使用第三方工具在字典转模型的时候统一校验，防止崩溃，\n第二个方面是在 code 里面的引起的崩溃。\n源头上其实需要代码生产者编码的时候注意下，假设出现触发的情况如何处理。\ncode 中的 crash # 一共有6种情况，分别如下\n1.unrecognized selector crash 为 NSObject 添加扩展，swizzle 消息转发的第二个方法，将消息转发给中间类，这个中间类 动态添加方法，并调用。同时返回 NSNull 。参考\n2.KVO/KVC crash kvo 可以使用基类中添加数组（管理观察者）使用 kvo 的时候将观察者放入数组中，dealoc 使用清空观察者。 kvc swizzle 所有的可能被调用的 set 与 get 方法，然后使用 Try catch 方式守护。\n3.NSNotification crash 4.NSTimer crash 使用中间类引用 NSTimer，便可以解决。\n5.Container crash（数组越界，插 nil 等） 对 常用的集合方法进行 swizzle,重写的方法要使用一些判断即可。\n6.NSString crash （字符串操作的 crash） 与集合操作类似\n7.Bad Access crash （野指针） 这个目前看没有最好的方法，即使加上防护也可能出现野指针的现象\n8.UI not on Main Thread Crash (非主线程刷 UI(机制待改善)) 刷新地方使用线程判断以及硬切换。即可避免。（可参考 SDwebImge 中的使用）\n总结 # 防护的中心思想第一在事件发生的时候进行杜绝。尽量使用保护策略，不行的话只能崩溃发现时候利用奔溃出现的 进行定位，在源头发生的地方进行解决。\n参考 # 简书 ","date":"2020年2月19日","externalUrl":"","permalink":"/ios%E7%9A%84crash%E6%83%85%E5%86%B5%E4%BB%A5%E5%8F%8A%E9%98%B2%E6%8A%A4/","section":"文章","summary":" Crash 防护 分为两个方面 开始阶段拒绝出现，要不就是预测所有的情况进行处理。\n","title":"iOS的Crash情况以及防护","type":"posts"},{"content":" 前言 # App 启动过程包括点击 App 图标到 app 出现首页渲染完成。这个启动过程是由三部分组成。第一部分 main 执行前（T1），第二 main 函数执行后到首页加载（T2），首页加载到渲染完成（T3）。\nmain 执行前的过程 T1 # 可以通过 xcode 设置打印相应阶段耗时 设置方法： 只需要在 Edit scheme -\u0026gt; Run -\u0026gt; Environment Variables 中将环境变量 DYLD_PRINT_STATISTICS 设为 1， T1 过程是可执行文件(.o)和 Dyld 加载到内存。Dyld 进行动态链接。再执行一系列动态链接操作和初始化操作的过程（加载、绑定、及初始化方法） 上图打印的过程是 Dyld 加载到内存后的操作。这些操作是 Dyld 进行的。下图来说明 Dyld 做了哪些工作。 这些过程是我们主要关注的，至于可执行文件与 Dyld 加载到内存我们不需要关心，这个是数据 copy 速度很快。\n优化 # 动态库数量（非系统动态库合并，最多 6 个）合并动态库方法\nobjc 类，方法 数量减少\nTEXT:objc_methname:中包含了代码中的所有方法，而DATAobjc_selrefs 中则包含了所有被使用的方法的引用，通过取两个集合的差集就可以得到所有未被使用的代码。可以objc_cover中的方法。\nload 方法减少或者用+initialize() 代替。\nC 的 constructor 与控制 C++ 全局变量的数量。\nmain 函数后 T2 阶段 # 在 main()之后主要工作是各种集成的第三发启动项的执行。包括调试工具，页面配置，第三放统计以及功能的 sdk 开启等。\n优化 # 分阶段启动\n页面配置相关的可以在 willFinishLaunch 阶段被执行，功能 sdk 可以放在首页加载完成后启动。\n规范启动项添加 输出文档规范\n组件话的话使用 自动注册启动。\n首屏页面的加载和渲染 T3 阶段 # 主界面的构建，例如 TabBarVC，HomeVC 等等，网络资源的加载，如图片 I/O、图片解码、控件的布局。可以查看主线程上这些方法的执行时间了解哪些方法耗时多，然后进行优化。\nTime Profiler # Time Profiler 是 Xcode 自带的时间性能分析工具，它按照固定的时间间隔来跟踪每一个线程的堆栈信息，通过统计比较时间间隔之间的堆栈状态，来推算某个方法执行了多久，并获得一个近似值。就是或者方法的执行时间。Time Profiler 使用\n小结：定时抓取主线程上的方法调用堆栈，计算一段时间里各个方法的耗时原因，自己可以实现相应功能的工具，集成到自己的 App 中，以便在真实环境中进行检查。或者利用fishhook与 objc_msgSend 汇编相关知识实现耗时方法的打印。相关开源里使用GCDFetchFeed中 SMCallTrace 部分。不止主页面其他页面的加载也可以使用它定位加载慢的问题。\n优化 # 串行改并行 io 操作使用资源较小的代替 利用闪屏加载首屏 首屏使用代码布局 网络资源的预缓存 首页测试代码进行删除 参考 # 美团外卖 iOS App 冷启动治理 ","date":"2019年12月25日","externalUrl":"","permalink":"/ios%E5%90%AF%E5%8A%A8%E5%88%B0%E9%A6%96%E9%A1%B5%E5%8A%A0%E8%BD%BD/","section":"文章","summary":" App 启动过程包括点击 App 图标到 app 出现首页渲染完成。这个启动过程是由三部分组成。第一部分 main 执行前（T1），第二 main 函数执行后到首页加载（T2），首页加载到渲染完成（T3）。\n","title":"ios启动到首页加载","type":"posts"},{"content":" 前言 # 解决内存提前释放，与weakself 相对。在使用的时候依然还存在。\nweakself # 首先 block 使用 weakself 来打破循环引用。新建的 weakself 指针 指向 self。 同时用 weak 来修饰 weakself。\n__weak typeof(self) weakself = self; 实现代码中的解析\n其中 typeof(self)返回的是这个 self 对象的类型。 这个时候可以在 block 里面 weakself。 strongself # strongself 是强引用指针，也就是引用加一。参与 self 的内存管理，保证了 block 中 self 一直存在， self 使用必须等到 block 运行完，这个强引用消失，self 才可以释放。如果用 weakself 在 block 里面执行一些 code，self 本身的内存管理 在外界决定的，self 计数可能为 0.导致 block 中有些 code 不执行。\n故为了保证 self 在 block 执行过程里一直存在，对他强引用 strongSelf。 实现代码\n__strong typeof(weakSelf) strongSelf = weakSelf; 方便使用的方法 # 宏定义 weakify与strongify，使用 @weakify(self);@strongify(self);\n#ifndef weakify #if DEBUG #if __has_feature(objc_arc) #define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object; #else #define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object; #endif #else #if __has_feature(objc_arc) #define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object; #else #define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object; #endif #endif #endif #ifndef strongify #if DEBUG #if __has_feature(objc_arc) #define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object; #else #define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object; #endif #else #if __has_feature(objc_arc) #define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object; #else #define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object; #endif #endif #endif 总结 # 外部新建 weaself 指针，内部新建 strongself 指针 解决了循环引用的问题有解决了 self 被提前换释放的问题。\n关于 Masonry ，它内部根本没有捕获变量 self，进入 block 的是 testButton，所以执行完毕后，block 会被销毁，没有形成环。所以，没有引起循环依赖。\n参考 # https://halfrost.com/ios_block_retain_circle/\n","date":"2018年8月19日","externalUrl":"","permalink":"/block-%E4%B8%AD-strongself%E5%89%96%E6%9E%90/","section":"文章","summary":" 解决内存提前释放，与weakself 相对。在使用的时候依然还存在。\n","title":"block 中 strongself剖析","type":"posts"},{"content":" 前言 # gcd 全名字是全局任务分发，线程管理的一种方式。GCD 更加趋近于底层性能较高。与gcd 相关的概念有，队列，队列组，同步异步函数。\n队列 # 其中管理任务的是队列，包括串行队列，并行队列。\n系统提供了全局队列\n（并行队列dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); ）\n系统提供了主队列\n（串行队列dispatch_get_main_queue()）。\n队列的不同指的是任务的分发不同，并行队列 一次下发多个任务，串行队列是一次下发一个任务。\n同步和异步 # 同步与异步是指任务执行是否需要等待其他任务执行完返回数据。同步是需要等待的，异步是不需要等待的。\n执行顺序（dispatch） # 使用同步函数（必须等到 block 执行完）与异步函数（不必等 block 执行完），来决定任务的执行顺序。同步用于阻塞当前线程。\n同步与异步 在执行的时候都是立马去执行，异步同步是一个开线程的能力而已 ，队列没有开线程的能力。\n同步执行串行队列 # 不开线程，等 blcok 任务执行完。会有阻塞当前线程的发生。（不开线程） 创建执行 注意：串行队列异步执行有开线程的能力\n异步执行并行队列 # 开线程不等待当前任务执行完返回数据，就可以执行接下来的任务。接下来的任务在在另外的线程执行。（开线程） 注意： 并行队列同步不开线程\n队列与线程 # 防止在串行队列中使用同步操作，会造成死锁。\n同步操作执行完才会返回。容易出现相互等待的情况。 例如：主线程执行同步操作会出现死锁。\n死锁例子 队列组与队列，队列完成通知 # 可解决多个如 ABC 任务（不分先后）执行后执行 D 任务。注意 ABC 开始执行是同步任务，在 D 之前这个用队列组没有问题，如果 ABC 是异步的话使用就会出现问题。\n任务中还嵌套了异步任务，线程组不会管这个嵌套异步任务是否执行完成，调用了这个任务就 OK 了，因为是异步的我就不会等你执行完成就继续下一步了，一旦线程组第一层的任务都执行完成了就会调用通知。如果将异步换成同步的，就比需要等待嵌套任务执行完成才回去通知。\nABC 同步任务 d 在 abc 执行后执行 如果是异步分发同步任务 ABC（等同步执行完）执行 D，可以使用 GCD group 与 groupnotify。或者 NSBlockOperation 依赖解决。以及栅栏函数 dispatch_barrier(a)sync。\nNSBlockOperation 依赖 # dispatch*barrier_async（栅栏函数） # dispatch_barrier*(a)sync 其中需要在新并行队列执行，异步栅栏函数或者同步栅栏函数就定了在执行栅栏函数内 block 是否要等待结果。参考\n在同步栅栏时栅栏函数在主线程中执行,而异步栅栏中开辟了子线程栅栏函数在子线程中执行\n例如图片下载 1，2，3，4 四个图片下载完在显示。上面使用都会无效。就要使用下面的结合信号量完成。\ndispatch_semaphore（信号量） # 信号常用的就是三个函数\nABC 三个任务改成异步任务(如 AFN 网络请求)、全部回调成功后进行数据整合。 使用 dispatch_semaphore_wait 将 blcok 的异步变为同步，只有 dispatch_semaphore_wait 执行完，才能算 group 的任务执行完。同时信号量也可以实现锁的功能，这个可在 SDWebImageManager 中看到。\ndispatch_group_enter 和 dispatch_group_leave（任务组） # 利用了 dispatch_group_enter 与 dispatch_group_leave 成对出现来完成同步功能，然后发 group 通知。\n任务整合总结 # 等待资源完成，在执行。\n异步并发执行同步任务后整合，\n使用 dispatch_group,以及通知，dispatch_barrier 和 NSOperation 的依赖\n异步并发执行异步任务后整合，\n可以用 dispatch_group+semaphore 初始化信号为 0，执行完一个异步通过 singnal 释放一个 wait 任务。\n例子如：多个任务网络请求，NSOperation 我感觉无法实现多网络任务执行完之后再最终统一执行某一个人任务。这个时候只有用 dispatch_group + semaphore 来实现。 或者用 disaptch_group_enter 和 dispatch_group_leave\ndispatch_after 是任务追加函数，\ndispatch_group_wait 超时判断函数。\n总结 # 代码执行时，资源的合理利用。任务分发机制（队列），任务的执行机制（异步函数同步函数）。 并发编程中，一般会面对这样的三个问题：竞态条件、优先倒置、死锁问题。 使用 tip # gcd 队列，异步同步，信号量，锁，栅栏函数，一次性代码，延迟执行，定时器。\n参考 # https://blog.csdn.net/Deft_MKJing/article/details/51518556 https://www.jianshu.com/p/51fd1362249e\n","date":"2018年7月19日","externalUrl":"","permalink":"/%E9%AB%98%E6%80%A7%E8%83%BD%E5%A4%9A%E7%BA%BF%E7%A8%8Bgcd/","section":"文章","summary":" gcd 全名字是全局任务分发，线程管理的一种方式。GCD 更加趋近于底层性能较高。与gcd 相关的概念有，队列，队列组，同步异步函数。\n","title":"高性能多线程gcd","type":"posts"},{"content":" 前言 # runtime 是一个运行时系统，是iOS object-c的核心，使 C语言 具有了面向对象的功能。runtime 库实现了面向对象的继承链。利用 c 的结构体和指针。并且有对象模型的定义。\n方法的解析（消息转发）在运行时，编译时不会对方法的实现进行校验，因此我们使用 runtime 库对运行时的对象的属性，方法进行操作。省去了 oc 层。运行 的时候对协议，属性，方法进行整合，如包括 rw 和 ro 整合。\nisa 与superclass # ​ 下图为说明： 图解 isa 解决对象的定义，类的定义。(对象方法的起源，类方法的起源) 图解 superClass 解决了继承问题。（方法调用的追溯）. 对象和类对象 # OC中几乎所有的类都继承自NSObject，OC的动态性也是通过NSObject实现的。OC 类、对象和方法等都对应了 C 中的结构体，而且我们都可以在 Rutime 源码中找到它们的定义。\nobjc_object # 我们可以找到NSObject的定义：\n@interface NSObject \u0026lt;NSObject\u0026gt; { Class isa OBJC_ISA_AVAILABILITY; } 可以看出NSObject里有一个指向Class的isa，其中对于Class的定义在objc.h：\ntypedef struct objc_class *Class; */// Represents an instance of a class.* struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; 总结：objc_class代表类对象，objc_object代表实例对象，objc_object的isa指向objc_class。这里可以得出一个结论，实例对象的isa是指向类（类对象）的。\nobjc_class # struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() const { return bits.data(); } } struct objc_object { private: isa_t isa; } union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; } class_data_bits_t # 我们再回来看类中的其他属性，之前表示类的属性、方法、以及遵循的协议都放在了class_data_bits_t中，更准确的说是放在了class_rw_t。\nstruct class_data_bits_t { friend objc_class; // Values are the FAST_ flags above. uintptr_t bits; class_rw_t* data() const { return (class_rw_t *)(bits \u0026amp; FAST_DATA_MASK); } } struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint16_t version; uint16_t witness; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; } struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; } 这里面引入了class_rw_t和class_ro_t（rw-readwrite，ro-readonly）两个结构体。可以看到class_rw_t是包含一个常量指针ro，结构体为class_ro_t。这里存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。在 ObjC 运行时的时候会调用 realizeClass 方法，将class_ro_t传入class_rw_t，所以新版的动态性是通过这种方式实现的。\nclass_rw_t # struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; // 方法列表 property_array_t properties; // 属性列表 protocol_array_t protocols; // 协议列表 Class firstSubclass; Class nextSiblingClass; char *demangledName; }; class_ro_t # class_ro_t *ro 是只读的，内部直接存储的直接就是 method_list_t、protocol_list_t 、property_list_t 类型的一维数组，数组里面分别存放的是类的初始信息，以 method_list_t 为例，method_list_t 中直接存放的就是 method_t，但是是只读的\n编译的类的方法，属性，成员变量属性协议等等都是存放在 class_ro_t 中的，当程序运行的时候，需要将分类中的列表跟类初始的列表合并在一起的时，就会将 class_ro_t 中的列表和分类中的列表合并起来存放在 class_rw_t 中，也就是说 class_rw_t 中有部分列表是从 class_ro_t 里面拿出来的。并且最终和分类的方法合并。\ncache_t # 一些多次调用的方法会放入这个集合中。\nmethod_t # 这是函数的结构体，里面包含3个成员变量。SEL是方法的名字name。types是类型编码，类型可参考Type Encoding。IMP是一个函数指针，指向的是函数的具体实现。在runtime中消息传递和转发的目的就是为了找到IMP，并执行函数。\nstruct method_t { SEL name; const char *types; MethodListIMP imp; struct SortBySELAddress : public std::binary_function\u0026lt;const method_t\u0026amp;, const method_t\u0026amp;, bool\u0026gt; { bool operator() (const method_t\u0026amp; lhs, const method_t\u0026amp; rhs) { return lhs.name \u0026lt; rhs.name; } }; }; 方法被编译为 Method 结构体，同时会有一个 sel 于 imp 对应关系表。可利用 runtime 库 中的函数拿到相应的 Method，imp。\nMethod 结构体包括 sel，imp，name。 sel 是方法的名字编号 imp 是方法实现的指针 name 方法的名字。 直接调用 imp 效率比较高。 objc_msgSend解析 # objc_msgSend # 向对象发送的消息在编译阶段被转化为 C 函数，即 objc_msgSend。objc_msgSend(receiver，selector) 在 runtime 库中有 objc_msgSend 实现。objc_msgSendSuper 的实现。\nobjc_msgSend 具体实现\n检测 selector target 。有些 selector 会被忽略掉不做处理，如果 target 为 nil 会被忽略掉也不处理。\n1.中都不满足，会根据 selector，找到对应 imp 进行执行。\n根据 target isa 指针找到对应的类，先从 cache 里面找，若可以找得到就跳到对应的函数去执行。找不到会在 methodLists 查找如果 methodLists 找不到，就到超类的方法列表里寻找，一直找，直到找到 NSObject 类为止。\n如果还找不到，Runtime 就提供了如下三种方法来处理：动态方法解析、消息接受者重定向、消息重定向，\n这三种方法的调用关系如下图：\n实际使用 # runtime 使用的话是使用里面定义好的 c 函数，如关联函数，获取 class 信息函数，反射函数。\n获取 class 的信息 # 属性列表，方法列表，协议列表，成员变量表 属性列表的使用\n动态归档解档使用\nunsigned int count = 0; Ivar *ivars = class_copyIvarList([Person class], \u0026amp;count); for (int i = 0; i \u0026lt; count; i++) { Ivar ivar = ivars[i]; const char * name = ivar_getName(ivar); NSString *key = [[NSString alloc] initWithUTF8String:name]; id value = [self valueForKeyPath:key]; [encoder encodeObject:value forKey:key]; } free(ivars); 字典转模型使用\nNSDictionary *dict = @ { @\u0026#34;name\u0026#34; : @\u0026#34;lxz\u0026#34;, @\u0026#34;age\u0026#34; : @18, @\u0026#34;gender\u0026#34; : @YES }; TestObject *object = [TestObject objectWithDict:dict]; @interface TestObject : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @property (nonatomic, assign) BOOL gender; + (instancetype)objectWithDict:(NSDictionary *)dict; @end @implementation TestObject + (instancetype)objectWithDict:(NSDictionary *)dict { return([[TestObject alloc] initWithDict:dict]); } - (instancetype)initWithDict:(NSDictionary *)dict { self = [super init]; if ( self ) { unsigned int count = 0; objc_property_t *propertys = class_copyPropertyList( [self class], \u0026amp;count ); for ( int i = 0; i \u0026lt; count; i++ ) { objc_property_t property = propertys[i]; const char *name = property_getName( property ); NSString *nameStr = [[NSString alloc] initWithUTF8String:name]; id value = [dict objectForKey:nameStr]; [self setValue:value forKey:nameStr]; } free( propertys ); } return(self); } 属性修改使用\nPerson *ps = [[Person alloc] init]; NSLog(@\u0026#34;ps-nickName: %@\u0026#34;,[ps valueForKey:@\u0026#34;nickName\u0026#34;]); //null // 第一步：遍历对象的所有属性 unsigned int count; Ivar *ivarList = class_copyIvarList([ps class], \u0026amp;count); for (int i= 0; i\u0026lt;count; i++) { // 第二步：获取每个属性名 Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSString *propertyName = [NSString stringWithUTF8String:ivarName]; if ([propertyName isEqualToString:@\u0026#34;_nickName\u0026#34;]) { // 第三步：匹配到对应的属性，然后修改；注意属性带有下划线 object_setIvar(ps, ivar, @\u0026#34;燕航\u0026#34;); } } NSLog(@\u0026#34;ps-nickName: %@\u0026#34;,[ps valueForKey:@\u0026#34;nickName\u0026#34;]); // 燕航 关联 # runtime 提供了給我们 3 个 API 以管理关联对象（存储、获取、移除）：\n是一个全局关联表来管理关联的对象以及被关联者的内存。\n// 关联对象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) // 获取关联的对象 id objc_getAssociatedObject(id object, const void *key) // 移除关联的对象 void objc_removeAssociatedObjects(id object) 其中的参数\nid object：被关联的对象（属性的所属方） const void *key：关联的 key，要求唯一 id value：关联的属性 objc_AssociationPolicy policy：内存管理的策略 使用例子 NSObject+AssociatedObject.m\n#import \u0026#34;NSObject+AssociatedObject.h\u0026#34; #import \u0026lt;objc/runtime.h\u0026gt; @implementation NSObject (AssociatedObject) - (void)setAssociatedObject:(id)associatedObject { objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)associatedObject { return objc_getAssociatedObject(self, _cmd); } @end 第三方框架都有使用如 massonry，MJRefresh 等。 注意：尽管可以模拟地为分类添加“属性”，但毕竟只是模拟。在分类中@property 不会生成_变量，也不会实现 getter 和 setter 方法。我们实现的只是 getter 和 setter 方法，并没有自动生成下划线开头的变量！\n操作方法 # class_replaceMethod 替换类方法的定义 method_exchangeImplementations 交换 2 个方法的实现 method_setImplementation 设置 1 个方法的实现 其中 method_exchangeImplementations 比较常用，替换系统方法调用。如埋点，页面统计，崩溃保护。\n交换解决崩溃（可以做崩溃保护 Method Swizzling）\n@implementation NSArray (Safe) + (void)load { //注意__NSArray0 类族 [NSClassFromString(@\u0026#34;__NSArray0\u0026#34;) swapMethod:@selector(objectAtIndex:) currentMethod:@selector(ls_zeroObjectAtIndex:)]; } - (id)ls_zeroObjectAtIndex:(NSUInteger)index { if (index \u0026gt;= self.count) { return nil; } return [self ls_zeroObjectAtIndex:index]; } //交换两个实例方法（ - 方法） + (void)swapMethod:(SEL)originMethod currentMethod:(SEL)currentMethod; { Method firstMethod = class_getInstanceMethod(self, originMethod); Method secondMethod = class_getInstanceMethod(self, currentMethod); method_exchangeImplementations(firstMethod, secondMethod); } 反射机制 # NSString、Class、SEL、Protocol 的转化\nFOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector); FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName); FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass); FOUNDATION_EXPORT Class _Nullable NSClassFromString(NSString *aClassName); FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); FOUNDATION_EXPORT Protocol * _Nullable NSProtocolFromString(NSString *namestr) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) 参考 # https://cloud.tencent.com/developer/article/1158131 https://cloud.tencent.com/developer/article/1336352 ","date":"2018年3月19日","externalUrl":"","permalink":"/object-c-runtime%E4%B8%AD%E5%AF%B9%E8%B1%A1/","section":"文章","summary":" runtime 是一个运行时系统，是iOS object-c的核心，使 C语言 具有了面向对象的功能。runtime 库实现了面向对象的继承链。利用 c 的结构体和指针。并且有对象模型的定义。\n","title":"runtime中的对象","type":"posts"},{"content":" 概述 # GNU C 的一大特色就是__attribute__ 机制。attribute 可以设置函数属性（Function Attribute ）、变量属性（Variable Attribute ）和类型属性（Type Attribute ）。\n主要在object-c 的源码中可以见到，有助于源码的理解。\n\u0026lsquo;attribute\u0026rsquo; 书写特征是：attribute 前后都有两个下划线，并切后面会紧跟一对原括弧，括弧里面是相应的__attribute__ 参数。\n\u0026lsquo;attribute\u0026rsquo; 语法格式为：\u0026quot;attribute ((attribute-list))\u0026quot;,其位置约束为：放于函数声明的尾部“ ；” 之前。\n使用__attribute__描述函数的属性 # 函数属性可以帮助开发者把一些特性添加到函数声明中，从而可以使编译器在错误检查方面的功能更强大。一般在attribute后边。\nattribute 的format属性 # 该__attribute__的属性可以给被声明的函数加上类似printf或者scanf的特征，\u0026ldquo;它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配\u0026rdquo;。该功能十分有用，尤其是处理一些很难发现的bug。\nformat的语法格式为： format (archetype, string-index, first-to-check)\nformat属性告诉编译器，按照printf, scanf, strftime或strfmon的参数表格式规则对该函数的参数进行检查。“archetype”指定是哪种风格；“string-index”指定传入函数的第几个参数是格式化字符串；“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。\n具体使用格式如下： “attribute((format(printf,m,n)))” attribute((format(scanf,m,n)))\n其中参数m与n的含义为：\nm：第几个参数为格式化字符串（format string）； n：参数集合中的第一个，即参数“…”里的第一个参数在函数参数总数排在第几，注意，有时函数参数里还有“隐身”的呢，后面会提到； 在使用上，attribute((format(printf,m,n)))是常用的，而另一种却很少见到。下面举例说明，其中myprint为自己定义的一个带有可变参数的函数，其功能类似于printf：\n//m=1；n=2 extern void myprint(const char *format,...) __attribute__((format(printf,1,2))); //m=2；n=3 extern void myprint(int l，const char *format,...) __attribute__((format(printf,2,3))); //需要特别注意的是，如果myprint是一个函数的成员函数，那么m和n的值可有点“悬乎”了，例如： //m=3；n=4 extern void myprint(int l，const char *format,...) __attribute__((format(printf,3,4))); //其原因是，类成员函数的第一个参数实际上一个“隐身”的“this”指针。（有点C++基础的都知道点this指针，不知道你在这里还知道吗？） 这里给出测试用例：attribute.c，代码如下：\n2：extern void myprint(const char *format,...) __attribute__((format(printf,1,2))); 3： 4：void test() 5：{ 6： myprint(\u0026#34;i=%d\\n\u0026#34;,6); 7： myprint(\u0026#34;i=%s\\n\u0026#34;,6); 8： myprint(\u0026#34;i=%s\\n\u0026#34;,\u0026#34;abc\u0026#34;); 9： myprint(\u0026#34;%s,%d,%d\\n\u0026#34;,1,2); 10：} attribute 相关其他属性 # 不只是上面format。attribute const 。该属性只能用于带有数值类型参数的函数上。当重复调用带有数值参数的函数时，由于返回值是相同的，所以此时编译器可以进行优化处理，除第一次需要运算外，其它只需要返回第一次的结果就可以了，进而可以提高效率。该属性主要适用于没有静态状态（static state）和副作用的一些函数，并且返回值仅仅依赖输入的参数。\nextern int square(int n) __attribute__((const)); ... for (i = 0; i \u0026lt; 100; i++ ){ total += square(5) + i; } 参考链接\n","date":"2017年9月17日","externalUrl":"","permalink":"/gnu-c%E7%89%B9%E8%89%B2__attribute__/","section":"文章","summary":" GNU C 的一大特色就是__attribute__ 机制。attribute 可以设置函数属性（Function Attribute ）、变量属性（Variable Attribute ）和类型属性（Type Attribute ）。\n","title":"_Attribute_","type":"posts"},{"content":" 前言 # 一个异步下载图片并且支持缓存的 UIImageView 分类 sdwebimage 使用分类协议扩展通知实现了图片下载（异步），缓存（自动），解码（ WebP 格式） code 文件结构 # manager（管理下载和缓存，供给上层 webCache Categories 使用） downloader（下载模块） cache（缓存模块包括 SDImageCache 与 SDDiskCache） Decoder（解码模块） utils（错误，平台，下载指示器，声明，宏定义，通用协议，） categories（数据分类如 NSdata 工具分类） webCache Categories（UIkit 相关的分类） 模块之 webCache Categories # UIkit 中的 uiview 作为 UIImageView 和 UIBtton 父类为其他子类提供便利方法来实现共用。uiview 的便利方法使用分类实现。 //所有 View Category 的 setImageUrl； - (void)sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock; 主要模块 # SDWebImageCompat 做机型适配的。 SDWebImageDownloader 异步下载器专用和优化图像加载。 SDWebImagePrefetcher 图片的预加载。 待续 # ","date":"2017年8月12日","externalUrl":"","permalink":"/%E4%BB%8E%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E7%9C%8Bsdwebimage/","section":"文章","summary":" 一个异步下载图片并且支持缓存的 UIImageView 分类 sdwebimage 使用分类协议扩展通知实现了图片下载（异步），缓存（自动），解码（ WebP 格式） manager（管理下载和缓存，供给上层 webCache …","title":"从文件结构看sdwebimage","type":"posts"},{"content":" 前言 # SDWebImageManager 是一个单利，管理整个 SDWebImge 这个库的下载，缓存，回调展示。\n属性功能有 imageCache（查找工具），imageLoader（下载工具），runningOperations（用以放 DWebImageCombinedOperation）。failedURLsLock，runningOperationsLock是dispatch_semaphore_t锁。信号量为 1，即只能用一个线程访问如果多个线程访问就会阻塞问题。保证写入数据时候只能一个线程操作。\nSDWebImageManager 中 方法逻辑 SDWebImageCombinedOperation # SDWebImageCombinedOperation，这个联合操作具有下载，缓存两个功能。以属性的形式依附在SDWebImageCombinedOperation中。每一图片都有经历下载和缓存两个步骤所以 用一个联合操作定义这两个步骤。为了说明联合操作有取消的功能，必须实现 cancnel方法。 SDWebImageCombinedOperation会与 key 进行关联，UIView+WebCache.m可以看出来相应的关联。 因为SDWebImageCombinedOperation 只有在 SDWebImageManager 中使用，所以定义在一起。同时SDWebImageCombinedOperation 对象有个 weak SDWebImageManager 对象，方便拿到自己的属于的管理者。 SDWebImageCombinedOperation会被加到 SDWebImageManager 对象的 NSMutableSet 属性中。 方便会被加到 SDWebImageManager 统一管理。（统一取消操作） - (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock { // Invoking this method without a completedBlock is pointless NSAssert(completedBlock != nil, @\u0026#34;If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead\u0026#34;); // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won\u0026#39;t // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString. if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } // Prevents app crashing on argument type error like sending NSNull instead of NSURL if (![url isKindOfClass:NSURL.class]) { url = nil; } SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; operation.manager = self; BOOL isFailedUrl = NO; if (url) { SD_LOCK(self.failedURLsLock); isFailedUrl = [self.failedURLs containsObject:url]; SD_UNLOCK(self.failedURLsLock); } if (url.absoluteString.length == 0 || (!(options \u0026amp; SDWebImageRetryFailed) \u0026amp;\u0026amp; isFailedUrl)) { [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @\u0026#34;Image url is nil\u0026#34;}] url:url]; return operation; } SD_LOCK(self.runningOperationsLock); [self.runningOperations addObject:operation]; SD_UNLOCK(self.runningOperationsLock); // Preprocess the options and context arg to decide the final the result for manager SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context]; // Start the entry to load image from cache [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock]; return operation; } Query cache process # Query cache process 缓存查询，使用 block 逆传值。查询不到后进行下载操作。下载完成后先缓存缓存。\n根据shouldQueryCache判断是否直接进行下载即执行还是经历过查找缓存后进行下载。 根据传进来的联合操作判断是否用户取消了这个操作，如果取消了，直接进行 return。并给外界相应的提示。 整体方法\n// Query cache process - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // Check whether we should query cache BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); if (shouldQueryCache) { id\u0026lt;SDWebImageCacheKeyFilter\u0026gt; cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; @weakify(operation); operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url]; [self safelyRemoveOperationFromRunning:operation]; return; } // Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock]; }]; } else { // Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock]; } } 带缓存结果的下载操作\n// Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock]; 不带缓存结果的下载操作\n// Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock]; 区别是 cachedImage 是否为 nil。\nDownload process # 主方法为callDownloadProcessForOperation。这个是在 Query cache process 过程总调用。callDownloadProcessForOperation都会接入 cachedImage 只是为 nil 或者有值。 检查是否应该下载，如果缓存图片不在，或者 SDWebImageRefreshCached 就应该进行下载。 其中如果缓存图片存在，且设置为 SDWebImageRefreshCached callCompletionBlockForOperation是需要调用两次。\n注意缓存图片存在有可能也要下载，由 SDWebImageRefreshCached。\n// Download process - (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context cachedImage:(nullable UIImage *)cachedImage cachedData:(nullable NSData *)cachedData cacheType:(SDImageCacheType)cacheType progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // Check whether we should download image from network BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly); shouldDownload \u0026amp;= (!cachedImage || options \u0026amp; SDWebImageRefreshCached); shouldDownload \u0026amp;= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]); shouldDownload \u0026amp;= [self.imageLoader canRequestImageForURL:url]; if (shouldDownload) { if (cachedImage \u0026amp;\u0026amp; options \u0026amp; SDWebImageRefreshCached) { // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server. [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image. SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage; context = [mutableContext copy]; } @weakify(operation); operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url]; } else if (cachedImage \u0026amp;\u0026amp; options \u0026amp; SDWebImageRefreshCached \u0026amp;\u0026amp; [error.domain isEqualToString:SDWebImageErrorDomain] \u0026amp;\u0026amp; error.code == SDWebImageErrorCacheNotModified) { // Image refresh hit the NSURLCache cache, do not call the completion block } else if ([error.domain isEqualToString:SDWebImageErrorDomain] \u0026amp;\u0026amp; error.code == SDWebImageErrorCancelled) { // Download operation cancelled by user before sending the request, don\u0026#39;t block failed URL [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url]; } else if (error) { [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url]; BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error]; if (shouldBlockFailedURL) { SD_LOCK(self.failedURLsLock); [self.failedURLs addObject:url]; SD_UNLOCK(self.failedURLsLock); } } else { if ((options \u0026amp; SDWebImageRetryFailed)) { SD_LOCK(self.failedURLsLock); [self.failedURLs removeObject:url]; SD_UNLOCK(self.failedURLsLock); } // Continue store cache process [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; } if (finished) { [self safelyRemoveOperationFromRunning:operation]; } }]; } else if (cachedImage) { [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; [self safelyRemoveOperationFromRunning:operation]; } else { // Image not in cache and download disallowed by delegate [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url]; [self safelyRemoveOperationFromRunning:operation]; } } 从方法主方法中可以看到 下载完成后并没有立即调用 callCompletionBlockForOperation而是调用了callStoreCacheProcessForOperation。\nStore cache process # 这个函数主要进行已经下载图片缓存处理。\n// Store cache process - (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context downloadedImage:(nullable UIImage *)downloadedImage downloadedData:(nullable NSData *)downloadedData finished:(BOOL)finished progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // the target image store cache type SDImageCacheType storeCacheType = SDImageCacheTypeAll; if (context[SDWebImageContextStoreCacheType]) { storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue]; } // the original store image cache type SDImageCacheType originalStoreCacheType = SDImageCacheTypeNone; if (context[SDWebImageContextOriginalStoreCacheType]) { originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue]; } id\u0026lt;SDWebImageCacheKeyFilter\u0026gt; cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; id\u0026lt;SDImageTransformer\u0026gt; transformer = context[SDWebImageContextImageTransformer]; id\u0026lt;SDWebImageCacheSerializer\u0026gt; cacheSerializer = context[SDWebImageContextCacheSerializer]; BOOL shouldTransformImage = downloadedImage \u0026amp;\u0026amp; (!downloadedImage.sd_isAnimated || (options \u0026amp; SDWebImageTransformAnimatedImage)) \u0026amp;\u0026amp; transformer; BOOL shouldCacheOriginal = downloadedImage \u0026amp;\u0026amp; finished; BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // if available, store original image to cache是否缓存原始数据 if (shouldCacheOriginal) { // normally use the store cache type, but if target image is transformed, use original store cache type instead通常使用存储缓存类型，但如果转换了目标图像，则使用原始存储缓存类型，急返回格式的确定。 SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType; if (cacheSerializer \u0026amp;\u0026amp; (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @autoreleasepool { NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url]; //// 在磁盘中缓存图像，使用cacheData [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{ // Continue transform process [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; }]; } }); } else { //缓存图像，使用downloadedData [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType waitStoreCache:waitStoreCache completion:^{ // Continue transform process [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; }]; } } else { // Continue transform process [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; } } 先原始缓存是否需要，如果进行原始缓存的判断是否包含硬盘缓存，如果包含那么为了高速缓存要对 downloadedData 进行转换为 cacheData。转换为 disk 容易缓存的格式，同时为了防止内存不够用使用自动释放池子。如果不包含 disk 缓存的话，就直接 缓存使用 downloadedData。缓存完成要处理即调用callTransformProcessForOperation方法。\nTransform process # 对上面格式处理后的图片进行转换，转换后的图片进行缓存存储。然后回调使用转换后的图片。\n知识点 # oc 之 nullable，迎合 swift 可选类型，代表可以为 nil。苹果文档 oc 自动释放池。池子收回对象也会收回，与 runloop 的创建的释放池有区别。 信号量加锁。 使用宏定义 主线程回归主队列, 线程所在队列判断加宏定义\n大量 block 回调，逆传值。 协议化对象 字典关联使用（SDWebImageContext），其中,SDWebImageContext *是在 5.0 中新引入的参数,可以灵活的自定义高级功能.SDWebImageContext/SDWebImageMutableContext 是以以 SDWebImageContextOption 为 key、id(指定类型或者协议)为 value 的 NSDictionary/NSMutableDictionary。blog 参考 源码地址 # https://github.com/SDWebImage/SDWebImage/blob/master/SDWebImage/Core/SDWebImageManager.m ","date":"2017年5月19日","externalUrl":"","permalink":"/sdwebimge%E4%B9%8B-sdwebimagemanager/","section":"文章","summary":" SDWebImageManager 是一个单利，管理整个 SDWebImge 这个库的下载，缓存，回调展示。\n属性功能有 imageCache（查找工具），imageLoader（下载工具），runningOperations（用以放 …","title":"SDWebImageManager","type":"posts"},{"content":" 前言 # block ，枚举字符串，内敛函数学习\nblock 声明 # typedef void(^SDWebImageNoParamsBlock)(void); 枚举字符串 # typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM; //SDWebImageContextOption类型 //NS_EXTENSIBLE_STRING_ENUM 可用于swift 枚举字符串 //SDWebImageContextOption 参看 csdn 解析\nFOUNDATION_EXPORT # 效率比较比#define 高，是地址的比较，在定义常量字符串时，要写在两个文件里面。\n//.h FOUNDATION_EXPORT NSString *const kMyConstantString; //.m NSString *const kMyConstantString = @\u0026#34;hello world\u0026#34;; #define kMyConstantString @\u0026#34;hello world\u0026#34;//只在.h 文件使用 FOUNDATION_EXPORT 有如下两点作用 Visible Symbolity\nCompatibility to other source code.\n内敛函数 # 可直接进行调用函数\n//.h文件 FOUNDATION_EXPORT CGFloat SDImageScaleFactorForKey(NSString * _Nullable key); //.m文件 inline CGFloat SDImageScaleFactorForKey(NSString * _Nullable key) { CGFloat scale = 1; if (!key) { return scale; } } //内联函数 移位枚举 # NS_OPTIONS 这种枚举用来处理多情况，也就是说既能响应开始的情况，又能输出之后的情况，\ntypedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) { UIViewAutoresizingNone = 0, UIViewAutoresizingFlexibleLeftMargin = 1 \u0026lt;\u0026lt; 0, UIViewAutoresizingFlexibleWidth = 1 \u0026lt;\u0026lt; 1, UIViewAutoresizingFlexibleRightMargin = 1 \u0026lt;\u0026lt; 2, UIViewAutoresizingFlexibleTopMargin = 1 \u0026lt;\u0026lt; 3, UIViewAutoresizingFlexibleHeight = 1 \u0026lt;\u0026lt; 4, UIViewAutoresizingFlexibleBottomMargin = 1 \u0026lt;\u0026lt; 5 }; 补充 # 应该用枚举表示状态机的状态、传递给方法的选项以及状态码等值，给这些值起个易懂的名字，就像监听网络状态的枚举。 如果把传递给某个方法的选项表示为枚举类型，而多个选项又可同时使用，那么就将各选项定义为 2 的幂，以便通过按位或操作将其组合起来。 用 NS_ENUM 与 NS_OPTIONS 宏来定义枚举类型，并指明其底层数据类型。这样可以确保枚举是用开发者所选的底层数据类型实现出来的，而不会采用编译器所选的类型 在处理枚举类型的 switch 语句中不要实现 default 分支。这样的话，加入新枚举之后，编译器就会提示开发者：switch 语句并未处理所有枚举 ","date":"2017年4月12日","externalUrl":"","permalink":"/sdwebimagedefine%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/","section":"文章","summary":" block ，枚举字符串，内敛函数学习\ntypedef void(^SDWebImageNoParamsBlock)(void); typedef NSString * SDWebImageContextOption …","title":"SDWebImageDefine源码学习","type":"posts"},{"content":" 前言 # kvo 全称是 key value observe。键值观察的意思，观察某个对象做出响应，由 NSKeyValueObserving 协议提供支持，NSObject 类继承了该协议，所以 NSObject 的子类都可使用该方法。\n使用步骤 # 注册一个监听（指定观察者，以及要观察的属性） /* options: 有4个值，分别是： NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法 NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法 NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法，一旦注册，立马就会调用一次。通常它会带有新值，而不会带有旧值。 NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。 */ //注册一个监听器用于监听指定的key路径 [self.person addObserver:self forKeyPath:@\u0026#34;name\u0026#34; options:NSKeyValueObservingOptionNew context:nil]; 实现一个协议方法（响应） //当key路径对应的属性值发生改变时，监听器就会回调自身的监听方法，如下 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary\u0026lt;NSKeyValueChangeKey,id\u0026gt; *)change context:(void *)contex } 移除观察者，在 监听器消失的时候记着 delloc 监听器\n[self removeObserver:self forKeyPath:@“age\u0026#34;];//注意点 注意点 # 移除与添加成对出现，防止出现野指针。\nkvo 使用场景 # 数据保存实时性（例如 user）\u0026mdash;kvc 也可触发kvo。 对象属性触发某个操作（滑动） KVO 的实现原理 # 如果原类为 Person，那么生成的派生类名为 NSKVONotifying_Person 每个类对象中都有一个 isa 指针指向当前类，当一个类对象的第一次被观察，那么 系统会偷偷将 isa 指针指向动态生成的派生类，从而在给被监控属性赋值时执行的是派生类的 setter 方法。\n键值观察通知依赖于 NSObject 的两个方法 willChangeValueForKey:didChangevlueForKey:;在一个被观察属性发生改变之前， willChangeValueForKey: 一定会被调用，这就 会记录旧的值。而当改变发生后，didChangeValueForKey: 会被调用，继而 observeValueForKey:ofObject:change:context: 也会被调用。\n补充:KVO 的这套实现机制中苹果还偷偷重写了 class 方法，让我们误认为还是使用的当前类，从而达到隐藏生成的派生类。\n总结 # 运用oc的runtime特性的isa-swizzling技术实现的，（修改对象的isa指向 重写setter方法来实现的监听机制。A对象注册观察者B后，A类在运行的时候生成一个中间子类c，C重写了 setter方法，子类setter方法调用父类的setter方法。调用父类的setter方法之前调用了willChangeValueForKey，之后调用了，didChangeValueForKey最后调用了observeValueForKeyPath。c也重写了class方法，作用是对外界隐藏以达到外界不知道这个类的存在。\nkvo三方库 # FBKVOController\n","date":"2017年3月19日","externalUrl":"","permalink":"/kvo%E4%BD%BF%E7%94%A8%E5%92%8C%E6%9C%BA%E7%90%86/","section":"文章","summary":" kvo 全称是 key value observe。键值观察的意思，观察某个对象做出响应，由 NSKeyValueObserving 协议提供支持，NSObject 类继承了该协议，所以 NSObject 的子类都可使用该方法。\n","title":"kvo使用和机理","type":"posts"},{"content":"SDWebImageIndicator\n我是前言 # SDWebImageIndicator 在图像加载期间定制指示器的协议。 判断平台 # #if SD_UIKIT || SD_MAC //code... #endif 如果是 UIkit 平台以及 mac 会进行 code 的编译。 协议声明 # @protocol SDWebImageIndicator \u0026lt;NSObject\u0026gt; @required /** The view associate to the indicator. @return The indicator view */ @property (nonatomic, strong, readonly, nonnull) UIView *indicatorView; /** Start the animating for indicator. */ - (void)startAnimatingIndicator; /** Stop the animating for indicator. */ - (void)stopAnimatingIndicator; @optional /** Update the loading progress (0-1.0) for indicator. Optional @param progress The progress, value between 0 and 1.0 */ - (void)updateIndicatorProgress:(double)progress; @end 这个协议也遵守了协议。 这个协议声明一个属性，但是这个属性在这个地方也只是方法声明。这个地方是生成了 get 方法。 协议的实现 # 在 SDWebImageIndicator.h 文件里有一个类的声明，这个类遵循了 SDWebImageIndicator 协议。同时出现了和协议的属性名字是完全一样的属性名称 indicatorView。 @interface SDWebImageActivityIndicator : NSObject \u0026lt;SDWebImageIndicator\u0026gt; #if SD_UIKIT @property (nonatomic, strong, readonly, nonnull) UIActivityIndicatorView *indicatorView; #else @property (nonatomic, strong, readonly, nonnull) NSProgressIndicator *indicatorView; #endif @end 上诉代码就是一个巧妙之处，一个协议里面的属性不是真正的属性，它只是方法声明，另一个巧妙之处是起到提醒遵守者你要有一个 indicatorView 属性哦。更牛的其实在后边。 //.m 文件 @interface SDWebImageActivityIndicator () #if SD_UIKIT @property (nonatomic, strong, readwrite, nonnull) UIActivityIndicatorView *indicatorView; #else @property (nonatomic, strong, readwrite, nonnull) NSProgressIndicator *indicatorView; #endif @end 这个里面可以发现作者有扩展一个 indicatorView 属性，这是又是为什么呢，为了实现 indicatorView 属性的方法。同时 为了 SDWebImageActivityIndicator 初始化的时候使用到这个属性的 get 方法。 协议总结 # 协议里使用属性方法的声明。\n遵循者要和协议属性保持一致也要有这个属性。\n扩展里添加这个属性第一 方法的实现作用，第二 SDWebImageActivityIndicator 初始化要使用这个属性。\n作者代码连贯性代码统一性做了很大优化。\n分类 # 首先这个分类功能是快速建立 SDWebImageIndicator 对象。并使用这个对象。 @interface SDWebImageProgressIndicator (Conveniences) /// default-style progress indicator @property (nonatomic, class, nonnull, readonly) SDWebImageProgressIndicator *defaultIndicator; /// bar-style progress indicator @property (nonatomic, class, nonnull, readonly) SDWebImageProgressIndicator *barIndicator API_UNAVAILABLE(macos, tvos); @end 分析 # 首先这个类 SDWebImageIndicator 包括了协议，分类，以及 Progress Indicator 这个类。 这个,h .m 文件有两个类因为功能相似所以放在一起，同时都遵循一样的协议，之所以这样写遵循一样的协议杜绝了两套方法声明。但是协议的实现还是要写的。 SDWebImageIndicator 学习要点 # 协议中加属性省略写 set 或者 get 方法 用遵循协议代替方法声明利于以后扩展。 将实现和分类写在一起减少文件。保持很好封装性，写 code 的时候不用多次翻看。 分类中为类方法，方便使用。 code 使用横向扩展方案，以后可以参考已经实现的扩展多个控件支持这个功能。 源码地址 # https://github.com/SDWebImage/SDWebImage/blob/master/SDWebImage/Core/SDWebImageIndicator.h https://github.com/SDWebImage/SDWebImage/blob/master/SDWebImage/Core/SDWebImageIndicator.m ","date":"2017年1月19日","externalUrl":"","permalink":"/sdwebimageindicator/","section":"文章","summary":"SDWebImageIndicator\nSDWebImageIndicator 在图像加载期间定制指示器的协议。 #if SD_UIKIT || SD_MAC //code... #endif 如果是 UIkit 平台以及 mac 会进行 …","title":"SDWebImageIndicator","type":"posts"},{"content":" 前言 # http 与 https 都是网络传输协议，其中 http 是超文本传输协议，https 是基于 SSL/TLS 协议的超文本传输协议。SSL/TLS 是安全认证协议。http 加上 ssl/TLS 作用是防止钓鱼和加密。 https # 防止钓鱼，保证服务端不被冒充，使用 https 时候，客户端从服务端下载证书，然后对证书进行 CA 认证，可以确认服务器身份。 加密，身份确定后，证书中有公钥，服务器有私钥。公钥私钥完成加密，保证了数据的安全。 https 和 http 的区别 # 认证的区别（https 需要证书）\n连接的区别 （https 是有状态的）\n用的端口区别（https 端口是 443，http 是 80）\nhttps 数据加密 # 当客户端接收了从服务端发送而来的数字证书，进行计算验证后发现证书没有问题，说明这个公钥是服务器发送的，从证书中获取服务端的公钥，就可以和服务器进行加密通信了。但是非对称加解密太耗费时间，因此客户端会发送一个对称密钥给服务端，由于这个密钥使用了公钥进行加密，只有服务端才能解开得到，往后服务端和客户端可以通过这个对称密钥进行高效的安全通讯。（身份非对称加密，真正数据对称加密）证书的配置需要在nginx上进行配置。\nhttps 证书的申请 # 服务器提供商可以申请，阿里云，腾讯云，需要与域名的匹配。\nhttps抓包 # 代理工具如mac如青花瓷，可进行抓包，抓到的数据如果再次加密了，基本无法解开。\n","date":"2017年1月19日","externalUrl":"","permalink":"/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AEhttps%E5%92%8Chttp/","section":"文章","summary":" http 与 https 都是网络传输协议，其中 http 是超文本传输协议，https 是基于 SSL/TLS 协议的超文本传输协议。SSL/TLS 是安全认证协议。http 加上 ssl/TLS 作用是防止钓鱼和加密。 防止钓鱼，保证 …","title":"https和http","type":"posts"},{"content":" 为什么要用 MVVM 代替 MVC？ # Lighthouse (image) Apple 倡导开发者们使用 MVC 模式开发 App 程序，但很多人都没有严格按照 MVC 的模式去开发，只是让程序的架构看上去像 MVC，而实际上是 MC 或 VC，很多入门开发者都有一个通病，就是把所有的逻辑，界面生成都写进 ViewController 中，这样 ViewController 就变成了一个 Massive View Controller（重量级视图控制器）。重量级视图控制器会让整个 ViewController 变得非常复杂且不可维护，让维护者崩溃却无从下手，只能忍痛默默的重写整个逻辑。这是传统 MVC 模式：\n有一种解决方案，可以解决 Massive View Controller 的问题，那就是 MVVM。\n常用的情况是将所有业务不加区分都放进 ViewController 中这样会不可避免的让 ViewController 变得臃肿，这是造成重量级视图控制器的重要原因。一个 ViewController 中包含大量业务的细节，将使这个 ViewController 在业务协调和调用中迷失，将让这个业务变得非常混乱，让业务逻辑变得无法维护。由于重量级 ViewController 的复杂性，其代码也将将难以复用。以上原因是传统 MVC 难以解决的，为解决这些问题，要采用 MVVM 的开发模式。\nMVVM 简单说就是将部分逻辑从 ViewController 中拆分出来，并整合起来在 ViewController 和 Model 中间加多一个 ViewModel，ViewModel 不直接引用 View，ViewController 也不引用 Model 中的方法，所有网络回调数据处理等逻辑都放到 ViewModel 中，ViewController 通过 ViewModel 来请求数据和更新数据。\n如何实践 MVVM # 第一步:创建 Model # AFNetworking 请求方法放到 Model 中\n@interface Model : NSObject @property (nonatomic, copy) NSString *col; @property (nonatomic, copy) NSString *sort; @property (nonatomic, copy) NSString *tag3; @property (nonatomic, assign) NSInteger startIndex; @property (nonatomic, assign) NSInteger returnNumber; @property (nonatomic, strong) NSArray *imgs; @property (nonatomic, copy) NSString *tag; @property (nonatomic, assign) NSInteger totalNum; + (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail; 具体实现，不多说：\n@implementation Model + (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail { NSString *urlString = [NSString stringWithFormat:@\u0026#34;%@%ld%@\u0026#34;, @\u0026#34;http://image.baidu.com/data/imgs?col=%e7%be%8e%e5%a5%b3\u0026amp;tag=%e5%b0%8f%e6%b8%85%e6%96%b0\u0026amp;sort=0\u0026amp;pn=1\u0026#34;, aPage,@\u0026#34;\u0026amp;rn=1\u0026amp;p=channel\u0026amp;from=1\u0026#34;]; AFHTTPRequestOperationManager *managere = [AFHTTPRequestOperationManager manager]; [managere GET:urlString parameters:nil success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) { success(responseObject,nil); NSLog(@\u0026#34;success\u0026#34;); } failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) { fail(nil,error); NSLog(@\u0026#34;fail\u0026#34;); }]; } @end 第二步:创建 ViewModel # ViewModel 的属性 data : 请求获取的数据 racMsg : 请求成功和失败的信号量（主要用 KVO 对这个进行监视） @interface ViewModel : NSObject @property (strong,nonatomic) NSDictionary *data; @property (strong,nonatomic) NSString *racMsg; - (void)getImagesList; - (void)getNextImagesList; - (void)getPreImagesList; @end ViewController 中主要监视 ViewModel 的 racMsg 来发现 data 更新\n#define WS(weakSelf) __weak __typeof(\u0026amp;*self)weakSelf = self; @interface ViewModel() @property (nonatomic) NSInteger currentPage; @end @implementation ViewModel - (instancetype)init { self = [super init]; self.currentPage = 0; return self; } - (void)getImagesList { WS(ws) [Model getImagesListWithPage:0 SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) { ws.data = responseObjectDict; ws.racMsg = @\u0026#34;success\u0026#34;; } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) { ws.data = nil; ws.racMsg = @\u0026#34;fail\u0026#34;; }]; } - (void)getNextImagesList { WS(ws) self.currentPage++; [Model getImagesListWithPage:self.currentPage SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) { ws.data = responseObjectDict; ws.racMsg = @\u0026#34;success\u0026#34;; } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) { ws.data = nil; ws.racMsg = @\u0026#34;fail\u0026#34;; }]; } - (void)getPreImagesList { WS(ws) self.currentPage = self.currentPage == 0 ? 0 : self.currentPage-1; [Model getImagesListWithPage:self.currentPage SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) { ws.data = responseObjectDict; ws.racMsg = @\u0026#34;success\u0026#34;; } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) { ws.data = nil; ws.racMsg = @\u0026#34;fail\u0026#34;; }]; } @end 可能会有人觉得为什么不直接监视 data，但我个人更倾向于采用一种类似于信号量的机制，监听特定的信号来更新数据。如果直接监视 data，则 data 只有 nil 和非 nil 两种情况，要进一步区分请求的状态的话必须要对 data 进行解析，增加了转换成本，不如直接采用多一个属性变量进行判断和协调\n第三步:ViewController 中 KVO 设置 # ViewController 直接持有 viewModel\n@interface ViewController () @property (strong,nonatomic) ViewModel *viewModel; @property (strong,nonatomic) UITextView *showTextView; @end 加载 ViewController 时初始化 KVO 和调用 ViewModel 方法 getImagesList 来请求数据\n- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // requestData [self _initViews]; [self setupKVO]; [self.viewModel getImagesList]; } ViewController 销毁时去除 KVO\n- (void)dealloc { [self removeKVO]; } KVO 相关的函数。observeValueForKeyPath 只需对 racMsg 进行判断就可以知道 data 的值是否更新了，如果更新了就更新一下 View。\npragma mark - KVO - (void)setupKVO { [self.viewModel addObserver:self forKeyPath:@\u0026#34;racMsg\u0026#34; options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil]; } - (void)removeKVO { [self.viewModel removeObserver:self forKeyPath:@\u0026#34;racMsg\u0026#34;]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary\u0026lt;NSString *,id\u0026gt; *)change context:(void *)context { if ([keyPath isEqualToString:@\u0026#34;racMsg\u0026#34;]) { if ([_viewModel.racMsg isEqualToString:@\u0026#34;success\u0026#34;]) { _showTextView.text = [NSString stringWithFormat:@\u0026#34;%@\u0026#34;,_viewModel.data]; } else { _showTextView.text = @\u0026#34;error\u0026#34;; } } } 按钮点击事件和 View 的初始化\n#pragma mark - Event Response - (void)getPre { [self.viewModel getPreImagesList]; } - (void)getNext { [self.viewModel getNextImagesList]; } #pragma mark - Private - (void)_initViews { UIButton *preBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 50, 200, 40)]; [preBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; [preBtn setTitle:@\u0026#34;Pre\u0026#34; forState:UIControlStateNormal]; [preBtn addTarget:self action:@selector(getPre) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:preBtn]; UIButton *nextBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 150, 200, 40)]; [nextBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; [nextBtn setTitle:@\u0026#34;nextBtn\u0026#34; forState:UIControlStateNormal]; [nextBtn addTarget:self action:@selector(getNext) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:nextBtn]; _showTextView = [[UITextView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)]; _showTextView.backgroundColor = [UIColor lightGrayColor]; [self.view addSubview:_showTextView]; } 实践 MVVM 的具体好处的例子 # 例子 1 # ViewController 需要一个额外请求一个文章列表，这个文章列表的请求参数与当前请求图片列表的接口返回结果没有任何关联，可以并行请求。 在这种情况下，在 ViewModel 中加入方法 getArticleList()和属性 articleList 以及 articleMsg，然后在 ViewController 中需要调用该方法的位置调用该方法即可。KVO 中的 observeValueForKeyPath 方法稍微修改一下\n(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary\u0026lt;NSString *,id\u0026gt; *)change context:(void *)context { if ([keyPath isEqualToString:@\u0026#34;racMsg\u0026#34;]) { if ([_viewModel.racMsg isEqualToString:@\u0026#34;success\u0026#34;]) { _showTextView.text = [NSString stringWithFormat:@\u0026#34;%@\u0026#34;,_viewModel.data]; } else { _showTextView.text = @\u0026#34;error\u0026#34;; } } else if([keyPath isEqualToString:@\u0026#34;articleMsg\u0026#34;]) { if ([_viewModel.articleMsg isEqualToString:@\u0026#34;success\u0026#34;]) { _articleTextView.text = _viewModel.articleList } else { _articleTextView.text = @\u0026#34;error\u0026#34;; } } } 可见并行业务功能上的扩展是非常简单的，整个 Controller 的总体逻辑几乎不用怎么变化，只需要改动局部细节即可\n例子 2 # 同例子 1，但是文章列表的请求参数需要通过图片列表接口返回的结果获取，请求是串联嵌套的（即先请求图片列表接口，请求完成后，根据返回结果再请求文章列表接口） 对于嵌套的请求，如果采用传统 MVC 模式，就要在 ViewController 中加入两个 block，一个嵌套另外一个，这样会让代码变得非常难看，而且会让子 block 依赖于父 block，难以对其进行拆分。但如果采用 MVVM，则会将所有的请求变化都置于 KVO 的监控之下，并作出统一的处理。 ViewModel 中的与例子 1 一样，但 ViewController 中的处理稍微不同\n- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary\u0026lt;NSString *,id\u0026gt; *)change context:(void *)context { if ([keyPath isEqualToString:@\u0026#34;racMsg\u0026#34;]) { if ([_viewModel.racMsg isEqualToString:@\u0026#34;success\u0026#34;]) { _showTextView.text = [NSString stringWithFormat:@\u0026#34;%@\u0026#34;,_viewModel.data]; // 请求文章 [_viewModel getArticleList]; } else { _showTextView.text = @\u0026#34;error\u0026#34;; } } else if([keyPath isEqualToString:@\u0026#34;articleMsg\u0026#34;]) { if ([_viewModel.articleMsg isEqualToString:@\u0026#34;success\u0026#34;]) { _articleTextView.text = _viewModel.articleList } else { _articleTextView.text = @\u0026#34;error\u0026#34;; } } } 可以看出还是不需要改动大逻辑，即可对有依赖的业务进行扩展。\n例子 3 # ViewController中有很多数据转换逻辑，多个ViewController的数据转换逻辑都相同的情况。 ViewModel 中不仅只包含请求逻辑，还可以包含数据转换的逻辑，还有一些不知要怎么归类的杂七杂八的逻辑。多个 ViewController 发生相同的数据转换的情况是经常会有的，如果把数据转换逻辑写到 Controller 之中，会让每一个 Controller 都持有一个转换逻辑，这对于数据转换逻辑的统一来说是非常糟糕的。如果把相同的数据转换逻辑都抽象封装到同一个 ViewModel 中，ViewController 不直接持有数据转换逻辑，而是通过 ViewModel 来调用的话，每个 ViewController 只需要维护一个 ViewModel 实例即可，所有转换细节都可以在 ViewModel 中进行统一修改。Controller 只关心数据和数据与 View 的交互，不应该关心数据之间的转换和数据怎样获取的，这应该是 MVVM 的一个原则。\n总结 # MVVM 的核心在于绑定，本文采用的是 KVO 的绑定机制，能够很好与 Objective-C 和 Cocoa 结合起来，不需要借用第三方的类库进行数据绑定。 除了使用 KVO，业界通常采用的是 ReactiveCocoa。但是，ReactiveCocoa 的学习成本过高，不适合轻量级的开发，而 MVVM 只是一种开发模式，并不是一种具体的框架，所以如果不是非常想深入使用 MVVM 的精髓的话，是没有必要去学习 ReactiveCocoa 的。网上还有一些讨论 MVVM 的博客提到 ViewModel 直接对 View 进行操作，其实这是一种很不严谨的做法，MVVM 中的 ViewModel 不应该关心 View 的显示，只应该关心数据的获取和转换，View 如何显示那是 ViewController 的职责。所以凡是在 ViewModel 中引用了 UIKit 的，个人认为都不是一种严格意义上的 MVVM。 当然 MVVM 也有其自身的不足，比如引入 ViewModel 之后，文件数量增加了不少，总的代码量其实也会增加，这对于极简主义者来说并不是一种很好的模式。而且 MVVM 的开发思路与 MVC 是不同的，开发者要转换思路采用 MVVM 的开发方式其实还是有不少的学习成本，而且对于大部分简单业务来说，使用 MVVM 会增加业务的复杂度，显得臃肿和多余。本文中使用 KVO 的 MVVM 模式，从本质上来说其实是 MVC 的衍生，把 C 中的一部分拆分出来并隔离 M 和 C，所以从模式上来说，这样是完全可以兼容传统的 MVC 开发模式的。因此，对于简单的业务，可以直接采用 MVC 的模式开发，不需要额外创建一个 ViewModel。对于复杂业务，就采用 KVO 的 MVVM 模式，进行业务拆分和复用。这种折中的方法能够将 MVC 和 MVVM 的优点都利用起来，避免只使用一个造成开发效率上的降低。 MVVM 不应该被误解和神化，使用 MVVM 只是提供多了一个不错的选择，要不要使用它，还是要看具体的项目而定。但是用上了，就停不下来了。 总结 # 不管 MVVM 还是 MVC 是一种拆分思维，将业务逻辑和显示逻辑进行拆分。减少 controller 的以及 model 层的代码量。利于后期维护。 参考 # Model-View-ViewModel for iOS MVVM 介绍 ReactiveCocoa 和 MVVM 入门 iOS 应用架构谈 view 层的组织和调用方案 iOS MVVM+RAC 从框架到实战 ","date":"2016年11月19日","externalUrl":"","permalink":"/mvvm/","section":"文章","summary":" Lighthouse (image) Apple 倡导开发者们使用 MVC 模式开发 App 程序，但很多人都没有严格按照 MVC 的模式去开发，只是让程序的架构看上去像 MVC，而实际上是 MC 或 VC，很多入门开发者都有一个通病，就 …","title":"KVO实现MVVM","type":"posts"},{"content":" 我是前言 # 签名机制需要了解 非对称加密，摘要，数字签名，证书概念非对称加密解决数据传递的保密性中途窃取无法解开，摘要是数据特性，保持数据的完整，数字签名是非对称加密和摘要的结合。一起解决数据的安全，和完整。证书保证了发送公钥的一方是值得信任的。 什么是非对称密钥加密？ # 非对称加密就是指加密密钥和解密密钥是不同的，而且加密密钥和解密密钥是成对出现。非对称加密又叫公钥加密，也就是说成对的密钥，其中一个是对外公开的，所有人都可以获得，或者说可以把这个公钥给很多人，称为公钥，而与之相对应的称为私钥，只有这对密钥的生成者才能拥有。 非对称密钥的特性 # 一个私钥对应一个公钥，公钥可以分发多人 一个公钥加密后的密文可以通过私钥进行解密，一个私钥加密的密文可以通过公钥进行解密 私钥公钥加密用法： # 对信息保密，将明文经过公钥进行加密，传送给他人，只有拥有私钥的才能解密进行阅读。中途窃取无法解密。 什么是摘要算法？ # 摘要算法是指，可以将任意长度的文本，通过一个算法，得到一个固定长度的文本。\n摘要算法具有以下重要特性： # 只要源文本不同，计算得到的结果，必然不同\n无法从结果反推出源\n文件如果发生改变，数字摘要的值也将发生变化。【摘要的唯一性】摘要算法，有 MD5 和 SHA，摘要算法主要为了验证信息完整，无修改。\n什么是数字签名？ # 将摘要信息用发送者的私钥加密，与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息，然后用 HASH 函数对收到的原文产生一个摘要信息，与解密的摘要信息对比。如果相同，则说明收到的信息是完整的，在传输过程中没有被修改，否则说明信息被修改过，因此数字签名能够验证信息的完整性（来源百度百科）\n下图中可以看出其用法 从上面可以看出，数字签名是摘要算法与私钥公钥加密的结合使用。\n什么是数字证书？ # 首先数字证书和我们平常用的证书在功能方面有一定的相似。生活中用的证书，如大学毕业证书，这个是机构颁发，向用人单位证明，我是什么学校毕业的。让用人单位相信你。数字证书就是，比如苹果公司就可以签发跟苹果公司有关的证书，而跟 web 访问有关的证书则是又几家公认的机构进行签发。这些签发机构称为 CA（Certificate Authority）。验证方是 ios 设备，以及浏览器。验证过程已经写在 ios 设备上以及浏览器上。 证书保证身份不被冒充。证书里面有公钥。 什么是证书授权链？ # 苹果 MC（Member Center）中获得的证书实际也是一个包含有证书链的证书，其中的根是苹果的 CA。我们获得的证书实际上是在告诉 iOS 设备：我们的证书是被苹果 CA 签过名的合法的证书。而 iOS 设备在执行 app 前，首先要先验证 CA 的签名是否合法，然后再通过证书中我们的公钥验证程序是否的确是我们发布的，且中途没有对程序进行过篡改。\nios 开发需要申请证书，并且需要签名，下图是一个别人画的流程图可以参看\n先申请证书，ios 设备对证书认证（内置证书机构），取出里面的公钥，对经私钥签名的代码以及资源进行解密，确保安全后安装。\n怎么才能申请 iOS 证书呢？ # 我们知道，如果我们开发 ios 程序，必须成为 ios 开发者，不然只能在模拟器上跑程序，并且也不能发布，只有成为开发者才能有权限申请证书。并且将证书安装到 xcode，xcode 信任此证书才能用我们自己的私钥进行代码签名。\n成为开发者后通过过 keychain 生成一个 CSR 文件，提交给苹果的 Apple Worldwide Developer Relations Certification Authority(WWDR)证书认证中心进行签名最后得到一个证书，从苹果官网下载并安装使用。\n生成的 CSR 文件包括 # 申请者信息，此信息是用申请者的私钥加密的 申请者公钥，此信息是申请者使用的私钥对应的公钥 摘要算法和公钥加密算法 申请的证书包含什么？ # 苹果取出 CertificateSigningRequest.certSigningRequest 中的公钥，根本不管我的其他信息，然后将我的 MC 账号信息和我提交的公钥封装在证书中，并进行数字签名（用 apple 的私钥）。双击安装证书后可看见下图 这个证书里有一个公钥，对应一个私钥, 后续在程序上真机的过程中，会使用这个私钥，对代码进行签名，而公钥会附带在 mobileprovision 文件中，打包进 app。 所以当有证书，然后没有私钥也是没有用的，需要证书的公钥与电脑的私钥关联起来。这是一个联合的机制。\n多人开发怎么解决一机一私钥的问题呢？ # 给出的解决方案就是“私钥导出”。没错，既然问题的关键是私钥，我们共享私钥不就行了，将最初申请证书的机器的私钥导出成.p12 文件，并让其他机器导入，同时其他机器也应该安装下载下来的证书。 由于 iOS 证书有多种类型，用于不同的用处，所以我们可能后续还会去 MC 上申请别的证书。所以强烈建议 CertificateSigningRequest.certSigningRequest 需要保留，因为如果再次生成 CertificateSigningRequest.certSigningRequest 文件，可能就是对应另一个私钥了！还需要在共享一次私钥，会比较麻烦。\n申请证书有哪些类型呢？ # iOS App Development。开发、真机调试用 Apple Push Notification service SSL (Sandbox)。开发阶段使用苹果的推送服务 App Store and Ad Hoc。上架和AdHoc方式发布时用 Apple Push Notification service SSL (Production)。上架后使用苹果推送服务 InHouse。企业版发布，需$299才能拥有，还需邓氏编码 描述文件【mobileprovision】 # mobileprovision 文件包含： # AppId.每个 app 必须在 MC 中创建一个对应的 AppId。规则不累述了。 使用哪些证书。上面说了，不同类型的证书就代表了不同的发布方式，还包括一些功能的能否使用（比如 APN） 功能授权列表 可安装的设备列表。对于 AdHoc 方式发布的 app 或者真机调试时，会有一个列表，这个列表里面是 iOS 设备的 UDID，每台 iOS 设备出厂的 UDID 都不同，所以可以用来标识设备。可通过 iTunes 连接设备，或者http://fir.im/udid这里获取 苹果的签名 AdHoc 发布和真机调试 # AdHoc 允许将测试版 app 发布给有限的设备安装，而无需通过 appstore 的审核。这里的关键是如何控制哪些设备可以装。答案就是 mobileprovision 文件，记得你在生成 mobileprovision 文件的时候需要选设备的 UDID 吧，所以这些设备需要事先添加到 MC 的 Devices 里面。对于开发时候的真机调试，原理差不多。都是通过 mobileprovision 的条目 4 来做到的。而苹果对于调试和测试用机的数量限制为 100 台！\n参考 # iOS Code Signing 学习笔记 数字签名是什么？ ","date":"2016年10月19日","externalUrl":"","permalink":"/ios%E8%AF%81%E4%B9%A6%E4%B8%8E%E7%AD%BE%E5%90%8D%E6%9C%BA%E5%88%B6/","section":"文章","summary":" 签名机制需要了解 非对称加密，摘要，数字签名，证书概念非对称加密解决数据传递的保密性中途窃取无法解开，摘要是数据特性，保持数据的完整，数字签名是非对称加密和摘要的结合。一起解决数据的安全，和完整。证书保证了发送公钥的一方是值得信任的。 非 …","title":"签名机制","type":"posts"},{"content":" 前言 # 根据时间线，可以分为下面八个过程。\n第一步过程 # 首先，你得在浏览器里输入要网址:例如百度或者facebook。 第二步过程 # 浏览器查找域名的IP地址（域名就是指输入的网址）\n浏览器缓存 – 浏览器会缓存DNS记录一段时间。 有趣的是，操作系统没有告诉浏览器储存DNS记录的时间，这样不同浏览器会储存个自固定的一个时间（2分钟到30分钟不等）。\n路由器缓存 – 接着，前面的查询请求发向路由器，它一般会有自己的DNS缓存。\nISP DNS 缓存 – 接下来要check的就是ISP缓存DNS的服务器。在这一般都能找到相应的缓存记录。\n递归搜索 – 你的ISP的DNS服务器从跟域名服务器开始进行递归搜索，从.com顶级域名服务器到Facebook的域名服务器。一般DNS服务器的缓存中会 有.com域名服务器中的域名，所以到顶级服务器的匹配过程不是那么必要了。\n第三步过程 # 在请求之前，需要浏览器与服务器建立连接（TCP或者UDP）\n与服务器建立连接时TCP属于安全的连接，需要三次握手，这里不在说明。 与服务器响应软件建立管道连接（socket）\n第四步过程 # 浏览器给web服务器发送一个HTTP请求\n因为像Facebook主页这样的动态页面，打开后在浏览器缓存中很快甚至马上就会过期，毫无疑问他们不能从中读取。\n所以，浏览器将把一下请求发送到Facebook所在的服务器：\n下面为整个一个请求\nGET HTTP://facebook.com/ HTTP/1.1 Accept: application/x-ms-application, image/jpeg, application/xaml+xml, [...] User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; [...] DontTrackMeHere: gzip, deflate Connection: Keep-Alive Host: facebook.com Cookie: datr=1265876274-[...]; locale=en_US; lsd=WW[...]; c_user=2101[...] 解读上面请求体\nGET 这个请求定义了要读取的URL： “HTTP://facebook.com/”。\nConnection头要求服务器为了后边的请求不要关闭TCP连接。\n请求中也包含浏览器存储的该域名的cookies。可能你已经知道，在不同页面请求当中，cookies是与跟踪一个网站状态相匹配的键值。这样cookies会存储登录用户名，服务器分配的密码和一些用户设置等。Cookies会以文本文档形式存储在客户机里，每次请求时发送给服务器。\n补充说明\n用来看原始HTTP请求及其相应的工具很多。作者比较喜欢使用fiddler，当然也有像FireBug这样其他的工具。这些软件在网站优 化时会帮上很大忙。 除了获取请求，还有一种是发送请求，它常在提交表单用到。发送请求通过URL传递其参数(e.g.: HTTP://robozzle.com/puzzle.aspx?id=85)。发送请求在请求正文头之后发送其参数。\n像“HTTP://facebook.com/”中的斜杠是至关重要的。这种情况下，浏览器能安全的添加斜杠。而像“HTTP: //example.com/folderOrFile”这样的地址，因为浏览器不清楚folderOrFile到底是文件夹还是文件，所以不能自动添加 斜杠。这时，浏览器就不加斜杠直接访问地址，服务器会响应一个重定向，结果造成一次不必要的握手。\n第五步过程 # 服务器“处理”请求 # 服务器接收到获取请求，然后处理并返回一个响应。 这表面上看起来是一个顺向的任务，但其实这中间发生了很多有意思的东西- 就像作者博客这样简单的网站，何况像baidu那样访问量大的网站呢！ Web 服务器软件web服务器软件（像IIS和阿帕奇）接收到HTTP请求，然后确定执行什么请求来处理它。请求处理就 是一个能够读懂请求并且能生成HTML来进行响应的程序（像ASP.NET,PHP,RUBY…）。 举 个最简单的例子，需求处理可以以映射网站地址结构的文件层次存储。像HTTP://example.com/folder1/page1.aspx这个地 址会映射/httpdocs/folder1/page1.aspx这个文件。\nweb服务器软件可以设置成为地址人工的对应请求处理，这样 page1.aspx的发布地址就可以是HTTP://example.com/folder1/page1。* 请求处理请求处理阅读 请求及它的参数和cookies。它会读取也可能更新一些数据，并讲数据存储在服务器上。然后，需求处理会生成一个HTML响应。\n所 有动态网站都面临一个有意思的难点 -如何存储数据。小网站一半都会有一个SQL数据库来存储数据，存储大量数据和/或访问量大的网站不得不找一些办法把数据库分配到多台机器上。解决方案有：sharding （基于主键值讲数据表分散到多个数据库中），复制，利用弱语义一致性的简化数据库。 委托工作给批处理是一个廉价保持数据更新的技术。举例来讲，Fackbook得及时更新新闻feed，但数据支持下的“你可能认识的人”功能只需要每晚更新（作者猜测是这样的，改功能如何完善不得而知）。批处理作业更新会导致一些不太重要的数据陈旧，但能使数据更新耕作更快更简洁。\n第六步 # 服务器发回一个HTML响应. 下面就是一个响应头（响应头与body体分开的） HTTP/1.1 200 OK Cache-Control: private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Expires: Sat, 01 Jan 2000 00:00:00 GMT P3P: CP=\u0026ldquo;DSP LAW\u0026rdquo; Pragma: no-cache Content-Encoding: gzip Content-Type: text/html; charset=utf-8 X-Cnection: close Transfer-Encoding: chunked Date: Fri, 12 Feb 2010 09:05:55 GMT 2b3Tn@[\u0026hellip;] 整个响应大小为35kB，其中大部分在整理后以blob类型传输。 内容编码头告诉浏览器整个响应体用gzip算法进行压缩。解压blob块后，你可以看到如下期望的HTML：\n\u003c!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"\u003e ... 关于压缩，头信息说明了是否缓存这个页面，如果缓存的话如何去做，有什么cookies要去设置（前面这个响应里没有这点）和隐私信息等等。 请注意报头中把Content-type设置为“text/html”。报头让浏览器将该响应内容以HTML形式呈现，而不是以文件形式下载它。浏览器会根据报头信息决定如何解释该响应，不过同时也会考虑像URL扩展内容等其他因素。 第七步 # 浏览器开始显示HTML 在浏览器没有完整接受全部HTML文档时，它就已经开始显示这个页面了 第八步 # 浏览器显示HTML时，它会注意到需要获取其他地址内容的标签。这时，浏览器会发送一个获取请求来重新获得这些文件。 下面是几个我们访问facebook.com时需要重获取的几个URL： 图片 http://static.ak.fbcdn.net/rsrc.php/z12E0/hash/8q2anwu7.gif http://static.ak.fbcdn.net/rsrc.php/zBS5C/hash/7hwy7at6.gif … CSS 式样表 http://static.ak.fbcdn.net/rsrc.php/z448Z/hash/2plh8s4n.css http://static.ak.fbcdn.net/rsrc.php/zANE1/hash/cvtutcee.css … JavaScript 文件 http://static.ak.fbcdn.net/rsrc.php/zEMOA/hash/c8yzb6ub.js http://static.ak.fbcdn.net/rsrc.php/z6R9L/hash/cq2lgbs8.js … 这些地址都要经历一个和HTML读取类似的过程。所以浏览器会在DNS中查找这些域名，发送请求，重定向等等\u0026hellip;\n但 不像动态页面那样，静态文件会允许浏览器对其进行缓存。有的文件可能会不需要与服务器通讯，而从缓存中直接读取。服务器的响应中包含了静态文件保存的期限 信息，所以浏览器知道要把它们缓存多长时间。还有，每个响应都可能包含像版本号一样工作的ETag头（被请求变量的实体值），如果浏览器观察到文件的版本 ETag信息已经存在，就马上停止这个文件的传输。\n试着猜猜看“fbcdn.net”在地址中代表什么？聪明的答案是\u0026quot;Facebook内容分发网络\u0026quot;。Facebook利用内容分发网络（CDN）分发像图片，CSS表和JavaScript文件这些静态文件。所以，这些文件会在全球很多CDN的数据中心中留下备份。\n静态内容往往代表站点的带宽大小，也能通过CDN轻松的复制。通常网站会使用第三方的CDN。例如，Facebook的静态文件由最大的CDN提供商Akamai来托管。\n举例来讲，当你试着ping static.ak.fbcdn.net的时候，可能会从某个akamai.net服务器上获得响应。有意思的是，当你同样再ping一次的时候，响应的服务器可能就不一样，这说明幕后的负载平衡开始起作用了。\n","date":"2016年8月14日","externalUrl":"","permalink":"/%E7%BD%91%E9%A1%B5%E8%BE%93%E5%85%A5url%E5%88%B0%E9%A1%B5%E9%9D%A2%E5%B1%95%E7%A4%BA%E6%B5%81%E7%A8%8B/","section":"文章","summary":" 根据时间线，可以分为下面八个过程。\n首先，你得在浏览器里输入要网址:例如百度或者facebook。 浏览器查找域名的IP地址（域名就是指输入的网址）\n","title":"网页输入url到页面展示流程","type":"posts"},{"content":"代码之外，留一点空间给书、思考和生活。\n","date":"","externalUrl":"","permalink":"/reading/","section":"关于","summary":"代码之外，留一点空间给书、思考和生活。\n","title":"读书与随笔","type":"page"},{"content":" 个人信息 # 专长 ios 开发，掌握 Hybrid App\n熟悉ReactNative 了解 flutter\n熟悉react，vue\n了解 nginx配置，ruby，shell，ci/cd webHook\n有小团队管理经验，熟悉jira wiki 相关管理软件\n有移动端和前端开发基础设施搭建经验\n教育经历 # 2012-2016 郑州大学本科毕业（cs 专业）\n技术工具 # vscode，Xcode，fastlane， docker ，nginx，\njira，gitlab，github，jenkins，GitHub Actions，fastlane\n产出工具 # Typora，Notion ，flomo\n公司 # 【2020-至今】 北京银行子公司\u0026mdash;北银金科\n负责北京银行智慧教育项目产品京彩校园app的开发 负责北京银行电子社保卡项目 5.0以及4.0 ios 开发，H5页面的开发 参与北京公积金住宅专项维修基金平台前端页面开发 参与北京银行企业网银移动端5.0项目的开发 【2018-2020】 药明康德子公司\u0026mdash;-明码生物\n负责来因健康ios开发 参与来因助手小程序开发 【2016-2018】 创业公司\u0026mdash;视动世纪\n负责共享产品三瓣西瓜 ios开发 阅读过的技术书籍 # 黑客与画家，代码简洁之道，设计模式之禅，编写高质量iOS与OS X代码的52个有效方法\n","date":"","externalUrl":"","permalink":"/about/","section":"关于","summary":" 专长 ios 开发，掌握 Hybrid App\n熟悉ReactNative 了解 flutter\n熟悉react，vue\n","title":"我是燕航","type":"page"}]