• 不能要求用户安装其他应用的客户端。比如第三方分享,未安装客户端时,应使用网页登录;无法使用网页登录时(如微信),隐藏分享入口。

  • If some features require signing in, provide a valid demo account username and password.

  • If there are special configurations to set, include the specifics. If features require an environment that is hard to replicate or require specific hardware, be prepared to provide a demo video or the hardware.

  • Apps or metadata that mentions the name of any other mobile platform will be rejected

  • Apps with placeholder text will be rejected (描述中提到与应用内容和功能无关信息会被拒。)

  • App names in iTunes Connect and as displayed on a device should be similar, so as not to cause confusion(应用在iTunes Connect与设备上显示的名称应该类似,否则会造成混淆。)

  • Small and large App icons should be similar, so as to not to cause confusion(不同尺寸的icon要一致,否则会造成混淆。)

  • Apps that provide Push Notifications without using the Apple Push Notification (APN) API will be rejected(不使用Apple Push Notification(APN) API提供消息推送的应用会被拒。)

  • Apps that send Push Notifications without first obtaining user consent will be rejected(在首次推送消息之前未取得的用户允许的应用会被拒。)

  • If some features require signing in, provide a valid demo account username and password.

  • If there are special configurations to set, include the specifics. If features require an environment that is hard to replicate or require specific hardware, be prepared to provide a demo video or the hardware.

  • iPhone Apps must also run on iPad without modification, at iPhone resolution, and at 2X iPhone 3GS resolution

  • Apps that include undocumented or hidden features inconsistent with the description of the App will be rejected

最近公司多了一个 XBOX 游戏机,老板带过来的。经常在公司的 XBOX 上玩 FIFA 2015。打得多了就有了一点心得,然后在网上搜了一些技巧。

直塞

直塞是最简单有效的进攻方法了。通过直塞能直接撕开对手防线,创造进攻机会,也是我最常用的进球方式。

直塞的方法是按 Y 键,地面直塞;LB+Y 空中直塞。

直塞的要点是力度、方向,还有最重要的时机。直塞时要注意前方球员不能处在越位位置。要看到本方球员启动向前冲了再传,不然传球了之后没有人接。地面直塞相对来说效果更好,但是如果传球线路有对方球员或传球方向太直时,选择空中直塞会比较好。

任意球

落叶球

朝着人墙发,一直按住上,然后按 B,力度大约45%~50%,看距离,远的话大一些。适用于近距离任意球,18~25码,大概就是禁区往外一点点,很有效的进球方式。

弧线球

球弧线从人墙边过。视角挪到球门一角,一直按住RB和右上(或者左上,朝着球门方向),然后按 B,力度大约60%。适用于25~40码。

过人

彩虹过人

按住 LT,按几下 RB,然后按右摇杆“下上上”

回旋

前进时按右摇杆右下或左下两次

护球键

LT,按住盘带或者需要时点几下。

拔球

右摇杆朝着要拔的方向按。

安装

使用 ruby 的 gem 命令即可下载安装:

1
2
$ sudo gem install cocoapods
$ pod setup

如果 gem 太老可能会有问题,使用以下命令升级:

1
$ sudo gem update --system

ruby 默认软件源在国外,速度较慢,更新一下 ruby 的源,使用如下代码将官方的 ruby 源替换成国内淘宝的源:

1
2
3
gem sources --remove https://rubygems.org/
gem sources -a https://ruby.taobao.org/
gem sources -l

使用 CocoaPods 的镜像索引

所有的项目的 Podspec 文件都托管在https://github.com/CocoaPods/Specs。第一次执行pod setup时,CocoaPods 会将这些podspec索引文件更新到本地的 ~/.cocoapods/目录下,这个索引文件比较大,有 80M 左右。所以第一次更新时非常慢。

一个叫 akinliu 的朋友在 gitcafe 和 oschina 上建立了 CocoaPods 索引库的镜像,因为 gitcafe 和 oschina 都是国内的服务器,所以在执行索引更新操作时,会快很多。如下操作可以将 CocoaPods 设置成使用 gitcafe 镜像:

1
2
3
pod repo remove master
pod repo add master https://gitcafe.com/akuandev/Specs.git
pod repo update

将以上代码中的 https://gitcafe.com/akuandev/Specs.git 替换成 http://git.oschina.net/akuandev/Specs.git 即可使用 oschina 上的镜像。

使用 CocoaPods

使用时在项目根目录中新建一个名为 Podfile 的文件,在里面指定项目需要的依赖库。以下是我在项目中使用的 Podfile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pod 'Masonry'           # 用于手写 autolayout
pod 'RSKImageCropper' # 图片裁剪
pod 'YYImage' # 图片编解码,支持 webp, apng, gif, png, jpeg 等格式
pod 'YYCache' # 缓存框架
pod 'JSPatch' # 热补丁修复
pod 'CocoaLumberjack' # 打印日志,可分等级颜色
pod 'TYAttributedLabel' # 加强版 Label,用来显示图文混排的内容

# ---- 闭源第三方库
pod 'UMengAnalytics-NO-IDFA' # 友盟统计
pod 'NewRelicAgent' # new relic,主要用来做性能统计
pod 'JPush' # 极光推送

# ---- shareSDK
# 主模块(必须)
pod 'ShareSDK3', '3.1.4'
# UI模块(非必须,需要用到ShareSDK提供的分享菜单栏和分享编辑页面需要以下1行)
pod 'ShareSDK3/ShareSDKUI'
# 平台SDK模块(对照一下平台,需要的加上。如果只需要QQ、微信、新浪微博,只需要以下3行)
pod 'ShareSDK3/ShareSDKPlatforms/QQ'
pod 'ShareSDK3/ShareSDKPlatforms/SinaWeibo'
pod 'ShareSDK3/ShareSDKPlatforms/WeChat'

编辑完 Podfile 后,执行

1
pod install

现在,你的所有第三方库都已经下载完成并且设置好了编译参数和依赖,只需要打开项目的.xcworkspace 就能使用 Podfile 里添加的第三方库了。

以后添加新库时,执行一次 pod install即可。
如果要更新库,执行 pod update。

以下是 Podfile 常用语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 库版本号相关
pod 'AFNetworking' # 不显式指定依赖库版本,表示每次都获取最新版本
pod 'AFNetworking', '2.0' # 只使用2.0版本
pod 'AFNetworking', '> 2.0' # 使用高于2.0的版本
pod 'AFNetworking', '>= 2.0' # 使用大于或等于2.0的版本
pod 'AFNetworking', '< 2.0' # 使用小于2.0的版本
pod 'AFNetworking', '<= 2.0' # 使用小于或等于2.0的版本 pod 'AFNetworking', '~> 0.1.2' # 使用大于等于0.1.2但小于0.2的版本
pod 'AFNetworking', '~>0.1' # 使用大于等于0.1但小于1.0的版本
pod 'AFNetworking', '~>0' # 高于0的版本,写这个限制和什么都不写是一个效果,都表示使用最新版本

# 指定仓库 git 地址
pod 'TYAttributedLabel', :git => 'https://github.com/jayi/TYAttributedLabel.git'
# 按 commit 指定仓库版本
pod 'TYAttributedLabel', :commit => 'ffae09'

关于 Podfile.lock

当你执行pod install之后,除了 Podfile 外,CocoaPods 还会生成一个名为Podfile.lock的文件,Podfile.lock 应该加入到版本控制里面,不应该把这个文件加入到.gitignore中。因为Podfile.lock会锁定当前各依赖库的版本,之后如果多次执行pod install 不会更改版本,要pod update才会改Podfile.lock了。这样多人协作的时候,可以防止第三方库升级时造成大家各自的第三方库版本不一致。

Git提供了多种修复开发过程中错误的方法. 方法的选择取决于你的情况: 包含有错误的文件是否提交了(commited); 如果已经提交了, 那么你是否已把有错误的提交与其它人共享这也很重要.

修复未提交文件中的错误(重置)

如果你现在的工作目录(work tree)里搞的一团乱麻, 但是你现在还没有把它们提交; 你可以通过下面的命令, 让工作目录回到上次提交时的状态(last committed state):

$ git reset --hard HEAD

这条件命令会把你工作目录中所有未提交的内容清空(当然这不包括未置于版控制下的文件 untracked files). 从另一种角度来说, 这会让”git diff” 和”git diff –cached”命令的显示法都变为空.

如果将 HEAD 替换成某个结点的 hash 值,会强制将当前分支指向那个结点。这个方法可以用来:

  • 清除已提交的 commit
  • 快速更新

git reset –soft, –mixed, –hard 关系如下图

如果你只是要恢复一个文件,如”hello.rb”, 你就要使用 git checkout

$ git checkout -- hello.rb

这条命令把hello.rb从HEAD中签出并且把它恢复成未修改时的样子.

以上命令分别对应 sourcetree 下的 reset to commit 和 reset 文件操作。

开发过程中建议先把可以正常工作或确定的代码 stage,这样清理代码时只需要清理 unstaged 的代码就可以了。

修复已提交文件中的错误

如果你已经做了一个提交(commit),但是你马上后悔了, 这里有两种截然不同的方法去处理这个问题:

  • 创建一个新的提交(commit), 在新的提交里撤消老的提交所作的修改. 这种作法在你已经把代码发布的情况下十分正确.
  • 你也可以去修改你的老提交(old commit). 但是如果你已经把代码发布了,那么千万别这么做; git不会处理项目的历史会改变的情况,如果一个分支的历史被改变了那以后就不能正常的合并.

创建新提交来修复错误

创建一个新的,撤消(revert)了前期修改的提交(commit)是很容易的; 只要把出错的提交(commit)的名字(reference)做为参数传给命令: git revert就可以了; 下面这条命令就演示了如何撤消最近的一个提交:

$ git revert HEAD

这样就创建了一个撤消了上次提交(HEAD)的新提交, 你就有机会来修改新提交(new commit)里的提交注释信息.

你也可撤消更早期的修改, 下面这条命令就是撤消“上上次”(next-to-last)的提交:

$ git revert HEAD^

在这种情况下,git尝试去撤消老的提交,然后留下完整的老提交前的版本. 如果你最近的修改和要撤消的修改有重叠(overlap),那么就会被要求手工解决冲突(conflicts), 就像解决合并(merge)时出现的冲突一样.

git revert 其实不会直接创建一个提交(commit), 把撤消后的文件内容放到索引(index)里,你需要再执行git commit命令,它们才会成为真正的提交(commit).

修改提交来修复错误

如果你刚刚做了某个提交(commit), 但是你又想马上修改这个提交; git commit 现在支持一个叫–amend的参数,它能让你修改刚才的这个提交(HEAD commit). 这项机制能让你在代码发布前,添加一些新的文件或是修改你的提交注释(commit message).

git commit –amend 建议只用在修改未上传到服务器的提交的注释。

如果你在老提交(older commit)里发现一个错误, 但是现在还没有发布到代码服务器上. 你可以使用 git rebase命令的交互模式, “git rebase -i”会提示你在编辑中做相关的修改. 这样其实就是让你在rebase的过程来修改提交.

确认当前所在分支

进行分支操作前,请先确认当前工作分支,避免出现误操作。

  • 使用 git branch 命令,见下文
  • souretree 左侧边栏 BRANCHES 下,加粗的是当前分支

rebase 与 merge

  • merge 是直接将两个分支汇合。
  • rebase 是将当前分支在要合并的分支上重新演化。工作流程如下:
    1. 先找到当前分支与要合并分支的分叉点
    2. 对分叉点后的每个提交,计算它与前一点的差异代码, 在要合并分支上应用并提交(这一步有可能产生冲突)
    3. 删除当前分支的到分叉点的所有记录
    4. 将当前分支指针指向第2步得到的演化结果

before-pull
合并前的状态

git-pull
git merge origin/master

git-pull-rebase
git rebase origin/master。算出 b1 到 a1的 差异代码,应用到 a2 上,master 指针指向新的 b1,删除旧的 b1。

远程分支

$ git pull

如果设置了某个分支用于跟踪某个远程仓库的分支,此命令可拉取数据,并自动合并对应分支。
默认情况下 git clone 命令本质上就是自动创建了本地的 master 分支用于跟踪远程仓库中的 master 分支

使用git pull 时,请加上 –rebase,保持线性历史记录
before-pull
pull 前的状态

git-pull
git pull 等价于 git merge origin/master,执行后产生了分叉

git-pull-rebase
git pull –rebase,等价于 git rebase origin/master,前当前所有提交记录在 origin/master 上重演,保持线性历史。


$ git fetch [remote-name]

从远程仓库抓取数据到本地。不指定远程仓库名时默认为origin。此命令仅拉取数据,并不自动合并。


$ git push [remote-name] [branch-name]

将本地分支推送到远程仓库。
如果在你推数据前,已经有其他人推送了若干更新,那你的推送操作就会被驳回。你必须先把他们的更新抓取到本地,合并到自己的项目中,然后才可以再次推送。


$ git push origin serverfix

推送本地serverfix分支到远程仓库origin的serverfix分支。


$ git push origin serverfix:awesomebranch

推送本地serverfix分支到远程仓库origin的awesomebranch。


$ git push origin :serverfix

删除远程仓库serverfix分支。


$ git branch

列出所有分支。* 所指的是 HEAD 所指向的分支,即当前工作分支。


$ git checkout -b serverfix origin/serverfix

根据远程仓库origin的serverfix分支新建本地serverfix分支。


$ git checkout --track origin/serverfix

效果同上。


$ git branch testing

在当前 commit 对象上新建一个分支指针。仅建立分支,但不切换。
HEAD指针总是指向当前正在工作的分支。


$ git checkout testing

切换到testing分支。


$ git checkout -b iss53

新建iss53分支,并切换到此分支,相当于:

$ git branch iss53
$ git checkout iss53

$ git branch -d hotfix

删除hotfix分支。-D为强制删除。


Git使用流程建议

  • Git 新建、切换分支速度很快,可以同时建多个分支进行不同功能的开发,调试。

  • 何时使用 rebase 何时使用 merge?

    • git pull 使用–rebase。本地分支操作用 merge。当master上有很多 commit 时,rebase 冲突的概率会很大,应当避免这种情况。这种情况更合理的做法是切换到一个新的分支,在新的分支开发。
    • 使用 sourcetree 进行本地分支的 merge 时,不要选择 Rebase instead of merge (最后一项)
  • 什么时候需要新建分支?

    • 需要多次提交时,或者改动较多时,新建分支开发。
    • 简单的 Bug 修复,或者只需要一两次提交小功能点,可以直接在本地的主干上开发。
  • 同步流程

    • 本地没有更新时,直接 git pull –rebase 即可与远程分支同步。
    • 在本地主干上开发,commit 到本地后,git pull –rebase 与远程分支同步,最后 push。
    • 在新分支上开发结束,要提交到远程分支时,先将 master 同步到最新,再将新分支 merge 到 master,最后 push。
    • 在新建的分支开发,需要与最新代码同步时,可以git merge origin/master。这种情况会造成较混乱提交历史记录,少用。

Mac 系统自带的 vim 编译时没加clipboard选项,不能使用系统剪贴板。
可以使用 brew的 macvim:

1
$ brew install macvim --with-override-system-vim
$ brew linkapps

如果 vimrc 添加了

1
set clipboard=unnamed

则可以直接使用系统剪贴板而不用加前缀”*

lambda 表达式

匿名函数,简化代码。
简化前:

1
2
3
4
5
6
7
8
9
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
for (String item : list) {
System.out.println(item);
}

使用 lambda 表达式简化后:

1
2
list.sort((a, b) -> b.compareTo(a));
list.forEach(System.out::println);

函数接口

定义:函数接口是只有一个抽象方法的接口,用于作为Lambda表达式的类型.

以下6个是 java 定义的6个常用函数接口

函数名 参数 返回类型 示例
Predicate T boolean 判断合格
Consumer T void 日志记录
Function T R 获取对象成员
Supplier None T 工厂方法
UnaryOperator T T 小写转大写
BinaryOperator (T,T) T 两数之和

Stream

  • Stream是Java8中重要的特性之一,它是一组对集合的操作方法,使得程序员得以站在更高的抽象层次上对集合进行操作
  • Stream中提供并行操作,使得对集合进行一些并行操作变得更加容易

示例

给字符串列表加前缀,并且转为数组,期间不能修改原来列表。

通常:

1
2
3
4
5
List<String> cityList = ....;
String[] cityArrays = new String[cityList.size()];
for (int i = 0; i < cityList.size(); i++) {
cityArrays[i] = pre + cityList.get(i);
}

使用Stream:

1
2
3
4
List<String> cityList = ....;
String[] cityArray = cityList.stream()
.map(city -> pre + city) // 惰性求值
.toArray(String[]::new); // 及早求值

使用Stream简化了代码,也使得业务流程更加清晰。

惰性求值是一个中间过程,它的返回值还是Stream。
及早求值是一个结束过程,它的返回值是相应的结果类型。
惰性求值一般表示Stream流还在继续,因此一条流中可以有多个惰性求值,而及早求值一般表示一条Stream流结束,因此一条流中最多只有一次及早求值。

注意:如果一条Stream调用链中,没有及早求值,该调用链实际是不执行的。

stream 方法

构建流对象的方式

  • Stream.of()

    1
    2
    3
    4
    5
    6
     public class StreamBuilders {
    public static void main(String[] args) {
    Stream<String> stream = Stream.of("a", "b", "c", "d", "e", "f", "g");
    stream.forEach(p -> System.out.println(p));
    }
    }
  • Collection.stream()

    1
    2
    3
    4
    5
    6
    7
    public class StreamBuilders {
    public static void main(String[] args) {
    List<String> strings = Arrays.asList("a", "b", "c", "d", "e", "f", "g");
    Stream<String> stream = strings.stream();
    stream.forEach(p -> System.out.println(p));
    }
    }
  • Stream.generate()

    1
    2
    3
    4
    5
    6
    7
    8
    public class StreamBuilders {
    public static void main(String[] args) {
    Stream<Double> stream = Stream.generate(() -> {
    return Math.random();
    }).limit(5);
    stream.forEach(p -> System.out.println(p));
    }
    }
  • 使用 iterate()

    1
    2
    3
    4
    5
    6
    public class StreamBuilders {
    public static void main(String[] args) {
    Stream<Integer> stream = Stream.iterate(10, i -> i + 10).limit(5);
    stream.forEach(i -> System.out.println(i));
    }
    }
  • 使用 CharSequence.chars()

    1
    2
    3
    4
    5
    6
    public class StreamBuilders {
    public static void main(String[] args) {
    IntStream stream = "ABCDEFG_abcdefg".chars();
    stream.forEach(p -> System.out.println(p));
    }
    }

转换流为集合

  • stream.collect(Collectors.toList()) 把 Stream 转换为 List

    1
    2
    3
    4
    5
    6
    7
    8
    public class StreamBuilders {
    public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Stream<Integer> stream = list.stream();
    List<Integer> oddNumbersList = stream.filter(i -> i % 2 != 0).collect(Collectors.toList());
    System.out.print(oddNumbersList);
    }
    }
  • stream.collect(Collectors.toSet()) 把 Stream 转换为 Set

    1
    2
    3
    4
    5
    6
    7
    8
    public class StreamBuilders {
    public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Stream<Integer> stream = list.stream();
    List<Integer> oddNumbersSet = stream.filter(i -> i % 2 != 0).collect(Collectors.toSet());
    System.out.print(oddNumbersSet);
    }
    }
  • stream.toArray(EntryType[]::new) 把 Stream 转换为数组

    1
    2
    3
    4
    5
    6
    7
    8
    public class StreamBuilders {
    public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Stream<Integer> stream = list.stream();
    Integer[] oddNumbersArray = stream.filter(i -> i % 2 != 0).toArray(Integer[]::new);
    System.out.print(oddNumbersArray);
    }
    }

核心的流操作

我们先构建一个字符串集合,下面的例子都会基于这个集合进行操作。

1
List<String> provinceNames = Arrays.asList("湖南", "湖北", "河南", "河北", "广东", "广西", "北京", "南京");

中间操作

中间操作返回流对象,所以你可以把多个中间操作串成一行。

  • filter()

    • filter 方法接受一个断言对象,用来过滤流中的元素。
      1
      2
      3
      4
      provinceNames.stream().filter((p) -> p.startsWith("湖")).forEach(p -> System.out.print(p + ", "));

      // 输出 :
      // 湖南, 湖北,
  • map()

    • 这个中间操作使用给定的函数把流中的每个元素转换为另一个对象。下面的例子把每个字符串转化为另一个字符串(末尾多了一个人字)。但是你也可以把每个对象转换为其它类型的对象。
      1
      2
      3
      4
      provinceNames.stream().map(p -> p + "人, ").forEach(p -> System.out.print(p));

      // 输出 :
      // 湖南人, 湖北人, 河南人, 河北人, 广东人, 广西人, 北京人, 南京人,
  • sorted()

    • sorted 会对流中的元素进行排序,默认按自然序进行排序,除非你指定一个自定义的比较器(Comparator) 。
      1
      2
      3
      4
      provinceNames.stream().sorted().map(p -> p + "人, ").forEach(p -> System.out.print(p));

      // 输出 :
      // 北京人, 南京人, 广东人, 广西人, 河北人, 河南人, 湖北人, 湖南人,
需要注意的是 sorted 仅仅对流中的元素进行排序,而不会影响后面的集合,集合中的元素排序保持不变。

末端操作

末端操作返回某一类型的结果,而不是流对象。

  • forEach()

    • 该方法帮助迭代流中的元素并在元素上执行一些操作。这些操作可以是 lambda 表达式或者方法引用。
      1
      provinceNames.stream().forEach(System.out::println);
  • collect()

    • collect() 方法用来从流中抽取元素然后保存到一个集合或者数组中。
      1
      2
      3
      4
      List<String> provinceNamesOrdered = provinceNames.stream().sorted().collect(Collectors.toList());
      System.out.print(provinceNamesOrdered);
      // 输出 :
      // [北京, 南京, 广东, 广西, 河北, 河南, 湖北, 湖南]
  • match()

    • 各种匹配操作用来检查流中的元素是否指定断言条件。所有的匹配操作都是末端操作,它们返回布尔结果。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      boolean matchedResult = provinceNames.stream().anyMatch((s) -> s.startsWith("湖"));
      System.out.println(matchedResult);

      matchedResult = provinceNames.stream().allMatch((s) -> s.startsWith("湖"));
      System.out.println(matchedResult);

      matchedResult = provinceNames.stream().noneMatch((s) -> s.startsWith("湖"));
      System.out.println(matchedResult);

      // 输出 :
      // true
      // false
      // false
  • count()

    • count 末端操作返回流中元素个数。
      1
      2
      3
      4
      long totalMatched = provinceNames.stream().count();
      System.out.println(totalMatched);

      // 输出 : 8
  • reduce()

    • 该末端操作使用给定的函数对流中的元素进行归约。它是这样一个过程:每次迭代,将上一次的迭代结果(第一次时为 identity 的元素,如没有 identity 则为流中的第一个元素)与下一个元素一同执行一个二元函数。在 reduce 操作中,identity 是可选的,如果使用,则作为第一次迭代的第一个元素使用。归约的结果保存在 Optional 中。
      1
      2
      3
      4
      5
      Optional<String> reduced = provinceNames.stream().reduce((s1, s2) -> s1 + "#" + s2);
      reduced.ifPresent(System.out::println);

      // 输出 :
      // 湖南#湖北#河南#河北#广东#广西#北京#南京

短路操作

流操作通常会在流中满足某一断言的所有元素上进行操作,有时我们希望在迭代过程中遇到匹配的元素时就终止操作,在外部迭代中,你需要写 if-else 代码块,在内部迭代中,有现成的方法可以使用,下面是两个这样的方法的示例:

  • anyMatch()

    • 该操作只要遇到满足断言条件的元素就会返回 true,在此之后就不再处理任何其它的元素了。
      1
      2
      3
      4
      boolean matched = provinceNames.stream().anyMatch((s) -> s.startsWith("河"));
      System.out.println(matched);

      // 输出 : true
  • findFirst()

    • 该操作会返回流中的第一个元素,然后就不再处理其它元素了。
      1
      2
      3
      4
      String firstMatchedName = provinceNames.stream().filter((s) -> s.startsWith("广")).findFirst().get();
      System.out.println(firstMatchedName);

      // 输出 : 广东

Optional 接口

Optional 被定义为一个简单的容器,其值可能是null或者不是null
在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null
在Java 8中,不推荐你返回null而是返回Optional

1
2
3
4
5
6
String temp = "temp";
Optional<String> stringOptional = Optional.ofNullable(temp);
// 不为空则执行
stringOptional.ifPresent(System.out::println);
// 不为空返回值,为空则返回指定的值
stringOptional.orElse("tempElse");

将电话号码列表转化为以逗号隔开的字符串,如果为空则抛出异常

1
2
3
String phoneNumber = subList.stream()
.reduce((a, b) -> a + "," + b)
.orElseThrow(Exception::new);

图标快捷菜单

iOS 9 新增了桌面图标快捷菜单的功能(Home Screen Quick Actions)。用户按压桌面图标能弹出几个选项,进行某些快捷操作。

iOS 9 下每个应用最多能显示4个桌面快捷选项。系统优先显示静态快捷选项,从上往下。如果未超出限制,且应用有定义动态快捷选项,显示动态快捷选项。

静态快捷选项

在应用的 Info.plist 顶层中新增 UIApplicationShortcutItems 数组。

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
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeSearch</string>
<key>UIApplicationShortcutItemSubtitle</key>
<string>shortcutSubtitle1</string>
<key>UIApplicationShortcutItemTitle</key>
<string>shortcutTitle1</string>
<key>UIApplicationShortcutItemType</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).First</string>
<key>UIApplicationShortcutItemUserInfo</key>
<dict>
<key>firstShorcutKey1</key>
<string>firstShortcutKeyValue1</string>
</dict>
</dict>
<dict>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeShare</string>
<key>UIApplicationShortcutItemSubtitle</key>
<string>shortcutSubtitle2</string>
<key>UIApplicationShortcutItemTitle</key>
<string>shortcutTitle2</string>
<key>UIApplicationShortcutItemType</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).Second</string>
<key>UIApplicationShortcutItemUserInfo</key>
<dict>
<key>secondShortcutKey1</key>
<string>secondShortcutValue1</string>
</dict>
</dict>
</array>

静态定义快速在运行时常用的key:
key | 说明
— | —
UIApplicationShortcutItemType | (必须使用) 用来区分与其他快速选项的分类
UIApplicationShortcutItemTitle | (必须使用) 快速选项显示的标题
UIApplicationShortcutItemSubtitle | 快速选项显示的子标题
UIApplicationShortcutItemIconType | 图片类型由系统提供
UIApplicationShortcutItemIconFile | 自定义的图标
UIApplicationShortcutItemUserInfo | 附加信息

动态快捷选项

指定[UIApplication sharedApplication].shortcutItems。
动态快捷选项必须启动应用且设置shortcutItems后,才能生效。

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
- (void)setupApplicationShortcutItems
{
UIApplicationShortcutIcon *homeIcon =
[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypeCompose];
NSDictionary *userInfo1 = @{@"key":@"home"};
NSDictionary *userInfo2 = @{@"key":@"message"};
UIMutableApplicationShortcutItem *homeShortcutItem =
[[UIMutableApplicationShortcutItem alloc] initWithType:@"home"
localizedTitle:@"主页"
localizedSubtitle:@"跳转主页"
icon:homeIcon
userInfo:userInfo1];
UIMutableApplicationShortcutItem *messageShortcutItem =
[[UIMutableApplicationShortcutItem alloc] initWithType:@"message"
localizedTitle:@"消息"
localizedSubtitle:@"跳转消息"
icon:nil
userInfo:userInfo2];
NSArray *items = @[homeShortcutItem,messageShortcutItem];
[UIApplication sharedApplication].shortcutItems = items;
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self setupApplicationShortcutItems];
...
}

快捷选项操作

在AppDelegate.m文件中添加
application:performActionForShortcutItem:completionHandler:
代理方法,根据UIApplicationShortcutItem的type属性和之前在info.plist设置UIApplicationShortcutItemType对应的在值来判断,用户点击的是哪个快速选项。这个方法在接近激活应用时就会调用,除了
-application:willFinishLaunchingWithOptions:
or
-application:didFinishLaunchingWithOptions
returns NO.
这种情况。

在-application:didFinishLaunchingWithOptions
中可以通过UIApplicationLaunchOptionsShortcutItemKey键来获取当前接收快速选项的UIApplicationShortcutItem对象,来处理用户的意图。

Peek 和 Pop

这个功能是一套全新的用户交互机制,在使用3D Touch时,ViewController中会有如下三个交互阶段:

  1. 提示用户这里有3D Touch的交互,会使交互控件周围模糊
  2. 继续深按,会出现预览视图
  3. 通过视图上的交互控件进行进一步交互

peek 和 pop 的核心主要是 iOS9 上新增的以下几个代理方法:

  • registerForPreviewingWithDelegate:sourceView:
  • previewingContext:viewControllerForLocation:
  • previewingContext:commitViewController:
  • previewActionItems

下面结合几段代码看看 peek 和 pop 的用法。

假设现在有两个 ViewController,MasterViewController 和 DetailViewController,MasterViewController 上有一个 TableView,点击上面的 Cell 进入 DetailViewController。

首先,MasterViewController 需要调用注册接口以支持3D touch,指定接收3D touch 事件的 view 和处理 peek 和 pop 的代理 。

1
2
3
4
5
6
// 判断设备是否支持3D touch
if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
// 支持 3D touch 的话,注册 ViewController 以使用 peek 和 pop。
// 指定 Previewing 的代理为 MasterViewcontroller 自己。
[self registerForPreviewingWithDelegate:self sourceView:self.view];
}

如果考虑得周全一点,应该在识别到用户关闭了 3D touch 时,调用unregisterForPreviewingWithContext:注销 3D touch。

接下来,在 MasterViewController 上需要实现以下代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Pop 事件代理, 重按时进入 DetailViewController
- (void)previewingContext:(id<UIViewControllerPreviewing>)previewingContext
commitViewController:(UIViewController *)viewControllerToCommit {
[self showViewController:viewControllerToCommit sender:self];
}

// Peek 事件代理,识别 Peek 操作点击区域,指定预览的 ViewController
- (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext
viewControllerForLocation:(CGPoint)location {
NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:location];
UITableViewCell *cell = [_tableView cellForRowAtIndexPath:indexPath];
if (cell == nil) return nil;

DetailViewController *detailViewController = [DetailViewController new];
// 读取对应 model 数据到 DetailViewController 中。
[detailViewControler loadDataFromModel:self.modelList[indexPath.row]];

// 设置点击的 cell,聚集此处,周围会产生模糊效果
[previewingContext setSourceRect:cell.frame];
// 设置预览框大小,设置或设置为0会使用默认大小。
[detailViewController setPreferredContentSize = CGSizeMake(0, previewDetail.preferredHeight)];

return cv;
}

通常,在弹出预览窗口后,可以上滑,弹出菜单,进行某些快捷操作。前提是预览的 ViewController 实现了-previewActionItems,指定了peek 快捷操作。如果没实现该代理,则没有快捷菜单,弹出预览后滑动没有效果,放开就消失了。
快捷菜单可以通过 UIPreiveActionGroup 组织成多个层级,详情见代码。

以下是 DetailViewController 的 previewActionItems 示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (NSArray<id<UIPreviewActionItem>> *)previewActionItems {
UIPreviewAction *action1 = [UIPreviewAction actionWithTitle:@"action1"
style:UIPreviewActionStyleDefault
handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
NSLog(@"preview action 1");
}];
UIPreviewAction *action2 = [UIPreviewAction actionWithTitle:@"action2"
style:UIPreviewActionStyleDestructive
handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
NSLog(@"preview action 2");
}];
UIPreviewAction *action3 = [UIPreviewAction actionWithTitle:@"action3"
style:UIPreviewActionStyleSelected
handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
NSLog(@"preview action 3");
}];
UIPreviewActionGroup *actionGroup = [UIPreviewActionGroup actionGroupWithTitle:@"action group"
style:UIPreviewActionStyleDefault
actions:@[action2, action3] ];
return @[action1, actionGroup];
}

UITouch

UITouch 新增两个用于 3d touch 的字段: force 和 maximumPossibleForce。这些属性可以让你侦测并响应APP接收的UIEvent触摸压力。

skiplist 又称跳跃表,是一个数据节构,可以实现红黑树类似的功能,插入或删除元素后保持有序。插入和删除的复杂度都是O(log n)

skiplist 除了具有红黑树的功能外,还能支持从下标取元素的操作(类似skipList[10]这样的访问)。

skiplist 基于概率均衡,不保证平衡性(但通常效果都不错)。它相对于传统的平衡树优势在于原理更简单,代码实现上更简洁高效。

原理

跳跃表可以看作是传统单链表的扩展。有序的单链表插入和删除的复杂度都是O(n)的。

跳跃表对有序单链表的每个节点进行了扩展。在每个节点原来有一个指向下一节点的指针的基础上,新增一系列指针,让第二个指针指向当前节点往后数第2个节点,第三个指针指向当前节点住后数第4个节点…以此类推,第n个指针指向当前点后的第$2^{n-1}$个节点。如下图如示。实际实现时,是基于随机算法,步长不是完全精确的为$2^{n-1}$。

跳跃表

这样一来,插入、删除或者查找节点时,从上往下,先用上层的指针确定范围,再往下一层,减小步长,继续查找,缩小范围,直到确定位置。

通过对单链表扩展,新增一系列步长指数增长的指针,跳跃表插入、删除、查找的复杂度优化到了O(log n)

实现

接下来看看跳跃表的 java 实现。

数据结构

首先需要一个数据结构存储每个节点。

1
2
3
4
5
6
7
8
private class Node<T> {
T element; // 该节点的标识,通常为 string
Integer score; // 该节点的值,作为排序依据
Node<T>[] backward; // 前驱
Node<T>[] level; // 后继
// 跳跃跨度,表示当前节点与level[i]中间有多少个结点
int[] span = new int[MAX_LEVEL];
}

然后是整个跳跃表类。

1
2
3
4
5
6
7
public class SkipList<E> {
final static int MAX_LEVEL = 32; // 最大层数,32表示能存2^32个元素
private Node<E> head; // 链表头指针,头指针和尾指针不存数据
private Node<E> tail; // 链表尾指针
public int level; // 当前跳跃表层数
public int size; // 链表元素个数
}

查询

查询对应位置的元素

在维护好跳跃表的前提下,查询跳跃表中给定位置的元素是很容易的。

  1. 从 head 结点开始,level = MAX_LEVEL - 1 开始遍历
  2. 如果跳到 next[level] 后超出查询的下标,level 减1,节点的下标由遍历时前驱的 span 之和计算得来;
  3. 否则跳到 next[level] 节点;
  4. 重复2,3步,直到level=-1.
1
2
3
4
5
6
7
8
9
10
11
public Node<E> findEntry(int index) {
Node<E>entry = head;
for (int i = MAX_LEVEL - 1; i >= 0; --i) {
while (entry.level[i] != null && entry.span[i] <= index) { index -= entry.span[i]; entry = entry.level[i]; } } return entry; } ``` ### 查询某个区间顺序所有元素 先利用上一步结果,查到 startIdx 对应的结点,然后从该点开始遍历即可。 ``` public Set<Pair<E, Integer>> range(int startIdx, int endIdx) {
TreeSet<Pair<E, Integer>> result = new TreeSet<Pair<E, Integer>>(pairComparator);
Node<E>node = this.findEntry(startIdx);
for (int idx = startIdx; idx < endIdx + 1 && node != null && node != tail; ++idx, node = node.level[0]) {
result.add(new Pair<E, Integer>(node.element, node.score));
}
return result;
}

查询某个元素的下标

和第一步查询下标对应的元素类似,上面是把 index 减到0,这边让 index 从0开始加,直到跳到链表中与要查询元素值一样的时候结束。最后得到的 index 便是需要的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private int getIndex(Node<E>node) {
int index = 0;
Node<E>entry = head;
for (int i = Math.min(level, MAX_LEVEL - 1); i >= 0; --i) {
while (entry.level[i] != null && entry.level[i].compareTo(node) <= 0) { index += entry.span[i]; entry = entry.level[i]; } assert entry.compareTo(node) <= 0; } return index; } ``` ## 插入元素 插入元素时,先利用前面的方法查找到需要插入的位置前插入,更新level 0的前驱后继关系。 ```java public boolean add(E element, int score) { Node<E>node = new Node<E>(element, score);
Node<E>entry = findEntry(element, score);
if (entry.compareTo(node) == 0 || (entry.level[0] != null && entry.level[0].compareTo(node) == 0)) {
return true;
}

node.span[0] = 1;
size += 1;
Node<E>next = entry.level[0];
if (next != null) {
next.backward[0] = node;
}
node.backward[0] = entry;
node.level[0] = next;
entry.level[0] = node;

updateSpanAdd(node);
return true;
}

插入元素后更新 span

如果只是插入更新 level 0 指针,这样就退化成简单的双向链表了。我们还需要结新加入的结点随机赋予高度,并更新 span 值。
更新 span 的主要思想是,新加入节点在 level i 的 span 值为在 level i 的前驱(backward[i])的 span 值减去新节点同前驱的 index 之差。与此同时,更新前驱的 span 值。

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
public void updateSpanAdd(Node<E> node) {
Node<E>entry = head;
int randomLevel = 0;

// 新节点赋予随机高度以维持平衡,保证平均复杂度为 O(log n)
for (int i = 0; i < level; ++i) {
if (Math.random() > 0.5) {
randomLevel++;
}
}
// 高度与目前最大高度相同,最大高度+1
if (randomLevel == level && level < MAX_LEVEL - 1) {
++level;
head.span[level] = size;
head.level[level] = tail;
tail.backward[level] = head;
}

int nodeIndex = this.getIndex(node);
int index = 0;
entry = head;
for (int i = Math.min(level, MAX_LEVEL - 1); i >= 1; --i) {
while (entry.level[i] != null && entry.level[i].compareTo(node) < 0) {
index += entry.span[i];
entry = entry.level[i];
}
assert entry.compareTo(node) < 0;
if (entry.span[i] != 0 && i > 0) entry.span[i] += 1;

if (i <= randomLevel) { node.span[i] = entry.span[i] - (nodeIndex - index); node.level[i] = entry.level[i]; entry.level[i] = node; entry.span[i] = nodeIndex - index; node.backward[i] = entry; Node<E>next = node.level[i];
if (next != null) {
next.backward[i] = node;
}
}
}
}

删除元素

删除前更新 span

与插入操作不同,删除元素时需要先更新 span,对要删除结点所有 level 上的前驱,对应的 span 减1。

1
2
3
4
5
6
public void updateSpanRemove(Node<E> node) {
Node<E>entry = head;
for (int i = Math.min(level, MAX_LEVEL - 1); i >= 1; --i) {
while (entry.level[i] != null && entry.level[i].compareTo(node) <= 0) { entry = entry.level[i]; } if (entry.span[i] != 0 && i> 0) entry.span[i] -= 1;
}
}

从跳跃表中删除元素

查找到要删除元素的位置,从链接中删除。更新每个 level 上的前驱后继关系和前驱的 span 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean remove(E element, int score) {
// FIXME: 必要时降低 level
Node<E>node = new Node<E>(element, score);

this.updateSpanRemove(node);
Node<E>entry = findEntry(element, score);
if (entry.compareTo(node) < 0 && entry.level[0] != null) entry = entry.level[0];
if (entry.compareTo(node) != 0) return false;
for (int i = level; i >= 0; --i) {
Node<E>prev = entry.backward[i];
Node<E>next = entry.level[i];
if (prev != null) {
prev.level[i] = next;
if (i > 0) prev.span[i] += entry.span[i];
}
if (next != null) {
next.backward[i] = prev;
}
}
--size;
return true;
}

1
find ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins -name Info.plist -maxdepth 3 | xargs -I{} defaults write {} DVTPlugInCompatibilityUUIDs -array-add `defaults read /Applications/Xcode.app/Contents/Info.plist DVTPlugInCompatibilityUUID`