注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

我写的页面打开才用了10秒,产品居然说我是腊鸡!!!

背景 产品:你看看这页面加载的如此之慢,怎么会有用户用呢?(并甩给了我一个录屏) 我: (抛出前端应对之策)前端需要加载vue,js,html,css这些都需要时间呀,是不是,别说还需要接口请求,数据库查询,js执行,这些都需要时间是不是,所以加载慢很正常,...
继续阅读 »

背景



  • 产品:你看看这页面加载的如此之慢,怎么会有用户用呢?(并甩给了我一个录屏)

  • : (抛出前端应对之策)前端需要加载vue,js,html,css这些都需要时间呀,是不是,别说还需要接口请求,数据库查询,js执行,这些都需要时间是不是,所以加载慢很正常,让用户用wifi嘛。(嗯。。。心安理得,就是这样。。)

  • 产品: 你上一家公司就是因为有你这样的优秀员工才倒闭的吧?!




这么说我就不服了,先看看视频:


我的影片我.gif
掐指一算,也就10s,还。。。。。。。。。。。。好吧,行吧,我编不下去。




前戏


欲练此功,必先自宫。额。。不对。欲解性能,必先分析。
市面上的体检套餐有很多种, 但其实都是换汤不换药. 那药 (标准) 是什么呢? 我们会在下面说明. 这里我选择了谷歌亲儿子 " 灯塔 "(LightHouse) 进行性能体检.


640.webp
从上面中我们可以看到灯塔是通过几种性能指标及不同权重来进行计分的. 这几种指标主要是根据 PerformanceTiming 和 PerformanceEntry API 标准进行定义. 市面上大多体检套餐也是基于这些指标定制的. 接下来我们来了解下这些指标的含义吧.


具体含义


FCP (First Contentful Paint)



First Contentful Paint (FCP) 指标衡量从页面开始加载到页面内容的任何部分在屏幕上呈现的时间。对于此指标,“内容”是指文本、图像(包括背景图像)、<svg> 元素或非白色 <canvas> 元素。



SI (Speed Index)



速度指数衡量页面加载期间内容的视觉显示速度。



LCP (Largest Contentful Paint)



LCP 测量视口中最大的内容元素何时呈现到屏幕上。这大约是页面的主要内容对用户可见的时间.



TTI (Time to Interactive)



TTI 衡量一个页面需要多长时间才能完全交互。在以下情况下,页面被认为是完全交互的:




  • 页面显示有用的内容,这是由 First Contentful Paint 衡量的,

  • 为大多数可见的页面元素注册了事件处理程序

  • 并且该页面会在 50 毫秒内响应用户交互。


TBT (Total Blocking Time)



FCP 到 TTI 之间, 主线程被 long task(超过 50ms) 阻塞的时间之和



TBT 衡量页面被阻止响应用户输入(例如鼠标点击、屏幕点击或键盘按下)的总时间。总和是通过将所有长任务的阻塞部分相加来计算的,即首次内容绘制和交互时间。任何执行时间超过 50 毫秒的任务都是长任务。 50 毫秒后的时间量是阻塞部分。例如,如果 Lighthouse 检测到 70 毫秒长的任务,则阻塞部分将为 20 毫秒。


CLS (Cumulative Layout Shift)



累计布局偏移值



FID (First Input Delay)



衡量您的用户可能遇到的最坏情况的首次输入延迟。首次输入延迟测量从用户第一次与您的网站交互(例如单击按钮)到浏览器实际能够响应该交互的时间。



体检结果


WechatIMG55139.png


哈哈哈,不愧是优秀的前端工程师。。。6项性能指标挂了5个。




手术方案


优化建议


1629886726026_C607FFC4-676D-4245-86DE-385AE0087581.png
那好,我们一个一个的逐个攻破。


减少初始服务器响应时间


下面是我和后端友好的对话:



  • : 你这首页接口2.39s,你是闭着眼睛写的接口吗?

  • 后端大佬: xxx哔哔哔哔哔哔xxxx,想死吗?!******xxxxx哔哔哔哔哔哔哔哔哔哔

  • : 我也觉得是前端的问题,嗯,打扰了。。。


行,下一个优化点。


减少未使用的 JavaScript


经过分析,我发现首页仅涉及到资源请求,并不需要请求库(我们内部封装)的加载,同时依赖的第三方的库也不需要长时间的版本更新,所以并不需要单独打包到chunk-vendors中。
查看基于 webpack-bundle-analyzer 生成的体积分析报告我发现有两个可优化的大产物:



内部封装的请求库需要md5和sha256加密请求,导致包打包出来多了600kb,于是在和领导商议之后决定用axios重写封装。




vue,vuex,vue-router,clipboard,vue-i18n,axios等三方的库上传cdn,首页预加载。



经过优化, bundle 体积 (gizp 前) 由原来的 841kb 减小至 278kb.


WechatIMG55140.png


避免向现代浏览器提供旧版 JavaScript


WechatIMG55141.png
没有想到太好的代替方案,暂时搁置。


视觉稳定性


优化未设置尺寸的图片元素



改善建议里提到了一项优先级很高的优化就是为图片元素设置显式的宽度和高度, 从而减少布局偏移和改善 CLS.



<img src="hello.png" width="640" height="320" alt="Hello World" />


避免页面布局发生偏移



我们产品中header是可配置的, 这个header会导致网站整体布局下移. 从而造成了较大的布局偏移. 跟产品 'qs'交易后, 讲页面拉长,header脱离文本流固定定位在上方。



最大的内容元素绘制


替换最大内容绘制元素



在改善建议中, 我发现首页的最大内容绘制元素是一段文本, 这也难怪 LCP 指标的数据表现不理想了, 原因: 链路过长 - 首页加载js -> 加载语言包 -> 显示文本内用.




于是, 我决定对最大内容绘制元素进行修改, 从而提升 LCP 时间. 我喵了一眼 Largest Contentful Paint API 关于该元素类型的定义, 将 "目标" 锁定到了一个 loading 元素 (绘制成本低: 默认渲染, 不依赖任何条件和判断). 经过我对该元素的尺寸动了手脚后 (变大), 该元素成功 "上位".



其他


除了针对上面几个指标维度进行优化外, 我还做了几点优化, 这里简单提一下:



  • 优化 DOM 嵌套层级及数量

  • 减少不必要的接口请求

  • 使用 translate 替换 top 做位移 / 动画


优化结果


WechatIMG55142.png


哎,优秀呀,还是优秀的前端工程师呀~~~~~hahahhahaha


链接:https://juejin.cn/post/7000330596043997198

收起阅读 »

这里是一个让你为所欲为,欲罢不能的抽奖demo

寒暄 抽奖系统有很多,各式各样的,不知道大伙都抽中过什么,还是像我这样经历了绝望,看破红尘,存起来留给下一代。 这种抽奖场景在活动中很常见,为了更好的摸鱼,决定自己去写一个插件来解决重复劳动。接下来为大伙介绍一个不错的宫格抽奖组件,请看官往下挪步 关于gri...
继续阅读 »

寒暄


抽奖系统有很多,各式各样的,不知道大伙都抽中过什么,还是像我这样经历了绝望,看破红尘,存起来留给下一代。


image.png


这种抽奖场景在活动中很常见,为了更好的摸鱼,决定自己去写一个插件来解决重复劳动。接下来为大伙介绍一个不错的宫格抽奖组件,请看官往下挪步


关于grid-roll


grid-roll是一个vue的宫格组件,它让ui和逻辑分离,封装了逻辑和宫格布局,让开发者只关注奖品和按钮的ui部分。



  • 自定义宫格数量,经典的3x3还是10x100都不在话下

  • 多抽功能,一次点击多次抽奖,谷底梭哈,就问你刺不刺激


安装


npm i grid-roll -S
yarn add grid-roll

引入


/** 引入 */
import { gridRoll, gridStart, gridPrize } from 'grid-roll'
import 'grid-roll/dist/grid-roll.min.css'

实践


通过vuecli搭起新项目,这边我们可以直接用掘金抽奖的图片链接,拿过来吧你。


图片上的奖品我都打上了数字记号,这些记号其实就奖品数组的下标,它们对应着奖品位置,布局从左到右一行一行排列,所以我们的奖品数组元素排序要注意下


image.png


通过使用grid-roll,我们只需要定义里面8个奖品和1个按钮的样式就行,用gridStart和gridPrize去包装这些物料,塞进gridRoll里面,gridRoll会帮我们自动调整成九宫格布局。这里,我更喜欢把奖品写成数据去循环生成gridPrize。然后样式布局基本是打开开发者工具复制掘金的样式,所以就不细说了


image.png


介绍下这3个组件:



  • gridRoll:interval这个属性用来定义宫格之前的间隔,默认是没有间隔的,这里我看感觉定义了6px。并且接受两个插槽button和prize

  • gridStart:专门用来做button插槽的组件

  • gridPrize:专门用来做prize插槽的组件









// 这里引入组件和样式
import { gridRoll, gridStart, gridPrize } from "grid-roll";
import "grid-roll/dist/grid-roll.min.css";
expoet default {
data () {
return {
prizes: [
{
id: 1,
text: "66矿石",
img: "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32ed6a7619934144882d841761b63d3c~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 2,
text: "随机限量徽章",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/71c68de6368548bd9bd6c8888542f911~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 3,
text: "掘金新款T恤",
img: "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5bf91038a6384fc3927dee294a38006b~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 4,
text: "Bug",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a4ce25d48b8405cbf5444b6195928d4~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 5,
text: "再抽2次解锁",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aabe49b0d5c741fa8d92ff94cd17cb90~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 6,
text: "掘金限量桌垫",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c78f363f41a741ffa11dcc8a92b72407~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 7,
text: "Yoyo抱枕",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33f4d465a6a9462f9b1b19b3104c8f91~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 8,
text: "再抽3次解锁",
img: "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4decbd721b2b48098a1ecf879cfca677~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
],
}
}
components: {
gridRoll,
gridStart,
gridPrize,
},
}

从上面可以看到,我们只需要通过gridStart和gridPrize定义好按钮和奖品的样式,放进gridRoll就行,不用再去管其他乱七八糟的操作。


disabled的使用


从官方的图看起来,这边还缺少一个“锁”样式,需要通过抽奖次数进行解锁,除了奖品样式的不同,在滚动的时候还会直接跳过未解锁的奖品。这边gridPrize也有一个对应的prop做这件事。


首先在prizes需要用到“锁”的元素中添加一个字段disabled: true,传给gridPrize,当抽奖开始的时候,滚动会直接跳过disabled为true的奖品,其次我们用disabled来做一些样式区分,这里样式也是照抄掘金




image.png


这里我们基本就完成静态样式啦,接下来就是说说怎么触发这个抽奖


抽奖


抽奖的行为是由gridPrize的startRoll函数提供的,这里通过ref获取gridRoll的实例,定义一个handleLottery方法用来触发startRoll函数。再把handleLottery绑定的抽奖按钮上







methods: {
async handleLottery() {
const value = 1;
/**
* 这里的value为1是指抽取id为1的奖品
* 返回一个Promise实例,内部为了防止多次触发抽奖逻辑,
* resolve会传递一个Boolean,进行是false,抽奖结束返回true
*/

const b = await this.$refs.dial.startRoll(value);
if (b) {
alert(
`🎉你抽到${this.prizes.find((prize) => prize.id === value).text}`
);
} else {
console.warn("稍安勿躁");
}
},
},

同时别忘记了,抽奖滚动的时候,有一个选中的样式,这里gridPrize作用域插槽提供了一个isSelect值用来判断是否滚动到当前奖品,用来做一些样式切换





收起阅读 »

vue、react函数式编程

函数式编程 JavaScript 语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在 JavaScript 语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript 是有史以...
继续阅读 »

函数式编程


JavaScript 语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在 JavaScript 语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript 是有史以来第一种被大规模采用的函数式编程语言。


ES6 的种种新增功能,使得函数式编程变得更方便、更强大。本章介绍 ES6 如何进行函数式编程。


柯里化


柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。


function add (a, b) {
return a + b;
}

add(1, 1) // 2

上面代码中,函数add接受两个参数ab


柯里化就是将上面的函数拆分成两个函数,每个函数都只接受一个参数。


function add (a) {
return function (b) {
return a + b;
}
}
// 或者采用箭头函数写法
const add = x => y => x + y;

const f = add(1);
f(1) // 2

上面代码中,函数add只接受一个参数a,返回一个函数f。函数f也只接受一个参数b


函数合成


函数合成(function composition)指的是,将多个函数合成一个函数。


const compose = f => g => x => f(g(x));

const f = compose (x => x * 4) (x => x + 3);
f(2) // 20

上面代码中,compose就是一个函数合成器,用于将两个函数合成一个函数。


可以发现,柯里化与函数合成有着密切的联系。前者用于将一个函数拆成多个函数,后者用于将多个函数合并成一个函数。


参数倒置


参数倒置(flip)指的是改变函数前两个参数的顺序。


var divide = (a, b) => a / b;
var flip = f.flip(divide);

flip(10, 5) // 0.5
flip(1, 10) // 10

var three = (a, b, c) => [a, b, c];
var flip = f.flip(three);
flip(1, 2, 3); // => [2, 1, 3]

上面代码中,如果按照正常的参数顺序,10 除以 5 等于 2。但是,参数倒置以后得到的新函数,结果就是 5 除以 10,结果得到 0.5。如果原函数有 3 个参数,则只颠倒前两个参数的位置。


参数倒置的代码非常简单。


let f = {};
f.flip =
fn =>
(a, b, ...args) => fn(b, a, ...args.reverse());

执行边界


执行边界(until)指的是函数执行到满足条件为止。


let condition = x => x > 100;
let inc = x => x + 1;
let until = f.until(condition, inc);

until(0) // 101

condition = x => x === 5;
until = f.until(condition, inc);

until(3) // 5

上面代码中,第一段的条件是执行到x大于 100 为止,所以x初值为 0 时,会一直执行到 101。第二段的条件是执行到等于 5 为止,所以x最后的值是 5。


执行边界的实现如下。


let f = {};
f.until = (condition, f) =>
(...args) => {
var r = f.apply(null, args);
return condition(r) ? r : f.until(condition, f)(r);
};

上面代码的关键就是,如果满足条件就返回结果,否则不断递归执行。


队列操作


队列(list)操作包括以下几种。



  • head: 取出队列的第一个非空成员。

  • last: 取出有限队列的最后一个非空成员。

  • tail: 取出除了“队列头”以外的其他非空成员。

  • init: 取出除了“队列尾”以外的其他非空成员。


下面是例子。


f.head(5, 27, 3, 1) // 5
f.last(5, 27, 3, 1) // 1
f.tail(5, 27, 3, 1) // [27, 3, 1]
f.init(5, 27, 3, 1) // [5, 27, 3]

这些方法的实现如下。


let f = {};
f.head = (...xs) => xs[0];
f.last = (...xs) => xs.slice(-1);
f.tail = (...xs) => Array.prototype.slice.call(xs, 1);
f.init = (...xs) => xs.slice(0, -1);

合并操作


合并操作分为concatconcatMap两种。前者就是将多个数组合成一个,后者则是先处理一下参数,然后再将处理结果合成一个数组。


f.concat([5], [27], [3]) // [5, 27, 3]
f.concatMap(x => 'hi ' + x, 1, [[2]], 3) // ['hi 1', 'hi 2', 'hi 3']

这两种方法的实现代码如下。


let f = {};
f.concat =
(...xs) => xs.reduce((a, b) => a.concat(b));
f.concatMap =
(f, ...xs) => f.concat(xs.map(f));

配对操作


配对操作分为zipzipWith两种方法。zip操作将两个队列的成员,一一配对,合成一个新的队列。如果两个队列不等长,较长的那个队列多出来的成员,会被忽略。zipWith操作的第一个参数是一个函数,然后会将后面的队列成员一一配对,输入该函数,返回值就组成一个新的队列。


下面是例子。


let a = [0, 1, 2];
let b = [3, 4, 5];
let c = [6, 7, 8];

f.zip(a, b) // [[0, 3], [1, 4], [2, 5]]
f.zipWith((a, b) => a + b, a, b, c) // [9, 12, 15]

上面代码中,zipWith方法的第一个参数是一个求和函数,它将后面三个队列的成员,一一配对进行相加。


这两个方法的实现如下。


let f = {};

f.zip = (...xs) => {
let r = [];
let nple = [];
let length = Math.min.apply(null, xs.map(x => x.length));

for (var i = 0; i < length; i++) {
xs.forEach(
x => nple.push(x[i])
);

r.push(nple);
nple = [];
}

return r;
};

f.zipWith = (op, ...xs) =>
f.zip.apply(null, xs).map(
(x) => x.reduce(op)
);


链接:https://juejin.cn/post/7000530780057239565

收起阅读 »

深入理解 Class 和 extends 原理

准备工作 在开始之前,我们需要一个 babel 的环境,方便查看 babel 后的代码,这里我推荐两种方式。chrome 插件 —— ScratchJS,可以设置 babel 来转换代码,通过点击 Toggle output 就能看到 babel 后的代码。b...
继续阅读 »

准备工作


在开始之前,我们需要一个 babel 的环境,方便查看 babel 后的代码,这里我推荐两种方式。

chrome 插件 —— ScratchJS,可以设置 babel 来转换代码,通过点击 Toggle output 就能看到 babel 后的代码。babel 官网推荐的在线编译工具 试一试,可以实时看到转换前后的代码。


本文将以 ScratchJS 转换后的代码为例进行代码分析。


1. class 实现


先从最简单的 class 开始看,下面这段代码涵盖了使用 class 时所有会出现的情况(静态属性、构造函数、箭头函数)。


class Person {
static instance = null;
static getInstance() {
return super.instance;
}
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log('hi');
}
sayHello = () => {
console.log('hello');
}
sayBye = function() {
console.log('bye');
}
}

而经过 babel 处理后的代码是这样的:


'use strict';

var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}();

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

var Person = function () {
function Person(name, age) {
_classCallCheck(this, Person);

this.sayHello = function () {
console.log('hello');
};

this.sayBye = function () {
console.log('bye');
};

this.name = name;
this.age = age;
}

_createClass(Person, [{
key: 'sayHi',
value: function sayHi() {
console.log('hi');
}
}]);

return Person;
}();

Person.instance = null;

最外层的 Person 变量被赋值给了一个立即执行函数,立即执行函数里面返回的是里面的 Person 构造函数,实际上最外层的 Person 就是里面的 Person 构造函数。


在 Person 类上用 static 设置的静态属性instance,在这里也被直接挂载到了 Person 构造函数上。


1.1 挂载属性方法


Person 类上各个属性的关系是这样的:


image_1dmjbel2cfvdls41h2e1hcmpn39.png-30.9kB


你是不是很好奇,为什么在 Person 类上面设置的 sayHisayHellosayBye 三个方法,编译后被放到了不同的地方处理?


从编译后的代码中可以看到 sayHellosayBye 被放到了 Person 构造函数中定义,而 sayHi_createClass 来处理(_createClasssayHi 添加到了 Person 的原型上面)。


曾经我也以为是 sayHello 使用了箭头函数的缘故才让它最终被绑定到了构造函数里面,后来我看到 sayBye 这种用法才知道这和箭头函数无关。


实际上 class 中定义属性还有一种写法,这种写法和 sayBye 如出一辙,在 babel 编译后会将其属性放到构造函数中,而非原型上面。


class Person {
name = 'tom';
age = 23;
}
// 等价于
class Person {
constructor() {
this.name = 'tom';
this.age = 23;
}
}

如果我们将 name 后面的 'tom' 换成函数呢?甚至箭头函数呢?这不就是 sayByesayHello 了吗?


因此,在 class 中不直接使用 = 来定义的方法,最终都会被挂载到原型上,使用 = 定义的属性和方法,最终都会被放到构造函数中。


1.2 _classCallCheck


Person 构造函数中调用了 _classCallCheck 函数,并将 this 和自身传入进去。
_classCallCheck 中通过 instanceof 来进行判断,instance 是否在 Constructor 的原型链上面,如果不在上面则抛出错误。这一步主要是为了避免直接将 Person 类当做函数来调用。
因此,在ES5中构造函数是可以当做普通函数来调用的,但在ES6中的类是无法直接当普通函数来调用的。



注意:为什么通过 instanceof 可以判断是否将 Person 类当函数来调用呢?
因为如果使用 new 操作符实例化 Person 的时候,那么 instance 就是当前的实例,指向 Person.prototypeinstance instanceof Constructor 必然为true。反之,直接调用 Person 构造函数,那么 instance 就不会指向 Person.prototype



1.3 _createClass


我们再来看 _createClass 函数,这个函数在 Person 原型上面添加了 sayHi 方法。


// 创建原型方法
_createClass(Person, [{
key: 'sayHi',
value: function sayHi() {
console.log('hi');
}
}]);

// _createClass也是一个立即执行函数
var _createClass = function () {
// 将props属性挂载到目标target上面
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
// 通过defineProperty来挂载属性
Object.defineProperty(target, descriptor.key, descriptor);
}
}
// 这个才是“真正的”_createClass
return function (Constructor, protoProps, staticProps) {
// 如果传入了需要挂载的原型方法
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
// 如果传入了需要挂载的静态方法
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}();

_createClass 函数接收三个参数,分别是 Constructor (构造函数)、protoProps(需要挂载到原型上的方法)、staticProps(需要挂载到类上的静态方法)。
在接收到参数之后,_createClass 会进行判断如果有 staticProps,则挂载到 Constructor 构造函数上;如果有 protoProps ,那么挂载到 Constructor 原型上面。
这里的挂载函数 defineProperties 是关键,它对传入的 props 进行了遍历,并设置了其 enumerable(是否可枚举) 和 configurable(是否可配置)、writable(是否可修改)等数据属性。
最后使用了 Object.defineProperty 函数来给设置当前对象的属性描述符。


2. extends 实现


通过上文对 Person 的分析,相信你已经知道了 ES6 中类的实现,这与ES5中的实现大同小异,接下来我们来具体看一下 extends 的实现。
以下面的 ES6 代码为例:


class Child extends Parent {
constructor(name, age) {
super(name, age);
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
}

class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}

babel后的代码则是这样的:


"use strict";

// 省略 _createClass
// 省略 _classCallCheck

function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call
&& (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

var Child = function (_Parent) {
_inherits(Child, _Parent);

function Child(name, age) {
_classCallCheck(this, Child);

var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));

_this.name = name;
_this.age = age;
return _this;
}

_createClass(Child, [{
key: "getName",
value: function getName() {
return this.name;
}
}]);

return Child;
}(Parent);

// 省略 Parent(类似上面的 Person 代码)

我们可以清楚地看到,继承是通过_inherits实现的。
为了方便理解,我这里整理了一下原型链的关系:


image_1dmec296p60q11bp1f8c1rid1rc52a.png-43.1kB


除去一些无关紧要的代码,最终的核心实现代码就只有这么多:


var Child = function (_Parent) {

_inherits(Child, _Parent);

function Child(name, age) {

var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));

_this.name = name;
_this.age = age;
return _this;
}

return Child;
}(Parent);

和前面的 Person 类实现有所不同的地方是,在 Child 方法中增加调用了 _inherits,还有在设置 nameage 属性的时候,使用的是执行 _possibleConstructorReturn 后返回的 _this,而非自身的 this,我们就重点分析这两步。


2.1 _inherits


先来看_inherits函数的实现代码:


function _inherits(subClass, superClass) { 
// 如果有一个不是函数,则抛出报错
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
// 将 subClass.prototype 设置为 superClass.prototype 的实例
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
// 将 subClass 设置为 superClass 的实例(优先使用 Object.setPrototypeOf)
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

_inherits 函数接收两个参数,分别是 subClass (子构造函数)和 subClass (父构造函数),将这个函数做的事情稍微做一下梳理。



  1. 设置 subClass.prototype[[Prototype]]指向 superClass.prototype[[Prototype]]

  2. 设置 subClass[[Prototype]] 指向 superClass


在《深入理解类和继承》一文中,曾经提到过 ES5 中的寄生组合式继承,extends 的实现与寄生组合式继承实则大同小异,仅仅只增加了第二步操作。


2.2 _possibleConstructorReturn


Child 中调用了 _possibleConstructorReturn 函数,将 thisObject.getPrototypeOf(Child).call(this, name, age)) 传了进去。
这个 this 我们很容易理解,就是构造函数的 this,但后面这么长的一串又是什么意思呢?
刚刚在 _inherits 中设置了 Child[[Prototype]] 指向了 Parent,因此可以将后面这串代码简化为 Parent.call(this, name, age)
这样你是不是就很熟悉了?这不就是组合继承中的执行一遍父构造函数吗?
那么 Parent.call(this, name, age) 执行后返回了什么呢?
正常情况下,应该会返回 undefined,但不排除 Parent 构造函数中直接返回一个对象或者函数的可能性。
*** 小课堂:**
在构造函数中,如果什么也没有返回或者返回了原始值,那么默认会返回当前的 this;而如果返回的是引用类型,那么最终实例化后的实例依然是这个引用类型(仅相当于对这个引用类型进行了扩展)。


const obj = {};
function Parent(name) {
this.name = name;
return obj;
}
const p = new Parent('tom');
obj.name; // 'tom'
p === obj; // true

如果没有 self,这里就会直接抛出错误,提示 super 函数还没有被调用。
最后会对 call 进行判断,如果 call 为引用类型,那么返回 call,否则返回 self



注意:call 就是 Parent.call(this, name, age) 执行后返回的结果。



function _possibleConstructorReturn(self, call) { 
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call
&& (typeof call === "object" || typeof call === "function") ? call : self;
}

Child 方法中,最终拿到 _possibleConstructorReturn 执行后的结果作为新的 this 来设置构造函数里面的属性。



思考题:如果直接用 this,而不是 _this,会出现什么问题?



总结


ES6 中提供的 classextends 本质上只是语法糖,底层的实现原理依然是构造函数和寄生组合式继承。
所以对于一个合格的前端工程师来说,即使 ES6 已经到来,对于 ES5 中的这些基础原理我们依然需要好好掌握。


作者:sh22n
链接:https://juejin.cn/post/7001025002287923207

收起阅读 »

高级线程应用之栅栏、信号量、调度组以及source(五)

4.3 Dispatch Source 封装 Timer目标是封装一个类似NSTimer的工具。void dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start...
继续阅读 »

4.3 Dispatch Source 封装 Timer

目标是封装一个类似NSTimer的工具。

void
dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start,
uint64_t interval,
uint64_t leeway);

  • source
    :事件源。
  • start:控制计时器第一次触发的时刻。
    • 参数类型是 dispatch_time_topaque类型),不能直接操作它。需要 dispatch_time 和 dispatch_walltime 函数来创建。
    • 常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 很常用。
    • 当使用dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时。
  • interval:回调间隔时间。
  • leeway:计时器触发的精准程度,就算指定为0系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。

首先实现一个最简单的封装:

- (instancetype)initTimerWithTimeInterval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue leeway:(NSTimeInterval)leeway repeats:(BOOL)repeats handler:(dispatch_block_t)handler {    
if (self == [super init]) {
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, leeway * NSEC_PER_SEC);
//解决与handler互相持有
__weak typeof(self) weakSelf = self;

//事件回调,这个函数在执行完之后 block 会立马执行一遍。后面隔一定时间间隔再执行一次。
dispatch_source_set_event_handler(self.timer, ^{
if (handler) {
handler();
}
if (!repeats) {
//repeats 为 NO 执行一次后取消
[weakSelf cancel];
}
});
}
return self;
}

这样就满足了最基本的要求,由于handler的调用在设置和恢复后会立马调用,所以需要过滤需改handler实现如下:
//忽略 handler 设置完马上回调
if (weakSelf.isAutoFirstCallback) {
@synchronized(weakSelf) {
weakSelf.isAutoFirstCallback = NO;
}
return;
}
//忽略挂起恢复后的立马回调
if (!weakSelf.resumeCallbackEnable && weakSelf.isResumeCallback) {
@synchronized(weakSelf) {
weakSelf.isResumeCallback = NO;
}
return;
}

if (handler) {
handler();
}

if (!repeats) {
//repeats 为 NO 执行一次后取消
[weakSelf cancel];
}

为了更灵活对注册以及取消source逻辑也进行暴露:

dispatch_source_set_registration_handler(self.timer, ^{
if (weakSelf.startBlock) {
weakSelf.startBlock();
}
});
//取消回调
dispatch_source_set_cancel_handler(self.timer, ^{
if (weakSelf.cancelBlock) {
weakSelf.cancelBlock();
}
});
由于source本身提供了挂起和恢复的功能,同样对其封装。并且需要进行释放操作,所以提供cancel功能:

- (void)start {
//为了与isResumeCallback区分开
@synchronized(self) {
if (!self.isStarted && self.timerStatus == HPTimerSuspend) {
self.isStarted = YES;
self.timerStatus = HPTimerResume;
dispatch_resume(self.timer);
}
}
}

- (void)suspend {
//挂起,挂起的时候不能设置timer为nil
@synchronized(self) {
if (self.timerStatus == HPTimerResume) {
self.timerStatus = HPTimerSuspend;
dispatch_suspend(self.timer);
}
}
}

- (void)resume {
//恢复
@synchronized(self) {
if (self.timerStatus == HPTimerSuspend) {
self.isResumeCallback = YES;
self.timerStatus = HPTimerResume;
dispatch_resume(self.timer);
}
}
}

- (void)cancel {
//取消
@synchronized(self) {
if (self.timerStatus != HPTimerCanceled) {
//先恢复再取消
if (self.timerStatus == HPTimerSuspend) {
[self resume];
}
self.timerStatus = HPTimerCanceled;
dispatch_source_cancel(self.timer);
_timer = nil;
}
}
}

- (void)dealloc {
[self cancel];
}

  • dealloc中主动进行cancel调用方可以不必在自己的dealloc中调用。

这样再暴露一些简单接口就可以直接调用了(调用方需要持有timer):

self.timer = [HPTimer scheduledTimerWithTimeInterval:3 handler:^{
NSLog(@"timer 回调");
}];

五、延迟函数(dispatch_after)

void
dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t work)
{
_dispatch_after(when, queue, NULL, work, true);
}

直接调用_dispatch_after


static inline void
_dispatch_after(dispatch_time_t when, dispatch_queue_t dq,
void *ctxt, void *handler, bool block)
{
dispatch_timer_source_refs_t dt;
dispatch_source_t ds;
uint64_t leeway, delta;
//FOREVER 直接返回什么也不做
if (when == DISPATCH_TIME_FOREVER) {
#if DISPATCH_DEBUG
DISPATCH_CLIENT_CRASH(0, "dispatch_after called with 'when' == infinity");
#endif
return;
}

delta = _dispatch_timeout(when);
if (delta == 0) {
if (block) {
//时间为0直接执行handler
return dispatch_async(dq, handler);
}
return dispatch_async_f(dq, ctxt, handler);
}
//精度 = 间隔 / 10
leeway = delta / 10; // <rdar://problem/13447496>
//<1 毫秒 的时候设置最小值为1毫秒
if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
//大于60s的时候设置为60s,也就是 1ms <= leeway <= 1min
if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;

// this function can and should be optimized to not use a dispatch source
//创建 type 为 after 的 source
ds = dispatch_source_create(&_dispatch_source_type_after, 0, 0, dq);
dt = ds->ds_timer_refs;

dispatch_continuation_t dc = _dispatch_continuation_alloc();
if (block) {
//包装handler
_dispatch_continuation_init(dc, dq, handler, 0, 0);
} else {
_dispatch_continuation_init_f(dc, dq, ctxt, handler, 0, 0);
}
// reference `ds` so that it doesn't show up as a leak
dc->dc_data = ds;
_dispatch_trace_item_push(dq, dc);
//存储handler
os_atomic_store2o(dt, ds_handler[DS_EVENT_HANDLER], dc, relaxed);
dispatch_clock_t clock;
uint64_t target;
_dispatch_time_to_clock_and_value(when, false, &clock, &target);
if (clock != DISPATCH_CLOCK_WALL) {
leeway = _dispatch_time_nano2mach(leeway);
}
dt->du_timer_flags |= _dispatch_timer_flags_from_clock(clock);
dt->dt_timer.target = target;
dt->dt_timer.interval = UINT64_MAX;
dt->dt_timer.deadline = target + leeway;
dispatch_activate(ds);
}
  • 延时时间设置为DISPATCH_TIME_FOREVER直接返回什么也不做。
  • 延时时间为0直接调用dispatch_async执行handler
  • 精度:1ms <= leeway <= 1min要在这个范围,否则会修正。
  • 创建_dispatch_source_type_after类型的source
  • 包装存储handler
  • 调用_dispatch_time_to_clock_and_value进行target设置。

本质上 dispatch_after 也是对 source的封装。

时间单位

#define NSEC_PER_SEC 1000000000ull      1秒 = 10亿纳秒              
#define NSEC_PER_MSEC 1000000ull 1毫秒 = 100万纳秒
#define USEC_PER_SEC 1000000ull 1秒 = 100万微秒
#define NSEC_PER_USEC 1000ull 1微秒 = 1000 纳秒

1s = 1000ms = 100万us = 10亿ns
1ms = 1000us
1us = 1000ns



作者:HotPotCat
链接:https://www.jianshu.com/p/84153e072f44
收起阅读 »

高级线程应用之栅栏、信号量、调度组以及source(四)

四、Dispatch Source在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 dispatch source 事先定义好的句柄(可以把句柄简单理解为一个 b...
继续阅读 »

四、Dispatch Source

在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 dispatch source 事先定义好的句柄(可以把句柄简单理解为一个 block ) 这个过程叫 用户事件(Custom event)。是 dispatch source 支持处理的一种事件。

句柄是一种指向指针的指针,它指向的就是一个类或者结构,它和系统有很密切的关系。比如:实例句柄(HINSTANCE),位图句柄(HBITMAP),设备表述句柄(HDC),图标句柄(HICON)等。这当中还有一个通用的句柄,就是HANDLE

Dispatch Source有两点:

  • CPU 负荷非常小,尽量不占用资源 。
  • 联结的优势。
  • dispatch source不受runloop的影响,底层封装的是pthread

相关API

  • dispatch_source_create 创建源
  • dispatch_source_set_event_handler 设置源事件回调
  • dispatch_source_merge_data 源事件设置数据
  • dispatch_source_get_data 获取源事件数据
  • dispatch_resume 继续
  • dispatch_suspend 挂起

4.1 应用

dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
uintptr_t mask,
dispatch_queue_t _Nullable queue);
  • typedispatch 源可处理的事件。比如:DISPATCH_SOURCE_TYPE_TIMERDISPATCH_SOURCE_TYPE_DATA_ADD
    • DISPATCH_SOURCE_TYPE_DATA_ADD: 将所有触发结果相加,最后统一执行响应。间隔的时间越长,则每次触发都会响应;如果间隔的时间很短,则会将触发后的结果相加后统一触发。也就是利用CPU空闲时间进行回调。
  • handle:可以理解为句柄、索引或id,如果要监听进程,需要传入进程的ID
  • mask:可以理解为描述,具体要监听什么。
  • queue:处理handle的队列。

有如下一个进度条的案例:

self.completed = 0;
self.queue = dispatch_queue_create("HotpotCat", NULL);
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
//设置句柄
dispatch_source_set_event_handler(self.source, ^{
NSLog(@"%@",[NSThread currentThread]);
NSUInteger value = dispatch_source_get_data(self.source);
self.completed += value;
double progress = self.completed / 100.0;
NSLog(@"progress: %.2f",progress);
self.progressView.progress = progress;
});
self.isRunning = YES;
//创建后默认是挂起状态
dispatch_resume(self.source);

创建了一个ADD类型的source,在handle获取进度增量并更新进度条。由于创建后source处于挂起状态,需要先恢复。

可以在按钮的点击事件中进行任务的挂起和恢复:

if (self.isRunning) {
dispatch_suspend(self.source);
dispatch_suspend(self.queue);
NSLog(@"pause");
self.isRunning = NO;
[sender setTitle:@"pause" forState:UIControlStateNormal];
} else {
dispatch_resume(self.source);
dispatch_resume(self.queue);
NSLog(@"running");
self.isRunning = YES;
[sender setTitle:@"running" forState:UIControlStateNormal];
}

任务的执行是一个简单的循环:

for (NSInteger i = 0; i < 100; i++) {
dispatch_async(self.queue, ^{
NSLog(@"merge");
//加不加 sleep 影响 handler 的执行次数。
sleep(1);
dispatch_source_merge_data(self.source, 1);//+1
});
}
  • 在循环中调用dispatch_source_merge_data触发回调。当queue挂起后后续任务就不再执行了。
  • 在不加sleep的情况下handler的回调是小于100次的,任务会被合并。

4.2 源码解析

4.2.1 dispatch_source_create

dispatch_source_t
dispatch_source_create(dispatch_source_type_t dst, uintptr_t handle,
uintptr_t mask, dispatch_queue_t dq)
{
dispatch_source_refs_t dr;
dispatch_source_t ds;
//add对应 _dispatch_source_data_create timer对应 _dispatch_source_timer_create
dr = dux_create(dst, handle, mask)._dr;
if (unlikely(!dr)) {
return DISPATCH_BAD_INPUT;
}
//创建队列
ds = _dispatch_queue_alloc(source,
dux_type(dr)->dst_strict ? DSF_STRICT : DQF_MUTABLE, 1,
DISPATCH_QUEUE_INACTIVE | DISPATCH_QUEUE_ROLE_INNER)._ds;
ds->dq_label = "source";
ds->ds_refs = dr;
dr->du_owner_wref = _dispatch_ptr2wref(ds);

//没有传队列,获取root_queues
if (unlikely(!dq)) {
dq = _dispatch_get_default_queue(true);
} else {
_dispatch_retain((dispatch_queue_t _Nonnull)dq);
}
//目标队列为传进来的dq
ds->do_targetq = dq;
//是timer 并且设置了interval则调用dispatch_source_set_timer进行设置
//也就是说type为timer的时候即使不设置timer也会默认设置。这里时间间隔设置为了handle
if (dr->du_is_timer && (dr->du_timer_flags & DISPATCH_TIMER_INTERVAL)) {
dispatch_source_set_timer(ds, DISPATCH_TIME_NOW, handle, UINT64_MAX);
}
_dispatch_object_debug(ds, "%s", __func__);
//返回自己创建的source,source本身也是队列。
return ds;
}
  • 根据type创建对应的队列。add对应_dispatch_source_data_createtimer对应_dispatch_source_timer_create
  • 如果创建的时候没有传处理handle的队列,会默认获取root_queues中的队列。
  • 设置目标队列为传进来的队列。
  • 如果typeDISPATCH_SOURCE_TYPE_INTERVAL(应该是私有的)则主动调用一次dispatch_source_set_timer
  • 返回自己创建的sourcesource本身也是队列。

_dispatch_source_data_create

static dispatch_unote_t
_dispatch_source_data_create(dispatch_source_type_t dst, uintptr_t handle,
uintptr_t mask)
{
if (handle || mask) {
return DISPATCH_UNOTE_NULL;
}

// bypass _dispatch_unote_create() because this is always "direct"
// even when EV_UDATA_SPECIFIC is 0
dispatch_unote_class_t du = _dispatch_calloc(1u, dst->dst_size);
du->du_type = dst;
du->du_filter = dst->dst_filter;
du->du_is_direct = true;
return (dispatch_unote_t){ ._du = du };
}

直接调用_dispatch_calloc创建返回。

_dispatch_source_timer_create

static dispatch_unote_t
_dispatch_source_timer_create(dispatch_source_type_t dst,
uintptr_t handle, uintptr_t mask)
{
dispatch_timer_source_refs_t dt;
......
//创建
dt = _dispatch_calloc(1u, dst->dst_size);
dt->du_type = dst;
dt->du_filter = dst->dst_filter;
dt->du_is_timer = true;
dt->du_timer_flags |= (uint8_t)(mask | dst->dst_timer_flags);
dt->du_ident = _dispatch_timer_unote_idx(dt);
dt->dt_timer.target = UINT64_MAX;
dt->dt_timer.deadline = UINT64_MAX;
dt->dt_timer.interval = UINT64_MAX;
dt->dt_heap_entry[DTH_TARGET_ID] = DTH_INVALID_ID;
dt->dt_heap_entry[DTH_DEADLINE_ID] = DTH_INVALID_ID;
return (dispatch_unote_t){ ._dt = dt };
}

内部时间给的默认值是最大值。

4.2.2 dispatch_source_set_event_handler

void
dispatch_source_set_event_handler(dispatch_source_t ds,
dispatch_block_t handler)
{
_dispatch_source_set_handler(ds, handler, DS_EVENT_HANDLER, true);
}

调用_dispatch_source_set_handler传递的类型为DS_EVENT_HANDLER

DISPATCH_NOINLINE
static void
_dispatch_source_set_handler(dispatch_source_t ds, void *func,
uintptr_t kind, bool is_block)
{
dispatch_continuation_t dc;
//创建dc存储handler
dc = _dispatch_source_handler_alloc(ds, func, kind, is_block);
//挂起
if (_dispatch_lane_try_inactive_suspend(ds)) {
//替换
_dispatch_source_handler_replace(ds, kind, dc);
//恢复
return _dispatch_lane_resume(ds, DISPATCH_RESUME);
}
......
}
  • 创建_dispatch_source_handler_alloc存储handler,内部会进行标记非DS_EVENT_HANDLER会标记为DC_FLAG_CONSUME
  • _dispatch_lane_try_inactive_suspend挂起队列。
  • _dispatch_source_handler_replace替换handler

  • static inline void
    _dispatch_source_handler_replace(dispatch_source_t ds, uintptr_t kind,
    dispatch_continuation_t dc)
    {
    //handler目标回调为空释放handler
    if (!dc->dc_func) {
    _dispatch_continuation_free(dc);
    dc = NULL;
    } else if (dc->dc_flags & DC_FLAG_FETCH_CONTEXT) {
    dc->dc_ctxt = ds->do_ctxt;
    }
    //保存
    dc = os_atomic_xchg(&ds->ds_refs->ds_handler[kind], dc, release);
    if (dc) _dispatch_source_handler_dispose(dc);
    }
    _dispatch_lane_resume恢复队列,调用队列对应的awake

    • 先调用_dispatch_lane_resume_activate(这也就是set后立马调用的原因):
    static void
    _dispatch_lane_resume_activate(dispatch_lane_t dq)
    {
    if (dx_vtable(dq)->dq_activate) {
    dx_vtable(dq)->dq_activate(dq);
    }

    _dispatch_lane_resume(dq, DISPATCH_ACTIVATION_DONE);
    }

    再调用_dispatch_lane_resume

    4.2.3 dispatch_source_merge_data

    void
    dispatch_source_merge_data(dispatch_source_t ds, uintptr_t val)
    {
    dispatch_queue_flags_t dqf = _dispatch_queue_atomic_flags(ds);
    dispatch_source_refs_t dr = ds->ds_refs;

    if (unlikely(dqf & (DSF_CANCELED | DQF_RELEASED))) {
    return;
    }
    //根据类型存值
    switch (dr->du_filter) {
    case DISPATCH_EVFILT_CUSTOM_ADD:
    //有累加
    os_atomic_add2o(dr, ds_pending_data, val, relaxed);
    break;
    case DISPATCH_EVFILT_CUSTOM_OR:
    os_atomic_or2o(dr, ds_pending_data, val, relaxed);
    break;
    case DISPATCH_EVFILT_CUSTOM_REPLACE:
    os_atomic_store2o(dr, ds_pending_data, val, relaxed);
    break;
    default:
    DISPATCH_CLIENT_CRASH(dr->du_filter, "Invalid source type");
    }
    //唤醒执行回调
    dx_wakeup(ds, 0, DISPATCH_WAKEUP_MAKE_DIRTY);
    }
    • 根据类型对值进行处理,处理完之后唤醒队列执行。

    对于主线程会执行_dispatch_main_queue_wakeup,其中会取到dispatch_queue获取到dc,最后进行handler的调用。

    4.2.4 dispatch_source_get_data

    uintptr_t
    dispatch_source_get_data(dispatch_source_t ds)
    {
    dispatch_source_refs_t dr = ds->ds_refs;
    #if DISPATCH_USE_MEMORYSTATUS
    if (dr->du_vmpressure_override) {
    return NOTE_VM_PRESSURE;
    }
    #if TARGET_OS_SIMULATOR
    if (dr->du_memorypressure_override) {
    return NOTE_MEMORYSTATUS_PRESSURE_WARN;
    }
    #endif
    #endif // DISPATCH_USE_MEMORYSTATUS
    //获取数据
    uint64_t value = os_atomic_load2o(dr, ds_data, relaxed);
    return (unsigned long)(dr->du_has_extended_status ?
    DISPATCH_SOURCE_GET_DATA(value) : value);
    }

    merge_data相反,一个存一个取。

    4.2.5 dispatch_resume

    void
    dispatch_resume(dispatch_object_t dou)
    {
    DISPATCH_OBJECT_TFB(_dispatch_objc_resume, dou);
    if (unlikely(_dispatch_object_is_global(dou) ||
    _dispatch_object_is_root_or_base_queue(dou))) {
    return;
    }
    if (dx_cluster(dou._do) == _DISPATCH_QUEUE_CLUSTER) {
    _dispatch_lane_resume(dou._dl, DISPATCH_RESUME);
    }
    }

    经过调试走的是_dispatch_lane_resume逻辑,与_dispatch_source_set_handler中调用的一致。awake队列。

    4.2.6 dispatch_suspend

    void
    dispatch_suspend(dispatch_object_t dou)
    {
    DISPATCH_OBJECT_TFB(_dispatch_objc_suspend, dou);
    if (unlikely(_dispatch_object_is_global(dou) ||
    _dispatch_object_is_root_or_base_queue(dou))) {
    return;
    }
    if (dx_cluster(dou._do) == _DISPATCH_QUEUE_CLUSTER) {
    return _dispatch_lane_suspend(dou._dl);
    }
    }

    调用_dispatch_lane_suspend挂起队列。

    4.2.7 dispatch_source_cancel

    dispatch_source_cancel(dispatch_source_t ds)
    {
    _dispatch_object_debug(ds, "%s", __func__);

    _dispatch_retain_2(ds);

    if (_dispatch_queue_atomic_flags_set_orig(ds, DSF_CANCELED) & DSF_CANCELED){
    _dispatch_release_2_tailcall(ds);
    } else {
    //_dispatch_workloop_wakeup
    dx_wakeup(ds, 0, DISPATCH_WAKEUP_MAKE_DIRTY | DISPATCH_WAKEUP_CONSUME_2);
    }
    }



    调用_dispatch_workloop_wakeup

    • cancel内部会对状态进行判断,如果是挂起状态会报错。所以需要在运行状态下取消。
    • 调用_dispatch_release_2_tailcall进行释放操作。

    4.2.8 dispatch_source_set_timer

    void
    dispatch_source_set_timer(dispatch_source_t ds, dispatch_time_t start,
    uint64_t interval, uint64_t leeway)
    {
    dispatch_timer_source_refs_t dt = ds->ds_timer_refs;
    dispatch_timer_config_t dtc;

    if (unlikely(!dt->du_is_timer)) {
    DISPATCH_CLIENT_CRASH(ds, "Attempt to set timer on a non-timer source");
    }
    //根据type配置timer和interval
    if (dt->du_timer_flags & DISPATCH_TIMER_INTERVAL) {
    dtc = _dispatch_interval_config_create(start, interval, leeway, dt);
    } else {
    dtc = _dispatch_timer_config_create(start, interval, leeway, dt);
    }
    if (_dispatch_timer_flags_to_clock(dt->du_timer_flags) != dtc->dtc_clock &&
    dt->du_filter == DISPATCH_EVFILT_TIMER_WITH_CLOCK) {
    DISPATCH_CLIENT_CRASH(0, "Attempting to modify timer clock");
    }
    //跟踪配置
    _dispatch_source_timer_telemetry(ds, dtc->dtc_clock, &dtc->dtc_timer);
    dtc = os_atomic_xchg2o(dt, dt_pending_config, dtc, release);
    if (dtc) free(dtc);
    //唤醒
    dx_wakeup(ds, 0, DISPATCH_WAKEUP_MAKE_DIRTY);
    }

    4.2.9 dispatch_source_set_registration_handler

    void
    dispatch_source_set_registration_handler(dispatch_source_t ds,
    dispatch_block_t handler)
    {
    _dispatch_source_set_handler(ds, handler, DS_REGISTN_HANDLER, true);
    }

    也是直接调用的_dispatch_source_set_handler,参数是DS_REGISTN_HANDLER

    4.2.10 dispatch_source_set_cancel_handler

    void
    dispatch_source_set_cancel_handler(dispatch_source_t ds,
    dispatch_block_t handler)
    {
    _dispatch_source_set_handler(ds, handler, DS_CANCEL_HANDLER, true);
    }
    • 直接调用的_dispatch_source_set_handler,参数是DS_CANCEL_HANDLER
    • 会根据DS_REGISTN_HANDLER、DS_CANCEL_HANDLER、DS_EVENT_HANDLER进行handler的获取和释放,因为这三者可能同时存在。

    那么就有个问题设置timer类型后我们没有主动调用dispatch_source_merge_data,那么它是在什么时机调用的呢?在回调中bt:

        frame #2: 0x000000010b6a29c8 libdispatch.dylib`_dispatch_client_callout + 8
    frame #3: 0x000000010b6a5316 libdispatch.dylib`_dispatch_continuation_pop + 557
    frame #4: 0x000000010b6b8e8b libdispatch.dylib`_dispatch_source_invoke + 2205
    frame #5: 0x000000010b6b4508 libdispatch.dylib`_dispatch_root_queue_drain + 351
    frame #6: 0x000000010b6b4e6d libdispatch.dylib`_dispatch_worker_thread2 + 135
    frame #7: 0x00007fff611639f7 libsystem_pthread.dylib`_pthread_wqthread + 220
    frame #8: 0x00007fff61162b77 libsystem_pthread.dylib`start_wqthread + 15

    搜索_dispatch_source_invoke只找到了:

    DISPATCH_VTABLE_INSTANCE(source,
    .do_type = DISPATCH_SOURCE_KEVENT_TYPE,
    .do_dispose = _dispatch_source_dispose,
    .do_debug = _dispatch_source_debug,
    .do_invoke = _dispatch_source_invoke,

    .dq_activate = _dispatch_source_activate,
    .dq_wakeup = _dispatch_source_wakeup,
    .dq_push = _dispatch_lane_push,
    );
    也就是调用的sourcedo_invoke,调用逻辑为_dispatch_root_queue_drain -> _dispatch_continuation_pop_inline -> dx_invoke

    void
    _dispatch_source_invoke(dispatch_source_t ds, dispatch_invoke_context_t dic,
    dispatch_invoke_flags_t flags)
    {
    _dispatch_queue_class_invoke(ds, dic, flags,
    DISPATCH_INVOKE_DISALLOW_SYNC_WAITERS, _dispatch_source_invoke2);

    #if DISPATCH_EVENT_BACKEND_KEVENT
    if (flags & DISPATCH_INVOKE_WORKLOOP_DRAIN) {
    dispatch_workloop_t dwl = (dispatch_workloop_t)_dispatch_get_wlh();
    dispatch_timer_heap_t dth = dwl->dwl_timer_heap;
    if (dth && dth[0].dth_dirty_bits) {
    //调用
    _dispatch_event_loop_drain_timers(dwl->dwl_timer_heap,
    DISPATCH_TIMER_WLH_COUNT);
    }
    }
    #endif // DISPATCH_EVENT_BACKEND_KEVENT
    }




    收起阅读 »

    高级线程应用之栅栏、信号量、调度组以及source(三)

    二、信号量(dispatch_semaphore_t)相关函数:dispatch_semaphore_create:创建信号量dispatch_semaphore_wait:信号量等待dispatch_semaphore_signal:信号量释放信号量有两个效...
    继续阅读 »

    二、信号量(dispatch_semaphore_t

    相关函数:

    • dispatch_semaphore_create:创建信号量
    • dispatch_semaphore_wait:信号量等待
    • dispatch_semaphore_signal:信号量释放

    信号量有两个效果:同步作为锁 与 控制GCD最大并发数

    二元信号量是最简单的一种锁,只有两种状态:占用与非占用。适合只能被唯一一个线程独占访问资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号置为占用状态,此后其他的所有视图获取该二元信号量的线程将会等待,直到该锁被释放。

    对于允许多个线程并发访问的资源,多元信号量简称信号量,它是一个很好的选择。一个初始值为 N 的信号量允许 N 个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:

    • 将信号量的值减1
    • 如果信号量的值小于0,则进入等待状态,否则继续执行。

    访问完资源之后,线程释放信号量,进行如下操作:

    • 将信号量的值+1
    • 如果信号量的值< 1,唤醒一个等待中的线程。

    2.1 应用

        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    dispatch_queue_t queue1 = dispatch_queue_create("HotpotCat", NULL);

    dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"1 start");
    NSLog(@"1 end");
    dispatch_semaphore_signal(sem);
    });

    dispatch_async(queue1, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"2 start");
    NSLog(@"2 end");
    dispatch_semaphore_signal(sem);
    });

    dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"3 start");
    NSLog(@"3 end");
    dispatch_semaphore_signal(sem);
    });

    dispatch_async(queue1, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"4 start");
    NSLog(@"4 end");
    dispatch_semaphore_signal(sem);
    });

    对于上面的例子输出:

    1 start
    1 end
    2 start
    2 end
    3 start
    3 end
    4 start
    4 end
    这个时候信号量初始化的是1,全局队列与自定义串行队列中的任务按顺序依次执行。
    当将信号量改为2后输出:
    1 start
    2 start
    2 end
    1 end
    3 start
    4 start
    3 end
    4 end

    这个时候1、2先执行无序,3、4后执行无序。这样就控制了GCD任务的最大并发数。

    修改代码如下:

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);

    dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"1 start");
    NSLog(@"1 end");
    });

    dispatch_async(queue, ^{
    sleep(2);
    NSLog(@"2 start");
    NSLog(@"2 end");
    dispatch_semaphore_signal(sem);
    });

    信号量初始值修改为0,在任务1wait,在任务2signal,这个时候输出如下:

    2 start
    2 end
    1 start
    1 end

    任务2比任务1先执行了。由于信号量初始化为0wait函数后面任务就执行不了一直等待;等到signal执行后发送信号wait就可以执行了。这样就达到了控制流程。任务2中的信号控制了任务1的执行。

    2.2 源码分析

    2.2.1 dispatch_semaphore_create

    /*
    * @param dsema
    * The semaphore. The result of passing NULL in this parameter is undefined.
    */


    dispatch_semaphore_t
    dispatch_semaphore_create(intptr_t value)
    {
    dispatch_semaphore_t dsema;

    // If the internal value is negative, then the absolute of the value is
    // equal to the number of waiting threads. Therefore it is bogus to
    // initialize the semaphore with a negative value.
    if (value < 0) { //>=0 才有用,否则直接返回
    return DISPATCH_BAD_INPUT;// 0
    }

    dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore),
    sizeof(struct dispatch_semaphore_s));
    dsema->do_next = DISPATCH_OBJECT_LISTLESS;
    dsema->do_targetq = _dispatch_get_default_queue(false);
    dsema->dsema_value = value;
    _dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
    dsema->dsema_orig = value;
    return dsema;
    }
    • value < 0的时候无效,只有>= 0才有效,才会执行后续流程。

    2.2.2 dispatch_semaphore_wait

    intptr_t
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
    {
    //--
    long value = os_atomic_dec2o(dsema, dsema_value, acquire);
    if (likely(value >= 0)) { //>=0 返回
    return 0;
    }
    return _dispatch_semaphore_wait_slow(dsema, timeout);
    }
    • --value大于等于0直接返回0。执行dispatch_semaphore_wait后续的代码。
    • 否则执行_dispatch_semaphore_wait_slow(相当于do-while循环)。

    _dispatch_semaphore_wait_slow
    当信号量为0的时候调用wait后(< 0)就走_dispatch_semaphore_wait_slow逻辑了:

    DISPATCH_NOINLINE
    static intptr_t
    _dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema,
    dispatch_time_t timeout)
    {
    long orig;

    _dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
    //超时直接break
    switch (timeout) {
    default:
    if (!_dispatch_sema4_timedwait(&dsema->dsema_sema, timeout)) {
    break;
    }
    // Fall through and try to undo what the fast path did to
    // dsema->dsema_value
    //NOW的情况下进行超时处理
    case DISPATCH_TIME_NOW:
    orig = dsema->dsema_value;
    while (orig < 0) {
    if (os_atomic_cmpxchgv2o(dsema, dsema_value, orig, orig + 1,
    &orig, relaxed)) {
    return _DSEMA4_TIMEOUT();
    }
    }
    // Another thread called semaphore_signal().
    // Fall through and drain the wakeup.
    //FOREVER则进入wait逻辑。
    case DISPATCH_TIME_FOREVER:
    _dispatch_sema4_wait(&dsema->dsema_sema);
    break;
    }
    return 0;
    }
    • 当值为timeout的时候直接break
    • 当值为DISPATCH_TIME_NOW的时候循环调用_DSEMA4_TIMEOUT()
    #define _DSEMA4_TIMEOUT() KERN_OPERATION_TIMED_OUT
    • 当值为DISPATCH_TIME_FOREVER的时候调用_dispatch_sema4_wait

    _dispatch_sema4_wait

    //    void
    // _dispatch_sema4_wait(_dispatch_sema4_t *sema)
    // {
    // int ret = 0;
    // do {
    // ret = sem_wait(sema);
    // } while (ret == -1 && errno == EINTR);
    // DISPATCH_SEMAPHORE_VERIFY_RET(ret);
    // }

    void
    _dispatch_sema4_wait(_dispatch_sema4_t *sema)
    {
    kern_return_t kr;
    do {
    kr = semaphore_wait(*sema);
    } while (kr == KERN_ABORTED);
    DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }
  • semaphore_wait并没有搜到实现,这是pthread内核封装的实现。
  • _dispatch_sema4_wait本质上是一个do-while循环,相当于在这里直接卡住执行不到后面的逻辑了。相当于:


  • dispatch_async(queue, ^{
    // dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    do {
    //循环
    } while (signal <= 0);
    NSLog(@"1 start");
    NSLog(@"1 end");
    });

    结论:value >= 0的时候执行后续的代码,否则do-while循环卡住后续逻辑

    2.2.3 dispatch_semaphore_signal

    /*!
    * @function dispatch_semaphore_signal
    *
    * @abstract
    * Signal (increment) a semaphore.
    *
    * @discussion
    * Increment the counting semaphore. If the previous value was less than zero,
    * this function wakes a waiting thread before returning.
    *
    * @param dsema The counting semaphore.
    * The result of passing NULL in this parameter is undefined.
    *
    * @result
    * This function returns non-zero if a thread is woken. Otherwise, zero is
    * returned.
    */

    intptr_t
    dispatch_semaphore_signal(dispatch_semaphore_t dsema)
    {
    //++操作
    long value = os_atomic_inc2o(dsema, dsema_value, release);
    if (likely(value > 0)) {
    return 0;
    }
    //++ 后还 < 0,则表示做wait操作(--)过多。报错。
    if (unlikely(value == LONG_MIN)) {
    DISPATCH_CLIENT_CRASH(value,
    "Unbalanced call to dispatch_semaphore_signal()");
    }
    //发送信号量逻辑,恢复wait等待的操作。
    return _dispatch_semaphore_signal_slow(dsema);
    }
    • os_atomic_inc2o执行++后值大于0直接返回能够执行。
    • 只有<= 0的时候才执行后续流程,调用_dispatch_semaphore_signal_slow进行异常处理。
    • 注释说明了当值< 0的时候在return之前唤醒一个等待线程。

    _dispatch_semaphore_signal_slow

    intptr_t
    _dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema)
    {
    _dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
    _dispatch_sema4_signal(&dsema->dsema_sema, 1);
    return 1;
    }

    直接调用_dispatch_sema4_signal

    _dispatch_sema4_signal

    #define DISPATCH_SEMAPHORE_VERIFY_KR(x) do { \
    DISPATCH_VERIFY_MIG(x); \
    if (unlikely((x) == KERN_INVALID_NAME)) { \
    DISPATCH_CLIENT_CRASH((x), \
    "Use-after-free of dispatch_semaphore_t or dispatch_group_t"); \
    } else if (unlikely(x)) { \
    DISPATCH_INTERNAL_CRASH((x), "mach semaphore API failure"); \
    } \
    } while (0)


    //经过调试走的是这个逻辑
    void
    _dispatch_sema4_signal(_dispatch_sema4_t *sema, long count)
    {
    do {
    kern_return_t kr = semaphore_signal(*sema);//+1
    DISPATCH_SEMAPHORE_VERIFY_KR(kr);// == -1 报错
    } while (--count);//do-while(0) 只执行一次
    }

    相当于内部做了+1操作。这也是当信号量初始值为0的时候dispatch_semaphore_signal执行完毕后dispatch_semaphore_wait能够执行的原因。

    小结:

    • dispatch_semaphore_wait进行--操作,减完是负值进入do-while循环,阻塞后续流程
    • dispatch_semaphore_signal进行++操作,加完值不大于0进入后续报错流程
    • semaphore_signal 与 semaphore_wait才是信号量能控制最大并发数的根本原因,否则dispatch_semaphore_signaldispatch_semaphore_signal都是判断后直接返回,相当于什么都没做

    semaphore_signal & semaphore_wait

    三、调度组

    最直接的作用: 控制任务执行顺序
    相关API:

    • dispatch_group_create 创建组
    • dispatch_group_async 进组任务 (与dispatch_group_enterdispatch_group_leave搭配使用效果相同)
      • dispatch_group_enter 进组
      • dispatch_group_leave 出组
    • dispatch_group_notify 进组任务执行完毕通知
    • dispatch_group_wait 进组任务执行等待时间

    3.1 应用

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    dispatch_group_async(group, queue, ^{
    sleep(3);
    NSLog(@"1");
    });

    dispatch_group_async(group, queue1, ^{
    sleep(2);
    NSLog(@"2");
    });

    dispatch_group_async(group, queue1, ^{
    sleep(1);
    NSLog(@"3");
    });

    dispatch_group_async(group, queue, ^{
    NSLog(@"4");
    });

    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"5");
    });

    有如上案例,任务5永远在任务1、2、3、4之后执行。

    当然也可以使用enterleave配合dispatch_async使用:

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    //先 enter 再 leave
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
    sleep(3);
    NSLog(@"1");
    dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue1, ^{
    sleep(2);
    NSLog(@"2");
    dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue1, ^{
    sleep(1);
    NSLog(@"3");
    dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
    NSLog(@"4");
    dispatch_group_leave(group);
    });

    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"5");
    });

    效果相同,需要注意的是dispatch_group_enter要比dispatch_group_leave先调用,并且必须成对出现,否则会崩溃。当然两种形式也可以混着用。

    3.2 源码分析

    根据上面的分析有3个问题:

    • 1.dispatch_group_enter为什么要比dispatch_group_leave先调用,否则崩溃?
    • 2.能够实现同步的原理是什么?
    • 3.dispatch_group_async为什么等价于dispatch_group_enter + dispatch_group_leave?

    之前的版本调度组是封装了信号量,目前新版本的是调度组自己写了一套逻辑。

    3.2.1 dispatch_group_create


    dispatch_group_t
    dispatch_group_create(void)
    {
    return _dispatch_group_create_with_count(0);
    }

    //creat & enter 写在一起的写法,信号量标记位1
    dispatch_group_t
    _dispatch_group_create_and_enter(void)
    {
    return _dispatch_group_create_with_count(1);
    }

    是对_dispatch_group_create_with_count的调用:

    static inline dispatch_group_t
    _dispatch_group_create_with_count(uint32_t n)
    {
    dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
    sizeof(struct dispatch_group_s));
    dg->do_next = DISPATCH_OBJECT_LISTLESS;
    dg->do_targetq = _dispatch_get_default_queue(false);
    if (n) {
    os_atomic_store2o(dg, dg_bits,
    (uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
    os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
    }
    return dg;
    }

    调用_dispatch_object_alloc创建group,与信号量写法相似

    3.2.2 dispatch_group_enter

    void
    dispatch_group_enter(dispatch_group_t dg)
    {
    // The value is decremented on a 32bits wide atomic so that the carry
    // for the 0 -> -1 transition is not propagated to the upper 32bits.
    //0-- -> -1,与信号量不同的是没有wait
    uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
    DISPATCH_GROUP_VALUE_INTERVAL, acquire);
    uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
    if (unlikely(old_value == 0)) {
    _dispatch_retain(dg); // <rdar://problem/22318411>
    }
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
    DISPATCH_CLIENT_CRASH(old_bits,
    "Too many nested calls to dispatch_group_enter()");
    }
    }
    • 0--变为-1,与信号量不同的是没有wait操作。

    3.2.3 dispatch_group_leave

    void
    dispatch_group_leave(dispatch_group_t dg)
    {
    // The value is incremented on a 64bits wide atomic so that the carry for
    // the -1 -> 0 transition increments the generation atomically.
    //-1++ -> 0
    uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
    DISPATCH_GROUP_VALUE_INTERVAL, release);
    //#define DISPATCH_GROUP_VALUE_MASK 0x00000000fffffffcULL
    // old_state & DISPATCH_GROUP_VALUE_MASK 是一个很大的值
    uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);
    //-1 & DISPATCH_GROUP_VALUE_MASK == DISPATCH_GROUP_VALUE_1,old_value = -1
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {//old_value == -1
    old_state += DISPATCH_GROUP_VALUE_INTERVAL;
    do {
    new_state = old_state;
    if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
    new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
    new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
    } else {
    // If the group was entered again since the atomic_add above,
    // we can't clear the waiters bit anymore as we don't know for
    // which generation the waiters are for
    new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
    }
    if (old_state == new_state) break;
    } while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
    old_state, new_state, &old_state, relaxed)));
    //调用 _dispatch_group_wake,唤醒 dispatch_group_notify
    return _dispatch_group_wake(dg, old_state, true);
    }
    //old_value 为0的情况下直接报错,也就是先leave的情况下直接报错
    if (unlikely(old_value == 0)) {
    DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
    "Unbalanced call to dispatch_group_leave()");
    }
    }
    • -1++变为0,当old_value == -1的时候调用_dispatch_group_wake唤醒dispatch_group_notify
    • 既然old_value == -1的时候才唤醒,那么多次enter只有最后一次leave的时候才能唤醒。
    • old_value == 0的时候直接报错,这也就是为什么先调用leave直接发生了crash

    3.2.4 dispatch_group_notify

    void
    dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_block_t db)
    {
    dispatch_continuation_t dsn = _dispatch_continuation_alloc();
    _dispatch_continuation_init(dsn, dq, db, 0, DC_FLAG_CONSUME);
    _dispatch_group_notify(dg, dq, dsn);
    }

    调用_dispatch_group_notify

    static inline void
    _dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_continuation_t dsn)
    {
    uint64_t old_state, new_state;
    dispatch_continuation_t prev;

    dsn->dc_data = dq;
    _dispatch_retain(dq);

    prev = os_mpsc_push_update_tail(os_mpsc(dg, dg_notify), dsn, do_next);
    if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg);
    os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next);
    if (os_mpsc_push_was_empty(prev)) {
    os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, {
    new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS;
    if ((uint32_t)old_state == 0) {//循环判断 old_state == 0 的时候 wake
    os_atomic_rmw_loop_give_up({
    return _dispatch_group_wake(dg, new_state, false);
    });
    }
    });
    }
    }
    • old_state == 0的时候调用_dispatch_group_wake,也就是调用blockcallout。与leave调用了同一个方法。

    为什么两个地方都调用了?
    因为在leave的时候dispatch_group_notify可能已经执行过了,任务已经保存在了group中,leave的时候本身尝试调用一次。
    当然leave中也可能是一个延时任务,当调用leave的时候notify可能还没有执行,就导致notify任务还不存在。所以需要在notify中也调用。

    _dispatch_group_wake

    static void
    _dispatch_group_wake(dispatch_group_t dg, uint64_t dg_state, bool needs_release)
    {
    uint16_t refs = needs_release ? 1 : 0; // <rdar://problem/22318411>

    if (dg_state & DISPATCH_GROUP_HAS_NOTIFS) {
    dispatch_continuation_t dc, next_dc, tail;

    // Snapshot before anything is notified/woken <rdar://problem/8554546>
    dc = os_mpsc_capture_snapshot(os_mpsc(dg, dg_notify), &tail);
    do {
    dispatch_queue_t dsn_queue = (dispatch_queue_t)dc->dc_data;
    next_dc = os_mpsc_pop_snapshot_head(dc, tail, do_next);
    //异步回调,执行block callout
    _dispatch_continuation_async(dsn_queue, dc,
    _dispatch_qos_from_pp(dc->dc_priority), dc->dc_flags);
    _dispatch_release(dsn_queue);
    } while ((dc = next_dc));

    refs++;
    }

    if (dg_state & DISPATCH_GROUP_HAS_WAITERS) {
    _dispatch_wake_by_address(&dg->dg_gen);
    }

    if (refs) _dispatch_release_n(dg, refs);
    }
    • 调用_dispatch_continuation_async相当于调用的是dispatch_async执行notify的任务。
    • 任务先保存在在group中,唤醒notify的时候才将任务加入队列。

    3.2.5 dispatch_group_async

    dispatch_group_async(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_block_t db)
    {
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    //标记 DC_FLAG_GROUP_ASYNC
    uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC;
    dispatch_qos_t qos;

    qos = _dispatch_continuation_init(dc, dq, db, 0, dc_flags);
    _dispatch_continuation_group_async(dg, dq, dc, qos);
    }

    调用_dispatch_continuation_group_async

    static inline void
    _dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_continuation_t dc, dispatch_qos_t qos)
    {
    //调用enter
    dispatch_group_enter(dg);
    dc->dc_data = dg;
    //dispatch_async
    _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
    }

    • 内部先调用dispatch_group_enter,在这里就等待wakeup的调用了
    • 再调用_dispatch_continuation_async,相当于dispatch_async

    那么leave在什么时候调用呢?
    肯定要在callout执行完毕后调用。_dispatch_continuation_async的调用以全局队列为例调用_dispatch_root_queue_push,最终会调用到_dispatch_continuation_invoke_inline






    在这里就进行了逻辑区分,有group的情况下(dispatch_group_async的时候dc_flags进行了标记)调用的是_dispatch_continuation_with_group_invoke

    static inline void
    _dispatch_continuation_with_group_invoke(dispatch_continuation_t dc)
    {
    struct dispatch_object_s *dou = dc->dc_data;
    unsigned long type = dx_type(dou);
    if (type == DISPATCH_GROUP_TYPE) {
    //callout
    _dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
    _dispatch_trace_item_complete(dc);
    //leave
    dispatch_group_leave((dispatch_group_t)dou);
    } else {
    DISPATCH_INTERNAL_CRASH(dx_type(dou), "Unexpected object type");
    }
    }


    • callout后调用了dispatch_group_leave

    dispatch_group_async 底层是对 dispatch_group_enter + dispatch_group_leave 的封装

    • dispatch_group_async中先进行dispatch_group_enter,然后执行dispatch_async
    • 在回调中先_dispatch_client_callout然后dispatch_group_leave


    收起阅读 »

    高级线程应用之栅栏、信号量、调度组以及source(二)

    1.2.1.2 _dispatch_lane_non_barrier_completestatic void _dispatch_lane_non_barrier_complete(dispatch_lane_t dq, dispatch_wa...
    继续阅读 »


    1.2.1.2 _dispatch_lane_non_barrier_complete

    static void
    _dispatch_lane_non_barrier_complete(dispatch_lane_t dq,
    dispatch_wakeup_flags_t flags)
    {
    ......
    _dispatch_lane_non_barrier_complete_finish(dq, flags, old_state, new_state);
    }

    其中是对_dispatch_lane_non_barrier_complete_finish的调用。

    DISPATCH_ALWAYS_INLINE
    static void
    _dispatch_lane_non_barrier_complete_finish(dispatch_lane_t dq,
    dispatch_wakeup_flags_t flags, uint64_t old_state, uint64_t new_state)
    {
    if (_dq_state_received_override(old_state)) {
    // Ensure that the root queue sees that this thread was overridden.
    _dispatch_set_basepri_override_qos(_dq_state_max_qos(old_state));
    }

    if ((old_state ^ new_state) & DISPATCH_QUEUE_IN_BARRIER) {
    if (_dq_state_is_dirty(old_state)) {
    //走_dispatch_lane_barrier_complete逻辑
    return _dispatch_lane_barrier_complete(dq, 0, flags);
    }

    if ((old_state ^ new_state) & DISPATCH_QUEUE_ENQUEUED) {
    if (!(flags & DISPATCH_WAKEUP_CONSUME_2)) {
    _dispatch_retain_2(dq);
    }
    dispatch_assert(!_dq_state_is_base_wlh(new_state));
    _dispatch_trace_item_push(dq->do_targetq, dq);
    return dx_push(dq->do_targetq, dq, _dq_state_max_qos(new_state));
    }

    if (flags & DISPATCH_WAKEUP_CONSUME_2) {
    _dispatch_release_2_tailcall(dq);
    }
    }

    走的是_dispatch_lane_barrier_complete逻辑:

    DISPATCH_NOINLINE
    static void
    _dispatch_lane_barrier_complete(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;
    dispatch_lane_t dq = dqu._dl;

    if (dq->dq_items_tail && !DISPATCH_QUEUE_IS_SUSPENDED(dq)) {
    struct dispatch_object_s *dc = _dispatch_queue_get_head(dq);
    //串行队列
    if (likely(dq->dq_width == 1 || _dispatch_object_is_barrier(dc))) {
    if (_dispatch_object_is_waiter(dc)) {
    //栅栏中的任务逻辑
    return _dispatch_lane_drain_barrier_waiter(dq, dc, flags, 0);
    }
    } else if (dq->dq_width > 1 && !_dispatch_object_is_barrier(dc)) {
    return _dispatch_lane_drain_non_barriers(dq, dc, flags);
    }

    if (!(flags & DISPATCH_WAKEUP_CONSUME_2)) {
    _dispatch_retain_2(dq);
    flags |= DISPATCH_WAKEUP_CONSUME_2;
    }
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }

    uint64_t owned = DISPATCH_QUEUE_IN_BARRIER +
    dq->dq_width * DISPATCH_QUEUE_WIDTH_INTERVAL;
    //执行栅栏后续的代码
    return _dispatch_lane_class_barrier_complete(dq, qos, flags, target, owned);
    }
    • _dispatch_lane_drain_barrier_waiter执行栅栏函数中的任务。
    • _dispatch_lane_class_barrier_complete执行栅栏函数后续的代码。

    调用_dispatch_lane_drain_barrier_waiter执行栅栏函数中的任务:


    static void
    _dispatch_lane_drain_barrier_waiter(dispatch_lane_t dq,
    struct dispatch_object_s *dc, dispatch_wakeup_flags_t flags,
    uint64_t enqueued_bits)
    {
    ......
    return _dispatch_barrier_waiter_redirect_or_wake(dq, dc, flags,
    old_state, new_state);
    }

    直接调用_dispatch_barrier_waiter_redirect_or_wake

    static void
    _dispatch_barrier_waiter_redirect_or_wake(dispatch_queue_class_t dqu,
    dispatch_object_t dc, dispatch_wakeup_flags_t flags,
    uint64_t old_state, uint64_t new_state)
    {
    ......
    return _dispatch_waiter_wake(dsc, wlh, old_state, new_state);
    }

    调用_dispatch_waiter_wake

    static void
    _dispatch_waiter_wake(dispatch_sync_context_t dsc, dispatch_wlh_t wlh,
    uint64_t old_state, uint64_t new_state)
    {
    dispatch_wlh_t waiter_wlh = dsc->dc_data;

    if ((_dq_state_is_base_wlh(old_state) && !dsc->dsc_from_async) ||
    _dq_state_is_base_wlh(new_state) ||
    waiter_wlh != DISPATCH_WLH_ANON) {
    _dispatch_event_loop_wake_owner(dsc, wlh, old_state, new_state);
    }
    if (unlikely(waiter_wlh == DISPATCH_WLH_ANON)) {
    //走这里
    _dispatch_waiter_wake_wlh_anon(dsc);
    }
    }

    调用_dispatch_waiter_wake_wlh_anon:

    static void
    _dispatch_waiter_wake_wlh_anon(dispatch_sync_context_t dsc)
    {
    if (dsc->dsc_override_qos > dsc->dsc_override_qos_floor) {
    _dispatch_wqthread_override_start(dsc->dsc_waiter,
    dsc->dsc_override_qos);
    }
    //执行
    _dispatch_thread_event_signal(&dsc->dsc_event);
    }

    其中是对线程发送信号。

    对于_dispatch_root_queue_wakeup而言:

    void
    _dispatch_root_queue_wakeup(dispatch_queue_global_t dq,
    DISPATCH_UNUSED dispatch_qos_t qos, dispatch_wakeup_flags_t flags)
    {
    if (!(flags & DISPATCH_WAKEUP_BLOCK_WAIT)) {
    DISPATCH_INTERNAL_CRASH(dq->dq_priority,
    "Don't try to wake up or override a root queue");
    }
    if (flags & DISPATCH_WAKEUP_CONSUME_2) {
    return _dispatch_release_2_tailcall(dq);
    }
    }

    内部没有对barrier的处理,所以全局队列栅栏函数无效。

    因为全局队列不仅有你的任务,还有其它系统的任务。如果加barrier不仅影响你自己的任务还会影响系统的任务。对于全局队列而言栅栏函数就是个普通的异步函数。

    整个流程如下:




    1.2.2 dispatch_barrier_async

    dispatch_barrier_async源码如下:


    void
    dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work)
    {
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
    dispatch_qos_t qos;

    qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
    _dispatch_continuation_async(dq, dc, qos, dc_flags);
    }

    调用的是_dispatch_continuation_async

    static inline void
    _dispatch_continuation_async(dispatch_queue_class_t dqu,
    dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
    {
    #if DISPATCH_INTROSPECTION
    if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
    _dispatch_trace_item_push(dqu, dc);
    }
    #else
    (void)dc_flags;
    #endif
    return dx_push(dqu._dq, dc, qos);
    }

    调用了dx_push,对应的自定义队列是_dispatch_lane_concurrent_push。全局队列是_dispatch_root_queue_push

    _dispatch_lane_concurrent_push:

    void
    _dispatch_lane_concurrent_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    if (dq->dq_items_tail == NULL &&
    !_dispatch_object_is_waiter(dou) &&
    !_dispatch_object_is_barrier(dou) &&
    _dispatch_queue_try_acquire_async(dq)) {
    return _dispatch_continuation_redirect_push(dq, dou, qos);
    }

    _dispatch_lane_push(dq, dou, qos);
    }

    断点跟踪走的是_dispatch_lane_push

    DISPATCH_NOINLINE
    void
    _dispatch_lane_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    dispatch_wakeup_flags_t flags = 0;
    struct dispatch_object_s *prev;

    if (unlikely(_dispatch_object_is_waiter(dou))) {
    return _dispatch_lane_push_waiter(dq, dou._dsc, qos);
    }

    dispatch_assert(!_dispatch_object_is_global(dq));
    qos = _dispatch_queue_push_qos(dq, qos);

    prev = os_mpsc_push_update_tail(os_mpsc(dq, dq_items), dou._do, do_next);
    if (unlikely(os_mpsc_push_was_empty(prev))) {
    _dispatch_retain_2_unsafe(dq);
    flags = DISPATCH_WAKEUP_CONSUME_2 | DISPATCH_WAKEUP_MAKE_DIRTY;
    } else if (unlikely(_dispatch_queue_need_override(dq, qos))) {
    _dispatch_retain_2_unsafe(dq);
    flags = DISPATCH_WAKEUP_CONSUME_2;
    }
    os_mpsc_push_update_prev(os_mpsc(dq, dq_items), prev, dou._do, do_next);
    if (flags) {
    //栅栏函数走这里。
    //#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
    //dx_wakeup 对应 dq_wakeup 自定义全局队列对应 _dispatch_lane_wakeup,全局队列对应 _dispatch_root_queue_wakeup
    return dx_wakeup(dq, qos, flags);
    }
    }

    栅栏函数走_dispatch_lane_wakeup逻辑:

    void
    _dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    return _dispatch_lane_barrier_complete(dqu, qos, flags);
    }
    if (_dispatch_queue_class_probe(dqu)) {
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }
    //走这里
    return _dispatch_queue_wakeup(dqu, qos, flags, target);
    }

    继续断点走_dispatch_queue_wakeup逻辑:


    void
    _dispatch_queue_wakeup(dispatch_queue_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags, dispatch_queue_wakeup_target_t target)
    {
    ......

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    ......
    //loop _dispatch_lane_wakeup //_dq_state_merge_qos
    return _dispatch_lane_class_barrier_complete(upcast(dq)._dl, qos,
    flags, target, DISPATCH_QUEUE_SERIAL_DRAIN_OWNED);
    }

    if (target) {
    ......
    #if HAVE_PTHREAD_WORKQUEUE_QOS
    } else if (qos) {
    ......
    if (likely((old_state ^ new_state) & enqueue)) {
    ...... //_dispatch_queue_push_queue断点断不住,断它内部断点
    return _dispatch_queue_push_queue(tq, dq, new_state);
    }
    #if HAVE_PTHREAD_WORKQUEUE_QOS
    if (unlikely((old_state ^ new_state) & DISPATCH_QUEUE_MAX_QOS_MASK)) {
    if (_dq_state_should_override(new_state)) {
    return _dispatch_queue_wakeup_with_override(dq, new_state,
    flags);
    }
    }
    #endif // HAVE_PTHREAD_WORKQUEUE_QOS
    done:
    if (likely(flags & DISPATCH_WAKEUP_CONSUME_2)) {
    return _dispatch_release_2_tailcall(dq);
    }
    }

    这里断点走了_dispatch_queue_push_queue逻辑(_dispatch_queue_push_queue本身断不住,断它内部断点):


    static inline void
    _dispatch_queue_push_queue(dispatch_queue_t tq, dispatch_queue_class_t dq,
    uint64_t dq_state)
    {
    #if DISPATCH_USE_KEVENT_WORKLOOP
    if (likely(_dq_state_is_base_wlh(dq_state))) {
    _dispatch_trace_runtime_event(worker_request, dq._dq, 1);
    return _dispatch_event_loop_poke((dispatch_wlh_t)dq._dq, dq_state,
    DISPATCH_EVENT_LOOP_CONSUME_2);
    }
    #endif // DISPATCH_USE_KEVENT_WORKLOOP
    _dispatch_trace_item_push(tq, dq);
    //_dispatch_lane_concurrent_push
    return dx_push(tq, dq, _dq_state_max_qos(dq_state));
    }

    内部走的是_dispatch_lane_concurrent_push逻辑,这里又继续走了_dispatch_lane_push的逻辑了,在这里就造成了循环等待。当队列中任务执行完毕后_dispatch_lane_wakeup中就走_dispatch_lane_barrier_complete逻辑了。

    可以通过barrier前面的任务加延迟去验证。直接断点_dispatch_lane_barrier_complete,当前面的任务执行完毕后就进入_dispatch_lane_barrier_complete断点了。

    _dispatch_lane_barrier_complete源码如下:

    static void
    _dispatch_lane_barrier_complete(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    ......
    return _dispatch_lane_class_barrier_complete(dq, qos, flags, target, owned);
    }

    走了_dispatch_lane_class_barrier_complete逻辑:

    static void
    _dispatch_lane_class_barrier_complete(dispatch_lane_t dq, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags, dispatch_queue_wakeup_target_t target,
    uint64_t owned)
    {
    ......
    again:
    os_atomic_rmw_loop2o(dq, dq_state, old_state, new_state, release, {
    ......
    } else if (unlikely(_dq_state_is_dirty(old_state))) {
    ......
    flags |= DISPATCH_WAKEUP_BARRIER_COMPLETE;
    //自定义并行队列 _dispatch_lane_wakeup
    return dx_wakeup(dq, qos, flags);
    });
    } else {
    new_state &= ~DISPATCH_QUEUE_MAX_QOS_MASK;
    }
    });
    ......
    }

    调用走的是_dispatch_lane_wakeup

    void
    _dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    //barrier完成了就走这里的逻辑,barrier之前的任务执行完毕。
    return _dispatch_lane_barrier_complete(dqu, qos, flags);
    }
    if (_dispatch_queue_class_probe(dqu)) {
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }
    //走这里
    return _dispatch_queue_wakeup(dqu, qos, flags, target);
    }

    这个时候就又走到了_dispatch_lane_barrier_complete

    DISPATCH_WAKEUP_BARRIER_COMPLETE状态是在_dispatch_lane_resume中进行变更的:

    _dispatch_root_queue_push内部并没有对barrier的处理,与全局队列逻辑一致。所以barrier函数传递全局队列无效。

    整个过程如下:




    作者:HotPotCat
    链接:https://www.jianshu.com/p/84153e072f44


    收起阅读 »

    高级线程应用之栅栏、信号量、调度组以及source(一)

    一、栅栏函数CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令...
    继续阅读 »

    一、栅栏函数

    CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。换句话说,barrier指令的作用类似于一个拦水坝,阻止换序穿透这个大坝。

    栅栏函数最直接的作用:控制任务执行顺序,导致同步效果。
    有两个函数:

    • dispatch_barrier_async:前面的任务执行完毕才会执行barrier中的逻辑,以及barrier后加入队列的任务。
    • dispatch_barrier_sync:作用相同,但是会堵塞线程,影响后面的任务执行 。

    ⚠️:栅栏函数只能控制同一队列并发,相当于针对队列而言。

    1.1 应用

    1.1.1 dispatch_barrier_async 与 dispatch_barrier_sync 效果

    有如下案例:

    - (void)test {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("HotpotCat", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
    sleep(3);
    NSLog(@"1");
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"2");
    });
    dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"3:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"4");
    });
    NSLog(@"5");
    }

    分析:barrier阻塞的是自己以及concurrentQueue队列中在它后面加入的任务。由于这里使用的是异步函数所以任务125顺序不定,34之前。
    输出:

    GCDDemo[49708:5622304] 5
    GCDDemo[49708:5622437] 2
    GCDDemo[49708:5622434] 1
    GCDDemo[49708:5622434] 3:<NSThread: 0x600003439040>{number = 6, name = (null)}
    GCDDemo[49708:5622434] 4

    如果将dispatch_barrier_async改为dispatch_barrier_sync同步函数,则任务5会被阻塞。12(顺序不定)在3之前执行,45(顺序不定)在之后。

    1.1.2 栅栏函数存在的问题

    1.1.2.1 栅栏函数与全局队列

    concurrentQueue改为全局队列:

    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);
    dispatch_async(concurrentQueue, ^{
    NSLog(@"1");
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"2");
    });
    dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"3:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"4");
    });
    NSLog(@"5");

    输出:

    GCDDemo[49872:5632760] 5
    GCDDemo[49872:5632979] 1
    GCDDemo[49872:5633673] 2
    GCDDemo[49872:5633675] 4
    GCDDemo[49872:5633674] 3:<NSThread: 0x600001160240>{number = 10, name = (null)}

    这个时候栅栏函数无论同步还是异步都无效了(有可能系统调度刚好符合预期)。
    这也就意味着全局并发队列不允许使用栅栏函数,一定是自定义队列才能使用。

    1.1.2.1 栅栏函数与不同队列

    将任务24放入另外一个队列:

    dispatch_queue_t concurrentQueue = dispatch_queue_create("Hotpot", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t concurrentQueue2 = dispatch_queue_create("Cat", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
    sleep(3);
    NSLog(@"1");
    });
    dispatch_async(concurrentQueue2, ^{
    NSLog(@"2");
    });
    dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"3:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue2, ^{
    NSLog(@"4");
    });
    NSLog(@"5");

    输出:

    GCDDemo[49981:5639766] 5
    GCDDemo[49981:5640003] 2
    GCDDemo[49981:5639998] 4
    GCDDemo[49981:5639997] 1
    GCDDemo[49981:5639998] 3:<NSThread: 0x600003761500>{number = 5, name = (null)}

    这个时候concurrentQueue2中的任务先执行了,它并不受栅栏函数的影响。那么说明 栅栏函数只对同一个队列中的任务起作用

    1.1.3 栅栏函数作为锁使用

    有如下代码:

    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("Hotpot", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
    [array addObject:@(i)];
    });
    }
    • 多个线程同时操作array
    • addObject的时候有可能存在同一时间对同一块内存空间写入数据。
      比如写第3个数据的时候,当前数组中数据是(1、2)这个时候有2个线程同时写入数据就存在了(1、2、3)(1、2、4)`这个时候数据就发生了混乱造成了错误。

    在运行的时候由于线程不安全(可变数组线程不安全),发生了写入错误直接报错:




    将数组添加元素的操作放入dispatch_barrier_async中:

    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("Hotpot", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
    dispatch_barrier_async(concurrentQueue , ^{
    [array addObject:@(i)];
    });
    });
    }

    这样就没问题了,加入栅栏函数写入数据的时候相当于加了锁。

    1.2 原理分析

    根据1.1中的案例有3个问题:

    • 1.为什么栅栏函数能起作用?
    • 2.为什么全局队列无效?
    • 3.为什么任务必须在同一队列才有效?

    1.2.1 dispatch_barrier_sync

    dispatch_barrier_sync源码如下:

    void
    dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work)
    {
    uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK;
    if (unlikely(_dispatch_block_has_private_data(work))) {
    return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
    }
    _dispatch_barrier_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
    }

    直接调用_dispatch_barrier_sync_f

    static void
    _dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt,
    dispatch_function_t func, uintptr_t dc_flags)
    {
    _dispatch_barrier_sync_f_inline(dq, ctxt, func, dc_flags);
    }

    仍然是对_dispatch_barrier_sync_f_inline的调用:

    static inline void
    _dispatch_barrier_sync_f_inline(dispatch_queue_t dq, void *ctxt,
    dispatch_function_t func, uintptr_t dc_flags)
    {
    dispatch_tid tid = _dispatch_tid_self();

    if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
    DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
    }

    dispatch_lane_t dl = upcast(dq)._dl;

    if (unlikely(!_dispatch_queue_try_acquire_barrier_sync(dl, tid))) {
    //死锁走这里的逻辑,同步栅栏函数也走这里
    return _dispatch_sync_f_slow(dl, ctxt, func, DC_FLAG_BARRIER, dl,
    DC_FLAG_BARRIER | dc_flags);
    }

    if (unlikely(dl->do_targetq->do_targetq)) {
    return _dispatch_sync_recurse(dl, ctxt, func,
    DC_FLAG_BARRIER | dc_flags);
    }
    _dispatch_introspection_sync_begin(dl);
    _dispatch_lane_barrier_sync_invoke_and_complete(dl, ctxt, func
    DISPATCH_TRACE_ARG(_dispatch_trace_item_sync_push_pop(
    dq, ctxt, func, dc_flags | DC_FLAG_BARRIER)));
    }

    栅栏函数这个时候走的也是_dispatch_sync_f_slow逻辑:

    static void
    _dispatch_sync_f_slow(dispatch_queue_class_t top_dqu, void *ctxt,
    dispatch_function_t func, uintptr_t top_dc_flags,
    dispatch_queue_class_t dqu, uintptr_t dc_flags)
    {
    dispatch_queue_t top_dq = top_dqu._dq;
    dispatch_queue_t dq = dqu._dq;
    if (unlikely(!dq->do_targetq)) {
    return _dispatch_sync_function_invoke(dq, ctxt, func);
    }
    ......
    _dispatch_trace_item_push(top_dq, &dsc);
    //死锁报错
    __DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq);

    if (dsc.dsc_func == NULL) {
    // dsc_func being cleared means that the block ran on another thread ie.
    // case (2) as listed in _dispatch_async_and_wait_f_slow.
    dispatch_queue_t stop_dq = dsc.dc_other;
    return _dispatch_sync_complete_recurse(top_dq, stop_dq, top_dc_flags);
    }

    _dispatch_introspection_sync_begin(top_dq);
    _dispatch_trace_item_pop(top_dq, &dsc);
    _dispatch_sync_invoke_and_complete_recurse(top_dq, ctxt, func,top_dc_flags
    DISPATCH_TRACE_ARG(&dsc));
    }

    断点调试走的是_dispatch_sync_complete_recurse

    static void
    _dispatch_sync_complete_recurse(dispatch_queue_t dq, dispatch_queue_t stop_dq,
    uintptr_t dc_flags)
    {
    bool barrier = (dc_flags & DC_FLAG_BARRIER);
    do {
    if (dq == stop_dq) return;
    if (barrier) {
    //唤醒执行
    //_dispatch_lane_wakeup
    dx_wakeup(dq, 0, DISPATCH_WAKEUP_BARRIER_COMPLETE);
    } else {
    //已经执行完成没有栅栏函数
    _dispatch_lane_non_barrier_complete(upcast(dq)._dl, 0);
    }
    dq = dq->do_targetq;
    barrier = (dq->dq_width == 1);
    } while (unlikely(dq->do_targetq));
    }
    • 这里进行了递归调用,循环条件是dq->do_targetq也就是 仅对当前队列有效
    • 唤醒执行栅栏前任务执行_dispatch_lane_wakeup逻辑。
    • 当栅栏前的任务执行完毕走_dispatch_lane_non_barrier_complete逻辑。这也就是为什么栅栏起作用的原因。

    dx_wakeup在全局队列是_dispatch_root_queue_wakeup,在自定义并行队列是_dispatch_lane_wakeup

    1.2.1.1 _dispatch_lane_wakeup

    void
    _dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    //barrier完成了就走这里的逻辑,barrier之前的任务执行完毕。
    return _dispatch_lane_barrier_complete(dqu, qos, flags);
    }
    if (_dispatch_queue_class_probe(dqu)) {
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }
    //走这里
    return _dispatch_queue_wakeup(dqu, qos, flags, target);
    }
    • 在栅栏函数执行完毕后才走_dispatch_lane_barrier_complete_dispatch_lane_non_barrier_complete中的逻辑就汇合了。
    • 没有执行完毕的时候执行_dispatch_queue_wakeup

    _dispatch_queue_wakeup源码如下:

    void
    _dispatch_queue_wakeup(dispatch_queue_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags, dispatch_queue_wakeup_target_t target)
    {
    ......

    if (likely((old_state ^ new_state) & enqueue)) {
    ......
    //_dispatch_queue_push_queue 断点断不住,走这里。
    return _dispatch_queue_push_queue(tq, dq, new_state);
    }
    ......
    }

    最终走的是_dispatch_queue_push_queue逻辑:

    static inline void
    _dispatch_queue_push_queue(dispatch_queue_t tq, dispatch_queue_class_t dq,
    uint64_t dq_state)
    {
    #if DISPATCH_USE_KEVENT_WORKLOOP
    if (likely(_dq_state_is_base_wlh(dq_state))) {
    _dispatch_trace_runtime_event(worker_request, dq._dq, 1);
    return _dispatch_event_loop_poke((dispatch_wlh_t)dq._dq, dq_state,
    DISPATCH_EVENT_LOOP_CONSUME_2);
    }
    #endif // DISPATCH_USE_KEVENT_WORKLOOP
    _dispatch_trace_item_push(tq, dq);
    //_dispatch_lane_concurrent_push
    return dx_push(tq, dq, _dq_state_max_qos(dq_state));
    }

    内部是对_dispatch_lane_concurrent_push的调用:

    void
    _dispatch_lane_concurrent_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    if (dq->dq_items_tail == NULL &&
    !_dispatch_object_is_waiter(dou) &&
    !_dispatch_object_is_barrier(dou) &&
    _dispatch_queue_try_acquire_async(dq)) {
    return _dispatch_continuation_redirect_push(dq, dou, qos);
    }

    _dispatch_lane_push(dq, dou, qos);
    }

    这里直接调用_dispatch_lane_push

    void
    _dispatch_lane_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    ......
    if (flags) {
    //栅栏函数走这里。
    //#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
    //dx_wakeup 对应 dq_wakeup 自定义全局队列对应 _dispatch_lane_wakeup,全局队列对应 _dispatch_root_queue_wakeup
    return dx_wakeup(dq, qos, flags);
    }
    }

    又调用回了_dispatch_lane_wakeup,相当于一直扫描。


    收起阅读 »

    iOS 基于定时器的动画 一

    基于定时器的动画我可以指导你,但是你必须按照我说的做。 -- 骇客帝国    在第10章“缓冲”中,我们研究了CAMediaTimingFunction,它是一个通过控制动画缓冲来模拟物理效果例如加速或者减速来...
    继续阅读 »

    基于定时器的动画

    我可以指导你,但是你必须按照我说的做。 -- 骇客帝国

        在第10章“缓冲”中,我们研究了CAMediaTimingFunction,它是一个通过控制动画缓冲来模拟物理效果例如加速或者减速来增强现实感的东西,那么如果想更加真实地模拟物理交互或者实时根据用户输入修改动画改怎么办呢?在这一章中,我们将继续探索一种能够允许我们精确地控制一帧一帧展示的基于定时器的动画。

    11.1 定时帧

    动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展示像素的时候并不能做到这一点。一般来说这种显示都无法做到连续的移动,能做的仅仅是足够快地展示一系列静态图片,只是看起来像是做了运动。

    我们之前提到过iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,CAAnimation最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。

    在第10章中,我们解决了如何自定义缓冲函数,然后根据需要展示的帧的数组来告诉CAKeyframeAnimation的实例如何去绘制。所有的Core Animation实际上都是按照一定的序列来显示这些帧,那么我们可以自己做到这些么?

    NSTimer

    实际上,我们在第三章“图层几何学”中已经做过类似的东西,就是时钟那个例子,我们用了NSTimer来对钟表的指针做定时动画,一秒钟更新一次,但是如果我们把频率调整成一秒钟更新60次的话,原理是完全相同的。

    我们来试着用NSTimer来修改第十章中弹性球的例子。由于现在我们在定时器启动之后连续计算动画帧,我们需要在类中添加一些额外的属性来存储动画的fromValuetoValueduration和当前的timeOffset(见清单11.1)。

    清单11.1 使用NSTimer实现弹性球动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) UIImageView *ballView;
    @property (nonatomic, strong) NSTimer *timer;
    @property (nonatomic, assign) NSTimeInterval duration;
    @property (nonatomic, assign) NSTimeInterval timeOffset;
    @property (nonatomic, strong) id fromValue;
    @property (nonatomic, strong) id toValue;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add ball image view
    UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
    self.ballView = [[UIImageView alloc] initWithImage:ballImage];
    [self.containerView addSubview:self.ballView];
    //animate
    [self animate];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //replay animation on tap
    [self animate];
    }

    float interpolate(float from, float to, float time)
    {
    return (to - from) * time + from;
    }

    - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
    {
    if ([fromValue isKindOfClass:[NSValue class]]) {
    //get type
    const char *type = [(NSValue *)fromValue objCType];
    if (strcmp(type, @encode(CGPoint)) == 0) {
    CGPoint from = [fromValue CGPointValue];
    CGPoint to = [toValue CGPointValue];
    CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
    return [NSValue valueWithCGPoint:result];
    }
    }
    //provide safe default implementation
    return (time < 0.5)? fromValue: toValue;
    }

    float bounceEaseOut(float t)
    {
    if (t < 4/11.0) {
    return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
    return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
    return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
    }

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //configure the animation
    self.duration = 1.0;
    self.timeOffset = 0.0;
    self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [self.timer invalidate];
    //start the timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0
    target:self
    selector:@selector(step:)
    userInfo:nil
    repeats:YES];
    }

    - (void)step:(NSTimer *)step
    {
    //update time offset
    self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration);
    //get normalized time offset (in range 0 - 1)
    float time = self.timeOffset / self.duration;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:self.fromValue
    toValue:self.toValue
    time:time];
    //move ball view to new position
    self.ballView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (self.timeOffset >= self.duration) {
    [self.timer invalidate];
    self.timer = nil;
    }
    }

    @end

    很赞,而且和基于关键帧例子的代码一样很多,但是如果想一次性在屏幕上对很多东西做动画,很明显就会有很多问题。

    NSTimer并不是最佳方案,为了理解这点,我们需要确切地知道NSTimer是如何工作的。iOS上的每个线程都管理了一个NSRunloop,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项:

    • 处理触摸事件
    • 发送和接受网络数据包
    • 执行使用gcd的代码
    • 处理计时器行为
    • 屏幕重绘

    当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。

    屏幕重绘的频率是一秒钟六十次,但是和定时器行为一样,如果列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,于是就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,于是动画看起来就跳动了。

    我们可以通过一些途径来优化:

    • 我们可以用CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
    • 基于真实帧的持续时间而不是假设的更新频率来做动画。
    • 调整动画计时器的run loop模式,这样就不会被别的事件干扰。

    CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。

    CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。

    计算帧的持续时间

    无论是使用NSTimer还是CADisplayLink,我们仍然需要处理一帧的时间超出了预期的六十分之一秒。由于我们不能够计算出一帧真实的持续时间,所以需要手动测量。我们可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。

    通过比较这些时间,我们就可以得到真实的每帧持续的时间,然后代替硬编码的六十分之一秒。我们来更新一下上个例子(见清单11.2)。

    清单11.2 通过测量没帧持续的时间来使得动画更加平滑

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) UIImageView *ballView;
    @property (nonatomic, strong) CADisplayLink *timer;
    @property (nonatomic, assign) CFTimeInterval duration;
    @property (nonatomic, assign) CFTimeInterval timeOffset;
    @property (nonatomic, assign) CFTimeInterval lastStep;
    @property (nonatomic, strong) id fromValue;
    @property (nonatomic, strong) id toValue;

    @end

    @implementation ViewController

    ...

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //configure the animation
    self.duration = 1.0;
    self.timeOffset = 0.0;
    self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [self.timer invalidate];
    //start the timer
    self.lastStep = CACurrentMediaTime();
    self.timer = [CADisplayLink displayLinkWithTarget:self
    selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
    forMode:NSDefaultRunLoopMode];
    }

    - (void)step:(CADisplayLink *)timer
    {
    //calculate time delta
    CFTimeInterval thisStep = CACurrentMediaTime();
    CFTimeInterval stepDuration = thisStep - self.lastStep;
    self.lastStep = thisStep;
    //update time offset
    self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration);
    //get normalized time offset (in range 0 - 1)
    float time = self.timeOffset / self.duration;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:self.fromValue toValue:self.toValue
    time:time];
    //move ball view to new position
    self.ballView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (self.timeOffset >= self.duration) {
    [self.timer invalidate];
    self.timer = nil;
    }
    }

    @end

    Run Loop 模式

    注意到当创建CADisplayLink的时候,我们需要指定一个run looprun loop mode,对于run loop来说,我们就使用了主线程的run loop,因为任何用户界面的更新都需要在主线程执行,但是模式的选择就并不那么清楚了,每个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。

    一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动,一些常见的run loop模式如下:

    • NSDefaultRunLoopMode - 标准优先级
    • NSRunLoopCommonModes - 高优先级
    • UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画

    在我们的例子中,我们是用了NSDefaultRunLoopMode,但是不能保证动画平滑的运行,所以就可以用NSRunLoopCommonModes来替代。但是要小心,因为如果动画在一个高帧率情况下运行,你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停,直到动画结束。

    同样可以同时对CADisplayLink指定多个run loop模式,于是我们可以同时加入NSDefaultRunLoopModeUITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:

    self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];

    CADisplayLink类似,NSTimer同样也可以使用不同的run loop模式配置,通过别的函数,而不是+scheduledTimerWithTimeInterval:构造器

    self.timer = [NSTimer timerWithTimeInterval:1/60.0
    target:self
    selector:@selector(step:)
    userInfo:nil
    repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer
    forMode:NSRunLoopCommonModes];
    收起阅读 »

    iOS 缓冲 二

    10.2 自定义缓冲函数在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?除了+f...
    继续阅读 »

    10.2 自定义缓冲函数

    在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?

    除了+functionWithName:之外,CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::(注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。

    使用这个方法,我们可以创建一个自定义的缓冲函数,来匹配我们的时钟动画,为了理解如何使用这个方法,我们要了解一些CAMediaTimingFunction是如何工作的。

    三次贝塞尔曲线

    CAMediaTimingFunction函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。我们可以用一个简单的图标来解释,横轴代表时间,纵轴代表改变的量,于是线性的缓冲就是一条从起点开始的简单的斜线(图10.1)。

    图10.1

    图10.1 线性缓冲函数的图像

    这条曲线的斜率代表了速度,斜率的改变代表了加速度,原则上来说,任何加速的曲线都可以用这种图像来表示,但是CAMediaTimingFunction使用了一个叫做三次贝塞尔曲线的函数,它只可以产出指定缓冲函数的子集(我们之前在第八章中创建CAKeyframeAnimation路径的时候提到过三次贝塞尔曲线)。

    你或许会回想起,一个三次贝塞尔曲线通过四个点来定义,第一个和最后一个点代表了曲线的起点和终点,剩下中间两个点叫做控制点,因为它们控制了曲线的形状,贝塞尔曲线的控制点其实是位于曲线之外的点,也就是说曲线并不一定要穿过它们。你可以把它们想象成吸引经过它们曲线的磁铁。

    图10.2展示了一个三次贝塞尔缓冲函数的例子

    图10.2

    图10.2 三次贝塞尔缓冲函数

    实际上它是一个很奇怪的函数,先加速,然后减速,最后快到达终点的时候又加速,那么标准的缓冲函数又该如何用图像来表示呢?

    CAMediaTimingFunction有一个叫做-getControlPointAtIndex:values:的方法,可以用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果能回答为什么不简单返回一个CGPoint),但是使用它我们可以找到标准缓冲函数的点,然后用UIBezierPathCAShapeLayer来把它画出来。

    曲线的起始和终点始终是{0, 0}和{1, 1},于是我们只需要检索曲线的第二个和第三个点(控制点)。具体代码见清单10.4。所有的标准缓冲函数的图像见图10.3。

    清单10.4 使用UIBezierPath绘制CAMediaTimingFunction

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create timing function
    CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
    //get control points
    CGPoint controlPoint1, controlPoint2;
    [function getControlPointAtIndex:1 values:(float *)&controlPoint1];
    [function getControlPointAtIndex:2 values:(float *)&controlPoint2];
    //create curve
    UIBezierPath *path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointZero];
    [path addCurveToPoint:CGPointMake(1, 1)
    controlPoint1:controlPoint1 controlPoint2:controlPoint2];
    //scale the path up to a reasonable size for display
    [path applyTransform:CGAffineTransformMakeScale(200, 200)];
    //create shape layer
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.lineWidth = 4.0f;
    shapeLayer.path = path.CGPath;
    [self.layerView.layer addSublayer:shapeLayer];
    //flip geometry so that 0,0 is in the bottom-left
    self.layerView.layer.geometryFlipped = YES;
    }

    @end

    图10.3

    图10.3 标准CAMediaTimingFunction缓冲曲线

    那么对于我们自定义时钟指针的缓冲函数来说,我们需要初始微弱,然后迅速上升,最后缓冲到终点的曲线,通过一些实验之后,最终结果如下:

    [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];

    如果把它转换成缓冲函数的图像,最后如图10.4所示,如果把它添加到时钟的程序,就形成了之前一直期待的非常赞的效果(见代清单10.5)。

    图10.4

    图10.4 自定义适合时钟的缓冲函数

    清单10.5 添加了自定义缓冲函数的时钟程序

    - (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
    {
    //generate transform
    CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
    if (animated) {
    //create transform animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform";
    animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"];
    animation.toValue = [NSValue valueWithCATransform3D:transform];
    animation.duration = 0.5;
    animation.delegate = self;
    animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
    //apply animation
    handView.layer.transform = transform;
    [handView.layer addAnimation:animation forKey:nil];
    } else {
    //set transform directly
    handView.layer.transform = transform;
    }
    }

    更加复杂的动画曲线

    考虑一个橡胶球掉落到坚硬的地面的场景,当开始下落的时候,它会持续加速知道落到地面,然后经过几次反弹,最后停下来。如果用一张图来说明,它会如图10.5所示。

    图10.5

    图10.5 一个没法用三次贝塞尔曲线描述的反弹的动画

    这种效果没法用一个简单的三次贝塞尔曲线表示,于是不能用CAMediaTimingFunction来完成。但如果想要实现这样的效果,可以用如下几种方法:

    • CAKeyframeAnimation创建一个动画,然后分割成几个步骤,每个小步骤使用自己的计时函数(具体下节介绍)。
    • 使用定时器逐帧更新实现动画(见第11章,“基于定时器的动画”)。

    基于关键帧的缓冲

    为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过keyTimes来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。

    清单10.6展示了实现反弹球动画的代码(见图10.6)

    清单10.6 使用关键帧实现反弹球的动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) UIImageView *ballView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add ball image view
    UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
    self.ballView = [[UIImageView alloc] initWithImage:ballImage];
    [self.containerView addSubview:self.ballView];
    //animate
    [self animate];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //replay animation on tap
    [self animate];
    }

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = @[
    [NSValue valueWithCGPoint:CGPointMake(150, 32)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)],
    [NSValue valueWithCGPoint:CGPointMake(150, 140)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)],
    [NSValue valueWithCGPoint:CGPointMake(150, 220)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)],
    [NSValue valueWithCGPoint:CGPointMake(150, 250)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)]
    ];

    animation.timingFunctions = @[
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]
    ];

    animation.keyTimes = @[@0.0, @0.3, @0.5, @0.7, @0.8, @0.9, @0.95, @1.0];
    //apply animation
    self.ballView.layer.position = CGPointMake(150, 268);
    [self.ballView.layer addAnimation:animation forKey:nil];
    }

    @end

    图10.6

    图10.6 使用关键帧实现的反弹球动画

    这种方式还算不错,但是实现起来略显笨重(因为要不停地尝试计算各种关键帧和时间偏移)并且和动画强绑定了(因为如果要改变动画的一个属性,那就意味着要重新计算所有的关键帧)。那该如何写一个方法,用缓冲函数来把任何简单的属性动画转换成关键帧动画呢,下面我们来实现它。

    流程自动化

    在清单10.6中,我们把动画分割成相当大的几块,然后用Core Animation的缓冲进入和缓冲退出函数来大约形成我们想要的曲线。但如果我们把动画分割成更小的几部分,那么我们就可以用直线来拼接这些曲线(也就是线性缓冲)。为了实现自动化,我们需要知道如何做如下两件事情:

    • 自动把任意属性动画分割成多个关键帧
    • 用一个数学函数表示弹性动画,使得可以对帧做便宜

    为了解决第一个问题,我们需要复制Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式如下(假设时间从0到1):

    value = (endValue – startValue) × time + startValue;

    那么如果要插入一个类似于CGPointCGColorRef或者CATransform3D这种更加复杂类型的值,我们可以简单地对每个独立的元素应用这个方法(也就CGPoint中的x和y值,CGColorRef中的红,蓝,绿,透明值,或者是CATransform3D中独立矩阵的坐标)。我们同样需要一些逻辑在插值之前对对象拆解值,然后在插值之后在重新封装成对象,也就是说需要实时地检查类型。

    一旦我们可以用代码获取属性动画的起始值之间的任意插值,我们就可以把动画分割成许多独立的关键帧,然后产出一个线性的关键帧动画。清单10.7展示了相关代码。

    注意到我们用了60 x 动画时间(秒做单位)作为关键帧的个数,这时因为Core Animation按照每秒60帧去渲染屏幕更新,所以如果我们每秒生成60个关键帧,就可以保证动画足够的平滑(尽管实际上很可能用更少的帧率就可以达到很好的效果)。

    我们在示例中仅仅引入了对CGPoint类型的插值代码。但是,从代码中很清楚能看出如何扩展成支持别的类型。作为不能识别类型的备选方案,我们仅仅在前一半返回了fromValue,在后一半返回了toValue

    清单10.7 使用插入的值创建一个关键帧动画

    float interpolate(float from, float to, float time)
    {
    return (to - from) * time + from;
    }

    - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
    {
    if ([fromValue isKindOfClass:[NSValue class]]) {
    //get type
    const char *type = [fromValue objCType];
    if (strcmp(type, @encode(CGPoint)) == 0) {
    CGPoint from = [fromValue CGPointValue];
    CGPoint to = [toValue CGPointValue];
    CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
    return [NSValue valueWithCGPoint:result];
    }
    }
    //provide safe default implementation
    return (time < 0.5)? fromValue: toValue;
    }

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //set up animation parameters
    NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    CFTimeInterval duration = 1.0;
    //generate keyframes
    NSInteger numFrames = duration * 60;
    NSMutableArray *frames = [NSMutableArray array];
    for (int i = 0; i < numFrames; i++) {
    float time = 1 / (float)numFrames * i;
    [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
    }
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = frames;
    //apply animation
    [self.ballView.layer addAnimation:animation forKey:nil];
    }

    这可以起到作用,但效果并不是很好,到目前为止我们所完成的只是一个非常复杂的方式来使用线性缓冲复制CABasicAnimation的行为。这种方式的好处在于我们可以更加精确地控制缓冲,这也意味着我们可以应用一个完全定制的缓冲函数。那么该如何做呢?

    缓冲背后的数学并不很简单,但是幸运的是我们不需要一一实现它。罗伯特·彭纳有一个网页关于缓冲函数(http://www.robertpenner.com/easing),包含了大多数普遍的缓冲函数的多种编程语言的实现的链接,包括C。这里是一个缓冲进入缓冲退出函数的示例(实际上有很多不同的方式去实现它)。

    float quadraticEaseInOut(float t) 
    {
    return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1;
    }

    对我们的弹性球来说,我们可以使用bounceEaseOut函数:

    float bounceEaseOut(float t)
    {
    if (t < 4/11.0) {
    return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
    return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
    return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
    }

    如果修改清单10.7的代码来引入bounceEaseOut方法,我们的任务就是仅仅交换缓冲函数,现在就可以选择任意的缓冲类型创建动画了(见清单10.8)。

    清单10.8 用关键帧实现自定义的缓冲函数

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //set up animation parameters
    NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    CFTimeInterval duration = 1.0;
    //generate keyframes
    NSInteger numFrames = duration * 60;
    NSMutableArray *frames = [NSMutableArray array];
    for (int i = 0; i < numFrames; i++) {
    float time = 1/(float)numFrames * i;
    //apply easing
    time = bounceEaseOut(time);
    //add keyframe
    [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
    }
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = frames;
    //apply animation
    [self.ballView.layer addAnimation:animation forKey:nil];
    }

    总结
    在这一章中,我们了解了有关缓冲和CAMediaTimingFunction类,它可以允许我们创建自定义的缓冲函数来完善我们的动画,同样了解了如何用CAKeyframeAnimation来避开CAMediaTimingFunction的限制,创建完全自定义的缓冲函数。

    在下一章中,我们将要研究基于定时器的动画--另一个给我们对动画更多控制的选择,并且实现对动画的实时操纵。


    收起阅读 »

    iOS 缓冲 一

    缓冲生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿在第九章“图层时间”中,我们讨论了动画时间和CAMediaTiming协议。现在我们来看一下另一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更自然,...
    继续阅读 »

    缓冲

    生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿

    在第九章“图层时间”中,我们讨论了动画时间和CAMediaTiming协议。现在我们来看一下另一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更自然,而不是看起来的那种机械和人工,在这一章我们将要研究如何对你的动画控制和自定义缓冲曲线。

    10.1 动画速度

    动画实际上就是一段时间内的变化,这就暗示了变化一定是随着某个特定的速率进行。速率由以下公式计算而来:

    velocity = change / time

    这里的变化可以指的是一个物体移动的距离,时间指动画持续的时长,用这样的一个移动可以更加形象的描述(比如positionbounds属性的动画),但实际上它应用于任意可以做动画的属性(比如coloropacity)。

    上面的等式假设了速度在整个动画过程中都是恒定不变的(就如同第八章“显式动画”的情况),对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。

    考虑一个场景,一辆车行驶在一定距离内,它并不会一开始就以60mph的速度行驶,然后到达终点后突然变成0mph。一是因为需要无限大的加速度(即使是最好的车也不会在0秒内从0跑到60),另外不然的话会干死所有乘客。在现实中,它会慢慢地加速到全速,然后当它接近终点的时候,它会慢慢地减速,直到最后停下来。

    那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于积累的动能转换伴随着一声巨响,砰!)。

    现实生活中的任何一个物体都会在运动中加速或者减速。那么我们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。

    CAMediaTimingFunction

    那么该如何使用缓冲方程式呢?首先需要设置CAAnimationtimingFunction属性,是CAMediaTimingFunction类的一个对象。如果想改变隐式动画的计时函数,同样也可以使用CATransaction+setAnimationTimingFunction:方法。

    这里有一些方式来创建CAMediaTimingFunction,最简单的方式是调用+timingFunctionWithName:的构造方法。这里传入如下几个常量之一:

    kCAMediaTimingFunctionLinear 
    kCAMediaTimingFunctionEaseIn
    kCAMediaTimingFunctionEaseOut
    kCAMediaTimingFunctionEaseInEaseOut
    kCAMediaTimingFunctionDefault

    kCAMediaTimingFunctionLinear选项创建了一个线性的计时函数,同样也是CAAnimationtimingFunction属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。

    kCAMediaTimingFunctionEaseIn常量创建了一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。

    kCAMediaTimingFunctionEaseOut则恰恰相反,它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。

    kCAMediaTimingFunctionEaseInEaseOut创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默认的选择,实际上当使用UIView的动画方法时,他的确是默认的,但当创建CAAnimation的时候,就需要手动设置它了。

    最后还有一个kCAMediaTimingFunctionDefault,它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说是默认的,但还是要记住当创建显式CAAnimation它并不是默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault作为它们的计时方法)。

    你可以使用一个简单的测试工程来实验一下(清单10.1),在运行之前改变缓冲函数的代码,然后点击任何地方来观察图层是如何通过指定的缓冲移动的。

    清单10.1 缓冲函数的简单测试

    @interface ViewController ()

    @property (nonatomic, strong) CALayer *colorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a red layer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
    self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.colorLayer];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //configure the transaction
    [CATransaction begin];
    [CATransaction setAnimationDuration:1.0];
    [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
    //set the position
    self.colorLayer.position = [[touches anyObject] locationInView:self.view];
    //commit transaction
    [CATransaction commit];
    }

    @end

    UIView的动画缓冲

    UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:

    UIViewAnimationOptionCurveEaseInOut 
    UIViewAnimationOptionCurveEaseIn
    UIViewAnimationOptionCurveEaseOut
    UIViewAnimationOptionCurveLinear

    它们和CAMediaTimingFunction紧密关联,UIViewAnimationOptionCurveEaseInOut是默认值(这里没有kCAMediaTimingFunctionDefault相对应的值了)。

    具体使用方法见清单10.2(注意到这里不再使用UIView额外添加的图层,因为UIKit的动画并不支持这类图层)。

    清单10.2 使用UIKit动画的缓冲测试工程

    @interface ViewController ()

    @property (nonatomic, strong) UIView *colorView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a red layer
    self.colorView = [[UIView alloc] init];
    self.colorView.bounds = CGRectMake(0, 0, 100, 100);
    self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
    self.colorView.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.colorView];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //perform the animation
    [UIView animateWithDuration:1.0 delay:0.0
    options:UIViewAnimationOptionCurveEaseOut
    animations:^{
    //set the position
    self.colorView.center = [[touches anyObject] locationInView:self.view];
    }
    completion:NULL];

    }

    @end

    缓冲和关键帧动画

    或许你会回想起第八章里面颜色切换的关键帧动画由于线性变换的原因(见清单8.5)看起来有些奇怪,使得颜色变换非常不自然。为了纠正这点,我们来用更加合适的缓冲方法,例如kCAMediaTimingFunctionEaseIn,给图层的颜色变化添加一点脉冲效果,让它更像现实中的一个彩色灯泡。

    我们不想给整个动画过程应用这个效果,我们希望对每个动画的过程重复这样的缓冲,于是每次颜色的变换都会有脉冲效果。

    CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。

    在这个例子中,我们自始至终想使用同一个缓冲函数,但我们同样需要一个函数的数组来告诉动画不停地重复每个步骤,而不是在整个动画序列只做一次缓冲,我们简单地使用包含多个相同函数拷贝的数组就可以了(见清单10.3)。

    运行更新后的代码,你会发现动画看起来更加自然了。

    清单10.3 对CAKeyframeAnimation使用CAMediaTimingFunction

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;
    @property (nonatomic, weak) IBOutlet CALayer *colorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
    }

    - (IBAction)changeColor
    {
    //create a keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.duration = 2.0;
    animation.values = @[
    (__bridge id)[UIColor blueColor].CGColor,
    (__bridge id)[UIColor redColor].CGColor,
    (__bridge id)[UIColor greenColor].CGColor,
    (__bridge id)[UIColor blueColor].CGColor ];
    //add timing function
    CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
    animation.timingFunctions = @[fn, fn, fn];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
    }

    @end
    收起阅读 »

    iOS - 图层时间 二

    9.2 层级关系时间在第三章“图层几何学”中,你已经了解到每个图层是如何相对在图层树中的父图层定义它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层...
    继续阅读 »

    9.2 层级关系时间

    在第三章“图层几何学”中,你已经了解到每个图层是如何相对在图层树中的父图层定义它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。另一个相似点是所有的动画都被按照层级组合(使用CAAnimationGroup实例)。

    CALayer或者CAGroupAnimation调整durationrepeatCount/repeatDuration属性并不会影响到子动画。但是beginTimetimeOffsetspeed属性将会影响到子动画。然而在层级关系中,beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整CALayerCAGroupAnimationspeed属性将会对动画以及子动画速度应用一个缩放的因子。

    全局时间和本地时间

    CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间(“马赫”实际上是iOS和Mac OS系统内核的命名)。马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了,你可以使用CACurrentMediaTime函数来访问马赫时间:

    CFTimeInterval time = CACurrentMediaTime();

    这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。

    因此马赫时间对长时间测量并不有用。比如用CACurrentMediaTime去更新一个实时闹钟并不明智。(可以用[NSDate date]代替,就像第三章例子所示)。

    每个CALayerCAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTimetimeOffsetspeed属性计算。就和转换不同图层之间坐标关系一样,CALayer同样也提供了方法来转换不同图层之间的本地时间。如下:

    - (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; 
    - (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

    当用来同步不同图层之间有不同的speedtimeOffsetbeginTime的动画,这些方法会很有用。

    暂停,倒回和快进

    设置动画的speed属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个CAAnimation实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用-animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。

    如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。

    一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。

    通过增加主窗口图层的speed,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如UIAlertview)。可以在app delegate设置如下进行验证:

    self.window.layer.speed = 100;

    你也可以通过这种方式来减速,但其实也可以在模拟器通过切换慢速动画来实现。

    9.3 手动动画

    timeOffset一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。

    举个简单的例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个UIPanGestureRecognizer,然后用timeOffset左右摇晃。

    因为在动画添加到图层之后不能再做修改了,我们来通过调整layertimeOffset达到同样的效果(清单9.4)。

    清单9.4 通过触摸手势手动控制动画

    @interface ViewController ()

    @property (nonatomic, weak) UIView *containerView;
    @property (nonatomic, strong) CALayer *doorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the door
    self.doorLayer = [CALayer layer];
    self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
    self.doorLayer.position = CGPointMake(150 - 64, 150);
    self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
    self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
    [self.containerView.layer addSublayer:self.doorLayer];
    //apply perspective transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //add pan gesture recognizer to handle swipes
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
    [pan addTarget:self action:@selector(pan:)];
    [self.view addGestureRecognizer:pan];
    //pause all layer animations
    self.doorLayer.speed = 0.0;
    //apply swinging animation (which won't play because layer is paused)
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y";
    animation.toValue = @(-M_PI_2);
    animation.duration = 1.0;
    [self.doorLayer addAnimation:animation forKey:nil];
    }

    - (void)pan:(UIPanGestureRecognizer *)pan
    {
    //get horizontal component of pan gesture
    CGFloat x = [pan translationInView:self.view].x;
    //convert from points to animation duration //using a reasonable scale factor
    x /= 200.0f;
    //update timeOffset and clamp result
    CFTimeInterval timeOffset = self.doorLayer.timeOffset;
    timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
    self.doorLayer.timeOffset = timeOffset;
    //reset pan gesture
    [pan setTranslation:CGPointZero inView:self.view];
    }

    @end

    这其实是个小诡计,也许相对于设置个动画然后每次显示一帧而言,用移动手势来直接设置门的transform会更简单。

    在这个例子中的确是这样,但是对于比如说关键这这样更加复杂的情况,或者有多个图层的动画组,相对于实时计算每个图层的属性而言,这就显得方便的多了。

    总结

    在这一章,我们了解了CAMediaTiming协议,以及Core Animation用来操作时间控制动画的机制。在下一章,我们将要接触缓冲,另一个用来使动画更加真实的操作时间的技术。

    收起阅读 »

    一个"水"按钮(滑水的水)

    🐳 前言 不知道大家平时有没有留意水滴落下的瞬间。 仔细去听,仔细去看,每一滴滴水珠落下泛起的涟漪都让人意向连篇。 一个个显现而消失的涟漪就像时光仿佛带走了什么,还是留下了什么,又似乎一切都没有变,却又感觉多了些什么,让人情不自禁想要点一个赞~~ 好了不开玩...
    继续阅读 »

    🐳 前言



    • 不知道大家平时有没有留意水滴落下的瞬间。

    • 仔细去听,仔细去看,每一滴滴水珠落下泛起的涟漪都让人意向连篇。

    • 一个个显现而消失的涟漪就像时光仿佛带走了什么,还是留下了什么,又似乎一切都没有变,却又感觉多了些什么,让人情不自禁想要点一个~~

    • 好了不开玩笑了我们来试试做这个涟漪按钮。


    water.gif


    🤽‍♂️ ToDoList



    • 一片静好

    • 蜻蜓点水

    • 阵阵微波


    🚿 Just Do It



    • 其实做一个这样的效果无非就是中间的按钮旁边会有两个渐渐变大的阴影,而当时间的推移,随着阴影范围变大也渐渐消失。


    🌱 一片静好



    • 我们先做一个平静的湖面,也就是我们的按钮。


    /** index.html **/
    <div class="waterButton">
    <div class="good">
    <div class="good_btn" id="waterButton">
    <img src="./good.png" alt="">
    </div>
    <span id="water1"></span>
    <span id="water2"></span>
    </div>
    </div>


    • 在基本布局中我们需要一个div包裹住一个点赞图片来表示一个按钮,另外还需要两个span标签来表示即将泛起涟漪,这个到后面会用到。


    /** button.css **/
    .waterButton {
    height: 27rem;
    display: flex;
    justify-content: center;
    align-items: center;
    }
    .good {
    width: 6rem;
    height: 6rem;
    position: relative;
    }
    .good_btn {
    width: 6rem;
    height: 6rem;
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
    z-index: 3;
    cursor: pointer;
    box-shadow: .4rem .4rem .8rem #c8d0e7,-.4rem -.4rem .8rem #fff;
    }
    img{
    width: 50%;
    height: 50%;
    z-index: 4;
    }


    • 因为是模拟在水中的效果所以如果按钮的阴影特别单一相同就不好了,这时候我们可以让按钮上面白色阴影下面灰色阴影,在这里推荐一个网站给大家如果需要制作这些阴影可以在这里调试 Neumorphism.io


    image.png


    🍃 蜻蜓点水



    • 因为是按钮我们需要一个点击事件来模拟水滴滴入湖中的感觉。

    • 而水波荡漾的感觉其实可以做成一个动画,让一个跟按钮一样的元素逐渐缩放到两倍后慢慢消失,我们可以使用两个这样的元素来在视觉上产生水波一个接一个的感觉。


    .good_water-1, .good_water-2 {
    width: 6rem;
    height: 6rem;
    border-radius: 50%;
    z-index: -1;
    position: absolute;
    top: 0;
    left: 0 ;
    filter: blur(1px);
    }
    .good_water-1 {
    box-shadow: .4rem .4rem .8rem #c8d0e7,
    -.4rem -.4rem .8rem #fff;
    background: linear-gradient(to bottom right, #c8d0e7 0%, #fff 100%);
    animation: waves 2s linear;
    }
    .good_water-2 {
    box-shadow: .4rem .4rem .8rem #c8d0e7,
    -.4rem -.4rem .8rem #fff;
    animation: waves 2s linear 1s;
    }
    @keyframes waves {
    0% {
    transform: scale(1);
    opacity: 1;
    }
    50% {
    opacity: 1;
    }
    100% {
    transform: scale(2);
    opacity: 0;
    }
    }


    • 跟按钮一样我们给两个水波元素也加上不同的阴影,这样的感觉会更有立体感,而为了营造水波逐渐消失的感觉,我们需要给一个过渡属性filter: blur(1px)


    /** JS **/
    <script>
    let btn=document.getElementById('waterButton')
    let water1=document.getElementById('water1')
    let water2=document.getElementById('water2')
    let timer=''
    btn.addEventListener('click', ()=>{
    window.clearTimeout(timer)
    water1.classList.add("good_water-1");
    water2.classList.add("good_water-2");
    setTimeout(()=>{
    water1.classList.remove("good_water-1");
    water2.classList.remove("good_water-2");
    }, 3000)
    })
    </script>


    • 接下来我们设定点击事件来动态添加样式并在动画结束后移除样式,这样我们来看看效果吧~


    water1.gif


    💦 阵阵微波



    • 如果我们不希望水波这么快停下的话,我们也可以设置水波动画为无限循环,这样的话我们就不需要点击按钮的时候再加样式了,我们之间把样式加到水波上,然后给animation设置无限循环播放infinite


    .good_water-1 {
    ...
    animation: waves 2s linear infinite;

    }
    .good_water-2 {
    ...
    animation: waves 2s linear 1s infinite;
    }


    • 接下来我们来看看效果吧~是不是还不错呢。


    water2.gif


    👋 写在最后



    • 首先感谢大家看到这里,这次分享的只是学习css中的一些乐趣,对于业务上可能不太实用,但是图个乐嘛~上班这么累,多用前端做点好玩的事情。

    • 前端世界太过奇妙,只有细心的人才能发现其乐趣,希望可以帮到有需要的人。

    • 如果您觉得这篇文章有帮助到您的的话不妨🍉关注+点赞+收藏+评论+转发🍉支持一下哟~~😛您的支持就是我更新的最大动力。

    链接:https://juejin.cn/post/7000652451435003918

    收起阅读 »

    【前端可视化】如何在React中优雅的使用ECharts

    这片文章由最近公司的一个可视化项目有感而发,随着前端的飞速发展,近年来数据可视化越来越火,有些公司的业务跟地图、位置、大数据等脱离不开关系,所以数据可视化甚至成了单独的一门前端行业,比如在杭州地区的前端可视化职位不但有一定的需求量且高薪, 至今为止,已经有很...
    继续阅读 »

    这片文章由最近公司的一个可视化项目有感而发,随着前端的飞速发展,近年来数据可视化越来越火,有些公司的业务跟地图、位置、大数据等脱离不开关系,所以数据可视化甚至成了单独的一门前端行业,比如在杭州地区的前端可视化职位不但有一定的需求量且高薪,


    截屏2021-08-25 下午10.57.37.png


    至今为止,已经有很多的可视化框架供我们选择,比如D3EChartsLeaflet....等等。


    本文使用的可视化框架为ECharts


    看完本文你可以学到什么?



    • 如何搭建react+ts+echarts项目

    • typescript基础使用

    • eCharts如何在react中更安全高效的使用

    • eCharts的使用

    • eCharts图表尺寸自适应

    • 启蒙可视化项目思想


    本文的源码地址:github.com/Gexle-Tuy/e…


    项目准备


    技术栈为:React+TypeScript+ECharts。既然提到优雅那肯定跟TS逃离不开关系,毕竟强大的类型系统能给我的🐶💩代码保驾护航,什么?我不会TS,我不看了,别急,本文不做过于复杂的类型检查,只在组件状态(state)、属性(props)上做基本使用,不会TS也能看的懂,废话不多说,咱们开始吧。


    使用的为react官方脚手架create-react-app,但是默认启动的为正常的js项目,如果想加上typescript类型检查,我们可以去它的仓库地址查看使用语法。在github上找到facebook/create-react-app。找到目录packages/cra-template-typescript。 在README中就可以看见启动命令create-react-app my-app --template typescript。
    image


    项目搭建完成之后看看src下的index文件的后缀名是否为tsx而不是jsx,为tsx就说明ts项目搭建成功了,就可以开始咱们的高雅之旅了~





    初探


    前面瞎吧啦半天完全跟我们本文的主角ECharts没有关系呀,伞兵作者?别急,这就开始,首先安装ECharts。


    npm i echarts

    安装好之后该干什么?当然是来个官方的入门例子感受一下了啦,打开官网->快速入手->绘制一个简单的图表。


    可以看到,每一个图表都需要一个DOM当作容器,在React中我们可以用ref来获取到DOM实例。


    image


    发现平时正常写的ref竟然报错了,这就是强大的ts发挥了作用,我们把鼠标放上去可以发现提示框有一大堆东西。


    不能将类型“RefObject<unknown>”分配给类型“LegacyRef<HTMLDivElement> | undefined”。
    不能将类型“RefObject<unknown>”分配给类型“RefObject<HTMLDivElement>”。
    不能将类型“unknown”分配给类型“HTMLDivElement”。
    .....

    可以根据它的提示来解决这个问题,将ref加上类型检查,本文不对ts做过多介绍,只使用简单的基础类型检查,我们直接给它加上一个:any。


    eChartsRef:any= React.createRef();

    这样报错就消失了,可以理解为any类型就是没有类型检查,跟普通的js一样没有区别。真正的重点不在这里,所以就直接使用any,其实应该按照它的提示加上真正的类型检查RefObject<HTMLDivElement>





    拿到实例之后,直接copy官方的配置项例子过来看看效果。


    import React, { PureComponent } from "react";
    import * as eCharts from "echarts";

    export default class App extends PureComponent {

    eChartsRef: any = React.createRef();

    componentDidMount() {
    const myChart = eCharts.init(this.eChartsRef.current);

    let option = {
    title: {
    text: "ECharts 入门示例",
    },
    tooltip: {},
    legend: {
    data: ["销量"],
    },
    xAxis: {
    data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
    },
    yAxis: {},
    series: [
    {
    name: "销量",
    type: "bar",
    data: [5, 20, 36, 10, 10, 20],
    },
    ],
    };

    myChart.setOption(option);
    }

    render() {
    return <div ref={this.eChartsRef} style={{
    width: 600,
    height: 400,
    margin: 100
    }}></div>;
    }
    }

    gif


    当图标的动态效果呈现在你眼前的时候是不是心动了,原来可视化这么简单,到这里你就会了最基本的使用了。





    接下来就开始本文的重点!如何在react里封装图表组件动态渲染并自适应移动端


    正文


    首先确定项目中我们要用到的图表,这里我选了四个最基本且常用的图表(折线图趋势图饼状图柱状图)。


    所有的图表都由无状态组件写(函数组件、Hooks),因为它们只负责拿到数据并渲染。并无自己维护的状态。


    接下来就是封装图表组件,这里就不把四个表的代码都贴出来了,只拿一个折线图举例子。可以把拉下源码看下其他的图。


    折线图:src/components/LineChart


    import React, { useEffect, useRef } from 'react';
    import { IProps } from "./type";
    import * as echarts from "echarts";

    const Index: React.FC<IProps> = (props) => {

    const chartRef:any = useRef(); //拿到DOM容器

    // 每当props改变的时候就会实时重新渲染
    useEffect(()=>{
    const chart = echarts.init(chartRef.current); //echart初始化容器
    let option = { //配置项(数据都来自于props)
    title: {
    text: props.title ? props.title : "暂无数据",
    },
    xAxis: {
    type: 'category',
    data: props.xData,
    },
    yAxis: {
    type: 'value'
    },
    series: [{
    data: props.seriesData,
    type: 'line'
    }]
    };

    chart.setOption(option);
    }, [props]);

    return <div ref={chartRef} className="chart"></div>
    }

    export default Index;

    同文件下新建一个type.ts,将要约束的props类型检查单独抽离出去,当然也可以直接写在index.tsx文件里面,看个人喜好。
    type.ts


    // 给props添加类型检查
    export interface IProps {
    title: string, //图表的标题(为string类型)
    xData: string[], //图表x轴数据的数组(数字里面每一项都为string类型)
    seriesData: number[], //跟x轴每个坐标点对应的数据(数字里面每一项都为number类型)
    }

    根据每张图表对应的配置项,选出你想要动态配置的属性,就可以写成props作为属性传递过来。(比如,一个项目里需要用到很多张折线图,但是每个图表的线条颜色是不一样的,就可以把color写成一个props作为属性值传递进来。)





    封装好之后,我们在App.tsx中引入使用一下。


    App.tsx


    import React, { PureComponent } from "react";
    import LineChart from "./components/LineChart/Index";
    import "./App.css";
    export default class App extends PureComponent {
    eChartsRef: any = React.createRef();

    state = {
    lineChartData: {
    //折线图模拟数据
    xData: [
    "2021/08/13",
    "2021/08/14",
    "2021/08/15",
    "2021/08/16",
    "2021/08/17",
    "2021/08/18",
    ],
    seriesData: [22, 19, 88, 66, 5, 90],
    },
    };

    componentDidMount() {}

    render() {
    return (
    <div className="homeWrapper">
    {/* 折线图 */}
    <div className="chartWrapper">
    <LineChart
    title="折线图模拟数据"
    xData={this.state.lineChartData.xData}
    seriesData={this.state.lineChartData.seriesData}
    />
    </div>
    </div>
    );
    }
    }

    如果使用LineChart组件的时候少传了任何一个属性,或者说属性传递的类型不对,那么就会直接报错,将报错扼杀在开发阶段,而不是运行代码阶段,而且还有一个好处就是,加上类型检查后会有强大的智能提示,普通的js项目写一个组件根本就不会提示你需要传递某些属性。


    忘记传递某个属性
    image


    传递的类型不符合类型检查
    image


    效果如下:


    gif


    这样一个基本的图表组件就完成了,但是都是我们模拟的数据,在真实的开发中数据都是来自于后端返回给我们,而且格式还不是我们想要的,那时候就需要我们自己处理下数据包装成需要的数据格式再传递。


    这样封装成函数组件还有一个好处就是每当props改变的时候就会进行重新渲染。比如我在componentDidMount中开启一个定时器定时添加数据来模拟实时数据。


    componentDidMount() {
    setInterval(() => {
    this.setState({
    lineChartData: {
    xData: [...this.state.lineChartData.xData, "2000/01/01"],
    seriesData: [...this.state.lineChartData.seriesData, Math.floor(Math.random() * 100)],
    }
    })
    }, 1500 );
    }

    gif


    这样就可以实现展示实时数据了,比如每秒的pv、uv数等等。我们把四个图表组件全部封装好之后的效果是这样的。


    gif


    前三个图表的数据都来自实时数据模拟,最后一张饼状图直接在组件中写死数据了,有兴趣的小伙伴可以拉下源码自行把它实现成实时的,可以看option中的配置哪些需要配置的,单独抽离出来写在type.ts文件中。





    移动端适配


    啥?echarts没做移动端适配?当然不是,echarts的官网中就介绍了移动端的相关优化:echarts.apache.org/zh/feature.… 当然也有跨平台使用。


    gif


    好像是那么回事,但感觉好像少了些什么,好像没有根据屏幕尺寸大小变化而自动发生调整尺寸。每次都要刷新一下也就是重新进入页面。


    别着急,在它的API文档中,有这么一个方法,echarts创建的实例也就是通过echarts.init()之后的对象会有一个resize的方法。


    我们可以监听窗口的变化,只要窗口尺寸变化了就调用resize方法。监听窗口的变化的方法很简单window.onresize可以在创建组件对象的时候都添加上一个window.onresize方法。





    注意:如果网页只有一个图表那么这么写是可以的,如果项目中图表不只一个的话,每个图表组件难道在后面都写一个window.onresize方法吗?这样写的话只有最后创建的组件会自适应屏幕尺寸大小,因为每创建一个组件都重新将window.onresize赋予为新的函数体了。





    解决:我们可以写一个公用方法,每一次创建组件的时候都加入到一个数组中,当屏幕尺寸变化的时候,都去循环遍历这个数组中的每一项,然后调用resize方法。


    src/util.js


    const echartsDom = [];  //所有echarts图表的数组
    /**
    * 当屏幕尺寸变化时,循环数组里的每一项调用resize方法来实现自适应。
    * @param {*} eDom
    */
    export function echartsResize(eDom) {
    echartsDom.push(eDom);
    window.onresize = () => {
    echartsDom.forEach((it)=>{
    it.resize();
    })
    };
    }

    写好方法之后,在每个图表组件设置好option之后将他添加到此数组内,然后当屏幕尺寸变化后就可以将每个图表变成自适应的了。





    这样之后每个图表就都可以自适应屏幕尺寸啦~


    gif


    结语


    本文主要介绍了如何在react中更安全高效的使用eCharts,所涉及的ts都为最基础的类型检查(有兴趣的同学可以自行拓展),只是为了给各位提供一个我在写一个eCharts项目的时候如何去做和管理项目,文章有错误的地方欢迎指出,大佬勿喷,大家伙儿有更好的思路和想法欢迎大家积极留言。感谢观看~




    链接:https://juejin.cn/post/7000551946029858830

    收起阅读 »

    DIff算法看不懂就一起来砍我(带图)

    前言 面试官:"你了解虚拟DOM(Virtual DOM)跟Diff算法吗,请描述一下它们"; 我:"额,...鹅,那个",完了😰,突然智商不在线,没组织好语言没答好或者压根就答不出来; 所以这次我总结一下相关的知识点,让你可以有一个清晰的认知之余也会让你在今...
    继续阅读 »

    前言


    面试官:"你了解虚拟DOM(Virtual DOM)Diff算法吗,请描述一下它们";


    我:"额,...鹅,那个",完了😰,突然智商不在线,没组织好语言没答好或者压根就答不出来;


    所以这次我总结一下相关的知识点,让你可以有一个清晰的认知之余也会让你在今后遇到这种情况可以坦然自若,应付自如,游刃有余:




    相关知识点:



    • 虚拟DOM(Virtual DOM):


      • 什么是虚拟dom




      • 为什么要使用虚拟dom




      • 虚拟DOM库





    • DIFF算法:

      • snabbDom源码

        • init函数

        • h函数

        • patch函数

        • patchVnode函数

        • updateChildren函数








    虚拟DOM(Virtual DOM)


    什么是虚拟DOM


    一句话总结虚拟DOM就是一个用来描述真实DOM的javaScript对象,这样说可能不够形象,那我们来举个🌰:分别用代码来描述真实DOM以及虚拟DOM


    真实DOM:


    <ul class="list">
    <li>a</li>
    <li>b</li>
    <li>c</li>
    </ul>

    对应的虚拟DOM:



    let vnode = h('ul.list', [
    h('li','a'),
    h('li','b'),
    h('li','c'),
    ])

    console.log(vnode)

    控制台打印出来的Vnode:


    image.png


    h函数生成的虚拟DOM这个JS对象(Vnode)的源码:


    export interface VNodeData {
    props?: Props
    attrs?: Attrs
    class?: Classes
    style?: VNodeStyle
    dataset?: Dataset
    on?: On
    hero?: Hero
    attachData?: AttachData
    hook?: Hooks
    key?: Key
    ns?: string // for SVGs
    fn?: () => VNode // for thunks
    args?: any[] // for thunks
    [key: string]: any // for any other 3rd party module
    }

    export type Key = string | number

    const interface VNode = {
    sel: string | undefined, // 选择器
    data: VNodeData | undefined, // VNodeData上面定义的VNodeData
    children: Array<VNode | string> | undefined, //子节点,与text互斥
    text: string | undefined, // 标签中间的文本内容
    elm: Node | undefined, // 转换而成的真实DOM
    key: Key | undefined // 字符串或者数字
    }


    补充:

    上面的h函数大家可能有点熟悉的感觉但是一时间也没想起来,没关系我来帮大伙回忆;
    开发中常见的现实场景,render函数渲染:


    // 案例1 vue项目中的main.js的创建vue实例
    new Vue({
    router,
    store,
    render: h => h(App)
    }).$mount("#app");

    //案例2 列表中使用render渲染
    columns: [
    {
    title: "操作",
    key: "action",
    width: 150,
    render: (h, params) => {
    return h('div', [
    h('Button', {
    props: {
    size: 'small'
    },
    style: {
    marginRight: '5px',
    marginBottom: '5px',
    },
    on: {
    click: () => {
    this.toEdit(params.row.uuid);
    }
    }
    }, '编辑')
    ]);
    }
    }
    ]



    为什么要使用虚拟DOM



    • MVVM框架解决视图和状态同步问题

    • 模板引擎可以简化视图操作,没办法跟踪状态

    • 虚拟DOM跟踪状态变化

    • 参考github上virtual-dom的动机描述

      • 虚拟DOM可以维护程序的状态,跟踪上一次的状态

      • 通过比较前后两次状态差异更新真实DOM



    • 跨平台使用

      • 浏览器平台渲染DOM

      • 服务端渲染SSR(Nuxt.js/Next.js),前端是vue向,后者是react向

      • 原生应用(Weex/React Native)

      • 小程序(mpvue/uni-app)等



    • 真实DOM的属性很多,创建DOM节点开销很大

    • 虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小

    • 复杂视图情况下提升渲染性能(操作dom性能消耗大,减少操作dom的范围可以提升性能)


    灵魂发问:使用了虚拟DOM就一定会比直接渲染真实DOM快吗?答案当然是否定的,且听我说:
    2c3559e204c5aae6a1c6bfdc8557efcd.jpeg


    举例:当一个节点变更时DOMA->DOMB


    image.png
    上述情况:
    示例1是创建一个DOMB然后替换掉DOMA;
    示例2创建虚拟DOM+DIFF算法比对发现DOMBDOMA不是相同的节点,最后还是创建一个DOMB然后替换掉DOMA;
    可以明显看出1是更快的,同样的结果,2还要去创建虚拟DOM+DIFF算啊对比
    所以说使用虚拟DOM比直接操作真实DOM就一定要快这个说法是错误的,不严谨的


    举例:当DOM树里面的某个子节点的内容变更时:


    image.png
    当一些复杂的节点,比如说一个父节点里面有多个子节点,当只是一个子节点的内容发生了改变,那么我们没有必要像示例1重新去渲染这个DOM树,这个时候虚拟DOM+DIFF算法就能够得到很好的体现,我们通过示例2使用虚拟DOM+Diff算法去找出改变了的子节点更新它的内容就可以了


    总结:复杂视图情况下提升渲染性能,因为虚拟DOM+Diff算法可以精准找到DOM树变更的地方,减少DOM的操作(重排重绘)




    虚拟dom库



    • Snabbdom

      • Vue.js2.x内部使用的虚拟DOM就是改造的Snabbdom

      • 大约200SLOC(single line of code)

      • 通过模块可扩展

      • 源码使用TypeScript开发

      • 最快的Virtual DOM之一



    • virtual-dom




    Diff算法


    在看完上述的文章之后相信大家已经对Diff算法有一个初步的概念,没错,Diff算法其实就是找出两者之间的差异;



    diff 算法首先要明确一个概念就是 Diff 的对象是虚拟DOM(virtual dom),更新真实 DOM 是 Diff 算法的结果。



    下面我将会手撕snabbdom源码核心部分为大家打开Diff的心,给点耐心,别关网页,我知道你们都是这样:


    src=http___img.wxcha.com_file_201905_17_f5a4d33d48.jpg&refer=http___img.wxcha.jpeg




    snabbdom的核心



    • init()设置模块.创建patch()函数

    • 使用h()函数创建JavaScript对象(Vnode)描述真实DOM

    • patch()比较新旧两个Vnode

    • 把变化的内容更新到真实DOM树


    init函数


    init函数时设置模块,然后创建patch()函数,我们先通过场景案例来有一个直观的体现:


    import {init} from 'snabbdom/build/package/init.js'
    import {h} from 'snabbdom/build/package/h.js'

    // 1.导入模块
    import {styleModule} from "snabbdom/build/package/modules/style";
    import {eventListenersModule} from "snabbdom/build/package/modules/eventListeners";

    // 2.注册模块
    const patch = init([
    styleModule,
    eventListenersModule
    ])

    // 3.使用h()函数的第二个参数传入模块中使用的数据(对象)
    let vnode = h('div', [
    h('h1', {style: {backgroundColor: 'red'}}, 'Hello world'),
    h('p', {on: {click: eventHandler}}, 'Hello P')
    ])

    function eventHandler() {
    alert('疼,别摸我')
    }

    const app = document.querySelector('#app')

    patch(app,vnode)

    当init使用了导入的模块就能够在h函数中用这些模块提供的api去创建虚拟DOM(Vnode)对象;在上文中就使用了样式模块以及事件模块让创建的这个虚拟DOM具备样式属性以及事件属性,最终通过patch函数对比两个虚拟dom(会先把app转换成虚拟dom),更新视图;


    image.png


    我们再简单看看init的源码部分:


    // src/package/init.ts
    /* 第一参数就是各个模块
    第二参数就是DOMAPI,可以把DOM转换成别的平台的API,
    也就是说支持跨平台使用,当不传的时候默认是htmlDOMApi,见下文
    init是一个高阶函数,一个函数返回另外一个函数,可以缓存modules,与domApi两个参数,
    那么以后直接只传oldValue跟newValue(vnode)就可以了*/
    export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {

    ...

    return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {}
    }



    h函数


    些地方也会用createElement来命名,它们是一样的东西,都是创建虚拟DOM的,在上述文章中相信大伙已经对h函数有一个初步的了解并且已经联想了使用场景,就不作场景案例介绍了,直接上源码部分:


    // h函数
    export function h (sel: string): VNode
    export function h (sel: string, data: VNodeData | null): VNode
    export function h (sel: string, children: VNodeChildren): VNode
    export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
    export function h (sel: any, b?: any, c?: any): VNode {
    var data: VNodeData = {}
    var children: any
    var text: any
    var i: number
    ...
    return vnode(sel, data, children, text, undefined) //最终返回一个vnode函数
    };

    // vnode函数
    export function vnode (sel: string | undefined,
    data: any | undefined,
    children: Array<VNode | string> | undefined,
    text: string | undefined,
    elm: Element | Text | undefined): VNode {
    const key = data === undefined ? undefined : data.key
    return { sel, data, children, text, elm, key } //最终生成Vnode对象
    }

    总结:h函数先生成一个vnode函数,然后vnode函数再生成一个Vnode对象(虚拟DOM对象)


    补充:


    在h函数源码部分涉及一个函数重载的概念,简单说明一下:



    • 参数个数或参数类型不同的函数()

    • JavaScript中没有重载的概念

    • TypeScript中有重载,不过重载的实现还是通过代码调整参数



    重载这个概念个参数相关,和返回值无关




    • 实例1(函数重载-参数个数)



    function add(a:number,b:number){

    console.log(a+b)

    }

    function add(a:number,b:number,c:number){

    console.log(a+b+c)

    }

    add(1,2)

    add(1,2,3)



    • 实例2(函数重载-参数类型)



    function add(a:number,b:number){

    console.log(a+b)

    }

    function add(a:number,b:string){

    console.log(a+b)

    }

    add(1,2)

    add(1,'2')




    patch函数(核心)


    src=http___shp.qpic.cn_qqvideo_ori_0_e3012t7v643_496_280_0&refer=http___shp.qpic.jpeg


    要是看完前面的铺垫,看到这里你可能走神了,醒醒啊,这是核心啊,上高地了兄弟;



    • pactch(oldVnode,newVnode)

    • 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点(核心)

    • 对比新旧VNode是否相同节点(节点的key和sel相同)

    • 如果不是相同节点,删除之前的内容,重新渲染

    • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnodetext不同直接更新文本内容(patchVnode)

    • 如果新的VNode有children,判断子节点是否有变化(updateChildren,最麻烦,最难实现)


    源码:


    return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {    
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    // cbs.pre就是所有模块的pre钩子函数集合
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    // isVnode函数时判断oldVnode是否是一个虚拟DOM对象
    if (!isVnode(oldVnode)) {
    // 若不是即把Element转换成一个虚拟DOM对象
    oldVnode = emptyNodeAt(oldVnode)
    }
    // sameVnode函数用于判断两个虚拟DOM是否是相同的,源码见补充1;
    if (sameVnode(oldVnode, vnode)) {
    // 相同则运行patchVnode对比两个节点,关于patchVnode后面会重点说明(核心)
    patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
    elm = oldVnode.elm! // !是ts的一种写法代码oldVnode.elm肯定有值
    // parentNode就是获取父元素
    parent = api.parentNode(elm) as Node

    // createElm是用于创建一个dom元素插入到vnode中(新的虚拟DOM)
    createElm(vnode, insertedVnodeQueue)

    if (parent !== null) {
    // 把dom元素插入到父元素中,并且把旧的dom删除
    api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))// 把新创建的元素放在旧的dom后面
    removeVnodes(parent, [oldVnode], 0, 0)
    }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
    }

    补充1: sameVnode函数


    function sameVnode(vnode1: VNode, vnode2: VNode): boolean { 通过key和sel选择器判断是否是相同节点
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
    }



    patchVnode



    • 第一阶段触发prepatch函数以及update函数(都会触发prepatch函数,两者不完全相同才会触发update函数)

    • 第二阶段,真正对比新旧vnode差异的地方

    • 第三阶段,触发postpatch函数更新节点


    源码:


    function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    if (oldVnode === vnode) return
    if (vnode.data !== undefined) {
    for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    vnode.data.hook?.update?.(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) { // 新节点的text属性是undefined
    if (isDef(oldCh) && isDef(ch)) { // 当新旧节点都存在子节点
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) //并且他们的子节点不相同执行updateChildren函数,后续会重点说明(核心)
    } else if (isDef(ch)) { // 只有新节点有子节点
    // 当旧节点有text属性就会把''赋予给真实dom的text属性
    if (isDef(oldVnode.text)) api.setTextContent(elm, '')
    // 并且把新节点的所有子节点插入到真实dom中
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) { // 清除真实dom的所有子节点
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) { // 把''赋予给真实dom的text属性
    api.setTextContent(elm, '')
    }
    } else if (oldVnode.text !== vnode.text) { //若旧节点的text与新节点的text不相同
    if (isDef(oldCh)) { // 若旧节点有子节点,就把所有的子节点删除
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
    api.setTextContent(elm, vnode.text!) // 把新节点的text赋予给真实dom
    }
    hook?.postpatch?.(oldVnode, vnode) // 更新视图
    }

    看得可能有点蒙蔽,下面再上一副思维导图:


    image.png




    题外话:diff算法简介


    传统diff算法



    • 虚拟DOM中的Diff算法

    • 传统算法查找两颗树每一个节点的差异

    • 会运行n1(dom1的节点数)*n2(dom2的节点数)次方去对比,找到差异的部分再去更新


    image.png


    snabbdom的diff算法优化



    • Snbbdom根据DOM的特点对传统的diff算法做了优化

    • DOM操作时候很少会跨级别操作节点

    • 只比较同级别的节点


    image.png


    src=http___img.wxcha.com_file_202004_03_1ed2e19e4f.jpg&refer=http___img.wxcha.jpeg


    下面我们就会介绍updateChildren函数怎么去对比子节点的异同,也是Diff算法里面的一个核心以及难点;




    updateChildren(核中核:判断子节点的差异)



    • 这个函数我分为三个部分,部分1:声明变量,部分2:同级别节点比较,部分3:循环结束的收尾工作(见下图);


    image.png



    • 同级别节点比较五种情况:

      1. oldStartVnode/newStartVnode(旧开始节点/新开始节点)相同

      2. oldEndVnode/newEndVnode(旧结束节点/新结束节点)相同

      3. oldStartVnode/newEndVnode(旧开始节点/新结束节点)相同

      4. oldEndVnode/newStartVnode(旧结束节点/新开始节点)相同

      5. 特殊情况当1,2,3,4的情况都不符合的时候就会执行,在oldVnodes里面寻找跟newStartVnode一样的节点然后位移到oldStartVnode,若没有找到在就oldStartVnode创建一个



    • 执行过程是一个循环,在每次循环里,只要执行了上述的情况的五种之一就会结束一次循环

    • 循环结束的收尾工作:直到oldStartIdx>oldEndIdx || newStartIdx>newEndIdx(代表旧节点或者新节点已经遍历完)


    为了更加直观的了解,我们再来看看同级别节点比较五种情况的实现细节:


    新开始节点和旧开始节点(情况1)


    image.png



    • 情况1符合:(从新旧节点的开始节点开始对比,oldCh[oldStartIdx]和newCh[newStartIdx]进行sameVnode(key和sel相同)判断是否相同节点)

    • 则执行patchVnode找出两者之间的差异,更新图;如没有差异则什么都不操作,结束一次循环

    • oldStartIdx++/newStartIdx++


    新结束节点和旧结束节点(情况2)


    image.png



    • 情况1不符合就判断情况2,若符合:(从新旧节点的结束节点开始对比,oldCh[oldEndIdx]和newCh[newEndIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

    • 执行patchVnode找出两者之间的差异,更新视图,;如没有差异则什么都不操作,结束一次循环

    • oldEndIdx--/newEndIdx--


    旧开始节点/新结束节点(情况3)


    image.png



    • 情况1,2都不符合,就会尝试情况3:(旧节点的开始节点与新节点的结束节点开始对比,oldCh[oldStartIdx]和newCh[newEndIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

    • 执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

    • oldCh[oldStartIdx]对应的真实dom位移到oldCh[oldEndIdx]对应的真实dom

    • oldStartIdx++/newEndIdx--;


    旧结束节点/新开始节点(情况4)


    image.png



    • 情况1,2,3都不符合,就会尝试情况4:(旧节点的结束节点与新节点的开始节点开始对比,oldCh[oldEndIdx]和newCh[newStartIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

    • 执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

    • oldCh[oldEndIdx]对应的真实dom位移到oldCh[oldStartIdx]对应的真实dom

    • oldEndIdx--/newStartIdx++;


    新开始节点/旧节点数组中寻找节点(情况5)


    image.png



    • 从旧节点里面寻找,若寻找到与newCh[newStartIdx]相同的节点(且叫对应节点[1]),执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

    • 对应节点[1]对应的真实dom位移到oldCh[oldStartIdx]对应的真实dom


    image.png



    • 若没有寻找到相同的节点,则创建一个与newCh[newStartIdx]节点对应的真实dom插入到oldCh[oldStartIdx]对应的真实dom

    • newStartIdx++


    379426071b8130075b11ba142f9468e2.jpeg




    下面我们再介绍一下结束循环的收尾工作(oldStartIdx>oldEndIdx || newStartIdx>newEndIdx):


    image.png



    • 新节点的所有子节点先遍历完(newStartIdx>newEndIdx),循环结束

    • 新节点的所有子节点遍历结束就是把没有对应相同节点的子节点删除


    image.png



    • 旧节点的所有子节点先遍历完(oldStartIdx>oldEndIdx),循环结束

    • 旧节点的所有子节点遍历结束就是在多出来的子节点插入到旧节点结束节点前;(源码:newCh[newEndIdx + 1].elm),就是对应的旧结束节点的真实dom,newEndIdx+1是因为在匹配到相同的节点需要-1,所以需要加回来就是结束节点


    最后附上源码:


    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    let oldStartIdx = 0; // 旧节点开始节点索引
    let newStartIdx = 0; // 新节点开始节点索引
    let oldEndIdx = oldCh.length - 1; // 旧节点结束节点索引
    let oldStartVnode = oldCh[0]; // 旧节点开始节点
    let oldEndVnode = oldCh[oldEndIdx]; // 旧节点结束节点
    let newEndIdx = newCh.length - 1; // 新节点结束节点索引
    let newStartVnode = newCh[0]; // 新节点开始节点
    let newEndVnode = newCh[newEndIdx]; // 新节点结束节点
    let oldKeyToIdx; // 节点移动相关
    let idxInOld; // 节点移动相关
    let elmToMove; // 节点移动相关
    let before;


    // 同级别节点比较
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (oldStartVnode == null) {
    oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
    }
    else if (oldEndVnode == null) {
    oldEndVnode = oldCh[--oldEndIdx];
    }
    else if (newStartVnode == null) {
    newStartVnode = newCh[++newStartIdx];
    }
    else if (newEndVnode == null) {
    newEndVnode = newCh[--newEndIdx];
    }
    else if (sameVnode(oldStartVnode, newStartVnode)) { // 判断情况1
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
    oldStartVnode = oldCh[++oldStartIdx];
    newStartVnode = newCh[++newStartIdx];
    }
    else if (sameVnode(oldEndVnode, newEndVnode)) { // 情况2
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
    oldEndVnode = oldCh[--oldEndIdx];
    newEndVnode = newCh[--newEndIdx];
    }
    else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right情况3
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
    api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
    oldStartVnode = oldCh[++oldStartIdx];
    newEndVnode = newCh[--newEndIdx];
    }
    else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left情况4
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
    api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
    oldEndVnode = oldCh[--oldEndIdx];
    newStartVnode = newCh[++newStartIdx];
    }
    else { // 情况5
    if (oldKeyToIdx === undefined) {
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
    }
    idxInOld = oldKeyToIdx[newStartVnode.key];
    if (isUndef(idxInOld)) { // New element // 创建新的节点在旧节点的新节点前
    api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
    }
    else {
    elmToMove = oldCh[idxInOld];
    if (elmToMove.sel !== newStartVnode.sel) { // 创建新的节点在旧节点的新节点前
    api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
    }
    else {
    // 在旧节点数组中找到相同的节点就对比差异更新视图,然后移动位置
    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
    oldCh[idxInOld] = undefined;
    api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
    }
    }
    newStartVnode = newCh[++newStartIdx];
    }
    }
    // 循环结束的收尾工作
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    if (oldStartIdx > oldEndIdx) {
    // newCh[newEndIdx + 1].elm就是旧节点数组中的结束节点对应的dom元素
    // newEndIdx+1是因为在之前成功匹配了newEndIdx需要-1
    // newCh[newEndIdx + 1].elm,因为已经匹配过有相同的节点了,它就是等于旧节点数组中的结束节点对应的dom元素(oldCh[oldEndIdx + 1].elm)
    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    // 把新节点数组中多出来的节点插入到before前
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    }
    else {
    // 这里就是把没有匹配到相同节点的节点删除掉
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
    }
    }



    key的作用



    • Diff操作可以更加快速;

    • Diff操作可以更加准确;(避免渲染错误)

    • 不推荐使用索引作为key


    以下我们看看这些作用的实例:


    Diff操作可以更加准确;(避免渲染错误):

    实例:a,b,c三个dom元素中的b,c间插入一个z元素


    没有设置key
    image.png
    当设置了key:


    image.png


    Diff操作可以更加准确;(避免渲染错误)

    实例:a,b,c三个dom元素,修改了a元素的某个属性再去在a元素前新增一个z元素


    没有设置key:


    image.png


    image.png


    因为没有设置key,默认都是undefined,所以节点都是相同的,更新了text的内容但还是沿用了之前的dom,所以实际上a->z(a原本打勾的状态保留了,只改变了text),b->a,c->b,d->c,遍历完毕发现还要增加一个dom,在最后新增一个text为d的dom元素


    设置了key:


    image.png


    image.png


    当设置了key,a,b,c,d都有对应的key,a->a,b->b,c->c,d->d,内容相同无需更新,遍历结束,新增一个text为z的dom元素


    不推荐使用索引作为key:

    设置索引为key:


    image.png


    这明显效率不高,我们只希望找出不同的节点更新,而使用索引作为key会增加运算时间,我们可以把key设置为与节点text为一致就可以解决这个问题:


    image.png




    最后


    如有描述错误或者不明的地方请在下方评论联系我,我会立刻更新,如有收获,请为我点个赞👍这是对我的莫大的支持,谢谢各位


    链接:https://juejin.cn/post/7000266544181674014

    收起阅读 »

    产品经理说你能不能让词云动起来?我觉得配得上!!!

    ☀️ 前言 事情是这样的,前段时间拿到公司的数据大屏原型图让我一天内把一整个页面做出来。 简单看了看,就是一个3840 * 1840的大屏然后几个列表几个图例看起来也没有多复杂。 唰!很快啊加了一会班把整个页面调整好了信心十足拿给产品经理看。 产品经理皱了皱...
    继续阅读 »

    ☀️ 前言



    • 事情是这样的,前段时间拿到公司的数据大屏原型图让我一天内把一整个页面做出来。

    • 简单看了看,就是一个3840 * 1840的大屏然后几个列表几个图例看起来也没有多复杂。

    • 唰!很快啊加了一会班把整个页面调整好了信心十足拿给产品经理看。

    • 产品经理皱了皱眉头:你这词云不会动啊??


    🌤️ 之前的效果



    • 听到这话我发现情况不对,我寻思着这原型图的词云也看不出他要没要求我动啊,而且明明我做的是会动的呀!


    🎢 关系图



    • 一开始我用的是echartsgraph关系图,这种图的特点是一开始会因为每个词的斥力会互相分开,在一开始会有一些动态效果,但是因为力引导布局会在多次迭代后才会稳定,所以到后面就不会继续运动了。


    ciyun1.gif



    • 我:是吧我没骗人吧?确实是会动的。

    • 产品经理:这样效果不好,没有科技感,而且我要字体大小每个都不同的,明天要拿给客户看一版,比较急,算了你别做动的了就让他词云填满然后每个词的大小要不一样。


    WPS图片编辑.png


    🎠 词云图



    • 做不动词云的那不就简单了,直接使用echartswordCloud图啊,直接唰唰配置一下就好了。


    image.png



    • 产品经理:客户看完了,整体还不错,但是词云这块我还是想它动起来,这样吧,你想个办法整整。


    src=http___5b0988e595225.cdn.sohucs.com_images_20181108_0b031f4213f4403ca4cfca30c2b369ca.jpeg&refer=http___5b0988e595225.cdn.sohucs.jpg


    🚄 自己手写



    • 对于这个词云,我一开始真的是死脑筋了,认定要用echarts来做,但实际上wordCloud官网也没有提供资料了,好像确实也没有办法让它动起来。

    • 思量片刻....等会,词云要不同大小不同颜色然后要在区域内随机移动,既然我不熟canvas,那我是不是可以用jscss来写一个2d的呢,说白了就是一个词语在一个容器内随机运动然后每个词语都动起来撒,好像能行....开干。


    🚅 ToDoList



    • 准备容器和需要的配置项

    • 生成所有静态词云

    • 让词云动起来


    🚈 Just Do It



    • 由于我这边的技术栈是vue 2.x的所以接下来会用vue 2.x的语法来分享,但实际上换成原生js也没有什么难度,相信大家可以接受的。


    🚎 准备容器和需要的配置项



    • 首先建立一个容器来包裹我们要装的词云,我们接下来的所有操作都围绕这个容器进行。


    <template>
    <div class="wordCloud" ref="wordCloud">
    </div>
    </template>

    image.png



    • 因为我们的词云需要有不同的颜色我们需要实现准备一个词语列表和颜色列表,再准备一个空数组来存储之后生成的词语。


    ...
    data () {
    return {
    hotWord: ['万事如意', '事事如意 ', '万事亨通', '一帆风顺', '万事大吉', '吉祥如意', '步步高升', '步步登高', '三羊开泰', '得心应手', '财源广进', '陶未媲美', '阖家安康', '龙马精神'],
    color: [
    '#a18cd1', '#fad0c4', '#ff8177',
    '#fecfef', '#fda085', '#f5576c',
    '#330867', '#30cfd0', '#38f9d7'
    ],
    wordArr: []
    };
    }
    ...


    • 准备的这些词语都是想对现在在读文章的你说的~如果觉得我说得对的不妨读完文章后给一个 ~

    • 好了不开玩笑,现在准备工作完成了,开始生成我们的词云。


    🚒 生成所有静态词云



    • 我们如果想让一个容器里面充满词语,按照正常我们切图的逻辑来说,每个词语占一个span,那么就相当于一个div里面有n(hotWord数量)个词语,也就是容器里面有对应数量的span标签即可。

    • 如果需要不同的颜色和大小,再分别对span标签分别加不同样式即可。


    ...
    mounted () {
    this.init();
    },
    methods: {
    init () {
    this.dealSpan();
    },
    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    // 根据词云数量生成span数量设置字体颜色和大小
    const spanDom = document.createElement('span');
    spanDom.style.position = 'relative';
    spanDom.style.display = "inline-block";
    spanDom.style.color = this.randomColor();
    spanDom.style.fontSize = this.randomNumber(15, 25) + 'px';
    spanDom.innerHTML = value;
    this.$refs.wordCloud.appendChild(spanDom);
    wordArr.push(spanDom);
    });
    this.wordArr = wordArr;
    },
    randomColor () {
    // 获取随机颜色
    var colorIndex = Math.floor(this.color.length * Math.random());
    return this.color[colorIndex];
    },
    randomNumber (lowerInteger, upperInteger) {
    // 获得一个包含最小值和最大值之间的随机数。
    const choices = upperInteger - lowerInteger + 1;
    return Math.floor(Math.random() * choices + lowerInteger);
    }
    }
    ...


    • 我们对hotWord热词列表进行遍历,每当有一个词语就生成一个span标签,分别使用randomColor()randomSize()设置不同的随机颜色和大小。

    • 最后再将这些span都依次加入div容器中,那么完成后是这样的。


    image.png


    🚓 让词云动起来



    • 词语是添加完了,接下来我们需要让他们动起来,那么该怎么动呢,我们自然而然会想到transformtranslateXtranslateY属性,我们首先要让一个词语先动起来,接下来所有的都应用这种方式就可以了。


    先动一下x轴


    • 怎么动呢?我们现在要做的是一件无限循环的事情,就是一个元素无限的移动,既然是无限,在js中用定时器可不可以实现呢?确实是可以的,但是会巨卡,万一词语一多你的电脑会爆炸,在另一方面编写动画循环的关键是要知道延迟时间多长合适,如果太长或者太短都不合适所以不用定时器。

    • 然后一不小心发现了window.requestAnimationFrame这个APIrequestAnimationFrame不需要设置时间间隔。



    requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。




    • 也就是说当我们循环无限的让一个元素在x轴或者y轴移动,假设每秒向右移动10px那么它的translateX就是累加10px,每个元素都是如此那么我们需要给span元素新增一个属性来代表它的位置。


    data () {
    return {
    ...
    timer: null,
    resetTime: 0
    ...
    };
    }
    methods: {
    init () {
    this.dealSpan();
    this.render();
    },
    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    ...
    spanDom.local = {
    position: {
    x: 0,
    y: 0
    }
    };
    ...
    });
    this.wordArr = wordArr;
    },
    render () {
    if (this.resetTime < 100) {
    //防止“栈溢出”
    this.resetTime = this.resetTime + 1;
    this.timer = requestAnimationFrame(this.render.bind(this));
    this.resetTime = 0;
    }
    this.wordFly();
    },
    wordFly () {
    this.wordArr.forEach((value) => {
    //每次循环加1
    value.local.position.x += 1;
    // 给每个词云加动画过渡
    value.style.transform = 'translateX(' + value.local.position.x + 'px)';
    });
    },
    },
    destroyed () {
    // 组件销毁,关闭定时执行
    cancelAnimationFrame(this.timer);
    },


    • 这时候我们给每个元素加了个local属性里面有它的初始位置,每当我们执行一次requestAnimationFrame的时候它的初始位置+1,再把这个值给到translateX这样我们每次循环都相当于移动了1px,现在我们来看看效果。


    ciyun2.gif


    调整范围


    • 嘿!好家伙,动是动起来了,但是怎么还过头了呢?

    • 我们发现每次translateX+1了但是没有给一个停止的范围给他,所以我们需要给一个让他到容器的边缘就开始掉头的步骤。

    • 那怎么样让他掉头呢?既然我们可以让他每次往右移动1px那么我们是不是可以检测到当它的x轴位置大于这个容器的位置时x轴位置小于这个容器的位置时并且换个方向就好换个方向我们只需要用正负数来判断即可。


    init () {
    this.dealSpan();
    this.initWordPos();
    this.render();
    },
    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    ...
    spanDom.local = {
    position: {
    // 位置
    x: 0,
    y: 0
    },
    direction: {
    // 方向 正数往右 负数往左
    x: 1,
    y: 1
    }
    };
    ...
    });
    this.wordArr = wordArr;
    },
    wordFly () {
    this.wordArr.forEach((value) => {
    // 设置运动方向 大于边界或者小于边界的时候换方向
    if (value.local.realPos.minx + value.local.position.x < this.ContainerSize.leftPos.x) {
    value.local.direction.x = -value.local.direction.x;
    }
    if (value.local.realPos.maxx + value.local.position.x > this.ContainerSize.rightPos.x) {
    value.local.direction.x = -value.local.direction.x;
    }
    //每次右移1个单位 当方向为负数时就是-1个单位也就是向左移1个单位
    value.local.position.x += 1 * value.local.direction.x;
    // 给每个词云加动画过渡
    value.style.transform = 'translateX(' + value.local.position.x + 'px)';
    });
    },
    initWordPos () {
    // 计算每个词的真实位置和容器的位置
    this.wordArr.forEach((value) => {
    value.local.realPos = {
    minx: value.offsetLeft,
    maxx: value.offsetLeft + value.offsetWidth
    };
    });
    this.ContainerSize = this.getContainerSize();
    },
    getContainerSize () {
    // 判断容器大小控制词云位置
    const el = this.$refs.wordCloud;
    return {
    leftPos: {
    // 容器左侧的位置和顶部位置
    x: el.offsetLeft,
    y: el.offsetTop
    },
    rightPos: {
    // 容器右侧的位置和底部位置
    x: el.offsetLeft + el.offsetWidth,
    y: el.offsetTop + el.offsetHeight
    }
    };
    }


    • 我们一开始先用initWordPos来计算每个词语现在处于的位置并把它的位置保存起来,再使用getContainerSize获取我们的外部容器的最左侧最右侧最上最下的位置保存起来。

    • 给我们每个span添加一个属性direction 方向,当方向为负数则往左,方向为正则往右,注释我写在代码上了,大家如果不清除可以看一下。

    • 也就是说我们的词云会在容器里面反复横跳,那我们来看看效果。


    ciyun3.gif


    随机位移


    • 很不错,是我们想要的效果!!!

    • 当然我们每次位移不可能写死只位移1px我们要做到那种凌乱美,那就需要做一个随机位移。

    • 那怎么来做随机位移呢?可以看出我们的词语其实是在做匀速直线运动而匀速直线运动的公式大家还记得吗?

    • 如果不记得的话这边建议回去翻一下物理书~ 匀速直线运动的位移公式是 x=vt

    • 这个x就是我们需要的位移,而这个t我们就不用管了因为我上面也说了这个requestAnimationFrame会帮助我们设置时间,那我们只需要控制这个v初速度是随机的就可以了。


    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    ...
    spanDom.local = {
    velocity: {
    // 每次位移初速度
    x: -0.5 + Math.random(),
    y: -0.5 + Math.random()
    },
    };
    ...
    });
    this.wordArr = wordArr;
    },
    wordFly () {
    this.wordArr.forEach((value) => {
    ...
    //利用公式 x=vt
    value.local.position.x += value.local.velocity.x * value.local.direction.x;
    ...
    });
    },


    • 我们给每个词语span元素一个初速度,这个初速度可以为- 也可以为+代表向左或者向右移动,当我们处理这个translateX的时候他就会随机处理了,现在我们来看看效果。


    ciyun4.gif


    完善y轴


    • 现在x轴已经按照我们想的所完成了,想让词云们上下左右都动起来那么我们需要按照x轴的方法来配一下y轴即可。

    • 由于代码长度问题我就不放出来啦,我下面会给出源码,大家有兴趣可以去下载看看~我们直接来看看成品!小卢感谢您的阅读,那我就在这里祝您


    ciyun5.gif



    • 至此一个简单的词云动画就完啦,具体源码我放在这里。

    链接:https://juejin.cn/post/7000300247947673630

    收起阅读 »

    贝塞尔曲线在前端,走近她,然后爱上她

    贝塞尔曲线在前端 css3的动画主要是 transition animation transition有transition-timing-function animation有animation-timing-function 以transition-t...
    继续阅读 »

    贝塞尔曲线在前端


    css3的动画主要是



    • transition

    • animation


    transition有transition-timing-function

    animation有animation-timing-function


    transition-timing-function为例


    image.png


    其内置 ease,linear,ease-in,ease-out,ease-in-out就是贝塞尔曲线函数, 作用是控制属性变化的速度。

    也可以自定义cubic-bizier(x1,y1,x2,y2), 这个嘛玩意呢,三阶贝塞尔曲线, x1,y1x2,y2是两个控制点。


    如图:
    x1, y1对应 P1点, x2,y2 对应P2点。

    要点:



    1. 曲线越陡峭,速度越快,反之,速度越慢!

    2. 控制点的位置会影响曲线形状


    image.png




    说道这里, 回想一下我们前端在哪些地方还会贝塞尔呢。



    • svg

    • canvas/webgl

    • css3 动画

    • animation Web API

      千万别以为JS就不能操作CSS3动画了


    这样说可能有些空洞,我们一起来看看曲线和实际的动画效果:

    红色ease和ease-out曲线前期比较陡峭,加速度明显比较快


    图片.png


    贝塞尔曲线运动-演示地址
    6af390fc619a4f1f8758a437d03e37c4~tplv-k3u1fbpfcp-watermark.image.gif




    什么是贝赛尔曲线


    贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。


    公式怎么理解呢?这里你可以假定



    • P0的坐标(0,0), 最终的点的坐标为(1,1)


    t从0不断的增长到1

    t的值和控制点的x坐标套入公式,得到一个新的x坐标值

    t的值和控制点的y坐标套入公式,得到一个新的y坐标值


    (新的x坐标值 , 新的y坐标值)坐标就是t时刻曲线的点的坐标。


    通用公式


    image.png


    线性公式


    无控制点,直线


    image.png


    二次方公式


    一个控制点


    image.png


    三次方公式


    两个控制点


    image.png


    这是我们的重点,因为css动画都是三次方程式


    P0作为起点,P3作为终点, 控制点是P1与P2, 因为我们一般会假定 P0 为 (0,0), 而 P3为(1,1)。


    控制点的变化,会影响整个曲线,我们一起来简单封装一下并进行实例操作。


    一阶二阶三阶封装


    我们基于上面公式的进行简单的封装,

    你传入需要的点数量和相应的控制点就能获得相应一组点的信息。


    class Bezier {
    getPoints(count = 100, ...points) {
    const len = points.length;
    if (len < 2 || len > 4) {
    throw new Error("参数points的长度应该大于等于2小于5");
    }
    const fn =
    len === 2
    ? this.firstOrder
    : len === 3
    ? this.secondOrder
    : this.thirdOrder;
    const retPoints = [];
    for (let i = 0; i < count; i++) {
    retPoints.push(fn.call(null, i / count, ...points));
    }
    return retPoints;
    }

    firstOrder(t, p0, p1) {
    const { x: x0, y: y0 } = p0;
    const { x: x1, y: y1 } = p1;
    const x = (x1 - x0) * t;
    const y = (y1 - y0) * t;
    return { x, y };
    }

    secondOrder(t, p0, p1, p2) {
    const { x: x0, y: y0 } = p0;
    const { x: x1, y: y1 } = p1;
    const { x: x2, x: y2 } = p2;
    const x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2;
    const y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2;
    return { x, y };
    }

    thirdOrder(t, p0, p1, p2, p3) {
    const { x: x0, y: y0 } = p0;
    const { x: x1, y: y1 } = p1;
    const { x: x2, y: y2 } = p2;
    const { x: x3, y: y3 } = p3;
    let x =
    x0 * Math.pow(1 - t, 3) +
    3 * x1 * t * (1 - t) * (1 - t) +
    3 * x2 * t * t * (1 - t) +
    x3 * t * t * t;
    let y =
    y0 * (1 - t) * (1 - t) * (1 - t) +
    3 * y1 * t * (1 - t) * (1 - t) +
    3 * y2 * t * t * (1 - t) +
    y3 * t * t * t;
    return { x, y };
    }
    }

    export default new Bezier();


    演示地址: xiangwenhu.github.io/juejinBlogs…


    一阶贝塞尔是一条直线:

    image.png


    二阶贝塞尔一个控制点:


    image.png


    三阶贝塞尔两个控制点:


    image.png


    贝塞尔曲线控制点


    回到最开始, animation和 transition都可以自定义三阶贝塞尔函数, 而需要的就是两个控制点的信息怎么通过测试曲线获得控制点呢?


    在线取三阶贝塞尔关键的方案早就有了。



    在线贝塞尔

    在线贝塞尔2



    但是不妨碍我自己去实现一个简单,加强理解。

    大致的实现思路



    • canvas 绘制效果

      canvas有bezierCurveTo方法,直接可以绘制贝塞尔曲线

    • 两个控制点用dom元素来显示


    逻辑



    • 点击时计算最近的点,同时修改最近点的坐标

    • 重绘


    当然这只是一个简单的版本。


    演示地址: xiangwenhu.github.io/juejinBlogs…

    截图:


    有了这个,你就可以通过曲线获得控制点了, 之前提到过,曲线的陡峭决定了速度的快慢,是不是很有用呢?


    当然,你可以自己加个贝塞尔的直线运动,查看实际的运动效果,其实都不难,难的是你不肯动手!!!


    链接:https://juejin.cn/post/7000525748578549774

    收起阅读 »

    高级IOS开发进阶 - 自旋锁、互斥锁以及读写锁!(二)

    4.3 源码分析initWithCondition:保存了condition参数以及NSCondition的创建。lockWhenCondition:open func lock(whenCondition condition: Int) { let ...
    继续阅读 »

    4.3 源码分析

    • initWithCondition




    • 保存了condition参数以及NSCondition的创建。

    • lockWhenCondition

    open func lock(whenCondition condition: Int) {
    let _ = lock(whenCondition: condition, before: Date.distantFuture)
    }

    内部调用了lockWhenCondition: before:,默认值传的Date.distantFuture

    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
    _cond.lock()
    while _thread != nil || _value != condition {
    if !_cond.wait(until: limit) {
    _cond.unlock()
    return false
    }
    }
    _thread = pthread_self()
    _cond.unlock()
    return true
    }

    NSCondition加锁判断condition条件是否满足,不满足调用NSConditionwait waitUntilDate方法进入等待,超时后解锁返回false。满足的情况下赋值_thread解锁返回true

    • unlockWithCondition
    open func unlock(withCondition condition: Int) {
    _cond.lock()
    _thread = nil
    _value = condition
    _cond.broadcast()
    _cond.unlock()
    }

    加锁后释放_thread,更新condition,调用broadcast后解锁。

    • lock
    open func lock() {
    let _ = lock(before: Date.distantFuture)
    }

    open func lock(before limit: Date) -> Bool {
    _cond.lock()
    while _thread != nil {
    if !_cond.wait(until: limit) {
    _cond.unlock()
    return false
    }
    }
    _thread = pthread_self()
    _cond.unlock()
    return true
    }

    判断是否有其它任务阻塞,没有阻塞直接创建_thread返回true

    • unlock
    open func unlock() {
    _cond.lock()
    _thread = nil
    _cond.broadcast()
    _cond.unlock()
    }

    广播并且释放_thread

    4.4 反汇编分析

    initWithCondition



    • lockWhenCondition

    -(int)lockWhenCondition:(int)arg2 {
    r0 = [arg0 lockWhenCondition:arg2 beforeDate:[NSDate distantFuture]];
    return r0;
    }

    调用自己的lockWhenCondition: beforeDate :


    unlockWithCondition


    -(int)unlockWithCondition:(int)arg2 {
    r0 = object_getIndexedIvars(arg0);
    [*r0 lock];
    *(int128_t *)(r0 + 0x8) = 0x0;
    *(int128_t *)(r0 + 0x10) = arg2;
    [*r0 broadcast];
    r0 = *r0;
    r0 = [r0 unlock];
    return r0;
    }
    • lock
    int -[NSConditionLock lock](int arg0) {
    r0 = [arg0 lockBeforeDate:[NSDate distantFuture]];
    return r0;
    }
    lockBeforeDate



    unlock

    int -[NSConditionLock unlock]() {
    r0 = object_getIndexedIvars(r0);
    [*r0 lock];
    *(r0 + 0x8) = 0x0;
    [*r0 broadcast];
    r0 = *r0;
    r0 = [r0 unlock];
    return r0;
    }

    汇编、源码以及断点调试逻辑相同。
    NSConditionLock 内部封装了NSCondition。

    五、OSSpinLock & os_unfair_lock




    OSSpinLockAPI注释以及它自己的命名说明了这是一把自旋锁,自iOS10之后被os_unfair_lock替代。



    • os_unfair_lock必须以OS_UNFAIR_LOCK_INIT初始化。
    • 它是用来代替OSSpinLock的。
    • 它不是自旋锁(忙等),是被内核唤醒的(闲等)。



    可以看到这两个锁都是定义在libsystem_platform.dylib中的。可以在openSource中找到他们libplatform的源码libplatform,实现是在/src/os目录下的lock.c文件中。

    5.1 OSSpinLock 源码分析

    OSSpinLock的使用一般会用到以下API

    OSSpinLock hp_spinlock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&hp_spinlock);
    OSSpinLockUnlock(&hp_spinlock);
    OSSpinLock
    typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);

    #define OS_SPINLOCK_INIT 0

    OSSpinLock本身是一个int32_t类型的值,初始化默认值为0

    5.1.1 OSSpinLockLock

    void
    OSSpinLockLock(volatile OSSpinLock *l)
    {
    OS_ATOMIC_ALIAS(spin_lock, OSSpinLockLock);
    OS_ATOMIC_ALIAS(_spin_lock, OSSpinLockLock);
    bool r = os_atomic_cmpxchg(l, 0, _OSSpinLockLocked, acquire);
    if (likely(r)) return;
    return _OSSpinLockLockSlow(l);
    }

    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    static const OSSpinLock _OSSpinLockLocked = 1;
    #else
    static const OSSpinLock _OSSpinLockLocked = -1;
    #endif

    OS_ATOMIC_ALIAS定义如下:

    #undef OS_ATOMIC_ALIAS
    #define OS_ATOMIC_ALIAS(n, o)
    static void _OSSpinLockLock(volatile OSSpinLock *l);

    这里相当于分了两条路径,通过_OSSpinLockLocked标记是否被锁定。在源码中并没有找到_OSSpinLockLock函数的实现。

    5.1.1.1 _OSSpinLockLockSlow

    #if OS_ATOMIC_UP
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    return _OSSpinLockLockYield(l); // Don't spin on UP
    }
    #elif defined(__arm64__)
    // Exclusive monitor must be held during WFE <rdar://problem/22300054>
    #if defined(__ARM_ARCH_8_2__)
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    uint32_t tries = OS_LOCK_SPIN_SPIN_TRIES;
    OSSpinLock lock;
    _spin:
    while (unlikely(lock = os_atomic_load_exclusive(l, relaxed))) {
    if (unlikely(lock != _OSSpinLockLocked)) {
    os_atomic_clear_exclusive();
    return _os_lock_corruption_abort((void *)l, (uintptr_t)lock);
    }
    if (unlikely(!tries--)) {
    os_atomic_clear_exclusive();
    return _OSSpinLockLockYield(l);
    }
    OS_LOCK_SPIN_PAUSE();
    }
    os_atomic_clear_exclusive();
    bool r = os_atomic_cmpxchg(l, 0, _OSSpinLockLocked, acquire);
    if (likely(r)) return;
    goto _spin;
    }
    #else // !__ARM_ARCH_8_2__
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    uint32_t tries = OS_LOCK_SPIN_SPIN_TRIES;
    OSSpinLock lock;
    os_atomic_rmw_loop(l, lock, _OSSpinLockLocked, acquire, if (unlikely(lock)){
    if (unlikely(lock != _OSSpinLockLocked)) {
    os_atomic_rmw_loop_give_up(return
    _os_lock_corruption_abort((void *)l, (uintptr_t)lock));
    }
    if (unlikely(!tries--)) {
    os_atomic_rmw_loop_give_up(return _OSSpinLockLockYield(l));
    }
    OS_LOCK_SPIN_PAUSE();
    continue;
    });
    }
    #endif // !__ARM_ARCH_8_2__
    #else // !OS_ATOMIC_UP
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    uint32_t tries = OS_LOCK_SPIN_SPIN_TRIES;
    OSSpinLock lock;
    while (unlikely(lock = *l)) {
    _spin:
    if (unlikely(lock != _OSSpinLockLocked)) {
    return _os_lock_corruption_abort((void *)l, (uintptr_t)lock);
    }
    if (unlikely(!tries--)) return _OSSpinLockLockYield(l);
    OS_LOCK_SPIN_PAUSE();
    }
    bool r = os_atomic_cmpxchgv(l, 0, _OSSpinLockLocked, &lock, acquire);
    if (likely(r)) return;
    goto _spin;
    }
    #endif // !OS_ATOMIC_UP

    可以看到内部有自转逻辑,这里直接分析_OSSpinLockLockYield

    5.1.1.2 _OSSpinLockLockYield

    static void
    _OSSpinLockLockYield(volatile OSSpinLock *l)
    {
    int option = SWITCH_OPTION_DEPRESS;
    mach_msg_timeout_t timeout = 1;
    uint64_t deadline = _os_lock_yield_deadline(timeout);
    OSSpinLock lock;
    while (unlikely(lock = *l)) {
    _yield:
    if (unlikely(lock != _OSSpinLockLocked)) {
    _os_lock_corruption_abort((void *)l, (uintptr_t)lock);
    }
    thread_switch(MACH_PORT_NULL, option, timeout);
    if (option == SWITCH_OPTION_WAIT) {
    timeout++;
    } else if (!_os_lock_yield_until(deadline)) {
    option = SWITCH_OPTION_WAIT;
    }
    }
    bool r = os_atomic_cmpxchgv(l, 0, _OSSpinLockLocked, &lock, acquire);
    if (likely(r)) return;
    goto _yield;
    }

    内部有超时时间以及线程切换逻辑。

    5.1.2 OSSpinLockUnlock

    void
    OSSpinLockUnlock(volatile OSSpinLock *l)
    {
    OS_ATOMIC_ALIAS(spin_unlock, OSSpinLockUnlock);
    OS_ATOMIC_ALIAS(_spin_unlock, OSSpinLockUnlock);
    return _os_nospin_lock_unlock((_os_nospin_lock_t)l);
    }

    内部调用了_os_nospin_lock_unlock

    5.1.2.1 _os_nospin_lock_unlock

    void
    _os_nospin_lock_unlock(_os_nospin_lock_t l)
    {
    os_lock_owner_t self = _os_lock_owner_get_self();
    os_ulock_value_t current;
    current = os_atomic_xchg(&l->oul_value, OS_LOCK_NO_OWNER, release);
    if (likely(current == self)) return;
    return _os_nospin_lock_unlock_slow(l, current);
    }

    _os_nospin_lock_unlock_slow

    static void
    _os_nospin_lock_unlock_slow(_os_nospin_lock_t l, os_ulock_value_t current)
    {
    os_lock_owner_t self = _os_lock_owner_get_self();
    if (unlikely(OS_ULOCK_OWNER(current) != self)) {
    return; // no unowned_abort for drop-in compatibility with OSSpinLock
    }
    if (current & OS_ULOCK_NOWAITERS_BIT) {
    __LIBPLATFORM_INTERNAL_CRASH__(current, "unlock_slow with no waiters");
    }
    for (;;) {
    int ret = __ulock_wake(UL_COMPARE_AND_WAIT | ULF_NO_ERRNO, l, 0);
    if (unlikely(ret < 0)) {
    switch (-ret) {
    case EINTR:
    continue;
    case ENOENT:
    break;
    default:
    __LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wake failure");
    }
    }
    break;
    }
    }

    5.2 os_unfair_lock 源码分析

    typedef struct os_unfair_lock_s {
    uint32_t _os_unfair_lock_opaque;
    } os_unfair_lock, *os_unfair_lock_t;

    初始化OS_UNFAIR_LOCK_INIT直接设置了默认值0

    #define OS_UNFAIR_LOCK_INIT ((os_unfair_lock){0})

    5.2.1 os_unfair_lock_lock


    void
    os_unfair_lock_lock(os_unfair_lock_t lock)
    {
    _os_unfair_lock_t l = (_os_unfair_lock_t)lock;
    os_lock_owner_t self = _os_lock_owner_get_self();
    bool r = os_atomic_cmpxchg(&l->oul_value, OS_LOCK_NO_OWNER, self, acquire);
    if (likely(r)) return;
    return _os_unfair_lock_lock_slow(l, OS_UNFAIR_LOCK_NONE, self);
    }

    _os_lock_owner_get_self


    OS_ALWAYS_INLINE OS_CONST
    static inline os_lock_owner_t
    _os_lock_owner_get_self(void)
    {
    os_lock_owner_t self;
    self = (os_lock_owner_t)_os_tsd_get_direct(__TSD_MACH_THREAD_SELF);
    return self;
    }

    _os_unfair_lock_lock_slow

    static void
    _os_unfair_lock_lock_slow(_os_unfair_lock_t l,
    os_unfair_lock_options_t options, os_lock_owner_t self)
    {
    os_unfair_lock_options_t allow_anonymous_owner =
    options & OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    options &= ~OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    if (unlikely(options & ~OS_UNFAIR_LOCK_OPTIONS_MASK)) {
    __LIBPLATFORM_CLIENT_CRASH__(options, "Invalid options");
    }
    os_ulock_value_t current, new, waiters_mask = 0;
    while (unlikely((current = os_atomic_load(&l->oul_value, relaxed)) !=
    OS_LOCK_NO_OWNER)) {
    _retry:
    if (unlikely(OS_ULOCK_IS_OWNER(current, self, allow_anonymous_owner))) {
    return _os_unfair_lock_recursive_abort(self);
    }
    new = current & ~OS_ULOCK_NOWAITERS_BIT;
    if (current != new) {
    // Clear nowaiters bit in lock value before waiting
    if (!os_atomic_cmpxchgv(&l->oul_value, current, new, &current,
    relaxed)){
    continue;
    }
    current = new;
    }
    int ret = __ulock_wait(UL_UNFAIR_LOCK | ULF_NO_ERRNO | options,
    l, current, 0);
    if (unlikely(ret < 0)) {
    switch (-ret) {
    case EINTR:
    case EFAULT:
    continue;
    case EOWNERDEAD:
    _os_unfair_lock_corruption_abort(current);
    break;
    default:
    __LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wait failure");
    }
    }
    if (ret > 0) {
    // If there are more waiters, unset nowaiters bit when acquiring lock
    waiters_mask = OS_ULOCK_NOWAITERS_BIT;
    }
    }
    new = self & ~waiters_mask;
    bool r = os_atomic_cmpxchgv(&l->oul_value, OS_LOCK_NO_OWNER, new,
    &current, acquire);
    if (unlikely(!r)) goto _retry;
    }

    内部是wait等待逻辑。

    5.2.2 os_unfair_lock_unlock

    void
    os_unfair_lock_unlock(os_unfair_lock_t lock)
    {
    _os_unfair_lock_t l = (_os_unfair_lock_t)lock;
    os_lock_owner_t self = _os_lock_owner_get_self();
    os_ulock_value_t current;
    current = os_atomic_xchg(&l->oul_value, OS_LOCK_NO_OWNER, release);
    if (likely(current == self)) return;
    return _os_unfair_lock_unlock_slow(l, self, current, 0);
    }

    内部调用了_os_unfair_lock_unlock_slow

    OS_NOINLINE
    static void
    _os_unfair_lock_unlock_slow(_os_unfair_lock_t l, os_lock_owner_t self,
    os_ulock_value_t current, os_unfair_lock_options_t options)
    {
    os_unfair_lock_options_t allow_anonymous_owner =
    options & OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    options &= ~OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    if (unlikely(OS_ULOCK_IS_NOT_OWNER(current, self, allow_anonymous_owner))) {
    return _os_unfair_lock_unowned_abort(OS_ULOCK_OWNER(current));
    }
    if (current & OS_ULOCK_NOWAITERS_BIT) {
    __LIBPLATFORM_INTERNAL_CRASH__(current, "unlock_slow with no waiters");
    }
    for (;;) {
    int ret = __ulock_wake(UL_UNFAIR_LOCK | ULF_NO_ERRNO, l, 0);
    if (unlikely(ret < 0)) {
    switch (-ret) {
    case EINTR:
    continue;
    case ENOENT:
    break;
    default:
    __LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wake failure");
    }
    }
    break;
    }
    }

    可以看到内部是有唤醒逻辑的。

    六、读写锁

    读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。要实现读写锁核心逻辑是:

    • 多读单写
    • 写写互斥
    • 读写互斥
    • 写不能阻塞任务执行

    有两套方案:

    • 1.使用 栅栏函数 相关API
    • 2.使用pthread_rwlock_t相关API

    6.1 dispatch_barrier_async 实现多读单写

    写:通过栅栏函数可以实现写写互斥以及读写互斥,写使用async可以保证写逻辑不阻塞当前任务执行。
    读:使用dispatch_sync同步效果实现多读(放入并发队列中)。

    • 首先定义一个并发队列以及字典存储数据:
    @property (nonatomic, strong) dispatch_queue_t concurrent_queue;
    @property (nonatomic, strong) NSMutableDictionary *dataDic;

    //初始化
    self.concurrent_queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    self.dataDic = [NSMutableDictionary dictionary];
    • 写入操作:
    - (void)safeSetter:(NSString *)name time:(int)time {
    dispatch_barrier_async(self.concurrent_queue, ^{
    sleep(time);
    [self.dataDic setValue:name forKey:@"HotpotCat"];
    NSLog(@"write name:%@,currentThread:%@",name,[NSThread currentThread]);
    });
    }

    为了方便测试key值写死,并且传入一个timebarrier保证了写之间互斥以及读写互斥。

    • 读取操作:
    - (NSString *)safeGetterWithTime:(int)time {
    __block NSString *result;
    //多条线程同时读,阻塞的是当前线程,多条线程访问就是多读了。同步使用concurrent_queue是为了配合栅栏函数读写互斥。
    dispatch_sync(self.concurrent_queue, ^{
    sleep(time);
    result = self.dataDic[@"HotpotCat"];
    });
    NSLog(@"result:%@,currentThread:%@,time:%@",result,[NSThread currentThread],@(time));
    return result;
    }

    使用同步函数配合栅栏函数(栅栏函数只能针对同一队列)实现读写互斥,当多条线程同时访问safeGetterWithTime时就实现了多读操作。

    • 写入验证:
    //调用
    [self safeSetter:@"1" time:4];
    [self safeSetter:@"2" time:1];
    [self safeSetter:@"3" time:2];
    [self safeSetter:@"4" time:1];

    输出:

    write name:1,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}
    write name:2,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}
    write name:3,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}
    write name:4,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}

    很明显写之间是互斥的,任务1没有执行完之前其它任务都在等待。


    • 读取验证:
    for (int i = 0; i < 5; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSString *result = [self safeGetterWithTime:5 - i];
    NSLog(@"result:%@",result);
    });
    }

    输出:


    result:4,currentThread:<NSThread: 0x281f80600>{number = 7, name = (null)},time:1
    result:4,currentThread:<NSThread: 0x281fce540>{number = 8, name = (null)},time:2
    result:4,currentThread:<NSThread: 0x281f80980>{number = 9, name = (null)},time:3
    result:4,currentThread:<NSThread: 0x281feb540>{number = 10, name = (null)},time:4
    result:4,currentThread:<NSThread: 0x281f80a80>{number = 11, name = (null)},time:5

    任务并行执行,顺序是由于设置了sleep时间,如果去掉时间或者时间一致,每次执行结果都不同了。

    6.2 pthread_rwlock_t 实现多读单写

    • 定义锁以及字典数据:
    {
    pthread_rwlock_t rw_lock;
    pthread_rwlockattr_t rw_lock_attr;
    }
    @property (nonatomic, strong) NSMutableDictionary *dataDic;

    pthread_rwlockattr_t读写属性有两种:lockkindpshared
    lockkind:读写策略,包括读取优先(默认属性)、写入优先。苹果系统里面没有提供 pthread_rwlockattr_getkind_np 与 pthread_rwlockattr_setkind_np 相关函数。
    psharedPTHREAD_PROCESS_PRIVATE(进程内竞争读写锁,默认属性)PTHREAD_PROCESS_SHARED(进程间竞争读写锁)

    • 初始化:
    self.dataDic = [NSMutableDictionary dictionary];
    //初始化
    pthread_rwlockattr_init(&rw_lock_attr);
    pthread_rwlock_init(&rw_lock, &rw_lock_attr);
    //进程内
    pthread_rwlockattr_setpshared(&rw_lock_attr, PTHREAD_PROCESS_PRIVATE);
    • 写入操作如下:
    - (void)safeSetter:(NSString *)name {
    //写锁
    pthread_rwlock_wrlock(&rw_lock);
    [self.dataDic setValue:name forKey:@"HotpotCat"];
    NSLog(@"write name:%@,currentThread:%@",name,[NSThread currentThread]);
    //释放
    pthread_rwlock_unlock(&rw_lock);
    }
    • 读取操作如下:
    - (NSString *)safeGetter {
    //读锁
    pthread_rwlock_rdlock(&rw_lock);
    NSString *result = self.dataDic[@"HotpotCat"];
    //释放
    pthread_rwlock_unlock(&rw_lock);
    NSLog(@"result:%@,currentThread:%@",result,[NSThread currentThread]);
    return result;
    }
    • 写入验证:
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"1"];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"2"];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"3"];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"4"];
    });

    输出:

    LockDemo[52251:5873172] write name:4,currentThread:<NSThread: 0x60000072e980>{number = 4, name = (null)}
    LockDemo[52251:5873177] write name:1,currentThread:<NSThread: 0x60000075d100>{number = 6, name = (null)}
    LockDemo[52251:5873170] write name:2,currentThread:<NSThread: 0x60000072f600>{number = 7, name = (null)}
    LockDemo[52251:5873178] write name:3,currentThread:<NSThread: 0x60000073d480>{number = 5, name = (null)}

    这里就与队列调度有关了,顺序不定,如果不加锁大量并发调用下则会crash

    • 读取验证:
    for (int i = 0; i < 5; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSString *result = [self safeGetter];
    });
    }

    输出:

    result:4,currentThread:<NSThread: 0x600001cdc200>{number = 5, name = (null)}
    result:4,currentThread:<NSThread: 0x600001cd1080>{number = 7, name = (null)}
    result:4,currentThread:<NSThread: 0x600001c95f40>{number = 6, name = (null)}
    result:4,currentThread:<NSThread: 0x600001c91ec0>{number = 3, name = (null)}
    result:4,currentThread:<NSThread: 0x600001c94d80>{number = 4, name = (null)}

    输出顺序也不一定。当然混合读写测试也可以,用数组更容易测试。


  • 对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。

  • 获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此对于某个给定资源的共享访问也称为共享-独占上锁






  • 作者:HotPotCat
    链接:https://www.jianshu.com/p/8f8e5f0d0b23





    收起阅读 »

    高级IOS开发进阶 - 自旋锁、互斥锁以及读写锁(一)

    一、锁的分类在分析其它锁之前,需要先区分清楚锁的区别,基本的锁包括了二类:互斥锁 和 自旋锁。1.1 自旋锁自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种 忙等。一旦获取了自旋锁,线程会一直保持该锁...
    继续阅读 »

    一、锁的分类

    在分析其它锁之前,需要先区分清楚锁的区别,基本的锁包括了二类:互斥锁 和 自旋锁

    1.1 自旋锁

    自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种 忙等。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

    自旋锁 = 互斥锁 + 忙等OSSpinLock就是自旋锁。

    1.2 互斥锁

    互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
    Posix Thread中定义有一套专⻔用于线程同步的mutex函数,mutex用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒(闲等)。

    创建和销毁:

    • POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁。
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
    • pthread_mutex_destroy ()用于注销一个互斥锁。

    锁操作相关API

     int pthread_mutex_lock(pthread_mutex_t *mutex)
    int pthread_mutex_unlock(pthread_mutex_t *mutex)
    int pthread_mutex_trylock(pthread_mutex_t *mutex)
    • pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

    互斥锁 分为 递归锁 和 非递归锁

    • 递归锁:
      @synchronized:多线程可递归。
      NSRecursiveLock:不支持多线程可递归。
      pthread_mutex_t(recursive):多线程可递归。
    • 非递归锁:NSLockpthread_mutexdispatch_semaphoreos_unfair_lock
    • 条件锁:NSConditionNSConditionLock
    • 信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是
      semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实
      现更加复杂的同步,而不单单是线程间互斥。dispatch_semaphore

    1.2.1 读写锁

    读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁 相对于自旋锁而言,能提高并发性。因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者,在读写锁保持期间也是抢占失效的。

    如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

    一次只有一个线程可以占有写模式的读写锁,可以有多个线程同时占有读模式的读写锁。正是因为这个特性,当读写锁是写加锁状态时,在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。
    通常当读写锁处于读模式锁住状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁⻓期占用而导致等待的写模式锁请求⻓期阻塞。
    读写锁适合于对数据结构的读次数比写次数多得多的情况。 因为读模式锁定可以共享,写模式锁住时意味着独占,所以读写锁又叫共享-独占锁

    创建和销毁API

    #include <pthread.h>
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

    成功则返回0, 出错则返回错误编号。

    同互斥锁一样, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作,释放由init分配的资源。

    锁操作相关API:

    #include <pthread.h>
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    成功则返回0, 出错则返回错误编号。这3个函数分别实现获取读锁,获取写锁和释放锁的操作。获取锁的两个函数是阻塞操作,同样非阻塞的函数为:

    #include <pthread.h>
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

    非阻塞的获取锁操作,如果可以获取则返回0, 否则返回错误的EBUSY

    二、NSLock & NSRecursiveLock 的应用以及原理

    2.1 案例一

    __block NSMutableArray *array;
    for (int i = 0; i < 10000; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    array = [NSMutableArray array];
    });
    }

    对于上面的代码运行会发生崩溃,常规处理是对它加一个锁,如下:

    __block NSMutableArray *array;
    self.lock = [[NSLock alloc] init];
    for (int i = 0; i < 10000; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self.lock lock];
    array = [NSMutableArray array];
    [self.lock unlock];
    });
    }

    这样就能解决array的创建问题了。

    2.2 案例二

    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    };
    testMethod(10);
    });
    }

    上面的例子中最终输出会错乱:



    可以在block调用前后加解锁解决:

    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    };
    [self.lock lock];
    testMethod(10);
    [self.lock unlock];
    });
    }

    但是在实际开发中锁往往是与业务代码绑定在一起的,如下:


    这个时候block在执行前会同一时间进入多次,相当于多次加锁了(递归),这样就产生了死锁。NSLog只会执行一次。

    NSLock改为NSRecursiveLock可以解决NSLock存在的死锁问题:


    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    [self.recursiveLock lock];
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    [self.recursiveLock unlock];
    };
    testMethod(10);
    });
    }

    但是在执行testMethod一次(也有可能是多次)递归调用后没有继续输出:



    由于NSRecursiveLock不支持多线程可递归。所以改为@synchronized

        for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    @synchronized (self) {
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    }
    };
    testMethod(10);
    });
    }

    就能完美解决问题了。

    NSRecursiveLock 解决了 NSLock 递归问题,@synchronized 解决了 NSRecursiveLock 多线程可递归问题问题。

    2.3 原理分析

    NSLockNSRecursiveLock是定义在Foundation框架中的,Foundation框架并没有开源。有三种方式来探索:

    • 分析Foundation动态库的汇编代码。
    • 断点跟踪加锁解锁流程。
    • Swift Foundation源码分析。虽然Foundation框架本身没有,但是苹果开源了Swift Foundation的代码。原理是想通的。swift-corelibs-foundation

    当然有兴趣可以尝试编译可运行版本进行调试 swift-foundation 源码编译

    FoundationlockunlockNSLocking协议提供的方法:


    @protocol NSLocking
    - (void)lock;
    - (void)unlock;
    @end

    Swift Foundation源码中同样有NSLocking协议:

    public protocol NSLocking {
    func lock()
    func unlock()
    }

    2.3.1 NSLock 源码分析



    底层是对pthread_mutex_init的封装。lockunlock同样是对pthread_mutex_lockpthread_mutex_unlock的封装:



    通过宏定义可以看到Swift的跨平台支持。

    2.3.2 NSRecursiveLock 源码分析



    内部是对PTHREAD_MUTEX_RECURSIVE的封装。lockunlock同样是对pthread_mutex_lockpthread_mutex_unlock的封装。

    三、NSCondition 原理

    NSCondition实际上作为一个  和一个 线程检查器。锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

    • [condition lock]:一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock后才可访问。
    • [condition unlock]:与lock同时使用。
    • [condition wait]:让当前线程处于等待状态。
    • [condition signal]CPU发信号告诉线程不用在等待,可以继续执行。

    3.1 生产者-消费者 案例

    - (void)testNSCondition {
    //创建生产-消费者
    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_producer];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_consumer];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_consumer];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_producer];
    });
    }
    }

    - (void)test_producer{
    [self.condition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产 + 1 剩余: %zd",self.ticketCount);
    [self.condition signal]; // 信号
    [self.condition unlock];
    }

    - (void)test_consumer{
    [self.condition lock];
    if (self.ticketCount == 0) {
    NSLog(@"等待 剩余: %zd",self.ticketCount);
    [self.condition wait];
    }
    //消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费 - 1 剩余: %zd ",self.ticketCount);
    [self.condition unlock];
    }

    输出:

    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    生产 + 1 剩余: 2
    消费 - 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    生产 + 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    消费 - 1 剩余: 1
    生产 + 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    消费 - 1 剩余: 1
    消费 - 1 剩余: 0

    因为有condition的存在保证了消费行为是在对应的生产行为之后。在这个过程中会有消费等待行为,signal信号通知消费。

    • 生产和消费的加锁保证了各个事务的额安全。
    • waitsignal保证了事务之间的安全。

    3.2 源码分析




    内部也是对pthread_mutex_init的包装,多了一个pthread_cond_init

    open func lock() {
    pthread_mutex_lock(mutex)
    }

    open func unlock() {
    pthread_mutex_unlock(mutex)
    }

    open func wait() {
    pthread_cond_wait(cond, mutex)
    }

    open func signal() {
    pthread_cond_signal(cond)
    }

    open func broadcast() {
    pthread_cond_broadcast(cond)
    }

    代码中去掉了windows相关宏逻辑:

    • NSCondition:锁(pthread_mutex_t) + 线程检查器(pthread_cond_t
    • 锁(pthread_mutex_t):lock(pthread_mutex_lock) + unlock(pthread_mutex_unlock)
    • 线程检查器(pthread_cond_t):wait(pthread_cond_wait) + signal(pthread_cond_signal)

    四、NSConditionLock 使用和原理

    NSConditionLock也是锁,一旦一个线程获得锁,其他线程一定等待。它同样遵循NSLocking协议,相关API:

    - (void)lockWhenCondition:(NSInteger)condition;
    - (void)unlockWithCondition:(NSInteger)condition;
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    • [conditionLock lock]:表示 conditionLock 期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁。
    • [conditionLock lockWhenCondition:A条件]:表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
    • [conditionLock unlockWithCondition:A条件]: 表示释放锁,同时把内部的condition设置为A条件。
    • return = [conditionLock lockWhenCondition:A条件 beforeDate:A时间]: 表示如果被锁定(没获得锁),并超过该时间则不再阻塞线程。注意:返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理。
    • 所谓的condition就是整数,内部通过整数比较条件。

    4.1案例

    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [conditionLock lockWhenCondition:1];
    NSLog(@"1");
    [conditionLock unlockWithCondition:0];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    [conditionLock lockWhenCondition:2];
    sleep(1);
    NSLog(@"2");
    [conditionLock unlockWithCondition:1];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [conditionLock lock];
    NSLog(@"3");
    [conditionLock unlock];
    });

    上面的案例2一定比1先执行,23之间无序。
    输出:3 2 1,如果任务2的优先级改为High则输出顺序变为2 1 3

    那么有以下疑问:

    • 1.NSConditionLock 与 NSCondition有关系么?
    • 2.NSConditionLock初始化的时候condition是什么?
    • 3.lockWhenCondition是如何控制的?
    • 4.unlockWithCondition是如何控制的?

    4.2 断点调试分析逻辑

    在拿不到源码以及拿不到动态库的情况下,断点分析调用流程是一个比较好的方案。
    分别在测试代码中打下以下断点:



    运行工程到达断点后下符号断点-[NSConditionLock initWithCondition:]过掉断点:



    这个时候就进入了initWithCondition的汇编实现。在汇编中对所有的b(跳转指令)下断点配合寄存器的值跟踪流程。

    • -[NSConditionLock initWithCondition:]:


    • 可以通过lldb读取寄存器的值,也可以查看全部寄存器中对应的值。

    过掉断点继续:






    最终返回了创建的NSConditionLock对象,它持有NSCondition对象以及初始化传的condition参数2
    -[NSConditionLock initWithCondition:]流程:

    -[NSConditionLock initWithCondition:]
    -[xxx init]
    -[NSConditionLock init]
    -[NSConditionLock zone]
    +[NSCondition allocWithZone:]
    -[NSCondition init]
    -[NSConditionLock lockWhenCondition:]
    同样添加-[NSConditionLock lockWhenCondition:]符号断点:






    调用了-[NSCondition unlock],这个时候继续过断点就又会回到线程4,调用逻辑和线程3相同。

    完整调用逻辑如下:

    线程4
    -[NSConditionLock lockWhenCondition:]
    +[NSDate distantFuture]
    -[NSConditionLock lockWhenCondition:beforeDate:]
    -[NSCondition lock]
    -[NSCondition waitUntilDate:]
    线程3
    -[NSConditionLock lockWhenCondition:]
    +[NSDate distantFuture]
    -[NSConditionLock lockWhenCondition:beforeDate:]
    -[NSCondition lock]
    -[NSCondition unlock]
    返回1(true)
    -[NSConditionLock unlockWithCondition:]
    -[NSCondition lock]
    -[NSCondition broadcast]
    -[NSCondition unlock]
    //回到线程4
    -[NSCondition unlock]
    返回1(true)
    -[NSConditionLock unlockWithCondition:]
    -[NSCondition lock]
    -[NSCondition broadcast]
    -[NSCondition unlock]


    流程总结:

    • 线程4调用[NSConditionLock lockWhenCondition:],此时因为不满足当前条件,所
      以会进入 waiting 状态,当前进入到 waiting 时,会释放当前的互斥锁。
    • 此时当前的线程2 调用[NSConditionLock lock:],本质上是调用[NSConditionLock lockBeforeDate:]这里不需要比对条件值,所以任务3会执行。
    • 接下来线程3 执行[NSConditionLock lockWhenCondition:],因为满足条件值,所以线任务2会执行,执行完成后会调用[NSConditionLock unlockWithCondition:],这个时候将
      condition 设置为 1,并发送 boradcast, 此时线程 4接收到当前的信号,唤醒执行并打印。
    • 这个时候任务执行顺序为任务3 -> 任务2 -> 任务1
    • [NSConditionLock lockWhenCondition:]会根据传入的 condition
      行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行。
    • [NSConditionLock unlockWithCondition:] 会先更改当前的condition值,然后进行广播,唤醒当前的线程。


    作者:HotPotCat
    链接:https://www.jianshu.com/p/8f8e5f0d0b23
    收起阅读 »

    锁的原理(二):@synchronized

    3.1 SyncData存储结构#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock #define LIST_FOR_OBJ(obj) sDataLists[obj].data static StripedMap<...
    继续阅读 »

    3.1 SyncData存储结构

    #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    static StripedMap<SyncList> sDataLists;

    //本身也是 os_unfair_lock
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    可以看到锁和SyncData都是从sDataLists获取的(hash map结构,存储的是SyncList),SyncList定义如下:

    struct SyncList {
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
    };

    StripedMap定义如下:

    class StripedMap {
    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
    #else
    enum { StripeCount = 64 };
    #endif

    struct PaddedT {
    T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    ......
    }

    iOS真机上容量为8,其它平台容量为64SynData根据前面的分析是一个单向链表, 那么可以得到在哈希冲突的时候是采用拉链法解决的。

    增加以下验证代码:

    HPObject *obj = [HPObject alloc];
    HPObject *obj2 = [HPObject alloc];
    HPObject *obj3 = [HPObject alloc];
    dispatch_async(dispatch_queue_create("HotpotCat", DISPATCH_QUEUE_CONCURRENT), ^{
    @synchronized (obj) {
    NSLog(@"obj");
    @synchronized (obj2) {
    NSLog(@"obj2");
    @synchronized (obj3) {
    NSLog(@"obj3");
    }
    }
    }
    });
    • sDataLists包装了array,其中存储的是SyncList集合,SyncListdata中存储的是synData

    3.2 从 TLS 获取 SyncData

      bool fastCacheOccupied = NO;//后续存储的时候用
    //对 pthread_getspecific 的封装,针对线程中第一次调用 @synchronized 是获取不到数据的。
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
    fastCacheOccupied = YES;
    //判断要查找的与存储的object是不是同一个。
    if (data->object == object) {
    // Found a match in fast cache.
    uintptr_t lockCount;

    result = data;
    //获取当前线程对该对象锁了几次
    lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
    if (result->threadCount <= 0 || lockCount <= 0) {
    _objc_fatal("id2data fastcache is buggy");
    }

    switch(why) {
    case ACQUIRE: {//enter 的时候 lockCount + 1,并且存储count到tls
    lockCount++;
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
    break;
    }
    case RELEASE: //exit的时候 lockCount - 1,并且存储count到tls
    lockCount--;
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
    //当 count 减少到 0 的情况下清除对应obj的SynData,这里并没有清空count,count在存储新objc的时候直接赋值为1
    if (lockCount == 0) {
    // remove from fast cache
    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
    // atomic because may collide with concurrent ACQUIRE
    //threadCount - 1
    OSAtomicDecrement32Barrier(&result->threadCount);
    }
    break;
    case CHECK:
    // do nothing
    break;
    }

    return result;
    }
    }

    • 通过tls_get_direct(是对_os_tsd_get_direct的封装)获取当前线程存储的SynData数据。
    • 在数据存在的情况下判断标记fastCacheOccupied存在。
    • 判断tls存储的数据是不是当前对象。是当前对象则进行进一步处理,否则结束tls逻辑。
    • 获取对象加锁的次数lockCount
    • enter逻辑:lockCount++并存储在tls
    • exit逻辑:lockCount--并存储在tls
      • lockCount0的时候释放SynData,直接在tls中置为NULL
      • 并且threadCount - 1

    线程局部存储(Thread Local Storage,TLS): 是操作系统为线程单独提供的私有空间,通常只有有限的容量。
    Linux系统下通常通过pthread库中的相关方法进行操作:
    pthread_key_create()
    pthread_getspecific()
    pthread_setspecific()
    pthread_key_delete()

    3.3 从 Cache 获取 SyncData

    tls中没有找到SynData的时候会去Cache中找:


        //获取线程缓存,参数NO 当缓存不存在的时候不进行创建。
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
    unsigned int i;
    for (i = 0; i < cache->used; i++) {
    SyncCacheItem *item = &cache->list[i];
    //找到obj对应的 item
    if (item->data->object != object) continue;

    // Found a match.
    //获取SynData
    result = item->data;
    if (result->threadCount <= 0 || item->lockCount <= 0) {
    _objc_fatal("id2data cache is buggy");
    }

    switch(why) {
    case ACQUIRE://enter lockCount + 1
    item->lockCount++;
    break;
    case RELEASE://exit lockCount - 1
    item->lockCount--;
    if (item->lockCount == 0) {//lockCount = 0 的时候 从cache中移除i的元素,将最后一个元素存储到原先i的位置。used - 1。也就是最后一个位置被标记为未使用了。
    // remove from per-thread cache
    cache->list[i] = cache->list[--cache->used];
    // atomic because may collide with concurrent ACQUIRE
    //threadCount - 1
    OSAtomicDecrement32Barrier(&result->threadCount);
    }
    break;
    case CHECK:
    // do nothing
    break;
    }

    return result;
    }
    }
    • 通过fetch_cache(是对pthread_getspecific的封装)找SyncCache,由于是读取数据,所以找不到的情况下这里不创建。
    • 遍历cache已使用的空间找到obj对应的SyncCacheItem
    • enter的情况下item->lockCount++
    • exit情况下item->lockCount--
      • item->lockCount == 0的时候将cache中这个item替换为cache中最后一个,used -1标记cache中使用的数量,这样就将cache中数据释放了。
      • syndatathreadCount进行-1

    3.3.1 SyncCache

    typedef struct {
    SyncData *data;//数据
    unsigned int lockCount; // 被当前线程加锁次数
    } SyncCacheItem;

    typedef struct SyncCache {
    unsigned int allocated;//总容量
    unsigned int used;//已使用
    SyncCacheItem list[0];//列表
    } SyncCache;
    • SyncCache中存储的是SyncCacheItem的一个listallocated用于记录开辟的总容量,used记录已经使用的容量。
    • SyncCacheItem存储了一个SyncData以及lockCount。记录的是针对当前线程SyncData被锁了多少次。SyncCacheItem存储的对应于TSL快速缓存的SYNC_COUNT_DIRECT_KEYSYNC_DATA_DIRECT_KEY

    3.3.2 fetch_cache

    static SyncCache *fetch_cache(bool create)
    {
    _objc_pthread_data *data;
    //creat用来处理是否新建。
    data = _objc_fetch_pthread_data(create);
    //data不存在直接返回,create为YES的情况下data不会为空
    if (!data) return NULL;
    //syncCache不存在
    if (!data->syncCache) {
    if (!create) {//不允许创建直接返回 NULL
    return NULL;
    } else {
    //允许创建直接 calloc 创建,初始容量为4.
    int count = 4;
    data->syncCache = (SyncCache *)
    calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
    data->syncCache->allocated = count;
    }
    }

    // Make sure there's at least one open slot in the list.
    //存满的情况下扩容 2倍扩容。
    if (data->syncCache->allocated == data->syncCache->used) {
    data->syncCache->allocated *= 2;
    data->syncCache = (SyncCache *)
    realloc(data->syncCache, sizeof(SyncCache)
    + data->syncCache->allocated * sizeof(SyncCacheItem));
    }

    return data->syncCache;
    }

    通过_objc_fetch_pthread_data获取_objc_pthread_data_objc_pthread_data存储了SyncCache信息,当然不仅仅是它:




    • data
      不存在直接返回,createYES的情况下data不会为空。
    • syncCache不存在的情况下,允许创建则进行calloc(初始容量4,这里是创建syncCache),否则返回NULL
    • syncCache存满(通过allocatedused判断)的情况下进行2被扩容。

    _objc_fetch_pthread_data

    _objc_pthread_data *_objc_fetch_pthread_data(bool create)
    {
    _objc_pthread_data *data;
    //pthread_getspecific TLS_DIRECT_KEY
    data = (_objc_pthread_data *)tls_get(_objc_pthread_key);
    if (!data && create) {
    //允许创建的的情况下创建
    data = (_objc_pthread_data *)
    calloc(1, sizeof(_objc_pthread_data));
    //保存
    tls_set(_objc_pthread_key, data);
    }

    return data;
    }
    • 通过tls_get获取_objc_pthread_data,不存在并且允许创建的情况下进行calloc创建_objc_pthread_data
    • 创建后保存到tls

    这里的cache也是存储在tls,与tls_get_direct的区别要看二者存取的逻辑,一个调用的是tls_get_direct,一个是tls_get


    #if defined(__PTK_FRAMEWORK_OBJC_KEY0)
    # define SUPPORT_DIRECT_THREAD_KEYS 1
    # define TLS_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY0)
    # define SYNC_DATA_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY1)
    # define SYNC_COUNT_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY2)
    # define AUTORELEASE_POOL_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
    # if SUPPORT_RETURN_AUTORELEASE
    # define RETURN_DISPOSITION_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY4)
    # endif
    #else
    # define SUPPORT_DIRECT_THREAD_KEYS 0
    #endif

    #if SUPPORT_DIRECT_THREAD_KEYS
    #define _objc_pthread_key TLS_DIRECT_KEY
    #else
    static tls_key_t _objc_pthread_key;
    #endif

    //key _objc_pthread_key
    static inline void *tls_get(tls_key_t k) {
    return pthread_getspecific(k);
    }

    //key SYNC_DATA_DIRECT_KEY 与 SYNC_COUNT_DIRECT_KEY
    static inline void *tls_get_direct(tls_key_t k)
    {
    ASSERT(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
    return _pthread_getspecific_direct(k);
    } else {
    return pthread_getspecific(k);
    }
    }

    __header_always_inline int
    _pthread_has_direct_tsd(void)
    {
    #if TARGET_IPHONE_SIMULATOR
    return 0;
    #else
    return 1;
    #endif
    }

    __header_always_inline void *
    _pthread_getspecific_direct(unsigned long slot)
    {
    #if TARGET_IPHONE_SIMULATOR
    return pthread_getspecific(slot);
    #else
    return _os_tsd_get_direct(slot);
    #endif
    }

    __attribute__((always_inline))
    static __inline__ void*
    _os_tsd_get_direct(unsigned long slot)
    {
    return _os_tsd_get_base()[slot];
    }

  • _objc_pthread_data通过pthread_getspecific获取缓存数据,key的类型是tls_key_t
    • 如果支持SUPPORT_DIRECT_THREAD_KEYSkey__PTK_FRAMEWORK_OBJC_KEY0
    • 不支持SUPPORT_DIRECT_THREAD_KEYSkey_objc_pthread_key
  • TLS快速缓存通过tls_get_direct获取,keytls_key_t类型。
    • SynData对应的key__PTK_FRAMEWORK_OBJC_KEY1
    • lockCount对应的key__PTK_FRAMEWORK_OBJC_KEY2
    • iOS模拟器通过pthread_getspecific获取
    • 其它通过_os_tsd_get_direct获取,调用的是_os_tsd_get_base(),不同架构对应不同汇编指令:
  • __attribute__((always_inline, pure))
    static __inline__ void**
    _os_tsd_get_base(void)
    {
    #if defined(__arm__)
    uintptr_t tsd;
    __asm__("mrc p15, 0, %0, c13, c0, 3\n"
    "bic %0, %0, #0x3\n" : "=r" (tsd));
    /* lower 2-bits contain CPU number */
    #elif defined(__arm64__)
    uint64_t tsd;
    __asm__("mrs %0, TPIDRRO_EL0\n"
    "bic %0, %0, #0x7\n" : "=r" (tsd));
    /* lower 3-bits contain CPU number */
    #endif

    return (void**)(uintptr_t)tsd;
    }

    3.4 从sDataLists获取SynData

        //sDataLists 中找 Syndata
    {
    SyncData* p;
    SyncData* firstUnused = NULL;
    //从SynList链表中查找SynData
    for (p = *listp; p != NULL; p = p->nextData) {
    if ( p->object == object ) {
    result = p;//找到
    // atomic because may collide with concurrent RELEASE
    //threadCount + 1,由于在上面线程缓存和tls的查找中没有找到,但是在 sDataLists 中找到了。所以肯定不是同一个线程了(那也肯定就不是exit,而是enter了),线程数量+1。
    OSAtomicIncrement32Barrier(&result->threadCount);
    goto done;
    }
    //没有找到的情况下找到了空位。
    if ( (firstUnused == NULL) && (p->threadCount == 0) )
    firstUnused = p;
    }

    // no SyncData currently associated with object
    //是exit就直接跳转到done的逻辑
    if ( (why == RELEASE) || (why == CHECK) )
    goto done;

    // an unused one was found, use it
    //找到一个未使用的(也有可能是之前使用过,threadCount现在变为0了),直接存储当前objc数据(这里相当于释放了sDataLists中的旧数据)。
    if ( firstUnused != NULL ) {
    result = firstUnused;
    //替换object
    result->object = (objc_object *)object;
    result->threadCount = 1;
    goto done;
    }
    }

    • 遍历开始获取的SynListobj对应的SynData
    • 找到的情况下threadCount + 1,由于在tls(快速以及cache中)没有找到数据,但是在sDataLists中找到了,所以肯定不在同一个线程(那也肯定就不是exit,而是enter了)直接跳转到done
    • eixt的逻辑直接跳转到done
    • 没有找到但是找到了threadCount = 0Syndata,也就是找到了空位(之前使用过,threadCount现在变为0了)。
      • 直接存储当前objc数据到synData中(这里相当于释放了sDataLists中的旧数据)。threadCount标记为1

    3.5 创建 SyncData

    tls中没有快速缓存、也没cache、并且sDataLists中没有数据也没有空位

    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    //对象本身
    result->object = (objc_object *)object;
    //持有线程数初始化为1
    result->threadCount = 1;
    //创建锁
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    //头插法
    result->nextData = *listp;
    //这里 sDataLists 中的 SynList就赋值了。
    *listp = result;
    • 开辟一个SyncData大小的内存并进行对齐。
    • 设置object以及threadCount
    • 创建mutex锁。
    • 头插法将创建的SynData插入SynList中。也就相当于将数据存入sDataLists中。nextData存在的情况是发生了哈希冲突。

    3.6 done 缓存存储逻辑

        //数据存储
    if (result) {//有result,无论是创建的还是从 sDataLists 获取的。
    // Only new ACQUIRE should get here.
    // All RELEASE and CHECK and recursive ACQUIRE are
    // handled by the per-thread caches above.
    if (why == RELEASE) {//exit不进行任何操作
    // Probably some thread is incorrectly exiting
    // while the object is held by another thread.
    return nil;
    }
    if (why != ACQUIRE) _objc_fatal("id2data is buggy");
    if (result->object != object) _objc_fatal("id2data is buggy");

    #if SUPPORT_DIRECT_THREAD_KEYS
    //TLS 快速缓存不存在,存储到快速缓存。
    if (!fastCacheOccupied) {//
    // Save in fast thread cache
    //存储Syndata
    tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
    //存储count为1
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
    } else
    #endif
    //cache存储 不支持 tls 快速缓存 或者 tls快速缓存存在的情况下
    {
    // Save in thread cache
    //获取SyncCache,不存在的时候进行创建
    if (!cache) cache = fetch_cache(YES);
    //将result放入list的最后一个元素,SyncCacheItem 中存储 result 以及 lockCount
    cache->list[cache->used].data = result;
    cache->list[cache->used].lockCount = 1;
    cache->used++;
    }
    }

    • exit的时候不进行任何操作:
      • TLS快速缓存会在获取缓存的时候进行释放。并且threadCount -1
      • cache逻辑会进行替换数据(相当于释放),并且threadCount -1
      • sDataLists获取数据逻辑本身不释放,会根据threadCount = 0找到空位进行替换,相当于释放。
    • 在支持快速缓存并且快速缓存不存在的情况下,将创建的SynData以及lockCount = 1存储到TLS快速缓存中。
    • 在不支持快速缓存或者快速缓存已经有值了的情况下将SynData构造SyncCacheItem存入SyncCache中。
    • 也就是说SynData只会在快速缓存与Cache中存在一个,同时会存储在sDataLists中。

    3.7 验证

    3.7.1 @synchronized 数据结构

    根据源码分析@synchronized数据结构如下:



    3.7.2 验证

    有如下代码:

    HPObject *obj = [HPObject alloc];
    HPObject *obj2 = [HPObject alloc];
    HPObject *obj3 = [HPObject alloc];
    dispatch_async(dispatch_queue_create("HotpotCat", DISPATCH_QUEUE_CONCURRENT), ^{
    @synchronized (obj) {
    @synchronized (obj) {
    @synchronized (obj) {
    //obj lockCount = 3 threadCount = 1
    NSLog(@"1 = %p",obj);
    @synchronized (obj2) {
    //obj2 lockCount = 1 threadCount = 1,有可能存在拉链
    NSLog(@"2 = %p",obj2);
    @synchronized (obj3) {
    //obj3 threadCount = 1, lockCount = 1,必然存在拉链(为了方便验证源码强制修改StripeCount为2)
    NSLog(@"3 = %p",obj3);
    dispatch_async(dispatch_queue_create("HotpotCat1", DISPATCH_QUEUE_CONCURRENT), ^{
    @synchronized (obj) {
    //obj threadCount = 2,一个线程的 lockCount = 3 另外一个 lockCount = 1
    NSLog(@"4 = %p",obj);
    }
    });
    //为了让 @synchronized 不exit
    sleep(10);
    }
    }
    }
    }
    }
    });

    do {

    } while (1);
    由于源码是mac工程,在main函数中写一个死循环。为了方便验证将源码中StripeCount改为2


    NSLog@synchronized处断点验证。

    • 1处的验证结果:



    • lockCount = 3threadCount = 1,并且sDataLists中存储的与快速缓存中是同一个SynData地址。符合预期。

    • 2处验证结果:



    可以看到这个时候第二个元素已经进行了拉链,并且obj2在链表的头结点。

    • 3处结果验证:


    仍然进行了拉链obj3 -> obj2 -> obj

    • 4处验证结果:


    这个时候obj对应的SynDatathreadCount2了。

    所有验证结果符合分析预期。

    四、总结

    • 参数传nil没有做任何事情。传self在使用过程中不会被释放,并且同一个类中如果都用self底层只会存在一个SynData

    • @synchronized底层是封装的os_unfair_lock

    • objc_sync_enter中加锁,objc_sync_exit中解锁。

    • @synchronized加锁的数据信息都存储在sDataLists全局哈希表中。同时还有TLS快速缓存(一个SynData数据,通常是第一个,释放后会存放新的)以及线程缓存(一组SyncData数据)。这两个缓存互斥,同一个SyncData只存在其中一个)

    • id2data获取SynData流程:

      • TLS快速缓存获取(SYNC_COUNT_DIRECT_KEY),obj对应的SyncData存在的情况下获取SYNC_COUNT_DIRECT_KEY对应的lockCount
        • enterlockCount++并存储到SYNC_COUNT_DIRECT_KEY
        • exitlockCount--并存储到SYNC_COUNT_DIRECT_KEYlockCount == 0清空SYNC_DATA_DIRECT_KEYthreadCount -1
      • TLS cache缓存获取,遍历cache找到对应的SyncData
        • enterlockCount++
        • exitlockCount--lockCount == 0替换cache->list对应的值为最后一个,used -1threadCount -1
      • sDataLists全局哈希表获取SyncData:找到的情况下threadCount + 1进入缓存逻辑,没有找到并且存在threadCount = 0则替换object相当于存储了新值。
      • SyncData创建:创建SyncData,赋值objectthreadCount初始化为1,创建mutex锁。并且采用头插法将SyncData插入sDataLists对应的SynList头部。
      • SyncData数据缓存:sDataLists添加了或者更新了数据会走到缓存逻辑,缓存逻辑是往TLS快速缓存以及TLS cache缓存添加数据
        • enterTLS快速缓存不存在的情况下将SyncData存储快速缓存,否则存入cache缓存的尾部。
        • exit:直接return
    • lockCount是针对单个线程而言的,当lockCount = 0的时候对数据进行释放

      • TLS快速缓存是直接设置为NULL(只有一个SyncData
      • TLS cache缓存是直接用最后一个数据进行替换(一组SyncData),然后used -1进行释放
      • 同时threadCount - 1相当于当前线程释放。
    • threadCount是针对跨线程的,在threadCount = 0的时候并不立即释放,而是在下次插入数据的时候进行替换。sDataLists保存所有的数据。

    • lockCount@synchronized可重入可递归的原因,threadCount@synchronized可跨线程的原因。

    @synchronized数据之间关系:





    作者:HotPotCat
    链接:https://www.jianshu.com/p/a816e8cf3646
    收起阅读 »

    锁的原理(一):@synchronized

    一、性能分析网上很多对比八大锁性能的文章,时间大部分比较早。苹果对某些锁内部进行了优化。这篇文章找中会以10万次数据做对比对主流锁性能进行分析。1.1 调用情况模拟OSSpinLockOSSpinLock在iOS 10以后废弃了,不过还可以调用。需要导入头文件...
    继续阅读 »

    一、性能分析

    网上很多对比八大锁性能的文章,时间大部分比较早。苹果对某些锁内部进行了优化。这篇文章找中会以10万次数据做对比对主流锁性能进行分析。

    1.1 调用情况模拟

    OSSpinLock
    OSSpinLockiOS 10以后废弃了,不过还可以调用。需要导入头文件<libkern/OSAtomic.h>


    int hp_runTimes = 100000;
    /** OSSpinLock 性能 */
    {
    OSSpinLock hp_spinlock = OS_SPINLOCK_INIT;
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    OSSpinLockLock(&hp_spinlock);//解锁
    OSSpinLockUnlock(&hp_spinlock);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("OSSpinLock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    dispatch_semaphore_t
    信号量是GCD提供的:

    /** dispatch_semaphore_t 性能 */
    {
    dispatch_semaphore_t hp_sem = dispatch_semaphore_create(1);
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    dispatch_semaphore_wait(hp_sem, DISPATCH_TIME_FOREVER);
    dispatch_semaphore_signal(hp_sem);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("dispatch_semaphore_t: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    os_unfair_lock
    os_unfair_lockiOS10推出的新类型的锁需要导入头文件<os/lock.h>

    /** os_unfair_lock_lock 性能 */
    {
    os_unfair_lock hp_unfairlock = OS_UNFAIR_LOCK_INIT;
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    os_unfair_lock_lock(&hp_unfairlock);
    os_unfair_lock_unlock(&hp_unfairlock);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent() ;
    printf("os_unfair_lock_lock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    pthread_mutex_t
    pthread_mutex_tlinux下提供的锁,需要导入头文件<pthread/pthread.h>:


    /** pthread_mutex_t 性能 */
    {
    pthread_mutex_t hp_metext = PTHREAD_MUTEX_INITIALIZER;
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    pthread_mutex_lock(&hp_metext);
    pthread_mutex_unlock(&hp_metext);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("pthread_mutex_t: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSLock
    NSLockFoundation框架提供的锁:

    /** NSlock 性能 */
    {
    NSLock *hp_lock = [NSLock new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_lock lock];
    [hp_lock unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("NSlock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSCondition

    /** NSCondition 性能 */
    {
    NSCondition *hp_condition = [NSCondition new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_condition lock];
    [hp_condition unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("NSCondition: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    pthread_mutex_t(recursive)

    /** PTHREAD_MUTEX_RECURSIVE 性能 */
    {
    pthread_mutex_t hp_metext_recurive;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init (&attr);
    pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init (&hp_metext_recurive, &attr);

    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    pthread_mutex_lock(&hp_metext_recurive);
    pthread_mutex_unlock(&hp_metext_recurive);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("PTHREAD_MUTEX_RECURSIVE: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSRecursiveLock

    /** NSRecursiveLock 性能 */
    {
    NSRecursiveLock *hp_recursiveLock = [NSRecursiveLock new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_recursiveLock lock];
    [hp_recursiveLock unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("NSRecursiveLock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSConditionLock

    /** NSConditionLock 性能 */
    {
    NSConditionLock *hp_conditionLock = [NSConditionLock new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_conditionLock lock];
    [hp_conditionLock unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent() ;
    printf("NSConditionLock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    @synchronized

    /** @synchronized 性能 */
    {
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    @synchronized(self) {}
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("@synchronized: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    锁内部没有处理任何逻辑,都执行的空操作,在10万次循环后计算时间差值。

    1.2 验证

    iPhone 12 pro max 14.3真机测试数据如下:

    OSSpinLock: 1.366019 ms
    dispatch_semaphore_t: 1.923084 ms
    os_unfair_lock_lock: 1.502037 ms
    pthread_mutex_t: 1.694918 ms
    NSlock: 2.384901 ms
    NSCondition: 2.082944 ms
    PTHREAD_MUTEX_RECURSIVE: 3.449082 ms
    NSRecursiveLock: 3.075957 ms
    NSConditionLock: 7.895947 ms
    @synchronized: 3.794074 ms

    iPhone 12 pro max 14.3模拟器测试数据如下:

    OSSpinLock: 1.199007 ms
    dispatch_semaphore_t: 1.991987 ms
    os_unfair_lock_lock: 1.762986 ms
    pthread_mutex_t: 2.611995 ms
    NSlock: 2.719045 ms
    NSCondition: 2.544045 ms
    PTHREAD_MUTEX_RECURSIVE: 4.145026 ms
    NSRecursiveLock: 5.039096 ms
    NSConditionLock: 8.215070 ms
    @synchronized: 10.205030 ms



    大部分锁在真机上性能表现更好,@synchronized在真机与模拟器中表现差异巨大。也就是说苹果在真机模式下优化了@synchronized的性能。与之前相比目前@synchronized的性能基本能满足要求。

    判断一把锁的性能好坏,一般情况下是与pthread_mutex_t做对比(因为底层都是对它的封装)。

    二、@synchronized

    由于@synchronized使用比较简单,并且目前真机性能也不错。所以先分析它。

    2.1售票案例

    有如下代码:

    @property (nonatomic, assign) NSUInteger ticketCount;

    - (void)testTicket {
    self.ticketCount = 10;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 5; i++) {
    [self saleTicket];
    }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 2; i++) {
    [self saleTicket];
    }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 3; i++) {
    [self saleTicket];
    }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 5; i++) {
    [self saleTicket];
    }
    });
    }

    - (void)saleTicket {
    if (self.ticketCount > 0) {
    self.ticketCount--;
    sleep(0.1);
    NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
    } else {
    NSLog(@"当前车票已售罄");
    }
    }

    模拟了多线程售票请款,输出如下:

    当前余票还剩:6张
    当前余票还剩:7张
    当前余票还剩:7张
    当前余票还剩:7张
    当前余票还剩:4张
    当前余票还剩:4张
    当前余票还剩:3张
    当前余票还剩:2张
    当前余票还剩:1张
    当前余票还剩:0张
    当前车票已售罄
    当前车票已售罄
    当前车票已售罄
    当前车票已售罄
    当前车票已售罄
    可以看到余票数量有重复以及顺序混乱。
    saleTicket加上@synchronized就能解决问题:

    - (void)saleTicket {
    @synchronized(self) {
    if (self.ticketCount > 0) {
    self.ticketCount--;
    sleep(0.1);
    NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
    } else {
    NSLog(@"当前车票已售罄");
    }
    }
    }

    一般参数传递self。那么有以下疑问:

    • 为什么要传self呢?传nil行不行?
    • @synchronized是怎么实现加锁的效果的呢?
    • {}代码块究竟是什么呢?
    • 是否可以递归呢?
    • 底层是什么数据结构呢?

    2.2 clang 分析 @synchronized

    @synchronized是个系统关键字,那么通过clang还原它的底层实现,为了方便实现在main函数中调用它:

    int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
    appDelegateClassName = NSStringFromClass([AppDelegate class]);
    @synchronized(appDelegateClassName) {

    }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    }

    clang还原后代码如下:

    int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    { __AtAutoreleasePool __autoreleasepool;
    appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    {
    id _rethrow = 0;
    id _sync_obj = (id)appDelegateClassName;
    objc_sync_enter(_sync_obj);
    try {
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {
    objc_sync_exit(sync_exit);
    }
    id sync_exit;
    } _sync_exit(_sync_obj);
    }
    catch (id e) {
    _rethrow = e;

    }
    {
    struct _FIN {
    _FIN(id reth) : rethrow(reth) {}
    ~_FIN() {
    if (rethrow) objc_exception_throw(rethrow);
    }
    id rethrow;
    } _fin_force_rethow(_rethrow);

    }
    }

    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
    }

    异常处理不关心,所以核心就是try的逻辑,精简后如下:

    id _sync_obj = (id)appDelegateClassName;
    objc_sync_enter(_sync_obj);
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {
    objc_sync_exit(sync_exit);
    }
    id sync_exit;
    } _sync_exit(_sync_obj);
    _SYNC_EXIT是个结构体的定义,_sync_exit析构的实现是objc_sync_exit(sync_exit),所以@synchronized本质上等价于enter + exit
    //@synchronized(appDelegateClassName) {}
    //等价
    objc_sync_enter(appDelegateClassName);
    objc_sync_exit(appDelegateClassName);

    它们是定义在objc中的。当然也可以通过对@synchronized打断点查看汇编定位:



    2.3 源码分析

    2.3.1 objc_sync_enter

    int objc_sync_enter(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
    //obj存在的情况下 获取 SyncData
    SyncData* data = id2data(obj, ACQUIRE);
    ASSERT(data);
    //加锁
    data->mutex.lock();
    } else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
    _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    //不存在调用objc_sync_nil
    objc_sync_nil();
    }

    return result;
    }
    • obj存在的情况下通过id2data获取SyncData,参数是objACQUIRE
      • 然后通过mutex.lock()加锁。
    • objnil的情况下调用objc_sync_nil,根据注释does nothing是一个空实现。

    mutex
    mutexrecursive_mutex_t mutex类型,本质上是recursive_mutex_tt


    using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
    class recursive_mutex_tt : nocopy_t {
    os_unfair_recursive_lock mLock;
    ......
    }

    typedef struct os_unfair_recursive_lock_s {
    os_unfair_lock ourl_lock;
    uint32_t ourl_count;
    } os_unfair_recursive_lock, *os_unfair_recursive_lock_t;

    os_unfair_recursive_lock是对os_unfair_lock的封装。所以 @synchronized 是对os_unfair_lock 的封装。

    objc_sync_nil
    objc_sync_nil的定义如下:

    BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
    );

    # define BREAKPOINT_FUNCTION(prototype) \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }

    替换还原后如下:

    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) 
    void objc_sync_nil(void) {
    asm("");
    }

    也就是一个空实现。

    2.3.2 objc_sync_exit

    int objc_sync_exit(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;//0
    if (obj) {
    //获取 SyncData
    SyncData* data = id2data(obj, RELEASE);
    if (!data) {//没有输出返回错误code - 1
    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
    } else {
    //获取到数据先解锁
    bool okay = data->mutex.tryUnlock();
    if (!okay) {//解锁失败返回-1
    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
    }
    }
    } else {
    // @synchronized(nil) does nothing
    }

    return result;
    }
    • obj存在的情况下通过id2data获取SyncData,参数是objRELEASE
    • 获取到数据进行解锁,解锁成功返回0,失败返回-1

    2.3.3 SyncData 数据结构

    SyncData是一个结构体:

    typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;//下一个节点
    DisguisedPtr<objc_object> object;//obj,@synchronized的参数
    int32_t threadCount; // number of THREADS using this block 线程数量
    recursive_mutex_t mutex;//锁
    } SyncData;
    • nextData指向下一个节点,SyncData是一个单向链表。
    • object存储的是@synchronized的参数,只不过进行了包装。
    • threadCount代表线程数量。支持多线程访问。
    • mutex创建的锁。递归锁只能递归使用不能多线程使用。

    三、id2data

    objc_sync_enterobjc_sync_exit中都调用了id2data获取数据,区别是第二个参数,显然id2data就是数据处理的核心了。

    进行代码块折叠后有如下逻辑:



    syndata要么从TLS获取,要么从cache获取。都没有的情况下进行创建。


    收起阅读 »

    Android模块化开发实践

    一、前言 随着业务的快速发展,现在的互联网App越来越大,为了提高团队开发效率,模块化开发已经成为主流的开发模式。正好最近完成了vivo官网App业务模块化改造的工作,所以本文就对模块化开发模式进行一次全面的介绍,并总结模块化改造经验,帮助兄弟项目避坑。 ...
    继续阅读 »

    一、前言


    随着业务的快速发展,现在的互联网App越来越大,为了提高团队开发效率,模块化开发已经成为主流的开发模式。正好最近完成了vivo官网App业务模块化改造的工作,所以本文就对模块化开发模式进行一次全面的介绍,并总结模块化改造经验,帮助兄弟项目避坑。


    二、什么是模块化开发


    首先我们搞清两个概念,Android客户端开发目前有两种模式:单工程开发模式模块化开发模式



    • **单工程开发模式:**早期业务少、开发人员也少,一个App对应一个代码工程,所有的代码都集中在这一个工程的一个module里。


    • **模块化开发模式:**简单来说,就是将一个App根据业务功能划分成多个独立的代码模块,整个App是由这些独立模块集成而成。



    在讲什么是模块化开发前,我们先定义清楚两个概念:组件和模块。



    • **组件:**指的是单一的功能组件,比如登录组件、分享组件;


    • **模块:**广义上来说是指功能相对独立、边界比较清晰的业务、功能等,本文如果单独出现模块这个词一般是该含义。狭义上是指一个业务模块,对应产品业务,比如商城模块、社区模块。



    模块和组件的本质思想是一样的,都是为了业务解耦和代码重用,组件相对模块粒度更细。在划分的时候,模块是业务导向,划分一个个独立的业务模块,组件是功能导向,划分一个个独立的功能组件。


    模块化开发模式又分为两种具体的开发模式:单工程多module模式多工程模式


    单工程多module模式


    所有代码位于一个工程中,模块以AndroidStudio的module形式存在,由一个App module和多个模块module组成。如图:



    多工程模式


    每个模块代码位于一个工程中,整个项目由一个主模块工程和多个子模块工程组成。其中主模块工程只有一个App module,用于集成子模块,进行整体调试、编包。子模块工程由一个App module和一个Library module组成,App module中是调试、测试代码,Library module中是业务、功能代码。如下图:




    下面我们来对比一下单工程多module模式和多工程模式的优缺点:



    通过上面的对比,我们可以看出来,多工程模式在代码管理、开发调试、业务并行等方面有明显优势,非常适合像vivo官网这种业务线多、工程大、开发人员多的App,所以vivo官网目前就采用的此模式。本文在讲解模块化开发时,一般也是指多工程模式。


    单工程多module模式,更适合开发人员少、业务并行程度低的项目。但是多工程模式也有两个缺点:代码仓较多、开发时需要打开多个工程,针对这两个缺点,我们也有解决方案。


    代码仓较多的问题


    要求我们在拆分模块时粒度不能太细,当一个模块膨胀到一定程度时再进行拆分,在模块化带来的效率提升与代码仓管理成本增加间保持平衡。


    要打开多个工程开发的问题


    我们基于Gradle插件开发了代码管理工具,可以方便的切换通过代码依赖子模块或者maven依赖子模块,实际开发体验跟单工程多module模式一样,如下图;



    模块化开发的流程也很简单:



    • 版本前期,每个模块由特定的开发人员负责,各子模块分别独立开发、调试;


    • 子模块开发完成后,集成到主模块工程进行整体调试;


    • 集成调试成功后,进入测试。



    三、模块化开发


    3.1 我们为什么要做模块化开发呢?


    这里我们说说单一工程开发模式的一些痛点。


    团队协作效率低



    • 项目早期业务少、开发人员也少,随着业务发展、团队扩张,由于代码都在同一个工程中,虽然各个人开发的功能不同,但是经常会修改同一处的代码,这时就需要相关开发人员沟通协调以满足各自需求,增加沟通成本;


    • 提交代码时,代码冲突也要沟通如何合并(否则可能引起问题),增加合代码成本;


    • 无法进行并行版本开发,或者勉强进行并行开发,代价是各个代码分支差异大,合并代码困难。



    代码维护成本高



    • 单一工程模式由于代码都在一起,代码耦合严重,业务与业务之间、业务与公共组件都存在很多耦合代码,可以说是你中有我、我中有你,任何修改都可能牵一发而动全身,随着版本的迭代,维护成本会越来越高。


    开发调试效率低



    • 任何一次的修改,即使是改一个字符,都需要编译整个工程代码,随着代码越来越多,编译也越来越慢,非常影响开发效率。


    3.2 如何解决问题


    说完单一工程开发模式的痛点,下面我们看看模块化开发模式怎么来解决这些问题的。


    提高团队协作效率



    • 模块化开发模式下,根据业务、功能将代码拆分成独立模块,代码位于不同的代码仓,版本并行开发时,各个业务线只在各自的模块代码仓中进行开发,互不干扰,对自己修改的代码负责;


    • 测试人员只需要重点测试修改过的功能模块,无需全部回归测试;


    • 要求产品层面要有明确的业务划分,并行开发的版本必须是不同业务模块。



    降低代码维护成本



    • 模块化开发对业务模块会划分比较明确的边界,模块间代码是相互独立的,对一个业务模块的修改不会影响其他模块;


    • 当然,这对开发人员也提出了要求,模块代码需要做到高内聚。



    提高编译速度



    • 开发阶段,只需要在自己的一个代码仓中开发、调试,无需集成完整App,编译代码量极少;


    • 集成调试阶段,开发的代码仓以代码方式依赖,其他不涉及修改的代码仓以aar方式依赖,整体的编译代码量也比较少。



    当然模块化开发也不是说全都是好处,也存在一些缺点,比如:



    1)业务单一、开发人员少的App不要模块化开发,那样反而会带来更多的维护成本;


    2)模块化开发会带来更多的重复代码;


    3)拆分的模块越多,需要维护的代码仓越多,维护成本也会升高,需要在拆分粒度上把握平衡。



    总结一下,模块化开发就像我们管理书籍一样,一开始只有几本书时,堆书桌上就可以了。随着书越来越多,有几十上百本时,我们需要一个书橱,按照类别放在不同的格子里。对比App迭代过程,起步时,业务少,单一工程模式效率最高,随着业务发展,我们要根据业务拆分不同的模块。


    所有这些目的都是为了方便管理、高效查找。


    四、模块化架构设计


    模块化架构设计的思路,我们总结为纵向和横向两个维度。纵向上根据与业务的紧密程度进行分层,横向上根据业务或者功能的边界拆分模块。


    下图是目前我们App的整体架构。



    4.1 纵向分层


    先看纵向分层,根据业务耦合度从上到下依次是业务层、组件层、基础框架层。



    • 业务层:位于架构最上层,根据业务模块划分(比如商城、社区等),与产品业务相对应;


    • 组件层:App的一些基础功能(比如登录、自升级)和业务公用的组件(比如分享、地址管理),提供一定的复用能力;


    • 基础框架层:完全与业务无关、通用的基础组件(比如网络请求、图片加载),提供完全的复用能力。



    框架层级从上往下,业务相关性越来越低,代码稳定性越来越高,代码入仓要求越来越严格(可以考虑代码权限收紧,越底层的代码,入仓要求越高)。


    4.2 横向分模块



    • 在每一层上根据一定的粒度和边界,拆分独立模块。比如业务层,根据产品业务进行拆分。组件层则根据功能进行拆分。


    • 大模块可以独立一个代码仓(比如商城、社区),小模块则多个模块组成一个代码仓(比如上图中虚线中的就是多个模块位于一个仓)。


    • 模块要高内聚低耦合,尽量减少与其他模块的依赖。



    面向对象设计原则强调组合优于继承,平行模块对应组合关系,上下层模块对应继承关系,组合的优点是封装性好,达到高内聚效果。所以在考虑框架的层级问题上,我们更偏向前者,也就是拆分的模块尽量平行,减少层级。


    层级多的问题在于,下层代码仓的修改会影响更多的上层代码仓,并且层级越多,并行开发、并行编译的程度越低。


    模块依赖规则:



    • 只有上层代码仓才能依赖下层代码仓,不能反向依赖,否则可能会出现循环依赖的问题;


    • 同一层的代码仓不能相互依赖,保证模块间彻底解耦。



    五、模块化开发需要解决哪些问题


    5.1 业务模块如何独立开发、调试?


    方式一:每个工程有一个App module和一个Library module,利用App module中的代码调试Library module中的业务功能代码。


    方式二:利用代码管理工具集成到主工程中调试,开发中的代码仓以代码方式依赖,其他模块以aar方式依赖。


    5.2 平行模块间如何实现页面跳转,包括Activity跳转、Fragment获取?


    根据模块依赖原则,平行模块间禁止相互依赖。隐式Intent虽然能解决该问题,但是需要通过Manifest集中管理,协作开发比较麻烦,所以我们选择了路由框架Arouter,Activity跳转和Fragment获取都能完美支持。另外Arouter的拦截器功能也很强大,比如处理跳转过程中的登录功能。


    5.3 平行模块间如何相互调用方法?


    Arouter服务参考——github.com/alibaba/ARo…


    5.4 平行模块间如何传递数据、驱动事件?


    Arouter服务、EventBus都可以做到,视具体情况定。


    六、老项目如何实施模块化改造


    老项目实施模块化改造非常需要耐心和细心,是一个循序渐进的过程。


    先看一下我们项目的模块化进化史,从单一工程逐步进化成纺锤形的多工程模块化模式。下图是进化的四个阶段,从最初的单个App工程到现在的4层多仓结构。





    注:此图中每个方块表示一个代码仓,上层代码仓依赖下层代码仓。


    早期项目都是采用单一工程模式的,随着业务的发展、人员的扩张,必然会面临将老项目进行模块化改造的过程。但是在模块化改造过程中,我们会面临很多问题,比如:



    • 代码逻辑复杂,缺乏文档、注释,不敢轻易修改,害怕引起功能异常;


    • 代码耦合严重,你中有我我中有你,牵一发动全身,拆分重构难度大;


    • 业务版本迭代与模块化改造并行,代码冲突频繁,影响项目进度;



    相信做模块化的人都会遇到这些问题,但是模块化改造势在必行,我们不可能暂停业务迭代,把人力都投入到模块化中来,一来业务方不可能同意,二来投入太多人反而会带来更多代码冲突。


    所以需要一个可行的改造思路,我们总结为先自顶向下划分,再自底向上拆分


    自顶向下



    • 从整体到细节逐层划分模块,先划分业务线,业务线再划分业务模块,业务模块中再划分功能组件,最终形成一个树状图。



    自底向上



    • 当我们把模块划分明确、依赖关系梳理清楚后,我们就需要自底向上,从叶子模块开始进行拆分,当我们把叶子模块都拆分完成后,枝干模块就可以轻松拆分,最后完成主干部分的拆分。


    • 另外整个模块化工作需要由专人统筹,整体规划,完成主要的改造工作,但是有复杂的功能也可以提需求给各模块负责人,协助完成改造。



    下面就讲讲我们在模块化改造路上打怪升级的一些经验。总的来说就是循序渐进,各个击破


    6.1 业务模块梳理


    这一步是自顶向下划分模块,也就是确定子模块代码仓。一个老项目必然经过多年迭代,经过很多人开发,你不一定要对所有的代码都很熟悉,但是你必须要基本了解所有的业务功能,在此基础上综合产品和技术规划进行初步的模块划分。


    此时的模块划分可以粒度粗一点,比如根据业务线或者大的业务模块进行划分,但是边界要清晰。一个App一般会有多个业务线,每个业务线下又会有多个业务模块,这时,我们梳理业务不需要太细,保持2层即可,否则过度的拆分会大大增加实施的难度。



    6.2 抽取公共组件


    划分完模块,但是如果直接按此来拆分业务模块,会有很大难度,并且会有很多重复代码,因为很多公共组件是每个业务模块都要依赖的(比如网络请求、图片加载、分享、登录)。所以模块化拆分的第一步就是要抽取、下沉这些公共组件。


    在这一步,我们在抽取公共组件时会遇到两类公共组件,一类是完全业务无关的基础框架组件(比如网络请求、图片加载),一类是业务相关的公共业务组件(比如分享、登录)。


    可以将这两类公共组件分成两层,便于后续的整体框架形成。比如我们的lib仓放的是基础框架组件和core仓放的是业务公共组件。如下图



    6.3 业务模块拆分


    抽取完公共组件后,我们要准备进行业务模块的拆分,这一步耗时最长,但也是效果最明显的,因为拆完我们就可以多业务并行开发了。


    确定要拆分的业务模块(比如下图的商城业务),先把代码仓拉出来,新功能直接在新仓开发。


    那老功能该怎么拆分迁移呢?我们不可能一口吃成大胖子,想一次把一个大业务模块全部拆分出来,难度太大。这时我们就要对业务模块内部做进一步的梳理,找出所有的子功能模块(比如商城业务中的支付、选购、商详等)。



    按照功能模块的独立程度,从易到难逐个拆分,比如支付的订单功能比较独立,那就先把订单功能的代码拆分到新仓。


    6.4 功能模块拆分


    在拆分具体功能时,我们依然使用Top-Down的逻辑来实施,首先找到入口类(比如Activity),迁移到新的代码仓中,此时你会发现一眼望去全是报红,就像拔草一样带出大量根须。依赖的布局、资源、辅助类等等都找不到,我们按照从易到难的顺序一个个解决,需要解决的依赖问题有以下几类:



    1)简单的依赖,比如字符串、图片。


    这类是最容易解决,直接把资源迁移过来即可。


    2)较复杂的依赖,比如布局文件、drawable。


    这类相对来说也比较容易解决,逐级迁移即可。比如布局依赖各种drawable、字符串、图片,drawable又依赖其他的drawable等,自顶向下逐个迁移就能解决。


    3)更复杂的依赖,类似A->B->C->D。


    对于这类依赖有两种解决方式,如果依赖的功能没有业务特性或只是简单封装系统 API,那可以考虑直接copy一份;如果依赖的代码是多个功能模块公用的或者多个功能模块需要保持一致,可以考虑将该功能代码抽取下沉到下一层代码仓。


    4)一时难以解决的依赖。


    可以先暂时注释掉,保证可以正常运行,后续理清逻辑再决定是进行解耦还是重构。斩断依赖链非常重要,否则可能坚持不下去。



    6.5 代码解耦


    下面介绍一下常用的代码解耦方法:



    公共代码抽取下沉


    比如:基础组件(eg.网络请求框架)、各模块需要保持功能一致的代码(eg.适配OS的动效);




    简单代码复制一份


    比如简单封装系统api(eg.获取packageName)、功能模块自用的自定义view(eg.提示弹窗);




    三个工具


    Arouter路由、Arouter服务、EventBus,能满足各种解耦场景。



    6.6 新老代码共存


    老项目模块化是一个长期的过程,新老代码共存也是一个长期的过程。经过上面改造后,一个功能模块就可以独立出来了,因为我们都是从老的App工程里拆分出来的,所以App工程依赖新仓后就可以正常运行。当我们持续从老工程中拆分出独立模块,最后老工程只需要保留一些入口功能,作为集成子模块的主工程。


    七、总结


    本文从模块化的概念模块化架构设计以及老项目如何实施模块化改造等几个方面介绍移动应用客户端模块化实践。当然模块化工作远不止这些,还包括模块aar管理、持续集成、测试、模块化代码管理、版本迭代流程等,本文就不一一赘述,希望这篇文章能给准备做模块化开发的项目提供帮助。



    作者:vivo互联网客户端团队-Wang Zhenyu


    收起阅读 »

    真·富文本编辑器的演进之路-Span的整体性控制

    时隔多日,终于又更新了。 在了解了Span的基本知识后,我们先来处理下「Span的整体性控制」,怎么理解呢?我们在编辑富文本的时候,经常会遇到一些整体内容的输入,例如「@用户」、输入话题「#什么话题#」、跳转链接「URL」,这些Span区别于普通文字,输入时...
    继续阅读 »

    时隔多日,终于又更新了。


    在了解了Span的基本知识后,我们先来处理下「Span的整体性控制」,怎么理解呢?我们在编辑富文本的时候,经常会遇到一些整体内容的输入,例如「@用户」、输入话题「#什么话题#」、跳转链接「URL」,这些Span区别于普通文字,输入时是整体输入,删除时,也是整体删除,而知中间也不能插入文字或者修改,这就是「Span的整体性控制」。


    所以,我们需要对Span做下面的限制:



    • 中间不允许光标插入

    • 增加时整体新增

    • 删除时整体删除


    对应这样的需求,我们有两种方式来处理,第一种是使用原本就是整体的Span,例如ImageSpan,这是最简单的方法,而且代码也非常简单,另一种是通过代码处理,让普通文本来实现整体性的功能。


    通过ImageSpan保证完整性


    将Span内容生成ImageSpan,从而实现整体性控制。这种方案简单易行,我们以新增「@用户」为例。



    1. 首先,创建一个ATSpan,继承自ImageSpan,附带@的数据信息

    2. 解析要添加的富文本数据,将要展示的内容,例如「@xuyisheng」,作为文本,创建一个TextView来承载

    3. 将生成的TextView转化为Drawable,设置给ATSpan,并传入@的相关数据信息

    4. 将ImageSpan插入Edittext,实现整体性Span的富文本插入


    可以发现,这种方案的实现步骤是比较简单的,但是它的确定也很明显:


    首先,由于是ImageSpan,所以在与普通文本的对齐方式上,始终会存在一些误差,这些误差有来自TextView-Drawable的转换过程,也有ImageSpan的对齐过程,所以,在样式上,对齐会有一些问题,同时,由于TextView-Drawable的整体性,一旦TextView有多行或者当前行剩余位置不够,那么第二行的剩余区域都将被View的矩形区域填满,从而导致这些区域无法再输入文本,如下所示。


    image-20210819162910988


    这是由于View的图形限制导致的问题,使用ImageSpan的话,是无法解决的问题,由此可见,ImageSpan虽然天生具有整体性,但是却只是一个妥协的方案,不能算是最好的实现方式。


    通过SpanWatcher控制


    第二种方案,我们使用普通文本,但是对普通文本增加Span标记,并对这个Span做整体性控制,这种方案复杂一点,要处理的地方也比较多,但是由于它使用的是普通文本,所以在样式上可以和其它普通文本完全保持一致,视觉样式应该是最好的。


    着色


    首先,我们来实现普通文本的变色功能,做一个蓝色的字色,这个比较简单,可以使用ClickableSpan或者其它Span来着色,为了方便我们富文本的输入和展示,这里直接选择ClickableSpan来实现。


    控制选中


    在讲解如何在普通文本中对Span做整体性控制前,我们先来考虑下选择的问题——如何让「整体性Span」的内部无法被选中。


    首先,我们要知道,Edittext的光标也是一种Span。也就是说,我们可以通过监听光标的移动事件,通过Selection实现当光标移动到Span内部时,让它重新移动到Span最近的边缘位置,从而让Span内部永远无法插入光标,这就是我们的主要思路。


    那么问题来了,我要怎么监听Edittext的光标呢?


    其实,Android的Span不仅功能非常强大,而且也提供了非常完善的管理API,在TextView和Edittext中,我们要监听Text的变化过程,可以使用TextWatcher,它可以在文本发生改变时进行回调,类似的,在SpannableStringBuidler中,也有类似的管理类——SpanWatcher,它同样可以用于在Span发生变化时进行回调。


    SpanWatcher,官方介绍如下。


    When an object of this type is attached to a Spannable, its methods will be called to notify it that other markup objects have been added, changed, or removed.

    在TextVIew的内部,它通过DynamicLayout来渲染Spannable数据,在其内部会设置SpanWatcher来监听Span的新增、修改和删除,当监听到变化后,会调用其内部的方法进行刷新。


    image-20210819165313706


    SpanWatcher和TextWatcher一样,都是继承自NoCopySpan,它们一个监听文本变化,一个监听Span变化。


    看完了SpanWatcher,再来看下Selection,Selection是为TextView和Edittext设计的一套管理选中态的工具类,借助Selection,可以在不依赖具体View的情况下,对Span做选中态的修改。


    Selection有两个状态,Start和End,而选择光标,就是Selection的两个状态,当两个状态重合时,就是光标的输入状态。


    现在我们的思路就很明显了,在SpanWatcher的onSpanChanged中监听Selection的Start和End状态即可,一旦Selection的Start和End在我们的「整体性Span」中,就将Selection光标移动到最近的Span标记处。


    image-20210819173317458


    那么SpanWatcher怎么使用呢?


    Edittext提供了Editable.Factory来自定义添加SpanWatcher,我们只需要在初始化的时候传入即可,代码如下所示。


    class ExEditableFactory(private val spans: List<NoCopySpan>) : Factory() {
    override fun newEditable(source: CharSequence): Editable {
    val spannableStringBuilder = RepairSpannableStringBuilder(source)
    for (span in spans) {
    spannableStringBuilder.setSpan(span, 0, source.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE or Spanned.SPAN_PRIORITY)
    }
    return spannableStringBuilder
    }
    }

    val watchers = ArrayList<NoCopySpan>()
    watchers.add(SelectionSpanWatcher(IntegratedSpan::class))
    setEditableFactory(ExEditableFactory(watchers))

    这样我们就完成了选中的整体性功能,当我们的Selection在「整体性Span」(通过IntegratedSpan来标记)中时,就自动修改Selection的位置,从而实现「整体性Span」中间无法插入光标。


    控制删除


    那么除了选中之外,剩下的一个问题就是删除的整体性控制。


    相比于选中来说,删除就比较简单了,我们可以通过setOnKeyListener来监听KeyEvent.KEYCODE_DEL和KeyEvent.ACTION_DOWN事件。


    当我们检测到这两个事件后,根据当前Selection的位置,拿到当前是否存在「整体性Span」,如果是「整体性Span」,那么在删除时则整体移除即可。



    这里有个很重要的地方,getSpan函数传入的Start和End,是闭区间,也就是说,即使光标现在在「整体性Span」的末尾,getSpan函数也是能拿到这个Span的。



    有了思路之后,我们的代码就很容易了,关键代码如下所示。


    image-20210820145414181



    其实这里除了对「整体性Span」进行整体性删除以为,你甚至可以使用removeSpan来移除「整体性Span」,从而将其恢复成普通文本,当然,这都是看你自己的需求了。



    好了,到此为止,我们又实现了富文本编辑器中的一个非常重要的功能——Span的整体性控制。

    收起阅读 »

    Flutter 安卓 Platform 与 Dart 端消息通信方式 Channel 源码解析

    背景 本系列前面已经分析了 Flutter 的很多知识,这一篇我们来看下 Flutter 平台通信相关原理。Flutter 官方提供三种 Platform 与 Dart 端消息通信方式,他们分别是 MethodChannel、BasicMessageChan...
    继续阅读 »


    背景


    本系列前面已经分析了 Flutter 的很多知识,这一篇我们来看下 Flutter 平台通信相关原理。Flutter 官方提供三种 Platform 与 Dart 端消息通信方式,他们分别是 MethodChannel、BasicMessageChannel、EventChannel,本文会继续延续前面系列对他们进行一个深度解析,源码依赖 Flutter 2.2.3 版本,Platform 选取熟悉的 Android 平台实现。


    对于 MethodChannel、BasicMessageChannel、EventChannel 三种官方消息通信方式来说,他们都是全双工通信,所以基于他们我们基本可以实现 Platform 与 Dart 的各种通信能力。他们各自适用场景如下:



    • MethodChanel:用于传递方法调用,MethodCallHandler 最终必须在 UI 线程通过result.success(x)方法返回结果,返回前自己可以异步新起线程做任意耗时操作。

    • BasicMessageChannel:用于传递字符串和半结构化的消息。

    • EventChannel:用于数据流的发送。


    基础使用技巧


    这些通信方式的基础用法我们这里就不再解释了,这里重点说下技巧,在编写 Platform 代码时有两个特别注意的点:



    • 对于 Mac 用户,如果你要通过 Mac 的 Android Studio 打开 Flutter 自动创建的.android 项目,记得吊起访达后通过快捷键Command + Shift + '.'显示隐藏目录即可。

    • 修改 Platform 端的代码后如果运行没生效则请关闭 app 重新编译,因为热部署对 Platform 无效。


    日常工作中我们使用最多的是 MethodChannel,但是他却不是类型安全的,为了解决这个问题官方推荐使用 Pigeon 包作为 MethodChannel 的替代品,它将生成以结构化类型安全方式发送消息的代码,但是他目前还不稳定。


    更多关于他们基础使用案例参见官方文档flutter.dev/docs/develo…


    消息收发传递源码分析


    下面源码分析我们依旧秉承以使用方式为入口,分 Platform、Engine、Dart 层各自展开。


    Platform 端收发实现流程


    在进行 Platform 端源码分析前请先记住下面这幅图,如下 Platform 的 Java 侧源码基于此图展开分析。 在这里插入图片描述 我们先分别看下 MethodChannel、BasicMessageChannel、EventChannel 在 Platform 端的构造成员源码:


    public class MethodChannel {
    private final BinaryMessenger messenger;
    private final String name;
    private final MethodCodec codec;
    //......
    private final class IncomingMethodCallHandler implements BinaryMessageHandler {
    private final MethodCallHandler handler;
    }
    }

    public final class BasicMessageChannel<T> {
    @NonNull private final BinaryMessenger messenger;
    @NonNull private final String name;
    @NonNull private final MessageCodec<T> codec;
    //......
    private final class IncomingMessageHandler implements BinaryMessageHandler {
    private final MessageHandler<T> handler;
    }
    }

    public final class EventChannel {
    private final BinaryMessenger messenger;
    private final String name;
    private final MethodCodec codec;
    //......
    private final class IncomingStreamRequestHandler implements BinaryMessageHandler {
    private final StreamHandler handler;
    }
    }

    可以看到,Platform 端无论哪种方式,他们都有三种重要的成员,分别是:



    • name:String 类型,唯一标识符代表 Channel 的名字,因为一个 Flutter 应用中存在多个 Channel,每个 Channel 在创建时必须指定一个独一无二的 name 作为标识,这点我们在前面系列源码分析中已经见过很多框架实现自己的 name 定义了。

    • messager:BinaryMessenger 类型,充当信使邮递员角色,消息的发送与接收工具人。

    • codec:MethodCodec 或MessageCodec<T>类型,充当消息的编解码器。


    所以,MethodChannel、BasicMessageChannel、EventChannel 的 Java 端源码其实自身是没有什么的,重点都在 BinaryMessenger,我们就不贴源码了(比较简单),整个 Java 端收发的流程(以 MethodChannel 为例)大致如下: 在这里插入图片描述 上面流程中的 DartMessenger 就是 BinaryMessenger 的实现,也就是 Platform 端与 Dart 端通信的信使,这一层通信使用的消息格式为二进制格式数据(ByteBuffer)。


    可以看到,当我们初始化一个 MethodChannel 实例并注册处理消息的回调 Handler 时会生成一个对应的 BinaryMessageHandler 实例,然后这个实例被放进信使的一个 Map 中,key 就是我们 Channel 的 name,当 Dart 端发送消息到 DartMessenger 信使时,信使会根据 name 找到对应 BinaryMessageHandler 调用,BinaryMessageHandler 中通过调用 MethodCodec 解码器进行二进制解码(默认 StandardMethodCodec 解码对应平台数据类型),接着我们就可以使用解码后的回调响应。


    当我们通过 Platform 调用 Dart 端方法时,也是先通过 MethodCodec 编码器对平台数据类型进行编码成二进制格式数据(ByteBuffer),然后通过 DartMessenger 信使调用 FlutterJNI 交给 Flutter Engine 调用 Dart 端对应实现。


    Dart Framework 端收发实现流程


    在进行 Dart 端源码分析前请先记住下面这幅图,如下源码基于此图展开分析。 在这里插入图片描述 是不是 Dart 端的像极了 Platform 端收发实现流程图,同理我们看下 Dart Framework 端对应 Channel 实现类成员:


    class MethodChannel {
    final String name;
    final MethodCodec codec;
    final BinaryMessenger? _binaryMessenger;
    //......
    }

    class BasicMessageChannel<T> {
    final String name;
    final MessageCodec<T> codec;
    final BinaryMessenger? _binaryMessenger;
    //......
    }

    class EventChannel {
    final String name;
    final MethodCodec codec;
    final BinaryMessenger? _binaryMessenger;
    //......
    }

    可以看到,Dart 端无论哪种方式,他们也都有三种重要的成员,分别是 name、codec、_binaryMessenger,而且他们的职责和 Platform 端完全一样。也就是说 Dart 端就是 Platform 端的一个镜像实现而已,框架设计到原理步骤完全一致,区别仅仅是实现语言的不同。


    所以,整个 Dart 端收发的流程(以 MethodChannel 为例)大致如下: 在这里插入图片描述 有了上图不用再贴代码了吧,和 Platform 端如出一辙,只是换了个语言实现而已。


    Flutter Engine C++ 收发实现流程


    上面 Platform 与 Dart 端的通信都分析完毕了,现在就差中间粘合层的 Engine 调用了,Engine 的分析我们依然依据调用顺序为主线查看。通过上面分析我们可以得到如下信息:



    • Platform 调用 Dart 时 Java 最终调用了 FlutterJNI 的private native void nativeDispatchPlatformMessage(long nativeShellHolderId, String channel, ByteBuffer message, int position, int responseId)方法传递到 Engine,Engine 最终调用了 Dart Framework 中hooks.dartvoid _dispatchPlatformMessage(String name, ByteData? data, int responseId)方法,然后层层传递到我们的 Widget 中的 MethodChannel。

    • Dart 调用 Platform 时 Dart 最终调用了 PlatformDispatcher 的String? _sendPlatformMessage(String name, PlatformMessageResponseCallback? callback, ByteData? data)方法(即native 'PlatformConfiguration_sendPlatformMessage')传递到 Engine,Engine 最终调用了 Platform 端 FlutterJNI 的public void handlePlatformMessage(final String channel, byte[] message, final int replyId)方法,然后层层传递到我们的 MethodChannel 设置的 MethodCallHandler 回调的 onMethodCall 方法中。


    因此我们顺着这两端的入口分析源码可以得到如下调用顺序图: 在这里插入图片描述 上图对应的 Engine C++ 代码调用及类所属文件都已经交代的很详细了,源码就不再贴片段了,相信你顺着这条链也能根懂源码。特别注意上面 Engine 在负责转发消息时的黄色 TaskRunner,其中 PlatformTaskRunner 就是平台层的主线程(安卓 UI 线程),所以 Channel 在安卓端的回调被切换运行在 UI 线程中,Channel 在 Dart 端的回调被切换运行在 Flutter Dart UI 线程(即 UITaskRunner 中)。


    消息编解码源码分析


    搞懂了 Channel 的收发流程,你可能对上面的编解码器还有疑惑,他是怎么做到 Dart 与不同平台语言类型间转换的? 我们都知道,一般跨语言或平台传输对象首选方案是通过 json 或 xml 格式,而 Flutter 也不例外,譬如他也提供了 JSONMessageCodec、JSONMethodCodec 等编解码器,同样也是将二进制字节流转换为 json 进行处理,像极了我们 http 请求中字节流转字符串转 json 转对象的机制,这样就抹平了平台差异。 对于 Flutter 的默认实现来说,最值得关注的就是 StandardMethodCodec 和 StandardMessageCodec,由于 StandardMethodCodec 是对 StandardMessageCodec 的一个包装,所以本质我们研究下 StandardMessageCodec 即可。如下:


    public class StandardMessageCodec implements MessageCodec<Object> {
    //把Java对象类型Object转为字节流ByteBuffer
    @Override
    public ByteBuffer encodeMessage(Object message) {
    //......
    final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
    writeValue(stream, message);
    final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
    buffer.put(stream.buffer(), 0, stream.size());
    return buffer;
    }
    //把字节流ByteBuffer转为Java对象类型Object
    @Override
    public Object decodeMessage(ByteBuffer message) {
    //......
    message.order(ByteOrder.nativeOrder());
    final Object value = readValue(message);
    //......
    return value;
    }
    //......
    }

    可以看到,在 Platform 端(Android Java)StandardMessageCodec 的作用就是字节流转 Java 对象类型,Java 对象类型转字节流,核心本质是 StandardMessageCodec 的 readValue 和 writeValue 方法,如下:


    protected void writeValue(ByteArrayOutputStream stream, Object value) {
    if (value == null || value.equals(null)) {
    stream.write(NULL);
    } else if (value instanceof Boolean) {
    stream.write(((Boolean) value).booleanValue() ? TRUE : FALSE);
    } else if (value instanceof Number) {
    if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
    stream.write(INT);
    writeInt(stream, ((Number) value).intValue());
    } else if (value instanceof Long) {
    stream.write(LONG);
    writeLong(stream, (long) value);
    } else if (value instanceof Float || value instanceof Double) {
    stream.write(DOUBLE);
    writeAlignment(stream, 8);
    writeDouble(stream, ((Number) value).doubleValue());
    } else if (value instanceof BigInteger) {
    stream.write(BIGINT);
    writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8));
    } else {
    throw new IllegalArgumentException("Unsupported Number type: " + value.getClass());
    }
    } else if (value instanceof String) {
    stream.write(STRING);
    writeBytes(stream, ((String) value).getBytes(UTF8));
    } else if (value instanceof byte[]) {
    stream.write(BYTE_ARRAY);
    writeBytes(stream, (byte[]) value);
    } else if (value instanceof int[]) {
    stream.write(INT_ARRAY);
    final int[] array = (int[]) value;
    writeSize(stream, array.length);
    writeAlignment(stream, 4);
    for (final int n : array) {
    writeInt(stream, n);
    }
    } else if (value instanceof long[]) {
    stream.write(LONG_ARRAY);
    final long[] array = (long[]) value;
    writeSize(stream, array.length);
    writeAlignment(stream, 8);
    for (final long n : array) {
    writeLong(stream, n);
    }
    } else if (value instanceof double[]) {
    stream.write(DOUBLE_ARRAY);
    final double[] array = (double[]) value;
    writeSize(stream, array.length);
    writeAlignment(stream, 8);
    for (final double d : array) {
    writeDouble(stream, d);
    }
    } else if (value instanceof List) {
    stream.write(LIST);
    final List<?> list = (List) value;
    writeSize(stream, list.size());
    for (final Object o : list) {
    writeValue(stream, o);
    }
    } else if (value instanceof Map) {
    stream.write(MAP);
    final Map<?, ?> map = (Map) value;
    writeSize(stream, map.size());
    for (final Entry<?, ?> entry : map.entrySet()) {
    writeValue(stream, entry.getKey());
    writeValue(stream, entry.getValue());
    }
    } else {
    throw new IllegalArgumentException("Unsupported value: " + value);
    }
    }

    不用解释了吧,这不就是枚举一堆支持的类型然后按照字节位数截取转换的操作,所以这也就是为什么官方文档中明确枚举了 Channel 支持的数据类型,如下: 在这里插入图片描述 上面是 Platform 端对象类型与二进制之间的转换原理,对于 Dart 端我想你应该也就懂了,无非也是类似操作,不再赘述。


    总结


    上面全程都以 MethodChannel 进行了源码分析,其他 Channel 我们没有进行分析,但其实本质都一样,仅仅是一种封装而已,希望你有需求的时候知道怎么举一反三。

    收起阅读 »

    『Android』 AndroidStudio多版本共存指南

    当AndroidStudio最新版本,提供许多新功能的时候。为了提升开发效率,必须跟着谷歌官方走。但是为了防止,将原本的Studio直接升级到新版Studio,然后导入以前项目,出现问题。因此,考虑多种版本共存的问题。 搭建方法 采用多个版本的Stud...
    继续阅读 »

    当AndroidStudio最新版本,提供许多新功能的时候。为了提升开发效率,必须跟着谷歌官方走。但是为了防止,将原本的Studio直接升级到新版Studio,然后导入以前项目,出现问题。因此,考虑多种版本共存的问题。



    搭建方法


    采用多个版本的Studio(例如:AndroidStudio2.3 和3.0)开发同一个项目,当新版本出现问题后,为了避免拖延开发时间,可及时切换会旧版本继续开发。


    1.下载最新的版本或者需要的版本:


    ★ ★ ★ AndroidStudio的下载分为安装版(.exe)和无安装版本(zip)。


    原本已经存在的了AndroidStudio和配置好的SDK,不需要进行替换成最新的AndroidStudio3.0版本。 只需要下载无安装版本的AndroidStudio。如下图所示:


    1.png


    接下来,下载完成后,解压到指定的目录下,如下图所示:


    2.png


    2.配置下载好的Studio版本:


    在解压后的目录下–>bin目录–>打开studio64.exe程序,下图所示:


    4.png


    运行AndroidStudio3.0程序后,弹出Import Studio设置弹窗,如下图所示:


    3.png



    • 第一个选项:是导入旧版本的设置。选择该项后,可以直接与旧版的Studio共同开发原本项目,无需手动配置SDK,导入指定项目等操作。


    • 第二个选项:导入指定的配置,和第一个选项类似。


    • 第三个选项:不导入先前配置,这里需要手动配置SDK和导入项目的操作。若是为了体验最新版本的Studio,创建新项目,可以选该选项。



    选择第一个选项第二个选项是,多版本Studio共同开发同一个项目,无需下面操作,重要的事情强调三遍。


    本人这里不导入先前配置,因此选择do not import settings,接下来手动导入原本的SDK配置。


    点击OK后,出现正常安装界面,如下图:


    5.png


    点击Next后,在Install Type界面上,选择Custom选项,自定义配置,如下图所示:


    6.png


    点击Next后,在SDK Components Setup界面,在SDK Location选项中,选择原本旧版本studio下载好的SDK路径,如下图所示:


    7.png


    点击Next后,在Verify Settings界面,选择Cancel,不更SDK的配置,如下图:


    8.png


    最后,Welcome to Android Studio界面,如下图所示:


    9.png


    接下,是新创建项目,还是从版本托管拖拉项目,还是导入原本旧项目,取决个自己的需求。


    资源参考:


    Studio多版本共存:developer.android.google.cn/studio/prev…


    Studio下载: developer.android.google.cn/studio/inde…








    收起阅读 »

    开发者实践丨盲水印插件:用户端的实时视频溯源保护

    本文作者是 RTE 2021 创新编程挑战赛获奖者董章晔团队。在实时音视频领域,视频内容会需要得到版权保护,而盲水印则是保护的措施之一。这支参赛团队基于声网 SDK 开发了一款应用于用户端的实时视频盲水印插件。其他使用声网 SDK 的开发者,也同样可以在自己的...
    继续阅读 »

    本文作者是 RTE 2021 创新编程挑战赛获奖者董章晔团队。在实时音视频领域,视频内容会需要得到版权保护,而盲水印则是保护的措施之一。这支参赛团队基于声网 SDK 开发了一款应用于用户端的实时视频盲水印插件。其他使用声网 SDK 的开发者,也同样可以在自己的应用中使用该插件。访问『阅读原文』,可以查看该项目的源码。

    项目介绍

    视频盲水印技术是将标识信息直接嵌入视频 RGB 或 YUV 的频域中,基本不影响原视频的观看质量,也不容易被人觉察或注意。通过这些隐藏在载体中的信息,可确认内容创建者、使用者或者判断视频是否被篡改。该技术通常由专业的版权保护服务商提供,用于广播电视版权保护,商业性较强。

    本项目基于声网的 SDK 开发了一款用户端的实时视频盲水印的插件,同时,配套提供了一款基于个人 PC 端的水印识别软件,用于水印验证。降低了使用盲水印服务的专业门槛,为个人用户的隐私保护和作品防盗版提供了便捷的解决方案。

    实现原理

    盲水印的实现原理是在频域上完成信息叠加,变换的方法包括离散傅立叶变换、小波变换等,比如采用傅里叶变换,在实部和虚部完成文字图像叠加,再通过逆变换显示视频帧。

    图片

    对视频帧提取水印的方法是对视频帧截图,对截图再进行一次傅里叶变换,得到频域数据,对频域幅度,即能量进行显示,得出频域幅度图,就会显示之前叠加的文字。

    图片

    快速傅里叶变换复杂度为 O(nlog(n)),原理上可以在视频处理过程中实现盲水印的实时叠加。

    设计实现

    程序设计包括声网 SDK 对接和盲水印开发两部分,盲水印开发分为 Android 端叠加水印和 Windows 提取水印两部分。分别是灰色、黄色和橙色三块。由于是演示 Demo,所以仅在本地视频预览上完成盲水印的处理,未来可扩展到视频显示上。

    图片

    该方案设计重点考虑 SDK 衔接和第三方兼容两大方面。主要是少拷贝YUV数据、视频处理串行化、第三方兼容性和场景泛化等方面。

    核心代码

    叠加水印的主流程:

    图片

    opencv 的调用函数:

    图片

    主要是傅立叶变换和叠加文字两个函数,声网 SDK 与 OpenCV 开源库兼容效果良好。

    效果展示

    图片

    第一幅图是原始视频,输入水印文字比如 wm,第二幅图是叠加盲水印的视频,可见视频效果基本不受影响,最后一幅图是将第二幅图上传到 PC 后,用户自己提取水印的图像,可见图像中有明显的 wm 文字。至此完成了验证。

    未来展望

    下一步计划主要从提高水印的鲁棒性、扩展水印的应用场景、丰富水印的数据维度等方面进行考虑。在水印鲁棒性方面,计划空域上进行网格化分割,针对不同分割区域进行频域水印叠加;采用不同的变换方法,例如 DWT,以求最佳效果;对水印本身进行冗余编码,提升水印辨识度,增加水印的隐蔽性。在扩展水印应用方面,在实时视频显示端,进行水印叠加,达到偷拍溯源的目的。在丰富数据维度方面,在音频处理上,可扩展声纹水印;结合视频内容特征,可扩展特征编码等。


    收起阅读 »

    Jetpact Compose状态管理简单理解

    概览所谓的状态可以简单的理解为应用中的某个值的变化,比如可以是一个布尔值、数组放在业务的场景中,可以是 TextField 中的文字、动画执行的状态、用户收藏的商品都是状态我们知道 compose 是声明式的 ui,每次我们重组页面的时候都会把组件重组,此时就...
    继续阅读 »

    概览

    所谓的状态可以简单的理解为应用中的某个值的变化,比如可以是一个布尔值、数组

    放在业务的场景中,可以是 TextField 中的文字、动画执行的状态、用户收藏的商品都是状态

    我们知道 compose 是声明式的 ui,每次我们重组页面的时候都会把组件重组,此时就需要引入状态进行管理,例如:

    我们在商品的 item 里面点击按钮收藏了商品,此时商品的收藏状态发生了改变,我们需要重组 ui 将商品变为已收藏状态,这个时候就需要用 remember 扩展方法保存重组状态,如果使用 boolean 这个基本类型保存那么就无法在重组 ui 后正常的设置组件的状态。

    代码举例(抄官方代码):

    @Composable
    fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
    OutlinedTextField(
    value = "输入的值",
    onValueChange = { },
    label = { Text("Name") }
    )
    }
    }

    运行上面的代码,我们会发现无论我们如何在 TextField 中输入内容,TextFile 的内容都不会变,这就是因为无法保存状态导致的,以下代码示例可以正常的改变 TextField 中的内容


    @Composable
    fun textFieldStateHasTextShow(){
    var value by remember {//这里就是对TextField中展示的文字进行状态保存的操作
    mutableStateOf("")
    }
    Box(modifier = Modifier.fillMaxSize(1f),contentAlignment = Alignment.Center) {
    OutlinedTextField(
    value = value,
    onValueChange = {
    value=it//每次输入内容的时候,都回调这个更新状态,从而刷新重组ui
    },
    label = { Text("Name") }
    )
    }
    }

    状态管理的常用方法

    remember 重组中保存状态

    组合函数可以通过remember记住单个对象,系统会在初始化期间将remember初始的值存储在组合中。重组的时候可以返回对象值,remember既可以用来存储可变对象又可以存储不可变的对象

    当可组合项被移除后,会忘记 remember 存储的对象。

    mutableStateOf

    mutableStateOf 会创建可观察的 MutableState<T>,例如如下代码: data 就是一个MutableState对象

    每当data.value值发生改变的时候,系统就会重组ui。

    var data = remember {
    mutableStateOf("")
    }

    注:mutableStateOf 必须使用 remember 嵌套才能在数据更改的时候重组界面

    rememberSaveable 保存配置

    remember可以帮助我们在界面重组的时候保存状态,而rememberSaveable可以帮助我们存储配置更改(重新创建activity或进程)时的状态。

    Livedata、Flow、RxJava 转换为状态

    这三个框架是安卓常用的三个响应式开发框架,都支持转化为State对象,以 Flow 举例,如下代码可以转化为一个 State:

      val favorites = MutableStateFlow<Set<String>>(setOf())
    val state = favorites.collectAsState()

    状态管理

    有状态和无状态

    使用 remember、rememberSaveState 方法保存状态的组合项是有状态组合

    反之是无状态组合

    状态提升

    如下代码是官方关于状态提升的代码:

    本例代码中 HelloContent 是无状态的,它的状态被提升到了 HelloScreen 中,HelloContent 有nameonNameChange两个参数,name 是状态,通过 HelloScreen 组合项传给 HelloContent

    而 HelloContent 中发生的更改它也不能自己进行处理,必须将更改传给HelloScreen进行处理并重组界面。

    以上的逻辑叫做:状态下降,事件上升

    @Composable
    fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
    }

    @Composable
    fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
    Text(
    text = "Hello, $name",
    modifier = Modifier.padding(bottom = 8.dp),
    style = MaterialTheme.typography.h5
    )
    OutlinedTextField(
    value = name,
    onValueChange = onNameChange,
    label = { Text("Name") }
    )
    }
    }

    存储状态的方式

    前面的介绍中我们知道使用rememberSaveable方法我们可以通过 Bundle 的方式保存状态,那么如果我们要保存的状态不方便用 Bundle 的情况下该何如处理呢?

    以下三种方式,可以实现对非 Bundle 的数据的保存(配置更改后的保存)

    Parcelize

    代码示例:

    @Parcelize
    data class City(val name: String, val country: String) : Parcelable

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

    MapSaver

    data class City(val name: String, val country: String)

    val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
    save = { mapOf(nameKey to it.name, countryKey to it.country) },
    restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
    }

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

    ListSaver

    data class City(val name: String, val country: String)

    val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },//数组中保存的值和City中的属性是顺序对应的
    restore = { City(it[0] as String, it[1] as String) }
    )

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

    状态管理源码分析

    remember

    初次阅读 remember 的源码,可能有理解不对的地方(但总得有人先去看不是),多多见谅,欢迎指正

    • remember 方法调用的主流程

    remember方法返回的是一个MutableState对象,MutableState可以在数据更新的时候通知系统重组ui

     rememberedValue 就是数据转换的逻辑

    • rememberedValue 方法解析

    inserting:如果我们正在将新的节点插入到视图数中,那么 inserting=true

    reusing:意为正在重用,我的理解是当前正在重新使用这个状态,所以避免多次获取

    • reader.next 方法 晒一段源码
      fun next(): Any? {
    if (emptyCount > 0 || currentSlot >= currentSlotEnd) return Composer.Empty
    return slots[currentSlot++]
    }

    slots是一个数组,currentSlot表示我们要获取到的状态在数组中的索引,compose 构建页面是单线程的,索引每次我们调用remember方法的时候如果状态已经存在就从slots中获取数据,然后把currentSlot索引加 1,这样当我们调用了最后一个remember方法的时候currentSlot索引刚好等于slots数组.length-1


    收起阅读 »

    我用index作为key也没啥问题啊,为什么面试还有人diao我???

    所有熟悉 Vue 技术栈的小伙伴,都知道在列表渲染的场景下,不能使用 index 或 random 作为 key。 也有很多小伙伴在面试的时候会被面试官比较详细的追问,假如使用 index 作为 key 会有什么问题?假如使用 random 作为 key 会有...
    继续阅读 »

    所有熟悉 Vue 技术栈的小伙伴,都知道在列表渲染的场景下,不能使用 indexrandom 作为 key


    也有很多小伙伴在面试的时候会被面试官比较详细的追问,假如使用 index 作为 key 会有什么问题?假如使用 random 作为 key 会有什么问题?假如使用一个唯一不变的 id 作为 key 有什么好处呢?


    这道题目,表面上看起来是考察我们对同级比较过程中 diff 算法的理解,唯一不变的 key 可以帮助我们更快的找到可复用的 VNode,节省性能开销,使用 index 作为 key 有可能造成 VNode 错误的复用,从而产生 bug ,而使用 random 作为 key 会导致VNode 始终无法复用,极大的影响性能。


    这么回答有问题么?没有问题。


    但是假如这道题目满分100,我只能给你99分。


    还有 1分,涉及到 Vue 更新流程中的一点点细节,若不理清,可能在实际的业务场景中给我们造成困扰。


    啥困扰呢?


    举个栗子


    直奔主题,看一段代码,index 作为 key ,假如我们删除某一条,结果会是啥呢?


    <template>
     <div id="app">
       <div v-for="(item, index) in data" :key="index">
         <Child />
         <button @click="handleDelete(index)">删除这一行</button>
       </div>
     </div>
    </template>

    <script>

    export default {
     name: "App",
     components: {
       Child: {
         template: '<span>{{name}}{{Math.floor(Math.random() * 1000)}}</span>',
         props: ['name']
      }
    },
     data() {
       return {
         data: [
          { name: "小明" },
          { name: "小红" },
          { name: "小蓝" },
          { name: "小紫" },
        ]
      };
    },
     methods: {
       handleDelete(index) {
         this.data.splice(index, 1);
      },
    }
    };
    </script>

    看结果



    可以观察到,虽然我们删除的不是最后一条,但最终却是最后一条被删除了,看起来很奇怪,但是假如你了解过 Vuediff 流程,这个结果应该是可以符合你的预期的。


    diff


    大段的列源码,会增加我们的理解负担,所以我把 Vue更新流程简化成一张图:



    通常来讲,我们说 Vuediff 流程,指的就是 patchVnode ,其中 updateChildren 就是我们说的同层比较,其实就是比较新旧两个 Vnode 数组。


    Vue 会声明四个指针变量,分别记录新旧 Vnode 数组的首尾索引,通过首尾索引指针的移动,根据新头旧头、新尾旧尾、旧头新尾、旧尾新头的顺序,依次比较新旧 Vnode ,若不能命中 sameVnode,则将oldVnode.key 维护成一个 map, 继续查询是否包含newVnode.key ,若命中 sameVnode ,则递归执行 patchVnode。若最终无法命中,说明无可复用的 Vnode ,创建新的 dom 节点。


    newVnode 的首尾指针先相遇,说明 newVnode 已经遍历完成,直接移除 oldVnode 多余部分,若 oldVnode 的首尾指针先相遇,说明 oldVnode 已经遍历完成,直接新增 newVnode 的多余部分。


    这种直接的文字描述会显得比较苍白,所以我给大家准备了个动画


    第一步:



    第二步:



    第三步:



    第四步:



    第五步:



    第六步:



    理论上,只要你滑动的足够快,这几张图就可以动起来😊



    上面描述updateChildren过程的图片均摘自 Vue技术揭秘 组件更新章节,建议大家翻阅原文


    我尝试了半天实在做不出来动画,同时感觉这几张图已经可以带给我们足够直观的感受了,所以直接搬运了


    侵删



    使用 index 作为 key 会有什么问题


    上面我们讲,判断新旧 Vnode 是否可以复用,取决于 sameNode 方法,这个方法非常简单,就是比对 Vnode 的部分属性,其中 key 是最关键的因素


    function sameVnode (a, b) {
       return (
         a.key === b.key &&
         a.asyncFactory === b.asyncFactory && (
          (
             a.tag === b.tag &&
             a.isComment === b.isComment &&
             isDef(a.data) === isDef(b.data) &&
             sameInputType(a, b)
          ) || (
             isTrue(a.isAsyncPlaceholder) &&
             isUndef(b.asyncFactory.error)
          )
        )
      )
    }

    我们再回到上面的栗子,看看是哪里出了问题


    上面代码生成的 VNode 大约是这样的:


    [
    {
       tag: 'div',
    key: 0,
    children: [
    {
    tag: VueComponent,
           elm: 408, // 这个Vnode对应的真实dom是408
    },
    {
    tag: 'button'
    }
    ]
    },
    {
       tag: 'div',
    key: 1,
    children: [
    {
    tag: VueComponent,
           elm: 227, // 这个Vnode对应的真实dom是227
    },
    {
    tag: 'button'
    }
    ]
    }
     ...
    ]

    我们删除第一条数据,新的 VNode 大约是这样的:


    [
    {
       tag: 'div',
    key: 0,
    children: [
    {
    tag: VueComponent,
           elm: 227, // 这个Vnode对应的真实dom是227
    },
    {
    tag: 'button'
    }
    ]
    },
    {
       tag: 'div',
    key: 1,
    children: [
    {
    tag: VueComponent,
           elm: 324, // 这个Vnode对应的真实dom是324
    },
    {
    tag: 'button'
    }
    ]
    }
     ...
    ]

    我们人肉逻辑 一下这两个 Vnode 数组,由于 key 都是0,所以比较第一条的时候,就会命中 sameNode ,导致错误复用,然后 updateChildren ,子节点的 Vnode 依然会命中 sameVnode ,同理,第二、三条均会命中 sameVnode ,而直接错误复用其关联的真实 dom 节点,所以我们明明删除的是第一条,UI表现却是最后一条被删除了。


    那么到这里就结束了么?


    当然没有,因为很多小伙伴在刚接触 Vue 的时候,也用过 index 作为 key ,部分牛逼的项目甚至已经上线了,似乎也没人来找麻烦


    why?


    为什么我用 index 作为 key 没出现问题


    如果我把代码改成这样,再删除某一条,会是什么结果呢?


    <template>
     <div id="app">
       <div v-for="(item, index) in data" :key="index">
         <Child :name="`${item.name}`" />
         <button @click="handleDelete(index)">删除这一行</button>
       </div>
     </div>
    </template>

    看结果



    法克,我们明明把 Vue更新流程捋清楚了,用 index 作为 key 会导致 Vnode 错误复用啊,怎么这里表现却正常了呢?


    我们再看一下更新流程简化图:



    组件类型的 Vnode ,在 patchVnode 的过程中会执行 prePatch 钩子函数,给组件的 propsData 重新赋值,从而触发 setter ,假如 propsData 的值有变化,则会触发 update ,重新渲染组件


    我们可以再人肉逻辑 一下,这次我们删除的是第二条,因为key 一致,新的 Vnode 数组依然会复用旧的 Vnode 数组的前三条,第一条 Vnode 是正确复用,组件的 propsData 未发生变化,不会触发 update ,直接复用其关联的真实 dom 节点,但是第二条 Vnode 是错误复用,但是组件的 propsData 发生变化,由小红变成了小蓝,触发了 update ,组件重新渲染,因此我们看到其实连 random 都发生了变化,第三条同理。


    呼~


    到这里,总算是搞明白了,我可真是个小机灵鬼


    那么到这里就结束了么?


    其实还没有,比如我们再改一下代码


    <template>
     <div id="app">
       <div v-for="(item, index) in data" :key="index">
         <span>{{item.name}}</span>
         <button @click="handleDelete(index)">删除这一行</button>
       </div>
     </div>
    </template>

    看结果



    这次我们没有组件类型 Vnode ,不会执行 prePatch,为啥表现还是正常的呢?


    再观察一下上面的更新流程图,文本类型的 Vnode ,新旧文本不同的时候是会直接覆盖的。


    到这里,我们已经完全明白,列表渲染的场景下,为什么推荐使用唯一不变的 id 作为 key了。抛开代码规范不谈,即使某些场景下,问题并未以 bug 的形式暴露出来,但是不能复用、或者错误复用 Vnode ,都会导致组件重新渲染,这部分的性能包袱还是非常沉重的!


    最后的1分


    纸上得来终觉浅,绝知此事要躬行


    我第一次读完 Vue2 源码的时候,以为自己已经清晰的明白了这部分知识,直到团队里的小伙伴拿着一个纯文本类型的列表来质问我


    不得已仔细 debug 了一遍更新流程,才算解开了心中疑惑,补上了这 1分 的缺口



    链接:https://juejin.cn/post/6999932053466644517

    收起阅读 »

    Android 10 启动分析之Init篇 (一)

    按下电源键时,android做了啥?当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好...
    继续阅读 »

    按下电源键时,android做了啥?

    当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。

    bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好软件环境,最后调用操作系统内核”。除此之外,bootloader还有保护功能,部分品牌的手机对bootloader做了加锁操作,防止boot分区和recovery分区被写入。

    或许有人会问了,什么是boot分区,什么又是recovery分区?

    我们先来认识一下Android系统的常见分区:

    /boot

    这个分区上有Android的引导程序,包括内核和内存操作程序。没有这个分区设备就不能被引导。恢复系统的时候会擦除这个分区,并且必须重新安装引导程序和ROM才能重启系统。

    /recovery

    recovery分区被认为是另一个启动分区,你可以启动设备进入recovery控制台去执行高级的系统恢复和管理操作。

    /data

    这个分区保存着用户数据。通讯录、短信、设置和你安装的apps都在这个分区上。擦除这个分区相当于恢复出厂设置,当你第一次启动设备的时候或者在安装了官方或者客户的ROM之后系统会自动重建这个分区。当你执行恢复出厂设置时,就是在擦除这个分区。

    /cache

    这个分区是Android系统存储频繁访问的数据和app的地方。擦除这个分区不影响你的个人数据,当你继续使用设备时,被擦除的数据就会自动被创建。

    /apex

    Android Q新增特性,将系统功能模块化,允许系统按模块来独立升级。此分区用于存放apex 相关的内容。

    为什么需要bootloader去拉起linux内核,而不把bootloader这些功能直接内置在linux内核中呢?这个问题不在此做出回答,留给大家自行去思考。

    bootloader完成初始化工作后,会载入 /boot 目录下面的 kernel,此时控制权转交给操作系统。操作系统将要完成的存储管理、设备管理、文件管理、进程管理、加载驱动等任务的初始化工作,以便进入用户态。

    内核启动完成后,将会寻找init文件(init文件位于/system/bin/init),启动init进程,也就是android的第一个进程。

    我们来关注一下内核的common/init/main.c中的kernel_init方法。

    static int __ref kernel_init(void *unused)
    {
    ...

    if (execute_command) {
    ret = run_init_process(execute_command);
    if (!ret)
    return 0;
    }
    if (CONFIG_DEFAULT_INIT[0] != '\0') {
    ret = run_init_process(CONFIG_DEFAULT_INIT);
    if (ret)
    pr_err("Default init %s failed (error %d)\n",CONFIG_DEFAULT_INIT, ret);
    else
    return 0;
    }

    if (!try_to_run_init_process("/sbin/init") ||!try_to_run_init_process("/etc/init") ||
    !try_to_run_init_process("/bin/init") ||!try_to_run_init_process("/bin/sh"))
    return 0;
    }

    可以看到,在init_kernel的最后,会调用run_init_process方法来启动init进程。

    static int run_init_process(const char *init_filename){
    const char *const *p;
    argv_init[0] = init_filename;
    return kernel_execve(init_filename, argv_init, envp_init);
    }

    kernel_execve是内核空间调用用户空间的应用程序的函数。

    接下来我们来重点分析init进程。

    init进程解析

    我们从system/core/init/main.cpp 这个文件开始看起。

    int main(int argc, char** argv) {
    #if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
    #endif

    if (!strcmp(basename(argv[0]), "ueventd")) {
    return ueventd_main(argc, argv);
    }

    if (argc > 1) {
    if (!strcmp(argv[1], "subcontext")) {
    android::base::InitLogging(argv, &android::base::KernelLogger);
    const BuiltinFunctionMap function_map;

    return SubcontextMain(argc, argv, &function_map);
    }

    if (!strcmp(argv[1], "selinux_setup")) {
    return SetupSelinux(argv);
    }

    if (!strcmp(argv[1], "second_stage")) {
    return SecondStageMain(argc, argv);
    }
    }

    return FirstStageMain(argc, argv);
    }

    第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数。

    main函数有四个参数入口:

    • 一是参数中有ueventd,进入ueventd_main

    • 二是参数中有subcontext,进入InitLogging 和SubcontextMain

    • 三是参数中有selinux_setup,进入SetupSelinux

    • 四是参数中有second_stage,进入SecondStageMain

    main的执行顺序如下:

    1.  FirstStageMain  启动第一阶段

    2. SetupSelinux    加载selinux规则,并设置selinux日志,完成SELinux相关工作

    3. SecondStageMain  启动第二阶段

    4.  ueventd_main    init进程创建子进程ueventd,并将创建设备节点文件的工作托付给ueventd。

    FirstStageMain

    我们来从FirstStageMain的源码看起,源码位于/system/core/init/first_stage_init.cpp

    int FirstStageMain(int argc, char** argv) {

    boot_clock::time_point start_time = boot_clock::now();

    #define CHECKCALL(x) \
    if (x != 0) errors.emplace_back(#x " failed", errno);

    // Clear the umask.
    umask(0);

    //初始化系统环境变量
    CHECKCALL(clearenv());
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
    // 挂载及创建基本的文件系统,并设置合适的访问权限
    CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
    CHECKCALL(mkdir("/dev/pts", 0755));
    CHECKCALL(mkdir("/dev/socket", 0755));
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
    #define MAKE_STR(x) __STRING(x)
    CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
    #undef MAKE_STR
    // 不要将原始命令行公开给非特权进程
    CHECKCALL(chmod("/proc/cmdline", 0440));
    gid_t groups[] = {AID_READPROC};
    CHECKCALL(setgroups(arraysize(groups), groups));
    CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
    CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));

    CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));

    if constexpr (WORLD_WRITABLE_KMSG) {
    CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
    }

    //创建linux随机伪设备文件
    CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
    CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));

    //log wrapper所必须的,需要在ueventd运行之前被调用
    CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
    CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

    ...

    //将内核的stdin/stdout/stderr 全都重定向/dev/null,关闭默认控制台输出
    SetStdioToDevNull(argv);
    // tmpfs已经挂载到/dev上,同时我们也挂载了/dev/kmsg,我们能够与外界开始沟通了
    //初始化内核log
    InitKernelLogging(argv);

    //检测上面的操作是否发生了错误
    if (!errors.empty()) {
    for (const auto& [error_string, error_errno] : errors) {
    LOG(ERROR) << error_string << " " << strerror(error_errno);
    }
    LOG(FATAL) << "Init encountered errors starting first stage, aborting";
    }

    LOG(INFO) << "init first stage started!";

    auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{opendir("/"), closedir};
    if (!old_root_dir) {
    PLOG(ERROR) << "Could not opendir("/"), not freeing ramdisk";
    }

    struct stat old_root_info;

    ...

    //挂载 system、cache、data 等系统分区
    if (!DoFirstStageMount()) {
    LOG(FATAL) << "Failed to mount required partitions early ...";
    }

    ...

    //进入下一步,SetupSelinux
    const char* path = "/system/bin/init";
    const char* args[] = {path, "selinux_setup", nullptr};
    execv(path, const_cast<char**>(args));

    return 1;
    }

    我们来总结一下,FirstStageMain到底做了哪些重要的事情:

    1. 挂载及创建基本的文件系统,并设置合适的访问权限

    2. 关闭默认控制台输出,并初始化内核级log。

    3. 挂载 system、cache、data 等系统分区

    SetupSelinux

    这个模块主要的工作是设置SELinux安全策略,本章内容主要聚焦于android的启动流程,selinux的内容在此不做展开。

    int SetupSelinux(char** argv) {

    ...

    const char* path = "/system/bin/init";
    const char* args[] = {path, "second_stage", nullptr};
    execv(path, const_cast<char**>(args));

    return 1;
    }

    SetupSelinux的最后,进入了init的第二阶段SecondStageMain。

    SecondStageMain

    不多说,先上代码。

    int SecondStageMain(int argc, char** argv) {
    // 禁止OOM killer 杀死该进程以及它的子进程
    if (auto result = WriteFile("/proc/1/oom_score_adj", "-1000"); !result) {
    LOG(ERROR) << "Unable to write -1000 to /proc/1/oom_score_adj: " << result.error();
    }

    // 启用全局Seccomp,Seccomp是什么请自行查阅资料
    GlobalSeccomp();

    // 设置所有进程都能访问的会话密钥
    keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);

    // 创建 /dev/.booting 文件,就是个标记,表示booting进行中
    close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

    //初始化属性服务,并从指定文件读取属性
    property_init();

    ...

    // 进行SELinux第二阶段并恢复一些文件安全上下文
    SelinuxSetupKernelLogging();
    SelabelInitialize();
    SelinuxRestoreContext();

    //初始化Epoll,android这里对epoll做了一层封装
    Epoll epoll;
    if (auto result = epoll.Open(); !result) {
    PLOG(FATAL) << result.error();
    }

    //epoll 中注册signalfd,主要是为了创建handler处理子进程终止信号
    InstallSignalFdHandler(&epoll);

    ...

    //epoll 中注册property_set_fd,设置其他系统属性并开启系统属性服务
    StartPropertyService(&epoll);
    MountHandler mount_handler(&epoll);

    ...

    ActionManager& am = ActionManager::GetInstance();
    ServiceList& sm = ServiceList::GetInstance();
    //解析init.rc等文件,建立rc文件的action 、service,启动其他进程,十分关键的一步
    LoadBootScripts(am, sm);

    ...

    am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");

    //执行rc文件中触发器为 on early-init 的语句
    am.QueueEventTrigger("early-init");

    // 等冷插拔设备初始化完成
    am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");

    am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
    am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
    am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");

    // 设备组合键的初始化操作
    Keychords keychords;
    am.QueueBuiltinAction(
    [&epoll, &keychords](const BuiltinArguments& args) -> Result<Success> {
    for (const auto& svc : ServiceList::GetInstance()) {
    keychords.Register(svc->keycodes());
    }
    keychords.Start(&epoll, HandleKeychord);
    return Success();
    },
    "KeychordInit");
    am.QueueBuiltinAction(console_init_action, "console_init");

    // 执行rc文件中触发器为on init的语句
    am.QueueEventTrigger("init");

    // Starting the BoringSSL self test, for NIAP certification compliance.
    am.QueueBuiltinAction(StartBoringSslSelfTest, "StartBoringSslSelfTest");

    // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
    // wasn't ready immediately after wait_for_coldboot_done
    am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");


    am.QueueBuiltinAction(InitBinder, "InitBinder");

    // 当设备处于充电模式时,不需要mount文件系统或者启动系统服务,充电模式下,将charger设为执行队列,否则把late-init设为执行队列
    std::string bootmode = GetProperty("ro.bootmode", "");
    if (bootmode == "charger") {
    am.QueueEventTrigger("charger");
    } else {
    am.QueueEventTrigger("late-init");
    }

    // 基于属性当前状态 运行所有的属性触发器.
    am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");

    while (true) {
    //开始进入死循环状态
    auto epoll_timeout = std::optional<std::chrono::milliseconds>{};

    //执行关机重启流程
    if (do_shutdown && !shutting_down) {
    do_shutdown = false;
    if (HandlePowerctlMessage(shutdown_command)) {
    shutting_down = true;
    }
    }

    if (!(waiting_for_prop || Service::is_exec_service_running())) {
    am.ExecuteOneCommand();
    }
    if (!(waiting_for_prop || Service::is_exec_service_running())) {
    if (!shutting_down) {
    auto next_process_action_time = HandleProcessActions();

    // If there's a process that needs restarting, wake up in time for that.
    if (next_process_action_time) {
    epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
    *next_process_action_time - boot_clock::now());
    if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
    }
    }

    // If there's more work to do, wake up again immediately.
    if (am.HasMoreCommands()) epoll_timeout = 0ms;
    }

    // 循环等待事件发生
    if (auto result = epoll.Wait(epoll_timeout); !result) {
    LOG(ERROR) << result.error();
    }
    }

    return 0;
    }

    总结一下,第二阶段做了以下这些比较重要的事情:

    1. 初始化属性服务,并从指定文件读取属性
    2. 初始化epoll,并注册signalfd和property_set_fd,建立和init的子进程以及部分服务的通讯桥梁
    3. 初始化设备组合键,使系统能够对组合键信号做出响应
    4. 解析init.rc文件,并按rc里的定义去启动服务
    5. 开启死循环,用于接收epoll的事件

    在第二阶段,我们需要重点关注以下问题:

    init进程是如何通过init.rc配置文件去启动其他的进程的呢?

    init.rc 解析

    我们从 LoadBootScripts(am, sm)这个方法开始看起,一步一部来挖掘init.rc 的解析流程。

    static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    //初始化ServiceParse、ActionParser、ImportParser三个解析器
    Parser parser = CreateParser(action_manager, service_list);

    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
    //bootscript为空,进入此分支
    parser.ParseConfig("/init.rc");
    if (!parser.ParseConfig("/system/etc/init")) {
    late_import_paths.emplace_back("/system/etc/init");
    }
    if (!parser.ParseConfig("/product/etc/init")) {
    late_import_paths.emplace_back("/product/etc/init");
    }
    if (!parser.ParseConfig("/product_services/etc/init")) {
    late_import_paths.emplace_back("/product_services/etc/init");
    }
    if (!parser.ParseConfig("/odm/etc/init")) {
    late_import_paths.emplace_back("/odm/etc/init");
    }
    if (!parser.ParseConfig("/vendor/etc/init")) {
    late_import_paths.emplace_back("/vendor/etc/init");
    }
    } else {
    parser.ParseConfig(bootscript);
    }
    }

    我们可以看到这句话,Parse开始解析init.rc文件,在深入下去之前,让我们先来认识一下init.rc。

     parser.ParseConfig("/init.rc")

    init.rc是一个可配置的初始化文件,负责系统的初步建立。它的源文件的路径为 /system/core/rootdir/init.rc

    init.rc文件有着固定的语法,由于内容过多,限制于篇幅的原因,在此另外单独开了一篇文章进行讲解:

    Android 10 启动分析之init语法

    了解了init.rc的语法后,我们来看看init.rc文件里的内容。

    import /init.environ.rc  //导入全局环境变量
    import /init.usb.rc //adb 服务、USB相关内容的定义
    import /init.${ro.hardware}.rc //硬件相关的初始化,一般是厂商定制
    import /vendor/etc/init/hw/init.${ro.hardware}.rc
    import /init.usb.configfs.rc
    import /init.${ro.zygote}.rc //定义Zygote服务

    我们可以看到,在/system/core/init目录下,存在以下四个zygote相关的文件

    image.png

    怎样才能知道我们当前的手机用的是哪个配置文件呢?

    答案是通过adb shell getprop | findstr ro.zygote命令,看看${ro.zygote}这个环境变量具体的值是什么,笔者所使用的华为手机的ro.zygote值如下所示:

    image.png

    什么是Zygote,Zygote的启动过程是怎样的,它的启动配置文件里又做了啥,在这里我们不再做进一步探讨, 只需要知道init在一开始在这个文件中对Zygote服务做了定义,而上述的这些问题将留到 启动分析之Zygote篇 再去说明。

    on early-init
    # Disable sysrq from keyboard
    write /proc/sys/kernel/sysrq 0

    # Set the security context of /adb_keys if present.
    restorecon /adb_keys

    # Set the security context of /postinstall if present.
    restorecon /postinstall

    mkdir /acct/uid

    # memory.pressure_level used by lmkd
    chown root system /dev/memcg/memory.pressure_level
    chmod 0040 /dev/memcg/memory.pressure_level
    # app mem cgroups, used by activity manager, lmkd and zygote
    mkdir /dev/memcg/apps/ 0755 system system
    # cgroup for system_server and surfaceflinger
    mkdir /dev/memcg/system 0550 system system

    start ueventd

    # Run apexd-bootstrap so that APEXes that provide critical libraries
    # become available. Note that this is executed as exec_start to ensure that
    # the libraries are available to the processes started after this statement.
    exec_start apexd-bootstrap

    紧接着是一个Action,Action的Trigger 为early-init,在这个 Action中,我们需要关注最后两行,它启动了ueventd服务和apex相关服务。还记得什么是ueventd和apex吗?不记得的读者请往上翻越再自行回顾一下。

    ueventd服务的定义也可以在init.rc文件的结尾找到,具体代码及含义如下:

    service ueventd    //ueventd服务的可执行文件的路径为 /system/bin/ueventd
    class core //ueventd 归属于 core class,同样归属于core class的还有adbd 、console等服务
    critical //表明这个Service对设备至关重要,如果Service在四分钟内退出超过4次,则设备将重启进入恢复模式。
    seclabel u:r:ueventd:s0 //selinux相关的配置
    shutdown critical //ueventd服务关闭行为

    然而,early-init 这个Trigger到底什么时候触发呢?

    答案是通过init.cpp代码调用触发。

    我们可以在init.cpp 代码中找到如下代码片段:

    am.QueueEventTrigger("early-init");

    QueueEventTrigger这个方法的实现机制我们稍后再进行探讨,目前我们只需要了解, ActionManager 这个类中的 QueueEventTrigger方法,负责触发init.rc中的Action。

    我们继续往下看init.rc的内容。

    on init

    ...

    # Start logd before any other services run to ensure we capture all of their logs.
    start logd

    # Start essential services.
    start servicemanager
    ...

    在Trigger 为init的Action中,我们只需要关注以上的关键内容。在init的action中启动了一些核心的系统服务,这些服务具体的含义为 :

    服务名含义
    logdAndroid L加入的服务,用于保存Android运行期间的日志
    servicemanagerandroid系统服务管理者,负责查询和注册服务

    接下来是late-init Action:

    on late-init
    //启动vold服务(管理和控制Android平台外部存储设备,包括SD插拨、挂载、卸载、格式化等)
    trigger early-fs
    trigger fs
    trigger post-fs
    trigger late-fs

    //挂载/data , 启动 apexd 服务
    trigger post-fs-data

    # 读取持久化属性或者从/data 中读取并覆盖属性
    trigger load_persist_props_action

    //启动zygote服务!!在启动zygote服务前会先启动netd服务(专门负责网络管理和控制的后台守护进程)
    trigger zygote-start

    //移除/dev/.booting 文件
    trigger firmware_mounts_complete

    trigger early-boot
    trigger boot //初始化网络环境,设置系统环境和守护进程的权限

    最后,我们用流程图来总结一下上述的启动过程:

    first_stage
    second_stage
    设置SetupSelinux安全策略
    挂载system、cache、data等系统分区
    初始化内核log
    初始化基本的文件系统
    初始化系统环境变量
    开启死循环,用于接收epoll的事件
    启动zygote服务
    启动netd服务
    挂载/data,启动apexd服务
    启动vold服务
    启动servicemanager服务
    启动logd服务
    启动ueventd服务
    解析init.rc文件
    初始化设备组合键
    初始化epoll
    初始化属性服务
    Click Power Button
    bootloader
    linux kernel
    init进程
    收起阅读 »

    Android 10 启动分析之servicemanager篇 (二)

    上一篇文章:Android 10 启动分析之Init篇 (一)在前文提到,init进程会在在Trigger 为init的Action中,启动servicemanager服务,这篇文章我们就来具体分析一下servicemanager服务,它到底做了哪些事情。se...
    继续阅读 »

    上一篇文章:

    Android 10 启动分析之Init篇 (一)

    在前文提到,init进程会在在Trigger 为init的Action中,启动servicemanager服务,这篇文章我们就来具体分析一下servicemanager服务,它到底做了哪些事情。

    servicemanager服务的源码位于/frameworks/native/cmds/servicemanager/service_manager.c,我们将从这个类的入口开始看起。

    int main(int argc, char** argv)
    {
    struct binder_state *bs;
    union selinux_callback cb;
    char *driver;

    if (argc > 1) {
    driver = argv[1];
    } else {
    //启动时默认无参数,走这个分支
    driver = "/dev/binder";
    }

    //打开binder驱动,并设置mmap的内存大小为128k
    bs = binder_open(driver, 128*1024);

    ...

    if (binder_become_context_manager(bs)) {
    ALOGE("cannot become context manager (%s)\n", strerror(errno));
    return -1;
    }

    cb.func_audit = audit_callback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);
    #ifdef VENDORSERVICEMANAGER
    cb.func_log = selinux_vendor_log_callback;
    #else
    cb.func_log = selinux_log_callback;
    #endif
    selinux_set_callback(SELINUX_CB_LOG, cb);

    #ifdef VENDORSERVICEMANAGER
    sehandle = selinux_android_vendor_service_context_handle();
    #else
    sehandle = selinux_android_service_context_handle();
    #endif
    selinux_status_open(true);

    if (sehandle == NULL) {
    ALOGE("SELinux: Failed to acquire sehandle. Aborting.\n");
    abort();
    }

    if (getcon(&service_manager_context) != 0) {
    ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n");
    abort();
    }


    /* binder_loop已封装如下步骤:
    while (1)
    {
    /* read data */
    /* parse data, and process */
    /* reply */
    }
    */
    binder_loop(bs, svcmgr_handler);

    return 0;
    }

    从main函数中可以看出,它主要做了三件事情:

    1. 打开/dev/binder设备,并在内存中映射128K的空间。
    2. 通知Binder设备,把自己变成context_manager,其他用户进程都通过0号句柄访问ServiceManager。
    3. 进入循环,不停的去读Binder设备,看是否有对service的请求,如果有的话,就去调用svcmgr_handler函数回调处理请求。

    我们再来看看svcmgr_handler函数的实现:

    int svcmgr_handler(struct binder_state *bs,
    struct binder_transaction_data_secctx *txn_secctx,
    struct binder_io *msg,
    struct binder_io *reply)
    {
    struct svcinfo *si;
    uint16_t *s;
    size_t len;
    uint32_t handle;
    uint32_t strict_policy;
    int allow_isolated;
    uint32_t dumpsys_priority;

    struct binder_transaction_data *txn = &txn_secctx->transaction_data;

    if (txn->target.ptr != BINDER_SERVICE_MANAGER)
    return -1;

    if (txn->code == PING_TRANSACTION)
    return 0;

    ...

    switch(txn->code) {
    case SVC_MGR_GET_SERVICE:
    case SVC_MGR_CHECK_SERVICE:
    s = bio_get_string16(msg, &len);
    if (s == NULL) {
    return -1;
    }
    handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid,
    (const char*) txn_secctx->secctx);
    if (!handle)
    break;
    bio_put_ref(reply, handle);
    return 0;

    case SVC_MGR_ADD_SERVICE:
    s = bio_get_string16(msg, &len);
    if (s == NULL) {
    return -1;
    }
    handle = bio_get_ref(msg);
    allow_isolated = bio_get_uint32(msg) ? 1 : 0;
    dumpsys_priority = bio_get_uint32(msg);
    if (do_add_service(bs, s, len, handle, txn->sender_euid, allow_isolated, dumpsys_priority,
    txn->sender_pid, (const char*) txn_secctx->secctx))
    return -1;
    break;

    case SVC_MGR_LIST_SERVICES: {
    uint32_t n = bio_get_uint32(msg);
    uint32_t req_dumpsys_priority = bio_get_uint32(msg);

    if (!svc_can_list(txn->sender_pid, (const char*) txn_secctx->secctx, txn->sender_euid)) {
    ALOGE("list_service() uid=%d - PERMISSION DENIED\n",
    txn->sender_euid);
    return -1;
    }
    si = svclist;
    // walk through the list of services n times skipping services that
    // do not support the requested priority
    while (si) {
    if (si->dumpsys_priority & req_dumpsys_priority) {
    if (n == 0) break;
    n--;
    }
    si = si->next;
    }
    if (si) {
    bio_put_string16(reply, si->name);
    return 0;
    }
    return -1;
    }
    default:
    ALOGE("unknown code %d\n", txn->code);
    return -1;
    }

    bio_put_uint32(reply, 0);
    return 0;
    }

    我们先来认识一下binder的数据传输载体binder_transaction_data:

    struct binder_transaction_data {
    union {
    /* 当binder_transaction_data是由用户空间的进程发送给Binder驱动时,
    handle是该事务的发送目标在Binder驱动中的信息,即该事务会交给handle来处理;
    handle的值是目标在Binder驱动中的Binder引用。*/
    __u32 handle;

    /* 当binder_transaction_data是有Binder驱动反馈给用户空间进程时,
    ptr是该事务的发送目标在用户空间中的信息,即该事务会交给ptr对应的服务来处理;
    ptr是处理该事务的服务的服务在用户空间的本地Binder对象。*/
    binder_uintptr_t ptr;

    } target; // 该事务的目标对象(即,该事务数据包是给该target来处理的)

    // 只有当事务是由Binder驱动传递给用户空间时,cookie才有意思,它的值是处理该事务的ServerC++层的本地Binder对象
    binder_uintptr_t cookie;
    // 事务编码。如果是请求,则以BC_开头;如果是回复,则以BR_开头。
    __u32 code;

    /* General information about the transaction. */
    __u32 flags;

    //表示事务发起者的pid和uid。
    pid_t sender_pid;
    uid_t sender_euid;

    // 数据大小
    binder_size_t data_size;

    //数据偏移量
    binder_size_t offsets_size;

    //data是一个共用体,当通讯数据很小的时,可以直接使用buf[8]来保存数据。当够大时,只能用指针buffer来描述一个申请的数据缓冲区。
    union {
    struct {
    /* transaction data */
    binder_uintptr_t buffer;

    binder_uintptr_t offsets;
    } ptr;
    __u8 buf[8];
    } data;
    };

    可以看到,svcmgr_handler函数中对binder data的事务编码进行了判断,并分别对SVC_MGR_GET_SERVICE(SVC_MGR_CHECK_SERVICE)SVC_MGR_ADD_SERVICESVC_MGR_LIST_SERVICES三种类型的事务编码做了业务处理。

    获取服务

      case SVC_MGR_CHECK_SERVICE:  
            s = bio_get_string16(msg, &len);  
            ptr = do_find_service(bs, s, len);  
            if (!ptr)  
                break;  
            bio_put_ref(reply, ptr);  
            return 0;

    do_find_service函数中主要执行service的查找,并把找到的服务句柄写入reply,返回给客户端。

    uint32_t do_find_service(const uint16_t *s, size_t len, uid_t uid, pid_t spid, const char* sid)
    {
    struct svcinfo *si = find_svc(s, len);

    ...

    return si->handle;
    }

    我们继续看find_svc函数:

    struct svcinfo *find_svc(const uint16_t *s16, size_t len)
    {
    struct svcinfo *si;

    for (si = svclist; si; si = si->next) {
    if ((len == si->len) &&
    !memcmp(s16, si->name, len * sizeof(uint16_t))) {
    return si;
    }
    }
    return NULL;
    }

    svclist 是一个单向链表,储存了所有向servicemanager注册的服务信息。find_svc遍历svclist链表,通过服务名称作为索引条件,最终找到符合条件的服务。

    注册服务

        case SVC_MGR_ADD_SERVICE:
    s = bio_get_string16(msg, &len);
    if (s == NULL) {
    return -1;
    }
    handle = bio_get_ref(msg);
    allow_isolated = bio_get_uint32(msg) ? 1 : 0;
    dumpsys_priority = bio_get_uint32(msg);
    if (do_add_service(bs, s, len, handle, txn->sender_euid, allow_isolated, dumpsys_priority,
    txn->sender_pid, (const char*) txn_secctx->secctx))
    return -1;

    我们继续看do_add_service函数中做了哪些事情。

    在该函数中,首先会去检查客户端是否有权限注册service,如果没有权限就直接返回,不能注册。

     if (!svc_can_register(s, len, spid, sid, uid)) {
    ALOGE("add_service('%s',%x) uid=%d - PERMISSION DENIED\n",
    str8(s, len), handle, uid);
    return -1;
    }

    然后会去检查该service是否已经注册过了,如果已经注册过,那么就不能再注册了。

     si = find_svc(s, len);
    if (si) {
    if (si->handle) {
    ALOGE("add_service('%s',%x) uid=%d - ALREADY REGISTERED, OVERRIDE\n",
    str8(s, len), handle, uid);
    svcinfo_death(bs, si);
    }
    si->handle = handle;
    }

    再判断内存是否足够。

     si = malloc(sizeof(*si) + (len + 1) * sizeof(uint16_t));
    if (!si) {
    ALOGE("add_service('%s',%x) uid=%d - OUT OF MEMORY\n",
    str8(s, len), handle, uid);
    return -1;
    }

    如果都没什么问题,会注册该service,并加入到svcList链表中。

    综上所述,servicemanager主要负责查询和注册其他的系统服务,是系统服务的管理者。

    文章的最后,留给大家一个问题进行思考:

    为什么Android需要设计servicemanager做中转来添加和获取系统服务,而不直接让客户端去获取服务端句柄?
    收起阅读 »

    Android 10 启动分析之Zygote篇 (三)

    上一篇文章:# Android 10 启动分析之servicemanager篇 (二)app_main在init篇中有提到,init进程会在在Trigger 为late-init的Action中,启动Zygote服务,这篇文章我们就来具体分析一下Zygote服...
    继续阅读 »

    上一篇文章:

    # Android 10 启动分析之servicemanager篇 (二)

    app_main

    在init篇中有提到,init进程会在在Trigger 为late-init的Action中,启动Zygote服务,这篇文章我们就来具体分析一下Zygote服务,去挖掘一下Zygote负责的工作。

    Zygote服务的启动入口源码位于 /frameworks/base/cmds/app_process/app_main.cpp,我们将从这个文件的main方法开始解析。

    int main(int argc, char* const argv[])
    {

    //声明AppRuntime类的实例runtime,在AppRuntime类的构造方法中初始化的skia图形引擎
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));

    ...

    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;

    ++i; // Skip unused "parent dir" argument.
    while (i < argc) {
    const char* arg = argv[i++];
    if (strcmp(arg, "--zygote") == 0) {
    zygote = true;
    //对于64位系统nice_name为zygote64; 32位系统为zygote
    niceName = ZYGOTE_NICE_NAME;
    } else if (strcmp(arg, "--start-system-server") == 0) {
    //是否需要启动system server
    startSystemServer = true;
    } else if (strcmp(arg, "--application") == 0) {
    //启动进入独立的程序模式
    application = true;
    } else if (strncmp(arg, "--nice-name=", 12) == 0) {
    //niceName 为当前进程别名,区别abi型号
    niceName.setTo(arg + 12);
    } else if (strncmp(arg, "--", 2) != 0) {
    className.setTo(arg);
    break;
    } else {
    --i;
    break;
    }
    }

    ...

    }

    可以看到,app_main根据启动时传入参数的区别,分为zygote 模式和application模式。

    我们可以从init.zygote64_32.rc文件中看到zygote的启动参数为:

    -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote

    我们接着往下看:

    Vector<String8> args;
    if (!className.isEmpty()) {
    // We're not in zygote mode, the only argument we need to pass
    // to RuntimeInit is the application argument.
    //
    // The Remainder of args get passed to startup class main(). Make
    // copies of them before we overwrite them with the process name.
    args.add(application ? String8("application") : String8("tool"));
    runtime.setClassNameAndArgs(className, argc - i, argv + i);

    if (!LOG_NDEBUG) {
    String8 restOfArgs;
    char* const* argv_new = argv + i;
    int argc_new = argc - i;
    for (int k = 0; k < argc_new; ++k) {
    restOfArgs.append(""");
    restOfArgs.append(argv_new[k]);
    restOfArgs.append("" ");
    }
    ALOGV("Class name = %s, args = %s", className.string(), restOfArgs.string());
    }
    } else {
    // We're in zygote mode.
    //初始化Dalvik虚拟机Cache目录和权限
    maybeCreateDalvikCache();

    if (startSystemServer) {
    //附加上start-system-serve 的arg
    args.add(String8("start-system-serve 的argr"));
    }

    char prop[PROP_VALUE_MAX];
    if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
    LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
    ABI_LIST_PROPERTY);
    return 11;
    }

    String8 abiFlag("--abi-list=");
    abiFlag.append(prop);
    args.add(abiFlag);

    // In zygote mode, pass all remaining arguments to the zygote
    // main() method.
    for (; i < argc; ++i) {
    args.add(String8(argv[i]));
    }
    }

    if (!niceName.isEmpty()) {
    runtime.setArgv0(niceName.string(), true /* setProcName */);
    }

    if (zygote) {
    //进入此分支
    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
    runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
    fprintf(stderr, "Error: no class name or --zygote supplied.\n");
    app_usage();
    LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
    }

    结合传入的启动参数来看,代码将从if语句的else分支继续往下执行,进入zygote模式。至于application模式我们暂时先忽略它,等我们分析app的启动过程时再来说明。

    上述代码最后将通过 runtime.start("com.android.internal.os.ZygoteInit", args, zygote);语句,将控制权限转交给AppRuntime类去继续执行。

    继续从AppRuntime的start函数看起:

    void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
    {

    ...

    // 虚拟机创建及启动,主要是关于虚拟机参数的设置
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) {
    return;
    }
    onVmCreated(env);

    //注册JNI方法
    if (startReg(env) < 0) {
    ALOGE("Unable to register all android natives\n");
    return;
    }

    /*
    * We want to call main() with a String array with arguments in it.
    * At present we have two arguments, the class name and an option string.
    * Create an array to hold them.
    */
    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;

    //等价于strArray[0] = "com.android.internal.os.ZygoteInit"
    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);
    assert(classNameStr != NULL);
    env->SetObjectArrayElement(strArray, 0, classNameStr);

    //strArray[1] = "start-system-server";
    //strArray[2] = "--abi-list=xxx";
    //其中xxx为系统响应的cpu架构类型,比如arm64-v8a.
    for (size_t i = 0; i < options.size(); ++i) {
    jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
    assert(optionsStr != NULL);
    env->SetObjectArrayElement(strArray, i + 1, optionsStr);
    }

    /*
    * Start VM. This thread becomes the main thread of the VM, and will
    * not return until the VM exits.
    */
    //将"com.android.internal.os.ZygoteInit"转换为"com/android/internal/os/ZygoteInit
    char* slashClassName = toSlashClassName(className != NULL ? className : "");
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
    ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
    /* keep going */
    } else {
    //找到这个类后就继续找成员函数main方法的Mehtod ID
    jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
    "([Ljava/lang/String;)V");
    if (startMeth == NULL) {
    ALOGE("JavaVM unable to find main() in '%s'\n", className);
    /* keep going */
    } else {
    // 通过Jni调用ZygoteInit.main()方法
    env->CallStaticVoidMethod(startClass, startMeth, strArray);

    #if 0
    if (env->ExceptionCheck())
    threadExitUncaughtException(env);
    #endif
    }
    }
    free(slashClassName);

    ALOGD("Shutting down VM\n");
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
    ALOGW("Warning: unable to detach main thread\n");
    if (mJavaVM->DestroyJavaVM() != 0)
    ALOGW("Warning: VM did not shut down cleanly\n");
    }

    start()函数主要做了三件事情,一调用startVm开启虚拟机,二调用startReg注册JNI方法,三就是使用JNI把Zygote进程启动起来。

    ZygoteInit

    通过上述分析,代码进入了ZygoteInit.java中的main方法继续执行。从这里开始,就真正的启动了Zygote进程。我们从/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java这个文件继续往下看。

    public static void main(String argv[]) {
    //ZygoteServer 是Zygote进程的Socket通讯服务端的管理类
    ZygoteServer zygoteServer = null;

    // 标记zygote启动开始,调用ZygoteHooks的Jni方法,确保当前没有其它线程在运行
    ZygoteHooks.startZygoteNoThreadCreation();

    //设置pid为0,Zygote进入自己的进程组
    try {
    Os.setpgid(0, 0);
    } catch (ErrnoException ex) {
    throw new RuntimeException("Failed to setpgid(0,0)", ex);
    }

    Runnable caller;
    try {

    ...

    //开启DDMS(Dalvik Debug Monitor Service)功能
    RuntimeInit.enableDdms();

    //解析app_main.cpp - start()传入的参数
    boolean startSystemServer = false;
    String zygoteSocketName = "zygote";
    String abiList = null;
    boolean enableLazyPreload = false;
    for (int i = 1; i < argv.length; i++) {
    if ("start-system-server".equals(argv[i])) {
    //启动zygote时,传入了参数:start-system-server,会进入此分支
    startSystemServer = true;
    } else if ("--enable-lazy-preload".equals(argv[i])) {
    //启动zygote_secondary时,才会传入参数:enable-lazy-preload
    enableLazyPreload = true;
    } else if (argv[i].startsWith(ABI_LIST_ARG)) {
    abiList = argv[i].substring(ABI_LIST_ARG.length());
    } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
    //SOCKET_NAME_ARG 为 zygote 或zygote_secondary,具体请参考 init.zyoget64_32.rc文件
    zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length());
    } else {
    throw new RuntimeException("Unknown command line argument: " + argv[i]);
    }
    }

    // 根据传入socket name来决定是创建socket还是zygote_secondary
    final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME);

    if (abiList == null) {
    throw new RuntimeException("No ABI list supplied.");
    }

    // In some configurations, we avoid preloading resources and classes eagerly.
    // In such cases, we will preload things prior to our first fork.
    // 在第一次zygote启动时,enableLazyPreload为false,执行preload
    if (!enableLazyPreload) {
    bootTimingsTraceLog.traceBegin("ZygotePreload");
    EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
    SystemClock.uptimeMillis());
    // 加载进程的资源和类
    preload(bootTimingsTraceLog);
    EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
    SystemClock.uptimeMillis());
    bootTimingsTraceLog.traceEnd(); // ZygotePreload
    } else {
    Zygote.resetNicePriority();
    }

    // Do an initial gc to clean up after startup
    bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
    gcAndFinalize();
    bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC

    bootTimingsTraceLog.traceEnd(); // ZygoteInit
    // Disable tracing so that forked processes do not inherit stale tracing tags from
    // Zygote.
    Trace.setTracingEnabled(false, 0);


    Zygote.initNativeState(isPrimaryZygote);

    ZygoteHooks.stopZygoteNoThreadCreation();

    // 调用ZygoteServer 构造函数,创建socket Server端,会根据传入的参数,
    // 创建两个socket:/dev/socket/zygote 和 /dev/socket/zygote_secondary
    zygoteServer = new ZygoteServer(isPrimaryZygote);

    if (startSystemServer) {
    //fork出system server进程
    Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);

    // {@code r == null} in the parent (zygote) process, and {@code r != null} in the
    // child (system_server) process.
    if (r != null) {
    // 启动SystemServer
    r.run();
    return;
    }
    }

    Log.i(TAG, "Accepting command socket connections");

    // ZygoteServer进入无限循环,处理请求
    caller = zygoteServer.runSelectLoop(abiList);
    } catch (Throwable ex) {
    Log.e(TAG, "System zygote died with exception", ex);
    throw ex;
    } finally {
    if (zygoteServer != null) {4
    zygoteServer.closeServerSocket();
    }
    }

    // We're in the child process and have exited the select loop. Proceed to execute the
    // command.
    if (caller != null) {
    caller.run();
    }
    }

    main方法中主要做了以下几件事:

    1. 加载进程的资源和类。
    2. 根据传入socket name来创建socket server。
    3. fork SystemServer 进程。

    preload

    既然preload方法是负责加载进程的资源和类,那么它究竟加载了哪些资源和哪些类呢,这些资源又位于什么位置呢?

    我们先来看看preload方法里具体做了什么:

    static void preload(TimingsTraceLog bootTimingsTraceLog) {

    beginPreload();
    //预加载类
    preloadClasses();

    cacheNonBootClasspathClassLoaders();
    //加载图片、颜色等资源文件
    preloadResources();
    //加载HAL相关内容
    nativePreloadAppProcessHALs();
    //加载图形驱动
    maybePreloadGraphicsDriver();
    // 加载 android、compiler_rt、jnigraphics等library
    preloadSharedLibraries();
    //用于初始化文字资源
    preloadTextResources();
    //用于初始化webview;
    WebViewFactory.prepareWebViewInZygote();
    endPreload();
    warmUpJcaProviders();


    sPreloadComplete = true;
    }

    preloadClasses

     private static void preloadClasses() {
    final VMRuntime runtime = VMRuntime.getRuntime();


    } catch (IOException e) {
    Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
    } finally {
    ...
    }
    }

    可以看到,preloadClasses方法读取/system/etc/preloaded-classes文件的内容,并通过Class.forName初始化类。那么在/system/etc/preloaded-classes文件具体有哪些类呢?

    由于内容过多,我这里只截取部分截图让大家看看具体装载是什么类。

    image.png

    image.png

    image.png

    从装载列表中,我们可以看到很多熟悉的类,实际上,装载的类都是我们应用程序运行时可能用到的java类。

    preloadResources

    private static void preloadResources() {
    final VMRuntime runtime = VMRuntime.getRuntime();


    mResources.finishPreloading();
    } catch (RuntimeException e) {
    Log.w(TAG, "Failure preloading resources", e);
    }
    }

    从上述代码可以看到,preloadResources加载了特定的图片资源和颜色资源。这些资源的路径又具体在哪里呢?

    com.android.internal.R.array.preloaded_drawables的路径位于/frameworks/base/core/res/res/values/arrays.xml中,其他的资源路径也可以类似找到。各位读者可以自行去该路径下去看看所包含的资源文件到底是什么样的。

    preloadSharedLibraries

    private static void preloadSharedLibraries() {
    Log.i(TAG, "Preloading shared libraries...");
    System.loadLibrary("android");
    System.loadLibrary("compiler_rt");
    System.loadLibrary("jnigraphics");
    }

    preloadSharedLibraries里的内容很简单,主要是加载位于/system/lib目录下的libandroid.so、libcompiler_rt.so、libjnigraphics.so三个so库。


    我们不妨想一下,为什么android要在Zygote中将资源先进行预加载,这么做有什么好处?

    这个问题留给各位读者去自行思考,在这里便不再回答了。

    forkSystemServer

     private static Runnable forkSystemServer(String abiList, String socketName,
    ZygoteServer zygoteServer) {
    ...


    return null;
    }

    forkSystemServer方法只是fork了一个Zygote的子进程,而handleSystemServerProcess方法构造了一个Runnable对象,创建一个子线程用于启动SystemServer的逻辑。

    private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) {
    Os.umask(S_IRWXG | S_IRWXO);

    if (parsedArgs.mNiceName != null) {
    //nicename 为 system_server
    Process.setArgV0(parsedArgs.mNiceName);
    }

    ...

    if (parsedArgs.mInvokeWith != null) {
    String[] args = parsedArgs.mRemainingArgs;
    // If we have a non-null system server class path, we'll have to duplicate the
    // existing arguments and append the classpath to it. ART will handle the classpath
    // correctly when we exec a new process.
    if (systemServerClasspath != null) {
    String[] amendedArgs = new String[args.length + 2];
    amendedArgs[0] = "-cp";
    amendedArgs[1] = systemServerClasspath;
    System.arraycopy(args, 0, amendedArgs, 2, args.length);
    args = amendedArgs;
    }

    WrapperInit.execApplication(parsedArgs.mInvokeWith,
    parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
    VMRuntime.getCurrentInstructionSet(), null, args);

    throw new IllegalStateException("Unexpected return from WrapperInit.execApplication");
    } else {
    //parsedArgs.mInvokeWith 为null,会进入此分支
    createSystemServerClassLoader();
    ClassLoader cl = sCachedSystemServerClassLoader;
    if (cl != null) {
    Thread.currentThread().setContextClassLoader(cl);
    }

    /*
    * Pass the remaining arguments to SystemServer.
    */
    return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
    parsedArgs.mRemainingArgs, cl);
    }

    /* should never reach here */
    }

    继续从ZygoteInit.zygoteInit看起:

    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv,
    ClassLoader classLoader) {
    ...

    RuntimeInit.commonInit();
    //注册两个jni函数
    //android_internal_os_ZygoteInit_nativePreloadAppProcessHALs
    //android_internal_os_ZygoteInit_nativePreloadGraphicsDriver
    ZygoteInit.nativeZygoteInit();
    return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
    }

    RuntimeInit.applicationInit

    protected static Runnable applicationInit(int targetSdkVersion, String[] argv,
    ClassLoader classLoader) {
    //true代表应用程序退出时不调用AppRuntime.onExit(),否则会在退出前调用
    nativeSetExitWithoutCleanup(true);

    //设置虚拟机的内存利用率参数值为0.75
    VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
    VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);

    final Arguments args = new Arguments(argv);

    // Remaining arguments are passed to the start class's static main
    return findStaticMain(args.startClass, args.startArgs, classLoader);
    }

    继续看findStaticMain:

     protected static Runnable findStaticMain(String className, String[] argv,
    ClassLoader classLoader) {
    Class<?> cl;

    }

    这里通过反射获得了 com.android.server.SystemServer 类中的main方法,并传递给MethodAndArgsCaller用于构造一个Runnable。只要执行此Runnable,就会开始调用com.android.server.SystemServer 类中的main方法。

    到此,Zygote的逻辑已经全部执行完毕,android启动进入了SystemServer的阶段。

    最后,我们再用一个流程图来总结一下Zygote的业务逻辑:

    app_mainAppRuntimeZygoteInit进入Zygote模式创建及启动Dalvik注册Jni方法预加载进程的资源和类Zygote创建socket Server端fork SystemServer子进程载入SystemServer逻辑进入无限循环,处理请求app_mainAppRuntimeZygoteInit

    收起阅读 »

    iOS 图层时间 一

    图层时间时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMedia...
    继续阅读 »

    图层时间

    时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克

    在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMediaTiming,看看Core Animation是如何跟踪时间的。

    9.1 CAMediaTiming协议

    CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayerCAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。

    持续和重复

    我们在第八章“显式动画”中简单提到过durationCAMediaTiming的属性之一),duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。

    这里的一次迭代是什么意思呢?CAMediaTiming另外还有一个属性叫做repeatCount,代表动画重复的迭代次数。如果duration是2,repeatCount设为3.5(三个半迭代),那么完整的动画时长将是7秒。

    durationrepeatCount默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了“默认”,也就是0.25秒和1次,你可以用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展示了程序的结果。

    清单9.1 测试durationrepeatCount

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, weak) IBOutlet UITextField *durationField;
    @property (nonatomic, weak) IBOutlet UITextField *repeatField;
    @property (nonatomic, weak) IBOutlet UIButton *startButton;
    @property (nonatomic, strong) CALayer *shipLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
    self.shipLayer.position = CGPointMake(150, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    }

    - (void)setControlsEnabled:(BOOL)enabled
    {
    for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {
    control.enabled = enabled;
    control.alpha = enabled? 1.0f: 0.25f;
    }
    }

    - (IBAction)hideKeyboard
    {
    [self.durationField resignFirstResponder];
    [self.repeatField resignFirstResponder];
    }

    - (IBAction)start
    {
    CFTimeInterval duration = [self.durationField.text doubleValue];
    float repeatCount = [self.repeatField.text floatValue];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = duration;
    animation.repeatCount = repeatCount;
    animation.byValue = @(M_PI * 2);
    animation.delegate = self;
    [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
    //disable controls
    [self setControlsEnabled:NO];
    }

    - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
    {
    //reenable controls
    [self setControlsEnabled:YES];
    }

    @end

    图9.1

    图9.1 演示durationrepeatCount的测试程序

    创建重复动画的另一种方式是使用repeatDuration属性,它让动画重复一个指定的时间,而不是指定次数。你甚至设置一个叫做autoreverses的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它(图9.2)。

    图9.2

    图9.2 摆动门的动画

    对门进行摆动的代码见清单9.2。我们用了autoreverses来使门在打开后自动关闭,在这里我们把repeatDuration设置为INFINITY,于是动画无限循环播放,设置repeatCountINFINITY也有同样的效果。注意repeatCountrepeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。

    清单9.2 使用autoreverses属性实现门的摇摆

    @interface ViewController ()

    @property (nonatomic, weak) UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the door
    CALayer *doorLayer = [CALayer layer];
    doorLayer.frame = CGRectMake(0, 0, 128, 256);
    doorLayer.position = CGPointMake(150 - 64, 150);
    doorLayer.anchorPoint = CGPointMake(0, 0.5);
    doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
    [self.containerView.layer addSublayer:doorLayer];
    //apply perspective transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //apply swinging animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y";
    animation.toValue = @(-M_PI_2);
    animation.duration = 2.0;
    animation.repeatDuration = INFINITY;
    animation.autoreverses = YES;
    [doorLayer addAnimation:animation forKey:nil];
    }

    @end

    相对时间

    每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。

    beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。

    speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。

    timeOffsetbeginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。

    beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。

    可以用清单9.3的测试程序验证一下,设置speedtimeOffset滑块到随意的值,然后点击播放来观察效果(见图9.3)

    清单9.3 测试timeOffsetspeed属性

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, weak) IBOutlet UILabel *speedLabel;
    @property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel;
    @property (nonatomic, weak) IBOutlet UISlider *speedSlider;
    @property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
    @property (nonatomic, strong) UIBezierPath *bezierPath;
    @property (nonatomic, strong) CALayer *shipLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    self.bezierPath = [[UIBezierPath alloc] init];
    [self.bezierPath moveToPoint:CGPointMake(0, 150)];
    [self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = self.bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
    self.shipLayer.position = CGPointMake(0, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    //set initial values
    [self updateSliders];
    }

    - (IBAction)updateSliders
    {
    CFTimeInterval timeOffset = self.timeOffsetSlider.value;
    self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
    float speed = self.speedSlider.value;
    self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
    }

    - (IBAction)play
    {
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.timeOffset = self.timeOffsetSlider.value;
    animation.speed = self.speedSlider.value;
    animation.duration = 1.0;
    animation.path = self.bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto;
    animation.removedOnCompletion = NO;
    [self.shipLayer addAnimation:animation forKey:@"slide"];
    }

    @end

    图9.3

    图9.3 测试时间偏移和速度的简单的应用程序

    fillMode

    对于beginTime非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion被设置为NO的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢?

    一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章“隐式动画”,模型图层和呈现图层的解释)。

    另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充,因为动画开始和结束的值用来填充开始之前和结束之后的时间。

    这种行为就交给开发者了,它可以被CAMediaTimingfillMode来控制。fillMode是一个NSString类型,可以接受如下四种常量:

    kCAFillModeForwards 
    kCAFillModeBackwards
    kCAFillModeBoth
    kCAFillModeRemoved

    默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。

    这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。

    收起阅读 »

    iOS 显示动画 二

    8.3 过渡有时候对于iOS应用程序来说,希望能通过属性动画来对比较难做动画的布局进行一些改变。比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移...
    继续阅读 »

    8.3 过渡

    有时候对于iOS应用程序来说,希望能通过属性动画来对比较难做动画的布局进行一些改变。比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移除图层,属性动画将不起作用。

    于是就有了过渡的概念。过渡并不像属性动画那样平滑地在两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。

    为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个typesubtype来标识变换效果。type属性是一个NSString类型,可以被设置成如下类型:

    kCATransitionFade 
    kCATransitionMoveIn
    kCATransitionPush
    kCATransitionReveal

    到目前为止你只能使用上述四种类型,但你可以通过一些别的方法来自定义过渡效果,后续会详细介绍。

    默认的过渡类型是kCATransitionFade,当你在改变图层属性之后,就创建了一个平滑的淡入淡出效果。

    我们在第七章的例子中就已经用到过kCATransitionPush,它创建了一个新的图层,从边缘的一侧滑动进来,把旧图层从另一侧推出去的效果。

    kCATransitionMoveInkCATransitionRevealkCATransitionPush类似,都实现了一个定向滑动的动画,但是有一些细微的不同,kCATransitionMoveIn从顶部滑动进入,但不像推送动画那样把老土层推走,然而kCATransitionReveal把原始的图层滑动出去来显示新的外观,而不是把新的图层滑动进入。

    后面三种过渡类型都有一个默认的动画方向,它们都从左侧滑入,但是你可以通过subtype来控制它们的方向,提供了如下四种类型:

    kCATransitionFromRight 
    kCATransitionFromLeft
    kCATransitionFromTop
    kCATransitionFromBottom

    一个简单的用CATransition来对非动画属性做动画的例子如清单8.11所示,这里我们对UIImageimage属性做修改,但是隐式动画或者CAPropertyAnimation都不能对它做动画,因为Core Animation不知道如何在插图图片。通过对图层应用一个淡入淡出的过渡,我们可以忽略它的内容来做平滑动画(图8.4),我们来尝试修改过渡的type常量来观察其它效果。

    清单8.11 使用CATransition来对UIImageView做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIImageView *imageView;
    @property (nonatomic, copy) NSArray *images;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //set up images
    self.images = @[[UIImage imageNamed:@"Anchor.png"],
    [UIImage imageNamed:@"Cone.png"],
    [UIImage imageNamed:@"Igloo.png"],
    [UIImage imageNamed:@"Spaceship.png"]];
    }


    - (IBAction)switchImage
    {
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    //apply transition to imageview backing layer
    [self.imageView.layer addAnimation:transition forKey:nil];
    //cycle to next image
    UIImage *currentImage = self.imageView.image;
    NSUInteger index = [self.images indexOfObject:currentImage];
    index = (index + 1) % [self.images count];
    self.imageView.image = self.images[index];
    }

    @end

    你可以从代码中看出,过渡动画和之前的属性动画或者动画组添加到图层上的方式一致,都是通过-addAnimation:forKey:方法。但是和属性动画不同的是,对指定的图层一次只能使用一次CATransition,因此,无论你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”,也就是常量kCATransition

    图8.4

    图8.4 使用CATransition对图像平滑淡入淡出

    隐式过渡

    CATransision可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。苹果当然意识到了这点,并且当设置了CALayercontent属性的时候,CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。

    我们在第七章使用CATransition作为一个图层行为来改变图层的背景色,当然backgroundColor属性可以通过正常的CAPropertyAnimation来实现,但这不是说不可以用CATransition来实行。

    对图层树的动画

    CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。

    这些例子和我们之前所讨论的情况完全不同,因为它们不仅涉及到图层的属性,而且是整个图层树的改变--我们在这种动画的过程中手动在层级关系中添加或者移除图层。

    这里用到了一个小诡计,要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer

    在清单8.2中,我们展示了如何在UITabBarController切换标签的时候添加淡入淡出的动画。这里我们建立了默认的标签应用程序模板,然后用UITabBarControllerDelegate-tabBarController:didSelectViewController:方法来应用过渡动画。我们把动画添加到UITabBarController的视图图层上,于是在标签被替换的时候动画不会被移除。

    清单8.12 对UITabBarController做动画

    #import "AppDelegate.h"
    #import "FirstViewController.h"
    #import "SecondViewController.h"
    #import
    @implementation AppDelegate
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
    UIViewController *viewController1 = [[FirstViewController alloc] init];
    UIViewController *viewController2 = [[SecondViewController alloc] init];
    self.tabBarController = [[UITabBarController alloc] init];
    self.tabBarController.viewControllers = @[viewController1, viewController2];
    self.tabBarController.delegate = self;
    self.window.rootViewController = self.tabBarController;
    [self.window makeKeyAndVisible];
    return YES;
    }
    - (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
    {
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    //apply transition to tab bar controller's view
    [self.tabBarController.view.layer addAnimation:transition forKey:nil];
    }
    @end

    自定义动画

    我们证实了过渡是一种对那些不太好做平滑动画属性的强大工具,但是CATransition的提供的动画类型太少了。

    更奇怪的是苹果通过UIView +transitionFromView:toView:duration:options:completion:+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransitiontype属性提供的常量完全不同UIView过渡方法中options参数可以由如下常量指定:

    UIViewAnimationOptionTransitionFlipFromLeft 

    UIViewAnimationOptionTransitionFlipFromRight UIViewAnimationOptionTransitionCurlUp UIViewAnimationOptionTransitionCurlDown UIViewAnimationOptionTransitionCrossDissolve UIViewAnimationOptionTransitionFlipFromTop UIViewAnimationOptionTransitionFlipFromBottom

    除了UIViewAnimationOptionTransitionCrossDissolve之外,剩下的值和CATransition类型完全没关系。你可以用之前例子修改过的版本来测试一下(见清单8.13)。

    清单8.13 使用UIKit提供的方法来做过渡动画

    @interface ViewController ()
    @property (nonatomic, weak) IBOutlet UIImageView *imageView;
    @property (nonatomic, copy) NSArray *images;
    @end
    @implementation ViewController
    - (void)viewDidLoad
    {
    [super viewDidLoad]; //set up images
    self.images = @[[UIImage imageNamed:@"Anchor.png"],
    [UIImage imageNamed:@"Cone.png"],
    [UIImage imageNamed:@"Igloo.png"],
    [UIImage imageNamed:@"Spaceship.png"]];
    - (IBAction)switchImage
    {
    [UIView transitionWithView:self.imageView duration:1.0
    options:UIViewAnimationOptionTransitionFlipFromLeft
    animations:^{
    //cycle to next image
    UIImage *currentImage = self.imageView.image;
    NSUInteger index = [self.images indexOfObject:currentImage];
    index = (index + 1) % [self.images count];
    self.imageView.image = self.images[index];
    }
    completion:NULL];
    }

    @end

    文档暗示过在iOS5(带来了Core Image框架)之后,可以通过CATransitionfilter属性,用CIFilter来创建其它的过渡效果。然是直到iOS6都做不到这点。试图对CATransition使用Core Image的滤镜完全没效果(但是在Mac OS中是可行的,也许文档是想表达这个意思)。

    因此,根据要实现的效果,你只用关心是用CATransition还是用UIView的过渡方法就可以了。希望下个版本的iOS系统可以通过CATransition很好的支持Core Image的过渡滤镜效果(或许甚至会有新的方法)。

    但这并不意味着在iOS上就不能实现自定义的过渡效果了。这只是意味着你需要做一些额外的工作。就像之前提到的那样,过渡动画做基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。

    事实证明,对图层做截图还是很简单的。CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。

    清单8.14演示了一个基本的实现。我们对当前视图状态截图,然后在我们改变原始视图的背景色的时候对截图快速转动并且淡出,图8.5展示了我们自定义的过渡效果。

    为了让事情更简单,我们用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation可以达到同样的效果,但是那样的话我们就需要对图层的变换和不透明属性创建单独的动画,然后当动画结束的是哦户在CAAnimationDelegate中把coverView从屏幕中移除。

    清单8.14 用renderInContext:创建自定义过渡效果

    @implementation ViewController
    - (IBAction)performTransition
    {
    //preserve the current view snapshot
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
    //insert snapshot view in front of this one
    UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];
    //update the view (we'll simply randomize the layer background color)
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //perform animation (anything you like)
    [UIView animateWithDuration:1.0 animations:^{
    //scale, rotate and fade the view
    CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
    transform = CGAffineTransformRotate(transform, M_PI_2);
    coverView.transform = transform;
    coverView.alpha = 0.0;
    } completion:^(BOOL finished) {
    //remove the cover view now we're finished with it
    [coverView removeFromSuperview];
    }];
    }
    @end

    图8.5

    图8.5 使用renderInContext:创建自定义过渡效果

    这里有个警告:-renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。但是用CATransition,或者用私有的截屏方式就没有这个限制了。


    8.4 在动画过程中取消动画

    之前提到过,你可以用-addAnimation:forKey:方法中的key参数来在添加动画之后检索一个动画,使用如下方法:

    - (CAAnimation *)animationForKey:(NSString *)key;

    但并不支持在动画运行过程中修改动画,所以这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。

    为了终止一个指定的动画,你可以用如下方法把它从图层移除掉:

    - (void)removeAnimationForKey:(NSString *)key;

    或者移除所有动画:

    - (void)removeAllAnimations;

    动画一旦被移除,图层的外观就立刻更新到当前的模型图层的值。一般说来,动画在结束之后被自动移除,除非设置removedOnCompletionNO,如果你设置动画在结束之后不被自动移除,那么当它不需要的时候你要手动移除它;否则它会一直存在于内存中,直到图层被销毁。

    我们来扩展之前旋转飞船的示例,这里添加一个按钮来停止或者启动动画。这一次我们用一个非nil的值作为动画的键,以便之后可以移除它。-animationDidStop:finished:方法中的flag参数表明了动画是自然结束还是被打断,我们可以在控制台打印出来。如果你用停止按钮来终止动画,它会打印NO,如果允许它完成,它会打印YES

    清单8.15是更新后的示例代码,图8.6显示了结果。

    清单8.15 开始和停止一个动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) CALayer *shipLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
    self.shipLayer.position = CGPointMake(150, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    }

    - (IBAction)start
    {
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    animation.delegate = self;
    [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
    }

    - (IBAction)stop
    {
    [self.shipLayer removeAnimationForKey:@"rotateAnimation"];
    }

    - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
    {
    //log that the animation stopped
    NSLog(@"The animation stopped (finished: %@)", flag? @"YES": @"NO");
    }

    @end

    图8.6

    图8.6 通过开始和停止按钮控制的旋转动画

    总结

    这一章中,我们涉及了属性动画(你可以对单独的图层属性动画有更加具体的控制),动画组(把多个属性动画组合成一个独立单元)以及过度(影响整个图层,可以用来对图层的任何内容做任何类型的动画,包括子图层的添加和移除)。

    在第九章中,我们继续学习CAMediaTiming协议,来看一看Core Animation是怎样处理逝去的时间。

    收起阅读 »

    call, call.call, call.call.call, 你也许还不懂这疯狂的call

    Function.prototype.call 我想大家都觉得自己很熟悉了,手写也没问题!! 你确认这个问题之前, 首先看看 三千文字,也没写好 Function.prototype.call, 看完,你感觉还OK,那么再看一道题: 请问如下的输出结果 fun...
    继续阅读 »

    Function.prototype.call 我想大家都觉得自己很熟悉了,手写也没问题!!

    你确认这个问题之前, 首先看看 三千文字,也没写好 Function.prototype.call,


    看完,你感觉还OK,那么再看一道题:

    请问如下的输出结果


    function a(){ 
    console.log(this,'a')
    };
    function b(){
    console.log(this,'b')
    }
    a.call.call(b,'b')

    如果,你也清晰的知道,结果,对不起,大佬, 打扰了,我错了!


    本文起源:

    一个掘友加我微信,私聊问我这个问题,研究后,又请教了 阿宝哥

    觉得甚有意思,遂与大家分享!


    结果


    结果如下: 惊喜还是意外,还是淡定呢?


    String {"b"} "b"

    再看看如下的代码:2个,3个,4个,更多个的call,输出都会是String {"b"} "b"


    function a(){ 
    console.log(this,'a')
    };
    function b(){
    console.log(this,'b')
    }
    a.call.call(b,'b') // String {"b"} "b"
    a.call.call.call(b,'b') // String {"b"} "b"
    a.call.call.call.call(b,'b') // String {"b"} "b"

    看完上面,应该有三个疑问?



    1. 为什么被调用的是b函数

    2. 为什么thisString {"b"}

    3. 为什么 2, 3, 4个call的结果一样


    结论:

    两个以上的call,比如call.call(b, 'b'),你就简单理解为用 b.call('b')


    分析


    为什么 2, 3, 4个call的结果一样


    a.call(b) 最终被调用的是a,

    a.call.call(b), 最终被调用的 a.call

    a.call.call.call(b), 最终被执行的 a.call.call


    看一下引用关系


    a.call === Function.protype.call  // true
    a.call === a.call.call // true
    a.call === a.call.call.call // true

    基于上述执行分析:

    a.call 被调用的是a

    a.call.calla.call.call.call 本质没啥区别, 被调用的都是Function.prototype.call


    为什么 2, 3, 4个call的结果一样,到此已经真相


    为什么被调用的是b函数


    看本质就要返璞归真,ES 标准对 Funtion.prototye.call 的描述



    Function.prototype.call (thisArg , ...args)


    When the call method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:



    1. If IsCallable(func) is false, throw a TypeError exception.

    2. Let argList be an empty List.

    3. If this method was called with more than one argument then in left to right order, starting with the second argument, append each argument as the last element of argList.

    4. Perform PrepareForTailCall().

    5. Return Call(functhisArgargList).



    中文翻译一下



    1. 如果不可调用,抛出异常

    2. 准备一个argList空数组变量

    3. 把第一个之后的变量按照顺序添加到argList

    4. 返回 Call(functhisArgargList)的结果


    这里的Call只不是是一个抽象的定义, 实际上是调用函数内部 [[Call]] 的方法, 其也没有暴露更多的有用的信息。


    实际上在这里,我已经停止了思考:


    a is a function, then what a.call.call really do? 一文的解释,有提到 Bound Function Exotic Objects , MDN的 Function.prototype.bind 也有提到:



    The bind() function creates a new bound function, which is an exotic function object (a term from ECMAScript 2015) that wraps the original function object. Calling the bound function generally results in the execution of its wrapped function.



    Function.prototype.call 相反,并没有提及!!! 但不排查在调用过程中有生成。


    Difference between Function.call, Function.prototype.call, Function.prototype.call.call and Function.prototype.call.call.call 一文的解释,我觉得是比较合理的


    function my(p) { console.log(p) }
    Function.prototype.call.call(my, this, "Hello"); // output 'Hello'


    Function.prototype.call.call(my, this, "Hello"); means:


    Use my as this argument (the function context) for the function that was called. In this case Function.prototype.call was called.


    So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.


    It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string.



    重点标出:

    So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.


    It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string


    翻译一下:

    Function.prototype.call.call(my, this, "Hello")表示: 用my作为上下文调用Function.prototype.call,也就是说my是最终被调用的函数。


    my带着这些 (this, "Hello") 被调用, this 作为被调用函数的上下文,此处是作为my函数的上下文, 唯一被传递的参数是 "hello"字符串。


    基于这个理解, 我们简单验证一下, 确实是这样的表象


    // case 1:
    function my(p) { console.log(p) }
    Function.prototype.call.call(my, this, "Hello"); // output 'Hello'

    // case 2:
    function a(){
    console.log(this,'a')
    };
    function b(){
    console.log(this,'b')
    }
    a.call.call(b,'b') // String {"b"} "b"

    为什么被调用的是b函数, 到此也真相了。


    其实我依旧不能太释怀, 但是这个解释可以接受,表象也是正确的, 期望掘友们有更合理,更详细的解答。


    为什么thisString {"b"}


    在上一节的分析中,我故意遗漏了Function.prototype.call的两个note



    NOTE 1: The thisArg value is passed without modification as the this value. This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value. Even though the thisArg is passed without modification, non-strict functions still perform these transformations upon entry to the function.




    NOTE 2: If func is an arrow function or a bound function then the thisArg will be ignored by the function [[Call]] in step 5.



    注意这一句:



    This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value



    两点:



    1. 如果thisArgundefined 或者null, 会用global object替换


    这里的前提是 非严格模式


    "use strict"

    function a(m){
    console.log(this, m); // undefined, 1
    }

    a.call(undefined, 1)


    1. 其他的所有类型,都会调用 ToObject进行转换


    所以非严格模式下, this肯定是个对象, 看下面的代码:


    Object('b') // String {"b"}

    note2的 ToObject 就是答案


    到此, 为什么thisSting(b) 这个也真相了


    万能的函数调用方法


    基于Function.prototype.call.call的特性,我们可以封装一个万能函数调用方法


    var call = Function.prototype.call.call.bind(Function.prototype.call);

    示例


    var person = {
    hello() {
    console.log('hello', this.name)
    }
    }

    call(person.hello, {"name": "tom"}) // hello tom

    写在最后


    如果你觉得不错,你的一赞一评就是我前行的最大动力。




    作者:云的世界
    链接:https://juejin.cn/post/6999781802923524132

    收起阅读 »

    iOS 显式动画 一

    显式动画如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做...
    继续阅读 »

    显式动画

    如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆

    上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动。


    8.1 属性动画

    CAAnimationDelegate在任何头文件中都找不到,但是可以在CAAnimation头文件或者苹果开发者文档中找到相关函数。在这个例子中,我们用-animationDidStop:finished:方法在动画结束之后来更新图层的backgroundColor

    当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为显式的CABasicAnimation,另一次是因为隐式动画,具体实现见订单8.3。

    清单8.3 动画完成之后修改图层的背景色

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
    }

    - (IBAction)changeColor
    {
    //create a new random color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //create a basic animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.toValue = (__bridge id)color.CGColor;
    animation.delegate = self;
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
    }

    - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
    {
    //set the backgroundColor property to match animation toValue
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue;
    [CATransaction commit];
    }

    @end

    CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,就是当你有多个动画的时候,无法在在回调方法中区分。在一个视图控制器中创建动画的时候,通常会用控制器本身作为一个委托(如清单8.3所示),但是所有的动画都会调用同一个回调方法,所以你就需要判断到底是那个图层的调用。

    考虑一下第三章的闹钟,“图层几何学”,我们通过简单地每秒更新指针的角度来实现一个钟,但如果指针动态地转向新的位置会更加真实。

    我们不能通过隐式动画来实现因为这些指针都是UIView的实例,所以图层的隐式动画都被禁用了。我们可以简单地通过UIView的动画方法来实现。但如果想更好地控制动画时间,使用显式动画会更好(更多内容见第十章)。使用CABasicAnimation来做动画可能会更加复杂,因为我们需要在-animationDidStop:finished:中检测指针状态(用于设置结束的位置)。

    动画本身会作为一个参数传入委托的方法,也许你会认为可以控制器中把动画存储为一个属性,然后在回调用比较,但实际上并不起作用,因为委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。

    当使用-addAnimation:forKey:把动画添加到图层,这里有一个到目前为止我们都设置为nilkey参数。这里的键是-animationForKey:方法找到对应动画的唯一标识符,而当前动画的所有键都可以用animationKeys获取。如果我们对每个动画都关联一个唯一的键,就可以对每个图层循环所有键,然后调用-animationForKey:来比对结果。尽管这不是一个优雅的实现。

    幸运的是,还有一种更加简单的方法。像所有的NSObject子类一样,CAAnimation实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:-valueForKey:方法来存取属性。但是CAAnimation有一个不同的性能:它更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。

    这意味着你可以对动画用任意类型打标签。在这里,我们给UIView类型的指针添加的动画,所以可以简单地判断动画到底属于哪个视图,然后在委托方法中用这个信息正确地更新钟的指针(清单8.4)。

    清单8.4 使用KVC对动画打标签

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIImageView *hourHand;
    @property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
    @property (nonatomic, weak) IBOutlet UIImageView *secondHand;
    @property (nonatomic, weak) NSTimer *timer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //adjust anchor points
    self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    //start timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
    //set initial hand positions
    [self updateHandsAnimated:NO];
    }

    - (void)tick
    {
    [self updateHandsAnimated:YES];
    }

    - (void)updateHandsAnimated:(BOOL)animated
    {
    //convert time to hours, minutes and seconds
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
    NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
    CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;
    //calculate hour hand angle //calculate minute hand angle
    CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
    //calculate second hand angle
    CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;
    //rotate hands
    [self setAngle:hourAngle forHand:self.hourHand animated:animated];
    [self setAngle:minuteAngle forHand:self.minuteHand animated:animated];
    [self setAngle:secondAngle forHand:self.secondHand animated:animated];
    }

    - (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
    {
    //generate transform
    CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
    if (animated) {
    //create transform animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    [self updateHandsAnimated:NO];
    animation.keyPath = @"transform";
    animation.toValue = [NSValue valueWithCATransform3D:transform];
    animation.duration = 0.5;
    animation.delegate = self;
    [animation setValue:handView forKey:@"handView"];
    [handView.layer addAnimation:animation forKey:nil];
    } else {
    //set transform directly
    handView.layer.transform = transform;
    }
    }

    - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
    {
    //set final position for hand view
    UIView *handView = [anim valueForKey:@"handView"];
    handView.layer.transform = [anim.toValue CATransform3DValue];
    }

    我们成功的识别出每个图层停止动画的时间,然后更新它的变换到一个新值,很好。

    不幸的是,即使做了这些,还是有个问题,清单8.4在模拟器上运行的很好,但当真正跑在iOS设备上时,我们发现在-animationDidStop:finished:委托方法调用之前,指针会迅速返回到原始值,这个清单8.3图层颜色发生的情况一样。

    问题在于回调方法在动画完成之前已经被调用了,但不能保证这发生在属性动画返回初始状态之前。这同时也很好地说明了为什么要在真实的设备上测试动画代码,而不仅仅是模拟器。

    我们可以用一个fillMode属性来解决这个问题,下一章会详细说明,这里知道在动画之前设置它比在动画结束之后更新属性更加方便。

    关键帧动画

    CABasicAnimation揭示了大多数隐式动画背后依赖的机制,这的确很有趣,但是显式地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。

    CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation类似,CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。

    关键帧起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过关键帧推算出)将由熟练的艺术家来完成。CAKeyframeAnimation也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进行插入。

    我们可以用之前使用颜色图层的例子来演示,设置一个颜色的数组,然后通过关键帧动画播放出来(清单8.5)

    清单8.5 使用CAKeyframeAnimation应用一系列颜色的变化

    - (IBAction)changeColor
    {
    //create a keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.duration = 2.0;
    animation.values = @[
    (__bridge id)[UIColor blueColor].CGColor,
    (__bridge id)[UIColor redColor].CGColor,
    (__bridge id)[UIColor greenColor].CGColor,
    (__bridge id)[UIColor blueColor].CGColor ];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
    }

    注意到序列中开始和结束的颜色都是蓝色,这是因为CAKeyframeAnimation并不能自动把当前值作为第一帧(就像CABasicAnimation那样把fromValue设为nil)。动画会在开始的时候突然跳转到第一帧的值,然后在动画结束的时候突然恢复到原始的值。所以为了动画的平滑特性,我们需要开始和结束的关键帧来匹配当前属性的值。

    当然可以创建一个结束和开始值不同的动画,那样的话就需要在动画启动之前手动更新属性和最后一帧的值保持一致,就和之前讨论的一样。

    我们用duration属性把动画时间从默认的0.25秒增加到2秒,以便于动画做的不那么快。运行它,你会发现动画通过颜色不断循环,但效果看起来有些奇怪。原因在于动画以一个恒定的步调在运行。当在每个动画之间过渡的时候并没有减速,这就产生了一个略微奇怪的效果,为了让动画看起来更自然,我们需要调整一下缓冲,第十章将会详细说明。

    提供一个数组的值就可以按照颜色变化做动画,但一般来说用数组来描述动画运动并不直观。CAKeyframeAnimation有另一种方式去指定动画,就是使用CGPathpath属性可以用一种直观的方式,使用Core Graphics函数定义运动序列来绘制动画。

    我们来用一个宇宙飞船沿着一个简单曲线的实例演示一下。为了创建路径,我们需要使用一个三次贝塞尔曲线,它是一种使用开始点,结束点和另外两个控制点来定义形状的曲线,可以通过使用一个基于C的Core Graphics绘图指令来创建,不过用UIKit提供的UIBezierPath类会更简单。

    我们这次用CAShapeLayer来在屏幕上绘制曲线,尽管对动画来说并不是必须的,但这会让我们的动画更加形象。绘制完CGPath之后,我们用它来创建一个CAKeyframeAnimation,然后用它来应用到我们的宇宙飞船。代码见清单8.6,结果见图8.1。

    清单8.6 沿着一个贝塞尔曲线对图层做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 64, 64);
    shipLayer.position = CGPointMake(0, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    [shipLayer addAnimation:animation forKey:nil];
    }

    @end

    图8.1

    图8.1 沿着一个贝塞尔曲线移动的宇宙飞船图片

    运行示例,你会发现飞船的动画有些不太真实,这是因为当它运动的时候永远指向右边,而不是指向曲线切线的方向。你可以调整它的affineTransform来对运动方向做动画,但很可能和其它的动画冲突。

    幸运的是,苹果预见到了这点,并且给CAKeyFrameAnimation添加了一个rotationMode的属性。设置它为常量kCAAnimationRotateAuto(清单8.7),图层将会根据曲线的切线自动旋转(图8.2)。

    清单8.7 通过rotationMode自动对齐图层到曲线

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    ...
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto;
    [shipLayer addAnimation:animation forKey:nil];
    }

    图8.2

    图8.2 匹配曲线切线方向的飞船图层

    虚拟属性

    之前提到过属性动画实际上是针对于关键路径而不是一个键,这就意味着可以对子属性甚至是虚拟属性做动画。但是虚拟属性到底是什么呢?

    考虑一个旋转的动画:如果想要对一个物体做旋转的动画,那就需要作用于transform属性,因为CALayer没有显式提供角度或者方向之类的属性,代码如清单8.8所示

    清单8.8 用transform属性对图层做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 128, 128);
    shipLayer.position = CGPointMake(150, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform";
    animation.duration = 2.0;
    animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI, 0, 0, 1)];
    [shipLayer addAnimation:animation forKey:nil];
    }

    @end

    这么做是可行的,但看起来更因为是运气而不是设计的原因,如果我们把旋转的值从M_PI(180度)调整到2 * M_PI(360度),然后运行程序,会发现这时候飞船完全不动了。这是因为这里的矩阵做了一次360度的旋转,和做了0度是一样的,所以最后的值根本没变。

    现在继续使用M_PI,但这次用byValue而不是toValue。也许你会认为这和设置toValue结果一样,因为0 + 90度 == 90度,但实际上飞船的图片变大了,并没有做任何旋转,这是因为变换矩阵不能像角度值那样叠加。

    那么如果需要独立于角度之外单独对平移或者缩放做动画呢?由于都需要我们来修改transform属性,实时地重新计算每个时间点的每个变换效果,然后根据这些创建一个复杂的关键帧动画,这一切都是为了对图层的一个独立做一个简单的动画。

    幸运的是,有一个更好的解决方案:为了旋转图层,我们可以对transform.rotation关键路径应用动画,而不是transform本身(清单8.9)。

    清单8.9 对虚拟的transform.rotation属性做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 128, 128);
    shipLayer.position = CGPointMake(150, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    [shipLayer addAnimation:animation forKey:nil];
    }

    @end

    结果运行的特别好,用transform.rotation而不是transform做动画的好处如下:

    • 我们可以不通过关键帧一步旋转多于180度的动画。
    • 可以用相对值而不是绝对值旋转(设置byValue而不是toValue)。
    • 可以不用创建CATransform3D,而是使用一个简单的数值来指定角度。
    • 不会和transform.position或者transform.scale冲突(同样是使用关键路径来做独立的动画属性)。

    transform.rotation属性有一个奇怪的问题是它其实并不存在。这是因为CATransform3D并不是一个对象,它实际上是一个结构体,也没有符合KVC相关属性,transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性。

    你不可以直接设置transform.rotation或者transform.scale,他们不能被直接使用。当你对他们做动画时,Core Animation自动地根据通过CAValueFunction来计算的值来更新transform属性。

    CAValueFunction用于把我们赋给虚拟的transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。你可以通过设置CAPropertyAnimationvalueFunction属性来改变,于是你设置的函数将会覆盖默认的函数。

    CAValueFunction看起来似乎是对那些不能简单相加的属性(例如变换矩阵)做动画的非常有用的机制,但由于CAValueFunction的实现细节是私有的,所以目前不能通过继承它来自定义。你可以通过使用苹果目前已经提供的常量(目前都是和变换矩阵的虚拟属性相关,所以没太多使用场景了,因为这些属性都有了默认的实现方式)。


    8.2 动画组

    动画组

    CABasicAnimationCAKeyframeAnimation仅仅作用于单独的属性,而CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。我们把清单8.6那种关键帧动画和调整图层背景色的基础动画组合起来(清单8.10),结果如图8.3所示。

    清单8.10 组合关键帧动画和基础动画

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add a colored layer
    CALayer *colorLayer = [CALayer layer];
    colorLayer.frame = CGRectMake(0, 0, 64, 64);
    colorLayer.position = CGPointMake(0, 150);
    colorLayer.backgroundColor = [UIColor greenColor].CGColor;
    [self.containerView.layer addSublayer:colorLayer];
    //create the position animation
    CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
    animation1.keyPath = @"position";
    animation1.path = bezierPath.CGPath;
    animation1.rotationMode = kCAAnimationRotateAuto;
    //create the color animation
    CABasicAnimation *animation2 = [CABasicAnimation animation];
    animation2.keyPath = @"backgroundColor";
    animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
    //create group animation
    CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
    groupAnimation.animations = @[animation1, animation2];
    groupAnimation.duration = 4.0;
    //add the animation to the color layer
    [colorLayer addAnimation:groupAnimation forKey:nil];
    }

    图8.3

    图8.3 关键帧路径和基础动画的组合

    收起阅读 »

    Vue3的7种和Vue2的12种组件通信,年轻人?还不收藏在等什么!!!

    Vue2.x组件通信12种方式写在后面了,先来 Vue3 的 奥力给! Vue3 组件通信方式 props $emit expose / ref $attrs v-model provide / inject Vuex Vue3 通信使用写法 props ...
    继续阅读 »

    Vue2.x组件通信12种方式写在后面了,先来 Vue3 的


    奥力给!


    Vue3 组件通信方式



    • props

    • $emit

    • expose / ref

    • $attrs

    • v-model

    • provide / inject

    • Vuex


    Vue3 通信使用写法


    props


    用 props 传数据给子组件有两种方法,如下


    方法一,混合写法


    // Parent.vue 传送
    <child :msg1="msg1" :msg2="msg2"></child>
    <script>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    export default {
    data(){
    return {
    msg1:"这是传级子组件的信息1"
    }
    },
    setup(){
    // 创建一个响应式数据

    // 写法一 适用于基础类型 ref 还有其他用处,下面章节有介绍
    const msg2 = ref("这是传级子组件的信息2")

    // 写法二 适用于复杂类型,如数组、对象
    const msg2 = reactive(["这是传级子组件的信息2"])

    return {
    msg2
    }
    }
    }
    </script>

    // Child.vue 接收
    <script>
    export default {
    props: ["msg1", "msg2"],// 如果这行不写,下面就接收不到
    setup(props) {
    console.log(props) // { msg1:"这是传给子组件的信息1", msg2:"这是传给子组件的信息2" }
    },
    }
    </script>

    方法二,纯 Vue3 写法


    // Parent.vue 传送
    <child :msg2="msg2"></child>
    <script setup>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    const msg2 = ref("这是传给子组件的信息2")
    // 或者复杂类型
    const msg2 = reactive(["这是传级子组件的信息2"])
    </script>

    // Child.vue 接收
    <script setup>
    // 不需要引入 直接使用
    // import { defineProps } from "vue"
    const props = defineProps({
    // 写法一
    msg2: String
    // 写法二
    msg2:{
    type:String,
    default:""
    }
    })
    console.log(props) // { msg2:"这是传级子组件的信息2" }
    </script>

    注意:


    如果父组件是混合写法,子组件纯 Vue3 写法的话,是接收不到父组件里 data 的属性,只能接收到父组件里 setup 函数里传的属性


    如果父组件是纯 Vue3 写法,子组件混合写法,可以通过 props 接收到 data 和 setup 函数里的属性,但是子组件要是在 setup 里接收,同样只能接收到父组件中 setup 函数里的属性,接收不到 data 里的属性


    官方也说了,既然用了 3,就不要写 2 了,所以不推荐混合写法。下面的例子,一律只用纯 Vue3 的写法,就不写混合写法了


    $emit


    // Child.vue 派发
    <template>
    // 写法一
    <button @click="emit('myClick')">按钮</buttom>
    // 写法二
    <button @click="handleClick">按钮</buttom>
    </template>
    <script setup>

    // 方法一 适用于Vue3.2版本 不需要引入
    // import { defineEmits } from "vue"
    // 对应写法一
    const emit = defineEmits(["myClick","myClick2"])
    // 对应写法二
    const handleClick = ()=>{
    emit("myClick", "这是发送给父组件的信息")
    }

    // 方法二 不适用于 Vue3.2版本,该版本 useContext()已废弃
    import { useContext } from "vue"
    const { emit } = useContext()
    const handleClick = ()=>{
    emit("myClick", "这是发送给父组件的信息")
    }
    </script>

    // Parent.vue 响应
    <template>
    <child @myClick="onMyClick"></child>
    </template>
    <script setup>
    import child from "./child.vue"
    const onMyClick = (msg) => {
    console.log(msg) // 这是父组件收到的信息
    }
    </script>

    expose / ref


    父组件获取子组件的属性或者调用子组件方法


    // Child.vue
    <script setup>
    // 方法一 不适用于Vue3.2版本,该版本 useContext()已废弃
    import { useContext } from "vue"
    const ctx = useContext()
    // 对外暴露属性方法等都可以
    ctx.expose({
    childName: "这是子组件的属性",
    someMethod(){
    console.log("这是子组件的方法")
    }
    })

    // 方法二 适用于Vue3.2版本, 不需要引入
    // import { defineExpose } from "vue"
    defineExpose({
    childName: "这是子组件的属性",
    someMethod(){
    console.log("这是子组件的方法")
    }
    })
    </script>

    // Parent.vue 注意 ref="comp"
    <template>
    <child ref="comp"></child>
    <button @click="handlerClick">按钮</button>
    </template>
    <script setup>
    import child from "./child.vue"
    import { ref } from "vue"
    const comp = ref(null)
    const handlerClick = () => {
    console.log(comp.value.childName) // 获取子组件对外暴露的属性
    comp.value.someMethod() // 调用子组件对外暴露的方法
    }
    </script>

    attrs


    attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合


    // Parent.vue 传送
    <child :msg1="msg1" :msg2="msg2" title="3333"></child>
    <script setup>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    const msg1 = ref("1111")
    const msg2 = ref("2222")
    </script>

    // Child.vue 接收
    <script setup>
    import { defineProps, useContext, useAttrs } from "vue"
    // 3.2版本不需要引入 defineProps,直接用
    const props = defineProps({
    msg1: String
    })
    // 方法一 不适用于 Vue3.2版本,该版本 useContext()已废弃
    const ctx = useContext()
    // 如果没有用 props 接收 msg1 的话就是 { msg1: "1111", msg2:"2222", title: "3333" }
    console.log(ctx.attrs) // { msg2:"2222", title: "3333" }

    // 方法二 适用于 Vue3.2版本
    const attrs = useAttrs()
    console.log(attrs) // { msg2:"2222", title: "3333" }
    </script>

    v-model


    可以支持多个数据双向绑定


    // Parent.vue
    <child v-model:key="key" v-model:value="value"></child>
    <script setup>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    const key = ref("1111")
    const value = ref("2222")
    </script>

    // Child.vue
    <template>
    <button @click="handlerClick">按钮</button>
    </template>
    <script setup>

    // 方法一 不适用于 Vue3.2版本,该版本 useContext()已废弃
    import { useContext } from "vue"
    const { emit } = useContext()

    // 方法二 适用于 Vue3.2版本,不需要引入
    // import { defineEmits } from "vue"
    const emit = defineEmits(["key","value"])

    // 用法
    const handlerClick = () => {
    emit("update:key", "新的key")
    emit("update:value", "新的value")
    }
    </script>

    provide / inject


    provide / inject 为依赖注入


    provide:可以让我们指定想要提供给后代组件的数据或


    inject:在任何后代组件中接收想要添加在这个组件上的数据,不管组件嵌套多深都可以直接拿来用


    // Parent.vue
    <script setup>
    import { provide } from "vue"
    provide("name", "沐华")
    </script>

    // Child.vue
    <script setup>
    import { inject } from "vue"
    const name = inject("name")
    console.log(name) // 沐华
    </script>

    Vuex


    // store/index.js
    import { createStore } from "vuex"
    export default createStore({
    state:{ count: 1 },
    getters:{
    getCount: state => state.count
    },
    mutations:{
    add(state){
    state.count++
    }
    }
    })

    // main.js
    import { createApp } from "vue"
    import App from "./App.vue"
    import store from "./store"
    createApp(App).use(store).mount("#app")

    // Page.vue
    // 方法一 直接使用
    <template>
    <div>{{ $store.state.count }}</div>
    <button @click="$store.commit('add')">按钮</button>
    </template>

    // 方法二 获取
    <script setup>
    import { useStore, computed } from "vuex"
    const store = useStore()
    console.log(store.state.count) // 1

    const count = computed(()=>store.state.count) // 响应式,会随着vuex数据改变而改变
    console.log(count) // 1
    </script>

    Vue2.x 组件通信方式


    Vue2.x 组件通信共有12种



    1. props

    2. $emit / v-on

    3. .sync

    4. v-model

    5. ref

    6. $children / $parent

    7. $attrs / $listeners

    8. provide / inject

    9. EventBus

    10. Vuex

    11. $root

    12. slot


    父子组件通信可以用:



    • props

    • $emit / v-on

    • $attrs / $listeners

    • ref

    • .sync

    • v-model

    • $children / $parent


    兄弟组件通信可以用:



    • EventBus

    • Vuex

    • $parent


    跨层级组件通信可以用:



    • provide/inject

    • EventBus

    • Vuex

    • $attrs / $listeners

    • $root


    Vue2.x 通信使用写法


    下面把每一种组件通信方式的写法一一列出


    1. props


    父组件向子组件传送数据,这应该是最常用的方式了


    子组件接收到数据之后,不能直接修改父组件的数据。会报错,所以当父组件重新渲染时,数据会被覆盖。如果子组件内要修改的话推荐使用 computed


    // Parent.vue 传送
    <template>
    <child :msg="msg"></child>
    </template>

    // Child.vue 接收
    export default {
    // 写法一 用数组接收
    props:['msg'],
    // 写法二 用对象接收,可以限定接收的数据类型、设置默认值、验证等
    props:{
    msg:{
    type:String,
    default:'这是默认数据'
    }
    },
    mounted(){
    console.log(this.msg)
    },
    }

    2. .sync


    可以帮我们实现父组件向子组件传递的数据 的双向绑定,所以子组件接收到数据后可以直接修改,并且会同时修改父组件的数据


    // Parent.vue
    <template>
    <child :page.sync="page"></child>
    </template>
    <script>
    export default {
    data(){
    return {
    page:1
    }
    }
    }

    // Child.vue
    export default {
    props:["page"],
    computed(){
    // 当我们在子组件里修改 currentPage 时,父组件的 page 也会随之改变
    currentPage {
    get(){
    return this.page
    },
    set(newVal){
    this.$emit("update:page", newVal)
    }
    }
    }
    }
    </script>

    3. v-model


    和 .sync 类似,可以实现将父组件传给子组件的数据为双向绑定,子组件通过 $emit 修改父组件的数据


    // Parent.vue
    <template>
    <child v-model="value"></child>
    </template>
    <script>
    export default {
    data(){
    return {
    value:1
    }
    }
    }

    // Child.vue
    <template>
    <input :value="value" @input="handlerChange">
    </template>
    export default {
    props:["value"],
    // 可以修改事件名,默认为 input
    model:{
    event:"updateValue"
    },
    methods:{
    handlerChange(e){
    this.$emit("input", e.target.value)
    // 如果有上面的重命名就是这样
    this.$emit("updateValue", e.target.value)
    }
    }
    }
    </script>

    4. ref


    ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;


    如果在子组件上,引用的指向就是子组件实例,然后父组件就可以通过 ref 主动获取子组件的属性或者调用子组件的方法


    // Child.vue
    export default {
    data(){
    return {
    name:"沐华"
    }
    },
    methods:{
    someMethod(msg){
    console.log(msg)
    }
    }
    }

    // Parent.vue
    <template>
    <child ref="child"></child>
    </template>
    <script>
    export default {
    mounted(){
    const child = this.$refs.child
    console.log(child.name) // 沐华
    child.someMethod("调用了子组件的方法")
    }
    }
    </script>

    5. $emit / v-on


    子组件通过派发事件的方式给父组件数据,或者触发父组件更新等操作


    // Child.vue 派发
    export default {
    data(){
    return { msg: "这是发给父组件的信息" }
    },
    methods: {
    handleClick(){
    this.$emit("sendMsg",this.msg)
    }
    },
    }
    // Parent.vue 响应
    <template>
    <child v-on:sendMsg="getChildMsg"></child>
    // 或 简写
    <child @sendMsg="getChildMsg"></child>
    </template>

    export default {
    methods:{
    getChildMsg(msg){
    console.log(msg) // 这是父组件接收到的消息
    }
    }
    }

    6. $attrs / $listeners


    多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时


    $attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合。通过 this.$attrs 获取父作用域中所有符合条件的属性集合,然后还要继续传给子组件内部的其他组件,就可以通过 v-bind="$attrs"


    $listeners:包含父作用域里 .native 除外的监听事件集合。如果还要继续传给子组件内部的其他组件,就可以通过 v-on="$linteners"


    使用方式是相同的


    // Parent.vue
    <template>
    <child :name="name" title="1111" ></child>
    </template
    export default{
    data(){
    return {
    name:"沐华"
    }
    }
    }

    // Child.vue
    <template>
    // 继续传给孙子组件
    <sun-child v-bind="$attrs"></sun-child>
    </template>
    export default{
    props:["name"], // 这里可以接收,也可以不接收
    mounted(){
    // 如果props接收了name 就是 { title:1111 },否则就是{ name:"沐华", title:1111 }
    console.log(this.$attrs)
    }
    }

    7. $children / $parent


    $children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等


    $parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等


    // Parent.vue
    export default{
    mounted(){
    this.$children[0].someMethod() // 调用第一个子组件的方法
    this.$children[0].name // 获取第一个子组件中的属性
    }
    }

    // Child.vue
    export default{
    mounted(){
    this.$parent.someMethod() // 调用父组件的方法
    this.$parent.name // 获取父组件中的属性
    }
    }

    8. provide / inject


    provide / inject 为依赖注入,说是不推荐直接用于应用程序代码中,但是在一些插件或组件库里却是被常用,所以我觉得用也没啥,还挺好用的


    provide:可以让我们指定想要提供给后代组件的数据或方法


    inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用


    要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象


    所以建议还是传递一些常量或者方法


    // 父组件
    export default{
    // 方法一 不能获取 methods 中的方法
    provide:{
    name:"沐华",
    age: this.data中的属性
    },
    // 方法二 不能获取 data 中的属性
    provide(){
    return {
    name:"沐华",
    someMethod:this.someMethod // methods 中的方法
    }
    },
    methods:{
    someMethod(){
    console.log("这是注入的方法")
    }
    }
    }

    // 后代组件
    export default{
    inject:["name","someMethod"],
    mounted(){
    console.log(this.name)
    this.someMethod()
    }
    }

    9. EventBus


    EventBus 是中央事件总线,不管是父子组件,兄弟组件,跨层级组件等都可以使用它完成通信操作


    定义方式有三种


    // 方法一
    // 抽离成一个单独的 js 文件 Bus.js ,然后在需要的地方引入
    // Bus.js
    import Vue from "vue"
    export default new Vue()

    // 方法二 直接挂载到全局
    // main.js
    import Vue from "vue"
    Vue.prototype.$bus = new Vue()

    // 方法三 注入到 Vue 根对象上
    // main.js
    import Vue from "vue"
    new Vue({
    el:"#app",
    data:{
    Bus: new Vue()
    }
    })

    使用如下,以方法一按需引入为例


    // 在需要向外部发送自定义事件的组件内
    <template>
    <button @click="handlerClick">按钮</button>
    </template>
    import Bus from "./Bus.js"
    export default{
    methods:{
    handlerClick(){
    // 自定义事件名 sendMsg
    Bus.$emit("sendMsg", "这是要向外部发送的数据")
    }
    }
    }

    // 在需要接收外部事件的组件内
    import Bus from "./Bus.js"
    export default{
    mounted(){
    // 监听事件的触发
    Bus.$on("sendMsg", data => {
    console.log("这是接收到的数据:", data)
    })
    },
    beforeDestroy(){
    // 取消监听
    Bus.$off("sendMsg")
    }
    }

    10. Vuex


    Vuex 是状态管理器,集中式存储管理所有组件的状态。这一块内容过长,如果基础不熟的话可以看这个Vuex,然后大致用法如下


    比如创建这样的文件结构


    微信图片_20210824003500.jpg


    index.js 里内容如下


    import Vue from 'vue'
    import Vuex from 'vuex'
    import getters from './getters'
    import actions from './actions'
    import mutations from './mutations'
    import state from './state'
    import user from './modules/user'

    Vue.use(Vuex)

    const store = new Vuex.Store({
    modules: {
    user
    },
    getters,
    actions,
    mutations,
    state
    })
    export default store

    然后在 main.js 引入


    import Vue from "vue"
    import store from "./store"
    new Vue({
    el:"#app",
    store,
    render: h => h(App)
    })

    然后在需要的使用组件里


    import { mapGetters, mapMutations } from "vuex"
    export default{
    computed:{
    // 方式一 然后通过 this.属性名就可以用了
    ...mapGetters(["引入getters.js里属性1","属性2"])
    // 方式二
    ...mapGetters("user", ["user模块里的属性1","属性2"])
    },
    methods:{
    // 方式一 然后通过 this.属性名就可以用了
    ...mapMutations(["引入mutations.js里的方法1","方法2"])
    // 方式二
    ...mapMutations("user",["引入user模块里的方法1","方法2"])
    }
    }

    // 或者也可以这样获取
    this.$store.state.xxx
    this.$store.state.user.xxx

    11. $root


    $root 可以拿到 App.vue 里的数据和方法


    12. slot


    就是把子组件的数据通过插槽的方式传给父组件使用,然后再插回来


    // Child.vue
    <template>
    <div>
    <slot :user="user"></slot>
    </div>
    </template>
    export default{
    data(){
    return {
    user:{ name:"沐华" }
    }
    }
    }

    // Parent.vue
    <template>
    <div>
    <child v-slot="slotProps">
    {{ slotProps.user.name }}
    </child>
    </div>
    </template>

    结语


    写作不易,你的一赞一评,就是我前行的最大动力。


    链接:https://juejin.cn/post/6999687348120190983

    收起阅读 »

    前端9种图片格式基础知识, 你应该知道的

    彩色深度 彩色深度标准通常有以下几种: 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约...
    继续阅读 »

    彩色深度


    彩色深度标准通常有以下几种:



    • 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。

    • 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。

    • 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约1680万种颜色。

    • 32位真彩色,即在24位真彩色图像的基础上再增加一个表示图像透明度信息的Alpha通道。

      32位真彩色并非是2的32次方的色数,它其实也是1677万多色,不过它增加了256阶颜色的灰度,为了方便称呼,就规定它为32位色


    图的分类


    光栅图和矢量图


    对于图片,一般分光栅图和矢量图。



    • 光栅图:是基于 pixel像素构成的图像。JPEG、PNG,webp等都属于此类

    • 矢量图:使用点,线和多边形等几何形状来构图,具有高分辨率和缩放功能. SVG就是一种矢量图。


    无压缩, 无损压缩, 有损压缩


    另一种分类




    • 无压缩。无压缩的图片格式不对图片数据进行压缩处理,能准确地呈现原图片。BMP格式就是其中之一。




    • 无损压缩。压缩算法对图片的所有的数据进行编码压缩,能在保证图片的质量的同时降低图片的尺寸。png是其中的代表。




    • 有损压缩。压缩算法不会对图片所有的数据进行编码压缩,而是在压缩的时候,去除了人眼无法识别的图片细节。因此有损压缩可以在同等图片质量的情况下大幅降低图片的尺寸。其中的代表是jpg。




    前端9种图片格式


    诞生时间


    对于超过30岁的程序员来说,她们都很年轻,真的是遇到好时光!


    85年前,人们都在干嘛呢?



    1. GIF - 1987

    2. Base64- 1987

    3. JPEG - 1992

    4. PNG - 1996

    5. SVG - 1999

    6. JPEG2000 - 1997 to 2000

    7. APNG - 2004

    8. WebP - 2010


    ico: 1985年??

    查阅文档说ico文件格式是伴随着 Windows 1.0 发行诞生的。


    GIF


    GIF是一种索引色模式图片,所以GIF每帧图所表现的颜色最多为256种。GIF能够支持动画,也能支持背景透明,这点连古老的IE6都支持,所以在以前想要在项目中使用背景透明图片,其中一种方案就是生成GIF图片。


    优点



    • 支持动画和透明背景

    • 兼容性好

    • 灰度图像表现佳

    • 支持交错

      部分接收到的文件可以以较低的质量显示。这在网络连接缓慢时特别有用。


    缺点



    • 最多支持 8 位 256 色,色阶过渡糟糕,图片具有颗粒感

    • 支持透明,但不支持半透明,边缘有杂边


    适用场景



    • 色彩简单的logo、icon、线框图适合采用gif格

    • 动画


    JPG/JPEG


    这里提个问题: jpg和jpeg有啥区别


    平常我们大部分见到的静态图基本都是这种图片格式。这种格式的图片能比较好的表现各种色彩,主要在压缩的时候会有所失真,也正因为如此,造就了这种图片格式体积的轻量。


    优点



    • 压缩率高

    • 兼容性好

    • 色彩丰富


    缺点



    • JPEG不适合用来存储企业Logo、线框类的这种高清图

    • 不支持动画、背景透明


    JPEG 2000 (了解即可)


    JPEG 2000是基于小波变换的图像压缩标准,由Joint Photographic Experts Group组织创建和维护。JPEG 2000通常被认为是未来取代JPEG(基于离散余弦变换)的下一代图像压缩标准。JPEG 2000文件的副档名通常为.jp2,MIME类型是image/jp2。


    JPEG2000的压缩比更高,而且不会产生原先的基于离散余弦变换的JPEG标准产生的块状模糊瑕疵。JPEG2000同时支持有损压缩无损压缩


    目前就safari支持,can is use-png2000支持18%。


    优点



    • 支持有损和无损压缩


    缺点



    • 支持率太低了


    ICO


    ICO (Microsoft Windows 图标)文件格式是微软为 Windows 系统的桌面图标设计的。网站可以在网站的根目录中提供一个名为 favicon.ICO, 在收藏夹菜单中显示的图标,以及其他一些有用的标志性网站表示形式。

    一个 ICO 文件可以包含多个图标,并以列出每个图标详细信息的目录开始。


    其主要用来做网站图标,现在png也是可以用来做网站图标的。


    PNG


    PNG格式是有三种版本的,分别为PNG-8,PNG-24,PNG-32,所有这些版本都不支持动画的。PNG-8跟GIF类似的属性是相似的,都是索引色模式,而且都支持背景透明。相对比GIF格式好的特点在与背景透明时,图像边缘没有什么噪点,颜色表现更优秀。PNG-24其实就是无损压缩的JPEG。而PNG-32就是在PNG-24的基础上,增加了透明度的支持。


    如果没有动画需求推荐使用png-8来替代gif


    优点



    1. 不失真的情况下尽可能压缩图像文件的大小

    2. 像素丰富

    3. 支持透明(alpha通道)


    缺点



    1. 文件大


    这里额外提一下,gif和jpg有渐进,png有交错,都是在没有完全下载图片的时候,能看到图片全貌。


    具体可以看在线示例: png正常,png交错,jpg渐进


    APNG:Animated PNG


    APNG(Animated Portable Network Graphics)顾名思义是基于 PNG 格式扩展的一种动画格式,增加了对动画图像的支持,同时加入了 24 位图像和 8 位 Alpha 透明度的支持,这意味着动画将拥有更好的质量,其诞生的目的是为了替代老旧的 GIF 格式,但它目前并没有获得 PNG 组织官方的认可。


    从Can I Use上查看,除了IE系列, chrome, firefox, safari均已支持。2021-08月的时候支持达到94%。


    相对GIF来说



    • 色彩丰富

    • 支持透明

    • 向下兼容 PNG

    • 支持动画


    缺点



    • 生成比较繁琐

    • 未标准化


    webP


    有损 WebP 图像平均比视觉上类似压缩级别的 JPEG 图像小25-35% 。无损耗的 WebP 图像通常比 PNG 格式的相同图像小26% 。WebP 还支持动画: 在有损的 WebP 文件中,图像数据由 VP8位流表示,该位流可能包含多个帧。


    包括体积小、色彩表现足够、支持动画。 简直了就是心中的完美女神!!


    can i use - webp上看,支持率95%。 主要是Safari低版本和IE低版本不兼容。


    优点



    • 同等质量更小

    • 压缩之后质量无明显变化

    • 支持无损图像

    • 支持动画


    缺点



    • 兼容性吧,相对jpg,png,gif来说


    SVG


    SVG 是一种基于 xml 的矢量图形格式,它将图像的内容指定为一组绘图命令,这些命令创建形状、线条、应用颜色、过滤器等等。SVG 文件是理想的图表,图标和其他图像,可以准确地绘制在任何大小。因此,SVG 是现代 Web 设计中用户界面元素的流行选择。


    优点



    • 可伸缩性

      你可以随心所欲地把它们做大或者做小,而不用牺牲质量



    • Svg 平均比 GIF、 JPEG、 PNG 小得多,甚至在极高的分辨率下也是如此

    • 支持动画

      更灵活,质量无与伦比

    • 与DOM无缝衔接

      Svg 可以直接使用 HTML、 CSS 和 JavaScript (例如动画)来操作


    缺点



    • SVG复杂度高会减慢渲染速度

    • 不适合游戏类等高互动动画


    base64


    图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址,图片随着 HTML 的下载同时下载到本地,不再单独消耗一个http来请求图片。


    优点



    • 无额外请求

    • 对于极小或者极简单图片

    • 可像单独图片一样使用,比如背景图片重复使用等

    • 没有跨域问题,无需考虑缓存、文件头或者cookies问题  


    缺点



    • 相比其他格式,体积会至少大1/3

    • 编码解码有额外消耗


    一些对比


    PNG, GIF, JPG 比较


    大小比较:通常地,PNG ≈ JPG > GIF 8位的PNG完全可以替代掉GIF

    透明性:PNG > GIF > JPG

    色彩丰富程度:JPG > PNG >GIF

    兼容程度:GIF ≈ JPG > PNG

    gif, jpg, png, web优缺点和使用场景



    链接:https://juejin.cn/post/7000154907156152327

    收起阅读 »

    Why | 为什么需要虚拟内存?

    冯-诺依曼老爷子告诉过我们,算术逻辑单元和控制器单元组成的 CPU 负责进行运算以及程序流程的控制。运算所需要的指令和数据由 内存 来提供。 那么,如果让你作为操作系统的顶层设计者,你会提供一种什么机制,让 CPU 可以从内存中获取指令和数据呢? 用 ...
    继续阅读 »

    冯-诺依曼老爷子告诉过我们,算术逻辑单元和控制器单元组成的 CPU 负责进行运算以及程序流程的控制。运算所需要的指令和数据由 内存 来提供。


    Von_Neumann_Architecture.png


    那么,如果让你作为操作系统的顶层设计者,你会提供一种什么机制,让 CPU 可以从内存中获取指令和数据呢?


    用 C 语言写一个 Hello World,通过 objdump 查看汇编代码。我随便截取一行。


    mov    0x200aed(%rip),%rax        # 200fe8 <__gmon_start__>

    这一行汇编代码中包含了一个内存地址。这个内存地址是物理内存中的真实地址吗?


    我们假设它就是真实的物理地址,但是程序员在编程时是无法得知要运行的设备的内存信息的,所以针对不同的操作系统,得在编译期将程序中的地址转换为真实物理地址。这在单道编程的情况下可行,对于多道编程呢?不同的程序之间如何确定各自在内存中的位置?


    从单道编程到多道编程是计算机发展前进的一大步。CPU 通过轮询时间片的方式让多个程序仿佛在同时运行。显然,在程序中使用真实的物理地址会打破这一幻像,不同的程序之间不得而知对方用的是哪一块物理内存,各自的内存完全无法得到保护。


    所以,程序中的地址不能是真实的物理地址,但又要与真实的物理地址存在一定的映射关系 。我们把程序中的地址称为 虚拟地址 ,它至少应该具备以下特性:



    • 能通过一定的机制映射到真实的物理地址

    • 保证不同的程序(进程) 映射的真实物理地址之间互相独立

    • 它应该是自动工作的,对于程序开发者来说是透明的


    基于这三个特性,我们一起来探究一下 虚拟内存 的工作方式。


    一个萝卜一个坑,分段


    最直观的解决方案,给每个程序分配一块独立的内存空间,如下图所示。


    动态定位.png


    对于每个程序来说,它的虚拟内存空间都从 0 开始,基址寄存器 中存储其在物理内存空间的起始地址。所以,物理地址和虚拟地址之间就存在这样的关系:


    物理地址 = 虚拟地址 + 基址

    这样的地址转换由叫做 内存管理单元(Memory Management Unit,MMU) 的硬件负责完成。


    界限寄存器 可以存储程序占用内存的大小,也可以存储界限的物理地址,它提供基本的内存访问保护。如果 MMU 转换出的物理地址超过了界限,将会触发异常。每个 CPU 都有一对基址寄存器和界限寄存器,当发生进程切换时,更新寄存器的值,这样就做到了进程间内存独立。


    乍一看,基本满足了虚拟内存的三个特性,但事实上基本没有操作系统会这么干。由于它需要在虚拟内存和物理内存中分别分配一块连续的内存空间,再进行内存映射。这样的缺点很明显。


    第一,容易造成内存碎片。假设内存经过一段时间的使用,还剩下两块 128 MB 的小块,但此时用户需要运行一个内存占用 129 MB 的程序,在此机制下就无法成功分配内存。虽然可以通过内存交换,将内存拾掇拾掇,和磁盘换来换去,把空余内存拼接起来,但是这么大一块数据,磁盘读写的速度实在太慢了,性能上根本无法接受。


    第二,浪费了很多内存空间。如果把二八法则搬到计算机上,一个程序最经常运行的代码可能两成都占不到。而上面的方案在一开始就要分配好整个程序需要的内存空间,堆和栈之间有一大块的内存是空闲的。


    上面的方案暂且可以看成一种特殊的 “分段”。我们可以试着把段分的更细一些。


    典型的 Linux 进程用户空间内存包含栈、共享库、堆、数据、代码等。我们可以按这些基本类型来分段,为了方便演示,下图中仅分为 栈、堆、代码 三个段。


    分段.png


    将程序按逻辑分为一段一段,放入内存中对应的段区域内,这样避免了之前的方案中堆和栈之间的空间浪费,真正需要内存的时候才会去申请。同时顺带实现了共享。对于一些可以公用的系统基本库,在之前的方案中仍然需要拷贝到各个进程独立的空间中。而分段的方案中,只需要一份拷贝就行,不同进程间的虚拟地址映射到这一份物理拷贝就可以了。


    但是由于各个段的大小不一致,内存碎片的问题可能并不比上一个方案好到哪里去。


    另外,上面提到的所有方案都没有考虑到程序大小的问题。如果程序大小大于物理内存,你再怎么分段也没有办法解决问题。


    把段再分细一点,分页


    为了解决分段产生的内存碎片问题,我们把段分的再细一些,细成一个一个固定大小的页面,虚拟内存和固定内存都是如此。这个固定大小在当前主流操作系统中一般是 4 KB ,部分系统也支持 8 KB、16 KB、64 KB。


    将虚拟页和物理页一一对应起来,虚拟地址到物理地址的转换就不是难事了。


    不论是虚拟内存还是物理内存,在分页之后,给每页拟定一个 页号,再根据 页内偏移量 就可以取到数据了。由于虚拟页和物理页的页大小是一致的,所以页内偏移量无需转换,只需要把虚拟页号转换为物理页号就可以了。


    而虚拟地址正是由 虚拟页号页内偏移量 组成。


    操作系统将虚拟页号到物理页号的映射关系保存在 页表 中,页表是一个 页表项(PTE) 的数组,页表项包含了有效位,物理地址等数据。页表直接使用虚拟页号作为索引,找到对应的页表项。


    分页.png


    上图中的第 3 个虚拟页被映射到了第 2 个物理页。其实 虚拟页可以被映射到任意物理页,连续的虚拟页也不需要对应连续的物理页,这给了操作系统很大的自由。不仅相对减少了内存碎片的产生,也能更方便的实现进程间的数据共享,只要将不同进程的虚拟页映射到同样的物理页就行了。


    为了能直接使用虚拟页号作为索引检索到页表项,页表中的所有页表项必须连续的,并且要提前创建好。那么问题来了,页表有多大?


    以 32 位操作系统为例,最大寻址空间为 2 ^ 32 = 4 GB,页的大小为 4 KB,所以共需要 1 M 个页表项。每个页表项大小为 4 个字节,所以一个页表的大小为 1 M * 4 B = 4 MB 。为实现进程隔离,每个进程又必须有自己独立的页表。顺手看一下你的操作系统,至少都得有上百个进程在同时运行,光页表就得占用几百兆内存,这显然是不合适的。


    实际上,对大多数程序来说,并不需要占用全部的 4 GB 虚拟内存,所以没有必要在一开始就分配完整个页表。使用多级页表可以解决这个问题。


    时间换空间,多级页表


    还是以 32 位操作系统为例,来看个简单的二级页表。


    二级页表.png


    第一级叫 页目录项 ,共有 1 K 项。每一个页目录项又对应着 1 K 个 页表项,总共 1 K * 1 K = 1 M 个页表项,正好对应着 4 GB 的寻址空间。


    对于 32 位的虚拟地址来说,正好 10 位对应着 1 K 个页目录项索引,10 位对应着指定页目录项下的 1 K 个页表项索引,剩下 12 位正好对应页大小 4 KB 的页内偏移量。


    算一下二级页表的大小。1 K 个一级页目录项一共 4 KB,1 M 个二级页表项一共 4 MB ,加起来一共 4.004 MB


    所以,二级页表比普通页表占用的内存还要大?其实并不然。


    首先得明确一点,不管是几级页表,都必须要能覆盖整个虚拟空间。对于只有一级的普通页表来说,一上来就得初始化所有页表项,才能覆盖到整个虚拟空间地址。而对于二级页表来说,1 K 个一级的页目录项就可以足以覆盖,二级页表项只有在需要的时候才被创建。这样就可以节省相当一部分内存。


    另外,二级页表可以不存储在内存中,而是存在磁盘中。这倒并不是专门为多级页表而设计的,这是虚拟内存分页的特性,也正因如此,程序的大小可以大于实际物理内存的大小。


    页命中和缺页


    回想一下之前描述的寻址过程。虚拟地址经过内存管理单元 MMU 的处理,找到对应的页表项 PTE ,转换为物理地址,然后在物理内存中定位到对应的数据。这种理想的情况叫做 页命中 ,根据虚拟地址直接就可以在内存中获取到数据。


    但是,并不是任何时候都可以直接根据 PTE 在内存中拿到数据的。最典型的情况,程序的大小大于物理内存,必然会有数据不存在内存中。另外,由于多级页表并不是开始就创建,所以 PTE 对应的数据可能也不在内存中。


    在任意时刻,虚拟内存页都可以分为三个状态:



    • 未分配的:还未分配(或创建)的页。没有任何数据与其关联,不占用任何磁盘空间

    • 已缓存的:当前已缓存在物理内存中的已分配页

    • 未缓存的:未缓存在物理内存中的已分配页


    只有已缓存的虚拟页可以发生页命中,实际上 PTE 会有一个有效位来表示页表是否有效,假设 0 表示有效,1 表示无效。


    有效位为 0,表示 PTE 可用,直接读数据即可。有效位为 1,在不考虑非法内存地址的情况下,可以认为是未分配或者未缓存,无法直接从内存中读取数据,这种情况称为 缺页


    一旦发生缺页,将由系统的缺页异常处理程序来接管,它会根据特定算法从内存中寻找一个 牺牲页,如果该牺牲页数据被修改过,要先写回磁盘,然后将需要的页换到该牺牲页的位置,并更新 PTE。当异常处理程序返回时,它会重新执行之前导致缺页的命令,也就是之前的寻址操作,这次就直接页命中了。


    看到这,你会发现缺页是一个非常昂贵的操作,操作系统必须尽量减少缺页的发生,所以如何寻找合适的牺牲页是个大问题。如果你替换了一个即将要访问的页,那么一会又得把它换回来,这样频繁的换来换去是无法接受的。关于具体的替换算法,可以阅读 《操作系统导论》第22章 超越物理内存:策略


    缺页.png


    给页表加一层缓存,TLB


    再说回到页表,将虚拟地址转换为物理地址,如果使用未分级的普通页表只需要一次内存访问,但占用内存较大。大多数操作系统使用的是多级页表,例如目前的 64 位 Linux 操作系统,使用的是 四级页表,内存占用小了很多,但付出的代价是要访问四次内存。其实这就是一个 时间换空间 的策略。


    另外,程序执行时的一连串指令的虚拟地址是连续的,相连几个虚拟地址通常是在一个虚拟页中,自然而然它们都对应着同一个物理页。但是无论页表如何设计,访问相邻的虚拟地址,每次仍然都要去访问页表。这里是一个可以优化的点。


    计算机科学领域里的任何问题,都可以通过引入一个中间层来解决。


    既要保留多级页表的低内存特性,又要避免多余的内存访问,那就再加一层 缓存 吧。


    TLB(Translation Lookaside Buffer) ,有的资料翻译成 翻译后备缓冲器,有的翻译成 地址变换高速缓存,且不纠结这个名字。TLB 是封装在 CPU 里的一块缓存芯片,它就是页表的缓存,存储了虚拟地址和页表项的映射关系。


    当进行地址转换时,第一步就是根据虚拟地址从 TLB 中查询是否存在对应的页表项 PTE 。如果 TLB 命中,就不用访问页表了,直接根据 TLB 中缓存的物理地址去 CPU Cache 或者内存取数据。如果 TLB 未命中,和缺页的处理流程类似,通过抛出一个异常,让 TLB 的异常处理程序来接手,它会去访问页表,找到对应的页表项,然后更新 TLB 。当异常处理程序执行完后,会再次执行原来的指令,这时候会命中 TLB 。可想而知, TLB 的命中率直接影响了操作系统运行的效率。


    TLB.png


    总结


    先说说为什么写了这么一篇文章。


    最近在读 《深入理解 Android 内核设计思想》Binder 相关章节的时候,发现对 mmap 没有一个深度认识的话,就很难理解 Binder 只复制一次的底层逻辑。而如果对虚拟内存机制又没有一个很好的理解的话,也很难去理解 mmap 的实现原理。算是一环扣一环,倒逼学习了一波。


    其实编程领域的很多问题,归根到底都是针对计算机操作系统的特性,做出的解决方案和妥协。打好操作系统的扎实基础,对学习任何编程相关的知识,都是大有裨益的。但另一方面,操作系统的知识也多且杂,我也不敢保证我这篇文章没有任何错误。如果你对上面的内容有不同意见,欢迎评论区和我探讨。


    最后,一张图总结虚拟内存的工作机制。



    虚拟内存.png

    收起阅读 »

    【开源项目】Compose仿豆瓣榜单客户端,了解一下~

    前言 Compose正式发布也有一段时间了,感觉要上手还是得实战一波。 所以借着空闲时间,参照豆瓣榜单页面的设计,开发了几个Compose版的豆瓣榜单页面 UI效果还是挺好看的,有兴趣的同学可以点个Star:Compose仿豆瓣榜单客户端 效果图 首先看...
    继续阅读 »

    前言


    Compose正式发布也有一段时间了,感觉要上手还是得实战一波。
    所以借着空闲时间,参照豆瓣榜单页面的设计,开发了几个Compose版的豆瓣榜单页面
    UI效果还是挺好看的,有兴趣的同学可以点个Star:Compose仿豆瓣榜单客户端


    效果图


    首先看下最终的效果图


    douban_compress.gif


    特性


    在项目中主要用到了以下几个特性,以美化UI及体验



    1. 支持设置沉浸式状态栏及状态栏颜色

    2. 支持水平方向滚动,竖直方向滚动等多种UI效果

    3. 支持给Image设置渐变滤镜,以美化显示效果

    4. 支持标题与列表页联动

    5. 通过Paging支持了分页加载


    主要实现


    具体源码可以直接查看,这里主要介绍一些主要功能的实现


    沉浸式状态栏设置


    状态栏主要是通过accompanist-insetsaccompanist-systemuicontroller库设置的
    accompanist上提供了一系列常用的,如状态栏,权限,FlowLayout,ViewPagerCompose
    如果有时你发现基础库里没有相应的内容,可以去这里查找下


    设置状态栏主要分为以下几步



    1. 设置沉浸时状态栏

    2. 获取状态栏高度

    3. 设置状态栏颜色


    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 1. 设置状态栏沉浸式
    WindowCompat.setDecorFitsSystemWindows(window, false)

    setContent {
    BD_ToolTheme {
    // 加入ProvideWindowInsets
    ProvideWindowInsets {
    // 2. 设置状态栏颜色
    rememberSystemUiController().setStatusBarColor(
    Color.Transparent, darkIcons = MaterialTheme.colors.isLight)
    Column {
    // 3. 获取状态栏高度并设置占位
    Spacer(modifier = Modifier
    .statusBarsHeight()
    .fillMaxWidth())
    Text(text = "首页\r\n首页1\r\n首页2\r\n首页3")
    }
    }
    }
    }
    }

    通过以上方法,就可以比较简单的实现沉浸状态栏的设置


    Image设置渐变滤镜


    豆瓣榜单页面都给Image设置了渐变滤镜,以美化UI效果
    其实实现起来也比较简单,给Image前添加一层渐变的蒙层即可


    @Composable
    fun TopRankItem(item: HomeTopRank) {
    Box(
    modifier = Modifier
    .size(180.dp, 220.dp)
    .padding(8.dp)
    .clip(RoundedCornerShape(10.dp))
    ) {
    // 1. 图片
    Image(
    painter = rememberCoilPainter(request = item.imgUrl),
    contentDescription = null,
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxSize()
    )
    Column(
    modifier = Modifier
    .fillMaxSize()
    // 渐变滤镜
    .background(
    brush = Brush.linearGradient(
    colors = listOf(Color(item.startColor), Color(item.endColor)),
    start = Offset(0f, Float.POSITIVE_INFINITY),
    end = Offset(Float.POSITIVE_INFINITY, 0f)
    )
    )
    .padding(8.dp)

    ) {
    //内容
    }
    }
    }

    如上所示,使用Box布局,给前景设置一个从左下到右上渐变的背景即可


    标题与列表联动


    具体效果可见上面的动图,即在列表滚动时标题会有一个渐现渐隐效果
    这个效果其实我们在Android View体系中也很常见,主要思路也很简单:



    1. 监听列表滚动,获取列表滚动offset

    2. 根据列表滚动offset设置Header效果,如背景或者高度变化等


    @Composable
    fun RankScreen(viewModel: RankViewModel = RankViewModel()) {
    val scrollState = rememberLazyListState()
    Box {
    // 1. 监听列表
    LazyColumn(state = scrollState) {
    //列表内容
    }
    RankHeader(scrollState)
    }
    }

    @Composable
    fun RankHeader(scrollState: LazyListState) {
    val target = LocalDensity.current.run {
    200.dp.toPx()
    }
    // 2. 根据列表偏移量计算比例
    val scrollPercent: Float = if (scrollState.firstVisibleItemIndex > 0) {
    1f
    } else {
    scrollState.firstVisibleItemScrollOffset / target
    }
    val activity = LocalContext.current as Activity
    val backgroundColor = Color(0xFF7F6351)
    Column() {
    Spacer(
    modifier = Modifier
    .fillMaxWidth()
    .statusBarsHeight()
    // 3. 根据比例设置Header的alpha,以实现渐变效果
    .alpha(scrollPercent)
    .background(backgroundColor)
    )
    //....
    }
    }

    如上所示,主要有三步:



    1. 监听列表

    2. 根据列表偏移量计算比例

    3. 根据比例设置Headeralpha,以实现渐变效果


    利用Paging实现分页


    目前Pagin3已经支持了Compose,我们可以利用Paging轻松实现分页效果
    主要分为以下几步:



    1. ViewModel中设置数据源

    2. 在页面中监听Paging数据

    3. 根据加载状态设置加载更多footr状态


    //1. 设置数据源
    class RankViewModel : ViewModel() {
    val rankItems: Flow<PagingData<RankDetail>> =
    Pager(PagingConfig(pageSize = 10, prefetchDistance = 1)) {
    MovieSource()
    }.flow
    }

    @Composable
    fun RankScreen(viewModel: RankViewModel = RankViewModel()) {
    val lazyMovieItems = viewModel.rankItems.collectAsLazyPagingItems()
    Box {
    LazyColumn(state = scrollState) {
    // 2. 在页面中监听paging
    items(lazyMovieItems) {
    it?.let {
    RankListItem(it)
    }
    }
    // 3. 根据paging状态设置加载更多footer状态等
    lazyMovieItems.apply {
    when (loadState.append) {
    is LoadState.Loading -> {
    item { LoadingItem() }
    }
    }
    }
    }
    }
    }

    通过以上步骤,就可以比较简单方便地实现分页了


    总结


    项目地址


    ComposeDouban
    开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~

    收起阅读 »

    如何优雅的在业务中使用设计模式(代码如诗)

    前言 有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。 当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&...
    继续阅读 »

    前言



    有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。


    当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&、*、as_mut、as_ref。。。让人头秃。。。


    之前看到过一句话,觉得很不错:学习Rust并不会给你带来智商上的优越感,但或许会让你重新爱上编程。



    大家如果阅读过一些开源框架的源码,可能会发现其中数不尽的抽象类,设计模式拈手而来,在功能框架中,可以使用设计模式随心所欲的解耦;在实际的复杂业务中,当然也可以应用合适的设计模式。


    这篇文章,我会结合较为常见的实际业务场景,探讨如何使用合适的设计模式将业务解耦



    • 此处的应用绝不是生搬硬套,是我经过深思熟虑,并将较为复杂的业务进行全面重构后,得出的一套行之有效的思路历程

    • 任何一个设计模式都是一个伟大的经验及其思想总结,千人千面,如果对文章中内容,有不同的意见,希望你能在评论中提出,我们共同探讨,共同进步


    本文章是一篇弱代码类型文章,我会画大量的图片向大家展示,引用设计模式后,会对原有的业务流程,产生什么样的影响。


    前置知识


    这里,需要了解下基础知识,什么是责任链模式和策略模式


    责任链模式,在很多开源框架中都是有所应用,你如果听到啥啥拦截器,基本就是责任链模式,责任链模式的思想很简单,但是有很多种实现方式



    • 最简单的链表实现就和OkHttp的拦截器实现大相径庭

    • OkHttp的拦截器实现和Dio拦截器实现结构相同,但遍历方式不一样

    • 很多骚操作:我喜欢OkHttp的实现方式,喜欢dio的Api设计,结尾会给出一个结合这俩者思想的通用拦截器


    策略模式,或是天生适合业务,同一模块不同类型业务,如果行为相同,或许就可以考虑使用策略模式去解耦了


    责任链模式


    这边用Dart写一个简单的拦截器,dart和java非常像



    • 为了减少语言差异,我就不使用箭头语法了

    • 下划线表示私有


    用啥语言不重要,这边只是用代码简单演示下思想


    此处实现就用链表了;如果,使用数组的形式,需要多写很多逻辑,数组的优化写法在结尾给出,此处暂且不表


    结构



    • 责任链的结构,通常有俩种结构

      • 链表结构:链表构建责任链,十分便捷的就能和下一节点建立联系

      • 数组结构:数组,用通用的List即可,方便增删,不固定长度(别费劲的用固定长度Array了,例如:int[]、String[])



    责任链结构



    • 实现一个链表实体很简单


    abstract class InterceptChain<T> {
    InterceptChain? next;

    void intercept(T data) {
    next?.intercept(data);
    }
    }

    实现



    • 拦截器实现


    /// 该拦截器以最简单的链表实现
    abstract class InterceptChain<T> {
    InterceptChain? next;

    void intercept(T data) {
    next?.intercept(data);
    }
    }

    class InterceptChainHandler<T> {
    InterceptChain? _interceptFirst;

    void add(InterceptChain interceptChain) {
    if (_interceptFirst == null) {
    _interceptFirst = interceptChain;
    return;
    }

    var node = _interceptFirst!;
    while (true) {
    if (node.next == null) {
    node.next = interceptChain;
    break;
    }
    node = node.next!;
    }
    }

    void intercept(T data) {
    _interceptFirst?.intercept(data);
    }
    }


    • 使用

      • 调整add顺序,就调整了对应逻辑的节点,在整个责任链中的顺序

      • 去掉intercept重写方法中的super.intercept(data),就能实现拦截后续节点逻辑



    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(OneIntercept());
    intercepts.add(TwoIntercept());
    intercepts.intercept("测试拦截器");
    }

    class OneIntercept extends InterceptChain<String> {
    @override
    void intercept(String data) {
    data = "$data:OneIntercept";
    print(data);
    super.intercept(data);
    }
    }

    class TwoIntercept extends InterceptChain<String> {
    @override
    void intercept(String data) {
    data = "$data:TwoIntercept";
    print(data);
    super.intercept(data);
    }
    }


    • 打印结果


    测试拦截器:OneIntercept
    测试拦截器:OneIntercept:TwoIntercept

    策略模式


    结构



    • 策略模式最重要的:应该就是对抽象类的设计,对行为的抽象


    策略模式应用


    实现



    • 定义抽象类,抽象行为


    /// 结合适配器模式的接口适配:抽象必须实现行为,和可选实现行为
    abstract class BusinessAction {
    ///创建相应资源:该行为必须实现
    void create();

    ///可选实现
    void dealIO() {}

    ///可选实现
    void dealNet() {}

    ///可选实现
    void dealSystem() {}

    ///释放资源:该行为必须实现
    void dispose();
    }


    • 实现策略类


    //Net策略
    class NetStrategy extends BusinessAction {
    @override
    void create() {
    print("创建Net资源");
    }

    @override
    void dealNet() {
    print("处理Net逻辑");
    }

    @override
    void dispose() {
    print("释放Net资源");
    }
    }

    ///IO策略
    class IOStrategy extends BusinessAction {
    @override
    void create() {
    print("创建IO资源");
    }

    @override
    void dealIO() {
    print("处理IO逻辑");
    }

    @override
    void dispose() {
    print("释放IO资源");
    }
    }


    • 使用


    void main() {
    var type = 1;
    BusinessAction strategy;

    //不同业务使用不同策略
    if (type == 0) {
    strategy = NetStrategy();
    } else {
    strategy = IOStrategy();
    }

    //开始创建资源
    strategy.create();
    //......... 省略N多逻辑(其中某些场景,会有用到Net业务,和上面type是关联的)
    //IO业务:开始处理业务
    strategy.dealIO();
    //......... 省略N多逻辑
    //释放资源
    strategy.dispose();
    }


    • 结果


    创建IO资源
    处理IO逻辑
    释放IO资源

    适合的业务场景


    这边举一些适合上述设计模式的业务场景,这些场景是真实存在的!


    这些真实的业务,使用设计模式解耦和纯靠if else怼,完全是俩种体验!


    代码如诗,这并不是一句玩笑话。


    连环弹窗业务


    业务描述


    连环弹窗夺命call来袭。。。



    • A弹窗弹出:有确定和取消按钮

      • 确定按钮:B弹窗弹出(有查看详情和取消按钮)

        • 查看详情按钮:C弹窗弹出(有同意和拒绝按钮)

          • 同意按钮:D弹窗弹出(有查看和下一步按钮)

            • 查看按钮:E弹窗弹出(只有下一步按钮)

              • 下一步按钮:F弹窗弹出(结束)


            • 下一步按钮:F弹窗弹出(结束)


          • 拒绝按钮:流程结束


        • 取消按钮:流程结束


      • 取消按钮:流程结束



    好家伙,套娃真是无所不在,真不是我们代码套娃,实在是业务套娃,手动滑稽.png


    img



    • 图示弹窗业务


    连环弹窗业务1


    直接开搞


    看到这个业务,大家会去怎么做呢?



    • 有人可能会想,这么简单的业务还需要想吗?直接写啊!

      • A:在确定回调里面,跳转B弹窗

      • B:查看详情按钮跳转C弹窗

      • 。。。


    • 好一通套后,终于写完了



    产品来了,加需求


    B和C弹窗之间要加个预览G弹窗,点击B的查看详情按钮,跳转预览G弹窗;预览G弹窗只有一个确定按钮,点击后跳转C弹窗



    img



    • 你心里可能要想了,这特么不是坑爹?

      • 业务本来就超吉尔套,我B弹窗里面写的跳转代码要改,传参要改,而且还要加弹窗!


    • 先要去找产品撕比,撕完后

      • 然后继续在屎山上,小心翼翼的再拉了坨shit

      • 这座克苏鲁山初成规模



    连环弹窗业务2



    产品又来了,第一稿需求不合理,需要调整需求


    交换C和D弹窗位置,D弹窗点击下一步的时候,需要加一个校验请求,通过后才能跳转到C弹窗



    img



    • 你眉头一皱,发现事情没有表面这么简单

      • 由于初期图简单,几乎都写在一个文件里,眼花缭乱弹窗回调太多,而且弹窗样式也不一样

      • 现在改整个流程,导致你整个人脑子嗡嗡响


    • 心中怒气翻涌,找到产品说


    img



    • 回来,坐在椅子上,心里想:

      • 老夫写的代码天衣无缝,这什么几把需求

      • 可恶,这次测试,起码要给我多提十几个BUG



    image-20210822215435299



    • 克苏鲁山开始狰狞


    连环弹窗业务3



    产品飘来,加改需求:如此,如此,,,这般,这般,,,




    • 你....


    img



    产品:改下,,,然后,扔给你几十页的PRD



    你看了看这改了几十版的克苏鲁山,这几十个弹窗逻辑居然都写在一个文件里,快一万行的代码。。。



    • 心里不禁想:

      • 本帅比写的代码果然牛批,或许这就是艺术!艺术总是曲高和寡,难被人理解!而我的代码更牛批,连我自己都看不懂了!

      • 这代码行数!这代码结构!不得拍个照留念下,传给以后的孩子当传家宝供着!


    • 心里不禁嘚瑟:

      • 这块业务,除了我,还有谁敢动,成为头儿的心腹,指日可待!



    16c3-ikhvemy5945899



    • 但,转念深思后:事了拂衣去,深藏功与名


    img


    重构



    随着业务的逐渐复杂,最初的设计缺点会逐渐暴露;重构有缺陷的代码流程,变得势在必行,这会极大的降低维护成本



    如果心中对责任链模式有一些概念的话,会发现上面的业务,极其适合责任链模式!


    对上面的业务进行分析,可以明确一些事



    • 这个业务是一个链式的,有着明确的方向性:单向,从头到尾指向

    • 业务拆分开,可以将一个弹窗作为单颗粒度,一个弹窗作为节点

    • 上级的业务节点可以对下级节点拦截(点击取消,拒绝按钮,不再进行后续业务)


    重构上面的代码,只要明确思想和流程就行了



    第一稿业务




    • 业务流程


    连环弹窗业务1



    • 责任链


    责任链业务1



    • 代码:简写


    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(AIntercept());
    intercepts.add(BIntercept());
    intercepts.add(CIntercept());
    intercepts.add(DIntercept());
    intercepts.add(EIntercept());
    intercepts.add(FIntercept());
    intercepts.intercept("测试拦截器");
    }


    第二稿业务




    • 业务流程


    连环弹窗业务2



    • 责任链


    责任链业务2



    • 代码:简写


    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(AIntercept());
    intercepts.add(BIntercept());
    intercepts.add(GIntercept());
    intercepts.add(CIntercept());
    intercepts.add(DIntercept());
    intercepts.add(EIntercept());
    intercepts.add(FIntercept());
    intercepts.intercept("测试拦截器");
    }


    第三稿业务




    • 业务流程


    连环弹窗业务3



    • 责任链


    责任链业务3



    • 代码:简写


    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(AIntercept());
    intercepts.add(BIntercept());
    intercepts.add(GIntercept());
    intercepts.add(DIntercept());
    intercepts.add(CIntercept());
    intercepts.add(EIntercept());
    intercepts.add(FIntercept());
    intercepts.intercept("测试拦截器");
    }


    总结



    经过责任链模式重构后,业务节点被明确的区分开,整个流程从代码上看,都相当的清楚,维护将变的异常轻松;或许,此时能感受到一些,编程的乐趣了


    img


    花样弹窗业务


    业务描述


    来描述一个新的业务:这个业务场景真实存在某办公软件



    • 进入APP首页后,和后台建立一个长连接

    • 后台某些工单处理后,会通知APP处理,此时app会弹出处理工单的弹窗(app顶部)

    • 弹窗类型很多:工单处理弹窗,流程审批弹窗,邀请类型弹窗,查看工单详情弹窗,提交信息弹窗。。。

    • 弹窗弹出类型,是根据后台给的Type进行判断:从而弹出不同类型弹窗、点击其按钮,跳转不同业务,传递不同参数。


    花样弹窗业务


    分析



    确定设计



    这个业务,是一种渐变性的引导你搭建克苏鲁代码山



    • 在前期开发的时候,一般只有俩三种类型弹窗,前期十分好做;根本不用考虑如何设计,抬手一行代码,反手一行代码,就能搞定

    • 但是后来整个业务会渐渐的鬼畜,不同类型会慢慢加到几十种之多!!!


    首先这个业务,使用责任链模式,肯定是不合适的,因为弹窗之间的耦合性很低,并没有什么明确的上下游关系


    但是,这个业务使用策略模式非常的合适!



    • type明确:不同类型弹出不同弹窗,按钮执行不同逻辑

    • 抽象行为明确:一个按钮就是一种行为,不同行为的实现逻辑大相径庭



    抽象行为



    多样弹窗的行为抽象,对应其按钮就行了


    确定、取消、同意、拒绝、查看详情、我知道了、提交


    直接画图来表示吧


    花样弹窗业务-抽象行为


    实现


    来看下简要的代码实现,代码不重要,重要的是思想,这边简要的看下代码实现流程



    • 抽象基类


    /// 默认实现抛异常,可提醒未实现方法被误用
    abstract class DialogAction {
    ///确定
    void onConfirm() {
    throw 'DialogAction:not implement onConfirm()';
    }

    ///取消
    void onCancel() {
    throw 'DialogAction:not implement onCancel()';
    }

    ///同意
    void onAgree() {
    throw 'DialogAction:not implement onAgree()';
    }

    ///拒绝
    void onRefuse() {
    throw 'DialogAction:not implement onRefuse()';
    }

    ///查看详情
    void onDetail() {
    throw 'DialogAction:not implement onDetail()';
    }

    ///我知道了
    void onKnow() {
    throw 'DialogAction:not implement onKnow()';
    }

    ///提交
    void onSubmit() {
    throw 'DialogAction:not implement onSubmit()';
    }
    }


    • 实现逻辑类


    class OneStrategy extends DialogAction {
    @override
    void onConfirm() {
    print("确定");
    }

    @override
    void onCancel() {
    print("取消");
    }
    }

    class TwoStrategy extends DialogAction{
    @override
    void onAgree() {
    print("同意");
    }

    @override
    void onRefuse() {
    print("拒绝");
    }
    }

    //........省略其他实现


    • 使用


    void main() {
    //根据接口获取
    var type = 1;
    DialogAction strategy;
    switch (type) {
    case 0:
    strategy = DefaultStrategy();
    break;
    case 1:
    strategy = OneStrategy();
    break;
    case 2:
    strategy = TwoStrategy();
    break;
    case 3:
    strategy = ThreeStrategy();
    break;
    case 4:
    strategy = FourStrategy();
    break;
    case 5:
    strategy = FiveStrategy();
    break;
    default:
    strategy = DefaultStrategy();
    break;
    }

    //聚合弹窗按钮触发事件
    BusinessDialog(
    //确定按钮
    onConfirm: () {
    strategy.onConfirm();
    },
    //取消按钮
    onCancel: () {
    strategy.onCancel();
    },
    //同意按钮
    onAgree: () {
    strategy.onAgree();
    },
    //拒绝按钮
    onRefuse: () {
    strategy.onRefuse();
    },
    //查看详情按钮
    onDetail: () {
    strategy.onDetail();
    },
    //我知道了按钮
    onKnow: () {
    strategy.onKnow();
    },
    //提交按钮
    onSubmit: () {
    strategy.onSubmit();
    },
    );
    }


    • 图示


    花样弹窗业务-业务流程


    一个复杂业务场景的演变


    我们看下,一个简单的提交业务流,怎么逐渐变的狰狞


    我会逐渐给出一个合适的解决方案,如果大家有更好的想法,务必在评论区告诉鄙人


    业务描述:我们的车子因不可抗原因坏了,要去维修厂修车,工作人员开始登记这个损坏车辆。。。


    业务的演变



    第一稿


    初始业务



    登记一个维修车辆的流程,实际上还是满麻烦的



    • 登记一个新车,需要将车辆详细信息登记清楚:车牌、车架、车型号、车辆类型、进出场时间、油量、里程。。。

    • 还需要登记一下用户信息:姓名、手机号、是否隶属公司。。。

    • 登记车损程度:车顶、车底、方向盘、玻璃、离合器、刹车。。。

    • 车内物品:车座皮套、工具。。。

    • 以及其他我没想到的。。。

    • 最后:提交所有登记好的信息


    第一稿,业务流程十分清晰,细节复杂,但是做起来不难


    车辆登记-第一稿



    第二稿(实际是多稿聚合):增加下述几个流程


    外部登记:外部登记了一个维修车辆部分信息(后台,微信小程序,H5等等),需要在app上完善信息,提交接口不同(必带车牌号)


    快捷洗车:洗车业务极其常见,快捷生成对应信息,提交接口不同


    预约订单登记:预约好了车辆一部分一些信息,可快捷登记,提交接口不同(必带车牌号)



    因为登记维修车辆流程,登记车辆信息流程极其细致繁琐,我们决定复用登记新车模块



    • 因为此处逻辑大多涉及开头和结尾,中间登记车辆信息操作几乎未改动,复用想法是可行的

    • 如果增加车辆登记项,新的三个流程也必须提交这些信息;所以,复用势在必行


    因为这一稿需求,业务也变得愈加复杂


    车辆登记-第二稿



    第三稿


    现在要针对不同的车辆类型,做不同的处理;车类型分:个人车,集团车


    不同类型的登记,在提交的时候,需要校验不同的信息;校验不通过,需要提示用户,并且不能进行提交流程


    提交后,需要处理下通用业务,然后跳转到某个页面



    第三稿的描述不多,但是,大大的增加了复杂度



    • 尤其是不同类型校验过程还不同,还能中断后续提交流程

    • 提交流程后,还需要跳转通用页面


    车辆登记-第三稿


    开发探讨


    第一稿



    • 业务流程


    车辆登记-第一稿



    • 开发


    正常流程开发、、、


    第二稿



    • 业务流程


    车辆登记-第二稿



    • 思考


    对于第二稿业务,可以好好考虑下,怎么去设计?


    开头和结尾需要单独写判断,去处理不同流程的业务,这至少要写俩个大的判断模块,接受数据的入口模块可能还要写判断


    这样就非常适合策略模式去做了


    开头根据执行的流程,选择相应的策略对象,后续将逻辑块替换抽象的策略方法就OK了,大致流程如下


    车辆登记-第二稿(策略模式)


    第三稿



    业务流程



    车辆登记-第三稿



    探讨




    • 第三稿的需求,实际上,已经比较复杂了



      • 整个流程中掺杂着不同业务流程处理,不同流程逻辑又拥有阻断下游机制(绿色模块)

      • 下游逻辑又会合流(结尾)的多种变换


    • 在这一稿的需求



      • 使用策略模式肯定是可以的

      • 阻断那块(绿色模块)需要单独处理下:抽象方法应该拥有返回值,外层根据返回值,判断是否进行后续流程

      • 但!这!也太不优雅了!


    • 思考上面业务一些特性



      • 拦截下游机制

      • 上游到下游、方向明确

      • 随时可能插入新的业务流程。。。



    可以用责任链模式!但,需要做一些小改动!这地方,我们可以将频繁变动的模块用责任链模式全都隔离出来



    • 看下,使用责任链模式改造后流程图


    车辆登记-第三稿(责任链模式)



    浏览上述流程图可发现,本来是极度杂乱糅合的业务,可以被设计相对更加平行的结构




    • 对于上述流程,可以进一步分析,并进一步简化:对整体业务分析,我们需要去关注其变或不变的部分



      • 不变:整体业务变动很小的是,登记信息流程(主体逻辑这块),此处的相关变动是很小的,对所有流程也是共用的部分

      • 变:可以发现,开头和结尾是变动更加频繁的部分,我们可以对此处逻辑进行整体的抽象


    • 抽象多变的开头和结尾



    车辆登记-第三稿(责任链模式——简化)



    • 所以我们抽象拦截类,可以做一些调整


    abstract class InterceptChainTwice<T> {
    InterceptChainTwice? next;

    void onInit(T data) {
    next?.onInit(data);
    }

    void onSubmit(T data) {
    next?.onSubmit(data);
    }
    }


    来看下简要的代码实现,代码不重要,主要看看实现流程和思想




    • 抽象拦截器


    abstract class InterceptChainTwice<T> {
    InterceptChainTwice? next;

    void onInit(T data) {
    next?.onInit(data);
    }

    void onSubmit(T data) {
    next?.onSubmit(data);
    }
    }

    class InterceptChainTwiceHandler<T> {
    InterceptChainTwice? _interceptFirst;

    void add(InterceptChainTwice interceptChain) {
    if (_interceptFirst == null) {
    _interceptFirst = interceptChain;
    return;
    }

    var node = _interceptFirst!;
    while (true) {
    if (node.next == null) {
    node.next = interceptChain;
    break;
    }
    node = node.next!;
    }
    }

    void onInit(T data) {
    _interceptFirst?.onInit(data);
    }

    void onSubmit(T data) {
    _interceptFirst?.onSubmit(data);
    }
    }


    • 实现拦截器


    /// 开头通用拦截器
    class CommonIntercept extends InterceptChainTwice<String> {
    @override
    void onInit(String data) {
    //如果有车牌,请求接口,获取数据
    //.................
    //填充页面
    super.onInit(data);
    }
    }

    /// 登记新车拦截器
    class RegisterNewIntercept extends InterceptChainTwice<String> {
    @override
    void onInit(String data) {
    //处理开头针对登记新车的单独逻辑
    super.onInit(data);
    }

    @override
    void onSubmit(String data) {
    var isPass = false;
    //如果校验不过,拦截下游逻辑
    if (!isPass) {
    return;
    }
    // ......
    super.onSubmit(data);
    }
    }

    /// 省略其他实现


    • 使用


    void main() {
    var type = 0;
    var intercepts = InterceptChainTwiceHandler();

    intercepts.add(CommonIntercept());
    intercepts.add(CarTypeDealIntercept());
    if (type == 0) {
    //登记新车
    intercepts.add(CommonIntercept());
    } else if (type == 1) {
    //外部登记
    intercepts.add(OutRegisterIntercept());
    } else if (type == 2) {
    //快捷洗车
    intercepts.add(FastWashIntercept());
    } else {
    //预约订单登记
    intercepts.add(OrderRegisterIntercept());
    }
    intercepts.add(TailIntercept());

    //业务开始
    intercepts.onInit("传入数据源");

    //开始处理N多逻辑
    //............................................................
    //经历了N多逻辑

    //提交按钮触发事件
    SubmitBtn(
    //提交按钮
    onSubmit: () {
    intercepts.onSubmit("传入提交数据");
    },
    );
    }

    总结


    关于代码部分,关键的代码,我都写出来,用心看看,肯定能明白我写的意思


    也不用找我要完整代码了,这些业务demo代码写完后,就删了


    本栏目这个业务,实际上是非常常见的的一个业务,一个提交流程与很多其它的流程耦合,整个业务就会慢慢的变的鬼畜,充满各种判断,很容易让人陷入泥泞,或许,此时可以对已有业务进行思考,如何进行合理的优化


    该业务的演变历程,和开发改造是本人的一次思路历程,如大家有更好的思路,还请不吝赐教。


    通用拦截器


    我结合OkHttp的思想和Dio的API,封装了俩个通用拦截器,这边贴下代码,如果哪里有什么不足,请及时告知本人


    说明下:这是Dart版本的


    抽象单方法


    ///一层通用拦截器,T的类型必须一致
    abstract class InterceptSingle<T> {
    void intercept(T data, SingleHandler handler) => handler.next(data);
    }

    ///添加拦截器,触发拦截器方法入口
    class InterceptSingleHandler<T> {
    _InterceptSingleHandler _handler = _InterceptSingleHandler(
    index: 0,
    intercepts: [],
    );

    void add(InterceptSingle intercept) {
    //一种类型的拦截器只能添加一次
    for (var item in _handler.intercepts) {
    if (item.runtimeType == intercept.runtimeType) {
    return;
    }
    }

    _handler.intercepts.add(intercept);
    }

    void delete(InterceptSingle intercept) {
    _handler.intercepts.remove(intercept);
    }

    void intercept(T data) {
    _handler.next(data);
    }
    }

    ///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
    abstract class SingleHandler {
    next(dynamic data);
    }

    ///实现init处理器
    class _InterceptSingleHandler extends SingleHandler {
    List<InterceptSingle> intercepts;

    int index;

    _InterceptSingleHandler({
    required this.index,
    required this.intercepts,
    });

    @override
    next(dynamic data) {
    if (index >= intercepts.length) {
    return;
    }

    var intercept = intercepts[index];
    var handler =
    _InterceptSingleHandler(index: index + 1, intercepts: intercepts);

    intercept.intercept(data, handler);
    }
    }

    抽象双方法


    ///俩层通用拦截器,T的类型必须一致
    abstract class InterceptTwice<T> {
    void onInit(T data, TwiceHandler handler) => handler.next(data);

    void onSubmit(T data, TwiceHandler handler) => handler.next(data);
    }

    ///添加拦截器,触发拦截器方法入口
    class InterceptTwiceHandler<T> {
    _TwiceInitHandler _init = _TwiceInitHandler(index: 0, intercepts: []);
    _TwiceSubmitHandler _submit = _TwiceSubmitHandler(index: 0, intercepts: []);

    void add(InterceptTwice intercept) {
    //一种类型的拦截器只能添加一次
    for (var item in _init.intercepts) {
    if (item.runtimeType == intercept.runtimeType) {
    return;
    }
    }

    _init.intercepts.add(intercept);
    _submit.intercepts.add(intercept);
    }

    void delete(InterceptTwice intercept) {
    _init.intercepts.remove(intercept);
    _submit.intercepts.remove(intercept);
    }

    void onInit(T data) {
    _init.next(data);
    }

    void onSubmit(T data) {
    _submit.next(data);
    }
    }

    ///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
    abstract class TwiceHandler {
    next(dynamic data);
    }

    ///实现init处理器
    class _TwiceInitHandler extends TwiceHandler {
    List<InterceptTwice> intercepts;

    int index;

    _TwiceInitHandler({
    required this.index,
    required this.intercepts,
    });

    @override
    next(dynamic data) {
    if (index >= intercepts.length) {
    return;
    }

    var intercept = intercepts[index];
    var handler = _TwiceInitHandler(index: index + 1, intercepts: intercepts);

    intercept.onInit(data, handler);
    }
    }

    ///实现submit处理器
    class _TwiceSubmitHandler extends TwiceHandler {
    List<InterceptTwice> intercepts;

    int index;

    _TwiceSubmitHandler({
    required this.index,
    required this.intercepts,
    });

    @override
    next(dynamic data) {
    if (index >= intercepts.length) {
    return;
    }

    var intercept = intercepts[index];
    var handler = _TwiceSubmitHandler(index: index + 1, intercepts: intercepts);

    intercept.onSubmit(data, handler);
    }
    }

    最后


    第一次,写这种结合业务的文章


    如有收获,还请点个赞,让我感受一下,各位是否读有所获~~


    img



    感谢阅读,下次再会~~


    img

    收起阅读 »

    PermissionX 1.5发布,支持申请Android特殊权限啦

    前言 Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。 不过之...
    继续阅读 »

    前言


    Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。


    不过之前一直有朋友在反映,对于 Android 中的一些特殊权限申请,PermissionX 并不支持。是的,PermissionX 本质上只是对 Android 运行时权限 API 进行了一层封装,用于简化运行时权限申请的。而这些特殊权限并不属于 Android 运行时权限的一部分,所以 PermissionX 自然也是不支持的。


    但是特殊权限却是我们这些开发者们可能经常要与之打交道的一部分,它们并不难写,但是每次去写都感觉很繁琐。因此经慎重考虑之后,我决定将几个比较常用的特殊权限纳入 PermissionX 的支持范围。那么本篇文章我们就来看一看,对于这几个常见的特殊权限,使用 PermissionX 和不使用 PermissionX 的写法有什么不同之处。


    事实上,Android 的权限机制也是经历过长久的迭代的。在 6.0 系统之前,Google 将权限机制设计的比较简单,你的应用程序需要用到什么权限,只需要在 AndroidManifest.xml 文件中声明一下就可以了。


    但是从 6.0 系统开始,Android 引入了运行时权限机制。Android 将常用的权限大致归成了几类,一类是普通权限,一类是危险权限,一类是特殊权限。


    普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,这种权限和过去一样,只需要在 AndroidManifest.xml 文件中声明一下就可以了,不需要做任何特殊处理。


    危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等。这部分权限需要通过代码进行申请,并要用户手动同意才可获得授权。PermissionX 库主要就是处理的这种权限的申请。


    而特殊权限则更加少见,Google 认为这种权限比危险权限还要敏感,因此不能仅仅让用户手动同意就可以获得授权,而是需要让用户到专门的设置页面去手动对某一个应用程序授权,该程序才能使用这个权限。


    不过相比于危险权限,特殊权限没有非常固定的申请方式,每个特殊权限可能都要使用不同的写法才行,这也导致申请特殊权限比申请危险权限还要繁琐。


    从 1.5.0 版本开始,PermissionX 对最常用的几个特殊权限进行了支持。正如刚才所说,特殊权限没有固定的申请方式,因此 PermissionX 也是针对于这几个特殊权限一个一个去适配并支持的。如果你发现你需要申请的某个特殊权限还没有被 PermissionX 支持,也可以向我提出需求,我会考虑在接下来的版本中加入。


    在过去,我们发布开源库通常都是发布到 jcenter 上的,但是相信大家现在都已经知道了,jcenter 即将停止服务,具体可以参考我的这篇文章 浅谈 JCenter 即将被停止服务的事件


    目前的 jcenter 处在一个半废弃的边缘,虽然还可以正常从 jcenter 下载开源库,但是已经不能再向 jcenter 发布新的开源库了。而在明年 2 月 1 号之后,下载服务也会被关停。


    所以,以后要想再发布开源库我们只能选择发布到其他仓库,比如现在 Google 推荐我们使用 Maven Central。


    于是,从 1.5.0 版本开始,PermissionX 也会将库发布到 Maven Center 上,之前的老版本由于迁移价值并不大,所以我也不想再耗费经历做迁移了。1.5.0 之前的版本仍然保留在 jcenter 上,提供下载服务直到明年的 2 月 1 号。


    而关于如何将库发布到 Maven Central,请参考 再见 JCenter,将你的开源库发布到 MavenCentral 上吧


    Android的特殊权限


    Android 里具体有哪些特殊权限呢?


    说实话,这个我也不太清楚。我所了解的特殊权限基本都是因为需要用到了,然后发现这个权限即不属于普通权限,也不属于危险权限,要用一种更加特殊的方式去申请,才知道原来这是一个特殊权限。


    因此,PermissionX 1.5.0 版本中对特殊权限的支持,也就仅限于我知道的,以及从网友反馈得来的几个最为常用的特殊权限。


    一共是以下 3 个:



    1. 悬浮窗

    2. 修改设置

    3. 管理外部存储


    接下来我就分别针对这 3 个特殊权限做一下更加详细的介绍。


    悬浮窗


    悬浮窗功能在不少应用程序中使用得非常频繁,因为你可能总有一些内容是要置顶于其他内容之上显示的,这个时候用悬浮窗来实现就会非常方便。


    当然,如果你只是在自己的应用内部实现悬浮窗功能是不需要申请权限的,但如果你的悬浮窗希望也能置顶于其他应用程序的上方,这就必须得要申请权限了。


    悬浮窗的权限名叫做 SYSTEM_ALERT_WINDOW,如果你去查一下这个权限的文档,会发现这个权限的申请方式比较特殊:



    按照文档上的说法,从 Android 6.0 系统开始,我们在使用 SYSTEM_ALERT_WINDOW 权限前需要发出一个 action 为 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 的 Intent,引导用户手动授权。另外我们还可以通过 Settings.canDrawOverlays() 这个 API 来判断用户是否已经授权。


    因此,想要申请悬浮窗权限,自然而然就可以写出以下代码:


    if (Build.VERSION.SDK_INT >= 23) {
    if (Settings.canDrawOverlays(context)) {
    showFloatView()
    } else {
    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
    startActivity(intent)
    }
    } else {
    showFloatView()
    }


    看上去也不复杂嘛。


    确实,但是它麻烦的点主要在于,它的请求方式是脱离于一般运行时权限的请求方式的,因此得要为它额外编写独立的权限请求逻辑才行。


    而 PermissionX 的目标就是要弱化这种独立的权限请求逻辑,减少差异化代码编写,争取使用同一套 API 来实现对特殊权限的请求。


    如果你已经比较熟悉 PermissionX 的用法了,那么以下代码你一定不会陌生:


    PermissionX.init(activity)
    .permissions(Manifest.permission.SYSTEM_ALERT_WINDOW)
    .onExplainRequestReason { scope, deniedList ->
    val message = "PermissionX需要您同意以下权限才能正常使用"
    scope.showRequestReasonDialog(deniedList, message, "Allow", "Deny")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(activity, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    可以看到,这就是最标准的 PermissionX 的正常用法,但是我们在这里却用来请求了悬浮窗权限。也就是说,即使是特殊权限,在 PermissionX 中也可以用普通的方式去处理。


    另外不要忘记,所有申请的权限都必须在 AndroidManifest.xml 进行注册才行:


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissionx.app">

    <uses-permission android: />

    </manifest>


    那么运行效果是什么样的呢?我们来看看吧:



    可以看到,PermissionX 还自带了一个权限提示框,友好地告知用户我们需要悬浮窗权限,引导用户去手动开启。


    修改设置


    了解了悬浮窗权限的请求方式之后,接下来我们就可以快速过一下修改设置权限的请求方式了,因为它们的用法是完全一样的。


    修改设置的权限名叫 WRITE_SETTINGS,如果我们去查看一下它的文档,你会发现它和刚才悬浮窗权限的文档简直如出一辙:



    同样是从 Android 6.0 系统开始,在使用 WRITE_SETTINGS 权限前需要先发出一个 action 为 Settings.ACTION_MANAGE_WRITE_SETTINGS 的 Intent,引导用户手动授权。然后我们还可以通过 Settings.System.canWrite() 这个 API 来判断用户是否已经授权。


    所以,如果是自己手动申请这个权限,相信你已经知道要怎么写了。


    那么用 PermissionX 申请的话应该要怎么写呢?这个当然就更简单了,只需要把要申请的权限替换一下即可,其他部分都不用作修改:


    PermissionX.init(activity)
    .permissions(Manifest.permission.WRITE_SETTINGS)
    ...


    当然,不要忘记在 AndroidManifest.xml 中注册权限:


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissionx.app">

    <uses-permission android: />

    </manifest>


    运行一下,效果如下图所示:



    管理外部存储


    管理外部存储权限也是一种特殊权限,它可以允许你的 App 拥有对整个 SD 卡进行读写的权限。


    有些朋友可能会问,SD 卡本来不就是可以全局读写的吗?为什么还要再申请这个权限?


    那你一定是没有了解 Android 11 上的 Scoped Storage 功能。从 Android 11 开始,Android 系统强制启用了 Scoped Storage,所有 App 都不再拥有对 SD 卡进行全局读写的权限了。


    关于 Scoped Storage 的更多内容,可以参考我的这篇文章 Android 11 新特性,Scoped Storage 又有了新花样


    但是如果有的应用就是要对 SD 卡进行全局读写该怎么办呢(比如说文件浏览器)?


    不用担心,Google 仍然还是给了我们一种解决方案,那就是请求管理外部存储权限。


    这个权限是 Android 11 中新增的,为的就是应对这种特殊场景。


    那么这个权限要怎么申请呢?我们还是先来看一看文档:



    大致可以分为几步吧:


    第一,在 AndroidManifest.xml 中声明 MANAGE_EXTERNAL_STORAGE 权限。


    第二,发出一个 action 为 Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 的 Intent,引导用户手动授权。


    第三,调用 Environment.isExternalStorageManager() 来判断用户是否已授权。


    传统请求权限的写法我就不再演示了,使用 PermissionX 来请求的写法仍然也还是差不多的。只不过要注意,因为 MANAGE_EXTERNAL_STORAGE 权限是 Android 11 系统新加入的,所以我们也只应该在 Android 11 以上系统去请求这个权限,代码如下所示:


    if (Build.VERSION.SDK_INT >= 30) {
    PermissionX.init(this)
    .permissions(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
    ...
    }


    AndroidManifest.xml 中的权限如下:


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissionx.app">

    <uses-permission android: />

    </manifest>


    运行一下程序,效果如下图所示:



    这样我们就拥有全局读写 SD 卡的权限了。


    另外 PermissionX 还有一个特别方便的地方,就是它可以一次性申请多个权限。假如我们想要同时申请悬浮窗权限和修改设置权限,只需要这样写就可以了:


    PermissionX.init(activity)
    .permissions(Manifest.permission.SYSTEM_ALERT_WINDOW, Manifest.permission.WRITE_SETTINGS)
    ...


    运行效果如下图所示:



    当然你也可以将特殊权限与普通运行时权限放在一起申请,PermissionX 对此也是支持的。只有当所有权限都请求结束时,PermissionX 才会将所有权限的请求结果一次性回调给开发者。


    关于 PermissionX 新版本的内容变化就介绍到这里,升级的方式非常简单,修改一下 dependencies 当中的版本号即可:


    repositories {
    google()
    mavenCentral()
    }


    dependencies {
    implementation 'com.guolindev.permissionx:permissionx:1.5.0'
    }


    注意现在一定要使用 mavenCentral 仓库,而不能再使用 jcenter 了。



    如果你对 PermissionX 的源码感兴趣,可以访问 PermissionX 的项目主页:


    github.com/guolindev/P…

    收起阅读 »

    iOS逆向必学-logos语法

    一、概述Logos语法其实是CydiaSubstruct框架提供的一组宏定义。便于开发者使用宏进行HOOK操作。语法简单,功能强大且稳定,它是跨平台的。[logos] (http://iphonedevwiki.net/index.php/Logos)二、lo...
    继续阅读 »

    一、概述

    Logos语法其实是CydiaSubstruct框架提供的一组宏定义。便于开发者使用宏进行HOOK操作。语法简单,功能强大且稳定,它是跨平台的。[logos] (http://iphonedevwiki.net/index.php/Logos)

    二、logos语法

    logos语法分为3类。

    2.1、Block level

    这一类型的指令会开辟一个代码块,以%end结束。

    %group

    用来将代码分组。开发中hook代码会很多,这样方便管理Logos代码。所有的group都必须初始化,否则编译报错。


    #import <UIKit/UIKit.h>

    %group group1

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式1
    return %orig;
    }

    %end

    %end


    %group group2

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式2
    return %orig;
    }

    %end

    %end

    %group group3

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式3
    return %orig;
    }

    %end

    %end

    //使用group要配合ctor
    %ctor {
    //[[UIDevice currentDevice] systemVersion].doubleValue 可以用来判断版本或其它逻辑。
    if ([[UIDevice currentDevice] systemVersion].doubleValue >= 11.0) {
    //这里group3会覆盖group1,不会执行group1逻辑。
    %init(group1)%init(group3);
    } else {
    %init(group2);
    }
    }



    • group初始化在%ctor中,需要%init初始化。
    • 所有group必须初始化,否则编译报错。
    • 在一个逻辑中同时初始化多个group,后面的会覆盖前面的。
    • 在不添加group的情况下,默认有个_ungrouped组,会自动初始化。

    Begin a hook group with the name Groupname. Groups cannot be inside another [%group](https://iphonedev.wiki/index.php/Logos#.25group "Logos") block. All ungrouped hooks are in the implicit "_ungrouped" group. The _ungrouped group is initialized for you if there are no other groups. You can use the %init directive to initialize it manually. Other groups must be initialized with the %init(Groupname) directive

    %hook

    HOOK某个类里面的某个方法。

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式1
    return %orig;
    }

    %end

    %hook后面需要跟需要hook的类名。

    %new
    为某个类添加新方法,在%hook 和 %end 中使用。

    %hook RichTextView

    %new
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    }

    %end

    %subclass

    %subclass Classname: Superclass <Protocol list>

    运行时创建子类,只能包含方法或者关联属性,不能包含属性。可以通过%c创建类实例。

    #import <UIKit/UIKit.h>

    @interface MyObject

    - (void)setSomeValue:(id)value;

    @end

    %subclass MyObject : NSObject

    - (id)init {
    self = %orig;
    [self setSomeValue:@"value"];
    return self;
    }

    %new
    - (id)someValue {
    return objc_getAssociatedObject(self, @selector(someValue));
    }

    %new
    - (void)setSomeValue:(id)value {
    objc_setAssociatedObject(self, @selector(someValue), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    %end

    %property

    %property (nonatomic|assign|retain|copy|weak|strong|getter|setter) Type name;

    subclass或者hook的类添加属性。必须在 %subclass 或%hook中。

    %property(nonatomic,assign) NSInteger age;

    %end

    与其它命令配对出现。

    2.2、Top level

    TopLevel指令不放在BlockLevel中。

    %config

    %config(Key=Value);

    logos设置标记。

    Configuration Flags

    keyvaluesnotes
    generatorMobileSubstrate生成的代码使用MobileSubstrate hook
    generatorinternal生成的代码只使用OC runtime方法hook
    warningsnone忽略所有警告
    warningsdefault没有致命的警告
    warningserror使所有警告报错
    dumpyamlYAML格式转储内部解析树

    %config(generator=internal);
    %config(warnings=error);
    %config(dump=yaml);

    %hookf

    hook函数,类似fishhook
    语法

    %hookf(rtype, symbolName, args...) { … }
    • rtype:返回值。
    • symbolName:原函数地址。
    • args...:参数。
      示例
    FILE *fopen(const char *path, const char *mode);
    %hookf(FILE *, fopen, const char *path, const char *mode) {
    NSLog(@"Hey, we're hooking fopen to deny relative paths!");
    if (path[0] != '/') {
    return NULL;
    }
    return %orig;
    }

    %ctor

    构造函数,用于确定加载那个组。和%init结合用。

    %dtor

    析构,做一些收尾工作。比如应用挂起的时候。

    2.3、Function level

    这一块的指令就放在方法中

    %init

    用来初始化某个组。

    %class

    %class Class;

    %class已经废弃了,不建议使用。

    %c

    类似getClass函数,获得一个类对象。一般用于调用类方法。

    //只是为了声明编译通过
    @interface MainViewController

    + (void)HP_classMethod;

    @end


    %hook MainViewController

    %new
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //方式一
    // [self.class HP_classMethod];
    //方式二
    // [NSClassFromString(@"MainViewController") HP_classMethod];
    //方式三
    [%c(MainViewController) HP_classMethod];
    }

    %new
    + (void)HP_classMethod {
    NSLog(@"HP_classMethod");
    }

    %end
    • %c 中没有引号。

    %orig

    保持原有的方法实现,如果原来的方法有返回值和参数,那么可以传递参数和接收返回值。

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //传递参数&接收返回值。
    BOOL result1 = %orig(arg1,arg2,arg3,arg4);
    BOOL result2 = %orig;
    return %orig;
    }

    %end

    • %orig
      可以接收返回值。
    • 可以传递参数,不传就是传递该方法的默认参数。

    %log

    能够输出日志,输出方法调用的详细信息 。

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    %log;
    return %orig;
    }

    %end

    输出:

     WeChat[11309:6708938] -[<RichTextView: 0x15c4c9720> setPrefixContent:(null) TargetContent:钱已经借给你了。 TargetParserString:<contentMD5>0399062cd62208dad884224feae2aa30</contentMD5><fontsize>20.287109</fontsize><fwidth>240.000000</fwidth><parser><type>1</type><range>{0, 8}</range><info><![CDATA[<style><range>{0, 8}</range><rect>{{0, 0}, {135, 21}}</rect></style>]]></info></parser> SuffixContent:(null)]

    能够输出详细的日志信息,包含类、方法、参数、以及控件信息等详细信息。

    总结

    • logos语法其实是CydiaSubstruct框架提供的一组宏定义。
    • 语法
      • %hook%end勾住某个类,在一个代码块中直接写需要勾住的方法。
      • %group%end用于分组。
        • 每一组都需要%ctor()函数构造。
        • 通过%init(组名称)进行初始化。
      • %log输出方法的详细信息(调用者、方法名、方法参数)
      • %orig调用原始方法。可以传递参数,接收返回值。
      • %c类似getClass函数,获取一个类对象。
      • %new添加某个方法。
    • .xm文件代表该文件支持OCC/C++语法。
    • 编译该文件时需要导入头文件以便编译通过。.xm文件不参与代码的执行,编译后生成的.mm文件参与代码的执行。


    作者:HotPotCat
    链接:https://www.jianshu.com/p/70151c602886


    收起阅读 »

    lookUpImpOrForward 消息慢速查找(下)

    3.1.2 search_method_list_inlineALWAYS_INLINE static method_t * search_method_list_inline(const method_list_t *mlist, SEL sel) { ...
    继续阅读 »

    3.1.2 search_method_list_inline

    ALWAYS_INLINE static method_t *
    search_method_list_inline(const method_list_t *mlist, SEL sel)
    {
    //methodlist是否已经修复
    int methodListIsFixedUp = mlist->isFixedUp();
    //是否有序
    int methodListHasExpectedSize = mlist->isExpectedSize();

    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
    //二分查找
    return findMethodInSortedMethodList(sel, mlist);
    } else {
    // Linear search of unsorted method list
    //无序,循环查找
    if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
    return m;
    }
    return nil;
    }
    • 首先判断有序无序。
    • 有序进入二分查找findMethodInSortedMethodList
    • 无序进入循环查找findMethodInUnsortedMethodList

    ⚠️ 那么就有个问题,排序是什么时候完成的?
    既然是method_t相关类型那就进去搜一下sort相关的关键字。发现了如下方法:

     struct SortBySELAddress :
    public std::binary_function<const struct method_t::big&,
    const struct method_t::big&, bool>
    {
    bool operator() (const struct method_t::big& lhs,
    const struct method_t::big& rhs)
    { return lhs.name < rhs.name; }
    };



    是在_read_images类加载映射的时候注册调用的。又见到了_read_images,这个方法将在后面继续研究。

    结论:类在加载实例化的时候进行的排序,是按照sel address进行排序的。

    3.1.3 findMethodInSortedMethodList 二分查找

    findMethodInSortedMethodList会根据架构最终进入各自的findMethodInSortedMethodList方法,这里以x86为例进行分析:

    ALWAYS_INLINE static method_t *
    findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
    {
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    //method list count
    uint32_t count;
    //count >>= 1 相当于除以2。加入count为8
    for (count = list->count; count != 0; count >>= 1) {//7 >> 1 = 3,前一个已经比较了4,这里就完美的避开了4。
    //base是为了配合少查找
    //probe中间元素,第一次 probe = 0 + 8 >> 1 = 4
    probe = base + (count >> 1);
    //sel
    uintptr_t probeValue = (uintptr_t)getName(probe);
    //与要查找的sel是否匹配
    if (keyValue == probeValue) {
    // `probe` is a match.
    // Rewind looking for the *first* occurrence of this value.
    // This is required for correct category overrides.
    //查找分类同名sel。如果匹配了就找分类中的。因为分类是在前面的,所以一直找到最开始的位置。
    while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
    probe--;
    }
    //匹配则返回。
    return &*probe;
    }
    //没有匹配
    if (keyValue > probeValue) {//大于的情况下,在后半部分
    //没有匹配的情况下,如果sel在后半部分,这里base指向了上次查找的后面一个位置。
    base = probe + 1;//5
    //count对应减1
    count--;//7 -- 操作为了少做比较,因为已经比较过了
    }
    //在前半部分不进行额外操作。
    }

    return nil;
    }
    • 首先是一个循环比较,条件是count >>= 1,这里是对count进行减半,相当于二分。
    • base是为了少做比较,相当于是一个基线,当要继续往后查找的时候base为当前查找元素的下一个元素。
    • 当要继续往后查找的时候count进行了--操作,这一步是为了count >>= 1不包含已经比较过的范围。
    • 找到值的时候会循环继续往前查找,因为存在分类与类中方法同名的情况(分类方法放在类中同名方法前面),一直找到不同名为止。

    ⚠️根据源码可以得到以下结论:
    1.分类方法调用先找类中方法,再逐次找到分类方法,直到找到第一个。
    2.因为判断条件是当前命中元素与前一个元素比较,sel相同的情况下才继续查找,那就说明分类的方法是插入类中方法列表中的,都在对应类中方法的前面。

    • 查找完毕后会返回lookUpImpOrForward

    这里以有8个方法为类来分析查找流程,过程如下:

    比较开始值:count = 8 base = 0 probe = 4
    - 第一次:比较 probe = 4
    - keyValue > probeValue count = 3(先--,再>>1) base = 5 probe = 6
    第二次: 比较 probe = 6
    - keyValue > probeValue count = 1(先--,再>>1) base = 7 probe = 7
    第三次:比较 probe = 7
    - keyValue > probeValue count = 0(先--,再>>1) base = 8 probe = 8count == 0
    - keyValue < probeValue count = 0>>1) base = 7 probe = 7count == 0
    - keyValue < probeValue count = 1>>1) base = 5 probe = 5
    第三次:比较 probe = 5
    - keyValue > probeValue count = 0(先--,再>>1) base = 6 probe = 6count == 0
    - keyValue < probeValue count = 0>>1) base = 5 probe = 5count == 0
    - keyValue < probeValue count = 4>>1) base = 0 probe = 2
    第二次:比较 probe = 2
    - keyValue > probeValue count = 1(先--,再>>1) base = 3 probe = 3
    第三次:比较 probe = 3
    - keyValue > probeValue count = 0(先--,再>>1) base = 4 --count == 0
    - keyValue < probeValue count = 0>>1) base = 3 --count == 0
    - keyValue < probeValue count = 2>>1) base = 1 probe = 1
    第三次:比较 probe = 1
    - keyValue > probeValue count = 0(先--,再>>1) base = 0 --count == 0
    - keyValue < probeValue count = 1>>1) base = 0 probe = 0
    第四次:比较 probe = 0
    - keyValue > probeValue count = 0(先--,再>>1) base = 1 --count == 0
    - keyValue < probeValue count = 0>>1) base = 0 --count == 0

    代码模拟:

    int testFindSortedMethods(int methodCount,int findKey) {
    int base = 0;
    int probe = 0;
    int round = 0;
    printf("查找key:%d\n",findKey);
    for (int count = methodCount; count != 0; count >>= 1) {
    round++;
    probe = base + (count >> 1);
    printf("\t第%d轮 scan count :%d, base:%d,probe:%d\n",round,count,base,probe);
    if (findKey == probe) {
    printf("\tfound prode:%d\n",probe);
    return probe;
    }
    if (findKey > probe) {
    base = probe + 1;
    count--;
    }
    }
    printf("\tnot found -1\n");
    return -1;
    }

    调用:

    testFindSortedMethods(8, 0);
    testFindSortedMethods(8, 1);
    testFindSortedMethods(8, 2);
    testFindSortedMethods(8, 3);
    testFindSortedMethods(8, 4);
    testFindSortedMethods(8, 5);
    testFindSortedMethods(8, 6);
    testFindSortedMethods(8, 7);
    testFindSortedMethods(8, 8);
    testFindSortedMethods(8, 9);

    输出:

    查找key:0
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :2, base:0,probe:1
    第4轮 scan count :1, base:0,probe:0
    found prode:0
    查找key:1
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :2, base:0,probe:1
    found prode:1
    查找key:2
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    found prode:2
    查找key:3
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :1, base:3,probe:3
    found prode:3
    查找key:4
    第1轮 scan count :8, base:0,probe:4
    found prode:4
    查找key:5
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:5,probe:5
    found prode:5
    查找key:6
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    found prode:6
    查找key:7
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    found prode:7
    查找key:8
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    not found -1
    查找key:9
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    not found -1

    可以看到输出与验证的结论一致。

    流程图:




    四、案例分析慢速查找流程

    定义一个HPObject以及它的子类HPSubObject
    HPObject定义和实现如下:

    @interface HPObject : NSObject

    - (void)instanceMethod1;

    - (void)instanceMethod2;

    + (void)classMethod;

    @end

    @implementation HPObject

    - (void)instanceMethod1 {
    NSLog(@"%s",__func__);
    }

    + (void)classMethod {
    NSLog(@"%s",__func__);
    }

    @end

    HPSubObject定义和实现如下:

    @interface HPSubObject : HPObject

    - (void)subInstanceMethod;

    @end

    @implementation HPSubObject

    - (void)subInstanceMethod {
    NSLog(@"%s",__func__);
    }

    @end

    根据前面分析的方法查找逻辑测试代码:

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
    HPSubObject *subObject = [HPSubObject new];
    //对象方法 根据慢速查找分析是能找到的
    [subObject subInstanceMethod];
    [subObject instanceMethod1];
    [subObject instanceMethod2];
    #pragma clang diagnostic pop

    输出:

    -[HPSubObject subInstanceMethod]
    -[HPObject instanceMethod1]
    -[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0
    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0'
  • subInstanceMethodinstanceMethod1符合预期。
  • instanceMethod2找不到报错unrecognized selector sent to instance,为什么报这个错误呢?
    查看调用堆栈如下:



    • 那么去源码中搜索错误信息找到以下内容:
    // Replaced by CF (throws an NSException)
    + (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
    class_getName(self), sel_getName(sel), self);
    }

    // Replaced by CF (throws an NSException)
    - (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
    object_getClassName(self), sel_getName(sel), self);
    }

    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
    "(no message forward handler is installed)",
    class_isMetaClass(object_getClass(self)) ? '+' : '-',
    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    那么调用的是哪个呢?断点后并没有进入。根据上面的分析imp找不到的时候会有两个选项resolveMethod_locked或者_objc_msgForward_impcache
    _objc_msgForward_impcache的汇编实现如下:

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    内部直接调用了__objc_msgForward

    ENTRY __objc_msgForward

    adrp x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgForward

    可以看到__objc_msgForward的实现是调用__objc_forward_handler,也就是:

    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
    "(no message forward handler is installed)",
    class_isMetaClass(object_getClass(self)) ? '+' : '-',
    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    这也就是报错信息的原因,里面进行了格式化的错误信息打印。

    接着增加一个NSObject的分类:

    @interface NSObject (Additions)

    - (void)categoryInstanceMethod;

    @end

    @implementation NSObject (Additions)

    - (void)categoryInstanceMethod {
    NSLog(@"%s",__func__);
    }

    @end
    调用:

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
    [HPSubObject classMethod];
    [HPSubObject performSelector:@selector(categoryInstanceMethod)];
    #pragma clang diagnostic pop
    输出:

    +[HPObject classMethod]
    -[NSObject(Additions) categoryInstanceMethod]
    • classMethod类方法能找到符合预期。
    • 为什么HPSubObject能调用categoryInstanceMethod实例方法?
      这就涉及到了类的继承链,NSObject元类的父类是NSObject类,所以能找到。

    再次说明OC的底层没有实例和类方法的区分,类方法和实例方法是人为加上去的。我们只是为了配合OC的演出视而不见。

    五、 总结

    慢速查找流程:

    • checkIsKnownClass检查注册类。
    • realizeAndInitializeIfNeeded_locked初始化类,为方法查找做好准备。
    • 递归查找imp,会涉及到动态缓存库的二次确认以及父类的快速慢速查找。
      • 查找过程会进行二分查找/递归查找。
      • 是否二分要看方法列表是否已经排序,排序操作是在类加载实例化的时候完成的。
      • 二分查找算法很经典,充分利用>>1以及--不多浪费一次机会。
    • 找到imp直接跳转返回。根据LOOKUP_NOCACHE判断是否插入缓存。
    • 没有找到则判断是否进行动态方法决议。
    • 不进行动态方法决议则判断是否要forward


    作者:HotPotCat
    链接:https://www.jianshu.com/p/db43c28e0e11


    收起阅读 »

    lookUpImpOrForward 消息慢速查找(上)

    上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。一、汇编中找不到缓存在汇编代码中只有_lookUpImpOrForward的调用而没有实现...
    继续阅读 »

    上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。

    一、汇编中找不到缓存

    在汇编代码中只有_lookUpImpOrForward的调用而没有实现,代码中直接搜这个也是搜不到的。因为实现在c/c++代码中,需要搜索lookUpImpOrForward。声明如下:

    extern IMP lookUpImpOrForward(id obj, SEL, Class cls, int behavior);

    那么参数肯定也就是汇编中传过来的,汇编中调用如下:

    .macro MethodTableLookup

    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    //x2 = cls
    mov x2, x16
    //x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver
    //_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    mov x3, #3
    bl _lookUpImpOrForward

    // IMP in x0
    mov x17, x0

    RESTORE_REGS MSGSEND

    .endmacro

    • 3个参数没有什么好说的,behaviorLOOKUP_INITIALIZE | LOOKUP_RESOLVER。那就证明lookUpImpOrForward是有查找模式的。
    • 调用完_lookUpImpOrForward后有mov x17, x0说明是有返回值的,与c/c++lookUpImpOrForward的声明对应上了。

    那么就有一个问题了,为什么cache查找要使用汇编?
    1.汇编更接近机器语言,执行速度快。为了快速找到方法,优化方法查找时间。
    2.消息发送参数是未知参数(比如可变参数),c参数必须明确,汇编相对能够更加动态化。
    3.更安全。


    二、 慢速查找流程

    慢速查找就是不断遍历methodlist的过程,遍历是一个耗时的过程,所以是使用c/c++来实现的。

    2.1 lookUpImpOrForward

    首先明确慢速查找流程的目标是找到sel对应的imp。所以核心就是lookUpImpOrForward中返回imp的逻辑,精简后源码如下:


    NEVER_INLINE
    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
    //forward_imp赋值
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    //要返回的imp
    IMP imp = nil;
    //当前查找的cls
    Class curClass;

    //初始化的一些处理,如果类没有初始化behavior会增加 LOOKUP_NOCACHE,判断是否初始化取的是data()->flags的第29位。
    if (slowpath(!cls->isInitialized())) {
    behavior |= LOOKUP_NOCACHE;
    }

    //类是否已经注册,注册后会加入allocatedClasses表中
    checkIsKnownClass(cls);
    //初始化需要的类。由于要去类中查找方法,如果rw,ro没有准备好那就没有办法查了。也就是为后面的查找代码做好准备。LOOKUP_INITIALIZE用在了这里
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    //赋值要查找的类
    curClass = cls;
    //死循环,除非return/break
    for (unsigned attempts = unreasonableClassCount();;) {//……}

    //参数LOOKUP_RESOLVER用在了这里,动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    return resolveMethod_locked(inst, sel, cls, behavior);
    }

    done:
    //没有初始化LOOKUP_NOCACHE就有值了,也就是查完后不要插入缓存。在这个流程中是插入
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    #if CONFIG_USE_PREOPT_CACHES
    //共享缓存
    while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
    cls = cls->cache.preoptFallbackClass();
    }
    #endif
    //填充缓存,这里填充的是`cls`。也就是父类如果有缓存也会被加进子类。
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
    done_unlock:
    runtimeLock.unlock();
    //forward_imp 并且有 LOOKUP_NIL 的时候直接返回nil。也就是不进行forward_imp
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
    return nil;
    }
    return imp;
    }
    • 先给forward_imp赋值_objc_msgForward_impcache,这个函数的实现是在汇编中。
    • impcurClass定义。
    • cls->isInitialized()类没有初始化则behavior增加LOOKUP_NOCACHE,类有没有初始化时由data()->flags的第29位决定的。
        bool isInitialized() {
    //#define uint32_t RW_INITIALIZED (1<<29)
    return getMeta()->data()->flags & RW_INITIALIZED;
    }

    • checkIsKnownClass判断类是否已经注册,注册后会加入allocatedClasses表中。
    • realizeAndInitializeIfNeeded_locked初始化需要的类,由于要去类中查找方法,如果rw ro没有准备好那就没有办法查了(methods就存在其中)。也就是为后面的查找代码做好准备。汇编中调用的时候传递的behaviorLOOKUP_INITIALIZE用在了这里。它的流程会在后面介绍。
    • 进入for死循环查找imp,核心肯定就是找imp赋值的地方了。那么就只有breakreturngoto才能停止循环,否则一直查找。
    • 如果上面imp没有找到,LOOKUP_RESOLVER是有值的,会进入动态方法决议。
    • 如果找到imp会跳转到done,判断是否需要插入缓存会调用log_and_fill_cache最终调用到cache.insert。父类如果有缓存找到也会加入到子类,这里是因为写入的时候参数是cls
    • 根据LOOKUP_NIL判断是否需要forward,不需要直接返回nil,需要返回imp

    2.1.1 behavior 说明

    在从汇编调入lookUpImpOrForward的时候传入的behavior参数是LOOKUP_INITIALIZELOOKUP_RESOLVER
    behavior类型如下:

    /* method lookup */
    enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
    };

    根据上面的分析可以得到大致结论:

    • LOOKUP_INITIALIZE: 控制是否去进行类的初始化。有值初始化,没有不初始化。
    • LOOKUP_RESOLVER:是否进行动态方法决议。有值决议,没有值不决议。
    • LOOKUP_NIL:是否进行forward。有值不进行,没有值进行。
    • LOOKUP_NOCACHE:是否插入缓存。有值不插入缓存,没有值插入。

    2.2 realizeAndInitializeIfNeeded_locked

    在这里主要进行类的实例化和初始化,有两个分支:RealizeInitialize

    2.2.1 Realize

    (这个分支一般在_read_images的时候就处理好了)
    在进行类的实例化的时候调用流程是这样的realizeAndInitializeIfNeeded_locked->realizeClassMaybeSwiftAndLeaveLocked->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift,最终会调用realizeClassWithoutSwiftswift会调用realizeSwiftClass。这个不是这篇文章的重点,分析下主要代码如下:


    static Class realizeClassWithoutSwift(Class cls, Class previously)
    {
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
    // This was a future class. rw data is already allocated.
    rw = cls->data();
    ro = cls->data()->ro();
    ASSERT(!isMeta);
    cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
    // Normal class. Allocate writeable class data.
    rw = objc::zalloc<class_rw_t>();
    rw->set_ro(ro);
    rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
    cls->setData(rw);
    }
    //赋类和元类的操作
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

    //关联类
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);
    return cls;
    }
    • 对类的ro以及rw进行处理。
    • 循环调用了父类和元类的realizeClassWithoutSwift
    • 关联了父类和元类。

    当对象调用方法的时候判断类是否初始化,如果初始化了再判断类的父类以及元类,相当于是递归操作了,一直到NSObject->nil为止。也就是说只要有一个类进行初始化它的上层(也就是父类和元类)都会进行初始化,是一个连锁反应。

    ⚠️为什么这么操作?
    就是为了查找方法。类没有实例方法的话会找父类,类没有类方法会找元类,所以需要这么操作。

    2.2.2 Initialized

    realizeAndInitializeIfNeeded_locked->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass。在initializeNonMetaClass中调用了callInitialize(cls)


    void callInitialize(Class cls)
    {
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
    }


    系统直接objc_msgSend发送了initialize消息。所以initialize是在类第一个方法被调用的时候进行调用的。也就是发送第一个消息的时候:
    消息慢速查找开始前进行类初始化的时候发送的initialize消息

    三、循环查找

    对于慢速查找流程,我们想到的就是先查自己然后再查父类一直找到NSObject->nil
    慢速查找流程应该是这样:
    1.查自己methodlist->(sel,imp)。
    2.查父类->NSObject->nil ->跳出来

    查看源码:

    //死循环,除非return/break
    for (unsigned attempts = unreasonableClassCount();;) {
    //先去共享缓存查找,防止这个时候共享缓存中已经写入了该方法。
    if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    #if CONFIG_USE_PREOPT_CACHES
    //这里也是调用到了`_cache_getImp`汇编代码,最终调用了`CacheLookup`查找共享缓存。
    imp = cache_getImp(curClass, sel);
    //找到后直接跳转done_unlock
    if (imp) goto done_unlock;
    curClass = curClass->cache.preoptFallbackClass();
    #endif
    } else {
    // curClass method list.进行循环查找
    Method meth = getMethodNoSuper_nolock(curClass, sel);
    //找到method
    if (meth) {
    //返回imp
    imp = meth->imp(false);
    //跳转done
    goto done;
    }
    //这里curClass 会赋值,直到找到 NSObject->nil就会返回forward_imp
    if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    imp = forward_imp;
    break;
    }
    }

    // Superclass cache.
    imp = cache_getImp(curClass, sel);
    if (slowpath(imp == forward_imp)) {

    break;
    }
    if (fastpath(imp)) {
    goto done;
    }
    }
    • 可以看到这是一个死循环。
    • 如果有共享缓存,先查找共享缓存,因为前面做了很多准备工作,防止这个时候共享缓存中已经写入了该方法(在汇编中已经查过一次了)。
    • 否则就进行二分查找流程,核心逻辑是在getMethodNoSuper_nolock中调用的,查找完成返回。
    • 如果找到method则获取imp跳转done,如果没有找到将父类赋值给curClass,父类不存在则imp = forward_imp;
      • 找到则进入找到imp done的逻辑。
        • log_and_fill_cache插入缓存,也就是调用cls->cache.insert与分析cache的时候逻辑对上了。
        • 返回imp
      • 没有找到则curClass赋值superclass,没有superclass也就是找到了NSObject->nil的情况下imp = forward_imp
      • 没有找到并且有父类的情况下通过cache_getImp去父类的cache中查找。这里与共享缓存的cache_getImp是一个逻辑,最终都是调用汇编_cache_getImp->CacheLookup

      父类也有快速和慢速查找。

    • 如果父类中也没有找到,则进入递归。直到imp找到或者变为forward_imp才结束循环。

    _cache_getImp 说明
    源码:

        STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0, 0
    CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

    LGetImpMissDynamic:
    mov p0, #0
    ret

    LGetImpMissConstant:
    mov p0, p2
    ret

    END_ENTRY _cache_getImp

    最终也是调用CacheLookup进行缓存查找。但是第三个参数是LGetImpMissDynamic实现是mov p0, #0 ret也就是找不到就返回了。不会去走__objc_msgSend_uncached逻辑。

    ⚠️ 找到父类缓存会插入自己的缓存

    3.1 二分查找流程

    3.1.1 getMethodNoSuper_nolock

    首先进入的是getMethodNoSuper_nolock,实现如下:


    static method_t *
    getMethodNoSuper_nolock(Class cls, SEL sel)
    {
    //获取methods
    auto const methods = cls->data()->methods();
    //循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
    for (auto mlists = methods.beginLists(),
    end = methods.endLists();
    mlists != end;
    ++mlists)
    {
    method_t *m = search_method_list_inline(*mlists, sel);
    if (m) return m;
    }
    return nil;
    }



    • 这里只是普通的循环,因为methods获取的数据类型是method_array_t,它存储的是method_list_t。这里的数据结构有可能是二维数据,因为动态加载方法和类导致。
    • 核心逻辑是调用search_method_list_inline实现的


    收起阅读 »

    为数不多的人知道的 Kotlin 技巧以及 原理解析

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁...
    继续阅读 »

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。

    结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是呢简洁的背后是有代价的,使用不当对性能可能会有损耗,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,关于 Kotlin 性能损失那些事,可以看一下我另外两篇文章。

    这两篇文章都分析了 Kotlin 使用不当对性能的影响,不仅如此 Kotlin 当中还有很多让人傻傻分不清楚的语法糖例如 run, with, let, also, apply 等等,这篇文章将介绍一种简单的方法来区分它们以及如何选择使用。

    通过这篇文章你将学习到以下内容,文中会给出相应的答案

    • 如何使用 plus 操作符对集合进行操作?
    • 当获取 Map 值为空时,如何设置默认值?
    • require 或者 check 函数做什么用的?
    • 如何区分 run, with, let, also and apply 以及如何使用?
    • 如何巧妙的使用 in 和 when 关键字?
    • Kotlin 的单例有几种形式?
    • 为什么 by lazy 声明的变量只能用 val?

    plus 操作符

    在 Java 中算术运算符只能用于基本数据类型,+ 运算符可以与 String 值一起使用,但是不能在集合中使用,在 Kotlin 中可以应用在任何类型,我们来看一个例子,利用 plus (+) 和 minus (-) 对 Map 集合做运算,如下所示。

    fun main() {
    val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)

    // plus (+)
    println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
    println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
    println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}

    // minus (-)
    println(numbersMap - "one") // {two=2, three=3}
    println(numbersMap - listOf("two", "four")) // {one=1, three=3}
    }

    其实这里用到了运算符重载,Kotlin 在 Maps.kt 文件里面,定义了一系列用关键字 operator 声明的 Map 的扩展函数。

    用 operator 关键字声明 plus 函数,可以直接使用 + 号来做运算,使用 operator 修饰符声明 minus 函数,可以直接使用 - 号来做运算,其实我们也可以在自定义类里面实现 plus (+) 和 minus (-) 做运算。


    data class Salary(var base: Int = 100){
    override fun toString(): String = base.toString()
    }

    operator fun Salary.plus(other: Salary): Salary = Salary(base + other.base)
    operator fun Salary.minus(other: Salary): Salary = Salary(base - other.base)

    val s1 = Salary(10)
    val s2 = Salary(20)
    println(s1 + s2) // 30
    println(s1 - s2) // -10

    Map 集合的默认值

    在 Map 集合中,可以使用 withDefault 设置一个默认值,当键不在 Map 集合中,通过 getValue 返回默认值。

    val map = mapOf(
    "java" to 1,
    "kotlin" to 2,
    "python" to 3
    ).withDefault { "?" }

    println(map.getValue("java")) // 1
    println(map.getValue("kotlin")) // 2
    println(map.getValue("c++")) // ?

    源码实现也非常简单,当返回值为 null 时,返回设置的默认值。

    internal inline fun <K, V> Map<K, V>.getOrElseNullable(key: K, defaultValue: () -> V): V {
    val value = get(key)
    if (value == null && !containsKey(key)) {
    return defaultValue()
    } else {
    @Suppress("UNCHECKED_CAST")
    return value as V
    }
    }

    但是这种写法和 plus 操作符在一起用,有一个 bug ,看一下下面这个例子。

    val newMap = map + mapOf("python" to 3)
    println(newMap.getValue("c++")) // 调用 getValue 时抛出异常,异常信息:Key c++ is missing in the map.

    这段代码的意思就是,通过 plus(+) 操作符合并两个 map,返回一个新的 map, 但是忽略了默认值,所以看到上面的错误信息,我们在开发的时候需要注意这点。

    使用 require 或者 check 函数作为条件检查

    // 传统的做法
    val age = -1;
    if (age <= 0) {
    throw IllegalArgumentException("age must not be negative")
    }

    // 使用 require 去检查
    require(age > 0) { "age must be negative" }

    // 使用 checkNotNull 检查
    val name: String? = null
    checkNotNull(name){
    "name must not be null"
    }

    那么我们如何在项目中使用呢,具体的用法可以查看我 GitHub 上的项目 DataBindingDialog.kt 当中的用法。

    如何区分和使用 run, with, let, also, apply

    感谢大神 Elye 的这篇文章提供的思路 Mastering Kotlin standard functions

    run, with, let, also, apply 都是作用域函数,这些作用域函数如何使用,以及如何区分呢,我们将从以下三个方面来区分它们。

    • 是否是扩展函数。
    • 作用域函数的参数(this、it)。
    • 作用域函数的返回值(调用本身、其他类型即最后一行)。

    是否是扩展函数

    首先我们来看一下 with 和 T.run,这两个函数非常的相似,他们的区别在于 with 是个普通函数,T.run 是个扩展函数,来看一下下面的例子。

    val name: String? = null
    with(name){
    val subName = name!!.substring(1,2)
    }

    // 使用之前可以检查它的可空性
    name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")

    在这个例子当中,name?.run 会更好一些,因为在使用之前可以检查它的可空性。

    作用域函数的参数(this、it)

    我们在来看一下 T.run 和 T.let,它们都是扩展函数,但是他们的参数不一样 T.run 的参数是 this, T.let 的参数是 it。

    val name: String? = "hi-dhl.com"

    // 参数是 this,可以省略不写
    name?.run {
    println("The length is ${this.length} this 是可以省略的 ${length}")
    }

    // 参数 it
    name?.let {
    println("The length is ${it.length}")
    }

    // 自定义参数名字
    name?.let { str ->
    println("The length is ${str.length}")
    }

    在上面的例子中看似 T.run 会更好,因为 this 可以省略,调用更加的简洁,但是 T.let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let。

    作用域函数的返回值(调用本身、其他类型)

    接下里我们来看一下 T.let 和 T.also 它们接受的参数都是 it, 但是它们的返回值是不同的 T.let 返回最后一行,T.also 返回调用本身。


    var name = "hi-dhl"

    // 返回调用本身
    name = name.also {
    val result = 1 * 1
    "juejin"
    }
    println("name = ${name}") // name = hi-dhl

    // 返回的最后一行
    name = name.let {
    val result = 1 * 1
    "hi-dhl.com"
    }
    println("name = ${name}") // name = hi-dhl.com

    从上面的例子来看 T.also 似乎没有什么意义,细想一下其实是非常有意义的,在使用之前可以进行自我操作,结合其他的函数,功能会更强大。

    fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

    当然 T.also 还可以做其他事情,比如利用 T.also 在使用之前可以进行自我操作特点,可以实现一行代码交换两个变量,在后面会有详细介绍

    T.apply 函数

    通过上面三个方面,大致了解函数的行为,接下来看一下 T.apply 函数,T.apply 函数是一个扩展函数,返回值是它本身,并且接受的参数是 this。

    // 普通方法
    fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
    }
    // 改进方法
    fun createInstance(args: Bundle)
    = MyFragment().apply { arguments = args }


    // 普通方法
    fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
    }
    // 改进方法,链式调用
    fun createIntent(intentData: String, intentAction: String) =
    Intent().apply { action = intentAction }
    .apply { data = Uri.parse(intentData) }

    汇总

    以表格的形式汇总,更方便去理解

    函数是否是扩展函数函数参数(this、it)返回值(调用本身、最后一行)
    with不是this最后一行
    T.runthis最后一行
    T.letit最后一行
    T.alsoit调用本身
    T.applythis调用本身

    使用 T.also 函数交换两个变量

    接下来演示的是使用 T.also 函数,实现一行代码交换两个变量?我们先来回顾一下 Java 的做法。

    int a = 1;
    int b = 2;

    // Java - 中间变量
    int temp = a;
    a = b;
    b = temp;
    System.out.println("a = "+a +" b = "+b); // a = 2 b = 1

    // Java - 加减运算
    a = a + b;
    b = a - b;
    a = a - b;
    System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

    // Java - 位运算
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

    // Kotlin
    a = b.also { b = a }
    println("a = ${a} b = ${b}") // a = 2 b = 1

    来一起分析 T.also 是如何做到的,其实这里用到了 T.also 函数的两个特点。

    • 调用 T.also 函数返回的是调用者本身。
    • 在使用之前可以进行自我操作。

    也就是说 b.also { b = a } 会先将 a 的值 (1) 赋值给 b,此时 b 的值为 1,然后将 b 原始的值(2)赋值给 a,此时 a 的值为 2,实现交换两个变量的目的。

    in 和 when 关键字

    使用 in 和 when 关键字结合正则表达式,验证用户的输入,这是一个很酷的技巧。

    // 使用扩展函数重写 contains 操作符
    operator fun Regex.contains(text: CharSequence) : Boolean {
    return this.containsMatchIn(text)
    }

    // 结合着 in 和 when 一起使用
    when (input) {
    in Regex("[0–9]") -> println("contains a number")
    in Regex("[a-zA-Z]") -> println("contains a letter")
    }

    in 关键字其实是 contains 操作符的简写,它不是一个接口,也不是一个类型,仅仅是一个操作符,也就是说任意一个类只要重写了 contains 操作符,都可以使用 in 关键字,如果我们想要在自定义类型中检查一个值是否在列表中,只需要重写 contains() 方法即可,Collections 集合也重写了 contains 操作符。

    val input = "kotlin"

    when (input) {
    in listOf("java", "kotlin") -> println("found ${input}")
    in setOf("python", "c++") -> println("found ${input}")
    else -> println(" not found ${input}")
    }

    Kotlin 的单例三种写法

    我汇总了一下目前 Kotlin 单例总共有三种写法:

    • 使用 Object 实现单例。
    • 使用 by lazy 实现单例。
    • 可接受参数的单例(来自大神 Christophe Beyls)。

    使用 Object 实现单例

    代码:

    object WorkSingleton

    Kotlin 当中 Object 关键字就是一个单例,比 Java 的一坨代码看起来舒服了很多,来看一下编译后的 Java 文件。

    public final class WorkSingleton {
    public static final WorkSingleton INSTANCE;

    static {
    WorkSingleton var0 = new WorkSingleton();
    INSTANCE = var0;
    }
    }

    通过 static 代码块实现的单例,优点:饿汉式且是线程安全的,缺点:类加载时就初始化,浪费内存。

    使用 by lazy 实现单例

    利用伴生对象 和 by lazy 也可以实现单例,代码如下所示。

    class WorkSingleton private constructor() {

    companion object {

    // 方式一
    val INSTANCE1 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WorkSingleton() }

    // 方式二 默认就是 LazyThreadSafetyMode.SYNCHRONIZED,可以省略不写,如下所示
    val INSTANCE2 by lazy { WorkSingleton() }
    }
    }

    lazy 的延迟模式有三种:

    • 上面代码所示 mode = LazyThreadSafetyMode.SYNCHRONIZED,lazy 默认的模式,可以省掉,这个模式的意思是:如果有多个线程访问,只有一条线程可以去初始化 lazy 对象。

    • 当 mode = LazyThreadSafetyMode.PUBLICATION 表达的意思是:对于还没有被初始化的 lazy 对象,可以被不同的线程调用,如果 lazy 对象初始化完成,其他的线程使用的是初始化完成的值。

    • mode = LazyThreadSafetyMode.NONE 表达的意思是:只能在单线程下使用,不能在多线程下使用,不会有锁的限制,也就是说它不会有任何线程安全的保证以及相关的开销。

    通过上面三种模式,这就可以理解为什么 by lazy 声明的变量只能用 val,因为初始化完成之后它的值是不会变的。

    可接受参数的单例

    但是有的时候,希望在单例实例化的时候传递参数,例如:

    Singleton.getInstance(context).doSome()

    上面这两种形式都不能满足,来看看大神 Christophe Beyls 在这篇文章给出的方法 Kotlin singletons with argument 代码如下。

    class WorkSingleton private constructor(context: Context) {
    init {
    // Init using context argument
    }

    companion object : SingletonHolder<WorkSingleton, Context>(::WorkSingleton)
    }


    open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile
    private var instance: T? = null

    fun getInstance(arg: A): T {
    val i = instance
    if (i != null) {
    return i
    }

    return synchronized(this) {
    val i2 = instance
    if (i2 != null) {
    i2
    } else {
    val created = creator!!(arg)
    instance = created
    creator = null
    created
    }
    }
    }
    }

    有没有感觉这和 Java 中双重校验锁的机制很像,在 SingletonHolder 类中如果已经初始化了直接返回,如果没有初始化进入 synchronized 代码块创建对象,利用了 Kotlin 伴生对象提供的非常强大功能,它能够像其他任何对象一样从基类继承,从而实现了与静态继承相当的功能。 所以我们将 SingletonHolder 作为单例类伴随对象的基类,在单例类上重用并公开 getInstance()函数。

    参数传递给 SingletonHolder 构造函数的 creator,creator 是一个 lambda 表达式,将 WorkSingleton 传递给 SingletonHolder 类构造函数。

    并且不限制传入参数的类型,凡是需要传递参数的单例模式,只需将单例类的伴随对象继承于 SingletonHolder,然后传入当前的单例类和参数类型即可,例如:

    class FileSingleton private constructor(path: String) {

    companion object : SingletonHolder<FileSingleton, String>(::FileSingleton)

    }
    收起阅读 »

    解析android匿名共享内存几个关键函数

    基础知识当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和...
    继续阅读 »

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }


    内存几个关键函数

    基础原理

    android系统在应用程序框架层中提供了两个C++类MemoryHeapBase和MemoryBase来创建和管理匿名共享内存。 如果一个进程需要与其他进程共享一块完整的匿名共享内存,那么就可以通过使用MemoryHeapBase类类创建这块匿名共享内存。如果一个进程创建一块匿名共享内存后,只希望与其他进程共享其中的一部分,那么就可以通过MemoryBase类来创建这块匿名共享内存。

    IMemory.h:定义内存相关类的接口,表示堆内存的类IMemoryHeap和BnMemoryHeap,表示一般内存的类IMemory和BnMemory。 MemoryHeapBase.h:定义类MemoryHeapBase,继承并实现BnMemoryHeap MemoryBase.h:定义类MemoryBase,继承并实现BnMemory。

    android系统在应用程序框架层中提供了java类MemoryFile来创建和管理匿名共享内存。使用java类MemoryFile创建的匿名共享内存可以在不同的Android应用程序之间进行共享。

    java代码解析

    匿名共享内存java类MemoryFile在系统中的source\frameworks\base\core\java\android\os\MemoryFile.java文件中实现。

    //匿名共享内存的构造函数,参数1表示创建匿名共享内存的名称,参数2表示创建匿名共享内存大小
    public MemoryFile(String name int length) throws IOException {
    mLength = length;
    if (length >= 0) {
    //通过调用jni的接口去打开匿名共享内存
    mFD = native_open(name length);
    } else {
    throw new IOException("Invalid length: " + length);
    }

    if (length > 0) {
    //进行映射
    mAddress = native_mmap(mFD length PROT_READ | PROT_WRITE);
    } else {
    mAddress = 0;
    }
    }

    C++关键函数解析

    //MemoryHeapBase构造函数的实现
    MemoryHeapBase::MemoryHeapBase(const char* device size_t size uint32_t flags)
    : mFD(-1) mSize(0) mBase(MAP_FAILED) mFlags(flags)
    mDevice(0) mNeedUnmap(false) mOffset(0)
    {
    int open_flags = O_RDWR;
    if (flags & NO_CACHING)
    open_flags |= O_SYNC;
    //通过调用open打开匿名共享内存设备文件
    int fd = open(device open_flags);
    ALOGE_IF(fd<0 "error opening %s: %s" device strerror(errno));
    if (fd >= 0) {
    //指定的匿名共享内存大小按页对齐
    const size_t pagesize = getpagesize();
    size = ((size + pagesize-1) & ~(pagesize-1));
    //匿名共享内存映射到当前进程地址空间
    if (mapfd(fd size) == NO_ERROR) {
    mDevice = device;
    }
    }
    }
    //MemoryHeapBase构造函数
    MemoryHeapBase::MemoryHeapBase(size_t size uint32_t flags char const * name)
    : mFD(-1) mSize(0) mBase(MAP_FAILED) mFlags(flags)
    mDevice(0) mNeedUnmap(false) mOffset(0)
    {
    //获得系统中页大小的内存
    const size_t pagesize = getpagesize();
    //内存页对齐
    size = ((size + pagesize-1) & ~(pagesize-1));
    //创建一块匿名共享内存
    int fd = ashmem_create_region(name == NULL ? "MemoryHeapBase" : name size);
    ALOGE_IF(fd<0 "error creating ashmem region: %s" strerror(errno));
    if (fd >= 0) {
    //创建的匿名共享内存映射到当前进程地址空间中
    if (mapfd(fd size) == NO_ERROR) {
    if (flags & READ_ONLY) {//如果地址映射成功,修改匿名共享内存的访问属性
    ashmem_set_prot_region(fd PROT_READ);
    }
    }
    }
    }

    初探android系统中input的java层实现

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }

    初探android系统中input的java层实现

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }


    收起阅读 »

    iOS 隐式动画 二

    图层行为现在来做个实验,试着直接对UIView关联的图层做动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了colorLayer,并且直接设置layerView关联图层的背景色。清单7.4 直接设置图层的属性@interface View...
    继续阅读 »

    图层行为

    现在来做个实验,试着直接对UIView关联的图层做动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了colorLayer,并且直接设置layerView关联图层的背景色。

    清单7.4 直接设置图层的属性

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //set the color of our layerView backing layer directly
    self.layerView.layer.backgroundColor = [UIColor blueColor].CGColor;
    }

    - (IBAction)changeColor
    {
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.layerView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
    }

    运行程序,你会发现当按下按钮,图层颜色瞬间切换到新的值,而不是之前平滑过渡的动画。发生了什么呢?隐式动画好像被UIView关联图层给禁用了。

    试想一下,如果UIView的属性都有动画特性的话,那么无论在什么时候修改它,我们都应该能注意到的。所以,如果说UIKit建立在Core Animation(默认对所有东西都做动画)之上,那么隐式动画是如何被UIKit禁用掉呢?

    我们知道Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把它关联的图层的这个特性关闭了。为了更好说明这一点,我们需要知道隐式动画是如何实现的。

    我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:

    • 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
    • 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
    • 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
    • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。

    所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。

    于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。我们可以用一个demo做个简单的实验(清单7.5)

    清单7.5 测试UIView的actionForLayer:forKey:实现

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //test layer action when outside of animation block
    NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //begin animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block
    NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //end animation block
    [UIView commitAnimations];
    }

    @end

    运行程序,控制台显示结果如下:

    $ LayerTest[21215:c07] Outside: <null>
    $ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>

    于是我们可以预言,当属性在动画块之外发生改变,UIView直接通过返回nil来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性,在这个例子就是CABasicAnimation(第八章“显式动画”将会提到)。

    当然返回nil并不是禁用隐式动画唯一的办法,CATransacition有个方法叫做+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的[CATransaction begin]之后添加下面的代码,同样也会阻止动画的发生:

    [CATransaction setDisableActions:YES];

    总结一下,我们知道了如下几点

    • UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画(具体细节见第八章)。
    • 对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。

    我们来对颜色渐变的例子使用一个不同的行为,通过给colorLayer设置一个自定义的actions字典。我们也可以使用委托来实现,但是actions字典可以写更少的代码。那么到底改如何创建一个合适的行为对象呢?

    行为通常是一个被Core Animation隐式调用的显式动画对象。这里我们使用的是一个实现了CATransaction的实例,叫做推进过渡

    第八章中将会详细解释过渡,不过对于现在,知道CATransition响应CAAction协议,并且可以当做一个图层行为就足够了。结果很赞,不论在什么时候改变背景颜色,新的色块都是从左侧滑入,而不是默认的渐变效果。

    清单7.6 实现自定义行为

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;
    @property (nonatomic, weak) IBOutlet CALayer *colorLayer;/*热心人发现这里应该改为@property (nonatomic, strong) CALayer *colorLayer;否则运行结果不正确。
    */

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];

    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add a custom action
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromLeft;
    self.colorLayer.actions = @{@"backgroundColor": transition};
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
    }

    - (IBAction)changeColor
    {
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    }

    @end

    图7.3

    图7.3 使用推进过渡的色值动画

    7.4 呈现与模型

    CALayer的属性行为其实很不正常,因为改变一个图层的属性并没有立刻生效,而是通过一段时间渐变更新。这是怎么做到的呢?

    当你改变一个图层的属性,属性值的确是立刻更新的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。

    当设置CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。

    我们讨论的就是一个典型的微型MVC模式CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。

    在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着CALayer除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。

    每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值(图7.4)。

    我们在第一章中提到除了图层树,另外还有呈现树。呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil

    你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。

    图7.4

    图7.4 一个移动的图层是如何通过数据模型呈现的

    大多数情况下,你不需要直接访问呈现图层,你可以通过和模型图层的交互,来让Core Animation更新显示。两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。

    • 如果你在实现一个基于定时器的动画(见第11章“基于定时器的动画”),而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。
    • 如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法(见第三章“图层几何学”)来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。

    我们可以用一个简单的案例来证明后者(见清单7.7)。在这个例子中,点击屏幕上的任意位置将会让图层平移到那里。点击图层本身可以随机改变它的颜色。我们通过对呈现图层调用-hitTest:来判断是否被点击。

    如果修改代码让-hitTest:直接作用于colorLayer而不是呈现图层,你会发现当图层移动的时候它并不能正确显示。这时候你就需要点击图层将要移动到的位置而不是图层本身来响应点击(这就是为什么用呈现图层来响应交互的原因)。

    清单7.7 使用presentationLayer图层来判断当前图层位置

    @interface ViewController ()

    @property (nonatomic, strong) CALayer *colorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a red layer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
    self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.colorLayer];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //get the touch point
    CGPoint point = [[touches anyObject] locationInView:self.view];
    //check if we've tapped the moving layer
    if ([self.colorLayer.presentationLayer hitTest:point]) {
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    } else {
    //otherwise (slowly) move the layer to new position
    [CATransaction begin];
    [CATransaction setAnimationDuration:4.0];
    self.colorLayer.position = point;
    [CATransaction commit];
    }
    }

    @end


    总结

    这一章讨论了隐式动画,还有Core Animation对指定属性选择合适的动画行为的机制。同时你知道了UIKit是如何充分利用Core Animation的隐式动画机制来强化它的显式系统,以及动画是如何被默认禁用并且当需要的时候启用的。最后,你了解了呈现和模型图层,以及Core Animation是如何通过它们来判断出图层当前位置以及将要到达的位置。

    在下一章中,我们将研究Core Animation提供的显式动画类型,既可以直接对图层属性做动画,也可以覆盖默认的图层行为。

    收起阅读 »

    iOS 隐式动画 一

    隐式动画按照我的意思去做,而不是我说的。 -- 埃德娜,辛普森我们在第一部分讨论了Core Animation除了动画之外可以做到的任何事情。但是动画是Core Animation库一个非常显著的特性。这一章我们来看看它是怎么做到的。具体来说,我们先...
    继续阅读 »

    隐式动画

    按照我的意思去做,而不是我说的。 -- 埃德娜,辛普森

    我们在第一部分讨论了Core Animation除了动画之外可以做到的任何事情。但是动画是Core Animation库一个非常显著的特性。这一章我们来看看它是怎么做到的。具体来说,我们先来讨论框架自动完成的隐式动画(除非你明确禁用了这个功能)。

    7.1 事务

    Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。

    当你改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。

    这看起来这太棒了,似乎不太真实,我们来用一个demo解释一下:首先和第一章“图层树”一样创建一个蓝色的方块,然后添加一个按钮,随机改变它的颜色。代码见清单7.1。点击按钮,你会发现图层的颜色平滑过渡到一个新值,而不是跳变(图7.1)。

    清单7.1 随机改变图层颜色

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;
    @property (nonatomic, weak) IBOutlet CALayer *colorLayer;/*热心人发现这里应该改为@property (nonatomic, strong) CALayer *colorLayer;否则运行结果不正确。
    */
    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
    }

    - (IBAction)changeColor
    {
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 
    }

    @end

    图7.1

    图7.1 添加一个按钮来控制图层颜色

    这其实就是所谓的隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。Core Animaiton同样支持显式动画,下章详细说明。

    但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为

    事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。

    事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc-init方法创建它。但是可以用+begin+commit分别来入栈或者出栈。

    任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。

    Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。

    明白这些之后,我们就可以轻松修改变色动画的时间了。我们当然可以用当前事务的+setAnimationDuration:方法来修改动画时间,但在这里我们首先起一个新的事务,于是修改时间就不会有别的副作用。因为修改当前事务的时间可能会导致同一时刻别的动画(如屏幕旋转),所以最好还是在调整动画之前压入一个新的事务。

    修改后的代码见清单7.2。运行程序,你会发现色块颜色比之前变得更慢了。

    清单7.2 使用CATransaction控制动画时间

    - (IBAction)changeColor
    {
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
    }

    如果你用过UIView的动画方法做过一些动画效果,那么应该对这个模式不陌生。UIView有两个方法,+beginAnimations:context:+commitAnimations,和CATransaction+begin+commit方法类似。实际上在+beginAnimations:context:+commitAnimations之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction的原因。

    在iOS4中,苹果对UIView添加了一种基于block的动画方法:+animateWithDuration:animations:。这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。

    CATransaction+begin+commit方法在+animateWithDuration:animations:内部自动调用,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对+begin+commit匹配的失误造成的风险。

    7.2 完成块

    基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。我们来调整上个例子,在颜色变化结束之后执行一些操作。我们来添加一个完成之后的block,用来在每次颜色变化结束之后切换到另一个旋转90的动画。代码见清单7.3,运行结果见图7.2。

    清单7.3 在颜色动画完成之后添加一个回调

    - (IBAction)changeColor
    {
    //begin a new transaction
    [CATransaction begin];
    //set the animation duration to 1 second
    [CATransaction setAnimationDuration:1.0];
    //add the spin animation on completion
    [CATransaction setCompletionBlock:^{
    //rotate the layer 90 degrees
    CGAffineTransform transform = self.colorLayer.affineTransform;
    transform = CGAffineTransformRotate(transform, M_PI_2);
    self.colorLayer.affineTransform = transform;
    }];
    //randomize the layer background color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    //commit the transaction
    [CATransaction commit];
    }

    图7.2

    图7.2 颜色渐变之完成之后再做一次旋转

    注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。


    收起阅读 »

    iOS 专用图层 八

    6.10 AVPlayerLayer最后一个图层类型是AVPlayerLayer。尽管它不是Core Animation框架的一部分(AV前缀看上去像),AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation...
    继续阅读 »

    6.10 AVPlayerLayer

    最后一个图层类型是AVPlayerLayer。尽管它不是Core Animation框架的一部分(AV前缀看上去像),AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation紧密地结合在一起,提供了一个CALayer子类来显示自定义的内容类型。

    AVPlayerLayer是用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。AVPlayerLayer的使用相当简单:你可以用+playerLayerWithPlayer:方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后用player属性绑定一个AVPlayer实例。

    在我们开始之前,我们需要添加AVFoundation到我们的项目中。然后,清单6.15创建了一个简单的电影播放器,图6.16是代码运行结果。

    清单6.15 用AVPlayerLayer播放视频

    #import "ViewController.h"
    #import
    #import

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView; @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //get video URL
    NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];

    //create player and player layer
    AVPlayer *player = [AVPlayer playerWithURL:URL];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];

    //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];

    //play the video
    [player play];
    }
    @end

    图6.16 用AVPlayerLayer图层播放视频的截图

    我们用代码创建了一个AVPlayerLayer,但是我们仍然把它添加到了一个容器视图中,而不是直接在controller中的主视图上添加。这样其实是为了可以使用自动布局限制使得图层在最中间;否则,一旦设备被旋转了我们就要手动重新放置位置,因为Core Animation并不支持自动大小和自动布局(见第三章『图层几何学』)。

    当然,因为AVPlayerLayerCALayer的子类,它继承了父类的所有特性。我们并不会受限于要在一个矩形中播放视频;清单6.16演示了在3D,圆角,有色边框,蒙板,阴影等效果(见图6.17).

    清单6.16 给视频增加变换,边框和圆角

    - (void)viewDidLoad
    {
    ...
    //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];

    //transform layer
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / 500.0;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
    playerLayer.transform = transform;

    //add rounded corners and border
    playerLayer.masksToBounds = YES;
    playerLayer.cornerRadius = 20.0;
    playerLayer.borderColor = [UIColor redColor].CGColor;
    playerLayer.borderWidth = 5.0;

    //play the video
    [player play];
    }

    图6.17 3D视角下的边框和圆角AVPlayerLayer

    总结
    这一章我们简要概述了一些专用图层以及用他们实现的一些效果,我们只是了解到这些图层的皮毛,像CATiledLayer和CAEMitterLayer这些类可以单独写一章的。但是,重点是记住CALayer是用处很大的,而且它并没有为所有可能的场景进行优化。为了获得Core Animation最好的性能,你需要为你的工作选对正确的工具,希望你能够挖掘这些不同的CALayer子类的功能。 这一章我们通过CAEmitterLayer和AVPlayerLayer类简单地接触到了一些动画,在第二章,我们将继续深入研究动画,就从隐式动画开始。

    收起阅读 »