homebrew

[toc]

一、homebrew库的下载、更新

1、homebrew库的下载、更新

1.1、库的下载

创建以 homebrew-开头的工程,如https://github.com/dvlpCI/homebrew-tools

这样执行 brew tap 的时候,就可以只输入brew tap dvlpCI/tools,其会自动映射到dvlpCI/homebrew-tools

image-20230415222441966

如截图,你的远程库索引就会放到本地的以下路径

/usr/local/Homebrew/Library/Taps/dvlpci/homebrew-tools

image-20230415230921079

1.2、库的更新

使用以下命令更新

1
2
3
brew update

brew upgrade tools

更新后

image-20230416215241520

2、homebrew库中的命令

2.1、brew install

①、brew install的使用

brew install可安装上述 homebrew-tool中的子库

如可执行 brew install brew 也可以 brew helloworld

②、brew install 安装到目录

brew install软件会安装在/usr/local/Cellar/bjf/

image-20230416190602315

执行结果如下:

如果想要有如下lib/src,则需要在rb文件中

1
2
3
4
5
6
def install
# Install script to bin
# bin.install "helloworld.sh"
bin.install "bjf"
lib.install Dir["*"] # 记得添加lib目录,并将源码存放于lib里的src下
end

image-20230416190923562

目录/usr/local/Cellar/bjf/0.0.3/lib/src/

2.2、brew uninstall

验证方式,你brew uninstall的时候也会提示

下载下来的文件存放位置:

/Users/lichaoqian/Library/Caches/Homebrew

rb文件介绍

1、xxx.rb名字规范

1、文件名:

建议文件名都是小写;如果有多个词需连接,建议用-

2、文件内容

文件内容中的类名,必须和文件名一致,且第一个字母大小,其余都是小写。否则会出现如下错误。

image-20230416034029544

库中的内容

存放rb文件

echo “hello world”

echo “1.1、创建 https://github.com/dvlpCI/script-branch-json-file.git"

echo “1.2、在上述创建的git下创建 helloworld.sh,并编写想要的脚本代码”

echo “1.3.1、shc -r -f helloworld.sh”
echo “1.3.2、修改上述生成的二进制可执行文件helloworld.sh.x名为helloworld(不填的话,你后续使用时候就不能输helloworld,而是要输helloworld.sh)”

echo “1.4、提交文件并打tag到远程”

echo “1.5、假如上述打的tag是0.0.1,则可以直接在浏览器里输入 https://github.com/dvlpCI/script-branch-json-file/archive/0.0.1.tar.gz 来回车下来该文件”

echo “1.6、shasum -a 256 0.0.1.tar.gz”

echo “hello world”

echo “hello world”

echo “2.1、创建 https://github.com/dvlpCI/homebrew-branch-json-file.git"

echo “2.2、在上述创建的git下添加 helloworld.rb 文件及其内容”

echo “hello world”

echo “hello world”

echo “tar -czvf v0.0.2.tar.gz helloworld.sh”

echo “mv v0.0.2.tar.gz archive/“

echo “hello world”

echo “hello world”

echo “brew tap dvlpCI/homebrew-branch-json-file”

echo “hello world”

echo “hello world”

Git获取信息

[toc]

一、获取commit信息:

1、获取指定分支最后一次提交的信息

1
2
3
Git获取信息
# 获取指定分支最后一次提交的作者
git show --format=%aN -b origin/dev_all -s

2、获取指定分支最后一次提交的信息–本地

我想可以获取到远程origin/shop_report分支的最后一次提交作者,但是我又不想说必须得去拉取到本地,请问我该怎么处理

如果您不想将 origin/shop_report 分支拉取到本地仓库中,您可以尝试使用以下命令来获取该分支的最后一次提交的作者名字:

1
git ls-remote --heads origin dev_all | cut -f 1 | xargs -I {} git show --format=%aN {} -s | head -n 1

该命令将会首先使用 git ls-remote 命令获取 origin/shop_report 分支的引用头(ref),然后使用 cut 命令提取该引用头的哈希值。接着,使用 xargs 命令将该哈希值作为参数传递给 git show 命令,并使用 --format=%aN 参数指定输出作者名字。最后,使用 head 命令提取第一行输出,即为该分支的最后一次提交的作者名字。

请注意,这种方式虽然可以不将 origin/shop_report 分支拉取到本地仓库中,但是每次执行该命令都会从远程仓库中获取该分支的引用头和最后一次提交的作者信息,因此速度可能较慢,特别是在网络条件不好的情况下。如果您需要频繁地获取 origin/shop_report 分支的最后一次提交的作者信息,建议还是将该分支拉取到本地仓库中。

1、git 查看远程分支

要查看远程分支,可以使用以下命令:

1
git branch -r

该命令会列出所有远程分支的列表,以remotes/为前缀。如果你想查看特定的远程分支,可以使用以下命令:

1
git branch -r | grep <remote-branch-name>

其中,<remote-branch-name>是指你要查看的远程分支名称,这个命令会查询匹配远程分支名称的所有分支,然后在终端输出它们的名称。

2、远程删掉了,但本地执行git branch -r 还是显示出来

2.1、git remote prune origin

如果你在远程删除了一个分支,但是本地执行git branch -r命令仍然显示该分支名,这可能是因为本地的远程分支列表缓存了该分支名。为了解决这个问题,你可以执行以下命令来更新本地的远程分支列表:

1
git remote prune origin

该命令会从远程 Git 仓库中删除已经不存在的分支,并更新你本地的远程分支列表。执行完这个命令后,再次执行git branch -r,就应该不会再看到已经被删除的分支名了。

2.2、git remote prune origin 执行出错了

如果git remote prune origin命令执行出错了,很可能是由于你的本地缓存信息有问题,请执行以下操作修复问题:

  1. 首先,运行以下命令来更新远程分支缓存:
1
2
# 这个命令会强制更新缓存,在更新过程中删除远程不存在的分支,然后更新你本地的远程分支列表
git remote update origin --prune

如果上述命令没有解决问题,你需要手动清除本地缓存。

1
2
3
4
5
6
7
8
9
# 这个命令会从远程 Git 仓库中下载所有最新的分支,同时删除远程不存在的分支,然后更新本地分支列表。
git fetch --all --prune

# 如果上述命令仍然无法解决问题,你可以尝试处理该git工程下的 .git/refs/remotes 文件夹
# 尝试方式1:手动删除指定分支

# 尝试方式2:删除所有本地缓存目录并重新克隆仓库。
rm -rf .git/refs/remotes
git clone <repository-url>

End

Git使用

详细而全的文档:Pro Git(中文版)

Git回滚代码到某个commit

回退命令:

1
2
3
4
5
6
7
8
在git push的时候,有时候我们会想办法撤销git commit的内容 
1、找到之前提交的git commit的id
git log
找到想要撤销的id
2、git reset –hard id
完成撤销,同时将代码恢复到前一commit_id 对应的版本
3、git reset id
完成Commit命令的撤销,但是不对代码修改进行撤销,可以直接通过git commit 重新提交对本地代码的修改
1
2
3
4
5
6
$ git reset HEAD^         			回退到上个版本,代码还在

注意:如果加上使用--hard会导致这里虽然回到了上个版本,但是commit上去,未push出去的就没法找回了
$ git reset --hard HEAD^ 回退到上个版本
$ git reset --hard HEAD~3 回退到前3次提交之前,以此类推,回退到n次提交之前
$ git reset --hard commit_id 退到/进到 指定commit的sha码

当你有多处备份的时候,你可以强制性的执行

git reset –hard HEAD^

但是你刚commit上去,未push出去的就没法找回了。

pod install报错:ArgumentError - Malformed version number string

网上的参考:pod install报错:ArgumentError - Malformed version number string

实际:通过执行sudo gem install cocoapods命令重装cocoapods即解决了。

其他CocoaPods那些错

GitHub 出现 POST git-receive-pack (chunked) 解决方案详解

出现 POST git-receive-pack (chunked) 的原因就是 当使用 HTTPS 提交到 Git 上时使用不检查加密要是东西过多将导致提交停止。

解决方法:

1
2
3
4
5
6
7
方案1>  使用 Git 提交代码
进入到要提交的代码的目录,里面包含 .git 文件夹,输入指令 git config http.postBuffer 524288000

方案2> 使用 SourceTreee 提交代码
如图按照顺序依次点击在最后一步增加
[http]
postBuffer = 524288000

POST git-receive-pack (chunked).png)

再次提交将会成功。

相关参考:GitHub 出现 POST git-receive-pack (chunked) 解决方案详解

Github Push 方式

[toc]

Git的其他参考文章:

  • Git 面試題

  • 問:用一般方式的合併,跟使用 rebase 方式合併,有什麼不同?各有何優缺點?

    一般的合併方式,有些情況(非快轉合併)會產生一個額外的 commit 來接合兩邊分支,而 rebase 合併分支跟一般的合併分支的明顯差別,就是使用 rebase 方式合併分支不會有這個合併的 Commit。

    如果就以最後的的結果來說,檔案內容來說是沒什麼差別,但在 Git 的歷史紀錄上來說就有一些差別,誰 rebase 誰,會造成歷史紀錄上先後順序不同的差別。例如 cat 分支 rebase 到 dog 分支的話,表示 cat 分支會被接到 dog 分支的後面;反之如果是 dog 分支 rebase 到 cat 上的話,表示 dog分支 會被接到 cat 分支的後面。

    使用 rebase 的好處,是整理出來的歷史紀錄不會有合併用的 commit,看起來比較乾淨(也是有些人不覺得這乾淨多少),另外歷史紀錄的順序可以依照誰 rebase 誰而決定先後關係(不過這點不一定是優點或缺點,端看整理的人而定);缺點就是它相對的比一般的合併來得沒那麼直覺,一個不小心可能會弄壞掉而且還不知道怎麼 reset 回來,或是發生衝突的時候就會停在一半,對不熟悉 rebase 的人來說也許是個困擾。

    通常在還沒有推出去但感覺得有點亂(或太瑣碎)的 commit,我會先使用 rebase 來整理分支後再推出去。rebase 等於是在修改歷史,這個行為會做出平行時空,修改已經推出去的歷史可能會對其它人帶來困擾,所以對於已經推出去的內容,請不要任意使用 rebase。

【狀況題】怎麼有時候推不上去…

偶爾在執行 Push 指令的時候會出現這個錯誤訊息:

1
2
3
4
5
6
7
8
9
$ git push
To https://github.com/eddiekao/dummy-git.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://github.com/eddiekao/dummy-git.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

這段訊息的意思是線上版本的內容比你電腦裡這份還要新,所以 Git 不讓你推上去。

怎麼造成的?

通常這個狀況會發生在多人一起開發的時候,想像一下這個情境:

new repository

  1. Sherly 跟 Eddie 兩個人在差不多的時間都從 Git Server 上拉了一份資料下來準備進行開發。
  2. Sherly 手腳比較快,先完成了,於是先把做好的成果推一份上去。
  3. Eddie 不久後也完成了,但當他要推上去的時候發現推不上去了…

怎麼解決?

解決方法算是有兩招

第一招:先拉再推

因為你電腦裡的內容是比較舊的,所以你應該先拉一份線上版本的回來更新,然後再推一次:

1
2
3
4
5
6
7
8
9
$ git pull --rebase
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/eddiekao/dummy-git
37aaef6..bab4d89 master -> origin/master
First, rewinding head to replay your work on top of it...
Applying: update index

這裡加了 --rebase 參數是表示「內容抓下來之後請使用 Rebase 方式合併」,當然你想用一般的合併方式也沒問題。合併如果沒發生衝突,接著應該就可以順利往上推了。

第二招:無視規則,總之就是聽我的!(誤)

凡事總有先來後到,在上面的例子中,Sherly 先推上去的內容,後推的人就是應該拉一份下來更新,不然照規定是推不上去的。不過這規則也是有例外,只要加上了 --force 或是 -f 參數,它就會強迫硬推上去,把 Sherly 之前的內容蓋掉:

1
2
3
4
5
6
7
8
9
$ git push -f
Counting objects: 19, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (17/17), done.
Writing objects: 100% (19/19), 2.16 KiB | 738.00 KiB/s, done.
Total 19 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), done.
To https://github.com/eddiekao/dummy-git.git
+ 6bf3967...c4ea775 master -> master (forced update)

雖然這樣一定會成功,但接下來你就要去面對 Sherly,跟她解釋為什麼你把她的進度蓋掉了。更多關於 Force Push 的說明,可參考「【狀況題】聽說 git push -f 這個指令很可怕,什麼情況可以用它呢?」章節介紹。

Github Push 失败问题

[toc]

参考文章:

github push代码不成功

问题一:Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.

image-20220423220008550

remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.

remote: Please see https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/ for more information.

意思就是你原先的密码凭证从2021年8月13日开始就不能用了,要使用个人访问令牌(personal access token),就是把你的密码替换成token。

解决:github push更新token验证方式。

1、Please use a personal access token instead.(密码方式不适用了,需改使用token方式登录)

1、登录github官网进入setting

img

2.页面拉到底 找到 Developer Setting

3.右侧菜单栏找到Personal access tokens 点击后 找到右上角的Generate new token

img

4、token的使用

在终端上cd到要项目中,然后执行git pull。

image-20220423223148829

此时弹出

Username for ‘https://github.com': 此处输入你github 用户名

Password for ‘https://用户名@github.com’:把复制的token粘贴到此处(这里原先是输入密码的现在改成token)回车就好了

2、Failed to connect to github.com 443(登录方式是token了,但连接失败。)

image-20220423221025009

image-20220423221809343

解决方式,终端输入如下命令:

git config –global http.proxy

3、remote: Write access to repository not granted.(connect上了,但其他操作失败,需指定token的权限)

image-20220423222009245

解决:

image-20220423222457565

创建的时候下面的权限和是否设置过期时间(我是吧所有权限都勾选了) 根据自己情况选择 最后创建完后 吧token复制下来

4、LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

image-20220423224215251

fatal: unable to access ‘https://github.com/dvlproad/001-UIKit-CQDemo-Flutter.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

image-20220423223640807

解决:终端输入

git config http.sslVerify “false”

git config –global http.sslVerify “false”

三、访问GitHub遇到SSL_ERROR_SYSCALL错误解决方法

其他参考文章:访问GitHub遇到SSL_ERROR_SYSCALL错误解决方法

检查1:电脑上是否有curl-openssl(一般都有)

openssl version

image-20220423232921517

如果没有,则安装curl-openssl。安装方法如下:

1
brew install curl-openssl

image-20220423233608415

2、安装xxx时候失败,Error: No such file or directory @ rb_sysopen

1、原因

原来是一个依赖包下载不成功(harfbuzz-3.1.1.arm64_monterey)

解决:使用brew单独安装即可,然后再就是再次下载之前的包

用brew单独下载依赖包,就避免了找不到依赖版本的错误

1
brew install zstd

image-20220423234308334

之后再返回继续执行之前的

1
brew install curl-openssl

image-20220423234350492

Git

目录

  • 一、https 和 SSH 的区别
  • 二、在 github 上添加 SSH key 的步骤
  • 三、git撤销

前言

很多朋友在用github管理项目的时候,都是直接使用https url克隆到本地,当然也有有些人使用 SSH url 克隆到本地。然而,为什么绝大多数人会使用https url克隆呢?

这是因为,使用https url克隆对初学者来说会比较方便,复制https url 然后到 git Bash 里面直接用clone命令克隆到本地就好了。而使用 SSH url 克隆却需要在克隆之前先配置和添加好 SSH key 。

因此,如果你想要使用 SSH url 克隆的话,你必须是这个项目的拥有者。否则你是无法添加 SSH key 的。

一、https 和 SSH 的区别:

1、前者可以随意克隆github上的项目,而不管是谁的;而后者则是你必须是你要克隆的项目的拥有者或管理员,且需要先添加 SSH key ,否则无法克隆。

2、https url 在push的时候是需要验证用户名和密码的;而 SSH 在push的时候,是不需要输入用户名的,如果配置SSH key的时候设置了密码,则需要输入密码的,否则直接是不需要输入密码的。

二、在 github 上添加 SSH key 的步骤:

1、首先需要检查你电脑是否已经有 SSH key

进入~/.ssh目录,检查是否已经存在 id_rsa.pubid_dsa.pub 文件,如果文件已经存在,那么你可以跳过步骤2,直接进入步骤3。

2、创建一个 SSH key

$ ssh-keygen -t rsa -C "your_email@example.com"

1
2
3
4
代码参数含义:
-t 指定密钥类型,默认是 rsa ,可以省略。
-C 设置注释文字,比如邮箱。
-f 指定密钥文件存储文件名。

以上代码省略了 -f 参数,因此,运行上面那条命令后会让你输入一个文件名,用于保存刚才生成的 SSH key 代码,如:

1
2
Generating public/private rsa key pair.
# Enter file in which to save the key (/c/Users/you/.ssh/id_rsa): [Press enter]

当然,你也可以不输入文件名,使用默认文件名(推荐),那么就会生成 id_rsa 和 id_rsa.pub 两个秘钥文件。

接着又会提示你输入两次密码(该密码是你push文件的时候要输入的密码,而不是github管理者的密码),

当然,你也可以不输入密码,直接按回车。那么push的时候就不需要输入密码,直接提交到github上了,如:

1
2
Enter passphrase (empty for no passphrase): 
# Enter same passphrase again:

接下来,就会显示如下代码提示,如:

1
2
3
4
Your identification has been saved in /c/Users/you/.ssh/id_rsa.
# Your public key has been saved in /c/Users/you/.ssh/id_rsa.pub.
# The key fingerprint is:
# 01:0f:f4:3b:ca:85:d6:17:a1:7d:f0:68:9d:f0:a2:db your_email@example.com

当你看到上面这段代码的收,那就说明,你的 SSH key 已经创建成功,你只需要添加到github的SSH key上就可以了。

3、添加你的 SSH key 到 github上面去

进入账号的SSH key添加处,将id_rsa.pub 文件的内容复制上去添加即可。(记得 SSH key 代码的前后不要留有空格或者回车。)

4、测试一下该SSH key

在git Bash 中输入以下代码

$ ssh -T git@github.com
当你输入以上代码时,会有一段警告代码,如:

1
2
3
The authenticity of host 'github.com (207.97.227.239)' can't be established.
# RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
# Are you sure you want to continue connecting (yes/no)?

这是正常的,你输入 yes 回车既可。如果你创建 SSH key 的时候设置了密码,接下来就会提示你输入密码,如:

Enter passphrase for key ‘/c/Users/Administrator/.ssh/id_rsa’:
当然如果你密码输错了,会再要求你输入,知道对了为止。

注意:输入密码时如果输错一个字就会不正确,使用删除键是无法更正的。

密码正确后你会看到下面这段话,如:

1
2
Hi username! You've successfully authenticated, but GitHub does not
# provide shell access.

如果用户名是正确的,你已经成功设置SSH密钥。如果你看到 “access denied” ,者表示拒绝访问,那么你就需要使用 https 去访问,而不是 SSH 。

github上如何修改别人的开源项目并提交

github上的开源项目是人人都可download下来修改,并贡献源码的,但不是无原则的,如果开源作者提交的代码,被任何人都download下来修改并且随意提交的话,那会有很多恶意修改源码的行为出现,也就没有github良好的开源生态, 那么github上是怎么修改别人的源码并且提交,让自己成为同一项目的源码贡献者呢。有一下三种机制:

1. 开源作者把你加入项目合作者里面

首先他会进入自己项目的settings里,如下:

Collaborators
然后在Collaborators里面搜索你的github用户名,add进去,之后你会在github上受到一个invitation提示,确认一下之后,你就可以在任何IDE里面用git clone到本地,URI选择这个github的开源地址就行,相当于这个github地址作为一个git的远程代码服务器。

2. 第二种方法是fork到本地,然后pull request

找到这个开源项目,点击右上角fork按钮,然后在你自己的github里面就会出现这个项目,修改完之后,在你的github里点击pull request, 之后原作者会收到这个请求,通过之后你就贡献了自己的源码

Fork&pull request

3. 建立organization

登录自己github账号后,进入github.com, 点击右上角,创建一个开源项目组织,人人都可贡献源码

Organization

三、git撤销

git reset HEAD^
拉取最近一次提交到版本库的文件到暂存区 改操作不影响工作区

四、git删除远程提交

这是一个之后需要删除掉的远程commit

在需要删除掉的远程commit后,我又提交了一个commit

在需要删除掉的远程commit后,我又提交了第二个commit

git 删除远程仓库的某次提交

git删除远程提交

1
2
3
4
5
6
7
8
9
10
11
删除上一次远程仓库的提交,修改上次提交的代码,做一次更完美的commit。
>
git reset commitId (注:不要带–hard)到上个版本
>
git stash 暂存修改
>
git push --force 强制push,远程的最新的一次commit被删除
>
git stash pop 释放暂存的修改,开始修改代码
>
git add . -> git commit -m "massage" -> git push

git删除远程提交

End

Git Permission

一、git分支添加访问权限

image-20220303102746825

二、Gitlab仓库管理 设置保护分支

设置保护分支

把认为哪个个分支不想让别人动,就把这个分支设置为保护分支,并且只有授权的用户才可以向这个分支推送代码

必备知识架构-线程与网络-①线程

[toc]

目录

1
2
3
4
5
>1、多线程的原理
>2、多线程的优缺点
>3、多线程在iOS开发中的应用
>4、iOS中三种多线程技术(NSThread、NSOperation/NSOperationQueue、GCD)
>
1
2
3
4
5
6
>线程间通信的体现:
>1. 一个线程传递数据给另一个线程
>2. 在一个线程中执行完特定任务后,转到另一个线程继续执行任务
>
>附:其他传值方法总结
>
1
2
3
4
5
6
7
8
>1、通常的做法/不好的做法
>2、合理的做法(使用多线程异步执行)
>1)、先说说简单的多线程同步问题(异步线程里没有再开启异步线程)
>2)、真正的线程同步问题(异步线程里再开启异步线程)
>3)、其他补充
>附1:多个请求依次执行
>附2:并发数控制
>
1
2
>1、线程安全
>
1
2
3
>1、在使用SQLite过程中,如果多条线程同时操作同一数据库会造成什么问题,怎么解决?
>2、串行队列与并行队列的区别
>

一、多线程介绍

< 返回目录

1、多线程的原理

同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)。多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
思考:如果线程非常非常多,会发生什么情况?
答:CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。

2、多线程的优缺点

1
2
3
4
5
6
7
8
多线程的优点
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)

多线程的缺点
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享

3、多线程在iOS开发中的应用

主线程:

一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”

主线程的主要作用:

显示\刷新UI界面,处理UI事件(比如点击事件、滚动事件、拖拽事件等)

主线程的使用注意:

别将比较耗时的操作放到主线程中。耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验。

4、iOS中三种多线程技术(NSThread、NSOperation/NSOperationQueue、GCD)

1.NSThread
  1. 使用nsthread对象建立一个线程非常方便
  2. 但是!要使用nsthread管理多个线程非常困难,不推荐使用
  3. 技巧!使用[NSThread currentThread]跟踪任务所在线程,适用于这三种技术
2.NSOperation/NSOperationQueue

NSOperation和NSOperationQueue相关

  1. 是使用gcd实现的一套objective-c的api
  2. 是面向对象的线程技术
  3. 提供了一些在gcd中不容易实现的特性,如:限制最大并发数量、操作之间的依赖关系
1
2
3
4
5
6
7
8
9
10
> NSOperation`是系统提供的抽象的基类,我们使用的时候需要使用继承于它的子类。系统为我们提供了两种继承于`NSOperation`的子类,分别是`NSInvocationOperation`和`NSBlockOperation。
>
> 当将操作添加到主操作队列时,所有操作会按照添加到队列中的先后顺序串行依次执行。(主队列是GCD自带的一种特殊串行队列。)
> NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
> [mainQueue addOperation:op1];
>
> 当将任务添加到自定义队列的时候,会开启子线程,操作会并发执行。
> NSOperationQueue *customQueue = [[NSOperationQueue alloc]init];
> [customQueue addOperation:op1];
>
3.GCD —— grand central dispatch
  1. 是基于c语言的底层api
  2. 用block定义任务,使用起来非常灵活便捷
  3. 提供了更多的控制能力以及操作队列中所不能使用的底层函数

NSOperationQueue与GCD的使用原则和场景

NSOperationQueue与GCD的使用原则和场景

三、线程等待(线程同步)、线程依赖、任务顺序问题

< 返回目录

多个任务中,某个线程的执行依赖其他线程的执行完毕,或者某个线程的执行需等待其他线程执行完毕。

常见场景:
某个页面加载时通过网络请求获得相应的数据,再做某些操作。有时候加载的内容需要通过好几个请求的数据组合而成,比如有两个请求A和B。

多线程同步需求分析图

1、通常的做法/不好的做法:把并发队列变成串行

常见的有:为了省事,会将B请求放在A请求成功的回调中发起,将C请求放在B请求成功的回调中发起,在C的成功回调中将数据组合起来,这样做有明显的问题:

1
2
①、请求如果多了,需要写许多嵌套的请求
②、请求被变成了同步的方式了,这是最大的问题,在网络差的情况下,如果有n个请求,意味着用户要等待n倍于并发请求的时间才能看到内容

2、合理的做法(使用多线程异步执行)

1)、先说说简单的多线程同步问题(异步线程里没有再开启异步线程)

首先我们先来看简单的多线程同步问题(异步线程里没有再开启异步线程),这种时候,我们可以使用的方法有

使用GCD线程组dispatch_group的dispatch_group_notify即可解决问题。其中notify的作用就是在group中的其他操作全部完成后,再操作自己的内容。
② 使用NSOperationQueue中operation的addDependency
③使用dispatch_barrier_async方法。

①、使用GCD线程组dispatch_group的dispatch_group_notify即可解决问题

使用dispatch_group方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
NSLog(@"Request_1");
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
NSLog(@"Request_2");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
②使用使用NSOperationQueue中operation的addDependency,则是
1
2
3
4
5
6
7
//4.设置依赖
[operation3 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
③dispatch_barrier_async

2)、真正的线程同步问题(异步线程里再开启异步线程)

对于上面的例子,当将上面三个操作改成真实的网络操作后,这个简单的做法突然变得无效了,这是为什么呢?

其实这个道理很简单,因为我们开启的网络请求,是一个异步线程,所谓的异步线程,就是告诉系统你不要管我是否完成了,你尽管执行其他操作,开一个线程让我到外面操作去执行就行了,对我的处理你已经完成了,也就是说线程只负责将请求发出去,就认为自己的任务算完成了。所以,当三个请求都发送出去后,不会管网络操作是否完成,就会执行notify中的内容,但由于请求结果本身需要一定的时间,所以导致了界面都刷新了,而请求结果才返回。

即上面的问题,总结为如果dispatch_group_async里执行的是异步代码dispatch_group_notify会直接触发而不会等待异步任务完成。

所以,对于这种异步线程里开异步线程的线程同步问题,该怎么处理呢?

为了应对这种异步线程里开异步线程的线程同步问题,其实我们需要多处理的只是监控异步线程里的所有操作(包括异步线程里的异步线程)都结束后,才算这个异步线程结束

这里我们的解决方法常见的有:

方法A:直接使用dispatch_group_enter和dispatch_group_leave,即只需要在任务开始前enter和结束后leave即可达到线程同步的效果。

dispatch_group_enter(group):下面的任务由group组管理,group组的任务数+1
dispatch_group_leave(group):相应的任务执行完成,group组的任务数-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)testGroupSync_userEnterLeave {
dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
dispatch_group_leave(group);
});
});

dispatch_group_enter(group);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
dispatch_group_leave(group);
});
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
}

方法B:使用信号量,控制异步线程的结束,来达到多线程同步的效果。

在GCD中有三个函数是semaphore的操作,分别是:

1
2
3
  dispatch_semaphore_create   创建一个semaphore
  dispatch_semaphore_signal   发送一个信号(信号量+1)
  dispatch_semaphore_wait    等待信号(wait执行完后,信号量-1)

实际使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)testGroupSync_semaphore_signal_wait {
dispatch_group_t group = dispatch_group_create();

// 创建一个控制线程同步的信号量,初始值为0(红灯)
dispatch_semaphore_t syncSemaphore = dispatch_semaphore_create(0);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//先再任务1的线程里中再开启一个异步线程执行请求1,同时堵塞住此时的任务1所在的线程,等到已开始的异步请求1结束之后,原本阻塞住的任务1线程才会恢复畅通,才代表任务1结束了。
//请求1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
// 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯)
dispatch_semaphore_signal(syncSemaphore);
});

// 开启信号等待,设置等待时间为永久,直到信号的信号量大于等于1(等绿灯)
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
// 使信号的信号量+1,这里的信号量本来为0,+1信号量为1(绿灯)
dispatch_semaphore_signal(syncSemaphore);
});

// 开启信号等待,设置等待时间为永久,直到信号的信号量大于等于1(等绿灯)
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER);
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
}

附:实际上,这段代码是一个常用的不控制并发数,只控制线程阻塞,实现线程同步的例子。在这里它是将等待信号dispatch_semaphore_wait放在线程执行后。往下看,待会会介绍控制并发数的例子。

上面例子中,先再任务1的线程里中再开启一个异步线程执行请求1,同时堵塞住此时的任务1所在的线程,等到已开始的异步请求1结束之后,恢复畅通,才代表任务1结束了。

3)、其他控制任务顺序的例子

附1:多个请求依次执行

例子:三个任务分别为下载图片,打水印和上传图片,三个任务需异步执行但需要顺序性。

对于这个问题通常会通过线程依赖进行解决。

  • 1、如果是GCD的话,设置线程依赖比较繁琐,所以这里就不讨论了。
  • 2、如果是NSOperation的话,我们直接通过addDependency来设置一个操作队列NSOperationQueue中的线程依赖就可以了。

代码如下(此例子不讨论线程里开线程,如果需要的话,请将信号量的使用添加上去即可解决):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 //1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
[self request_A];
}];

//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[self request_B];
}];

//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
[self request_C];
}];

//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

四、多线程并发数控制

当我们在处理一系列线程的时候,当数量达到一定量,我们需要处理并发数控制。

  • 1、如果是GCD的话,怎么快速的控制并发呢?答案就是dispatch_semaphore。这个比较复杂,但我们这边还是谈一下
  • 2、如果是NSOperation的话,我们直接使用NSOperationQueue来控制并发,这个就不谈了。

GCD使用信号量dispatch_semaphore控制并发

说道信号量,这里顺便谈下信号量的概念:

信号量就是一个资源计数器,它是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);,线程会被阻塞(如果有必要的话),直至计数器大于零,然后线程会减少这个计数。

上面的例子中,信号量初始值为0,其先执行到dispatch_semaphore_wait,此时由于信号量为0,所以造成了阻塞,而使得线程1没能结束它的工作。而到线程1里的异步线程执行完后,给信号量发送了一个通知,使得信号量的值加上了1,此时刚才线程1里的dispatch_semaphore_wait发现信号量大于等于1了,它就不再阻塞,而是继续执行下去,从而使得了线程1这时候真正结束了它所应该处理的工作。wait执行完后,信号量又减1了。

也就是通过这种线程阻塞/等待的方法,我们实现了线程的同步。

当然信号量的用途,除了可以用来实现线程同步外,还可以用来实现控制GCD的并发数

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)testBingfa {
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建信号量,并且设置值为10
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++)
{ // 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i);
sleep(2);
// 每次发送信号则semaphore会+1,
dispatch_semaphore_signal(semaphore);
});
}
}

比较这段代码与上面的代码,控制并发一般都是将等待信号dispatch_semaphore_wait放在线程执行前。而我们常用的只控制线程阻塞,实现线程同步的,都是将等待信号dispatch_semaphore_wait放在线程执行后。所以,如果一个多线程,它需要同时控制线程同步和线程并发数的话,那它就需要创建两个信号量来分别控制。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- (void)testGroupSyncAndBingfa_semaphore_signal_wait {
dispatch_group_t group = dispatch_group_create();

// 创建一个控制线程并发的信号量,初始值为最大并发数2(红灯)
dispatch_semaphore_t bingfaSemaphore = dispatch_semaphore_create(2);

// 创建一个控制线程同步的信号量,初始值为0(红灯)
dispatch_semaphore_t syncSemaphore = dispatch_semaphore_create(0);

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号

//请求1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
dispatch_semaphore_signal(syncSemaphore); //同步信号
});
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号

//请求2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
dispatch_semaphore_signal(syncSemaphore); //同步信号
});
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(bingfaSemaphore, DISPATCH_TIME_FOREVER); //并发信号

//请求3
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(6);
NSLog(@"任务三完成");
dispatch_semaphore_signal(syncSemaphore); //同步信号
});
dispatch_semaphore_wait(syncSemaphore, DISPATCH_TIME_FOREVER); //同步信号
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
}

通过所附加的两个例子,我们明显的看出NSOperation在处理线程依赖以及并发数的问题上,明显更高级。所以,如果有类似的问题,我们优先使用NSOperation来处理。

常见笔试/面试题

< 返回目录

1、在使用SQLite过程中,如果多条线程同时操作同一数据库会造成什么问题,怎么解决?

答:(1)容易造成系统崩溃
(2)解决方案:开启串行模式,使用一个类(单例方式)操作数据库。

END

< 返回目录

必备知识架构

知识架构

iOS知识库

Android知识库

目录

  • 十一、谈谈设计模式
  • 十二、如何优化过于臃肿的Controller
  • 十三、谈谈性能优化(功耗)
  • 十四、UITableView的优化、重用
  • 十五、布局 layoutsubview、drawrect等
  • 十六、有逼格的代码
  • 十七、单元测试
  • 十八、APP审核

  • 常见笔试/面试题

  • END

十一、谈谈设计模式

< 返回目录

十二、如何优化过于臃肿的Controller

< 返回目录
如何优化过于臃肿的Controller

十三、谈谈性能优化(功耗)

< 返回目录

UITableView的优化、重用

< 返回目录

布局 layoutsubview、drawrect等

< 返回目录

有逼格的代码

typedef NSString * NSRunLoopMode NS_EXTENSIBLE_STRING_ENUM;
提升自己逼格的编程之美之代码规范
iOS开发细节 | 通知怎么写?
实现NS_ENUM的自定义反射

单元测试

< 返回目录

APP审核

< 返回目录

如果一个页面被苹果拒回。那怎么通过审核。(参考金融类APP提交苹果审核,怎么避开5.2.1)

答:

1.第一种是有后台控制壳什么时候出现。移动端传一个版本号给后台,后台根据版本号判断App否在审核,在审核就显示壳,不在审核就显示原生界面。

2.直接由移动端来切换界面。此时App需要登录才能看到主界面,对给苹果审核人员的测试账号做一个判断,如果是测试账号,就显示壳,非测试账号显示原生界面。

其中:

第一种方式用户进来就可以看到主界面,更有利于吸引自然流量,但是如果苹果后期复查,发现了用户使用的App跟审核的不一样,会被下架。

第二种方式不利于吸引自然流量,但是被下架的风险很小,因为苹果的审核团队在美国,不可能自己去注册账号,复查也没关系,除非有人举报你的App。

常见笔试/面试题

< 返回目录

1、如何使用一个for循环输出九九乘法表

NSJSONSerialization
1
2
NSData *data = [operation.responseString dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary *responseObject_dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves error:nil];

//NSJSONReadingMutableContainers的作用: http://blog.csdn.net/chenyong05314/article/details/45691041

1
2
3
NSJSONReadingMutableContainers:返回可变容器,NSMutableDictionary或NSMutableArray。
NSJSONReadingMutableLeaves:返回的JSON对象中字符串的值为NSMutableString
NSJSONReadingAllowFragments:允许JSON字符串最外层既不是NSArray也不是NSDictionary,但必须是有效的JSON Fragment。例如使用这个选项可以解析 @“123” 这样的字符串。

iOS中常见Crash总结

END

< 返回目录