日期: 2021 年 7 月 20 日

简单使用Git和Github来管理自己的代码和读书笔记

 

  • 以前不知道使用代码管理工具,*后写的一些东西都没有了,由于硬盘坏了或者不小心格式化了之类的,后来使用了Git

和Github来托管自己的代码和读书笔记方便了不少,到哪里只要有网就可以把自己的东西拷贝下来继续使用。

我这里简单的记录一下我使用的过程,*简单的使用都是,高级的功能我一直没有使用到,虽然买一本《Git权威指南》

但是很多东西用不到就不能够真的会。下面开始简单介绍我使用的方法,我这个是在windows上使用的。我使用分两种情况,

因为我的代码都是在Linux下写的,所以在linux下主要是托管代码用,在windows下主要是托管笔记使用的,比如一些PDF

文档,我在看的时候会加入自己的注释,这样使用托管功能,在哪里都可以接着注释,不用总是拷贝或者总是需要复制。

  1. 先注册github.com的账号官方网站: https://github.com/

注册界面,*个用户名,以后会用到

简单使用Git和Github来管理自己的代码和读书笔记

2.      登录界面

简单使用Git和Github来管理自己的代码和读书笔记

3.      登录成功后界面

简单使用Git和Github来管理自己的代码和读书笔记

4.      创建仓库,我现在想使用github来托管自己的NowToDo项目的代码,因此先要创建一个仓库,仓库分公开的和私有的,公开的是免费的,私有的是收费的,我使用的是公开的仓库,如下创建方式

简单使用Git和Github来管理自己的代码和读书笔记

点击New repository按钮,弹出如下界面,*行填仓库名,这里就随便叫Test了,第二行是对这个仓库的描述,之后那个Public就是公共仓库的意思,接下来的README就是在仓库里创建一个README文件,可以往里写一些介绍你这个项目的功能之类的东西,再下面那个Add gitignore按钮,可以选择你这个项目是用什么语言之类的,我这里选择了Qt,后面那个License我没有选,点击”Create repository”

简单使用Git和Github来管理自己的代码和读书笔记

5.      创建仓库成功后,界面如下显示,可以点击README.md来编译这个文件

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

在windows下安装Git

1.      下载网址: http://code.google.com/p/msysgit/downloads/list

2.      下载完毕,打开安装,点击”next”

简单使用Git和Github来管理自己的代码和读书笔记

3.      点击”next”

简单使用Git和Github来管理自己的代码和读书笔记

4.      可以更改安装路径后,点击”next”

简单使用Git和Github来管理自己的代码和读书笔记

5.      按照默认的就可以,我这里更改了一处,可以不改,这个*好还是用默认配置好,不要改了,我第二次装就没有改这里,要不然可以有不一致的地方,推荐不要改了,使用默认

简单使用Git和Github来管理自己的代码和读书笔记

6.      直接点”next”

简单使用Git和Github来管理自己的代码和读书笔记

7.      默认设置就可以

简单使用Git和Github来管理自己的代码和读书笔记

8.      默认设置就可以,如果你的机器装了SVN的话,这里可能还会有一步,就是让你选择SSH的,也不要更改,就直接默认就好,也就是说安装这个软件的时候,其实只有安装路径想改的话,更改一下就好了,其它的都保持默认就OK了

简单使用Git和Github来管理自己的代码和读书笔记

9.      点击”next”

简单使用Git和Github来管理自己的代码和读书笔记

使用git和github托管项目代码

1.      双击图标”Git Bash”

简单使用Git和Github来管理自己的代码和读书笔记

2.      打开界面如下

简单使用Git和Github来管理自己的代码和读书笔记

3.      配置Git,图示如下:

a)

1
输入

 

      $ ssh-keygen -t rsa -"abcd@efgh.com" //邮箱同上

 

b)        回车之后,会出现一行,让你输入一个保存密钥的地方,括号里面是它默认的位置,可以在冒号后面自己写一个位置保存,我这个是在E盘下创建了一个目录叫Hello,后面那个id_rsa就是密钥要保存的文件名,这个文件是自动生成的,后生成两个一个叫id_rsa,一个叫id_rsa.pub,我这么做了之后,发现生成的密钥目录里少了一个文件known_hosts,如果按照默认的目录的话是有这个文件的,如果没有这个文件,后序出现了问题,因此,建议还是直接用他的默认路径就好,这里就不要自己填路径了,直接回车

c)        回车之后,会设置密码,我这里没有设置,直接点了回车两次,这样密钥就生成了,可以打开id_rsa.pub来查看,我使用的是记事本直接打开的这个文件,里面的所有内容就是这个密钥,一会需要使用的时候,就直接全选复制就可以了

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

d)        现在转到github网站上去配置一下ssh key,点击箭头指示图标Account settings,然后点击左侧的SSH Keys,之后点击右侧的Add SSH Key,这样就会出现添加SSH Key的界面,在Title这一栏填一个名字,名字随意起,之后打开刚才生成的那个文件id_rsa.pub,全选复制里面的内容到Key这一栏中,点击Add Key按钮完成操作,这时你填的邮箱会收到一封确认的邮件,不用管它

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

e)        验证一下是否设置成功,在git bash下输入如下命令:

ssh –T git@github.com

出现如下图示,因为我不是*次设置了,所以是这样的,如果你是*次,会让你输入yes或no,这时输入yes就可以了,其它显示就和我这个是一样的。如果你的是出现不是这些内容,有可能是显示权限问题什么的,就应该是我上面提到的那种情况,你看一下你生成密钥时是否操作正确,目录下是否有那个known_hosts这个文件

简单使用Git和Github来管理自己的代码和读书笔记

f)        现在配置一下用户名和邮箱:

git config –global user.name “用户名”

git config –global user.email “邮箱”

简单使用Git和Github来管理自己的代码和读书笔记

4.      到现在为止,我们就算把Git和github配置完了,我现在要做的事情是把自己的QT项目NowToDo托管到github上,那么就开始操作吧

a)        随意创建了一个目录叫git_project

简单使用Git和Github来管理自己的代码和读书笔记

b)        右击目录,出现的菜单中有Git Init Here,点击它,这时在这个目录下会出现一个隐藏目录.git,这个是有关配置等功能的,不用管,我的项目NowToDo在这个NowToDo目录下,包括.cpp等文件

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

c)        现在回到git_project那里,在这个目录上继续右击,点击Git Bash,现在就出现一个和开始使用那个Git Bash一样的窗口,不过现在是在git_project目录下使用了,其实如果知道Shell命令,可以直接使用刚才的那个窗口直接使用cd命令进入这个目录下,效果是一样的,现在输入如下命令:

git remote add origin git@github.com:bxxfighting/NowToDo.git

其中bxxfighting是我在网站上注册时使用的用户名,NowToDo.git是我为这个项目建立的另一个仓库名,在网站上显示是这样的

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

d)        由于我建立仓库的时候创建README.md之时,已经算一次提交了,我需要先在本地同步一下仓库的内容,命令如下:

git pull git@github.com:bxxfighting/NowToDo.git

完成的效果如下图,并且本地目录下多出了两个文件,这是原本在github上的两个文件

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

e)        下面就要把我们本地的上传到仓库上去了,首先执行增加命令,如下:

git add .

add后面加了一个点,是想要提交所有文件,如果想提交指定的文件,可以写文件名,执行完增加命令后,要执行提交命令,如下:

git commit –m “NowToDo_v1.0版本”

-m后面跟提示信息,这个提示信息是一定要写的,不仅是规则,同时也方便我们记录我们提交的过程,写清晰为什么提交或修改了什么是非常有用的,提交完成后,我们就要把它推送到远程仓库上去了,命令如下:

git push git@github.com:bxxfighting/NowToDo.git

这样就完成了我们要做的所有任务

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

简单使用Git和Github来管理自己的代码和读书笔记

到这就差不多了,我每次用其实都来看看,不是总是用,有点记不住,*次这样就过就记下了,方便以后使用。其实

这真是非常方便的,可以使用熟练,管理自己的代码和笔记很好的东西,以前管理代码,*近发现每次看电子书,里面写上笔记,但是同步费劲,现在决定用它来管理了,回去在自己的笔记本上也这样来用,就可以同步自己看的进度了。

iOS cocoPod的基本使用

虽然网上关于CocoaPods安装教程多不胜数,但是我在安装的过程中还是出现了很多错误,所以大家可以照下来步骤装一下,我相信会很好用.

前言

iOS项目中使用第三方类库可以说是非常常见的事,但是要正确地配置他们有时候是非常繁琐的事情,幸运的是CocoaPods是一个很好的解决方案。

什么是CocoaPods

CocoaPodsOS XiOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods”的依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。

Cocoapods意义体现在两个方面。*,在引入第三方库时它可以自动为我们完成各种各样的配置,包括配置编译阶段、连接器选项、甚至是ARC环境下的-fno-objc-arc配置等。第二,使用CocoaPods可以很方便地查找新的第三方库,这些类库是比较标准的,而不是网上随便找到的,这样可以让我们找到真正好用的类库。

接下来我们将介绍CocoaPods的使用。

CocoaPods的核心组件

CocoaPods是用Ruby写的,并划分成了若干个Gem包。

CocoaPods在解析执行过程中*重要的几个包的路径分别是:CocoaPods/CocoaPodsCocoaPods/Core CocoaPods/Xcodeproj 

CocoaPods / CocoaPod:这是面向用户的组件,每当执行一个pod命令时,这个组件将被激活。它包括了所有实用CocoaPods的功能,并且还能调用其他gem包来执行任务。 

CocoaPods / CoreCore gem提供了与CocoaPods相关的文件(主要是podfilepodspecs)的处理。 

Podfile:该文件用于配置项目所需要的第三方库,它可以被高度定制。本文中我们主要在这里做动作。

Podspec:该文件描述了一个库将怎样被添加进工程中。.podspec文件可以标识该第三方库所需要的源码文件、依赖库、编译选项,以及其他第三方库需要的配置。 

CocoaPods / Xcodeproj:这个包负责处理工程文件,它能创建以及修改.xcodeproj文件和.xcworkspace文件。它也可以作为一个独立的包使用,当你要编写修改项目文件的脚本时,可以考虑使用CocoaPods/Xcodeproj

 

Cocoapods安装步骤

1、升级Ruby环境

终端输入:$gem update –system

此时会出现

ERROR:  While executing gem … (Gem::FilePermissionError)

    You don‘t have write permissions for the /Library/Ruby/Gems/2.0.0 directory.

zijingMacBook-Pro:~ shiyunlei$ sudo gem update —system

这个是因为你没有权限去更新Ruby

这个时候你可以在终端使用:$sudo gem update —system来进行升级

 

这个时候表示Ruby升级完成。

2、安装CocoaPods时我们要访问cocoapods.org,该网站可能被我们大天朝墙了,这样下载安装可能会是龟速,幸好我们可以用淘宝的Ruby镜像来访问该网站,方法如下:

1、gem sources –removehttps://rubygems.org/

2、gem sources -ahttp://ruby.taobao.org/

可以使用下面的命令(红色部分)查看是否更换镜像成功(如下图所示):
$ gem sources -l

%title插图%num

3、安装Cocoapods,在终端输入命令

$sudo gem install cocoapods

这个时候会提示你输入密码,注:在终端输入密码不会有提示,光标也不会移动,一定要注意

开始安装,如下图:PS:这个过程可能花费时间比较长

%title插图%num

安装成功如下:

%title插图%num

4、使用search命令搜索类库名: 

$pod search AFNetworking

如果CocoaPods支持,将会输出搜索到的所有类库版本和信息,以及在Podfile中配置的写法,终端返回信息如下:

-> AFNetworking (2.3.1)

   A delightful iOS and OS X networking framework.

  pod ‘AFNetworking‘, ‘~> 2.3.1’(这个语句是需要在编辑Podfile的时候输入的命令,即是如何安装下载相关类库的指令)

   – Homepage: https://github.com/AFNetworking/AFNetworking

   – Source:   https://github.com/AFNetworking/AFNetworking.git

   – Versions: 2.3.1, 2.3.0, 2.2.4, 2.2.3, 2.2.2, 2.2.1, 2.2.0, 2.1.0,

   2.0.3, 2.0.2, 2.0.1, 2.0.0, 2.0.0-RC3, 2.0.0-RC2, 2.0.0-RC1, 1.3.4, 1.3.3,

   1.3.2, 1.3.1, 1.3.0, 1.2.1, 1.2.0, 1.1.0, 1.0.1, 1.0, 1.0RC3, 1.0RC2, 1.0RC1,

   0.10.1, 0.10.0, 0.9.2, 0.9.1, 0.9.0, 0.7.0, 0.5.1 [master repo]

   – Sub specs:   – AFNetworking/Serialization (2.3.1)   –

   AFNetworking/Security (2.3.1)   – AFNetworking/Reachability (2.3.1)   –

   AFNetworking/NSURLConnection (2.3.1)   – AFNetworking/NSURLSession (2.3.1)

   – AFNetworking/UIKit (2.3.1)

-> AFNetworking+AutoRetry (0.0.5)

   Auto Retries for AFNetworking requests

   pod ‘AFNetworking+AutoRetry‘, ‘~> 0.0.5‘

   – Homepage: https://github.com/shaioz/AFNetworking-AutoRetry

   – Source:   https://github.com/shaioz/AFNetworking-AutoRetry.git

   – Versions: 0.0.5, 0.0.4, 0.0.3, 0.0.2, 0.0.1 [master repo]

5、需要在工程中创建一个Podfile文件,

使用命令:

$cd /Users/shiyunlei/Desktop/CocoapodsSample(进入工程目录,cd后面的是工程的路径)

$ touch Podfile(创建Podfile文件)

%title插图%num

然后使用vim编辑Podfile文件,使用命令:

%title插图%num
进入如下界面

%title插图%num

然后紧接着按键盘上的“i”键,变成如下状态,这个时候可以对Podfile文件进行编辑:

$platform:ios, ‘7.0‘

$ pod ‘AFNetworking‘, ‘~> 2.3.1‘

在编辑完成后按“esc”,

%title插图%num

在按“:”,这个时候输入wq,点击回车,保存并退出。

%title插图%num

这个时候打开Podfile文件就会看到里面添加了刚才在终端输入的一行语句,如下图

%title插图%num

然后在终端输入命令安装相应的第三方类库

$pod install

%title插图%num

因为这个过程需要下载相应的类库,所以时间和网速有关。

安装成功之后截图如下:

%title插图%num

这个时候会看到之后打开工程都需要从类型为工程名.xcworkspace文件打开。

%title插图%num

Git中.gitignore文件不起作用的解决以及Git中的忽略规则介绍

在Studio里使用Git管理代码的过程中,可以修改.gitignore文件中的标示的方法来忽略开发者想忽略掉的文件或目录,如果没有.gitignore文件,可以自己手工创建。在.gitignore文件中的每一行保存一个匹配的规则例如:

1
2
3
4
5
6
7
# 此为注释 – 将被 Git 忽略
*.a       # 忽略所有 .a 结尾的文件
!lib.a    # 但 lib.a 除外
/TODO     # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/    # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt

在填写忽略文件的过程中,我发现在Android Studio里面,.gitignore中已经标明忽略的文件目录下的文件,当我想git push的时候还会出现在push的目录中,原因是因为在Studio的git忽略目录中,新建的文件在git中会有缓存,如果某些文件已经被纳入了版本管理中,就算是在.gitignore中已经声明了忽略路径也是不起作用的,这时候我们就应该先把本地缓存删除,然后再进行git的push,这样就不会出现忽略的文件了。git清除本地缓存命令如下:

1
2
3
git rm -r --cached .
git add .
git commit -m 'update .gitignore'

 

Git查看、删除、重命名远程分支和tag

Git查看、删除、重命名远程分支和tag

 


文章记录我在使用git的过程中碰到远程分支和tag的相关内容,提纲:

  1. 查看远程分支
  2. 删除远程分支和tag
  3. 删除不存在对应远程分支的本地分支
  4. 重命名远程分支
  5. 把本地tag推送到远程
  6. 获取远程tag

查看远程分支

加上-a参数可以查看远程分支,远程分支会用红色表示出来(如果你开了颜色支持的话):

1
2
3
4
5
6
7
8
9
10
$ git branch -a
master
remote
tungway
v1.52
* zrong
remotes/origin/master
remotes/origin/tungway
remotes/origin/v1.52
remotes/origin/zrong

删除远程分支和tag

在Git v1.7.0 之后,可以使用这种语法删除远程分支:

1
$ git push origin –delete <branchName>

删除tag这么用:

1
git push origin –delete tag <tagname>

否则,可以使用这种语法,推送一个空分支到远程分支,其实就相当于删除远程分支:

1
git push origin :<branchName>

这是删除tag的方法,推送一个空tag到远程tag:

1
2
git tag -d <tagname>
git push origin :refs/tags/<tagname>

两种语法作用完全相同。

删除不存在对应远程分支的本地分支

假设这样一种情况:

  1. 我创建了本地分支b1并pull到远程分支 origin/b1
  2. 其他人在本地使用fetch或pull创建了本地的b1分支;
  3. 我删除了 origin/b1 远程分支;
  4. 其他人再次执行fetch或者pull并不会删除这个他们本地的 b1 分支,运行 git branch -a 也不能看出这个branch被删除了,如何处理?

使用下面的代码查看b1的状态:

1
2
3
4
5
6
7
8
9
10
11
12
$ git remote show origin
* remote origin
Fetch URL: git@github.com:xxx/xxx.git
Push URL: git@github.com:xxx/xxx.git
HEAD branch: master
Remote branches:
master tracked
refs/remotes/origin/b1 stale (use ‘git remote prune’ to remove)
Local branch configured for ‘git pull’:
master merges with remote master
Local ref configured for ‘git push’:
master pushes to master (up to date)

这时候能够看到b1是stale的,使用 git remote prune origin 可以将其从本地版本库中去除。

更简单的方法是使用这个命令,它在fetch之后删除掉没有与远程分支对应的本地分支:

1
git fetch -p

重命名远程分支

在git中重命名远程分支,其实就是先删除远程分支,然后重命名本地分支,再重新提交一个远程分支。

例如下面的例子中,我需要把 devel 分支重命名为 develop 分支:

1
2
3
4
5
6
7
8
9
10
$ git branch -av
* devel 752bb84 Merge pull request #158 from Gwill/devel
master 53b27b8 Merge pull request #138 from tdlrobin/master
zrong 2ae98d8 modify CCFileUtils, export getFileData
remotes/origin/HEAD -> origin/master
remotes/origin/add_build_script d4a8c4f Merge branch ‘master’ into add_build_script
remotes/origin/devel 752bb84 Merge pull request #158 from Gwill/devel
remotes/origin/devel_qt51 62208f1 update .gitignore
remotes/origin/master 53b27b8 Merge pull request #138 from tdlrobin/master
remotes/origin/zrong 2ae98d8 modify CCFileUtils, export getFileData

删除远程分支:

1
2
3
$ git push –delete origin devel
To git@github.com:zrong/quick-cocos2d-x.git
– [deleted] devel

重命名本地分支:

1
git branch -m devel develop

推送本地分支:

1
2
3
4
5
6
7
8
$ git push origin develop
Counting objects: 92, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (48/48), done.
Writing objects: 100% (58/58), 1.38 MiB, done.
Total 58 (delta 34), reused 12 (delta 5)
To git@github.com:zrong/quick-cocos2d-x.git
* [new branch] develop -> develop

然而,在 github 上操作的时候,我在删除远程分支时碰到这个错误:

1
2
3
4
5
$ git push –delete origin devel
remote: error: refusing to delete the current branch: refs/heads/devel
To git@github.com:zrong/quick-cocos2d-x.git
! [remote rejected] devel (deletion of the current branch prohibited)
error: failed to push some refs to ‘git@github.com:zrong/quick-cocos2d-x.git’

这是由于在 github 中,devel 是项目的默认分支。要解决此问题,这样操作:

  1. 进入 github 中该项目的 Settings 页面;
  2. 设置 Default Branch 为其他的分支(例如 master);
  3. 重新执行删除远程分支命令。

把本地tag推送到远程

1
git push –tags

获取远程tag

1
git fetch origin tag <tagname>

git本地及远程分支回退

1. git本地版本回退

Git reset --hard commit_id(可用 git log –oneline 查看)

2. git远程版本回退

git push origin HEAD --force #远程提交回退

下面的命令也可以实现远程版本回退

git reset --hard HEAD~1
git push --force

3. git reverse和git reset的区别

    1. git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit。
    1. 在回滚这一操作上看,效果差不多。但是在日后继续merge以前的老版本时有区别。因为git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,导致这部分改变不会再次出现,但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入。
    1. git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容。

git reset + commit号

git reset命令后面是需要加2种参数的:
”–hard””–soft”

这条命令默认情况下是”–soft”。执行上述命令时,这该条commit号之 后(时间作为参考点)的所有commit的修改都会退回到git缓冲区中。

使用git status命令可以在缓冲区中看到这些修改。而如果加上”–hard”参数,则缓冲区中不会存储这些修改,git会直接丢弃这部分内容。

但需要注意的一 个问题是:由于这样的重置是直接在本地的修改,无法提交到远程服务器,如果直接丢弃的内容已经被推到远程服务器上了,则会造成本地和服务器无法同步的问题。

即git reset –hard只能针对本地操作,不能针对远程服务器进行同样操作。如果从本地删掉的内容没有推到服务器上,则不会有副作用;如果被推到服务器,则下次本地和 服务器进行同步时,这部分删掉的内容仍然会回来。

而上面注意中提到的问题则可以很好的被git revert 命令解决。

git revert + commit 号
该命令撤销对某个commit的提交,这一撤销动作会作为一个新的修改存储起来,这样,当你和服务器同步时,就不会产生什么副作用。

关于Git 详解

1. Git

1.1. Git是何方神圣?

Git是用C语言开发的分布版本控制系统。版本控制系统可以保留一个文件集合的历史记录,并能回滚文件集合到另一个状态(历史记录状态)。另一个状 态可以是不同的文件,也可以是不同的文件内容。举个例子,你可以将文件集合转换到两天之前的状态,或者你可以在生产代码和实验性质的代码之间进行切换。文 件集合往往被称作是“源代码”。在一个分布版本控制系统中,每个人都有一份完整的源代码(包括源代码所有的历史记录信息),而且可以对这个本地的数据进行 操作。分布版本控制系统不需要一个集中式的代码仓库。

当你对本地的源代码进行了修改,你可以标注他们跟下一个版本相关(将他们加到index中),然后提交到仓库中来(commit)。Git保存了所 有的版本信息,所以你可以转换你的源代码到任何的历史版本。你可以对本地的仓库进行代码的提交,然后与其他的仓库进行同步。你可以使用Git来进行仓库的 克隆(clone)操作,完整的复制一个已有的仓库。仓库的所有者可以通过push操作(推送变更到别处的仓库)或者Pull操作(从别处的仓库拉取变 更)来同步变更。

Git支持分支功能(branch)。如果你想开发一个新的产品功能,你可以建立一个分支,对这个分支的进行修改,而不至于会影响到主支上的代码。

Git提供了命令行工具;这个教程会使用命令行。你也可以找到图形工具,譬如与Eclipse配套的EGit工具,但是这些都不会在这个教程中进行描述。

1.2. 重要的术语

表 1. Git 术语

术语 定义
仓库(Repository) 一个仓库包括了所有的版本信息、所有的分支和标记信息。在Git中仓库的每份拷贝都是完整的。仓库让你可以从中取得你的工作副本。
分支(Branches) 一个分支意味着一个独立的、拥有自己历史信息的代码线(code line)。你可以从已有的代码中生成一个新的分支,这个分支与剩余的分支完全独立。默认的分支往往是叫master。用户可以选择一个分支,选择一个分支叫做checkout.
标记(Tags) 一个标记指的是某个分支某个特定时间点的状态。通过标记,可以很方便的切换到标记时的状态,例如2009年1月25号在testing分支上的代码状态
提交(Commit) 提交代码后,仓库会创建一个新的版本。这个版本可以在后续被重新获得。每次提交都包括作者和提交者,作者和提交者可以是不同的人
URL URl用来标识一个仓库的位置
修订(Revision) 用来表示代码的一个版本状态。Git通过用SHA1 hash算法表示的id来标识不同的版本。每一个 SHA1 id都是160位长,16进制标识的字符串.。*新的版本可以通过HEAD来获取。之前的版本可以通过”HEAD~1″来获取,以此类推。

1.3. 索引

Git 需要将代码的变化显示的与下一次提交进行关联。举个例子,如果你对一个文件继续了修改,然后想将这些修改提交到下一次提交中,你必须将这个文件提交到索引中,通过git add file命令。这样索引可以保存所有变化的快照。

新增的文件总是要显示的添加到索引中来。对于那些之前已经提交过的文件,可以在commit命令中使用-a 选项达到提交到索引的目的。

2. 安装

在Ubuntu上,你可以通过apt来安装git命令行工具

 

sudo apt-get install git-core

对于其他的Linux版本,请查看相关的软件包安装工具使用方法

msysgit项目提供了Windows版本的Git,地址是http://code.google.com/p/msysgit/

3. 配置

你可以在.gitconfig文件中防止git的全局配置。文件位于用户的home目录。 上述已经提到每次提交都会保存作者和提交者的信息,这些信息都可以保存在全局配置中。

后续将会介绍配置用户信息、高亮显示和忽略特定的文件

3.1. 用户信息

通过如下命令来配置用户名和Email

复制代码
# Configure the user which will be used by git
# Of course you should use your name
git config --global user.name "Example Surname"
# Same for the email address
git config --global user.email "your.email@gmail.com"
# Set default so that all changes are always pushed to the repository
git config --global push.default "matching"
复制代码

 

获取Git配置信息,执行以下命令:

 

git config --list

 

3.2. 高亮显示

以下命令会为终端配置高亮

 

git config --global color.status auto
git config --global color.branch auto

 

3.3. 忽略特定的文件

可以配置Git忽略特定的文件或者是文件夹。这些配置都放在.gitignore文件中。这个文件可以存在于不同的文件夹中,可以包含不同的文件匹配模式。为了让Git忽略bin文件夹,在主目录下放置.gitignore文件,其中内容为bin。

同时Git也提供了全局的配置,core.excludesfile。

3.4. 使用.gitkeep来追踪空的文件夹

Git会忽略空的文件夹。如果你想版本控制包括空文件夹,根据惯例会在空文件夹下放置.gitkeep文件。其实对文件名没有特定的要求。一旦一个空文件夹下有文件后,这个文件夹就会在版本控制范围内。

4. 开始操作Git

后续将通过一个典型的Git工作流来学习。在这个过程中,你会创建一些文件、创建一个本地的Git仓库、提交你的文件到这个仓库中。这之后,你会克隆一个仓库、在仓库之间通过pull和push操作来交换代码的修改。注释(以#开头)解释了命令的具体含义

让我们打开命令行开始操作吧

4.1. 创建内容

下面创建一些文件,它们会被放到版本控制之中

 

复制代码
#Switch to home
cd ~/
# Create a directory
mkdir ~/repo01
# Switch into it
cd repo01
# Create a new directory
mkdir datafiles
# Create a few files
touch test01
touch test02
touch test03
touch datafiles/data.txt
# Put a little text into the first file
ls >test01
复制代码

 

4.2. 创建仓库、添加文件和提交更改

每个Git仓库都是放置在.git文件夹下.这个目录包含了仓库的所有历史记录,.git/config文件包含了仓库的本地配置。

以下将会创建一个Git仓库,添加文件倒仓库的索引中,提交更改。

 

复制代码
# Initialize the local Git repository
git init
# Add all (files and directories) to the Git repository
git add .
# Make a commit of your file to the local repository
git commit -m "Initial commit"
# Show the log file
git log
复制代码

 

4.3. diff命令与commit更改

通过git diff命令,用户可以查看更改。通过改变一个文件的内容,看看git diff命令输出什么,然后提交这个更改到仓库中

 

复制代码
# Make some changes to the file
echo "This is a change" > test01
echo "and this is another change" > test02

# Check the changes via the diff command 
git diff

# Commit the changes, -a will commit changes for modified files
# but will not add automatically new files
git commit -a -m "These are new changes"
复制代码

 

4.4. Status, Diff 和 Commit Log

下面会向你展示仓库现有的状态以及过往的提交历史

复制代码
# Make some changes in the file
echo "This is a new change" > test01
echo "and this is another new change" > test02


# See the current status of your repository 
# (which files are changed / new / deleted)
git status
# Show the differences between the uncommitted files 
# and the last commit in the current branch
git diff

# Add the changes to the index and commit
git add . && git commit -m "More chaanges - typo in the commit message"

# Show the history of commits in the current branch
git log
# This starts a nice graphical view of the changes
gitk --all
复制代码

 

4.5. 更正提交的信息 – git amend

通过git amend命令,我们可以修改*后提交的的信息

上述的提交信息中存在错误,下面会修改这个错误

git commit --amend -m "More changes - now correct"

 

4.6. 删除文件

如果你删除了一个在版本控制之下的文件,那么使用git add .不会在索引中删除这个文件。需要通过带-a选项的git commit命令和-A选项的git add命令来完成

复制代码
# Create a file and put it under version control
touch nonsense.txt
git add . && git commit -m "a new file has been created"
# Remove the file
rm nonsense.txt
# Try standard way of committing -> will not work 
git add . && git commit -m "a new file has been created"
# Now commit with the -a flag
git commit -a -m "File nonsense.txt is now removed"
# Alternatively you could add deleted files to the staging index via
git add -A . 
git commit -m "File nonsense.txt is now removed"
复制代码

 

5. 远端仓库(remote repositories)

5.1. 设置一个远端的Git仓库

我们将创建一个远端的Git仓库。这个仓库可以存储在本地或者是网络上。

远端Git仓库和标准的Git仓库有如下差别:一个标准的Git仓库包括了源代码和历史信息记录。我们可以直接在这个基础上修改代码,因为它已经包含了一个工作副本。但是远端仓库没有包括工作副本,只包括了历史信息。可以使用–bare选项来创建一个这样的仓库。

为了方便起见,示例中的仓库创建在本地文件系统上

复制代码
# Switch to the first repository
cd ~/repo01
# 
git clone --bare . ../remote-repository.git

# Check the content, it is identical to the .git directory in repo01
ls ~/remote-repository.git
复制代码

 

5.2. 推送更改到其他的仓库

做一些更改,然后将这些更改从你的*个仓库推送到一个远端仓库

复制代码
# Make some changes in the first repository
cd ~/repo01

# Make some changes in the file
echo "Hello, hello. Turn your radio on" > test01
echo "Bye, bye. Turn your radio off" > test02

# Commit the changes, -a will commit changes for modified files
# but will not add automatically new files
git commit -a -m "Some changes"

# Push the changes
git push ../remote-repository.git
复制代码

 

5.3. 添加远端仓库

除了通过完整的URL来访问Git仓库外,还可以通过git remote add命令为仓库添加一个短名称。当你克隆了一个仓库以后,origin表示所克隆的原始仓库。即使我们从零开始,这个名称也存在。

复制代码
# Add ../remote-repository.git with the name origin
git remote add origin ../remote-repository.git 

# Again some changes
echo "I added a remote repo" > test02
# Commit
git commit -a -m "This is a test for the new remote origin"
# If you do not label a repository it will push to origin
git push origin
复制代码

 

5.4. 显示已有的远端仓库

通过以下命令查看已经存在的远端仓库

# Show the existing defined remote repositories
git remote

 

5.5. 克隆仓库

通过以下命令在新的目录下创建一个新的仓库

复制代码
# Switch to home
cd ~
# Make new directory
mkdir repo02

# Switch to new directory

cd ~/repo02
# Clone
git clone ../remote-repository.git .
复制代码

 

5.6. 拉取(Pull)更改

通过拉取,可以从其他的仓库中获取*新的更改。在第二个仓库中,做一些更改,然后将更改推送到远端的仓库中。然后*个仓库拉取这些更改

复制代码
# Switch to home
cd ~

# Switch to second directory
cd ~/repo02
# Make changes
echo "A change" > test01
# Commit
git commit -a -m "A change"
# Push changes to remote repository
# Origin is automatically maintained as we cloned from this repository
git push origin
# Switch to the first repository and pull in the changes
cd ~/repo01
git pull ../remote-repository.git/
# Check the changes
less test01
复制代码

 

 

6. 还原更改

如果在你的工作副本中,你创建了不想被提交的文件,你可以丢弃它。

复制代码
# Create a new file with content
touch test04
echo "this is trash" > test04

# Make a dry-run to see what would happen
# -n is the same as --dry-run 
git clean -n

# Now delete
git clean -f
复制代码

 

你可以提取老版本的代码,通过提交的ID。git log命令可以查看提交ID

复制代码
# Switch to home
cd ~/repo01
# Get the log
git log

# Copy one of the older commits and checkout the older revision via  译者注:checkout 后加commit id就是把commit的内容复制到index和工作副本中 
git checkout commit_name
复制代码

 

如果你还未把更改加入到索引中,你也可以直接还原所有的更改

复制代码
#Some nonsense change
echo "nonsense change" > test01
# Not added to the staging index. Therefore we can 
# just checkout the old version
#译者注:checkout后如果没有commit id号,就是从index中拷贝数据到工作副本,不涉及commit部分的改变
git checkout test01
# Check the result
cat test01
# Another nonsense change
echo "another nonsense change" > test01
# We add the file to the staging index
git add test01
# Restore the file in the staging index
#译者注:复制HEAD所指commit的test01文件到index中
git reset HEAD test01
# Get the old version from the staging index
#译者注:复制index中test01到工作副本中
git checkout test01
#译者注,以上两条命令可以合并为git checkout HEAD test01
复制代码

 

也可以通过revert命令进行还原操作

# Revert a commit
git revert commit_name

 

即使你删除了一个未添加到索引和提交的文件,你也可以还原出这个文件

# Delete a file
rm test01
# Revert the deletion
git checkout test01

 

如果你已经添加一个文件到索引中,但是未提交。可以通过git reset file 命令将这个文件从索引中删除

复制代码
// Create a file
touch incorrect.txt
// Accidently add it to the index
git add .
// Remove it from the index
git reset incorrect.txt
// Delete the file
rm incorrect.txt
复制代码

 

如果你删除了文件夹且尚未提交,可以通过以下命令来恢复这个文件夹 。译者注:即使已经提交,也可以还原

git checkout HEAD -- your_dir_to_restore

译者注:checkout和reset这两个命令的含义是不同的,可以参阅这篇文章http://marklodato.github.com/visual-git-guide/index-en.html

 

7. 标记

Git可以使用对历史记录中的任一版本进行标记。这样在后续的版本中就能轻松的找到。一般来说,被用来标记某个发行的版本

可以通过git tag命令列出所有的标记,通过如下命令来创建一个标记和恢复到一个标记

 

git tag version1.6 -m 'version 1.6'      
git checkout <tag_name>

 

8. 分支、合并

8.1. 分支

通过分支,可以创造独立的代码副本。默认的分支叫master。Git消耗很少的资源就能创建分支。Git鼓励开发人员多使用分支

下面的命令列出了所有的本地分支,当前所在的分支前带有*号

git branch

 

如果你还想看到远端仓库的分支,可以使用下面的命令

git branch -a

 

可以通过下面的命令来创建一个新的分支

复制代码
# Syntax: git branch <name> <hash>
# <hash> in the above is optional 
# if not specified the last commit will be used
# If specified the corresponding commit will be used
git branch testing
# Switch to your new branch
git checkout testing
# Some changes
echo "Cool new feature in this branch" > test01
git commit -a -m "new feature"
# Switch to the master branch
git checkout master
# Check that the content of test01 is the old one
cat test01
复制代码

 

8.2. 合并

通过Merge我们可以合并两个不同分支的结果。Merge通过所谓的三路合并来完成。分别来自两个分支的*新commit和两个分支的*新公共commit

可以通过如下的命令进行合并

# Syntax: git merge <branch-name>
git merge testing

一旦合并发生了冲突,Git会标志出来,开发人员需要手工的去解决这些冲突。解决冲突以后,就可以将文件添加到索引中,然后提交更改

8.3. 删除分支

删除分支的命令如下:

#Delete branch testing
git branch -d testing
# Check if branch has been deleted
git branch

 

8.4. 推送(push)一个分支到远端仓库

默认的,Git只会推送匹配的分支的远端仓库。这意味在使用git push命令默认推送你的分支之前,需要手工的推送一次这个分支。

复制代码
# Push testing branch to remote repository
git push origin testing

# Switch to the testing branch
git checkout testing

# Some changes
echo "News for you" > test01
git commit -a -m "new feature in branch"

# Push all including branch
git push
复制代码

通过这种方式,你可以确定哪些分支对于其他仓库是可见的,而哪些只是本地的分支

9. 解决合并冲突

如果两个不同的开发人员对同一个文件进行了修改,那么合并冲突就会发生。而Git没有智能到自动解决合并两个修改

在这一节中,我们会首先制造一个合并冲突,然后解决它,并应用到Git仓库中

下面会产生一个合并冲突

复制代码
# Switch to the first directory
cd ~/repo01
# Make changes
touch mergeconflict.txt
echo "Change in the first repository" > mergeconflict.txt
# Stage and commit
git add . && git commit -a -m "Will create merge conflict 1"

# Switch to the second directory
cd ~/repo02
# Make changes
touch mergeconflict.txt
echo "Change in the second repository" > mergeconflict.txt
# Stage and commit
git add . && git commit -a -m "Will create merge conflict 2"
# Push to the master repository
git push

# Now try to push from the first directory
# Switch to the first directory
cd ~/repo01
# Try to push --> you will get an error message
git push
# Get the changes
git pull origin master
复制代码

 

Git将冲突放在收到影响的文件中,文件内容如下:

<<<<<<< HEAD
Change in the first repository
=======
Change in the second repository
>>>>>>> b29196692f5ebfd10d8a9ca1911c8b08127c85f8

上面部分是你的本地仓库,下面部分是远端仓库。现在编辑这个文件,然后commit更改。另外的,你可以使用git mergetool命令

 

# Either edit the file manually or use 
git mergetool
# You will be prompted to select which merge tool you want to use
# For example on Ubuntu you can use the tool "meld"
# After  merging the changes manually, commit them
git commit -m "merged changes"

 

 

10. 变基(Rebase)

10.1. 在同一分支中应用Rebase Commit

通过rebase命令可以合并多个commit为一个。这样用户push更改到远端仓库的时候就可以先修改commit历史

接下来我们将创建多个commit,然后再将它们rebase成一个commit

复制代码
# Create a new file
touch rebase.txt

# Add it to git
git add . && git commit -m "rebase.txt added to index"

# Do some silly changes and commit
echo "content" >> rebase.txt
git add . && git commit -m "added content"
echo " more content" >> rebase.txt
git add . && git commit -m "added more content"
echo " more content" >> rebase.txt
git add . && git commit -m "added more content"
echo " more content" >> rebase.txt
git add . && git commit -m "added more content"
echo " more content" >> rebase.txt
git add . && git commit -m "added more content"
echo " more content" >> rebase.txt
git add . && git commit -m "added more content"

# Check the git log message
git log
复制代码

 

我们合并*后的七个commit。你可以通过如下的命令交互的完成

git rebase -i HEAD~7

这个命令会打开编辑器让你修改commit的信息或者 squashfixup*后一个信息

Squash会合并commit信息而fixup会忽略commit信息(待理解)

10.2. Rebasing多个分支

你也可以对两个分支进行rebase操作。如下所述,merge命令合并两个分支的更改。rebase命令为一个分支的更改生成一个补丁,然后应用这个补丁到另一分支中

使用merge和rebase,*后的源代码是一样的,但是使用rebase产生的commit历史更加的少,而且历史记录看上去更加的线性

 

复制代码
# Create new branch 
git branch testing
# Checkout the branch
git checkout testing
# Make some changes
echo "This will be rebased to master" > test01
# Commit into testing branch
git commit -a -m "New feature in branch"
# Rebase the master
git rebase master
复制代码

 

10.3.Rebase*佳实践

在push更改到其他的Git仓库之前,我们需要仔细检查本地分支的commit历史

在Git中,你可以使用本地的commit。开发人员可以利用这个功能方便的回滚本地的开发历史。但是在push之前,需要观察你的本地分支历史,是否其中有些commit历史对其他用户来说是无关的

如果所有的commit历史都跟同一个功能有关,很多情况下,你需要rebase这些commit历史为一个commit历史。

交互性的rebase主要就是做重写commit历史的任务。这样做是安全的,因为commit还没有被push到其它的仓库。这意味着commit历史只有在被push之前被修改

如果你修改然后push了一个已经在目标仓库中存在的commit历史,这看起来就像是你实现了一些别人已经实现的功能

11. 创建和应用补丁

一个补丁指的是一个包含对源代码进行修改的文本文件。你可以将这个文件发送给某人,然后他就可以应用这个补丁到他的本地仓库

下面会创建一个分支,对这个分支所一些修改,然后创建一个补丁,并应用这个补丁到master分支

复制代码
# Create a new branch
git branch mybranch
# Use this new branch
git checkout mybranch
# Make some changes
touch test05
# Change some content in an existing file
echo "New content for test01" >test01
# Commit this to the branch
git add .
git commit -a -m "First commit in the branch"

# Create a patch --> git format-patch master
git format-patch origin/master
# This created patch 0001-First-commit-in-the-branch.patch

# Switch to the master
git checkout master

# Apply the patch
git apply 0001-First-commit-in-the-branch.patch
# Do your normal commit in the master 
git add .
git commit -a -m "Applied patch"

# Delete the patch 
rm 0001-First-commit-in-the-branch.patch
复制代码

 

12. 定义同名命令

Git允许你设定你自己的Git命令。你可以给你自己常用的命令起一个缩写命令,或者合并几条命令道一个命令上来。

下面的例子中,定义了git add-commit 命令,这个命令合并了git add . -A 和git commit -m 命令。定义这个命令后,就可以使用git add-commit -m "message" 了.

git config --global alias.add-commit '!git add . -A && git commit'

但是非常不幸,截止写这篇文章之前,定义同名命令在msysGit中还没有支持。同名命令不能以!开始。

13. 放弃跟踪文件

有时候,你不希望某些文件或者文件夹被包含在Git仓库中。但是如果你把它们加到.gitignore文件中以后,Git会停止跟踪这个文件。但是 它不会将这个文件从仓库中删除。这导致了文件或者文件夹的*后一个版本还是存在于仓库中。为了取消跟踪这些文件或者文件夹,你可以使用如下的命令

 

# Remove directory .metadata from git repo
git rm -r --cached .metadata
# Remove file test.txt from repo
git rm --cached test.txt

这样做不会将这些文件从commit历史中去掉。如果你想将这些文件从commit历史中去掉,可以参考git filter-branch命令

14. 其他有用的命令

下面列出了在日常工作中非常有用的Git命令

Table 2. 有用的Git命令

命令 描述
git blame filename 谁创建了或者是修改了这个文件
git checkout -b mybranch master~1 以上上个commit信息为起点,创建一条新的分支

15. 安装Git服务

如上所述,我们的操作不需要Git服务。我可以只使用文件系统或者是Git仓库的提供者,像Github或Bitbucket。但是,有时候,拥有一个自己的服务是比较方便的,在ubuntu下安装一个服务相对来说是比较容易的

确定你已经安装了ssh

apt-get install ssh

 

如果你还没有安装Git服务,安装它

sudo apt-get install git-core

 

添加一个名为git的用户

sudo adduser git

 

然后使用git用户进行登陆,创建一个空的仓库

# Login to server
# to test use localhost
ssh git@IP_ADDRESS_OF_SERVER

# Create repository
git init --bare example.git

 

 

现在你就可以向远端的仓库提交变更了

复制代码
mkdir gitexample
cd gitexample
git init
touch README
git add README
git commit -m 'first commit'
git remote add origin git@IP_ADDRESS_OF_SERVER:example.git
git push origin master
复制代码

 

16. 在线的远端仓库

16.1. 克隆远端仓库

Git支持远端的操作。Git支持多种的传输类型,Git自带的协议就叫做git。下面的的命令通过git协议从克隆一个仓库

git clone git@github.com:vogella/gitbook.git

同样的,你可以通过http协议来克隆仓库

# The following will clone via HTTP 
git clone http://vogella@github.com/vogella/gitbook.git

 

16.2. 添加远端仓库

如果你克隆了一个远端仓库,那么原先的仓库就叫做origin

你可以push修改到origin中,通过 git push origin 命令. 当然,push到一个远端的仓库需要对仓库的写权限

你可以通过git remote add name gitrepo 命令添加多个仓库。例如,你可以通过http协议再次添加之前clone过来的仓库:

// Add the https protocol 
git remote add githttp https://vogella@github.com/vogella/gitbook.git

 

 

16.3. 通过http和代理服务器进行远端操作

如果你的防火墙屏蔽了出http以外的所有协议,那么使用http协议来获取仓库是非常好的方法。.

Git同样支持通过代理服务器使用http协议。下面的Git命令会展示这一点。你可以为所有的程序设置代理服务器或者只是为Git服务提供。

下面的例子用到了环境变量

复制代码
# Linux
export http_proxy=http://proxy:8080
# On Windows
# Set http_proxy=http://proxy:8080 
git clone http://dev.eclipse.org/git/org.eclipse.jface/org.eclipse.jface.snippets.git
# Push back to the origin using http
git push origin
复制代码

 

下面的例子只是用到了Git的配置

// Set proxy for git globally
 git config --global http.proxy http://proxy:8080
// To check the proxy settings
git config --get http.proxy
// Just in case you need to you can also revoke the proxy settings
git config --global --unset http.proxy

 

 

17. Git服务提供商

除了假设自己的服务,你也可以使用Git服务提供商提供的服务。*流行的Git服务提供网站是GitHub和Bitbucket。它们都提供了有限制的免费服务

17.1. GitHub

可以通过 https://github.com/ 访问GitHub. GitHub上所有的公开仓库都是免费的。如果你想在上面使用私有的仓库,那么就需要付费给GitHub

GitHub需要你创建ssh的公钥私钥。生成一份Ubuntu的公钥私钥可以访问 ssh key creation in Ubuntu,Windows环境可以访问msysgit ssh key generation.

在GitHub上创建一个账户和一个仓库以后。你会收到如何将你的项目上传到GitHUb的指南,其中的命令大致如下:

复制代码
Global setup:
 Set up git
  git config --global user.name "Your Name"
  git config --global user.email your.email@gmail.com
      
Next steps:
  mkdir gitbook 
  cd gitbook
  git init
  touch README
  git add README
  git commit -m 'first commit'
  git remote add origin git@github.com:vogella/gitbook.git
  git push -u origin master
      
Existing Git Repo?
  cd existing_git_repo
  git remote add origin git@github.com:vogella/gitbook.git
  git push -u origin master
复制代码

 

 

17.2. Bitbucket

可以通过 https://bitbucket.org/ 访问Bitbucket. Bitbucket 提供了无限制了公共仓库和只能有五个人访问的私有仓库。如果你需要超过五个人访问私有仓库,就需要付费给Bitbucket

18. Git的图形接口

这个教程主要说明Git命令行的使用。完成了这个教程以后,你可能想要找到一个Git的图形工具

Git提供了两个图形工具。 gitk能够展示仓库的历史信息、git gui 让你可以通过编辑器来完成Git操作

Eclipse EGit 项目提供了Git与Eclipse的集成,在*新的Eclipse版本中可以找到

iOS super关键字帮我们做了什么?

本篇文章讲的是super的实际运作原理,如有同学对super与self的区分还有疑惑的,请参考ChenYilong大神的《招聘一个靠谱的iOS》面试题参考答案(上)

super究竟在干什么?

官方提到的super关键字?

打开苹果API文档,搜索objc_msgSendSuper(对该函数陌生的先去补补rumtime)。

%title插图%num

super官方解释

里面明确提到了使用super关键字发送消息会被编译器转化为调用objc_msgSendSuper以及相关函数(由返回值决定)。

再让我们看看该函数的定义(这是文档中的定义):

id objc_msgSendSuper(struct objc_super *super, SEL op, ...);

这里的super已经不再是我们调用时写的[super init]super了,这里指代的是struct objc_super结构体指针。文档中明确指出,该结构体需要包含接收消息的实例以及一开始寻找方法实现的父类

  1. struct objc_super {
  2. /// Specifies an instance of a class.
  3. __unsafe_unretained id receiver;
  4. /// Specifies the particular superclass of the instance to message.
  5. __unsafe_unretained Class super_class;
  6. /* super_class is the first class to search */
  7. };

%title插图%num

objc_super结构体

既然知道了super是如何调用的,那么我们来尝试自己实现一个super

手动实现super关键字

让我们先定义两个类:

这是父类:Father类

  1. // Father.h
  2. @interface Father : NSObject
  3. – (void)eat;
  4. @end
  5. // Father.m
  6. @implementation Father
  7. – (void)eat {
  8. NSLog(@“Father eat”);
  9. }
  10. @end

这是子类:Son类

  1. // Son.h
  2. @interface Son : Father
  3. – (void)eat;
  4. @end
  5. // Son.m
  6. @implementation Son
  7. – (void)eat {
  8. [super eat];
  9. }
  10. @end

在这里,我们的Son类重写了父类的eat方法,里面只做一件事,就是调用父类的eat方法。

让我们在main中开始进行测试:

  1. int main(int argc, char * argv[]) {
  2. Son *son = [Son new];
  3. [son eat];
  4. }
  5. // 输出:
  6. 2017-05-14 22:44:00.208931+0800 TestSuper[7407:3788932] Father eat

到这里没毛病,一个Son对象调用了eat方法(内部调用父类的eat),输出了结果。

1. 下面,我们来自己实现super的效果:

改写Son.m:

  1. // Son.m
  2. – (void)eat {
  3. // [super eat];
  4. struct objc_super superReceiver = {
  5. self,
  6. [self superclass]
  7. };
  8. objc_msgSendSuper(&superReceiver, _cmd);
  9. }

运行我们的main函数:

  1. //输出
  2. 2017-05-14 22:47:00.109379+0800 TestSuper[7417:3790621] Father eat

没毛病,我们可是根据官方文档来实现super的效果。

难道super真的就是如此?

让我们持怀疑的态度看看下面这个例子:

在这里,我们又有个Son的子类出现了:Grandson类

  1. // Grandson.h
  2. @interface Grandson : Son
  3. @end
  4. // Grandson.m
  5. @implementation Grandson
  6. @end

该类啥什么都没实现,纯粹继承自Son。

然后让我们改写main函数:

  1. int main(int argc, char * argv[]) {
  2. Grandson *grandson = [Grandson new];
  3. [grandson eat];
  4. }

运行起来,过一会就crash了,如图:

%title插图%num

崩溃提示

再看看相关线程中的方法调用:

%title插图%num

crash方法调用

这是一个死循环,所以系统让该段代码强制停止了。可为什么这里会构成死循环呢?让我们好好分析分析:

  1. Grandson中没有实现eat方法,所以main函数中Grandson的实例执行eat方法是这样的:根据类继承关系自下而上寻找,在Grandson的父类Son类中找到了eat方法,进行调用。
  2. 在Son的eat方法的实现中,我们构建了一个superReceiver结构体,内部包含了self以及[self superclass]。在调用过程中,self指代的应是Grandson实例,也就是grandson这个变量,那么[self superclass]方法返回值也就是Son这个类。
  3. 根据第2点的分析,以及我们在文章开头的文档中,苹果指出superReceiver中的父类就是开始寻找方法实现的那个父类,我们可以得出,此时的objc_msgSendSuper(&superReceiver, _cmd)函数调用的方法实现即是Son类中的eat方法的实现。即,构成了递归。

既然这里不能使用superclass方法,那么我们要如何自己实现super的作用呢?

我们是这段代码的作者,所以,我们可以这样:

  1. // 我们修改了Son.m
  2. – (void)eat {
  3. // [super eat];
  4. struct objc_super superReceiver = {
  5. self,
  6. objc_getClass(“Father”)
  7. };
  8. objc_msgSendSuper(&superReceiver, _cmd);
  9. }
  10. // 输出
  11. 2017-05-14 23:16:49.232375+0800 TestSuper[7440:3798009] Father eat

我们直接指明superReceiver中要寻找方法实现的父类:Father。这里必定有人会问:这样子岂不是每个调用[super xxxx]的地方都需要直接指明父类

“直接指明”的意思是,代码中直接写出这个类,比如直接写:[Father class]或者objc_getClass("Father"),这里面的Father与”Father”就是我们在代码里写死的。

先不谈这个疑问,我们来分析这段代码:

  1. Grandson中没有实现eat方法,所以main函数中Grandson的实例执行eat方法是这样的:根据类继承关系自下而上寻找,在Grandson的父类Son类中找到了eat方法,进行调用。
  2. 在Son的eat方法的实现中,我们构建了一个superReceiver结构体,内部包含了self以及Father这个类。
  3. objc_msgSendSuper函数直接去Father类中寻找eat方法的实现,并执行(输出)。

现在这段代码是以正常逻辑执行的。

2. [super xxxx]真的要直接指明父类?

我们使用clang的rewrite指令重写Son.m:

clang -rewrite-objc Son.m 

生成的Son.cpp文件:

  1. static void _I_Son_eat(Son * self, SEL _cmd) {
  2. ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass(“Son”))}, sel_registerName(“eat”));
  3. }

这一行到底的代码可读性太差,让我们稍稍分解下(由于语法问题我们作了少量语法修改以通过编译,实际作用与原cpp中一致):

  1. static void _I_Son_eat(Son * self, SEL _cmd) {
  2. __rw_objc_super superReceiver = (__rw_objc_super){
  3. (__bridge struct objc_object *)(id)self,
  4. (__bridge struct objc_object *)(id)class_getSuperclass(objc_getClass(“Son”))};
  5. typedef void *Func(__rw_objc_super *, SEL);
  6. Func *func = (void *)objc_msgSendSuper;
  7. func(&superReceiver, sel_registerName(“eat”));
  8. }

先修改Son.m运行起来:

  1. // Son.m
  2. (void)eat {
  3. // [super eat];
  4. //_I_Son_eat即为重写的函数
  5. _I_Son_eat(self, _cmd);
  6. }
  7. // 输出
  8. 2017-05-15 00:08:37.782519+0800 TestSuper[7460:3810248] Father eat

没有毛病。

重写的代码里构建了一个__rw_objc_super的结构体,定义如下:

  1. struct __rw_objc_super {
  2. struct objc_object *object;
  3. struct objc_object *superClass;
  4. // cpp里的语法,忽略即可
  5. __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
  6. };

该结构体与struct objc_super一致。之后我们将objc_msgSendSuper函数转换为指定参数的函数func进行调用。这里请注意__rw_objc_super superReceiver中的第二个值class_getSuperclass(objc_getClass("Son"))

该代码直接指明的类是本类:Son类。但是__rw_objc_super结构体中的superClass并不是本类,而是通过runtime查找出的父类。这与我们自己实现的 “直接指明Father为objc_super结构体的super_class值” *后达到的效果是一样的。

所以,[super xxxx]肯定要通过指明一个类,可以是父类,也可以是本类,来达到正确调用父类方法的目的!只不过“直接指明”这件事,编译器会帮我们搞定,我们只管写super即可。

clang rewrite不可靠

为何clang不可靠

clang的rewrite功能所提供的重写后的代码并非编译器(LLVM)转换后的代码,如今的编译器在Xcode开启bitcode功能后会生成一种中间代码:LLVM Intermediate Representation(LLVM IR)。该代码向上可统一大部分高级语言,向下可支持多种不同架构的CPU,具体可查看LLVM文档。所以我们的目标是从IR代码求证super究竟在做什么事!

查看IR代码

终端里cd到Son.m文件所在目录,执行:

clang -emit-llvm Son.m -S -o son.ll

生成的IR代码比较多,我们挑重点进行查看:

  1. %0 = type opaque
  2. // Son的eat方法
  3. define internal void @”\01-[Son eat]“(%0*, i8*) #0 {
  4. %3 = alloca %0*, align 8 // 分配一个指针的内存,8字节对齐(声明一个指针变量)
  5. %4 = alloca i8*, align 8 // 分配一个char *的内存(声明一个char *指针变量)
  6. %5 = alloca %struct._objc_super, align 8 // 给_objc_super分配内存(声明一个struct._objc_super变量)
  7. store %0* %0, %0** %3, align 8 // 将*个参数,id self 写入%3分配的内存中去
  8. store i8* %1, i8** %4, align 8 // 将_cmd写入%4分配的内存中区
  9. %6 = load %0*, %0** %3, align 8 // 读出%3内存中的数据到%6这个临时变量(%3中存的是self)
  10. %7 = bitcast %0* %6 to i8* // 将%6变量的类型转换为char *指针类型,指向的还是self
  11. %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0 // 取struct._objc_super变量(%5)中的第0个元素,声明为%8
  12. store i8* %7, i8** %8, align 8 // 将%7存入%8这个变量中,即把i8* 类型的 self存入了结构体第0个元素中
  13. %9 = load %struct._class_t*, %struct._class_t** @”OBJC_CLASSLIST_SUP_REFS_$_”, align 8 // 声明%9临时变量为struct._class_t*类型,内容为@”OBJC_CLASSLIST_SUP_REFS_$_
  14. %10 = bitcast %struct._class_t* %9 to i8* // 将%9的变量强转为char *类型
  15. %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1 // 取struct._objc_super变量(%5)中的第1个元素,声明为%11
  16. store i8* %10, i8** %11, align 8 // 将%9的变量,即@”OBJC_CLASSLIST_SUP_REFS_$_”存入结构体第1个元素中
  17. %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !7 // 将@selector(eat)的引用放入char *类型的%12变量中
  18. // 函数调用,传入参数为上述生成的struct._objc_super结构体和 @selector(eat),调用函数objc_msgSendSuper2
  19. call void bitcast (i8* (%struct._objc_super*, i8*, …)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12)
  20. ret void
  21. }
  22. @”OBJC_CLASS_$_Son” = global %struct._class_t {
  23. %struct._class_t* @”OBJC_METACLASS_$_Son”,
  24. %struct._class_t* @”OBJC_CLASS_$_Father“,
  25. %struct._objc_cache* @_objc_empty_cache,
  26. i8* (i8*, i8*)** null,
  27. %struct._class_ro_t* @”\01l_OBJC_CLASS_RO_$_Son”
  28. }, section “__DATA, __objc_data”, align 8
  29. // 直接存放进入struct._objc_super的变量, 内容为@”OBJC_CLASS_$_Son
  30. @”OBJC_CLASSLIST_SUP_REFS_$_” = private global %struct._class_t* @”OBJC_CLASS_$_Son“, section “__DATA, __objc_superrefs, regular, no_dead_strip“, align 8

IR的语法其实不难记,还是比较好懂的。这里我们只要对照着看即可:

  • %1,%2,@xxx之类的都是指代变量,理解为变量名就可以了
  • i8指8位的int类型,即1个字节的char类型。i8*就是指char *指针
  • alloca指分配内存,理解为声明一个变量即可,如alloca i8*即为一个char *的变量
  • %0在开头的代码里说明了是一个不透明的类型,所以%0*就指代一个万能指针,理解为id即可
  • store为写入内存
  • load为从内存中读取出来
  • bitcast为类型转换
  • getelementptr inbounds取指定内存偏移

代码中既有汇编的赶脚,又有高级语言的味道。基本上注释都补全了,代码中的逻辑和上文中我们自己实现的/clang重写的代码基本相似。但是这里注意@"OBJC_CLASSLIST_SUP_REFS_$_"这个变量。

@"OBJC_CLASSLIST_SUP_REFS_$_"其实就是对应到struct objc_super结构中的第二个元素:super_class。在IR代码的%11以及后面那一行就是体现。

@"OBJC_CLASSLIST_SUP_REFS_$_"的定义就是@"OBJC_CLASS_$_Son"这个全局变量。@"OBJC_CLASS_$_Son"全局变量就是Son这个类对象,里面包含了元类:@"OBJC_METACLASS_$_Son",以及父类:@"OBJC_CLASS_$_Father",以及其他的一些数据。然而,看到这里,我们发现这和我们自己实现的super,以及clang重写的super都不一样:这里是直接将[Son class]作为struct objc_supersuper_class,但是并没有任何调用class_getSuperclass的地方…

查看汇编源码

但是,这里唯一的一个函数@objc_msgSendSuper2貌似与众不同,与我们之前看到的objc_msgSendSuper相比多了个2,难道是这个函数在作鬼?那就让我们到官方的objc4-709源码里查询下这个函数(位于objc-msg-arm64.s文件中):

  1. ENTRY _objc_msgSendSuper2
  2. UNWIND _objc_msgSendSuper2, NoFrame
  3. MESSENGER_START
  4. ldp x0, x16, [x0] // x0 = real receiver, x16 = class
  5. ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
  6. CacheLookup NORMAL
  7. END_ENTRY _objc_msgSendSuper2

这是一段汇编代码,没错,苹果为了提高运行效率,发送消息相关的函数是直接用汇编实现的。

这里我们来简单分析下这个函数:

  1. ldp x0, x16, [x0]:从x0出读取两个字数据到x0与x16中,根据注释,读取的数据应该是对应的self[Son class]
  2. ldr x16, [x16, #SUPERCLASS]:将x16的数值+SUPERCLASS值的偏移作为地址,取出该地址的数值保存在x16中。这里的SUPERCLASS定义是#define SUPERCLASS 8,也就是偏移8位,那么取到的应该就是@"OBJC_CLASS_$_Father"这个父类[Father class]到x16中。
  3. 执行CacheLookup函数,参数为NORMAL。

让我们看看CacheLookup的定义:

  1. /********************************************************************
  2. *
  3. * CacheLookup NORMAL|GETIMP|LOOKUP
  4. *
  5. * Locate the implementation for a selector in a class method cache.
  6. *
  7. * Takes:
  8. * x1 = selector
  9. * x16 = class to be searched
  10. *
  11. * Kills:
  12. * x9,x10,x11,x12, x17
  13. *
  14. * On exit: (found) calls or returns IMP
  15. * with x16 = class, x17 = IMP
  16. * (not found) jumps to LCacheMiss
  17. *
  18. ********************************************************************/
  19. #define NORMAL 0
  20. #define GETIMP 1
  21. #define LOOKUP 2
  22. .macro CacheLookup
  23. // x1 = SEL, x16 = isa
  24. ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
  25. and w12, w1, w11 // x12 = _cmd & mask
  26. add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
  27. ldp x9, x17, [x12] // {x9, x17} = *bucket
  28. 1: cmp x9, x1 // if (bucket->sel != _cmd)
  29. b.ne 2f // scan more
  30. CacheHit $0 // call or return imp
  31. 2: // not hit: x12 = not-hit bucket
  32. CheckMiss $0 // miss if bucket->sel == 0
  33. cmp x12, x10 // wrap if bucket == buckets
  34. b.eq 3f
  35. ldp x9, x17, [x12, #-16]! // {x9, x17} = *–bucket
  36. b 1b // loop
  37. 3: // wrap: x12 = first bucket, w11 = mask
  38. add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
  39. // Clone scanning loop to miss instead of hang when cache is corrupt.
  40. // The slow path may detect any corruption and halt later.
  41. ldp x9, x17, [x12] // {x9, x17} = *bucket
  42. 1: cmp x9, x1 // if (bucket->sel != _cmd)
  43. b.ne 2f // scan more
  44. CacheHit $0 // call or return imp
  45. 2: // not hit: x12 = not-hit bucket
  46. CheckMiss $0 // miss if bucket->sel == 0
  47. cmp x12, x10 // wrap if bucket == buckets
  48. b.eq 3f
  49. ldp x9, x17, [x12, #-16]! // {x9, x17} = *–bucket
  50. b 1b // loop
  51. 3: // double wrap
  52. JumpMiss $0
  53. .endmacro

具体的CacheLookup我们这里就不再展开了,我们只关心这里是从哪里查找方法的。在注释中,明确说到这是一个“去类的方法缓存中寻找方法实现”的函数,参入的参数是x1中的selector,x16中的class(class to be searched 就是说从这个类中开始查找),而这时候的x16,恰恰是我们刚才在_objc_msgSendSuper2存入的父类[Father class],因此,方法会从这个类中开始查找

整体调用流程

从手动实现->查看clang重写->查看IR码->查看汇编源码这几个过程分析下来,我们总算是把这条真实的super调用链路搞搞清楚了:

  1. 编译器指定一个struct._objc_super结构体, 结构体中self为接收对象,直接指明自身的类为结构体第二个class类型的值。
  2. 调用_objc_msgSendSuper2函数,传入上述struct._objc_super结构体。
  3. _objc_msgSendSuper2函数中直接通过偏移量直接查找父类。
  4. 调用CacheLookup函数去父类中查找指定方法。

结论

所以,从真实的IR代码中,super关键字其实是直接指明本类Son,再结合_objc_msgSendSuper2函数直接获取父类去查找方法的,而并非像clang重写的那样,指明本类,再通过runtime查找父类。

其实先指明本类,再通过runtime查找父类,也是没有问题的,这还可以避免一些运行时“更改父类”的情况。但是LLVM的做法应该是有他的道理的,可能是出于性能考虑?

30分钟搞定iOS自定义相机

30分钟搞定iOS自定义相机

字数1490  阅读1126  评论11  

  *近公司的项目中用到了相机,由于不用系统的相机,UI给的相机切图,必须自定义才可以。就花时间简单研究了一下相机的自定义。
相机属于系统硬件,这就需要我们来手动调用iPhone的相机硬件,分为以下步骤:

%title插图%num

2016-03-30 上午9.51.09.png

1、首先声明以下对象

  1. #import <AVFoundation/AVFoundation.h>
  2. //捕获设备,通常是前置摄像头,后置摄像头,麦克风(音频输入)
  3. @property (nonatomic, strong) AVCaptureDevice *device;
  4. //AVCaptureDeviceInput 代表输入设备,他使用AVCaptureDevice 来初始化
  5. @property (nonatomic, strong) AVCaptureDeviceInput *input;
  6. //输出图片
  7. @property (nonatomic ,strong) AVCaptureStillImageOutput *imageOutput;
  8. //session:由他把输入输出结合在一起,并开始启动捕获设备(摄像头)
  9. @property (nonatomic, strong) AVCaptureSession *session;
  10. //图像预览层,实时显示捕获的图像
  11. @property (nonatomic ,strong) AVCaptureVideoPreviewLayer *previewLayer;

2、初始化各个对象

  1. – (void)cameraDistrict
  2. {
  3. // AVCaptureDevicePositionBack 后置摄像头
  4. // AVCaptureDevicePositionFront 前置摄像头
  5. self.device = [self cameraWithPosition:AVCaptureDevicePositionFront];
  6. self.input = [[AVCaptureDeviceInput alloc] initWithDevice:self.device error:nil];
  7. self.imageOutput = [[AVCaptureStillImageOutput alloc] init];
  8. self.session = [[AVCaptureSession alloc] init];
  9. // 拿到的图像的大小可以自行设定
  10. // AVCaptureSessionPreset320x240
  11. // AVCaptureSessionPreset352x288
  12. // AVCaptureSessionPreset640x480
  13. // AVCaptureSessionPreset960x540
  14. // AVCaptureSessionPreset1280x720
  15. // AVCaptureSessionPreset1920x1080
  16. // AVCaptureSessionPreset3840x2160
  17. self.session.sessionPreset = AVCaptureSessionPreset640x480;
  18. //输入输出设备结合
  19. if ([self.session canAddInput:self.input]) {
  20. [self.session addInput:self.input];
  21. }
  22. if ([self.session canAddOutput:self.imageOutput]) {
  23. [self.session addOutput:self.imageOutput];
  24. }
  25. //预览层的生成
  26. self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
  27. self.previewLayer.frame = CGRectMake(0, 64, SCREEN_WIDTH, SCREEN_HEIGHT64);
  28. self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
  29. [self.view.layer addSublayer:self.previewLayer];
  30. //设备取景开始
  31. [self.session startRunning];
  32. if ([_device lockForConfiguration:nil]) {
  33. //自动闪光灯,
  34. if ([_device isFlashModeSupported:AVCaptureFlashModeAuto]) {
  35. [_device setFlashMode:AVCaptureFlashModeAuto];
  36. }
  37. //自动白平衡,但是好像一直都进不去
  38. if ([_device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeAutoWhiteBalance]) {
  39. [_device setWhiteBalanceMode:AVCaptureWhiteBalanceModeAutoWhiteBalance];
  40. }
  41. [_device unlockForConfiguration];
  42. }
  43. }

根据前后置位置拿到相应的摄像头:

  1. – (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position{
  2. NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
  3. for ( AVCaptureDevice *device in devices )
  4. if ( device.position == position ){
  5. return device;
  6. }
  7. return nil;
  8. }

3、拍照拿到相应图片:

  1. – (void)photoBtnDidClick
  2. {
  3. AVCaptureConnection *conntion = [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];
  4. if (!conntion) {
  5. NSLog(@”拍照失败!”);
  6. return;
  7. }
  8. [self.imageOutput captureStillImageAsynchronouslyFromConnection:conntion completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
  9. if (imageDataSampleBuffer == nil) {
  10. return ;
  11. }
  12. NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
  13. self.image = [UIImage imageWithData:imageData];
  14. [self.session stopRunning];
  15. [self.view addSubview:self.cameraImageView];
  16. }

4、保存照片到相册:

  1. #pragma – 保存至相册
  2. – (void)saveImageToPhotoAlbum:(UIImage*)savedImage
  3. {
  4. UIImageWriteToSavedPhotosAlbum(savedImage, self, @selector(image:didFinishSavingWithError:contextInfo:), NULL);
  5. }
  6. // 指定回调方法
  7. – (void)image: (UIImage *) image didFinishSavingWithError: (NSError *) error contextInfo: (void *) contextInfo
  8. {
  9. NSString *msg = nil ;
  10. if(error != NULL){
  11. msg = @”保存图片失败” ;
  12. }else{
  13. msg = @”保存图片成功” ;
  14. }
  15. UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@”保存图片结果提示”
  16. message:msg
  17. delegate:self
  18. cancelButtonTitle:@”确定”
  19. otherButtonTitles:nil];
  20. [alert show];
  21. }

5、前后置摄像头的切换

  1. – (void)changeCamera{
  2. NSUInteger cameraCount = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
  3. if (cameraCount > 1) {
  4. NSError *error;
  5. //给摄像头的切换添加翻转动画
  6. CATransition *animation = [CATransition animation];
  7. animation.duration = .5f;
  8. animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
  9. animation.type = @”oglFlip”;
  10. AVCaptureDevice *newCamera = nil;
  11. AVCaptureDeviceInput *newInput = nil;
  12. //拿到另外一个摄像头位置
  13. AVCaptureDevicePosition position = [[_input device] position];
  14. if (position == AVCaptureDevicePositionFront){
  15. newCamera = [self cameraWithPosition:AVCaptureDevicePositionBack];
  16. animation.subtype = kCATransitionFromLeft;//动画翻转方向
  17. }
  18. else {
  19. newCamera = [self cameraWithPosition:AVCaptureDevicePositionFront];
  20. animation.subtype = kCATransitionFromRight;//动画翻转方向
  21. }
  22. //生成新的输入
  23. newInput = [AVCaptureDeviceInput deviceInputWithDevice:newCamera error:nil];
  24. [self.previewLayer addAnimation:animation forKey:nil];
  25. if (newInput != nil) {
  26. [self.session beginConfiguration];
  27. [self.session removeInput:self.input];
  28. if ([self.session canAddInput:newInput]) {
  29. [self.session addInput:newInput];
  30. self.input = newInput;
  31. } else {
  32. [self.session addInput:self.input];
  33. }
  34. [self.session commitConfiguration];
  35. } else if (error) {
  36. NSLog(@”toggle carema failed, error = %@”, error);
  37. }
  38. }
  39. }

6、相机的其它参数设置

  1. //AVCaptureFlashMode 闪光灯
  2. //AVCaptureFocusMode 对焦
  3. //AVCaptureExposureMode 曝光
  4. //AVCaptureWhiteBalanceMode 白平衡
  5. //闪光灯和白平衡可以在生成相机时候设置
  6. //曝光要根据对焦点的光线状况而决定,所以和对焦一块写
  7. //point为点击的位置
  8. – (void)focusAtPoint:(CGPoint)point{
  9. CGSize size = self.view.bounds.size;
  10. CGPoint focusPoint = CGPointMake( point.y /size.height ,1-point.x/size.width );
  11. NSError *error;
  12. if ([self.device lockForConfiguration:&error]) {
  13. //对焦模式和对焦点
  14. if ([self.device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
  15. [self.device setFocusPointOfInterest:focusPoint];
  16. [self.device setFocusMode:AVCaptureFocusModeAutoFocus];
  17. }
  18. //曝光模式和曝光点
  19. if ([self.device isExposureModeSupported:AVCaptureExposureModeAutoExpose ]) {
  20. [self.device setExposurePointOfInterest:focusPoint];
  21. [self.device setExposureMode:AVCaptureExposureModeAutoExpose];
  22. }
  23. [self.device unlockForConfiguration];
  24. //设置对焦动画
  25. _focusView.center = point;
  26. _focusView.hidden = NO;
  27. [UIView animateWithDuration:0.3 animations:^{
  28. _focusView.transform = CGAffineTransformMakeScale(1.25, 1.25);
  29. }completion:^(BOOL finished) {
  30. [UIView animateWithDuration:0.5 animations:^{
  31. _focusView.transform = CGAffineTransformIdentity;
  32. } completion:^(BOOL finished) {
  33. _focusView.hidden = YES;
  34. }];
  35. }];
  36. }
  37. }

7、遇到的一些坑和解决办法

1) 前后置摄像头的切换

前后值不能切换,各种尝试找了半天没找到有原因。后来发现我在设置图片尺寸的时候设置为1080P [self.session canSetSessionPreset: AVCaptureSessionPreset1920x1080] ,前置摄像头并不支持这么大的尺寸,所以就不能切换前置摄像头。我验证了下 前置摄像头*高支持720P,720P以内可自由切换。  当然也可以在前后置摄像头切换的时候,根据前后摄像头来设置不同的尺寸,这里不在赘述。

2)焦点位置

CGPoint focusPoint = CGPointMake( point.y /size.height ,1-point.x/size.width );
setExposurePointOfInterest:focusPoint 函数后面Point取值范围是取景框左上角(0,0)到取景框右下角(1,1)之间。官方是这么写的:
The value of this property is a CGPoint that determines the receiver’s focus point of interest, if it has one. A value of (0,0) indicates that the camera should focus on the top left corner of the image, while a value of (1,1) indicates that it should focus on the bottom right. The default value is (0.5,0.5).
我也试了按这个来但位置就是不对,只能按上面的写法才可以。前面是点击位置的y/PreviewLayer的高度,后面是1-点击位置的x/PreviewLayer的宽度

3)对焦和曝光

我在设置对焦是 先设置了模式setFocusMode,后设置对焦位置,就会导致很奇怪的现象,对焦位置是你上次点击的位置。所以一定要先设置位置,再设置对焦模式。
曝光同上

8、写在*后

附上demo:https://github.com/nanshanyi/photographDemo
常用到的基本就这么多,写的并不完善,有什么不对的,欢迎大家批评指正,共同学习。

iOS – Masonry 使用中的一些整理

个人喜欢用纯代码写东西,其中用到*多的就是Masonry,我整理一些使用过程中一些点,方便以后使用.(基本的语法就不说了)

首先说几点:

  1. 我一般将数值类型的约束用mas_equalTo,而相对于某个控件,或者某个控件的某个约束,我会使用equalTo,如:
    make.size.mas_equalTo(CGSizeMake(100, 100));
    make.center.equalTo(weakSelf.view);
  2. setNeedsLayout:告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用layoutSubviews。
    layoutIfNeeded:告知页面布局立刻更新。所以一般都会和setNeedsLayout一起使用。如果希望立刻生成新的frame需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。
    layoutSubviews:系统重写布局
    setNeedsUpdateConstraints:告知需要更新约束,但是不会立刻开始
    updateConstraintsIfNeeded:告知立刻更新约束
    updateConstraints:系统更新约束
  3. - (void)updateViewConstraints ViewController的View在更新视图布局时,会先调用ViewController的updateViewConstraints 方法。我们可以通过重写这个方法去更新当前View的内部布局,而不用再继承这个View去重写-updateConstraints方法。我们在重写这个方法时,务必要调用 super 或者 调用当前View的 -updateConstraints 方法。

1. 视图居中显示

  1. // 防止block中的循环引用
  2. __weak typeof(self) weakSelf = self;
  3. UIView* view = [UIView new];
  4. view.backgroundColor = [UIColor brownColor];
  5. [self.view addSubview:view];
  6. //使用mas_makeConstraints添加约束
  7. [view mas_makeConstraints:^(MASConstraintMaker *make) {
  8. // 添加大小约束(make就是要添加约束的控件view)
  9. make.size.mas_equalTo(CGSizeMake(200, 200));
  10. // 添加居中约束(居中方式与self相同)
  11. make.center.equalTo(weakSelf.view);
  12. }];

2. 两个视图等宽高边距

  1. UIView* blackView = [UIView new];
  2. blackView.backgroundColor = [UIColor blackColor];
  3. [self.view addSubview:blackView];
  4. [blackView mas_makeConstraints:^(MASConstraintMaker *make) {
  5. //添加约束大小
  6. make.size.mas_equalTo(CGSizeMake(100, 100));
  7. //在 左,上 添加约束 (左、上约束都是20)
  8. make.left.and.top.mas_equalTo(20);
  9. }];
  10. UIView* grayView = [UIView new];
  11. grayView.backgroundColor = [UIColor lightGrayColor];
  12. [self.view addSubview:grayView];
  13. [grayView mas_makeConstraints:^(MASConstraintMaker *make) {
  14. // 大小、上边距约束与黑色view相同
  15. make.size.and.top.equalTo(blackView);
  16. // 添加右边距约束(这里的间距是有方向性的,左、上边距约束为正数,右、下边距约束为负数)
  17. make.right.mas_equalTo(20);
  18. }];

3. 键盘弹出和收回

  1. – (void)dealloc {
  2. [[NSNotificationCenter defaultCenter] removeObserver:self];
  3. }
  4. – (void)viewDidLoad {
  5. [super viewDidLoad];
  6. // Do any additional setup after loading the view.
  7. __weak typeof(self) weakSelf = self;
  8. _textField = [UITextField new];
  9. _textField.backgroundColor = [UIColor redColor];
  10. [self.view addSubview:_textField];
  11. [_textField mas_makeConstraints:^(MASConstraintMaker *make) {
  12. //left,right,centerx,y 不能共存只能有其二
  13. make.left.mas_equalTo(20);
  14. // make.right.mas_equalTo(-60);
  15. make.centerX.equalTo(weakSelf.view);
  16. make.height.mas_equalTo(40);
  17. make.bottom.mas_equalTo(0);
  18. }];
  19. // 注册键盘通知
  20. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrameNotification:) name:UIKeyboardWillChangeFrameNotification object:nil];
  21. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
  22. }
  23. – (void)keyboardWillChangeFrameNotification:(NSNotification *)notification {
  24. // 获取键盘基本信息(动画时长与键盘高度)
  25. NSDictionary *userInfo = [notification userInfo];
  26. CGRect rect = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
  27. CGFloat keyboardHeight = CGRectGetHeight(rect);
  28. CGFloat keyboardDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
  29. // 修改下边距约束
  30. [_textField mas_updateConstraints:^(MASConstraintMaker *make) {
  31. make.bottom.mas_equalTo(-keyboardHeight);
  32. }];
  33. // 更新约束
  34. [UIView animateWithDuration:keyboardDuration animations:^{
  35. [self.view layoutIfNeeded];
  36. }];
  37. }
  38. – (void)keyboardWillHideNotification:(NSNotification *)notification {
  39. // 获得键盘动画时长
  40. NSDictionary *userInfo = [notification userInfo];
  41. CGFloat keyboardDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
  42. // 修改为以前的约束(距下边距0)
  43. [_textField mas_updateConstraints:^(MASConstraintMaker *make) {
  44. make.bottom.mas_equalTo(0);
  45. }];
  46. // 更新约束
  47. [UIView animateWithDuration:keyboardDuration animations:^{
  48. [self.view layoutIfNeeded];
  49. }];
  50. }
  51. – (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  52. [super touchesBegan:touches withEvent:event];
  53. [self.view endEditing:YES];
  54. }

4. 三控件等宽间距

方法一:

array 的 mas_distributeViewsAlongAxis withFixedSpacing 变化的是控件 长度或宽度

定义一个存放三个控件的数组NSArray *array;
array = @[greenView,redView,blueView];

直接调用下面的方法:

  1. - (void)getHorizontalone
  2. {
  3. //方法一,array 的 mas_distributeViewsAlongAxis
  4. /**
  5. * 多个控件固定间隔的等间隔排列,变化的是控件的长度或者宽度值
  6. *
  7. * @param axisType 轴线方向
  8. * @param fixedSpacing 间隔大小
  9. * @param leadSpacing 头部间隔
  10. * @param tailSpacing 尾部间隔
  11. */
  12. // MASAxisTypeHorizontal 水平
  13. // MASAxisTypeVertical 垂直
  14. [arrayList mas_distributeViewsAlongAxis:MASAxisTypeHorizontal
  15. withFixedSpacing:20
  16. leadSpacing:5
  17. tailSpacing:5];
  18. [arrayList mas_makeConstraints:^(MASConstraintMaker *make) {
  19. make.top.mas_equalTo(60);
  20. make.height.mas_equalTo(100);
  21. }];

}

方法二:

array de mas_distributeViewsAlongAxis withFixedItemLength 控件size不变,变化的是间隙

  1. - (void)getVertical
  2. {
  3. /**
  4. * 多个固定大小的控件的等间隔排列,变化的是间隔的空隙
  5. *
  6. * @param axisType 轴线方向
  7. * @param fixedItemLength 每个控件的固定长度或者宽度值
  8. * @param leadSpacing 头部间隔
  9. * @param tailSpacing 尾部间隔
  10. */
  11. [arrayList mas_distributeViewsAlongAxis:MASAxisTypeVertical
  12. withFixedItemLength:60
  13. leadSpacing:40
  14. tailSpacing:10];
  15. [arrayList mas_makeConstraints:^(MASConstraintMaker *make) {
  16. // make.top.mas_equalTo(100);
  17. // make.height.mas_equalTo(100);
  18. make.left.mas_equalTo(20);
  19. make.right.mas_equalTo(-20);
  20. }];

}

以上俩方法都在NSArray+MASAdditions
方法三:直接设置multiplier实现等间距
  1. for (NSUInteger i = 0; i < 4; i++) {
  2. UIView *itemView = [self getItemViewWithIndex:i];
  3. [_containerView addSubview:itemView];
  4. [itemView mas_makeConstraints:^(MASConstraintMaker *make) {
  5. make.width.and.height.equalTo(@(ITEM_SIZE));
  6. make.centerY.equalTo(_containerView.mas_centerY);
  7. make.centerX.equalTo(_containerView.mas_right).multipliedBy(((CGFloat)i + 1) / ((CGFloat)ITEM_COUNT + 1));
  8. }];
  9. }
方法四: 利用透明等宽度的SpaceView实现等间距
  1. UIView *lastSpaceView = [UIView new];
  2. lastSpaceView.backgroundColor = [UIColor greenColor];
  3. [_containerView1 addSubview:lastSpaceView];
  4. [lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
  5. make.left.and.top.and.bottom.equalTo(_containerView1);
  6. }];
  7. for (NSUInteger i = 0; i < ITEM_COUNT; i++) {
  8. UIView *itemView = [self getItemViewWithIndex:i];
  9. [_containerView1 addSubview:itemView];
  10. [itemView mas_makeConstraints:^(MASConstraintMaker *make) {
  11. make.height.and.width.equalTo(@(ITEM_SIZE));
  12. make.left.equalTo(lastSpaceView.mas_right);
  13. make.centerY.equalTo(_containerView1.mas_centerY);
  14. }];
  15. UIView *spaceView = [UIView new];
  16. spaceView.backgroundColor = [UIColor greenColor];
  17. [_containerView1 addSubview:spaceView];
  18. [spaceView mas_makeConstraints:^(MASConstraintMaker *make) {
  19. make.left.equalTo(itemView.mas_right).with.priorityHigh(); // 降低优先级,防止宽度不够出现约束冲突
  20. make.top.and.bottom.equalTo(_containerView1);
  21. make.width.equalTo(lastSpaceView.mas_width);
  22. }];
  23. lastSpaceView = spaceView;
  24. }
  25. [lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
  26. make.right.equalTo(_containerView1.mas_right);
  27. }];
5. 动态改变字体宽度

和面方法4一样,利用spaceView来实现

  1. UIView* bgView = [[UIView alloc]init];
  2. bgView.backgroundColor = [UIColor yellowColor];
  3. [self.view addSubview:bgView];
  4. [bgView mas_makeConstraints:^(MASConstraintMaker *make) {
  5. make.left.and.right.mas_equalTo(0);
  6. make.top.mas_equalTo(@100);
  7. make.height.mas_equalTo(@100);
  8. }];
  9. listText = @[@”北京”,@”地大吴波啊”,@”你大爷”,@”我们的爱哎哎”];
  10. UIView *lastSpaceView = nil;
  11. for(int i = 0 ; i < listText.count; i ++)
  12. {
  13. UILabel* label = [UILabel new];
  14. label.text = listText[i];
  15. label.backgroundColor = RANDOMCOLOR;
  16. [bgView addSubview:label];
  17. UIView* lineView = [UIView new];
  18. lineView.backgroundColor = [UIColor redColor];
  19. [bgView addSubview:lineView];
  20. [label mas_makeConstraints:^(MASConstraintMaker *make) {
  21. make.top.bottom.mas_equalTo(0);
  22. if (lastSpaceView)
  23. {
  24. NSLog(@”存在 lastView”);
  25. make.left.equalTo(lastSpaceView.mas_right).mas_offset(@20);
  26. }else
  27. {
  28. NSLog(@”不存在存在 lastView”);
  29. make.left.equalTo(bgView.mas_left);
  30. }
  31. make.height.equalTo(bgView);
  32. }];
  33. lastSpaceView = label;
  34. [lineView mas_makeConstraints:^(MASConstraintMaker *make) {
  35. make.top.and.bottom.mas_equalTo(0);
  36. make.width.mas_equalTo(1);
  37. make.left.mas_equalTo(label.mas_right).mas_offset(@10);
  38. }];
  39. }
效果图:
%title插图%num

6. 父视图的高度,是里面俩控件高度的和
  1. UIView* bgView = [UIView new];
  2. bgView.backgroundColor = [UIColor purpleColor];
  3. [self.view addSubview:bgView];
  4. UILabel* titleLab = [UILabel new];
  5. titleLab.backgroundColor = [UIColor redColor];
  6. titleLab.textAlignment = NSTextAlignmentCenter;
  7. titleLab.font = [UIFont systemFontOfSize:15.f];
  8. titleLab.text = @”曹操——《短歌行》”;
  9. [bgView addSubview:titleLab];
  10. UILabel* contentLab = [UILabel new];
  11. contentLab.numberOfLines = 0 ;
  12. contentLab.textAlignment = NSTextAlignmentCenter;
  13. contentLab.backgroundColor = [UIColor brownColor];
  14. contentLab.font = [UIFont systemFontOfSize:13.f];
  15. contentLab.text = @” 对酒当歌,人生几何? 譬如朝露,去日苦多。\n 慨当以慷,忧思难忘。 何以解忧?唯有杜康。\n 青青子衿,悠悠我心。 但为君故,沉吟至今。\n 呦呦鹿鸣,食野之苹。 我有嘉宾,鼓瑟吹笙。\n 明明如月,何时可掇? 忧从中来,不可断*。\n 越陌度阡,枉用相存。 契阔谈宴,心念旧恩。\n 月明星稀,乌鹊南飞。 绕树三匝,何枝可依?\n 山不厌高,海不厌深。 周公吐哺,天下归心。”;
  16. [bgView addSubview:contentLab];
  17. //思路: 父视图的上间距等于title的上间距,父视图的下间距等于content的下间距
  18. __weak typeof(self) weakSelf = self;
  19. [bgView mas_makeConstraints:^(MASConstraintMaker *make) {
  20. make.left.mas_offset(@30);
  21. make.right.mas_offset(@30);
  22. make.centerY.equalTo(weakSelf.view);
  23. }];
  24. [titleLab mas_makeConstraints:^(MASConstraintMaker *make) {
  25. make.left.top.right.mas_equalTo(@0);
  26. }];
  27. [contentLab mas_makeConstraints:^(MASConstraintMaker *make) {
  28. make.left.right.mas_equalTo(@0);
  29. make.top.equalTo(titleLab.mas_bottom).mas_offset(@10);
  30. make.bottom.equalTo(bgView);
  31. }];

效果图:

%title插图%num

以后慢慢更新,记录方便以后使用

文/栋飞

//一些扒的别人的记录

自适应布局允许将宽度或高度设置为固定值.如果你想要给视图一个*小或*大值,你可以这样:

//width >= 200 && width <= 400make.width.greaterThanOrEqualTo(@200);make.width.lessThanOrEqualTo(@400)

约束的优先级

.priority允许你指定一个精确的优先级,数值越大优先级越高.*高1000.
.priorityHigh等价于 UILayoutPriorityDefaultHigh .优先级值为 750.
.priorityMedium介于高优先级和低优先级之间,优先级值在 250~750之间.
.priorityLow等价于 UILayoutPriorityDefaultLow , 优先级值为 250.

优先级可以在约束的尾部添加:

make.left.greaterThanOrEqualTo(label.mas_left).with.priorityLow();
make.top.equalTo(label.mas_top).with.priority(600);

center 中心

//使 centerX和 centerY = button1
make.center.equalTo(button1)

//使 centerX = superview.centerX – 5, centerY = superview.centerY + 10make.center.equalTo(superview).centerOffset(CGPointMake(-5, 10))

指定宽度为父视图的 1/4.

make.width.equalTo(superview).multipliedBy(0.25);

iOS-XCode不自动提示代码问题汇总

1.代码提示功能失效

解决方法:

在Xcode->Window->Projects选中你的项目,点击Derived Data右侧的Delete按钮

%title插图%num

用命令行解决,效果同上方

  1. cd到Xcode目录下:Xcode~/Library/Developer/Xcode/DerivedData
  2. ls(显示当前路径下的文件)
  3. 找到你项目名开头的目录
  4. cd 目录名
  5. rm -r Index 删除掉你的项目所用的索引文件夹

如果上面两种方法都不管用

1.找到DerivedData 文件夹 删除 (路径: ~/Library/Developer/Xcode/DerivedData)
2.删除com.apple.dt.Xcode 文件 (路径: ~/Library/Caches/com.apple.dt.Xcode)
3.如果不让删除,就退出Xcode,重启电脑即可
4.运行code就可以了

2.使用cocoapods导入第三方库,使用import不提示第三方库的头文件

解决方法:

选择target -> BuildSettings -> search Paths 下的 User Header Search Paths, 添加 $(PODS_ROOT),并设置为“recursive”

%title插图%num

3.Xcode7.3 添加自己创建的类,import类名不提示

解决方法:

buildingSetting —> 将Weak References in Manual Retain Release 设置为Yes

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