<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>TommyWu&apos;s Lab</title><description>技术、AI 与工程实践记录</description><link>https://www.tommywutong.cn/</link><language>zh_CN</language><item><title>Hello, TommyWu&apos;s Lab</title><link>https://www.tommywutong.cn/posts/hello-tommywu-lab/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/hello-tommywu-lab/</guid><description>从这里开始记录技术、AI 与工程实践。</description><pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Hello, TommyWu&apos;s Lab&lt;/h1&gt;
&lt;p&gt;这是第一篇测试文章。&lt;/p&gt;
&lt;p&gt;以后你可以在 Obsidian 的 &lt;code&gt;TommyWu Lab&lt;/code&gt; 文件夹里写公开文章，然后在博客项目里运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm sync-posts
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想先写草稿，可以把 &lt;code&gt;draft&lt;/code&gt; 设置成 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>TommyWu&apos;s Lab 使用指南</title><link>https://www.tommywutong.cn/posts/tommywu-lab-usage-guide/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/tommywu-lab-usage-guide/</guid><description>这篇文章记录 TommyWu&apos;s Lab 的日常写作、同步、预览、构建和发布流程。</description><pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;TommyWu&apos;s Lab 使用指南&lt;/h1&gt;
&lt;p&gt;这篇指南记录这个博客的日常使用方式。它适合当成自己的操作手册：以后忘了怎么新增文章、怎么同步、怎么预览、怎么排查问题，就回来翻这一篇。&lt;/p&gt;
&lt;p&gt;当前博客由三部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;博客项目：&lt;code&gt;/Users/tommywu/tommywu-lab&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Obsidian 公开写作目录：&lt;code&gt;/Users/tommywu/Obsidian/TommyWu Lab&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;静态站点框架：Fuwari / Astro&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只有放在 &lt;code&gt;TommyWu Lab&lt;/code&gt; 这个 Obsidian 文件夹里的内容会被同步到网站。其他私人笔记不会被博客读取。&lt;/p&gt;
&lt;h2&gt;日常写作流程&lt;/h2&gt;
&lt;p&gt;最常用的流程是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /Users/tommywu/tommywu-lab
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后打开：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://127.0.0.1:4321/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着在 Obsidian 里编辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/Users/tommywu/Obsidian/TommyWu Lab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次启动 &lt;code&gt;pnpm dev&lt;/code&gt; 时，博客会先自动执行同步，把 Obsidian 公开目录里的 Markdown 文件同步到博客项目的 &lt;code&gt;src/content/posts&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果开发服务器已经开着，新增或修改文章后，可以手动同步一次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm sync-posts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;新增一篇文章&lt;/h2&gt;
&lt;p&gt;在 Obsidian 的 &lt;code&gt;TommyWu Lab&lt;/code&gt; 文件夹中新建一个 &lt;code&gt;.md&lt;/code&gt; 文件，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-first-technical-note.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件顶部需要写 frontmatter：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: &quot;我的第一篇技术笔记&quot;
published: 2026-05-04
description: &quot;这是一篇测试文章。&quot;
tags: [&quot;Astro&quot;, &quot;Obsidian&quot;]
category: &quot;工程&quot;
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在下面写正文：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 我的第一篇技术笔记

这里是正文内容。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写完后运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /Users/tommywu/tommywu-lab
pnpm sync-posts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果本地服务器开着，页面会自动刷新或在刷新浏览器后看到新文章。&lt;/p&gt;
&lt;h2&gt;frontmatter 字段说明&lt;/h2&gt;
&lt;p&gt;每篇文章建议都包含这些字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: &quot;文章标题&quot;
published: 2026-05-04
description: &quot;文章摘要，会用于 SEO、首页卡片和搜索结果。&quot;
tags: [&quot;Tag1&quot;, &quot;Tag2&quot;]
category: &quot;分类&quot;
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;：文章标题，必填。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;published&lt;/code&gt;：发布日期，必填，格式必须是 &lt;code&gt;YYYY-MM-DD&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;：文章摘要，建议填写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tags&lt;/code&gt;：标签，可以有多个。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;category&lt;/code&gt;：分类，通常放一个大方向，比如 &lt;code&gt;工程&lt;/code&gt;、&lt;code&gt;AI&lt;/code&gt;、&lt;code&gt;iOS&lt;/code&gt;、&lt;code&gt;读书&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;draft&lt;/code&gt;：是否草稿。&lt;code&gt;false&lt;/code&gt; 表示发布，&lt;code&gt;true&lt;/code&gt; 表示隐藏。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步脚本会检查 &lt;code&gt;title&lt;/code&gt; 和 &lt;code&gt;published&lt;/code&gt;。如果缺少这两个字段，同步会失败，这样可以避免不完整的文章被发布。&lt;/p&gt;
&lt;h2&gt;隐藏文章&lt;/h2&gt;
&lt;p&gt;如果文章还没写完，但你想保留在公开写作目录里，可以设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;draft: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fuwari 会把它当成草稿，不展示在网站上。&lt;/p&gt;
&lt;p&gt;等要发布时再改回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;draft: false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除文章&lt;/h2&gt;
&lt;p&gt;删除文章只需要两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 Obsidian 的 &lt;code&gt;TommyWu Lab&lt;/code&gt; 文件夹里删除对应 &lt;code&gt;.md&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;在博客项目中运行同步：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cd /Users/tommywu/tommywu-lab
pnpm sync-posts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步脚本会清空并重建博客项目里的文章目录，所以网站内容会和 Obsidian 公开目录保持一致。&lt;/p&gt;
&lt;h2&gt;修改文章文件名&lt;/h2&gt;
&lt;p&gt;文章 URL 和文件名有关。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hello-tommywu-lab.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会生成类似这样的地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/posts/hello-tommywu-lab/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你修改文件名，文章地址也会变化。已经发出去的旧链接可能会失效，所以正式发布后的文章文件名尽量少改。&lt;/p&gt;
&lt;h2&gt;图片使用&lt;/h2&gt;
&lt;p&gt;最稳妥的方式是把图片放在文章旁边。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TommyWu Lab/
  my-post.md
  my-image.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Obsidian 里可以写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![my-image.png](./my-image.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步脚本会把它转换成标准 Markdown：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![my-image.png](./my-image.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以手动写标准 Markdown 图片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![图片说明](./my-image.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;支持同步的图片格式包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.png&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.jpg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.jpeg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gif&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.webp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.avif&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.svg&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Obsidian 双链&lt;/h2&gt;
&lt;p&gt;同步脚本会简单处理 Obsidian 双链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[hello-tommywu-lab](/posts/hello-tommywu-lab/)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会转换成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[hello-tommywu-lab](/posts/hello-tommywu-lab/)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[这篇测试文章](/posts/hello-tommywu-lab/)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会转换成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[这篇测试文章](/posts/hello-tommywu-lab/)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：双链转换是按文件名推断 URL 的，所以文件名最好使用英文、小写和短横线。&lt;/p&gt;
&lt;h2&gt;推荐文件命名&lt;/h2&gt;
&lt;p&gt;建议使用英文小写和短横线：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;build-my-blog-with-astro.md
notes-on-rag.md
ios-memory-debugging.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不太建议使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;我的第一篇博客.md
My First Blog.md
2026/05/04/文章.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中文标题可以放在 &lt;code&gt;title&lt;/code&gt; 里，文件名保持稳定、简短、URL 友好。&lt;/p&gt;
&lt;h2&gt;本地预览&lt;/h2&gt;
&lt;p&gt;启动开发服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /Users/tommywu/tommywu-lab
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://127.0.0.1:4321/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常看的页面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首页：&lt;code&gt;http://127.0.0.1:4321/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;归档：&lt;code&gt;http://127.0.0.1:4321/archive/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;关于：&lt;code&gt;http://127.0.0.1:4321/about/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;RSS：&lt;code&gt;http://127.0.0.1:4321/rss.xml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;构建检查&lt;/h2&gt;
&lt;p&gt;正式发布前建议运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /Users/tommywu/tommywu-lab
pnpm check
pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pnpm check&lt;/code&gt; 用来检查 Astro、TypeScript 和内容结构。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pnpm build&lt;/code&gt; 会生成生产环境静态网站，并生成搜索索引、RSS、sitemap。&lt;/p&gt;
&lt;p&gt;当前 &lt;code&gt;pnpm build&lt;/code&gt; 已经配置为先自动同步 Obsidian 文章，所以不用额外先跑 &lt;code&gt;pnpm sync-posts&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;常用命令速查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd /Users/tommywu/tommywu-lab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步文章：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm sync-posts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地预览：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型和内容检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产构建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览生产构建结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm preview
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;发布前检查清单&lt;/h2&gt;
&lt;p&gt;发布文章前可以扫一遍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt; 是否准确。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;published&lt;/code&gt; 是否是正确日期。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt; 是否能概括文章内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tags&lt;/code&gt; 是否不要太散。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;category&lt;/code&gt; 是否和已有分类一致。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;draft&lt;/code&gt; 是否已经改成 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;图片是否放在公开目录里。&lt;/li&gt;
&lt;li&gt;文章里是否有不想公开的私人信息。&lt;/li&gt;
&lt;li&gt;本地 &lt;code&gt;pnpm build&lt;/code&gt; 是否通过。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;同步失败：缺少 title&lt;/h3&gt;
&lt;p&gt;说明某篇文章没有写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: &quot;文章标题&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补上后重新运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm sync-posts
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;同步失败：缺少 published&lt;/h3&gt;
&lt;p&gt;发布日期必须是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;published: 2026-05-04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;published: 2026/05/04
published: May 4, 2026
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文章没有显示&lt;/h3&gt;
&lt;p&gt;优先检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件是否在 &lt;code&gt;/Users/tommywu/Obsidian/TommyWu Lab&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;文件扩展名是否是 &lt;code&gt;.md&lt;/code&gt; 或 &lt;code&gt;.mdx&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;draft&lt;/code&gt; 是否是 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;是否运行过 &lt;code&gt;pnpm sync-posts&lt;/code&gt; 或重新启动过 &lt;code&gt;pnpm dev&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图片没有显示&lt;/h3&gt;
&lt;p&gt;优先检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图片是否也在 &lt;code&gt;TommyWu Lab&lt;/code&gt; 目录里。&lt;/li&gt;
&lt;li&gt;图片路径是否写对。&lt;/li&gt;
&lt;li&gt;图片文件名是否和 Markdown 里的引用一致。&lt;/li&gt;
&lt;li&gt;图片格式是否是支持同步的格式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;搜索搜不到最新文章&lt;/h3&gt;
&lt;p&gt;搜索索引是生产构建时生成的。本地开发环境里搜索可能不是最终效果。&lt;/p&gt;
&lt;p&gt;要测试搜索，运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm build
pnpm preview
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在预览页面里测试搜索。&lt;/p&gt;
&lt;h2&gt;后续可以优化的地方&lt;/h2&gt;
&lt;p&gt;现在这套流程已经能稳定写作和发布。之后还可以继续优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;替换默认头像和 favicon。&lt;/li&gt;
&lt;li&gt;配置真实域名到 &lt;code&gt;astro.config.mjs&lt;/code&gt; 的 &lt;code&gt;site&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;接入 Cloudflare Pages 自动部署。&lt;/li&gt;
&lt;li&gt;给 Obsidian 增加文章模板。&lt;/li&gt;
&lt;li&gt;给常用分类和标签定一套命名规则。&lt;/li&gt;
&lt;li&gt;增加自动提交和发布脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先把写作流程跑顺，比一开始就追求完美更重要。&lt;/p&gt;
</content:encoded></item><item><title>【iOS】SDK</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-160308084-ios-sdk/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-160308084-ios-sdk/</guid><description>SDK SDK 是Software Development Kit的缩写,译为” 软件开发工具包 ”,通常是为辅助开发某类软件而编写的特定软件包,框架集合等, SDK 一般包含相关 文档 , 范例 和 工具 。客户端 SDK，顾名思义，是集成在应用客户端的 SDK。SDK 作为产</description><pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;SDK&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SDK&lt;/code&gt;是Software Development Kit的缩写,译为”&lt;code&gt;软件开发工具包&lt;/code&gt;”,通常是为辅助开发某类软件而编写的特定软件包,框架集合等,&lt;code&gt;SDK&lt;/code&gt;一般包含相关&lt;code&gt;文档&lt;/code&gt;,&lt;code&gt;范例&lt;/code&gt;和&lt;code&gt;工具&lt;/code&gt;。客户端 SDK，顾名思义，是集成在应用客户端的 SDK。SDK 作为产品与app 开发者的交互，从下载集成开始，历经功能开发、测试、打包等流程后作为应用的一部分上线，最终交付给 app 用户。&lt;/p&gt;
&lt;p&gt;SDK分为系统SDK和应用SDK&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统SDK：是为特定的软件包、软件框架、硬件平台、操作系统等简历应用时说使用的开发工具集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;应用SDK&lt;/code&gt;: 则是基于&lt;code&gt;系统SDK&lt;/code&gt;开发的独立于具体业务而具有特定功能的集合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://blog.bihe0832.com/sdk_summary.html&lt;/p&gt;
&lt;p&gt;https://juejin.cn/post/6844904131002368008?searchId=20260419124733C977621A28DE027C97B2&lt;/p&gt;
&lt;h3&gt;1. SDK的作用&lt;/h3&gt;
&lt;p&gt;SDK（Software Development Kit）的作用主要体现在以下几个方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提供API接口&lt;/strong&gt;：SDK通常包含一套API接口，这些接口是预先定义好的，开发者可以通过调用这些接口，实现与底层系统的交互，从而简化开发过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供库文件&lt;/strong&gt;：SDK中通常包含一些库文件，这些库文件包含了大量的函数和类，开发者可以直接使用这些函数和类，而无需从头开始编写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供文档和示例代码&lt;/strong&gt;：SDK还会提供详细的开发文档和示例代码，帮助开发者理解和使用API接口和库文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说，SDK的作用就是帮助开发者更快、更方便地开发应用程序。通过提供开发工具、API接口、库文件以及文档和示例代码，SDK降低了开发的难度，提高了开发的效率。&lt;/p&gt;
&lt;p&gt;在代码层面，SDK 就是一段预先编译好的、封装了特定功能的代码块。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iOS：&lt;/strong&gt; 通常以 &lt;code&gt;.framework&lt;/code&gt; 或 &lt;code&gt;.xcframework&lt;/code&gt; 的形式存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android：&lt;/strong&gt; 通常以 &lt;code&gt;.aar&lt;/code&gt;（包含 UI 和资源）或 &lt;code&gt;.jar&lt;/code&gt;（纯代码）的形式存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web：&lt;/strong&gt; 通常是一个 &lt;code&gt;.js&lt;/code&gt; 文件，通过 &lt;code&gt; &lt;/code&gt; 标签或 npm 包引入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;苹果官方本身就提供了很多SDK，平时在开发中就一直在用他们&lt;/p&gt;
&lt;p&gt;最常见的就是这些：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iOS SDK：开发 iPhone/iPad App 用的整套官方开发包&lt;/li&gt;
&lt;li&gt;macOS SDK：开发 Mac 应用&lt;/li&gt;
&lt;li&gt;watchOS SDK：开发 Apple Watch 应用&lt;/li&gt;
&lt;li&gt;tvOS SDK：开发 Apple TV 应用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这些 SDK 里面，又包含很多具体框架，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UIKit / SwiftUI：做界面&lt;/li&gt;
&lt;li&gt;AVFoundation：相机、音视频&lt;/li&gt;
&lt;li&gt;CoreLocation：定位&lt;/li&gt;
&lt;li&gt;Speech：语音识别&lt;/li&gt;
&lt;li&gt;Vision / CoreML：图像分析、机器学习&lt;/li&gt;
&lt;li&gt;UserNotifications：通知&lt;/li&gt;
&lt;li&gt;StoreKit：内购&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. API &amp;amp;&amp;amp; SDK &amp;amp;&amp;amp; Framework &amp;amp;&amp;amp; Library&lt;/h3&gt;
&lt;h4&gt;API&lt;/h4&gt;
&lt;p&gt;API 全称是 &lt;strong&gt;Application Programming Interface&lt;/strong&gt;，中文一般叫 &lt;strong&gt;应用程序编程接口&lt;/strong&gt;。他不是具体实现，而是“别人暴露给你用的功能入口”。他的作用就是将低复杂度，你调用简单的接口，底层逻辑由系统或第三方完成。&lt;/p&gt;
&lt;p&gt;API在iOS中的常见形式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;UIViewController
  UITableView
  AVPlayer
  NSURLSession
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方法 ```objective-c&lt;/li&gt;
&lt;li&gt;(void)viewDidLoad;&lt;/li&gt;
&lt;li&gt;(void)reloadData;&lt;/li&gt;
&lt;li&gt;(void)play;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;- 属性 ```objective-c
@property (nonatomic, strong) UIColor *backgroundColor;
@property (nonatomic, copy) NSString *text;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;协议 ```objective-c
UITableViewDataSource
UITableViewDelegate
AVCapturePhotoCaptureDelegate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@interface TLWIdentifyPageController () &amp;lt;AVCapturePhotoCaptureDelegate&amp;gt;
@end&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 常量和枚举 ```objective-c
PHAuthorizationStatusAuthorized
UIViewAnimationOptionCurveEaseInOut
AVPlayerStatusReadyToPlay
#### Library：库


Libarary的中文叫 库，他是一组写好的代码 可以被项目复用


```objective-c
#import &amp;lt;Masonry/Masonry.h&amp;gt;
#import &amp;lt;AFNetworking/AFNetworking.h&amp;gt;
#import &amp;lt;SDWebImage/SDWebImage.h&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些都可以理解为常见的第三方库。Libarary有两种常见形式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态库，编译时会被拷贝进你的App可执行文件，运行时不需要额外加载，但会增大最终app的体积，通常以.a后缀&lt;/li&gt;
&lt;li&gt;动态库，App 运行时才加载，但是 iOS 对动态库限制比较严格。普通 App 不能随便下载并加载新的动态库，因为这会影响安全审核。实际iOS开发中，更常见的是动态Framework，而不是直接使用.dylib&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Framework ：框架&lt;/h4&gt;
&lt;p&gt;他可以理解为Library的升级版，不仅有代码，还可以把头文件、资源文件、模块信息等打包在一起。常见后缀就是.framework&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;UIKit/UIKit.h&amp;gt;
#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &amp;lt;AVFoundation/AVFoundation.h&amp;gt;
#import &amp;lt;Photos/Photos.h&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来说，library更倾向于一个代码的集合，但是Framework则是一个完整的模块。比如一个.a静态库通常只有编译后的代码，它不自带头文件目录结构、不自带资源管理方式。而Framework可以同时放：编译好的代码、公开头文件、图片资源等，所有实际开发中他更像一个标准化封装包&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;系统Framework&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UIKit.framework
Foundation.framework
AVFoundation.framework
Photos.framework
CoreLocation.framework
CoreData.framework
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;包括使用相机时候的AVFoundation，用相册的&amp;lt;Photos/Photos.h&amp;gt;，都是系统Framework&lt;/p&gt;
&lt;h4&gt;SDK：开发软件包&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;SDK 是为了开发某个平台或某个服务而提供的一整套东西。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SDK 是这几个概念里最大的。一个SDK可以包含以上的所有东西，比如一个iOS的SDK里有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iOS SDK
├── UIKit.framework
├── Foundation.framework
├── AVFoundation.framework
├── Photos.framework
├── CoreLocation.framework
├── CoreData.framework
├── Metal.framework
├── Swift 标准库支持
├── 模拟器支持
├── 编译链接配置
└── 文档
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你能importUIKit框架，就是因为你的Xcode装了iOS的SDK&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SDK
│
├── Framework
│   ├── API
│   ├── 二进制实现代码
│   ├── 头文件
│   └── 资源文件
│
├── Library
│   ├── API
│   └── 二进制实现代码
│
├── 文档
├── Demo
└── 工具
### 3. SDK的使用


#### 1. 获取SDK


一般来说，SDK 会提供：


- 接入方式，比如 CocoaPods、SPM、npm、Gradle
- README 文档
- Demo 或示例代码
- API 文档和错误码说明 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0344695e6eb148cb9c6572d9a0101516.png)

一般来讲，SDK都会有一个Readme文档，教你快速上手。


#### 2. 配置全局环境


SDK 通常需要一些全局配置，比如：


- 服务端地址 host
- 鉴权信息 accessToken
- 超时时间
- 默认请求头
- 日志和调试开关

在项目里，对应的就是官方 SDK 里的 AGDefaultConfiguration。

其中 host 属于静态环境配置，启动时就可以设置； accessToken 属于动态登录态数据，通常在登录成功、冷启动恢复、token 刷新时再写入。


我们依次设置好URL和accessToken


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/7e648440718b42e18820ad544ee7c65e.png)


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/db65a28c5fad4aac8e226065ed932311.png)


#### 3. 发起具体调用


配置完成后，真正承接业务接口调用的是 AGApiService。

它会把后端的 REST 接口映射成一个个 Objective-C 方法。


比如后端有一个接口：


POST /api/my-crops/{id}/tags

在 SDK 里就会被映射成这样一个方法：


```objective-c
///
/// 添加种植标签 (浇水、施肥、用药、笔记)
///
///  @param _id
///
///  @param tagOperationRequest
///
///  @returns AGResultVoid*
///
-(NSURLSessionTask*) addTagWithId: (NSNumber*) _id
    tagOperationRequest: (AGTagOperationRequest*) tagOperationRequest
    completionHandler: (void (^)(AGResultVoid* output, NSError* error)) handler {
    // verify the required parameter &apos;_id&apos; is set
    if (_id == nil) {
        NSParameterAssert(_id);
        if(handler) {
            NSDictionary * userInfo = @{NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@&quot;Missing required parameter &apos;%@&apos;&quot;, nil),@&quot;_id&quot;] };
            NSError* error = [NSError errorWithDomain:kAGApiServiceErrorDomain code:kAGApiServiceMissingParamErrorCode userInfo:userInfo];
            handler(nil, error);
        }
        return nil;
    }

    // verify the required parameter &apos;tagOperationRequest&apos; is set
    if (tagOperationRequest == nil) {
        NSParameterAssert(tagOperationRequest);
        if(handler) {
            NSDictionary * userInfo = @{NSLocalizedDescriptionKey : [NSString stringWithFormat:NSLocalizedString(@&quot;Missing required parameter &apos;%@&apos;&quot;, nil),@&quot;tagOperationRequest&quot;] };
            NSError* error = [NSError errorWithDomain:kAGApiServiceErrorDomain code:kAGApiServiceMissingParamErrorCode userInfo:userInfo];
            handler(nil, error);
        }
        return nil;
    }

    NSMutableString* resourcePath = [NSMutableString stringWithFormat:@&quot;/api/my-crops/{id}/tags&quot;];

    NSMutableDictionary *pathParams = [[NSMutableDictionary alloc] init];
    if (_id != nil) {
        pathParams[@&quot;id&quot;] = _id;
    }

    NSMutableDictionary* queryParams = [[NSMutableDictionary alloc] init];
    NSMutableDictionary* headerParams = [NSMutableDictionary dictionaryWithDictionary:self.apiClient.configuration.defaultHeaders];
    [headerParams addEntriesFromDictionary:self.defaultHeaders];
    // HTTP header `Accept`
    NSString *acceptHeader = [self.apiClient.sanitizer selectHeaderAccept:@[@&quot;*/*&quot;]];
    if(acceptHeader.length &amp;gt; 0) {
        headerParams[@&quot;Accept&quot;] = acceptHeader;
    }

    // response content type
    NSString *responseContentType = [[acceptHeader componentsSeparatedByString:@&quot;, &quot;] firstObject] ?: @&quot;&quot;;

    // request content type
    NSString *requestContentType = [self.apiClient.sanitizer selectHeaderContentType:@[@&quot;application/json&quot;]];

    // Authentication setting
    NSArray *authSettings = @[@&quot;BearerAuth&quot;];

    id bodyParam = nil;
    NSMutableDictionary *formParams = [[NSMutableDictionary alloc] init];
    NSMutableDictionary *localVarFiles = [[NSMutableDictionary alloc] init];
    bodyParam = tagOperationRequest;

    return [self.apiClient requestWithPath: resourcePath
                                    method: @&quot;POST&quot;
                                pathParams: pathParams
                               queryParams: queryParams
                                formParams: formParams
                                     files: localVarFiles
                                      body: bodyParam
                              headerParams: headerParams
                              authSettings: authSettings
                        requestContentType: requestContentType
                       responseContentType: responseContentType
                              responseType: @&quot;AGResultVoid*&quot;
                           completionBlock: ^(id data, NSError *error) {
                                if(handler) {
                                    handler((AGResultVoid*)data, error);
                                }
                            }];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- (NSURLSessionTask*) requestWithPath: (NSString*) path
                               method: (NSString*) method
                           pathParams: (NSDictionary *) pathParams
                          queryParams: (NSDictionary*) queryParams
                           formParams: (NSDictionary *) formParams
                                files: (NSDictionary *) files
                                 body: (id) body
                         headerParams: (NSDictionary*) headerParams
                         authSettings: (NSArray *) authSettings
                   requestContentType: (NSString*) requestContentType
                  responseContentType: (NSString*) responseContentType
                         responseType: (NSString *) responseType
                      completionBlock: (void (^)(id, NSError *))completionBlock {

    AFHTTPRequestSerializer &amp;lt;AFURLRequestSerialization&amp;gt; * requestSerializer = [self requestSerializerForRequestContentType:requestContentType];

    __weak id&amp;lt;AGSanitizer&amp;gt; sanitizer = self.sanitizer;

    // sanitize parameters
    pathParams = [sanitizer sanitizeForSerialization:pathParams];
    queryParams = [sanitizer sanitizeForSerialization:queryParams];
    headerParams = [sanitizer sanitizeForSerialization:headerParams];
    formParams = [sanitizer sanitizeForSerialization:formParams];
    if(![body isKindOfClass:[NSData class]]) {
        body = [sanitizer sanitizeForSerialization:body];
    }

    // auth setting
    [self updateHeaderParams:&amp;amp;headerParams queryParams:&amp;amp;queryParams WithAuthSettings:authSettings];

    NSMutableString *resourcePath = [NSMutableString stringWithString:path];
    [pathParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        NSString * safeString = ([obj isKindOfClass:[NSString class]]) ? obj : [NSString stringWithFormat:@&quot;%@&quot;, obj];
        safeString = AGPercentEscapedStringFromString(safeString);
        [resourcePath replaceCharactersInRange:[resourcePath rangeOfString:[NSString stringWithFormat:@&quot;{%@}&quot;, key]] withString:safeString];
    }];

    NSString* pathWithQueryParams = [self pathWithQueryParamsToString:resourcePath queryParams:queryParams];
    if ([pathWithQueryParams hasPrefix:@&quot;/&quot;]) {
        pathWithQueryParams = [pathWithQueryParams substringFromIndex:1];
    }

    NSString* urlString = [[NSURL URLWithString:pathWithQueryParams relativeToURL:self.baseURL] absoluteString];

    NSError *requestCreateError = nil;
    NSMutableURLRequest * request = nil;
    if (files.count &amp;gt; 0) {
        request = [requestSerializer multipartFormRequestWithMethod:@&quot;POST&quot; URLString:urlString parameters:nil constructingBodyWithBlock:^(id&amp;lt;AFMultipartFormData&amp;gt; formData) {
                                                   [formParams enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                                                       NSString *objString = [sanitizer parameterToString:obj];
                                                       NSData *data = [objString dataUsingEncoding:NSUTF8StringEncoding];
                                                       [formData appendPartWithFormData:data name:key];
                                                   }];
                                                   [files enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                                                       if ([obj isKindOfClass:[NSArray class]]) {
                                                           for (NSURL *fileURL in (NSArray *)obj) {
                                                               [formData appendPartWithFileURL:fileURL name:key error:nil];
                                                           }
                                                       } else {
                                                           [formData appendPartWithFileURL:(NSURL *)obj name:key error:nil];
                                                       }
                                                   }];
                        } error:&amp;amp;requestCreateError];
    }
    else {
        if (formParams) {
            request = [requestSerializer requestWithMethod:method URLString:urlString parameters:formParams error:&amp;amp;requestCreateError];
        }
        if (body) {
            request = [requestSerializer requestWithMethod:method URLString:urlString parameters:body error:&amp;amp;requestCreateError];
        }
    }
    if(!request) {
        completionBlock(nil, requestCreateError);
        return nil;
    }

    if ([headerParams count] &amp;gt; 0){
        for(NSString * key in [headerParams keyEnumerator]){
            [request setValue:[headerParams valueForKey:key] forHTTPHeaderField:key];
        }
    }
    [requestSerializer setValue:responseContentType forHTTPHeaderField:@&quot;Accept&quot;];

    [self postProcessRequest:request];


    NSURLSessionTask *task = nil;

    if ([self.downloadTaskResponseTypes containsObject:responseType]) {
        task = [self downloadTaskWithCompletionBlock:request completionBlock:^(id data, NSError *error) {
            completionBlock(data, error);
        }];
    } else {
        __weak typeof(self) weakSelf = self;
        task = [self taskWithCompletionBlock:request completionBlock:^(id data, NSError *error) {
            NSError * serializationError;
            id response = [weakSelf.responseDeserializer deserialize:data class:responseType error:&amp;amp;serializationError];

            if(!response &amp;amp;&amp;amp; !error){
                error = serializationError;
            }
            completionBlock(response, error);
        }];
    }

    [task resume];

    return task;
}
#### 4. 在项目中再封装一层


我们统一封装了一个SDKManager，用来把第三方的都收进一个统一入口，避免业务层面到处直接依赖底层SDK。正如图上所展示，他负责配置SDK，同时把官方的SDK包装成项目自己的统一接口，同时把登录鉴权等进行统一化，而不是每个页面自己处理。


```objective-c
Controller
  ↓
TLWSDKManager
  ↓
AGApiService
  ↓
AGApiClient
  ↓
AGDefaultConfiguration
  ↓
Server API
### 4. SDK 的优点、局限


#### SDK 的优点


##### 1. 降低开发成本


SDK 最大的价值，就是把复杂能力封装好，开发者不需要从零实现底层逻辑。主流 SDK（如微信、高德、腾讯云）通常由大厂的专业团队维护。他们会针对 iOS 系统进行持续优化，解决各种机型适配和系统兼容性问题。


在 iOS 开发中，如果没有现成 SDK，很多功能都要自己处理：


- 网络请求封装
- 鉴权头拼接
- 请求参数序列化
- 响应模型解析
- 错误码处理
- 上传下载逻辑

而有了 SDK 之后，开发者只需要调用现成方法即可，由SDK发行者为你处理背后的复杂工程逻辑。


##### 2. 统一接口风格


一个成熟的 SDK 往往会把后端接口统一成固定风格的方法和数据模型。


例如：


```objective-c
[apiService loginBySmsWithSmsLoginRequest:req completionHandler:...];
[apiService chatWithChatRequest:req completionHandler:...];
[apiService uploadFileWithFile:fileURL prefix:@&quot;identify/&quot; completionHandler:...];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种统一的方法命名、统一的 &lt;code&gt;Request / Result / DTO&lt;/code&gt; 模型，会让整个项目的调用方式更加规范。&lt;/p&gt;
&lt;h5&gt;3. 提高联调效率&lt;/h5&gt;
&lt;p&gt;如果客户端直接自己手写请求，前后端在联调时很容易出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字段名不一致&lt;/li&gt;
&lt;li&gt;参数类型不一致&lt;/li&gt;
&lt;li&gt;路径写错&lt;/li&gt;
&lt;li&gt;Header 缺失&lt;/li&gt;
&lt;li&gt;响应结构理解偏差&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而通过 OpenAPI 自动生成的 SDK，可以在很大程度上减少这类问题。&lt;/p&gt;
&lt;p&gt;因为接口定义已经提前固化成代码，前后端对接会更顺畅。&lt;/p&gt;
&lt;p&gt;同时，  通过 CocoaPods 集成，你可以轻松管理版本。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pod update&lt;/code&gt; 一键升级。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod install&lt;/code&gt; 自动配置 Search Paths 和 Linker Flags，省去了手动配置 Xcode 工程的痛苦。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. SDK 的局限&lt;/h4&gt;
&lt;h5&gt;1. 自动生成代码通常比较冗长&lt;/h5&gt;
&lt;p&gt;像基于 OpenAPI 自动生成的 SDK，优点是统一，但缺点也很明显：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法名偏长&lt;/li&gt;
&lt;li&gt;代码模板味道重&lt;/li&gt;
&lt;li&gt;可读性一般&lt;/li&gt;
&lt;li&gt;结构比较“机械”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(NSURLSessionTask*) addTagWithId: (NSNumber*) _id
    tagOperationRequest: (AGTagOperationRequest*) tagOperationRequest
    completionHandler: (void (^)(AGResultVoid* output, NSError* error)) handler;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种代码虽然规范，但不一定足够优雅，也不一定最符合业务开发者的阅读习惯。&lt;/p&gt;
&lt;h5&gt;2. SDK 不一定完全贴合业务场景&lt;/h5&gt;
&lt;p&gt;SDK 更多是面向“通用接口调用”设计的，但实际项目里经常会出现更复杂的业务诉求。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;token 失效后自动刷新&lt;/li&gt;
&lt;li&gt;流式响应的逐帧处理&lt;/li&gt;
&lt;li&gt;断点续传等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些往往不是 SDK 本身能完全覆盖的，仍然需要业务侧补充一层自己的封装。&lt;/p&gt;
&lt;p&gt;但在 AI 流式对话这一块，我没有直接使用 SDK 自带的 &lt;code&gt;chatStreamWithChatRequest:&lt;/code&gt;，而是自己实现了一层 &lt;code&gt;TLWAIStreamClient&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原因是 SDK 的默认实现更适合普通请求场景，而我们项目需要的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSE 逐帧消费&lt;/li&gt;
&lt;li&gt;更及时的 UI 刷新&lt;/li&gt;
&lt;li&gt;更细粒度的流式事件解析&lt;/li&gt;
&lt;li&gt;token 失效后的重建 stream&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就说明，在工程实践里，SDK 并不是“全盘照搬”，而是要根据实际业务体验做取舍。&lt;/p&gt;
&lt;h5&gt;3. 增加APP包体积&lt;/h5&gt;
&lt;p&gt;有些SDK内部会带有非常庞大的模型或图片数据等，会导致ipa明显变大。&lt;/p&gt;
&lt;p&gt;另外，哪怕你只需要SDK中的一个方法或者函数，那么你也必须引入整个静态库，造成大量冗余&lt;/p&gt;
&lt;h5&gt;4. 调试非常困难&lt;/h5&gt;
&lt;p&gt;SDK通常是闭源的文件，如果内部出现问题 你只能等待厂商发版修复，自己无能为力沈。&lt;/p&gt;
&lt;p&gt;（ 如果是GitHub仓库，你可以考虑fork一下然后自己修复。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/160308084&quot;&gt;【iOS】SDK&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】block</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-158811388-ios-block/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-158811388-ios-block/</guid><description>block 对应结构体的定义如下： 图中我们可以看到，isa其实有六个部分 isa指针，所以对象都有isa指针。这就证明了 block其实本质上就是一个Objective C对象 ，他的值通常是这三种 NSConcreteGlobalBlock (全局区：没捕获任何外部变量) N</description><pubDate>Sun, 08 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;block&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/6a109bda543e4fffb15e270ba4f33619.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对应结构体的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图中我们可以看到，isa其实有六个部分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;isa指针，所以对象都有isa指针。这就证明了&lt;strong&gt;block其实本质上就是一个Objective-C对象&lt;/strong&gt; ，他的值通常是这三种 &lt;code&gt;_NSConcreteGlobalBlock&lt;/code&gt; (全局区：没捕获任何外部变量)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;_NSConcreteStackBlock&lt;/code&gt; (栈区：捕获了变量，但还没被强引用)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;_NSConcreteMallocBlock&lt;/code&gt; (堆区：被拷贝到了堆上，生命周期由你掌控)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;block如果不访问自由变量的话,都是存储在全局区的,如果访问全局变量的话,也是存储在全局区的Block&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;block如果访问自由变量的话 如果没有创建block变量,才会创建一个栈区Block变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建了一个Block变量,且访问自由变量,才会创建出一个堆区的Block,这里创建出堆区Block的原因是 栈区的Block执行了拷贝操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flags，他是有个int整型数字，按bit来存储Block的各种附加状态。Runtime需要靠它来判断这个Block的特性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reserved，一个保留字段 基本没啥用 主要是为了内存对齐&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;invoke，这是一个函数指针 当你在大括号里写下逻辑时，编译器会把括号里的代码抽离出来变为一个普通的C语言函数。invoke指向这个函数的内存地址。当你调用 &lt;code&gt;myBlock()&lt;/code&gt; 时，底层实际上执行的是 &lt;code&gt;myBlock-&amp;gt;invoke(myBlock, ...)&lt;/code&gt;。&lt;strong&gt;它把 Block 自己作为第一个参数传了进去&lt;/strong&gt; 因为只有把 Block 自己传进去，里面的代码才能拿到储存在 Block 尾部的那些“捕获变量”。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;descriptor，他是只想另一个结构体的指针，记录这个Block的辅助信息：size，copy函数指针（当 Block 从栈拷贝到堆（Malloc）时，调用它来保住捕获的对象（比如执行 &lt;code&gt;retain&lt;/code&gt;）），dispose函数指针（当 Block 从堆上销毁时，调用它来释放捕获的对象（比如执行 &lt;code&gt;release&lt;/code&gt;）。）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;variables，capture过来的变量，block能够访问它外部的局部变量，就是因为这些变量（或其地址）复制到了结构体中。他是6个部分中&lt;strong&gt;唯一动态变化&lt;/strong&gt;的部分，前面的 1~5 是所有 Block 都有的“固定头部”，而第 6 部分是直接&lt;strong&gt;拼贴&lt;/strong&gt;在描述信息之后的内存块。block的灵魂就在这里，如果block用到了外部的int a和NSString *b，编译器会在这个位置塞入a的值和b的指针。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;_NSConcreteGlobalBlock&lt;/h3&gt;
&lt;p&gt;全局的静态block，不会访问任何外部变量，就是NSConcreteGlobalBlock。如下所示：他是一个全局的block，在编译期间就已经决定了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main()
{
    ^{ printf(&quot;Hello, World!\n&quot;); } ();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过clang命令后 其中关键代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    //  构造函数，用来初始化block
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &amp;amp;_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf(&quot;Hello, World!\n&quot;);
}

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) };

int main()
{
    (void (*)())&amp;amp;__main_block_impl_0((void *)__main_block_func_0, &amp;amp;__main_block_desc_0_DATA) ();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;__main_block_impl_0就是该block的实现&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个block实际就是一个对象，他主要由一个isa和一个impl和一个descriptor组成。&lt;/li&gt;
&lt;li&gt;由于 clang 改写的具体实现方式和 LLVM 不太一样，并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是&lt;code&gt;_NSConcreteStackBlock&lt;/code&gt;。但在 LLVM 的实现中，开启 ARC 时，block 应该是 _NSConcreteGlobalBlock 类型，具体可以看 &lt;a href=&quot;http://blog.parse.com/2013/02/05/objective-c-blocks-quiz/&quot;&gt;《objective-c-blocks-quiz》&lt;/a&gt; 第二题的解释。&lt;/li&gt;
&lt;li&gt;impl 是实际的函数指针，本例中，它指向 __main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量，只是 clang 编译器对变量的命名不一样而已。&lt;/li&gt;
&lt;li&gt;descriptor 是用于描述当前这个 block 的附加信息的，包括结构体的大小，需要 capture 和 dispose 的变量列表等。结构体大小需要保存是因为，每个 block 因为会 capture 一些变量，这些变量会加到 __main_block_impl_0 这个结构体中，使其体积变大。在该例子中我们还看不到相关 capture 的代码，后面将会看到。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;_NSConcreteStackBlock&lt;/h3&gt;
&lt;p&gt;保存在栈的block，当函数返回时被销毁。可以这么理解，他就是引用了外部变量的block&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    int a = 100;
    void (^block2)(void) = ^{
        printf(&quot;%d\n&quot;, a);
    };
    block2();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转化后代码如下，NSConcreteStackBlock内部会有一个结构体__main_block_impl_0，这个结构体会保存外部变量，使其体积变大。而这就导致了NSConcreteStackBlock并不像宏一样，而是一个动态的对象。而它由于没有被持有，所以在它的内部，它也不会持有其外部引用的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;	//多了一个被捕获的变量a
  //	构造函数，cpp的初始化列表中，它把外面传进来的100，复制了一份
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &amp;amp;_NSConcreteStackBlock;	//分配在栈上
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself-&amp;gt;a; // bound by copy（复制过来的）
    printf(&quot;%d\n&quot;, a);
}

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main()
{
    int a = 100;
    void (*block2)(void) = (void (*)())&amp;amp;__main_block_impl_0((void *)__main_block_func_0, &amp;amp;__main_block_desc_0_DATA, a);
    ((void (*)(__block_impl *))((__block_impl *)block2)-&amp;gt;FuncPtr)((__block_impl *)block2);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;__main_block_impl_0 中增加了一个变量 a，在 block 中引用的变量 a 实际是在申明 block 时，被复制到 &lt;code&gt;__main_block_impl_0&lt;/code&gt; 结构体中的那个变量 a。因为这样，我们就能理解，在 block 内部修改变量 a 的内容，不会影响外部的实际变量 a。&lt;/li&gt;
&lt;li&gt;__main_block_impl_0 中由于增加了一个变量 a，所以结构体的大小变大了，该结构体大小被写在了 &lt;code&gt;__main_block_desc_0&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__main_block_func_0&lt;/code&gt;中的a是值拷贝，如果此时在block内部实现中作 a++操作，是有问题的，会造成编译器的代码歧义，即此时的a是只读的.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt; block捕获外部变量时，在&lt;code&gt;内部自动生成同一个属性来保存&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/16f180f3bef047d0a8b0b4df0f5b971b.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们在变量前面加一个__block关键字后，重新观察生成代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct __Block_byref_i_0 {
    void *__isa;
    __Block_byref_i_0 *__forwarding;	//新增结构体，用于保存我们要capture并且修改变量的i
    int __flags;
    int __size;
    int i;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_i_0 *i; // by ref   达到修改外部变量的作用
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i-&amp;gt;__forwarding) {
        impl.isa = &amp;amp;_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_i_0 *i = __cself-&amp;gt;i; // bound by ref

    printf(&quot;%d\n&quot;, (i-&amp;gt;__forwarding-&amp;gt;i));
    (i-&amp;gt;__forwarding-&amp;gt;i) = 1023;
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&amp;amp;dst-&amp;gt;i, (void*)src-&amp;gt;i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src-&amp;gt;i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);	//Block被拷贝到堆上时，把内部的i也copy进去
    void (*dispose)(struct __main_block_impl_0*);//	Block从堆上销毁时，把i一同销毁
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&amp;amp;i, 0, sizeof(__Block_byref_i_0), 1024};
    void (*block1)(void) = (void (*)())&amp;amp;__main_block_impl_0((void *)__main_block_func_0, &amp;amp;__main_block_desc_0_DATA, (__Block_byref_i_0 *)&amp;amp;i, 570425344);
    ((void (*)(__block_impl *))((__block_impl *)block1)-&amp;gt;FuncPtr)((__block_impl *)block1);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;我们观察到新增了一个名为 __Block_byref_i_0 的结构体，用来保存我们要 capture 并且修改的变量 i的指针和值。他的里面有一个isa指针，也就意味着他变成了一个对象。这样在Block从栈被拷贝到堆的时候，底层才能通过引用计数来管理他的生命周期。&lt;/li&gt;
&lt;li&gt;__main_block_impl_0 中引用的是 __Block_byref_i_0 的结构体指针，这样就可以达到修改外部变量的作用。&lt;/li&gt;
&lt;li&gt;我们需要负责 __Block_byref_i_0 结构体相关的内存管理，所以 __main_block_desc_0 中增加了 copy 和 dispose 函数指针，对于在调用前后修改相应变量的引用计数。&lt;/li&gt;
&lt;li&gt;(i-&amp;gt;__forwarding-&amp;gt;i) = 1023; 为什么要这么绕一圈？ 刚执行在栈上时，这个指针指向他自己，forwarding相对于在改变自己的内存。当Block被拷贝到堆上（比如使用GCD异步）时，系统会把这个&lt;code&gt;__Block_byref_i_0&lt;/code&gt;结构体原封不动复制在堆上一份，此时有两个i结构体，一个在栈一个在堆，系统会把那个旧结构体的&lt;code&gt;forwarding&lt;/code&gt;指针强行掰弯，让他指向堆上的那个新结构体，同时，堆上新结构体的&lt;code&gt;__forwarding&lt;/code&gt;指针指向他自己。这样无论从外面栈上的作用域去访问 还是从Block内部（堆上的作用域）去访问i，所有人都要经过&lt;code&gt;i-&amp;gt;__forwarding-&amp;gt;i&lt;/code&gt;，所以对最终访问到的，永远是堆上的同一块内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e351e7c781a347febeae43c76f85036c.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;_NSConcreteMallocBlock&lt;/h3&gt;
&lt;p&gt;保存在堆上的block，当引用计数为0时会被销毁。NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现，因为默认它是当一个 block 被 copy 的时候，才会将这个 block 复制到堆中。以下是一个 block 被 copy 时的示例代码 (来自 &lt;a href=&quot;http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/&quot;&gt;这里&lt;/a&gt;)，可以看到，在第 8 步，目标的 block 类型被修改为 _NSConcreteMallocBlock。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE &amp;amp; flags) == WANTS_ONE;

    // 1
    if (!arg) return NULL;

    // 2
    aBlock = (struct Block_layout *)arg;

    // 3
    if (aBlock-&amp;gt;flags &amp;amp; BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&amp;amp;aBlock-&amp;gt;flags);
        return aBlock;
    }

    // 4
    else if (aBlock-&amp;gt;flags &amp;amp; BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    // 5
    struct Block_layout *result = malloc(aBlock-&amp;gt;descriptor-&amp;gt;size);
    if (!result) return (void *)0;

    // 6
    memmove(result, aBlock, aBlock-&amp;gt;descriptor-&amp;gt;size); // bitcopy first

    // 7
    result-&amp;gt;flags &amp;amp;= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result-&amp;gt;flags |= BLOCK_NEEDS_FREE | 1;

    // 8
    result-&amp;gt;isa = _NSConcreteMallocBlock;

    // 9
    if (result-&amp;gt;flags &amp;amp; BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock-&amp;gt;descriptor-&amp;gt;copy)(result, aBlock); // do fixup
    }

    return result;
}
### 变量的复制


对应block外的变量引用，block默认是将其复制到其数据结构中实现访问，


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9f437aa583a34cf89bb873866b498792.png)


对于用__block修饰的外部变量引用，block通过复制其地址来实现


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1f9902ac6a1f4056a3d2263c24c32cc0.png)


### Block的三层拷贝


想要分析block的三层copy，首先需要知道外部变量的种类有哪些，其中用的最多的是`BLOCK_FIELD_IS_OBJECT`和`BLOCK_FIELD_IS_BYREF`


```objc
// Runtime support functions used by compiler when generating copy/dispose helpers

// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
    // see function implementation for a more complete description of these fields and combinations
    //普通对象，即没有其他的引用类型,也就是我们任何的一个Object都是这个逻辑
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    //block类型作为变量，block套block
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    //经过__block修饰的变量和对象
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    //weak 弱引用变量
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    //返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记，配合flags一起使用
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};
#### __Block_copy_internal


```objc
static void *_Block_copy_internal(const void *arg, const int flags)
{
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE &amp;amp; flags) == WANTS_ONE;

    if (!arg) return NULL;

    aBlock = (struct Block_layout *)arg; // 强制转化成Block_layout对象,防止对外界造成影响
    if (aBlock-&amp;gt;flags &amp;amp; BLOCK_NEEDS_FREE)         // NSConcreteMallocBlock
      //是否需要释放
    {
        latching_incr_int(&amp;amp;aBlock-&amp;gt;flags);
        return aBlock;
    }
    else if (aBlock-&amp;gt;flags &amp;amp; BLOCK_IS_GLOBAL)     // NSConcreteGlobalBlock
      //如果是全局block,直接返回
    {
        return aBlock;
    }
		//	为栈block或者是堆区block,由于堆区需要申请内存,所以是栈区的block的操作
                                                 // Its a stack block.  Make a copy.
    struct Block_layout *result = malloc(aBlock-&amp;gt;descriptor-&amp;gt;size);
    if (!result) return (void *)0;
  //通过memmove内存拷贝,将aBlock按位拷贝到result
    memmove(result, aBlock, aBlock-&amp;gt;descriptor-&amp;gt;size); // bitcopy first
    // reset refcount
    result-&amp;gt;flags &amp;amp;= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result-&amp;gt;flags |= BLOCK_NEEDS_FREE | 1;
    result-&amp;gt;isa = _NSConcreteMallocBlock;	//isa指针被重写 变身堆block
    if (result-&amp;gt;flags &amp;amp; BLOCK_HAS_COPY_DISPOSE) 	//如果 Block 捕获了对象（比如强引用了 self），或者使用了 __block 变量，它的 flags 里就会带有 BLOCK_HAS_COPY_DISPOSE 标记。
    {
            (*aBlock-&amp;gt;descriptor-&amp;gt;copy)(result, aBlock); // do fixup
      // 对__block对象 把它也拷贝到堆上，并把那个forwarding指针指向堆区新位置
    }

    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个copy就是栈block到堆block到核心，无论你在上层写 &lt;code&gt;[block copy]&lt;/code&gt; 还是 ARC 帮你自动 copy，全天下的 Block 最终都要流经这个。&lt;/p&gt;
&lt;h4&gt;__Block_object_assign&lt;/h4&gt;
&lt;p&gt;只有当你的 &lt;code&gt;__block&lt;/code&gt; 修饰的不仅是个基本类型，而是一个&lt;strong&gt;真正的 Objective-C 对象&lt;/strong&gt;时（比如 &lt;code&gt;__block NSObject *obj&lt;/code&gt;），才会触发这最深的一层。现在&lt;code&gt;Block_byref&lt;/code&gt; 结构体已经搬到堆上了，它里面还装着一个 &lt;code&gt;NSObject *obj&lt;/code&gt; 的指针！这个 &lt;code&gt;NSObject&lt;/code&gt; 可是由 ARC（引用计数）管理的。既然堆上的 &lt;code&gt;Block_byref&lt;/code&gt; 现在持有了它，就必须对它负责！堆上的 &lt;code&gt;Block_byref&lt;/code&gt; 结构体会调用 &lt;code&gt;_Block_object_assign&lt;/code&gt;，并传入代号 &lt;code&gt;BLOCK_FIELD_IS_OBJECT&lt;/code&gt; (值为 3)。系统一看，懂了，对肚子里这个真正的 OC 对象执行一次 &lt;code&gt;retain&lt;/code&gt; 操作（引用计数 +1），确保它不会被提前释放。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是普通对象（3 - IS_OBJECT），则交给&lt;code&gt;系统arc处理&lt;/code&gt;，并&lt;code&gt;拷贝对象指针&lt;/code&gt;，即&lt;code&gt;引用计数+1&lt;/code&gt;，所以外界变量不能释放&lt;/li&gt;
&lt;li&gt;如果是&lt;code&gt;block类型&lt;/code&gt;的变量（7 - IS_BLOCK），则通过&lt;code&gt;_Block_copy&lt;/code&gt;操作，将block从&lt;code&gt;栈区拷贝到堆区&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果是&lt;code&gt;__block修饰&lt;/code&gt;的变量（8 - IS_BYREF），调用&lt;code&gt;_Block_byref_copy&lt;/code&gt;函数 进行内存拷贝以及常规处理&lt;/li&gt;
&lt;li&gt;如果是弱引用（16 - IS_WEAK），他只做弱引用绑定，不增加引用计数，防止循环引用&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;__Block_byref_copy&lt;/h4&gt;
&lt;p&gt;如果block的标识位里有&lt;code&gt;_Block_byref_copy&lt;/code&gt;有 &lt;code&gt;BLOCK_HAS_COPY_DISPOSE&lt;/code&gt;，并且捕获的变量带有 &lt;code&gt;__block&lt;/code&gt; 前缀。发生第二层拷贝&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static struct Block_byref *_Block_byref_copy(const void *arg) {

    //强转为Block_byref结构体类型，保存一份
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src-&amp;gt;forwarding-&amp;gt;flags &amp;amp; BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack 申请内存
        struct Block_byref *copy = (struct Block_byref *)malloc(src-&amp;gt;size);
        copy-&amp;gt;isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy-&amp;gt;flags = src-&amp;gt;flags | BLOCK_BYREF_NEEDS_FREE | 4;
        //block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个，这也是为什么__block修饰的变量具有修改能力
        //copy 和 scr 的地址指针达到了完美的同一份拷贝，目前只有持有能力
        copy-&amp;gt;forwarding = copy; // patch heap copy to point to itself // 把堆区的指针指向自己
        src-&amp;gt;forwarding = copy;  // patch stack to point to heap copy // 把栈区的指针指向堆区,保证数值一致
        copy-&amp;gt;size = src-&amp;gt;size;
        //如果有copy能力
        if (src-&amp;gt;flags &amp;amp; BLOCK_BYREF_HAS_COPY_DISPOSE) { // 有自己的一个copy的逻辑
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            //Block_byref_2是结构体，__block修饰的可能是对象，对象通过byref_keep保存，在合适的时机进行调用
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2-&amp;gt;byref_keep = src2-&amp;gt;byref_keep; // 调用自定义复制逻辑
            copy2-&amp;gt;byref_destroy = src2-&amp;gt;byref_destroy;

            if (src-&amp;gt;flags &amp;amp; BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3-&amp;gt;layout = src3-&amp;gt;layout;
            }
            //等价于 __Block_byref_id_object_copy
            (*src2-&amp;gt;byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src-&amp;gt;size - sizeof(*src)); //如果为简单类型就直接进行一个内存拷贝
        }
    }
    // already copied to heap
    else if ((src-&amp;gt;forwarding-&amp;gt;flags &amp;amp; BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&amp;amp;src-&amp;gt;forwarding-&amp;gt;flags);
    }

    return src-&amp;gt;forwarding;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;咱们之前说过，加了 &lt;code&gt;__block&lt;/code&gt; 的变量会被编译器变成一个庞大的结构体（&lt;code&gt;Block_byref&lt;/code&gt;）。这个结构体最开始也是在栈上的，也需要被搬到堆上面。这一层解决了__block变量在堆区的存活和数据同步问题，如果是类似&lt;code&gt;__block int a = 10&lt;/code&gt;，拷贝到这里也结束了。&lt;/p&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一层通过_Block_copy实现对象的自身拷贝,从栈区拷贝到堆区&lt;/li&gt;
&lt;li&gt;第二层通过调用_Block_byref_copy这个来实现对于对象拷贝成Block_byref类型&lt;/li&gt;
&lt;li&gt;第三次调用_Block_object_assign对于__block修饰的当前变量内部对象的 内存 管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;当且仅当用__block变量的时候才会有三次拷贝.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;循环引用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Weak wrong dance在这里不过多赘述，给出几行总结 如果block内部没有直接嵌套block，直接使用&lt;code&gt;__weak&lt;/code&gt;修饰self&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果block内部嵌套block，需要同时使用&lt;code&gt;__weak&lt;/code&gt; 和 &lt;code&gt;__strong&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;__block修饰符。我们可以采用&lt;code&gt;__block&lt;/code&gt;修饰符,在主动调用完后手动释放self ```objective-c
__block PersonViewController* vc = self;
self.testBlock = ^(void){
NSLog(@&quot;%@&quot;, vc.name);
vc = nil;
};
self.testBlock();&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;- 对象self作为参数。主要是把对象self作为参数,提供给block内部使用,不会有引用计数问题


```objective-c
self.testBlock = ^(PersonViewController* vc){
        NSLog(@&quot;%@&quot;, vc.name);
    };
### ACR与block类型的影响


&amp;gt; 在 ARC 开启的情况下，将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。


在上面介绍NSConcreteStackBlock的时候，是在ARC环境下跑的，而打印出来的日志明确的显示出，当时的block类型为NSConcreteStackBlock。


而实际上，为什么大家普遍会认为ARC下不存在NSConcreteStackBlock呢？


**这是因为本身我们常常将block赋值给变量，而ARC下默认的赋值操作是strong的，到了block身上自然就成了copy，所以常常打印出来的block就是NSConcreteMallocBlock了。**


原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中，会输出 ` `。在苹果的 [官方文档](http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html) 中也提到，当把栈中的 block 返回时，不需要调用 copy 方法了。


我个人认为这么做的原因是，由于 ARC 已经能很好地处理对象的生命周期的管理，这样所有对象都放到堆上管理，对于编译器实现来说，会比较方便。

---

原文发布于 CSDN：[【iOS】block](https://blog.csdn.net/2402_86720949/article/details/158811388)&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Effective Objective-C 熟悉Oc</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-157810765-effective-objective-c-oc/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-157810765-effective-objective-c-oc/</guid><description>熟悉Objective C 了解Objective C语言的起源 1. 使用消息结构的语言，其运行时所执行的代码由运行环境决定，使用函数调用的语言，则由编译器决定 详细说说： 在C语言中，编译器在编译阶段或链接阶段就已经知道了函数在内存中的地址（或相对偏移量），他生成的汇编指令是</description><pubDate>Thu, 12 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;熟悉Objective-C&lt;/h2&gt;
&lt;h3&gt;了解Objective-C语言的起源&lt;/h3&gt;
&lt;h4&gt;1. 使用消息结构的语言，其运行时所执行的代码由运行环境决定，使用函数调用的语言，则由编译器决定&lt;/h4&gt;
&lt;p&gt;详细说说：&lt;/p&gt;
&lt;p&gt;在C语言中，编译器在编译阶段或链接阶段就已经知道了函数在内存中的地址（或相对偏移量），他生成的汇编指令是硬编码的。&lt;strong&gt;运行时的行为：&lt;/strong&gt; CPU 执行到这一行指令时，毫不犹豫地跳到那个地址执行。运行时环境（Runtime），也没有查找过程。如果不小心那个地址是错的，程序直接崩溃，没有任何商量的余地。&lt;/p&gt;
&lt;p&gt;在 Objective-C 中，&lt;strong&gt;运行环境 (Runtime)&lt;/strong&gt; 是真正的“决策者”。编译器只是一个“发令员”，它不决定具体执行哪段代码，它只负责发送请求。他不知道具体的代码在哪里，甚至不知道能不能响应这个消息。在程序运行到这一行时，objc_msgSend接管控制权（拿到obj指针；查找所属Class；在Class方法列表/缓存中通过方法名查找对应的函数指针；如果找到了，Runtime才跳转去执行函数指针指向的代码，如果找不到，Runtime会去父类找，或者启动消息转发机制）。&lt;/p&gt;
&lt;p&gt;这个区别带来了什么？ 正是因为由运行环境决定，所以在Oc中我们可以重写原有方法，对动态类型（ID）发送任何消息，抑或实现消息转发。&lt;/p&gt;
&lt;h4&gt;2. Objective - C 的对象强制只能在堆（Heap）上分配内存，而不能在栈（Stack）上分配。我们在栈上拥有的，永远只是指向那个堆内存的指针。&lt;/h4&gt;
&lt;p&gt;C++ 允许在栈上分配对象，但同时也带来了“对象切割”的风险。Objective-C 为了支持纯粹的动态多态性，必须避免这个问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;栈的特性：&lt;/strong&gt; 栈上的变量在编译期必须确定&lt;strong&gt;确切的大小&lt;/strong&gt;（Size）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多态的矛盾：&lt;/strong&gt; 在面向对象编程中，子类通常比父类大（因为子类有更多的属性）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果ObjC允许栈分配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 假设这是合法的（实际上是非法的）
NSObject obj; // 编译器在栈上预留了 NSObject 大小的空间（很小，只有一个 isa 指针）
​
// 如果我们尝试把一个巨大的子类赋值给它
MyHugeObject *huge = [[MyHugeObject alloc] init];
obj = *huge; // jj了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;code&gt;obj&lt;/code&gt; 在栈上只预留了很小的空间，&lt;code&gt;MyHugeObject&lt;/code&gt; 中多出来的属性根本塞不进去，会被强行“切掉”（Sliced off）。这会导致数据丢失，对象变成一个残废的 &lt;code&gt;NSObject&lt;/code&gt;，多态性完全失效。&lt;/p&gt;
&lt;p&gt;因此在ObjC中，强制使用指针。无论对象本身多大，指针的大小是固定的，栈上只储存指针，而真正的内容存在堆中，完美支持多态。&lt;/p&gt;
&lt;p&gt;Objective-C 是一门**引用计数（Reference Counting）**语言。对象经常需要在不同的函数、不同的控制器之间传递。为了让对象能够“活”得比创建它的函数更长，ObjC 选择将对象全部分配在堆上，通过 &lt;code&gt;retain/release&lt;/code&gt; 来灵活控制它的生死。&lt;/p&gt;
&lt;p&gt;Objective-C 的核心是 Runtime。所有的对象都必须以 &lt;code&gt;isa&lt;/code&gt; 指针开头。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;isa 指izzling：&lt;/strong&gt; KVO（键值观察）等技术会在运行时动态修改对象的 &lt;code&gt;isa&lt;/code&gt; 指针，甚至动态添加类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存布局：&lt;/strong&gt; 虽然理论上栈对象也可以有 &lt;code&gt;isa&lt;/code&gt;，但 ObjC 的消息发送机制（&lt;code&gt;objc_msgSend&lt;/code&gt;）以及弱引用系统（Weak Table）等基础设施，都是围绕着“对象是堆上的稳定内存块”这一假设构建的。如果对象在栈上，随着函数调用栈的推入弹出，内存地址不稳定，会让 Runtime 的很多特性难以实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Objective-C 禁止栈上分配对象，是用&lt;strong&gt;微小的性能损耗&lt;/strong&gt;（堆分配比栈慢，且有指针间接访问的开销），换取了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无限的多态能力&lt;/strong&gt;（彻底解决对象切割）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活的生命周期管理&lt;/strong&gt;（引用计数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强大的动态运行时特性&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;在类的头文件中尽量少引入其他头文件&lt;/h3&gt;
&lt;h4&gt;1. 除非确有必要，否则不要引入头文件。一般来说，应在某个类的头文件中使用向前声明来提及别的类，并在实现文件中应入那些类的头文件，这样做可以尽量降低类之间的耦合&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;解决了循环依赖&lt;/li&gt;
&lt;li&gt;减少了编译时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当编译器不需要知道具体的内存布局或者方法细节时，可以使用Class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@class NLSong; // .h 文件
@property (nonatomic, strong) NLSong *currentSong;
​
- (void)playSong:(NLSong *)song;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当编译器必须知道具体细节时，必须#import&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;继承：父类的所有属性和方法子类都要有，编译器必须知道父类&lt;/li&gt;
&lt;li&gt;遵循协议：虽然也可以用 &lt;code&gt;@protocol&lt;/code&gt; 向前声明，但通常直接 import 更常见，除非是为了解耦&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2，有时无法使用向前声明，比如声明某个类遵循某一协议。这种情况下，尽量把“该类遵循某协议”的这条声明单独移至&quot;class-continuation分类中&quot;。如果不行的话，就把协议单独放在一个头文件中，然后引入&lt;/h4&gt;
&lt;p&gt;我们详细来说说：当你只是&lt;strong&gt;使用&lt;/strong&gt;某个类或协议作为类型时，编译器不需要知道它的内部结构，只需要知道名字。但是，当你声明一个&lt;strong&gt;类遵循某个协议&lt;/strong&gt;时，情况就变了：编译器在编译类的定义时，必须验证类是否有能力实现协议里规定的方法，或者至少需要知道协议里到底有什么方法（继承关系、属性等）。&lt;strong&gt;编译器必须看到协议的完整定义&lt;/strong&gt;。因此，你被迫在 &lt;code&gt;.h&lt;/code&gt; 文件中写 &lt;code&gt;#import&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果协议定义在一个很大的头文件里，那么引入就会连带引入一堆无关的东西，导致编译变慢，甚至引发循环引用。&lt;/p&gt;
&lt;h5&gt;方案1： 移至“Class-Extension” (类扩展)&lt;/h5&gt;
&lt;p&gt;适用场景于 这个协议只是你&lt;strong&gt;内部实现细节&lt;/strong&gt;，外部使用者不需要知道你遵循了这个协议。&lt;/p&gt;
&lt;p&gt;我们在h中不引入任何协议头文件，自在m中引入，不影响外部编译&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NLPlayerManager.m
#import &quot;NLPlayerManager.h&quot;
#import &amp;lt;SDWebImage/SDWebImageManager.h&amp;gt; // ✅ 引用只发生在这里，不扩散
​
//  这里就是 Class-Extension
// 编译器在编译 .m 时会看到这个声明，知道你要实现这些方法
@interface NLPlayerManager () &amp;lt;SDWebImageManagerDelegate&amp;gt;
// 你甚至可以在这里定义私有属性
@property (nonatomic, strong) SDWebImageManager *imageManager;
@end
​
@implementation NLPlayerManager
​
- (void)imageManager:(SDWebImageManager *)imageManager didFinishWithImage:(UIImage *)image {
    // 实现协议方法
}
​
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;封装性 (Encapsulation)&lt;/strong&gt;：遵循什么协议是类的“内部实现细节”。外部不需要知道你用 SDWebImage 还是 YYImage。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编译隔离&lt;/strong&gt;：如果 &lt;code&gt;SDWebImage&lt;/code&gt; 升级了或改名了，所有引用 &lt;code&gt;NLPlayerManager.h&lt;/code&gt; 的其他业务模块（比如 &lt;code&gt;HomeVC&lt;/code&gt;）完全不需要重新编译，因为它们根本不知道 &lt;code&gt;SDWebImage&lt;/code&gt; 的存在。&lt;/p&gt;
&lt;h5&gt;方案2： 协议单独放在一个头文件&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;： 这个协议必须是&lt;strong&gt;公开&lt;/strong&gt;的，外部使用者需要知道你遵循了这个协议，或者外部需要使用这个协议类型。&lt;/p&gt;
&lt;p&gt;把协议从类文件中剥离出来，放到一个只有协议的 &lt;code&gt;.h&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NLPlayable.h
// 这个文件非常轻量，没有复杂的依赖
​
@class NLSong; // 配合向前声明使用
​
@protocol NLPlayable &amp;lt;NSObject&amp;gt;
- (void)playSong:(NLSong *)song;
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// NLMusicPlayer.h
#import &quot;NLPlayable.h&quot; // 引入这个轻量级文件
​
// 因为协议文件很小，即使 import 也没什么负担
@interface NLMusicPlayer : NSObject &amp;lt;NLPlayable&amp;gt;
@end
### 多用字面量语法（语法糖）


**类型** **字面量语法 (推荐)** **等价的旧式写法 (Verbose)** **备注** **NSNumber**(整数) `@100` `[NSNumber numberWithInt:100]` 还有 `numberWithInteger:` **NSNumber**(浮点) `@3.14` `[NSNumber numberWithDouble:3.14]` **NSNumber**(布尔) `@YES` `[NSNumber numberWithBool:YES]` **NSNumber**(字符) `@&apos;A&apos;` `[NSNumber numberWithChar:&apos;A&apos;]` **NSString** `@&quot;Hello&quot;` `[NSString stringWithUTF8String:&quot;Hello&quot;]` (仅作示意，NSString 本身就是特殊的) **NSArray** `@[a, b, c]` `[NSArray arrayWithObjects:a, b, c, nil]` **注意结尾的 nil** **NSDictionary** `@{key : value}` `[NSDictionary dictionaryWithObjectsAndKeys:value, key, nil]` **注意 Key-Value 顺序**


- 字面量创建的对象全是**“不可变”**的
- **小心 `nil`**：在使用字面量插入数据前，如果数据可能为空，务必做判空处理，否则 App 会闪退。
- 通过取下标的操作来访问数组下表或字典中的键对应的元素。


### 多用类型常量，少用#define预处理指令


- #### 不要用预处理指令定义常量。这样定义的常量不含类型信息，编译器知识会在编译前据此执行查找和替换操作。即使有人重新定义常量值，也不会产生警告


const活在预处理阶段，把代码里所有的宏名直接替换成后面的字符串。而const则是在编译阶段，编译器知道这是一个变量，有具体的类型（`NSTimeInterval`），并且被标记为只读。它会进入符号表（Symbol Table）。


- #### 在实现文件中还是要static const 来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中，所以无需为其名称加前缀


在我们不加static的时候，默认是extern，编译器会创建一个全局符号，如果你在另一个文件也写了相同的一句定义，那么编译器在编译时都没有问题，但在链接阶段，链接器会报错：重复定义。而加上static就不会出现这种情况


- #### 在头文件使用extern来声明全局变量，并在相关实现文件中定义其值。这种常量要出现在全局符号表中，所以其名称应加以区隔，通常用与之相关的类名做前缀


在头文件中，我extern声明这个变量，但是现在不分配内存 先进行编译，等链接阶段再去找到他。


在实现文件中，对他进行赋值，为其分配内存，写入具体的值。


**什么是全局符号表？** 链接器（Linker）在把所有的 `.o` 文件（编译产物）合并成一个可执行文件时，会把所有全局符号放在一张大表里进行匹配。如果不加前缀，如果你的文件中定义来一个变量，同时，你引入的第三方库中也定义了一个全局变量，链接器在合并时就会因符号重复定义而崩溃。


因此，需要加上类前缀或模块前缀来模拟命名空间，保证全局唯一性。


- #### 对于对象类型，const位置也很重要 正确：`NSString * const` 是指针常量。
- `NLNotification` 这个**指针**一旦指向了那个字符串对象的地址，就不能变了。这才是我们要的“常量”。


	错误：`const NSString *`


- 是指向常量的指针。
- 这意味着你不能通过指针修改字符串内容（但在 ObjC 中 NSString 本来就不可变），但你可以让 `NLNotification` 指向别的字符串。这就不安全了。


### 用枚举表示状态、选项、状态码


在Objective-C中，我们应该摒弃原始的 C 语言 `enum` 写法，转而使用 Apple 提供的两个宏：**`NS_ENUM`** 和 **`NS_OPTIONS`**。


这两个宏不仅让代码更规范，还能确保你的代码在 C++ 模式下编译通过，并且能完美地桥接到 Swift。


- #### 用枚举表示状态机的状态、传递给方法的选项以及状态码等值，给这些值起一个易懂的名字
- #### 如果把传递给某个方法的选项表示为枚举类型，而多个选项又可以同时使用，那么就讲各选项值定义为2的幂，以便通过按位或操作将其组合起来
- #### NS_ENUM与NS_OPTIONS定义枚举类型，并指明底层数据类型，可以确保枚举使用开发者所选的底层数据类型实现的，而不是编译器所选类型


在古老的 C 语言标准中，编译器在处理 `enum` 时有很大的自由度。编译器只需要保证底层类型能装得下你定义的最大值即可。


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;enum NLConnectionState {
   NLConnectionStateDisconnected = 0,
   NLConnectionStateConnected = 1,
};&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

**编译器 A** 可能会看这两个值很小（0 和 1），为了省内存，决定用 `char` (1 字节) 来存储它。


**编译器 B** 可能会为了性能对齐，决定用 `int` (4 字节) 来存储它.


如果你编写的是一个静态库（.a 或 .framework），你的库是用编译器 A 编译的（占 1 字节）。而使用你库的 App 是用编译器 B 编译的（以为占 4 字节）。当 App 试图读取你的枚举值时，内存读写就会错位，导致崩溃或数据异常。


而apple提供的宏强制制定底层数据类型会在与swift桥接和向前声明中更有优势。


- #### 处理枚举类型的switch不要实现default分之。 不写 `default` 分支是为了利用编译器的**全面性检查 (Exhaustiveness Check)**，这是防止“漏改”逻辑的最后一道防线。

---

原文发布于 CSDN：[Effective Objective-C 熟悉Oc](https://blog.csdn.net/2402_86720949/article/details/157810765)&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【iOS】MVVM</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-155750158-ios-mvvm/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-155750158-ios-mvvm/</guid><description>MVC模式 MVC的前世： 这个架构中，三个实体联系太过紧密，每个实体都知道另外的两个实体。这就导致了复用性能急剧下降。 apple MVC : 苹果官方推荐使用的MVC，结构大致如下：https://developer.apple.com/library/archive/doc</description><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;MVC模式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MVC的前世：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/12a11958a8d092acb45d225cb2f3c2ef.png&quot; alt=&quot;Traditional MVC&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个架构中，三个实体联系太过紧密，每个实体都知道另外的两个实体。这就导致了复用性能急剧下降。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;apple MVC&lt;/strong&gt; :&lt;/p&gt;
&lt;p&gt;苹果官方推荐使用的MVC，结构大致如下：https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/a95bcdf570a510ea69fd03d74ba1ee3c.png&quot; alt=&quot;Model-View-Controller design pattern&quot; /&gt;&lt;/p&gt;
&lt;p&gt;友链：https://www.jianshu.com/p/9e8fd85b61e0&lt;/p&gt;
&lt;p&gt;可以看出&lt;code&gt;View&lt;/code&gt;跟&lt;code&gt;Model&lt;/code&gt;事实上是没有交互的，由&lt;code&gt;Controller&lt;/code&gt;负责&lt;code&gt;Model&lt;/code&gt;与&lt;code&gt;View&lt;/code&gt;之间的交互，交互越多，&lt;code&gt;Controller&lt;/code&gt;就越臃肿，更别提实际运用中有些还去掉了&lt;code&gt;View&lt;/code&gt;层或者&lt;code&gt;Model&lt;/code&gt;层。目前对MVC架构划分是&lt;code&gt;Model&lt;/code&gt;作为&lt;code&gt;数据管理者&lt;/code&gt;，&lt;code&gt;View&lt;/code&gt;作为&lt;code&gt;数据展示者&lt;/code&gt;，&lt;code&gt;Controller&lt;/code&gt;作为&lt;code&gt;数据加工者&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然而在iOS中&lt;code&gt;Controller&lt;/code&gt;中由于有苹果内定的一些视图的生命周期在里面，比如&lt;code&gt;viewDidLoad&lt;/code&gt;等等，于是就出现了一些关于iOS的MVC架构方面的争论，有些认为在iOS开发中并没有什么&lt;code&gt;View&lt;/code&gt;和&lt;code&gt;Controller&lt;/code&gt;，只有&lt;code&gt;Model+ViewController&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里，给出一个某大咖关于MVC框架的理解：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Model应该做的： - 给ViewController提供数据 - 给ViewController储存数据提供接口 - 提供经过抽象的业务逐渐，供Controller调度 Controller应该做： - 管理生命周期 - 生成所有的View的实例，并放入ViewController - 监听来自View与业务有关的时间，通过和Model合作，来完成对应事件的业务。 View应该做： - 响应与业务无关的时间，并因此引发动画效果，点击反馈（如果合适还是放在View去做） - 界面元素表达&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是，几十年过去了，我们对于 MVC 这种设计模式真的用得好吗？其实不是的，MVC 这种分层方式虽然清楚，但是如果使用不当，很可能让大量代码都集中在 Controller 之中，让 MVC 模式变成了 &lt;code&gt;Massive View Controller&lt;/code&gt; 模式。（巨屎控制器）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MVC现实情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/9d4d0ae26733054caf0bded453321815.png&quot; alt=&quot;Realistic Cocoa MVC&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cocoa MVC&lt;/code&gt;鼓励你使用大型的视图控制器（&lt;strong&gt;Massive&lt;/strong&gt; View Controllers），由于他们都参与到了视图（&lt;code&gt;View&lt;/code&gt;）的生命周期中了以至于很难说他们是分离的。尽管你仍有能力分流一些业务逻辑和数据转换功能到模型（&lt;code&gt;Model&lt;/code&gt;）中，但是当涉及到把工作分流到视图（&lt;code&gt;View&lt;/code&gt;）中去时你就诶有更多的选择了，因为在大多数时候视图（&lt;code&gt;View&lt;/code&gt;）的所有职责是把动作传递到控制器（&lt;code&gt;Controller&lt;/code&gt;）中。视图控制器（&lt;code&gt;View Controller&lt;/code&gt;）最终最为所有控件的委托和数据源，通常负责调度和取消网络请求…应有尽有&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;M——VC&lt;/strong&gt; 可能是对iOS开发中&lt;code&gt;iOS&lt;/code&gt;模式更为准确的解读，也更为准确地描述了我们日常开发编写的MVC代码。&lt;/p&gt;
&lt;p&gt;就像在日常开发中的制定也cell，正是直接由View来调Model，所以事实上典型的MVC以及违背了，但是人们一直不觉得这有哪些不对。如果严格遵守&lt;code&gt;MVC&lt;/code&gt;的话，你会把对&lt;code&gt;cell&lt;/code&gt;的设置放在&lt;code&gt;Controller&lt;/code&gt;中，不向&lt;code&gt;View&lt;/code&gt;传递一个&lt;code&gt;Model&lt;/code&gt;对象，这样就会大大增加&lt;code&gt;Controller的体积&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;综上所述，&lt;code&gt;Cocoa MVC&lt;/code&gt;是一个相当糟糕的事情。但是如果你没有打算在项目架构上耗费太多时间，那么他就是你的最好选择。。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cocoa MVC is the best architectural pattern in term of the speed of the development. 在开发速度上面&lt;code&gt;Cocoa MVC&lt;/code&gt;是最好的架构模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/0914b98341d74c6d964447b9432b37b6.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;如何解决Controller臃肿的问题？&lt;/h3&gt;
&lt;p&gt;这里我摘录一段博客：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于 View 来说，你如果抽象得好，那么一个 App 的动画效果可以很方便地移植到别的 App 上，而 Github 上也有很多 UI 控件，这些控件都是在 View 层做了很好的封装设计，使得它能够方便地开源给大家复用。 对于 Model 来说，它其实是用来存储业务的数据的，如果做得好，它也可以方便地复用。 说完 View 和 Model 了，那我们想想 Controller，Controller 有多少可以复用的？我们写完了一个 Controller 之后，可以很方便地复用它吗？结论是：非常难复用。在某些场景下，我们可能可以用 &lt;code&gt;addSubViewController&lt;/code&gt; 之类的方式复用 Controller，但它的复用场景还是非常非常少的。 如果我们能够意识到 Controller 里面的代码不便于复用，我们就能知道什么代码应该写在 Controller 里面了，那就是那些不能复用的代码。在我看来，Controller 里面就只应该存放这些不能复用的代码，这些代码包括： - 在初始化时，构造相应的 View 和 Model。 - 监听 Model 层的事件，将 Model 层的数据传递到 View 层。 - 监听 View 层的事件，并且将 View 层的事件转发到 Model 层。 如果 Controller 只有以上的这些代码，那么它的逻辑将非常简单，而且也会非常短。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;把每一个网络请求封装为单例类&lt;/h4&gt;
&lt;p&gt;这其实是设计模式中的Command模式，优点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**解耦网络库：**C/M不直接依赖AFN&lt;/li&gt;
&lt;li&gt;**公共逻辑统一处理：**鉴权、token、版本号、统一错误处理、统一header等都可以在基类处理&lt;/li&gt;
&lt;li&gt;**缓存/数据持久化：**可以自行实现（如离线队列等）&lt;/li&gt;
&lt;li&gt;**更好的拓展性能：**支持插件、日志、JSON校验、请求组合（并行/串联）、短点续传等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这部分代码从Controller剥离后，不但简化了Controller逻辑，也达到了网络层的代码复用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Controller / ViewModel ↓ 使用 Request 对象（构造 + 配置） Request (BaseRequest) ↓ 调用 RequestManager（封装 AFNetworking） RequestManager -&amp;gt; AFHTTPSessionManager ↓ 插件/拦截器（可选） Response / Error / Cache&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;把界面拼装到专门类中&lt;/h4&gt;
&lt;p&gt;新手喜欢在controller中把UILabel UIButton UITextfield 往&lt;code&gt;self.view&lt;/code&gt;上用&lt;code&gt;addsubView&lt;/code&gt;方法放。可以两种方法把代码从Controller剥离。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构建专门的UIView子类，来负责这些控件的拼装。不过稍微麻烦的是，你需要把这些控件的事件回调先接管，再一一暴露回controller&lt;/li&gt;
&lt;li&gt;用一个静态Util类，帮你做UIView的拼接工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法一比较适合大型的项目，需要统一样式与行为。而方案二适合于少量封装和小工程量代码&lt;/p&gt;
&lt;h4&gt;构造ViewModel&lt;/h4&gt;
&lt;p&gt;MVC不是不能用ViewModel？ MVVM的优点一样可以借鉴。具体来说就是吧ViewController给View传递数据这个过程，抽象为构建ViewModel的过程。&lt;/p&gt;
&lt;p&gt;ViewMode包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从service获取ViewModel&lt;/li&gt;
&lt;li&gt;把ViewModel交给View&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时在具体的实践中，我们可以创建构造专门的ViewModel工厂类。&lt;/p&gt;
&lt;p&gt;那么又要有人问了，这不是和MVVM一样了吗？其实不然。&lt;code&gt;MVC + ViewModel&lt;/code&gt;只是多了一个数据的格式化层，而真正的MVVM使用&lt;code&gt;ViewModel&lt;/code&gt;和双向绑定让View和VM自动同步。 核心区别有两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有没有绑定&lt;/li&gt;
&lt;li&gt;ViewModel的职责是否拓展到事件处理和状态管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正的MVVM中，&lt;code&gt;ViewModel&lt;/code&gt; 输出UI所需要的状态并处理用户输入，而&lt;code&gt;Controller&lt;/code&gt;只是负责把View的事件绑定到ViewModel的input，吧View的UI绑定到ViewModel的output。而MVC中的VM只是把Controller的逻辑挪出去而已，&lt;/p&gt;
&lt;h4&gt;专门构建储存类&lt;/h4&gt;
&lt;p&gt;storage专门做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地缓存&lt;/li&gt;
&lt;li&gt;数据迁移&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把所有本地数据读写、缓存策略、迁移逻辑集中在一个 Storage（或 Repository）层，Controller 和 ViewModel 只通过接口访问，不了解底层实现（NSUserDefaults / sqlite / Realm / 文件等）。符合单一职责原则&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;All in all：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过这些操作，Controller中最终只有两类代码： 生命周期和事件响应。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Controller 本身不写：网络、数据处理、UI 布局、业务逻辑、存储逻辑、字符串拼接、格式化。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;MVVM模式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MVVM&lt;/strong&gt;，则是由MVC演化来的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/f262e2dc0cc6de4dadee650bd4223ede.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;抽出&lt;code&gt;ViewMode&lt;/code&gt;层负责数据与视图的交互部分，它主要作用是拿到原始的数据，根据具体业务逻辑需要进行处理，之后将处理好的东西塞到View中去，其职责之一是静态模型，表示View显示自身所需的数据，这使View具有更清晰定义的任务，即呈现视图模型提供的数据，总结为一句话就是&lt;strong&gt;与View直接对应的Model，逻辑上是属于Model层&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Controller&lt;/code&gt;仅协调各部分的绑定关系以及逻辑处理，唯一关注的是使用来自ViewModel的数据配置和管理各种View，不需要了解web调用，model对象等，这些都交给ViewModel操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/173e53856579e8b7442dd7988d114682.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/cd664343385116fe844989e9a96bbe83.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ViewModel并不一定只服务一个View，同理，一个Controller也可以持有很多ViewModel，来实现不同的逻辑控制多重的View。&lt;/p&gt;
&lt;p&gt;Controller唯一关注的是使用来自ViewModel的数据配置和管理各种View，并让ViewModel知道何时发生需要更改上游数据的相关用户输入。 Controller不需要知道网络请求、数据库操作以及Model等，这样就让Controller更集中的去处理具体的业务逻辑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/5e3c5cbfbff3eafe4139bada682afc2b.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;模块层级图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/1fea43d51c15c930846949b5d4e026d1.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;MVVM&lt;/code&gt;中，View和ViewCotroller正式联系在一起，我们把他视为一个组件，他们都不能直接引用Model，而是应用视图模型ViewModel，虽然会轻微增加代码量，但是总体上减少了代码的复杂度。&lt;/p&gt;
&lt;p&gt;MVVM 的注意事项&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;view&lt;/code&gt; 引用&lt;code&gt;viewModel&lt;/code&gt; ，但反过来不行（即不要在&lt;code&gt;viewModel&lt;/code&gt;中引入&lt;code&gt;#import UIKit.h&lt;/code&gt;，任何视图本身的引用都不应该放在&lt;code&gt;viewModel&lt;/code&gt;中）（PS：&lt;strong&gt;必须满足&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewModel&lt;/code&gt; 引用&lt;code&gt;model&lt;/code&gt;，但反过来不行&lt;/li&gt;
&lt;li&gt;ViewController尽量不涉及业务逻辑，让&lt;code&gt;ViewModel&lt;/code&gt;去做这些事情&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大致实现以下效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/f6ee87327ca05d69a5f3992593d98774.png&quot; alt=&quot;图片一&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/a8fd064b319b34758954769b686a5192.png&quot; alt=&quot;图片二&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MVC&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/em&gt; 通用架构； 处理耦合度高的逻辑方便； &lt;em&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/em&gt; 耦合度高； 复用性差； 测试性差；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;MVVM&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/em&gt; 耦合度低； 复用性高； 测试性高； 层次更清晰； 重构成本低； &lt;em&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/em&gt; 处理耦合度高的逻辑比较复杂； 若加入RAC，增加学习成本； 一些Bug比较难调试&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MVVM 在使用当中，通常还会利用双向绑定技术，使得 Model 变化时，ViewModel 会自动更新，而 ViewModel 变化时，View 也会自动变化。所以，MVVM 模式有些时候又被称作：&lt;a href=&quot;https://en.wikipedia.org/wiki/Model_View_ViewModel&quot;&gt;model-view-binder&lt;/a&gt; 模式。&lt;/p&gt;
&lt;h3&gt;MVVM的问题&lt;/h3&gt;
&lt;p&gt;在实际使用中，确实能够使得Model层和View层解耦，但是如果实现MVVM中的双向绑定的话，就需要引入更多复杂的框架实现了。&lt;/p&gt;
&lt;p&gt;某种意义来说，就是数据绑定使MVVM变得复杂难用了。由于Obj - C 没有像Swift那样的原生数据绑定支持，我们需要手动实现数据绑定机制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KVO View可以监听ViewModel中属性的变化，并自动更新。但是比较难用容易出问题，最好还是用RAC封装&lt;/li&gt;
&lt;li&gt;通知中心（NSNotifiction）提供了一种松耦合的通信方式，ViewModel可以通过通知来告知View状态的变化。适用于有全局事件&lt;/li&gt;
&lt;li&gt;代理模式（Delegate）ViewModel可以定义协议，让View遵循协议并实现相应的方法来响应状态变化。&lt;/li&gt;
&lt;li&gt;block 最简单最小巧（黎果菲方案，组长评价 “很可以”）&lt;/li&gt;
&lt;li&gt;RAC第三方库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/290bbad88c65bf9644fa39df2e1508c8.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;block&lt;/h4&gt;
&lt;p&gt;block 的作用：保存一段代码，到恰当的时候调用,很多时候block是代理的一种优化方案&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;block比protocol更灵活，更高聚合，低耦合。 例如AFN的网络框架中，就可以将“准备请求参数”的代码和“处理后台返回数据”的代码放在一起。&lt;/li&gt;
&lt;li&gt;block的灵活还体现在他可以当作方法参数以及返回值。 Block可以作为函数参数或者函数的返回值，而其本身又可以带输入参数或返回值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;KVO&lt;/h4&gt;
&lt;p&gt;实现的方式中KVO不需要通知中心将可以实现属性的监听；与block以及代理相比，可以减少大量的代理方法以及block中的处理逻辑代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;anObserver：观察者对象，这个对象必须实现&lt;code&gt;observeValueForKeyPath:ofObject:change:context:&lt;/code&gt;方法，以响应属性的修改通知,否则将报错&lt;code&gt;An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;keyPath：被监听的属性。这个值不能为nil。&lt;/p&gt;
&lt;p&gt;options：监听选项，这个值可以是NSKeyValueObservingOptions选项的组合。关于监听选项，我们会在下面介绍。&lt;/p&gt;
&lt;p&gt;context：任意的额外数据，我们可以将这些数据作为上下文数据，它会传递给观察者对象的observeValueForKeyPath:ofObject:change:context:方法。这个参数的意义在于用于区分同一对象监听同一属性的多个不同的监听。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)dealloc {
    // 移除监听
    [self.webVIew removeObserver:self forKeyPath:@&quot;xxxx&quot;];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/154543600?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=154543600&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;KVO&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;ReactiveCocoa&lt;/h4&gt;
&lt;p&gt;他可以监听UI事件、文本变化、属性变化（不需要写KVO），代替代理/回调。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ReactiveCocoa&lt;/code&gt;（简称为&lt;code&gt;RAC&lt;/code&gt;）,是由&lt;code&gt;Github&lt;/code&gt;开源的一个应用于iOS和OS开发的新框架。&lt;code&gt;RAC&lt;/code&gt;结合了&lt;code&gt;函数式编程（Functional Programming）&lt;/code&gt;和&lt;code&gt;响应式编程（React Programming）&lt;/code&gt;的框架，也可称其为&lt;code&gt;函数响应式编程（FRP）&lt;/code&gt;框架 。
&lt;code&gt;函数响应式编程&lt;/code&gt;利用下图&lt;/p&gt;
&lt;p&gt;来解释最好不过了：&lt;code&gt;c = a + b&lt;/code&gt; 定义好后，当&lt;code&gt;a&lt;/code&gt;的值变化后，&lt;code&gt;c&lt;/code&gt;的值就会自动变化。不过&lt;code&gt;a&lt;/code&gt;的值变化时会产生一个信号，这个信号会通知&lt;code&gt;c&lt;/code&gt;根据&lt;code&gt;a&lt;/code&gt;变化的值来变化自己的值。&lt;code&gt;b&lt;/code&gt;的值变化同样也影响&lt;code&gt;c&lt;/code&gt;的值，这就是函数响应式编程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/28fcffbf5783d6625a16248a4339f6c0.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RAC是函数响应式编程（FRP）框架。ReactiveCocoa结合了几种编程风格：函数式编程（Functional Programming）响应式编程（Reactive Programming）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用RAC解决问题，就不需要考虑调用顺序。每一次操作都写成一系列嵌套的方法中，使代码高聚合，方便管理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/df1aeed2963ea7230aa170990957ea95.png&quot; alt=&quot;CleanShot 2025-12-09 at 19.35.15@2x&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;函数式编程（Functional Programming）和响应式编程（React Programming）也是当前很火的两个概念，它们的结合可以很方便地实现数据的绑定。于是，在 iOS 编程中，ReactiveCocoa 横空出世了，它的概念都非常 新，包括：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;函数式编程（Functional Programming），函数也变成一等公民了，可以拥有和对象同样的功能，例如当成参数传递，当作返回值等。看看 Swift 语言带来的众多函数式编程的特性，就你知道这多 Cool 了。&lt;/li&gt;
&lt;li&gt;响应式编程（React Programming），原来我们基于事件（Event）的处理方式都弱了，现在是基于输入（在 ReactiveCocoa 里叫 Signal）的处理方式。输入还可以通过函数式编程进行各种 Combine 或 Filter，尽显各种灵活的处理。&lt;/li&gt;
&lt;li&gt;无状态（Stateless），状态是函数的魔鬼，无状态使得函数能更好地测试。&lt;/li&gt;
&lt;li&gt;不可修改（Immutable），数据都是不可修改的，使得软件逻辑简单，也可以更好地测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于这个第三方库的具体内容笔者现在还没有具体了解，具体的使用以及源码的相关内容会在后续更新。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fraw.githubusercontent.com%2FBiscoffee%2Fpiccbes%2Fmaster%2F%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20251209150308_469_51.jpg&amp;amp;pos_id=img-YaVCR2KL-1765280989857&quot; alt=&quot;外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/155750158&quot;&gt;【iOS】MVVM&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】SDWebImage解析</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-155455710-ios-sdwebimage/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-155455710-ios-sdwebimage/</guid><description>笔者最近在完成抖音关注页面的仿写过程中了解到可以使用SDWebImage来进行头像加载的优化，当时只来得及了解其简单使用。现在有时间了，准备了解一下他的内部源码实现。 一、简介 SDWebImage是iOS中提供图片加载的第三方库，可以给UIKit框架中的控件比如 UIImage</description><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d6a3ab564ba1402e9956ff25014ea1f6.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;笔者最近在完成抖音关注页面的仿写过程中了解到可以使用SDWebImage来进行头像加载的优化，当时只来得及了解其简单使用。现在有时间了，准备了解一下他的内部源码实现。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/efae5bd58eac411e95ca0ced23284a21.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0dccb380229a45bc954d51771931886c.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;一、简介&lt;/h2&gt;
&lt;p&gt;SDWebImage是iOS中提供图片加载的第三方库，可以给UIKit框架中的控件比如&lt;code&gt;UIImageView和UIButton&lt;/code&gt;提供从网络上下载和缓存的图片。它的接口十分简洁，如果给&lt;strong&gt;UIImageView控件添加图片&lt;/strong&gt;可以使用如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[imageView sd_setImageWithURL:imageUrl placeholderImage:nil];//第一个参数是图片的URL第二个参数是占位图片加载失败时显示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;strong&gt;给UIButton&lt;/strong&gt; 添加图片可以使用以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[button sd_setImageWithURL:imageUrl forState:UIControlStateNormal placeholderImage:nil];//第一个参数是图片的URL，第二个参数是按钮状态，第三个参数是占位图片，加载失败时显示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SDWebImage还提供了其他方法， 但是我们通过查看源码发现，最终都是调用气&lt;strong&gt;全能方法&lt;/strong&gt;，全能方法还提供了占位图、可选项、加载进度和完成回调。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/094335e4e4c4463287ed05d13aea9382.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;占位图像：&lt;/strong&gt; 在网络图片加载过程中，ImageView会显示占位图像，给用户一个视觉上的暂时反馈。当网络图片加载完成后，ImageView中会显示加载完成的图片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选项：&lt;/strong&gt; 是一个枚举类型的值，用于设置加载图片的选项。其中包含了一些常用的选项，例如缓存策略、图片解码方式、加载优先级等。通过传递不同的选项，可以对图片加载的行为进行定制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进度：&lt;/strong&gt; 它们提供了对图片加载过程中的进度和结果的反馈。它们提供了对图片加载过程中的进度和结果的反馈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完成回调：&lt;/strong&gt; 是一个块对象，用于在图片加载完成后执行相应的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SDWebImage有下面一些常见的功能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过异步方式加载图片&lt;/li&gt;
&lt;li&gt;可以自动缓存到内存和磁盘中，并且可以自动清理过期的缓存&lt;/li&gt;
&lt;li&gt;支持多种的图片格式包括jpg、jepg、png等，同时还支持多种动图格式包括GIF、APNG等&lt;/li&gt;
&lt;li&gt;同一图片的URL不会重复下载&lt;/li&gt;
&lt;li&gt;对失效的图片URL不会重复尝试下载&lt;/li&gt;
&lt;li&gt;在子线程中进行操作，确保不会阻塞主线程&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、SDWebImage调用流程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c8da3ea7eecd480ab440824de9700582.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当使用&lt;code&gt;[imageView sd_setImageWithURL:imageUrl placeholderImage:nil];&lt;/code&gt;方法时，会执行UIImageView+WebCache类中的相应方法，当使用&lt;code&gt;[button sd_setImageWithURL:imageUrl forState:UIControlStateNormal placeholderImage:nil];&lt;/code&gt;方法时会执行UIBUtton+WebCache类中的相应方法，但是最后都会调用UIView+WebCache类中的&lt;code&gt;- (nullable id )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 {};&lt;/code&gt;方法。接着根据URL，通过SDWebImageManager的&lt;code&gt;loadImageWithURL:options:context:progress:completed:&lt;/code&gt;方法加载图片，接着通过sd_setImageLoadOperation方法将operation加入到SDOperationsDictionary中。然后调用&lt;code&gt;queryCacheOperationForKey&lt;/code&gt;方法进行查询图片缓存，通过查询内存和磁盘中是否有缓存，如果有则通过&lt;code&gt;SDImageCacheDelegate&lt;/code&gt;回调&lt;code&gt;imageCache:didFindImage:forkey:userInfo&lt;/code&gt;到&lt;code&gt;SDWebImageManager&lt;/code&gt;，然后其回调&lt;code&gt;webImageManager:didFinishWithImage:&lt;/code&gt;到&lt;code&gt;UIImageView + WebCache&lt;/code&gt;前短展示页面。&lt;/p&gt;
&lt;p&gt;根据URLKye在硬盘缓存目录尝试读取图片文件，这一步在NSOperation操作，回主线程结果回调&lt;code&gt;notifyDelegate:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果从硬盘读取到了图片，将图片添加到内存缓存中（如果空闲内存过小，会先清空内存缓存）。&lt;code&gt;SDImageCacheDelegate&lt;/code&gt;回调 &lt;code&gt;imageCache:didFindImage:forKey:userInfo:&lt;/code&gt;进而回调展示图片。&lt;/p&gt;
&lt;p&gt;如果从硬盘缓存目录读取不到图片，说明所有缓存都不存在该图片，需要下载图片，回调 &lt;code&gt;imageCache:didNotFindImageForKey:userInfo:&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;共享或重新生成一个下载器 &lt;code&gt;SDWebImageDownloader&lt;/code&gt; 开始下载图片。&lt;/p&gt;
&lt;p&gt;图片下载由 NSURLConnection (3.8.0 之后使用了 NSURLSession)，实现相关 delegate 来判断图片下载中、下载完成和下载失败。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;connection:didReceiveData:&lt;/code&gt;中利用 ImageIO 做了按图片下载进度加载效果。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;connectionDidFinishLoading:&lt;/code&gt; 数据下载完成后交给 &lt;code&gt;SDWebImageDecoder&lt;/code&gt;做图片解码处理。图片解码处理在一个 NSOperationQueue 完成，不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理，最好也在这里完成，效率会好很多。&lt;/p&gt;
&lt;p&gt;在主线程 &lt;code&gt;notifyDelegateOnMainThreadWithInfo:&lt;/code&gt;宣告解码完成，&lt;code&gt;imageDecoder:didFinishDecodingImage:userInfo:&lt;/code&gt;回调给 &lt;code&gt;SDWebImageDownloader&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;imageDownloader:didFinishWithImage:&lt;/code&gt; 回调给 &lt;code&gt;SDWebImageManager&lt;/code&gt;告知图片下载完成。&lt;/p&gt;
&lt;p&gt;通知所有的&lt;code&gt;downloadDelegates&lt;/code&gt;下载完成，回调给需要的地方展示图片。&lt;/p&gt;
&lt;p&gt;将图片保存到&lt;code&gt;SDImageCache&lt;/code&gt;中，内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成，避免拖慢主线程。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SDImageCache&lt;/code&gt; 在初始化的时候会注册一些消息通知，在内存警告或退到后台的时候清理内存图片缓存，应用结束的时候清理过期图片。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SDWebImagePrefetcher&lt;/code&gt;可以预先下载图片，方便后续使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/bcd7affe9c90408bb1244694ca5be142.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d0fbe483297147f8a610a7711c719478.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过进一步探索，我们可以找到他的&lt;strong&gt;具体实现：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (nullable id&amp;lt;SDWebImageOperation&amp;gt;)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 {

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won&apos;t
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    //  if url is NSString and shouldUseWeakMemoryCache is true, [cacheKeyForURL:context] will crash. just for a  global protect.
    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;
    }

    if (context) {
        // copy to avoid mutable object
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    self.sd_latestOperationKey = validOperationKey;
    if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidAutoCancelImage))) {
        // cancel previous loading for the same set-image operation key by default
        [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    }
    SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:validOperationKey];
    if (!loadState) {
        loadState = [SDWebImageLoadState new];
    }
    loadState.url = url;
    [self sd_setImageLoadState:loadState forKey:validOperationKey];

    SDWebImageManager *manager = context[SDWebImageContextCustomManager];
    if (!manager) {
        manager = [SDWebImageManager sharedManager];
    } else {
        // remove this manager to avoid retain cycle (manger -&amp;gt; loader -&amp;gt; operation -&amp;gt; context -&amp;gt; manager)
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextCustomManager] = nil;
        context = [mutableContext copy];
    }
    SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
    BOOL shouldUseWeakCache = NO;
    if ([manager.imageCache isKindOfClass:SDImageCache.class]) {
        shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;
    }
    if (!(options &amp;amp; SDWebImageDelayPlaceholder)) {
        if (shouldUseWeakCache) {
            NSString *key = [manager cacheKeyForURL:url context:context];
            // call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query
            // this unfortunately will cause twice memory cache query, but it&apos;s fast enough
            // in the future the weak cache feature may be re-design or removed
            [((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
        }
        [(queue ?: SDCallbackQueue.mainQueue) async:^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        }];
    }

    id &amp;lt;SDWebImageOperation&amp;gt; operation = nil;

    if (url) {
        // reset the progress
        NSProgress *imageProgress = loadState.progress;
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }

#if SD_UIKIT || SD_MAC
        // check and start image indicator
        [self sd_startImageIndicatorWithQueue:queue];
        id&amp;lt;SDWebImageIndicator&amp;gt; imageIndicator = self.sd_imageIndicator;
#endif

        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        @weakify(self);
        operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            @strongify(self);
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            if (imageProgress &amp;amp;&amp;amp; finished &amp;amp;&amp;amp; !error &amp;amp;&amp;amp; imageProgress.totalUnitCount == 0 &amp;amp;&amp;amp; imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }

#if SD_UIKIT || SD_MAC
            // check and stop image indicator
            if (finished) {
                [self sd_stopImageIndicatorWithQueue:queue];
            }
#endif

            BOOL shouldCallCompletedBlock = finished || (options &amp;amp; SDWebImageAvoidAutoSetImage);
            BOOL shouldNotSetImage = ((image &amp;amp;&amp;amp; (options &amp;amp; SDWebImageAvoidAutoSetImage)) ||
                                      (!image &amp;amp;&amp;amp; !(options &amp;amp; SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClosure = ^{
                if (!self) { return; }
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                if (completedBlock &amp;amp;&amp;amp; shouldCallCompletedBlock) {
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };

            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
            if (shouldNotSetImage) {
                [(queue ?: SDCallbackQueue.mainQueue) async:callCompletedBlockClosure];
                return;
            }

            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                targetImage = image;
                targetData = data;
            } else if (options &amp;amp; SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                targetImage = placeholder;
                targetData = nil;
            }

#if SD_UIKIT || SD_MAC
            // check whether we should use the image transition
            SDWebImageTransition *transition = nil;
            BOOL shouldUseTransition = NO;
            if (options &amp;amp; SDWebImageForceTransition) {
                // Always
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) {
                // From network
                shouldUseTransition = YES;
            } else {
                // From disk (and, user don&apos;t use sync query)
                if (cacheType == SDImageCacheTypeMemory) {
                    shouldUseTransition = NO;
                } else if (cacheType == SDImageCacheTypeDisk) {
                    if (options &amp;amp; SDWebImageQueryMemoryDataSync || options &amp;amp; SDWebImageQueryDiskDataSync) {
                        shouldUseTransition = NO;
                    } else {
                        shouldUseTransition = YES;
                    }
                } else {
                    // Not valid cache type, fallback
                    shouldUseTransition = NO;
                }
            }
            if (finished &amp;amp;&amp;amp; shouldUseTransition) {
                transition = self.sd_imageTransition;
            }
#endif
            [(queue ?: SDCallbackQueue.mainQueue) async:^{
#if SD_UIKIT || SD_MAC
                [self sd_setImage:targetImage imageData:targetData options:options basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL callback:callCompletedBlockClosure];
#else
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
                callCompletedBlockClosure();
#endif
            }];
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if SD_UIKIT || SD_MAC
        [self sd_stopImageIndicatorWithQueue:queue];
#endif
        if (completedBlock) {
            [(queue ?: SDCallbackQueue.mainQueue) async:^{
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @&quot;Image url is nil&quot;}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }];
        }
    }

    return operation;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法&lt;strong&gt;简要流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;讲&lt;code&gt;SDWebImageContext&lt;/code&gt; 复制并转换为&lt;code&gt;immutable&lt;/code&gt;，获取其中的&lt;code&gt;validOperationkey&lt;/code&gt;值作为校验值id，默认值为当前view的类名&lt;/li&gt;
&lt;li&gt;执行 sd_cancelImageLoadOperationWithKey 取消上一次任务，保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突；&lt;/li&gt;
&lt;li&gt;设置占位图；&lt;/li&gt;
&lt;li&gt;初始化 SDWebImageManager 、SDImageLoaderProgressBlock , 重置 NSProgress、SDWebImageIndicator;&lt;/li&gt;
&lt;li&gt;开启下载loadImageWithURL: 并将返回的 SDWebImageOperation 存入 sd_operationDictionary，key 为 validOperationKey;&lt;/li&gt;
&lt;li&gt;取到图片后，调用 sd_setImage: 同时为新的 image 添加 Transition 过渡动画；&lt;/li&gt;
&lt;li&gt;动画结束后停止 indicator。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体的代码可以参考上文代码块⬆️&lt;/p&gt;
&lt;h3&gt;取消正在进行的异步下载&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 生成一个有效的操作密钥
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    // 如果传入了参数就用传入的，否则就用当前类的类名
    if (!validOperationKey) {
        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
// 取消先前下载的任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

... // 下载图片操作

// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        // 从队列中取消正在进行的下载程序

		// 获取添加在UIView的自定义属性
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id&amp;lt;SDWebImageOperation&amp;gt; operation;

        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
        // 实现了SDWebImageOperation的协议
            if ([operation respondsToSelector:@selector(cancel)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- (void)sd_setImageLoadOperation:(nullable id&amp;lt;SDWebImageOperation&amp;gt;)operation forKey:(nullable NSString *)key {
    if (key) {
    // 如果之前已经有过该图片的下载操作,则取消之前的图片下载操作
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里引用学长的一段话：&lt;/p&gt;
&lt;p&gt;实际上，所有的操作都是由一个实际上，所有的操作都是由一个operationDictionary字典维护的，执行新的操作之前，cancel所有的operation。&lt;/p&gt;
&lt;p&gt;通俗来说：这两个方法的组合使用，可以实现对图片加载操作的管理。&lt;strong&gt;在设置新的图片加载操作之前，会先取消之前的操作，从而确保每个视图只执行最新的图片加载操作。&lt;/strong&gt; 这样可以**避免出现重复加载或并发加载的问题，**同时也提高了图片加载的效率和用户体验。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重复加载：&lt;/strong&gt; 指的是在同一时间点或短时间内多次加载相同资源的情况。这可能是由于代码逻辑错误、用户操作或系统问题导致的。重复加载可能会造成资源浪费、性能下降和不必要的网络请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发加载：&lt;/strong&gt; 指的是在同一时间点或短时间内同时进行多个加载操作的情况。这种情况通常发生在多线程或并发执行的环境下，多个加载操作可以同时进行，以提高效率和响应性。但是必须确保每个加载操作独立处理自己的资源，否则会出现数据竞争和冲突。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载图片&lt;/h3&gt;
&lt;p&gt;这个操作是由一个SDWebImageManager完成的，是一个单例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                 completed:(nonnull SDInternalCompletionBlock)completedBlock;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际使用中并不直接使用SDWebImageDownloader和SDImageCache类对图片进行下载和存储，而是使用SDWebImageManager来管理。包括平常使用UIImageView+WebCache等控件的分类，都是使用SDWebImageManager来处理，该对象内部定义了一个图片下载器（SDWebImageDownloader）和图片缓存（SDImageCache）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface SDWebImageManager : NSObject

@property (weak, nonatomic) id &amp;lt;SDWebImageManagerDelegate&amp;gt; delegate;

@property (strong, nonatomic, readonly) SDImageCache *imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;

...

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他的几个核心api：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 初始化SDWebImageManager单例，在init方法中已经初始化了cache单例和downloader单例。
- (nonnull instancetype)initWithCache:(nonnull id&amp;lt;SDImageCache&amp;gt;)cache loader:(nonnull id&amp;lt;SDImageLoader&amp;gt;)loader NS_DESIGNATED_INITIALIZER;
// 下载图片
- (id )downloadImageWithURL:(NSURL *)url
                    options:(SDWebImageOptions)options
                   progress:(SDWebImageDownloaderProgressBlock)progressBlock
                  completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
// 缓存给定URL的图片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
// 取消当前所有的操作
- (void)cancelAll;
// 监测当前是否有进行中的操作
- (BOOL)isRunning;
// 监测图片是否在缓存中， 先在memory cache里面找  再到disk cache里面找
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
// 监测图片是否缓存在disk里
- (BOOL)diskImageExistsForURL:(NSURL *)url;
// 监测图片是否在缓存中,监测结束后调用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
// 监测图片是否缓存在disk里,监测结束后调用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回给定URL的cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载图片的主要过程，也是这几个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                 completed:(nonnull SDInternalCompletionBlock)completedBlock;
### SDWebImageDownloader


具体的网络操作由`SDWebImageDownloaderOperation`对象处理，图片的下载是放在一个`NSOperationQueue`完成的


```objective-c
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况的最大并发数是6，如果需要我们可以通过&lt;code&gt;maxConcurrentDownloads&lt;/code&gt;修改&lt;/p&gt;
&lt;p&gt;通过观察&lt;code&gt;- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock&lt;/code&gt;的代码，我们可以看出来，&lt;code&gt;SDWebImageDownloader把operation&lt;/code&gt;加到队列中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 将操作对象添加到操作队列中
[self.downloadQueue addOperation:operation];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两行代码是&lt;code&gt;SDWebImageDownloader&lt;/code&gt;开启下载任务的 关键 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成下载任务的Operation操作；&lt;/li&gt;
&lt;li&gt;将Operation操作加入到队列中，实现异步任务下载；&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;SDWebImageOptions&lt;/h4&gt;
&lt;p&gt;参数列表中有一项&lt;code&gt;SDWebImageOptions&lt;/code&gt;，这.h文件中定义了一个可以暴露在外的选择方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {

    // 默认情况下，当URL下载失败时，URL会被列入黑名单，导致库不会再去重试，该标记用于禁用黑名单
    SDWebImageRetryFailed = 1 &amp;lt;&amp;lt; 0,

    // 默认情况下，图片下载开始于UI交互，该标记禁用这一特性，这样下载延迟到UIScrollView减速时
    SDWebImageLowPriority = 1 &amp;lt;&amp;lt; 1,

    // 该标记禁用磁盘缓存
    SDWebImageCacheMemoryOnly = 1 &amp;lt;&amp;lt; 2,

    // 该标记启用渐进式下载，图片在下载过程中是渐渐显示的，如同浏览器一下。
    // 默认情况下，图像在下载完成后一次性显示
    SDWebImageProgressiveDownload = 1 &amp;lt;&amp;lt; 3,

    // 即使图片缓存了，也期望HTTP响应cache control，并在需要的情况下从远程刷新图片。
    // 磁盘缓存将被NSURLCache处理而不是SDWebImage，因为SDWebImage会导致轻微的性能下载。
    // 该标记帮助处理在相同请求URL后面改变的图片。如果缓存图片被刷新，则完成block会使用缓存图片调用一次
    // 然后再用最终图片调用一次
    SDWebImageRefreshCached = 1 &amp;lt;&amp;lt; 4,

    // 在iOS 4+系统中，当程序进入后台后继续下载图片。这将要求系统给予额外的时间让请求完成
    // 如果后台任务超时，则操作被取消
    SDWebImageContinueInBackground = 1 &amp;lt;&amp;lt; 5,

    // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES;来处理存储在NSHTTPCookieStore中的cookie
    SDWebImageHandleCookies = 1 &amp;lt;&amp;lt; 6,

    // 允许不受信任的SSL认证
    SDWebImageAllowInvalidSSLCertificates = 1 &amp;lt;&amp;lt; 7,

    // 默认情况下，图片下载按入队的顺序来执行。该标记将其移到队列的前面，
    // 以便图片能立即下载而不是等到当前队列被加载
    SDWebImageHighPriority = 1 &amp;lt;&amp;lt; 8,

    // 默认情况下，占位图片在加载图片的同时被加载。该标记延迟占位图片的加载直到图片已以被加载完成
    SDWebImageDelayPlaceholder = 1 &amp;lt;&amp;lt; 9,

    // 通常我们不调用动画图片的transformDownloadedImage代理方法，因为大多数转换代码可以管理它。
    // 使用这个票房则不任何情况下都进行转换。
    SDWebImageTransformAnimatedImage = 1 &amp;lt;&amp;lt; 10,
};
### SDImageCache


SDWebImage.h这个类在源代码有这样一个注释，翻译过来大概就是


&amp;gt; 它维护了一个内存缓存和一个可选的磁盘缓存


磁盘缓存写入操作是异步执行的，因此不会给 UI 增加不必要的延迟。这个类还有个私有类`SDMemoryCache`，它继承自自NSCache，负责管理图像的内存缓存，其内部就是将`NSCache` 扩展为了`SDMemoryCache` 协议，并为iOS/tvOS平台添加了`@property (nonatomic, strong, nonnull) NSMapTable  *weakCache; // strong-weak cache，`并为其添加了信号量锁来保证线程安全。

weakCache 的作用在于恢复缓存，它通过 `CacheConfig 的 shouldUseWeakMemoryCache`开关以控制。


```objective-c
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法将图像异步缓存到指定密钥下的内存中，可以选择是否缓存到磁盘中，并且可以直接将图像的数据保存到磁盘中，就不必再通过将原图像编码后获取图像的数据再保存到磁盘中，以节省硬件资源.&lt;/p&gt;
&lt;p&gt;刚才我们可能注意到，下载的方法中都有一个&lt;code&gt;SDImageType&lt;/code&gt;的参数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c26bb043391c4a0bbd51bf49c699e7f3.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//定义Cache类型
typedef NS_ENUM(NSInteger, SDImageCacheType) {
//不使用cache获得图片,依然会从web下载图片
    SDImageCacheTypeNone,
//图片从disk获得
    SDImageCacheTypeDisk,
//图片从Memory中获得
    SDImageCacheTypeMemory
};
#### 设置最大缓存和时间设置


```objective-c
SDImageCache类的源码
//这个变量默认值为YES，显示比较高质量的图片，但是会浪费比较多的内存，可以通过设置NO来缓解内存
@property (assign, nonatomic) BOOL shouldDecompressImages;
//总共的内存允许图片的消耗值
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//图片存活于内存的时间初始化的时候默认为一周
@property (assign, nonatomic) NSInteger maxCacheAge;
//每次存储图片大小的限制
@property (assign, nonatomic) NSUInteger maxCacheSize;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还可以设置maxCache&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager.imageCache setMaxMemoryCost:1000000];//设置总缓存大小，默认为0没有限制
[manager.imageCache setMaxCacheSize:640000];//设置单个图片限制大小
[manager.imageDownloader setMaxConcurrentDownloads:1];//设置同时下载线程数，这是下载器的内容，下面将会介绍
[manager downloadImageWithURL:[NSURL URLWithString:@&quot;http://p9.qhimg.com/t01eb74a44c2eb43193.jpg&quot;]
                      options:SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                          NSLog(@&quot;%lu&quot;, receivedSize);
                      } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                          self.imageView1.image = image;

                      }];
[manager downloadImageWithURL:[NSURL URLWithString:@&quot;http://img.article.pchome.net/00/28/33/87/pic_lib/wm/kuanpin12.jpg&quot;]
                      options:SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                          NSLog(@&quot;%lu&quot;, receivedSize);
                      } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                          self.imageView2.image = image;

                      }];
NSUInteger size = [manager.imageCache getSize];
NSUInteger count = [manager.imageCache getDiskCount];
NSLog(@&quot;size = %lu&quot;, size); // 644621（两张测试图片）
NSLog(@&quot;count = %lu&quot;, count); // 2
[manager.imageCache clearDisk];
size = [manager.imageCache getSize];
count = [manager.imageCache getDiskCount];
NSLog(@&quot;sizeClean = %lu&quot;, size);  //  0
NSLog(@&quot;countClean = %lu&quot;, count);     //  0   这里使用的是clear
#### 三种缓存（内存图片缓存、磁盘图片缓存、内存操作缓存）


- 先查看内存图片缓存，内存图片缓存没有，后生成操作，查看磁盘图片缓存
- 磁盘图片缓存有，就加载到内存缓存，没有就下载图片
- 在建立下载操作之前，判断下载操作是否存在
- 默认情况下，下载的图片数据会同时缓存到内存和磁盘中

**1、内存缓存及磁盘缓存**


- **内存缓存**的处理由NSCache对象实现，NSCache类似一个集合的容器，它存储key-value对，类似于nsdictionary类，我们通常使用缓存来临时存储短时间使用但创建昂贵的对象，重用这些对象可以优化新能，同时这些对象对于程序来说不是紧要的，如果内存紧张就会自动释放。
- **磁盘缓存**的处理使用NSFileManager对象实现，图片存储的位置位于cache文件夹，另外SDImageCache还定义了一个串行队列来异步存储图片。
- SDImageCache提供了大量方法来缓存、获取、移除及清空图片。对于图片的索引，我们通过一个key来索引，在内存中，我们将其作为NSCache的key值，而在磁盘中，我们用这个key值作为图片的文件名，对于一个远程下载的图片其url实作为这个key的最佳选择。

**2、存储图片**

先在内存中放置一份缓存，如果需要缓存到磁盘，将磁盘缓存操作作为一个task放到串行队列中处理，会先检查图片格式是jpeg还是png，将其转换为响应的图片数据，最后吧数据写入磁盘中（文件名是对key值做MD5后的串）


**3、查询图片**

内存和磁盘查询图片API：


```objective-c
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看本地是否存在指定key的图片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一些其他的常见接口和属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;（1）-getSize  ：获得硬盘缓存的大小

（2）-getDiskCount ： 获得硬盘缓存的图片数量

（3）-clearMemory  ： 清理所有内存图片

（4）- removeImageForKey:(NSString *)key  系列的方法 ： 从内存、硬盘按要求指定清除图片

（5）maxMemoryCost  ：  保存在存储器中像素的总和

（6）maxCacheSize  ：  最大缓存大小 以字节为单位。默认没有设置，也就是为0，而清理磁盘缓存的先决条件为self.maxCacheSize &amp;gt; 0，所以0表示无限制。

（7）maxCacheAge ： 在内存缓存保留的最长时间以秒为单位计算，默认是一周
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/63e2f4225992443a9bddc428a731795c.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SDWebImage的大部分工作是由缓存对象SDImageCache和异步下载器管理对象SDWebImageManager来完成的。&lt;/li&gt;
&lt;li&gt;SDWebImage的图片下载是由SDWebImageDownloader这个类来实现的，它是一个异步下载管理器，下载过程中增加了对图片加载做了优化的处理。而真正实现图片下载的是自定义的一个Operation操作，将该操作加入到下载管理器的操作队列downloadQueue中，Operation操作依赖系统提供的NSURLConnection类实现图片的下载。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、小结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SDWebImage&lt;/code&gt;通过了一系列复杂的封装，把一个繁琐的过程封装成简单的接口来使用，实现了并发的图片下载及解压缩操作以及内存与磁盘存储等复杂功能&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/17c75ff59ba443e38743f924a79dbd9d.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/155455710&quot;&gt;【iOS】SDWebImage解析&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】数据持久化</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-155421618-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-155421618-ios-/</guid><description>根据要存储的数据大小、存储数据以及存储类型，存储方式主要分为一下几种： Plist（属性列表），不能存放自定义对象 Preference（偏好设置/NSUserDefaults） NSCoding（NSKeyedArchiver/NSKeyedUnarchiver，归档/解档） </description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;根据要存储的数据大小、存储数据以及存储类型，存储方式主要分为一下几种：&lt;/p&gt;
&lt;p&gt;Plist（属性列表），不能存放自定义对象
Preference（偏好设置/NSUserDefaults）
NSCoding（NSKeyedArchiver/NSKeyedUnarchiver，归档/解档）
SQLite3
Core Data（面向对象）
FMDB&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/5d3a830352454902a40240a0d48225fe.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;沙盒机制&lt;/h2&gt;
&lt;p&gt;沙盒机制是iOS APP特有的一种机制：&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html&quot;&gt;https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;iOS程序默认情况下只能访问自己的目录，这个目录被称为 “沙盒”&lt;/p&gt;
&lt;p&gt;沙盒其实就是每一个iOS App特有的一个文件夹，每个iOS App都有自己的应用沙盒（文件系统目录），其结构和目录特性都是一样的&lt;/p&gt;
&lt;p&gt;沙盒目录与其他文件系统隔离，应用必须呆在自己的沙盒里，其他应用不能访问该沙盒&lt;/p&gt;
&lt;p&gt;简言之，应用只能访问&lt;strong&gt;自己应用&lt;/strong&gt;下的文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString* path = NSHomeDirectory();
NSLog(@&quot;%@&quot;, path);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**NSHomeDictionary **可以直接得到该沙盒路径。&lt;/p&gt;
&lt;h3&gt;沙盒目录特性&lt;/h3&gt;
&lt;p&gt;沙盒中每个文件夹都有各自的特性，所以在选择存放目录时，一定要选择合适的目录&lt;/p&gt;
&lt;p&gt;应用程序包： 除沙盒目录之外，每一个App还有一个**Bundle目录，**即 “应用程序包（Application）”，该目录下存放的是应用程序的源文件，包括资源文件和可执行文件，上架前经过数字签名，上架后不可修改。获取Bundle路径的方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString* path = [[NSBundle mainBundle] bundlePath];
NSLog(@&quot;%@&quot;, path);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要借用某个App的图标或贴图，可以在&lt;strong&gt;该App中的程序应用包&lt;/strong&gt;中找到.app结尾的源文件，然后右键点击显示包内容即可直接获取到其所有的图标和贴图&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Documents：&lt;/strong&gt; 保存应用运行时生成的需要持久化的数据，iTunes同步该应用时会同步该文件夹中的内容，适合存储重要数据。获取该文件路径的方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString* path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSLog(@&quot;%@&quot;, path);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Library/Caches：&lt;/strong&gt; iTunes同步该应用时不会同步该文件夹中的内容，适合存储体积大、无需备份的非重要文件。比如网络数据缓存就会存储到cache文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//获取Library：NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject
NSString* path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSLog(@&quot;%@&quot;, path);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**Library/Preferences:  **iTunes同步该应用时会同步此文件夹的内容，通常保存应用的偏好设置，使用
NSUserDefaults
类来获取和设置应用的偏好
&lt;strong&gt;tmp： i&lt;/strong&gt;Tunes不会同步此文件夹，此目录用于存放临时数据，使⽤完毕后相应的文件会从该目录删除，保存应用程序再次启动过程中不需要的信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString* path = NSTemporaryDirectory();
NSLog(@&quot;%@&quot;, path);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;NSSearchPathForDirectoriesInDomains&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9f01be65de9147bea48419c69f2e7312.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;directory 表明我们要搜索的目录名称，比如NSDocumentDirectory搜索Documents目录、NSCachesDirectory搜索Library/Caches目录&lt;/p&gt;
&lt;p&gt;domainMask 指定搜索范围，NSUserDomainMask表示搜索范围限制在当前应用的沙盒目录，还有NSLocalDomainMask（表示/Library）、NSNetworkDomainMask（表示/Network）&lt;/p&gt;
&lt;p&gt;expandTilde BOOL值，表示是否展开波浪线。
比如该值为YES表示路径写成全写形式：/Users/Username/Library/Developer/CoreSimulator/Devices/8D71115A-D081-4440-9C94-13BD102412DB/data/Containers/Data/Application/D53B8C34-A16B-4A3D-9931-001D06F0C51F/Library/Caches
该值为NO表示路径写成：~/Library/Caches&lt;/p&gt;
&lt;h2&gt;NSUserDefaults&lt;/h2&gt;
&lt;p&gt;用于存储用户的偏好设置和用户信息,如用户名,是否自动登录,字体大小等.
数据自动保存在沙盒的Libarary/Preferences目录下.
NSUserDefaults将输入的数据储存在.plist格式的文件下,这种存储方式就决定了它的安全性几乎为0,所以不建议存储一些敏感信息如:用户密码,token,加密私钥等!
它能存储的数据类型为:NSNumber（NSInteger、float、double），NSString，NSDate，NSArray，NSDictionary，BOOL.
不支持自定义对象的存储.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
    [userDefault setInteger:1 forKey:@&quot;integer&quot;];
    [userDefault setBool:YES forKey:@&quot;BOOl&quot;];
    [userDefault setFloat:6.5 forKey:@&quot;float&quot;];
    [userDefault setObject:@&quot;123&quot; forKey:@&quot;numberString&quot;];
    NSString *numberString = [userDefault objectForKey:@&quot;numberString&quot;];
    BOOL myBool = [userDefault boolForKey:@&quot;BOOl&quot;];
    [userDefault removeObjectForKey:@&quot;float&quot;];
    [userDefault synchronize];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时我们需要注意：&lt;/p&gt;
&lt;p&gt;NSUserDefaults存储的数据都是不可变的,想将可变数据存入需要先转为不可变才可以存储.
NSUserDefaults是定时把缓存中的数据写入磁盘的，而不是即时写入，为了防止在写完NSUserDefaults后程序退出导致的数据丢失，可以在写入数据后使用synchronize强制立即将数据写入磁盘.&lt;/p&gt;
&lt;h2&gt;plist&lt;/h2&gt;
&lt;p&gt;属性列表是一种XML格式的文件，拓展名为plist如果对是NSString、NSDictionary、NSArray、NSData、NSNumber等类型，就可以使用writeToFile:atomically:方法直接将对象写到属性列表文件中。它是一种用来储存串行化后的对象的文件，用来储存程序中经常用到且数据量较小不轻易改动的数据。&lt;/p&gt;
&lt;p&gt;属性列表-归档NSDictionary将一个NSDictionary对象归档到一个plist属性列表中&lt;/p&gt;
&lt;p&gt;下图为属性列表-NSDictionary的存储和读取过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c4532c497a9f4efab7e5a1a99d11931b.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 将数据封装成字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@&quot;张三&quot; forKey:@&quot;name&quot;];
[dict setObject:@&quot;155xxxxxxx&quot; forKey:@&quot;phone&quot;];
[dict setObject:@&quot;27&quot; forKey:@&quot;age&quot;];
// 将字典持久化到Documents/stu.plist文件中
[dict writeToFile:path atomically:YES];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 读取Documents/stu.plist的内容，实例化
NSDictionaryNSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSLog(@&quot;name:%@&quot;, [dict objectForKey:@&quot;name&quot;]);
NSLog(@&quot;phone:%@&quot;, [dict objectForKey:@&quot;phone&quot;]);
NSLog(@&quot;age:%@&quot;, [dict objectForKey:@&quot;age&quot;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;偏好设置&lt;/h2&gt;
&lt;p&gt;很多iOS应用都支持偏好设置，比如保存用户名、密码、字体大小等设置，iOS提供了一套标准的解决方案来为应用加入偏好设置功能。
每个应用都有个NSUserDefaults实例，通过它来存取偏好设置。&lt;/p&gt;
&lt;p&gt;比如，保存用户名、字体、是否自动登录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@&quot;张三&quot; forKey:@&quot;username&quot;];
[defaults setFloat:18.0f forKey:@&quot;text_size&quot;];
[defaults setBool:YES forKey:@&quot;auto_login&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读取上次设置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@&quot;张三&quot; forKey:@&quot;username&quot;];
[defaults setFloat:18.0f forKey:@&quot;text_size&quot;];
[defaults setBool:YES forKey:@&quot;auto_login&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; UserDefaults设置数据时，不是立即写入，而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题，可以通过调用synchornize方法&lt;code&gt;[defaults synchornize];&lt;/code&gt;强制写入。&lt;/p&gt;
&lt;h2&gt;Keychain&lt;/h2&gt;
&lt;p&gt;很多时候，我们都需要将敏感数据(password, accessToken, secretKey等)存储到本地。为了能安全的在本地存储敏感信息，我们应当使用苹果提供的&lt;code&gt;KeyChain&lt;/code&gt;服务。他的数据储存在硬盘上，删除了应用，保存的数据还在。&lt;/p&gt;
&lt;p&gt;这里引用一篇写的很好的博客：&lt;a href=&quot;https://www.cnblogs.com/m4abcd/p/5242254.html&quot;&gt;https://www.cnblogs.com/m4abcd/p/5242254.html&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;归档与解档&lt;/h2&gt;
&lt;p&gt;在iOS中，对象的序列化和反序列化分别使用&lt;strong&gt;NSKeyedArchiver和NSKeyedUnarchiver&lt;/strong&gt;两个类，我们可以把一个类对象进行序列化然后保存到文件中，使用时再读取文件，把内容反序列化出来。这个过程通常也被称为 **
对象的编码（归档）和解码（解档）
**&lt;/p&gt;
&lt;p&gt;**归档 **— 将对象以文件（二进制数据）的形式保存到磁盘上中（也称序列化，持久化）
**解档 **— 使用时从磁盘上读取该文件的保存路径，从而读取文件的内容（也称反序列化）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;归档一般保存自定义对象、自定义对象数组，由于自定义对象不具有归档的性质，所以只有遵循了
NSCoding协议
的类才可以归档（NSCodong：即苹果规定的序列化接口）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于决大多数支持存储数据的Foundation和Cocoa Touch类都遵循了NSCoding协议，因此，对于大多数OC提供的类来说，归档相对而言还是比较容易实现的。&lt;/p&gt;
&lt;p&gt;对象归档的文件是&lt;strong&gt;保密&lt;/strong&gt;的，在磁盘上无法查看文件中的内容，而属性列表是明文的，可以查看。通过文件归档产生的文件是不可见的，如果打开归档文件的话，内容是乱码的；ta不同于属性列表和plist文件是可见的，正因为不可见的缘故，使得&lt;strong&gt;这种持久性的数据保存更有可靠性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NSCoding 协议有2个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;encodeWithCoder&lt;/code&gt;: 每次归档对象时，都会调用这个方法。一般在这个方法里面指定如何归档对象中的每个实例变量，可以使用encodeObject:forKey:方法归档实例变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initWithCoder&lt;/code&gt;: 每次从文件中恢复(解码)对象时，都会调用这个方法。一般在这个方法里面指定如何解码文件中的数据为对象的实例变量，可以使用decodeObject:forKey方法解码实例变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;自定义对象的单个对象的解档与归档&lt;/h3&gt;
&lt;p&gt;iOS13中只有支持**NSSecureCoding协议（父协议为NSCoding）**才能支持归档&lt;/p&gt;
&lt;p&gt;1、自定义一个Person类病实现 NSCoding协议的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Person : NSObject &amp;lt;NSSecureCoding&amp;gt;

@property (nonatomic, copy)NSString* name;
@property (nonatomic, assign)int age;
@property (nonatomic, assign)double weight;

@end

@implementation Person

//NSCoder是一个抽象类
//归档的协议方法
//将归档对象序列化
- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject: self.name forKey: @&quot;name&quot;];
    [coder encodeInt: self.age forKey: @&quot;age&quot;];
    [coder encodeDouble: self.weight forKey: @&quot;weight&quot;];
}

//解档的协议方法
//将解档对象反序列化
- (instancetype)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        self.name = [coder decodeObjectForKey: @&quot;name&quot;];
        self.age = [coder decodeIntForKey: @&quot;age&quot;];
        self.weight = [coder decodeDoubleForKey: @&quot;weight&quot;];
    }

    return self;
}

@end

//NSSecureCoding的协议方法
+ (BOOL)supportsSecureCoding {
    return YES;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、初始化待归档对象病进行归档&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(nullable NSData *)archivedDataWithRootObject:(id)object requiringSecureCoding:(BOOL)requiresSecureCoding error:(NSError **)error;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Person* person = [[Person alloc] init];
        person.name = @&quot;XY&quot;;
        person.age = 20;
        person.weight = 125.0;

        //归档成二进制数据流
        NSError* error;
        NSData* data1 = [NSKeyedArchiver archivedDataWithRootObject: person requiringSecureCoding: YES error: &amp;amp;error];
        if (error) {
            NSLog(@&quot;归档错误：%@&quot;, error);
            return 0;
        }
        //写入指定路径（一般写入到沙盒，这里方便演示存到一个新的文件夹）
        [data1 writeToFile: @&quot;/Users/jakey/Desktop/CS/Xcode/NSKeyedArchiverTest/test.archiver&quot; atomically: YES];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存后的文件一般无法打开，我们通过终端打开后，发现内容是经过加密的，正好对应前文。&lt;/p&gt;
&lt;p&gt;3、开始解档&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(nullable id)unarchivedObjectOfClass:(Class)cls fromData:(NSData *)data error:(NSError **)error;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//解档此二进制数据
        error = nil;
        NSData* data2 = [NSData dataWithContentsOfFile: @&quot;/Users/jakey/Desktop/CS/Xcode/NSKeyedArchiverTest/test.archiver&quot;];
        Person* unarchiverPerson = (Person *)[NSKeyedUnarchiver unarchivedObjectOfClass: [Person class] fromData: data2 error: &amp;amp;error];
        if (error) {
            NSLog(@&quot;解档错误：%@&quot;, error);
        }
        NSLog(@&quot;unarchiverPerson：%@&quot;, unarchiverPerson);
### 多个对象解档归档


1、初始化待归档对象并归档


```objective-c
Person* person1 = [[Person alloc] init];
person1.name = @&quot;XY&quot;;
person1.age = 20;
person1.weight = 125.0;
Dog* dog1 = [[Dog alloc] init];
dog1.name = @&quot;Bruce&quot;;
person1.dog = dog1;

Person* person2 = [[Person alloc] init];
person2.name = @&quot;Jacky&quot;;
person2.age = 21;
person2.weight = 130.0;
Dog* dog2 = [[Dog alloc] init];
dog2.name = @&quot;Oudy&quot;;
person2.dog = dog2;

//创建归档对象
NSKeyedArchiver* archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding: NO];

//进行归档（编码）操作
[archiver encodeObject: person1 forKey: @&quot;personOne&quot;];
[archiver encodeObject: person2 forKey: @&quot;personTwo&quot;];

//将归档（序列化）后的数据写入指定文件中
[archiver.encodedData writeToFile: @&quot;/Users/jakey/Desktop/CS/Xcode/NSKeyedArchiverTest/test.archiver&quot; atomically: YES];

//结束归档
[archiver finishEncoding];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、依次解档&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//解档
NSData* data = [NSData dataWithContentsOfFile: @&quot;/Users/jakey/Desktop/CS/Xcode/NSKeyedArchiverTest/test.archiver&quot;];
NSKeyedUnarchiver* unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData: data error: nil];
unarchiver.requiresSecureCoding = NO;

Person* unchiverPerson1 = [unarchiver decodeObjectForKey: @&quot;personOne&quot;];
NSLog(@&quot;%@ %d %lf %@&quot;, unchiverPerson1.name, unchiverPerson1.age, unchiverPerson1.weight, unchiverPerson1.dog.name);
Person* unchiverPerson2 = [unarchiver decodeObjectForKey: @&quot;personTwo&quot;];
NSLog(@&quot;%@ %d %lf %@&quot;, unchiverPerson2.name, unchiverPerson2.age, unchiverPerson2.weight, unchiverPerson2.dog.name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/4d3809bc4cdd4e0583bf875af8eeaf18.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;FMDB&lt;/h2&gt;
&lt;p&gt;这是一个轻量化数据库，也是对iOS相关SQLite的API进行封装的第三方库，使用者只需要调取该框架的API就能很方便对本地数据库进行操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用起来更为面向对象&lt;/li&gt;
&lt;li&gt;提高多线程安全，有效防止数据混乱（原来的SQLite线程不是安全的）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;失去的SQLite原有的跨平台性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用cocoapods导入FMDB库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/827808c6a9a84905a48eaa7d77517e15.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;FMDB一般涉及以下三个核心类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FMDataBase&lt;/strong&gt;：代表一个单一的SQLite数据库，也有许多执行SQLite语句的语法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FMResult&lt;/strong&gt;：结果集，使用FMDataBase执行SQLite查询语句后的结果集&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FMDatabaseQueue（数据库队列）&lt;/strong&gt;：在多个线程来执行查询和更新时会使用这个类。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来，我们讲讲FMDB的几个操作：&lt;/p&gt;
&lt;h3&gt;创建数据库&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;/*&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果该路径下已经存在该数据库，直接获取该数据库;&lt;/li&gt;
&lt;li&gt;如果不存在就创建一个新的数据库;&lt;/li&gt;
&lt;li&gt;如果传@&quot;&quot;，会在临时目录创建一个空的数据库，当数据库关闭时，数据库文件也被删除;&lt;/li&gt;
&lt;li&gt;如果传nil，会在内存中临时创建一个空的数据库，当数据库关闭时，数据库文件也被删除;
*/&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;(FMDatabase *)databaseWithPath:(NSString *)filePath;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;打开（关闭）数据库&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;/* 打开数据库，成功返回YES，失败返回NO */&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(BOOL)open;
/* 关闭数据库，成功返回YES，失败返回NO */&lt;/li&gt;
&lt;li&gt;(BOOL)close;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;执行更新的SQL语句&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在FMDB中，除了查询以外的所有操作都称为更新。如 &lt;code&gt;create&lt;/code&gt; &lt;code&gt;drop&lt;/code&gt; &lt;code&gt;insert&lt;/code&gt; &lt;code&gt;update&lt;/code&gt; &lt;code&gt;delete&lt;/code&gt; 等命令都是更新操作&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;/* 执行更新的SQL语句，字符串里面的&quot;?&quot;，依次用后面的参数替代，必须是对象，不能是int等基本类型 */&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(BOOL)executeUpdate:(NSString &lt;em&gt;)sql,... ;
/&lt;/em&gt; 执行更新的SQL语句，可以使用字符串的格式化进行构建SQL语句 */&lt;/li&gt;
&lt;li&gt;(BOOL)executeUpdateWithFormat:(NSString*)format,... ;
/* 执行更新的SQL语句，字符串中有&quot;?&quot;，依次用arguments的元素替代 */&lt;/li&gt;
&lt;li&gt;(BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
下面的几种方式等价：


```objective-c
/* 1. 直接使用完整的SQL更新语句 */
[database executeUpdate:@&quot;insert into mytable(num,name,sex) values(0,&apos;liuting&apos;,&apos;m&apos;);&quot;];

NSString *sql = @&quot;insert into mytable(num,name,sex) values(?,?,?);&quot;;
/* 2. 使用不完整的SQL更新语句，里面含有待定字符串&quot;?&quot;，需要后面的参数进行替代 */
[database executeUpdate:sql,@0,@&quot;liuting&quot;,@&quot;m&quot;];
/* 3. 使用不完整的SQL更新语句，里面含有待定字符串&quot;?&quot;，需要数组参数里面的参数进行替代 */
[database executeUpdate:sql
   withArgumentsInArray:@[@0,@&quot;liuting&quot;,@&quot;m&quot;]];

/* 4. SQL语句字符串可以使用字符串格式化，这种我们应该比较熟悉 */
[database executeUpdateWithFormat:@&quot;insert into mytable(num,name,sex) values(%d,%@,%@);&quot;,0,@&quot;liuting&quot;,&quot;m&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e0b683bbdd344a3181ed024746f22193.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;执行查询的SQL语句&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 执行查询SQL语句，返回FMResultSet查询结果 */
- (FMResultSet *)executeQuery:(NSString*)sql, ... ;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... ;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* 获取下一个记录 */
- (BOOL)next;
/* 获取记录有多少列 */
- (int)columnCount;
/* 通过列名得到列序号，通过列序号得到列名 */
- (int)columnIndexForName:(NSString *)columnName;
- (NSString *)columnNameForIndex:(int)columnIdx;
/* 获取存储的整形值 */
- (int)intForColumn:(NSString *)columnName;
- (int)intForColumnIndex:(int)columnIdx;
/* 获取存储的长整形值 */
- (long)longForColumn:(NSString *)columnName;
- (long)longForColumnIndex:(int)columnIdx;
/* 获取存储的布尔值 */
- (BOOL)boolForColumn:(NSString *)columnName;
- (BOOL)boolForColumnIndex:(int)columnIdx;
/* 获取存储的浮点值 */
- (double)doubleForColumn:(NSString *)columnName;
- (double)doubleForColumnIndex:(int)columnIdx;
/* 获取存储的字符串 */
- (NSString *)stringForColumn:(NSString *)columnName;
- (NSString *)stringForColumnIndex:(int)columnIdx;
/* 获取存储的日期数据 */
- (NSDate *)dateForColumn:(NSString *)columnName;
- (NSDate *)dateForColumnIndex:(int)columnIdx;
/* 获取存储的二进制数据 */
- (NSData *)dataForColumn:(NSString *)columnName;
- (NSData *)dataForColumnIndex:(int)columnIdx;
/* 获取存储的UTF8格式的C语言字符串 */
- (const unsigned cahr *)UTF8StringForColumnName:(NSString *)columnName;
- (const unsigned cahr *)UTF8StringForColumnIndex:(int)columnIdx;
/* 获取存储的对象，只能是NSNumber、NSString、NSData、NSNull */
- (id)objectForColumnName:(NSString *)columnName;
- (id)objectForColumnIndex:(int)columnIdx;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c1e89e7a38474e5eaaa6d077457df832.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;FMDatabaseQueue&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;FMDatabase&lt;/code&gt;这个类是线程不安全的，如果在多个线程同时使用一个&lt;code&gt;FMDatabase&lt;/code&gt;实例，会造成数据混乱问题。 为了保证线程安全，FMDB提供方便快捷的&lt;code&gt;FMDatabaseQueue&lt;/code&gt;类，要使用这个类，需要&lt;code&gt;#import&lt;/code&gt;导入头文件&lt;code&gt;&quot;FMDatabaseQueue.h&quot;&lt;/code&gt;，&lt;code&gt;FMDatabaseQueue&lt;/code&gt;类的操作很多都和&lt;code&gt;FMDatabase&lt;/code&gt;很相似&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@&quot;FMDB.db&quot;];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[queue inDatabase:^(FMDatabase*db) {
        //FMDatabase数据库操作
}];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;事物&lt;/h2&gt;
&lt;p&gt;事物是指作为单个逻辑工作但愿执行的一系列操作，要么完整执行，要么完全的不自信。&lt;/p&gt;
&lt;p&gt;想象一个场景，比如你要更新数据库的大量数据，我们需要确保所有的数据更新成功，才采取这种更新方案，如果在更新期间出现错误，就不能采取这种更新方案了，如果我们不使用事务，我们的更新操作直接对每个记录生效，万一遇到更新错误，已经更新的数据怎么办？难道我们要一个一个去找出来修改回来吗？怎么知道原来的数据是怎么样的呢？这个时候就需要使用&lt;strong&gt;事务&lt;/strong&gt;实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;只要在执行SQL语句前加上以下的SQL语句，就可以使用事务功能了：
开启事务的SQL语句，&quot;begin transaction;&quot;
进行提交的SQL语句，&quot;commit transaction;&quot;
进行回滚的SQL语句，&quot;rollback transaction;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有事物提交了，开启事物的操作才会生效&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;FMDB对SQLite进行了良好的封装，使用起来非常方便，对于那些使用纯SQLite来进行&lt;a href=&quot;https://so.csdn.net/so/search?q=%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C&amp;amp;spm=1001.2101.3001.7020&quot;&gt;数据库操作&lt;/a&gt;的项目，可以将其迁移到FMDB上，提高数据库相关功能维护的效率 具体的一些其他方法将继续参考这个链接进行学习：https://ccgus.github.io/fmdb/html/index.html&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Core Data&lt;/h2&gt;
&lt;p&gt;因为笔者最近比较繁忙，这部分留到后面再进行学习&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/155421618&quot;&gt;【iOS】数据持久化&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】TableView的优化</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-155425916-ios-tableview/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-155425916-ios-tableview/</guid><description>一、优化的本质 UITableView 的优化本质在于提高滚动性能和减少内存使用，以保证流畅的用户体验，从计算机层面来讲，其核心本质为降低 CPU和GPU 的工作来提升性能 CPU：对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制 G</description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、优化的本质&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UITableView&lt;/code&gt; 的优化本质在于提高滚动性能和减少内存使用，以保证流畅的用户体验，从计算机层面来讲，其核心本质为降低 CPU和GPU 的工作来提升性能&lt;/p&gt;
&lt;p&gt;CPU：对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制
GPU：接收提交的纹理和顶点描述、应用变换、混合并渲染、输出到屏幕&lt;/p&gt;
&lt;h2&gt;二、卡顿产生原因&lt;/h2&gt;
&lt;p&gt;App主线程在&lt;code&gt;CPU&lt;/code&gt;中显示计算内容，比如视图的创建，布局的计算，图片解码，文本绘制，然后我们的CPU会将计算好的内容提交到&lt;code&gt;GPU&lt;/code&gt;中进行变换，合成，渲染，这其中也包括我们常说的离屏渲染&lt;/p&gt;
&lt;p&gt;在开发中，&lt;code&gt;CPU与GPU&lt;/code&gt;任何一个压力过大都会导致掉帧&lt;/p&gt;
&lt;h2&gt;三、CPU层面的优化&lt;/h2&gt;
&lt;h3&gt;cellForRowAtIntexPath 方法不要进行耗时操作&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1、不读取/写入文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、尽量少用addView给Cell动态添加View，可以在初始化就添加，然后通过设置气hidden来控制是否显示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为我们在滑动&lt;code&gt;UITableView&lt;/code&gt;的过程中会不断调用这个方法。&lt;/p&gt;
&lt;h3&gt;cell的复用&lt;/h3&gt;
&lt;p&gt;简单来讲就是我们的&lt;code&gt;UITableView&lt;/code&gt;只会创建比一个屏幕所有显示的cell+1个单元格，当当前的cell划出屏幕时，我们的cell并不会销毁，而是会将其存入我们的复用池&lt;/p&gt;
&lt;p&gt;当要显示某一个位置的cell时首先会去复用池中查找，如果找不到才会重新创建，而不是每一次都进行重新创建，这样就极大地减少了内存开销&lt;/p&gt;
&lt;p&gt;可以参考我之前的博客：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/148500158?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=148500158&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;自定义cell与cell的复用&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;提前计算布局&lt;/h3&gt;
&lt;p&gt;解了这件事情与tableView的复用机制之后，我们再回头看我们的cellForRow与heightForRow方法，我们知道当我们滑动tableview时就不断地调用这两个方法，因此这两个方法是性能优化的关键&lt;/p&gt;
&lt;p&gt;UITableViewCell高度计算主要分为两种，一种&lt;strong&gt;固定高度&lt;/strong&gt;，另外一种&lt;strong&gt;动态高度&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;rowHeight&lt;/strong&gt;
这个属性是我们的固定高度，对于定高需求的表格，强烈建议使用这种方法来避免不必要的高度计算以及调用。或者我们可以使用&lt;code&gt;UITableViewDelegate&lt;/code&gt;的&lt;code&gt;heightForRowAtIndexPath&lt;/code&gt;，然而，实现这个方法后我们的rowHeight将无效，所以这个方法适合具有多种cell的UITableView。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;estimatedRowHeight&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是一个估算行高的属性，对于动态计算行高，这里有多种方法，但核心还是通过&lt;code&gt;设置预算高度和estimatedRowHeight = UITableViewAutomaticDimension&lt;/code&gt;，然后&lt;code&gt;用AutoLayout对控件进行约束达到撑开cell&lt;/code&gt;的目的。&lt;/p&gt;
&lt;p&gt;但是这也不可避免地加大了内存开销，因为AutoLayout最终需要转成frame&lt;/p&gt;
&lt;p&gt;我们通过estimatedRowHeight与AutoLayout大大简化了我们动态计算行高的过程，同时我们需要尽可能精确估计estimatedRowHeight的范围，即使面对种类不同的 cell，我们依然可以使用简单的 estimatedRowHeight 属性赋值，只要整体估算值接近就可以，比如大概有一半 cell 高度是 44， 一半 cell 高度是 88， 那就可以估算一个 66，基本符合预期。尽可能精确的估算可以使初次加载和滚动表格时更加流畅&lt;/p&gt;
&lt;h4&gt;1、设置预估行高&lt;/h4&gt;
&lt;p&gt;我们知道&lt;code&gt;UITableView&lt;/code&gt;是通过UITableView代理方法&lt;code&gt;heightForRowAtIndexPath:&lt;/code&gt;方法来设置行高。自从iOS8.0之后，苹果新增了self-sizing cell的概念，也是cell可以自己计算行高，使用需要满足是三个条件：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(1) 使用&lt;code&gt;Autolayout&lt;/code&gt;进行UI布局约束 (2) 指定&lt;code&gt;TableView的estimatedRowHeight&lt;/code&gt;属性的默认值 (3) 指定TableView的rowHeight的属性为&lt;code&gt;UITableViewAutomaticDimension&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TableView在加载数据时会先通过&lt;code&gt;estimatedRowHeight:AtIndexPath&lt;/code&gt;处理全部数据，此时我们只需要提供一个粗略的高度，待到cell对象创建之后再去设置cell的真实高度。而且只会处理当前屏幕范围内的cell，这样子会显著的提升加载的性能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/25a9c49d8cc04614934933bb49f0b4e9.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2、预先计算并缓存行高&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/382d6b7f5f864e57bfb6032656d28b09.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上图可以很容易的分析出，iOS8.0之后在获取cell对象之后会再次调用&lt;code&gt;heightForRowAtIndexPath:&lt;/code&gt;方法获取行高，这也就意味着我们其实可以先创建cell对象，之后再提供行高。具体方法我们可以在cell类中添加layoutAttribute属性，记录相应的&lt;code&gt;UIEdgeInsets&lt;/code&gt;，然后在设置cell真实高度的时候返回。iOS7.0之前则必须在cell对象处啊给你讲爱你之前先获得所有cell的高度。&lt;/p&gt;
&lt;h3&gt;异步加载图片：SDWebImage 的使用&lt;/h3&gt;
&lt;p&gt;（1）使用异步子线程处理，然后再返回主线程操作；
（2）图片缓存处理，避免多次处理操作；
（3）图片圆角处理时，设置 layer 的 shouldRasterize 属性为 YES，可以将负载转移给 CPU；&lt;/p&gt;
&lt;p&gt;这部分内容笔者现在也不是很清楚，会在学习网SDWebImage源码后再进行补充。&lt;/p&gt;
&lt;h2&gt;四、GPU层面优化&lt;/h2&gt;
&lt;h3&gt;离屏渲染&lt;/h3&gt;
&lt;p&gt;什么是离屏渲染？我们知道iOS底层的渲染框架使用的是&lt;code&gt;OpenGL ES&lt;/code&gt;。OpenGL中，GPU渲染屏幕方式有两种：当前屏幕渲染&lt;code&gt;（On-Screen Rendering）&lt;/code&gt;和离屏渲染&lt;code&gt;（Off-Screen Rendering）&lt;/code&gt;。它们的区别是当前屏幕渲染操作是在当前屏幕缓冲区完成，而离屏渲染会在另外一个&lt;strong&gt;新开辟的缓冲区完成渲染操作&lt;/strong&gt;，开启离屏渲染的代价就是需要新开辟一块新的缓冲区，在渲染的过程中还会多次切换上下文，这些都是很消耗性能的。&lt;/p&gt;
&lt;p&gt;官方对离屏渲染产生性能问题也进行了优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iOS 9.0 之前&lt;code&gt;UIimageView跟UIButton&lt;/code&gt;设置圆角都会触发离屏渲染。&lt;/li&gt;
&lt;li&gt;iOS 9.0 之后&lt;code&gt;UIButton设置圆角&lt;/code&gt;会触发离屏渲染，而&lt;code&gt;UIImageView里png图片&lt;/code&gt;设置圆角不会触发离屏渲染了，如果设置其他阴影效果之类的还是会触发离屏渲染的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一下情况均会造成 离屏渲染 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为图层设置遮罩（layer.mask）&lt;/li&gt;
&lt;li&gt;设置图层的layer.masksToBounds/view.clipsToBounds属性为True&lt;/li&gt;
&lt;li&gt;设置图层的layer.allowsGroupOpacity的属性为True和layer.opacity小于1.0&lt;/li&gt;
&lt;li&gt;设置图层阴影（layer.shadow）&lt;/li&gt;
&lt;li&gt;设置图层的layer.shouldRasterize的属性为True -具有layer.cornerRadius，layer.edgeAntialiasingMask， – layer.allowsAntialiasing的图层&lt;/li&gt;
&lt;li&gt;文本（任何种类，包括UILabel、CATextLayer、Core Text等）&lt;/li&gt;
&lt;li&gt;使用CGContext在drawRect:方法中绘制&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;离屏渲染的优化&lt;/h4&gt;
&lt;p&gt;** 使用贝塞尔曲线 + &lt;code&gt;Core Graphics&lt;/code&gt; 框架设置圆角**&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)setImageCircularEdge:(UIImageView *)imageView {

    //开始对imageView进行画图
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
    //使用贝塞尔曲线画出一个圆形图
    [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
    [imageView drawRect:imageView.bounds];
    imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    //结束画图
    UIGraphicsEndImageContext();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;** 使用贝塞尔曲线 + &lt;code&gt;CAShapeLayer&lt;/code&gt; 设置圆角 **&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)setImageCircularEdge2:(UIImageView *)imageView {

    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
    CAShapeLayer *maskLayer=[[CAShapeLayer alloc] init];
    //设置大小
    maskLayer.frame = imageView.bounds;
    //设置图形样子
    maskLayer.path = maskPath.CGPath;
    imageView.layer.mask = maskLayer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于方案2需要解释的是：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CAShapeLayer&lt;/code&gt;继承于&lt;code&gt;CALayer&lt;/code&gt;,可以使用CALayer的所有属性值；
&lt;code&gt;CAShapeLayer需要贝塞尔曲线配合使用才有意义&lt;/code&gt;（也就是说才有效果）
使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect（继承于CoreGraphics走的是CPU,消耗的性能较大）方法中画出一些想要的图形
CAShapeLayer动画渲染直接提交到手机的GPU当中，相较于view的drawRect方法使用CPU渲染而言，其效率极高，能大大优化内存使用情况。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;总的来说就是用&lt;code&gt;CAShapeLayer&lt;/code&gt;的内存消耗少，渲染速度快，建议使用优化方案2。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于离屏渲染的检测，据说苹果为我们提供了一个测试工具，这里先埋一个坑，以后再来填。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;UITableView的性能优化涉及到了许多层面，下到底层的Layer属性，上到第三方库SDWebImage与RunLoop，这些东西的实现都十分巧妙，还有很长一段路需要学习&lt;/p&gt;
&lt;p&gt;这里笔者写一下Tableview 性能优化方法总览&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现Tableview的懒加载以及cell的复用（这是优化Tableview最基础的部分，老生常谈了，特别是复用池这一块的内容：将加载过的cell加入到复用池中，需要时取出）&lt;/li&gt;
&lt;li&gt;高度缓存（本篇文章着重介绍了cell的高度缓存机制，因为heightForRowAtIndexPath: 是调用最频繁的方法，我们围绕这个方法展开，通过避免重复计算来减少我们的内存开销。当行高固定时使用固定行高，不固定时缓存一次后返回固定行高）&lt;/li&gt;
&lt;li&gt;预缓存（在高度缓存的实现上进行优化，涉及到RunLoop层面的知识，后面加以补充，似乎与SDWebImage有异曲同工之处，十分巧妙）&lt;/li&gt;
&lt;li&gt;异步加载图片（SDWebImage的使用，后面看源码看完再回来总结）&lt;/li&gt;
&lt;li&gt;按需加载内容（涉及到许多协议，当快速滑动时不加载资源，即将停止滑动时加载资源，同时释放那些超出屏幕的资源，只显示目前呈现在屏幕上的内容）&lt;/li&gt;
&lt;li&gt;视图层面（避免离屏渲染，使用贝塞尔曲线或是直接呈现采裁剪后的圆角图片来避免离屏渲染的发生）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/155425916&quot;&gt;【iOS】TableView的优化&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】多线程与GCD</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-154915356-ios-gcd/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-154915356-ios-gcd/</guid><description>线程与进程 线程和进程的定义 线程 线程是进程的基本执行单元，一个进程的所有任务都在线程中执行 进程要想执行任务，必须要有线程，进程至少要有一条线程 程序启动时会默认开启一条线程，这条线程被称为主线程或UI线程 进程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的，</description><pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;线程与进程&lt;/h2&gt;
&lt;h3&gt;线程和进程的定义&lt;/h3&gt;
&lt;h4&gt;线程&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;线程是进程的基本执行单元，一个进程的所有任务都在线程中执行&lt;/li&gt;
&lt;li&gt;进程要想执行任务，必须要有线程，进程至少要有一条线程&lt;/li&gt;
&lt;li&gt;程序启动时会默认开启一条线程，这条线程被称为主线程或UI线程&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;进程&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;进程是指在系统中正在运行的一个应用程序&lt;/li&gt;
&lt;li&gt;每个进程之间是独立的，每个进程均运行在其专用的且受保护的内存空间内&lt;/li&gt;
&lt;li&gt;通过“活动监视器”可以查看mac系统内开启的线程 所以，可以简单理解为：进程是线程的容器，而线程用来执行任务。在iOS中是单进程开发，一个进程就是一个app，进程之间是相互独立的，如支付宝、微信、qq等，这些都是属于不同的进程&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;进程与线程的关系&lt;/h4&gt;
&lt;p&gt;进程与线程之间的关系主要涉及两个方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;地址空间 同一个进程的线程共享本进程的地址空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而进程之间则是独立的地址空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;资源拥有 同一个进程内线程共享本进程的资源，如内存、I/O、cpu等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是进程之间资源是独立的 两个之间的关系就相当于工厂与流水线的关系，工厂与工厂之间是相互独立的，而工厂中的流水线是共享工厂的资源的，即 进程相当于一个工厂，线程相当于工厂中的一条流水线 针对进程和线程，还有以下几点说明：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1: 多进程要比多线程健壮 一个进程崩溃后，在保护模式下不会对其他进程产生影响&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而一个线程崩溃整个进 程都死掉&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2: 使用场景：频繁切换、并发操作 进程切换时，消耗的资源大，效率高。所以涉及到频繁的切换时，使用线程要好于进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同样如果要求同时进行并且又要共享某些变量的并发操作，只能用线程不能用进程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3: 执行过程 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是 线程不能独立执行，必须依存在应用程序中，由应用程序提供多个线程执行控制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4: 线程是处理器调度的基本单位，但是进程不是。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5: 线程没有地址空间,线程包含在进程地址空间中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;多线程&lt;/h2&gt;
&lt;h3&gt;多线程原理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对于单核CPU， 同一时间，CPU只能处理一条线程，即只有一条线程在工作&lt;/li&gt;
&lt;li&gt;iOS中的多线程同时执行的本质是CPU在多个人物之间直接进行快速的切换 ，由于CPU调度线程的时间足够快，就造成了 多线程同时执行的效果。其中切换到时间间隔就是时间片。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多线程意义&lt;/h3&gt;
&lt;h4&gt;优点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;能适当提高程序的执行效率&lt;/li&gt;
&lt;li&gt;能适当提高资源利用率，如CPU、内存&lt;/li&gt;
&lt;li&gt;线程上的任务执行完成后，线程会自动销毁&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;缺点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;开启线程需要占用一定的内存空间，默认情况下，每一个线程占用512KB&lt;/li&gt;
&lt;li&gt;如果开启大量线程，会占用大量的内存空间，降低程序的性能&lt;/li&gt;
&lt;li&gt;线程越多，CPU在调用线程上的开销就越大&lt;/li&gt;
&lt;li&gt;程序设计更加复杂，比如线程间的通信，多线程的数据共享&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多线程的生命周期&lt;/h3&gt;
&lt;p&gt;新建 - 就绪 - 运行 - 阻塞 - 死亡&lt;/p&gt;
&lt;p&gt;线程与进程&lt;/p&gt;
&lt;p&gt;线程和进程的定义&lt;/p&gt;
&lt;p&gt;线程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程是进程的基本执行单元，一个进程的所有任务都在线程中执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程要想执行任务，必须要有线程，进程至少要有一条线程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序启动时会默认开启一条线程，这条线程被称为主线程或UI线程 进程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程是指在系统中正在运行的一个应用程序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个进程之间是独立的，每个进程均运行在其专用的且受保护的内存空间内&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过“活动监视器”可以查看mac系统内开启的线程 所以，可以简单理解为：进程是线程的容器，而线程用来执行任务。在iOS中是单进程开发，一个进程就是一个app，进程之间是相互独立的，如支付宝、微信、qq等，这些都是属于不同的进程 进程与线程的关系 进程与线程之间的关系主要涉及两个方面：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;地址空间 同一个进程的线程共享本进程的地址空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而进程之间则是独立的地址空间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;资源拥有 同一个进程内线程共享本进程的资源，如内存、I/O、cpu等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是进程之间资源是独立的 两个之间的关系就相当于工厂与流水线的关系，工厂与工厂之间是相互独立的，而工厂中的流水线是共享工厂的资源的，即 进程相当于一个工厂，线程相当于工厂中的一条流水线 针对进程和线程，还有以下几点说明：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1: 多进程要比多线程健壮 一个进程崩溃后，在保护模式下不会对其他进程产生影响&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而一个线程崩溃整个进 程都死掉&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2: 使用场景：频繁切换、并发操作 进程切换时，消耗的资源大，效率高。所以涉及到频繁的切换时，使用线程要好于进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同样如果要求同时进行并且又要共享某些变量的并发操作，只能用线程不能用进程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3: 执行过程 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是 线程不能独立执行，必须依存在应用程序中，由应用程序提供多个线程执行控制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4: 线程是处理器调度的基本单位，但是进程不是。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5: 线程没有地址空间,线程包含在进程地址空间中 多线程 多线程原理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于单核CPU， 同一时间，CPU只能处理一条线程，即只有一条线程在工作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;iOS中的多线程同时执行的本质是CPU在多个人物之间直接进行快速的切换 ，由于CPU调度线程的时间足够快，就造成了 多线程同时执行的效果。其中切换到时间间隔就是时间片。 多线程意义 优点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能适当提高程序的执行效率&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能适当提高资源利用率，如CPU、内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程上的任务执行完成后，线程会自动销毁 缺点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开启线程需要占用一定的内存空间，默认情况下，每一个线程占用512KB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果开启大量线程，会占用大量的内存空间，降低程序的性能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程越多，CPU在调用线程上的开销就越大&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序设计更加复杂，比如线程间的通信，多线程的数据共享&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多线程的生命周期&lt;/p&gt;
&lt;p&gt;新建 - 就绪 - 运行 - 阻塞 - 死亡&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b38587e6206048e1980176ca7cddfbdb.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新建：主要是实例化线程对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;就绪：线程对象调用start方法，将线程对象加入可调度线程池，等待CPU的调用，即调用start方法，并不会立即执行，进入就绪状态，需要等待一段时间，经CPU调度后才执行，也就是从就绪状态进入运行状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行：CPU负责调度可调度线城市中线程的执行，在线程执行完成之前，其状态可能会在就绪和运行之间来回切换，这个变化是由CPU负责，开发人员不能干预。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;阻塞：当满足某个预定条件时，可以使用休眠，即sleep，或者同步锁，阻塞线程执行。当进入sleep时，会重新将线程加入就绪中。下面关于休眠的时间设置，都是NSThread的 sleepUntilDate: 阻塞当前线程，直到指定的时间为止，即休眠到指定时间&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sleepForTimeInterval: 在给定的时间间隔内休眠线程，即指定休眠时长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同步锁：@synchronized(self)：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;死亡：分为两种情况 正常死亡，即线程执行完毕&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非正常死亡，即当满足某个条件后，在线程内部（或者主线程中）终止执行（调用exit方法等退出）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简要说明，就是处于运行中的线程拥有一段可以执行的时间（称为时间片），&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果时间片用尽，线程就会进入就绪状态队列&lt;/li&gt;
&lt;li&gt;如果时间片没有用尽，且需要开始等待某事件，就会进入阻塞状态队列&lt;/li&gt;
&lt;li&gt;等待事件发生后，线程又会重新进入就绪状态队列&lt;/li&gt;
&lt;li&gt;每当一个线程离开运行，即执行完毕或者强制退出后，会重新从就绪状态队列中选择一个线程继续执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程的exit与cancel说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;exit：一旦强行终止线程，后续的代码都不会执行&lt;/li&gt;
&lt;li&gt;cancel：取消当前线程，但不能取消正在执行的线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;线程的优先级越高，是不是意味着任务的执行越快？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;线程自信的快慢，除了要看优先级，还要查看资源的大小（即任务的复杂程度）、以及CPU调度情况。在NSThread中，线程优先级threadPriority已经被服务质量qualityOfService取代。&lt;/p&gt;
&lt;p&gt;在早期NSThread中，我们就可以通过threadPriority设置线程的“优先级”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSThread *thread = [[NSThread alloc] initWithBlock:^{
    // 执行任务
}];
thread.threadPriority = 0.8; // 设置优先级，取值范围 0.0 ~ 1.0
[thread start];

0.0是优先级最低，1.0优先级最高，默认值大约为0.5
Apple后来引入了 服务质量 概念，用于更智能地管理线程优先级，在NSThread、NSOperation、GCD都支持这种机制，
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    // 执行任务
}];
thread.qualityOfService = NSQualityOfServiceUserInitiated; // 设置服务质量
[thread start];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/7f539a1baada444193bccb4c728c192f.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/aeab961a7f3742cd9cd1708c9616bc1c.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;线程池&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0d8c0521b8a24870b0059ce57f82e942.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;iOS多线程&lt;/h2&gt;
&lt;p&gt;iOS中的多线程实现方式，主要有四种：pthread、NSThread、GCD、NSOperation，汇总如图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/90b585e12c774155b58fda6a3451a662.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// *********1: pthread*********
pthread_t threadId = NULL;//c字符串char *cString = &quot;HelloCode&quot;;/**
 pthread_create 创建线程
 参数：
 1. pthread_t：要创建线程的结构体指针，通常开发的时候，如果遇到 C 语言的结构体，类型后缀 `_t / Ref` 结尾
 同时不需要 `*`
 2. 线程的属性，nil(空对象 - OC 使用的) / NULL(空地址，0 C 使用的)
 3. 线程要执行的`函数地址`
 void *: 返回类型，表示指向任意对象的指针，和 OC 中的 id 类似
 (*): 函数名
 (void *): 参数类型，void *
 4. 传递给第三个参数(函数)的`参数`
 */int result = pthread_create(&amp;amp;threadId, NULL, pthreadTest, cString);if (result == 0) {NSLog(@&quot;成功&quot;);} else {NSLog(@&quot;失败&quot;);}//*********2、NSThread*********[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];//*********3、GCD*********dispatch_async(dispatch_get_global_queue(0, 0), ^{[self threadTest];});//*********4、NSOperation*********[[[NSOperationQueue alloc] init] addOperationWithBlock:^{[self threadTest];}];- (void)threadTest{NSLog(@&quot;begin&quot;);
    NSInteger count = 1000 * 100;for (NSInteger i = 0; i &amp;lt; count; i++) {// 栈区
        NSInteger num = i;// 常量区
        NSString *name = @&quot;zhang&quot;;// 堆区
        NSString *myName = [NSString stringWithFormat:@&quot;%@ - %zd&quot;, name, num];NSLog(@&quot;%@&quot;, myName);}NSLog(@&quot;over&quot;);}void *pthreadTest(void *para){// 接 C 语言的字符串//    NSLog(@&quot;===&amp;gt; %@ %s&quot;, [NSThread currentThread], para);// __bridge 将 C 语言的类型桥接到 OC 的类型
    NSString *name = (__bridge NSString *)(para);NSLog(@&quot;===&amp;gt;%@ %@&quot;, [NSThread currentThread], name);return NULL;}
### 线程安全问题


当多个线程同时访问一块资源时，容易引发数据错乱和数据安全问题，有以下两种解决方案：


- 互斥锁
- 自旋锁


#### 互斥锁


- 用于保护临界区，确保同一时间，只有一条线程能够执行
- 如果代码中只有一个地方需要加锁，大多都使用 self，这样可以避免单独再创建一个锁对象
- 加了互斥锁的代码，当新线程访问时，如果发现其他线程正在执行锁定的代码，新线程就会进入休眠 针对互斥锁，还需要注意以下几点：
- 互斥锁的锁定范围，应该尽量小，锁定范围越大，效率越差
- 能够加锁的任意 NSObject 对象
- 锁对象一定要保证所有的线程都能够访问


#### 自旋锁


- 自旋锁与互斥锁类似，但它不是通过休眠使线程阻塞，而是在获取锁之前一直处于忙等（即原地打转，称为自旋）阻塞状态
- 使用场景：锁持有的时间短，且线程不希望在重新调度上花太多成本时，就需要使用自旋锁，属性修饰符atomic，本身就有一把自旋锁
- 加入了自旋锁，当新线程访问代码时，如果发现有其他线程正在锁定代码，新线程会用死循环的方法，一直等待锁定的代码执行完成，即不停的尝试执行代码，比较消耗性能

**【面试题】：自旋锁 vs 互斥锁**


- 同：在同一时间，保证了只有一条线程执行任务，即保证了相应同步的功能
- 不同： 互斥锁：发现其他线程执行，当前线程 休眠（即就绪状态），进入等待执行，即挂起。一直等其他线程打开之后，然后唤醒执行
- 自旋锁：发现其他线程执行，当前线程 一直询问（即一直访问），处于忙等状态，耗费的性能比较高


- 场景：根据任务复杂度区分，使用不同的锁，但判断不全时，更多是使用互斥锁去处理 当前的任务状态比较短小精悍时，用自旋锁
- 反之的，用互斥锁


#### atomic 原子锁 &amp;amp; nonatomic 非原子锁


atomic 和 nonatomic主要用于属性的修饰，以下是相关的一些说明：


- atomic是原子属性，是为多线程开发准备的，是默认属性！ 同一时间 单(线程)写多(线程)读的线程处理技术
- Mac开发中常用


- nonatomic 是非原子属性 没有锁！性能高！
- 移动端开发常用

atomic的底层在最早是通过轻量级自旋锁 OSSpinLock实现的，后来因为存在优先级反转问题被弃用了，现在内部已经改成使用os_unfair_lock


```objective-c
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else { // 加锁修饰不加锁修饰
        spinlock_t&amp;amp; slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;
        slotlock.unlock();
    }

    objc_release(oldValue);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在代码中我们可以看出，&lt;/p&gt;
&lt;p&gt;对于atomic修饰的属性，进行了spinlock_t加锁处理，但是在前文中提到OSSpinLock已经废弃了，这里的spinlock_t在底层是通过os_unfair_lock替代了OSSpinLock实现的加锁。同时为了防止哈希冲突，还是用了加盐操作&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;加盐（Salting）&lt;/strong&gt; 就是在密码前后加上随机字符串（称为“盐”），再进行哈希。 密码：123 盐：XyZ@9 哈希前的内容：“123XyZ@9” 哈希结果：MD5(“123XyZ@9”) = 7b3e25c0b34a7f… 同样的密码，加上不同的盐，哈希值完全不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;using spinlock_t = mutex_tt&amp;lt;LOCKDEBUG&amp;gt;;class mutex_tt : nocopy_t {
    os_unfair_lock mLock;...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;getter方法对atomic的处理，通setter是大致相同的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;

    // Atomic retain release world
    spinlock_t&amp;amp; slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();

    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;【面试题】atomic与nonatomic 的区别&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;nonatomic 非原子属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非线程安全，适合内存小的移动设备&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;atomic 原子属性(线程安全)，针对多线程设计的，默认值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;atomic 本身就有一把锁(自旋锁) 单写多读:单个线程写入，多个线程可以读取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;线程安全，需要消耗大量的资源 iOS 开发的建议&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有属性都声明为 nonatomic&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尽量避免多线程抢夺同一块资源 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理，减小移动客户端的压力 atomic修饰的属性绝对安全吗？ atomic只能保证我们的setter和getter方法的线程安全，并不能保证数据安全。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;GCD&lt;/h2&gt;
&lt;p&gt;GCD是异步执行任务的技术之一，是Apple开发的一个多核变成的较新的解决方式。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。他是在一个线程池的基础上，并行执行任务。&lt;/p&gt;
&lt;h3&gt;GCD核心&lt;/h3&gt;
&lt;p&gt;GCD的核心主要由 任务+队列+函数 组成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//********GCD基础写法********//创建任务
dispatch_block_t block = ^{NSLog(@&quot;hello GCD&quot;);};//创建串行队列
dispatch_queue_t queue = dispatch_queue_create(&quot;com.CJL.Queue&quot;, NULL);//将任务添加到队列，并指定函数执行dispatch_async(queue, block);
#### 任务


任务就是执行操作的意思，换句话说就是我们在线程执行的那段代码，在GCD中是放在block的


- 同步执行 ( sync ) 同步添加任务到指定的队列中,在添加的任务执行结束前,会一直等待,直到里面的任务完成之后再继续执行 只能在当前线程中执行任务,不具备开启新线程的能力
- 异步执行(async) 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行后面的任务 可以在新的线程中执行任务,具备开启新线程的能力 进入dispatch_sync函数后程序不会立刻返回，因此会阻塞线程 而进入dispatch_async函数后程序会立刻返回，不会妨碍进行下一步操作


#### 队列


![请添加图片描述](https://i-blog.csdnimg.cn/direct/28545f097a134aafa4da923fc30ad0e4.png)


队列（Dispatch Queue） ： 这里的队列指执行任务的等待队列，即用来存放任务的队列。队列是一种特殊的线性表，采用FIFO（先进线出）的原则，即新任务总是被插入到队列的末尾，而读取任务的时候总是从队列的头部开始读取。每读取一个任务，从队列中释放一个任务。


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/a16fee39e3cd484ba947d75103d0ef93.png)


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/db2ee593d76544678761a39b510554d8.png)


- 串行队列：每次只有一个任务被执行，让一个任务接着一个的去执行
- 并行队列：可以让多个任务并发执行，可以开启多个线程，并且同时执行任务

并发队列的并发功能只有在异步函数下才可以体现出来


```objective-c
dispatch_queue_t queue = dispatch_queue_create(&quot;SerialQueue&quot;, DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queue2 = dispatch_queue_create(&quot;ConcurrentQueue&quot;, DISPATCH_QUEUE_CONCURRENT);

        //对于串行队列，GCD提供了一个特殊的队列：主队列
        dispatch_queue_t mainQueue = dispatch_get_main_queue();//获取串行队列

        //对于并发队列，GCD提供了一个全局并发队列
        dispatch_queue_t uque = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b886f0a3aac8429a96e6a9fbda5e625f.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/7e140e3735664994931b5c4e5f6a3a85.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;串行并行与同步异步&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;同步异步函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同步函数（sync）和异步函数（async），只能决定能否开启新的线程执行任务，不能决定函数是串行执行还是并行执行&lt;/p&gt;
&lt;p&gt;同步:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在当前线程中执行,不具备开启新线程的能力&lt;/li&gt;
&lt;li&gt;队列后面的内容需要等到同步函数返回后才可以继续执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;异步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拥有开创新线程的能力`,但是不一定会开启新线程&lt;/li&gt;
&lt;li&gt;后面的内容不需要等异步函数返回后执行,后面的内容可以直接执行,所以需要开启新线程,因此具备开启新线程的能力 同步函数: 同步函数会阻塞当前函数的返回, 异步函数会立即执行,执行下面的代码&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;同步函数后面的内容要等到同步函数返回才可以执行,异步函数后面的内容不需要等异步函数返回才执行,可以直接执行,异步函数里面的内可能需要开启一个新线程来执行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;串行并行队列&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主要影响任务的执行方式，具体开不开开启新线程取决于上面的函数。&lt;/p&gt;
&lt;p&gt;并行：多个任务并发执行&lt;/p&gt;
&lt;p&gt;串行：一个任务执行完毕后才去执行下一个任务&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果传入的是一个并发队列，那就并发执行任务&lt;/li&gt;
&lt;li&gt;如果传入一个手动创建的串行队列，那就在子队列串行执行&lt;/li&gt;
&lt;li&gt;如果传入的是主队列，那就在主线程串行执行&lt;/li&gt;
&lt;li&gt;如果传入的是一个主队列，那么就不会开启新线程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GCD的基本使用&lt;/h3&gt;
&lt;h4&gt;同步串行&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t serialQueue = dispatch_queue_create(&quot;serialQueue&quot;, DISPATCH_QUEUE_SERIAL);
        dispatch_sync(serialQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;1 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
        dispatch_sync(serialQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;2 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
    }
    return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[图片]&lt;/p&gt;
&lt;p&gt;结论 ：同步串行队列没有开启新线程也没有异步执行&lt;/p&gt;
&lt;h4&gt;异步串行&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t serialQueue = dispatch_queue_create(&quot;serialQueue&quot;, DISPATCH_QUEUE_SERIAL);
        dispatch_async(serialQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;1 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
        dispatch_async(serialQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;2 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
        sleep(2);    //如果没有sleep，那么程序直接退出而没有输出
    }
    return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/138099d1d13642f388c618538b6bef4c.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论 ：&lt;/strong&gt; 同步串行队列没有开启新线程也没有异步执行&lt;/p&gt;
&lt;h4&gt;异步串行&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t serialQueue = dispatch_queue_create(&quot;serialQueue&quot;, DISPATCH_QUEUE_SERIAL);
        dispatch_async(serialQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;1 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
        dispatch_async(serialQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;2 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
        sleep(2);    //如果没有sleep，那么程序直接退出而没有输出
    }
    return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[图片]&lt;/p&gt;
&lt;p&gt;这里开启了一个新线程来执行这里的代码，但是因为串行队列还是顺序执行&lt;/p&gt;
&lt;h4&gt;同步并行&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t serialQueue = dispatch_queue_create(&quot;serialQueue&quot;, DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t concurrentQueue = dispatch_queue_create(&quot;concurrentQueue&quot;, DISPATCH_QUEUE_CONCURRENT);
        dispatch_sync(concurrentQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;1 %d == %@&quot;, i, [NSThread currentThread]);
                //sleep(1);
            }
        });
        dispatch_sync(concurrentQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;2 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
        //sleep(2);
    }
    return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[图片]&lt;/p&gt;
&lt;p&gt;同步函数没有开辟新线程的能力，也没有体现并发的特点&lt;/p&gt;
&lt;h4&gt;异步并行&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t serialQueue = dispatch_queue_create(&quot;serialQueue&quot;, DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t concurrentQueue = dispatch_queue_create(&quot;concurrentQueue&quot;, DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;1 %d == %@&quot;, i, [NSThread currentThread]);
                //sleep(1);
            }
        });
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i &amp;lt; 5; i++) {
                NSLog(@&quot;2 %d == %@&quot;, i, [NSThread currentThread]);
            }
        });
        sleep(2);
    }
    return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[图片]&lt;/p&gt;
&lt;p&gt;这里创建了新线程的同时还体现了并发执行的特点。&lt;/p&gt;
&lt;h3&gt;面试问题&lt;/h3&gt;
&lt;h4&gt;异步函数 + 并行队列&lt;/h4&gt;
&lt;p&gt;下面代码的输出顺序是什么&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)interview01{//并行队列
    dispatch_queue_t queue = dispatch_queue_create(&quot;com.CJL.Queue&quot;, DISPATCH_QUEUE_CONCURRENT);
    NSLog(@&quot;1&quot;);// 耗时
    dispatch_async(queue, ^{
        NSLog(@&quot;2&quot;);
        dispatch_async(queue, ^{
            NSLog(@&quot;3&quot;);
        });
        NSLog(@&quot;4&quot;);
    });
    NSLog(@&quot;5&quot;);
}
    ----------打印结果-----------
输出顺序为：1 5 2 4 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[图片]&lt;/p&gt;
&lt;p&gt;主线程的任务队列为：&lt;code&gt;任务1、异步block1、任务5，&lt;/code&gt;其中异步block1会比较耗费性能，任务1和任务5的任务复杂度是相同的，所以任务1和任务5优先于异步block1执行&lt;/p&gt;
&lt;p&gt;在异步block1中，任务队列为：任务2、异步block2、任务4，其中block2相对比较耗费性能，任务2和任务4是复杂度一样，所以任务2和任务4优先于block2执行&lt;/p&gt;
&lt;p&gt;最后执行block2中的任务3&lt;/p&gt;
&lt;p&gt;在极端情况下，可能出现 任务2先于任务1和任务5执行，原因是出现了当前主线程卡顿或者 延迟的情况&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码修改&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;【修改1】：&lt;/strong&gt; 将并行队列 改成 串行队列，对结果没有任何影响，顺序仍然是 1 5 2 4 3&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;【修改2】：&lt;/strong&gt; 在任务5之前，休眠2s，即sleep(2)，执行的顺序为：1 2 4 3 5,原因是因为I/O的打印，相比于休眠2s，复杂度更简单，所以异步block1 会先于任务5执行。当然如果主队列堵塞，会出现其他的执行顺序&lt;/p&gt;
&lt;h4&gt;异步函数嵌套同步函数 + 并发队列&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/bc139fac2d9d4d2d8b78a7be1c04a960.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;任务1 和 任务5的分析同前面一致，执行顺序为 &lt;code&gt;任务1 任务5 异步block&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在异步block中，首先执行&lt;code&gt;任务2，然后走到同步block，&lt;/code&gt;由于&lt;code&gt;同步函数会阻塞主线程，&lt;/code&gt;所以任务4需要等待任务3执行完成后，才能执行，所以异步block中的执行顺序是：&lt;code&gt;任务2 任务3 任务4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;[图片]&lt;/p&gt;
&lt;h4&gt;异步函数嵌套同步函数 + 串行队列&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c289bed788d04647aae2888f6915e025.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先执行任务1，接下来是&lt;code&gt;异步block，并不会阻塞主线程，&lt;/code&gt;相比任务5而言，复杂度更高，所以&lt;code&gt;优先执行任务5，在执行异步block&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;异步block中，先执行任务2&lt;/code&gt;，接下来是同步block，同步函数会阻塞线程，所以执行任务4需要等待任务3执行完成，而任务3的执行，需要等待异步block执行完成，相当于任务3等待任务4完成&lt;/p&gt;
&lt;p&gt;所以就造成了&lt;code&gt;任务4等待任务3，任务3等待任务4，即互相等待的局面，就会造成死锁，这里有个&lt;/code&gt;重点是&lt;code&gt;关键的堆栈 slow&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改&lt;/strong&gt; ：如果删掉任务4，执行顺序是什么？&lt;/p&gt;
&lt;p&gt;还是会死锁，因为 &lt;strong&gt;任务3等待的是异步block执行完毕，而异步block等待任务3&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;死锁&lt;/h3&gt;
&lt;p&gt;死锁死因为资源有限以及线程的交错执行导致的。&lt;/p&gt;
&lt;p&gt;死锁产生的四个必要条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥访问，在有互斥访问的情况下，线程才会出现等待&lt;/li&gt;
&lt;li&gt;持有并等待，线程持有一些资源，并等待一些资源&lt;/li&gt;
&lt;li&gt;资源非抢占，一旦一个资源被持有，除非持有者主动放弃，否则其他竞争者都无法获取这个资源&lt;/li&gt;
&lt;li&gt;循环等待：循环等待是指存在一系列线程T0,T1,TnT0等待T1,T1等待T2,T2等待Tn这样便出现了一个循环等待&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步会阻塞当前函数的返回，异步函数会立刻返回，直接执行下面的代码&lt;/p&gt;
&lt;p&gt;因为同步函数会阻塞当前的一个函数，等待这个函数返回后才去执行任务，因此同步函数会等待函数的返回&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;队列上是放任务，而线程是执行队列上的任务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;同步函数 + 主队列&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/2f5f90335de74a06a67051aeedd9d617.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主队列首先有viewDidLoad任务，而这个时候给主队列添加了一个新的任务，这个任务会等待我们的viewDidLoad执行完毕才会继续执行任务。但主队列绑定主线程，主线程正在执行，他必须空出来才能执行队列的renew，但是此时主线程被sync挂住了，他在等待主队列任务执行完才能继续。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;p&gt;你（主线程）在厨房干活的时候（执行 main 函数）&lt;/p&gt;
&lt;p&gt;忽然接到一个命令：&lt;/p&gt;
&lt;p&gt;“把一道菜（block）放进你的订单列表（主队列），&lt;/p&gt;
&lt;p&gt;然后等它做完再继续干其他事。”&lt;/p&gt;
&lt;p&gt;于是：&lt;/p&gt;
&lt;p&gt;•        厨师（主线程）把任务（block）加进订单列表；&lt;/p&gt;
&lt;p&gt;•        然后原地等着这道菜做完；&lt;/p&gt;
&lt;p&gt;•        但厨房里只有他一个厨师（串行执行）；&lt;/p&gt;
&lt;p&gt;•        可他现在在等自己；&lt;/p&gt;
&lt;p&gt;•        所以他永远不会开始做这道菜。&lt;/p&gt;
&lt;h4&gt;异步串行队列嵌套同步串行队列&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/596da1d61be84b62a20437b229aba9fa.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（1）执行task1&lt;/p&gt;
&lt;p&gt;（2）执行task5&lt;/p&gt;
&lt;p&gt;（3）执行task2&lt;/p&gt;
&lt;p&gt;（4）阻塞同步线程，把task3加入到队列myQueue的队尾&lt;/p&gt;
&lt;p&gt;（5）task4需要等待task3执行完成后执行，但是此时task3又排在task4后面，所以造成了死锁&lt;/p&gt;
&lt;h4&gt;信号量阻塞主线程&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_main_queue(), ^{
    dispatch_semaphore_signal(sem);
    NSLog(@&quot;the sem +1&quot;);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@&quot;the sem -1&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主线程中&lt;code&gt;dispatch_semaphore_wait&lt;/code&gt;一直等着&lt;code&gt;dispatch_semaphore_signal&lt;/code&gt;改变信号量（+1操作），但是dispatch_semaphore_wait却阻塞了主线程导致dispatch_semaphore_signal无法执行，从而造成了死锁。&lt;/p&gt;
&lt;h3&gt;GCD相关方法&lt;/h3&gt;
&lt;h4&gt;dispatch_set_target_queue&lt;/h4&gt;
&lt;p&gt;dispatch_queue_create 方法生成的Queue不论是串行还是并行队列，都是和globalQueue的默认优先级相同执行优先级的线程，对于变更优先级我们需要使用&lt;code&gt;dispatch_set_target_queue函数&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dispatch_set_target_queue(serialQueue, globalDispatchQueueBackGround);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个是需要变更优先级的参数，第二个参数制定了与那个优先级相同的队列&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/3a6afc702c134db3a75eb42bbf8fda5a.png&quot; alt=&quot;在这里插入图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在必须将不可并行执行的处理追加到多个Serial Dispatch Queue 中时，如果使用dispatch set target queue 西数将目标指定为某 一个Serial DispatchQueue，即可防止处理并行执行。&lt;/p&gt;
&lt;h4&gt;dispatch_semaphore&lt;/h4&gt;
&lt;p&gt;什么是信号量，这里引用学长的学长的一段话&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;简单起见，假设停车场只有三个车位，一开始三个车位都是空的。这时如果同时来了五辆车，看 门人允许其中三辆直接进入，然后放下车拦，剩下的车则必须在入口等待，此后来的车也都不得不在入口处等待。这时，有一辆车离开停车场，看门人得知后，打开 车拦，放入外面的一辆进去，如果又离开两辆，则又可以放入两辆，如此往复。在这个停车场系统中，车位是公共资源，每辆车好比一个线程，看门人起的就是信号量的作用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;dispatch_semaphore_t currSingal = dispatch_semaphore_create(value);// 创建信号量,如果小于0则会返回NULL
dispatch_semaphore_signal(dispatch_semaphore_t  _Nonnull dsema); // 发送信号量让信号量加1
dispatch_semaphore_wait(dispatch_semaphore_t  _Nonnull dsema, dispatch_time_t timeout); // 可以让总信号量减1,信号量小于0的时候就会一直等待,否则就可以正常执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们要清楚需要哪一个线程等待，哪一个线程继续执行，然后使用信号量&lt;/p&gt;
&lt;p&gt;作用 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保持线程同步，将异步任务转化为同步执行任务&lt;/li&gt;
&lt;li&gt;保障线程的安全，为线程加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;并发队列里的异步任务顺序执行&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;dispatch_semaphore_t currentSingal = dispatch_semaphore_create(0);
    dispatch_queue_t que = dispatch_get_global_queue(0, 0);
    dispatch_async(que, ^{
        NSLog(@&quot;执行任务1， %@&quot;, [NSThread currentThread]);
        dispatch_semaphore_signal(currentSingal);
    });
    dispatch_semaphore_wait(currentSingal, DISPATCH_TIME_FOREVER);

    dispatch_async(que, ^{
        NSLog(@&quot;执行任务2， %@&quot;, [NSThread currentThread]);
        dispatch_semaphore_signal(currentSingal);
    });
    dispatch_semaphore_wait(currentSingal, DISPATCH_TIME_FOREVER);
    dispatch_async(que, ^{
        NSLog(@&quot;执行任务3， %@&quot;, [NSThread currentThread]);
        dispatch_semaphore_signal(currentSingal);
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/758b6076c7774b5abbcfc85723e590a4.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/200ff32c2a0a439caaefbe0d3df9c17d.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * semaphore 线程同步
 */
- (void)semaphoreSync {

    NSLog(@&quot;currentThread---%@&quot;,[NSThread currentThread]);  // 打印当前线程
    NSLog(@&quot;semaphore---begin&quot;);

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@&quot;1---%@&quot;,[NSThread currentThread]);      // 打印当前线程

        number = 100;

        dispatch_semaphore_signal(semaphore);
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@&quot;semaphore---end,number = %zd&quot;,number);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/39c5125ffc224f21bb372f5b7feb1006.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/24843211544c49279ac0297cf62ee5c1.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这段代码里：&lt;/p&gt;
&lt;p&gt;•        主线程被 wait 阻塞；&lt;/p&gt;
&lt;p&gt;•        异步任务完成后 signal；&lt;/p&gt;
&lt;p&gt;•        主线程恢复运行；&lt;/p&gt;
&lt;p&gt;→ 实现了异步任务的同步等待。&lt;/p&gt;
&lt;h5&gt;设置一个最大开辟的线程数 ：&lt;/h5&gt;
&lt;p&gt;在我们如果要下载很多图片的话,并发异步进行,每一个下载都会开辟一个新线程,可以我们又担心太多线程会道指内存开销太大,以及线程的上下文切换给我们的cpu带来的开销太大导致的问题所以我们要设置一下对应的一个线程最大数量就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dispatch_semaphore_t currSingal = dispatch_semaphore_create(3);// 创建信号量,如果小于0则会返回NULL
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i &amp;lt; 10; i++) {
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(currSingal, DISPATCH_TIME_FOREVER);
            NSLog(@&quot;执行任务, %d&quot;, i);
            sleep(1);
            NSLog(@&quot;完成任务, %d&quot;, i);
            dispatch_semaphore_signal(currSingal);
        });
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e91ac3b30c62447c84627ec13e88ce43.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在诶里可以看出我们这里最开始的三个异步操作,剩下的需要同步等待,只有释放出来的操作才可以进行异步操作&lt;/p&gt;
&lt;p&gt;这里其实出现了一个优先级反转的问题,这里我们看一下:如果打印我们的NSThread,他会出现什么:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/06f7260a168a4660919b96dd58e953a0.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个报错锁我们的User-initiated这个线程在等待交底优先级的线程.出现了一个优先级反转的内容:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;优先级反转&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;假设现在有3个任务A、B｀C’它们的优先 级为A＞B＞C任务C在运行时持有一把锁’然后它被高优先级的任务A抢占了（任 务C的锁没有被释放）。此时任务A恰巧也想申请任务C持有的锁’但是申请失败,因 此进入阻塞状态等待任务C放锁。此时’任务B、C都处于可以运行的状态,由于任务 B的优先级高于C,因此B优先运行°综合观察该情况’就会发现任务B好像优先级高 于任务A’先于任务A执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/f68a75ac91cd47ae9bee6c4cb2136089.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我们来理解一下上面为什么会出现一个优先级反转的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前三个任务线进入工作状态&lt;/li&gt;
&lt;li&gt;后面4 - 10个任务被阻塞&lt;/li&gt;
&lt;li&gt;系统会在这些阻塞的线程中，挑选一个来唤醒（注意这里并不一定考虑线程的一个优先级）&lt;/li&gt;
&lt;li&gt;这时候如果某一个优先级比较低的线程被唤醒会开始执行&lt;/li&gt;
&lt;li&gt;此时另一个高优先级的线程也在等待信号量,去不许等这个低优先级的线程先完成,这样那个自高优先级的线程就被低优先级的线程卡住了,这就是我们这里说的优先级反转的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;信号量调度是公平队列（FIFO / 不考虑QoS）+ 系统经常调度是根据优先级来的，两者机制不一致，所以可能造成高优先级线程因为信号量资源被低优先级线程占用而“被阻塞”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以在iOS中为了避免我们出现优先级反转的问题,尽量减少采用信号量的方式&lt;/p&gt;
&lt;h5&gt;Dispatch Semaphore 线程安全和线程同步（为线程加锁）&lt;/h5&gt;
&lt;p&gt;在程序执行时多个任务可能会对同—份数据产生 竞争’因此任务会使用锁来保护共享数据°&lt;/p&gt;
&lt;p&gt;线程安全：如果你的代码所在的进程中有多个线程在同时运行，而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的，而且其他的变量的值也和预期的是一样的，就是线程安全的。&lt;/p&gt;
&lt;p&gt;若每个线程中对全局变量、静态变量只有读操作，而无写操作，一般来说，这个全局变量是线程安全的；若有多个线程同时执行写操作（更改变量），一般都需要考虑线程同步，否则的话就可能影响线程安全。&lt;/p&gt;
&lt;p&gt;线程同步：可理解为线程 A 和 线程 B 一块配合，A 执行到一定程度时要依靠线程 B 的某个结果，于是停下来，示意 B 运行；B 依言执行，再将结果给 A；A 再继续操作。&lt;/p&gt;
&lt;p&gt;这里就涉及到之前了解过的售票问题，下面，我们模拟火车票售卖的方式，实现 NSThread 线程安全和解决线程同步问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 售卖火车票（线程安全）
 */
- (void)saleTicketSafe {
    while (1) {
        // 相当于加锁
        dispatch_semaphore_t semaphoreLock = dispatch_semaphore_create(1);        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);

        if (self.ticketSurplusCount &amp;gt; 0) {  // 如果还有票，继续售卖
            self.ticketSurplusCount--;
            NSLog(@&quot;%@&quot;, [NSString stringWithFormat:@&quot;剩余票数：%ld 窗口：%@&quot;, (long)self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完，关闭售票窗口
            NSLog(@&quot;所有火车票均已售完&quot;);

            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }

        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就保证了我们的线程一次只能处理一个任务，也就使我们的进程由并发队列+异步任务转换为了并发队列+同步任务&lt;/p&gt;
&lt;h4&gt;dispatch_after&lt;/h4&gt;
&lt;p&gt;表示在某队列中的block延迟执行&lt;/p&gt;
&lt;p&gt;应用：在主队列延迟执行一项任务，如viewDidLoad后延迟1s，提示一个alertView（是延迟加入到队列，而不是延迟执行）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dispatch_once
- (void)cjl_testOnce{
    /*
     dispatch_once保证在App运行期间，block中的代码只执行一次
     应用场景：单例、method-Swizzling
     */
    static dispatch_once_t onceToken;
    dispatch_once(&amp;amp;onceToken, ^{
        //创建单例、method swizzled或其他任务
        NSLog(@&quot;创建单例&quot;);
    });
}
#### dispatch_apply


按照指定的次数将指定的任务追加到指定的队列中，快速迭代一组任务，并等待全部队列执行结束。主要用于需要快速并行处理大量相同类型任务的场景，如图像处理、数据处理或任何形式的批处理任务。


在串行队列中无法体现出它的作用，因为队列中的任务始终是一次执行一个。


在并发队列中如果开启了多个线程的话就能在多个线程中同时展开执行，效率特别高。


```objective-c
- (void)cjl_testApply{
    /*
     dispatch_apply将指定的Block追加到指定的队列中重复执行，并等到全部的处理执行结束——相当于线程安全的for循环

     应用场景：用来拉取网络数据后提前算出各个控件的大小，防止绘制时计算，提高表单滑动流畅性
     - 添加到串行队列中——按序执行
     - 添加到主队列中——死锁
     - 添加到并发队列中——乱序执行
     - 添加到全局队列中——乱序执行
     */

    dispatch_queue_t queue = dispatch_queue_create(&quot;CJL&quot;, DISPATCH_QUEUE_SERIAL);
    NSLog(@&quot;dispatch_apply前&quot;);
    /**
         param1：重复次数
         param2：追加的队列
         param3：执行任务
         */
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@&quot;dispatch_apply 的线程 %zu - %@&quot;, index, [NSThread currentThread]);
    });
    NSLog(@&quot;dispatch_apply后&quot;);
}
#### dispatch_group_t


notify的作用就是：**当 group 中所有任务完成（enter/leave 配对完或者 dispatch_group_async 内 block 执行完）时，执行 block。**


##### dispatch_group_async + dispatch_group_notify


```objective-c
- (void)cjl_testGroup1{
    /*
     dispatch_group_t：调度组将任务分组执行，能监听任务组完成，并设置等待时间

     应用场景：多个接口请求之后刷新页面
     */

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    dispatch_group_async(group, queue, ^{
        NSLog(@&quot;请求一完成&quot;);
    });

    dispatch_group_async(group, queue, ^{
        NSLog(@&quot;请求二完成&quot;);
    });

    // 当group内所有任务完成时，这个block会在主线程执行。很适合UI刷新，不阻塞主线程

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@&quot;刷新页面&quot;);
    });
}
##### dispatch_group_enter + dispatch_group_leave + dispatch_group_notify


```objective-c
- (void)cjl_testGroup2{
    /*
     dispatch_group_enter和dispatch_group_leave成对出现，使进出组的逻辑更加清晰
     */
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@&quot;请求一完成&quot;);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@&quot;请求二完成&quot;);
        dispatch_group_leave(group);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@&quot;刷新界面&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;dispatch_group_enter(group)：标记“组里有一个任务开始了”。&lt;/li&gt;
&lt;li&gt;dispatch_group_leave(group)：标记“组里的一个任务完成了”。&lt;/li&gt;
&lt;li&gt;**enter/leave 必须配对出现，**否则组永远不会完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;可以理解为给 group 维护了一个计数器：每次 enter +1，每次 leave -1，当计数器归零时，group 完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;dispatch_wait&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/8d8beb2d29034599859f3a7f76938392.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;wait会阻塞线程，notify不会阻塞 是异步等待&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)cjl_testGroup3{
    /*
     long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)

     group：需要等待的调度组
     timeout：等待的超时时间（即等多久）
        - 设置为DISPATCH_TIME_NOW意味着不等待直接判定调度组是否执行完毕
        - 设置为DISPATCH_TIME_FOREVER则会阻塞当前调度组，直到调度组执行完毕


     返回值：为long类型
        - 返回值为0——在指定时间内调度组完成了任务
        - 返回值不为0——在指定时间内调度组没有按时完成任务

     */
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@&quot;请求一完成&quot;);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@&quot;请求二完成&quot;);
        dispatch_group_leave(group);
    });

//    long timeout = dispatch_group_wait(group, DISPATCH_TIME_NOW);
//    long timeout = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 *NSEC_PER_SEC));
    NSLog(@&quot;timeout = %ld&quot;, timeout);
    if (timeout == 0) {
        NSLog(@&quot;按时完成任务&quot;);
    }else{
        NSLog(@&quot;超时&quot;);
    }

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@&quot;刷新界面&quot;);
    });
}
#### dispatch_barrier_sync &amp;amp; dispatch_barrier_async


dispatch_barrier_async 是 GCD（Grand Central Dispatch）中的一个方法，它用于在并发队列中插入一个“栅栏”（barrier）。栅栏的作用是在其前面的任务执行完毕后，再执行栅栏任务，然后再执行栅栏之后的任务。这可以用于确保在多线程环境中某些操作的顺序性和互斥性。

栅栏函数，主要有两种使用场景：串行队列、并发队列


```objective-c
- (void)cjl_testBarrier{
    /*
     dispatch_barrier_sync &amp;amp; dispatch_barrier_async

     应用场景：同步锁

     等栅栏前追加到队列中的任务执行完毕后，再将栅栏后的任务追加到队列中。
     简而言之，就是先执行栅栏前任务，再执行栅栏任务，最后执行栅栏后任务

     - dispatch_barrier_async：前面的任务执行完毕才会来到这里
     - dispatch_barrier_sync：作用相同，但是这个会堵塞线程，影响后面的任务执行

     - dispatch_barrier_async可以控制队列中任务的执行顺序，
     - 而dispatch_barrier_sync不仅阻塞了队列的执行，也阻塞了线程的执行（尽量少用）
     */

    [self cjl_testBarrier1];
    [self cjl_testBarrier2];
}
- (void)cjl_testBarrier1{
    //串行队列使用栅栏函数

    dispatch_queue_t queue = dispatch_queue_create(&quot;CJL&quot;, DISPATCH_QUEUE_SERIAL);

    NSLog(@&quot;开始 - %@&quot;, [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@&quot;延迟2s的任务1 - %@&quot;, [NSThread currentThread]);
    });
    NSLog(@&quot;第一次结束 - %@&quot;, [NSThread currentThread]);

    //栅栏函数的作用是将队列中的任务进行分组，所以我们只要关注任务1、任务2
    dispatch_barrier_async(queue, ^{
        NSLog(@&quot;------------栅栏任务------------%@&quot;, [NSThread currentThread]);
    });
    NSLog(@&quot;栅栏结束 - %@&quot;, [NSThread currentThread]);

    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@&quot;延迟2s的任务2 - %@&quot;, [NSThread currentThread]);
    });
    NSLog(@&quot;第二次结束 - %@&quot;, [NSThread currentThread]);
}
- (void)cjl_testBarrier2{
    //并发队列使用栅栏函数

    dispatch_queue_t queue = dispatch_queue_create(&quot;CJL&quot;, DISPATCH_QUEUE_CONCURRENT);

    NSLog(@&quot;开始 - %@&quot;, [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@&quot;延迟2s的任务1 - %@&quot;, [NSThread currentThread]);
    });
    NSLog(@&quot;第一次结束 - %@&quot;, [NSThread currentThread]);

    //由于并发队列异步执行任务是乱序执行完毕的，所以使用栅栏函数可以很好的控制队列内任务执行的顺序
    dispatch_barrier_async(queue, ^{
        NSLog(@&quot;------------栅栏任务------------%@&quot;, [NSThread currentThread]);
    });
    NSLog(@&quot;栅栏结束 - %@&quot;, [NSThread currentThread]);

    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@&quot;延迟2s的任务2 - %@&quot;, [NSThread currentThread]);
    });
    NSLog(@&quot;第二次结束 - %@&quot;, [NSThread currentThread]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;栅栏函数最典型的用途就是实现读写安全：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dispatch_queue_t rwQueue = dispatch_queue_create(&quot;com.example.rwQueue&quot;, DISPATCH_QUEUE_CONCURRENT);
NSMutableArray *array = [NSMutableArray array];

// 写操作：用 barrier 独占执行
dispatch_barrier_async(rwQueue, ^{
    [array addObject:@&quot;数据&quot;];
    NSLog(@&quot;写入完成&quot;);
});

// 读操作：可以并发执行
dispatch_async(rwQueue, ^{
    NSLog(@&quot;读取数据1：%@&quot;, array);
});
dispatch_async(rwQueue, ^{
    NSLog(@&quot;读取数据2：%@&quot;, array);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读操作可以并发，提高性能。写操作被barrier独占，保证数据安全&lt;/p&gt;
&lt;h4&gt;dispatch_source_t&lt;/h4&gt;
&lt;p&gt;暂时无法在飞书文档外展示此内容&lt;/p&gt;
&lt;p&gt;GCD 中&lt;strong&gt;最底层、最强大的系统事件处理机制之一&lt;/strong&gt;，被称为 &lt;strong&gt;“事件源 (Dispatch Source)”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它可以用来监听 系统底层事件（如文件变化、计时器、进程、信号、网络、I/O 等）。&lt;/p&gt;
&lt;p&gt;主要用于计时操作，其原因是因为他创建的timer不依赖于RunLoop，且即使精准度比NSTimer高&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)cjl_testSource{
    /*
     dispatch_source

     应用场景：GCDTimer
     在iOS开发中一般使用NSTimer来处理定时逻辑，但NSTimer是依赖Runloop的，而Runloop可以运行在不同的模式下。如果NSTimer添加在一种模式下，当Runloop运行在其他模式下的时候，定时器就挂机了；又如果Runloop在阻塞状态，NSTimer触发时间就会推迟到下一个Runloop周期。因此NSTimer在计时上会有误差，并不是特别精确，而GCD定时器不依赖Runloop，计时精度要高很多

     dispatch_source是一种基本的数据类型，可以用来监听一些底层的系统事件
        - Timer Dispatch Source：定时器事件源，用来生成周期性的通知或回调
        - Signal Dispatch Source：监听信号事件源，当有UNIX信号发生时会通知
        - Descriptor Dispatch Source：监听文件或socket事件源，当文件或socket数据发生变化时会通知
        - Process Dispatch Source：监听进程事件源，与进程相关的事件通知
        - Mach port Dispatch Source：监听Mach端口事件源
        - Custom Dispatch Source：监听自定义事件源

     主要使用的API：
        - dispatch_source_create: 创建事件源
        - dispatch_source_set_event_handler: 设置数据源回调
        - dispatch_source_merge_data: 设置事件源数据
        - dispatch_source_get_data： 获取事件源数据
        - dispatch_resume: 继续
        - dispatch_suspend: 挂起
        - dispatch_cancle: 取消
     */

    //1.创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //2.创建timer
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //3.设置timer首次执行时间，间隔，精确度
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0*NSEC_PER_SEC, 0.1*NSEC_PER_SEC);
    //4.设置timer事件回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@&quot;GCDTimer&quot;);
    });
    //5.默认是挂起状态，需要手动激活
    dispatch_resume(timer);

}
#### disoatch_suspend &amp;amp; dispatch_resume


这两个函数可以随时挂起某个队列，等需要执行的时候恢复即可


```objective-c
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_suspend(queue);
    dispatch_resume(queue);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GCD任务能取消吗？&lt;/h2&gt;
&lt;p&gt;GCD本身没有提供取消任务的API，但是可以通过操作来间接设置取消任务比如：&lt;/p&gt;
&lt;p&gt;在 block 内部添加一个布尔变量作为取消标志，然后在外部改变这个标志的值。block 在执行时可以检查这个标志是否被设置为取消状态，如果被设置，则提前结束 block 的执行。&lt;/p&gt;
&lt;p&gt;DispatchSource 可以用来监控文件描述符、定时器等事件，但也可以用于模拟任务取消。可以创建一个 DispatchSourceTimer，并在定时器触发时检查是否应该取消任务。&lt;/p&gt;
&lt;h2&gt;如何使用GCD实现一个常驻线程？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用无限循环 在一个线程中使用无限循环（如 while 循环）来持续执行任务或监听事件。为了防止线程占用过多的 CPU 资源，可以在循环中加入适当的延时。&lt;/li&gt;
&lt;li&gt;使用 dispatch_source_t dispatch_source_t 是 GCD 中一种可以用于监听事件的对象，比如定时器、文件描述符等。可以创建一个 dispatch_source_t 定时器，让它定期触发并执行任务。&lt;/li&gt;
&lt;li&gt;使用NSThread 使用NSThread创建线程并在其中执行无限循环或监听事件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;怎样利用GCD实现多读单写呢（怎么实现一个多读单写的模型）？&lt;/h2&gt;
&lt;p&gt;通过栅栏异步调用的方式，分配写操作到并发队列当中。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/154915356&quot;&gt;【iOS】多线程与GCD&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】KVO</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-154543600-ios-kvo/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-154543600-ios-kvo/</guid><description>In order to understand key value observing, you must first understand key value coding. KVC是键值编码 ，在对象创建完成后，可以 动态的给对象属性赋值 ，而 KVO是键值观察 ，提供了一种监</description><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In order to understand key-value observing, you must first understand key-value coding.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;KVC是键值编码&lt;/code&gt;，在对象创建完成后，可以&lt;code&gt;动态的给对象属性赋值&lt;/code&gt;，而&lt;code&gt;KVO是键值观察&lt;/code&gt;，提供了一种监听机制，当指定的对象的属性被修改后，则对象会收到通知，所以可以看出&lt;code&gt;KVO是基于KVC的基础上对属性动态变化的监听&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在iOS日常开发中，经常&lt;code&gt;使用KVO来监听对象属性的变化&lt;/code&gt;，并及时做出响应，即当指定的被观察的对象的属性被修改后，&lt;code&gt;KVO会自动通知相应的观察者&lt;/code&gt;，那么&lt;code&gt;KVO&lt;/code&gt;与&lt;code&gt;NSNotificatioCenter&lt;/code&gt;有什么区别呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;相同点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1、两者的实现原理都是观察者模式，都是用于监听&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2、都能实现一对多的操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不同点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1、KVO只能用于监听对象属性的变化，并且属性名都是通过NSString来查找，编译器不会帮你检测对错和补全,纯手敲会比较容易出错&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2、NSNotification的发送监听（post）的操作我们可以控制，kvo由系统控制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3、KVO可以记录新旧值变化&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;KVO使用&lt;/h2&gt;
&lt;h3&gt;1、 基本使用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;注册观察者&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[person addObserver:person forKeyPath:@&quot;age&quot; options:NSKeyValueObservingOptionNew context:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;设置KVO回调&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary&amp;lt;NSKeyValueChangeKey,id&amp;gt; *)change context:(void *)context {
  if ([keyPath isEqualToString:@&quot;age&quot;]) {
    NSLog(@&quot;KVO发送变化：old = %@, new = %@&quot;, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;移除观察者&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[person removeObserver:person forKeyPath:@&quot;age&quot;];
### 2、context使用


在官方文档中，针对`context`有以下说明

![请添加图片描述](https://i-blog.csdnimg.cn/direct/0174aa4d14744f619b8041463cbb3784.png)


大致含义就是：`addObserver：forKeyPath：options：context：`方法中的上下文context指针包含任意数据，这些数据将在相应的更改通知中传递回观察者。可以通过`指定context为NULL`，从而依靠keyPath即键路径字符串传来确定更改通知的来源，但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的keyPath创建一个不同的context，从而`完全不需要进行字符串比较`，从而可以更有效地进行通知解析


通俗的讲，context上下文主要是用于区分**不同对象的同名属性**，从而在KVO回调方法中可以直接使用context进行区分，可以`大大提升性能，以及代码的可读性`


context用来处理有多个观察者的情况：


- 同名键路径冲突：当多个对象或同一对象的不同属性使用相同的 keyPath 时，用 context 精准定位通知来源。
- 性能优化：通过指针地址直接匹配 context，避免字符串比较（keyPath 判断）的性能损耗。
- 安全性：防止父类与子类观察同一 keyPath 时的逻辑混淆。


#### context使用总结


- 不使用context，使用keyPath区分通知来源


```objective-c
//context的类型是 nullable void *，应该是NULL，而不是nil
[self.person addObserver:self forKeyPath:@&quot;nick&quot; options:NSKeyValueObservingOptionNew context:NULL];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用context区分通知来源&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//定义context
static void *PersonNickContext = &amp;amp;PersonNickContext;
static void *PersonNameContext = &amp;amp;PersonNameContext;

//注册观察者
[self.person addObserver:self forKeyPath:@&quot;nick&quot; options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@&quot;name&quot; options:NSKeyValueObservingOptionNew context:PersonNameContext];


//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary&amp;lt;NSKeyValueChangeKey,id&amp;gt; *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@&quot;%@&quot;,change);
    }else if (context == PersonNameContext){
        NSLog(@&quot;%@&quot;,change);
    }
}
### 3、移除KVO通知的必要性


![请添加图片描述](https://i-blog.csdnimg.cn/direct/330bbf68b44a493186f233a510229573.png)


删除观察者时，请记住以下几点：


- 要求被移除为观察者（如果尚未注册为观察者）会导致`NSRangeException`。您可以对`removeObserver：forKeyPath：context：`进行一次调用，以对应对`addObserver：forKeyPath：options：context：`的调用，或者，如果在您的应用中不可行，则将removeObserver：forKeyPath：context：调用在`try / catch块`内处理潜在的异常。
- `释放后，观察者不会自动将其自身移除。`被观察对象继续发送通知，而忽略了观察者的状态。但是，与发送到已释放对象的任何其他消息一样，更改通知会触发内存访问异常。因此，您可以`确保观察者在从内存中消失之前将自己删除。`
- 该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间（例如，`在init或viewDidLoad中）注册为观察者`，并在释放过程中（通常`在dealloc中）注销`，以`确保成对和有序地添加和删除消息，并确保观察者在注册之前被取消注册，从内存中释放出来。`

所以，总的来说，KVO`注册观察者 和移除观察者是需要成对出现的`，如果只注册，不移除，会出现类似`野指针的崩溃，`


崩溃的原因是，由于第一次注册`KVO观察者后没有移除`，再次进入界面，会导致第二次注册KVO观察者，导致`KVO观察的重复注册`，而且第一次的通知对象还在内存中，没有进行释放，此时接收到属性值变化的通知，会出现`找不到原有的通知对象，只能找到现有的通知对象，`即第二次KVO注册的观察者，所以导致了类似野指针的崩溃，即一直保持着一个野通知，且一直在监听


注：这里的崩溃案例是通过`单例对象`实现（崩溃有很大的几率，不是每次必现），因为单例对象在内存是常驻的，针对一般的类对象，貌似不移除也是可以的，但是为了防止线上意外，建议还是移除比较好


### 4、KVO的自动触发和手动出发


- 自动开关，返回NO，就监听不到，返回YES，表示监听


```objective-c
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;自动开关关闭的时候，可以通过&lt;code&gt;手动关闭监听&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;- (void)setName:(NSString *)name{
    //手动开关
    [self willChangeValueForKey:@&quot;name&quot;];
    _name = name;
    [self didChangeValueForKey:@&quot;name&quot;];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用手动开关的好处就是你想监听就监听，不想监听关闭即可，比自动触发更方便灵活&lt;/p&gt;
&lt;h3&gt;5、KVO观察：一对多&lt;/h3&gt;
&lt;p&gt;KVO观察中的&lt;code&gt;一对多&lt;/code&gt; ，意思是通过&lt;code&gt;注册一个KVO观察者，可以监听多个属性的变化&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;以下载进度为例，比如目前有一个需求，需要根据&lt;code&gt;总的下载量totalData&lt;/code&gt;和&lt;code&gt;当前下载量currentData&lt;/code&gt; 来计算当前的&lt;code&gt;下载进度currentProcess&lt;/code&gt;，实现有两种方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分别观察 总的下载量totalData 和当前下载量currentData 两个属性，当其中一个发生变化计算 当前下载进度currentProcess&lt;/li&gt;
&lt;li&gt;实现&lt;code&gt;keyPathsForValuesAffectingValueForKey&lt;/code&gt;方法，将两个观察合为一个观察，即&lt;code&gt;观察当前下载进度currentProcess&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//1、合二为一的观察方法
+ (NSSet&amp;lt;NSString *&amp;gt; *)keyPathsForValuesAffectingValueForKey:(NSString *)key{

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@&quot;currentProcess&quot;]) {
        NSArray *affectingKeys = @[@&quot;totalData&quot;, @&quot;currentData&quot;];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

//2、注册KVO观察
[self.person addObserver:self forKeyPath:@&quot;currentProcess&quot; options:(NSKeyValueObservingOptionNew) context:NULL];

//3、触发属性值变化
- (void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event{
    self.person.currentData += 10;
    self.person.totalData  += 1;
}

//4、移除观察者
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@&quot;currentProcess&quot;];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;解释一下上面这段代码：&lt;/strong&gt; 我们注册的监听是currentProcess，但在触摸事件里改动的却是 currentData和totalData， 也就是说，你没有直接改 currentProcess， 但是你希望当这两个值（currentData 和 totalData）变化时， currentProcess 的观察回调也会自动触发。而这就是 keyPathsForValuesAffectingValueForKey: 的作用！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实还有另一种方法，借助ai的回答如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/3591ab4748eb485aacaf02c7c5ec678c.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6、KVO观察可变数组&lt;/h3&gt;
&lt;p&gt;KVO是基于KVC基础之上的，所以可变数组如果直接添加数据，是不会调用&lt;code&gt;setter方法&lt;/code&gt;的，所有&lt;code&gt;对可变数组的KVO观察下面这种方式不生效&lt;/code&gt;的,即直接通过&lt;code&gt;[self.person.dateArray addObject:@&quot;1&quot;];&lt;/code&gt;向数组添加元素，是不会触发kvo通知回调的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//1、注册可变数组KVO观察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@&quot;dateArray&quot; options:(NSKeyValueObservingOptionNew) context:NULL];

//2、KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary&amp;lt;NSKeyValueChangeKey,id&amp;gt; *)change context:(void *)context{
    NSLog(@&quot;%@&quot;,change);
}

//3、移除观察者
- (void)dealloc{
 [self.person removeObserver:self forKeyPath:@&quot;dateArray&quot;];
}

//4、触发数组添加数据
- (void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event{
    [self.person.dateArray addObject:@&quot;1&quot;];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在KVC官方文档中，针对&lt;code&gt;可变数组的集合类型，&lt;/code&gt;有如下说明，即访问集合对象需要需要通过&lt;code&gt;mutableArrayValueForKey&lt;/code&gt;方法，这样才能将元素添加到可变数组中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/8b655f8a30814a498995b8a8f39d15d7.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;修改代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.person mutableArrayValueForKey:@&quot;dateArray&quot;] addObject:@&quot;1&quot;];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以实现了。&lt;/p&gt;
&lt;p&gt;打印出来的kind是一个枚举类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//设值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替换
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mutableArrayValueForKey&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤 1：&lt;/strong&gt; 优先查找数组操作方法&lt;/p&gt;
&lt;p&gt;方法名规则：&lt;/p&gt;
&lt;p&gt;在对象的类中搜索以下方法（优先级顺序）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;insertObject:inAtIndex: 和 removeObjectFromAtIndex:（对应 NSMutableArray 的基础增删方法）&lt;/li&gt;
&lt;li&gt;insert:atIndexes: 和 removeAtIndexes:&lt;/li&gt;
&lt;li&gt;replaceObjectInAtIndex:withObject: 或 replaceAtIndexes:with:（高性能替换方法）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;触发条件：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;若类实现了至少 一个插入方法 和 一个删除方法，则所有 NSMutableArray 操作（如 addObject:、removeLastObject）都会被 自动映射到这些自定义方法，确保数据同步和 KVO 通知。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤 2：&lt;/strong&gt; 退而使用 Setter 方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法名规则： 若未找到数组操作方法，则查找 set: 方法。&lt;/li&gt;
&lt;li&gt;触发条件： 每次通过代理对象修改数组时，会 生成新数组 并调用 set: 方法 更新原属性。 性能问题：频繁生成新数组会导致性能损耗（需优先实现步骤 1 的方法优化）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;步骤 3：&lt;/strong&gt; 直接访问实例变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量名规则： 若 accessInstanceVariablesDirectly 返回 YES，则按顺序查找实例变量 _ 或 。&lt;/li&gt;
&lt;li&gt;触发条件： 代理对象直接操作实例变量（必须是 &lt;code&gt;NSMutableArray 或其子类实例&lt;/code&gt;），修改会 直接影响原数据 并触发 KVO 通知。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;步骤 4：&lt;/strong&gt; 兜底异常处理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;触发条件： 若上述方法均未找到，返回一个代理对象，但其操作会调用 setValue:forUndefinedKey:。&lt;/li&gt;
&lt;li&gt;默认行为： 抛出 NSUndefinedKeyException 异常。可通过重写 setValue:forUndefinedKey: 自定义处理逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用一句话理解，mutableArrayValueForKey: 就是给数组加了一个 KVO 代理壳，让你对数组做增删改操作时，系统能够自动触发观察者通知，并且按优先级选择最优的触发方式。&lt;/p&gt;
&lt;h2&gt;KVO底层的一些结论&lt;/h2&gt;
&lt;p&gt;KVO对&lt;code&gt;成员变量&lt;/code&gt;不观察，只对&lt;code&gt;属性&lt;/code&gt;观察，属性和成员变量的区别在于&lt;code&gt;属性多一个 setter 方法，而KVO恰好观察的是setter 方法&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在注册观察者后，实例对象的&lt;code&gt;isa指针&lt;/code&gt;指向由&lt;code&gt;LGPerson类&lt;/code&gt;变为了&lt;code&gt;NSKVONotifying_LGPerson中间类&lt;/code&gt;，即实例对象的isa指针指向发生了变化&lt;/p&gt;
&lt;p&gt;关于&lt;code&gt;中间类&lt;/code&gt;，有如下说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;实例对象isa的指向在注册KVO观察者之后，由&lt;code&gt;原有类更改为指向中间类&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中间类重写了观察属性的&lt;code&gt;setter方法、class、dealloc、_isKVOA&lt;/code&gt;方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dealloc方法中，移除KVO观察者之后，实例对象&lt;code&gt;isa指向由中间类更改为原有类&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中间类从创建后，就一直存在内存中，不会被销毁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自定义KVO大致分为以下几步&lt;/p&gt;
&lt;p&gt;注册观察者 &amp;amp; 响应&lt;/p&gt;
&lt;p&gt;1、验证是否存在setter方法&lt;/p&gt;
&lt;p&gt;2、保存信息&lt;/p&gt;
&lt;p&gt;3、动态生成子类，需要重写class、setter方法&lt;/p&gt;
&lt;p&gt;4、在子类的setter方法中向父类发消息，即自定义消息发送&lt;/p&gt;
&lt;p&gt;5、让观察者响应&lt;/p&gt;
&lt;p&gt;移除观察者&lt;/p&gt;
&lt;p&gt;1、更改isa指向为原有类&lt;/p&gt;
&lt;p&gt;2、重写子类的dealloc方法&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/154543600&quot;&gt;【iOS】KVO&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】内存五大分区</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-154609757-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-154609757-ios-/</guid><description>栈区 定义 栈是一种数据结构，其对应的 进程或线程是唯一的 栈是一种 向低地址拓展 的数据结构 栈是一块 连续的内存 ， 遵循先进后出原则 栈道地址空间在iOS中 以0X7 开头 栈区一般在运行时分配 存储 栈区是由 编译器自动分配并释放 的，主要用来存储 局部变量 函数的参数，</description><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/85222a7e59004bb896e8a5705382360b.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;栈区&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈是一种数据结构，其对应的&lt;code&gt;进程或线程是唯一的&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;栈是一种&lt;code&gt;向低地址拓展&lt;/code&gt;的数据结构&lt;/li&gt;
&lt;li&gt;栈是一块&lt;code&gt;连续的内存&lt;/code&gt;， 遵循先进后出原则&lt;/li&gt;
&lt;li&gt;栈道地址空间在iOS中&lt;code&gt;以0X7&lt;/code&gt;开头&lt;/li&gt;
&lt;li&gt;栈区一般在运行时分配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;存储&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;栈区是由&lt;code&gt;编译器自动分配并释放&lt;/code&gt;的，主要用来存储&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部变量&lt;/li&gt;
&lt;li&gt;函数的参数，例如函数的隐藏参数（id self，SEL _cmd）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：因为栈是由编译器自动分配并释放的，不会产生内存碎片，所以快速高效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：栈的内存大小有限制，数据不灵活&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;iOS主线程栈大小是1MB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他线程是512KB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MAC只有8M&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;堆区（Heap）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆是&lt;code&gt;向高地址扩展&lt;/code&gt;的数据结构&lt;/li&gt;
&lt;li&gt;堆是&lt;code&gt;不连续的内存区域&lt;/code&gt;，类似于链表结构（便于增删，不便于查询），遵循先进先出（FIFO）原则&lt;/li&gt;
&lt;li&gt;堆的地址空间在iOS中是以&lt;code&gt;0x6开头&lt;/code&gt;，其空间的分配总是动态的&lt;/li&gt;
&lt;li&gt;堆区的分配一般是在&lt;code&gt;运行时&lt;/code&gt;分配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;存储&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆区是由程序员动态分配和释放的，如果程序员不释放，程序结束后，可能由操作系统回收，主要用于存放&lt;/li&gt;
&lt;li&gt;OC中&lt;code&gt;使用alloc或者 使用new开辟空间创建对象&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C语言中&lt;code&gt;使用malloc、calloc、realloc分配的空间，需要free释放&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：灵活方便，数据适应面广泛&lt;/li&gt;
&lt;li&gt;缺点：需&lt;code&gt;手动管理，速度慢、容易产生内存碎片&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当需要访问堆中内存时，一般需要先通过对象&lt;code&gt;读取到栈区的指针地址&lt;/code&gt;，然后通过指针地址访问堆区&lt;/p&gt;
&lt;h2&gt;全局区（静态区，即.bss &amp;amp; .data）&lt;/h2&gt;
&lt;p&gt;全局区是&lt;code&gt;编译时分配的内存空间&lt;/code&gt;，在iOS中一般以&lt;code&gt;0x1&lt;/code&gt;开头，在程序运行过程中，此内存中的数据一直存在，程序结束后由系统释放，主要存放&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未初始化的全局变量和静态变量，即BSS区（.bss）&lt;/li&gt;
&lt;li&gt;已初始化的全局变量和静态变量，即数据区（.data）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，全局变量是指变量值可以在&lt;code&gt;运行时被动态修改&lt;/code&gt;，而静态变量是static修饰的变量，包含静态局部变量和静态全局变量&lt;/p&gt;
&lt;h2&gt;常量区（即.rodata）&lt;/h2&gt;
&lt;p&gt;常量区是	&lt;code&gt;编译时分配的内存空间&lt;/code&gt;，在程序结束后由系统释放，主要存放&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经使用了的，且没有指向的&lt;code&gt;字符串常量&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;字符串常量因为可能在程序中被多次使用，所以`在程序运行之前就会提前分配内存&lt;/p&gt;
&lt;h2&gt;代码区（即.text）&lt;/h2&gt;
&lt;p&gt;代码区是	&lt;code&gt;编译时分配&lt;/code&gt;主要用于&lt;code&gt;存放程序运行时的代码&lt;/code&gt;,代码会被&lt;code&gt;编译成二进制&lt;/code&gt;存进内存的&lt;/p&gt;
&lt;h2&gt;函数栈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;函数栈又称为栈区，在内存中从高地址往低地址分配，与堆区相对，具体图示请查看文章最开始的图示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;栈帧是指函数（运行中且未完成）占用的一块独立的连续内存区域&lt;/code&gt; &lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9a0c14d8e7db471f94b7d761adfcd8c0.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/li&gt;
&lt;li&gt;应用中新创建的&lt;code&gt;每个线程都有专用的栈空间&lt;/code&gt;，栈可以在线程期间自由使用。而线程中有千千万万的函数调用，这些函数共享进程的这个&lt;code&gt;栈空间&lt;/code&gt;。&lt;code&gt;每个函数所使用的栈空间是一个栈帧，所有的栈帧就组成了这个线程完整的栈&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;函数调用是发生在&lt;code&gt;栈上&lt;/code&gt;的，每个&lt;code&gt;函数的相关信息&lt;/code&gt;（例如局部变量、调用记录等）都&lt;code&gt;存储在一个栈帧&lt;/code&gt;中，每执行一次函数调用，就会生成一个与其相关的栈帧，然后将其&lt;code&gt;栈帧压入函数栈&lt;/code&gt;，而当函数&lt;code&gt;执行结束&lt;/code&gt;，则将此&lt;code&gt;函数对应的栈帧出栈并释放掉&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如下图所示，是经典图 - ARM的栈帧布局方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其中&lt;code&gt;main stack frame&lt;/code&gt;为调用函数的栈帧&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func1 stack frame&lt;/code&gt;为&lt;code&gt;当前函数(被调用者)的栈帧&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;栈底在高地址，栈向下增长。&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;FP就是&lt;code&gt;栈基址&lt;/code&gt;，它指向函数的&lt;code&gt;栈帧起始地址&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SP&lt;/code&gt;则是函数的&lt;code&gt;栈指针&lt;/code&gt;，它指向栈顶的位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;-&lt;code&gt;ARM压栈&lt;/code&gt;的顺序很是规矩(也比较容易被黑客攻破么)，依次为&lt;code&gt;当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。&lt;/code&gt;如果函数准备调用另一个函数，跳转之前临时变量区先要保存另一个函数的参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ARM也可以&lt;code&gt;用栈基址和栈指针明确标示栈帧的位置，&lt;/code&gt;栈指针SP一直移动，ARM的特点是，&lt;code&gt;两个栈空间内的地址（SP+FP）前面，必然有两个代码地址（PC+LR）明确标示着调用函数位置内的某个地址。&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;堆栈溢出&lt;/h3&gt;
&lt;p&gt;一般情况下应用程序是不需要考虑堆和栈的大小的，但是事实上堆和栈&lt;code&gt;都不是无上限的&lt;/code&gt;，&lt;code&gt;过多的递归会导致栈溢出，过多的alloc变量会导致堆溢出。&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以预防堆栈溢出的方法：&lt;/p&gt;
&lt;p&gt;（1）避免层次过深的递归调用；&lt;/p&gt;
&lt;p&gt;（2）&lt;code&gt;不要使用过多的局部变量，&lt;/code&gt;控制局部变量的大小；&lt;/p&gt;
&lt;p&gt;（3）避免分配占用空间太大的对象，并&lt;code&gt;及时释放&lt;/code&gt;;&lt;/p&gt;
&lt;p&gt;（4）实在不行，适当的情景下&lt;code&gt;调用系统API修改线程的堆栈大小；&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/154609757&quot;&gt;【iOS】内存五大分区&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】alloc、init、new</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-152164860-ios-alloc-init-new/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-152164860-ios-alloc-init-new/</guid><description>1. alloc 进入到alloc的源码里面，我们发现alloc调用了 objc rootAlloc方法，而 objc rootAlloc调用了callAlloc方法。 具体内容本人暂时也一知半解，贴一张图 alloc为我们创建了1个对象并申请了一块不小于16字节的内存空间 具体</description><pubDate>Tue, 14 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. alloc&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/a285d485763b40cf99f4f13dbeb7fc5e.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进入到alloc的源码里面，我们发现alloc调用了_objc_rootAlloc方法，而_objc_rootAlloc调用了callAlloc方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ (id)alloc {
    return _objc_rootAlloc(self);
}

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil &amp;amp;&amp;amp; !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls-&amp;gt;ISA()-&amp;gt;hasCustomAWZ())) {
        if (fastpath(cls-&amp;gt;canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls-&amp;gt;hasCxxDtor();
            id obj = (id)calloc(1, cls-&amp;gt;bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj-&amp;gt;initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体内容本人暂时也一知半解，贴一张图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/f5df660fc9de46269a9e8cbdb8e12a46.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;alloc为我们创建了1个对象并申请了一块不小于16字节的内存空间&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体可以参考一下这篇博客
&lt;a href=&quot;https://juejin.cn/post/7063036829972299813&quot;&gt;博客&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2. init&lt;/h2&gt;
&lt;p&gt;我们进入init的方法源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;额的天呐，init啥都没做，只是把当前的对象返回了。既然啥都没做那我们还需要调用init吗？答案是肯定的，其实init就是一个工厂范式，方便开发者自行重写定义。&lt;/p&gt;
&lt;p&gt;我们在来看看new方法做了啥。&lt;/p&gt;
&lt;h2&gt;3. init&lt;/h2&gt;
&lt;p&gt;我们再看看init的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;init调用的是callAlloc的方法和init，那么可以理解为new实际上是alloc + init的综合体。&lt;/p&gt;
&lt;h2&gt;4. 内存对齐&lt;/h2&gt;
&lt;h3&gt;内存字节对齐原则&lt;/h3&gt;
&lt;p&gt;在解释为什么需要16字节对齐之前，首先需要了解内存字节对齐的原则，主要有以下三点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据成员对齐规则：struct 或者 union 的数据成员，第一个数据成员放在offset为0的地方，以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小（只要该成员有子成员，比如数据、结构体等）的整数倍开始（例如int在32位机中是4字节，则要从4的整数倍地址开始存储）&lt;/li&gt;
&lt;li&gt;数据成员为结构体：如果一个结构里有某些结构体成员，则结构体成员要从其内部最大元素大小的整数倍地址开始存储（例如：struct a里面存有struct b，b里面有char、int、double等元素，则b应该从8的整数倍开始存储）&lt;/li&gt;
&lt;li&gt;结构体的整体对齐规则：结构体的总大小，即sizeof的结果，必须是其内部做大成员的整数倍，不足的要补齐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么需要16字节对齐&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;需要字节对齐的原因，有以下几点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常内存是由一个个字节组成的，cpu在存取数据时，并不是以字节为单位存储，而是以块为单位存取，块的大小为内存存取力度。频繁存取字节未对齐的数据，会极大降低cpu的性能，所以&lt;strong&gt;可以通过减少存取次数来降低cpu的开销&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;16字节对齐，是由于在一个对象中，第一个属性isa占8字节，当然一个对象肯定还有其他属性，当无属性时，会预留8字节，即16字节对齐，如果不预留，相当于这个对象的isa和其他对象的isa紧挨着，容易造成访问混乱&lt;/li&gt;
&lt;li&gt;16字节对齐后，可以 加快CPU读取速度，同时使访问更安全 ，不会产生访问混乱的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;字节对齐-总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在字节对齐算法中，对齐的主要是对象，而对象的本质则是一个 struct objc_object的结构体，&lt;/li&gt;
&lt;li&gt;结构体在内存中是连续存放的，所以可以利用这点对结构体进行强转。&lt;/li&gt;
&lt;li&gt;苹果早期是8字节对齐，现在是16字节对齐&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;alloc创建了对象并且申请了一块不少于16字节的内存空间。&lt;/li&gt;
&lt;li&gt;init其实什么也没做，返回了当前的对象。其作用在于提供一个范式，方便开发者自定义。&lt;/li&gt;
&lt;li&gt;new其实是alloc+init的一个综合体。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/152164860&quot;&gt;【iOS】alloc、init、new&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】KVC总结</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-152734765-ios-kvc/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-152734765-ios-kvc/</guid><description>什么是KVC KVC 的全称是Key Value Coding（键值编码），是由NSKeyValueCoding非正式协议启用的一种机制，对象采用这种机制来提供对其属性的间接访问，可以通过字符串来访问一个对象的成员变量或其关联的存取方法（getter or setter）。 通常</description><pubDate>Fri, 10 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/11e9cd845d314cb49fdadd68fbd3ecfa.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;什么是KVC&lt;/h2&gt;
&lt;p&gt;KVC 的全称是Key-Value Coding（键值编码），是由NSKeyValueCoding非正式协议启用的一种机制，对象采用这种机制来提供对其属性的间接访问，可以通过字符串来访问一个对象的成员变量或其关联的存取方法（getter or setter）。&lt;/p&gt;
&lt;p&gt;通常，我们可以直接通过存取方法或变量名来访问对象的属性。我们也可以使用KVC间接访问对象的属性，并且KVC还可以访问私有变量。某些情况下，KVC还可以帮助简化代码。&lt;/p&gt;
&lt;p&gt;KVC是许多其他 Cocoa 技术的基础概念，比如 &lt;strong&gt;KVO、Cocoa bindings、Core Data、AppleScript-ability&lt;/strong&gt; 等等。&lt;/p&gt;
&lt;h2&gt;KVC常见API&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;常用方法：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主要有以下四个常用的方法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过 key 设值/取值&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;key和keyPath的区别&lt;/strong&gt;： key用于访问单一属性，一层，访问对象的直接属性 keyPath用于访问嵌套属性，多层，访问嵌套对象的属性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;通过 keyPath （即路由）设值/取值&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;

//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;其他方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//默认返回YES，表示如果没有找到Set&amp;lt;Key&amp;gt;方法的话，会按照_key，_iskey，key，iskey的顺序搜索成员，设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供属性值正确性验证的API，它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//这是集合操作的API，里面还有一系列这样的API，如果属性是一个NSMutableArray，那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

//如果Key不存在，且KVC无法搜索到任何和Key有关的字段或者属性，则会调用这个方法，默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样，但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时面给Value传nil，则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

//输入一组key,返回该组key对应的Value，再转成字典返回，用于将Model转到字典。
- (NSDictionary&amp;lt;NSString *, id&amp;gt; *)dictionaryWithValuesForKeys:(NSArray&amp;lt;NSString *&amp;gt; *)keys;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见操作&lt;/h2&gt;
&lt;h3&gt;key&lt;/h3&gt;
&lt;p&gt;如下声明了一个BankAccount类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray&amp;lt; Transaction* &amp;gt;* transactions; // A to-many relation
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于BankAccount的实例对象myAccout。我们可以使用setter方法为current属性赋值，这是直接的，但是缺乏灵活性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[myAccount setCurrentBalance:@(100.0)];

//听过KVC间接为currentBalance属性赋值，通过其键key设置
[myAccount setValue:@(100.0) forKey:@&quot;currentBalance&quot;];
### KeyPath


KVC还支持 多级访问 ，keyPath用法和点语法相同。例如：我们想对myAccount的owner属性的address属性的street赋值，其keyPath为**owner.address.street**.


```objective-c
[myAccount setValue:@&quot;地址&quot; forKeyPath:@&quot;owner.address.street&quot;];
### 多值操作


给定一组key，获得一组value，以字典形式返回。该方法为数组中每个Key调用 valueForKey：  方法


```objective-c
- (NSDictionary&amp;lt;NSString *,id&amp;gt; *)dictionaryWithValuesForKeys:(NSArray&amp;lt;NSString *&amp;gt; *)keys;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将指定字典中的值设置到消息接收者的属性中，使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ，会根据需要用nil替换NSNull对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)setValuesForKeysWithDictionary:(NSDictionary&amp;lt;NSString *,id&amp;gt; *)keyedValues;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能还是有同学不明白。我再来解释一下：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dictionaryWithValuesforKeys&lt;/strong&gt; 是将对象转化为字典。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person *p = [[Person alloc] init];
p.name = @&quot;吴桐&quot;;
p.age = 19;
p.city = @&quot;西安&quot;;

NSDictionary *info = [p dictionaryWithValuesForKeys:@[@&quot;name&quot;, @&quot;age&quot;]];
NSLog(@&quot;%@&quot;, info);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你传入了一个键名数组，方法会按照顺序读取对象对应的属性值。&lt;/p&gt;
&lt;p&gt;返回的字典中：key是字符串（属性名），value是属性的值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;setValuesForKeysWithDictionary&lt;/strong&gt; 是将字典转化为对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSDictionary *dict = @{
    @&quot;name&quot;: @&quot;吴桐&quot;,
    @&quot;age&quot;: @19,
    @&quot;city&quot;: @&quot;西安&quot;
};

Person *p = [[Person alloc] init];
[p setValuesForKeysWithDictionary:dict];

NSLog(@&quot;%@ - %ld - %@&quot;, p.name, (long)p.age, p.city);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统会遍历dict的所有键，调用setValue：forKey：给对象属性赋值，只要字典的键名与属性名匹配，就能成功设置。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;dictionaryWithValuesForKeys:&lt;/strong&gt; 是“导出”对象属性到字典。 &lt;strong&gt;setValuesForKeysWithDictionary:&lt;/strong&gt; 是“导入”字典数据到对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;二者方向相反，但底层都是通过 &lt;strong&gt;KVC 的 forKey: / forKeyPath:&lt;/strong&gt; 实现的. 在开发中： 从网络获取 JSON 并转模型 [p setValuesForKeysWithDictionary:jsonDict]; 将模型转为字典方便保存/上传 [p dictionaryWithValuesForKeys:@[@“name”, @“age”, @“city”]];&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;访问集合属性&lt;/h3&gt;
&lt;p&gt;我们可以像访问其它对象一样使用 valueForKey:  或 setValue:forKey:  方法来&lt;strong&gt;获取或设置&lt;/strong&gt;集合对象（主要指NSArray和NSSet）。但是，当我们要操作集合对象的内容，比如&lt;strong&gt;添加或者删除&lt;/strong&gt;元素时，通过KVC的可变代理方法获取集合代理对象是最有效的。&lt;/p&gt;
&lt;p&gt;根据KVO的实现原理，是在运行时动态生成子类并 重写setter  方法来达到可以通知所有观察者对象的目的，因此我们&lt;strong&gt;对集合对象进行操作是不会触发KVO的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当我们要使用KVO监听集合对象变化时，需要&lt;strong&gt;通过KVC的可变代理方法获取集合代理对象，然后对代理对象进行操作。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当代理对象的&lt;strong&gt;内部对象发生改变时，会触发KVO的监听方法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KVC提供了三种不同的代理对象访问的代理方法，每种都有Key和KeyPath两种方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;mutableArrayValueForKey:&lt;/strong&gt;&lt;/em&gt; 和 &lt;em&gt;&lt;strong&gt;mutableArrayValueForKeyPath:&lt;/strong&gt;&lt;/em&gt; 返回NSMutableArray对象的代理对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;mutableSetValueForKey:&lt;/strong&gt;&lt;/em&gt; 和 &lt;em&gt;&lt;strong&gt;mutableSetValueForKeyPath:&lt;/strong&gt;&lt;/em&gt; 返回NSMutableSet对象的代理对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;mutableOrderedSetValueForKey:&lt;/strong&gt;&lt;/em&gt; 和 &lt;em&gt;&lt;strong&gt;mutableOrderedSetValueForKeyPath:&lt;/strong&gt;&lt;/em&gt; 返回NSMutableOrderedSet对象的代理对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这些返回的不是实际集合本身，而是一个特殊的“代理对象”：&lt;/p&gt;
&lt;p&gt;•	当你在这个代理对象上调用 addObject:、removeObject: 等操作时，&lt;/p&gt;
&lt;p&gt;它内部会自动帮你调用&lt;/p&gt;
&lt;p&gt;willChangeValueForKey: 和 didChangeValueForKey:，&lt;/p&gt;
&lt;p&gt;让 KVO 能检测到变化。&lt;/p&gt;
&lt;h4&gt;举个例子：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@interface Person : NSObject
@property (nonatomic, strong) NSMutableArray *friends;
@end

@implementation Person
- (instancetype)init {
    if (self = [super init]) {
        _friends = [NSMutableArray array];
    }
    return self;
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我们设置观察者：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person *p = [[Person alloc] init];
[p addObserver:self
    forKeyPath:@&quot;friends&quot;
       options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
       context:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如果你直接修改属性如下，则不会触发KVO回调。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[p.friends addObject : @&quot;Tommy&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如果通过如下的代理修改，就会触发观察者回调：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSMutableArray *proxyArray = [p mutableArrayValueForKey:@&quot;friends&quot;];
[proxyArray addObject:@&quot;Tommy&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;输出：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;friends changed:
old = ()
new = (Tommy)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为这个proxyArray是一个特殊的“代理集合”，在修改前后自动调用&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[p willChangeValueForKey:@“friends”]; [p didChangeValueForKey:@“friends”];&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从而通知所有观察者。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+--------------------------+
|  Person (被观察者)       |
|  friends = NSMutableArray|
+--------------------------+
            ▲
            | 调用 mutableArrayValueForKey:
            |
   返回一个代理对象 (NSKeyValueMutableArray)
            |
     当你调用 add/remove 等操作时
            ↓
 willChangeValueForKey:@&quot;friends&quot;
 [friends addObject:...]
 didChangeValueForKey:@&quot;friends&quot;
            ↓
       通知 KVO 观察者
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用集合运算符&lt;/h2&gt;
&lt;p&gt;KVC 支持对集合类型（NSArray、NSSet）进行&lt;strong&gt;统计和聚合计算&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;比如，我们有一个学生数组，每个学生都有 age 属性。&lt;/p&gt;
&lt;p&gt;如果想算平均年龄、最大年龄、最小年龄等，用传统方式要写循环，但用 KVC 可以一句话搞定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSNumber *avg = [students valueForKeyPath:@&quot;@avg.age&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下是 KeyPath 集合运算符的格式，主要分为三个部分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Left key Path：左键路径，要操作的集合对象，如果消息接受者就是集合对象，则可以省略left部分&lt;/li&gt;
&lt;li&gt;Collection operator：集合运算符；&lt;/li&gt;
&lt;li&gt;Right key Path：右键路径，要进行运算的集合中的属性 &lt;img src=&quot;https://i-blog.csdnimg.cn/direct/db6cfa57a3874312adf3992487635faf.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[left key path].@运算符.right key path&lt;/strong&gt; &lt;strong&gt;[students valueForKeyPath:@“@avg.score”];&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;集合运算符主要分为三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;① 聚合运算符：以某种方式合并集合中的对象，并返回右键路径中指定的属性的数据类型匹配的一个对象，一般返回 NSNumber 实例。&lt;/li&gt;
&lt;li&gt;② 数组运算符：根据运算符的条件，将符合条件的对象以一个 NSArray 实例返回。&lt;/li&gt;
&lt;li&gt;③ 嵌套运算符：处理集合对象中嵌套其他集合对象的情况，并根据运算符返回一个 NSArray或NSSet 实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;聚合运算符&lt;/h3&gt;
&lt;h4&gt;@avg&lt;/h4&gt;
&lt;p&gt;读取集合中每个元素的右键路径指定的属性，将其转换为double类型 (nil用 0 替代)，并计算这些值的算术平均值。然后将结果以NSNumber实例返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 计算上表中 amount 的平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@&quot;@avg.amount&quot;];
// transactionAverage 格式化的结果为 $ 456.54。
#### @count


计算集合中的元素个数，以NSNumber实例返回。


```objective-c
// 计算 transactions 集合中的元素个数。
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@&quot;@count&quot;];
// numberOfTransactions 的值为 13。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;备注&lt;/strong&gt; ：@count运算符比较特别，它不需要写右键路径，即使写了也会被忽略&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;@sum&lt;/h4&gt;
&lt;p&gt;读取集合中每个元素的右键路径制定的属性，将其转化为 double 类型（nil用0代替），并计算这些值的总和。然后将结果以NSNumber实例返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 计算上表中 amount 的总和。
NSNumber *amountSum = [self.transactions valueForKeyPath:@&quot;@sum.amount&quot;];
// amountSum 的结果为 $ 5935.00。
#### @max / @min


**返回集合中右键路径制定的属性的最小值或最大值**


```objective-c
// 获取日期的最大值。
NSDate *latestDate = [self.transactions valueForKeyPath:@&quot;@max.date&quot;];
// latestDate 的值为 Jul 15, 2016.


// 获取日期的最小值。
NSDate *earliestDate = [self.transactions valueForKeyPath:@&quot;@min.date&quot;];
// earliestDate 的值为 Dec 1, 2015.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;备注：&lt;/strong&gt;@max和@min根据右键路径指定的属性在集合中搜索，搜索使用compare:方法进行比较，许多基础类 (如NSNumber类) 中都有定义。因此，右键路径指定的属性必须能响应compare:消息。搜索忽略值为nil的集合项。可以通过重写compare:方法对搜索过程进行控制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;数组运算符&lt;/h3&gt;
&lt;p&gt;根据符合运算符的条件，将符合条件的对象以一个 NSArray 实例返回。&lt;/p&gt;
&lt;h4&gt;@unionOfObjects&lt;/h4&gt;
&lt;p&gt;读取数组中每个元素的右键路径指定的属性，反在一个 NSArray 实例中并返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取集合中的所有 payee 对象。
NSArray *payees = [self.transactions valueForKeyPath:@&quot;@unionOfObjects.payee&quot;];
// payees 数组包含以下字符串：Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital。
#### @distinctUnionOfObjects


读取数组中每个元素的右键路径指定的属性，放在一个NSArray实例中，将数组进行去重后返回。


```objective-c
// 获取集合中的所有不同的 payee 对象。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@&quot;@distinctUnionOfObjects.payee&quot;];
// distinctPayees 数组包含以下字符串：Car Loan, General Cable, Animal Hospital, Green Power, Mortgage。
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 在使用数组运算符时，如果有任何操作的对象为nil，则valueForKeyPath:方法将引发异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;嵌套运算符&lt;/h3&gt;
&lt;p&gt;处理集合对象中嵌套其他集合对象的情况，并根据运算符返回一个 NSArray 或 NSSet 实例。&lt;/p&gt;
&lt;p&gt;如下 moreTransactions 是装着 transaction 对象的数组，arrayOfArrays 数组中嵌套了 self.transactions 和 moreTransactions 两个数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSArray* moreTransactions = @[&amp;lt;# transaction data #&amp;gt;];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
#### @unionOfArrays


读取集合中的每个集合中的每个元素的右键路径的制定的属性，放在一个NSArray实例中返回。


```objective-c
// 获取 arrayOfArrays 集合中的每个集合中的所有 payee 对象。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@&quot;@unionOfArrays.payee&quot;];
// collectedPayees 数组包含以下字符串：Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop.
#### distinctUnionOfArrays


读取集合中的每个集合中的每个元素的右键路径制定的属性，放在一个 NSArray 实例中，将数组去重后返回，用法与上相似。


#### @distinctUnionOfSets


读取集合中的每个集合中的每个元素的右键路径指定的属性，放在一个NSSet实例中，去重后返回。


```objective-c
NSSet *collectedDistinctPayees = [setOfSets valueForKeyPath:@&quot;@distinctUnionOfSets.payee&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; - 在使用嵌套运算符时，valueForKeyPath:内部会根据运算符创建一个NSMutableArray或NSMutableSet对象，将集合中的array和set添加进去再进行操作。如果集合中有非集合元素，会导致Crash。 - 使用unionOfArrays或distinctUnionOfArrays运算符，消息接收者应该是arrayOfArrays类型，即NSArray * arrayOfArrays;；使用distinctUnionOfSets运算符，消息接收者应该是setOfSets或者arrayOfSets类型。否则会发生异常。 - 在使用嵌套运算符时，如果有任何操作的对象为nil， 则valueForKeyPath:方法将引发异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;拓展：&lt;/h4&gt;
&lt;p&gt;如果集合中元素的对象都是NSNumber，右键路径可以用self。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSArray *array = @[@1, @2, @3, @4, @5];
    NSNumber *sum = [array valueForKeyPath:@&quot;@sum.self&quot;];
    NSLog(@&quot;%d&quot;,[sum intValue]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;非对象值处理&lt;/h2&gt;
&lt;p&gt;在 Objective-C 中，KVC（Key-Value Coding） 不仅能处理对象属性，还能处理**基本数据类型（primitive types）和结构体（struct）类型。&lt;/p&gt;
&lt;p&gt;KVC 在底层通过自动装箱（boxing）与自动拆箱（unboxing）**机制完成两者之间的桥接。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当进行取值如 valueForKey: 时，如果返回值非对象，会使用该值初始化一个NSNumber（用于基础数据类型）或NSValue（用于结构体）实例，然后返回该实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果属性是对象类型 → 直接返回对象； 如果属性是 基本类型**（int、float、BOOL 等）** → KVC 会自动用该值创建一个 NSNumber 实例； 如果属性是 结构体**（CGRect、CGPoint、CGSize 等）** → KVC 会自动创建一个 NSValue 实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;NSNumber *num = [person valueForKey:@&quot;age&quot;];     // age 是 int
NSValue  *val = [view valueForKey:@&quot;frame&quot;];     // frame 是 CGRect
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当进行赋值如 setValue:forKey: 时，如果key的数据类型非对象，则会发送一条Value消息给value对象以提取基础数据，然后赋值给key。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;当属性是非对象类型时： KVC 会对 value 发送对应的 Value 消息， 提取出原始基础类型数值； 然后再赋值给该属性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;例如：
[person setValue:@25 forKey:@&quot;age&quot;];

在底层相当于执行：
person.age = [@25 intValue];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为Swift中的所有属性都是对象，所以这里仅适用于Objective-C属性。&lt;/li&gt;
&lt;li&gt;当进行赋值如setValue:forKey:时，&lt;strong&gt;如果key的数据类型是非对象类型，则value就禁止传nil。&lt;/strong&gt; 否则会调用setNilValueForKey:方法，该方法的默认实现抛出异常NSInvalidArgumentException，并导致程序Crash。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;你可以通过&lt;strong&gt;重写该方法&lt;/strong&gt;来处理：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@&quot;age&quot;]) {
        [self setValue:@0 forKey:@&quot;age&quot;]; // 给默认值
    } else {
        [super setNilValueForKey:key];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下表是KVC对于基础数据类型和NSNumber对象之间的转换。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/979c0bc5c8a94dac91411f3672d548d8.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0ca7e04037644fc39a5b2fd3337efa12.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;属性验证&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;- (BOOL)validateValue:(id  _Nullable *)value
               forKey:(NSString *)key
                error:(NSError * _Nullable *)error;

- (BOOL)validateValue:(inout id  _Nullable *)ioValue
           forKeyPath:(NSString *)inKeyPath
                error:(out NSError * _Nullable *)outError;

验证 key 对应属性的值是否有效。
value：要被验证的值（是一个指针的指针 id *，所以可以修改它的内容）
key：属性名
error：如果验证失败，可返回一个 NSError 对象说明原因
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Oc的 KVC 系统中，当我们调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[person setValue:@(-5) forKey:@&quot;age&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;validateValue 方法的默认实现是查看消息接收者类中是否实现了遵循命名规则为 validate :error: 的方法，如果有的话就返回调用该方法的结果；如果没有的话，则默认验证成功并返回YES。我们可以在消息接收者类中实现validate:error:的方法来自定义逻辑返回YES或NO。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;举个例子，&lt;/strong&gt; 下面类中，实现了validateName:error:方法，来验证给name赋的是不是jack&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ViewController.m
    Person *person = [[Person alloc] init];
    NSString *value = @&quot;rose&quot;;
    NSString *key = @&quot;name&quot;;
    NSError  *error;
    BOOL result = [person validateValue:&amp;amp;value forKey:key error:&amp;amp;error];

    if (error) {
        NSLog(@&quot;error = %@&quot;, error);
        return;
    }
    NSLog(@&quot;%d&quot;,result);

// Person.m
- (BOOL)validateName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    BOOL result = NO;
    if ([name isEqualToString:@&quot;jack&quot;]) {
        result = YES;
    }
    return result;
}
// 打印：0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次调用 setValue:forKey:  或手动验证时 都会调用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;validateValue:forKey:error:&lt;/strong&gt; ，由NSObject默认实现，validate&amp;lt; Key&amp;gt;:error: 由上者间接调用需要你自己实现。&lt;/p&gt;
&lt;h2&gt;搜索规则&lt;/h2&gt;
&lt;h3&gt;set（设值）&lt;/h3&gt;
&lt;p&gt;**当调用setValue:forKey:设置属性value时，**其底层的执行流程为&lt;/p&gt;
&lt;p&gt;【第一步】首先查找是否有这三种setter方法，按照查找顺序为&lt;strong&gt;set&amp;lt; Key&amp;gt;：-&amp;gt; _set&amp;lt; Key&amp;gt; -&amp;gt; setIs&amp;lt; Key&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果有其中任意一个setter方法，则直接设置属性的value（主注意：key是指成员变量名，首字符大小写需要符合KVC的命名规范）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果都没有，则进入【第二步】&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;【第二步】：如果没有第一步中的三个简单的setter方法，则查找&lt;strong&gt;accessInstanceVariablesDirectly&lt;/strong&gt;是否返回YES，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果返回YES，则查找间接访问的实例变量进行赋值，查找顺序为：_  -&amp;gt; _is  -&amp;gt;   -&amp;gt; is&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果找到其中任意一个实例变量，则赋值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果都没有，则进入【第三步】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果返回NO，则进入【第三步】&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;【第三步】如果setter方法 或者 实例变量都没有找到，系统会执行该对象的setValue：forUndefinedKey:方法，默认抛出NSUndefinedKeyException类型的异常&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/24be9af7c7524531afcef7818796065d.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;get（取值）&lt;/h3&gt;
&lt;p&gt;当调用**valueForKey：**时，其底层的执行流程如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;【第一步】首先查找getter方法，按照 get  -&amp;gt;   -&amp;gt; is  -&amp;gt; _  的方法顺序查找，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果找到，则进入【第五步】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有找到，则进入【第二步】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;【第二步】如果第一步中的getter方法没有找到，KVC会查找 countOf  和objectIn   AtIndex :和  AtIndexes :&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果找到countOf  和其他两个中的一个，则会创建一个响应所有NSArray方法的集合代理对象，并返回该对象，即&lt;strong&gt;NSKeyValueArray&lt;/strong&gt;，是NSArray的子类。代理对象随后将接收到的所有NSArray消息转换为**countOf ，objectIn  AtIndex：和 AtIndexes：**消息的某种组合，用来创建键值编码对象。如果原始对象还实现了一个名为get ：range：之类的可选方法，则代理对象也将在适当时使用该方法（注意：方法名的命名规则要符合KVC的标准命名方法，包括方法签名。）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有找到这三个访问数组的，请继续进入【第三步】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;【第三步】如果没有找到上面的几种方法，则会同时查找&lt;strong&gt;countOf  ，enumeratorOf 和memberOf 这三个方法&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果这三个方法都找到，则会创建一个响应所有NSSet方法的集合代理对象，并返回该对象，此代理对象随后将其收到的所有NSSet消息转换为**countOf ，enumeratorOf 和memberOf ：**消息的某种组合，用于创建它的对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果还是没有找到，则进入【第四步】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;【第四步】如果还没有找到，检查类方法&lt;strong&gt;InstanceVariablesDirectly&lt;/strong&gt;是否YES，依次搜索_ ，_is ， 或is 的实例变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果搜到，直接获取实例变量的值，进入【第五步】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;【第五步】根据搜索到的属性值的类型，返回不同的结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是对象指针，则直接返回结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是NSNumber支持的标量类型，则将其存储在NSNumber实例中并返回它&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是是NSNumber不支持的标量类型，请转换为NSValue对象并返回该对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;【第六步】如果上面5步的方法均失败，系统会执行该对象的valueForUndefinedKey:方法，默认抛出NSUndefinedKeyException类型的异常&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/5b8605d222b04d5cbd6f3bf198ae7a39.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;异常处理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;① 根据KVC搜索规则，当没有搜索到对应的key或者keyPath相关方法或者变量时，会调用对应的异常方法valueForUndefinedKey:或setValue:forUndefinedKey:，这两个方法的默认实现是抛出异常NSUnknownKeyException，并导致程序Crash。我们可以重写这两个方法来处理异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;② 当进行赋值如setValue:forKey:时，如果key的数据类型是非对象类型，则value就禁止传nil。否则会调用setNilValueForKey:方法，该方法的默认实现是抛出异常NSInvalidArgumentException，并导致程序Crash。我们可以重写这个方法来处理异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@&quot;hidden&quot;]) {
        [self setValue:@(NO) forKey:@”hidden”];
    } else {
        [super setNilValueForKey:key];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见问题：&lt;/h2&gt;
&lt;h3&gt;Q：通过 KVC 修改属性会触发 KVO 吗？&lt;/h3&gt;
&lt;p&gt;会，通过KVC修改成员变量值也会触发KVO。&lt;/p&gt;
&lt;h3&gt;Q：通过 KVC 键值编码技术是否会破坏面向对象的编程方法，或者说违背面向对象的编程思想呢？&lt;/h3&gt;
&lt;p&gt;valueForKey:和setValue:forKey:这里面的key是没有任何限制的，当我们知道一个类或实例它内部的私有变量名称的情况下，我们在外界可以通过已知的key来对它的私有变量进行访问或者赋值的操作，从这个角度来讲KVC键值编码技术会违背面向对象的编程思想。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/152734765&quot;&gt;【iOS】KVC总结&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】内存管理初级</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-152130856-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-152130856-ios-/</guid><description>​ 内存管理是程序在运行时分配内存、使用内存，并在程序完成时释放内存的过程。在Objective C中，也被看作是在众多数据和代码之间分配有限内存资源的所有权(Ownership)的一种方式。 内存管理关心的是清理或回收不用的内存，以便内存能够再次利用。 如果一个对象不再使用，就</description><pubDate>Thu, 09 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;内存管理是程序在运行时分配内存、使用内存，并在程序完成时释放内存的过程。在Objective-C中，也被看作是在众多数据和代码之间分配有限内存资源的所有权(Ownership)的一种方式。&lt;/p&gt;
&lt;p&gt;内存管理关心的是清理或回收不用的内存，以便内存能够再次利用。 如果一个对象不再使用，就需要释放对象占用的内存。Objective-C提供了两种内存管理的方法：手动管理内存计数(MRR)和自动引用计数(ARC)。&lt;/p&gt;
&lt;p&gt;这两种方法都采用了一种称为“引用计数”的模型来实现，该模型由Foundation框架的NSObject类和运行时环境（Runtime Environment）共同提供。&lt;/p&gt;
&lt;p&gt;​​&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/943d618f20fd4a5cbf2ba339caf065c1.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;1. 简单聊聊GC与RC&lt;/h2&gt;
&lt;p&gt;随着各个平台的发展，现在被广泛采用的内存管理机制主要有 GC 和 RC 两种。&lt;/p&gt;
&lt;p&gt;GC (Garbage Collection)：垃圾回收机制，定期查找不再使用的对象，释放对象占用的内存。&lt;/p&gt;
&lt;p&gt;RC (Reference Counting)：引用计数机制。采用引用计数来管理对象的内存，当需要持有一个对象时，使它的引用计数 +1；当不需要持有一个对象的时候，使它的引用计数 -1；当一个对象的引用计数为 0，该对象就会被销毁。&lt;/p&gt;
&lt;p&gt;Objective-C 程序现基本都已使用 ARC 内存管理机制。之前 Mac OS X 平台的 Objective-C 程序可以启用 GC，但从 Mac OS X 10.8 开始，GC 机制就苹果被废弃了，改用 ARC 机制。而 iOS 平台的 Objective-C 程序从未支持过 GC，以前使用 MRC，iOS 5、OS X Lion 之后 ARC 诞生。&lt;/p&gt;
&lt;p&gt;引用计数（Reference Count）是一个简单而有效的管理对象生命周期的方式，&lt;/p&gt;
&lt;p&gt;当创建一个新的对象时，初始的引用计数为1。为保证对象的存在，每当创建一个引用到该对象时，通过给对象发送retain消息，为引用计数加1；当不再需要对象时，通过给对象发送release消息，为引用计数减1；当对象的引用计数为0时，系统就知道这个对象不再使用了，通过给对象发送dealloc消息，销毁对象并回收内存。一般在retain方法之后，引用计数通常也被称为保留计数(retain count)。&lt;/p&gt;
&lt;h2&gt;2. 引用计数&lt;/h2&gt;
&lt;h3&gt;2.1 什么是引用计数&lt;/h3&gt;
&lt;p&gt;当一个对象创建并在堆区申请内存时，对象的引用计数为1；当其他的对象需要持有这个对象时，就需要将这个对象的引用计数加1；当其他的对象不再需要持有这个对象时，需要将对象的引用计数减1；当对象的引用计数为0时，对象的内存就会立即释放，对象销毁。&lt;/p&gt;
&lt;p&gt;调用alloc、new、copy、mutableCopy名称开头的方法创建的对象，该对象的引用计数加1。&lt;/p&gt;
&lt;p&gt;调用retain方法时，该对象的引用计数加1。&lt;/p&gt;
&lt;p&gt;调用release方法时，该对象的引用计数减1。&lt;/p&gt;
&lt;p&gt;autorelease方法不改变该对象的引用计数器的值，只是将对象添加到自动释放池中。&lt;/p&gt;
&lt;p&gt;retainCount方法返回该对象的引用计数值。&lt;/p&gt;
&lt;h3&gt;2.3 引用计数的存储&lt;/h3&gt;
&lt;p&gt;Objective - C中的“对象”通过引用计数来管理他的内存周期。那么，  对象的引用计数是如何存储的呢？存储在哪个数据结构里？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;isa&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;isa指针用来维护 “对象” 和 “类” 之间的关系，并确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/a3bd713c74db4cd995a8780599beebfb.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;在arm64之前，isa就是一个普通指针，直接指向objc_class，存储着class、Meta-class对象的内存地址。instance对象的isa指向class对象，class对象的isa指向meta-class对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc.h
struct objc_object {
    Class isa;  // 在 arm64 架构之前
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 arm64 架构下，对 isa 进行了优化，用 nonpointer 表示，变成了一个共用体（union）结构，还使用 位域 来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西，其中的 33 位才是拿来存储 class、meta-class 对象的内存地址信息。要通过位运算将 isa 的值 &amp;amp; ISA_MASK 掩码，才能得到 class、meta-class 对象的内存地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;union isa_t {
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // 真正的 class 指针部分
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19; // 引用计数
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果isa非nonpointer，即 arm64 架构之前的isa指针。由于它只是一个普通的指针，存储着Class、Meta-Class对象的内存地址，所以它本身不能存储引用计数，所以以前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap（引用计数表）散列表中。&lt;/p&gt;
&lt;p&gt;如果isa是nonpointer，则它本身可以存储一些引用计数。从以上union isa_t的定义中我们可以得知，isa_t中存储了两个引用计数相关的东西：extra_rc和has_sidetable_rc。&lt;/p&gt;
&lt;p&gt;extra_rc：19 位，用于存储对象的引用计数（除基础的 1 之外），如果这19位不够存储，has_sidetable_rc 会变为1.&lt;/p&gt;
&lt;p&gt;has_sidetable_rc：1 位，用来表示是否这个对象的引用计数“超出 isa 能存储的范围”，这时候多余的部分要放进 &lt;em&gt;&lt;strong&gt;SideTable的RefCountMap&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// objc-private.h
struct objc_object {
private:
    isa_t isa;  // 在 arm64 架构开始
};

union isa_t
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__  // 在 __arm64__ 架构下
#   define ISA_MASK        0x0000000ffffffff8ULL  // 用来取出 Class、Meta-Class 对象的内存地址
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;  // 0：代表普通的指针，存储着 Class、Meta-Class 对象的内存地址
                                          // 1：代表优化过，使用位域存储更多的信息
        uintptr_t has_assoc         : 1;  // 是否有设置过关联对象，如果没有，释放时会更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有C++的析构函数（.cxx_destruct），如果没有，释放时会更快
        uintptr_t shiftcls          : 33; // 存储着 Class、Meta-Class 对象的内存地址信息
        uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否有被弱引用指向过，如果没有，释放时会更快
        uintptr_t deallocating      : 1;  // 对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;  // 如果为1，代表引用计数过大无法存储在 isa 中，那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap（引用计数表）散列表中
        uintptr_t extra_rc          : 19; // 里面存储的值是对象本身之外的引用计数的数量，retainCount - 1
#       define RC_ONE   (1ULL&amp;lt;&amp;lt;45)
#       define RC_HALF  (1ULL&amp;lt;&amp;lt;18)
    };
......  // 在 __x86_64__ 架构下
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 内存管理方案&lt;/h2&gt;
&lt;p&gt;应用程序内存管理是在程序运行时分配内存，使用它并在使用完后释放它的过程。编写良好的程序将使用尽可能少的内存。在 Objective-C 中，它也可以看作是在许多数据和代码之间分配有限内存资源所有权的一种方式。掌握内存管理知识，我们就可以很好地管理对象生命周期并在不再需要它们时释放它们，从而管理应用程序的内存。&lt;/p&gt;
&lt;p&gt;虽然通常在单个对象级别上考虑内存管理，但实际上我们的目标是管理对象图，要保证在内存中只保留需要用到的对象，确保没有发生内存泄漏。&lt;/p&gt;
&lt;p&gt;下图是苹果官方文档给出的 “内存管理对象图”，很好地展示了一个对象 “创建——持有——释放——销毁” 的过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9c045d2e03274cdf93167357604ab25d.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.1 对象持有规则&lt;/h3&gt;
&lt;p&gt;对象持有规则如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;1. 自己生成的对象，自己持有 2. 非自己生成的对象，自己也能持有 3. 不再需要自己持有的对象时释放 4. 非自己持有的对象无法释放&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对象操作				Objective-C方法&lt;/p&gt;
&lt;p&gt;生成并持有对象	alloc/new/copy/mutableCopy等方法&lt;/p&gt;
&lt;p&gt;持有对象				retain方法&lt;/p&gt;
&lt;p&gt;释放对象				release方法&lt;/p&gt;
&lt;p&gt;废弃对象				dealloc方法&lt;/p&gt;
&lt;h4&gt;3.1.1 自己创建的对象，自己持有&lt;/h4&gt;
&lt;p&gt;使用以下名称开头的方法名意味着自己生成的对象只有自己持有：&lt;strong&gt;alloc、new、copy、mutableCopy&lt;/strong&gt;。 在OC中对象的创建可以通过_&lt;strong&gt;alloc和new&lt;/strong&gt;_这两种方式来创建一个对象。其RC（引用计数，以下统一使用RC）初始值为 1，我们直接使用即可，在不需要使用的时候调用一下release方法进行释放。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSObject *obj = [NSObject alloc];
NSObject *obj1 = [NSObject new];//等价于 NSObject *obj1 = [[NSObject alloc]init];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以使用retainCount来查看对象的引用数值，但最好不要使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSLog(@&quot;%ld&quot;, [obj retainCount]);
#### 3.1.2 你retain的对象，自己也能持有


用alloc、new、copy、mutableCopy之外的方法获得的对象，因为并非自己生产持有，所以自己不是该对象的持有者。如果要使用（持有）该对象，需要先进行retain，否则可能会导致程序Crash。原因是这些方法内部是给对象调用了autorelease方法，所以这些对象会被加入到自动释放池中。


```objective-c
//非自己生成的对象，暂时没有持有
id obj = [NSMutableArray array];

//通过retain持有对象
[obj retain];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码中的NSMutableArray通过类方法array生成了一个对象赋值给变量obj，但obj自己并不持有该对象。使用retain方法可以持有对象&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 正确的用法 */

    id obj = [NSMutableArray array]; // 创建对象但并不持有，对象加入自动释放池，RC = 1

    [obj retain]; // 使用之前进行 retain，对对象进行持有，RC = 2
    /*
     * 使用该对象，RC = 2
     */
    [obj release]; // 在不需要使用的时候调用 release，RC = 1
    /*
     * RunLoop 可能在某一时刻迭代结束，给自动释放池中的对象调用 release，RC = 0，对象被销毁
     * 如果这时候 RunLoop 还未迭代结束，该对象还可以被访问，不过这是非常危险的，容易导致 Crash
     */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;情况2:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 错误的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 创建对象但并不持有，对象加入自动释放池，RC = 1
    } // @autoreleasepool 作用域结束，对象 release，RC = 0，对象被销毁
    NSLog(@&quot;%@&quot;,obj); // EXC_BAD_ACCESS
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* 正确的用法 */

    id obj;
    @autoreleasepool {
        obj = [NSMutableArray array]; // 创建对象但并不持有，对象加入自动释放池，RC = 1
        [obj retain]; // RC = 2
    } // @autoreleasepool 作用域结束，对象 release，RC = 1
    NSLog(@&quot;%@&quot;,obj); // 正常访问
    /*
     * 使用该对象，RC = 1
     */
    [obj release]; // 在不需要使用的时候调用 release，RC = 0，对象被销毁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们通过&lt;strong&gt;自定义方法&lt;/strong&gt; &lt;em&gt;&lt;strong&gt;创建但并不持有对象&lt;/strong&gt;&lt;/em&gt;，则方法名就不应该以 alloc/new/copy/mutableCopy 开头，且返回对象前应该要先通过autorelease方法将该对象加入自动释放池。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (id)object
{
    id obj = [NSObject alloc] init];
    [obj autorelease];
    retain obj;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样调用方在使用该方法创建对象的时候，通过方法名他就会知道他不持有该对象，于是他会在使用该对象前进行retain，并在不需要该对象时进行release。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;备注：&lt;strong&gt;release&lt;/strong&gt;和&lt;strong&gt;autorelease&lt;/strong&gt;的区别： 调用release，对象的RC会立即 -1； 调用autorelease，对象的RC不会立即 -1，而是将对象添加进自动释放池，它会在一个恰当的时刻自动给对象调用release，所以autorelease相当于延迟了对象的释放。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3.1.3 当你不再需要时，release&lt;/h4&gt;
&lt;p&gt;自己持有的对象，一旦该对象不再需要时，持有者有义务调用release方法释放该对象。当然在ARC环境下并不需要开发者主动调用方法，系统会自动调用该方法，但是在MRC环境下需要开发者手动在合适的地方做对象的retain 方法和release方法的调用。&lt;/p&gt;
&lt;h4&gt;3.1.4 非自己持有的对象无法release&lt;/h4&gt;
&lt;p&gt;对于用alloc、new、copy、mutableCopy方法生成并持有的对象，或是用retain方法持有的对象，由于持有者是自己，所以在不需要该对象时需要将其释放。而由此以外所得到的对象绝对不能释放。倘若在程序中释放了非自己所持有的对象就会造成崩溃。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id obj = [[NSObject alloc] init]; // 创建并持有对象，RC = 1
    [obj release]; // 如果自己是持有者，在不需要使用的时候调用 release，RC = 0
    /*
     * 此时对象已被销毁，不应该再对其进行访问
     */
    [obj release]; // EXC_BAD_ACCESS，这时候自己已经不是持有者，再 release 就会 Crash
    /*
     * 再次 release 已经销毁的对象（过度释放），或是访问已经销毁的对象都会导致崩溃
     */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;释放了非自己持有的对象，肯定会导致应用崩溃。因此绝对不要去释放非自己持有的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id obj = [NSMutableArray array]; // 创建对象，但并不持有对象，RC = 1
    [obj release]; // EXC_BAD_ACCESS 虽然对象的 RC = 1，但是这里并不持有对象，所以导致 Crash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一种情况，这是不容易发现问题的情况。执行如下代码，可能会有问题，也可能没有问题。对象所占内存在 “解除分配(deallocated)” 之后，只是放回可用内存池。如果对象所占内存还没有分配给别人，这时候访问没有问题，如果已经分配给了别人，再次访问就会崩溃。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person *person = [[Person alloc] init]; // 创建并持有对象，RC = 1
    [person release]; // 如果自己是持有者，在不需要使用的时候调用 release，RC = 0
    [person release]; // !!!向业已回收的对象发送消息是不安全的
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Objective-C 在iOS中提供了两种内存管理方法：&lt;/p&gt;
&lt;p&gt;MRC，也是本篇文章要讲解的内容，我们通过跟踪自己持有的对象来显式管理内存。这是使用一个称为 “引用计数” 的模型来实现的，由 Foundation 框架的 NSObject 类与运行时环境一起提供。&lt;/p&gt;
&lt;p&gt;ARC，系统使用与MRC相同的引用计数系统，但是它会在编译时为我们插入适当的内存管理方法调用。使用ARC，我们通常就不需要了解本文章中描述的MRC的内存管理实现，尽管在某些情况下它可能会有所帮助。但是，作为一名合格的iOS开发者，掌握这些知识是很有必要的。&lt;/p&gt;
&lt;h3&gt;3.2 使用dealloc放弃对象的所有权&lt;/h3&gt;
&lt;p&gt;NSObject 类定义了一个&lt;strong&gt;dealloc&lt;/strong&gt;方法，该方法会在一个对象没有所有者（RC=0）并且它的内存被回收时由系统自动调用 —— 在 Cocoa 术语中称为freed或deallocated。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dealloc&lt;/strong&gt;方法的作用是 销毁对象自身的内存，并释放它持有的任何资源，包括任何实例变量的所有权。&lt;/p&gt;
&lt;p&gt;以下举了一个在 Person 类中实现 dealloc方法的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end

@implementation Person
// ...
- (void)dealloc
    [_firstName release];
    [_lastName release];
    [super dealloc];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意： 切勿直接调用另一个对象dealloc的方法； 你必须在实现结束时调用[super dealloc]； 当应用程序终止时，可能不会向对象发送dealloc消息。因为进程的内存在退出时会自动清除，所以让操作系统清理资源比调用所有对象的dealloc方法更有效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;dealloc与release&lt;/strong&gt;的区别：&lt;/p&gt;
&lt;p&gt;release是减少引用计数的方法，调用对象的release方法，会使该对象的&lt;strong&gt;引用计数（RC）&lt;/strong&gt; 减1。&lt;/p&gt;
&lt;p&gt;在MRC下，每个对象创建后的引用计数为1，当调用release后，该对象引用计数减小为0，如果此时引用计数变为 0，系统会 &lt;strong&gt;自动调用对象的 dealloc 方法&lt;/strong&gt; 来销毁对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dealloc&lt;/strong&gt; 是系统在对象引用计数为 0 时自动调用的。&lt;/p&gt;
&lt;p&gt;我们不能直接调用 dealloc，因为它是 销毁对象的底层方法，会释放对象占用的内存。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/152130856&quot;&gt;【iOS】内存管理初级&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】仿写 —— 计算器</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-149743986-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-149743986-ios-/</guid><description>在计算器的仿写中，本人首次实际使用MVC构架和Mansory自动布局。 安装Mansory 首先安装cocoapods，然后在终端找到项目文件夹，执行 pod init 随后修改文件，将内容改为： platform :ios, &apos;12.0&apos; target &apos;计算器77&apos; do p</description><pubDate>Sun, 28 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在计算器的仿写中，本人首次实际使用MVC构架和Mansory自动布局。&lt;/p&gt;
&lt;h2&gt;安装Mansory&lt;/h2&gt;
&lt;p&gt;首先安装cocoapods，然后在终端找到项目文件夹，执行 pod init&lt;/p&gt;
&lt;p&gt;随后修改文件，将内容改为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;platform :ios, &apos;12.0&apos; target &apos;计算器77&apos; do pod &apos;LookinServer&apos;, :configurations =&amp;gt; [&apos;Debug&apos;] pod &apos;Masonry&apos; end&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;作者同时安装了Lookin方便调试，随后在终端执行： pod install。&lt;/p&gt;
&lt;p&gt;如果下载有问题，请给终端挂一个梯子，因为下载源是国外的。&lt;/p&gt;
&lt;p&gt;就此安装完成。&lt;/p&gt;
&lt;h2&gt;框架&lt;/h2&gt;
&lt;p&gt;M：我的Model负责执行运算逻辑&lt;/p&gt;
&lt;p&gt;V：我的View负责实现一个基本的计算器页面&lt;/p&gt;
&lt;p&gt;C：负责联系主页面和Model，同时负责各种判断各种非法运算&lt;/p&gt;
&lt;p&gt;这是我的另一篇博客，详细介绍了MVC：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151052260?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=151052260&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;https://blog.csdn.net/2402_86720949/article/details/151052260?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=151052260&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;页面：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/cf1d545a7d864628b2ca902280eb19c9.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我实现的页面如图。&lt;/p&gt;
&lt;p&gt;我使用两层for循环，外层循环负责便历5行，内层循环负责遍历每一行的四个按钮，同时使用Masonry自动布局&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (i == 4) {
                    if (j == 0) {
                        make.left.equalTo(rowView);
                        make.width.equalTo(rowView).multipliedBy(0.5).offset(-btnSpacing/2);
                    } else {
                        make.left.equalTo(lastButton.mas_right).offset(btnSpacing);
                        make.width.equalTo(rowView).multipliedBy(0.25).offset(-btnSpacing*0.75);
                    }
                } else {
                    if (lastButton) {
                        make.left.equalTo(lastButton.mas_right).offset(btnSpacing);
                    } else {
                        make.left.equalTo(rowView);
                    }
                    make.top.bottom.equalTo(rowView);
                    make.width.equalTo(@(btnHeight));
                }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Controller：&lt;/h2&gt;
&lt;p&gt;这部分属于重中之重，尤其在于各种非法输入的防止。&lt;/p&gt;
&lt;p&gt;我分别设置了几个属性，来辅助进行判定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.poin
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149743986&quot;&gt;【iOS】仿写 —— 计算器&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】对象复制与属性关键字</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151194120-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151194120-ios-/</guid><description>目录 对象复制 一、copy与mutableCopy方法 二、NSCopying和NSmutableCopying协议 三、深复制与浅复制 按照类型说明： 非容器类对象的深拷贝与浅拷贝 不可变字符串 可变类型字符串 容器类对象的深浅拷贝 自定义类型的拷贝 容器类对象的深拷贝 归档</description><pubDate>Sat, 27 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%AF%B9%E8%B1%A1%E5%A4%8D%E5%88%B6&quot;&gt;对象复制&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%80%E3%80%81copy%E4%B8%8EmutableCopy%E6%96%B9%E6%B3%95&quot;&gt;一、copy与mutableCopy方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BA%8C%E3%80%81NSCopying%E5%92%8CNSmutableCopying%E5%8D%8F%E8%AE%AE&quot;&gt;二、NSCopying和NSmutableCopying协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%89%E3%80%81%E6%B7%B1%E5%A4%8D%E5%88%B6%E4%B8%8E%E6%B5%85%E5%A4%8D%E5%88%B6&quot;&gt;三、深复制与浅复制&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%8C%89%E7%85%A7%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E%EF%BC%9A&quot;&gt;按照类型说明：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%9D%9E%E5%AE%B9%E5%99%A8%E7%B1%BB%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%B7%B1%E6%8B%B7%E8%B4%9D%E4%B8%8E%E6%B5%85%E6%8B%B7%E8%B4%9D&quot;&gt;非容器类对象的深拷贝与浅拷贝&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%AD%97%E7%AC%A6%E4%B8%B2&quot;&gt;不可变字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%8F%AF%E5%8F%98%E7%B1%BB%E5%9E%8B%E5%AD%97%E7%AC%A6%E4%B8%B2%C2%A0&quot;&gt;可变类型字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%AE%B9%E5%99%A8%E7%B1%BB%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%B7%B1%E6%B5%85%E6%8B%B7%E8%B4%9D&quot;&gt;容器类对象的深浅拷贝&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A0%E8%87%AA%E5%AE%9A%E4%B9%89%E7%B1%BB%E5%9E%8B%E7%9A%84%E6%8B%B7%E8%B4%9D&quot;&gt;自定义类型的拷贝&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%AE%B9%E5%99%A8%E7%B1%BB%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%B7%B1%E6%8B%B7%E8%B4%9D&quot;&gt;容器类对象的深拷贝&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%BD%92%E6%A1%A3%E4%B8%8E%E8%A7%A3%E6%A1%A3%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0&quot;&gt;归档与解档代码实现&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%B1%9E%E6%80%A7%E5%85%B3%E9%94%AE%E5%AD%97&quot;&gt;属性关键字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#1%C2%A0%E5%8E%9F%E5%AD%90%E6%80%A7&quot;&gt;1 原子性&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#2%20%E8%AF%BB%E5%86%99%E6%9D%83%E9%99%90&quot;&gt;2 读写权限&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#3.%20%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86&quot;&gt;3. 内存管理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#assign&quot;&gt;assign&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#weak&quot;&gt;weak&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#unsafe_unretained&quot;&gt;unsafe_unretained&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#retain&quot;&gt;retain&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#strong&quot;&gt;strong&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#copy&quot;&gt;copy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#4.%20%E4%BF%AE%E9%A5%B0%E5%8F%98%E9%87%8F%E5%85%B3%E9%94%AE%E5%AD%97&quot;&gt;4. 修饰变量关键字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%B8%B8%E9%87%8F%EF%BC%88const%EF%BC%89%E5%92%8C%E5%AE%8F%E5%AE%9A%E4%B9%89%EF%BC%88define%EF%BC%89%E7%9A%84%E5%8C%BA%E5%88%AB%3A&quot;&gt;常量（const）和宏定义（define）的区别:&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Q%26A%EF%BC%9A&quot;&gt;Q&amp;amp;A：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#copy%E5%92%8Cstrong%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9A%EF%BC%88%E6%B7%B1%E6%8B%B7%E8%B4%9D%20%E6%B5%85%E6%8B%B7%E8%B4%9D%EF%BC%89&quot;&gt;copy和strong的区别：（深拷贝 浅拷贝）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Q%EF%BC%9A%E4%BB%A5%E4%B8%8B%E4%BB%A3%E7%A0%81%E4%BC%9A%E5%87%BA%E7%8E%B0%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%9F&quot;&gt;Q：以下代码会出现什么问题？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Q%EF%BC%9Aassign%20%E5%92%8C%20weak%20%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E5%8C%BA%E5%88%AB%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F&quot;&gt;Q：assign 和 weak 关键字的区别有哪些？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Q%EF%BC%9Aatomic%20%E4%BF%AE%E9%A5%B0%E7%9A%84%E5%B1%9E%E6%80%A7%E6%98%AF%E6%80%8E%E4%B9%88%E6%A0%B7%E4%BF%9D%E5%AD%98%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%EF%BC%9F&quot;&gt;Q：atomic 修饰的属性是怎么样保存线程安全的？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Q%EF%BC%9Aweak%E5%92%8Cassign%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F&quot;&gt;Q：weak和assign的区别？&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;对象复制&lt;/h2&gt;
&lt;h3&gt;一、copy与mutableCopy方法&lt;/h3&gt;
&lt;p&gt;copy方法用于复制对象的副本，复制下来的该副本是不可修改的，哪怕是调用NSMutableString的copy方法也不可修改。&lt;/p&gt;
&lt;p&gt;而mutableCopy方法复制下来的副本是可修改的，即使被复制的对象原本是不可修改的。例如调用mutableCopy方法复制NSString的，返回的是一个NSMutableString对象。&lt;/p&gt;
&lt;p&gt;以下用代码演示copy和mutableCopy方法的功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //copy与mutableCopy
        NSMutableString *book = [NSMutableString stringWithString: @&quot;疯狂iOS讲义&quot;];

        NSMutableString *bookCopy = [book mutableCopy]

        [bookCopy replaceCharactersInRange: NSMakeRange(2, 3) withString: @&quot;Android&quot;];//复制后的bookCopy副本是可以修改的，这里做个修改，对原字符串的值也没有影响

        NSLog(@&quot;book的值为：%@&quot;,book);//原值

        NSLog(@&quot;bookCopy的值为：%@&quot;,bookCopy);//副本修改后的值

        NSString *str = @&quot;fkit&quot;;//定义一个str字符串
        NSMutableString *strCopy = [str mutableCopy];//用mutableCopy给str复制一个副本

        [strCopy appendString:@&quot;.org&quot;];//向可变字符串后面追加字符串
        NSLog(@&quot;%@&quot;,strCopy);

        NSMutableString *bookCopy2 = [book copy];//用copy方法复制一个book的副本（这个副本不可变）
        [bookCopy2 appendString:@&quot;aa&quot;];//这里会报错，因为copy创建的副本不可变，修改了就崩了

    }
    return 0;
}
### 二、NSCopying和NSmutableCopying协议


当我们想将自定义类用上一节的两个方法复制副本时，我们可能会直接创建完对象后用”类名* 对象2 = [对象1 copy]；“这样的格式来复制副本，但实际上直接这样复制是不对的，会报错说找不到copyWithZone：方法，mutableCopy也是一样。因此我们可以看出，自定义类是不能直接调用这两个方法来复制自身的。


        这是为什么呢？是因为当程序调用copy/mutableCopy方法复制时，程序底层需要调用copyWithZone：/mutableCopyWithZone：方法来完成复制的工作，并返回这两个方法的值。因此为了保证可以复制，需要在自定义类的接口部分声明NSCopying/NSMutableCopying协议，然后再类的实现部分增加copyWithZone：/mutableCopyWithZone：方法，因此，对自定义对象的复制应该如下所示：


接下来，我们以一个自定义的Person类为例，支持copy和mutablecopy：


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

@interface Person : NSObject &amp;lt;NSCopying, NSMutableCopying&amp;gt;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person

// 实现 copy（返回不可变副本）
- (id)copyWithZone:(NSZone *)zone {
    Person *copy = [[[self class] allocWithZone:zone] init];
    copy.name = [self.name copy];
    copy.age = self.age;
    return copy;
}

// 实现 mutableCopy（返回可变副本）
- (id)mutableCopyWithZone:(NSZone *)zone {
    Person *copy = [[[self class] allocWithZone:zone] init];
    copy.name = [self.name mutableCopy];  // 注意生成可变副本
    copy.age = self.age;
    return copy;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p1 = [[Person alloc] init];
        p1.name = @&quot;Tom&quot;;
        p1.age = 18;

        Person *p2 = [p1 copy];         // 调用 copyWithZone
        Person *p3 = [p1 mutableCopy];  // 调用 mutableCopyWithZone

        NSLog(@&quot;原始：%@ %ld&quot;, p1.name, p1.age);
        NSLog(@&quot;copy：%@ %ld&quot;, p2.name, p2.age);
        NSLog(@&quot;mutableCopy：%@ %ld&quot;, p3.name, p3.age);
    }
    return 0;
}
### 三、深复制与浅复制


深复制和浅复制是面向对象编程中非常重要的概念。


浅复制：仅复制对象的指针地址，多个变量共享同一个对象。


深复制： 不仅复制指针，还会复制整个对象内容，使得原对象和副本完全独立。


举个例子：


```objective-c
NSMutableString *str1 = [NSMutableString stringWithString:@&quot;Hello&quot;];
NSMutableString *str2 = str1;            // 浅复制（赋值）
// 修改 str1，str2 也变了
[str1 appendString:@&quot; World&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此总而言之，浅拷贝就是创建一个副本，对内存地址的复制。深拷贝就是创建一个副本，对内容完全复制。原始对象与副本对象内存地址不同。&lt;/p&gt;
&lt;h3&gt;按照类型说明：&lt;/h3&gt;
&lt;h4&gt;非容器类对象的深拷贝与浅拷贝&lt;/h4&gt;
&lt;h5&gt;不可变字符串&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString* str1 = @&quot;dddddd&quot;;
        NSString* str2 = [str1 copy];
        NSString* str3 = [str1 mutableCopy];
        NSMutableString* str4 = [str1 copy];
        NSMutableString* str5 = [str1 mutableCopy];
        NSLog(@&quot;str1:%p&quot;, str1);
        NSLog(@&quot;str2:%p&quot;, str2);
        NSLog(@&quot;str3:%p&quot;, str3);
        NSLog(@&quot;str4:%p&quot;, str4);
        NSLog(@&quot;str5:%p&quot;, str5);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/00c382a0e5a24f18b1e8c3c48010a412.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;得出结论： 不可变字符串，只要是copy就是浅拷贝，mutableCopy是深拷贝。tips：我们用NSString stringWithstring 方式创建的是一个常量区字符串。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/972630e85d32415ebd9d4fc0def5d3a3.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151194120&quot;&gt;【iOS】对象复制与属性关键字&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】UIViewController</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151360720-ios-uiviewcontroller/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151360720-ios-uiviewcontroller/</guid><description>目录 ​编辑 视图加载： 视图可见性 viewWillAppear： viewDidAppear： viewWillDisappear： 总结 视图加载： 视图初始化会设计两个方法：loadView和ViewDidload 当新添加一个视图控制器时，通过xcode生成的代码模版只</description><pubDate>Sun, 14 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E2%80%8B%E7%BC%96%E8%BE%91&quot;&gt;​编辑&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%A7%86%E5%9B%BE%E5%8A%A0%E8%BD%BD%EF%BC%9A&quot;&gt;视图加载：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%A7%86%E5%9B%BE%E5%8F%AF%E8%A7%81%E6%80%A7&quot;&gt;视图可见性&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#viewWillAppear%EF%BC%9A&quot;&gt;viewWillAppear：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#viewDidAppear%EF%BC%9A&quot;&gt;viewDidAppear：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#viewWillDisappear%EF%BC%9A&quot;&gt;viewWillDisappear：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%80%BB%E7%BB%93&quot;&gt;总结&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;视图加载：&lt;/h2&gt;
&lt;p&gt;视图初始化会设计两个方法：loadView和ViewDidload&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0ee63ea2effa47a59ec0ec6f9cbe3600.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当新添加一个视图控制器时，通过xcode生成的代码模版只有viewDidLoad代码。当视图控制器的view被请求时，loadView方法会被调用，但因为他还没背创建，所以会是nil。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;视图通常会通过以下三种方式加载：&lt;/strong&gt; 从nibs 从故事板（UIStoryboardSegue） 使用自定义代码创建UI&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如果通过覆写loadview方法创建了自定义UI，需要牢记：&lt;/strong&gt; 将view视图设置到视图层级的根上 确保视图正在被其他视图控制器共享 不要调用[super loadView]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/ad8efab32ec742a18d2b5bb4235b497b.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在视图层次结构准备就绪后，视图呈现给用户之前，viewDidload会被调用一次。可以在方法中做一些一次性的初始化操作。&lt;/p&gt;
&lt;h2&gt;视图可见性&lt;/h2&gt;
&lt;p&gt;视图控制器提供了四个生命周期方法，以接收有关视图可视性的通知。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0d8cd842c7b64d0a9ae727e53ed76c33.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/994b0a3a00f1405a8f858d88ecc764cc.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;viewWillAppear：&lt;/h3&gt;
&lt;p&gt;当视图层级已经准备好，且视图即将被放入视图窗口时，此方法会被调用。在即将展示视图控制器或之前入栈（modal或者其他）的视图控制器弹出时，这种情况就会发生。&lt;/p&gt;
&lt;p&gt;在这个时刻，过渡动画还未开始，视图对终端用户也是不可见的。不要启动任何视图动画，因为没有任何作用。&lt;/p&gt;
&lt;h3&gt;viewDidAppear：&lt;/h3&gt;
&lt;p&gt;当视图在视图窗口展示出来，且过渡动画完成后，此方法会被调用。&lt;/p&gt;
&lt;p&gt;因为动画会耗费约300毫秒，所以，对比viewWillAppear：和viewDidLoad：，viewDidAppear：和viewWillDisappear：之间的时间差可能会比较大。&lt;/p&gt;
&lt;h3&gt;viewWillDisappear：&lt;/h3&gt;
&lt;p&gt;该方法表示视图将要从屏幕上隐藏起来。这可能是因为其他视图控制器想要接管屏幕，或该视图控制器将要出栈。&lt;/p&gt;
&lt;p&gt;你可能会注意到，当此方法被调用时，没有办法能直接够判断这是由当前视图控制器要出栈还是其他视图控制器入栈导致的。&lt;/p&gt;
&lt;p&gt;下文摘自【高性能iOS应用开发】，笔者会在以后慢慢理解&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;以下有一些高效使用生命周期事件的最佳实践：&lt;/strong&gt; ** **不要重写loadView 如果每次都需要展示最新的信息，那么就在viewWillAppear中更新UI元素。 viewDidappear中开始动画，如果有视频流内容，也可以开始播放了 viewWillDisappear来暂停或停止动画，不做多余操作 viewDid Disappear 销毁内存中的复杂数据结构。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;UIViewController生命周期&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+ (void)initialize&lt;/code&gt;：函数并不会每次创建对象都调用，只有在第一次初始化的时候才会调用，再次创建将不会调用&lt;code&gt;initialize&lt;/code&gt;方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;init&lt;/code&gt;方法和&lt;code&gt;initCoder&lt;/code&gt;方法相似，知识被调用的环境不一样。如果用代码初始化，会调用&lt;code&gt;init&lt;/code&gt;方法，从nib文件或者归档(&lt;code&gt;xib&lt;/code&gt;、&lt;code&gt;storyboard&lt;/code&gt;)进行初始化会调用&lt;code&gt;initCoder&lt;/code&gt;。&lt;code&gt;initCoder&lt;/code&gt;是&lt;code&gt;NSCoding&lt;/code&gt;协议中的方法，&lt;code&gt;NSCoding&lt;/code&gt;是负责编码解码，归档处理的协议。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loadView&lt;/code&gt;：是开始加载&lt;code&gt;view&lt;/code&gt;的起始方法，除非手动调用，否则在&lt;code&gt;ViewController&lt;/code&gt;的生命周期中只调用一次。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewDidLoad&lt;/code&gt;：是我们最常用的方法，类成员对象和变量的初始化我们都会放在这个方法中。在创建类后无论视图展现还是消失，这个方法也只会在布局是调用一次。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewWillAppear:(BOOL)animated&lt;/code&gt;：方法 是在视图将要展现出来的时候调用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewWillLayoutSubviews&lt;/code&gt;：方法是在将要布局子视图的时候调用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewDidLayoutSubviews&lt;/code&gt;：方法是在子视图布局完成后调用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewDidAppear:(BOOL)animated&lt;/code&gt;：方法是视图已经出现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewWillDisappear:(BOOL)animated&lt;/code&gt;：方法是视图即将消失。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;viewDidDisappear:(BOOL)animated&lt;/code&gt;：视图已经消失。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dealloc&lt;/code&gt;：&lt;code&gt;ViewController&lt;/code&gt;被释放时调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151360720&quot;&gt;【iOS】UIViewController&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】AFNetworking</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151627674-ios-afnetworking/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151627674-ios-afnetworking/</guid><description>文件构成 AFURLSessionManager AFHTTPSessionManager 一次完整的GET请求过程 1. 初始化和配置 2. GET请求 3. 添加请求头的GET POST请求 举例：Spotify内容的获取 文件构成 AFNetworking的构成很简单，主要</description><pubDate>Sun, 14 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#----&quot;&gt;文件构成&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#afurlsessionmanager&quot;&gt;AFURLSessionManager&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#afhttpsessionmanager&quot;&gt;AFHTTPSessionManager&lt;/a&gt; &lt;a href=&quot;#---------&quot;&gt;一次完整的GET请求过程&lt;/a&gt; &lt;a href=&quot;#1-------&quot;&gt;1. 初始化和配置&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#2-get--&quot;&gt;2. GET请求&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#3-------get&quot;&gt;3. 添加请求头的GET&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#post--&quot;&gt;POST请求&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#---spotify-----&quot;&gt;举例：Spotify内容的获取&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;文件构成&lt;/h2&gt;
&lt;p&gt;AFNetworking的构成很简单，主要就四个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manager : 负责处理网络请求的两个&lt;code&gt;Manager&lt;/code&gt;，主要实现都在&lt;code&gt;AFURLSessionManager&lt;/code&gt;中。&lt;/li&gt;
&lt;li&gt;Reachability : 网络状态监控。&lt;/li&gt;
&lt;li&gt;Security : 处理网络安全和&lt;code&gt;HTTPS&lt;/code&gt;相关的。&lt;/li&gt;
&lt;li&gt;Serialization : 请求和返回数据的格式化器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AFURLSessionManager&lt;/h2&gt;
&lt;p&gt;在AFN中，网络请求的manager主要有AFHTTPSessionManager 和 AFURLSessionManager 构成，二者为父子关系。这两个类职责划分很清晰，父类负责处理一些基础的网络代码，并且接受NSURLRequest 对象，而子类则负责处理和http协议有关的逻辑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类名&lt;/strong&gt; &lt;strong&gt;继承关系&lt;/strong&gt; &lt;strong&gt;主要用途&lt;/strong&gt; &lt;strong&gt;适用场景&lt;/strong&gt; &lt;strong&gt;AFURLSessionManager&lt;/strong&gt; 基类（直接基于 NSURLSession 封装） 管理 session 和 task，偏底层 下载、上传、自定义请求、复杂任务管理 &lt;strong&gt;AFHTTPSessionManager&lt;/strong&gt; 继承自 AFURLSessionManager 封装了常见的 HTTP 请求方法 普通 HTTP 请求（GET/POST/PUT/DELETE），业务开发常用&lt;/p&gt;
&lt;h2&gt;AFHTTPSessionManager&lt;/h2&gt;
&lt;p&gt;AFNetworking 中使用最高频率的是 AFHTTPSessionManager，负责各种 HTTP 请求的发起和处理，它继承自 AFURLSessionManager，是各种请求的直接执行者。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/26266f25c6494449bbf26b50b01c34f8.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;一次完整的GET请求过程&lt;/h3&gt;
&lt;p&gt;这里以 GET 为例，展示发起一次 GET 请求的具体过程。
AFHTTPSessionManager 支持创建 GET、HEAD、POST、PUT、PATCH、DELETE 等请求，其中 GET 请求支持以下方法发起&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(nullable id)parameters
                      headers:(nullable NSDictionary &amp;lt;NSString *, NSString *&amp;gt; *)headers
                     progress:(nullable void (^)(NSProgress * _Nonnull))downloadProgress
                      success:(nullable void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
                      failure:(nullable void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{

    NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@&quot;GET&quot;
                                                        URLString:URLString
                                                       parameters:parameters
                                                          headers:headers
                                                   uploadProgress:nil
                                                 downloadProgress:downloadProgress
                                                          success:success
                                                          failure:failure];

    [dataTask resume];

    return dataTask;
}
#### 1. 初始化和配置


首先，要导入AFNetworking头文件


![请添加图片描述](https://i-blog.csdnimg.cn/direct/3daf3946218d4cd78a3cba270e91091c.png)


默认配置设置：


AFHTTPRequestSerializer请求序列化
 AFJSONResponseSerializer用于响应序列化


- 初始化Manager：


```objective-c
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
//本质上是调用：
[[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;请求序列化器：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;决定请求参数如何拼接/序列化&lt;/p&gt;
&lt;p&gt;默认是AFHTTPRequestSerializer(普通HTTP表单)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;manager.requestSerializer = [AFJSONRequestSerializer serializer]; //参数转为 JSON
manager.requestSerializer.timeoutInterval = 15; //超时时间
[manager.requestSerializer setValue:@&quot;application/json&quot; forHTTPHeaderField:@&quot;Content-Type&quot;]; //自定义Header
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;响应序列化器：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;决定返回数据如何解析（默认是 AFJSONResponseSerializer，会把 JSON 自动转成 NSDictionary / NSArray。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;manager.responseSerializer = [AFJSONResponseSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@&quot;application/json&quot;, @&quot;text/json&quot;, @&quot;text/javascript&quot;, @&quot;text/html&quot;, nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;AFHTTPSessionManager使用序列化器在本机对象和网络表示之间进行转换：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;requestSerializer属性（类型为AFHTTPRequestSerializer）将参数转换为URL查询字符串或HTTP正文内容
responseSerializer属性（类型为AFHTTPResponseSerializer）将原始响应数据转换为可用的对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d9eb45a475d5450baf7ce9d323777638.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;默认情况下，AFHTTPSessionManager配置为：
AFHTTPRequestSerializer请求序列化
AFJSONResponseSerializer用于响应序列化&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;也可以根据API要求自定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// For working with JSON APIs
manager.requestSerializer = [AFJSONRequestSerializer serializer];
manager.responseSerializer = [AFJSONResponseSerializer serializer];

// For XML services
manager.responseSerializer = [AFXMLParserResponseSerializer serializer];

// For image downloads
manager.responseSerializer = [AFImageResponseSerializer serializer];
#### 2. GET请求


![请添加图片描述](https://i-blog.csdnimg.cn/direct/b40054b9749a43b390c9469d87173a62.png)


 示例：提出GET请求：


```objective-c
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];

// 发起 GET 请求
[manager GET:urlString parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    NSLog(@&quot;数据如下: %@&quot;, responseObject);
    //接下来可以在这里处理返回的数据
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    NSLog(@&quot;错误: %@&quot;, error);
}];
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;URLString（NSString类型&lt;/strong&gt;）：表示要发送GET请求的URL字符串，即请求的目标地址。 &lt;strong&gt;parameters（可选的id类型）&lt;/strong&gt;：包含GET请求的参数，这些参数会附加到URL字符串中，以便服务器可以根据这些参数返回相应的数据。它通常是一个NSDictionary或其他数据结构，其中包含键值对，表示请求参数。 &lt;strong&gt;headers（可选的NSDictionary类型）&lt;/strong&gt;：包含HTTP请求头的字典。HTTP请求头通常包含与请求相关的信息，例如授权令牌、用户代理、接受的数据类型等。这里的参数允许你自定义请求头。 &lt;strong&gt;downloadProgress（可选的NSProgress类型块）&lt;/strong&gt;：一个块对象，用于跟踪下载进度。这个块会在下载数据时被调用，可以用来更新UI或记录下载进度等。 &lt;strong&gt;success（可选的块）&lt;/strong&gt;：一个成功回调块，当请求成功完成时会被调用。这个块通常接受两个参数，第一个参数是包含响应数据的NSURLSessionDataTask对象，第二个参数是响应数据，通常是一个NSDictionary或其他数据结构。 &lt;strong&gt;failure（可选的块）&lt;/strong&gt;：一个失败回调块，当请求失败时会被调用。这个块通常接受两个参数，第一个参数是包含请求任务信息的NSURLSessionDataTask对象，第二个参数是一个NSError对象，包含了关于请求失败的信息。 在这个方法内部，首先通过调用&lt;strong&gt;dataTaskWithHTTPMethod:URLString:parameters:headers:uploadProgress:downloadProgress:success:failure&lt;/strong&gt;:方法创建一个NSURLSessionDataTask对象，然后使用resume方法开始执行这个任务（发送GET请求），最后返回该任务对象，以便调用者可以对任务进行进一步操作或取消。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;详细说说parameters：看着文字可能会一头雾水，我们来句一个例子
在天气预报的请求中我们时常会遇到&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d8601658bcce48cebfedb73f028b44f7.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们需要自己手动拼接字符串，而使用parameters后，我们可以&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/5ba3798086b74cf8b97580d6176fe3c8.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里补充一个细节问题：APIURL是接口的基本路径，而parameters是传给接口的查询参数或请求体，AFN会自动把parameters序列化并拼接到URL或body，但是前提是必须有一个基础URL来承载这些参数。
至于headers，会在下面讲解。&lt;/p&gt;
&lt;h4&gt;3. 添加请求头的GET&lt;/h4&gt;
&lt;p&gt;在HTTP请求中，每个请求头都是一对 键值对，用来给服务器传递额外的信息&lt;/p&gt;
&lt;p&gt;HTTP请求示例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GET /api/data?param1=value1 HTTP/1.1 Host: example.com Authorization: Bearer BQAG8… Content-Type: application/json App-Name: MyApp App-Key: 123456&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;里面的Authorization / Content - Type / App-Name / App- Key 都是请求头，他们不属于URL，也不属于请求体，只是告诉服务器如何请求&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Header 名称&lt;/strong&gt; &lt;strong&gt;用途&lt;/strong&gt; Authorization 身份验证。比如 OAuth 的 Bearer token，告诉服务器你是谁，有访问权限。 Content-Type 告诉服务器请求体的数据类型，如 JSON、表单或 XML。 Accept 告诉服务器你希望返回什么类型的数据，例如 JSON。 User-Agent 告诉服务器客户端信息，例如浏览器类型、App 名称。 自定义 Header（如 App-Name, App-Key） 自定义数据传递，可用于验证 API Key 或标识请求来源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(void) getRequestWithHeader {
    NSString *urlString = @&quot;http://&quot;;
    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];
    [manager.requestSerializer setValue:(nullable NSString *) forHTTPHeaderField:(nonnull NSString *)];
    [manager GET:urlString parameters:nil headers:nil progress:^(NSProgress * _Nonnull downloadProgress) {
        NSLog(@&quot;%@&quot;, downloadProgress);
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@&quot;%@&quot;,responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@&quot;%@&quot;, error);
    }];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Q1 : 前文的Manger基础设置中也能决定返回数据如何解析，和请求头里的Accept是什么关系呢？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Accept请求头，是高速服务器希望接收的数据类型。比如： application/json → 希望返回 JSON text/html → 希望返回 HTML application/xml → 希望返回 XML&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;而AFN的response Serializer决定了客户端如何解析服务器返回的数据： AFJSONResponseSerializer → 把返回数据解析成 NSDictionary / NSArray AFHTTPResponseSerializer → 只返回 NSData AFXMLParserResponseSerializer → 返回 NSXMLParser&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总而言之：Accept决定服务器返回什么格式， 而response Serializer决定客户端如何解析这个格式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2:前面说过，在GET请求的Headers是一个请求头的字典，为什么通过request Serializer设置请求头，而headers设置为nil？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过request Serializer设置的请求头，是给整个manager设置的默认请求头，之后这个manager发出的所有请求（GET / POST / PUT）都会带上这个请求头。
而headers是在单个请求中穿headers参数，仅给这一条GET请求加请求头，和request Serializer不冲突，优先级更高&lt;/p&gt;
&lt;h3&gt;POST请求&lt;/h3&gt;
&lt;p&gt;GET请求通常用于获取资源，不改变服务器状态；
而POST请求多用于创建和修改资源。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;POST比GET通常会多一个请求体，POST中，参数放在请求体中，服务器通过body解析参数。 而GET的参数全部放在URL上，服务器通过URL解析参数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;AFNetworking/AFNetworking.h&amp;gt;

- (void)doPostRequest {
    //请求 URL
    NSString *urlString = @&quot;https://api.example.com/v1/login&quot;;
    //请求参数
    NSDictionary *parameters = @{
        @&quot;username&quot;: @&quot;Tom&quot;,
        @&quot;password&quot;: @&quot;123456&quot;
    };
    //创建 manager
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];

    //可以加请求头
    [manager.requestSerializer setValue:@&quot;Bearer Aceess_Token&quot;
                     forHTTPHeaderField:@&quot;Authorization&quot;];
    [manager.requestSerializer setValue:@&quot;application/json&quot;
                     forHTTPHeaderField:@&quot;Accept&quot;];

    //发起 POST 请求
    [manager POST:urlString
       parameters:parameters
          headers:nil
         progress:nil
          success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
              NSLog(@&quot;请求成功: %@&quot;, responseObject);
          }
          failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
              NSLog(@&quot;请求失败: %@&quot;, error);
          }];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;举例：Spotify内容的获取&lt;/h2&gt;
&lt;p&gt;查看官方文档是很重要的一环。
通过文档得知，我们通过API访问数据，需要一个访问令牌&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/71ed6e74b82644cbac6f7df322edf834.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/261f14693ee24d89a37c4fa5422a8ccc.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由图可知，访问令牌需要我们获取ClientID和secret，此过程在网站内进行，当前跳过。
• -X POST → HTTP POST 请求
• -H → 设置请求头
• -d → 请求体参数（form-urlencoded）
我们按照步骤一步步完成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(void)fetchAccessTokenWithClientID:(NSString *)clientID
                      clientSecret:(NSString *)clientSecret {
    NSString *urlString = @&quot;https://accounts.spotify.com/api/token&quot;;

    NSDictionary *parameters = @{
        @&quot;grant_type&quot;: @&quot;client_credentials&quot;,
        @&quot;client_id&quot;: clientID,
        @&quot;client_secret&quot;: clientSecret
    };

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.requestSerializer = [AFHTTPRequestSerializer serializer];
    [manager.requestSerializer setValue:@&quot;application/x-www-form-urlencoded&quot; forHTTPHeaderField:@&quot;Content-Type&quot;];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];

    [manager POST:urlString
       parameters:parameters
          headers:nil
         progress:nil
          success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
              NSString *accessToken = responseObject[@&quot;access_token&quot;];
              self.accessToken = accessToken;
              NSLog(@&quot;Access Token: %@&quot;, accessToken);
          }
          failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
              NSLog(@&quot;Error fetching access token: %@&quot;, error);
          }];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后得出结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c43016ca24f24e99807e5a2e34e7e9ae.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d8ffc093440446f8b482444d880cc0d6.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在获取到accessToken后，我们在按照官网的流程，GET到对应信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)fetchHotSongWithID:(NSString *)artistID
                   market:(NSString *)market {
    NSString *urlString = [NSString stringWithFormat:@&quot;https://api.spotify.com/v1/artists/%@/top-tracks?market=%@&quot;, artistID, market];
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    [manager.requestSerializer setValue:[NSString stringWithFormat:@&quot;Bearer %@&quot;, self.accessToken] forHTTPHeaderField:@&quot;Authorization&quot;];
    [manager GET:urlString parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@&quot;Hot songs info: %@&quot;, responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@&quot;Error fetching artist info: %@&quot;, error);
    }];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为输入的是贾斯汀比伯的ID 最终我们成功请求到他的信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/2f62865629e44ab0b90987806a36b358.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/3e371720eb534ff1a86dc654ef5b27a3.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151627674&quot;&gt;【iOS】AFNetworking&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】 单例模式</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151372538-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151372538-ios-/</guid><description>1. 认识单例模式 首先让我们先看下关于 单例模式的定义 （来自于《设计模式》(Addison Wesley,1994)） 一个类 有且仅有 一个实例，并且自行实例化向整个系统提供。 如果说每一个人都是一个类，那么从他出生开始，他就是生活中的 唯一 实例，每当有人要拜访或者联系你</description><pubDate>Sat, 13 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 认识单例模式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d572af2a46314956816929718fb182ac.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先让我们先看下关于&lt;code&gt;单例模式的定义&lt;/code&gt;（来自于《设计模式》(Addison-Wesley,1994)）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个类&lt;strong&gt;有且仅有&lt;/strong&gt;一个实例，并且自行实例化向整个系统提供。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果说每一个人都是一个类，那么从他出生开始，他就是生活中的&lt;strong&gt;唯一&lt;/strong&gt;实例，每当有人要拜访或者联系你的时候，无论别人认识你的时候你是什么状态，他所能联系到的都是&lt;strong&gt;现在&lt;/strong&gt;的你。你本身的状态会在_其他地方_发生改变，即当你状态改变后，后续所有找到你的，都是看到状态改变后的你。那么，我们就可以认为每一个人都处于单例模式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 iOS 中，系统默认就有许多支持单例方法的地方，例如通知中心、应用管理、本地保存等，但是一般都属于非严格的单例模式，也就是你可以使用 &lt;code&gt;[UIApplication sharedApplication]&lt;/code&gt; 来获取单例对象，也可以通过 &lt;code&gt;[UIApplication new]&lt;/code&gt; 生成一个新的对象，而如果按照准确的单例定义来说，依旧以 &lt;code&gt;UIApplication&lt;/code&gt; 类为例， &lt;code&gt;[UIApplication new]&lt;/code&gt; 与 &lt;code&gt;[UIApplication sharedApplication]&lt;/code&gt; 应该在任何时候返回的对象都是相同的。也就是说无论你怎么操作，只会存在一个实例，不会创建其他的副本内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. 单例模式的使用&lt;/h2&gt;
&lt;p&gt;单例模式需要实现一个公共访问的类方法，一般命名为 shared + 类名。在该方法的具体实现方案，是推荐通过dispatch_once 来实现类的实例化。&lt;/p&gt;
&lt;p&gt;可以直接通过重写父类的方法，把分配内存的方法变成&lt;strong&gt;只执行一次&lt;/strong&gt;。从根本上实现了单例。&lt;/p&gt;
&lt;p&gt;如果按照严格的单例写法的话，单例模式一共需要重写五个方法 :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+ (instancetype)allocWithZone:(struct _NSZone *)zone
+ (id)copyWithZone:(struct _NSZone *)zone
+ (id)mutableCopyWithZone:(struct _NSZone *)zone
- (id)copyWithZone:(NSZone *)zone
- (id)mutableCopyWithZone:(NSZone *)zone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，可以把这些方法全都通过attribute((unavailable (invalid ))); 方式禁止，以保证一定使用shhared的单例方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SingleView.h

#import &amp;lt;Foundation/Foundation.h&amp;gt;

@interface SingleView : NSObject

#pragma mark- method
+ (SingleView *)sharedSingleView;
+ (id)alloc __attribute__((unavailable(&quot;invalid, use sharedSingleView instead&quot;)));
+ (id)new __attribute__((unavailable(&quot;invalid, use sharedSingleView instead&quot;)));
- (id)copy __attribute__((unavailable(&quot;invalid, use sharedSingleView instead&quot;)));
- (id)mutableCopy __attribute__((unavailable(&quot;invalid, use sharedSingleView instead&quot;)));

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/img_convert/07a3b7af9a870627b9dbf0c5039d566e.webp?x-oss-process=image/format,png&quot; alt=&quot;截屏2022-07-31 12.03.41.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果只是在自己的项目中使用的话，那么直接实现一个 shared 的类方法基本都能满足需求，由于是自己的内容，代码是受控的，实现并调用即可。而如果是对外的库的话（静态库、动态库），这需要根据具体的业务内容考虑是否需要按照严格的单例写法来实现了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;懒汉&lt;/h3&gt;
&lt;p&gt;懒汉式创建单例模式的意思其实就是指我们在创建的时间是我们需要用到这个单例的时候，我们才开始创建这个唯一实例，这种创建模式有助于提高性能，以及节省资源的效果。简单来说，就是我们平时日常生活中的deadline，只要在达到deadline的时候我们才去提交工作，这和他的名字也很相似，懒汉式延迟创建。&lt;/p&gt;
&lt;h4&gt;dispatch_once&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;Singleton.h&quot;

@implementation Singleton

+ (instancetype)sharedInstance {
    static Singleton *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&amp;amp;onceToken, ^{
        instance = [[super allocWithZone:NULL] init];
    });
    return instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [self sharedInstance];
}

- (id)copyWithZone:(NSZone *)zone {
    return self;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    return self;
}

@end
#### 同步锁实现


```objective-c
static Singleton* instance = nil;
+(id) sharedInstance  {
	//在这里判断，为了优化资源，防止多次加锁和判断锁
    if (instance == nil) {
    	//在这里加一把锁（利用本类为锁）进行多线程问题的解决
        @synchronized (self) {
            if (instance == nil) {
            	//调用super的allocwithZone方法来分配内存空间
                instance = [[super allocWithZone:NULL] init];
            }
        }
    }
    return instance;
}

+(id) allocWithZone:(struct _NSZone *)zone {
    if (instance == nil) {
        @synchronized (self) {
            if (!instance) {
                instance = [[super allocWithZone:NULL] init];
            }
        }
    }
    return instance;
}

-(id) copyWithZone:(NSZone *)zone {
    return self;
}

-(id) mutableCopyWithZone:(NSZone *)zone {
    return  self;
}
@end
### 饿汉


饿汉式创建单例则是在你的类加载的时候立刻就开始加载一个单例的一种单例模式，这种模式我们需要把我们的加载单例的代码写在类第一次加载的位置。

在使用代码去创建对象之前就已经创建好了对象。


load方法：当类加载到运行环境中的时候就会调用且仅调用一次，同时注意一个类只会加载一次（类加载有别于引用类，可以这么说，所有类都会在程序启动的时候加载一次，不管有没有在目前显示的视图类中引用到

initialize方法：当第一次使用类的时候加载且仅加载一次

二者相比较

在不考虑开发者主动使用的情况下，系统最多会调用一次

如果父类和子类都被调用，父类的调用一定在子类之前

都是为了应用运行前创建合适的运行环境


在使用时都不要过重地依赖于这两个方法，除非真正必要


它们的相同点在于：方法只会被调用一次。（其实这是相对 runtime 来说的，后边会做进一步解释）。


load 是只要类所在文件被引用就会被调用，而 initialize 是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目，就不会有 load 调用；但即使类文件被引用进来，但是没有使用，那么 initialize 也不会被调用。


文档也明确阐述了方法调用的顺序：父类(Superclass)的方法优先于子类(Subclass)的方法，类中的方法优先于分类(Category)中的方法。


![请添加图片描述](https://i-blog.csdnimg.cn/direct/a757b25da0bc423396c184c804f7698a.png)


首先，在类被加载的时候会调用且仅调用一次load方法，而load方法里面又调用了alloc方法，所以，第一次调用肯定是创建好了对象，而且这时候不会存在多线程问题。当我们手动去使用alloc的时候，无论如何都过不了判断，所以也不会存在多线程的问题了。

![请添加图片描述](https://i-blog.csdnimg.cn/direct/551c1140db82404e9acb910d1f51a07f.png)


```objective-c
static id _instance;
@implementation sharedSingleton
+ (void)load { // 在类加载到OC运行时的环境内存中，就会调用这部分内容，只会加载一次，也就不需要加锁
    _instance = [[self alloc] init];
}
+(instancetype)sharedSingleton {
    return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    if (!_instance) {//防止一个多次创建
        _instance = [super allocWithZone:zone];
    }
    return _instance;
}
- (id)copyWithZone:(NSZone *)zone {
    return _instance;
}
-(id)mutableCopyWithZone:(NSZone *)zone {
    return _instance;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果如下证明单例代码有效：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b72c81a58f9548f99760e5f928388385.png&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;因为对block部分对不了解，引用学长博客中的一段话：&lt;/strong&gt; ispatch_once 主要是根据 onceToken 的值来决定怎么去执行代码。 1.当 onceToken = 0 时，线程执行 dispatch_once 的 block 中代码； 2.当 onceToken = -1 时，线程跳过 dispatch_once 的 block 中代码不执行； 3.当 onceToken 为其他值时，线程被阻塞，等待 onceToken 值改变。 当线程调用mySingleton方法时，此时 onceToken = 0，调用 block 中的代码，此时 onceToken =其他值。 当其他线程再调用 mySingleton 方法时，onceToken为其他值，线程阻塞。当 block 线程执行完 block之后，onceToken = -1，其他线程不再阻塞，跳过 block。下次再调用mySingleton方法 时， block 已经为-1，直接跳过 block。转载自[【iOS】—— 单例模式] (https://blog.csdn.net/m0_73974920/article/details/132909916?spm=1001.2014.3001.5502) 这里又要注意一下要调用父类的【super allocWithZone:null】这个方法，因为我们这里已经重写这个类的方法了，不然会出现一个循环调用的问题。同时为了防止copy和mutablecopy两个方法的出现崩溃的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3.总结&lt;/h2&gt;
&lt;p&gt;单例模式是一种非常常见、使用频率也很高的一种设计模式。单例能够在许多场合使用，有时候也可以用来保存数据。&lt;/p&gt;
&lt;p&gt;它（严格单例）存在着以下特点：&lt;/p&gt;
&lt;p&gt;节省内存；&lt;/p&gt;
&lt;p&gt;在程序的运行周期中保存数据；&lt;/p&gt;
&lt;p&gt;只能在自己的类中实现实例；&lt;/p&gt;
&lt;p&gt;程序的运行周期中，内存不会被释放；&lt;/p&gt;
&lt;p&gt;类中有且仅有一个实例；&lt;/p&gt;
&lt;h3&gt;懒汉模式：&lt;/h3&gt;
&lt;p&gt;· 优点：&lt;/p&gt;
&lt;p&gt;延迟加载：懒汉模式只有在第一次访问单例实例时才会进行初始化，可以节省资源，提高性能，因为实例只有在需要时才会被创建。&lt;/p&gt;
&lt;p&gt;节省内存：如果单例对象很大或者初始化过程开销较大，懒汉模式可以避免在程序启动时就创建不必要的对象。&lt;/p&gt;
&lt;p&gt;线程安全性：可以通过加锁机制（如双重检查锁定）来实现线程安全。&lt;/p&gt;
&lt;p&gt;· 缺点：&lt;/p&gt;
&lt;p&gt;线程安全性开销：懒汉模式在实现线程安全时可能需要额外的同步机制，这会引入一些性能开销。&lt;/p&gt;
&lt;p&gt;复杂性增加：实现线程安全的懒汉模式可能需要编写复杂的代码，容易引入错误。&lt;/p&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;p&gt;线程安全性开销：懒汉模式在实现线程安全时可能需要额外的同步机制，这会引入一些性能开销。&lt;/p&gt;
&lt;p&gt;复杂性增加：实现线程安全的懒汉模式可能需要编写复杂的代码，容易引入错误。&lt;/p&gt;
&lt;h3&gt;饿汉模式：&lt;/h3&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;p&gt;简单：饿汉模式实现简单，不需要考虑线程安全问题，因为实例在类加载时就已经创建。&lt;/p&gt;
&lt;p&gt;线程安全性：由于实例在类加载时创建，不会存在多个实例的风险，因此线程安全。&lt;/p&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;p&gt;无法实现延迟加载：饿汉模式在程序启动时就创建实例，无法实现延迟加载，可能会浪费资源，特别是当实例很大或初始化开销较大时。&lt;/p&gt;
&lt;p&gt;可能引起性能问题：如果单例类的实例在程序启动时没有被使用，那么创建实例的开销可能是不必要的。&lt;/p&gt;
&lt;p&gt;不适用于某些情况：如果单例对象的创建依赖于某些外部因素，而这些因素在程序启动时无法确定，那么饿汉模式可能不适用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;总的来说，懒汉模式适用于需要延迟加载实例的情况，可以节省资源和提高性能，但需要考虑线程安全性。饿汉模式适用于需要简单实现和线程安全性的情况，但不支持延迟加载。选择哪种模式应根据具体需求和性能考虑来决定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/a0008aa3e7dd417fa3ce9953aa960fdf.jpeg&quot; alt=&quot;请添加图片描述&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151372538&quot;&gt;【iOS】 单例模式&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】设计模式的六大原则</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-150927742-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-150927742-ios-/</guid><description>开闭原则 开放封闭原则的意思是： 对拓展开放，对修改关闭。 程序中的模块、类、方法应该允许在 不修改原来代码 的前提下进行功能拓展。 假设一个支付系统，最开始只有微信支付，如果后面还要支持银联或者支付宝支付： 新增支付时候，只需写一个新类基层Payment而不需要修改原有代码。 </description><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;开闭原则&lt;/h2&gt;
&lt;p&gt;开放封闭原则的意思是： 对拓展开放，对修改关闭。&lt;/p&gt;
&lt;p&gt;程序中的模块、类、方法应该允许在 &lt;strong&gt;不修改原来代码&lt;/strong&gt; 的前提下进行功能拓展。&lt;/p&gt;
&lt;p&gt;假设一个支付系统，最开始只有微信支付，如果后面还要支持银联或者支付宝支付：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Payment : NSObject
- (void)pay;
@end
@implementation Payment
- (void)pay {}
@end


@interface WeChatPayment : Payment
@end
@implementation WeChatPayment
- (void)pay {
    NSLog(@&quot;使用微信支付&quot;);
}
@end

@interface AlipayPayment : Payment
@end
@implementation AlipayPayment
- (void)pay {
    NSLog(@&quot;使用支付宝支付&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Payment *payment = [[WeChatPayment alloc] init];
[payment pay];

payment = [[AlipayPayment alloc] init];
[payment pay];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新增支付时候，只需写一个新类基层Payment而不需要修改原有代码。&lt;/p&gt;
&lt;p&gt;1.    UITableView&lt;/p&gt;
&lt;p&gt;•    UITableView 本身是封闭的，但我们可以通过实现 UITableViewDataSource 和 UITableViewDelegate 协议来扩展它的功能。&lt;/p&gt;
&lt;p&gt;•    这就是 OCP：Apple 不让你改 UITableView 源码，但提供扩展点给你。&lt;/p&gt;
&lt;p&gt;2.    分类 (Category)&lt;/p&gt;
&lt;p&gt;•    OC 的 Category 就是典型的 “对扩展开放” 的手段，可以在不修改原类源码的情况下加方法。&lt;/p&gt;
&lt;h2&gt;单一职能原则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/a0b29e6fbf244682b481e3a825d2f6a9.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个类应该仅有一个引起他变化的原因。&lt;/p&gt;
&lt;p&gt;这样可以提高可维护性，可拓展性，降低耦合度&lt;/p&gt;
&lt;p&gt;iOS的学习中，MVC架构其实就是单一职责的体现。（M管理数据，V管理页面， C管理交互）&lt;/p&gt;
&lt;h2&gt;李氏替换原则&lt;/h2&gt;
&lt;p&gt;子类对象能够拓展父类的功能但不能改变父类原有的功能。它包含两层含义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;子类可以实现父类的抽象方法，饭不能覆盖父类的非抽象方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;子类中可以增加自己的特有方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;里氏替换原则是实现开放封闭原则的重要方式之一。 - 它克服了继承中重写父类造成的可复用性变差的缺点。 - 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误，降低了代码出错的可能性&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@protocol Shape &amp;lt;NSObject&amp;gt;
- (NSInteger)calculateArea; //定义一个协议方法，也就是一个公共的接口
@end

NS_ASSUME_NONNULL_END

#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;Shape.h&quot;
NS_ASSUME_NONNULL_BEGIN

@interface Squre : NSObject&amp;lt;Shape&amp;gt; //实现一个正方形类来实现对应的接口的内容
@property (nonatomic, assign) NSInteger length;
@end

NS_ASSUME_NONNULL_END

#import &quot;Squre.h&quot;

@implementation Squre
- (NSInteger)calculateArea {
    return self.length * self.length;
}
@end

#import &quot;Shape.h&quot;
#import &amp;lt;Foundation/Foundation.h&amp;gt;
NS_ASSUME_NONNULL_BEGIN

@interface Rectangle : NSObject&amp;lt;Shape&amp;gt; //实现长方形类来实现对应接口的内容
@property (nonatomic, assign) NSInteger width;
@property (nonatomic, assign) NSInteger height;
@end

NS_ASSUME_NONNULL_END

#import &quot;Rectangle.h&quot;

@implementation Rectangle
- (NSInteger)calculateArea {
    return self.width * self.height;
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;依赖倒置原则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b2a3dce7c4fb407b9c338f140ecdcc76.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;高层模块不应该依赖底层模块，两者都应该依赖抽象。在oc中，抽象通常就是协议。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;高层模块：业务逻辑，比如 ViewController。 低层模块：具体实现，比如网络请求类、数据库类。 抽象：协议（protocol）或者基类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通俗的来说，不要让控制器依赖一个具体的类，而是依赖一个抽象接口。这样底层可以随时替换而不影响高层代码。&lt;/p&gt;
&lt;p&gt;模块间通过抽象发生；实现类之间没有依赖关系，所有的依赖关系通过接口/抽象类产生。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;我们先定义一个抽象协议：

@protocol NetworkService &amp;lt;NSObject&amp;gt;
- (void)fetchData;
@end

然后让具体类去实现：

@interface NetworkManager : NSObject &amp;lt;NetworkService&amp;gt;
@end

@implementation NetworkManager
- (void)fetchData {
    NSLog(@&quot;使用默认方式请求数据&quot;);
}
@end

控制器依赖的是协议而非具体类：

@interface MyViewController : UIViewController
@property (nonatomic, strong) id&amp;lt;NetworkService&amp;gt; networkManager;
@end

调用时可以随便替换实现：

self.networkManager = [[NetworkManager alloc] init];
[self.networkManager fetchData];

如果以后写一个MockNetworkManager来做单元测试，只要遵守NetWorkService协议，就能无缝切换
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;接口隔离原则&lt;/h2&gt;
&lt;p&gt;类之间的依赖关系应该建立在最小的接口上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;假设我们有一个多功能打印机协议

@protocol MultiFunctionPrinter &amp;lt;NSObject&amp;gt;
- (void)print;
- (void)scan;
- (void)fax;
@end

但是我现在有一个低级的打印机，只能实现打印，但却被迫实现scan和fax， 这就违反了接口隔离原则。

而正确做法，应该是我们把大接口分成多个小接口，这样 多功能打印机实现所有协议，低级打印机实现prit协议，扫描仪实现scanner协议。

@protocol Printer &amp;lt;NSObject&amp;gt;
- (void)print;
@end

@protocol Scanner &amp;lt;NSObject&amp;gt;
- (void)scan;
@end

@protocol Fax &amp;lt;NSObject&amp;gt;
- (void)fax;
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在 iOS/OC 开发中的应用 &lt;strong&gt;1. UITableView&lt;/strong&gt; • Apple 把 UITableView 的功能拆成了两个协议： • UITableViewDataSource → 专管数据 • UITableViewDelegate → 专管行为、UI • 而不是写成一个超级大协议。 ** 2. 可选协议方法** • 在 OC 里，有时协议方法是 @optional，这也是一种接口隔离的体现：只实现你需要的部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;迪米特法则&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9632bb789bce471f815f43d44bbbf163.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个对象应该尽可能少的了解其他对象的细节 （只和直接朋友交流，不和朋友的朋友交流）&lt;/p&gt;
&lt;p&gt;假如一个用户user，它里面有account，account里又有bank。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[user.account.bank deposit:100];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;user不仅知道了account，还知道了bank。 一旦bank的结构改了，外部代码全部受影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface User : NSObject
@property (nonatomic, strong) Account *account;
- (void)deposit:(CGFloat)amount;
@end

@implementation User
- (void)deposit:(CGFloat)amount {
    [self.account deposit:amount];
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而正确做法是我们在user里面封装一个方法，&lt;/p&gt;
&lt;p&gt;外部调用只需 [user deposit : 100];&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 iOS/OC 开发中的应用 1. 自定义控件 • 比如你的聊天程序里 chatViewController 的输入框 _textField 和发送按钮 _sendButton。 • 如果外部需要设置占位符，不应该直接 chatVC.textField.placeholder = @&quot;...&quot;， • 更好的做法是：在 chatViewController 提供一个方法 - (void)setInputPlaceholder:(NSString *)text;。 • 外部只调用这个方法，不关心内部是否用 UITextField 还是 UISearchBar。 2. 控制器之间的通信 • 不要直接访问另一个控制器的子控件，而是通过 属性、方法、代理 来沟通。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/150927742&quot;&gt;【iOS】设计模式的六大原则&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】多界面传值</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151262536-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151262536-ios-/</guid><description>目录 属性传值（正向传值） 协议传值 通知传值 block传值 KVO传值 在iOS中的页面传值方式主要以下六种： 属性传值 单例传值 NSUserDefault传值 代理传值 block传值 属性传值（正向传值） 属性传值是通过定义属性并设置值来实现数据传递的方式，多用于前一个</description><pubDate>Sun, 07 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%B1%9E%E6%80%A7%E4%BC%A0%E5%80%BC%EF%BC%88%E6%AD%A3%E5%90%91%E4%BC%A0%E5%80%BC%EF%BC%89&quot;&gt;属性传值（正向传值）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%8D%8F%E8%AE%AE%E4%BC%A0%E5%80%BC&quot;&gt;协议传值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%80%9A%E7%9F%A5%E4%BC%A0%E5%80%BC&quot;&gt;通知传值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#block%E4%BC%A0%E5%80%BC&quot;&gt;block传值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#KVO%E4%BC%A0%E5%80%BC&quot;&gt;KVO传值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在iOS中的页面传值方式主要以下六种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性传值&lt;/li&gt;
&lt;li&gt;单例传值&lt;/li&gt;
&lt;li&gt;NSUserDefault传值&lt;/li&gt;
&lt;li&gt;代理传值&lt;/li&gt;
&lt;li&gt;block传值&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;属性传值（正向传值）&lt;/h2&gt;
&lt;p&gt;属性传值是通过定义属性并设置值来实现数据传递的方式，多用于前一个页面向后一个页面传值。&lt;/p&gt;
&lt;p&gt;我们现在要跳转的第二个页面设置属性值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@property (nonatomic, strong) NSString *username;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在原来的页面创建B的实例，然后直接给他赋值，然后后一个页面就能拿到前一个页面的数值了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BViewController *bVC = [[BViewController alloc] init];
bVC.username = @&quot;小明&quot;;
[self.navigationController pushViewController:bVC animated:YES];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在天气预报中，本人就使用了这种传值，效果如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/53203036c7694304ab0a698089f52ca7.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;协议传值&lt;/h2&gt;
&lt;p&gt;协议传值是通过定义协议和代理方法，在不同页面之间传递数据。多用于后一个页面向前一个页面回传。在3gshare中，我们就假日详情页向首页传值就用到了这个。相当于上一个动图的反向。&lt;/p&gt;
&lt;p&gt;我们先在需要传输信息的页面定义协议：其中包括需要传递的数据的代理方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/36e766236efa46a6ac6605dd225f13d2.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d9f556757f2f46bb8cbab673145c311b.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后在发送数据的视图控制器的头文件声明代理属性，保存代理对象。&lt;/p&gt;
&lt;p&gt;触发代理方法：&lt;/p&gt;
&lt;p&gt;在发送数据的视图控制器中，在适当的时机，触发代理方法，并将需要传递的数据作为参数传递给代理方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/334f2a697699427395417b671c96c6a6.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后在接收的视图控制器中实现方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)holidayDetail:(HolidayDetailViewController *)detail
  didChangeLikeStatus:(BOOL)isLiked
         newLikeCount:(NSInteger)likeCount
{
    NSMutableDictionary *item = [self.dataArray[0] mutableCopy];
    item[@&quot;isLiked&quot;] = @(isLiked);
    item[@&quot;likeCount&quot;] = @(likeCount);
    [self.dataArray replaceObjectAtIndex:0 withObject:item];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:1];
    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后把后面VC的代理设为前一个VC，让后一个为前一个代理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/1c0f840f12af4418b09820909777b79a.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;通知传值&lt;/h2&gt;
&lt;p&gt;创建并发送通知：
首先，在发送者对象中创建一个通知，并指定通知的名称（通常使用字符串来表示）。可以通过NSNotification类或NSNotificationName宏来创建通知。需要传递信息给其他对象时，可以通过NSNotificationCenter的postNotificationName:object:userInfo:方法来发送通知。在发送通知时，可以附带一些额外的信息（如字典）作为通知的userInfo参数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/27f6aecc5f754d6ca8a20dca81cf4228.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后，我们需要注册观察者：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/a5ddd03547bc4977a2469c09a88a60c6.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接收通知：
接收者对象需要实现一个方法，用于处理接收到的通知。这个方法是在观察者注册时通过selector参数指定的。当通知被发送时，通知中心会调用这个方法，并传递相关的信息给观察者。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/fc1cc5f0794d498cabd5a7f5a0c8dcea.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后，我们别忘了移除观察者，将其从通知中心移除，避免内存泄露。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d808c01e360145c9ba194802e3509efd.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;block传值&lt;/h2&gt;
&lt;p&gt;在iOS开发中，可以使用Block（闭包）进行值的传递和回调操作。Block是一种封装了一段代码的对象，可以在需要的时候执行该代码块。
其基本步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在后一个页面定义一个Block属性&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@property (nonatomic, copy) void(^reutnblock)(NSString* temp, NSInteger num);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;设置一个时间来触发返回上一级视图控制器的时候，使用第一步定义的block，把需要传递的放在^blcok中间&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;-(void)press{
    self.reutnblock(self.label.text, --_tag); // 重点部分
    [self.navigationController popViewControllerAnimated:YES];

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在前一个页面跳转到后一个页面的事件函数中，调用后一个页面的Block这个属性&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;-(void)press {
    secondViewController* second = [[secondViewController alloc] init];
    second.reutnblock = ^(NSString * _Nonnull temp, NSInteger num) {
        self.label.text = temp;
        self.num = num;
    };
    [self.navigationController pushViewController:second animated:YES];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点&lt;/p&gt;
&lt;p&gt;Block提供了一种灵活的方式，可以封装一段代码并在需要的时候执行。这使得值传递的逻辑可以根据具体需求进行定制和扩展。
Block可以用于异步操作，例如在网络请求完成后执行回调操作。这样可以实现非阻塞的异步编程模式，提高代码的响应性和用户体验。
缺点&lt;/p&gt;
&lt;p&gt;使用Block时，需要注意对循环引用和内存泄漏的处理。Block可能会持有其所捕获的变量和对象，如果不正确地处理循环引用，可能会导致内存泄漏。
较复杂的Block可能会导致代码变得难以理解和维护。由于Block可以封装任意代码逻辑，过度复杂或嵌套的Block可能会降低代码的可读性和可维护性。&lt;/p&gt;
&lt;p&gt;block传值我目前还没在项目中实际使用过，以后在项目中的具体应用我会继续补充在博客中&lt;/p&gt;
&lt;h2&gt;KVO传值&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;概述&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KVO全称KeyValueObserve也就是观察者模式,是apple提供的一套事件通知机制.允许对象监听另一个对应特殊属性的改变,并在改变时接受到该事件.一般继承自NSObject的对象都默认是支持KVO。&lt;/p&gt;
&lt;p&gt;KVO(Key-Value-Observing,键值观察)，即观察关键字的值的变化。首先在子页面中声明一个待观察的属性，在返回主页面之前修改该属性的值。在主页面中提前分配并初始化子页面，并且注册对子页面中对应属性的观察者。在从子页面返回主页面之前，通过修改观察者属性的值，在主页面中就能自动检测到这个改变，从而读取子页面的数据。KVO只对属性发生作用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传递方向&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;主要也是从后往前。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.注册观察者&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@property (nonatomic, strong) secondViewController* second; //这里现在第一个页面中设置第二个页面作为自己的一个属性
- (void)press { //这里我才用了一个按钮来触发事件
    //[self willChangeValueForKey:@&quot;ary&quot;];
    self.second = [[secondViewController alloc] init];
    [self.second addObserver:self forKeyPath:@&quot;userName&quot; options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [self.navigationController pushViewController:self.second animated:YES];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;observer: 这是观察者对象，即要接收属性变化通知的对象。通常是当前视图控制器或其他感兴趣的对象。 keyPath: 要监听的属性的名称，以字符串表示。当该属性的值发生变化时，KVO 就会通知观察者。 options: 一个枚举值，用于指定监听的选项。这个参数可以设置为多个选项的组合，使用按位或（|）进行连接。常见的选项有： NSKeyValueObservingOptionNew: 当属性的值发生变化时，提供新的属性值作为通知的参数。 NSKeyValueObservingOptionOld: 当属性的值发生变化时，提供旧的属性值作为通知的参数。 NSKeyValueObservingOptionInitial: 在添加观察者时，立即发送一次通知，提供当前属性的值作为通知的参数。 NSKeyValueObservingOptionPrior: 在属性值发生实际变化之前，先发送一次通知，提供旧的属性值作为通知的参数。 context: 这是一个指针类型的参数，用于传递额外的上下文信息。通常情况下可以传入 NULL，表示不需要传递上下文信息。如果你需要在观察者中处理一些额外的信息，可以使用自定义的指针类型来传递数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;2.实现监听方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary&amp;lt;NSKeyValueChangeKey,id&amp;gt; *)change context:(void *)context {
    if ([keyPath isEqualToString:@&quot;userName&quot;]) {
        self.label.text = self.second.userName;
        NSLog(@&quot;old text:%@   new text:%@&quot;, [change objectForKey:NSKeyValueChangeOldKey], [change objectForKey:NSKeyValueChangeNewKey]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3.移除观察者&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)dealloc {
    [self removeObserver:self forKeyPath:@&quot;userName&quot;];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;tips&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;这种方式不可以实现对于数组元素的一个监听，因为KVO是对于setter方法的监听,而数组的addObject方法并不是setter方法的内容，所以无法通过上述方法实现对于数组的一个监听。&lt;/p&gt;
&lt;p&gt;不使用时要移除KVO&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151262536&quot;&gt;【iOS】多界面传值&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Cell的复用与自定义cell</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-148500158-cellcell/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-148500158-cellcell/</guid><description>本着what how why的法则，我们首先要了解什么是cell。 一、Cell简介 在 Objective C（OC） 中， cell 通常指的是 UITableViewCell 或 UICollectionViewCell，也就是 表格或网格视图中的“单元格” 。 UITab</description><pubDate>Sat, 06 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本着what how why的法则，我们首先要了解什么是cell。&lt;/p&gt;
&lt;h2&gt;一、Cell简介&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;Objective-C（OC）&lt;/strong&gt; 中，&lt;strong&gt;cell&lt;/strong&gt; 通常指的是 UITableViewCell 或 UICollectionViewCell，也就是&lt;strong&gt;表格或网格视图中的“单元格”&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;UITableViewCell 是 Apple 提供的标准类，用于 UITableView； - UICollectionViewCell 是用于 UICollectionView； - 可以使用系统提供的，也可以自定义 cell 样式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如在我们的微信 在“我”页面中的每一项就是一个cell。&lt;/p&gt;
&lt;p&gt;每一个 cell 可以显示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图标（imageView）&lt;/li&gt;
&lt;li&gt;文字（textLabel）&lt;/li&gt;
&lt;li&gt;副标题（detailTextLabel）&lt;/li&gt;
&lt;li&gt;指示箭头（accessoryView）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/1ca37c3a655e49c9a7d77f750d135bb0.jpeg&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如下图为UITableViewDelegate和UITableViewDataSource:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/1375d7d806e24caf9886af16539ba59d.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、cell的复用机制&lt;/h2&gt;
&lt;p&gt;如果你创建了 1000 行数据，滚动时不会创建 1000 个 cell，而是 &lt;strong&gt;只创建屏幕显示范围内的 cell + 少量缓存&lt;/strong&gt;，来回复用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;初始化的时候他会先创建cell的缓存字典 和 section的缓存array，以及一个用于存放复用cell的mutableSet（可变的集合）。并且它会去创建显示的（n+1）个cell，其他都是从中取出来重用。 当有cell滑出屏幕时，会将其放入到一个set中（相当于一个重用池），当UITableView要求返回cell的时候，datasource会先在集合中查找是否有闲置的cell，若有则会将数据配置到这个cell中，并将cell返回给UITabelView。 这大大减少了内存的开销。 因为我在滚动的过程中会出现一个将cell滚出屏幕外的时候，这时候如果我们一直创建cell的话，如果cell太多了就会出现一个内存开销过多的一个问题。所以我们要采用这个复用的方式来提高内存利用率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在表视图显示的时候，会创建（视图中可看的单元格个数+1）个单元格，一旦单元格因为滑动的而消失在我们的视野中的时候，消失的单元格就会进入缓存池（或叫复用池），当有新的单元格需要显示的时候，会先从缓存池中取可用的单元格，获取成功则使用获取到的单元格，获取失败则重新创建新的单元格，这就是整个的复用机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cell的复用与两种不同方式：&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;手动进行Cell复用（非注册）：&lt;/h3&gt;
&lt;p&gt;设置复用标识符：在创建Cell的时候，我们需要给每个Cell设置一个复用标识符，这个标识符通常是一个字符串，用来表示这个Cell的类型。在创建Cell的时候，我们会把这个标识符作为参数传入。&lt;/p&gt;
&lt;p&gt;请求重用的Cell：在需要显示新的Cell时，我们会使用复用标识符去请求一个已经不再显示，但是还没有被销毁的Cell。这个请求的过程是通过调用UITableView或UICollectionView的dequeueReusableCell(withIdentifier:)方法来完成的，这个方法会返回一个可选类型的Cell，如果有可用的重用Cell，就会返回一个Cell，否则返回nil。&lt;/p&gt;
&lt;p&gt;配置Cell：无论是新创建的Cell还是重用的Cell，都需要进行配置，以显示新的数据。配置Cell通常会在tableView(:cellForRowAt:)或collectionView(:cellForItemAt:)方法中完成。&lt;/p&gt;
&lt;p&gt;dequeueReusableCellWithIdentifier：意思是出列的可用的cell，即使用这个方法可以获取通过滚动创建过并放回对象池中的可以复用的cell对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)viewDidLoad {
    [super viewDidLoad];
    _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style: UITableViewStyleGrouped];
    //设置两个代理
    _tableView.delegate = self;
    _tableView.dataSource = self;
    [self.view addSubview:_tableView];
    // Do any additional setup after loading the view.
}
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString* cellStr = @&quot;cell&quot;;
    UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellStr];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellStr];
    }
    return cell;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自动（注册）：&lt;/h2&gt;
&lt;p&gt;使用cell的注册机制，在cell的复用的时候不需要判空，在viewDidLoad中先对需要复用的cell使用registerClass进行注册，然后在创建cell的函数中使用dequeueReusableCellWithIdentifier获取可复用的cell，如果没有可复用的cell，就自动利用注册cell时提供的类创建一个新的cell并返回。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;(void)viewDidLoad {
[super viewDidLoad];
self.tableView = [[UITableView alloc] initWithFrame:self.view.frame style:UITableViewStyleGrouped];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.ary = @[@&quot;头像&quot;, @&quot;名字&quot;, @&quot;微信号&quot;];
[self.view addSubview:_tableView];
[self.tableView registerClass:[cell02 class] forCellReuseIdentifier:@&quot;cell&quot;]; //使用代码自定义cell
// Do any additional setup after loading the view.
}&lt;/li&gt;
&lt;li&gt;(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath &lt;em&gt;)indexPath {
cell02&lt;/em&gt; cell = [tableView dequeueReusableCellWithIdentifier:@&quot;cell&quot; forIndexPath:indexPath];
return cell;
}&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;

&amp;gt; 非注册和注册的区别在于： 非注册每一次使用都要判空，注册的方法需要先注册要复用的cell，当不需要在获取cell的时候手动判断cell是否为nil


这两种方式都是可以复用，两者有部分区别，引用学长的学长的话：


&amp;gt; 上述代码的区别在于注册方法需要提前对我们要使用的cell类进行注册，如此一来就不需要在后续过程中对我们的单元格进行判空。 这是因为我们的注册方法： (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier API_AVAILABLE(ios(6.0)); 在调用过程中会自动返回一个单元格实例，如此一来我们就避免了判空操作


&amp;gt; 引用的方法不同 - (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0); 第一个 method 用在了非注册的方式里，第二个 method 用在了需要注册的方式里。经过验证，第一个 method 也可以用在注册的方式里，但是第二个 method 如果用于非注册的方式，则会报错崩溃:


## 三、自定义cell


&amp;gt; 自定义 cell 就是你自己创建一个 UITableViewCell 的子类，并在其中添加你需要的控件（UILabel、UIImageView、UIButton 等），然后在表格中使用它。


我们首先要实现自定义cell需要两个协议


UITableViewDelegate和UITableViewDataSource。


前者的主要用于实现显示单元格，设置单元格的行高和对于制定的单元格的操作设置头视图和尾视图。


```objective-c
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
- (void)tableView:(UITableView *)tableView didEndDisplayingFooterView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后者主要用于设置UITableView的section和row的数量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;

// Row display. Implementers should *always* try to reuse cells by setting each cell&apos;s reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的UIVIewContorller中要实现上面的部分协议函数，后面就是我们有关自定义cell的核心部分，就是我们的自定义的cell的类的部分。
我们自定义cell先要创建一个类继承UITableViewCell&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;UIKit/UIKit.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN
@interface myCustomCell : UITableViewCell
@property (nonatomic, strong) UIButton* btn;
@property (nonatomic, strong) UILabel* label1;
@property (nonatomic, strong) UILabel* label2;
@property (nonatomic, strong) UIImageView* imageView01;
@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后需要重写一个方法&lt;code&gt;- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];

    if ([self.reuseIdentifier isEqualToString:@&quot;picture&quot;]) {
        _label1 = [[UILabel alloc] init];
        _label1.textColor = UIColor.blackColor;
        _label1.font = [UIFont systemFontOfSize:17];
        [self.contentView addSubview:_label1];

        _label2 = [[UILabel alloc] init];
        _label2.textColor = UIColor.grayColor;
        _label2.font = [UIFont systemFontOfSize:10];
        [self.contentView addSubview:_label2];

        _imageView01 = [[UIImageView alloc] init]; // 创建UIImageView对象
        [self.contentView addSubview:_imageView01];

        _btn = [UIButton buttonWithType:UIButtonTypeCustom];
        [self.contentView addSubview:_btn];
    }
    return self;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们对cell中的控件进行布局&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(void) layoutSubviews {
    self.name.frame = CGRextMake(50,0,200,60);
    self.icon.frame = CGRectMake(15, 20, 20, 20);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、cell的复用原理&lt;/h2&gt;
&lt;p&gt;在上上届学长的博客中有完整提及，本人能力有限，还望大家移步进行了解5&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_72437555/article/details/131273183?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=131273183&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;【iOS】自定义cell及其复用机制_表视图复用机制原理-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/148500158&quot;&gt;Cell的复用与自定义cell&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>OC语言学习——Foundation框架回顾及考核补缺</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-148107284-ocfoundation/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-148107284-ocfoundation/</guid><description>目录 和点语法区别： 属性关键字 Foundation框架 数组（NSArray和NSMutableArray） 对几何元素的整体调用方法 排序 使用枚举器遍历元素 可变数组 集合（NSSet与NSMutableSet） NSMutableSet NSCountedSet的用法 </description><pubDate>Sat, 06 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#-%3E%E5%92%8C%E7%82%B9%E8%AF%AD%E6%B3%95%E5%8C%BA%E5%88%AB%EF%BC%9A&quot;&gt;-&amp;gt;和点语法区别：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%B1%9E%E6%80%A7%E5%85%B3%E9%94%AE%E5%AD%97&quot;&gt;属性关键字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Foundation%E6%A1%86%E6%9E%B6%C2%A0&quot;&gt;Foundation框架&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%95%B0%E7%BB%84%EF%BC%88NSArray%E5%92%8CNSMutableArray%EF%BC%89%C2%A0&quot;&gt;数组（NSArray和NSMutableArray）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%AF%B9%E5%87%A0%E4%BD%95%E5%85%83%E7%B4%A0%E7%9A%84%E6%95%B4%E4%BD%93%E8%B0%83%E7%94%A8%E6%96%B9%E6%B3%95&quot;&gt;对几何元素的整体调用方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%8E%92%E5%BA%8F&quot;&gt;排序&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8%E6%9E%9A%E4%B8%BE%E5%99%A8%E9%81%8D%E5%8E%86%E5%85%83%E7%B4%A0&quot;&gt;使用枚举器遍历元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%8F%AF%E5%8F%98%E6%95%B0%E7%BB%84%C2%A0&quot;&gt;可变数组&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%9B%86%E5%90%88%EF%BC%88NSSet%E4%B8%8ENSMutableSet%EF%BC%89&quot;&gt;集合（NSSet与NSMutableSet）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#NSMutableSet&quot;&gt;NSMutableSet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#NSCountedSet%E7%9A%84%E7%94%A8%E6%B3%95&quot;&gt;NSCountedSet的用法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%9C%89%E5%BA%8F%E9%9B%86%E5%90%88&quot;&gt;有序集合&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%AD%97%E5%85%B8&quot;&gt;字典&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#NSDictionary%C2%A0&quot;&gt;NSDictionary&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#NSMutableDictionary&quot;&gt;NSMutableDictionary&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;因作者在考核中被倪神殴打，痛定思痛决定对部分内容开展回头看。&lt;/p&gt;
&lt;h2&gt;-&amp;gt;和点语法区别：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. -&amp;gt;这个方式的访问是直接调用所指向的一个内存，这样子更快，而点语法实际上是调用他的setter语句和getter语句,这个语句调用的速度更慢&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 需要注意的是使用点语法访问属性的时候可以直接用“.”+属性名，而使用“-&amp;gt;&quot;访问属性的时候则不能省略成员变量名前的下划线“_”否则会报错。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们一般不采用-&amp;gt;的方式访问成员变量，但是成员变量默认受保护，所以常常报错。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;self在类方法里指向的是（调用当前类方法的）类&lt;/p&gt;
&lt;p&gt;self在对象方法中，self指的是对象。&lt;/p&gt;
&lt;h2&gt;属性关键字&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;atomic和nonatomic&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;atomic：在OC中属性的默认声明为atomic，他可以保证对于属性的赋值和取值是一定线程安全的，但是如果对于数组这种对象的话他有存在问题，他对于数组对象的删除和添加操作是不安全的。保证读写操作的安全。&lt;/li&gt;
&lt;li&gt;nonatomic：这个是不保证线程安全的，这个访问速度更快&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;stong和weak&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;strong：这个关键字会让修饰的对象引用计数加一。strong可以保证被该属性引用的对象被不被回收&lt;/li&gt;
&lt;li&gt;weak：这个关键字表示对这个对象进行一个弱引用，该指示符主要的用处是可以避免循环引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;strong和copy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果属性声明中指定了copy方法，合成方法会使用类的copy方法，这里注意：属性没有mutablecopy属性。即使是可变的实例变量，也是使用copy特性，正如方法copyWithZone：的执行结果。所以，按照约定会生成一个对象的不可变副本。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSInteger age;
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用copy修饰NSString&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person* p1 = [[Person alloc] init];
        NSMutableString *s1 = [NSMutableString stringWithString:@&quot;ggg&quot;];
        p1.name = s1;
        [s1 appendString:@&quot;111&quot;];
        NSLog(@&quot;%@&quot;, p1.name);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/3bb34fdddbdc4e7b821e2279bf39695d.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果我们用strong修饰，那么就&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d7242baa212c456483cd297a713df582.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为s1是可变的，person.name属性是copy，所以创建了新的字符串，属于深拷贝，内容拷贝，我们拷贝出来了一个对象，后面的赋值操作都是针对新建的对象进行操作，而我们实际的调用还是原本的对象。所以值并不会改变。 如果设置为strong，strong会持有原来的对象，使原来的对象引用计数+1，其实就是浅拷贝、指针拷贝。这时我们进行操作，更改其值就使本对象发生了改变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Foundation框架&lt;/h2&gt;
&lt;h3&gt;数组（NSArray和NSMutableArray）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;增&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSArray *array = @[@&quot;苹果&quot;, @&quot;香蕉&quot;, @&quot;橘子&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NSMutableArray *mutableArray = [NSMutableArray array]; // 空数组
[mutableArray addObject:@&quot;苹果&quot;];                     // 添加一个元素
[mutableArray addObjectsFromArray:@[@&quot;香蕉&quot;, @&quot;橘子&quot;]]; // 添加多个元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString *fruit = [mutableArray objectAtIndex:1];  // 访问第2个元素
NSLog(@&quot;第二个水果是：%@&quot;, fruit);

NSLog(@&quot;数组元素总数：%lu&quot;, (unsigned long)[mutableArray count]);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;for (NSString *item in mutableArray) {
    NSLog(@&quot;%@&quot;, item);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mutableArray replaceObjectAtIndex:0 withObject:@&quot;西瓜&quot;]; // 替换第一个元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mutableArray removeObjectAtIndex:1];     // 删除索引为1的元素
[mutableArray removeObject:@&quot;西瓜&quot;];       // 删除指定值（第一次出现的）
[mutableArray removeLastObject];          // 删除最后一个元素
[mutableArray removeAllObjects];          // 删除所有元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建数组的常见方法介绍：&lt;/p&gt;
&lt;p&gt;arrary：创建一个不包含任何元素的空NSArray
arrayWithContentsOfFile:/initWithContentsOfFile:读取文件内容来创建
arrayWithObject：创建仅包含指定元素的NSArray
arrayWithObjects：创建包含指定的N个元素的NSArray；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray* array = [NSArray arrayWithObjects:@&quot;LV&quot;, @&quot;Burberry&quot;,@&quot;BV&quot;,@&quot;LR&quot; ,@&quot;miumiu&quot;,nil];
        NSLog(@&quot;first:%@&quot;,[array objectAtIndex:1]);
        NSLog(@&quot;last:%@&quot;,[array lastObject]);

        //截取索引2开始长度为1的数组
        NSArray* array1 = [array objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(2, 1)]];
        NSLog(@&quot;%@&quot;,array1);
        //查找对象在数组中的索引
        NSLog(@&quot;miumiu: %ld&quot;, [array indexOfObject:@&quot;miumiu&quot;]);
        //在索引范围2，3中查找BV，结果是：
        NSLog(@&quot;BV in(2,2) :%ld&quot;, [array indexOfObject:@&quot;BV&quot; inRange: NSMakeRange(2, 2)]);
        array = [array arrayByAddingObject:@&quot;Mr.Dandy&quot;];
        array = [array arrayByAddingObjectsFromArray:[NSArray arrayWithObjects:@&quot;alo&quot;, @&quot;an&quot;, nil]];
        for (int i = 0; i &amp;lt; array.count; i++) {
            NSLog(@&quot;%@&quot;, [array objectAtIndex:i]);
        }
        //从索引2开始截取1个元素，得到新数组array2，只包含“BV”
        NSArray* array2 = [array subarrayWithRange:NSMakeRange(2, 1)];
        for (int i = 0; i &amp;lt; array2.count; i++) {
            NSLog(@&quot;%@&quot;, [array2 objectAtIndex:i]);
        }
    }
    return 0;
}
#### 对几何元素的整体调用方法


笔者暂时还未理解，后续补全


#### 排序


&amp;gt; _sortedArrayUsingFunction:contes_:该方法使用排序函数对集合元素进行一个排序,该排序函数必须返回NSOrderedDesceding,NSOrderedAscending,NSOrderedSame三个枚举常量。 _sortedArrayUsingSelector:_该方法使用集合元素自身的方法对集合元素进行排序，它的排序函数同样会返回上面给出的三个枚举值。 _sortedArrayUsingComparator:_该方法使用代码块对与集合元素进行排序


三个方法的返回值都是一个排序好的NSArray对象。


```objective-c
NSInteger inSort (id num1, id num2, void* context) {
    int v1 = [num1 intValue];
    int v2 = [num2 intValue];
    if (v1 &amp;lt; v2) {
        return NSOrderedAscending;
    } else if (v1 == v2) {
        return NSOrderedSame;
    } else {
        return NSOrderedDescending;
    }
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray* array1 = [NSArray arrayWithObjects:@&quot;C++&quot;,@&quot;Python&quot;,@&quot;Perl&quot;,@&quot;C&quot;,@&quot;Objective-C&quot;,@&quot;Ruby&quot;, nil];
        array1 = [array1 sortedArrayUsingSelector:@selector(compare:)];
        NSLog(@&quot;%@&quot;, array1);
        NSArray* array2 = [NSArray arrayWithObjects:[NSNumber numberWithInt:20], [NSNumber numberWithInt:12], [NSNumber numberWithInt:-8],[NSNumber numberWithInt:50], [NSNumber numberWithInt:19],nil];
        array2 = [array2 sortedArrayUsingFunction:inSort context:nil];
        NSLog(@&quot;%@&quot;, array2);
        NSArray* array3 = [array2 sortedArrayUsingComparator:^(id obj1, id obj2) {
            if ([obj1 intValue] &amp;gt; [obj2 intValue]) {
                return NSOrderedDescending;
            } else if ([obj1 intValue] &amp;lt; [obj2 intValue]) {
                return NSOrderedAscending;
            } else {
                return NSOrderedSame;
            }
        }];
        NSLog(@&quot;%@&quot;, array3);
    }
    return 0;
}
#### 使用枚举器遍历元素


我们有两个方法获得枚举器


- objectEnumerator:正序遍历
- reverseObjectEnumerator:逆序遍历

枚举器的方法有以下两个


- allObjects:获取枚举集合中所有的元素
- reverseObjectEnumerator:获取被枚举集合中的下一个元素。


```objective-c
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray* array1 = [NSArray arrayWithObjects:@&quot;C++&quot;,@&quot;Python&quot;,@&quot;Perl&quot;,@&quot;C&quot;,@&quot;Objective-C&quot;,@&quot;Ruby&quot;, nil];
        NSEnumerator* en = [array1 objectEnumerator];
        id object;
        while (object = [en nextObject]) {
            NSLog(@&quot;%@&quot;, object);
        }
        NSLog(@&quot;&quot;);
        NSEnumerator* an = [array1 reverseObjectEnumerator];
        while (object = [an nextObject]) {
            NSLog(@&quot;%@&quot;, object);
        }

    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/2bce35d574e4485497d42d2973c07a9f.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们也可以使用（for  in）语法快速枚举&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(type variableName in collection) {
  //variableName自动迭代访问每一个元素
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray* array1 = [NSArray arrayWithObjects:@&quot;C++&quot;,@&quot;Python&quot;,@&quot;Perl&quot;,@&quot;C&quot;,@&quot;Objective-C&quot;,@&quot;Ruby&quot;, nil];
        for (id object in array1) {
            NSLog(@&quot;%@&quot;, object);
        }

    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/fae3adac43214fb3a5fbc9f9d16bbfb4.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;可变数组&lt;/h3&gt;
&lt;p&gt;NSArray表示元素不可变的集合，一旦创建成功，程序不能向集合调价新的元素。&lt;/p&gt;
&lt;p&gt;可变数组给出了增加，删除，替换元素方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray* initArray = [[NSArray alloc] initWithObjects:@&quot;BV&quot;,@&quot;Barbour&quot;,@&quot;miumiu&quot;, nil];
        NSLog(@&quot;initArray: %@&quot;, initArray);
        NSArray* initArray2 = [NSArray arrayWithObjects:@&quot;BV&quot;,@&quot;Barbour&quot;,@&quot;miomio&quot;, nil];
        NSArray* initArray3 = @[@&quot;BV&quot;,@&quot;On&quot;,@&quot;Lulu&quot;];
        NSArray* initArray4 = [[NSArray alloc] initWithArray:initArray2];
        NSLog(@&quot;initArray2: %@&quot;, initArray2);
        NSLog(@&quot;initArray3: %@&quot;, initArray3);
        NSLog(@&quot;initArray4: %@&quot;, initArray4);

        NSInteger count = [initArray4 count];
        NSLog(@&quot;%ld&quot;,(long)count);

        NSString* str = [initArray objectAtIndex:1];  //从零开始，所以是第二个
        NSLog(@&quot;%@&quot;, str);
        NSLog(@&quot;%@&quot;, initArray[1]);

        BOOL flag = [initArray containsObject:@&quot;BV&quot;];
        NSLog(@&quot;%d&quot;, flag);

        for (NSString* name in initArray) {
            NSLog(@&quot;%@&quot;, name);
        }
        for (int i = 0; i &amp;lt; initArray.count; i++) {
            NSLog(@&quot;%@&quot;, initArray[i]);
        }

        // 空数组
        NSMutableArray* mutableArray = [NSMutableArray array];
        NSMutableArray* mutableArray2 = [[NSMutableArray alloc] init];
        NSArray* origin = @[@&quot;A&quot;, @&quot;B&quot;];
        NSMutableArray* mutableArray3 = [NSMutableArray arrayWithArray:origin];

        [mutableArray insertObject:@&quot;Burberry&quot; atIndex:0];
        NSLog(@&quot;mutableArray:%@&quot;, mutableArray);
        [mutableArray addObject:@&quot;ecco&quot;];
        NSLog(@&quot;mutableArray:%@&quot;, mutableArray);

        NSMutableArray* mutableArray4 = [@[@&quot;a&quot;, @&quot;b&quot;,@&quot;c&quot;, @&quot;d&quot;]mutableCopy];
        NSLog(@&quot;mutableArray4:%@&quot;, mutableArray4);

        [mutableArray4 replaceObjectAtIndex:3 withObject:@&quot;dd&quot;];
        NSLog(@&quot;mutableArray4:%@&quot;, mutableArray4);

        [mutableArray4 removeObject:@&quot;c&quot;];
        NSLog(@&quot;mutableArray4:%@&quot;, mutableArray4);

        [mutableArray4 removeObjectAtIndex:2];
        NSLog(@&quot;mutableArray4:%@&quot;, mutableArray4);


    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;集合（NSSet与NSMutableSet）&lt;/h2&gt;
&lt;p&gt;NSSet集合不允许包含相同元素（他没有明显顺序），如果试图将两个相同的元素放在同一个NSSet集合，则只会保留一个&lt;/p&gt;
&lt;p&gt;功能与用法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实际上，NSArray与NSSet依然有大量的相似之处。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;·都可以通过count方法获取集合元素数量
·可以用快速枚举遍历集合元素
·可以通过objectEnumerator方法获取NSEnumerator枚举器对集合元素遍历。
·提供了makeObjectsPerformSelector:方法对集合元素真题调用某个方法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接下来我们介绍一下NSSet的常用方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;setByAddingObject:向集合中添加一个新元素，返回添加元素后的新集合
setByAddingObjectsFromSet:使用NSSet集合向集合中添加多个新元素，返回新集合
setByAddingObjectsFromArray:使用NSArray集合向集合中添加多个新元素，返回新集合
allObjects:返回集合中所有元素组成的NSArray
anyObject:返回集合中的某个元素。（但是并不保证随机返回集合元素）。
containsObject:判断集合是否包含指定元素
Member:判断该集合是否包括与该参数相等的元素，如果包含就返回相等的元素，反之则返回nil。
objectsPassingTest：需要传入一个代码块对集合元素进行一个过滤，满足该代码块的集合元素会被保留下来并组成一个新的NSSet集合作为返回值
objectsWithOptions:passingTest:和前一个方法的功能基本相似，只是可以额外地传入一个NSEnumerationOptions作为迭代参数
isSubsetOfSet:判断是否为一个子集
intersectsSet:判断是否有交集
isEqualToset:判断集合是否相同&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NSString *NSCollectionToString(id array) {
    NSMutableString *result = [NSMutableString stringWithString:@&quot;[&quot;];
    for (id obj in array) {
        [result appendString: [obj description]];
        [result appendString:@&quot;,&quot;];
    }
    NSUInteger len = [result length];
    [result deleteCharactersInRange:NSMakeRange(len - 1, 1)];//去掉最后一个字符
    [result appendString:@&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSSet* set1 = [NSSet setWithObjects:@&quot;iOS&quot;,@&quot;Android&quot;, @&quot;web&quot;, @&quot;server&quot;, nil];
        NSLog(@&quot;set的个数为%ld&quot;, [set1 count]);
        NSLog(@&quot;%@&quot;, NSCollectionToString(set1));
        NSSet* set2 = [NSSet setWithObjects:@&quot;krystal&quot;,@&quot;xiyang&quot;,@&quot;fenfneko&quot;,@&quot;web&quot;, nil];
        NSLog(@&quot;%@&quot;, NSCollectionToString(set2));
        set1 = [set1 setByAddingObject: @&quot;shenyi&quot;];
        NSSet* s1 = [set1 setByAddingObjectsFromSet:set2];
        NSLog(@&quot;并集合%@&quot;, NSCollectionToString(s1));
        BOOL bo = [set2 isSubsetOfSet:set1];
        NSLog(@&quot;set2是否是set1子集&quot;, bo);
        BOOL bb = [set1 containsObject:@&quot;Android&quot;];
        NSLog(@&quot;是否包含Android:%d&quot;,bb);
        NSLog(@&quot;从set1中取出一个%@&quot;, [set1 anyObject]);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/64013755d8b2494ebd13028db06b43a0.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;判断元素重复的标准&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里涉及到了一个Hash表的内容，我们每存入一个元素那么我们的NSSet会带调用该对象的一个Hash方法来计算一个Hash值，然后根据HashCode计算出元素在底层Hash表中的存储位置。如果出现了两个元素通过了isEqual方法但是它俩的值相同，那么我们这里的方式是通过链表将这两个元素连接起来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;NSSet中判断两个元素相等的标准如下：&lt;/p&gt;
&lt;p&gt;两个对象通过isEqual方法返回YES；&lt;/p&gt;
&lt;p&gt;两个对象的hash方法的返回值也相等；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSSet* array = [NSSet setWithObjects:
                                  [[FKUser alloc] initWithName:@&quot;sun&quot; pass:@&quot;123&quot;],
                                  [[FKUser alloc] initWithName:@&quot;bai&quot; pass:@&quot;345&quot;],
                                   [[FKUser alloc] initWithName:@&quot;zhu&quot; pass:@&quot;123&quot;],
                                    [[FKUser alloc] initWithName:@&quot;tang&quot; pass:@&quot;178&quot;],
                                     [[FKUser alloc] initWithName:@&quot;niu&quot; pass:@&quot;155&quot;], nil];
        NSLog(@&quot;元素个数为%ld&quot;, [array count]);
        NSLog(@&quot;%@&quot;, NSCollectionToString(array));
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们会发现输出两个相同的 “sun”和“123”；因此我们要重写一下hash方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (NSUInteger) hash {
    NSLog(@&quot;===hash===&quot;);
    NSUInteger nameHash = _name == nil ? 0 : [_name hash];
    NSUInteger passHash = _pass == nil ? 0 : [_pass hash];
    return nameHash * 31 + passHash;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后结果就正常了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9116326356304d21853a2681052328d6.jpeg&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我们执行了5次hash方法，这一步说明我们每次添加一个集合与元素，总会先调用该元素的hash方法，在重写这两种方法的时候，我们的目的主要是为了满足NSSet性质，为了保证我们的NSSet保持一个较高的性能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tips：Hash方法对于NSSet是非常重要的，下面给出重写Hash方法的基本原则。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一对象返回的Hash值应该是相同的
isEqual:方法比较返回YES的时候，这两个对象的Hash应返回相等的值·
对象中所有被isEqual比较标准的实例变量都应该用来就算hashCode值。
一般情况我们的返回值都写成[f1 hash] * (质数) + [f2 hash]。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;NSMutableSet&lt;/h3&gt;
&lt;p&gt;和array相似，add（增加），remove（删除）。&lt;/p&gt;
&lt;p&gt;主要要记住交，并，交，差集的函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unionSet:计算并集&lt;/li&gt;
&lt;li&gt;minusSet:计算差集&lt;/li&gt;
&lt;li&gt;intersectSet:计算交集&lt;/li&gt;
&lt;li&gt;setSet:用后一个元素替换已有集合中的所有元素&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;NSCountedSet的用法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;NSCountedSet主要是有一个特色就是它会为每一个元素增加一个该元素出现的个数，我们可以不可以添加元素，但是如果我们添加的元素重复了，但是会将该元素的添加次数加1.删除元素的时候也是将这个元素添加次数减1，知道它为0的时候，这个元素才会真正的从NSCountedSet中删除。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSCountedSet* set1 = [NSCountedSet setWithObjects:@&quot;iOS讲义&quot;,@&quot;Android&quot;,@&quot;Java&quot;, nil];
        [set1 addObject:@&quot;Java&quot;];
        [set1 addObject:@&quot;Java&quot;];
        NSLog(@&quot;%@&quot;, NSCollectionToString(set1));
        NSLog(@&quot;Java的添加次数为%ld&quot;, [set1 countForObject:@&quot;Java&quot;]);
        [set1 removeObject:@&quot;Java&quot;];
        NSLog(@&quot;%@&quot;, NSCollectionToString(set1));
        NSLog(@&quot;Java的添加次数为%ld&quot;, [set1 countForObject:@&quot;Java&quot;]);
        [set1 removeObject:@&quot;Java&quot;];
        [set1 removeObject:@&quot;Java&quot;];
        NSLog(@&quot;%@&quot;, NSCollectionToString(set1));
        NSLog(@&quot;Java的添加次数为%ld&quot;, [set1 countForObject:@&quot;Java&quot;]);
    }
    return 0;
}
### 有序集合


NSOrderedSet最主要的点在于可以保持与安素的添加顺序，而且每一个元素都有缩影，可以根据缩影来操作元素。


```objective-c
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSOrderedSet* set = [NSOrderedSet orderedSetWithObjects:[NSNumber numberWithInt:40],[NSNumber numberWithInt:12],[NSNumber numberWithInt:-9],[NSNumber numberWithInt:20], nil];
        NSLog(@&quot;first :%@&quot;, [set firstObject]);
        NSLog(@&quot;last:%@&quot;, [set lastObject]);
        NSLog(@&quot;索引为2的元素:%@&quot;, [set objectAtIndex:2]);
        NSIndexSet* indexSet = [set indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL* stop) {
            return (BOOL)([obj intValue] &amp;gt; 10); // 返回大于10的值
        }];
        NSLog(@&quot;%@&quot;, indexSet);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[mdict setObject:@&quot;西邮&quot; forKey:@&quot;university&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e6c58d76ad04416ead85ada33b285414.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSSet* initSet = [[NSSet alloc] initWithObjects:@&quot;a&quot;,@&quot;b&quot;,@&quot;c&quot;, nil];
        NSLog(@&quot;%@&quot;, initSet);

        NSSet* initSet2 = [NSSet setWithObjects:@&quot;a&quot;,@&quot;b&quot;,@&quot;c&quot;, nil];
        NSLog(@&quot;%@&quot;, initSet2);

        NSString* str = [initSet2 anyObject];
        NSLog(@&quot;%@&quot;, str);

NSMutableSet *mInitSet = [[NSMutableSet alloc]initWithCapacity:2];

            //添加 (再次添加后输出时还是只有一个，但是不会报错)
            [mInitSet addObject:@&quot;xiaohong&quot;];
            NSLog(@&quot;mInitSet = %@&quot;,mInitSet);
            //重复添加是无效的，但是不会报错
            [mInitSet addObject:@&quot;xiaohuang&quot;];
            NSLog(@&quot;mInitSet = %@&quot;,mInitSet);

            //删除
            [mInitSet removeObject:@&quot;xiaohong&quot;];
            NSLog(@&quot;mInitSet = %@&quot;,mInitSet);

            //删除所有
            [mInitSet removeAllObjects];
            NSLog(@&quot;mInitSet = %@&quot;,mInitSet);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字典&lt;/h2&gt;
&lt;p&gt;这里的字典和C++的map大致相同，他拥有一个键值对，所以key放在一个set集合，但是如果我们采用一个allKeys方法来返回一个NSArray。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSDictionary *dict = @{@&quot;name&quot;: @&quot;胖猫&quot;, @&quot;age&quot;: @18};


NSMutableDictionary *mdict = [NSMutableDictionary dictionary];
[mdict setObject:@&quot;胖猫&quot; forKey:@&quot;name&quot;];
[mdict setObject:@18 forKey:@&quot;age&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果key不存在，新增。如果key已存在，相当于修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mdict setObject:@&quot;西邮&quot; forKey:@&quot;university&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString *name = [mdict objectForKey:@&quot;name&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NSString *name = mdict[@&quot;name&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;改：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mdict setObject:@&quot;乌萨奇&quot; forKey:@&quot;name&quot;]; // 原来的 &quot;胖猫&quot; 被覆盖
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;删：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mdict removeObjectForKey:@&quot;age&quot;];
### NSDictionary


键值对：NSDictionary 由键（key）和相应的值（value）组成。键必须是对象类型，通常为 NSString，而值可以是任何对象类型，甚至可以是 nil。每个键在 NSDictionary 中必须是唯一的，但值可以重复。


**不可变性**：一旦创建 NSDictionary，就不能修改其内容。如果需要进行修改操作，可以使用可变版本的数据结构 NSMutableDictionary。


**无序性**：NSDictionary 中的键值对是无序的，即没有固定的顺序。如果需要按照特定顺序访问键值对，可以使用 NSArray 来存储键的顺序，并通过键来访问对应的值。


快速查找：NSDictionary 使用散列表（hash table）实现，对于大多数情况下的查找操作具有很高的效率。通过给定键，可以快速检索对应的值。


**创建 NSDictionary 对象的方法：**


使用字面量语法创建：
@{key1: value1, key2: value2, ...}

 使用类方法创建：+[NSDictionary dictionaryWithObjectsAndKeys:] 或 +[NSDictionary dictionaryWithObjects:forKeys:]
 使用初始化方法创建：-initWithObjectsAndKeys: 或 -initWithObjects:forKeys:


 **访问数据：**


通过键获取值：-objectForKey: 或 [] 语法。
 获取所有键或值的集合：-allKeys 和 -allValues 方法。


 **遍历字典：**可以使用快速枚举语法 for-in，或者使用 -enumerateKeysAndObjectsUsingBlock: 方法传入一个 block 进行遍历操作。
 **判断是否包含某个键或值**：可以使用 -containsKey: 和 -containsObject: 方法来判断是否存在指定的键或值。


需要注意的是，NSDictionary 是不可变的，一旦创建就不能修改其内容。如果需要在运行时进行增加、删除或修改操作，可以使用可变版本的 NSMutableDictionary。


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

void demoNSDictionary() {
    NSLog(@&quot;\n=== NSDictionary 示例 ===&quot;);

    // 创建方式
    NSDictionary *dict1 = @{@&quot;name&quot;: @&quot;胖猫&quot;, @&quot;age&quot;: @3};
    NSDictionary *dict2 = [NSDictionary dictionaryWithObjectsAndKeys:@&quot;胖猫&quot;, @&quot;name&quot;, @3, @&quot;age&quot;, nil];
    NSArray *keys = @[@&quot;name&quot;, @&quot;age&quot;];
    NSArray *values = @[@&quot;胖猫&quot;, @3];
    NSDictionary *dict3 = [[NSDictionary alloc] initWithObjects:values forKeys:keys];

    // 访问数据
    NSLog(@&quot;name = %@&quot;, [dict1 objectForKey:@&quot;name&quot;]);
    NSLog(@&quot;age = %@&quot;, dict1[@&quot;age&quot;]);

    // 获取所有键 / 值
    NSLog(@&quot;所有键: %@&quot;, [dict1 allKeys]);
    NSLog(@&quot;所有值: %@&quot;, [dict1 allValues]);

    // 遍历 for-in
    for (id key in dict1) {
        NSLog(@&quot;%@ =&amp;gt; %@&quot;, key, dict1[key]);
    }

    // 遍历 block
    [dict1 enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        NSLog(@&quot;(block) %@ = %@&quot;, key, obj);
    }];
}
### NSMutableDictionary


 当涉及到 NSDictionary 的可变版本时，我们可以使用 NSMutableDictionary 来进行增加、删除和修改操作。NSMutableDictionary 继承自 NSDictionary，因此它具有 NSDictionary 的所有特性，并添加了一些额外的方法来修改其内容。


下面是一些常用的 NSMutableDictionary 方法：


添加和修改操作：
 -setObject:forKey:：将指定的值与键关联，如果键已存在，则替换对应的值。
 -setObject:forKeyedSubscript:：通过下标语法为指定键设置值，与 -setObject:forKey: 等效。
 -addEntriesFromDictionary:：将另一个字典中的键值对添加到当前字典中。
 删除操作：
 -removeObjectForKey:：移除指定键的键值对。
 -removeAllObjects：移除所有的键值对。
 替换操作：
 -replaceObjectForKey:withObject:：用指定的值替换指定键的当前值。
 -setDictionary:：用另一个字典中的键值对替换当前字典的内容。
 键值对的获取和遍历：
 -objectForKey:：通过给定的键获取对应的值。
 -allKeys 和 -allValues：获取所有键或所有值的集合。
 -enumerateKeysAndObjectsUsingBlock:：通过 block 遍历字典的键值对。
 判断是否包含某个键或值：
 -containsKey: 和 -containsObject:：判断字典中是否包含指定的键或值。
  


```objective-c
NSDictionary* init = [[NSDictionary alloc] initWithObjectsAndKeys:@&quot;wutong&quot;,@&quot;name&quot;,@18,@&quot;age&quot;,@&quot;RDFZ&quot;,@&quot;school&quot;, nil];
        //键在前，指在后
        NSLog(@&quot;init: %@&quot;,init);

        NSDictionary* init2 = @{@&quot;name&quot;:@&quot;tommy&quot;,@&quot;age&quot;:@19, @&quot;school&quot;:@&quot;xupt&quot;};
        NSLog(@&quot;init2: %@&quot;,init2);

        NSInteger count = [init count];
        NSLog(@&quot;count: %ld&quot;,count);

        //字典只能通过键去获取对应的值
        //字典中键是唯一的
        //获取所以的键-key
        NSArray* allkey = [init allKeys];
        NSLog(@&quot;%@&quot;,allkey);

        //获取value同上
        //通过key获取对应的value
        NSString* name = [init objectForKey:@&quot;name&quot;];
        NSLog(@&quot;%@&quot;, name);
        NSLog(@&quot;%@&quot;, init[@&quot;age&quot;]);

        NSArray* testkey = [init allKeys];
        for (id key in init) {
            NSLog(@&quot;%@ =&amp;gt; %@&quot;, key, init[key]);
        }


        NSMutableDictionary* initDic = [[NSMutableDictionary alloc] initWithCapacity:2];

        NSMutableDictionary* initDic2 = [@{@&quot;name&quot;: @&quot;a&quot;, @&quot;age&quot;: @&quot;10&quot;}mutableCopy];
        NSLog(@&quot;%@&quot;, initDic2);

        [initDic2 setObject:@&quot;Cici&quot; forKey:@&quot;friend&quot;];
        NSLog(@&quot;%@&quot;, initDic2);

        [initDic2 setObject:@&quot;ypt&quot; forKey:@&quot;name&quot;];
        NSLog(@&quot;%@&quot;, initDic2);

        initDic2[@&quot;age&quot;] = @199;
        NSLog(@&quot;%@&quot;, initDic2);

        [initDic2 setValue:nil forKey:@&quot;age&quot;];
        NSLog(@&quot;%@&quot;, initDic2);

        [initDic2 removeObjectForKey:@&quot;name&quot;];
        NSLog(@&quot;%@&quot;, initDic2);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/148107284&quot;&gt;OC语言学习——Foundation框架回顾及考核补缺&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>iOS —— 网易云仿写</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-149338308-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-149338308-ios-/</guid><description>目录 首页​编辑​编辑 上方导航栏 主页内容 海报 推荐内容 猜你喜欢 “我的”页面 ​编辑 导航栏 主要内容 我的信息 工具栏： 歌单部分： 设置页面 与网易云相比，我只完成了首页以及“我的”页面的仿写。 首页 首页实现效果如上图。 上方导航栏 导航栏的左右按钮和搜索栏代码如上</description><pubDate>Sat, 06 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%A6%96%E9%A1%B5%E2%80%8B%E7%BC%96%E8%BE%91%E2%80%8B%E7%BC%96%E8%BE%91&quot;&gt;首页​编辑​编辑&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%8A%E6%96%B9%E5%AF%BC%E8%88%AA%E6%A0%8F&quot;&gt;上方导航栏&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%BB%E9%A1%B5%E5%86%85%E5%AE%B9&quot;&gt;主页内容&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%B5%B7%E6%8A%A5&quot;&gt;海报&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%8E%A8%E8%8D%90%E5%86%85%E5%AE%B9&quot;&gt;推荐内容&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%8C%9C%E4%BD%A0%E5%96%9C%E6%AC%A2&quot;&gt;猜你喜欢&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E2%80%9C%E6%88%91%E7%9A%84%E2%80%9D%E9%A1%B5%E9%9D%A2&quot;&gt;“我的”页面&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A0%E2%80%8B%E7%BC%96%E8%BE%91&quot;&gt;​编辑&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%AF%BC%E8%88%AA%E6%A0%8F&quot;&gt;导航栏&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%BB%E8%A6%81%E5%86%85%E5%AE%B9&quot;&gt;主要内容&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%88%91%E7%9A%84%E4%BF%A1%E6%81%AF&quot;&gt;我的信息&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%B7%A5%E5%85%B7%E6%A0%8F%EF%BC%9A&quot;&gt;工具栏：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%AD%8C%E5%8D%95%E9%83%A8%E5%88%86%EF%BC%9A&quot;&gt;歌单部分：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%AE%BE%E7%BD%AE%E9%A1%B5%E9%9D%A2&quot;&gt;设置页面&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;与网易云相比，我只完成了首页以及“我的”页面的仿写。&lt;/p&gt;
&lt;h2&gt;首页&lt;/h2&gt;
&lt;p&gt;首页实现效果如上图。&lt;/p&gt;
&lt;h3&gt;上方导航栏&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;self.leftbtn = [[UIBarButtonItem alloc] initWithImage: [UIImage imageNamed: @&quot;caidan&quot;] style: UIBarButtonItemStylePlain target: self action: @selector(pressMenu)];
    self.rightbtn = [[UIBarButtonItem alloc] initWithImage: [UIImage imageNamed: @&quot;tinggeshiqu&quot;] style: UIBarButtonItemStylePlain target: nil action: nil];
    self.navigationItem.leftBarButtonItem = self.leftbtn;
    self.navigationItem.rightBarButtonItem = self.rightbtn;
    self.navigationItem.leftBarButtonItem.tintColor = [UIColor blackColor];
    self.navigationItem.rightBarButtonItem.tintColor = [UIColor blackColor];
    self.searchbar = [[UISearchBar alloc] init];
    self.searchbar.placeholder = @&quot;安和桥 宋冬野&quot;;
    self.searchbar.showsSearchResultsButton = YES;
    self.navigationItem.titleView = self.searchbar;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导航栏的左右按钮和搜索栏代码如上&lt;/p&gt;
&lt;h3&gt;主页内容&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/4750fc1e0a4944bcb9418f6eeb843287.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图Lookin，我把主页分成了三个部分：poster , recommend , guessyoulike&lt;/p&gt;
&lt;p&gt;分别对应显示效果中的无线轮播海报 ， 私人漫游 ， 根据你喜欢的歌曲推荐。&lt;/p&gt;
&lt;h4&gt;海报&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- (void)pageChanged:(UIPageControl *)sender {
    NSInteger page = sender.currentPage;
    CGFloat width = WIDTH;
    [self.scrollView setContentOffset:CGPointMake((page + 1) * width, 0) animated:YES];
}

//自动翻页
- (void)autoScroll {
    CGFloat screenWidth = self.scrollView.bounds.size.width;
    CGFloat currentOffset = self.scrollView.contentOffset.x;
    NSInteger nextPage = (NSInteger)(currentOffset / screenWidth) + 1;

    NSInteger realPageCount = self.page.numberOfPages;

    if (nextPage &amp;gt;= realPageCount + 2) {
        [self.scrollView setContentOffset:CGPointMake(screenWidth, 0) animated:NO];
        self.page.currentPage = 0;
        return;
    }

    [self.scrollView setContentOffset:CGPointMake(nextPage * screenWidth, 0) animated:YES];

    self.page.currentPage = (nextPage - 1) % realPageCount;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView != self.scrollView) return;

    CGFloat screenWidth = scrollView.bounds.size.width;
    CGFloat offsetX = scrollView.contentOffset.x;
    NSInteger realPageCount = self.page.numberOfPages;
    NSInteger totalPages = realPageCount + 2;
    if (offsetX &amp;lt;= 0) {
        scrollView.contentOffset = CGPointMake(screenWidth * (totalPages - 2), 0);
        self.page.currentPage = realPageCount - 1;
    }
    else if (offsetX &amp;gt;= screenWidth * (totalPages - 1)) {
        scrollView.contentOffset = CGPointMake(screenWidth, 0);
        self.page.currentPage = 0;
    }
    else {
        NSInteger currentPage = (NSInteger)(offsetX / screenWidth) - 1;
        if (currentPage &amp;lt; 0) currentPage = realPageCount - 1;
        if (currentPage &amp;gt;= realPageCount) currentPage = 0;

        self.page.currentPage = currentPage;
    }
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    [self.timer invalidate];
    self.timer = nil;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    [self startAutoScrollTimer];
}

- (void)startAutoScrollTimer {
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }

    self.timer = [NSTimer timerWithTimeInterval:5.0 target:self selector:@selector(autoScroll) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一些控制轮播的关键函数如上图，如代码可简化或有误烦请指出。&lt;/p&gt;
&lt;p&gt;同时，我添加了一个点击海报放大功能，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)bannerTapped:(UITapGestureRecognizer *)gesture {
    //取出被点击的对象
    UIImageView *imgView = (UIImageView *)gesture.view;
    UIImage *image = imgView.image;
    if (!image) return;

    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    UIView *backgroundView = [[UIView alloc] initWithFrame:keyWindow.bounds];
    backgroundView.backgroundColor = [UIColor whiteColor];
    backgroundView.alpha = 0.5;
    UIImageView *fullImageView = [[UIImageView alloc] initWithFrame:backgroundView.bounds];
    fullImageView.contentMode = UIViewContentModeScaleAspectFit;
    fullImageView.image = image;
    fullImageView.userInteractionEnabled = YES;

    UITapGestureRecognizer *tapClose = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissFullScreenImage:)];
    [fullImageView addGestureRecognizer:tapClose];

    [backgroundView addSubview:fullImageView];
    [keyWindow addSubview:backgroundView];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与此同时，我也添加了一个点击后取消&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)dismissFullScreenImage:(UITapGestureRecognizer *)tap {
    UIView *backgroundView = tap.view.superview;
    [UIView animateWithDuration:0.3 animations:^{
        backgroundView.alpha = 0;
    } completion:^(BOOL finished) {
        [backgroundView removeFromSuperview];
    }];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/7313d7721632430b816f1fa8640b47dd.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;推荐内容&lt;/h4&gt;
&lt;p&gt;我建立一个数组用于储存内容，代码难度较低&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)setupRecommandCell {
    self.scrollView02 = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, WIDTH, 211)];
    self.scrollView02.scrollEnabled = YES;
    self.scrollView02.pagingEnabled = YES;
    self.scrollView02.alwaysBounceHorizontal = YES;
    self.scrollView02.alwaysBounceVertical = NO;
    self.scrollView02.contentSize = CGSizeMake(WIDTH * 2.1, 211);
    self.scrollView02.showsHorizontalScrollIndicator = NO;
    [self.contentView addSubview:self.scrollView02];

    NSArray* arrayLabel02 = @[@&quot;私人漫游&quot;, @&quot;欧美日推&quot;, @&quot;民谣日推&quot;, @&quot;快乐旅行&quot;, @&quot;电音日推&quot;, @&quot;每日推荐&quot;];
    NSArray* arrayLabel2 = @[@&quot;1.2亿&quot;, @&quot;23w&quot;, @&quot;30w&quot;, @&quot;460w&quot;, @&quot;521w&quot;, @&quot;6亿&quot;];
    BOOL isNight = [NightModeManager sharedManager].isNightMode;

    for (int i = 0; i &amp;lt; 6; i++) {
        NSString* strName = [NSString stringWithFormat: @&quot;guess%d&quot;, i + 1];
        UIImageView* iView = [[UIImageView alloc] initWithImage: [UIImage imageNamed: strName]];
        iView.frame = CGRectMake(10 + 135 * i, 5, 125, 166);
        iView.layer.cornerRadius = 9;
        iView.layer.masksToBounds = YES;
        [self.scrollView02 addSubview: iView];

        self.label02 = [[UILabel alloc] initWithFrame: CGRectMake(10 + 135 * i, 160, 130, 50)];
        self.label02.font = [UIFont systemFontOfSize: 15];
        self.label02.text = arrayLabel02[i];
        self.label02.textColor = [UIColor darkGrayColor];
        self.label02.numberOfLines = 2;
        [self.scrollView02 addSubview: self.label02];

        self.label2 = [[UILabel alloc] initWithFrame: CGRectMake(5, 88, 100, 50)];
        self.label2.font = [UIFont systemFontOfSize: 15];
        self.label2.text = arrayLabel2[i];
        self.label2.textColor = [UIColor whiteColor];
        [iView addSubview: self.label2];
    }
}
#### 猜你喜欢


因为猜你喜欢部分有三个横向栏目，我分别建立tableView0301 0302 0303


以其中0301为例： 


```objective-c
- (UITableViewCell *)createCellForTableView0301:(NSIndexPath *)indexPath {

    static NSString *cellIdentifier = @&quot;CustomSongCell&quot;;
    UITableViewCell *cell = [self.tableView0301 dequeueReusableCellWithIdentifier:cellIdentifier];
    BOOL isNight = [NightModeManager sharedManager].isNightMode;
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];

        UIImageView *albumImageView = [[UIImageView alloc] initWithFrame:CGRectMake(15, 5, 55, 55)];
        albumImageView.tag = 100;
        albumImageView.layer.cornerRadius = 5;
        albumImageView.layer.masksToBounds = YES;
        albumImageView.contentMode = UIViewContentModeScaleAspectFill;
        [cell.contentView addSubview:albumImageView];
        UILabel *songLabel = [[UILabel alloc] initWithFrame:CGRectMake(80, 10, WIDTH - 150, 20)];
        songLabel.tag = 101;
        songLabel.textColor = isNight ? [UIColor whiteColor] : [UIColor blackColor];
        songLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
        [cell.contentView addSubview:songLabel];

        UILabel *artistLabel = [[UILabel alloc] initWithFrame:CGRectMake(80, 35, WIDTH - 150, 15)];
        artistLabel.tag = 102;
        artistLabel.font = [UIFont systemFontOfSize:12];
        artistLabel.textColor = [UIColor grayColor];
        [cell.contentView addSubview:artistLabel];

        UIButton *playButton = [UIButton buttonWithType:UIButtonTypeCustom];
        playButton.frame = CGRectMake(WIDTH - 45, 20, 30, 30);
        playButton.tag = 103;
        [playButton setImage:[UIImage imageNamed:@&quot;bofang.png&quot;] forState:UIControlStateNormal];
        playButton.tintColor = [UIColor systemPurpleColor];
        [playButton addTarget:self action:@selector(playButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
        [cell.contentView addSubview:playButton];
    }

    NSArray* array01 = @[@&quot;董小姐&quot;, @&quot;Hate Me&quot;, @&quot;野孩子&quot;];
    NSArray* array011 = @[@&quot;宋冬野 - 安河桥北&quot;, @&quot;two - Hate Me&quot;, @&quot;杨千嬅&quot;];
    NSArray* imageNames = @[@&quot;011&quot;, @&quot;012&quot;, @&quot;013&quot;];

    UIImageView *albumImageView = [cell.contentView viewWithTag:100];
    albumImageView.image = [UIImage imageNamed:imageNames[indexPath.row]];

    UILabel *songLabel = [cell.contentView viewWithTag:101];
    songLabel.text = array01[indexPath.row];

    UILabel *artistLabel = [cell.contentView viewWithTag:102];
    artistLabel.text = array011[indexPath.row];

    return cell;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;“我的”页面&lt;/h2&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;我依旧把这个页面分成三个cell文件 UserInfoCell.  ToolbarCell.  SegmentTabCell&lt;/p&gt;
&lt;p&gt;分别对应我的用户信息、工具栏：例如最近、本地、网盘等、和下方的音乐、博客、笔记分栏。&lt;/p&gt;
&lt;h3&gt;导航栏&lt;/h3&gt;
&lt;p&gt;导航栏设置与之前无异&lt;/p&gt;
&lt;h3&gt;主要内容&lt;/h3&gt;
&lt;h4&gt;我的信息&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- (void)setupViews {
    _avatarImageView = [[UIImageView alloc] initWithFrame:CGRectMake( 155, -30, 90, 90)];
    _avatarImageView.layer.cornerRadius = 45;
    _avatarImageView.image = [UIImage imageNamed:@&quot;avater&quot;];
    _avatarImageView.layer.masksToBounds = YES;
    _avatarImageView.layer.borderColor = [UIColor whiteColor].CGColor;
    _avatarImageView.layer.borderWidth = 2.0;
    _avatarImageView.userInteractionEnabled = YES;

    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(avatarTapped:)];
    [_avatarImageView addGestureRecognizer:tapGesture];

    [self.contentView addSubview:_avatarImageView];
    _vipLabel = [[UILabel alloc] initWithFrame:CGRectMake(240, 70, 60, 25)];
    _vipLabel.textColor = [UIColor colorWithRed:1.0 green:0.8 blue:0.2 alpha:1.0];
    _vipLabel.font = [UIFont boldSystemFontOfSize:12];
    _vipLabel.textAlignment = NSTextAlignmentCenter;
    _vipLabel.backgroundColor = [UIColor blackColor];
    _vipLabel.layer.cornerRadius = 12;
    _vipLabel.layer.masksToBounds = YES;
    [self.contentView addSubview:_vipLabel];

    _usernameLabel = [[UILabel alloc] initWithFrame:CGRectMake(35, 70, self.contentView.bounds.size.width, 30)];
    _usernameLabel.textColor = [UIColor whiteColor];
    _usernameLabel.font = [UIFont boldSystemFontOfSize:20];
    _usernameLabel.textAlignment = NSTextAlignmentCenter;
    [self.contentView addSubview:_usernameLabel];

    _bioLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, 100, self.contentView.bounds.size.width, 25)];
    _bioLabel.textColor = [UIColor colorWithWhite:1.0 alpha:0.7];
    _bioLabel.font = [UIFont systemFontOfSize:15];
    _bioLabel.textAlignment = NSTextAlignmentCenter;
    [self.contentView addSubview:_bioLabel];

    _statusButton = [UIButton buttonWithType:UIButtonTypeCustom];
    _statusButton.frame = CGRectMake(150, -60, 100, 24);
    [_statusButton setTitle:@&quot;+添加状态&quot; forState:UIControlStateNormal];
    [_statusButton setTitleColor:[UIColor systemMintColor] forState:UIControlStateNormal];
    _statusButton.titleLabel.font = [UIFont systemFontOfSize:14];
    _statusButton.layer.borderColor = [UIColor colorWithRed:0.0 green:0.6 blue:1.0 alpha:1.0].CGColor;
    _statusButton.layer.borderWidth = 0;
    _statusButton.layer.cornerRadius = 12;
    [self.contentView addSubview:_statusButton];

    _statsView = [[UIView alloc] initWithFrame:CGRectMake(80, 130, self.contentView.bounds.size.width - 60, 50)];
    [self.contentView addSubview:_statsView];

    NSArray *statTitles = @[@&quot;33\n关注&quot;, @&quot;8\n粉丝&quot;, @&quot;Lv.9\n等级&quot;, @&quot;1641\n时长&quot;];
    CGFloat statWidth = _statsView.bounds.size.width / statTitles.count;

    for (int i = 0; i &amp;lt; statTitles.count; i++) {
        UILabel *statLabel = [[UILabel alloc] initWithFrame:CGRectMake(i * statWidth, 0, statWidth, 50)];
        statLabel.text = statTitles[i];
        statLabel.numberOfLines = 2;
        statLabel.textColor = [UIColor whiteColor];
        statLabel.font = [UIFont systemFontOfSize:14];
        statLabel.textAlignment = NSTextAlignmentCenter;
        [_statsView addSubview:statLabel];
    }

    _badgeView = [[UIView alloc] initWithFrame:CGRectMake(100, 190, 200, 30)];
    _badgeView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.15];
    _badgeView.layer.cornerRadius = 10;
    [self.contentView addSubview:_badgeView];

    UILabel *badgeDesc = [[UILabel alloc] initWithFrame:CGRectMake(14, 3, 300, 20)];
    badgeDesc.text = @&quot;♂ | 日常摸鱼中 | 黑胶收藏者&quot;;
    badgeDesc.textColor = [UIColor colorWithWhite:1.0 alpha:0.7];
    badgeDesc.font = [UIFont systemFontOfSize:14];
    [_badgeView addSubview:badgeDesc];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，我增加了照片墙换头像功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代理传值：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;UserInfoCell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)avatarTapped:(UITapGestureRecognizer *)gesture {
    NSLog(@&quot;头像被点击&quot;);
    if ([self.delegate respondsToSelector:@selector(didTapAvatarInCell:)]) {
        [self.delegate didTapAvatarInCell:self];
    }
}

- (void)setAvatarImageView:(UIImageView *)avatarImageView {
    _avatarImageView.image = avatarImageView;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;myVC控制器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)didTapAvatarInCell:(UserInfoCell *)cell {

    NSLog(@&quot;代理方法&quot;);
    PhotoWallVC *photoVC = [[PhotoWallVC alloc] init];
    photoVC.delegate = self;
    [self presentViewController:photoVC animated:YES completion:nil];
}

#pragma mark - PhotoWallDelegate

- (void)didSelectAvatar:(UIImage *)selectedAvatar {
    NSLog(@&quot;接收头像&quot;);

    self.currentAvatar = selectedAvatar;

    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    UserInfoCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
    [cell setAvatarImageView: _currentAvatar];
}
#### 工具栏：


我还是通过两个数组记录每个工具的文字和图片


```objective-c
- (void)setupUI {
    self.backgroundColor = [UIColor clearColor];
    self.selectionStyle = UITableViewCellSelectionStyleNone;
    self.contentView.backgroundColor = [UIColor clearColor];

    _buttons = [NSMutableArray array];
    _backgroundViews = [NSMutableArray array];

    for (int i = 0; i &amp;lt; 5; i++) {
        UIView *backgroundView = [[UIView alloc] init];
        backgroundView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.1];
        backgroundView.layer.cornerRadius = 10;
        [self.contentView addSubview:backgroundView];
        [_backgroundViews addObject:backgroundView];

        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.tag = i;
        button.titleLabel.font = [UIFont systemFontOfSize:13];
        button.titleLabel.textAlignment = NSTextAlignmentCenter;
        button.titleLabel.adjustsFontSizeToFitWidth = YES;
        button.titleLabel.minimumScaleFactor = 0.8;
        [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
        [self.contentView addSubview:button];
        [_buttons addObject:button];
    }
}

- (void)layoutSubviews {
    [super layoutSubviews];

    CGFloat padding = 15.0;
    CGFloat buttonSpacing = 5.0;
    CGFloat contentWidth = CGRectGetWidth(self.contentView.bounds) - padding * 2;
    CGFloat contentHeight = 40.0;

    CGFloat buttonWidth = (contentWidth - (4 * buttonSpacing)) / _buttons.count;

    for (int i = 0; i &amp;lt; _buttons.count; i++) {
        CGFloat xPosition = padding + i * (buttonWidth + buttonSpacing);


        UIView *backgroundView = _backgroundViews[i];
        backgroundView.frame = CGRectMake(xPosition, -30, buttonWidth, contentHeight);

        UIButton *button = _buttons[i];
        button.frame = CGRectMake(xPosition, -30, buttonWidth, contentHeight);

        [self layoutButtonContent:button];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终实现效果如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/918509012665406c81bee5e5e225de79.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;歌单部分：&lt;/h4&gt;
&lt;p&gt;我加入了一个分栏实现音乐、播客、笔记之间的切换&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_segmentedControl = [[UISegmentedControl alloc] init];
    _segmentedControl.selectedSegmentTintColor = [UIColor colorWithRed:0.98 green:0.30 blue:0.30 alpha:1.0];
    [_segmentedControl setBackgroundImage:[UIImage new] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [_segmentedControl setBackgroundImage:[UIImage new] forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
    [_segmentedControl setDividerImage:[UIImage new] forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    UIFont *selectedFont = [UIFont boldSystemFontOfSize:16];
    UIFont *normalFont = [UIFont systemFontOfSize:14];

    [_segmentedControl setTitleTextAttributes:@{
        NSForegroundColorAttributeName: [UIColor whiteColor],
        NSFontAttributeName: selectedFont
    } forState:UIControlStateSelected];

    [_segmentedControl setTitleTextAttributes:@{
        NSForegroundColorAttributeName: [UIColor lightGrayColor],
        NSFontAttributeName: normalFont
    } forState:UIControlStateNormal];

    [_segmentedControl addTarget:self action:@selector(segmentChanged:) forControlEvents:UIControlEventValueChanged];
    [self.contentView addSubview:_segmentedControl];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设置页面&lt;/h2&gt;
&lt;p&gt;我实现了一个抽屉视图，并设置了黑夜模式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/88275be0995748a08b5d2181de359989.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;recommendVC：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(void) pressMenu {
    settingViewController *settingView = [[settingViewController alloc] init];
        settingView.modalPresentationStyle = UIModalPresentationOverFullScreen;
        settingView.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;

        [self presentViewController:settingView animated:YES completion:nil];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;settingViewController：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(updateAppearance)
                                                   name:@&quot;NightModeChangedNotification&quot;
                                                 object:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先通过这段代码接收通知&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)updateAppearance {
    BOOL isNightMode = [NightModeManager sharedManager].isNightMode;

    if (isNightMode) {
        self.tableView.backgroundColor = [UIColor blackColor];
        self.tabBarController.tabBar.backgroundColor = [UIColor darkGrayColor];
        self.tabBarController.tabBar.barTintColor = [UIColor darkGrayColor];
        self.tabBarController.tabBar.tintColor = [UIColor redColor]; // 设置选中颜色
    } else {
        UIColor *wechatBackgroundColor = [UIColor colorWithRed:247/255.0 green:247/255.0 blue:247/255.0 alpha:1.0];
        self.tableView.backgroundColor = wechatBackgroundColor;
        self.tabBarController.tabBar.barTintColor = [UIColor whiteColor];
        self.tabBarController.tabBar.backgroundColor = [UIColor whiteColor];
        self.tabBarController.tabBar.tintColor = [UIColor grayColor];
    }
    [self.tableView reloadData];
}


//在当前页面上加一个半透明黑色的背景蒙层，并且能点击关闭设置面板
- (void)setupBackgroundDimmingView {
    self.backgroundDimmingView = [[UIView alloc] initWithFrame:self.view.bounds];
    self.backgroundDimmingView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
    self.backgroundDimmingView.alpha = 0;
    [self.view addSubview:self.backgroundDimmingView];

    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissSetting)];
    [self.backgroundDimmingView addGestureRecognizer:tapGesture];
}

//通知中心取消
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后实现白天黑夜模式切换。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149338308&quot;&gt;iOS —— 网易云仿写&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>iOS —— UI 初探</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-148151128-ios-ui-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-148151128-ios-ui-/</guid><description>简介 第一次新建时，你可能会好奇。为什么有这么多文件，他们都有什么用？ App 启动与生命周期管理相关 文件名 类型 作用 main.m m 程序入口，main() 函数定义在这里 AppDelegate.h/.m h/m App 启动/进入后台/退出等全局事件的管理者 Scen</description><pubDate>Sat, 06 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;第一次新建时，你可能会好奇。为什么有这么多文件，他们都有什么用？&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9f768a5780544ea5b5a6ba3ba83f2bba.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;App 启动与生命周期管理相关&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件名&lt;/strong&gt; &lt;strong&gt;类型&lt;/strong&gt; &lt;strong&gt;作用&lt;/strong&gt; main.m m 程序入口，main() 函数定义在这里 AppDelegate.h/.m h/m App 启动/进入后台/退出等全局事件的管理者 SceneDelegate.h/.m h/m iOS 13+ 的窗口场景管理器，加载第一个页面&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;main.m → AppDelegate → SceneDelegate → ViewController（显示）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;界面与资源相关&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件名&lt;/strong&gt; &lt;strong&gt;类型&lt;/strong&gt; &lt;strong&gt;作用&lt;/strong&gt; ViewController.h/.m h/m 默认页面的控制器，在这里写 UI 和交互逻辑 LaunchScreen.storyboard  启动时显示的静态页面（类似“闪屏”Logo） Main.storyboard  如果你使用 Storyboard，这里就是主界面布局 Assets.xcassets  图片、颜色、App图标等资源文件存放处 Info.plist  App 的配置信息（名称、权限、图标路径等）&lt;/p&gt;
&lt;h2&gt;UILabel&lt;/h2&gt;
&lt;p&gt;UIlabel是一种可以显示在屏幕上，并且可以显示文字的一种UI视图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController
// 创建ui控件函数
-(void) createUI {
    //定义并创建一个uilabei对象
    //UILabel可以显示在屏幕上并且可以显示文字的一种UI
    UILabel* label = [[UILabel alloc]init];
    //显示文字，赋值
    label.text = @RDFZ&quot;;
    //显示位置
    label.frame = CGRectMake(10, 400, 410, 200);
    label.backgroundColor = [UIColor whiteColor];
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:label];
    label.font = [UIFont systemFontOfSize:34];//labei大小和字体
    label.textColor = [UIColor blackColor];
    label.shadowColor = [UIColor greenColor];//字体阴影颜色
    label.shadowOffset = CGSizeMake(100, 0);//阴影偏离位置

    label.textAlignment = NSTextAlignmentCenter;//设置居中对齐
    label.numberOfLines = 3;//文字尽量按照设计的数来显示
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self createUI];
}


@end
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里主要注意的是两个类型，一个是CGRectMake类型（这个&lt;a href=&quot;https://so.csdn.net/so/search?q=%E7%BB%93%E6%9E%84%E4%BD%93&amp;amp;spm=1001.2101.3001.7020&quot;&gt;结构体&lt;/a&gt;又包括了origin和size两个&lt;a href=&quot;https://so.csdn.net/so/search?q=%E6%88%90%E5%91%98%E5%8F%98%E9%87%8F&amp;amp;spm=1001.2101.3001.7020&quot;&gt;成员变量&lt;/a&gt;），origin表示的是一个label的起始点，size表示的是一个显示出来的矩阵的宽和高，我们的坐标系是以屏幕左上角为基准点，向下为y，向右为x。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/075d36d092c340a580ae049570a2fb62.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;UIButton&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;图片按钮&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void) creatImageButton {
    UIButton* btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(10, 300, 150, 100);
    UIImage* icon1 = [UIImage imageNamed:@&quot;btn02.jpeg&quot;];//设置路径
    UIImage* icon2 = [UIImage imageNamed:@&quot;btn03.jpg&quot;];
    [btn setImage:icon1 forState:UIControlStateNormal];
    [btn setImage:icon2 forState:UIControlStateHighlighted];
    [self.view addSubview:btn];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;** UIButton事件触发**&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态&lt;/strong&gt; &lt;strong&gt;枚举值&lt;/strong&gt; &lt;strong&gt;示例&lt;/strong&gt; 普通状态 UIControlStateNormal 默认状态 高亮状态 UIControlStateHighlighted 按下时显示 被禁用状态 UIControlStateDisabled 不能点击时显示 被选中状态 UIControlStateSelected 可切换按钮时用&lt;/p&gt;
&lt;p&gt;UIButtonTypeSystem 系统按钮（默认蓝色文字） 有文字变化动画 登录按钮、普通按钮 UIButtonTypeCustom 自定义按钮 无边框、背景、动画 自定义 UI（图像按钮、透明按钮） UIButtonTypeDetailDisclosure 信息按钮 ⓘ iOS 内置样式 表格行详情按钮 UIButtonTypeContactAdd 加号按钮 ➕ iOS 内置样式 添加联系人 UIButtonTypeInfoLight / InfoDark info 图标按钮 灰或白 info 图标 显示提示信息&lt;/p&gt;
&lt;h2&gt;UIView&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;//所有看到的对象全部都是UIView的子类
- (void)viewDidLoad {
    [super viewDidLoad];
    UIView* view = [[UIView alloc] init];
    //设置一个位置
    view.frame = CGRectMake(10, 100, 230, 70);
    view.backgroundColor = [UIColor blueColor];
    self.view.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:view];//父视图添加子视图
    //view.hidden = YES为不显示
    //view.alpha = 0.6;
    view.opaque = YES;//是否显示不透明
    //1不透明
    //0透明
     //将新建的视图显示到屏幕上
    //子视图会受到父视图的管理
    //[self creatUIRectButton];
    //[self creatImageButton];
    // Do any additional setup after loading the view.
}


@end
### 多个视图之间的关系


```objective-c
- (void)viewDidLoad {
    [super viewDidLoad];
    UIView* view = [[UIView alloc] init];
    //设置一个位置
    view.frame = CGRectMake(100, 100, 150, 150);
    view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:view];//父视图添加子视图

    UIView* view1 = [[UIView alloc] init];
    //设置一个位置
    view1.frame = CGRectMake(125, 125, 150, 150);
    view1.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:view1];//父视图添加子视图

    UIView* view2 = [[UIView alloc] init];
    //设置一个位置
    view2.frame = CGRectMake(150, 150, 150, 150);
    view2.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:view2];//父视图添加子视图
    //[self.view bringSubviewToFront:view];//将视图跳涨到最前面
    //[self.view sendSubviewToBack:view2];//调整到最后面
    UIView* viewfront = self.view.subviews[0];
    if (viewfront == view) {
        NSLog(@&quot;dddd&quot;);
    }
}


@end
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;我们的第三个视图会覆盖第二个，第二个会覆盖第一个，所以我们可以理解为一个后面的视图会覆盖前面的视图。 控制台会输出dddd，因此我们的添加视图到自己的subview中间的顺序是后添加的视图插入到后面部分&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;UIWindow&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;UIWindow 是所有视图的 &lt;strong&gt;顶级容器&lt;/strong&gt;，承载整个 App 界面，是屏幕上所有内容的“根舞台”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
    //UIWindow继承于UIView，它是一个特殊的UIView
    //UIScreen：屏幕硬件表示类
    //mainScreen表示主屏幕的设备信息
    //bounds表示屏幕的宽高值
    self.window.rootViewController = [[UIViewController alloc] init];//创建根视图控制器
    self.window.backgroundColor = [UIColor blueColor];
    UIView* view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)];
    view.backgroundColor = [UIColor orangeColor];
    UIView* backview = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 240, 360)];
    backview.backgroundColor = [UIColor redColor];
    //子视图的坐标是参照父亲视图的坐标系
    //当父亲视图移动的时候，所有的子视图都会移动

    [backview addSubview: view];
    [self.window addSubview: backview];

    [self.window makeKeyAndVisible]; //显示我们的根视图
    NSLog(@&quot;%@\n, %@\n, %@\n&quot;, view.window, backview.window, self.window);
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
}


- (void)sceneDidDisconnect:(UIScene *)scene {
    // Called as the scene is being released by the system.
    // This occurs shortly after the scene enters the background, or when its session is discarded.
    // Release any resources associated with this scene that can be re-created the next time the scene connects.
    // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}


- (void)sceneDidBecomeActive:(UIScene *)scene {
    // Called when the scene has moved from an inactive state to an active state.
    // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}


- (void)sceneWillResignActive:(UIScene *)scene {
    // Called when the scene will move from an active state to an inactive state.
    // This may occur due to temporary interruptions (ex. an incoming phone call).
}


- (void)sceneWillEnterForeground:(UIScene *)scene {
    // Called as the scene transitions from the background to the foreground.
    // Use this method to undo the changes made on entering the background.
}


- (void)sceneDidEnterBackground:(UIScene *)scene {
    // Called as the scene transitions from the foreground to the background.
    // Use this method to save data, release shared resources, and store enough scene-specific state information
    // to restore the scene back to its current state.
}


@end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;UIViewController&lt;/h2&gt;
&lt;h3&gt;调用顺序&lt;/h3&gt;
&lt;p&gt;因为学长的博客提到了程序的调用顺序，我这里也做一下简单了解和分享。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;main.m → UIApplicationMain() → AppDelegate → SceneDelegate → UIWindow → rootViewController&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;步骤&lt;/strong&gt; &lt;strong&gt;调用&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; ① main.m 中的 UIApplicationMain() 程序入口，创建 App 实例，启动主 runloop ② AppDelegate 的 application:didFinishLaunchingWithOptions: App 启动完毕，适合做初始化，如设置窗口、SDK等 ③ （iOS13+）调用 SceneDelegate 的 scene:willConnectToSession: 多窗口支持，创建 UIWindow 并设置 rootViewController ④ UIWindow 被设置为 keyWindow 显示主界面 ⑤ rootViewController 的 viewDidLoad 被调用 加载主界面视图层&lt;/p&gt;
&lt;p&gt;**UIViewController的调用顺序（生命周期） **&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;调用方法&lt;/strong&gt; &lt;strong&gt;时机 &amp;amp; 作用&lt;/strong&gt; init / initWithNibName: 创建控制器实例时 loadView 加载 view（可自定义视图） viewDidLoad 视图加载完毕，一般写 UI 初始化代码 viewWillAppear: 即将出现在屏幕上，适合刷新数据 viewDidAppear: 已经显示完毕，适合播放动画 viewWillDisappear: 页面即将被覆盖（如 push 到下一页） viewDidDisappear: 页面已被完全覆盖&lt;/p&gt;
&lt;p&gt;他是iOS应用开发中非常核心的一个类，几乎所有的界面页面都是他的子类。他的作用就是帮你&lt;strong&gt;管理界面，响应用户交互，协调视图之间的切换和流转。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;p&gt;你在控制器写了一行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSLog(@&quot;viewDidLoad called&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会看到在 App 启动后打印了这句话，说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统先从 main.m → AppDelegate → SceneDelegate → UIWindow → rootViewController&lt;/li&gt;
&lt;li&gt;最终调用了 ViewController 的 viewDidLoad&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UIViewController&lt;/h3&gt;
&lt;p&gt;它是 UIKit 框架中的一个类，表示应用中的一个“屏幕”或“页面”。每个视图控制器都负责管理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个视图（self.view）&lt;/li&gt;
&lt;li&gt;该视图中的子视图（按钮、标签、图片等）&lt;/li&gt;
&lt;li&gt;用户与这些视图的交互&lt;/li&gt;
&lt;li&gt;页面之间的跳转逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;视图中的界面切换&lt;/h3&gt;
&lt;p&gt;首先新建一个view02类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewC02.h&quot;

@interface ViewC02 ()

@end

@implementation ViewC02

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    NSLog(@&quot;%@ load&quot;, [self class]);
}
- (void) touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {

    //使当前的控制器消失掉，传入两个参数
    //第一个参数指是否有动画效果
    //第二个参数指结束后是否调用block块操作，不需要为nil
    [self dismissViewControllerAnimated: YES completion: nil];
}

- (void) viewWillDisappear:(BOOL)animated {
    NSLog(@&quot;%@ 视图即将消失&quot;, [self class]);
}

- (void) viewDidDisappear:(BOOL)animated {
    NSLog(@&quot;%@ 视图已消失&quot;, [self class]);
}

- (void) viewDidAppear:(BOOL)animated {
    NSLog(@&quot;%@ 视图已显示&quot;, [self class]);
}

- (void) viewWillAppear:(BOOL)animated {
    NSLog(@&quot;%@ 视图即将显示&quot;, [self class]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后写ViewController程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;
#import &quot;ViewC02.h&quot;

@interface ViewController ()

@end

@implementation ViewController

//当屏幕被点击的时候，调用此函数
- (void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {
    //创建视图控制器二
    ViewC02 *v2 = [[ViewC02 alloc] init];

    //显示一个新的视图控制器界面到屏幕上
    //该函数会传入三个参数：第一个参数指新的控制器对象
    //第二个参数指是否使用动画切换效果
    //第三个参数指切换结束后是否调用block块操作，不需要为nil
    [self presentViewController: v2 animated: YES completion: nil];
}

//当视图控制器第一次被加载显示视图的时，调用此函数
//布局初始化视图来使用，初始化资源使用
- (void)viewDidLoad {
    //调用父类的加载视图函数
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor blueColor];
    NSLog(@&quot;viewDidLoad第一次加载视图&quot;);


    UIView *view = [[UIView alloc] init];

    view.frame = CGRectMake(100, 100, 100, 200);

    //将视图添加到当前控制视图上
    [self.view addSubview: view];

    view.backgroundColor = [UIColor orangeColor];

    self.view.backgroundColor = [UIColor blueColor];
}


@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e4458f57fbe44c5988d3944d4e01b020.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们留意一下这行代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self presentViewController:v2 animated:YES completion:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码是是用于以模态方式（modal）展示另一个视图控制器的方法。可能有同学会好奇，还有没有其他方法？&lt;/p&gt;
&lt;h3&gt;iOS 中常见的视图控制器切换方式总结&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;因为笔者目前进度较慢，因此还需借助ai帮助完成总结&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当然有，除了 presentViewController:animated:completion: 这种 &lt;strong&gt;模态（Modal）方式&lt;/strong&gt; 展示视图控制器外，iOS 中还有其他几种常见的方式来展示或切换视图控制器。下面是总结：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 模态展示（Modal Presentation）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self presentViewController:vc animated:YES completion:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：当前控制器之上“弹出”一个新的控制器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见用途&lt;/strong&gt;：登录界面、设置页、全屏内容展示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可自定义样式&lt;/strong&gt;（iOS 13+）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vc.modalPresentationStyle = UIModalPresentationFullScreen; // 或其他样式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 导航控制器推送（Push）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self.navigationController pushViewController:vc animated:YES];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：需要当前控制器嵌套在 UINavigationController 中，使用“栈”的形式来管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见用途&lt;/strong&gt;：多层级界面导航（如设置 → 通知 → 声音）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回方式&lt;/strong&gt;：自动带有返回按钮。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 切换根控制器（Root View Controller）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UIApplication.sharedApplication.delegate.window.rootViewController = vc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或在 SceneDelegate 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.window.rootViewController = vc;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：直接替换整个应用的根视图控制器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见用途&lt;/strong&gt;：如登录完成后进入主界面，或退出登录返回登录页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无动画&lt;/strong&gt;，如需动画要手动添加。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. 使用 Container View Controller（容器控制器）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UITabBarController&lt;/li&gt;
&lt;li&gt;UINavigationController&lt;/li&gt;
&lt;li&gt;UIPageViewController&lt;/li&gt;
&lt;li&gt;自定义容器控制器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以使用这些容器来自定义多个子控制器的切换，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self addChildViewController:vc];
[self.view addSubview:vc.view];
[vc didMoveToParentViewController:self];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：适合自定义嵌套视图控制器结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见用途&lt;/strong&gt;：页面内多个子控制器的嵌套、选项卡切换等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;5. 使用 Storyboard Segue（界面跳转）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Interface Builder 中设置 Segue：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self performSegueWithIdentifier:@&quot;ShowDetail&quot; sender:self];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：图形化管理界面跳转，适合 Storyboard 构建的应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可选方式&lt;/strong&gt;：Push、Modal、Custom 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;** 6. 使用转场动画（Transition Animation）**&lt;/p&gt;
&lt;p&gt;在根视图上添加过渡动画，手动控制切换效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[UIView transitionWithView:self.view
                  duration:0.5
                   options:UIViewAnimationOptionTransitionFlipFromLeft
                animations:^{
    [self.view addSubview:vc.view];
} completion:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：完全自定义切换动画。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适合&lt;/strong&gt;：卡片翻转、淡入淡出、缩放等特殊场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结对比表&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方式&lt;/strong&gt; &lt;strong&gt;是否动画&lt;/strong&gt; &lt;strong&gt;是否返回&lt;/strong&gt; &lt;strong&gt;使用场景&lt;/strong&gt; presentViewController ✅ ❌ 模态弹窗、登录页面等 pushViewController ✅ ✅ 多层级导航 set rootViewController ❌（可自定义） ❌ 登录后进入主界面 addChildViewController ✅（自定义） ❌ 嵌套子控制器 performSegueWithIdentifier ✅ 视情况而定 Storyboard中界面跳转 UIView transition ✅ ❌ 自定义动画效果&lt;/p&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;iOS视图控制器的生命周期：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1 init函数(init;initWithFrame;initWithCoder;等)－－初始化 2 awakeFromNib－－在loadView之前的工作放在这里 3 viewDidLoad－－注意，一个ViewController一个生命周期内这个函数只会调用一次 4 viewWillAppear －－ view将要出现，每次View消失再出现都会调用 5 viewWillLayoutSubviews－－简要对子试图进行布局 6 viewDidLayoutSubivews－－完成对子试图布局 7 viewDidAppear－－视图将要出现在屏幕上 －－－上述代码不含部分 8 viewWillDisappear－－View将要消失 9 viewDidDisappear－－View已经消失&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/06b61a6b11a44909a332b8e109ebf70c.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;前三行为程序运行后的输出，单击屏幕后显示4-6，再次单击后显示最后两行。&lt;/p&gt;
&lt;p&gt;作者在编写程序时发现，如下代码如果从viewC02中转移到ViewController，则只会有上图的前三行输出。在程序中加入了
ViewC02  load
的输出后，我没发现每次点击后都会创建并跳转到一个ViewC02视图控制器。&lt;/p&gt;
&lt;h2&gt;定时器与视图移动&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;NSTimer&lt;/strong&gt; 是 iOS 中用来 &lt;strong&gt;按固定时间间隔重复执行某个操作&lt;/strong&gt; 的工具。它可以让你设定一个时间间隔，让程序在这个间隔之后执行指定方法，并可选择是否重复执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                   target:self
                                                 selector:@selector(myFunction:)
                                                 userInfo:nil
                                                  repeats:YES];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; 1.0 每隔多少秒触发一次（单位是秒） target 谁来执行这个定时器方法（通常是 self） selector 要执行的方法（函数）名，格式是 @selector(methodName:) userInfo 可传递的附加信息，可为 nil repeats 是否重复触发：YES 为重复，NO 为只触发一次&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;功能&lt;/strong&gt; &lt;strong&gt;代码&lt;/strong&gt; 创建定时器 scheduledTimerWithTimeInterval:... 停止定时器 [timer invalidate]; 还要重置为nil 暂停定时器 [timer setFireDate:[NSDate distantFuture]]; 恢复定时器 [timer setFireDate:[NSDate date]];&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;
//
//@interface ViewController ()
//@property (nonatomic, strong) NSTimer *timeView;
//@end
//
@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    //设置颜色透明度
    self.view.backgroundColor = [UIColor colorWithRed: 0.1 green: 0.3 blue: 0.5 alpha: 0.8];
    //启动定时器按钮
    UIButton *btn = [UIButton buttonWithType: UIButtonTypeRoundedRect];
    btn.frame = CGRectMake(100, 100, 180, 140);
    btn.backgroundColor = [UIColor colorWithRed:0.2 green:0.7 blue:0.7 alpha:1];
    [btn setTitle: @&quot;启动定时器&quot; forState: UIControlStateNormal];
    btn.titleLabel.font = [UIFont systemFontOfSize: 24];
    btn.tintColor = [UIColor blueColor];
    //绑定点击事件
    [btn addTarget: self action: @selector(pressStart) forControlEvents: UIControlEventTouchUpInside];
    //添加到视图
    [self.view addSubview: btn];

    UIButton *stopBtn = [UIButton buttonWithType: UIButtonTypeRoundedRect];
    stopBtn.frame = CGRectMake(100, 300, 180, 140);
    stopBtn.backgroundColor = [UIColor colorWithRed: 0.79 green: 0.29 blue: 0.71 alpha: 1];
    [stopBtn setTitle: @&quot;停止定时器&quot; forState: UIControlStateNormal];
    stopBtn.titleLabel.font = [UIFont systemFontOfSize: 24];
    stopBtn.tintColor = [UIColor orangeColor];
    [stopBtn addTarget: self action: @selector(pressStop) forControlEvents: UIControlEventTouchUpInside];
    [self.view addSubview: stopBtn];
    //创建一个橙色视图，tag=101方便后续查找
    UIView *view = [[UIView alloc] init];
    view.backgroundColor = [UIColor orangeColor];
    //为view对象设置标签值
    view.tag = 101;
    [self.view addSubview: view];
}


- (void) pressStart {
    //NSTimer的类方法创建一个定时器并启动，该定时器传入五个参数
    //第一个参数指每隔多少秒执行一次事件函数
    //第二个参数表示实现参数的对象
    //第三个参数表示事件函数
    //第四个参数表示可以为定时器函数传入一个函数，无参数可以传nil
    //第五个参数表示该定时器是否重复操作，YES则重复，NO则仅一次
    //返回值为一个新建好的定时器对象
    if (_timeView != nil) //[_timeView setFireDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
        return;  //两种均可，上面的会在重复点击启动时一卡一卡，直接return则不会
    else {
        _timeView = [NSTimer scheduledTimerWithTimeInterval: 0.001 target: self selector: @selector(updateTimer:) userInfo: @&quot;北京&quot; repeats: YES];
    }
}

//事件函数
//可以将定时器本身作为参数传入
- (void) updateTimer: (NSTimer*) timer {
    NSLog(@&quot;六朝古都！%@&quot;, timer.userInfo);
    UIView *view = [self.view viewWithTag: 101];
    //修改视图位置（每次x，y增加0.1）
    view.frame = CGRectMake(view.frame.origin.x + 0.1, view.frame.origin.y + 0.1, 80, 80);
}

- (void) pressStop {
    //停止定时器
    if (_timeView != nil) {
        [_timeView invalidate];//让定时器失效
        _timeView = nil;  //非常重要 不加会导致暂停后不能重新在原来进度处启动
    }
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在pressStop函数中，我们批注了一行代码“非常重要”。&lt;/p&gt;
&lt;p&gt;如果不设为 nil：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下次启动时以为还在&lt;strong&gt;用原来的定时器&lt;/strong&gt;，可能不创建新的。&lt;/li&gt;
&lt;li&gt;或者多个定时器&lt;strong&gt;重复叠加&lt;/strong&gt;，导致多个同时移动和打印。&lt;/li&gt;
&lt;li&gt;运行结果如下：&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/12ed1669033e4fdb99a591f257e4a506.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;UITextView和UITextField&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UITextField&lt;/strong&gt;：单行输入，适合填写表单（如用户名、密码、邮箱）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UITextView&lt;/strong&gt;：多行输入，适合写段落（如评论、文章内容、聊天）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 100, 280, 40)];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.placeholder = @&quot;请输入用户名&quot;;
textField.delegate = self;
[self.view addSubview:textField];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(20, 160, 280, 100)];
textView.font = [UIFont systemFontOfSize:16];
textView.layer.borderColor = [UIColor grayColor].CGColor;
textView.layer.borderWidth = 1.0;
textView.delegate = self;
[self.view addSubview:textView];
### UITextFieldDelegate协议


UITextFieldDelegate 是 iOS 中 UITextField 的委托协议（protocol），用来监听和处理用户在文本输入框中的各种交互行为，比如开始编辑、结束编辑、内容变化、是否允许输入等。


在这个协议里有一些函数，在使用这些函数前要先在接口部分声明这个协议，函数如下：


1、- (void) textFieldDidBeginEditing：在手机键盘弹出的一瞬间开始调用，在这里可以为开始输入时添加动作


2、- (void) textFieldDidEndEditing：在手机键盘收回的一瞬间开始调用，在这里可以为结束输入时添加动作


3、- (BOOL) textFieldShouldBeginEditing：表示是否可以进行输入，返回值为YES的时候可以输入，反之不能输入，默认为YES


4、- (BOOL) textFieldShouldEndEditing：表示是否可以结束输入，返回值为YES的时候可以结束，反之不能结束，默认为YES


首先要**遵守协议**


```objective-c
@interface ViewController : UIViewController &amp;lt;UITextFieldDelegate&amp;gt; {
    //定义textfield
    UITextField* _textField;
}
@property (retain, nonatomic) UITextField* textField;

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController

@synthesize textField = _textField;

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建textField对象
    self.textField = [[UITextField alloc] init];
    self.textField.frame = CGRectMake(100, 200, 200, 50);
    self.textField.text = @&quot;用户名&quot;;
    //为输入框的文字设置风格和大小
    self.textField.font = [UIFont systemFontOfSize:17];
    //设置字体颜色
    self.textField.textColor = [UIColor blueColor];

    //设置输入边框的风格
    //圆角风格（默认）
    self.textField.borderStyle = UITextBorderStyleRoundedRect;
    //线框风格
    //self.textField.borderStyle = UITextBorderStyleLine;
    //bezel线框
    //self.textField.borderStyle = UITextBorderStyleBezel;
    //无边框风格
    //self.textField.borderStyle = UITextBorderStyleNone;

    //设置键盘风格，在此处测试时虚拟的的手机键盘如没出现，只需直接cmd+k或点虚拟机然后在山东I/O选keyboard然后选第三即可
    self.textField.keyboardType = UIKeyboardTypeDefault;
//    字母与数字组合风格
//    self.textField.keyboardType = UIKeyboardTypePhonePad;
//    纯数字风格
//    self.textField.keyboardType = UIKeyboardTypeNumberPad;

    //当输入框没有文字时，提示（默认浅灰色半透明）
    self.textField.placeholder = @&quot;你等着我给你填呢&quot;;

    //是否作为密码输入
    //当传入YES时，即作为密码处理，使用圆点加密，NO则正常输入
    self.textField.secureTextEntry = NO;
    [self.view addSubview:_textField];
}
//回收键盘（点击空白处）
-(void) touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {
    [self.textField resignFirstResponder];
}

//UITextField中的一些函数
//在手机键盘弹出的一瞬间开始调用，在这里可以作为开始输入的动作
-(void) textFieldDidBeginEditing:(UITextField *)textField {
    NSLog(@&quot;原神启动&quot;);
}
//与上文相反
-(void) textFieldDidBegEndEditing:(UITextField *)textField {
    NSLog(@&quot;原神关闭&quot;);
}

- (BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string {
    NSLog(@&quot;正在输入字符: %@&quot;, string);
    return YES; // 返回 NO 表示禁止输入
}

//表示是否可以进行输入，返回值为YES的时候可以输入，反之不能输入，默认为YES
- (BOOL) textFieldShouldBeginEditing:(UITextField *)textField {
    return YES;
}

//表示是否可以结束输入，返回值为YES的时候可以结束，反之不能结束，默认为YES
- (BOOL) textFieldShouldEndEditing:(UITextField *)textField {
    return YES;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你直接照抄原函数，是不能得到如下的自定义输出的。为什么呢&lt;/p&gt;
&lt;p&gt;我们少了一行代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.textField.delegate = self;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码，是**设置UITextField的代理（delegate）**为当前的视图控制器self。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在iOS中，delegate（代理）是一种设计模式，他允许一个对象将某些任务“委托”给另一个对象处理。 对于UITextField来说： - 它本身不会直接处理所有用户交互（如：输入时、点击 return 键、结束编辑等）。 - 它通过调用它的 delegate 中的特定方法，来询问或通知这些事件的发生。 - 而你设置了 delegate = self，就表示你这个视图控制器（ViewController）将会负责处理这些事件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/f94b2f528a83487597e30cc99857caa2.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d79fe861ca0c4d6b8c9c1568cbad7515.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/4f7133989e734b3fad1b40524f4b86c3.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;样式&lt;/strong&gt; &lt;strong&gt;效果&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; UITextBorderStyleNone 无边框 输入框看不到边线 UITextBorderStyleLine 单线边框 四周是一条细线 UITextBorderStyleBezel 凸起边框 有阴影的边框（老式效果） UITextBorderStyleRoundedRect 圆角边框 常见的 iOS 输入框样式，推荐使用&lt;/p&gt;
&lt;h3&gt;UITextField&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController
@synthesize textField = _textField;
- (void)viewDidLoad {
    [super viewDidLoad];
    self.textField = [[UITextField alloc] init];
    //创建一个文本输入区对象
    self.textField.frame = CGRectMake(100, 100, 100, 40);
    //设定位置
    self.textField.text = @&quot;用户名&quot;;
    self.textField.font = [UIFont systemFontOfSize:15];//设置字体大小
    self.textField.textColor = [UIColor blackColor];
    self.textField.borderStyle = UITextBorderStyleRoundedRect;//设置圆角风格
    //self.textField.borderStyle = UITextBorderStyleLine; // 线框风格
    self.textField.keyboardType = UIKeyboardTypeNumberPad;
    //设置虚拟键盘风格
    //UIKeyboardTypeDefault默认风格
    //UIKeyboardTyprNamePhonePad字母和数字的组合风格
    //UIKeyboradTypeNumberPad：纯数字风格
    self.textField.placeholder = @&quot;请输入用户名&quot;;
    //提示文字
    self.textField.secureTextEntry = NO;
    //是否为密码输入
    //YES:作为密码处理，原点加密
    //NO：正常显示
    [self.view addSubview:self.textField];
    self.textField.delegate = self;
    // Do any additional setup after loading the view.
}
-(void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {
    [self.textField resignFirstResponder];//让虚拟键盘回收，不再作为第一消息响应者
}
-(void)textFieldDidBeginEditing:(UITextField *)textField {
    NSLog(@&quot;开始编辑了&quot;);
}
-(void) textFieldDidEndEditing:(UITextField *)textField {
    self.textField.text = @&quot;&quot;;
    NSLog(@&quot;开始结束编辑了&quot;);
}
//是否可以进行输入
-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
    return YES;
}
//是否可以结束输入
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
    if (self.textField.text.length &amp;lt; 8) {
        return NO;
    } else {
        return YES;
    }

}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;UISwitch&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建一个继承于UIView的开关对象
    _myswitch = [[UISwitch alloc] init];
    //UISwitch控件的位置X，Y可以改变，当大小无法改变，后两个数字没用
    _myswitch.frame = CGRectMake(150, 200, 80, 40);

    [_myswitch setOn:YES animated:NO];
    [_myswitch setOnTintColor:[UIColor colorWithRed:0.5 green:0.2 blue:0.4 alpha:0.7]];
    [_myswitch addTarget:self action:@selector(pressA) forControlEvents:UIControlEventValueChanged];
    [self.view addSubview:_myswitch];
}

-(void) pressA {
    _myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(pressB) userInfo:@&quot;一只酸奶牛&quot; repeats:YES];
}

-(void) pressB {
    NSLog(@&quot;喝喝喝!%@&quot;, _myTimer.userInfo);
}


@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/73ac23e6e16140059302b5920de5a5e4.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;UISlider和UIProgressSlid&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //创建进度条
    _progressView = [[UIProgressView alloc] init];
    _progressView.frame = CGRectMake(150, 100, 200, 80);
    _progressView.progressTintColor = [UIColor blueColor];
    //设置进度条的进度，传入的参数是0～1的值
    _progressView.progress = 0.5;
    //设置进度条风格特征
    _progressView.progressViewStyle = UIProgressViewStyleDefault;
    [self.view addSubview: _progressView];

    //创建滑动条
    _slider = [[UISlider alloc] init];
    //滑动条的高度是不可改变的
    _slider.frame = CGRectMake(150, 200, 200, 80);
    _slider.tintColor = [UIColor orangeColor];
    //设置滑动条最大值，最小值，最小值可以为负
    _slider.maximumValue = 100;
    _slider.minimumValue = 0;
    //设置滑动条滑块的位置
    _slider.value = 30;

    //设置左侧滑条颜色
    _slider.minimumTrackTintColor = [UIColor orangeColor];
    //设置右侧滑条颜色
    _slider.maximumTrackTintColor = [UIColor brownColor];
    //设置滑块颜色
    _slider.thumbTintColor = [UIColor purpleColor];

    //为滑动条添加事件函数
    [_slider addTarget: self action: @selector(pressSlider) forControlEvents: UIControlEventValueChanged];

    [self.view addSubview: _slider];
}

- (void) pressSlider {
    //使进度条随着滑动条的变化而变化
    _progressView.progress = (_slider.value - _slider.minimumValue) / (_slider.maximumValue - _slider.minimumValue);
    NSLog(@&quot;value = %f&quot;, _slider.value);
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0da9778ca96e4953bcb7f504a245c563.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;UIScollView&lt;/h2&gt;
&lt;p&gt;UIScrollView 是 iOS 中非常重要的一个视图组件，用于&lt;strong&gt;滚动显示超出屏幕范围的内容&lt;/strong&gt;。当你的页面内容太长（或太宽）放不下时，就可以用 UIScrollView 来滚动查看。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; contentSize CGSize 内容区域的大小（超出部分才能滚动） contentOffset CGPoint 当前滚动的偏移位置（默认是 (0,0)） contentInset UIEdgeInsets 内容的内边距（上下左右留白） isScrollEnabled BOOL 是否允许滚动（默认 YES） bounces BOOL 滑到边缘是否回弹（默认 YES） pagingEnabled BOOL 是否分页滑动（像翻页一样） showsHorizontalScrollIndicator BOOL 是否显示水平滚动条 showsVerticalScrollIndicator BOOL 是否显示垂直滚动条 delegate UIScrollViewDelegate 设置代理，用于监听滚动事件等&lt;/p&gt;
&lt;h3&gt;最基本的垂直滚动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 获取图片
    UIImage *image = [UIImage imageNamed:@&quot;long_image.jpg&quot;]; // 确保图片存在并且较高

    // 创建 UIImageView 显示图片
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];

    // 图片实际尺寸
    CGSize imageSize = image.size;

    // 按照屏幕宽度等比例缩放图片
    CGFloat screenWidth = self.view.frame.size.width;
    CGFloat scale = screenWidth / imageSize.width;
    CGFloat scaledHeight = imageSize.height * scale;

    imageView.frame = CGRectMake(0, 0, screenWidth, scaledHeight);
    imageView.contentMode = UIViewContentModeScaleToFill;

    // 创建 UIScrollView 并设置内容大小
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    scrollView.contentSize = CGSizeMake(screenWidth, scaledHeight);

    // 添加图片视图到滚动视图
    [scrollView addSubview:imageView];
    [self.view addSubview:scrollView];
}

@end
### 横向分页滚动图片


```objective-c
#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //定义并创建一个滚动视图并设置其位置，滚动视图可以对视图内容进行滚屏查看
    UIScrollView* sv = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 394, 852)];

    //是否按照整页滚动视图
    sv.pagingEnabled = YES;
    //是否可以开启滚动效果
    sv.scrollEnabled = YES;

    //设置画布的大小，画布显示在滚动视图的内部，一般大于frame的大小，第一个参数表示宽，第二个表示高
    sv.contentSize = CGSizeMake(394 * 5, 852);

    //是否可以边缘弹动效果
        sv.bounces = YES;
        //开启横向弹动效果
        sv.alwaysBounceHorizontal = YES;
        //开启纵向弹动效果
        sv.alwaysBounceVertical = YES;

        //是否显示横向滚动条
        sv.showsHorizontalScrollIndicator = YES;
        //是否显示纵向滚动条
        sv.showsVerticalScrollIndicator = YES;

    for (int i = 0; i &amp;lt; 5; i++) {
        NSString* imageName = [NSString stringWithFormat:@&quot;微信图片_20250515214344_14.jpg&quot;, i + 1];
        UIImage* aImage = [UIImage imageNamed:imageName];
        UIImageView* aView = [[UIImageView alloc] initWithImage:aImage];
        aView.frame = CGRectMake(394*i, 0, 394, 852);
        [sv addSubview:aView];
    }

    sv.backgroundColor = [UIColor whiteColor];
    [self.view addSubview: sv];
}


@end
### 缩放功能


```objective-c
#import &quot;ViewController.h&quot;

@interface ViewController () &amp;lt;UIScrollViewDelegate&amp;gt;

@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建滚动视图
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    scrollView.backgroundColor = [UIColor blackColor];
    scrollView.delegate = self;

    // 设置缩放比例
    scrollView.minimumZoomScale = 1.0;
    scrollView.maximumZoomScale = 4.0;

    // 加载图片
    UIImage *image = [UIImage imageNamed:@&quot;微信图片_20250515214344_14.jpg&quot;];

    // 创建图片视图
    self.imageView = [[UIImageView alloc] initWithImage:image];
    self.imageView.frame = self.view.bounds;
    self.imageView.contentMode = UIViewContentModeScaleAspectFit;

    // 设置内容区域与图片大小一致
    scrollView.contentSize = self.imageView.frame.size;

    [scrollView addSubview:self.imageView];
    [self.view addSubview:scrollView];
}

// 返回可缩放的视图
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return self.imageView;
}

@end
### 滚动事件监听


```objective-c
_scrolView.delegate = self;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码的作用是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;告诉 UIScrollView：“有滑动行为时，请通知当前这个控制器（self）。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前提是你的 ViewController 遵守了 UIScrollViewDelegate 协议（在 .h 文件中一般这样声明）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface ViewController : UIViewController &amp;lt;UIScrollViewDelegate&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方法名&lt;/strong&gt; &lt;strong&gt;触发时机&lt;/strong&gt; &lt;strong&gt;常见用途&lt;/strong&gt; - (void)scrollViewDidScroll:(UIScrollView *)scrollView &lt;strong&gt;滚动过程中持续触发&lt;/strong&gt;（每滑动一帧都会调用） 实时监听滑动位置，做视差效果、懒加载等 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView &lt;strong&gt;用户开始拖动&lt;/strong&gt; scrollView 时触发 可用于暂停动画、记录起始位置等 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset &lt;strong&gt;手指即将离开屏幕&lt;/strong&gt;，即将触发减速滑动时调用 可用于自定义目标滚动位置（翻页）等 - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView &lt;strong&gt;用户松手后，scrollView 开始减速时&lt;/strong&gt;调用 可用于记录状态、加载新内容提示等 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView &lt;strong&gt;减速完成、滚动完全停止时&lt;/strong&gt;触发 常用于：滚动结束后更新页码、加载数据 - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView 使用 setContentOffset:animated:或 scrollRectToVisible:animated:触发的&lt;strong&gt;滚动动画结束时&lt;/strong&gt;调用 常用于：程序自动滚动后执行逻辑，比如跳转到特定位置后加载内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _scrolView = [[UIScrollView alloc] init];
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    _scrolView.frame = CGRectMake(0, 0, screenBounds.size.width, screenBounds.size.height*0.75);
    _scrolView.bounces = YES;//回弹效果，即滑到底后会不会继续拉动

    //_scrolView.userInteractionEnabled = NO;
    //是否接受触碰事件，yes接受，no不接受
    _scrolView.contentSize = CGSizeMake(screenBounds.size.width, screenBounds.size.height * 5 * 0.75);
    for (int i = 0; i &amp;lt; 5; i++) {
        NSString* str = [NSString stringWithFormat:@&quot;image%d.jpg&quot;, i + 1];
        UIImage* image = [UIImage imageNamed:str];
        UIImageView* iView = [[UIImageView alloc] initWithImage:image];
        iView.frame = CGRectMake(0, screenBounds.size.height * i * 0.75, screenBounds.size.width, screenBounds.size.height * 0.75);
        [_scrolView addSubview:iView];
    }
    [self.view addSubview:_scrolView];
    _scrolView.contentOffset = CGPointMake(0, 0);
    _scrolView.pagingEnabled = NO;//是否开启分页效果。这里禁用了分页滑动（一个屏幕一页）。
    _scrolView.delegate = self;//设置 scrollView 的代理对象为当前控制器，用于接收滑动相关事件（实现代理方法）。
    // Do any additional setup after loading the view.
}
- (void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    [_scrolView scrollRectToVisible:CGRectMake(0, 0, screenBounds.size.width, screenBounds.size.height * 0.75) animated:YES];
}
//当视图移动时，都会调用这个函数
//调用这个协议的滚动视图对象
//使用这个函数来监控滚动视图的位置
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat offsetY = scrollView.contentOffset.x;
    NSLog(@&quot;y = %lf&quot;, offsetY);

    CGFloat hight = scrollView.frame.size.height;
    NSInteger page = (scrollView.contentOffset.y + hight / 2) / hight;
    NSLog(@&quot;当前页数：%ld&quot;, (long)page);
}

//结束拖动的时候调用这个函数
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
    NSLog(@&quot;结束拖动的时候调用这个函数&quot;);
}
//滚动视图即将开始被拖动的时候
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    NSLog(@&quot;滚动视图即将开始被拖动的时候&quot;);
}
//即将结束拖动的时候调用
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    NSLog(@&quot;即将结束拖动的时候调用&quot;);
}
//视图即将减速的时候
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
    NSLog(@&quot;视图即将减速的时候&quot;);
}
//视图即将结束减速的时候调用，视图停止的瞬间调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSLog(@&quot;视图即将结束减速的时候调用&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/ebd785ef773c4f9cb789e0a954d01d09.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;** 同理：**&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController () &amp;lt;UIScrollViewDelegate&amp;gt;

@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIPageControl *pageControl;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    CGFloat screenWidth = self.view.frame.size.width;
    CGFloat screenHeight = self.view.frame.size.height;

    // 1. 创建 UIScrollView
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.pagingEnabled = YES;
    self.scrollView.delegate = self;
    self.scrollView.showsHorizontalScrollIndicator = NO;

    // 内容大小（5 页）
    self.scrollView.contentSize = CGSizeMake(screenWidth * 5, screenHeight);

    // 添加 5 张图片
    NSArray *mediaFiles = @[
        @&quot;image1.jpg&quot;,
        @&quot;image2.jpg&quot;,
        @&quot;image3.jpg&quot;,
        @&quot;image4.jpg&quot;,
        @&quot;image5.jpg&quot;
    ];
    for (int i = 0; i &amp;lt; 5; i++) {
        NSString *imageName = [NSString stringWithFormat:@&quot;image%d.jpg&quot;, i + 1];
        UIImage *image = [UIImage imageNamed:imageName];

        UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
        imageView.frame = CGRectMake(screenWidth * i, 0, screenWidth, screenHeight);
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        [self.scrollView addSubview:imageView];
    }

    [self.view addSubview:self.scrollView];

    // 2. 创建 UIPageControl
    self.pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(0, screenHeight - 50, screenWidth, 30)];
    self.pageControl.numberOfPages = 5;
    self.pageControl.currentPage = 0;
    self.pageControl.pageIndicatorTintColor = [UIColor lightGrayColor];
    self.pageControl.currentPageIndicatorTintColor = [UIColor blackColor];
    [self.view addSubview:self.pageControl];
}

#pragma mark - UIScrollViewDelegate

// 滚动时持续触发
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGFloat width = scrollView.frame.size.width;
    NSInteger page = (scrollView.contentOffset.x + width / 2) / width;
    self.pageControl.currentPage = page;
}

// 滑动完全停止（也可以在这里设置当前页）
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSLog(@&quot;滑动停止，当前页: %ld&quot;, (long)self.pageControl.currentPage);
}


@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/509ae1312db74613b6277e27eefb8090.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;UIAlertController和UIActivityIndicatorView&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;ViewController.h&quot;

@interface ViewController ()

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i &amp;lt; 2; i++) {
        UIButton* btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        btn.frame = CGRectMake(100, 100 + 100 * i, 100, 40);
        if (i == 0) {
            [btn setTitle:@&quot;米哈游警告&quot; forState:UIControlStateNormal];
        } else if (i == 1) {
            [btn setTitle:@&quot;等待提示器&quot; forState:UIControlStateNormal];
        }
        btn.tag = 101 + i;
        [btn addTarget:self action:@selector(press:) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview: btn];
    }

    // Do any additional setup after loading the view.
}
- (void) press:(UIButton*) btn {
    if (btn.tag == 101) {
        _alertController = [UIAlertController alertControllerWithTitle:@&quot;警告&quot; message:@&quot;手机没有安装原神&quot; preferredStyle:UIAlertControllerStyleAlert];
        // 添加一个&quot;取消&quot;按钮
        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@&quot;我的其他移动设备有原神&quot;
                                                              style:UIAlertActionStyleCancel
                                                            handler:nil];
        [_alertController addAction:cancelAction];
        UIAlertAction *newAction = [UIAlertAction actionWithTitle:@&quot;安装崩铁和原神&quot;
                                                              style:UIAlertActionStyleDefault
                                                            handler:nil];
        [_alertController addAction:newAction];
        // 添加一个&quot;确认&quot;按钮
        UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@&quot;安装原神&quot;
                                                               style:UIAlertActionStyleDefault
                                                             handler:^(UIAlertAction * _Nonnull action) {
            NSLog(@&quot;点击了确认按钮&quot;);
                                                             }];
        [_alertController addAction:confirmAction];
        [self presentViewController: _alertController animated:YES completion:nil];

    } else if (btn.tag == 102) {
        _activi = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(100, 300, 80, 80)];
        _activi.activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium;
        [self.view addSubview:_activi];
        [_activi startAnimating];
        //[_activi stopAnimating];
    }
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/442803332cbd4133b7ab9cc3feb78c07.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;一些拓展：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;preferredStyle： UIAlertControllerStyleAlert：弹窗在屏幕中央&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UIAlertControllerStyleActionSheet：底部弹出，适合操作选项选择（iPhone）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;style 类型： .Default：普通按钮&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;.Cancel：取消按钮（一个对话框最多只能有一个）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;.Destructive：红色字体，表示“危险操作”&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/16ab7b472db741eda7e774fb26ccff5a.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/ad228bc63a92440a97d39849688b99b8.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/5085307ecdea42749222928bae11f6e9.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/148151128&quot;&gt;iOS —— UI 初探&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>iOS —— 3Gshare项目总结与思考</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-149467095-ios-3gshare/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-149467095-ios-3gshare/</guid><description>目录 登陆注册及推出主页面 自动登录 登陆 自动收起键盘 注册 HomeVC SearchVC 上传图片： ArticleVC MyVC 我的信息 评论 &amp;&amp; 活动通知 &amp;&amp; 我的推荐 新关注的 私信： 设置 Tips： 登陆注册及推出主页面 这部分有两种写法：一种是在Scene</description><pubDate>Sat, 06 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%99%BB%E9%99%86%E6%B3%A8%E5%86%8C%E5%8F%8A%E6%8E%A8%E5%87%BA%E4%B8%BB%E9%A1%B5%E9%9D%A2&quot;&gt;登陆注册及推出主页面&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%87%AA%E5%8A%A8%E7%99%BB%E5%BD%95&quot;&gt;自动登录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%99%BB%E9%99%86&quot;&gt;登陆&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%87%AA%E5%8A%A8%E6%94%B6%E8%B5%B7%E9%94%AE%E7%9B%98&quot;&gt;自动收起键盘&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%B3%A8%E5%86%8C&quot;&gt;注册&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#HomeVC&quot;&gt;HomeVC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#SearchVC&quot;&gt;SearchVC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%8A%E4%BC%A0%E5%9B%BE%E7%89%87%EF%BC%9A%C2%A0&quot;&gt;上传图片：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#ArticleVC&quot;&gt;ArticleVC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#MyVC&quot;&gt;MyVC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%88%91%E7%9A%84%E4%BF%A1%E6%81%AF&quot;&gt;我的信息&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%AF%84%E8%AE%BA%20%26%26%20%E6%B4%BB%E5%8A%A8%E9%80%9A%E7%9F%A5%20%26%26%20%E6%88%91%E7%9A%84%E6%8E%A8%E8%8D%90&quot;&gt;评论 &amp;amp;&amp;amp; 活动通知 &amp;amp;&amp;amp; 我的推荐&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%96%B0%E5%85%B3%E6%B3%A8%E7%9A%84&quot;&gt;新关注的&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%A7%81%E4%BF%A1%EF%BC%9A&quot;&gt;私信：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%AE%BE%E7%BD%AE&quot;&gt;设置&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Tips%EF%BC%9A&quot;&gt;Tips：&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;登陆注册及推出主页面&lt;/h2&gt;
&lt;p&gt;这部分有两种写法：一种是在SceneDelegate中推出LoginVC，后在判断登陆成功后退去主要程序。另一种则是先加载主程序，后推出登陆页面。通过同组同学实践证明，后者在推出登陆页面时会闪一下，因此还是建议采用第一种方法。&lt;/p&gt;
&lt;p&gt;本人的登陆页面在最初使用数组储存用户名和密码，后来发现在修改密码时会较为困难，因此我创建了一个单例类。&lt;/p&gt;
&lt;p&gt;同时我简单了解了一种轻量化本地存储方式 setObject: forKey:&lt;/p&gt;
&lt;p&gt;他支持的对象类型包括：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt; &lt;strong&gt;示例&lt;/strong&gt; NSString 用户名、token 等 NSNumber 整数、布尔值等 NSArray 字符串数组、数字数组等 NSDictionary 键值对结构 NSDate 时间 NSData 二进制数据（如图片、加密）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)saveLoginStatus {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:@&quot;wutong&quot; forKey:@&quot;username&quot;];
    [defaults setBool:YES forKey:@&quot;isLoggedIn&quot;];
    [defaults synchronize]; // 可选
}


//读取时

NSString *name = [[NSUserDefaults standardUserDefaults] objectForKey:@&quot;username&quot;];
BOOL isLoggedIn = [[NSUserDefaults standardUserDefaults] boolForKey:@&quot;isLoggedIn&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;NSUserDefaults 的 setObject:forKey: 是用于保存简单用户数据的 API，轻量、易用，适合设置类数据（如用户名、偏好设置、登录状态等）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;//  UserManager.m
//  3GShareee
//
//  Created by 吴桐 on 2025/7/18.
//

#import &quot;UserManager.h&quot;

@implementation UserManager

//单例
+ (instancetype)sharedManager {
    static UserManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&amp;amp;onceToken, ^{
        manager = [[UserManager alloc] init];
        [manager loadUserData];
    });
    return manager;
}

- (id)init {
    self = [super init];
    if (self) {
        _usernames = [NSMutableArray array];
        _passwords = [NSMutableArray array];
        [_usernames addObject:@&quot;1&quot;];
        [_passwords addObject:@&quot;1&quot;];
    }
    return self;
}


//保存到本地
- (void)saveUserData {
    [[NSUserDefaults standardUserDefaults] setObject:self.usernames forKey:@&quot;savedUsernames&quot;];
    [[NSUserDefaults standardUserDefaults] setObject:self.passwords forKey:@&quot;savedPasswords&quot;];
    [[NSUserDefaults standardUserDefaults] synchronize];    //不懂，好像是保存
}

- (void)loadUserData {
    NSArray *savedUsernames = [[NSUserDefaults standardUserDefaults] objectForKey:@&quot;savedUsernames&quot;];
    NSArray *savedPasswords = [[NSUserDefaults standardUserDefaults] objectForKey:@&quot;savedPasswords&quot;];
    NSDictionary *savedGenders = [[NSUserDefaults standardUserDefaults] objectForKey:@&quot;savedUserGenders&quot;];

    if (savedUsernames) {
        self.usernames = [savedUsernames mutableCopy];
    }
    if (savedPasswords) {
        self.passwords = [savedPasswords mutableCopy];
    }

    if (![self.usernames containsObject:@&quot;1&quot;]) {
        [self.usernames addObject:@&quot;1&quot;];
        [self.passwords addObject:@&quot;1&quot;];
    }
}


- (BOOL)updatePasswordForUser:(NSString *)username oldPassword:(NSString *)oldPassword newPassword:(NSString *)newPassword {
    NSUInteger index = [self.usernames indexOfObject:username];

    if (index == NSNotFound) {  //NSNotFound 是 Objective-C 中的一个常量，表示“没有找到”的情况，常用于查找操作的结果。
        return NO;
    }
    if (![oldPassword isEqualToString:self.passwords[index]]) {
        return NO;
    }

    // 更新密码
    self.passwords[index] = newPassword;
    [self saveUserData];

    return YES;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，我设置了管理员密码1 1，用于绕过调试时便捷登陆，不必输入较长密码。注册功能和修改密码功能中，密码为不小于6位的数字或字母。&lt;/p&gt;
&lt;h4&gt;自动登录&lt;/h4&gt;
&lt;p&gt;我设置了autoBtn，和autoBtn01。前者为切换的按钮主体，后者为一个辅助按钮，当用户点击文字时，也会触发和前者相同的函数从而实现按钮图标的切换，更加人性化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self.autoBtn setImage: [UIImage imageNamed: @&quot;autoreserved.png&quot;] forState: UIControlStateNormal];
    [self.autoBtn setImage: [UIImage imageNamed: @&quot;autohighlighted.png&quot;] forState: UIControlStateSelected];
    self.autoBtn.selected = NO;
    [self.autoBtn addTarget: self action: @selector(pressAuto) forControlEvents: UIControlEventTouchUpInside];

self.autoBtn1 = [UIButton buttonWithType: UIButtonTypeRoundedRect];
    self.autoBtn1.frame = CGRectMake(67, 550, 64, 16);
    [self.autoBtn1 setTitle: @&quot;自动登录&quot; forState: UIControlStateNormal];
    [self.autoBtn1 setTintColor: [UIColor colorWithDisplayP3Red: 14.0 / 255 green: 46.0 / 255 blue: 121.0 / 255 alpha: 1.0]];
    [self.autoBtn1 addTarget: self action: @selector(pressAuto) forControlEvents: UIControlEventTouchUpInside];
#### 登陆


```objective-c
UserManager *userManager = [UserManager sharedManager];
    self.arrayUsername = userManager.usernames;
    self.arrayPassword = userManager.passwords;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-(void) pressLeft:(UIButton *) button{
    NSString *username = self.userName.text;
    NSString *password = self.passWord.text;

    // 非空检查
    if (username.length == 0 || password.length == 0) {
        [self showAlertWithMessage:@&quot;用户名和密码不能为空&quot;];
        return;
    }

    // 长度限制
    if (username.length &amp;gt; 10 || password.length &amp;gt; 10) {
        [self showAlertWithMessage:@&quot;用户名和密码不能超过10个字符&quot;];
        return;
    }

    // 正则判断是否仅包含字母、数字、下划线
    // 本人暂时还没学...
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@&quot;^[A-Za-z0-9_]+$&quot; options:0 error:nil];
    if ([regex numberOfMatchesInString:username options:0 range:NSMakeRange(0, username.length)] == 0 ||
        [regex numberOfMatchesInString:password options:0 range:NSMakeRange(0, password.length)] == 0) {
        [self showAlertWithMessage:@&quot;用户名和密码只能包含字母、数字和下划线&quot;];
        return;
    }

    BOOL correct = NO;
    for (int i = 0; i &amp;lt; self.arrayUsername.count; i++) {
            if ([self.arrayUsername[i] isEqualToString: self.userName.text] &amp;amp;&amp;amp;
                [self.arrayPassword[i] isEqualToString: self.passWord.text] &amp;amp;&amp;amp;
                (self.userName.text != nil) &amp;amp;&amp;amp;
                (self.passWord.text != nil)) {
                correct = YES;

                // 保存当前登录用户
                UserManager *userManager = [UserManager sharedManager];
                userManager.currentUser = self.userName.text;

                break;
            }
        }
    if (!correct) {
        UIAlertController* wrongWarning  = [UIAlertController alertControllerWithTitle:@&quot;❗️&quot; message:@&quot;账号密码错误！&quot; preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction* sure = [UIAlertAction actionWithTitle:@&quot;O K&quot; style:UIAlertActionStyleDefault handler:nil];
        [wrongWarning addAction:sure];
        [self presentViewController:wrongWarning animated:YES completion:nil];
    } else {
        FirstVC* firstView = [[FirstVC alloc] init];
        firstView.view.backgroundColor = [UIColor colorWithRed: (230.0 / 255) green: (222.0 / 255) blue: (220.0 / 255) alpha: 1];
        firstView.tabBarItem = [[UITabBarItem alloc] initWithTitle: nil image: [[UIImage imageNamed: @&quot;FirstVC.png&quot;]  imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] selectedImage: [[UIImage imageNamed: @&quot;FirstVC_tapped.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] ];
        SecondVC* secondView = [[SecondVC alloc] init];
        secondView.view.backgroundColor = [UIColor colorWithRed: (230.0 / 255) green: (222.0 / 255) blue: (220.0 / 255) alpha: 1];
        secondView.tabBarItem = [[UITabBarItem alloc] initWithTitle: nil image: [[UIImage imageNamed: @&quot;SecondVC.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] selectedImage: [[UIImage imageNamed: @&quot;SecondVC_tapped.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] ];
        ThirdVC* thirdView = [[ThirdVC alloc] init];
        thirdView.view.backgroundColor = [UIColor colorWithRed: (230.0 / 255) green: (222.0 / 255) blue: (220.0 / 255) alpha: 1];
        thirdView.tabBarItem = [[UITabBarItem alloc] initWithTitle: nil image: [[UIImage imageNamed: @&quot;ThirdVC.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] selectedImage: [[UIImage imageNamed: @&quot;ThirdVC_tapped.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] ];
        FourthVC* fourthView = [[FourthVC alloc] init];
        fourthView.view.backgroundColor = [UIColor colorWithRed: (230.0 / 255) green: (222.0 / 255) blue: (220.0 / 255) alpha: 1];
        fourthView.tabBarItem = [[UITabBarItem alloc] initWithTitle: nil image: [[UIImage imageNamed: @&quot;FourthVC.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] selectedImage: [[UIImage imageNamed: @&quot;FourthVC_tapped.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] ];
        FifthVC* fifthView = [[FifthVC alloc] init];
        fifthView.view.backgroundColor = [UIColor colorWithRed: (230.0 / 255) green: (222.0 / 255) blue: (220.0 / 255) alpha: 1];
        fifthView.tabBarItem = [[UITabBarItem alloc] initWithTitle: nil image: [[UIImage imageNamed: @&quot;FifthVC.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] selectedImage: [[UIImage imageNamed: @&quot;FifthVC_tapped.png&quot;] imageWithRenderingMode: UIImageRenderingModeAlwaysOriginal] ];

        //用NavigationController将每个视图包起来
        UINavigationController* navigationFirst = [[UINavigationController alloc] initWithRootViewController:firstView];
        UINavigationController* navigationSecond = [[UINavigationController alloc] initWithRootViewController:secondView];
        UINavigationController* navigationThird = [[UINavigationController alloc] initWithRootViewController:thirdView];
        UINavigationController* navigationFourth = [[UINavigationController alloc] initWithRootViewController:fourthView];
        UINavigationController* navigationFifth = [[UINavigationController alloc] initWithRootViewController:fifthView];

        //组装
        UINavigationBarAppearance* appearance = [[UINavigationBarAppearance alloc] init];
        appearance.backgroundColor = [UIColor colorWithRed: (43.0 / 255) green: (123.0 / 255) blue: (191.0 / 255) alpha: 1];
        firstView.navigationController.navigationBar.standardAppearance = appearance;
        firstView.navigationController.navigationBar.barStyle = UIBarStyleDefault;

        firstView.navigationController.navigationBar.scrollEdgeAppearance = appearance;
        secondView.navigationController.navigationBar.scrollEdgeAppearance = appearance;
        thirdView.navigationController.navigationBar.scrollEdgeAppearance = appearance;
        fourthView.navigationController.navigationBar.scrollEdgeAppearance = appearance;
        fifthView.navigationController.navigationBar.scrollEdgeAppearance = appearance;

        NSArray* arrayViewController = [NSArray arrayWithObjects: navigationFirst, navigationSecond, navigationThird, navigationFourth, navigationFifth, nil];
        UITabBarController* tabBarViewController = [[UITabBarController alloc] init];
        tabBarViewController.viewControllers = arrayViewController;
        // 在tabBar上方添加自定义覆盖视图
        UIView* overlayView = [[UIView alloc] initWithFrame:CGRectMake(0, 50, WIDTH, tabBarViewController.tabBar.bounds.size.height)];
        overlayView.backgroundColor = [UIColor blackColor];
        overlayView.tag = 1001;
        [tabBarViewController.tabBar addSubview:overlayView];
        [tabBarViewController.tabBar bringSubviewToFront:overlayView];
        tabBarViewController.modalPresentationStyle = UIModalPresentationFullScreen;
        [self presentViewController: tabBarViewController animated: YES completion: nil];
    }

}
#### 自动收起键盘


```objective-c
- (void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {
    [self.view endEditing:YES];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两个方法笔者暂时也不算很清楚讲述，只算一知半解，只知道能实现这个功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)keyboardWillAppear:(NSNotification *)notification{
    CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    CGFloat keyboardY = keyboardFrame.origin.y;
    [UIView animateWithDuration:0.3 animations:^{
        self.view.transform = CGAffineTransformMakeTranslation(0, keyboardY - self.view.frame.size.height + 20);
    }];
}

- (void)keyboardWillDisAppear:(NSNotification *)notification{
    [UIView animateWithDuration:0.3 animations:^{
        self.view.transform = CGAffineTransformIdentity;
    }];
}
#### 注册


核心代码如下


```objective-c
-(void) pressConfirm {
    NSString *username = self.usernameTextField.text;
    NSString *password = self.passwordTextField.text;
    UserManager *userManager = [UserManager sharedManager];


    if (username.length == 0 || password.length == 0) {
        UIAlertController* warning = [UIAlertController alertControllerWithTitle:@&quot;提示&quot;
                                                                         message:@&quot;账号或密码不能为空&quot;
                                                                  preferredStyle:UIAlertControllerStyleAlert];

        UIAlertAction* warn = [UIAlertAction actionWithTitle:@&quot;确定&quot;
                                                       style:UIAlertActionStyleDefault
                                                     handler:nil];

        [warning addAction:warn];
        [self presentViewController:warning animated:YES completion:nil];
        return;
    }

    // 检查密码长度是否大于6位
    if (password.length &amp;lt; 6) {
        UIAlertController* warning = [UIAlertController alertControllerWithTitle:@&quot;提示&quot;
                                                                         message:@&quot;密码长度必须大于6位&quot;
                                                                  preferredStyle:UIAlertControllerStyleAlert];

        UIAlertAction* warn = [UIAlertAction actionWithTitle:@&quot;确定&quot;
                                                       style:UIAlertActionStyleDefault
                                                     handler:nil];

        [warning addAction:warn];
        [self presentViewController:warning animated:YES completion:nil];
        return;
    }

    // 检查用户名是否已存在
    if ([userManager.usernames containsObject:username]) {
        UIAlertController* warning = [UIAlertController alertControllerWithTitle:@&quot;提示&quot;
                                                                         message:@&quot;该用户名已被注册&quot;
                                                                  preferredStyle:UIAlertControllerStyleAlert];

        UIAlertAction* warn = [UIAlertAction actionWithTitle:@&quot;确定&quot;
                                                       style:UIAlertActionStyleDefault
                                                     handler:nil];
        [warning addAction:warn];
        [self presentViewController:warning animated:YES completion:nil];
        return;
    }

    // 所有检查通过，注册新用户
    [userManager.usernames addObject:username];
    [userManager.passwords addObject:password];
    [userManager saveUserData]; // 保存到磁盘

    // 显示注册成功提示
    UIAlertController* successAlert = [UIAlertController alertControllerWithTitle:@&quot;注册成功&quot;
                                                                         message:@&quot;您已成功注册&quot;
                                                                  preferredStyle:UIAlertControllerStyleAlert];

    UIAlertAction* okAction = [UIAlertAction actionWithTitle:@&quot;确定&quot;
                                                       style:UIAlertActionStyleDefault
                                                     handler:^(UIAlertAction * _Nonnull action) {
        // 关闭注册页面
        [self dismissViewControllerAnimated:YES completion:nil];
    }];

    [successAlert addAction:okAction];
    [self presentViewController:successAlert animated:YES completion:nil];

    // 清空输入框
    self.usernameTextField.text = @&quot;&quot;;
    self.passwordTextField.text = @&quot;&quot;;
    self.emailTextField.text = @&quot;&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HomeVC&lt;/h2&gt;
&lt;p&gt;因为文章的格式类似如图，我新建了一个cell用于设置所有类似的页面，textTableViewCell&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/05d69ce910de44f9a6b59840dd07d049.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首页中，我们需要在点击假日时跳转到另一个页面，同时实现两个页面之间的点赞同步。&lt;/p&gt;
&lt;p&gt;先来说说推出假日页面。&lt;/p&gt;
&lt;p&gt;我在texttableView中添加了一个手势识别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
                                                    initWithTarget:self
                                                    action:@selector(CellTap)];
                [self.contentView addGestureRecognizer:tapGesture];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- (void)CellTap {
    if ([self.delegate respondsToSelector:@selector(textTableViewCellDidTap:)]) {
        [self.delegate textTableViewCellDidTap:self];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在首页中： 我们只处理第一行的情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)textTableViewCellDidTap:(textTableViewCell *)cell {
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    if (indexPath.section == 1 &amp;amp;&amp;amp; indexPath.row == 0) {
        NSMutableDictionary *holidayData = [self.dataArray[0] mutableCopy];

        HolidayDetailViewController *detailVC = [[HolidayDetailViewController alloc] init];
        detailVC.holidayData = holidayData;
        detailVC.delegate = self;
        detailVC.isLiked = [holidayData[@&quot;isLiked&quot;] boolValue]; // 传递当前点赞状态

        // 推入导航栈
        [self.navigationController pushViewController:detailVC animated:YES];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来说说点赞。&lt;/p&gt;
&lt;p&gt;其实下图中的方法更为简便&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b74094ed04b24ef5b3c87559fcfe4c0d.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;列表页点赞：
textTableViewCell (按钮点击) → FirstVC (更新数据源) → 刷新UI&lt;/p&gt;
&lt;p&gt;详情页点赞：
HolidayDetailViewController → FirstVC (通过代理回调) → 更新数据源刷新列表页单元格&lt;/p&gt;
&lt;p&gt;我单独有一篇博客讲解这部分：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149515719?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=149515719&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;细说3Gshare 项目中的点赞双向传值-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;SearchVC&lt;/h2&gt;
&lt;p&gt;搜索大白时，推出页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    if ([self.searchBar.text isEqualToString:@&quot;大白&quot;]) {
        SearchResultViewController* searchResultsView = [[SearchResultViewController alloc] init];
        [self.navigationController pushViewController: searchResultsView animated: YES];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一部分是上传页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/4d9bea6a78ab417da4eacffd6e3a9be6.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实现效果如图。两个textField用来输入作品名称和文章内容&lt;/p&gt;
&lt;h4&gt;上传图片：&lt;/h4&gt;
&lt;p&gt;一个choosePhoto按钮用来弹出照片墙，另一个numbersOfPhotolabel用来显示选中的照片数量。如上文示范图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)pressChoosePhotoButton {
    PhotoWallViewController* photoWallViewController = [[PhotoWallViewController alloc] init];
    photoWallViewController.delegate = self;
    [self.navigationController pushViewController: photoWallViewController animated: YES];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- (void)pressPhoto: (UIButton*)button {
    if (button.selected == NO) {
        int selectNumber = (int)(button.tag - 100);
        self.numbersOfPhoto++;
        [self.imageNameArray addObject: [NSString stringWithFormat: @&quot;photo%d.jpg&quot;, selectNumber]];
        button.selected = YES;
    } else {
        int selectNumber = (int)(button.tag - 100);
        self.numbersOfPhoto--;
        [self.imageNameArray removeObject: [NSString stringWithFormat: @&quot;photo%d.jpg&quot;, selectNumber]];
        button.selected = NO;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;UIAlertAction* boomAction= [UIAlertAction actionWithTitle: @&quot;确定&quot; style: UIAlertActionStyleDefault handler: ^(UIAlertAction *action) {
            // 返回前调用代理方法
            if ([self.delegate respondsToSelector:@selector(changedPhotoName:andNumber:)]) {
                [self.delegate changedPhotoName:self.imageNameArray.firstObject andNumber:self.numbersOfPhoto];
            }
            [self.navigationController popViewControllerAnimated: YES];
        }];
        [boomAlert addAction: boomAction];
        [self presentViewController: boomAlert animated:YES completion:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，在这里我们传回来的是数组的第一个元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)changedPhotoName:(NSString *)nameOfPhoto andNumber:(int)numbersOfPhoto {
    self.numbersOfPhoto = numbersOfPhoto;
    if (nameOfPhoto) {
        [self.choosePhoto setBackgroundImage:[UIImage imageNamed:nameOfPhoto] forState:UIControlStateNormal];
        [self.choosePhoto setTitle:@&quot;&quot; forState:UIControlStateNormal];
    }

    //更新图片数量标签
    self.numbersOfPhotoLabel.text = [NSString stringWithFormat:@&quot;%d&quot;, numbersOfPhoto];
    self.numbersOfPhotoLabel.hidden = (numbersOfPhoto == 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在发布页面修改照片数量，同时修改那个背景。效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b9ae50528c5a4425b2d6747e368f4b2e.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;ArticleVC&lt;/h2&gt;
&lt;p&gt;直接注册三个一模一样的cell，以实现互不干涉。古老简单但是有效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)setupTableViews {
    CGFloat tableHeight = HEIGHT - 150;

    self.tableView01 = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, WIDTH, tableHeight) style:UITableViewStylePlain];
    self.tableView01.delegate = self;
    self.tableView01.dataSource = self;
    self.tableView01.backgroundColor = [UIColor whiteColor];
    self.tableView01.showsVerticalScrollIndicator = NO;
    self.tableView01.separatorStyle = UITableViewCellSeparatorStyleNone;

    self.tableView02 = [[UITableView alloc] initWithFrame:CGRectMake(WIDTH, 0, WIDTH, tableHeight) style:UITableViewStylePlain];
    self.tableView02.delegate = self;
    self.tableView02.dataSource = self;
    self.tableView02.backgroundColor = [UIColor whiteColor];
    self.tableView02.showsVerticalScrollIndicator = NO;
    self.tableView02.separatorStyle = UITableViewCellSeparatorStyleNone;

    self.tableView03 = [[UITableView alloc] initWithFrame:CGRectMake(WIDTH * 2, 0, WIDTH, tableHeight) style:UITableViewStylePlain];
    self.tableView03.delegate = self;
    self.tableView03.dataSource = self;
    self.tableView03.backgroundColor = [UIColor whiteColor];
    self.tableView03.showsVerticalScrollIndicator = NO;
    self.tableView03.separatorStyle = UITableViewCellSeparatorStyleNone;

    // 注册cell
    [self.tableView01 registerClass:[textTableViewCell class] forCellReuseIdentifier:@&quot;cell&quot;];
    [self.tableView02 registerClass:[textTableViewCell class] forCellReuseIdentifier:@&quot;cell&quot;];
    [self.tableView03 registerClass:[textTableViewCell class] forCellReuseIdentifier:@&quot;cell&quot;];

    [self.scrollView addSubview:self.tableView01];
    [self.scrollView addSubview:self.tableView02];
    [self.scrollView addSubview:self.tableView03];
}

- (void)createArticles {
    UIImage *defaultImage = [UIImage systemImageNamed:@&quot;photo&quot;];

    self.articlesSection0 = [NSMutableArray arrayWithArray:@[
        @{@&quot;thumbnail&quot;: [UIImage imageNamed:@&quot;article1&quot;] ?: defaultImage, @&quot;title&quot;: @&quot;如期而至&quot;, @&quot;author&quot;: @&quot;SHARE 钢蛋&quot;, @&quot;category&quot;: @&quot;&quot;, @&quot;time&quot;: @&quot;16&quot;, @&quot;isLiked&quot;: @NO},
        @{@&quot;thumbnail&quot;: [UIImage imageNamed:@&quot;article2&quot;] ?: defaultImage, @&quot;title&quot;: @&quot;duck的学问&quot;, @&quot;author&quot;: @&quot;SHARE 王二麻&quot;, @&quot;category&quot;: @&quot;&quot;, @&quot;time&quot;: @&quot;20&quot;, @&quot;isLiked&quot;: @NO},
        @{@&quot;thumbnail&quot;: [UIImage imageNamed:@&quot;article3&quot;] ?: defaultImage, @&quot;title&quot;: @&quot;您的故事&quot;, @&quot;author&quot;: @&quot;SHARE 和尚&quot;, @&quot;category&quot;: @&quot;&quot;, @&quot;time&quot;: @&quot;25&quot;, @&quot;isLiked&quot;: @NO},
        @{@&quot;thumbnail&quot;: [UIImage imageNamed:@&quot;article4&quot;] ?: defaultImage, @&quot;title&quot;: @&quot;八月的故事&quot;, @&quot;author&quot;: @&quot;SHARE 二五&quot;, @&quot;category&quot;: @&quot;&quot;, @&quot;time&quot;: @&quot;60&quot;, @&quot;isLiked&quot;: @NO},
        @{@&quot;thumbnail&quot;: [UIImage imageNamed:@&quot;article5&quot;] ?: defaultImage, @&quot;title&quot;: @&quot;我们终将再见&quot;, @&quot;author&quot;: @&quot;SHARE 小唐&quot;, @&quot;category&quot;: @&quot;&quot;, @&quot;time&quot;: @&quot;60&quot;, @&quot;isLiked&quot;: @NO}
    ]];

    self.articlesSection1 = [self.articlesSection0 mutableCopy];
    self.articlesSection2 = [self.articlesSection0 mutableCopy];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MyVC&lt;/h2&gt;
&lt;h4&gt;我的信息&lt;/h4&gt;
&lt;h5&gt;评论 &amp;amp;&amp;amp; 活动通知 &amp;amp;&amp;amp; 我的推荐&lt;/h5&gt;
&lt;p&gt;这段代码我也没有很懂，只知道能实现一个类似这样的效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:@&quot;没有新内容&quot; preferredStyle:UIAlertControllerStyleAlert];
        [self presentViewController:alert animated:YES completion:nil];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [alert dismissViewControllerAnimated:YES completion:nil];
        });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/eddef9dd873b4ccdbc8857feac69b174.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;新关注的&lt;/h5&gt;
&lt;p&gt;这部分需要实现一个关注的留存如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/61739ca1fccd43e8a073b62550378015.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过如下方式可以确保只创建一个followVC，进而保存之前的关注。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@property (nonatomic, strong) followViewController *followVC;


else if ([messageType isEqualToString:@&quot;新关注的&quot;]) {
        // 使用强引用确保只创建一次 followViewController
        if (!_followVC) {
            _followVC = [[followViewController alloc] init];
            _followVC.title = @&quot;关注列表&quot;;
            _followVC.view.backgroundColor = [UIColor whiteColor];
        }
        [self.navigationController pushViewController:_followVC animated:YES];
##### 私信：


首先要隐藏tabBar


```objective-c
// 隐藏tabBar
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.tabBarController.tabBar.hidden = YES;
}

// 恢复tabBar
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.tabBarController.tabBar.hidden = NO;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/7feec3dccfff4b8ea8a476b6f63dee54.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)setupMessages {
    _messageArray = [NSMutableArray array];
    _rowHeightArray = [NSMutableArray array];

    [self addMessage:@&quot;1&quot; isOutgoing:NO];
    [self addMessage:@&quot;2&quot; isOutgoing:YES];
    [self addMessage:@&quot;3&quot; isOutgoing:NO];
    [self addMessage:@&quot;4&quot; isOutgoing:YES];
    [self addMessage:@&quot;5&quot; isOutgoing:NO];
    [self addMessage:@&quot;6&quot; isOutgoing:YES];
    [self addMessage:@&quot;7&quot; isOutgoing:YES];
    [self scrollToBottom];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码是我设置的初始聊天，isoutgoing属性用来表示是发送还是接收，实现信息的交替出现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)addMessage:(NSString *)message isOutgoing:(BOOL)isOutgoing {
    NSDictionary *messageDict = @{
        @&quot;text&quot;: message,
        @&quot;outgoing&quot;: @(isOutgoing)
    };
    [_messageArray addObject:messageDict];

    NSDictionary *attri = @{NSFontAttributeName: [UIFont systemFontOfSize:16]};
    /*
     boundingRectWithSize:... 是 NSString 的一个方法
     你告诉他最大容纳的尺寸 会帮你计算出这段字符串在这些限制下需要多大的空间
    */
    CGSize size = [message boundingRectWithSize:CGSizeMake(WIDTH * 0.6, CGFLOAT_MAX)
                                       options:NSStringDrawingUsesLineFragmentOrigin
                                    attributes:attri
                                       context:nil].size;
    CGFloat height = MAX(60, size.height + 40);
    [_rowHeightArray addObject:@(height)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- (void)sendMessage {
    if (self.textField.text.length == 0) return;

    [self addMessage:self.textField.text isOutgoing:self.isNextOutgoing];
    self.isNextOutgoing = !self.isNextOutgoing; //实现交替发送
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.messageArray.count - 1 inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];
    /*
     直接到底部
     */
    [self scrollToBottom];
    self.textField.text = @&quot;&quot;;
}

- (void)scrollToBottom {
    if (self.messageArray.count &amp;gt; 0) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.messageArray.count - 1 inSection:0];
        [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
        /*
         取出最后一条消息然后直接滚动到该条消息处
         */
    }
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.messageArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MessageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@&quot;MessageCell&quot; forIndexPath:indexPath];
    NSDictionary *message = self.messageArray[indexPath.row];
    [cell configureWithText:message[@&quot;text&quot;] isOutgoing:[message[@&quot;outgoing&quot;] boolValue]];
    return cell;
}
### 设置


基本资料需要保存之前修改后的男女性别 不能人家改成女的推出去以后又成男的了


还是以前那个方法，只创建一次


```objective-c
- (void)showBasicInfo {
    if (!_basicsVC) {
            _basicsVC = [[basicsViewController alloc] init];
            _basicsVC.view.backgroundColor = [UIColor whiteColor];
        }
        [self.navigationController pushViewController:_basicsVC animated:YES];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tips：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // 移除分隔线
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个代码可以用来移除cell间的分界线&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UIBarButtonItem* btn = [[UIBarButtonItem alloc] initWithImage: [UIImage imageNamed: @&quot;holidayfanhui.png&quot;] style: UIBarButtonItemStylePlain target: self action: @selector(pressReturn)];
    self.navigationItem.leftBarButtonItem = btn;
    btn.tintColor = [UIColor whiteColor];


- (void)pressReturn {
    [self.navigationController popViewControllerAnimated: YES];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码可以用来自定义返回键类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在tabBar上方添加自定义覆盖视图
        UIView* overlayView = [[UIView alloc] initWithFrame:CGRectMake(0, 50, WIDTH, tabBarViewController.tabBar.bounds.size.height)];
        overlayView.backgroundColor = [UIColor blackColor];
        [tabBarViewController.tabBar addSubview:overlayView];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/04412391b142447089b2e626240b8c6e.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个代码可以实现遮挡tabBar和屏幕底部之间的区域，更加美观，当然如果为了更加自然可以自己调颜色。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149467095&quot;&gt;iOS —— 3Gshare项目总结与思考&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】push 和 present</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151260513-ios-push-present/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151260513-ios-push-present/</guid><description>转场动画是下面几个情况： 导航控制器的push和pop动画。 普通控制器的present和Dismiss动画。 pesent和dismiss : dismiss多级 present还有两个方法可以让我们实现一个跨级返回的效果。presentingViewController 和p</description><pubDate>Sat, 06 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;转场动画是下面几个情况： 导航控制器的push和pop动画。 普通控制器的present和Dismiss动画。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;pesent和dismiss :&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/456da7a46ca64624b9436663279db117.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ViewController3* vc2 = [[ViewController3 alloc] init];
[self presentViewController:vc2 animated:YES completion:nil];

[self dismissViewControllerAnimated:YES completion:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/7947cbc6cf9b476b8c46badd9b3f6710.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;dismiss多级&lt;/h3&gt;
&lt;p&gt;present还有两个方法可以让我们实现一个跨级返回的效果。presentingViewController 和presentedViewController这两个方法分别是什么呢？这里简单解释一下返回两个的对应视图控制器。&lt;/p&gt;
&lt;p&gt;当从1中弹出2后：&lt;/p&gt;
&lt;p&gt;self.presentingViewController 在1中，就是nil；在2中，就是1&lt;/p&gt;
&lt;p&gt;self.presentedViewController在1中，就是2；在2中，就是nil&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;push和pop&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pushViewController:animated:&lt;/code&gt; 方法用于将新的视图控制器推入导航栈。这意味着新控制器将显示在当前控制器的上方，同时当前控制器仍然在堆栈中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SecondViewController *second = [[SecondViewController alloc] init];
    [self.navigationController pushViewController:second animated:YES];

	//返回上一级
	[self.navigationController popViewControllerAnimated:YES];

	//返回根视图
	[self.navigationController popToRootViewControllerAnimated:YES];

	//返回指定级数 （objectAtIndex:参数为想要返回的级数）
	[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:0]  animated:YES];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pop有两个类别：&lt;/p&gt;
&lt;p&gt;第一个类别就是返回上一层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self.navigationController popViewControllerAnimated:YES];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个类别就是返回到某一层&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[self.navigationController popToRootViewControllerAnimated:YES];//这个是返回到根视图
[self.navigationController popToViewController:viewController animated:YES];//返回指定的某一层视图控制器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们返回某一层的视图控制器可以通过这种方式来返回self.navigationController.viewControllers[i]这里的i是你需要的viewController的层级也可以采用for循环通过判断我们的一个view是否符合isKindeOfClass这个方法来找到对应的UIView，来实现返回某一层的ViewController。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(void)touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {
    [self.navigationController popToViewController:self.navigationController.viewControllers[0] animated:YES];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;push和present的区别：&lt;/h2&gt;
&lt;p&gt;push是需要依赖导航控制器Nav的，页面有层级关系，压栈和出栈。而present不需要导航控制器，独立弹出覆盖在原页面。&lt;/p&gt;
&lt;p&gt;在我看来，push多用来进行层级之间的导航，适用于二级页面&lt;/p&gt;
&lt;p&gt;而present则适合于独立的任务如登陆设置等。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151260513&quot;&gt;【iOS】push 和 present&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>细说3Gshare 项目中的点赞双向传值</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-149515719-3gshare-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-149515719-3gshare-/</guid><description>首先附上最终效果图 笔者写这篇文章主要是为了进行一个自我总结。今天在修改这方面bug中思路有些混乱，也借此机会重新总结复盘一下。 一、简介 首先，我们介绍一下代理传值： “代理”本质上是一个 协议（protocol）+ 指针属性 + 回调方法 的组合，它允许 一个控制器（如 A）</description><pubDate>Fri, 05 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先附上最终效果图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b86d7b976dd04130a515fd2f19a19bb7.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;笔者写这篇文章主要是为了进行一个自我总结。今天在修改这方面bug中思路有些混乱，也借此机会重新总结复盘一下。&lt;/p&gt;
&lt;h2&gt;一、简介&lt;/h2&gt;
&lt;p&gt;首先，我们介绍一下代理传值：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“代理”本质上是一个 &lt;strong&gt;协议（protocol）+ 指针属性 + 回调方法&lt;/strong&gt; 的组合，它允许 &lt;strong&gt;一个控制器（如 A）把数据传回另一个控制器（如 B）&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;二、从假日页传值到首页&lt;/h2&gt;
&lt;p&gt;因为这个是我在实操中遇到bug的地方，因此先说这个。&lt;/p&gt;
&lt;h4&gt;在 HolidayDetailViewController.h中声明协议&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@protocol HolidayDetailViewControllerDelegate &amp;lt;NSObject&amp;gt;
- (void)holidayDetail:(HolidayDetailViewController *)detail didChangeLikeStatus:(BOOL)isLiked newLikeCount:(NSInteger)likeCount;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这段代码是定义一个“协议”，告诉别人：“只要你成为我的代理，我就会在用户点赞后通知你，并传给你点赞状态和数量。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;随后，我们定义了一个代理属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@property (nonatomic, weak) id&amp;lt;HolidayDetailViewControllerDelegate&amp;gt; delegate;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面这段代码将点赞的信息从textCell传到详情页再传到首页&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)textTableViewCell:(TextTableViewCell *)cell
       didChangeLikeStatus:(BOOL)isLiked
              newLikeCount:(NSInteger)likeCount
{
    self.isLiked = isLiked;
    self.holidayData[@&quot;isLiked&quot;] = @(isLiked);
    self.holidayData[@&quot;likeCount&quot;] = @(likeCount);
    if ([self.delegate respondsToSelector:@selector(holidayDetail:didChangeLikeStatus:newLikeCount:)]) {
        [self.delegate holidayDetail:self
                didChangeLikeStatus:isLiked
                       newLikeCount:likeCount];
    }
}
#### HomeVC遵守协议并设置自己为代理人


```objective-c
- (void)holidayDetail:(HolidayDetailViewController *)detail
  didChangeLikeStatus:(BOOL)isLiked
         newLikeCount:(NSInteger)likeCount
{
//取出第一行“假日”，然后进行修改，然后替换
    NSMutableDictionary *item = [self.dataArray[0] mutableCopy];
    item[@&quot;isLiked&quot;] = @(isLiked);
    item[@&quot;likeCount&quot;] = @(likeCount);
    [self.dataArray replaceObjectAtIndex:0 withObject:item];

//刷新第一行，
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:1];
    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、 从首页传值到详情页&lt;/h2&gt;
&lt;p&gt;cell中：按钮点击后，触发这个函数，把点赞告诉控制器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)likeButtonTapped:(UIButton *)sender {
    sender.selected = !sender.selected;
    NSInteger currentCount = [self.likeCountLabel.text integerValue];
    if (sender.selected) {
        currentCount++;
    } else {
        currentCount--;

    }
    self.likeCountLabel.text = [NSString stringWithFormat:@&quot;%ld&quot;, (long)currentCount];
    if ([self.delegate respondsToSelector:@selector(textTableViewCell:didChangeLikeStatus:newLikeCount:)]) {
            [self.delegate textTableViewCell:self
                          didChangeLikeStatus:sender.selected
                                 newLikeCount:currentCount];
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在HomeVC中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)textTableViewCellDidTap:(TextTableViewCell *)cell {
    // --------用于获取点击单元格的索引----------
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];

    if (indexPath.section == 1 &amp;amp;&amp;amp; indexPath.row == 0) {
        NSMutableDictionary *holidayData = self.dataArray[0];
        HolidayDetailViewController *detailVC = [[HolidayDetailViewController alloc] init];
        detailVC.holidayData = [self.dataArray[0] mutableCopy];
        detailVC.delegate = self;
        detailVC.isLiked = [self.dataArray[0][@&quot;isLiked&quot;] boolValue];
        /*
         boolValue用于读取YES / NO
         在oc中，字典数组等职能存储对象（指针类型），不能储存基本数据类型，因此储存时候，将基本类型包装成NSNumber对象：
         BOOL isLiked = YES;
         NSNumber *numberObj = @(isLiked); // 或者 [NSNumber numberWithBool:isLiked]
         读取时，将NSNumber解包为基本类型：
         NSNumber *numberObj = data[@&quot;isLiked&quot;];
         BOOL isLiked = [numberObj boolValue];
        */
        [self.navigationController pushViewController:detailVC animated:YES];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- (void)textTableViewCell:(textTableViewCell *)cell
   didChangeLikeStatus:(BOOL)isLiked
          newLikeCount:(NSInteger)likeCount
{
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    if (indexPath.row &amp;gt;= self.dataArray.count) return;
    NSDictionary *originalItem = self.dataArray[indexPath.row];
    NSMutableDictionary *item = [originalItem mutableCopy];
    item[@&quot;isLiked&quot;] = @(isLiked);
    item[@&quot;likeCount&quot;] = @(likeCount);
    [self.dataArray replaceObjectAtIndex:indexPath.row withObject:item];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149515719&quot;&gt;细说3Gshare 项目中的点赞双向传值&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】天气预报仿写总结</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-149641988-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-149641988-ios-/</guid><description>目录 首页 搜索页面 城市详情页 首页 接收两个通知，一个是搜索页面的城市，另一个是详情页面的删除指令。 这个页面要执行两个操作： 点击加号添加城市，删除城市。 这个左滑删除功能需要两段代码 网络请求部分我在另一篇博客中有介绍，在这里不多赘述： 【iOS】网络请求与异步加载 io</description><pubDate>Fri, 05 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%A6%96%E9%A1%B5&quot;&gt;首页&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%90%9C%E7%B4%A2%E9%A1%B5%E9%9D%A2&quot;&gt;搜索页面&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%9F%8E%E5%B8%82%E8%AF%A6%E6%83%85%E9%A1%B5&quot;&gt;城市详情页&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;首页&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/364b360d3b3d4426a1ef4a7d8607f77a.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleAddCityNotification:)
                                                     name:@&quot;AddNewCityNotification&quot;
                                                   object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                                    selector:@selector(handleDeleteCityNotification:)
                                                        name:@&quot;DeleteCityNotification&quot;
                                                      object:nil];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收两个通知，一个是搜索页面的城市，另一个是详情页面的删除指令。&lt;/p&gt;
&lt;p&gt;这个页面要执行两个操作： 点击加号添加城市，删除城市。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/4ca4df3e287f4eab87c735f10e9e4f27.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个左滑删除功能需要两段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 添加支持滑动删除
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
  forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        /*
         要确保是可变数组，不然程序会报错
         */
        self.cityData = [self.cityData mutableCopy];
        self.dicArray = [self.dicArray mutableCopy];
        [self.cityData removeObjectAtIndex:indexPath.section];
        if (indexPath.section &amp;lt; self.dicArray.count) {
            [self.dicArray removeObjectAtIndex:indexPath.section];
        }
        [tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section]
                 withRowAnimation:UITableViewRowAnimationAutomatic];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，我不同城市的天气对应着不同的背景图片：&lt;/p&gt;
&lt;p&gt;我们在MainVC中向自定义cell传了一个conditionCode（NSInteger），这个数值的范围代表了该地现在的天气。然后在cell中设置背景。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TextTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@&quot;main&quot; forIndexPath:indexPath];
    if (indexPath.section &amp;lt; self.dicArray.count) {
        NSDictionary* weatherData = self.dicArray[indexPath.section];
        NSDictionary *current = weatherData[@&quot;current&quot;];
        NSDictionary *condition = current[@&quot;condition&quot;];
        NSString *iconURL = condition[@&quot;icon&quot;];
        NSString *cityName = self.cityData[indexPath.section][@&quot;name&quot;];
        NSString *temp = current[@&quot;temp_c&quot;];
        [cell configureWithCity:cityName
                          temp:[NSString stringWithFormat:@&quot;%@℃&quot;, temp]
               weatherIconURL:iconURL
                 conditionCode:[condition[@&quot;code&quot;] integerValue]];
    }
    return cell;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//textTableViewCell.h

- (void)setBackgroundImageForWeatherCode:(NSInteger)code {
    NSString *imageName = @&quot;photo1.jpg&quot;;
    if (code == 1000) {
        imageName = @&quot;photo1.jpg&quot;;
    }//云
    else if (code &amp;gt;= 1003 &amp;amp;&amp;amp; code &amp;lt;= 1009) {
        imageName = @&quot;photo2.jpg&quot;;
    }//雨
    else if (code &amp;gt;= 1030 &amp;amp;&amp;amp; code &amp;lt;= 1282) {
        imageName = @&quot;photo3.jpg&quot;;
    }//雪
    else if (code &amp;gt;= 1066 &amp;amp;&amp;amp; code &amp;lt;= 1237) {
        imageName = @&quot;photo4.jpg&quot;;
    }//雷
    else if (code &amp;gt;= 1273 &amp;amp;&amp;amp; code &amp;lt;= 1282) {
        imageName = @&quot;photo5.jpg&quot;;
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        self-&amp;gt;_backgroundImageView.image = [UIImage imageNamed:imageName];
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网络请求部分我在另一篇博客中有介绍，在这里不多赘述：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149614611?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=149614611&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;【iOS】网络请求与异步加载_ios imagewithdata 异步-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;搜索页面&lt;/h2&gt;
&lt;p&gt;我们首先要实现搜索结果随输入内容变化而变化：&lt;/p&gt;
&lt;p&gt;我们在.h文件中遵守UISearchBarDelegate，下图为UISearchBarDelegate源码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/ce067a034d1e466e9567e58dfdf9b333.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们在AddVC中加入如下代码，每次检测到改变都重新刷新tableView&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
    if (searchText.length == 0) {
        self.searchResults = @[];
        //self.searchResults = [[NSArray alloc] init];
        [self.tableView reloadData];
        return;
    }
    [self searchCitiesWithKeyword:searchText];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-(void) addCityToMain:(NSDictionary *)cityInfo {
    NSString *cityName = cityInfo[@&quot;name&quot;];
    NSDictionary *userInfo = @{@&quot;cityName&quot;: cityName};
    [[NSNotificationCenter defaultCenter] postNotificationName:@&quot;AddNewCityNotification&quot; object:nil userInfo:userInfo];

    // 关闭添加页面
//    [self dismissViewControllerAnimated:YES completion:nil];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//选中哪个就传给主页
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    if (indexPath.row &amp;lt; self.searchResults.count) {
        NSDictionary *city = self.searchResults[indexPath.row];

        DetailViewController *detailVC = [[DetailViewController alloc] init];
        detailVC.cityName = city[@&quot;name&quot;];
        detailVC.canAddCity = YES;

        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:detailVC];
        nav.modalPresentationStyle = UIModalPresentationFullScreen;
        [self presentViewController:nav animated:YES completion:nil];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们点击其中一行时，我们会跳转到该城市。同时，我们在主页面添加了一个判重逻辑，避免城市重复添加。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (NSDictionary *city in self.cityData) {
        if ([city[@&quot;name&quot;] isEqualToString:newCity]) {
            exists = YES;
            break;
        }
    }
    if (!exists) {
        [self.cityData addObject:@{@&quot;name&quot;: newCity}];
        [self createUrl];
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;城市详情页&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/a68ad9464dfe48bc942331efec8c5fe1.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我的城市详情页分为两种情况：&lt;/p&gt;
&lt;p&gt;在搜索栏点击城市后显示的未被添加过的城市详情右上角是一个添加，而如果该城市是在主页点的，右上角是一个删除。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/de03b2ec6846468a9db88f556bd9aa27.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们设置了一个BOOL属性:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (self.canAddCity) {
        UIButton *addBtn = [UIButton buttonWithType:UIButtonTypeSystem];
        [addBtn setImage:[UIImage systemImageNamed:@&quot;plus&quot;] forState:UIControlStateNormal];
        addBtn.frame = CGRectMake(self.view.bounds.size.width - 60, 50, 40, 40);
        addBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
        [addBtn setTintColor:[UIColor whiteColor]];
        [addBtn addTarget:self action:@selector(addCity) forControlEvents:UIControlEventTouchUpInside];
        addBtn.backgroundColor = [UIColor colorWithWhite:0 alpha:0.25];
        addBtn.layer.cornerRadius = 20;
        addBtn.clipsToBounds = YES;
        [self.scrollView addSubview:addBtn];
    } else {
        UIButton *deleteBtn = [UIButton buttonWithType:UIButtonTypeSystem];
        [deleteBtn setImage:[UIImage systemImageNamed:@&quot;trash&quot;] forState:UIControlStateNormal];
        deleteBtn.frame = CGRectMake(self.view.bounds.size.width - 60, 50, 40, 40);
        deleteBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
        [deleteBtn setTintColor:[UIColor whiteColor]];
        [deleteBtn addTarget:self action:@selector(deleteCity) forControlEvents:UIControlEventTouchUpInside];
        deleteBtn.backgroundColor = [UIColor colorWithWhite:0 alpha:0.25];
        deleteBtn.layer.cornerRadius = 20;
        deleteBtn.clipsToBounds = YES;
        [self.scrollView addSubview:deleteBtn];
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，我实现了一个城市之间的横向滑动切换。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/72d504a8b4fc42e18fb89c412330d0d4.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;据我了解，这个有两种实现方式：用scrollView或PageViewcontroller。因为本人还未使用过后者，因此尝试一下。&lt;/p&gt;
&lt;p&gt;首先，我们要实现UIPageViewControllerDelegate, UIPageViewControllerDataSource。&lt;/p&gt;
&lt;p&gt;下图分别为DataSource和Delegate的definition。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/849c41879029411ebd27b2fd1d6a94c1.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/15b8cc5ff2bf4ccca87a80975d1b52b5.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其次，UIPageViewController默认的是使用 UIPageViewControllerTransitionStylePageCurl ，是一种仿真的翻书效果，在天气预报中，我们要把它改成StryleScroll&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(id) init  {
    self = [super initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
                        navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                                      options:nil];
    return self;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，我们必须实现：UIPageViewControllerDataSource 和 UIPageViewControllerDelegate。前者用于告诉程序他的上一页和下一页是谁，后者是范爷完成的回调。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 分别获取前一个和后一个VC
 */
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
    NSInteger index = [(DetailViewController *)viewController index];
    //不是第一个就去上一个
    if (index &amp;gt; 0) {
        return [self detailViewControllerForIndex:index - 1];
    }
    return nil;
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
    NSInteger index = [(DetailViewController *)viewController index];
    if (index &amp;lt; self.cities.count - 1) {
        return [self detailViewControllerForIndex:index + 1];
    }
    return nil;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149641988&quot;&gt;【iOS】天气预报仿写总结&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【OC】OC语言学习——面向对象（下）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147772892-oc-oc-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147772892-oc-oc-/</guid><description>目录 一、OC的包装类 1.1 以下不是包装类： 1.2 以下是包装类： 二、处理对象 2.1 处理对象和description方法 一、NSLog(@&quot;%@&quot;, obj) 做了什么？ 二、默认行为 三、如何自定义打印内容？ 输出： 四、打印集合对象（NSArray、NSDict</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%80%E3%80%81OC%E7%9A%84%E5%8C%85%E8%A3%85%E7%B1%BB&quot;&gt;一、OC的包装类&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#1.1%20%E4%BB%A5%E4%B8%8B%E4%B8%8D%E6%98%AF%E5%8C%85%E8%A3%85%E7%B1%BB%EF%BC%9A&quot;&gt;1.1 以下不是包装类：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#1.2%20%E4%BB%A5%E4%B8%8B%E6%98%AF%E5%8C%85%E8%A3%85%E7%B1%BB%EF%BC%9A&quot;&gt;1.2 以下是包装类：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BA%8C%E3%80%81%E5%A4%84%E7%90%86%E5%AF%B9%E8%B1%A1&quot;&gt;二、处理对象&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#2.1%20%E5%A4%84%E7%90%86%E5%AF%B9%E8%B1%A1%E5%92%8Cdescription%E6%96%B9%E6%B3%95&quot;&gt;2.1 处理对象和description方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A0%E4%B8%80%E3%80%81NSLog%28%40%22%25%40%22%2C%20obj%29%C2%A0%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F&quot;&gt;一、NSLog(@&quot;%@&quot;, obj) 做了什么？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A0%E4%BA%8C%E3%80%81%E9%BB%98%E8%AE%A4%E8%A1%8C%E4%B8%BA&quot;&gt;二、默认行为&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A0%E4%B8%89%E3%80%81%E5%A6%82%E4%BD%95%E8%87%AA%E5%AE%9A%E4%B9%89%E6%89%93%E5%8D%B0%E5%86%85%E5%AE%B9%EF%BC%9F&quot;&gt;三、如何自定义打印内容？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%BE%93%E5%87%BA%EF%BC%9A&quot;&gt;输出：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A0%E5%9B%9B%E3%80%81%E6%89%93%E5%8D%B0%E9%9B%86%E5%90%88%E5%AF%B9%E8%B1%A1%EF%BC%88NSArray%E3%80%81NSDictionary%EF%BC%89&quot;&gt;四、打印集合对象（NSArray、NSDictionary）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#2.2%20%3D%3D%20%E5%92%8C%20isEqual%E6%96%B9%E6%B3%95&quot;&gt;2.2 == 和 isEqual方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#2.2.1%20%3D%3D%E6%96%B9%E6%B3%95&quot;&gt;2.2.1 ==方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#2.2.2%20%C2%A0isEqual%E6%96%B9%E6%B3%95&quot;&gt;2.2.2 isEqual方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%89%E3%80%81%E7%B1%BB%E5%88%AB%E4%B8%8E%E6%8B%93%E5%B1%95&quot;&gt;三、类别与拓展&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#3.1%20%E7%B1%BB%E5%88%AB&quot;&gt;3.1 类别&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#3.1.1%20%E5%88%A9%E7%94%A8%E7%B1%BB%E5%88%AB%E8%BF%9B%E8%A1%8C%E6%A8%A1%E5%9D%97%E5%8C%96%E8%AE%BE%E8%AE%A1&quot;&gt;3.1.1 利用类别进行模块化设计&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#3.1.2%20%E4%BD%BF%E7%94%A8%E7%B1%BB%E5%88%AB%E6%9D%A5%E8%B0%83%E7%94%A8%E7%A7%81%E6%9C%89%E6%96%B9%E6%B3%95&quot;&gt;3.1.2 使用类别来调用私有方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#3.2%E6%8B%93%E5%B1%95&quot;&gt;3.2拓展&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%9B%9B%E3%80%81%E5%8D%8F%E8%AE%AE%EF%BC%88protocol%EF%BC%89%E4%B8%8E%E5%A7%94%E6%89%98%C2%A0&quot;&gt;四、协议（protocol）与委托&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#4.1%20%E5%8D%8F%E8%AE%AE&quot;&gt;4.1 协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#4.1.1%20%E4%BD%BF%E7%94%A8%E7%B1%BB%E5%88%AB%E5%AE%9E%E7%8E%B0%E9%9D%9E%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE&quot;&gt;4.1.1 使用类别实现非正式协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A04.1.2%20%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%AE%9A%E4%B9%89&quot;&gt;4.1.2 正式协议的定义&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#4.1.3%20%E9%81%B5%E5%AE%88%EF%BC%88%E5%AE%9E%E7%8E%B0%EF%BC%89%E5%8D%8F%E8%AE%AE&quot;&gt;4.1.3 遵守（实现）协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#4.1.4%20%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE%E4%B8%8E%E9%9D%9E%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%B7%AE%E5%BC%82&quot;&gt;4.1.4 正式协议与非正式协议的差异&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#4.1.5%20%E5%8D%8F%E8%AE%AE%E4%B8%8E%E5%A7%94%E6%89%98%EF%BC%88delegate%EF%BC%89&quot;&gt;4.1.5 协议与委托（delegate）&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;一、OC的包装类&lt;/h2&gt;
&lt;p&gt;OC提供了NSValue、NSNumber来封装C语言基本类型（short、int、float等）。&lt;/p&gt;
&lt;p&gt;在 Objective-C 中，**包装类（Wrapper Classes）**是用来把基本数据类型（如 int、float、char 等）“包装”为对象的类。因为 Objective-C 是面向对象的语言，有时候我们需要把基本类型当作对象使用，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;放入 NSArray、NSDictionary 这样的集合中（这些集合只能存放对象）；&lt;/li&gt;
&lt;li&gt;使用对象方法对数值进行操作；&lt;/li&gt;
&lt;li&gt;与 Foundation 框架接口交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1 以下不是包装类：&lt;/h3&gt;
&lt;p&gt;1、NSInteger：大致等于long型整数&lt;/p&gt;
&lt;p&gt;2、NSUInteger：大致等于unsigned long型整数
        3、CGFLoat：在64位平台相当于double，在32位平台相当于float&lt;/p&gt;
&lt;p&gt;以上的类型只是基本类型。为了更好的兼容不同的平台，当程序需要定义整形变量的时候，建议使用&lt;strong&gt;NSInteger，NSUInteger&lt;/strong&gt;；当程序需要定义浮点型变量的时候，建议使用&lt;strong&gt;CGFLoat&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1.2 以下是包装类：&lt;/h3&gt;
&lt;p&gt;1、&lt;strong&gt;NSValue&lt;/strong&gt;是NSNumber的父类，它代表一个更通用的包装类，可以包装int、short、long、float、char、指针、对象id等数据项。并将它们添加到NSArray、NSSet等集合中去。&lt;/p&gt;
&lt;p&gt;2、&lt;strong&gt;NSNumber&lt;/strong&gt;是更具体的包装类，用于包装c语言的各种数值类型。它有如下三类方法：
           ** - [x] +numberWithXxx：直接将特定类型的值包装成NSNumber
            - [x] -initWithXxx：该实例方法需要先创建一个NSNumber对象，再用一个基本类型的值来初始化NSNumber
            - [x] -xxxValue：该实例方法返回该NSNumber对象包装的基本类型的值**
        如上方法的Xxx是Int，Char，Double，string等各种数据类型。&lt;/p&gt;
&lt;p&gt;使用NSNumber的compare方法比较两个值，返回的对象可以转化为-1、0、1，分别代表小于、等于、大于。与bool值比较时，YES代表1，当另一个数大于1时返回1，小于1时返回-1。&lt;/p&gt;
&lt;p&gt;基本类型变量和包装类对象之间的转换关系可以理解为：基本类型变量通过调用numberWithXxx：类方法来转换并返回包装类对象；包装类对象通过调用xxxValue来获取基本类型的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSNumber *num = [NSNumber numberWithInt:66];
        NSNumber *de = [NSNumber numberWithDouble:7.7];
        NSLog(@&quot;%d&quot;,[num intValue]);
        NSLog(@&quot;%g&quot;,[de doubleValue]);
        NSNumber *ch = [[NSNumber alloc] initWithChar:&apos;t&apos;];
        NSLog(@&quot;%@&quot;,ch);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、处理对象&lt;/h2&gt;
&lt;h3&gt;2.1 处理对象和description方法&lt;/h3&gt;
&lt;p&gt;在 Objective-C 中，&lt;strong&gt;打印对象（NSLog(@&quot;%@&quot;, obj)）&lt;/strong&gt; 实际上是调用对象的 -description 方法。这个方法决定了你在控制台看到的输出内容。&lt;/p&gt;
&lt;h4&gt;一、NSLog(@&quot;%@&quot;, obj) 做了什么？&lt;/h4&gt;
&lt;p&gt;当你写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSLog(@&quot;%@&quot;, obj);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它相当于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSLog(@&quot;%@&quot;, [obj description]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;p&gt;NSLog 并不会直接打印对象地址；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它调用了 obj 的 -description 方法，获取一个 NSString* 类型的描述字符串来打印。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;二、默认行为&lt;/h4&gt;
&lt;p&gt;如果你没有重写 -description，那么会输出类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ClassName: 0x10060ae50&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 NSObject 默认的格式，表示“类名 + 内存地址”。&lt;/p&gt;
&lt;h4&gt;三、如何自定义打印内容？&lt;/h4&gt;
&lt;p&gt;你可以重写 -description 方法来自定义输出内容：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end

@implementation Person
- (NSString *)description {
    return [NSString stringWithFormat:@&quot;Person: name=%@, age=%d&quot;, self.name, self.age];
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person *p = [[Person alloc] init];
p.name = @&quot;Tom&quot;;
p.age = 20;
NSLog(@&quot;%@&quot;, p);
#### 输出：


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Person: name=Tom, age=20&lt;/p&gt;
&lt;h4&gt;四、打印集合对象（NSArray、NSDictionary）&lt;/h4&gt;
&lt;p&gt;集合类如 NSArray、NSDictionary、NSSet，当你 NSLog 打印它们时，它们也会调用&lt;strong&gt;内部所有对象的 -description 方法&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSArray *arr = @[p];
NSLog(@&quot;%@&quot;, arr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你没有给 p 写 -description，你就会看到一串地址；&lt;/p&gt;
&lt;p&gt;如果写了，就会输出里面每个对象的自定义内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end

@implementation Person

//重写 description 方法
- (NSString *)description {
    return [NSString stringWithFormat:@&quot;Person: name=%@, age=%d&quot;, self.name, self.age];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.name = @&quot;Tom&quot;;
        p.age = 20;

        // 打印对象
        NSLog(@&quot;%@&quot;, p); // 自动调用 [p description]
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不重写 VS 重写后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Person: 0x600000e812e0&amp;gt;

Person: name=Tom, age=20
### 2.2 == 和 isEqual方法


oc中测试两个变量事都相等的方式有两个，分别是：==方法和isEqual方法


#### 2.2.1 ==方法


当用==方法时，若️①两个变量是基本类型的变量，️②两个变量都是数值型的变量（不一定要求数据类型严格相等），️③两个变量的值相等。则==判断返回真，否则返回假。
         而对于指针类型的变量，则要两个指针指向同一个对象，则==返回真，否则返回假。
         当使用==的两个类没有继承关系时，编译器会提示警告。


**@“hello”和[NSString stringWithFormat:@“hello”]的区别：**
          当OC直接使用@”hello“，系统会使用常量池来管理这些字符串。常量池保证相同的字符串只会有一个，不会产生多个副本，因此创建的所有指向@“hello”的指针，指针变量保存的地址都是完全相同的。
          而使用[NSStringstringWithFormat:@“hello”]创建的字符串对象是运行时创建出来的，它被保存在运行时的内存中（即堆内存），不会放入常量池中。因此它的地址和@“hello”的地址并不相同。


以下代码演示了==的用法：


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int it = 65;
        int fl = 65.0f;
        char ch = &apos;A&apos;;
        NSString *str1 = @&quot;hello&quot;;
        NSString *str2 = @&quot;hello&quot;;
        NSString *str3 = @&quot;byebye&quot;;
        NSLog(@&quot;%d&quot;,(it==fl)); //结果为1
        NSLog(@&quot;%d&quot;,(fl == ch)); //结果为1
        NSLog(@&quot;%d&quot;,(str1 == str2)); //结果为1
        NSLog(@&quot;%d&quot;,(str2 == str3)); //结果为0
        //常量池
        NSString *p1 = @&quot;朱斌&quot;;
        NSString *p2 = @&quot;朱斌&quot;;
        NSLog(@&quot;p1地址：%p,p2地址：%p&quot;,p1,p2);
        NSLog(@&quot;%d&quot;,(p1 == p2)); //结果为1
        NSString *p3 = [NSString stringWithFormat:@&quot;朱斌&quot;];
        NSString *p4 = [NSString stringWithFormat:@&quot;朱斌&quot;];
        NSLog(@&quot;p3地址：%p&quot;,p3);
        NSLog(@&quot;p4地址：%p&quot;,p4);
        NSLog(@&quot;%d&quot;,(p1 == p3)); //结果为0
        NSLog(@&quot;%d&quot;,(p4 == p3)); //结果为0
        NSString *p5 = [NSString stringWithFormat:@&quot;zbchi&quot;];
        NSString *p6 = [NSString stringWithFormat:@&quot;zbchi&quot;];
        NSLog(@&quot;p5地址：%p&quot;,p5);
        NSLog(@&quot;p6地址：%p&quot;,p6);
        NSLog(@&quot;%d&quot;,(p5 == p6)); //结果为1
    }
    return 0;
}
#### 2.2.2 isEqual方法


isEqual比较的是对象的内容。


isEqual默认实现是比较地址（跟 == 一样），但很多系统类（如NSString，NSArray，NSNumber）都 **重写该方法 **来比较内容。所以这个时候输出的是** 内容相等**


```objective-c
#import &quot;FKPerson.h&quot;

@implementation FKPerson

- (id) initWithName: (NSString*) name idStr: (NSString*) idStr {
    if(self = [super init]) {
        self.name = name;
        self.idStr = idStr;
    }
    return self;
}
- (BOOL) isEqual:(id) other {
    //如果两个对象指针相等，为同一个对象
    if(self == other) {
        return YES;
    }
    //当other不为nil且它为FKPerson的实例时
    if(other != nil &amp;amp;&amp;amp; [other isMemberOfClass:FKPerson.class]) {
        FKPerson* target = (FKPerson*)other;
        //并且要判断当前对象的idStr和target对象的idStr相等才可以判断两个对象相等
        return [self.idStr isEqual: target.idStr];
    }
    return NO;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、类别与拓展&lt;/h2&gt;
&lt;p&gt;在oc中，类别和拓展都是对类进行的“补充”机制。&lt;/p&gt;
&lt;h3&gt;3.1 类别&lt;/h3&gt;
&lt;p&gt;类别是oc中用于給已有方法添加方法的一种机制，不能添加成员变量&lt;/p&gt;
&lt;p&gt;类别的定义：&lt;/p&gt;
&lt;p&gt;命名规则：在接口文件部分的文件命名是“类名+类别名.h” 在实现部分的文件命名是“类名+类别名.m” 的形式。&lt;/p&gt;
&lt;p&gt;类别的接口部分的声明和类的定义十分相似，但类别不继承父类，只需要在已有类的类名后面加一个括号，写入类别名，然后再在下面定义方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface ClassName (CategoryName)
- (void)newMethod;
@end

@implementation ClassName (CategoryName)
- (void)newMethod {
    NSLog(@&quot;Category method called&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特点：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只能添加方法，不能添加实例变量（属性也不行）。&lt;/p&gt;
&lt;p&gt;方法是运行时动态添加的&lt;/p&gt;
&lt;p&gt;可以重写原类的方法，但不建议这么做（会覆盖原方法）建议是通过原有类为父类派生一个子类，然后在子类中重写父类的方法&lt;/p&gt;
&lt;p&gt;多个类别中如果有相同方法名，最后编译进来的那个生效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//FKPerson.h


#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

- (void)test1;

@end

NS_ASSUME_NONNULL_END


//FKPerson.m

#import &quot;FKPerson.h&quot;

@implementation FKPerson

- (void)test1 {
    NSLog(@&quot;test1&quot;);
}

@end


//FKPerson+Test2.h

#import &quot;FKPerson.h&quot;

NS_ASSUME_NONNULL_BEGIN

@interface FKPerson (Test2)

- (void)dda;

@end

NS_ASSUME_NONNULL_END


//FKPerson+Test2.m

#import &quot;FKPerson+Test2.h&quot;

@implementation FKPerson (Test2)

- (void)dda {
    NSLog(@&quot;test2&quot;);
}

@end


//NSNumber+FK

#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface NSNumber (FK)

- (NSNumber *)add:(double)num2;
- (NSNumber *)subtract:(double)num2;
- (NSNumber *)multiply:(double)num2;
- (NSNumber *)divide:(double)num2;

@end

NS_ASSUME_NONNULL_END


//NSNumber+FK.m


#import &quot;NSNumber+FK.h&quot;

@implementation NSNumber (FK)

- (NSNumber *)add:(double)num2 {
    return @(self.doubleValue + num2);
}

- (NSNumber *)subtract:(double)num2 {
    return @(self.doubleValue - num2);
}

- (NSNumber *)multiply:(double)num2 {
    return @(self.doubleValue * num2);
}

- (NSNumber *)divide:(double)num2 {
    return @(self.doubleValue / num2);
}

@end


//main


#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKPerson.h&quot;
#import &quot;FKPerson+Test2.h&quot;
#import &quot;NSNumber+FK.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 测试 FKPerson 类和分类
        FKPerson *person = [[FKPerson alloc] init];
        person.name = @&quot;Tom&quot;;
        person.age = 18;

        [person test1];  // 输出 test1
        [person dda];    // 输出 test2

        // 测试 NSNumber 分类
        NSNumber *num1 = @10;
        NSLog(@&quot;Add: %@&quot;, [[num1 add:5] stringValue]);        // 15
        NSLog(@&quot;Sub: %@&quot;, [[num1 subtract:3] stringValue]);   // 7
        NSLog(@&quot;Mul: %@&quot;, [[num1 multiply:2] stringValue]);   // 20
        NSLog(@&quot;Div: %@&quot;, [[num1 divide:2] stringValue]);     // 5
    }
    return 0;
}
#### 3.1.1 利用类别进行模块化设计


         类的实现部分不能分布到多个.m文件中去，因此当某个类非常大时，会导致那个类实现所在的文件非常大，以至于维护起来非常困难。因此如果需要将一个较大的类分模块设计，使用类别是一个不错的选择。通过类别可以对类实现按模块分布到不同的*.m文件中，从而提高项目后期的可维护性。


#### 3.1.2 使用类别来调用私有方法


         在前面的学习中我们知道，在实现部分定义的方法相当于私有方法，通常不允许调用。但是实际上私有方法仍然有办法调用。比如这一小节要学的，通过类别来定义前向引用，从而实现对私有方法的调用。


我们用代码来演示如何调用：


首先，我们定义一个FKItem类的接口部分，只声明了一个info方法


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

@interface FKItem : NSObject

@property (nonatomic,assign) double price;
- (void) info;

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在实现部分新添加一个方法，相当于私有方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKItem.h&quot;

@implementation FKItem

@synthesize price;
- (void) info {
    NSLog(@&quot;这是一个普通的方法&quot;)
}
//新增的私有方法
- (double) calDiscount: (double) discount {
    return self.price *discount;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在主函数中在调用这个私有方法的时候，会报错说没有这个方法。我们可以在main（）函数下新建一个类别，然后再在类别中声明calDiscount方法，这时就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKItem.h&quot;

@interface FKItem (fk)

- (double) calDiscount: (double)diacount;

@end

int main(int argc,char *argv[]) {
    @autoreleasepool {
        FKItem *item = [[FKItem alloc] init];
        item.price = 109;
        [item info];
        NSLog(@&quot;物品打折的价格是：%g&quot;,[item calDiscount:.75]);
    }
}
### 3.2拓展


拓展与类别相似，拓展相当于匿名类别，只能在类的实现文件（.m 文件）中声明，语法如下：


```objective-c
// MyClass.m
@interface MyClass ()  // 没有名字
@property (nonatomic, strong) NSString *secret;  // 私有属性
- (void)privateMethod;                        // 私有方法
@end

@implementation MyClass
// 实现方法
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从语法上来看，扩展相当于定义一个匿名的类别，但从用法来看，类别一般是有特定的.h和.m文件，扩展则用于临时对某个类的接口进行扩展，类实现部分同时实现类接口部分定义的方法和扩展中定义的方法。&lt;/p&gt;
&lt;p&gt;扩展和类别的不同点还有就是：定义类的扩展的时候，可以额外增加实例变量，也可以用@property来合成属性（包括setter和getter方法和对应的成员变量），但定义类的类别的时候，是不允许额外定义实例变量和合成属性的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKCar : NSObject

@property (nonatomic,copy) NSString *brand;
@property (nonatomic,copy) NSString *model;

- (void) drive;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面只是定义了一个FKCar接口，在该接口中定义了两个属性和一个方法，接下来对该类进行拓展&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKCar.h&quot;

NS_ASSUME_NONNULL_BEGIN

@interface FKCar ()

@property (nonatomic,copy) NSString *color;
- (void) drive: (NSString*) owner;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来是FKCar的实现部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKCar.h&quot;
#import &quot;FKCar+drive.h&quot;

@implementation FKCar

@synthesize brand;
@synthesize model;
@synthesize color;

- (void) drive {
    NSLog(@&quot;汽车正在路上跑&quot;);
}
- (void) drive: (NSString*) owner {
    NSLog(@&quot;%@正驾驶着%@汽车在路上跑&quot;,owner,self);
}
- (NSString*) description {
    return [NSString stringWithFormat:@&quot;&amp;lt;FK[brand = %@,model = %@,color = %@&quot;,self.brand,self.model,self.color];
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKCar+drive.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FKCar *car = [[FKCar alloc] init];
        car.brand = @&quot;宝马&quot;;
        car.model = @&quot;X5&quot;;
        car.color = @&quot;黑色&quot;;
        [car drive];
        [car drive: @&quot;孙悟空&quot;];
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;相同点&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; 都是通过 @interface 声明的 都以 @interface 开始来声明方法或属性扩展 都能为已有类添加方法 都可以为类添加实例方法和类方法 都不需要修改原始类的实现 用于对已有类功能的增强或补充 编译后的表现一样 编译器会把类拓展和类别“合并进原类”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;比较项&lt;/strong&gt; &lt;strong&gt;类别（Category）&lt;/strong&gt; &lt;strong&gt;拓展（Extension）&lt;/strong&gt; 声明位置 通常在 .h 文件中公开使用 一般在 .m 文件中，仅在类实现内部使用 是否有名字 有名字，如 MyClass (Custom) 没有名字，是匿名的 方法可见性 添加的是&lt;strong&gt;公有方法&lt;/strong&gt; 添加的是&lt;strong&gt;私有方法&lt;/strong&gt; 属性添加 &lt;strong&gt;不能添加属性&lt;/strong&gt;（除非用 runtime 关联对象） &lt;strong&gt;可以添加属性&lt;/strong&gt;，编译器自动生成 getter/setter 实例变量 &lt;strong&gt;不能添加实例变量&lt;/strong&gt; &lt;strong&gt;可以间接添加实例变量（通过属性）&lt;/strong&gt; 编译器检查 不会强制你实现类别中声明的方法 会强制你实现拓展中声明的方法 典型用途 为系统类或第三方类添加功能 实现类的私有功能（属性和方法）&lt;/p&gt;
&lt;h2&gt;四、协议（protocol）与委托&lt;/h2&gt;
&lt;h3&gt;4.1 协议&lt;/h3&gt;
&lt;p&gt;类是一种具体实现体，而协议则是定义了一种规范，定义了某一批类所需要遵守的规范。协议不提供任何实现，它体现的是规范和实现分离的设计哲学。&lt;/p&gt;
&lt;p&gt;让规范和实现分离正是协议的好处，是一种松耦合的设计。&lt;/p&gt;
&lt;h4&gt;4.1.1 使用类别实现非正式协议&lt;/h4&gt;
&lt;p&gt;这也是类别的作用之一。&lt;/p&gt;
&lt;p&gt;当某个类实现NSObject的该类别时，就需要实现该类别下所有方法，这种基于NSObject定义的类别即可认为是非正式协议。&lt;/p&gt;
&lt;p&gt;以下用代码来演示非正式协议：&lt;/p&gt;
&lt;p&gt;首先我们创建一个NSObject的类别，名为EaTable，并且为其定义一个taste方法：&lt;/p&gt;
&lt;h4&gt;4.1.2 正式协议的定义&lt;/h4&gt;
&lt;p&gt;定义正式协议的时候，不再使用@interface和@implementation关键字了，而是使用@protocol关键字，定义正式协议的语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@protocol 协议名 &amp;lt;父协议1,子协议2&amp;gt; {
    零到多个方法定义......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、协议名应与类名采用相同的命名规则。即协议名应该由多个有意义的单词连接而成，每个单词首字母大写。&lt;/p&gt;
&lt;p&gt;2、一个协议可以有多个直接父协议，但协议只能继承协议，不能继承类。&lt;/p&gt;
&lt;p&gt;3、协议中定义的方法只有方法签名，没有方法实现，协议中包含的方法即可以是类方法也可以是实例方法。&lt;/p&gt;
&lt;p&gt;4、协议中所有方法都是公开的访问权限。&lt;/p&gt;
&lt;p&gt;以下是三个定义正式协议的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
 
NS_ASSUME_NONNULL_BEGIN
 
@protocol FKOutput
 
- (void) output;
- (void) addData(String msg);
 
@end
 
NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面定义了一个FKOutput协议，这个协议定义了两个方法，分别表示添加数据和输出数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
 
NS_ASSUME_NONNULL_BEGIN
 
@protocol FKProoductable
 
- (NSData*) getProductTime;
 
@end
 
NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上定义了一个FKProductable协议，其中定义了一个getProductTime方法，用来返回产品的生产时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKOutput.h&quot;
#import &quot;FKProductable.h&quot;
 
NS_ASSUME_NONNULL_BEGIN
 
@protocol FKPrintable &amp;lt;FKOutput,FKProductable&amp;gt;
 
- (NSString*) printColor;
 
@end
 
NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这个是一个打印机协议，该协议同时继承以上两个协议。&lt;/p&gt;
&lt;p&gt;协议的继承和类的继承不一样，协议完全支持多继承，即一个协议可以有多个直接的父协议。和类继承相似，子协议继承某个父协议，将会获得父协议中的所有方法。&lt;/p&gt;
&lt;p&gt;一个协议继承多个父协议时,多个父协议排在&amp;lt;&amp;gt;中间，多个协议口见以（,）隔开。&lt;/p&gt;
&lt;h4&gt;4.1.3 遵守（实现）协议&lt;/h4&gt;
&lt;p&gt;在类定义的接口部分可以指定该类继承的父类，以及遵守的协议，语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface 类名: 父类&amp;lt;协议1,协议2...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由上面的语法格式可以看出，一个类可以同时遵守多个协议。&lt;/p&gt;
&lt;p&gt;如果程序需要使用协议来定义变量，有如下两种语法：&lt;/p&gt;
&lt;p&gt;1、NSObject&amp;lt;协议1,协议2...&amp;gt;* 变量；&lt;/p&gt;
&lt;p&gt;2、id&amp;lt;协议1,协议2...&amp;gt;* 变量；&lt;/p&gt;
&lt;p&gt;通过上面的语法格式定义的变量，它们的编译时的类型仅仅只是所遵守的协议类型，因此只能调用该协议中定义的方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;声明一个协议&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@protocol MyDelegate &amp;lt;NSObject&amp;gt;
@required
- (void)didFinishTask;
@optional
- (void)didStartTask;
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如何让类遵守协议？ 在类的声明中使用尖括号&amp;lt;协议名&amp;gt; 来表示遵守&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Worker : NSObject &amp;lt;MyDelegate&amp;gt;
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**然后在实现中写出协议方法 **&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@implementation Worker

- (void)didFinishTask {
    NSLog(@&quot;Task finished!&quot;);
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你没有实现@required 方法，**编译器会报警告 **&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用协议的类如何调用这些方法？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Manager : NSObject
@property (nonatomic, weak) id&amp;lt;MyDelegate&amp;gt; delegate;
- (void)doWork;
@end

@implementation Manager

- (void)doWork {
    NSLog(@&quot;Manager is working...&quot;);

    if ([self.delegate respondsToSelector:@selector(didStartTask)]) {
        [self.delegate didStartTask];
    }

    // 工作完成
    if ([self.delegate respondsToSelector:@selector(didFinishTask)]) {
        [self.delegate didFinishTask];
    }
}
@end
#### 4.1.4 正式协议与非正式协议的差异


         1、非正式协议通过为NSObject创建类别来实现，而正式协议直接使用@protocol创建；


        2、遵守非正式协议通过继承带特定类别的NSObject来实现，而遵守正式协议则有专门的OC语法来实现；


        3、遵守非正式协议不要求实现协议中定义的所有方法；而遵守正式协议则必须实现协议中定义的所有方法。


        在OC中还有两个关键字：


        1、**@optional**：位于该关键字只后、@required或@end之前声明的方法是可选的，实现类可选择是否实现这些方法。


        2、**@required**：位于该关键字之后、@optional或@end之前声明的方法是必需的，实现类必需实现这些方法。


        通过在正式协议中使用以上两个关键字，正式协议完全可以代替非正式协议的功能。


#### 4.1.5 协议与委托（delegate）


         定义协议的类可以把协议定义的方法委托给实现协议的类，这样可以让类定义具有更好的通用性质。


        更通用的，当应用程序启动时，应用程序启动的开始加载、加载完成等系列事件，都是委托给相应的代理对象完成的。

---

原文发布于 CSDN：[【OC】OC语言学习——面向对象（下）](https://blog.csdn.net/2402_86720949/article/details/147772892)&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【OC】OC语言学习 —— 重点内容总结与拓展（上）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147934508-oc-oc-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147934508-oc-oc-/</guid><description>目录 隐藏和封装 访问控制符 理解@package访问控制符 对象初始化 一、为对象分配空间 二、初始化方法与对象初始化 OC属性及属性关键字 类、元类、父类的关系 isKindOfClass &amp;&amp; isMemberOfClass 关键字 单例模式 基本创建 使用dispatch</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%9A%90%E8%97%8F%E5%92%8C%E5%B0%81%E8%A3%85&quot;&gt;隐藏和封装&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6%E7%AC%A6&quot;&gt;访问控制符&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%90%86%E8%A7%A3%40package%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6%E7%AC%A6&quot;&gt;理解@package访问控制符&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%AF%B9%E8%B1%A1%E5%88%9D%E5%A7%8B%E5%8C%96&quot;&gt;对象初始化&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%80%E3%80%81%E4%B8%BA%E5%AF%B9%E8%B1%A1%E5%88%86%E9%85%8D%E7%A9%BA%E9%97%B4&quot;&gt;一、为对象分配空间&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BA%8C%E3%80%81%E5%88%9D%E5%A7%8B%E5%8C%96%E6%96%B9%E6%B3%95%E4%B8%8E%E5%AF%B9%E8%B1%A1%E5%88%9D%E5%A7%8B%E5%8C%96&quot;&gt;二、初始化方法与对象初始化&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#OC%E5%B1%9E%E6%80%A7%E5%8F%8A%E5%B1%9E%E6%80%A7%E5%85%B3%E9%94%AE%E5%AD%97&quot;&gt;OC属性及属性关键字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%B1%BB%E3%80%81%E5%85%83%E7%B1%BB%E3%80%81%E7%88%B6%E7%B1%BB%E7%9A%84%E5%85%B3%E7%B3%BB&quot;&gt;类、元类、父类的关系&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#isKindOfClass%20%26%26%20isMemberOfClass&quot;&gt;isKindOfClass &amp;amp;&amp;amp; isMemberOfClass&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%85%B3%E9%94%AE%E5%AD%97&quot;&gt;关键字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F&quot;&gt;单例模式&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%9F%BA%E6%9C%AC%E5%88%9B%E5%BB%BA&quot;&gt;基本创建&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8dispatch_once&quot;&gt;使用dispatch_once&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%88%91%E4%BB%AC%E8%BF%98%E9%9C%80%E8%A6%81%E9%87%8D%E5%86%99alloc%E5%87%BD%E6%95%B0%EF%BC%8C%E4%BD%BFalloc%E5%87%BD%E6%95%B0%E8%BF%94%E5%9B%9E%E5%90%8C%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E5%9C%B0%E5%9D%80%E3%80%82&quot;&gt;我们还需要重写alloc函数，使alloc函数返回同一个对象地址。&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%87%B3%E4%BA%8E%E4%B8%A4%E7%A7%8D%E4%B8%8D%E5%90%8C%E6%A8%A1%E5%BC%8F%EF%BC%9A&quot;&gt;至于两种不同模式：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%87%8D%E5%86%99description%E6%96%B9%E6%B3%95&quot;&gt;重写description方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%3D%3D%E4%B8%8EisEqual%E4%B8%8Ehash&quot;&gt;==与isEqual与hash&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;隐藏和封装&lt;/h2&gt;
&lt;p&gt;有四种访问控制符：==@private(当前类访问权限)，@package(与映像访问权限相同)，@protect(子类访问权限)，@public(公共访问权限)。&lt;/p&gt;
&lt;h3&gt;访问控制符&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1.private（当前类访问权限）&lt;/strong&gt;
成员变量只能在当前类的内部被访问，用于彻底隐藏成员变量。在类的实现部分定义的成员变量默认使用这种访问权限。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.package（与映像访问权限相同）&lt;/strong&gt;
成员变量可以在当前类以及当前类实现的同一个映像的任意地方进行访问。用于部分隐藏成员变量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.protected（子类访问权限）&lt;/strong&gt;
成员变量可以在当前类，当前类的子类的任意地方进行访问。类的接口部分定义的成员变量默认这个访问权限&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4.public（公共访问权限）&lt;/strong&gt;
这个成员变量可以在任意地方进行访问。&lt;/p&gt;
&lt;h3&gt;理解@package访问控制符&lt;/h3&gt;
&lt;p&gt;@package使受他控制的成员变量不仅可以在当前类访问，还可以在相同映像的其他程序中访问。关键何为相同映像？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一映像&lt;/strong&gt;的概念：
简单的说，就是
编译后生成的同一个框架或同一个执行文件
，当我们想开发一个基础框架时，如果用private就限制的太死了，其他函数可能需要直接访问这个成员变量，但是该框架又不希望外部程序访问我们的成员变量，就可以考虑用package了&lt;/p&gt;
&lt;p&gt;**
当编译器最后把@package限制的成员变量所在的类，其他类，函数编译成一个框架库后，这些类、函数就都在一个映像中（注意这里的函数包括我们的主函数）
**。也就是说当我们使用@package之后我们的主函数也可以调用我们的成员变量，但是当其他程序引用这个框架库时，由于其他程序与这个框架库不在一个映像中，其他程序就无法访问我们的被@package限制的成员变量。&lt;/p&gt;
&lt;h2&gt;对象初始化&lt;/h2&gt;
&lt;h3&gt;一、为对象分配空间&lt;/h3&gt;
&lt;p&gt;无论创建哪个对象，我们都先需要使用alloc方法来分配我们的内存，alloc方法来自NSObject类，而所有的类都是他的子类。
我们调用alloc方法时，系统帮我们完成以下的事情。&lt;/p&gt;
&lt;p&gt;1、系统为我们的所有实例变量分配内存空间
2、将每个实例变量的内存空间都置为0，同时对应的类型置为对应的空值。
     仅仅分配内存空间的对象还不能使用，还需要执行初始化，也就是init才可以使用它。&lt;/p&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;二、初始化方法与对象初始化&lt;/h3&gt;
&lt;p&gt;重写一个init来代替NSObject的init&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;Person.h&quot;

@implementation Person

-(id) init {
    if (self = [super init]) {
        self.name = @&quot;四川芬达&quot;;
        self.saying = @&quot;他们朝我扔粑粑&quot;;
        self.age = 123;
    }
    return self;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic,copy) NSString* name;
@property (nonatomic,copy) NSString* saying;
@property (nonatomic,assign) NSInteger age;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;Person.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person* singer = [[Person alloc] init];
        NSLog(@&quot;%@&quot;,singer.name);
        NSLog(@&quot;%@&quot;,singer.saying);
        NSLog(@&quot;%ld&quot;,singer.age);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OC属性及属性关键字&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;@property&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;属性用于封装对象中数据，属性的本质是 ivar + setter + getter。&lt;/p&gt;
&lt;p&gt;可以用 @property 语法来声明属性。@property 会帮我们自动生成属性的 setter 和 getter 方法的声明。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;@synthesize&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;帮我们自动生成 setter 和 getter 方法的实现以及 _ivar。&lt;/p&gt;
&lt;p&gt;你还可以通过 @synthesize 来指定实例变量名字，如果你不喜欢默认的以下划线开头来命名实例变量的话。但最好还是用默认的，否则影响可读性。&lt;/p&gt;
&lt;p&gt;如果不想令编译器合成存取方法，则可以自己实现。如果你只实现了其中一个存取方法 setter or getter，那么另一个还是会由编译器来合成。但是需要注意的是，如果你实现了属性所需的全部方法（如果属性是 readwrite 则需实现 setter and getter，如果是 readonly 则只需实现 getter 方法），那么编译器就不会自动进行 @synthesize，这时候就不会生成该属性的实例变量，需要根据实际情况自己手动 @synthesize 一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@synthesize ivar = _ivar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;@dynamic&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;告诉编译器不用自动进行 @synthesize，你会在运行时再提供这些方法的实现，无需产生警告，但是它不会影响 @property 生成的 setter 和 getter 方法的声明。@dynamic 是 OC 为动态运行时语言的体现。动态运行时语言与编译时语言的区别：动态运行时语言将函数决议推迟到运行时，编译时语言在编译器进行函数决议。&lt;/p&gt;
&lt;h2&gt;类、元类、父类的关系&lt;/h2&gt;
&lt;p&gt;我们先给出类与对象源码的定义：&lt;/p&gt;
&lt;p&gt;我们的类实际上就是一个结构体指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再来看看对象的定义：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/d43d502ef22f4e3f88d02d9d3253e7ad.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OC的类其实也是一个对象，一个对象就要有一个它属于的类，意味着类也要有一个isa指针，指向其所属的类。那么类的类是什么？就是我们所说的元类，所以，元类就是类的所属类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/ef70aa504dec4f83be213364d95bce08.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样我们便可以总结出我们类、元类、父类的基本关系：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;isa指向&lt;/strong&gt;
实例对象的所属类实际上就是其所属类，我们的实例对象就是图中的Instance of Subclass。
接下来我们的isa指向了我们的类，那么继续研究我们类的所属类。
从图中可以看出，我们类的所属类实际上就指向了我们的元类。
接下来我们再看我们的元类，我们所有元类的所属类实际上都是根元类，在笔者参考的资料中是这样理解的，因为我们OC中几乎所有的类都是NSObject的子类，所以我们的根元类也可以认为是我们的NSObject的元类。
接着我们再看图中根元类的虚线，根元类所属的类指向了它本身。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;superClass指向&lt;/strong&gt;
我们这里的superClass指向就是我们类的父类，到了这里其实就比较好理解了，元类与我们的类一样，其父类都是一层层往上的，而不像元类所属的类一样，全部指向我们的根元类&lt;/p&gt;
&lt;p&gt;在这里唯一需要注意的是我们的根元类的父类是我们的根类，也可以理解为NSObject类。而NSObject类的父类就是nil了。&lt;/p&gt;
&lt;h2&gt;isKindOfClass &amp;amp;&amp;amp; isMemberOfClass&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;re1：1    [NSObject类对象 isKindOfClass:[NSObject class]]
NSObject 的元类的继承链最终指向 NSObject 类本身（因为根元类的父类是 NSObject），因此返回 YES（1）

re2：0    [NSObject类对象 isMemberOfClass:[NSObject class]]
NSObject 的类对象的元类是 NSObject 的元类（metaclass），而非 NSObject 类本身，因此返回 NO（0）。

re3：0    [GGObject类对象 isKindOfClass:[GGObject class]]
GGObject 的元类的继承链未指向 GGObject 类本身（除非显式修改元类继承关系），因此 isKindOfClass: 返回 NO（0）。

re4：0    [GGObject类对象 isMemberOfClass:[GGObject class]]
类对象的元类与目标类 GGObject 不匹配，因此 isMemberOfClass: 返回 NO（0）。

re5：1    [[NSObject分配的对象] isKindOfClass:[NSObject class]]
实例对象是 NSObject 的直接实例，且 isKindOfClass: 会检查类的继承链。
NSObject 是所有类的根类，因此返回 YES（1）。

re6：1    [[NSObject分配的对象] isMemberOfClass:[NSObject class]]
实例对象直接属于 NSObject 类，因此 isMemberOfClass: 返回 YES（1）。

re7：1    [[GGObject分配的对象] isKindOfClass:[GGObject class]]
GGObject 实例的类继承自 NSObject，但 isKindOfClass: 会检查到其自身类 GGObject，因此返回 YES（1）。

re8：1    [[GGObject分配的对象] isMemberOfClass:[GGObject class]]
实例对象直接属于 GGObject 类，因此 isMemberOfClass: 返回 YES（1）。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;isKindOfClasss：判断类或元类继承链是否包含目标类&lt;/p&gt;
&lt;p&gt;isMemberOfClass：严格匹配当前类或元类是否等于目标类&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特殊&lt;/strong&gt;：&lt;code&gt;NSObject&lt;/code&gt; 的元类的父类是 &lt;code&gt;NSObject&lt;/code&gt; 类本身，因此 &lt;code&gt;[NSObject class] isKindOfClass:[NSObject class]&lt;/code&gt; 返回 &lt;code&gt;YES&lt;/code&gt;，而其他类（如 &lt;code&gt;GGObject&lt;/code&gt;）的元类无此特性&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;+isKindOfClass 类方法是从当前类的isa指向 (也就是当前类的元类) 开始，沿着 superclass 继承链查找判断和对比类是否相等。 -isKindOfClass 对象方法是从 [self class] (当前类) 开始，沿着 superclass 继承链查找判断和对比类是否相等。 +isMemberOfClass 类方法是直接判断当前类的isa指向 (也就是当前类的元类) 和对比类是否相等。 -isMemberOfClass 对象方法是直接判断 [self class] (当前类) 和对比类是否相等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/fe3aac15624c4f968a3413c5d57a2957.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;编译结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/8595764f3c024a04beefe70abc3f964e.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关键字&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;关键字&lt;/strong&gt; &lt;strong&gt;含义与用途&lt;/strong&gt; @interface 声明一个类或协议 @implementation 实现一个类或协议 @end 结束一个类、实现或协议的定义 @class 向前声明类，解决循环引用 @public @private @protected 成员变量访问控制 @property 声明属性（自动生成 getter/setter） @synthesize 合成属性对应的实例变量（早期） @dynamic 声明属性由开发者手动实现 @protocol 声明协议（类似 Java 中 interface） @optional / @required 协议方法的实现可选或必须 @selector 获取方法选择器 SEL @encode 获取类型的编码 @defs 获取结构体定义（已弃用） @try @catch @finally 异常处理 @throw 抛出异常 @autoreleasepool 自动释放池，用于内存管理 @import 模块导入（代替 #import，更现代） @synchronized 线程同步&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface MyClass : NSObject {
    int _value;
}
- (void)doSomething;
@end

@implementation MyClass
- (void)doSomething {
    NSLog(@&quot;Doing something&quot;);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *str = [[NSString alloc] initWithFormat:@&quot;Hello&quot;];
        NSLog(@&quot;%@&quot;, str);
    } // 自动释放池在此处释放
    return 0;
}

@interface MyClass : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation MyClass
@synthesize name; // 自动生成 _name 和 name 的 getter/setter
@end

@protocol MyDelegate &amp;lt;NSObject&amp;gt;
@required
- (void)doTask;

@optional
- (void)optionalTask;
@end

SEL s = @selector(doSomething);


@class AnotherClass;

@interface MyClass : NSObject
@property (nonatomic, strong) AnotherClass *obj;
@end


@synchronized(self) {
    // 线程安全的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;assign&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;assign：对属性只是简单赋值，不更改对所赋的值的引用计数，这个指示符主要适用于NSInteger等基本类型，以及short、float、double、结构体等各种C数据类型。它是一个弱引用声明类型，我们一般不使用assign来修饰对象，因为被assign修饰的对象，在被释放掉以后，指针的地址还是存在的，也就是说指针不会被置为nil，造成野指针，可能导致程序崩掉。那为什么assign就能用来修饰基本数据类型呢，是因为基本数据类型一般分布在栈上，栈的内存会由系统自动处理，因此不会造成野指针&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;weak&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;weak：该属性也是一个弱引用声明类型，使用weak修饰的对象是不会造成引用计数器+1的，并且引用的对象如果被释放了以后会自动变成nil，不会出现野指针，很好的解决了内存引起的崩溃情况。通常我们会在block和协议的时候使用weak修饰，通过这样的修饰，我们可以规避掉循环引用的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;strong&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;strong：该属性是一个强引用声明类型，只要该强引用指向被赋值的对象，那么该对象就不会自动回收。&lt;/p&gt;
&lt;p&gt;**retain    **&lt;/p&gt;
&lt;p&gt;retain：释放旧的对象，将旧对象的值赋予输入对象，再提高输入对象的索引计数为1；在ARC模式下很少使用，通常用于指针变量，就是说你定义了一个变量，然后这个变量在程序的运行过程当中会改变，并且影响到其他方法。一般用于字符串、数组等&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;copy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;copy：若使用copy指示符，则调用setter方法给变量赋值的时候，会将被赋值的对象复制一个副本，再将该副本赋值给成员变量。copy指示符会将原成员变量所引用对象的计数减1。
         相当于就是说，不用copy的话，会创建一个新的空间，它的内容和原对象内容一模一样，然后指针是指向新空间的。当再有什么操作在对那个对象操作的话，只是在原空间上操作，对新空间没有影响。当成员变量的类型是可变类型，或其子类是可变类型的时候，被赋值的对象有可能在赋值后被修改，如果程序不需要这种修改影响setter方法设置的成员变量的值，此时就可考虑用copy指示符。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;strong、copy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**
如果属性声明中指定了copy特性，合成方法会使用类的copy方法
**
，这里注意：&lt;strong&gt;属性并没有MutableCopy特性。&lt;/strong&gt;
即使是可变的实例变量，也是使用copy特性，正如方法copyWithZone：的执行结果。所以，按照约定会生成一个对象的不可变副本。&lt;/p&gt;
&lt;p&gt;相同之处：用于修饰标识拥有关系的对象&lt;/p&gt;
&lt;p&gt;不同之处：strong赋值是多个指针指向同一个地址，而copy的复制是每次会在内存中复制一份对象，指针指向不同的地址。所有对于不可变对象我们都应该用copy修饰，为确保对象中的字符串值不会无意变动，应该在设置新属性时拷贝一份。&lt;/p&gt;
&lt;p&gt;回顾一下深浅拷贝&lt;/p&gt;
&lt;p&gt;深拷贝就是内容拷贝，浅拷贝就是指针拷贝。&lt;/p&gt;
&lt;p&gt;深拷贝就是拷贝出来和原来仅仅是值一样，但是内存地址完全不一样的新的对象，创建后和原对象没有任何关系。浅拷贝就是拷贝指向原来对象的指针，使原来的对象的引用计数➕1，可以理解为创建了一个指向原对象的指针的新指针而已，并没有创建一个全新的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;Person.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableString *otherName = [[NSMutableString alloc] initWithString:@&quot;Jack &quot;];
             Person *person = [[Person alloc] init];
             person.name = otherName;
             person.age = 23;

             [otherName appendString:@&quot;and rose&quot;];
             NSLog(@&quot;person.name = %@&quot;,person.name);

    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;copy的结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/447f783ebd8c46fda38ab15d8d5034b6.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;strong的结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/7909857e02fb461383f2fd226eaba7f0.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为otherName是可变的，person.name属性是copy，所以创建了新的字符串，属于深拷贝，内容拷贝，我们拷贝出来了一个对象，后面的赋值操作都是针对新建的对象进行操作，而我们实际的调用还是原本的对象。所以值并不会改变。
如果设置为strong，strong会持有原来的对象，使原来的对象引用计数+1，其实就是浅拷贝、指针拷贝。这时我们进行操作，更改其值就使本对象发生了改变。&lt;/p&gt;
&lt;p&gt;我们既然创建的是不可变类型，那我们就不希望其发生改变，所以这个时候我们就应该使用copy关键字，strong会使其在外部被修改时发生改变&lt;/p&gt;
&lt;p&gt;那么对于可变类型字符串 ，我们使用copy时，按约定会生成一个对象的不可变副本，此时我们进行增删改操作就会因为找不到对象而崩溃&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/840f54b495ec48adb27636ac3293d825.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;readonly、readwrite&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是访问权限的控制，决定该属性是否可读和可写，默认是readwrite，所以我们定义属性的时候，一般不需要这个修饰。只有只读属性才加上readonly&lt;/p&gt;
&lt;p&gt;readonly来控制读写权限的方式就是**只生成setter，**不生成getter&lt;/p&gt;
&lt;h2&gt;单例模式&lt;/h2&gt;
&lt;p&gt;单例模式是因为在某些时候，程序多次创建这一类的对象没有实际的意义，那么我们就只在程序运行的时候只初始化一次，自然就产生了一个单例模式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定义：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如果一个类始终只能创建一个实例，则这个类被称为单例类。在程序中，单例类只在程序中被初始化一次，所以单例类是存储在全局区域，在编译时分配内存，只要程序还在运行就一只占用内存，只要在程序结束的时候释放这一部分内存&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有三个注意点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单例类只有一个实例&lt;/li&gt;
&lt;li&gt;单例类只能自己创建自己的实例&lt;/li&gt;
&lt;li&gt;单例类必须给其他对象提供这一实例&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;基本创建&lt;/h3&gt;
&lt;p&gt;单例模式最简单的创建方法是先定义一个static全局变量用于保存已创建的Singleton对象。每次需要获取该实例时，程序先判断，该static变量是否为nil，如果该全局变量为nil，则初始化一个实例并赋值给static全局变量。&lt;/p&gt;
&lt;p&gt;这里只展示此方法下Singleton类的实现部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static id _instance = nil;  //定义static全局变量

@implementation Singleton

+ (id) shareInstance {
	// 先判断_instance是否为空
    if (_instance == nil) {
    	// 为空则初始化
        _instance = [[self alloc] init];
    }
    // 返回实例
    return _instance;
}

@end
### 使用dispatch_once


上面方法在单线程下可以保证单例只被初始化一次；但是多线程的出现，使得在极端条件下，单例也可能返回了不同的对象。如在单例初始化完成前，多个进程同时访问单例，那么这些进程可能都获得了不同的单例对象。


苹果提供了 dispatch_once(dispatch_once_t *predicate,dispatch_block_t block);函数来避免这个问题，该函数保证相关的块必定会执行，且执行一次。此操作完全是线程安全的。


同样只有实现部分代码：


```objective-c
static id _instance = nil;  //定义static全局变量

@implementation Singleton

+ (id) shareInstance {
    // static修饰标记变量
    static dispatch_once_t onceToken;
    dispatch_once(&amp;amp;onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

@end
### 我们还需要 重写alloc函数 ，使alloc函数返回同一个对象地址。


```objective-c
#import &quot;Singleton.h&quot;

@implementation Singleton

// 懒汉式线程安全单例
+ (instancetype)sharedInstance {
    static Singleton *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&amp;amp;onceToken, ^{
        instance = [[super allocWithZone:NULL] init];
    });
    return instance;
}

// 防止通过 alloc/init 创建新实例
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [self sharedInstance];
}

// 防止 copy
- (id)copyWithZone:(NSZone *)zone {
    return self;
}

// 防止 mutableCopy
- (id)mutableCopyWithZone:(NSZone *)zone {
    return self;
}

@end
### 至于两种不同模式：


**懒汉模式**：


· 优点：
 **延迟加载**：懒汉模式只有在第一次访问单例实例时才会进行初始化，可以节省资源，提高性能，因为实例只有在需要时才会被创建。
 **节省内存：**如果单例对象很大或者初始化过程开销较大，懒汉模式可以避免在程序启动时就创建不必要的对象。
 **线程安全性：**可以通过加锁机制（如双重检查锁定）来实现线程安全。


 · 缺点：
 **线程安全性开销：**懒汉模式在实现线程安全时可能需要额外的同步机制，这会引入一些性能开销。
 **复杂性增加：**实现线程安全的懒汉模式可能需要编写复杂的代码，容易引入错误。
 缺点：
 **线程安全性开销：**懒汉模式在实现线程安全时可能需要额外的同步机制，这会引入一些性能开销。
 **复杂性增加：**实现线程安全的懒汉模式可能需要编写复杂的代码，容易引入错误。


 **饿汉模式：**


**优点：**
 **简单：**饿汉模式实现简单，不需要考虑线程安全问题，因为实例在类加载时就已经创建。
 **线程安全性：**由于实例在类加载时创建，不会存在多个实例的风险，因此线程安全。


 **缺点：**
 **无法实现延迟加载：**饿汉模式在程序启动时就创建实例，无法实现延迟加载，可能会浪费资源，特别是当实例很大或初始化开销较大时。
 **可能引起性能问题：**如果单例类的实例在程序启动时没有被使用，那么创建实例的开销可能是不必要的。
 不适用于某些情况：如果单例对象的创建依赖于某些外部因素，而这些因素在程序启动时无法确定，那么饿汉模式可能不适用。


 总的来说，懒汉模式适用于需要延迟加载实例的情况，可以节省资源和提高性能，但需要考虑线程安全性。饿汉模式适用于需要简单实现和线程安全性的情况，但不支持延迟加载。选择哪种模式应根据具体需求和性能考虑来决定。


## 重写description方法


我们将我们的值包装成对象后我们肯定就需要打印他了。**当我们用NSLog单独打印我们的对象时，实际上是调用我们的description方法来返回我们的类的十六位进制**，但是我们并不想要得到这个东西，我们想要得到的是他的值，所以我们就要重写我们的description方法。


```objective-c
@implementation Person

- (NSString *)description {
    return [NSString stringWithFormat:@&quot;Person: name = %@, age = %ld&quot;, self.name, (long)self.age];
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;==与isEqual与hash&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;比较方式&lt;/strong&gt; &lt;strong&gt;方法/符号&lt;/strong&gt; &lt;strong&gt;作用&lt;/strong&gt; &lt;strong&gt;是否可重写&lt;/strong&gt; &lt;strong&gt;比较内容&lt;/strong&gt; 指针比较 == 判断两个对象地址是否相同 不可重写 &lt;strong&gt;内存地址&lt;/strong&gt; 内容比较 isEqual: 判断两个对象内容是否相等 通常需重写 &lt;strong&gt;对象内容&lt;/strong&gt; 哈希值比较 hash 通常与 isEqual: 搭配使用 通常需重写 &lt;strong&gt;内容生成的整数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;isEqual 默认是实现是==（也就是地址比较）因此，如果你定义自己的类，**要比较内容，必须重写该方法 **&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@implementation Person

- (BOOL)isEqual:(id)object {
    if (![object isKindOfClass:[Person class]]) return NO;
    Person *other = (Person *)object;
    return [self.name isEqualToString:other.name] &amp;amp;&amp;amp; self.age == other.age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;hash：对象的哈希值，用于快速查找和集合操作。默认NSObject会返回一个基于地址的hash。当你重写了isEqual，&lt;strong&gt;必须配套重写hash&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下面举几个例子：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person

- (BOOL)isEqual:(id)object {
    if (![object isKindOfClass:[Person class]]) return NO;
    Person *other = (Person *)object;
    return [self.name isEqualToString:other.name] &amp;amp;&amp;amp; self.age == other.age;
}

- (NSUInteger)hash {
    return self.name.hash ^ self.age;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@interface Book : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *isbn;
@end

@implementation Book

- (BOOL)isEqual:(id)object {
    if (![object isKindOfClass:[Book class]]) return NO;
    return [self.isbn isEqualToString:((Book *)object).isbn];
}

- (NSUInteger)hash {
    return self.isbn.hash; // 因为只看 isbn 是否相同
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@interface UserAccount : NSObject
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@end

@implementation UserAccount

- (BOOL)isEqual:(id)object {
    if (![object isKindOfClass:[UserAccount class]]) return NO;
    UserAccount *other = object;
    return [self.username isEqualToString:other.username] &amp;amp;&amp;amp;
           [self.email isEqualToString:other.email];
}

- (NSUInteger)hash {
    return self.username.hash ^ self.email.hash;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特别鸣谢：感谢iOS各级学长学姐的博客分享&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147934508&quot;&gt;【OC】OC语言学习 —— 重点内容总结与拓展（上）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Oc语言学习 —— 重点内容总结与拓展（下）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-148003789-oc-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-148003789-oc-/</guid><description>目录 类别（分类）和拓展 三种字符串的管理方式 NSCFConstantString Constant 常量 NSCFString NSTaggedPointerString 三种字符串类型的copy/ mutablecopy / retainCount情况 内存分布补充 NSC</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%B1%BB%E5%88%AB%EF%BC%88%E5%88%86%E7%B1%BB%EF%BC%89%E5%92%8C%E6%8B%93%E5%B1%95&quot;&gt;类别（分类）和拓展&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%89%E7%A7%8D%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E7%AE%A1%E7%90%86%E6%96%B9%E5%BC%8F&quot;&gt;三种字符串的管理方式&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#__NSCFConstantString&quot;&gt;__NSCFConstantString&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#Constant-%3E%E5%B8%B8%E9%87%8F&quot;&gt;Constant-&amp;gt;常量&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#__NSCFString&quot;&gt;__NSCFString&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#NSTaggedPointerString&quot;&gt;NSTaggedPointerString&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%89%E7%A7%8D%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%B1%BB%E5%9E%8B%E7%9A%84copy%2F%20mutablecopy%20%2F%20retainCount%E6%83%85%E5%86%B5&quot;&gt;三种字符串类型的copy/ mutablecopy / retainCount情况&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%86%85%E5%AD%98%E5%88%86%E5%B8%83%E8%A1%A5%E5%85%85&quot;&gt;内存分布补充&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#_NSCFConstantString&quot;&gt;_NSCFConstantString&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#__NSCFString&quot;&gt;__NSCFString&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#NSTaggedPointerString%C2%A0&quot;&gt;NSTaggedPointerString&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%A4%9A%E6%80%81&quot;&gt;多态&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BA%8C%E3%80%81%E6%8C%87%E9%92%88%E7%B1%BB%E5%9E%8B%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2&quot;&gt;二、指针类型强制转换&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E2%80%8B%E5%8D%8F%E8%AE%AE%E4%B8%8E%E5%A7%94%E6%89%98%C2%A0&quot;&gt;​协议与委托&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%A7%84%E8%8C%83%E3%80%81%E5%8D%8F%E8%AE%AE%E4%B8%8E%E6%8E%A5%E5%8F%A3&quot;&gt;规范、协议与接口&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8%E7%B1%BB%E5%88%AB%E5%AE%9E%E7%8E%B0%E9%9D%9E%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE&quot;&gt;使用类别实现非正式协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE&quot;&gt;正式协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E9%81%B5%E5%AE%88%EF%BC%88%E5%AE%9E%E7%8E%B0%E5%8D%8F%E8%AE%AE%EF%BC%89&quot;&gt;遵守（实现协议）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%8D%8F%E8%AE%AE%E7%9A%84%E6%84%8F%E4%B9%89&quot;&gt;协议的意义&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE%E4%B8%8E%E9%9D%9E%E6%AD%A3%E5%BC%8F%E5%8D%8F%E8%AE%AE&quot;&gt;正式协议与非正式协议&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E5%8D%8F%E8%AE%AE%E4%B8%8E%E5%A7%94%E6%89%98&quot;&gt;协议与委托&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%C2%A0%E2%80%8B%E6%B7%B1%E6%8B%B7%E8%B4%9D%E4%B8%8E%E6%B5%85%E6%8B%B7%E8%B4%9D&quot;&gt;​深拷贝与浅拷贝&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;类别（分类）和拓展&lt;/h2&gt;
&lt;p&gt;1、拓展在
编译时
就被添加到类中，而分类则是
运行时
才被整合到类信息中。&lt;/p&gt;
&lt;p&gt;2、合并后的分类数据（方法，属性，协议），会被插入到原来数据的前面。也就是说当分类的方法与原始类的方法重名时，会先去调用分类中实现的方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分类：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;专门用来给类添加新方法&lt;/p&gt;
&lt;p&gt;不能给类添加成员属性，添加成员属性也无法取到&lt;/p&gt;
&lt;p&gt;注意：其实可与通过runtime 给分类添加属性，即属性关联，重写setter，getter方法&lt;/p&gt;
&lt;p&gt;分类中用@property 定义变量，只会生成变量的setter，getter方法的声明，不能生成方法实现和带下划线的成员变量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拓展：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以说是特殊的分类，也可以称为匿名分类&lt;/p&gt;
&lt;p&gt;可以给类添加成员属性，但是是私有变量&lt;/p&gt;
&lt;p&gt;可以给类添加方法，也是私有方法&lt;/p&gt;
&lt;p&gt;拓展只能本类来用&lt;/p&gt;
&lt;h2&gt;三种字符串的管理方式&lt;/h2&gt;
&lt;p&gt;NSString的三种类型：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实际类型&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; &lt;strong&gt;特点&lt;/strong&gt; __NSCFConstantString 编译时字面量字符串 常量区，不可变 NSTaggedPointerString 小型字符串优化类型 存储在指针本身中，提高性能 __NSCFString NSMutableString 或动态创建的 NSString 存储在堆上，可变或不可变&lt;/p&gt;
&lt;h3&gt;__NSCFConstantString&lt;/h3&gt;
&lt;h3&gt;Constant-&amp;gt;常量&lt;/h3&gt;
&lt;p&gt;通俗理解其就是&lt;strong&gt;常量字符串&lt;/strong&gt;，是一种编译时常量。
这种对象存储在字符串常量区。&lt;/p&gt;
&lt;p&gt;通过打印起retainCount的值，发现很大，2^64 - 1，测试证明对其进行release操作时，retainCount不会产生任何变化是创建之后便无法释放掉的对象。&lt;/p&gt;
&lt;p&gt;当我们使用不同的字符串对象进行创建时当内容相同，其对象的地址也相同，这也就证明了常量字符串是一种
**
单例
**
。&lt;/p&gt;
&lt;p&gt;这种对象一般通过字面值 @&quot;…&quot;、CFSTR(&quot;…&quot;) (一种宏定义创建字符串的方法)或者 stringWithString: 方法（现在会报警告⚠️这个方法等同于字面值创建的方法）。&lt;/p&gt;
&lt;h3&gt;__NSCFString&lt;/h3&gt;
&lt;p&gt;和 __NSCFConstantString 不同， __NSCFString 对象是在运行时创建的一种 NSString 子类，他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。&lt;/p&gt;
&lt;p&gt;这种对象被存储在堆上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过 NSString 的 stringWithFormat 等方法创建的 NSString 对象一般都是这种类型。&lt;/strong&gt;
如果字符串长度大于9或者如果有中文或其他特殊符号（可能是非 ASCII 字符）存在的话则会直接成为 __NSCFString 类型&lt;/p&gt;
&lt;h3&gt;NSTaggedPointerString&lt;/h3&gt;
&lt;p&gt;理解这个类型，需要明白什么是标签指针，这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。&lt;/p&gt;
&lt;p&gt;简单来讲可以理解为**
把指针指向的内容直接放在了指针变量的内存地址中
&lt;strong&gt;，因为在 64 位环境下指针变量的大小达到了&lt;/strong&gt; 8 字节足以容纳一些长度较小的内容**。于是使用了标签指针这种方式来优化数据的存储方式。&lt;/p&gt;
&lt;p&gt;从其的引用计数可以看出，这也是&lt;strong&gt;一个释放不掉的单例常量对象&lt;/strong&gt;。当我们使用不同的字符串对象进行创建时当内容相同，其对象的地址也相同。在运行时根据实际情况创建。&lt;/p&gt;
&lt;p&gt;对于 NSString 对象来讲，当非字面值常量的数字，英文字母字符串的长度小于等于 9 的时候会自动成为 NSTaggedPointerString 类型。(小于等于9这个数据也是原博主进行猜测，经过测试在字符串含有q的时候是小于等于7)&lt;/p&gt;
&lt;h3&gt;三种字符串类型的copy/ mutablecopy / retainCount情况&lt;/h3&gt;
&lt;p&gt;总结就是&lt;/p&gt;
&lt;p&gt;无论原来的三个的类型是NSString还是NSMutableString类型&lt;/p&gt;
&lt;p&gt;copy 会使原来的对象引用计数加一
(当然仅有正常类型的字符串，而不是单例创建的，毕竟那两个引用计数是无限的)，并拷贝对象地址给新的指针，所以类型与原类型一致。&lt;/p&gt;
&lt;p&gt;mutableCopy 不会改变引用计数
，会拷贝内容到堆上，生成一个 __NSCFString 对象，新对象的引用计数为1.&lt;/p&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;内存分布补充&lt;/h3&gt;
&lt;h4&gt;_NSCFConstantString&lt;/h4&gt;
&lt;p&gt;对于ConstantString，我们想查看内存分布情况，直接打印str得到的其实是str这个指针的地址信息，前8位是isa指针，17到24位是对应常量字符串的地址，25~32位是字符串的长度。&lt;/p&gt;
&lt;h4&gt;__NSCFString&lt;/h4&gt;
&lt;p&gt;与__NSCFConstantString的存储常量的地址不同&lt;/p&gt;
&lt;p&gt;__NSCFString直接将对应字符串的ASCII码存储在之前17~24字节存储对应字符串地址的地方，而不是通过再存一个地址来进行存储。&lt;/p&gt;
&lt;p&gt;所以对于常量字符串的单例来说，仅仅存储地址，哪怕后面再创建新的字符串，但是只要内容相同，str对象里面存储的该字符串的地址都是一样的。而对于CFString来说，每个对象都是新的，每个对象都是由自己内部的地址来直接存储，省略了再次通过地址获取内容的步骤。大家哪怕内容相同，自己也是自己的。&lt;/p&gt;
&lt;p&gt;其实存在于堆中的&lt;/p&gt;
&lt;h4&gt;NSTaggedPointerString&lt;/h4&gt;
&lt;p&gt;所以taggedPointer是进行了一个编码的过程，在Mac10.14和iOS12之前，对value做异或操作的objc_debug_taggedpointer_obfuscator值为0，之后为objc_debug_taggedpointer_obfuscator &amp;amp;= ~_OBJC_TAG_MASK。&lt;/p&gt;
&lt;p&gt;所以我们想得到解码，重新声明全局变量objc_debug_taggedpointer_obfuscator和内联函数_objc_decodeTaggedPointer就好了&lt;/p&gt;
&lt;p&gt;Tagged Pointer是一个特殊的指针，不指向任何实质地址。使用编码的方式产生一个假地址，在需要时，通过解码方式得到其内部存储的数据。TaggedPointer极大的提高了内存利用率和简化了查询步骤。它不单单是一个指针，还包括了其值+某些具体信息（比如个数等等），节省了对象的查询流程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;​ &lt;strong&gt;关于三种字符串的管理方式这部分，详情可以参考学长的博客&lt;/strong&gt;&lt;a href=&quot;https://blog.csdn.net/m0_46110288/article/details/117332127?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=117332127&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;[iOS开发]NSString的三种类型管理方式_ios nsstring 类型-CSDN博客&lt;/a&gt; ​&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这一部分有些无聊。。&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h2&gt;多态&lt;/h2&gt;
&lt;p&gt;oc中指针变量有两个，一个是&lt;strong&gt;编译时的类型&lt;/strong&gt;，一个是&lt;strong&gt;运行时的类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编译时的类型由声明该变量时使用的类型决定&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;运行时的类型与实际赋给该变量的对象决定&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果编译和运行的类型不一致就可能出现所谓的多态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FKBaba：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKBaba.h&quot;

@implementation FKBaba

-(void) baba {
    NSLog(@&quot;别墅里面唱k&quot;);
}
-(void) test {
    NSLog(@&quot;我送阿叔茶具&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FKSon：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKSon.h&quot;

@implementation FKSon

-(void)son {
    NSLog(@&quot;水池里面银龙鱼&quot;);
}
-(void) test {
    NSLog(@&quot;研磨下笔亲手为我提笔字：大展宏图&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/443d0fd1855d4de981b6d6551be2b43d.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;将&lt;strong&gt;子类对象赋给父类指针变量&lt;/strong&gt;，这里就出现了多态。&lt;/p&gt;
&lt;p&gt;a编译时是FKBaba类型，运行时是FKSon类型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/3709307ddc694b32a928473d4d47c3c6.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;同时指针变量在编译时只能调用其编译类型所具有的方法，但在运行时将执行运行时类型所具有的方法。&lt;/p&gt;
&lt;p&gt;我们以这个程序为例子，虽然我们定义了父类的指针变量，但我们指针变量所指向的对象是子类。&lt;/p&gt;
&lt;p&gt;所以我们只能调用父类中的方法，但是因为我们的变量实际指向子类，所以我们用的&lt;strong&gt;方法实际上是子类覆盖父类的方法或父类继承给子类的方法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这也是我们在上面单独调用子类方法会出错的原因。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当把一个子类对象直接赋给父类指针变量，例如我们上方的初始化操作，a变量编译时是FKBaba类型，运行时类型是FKSon。当运行时调用该指针变量的方法，其方法行为总是表现出子类方法的行为特征。这就可能出现：&lt;strong&gt;相同类型的变量&lt;/strong&gt;调用&lt;strong&gt;同一个方法&lt;/strong&gt;呈现出不同的行为特征，这就是多态&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当把一个子类对象直接赋给父类指针变量，例如我们上方的初始化操作，a变量编译时是FKBaba类型，运行时类型是FKSon。当运行时调用该指针变量的方法，其方法行为总是表现出子类方法的行为特征。这就可能出现：&lt;strong&gt;相同类型的变量&lt;/strong&gt;调用&lt;strong&gt;同一个方法&lt;/strong&gt;呈现出不同的行为特征，这就是多态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/828bdd8c91ce4e5b8e5bac8dce95e786.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;而在这个程序中，编译时  [b son]; 不会报错，因为a在编译时系统认为其是FKSon类型，但是运行时就不行了，会报错&lt;/p&gt;
&lt;p&gt;那为什么[a baba]又可以通过编译呢？ 因为FKSon是FKBaba，自动继承了FKBaba的所有非私有成员方法，所以FKSon的对象自然可以调用FKBaba的方法！&lt;/p&gt;
&lt;p&gt;当然了，在这个程序中我迷把一个FKBaba对象赋值给FKSon*类型的变量，这在&lt;strong&gt;逻辑上不合理。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FKSon是FKBaba的子类，你可以把FKSon指针赋给FKBaba（向上转型），但是不能向下转型，除非你显示强转。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;二、指针类型强制转换&lt;/h3&gt;
&lt;p&gt;这一点与C语言十分类似。
除了id类型变量之外，指针变量只能调用编译时的类型方法，而不能调用运行时的类型方法所以我们需要将变量类型强制转换为运行时的类型，也就是对象的类。
​&lt;/p&gt;
&lt;h2&gt;​ 协议与委托&lt;/h2&gt;
&lt;h3&gt;规范、协议与接口&lt;/h3&gt;
&lt;p&gt;类是一种具体的实现题，而协议这是定义了一种规范，定义了某一类所需要遵守的规范。&lt;/p&gt;
&lt;p&gt;协议&lt;strong&gt;不提供任何实现，&lt;strong&gt;它体现的是&lt;/strong&gt;规范和实现分离&lt;/strong&gt;的设计哲学。
  让规范和实现分离正是协议的好处，是一种松耦合的设计。&lt;/p&gt;
&lt;p&gt;tips：OC中协议的作用就相当于其他语言中接口的作用。&lt;/p&gt;
&lt;p&gt;协议定义的是多个类共同的公共行为规范，协议里通常定义的是一组公用方法，但不会为这些方法提供实现，方法的实现则交给类去实现。&lt;/p&gt;
&lt;h3&gt;使用类别实现非正式协议&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (EaTable)
-(void) Lai;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKLaiCai : NSObject
-(void) Lai;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKLaiCai.h&quot;

@implementation FKLaiCai
-(void) Lai {
    NSLog(@&quot;虔诚拜三拜，钱包里面多几百&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKLaiCai.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FKLaiCai* Laicai = [[FKLaiCai alloc] init];
        [Laicai Lai];
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/08f258b0360847849693d6016298cdad.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h3&gt;正式协议&lt;/h3&gt;
&lt;p&gt;定义正式协议的时候，&lt;strong&gt;不再使用@interface，@implementation&lt;/strong&gt;关键字了，而是&lt;strong&gt;使用@protocol&lt;/strong&gt;关键字，定义正式协议语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@protocol 协议名&amp;lt;父协议1，父协议2&amp;gt;

{

    多个方法定义

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于上述语法，做出以下详细说明：&lt;/p&gt;
&lt;p&gt;1、协议名应与类名采用相同的命名规则&lt;/p&gt;
&lt;p&gt;2、一个协议可以有多个字节父协议，但协议只能继承协议，不能继承类&lt;/p&gt;
&lt;p&gt;3、协议中定义的方法只有方法签名，没有方法实现&lt;/p&gt;
&lt;p&gt;4、协议中包含的方法可以是类方法，也可以是实例方法&lt;/p&gt;
&lt;p&gt;tips：因为协议定义的是多个类共同的公共行为规范，所以，协议里所有的方法都是&lt;strong&gt;公开的访问权限。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Printable.h


#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@protocol Printable &amp;lt;NSObject&amp;gt;

@required
-(void)printInfo;


@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//Person.h


#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;Printable.h&quot;

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject&amp;lt;Printable&amp;gt;

@property (nonatomic,copy) NSString* name;
@property (nonatomic,assign) NSInteger age;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//Person.m


#import &quot;Person.h&quot;

@implementation Person

-(void) printInfo {
    NSLog(@&quot;Name: %@, Age: %ld&quot;, self.name, self.age);
}

- (NSString *)infoSummary {
    return [NSString stringWithFormat:@&quot;%@ is %ld years old.&quot;, self.name, (long)self.age];
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;Person.h&quot;
#import &quot;Printable.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.name = @&quot;揽佬&quot;;
        p.age = 99;
        [p printInfo];
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/f98bba6fe21e481fb32ca5f3af244f32.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;我们也可以一个协议，这个协议同时继承两个协议：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKOutput.h&quot;
#import &quot;FKProductable.h&quot;

NS_ASSUME_NONNULL_BEGIN

//定义协议，继承了FKutput、FKProductable两个协议
@protocol FKPrintable &amp;lt;FKOutput, FKProductable&amp;gt;
//定义协议的方法
- (NSString*) printColor;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tips：协议的继承和类的继承不一样，协议&lt;strong&gt;完全支持多继承&lt;/strong&gt;，即一个协议可以有多个直接的父协议。和类继承相似，子协议继承某个父协议，将会获得父协议中的所有方法。一个协议继承多个父协议时,多个父协议排在&amp;lt;&amp;gt;中间，多个协议口见以（,）隔开。&lt;/p&gt;
&lt;h3&gt;遵守（实现协议）&lt;/h3&gt;
&lt;p&gt;在类的接口可以继承该类继承的父类，以及遵守的协议，一个类可以同时遵守多个协议，语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface 类名:父类&amp;lt;协议1, 协议2...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果程序需要使用协议来定义变量，有以下两种语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- NSObject&amp;lt;协议1,协议2...&amp;gt;* 变量；
 - id&amp;lt;协议1,协议2...&amp;gt;* 变量；
### 协议的意义


1，在多个类具有相同行为时，如果没有协议，你必须知道每个对象的具体类型并单独处理。


```objective-c
NSArray&amp;lt;id&amp;lt;Printable&amp;gt;&amp;gt; *objects = @[p, d, r];

for (id&amp;lt;Printable&amp;gt; obj in objects) {
    [obj printInfo]; // 不关心具体类名
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在使用协议后，只要你遵守了协议，我就可以调用你的方法。&lt;/p&gt;
&lt;p&gt;2，解耦代码，实现高内聚&lt;/p&gt;
&lt;h3&gt;正式协议与非正式协议&lt;/h3&gt;
&lt;p&gt;非正式协议通过为NSObject创建类别来实现，而正式协议直接使用@protocol创建；
遵守非正式协议通过继承带特定类别的NSObject来实现，而遵守正式协议则有专门的OC语法来实现；
遵守非正式协议不要求实现协议中定义的所有方法；而遵守正式协议则必须实现协议中定义的所有方法。
  为了弥补遵守正式协议必须实现协议的所有方法造成灵活性不足，在OC还有两个关键字：&lt;/p&gt;
&lt;p&gt;**@optional：**位于该关键字只后、@required或@end之前声明的方法是可选的，实现类可选择是否实现这些方法。
**@required：**位于该关键字之后、@optional或@end之前声明的方法是必需的，实现类必需实现这些方法。
通过在正式协议中使用以上两个关键字，正式协议完全可以代替非正式协议的功能。&lt;/p&gt;
&lt;h3&gt;协议与委托&lt;/h3&gt;
&lt;p&gt;协议体现的是一种规范，定义协议的类可以把协议定义的方法委托给实现协议的类，这样可以让类定义具有更好的通用性，因为具体的动作将由该协议的实现类去完成。无论是基于Mac的Cococa应用开发还是iOS开发，各种应用程序大量依赖委托这个概念。&lt;/p&gt;
&lt;h2&gt;​ 深拷贝与浅拷贝&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本人拙作呈上： &lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147851035?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=147851035&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;OC语言学习——对象复制-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/148003789&quot;&gt;Oc语言学习 —— 重点内容总结与拓展（下）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>AutoLayout与Masonry：简化iOS布局</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-150935318-autolayoutmasonry-ios/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-150935318-autolayoutmasonry-ios/</guid><description>Auto Layout 与 Masonry 苹果提供的自动布局（Auto Layout）能够对视图进行灵活有效的布局。但是，使用原生的自动布局相关的语法创建约束的过程是非常冗长的，可读性也比较差。 Masonry 的目标其实就是 为了解决原生自动布局语法冗长的问题 。 其实说到本</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Auto Layout 与 Masonry&lt;/h2&gt;
&lt;p&gt;苹果提供的自动布局（Auto Layout）能够对视图进行灵活有效的布局。但是，使用原生的自动布局相关的语法创建约束的过程是非常冗长的，可读性也比较差。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/7b580325e618460b9cc1b84972772360.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Masonry 的目标其实就是 &lt;strong&gt;为了解决原生自动布局语法冗长的问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;其实说到本质，它和手动布局是一样的。对一个控件放在哪里，我们依然只关心它的&lt;code&gt;(x, y, width, height)&lt;/code&gt;。但手动布局的方式是，一次性计算出这四个值，然后设置进去，完成布局。但当父控件或屏幕发生变化时，子控件的计算就要重新来过，非常麻烦。&lt;/p&gt;
&lt;p&gt;因此，在自动布局中，我们不再关心&lt;code&gt;(x, y, width, height)&lt;/code&gt;的具体值，我们只关心&lt;code&gt;(x, y, width, height)&lt;/code&gt;四个量对应的约束。&lt;/p&gt;
&lt;h3&gt;约束&lt;/h3&gt;
&lt;h4&gt;添加约束的规则：&lt;/h4&gt;
&lt;p&gt;如果两个控件是父子控件，则添加到父控件中。&lt;/p&gt;
&lt;p&gt;如果两个控件不是父子控件，则添加到层级最近的共同父控件中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/f0d681dfd62541788d27b1feee3f1543.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们查看源码：&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;基本属性&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0d519883628a4cb9a5f226a89b0ab221.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/2416cd34736242988691df8d6dfc1a94.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;添加约束的三种方法：&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0698cdef639c4be8af811881b486b5af.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Masonry是基于AutoLayout实现计算视图Frame的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义这个常量，就可以不用在开发过程中使用&quot;mas_&quot;前缀。
#define MAS_SHORTHAND
// 定义这个常量，就可以让Masonry帮我们自动把基础数据类型的数据，自动装箱为对象类型。
#define MAS_SHORTHAND_GLOBALS
### Masonry的使用：


mas_equalTo和equalTo的区别


**用法** **常见场景** **例子** equalTo(view) 对齐某个 view 的同属性 make.left.equalTo(self.view) equalTo(view.mas_xxx) 对齐某个 view 的特定属性 make.left.equalTo(otherView.mas_right) equalTo(@数值) 固定数值（必须手动包对象） make.width.equalTo(@100) mas_equalTo(数值) 固定数值、结构体（推荐写法） make.width.mas_equalTo(100


```objective-c
[button mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view).offset(20)
        make.bottom.equalTo(self.view).offset(0);
        make.right.equalTo(self.view).offset(10);
        make.left.equalTo(self.view).offset(200);
        make.centerX.equalTo(self.view);//水平居中
        make.centerY.equalTo(self.view);//垂直居中
        make.width.height.mas_equalTo(60);
        make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(20, 20, 20, 20));//四周都留20
        make.width.equalTo(self.view).multipliedBy(0.5); //宽度 = 父视图的一半
    }];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下图为使用Masonry实现的折叠cell demo。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b36fb3ebb8d14f00ac104f65205bc3e5.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)viewDidLoad {
    [super viewDidLoad];
    self.foldTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    self.foldTableView.delegate = self;
    self.foldTableView.dataSource = self;
    [self.view addSubview:self.foldTableView];
    [self.foldTableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(200);
        make.top.equalTo(self.view).offset(300);
        make.width.mas_equalTo(100);
        make.height.mas_equalTo(30);
    }];

    self.array = [NSMutableArray arrayWithObjects:@&quot;1&quot;, @&quot;2&quot;, @&quot;3&quot;, @&quot;4&quot;, @&quot;5&quot;, nil];

    self.btn = [UIButton buttonWithType:UIButtonTypeCustom];
    [self.btn setImage:[UIImage imageNamed:@&quot;kaiqi&quot;] forState:UIControlStateNormal];
    [self.btn setImage:[UIImage imageNamed:@&quot;guanbi&quot;] forState:UIControlStateSelected];
    [self.btn addTarget:self action:@selector(press:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.btn];
    [self.btn mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(270);
        make.top.equalTo(self.view).offset(300);
        make.width.mas_equalTo(40);
        make.height.mas_equalTo(40);
    }];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/150935318&quot;&gt;AutoLayout与Masonry：简化iOS布局&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】折叠cell</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151195540-ios-cell/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151195540-ios-cell/</guid><description>先给出实现效果： 折叠cell的实现 折叠cell的关键是通过按钮控制tableView的高度： 如此以来，点击按钮可以实现tableView高度的变化。 实现选中的单元格移到最前： 我们先获取Array中被选中的项目 然后在数株中移除，并插到头部，然后重新刷新tableView</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;先给出实现效果：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e71ff9c89abb4f008d87d41a1352ab78.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;折叠cell的实现&lt;/h2&gt;
&lt;p&gt;折叠cell的关键是通过按钮控制tableView的高度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)press:(UIButton *)btn {
    [self.foldTableView mas_updateConstraints:^(MASConstraintMaker *make) {
        if (btn.selected) {
            make.height.mas_equalTo(30);
        } else {
            make.height.mas_equalTo(150);
        }
    }];
    btn.selected = !btn.selected;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此以来，点击按钮可以实现tableView高度的变化。&lt;/p&gt;
&lt;p&gt;实现选中的单元格移到最前：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *str = self.array[indexPath.section];
    [self.array removeObjectAtIndex:indexPath.section];
    [self.array insertObject:str atIndex:0];
    [self.foldTableView reloadData];
    [self press:self.btn];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先获取Array中被选中的项目&lt;/p&gt;
&lt;p&gt;然后在数株中移除，并插到头部，然后重新刷新tableView。这样就可以实现效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/06f6608dc3d3475281af92d1e351b541.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151195540&quot;&gt;【iOS】折叠cell&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】 懒加载</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151195677-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151195677-ios-/</guid><description>懒加载 懒加载——也称为延迟加载，即在需要的时候才加载（效率低，占用内存小）。所谓懒加载，写的是其getter方法。说的通俗一点，就是在开发中，当程序中需要利用的资源时。在程序启动的时候不加载资源，只有在运行当需要一些资源时，再去加载这些资源。 使用懒加载的好处： （1）不必将创</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;懒加载&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;懒加载——也称为延迟加载，即在需要的时候才加载（效率低，占用内存小）。所谓懒加载，写的是其getter方法。说的通俗一点，就是在开发中，当程序中需要利用的资源时。在程序启动的时候不加载资源，只有在运行当需要一些资源时，再去加载这些资源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;使用懒加载的好处：&lt;/strong&gt; （1）不必将创建对象的代码全部写在viewDidLoad方法中，代码的可读性更强 （2）每个控件的getter方法中分别负责各自的实例化处理，代码彼此之间的独立性强，松耦合 （3）只有当真正需要资源时，再去加载，节省了内存资源。&lt;/p&gt;
&lt;h2&gt;实现步骤如下：&lt;/h2&gt;
&lt;p&gt;需要注意的是，定义控件属性时必须用strong修饰，至于为什么先来简单看一下strong和weak的区别。strong是一个对象只要有strong指针指向它，那么它就不会被释放，而weak型的指针变量仍然可以指向一个对象，但不属于对象的拥有者。即当对象被销毁的时候，这个weak指针也就自动指向nil（空指针）。也就是说如果我们用weak修饰控件的话，当我们将控件进行创建并初始化时还没来得及将其赋给一个指针并添加到视图上时就被自动释放掉了，也就相当于创建失败了。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;创建属性：在对象的接口中声明一个属性，并在私有成员变量中创建一个实例变量来保存这个属性。 重写 Getter 方法：在实现文件中重写该属性的getter 方法。在 getter 方法中，首先检查私有成员变量是否已经被初始化，如果没有，则进行初始化。 懒加载初始化：在 getter 方法中进行懒加载的初始化。根据具体需求，可以在此处创建对象、加载资源、进行网络请求等。 返回实例：将初始化后的对象或资源返回给调用者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Demo：&lt;/h2&gt;
&lt;p&gt;我们在.h 文件创建一个属性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;@property&lt;/strong&gt; (&lt;strong&gt;strong&lt;/strong&gt;, &lt;strong&gt;nonatomic&lt;/strong&gt;) UILabel* label;&lt;/p&gt;
&lt;p&gt;然后，我们重写getter方法，然后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//  ViewController.m
//  testt
//
//  Created by 吴桐 on 2025/9/4.
//

#import &quot;ViewController.h&quot;
#import &quot;Masonry.h&quot;

@implementation ViewController
//重写UILabel

-(UILabel *) label {
    if (!_label) {
        _label = [[UILabel alloc] initWithFrame: CGRectMake(100, 100, 1000, 100)];
        _label.text =@&quot;懒加载测试&quot;;
        _label.font = [UIFont systemFontOfSize:20];

    }
    return _label;
}

- (void)viewDidLoad {
    [super viewDidLoad];
}

-(void) touchesBegan:(NSSet&amp;lt;UITouch *&amp;gt; *)touches withEvent:(UIEvent *)event {
    [self label];
    [self.view addSubview:self.label];
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;实现效果如下：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/5d3fe32c789d4d00a0504cc0c68df4f7.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151195677&quot;&gt;【iOS】 懒加载&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】网络请求与异步加载</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-149614611-ios-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-149614611-ios-/</guid><description>目录 网络请求 为什么你的图片要异步加载？ ​编辑 什么是“异步网络请求” GET 与 POST GET POST 网络请求 1，用NSString创建URL 2，创建NSURL对象：用URLSession对象进行网络请求 3，创建URLRequeset对其进行相关请求，分为GE</description><pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82&quot;&gt;网络请求&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%A0%E7%9A%84%E5%9B%BE%E7%89%87%E8%A6%81%E5%BC%82%E6%AD%A5%E5%8A%A0%E8%BD%BD%EF%BC%9F&quot;&gt;为什么你的图片要异步加载？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#&quot;&gt;​编辑&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%BB%80%E4%B9%88%E6%98%AF%E2%80%9C%E5%BC%82%E6%AD%A5%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82%E2%80%9D&quot;&gt;什么是“异步网络请求”&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#GET%20%E4%B8%8E%20POST&quot;&gt;GET 与 POST&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#GET&quot;&gt;GET&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#POST&quot;&gt;POST&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;网络请求&lt;/h2&gt;
&lt;p&gt;1，用NSString创建URL&lt;/p&gt;
&lt;p&gt;2，创建NSURL对象：用URLSession对象进行网络请求&lt;/p&gt;
&lt;p&gt;3，创建URLRequeset对其进行相关请求，分为GET和POST方法&lt;/p&gt;
&lt;p&gt;4，根据会话创建任务&lt;/p&gt;
&lt;p&gt;5，启动任务&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/eb0e0b66628d4197b111e92a7a074190.jpeg&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在与web服务器进行通信的过程中，这些类各自扮演了重要的角色。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NSURL&lt;/strong&gt;对象负责以URL的格式保存web应用的位置。对大多数web服务，URL将包含基地址（base address）、web应用名和需要传送的参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NSURLRequest&lt;/strong&gt;对象负责保存需要传送给web服务器的全部数据，这些数据包括：一个&lt;strong&gt;NSURL&lt;/strong&gt;对象、缓存方案（caching policy）、等待web服务器响应的最长时间和需要通过HTTP协议传送的额外信息（&lt;strong&gt;NSMutableURLRequest&lt;/strong&gt; 是** NSURLRequest** 的可变子类）。&lt;/p&gt;
&lt;p&gt;每一个&lt;strong&gt;NSURLSessionTask&lt;/strong&gt;对象都表示一个&lt;strong&gt;NSURLRequest&lt;/strong&gt; 的生命周期。&lt;strong&gt;NSURLSessionTask&lt;/strong&gt;可以跟踪NSURLRequest的状态，还可以对NSURLRequest执行取消、暂停和继续操作。NSURLSessionTask有多种不同功能 的子类，包括&lt;strong&gt;NSURLSessionDataTask&lt;/strong&gt;，&lt;strong&gt;NSURLSessionUploadTask&lt;/strong&gt; 和** NSURLSessionUDownloadTask** 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NSURLSession&lt;/strong&gt;对象可以看作是一个生产&lt;strong&gt;NSURLSessionTask&lt;/strong&gt;对象的工厂。可以设置其生产出的&lt;strong&gt;NSURLSessionTask&lt;/strong&gt;对象的通用属性，例如请求头的内容、是否允许在蜂窝网络下发送请求等。NSURLSession对象还有一个功能强大的委托，可以跟踪NSURLSessionTask对象的状态、处理服务器的认证要求等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(void) fetchData {
    //1、创建URL
    NSString *key = @&quot;2db0265c1d084416b9275428252207&quot;;
    NSString *urlString = [NSString stringWithFormat:@&quot;https://api.weatherapi.com/v1/forecast.json?key=%@&amp;amp;q=%@&amp;amp;days=7&amp;amp;lang=zh&amp;amp;aqi=yes&quot;, key, self.cityName];
    urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    //2、创建URL对象
    NSURL *url = [NSURL URLWithString:urlString];
    //3、创建URL Request，创建任务
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSDictionary *weatherDic = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        self.weatherData = weatherDic;  //最终数据落脚在weatherData
        NSLog(@&quot;%@&quot;,weatherDic);
        dispatch_async(dispatch_get_main_queue(), ^{
            /*
             分别显示基本天气
             每日天气
             每小时天气
             */
            [self setupWeatherData];
            [self setupDailyForecastData];
            [self setupHourlyForecastData];
        });
    }];
    //启动任务
    [task resume];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b8f71ada117846439a257efc4ad9f7b3.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;为什么你的图片要异步加载？&lt;/h2&gt;
&lt;p&gt;在仿写天气预报时，我们常常需要从网络加载天气图标，例如显示某个小时的天气状态图标。这看似简单的事情，如果处理不当，却很容易造成界面卡顿，甚至影响整个 App 的用户体验。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]];
self.weatherIcon.image = [UIImage imageWithData:data];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码会直接在主线程中进行网络请求和图片解码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果网络慢，用户界面会卡住。如果在滑动列表（比如 TableView）中这么做，滚动会非常不流畅&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 iOS 中，&lt;strong&gt;主线程负责所有界面渲染、用户交互&lt;/strong&gt;。一旦主线程被“堵住”，就会引发体验崩溃。&lt;/p&gt;
&lt;p&gt;而且在使用同步加载时：编译器会提示如下：&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;p&gt;因此，我们需要异步网络请求。&lt;/p&gt;
&lt;h2&gt;什么是“异步网络请求”&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;发起请求后不阻塞主线程（UI 线程），系统在线程池里做网络 I/O；等到有结果再通过回调/代理/await 把数据交回来。UI 始终流畅，你再把需要的 UI 更新切回主线程执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;1，URLSession + completion handler（块回调）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最常用、简单。适合大多数 GET/POST、加载图片等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2，URLSession + delegate（代理）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;适合需要“边下边写文件/显示进度/断点续传”的下载或大文件上传。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3，URLSession + background session&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;App 退到后台仍能传输（系统托管），适合较大文件上传下载。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;异步加载 + 主线程更新 UI&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面是一段优化后的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void)loadWeatherIcon:(NSString *)urlString {
    NSURL *url = [NSURL URLWithString:urlString];
    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
        completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
            if (data) {
                UIImage *image = [UIImage imageWithData:data];
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.weatherIcon.image = image;
                });
            }
        }];
    [task resume];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码我们使用的是第一种：URLSession + completion handler 的 dataTask 异步请求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解释：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;dataTask : 适合获取内存数据（图片，JSON）&lt;/p&gt;
&lt;p&gt;回调方式： completion handler block -&amp;gt; 请求完成后系统会在后台线程池调用&lt;/p&gt;
&lt;p&gt;配置：sharedSession&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dataTaskWithURL: 异步方法，不会阻塞主线程。默认就在系统分配的后台线程池中运行&lt;/li&gt;
&lt;li&gt;dispatch_async(dispatch_get_main_queue(), ^{ ... }) 是我们手动回到主线程更新 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用异步请求后，会显著提升页面响应速度。&lt;/p&gt;
&lt;p&gt;至于线程及其他本人暂时还未了解，以后再来填坑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/fc39efffe7aa4aac8c05666b9da19750.gif&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;GET 与 POST&lt;/h2&gt;
&lt;h4&gt;GET&lt;/h4&gt;
&lt;p&gt;优点：3个全在一起（接口、链接、数据）可以在浏览器查看，书写简单。所有信息附加都在地址后面
缺点：明文，保密性差，通过GET提交数据，用户名和密码将明文出现在URL上。文件操作不方便&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString *urlString = @&quot;https://api.weatherapi.com/v1/current.json?key=xxx&amp;amp;q=beijing&quot;;
NSURL *url = [NSURL URLWithString:urlString];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error) {
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            NSLog(@&quot;%@&quot;, json);
        }
    }];
[task resume];
#### POST


优点：密文，保密性好，文件类操作方便，后面不会出现?bjngh
 缺点：无法在浏览器里面查看，实现复杂
 POST把提交的数据则放置在是HTTP包的包体中，GET方式提交的数据最多只能是1024字节，理论上POST没有限制
  


因为post我还没详细了解，给出一段ai代码：


```objective-c
NSURL *url = [NSURL URLWithString:@&quot;https://example.com/api/login&quot;];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @&quot;POST&quot;;

// 参数放在请求体
NSString *bodyString = @&quot;username=wutong&amp;amp;password=123456&quot;;
request.HTTPBody = [bodyString dataUsingEncoding:NSUTF8StringEncoding];

// 设置请求头
[request setValue:@&quot;application/x-www-form-urlencoded&quot; forHTTPHeaderField:@&quot;Content-Type&quot;];

NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request
    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error) {
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
            NSLog(@&quot;%@&quot;, json);
        }
    }];
[task resume];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为本人的这个天气预报类似于一个小demo，所以使用GET，较为简便&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/149614611&quot;&gt;【iOS】网络请求与异步加载&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【OC】Objective - C初探之面向对象</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147576942-oc-objective-c/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147576942-oc-objective-c/</guid><description>面向对象与面向过程设计思想 面向过程 是以过程（步骤）为中心的编程方式。 把一件要做的事，从头到尾， 按照步骤拆成一系列操作 ，一步步完成。 重点是 怎么做，顺序是什么 面向过程的特点 注重 流程控制 （先做什么、后做什么） 编程思维是： “先干这个，再干那个” 主要用 函数 组</description><pubDate>Sun, 31 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;面向对象与面向过程设计思想&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;面向过程&lt;/strong&gt;是以过程（步骤）为中心的编程方式。&lt;/p&gt;
&lt;p&gt;把一件要做的事，从头到尾，&lt;strong&gt;按照步骤拆成一系列操作&lt;/strong&gt;，一步步完成。&lt;/p&gt;
&lt;p&gt;重点是&lt;strong&gt;怎么做，顺序是什么&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;面向过程的特点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注重&lt;strong&gt;流程控制&lt;/strong&gt;（先做什么、后做什么）&lt;/p&gt;
&lt;p&gt;编程思维是：**“先干这个，再干那个” **&lt;/p&gt;
&lt;p&gt;主要用&lt;strong&gt;函数&lt;/strong&gt;组织代码&lt;/p&gt;
&lt;p&gt;代码容易从上到下直接执行&lt;/p&gt;
&lt;p&gt;小程序简单直接，速度快&lt;/p&gt;
&lt;p&gt;面向对象以对象（数据 + 行为）为中心的编程方式&lt;/p&gt;
&lt;p&gt;把现实世界的实体抽象为程序中的对象，每个对象既包含数据（属性），又有能做的事（方法），强调封装 继承 多态&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;面向对象的特点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注重&lt;strong&gt;数据和行为的封装&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;编程思维是：&lt;strong&gt;“谁负责做这件事”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;代码模块化，便于维护和扩展&lt;/li&gt;
&lt;li&gt;适合&lt;strong&gt;大型、复杂系统&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;类与对象的概念&lt;/h2&gt;
&lt;p&gt;类是对同一类食物高度的抽象，类中定义了这一类对象所应具有的静态属性（属性）和动态属性（方法）&lt;/p&gt;
&lt;p&gt;对象是类的一个实例，是一个具体事物&lt;/p&gt;
&lt;p&gt;类与对象是抽象与具体的关系&lt;/p&gt;
&lt;p&gt;类其实就是一种数据类型，他的变量就是 对象&lt;/p&gt;
&lt;h3&gt;类与类之间的关系&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;继承（is-a） 一个类是另一个类的特殊版 “学生是人” 2. 组合 / 聚合（has-a） 一个类拥有另一个类作为成员 “学校有学生” 3. 依赖（use-a） 一个类短暂使用另一个类 “老师使用教案” 4. 关联（link-a） 类之间长期联系 “医生关联病人”&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;OC与面向对象&lt;/h2&gt;
&lt;p&gt;对象是oc程序的核心&lt;/p&gt;
&lt;p&gt;类是用来创造同一类型的对象的模板，在一个类中定义了该类对象所具有的成员变量和方法&lt;/p&gt;
&lt;p&gt;类可以看成是静态属性（实例变量）和动态属性（方法）的结合体&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;OC类的声明和实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@interface NewClassName: ParentClassName
{
    实例变量;
    ...
}
方法的声明;
    ...
@end

@implementation NewClassName
方法的实现
{
    //code
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@interface Person : NSObject
{
    //实例变量声明
    int identify;
    int age;
}
//方法声明
- (id) initWithAge:(int) _age identify: (int) _identify;
- (int) getIdentify;
- (int) getAge;
- (void) setAge:(int) _age;
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类的声明放在 “类名.h”文件&lt;/p&gt;
&lt;h3&gt;实例变量&lt;/h3&gt;
&lt;p&gt;实例变量可以使用oc语言任何一种数据类型（包括基本类型和指针类型）&lt;/p&gt;
&lt;p&gt;在声明实例变量的时候不能为其初始化，系统默认会初始化&lt;/p&gt;
&lt;p&gt;实例变量的默认作用域是整个类&lt;/p&gt;
&lt;h3&gt;OC的方法声明&lt;/h3&gt;
&lt;p&gt;oc中方法和其他语言一样，是一段用来完成特定功能的代码片段：声明格式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/1cdff7c30b1249b6a07f55f65fefee6b.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- (void) setAge:(int) _age;
- (int) getAge;
- (id) initWithAge:(int) _age identify:(int) _identify
+ (Person *) shareRerson;
### OC中方法的调用


oc语言中采用特定的语言调用类或者实例（对象）的方法称为发送消息或方法调用


oc中方法的调用有两种：


【类名或对象名     方法名】


【对象名.方法名】 


```objective-c
[ClassOrInstance method];

[ClassOrInstace method:arg1];

[ClassOrInstance method1:arg1 method2:arg2];

[[ClassOrInstance method:arg1] otherMethod];
### 类的实现


```objective-c
@implementation Person

-(id) initWithAge:(int)_age identify:(int)_identify:(int) _identify
{
    if(self = [super init])
    {
        age = _age;
        identify = _identify;
    }
    return self;
}
- (int) getIdentify
{
    return identify;
}
-(int) getAge
{
    return age;
}
-(void) setAge:(int)_age
{
    age = _age;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKPerson.h&quot;

@implementation FKPerson
{
    int _testAttr;
}

-(void) setName:(NSString*) n andAge: (int) a
{
    _name = n;
    _age = a;
}

-(void) say:(NSString *) content
{
    NSLog(@&quot;%@&quot; , content);
}

-(NSString*) info
{
    [self test];
    return [NSString stringWithFormat:
            @&quot;我是一个人，名字为：%@，年龄为%d。&quot; , _name, _age];
}

-(void) test
{
    NSLog(@&quot;FKPersonw类的类方法，通过类名调用&quot;);
}

+ (void) foo
{
    NSLog(@&quot;FKPerson的类的类方法，通过类名调用&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKPerson : NSObject
{
    //下面定义两个成员变量
    NSString* _name;
    int _age;
}
//下面定义了一个setName:andAge:方法
-(void) setName:(NSString*) name andAge: (int) age;
//下面定义了一个say：方法，但不提供实现
-(void) say: (NSString *) content;
//下面定义了一个不带形参的info方法
-(NSString*) info;
//定义了一个类方法
+ (void) foo;
@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OC面向对象的基本概念——指针&lt;/h2&gt;
&lt;p&gt;oc语言中除基本数据类型意外的变量类型都称为指针类型&lt;/p&gt;
&lt;p&gt;oc中的对象是通过指针对其操作的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 声明了一个NSString类型的指针变量，但他并没有指向任何一个对象

NSString *s

//使用alloc的方法创建了一个NSString类型的对象并用s 指向他，以后可以通过s完成对其的操作

s = [[NSString alloc] initWithString:@&quot;Hello iPhine4S&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类是静态的概念&lt;/p&gt;
&lt;p&gt;对象是alloc出来的，存放在堆区，类的每个实例变量在不同的对象中都有不同的值（静态变量除外）&lt;/p&gt;
&lt;p&gt;方法也只是在被调用的时候，程序运行的是才占用内存&lt;/p&gt;
&lt;h3&gt;对象的创建和使用&lt;/h3&gt;
&lt;p&gt;oc中对象通过指针来声明。如： ClassA *object;&lt;/p&gt;
&lt;p&gt;oc中对象的创建，使用alloc来创建一个对象。编译器会对object对象分配一块可用的内存地址。然后需要对对象进行初始化即调用init方法，这样这个对象才可以使用。如：&lt;/p&gt;
&lt;p&gt;Persom* person = [Person alloc];&lt;/p&gt;
&lt;p&gt;person = [person init];&lt;/p&gt;
&lt;p&gt;ClassA* object = [[ClassA alloc] init];&lt;/p&gt;
&lt;h3&gt;对象的初始化&lt;/h3&gt;
&lt;p&gt;对象必须先创建，然后初始化，才能使用。&lt;/p&gt;
&lt;p&gt;NSObject *object = [[NSObject alloc] init];&lt;/p&gt;
&lt;h3&gt;self关键字&lt;/h3&gt;
&lt;p&gt;self关键字总是指向该方法的调用者（对象或类），但self出现在实例方法中时，self代表调用该方法的对象；self出现在类方法时，self代表调用该方法的类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e2c26ea2dcc6424ba8c8cb9a898ca0dd.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样的做法当然不够好， 按照语法的话，我们需要创建一个对象，调用这个对象的方法，才可实现我们的run方法。但是在实际设计中，我们发现我们其实做了一件毫无意义的事情，我们实际上无需创建一个新的对象去执行这一个操作，我们可以通过&lt;strong&gt;self&lt;/strong&gt;关键字可以获得调用该方法的对象。&lt;/p&gt;
&lt;p&gt;self总是代表当前类的对象，当出现在方法体时，它所代表的对象是不确定的，但是类型是确定的，它所代表的对象只能是当前类的实例。当我们在主函数中调用方法时，他的对象就确定了。谁调用这个方法，self就代表谁。&lt;/p&gt;
&lt;p&gt;但是self却不能出现在类方法中，因为self所代表的是一个实例，而不是一个类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@interface Myperson : NSObject {
    NSString* _name;
    int _age;
}
- (void) say: (NSString*) content andAge : (int) age;
- (void) whatSay;
+ (void) foo;
- (void) rap;
- (void) dance;
@end
@implementation Myperson {

}
- (void) say: (NSString*) content andAge : (int) age {
    _name = content;
    _age = age;
}
- (void) whatSay {
    NSLog(@&quot;我叫%@，今年%d&quot;, _name, _age);
}
- (void) rap {
    NSLog(@&quot;man&quot;);
}
- (void) dance {
    [self rap];
    NSLog(@&quot;what can i say&quot;);
}
+ (void) foo {
    NSLog(@&quot;我是老大&quot;);
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当self 作为对象或类本身的默认引用使用时，程序可以像访问普通指针变量一样访问这个self引用，甚至可以把self当成普通方法的返回值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/df58115c560441a8a9b6a5e272f79b21.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;id类型&lt;/h3&gt;
&lt;p&gt;Oc 提供了一个 id 类型，这个id类型可以代表所有对象类型。任意类的对象都能赋值给id类型变量。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;id类型的变量来调用方法时，oc会执行动态绑定，跟踪判断该对象所属的类，并在运行时确定需要动态调用的方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/4ba6d5f81ac04011a8999d4d5050eacb.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147576942&quot;&gt;【OC】Objective - C初探之面向对象&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【OC】OC的实例对象，类对象，元类对象</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147772708-oc-oc-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147772708-oc-oc-/</guid><description>OC的实例对象，类对象，元类对象 在 Objective C 中，一切都是对象 —— 包括“类”本身。它背后的运行机制依赖于一个强大的 元类（metaclass）系统 。 实例对象 类对象 元类对象 是 Objective C Runtime 的三种核心对象，它们构成了类与对象的</description><pubDate>Sun, 31 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;OC的实例对象，类对象，元类对象&lt;/h2&gt;
&lt;p&gt;在 Objective-C 中，一切都是对象 —— 包括“类”本身。它背后的运行机制依赖于一个强大的&lt;strong&gt;元类（metaclass）系统&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实例对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类对象&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元类对象&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;是 Objective-C Runtime 的三种核心对象，它们构成了类与对象的运行时结构。&lt;/p&gt;
&lt;h3&gt;一、实例对象&lt;/h3&gt;
&lt;p&gt;通过 [[类名 alloc] init] 或类似方式创建的&lt;strong&gt;具体对象&lt;/strong&gt;，就是&lt;strong&gt;实例对象&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Person *p = [[Person alloc] init];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时p就是一个实例对象，拥有person类定义的所有实例变量、属性、方法等。&lt;/p&gt;
&lt;h3&gt;二、类对象&lt;/h3&gt;
&lt;p&gt;每个类在运行时只有一个类对象，它描述了这个类的结构和行为，是“类的实例”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class cls = [Person class];


//或者

object_getClass(p); //获取对象p所属的类
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;作用：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;储存类方法（+方法）&lt;/p&gt;
&lt;p&gt;存储类名、父类、方法列表、属性列表等&lt;/p&gt;
&lt;p&gt;所有实例对象共享这个类对象&lt;/p&gt;
&lt;h3&gt;三、元类对象&lt;/h3&gt;
&lt;p&gt;类对象本身也是一个对象，它是“某个元类的实例”&lt;/p&gt;
&lt;p&gt;元类描述的类对象的行为，即：类方法（+方法）实际上存储在元类对象中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class metaClass = object_getClass([Person class]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt; &lt;strong&gt;举例&lt;/strong&gt; &lt;strong&gt;本质&lt;/strong&gt; &lt;strong&gt;方法存储位置&lt;/strong&gt; 实例对象 Person *p = [[Person alloc] init] 类的一个具体对象 实例方法（- 方法） 类对象 [Person class] Person 类的唯一对象 类方法在元类中 元类对象 object_getClass([Person class]) 类对象的“类” 存储类方法（+ 方法）&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147772708&quot;&gt;【OC】OC的实例对象，类对象，元类对象&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【iOS】MVC架构</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-151052260-ios-mvc/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-151052260-ios-mvc/</guid><description>目录 1 前言 2 MVC简单介绍 胖Model 和瘦Model 3 MVC的问题 1. 视图控制器过于臃肿 2. view和controller的边界很模糊 为了解决这些缺点，衍生出了许多改进的架构模式： &quot;MVC&quot;，即Model（模型），View（视图），Controller</description><pubDate>Sun, 31 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#1%20%E5%89%8D%E8%A8%80&quot;&gt;1 前言&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#2%20MVC%E7%AE%80%E5%8D%95%E4%BB%8B%E7%BB%8D&quot;&gt;2 MVC简单介绍&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E8%83%96Model%20%E5%92%8C%E7%98%A6Model&quot;&gt;胖Model 和瘦Model&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#3%20MVC%E7%9A%84%E9%97%AE%E9%A2%98%C2%A0&quot;&gt;3 MVC的问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#1.%20%E8%A7%86%E5%9B%BE%E6%8E%A7%E5%88%B6%E5%99%A8%E8%BF%87%E4%BA%8E%E8%87%83%E8%82%BF&quot;&gt;1. 视图控制器过于臃肿&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#2.%20view%E5%92%8Ccontroller%E7%9A%84%E8%BE%B9%E7%95%8C%E5%BE%88%E6%A8%A1%E7%B3%8A&quot;&gt;2. view和controller的边界很模糊&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#%E4%B8%BA%E4%BA%86%E8%A7%A3%E5%86%B3%E8%BF%99%E4%BA%9B%E7%BC%BA%E7%82%B9%EF%BC%8C%E8%A1%8D%E7%94%9F%E5%87%BA%E4%BA%86%E8%AE%B8%E5%A4%9A%E6%94%B9%E8%BF%9B%E7%9A%84%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F%EF%BC%9A&quot;&gt;为了解决这些缺点，衍生出了许多改进的架构模式：&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&quot;MVC&quot;，即Model（模型），View（视图），Controller（控制器）。&lt;/p&gt;
&lt;p&gt;如何设计一个程序的结构，这是一门专门的学问，叫做&quot;&lt;strong&gt;架构模式&lt;/strong&gt;&quot;（architectural pattern），属于编程的方法论。&lt;strong&gt;MVC 模式就是架构模式的一种&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;一、 前言&lt;/h2&gt;
&lt;p&gt;首先，作为一名合格的程序猿，我们在写代码的时候都应该追求&lt;strong&gt;美&lt;/strong&gt;。比如在敲每一行代码的时候，都应该注重&lt;strong&gt;代码规范&lt;/strong&gt;，写出一份看得舒服，让别人也看得懂的代码，这样也能提高效率；比如在设计代码的时候，应该追求的是我怎样才能写出一个好的&lt;strong&gt;架构&lt;/strong&gt;（App 架构类似于现代建筑的脚手架或者是地基，一旦确定，剩下的工作就是在现成的App里添砖加瓦），让我的代码&lt;strong&gt;模块化，分工明确，从而提高我的工作效率&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;总结一下什么是好的架构：&lt;strong&gt;高内聚，低耦合。 代码均摊，易于扩展，具有易用性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以说，&lt;strong&gt;代码规范，架构模式&lt;/strong&gt;，&lt;strong&gt;设计模式&lt;/strong&gt;是检验一个程序猿水平的重要参考。&lt;/p&gt;
&lt;p&gt;但是，我们创建一个控件，设置这个控件的样子，设置这个控件的交互方法，展示这个控件，都需要一定的代码量，控件少的时候还好，看得过去，但是控件一多，像下面这个App 的界面，各种控件就多起来了，这个时候如果还把他们都堆在一个ViewController 里面就不合适了，要改bug，要添加控件就会变得非常麻烦。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而MVC架构，就是用来解决这些问题的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;二、 MVC简单介绍&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;MVC模式的基本思想：将应用程序分成三个部分，使它们各自负责不同的功能。MVC 是一种软件架构模式，它的目的是将应用程序的数据、用户界面和控制逻辑分离开来，以便各个部分可以独立地演变和修改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;M：Model（模型）负责处理数据，以及处理部分的业务逻辑&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通俗来说，&lt;strong&gt;就是你的程序是什么&lt;/strong&gt;，就是你的程序将要实现的功能，或者是它所能干的事情。也就是微信消息列表里的人名字，信息内容，头像，是否屏蔽该人的消息等等数据，可以认为，Model 里面装满了这个程序的各种数据，它负责处理数据、以及处理部分的业务逻辑。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;V：View（视图）负责数据的展示和事件捕捉&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通俗来说，&lt;strong&gt;在屏幕上你所看到的&lt;/strong&gt;，这里有一个UITableView，TableView 里面有UILabel，UIImageView，你在屏幕上看到的组件，都可以归类为View。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C：Controller / ViewController / VC（控制器）负责协调Model 和 View，处理大部分逻辑&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它将数据从Model 层传送到View 层并展示出来，同时将View 层的交互传到Model 层以改变数据。&lt;strong&gt;大部分的逻辑操作（点击Button就是一种逻辑）都应该交由VC完成。（有少部分的逻辑处理交由Model 完成，这是下文中我要提到的胖Model 和瘦Model）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通俗来说，&lt;strong&gt;就是如何使你的模型呈现给用户&lt;/strong&gt;，比如让View 上呈现Model 的数据，就是Controller 的工作。所以你可以把Controller 看成是连接Model 和View 的桥梁。&lt;/p&gt;
&lt;p&gt;用户点击 View–&amp;gt; 视图响应事件 --&amp;gt;通过代理传递事件到Controller–&amp;gt;发起&lt;a href=&quot;https://so.csdn.net/so/search?q=%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82&amp;amp;spm=1001.2101.3001.7020&quot;&gt;网络请求&lt;/a&gt;更新Model—&amp;gt;Model处理完数据–&amp;gt;代理或通知给Controller–&amp;gt;改变视图样式–&amp;gt;完成&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/f6f20ec663234c8b8065d091e63a02ff.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;胖Model 和瘦Model&lt;/h3&gt;
&lt;p&gt;我们刚刚说了Model有时候不但是数据源，有时候也会处理部分的业务逻辑。这种情况就是当Model 里面有很多原始数据，但View希望展示的数据是经过加工的数据，那么这个加工的过程到底应该放在VC里面还是Model里面呢，来举个栗子说明：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;View想展示今天的日期，Model拿到的原始数据是20221124，但是View希望展示的数据是2022年11月24日。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以不难想象，&lt;code&gt;20221124&lt;/code&gt;这串数字需要经过一定的加工，才会变成我们想要的&lt;code&gt;2022年11月24日&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是这个加工过程应该放在哪里呢？是VC还是Model里面？&lt;/p&gt;
&lt;p&gt;开发者们也思考过这个问题，因此产生了&lt;strong&gt;胖Model&lt;/strong&gt; &lt;strong&gt;（Fat Model）&lt;/strong&gt; 和&lt;strong&gt;瘦Model&lt;/strong&gt; &lt;strong&gt;（Thin Model）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;胖Model对应的是瘦的VC（Skinny Controller），在Model 中&lt;/strong&gt; &lt;strong&gt;对数据进行处理&lt;/strong&gt; &lt;strong&gt;，让Controller可以直接使用经过处理后的数据。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;瘦Model对应的是胖的VC（Fat Controller），Model中的数据&lt;/strong&gt; &lt;strong&gt;不进行任何处理或修改&lt;/strong&gt; &lt;strong&gt;，原封不动的把服务器返回内容发送给Controller。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还是用刚刚的栗子说明，&lt;strong&gt;胖Model&lt;/strong&gt;对应的是把这个加工过程放在Model里面（所以Model胖了），相反，瘦Model就是把加工过程放在VC里面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/83e7561240254390926b7e8970966a9f.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;三、 MVC的问题&lt;/h2&gt;
&lt;h3&gt;1. 视图控制器过于臃肿&lt;/h3&gt;
&lt;p&gt;在实际开发中，VC常常承担过多职责倒置代码量巨大，可读性差&lt;/p&gt;
&lt;h3&gt;2. view和controller的边界很模糊&lt;/h3&gt;
&lt;p&gt;这种模糊的边界使得开发者很容易弄混代码应该放置的位置，最终导致了第一条的代码臃肿问题&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4&gt;为了解决这些缺点，衍生出了许多改进的架构模式： - ​&lt;strong&gt;MVVM (Model-View-ViewModel)​&lt;/strong&gt;​ ​&lt;strong&gt;核心思想&lt;/strong&gt;​： 引入一个 &lt;code&gt;ViewModel&lt;/code&gt; 层，专门负责从 &lt;code&gt;Model&lt;/code&gt; 获取数据并进行处理，转换成 &lt;code&gt;View&lt;/code&gt; 可以直接显示的数据。它通过数据绑定（Data Binding）机制（如 KVO、Notification、RAC 或 Combine）通知 &lt;code&gt;View&lt;/code&gt; 更新。 - ​&lt;strong&gt;优点&lt;/strong&gt;​： 极大地减轻了 &lt;code&gt;ViewController&lt;/code&gt; 的负担，使其只负责视图绑定和简单的逻辑转发。&lt;code&gt;ViewModel&lt;/code&gt; 不依赖 &lt;code&gt;UIKit&lt;/code&gt;，易于测试。 - ​&lt;strong&gt;MVP (Model-View-Presenter)​&lt;/strong&gt;​ ​&lt;strong&gt;核心思想&lt;/strong&gt;​： &lt;code&gt;Controller&lt;/code&gt;/&lt;code&gt;View&lt;/code&gt; 的角色被弱化，由一个 &lt;code&gt;Presenter&lt;/code&gt; 来承担绝大部分业务逻辑。&lt;code&gt;View&lt;/code&gt; 通过协议（Protocol）与 &lt;code&gt;Presenter&lt;/code&gt; 通信，实现了更好的解耦。 - ​&lt;strong&gt;优点&lt;/strong&gt;​： 视图和逻辑完全分离，可测试性极强。 - ​&lt;strong&gt;VIPER (View-Interactor-Presenter-Entity-Router)​&lt;/strong&gt;​ ​&lt;strong&gt;核心思想&lt;/strong&gt;​： 将职责划分得更加细致，每个字母代表一个明确的职责模块。它更适用于超大型项目，追求极致的可测试性和模块化。 - ​&lt;strong&gt;缺点&lt;/strong&gt;​： 引入的模板代码较多，学习曲线较陡峭，对于中小型项目可能显得“杀鸡用牛刀”。&lt;/h4&gt;
&lt;/blockquote&gt;
&lt;p&gt;在接下来的项目如计算器中，我会开始尝试使用MVC架构。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jianshu.com/p/edd8d8a49d06?utm_campaign=maleskine&amp;amp;utm_content=note&amp;amp;utm_medium=seo_notes&quot;&gt;你真的了解MVC吗？ - 简书&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/7098623916452610078?searchId=20250831221503BC226F5D134DA78BCF89&quot;&gt;https://juejin.cn/post/7098623916452610078?searchId=20250831221503BC226F5D134DA78BCF89&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/151052260&quot;&gt;【iOS】MVC架构&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Oc语言学习 —— Foundation框架总结</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-148030438-oc-foundation/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-148030438-oc-foundation/</guid><description>1、NSString类 我们对一个NSString对象赋值的方法是直接将字符串常量赋给对象，例如： NSString str = @&quot;hello&quot;; 因为我们的NSString是不可变的，所以我们只能通过一些方法来在我们原来的字符串后面追加或初始化我们的字符串来间接修改我们的对象</description><pubDate>Sun, 18 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1、NSString类&lt;/h2&gt;
&lt;p&gt;我们对一个NSString对象赋值的方法是直接将字符串常量赋给对象，例如：&lt;code&gt;NSString *str = @&quot;hello&quot;;&lt;/code&gt;
因为我们的NSString是不可变的，所以我们只能通过一些方法来在我们原来的字符串后面追加或初始化我们的字符串来间接修改我们的对象，例如：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9445a3ce72f74d9d84152fca72575841.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这两种方法都是
对象不改变
，将新生成的字符串重新赋值给str指针变量&lt;/p&gt;
&lt;h2&gt;2、NSMutableString类&lt;/h2&gt;
&lt;p&gt;我们这个类与上面不同，他的字符序列可以改变，我们通过一些方法对其调用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0855c48130b4418680f5e3771687df28.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意，我们的a在初始化时不可使用NSString式的方法。&lt;/p&gt;
&lt;p&gt;在这里我们可以注意一下我们的的可变与不可变的区别：我们对我们的NSString对象进行赋值时，&lt;strong&gt;必须要将调用后的方法返回的值重新赋给我们的对象&lt;/strong&gt;，例如a = [a stringByAppendingString:@&quot;iii&quot;];，而我们的NSMutableString就不用重新赋值，可以直接修改。 两个类方法的不同就是&lt;strong&gt;一个有stringby前缀，另一个没有。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;3、日期与时间&lt;/h2&gt;
&lt;p&gt;oc为处理这些提供了NSDate，NSCalender对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
//  main.m
//  日期与时间
//
//  Created by 吴桐 on 2025/5/18.
//

#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDate *data1 = [NSDate date];
        NSDate *data2 = [[NSDate alloc] initWithTimeIntervalSinceNow:3600 * 24];
        //获得后一天的时间
        NSLog(@&quot;%@&quot;, data1);
        NSLog(@&quot;%@&quot;, data2);
        NSDate *data3 = [[NSDate alloc] initWithTimeIntervalSinceNow:-3 * 3600 * 24];
        //获得三天前的日期
        NSLog(@&quot;%@&quot;, data3);
//        NSDate *data4 = [[NSDate alloc] initWithTimeIntervalSince1970:3600 * 24 * 366 * 30];
//        NSLog(@&quot;%@&quot;, data4);//获得1970一月一日之后30年的日期
        NSLocale* cn = [NSLocale currentLocale];
        // 获取NSdate
        NSLog(@&quot;%@&quot;, [data1 descriptionWithLocale:cn]);
        //将data1赋给cn，获得当前地区的时间
        //获取系统当前的locale
//        NSLocale *cn1 = [NSLocale currentLocale];
//        NSLog(@&quot;%@&quot;, cn1);
        //获取NSdata在当前locale下对应的字符串
        NSLog(@&quot;%@&quot;, [data1 descriptionWithLocale:cn]);
        //获取两个之间较早的
        NSLog(@&quot;%@&quot;, [data1 earlierDate:data2]);

        //compare 方法返回NScomparisonResult枚举值
       // 枚举包含
        //NSOrderedAscending,NSOrderedSame, NSOrderedDescending
        // 分别代表了 调用compare 的日期位于被比较日期的之前 相同 之后
        switch ([data1 compare:data2]) {
            case NSOrderedSame:
                NSLog(@&quot;date1 == date3&quot;);
            case NSOrderedAscending:
                NSLog(@&quot;data1 在 data3 之前&quot;);
            case NSOrderedDescending:
                NSLog(@&quot;data1 在 data3 之后&quot;);
        }
        NSLog(@&quot;时间差为%g&quot;, [data1 timeIntervalSinceDate:data3]);
        //interval的意思是间隔，上述是data1 与 data3 的时间间隔
        NSLog(@&quot;差为%g&quot;, [data2 timeIntervalSinceNow]);
        //与现在的时间间隔


    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/de4e71cd02f741d780d4e4b48b045f05.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4、日期格式器&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/8e246fea3ba842bba3dd0ac6c8f822a4.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/250b39f24cfa450aaf5886413c39f2e4.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;5、定时器&lt;/h2&gt;
&lt;h2&gt;6、对象复制&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147851035?fromshare=blogdetail&amp;amp;sharetype=blogdetail&amp;amp;sharerId=147851035&amp;amp;sharerefer=PC&amp;amp;sharesource=2402_86720949&amp;amp;sharefrom=from_link&quot;&gt;OC语言学习——对象复制-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;7、array&lt;/h2&gt;
&lt;p&gt;这里我们需要着重记忆的是我们向数组内传参数的方法：arrayWithObjects&lt;/p&gt;
&lt;p&gt;然后我们需要看一下如何遍历集合类的元素&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/64693fc4fdd743929901b1c03a40cfd2.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;8、set&lt;/h2&gt;
&lt;p&gt;set中元素没有固定顺序，自动去重，查找比array快。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e7b66d26e71d4e23a79148cf584eb087.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们在使用set时需要注意的是我们一般会对hash进行重写，因为我们集合中判断2个元素相等的条件是：方法isEqual返回yes与两个对象的hash方法返回值也相等，set才会判断这两个对象相等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e8a02550bcda4bd9820f073381e0bc1b.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;9、dictionary&lt;/h2&gt;
&lt;p&gt;Dictionary是一种&lt;strong&gt;键值对结构&lt;/strong&gt;的集合。每个元素是一个key-value对，&lt;strong&gt;key必须唯一&lt;/strong&gt;。与数组不同，字典中**通过key查找value，**而不是下标&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/148030438&quot;&gt;Oc语言学习 —— Foundation框架总结&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>NSString的三种实现方式</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-148049357-nsstring/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-148049357-nsstring/</guid><description>oc里的NSString有三种实现方式，为 NSCFConstantString、 NSCFString、NSTaggedPointerString 1. NSCFConstantString(字面量字符串) 从字面意思上可以看出， NSCFContantString可以理解为常</description><pubDate>Sun, 18 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;oc里的NSString有三种实现方式，为_ _NSCFConstantString、__NSCFString、NSTaggedPointerString&lt;/p&gt;
&lt;h2&gt;1._ _NSCFConstantString(字面量字符串)&lt;/h2&gt;
&lt;p&gt;从字面意思上可以看出，_ _NSCFContantString可以理解为常量字符串，这种类型的字符串在编译期就确定了，也就是我们常说的字面量字符串，如 NSString *str = @“Hello, World!”，它们被存储在常量区，不可变，这种类型的字符串效率较高，因为在程序的生命周期内，
它的值和内存地址都不会改变
，即其有如下特点：&lt;/p&gt;
&lt;p&gt;内存特性：存储在&lt;strong&gt;常量区&lt;/strong&gt;，程序启动时分配，生命周期贯穿整个程序运行期间，不可修改。&lt;/p&gt;
&lt;p&gt;创建方式：&lt;strong&gt;使用字符串字面量 如 @&quot;Hello&quot;创建的字符串&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;相同的常量字符串会共享同一内存地址。&lt;/p&gt;
&lt;p&gt;无法通过release释放，是一个单例模式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString* str1 = @&quot;iOS&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;官方解释：在计算机科学中，字面量（literal）是用于表达源代码中一个固定值的表示法（notation）。几乎所有计算机编程语言都具有对基本值的字面量表示，诸如：整数、浮点数以及字符串；而有很多也对布尔类型和字符类型的值也支持字面量表示；还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。 说人话就是 代码中直接写出的固定值，是编译时可知、不可变的常量，比如数字、字符串、布尔值、数组等，具有“所见即所得”的特性。例如上述的语句，@&quot;iOS&quot;为子面量， str为变量&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2._ _NSCFString&lt;/h2&gt;
&lt;p&gt;_ _NSCFString是动态分配的不可变字符串，用使用 stringWithFormat:、initWithString: 等方法动态创建 本质上不可变，但是其具备了可变接口(__NSCFString 是 Core Foundation 的 CFMutableStringRef 和 Foundation 的 NSMutableString 的桥接类型),简要来说,即：NSMutableString 底层也是 __NSCFString，只是它具备“可变接口”，而NSString没有&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;即使两个对象的内容相同，它们在堆上的内存地址也是不同的。每个对象都在独立的内存空间中存储，具有自己的地址。这意味着通过不同的对象引用访问这两个对象时，实际上访问的是不同的内存地址。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;该底层下的不可变声明:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString *str2 = [NSString stringWithFormat:@&quot;hello %d&quot;, 123];
NSLog(@&quot;%@&quot;, [str2 class]); // 输出：__NSCFString
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可变声明如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSMutableString *mStr = [NSMutableString stringWithString:@&quot;abc&quot;];
NSLog(@&quot;%@&quot;, [mStr class]); // 输出：__NSCFString
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.NSTaggedPointerString&lt;/h2&gt;
&lt;p&gt;在 64 位系统上，当字符串的内容较短（7 个字节以内）时，会使用这种类型来保存字符串。这是一种优化存储的手段，因为它将字符串内容直接保存在指针中，而不是在堆或者栈中创建一个实际的对象，从而节省了内存空间。但是这种方式只适用于较短的字符串。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NSString *str2 = [NSString stringWithFormat:&quot;%c%c%c&quot;, &apos;i&apos;, &apos;O&apos;, &apos;S&apos;];
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;TaggedPointer的意思是标签指针，这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中，因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。从他的引用计数可以看出，这货也是一个释放不掉的单例常量对象。在运行时根据实际情况创建&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e9992c17f8da4ee18d6d144baa8b9331.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;附上实例一个⬆️&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/148049357&quot;&gt;NSString的三种实现方式&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>OC语言学习——Foundation框架（下）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147879235-ocfoundation-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147879235-ocfoundation-/</guid><description>四、Objective — C 集合表述 OC集合类可以用于存储数量不等的多个对象，并且可以实现常用的数据结构，例如栈和队列等，除此以外，OC集合还可以用来保存具有映射关系的关联数组。 OC的集合大致上可以分为三种体系： NSArray 代表有序、可重复的集合，很像一个数组 NS</description><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;四、Objective — C 集合表述&lt;/h2&gt;
&lt;p&gt;OC集合类可以用于存储数量不等的多个对象，并且可以实现常用的数据结构，例如栈和队列等，除此以外，OC集合还可以用来保存具有映射关系的关联数组。&lt;/p&gt;
&lt;p&gt;OC的集合大致上可以分为三种体系：&lt;/p&gt;
&lt;p&gt;NSArray                                代表有序、可重复的集合，很像一个数组&lt;/p&gt;
&lt;p&gt;NSSet                                   代表无序、不可重复的集合&lt;/p&gt;
&lt;p&gt;NSDictionary                       代表具有映射关系的集合&lt;/p&gt;
&lt;p&gt;在实际编程里，面向的是NSArray（及其子类NSMutableArray）、NSSet（及其子类NSMutableSet）、NSDictionary（及其子类NSMutableDictionary）编程，程序创建的也可能是它们的子类的实例。&lt;/p&gt;
&lt;p&gt;集合类和数组不一样，数组保存的元素既可以是基本类型的值，也可以是对象（实际上是对象的指针变量）；而集合里只能保存对象（实际上是对象的指针变量）。&lt;/p&gt;
&lt;p&gt;OC集合中，NSSet集合类似于一个罐子，把一个对象添加到NSSet集合时，NSSet无法记住添加这个元素的顺序，因此NSSet的元素不可以重复。且访问其元素，只能根据元素本身来访问。&lt;/p&gt;
&lt;p&gt;NSArray类似于一个数组，它可以记住每次添加元素的顺序，因此它的元素可以重复，且NSMutableArray的长度可变。访问其中的元素，只需要根据元素的索引来访问。&lt;/p&gt;
&lt;p&gt;NSDictionary集合也像一个罐子，只是它里面的每一项数据都由两个值组成。访问其中的元素，可以根据每项元素的key值来访问其value。&lt;/p&gt;
&lt;h2&gt;五、数组（NSArray和NSMutableArray）&lt;/h2&gt;
&lt;h3&gt;5.1 NSArray的功能与用法&lt;/h3&gt;
&lt;p&gt;NSArray分别提供了类方法和实例方法来创建NSArray，两种创建方式要传入的参数基本相似，只是类方法由array开头，实例方法以init开头。&lt;/p&gt;
&lt;p&gt;创建NSArray对象的几个常见方法：&lt;/p&gt;
&lt;p&gt;array                                                                                     创建一个不包含任何元素的空NSArray&lt;/p&gt;
&lt;p&gt;arrayWithContentsOfFile：/initWithContentsOfFile：    读取文件内容来创建NSArray&lt;/p&gt;
&lt;p&gt;arrayWithObject：  /initWithObject：                               创建只包含指定元素的NSArray&lt;/p&gt;
&lt;p&gt;arrayWithObjects：/initWithObjects：                              创建包含指定的n个元素的NSArray&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //NSArray集合
        NSArray *arr = [NSArray arrayWithObjects:@&quot;疯狂iOS讲义&quot;,@&quot;疯狂111&quot;,@&quot;疯狂222&quot;,@&quot;疯狂333&quot;,@&quot;疯狂444&quot;, nil];
        NSLog(@&quot;第一个元素是：%@&quot;,[arr objectAtIndex: 0]);
        NSLog(@&quot;索引为1的元素：%@&quot;,[arr objectAtIndex: 1]);
        NSLog(@&quot;最后一个元素：%@&quot;,[arr lastObject]);

        //获取索引从2到5的元素组成的新集合
        NSArray *arr1 = [arr objectsAtIndexes: [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(2, 3)]];
        NSLog(@&quot;%@&quot;,arr1);

        //获取元素在集合中的位置
        NSLog(@&quot;疯狂111的位置为：%ld&quot;,[arr indexOfObject: @&quot;疯狂111&quot;]);
        //获取元素在集合指定范围中的位置
        NSLog(@&quot;疯狂111的位置为：%ld&quot;,[arr indexOfObject: @&quot;疯狂111&quot; inRange:NSMakeRange(2, 3)]);

        //向数组的末尾追加元素
        //原arr的本身没有变，只是将新返回的NSArray赋给arr
        arr = [arr arrayByAddingObject:@&quot;晓美焰&quot;];//追加单个元素
        arr = [arr arrayByAddingObjectsFromArray:[NSArray arrayWithObjects: @&quot;鹿目圆&quot;,@&quot;美树沙耶香&quot;, nil]];//将另一个数组中所有元素追加到原数组后面
        for (int i = 0; i &amp;lt; arr.count; i++) {
            NSLog(@&quot;%@&quot;,[arr objectAtIndex: i]);//也可简写为：NSLog(@&quot;%@&quot;,[array objectAtIndex: i]);
        }
        //获取array数组中索引为5到8的所有元素
        NSArray *arr2 = [arr subarrayWithRange: NSMakeRange(5, 3)];
        //将NSArray集合的元素写入文件
        [arr2 writeToFile: @&quot;myFile.txt&quot; atomically: YES];

        for (int j = 0; j &amp;lt; 8; j++) { //也可以用下标法来访问元素
            NSLog(@&quot;%@&quot;,arr[j]);
        }

    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/cbb5eb07ddb2458d8c3007da8c6188fe.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e6b2948f3d8d4cc48504917491acf4cc.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在上面的代码中，传入集合的元素中的最后一个是nil，代表NSArray元素结束，其实这个nil元素并不会存入NSArray集合中。&lt;/p&gt;
&lt;p&gt;上面代码还用了一个NSIndexSet集合，这个集合和NSSet的功能基本相似，区别只是NSIndexSet集合主要用于保存索引值，因此它的集合都是NSUInteger对象。&lt;/p&gt;
&lt;p&gt;在iOS 5.0以上的版本可以直接用下标法来访问元素，以下两个代码作用是相同的：&lt;/p&gt;
&lt;p&gt;[array objectAtIndex: i];&lt;/p&gt;
&lt;p&gt;array[i];&lt;/p&gt;
&lt;h4&gt;5.1.1 NSArray 判断制定元素位置的标志&lt;/h4&gt;
&lt;p&gt;NSSArray判断指定元素位置的标准只有一条：只有某个集合元素与被查找的元素通过isEqual：方法比较返回YES，即可认为该NSArray集合包含该元素，并不需要两个元素是同一个元素。&lt;/p&gt;
&lt;p&gt;下面用代码来证实NSArray的比较机制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKUser : NSObject

@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *pass;

- (id) initWithName: (NSString*) aName pass: (NSString*) aPass;
- (void) say: (NSString*) content;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKUser.h&quot;

@implementation FKUser

@synthesize name;
@synthesize pass;

- (id) initWithName:(NSString *)aName pass:(NSString *)aPass {
    if (self = [super init]) {
        name = aName;
        pass = aPass;
    }
    return self;
}
- (void) say: (NSString*) content {
    NSLog(@&quot;%@说：%@&quot;,self.name,content);
}
- (BOOL) isEqual:(id)other {
    if (self == other) {
        return YES;
    }
    if ([other class] == FKUser.class) {
        FKUser *target = (FKUser*)other;
        return [self.name isEqualToString: target.name] &amp;amp;&amp;amp; [self.pass isEqualToString: target.pass];
    }
    return NO;
}

//为了直接看到FKUser的内部状态，因此改写了description方法
- (NSString*) description {
    return [NSString stringWithFormat:@&quot;&amp;lt;FKUser[name = %@,pass = %@&amp;gt;&quot;,self.name,self.pass];
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c3e645fae0fa4ec3a6081694269b8e07.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.2 对集合元素整体调用方法&lt;/h3&gt;
&lt;p&gt;对于简单的调用集合中的元素的方法，可以通过NSArray的如下两种方法：&lt;/p&gt;
&lt;p&gt;1、makeObjectsPerformSelector：依次调用元素中每个元素的指定方法，该方法需要传入一个SEL参数，用于指定调用哪种方法。&lt;/p&gt;
&lt;p&gt;2、makeObjectsPerformSelector： withObject：：依次调用NSArray集合中的每个元素的指定方法，该方法第一个SEL参数用于指定调用哪个方法；第二个参数用于调用集合元素的方法时传入参数；第三个参数用于控制是否中止迭代，如果在处理某个元素后，将第三个元素赋为YES，该方法就会中止迭代使用。&lt;/p&gt;
&lt;p&gt;如果希望对集合中的所有元素进行隐式访问，并使用集合元素来执行某一段代码，则可通过NSArray的以下方法来完成。&lt;/p&gt;
&lt;p&gt;1、enumerateObjectsUsingBlock：：遍历集合中的所有元素，并依次使用元素来执行指定的代码块。&lt;/p&gt;
&lt;p&gt;2、enumerateObjectsWithOptions： usingBlock：：遍历集合中的所有元素，并依次使用元素来执行指定的代码块。该方法可以额外传入一个参数，用于控制遍历的选项，如反向遍历。&lt;/p&gt;
&lt;p&gt;3、enumerateObjectsAtIndexes：options：usingBlock：：遍历集合中指定范围内的元素，并依次使用元素来执行指定的代码块。该方法可以传入一个选项参数，用于控制遍历的选项，如反向遍历。&lt;/p&gt;
&lt;p&gt;上面方法都必须传入一个代码块参数，该代码块必须带三个参数，前一个参数代表正在遍历的集合元素，第二个参数代表正在遍历的集合元素的索引。&lt;/p&gt;
&lt;h3&gt;5.3 对NSSArray进行排序&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数组类型&lt;/strong&gt; &lt;strong&gt;方法名&lt;/strong&gt; &lt;strong&gt;传入参数类型&lt;/strong&gt; &lt;strong&gt;是否原地修改&lt;/strong&gt; &lt;strong&gt;是否返回新数组&lt;/strong&gt; &lt;strong&gt;支持 block&lt;/strong&gt; &lt;strong&gt;支持 selector&lt;/strong&gt; &lt;strong&gt;支持函数指针&lt;/strong&gt; &lt;strong&gt;可用于自定义对象排序&lt;/strong&gt; &lt;strong&gt;常用程度&lt;/strong&gt; NSMutableArray sortUsingSelector: SEL（方法选择器） ✅ 是 ❌ 否 ❌ 否 ✅ 是 ❌ 否 ✅ 是（如 compare:） ⭐⭐⭐ NSMutableArray sortUsingComparator: NSComparator（block） ✅ 是 ❌ 否 ✅ 是 ❌ 否 ❌ 否 ✅ 是 ⭐⭐⭐⭐ NSMutableArray sortUsingFunction:context: 函数指针 + void * ✅ 是 ❌ 否 ❌ 否 ❌ 否 ✅ 是 ✅ 是 ⭐ NSArray sortedArrayUsingSelector: SEL（方法选择器） ❌ 否 ✅ 是 ❌ 否 ✅ 是 ❌ 否 ✅ 是（如 compare:） ⭐⭐⭐ NSArray sortedArrayUsingComparator: NSComparator（block） ❌ 否 ✅ 是 ✅ 是 ❌ 否 ❌ 否 ✅ 是 ⭐⭐⭐⭐ NSArray sortedArrayUsingFunction:context: 函数指针 + void * ❌ 否 ✅ 是 ❌ 否 ❌ 否 ✅ 是 ✅ 是 ⭐ NSArray / NSMutableArray sortedArrayUsingDescriptors: NSSortDescriptor数组 ❌ 否 ✅ 是 ❌ block&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

//定义一个比较函数，根据两个对象的intValue进行比较
NSInteger intSort(id num1,id num2,void *context) {
    int v1 = [num1 intValue];
    int v2 = [num2 intValue];
    if (v1 &amp;lt; v2)
        return NSOrderedAscending;
    else if (v1 &amp;gt; v2)
        return NSOrderedDescending;
    else
        return NSOrderedSame;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *array1 = [NSArray arrayWithObjects: @&quot;Objective-C&quot;,@&quot;C&quot;,@&quot;C++&quot;,@&quot;Ruby&quot;,@&quot;Perl&quot;,@&quot;Python&quot;,nil];//初始化一个元素为NSString的NSArray对象
        //使用集合的compare：方法进行排序
        array1 = [array1 sortedArrayUsingSelector: @selector(compare:)];
        NSLog(@&quot;%@&quot;,array1);

        NSArray *array2 = [NSArray arrayWithObjects: [NSNumber numberWithInt:20],[NSNumber numberWithInt:12],[NSNumber numberWithInt:-8],[NSNumber numberWithInt:50],[NSNumber numberWithInt:19],nil];//初始化一个元素为int的NSArray对象
        //使用intSort函数进行排序
        array2 = [array2 sortedArrayUsingFunction: intSort context: nil];
        NSLog(@&quot;%@&quot;,array2);

        //使用代码块对array2的元素进行排序
        NSArray *array3 = [array2 sortedArrayUsingComparator: ^(id obj1,id obj2) {
            //该代码块根据集合元素的intValue进行比较
            if ([obj1 intValue] &amp;gt; [obj2 intValue]) {
                return NSOrderedDescending;
            }
            if ([obj1 intValue] &amp;lt; [obj2 intValue]) {
                return NSOrderedAscending;
            }
            return NSOrderedSame;
        }];
        NSLog(@&quot;%@&quot;,array3);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中，我们可以看见第一种方法使用NSString自身的compare：方法进行排序。这是因为NSString自身已经实现了compare：方法，这意味着NSString对象本身就可以比较大小——NSString自身比较大小的方法是根据字符对应的编码来的。&lt;/p&gt;
&lt;p&gt;compare：方法的比较是：compare在要比较的字符串中，依次取出对应的数组元素,按ascii码值比较,如果ascii值能比较出结果了,就不往后比较。默认返回值为升序&lt;/p&gt;
&lt;p&gt;后两种方法通过调用代码块或者函数来比较大小，代码块相当于一个匿名函数，因此后面两种方式的本质是一样的，它们都可以通过自定义的比较规则来比较集合元素的大小。&lt;/p&gt;
&lt;h3&gt;5.4 使用枚举遍历器遍历NSArray集合元素&lt;/h3&gt;
&lt;p&gt;可以调用NSArray对象的如下两个方法来返回枚举器：&lt;/p&gt;
&lt;p&gt;1、objectEnumerator：返回NSArray集合的顺序枚举器。&lt;/p&gt;
&lt;p&gt;2、reverseObjectEnumerator：返回NSArray逆序枚举器。&lt;/p&gt;
&lt;p&gt;上面两个方法都返回一个NSEnumerator枚举器，该枚举器只包含如下两个方法：&lt;/p&gt;
&lt;p&gt;1、allObjects：获取被枚举集合中的所有元素。&lt;/p&gt;
&lt;p&gt;2、nextObject：获取被枚举集合中的下一个元素。&lt;/p&gt;
&lt;p&gt;借助nextObject方法即可对集合元素进行枚举：程序可采用循环不断获取nextObject方法的返回值，直到该方法的返回值为nil结束循环。&lt;/p&gt;
&lt;p&gt;用以下代码演示上述方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *array = [NSArray arrayWithObjects: @&quot;健身减脂&quot;,@&quot;郑成龙&quot;,@&quot;pp小徐&quot;,@&quot;阿娜娜自助烤肉&quot;,nil];//初始化一个NSArray集合
        //获取NSArray的顺序枚举器
        NSEnumerator *en = [array objectEnumerator];
        id object;
        while (object = [en nextObject]) {
            NSLog(@&quot;%@&quot;,object);
        }
        NSLog(@&quot;-----下面是逆序遍历------&quot;);
        //获取NSArray的逆序枚举器
        en = [array reverseObjectEnumerator];
        while (object = [en nextObject]) {
            NSLog(@&quot;%@&quot;,object);
        }
    }
    return 0;
}
### 5.5 快速枚举（for...in）


        OC提供了一种快速枚举的方法来遍历集合（包括NSArray、NSSet、NSDictionary等集合），使用快速枚举遍历集合元素的时候，无需获取集合的长度，也无需根据索引来访问集合元素，即可快速枚举自动遍历集合的每个元素。其语法格式如下： 


```objective-c
for (type variableName in collection) {
    //variableName自动迭代访问每个元素
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的语法格式中，type是集合元素的类型，variableName是一个形参名，快速枚举将自动将集合元素赋给该变量。如果使用快速枚举来遍历NSDictionary对象，快速枚举中循环计数器依次代表NSDictionary的每个key值。&lt;/p&gt;
&lt;p&gt;快速枚举的本质是一个foreach循环，foreach循环和普通循环不同的是，它无需循环条件，也无需循环迭代语句，这些部分都是由系统来完成的，foreach循环自动迭代数组的每个元素，当每个元素都被迭代一次后，foreach循环自动结束。&lt;/p&gt;
&lt;p&gt;代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray* array = [NSArray arrayWithObjects:@&quot;pp小徐&quot;,@&quot;ACAT&quot;,@&quot;KFCvivo50&quot;,nil];
        for (id object in array) {
            NSLog(@&quot;%@&quot;,object);
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/b6f5b43c95204f72967e5402e41dd9f8.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.6 可变数组NSMutableArray&lt;/h3&gt;
&lt;p&gt;NSArray代表元素不可变的集合，一旦NSArray创建成功，程序中不能向集合中添加新的元素，不能删除已有的元素，也不能替换集合元素。NSArray只是保存对象的指针，因此，NSArray只保证这些指针变量中的地址不能改变，但指针变量所指向的对象是可改变的。&lt;/p&gt;
&lt;p&gt;NSMutableArray是NSArray的子类，因此它可以当作NSArray使用。它代表一个元素可变的集合，因此它程序可以向它中增添、删除、替换元素。创建NSMutableArray时可以通过参数指定底层数组的初始容量。&lt;/p&gt;
&lt;p&gt;NSMutableArray新增了以下方法：&lt;/p&gt;
&lt;p&gt;添加几何元素：                                以为add开头&lt;/p&gt;
&lt;p&gt;删除集合元素：                                以remove开头&lt;/p&gt;
&lt;p&gt;替换几何元素中的方法：                 以replace开头&lt;/p&gt;
&lt;p&gt;对集合本身排序的方法：                 以sort开头&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

//定义一个函数，该函数用于把NSArray集合转换为字符串
//这样方便我们调试的时候看到NSArray集合中的元素
NSString *NSCollectionToString(NSArray *array) {
    NSMutableString *result = [NSMutableString stringWithString:@&quot;[&quot;];
    for (id obj in array) {
        [result appendString: [obj description]];
        [result appendString:@&quot;,&quot;];
    }
    NSUInteger len = [result length];//获取字符串长度
    [result deleteCharactersInRange:NSMakeRange(len - 1, 1)];//去掉最后一个字符
    [result appendString:@&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //初始化NSMutableArray集合
        NSMutableArray *array = [NSMutableArray arrayWithObjects:@&quot;晓美焰&quot;,@&quot;鹿目圆&quot;,@&quot;巴麻美&quot;,@&quot;美树沙耶香&quot;, nil];

        //向集合最后追加元素
        [array addObject:@&quot;佐仓杏子&quot;];
        NSLog(@&quot;追加一个元素后:%@&quot;,NSCollectionToString(array));
        [array addObjectsFromArray:[NSArray arrayWithObjects:@&quot;丘比&quot;,@&quot;仁美&quot;, nil]];
        NSLog(@&quot;最后追加两个元素后：%@&quot;,NSCollectionToString(array));

        //向集合指定位置插入元素
        [array insertObject:@&quot;蓓蓓&quot; atIndex:2];
        NSLog(@&quot;在索引为2的位置插入一个元素后：%@&quot;,NSCollectionToString(array));
        [array insertObjects:[NSArray arrayWithObjects:@&quot;吼拉姆&quot;,@&quot;馒头卡&quot;, nil] atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(3, 2)]];
        NSLog(@&quot;插入多个元素后：%@&quot;,NSCollectionToString(array));

        //删除集合中指定位置的元素
        [array removeLastObject];
        NSLog(@&quot;删除最后一个元素后：%@&quot;,NSCollectionToString(array));
        [array removeObjectAtIndex:5];
        NSLog(@&quot;删除索引为5的元素后：%@&quot;,NSCollectionToString(array));
        [array removeObjectsInRange:NSMakeRange(2, 3)];
        NSLog(@&quot;删除索引为2-5的元素后：%@&quot;,NSCollectionToString(array));

        //替换集合中指定位置的元素
        [array replaceObjectAtIndex:2 withObject:@&quot;Q币&quot;];
        NSLog(@&quot;替换索引为2处的元素后：%@&quot;,NSCollectionToString(array));
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

//定义一个函数，该函数用于把NSArray集合转换为字符串
//这样方便我们调试的时候看到NSArray集合中的元素
NSString *NSCollectionToString(NSArray *array) {
    NSMutableString *result = [NSMutableString stringWithString:@&quot;[&quot;];
    for (id obj in array) {
        [result appendString: [obj description]];
        [result appendString:@&quot;,&quot;];
    }
    NSUInteger len = [result length];//获取字符串长度
    [result deleteCharactersInRange:NSMakeRange(len - 1, 1)];//去掉最后一个字符
    [result appendString:@&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //初始化NSMutableArray集合
        NSMutableArray *array = [NSMutableArray arrayWithObjects:@&quot;波特&quot;,@&quot;罗恩&quot;,@&quot;赫敏&quot;,@&quot;阿瓦达啃大瓜&quot;, nil];

        //向集合最后追加元素
        [array addObject:@&quot;邓布利多&quot;];
        NSLog(@&quot;追加一个元素后:%@&quot;,NSCollectionToString(array));
        [array addObjectsFromArray:[NSArray arrayWithObjects:@&quot;弗利维&quot;,@&quot;米勒娃&quot;, nil]];
        NSLog(@&quot;最后追加两个元素后：%@&quot;,NSCollectionToString(array));

        //向集合指定位置插入元素
        [array insertObject:@&quot;马尔福&quot; atIndex:2];
        NSLog(@&quot;在索引为2的位置插入一个元素后：%@&quot;,NSCollectionToString(array));
        [array insertObjects:[NSArray arrayWithObjects:@&quot;金妮&quot;,@&quot;秋张&quot;, nil] atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(3, 2)]];
        NSLog(@&quot;插入多个元素后：%@&quot;,NSCollectionToString(array));

        //删除集合中指定位置的元素
        [array removeLastObject];
        NSLog(@&quot;删除最后一个元素后：%@&quot;,NSCollectionToString(array));
        [array removeObjectAtIndex:5];
        NSLog(@&quot;删除索引为5的元素后：%@&quot;,NSCollectionToString(array));
        [array removeObjectsInRange:NSMakeRange(2, 3)];
        NSLog(@&quot;删除索引为2-5的元素后：%@&quot;,NSCollectionToString(array));

        //替换集合中指定位置的元素
        [array replaceObjectAtIndex:2 withObject:@&quot;徐邵东&quot;];
        NSLog(@&quot;替换索引为2处的元素后：%@&quot;,NSCollectionToString(array));
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e40f7a5b29bb421f898302a7f68c964d.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;六、集合（NSSet和NSMutableSet）&lt;/h2&gt;
&lt;h3&gt;6.1 NSSet的功能与用法&lt;/h3&gt;
&lt;p&gt;NSSet集合像一个罐子，把对象放进去后是无序的，因此里面的元素不能重复。&lt;/p&gt;
&lt;p&gt;在前面说过，NSSet集合就像一个罐子，把对象放进去后是无序的，因此里面的元素不能重复。NSSet按Hash算法来存储集合中的元素，因此具有很好的存取和查找功能。与NSArray相比，NSSet最大的区别是元素没有索引，因此前面的NSArray的所有关于索引的方法都不能用于NSSet。&lt;/p&gt;
&lt;p&gt;但是NSSet和NSArray还是有相似之处，比如：1、它们都可以通过count方法来获取集合元素的数量。2、都可以使用快速枚举来集合遍历元素。3、都可以通过objectEnumerator方法获取NSEnumerator枚举器对集合元素进行遍历。4、都提供了makeObjectsPerformSelector：、makeObjectsPerformSelector：withObject：方法对集合元素整体调用某个方法，以及enumerateObjectsUsingBlock：、enumerateObjectsWithOptions：usingBlock对集合整体或部分元素迭代执行代码块。5、都提供了valueForKey：和setValue： forKey：方法对集合元素进行KVC编程。6、都提供了集合所有元素和部分元素进行KVC编程的方法。&lt;/p&gt;
&lt;p&gt;在NSSet集合中同样，以set开头的是类方法，以init开头的是实例方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

//定义一个函数，可以把NSSet集合转化为字符串
//方便我们调试观察结果
NSString *NSCollectionToString(id collection) {
    NSMutableString *result = [NSMutableString stringWithString: @&quot;[&quot;];
    for (id obj in collection) {
        [result appendString: [obj description]];
        [result appendString:@&quot;,&quot;];
    }
    NSUInteger len = [result length];//获取字符串长度
    [result deleteCharactersInRange: NSMakeRange(len - 1, 1)];//去除最后一个字符
    [result appendString: @&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //初始化集合set1和set2
        //在初始化集合set1的时候故意传入两个相同的元素，可以看到结果是只保留了一个
        NSSet *set1 = [NSSet setWithObjects: @&quot;缉毒英雄&quot;, @&quot;祁同伟&quot;, @&quot;省委书记&quot;, @&quot;高育良&quot;, nil];
        NSLog(@&quot;set1集合中元素个数为%ld&quot;, [set1 count]);
        NSLog(@&quot;s1集合：%@&quot;,NSCollectionToString(set1));
        NSSet *set2 = [NSSet setWithObjects: @&quot;狙击步枪&quot;, @&quot;省委书记&quot;, @&quot;沙瑞金&quot;,  nil];
        NSLog(@&quot;s2集合：%@&quot;, NSCollectionToString(set2));

        //向集合中追加单个元素
        set1 = [set1 setByAddingObject: @&quot;高小琴&quot;];
        NSLog(@&quot;添加一个元素后：%@&quot;, NSCollectionToString(set1));

        //获取两个集合的并集
        NSSet *s = [set1 setByAddingObjectsFromSet: set2];
        NSLog(@&quot;set1和set2的并集：%@&quot;, NSCollectionToString(s));

        //判断两个集合是否有交集
        BOOL b = [set1 intersectsSet: set2];
        NSLog(@&quot;set1和set2是否有交集：%d&quot;, b);

        //判断一个集合是否是另一个集合的子集
        BOOL bo = [set2 isSubsetOfSet: set1];
        NSLog(@&quot;set2是否是set1的子集：%d&quot;, bo);

        //判断集合中是否包含某个元素
        BOOL bb = [set1 containsObject: @&quot;祁同伟&quot;];
        NSLog(@&quot;set1是否包含祁同伟：%d&quot;, bb);

        //随机从集合中取出一个元素，但是同时写两个下面的代码输出的结果是相同的
        NSLog(@&quot;set1随机取出一个元素：%@&quot;, [set1 anyObject]);
        NSLog(@&quot;set1随机取出一个元素：%@&quot;, [set1 anyObject]);

        //使用代码块对集合元素进行过滤
        NSSet *filteredSet = [set2 objectsPassingTest: ^(id obj,BOOL *stop) {
            return (BOOL)([obj length] &amp;gt; 3);
        }];
        NSLog(@&quot;set2中的元素长度大于3的集合元素有：%@&quot;, NSCollectionToString(filteredSet));
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/e7d6b897c33e41dc984cc4ca7ffdb8a1.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.2 NSSet判断集合元素重复的标准&lt;/h3&gt;
&lt;p&gt;当向NSSet集合中存入一个元素时，NSSet会调用该对象的Hash方法来得到对象的hashCode值，然后根据该值决定该对象在底层Hash表中的存储位置，如果根据hashCode计算出该元素在底层Hash表中的存储位置已经不相同，那么系统自然的将它们存在不同的位置。&lt;/p&gt;
&lt;p&gt;如果两个元素的hashCode相同，接下来就要通过isEqual：方法判断两个元素是否相等，如果有两个元素通过isEqual：方法比较返回NO，NSSet依然认为它们不相等，NSSet会把它们都存在底层的Hash表的同一个位置，只是将在这个位置形成链，后面的元素添加失败。&lt;/p&gt;
&lt;p&gt;因此，HashSet集合判断两个元素相等的标准为：1、两个对象通过isEqual：方法比较返回YES；2、两个对象的hash方法返回值相等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKUser : NSObject

@property (nonatomic,copy) NSString* name;
@property (nonatomic,copy) NSString* pass;

-(id) initWithName: (NSString*) aName pass: (NSString*) aPass;
-(void) say:(NSString*) content;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKUser.h&quot;

@implementation FKUser

- (id) initWithName:(NSString *)aName pass:(NSString *)aPass {
    if (self = [super init]) {
        self.name = aName;
        self.pass = aPass;
    }
    return self;
}
-(void) say:(NSString *)content {
    NSLog(@&quot;%@说：&quot;,self.name,content);
}
-(BOOL) isEqual:(id)other {
    if (self == other) {
        return YES;
    }
    if ([other class] == FKUser.class) {
        FKUser *target = (FKUser*)other;
        return [self.name isEqualToString:target.name] &amp;amp;&amp;amp; [self.pass isEqualToString:target.pass];
    }
    return NO;
}
-(NSString*) description {
    return [NSString stringWithFormat:@&quot;FKUser[name = %@,pass = %@&amp;gt;&quot;,self.name,self.pass];
}
@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKUser.h&quot;

NSString* NSCollectionToString(id array) {
    NSMutableString *result = [NSMutableString stringWithString:@&quot;[&quot;];
    for (id obj in array) {
        [result appendString:[obj description]];
        [result appendString:@&quot;,&quot;];
    }
    NSInteger len = [result length];
    [result deleteCharactersInRange:NSMakeRange(len - 1, 1)];
    [result appendString:@&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSSet* set = [NSSet setWithObjects: [[FKUser alloc] initWithName:@&quot;messi&quot; pass: @&quot;30&quot;],[[FKUser alloc] initWithName:@&quot;Ronaldo&quot; pass:@&quot;7&quot;],[[FKUser alloc] initWithName: @&quot;mbappe&quot; pass:@&quot;9&quot;],[[FKUser alloc] initWithName:@&quot;Ronaldo&quot; pass:@&quot;7&quot;],[[FKUser alloc] initWithName:@&quot;weishihao&quot; pass:@&quot;7&quot;],nil];

        NSLog(@&quot;set集合元素的个数：%ld&quot;,[set count]);
        NSLog(@&quot;%@&quot;,NSCollectionToString(set));
        return 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我在添加时候，故意将C罗添加了两遍，结果在集合中真就出现了两遍，但我们知道NSSet中是不能有重复元素的，这是为什么呢因为程序只重写了isEqual方法但是没有重写hash方法，然后导致两个新元素的hashcode不相同，使得NSSet认为他们两个不相等，就都存到集合中了。因此，应该在重写hash方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-(NSUInteger) hash  {
    NSLog(@&quot;===hash===&quot;);
    NSUInteger nameHash = self.name == nil ? 0 : [self.name hash];
    NSUInteger passHash = self.pass == nil ? 0 : [self.pass hash];
    return nameHash * 31 + passHash;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/c39861b364f8476cb8a4723bc769dc26.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;重写hash方法的基本原则： 1，程序运行过程中，同一个对象多次调用hash方法应该返回相同的值 2，当两个对象通过isEqual：方法返回YES时，两个对象的hash应该返回相同的值 3，对象中作为isEqual：比较表村的实例变量，都应该用来hashcode值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6.3 NSMutableSet的功能和用法&lt;/h3&gt;
&lt;p&gt;和前面类似，NSMutableSet和NSSet的区别是前者可变后者不可变。NSMutableSet在NSSet的基础上新增了这几个方法：&lt;/p&gt;
&lt;p&gt;addObject：                                 向集合中添加单个元素&lt;/p&gt;
&lt;p&gt;removeObject：                          从集合中删除单个元素&lt;/p&gt;
&lt;p&gt;removeAllObject：                      删除集合中所有元素&lt;/p&gt;
&lt;p&gt;addObjectsFromArray：            使用NSArray数组作为参数，向NSSet集合中添加参数数组中的所有元素&lt;/p&gt;
&lt;p&gt;unionSet：                                   计算两个NSSet元素的并集&lt;/p&gt;
&lt;p&gt;minusSet：                                  计算两个NSSet集合的差集&lt;/p&gt;
&lt;p&gt;intersectSet：                             计算两个NSSet集合的交集&lt;/p&gt;
&lt;p&gt;setSet：                                       用后一个集合的元素替换已有集合中所有元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

//定义一个函数，可以把NSSet集合转化为字符串
//方便我们调试观察结果
NSString *NSCollectionToString(id collection) {
    NSMutableString *result = [NSMutableString stringWithString: @&quot;[&quot;];
    for (id obj in collection) {
        [result appendString: [obj description]];
        [result appendString: @&quot;,&quot;];
    }
    NSUInteger len = [result length];//获取字符串长度
    [result deleteCharactersInRange: NSMakeRange(len - 1, 1)];//去除最后一个字符
    [result appendString: @&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //创建一个初始容量为10的set集合
        NSMutableSet *set = [NSMutableSet setWithCapacity: 10];

        //向集合中添加一个元素
        [set addObject: @&quot;疯狂iOS讲义&quot;];
        NSLog(@&quot;set添加一个元素后：%@&quot;, NSCollectionToString(set));

        //利用NSArray向集合中添加多个元素
        [set addObjectsFromArray: [NSArray arrayWithObjects: @&quot;疯狂andro讲义&quot;, @&quot;疯狂Ajax讲义&quot;, @&quot;疯狂XML讲义&quot;, nil]];
        NSLog(@&quot;set使用NSArray添加三个元素后：%@&quot;, NSCollectionToString(set));

        //删除集合中指定元素
        [set removeObject: @&quot;疯狂XML讲义&quot;];
        NSLog(@&quot;set删除一个元素后：%@&quot;, NSCollectionToString(set));

        NSSet *set2 = [NSSet setWithObjects: @&quot;埃德加&quot;, @&quot;疯狂iOS讲义&quot;, nil];
        //计算两个集合的并集
        [set unionSet: set2];
        NSLog(@&quot;set和set2的并集：%@&quot;, NSCollectionToString(set));
        //计算两个集合的差集
        [set minusSet: set2];
        NSLog(@&quot;set和set2的差集：%@&quot;, NSCollectionToString(set));
        //计算两个集合的交集
        [set intersectSet: set2];
        NSLog(@&quot;set和set2的交集：%@&quot;, NSCollectionToString(set));
        //用set2的集合元素替换set集合的所有元素
        [set setSet: set2];
        NSLog(@&quot;用set2的集合元素替换set集合的所有元素：%@&quot;, NSCollectionToString(set));
    }
    return 0;
}
### 6.4 NSCountedSet的功能和用法


        NSCountedSet是NSMutableSet的子类，它与NSMutableSet集合不同的是：NSCountedSet为每个元素额外维护一个添加次数的状态。当程序向NSCountedSet中添加一个元素的时候，如果NSCountedSet集合中不包含该元素，NSCountedSet接纳该元素，并将该元素的添加次数标记为1；当程序向NSCountedSet中添加一个元素的时候，如果NSCountSet集合中已经包含该元素，NSCountedSet不会接纳该元素，但会将该元素添加次数加一。


        当程序从NSCountedSet中删除元素时，NSCountedSet只是将该元素的添加次数减一，只有当该元素添加次数变为0的时候，该元素才会真正的从NSCountedSet中删除。


        它提供了countForObject：方法来获取指定元素的添加次数。


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

//定义一个函数，可以把NSSet集合转化为字符串
//方便我们调试观察结果
NSString *NSCollectionToString(id collection) {
    NSMutableString *result = [NSMutableString stringWithString: @&quot;[&quot;];
    for (id obj in collection) {
        [result appendString: [obj description]];
        [result appendString: @&quot;,&quot;];
    }
    NSUInteger len = [result length];//获取字符串长度
    [result deleteCharactersInRange: NSMakeRange(len - 1, 1)];//去除最后一个字符
    [result appendString: @&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSCountedSet *set = [NSCountedSet setWithObjects: @&quot;疯狂iOS讲义&quot;, @&quot;疯狂andro讲义&quot;, @&quot;疯狂Ajax讲义&quot;, nil];

        //向set里添加两次对应字符串
        [set addObject: @&quot;疯狂iOS讲义&quot;];
        [set addObject: @&quot;疯狂iOS讲义&quot;];
        NSLog(@&quot;%@&quot;, NSCollectionToString(set));
        NSLog(@&quot;疯狂iOS讲义的添加次数为：%ld&quot;, [set countForObject: @&quot;疯狂iOS讲义&quot;]);

        //从set中删除对应字符串但不删完
        [set removeObject: @&quot;疯狂iOS讲义&quot;];
        NSLog(@&quot;删除疯狂iOS讲义一次后的结果：%@&quot;, NSCollectionToString(set));
        NSLog(@&quot;删除疯狂iOS讲义一次后的添加次数：%ld&quot;, [set countForObject: @&quot;疯狂iOS讲义&quot;]);

        //从set中删除对应字符串且删完
        [set removeObject: @&quot;疯狂iOS讲义&quot;];
        [set removeObject: @&quot;疯狂iOS讲义&quot;];
        NSLog(@&quot;删除疯狂iOS讲义3次后的结果：%@&quot;, NSCollectionToString(set));
        NSLog(@&quot;删除疯狂iOS讲义3次后的添加次数：%ld&quot;, [set countForObject: @&quot;疯狂iOS讲义&quot;]);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;七、有序集合&lt;/h2&gt;
&lt;p&gt;NSOderedSet和NSMutableOrderedSet既具有NSSet集合的特征，又具有NSArray类似的功能。它有以下两个特点：&lt;/p&gt;
&lt;p&gt;1、NSOrderedSet不允许元素重复。&lt;/p&gt;
&lt;p&gt;2、NSOrderedSet可以保持元素的添加顺序，而且每个元素都有索引，可以根据索引来操作元素。&lt;/p&gt;
&lt;p&gt;NSMutableOrderedSet是NSOrderedSet的子类，代表集合元素可变的有序集合。与前面一样，它可以增添，删除，替换，排序元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

//定义一个函数，可以把NSSet集合转化为字符串
//方便我们调试观察结果
NSString *NSCollectionToString(id collection) {
    NSMutableString *result = [NSMutableString stringWithString: @&quot;[&quot;];
    for (id obj in collection) {
        [result appendString: [obj description]];
        [result appendString: @&quot;,&quot;];
    }
    NSUInteger len = [result length];//获取字符串长度
    [result deleteCharactersInRange: NSMakeRange(len - 1, 1)];//去除最后一个字符
    [result appendString: @&quot;]&quot;];
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //创建集合时故意用重复的元素，可看到程序只会保留其中一个
        NSOrderedSet *set = [NSOrderedSet orderedSetWithObjects: [NSNumber numberWithInt: 40], [NSNumber numberWithInt: 12], [NSNumber numberWithInt: -9], [NSNumber numberWithInt: 28], [NSNumber numberWithInt: 12], [NSNumber numberWithInt: 17], nil];
        NSLog(@&quot;%@&quot;, NSCollectionToString(set));

        //根据索引获取元素
        NSLog(@&quot;set集合中的第一个元素：%@&quot;, [set firstObject]);
        NSLog(@&quot;set集合中的最后一个元素：%@&quot;, [set lastObject]);
        NSLog(@&quot;set集合中索引为2的元素：%@&quot;, [set objectAtIndex: 2]);
        NSLog(@&quot;28在set集合中的索引为：%ld&quot;, [set indexOfObject: [NSNumber numberWithInt:28]]);

        //对集合进行过滤，获取元素值大于20的元素的索引
        NSIndexSet *indexSet = [set indexesOfObjectsPassingTest: ^(id obj, NSUInteger idx, BOOL *stop) {
            return (BOOL)([obj intValue] &amp;gt; 20);
        }];
        NSLog(@&quot;set中元素值大于20的元素的索引为：%@&quot;, indexSet);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;八、字典（NSDictionary和NSMutableDictionary）&lt;/h2&gt;
&lt;p&gt;NSDictionary用于保存具有映射关系的数据，因此NSDictionary中保存着两组值，一组值用于保存key，另一组用于保存value。key和value都可以是任何引用类型的数据，Map的key不允许重复。&lt;/p&gt;
&lt;p&gt;key和value之间存在单向一对一的关系，即通过指定的key，总能找到唯一的、确定的value。&lt;/p&gt;
&lt;p&gt;NSDictionary包含了一个allKeys方法，用于返回NSDictionary中所有key组成的NSArray集合。&lt;/p&gt;
&lt;h3&gt;8.1 NSDictionary的功能和用法&lt;/h3&gt;
&lt;p&gt;NSDictionary由多组key-value对组成，因此创建NSDictionary时需要同时指定多组key、value对。NSDictionary分别提供了dictionary开头的类方法和init开头的实例方法。下面是创建NSDIctionary常见的几种方法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法名&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; &lt;strong&gt;示例&lt;/strong&gt; dictionary 创建一个空的 NSDictionary NSDictionary *dict = [NSDictionary dictionary]; dictionaryWithContentsOfFile: / initWithContentsOfFile: 通过读取指定文件（通常是 .plist）初始化字典 NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:@&quot;/path/to/file.plist&quot;]; dictionaryWithDictionary: / initWithDictionary: 用另一个 NSDictionary 的 key-value 对创建新的字典 NSDictionary *dict2 = [NSDictionary dictionaryWithDictionary:dict1]; dictionaryWithObjects:forKeys:/ initWithObjects:forKeys: 通过两个 NSArray（对象数组和值数组）来创建字典 NSDictionary *dict = [NSDictionary dictionaryWithObjects:@[@&quot;张三&quot;, @&quot;25&quot;] forKeys:@[@&quot;name&quot;, @&quot;age&quot;]]; dictionaryWithObject:forKey: 使用单个 key-value 对来创建字典 NSDictionary *dict = [NSDictionary dictionaryWithObject:@&quot;张三&quot; forKey:@&quot;name&quot;]; dictionaryWithObjectsAndKeys:/ initWithObjectsAndKeys: 使用多个 value-key 对按顺序传入（最后以 nil 结尾）创建字典 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@&quot;张三&quot;, @&quot;name&quot;, @&quot;25&quot;, @&quot;age&quot;, nil];&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/9222184334fd4bfda9f8f348d71f7ee3.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在用代码演示上述功能之前，我们先写一个代码为NSDictionary扩展了一个print类别，在类别中扩展了一个print方法，用于打印NSDictionary中key-value对的详情：&lt;/p&gt;
&lt;p&gt;这是接口部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface NSDictionary (printf)

- (void) print;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;NSDictionary+printf.h&quot;

@implementation NSDictionary (printf)

- (void) print {
    NSMutableString *result = [NSMutableString stringWithString: @&quot;{&quot;];//创建一个可变字符串，初始化为左花括号
    //快速枚举遍历调用该方法的对象的所有key元素
    for (id key in self) {
        [result appendString: [key description]];//向最开始的result对象后面追加访问到的key调用的改写过的description方法的返回值
        [result appendString: @&quot;=&quot;];
        [result appendString: [self[key] description]];//使用下标访问法根据key获取对应的value
        [result appendString: @&quot;,&quot;];
    }
    NSUInteger len = [result length];//获取字符串长度
    [result deleteCharactersInRange: NSMakeRange(len - 2, 2)];//去掉字符串最后两个字符
    [result appendString: @&quot;}&quot;];
    NSLog(@&quot;%@&quot;, result);
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码演示了NSDictionary的两个基本用法，程序可以使用快速枚举来遍历NSDictionary的所有key。除此之外，程序也可以根据key来获取对应的value。通过key来获取value有如下两种语法：&lt;/p&gt;
&lt;p&gt;1、调用NSDictionary的objectForKey：方法即可根据key来获取对应的value。&lt;/p&gt;
&lt;p&gt;2、直接使用下标法根据key来获取对应的value。使用这个语法获取时实际上就是调用NSDictionary的objectForKeyedSubscript：方法访问。&lt;/p&gt;
&lt;p&gt;上面两个方法对应下面两个代码，它们的功能是相同的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dictionary objectForKey: key];
dictionary[key];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后又要用到之前写的那个FKUser类，类的接口和实现部分如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKUser : NSObject

@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *pass;

- (id) initWithName: (NSString*) aName pass: (NSString*) aPass;//重写初始化方法
- (void) say: (NSString*) content;//定义一个say方法

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKUser.h&quot;

@implementation FKUser

@synthesize name;
@synthesize pass;

- (id) initWithName:(NSString *) aName pass:(NSString *) aPass {
    if (self = [super init]) {
        name = aName;
        pass = aPass;
    }
    return self;
}
- (void) say: (NSString*) content {
    NSLog(@&quot;%@说：%@&quot;, self.name, content);
}

//重写自定义isEqual方法
- (BOOL) isEqual: (id)other {
    if (self == other) {
        return YES;
    }
    if ([other class] == FKUser.class) {
        FKUser *target = (FKUser*) other;
        return [self.name isEqualToString: target.name] &amp;amp;&amp;amp; [self.pass isEqualToString: target.pass];
    }
    return NO;
}
//重写description方法
- (NSString*) description {
    return [NSString stringWithFormat:@&quot;&amp;lt;FKUser[name = %@, pass = %@&amp;gt;&quot;, self.name, self.pass];
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;FKUser.h&quot;
#import &quot;NSDictionary+printf.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //使用多个key-value对初始化创建NSDictionary对象
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: [[FKUser alloc] initWithName: @&quot;晓美焰&quot; pass: @&quot;123&quot;], @&quot;one&quot;, [[FKUser alloc] initWithName: @&quot;鹿目圆&quot; pass: @&quot;345&quot;], @&quot;two&quot;, [[FKUser alloc] initWithName: @&quot;晓美焰&quot; pass: @&quot;123&quot;], @&quot;three&quot;, [[FKUser alloc] initWithName: @&quot;巴麻美&quot; pass: @&quot;178&quot;], @&quot;four&quot;, [[FKUser alloc] initWithName: @&quot;美树沙耶香&quot; pass: @&quot;155&quot;], @&quot;five&quot;, nil];
        //对dict对象调用print方法，输出value和key的值
        [dict print];
        //调用count方法获得key-value对的数量
        NSLog(@&quot;dict包含%ld个key-value对&quot;, [dict count]);
        //调用allKey方法获得所有的key值
        NSLog(@&quot;dict的所有key是：%@&quot;, [dict allKeys]);
        //调用allKeysForObject方法获取指定value对应的全部key
        NSLog(@&quot;&amp;lt;FKUser[name = 晓美焰, pass = 123]&amp;gt;对应的所有的key为：%@&quot;, [dict allKeysForObject: [[FKUser alloc] initWithName: @&quot;晓美焰&quot; pass: @&quot;123&quot;]]);

        //获取遍历dict所有value的枚举器
        NSEnumerator *en = [dict objectEnumerator];
        NSObject *value;
        //使用枚举器遍历dict中的所有value
        while (value = [en nextObject]) {
            NSLog(@&quot;%@&quot;, value);
        }

        //使用指定代码块来迭代执行该集合中所有key-value对
        [dict enumerateKeysAndObjectsUsingBlock: ^(id key, id value, BOOL *stop) {
            if (![key  isEqual: @&quot;two&quot;]) {
                NSLog(@&quot;key的值为：%@&quot;, key);
                [value say: @&quot;圆神怎么你了&quot;];
            } else {
                NSLog(@&quot;key的值为：%@&quot;, key);
                [value say: @&quot;我怎么你了&quot;];
            }
        }];
    }
    return 0;
}
### 8.2 对NSDictionary的key排序


  NSDictionary还提供了方法对NSDictionary的所有key执行排序，这些方法执行完成后将返回排序完成后所有key组成的NSSArray。NSDictionary提供的排序方法如下：


        1、keysSortedByValueUsingSelector:：根据NSDictionary的所有value的指定方法的返回值对key排序；调用value的该方法必须返回NSOrderedAscending（升序）、NSOrderedDesending（降序）、NSOrderedSame（同序）的三个值之一。


        2、keysSortedByValueUsingComparator:：该方法使用指定的代码块来遍历key-value对，并根据执行结果返回NSOrderedAscending（升序）、NSOrderedDesending（降序）、NSOrderedSame（同序）的三个值之一来对NSDictionary的所有key排序。


        3、keysSortedByValueWithOptions：usingComparator:：与前一个方法的功能相似，只是该方法可以传入一个额外的NSEnumerationOptions参数。


下面用代码演示：


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;NSDictionary+print.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //使用多个key-value对创建NSDictionary
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: @&quot;OC&quot;, @&quot;one&quot;, @&quot;Ruby&quot;, @&quot;two&quot;, @&quot;Python&quot;, @&quot;three&quot;, @&quot;Perl&quot;, @&quot;four&quot;, nil];
        [dict print];//调用print打印dict中所有的元素

        //
        NSArray *keyArr1 = [dict keysSortedByValueUsingSelector: @selector(compare:)];
        NSLog(@&quot;%@&quot;, keyArr1);
        NSArray *keyArr2 = [dict keysSortedByValueUsingComparator: ^(id value1, id value2) {
            if ([value1 length] &amp;gt; [value2 length]) {
                return NSOrderedDescending;
            }
            if ([value1 length] &amp;lt; [value2 length]) {
                return NSOrderedAscending;
            }
            return NSOrderedSame;
        }];
        NSLog(@&quot;%@&quot;, keyArr2);
        [dict writeToFile: @&quot;myFile.txt&quot; atomically: YES];
    }
    return 0;
}
### 8.3 对NSDictionary的key进行过滤


NSDictionary提供了以下两个过滤方法：


        1、keysOfEntriesPassingTest:：使用代码块迭代处理NSDictionary中的每个key-value对，并对其进行过滤，该代码必须返回BOOL类型的值，只有当该代码返回YES的时候，该key才会被保留下来；该代码块可以接受三个参数，第一个参数表示正在迭代处理的key，第二个参数代表正在迭代处理的value，第三个参数代表是否需要继续迭代。


        2、keysOfEntriesWithOptions: passingTest:：该方法的功能与前一个方法的功能基本相同，只是该方法额外传入一个NSEnumerationOptions选项参数。


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;NSDictionary+print.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: 89], @&quot;Objective-C&quot;, [NSNumber numberWithInt: 69], @&quot;Ruby&quot;, [NSNumber numberWithInt: 75], @&quot;Python&quot;, [NSNumber numberWithInt: 109], @&quot;Perl&quot;,  nil];
        [dict print];

        //对NSDictionary的所有key过滤
        NSSet *KeySet = [dict keysOfEntriesPassingTest: ^(id key, id value, BOOL *stop) {
        //对NSDictionary的value进行比较
        //当value的值大于80的key才会被保留
            return (BOOL)([value intValue] &amp;gt; 80);
        }];
        NSLog(@&quot;%@&quot;, KeySet);
    }
    return 0;
}
### 8.4 使用自定义类作为NSDictionary的key


 


如果程序打算使用自定义类作为key，需要满足以下要求：


        1、该自定义类正确重写过isEqual和hash方法，即当两个对象通过isEqual：方法判断相等时它们的hash方法的返回值也相等。


        2、该自定义类必须实现了copyWithZone：方法，该方法最好能返回对象的不可变副本。


以下是代码演示：


首先还是要有扩展的print，和上面一样就不写了


FKUser的接口和实现，在实现中，要重写isEqual和hash方法


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

NS_ASSUME_NONNULL_BEGIN

@interface FKUser : NSObject&amp;lt;NSCopying&amp;gt;

@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *pass;

- (id) initWithName: (NSString*) aName pass: (NSString*) aPass;
- (void) say: (NSString*) content;

@end

NS_ASSUME_NONNULL_END
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;FKUser.h&quot;

@implementation FKUser

@synthesize name;
@synthesize pass;

- (id) initWithName:(NSString *) aName pass:(NSString *) aPass {
    if (self = [super init]) {
        self.name = aName;
        self.pass = aPass;
    }
    return self;
}
- (void) say: (NSString *) content {
    NSLog(@&quot;%@说:%@&quot;, self.name, content);
}
- (BOOL) isEqual: (id)object {
    if (self == object) {
        return YES;
    }
    if ([object class] == FKUser.class) {
        FKUser *target = (FKUser*) object;
        return [self.name isEqualToString: target.name] &amp;amp;&amp;amp; [self.pass isEqualToString: target.pass];
    }
    return NO;
}
- (NSString*) description {
    return [NSString stringWithFormat: @&quot;&amp;lt;FKUSer[name = %@, pass = %@]&amp;gt;&quot;, self.name, self.pass];
}
- (id) copyWithZone: (NSZone *) zone {
    NSLog(@&quot;-----正在复制-----&quot;);
    FKUser *newUser = [[[self class] allocWithZone: zone] init];
    newUser.name = self.name;
    newUser.pass = self.pass;
    return newUser;
}

//重写hash方法，重写该方法的比较标准是：
//如果两个FKUser的name、pass相等，两个FKUser的Hash方法返回值相等
- (NSUInteger) hash {
    NSUInteger nameHash = name == nil ? 0 : [name hash];
    NSUInteger passHash = pass == nil ? 0 : [pass hash];
    return nameHash * 31 + passHash;
}

@end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;NSDictionary+print.h&quot;
#import &quot;FKUser.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FKUser *u1 = [[FKUser alloc] initWithName: @&quot;晓美焰&quot; pass: @&quot;345&quot;];
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: @&quot;one&quot;, [[FKUser alloc] initWithName: @&quot;鹿目圆&quot; pass: @&quot;123&quot;], @&quot;two&quot;, u1, @&quot;three&quot;, [[FKUser alloc] initWithName: @&quot;鹿目圆&quot; pass: @&quot;123&quot;], @&quot;four&quot;, [[FKUser alloc] initWithName: @&quot;巴麻美&quot; pass: @&quot;178&quot;], @&quot;five&quot;, [[FKUser alloc] initWithName: @&quot;美树沙耶香&quot; pass: @&quot;155&quot;], nil];
        u1.pass = nil;
        [dict print];
    }
    return 0;
}
### 8.5 NSMutableDictionary的功能和用法


      NSMutableDictionary继承了NSDictionary，代表一个key-value可变的NSDictionary集合。


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &quot;NSDictionary+print.h&quot;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: 89], @&quot;疯狂iOS讲义&quot;, nil];

        //使用下标法设置key-value对，由于NSDictionary中存在该key
        //所以此处设置的value会覆盖之前的value
        dict[@&quot;疯狂iOS讲义&quot;] = [NSNumber numberWithInt: 99];
        [dict print];
        NSLog(@&quot;--再次添加key-value对--&quot;);
        dict[@&quot;疯狂XML讲义&quot;] = [NSNumber numberWithInt: 69];
        dict[@&quot;疯狂Android讲义&quot;] = [NSNumber numberWithInt: 69];
        [dict print];
        NSDictionary *dict2 = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: 79], @&quot;疯狂Ajax讲义&quot;, [NSNumber numberWithInt: 89], @&quot;Struts 2.x权威指南&quot;, nil];

        //将另一个NSDictionary中的key-value对添加到该集合中
        [dict addEntriesFromDictionary: dict2];
        [dict print];
        //根据key来删除key-value对
        [dict removeObjectForKey: @&quot;Struts 2.x权威指南&quot;];
        [dict print];
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147879235&quot;&gt;OC语言学习——Foundation框架（下）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>OC语言学习——Foundation框架（上）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147835869-ocfoundation-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147835869-ocfoundation-/</guid><description>一、字符串 NSString代表字符序列不可变的字符串，而NSMutable代表字符序列可变的字符串。 1.1 NSString字符串及功能 通过NSString，我们可以： 1、创建字符串。2、读取文件或网络URL来初始化字符串，或者将字符串写入文件或URL。3、获取字符串长度</description><pubDate>Sun, 11 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、字符串&lt;/h2&gt;
&lt;p&gt;NSString代表字符序列不可变的字符串，而NSMutable代表字符序列可变的字符串。&lt;/p&gt;
&lt;h3&gt;1.1 NSString字符串及功能&lt;/h3&gt;
&lt;p&gt;通过NSString，我们可以：&lt;/p&gt;
&lt;p&gt;1、创建字符串。2、读取文件或网络URL来初始化字符串，或者将字符串写入文件或URL。3、获取字符串长度，即可获取字符串字符个数，也可获取字符串包括的字节个数。4、获取字符串中的字符或字节，即可获取指定位置的字符，也可获取指定范围的字符。5、获取字符串对应的C风格字符串。6、连接、分隔、查找、替换、比较字符串。7、对字符串中的字符进行大小写转换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //使用Unicode数值数组初始化字符串
        unichar data[6] = {97,98,99,100,101,102};
        NSString *str1 = [[NSString alloc] initWithCharacters: data length: 6];
        NSLog(@&quot;%@&quot;,str1);   //abcdef

        //将C风格的字符串转换为NSString对象
        char *cstr = &quot;Hello,iOS!&quot;;
        NSString *str2 = [NSString stringWithUTF8String: cstr];
        NSLog(@&quot;%@&quot;,str2);   //Hello,iOS!

        //将字符串写入指定对象
        [str2 writeToFile: @&quot;myFile.txt&quot; atomically:YES encoding:NSUTF8StringEncoding error: nil];

        //读取文件内容，用文件内容初始化字符串
        NSString *str3 = [NSString stringWithContentsOfFile:@&quot;NSStringTest.m&quot;encoding:NSUTF8StringEncoding error:nil];
        NSLog(@&quot;%@&quot;,str3);    // (null)
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;unichar 是无符号 16 位字符类型，表示 UTF-16 编码；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接下来的一段代码，我们来演示NSString的其他功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *str1 = @&quot;Hello&quot;;
        NSString *str2 = @&quot;Hello&quot;;
        NSString *book = @&quot;《疯狂iOS》&quot;;

        //追加字符串
        str1 = [str1 stringByAppendingString: @&quot;,iOS!&quot;];
        NSLog(@&quot;str1后接“，iOS”得到的字符串：%@&quot;,str1);

        str2 = [str2 stringByAppendingString: book];
        NSLog(@&quot;str2后接book得到的字符串：%@&quot;,str2);

        //获取C风格的字符串
        const char *cstr = [str1 UTF8String];
        NSLog(@&quot;获取的C字符串：%s&quot;,cstr);

        str1 = [str1 stringByAppendingFormat: @&quot;%@是一本非常不错的书&quot;,book];
        NSLog(@&quot;%@&quot;,str1);

        //获取指定字符
        NSString *s1 = [str1 substringToIndex: 5];
        NSLog(@&quot;s1前5个字符组成的字符串：%@&quot;,s1);

        NSString *s2 = [str1 substringFromIndex: 10];
        NSLog(@&quot;获取str1从第10个字符开始以后的所有字符：%@&quot;,s2);

        NSString *s3 = [str1 substringWithRange: NSMakeRange(5,10)];
        NSLog(@&quot;获取str1中第5个到第10个字符：%@&quot;,s3);

        NSRange pos = [str1 rangeOfString: @&quot;iOS&quot;];
        NSLog(@&quot;iOS在str1中出现的开始位置：%ld,长度为：%ld&quot;,pos.location,pos.length);

        //对字符进行大小转换
        str1 = [str1 uppercaseString];
        NSLog(@&quot;str1的字符转化为大写：%@&quot;,str1);

        //获取字符串长度和字节数
        NSLog(@&quot;str1的字符个数为：%lu&quot;,[str1 length]);
        NSLog(@&quot;str1按UTF-8解码后字节数为：%lu&quot;,[str1 lengthOfBytesUsingEncoding: NSUTF8StringEncoding]);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码使用了一个NSRange类型的变量，NSRange并不是一个类，它只是一个结构体，包括了location和length两个unsigned int整型值，分别代表起始位置和长度。&lt;/p&gt;
&lt;p&gt;还有一个NSMakeRange(loc，len)，它是一个结构体类型，包含两个参数，loc是起始位置，len是长度。表示字符串要传进来的起始位置和长度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/0bf7bf993f3e49ac9e6ab49112ceef08.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1.2 可变字符串 NSMutableString&lt;/h3&gt;
&lt;p&gt;NSString字符串是不可变的字符串，即一旦NSString对象被创建，其中的字符序列就不能更改了。而NSMutableString字符串就不一样了，它的字符串序列是可更改的。而且NSMutableString是NSString的子类，因此，上一节说的NSString的方法，NSMutableString都可以直接使用。&lt;/p&gt;
&lt;p&gt;NSMutableString提供了以下的方法来改变字符串字符序列：&lt;/p&gt;
&lt;p&gt;追加字符串 appendString: / appendFormat: 插入字符串 insertString:atIndex: 删除字符 deleteCharactersInRange: 替换内容 replaceOccurrencesOfString:withString:options:range: 获取长度 length 转为不可变 [NSString stringWithString:mutableStr]&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *book = @&quot;《疯狂iOS》&quot;;
        NSMutableString *str1 = [NSMutableString stringWithString: @&quot;Hello&quot;];

        [str1 appendString: @&quot;,iOS!&quot;];//追加固定字符串
        NSLog(@&quot;%@&quot;,str1);

        [str1 appendFormat:@&quot;%@是一本非常不错的图书&quot;,book];//追加带变量的字符串
        NSLog(@&quot;%@&quot;,str1);

        [str1 insertString:@&quot;fkit.org&quot; atIndex: 6];//在指定位置插入字符串
        NSLog(@&quot;%@&quot;,str1);

        [str1 deleteCharactersInRange: NSMakeRange(6, 12)];//删除第六到第十二个字符
        NSLog(@&quot;%@&quot;,str1);

        [str1 replaceCharactersInRange:NSMakeRange(6, 15) withString:@&quot;Objective-C&quot;];//将第六到第十五个字符改为“Objective-C”
        NSLog(@&quot;%@&quot;,str1);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i-blog.csdnimg.cn/direct/bfe541a736ed4a659f991265b5a55234.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、日期与时间&lt;/h2&gt;
&lt;p&gt;Objective- C为处理日期、时间提供了NSDate，NSCalender 对象，还提供了日期格式器来处理日期与字符串之间的转换&lt;/p&gt;
&lt;h3&gt;2.1 日期与时间（NSDate）&lt;/h3&gt;
&lt;p&gt;其中，NSDate对象代表日期与时间，OC既提供了类方法来创建NSDate对象，也提供了大量init开头的方法来初始化NSDate对象。创建NSDate的类方法和实例方法基本相似，只是&lt;strong&gt;类方法以date开头，实例方法以init开头。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//获取代表当前日期，时间的NSDate
        NSDate* date1= [NSDate date];
        NSLog(@&quot;%@&quot;, date1);
        //获取从当前时间开始，一天以后的日期
        NSDate* date2 = [[NSDate alloc] initWithTimeIntervalSinceNow:3600*24];
        NSLog(@&quot;%@&quot;, date2);
        //获取从当前时间开始，3天以前的日期
        NSDate* date3 = [[NSDate alloc] initWithTimeIntervalSinceNow:-3*3600*24];
        NSLog(@&quot;%@&quot;, date3);
### 2.2 日期格式器


 ** NSDateFormatte**r代表一个日期格式器，它的作用就是完成NSDate和NSString之间的转换。在进行转换时，我们首先需要创建一个NSDateFormatter对象，然后调用该对象的setDateStyle：、setTimeStyle：方法设置格式化日期、时间的风格。其中日期、时间风格支持以下几个枚举值：


NSDateFormatterNoStyle    不显示日期、时间的风格

NSDateFormatterShortStyle    显示“短”的日期、时间的风格

NSDateFormatterLongStyle    显示“长”的日期、时间的风格

NSDateFormatterMediumStyle    显示“中等”的日期、时间的风格

NSDateFormatterFullStyle    显示“完整”的日期、时间的风格

        除了这几个枚举值，我们还可以通过调用setDateFormate：方法设置日期、时间的风格模版。


        如果需要将NSDate转换为NSString，可以调用NSDateFormatter的stringFromDate：方法执行格式化即可；如果需要将NSString转换为NSDate，可以调用NSDateFormatter的dateFromString：方法执行格式化即可。


接下来用代码来演示NSDate和NSDateFormatter的功能：


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //NSDate的功能演示
        NSLog(@&quot;----------------------以下是NSDate的功能运行结果--------------------&quot;);
        NSDate *date1 = [NSDate date]; //获取当前时间
        NSLog(@&quot;%@&quot;,date1);

        NSDate *date2 = [[NSDate alloc] initWithTimeIntervalSinceNow:3600 * 24];//获取从当前时间开始的后一天的时间
        NSLog(@&quot;%@&quot;,date2);

        NSDate *date3 = [[NSDate alloc] initWithTimeIntervalSinceNow:-3 * 3600 * 24];//获取从现在开始三天前的时间
        NSLog(@&quot;%@&quot;,date3);

        NSDate *date4 = [[NSDate alloc] initWithTimeIntervalSince1970:3600 * 24 * 366 * 20];//获取从1970年1月1日开始往后20年的时间
        NSLog(@&quot;%@&quot;,date4);

        NSLocale *cn = [NSLocale currentLocale];//NSLocale代表一个语言，这里表示中文
        NSLog(@&quot;%@&quot;,[date1 descriptionWithLocale: cn]);//用中文输出date1的时间

        NSDate *earlier = [date1 earlierDate: date2];
        NSLog(@&quot;%@&quot;,earlier);//获取两个时间中较早的时间

        NSDate *later = [date1 laterDate: date2];
        NSLog(@&quot;%@&quot;,later);//获取两个时间中较晚的时间

        //比较两个日期用：compare：方法，它包括如下三个值
        //三个值分别代表调用compare的日期位于被比较日期之前、相同、之后
        switch([date1 compare: date3]) {
            case NSOrderedAscending: NSLog(@&quot;date1在date3之前&quot;);
                break;
            case NSOrderedSame: NSLog(@&quot;date1和date3时间想相同&quot;);
                break;
            case NSOrderedDescending: NSLog(@&quot;date1在date3时间之后&quot;);
                break;
        }

        NSLog(@&quot;date1和date3的时间差是%g秒&quot;,[date1 timeIntervalSinceDate: date3]);//获取两个时间的时间差
        NSLog(@&quot;date2与现在的时间差%g秒&quot;,[date2 timeIntervalSinceNow]);//获取指定时间和现在的时间差


        //NSDateFormatter的功能
        NSLog(@&quot;----------------以下是NSDateFormatter的功能的运行结果----------------&quot;);
        NSDateFormatter *dt = [NSDate dateWithTimeIntervalSince1970:3600 * 24 * 366 * 20];//格式化时间为从1970年1月1日开始的20年后的时间

        NSLocale *locales[] = {[[NSLocale alloc] initWithLocaleIdentifier:@&quot;zh_CN&quot;],[[NSLocale alloc] initWithLocaleIdentifier:@&quot;en_US&quot;]};//创建两个NSLocale分别表示中国、美国
        NSDateFormatter *df[8];//为上面两个NSLocale创建8个NSDateFormatter对象

        for (int i = 0; i &amp;lt; 2; i++) {
            df[i * 4] = [[NSDateFormatter alloc] init];
            [df[i * 4] setDateStyle:NSDateFormatterShortStyle];//设置NSDateFormatter的日期、时间风格
            [df[i * 4] setTimeStyle:NSDateFormatterShortStyle];
            [df[i * 4] setLocale: locales[i]];//设置NSDateFormatter的NSLocale

            df[i * 4 + 1] = [[NSDateFormatter alloc] init];
            [df[i * 4 + 1]setDateStyle:NSDateFormatterMediumStyle];//设置NSDateFormatter的日期、时间风格
            [df[i * 4 + 1]setDateStyle:NSDateFormatterMediumStyle];
            [df[i * 4 + 1] setLocale: locales[i]];//设置NSDateFormatter的NSLocale

            df[i * 4 + 2] = [[NSDateFormatter alloc] init];
            [df[i * 4 + 2] setDateStyle:NSDateFormatterLongStyle];//设置NSDateFormatter的日期、时间风格
            [df[i * 4 + 2] setTimeStyle:NSDateFormatterLongStyle];
            [df[i * 4 + 2] setLocale: locales[i]];//设置NSDateFormatter的NSLocale

            df[i * 4 + 3] = [[NSDateFormatter alloc] init];
            [df[i * 4 + 3] setDateStyle:NSDateFormatterFullStyle];//设置NSDateFormatter的日期、时间风格
            [df[i * 4 + 3] setTimeStyle:NSDateFormatterFullStyle];
            [df[i * 4 + 3] setLocale: locales[i]];//设置NSDateFormatter的NSLocale
        }
        for (int i = 0; i &amp;lt; 2; i++) {
            switch (i) {
                case 0: NSLog(@&quot;-----中国日期格式------&quot;);
                    break;
                case 1: NSLog(@&quot;-----美国日期格式------&quot;);
                    break;
            }
            NSLog(@&quot;SHORT格式的日期格式：%@&quot;,[df[i * 4] stringFromDate: dt]);
            NSLog(@&quot;MEDIUM格式的日期格式：%@&quot;,[df[i * 4 + 1] stringFromDate: dt]);
            NSLog(@&quot;LONG格式的日期格式：%@&quot;,[df[i * 4 + 2] stringFromDate: dt]);
            NSLog(@&quot;FULL格式的日期格式：%@&quot;,[df[i * 4 + 3] stringFromDate: dt]);
        }
        NSDateFormatter *df2 = [[NSDateFormatter alloc] init];
        [df2 setDateFormat:@&quot;公元yyyy年MM月DD日HH时mm分&quot;];//设置自定义格式器模版
        NSLog(@&quot;%@&quot;,[df2 stringFromDate: dt]);//执行格式化
        NSString *dateStr = @&quot;2013-03-02&quot;;
        NSDateFormatter *df3 = [[NSDateFormatter alloc] init];
        [df3 setDateFormat: @&quot;yyyy-MM-DD&quot;];//根据日期字符串的格式设置格式模版
        NSDate *date6 = [df3 dateFromString: dateStr];//将字符串转化为NSDate对象
        NSLog(@&quot;%@&quot;,date6);
    }
    return 0;
}
### 2.3 日历（NSCalender）与日期组件（NSDateComponents）


 当需要将年、月、日的数值转换为NSDate的时候，或者从NSDate对象中获取其包含的年、月、日信息时，我们就需要将NSDate对象的各个字段数据分开提取。


        为了能分开NSDate对象包含的各个字段数据，Foundation框架提供了NSCalendar对象，该对象包含了以下两个常用方法：


        1、（NSDateComponents*）components： fromDate： ：从NSDate提取年、月、日、时、分、秒各时间字段的信息。


        2、dateFromComponents：（NSDateComponents*）comps：使用comps对象包含的年、月、日、时、分、秒各时间字段的信息来创建NSDate。


        NSDateComponents对象是专门用于封装年、月、日、时、分、秒各时间字段的信息，该对象只包含了对year、month、day、hour、minute、second、week、weekday等各字段的getter和setter方法。


```objective-c
#import &amp;lt;Foundation/Foundation.h&amp;gt;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier: NSCalendarIdentifierGregorian];//获取代表公历的Calendar对象
        NSDate *dt = [NSDate date];//获取当前日期

        unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitWeekday;//定义一个时间字段的旗标，指定将会获取指定年、月、日、时、分、秒的信息
        NSDateComponents *comp = [gregorian components: unitFlags fromDate: dt];//获取不同时间字段的信息

        //获取各时间字段的数值
        NSLog(@&quot;现在是%ld年&quot;,comp.year);
        NSLog(@&quot;现在是%ld月&quot;,comp.month);
        NSLog(@&quot;现在是%ld日&quot;,comp.day);
        NSLog(@&quot;现在是%ld时&quot;,comp.hour);
        NSLog(@&quot;现在是%ld分&quot;,comp.minute);
        NSLog(@&quot;现在是%ld秒&quot;,comp.second);
        NSLog(@&quot;现在是星期%ld&quot;,comp.weekday);//这里输出的数字会比按照日历的星期数多一，是因为按西方他们是把周天当每周的第一天的

        NSDateComponents *comp2 = [[NSDateComponents alloc] init];//再次创建一个NSDateComponents对象

        //设置各时间字段的数值
        comp2.year = 2023;
        comp2.month = 5;
        comp2.day = 10;
        comp2.hour = 18;
        comp2.minute = 15;

        //通过NSDateComponents所包含的时间字段的数值来恢复NSDate对象
        NSDate *date = [gregorian dateFromComponents: comp2];
        NSLog(@&quot;获取的日期为：%@&quot;,date);
    }
    return 0;
}
### 2.4 定时器


  当程序需要让某个方法重复执行，可以借助OC中的定时器来完成。


        通过调用NSTimer的scheduledTimerWithTimeInterval： invocation： repeats：或scheduledTimerWithTimeInterval： targe：selector： userInfo： repeats：类方法来创建NSTimer对象。调用该方法时需要传入以下参数：


        1、timeInterval：指定每隔多少秒执行一次任务


        2、invocation或target与selector：指定重复执行的任务。如果指定target和selector参数，则指定用某个对象的特定方法作为重复执行的任务；如果指定invocation参数，该参数需要传入一个NSInvocation对象，该对象也是封装target和selector的，其实也是指定用某个对象的特定方法作为重复执行的任务。


        3、userInfo：该参数用于传入额外的附加信息。


        4、repeats：该参数需要指定一个BOOL值，该参数控制是否需要重复执行任务。


        在执行完定时器后，也需要销毁定时器，只要调用定时器的invalidate方法即可。

---

原文发布于 CSDN：[OC语言学习——Foundation框架（上）](https://blog.csdn.net/2402_86720949/article/details/147835869)&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Leetcode hot 100 个人总结（持续更新）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147483386-leetcode-hot-100-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147483386-leetcode-hot-100-/</guid><description>笨人算法fresh man，如有纰漏万望您拨冗指正，不胜感激 二分查找 个人认为二分查找的难点主要分布在两点： 1. 不知道应该使用二分查找 2. 二分查找边界条件判断（ &amp; nums, int target) { auto ans = lower bound(nums.begi</description><pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;笨人算法fresh man，如有纰漏万望您拨冗指正，不胜感激&lt;/h2&gt;
&lt;h2&gt;二分查找&lt;/h2&gt;
&lt;p&gt;个人认为二分查找的难点主要分布在两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不知道应该使用二分查找&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二分查找边界条件判断（ &amp;lt;= or &amp;lt; ;  mid = left + 1 or left）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h3&gt;35. 搜索插入位置&lt;/h3&gt;
&lt;p&gt;不做赘述，有问题的需巩固基础&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int searchInsert(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        auto ans = lower_bound(nums.begin(), nums.end(), target);
        int answer = ans - nums.begin();
        return answer;
    }
};
### 34. 在排序数组中查找元素的第一个和最后一个位置


本题可以算是lower_bound和upper_bound的模板题，但需注意：


upper_bound 是查找第一个大于target的位置


lower_bound 是查找第一个大于等于target的位置


两者均返回迭代器，所以需用auto。


```cpp
#include &amp;lt;algorithm&amp;gt;
class Solution {
public:
    vector&amp;lt;int&amp;gt; searchRange(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        // if (!binary_search(nums.begin(), nums.end(), target))  {
        //     return {-1, -1};
        // }
        if (find(nums.begin(), nums.end(), target) == nums.end()) {
            return {-1, -1};
        }
        auto first = lower_bound(nums.begin(), nums.end(), target);
        int ans1 = first - nums.begin();
        auto last = upper_bound(nums.begin(), nums.end(), target);
        int ans2 = last - nums.begin() - 1;
        return {ans1, ans2};
    }
};
###


### 74. 搜索二维矩阵


本题可将二维矩阵视为一维排序数组，进而进行常规二分查找，公式题


```cpp
class Solution {
public:
    bool searchMatrix(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix, int target) {
        if (matrix.empty() || matrix[0].empty()) {
            return false;
        }
        int m = matrix.size();
        int n = matrix[0].size();
        int l = 0, r = m * n - 1;
        while (l &amp;lt;= r) {
            int mid = l + (r - l) / 2;
            int col = mid % n;
            int row = mid / n;
            int midValue = matrix[row][col];

            if (midValue == target) {
                return true;
            } else if (midValue &amp;lt; target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }

        return false;
    }
};
###


### 33. 搜索旋转排序数组


```cpp
class Solution {
public:
    int search(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        int left = 0, right = nums.size() - 1;

        while (left &amp;lt;= right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return mid;
            }

            // 左半部分有序
            if (nums[left] &amp;lt;= nums[mid]) {
                if (nums[left] &amp;lt;= target &amp;amp;&amp;amp; target &amp;lt; nums[mid]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            }
            // 右半部分有序
            else {
                if (nums[mid] &amp;lt; target &amp;amp;&amp;amp; target &amp;lt;= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }

        return -1;
    }
};
###


### 153. 寻找旋转排序数组中的最小值


```cpp
class Solution {
public:
    int findMin(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        int l = 0, r = n - 1;
        int res = nums[0];
        while (l &amp;lt;= r) {
            int mid = l + (r - l) / 2;
            res = min(res, nums[mid]);
            if (nums[mid] &amp;gt;= nums[r]) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return res;
    }
};
###


### 154. 寻找旋转排序数组中的最小值 II


 本题相比153，数组可能包含重复元素


Leetcode 153 假设 **没有重复元素**，所以 nums[mid] 和 nums[right] 总是能比较出“最小值在哪边”。


但是154的问题是：nums[mid] == nums[right] 这时无法判断哪边有最小值，要特殊处理。


因此，需要在程序中特殊处理这种情况


```cpp
class Solution {
public:
    int findMin(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int left = 0, right = nums.size() - 1;

        while (left &amp;lt; right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] &amp;lt; nums[right]) {
                // 最小值在左边（包含 mid）
                right = mid;
            } else if (nums[mid] &amp;gt; nums[right]) {
                // 最小值在右边（不包含 mid）
                left = mid + 1;
            } else {
                // nums[mid] == nums[right]，无法判断最小值在哪边
                // 所以只能保守地缩小边界
                right--; // ✅ 注意不能直接排除 mid
            }
        }

        return nums[left];  // 此时 left == right
    }
};
###


### 4. 寻找两个正序数组的中位数


```cpp
class Solution {
public:
    double findMedianSortedArrays(vector&amp;lt;int&amp;gt;&amp;amp; nums1, vector&amp;lt;int&amp;gt;&amp;amp; nums2) {
        int m = nums1.size(), n = nums2.size();
        vector&amp;lt;int&amp;gt; a(m + n);
        int i = 0, j = 0, k = 0;
        while (i &amp;lt; m &amp;amp;&amp;amp; j &amp;lt; n) {
            if (nums1[i] &amp;lt; nums2[j])
                a[k++] = nums1[i++];
            else
                a[k++] = nums2[j++];
        }

        while (i &amp;lt; m) {
            a[k++] = nums1[i++];
        }

        while (j &amp;lt; n) {
            a[k++] = nums2[j++];
        }

        int len = m + n;
        if (len % 2 == 0) {
            return (a[len / 2] + a[len / 2 - 1]) / 2.0;
        } else {
            return a[len / 2];
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;链表&lt;/h2&gt;
&lt;h3&gt;160. 相交链表&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;逻辑讲解：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;temp1 从 headA 开始，temp2 从 headB 开始。&lt;/li&gt;
&lt;li&gt;每次都向下走一个节点，如果走到末尾就跳到另一个链表的头。&lt;/li&gt;
&lt;li&gt;如果两个链表有交点，他们最终会在某一个相交节点相遇（地址相同）。&lt;/li&gt;
&lt;li&gt;如果没有交点，他们都走到 nullptr，循环结束，返回空。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;为什么这样能找到相交点？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为两个指针在&lt;strong&gt;第二次遍历&lt;/strong&gt;时，走的长度是一样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次遍历：temp1 走了 A 的长度，接着走 B；&lt;/li&gt;
&lt;li&gt;temp2 走了 B 的长度，接着走 A；&lt;/li&gt;
&lt;li&gt;所以无论两个链表的长度如何，最终走的路径长度是一样的（A + B）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当相交时，他们在同一个节点相遇；不相交就都走到末尾 nullptr，也会相等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
    public:
        ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
            ListNode* temp1 = headA;
            ListNode* temp2 = headB;
            while(temp1 != temp2) {
                temp1 = temp1 == nullptr ? headB : temp1-&amp;gt;next;
                temp2 = temp2 == nullptr ? headA : temp2-&amp;gt;next;
            }
            return temp1;
        }
    };
###


### 206. 反转链表


1. 迭代法


```cpp
ListNode* reverseList(ListNode* head) {
    ListNode* prev = nullptr;
    ListNode* curr = head;
    while (curr) {
        ListNode* next = curr-&amp;gt;next; // 暂存下一节点
        curr-&amp;gt;next = prev;           // 当前节点指向前一个
        prev = curr;                 // prev 向后移动
        curr = next;                 // curr 向后移动
    }
    return prev;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;递归法&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ListNode* reverseList(ListNode* head) {
    if (head == nullptr || head-&amp;gt;next == nullptr) return head;
    ListNode* newHead = reverseList(head-&amp;gt;next);
    head-&amp;gt;next-&amp;gt;next = head; // 把当前节点挂到后面那个节点的后面
    head-&amp;gt;next = nullptr;    // 当前节点断开原来的 next
    return newHead;
}
###


### 234. 回文链表


我们可以使用快慢指针定位到链表中间位置，反转后半段链表，然后比较前后半段链表


```cpp
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
    public:
        bool isPalindrome(ListNode* head) {
            ListNode* fast = head;
            ListNode* slow = head;
            while(fast &amp;amp;&amp;amp; fast-&amp;gt;next){
                ListNode* temp = fast -&amp;gt; next;
                fast = temp -&amp;gt; next;
                slow = slow -&amp;gt; next;
            }
            //反转后半部分
            ListNode* prev = nullptr;
            ListNode* cur = slow;
            while(cur){
                ListNode*next = cur-&amp;gt;next;
                cur -&amp;gt; next = prev;
                prev = cur;
                cur = next;
            }
            //比较
            ListNode* temp1 = head;
            ListNode* temp2 = prev;
            while(temp2){
                if(temp1-&amp;gt;val!=temp2-&amp;gt;val){
                    return false;
                }
                temp1 = temp1-&amp;gt;next;
                temp2 = temp2-&amp;gt;next;
            }
            return true;
        }
    };
###


### 142. 环形链表 II


依旧是快慢指针，如果快慢指针重合，则说明链表为环形


两个指针相遇以后，其中一个指针从头开始，另一个从相遇点出发。两者每次都走一步，再次相遇点节点就是环形入口。


** 数学证明（简化版）：**


设：


- 头到入环点的距离为 a
- 入环点到相遇点的距离为 b
- 环的长度为 c

相遇时：


- 慢指针走了 a + b 步；
- 快指针走了 a + b + n*c 步（多走了整圈）

由于快指针速度是慢指针的两倍：


2(a + b) = a + b + n*c


化简得：a = n*c - b


这意味着从头到入环点的距离 a，和从相遇点再走 c - b 步，会在入环点相遇。


```cpp
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast &amp;amp;&amp;amp; fast -&amp;gt; next){
            fast = fast -&amp;gt; next -&amp;gt; next;
            slow = slow -&amp;gt; next;
            if(fast == slow){
                ListNode* temp1 = head;
                ListNode* temp2 = slow;
                while(temp1 != temp2){
                    temp1 = temp1-&amp;gt;next;
                    temp2 = temp2-&amp;gt; next;
                }
                return temp1;
            }
        }
        return NULL;
    }
};
###


### 2. 两数相加


类似于高精度加法，只是由数组变换成链表，模拟加法的竖式运算过程


需注意处理进位


```cpp
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* dummy = new ListNode(0);
        ListNode* cur = dummy;
        int carry = 0;
        while (l1 || l2 || carry) {
            int sum = carry;
            if (l1) {
                sum += l1-&amp;gt;val;
                l1 = l1-&amp;gt;next;
            }
            if (l2) {
                sum += l2-&amp;gt;val;
                l2 = l2-&amp;gt;next;
            }
            carry = sum / 10;
            cur-&amp;gt;next = new ListNode(sum % 10);
            cur = cur-&amp;gt;next;
        }
        return dummy-&amp;gt;next;
    }
};
### 19. 删除链表的倒数第 N 个结点


很典型的双指针快慢指针类型，快指针先走n，随后快慢同步走，慢指针停留在倒数第n个节点


```cpp
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0);
        dummy -&amp;gt; next = head;
        ListNode* fast = dummy;
        ListNode* slow = dummy;
        for(int i = 0; i &amp;lt;= n ;i++){
            fast  = fast -&amp;gt; next;
        }
        while(fast){
            fast = fast-&amp;gt; next;
            slow = slow -&amp;gt; next;
        }
        ListNode* nxt = slow -&amp;gt; next;
        slow -&amp;gt; next = nxt -&amp;gt;next;
        delete nxt;
        ListNode* res = dummy -&amp;gt; next;
        delete dummy;
        return res;
    }
};
### 24. 两两交换链表中的节点


```cpp
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummy = new ListNode(0);
        dummy -&amp;gt; next = head;
        ListNode* cur = dummy;
        while(cur -&amp;gt; next &amp;amp;&amp;amp; cur -&amp;gt; next -&amp;gt; next){
            ListNode* temp1 = cur -&amp;gt; next;
            ListNode* temp2 = cur -&amp;gt; next -&amp;gt; next -&amp;gt; next;
            cur -&amp;gt; next = cur -&amp;gt; next -&amp;gt; next;
            cur -&amp;gt; next -&amp;gt; next = temp1;
            cur -&amp;gt; next -&amp;gt; next -&amp;gt; next = temp2;
            cur = cur -&amp;gt; next -&amp;gt; next;
        }
        ListNode* result = dummy -&amp;gt; next;
        delete dummy;
        return result;
    }
};
### 25. K 个一组翻转链表


其实就是上一道题的升级版。 


```cpp
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
    public:
        ListNode* reverseKGroup(ListNode* head, int k) {
            int n = 0;
            //计算链表长度
            for(ListNode* cur = head;cur;cur = cur -&amp;gt; next){
                n++;
            }
            ListNode dummy (0,head);
            ListNode* p0 = &amp;amp;dummy;
            ListNode* prev = nullptr;
            ListNode* cur = head;
            //分成k个一组
            for(; n &amp;gt;= k;n -= k){
                for(int i = 0; i &amp;lt; k;i++){
                    ListNode* next = cur -&amp;gt; next;
                    cur -&amp;gt; next = prev;
                    prev = cur;
                    cur = next;
                }
                // 把翻转后的链表接回前后内容
                ListNode* nxt = p0 -&amp;gt; next;
                p0-&amp;gt;next-&amp;gt;next = cur;
                p0 -&amp;gt; next = prev;
                p0 = nxt;
            }
            return dummy.next;
    }
};
### 138. 随机链表的复制


方法1 ：第一次遍历，复制每个节点，并建立原节点到新节点映射 old -&amp;gt; new; 第二次遍历，链接新节点到next和random。返回新链表的头节点。


```cpp
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;

        unordered_map&amp;lt;Node*, Node*&amp;gt; oldToNew;

        // 第一步：复制每个节点
        for (Node* cur = head; cur; cur = cur-&amp;gt;next) {
            oldToNew[cur] = new Node(cur-&amp;gt;val);
        }

        // 第二步：连边
        for (Node* cur = head; cur; cur = cur-&amp;gt;next) {
            oldToNew[cur]-&amp;gt;next = oldToNew[cur-&amp;gt;next];
            oldToNew[cur]-&amp;gt;random = oldToNew[cur-&amp;gt;random];
        }

        return oldToNew[head];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法2：在每个节点后面插入一个副本，比如 A → A’ → B → B’ …，第二遍设置副本的random，比如   A’-&amp;gt;random = A-&amp;gt;random-&amp;gt;next；最后拆分链表，把原始链表和副本链表分开&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;

        // 第一步：在每个节点后插入复制节点
        for (Node* cur = head; cur;) {
            Node* copy = new Node(cur-&amp;gt;val);
            copy-&amp;gt;next = cur-&amp;gt;next;
            cur-&amp;gt;next = copy;
            cur = copy-&amp;gt;next;
        }

        // 第二步：复制 random 指针
        for (Node* cur = head; cur; cur = cur-&amp;gt;next-&amp;gt;next) {
            if (cur-&amp;gt;random) {
                cur-&amp;gt;next-&amp;gt;random = cur-&amp;gt;random-&amp;gt;next;
            }
        }

        // 第三步：拆分两个链表
        Node* dummy = new Node(0);
        Node* copyCur = dummy;
        for (Node* cur = head; cur;) {
            Node* copy = cur-&amp;gt;next;
            copyCur-&amp;gt;next = copy;
            copyCur = copy;

            cur-&amp;gt;next = copy-&amp;gt;next;
            cur = cur-&amp;gt;next;
        }

        return dummy-&amp;gt;next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很多同学可能会疑惑：一道链表题怎么要用到哈希表呢？&lt;/p&gt;
&lt;p&gt;本题的特点在于：普通的复制链表，只要复制next就可以了，而这道题中有random。你不能一边复制一遍正确指向random，因为你不知道random指向的新节点在哪&lt;/p&gt;
&lt;p&gt;哈希表的作用： 记录原节点和新节点的对应关系。当遇到某个random指向的原节点时，可以马上找到对应新节点。是不是非常美妙？&lt;/p&gt;
&lt;h3&gt;148. 排序链表&lt;/h3&gt;
&lt;p&gt;本题可以借用归并排序的思想，然后新建一个合并链表函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (!head || !head-&amp;gt;next) return head;

        // 找中点
        ListNode* slow = head, *fast = head-&amp;gt;next;
        while (fast &amp;amp;&amp;amp; fast-&amp;gt;next) {
            slow = slow-&amp;gt;next;
            fast = fast-&amp;gt;next-&amp;gt;next;
        }

        // 分割
        ListNode* mid = slow-&amp;gt;next;
        slow-&amp;gt;next = nullptr;

        // 递归排序左右部分
        ListNode* left = sortList(head);
        ListNode* right = sortList(mid);

        // 合并
        return merge(left, right);
    }

    ListNode* merge(ListNode* l1, ListNode* l2) {
        ListNode dummy(0), *tail = &amp;amp;dummy;
        while (l1 &amp;amp;&amp;amp; l2) {
            if (l1-&amp;gt;val &amp;lt; l2-&amp;gt;val) {
                tail-&amp;gt;next = l1;
                l1 = l1-&amp;gt;next;
            } else {
                tail-&amp;gt;next = l2;
                l2 = l2-&amp;gt;next;
            }
            tail = tail-&amp;gt;next;
        }
        tail-&amp;gt;next = l1 ? l1 : l2;
        return dummy.next;
    }
};
### 23. 合并 K 个升序链表


本题虽然标记为困难，但是可以不断调用合并两个升序链表暴力合并k个升序链表


```cpp
class Solution {
public:
    ListNode* mergetwoLists(ListNode* list1, ListNode* list2) {
        ListNode* dummy = new ListNode(0);
        ListNode* tail = dummy;
        while (list1 &amp;amp;&amp;amp; list2) {
            if (list1-&amp;gt;val &amp;lt; list2-&amp;gt;val) {
                tail-&amp;gt;next = list1;
                list1 = list1-&amp;gt;next;
            } else {
                tail-&amp;gt;next = list2;
                list2 = list2-&amp;gt;next;
            }
            tail = tail-&amp;gt;next;
        }
        if (list1) {
            tail-&amp;gt;next = list1;
        } else {
            tail-&amp;gt;next = list2;
        }
        return dummy-&amp;gt;next;
    }
    ListNode* mergeKLists(vector&amp;lt;ListNode*&amp;gt;&amp;amp; lists) {
        if (lists.empty()) {
            return nullptr;
        }
        ListNode* dummy = new ListNode(0);
        int n = lists.size();
        for (int i = 0; i &amp;lt; n; i++) {
            if (lists[i]) {
                dummy-&amp;gt;next = mergetwoLists(dummy-&amp;gt;next, lists[i]);
            }
        }
        return dummy-&amp;gt;next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;普通数组&lt;/h2&gt;
&lt;h3&gt;53. 最大子数组和&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxSubArray(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        int ans = nums[0];
        int currentSum = nums[0];
        for (int i = 1; i &amp;lt; n; i++) {
            currentSum = max(nums[i], currentSum + nums[i]);
            ans = max(ans, currentSum);
        }
        return ans;
    }
};
### 56. 合并区间


先按照左端点排序，用一个结果数组result来合并区间。如果当前区间跟result的最后一个区间没有重叠，直接添加；如果有重叠，修改result最后一个区间的右端点


```cpp
class Solution {
public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; merge(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; intervals) {
        if (intervals.size() == 0) {
            return {};
        }
        sort(intervals.begin(), intervals.end());
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; merged;
        for (int i = 0; i &amp;lt; intervals.size(); ++i) {
            int L = intervals[i][0], R = intervals[i][1];
            if (!merged.size() || merged.back()[1] &amp;lt; L) {
                merged.push_back({L, R});
            }
            else {
                merged.back()[1] = max(merged.back()[1], R);
            }
        }
        return merged;
    }
};
### 189. 轮转数组


可以自己手动模拟一下


```cpp
class Solution {
public:
    void rotate(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        int n = nums.size();
        k = k % n;
        reverse(nums.begin(), nums.end());          //整体翻转
        reverse(nums.begin(), nums.begin() + k);    //翻转前k个
        reverse(nums.begin() + k, nums.end());      //翻转后n-k个
    }
};
### 238. 除自身以外数组的乘积


```cpp
class Solution {
public:
    vector&amp;lt;int&amp;gt; productExceptSelf(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        vector&amp;lt;int&amp;gt; answer(n, 1);

        // 先计算左边所有乘积
        for (int i = 1; i &amp;lt; n; ++i) {
            answer[i] = answer[i - 1] * nums[i - 1];
        }

        // 再从右往左乘
        int right = 1;
        for (int i = n - 1; i &amp;gt;= 0; --i) {
            answer[i] *= right;
            right *= nums[i];
        }

        return answer;
    }
};
### 41. 缺失的第一个正数


本题核心目标是让第 i 位上的数字是 i + 1。


** 完整步骤**


- 遍历整个数组： 如果 nums[i] 是合法数字（1 ~ n）并且没在正确位置上，
- 就交换 nums[i] 和 nums[nums[i] - 1]。
- 注意是**不断交换直到当前位置合法**！

- 再次遍历数组： 找第一个 nums[i] != i+1 的位置，
- 答案就是 i+1。

- 如果遍历完都符合，就返回 n+1。


```cpp
class Solution {
public:
    int firstMissingPositive(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();

        for (int i = 0; i &amp;lt; n; ++i) {
            while (nums[i] &amp;gt; 0 &amp;amp;&amp;amp; nums[i] &amp;lt;= n &amp;amp;&amp;amp; nums[nums[i]-1] != nums[i]) {
                swap(nums[i], nums[nums[i] - 1]);
            }
        }

        for (int i = 0; i &amp;lt; n; ++i) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }

        return n + 1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动态规划&lt;/h2&gt;
&lt;h3&gt;322. 零钱兑换&lt;/h3&gt;
&lt;p&gt;很常规的完全背包dp，需要注意： 完全背包中内循环外循环均为正序遍历&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int coinChange(vector&amp;lt;int&amp;gt;&amp;amp; coins, int amount) {
        const int INF = amount + 1;
        vector&amp;lt;int&amp;gt; dp(amount + 1, INF);
        dp[0] = 0;

        for (int coin : coins) {
            for (int i = coin; i &amp;lt;= amount; ++i) {
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }

        return dp[amount] == INF ? -1 : dp[amount];
    }
};
### 213. 打家劫舍 II


hot100中本来为打家劫舍1，但是2中也有1的模板且多了一层思维，故替换为2.


因本题中房屋为环形，为最大程度利用1中的代码，我们可以分成两种情况：


1. 不偷第一个


2. 不偷最后一个


每种情况都可以应用打家劫舍1的代码，最后讲两种情况取一个最大值即可


```cpp
class Solution {
public:
    int robb(vector&amp;lt;int&amp;gt;&amp;amp; nums, int i, int j) {
        int n = j - i + 1;
        if (n == 1) {
            return nums[i];
        }
        vector&amp;lt;int&amp;gt; dp(n, 0);
        dp[0] = nums[i];
        dp[1] = max(nums[i], nums[i + 1]);
        for (int k = 2; k &amp;lt; n; k++) {
            dp[k] = max(dp[k - 1], dp[k - 2] + nums[i + k]);
        }
        return dp[n - 1];
    }
    int rob(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        if (n == 0) return 0;
        if (n == 1) return nums[0];
        //不偷第一个
        int case1 = robb(nums, 1, n - 1);
        //不偷最后一个
        int case2 = robb(nums, 0, n - 2);
        return max(case1, case2);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意：robb函数在添加左右范围后dp数组的初始值和递归公式都会有细微的变化（以前基于0，现在基于i）因此一个重新整理思路而非盲目默写﫡&lt;/p&gt;
&lt;h3&gt;279. 完全平方数&lt;/h3&gt;
&lt;p&gt;需要一些小小的应变，主要还是考察对动态规划双层遍历的理解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numSquares(int n) {
        vector&amp;lt;int&amp;gt; dp(n + 1, INT_MAX);
        dp[0] = 0;

        for (int i = 1; i &amp;lt;= n; ++i) {
            for (int j = 1; j * j &amp;lt;= i; ++j) {
                dp[i] = min(dp[i], dp[i - j * j] + 1);
            }
        }

        return dp[n];
    }
};
### 322. 零钱兑换


基本还是不需要变通，乱套公式即可得分


```cpp
class Solution {
public:
    int coinChange(vector&amp;lt;int&amp;gt;&amp;amp; coins, int amount) {
        const int INF = amount + 1;
        vector&amp;lt;int&amp;gt; dp(amount + 1, INF);
        dp[0] = 0;

        for (int coin : coins) {
            for (int i = coin; i &amp;lt;= amount; ++i) {
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }

        return dp[amount] == INF ? -1 : dp[amount];
    }
};
### 139. 单词拆分


我们定义一个bool数组dp，dp[i] = true / false 表示前i个字符能不能被拆分


```cpp
class Solution {
public:
    bool wordBreak(string s, vector&amp;lt;string&amp;gt;&amp;amp; wordDict) {
        unordered_set&amp;lt;string&amp;gt; dict(wordDict.begin(), wordDict.end());
        int n = s.size();
        vector&amp;lt;bool&amp;gt; dp(n + 1, false);
        dp[0] = true; // 空串可以拆分

        for (int i = 1; i &amp;lt;= n; ++i) {
            for (int j = 0; j &amp;lt; i; ++j) {
                if (dp[j] &amp;amp;&amp;amp; dict.count(s.substr(j, i - j))) {
                    dp[i] = true;
                    break; // 找到一种拆分方式就可以停止了
                }
            }
        }

        return dp[n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;栈&lt;/h2&gt;
&lt;p&gt;栈常用于具有后进先出特性的题目，常用于匹配或寻找临近元素&lt;/p&gt;
&lt;h3&gt;20. 有效的括号&lt;/h3&gt;
&lt;p&gt;一道很经典的栈匹配算法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        if (s.size() % 2 != 0) {
            return false;
        }
        stack&amp;lt;char&amp;gt; stk;
        for (char c : s) {
            if (c == &apos;(&apos;) {
                stk.push(&apos;)&apos;);
            } else if (c == &apos;[&apos;) {
                stk.push(&apos;]&apos;);
            } else if (c == &apos;{&apos;) {
                stk.push(&apos;}&apos;);
            } else if (stk.empty() || stk.top() != c) {
                return false;
            } else {
                stk.pop();
            }
        }
        return stk.empty();
    }
};
### 394. 字符串解码


本题特点在于字符串括号内重复嵌套括号，因此需要用到栈的知识来匹配。使用辅助栈来解题，需注意，如果录入的是数字，需将数字前一位 *10 


```cpp
class Solution {
public:
    string decodeString(string s) {
    stack&amp;lt;int&amp;gt; countStack;
    stack&amp;lt;string&amp;gt; stringStack;
    string currentString = &quot;&quot;;
    int currentNum = 0;

    for (char ch : s) {
        if (isdigit(ch)) {
            currentNum = currentNum * 10 + (ch - &apos;0&apos;);

        } else if (ch == &apos;[&apos;) {
            countStack.push(currentNum);
            stringStack.push(currentString);
            currentString = &quot;&quot;;
            currentNum = 0;
        } else if (ch == &apos;]&apos;) {
            string prevString = stringStack.top(); stringStack.pop();
            int repeatTimes = countStack.top(); countStack.pop();
            string temp = &quot;&quot;;
            for (int i = 0; i &amp;lt; repeatTimes; ++i) {
                temp += currentString;
            }
            currentString = prevString + temp;
        } else {
            currentString += ch;
        }
    }
    return currentString;
    }
};
### 739. 每日温度


本题为单调栈的经典例题。维护一个存储下标的单调栈，从栈底到栈顶的下标对应的温度列表中的温度依次递减。如果一个下标在单调栈里，则表示尚未找到下一次温度更高的下标。


正向遍历温度列表。对于温度列表中的每个元素 temperatures[i]，如果栈为空，则直接将 i 进栈，如果栈不为空，则比较栈顶元素 prevIndex 对应的温度 temperatures[prevIndex] 和当前温度 temperatures[i]，如果 temperatures[i] &amp;gt; temperatures[prevIndex]，则将 prevIndex 移除，并将 prevIndex 对应的等待天数赋为 i - prevIndex，重复上述操作直到栈为空或者栈顶元素对应的温度小于等于当前温度，然后将 i 进栈。


为什么可以在弹栈的时候更新 ans[prevIndex] 呢？因为在这种情况下，即将进栈的 i 对应的 temperatures[i] 一定是 temperatures[prevIndex] 右边第一个比它大的元素，试想如果 prevIndex 和 i 有比它大的元素，假设下标为 j，那么 prevIndex 一定会在下标 j 的那一轮被弹掉。


由于单调栈满足从栈底到栈顶元素对应的温度递减，因此每次有元素进栈时，会将温度更低的元素全部移除，并更新出栈元素对应的等待天数，这样可以确保等待天数一定是最小的。


以下用一个具体的例子帮助读者理解单调栈。对于温度列表 [73,74,75,71,69,72,76,73]，单调栈 stack 的初始状态为空，答案 ans 的初始状态是 [0,0,0,0,0,0,0,0]，按照以下步骤更新单调栈和答案，其中单调栈内的元素都是下标，括号内的数字表示下标在温度列表中对应的温度。


```cpp
class Solution {
public:
    vector&amp;lt;int&amp;gt; dailyTemperatures(vector&amp;lt;int&amp;gt;&amp;amp; temperatures) {
        int n = temperatures.size();
        vector&amp;lt;int&amp;gt; ans(n);
        stack&amp;lt;int&amp;gt; s;
        for (int i = 0; i &amp;lt; n; ++i) {
            while (!s.empty() &amp;amp;&amp;amp; temperatures[i] &amp;gt; temperatures[s.top()]) {
                int previousIndex = s.top();
                ans[previousIndex] = i - previousIndex;
                s.pop();
            }
            s.push(i);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;矩阵&lt;/h2&gt;
&lt;p&gt;矩阵系列问题更多的是数组和模拟思路的结合，主要考察代码能力而非算法思想&lt;/p&gt;
&lt;h3&gt;73. 矩阵置零&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    void setZeroes(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        vector&amp;lt;int&amp;gt; row(m), col(n);
        for (int i = 0; i &amp;lt; m; i++) {
            for (int j = 0; j &amp;lt; n; j++) {
                if (!matrix[i][j]) {
                    row[i] = col[j] = true;
                }
            }
        }
        for (int i = 0; i &amp;lt; m; i++) {
            for (int j = 0; j &amp;lt; n; j++) {
                if (row[i] || col[j]) {
                    matrix[i][j] = 0;
                }
            }
        }
    }
};
### 54. 螺旋矩阵


一道非常考验代码能力的题，层层循环的嵌套控制边界。在写代码前整理好思路 


```cpp
class Solution {
public:
    vector&amp;lt;int&amp;gt; spiralOrder(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix) {
        vector&amp;lt;int&amp;gt; result;
        if (matrix.empty() || matrix[0].empty())
            return result;

        int m = matrix.size();    // 行数
        int n = matrix[0].size(); // 列数

        int top = 0, bottom = m - 1;
        int left = 0, right = n - 1;

        while (top &amp;lt;= bottom &amp;amp;&amp;amp; left &amp;lt;= right) {
            // 从左到右
            for (int j = left; j &amp;lt;= right; ++j) {
                result.push_back(matrix[top][j]);
            }
            top++;

            // 从上到下
            for (int i = top; i &amp;lt;= bottom; ++i) {
                result.push_back(matrix[i][right]);
            }
            right--;

            // 从右到左（注意是否还存在这一行）
            if (top &amp;lt;= bottom) {
                for (int j = right; j &amp;gt;= left; --j) {
                    result.push_back(matrix[bottom][j]);
                }
                bottom--;
            }

            // 从下到上（注意是否还存在这一列）
            if (left &amp;lt;= right) {
                for (int i = bottom; i &amp;gt;= top; --i) {
                    result.push_back(matrix[i][left]);
                }
                left++;
            }
        }

        return result;
    }
};
### 48. 旋转图像


```cpp
class Solution {
public:
    void rotate(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix) {
        int n = matrix.size();
        // 深拷贝 matrix -&amp;gt; tmp
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; tmp = matrix;
        // 根据元素旋转公式，遍历修改原矩阵 matrix 的各元素
        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; n; j++) {
                matrix[j][n - 1 - i] = tmp[i][j];
            }
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147483386&quot;&gt;Leetcode hot 100 个人总结（持续更新）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>二叉树的遍历</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147519839/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147519839/</guid><description>前言： 什么是二叉树遍历？ 遍历就是“按照一定的顺序访问树中的每一个节点”，常见顺序包括： 前序遍历（ 根 → 左 → 右 ） 中序遍历（ 左 → 根 → 右 ） 后序遍历（ 左 → 右 → 根 ） 层序遍历（ 一层一层访问，从上到下，从左到右 ） 我们重点讲解 前、中、后序 的</description><pubDate>Sat, 26 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;什么是二叉树遍历？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;遍历就是“按照一定的顺序访问树中的每一个节点”，常见顺序包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前序遍历（&lt;strong&gt;根 → 左 → 右&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;中序遍历（&lt;strong&gt;左 → 根 → 右&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;后序遍历（&lt;strong&gt;左 → 右 → 根&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;层序遍历（&lt;strong&gt;一层一层访问，从上到下，从左到右&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们重点讲解&lt;strong&gt;前、中、后序&lt;/strong&gt;的&lt;strong&gt;递归和迭代两种实现&lt;/strong&gt;，最后也讲讲层序遍历。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;一、前序遍历（Preorder Traversal）&lt;/h3&gt;
&lt;h4&gt;✅ 顺序：&lt;/h4&gt;
&lt;h4&gt;根 → 左 → 右&lt;/h4&gt;
&lt;h4&gt;✅ 递归代码：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;void preoerder(treeNode* root) {
    if (!root) {
        return;
    }
    cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;   //根   leetcode一般存到一个数组里
    preoerder(root-&amp;gt;left);      //左
    preoerder(root-&amp;gt;right);     //右
}
#### ✅ 迭代实现：


```cpp
void preorder(TreeNode* root) {
    if (!root) return;
    stack&amp;lt;TreeNode*&amp;gt; st;
    st.push(root);
    while (!st.empty()) {
        TreeNode* node = st.top(); st.pop();
        cout &amp;lt;&amp;lt; node-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
        if (node-&amp;gt;right) st.push(node-&amp;gt;right);  // 注意：先右
        if (node-&amp;gt;left) st.push(node-&amp;gt;left);
    }
}
### 二、中序遍历（Inorder Traversal）


#### ✅ 顺序：


#### 左 → 根 → 右


#### ✅ 递归代码：


```cpp
void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root-&amp;gt;left);          // 遍历左子树
    cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;     // 访问根
    inorder(root-&amp;gt;right);         // 遍历右子树
}
#### ✅ 迭代实现：


```cpp
class Solution {
public:
    vector&amp;lt;int&amp;gt; inorderTraversal(TreeNode* root) {
        vector&amp;lt;int&amp;gt; result;
        stack&amp;lt;TreeNode*&amp;gt; st;
        TreeNode* cur = root;
        while (cur != NULL || !st.empty()) {
            if (cur != NULL) { // 指针来访问节点，访问到最底层
                st.push(cur); // 将访问的节点放进栈
                cur = cur-&amp;gt;left;                // 左
            } else {
                cur = st.top(); // 从栈里弹出的数据，就是要处理的数据（放进result数组里的数据）
                st.pop();
                result.push_back(cur-&amp;gt;val);     // 中
                cur = cur-&amp;gt;right;               // 右
            }
        }
        return result;
    }
};
### 三、后序遍历（Postorder Traversal）


#### ✅ 顺序：


#### 左 → 右 → 根


#### ✅ 递归代码：


```cpp
void postorder(TreeNode* root) {
    if (!root) return;
    postorder(root-&amp;gt;left);        // 遍历左子树
    postorder(root-&amp;gt;right);       // 遍历右子树
    cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;     // 访问根
}
#### ✅ 迭代技巧实现：


**方法：“根-&amp;gt;右-&amp;gt;左”后反转**


```cpp
void postorder(TreeNode* root) {
    if (!root) return;
    stack&amp;lt;TreeNode*&amp;gt; st;
    vector&amp;lt;int&amp;gt; result;
    st.push(root);
    while (!st.empty()) {
        TreeNode* node = st.top(); st.pop();
        result.push_back(node-&amp;gt;val); // 根
        if (node-&amp;gt;left) st.push(node-&amp;gt;left);   // 左
        if (node-&amp;gt;right) st.push(node-&amp;gt;right); // 右
    }
    reverse(result.begin(), result.end());
    for (int v : result) cout &amp;lt;&amp;lt; v &amp;lt;&amp;lt; &quot; &quot;;
}
### 四、层序遍历（Level-order Traversal）


**✅ 顺序：**


#### 按层访问，每层从左到右


**✅ 使用队列（queue）**


```cpp
vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; levelOrder(TreeNode* root) {
    queue&amp;lt;TreeNode*&amp;gt; que;
    if (root != NULL) que.push(root);
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; result;
    while (!que.empty()) {
        int size = que.size();
        vector&amp;lt;int&amp;gt; vec;
        for (int i = 0; i &amp;lt; size; i++) {
            TreeNode* node = que.front(); que.pop();
            vec.push_back(node-&amp;gt;val);
            if (node-&amp;gt;left) que.push(node-&amp;gt;left);
            if (node-&amp;gt;right) que.push(node-&amp;gt;right);
        }
        result.push_back(vec);
    }
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147519839&quot;&gt;二叉树的遍历&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>动态规划分享之 —— 买卖股票的最佳时机</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147234256--/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147234256--/</guid><description>我今天分享的是关于动态规划中最有名的一组题目——股票买卖问题。为什么选它？因为它覆盖了大部分DP的建模套路，同时题意又很好理解，非常适合入门。 DP 类型 简要说明 典型例子 1. 线性 DP 当前状态只与前一两个状态有关 斐波那契数列、爬楼梯、打家劫舍 2. 区间 DP 处理“</description><pubDate>Sun, 20 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我今天分享的是关于动态规划中最有名的一组题目——股票买卖问题。为什么选它？因为它覆盖了大部分DP的建模套路，同时题意又很好理解，非常适合入门。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DP 类型&lt;/strong&gt; &lt;strong&gt;简要说明&lt;/strong&gt; &lt;strong&gt;典型例子&lt;/strong&gt; 1. 线性 DP 当前状态只与前一两个状态有关 斐波那契数列、爬楼梯、打家劫舍 2. 区间 DP 处理“区间”上问题 括号匹配、石子合并 3. 背包 DP 决策是否选某个物品 01 背包、完全背包、多重背包 4. 树形 DP 在树结构上处理最优解 树的直径、选点问题 5. 状压 DP 用二进制压缩状态集合 旅行商问题、Domino 覆盖 6. 记忆化搜索 用递归 + 记忆表解决 DP 问题 所有 DP 都可以转化为搜索 7. 状态机 DP 明确的状态转换模型 买卖股票、任务调度、限制性路径选择 8. 多维 DP 状态有多个维度（例如天数+状态） 买卖股票（第 i 天 + 状态） 9. 插头 DP 用于网格问题、覆盖问题（较高级） Grid覆盖、最小路径覆盖&lt;/p&gt;
&lt;h2&gt;题目目录：&lt;/h2&gt;
&lt;p&gt;简单 &lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/&quot;&gt;121. 买卖股票的最佳时机&lt;/a&gt; 记录最小值，贪心 or DP  中等 &lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/&quot;&gt;122. 买卖股票的最佳时机 II&lt;/a&gt; 多次交易，0/1状态DP  中等 &lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/&quot;&gt;123. 买卖股票的最佳时机 III&lt;/a&gt; 最多交易两次，状态维度升级  困难 &lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/&quot;&gt;188. 买卖股票的最佳时机 IV&lt;/a&gt; 最多交易 k 次，通用模型  中等 &lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/&quot;&gt;309. 最佳买卖股票时机含冷冻期&lt;/a&gt; 状态带冷冻，状态机思想  中等 &lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/&quot;&gt;714. 买卖股票的最佳时机含手续费&lt;/a&gt; 状态机 + 手续费处理&lt;/p&gt;
&lt;h2&gt; 什么是状态机 DP？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;状态机 DP&lt;/strong&gt; 本质上是：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;把题目的操作过程，抽象成“状态”之间的转移关系&lt;/strong&gt;，&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;每种“状态”表示一种特定的场景，&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;然后我们用 DP 来计算从起点状态走到终点状态的最大/最小值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它广泛用于 &lt;strong&gt;具有阶段性、操作流程清晰的问题&lt;/strong&gt;，比如股票交易、机器人走格子、任务调度等。&lt;/p&gt;
&lt;h3&gt; 为什么叫“状态机”？&lt;/h3&gt;
&lt;p&gt;我们借用了**有限状态自动机（Finite State Machine，FSM）**的思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态：比如“持有股票”或“未持有股票”&lt;/li&gt;
&lt;li&gt;操作：比如“买入”、“卖出”、“什么都不做”&lt;/li&gt;
&lt;li&gt;状态转移图：画出状态之间的合法转移&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;就像一台机器在不同状态之间切换，直到最终完成任务。&lt;/p&gt;
&lt;h3&gt;通俗理解：&lt;/h3&gt;
&lt;p&gt;比如你在打游戏：&lt;/p&gt;
&lt;p&gt;你当前是 &lt;strong&gt;“未装备武器”&lt;/strong&gt; 状态；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;捡到武器后进入 &lt;strong&gt;“已装备武器”&lt;/strong&gt; 状态；&lt;/li&gt;
&lt;li&gt;打了一场仗后你又 &lt;strong&gt;失去武器&lt;/strong&gt;，回到 &lt;strong&gt;“未装备武器”&lt;/strong&gt; 状态……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是“状态”和“状态转移”！&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;121. 买卖股票的最佳时机&lt;/h2&gt;
&lt;p&gt;给定一个数组 &lt;code&gt;prices&lt;/code&gt; ，它的第 &lt;code&gt;i&lt;/code&gt; 个元素 &lt;code&gt;prices[i]&lt;/code&gt; 表示一支给定股票第 &lt;code&gt;i&lt;/code&gt; 天的价格。&lt;/p&gt;
&lt;p&gt;你只能选择 &lt;strong&gt;某一天&lt;/strong&gt; 买入这只股票，并选择在 &lt;strong&gt;未来的某一个不同的日子&lt;/strong&gt; 卖出该股票。设计一个算法来计算你所能获取的最大利润。&lt;/p&gt;
&lt;p&gt;返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 &lt;code&gt;0&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：[7,1,5,3,6,4]
输出：5
解释：在第 2 天（股票价格 = 1）的时候买入，在第 5 天（股票价格 = 6）的时候卖出，最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [7,6,4,3,1]
输出：0
解释：在这种情况下, 没有交易完成, 所以最大利润为 0。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;122. 买卖股票的最佳时机 II&lt;/h2&gt;
&lt;p&gt;**总而言之，与1的区别就是可以进行任意次交易，但不可同时参与多笔交易 **&lt;/p&gt;
&lt;p&gt;详细题目如下：&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;prices&lt;/code&gt; ，其中 &lt;code&gt;prices[i]&lt;/code&gt; 表示某支股票第 &lt;code&gt;i&lt;/code&gt; 天的价格。&lt;/p&gt;
&lt;p&gt;在每一天，你可以决定是否购买和/或出售股票。你在任何时候 &lt;strong&gt;最多&lt;/strong&gt; 只能持有 &lt;strong&gt;一股&lt;/strong&gt; 股票。你也可以先购买，然后在 &lt;strong&gt;同一天&lt;/strong&gt; 出售。&lt;/p&gt;
&lt;p&gt;返回 &lt;em&gt;你能获得的 &lt;strong&gt;最大&lt;/strong&gt; 利润&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [7,1,5,3,6,4]
输出：7
解释：在第 2 天（股票价格 = 1）的时候买入，在第 3 天（股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后，在第 4 天（股票价格 = 3）的时候买入，在第 5 天（股票价格 = 6）的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [1,2,3,4,5]
输出：4
解释：在第 1 天（股票价格 = 1）的时候买入，在第 5 天 （股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
最大总利润为 4 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [7,6,4,3,1]
输出：0
解释：在这种情况下, 交易无法获得正利润，所以不参与交易可以获得最大利润，最大利润为 0。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;123. 买卖股票的最佳时机 III&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;在问题上中，我们可以进行  &amp;lt;= 2 笔交易，这是与问题2的不同&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;给定一个数组，它的第_ &lt;em&gt;&lt;code&gt;i&lt;/code&gt; 个元素是一支给定的股票在第 &lt;code&gt;i&lt;/code&gt;&lt;/em&gt; _天的价格。&lt;/p&gt;
&lt;p&gt;设计一个算法来计算你所能获取的最大利润。你最多可以完成 **两笔 **交易。&lt;/p&gt;
&lt;p&gt;**注意：**你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [3,3,5,0,0,3,1,4]
输出：6
解释：在第 4 天（股票价格 = 0）的时候买入，在第 6 天（股票价格 = 3）的时候卖出，这笔交易所能获得利润 = 3-0 = 3 。
     随后，在第 7 天（股票价格 = 1）的时候买入，在第 8 天 （股票价格 = 4）的时候卖出，这笔交易所能获得利润 = 4-1 = 3 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [1,2,3,4,5]
输出：4
解释：在第 1 天（股票价格 = 1）的时候买入，在第 5 天 （股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。  
     注意你不能在第 1 天和第 2 天接连购买股票，之后再将它们卖出。  
     因为这样属于同时参与了多笔交易，你必须在再次购买前出售掉之前的股票。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [7,6,4,3,1]
输出：0
解释：在这个情况下, 没有交易完成, 所以最大利润为 0。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 4：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [1]
输出：0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`1&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;思路：&lt;/h4&gt;
&lt;h5&gt;三维：&lt;/h5&gt;
&lt;p&gt;这题关键在于“&lt;strong&gt;最多交易两次&lt;/strong&gt;”，所以我们要引入一个新的维度：&lt;strong&gt;交易次数 k（最多 2）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;状态定义：&lt;/p&gt;
&lt;p&gt;我们用 dp[i][k][j] 表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 i 天（从 0 开始）&lt;/li&gt;
&lt;li&gt;你最多还能进行 k 次交易（k ∈ {0,1,2}）&lt;/li&gt;
&lt;li&gt;当前是否持股：j = 0 表示未持股，j = 1 表示持股&lt;/li&gt;
&lt;li&gt;dp[i][k][j]：表示在这些条件下的 &lt;strong&gt;最大利润&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据上两题的基础，我们可得出如下状态转移方程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 不持股
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
// 今天不持股 = max(昨天就不持股，昨天持股今天卖出)

// 持股
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
// 今天持股 = max(昨天就持股，昨天不持股今天买入一次（交易数-1）)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;初始化：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[0][0][0] = 0;             // 第0天不持股0次交易
dp[0][1][0] = 0;
dp[0][2][0] = 0;

dp[0][0][1] = -INF;          // 第0天持股0次交易（非法）
dp[0][1][1] = -prices[0];    // 第0天持股1次交易
dp[0][2][1] = -prices[0];    // 第0天持股2次交易
##### 二维：


既然是状态机dp，那我们首先要确定他现在的状态种类表示


**状态编号** **状态含义** 0 什么也没做 1 第一次买入 2 第一次卖出 3 第二次买入（在第一次卖出之后） 4 第二次卖出（最终状态）


需要注意：dp[i][1]，**表示的是第i天，买入股票的状态，并不是说一定要第i天买入股票，这是很多同学容易陷入的误区**。 


**不急，我们一步一步推导：**


达到dp[i][1]状态，有两个具体操作：


- 操作一：第i天买入股票了，那么dp[i][1] = dp[i-1][0] - prices[i]
- 操作二：第i天没有操作，而是沿用前一天买入的状态，即：dp[i][1] = dp[i - 1][1]

那么dp[i][1]究竟选 dp[i-1][0] - prices[i]，还是dp[i - 1][1]呢？


一定是选最大的，所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);


同理dp[i][2]也有两个操作：


- 操作一：第i天卖出股票了，那么dp[i][2] = dp[i - 1][1] + prices[i]
- 操作二：第i天没有操作，沿用前一天卖出股票的状态，即：dp[i][2] = dp[i - 1][2]

所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])


同理可推出剩下状态部分：


dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);


dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);


**有了这些不够，还要了解dp数组如何初始化。**


第0天没有操作，这个最容易想到，就是0，即：dp[0][0] = 0;


第0天做第一次买入的操作，dp[0][1] = -prices[0];


第0天做第一次卖出的操作，这个初始值应该是多少呢？


此时还没有买入，怎么就卖出呢？ 其实大家可以理解当天买入，当天卖出，所以dp[0][2] = 0;


第0天第二次买入操作，初始值应该是多少呢？应该不少同学疑惑，第一次还没买入呢，怎么初始化第二次买入呢？


第二次买入依赖于第一次卖出的状态，其实相当于第0天第一次买入了，第一次卖出了，然后再买入一次（第二次买入），那么现在手头上没有现金，只要买入，现金就做相应的减少。


所以第二次买入操作，初始化为：dp[0][3] = -prices[0];


同理第二次卖出初始化dp[0][4] = 0;


以上这些分析完，写出代码是不是就轻轻松松了﫣


**其实，还有另一种方法：压缩  i**


```cpp
dp[k][j]

// k表示目前进行了几次交易，j表示是否持股
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;状态转移方程因此变为：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[k][0] = max(dp[k][0], dp[k][1] + price);       // 今天不持股
dp[k][1] = max(dp[k][1], dp[k-1][0] - price);     // 今天持股（进行了一次买入）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意顺序：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;必须从 &lt;strong&gt;k = 2 到 k = 1&lt;/strong&gt; 倒序更新！&lt;/p&gt;
&lt;p&gt;为什么？因为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[k][1] = max(dp[k][1], dp[k-1][0] - price);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你顺序更新，会把dp[k - 1][0] 覆盖成当前的值，导致出错！&lt;/p&gt;
&lt;h4&gt;代码：&lt;/h4&gt;
&lt;h5&gt;三维：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
    int n = prices.size();
    if (n == 0) return 0;

    vector&amp;lt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;gt; dp(n, vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;(3, vector&amp;lt;int&amp;gt;(2)));

    // 初始化
    for (int k = 1; k &amp;lt;= 2; ++k) {
        dp[0][k][0] = 0;
        dp[0][k][1] = -prices[0];
    }

    for (int i = 1; i &amp;lt; n; ++i) {
        for (int k = 1; k &amp;lt;= 2; ++k) {
            dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
        }
    }

    return dp[n-1][2][0]; // 最后一天，最多交易两次，不持股
}
##### 二维：


```cpp
class Solution {
public:
    int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        int n = prices.size();
        if (n == 0) return 0;

        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(n, vector&amp;lt;int&amp;gt;(5, 0));

        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        dp[0][4] = 0;

        for (int i = 1; i &amp;lt; n; ++i) {
            dp[i][0] = dp[i-1][0];
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);
            dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]);
            dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i]);
            dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i]);
        }

        return max({dp[n-1][0], dp[n-1][2], dp[n-1][4]});
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(3, vector&amp;lt;int&amp;gt;(2, 0));
    // 初始化第一天
    dp[1][1] = dp[2][1] = -prices[0];

    for (int i = 1; i &amp;lt; prices.size(); ++i) {
        for (int k = 2; k &amp;gt;= 1; --k) {
            dp[k][0] = max(dp[k][0], dp[k][1] + prices[i]);    // 卖出
            dp[k][1] = max(dp[k][1], dp[k-1][0] - prices[i]);  // 买入
        }
    }

    return dp[2][0]; // 最后一天，最多交易两次，不持股
}
### 还有一件事！


**我们是不是可以把空间压缩为一维，直接用五个变量来代表五个状态岂不美哉！**


s0 什么都没做，或者卖完第二次之后 s1 第一次买入后手里有股票 s2 第一次卖出后，没有股票 s3 第二次买入后，手里有股票 s4 第二次卖出后，没有股票


```cpp
class Solution {
public:
    int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        int n = prices.size();
        if (n == 0) return 0;

        int s0 = 0;
        int s1 = -prices[0];
        int s2 = 0;
        int s3 = -prices[0];
        int s4 = 0;

        for (int i = 1; i &amp;lt; n; ++i) {
            s1 = max(s1, s0 - prices[i]);  // 第一次买
            s2 = max(s2, s1 + prices[i]);  // 第一次卖
            s3 = max(s3, s2 - prices[i]);  // 第二次买
            s4 = max(s4, s3 + prices[i]);  // 第二次卖
        }

        return max({s0, s2, s4}); // 最终可以处于这些状态
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为最终必须 &lt;strong&gt;不持有股票&lt;/strong&gt; 才是有效的利润，合法的结束状态有&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;s0：从未进行过交易&lt;/li&gt;
&lt;li&gt;s2：完成一次交易&lt;/li&gt;
&lt;li&gt;s4：完成两次交易&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不包括 s1 和 s3，因为持有股票就意味着还没卖出，收益没落实！&lt;/p&gt;
&lt;p&gt;你学费了吗？&lt;/p&gt;
&lt;p&gt;这就是状态机DP的精髓之一：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt; &lt;strong&gt;方法&lt;/strong&gt; 状态少（常数级别） 直接用变量代替数组 状态多（变量 k） 用 dp[k+1][2] 表示交易数和是否持股 状态多 + 节省空间 状态压缩 + 滚动数组/变量&lt;/p&gt;
&lt;h2&gt;188. 买卖股票的最佳时机 IV&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;这次我们可以完成  &amp;lt;=K  笔交易了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;prices&lt;/code&gt; 和一个整数 &lt;code&gt;k&lt;/code&gt; ，其中 &lt;code&gt;prices[i]&lt;/code&gt; 是某支给定的股票在第 &lt;code&gt;i&lt;/code&gt;_ _天的价格。&lt;/p&gt;
&lt;p&gt;设计一个算法来计算你所能获取的最大利润。你最多可以完成 &lt;code&gt;k&lt;/code&gt; 笔交易。也就是说，你最多可以买 &lt;code&gt;k&lt;/code&gt;次，卖 &lt;code&gt;k&lt;/code&gt; 次。&lt;/p&gt;
&lt;p&gt;**注意：**你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：k = 2, prices = [2,4,1]
输出：2
解释：在第 1 天 (股票价格 = 2) 的时候买入，在第 2 天 (股票价格 = 4) 的时候卖出，这笔交易所能获得利润 = 4-2 = 2 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：k = 2, prices = [3,2,6,5,0,3]
输出：7
解释：在第 2 天 (股票价格 = 2) 的时候买入，在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后，在第 5 天 (股票价格 = 0) 的时候买入，在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;309. 最佳买卖股票时机含冷冻期 略&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;与前几题相比，本题引入了冷冻期概念。卖出股票后，你无法在第二天买入股票&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;** 当你仍然可以无限次♾️地买卖股票。**&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正题如下：⬇️&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;给定一个整数数组&lt;code&gt;prices&lt;/code&gt;，其中第 _ _&lt;code&gt;prices[i]&lt;/code&gt; 表示第 &lt;code&gt;_i_&lt;/code&gt; 天的股票价格 。​&lt;/p&gt;
&lt;p&gt;设计一个算法计算出最大利润。在满足以下约束条件下，你可以尽可能地完成更多的交易（多次买卖一支股票）:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;卖出股票后，你无法在第二天买入股票 (即冷冻期为 1 天)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**注意：**你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入: prices = [1]
输出: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;714. 买卖股票的最佳时机含手续费&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;（本次更新移除了冷冻期机制，并添加了手续费bushi）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;给定一个整数数组 &lt;code&gt;prices&lt;/code&gt;，其中 &lt;code&gt;prices[i]&lt;/code&gt;表示第 &lt;code&gt;i&lt;/code&gt; 天的股票价格 ；整数 &lt;code&gt;fee&lt;/code&gt; 代表了交易股票的手续费用。&lt;/p&gt;
&lt;p&gt;你可以无限次地完成交易，但是你每笔交易都需要付手续费。如果你已经购买了一个股票，在卖出它之前你就不能再继续购买股票了。&lt;/p&gt;
&lt;p&gt;返回获得利润的最大值。&lt;/p&gt;
&lt;p&gt;**注意：**这里的一笔交易指买入持有并卖出股票的整个过程，每笔交易你只需要为支付一次手续费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [1, 3, 2, 8, 4, 9], fee = 2
输出：8
解释：能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：prices = [1,3,7,5,10,3], fee = 3
输出：6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`1&lt;/li&gt;
&lt;li&gt;dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) 解释：今天持股，可以是昨天就持股，或者昨天不持股但今天买了（花钱买）；&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;初始值：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;dp[0][0] = 0：第 0 天不持股，收益是 0；&lt;/li&gt;
&lt;li&gt;dp[0][1] = -prices[0]：第 0 天持股，花了钱买入股票；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;代码：&lt;/h3&gt;
&lt;h4&gt;二维：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices, int fee) {
    int n = prices.size();
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(n, vector&amp;lt;int&amp;gt;(2, 0));

    // 初始化
    dp[0][0] = 0;               // 第 0 天不持股
    dp[0][1] = -prices[0];      // 第 0 天持股

    for (int i = 1; i &amp;lt; n; ++i) {
        // 今天不持股 = max(昨天不持股, 昨天持股 + 今天卖出的收益 - 手续费)
        dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i] - fee);

        // 今天持股 = max(昨天持股, 昨天不持股 - 今天买入花的钱)
        dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);
    }

    // 最后一天不能持股才能最大收益
    return dp[n-1][0];
}
#### 一维：


```cpp
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices, int fee) {
    int n = prices.size();
    int dp0 = 0;              // 不持有股票
    int dp1 = -prices[0];     // 持有股票

    for (int i = 1; i &amp;lt; n; ++i) {
        int temp = dp0;
        dp0 = max(dp0, dp1 + prices[i] - fee);  // 卖出时要减去手续费
        dp1 = max(dp1, temp - prices[i]);
    }

    return dp0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;题号 / 名称&lt;/strong&gt; &lt;strong&gt;问题描述&lt;/strong&gt; &lt;strong&gt;交易次数限制&lt;/strong&gt; &lt;strong&gt;特殊限制&lt;/strong&gt; &lt;strong&gt;状态设计（dp[i][…])&lt;/strong&gt; &lt;strong&gt;状态数&lt;/strong&gt; &lt;strong&gt;DP 维度&lt;/strong&gt; &lt;strong&gt;说明&lt;/strong&gt; 121. 买卖股票 I 最多一次交易 1 次 无 dp[i][0/1] 2 个状态 二维 基础模型 122. 买卖股票 II 无限次交易 无限制 无 dp[i][0/1] 2 个状态 二维 贪心更优 123. 买卖股票 III 最多两次交易 2 次 无 dp[i][0~4]（状态机） 5 个状态 二维 状态机 DP 188. 买卖股票 IV 最多 k 次交易 给定 k 无 dp[i][k][0/1] 2k 个状态 三维 正统模型 309. 买卖股票含冷冻期 无限次交易 无限制 卖出后第 2 天才能再买 dp[i][0/1/2]（状态机） 3 个状态 二维 状态机加限制 714. 买卖股票含手续费 无限次交易 无限制 每次交易收手续费 dp[i][0/1] 2 个状态 二维 买入时减手续费&lt;/p&gt;
&lt;h3&gt;类似题目：&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-energy-boost-from-two-drinks/&quot;&gt;3259. 超级饮料的最大强化能量 - 力扣（LeetCode）&lt;/a&gt;​​​​​​​&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147234256&quot;&gt;动态规划分享之 —— 买卖股票的最佳时机&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>动态规划 —— 完全背包问题（题集）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147154348--/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147154348--/</guid><description>完全背包的四大经典模型 ✅ 模型 1：最值模型 求最少硬币数（LeetCode 322） 易 场景：凑出目标金额，最少需要多少硬币？  示例： • 输入：coins = {1, 2, 5}, amount = 11 • 输出：{5, 5, 1}（一组可能的解）  总结对比表 </description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;完全背包的四大经典模型&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;✅ 模型 1：最值模型 - 求最少硬币数（LeetCode 322）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;易 场景：凑出目标金额，最少需要多少硬币？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int coinChange(vector&amp;lt;int&amp;gt;&amp;amp; coins, int amount) {
    const int INF = amount + 1;
    vector&amp;lt;int&amp;gt; dp(amount + 1, INF);
    dp[0] = 0;
    for (int coin : coins)
        for (int j = coin; j &amp;lt;= amount; ++j)
            dp[j] = min(dp[j], dp[j - coin] + 1);
    return dp[amount] == INF ? -1 : dp[amount];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 示例：coins = {1, 2, 5}, amount = 11 → 输出：3（5 + 5 + 1）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ 模型 2：计数模型 - 凑出金额的组合数（LeetCode 518）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;易 场景：凑出目标金额，有多少种组合方式？&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int change(int amount, vector&amp;lt;int&amp;gt;&amp;amp; coins) {
    vector&amp;lt;int&amp;gt; dp(amount + 1);
    dp[0] = 1;
    for (int coin : coins)
        for (int j = coin; j &amp;lt;= amount; ++j)
            dp[j] += dp[j - coin];
    return dp[amount];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 示例：coins = {1, 2, 5}, amount = 5 → 输出：4&lt;/p&gt;
&lt;p&gt;（四种组合：5, 2+2+1, 2+1+1+1, 1+1+1+1+1）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ 模型 3：可行性模型 - 是否能正好凑出金额&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;易 场景：判断某个金额是否能被某些硬币正好凑出&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool canMakeAmount(int amount, vector&amp;lt;int&amp;gt;&amp;amp; coins) {
    vector&amp;lt;bool&amp;gt; dp(amount + 1);
    dp[0] = true;
    for (int coin : coins)
        for (int j = coin; j &amp;lt;= amount; ++j)
            dp[j] = dp[j] || dp[j - coin];
    return dp[amount];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 示例：coins = {2, 4}, amount = 7 → 输出：false&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ 模型 4：路径模型 - 恢复最少硬币的选取路径&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;易 场景：不仅要求最少硬币数，还想知道具体用的哪些硬币&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; coinChangePath(vector&amp;lt;int&amp;gt;&amp;amp; coins, int amount) {
    const int INF = amount + 1;
    vector&amp;lt;int&amp;gt; dp(amount + 1, INF);
    vector&amp;lt;int&amp;gt; prev(amount + 1, -1);
    dp[0] = 0;

    for (int coin : coins)
        for (int j = coin; j &amp;lt;= amount; ++j)
            if (dp[j] &amp;gt; dp[j - coin] + 1) {
                dp[j] = dp[j - coin] + 1;
                prev[j] = j - coin;
            }

    if (dp[amount] == INF) return {}; // 无解

    // 回溯路径
    vector&amp;lt;int&amp;gt; res;
    for (int cur = amount; cur &amp;gt; 0; cur = prev[cur])
        res.push_back(cur - prev[cur]);
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 示例：&lt;/p&gt;
&lt;p&gt;• 输入：coins = {1, 2, 5}, amount = 11&lt;/p&gt;
&lt;p&gt;• 输出：{5, 5, 1}（一组可能的解）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; 总结对比表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型&lt;/strong&gt; &lt;strong&gt;场景&lt;/strong&gt; &lt;strong&gt;状态表示&lt;/strong&gt; &lt;strong&gt;转移逻辑&lt;/strong&gt; 最值模型 求最少/最多个数/价值 dp[j] 最小/最大 dp[j] = min(dp[j], dp[j - w] + v) 计数模型 统计组合种类 dp[j] 为方案数 dp[j] += dp[j - w] 可行性模型 判断是否可达 dp[j] 为 bool `dp[j] = dp[j] 路径模型 恢复选法路径 dp[j] + 前驱 额外记录 prev[j]，回溯&lt;/p&gt;
&lt;h2&gt;零钱兑换&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/coin-change/&quot;&gt;322. 零钱兑换&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/coin-change-ii/&quot;&gt;518. 零钱兑换 II&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;322. 零钱兑换 - 力扣（LeetCode）&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;coins&lt;/code&gt; ，表示不同面额的硬币；以及一个整数 &lt;code&gt;amount&lt;/code&gt; ，表示总金额。&lt;/p&gt;
&lt;p&gt;计算并返回可以凑成总金额所需的 &lt;strong&gt;最少的硬币个数&lt;/strong&gt; 。如果没有任何一种硬币组合能组成总金额，返回 &lt;code&gt;-1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你可以认为每种硬币的数量是无限的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：coins = [1, 2, 5], amount = 11
输出：3
解释：11 = 5 + 5 + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：coins = [2], amount = 3
输出：-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：coins = [1], amount = 0
输出：0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= coins.length &amp;lt;= 12&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= coins[i] &amp;lt;= 231 - 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= amount &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;思路&lt;/h4&gt;
&lt;p&gt;dp[i] 表示凑出金额 i 所需的最少硬币数，每个硬币个数都是无限的，因此本题是一个完全背包dp。&lt;/p&gt;
&lt;p&gt;外层遍历物品（在本题中是硬币）内层遍历背包大小，完全背包是从小到大遍历&lt;/p&gt;
&lt;p&gt;递推公式：dp[i] = min(dp[i], dp[i - coin] + 1);   本质上还是选不选硬币coin，在两种选择中选出较小值。 思路较为简单因此很容易得出代码&lt;/p&gt;
&lt;h4&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int coinChange(vector&amp;lt;int&amp;gt;&amp;amp; coins, int amount) {
        const int INF = amount + 1; // 初始化为一个足够大的值
        vector&amp;lt;int&amp;gt; dp(amount + 1, INF);
        dp[0] = 0; // 凑出金额 0，需要 0 个硬币

        for (int coin : coins) {
            for (int i = coin; i &amp;lt;= amount; ++i) {
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }

        return dp[amount] == INF ? -1 : dp[amount];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;518. 零钱兑换 II - 力扣（LeetCode）&lt;/h3&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;coins&lt;/code&gt; 表示不同面额的硬币，另给一个整数 &lt;code&gt;amount&lt;/code&gt; 表示总金额。&lt;/p&gt;
&lt;p&gt;请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额，返回 &lt;code&gt;0&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;假设每一种面额的硬币有无限个。&lt;/p&gt;
&lt;p&gt;题目数据保证结果符合 32 位带符号整数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：amount = 5, coins = [1, 2, 5]
输出：4
解释：有四种方式可以凑成总金额：
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：amount = 3, coins = [2]
输出：0
解释：只用面额 2 的硬币不能凑成总金额 3 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：amount = 10, coins = [10]
输出：1
#### 思路


本题就是一个计数模型了，dp[i] 表示凑出金额为 i 的组合数。 dp[i] 表示：凑出金额 i 的组合数。


然后继续外层遍历物品，内层遍历背包容器，完全背包内层顺序便利。然后可以得出代码


#### 代码


```cpp
class Solution {
public:
    int change(int amount, vector&amp;lt;int&amp;gt;&amp;amp; coins) {
        int n = coins.size();
        vector&amp;lt;unsigned int&amp;gt; dp(amount + 1);
        dp[0] = 1;
        for (int coin : coins) {
            for (int j = coin; j &amp;lt;= amount; j++) {
                dp[j] += dp[j - coin];
            }
        }
        return dp[amount];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他例题&lt;/h2&gt;
&lt;h3&gt;279. 完全平方数&lt;/h3&gt;
&lt;p&gt;给你一个整数 &lt;code&gt;n&lt;/code&gt; ，返回 &lt;em&gt;和为 &lt;code&gt;n&lt;/code&gt; 的完全平方数的最少数量&lt;/em&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完全平方数&lt;/strong&gt; 是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;4&lt;/code&gt;、&lt;code&gt;9&lt;/code&gt; 和 &lt;code&gt;16&lt;/code&gt; 都是完全平方数，而 &lt;code&gt;3&lt;/code&gt; 和 &lt;code&gt;11&lt;/code&gt; 不是。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：n = 12
输出：3
解释：12 = 4 + 4 + 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：n = 13
输出：2
解释：13 = 4 + 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= n &amp;lt;= 104&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;思路&lt;/h4&gt;
&lt;p&gt;dp[i] 表示和为 i 所需的最少完全平方数的个数。dp[i] = min(dp[i], dp[i - j*j] + 1);&lt;/p&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h4&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numSquares(int n) {
        vector&amp;lt;int&amp;gt; dp(n + 1, INT_MAX);
        dp[0] = 0;

        for (int i = 1; i &amp;lt;= n; i++) {
            for (int j = 1; j * j &amp;lt;= i; j++) {
                dp[i] = min(dp[i], dp[i - j * j] + 1);
            }
        }
        return dp[n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;1449. 数位成本和为目标值的最大数字&lt;/h3&gt;
&lt;h4&gt;思路&lt;/h4&gt;
&lt;p&gt;设 dp[i] 表示「总花费为 i 时，最多可以构造多少位数字。我们的目标是得到 dp[target] —— 最多可以拼出多少位数。遍历每个 i = 1 ~ target，然后遍历所有数字 d = 1 ~ 9（用下标 0~8 表示），尝试用数字 d 拼出新状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (i &amp;gt;= cost[d] &amp;amp;&amp;amp; dp[i - cost[d]] != -1) {
    dp[i] = max(dp[i], dp[i - cost[d]] + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义是：&lt;/p&gt;
&lt;p&gt;如果我当前的预算是 i，想加入一个代价为 cost[d] 的数字 d+1，那么我之前的状态得是 i - cost[d]。dp[i - cost[d]] + 1 表示在这个新状态下，数字位数增加了一个。&lt;/p&gt;
&lt;p&gt;随后用贪心优先选择大的数填充字符串。&lt;/p&gt;
&lt;h4&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    string largestNumber(vector&amp;lt;int&amp;gt;&amp;amp; cost, int target) {
        vector&amp;lt;int&amp;gt; dp(target + 1, -1); // dp[i] 表示成本为 i 时能构造的最大位数
    dp[0] = 0;

    // 完全背包：构造最多的位数
    for (int t = 1; t &amp;lt;= target; ++t) {
        for (int d = 0; d &amp;lt; 9; ++d) {
            if (t &amp;gt;= cost[d] &amp;amp;&amp;amp; dp[t - cost[d]] != -1) {
                dp[t] = max(dp[t], dp[t - cost[d]] + 1);
            }
        }
    }

    if (dp[target] &amp;lt; 0) return &quot;0&quot;;

    // 反向构造最大数（贪心，尽量从9开始选）
    string res = &quot;&quot;;
    int t = target;
    for (int d = 8; d &amp;gt;= 0; --d) {
        while (t &amp;gt;= cost[d] &amp;amp;&amp;amp; dp[t] == dp[t - cost[d]] + 1) {
            res += char(&apos;1&apos; + d);
            t -= cost[d];
        }
    }

    return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147154348&quot;&gt;动态规划 —— 完全背包问题（题集）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>动态规划 —— 打家劫舍问题及其变式总结</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147118614--/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147118614--/</guid><description>前言 除了爬楼梯类问题外，入门DP的另一大类即是打家劫舍问题。 198. 打家劫舍 740. 删除并获得点数 2320. 统计放置房子的方式数 1608 213. 打家劫舍 II 3186. 施咒的最大总伤害 1841 题单⬆️（0x3F总结版，特别鸣谢） 例题 ：Leetcod</description><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;除了爬楼梯类问题外，入门DP的另一大类即是打家劫舍问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber/&quot;&gt;198. 打家劫舍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/delete-and-earn/&quot;&gt;740. 删除并获得点数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/count-number-of-ways-to-place-houses/&quot;&gt;2320. 统计放置房子的方式数&lt;/a&gt; 1608&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber-ii/&quot;&gt;213. 打家劫舍 II&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-total-damage-with-spell-casting/&quot;&gt;3186. 施咒的最大总伤害&lt;/a&gt; 1841&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;题单⬆️（0x3F总结版，特别鸣谢）&lt;/p&gt;
&lt;h2&gt;例题 ：Leetcode 198.打家劫舍&lt;/h2&gt;
&lt;p&gt;你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，&lt;strong&gt;如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;给定一个代表每个房屋存放金额的非负整数数组，计算你** 不触动警报装置的情况下 **，一夜之内能够偷窃到的最高金额。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：[1,2,3,1]
输出：4
解释：偷窃 1 号房屋 (金额 = 1) ，然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：[2,7,9,3,1]
输出：12
解释：偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9)，接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= nums.length &amp;lt;= 100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0 &amp;lt;= nums[i] &amp;lt;= 400&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;题目思路&lt;/h3&gt;
&lt;p&gt;首先考虑最简单的情况。如果只有一间房屋，则偷窃该房屋，可以偷窃到最高总金额。如果只有两间房屋，则由于两间房屋相邻，不能同时偷窃，只能偷窃其中的一间房屋，因此选择其中金额较高的房屋进行偷窃，可以偷窃到最高总金额。&lt;/p&gt;
&lt;p&gt;如果房屋数量大于两间，应该如何计算能够偷窃到的最高总金额呢？对于第 k (k&amp;gt;2) 间房屋，有两个选项：&lt;/p&gt;
&lt;p&gt;偷窃第 k 间房屋，那么就不能偷窃第 k−1 间房屋，偷窃总金额为前 k−2 间房屋的最高总金额与第 k 间房屋的金额之和。&lt;/p&gt;
&lt;p&gt;不偷窃第 k 间房屋，偷窃总金额为前 k−1 间房屋的最高总金额。&lt;/p&gt;
&lt;p&gt;在两个选项中选择偷窃总金额较大的选项，该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。&lt;/p&gt;
&lt;p&gt;用 dp[i] 表示前 i 间房屋能偷窃到的最高总金额，那么就有如下的状态转移方程：&lt;/p&gt;
&lt;p&gt;dp[i]=max(dp[i−2]+nums[i],dp[i−1])&lt;/p&gt;
&lt;p&gt;边界条件为：&lt;/p&gt;
&lt;p&gt;dp[0]=nums[0]            
dp[1]=max(nums[0],nums[1])      
​    
  
只有一间房屋，则偷窃该房屋
只有两间房屋，选择其中金额较高的房屋进行偷窃
​    
 
最终的答案即为 dp[n−1]，其中 n 是数组的长度。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int rob(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();

        if(nums.empty()) {
            return 0;
        }
        if (n == 1) {
            return nums[0];
        }

        vector&amp;lt;int&amp;gt;dp (n, 0);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2;i &amp;lt; n; i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }

        return dp[n - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他变式&lt;/h2&gt;
&lt;p&gt;好，你现在已经学会最基本的打家劫舍了，那我们试着加一点难度。&lt;/p&gt;
&lt;h3&gt;740. 删除并获得点数&lt;/h3&gt;
&lt;h4&gt;思路&amp;amp;&amp;amp;代码&lt;/h4&gt;
&lt;p&gt;这道题与打家劫舍极为相似，只需遍历nums，用一个数组points[i] 表示每个i * 次数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int deleteAndEarn(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        if (nums.empty()) {
            return 0;
        }
        int maxVal = *max_element(nums.begin(), nums.end());
        vector&amp;lt;int&amp;gt; points(maxVal + 1, 0);
        vector&amp;lt;int&amp;gt; dp(maxVal + 1, 0);
        for (int num : nums) {
            points[num] += num;
        }
        dp[1] = points[1];
        for (int i = 2; i &amp;lt;= maxVal; i++) {
            dp[i] = max(dp[i - 1], dp[i - 2] + points[i]);
        }
        return dp[maxVal];
    }
};
### 2320. 统计放置房子的方式数


#### 思路&amp;amp;&amp;amp;代码


这个题可以把每一侧道路都看作一个“不能放邻近房子” 问题，因此，单侧就变成了一个经典的斐波那契型递归问题。


而两侧结果是独立的 互不影响，因此最后结果是单侧的平方


```cpp
class Solution {
public:
    int countHousePlacements(int n) {
        const int MOD = 1e9 + 7;
        vector&amp;lt;long long&amp;gt; dp(n + 1, 0);
        dp[0] = 1;
        dp[1] = 2;
        for (int i = 2; i &amp;lt;= n; i++) {
            dp[i] = (dp[i - 1] + dp[i - 2]) % MOD;
        }
        long long res = (dp[n] * dp[n]) % MOD;
        return res;
    }
};
#### ***Tip


补充非常重要的一点 ： 在代码中，我们取模了两次，因此有同学可能会疑惑，取模两次会不会造成错误结果。


其实，这是一个非常常见的问题，在for循环中的，dp[i] 取模式为了防止中间爆 long long 因此必须取余一次。


** 所以，我们通常的做法是：**


• 所有加法中都及时 % MOD，防止中间结果超限；


• 最后再对乘法结果 % MOD，是对的；


• **即使前面的 dp[n] 已经 % MOD，我们也要对最终乘积 res 再 % MOD。**


为什么？


举个例子说明：


```cpp
MOD = 1000000007;
dp[n] = 1000000006; // 即使已经 % MOD
res = (dp[n] * dp[n]) % MOD;
// 即 (1000000006 * 1000000006) % MOD = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不在res中 % MOD ，可能res会溢出。&lt;/p&gt;
&lt;p&gt;综上所述，对dp取余，又对res取余不是重复，是必须的！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h3&gt;213. 打家劫舍 II&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;思路&amp;amp;&amp;amp;代码&lt;/h4&gt;
&lt;p&gt;本题与打家劫舍 I 主要区别在于房子形成环形，因此，我们可以把环拆成两种情况处理，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不偷第一个房子  考虑区间【1，n - 1】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不偷最后一个房子 考虑区间【0，n - 2】&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，我们可以讲打家劫舍 I  的函数封装为一个函数，并分别带入两种情况，求出最大值。&lt;/p&gt;
&lt;p&gt;代码如下 ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int robb(vector&amp;lt;int&amp;gt;&amp;amp; nums, int start, int end) {
        int len = end - start + 1;
        if (len == 1) {
            return nums[start];
        }
        vector&amp;lt;int&amp;gt; dp(len, 0);
        dp[0] = nums[start];
        dp[1] = max(nums[start], nums[start + 1]);
        for (int i = 2; i &amp;lt; len; i++) {
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[start + i]);
        }
        return dp[len - 1];
    }
    int rob(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        if (n == 0) return 0;
        if (n == 1) return nums[0];
        //不偷第一个
        int case1 = robb(nums, 1, n - 1);
        //不偷最后一个
        int case2 = robb(nums, 0, n - 2);
        return max(case1, case2);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中有一下需注意⚠️：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在robb 函数中，需加入 if 判断以避免求 dp[1] 时报错&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;robb函数中nums是从 start 开始的，不要傻乎乎写 nums[0]  nums[i] 哦&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3186. 施咒的最大总伤害&lt;/h3&gt;
&lt;h4&gt;思路&amp;amp;&amp;amp;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/// dp[i] = max(dp[x]..., dp[y] + power[i] * count[power[i]])  :
        ///     power[i] &amp;gt; power[x] &amp;gt;= power[i] - 2}
        ///     y（y&amp;lt;i)是第一个满足‘power[y] &amp;lt; power[i] - 2’的索引
class Solution {
public:
    long long maximumTotalDamage(vector&amp;lt;int&amp;gt;&amp;amp; power) {
        /// 记录每个‘伤害’的个数
        unordered_map&amp;lt;long long, long long&amp;gt; countMap;
        for (int x : power) countMap[x]++;

        /// 排序
        sort(power.begin(), power.end());

        /// 去重
        auto new_end = unique(power.begin(), power.end());
        power.erase(new_end, power.end());

        //DP
        int size = power.size();
        vector&amp;lt;long long&amp;gt; dp(size);
        dp[0] = countMap[power[0]] * power[0];

        long long result = dp[0];
        for (int i = 0; i &amp;lt; size; i++) {
            long long curVal = power[i] * countMap[power[i]];
            dp[i] = curVal;
            for (int j = 1; j &amp;lt;= 3; j++) {
                if (i - j &amp;lt; 0) continue;

                if (power[i] - 2 &amp;gt; power[i - j]) {
                    dp[i] = max(dp[i], dp[i - j] + curVal);
                    break;
                } else {
                    dp[i] = max(dp[i], dp[i - j]);
                }
            }
            result = max(result, dp[i]);
        }
        return result;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147118614&quot;&gt;动态规划 —— 打家劫舍问题及其变式总结&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++ 中二分查找库函数</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-147111379-c-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-147111379-c-/</guid><description>函数简介 C++ 标准库中提供了三个和二分查找相关的函数，都定义在 头文件中，非常实用而且高效，下面是它们的功能和使用方法： ⸻ ✅ 1. binary search  判断某个元素是否存在于有序数组中 include bool found = binary search(nu</description><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;函数简介&lt;/h2&gt;
&lt;p&gt;C++ 标准库中提供了三个和二分查找相关的函数，都定义在  头文件中，非常实用而且高效，下面是它们的功能和使用方法：&lt;/p&gt;
&lt;p&gt;⸻&lt;/p&gt;
&lt;h3&gt;✅ 1. binary_search&lt;/h3&gt;
&lt;p&gt; 判断某个元素是否存在于有序数组中&lt;/p&gt;
&lt;p&gt;#include&lt;/p&gt;
&lt;p&gt;bool found = binary_search(nums.begin(), nums.end(), target);&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;•	返回值：true 表示找到了 target，否则为 false。
•	时间复杂度：O(log n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⸻&lt;/p&gt;
&lt;h3&gt;✅ 2. lower_bound&lt;/h3&gt;
&lt;p&gt; 查找第一个 大于等于 target 的元素的位置（返回迭代器）&lt;/p&gt;
&lt;p&gt;auto it = lower_bound(nums.begin(), nums.end(), target);&lt;/p&gt;
&lt;p&gt;int index = it - nums.begin();  // 获取下标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;•	如果 target 存在，返回它的第一个出现位置；
•	如果不存在，返回第一个大于 target 的元素位置；
•	若返回 nums.end()，说明没有比 target 大或等于它的元素。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⸻&lt;/p&gt;
&lt;h3&gt;✅ 3. upper_bound&lt;/h3&gt;
&lt;p&gt; 查找第一个 大于 target 的元素的位置（返回迭代器）&lt;/p&gt;
&lt;p&gt;auto it = upper_bound(nums.begin(), nums.end(), target);&lt;/p&gt;
&lt;p&gt;int index = it - nums.begin();  // 获取下标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;•	如果 target 存在多次，upper_bound 返回的是最后一个的下一个位置；
•	若返回 nums.end()，说明所有元素都 ≤ target。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⸻&lt;/p&gt;
&lt;h3&gt; 三者比较总结：&lt;/h3&gt;
&lt;p&gt;函数名	返回含义	用法	时间复杂度&lt;/p&gt;
&lt;p&gt;binary_search	是否存在 target	binary_search(v.begin(), v.end(), x)	O(log n)&lt;/p&gt;
&lt;p&gt;lower_bound	第一个 ≥ target 的位置	lower_bound(v.begin(), v.end(), x)	O(log n)&lt;/p&gt;
&lt;p&gt;upper_bound	第一个 &amp;gt; target 的位置	upper_bound(v.begin(), v.end(), x)	O(log n)&lt;/p&gt;
&lt;p&gt;⸻&lt;/p&gt;
&lt;p&gt;離 举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
using namespace std;

int main() {
    vector&amp;lt;int&amp;gt; nums = {1, 2, 4, 4, 5, 6};
    int target = 4;

    cout &amp;lt;&amp;lt; binary_search(nums.begin(), nums.end(), target) &amp;lt;&amp;lt; endl; // 1 (true)
    cout &amp;lt;&amp;lt; lower_bound(nums.begin(), nums.end(), target) - nums.begin() &amp;lt;&amp;lt; endl; // 2
    cout &amp;lt;&amp;lt; upper_bound(nums.begin(), nums.end(), target) - nums.begin() &amp;lt;&amp;lt; endl; // 4

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;适用场景&lt;/h2&gt;
&lt;h3&gt;里 示例 1：插入一个数使数组仍然有序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; nums = {1, 3, 5, 7};
int target = 4;

// 插入 target 保持升序
auto it = lower_bound(nums.begin(), nums.end(), target);
nums.insert(it, target);  // 插入到合适位置

// 结果：nums = {1, 3, 4, 5, 7}
###  示例 2：查找数组中第一个 ≥ target 的元素


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vector&amp;lt;int&amp;gt; nums = {1, 3, 5, 7, 9};
int target = 6;&lt;/p&gt;
&lt;p&gt;auto it = lower_bound(nums.begin(), nums.end(), target);&lt;/p&gt;
&lt;p&gt;if (it != nums.end()) {
cout &amp;lt;&amp;lt; &quot;第一个 ≥ &quot; &amp;lt;&amp;lt; target &amp;lt;&amp;lt; &quot; 的元素是 &quot; &amp;lt;&amp;lt; *it &amp;lt;&amp;lt; endl;
} else {
cout &amp;lt;&amp;lt; &quot;所有元素都小于 &quot; &amp;lt;&amp;lt; target &amp;lt;&amp;lt; endl;
}&lt;/p&gt;
&lt;h3&gt; 示例 3：查找数组中第一个 &amp;gt; target 的元素&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; nums = {1, 3, 5, 7, 9};
int target = 5;

auto it = upper_bound(nums.begin(), nums.end(), target);

if (it != nums.end()) {
    cout &amp;lt;&amp;lt; &quot;第一个 &amp;gt; &quot; &amp;lt;&amp;lt; target &amp;lt;&amp;lt; &quot; 的元素是 &quot; &amp;lt;&amp;lt; *it &amp;lt;&amp;lt; endl;
}
###  示例 4：查找元素出现的次数（重复元素）


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vector&amp;lt;int&amp;gt; nums = {1, 3, 3, 3, 5, 7};
int target = 3;&lt;/p&gt;
&lt;p&gt;// 出现次数 = upper_bound - lower_bound
int count = upper_bound(nums.begin(), nums.end(), target)
- lower_bound(nums.begin(), nums.end(), target);&lt;/p&gt;
&lt;p&gt;cout &amp;lt;&amp;lt; &quot;数字 &quot; &amp;lt;&amp;lt; target &amp;lt;&amp;lt; &quot; 出现了 &quot; &amp;lt;&amp;lt; count &amp;lt;&amp;lt; &quot; 次&quot; &amp;lt;&amp;lt; endl;&lt;/p&gt;
&lt;h3&gt; 示例 5：查找离 target 最近的数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; nums = {1, 3, 6, 8, 10};
int target = 5;

auto it = lower_bound(nums.begin(), nums.end(), target);
int closest;

if (it == nums.begin()) {
    closest = *it;
} else if (it == nums.end()) {
    closest = *(--it);
} else {
    int a = *it;
    int b = *(--it);
    closest = (abs(a - target) &amp;lt; abs(b - target)) ? a : b;
}

cout &amp;lt;&amp;lt; &quot;离 &quot; &amp;lt;&amp;lt; target &amp;lt;&amp;lt; &quot; 最近的数是 &quot; &amp;lt;&amp;lt; closest &amp;lt;&amp;lt; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;例题&lt;/h2&gt;
&lt;p&gt;https://leetcode.cn/problems/maximum-count-of-positive-integer-and-negative-integer/&lt;/p&gt;
&lt;p&gt;给你一个按 非递减顺序 排列的数组 nums ，返回正整数数目和负整数数目中的最大值。&lt;/p&gt;
&lt;p&gt;换句话讲，如果 nums 中正整数的数目是 pos ，而负整数的数目是 neg ，返回 pos 和 neg二者中的最大值。&lt;/p&gt;
&lt;p&gt;注意：0 既不是正整数也不是负整数。&lt;/p&gt;
&lt;p&gt;示例 1：&lt;/p&gt;
&lt;p&gt;输入：nums = [-2,-1,-1,1,2,3]&lt;/p&gt;
&lt;p&gt;输出：3&lt;/p&gt;
&lt;p&gt;解释：共有 3 个正整数和 3 个负整数。计数得到的最大值是 3 。&lt;/p&gt;
&lt;p&gt;示例 2：&lt;/p&gt;
&lt;p&gt;输入：nums = [-3,-2,-1,0,0,1,2]&lt;/p&gt;
&lt;p&gt;输出：3&lt;/p&gt;
&lt;p&gt;解释：共有 2 个正整数和 3 个负整数。计数得到的最大值是 3 。&lt;/p&gt;
&lt;p&gt;示例 3：&lt;/p&gt;
&lt;p&gt;输入：nums = [5,20,66,1314]&lt;/p&gt;
&lt;p&gt;输出：4&lt;/p&gt;
&lt;p&gt;解释：共有 4 个正整数和 0 个负整数。计数得到的最大值是 4 。&lt;/p&gt;
&lt;p&gt;提示：&lt;/p&gt;
&lt;p&gt;1 &amp;lt;= nums.length &amp;lt;= 2000&lt;/p&gt;
&lt;p&gt;-2000 &amp;lt;= nums[i] &amp;lt;= 2000&lt;/p&gt;
&lt;p&gt;nums 按 非递减顺序 排列。&lt;/p&gt;
&lt;p&gt;进阶：你可以设计并实现时间复杂度为 O(log(n)) 的算法解决此问题吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;

using namespace std;

class Solution {
public:
    int maximumCount(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        // 统计负数：最后一个 &amp;lt;0 的位置 +1
        int neg = upper_bound(nums.begin(), nums.end(), -1) - nums.begin();

        // 统计正数：end - 第一个 &amp;gt;0 的位置
        int pos = nums.end() - lower_bound(nums.begin(), nums.end(), 1);

        return max(neg, pos);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/147111379&quot;&gt;C++ 中二分查找库函数&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>西邮移动应用开发实验室二面题解</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-146986318/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-146986318/</guid><description>B2110 找第一个只出现一次的字符 题目描述 给定一个只包含小写字母的字符串，请你找到第一个仅出现一次的字符。如果没有，输出 no 。 输入格式 一个字符串，长度小于 11001100 1100 。 输出格式 输出第一个仅出现一次的字符，若没有则输出 no 。 输入输出样例 1</description><pubDate>Fri, 04 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;B2110 找第一个只出现一次的字符&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;给定一个只包含小写字母的字符串，请你找到第一个仅出现一次的字符。如果没有，输出 &lt;code&gt;no&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;输入格式&lt;/h4&gt;
&lt;p&gt;一个字符串，长度小于&lt;/p&gt;
&lt;p&gt;11001100&lt;/p&gt;
&lt;p&gt;1100&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;h4&gt;输出格式&lt;/h4&gt;
&lt;p&gt;输出第一个仅出现一次的字符，若没有则输出 &lt;code&gt;no&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;输入输出样例 #1&lt;/h4&gt;
&lt;h5&gt;输入 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;abcabd
##### 输出 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;c&lt;/p&gt;
&lt;h4&gt;输入输出样例 #2&lt;/h4&gt;
&lt;h5&gt;输入 #2&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;aabbcc
##### 输出 #2


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;no&lt;/p&gt;
&lt;h3&gt;解题思路&lt;/h3&gt;
&lt;p&gt;很常见的哈希，先遍历字符串统计每个字母出现的次数，再按顺序找出第一个出现一次的字符串&lt;/p&gt;
&lt;h3&gt;解题代码&lt;/h3&gt;
&lt;p&gt;哈希数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;

int main() {
    string s;
    cin &amp;gt;&amp;gt; s;
    int num[26];
    for (int i = 0; i &amp;lt; 26; i++) {
        num[i] = 0;
    }
    for (int i = 0; i &amp;lt; s.size(); i++) {
        num[s[i] - &apos;a&apos;]++;
    }
    for (char c : s) {
        if (num[c - &apos;a&apos;] == 1) {
            cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
            return 0;
        }
    }
    cout &amp;lt;&amp;lt; &quot;no&quot; &amp;lt;&amp;lt; endl;
    return 0;
}
### 补充


#### 当我们需要快速的查找，去重或映射关系时，我们常用到哈希表。


常见的三种哈希结构：


- 数组
- set（集合）
- map（映射）

数组就是简单的哈希表，一些场景就是为数组量身定做的。但需要注意，数组的大小是有限的


在题中可以确定，数组的大小仅需为26（26个字母），用map确实可以，但使用map的空间消耗要比数组大一些，因为map要维护红黑树或者符号表，而且还要做哈希函数的运算。所以数组更加简单直接有效。


## B2136 素数回文数的个数


### 题目描述


求


1111


11


 到


nn


n


 之间（包括


nn


n


），既是素数又是回文数的整数有多少个。


#### 输入格式


一个大于


1111


11


 小于


1000010000


10000


 的整数


nn


n


。


#### 输出格式


1111


11


 到


nn


n


 之间的素数回文数个数。


#### 输入输出样例 #1


##### 输入 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;23&lt;/p&gt;
&lt;h5&gt;输出 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;1
#### 说明/提示


回文数指左右对称的数，如：


1111


11


，


1212112121


12121


### 解题思路


本题需要判断一个数是否同时满足两个条件：

1.	是素数（只能被 1 和自身整除）。

2.	是回文数（正序与倒序相同）。


### 解题代码


```cpp
#include&amp;lt;iostream&amp;gt;
using namespace std;
bool palindrome (int n) {
    int a[10];
    int i = 0;
    while(n) {
        a[i++] = n % 10;
        n /= 10;
    }
    for (int j = 0; j &amp;lt; i / 2; j++) {
        if (a[j] != a[i - j - 1]) {
          return false;
        }
    }
    return true;
}
bool isPrime(int n) {
    if (n &amp;lt; 2) {
      return false;
    }
    for (int i = 2; i &amp;lt;= n / i; i++) {
        if (n % i == 0) {
          return false;
        }
    }
    return true;
}
int main() {
    int n;
    int cnt = 0;
    cin &amp;gt;&amp;gt; n;
    for (int i = 11; i &amp;lt;= n;i++) {
        if (palindrome(i) &amp;amp;&amp;amp; isPrime(i)) {
            cnt++;
        }
    }
    cout &amp;lt;&amp;lt; cnt &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;B2111 基因相关性&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;为了获知基因序列在功能和结构上的相似性，经常需要将几条不同序列的 DNA 进行比对，以判断该比对的 DNA 是否具有相关性。&lt;/p&gt;
&lt;p&gt;现比对两条长度相同的 DNA 序列。首先定义两条 DNA 序列相同位置的碱基为一个碱基对，如果一个碱基对中的两个碱基相同的话，则称为相同碱基对。接着计算相同碱基对占总碱基对数量的比例，如果该比例大于等于给定阈值时则判定该两条 DNA 序列是相关的，否则不相关。&lt;/p&gt;
&lt;h4&gt;输入格式&lt;/h4&gt;
&lt;p&gt;有三行，第一行是用来判定出两条 DNA 序列是否相关的阈值，随后&lt;/p&gt;
&lt;p&gt;22&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;行是两条 DNA 序列（长度不大于&lt;/p&gt;
&lt;p&gt;500500&lt;/p&gt;
&lt;p&gt;500&lt;/p&gt;
&lt;p&gt;）。&lt;/p&gt;
&lt;h4&gt;输出格式&lt;/h4&gt;
&lt;p&gt;若两条 DNA 序列相关，则输出 &lt;code&gt;yes&lt;/code&gt;，否则输出&lt;code&gt;no&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;输入输出样例 #1&lt;/h4&gt;
&lt;h5&gt;输入 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;0.85
ATCGCCGTAAGTAACGGTTTTAAATAGGCC
ATCGCCGGAAGTAACGGTCTTAAATAGGCC
##### 输出 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;yes&lt;/p&gt;
&lt;h3&gt;解题思路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;读取两条 DNA 序列：用 string 类型存储，后计算相同碱基对的比例。
若相同比例 &amp;gt;= 阈值，输出 &quot;yes&quot;，否则输出 &quot;no&quot;。
### 解题代码


```cpp
#include &amp;lt;iostream&amp;gt;
using namespace std;
int main() {
    double n;
    cin &amp;gt;&amp;gt; n;
    string s1;
    string s2;
    cin &amp;gt;&amp;gt; s1;
    cin &amp;gt;&amp;gt; s2;
    int cnt = 0;
    for (int i = 0; i &amp;lt; s1.size(); i++) {
        if (s1[i] == s2[i]) {
            cnt++;
        }
    }
    double res = (double)cnt / s1.size();
    if (res &amp;gt;= n) {
        cout &amp;lt;&amp;lt; &quot;yes&quot; &amp;lt;&amp;lt; endl;
    } else {
        cout &amp;lt;&amp;lt; &quot;no&quot; &amp;lt;&amp;lt; endl;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P1152 欢乐的跳&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;一个&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个元素的整数数组，如果数组两个连续元素之间差的绝对值包括了&lt;/p&gt;
&lt;p&gt;[1,n−1][1,n-1]&lt;/p&gt;
&lt;p&gt;[&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;−&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;]&lt;/p&gt;
&lt;p&gt;之间的所有整数，则称之符合“欢乐的跳”，如数组&lt;/p&gt;
&lt;p&gt;{1,4,2,3}{1,4,2,3}&lt;/p&gt;
&lt;p&gt;{&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;4&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;3&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;符合“欢乐的跳”，因为差的绝对值分别为：&lt;/p&gt;
&lt;p&gt;3,2,13,2,1&lt;/p&gt;
&lt;p&gt;3&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;p&gt;给定一个数组，你的任务是判断该数组是否符合“欢乐的跳”。&lt;/p&gt;
&lt;h4&gt;输入格式&lt;/h4&gt;
&lt;p&gt;每组测试数据第一行以一个整数&lt;/p&gt;
&lt;p&gt;n(1≤n≤1000)n(1 \le n \le 1000)&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;(&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1000&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;开始，接下来&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个空格隔开的在&lt;/p&gt;
&lt;p&gt;[−108,108][-10^8,10^8]&lt;/p&gt;
&lt;p&gt;[&lt;/p&gt;
&lt;p&gt;−&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;8&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;8&lt;/p&gt;
&lt;p&gt;]&lt;/p&gt;
&lt;p&gt;之间的整数。&lt;/p&gt;
&lt;h4&gt;输出格式&lt;/h4&gt;
&lt;p&gt;对于每组测试数据，输出一行若该数组符合“欢乐的跳”则输出 &lt;code&gt;Jolly&lt;/code&gt;，否则输出 &lt;code&gt;Not jolly&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;输入输出样例 #1&lt;/h4&gt;
&lt;h5&gt;输入 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;4 1 4 2 3
##### 输出 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jolly&lt;/p&gt;
&lt;h4&gt;输入输出样例 #2&lt;/h4&gt;
&lt;h5&gt;输入 #2&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;5 1 4 2 -1 6
##### 输出 #2


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not jolly&lt;/p&gt;
&lt;h4&gt;说明/提示&lt;/h4&gt;
&lt;p&gt;1≤n≤10001 \le n \le 1000&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1000&lt;/p&gt;
&lt;h3&gt;解题思路&lt;/h3&gt;
&lt;p&gt;本题的核心是判断连续元素的差值的绝对值是否形成&lt;/p&gt;
&lt;p&gt;[1,n−1][1, n-1]&lt;/p&gt;
&lt;p&gt;[&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;−&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;]&lt;/p&gt;
&lt;p&gt;的完整集合。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 n = 1，默认符合 “欢乐的跳”（因为没有差值）。&lt;/li&gt;
&lt;li&gt;计算 |a[i] - a[i-1]| 并存入哈希map。&lt;/li&gt;
&lt;li&gt;判断是否包含所有整数 1 到 n-1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解题代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;unordered_map&amp;gt;        //不要忘了加这个头文件
#include &amp;lt;vector&amp;gt;
#include &amp;lt;cmath&amp;gt;
using namespace std;

int main() {
    int n;
    cin &amp;gt;&amp;gt; n;
    vector&amp;lt;int&amp;gt; nums(n);
    unordered_map&amp;lt;int, int&amp;gt; s;

		for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; nums[i];
    }

    for (int i = 1; i &amp;lt; n; i++) {
        int diff = abs(nums[i] - nums[i - 1]); //abs用于计算绝对值
        if (diff &amp;gt;= 1 &amp;amp;&amp;amp; diff &amp;lt; n) {
            s[diff] = 1;
        }
    }

    for (int i = 1; i &amp;lt; n; i++) {
        if (s.count(i) == 0) {
            cout &amp;lt;&amp;lt; &quot;Not jolly&quot; &amp;lt;&amp;lt; endl;
            return 0;
        }
    }

    cout &amp;lt;&amp;lt; &quot;Jolly&quot; &amp;lt;&amp;lt; endl;
    return 0;
}
### 补充


#### unordered_map


unordered_map 是 C++ STL（标准模板库）中提供的一个容器，用于存储键值对（key-value pairs）。与 map 不同，unordered_map 使用哈希表（hash table）来存储元素，因此它的元素是无序的。unordered_map 提供的查找、插入和删除操作


##### 常见操作：


插入元素：

•	使用 [] 运算符：umap[key] = value;

•	使用 insert 方法：umap.insert({key, value});


查找元素：

•	使用 find 方法：umap.find(key);

•	find 返回一个迭代器，如果元素存在，返回指向该元素的迭代器；否则，返回 umap.end()。


删除元素：

•	使用 erase 方法：umap.erase(key); 删除某个键值对。

•	也可以删除指定范围的元素，或通过迭代器删除。


访问元素：

•	使用 [] 运算符：umap[key]，如果键不存在，会自动插入一个值为 T()（类型 T 的默认值）的元素。

•	使用 at() 方法：umap.at(key)，如果键不存在，会抛出 out_of_range 异常。


检查元素是否存在：

•	使用 find 方法，返回值与 end() 比较。

•	使用 count 方法：umap.count(key)，如果键存在返回 1，否则返回 0。


## B2092 开关灯


### 题目描述


假设有


NN


N


 盏灯（


NN


N


 为不大于


50005000


5000


 的正整数），从


11


1


 到


NN


N


 按顺序依次编号，初始时全部处于开启状态；第一个人（


11


1


 号）将灯全部关闭，第二个人（


22


2


 号）将编号为


22


2


 的倍数的灯打开，第三个人（


33


3


 号）将编号为


33


3


 的倍数的灯做相反处理（即，将打开的灯关闭，将关闭的灯打开）。依照编号递增顺序，以后的人都和


33


3


 号一样，将凡是自己编号倍数的灯做相反处理。问当第


NN


N


 个人操作完之后，有哪些灯是关闭着的？


#### 输入格式


输入为一行，一个整数


NN


N


，为灯的数量。


#### 输出格式


输出为一行，按顺序输出关着的灯的编号。编号与编号之间间隔一个空格。


#### 输入输出样例 #1


##### 输入 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;10&lt;/p&gt;
&lt;h5&gt;输出 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;1 4 9
#### 输入输出样例 #2


##### 输入 #2


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5&lt;/p&gt;
&lt;h4&gt;输出 #2&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;1 4
### 解题思路


依题，操作数为偶数时，灯光为开启状态（不变），操作数为基数时，灯光熄灭。

操作数和其因子数量有关，由此不难得出，只有编号为完全平方数的灯最终才会是关闭的

eg:  4的因子为1，2，4（3个）


### 解题代码


```cpp
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cmath&amp;gt;
using namespace std;

int main() {
    int N;
    cin &amp;gt;&amp;gt; N;
    for (int i = 1; i * i &amp;lt;= N; i++) {
        cout &amp;lt;&amp;lt; i * i &amp;lt;&amp;lt; &quot; &quot;;
    }
    cout &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;B2140 二进制分类&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;若将一个正整数化为二进制数，在此二进制数中，我们将数字&lt;/p&gt;
&lt;p&gt;11&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;的个数多于数字&lt;/p&gt;
&lt;p&gt;00&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;的个数的这类二进制数称为&lt;/p&gt;
&lt;p&gt;AA&lt;/p&gt;
&lt;p&gt;A&lt;/p&gt;
&lt;p&gt;类数，否则就称其为&lt;/p&gt;
&lt;p&gt;BB&lt;/p&gt;
&lt;p&gt;B&lt;/p&gt;
&lt;p&gt;类数。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;(13)10=(1101)2(13)_{10}=(1101)_2&lt;/p&gt;
&lt;p&gt;(&lt;/p&gt;
&lt;p&gt;13&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;10&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;=&lt;/p&gt;
&lt;p&gt;(&lt;/p&gt;
&lt;p&gt;1101&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;，其中&lt;/p&gt;
&lt;p&gt;11&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;的个数为&lt;/p&gt;
&lt;p&gt;33&lt;/p&gt;
&lt;p&gt;3&lt;/p&gt;
&lt;p&gt;，&lt;/p&gt;
&lt;p&gt;00&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;的个数为&lt;/p&gt;
&lt;p&gt;11&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;，则称此数为&lt;/p&gt;
&lt;p&gt;AA&lt;/p&gt;
&lt;p&gt;A&lt;/p&gt;
&lt;p&gt;类数；&lt;/p&gt;
&lt;p&gt;(10)10=(1010)2(10)_{10}=(1010)_2&lt;/p&gt;
&lt;p&gt;(&lt;/p&gt;
&lt;p&gt;10&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;10&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;=&lt;/p&gt;
&lt;p&gt;(&lt;/p&gt;
&lt;p&gt;1010&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;，其中&lt;/p&gt;
&lt;p&gt;11&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;的个数为&lt;/p&gt;
&lt;p&gt;22&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;，&lt;/p&gt;
&lt;p&gt;00&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;的个数也为&lt;/p&gt;
&lt;p&gt;22&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;，称此数为&lt;/p&gt;
&lt;p&gt;BB&lt;/p&gt;
&lt;p&gt;B&lt;/p&gt;
&lt;p&gt;类数；&lt;/p&gt;
&lt;p&gt;(24)10=(11000)2(24)_{10}=(11000)_2&lt;/p&gt;
&lt;p&gt;(&lt;/p&gt;
&lt;p&gt;24&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;10&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;=&lt;/p&gt;
&lt;p&gt;(&lt;/p&gt;
&lt;p&gt;11000&lt;/p&gt;
&lt;p&gt;)&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;，其中&lt;/p&gt;
&lt;p&gt;11&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;的个数为&lt;/p&gt;
&lt;p&gt;22&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;，&lt;/p&gt;
&lt;p&gt;00&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;的个数为&lt;/p&gt;
&lt;p&gt;33&lt;/p&gt;
&lt;p&gt;3&lt;/p&gt;
&lt;p&gt;，则称此数为&lt;/p&gt;
&lt;p&gt;BB&lt;/p&gt;
&lt;p&gt;B&lt;/p&gt;
&lt;p&gt;类数；&lt;/p&gt;
&lt;p&gt;程序要求：求出 1~n 之中（&lt;/p&gt;
&lt;p&gt;1≤n≤10001 \le n \le 1000&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1000&lt;/p&gt;
&lt;p&gt;），全部&lt;/p&gt;
&lt;p&gt;A,BA,B&lt;/p&gt;
&lt;p&gt;A&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;B&lt;/p&gt;
&lt;p&gt;两类数的个数。&lt;/p&gt;
&lt;h3&gt;输入格式&lt;/h3&gt;
&lt;p&gt;输入&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;h4&gt;输出格式&lt;/h4&gt;
&lt;p&gt;一行，包含两个整数，分别是&lt;/p&gt;
&lt;p&gt;AA&lt;/p&gt;
&lt;p&gt;A&lt;/p&gt;
&lt;p&gt;类数和&lt;/p&gt;
&lt;p&gt;BB&lt;/p&gt;
&lt;p&gt;B&lt;/p&gt;
&lt;p&gt;类数的个数，中间用单个空格隔开。&lt;/p&gt;
&lt;h4&gt;输入输出样例 #1&lt;/h4&gt;
&lt;h5&gt;输入 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;7
##### 输出 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5 2&lt;/p&gt;
&lt;h3&gt;解题思路&lt;/h3&gt;
&lt;p&gt;本题无复杂思维逻辑，进行代码实现即可&lt;/p&gt;
&lt;h3&gt;解题代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
using namespace std;
int tentotwo(int n) {
    int res = 0;
    int t = 1;
    while(n) {
        res += (n % 2) * t;
        t *= 10;
        n /= 2;
    }
    return res;
}
bool AorB(int res) {
    int cnt1 = 0;
    int cnt2 = 0;
    while(res) {
        if(res % 10 == 1) {
            cnt1++;
        } else {
            cnt2++;
        }
        res /= 10;
    }
    if(cnt1 &amp;gt; cnt2) {
        return true;
    } else {
        return false;
    }
}
int main() {
    int n;
    cin &amp;gt;&amp;gt; n;
    int res = tentotwo(n);
    int cnta = 0;
    int cntb = 0;
    for (int i = 1; i &amp;lt;= n; i++) {
        int temp = tentotwo(i);
        if(AorB(temp)) {
            cnta++;
        } else {
            cntb++;
        }
    }
    cout &amp;lt;&amp;lt; cnta &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; cntb &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;B3639 T2 点亮灯笼&lt;/h2&gt;
&lt;h4&gt;题目背景&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;请尽量在 20min 之内写完题目。这是指「写代码」的时间；「读题」时间不计算在内。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;题目描述&lt;/h4&gt;
&lt;p&gt;有&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个灯笼环形摆放。最开始，这些灯笼都是关闭的状态。&lt;/p&gt;
&lt;p&gt;操作台上有&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个按钮，按下第&lt;/p&gt;
&lt;p&gt;xx&lt;/p&gt;
&lt;p&gt;x&lt;/p&gt;
&lt;p&gt;个按钮时，会反转灯笼&lt;/p&gt;
&lt;p&gt;xx&lt;/p&gt;
&lt;p&gt;x&lt;/p&gt;
&lt;p&gt;以及相邻两个灯笼的状态。「反转」是指关闭变成点亮、点亮变成关闭。&lt;/p&gt;
&lt;p&gt;举一个例子：如果按下第&lt;/p&gt;
&lt;p&gt;55&lt;/p&gt;
&lt;p&gt;5&lt;/p&gt;
&lt;p&gt;个按钮，则&lt;/p&gt;
&lt;p&gt;44&lt;/p&gt;
&lt;p&gt;4&lt;/p&gt;
&lt;p&gt;、&lt;/p&gt;
&lt;p&gt;55&lt;/p&gt;
&lt;p&gt;5&lt;/p&gt;
&lt;p&gt;、&lt;/p&gt;
&lt;p&gt;66&lt;/p&gt;
&lt;p&gt;6&lt;/p&gt;
&lt;p&gt;号灯笼都会反转；如果按下第&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个按钮，则&lt;/p&gt;
&lt;p&gt;n−1,n,1n-1, n, 1&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;−&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;这三个灯笼状态反转。这是因为灯笼放置为环形，&lt;/p&gt;
&lt;p&gt;n−1n-1&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;−&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;和&lt;/p&gt;
&lt;p&gt;11&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;是与&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;相邻的灯笼。&lt;/p&gt;
&lt;p&gt;我们依次按下了一些按钮。你需要编程求出当我们的操作完成后，最终这些灯笼的状态。&lt;/p&gt;
&lt;h4&gt;输入格式&lt;/h4&gt;
&lt;p&gt;第一行，两个正整数&lt;/p&gt;
&lt;p&gt;n,mn, m&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;，分别表示共有&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个灯笼、我们按了&lt;/p&gt;
&lt;p&gt;mm&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;次按钮。&lt;/p&gt;
&lt;p&gt;接下来&lt;/p&gt;
&lt;p&gt;mm&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;行，每行一个正整数，表示我们在那一次操作中按下了哪个按钮。&lt;/p&gt;
&lt;h4&gt;输出格式&lt;/h4&gt;
&lt;p&gt;仅一行，&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个整数，依次表示&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个灯笼的状态，用空格隔开。以 &lt;code&gt;0&lt;/code&gt; 代表灯笼关闭，以 &lt;code&gt;1&lt;/code&gt; 代表灯笼点亮。&lt;/p&gt;
&lt;h4&gt;输入输出样例 #1&lt;/h4&gt;
&lt;h5&gt;输入 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;5 4
1
3
1
2
##### 输出 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1 0 0 1 0&lt;/p&gt;
&lt;h4&gt;说明/提示&lt;/h4&gt;
&lt;h5&gt;样例解释&lt;/h5&gt;
&lt;p&gt;灯笼序列的状态如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 0 0 0 0  # 初始状态
1 1 0 0 1  # 按下 1 之后的状态
1 0 1 1 1  # 按下 3 之后的状态
0 1 1 1 0  # 按下 1 之后的状态
1 0 0 1 0  # 按下 2 之后的状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此你应当输出 &lt;code&gt;1 0 0 1 0&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;数据规模与约定&lt;/h5&gt;
&lt;p&gt;对于&lt;/p&gt;
&lt;p&gt;100%100%&lt;/p&gt;
&lt;p&gt;100%&lt;/p&gt;
&lt;p&gt;的数据，有&lt;/p&gt;
&lt;p&gt;n≤1000n\leq 1000&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1000&lt;/p&gt;
&lt;p&gt;，&lt;/p&gt;
&lt;p&gt;m≤1000m\leq 1000&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1000&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;h3&gt;解题思路&lt;/h3&gt;
&lt;p&gt;我们可以用一个长度为 n 的数组来记录每个灯笼的状态，初始全部为 0。每次按下一个按钮时，我们就将对应灯笼以及它的左右相邻灯笼的状态取反（0 变 1，1 变 0）。由于是环形排列，需要注意边界处理，比如第一个灯笼的“左边”是第 n 个灯笼。&lt;/p&gt;
&lt;h3&gt;解题代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
using namespace std;

int main() {
    int n, m;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;

    int lanterns[1005] = {0};

    for (int i = 0; i &amp;lt; m; i++) {
        int x;
        cin &amp;gt;&amp;gt; x;
        int left = (x - 2 + n) % n;
        int mid = (x - 1);
        int right = x % n;

        lanterns[left] ^= 1;
        lanterns[mid] ^= 1;
        lanterns[right] ^= 1;
    }

    for (int i = 0; i &amp;lt; n; i++) {
        cout &amp;lt;&amp;lt; lanterns[i];
        if (i != n - 1) {
          cout &amp;lt;&amp;lt; &quot; &quot;;
        }
    }
    cout &amp;lt;&amp;lt; endl;
    return 0;
}
### 补充


#### 关于位运算


题中取反操作也就是：0变成1， 1变成0 。代码中我们用到的是这个   ^=      符号，这叫做按位异或。

假设 a 是一个灯笼状态，只有两个可能值：


a 1 a^1 说明 0 1 1 0^1 = 1 1 1 0 1^1 = 0

所以 a ^= 1 的效果刚好就是“取反”。若实在不理解，以下方式依旧可以：（只是不够优雅）


```cpp
if (lanterns[i] == 0)
    lanterns[i] = 1;
else
    lanterns[i] = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P2249 【深基13.例1】查找&lt;/h2&gt;
&lt;h4&gt;题目描述&lt;/h4&gt;
&lt;p&gt;输入&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个不超过&lt;/p&gt;
&lt;p&gt;10910^9&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;9&lt;/p&gt;
&lt;p&gt;的单调不减的（就是后面的数字不小于前面的数字）非负整数&lt;/p&gt;
&lt;p&gt;a1,a2,…,ana_1,a_2,\dots,a_{n}&lt;/p&gt;
&lt;p&gt;a&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;a&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;a&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;，然后进行&lt;/p&gt;
&lt;p&gt;mm&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;次询问。对于每次询问，给出一个整数&lt;/p&gt;
&lt;p&gt;qq&lt;/p&gt;
&lt;p&gt;q&lt;/p&gt;
&lt;p&gt;，要求输出这个数字在序列中第一次出现的编号，如果没有找到的话输出&lt;/p&gt;
&lt;p&gt;−1-1&lt;/p&gt;
&lt;p&gt;−&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;h4&gt;输入格式&lt;/h4&gt;
&lt;p&gt;第一行&lt;/p&gt;
&lt;p&gt;22&lt;/p&gt;
&lt;p&gt;2&lt;/p&gt;
&lt;p&gt;个整数&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;和&lt;/p&gt;
&lt;p&gt;mm&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;，表示数字个数和询问次数。&lt;/p&gt;
&lt;p&gt;第二行&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个整数，表示这些待查询的数字。&lt;/p&gt;
&lt;p&gt;第三行&lt;/p&gt;
&lt;p&gt;mm&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;个整数，表示询问这些数字的编号，从&lt;/p&gt;
&lt;p&gt;11&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;开始编号。&lt;/p&gt;
&lt;h4&gt;输出格式&lt;/h4&gt;
&lt;p&gt;输出一行，&lt;/p&gt;
&lt;p&gt;mm&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;个整数，以空格隔开，表示答案。&lt;/p&gt;
&lt;h4&gt;输入输出样例 #1&lt;/h4&gt;
&lt;h5&gt;输入 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
##### 输出 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1 2 -1&lt;/p&gt;
&lt;h4&gt;说明/提示&lt;/h4&gt;
&lt;p&gt;数据保证，&lt;/p&gt;
&lt;p&gt;1≤n≤1061 \leq n \leq 10^6&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;6&lt;/p&gt;
&lt;p&gt;，&lt;/p&gt;
&lt;p&gt;0≤ai,q≤1090 \leq a_i,q \leq 10^9&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;a&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;,&lt;/p&gt;
&lt;p&gt;q&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;9&lt;/p&gt;
&lt;p&gt;，&lt;/p&gt;
&lt;p&gt;1≤m≤1051 \leq m \leq 10^5&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;m&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;5&lt;/p&gt;
&lt;p&gt;本题输入输出量较大，请使用较快的 IO 方式。&lt;/p&gt;
&lt;h3&gt;解题思路&lt;/h3&gt;
&lt;p&gt;题中给出数列单增不减的条件，这个特性让我们想到可以使用二分法来高速查找第一次出现的位置。且题中给出本题输入输出量较大，请使用较快IO方式，从侧面反映需要一种高效查找方式。&lt;/p&gt;
&lt;h3&gt;解题代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

int main(){
    int n,m;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;
    vector&amp;lt;int&amp;gt;nums(n);
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; nums[i];
    }
    vector&amp;lt;int&amp;gt;ask(m);
    for (int i = 0; i &amp;lt; m; i++) {
        cin &amp;gt;&amp;gt; ask[i];
    }
    for (int i = 0; i &amp;lt; m; i++) {
        int l = 0;
        int r = n - 1;
        while (l &amp;lt; r) {
            int mid = l + r &amp;gt;&amp;gt; 1;
            if (nums[mid] &amp;gt;= ask[i]) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        if (nums[l] == ask[i]) {
            cout &amp;lt;&amp;lt; l + 1 &amp;lt;&amp;lt; &quot; &quot;;
        } else {
            cout &amp;lt;&amp;lt; -1 &amp;lt;&amp;lt; &quot; &quot;;
        }
    }
    return 0;
}
### 补充


#### 1. 关于输入输出的优化。


```cpp
ios::sync_with_stdio(false);  // 关闭C++流和C流的同步
cin.tie(0);                   // 解除cin与cout的绑定
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，cin 和 cout 与 stdio.h 库中的 scanf 和 printf 是同步的。&lt;/p&gt;
&lt;p&gt;这种同步会导致额外的性能开销。如果你不需要同时使用 cin 和 scanf 或 cout 和 printf，你可以关闭这种同步。&lt;/p&gt;
&lt;p&gt;哥们，你这个办法还是太吃操作了，有没有跟简单的方法？&lt;/p&gt;
&lt;p&gt;有的兄弟有的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用 scanf 和 printf
int n;
scanf(&quot;%d&quot;, &amp;amp;n);  // 输入
printf(&quot;%d\n&quot;, n); // 输出
#### 2.二分法


二分模板奉上


```cpp
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

int lower_bound1(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
    int l = 0, r = (int) nums.size() - 1;
    while (l &amp;lt;= r) {
        int mid = (l + r) &amp;gt;&amp;gt; 1;
        if (nums[mid] &amp;lt; target) {
          l = mid + 1;
        }
        else {
          r = mid - 1;
        }
    }
    return l;
}

int lower_bound2(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
    int l = 0, r = (int) nums.size();   //左闭右开
    while (l &amp;lt;= r) {
        int mid = (l + r) &amp;gt;&amp;gt; 1;
        if (nums[mid] &amp;lt; target) {
          l = mid + 1;
        }
        else {
          r = mid;
        }
    }
    return l;  //return right也可以
}

int lower_bound3(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
    int l = -1, r = (int) nums.size();  //开区间
    while (l + 1 &amp;lt; r){
        int mid = (l + r) &amp;gt;&amp;gt; 1;
        if (nums[mid] &amp;lt; target) {
          l = mid;
        }
        else {
          r = mid;
        }
    }
    return l;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;P1223 排队接水&lt;/h2&gt;
&lt;h3&gt;题目描述&lt;/h3&gt;
&lt;p&gt;有&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个人在一个水龙头前排队接水，假如每个人接水的时间为&lt;/p&gt;
&lt;p&gt;TiT_i&lt;/p&gt;
&lt;p&gt;T&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;，请编程找出这&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个人排队的一种顺序，使得&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个人的平均等待时间最小。&lt;/p&gt;
&lt;h4&gt;输入格式&lt;/h4&gt;
&lt;p&gt;第一行为一个整数&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;p&gt;第二行&lt;/p&gt;
&lt;p&gt;nn&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;个整数，第&lt;/p&gt;
&lt;p&gt;ii&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;个整数&lt;/p&gt;
&lt;p&gt;TiT_i&lt;/p&gt;
&lt;p&gt;T&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;表示第&lt;/p&gt;
&lt;p&gt;ii&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;个人的接水时间&lt;/p&gt;
&lt;p&gt;TiT_i&lt;/p&gt;
&lt;p&gt;T&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;h4&gt;输出格式&lt;/h4&gt;
&lt;p&gt;输出文件有两行，第一行为一种平均时间最短的排队顺序；第二行为这种排列方案下的平均等待时间（输出结果精确到小数点后两位）。&lt;/p&gt;
&lt;h4&gt;输入输出样例 #1&lt;/h4&gt;
&lt;h5&gt;输入 #1&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;10
56 12 1 99 1000 234 33 55 99 812
##### 输出 #1


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3 2 7 8 1 4 9 6 10 5
291.90&lt;/p&gt;
&lt;h4&gt;说明/提示&lt;/h4&gt;
&lt;p&gt;1≤n≤10001\le n \leq 1000&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;n&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1000&lt;/p&gt;
&lt;p&gt;，&lt;/p&gt;
&lt;p&gt;1≤ti≤1061\le t_i \leq 10^6&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;t&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;≤&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;0&lt;/p&gt;
&lt;p&gt;6&lt;/p&gt;
&lt;p&gt;，不保证&lt;/p&gt;
&lt;p&gt;tit_i&lt;/p&gt;
&lt;p&gt;t&lt;/p&gt;
&lt;p&gt;i&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;不重复。&lt;/p&gt;
&lt;h3&gt;解题思路&lt;/h3&gt;
&lt;p&gt;使n个人平均等待时间最少，也就是要使等待的总时间最少。因此，应该优先让接水时间短的人接水，这样就能避免较长的等待时间。&lt;/p&gt;
&lt;p&gt;这个策略背后就是“先短后长”的贪心策略，类似于“最短作业优先”调度算法。&lt;/p&gt;
&lt;p&gt;此题是一个经典的入门贪心问题。&lt;/p&gt;
&lt;h3&gt;解题代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
using namespace std;
int main() {
    int n;
    cin &amp;gt;&amp;gt; n;
    vector&amp;lt;pair&amp;lt;int,int&amp;gt; &amp;gt; nums(n); // 也可定义一个struct结构体来代替
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; nums[i].first;
        nums[i].second = i + 1;
    }
    sort(nums.begin(), nums.end());
    long long total_wait = 0;
    long long sum = 0;
    for (int i = 0; i &amp;lt; n; i++) {
        total_wait += sum;
        sum += nums[i].first;
    }
    double avg = (double)total_wait / n;
    for (int i = 0; i &amp;lt; n; i++) {
        cout &amp;lt;&amp;lt; nums[i].second &amp;lt;&amp;lt; &quot; &quot;;
    }
    cout &amp;lt;&amp;lt; endl;
    printf(&quot;%.2f\n&quot;, avg);    //注意：输出结果精确到小数点后两位
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/146986318&quot;&gt;西邮移动应用开发实验室二面题解&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>链表&amp;&amp;栈&amp;&amp;队列（顺序/链式）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-146613136--/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-146613136--/</guid><description>链表 链栈 队列 顺序队列 链队列</description><pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;链表&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdlib&amp;gt; // 用于 malloc 和 free
using namespace std;

// 定义链表节点
typedef struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(NULL) {} // 构造函数
} Node;

// 创建新节点
Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (!newNode) { // 确保分配成功
        cout &amp;lt;&amp;lt; &quot;内存分配失败！&quot; &amp;lt;&amp;lt; endl;
        exit(1);
    }
    newNode-&amp;gt;val = data;
    newNode-&amp;gt;next = NULL;
    return newNode;
}

// 在链表头部插入节点
void insertAtHead(Node** head, int data) {
    Node* newNode = createNode(data);
    newNode-&amp;gt;next = *head;
    *head = newNode;
}

// 删除指定值的节点
void deleteNode(Node** head, int key) {
    if (*head == NULL) return; // 避免访问空指针

    Node* temp = *head, *prev = NULL;

    if (temp != NULL &amp;amp;&amp;amp; temp-&amp;gt;val == key) {
        *head = temp-&amp;gt;next; // 确保头指针更新
        free(temp);
        return;
    }

    while (temp != NULL &amp;amp;&amp;amp; temp-&amp;gt;val != key) {
        prev = temp;
        temp = temp-&amp;gt;next;
    }

    if (temp == NULL) return;

    prev-&amp;gt;next = temp-&amp;gt;next;
    free(temp);
}

// 更新指定值的节点
void updateNode(Node* head, int key, int newData) {
    Node* temp = head;
    while (temp != NULL) {
        if (temp-&amp;gt;val == key) {
            temp-&amp;gt;val = newData;
            break;
        }
        temp = temp-&amp;gt;next;
    }
}

// 查找指定值的节点
Node* searchNode(Node* head, int key) {
    Node* temp = head;
    while (temp != NULL) {
        if (temp-&amp;gt;val == key) {
            return temp;
        }
        temp = temp-&amp;gt;next;
    }
    return NULL;
}

// 反转链表（迭代）
void reverseList(Node** head) {
    if (*head == NULL) return; // 避免访问空指针

    Node* prev = NULL;
    Node* current = *head;
    Node* next = NULL;

    while (current != NULL) {
        next = current-&amp;gt;next;
        current-&amp;gt;next = prev;
        prev = current;
        current = next;
    }

    *head = prev;
}

// 反转链表（递归）
Node* reverse(Node* head) {
    if (head == NULL || head-&amp;gt;next == NULL) {
        return head;
    }
    Node* newHead = reverse(head-&amp;gt;next);
    head-&amp;gt;next-&amp;gt;next = head;
    head-&amp;gt;next = NULL;
    return newHead;
}

// 打印链表
void printList(Node* head) {
    Node* temp = head;
    while (temp != NULL) {
        cout &amp;lt;&amp;lt; temp-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
        temp = temp-&amp;gt;next;
    }
    cout &amp;lt;&amp;lt; endl;
}

int main() {
    Node* head = NULL; // 初始化链表为空

    // 插入节点
    insertAtHead(&amp;amp;head, 3);
    insertAtHead(&amp;amp;head, 2);
    insertAtHead(&amp;amp;head, 1);
    cout &amp;lt;&amp;lt; &quot;插入后的链表: &quot;;
    printList(head);

    // 删除节点
    deleteNode(&amp;amp;head, 2);
    cout &amp;lt;&amp;lt; &quot;删除后的链表: &quot;;
    printList(head);

    // 更新节点
    updateNode(head, 3, 30);
    cout &amp;lt;&amp;lt; &quot;更新后的链表: &quot;;
    printList(head);

    // 查找节点
    Node* found = searchNode(head, 30);
    if (found) {
        cout &amp;lt;&amp;lt; &quot;找到节点: &quot; &amp;lt;&amp;lt; found-&amp;gt;val &amp;lt;&amp;lt; endl;
    } else {
        cout &amp;lt;&amp;lt; &quot;未找到节点&quot; &amp;lt;&amp;lt; endl;
    }

    // 反转链表（迭代）
    reverseList(&amp;amp;head);
    cout &amp;lt;&amp;lt; &quot;反转后的链表: &quot;;
    printList(head);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;栈&lt;/h2&gt;
&lt;h4&gt;顺序栈&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;

#define MAXSIZE 100 // 定义栈的最大大小

typedef struct {
    int data[MAXSIZE];
    int top;
} stack;

void init(stack *s) {
    s-&amp;gt;top = -1;
}

// 判断栈是否为空
bool isEmpty(stack *s) {
    return s-&amp;gt;top == -1;
}

// 判断栈是否满
bool isFull(stack *s) {
    return s-&amp;gt;top == MAXSIZE - 1;
}

// 入栈
void push(stack* s, int x) {
    if (isFull(s)) {
        printf(&quot;栈满\n&quot;);
        return;
    }
    s-&amp;gt;data[++s-&amp;gt;top] = x;
}

// 出栈
void pop(stack *s, int *x) {
    if (isEmpty(s)) {
        printf(&quot;栈为空\n&quot;);
    }
    *x = s-&amp;gt;data[s-&amp;gt;top--]; // 先取值，再减少 top
}

// 打印栈
void printStack(stack *s) {
    if (isEmpty(s)) {
        printf(&quot;栈为空\n&quot;);
        return;
    }
    printf(&quot;栈内容: &quot;);
    for (int i = 0; i &amp;lt;= s-&amp;gt;top; i++) {
        printf(&quot;%d &quot;, s-&amp;gt;data[i]);
    }
    printf(&quot;\n&quot;);
}

int main() {
    stack s;
    init(&amp;amp;s);

    push(&amp;amp;s, 10);
    push(&amp;amp;s, 20);
    push(&amp;amp;s, 30);
    printStack(&amp;amp;s);

    int val;
    if (pop(&amp;amp;s, &amp;amp;val)) {
        printf(&quot;出栈: %d\n&quot;, val);
    }
    printStack(&amp;amp;s);

    return 0;
}
#### 链栈


```cpp
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;

// 链栈结点
typedef struct StackNode {
    int data;
    struct StackNode *next;
} StackNode;

// 链栈结构
typedef struct {
    StackNode *top;
    int count;
} LinkStack;

// 初始化链栈
void InitStack(LinkStack *stack) {
    stack-&amp;gt;top = NULL;
    stack-&amp;gt;count = 0;
}

// 入栈操作
void push(LinkStack *s, int x) {
    StackNode *p = (StackNode *)malloc(sizeof(StackNode));
    if (p == NULL) { // 确保内存分配成功
        printf(&quot;内存分配失败！\n&quot;);
        return;
    }
    p-&amp;gt;data = x;
    p-&amp;gt;next = s-&amp;gt;top;
    s-&amp;gt;top = p;
    s-&amp;gt;count++;
    printf(&quot;入栈: %d\n&quot;, x);
}

// 出栈操作
bool pop(LinkStack *s, int *x) {
    if (s-&amp;gt;top == NULL) {
        printf(&quot;栈为空，无法出栈！\n&quot;);
        return false; // 失败返回 false
    }
    StackNode *p = s-&amp;gt;top;
    if (x != NULL) {
        *x = p-&amp;gt;data;
    }
    s-&amp;gt;top = s-&amp;gt;top-&amp;gt;next;
    free(p);
    s-&amp;gt;count--;

    if (x != NULL) {
        printf(&quot;出栈: %d\n&quot;, *x);
    }
    return true; // 成功返回 true
}

// 打印栈
void printStack(LinkStack *s) {
    StackNode *p = s-&amp;gt;top;
    printf(&quot;当前栈内容: &quot;);
    while (p) {
        printf(&quot;%d &quot;, p-&amp;gt;data);
        p = p-&amp;gt;next;
    }
    printf(&quot;\n&quot;);
}

// 测试代码
int main() {
    LinkStack stack;
    InitStack(&amp;amp;stack);

    push(&amp;amp;stack, 10);
    push(&amp;amp;stack, 20);
    push(&amp;amp;stack, 30);
    printStack(&amp;amp;stack);

    int val;
    if (pop(&amp;amp;stack, &amp;amp;val)) {
        printf(&quot;成功出栈: %d\n&quot;, val);
    }
    printStack(&amp;amp;stack);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;队列&lt;/h2&gt;
&lt;h4&gt;顺序队列&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;

#define MAXSIZE 5  // 定义队列最大长度

typedef struct {
    int data[MAXSIZE];
    int front;  // 头指针，指向队首元素
    int rear;   // 尾指针，指向队尾元素的下一个位置
} Queue;

// 初始化队列
void init(Queue *q) {
    q-&amp;gt;front = q-&amp;gt;rear = 0;
}

// 判断队列是否为空
bool isEmpty(Queue *q) {
    return q-&amp;gt;front == q-&amp;gt;rear;
}

// 判断队列是否已满
bool isFull(Queue *q) {
    return (q-&amp;gt;rear + 1) % MAXSIZE == q-&amp;gt;front;
}

// 入队操作
void enqueue(Queue *q, int x) {
    if (isFull(q)) {
        printf(&quot;队列已满\n&quot;);
        return;
    }
    q-&amp;gt;data[q-&amp;gt;rear] = x;
    q-&amp;gt;rear = (q-&amp;gt;rear + 1) % MAXSIZE;
    printf(&quot;入队: %d\n&quot;, x);
}

// 出队操作
bool dequeue(Queue *q, int *x) {
    if (isEmpty(q)) {
        printf(&quot;队列为空\n&quot;);
        return false;
    }
    if (x != NULL) {
        *x = q-&amp;gt;data[q-&amp;gt;front];
    }
    q-&amp;gt;front = (q-&amp;gt;front + 1) % MAXSIZE;
    printf(&quot;出队: %d\n&quot;, *x);
    return true;
}

// 打印队列
void printQueue(Queue *q) {
    if (isEmpty(q)) {
        printf(&quot;队列为空\n&quot;);
        return;
    }
    printf(&quot;队列内容: &quot;);
    for (int i = q-&amp;gt;front; i != q-&amp;gt;rear; i = (i + 1) % MAXSIZE) {
        printf(&quot;%d &quot;, q-&amp;gt;data[i]);
    }
    printf(&quot;\n&quot;);
}

// 测试代码
int main() {
    Queue q;
    init(&amp;amp;q);

    enqueue(&amp;amp;q, 10);
    enqueue(&amp;amp;q, 20);
    enqueue(&amp;amp;q, 30);
    enqueue(&amp;amp;q, 40);
    printQueue(&amp;amp;q);

    int val;
    if (dequeue(&amp;amp;q, &amp;amp;val)) {
        printf(&quot;成功出队: %d\n&quot;, val);
    }
    printQueue(&amp;amp;q);

    enqueue(&amp;amp;q, 50);
    enqueue(&amp;amp;q, 60);
    printQueue(&amp;amp;q);

    return 0;
}
#### 链队列


```cpp
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;

// 链表节点定义
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// 队列定义
typedef struct {
    Node *front;
    Node *rear;
} Queue;

// 初始化队列
void initQueue(Queue *q) {
    q-&amp;gt;front = q-&amp;gt;rear = NULL;
}

// 判断队列是否为空
bool isEmpty(Queue *q) {
    return q-&amp;gt;front == NULL;
}

// 入队操作
void enqueue(Queue *q, int x) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf(&quot;内存分配失败！\n&quot;);
        return;
    }
    newNode-&amp;gt;data = x;
    newNode-&amp;gt;next = NULL;

    if (q-&amp;gt;rear == NULL) {
        q-&amp;gt;front = newNode;
        q-&amp;gt;rear = newNode;
    } else {
        q-&amp;gt;rear-&amp;gt;next = newNode;
        q-&amp;gt;rear = newNode;
    }
    printf(&quot;入队: %d\n&quot;, x);
}

// 出队操作
bool dequeue(Queue *q, int *x) {
    if (isEmpty(q)) {
        printf(&quot;队列为空，无法出队！\n&quot;);
        return false;
    }

    Node *p = q-&amp;gt;front;
    *x = p-&amp;gt;data;
    q-&amp;gt;front = p-&amp;gt;next;
    if (q-&amp;gt;front == NULL) {
        q-&amp;gt;rear = NULL;  // 如果队列空了，更新队尾指针
    }
    free(p);
    printf(&quot;出队: %d\n&quot;, *x);
    return true;
}

// 打印队列
void printQueue(Queue *q) {
    if (isEmpty(q)) {
        printf(&quot;队列为空\n&quot;);
        return;
    }
    Node *p = q-&amp;gt;front;
    printf(&quot;队列内容: &quot;);
    while (p != NULL) {
        printf(&quot;%d &quot;, p-&amp;gt;data);
        p = p-&amp;gt;next;
    }
    printf(&quot;\n&quot;);
}

// 测试代码
int main() {
    Queue q;
    initQueue(&amp;amp;q);

    enqueue(&amp;amp;q, 10);
    enqueue(&amp;amp;q, 20);
    enqueue(&amp;amp;q, 30);
    printQueue(&amp;amp;q);

    int val;
    if (dequeue(&amp;amp;q, &amp;amp;val)) {
        printf(&quot;成功出队: %d\n&quot;, val);
    }
    printQueue(&amp;amp;q);

    enqueue(&amp;amp;q, 40);
    printQueue(&amp;amp;q);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/146613136&quot;&gt;链表&amp;amp;&amp;amp;栈&amp;amp;&amp;amp;队列（顺序/链式）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>链表完全版 C/C++（数组模拟/指针）</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-146347076--cc-/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-146347076--cc-/</guid><description>数组模拟(可实现指针链表的所有功能，在算法题中效率更高) 结构体指针 有同学可能会问：什么是 head ，什么是 head ？ 在Node head中，head是一个只想Node结构体的指针，他储存着链表头节点的地址， head 代表作着Node结构，也就是头节点本身。 如果he</description><pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;数组模拟(可实现指针链表的所有功能，在算法题中效率更高)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int head;     //头节点下标

int e[N];     //节点i的值，相当于结构体指针中的val

int ne[N];    //表示节点i的next指针是多少

int idx;      //当前用到哪个节点
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void init()
{
    head = -1;
    idx = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void add_at_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    idx++;
}


void add_at_k(int x , int k)
{
    e[k] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}

void remove(int k) //删除第k个点后面的点
{
    ne[k] = ne[ne[k]];
}
### 结构体指针


```cpp
typedef struct ListNode {
    int data;
    ListNode* next;
} Node;

//创建新节点

Node* createNode(int data)
{
    Node* newNode = (Node*) malloc(sizeof(Node));
    newNode-&amp;gt;data = data;
    newNode-&amp;gt;next = NULL;
    return newNode;
}

//头插

void insertAtHead(Node** head , int x)
{
    Node* newNode = createNode(x);
    newNode -&amp;gt;next = *head;
    *head = newNode;
}
#### 有同学可能会问：什么是*head ，什么是**head ？


在Node*head中，head是一个只想Node结构体的指针，他储存着链表头节点的地址，*head 代表作着Node结构，也就是头节点本身。


如果head指向某个节点，那么head存的是地址，*head存的是节点的内容（data和next）。


而**head，是一个指向head指针的指针。因为如果要在函数内部修改head值，必须传递指针的指针。


因此在程序中，head是头指针的地址。  *head是头指针本身，储存着链表第一个头节点地址。


**newNode-.next = *head;  让新节点的next指向当前head所指向头节点**


***head= newNode； 让head指向newNode，使新节点成为链表新的头节点**


-----------------------------------------------------------------------------------------------


```cpp
//删除指定值节点

void deleteNode(Node** head, int x)
{
    Node* temp = *head, *prev = NULL;
    if(temp != NULL &amp;amp;&amp;amp; temp -&amp;gt; data == x){ //如果头节点是要删除的节点
        *head = temp -&amp;gt; next;
        free(temp);
        return;
    }
    while(temp != NULL &amp;amp;&amp;amp; temp -&amp;gt; data != x){
        prev = temp;
        temp = temp -&amp;gt; next;
    }
    if(temp == NULL) return;
    prev -&amp;gt; next = temp -&amp;gt; next;
    free(temp);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**为什么要单独判断是否是头节点？ **&lt;/p&gt;
&lt;p&gt;未处理头节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void deleteNode(Node** head, int x) {
    Node* temp = *head;
    Node* prev = NULL;

    while (temp != NULL &amp;amp;&amp;amp; temp-&amp;gt;val != x) {
        prev = temp;
        temp = temp-&amp;gt;next;
    }

    if (temp == NULL) return;  // 没找到

    prev-&amp;gt;next = temp-&amp;gt;next;  // 试图删除
    free(temp);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果删除的是头节点，则prev == NULL , 访问prev -&amp;gt; next 会导致段错误&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;//更新指定值的节点

void updateNode(Node* head, int x, int newData)
{
    Node* temp = head;
    while(temp != NULL &amp;amp;&amp;amp; temp -&amp;gt; data != x){
        temp = temp -&amp;gt; next;
    }
    if(temp == NULL) return;
    temp -&amp;gt; data = newData;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//查找指定值的节点

Node* searchNode(Node* head,int x)
{
    Node* temp = head;
    while(temp != NULL)
    {
        if(temp -&amp;gt; data == x)   return temp;
        temp = temp -&amp;gt; next;
    }
    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重点来了：反转链表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void reverseList(Node** head)
{
    Node* prev = NULL;
    Node* next = NULL;
    Node* current = *head;
    while(current != NULL){
        next = current -&amp;gt; next;  //记录下一个节点
        current -&amp;gt; next = prev;    //反转指针
        prev = current;            //更新prev
        current = next;            //移动current
    }
    *head = prev;                    //更新头节点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;过程演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;head -&amp;gt; [1] -&amp;gt; [2] -&amp;gt; [3] -&amp;gt; [4] -&amp;gt; NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;prev current next &lt;strong&gt;链表状态&lt;/strong&gt; NULL 1 2 1 -&amp;gt; NULL 1 2 3 2 -&amp;gt; 1 -&amp;gt; NULL 2 3 4 3 -&amp;gt; 2 -&amp;gt; 1 -&amp;gt; NULL 3 4 NULL 4 -&amp;gt; 3 -&amp;gt; 2 -&amp;gt; 1 -&amp;gt; NULL 4 NULL NULL 结束循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;head -&amp;gt; [4] -&amp;gt; [3] -&amp;gt; [2] -&amp;gt; [1] -&amp;gt; NULL
//成功反转
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见问题：链表反转过程中使用了几个指针？  三个。&lt;/p&gt;
&lt;p&gt;1. &lt;strong&gt;prev&lt;/strong&gt;（前驱指针）：用于存储当前节点的&lt;strong&gt;前一个节点&lt;/strong&gt;，初始值为 NULL。&lt;/p&gt;
&lt;p&gt;2. &lt;strong&gt;current&lt;/strong&gt;（当前指针）：遍历链表，指向当前处理的节点，初始值为 *head（链表头）。&lt;/p&gt;
&lt;p&gt;3. &lt;strong&gt;next&lt;/strong&gt;（后继指针）：用于&lt;strong&gt;暂存当前节点的下一个节点&lt;/strong&gt;，防止断链。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//递归反转链表

Node* reverse(Node* head) {
    if (head == NULL || head-&amp;gt;next == NULL) {
        return head;
    }
    Node* newHead = reverse(head-&amp;gt;next);  // 递归反转后续链表
    head-&amp;gt;next-&amp;gt;next = head;               // 将当前节点反向指向前一个节点
    head-&amp;gt;next = NULL;                     // 当前节点变为新链表的尾节点
    return newHead;                        // 返回反转后的新头节点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/146347076&quot;&gt;链表完全版 C/C++（数组模拟/指针）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++中常用库函数</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-146485441-c/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-146485441-c/</guid><description>1. 字符串操作 C++ 提供了强大的字符串处理功能，尤其是在 std::string 类中，提供了许多非常方便的成员函数。下面是常见的字符串操作方法。 1.1. 创建和初始化字符串 1.2. 获取字符串长度 std::vector vec = {1, 2, 3, 4}; // </description><pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 字符串操作&lt;/h2&gt;
&lt;p&gt;C++ 提供了强大的字符串处理功能，尤其是在 std::string 类中，提供了许多非常方便的成员函数。下面是常见的字符串操作方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.1. 创建和初始化字符串&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str1 = &quot;Hello&quot;; // 使用字符常量初始化
std::string str2 = str1;    // 拷贝构造
std::string str3(10, &apos;A&apos;);  // 创建一个长度为10，字符为 &apos;A&apos; 的字符串 &quot;AAAAAAAAAA&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1.2. 获取字符串长度&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str = &quot;Hello&quot;;
size_t len = str.size(); // 获取字符串长度，等价于 str.length()
std::cout &amp;lt;&amp;lt; len; // 输出 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1.3. 子字符串操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::string::substr(start_index, length)：返回一个子字符串，起始位置 start_index，长度 length。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str = &quot;Hello World!&quot;;
std::string sub = str.substr(6, 5); // 从第6个字符开始，取5个字符，结果是 &quot;World&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1.4. 查找字符或子字符串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::string::find(substring)：返回子字符串首次出现的位置，如果没有找到返回 std::string::npos。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str = &quot;Hello World!&quot;;
size_t pos = str.find(&quot;World&quot;); // 查找 &quot;World&quot; 的位置，返回 6
if (pos != std::string::npos) {
    std::cout &amp;lt;&amp;lt; &quot;Found at position &quot; &amp;lt;&amp;lt; pos &amp;lt;&amp;lt; std::endl; // 输出: Found at position 6
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• std::string::rfind(substring)：返回子字符串最后一次出现的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str = &quot;Hello World! World!&quot;;
size_t pos = str.rfind(&quot;World&quot;); // 查找最后一个 &quot;World&quot; 的位置，返回 13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1.5. 替换字符或子字符串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::string::replace(start_index, length, new_substring)：将指定位置的部分内容替换为新的子字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str = &quot;Hello World!&quot;;
str.replace(6, 5, &quot;Universe&quot;); // 将 &quot;World&quot; 替换为 &quot;Universe&quot;
std::cout &amp;lt;&amp;lt; str; // 输出: &quot;Hello Universe!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1.6. 拼接字符串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• 使用 + 运算符或者 std::string::append() 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str1 = &quot;Hello&quot;;
std::string str2 = &quot; World!&quot;;
std::string str3 = str1 + str2; // 拼接 &quot;Hello&quot; 和 &quot; World!&quot;，结果为 &quot;Hello World!&quot;
str1.append(&quot; Universe!&quot;);     // 追加 &quot; Universe!&quot; 到 str1
std::cout &amp;lt;&amp;lt; str1; // 输出: &quot;Hello Universe!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1.7. 转换为 C 风格字符串&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::string::c_str()：返回 const char* 类型的 C 风格字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str = &quot;Hello&quot;;
const char* cstr = str.c_str(); // 获取 C 风格字符串 &quot;Hello&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1.8. 字符串比较&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::string::compare()：比较两个字符串，返回值：0 相等，负数小于，正数大于。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string str1 = &quot;abc&quot;;
std::string str2 = &quot;abc&quot;;
if (str1.compare(str2) == 0) {
    std::cout &amp;lt;&amp;lt; &quot;Strings are equal&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 数学相关&lt;/h2&gt;
&lt;p&gt;C++ 提供了大量的数学函数，很多函数都在 cmath 头文件中，以下是一些常用的数学函数和操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.1. 数学基本函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::abs(x)：返回 x 的绝对值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int abs_val = std::abs(-5); // 返回 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• std::pow(base, exponent)：计算 base 的 exponent 次幂。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double result = std::pow(2, 3); // 2 的 3 次方，结果为 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• std::sqrt(x)：返回 x 的平方根。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double root = std::sqrt(16); // 返回 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• std::ceil(x)：返回不小于 x 的最小整数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double ceil_val = std::ceil(2.3); // 返回 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• std::floor(x)：返回不大于 x 的最大整数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double floor_val = std::floor(2.7); // 返回 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2.2. 三角函数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::sin(x)：计算角度 x 的正弦值（弧度制）。&lt;/p&gt;
&lt;p&gt;• std::cos(x)：计算角度 x 的余弦值（弧度制）。&lt;/p&gt;
&lt;p&gt;• std::tan(x)：计算角度 x 的正切值（弧度制）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.3. 最大最小值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::max(x, y)：返回 x 和 y 中的较大值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int max_val = std::max(10, 20); // 返回 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• std::min(x, y)：返回 x 和 y 中的较小值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int min_val = std::min(10, 20); // 返回 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2.4. 最大公约数与最小公倍数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::gcd(a, b)：返回 a 和 b 的最大公约数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int gcd_val = std::gcd(24, 36); // 返回 12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• std::lcm(a, b)：返回 a 和 b 的最小公倍数（C++20 新增）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int lcm_val = std::lcm(24, 36); // 返回 72
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2.5. 随机数生成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;• std::rand()：生成随机整数。&lt;/p&gt;
&lt;p&gt;• std::srand(seed)：设置随机数种子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::srand(std::time(0)); // 设置随机数种子
int random_num = std::rand(); // 获取一个随机数
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 容器相关&lt;/h2&gt;
&lt;p&gt;C++ 提供了丰富的标准容器类（在 &amp;lt;vector&amp;gt;, &amp;lt;deque&amp;gt;, &amp;lt;list&amp;gt;, &amp;lt;set&amp;gt;, &amp;lt;map&amp;gt; 等头文件中定义）。这些容器广泛应用于各种算法题和实际编程中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.1. 向量 std::vector&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;std::vector 是 C++ 中的动态数组，支持随机访问元素，能够动态扩展。&lt;/p&gt;
&lt;p&gt;• 创建和初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;int&amp;gt; vec = {1, 2, 3, 4}; // 初始化
std::vector&amp;lt;int&amp;gt; vec2(5, 0); // 初始化长度为 5 的向量，元素为 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• 常用方法&lt;/p&gt;
&lt;p&gt;• vec.size()：返回向量的元素个数。&lt;/p&gt;
&lt;p&gt;• vec.push_back(x)：在向量末尾添加元素 x。&lt;/p&gt;
&lt;p&gt;• vec.pop_back()：删除向量末尾的元素。&lt;/p&gt;
&lt;p&gt;• vec[index]：访问元素。&lt;/p&gt;
&lt;p&gt;• vec.begin() 和 vec.end()：返回向量的起始和结束迭代器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vec.push_back(10);  // 向 vec 添加元素 10
std::cout &amp;lt;&amp;lt; vec.size(); // 输出 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3.2. 双端队列 std::deque&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;std::deque 是一个双端队列，支持从两端进行高效的插入和删除。&lt;/p&gt;
&lt;p&gt;• 创建和初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::deque&amp;lt;int&amp;gt; dq = {1, 2, 3, 4};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• 常用方法&lt;/p&gt;
&lt;p&gt;• dq.push_front(x)：将元素 x 添加到队列前端。&lt;/p&gt;
&lt;p&gt;• dq.push_back(x)：将元素 x 添加到队列后端。&lt;/p&gt;
&lt;p&gt;• dq.pop_front()：从队列前端删除元素。&lt;/p&gt;
&lt;p&gt;• dq.pop_back()：从队列后端删除元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.3. 链表 std::list&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;std::list 是一个双向链表，适用于频繁插入和删除操作的场景。&lt;/p&gt;
&lt;p&gt;• 创建和初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::list&amp;lt;int&amp;gt; lst = {1, 2, 3, 4};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• 常用方法&lt;/p&gt;
&lt;p&gt;• lst.push_front(x)：将元素 x 添加到链表前端。&lt;/p&gt;
&lt;p&gt;• lst.push_back(x)：将元素 x 添加到链表后端。&lt;/p&gt;
&lt;p&gt;• lst.pop_front()：删除链表前端的元素。&lt;/p&gt;
&lt;p&gt;• lst.pop_back()：删除链表后端的元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.4. 集合和映射 std::set 和 std::map&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;std::set 和 std::map 都是基于红黑树实现的有序集合和映射，元素按顺序排列，自动去重。&lt;/p&gt;
&lt;p&gt;• 创建和初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::set&amp;lt;int&amp;gt; s = {1, 2, 3, 4};
std::map&amp;lt;int, std::string&amp;gt; m = {{1, &quot;one&quot;}, {2, &quot;two&quot;}};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;• 常用方法&lt;/p&gt;
&lt;p&gt;• s.insert(x)：插入元素。&lt;/p&gt;
&lt;p&gt;• s.find(x)：查找元素是否存在，返回迭代器。&lt;/p&gt;
&lt;p&gt;• m[key] = value：向映射中插入元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.5. 哈希集合和哈希映射 std::unordered_set 和 std::unordered_map&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;std::unordered_set 和 std::unordered_map 是基于哈希表实现的容器，提供平均常数时间复杂度的查找、插入和删除操作。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/146485441&quot;&gt;C++中常用库函数&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Leetcode经典链表问题之反转链表</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-146438398-leetcode/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-146438398-leetcode/</guid><description>206. 反转链表 力扣（LeetCode） 92. 反转链表 II 力扣（LeetCode） 反转链表2，只反转部分链表 关于哨兵节点 哨兵节点的作用 ✅ (1) 确保 m 1 位置正确连接 • 反转部分链表时，m 1 位置的节点需要正确指向反转后的 m 位置。 • 哨兵节点</description><pubDate>Sat, 22 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;206. 反转链表 - 力扣（LeetCode）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* next = nullptr;
        ListNode* temp = head;
        while(temp!= nullptr){
            next = temp -&amp;gt; next;
            temp -&amp;gt; next = prev;
            prev = temp;
            temp = next;
        }
        return prev;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;92. 反转链表 II - 力扣（LeetCode）&lt;/h2&gt;
&lt;p&gt;反转链表2，只反转部分链表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode* dummy = new ListNode(0);
        dummy-&amp;gt;next = head;
        ListNode* p0 = dummy;
        for(int i = 1;i &amp;lt; left;i++){    //i 从1开始，移动left- 1次，指向left前一个节点
            p0 = p0-&amp;gt;next;
        }
        ListNode* prev = nullptr;   // 不是p0
        ListNode* cur = p0 -&amp;gt; next;
        for(int i = left ;i &amp;lt;= right;i++){
            ListNode* next = cur-&amp;gt;next;
            cur -&amp;gt; next = prev;
            prev = cur;
            cur = next;
        }
        p0 -&amp;gt; next -&amp;gt; next = cur;
        p0 -&amp;gt; next = prev;
        return dummy -&amp;gt; next;
    }
};
### 关于哨兵节点


** 哨兵节点的作用**


**✅ (1) 确保 m-1 位置正确连接**


• 反转部分链表时，m-1 位置的节点需要正确指向反转后的 m 位置。


• 哨兵节点 dummy 让 prev 总是存在，无论 m=1 还是 m&amp;gt;1，都可以通过 dummy-&amp;gt;next 访问 m-1 位置。


**✅ (2) 处理 m=1 的情况，防止 head 丢失**


• 反转部分链表时，如果 m=1，head 需要更新。


• 如果直接修改 head，可能导致 head 丢失，难以返回正确的新头。


• 使用 dummy，dummy-&amp;gt;next 总是指向 head，即使 head 改变，我们仍然可以返回 dummy.next，保证正确性。


## 25. K 个一组翻转链表 - 力扣（LeetCode）


```cpp
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        int n = 0;
        ListNode* temp = head;
        while(temp){
            n++;
            temp = temp -&amp;gt; next;
        }
        ListNode dummy(0,head);
        ListNode* p0 = &amp;amp;dummy;
        ListNode* prev = nullptr;
        ListNode* cur = head;
        for(;n &amp;gt;= k; n -= k){
            for(int i = 0; i &amp;lt; k ;i++){
                ListNode* next= cur-&amp;gt;next;
                cur -&amp;gt; next = prev;
                prev = cur;
                cur = next;
            }
            ListNode* nxt = p0-&amp;gt;next;
            p0 -&amp;gt; next -&amp;gt; next = cur;
            p0 -&amp;gt; next = prev;
            p0 = nxt;
        }
        return dummy.next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/146438398&quot;&gt;Leetcode经典链表问题之反转链表&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>巧妙的滑动窗口 -- leetcode1423</title><link>https://www.tommywutong.cn/posts/csdn-import/csdn-146301751--leetcode1423/</link><guid isPermaLink="true">https://www.tommywutong.cn/posts/csdn-import/csdn-146301751--leetcode1423/</guid><description>题干 1423. 可获得的最大点数 几张卡牌 排成一行 ，每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。 每次行动，你可以从行的开头或者末尾拿一张卡牌，最终你必须正好拿 k 张卡牌。 你的点数就是你拿到手中的所有卡牌的点数之和。 给你一个整数数组 car</description><pubDate>Sun, 16 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题干&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-points-you-can-obtain-from-cards/&quot;&gt;1423. 可获得的最大点数&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;几张卡牌** 排成一行**，每张卡牌都有一个对应的点数。点数由整数数组 &lt;code&gt;cardPoints&lt;/code&gt; 给出。&lt;/p&gt;
&lt;p&gt;每次行动，你可以从行的开头或者末尾拿一张卡牌，最终你必须正好拿 &lt;code&gt;k&lt;/code&gt; 张卡牌。&lt;/p&gt;
&lt;p&gt;你的点数就是你拿到手中的所有卡牌的点数之和。&lt;/p&gt;
&lt;p&gt;给你一个整数数组 &lt;code&gt;cardPoints&lt;/code&gt; 和整数 &lt;code&gt;k&lt;/code&gt;，请你返回可以获得的最大点数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：cardPoints = [1,2,3,4,5,6,1], k = 3
输出：12
解释：第一次行动，不管拿哪张牌，你的点数总是 1 。但是，先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌，最终点数为 1 + 6 + 5 = 12 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：cardPoints = [2,2,2], k = 2
输出：4
解释：无论你拿起哪两张卡牌，可获得的点数总是 4 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 3：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：cardPoints = [9,7,7,9,7,7,9], k = 7
输出：55
解释：你必须拿起所有卡牌，可以获得的点数为所有卡牌的点数之和。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 4：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：cardPoints = [1,1000,1], k = 1
输出：1
解释：你无法拿到中间那张卡牌，所以可以获得的最大点数为 1 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例 5：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入：cardPoints = [1,79,80,1,1,1,200,1], k = 3
输出：202
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= cardPoints.length &amp;lt;= 10^5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= cardPoints[i] &amp;lt;= 10^4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 &amp;lt;= k &amp;lt;= cardPoints.length&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解题思路&lt;/h2&gt;
&lt;p&gt;依照题意，只能从头尾拿牌，因此剩余的牌一定为连续（重点）由此联想到滑动窗口&lt;/p&gt;
&lt;p&gt;且求出可拿牌的最大和，因此剩下的牌一定总和最小&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题转换为：求和最小的定长为 n - k的滑动窗口&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public:
        int maxScore(vector&amp;lt;int&amp;gt;&amp;amp; cardPoints, int k) {
            int n = cardPoints.size();
            int sum = 0;
            if(n == k) return accumulate(cardPoints.begin(), cardPoints.end(), 0);
            for(int i = 0;i &amp;lt; n - k;i++){
                sum += cardPoints[i];
            }
            int minSum = sum;
            for(int i = n - k;i &amp;lt; n;i++){
                sum += cardPoints[i] - cardPoints[i - n + k];
                minSum = min(minSum, sum);
        }
        return accumulate(cardPoints.begin(), cardPoints.end(), 0) - minSum;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;原文发布于 CSDN：&lt;a href=&quot;https://blog.csdn.net/2402_86720949/article/details/146301751&quot;&gt;巧妙的滑动窗口 -- leetcode1423&lt;/a&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>