注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS逆向-逆向比较实用的工具

ChiselChisel is a collection of LLDB commands to assist in the debugging of iOS apps通过github上面说明安装一下pviews 找所有的视图pviews -u 查看上一层视图...
继续阅读 »

Chisel

Chisel is a collection of LLDB commands to assist in the debugging of iOS apps
通过github上面说明安装一下
pviews 找所有的视图
pviews -u 查看上一层视图
pvc 打印所有的控制器
pmethods 0x107da5370 打印所有方法
pinternals 0x107da5370 打印所有成员
fvc -v 0x107da5370,根据视图找到控制器
fv



flicker 会让视图闪烁两次

LLDB

search class 搜索对象
methods 0x 方法
b -a 0x02 下断点
sbt 恢复方法符号

cycript

./cycript 开始
ctrl + d 退出
首先要配置cycript,我这里面配置的是moneyDev,因为moneyDev里面包含cycript
./cycript -r 192.168.1.101:6666找到ip地址+:调试端口号默认6666

cy# keyWd .recursiveDescription().toString()层级视图



choose (UIButton)
这个工具不会阻塞进程
只要进程不被kill,ctrl+d在重新进入变量是都在的、
使用自己的cy

封装成脚本,在任意位置sh cyConnect.sh


配置.zshrc



使用


这里面也可以使用pviews() pvcs()等


转自链接:https://www.jianshu.com/p/a1c619e2da97
收起阅读 »

iOS逆向-18:LLDB调试

在逆向环境中,拿不到源码,只能通过指令设置断点LLDB(Low Lever Debug)默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足...
继续阅读 »

在逆向环境中,拿不到源码,只能通过指令设置断点

LLDB(Low Lever Debug)

默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足实际需要。

这里列举了一些常用的命令:

断点设置

  • 设置断点
    breakpoint set -n XXX
    set 是子命令
    -n 是选项 是--name 的缩写!

  • 查看断点列表
    breakpoint list

  • 删除
    breakpoint delete 组号

  • 禁用/启用
    breakpoint disable 禁用
    breakpoint enable 启用

  • 遍历整个项目中满足Game:这个字符的所有方法
    breakpoint set -r Game:

流程控制

  • 继续执行
    continue c

  • 单步运行,将子函数当做整体一步执行
    n next
    单步运行,遇到子函数会进去
    s

  • stop-hook
    让你在每次stop的时候去执行一些命令,只对breadpoint,watchpoint

  • 其他命令
    image list
    p expression 除了打印还可以执行一些代码
    b -[xxx xxx]
    x16进制打印
    register read 读寄存器
    po
    b -r xx断住所有包含的方法
    b -selector xx断住所有xx方法
    help xx查看指令

函数调用栈

bt //所有调用栈
up //跳上层堆栈
down
frame select 12 跳指定下标堆栈
frame variable 当前函数参数,只能修改当前函数参数
thread return 代码回滚,直接返回,不执行后面的代码。提前返回,可以通过这种方式绕过hook

内存断点

Person *person = [Person new];
person.name = "FY";
下内存断点:
watchpoint set variable person->_name
watchpoint set expression 0x456 &person->_name
当进行修改的时候就会触发内存断点


然后我们bt一下

可以看到方法的调用栈
break command add 1
在断点中添加一些指令

让你在每次stop的时候去执行一些命令,只对breadpoint,watchpoint
target stop-hook add -o "frame variable"
target stop-hook add -o "po self.view"
target stop-hook list

这些指令也可以放到家目录下的.lldbinit中,只要lldb一启动就会执行里面的命令,一般的lldb插件就是在这个目录配置的

cd ~ 进入家目录
.lldbinit




转自链接:https://www.jianshu.com/p/59123ee28503
收起阅读 »

Swift中的闭包

一、简介闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 OC 中的代码块(blocks)以及其他一些编程语言中的 匿名函数 ...
继续阅读 »

一、简介

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。
Swift 中的闭包与 C 和 OC 中的代码块(blocks)以及其他一些编程语言中的 匿名函数 比较相似。全局函数和嵌套函数其实就是特殊的闭包。

由于之前对 Swift 中的闭包不太熟悉,所以在此归纳总结一下闭包的语法。

二、语法

Swift 中的闭包有很多优化的地方:

根据上下文推断参数和返回值类型
从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略 return)
可以使用简化参数名,如$0, $1(从 0 开始,表示第 i 个参数...)
提供了尾随闭包语法(Trailing closure syntax)
闭包表达式 提供了一些语法优化,使得撰写闭包变得简单明了。下面 闭包表达式 的例子通过使用几次迭代展示了 sorted(by:) 方法定义和语法优化的方式。下面的每一次迭代都用更简洁的方式描述了相同的功能。🐳->🐘->🦛->🐷->🐔->🐹->🦟。

sorted(by:) 函数介绍:
Swift 标准库提供了名为 sorted(by:) 的方法,它会根据你所提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

下面的闭包表达式示例使用 sorted(by:)方法对一个 String 类型的数组进行字母逆序排序。以下是初始数组:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:)方法接受一个 闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

该例子对一个 String 类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool

原始实现方式:

提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 sorted(by:)方法的参数传入:

func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// 打印可得 reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

然而,以这种方式来编写一个实际上很简单的表达式(a > b),确实太过繁琐了。对于这个例子来说,利用 闭包表达式语法 可以更好地构造一个 内联排序闭包

闭包表达式语法:

闭包表达式语法有如下的一般形式:

{ (parameters) -> return type in
statements
}

闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。

第一次精简——闭包语法:

下面的例子展示了之前 backward(_:_:) 函数对应的闭包表达式版本的代码:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:) 函数类型声明相同。在这两种方式中,都写成了 (s1: String, s2: String) -> Bool。然而在内联闭包表达式中,函数和返回值类型都写在大括号 ,而不是大括号 
关键字 in:闭包的函数体部分由关键字 in 引入。该关键字表示 “闭包的参数和返回值类型定义已经完成,闭包函数体即将开始”。

该例中 sorted(by:) 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了 内联闭包

第二次精简——根据上下文推断类型:

因为排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。sorted(by:)方法被一个字符串数组调用,因此其参数必须是 (String, String) -> Bool 类型的函数。这意味着 (String, String)和 Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。

第三次精简——单表达式闭包隐式返回:

单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

在这个例子中,sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。因为闭包函数体只包含了一个单一表达式(s1 > s2),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略。

第四次精简——参数名称缩写:

Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0,$1,$2 来顺序调用闭包的参数,以此类推。

如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
reversedNames = names.sorted(by: { $0 > $1 } )

在这个例子中,$0 和 $1 表示闭包中第一个和第二个 String 类型的参数。

第五次精简——运算符方法:

实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断出你想使用大于号的字符串函数实现:
reversedNames = names.sorted(by: >)

第六次精简——尾随闭包:

什么是尾随闭包?
如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用 尾随闭包 来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}

// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})

// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}

在上面 sorted(by:) 方法参数的字符串排序闭包可以改写为:

reversedNames = names.sorted() { $0 > $1 }

如果闭包表达式是函数或方法的唯一参数,则当使用尾随闭包时,甚至可以把 () 省略掉:

reversedNames = names.sorted { $0 > $1 }

以上只是列举了闭包的一些基本语法与用法,还有一些其他概念需要继续学习,如 自动闭包、逃逸闭包,后续我会慢慢补齐总结的,谢谢!


转自链接:https://www.jianshu.com/p/7043ffaac2f2
收起阅读 »

YTKNetwork的基本使用

YTKNetwork是一个对AFNetworking封装的一个框架,虽然二者底层原理相同,但使用方法和使用效果是大不相同的。YTKNetwork 提供了以下更高级的功能:1.支持按时间缓存网络请求内容2.支持按版本号缓存网络请求内容3.支持统一设置服务器和 C...
继续阅读 »

YTKNetwork是一个对AFNetworking封装的一个框架,虽然二者底层原理相同,但使用方法和使用效果是大不相同的。YTKNetwork 提供了以下更高级的功能:

1.支持按时间缓存网络请求内容
2.支持按版本号缓存网络请求内容
3.支持统一设置服务器和 CDN 的地址
4.支持检查返回 JSON 内容的合法性
5.支持文件的断点续传
6.支持 block 和 delegate 两种模式的回调方式
7.支持批量的网络请求发送,并统一设置它们的回调(实现在 YTKBatchRequest 类中)
支持方便地设置有相互依赖的网络请求的发送,例如:发送请求 A,根据请求 A 的结果,选择性的发送请求 B
和 C,再根据 B 和 C 的结果,选择性的发送请求 D。(实现在 YTKChainRequest 类中)
支持网络请求 URL 的 filter,可以统一为网络请求加上一些参数,或者修改一些路径。


YTKNetwork包含了这几个类:1、YTKNetworkConfig (设置域名) 2、YTKRequest (网络请求)3、YTKBatchRequest (请求多个类 )4、YTKChainRequest (依赖请求)5、YTKBaseRequest(YTKRequest的父类)

YTKNetwork 的基本思想

YTKNetwork 的基本的思想是把每一个网络请求封装成对象。所以使用 YTKNetwork,你的每一个请求都需要继承 YTKRequest 类,通过覆盖父类的一些方法来构造指定的网络请求。


集约式和离散式API

集约式API

介绍:所有API的调用只有一个类,然后这个类接收API名字,API参数,以及回调着陆点,即项目中的每个请求都会走统一的入口,对外暴露了请求的 URL 和 Param 以及请求方式,入口一般都是通过单例 来实现,AFNetworking 的官方 demo 就是采用的集约式的方式对网络请求进行的封装,也是目前比较流行的网络请求方式。

优点:使用便捷,能实现快速开发
缺点:
1.对每个请求的定制型不够强
2.不方便后期业务拓展

我们常用的AFNetworking框架就是集约式,在简单程序中AFNetworking 将请求逻辑写在 Controller 中比YTK更加方便,也不用一个个请求新建不同的request类。而YTKNetworking则是离散式的

离散式API

介绍:离散型API调用是这样的,一个API对应于一个APIManager,然后这个APIManager只需要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。即每个网络请求类都是一个对象,它的 URL 以及请求方式和响应方式 均不暴露给外部调用。只能内部通过 重载或实现协议 的方式来指定,外部调用只需要传 Param 即可,YTKNetwork就是采用的这种网络请求方式。


优点:URL 以及请求和响应方式不暴露给外部,避免外部调用的时候写错
业务方使用起来较简单,业务使用者不需要去关心它的内部实现
可定制性强,可以为每个请求指定请求的超时时间以及缓存的周期
缺点:
网络层需要业务实现方去写,变相的增加了部分工作量
文件增多,程序包会变大一些

YTKNetworkConfig 类

YTKNetworkConfig 类有两个作用:
统一设置网络请求的服务器和 CDN 的地址。
管理网络请求的 YTKUrlFilterProtocol 实例
我们为什么需要统一设置服务器地址呢?因为:

按照设计模式里的 Do Not Repeat Yourself 原则,我们应该把服务器地址统一写在一个地方。
在实际业务中,我们的测试人员需要切换不同的服务器地址来测试。统一设置服务器地址到 YTKNetworkConfig 类中,也便于我们统一切换服务器地址。
具体的用法是,在程序刚启动的回调中,设置好 YTKNetworkConfig 的信息,如下所示:

- (BOOL)application:(UIApplication *)application 
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig];
config.baseUrl = @"http://yuantiku.com";
config.cdnUrl = @"http://fen.bi";
}

设置好之后,所有的网络请求都会默认使用 YTKNetworkConfig 中 baseUrl 参数指定的地址。

大部分企业应用都需要对一些静态资源(例如图片、js、css)使用 CDN。YTKNetworkConfig 的 cdnUrl 参数用于统一设置这一部分网络请求的地址。

当我们需要切换服务器地址时,只需要修改 YTKNetworkConfig 中的 baseUrl 和 cdnUrl 参数即可。

YTKRequest

YTK把每个请求实例化,管理它的生命周期,也可以管理多个请求,在github的基础教程里面我们可以看到YTK是把每个网络请求都封装成对象,每一种网络请求继承 YTKRequest 类后,需要用方法覆盖(overwrite)的方式,来指定网络请求的具体信息。如下是一个示例:

假如我们要向网址 http://www.yuantiku.com/iphone/register 发送一个 POST 请求,请求参数是 username 和 password。那么,这个类应该如下所示:

// RegisterApi.h
#import "YTKRequest.h"

@interface RegisterApi : YTKRequest

- (id)initWithUsername:(NSString *)username password:(NSString *)password;

@end


// RegisterApi.m

#import "RegisterApi.h"

@implementation RegisterApi {
NSString *_username;
NSString *_password;
}

- (id)initWithUsername:(NSString *)username password:(NSString *)password {
self = [super init];
if (self) {
_username = username;
_password = password;
}
return self;
}

- (NSString *)requestUrl {
// “ http://www.yuantiku.com ” 在 YTKNetworkConfig 中设置,这里只填除去域名剩余的网址信息
return @"/iphone/register";
}

- (YTKRequestMethod)requestMethod {
return YTKRequestMethodPOST;
}

- (id)requestArgument {
return @{
@"username": _username,
@"password": _password
};
}

@end

在上面这个示例中,我们可以看到:

  • 我们通过覆盖 YTKRequest 类的 requestUrl 方法,实现了指定网址信息。并且我们只需要指定除去域名剩余的网址信息,因为域名信息在 YTKNetworkConfig 中已经设置过了。
  • 我们通过覆盖 YTKRequest 类的 requestMethod 方法,实现了指定 POST 方法来传递参数。
  • 我们通过覆盖 YTKRequest 类的 requestArgument 方法,提供了 POST 的信息。这里面的参数 username 和 password 如果有一些特殊字符(如中文或空格),也会被自动编码。

调用 RegisterApi

在构造完成 RegisterApi 之后,具体如何使用呢?我们可以在登录的 ViewController 中,调用 RegisterApi,并用 block 的方式来取得网络请求结果:
- (void)loginButtonPressed:(id)sender {
NSString *username = self.UserNameTextField.text;
NSString *password = self.PasswordTextField.text;
if (username.length > 0 && password.length > 0) {
RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
[api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
// 你可以直接在这里使用 self
NSLog(@"succeed");
} failure:^(YTKBaseRequest *request) {
// 你可以直接在这里使用 self
NSLog(@"failed");
}];
}
}

注意:你可以直接在 block 回调中使用 self,不用担心循环引用。因为 YTKRequest 会在执行完 block 回调之后,将相应的 block 设置成 nil。从而打破循环引用。

除了 block 的回调方式外,YTKRequest 也支持 delegate 方式的回调:

- (void)loginButtonPressed:(id)sender {
NSString *username = self.UserNameTextField.text;
NSString *password = self.PasswordTextField.text;
if (username.length > 0 && password.length > 0) {
RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
api.delegate = self;
[api start];
}
}

- (void)requestFinished:(YTKBaseRequest *)request {
NSLog(@"succeed");
}

- (void)requestFailed:(YTKBaseRequest *)request {
NSLog(@"failed");
}

验证服务器返回内容

有些时候,由于服务器的 Bug,会造成服务器返回一些不合法的数据,如果盲目地信任这些数据,可能会造成客户端 Crash。如果加入大量的验证代码,又使得编程体力活增加,费时费力。

使用 YTKRequest 的验证服务器返回值功能,可以很大程度上节省验证代码的编写时间。

例如,我们要向网址 http://www.yuantiku.com/iphone/users 发送一个 GET 请求,请求参数是 userId 。我们想获得某一个用户的信息,包括他的昵称和等级,我们需要服务器必须返回昵称(字符串类型)和等级信息(数值类型),则可以覆盖 jsonValidator 方法,实现简单的验证。

- (id)jsonValidator {
return @{
@"nick": [NSString class],
@"level": [NSNumber class]
};
}

断点续传

要启动断点续传功能,只需要覆盖 resumableDownloadPath 方法,指定断点续传时文件的存储路径即可,文件会被自动保存到此路径。如下代码将刚刚的取图片的接口改造成了支持断点续传:

@implementation GetImageApi {
NSString *_imageId;
}

- (id)initWithImageId:(NSString *)imageId {
self = [super init];
if (self) {
_imageId = imageId;
}
return self;
}

- (NSString *)requestUrl {
return [NSString stringWithFormat:@"/iphone/images/%@", _imageId];
}

- (BOOL)useCDN {
return YES;
}

- (NSString *)resumableDownloadPath {
NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *cachePath = [libPath stringByAppendingPathComponent:@"Caches"];
NSString *filePath = [cachePath stringByAppendingPathComponent:_imageId];
return filePath;
}

@end

按时间缓存内容

刚刚我们写了一个 GetUserInfoApi ,这个网络请求是获得用户的一些资料。

我们想像这样一个场景,假设你在完成一个类似微博的客户端,GetUserInfoApi 用于获得你的某一个好友的资料,因为好友并不会那么频繁地更改昵称,那么短时间内频繁地调用这个接口很可能每次都返回同样的内容,所以我们可以给这个接口加一个缓存。

在如下示例中,我们通过覆盖 cacheTimeInSeconds 方法,给 GetUserInfoApi 增加了一个 3 分钟的缓存,3 分钟内调用调 Api 的 start 方法,实际上并不会发送真正的请求。

@implementation GetUserInfoApi {
NSString *_userId;
}

- (id)initWithUserId:(NSString *)userId {
self = [super init];
if (self) {
_userId = userId;
}
return self;
}

- (NSString *)requestUrl {
return @"/iphone/users";
}

- (id)requestArgument {
return @{ @"id": _userId };
}

- (id)jsonValidator {
return @{
@"nick": [NSString class],
@"level": [NSNumber class]
};
}

- (NSInteger)cacheTimeInSeconds {
// 3 分钟 = 180 秒
return 60 * 3;
}

@end

该缓存逻辑对上层是透明的,所以上层可以不用考虑缓存逻辑,每次调用 GetUserInfoApi 的 start 方法即可。GetUserInfoApi 只有在缓存过期时,才会真正地发送网络请求。



转自链接:https://www.jianshu.com/p/8213f3e3b0ea
收起阅读 »

Linux - 远程操作

shotdown命令,默认表示1分钟后关机.命令格式:$shutdown [选项] <参数>参数示例一分钟以后关机$shutdown 立刻关机$shutdown now 在今天的21:30关机$shutdown 21:30 10分钟以后关机$s...
继续阅读 »


关机重/启命令

shutdown命令可以安全关闭 或者 重新启动系统,直接使用 shotdown命令,默认表示1分钟后关机.
命令格式:

$shutdown [选项] <参数>

选项
功能
[-r]重新启动
[-c]取消之前的关机计划

参数

  • [时间]:设置多久时间后执行shutdown指令;
  • [警告信息]:要传送给所有登入用户的信息。


示例

  • 一分钟以后关机
$shutdown  
  • 立刻关机
$shutdown now
  • 在今天的21:30关机
$shutdown 21:30
  • 10分钟以后关机
$shutdown +10
  • 10分钟以后关机,同时发出警告信息
$shutdown +10 "System will shutdown after 10 minutes"
  • 取消关机计划
$shutdown -c

reboot命令也可以用来重新启动正在运行的Linux操作系统。
和 shutdown -r now一样

网络配置命令

命令功能
ifconfigconfigure a network interface,查看/配置计算机当前的网卡信息
ping测试目标ip地址的连接是否正常

ifconfig命令

ifconfig命令被用于配置和显示Linux中网卡信息。
查看网卡信息

$ifconfig

快速定位IP地址

$ifconfig | grep inet

一台计算机中可能会有一个 物理网卡 和 多个虚拟网卡,在Linux中物理网卡名字一般是 ensXX

  • 127.0.0.1这个地址是一个比较特殊的地址,称之为本地回环地址,可以用来测试本机网卡是否正常工作。

ping命令

ping命令用来测试主机之间网络的连通性。执行ping指令会使用ICMP传输协议,发出要求回应的信息。一般用于检测计算机之间的网络通讯是否正常。

由于ping命令的工作原理,服务器人员给往往将ping用作动词。经常说:“ping一下某某计算机”

示例:

“ping”目标主机

$ping IP地址

检测本地网卡是否正常

$ping 127.0.0.1

结束ping的执行使用Ctrl+C。在Linux中终止一个终端程序绝大多数都可以使用Ctrl+C

SSH(Secure Shell)

简单说,SSH是一种网络协议,用于计算机之间的加密登录。
最早的时候,互联网通信都是明文通信,一旦被截获,内容就暴露无疑。1995年,芬兰学者Tatu Ylonen设计了SSH协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广,目前已经成为Linux系统的标准配置。

OpenSSH

SSH只是一种协议,存在多种实现OpenSSH就是其中一种,它是一款软件,应用非常广泛在Mac以及Ubuntu中都自带OpenSSH

SSH的登录过程

  • (1)远程主机收到用户的登录请求,把自己的公钥发给用户。
  • (2)用户使用这个公钥,将登录密码加密后,发送回来。
  • (3)远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录。

SSH客户端命令

ssh [-p port] user@remote

  • user 是远程端上的用户名,默认是当前用户
  • remote是远程端的地址,可以是IP/域名
  • port是远程端的端口,默认是22

Ubuntu下开启SSH

Ubuntu下SSH分

  • openssh-client(客户端)
  • openssh-server (服务端)
检测是否有开启ssh服务
hank@ubuntu:~$ ps -e | grep ssh
4910 ? 00:00:00 sshd
其中sshd 为server端的守护进程,如果没有出现sshd,那么很有可能你的系统中没有安装server端。或者ssh服务没有启动。

开启ssh服务
hank@ubuntu:~$ sudo /etc/init.d/ssh start
[ ok ] Starting ssh (via systemctl): ssh.service.
安装openssh-server

如果显示上述命令找不到。那么是因为我们的Ubuntu系统默认没有服务端,所以可以通过下面命令安装。
$ sudo apt-get install openssh-server

可能出现错误
$ sudo apt-get install openssh-server
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
有一些软件包无法被安装。如果您用的是 unstable 发行版,这也许是
因为系统无法达到您要求的状态造成的。该版本中可能会有一些您需要的软件
包尚未被创建或是它们已被从新到(Incoming)目录移出。
下列信息可能会对解决问题有所帮助:

下列软件包有未满足的依赖关系:
openssh-server : 依赖: openssh-client (= 1:7.1p1-4)
依赖: openssh-sftp-server 但是它将不会被安装
推荐: ssh-import-id 但是它将不会被安装
E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。

因为openssh-server 需要依赖openssh-client,但是很明显,我们系统自带的版本和目前要安装的server版本不同。所以我们重新安装一下client版本。


hank@ubuntu:~$ sudo apt-get install openssh-client=1:7.1p1-4
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
建议安装:
ssh-askpass libpam-ssh keychain monkeysphere
下列软件包将被【降级】:
openssh-client
升级了 0 个软件包,新安装了 0 个软件包,降级了 1 个软件包,要卸载 0 个软件包,有 0 个软件包未被升级。
需要下载 581 kB 的归档。
解压缩后将会空出 36.9 kB 的空间。
您希望继续执行吗? [Y/n] y
获取:1 http://mirror.neu.edu.cn/ubuntu xenial/main amd64 openssh-client amd64 1:7.1p1-4 [581 kB]
已下载 581 kB,耗时 33 (17.6 kB/s)
dpkg:警告:即将把 openssh-client 1:7.2p2-4 降级到 1:7.1p1-4
正在将 openssh-client (1:7.1p1-4) 解包到 (1:7.2p2-4) 上 ...
正在处理用于 man-db (2.7.5-1) 的触发器 ...
正在设置 openssh-client (1:7.1p1-4) ...
正在安装新版本配置文件 /etc/ssh/ssh_config ...
这样可以看到降级成功。然后我们再次安装openssh-server就OK了!

hank@ubuntu:~$ sudo apt-get install openssh-server

SCP(Secure copy)

  • scp scp是linux系统下基于ssh登陆进行安全的远程文件拷贝命令。
  • 命令格式
scp -P port 源文件路径 目标文件路径
# 将本地目录下的123.txt拷贝到远程桌面目录下
$scp -P port 123.txt user@remote:Desktop/123.txt

# 把远程桌面目录下的123.txt文件 复制到 本地当前目录下
scp -P port user@remote:Desktop/123.txt 123.txt

# 加上 -r 选项可以传送文件夹
# 把当前目录下的 demo 文件夹 复制到 远程 家目录下的 Desktop
scp -r demo user@remote:Desktop

# 把远程 家目录下的 Desktop 复制到 当前目录下的 demo 文件夹
scp -r user@remote:Desktop demo

选项功能
-r若给出的源文件是目录文件,则 scp 将递归复制该目录下的所有子目录和文件,目标文件必须为一个目录名
-P若远程 SSH 服务器的端口不是 22,需要使用大写字母 -P 选项指定端口

SSH常用配置

免密登陆

  • 配置公钥
    执行 ssh-keygen 即可生成 SSH 钥匙,一路回车即可
  • 上传公钥到服务器
    执行 ssh-copy-id -p port user@remote,可以让远程服务器记住我们的公钥

配置别名

每次都输入ssh -p port user@remote,非常不方便,而且还不好记忆

而 配置别名 可以让我们进一步偷懒,譬如用:ssh mac 来替代上面这么一长串,那么就在 ~/.ssh/config 里面追加以下内容:


Host mac
HostName ip地址
User H
Port 22

保存之后,即可用 ssh mac 实现远程登录了,scp 同样可以使用。


作者:请叫我Hank
链接:https://www.jianshu.com/p/9b31892a572f



收起阅读 »

Linux简介

Linux 内核以及发行版Linux内核(kernel)操作系统内核是指大多数操作系统的核心部分。它由操作系统中用于管理存储器、文件、外设和系统资源的那些部分组成。操作系统内核通常运行进程,并提供进程间的通信。Linux 内核版本又分为 稳定版&nb...
继续阅读 »

Linux 内核以及发行版

  • Linux内核(kernel)

操作系统内核是指大多数操作系统的核心部分。它由操作系统中用于管理存储器、文件、外设和系统资源的那些部分组成。操作系统内核通常运行进程,并提供进程间的通信。
Linux 内核版本又分为 稳定版 和 开发版,两种版本是相互关联,相互循环

  • 稳定版:具有工业级强度,可以广泛地应用和部署。

  • 开发版:由于要试验各种解决方案,所以变化很快

  • 内核源码网址:http://www.kernel.org

  • Linux发行版

Linux 发行版:我们常说的Linux操作系统,也是由Linux内核与各种常用软件的集合产品. 类似Windows包含了桌面环境.全球大约有数百款的Linux系统版本,每个系统版本都有自己的特性和目标人群.

Ubuntu(乌班图)

Ubuntu是一个以桌面应用为主的开源GNU/Linux操作系统,主要依赖Canonical有限公司的支持,同时也有很多来自Linux社区的热心人士提供协助。
作为Linux发行版之一.Canonical 的Ubuntu 胜过其他所有的 Linux 服务器发行版 ,它简单易用同时又相当稳定,而且具有庞大的社区力量,用户可以方便地从社区获得帮助.Ubuntu在服务器领域是妥妥的赢家.

Ubuntu的目录结构



Ubuntu的主要目录
  • /:根目录,一般根目录下只存放目录,在 linux 下有且只有一个根目录,所有的东西都是从这里开始
  • /bin、/usr/bin:可执行二进制文件的目录,如常用的命令 ls、tar、mv、cat 等
  • /boot:放置 linux 系统启动时用到的一些文件,如 linux 的内核文件:/boot/vmlinuz,系统引导管理器:/boot/grub
  • /dev:存放linux系统下的设备文件,访问该目录下某个文件,相当于访问某个设备,常用的是挂载光驱mount /dev/cdrom /mnt
  • /etc:系统配置文件存放的目录,不建议在此目录下存放可执行文件,重要的配置文件有
    • /etc/inittab
    • /etc/fstab
    • /etc/init.d
    • /etc/X11
    • /etc/sysconfig
    • /etc/xinetd.d
  • /home:系统默认的用户家目录,新增用户账号时,用户的家目录都存放在此目录下
    • ~ 表示当前用户的家目录
    • ~edu 表示用户 edu 的家目录
  • /lib、/usr/lib、/usr/local/lib:系统使用的函数库的目录,程序在执行过程中,需要调用一些额外的参数时需要函数库的协助
  • /lost+fount:系统异常产生错误时,会将一些遗失的片段放置于此目录下
  • /mnt: /media:光盘默认挂载点,通常光盘挂载于 /mnt/cdrom 下,也不一定,可以选择任意位置进行挂载
  • /opt:给主机额外安装软件所摆放的目录
  • /proc:此目录的数据都在内存中,如系统核心,外部设备,网络状态,由于数据都存放于内存中,所以不占用磁盘空间,比较重要的文件有:/proc/cpuinfo、/proc/interrupts、/proc/dma、/proc/ioports、/proc/net/* 等
  • /root:系统管理员root的家目录
  • /sbin、/usr/sbin、/usr/local/sbin:放置系统管理员使用的可执行命令,如 fdisk、shutdown、mount 等。与 /bin 不同的是,这几个目录是给系统管理员 root 使用的命令,一般用户只能"查看"而不能设置和使用
  • /tmp:一般用户或正在执行的程序临时存放文件的目录,任何人都可以访问,重要数据不可放置在此目录下
  • /srv:服务启动之后需要访问的数据目录,如 www 服务需要访问的网页数据存放在 /srv/www 内
  • /usr:应用程序存放目录
    • /usr/bin:存放应用程序
    • /usr/share:存放共享数据
    • /usr/lib:存放不能直接运行的,却是许多程序运行所必需的一些函数库文件
    • /usr/local:存放软件升级包
    • /usr/share/doc:系统说明文件存放目录
    • /usr/share/man:程序说明文件存放目录
  • /var:放置系统执行过程中经常变化的文件
    • /var/log:随时更改的日志文件
    • /var/spool/mail:邮件存放的目录
    • /var/run:程序或服务启动后,其 PID 存放在该目录下
Ubuntu的常见快捷键

可以在System Setting -> Keyboard -> Shortcuts中查看各种快捷键.

  • 终端: Ctrl+Alt+T
  • 终端新建标签页: Ctrl+Shift+T
  • 终端复制粘贴: Ctrl+Shift+C, Ctrl+Shift+V
  • 显示常用快捷键: 按住Super(Win)不动
  • 截活动窗口图: Alt+Print
  • 区域截图: Shift+Print
  • 源切换: Super(Win)+Space
  • 安装: sudo apt-get install
  • 卸载: sudo apt-get remove
  • 移除没用的包: sudo apt-get autoremove
Ubuntu的常见设置
首先语言设置
  • 通过右上角的 设置按钮 找到System Settings...
  • 然后选中Language Support 项
  • 注意Ubuntu的语言选项有多种语言.将第一语言设置为中文(因为如果中文显示不了的,会使用英文显示)




  • 设置完成后.选择Apply System-wide(应用到整个系统)这时,输入管理员密码以确认.最后点击 Close 按钮关闭对话框,重启电脑。


注意:重启成功后,会让你选择文件夹名称显示.如果是为了学习.我建议大家保持原来的文件夹名称,这样便于后期在学习中熟悉Linux目录结构. 选择Keep Old Names





Launcher(菜单栏)设置

在系统设置中,找不到菜单栏的位置设置.所以只能通过终端命令进行设置

  • 菜单栏靠左(注意参数首字母大写)
$ gsettings set com.canonical.Unity.Launcher launcher-position Left
  • 菜单栏靠下
$ gsettings set com.canonical.Unity.Launcher launcher-position Bottom
Ubuntu常用软件
  • 设置软件源: 默认的软件源是官方的, 速度慢的令人发指, 所以需要先设置一个速度较快的软件源, System Settings -> Software & Updates -> Ubuntu Software -> Download from选择Others, 然后自动选择一个网速比较快的服务器(多半是某个大学的)即可:
  • apt(Advanced Packaging Tool) 安装/卸载软件 (Ctrl+Alt+T 调出终端)

安装软件

$ sudo apt install 软件包

卸载软件

$ sudo apt remove 软件名

更新已安装的包

$ sudo apt upgrade  或者 sudo apt-get upgrade
升级

 sudo apt-get update.

那么由于有些Ubuntu中没有自带vim 而是 vi 这个古老的编辑器.所以我们需要安装vim

sudo apt-get install vim
在安装过程中有可能出现下列错误
vim : 依赖: vim-common (= 2:7.4.826-1ubuntu1) 但是 2:7.4.1689-3ubuntu1.1 正要被安装
E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。

解决方案:

sudo apt-get remove vim-common
sudo apt-get install vim


作者:Hank
链接:https://www.jianshu.com/p/2ca7f448ffa7




收起阅读 »

汇编-函数本质(下)

函数的返回值一般是一个指针,不会超过8字节。寄存器就完全够用了。如果要返回一个结构体类型超过字节。下面的例子(结构体占用字节):汇编代码:str这里没有使用作为返回值,而是使用了栈空间。8字节,也会保存在栈中返回(上一个函数栈空间)struct str { ...
继续阅读 »

篇幅限制,分为2篇


返回值



函数的返回值一般是一个指针,不会超过8字节。X0寄存器就完全够用了。如果要返回一个结构体类型超过8字节。
下面的例子(str结构体占用24字节):

struct str {
int a;
int b;
int c;
int d;
int e;
int f;
};

struct str getStr(int a, int b, int c, int d, int e, int f) {
struct str str1;
str1.a = a;
str1.b = b;
str1.c = c;
str1.d = d;
str1.e = e;
str1.f = f;
return str1;
}

- (void)viewDidLoad {
[super viewDidLoad];
struct str str2 = getStr(1,2,3,4,5,6);
}

汇编代码:

TestDemo`-[ViewController viewDidLoad]:
0x1042b5e58 <+0>: sub sp, sp, #0x50 ; =0x50
0x1042b5e5c <+4>: stp x29, x30, [sp, #0x40]
0x1042b5e60 <+8>: add x29, sp, #0x40 ; =0x40
0x1042b5e64 <+12>: stur x0, [x29, #-0x8]
0x1042b5e68 <+16>: stur x1, [x29, #-0x10]
0x1042b5e6c <+20>: ldur x8, [x29, #-0x8]
0x1042b5e70 <+24>: add x9, sp, #0x20 ; =0x20
0x1042b5e74 <+28>: str x8, [sp, #0x20]
0x1042b5e78 <+32>: adrp x8, 4
0x1042b5e7c <+36>: add x8, x8, #0x418 ; =0x418
0x1042b5e80 <+40>: ldr x8, [x8]
0x1042b5e84 <+44>: str x8, [x9, #0x8]
0x1042b5e88 <+48>: adrp x8, 4
0x1042b5e8c <+52>: add x8, x8, #0x3e8 ; =0x3e8
0x1042b5e90 <+56>: ldr x1, [x8]
0x1042b5e94 <+60>: mov x0, x9
0x1042b5e98 <+64>: bl 0x1042b6564 ; symbol stub for: objc_msgSendSuper2
//x8指向栈空间的区域,预留足够的空间
0x1042b5e9c <+68>: add x8, sp, #0x8 ; =0x8
0x1042b5ea0 <+72>: mov w0, #0x1
0x1042b5ea4 <+76>: mov w1, #0x2
0x1042b5ea8 <+80>: mov w2, #0x3
0x1042b5eac <+84>: mov w3, #0x4
0x1042b5eb0 <+88>: mov w4, #0x5
0x1042b5eb4 <+92>: mov w5, #0x6
0x1042b5eb8 <+96>: bl 0x1042b5e04 ; getStr at ViewController.m:59
-> 0x1042b5ebc <+100>: ldp x29, x30, [sp, #0x40]
0x1042b5ec0 <+104>: add sp, sp, #0x50 ; =0x50
0x1042b5ec4 <+108>: ret
str函数:

    TestDemo`getStr:
-> 0x1001d1e04 <+0>: sub sp, sp, #0x20 ; =0x20
//参数分别放入栈中
0x1001d1e08 <+4>: str w0, [sp, #0x1c]
0x1001d1e0c <+8>: str w1, [sp, #0x18]
0x1001d1e10 <+12>: str w2, [sp, #0x14]
0x1001d1e14 <+16>: str w3, [sp, #0x10]
0x1001d1e18 <+20>: str w4, [sp, #0xc]
0x1001d1e1c <+24>: str w5, [sp, #0x8]

//取出来放入w9,
0x1001d1e20 <+28>: ldr w9, [sp, #0x1c]
//存入x8,也就是上一个栈中直到写完
0x1001d1e24 <+32>: str w9, [x8]
0x1001d1e28 <+36>: ldr w9, [sp, #0x18]
0x1001d1e2c <+40>: str w9, [x8, #0x4]
0x1001d1e30 <+44>: ldr w9, [sp, #0x14]
0x1001d1e34 <+48>: str w9, [x8, #0x8]
0x1001d1e38 <+52>: ldr w9, [sp, #0x10]
0x1001d1e3c <+56>: str w9, [x8, #0xc]
0x1001d1e40 <+60>: ldr w9, [sp, #0xc]
0x1001d1e44 <+64>: str w9, [x8, #0x10]
0x1001d1e48 <+68>: ldr w9, [sp, #0x8]
0x1001d1e4c <+72>: str w9, [x8, #0x14]
//栈平衡,这里没有以 x0 作为返回值,已经全部写入上一个函数栈x8中。
0x1001d1e50 <+76>: add sp, sp, #0x20 ; =0x20
0x1001d1e54 <+80>: ret
这里没有使用X0作为返回值,而是使用了栈空间。



如果返回值大于8字节,也会保存在栈中返回(上一个函数栈空间)

那么结构体参数超过8个呢?
猜测参数和返回值都存在上一个函数的栈中,参数应该在低地址。返回值在高地址。


struct str {
int a;
int b;
int c;
int d;
int e;
int f;
int g;
int h;
int i;
int j;
};

struct str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) {
struct str str1;
str1.a = a;
str1.b = b;
str1.c = c;
str1.d = d;
str1.e = e;
str1.f = f;
str1.g = g;
str1.h = h;
str1.i = i;
str1.j = j;
return str1;
}

- (void)viewDidLoad {
[super viewDidLoad];
struct str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
printf("%d",func(10,20));
}

⚠️:有两个函数 A BA -> B,在B执行完后A传递给B的参数释放了么?
在上面的例子中910没有释放,相当于A的局部变量。

对应的汇编代码:

TestDemo`-[ViewController viewDidLoad]:
//函数开始
0x100c31ee4 <+0>: sub sp, sp, #0x60 ; =0x60
0x100c31ee8 <+4>: stp x29, x30, [sp, #0x50]
0x100c31eec <+8>: add x29, sp, #0x50 ; =0x50

//参数入栈
0x100c31ef0 <+12>: stur x0, [x29, #-0x8]
0x100c31ef4 <+16>: stur x1, [x29, #-0x10]
//x8获取参数x0
0x100c31ef8 <+20>: ldur x8, [x29, #-0x8]
//x9指向 x29 - 0x20
0x100c31efc <+24>: sub x9, x29, #0x20 ; =0x20
//x8 存入 x29 - 0x20
0x100c31f00 <+28>: stur x8, [x29, #-0x20]

//address page 内存中取数据
0x100c31f04 <+32>: adrp x8, 4
0x100c31f08 <+36>: add x8, x8, #0x418 ; =0x418
//x8 所指的内存取出来
0x100c31f0c <+40>: ldr x8, [x8]
0x100c31f10 <+44>: str x8, [x9, #0x8]
0x100c31f14 <+48>: adrp x8, 4
0x100c31f18 <+52>: add x8, x8, #0x3e8 ; =0x3e8
0x100c31f1c <+56>: ldr x1, [x8]
0x100c31f20 <+60>: mov x0, x9
0x100c31f24 <+64>: bl 0x100c32584 ; symbol stub for: objc_msgSendSuper2
//x8指向 sp + 0x8
0x100c31f28 <+68>: add x8, sp, #0x8 ; =0x8
0x100c31f2c <+72>: mov w0, #0x1
0x100c31f30 <+76>: mov w1, #0x2
0x100c31f34 <+80>: mov w2, #0x3
0x100c31f38 <+84>: mov w3, #0x4
0x100c31f3c <+88>: mov w4, #0x5
0x100c31f40 <+92>: mov w5, #0x6
0x100c31f44 <+96>: mov w6, #0x7
0x100c31f48 <+100>: mov w7, #0x8
//sp的值给x9
0x100c31f4c <+104>: mov x9, sp
//9 w10
0x100c31f50 <+108>: mov w10, #0x9
//w10写入 x9 所指向的地址
0x100c31f54 <+112>: str w10, [x9]
//10 w10
0x100c31f58 <+116>: mov w10, #0xa
//w10写入 x9 所指向的地址 偏移4个字节
0x100c31f5c <+120>: str w10, [x9, #0x4]
//跳转getStr
0x100c31f60 <+124>: bl 0x100c31e58 ; getStr at ViewController.m:31

//函数结束
-> 0x100c31f64 <+128>: ldp x29, x30, [sp, #0x50]
0x100c31f68 <+132>: add sp, sp, #0x60 ; =0x60
0x100c31f6c <+136>: ret
str:

TestDemo`getStr:
//开辟空间
0x100c31e58 <+0>: sub sp, sp, #0x30 ; =0x30
//从上一个栈空间 获取9 10
0x100c31e5c <+4>: ldr w9, [sp, #0x30]
0x100c31e60 <+8>: ldr w10, [sp, #0x34]
//参数入栈
0x100c31e64 <+12>: str w0, [sp, #0x2c]
0x100c31e68 <+16>: str w1, [sp, #0x28]
0x100c31e6c <+20>: str w2, [sp, #0x24]
0x100c31e70 <+24>: str w3, [sp, #0x20]
0x100c31e74 <+28>: str w4, [sp, #0x1c]
0x100c31e78 <+32>: str w5, [sp, #0x18]
0x100c31e7c <+36>: str w6, [sp, #0x14]
0x100c31e80 <+40>: str w7, [sp, #0x10]
0x100c31e84 <+44>: str w9, [sp, #0xc]
0x100c31e88 <+48>: str w10,[sp, #0x8]

//获取参数分别存入上一个栈x8所指向的地址中
-> 0x100c31e8c <+52>: ldr w9, [sp, #0x2c]
0x100c31e90 <+56>: str w9, [x8]
0x100c31e94 <+60>: ldr w9, [sp, #0x28]
0x100c31e98 <+64>: str w9, [x8, #0x4]
0x100c31e9c <+68>: ldr w9, [sp, #0x24]
0x100c31ea0 <+72>: str w9, [x8, #0x8]
0x100c31ea4 <+76>: ldr w9, [sp, #0x20]
0x100c31ea8 <+80>: str w9, [x8, #0xc]
0x100c31eac <+84>: ldr w9, [sp, #0x1c]
0x100c31eb0 <+88>: str w9, [x8, #0x10]
0x100c31eb4 <+92>: ldr w9, [sp, #0x18]
0x100c31eb8 <+96>: str w9, [x8, #0x14]
0x100c31ebc <+100>: ldr w9, [sp, #0x14]
0x100c31ec0 <+104>: str w9, [x8, #0x18]
0x100c31ec4 <+108>: ldr w9, [sp, #0x10]
0x100c31ec8 <+112>: str w9, [x8, #0x1c]
0x100c31ecc <+116>: ldr w9, [sp, #0xc]
0x100c31ed0 <+120>: str w9, [x8, #0x20]
0x100c31ed4 <+124>: ldr w9, [sp, #0x8]
0x100c31ed8 <+128>: str w9, [x8, #0x24]
//恢复栈
0x100c31edc <+132>: add sp, sp, #0x30 ; =0x30
0x100c31ee0 <+136>: ret



和之前的猜测相符。

函数的局部变量


int func1(int a, int b) {
int c = 6;
return a + b + c;
}

- (void)viewDidLoad {
[super viewDidLoad];
func1(10, 20);
}

对应的汇编指令:

TestDemo`func1:
-> 0x104bc5e40 <+0>: sub sp, sp, #0x10 ; =0x10
0x104bc5e44 <+4>: str w0, [sp, #0xc]
0x104bc5e48 <+8>: str w1, [sp, #0x8]
//局部变量c存入自己的栈区
0x104bc5e4c <+12>: mov w8, #0x6
0x104bc5e50 <+16>: str w8, [sp, #0x4]
0x104bc5e54 <+20>: ldr w8, [sp, #0xc]
0x104bc5e58 <+24>: ldr w9, [sp, #0x8]
0x104bc5e5c <+28>: add w8, w8, w9
0x104bc5e60 <+32>: ldr w9, [sp, #0x4]
0x104bc5e64 <+36>: add w0, w8, w9
0x104bc5e68 <+40>: add sp, sp, #0x10 ; =0x10
0x104bc5e6c <+44>: ret
函数的局部变量放在栈里面!(自己的栈)
那么有嵌套调用呢?

int func1(int a, int b) {
int c = 6;
int d = func2(a, b, c);
int e = func2(a, b, c);
return d + e;
}

int func2(int a, int b, int c) {
int d = a + b + c;
printf("%d",d);
return d;
}

- (void)viewDidLoad {
[super viewDidLoad];
func1(10, 20);
}
对应的汇编:

TestDemo`func1:
//函数的开始
-> 0x100781d9c <+0>: sub sp, sp, #0x30 ; =0x30
0x100781da0 <+4>: stp x29, x30, [sp, #0x20]
0x100781da4 <+8>: add x29, sp, #0x20 ; =0x20

//参数入栈
0x100781da8 <+12>: stur w0, [x29, #-0x4]
0x100781dac <+16>: stur w1, [x29, #-0x8]

//局部变量入栈
0x100781db0 <+20>: mov w8, #0x6
0x100781db4 <+24>: stur w8, [x29, #-0xc]

//读取参数和局部变量
0x100781db8 <+28>: ldur w0, [x29, #-0x4]
0x100781dbc <+32>: ldur w1, [x29, #-0x8]
0x100781dc0 <+36>: ldur w2, [x29, #-0xc]

//执行func2
0x100781dc4 <+40>: bl 0x100781df8 ; func2 at ViewController.m:86
//func2 返回值入栈
0x100781dc8 <+44>: str w0, [sp, #0x10]

//读取参数和局部变量
0x100781dcc <+48>: ldur w0, [x29, #-0x4]
0x100781dd0 <+52>: ldur w1, [x29, #-0x8]
0x100781dd4 <+56>: ldur w2, [x29, #-0xc]

//第二次执行func2
0x100781dd8 <+60>: bl 0x100781df8 ; func2 at ViewController.m:86

//func2 返回值入栈
0x100781ddc <+64>: str w0, [sp, #0xc]

//读取两次 func2 返回值
0x100781de0 <+68>: ldr w8, [sp, #0x10]
0x100781de4 <+72>: ldr w9, [sp, #0xc]
//相加存入w0返回上层函数
0x100781de8 <+76>: add w0, w8, w9

//函数的结束
0x100781dec <+80>: ldp x29, x30, [sp, #0x20]
0x100781df0 <+84>: add sp, sp, #0x30 ; =0x30
0x100781df4 <+88>: ret
可以看到参数被保存到栈中。
⚠️:现场保护包含:FPLR参数返回值

总结

    • 是一种具有特殊的访问方式的存储空间(后进先出,LIFO)
    • SP和FP寄存器
      • sp寄存器在任意时刻保存栈顶的地址
      • fp(x29)寄存器属于通用寄存器,在某些时刻利用它保存栈底的地址(嵌套调用)
    • ARM64里面栈的操作16字节对齐
    • 栈读写指令
      • 读:ldr(load register)指令LDR、LDP
      • 写:str(store register)指令STR、STP
    • 汇编练习
      • 指令:
        • sub sp, sp,#0x10 ;拉伸栈空间16个字节
        • stp x0,x1,[sp];往sp所在位置存放x0和x1
        • ldp x0,x1,[sp];读取sp存入x0和x1
        • add sp,#0x10;恢复栈空间
      • 简写:
        • stp x0, x1,[sp,#-0x10]!;前提条件是正好开辟的空间放满栈。先开辟空间,存入值,再改变sp的值。
        • ldp x0,x1,[sp],#0x10
  • bl指令
    • 跳转指令:bl标号,转到标号处执行指令并将下一条指令的地址保存到lr寄存器
    • B代表跳转
    • L代表lr(x30)寄存器
  • ret指令
    • 类似函数中的return
    • 让CPU执行lr寄存器所指向的指令
    • 有跳转需要“保护现场”
  • 函数
    • 函数调用栈
      • ARM64中栈是递减栈,向低地址延伸的栈
      • SP寄存器指向栈顶的位置
      • X29(FP)寄存器指向栈底的位置
    • 函数的参数
      • ARM64中,默认情况下参数是放在X0~X7的8个寄存器中
      • 如果是浮点数,会用浮点寄存器
      • 如果超过8个参数会用栈传递(多过8个的参数在函数调用结束后参数不会释放,相当于局部变量,属于调用方,只有调用方函数执行结束栈平衡后才释放。)
    • 函数的返回值
      • 一般情况下函数的返回值使用X0寄存器保存
      • 如果返回值大于了8个字节(放不下),就会利用内存。写入上一个调用栈内部,用X8寄存器作为参照。
    • 函数的局部变量
      • 使用栈保存局部变量
    • 函数的嵌套调用
      • 会将X29,X30寄存器入栈保护。
      • 同时现场保护的还有:FP,LR,参数,返回值。


收起阅读 »

汇编-函数本质(上)

栈函数调用栈恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。内存读写指令⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x02和0x03。str(store register)指...
继续阅读 »


栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)


SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址。
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!(没有出现函数嵌套调用的时候不需要fp,相当于分界点)
    ⚠️:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stpARM64里面 对栈的操作是16字节对齐的!!

ARM64是先开辟一段栈空间,fp移动到栈顶再往栈中存放内容(编译期就已经确定大小)。不存在push操作。在iOS中栈是往低地址开辟空间




函数调用栈

常见的函数调用开辟和恢复的栈空间:

//开辟栈空间
sub sp, sp, #0x40 ; 拉伸0x4064字节)空间
stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
add x29, sp, #0x30 ; x29指向栈帧的底部
...
//恢复栈空间
ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
add sp, sp, #0x40 ;栈平衡
ret

恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。

内存读写指令

⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x020x03

str(store register)指令
将数据从寄存器中读出来,存到内存中。

ldr(load register)指令
将数据从内存中读出来,存到寄存器中。

ldr 和 str 的变种 ldp 和 stp 还可以操作2个寄存器。


堆栈操作案例

使用32个字节空间作为这段程序的栈空间,然后利用栈将x0x1的值进行交换。

.text
.global _C

_C:
sub sp, sp, #0x20 ;拉伸栈空间32个字节
stp x0, x1, [sp, #0x10] ;sp 偏移 16字节存放 x0和x1 []的意思是寻址。这sp并没有改变
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0。这里内存相当于temp 交换了 x0 和 x1。寄存器中的值交换了,内存中的值不变。
add sp, sp, #0x20 ;恢复栈空间
ret
这段代码相当于 x0,x1遍历,sp和内存没有变。
栈空间分配:





断点调试

0x102e6e518断点处对x0x1分别赋值0xa0xb。然后单步执行:




拉伸后sp也变了。

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb)

看下0x000000016cf95b10的空间:




目前还没有写入内存,是脏数据。接着单步执行:



这个时候x0x1的数据完成了交换。内存的数据并没有变化。
继续单步执行:

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb)

sp还原了,栈空间释放,这时候0xa0xb还依然存在内存中,等待下次拉伸栈空间写数据覆盖:




bl和ret指令

bl标号

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处执行指令

b就是跳转,l将下一条指令的地址放入lr(x30)寄存器。


lr相当于保存的”回家的路“。


ret

  • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

ret只会看lr


ARM64平台的特色指令,它面向硬件做了优化处理。



x30寄存器

x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!
一个嵌套调用的案例,汇编代码如下:

.text
.global _C, _D

_C:
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
ret

_D:
mov x0,#0xbbbb
ret
ViewController.m中调用:

int C();
int D();
- (void)viewDidLoad {
[super viewDidLoad];
printf("C");
C();
printf("D");
}
C();打断点执行,进入C中:






继续执行发现一直在0x104c8e4f80x104c8e4fc中跳转返不回去viewDidLoad中了,发生了死循环。

->  0x104c8e4f8 <+8>:  mov    x0, #0xaaaa
0x104c8e4fc <+12>: ret
那么如果要返回,就必须将viewDidLoad中下一条指令告诉lr,这个时候就必须在bl之前保护lr寄存器(遇到bllr就会改变。需要保护“回家的路”)。那么这个时候能不能把lr保存到其它寄存器?这里我们没法保证其它寄存器不会被使用。这个时候唯一属于当前函数的也就是自己的栈区了。保存到栈区应该就能解决了。
可以看下系统是怎么实现的,写一个c函数断点调试看下:

void c() {
d();
return;;
}

void d() {

}

- (void)viewDidLoad {
[super viewDidLoad];
c();
}

系统的实现如下:



TestDemo`c:
//边开辟空间边写入 x29(fp) x30(lr) 的值。[sp, #-0x10]! !代表赋值给sp,相当于 sp -= 0x10
-> 0x102a21e84 <+0>: stp x29, x30, [sp, #-0x10]!
0x102a21e88 <+4>: mov x29, sp
0x102a21e8c <+8>: bl 0x102a21e98 ; d at ViewController.m:34:1
//将sp所指向的地址读取给x29,x30。[sp], #0x10 等价于 sp += 0x10
0x102a21e90 <+12>: ldp x29, x30, [sp], #0x10
0x102a21e94 <+16>: ret

可以看到系统先开辟栈空间,然后将x29x30寄存器的值存入栈区。在ret之前恢复x29x30的值。

  • stp x29, x30, [sp, #-0x10]!:开辟空间并将x29x30存入栈区。!代表赋值给sp,相当于 sp -= 0x10
  • ldp x29, x30, [sp], #0x10:将栈区的值给x29x30并回收空间。[sp], #0x10 等价于 sp += 0x10

那么对于CD的案例自己实现下保存和恢复lr寄存器。


.text
.global _C, _D

_C:
//sub sp,sp,#0x10
//str x30,[sp] ;等价
str x30, [sp,#-0x10]! ;16字节对齐,必须最小0x10
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
//ldr x30,[sp]
//add sp,#0x10 ;等价
ldr x30,[sp],#0x10
ret

_D:
mov x0,#0xbbbb
ret



这个时候进入Dlr值已经发生变化。



继续执行正常返回viewDidload了,这个时候死循环就已经解决了。

⚠️:在函数嵌套调用的时候,需要将x30入栈!开辟空间需要16字节对齐。如果开辟8字节再读的时候会坏地址访问。写的时候没问题。




函数的参数和返回值

先看下系统的实现:

int sum(int a, int b) {
return a + b;
}

- (void)viewDidLoad {
[super viewDidLoad];
sum(10,20);
}




可以看到变量1020分别存入了w0w1
sum调用如下(release模式下编译器会优化):

TestDemo`sum:
//开辟空间
-> 0x100121e68 <+0>: sub sp, sp, #0x10 ; =0x10
//w0 w1 存入栈中
0x100121e6c <+4>: str w0, [sp, #0xc]
0x100121e70 <+8>: str w1, [sp, #0x8]
//从栈中读取参数
0x100121e74 <+12>: ldr w8, [sp, #0xc]
0x100121e78 <+16>: ldr w9, [sp, #0x8]
//参数相加存入w0
0x100121e7c <+20>: add w0, w8, w9
//恢复栈空间
0x100121e80 <+24>: add sp, sp, #0x10 ; =0x10
//返回
0x100121e84 <+28>: ret
从上面可以看出返回值在w0中。那么自己实现sum函数的汇编代码:

.text
.global _suma

_suma:
add x0,x0,x1
ret
调用:

int suma(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
printf("%d",suma(10,20));
}

⚠️ARM64下,函数的参数是存放在X0X7(W0W7)这8个寄存器里面的。如果超过8个参数就会入栈。那么oc的方法最好不要超过6个(selfcmd)。
函数的返回值是放在X0寄存器里面的。


参数超过8个


int test(int a, int b, int c ,int d, int e, int f, int g, int h, int i) {
return a + b + c + d + e + f + g + h + i;
}

- (void)viewDidLoad {
[super viewDidLoad];
test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}




可以看到前8个参数分别保存在w0~w7寄存器中,第9个参数先保存在w10中,然后写入x8中(这个时候x8指向sp,相当于第9个参数写入了当前函数栈中)。


TestDemo`-[ViewController viewDidLoad]:
//拉伸栈空间,保存fp lr
0x100f09e5c <+0>: sub sp, sp, #0x40 ; =0x40
0x100f09e60 <+4>: stp x29, x30, [sp, #0x30]

//fp指向 sp+0x30
0x100f09e64 <+8>: add x29, sp, #0x30 ; =0x30
//fp-0x8 存放x0
0x100f09e68 <+12>: stur x0, [x29, #-0x8]
//fp-0x10 存放x1
0x100f09e6c <+16>: stur x1, [x29, #-0x10]
//fp-0x8 给到 x8
0x100f09e70 <+20>: ldur x8, [x29, #-0x8]
//sp+0x10 指针给到 x9
0x100f09e74 <+24>: add x9, sp, #0x10 ; =0x10
//x8写入 sp+0x10
0x100f09e78 <+28>: str x8, [sp, #0x10]

//adrp = address page 内存中取数据
0x100f09e7c <+32>: adrp x8, 4
0x100f09e80 <+36>: add x8, x8, #0x418 ; =0x418
//x8所指向的内容去出来
0x100f09e84 <+40>: ldr x8, [x8]
//x8写入栈中,这个时候x9指向地址,这个时候是一个新的x8
0x100f09e88 <+44>: str x8, [x9, #0x8]
0x100f09e8c <+48>: adrp x8, 4
0x100f09e90 <+52>: add x8, x8, #0x3e8 ; =0x3e8
0x100f09e94 <+56>: ldr x1, [x8]
0x100f09e98 <+60>: mov x0, x9
0x100f09e9c <+64>: bl 0x100f0a568 ; symbol stub for: objc_msgSendSuper2

//sp 一直没有改变过,w0~w7 分别存放前8个参数
0x100f09ea0 <+68>: mov w0, #0x1
0x100f09ea4 <+72>: mov w1, #0x2
0x100f09ea8 <+76>: mov w2, #0x3
0x100f09eac <+80>: mov w3, #0x4
0x100f09eb0 <+84>: mov w4, #0x5
0x100f09eb4 <+88>: mov w5, #0x6
0x100f09eb8 <+92>: mov w6, #0x7
0x100f09ebc <+96>: mov w7, #0x8
//x8 指向 sp
-> 0x100f09ec0 <+100>: mov x8, sp
//参数 9 存入 w10
0x100f09ec4 <+104>: mov w10, #0x9
//w10 存入 x8地址中,也就是sp栈底中
0x100f09ec8 <+108>: str w10, [x8]

0x100f09ecc <+112>: bl 0x100f09de4 ; test at ViewController.m:41
0x100f09ed0 <+116>: ldp x29, x30, [sp, #0x30]
0x100f09ed4 <+120>: add sp, sp, #0x40 ; =0x40
0x100f09ed8 <+124>: ret



接着往下直接跳转到test函数中:

TestDemo`test:
//开辟空间48字节
0x100f09de4 <+0>: sub sp, sp, #0x30 ; =0x30

//从viewDidLoad栈中取数据 第9个参数(读写往高地址)
0x100f09de8 <+4>: ldr w8, [sp, #0x30]

//参数入栈,分别占4个字节
0x100f09dec <+8>: str w0, [sp, #0x2c]
0x100f09df0 <+12>: str w1, [sp, #0x28]
0x100f09df4 <+16>: str w2, [sp, #0x24]
0x100f09df8 <+20>: str w3, [sp, #0x20]
0x100f09dfc <+24>: str w4, [sp, #0x1c]
0x100f09e00 <+28>: str w5, [sp, #0x18]
0x100f09e04 <+32>: str w6, [sp, #0x14]
0x100f09e08 <+36>: str w7, [sp, #0x10]
0x100f09e0c <+40>: str w8, [sp, #0xc]

-> 0x100f09e10 <+44>: ldr w8, [sp, #0x2c]
0x100f09e14 <+48>: ldr w9, [sp, #0x28]
0x100f09e18 <+52>: add w8, w8, w9
0x100f09e1c <+56>: ldr w9, [sp, #0x24]
0x100f09e20 <+60>: add w8, w8, w9
0x100f09e24 <+64>: ldr w9, [sp, #0x20]
0x100f09e28 <+68>: add w8, w8, w9
0x100f09e2c <+72>: ldr w9, [sp, #0x1c]
0x100f09e30 <+76>: add w8, w8, w9
0x100f09e34 <+80>: ldr w9, [sp, #0x18]
0x100f09e38 <+84>: add w8, w8, w9
0x100f09e3c <+88>: ldr w9, [sp, #0x14]
0x100f09e40 <+92>: add w8, w8, w9
0x100f09e44 <+96>: ldr w9, [sp, #0x10]
0x100f09e48 <+100>: add w8, w8, w9
0x100f09e4c <+104>: ldr w9, [sp, #0xc]
//最终相加结果给 w0
0x100f09e50 <+108>: add w0, w8, w9
//栈平衡
0x100f09e54 <+112>: add sp, sp, #0x30 ; =0x30
0x100f09e58 <+116>: ret



最终函数返回值放入w0中,如果在release模式下test不会被调用(被优化掉,因为没有意义,有没有对app没有影响。)

自己实现一个简单有参数并且嵌套调用的汇编:


.text
.global _func,_sum

_func:
//sub sp,sp,#0x10
//stp x29,x30,[sp]
stp x29,x30,[sp, #-0x10]!
bl _sum
//ldp x29,x30,[sp]
//add sp,sp,#0x10
ldp x29,x30,[sp],#0x10
ret
_sum:
add x0,x0,x1
ret


篇幅限制 分为2篇

作者:HotPotCat
链接:https://www.jianshu.com/p/69b9c49b0e71




收起阅读 »

汇编-基本概念

在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。汇编语言的发展机器语言由0...
继续阅读 »


在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。


汇编语言的发展

机器语言

01组成的机器指令。0代表有电,1代表没电。

  • 加:0100 0000
  • 减:0100 1000
  • 乘:1111 0111 1110 0000
  • 除:1111 0111 1111 0000

汇编语言(assembly language)

为了高效的写代码出现了助记符,使用助记符代替机器语言,如:

  • 加:INC EAX 通过编译器 0100 0000
  • 减:DEC EAX 通过编译器 0100 1000
  • 乘:MUL EAX 通过编译器 1111 0111 1110 0000
  • 除:DIV EAX 通过编译器 1111 0111 1111 0000

助记符就是汇编语言的前身,当有专门的编译器出现的时候就有了汇编语言。

高级语言(High-level programming language)

C\C++\Java\OC\Swift,更加接近人类的自然语言。
比如C语言:

  • 加:A + B 通过编译器 0100 0000
  • 减:A - B 通过编译器 0100 1000
  • 乘:A * B 通过编译器 1111 0111 1110 0000
  • 除:A / B 通过编译器 1111 0111 1111 0000

代码在终端设备上的过程:





  • 汇编语言机器语言一一对应,每一条机器指令都有与之对应的汇编指令
  • 汇编语言可以通过编译得到机器语言机器语言可以通过反汇编得到汇编语言
  • 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言(不是一一对应关系,反推出是不准确的,只能大致。)

汇编语言的特点


  • 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
  • 能够不受编译器的限制,对生成的二进制代码进行完全的控制
  • 目标代码简短,占用内存少,执行速度快
  • 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
  • 开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
  • 不区分大小写,比如movMOV是一样的

汇编的用途

  • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
  • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编
  • 软件安全
    1.病毒分析与防治
    2.逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
  • 理解整个计算机系统的最佳起点和最有效途径
  • 为编写高效代码打下基础
  • 弄清代码的本质

汇编语言的种类

目前讨论比较多的汇编语言有:

  • 8086汇编(8086处理器是16bitCPU
  • Win32汇编
  • Win64汇编
  • ARM汇编(嵌入式、MaciOS
  • ......

iPhone里面用到的是ARM汇编,但是不同的设备也有差异(因CPU的架构不同)。

位数架构设备
32armv6iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch
32armv7iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4
32armv7siPhone5, iPhone5C, iPad4(iPad with Retina Display)
64arm64iPhone5s,iPhone6、7、8,iPhone6、7、8 Plus,iPhone X,iPad Air,iPad mini2(iPad mini with Retina Display)
64arm64eXS/XS Max/XR/ iPhone 11, iPhone 11 pro 及以后
64x86_64模拟器64位处理器 (intel)
32i386模拟器32位处理器(intel)

⚠️:苹果A7处理器支持两个不同的指令集:32ARM指令集(armv6|armv7|armv7s)和64ARM指令集(arm64

汇编相关的学习需要了解CPU等硬件结构,最为重要的是CPU/内存。在汇编中,大部分指令都是和CPU与内存相关的。
APP/程序的执行过程:





执行过程:
1.地址总线先去内存地址。
2.控制读取发送读/写命令。
3.数据总线写数据->内存/ 内存发送数据->数据总线

地址总线

  • 它的宽度决定了CPU的寻址能力(也就是寻址范围)
  • 8086的地址总线宽度是20,所以寻址能力是1M( 220)(这里的M是大小,数量单位)




内存中的MB是容量单位。如果内存很大, 地址总线宽度不够怎么处理?以前的cpu是通过2次寻址相加得到一个最终的值来访问内存,现在的cpu没有寻址能力的问题。
数量单位:M,K。1M = 1024K,1K= 1024。比如:10,100
容量单位:字节Byte。 1024B = 1KB,1024KB = 1MB。比如:10个,100只。(大部分计算机都是以1个字节为单位。银行系统的IBM电脑例外是2个字节为单位。)
对于100M 宽带,这里的100M是100Mbps(每秒钟传递多少二进制位,bit位。所以100M带宽理论下载速度12.5MB/s)。

数据总线

  • 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度(吞吐量)
  • 8086的数据总线宽度是16,所以单次最大传递2个字节的数据

我们现在常说的32位,64位cpu说的就是它的数据吞吐量。1次放电分别4字节,8字节数据。

控制总线

  • 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制

案例:
1.一个CPU 的寻址能力为8KB,那么它的地址总线的宽度为____
答案:8KB对应 8192, 213 = 8192 所以为13。

  1. 8080,8088,80286,80386 的地址总线宽度分别为16根,20根,24根,32根。那么他们的寻址能力分别为多少____KB, ____MB,____MB,____GB?
    答案:1kb = 210 = 1024
    1kb * 26 = 64kb
    1kb * 1kb = 1mb
    1mb * 24 = 16mb
    1kb * 1kb * 1kb * 22 = 4gb

  2. 8080,8088,8086,80286,80386 的数据总线宽度分别为8根,8根,16根,16根,32根.那么它们一次可以传输的数据为:____B,____B,____B,____B,____B
    答案:1 、1、2、2、4

4.从内存中读取1024字节的数据,8086至少要读____次,80386至少要读取____次.
答案:8086 数据总线宽度为16。8086一次读2个字节,那么需要512次,80286数据总线宽度为32,一次4个字节,需要256次。

内存




  • 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位220个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
  • 0x00000~0x9FFFF:主存储器。可读可写
  • 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
  • 0xC0000~0xFFFFF:存储各种硬件\系统信息。只读

  • 进制

    想学好进制首先要忘掉十进制,也要忘掉进制间的转换。

    进制的定义

    • 八进制由8个符号组成:0 1 2 3 4 5 6 7 逢八进一
    • 十进制由10个符号组成:0 1 2 3 4 5 6 7 8 9逢十进一
    • N进制就是由N个符号组成:逢N进一

    ⚠️:进制的本质是符号。

    案例

    1. 1 + 1 在____情况下等于 3 ?
      除了算错的情况下。在十进制由10个符号组成,假如由: 0 1 3 2 8 A B E S 7组成逢十进一,那么在这种情况下1+1=3

    传统定义的十进制和自定义的十进制不一样。那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的,可以用于加密!
    ⚠️:十进制由十个符号组成,逢十进一,符号是可以自定义的!!!

    1. 八进制运算:
    • 2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
      答案:5,6,11,24
    • 277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .
      答案:632, 20250, 163,47


    八进制加法表
    0 1 2 3 4 5 6 7
    10 11 12 13 14 15 16 17
    20 21 22 23 24 25 26 27
    ...

    1+1 = 2                     
    1+2 = 3 2+2 = 4
    1+3 = 4 2+3 = 5 3+3 = 6
    1+4 = 5 2+4 = 6 3+4 = 7 4+4 = 10
    1+5 = 6 2+5 = 7 3+5 = 10 4+5 = 11 5+5 = 12
    1+6 = 7 2+6 = 10 3+6 = 11 4+6 = 12 5+6 = 13 6+6 = 14
    1+7 = 10 2+7 = 11 3+7 = 12 4+7 = 13 5+7 = 14 6+7 = 15 7+7 = 16

    八进制乘法表
    0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27...

    1*1 = 1                     
    1*2 = 2 2*2 = 4
    1*3 = 3 2*3 = 6 3*3 = 11
    1*4 = 4 2*4 = 10 3*4 = 14 4*4 = 20
    1*5 = 5 2*5 = 12 3*5 = 17 4*5 = 24 5*5 = 31
    1*6 = 6 2*6 = 14 3*6 = 22 4*6 = 30 5*6 = 36 6*6 = 44
    1*7 = 7 2*7 = 16 3*7 = 25 4*7 = 34 5*7 = 43 6*7 = 52 7*7 = 61

    二进制的简写形式

                   二进制: 1 0 1 1 1 0 1 1 1 1 0 0
    三个二进制一组: 101 110 111 100
                    八进制:    5     6     7      4
    四个二进制一组: 1011 1011 1100
                十六进制:     b        b       c

    二进制:从 0 写到 1111
    0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
    这种二进制使用起来太麻烦,改成更简单一点的符号:
    0 1 2 3 4 5 6 7 8 9 A B C D E F 这就是十六进制了

    数据的宽度

    数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。


    int test() {
    int cTemp = 0x1FFFFFFFF;
    return cTemp;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%x",test());
    }
    输出:

    ffffffff
    数据溢出了。刚开始cTemp默认值1,溢出后变为-1第一位符号位,1代表负数,0代表正数。往后逐位取反,末尾加1。)。

    (lldb) p cTemp
    (int) $0 = 1
    (lldb) p cTemp
    (int) $1 = -1
    (lldb) p &cTemp
    (int *) $2 = 0x000000016b3a9b1c
    (lldb) x 0x000000016b3a9b1c
    0x16b3a9b1c: ff ff ff ff 10 00 00 00 00 00 00 00 ef 98 3a 6b ..............:k
    0x16b3a9b2c: 01 00 00 00 70 a9 f0 59 01 00 00 00 50 d4 a5 04 ....p..Y....P...
    (lldb) p (uint)cTemp
    (uint) $3 = 4294967295

    Debug -> Debug Workflow -> View Memory中也可以查看(这里查看内容更新后需要翻页刷新然后切换回来才能显示新值):




    再看下汇编代码(Debug -> Debug Workflow -> Always Show Disassembly):


    TestDemo`test:
    0x104a59ec8 <+0>: sub sp, sp, #0x10 ; =0x10
    0x104a59ecc <+4>: mov w8, #-0x1
    0x104a59ed0 <+8>: str w8, [sp, #0xc]
    -> 0x104a59ed4 <+12>: ldr w0, [sp, #0xc]
    0x104a59ed8 <+16>: add sp, sp, #0x10 ; =0x10
    0x104a59edc <+20>: ret

    可以看到直接将-1给力w8。指令在内存中占用4字节。

    计算机中常见的数据宽度

    • 位(Bit): 1个位就是1个二进制位。0或者1
    • 字节(Byte): 1个字节由8个Bit组成(8位)。内存中的最小单元Byte
    • 字(Word): 1个字由2个字节组成(16位),这2个字节分别称为高字节低字节
    • 双字(Doubleword): 1个双字由两个字组成(32位)。

    计算机存储数据会分为有符号数和无符号数(对于数据本身内容没有变化,取决于你怎么看):


    无符号数,直接换算!
    有符号数:
    正数: 0 1 2 3 4 5 6 7
    负数: F E D B C A 9 8
    -1 -2 -3 -4 -5 -6 -7 -8

    自定义进制符号

    案例:

    • 现在有10进制数10个符号分别是:2,9,1,7,6,5,4, 8,3 , A 逢10进1 那么: 123 + 234 = ____

    十进制:
    0 1 2 3 4 5 6 7 8 9
    自定义:
    2 9 1 7 6 5 4 8 3 A
    92 99 91 97 96 95 94 98 93 9A
    12 19 11 17 16 15 14 18 13 1A
    72 79 71 77 76 75 74 78 73 7A
    62 69 61 67 66 65 64 68 63 6A
    52 59 51 57 56 55 54 58 53 5A
    42 49 41 47 46 45 44 48 43 4A
    82 89 81 87 86 85 84 88 83 8A
    32 39 31 37 36 35 34 38 33 3A
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = A 5+5 = 92
    9+4 = 8 1+4 = 3 7+4 = a 6+4 = 92 5+4 = 99 4+4 = 91
    9+8 = 3 1+8 = A 7+8 = 92 6+8 = 99 5+8 = 91 4+8 = 97 8+8 = 96
    9+3 = A 1+3 = 92 7+3 = 99 6+3 = 91 5+3 = 97 4+3 = 96 8+3 = 95 3+3 = 94
    9+A = 92 1+A = 99 7+A = 91 6+A = 97 5+A = 96 4+A = 95 8+A = 94 3+A = 98 A+A = 93

    123 + 234 = 1A6

    • 现在有9进制数 9个符号分别是:2,9,1,7,6,5,4, 8,3 逢9进1 那么: 123 + 234 = __

    十进制:
    0 1 2 3 4 5 6 7 8
    自定义:
    2 9 1 7 6 5 4 8 3
    92 99 91 97 96 95 94 98 93
    12 19 11 17 16 15 14 18 13
    72 79 71 77 76 75 74 78 73
    62 69 61 67 66 65 64 68 63
    52 59 51 57 56 55 54 58 53
    42 49 41 47 46 45 44 48 43
    82 89 81 87 86 85 84 88 83
    32 39 31 37 36 35 34 38 33
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = 92 5+5 = 99
    9+4 = 8 1+4 = 3 7+4 = 92 6+4 = 99 5+4 = 91 4+4 = 97
    9+8 = 3 1+8 = 92 7+8 = 99 6+8 = 91 5+8 = 97 4+8 = 96 8+8 = 95
    9+3 = 92 1+3 = 99 7+3 = 91 6+3 = 97 5+3 = 96 4+3 = 95 8+3 = 94 3+3 = 98

    123 + 234 = 725

    CPU&寄存器

    内部部件之间由总线连接


    CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。

    CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器

    对于arm64系的CPU来说, 如果寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的

    • 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
    • 不同的CPU,寄存器的个数、结构是不相同的

    浮点寄存器

    因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数。

    • 浮点寄存器 64位D0 - D31 32位: S0 - S31

    向量寄存器

    现在的CPU支持向量运算。(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

    • 向量寄存器 128位:V0-V31

    通用寄存器

    • 通用寄存器也称数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
    • ARM64拥有32个64位的通用寄存器x0x30,以及XZR(零寄存器),这些通用寄存器有时也有特定用途。
      1.64位X0-X30, XZR(零寄存器)w0 到 w28 这些是32位的。因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
      2.32位W0-W30, WZR(零寄存器)。 w0 就是 x0 的低32位!

    ⚠️:了解过8086汇编的都知道,有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中。在ARM中并没有。

    在"Xcode"中我们可以查看具体寄存器的内容:




    分别看一下x0w0的值:
    x0  unsigned long   0x0000000159f0a970
    w0 unsigned int 0x59f0a970

    验证了w0x0的低32位。

    通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
    假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间:




    pc寄存器

    单步执行汇编代码(pc始终指向下一条指令):




  • 为指令指针寄存器,它指示了CPU当前要读取指令的地址(指向下一条即将执行的指令
  • 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
  • CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义

  • 比如 1110 0000 0000 0011 0000 1000 1010 1010,
    可以当做数据 0xE003008AA。
    也可以当做指令 mov x0, x8

    • CPU根据什么将内存中的信息看做指令?

    CPU将pc指向的内存单元的内容看做指令
    如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过。

    高速缓存

    iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M。

    CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成)。CPU直接从高速缓存依次读取指令来执行。

    bl指令

    bl分位bl:
    b:跳转。
    l:lr寄存器。

    • CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令
    • ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如:
      mov x0,#10、mov x1,#20
    • 但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能
    • ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的是bl指令

    案例
    现在有两段代码!假设程序先执行A,请写出指令执行顺序。最终寄存器x0的值是多少?

    _A:
    mov x0,#0xa0
    mov x1,#0x00
    add x1, x0, #0x14
    mov x0,x1
    bl _B
    mov x0,#0x0
    ret

    _B:
    add x0, x0, #0x10
    ret

    分析:


    Xcode中创建Empty文件命名为asm.s.s汇编代码会被Xcode自动识别编译)。


    //asm.s
    .text // 告诉是代码
    .global _A, _B //.global 是标号

    _A:
    mov x0,#0xa0 //a0 给 x0 x0 = 0xa0
    mov x1,#0x00 //00 给x1 x1 = 0x00
    add x1, x0, #0x14 //x0 + 0x14 给 x1 x1 = 0xb4
    mov x0,x1 //x1 的值给 x0 x0 = 0xb4
    bl _B //跳转B
    mov x0,#0x0 //0x0 给 x0 x0 = 0x0
    ret //return 上层调用的地方

    _B:
    add x0, x0, #0x10 //x0 + 0x10 给 x0 x0 = 0xc4
    ret //return A

    oc调用汇编:

    //ViewController.m
    int A();

    - (void)viewDidLoad {
    [super viewDidLoad];
    A();
    }
    swift调用汇编:

    //声明方法A。Swift中C和汇编都可以这么暴露。
    @_silgen_name("A")
    func A()

    class ViewController: UIViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    A();
    }

    }
    答案:0x00



    断点验证了x0最终值为0x00。这里有个问题是发生死循环了。(bl跳转指令导致的,lr寄存器在跳转后需要保护现场还原。)


    总结

    • 汇编概述:
      • 使用助记符代替集齐指令的一种编程语言。
      • 汇编和及其指令是一一对应的关系,拿到二进制就可以反汇编。
      • 由于汇编和CPU指令集是对应的,所以汇编不具备移植性。
    • 总线:是一堆导线的集合
      • 地址总线:地址总线的宽度决定了寻址能力
      • 数据总线:数据总线的宽度决定了CPU的吞吐量
    • 进制
      • 任意进制都是由对应个数的符号组成的。符号可以自定义。
      • 2/8/16是相对完美的集智,他们之间的关系
        • 3个2进制使用一个8进制标识
        • 4个2进制使用一个16进制标识
        • 两个16进制位可以标识一个字节
      • 数量单位
        • 1024 = 1K;1024K = 1M;1024M = 1G
      • 容量单位
        • 1024B = 1KB;1024KB = 1MB; 1024MB = 1GB
        • B:byte(字节)1B = 8bit
        • bit(比特):一个二进制位
      • 数据的宽度
        • 计算机中的数据是有宽度的,超过了就会溢出
    • 寄存器:CPU为了性能,在内部开辟了一小块临时存储区域
      • 浮点向量寄存器
      • 异常状态寄存器
      • 通用寄存器:除了存放数据有时候也有特殊的用途
        • ARM64拥有32个64位的通用寄存器X0—X30以及XZR(令寄存器)
        • 为了兼容32位,所以ARM64拥有W0—W28\WZR 30个32位寄存器
        • 32位寄存器并不是独立存在的,比如W0是X0的低32位
      • PC寄存器:指令指针寄存器
        • PC寄存器里面的值保存的就是CPU接下来需要执行的指令地址!
        • 改变PC的值可以改变程序的执行流程!


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



    收起阅读 »

    iOS越狱

    一、概述越狱(jailBreak),通过iOS系统安全启动链漏洞,从而禁止掉信任链中负责验证的组件。拿到iOS系统最大权限ROOT权限。iOS系统安全启动链当启动一台iOS设备时,系统首先会从只读的ROM中读取初始化指令,也就是系统的引导程序(事实上所有的操作...
    继续阅读 »

    一、概述

    越狱(jailBreak),通过iOS系统安全启动链漏洞,从而禁止掉信任链中负责验证的组件。拿到iOS系统最大权限ROOT权限。

    iOS系统安全启动链
    当启动一台iOS设备时,系统首先会从只读的ROM中读取初始化指令,也就是系统的引导程序(事实上所有的操作系统启动时都要经过这一步,只是过程略有不同)。这个引导ROM包含苹果官方权威认证的公钥,他会验证底层启动加载器(LLB)的签名,一旦通过验证后就启动系统。LLB会做一些基础工作,然后验证第二级引导程序iBootiBoot启动后,设备就可以进入恢复模式或启动内核。在iBoot验证完内核签名的合法性之后,整个启动程序开始步入正轨:加载驱动程序、检测设备、启动系统守护进程。这个信任链会确保所有的系统组件都有苹果官方写入、签名、分发,不能来自第三方机构。


    越狱 的工作原理正是攻击这一信任链。所有的越狱工具的作者都需要找到这一信任链上的漏洞,从而禁止掉信任链中负责验证的组件。拿到iOS系统最大权限ROOT权限。

    熟悉越狱的都听说过 完美越狱 和 非完美越狱

    • 完美越狱:所谓完美越狱就是破解iOS系统漏洞之后,每次系统重启都能自动调用注入的恶意代码,达到破坏安全验证,再次获得ROOT权限。

    • 非完美越狱:所谓非完美越狱是指越狱系统后,并没有完全破解安全链,有部分信息或功能应用不佳;比如关机以后必须去连接越狱软件来引导开机;或者重启会导致越狱的失效;这样的越狱称为 不完美越狱

    目前iOS10以上没有完美越狱工具开放出来,iOS10以下有。目前比较靠谱的两个越狱工具:uncOver 和 Odyssey


    二、unc0ver越狱


    macOSunc0ver有3种越狱方式,这里使用Xcode重签名的方式越狱。其它方式参考官网方式就可以了。

    2.1 环境配置

    • Xcode
    • unc0ver
    • iOS App Signer(️:脚本/Monkey方式不需要这个)

    #1.网站下载
    https://dantheman827.github.io/ios-app-signer/
    #2.命令安装
    sudo gem install sigh
    • 设备 iPhone7 14.0(需要确保设备在自己的账号下)

    2.2 工程配置

    1.安装好Xcode并且新建一个iOS App
    确保自己的设备加入到自己的账号中(我这里使用免费账号)

    2.连接手机build新建的iOS App到设备
    在这个过程中需要手机信任证书(设置->通用->描述文件与设备管理


    2.3 方式一:iOS App Signer 重签名

    1.导出embedded.mobileprovision
    个人开发者账号有效期为7天,由于个人开发者账号苹果官网没有提供导出入口,需要build成功后在products app 中拷贝。如果有付费账号直接官网导出就可以了。



    2.Signer重签名
    Input File为要重签名的ipa包,这里是下载好的unc0ver,证书选择自己的证书(免费开发者账号也可以,有效期7天,前提是自己的设备已经加入免费账号并且导出.mobileprovision)。当然有企业证书是最好的。




    3.Xcode安装重签名后的unc0ver
    Xcode中打开Window → Devices and Simulatorscommand + shift +2),然后在Installed Apps中拖入重签名的unc0ver进行安装。


    4.打开unc0ver进行越狱
    越狱成功后桌面会出现CydiaSubstitute。没有出现的话uncover重新操作一遍。

    2.4 方式二:脚本重签名

    1.项目根目录下创建IPA文件夹并将unc0ver ipa包拷贝放到目录中




    2.根目录下创建appResign.sh重签名脚本
    脚本内容如下:

    # SRCROOT 为工程所在目录,Temp 为创建的临时存放 ipa 解压文件的文件夹。
    TEMP_PATH="${SRCROOT}/Temp"
    # APP 文件夹,存放要重签名的ipa包。
    IPA_PATH="${SRCROOT}/IPA"
    #重签名 ipa 包路径
    TARGRT_IPA_PATH="${IPA_PATH}/*.ipa"

    #清空 Temp 文件夹,重新创建目录
    rm -rf "$TEMP_PATH"
    mkdir -p "$TEMP_PATH"



    #1.解压 ipa 包到 Temp 目录下
    unzip -oqq "$TARGRT_IPA_PATH" -d "$TEMP_PATH"
    #获取解压后临时 App 路径
    TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
    echo "临时App路径:$TEMP_APP_PATH"

    #2.将解压出来的 .app 拷贝到工程目录,
    # BUILT_PRODUCTS_DIR 工程生成的App包路径
    # TARGET_NAME target 名称
    TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
    echo "app路径:$TARGET_APP_PATH"

    #删除工程自己创建的 app
    rm -rf "$TARGET_APP_PATH"
    mkdir -p "$TARGET_APP_PATH"
    #拷贝解压的临时 Temp 文件到工程目录
    cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"

    #3.删除 extension 和 WatchAPP。个人证书无法签名 Extention
    rm -rf "$TARGET_APP_PATH/PlugIns"
    rm -rf "$TARGET_APP_PATH/Watch"


    #4.更新 info.plist 文件 CFBundleIdentifier
    # 设置:"Set :KEY Value" "目标文件路径"
    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"


    #5. macho 文件加上可执行权限。
    #获取 macho 文件路径
    APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
    #加上可执行权限
    chmod +x "$TARGET_APP_PATH/$APP_BINARY"


    #6.重签名第三方 FrameWorks
    TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
    if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
    then
    for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
    do
    #签名
    /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
    done
    fi

    3.在Xcode工程中配置重签名脚本



    4.重新build工程到手机上。

    5.打开unc0ver进行越狱
    越狱成功后桌面会出现CydiaSubstitute。没有出现的话uncover重新操作一遍。

    2.5 方式三:MonkeyDev


    通过Monkey可以帮助我们自动重签名,只需要准备好要签名的包和配置好证书运行工程就可以了

    Settings -> Restore RootFS 可以恢复到未越狱状态(越狱相关的内容会被删干净)





    越狱前最好在设置中勾选OpenSSH选项,一个连接手机的工具。



    三、Odyssey越狱

    Odysseyunc0ver越狱流程差不多,推荐使用Monkey。区别是Odyssey安装好后的应用商店是Sileounc0verCydia。更推荐使用unc0ver

    ️越狱注意事项:

    • odyssey 越狱中断开网络开始执行越狱,等需要开启网络的时候再联网。
    • 两种越狱方式都在安装好包后断开Xcode连接再进行越狱操作。(Xcode启动应用是附加的状态)
    • 在越狱的过程中遇到任何错误重新恢复手机再尝试。
    • iOS10以下设备直接用爱思助手越狱。
    • 恢复和越狱出错的情况下请删除unc0ver重新安装尝试。

    总结

    越狱:通过破解iOS的安全启启动链的漏洞,拿到iOSRoot权限。

    • 完美越狱:每次系统重新启动都会再次进入越狱状态。
    • 非完美越狱:没有完全破解,一般重启以后会失去越狱环境。

    附系统查询:






















































    作者:HotPotCat
    链接:https://www.jianshu.com/p/2ded2dc425cc










    收起阅读 »

    什么是库(Library)?

    常见库文件格式:.a,.dylib,.framework,.xcframework,.tdb什么是库(Library)?库(Library)本质上就是一段编译好的二进制代码,加上头文件就可以供别人使用。应用场景?某些代码需要给别人使用,但是不希望别人看到源码,...
    继续阅读 »

    常见库文件格式:.a.dylib.framework.xcframework.tdb

    什么是库(Library)?

    库(Library)本质上就是一段编译好的二进制代码,加上头文件就可以供别人使用。

    应用场景?

    1. 某些代码需要给别人使用,但是不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。
    2. 对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。

    什么是链接(Link)?

    库在使用的时候需要链接(Link),链接 的方式有两种:

    1. 静态
    2. 动态

    静态库

    静态库即静态链接库:可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的文件。Windows 下的 .libLinux 和 Mac 下的 .aMac独有的.framework

    缺点: 浪费内存和磁盘空间,模块更新困难。

    静态库链接

    将一份AFNetworking静态库文件(.h头文件和.a组成)和test.m放到统一目录。test.m如下:

    #import <Foundation/Foundation.h>
    #import <AFNetworking.h>

    int main(){
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    NSLog(@"test----%@", manager);
    return 0;
    }


    直接终端查看下.a静态库究竟是什么。

    ➜  AFNetworking file libAFNetworking.a
    libAFNetworking.a: current ar archive

    可以看到.a实际上是一个文档格式。也就是.o文件的合集。可以通过ar命令验证下。

    ar -- create and maintain library archives

    ➜  AFNetworking ar -t libAFNetworking.a
    __.SYMDEF
    AFAutoPurgingImageCache.o
    AFHTTPSessionManager.o
    AFImageDownloader.o
    AFNetworkActivityIndicatorManager.o
    AFNetworking-dummy.o
    AFNetworkReachabilityManager.o
    AFSecurityPolicy.o
    AFURLRequestSerialization.o
    AFURLResponseSerialization.o
    AFURLSessionManager.o
    UIActivityIndicatorView+AFNetworking.o
    UIButton+AFNetworking.o
    UIImageView+AFNetworking.o
    UIProgressView+AFNetworking.o
    UIRefreshControl+AFNetworking.o
    WKWebView+AFNetworking.o
    确认.a确实是.o文件的合集。清楚了.a后将AFNetworking链接到test.m文件。
    1.通过clangtest.m编译成目标文件.o

    clang - the Clang C, C++, and Objective-C compiler
    DESCRIPTION
    clang is a C, C++, and Objective-C compiler which encompasses prepro-
    cessing, parsing, optimization, code generation, assembly, and linking.
    Depending on which high-level mode setting is passed, Clang will stop
    before doing a full link. While Clang is highly integrated, it is
    important to understand the stages of compilation, to understand how to
    invoke it. These stages are:
    Driver The clang executable is actually a small driver which controls
    the overall execution of other tools such as the compiler,
    assembler and linker. Typically you do not need to interact
    with the driver, but you transparently use it to run the other
    tools.
    通过man命令我们看到clangCC++OC编译器,是一个集合包含了预处理解析优化代码生成汇编化链接

    clang -x objective-c \
    -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -I ./AFNetworking \
    -c test.m -o test.o

    回车后就生成了test.o目标文件。

    \为了转译回车,让命令换行更易读。-x制定编译语言,-target指定编译平台,-fobjc-arc编译成ARC-isysroot指定用到的Foundation的路径,-I<directory>在指定目录寻找头文件 header search path

    为什么生成目标文件只需要告诉头文件的路径就可以了?
    因为在生成目标文件的时候,重定位符号表只需要记录哪个地方的符号需要重定位。在连接的时候链接器会自动重定位。(上面的例子中只需要保留AFHTTPSessionManager的符号。)
    2..o生成可执行文件

    clang -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -L./AFNetworking \
    -lAFNetworking \
    test.o -o test

    这个时候test可执行程序就生成了。

    -L要链接的库文件(libAFNetworking.a)目录,-l要链接的库文件(libAFNetworking.a)这里只写AFNetworking是有查找规则的:先找lib+<library_name>的动态库,找不到,再去找lib+<library_name>的静态库,还找不到,就报错。会自动去找libAFNetworking

    经过上面的编译和链接清楚了其它参数都是固定的,那么链接成功一个库文件有3个要素:
    1.  -I<directory> 在指定目录寻找头文件 header search path头文件
    2.  -L<dir> 指定库文件路径(.a\.dylib库文件) library search path库文件路径
    3.  -l<library_name> 指定链接的库文件名称(.a\.dylib库文件)other link flags -lAFNetworking (库文件名称

    生成静态库

    将自己的一个工程编译成.a静态库。工程只有一个文件HPExample``.h和 .m

    #import <Foundation/Foundation.h>

    @interface HPExample : NSObject

    - (void)hp_test:(_Nullable id)e;

    @end

    #import "HPExample.h"

    @implementation HPExample

    - (void)hp_test:(_Nullable id)e {
    NSLog(@"hp_test----");
    }

    @end

    HPExample.m编译成.o文件:

    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I./StaticLibrary \
    -c HPExample.m -o HPExample.o

    这个时候生成了HPExample.o文件,由于工程只有一个.o文件,直接将文件修改为libExample.dylib或者libHPExample.a
    然后创建一个test.m文件调用HPExample:


    #import <Foundation/Foundation.h>
    #import "HPExample.h"

    int main(){
    NSLog(@"testApp----");
    HPExample *manager = [HPExample new];
    [manager hp_test: nil];
    return 0;
    }

    test.m编译成test.o

    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I./StaticLibrary \
    > -c test.m -o test.o

    test.o链接HPExample

    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -L./StaticLibrary \
    -lHPExample \
    test.o -o test
    现在就已经生成了可执行文件test
    终端lldb执行test:

    ➜  staticLibraryCreat lldb
    (lldb) file test
    Current executable set to '/Users/binxiao/projects/library/staticLibraryCreat/test' (x86_64).
    (lldb) r
    Process 2148 launched: '/Users/binxiao/projects/library/staticLibraryCreat/test' (x86_64)
    2021-02-13 13:22:49.150091+0800 test[2148:13026772] testApp----
    2021-02-13 13:22:49.150352+0800 test[2148:13026772] hp_test----
    Process 2148 exited with status = 0 (0x00000000)
    这也从侧面印证了.a就是.o的合集。file test是创建一个targetr是运行的意思。
    接着再看下libHPExample.a文件。

    objdump --macho --private-header libHPExample.a
    Mach header
    magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
    MH_MAGIC_64 X86_64 ALL 0x00 OBJECT 4 1160 SUBSECTIONS_VIA_SYMBOLS
    确认还是一个目标文件。

    静态库的合并

    根据上面的分析,那么静态库的合并也就是将所有的.o放到一个文件中。
    有两个.a库:
    静态库的合并有两种方式:libAFNetworking.a,libSDWebImage.a
    1.ar -rc libAFNetworking.a libSDWebImage.a

    ar -rc libAFNetworking.a  libSDWebImage.a

    就相当于将后面的libSDWebImage.a合并到libAFNetworking.a

    2.libtool -static -o <OUTPUT NAME> <LIBRARY_1> <LIBRARY_2>
    libtool合并静态库。

    libtool -static \
    -o \
    libMerge.a \
    libAFNetworking.a \
    libSDWebImage.a
    //libAFNetworking.a要为目标文件路径,libMerge.a为输出文件
    这样就合并了libAFNetworking.alibSDWebImage.alibMerge.a了。在这个过程中libtool会先解压两个目标文件,然后合并。在合并的过程中有两个问题:
    1.冲突问题。
    2..h文件。

    clang提供了mudule可以预先把头文件(.h)预先编译成二进制缓存到系统目录中, 再去编译.m的时候就不需要再去编译.h了。

    LC_LINKER_OPTION链接器的特性,Auto-Link。启用这个特性后,当我们import <模块>,不需要我们再去往链接器去配置链接参数。比如import <framework>我们在代码里使用这个framework格式的库文件,那么在生成目标文件时,会自动在目标文件的Mach-O中,插入一个 load command格式是LC_LINKER_OPTION,存储这样一个链接器参数-framework <framework>

    动态库

    与静态库相反,动态库在编译时并不会被拷⻉到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。格式有:.framework.dylib.tdb

    缺点:会导致一些性能损失。但是可以优化,比如延迟绑定(Lazy Binding)技术。

    .tdb

    tbd全称是text-based stub libraries本质上就是一个YAML描述的文本文件。他的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息。用于避免在真机开发过程中直接使用传统的dylib。对于真机来说,由于动态库都是在设备上,在Xcode上使用基于tbd格式的伪framework可以大大减少Xcode的大小。

    framework

    Mac OS/iOS 平台还可以使用 FrameworkFramework 实际上是一种打包方式,将库的二进制文件、头文件和有关的资源文件打包到一起方便管理和分发。

    Framework 和系统的 UIKit.Framework 还是有很大区别。系统的 Framework 不需要拷⻉到目标程序中,我们自己做出来的 Framework 哪怕是动态的,最后也还是要拷⻉到 App 中(App 和 Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为 Embedded Framework

    Embedded Framework

    开发中使用的动态库会被放入到ipa下的framework目录下,基于沙盒运行。
    不同的App使用相同的动态库,并不会只在系统中存在一份。而是会在多个App中各自打包、签名、加载一份。


    framework即可以代表动态库也可以代表静态库。

    生成framework




    编译test.m

    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I ./Frameworks/HPExample.framework/Headers \
    -c test.m -o test.o
    链接.o生成test可执行文件
    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -F./Frameworks \
    -framework HPExample \
    test.o -o test
    那么链接一个framework也就需要三个条件:
    1.  -I<directory>:在指定目录寻找头文件 header search path(头文件)
    2. -F<directory>:在指定目录寻找framework framework search path
    3. -framework <framework_name>:指定链接的framework名称 other link flags -framework AFNetworking


    脚本执行命令

    上面都是通过命令行来进行编译连接的,每次输入都很麻烦(即使粘贴复制),我们可以将命令保存在脚本中,通过执行脚本来执行命令。
    还是以HPExample为例,整理后脚本如下(可以加一些日志观察执行问题):

    echo "test.m -> test.o"
    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I ./StaticLibrary \
    -c test.m -o test.o

    echo "pushd -> StaticLibrary"
    #cd可以进入到一个目录不推荐使用,cd会修改目录栈上层,推荐使用 pushd,pushd是往目录栈中push一个目录。
    pushd ./StaticLibrary

    echo "HPExample.m -> HPExample.o"
    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I./StaticLibrary \
    -c HPExample.m -o HPExample.o

    echo "HPExample.o -> libHPExample.a"
    #打包.o成静态库
    ar -rc libHPExample.a HPExample.o
    echo "popd -> StaticLibrary"
    popd

    echo "test.o -> test"
    #链接
    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -L./StaticLibrary \
    -lHPExample \
    test.o -o test
    这个时候就已经自动编译链接完成了,其中路径是pushdpopd自动生成的。
    可以简单优化下脚本:

    SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk
    #${SYSROOT}和$SYSROOT都行,如果要匹配比如${SYSROOT}.mm则用{}
    FILE_NAME=test
    HEADER_SEARCH_PATH=./StaticLibrary

    function MToOOrExec {
    if [[ $2 == ".m" ]]; then
    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot ${SYSROOT} \
    -I${HEADER_SEARCH_PATH} \
    -c $1.m -o $1.o
    else
    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot ${SYSROOT} \
    -L${HEADER_SEARCH_PATH} \
    -l$1 \
    ${FILE_NAME}.o -o ${FILE_NAME}
    fi
    return 0
    }

    echo "test.m -> test.o"
    MToOOrExec ${FILE_NAME} ".m"
    echo "pushd -> StaticLibrary"
    #cd可以进入到一个目录不推荐使用,cd会修改目录栈上层,推荐使用 pushd,pushd是往目录栈中push一个目录。
    pushd ${HEADER_SEARCH_PATH}
    echo "HPExample.m -> HPExample.o"
    MToOOrExec HPExample ".m"
    echo "HPExample.o -> libHPExample.a"
    #打包.o成静态库
    ar -rc libHPExample.a HPExample.o
    echo "popd -> StaticLibrary"
    popd
    echo "test.o -> test"
    #链接
    MToOOrExec HPExample ".o"

    dead code strip

    对于上面的例子,如果我们在test.m中不使用HPExample只是导入。

    #import <Foundation/Foundation.h>
    #import "HPExample.h"

    int main(){
    NSLog(@"test----");
    // HPExample *manager = [HPExample new];
    // [manager hp_test: nil];
    return 0;
    }
    默认clangdead code strip是生效的。
    在有分类的情况下
    看另外一个例子,我们直接用Xcode创建一个framework,设置为静态库。(Targets -> Build Settings -> Linking -> Macho-type)

    这个库有一个HPTestObject以及HPTestObject+HPAdditions。实现如下:
    HPTestObject

    //.h
    #import <Foundation/Foundation.h>

    @interface HPTestObject : NSObject

    - (void)hp_test;

    @end

    //.m
    #import "HPTestObject.h"
    #import "HPTestObject+HPAdditions.h"

    @implementation HPTestObject

    - (void)hp_test {
    [self hp_test_additions];
    }

    @end

    HPTestObject+HPAdditions

    //.h
    #import "HPTestObject.h"

    @interface HPTestObject (HPAdditions)

    - (void)hp_test_additions;

    @end

    //.m
    #import "HPTestObject+HPAdditions.h"

    @implementation HPTestObject (HPAdditions)

    - (void)hp_test_additions {
    NSLog(@"log: hp_test_additions");
    }

    @end
    HPTestObject设置为public

    我们知道分类是在运行时动态创建的,dead code strip是在链接的过程中生效的。那么应该在链接的时候会strip掉分类。
    我们创建一个workspace验证下

    workspace
    A. 可重用性。多个模块可以在多个项目中使用。节约开发和维护时间。
    B. 节省测试时间。单独模块意味着每个模块中都可以添加测试功能。
    C. 更好的理解模块化思想。

    1.File -> save as workspace




    2.创建一个project(TestApp)。



    3.打开workspace,添加一个project(创建的TestApp)(⚠️需要关闭打开的文件才会出现Add Files to TestDeadCodeStrip):





    4.ViewController.m中使用HPTestObject

    #import <HPStaticFramework/HPTestObject.h>

    - (void)viewDidLoad {
    [super viewDidLoad];
    HPTestObject *hpObject = [HPTestObject new];
    [hpObject hp_test];
    }

    5.运行

    libc++abi.dylib: terminating with uncaught exception of type NSException
    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HPTestObject hp_test_additions]: unrecognized selector sent to instance 0x600001048020'
    terminating with uncaught exception of type NSException

    和预想的一样直接报错了,原因是dead code strip脱掉了分类。要解决问题还是要告诉编译器不要脱。
    6.配置XCConfig告诉编译器不要脱。

    //-Xlinker 告诉 clang -all_load 参数是传给 ld 的。
    OTHER_LDFLAGS=-Xlinker -all_load

    再次运行App:

     TestApp[8958:13347736] log: hp_test_additions

    ⚠️
    -Xlinker 告诉 clang -all_load 参数是传给ld的。
    -all_load:全部链接

    OTHER_LDFLAGS=-Xlinker -all_load

    -ObjCOC相关的代码不要剥离

    //OTHER_LDFLAGS=-Xlinker -ObjC

    -force_load:指定哪些静态库不要 dead strip

    HPSTATIC_FRAMEWORK_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/HPStaticFramework.framework/HPStaticFramework
    OTHER_LDFLAGS=-Xlinker -force_load $HPSTATIC_FRAMEWORK_PATH

    -noall_load: 默认,没有使用静态库代码则不往可执行文件添加。This is the default. This option is obsolete.

    以上4种参数仅针对静态库。dead code strip是在链接过程中连接器提供的优化方式。

    -dead_strip
    Remove functions and data that are unreachable by the entry point or exported symbols.
    移除没有被入口点(也就是main)和导出符号用到的代码。

    接着libraryDeadCodeStrip工程验证下:
    修改test.m如下:


    #import <Foundation/Foundation.h>
    //#import "HPExample.h"

    //全局函数
    void global_function() {

    }

    //entry point
    int main(){
    // global_function();
    NSLog(@"test----");
    // HPExample *manager = [HPExample new];
    // [manager hp_test: nil];
    return 0;
    }

    //本地
    static void static_function(){

    }

    运行build.sh
    可以看到没有静态库libHPExample.a相关的代码,加上all_load再查看下:
    build.sh修改增加

    -Xlinker -all_load \
    hp_test方法已经有了。
    修改-Xlinker -all_load-Xlinker -dead_strip 再查看下:

    global_functionhp_test都没有了。
    打开mainglobal_function()的注释再看下:

    所以dead code strip-all_load-ObjC-force_load-noall_load不是一个东西,他有一定规则:

    1. 入口点没有使用->干掉
    2. 没有被导出符号使用->干掉

    接着-Xlinker -dead_strip-Xlinker -all_load一起添加:


    链接器有一个参数-why_live可以查看某一个符号为什么没有被干掉,比如我们要知道global_function为什么没有被干掉:
        -Xlinker -why_live -Xlinker _global_function

    .o -> .o.o -> .a

    .o -> .o是合并成一个大的.o再去链接生成可执行文件。先组合再链接。所以这里dead code strip干不掉,可以通过LTO(Link-Time Optimization)去优化。
    .o链接静态库是.o是去使用静态库。先dead code strip再使用。





  • Do Not Embed
    用于静态库
  • Embed & Sign
    嵌入,用于动态库,动态库在运行时链接,所以它们编译的时候需要被打进bundle里面。静态库链接的时候代码就已经在一起了,所以不需要拷贝,直接Do Not Embed就可以了。可以通过file命令验证:

  • file HPStaticFramework.framework/HPStaticFramework
    HPStaticFramework.framework/HPStaticFramework: current ar archive random library

    current ar archive:说明是静态库,选择Do not embed
    Mach-0 dynamically:说明是动态库,选择Embed

    1. Embed Without Signing
      Signing:只用于动态库,如果已经有签名了就不需要再签名。终端执行codesign -dv判断:

    codesign -dv HPStaticFramework.framework
    Executable=/Users/***/Library/Developer/Xcode/DerivedData/TestDeadCodeStrip-fhbiunbplvqefkftfystdixdxmkq/Build/Products/Debug-iphonesimulator/HPStaticFramework.framework/HPStaticFramework
    Identifier=HotpotCat.HPStaticFramework
    Format=bundle with generic
    CodeDirectory v=20100 size=204 flags=0x2(adhoc) hashes=1+3 location=embedded
    Signature=adhoc
    Info.plist entries=20
    TeamIdentifier=not set
    Sealed Resources version=2 rules=10 files=2
    Internal requirements count=0 size=12

    命令总结

    clang命令参数

    -x: 指定编译文件语言类型
    -g: 生成调试信息
    -c: 生成目标文件,只运行preprocesscompileassemble不链接
    -o: 输出文件
    -isysroot: 使用的SDK路径
    -I<directory>: 在指定目录寻找头文件 header search path
    -L<directory> :指定库文件路径(.a.dylib库文件)library search path
    -l<library_name>: 指定链接的库文件名称(.a.dylib库文件)other link flags -lAFNetworking。链接的名称为libAFNetworking/AFNetworking的动态库或者静态库,查找规则:先找lib+<library_name>的动态库,找不到,再去找lib+<library_name>的静态库,还找不到,就报错。
    -F<directory>: 在指定目录寻找framework,framework search path
    -framework <framework_name>: 指定链接的framework名称,other link flags -framework AFNetworking

    test.m编译成test.o过程

    1. 使用OC
    2. 生成指定架构的代码,Big Sur是:x86_64-apple-macos11.1,之前是:x86_64-apple-macos10.15。iOS模拟器是:x86_64-apple-ios14-simulator。更多内容可以参考target部分。
    3. 使用ARC
    4. 使用的SDK的路径在:
      Big Sur是:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk
      之前是:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk
      模拟器是:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
      更多内容可以参考sdk部分。
    5. 用到的其他库的头文件地址在./Frameworks
      命令示例:
    clang -x objective-c \
    -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -I ./AFNetworking \
    -c test.m -o test.o

    test.o链接生成test可执行文件

    clang链接.a静态库

    顺序和生成.o差不多,不需要指定语言。
    命令示例:

    clang -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -L./AFNetworking \
    -lAFNetworking \
    test.o -o test

    ld链接.framework静态库

    ld -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
    -lsystem -framework Foundation \
    -lAFNetworking \
    -L.AFNetworking \
    test.o -o test





    作者:HotPotCat
    链接:https://www.jianshu.com/p/298efeb8732c










    收起阅读 »

    Mac终端快捷键

    编辑命令行

    快捷键说明
    control + k删除从光标到行尾
    control + u删除从光标到行首
    control + w从光标向前一个单词剪切到剪贴板
    option + d从光标向后删除一个单词。⚠️option键需要自己配置。详见后面[终端option键配置]
    control + d删除光标下一个字母
    control + h删除光标前一个字母
    option + tswap(当前单词,上一个单词),尾部会交换前两个单词
    control + tswap(当前字母,上一个字母)
    control + y粘贴上一次删除的文本
    option + c大写当前字母,并移动光标到单词尾
    option + u大写从当前光标到单词尾
    option + c小写从当前光标到单词尾,光标后的第一个字母会大写
    control + r向后搜索历史命令,control + r后输入关键字比如i然后再control + r一直往上查找,当然也可以通过control + pcontrol + n配合查找
    control + g退出搜索
    control + p历史中上一个命令
    control + n历史中下一个命令
    option + .上一个命令的最后一个单词
    control + l / command + k清屏,当前命令前面的所有内容
    control + s停止输出(zsh中为向前搜索历史命令)
    control + q继续输出
    control + c终止当前命令
    control + z挂起当前命令
    control + d结束输出(产生一个EOF
    control + a移动光标到行首
    control + e移动光标到行尾
    option + b移动光标后退一个单词(词首)
    option + f移动光标前进一个单词(词首)
    control + b光标前进一个字母(这两个没什么实际意义,通过左右箭头就可以操作了-><-
    control + f光标后退一个字母
    control + xx当前位置与行首之间选中
    control + -撤销,类似macOS系统的control +z
    option + r取消更改,并恢复历史记录中的行(还原)
    esc + t1.光标在行尾交换光标前的最后两个单词。
    2.在中间交换光标前后单词。
    3.在行首无效。
    !!重复上一条命令,类似上箭头
    !n交换光标前的最后两个单词
    !:n-m重复最后一条命令取参数n-m,比如:!:3-4
    !:n-$重复最后一条命令取参数n-最后,比如:!:3-$
    !:q引用最后一条命令,相当于分割单词
    !:q命令
    !$上一条命令的最后一个参数
    !*上一条命令的所有参数
    !*命令
    option + 方向键光标以单词为单位移动(仅在Terminal有效,iTerm无效
    command + fn + 左/右箭头滚动到顶部/底部
    command + fn + 上/下箭头上/下一页
    optional + command + fn + 上/下箭头上/下一行
    delete/fn + delete向前/后删除一个字符

    分屏

    快捷键说明
    command + d分屏
    1.在mac默认终端Terminal下是上下分屏,显示内容一致。
    2.在iTerm下是横向分屏相当于多个终端
    command + shift + d
    1.在mac默认终端Terminal下是取消分屏。
    2.在iTerm下是纵向分屏

    标签&窗口

    快捷键说明
    command + t新建标签
    command + w关闭标签
    command + shift + 左右箭头/control + tab/control + shift + tab选择标签
    command + shift + |mac默认终端Terminal下有效。相当于Mac触摸板的四指上滑 (调度中心) 
    image.png
    command + n新建窗口
    shift + command + t显示或隐藏标签页栏
    隐藏
    显示
    shift + command + n新建命令(Terminal下有效)
    shift + command + k新建远程连接(Terminal下有效)
    command + i显示或隐藏检查器(Terminal下有效) 
    image.png
    command + +/-放大/缩小字体
    command + 重音符/command + shift + 重音符下/上一个窗口,重音符(`)


    1.使用“终端”窗口和标签页

    操作
    快捷键
    新建窗口
    Command-N
    使用相同命令新建窗口
    Control-Command-N
    新建标签页
    Command-T
    使用相同命令新建标签页
    Control-Command-T
    显示或隐藏标签页栏
    Shift-Command-T
    显示所有标签页或退出标签页概览
    Shift-Command-反斜杠 (\)
    新建命令
    Shift-Command-N
    新建远程连接
    Shift-Command-K
    显示或隐藏检查器
    Command-I
    编辑标题
    Shift-Command-I
    编辑背景颜色
    Option-Command-I
    放大字体
    Command-加号键 (+)
    缩小字体
    Command-减号键 (–)
    下一个窗口
    Command-重音符键 (`)
    上一个窗口
    Command-Shift-波浪符号 (~)
    下一个标签页
    Control-Tab
    上一个标签页
    Control-Shift-Tab
    将窗口拆分为两个面板
    Command-D
    关闭拆分面板
    Shift-Command-D
    关闭标签页
    Command-W
    关闭窗口
    Shift-Command-W
    关闭其他标签页
    Option-Command-W
    全部关闭
    Option-Shift-Command-W
    滚动到顶部
    Command-Home
    滚动到底部
    Command-End
    上一页
    Command-Page Up
    下一页
    Command-Page Down
    上一行
    Option-Command-Page Up
    下一行
    Option-Command-Page Down


    2.编辑命令行

    操作
    快捷键
    重新定位插入点
    在按住 Option 键的同时将指针移到新的插入点。
    将插入点移到行的开头
    Control-A
    将插入点移到行的结尾
    Control-E
    将插入点前移一个字符
    右箭头键
    将插入点后移一个字符
    左箭头键
    将插入点前移一个字词
    Option-右箭头键
    将插入点后移一个字词
    Option-左箭头键
    删除到行的开头
    Control-U
    删除到行的结尾
    Control-K
    向前删除到字词的结尾
    Option-D(选中将 Option 键用作 Meta 键后可用)
    向后删除到字词的开头
    Control-W
    删除一个字符
    Delete
    向前删除一个字符
    向前删除(或使用 Fn-Delete)
    转置两个字符
    Control-T


    3.在“终端”窗口中选择和查找文本

    操作
    快捷键
    选择完整文件路径
    按住 Shift-Command 键并连按路径
    选择整行文本
    点按该行三下
    选择一个词
    连按该词
    选择 URL
    按住 Shift-Command 键并连按 URL
    选择矩形块
    按住 Option 键并拖移来选择文本
    剪切
    Command-X
    拷贝
    Command-C
    不带背景颜色拷贝
    Control-Shift-Command-C
    拷贝纯文本
    Option-Shift-Command-C
    粘贴
    Command-V
    粘贴所选内容
    Shift-Command-V
    粘贴转义文本
    Control-Command-V
    粘贴转义的所选内容
    Control-Shift-Command-V
    查找
    Command-F
    查找下一个
    Command-G
    查找上一个
    Command-Shift-G
    使用选定的文本查找
    Command-E
    跳到选定的文本
    Command-J
    全选
    Command-A
    打开字符检视器
    Control-Command-Space


    4.使用标记和书签

    操作
    快捷键
    标记
    Command-U
    标记为书签
    Option-Command-U
    取消标记
    Shift-Command-U
    标记命令行并发送返回结果
    Command-Return
    发送返回结果但不标记
    Shift-Command-Return
    插入书签
    Shift-Command-M
    插入包含名称的书签
    Option-Shift-Command-M
    跳到上一个标记
    Command-上箭头键
    跳到下一个标记
    Command-下箭头键
    跳到上一个书签
    Option-Command-上箭头键
    跳到下一个书签
    Option-Command-下箭头键
    清除到上一个标记
    Command-L
    清除到上一个书签
    Option-Command-L
    清除到开头
    Command-K
    在标记之间选择
    Shift-Command-A


    5.其他快捷键

    操作
    快捷键
    进入或退出全屏幕
    Control-Command-F
    显示或隐藏颜色
    Shift-Command-C
    打开“终端”偏好设置
    Command-逗号键 (,)
    中断
    键入 Command-句点键 (.) 等于在命令行上输入 Control-C
    打印
    Command-P
    软重置终端仿真器状态
    Option-Command-R
    硬重置终端仿真器状态
    Control-Option-Command-R
    打开 URL
    按住 Command 键并连按 URL
    添加至文件的完整路径
    从“访达”将文件拖移到“终端”窗口中
    将文本导出为
    Command-S
    将选定的文本导出为
    Shift-Command-S
    反向搜索命令历史
    Control-R
    开关“允许鼠标报告”选项
    Command-R
    开关“将 Option 键用作 Meta 键”选项
    Command-Option-O
    显示备用屏幕
    Option-Command-Page Down
    隐藏备用屏幕
    Option-Command-Page Up
    打开所选内容的 man 页面
    Control-Shift-Command-问号键 (?)
    搜索所选内容的 man 页面索引
    Control-Option-Command-斜杠 (/)
    完整的目录或文件名称
    在命令行上,键入一个或多个字符,然后按下 Tab 键
    显示可能的目录或文件名称补全列表
    在命令行上,键入一个或多个字符,然后按下 Tab 键两次


    MacHomeEndPageUPPageDOWN

    • Home = Fn + 左方向
    • End = Fn + 右方向、
    • PageUP = Fn + 上方向
    • PageDOWN = Fn + 下方向
    • 向前Delete = Fn + delete

    终端option键配置

    将 Option 键用作 Meta 键

    Terminal配置

    Preferences -> Profiles -> 将optional键用作Meta键

    Terminal配置>

    iTerm配置

    iTerm需要在Preferences -> Profiles -> "your Profile" -> Keys -> left/right option key ->Esc+配置。
    ⚠️这里是配置成Esc+不是Meta




     

    作者:HotPotCat
    开码牛

    链接:https://www.jianshu.com/p/524d02ee49cf

    https://blog.csdn.net/helunqu2017/article/details/113749611

     

    Xcode多环境配置

    Xcode多环境配置一共有3种形式:TargetSchemexcconfigProject:包含了项目所有的代码、资源文件、所有信息。(一个项目是多个project的集合)Target:对指定代码和资源文件的具体构建方式。(指定某些代码如何生成ipa包,类似打...
    继续阅读 »

    Xcode多环境配置一共有3种形式:

    • Target
    • Scheme
    • xcconfig

    Project:包含了项目所有的代码、资源文件、所有信息。(一个项目是多个project的集合)
    Target:对指定代码和资源文件的具体构建方式。(指定某些代码如何生成ipa包,类似打工人的角色)
    Scheme:对指定Target的环境配置。(配置编译环境变量)
    这也就是我们修改一些配置的时候需要选中Target再去修改的原因。

    多Target配置

    在项目中选中Target复制就生成新的Target了。




    相当于可以直接分别配置Info.plist文件,在Target中修改bundleId后就相当于两个Target是两个App了。
    同时可以在Preprocessor Macros中配置一些宏定义用于代码中区分Target



    #if DEV
    #import "TestMutableConfig_dev-Swift.h"
    #else
    #import "TestMutableConfig-Swift.h"
    #endif


    ~  swiftc --help | grep -- '-D'
    -D <value> Marks a conditional compilation flag as true


    Target方式配置多环境
    1.会生成多个Info.plist文件;
    2.配置比较繁琐,需要同步配置容易混乱
    那么对于多Target的场景是可以在Build Phases中控制要编译的文件和资源。



    多scheme配置

    scheme默认有DebugReleaseconfig我们可以按需添加。在Target中添加变量的时候已经用到过了。
    配置在Project -> Info -> Configurations




    运行/打包的时候选择对应的Scheme就可以了。



    这个时候只需要切换Scheme运行就可以了。

    比如我们上传打包ipa的时候,有时候会错将debug模式下的包上传上去,尤其是在发灰度包的时候。这里有两个方案:
    1.通过config配置。
    2.打包的时候通过脚本修改info.plist文件增加一个变量。

    release包赋值为0debug包赋值为1。这样在上传ipa包的时候后端读取Info.plist做判断,debug包直接报错不让传。
    这里实现以下方式1:
    Targets -> Build Settings -> + -> Add User-Defined Setting





    到这里就完成了

    • User-Defined添加配置;
    • Info.plist暴露配置的目的。

    在代码中测试下:

        NSString *infoPath = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
    NSDictionary *infoDic = [[NSDictionary alloc] initWithContentsOfFile:infoPath];
    NSLog(@"IPAFLAG = %@",infoDic[@"IPAFLAG"]);
    Debug下:

     IPAFLAG = 1
    当然也可以配置app图标:
    Assets.xcassets中添加不同的资源文件


    Scheme情况只需要在一个build setting中就能完成配置了,比多Target方便好维护。
    缺点是还需要在build setting中设置。

    xcconfig


    就是使用xcconfig配置的:

    FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking"
    GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
    HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking/AFNetworking.framework/Headers"
    LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
    OTHER_LDFLAGS = $(inherited) -framework "AFNetworking"
    PODS_BUILD_DIR = ${BUILD_DIR}
    PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
    PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
    PODS_ROOT = ${SRCROOT}/Pods
    USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

    xcconfigkey-value的形式配置的。那么怎么对应到xcconfig文件的呢?


    Configurations中对应配置的。

    配置自己的xcconfig文件

    新建 -> Configuration Settings File





    • 1中设置是对整个Project生效。
    • 2中设置是对Target生效。

    还是以IPAFLAG为例,以xcconfig的方式配置。
    plist中的配置不变,User-Defined配置删除

        <key>IPAFLAG</key>
    <string>${IPAFLAG}</string>
    Config-TestMutableConfig.Debug.xcconfig

    IPAFLAG = 1

    Config-TestMutableConfig.Release.xcconfig

    IPAFLAG = 0
    代码中读取下:

        NSString *infoPath = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
    NSDictionary *infoDic = [[NSDictionary alloc] initWithContentsOfFile:infoPath];
    NSLog(@"IPAFLAG = %@",infoDic[@"IPAFLAG"]);
    IPAFLAG = 1
    这样配置更清晰,便于管理。


    xcconfig配置总结

    key-value 组成

    配置文件由一系列键值分配组成:

    BUILD_SETTING_NAME = value

    注释

    xcconfig文件只有一种注释方式//

    //

    那么这里就有一个问题了,如果我们要配置一个域名该怎么办呢?比如:

    HOST_URL = https://127.0.0.1

    可以通过中间值解决:

    TEMP=/
    HOST_URL = https:${TEMP}/127.0.0.1

    include导入其他设置

    可以通过include关键字导入其他的xcconfig内的配置。通过include关键字后接上双引号:

    #include "Other.xcconfig"

    在引入的文件时,如果是以/开头,代表绝对路径:

    #include "/Users/zaizai/Desktop/TestMutableConfig/Pods/Target Support Files/Pods-TestMutableConfig/Pods-TestMutableConfig.debug.xcconfig"

    相对路径,以${SRCROOT}路径为开始:

    #include "Pods/Target Support Files/Pods-TestMutableConfig/Pods-TestMutableConfig.debug.xcconfig"

    变量

    变量定义,按照OC命名规则,仅由大写字母,数字和下划线_组成,原则上大写,也可以不。字符串可以是"也可以是'号。

    1. xcconfig中定义的变量与Build Settings的一致,会发生覆盖。可以通过$(inherited)让当前变量继承变量原有值。(当然对于系统的key最好都加上$(inherited)`)
    //A config
    OTHER_LDFLAGS = -framework SDWebImage
    //B config
    OTHER_LDFLAGS = $(inherited) -framework AFNetworking
    //build setting中
    // OTHER_LDFLAGS = -framework SDWebImage -framework AFNetworking

    ⚠️:有部分变量不能通过xcconfig配置到Build Settings中。如:配置PRODUCT_BUNDLE_IDENTIFIER不起作用。

    1. 引用变量,$()${}两种写法都可以
    VALUE=HotpotCat

    KEY1=$(VALUE)
    KEY2=${VALUE}
    1. 条件变量,根据SDKArchConfigration对设置进行条件化:
    // 指定`Configration`是`Debug`
    // 指定`SDK`是模拟器,还有iphoneos*、macosx*等
    // 指定生效架构为`x86_64`
    OTHER_LDFLAGS[config=Debug][sdk=iphonesimulator*][arch=x86_64]= $(inherited) -framework "HotpotCat"

    ⚠️:在Xcode 11.4及以后版本,可以使用default来指定变量为空时的默认值。

    $(BUILD_SETTING_NAME:default=value)

    优先级(高->低)

    • 手动配置Target Build Settings;
    • Target中配置的xcconfig文件;
    • 手动配置Project Build Settings;
    • Project中配置的xcconfig文件。



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

    llvm优化alloc

    为什么调用alloc最终调用了objc_alloc?objc源码中探索分析在源码中我们点击alloc会进入到+ (id)alloc方法,但是在实际调试中却是先调用的objc_alloc,系统是怎么做到的呢?可以看到在这个方法中进行了imp的重新绑定将alloc...
    继续阅读 »

    为什么调用alloc最终调用了objc_alloc


    objc源码中探索分析

    在源码中我们点击alloc会进入到+ (id)alloc方法,但是在实际调试中却是先调用的objc_alloc,系统是怎么做到的呢?




    可以看到在这个方法中进行了imp的重新绑定将alloc绑定到了objc_alloc上面。当然retainrelease等都进行了同样的操作。
    既然在_read_images中出现问题的时候尝试进行fixup,那么意味着正常情况下在_read_images之前llvm的编译阶段就完成了绑定。

    llvm源码探索分析

    那么直接在llvm中搜索objc_alloc,在ObjCRuntime.h中发现了如下注释:

      /// When this method returns true, Clang will turn non-super message sends of
    /// certain selectors into calls to the corresponding entrypoint:
    /// alloc => objc_alloc
    /// allocWithZone:nil => objc_allocWithZone

    这说明方向没有错,最中在CGObjC.cpp中找到了如下代码:


      case OMF_alloc:
    if (isClassMessage &&
    Runtime.shouldUseRuntimeFunctionsForAlloc() &&
    ResultType->isObjCObjectPointerType()) {
    // [Foo alloc] -> objc_alloc(Foo) or
    // [self alloc] -> objc_alloc(self)
    if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
    return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
    // [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or
    // [self allocWithZone:nil] -> objc_allocWithZone(self)
    if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 &&
    Args.size() == 1 && Args.front().getType()->isPointerType() &&
    Sel.getNameForSlot(0) == "allocWithZone") {
    const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal();
    if (isa<llvm::ConstantPointerNull>(arg))
    return CGF.EmitObjCAllocWithZone(Receiver,
    CGF.ConvertType(ResultType));
    return None;
    }
    }
    break;

    可以看出来alloc最后执行到了objc_alloc。那么具体的实现就要看CGF.EmitObjCAlloc方法:

    llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
    llvm::Type *resultType) {
    return emitObjCValueOperation(*this, value, resultType,
    CGM.getObjCEntrypoints().objc_alloc,
    "objc_alloc");
    }

    llvm::Value *CodeGenFunction::EmitObjCAllocWithZone(llvm::Value *value,
    llvm::Type *resultType) {
    return emitObjCValueOperation(*this, value, resultType,
    CGM.getObjCEntrypoints().objc_allocWithZone,
    "objc_allocWithZone");
    }

    llvm::Value *CodeGenFunction::EmitObjCAllocInit(llvm::Value *value,
    llvm::Type *resultType) {
    return emitObjCValueOperation(*this, value, resultType,
    CGM.getObjCEntrypoints().objc_alloc_init,
    "objc_alloc_init");
    }

    这里可以看到alloc以及objc_alloc_init相关的逻辑。这样就实现了绑定。那么系统是怎么走到OMF_alloc的逻辑的呢?
    通过发送消息走到这块流程:

    CodeGen::RValue CGObjCRuntime::GeneratePossiblySpecializedMessageSend(
    CodeGenFunction &CGF, ReturnValueSlot Return, QualType ResultType,
    Selector Sel, llvm::Value *Receiver, const CallArgList &Args,
    const ObjCInterfaceDecl *OID, const ObjCMethodDecl *Method,
    bool isClassMessage) {
    //尝试发送消息
    if (Optional<llvm::Value *> SpecializedResult =
    tryGenerateSpecializedMessageSend(CGF, ResultType, Receiver, Args,
    Sel, Method, isClassMessage)) {
    return RValue::get(SpecializedResult.getValue());
    }
    return GenerateMessageSend(CGF, Return, ResultType, Sel, Receiver, Args, OID,
    Method);
    }
  • 苹果对alloc等特殊函数做了hook,会先走底层的标记emitObjCValueOperation。最终再走到alloc等函数。
  • 第一次会走tryGenerateSpecializedMessageSend分支,第二次就走GenerateMessageSend分支了。
    • 也就是第一次alloc调用了objc_alloc,第二次alloc后就没有调用objc_alloc走了正常的objc_msgSendalloc-> objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc。这也就是callAlloc走两次的原因。
    • 再创建个对象调用流程就变成了:alloc -> objc_alloc -> callAlloc


  • 内存分配优化

    HPObject *hpObject = [HPObject alloc];
    NSLog(@"%@:",hpObject);

    对于hpObject我们查看它的内存数据如下:

    (lldb) x hpObject
    0x6000030cc2e0: c8 74 e6 0e 01 00 00 00 00 00 00 00 00 00 00 00 .t..............
    0x6000030cc2f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
    (lldb) p 0x000000010ee674c8
    (long) $4 = 4544951496

    可以打印的isa4544951496并不是HPObject。因为这里要&mask,在源码中有一个&mask结构。arm64`定义如下:

    #   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
    # define ISA_MASK 0x007ffffffffffff8ULL
    # else
    # define ISA_MASK 0x0000000ffffffff8ULL

    这样计算后就得到isa了:

    (lldb) po 0x000000010ee674c8 & 0x007ffffffffffff8
    HPObject

    HPObjetc添加属性并赋值,修改逻辑如下:

    @interface HPObject : NSObject

    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) double height;
    @property (nonatomic, assign) BOOL marry;

    @end
    调用:
        HPObject *hpObject = [HPObject alloc];
    hpObject.name = @"HotpotCat";
    hpObject.age = 18;
    hpObject.height = 180.0;
    hpObject.marry = YES;
    这个时候发现agemarry存在了isa后面存在了一起。
    那么多增加几个BOOL属性呢?

    @interface HPObject : NSObject

    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) double height;
    @property (nonatomic, assign) BOOL marry;
    @property (nonatomic, assign) BOOL flag1;
    @property (nonatomic, assign) BOOL flag2;
    @property (nonatomic, assign) BOOL flag3;

    @end

    int类型的age单独存放了,5bool值放在了一起。这也就是内存分配做的优化。

    init源码探索

    既然alloc已经完成了内存分配和isa与类的关联那么init中做了什么呢?

    init
    init源码定义如下:

    - (id)init {
    return _objc_rootInit(self);
    }

    _objc_rootInit

    id
    _objc_rootInit(id obj)
    {
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
    }




    可以看到init中调用了_objc_rootInit,而_objc_rootInit直接返回obj没有做任何事情。就是给子类用来重写的,提供接口便于扩展。所以如果没有重写init方法,那么在创建对象的时候可以不调用init方法。

    有了alloc底层骚操作的经验后,打个断点调试下:

    NSObject *obj = [NSObject alloc];
    [obj init];

    这里allocinit分开写是为了避免被优化。这时候调用流程和源码看到的相同。

    那么修改下调用逻辑:

    NSObject *obj = [[NSObject alloc] init];

    alloc init一起调用后会先进入objc_alloc_init方法。

    objc_alloc_init

    id
    objc_alloc_init(Class cls)
    {
    return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
    }

    objc_alloc_init调用了callAllocinit

    new源码探索

    既然alloc init 和new都能创建对象,那么它们之间有什么区别呢?
    new

    + (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
    }
    alloc init一起调用的不同点是checkNil传递的是fasle
    源码调试发现new调用的是objc_opt_new

    // Calls [cls new]
    id
    objc_opt_new(Class cls)
    {
    #if __OBJC2__
    if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
    return [callAlloc(cls, false/*checkNil*/) init];
    }
    #endif
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
    }

    objc2下也是callAllocinit

    • init方法内部默认没有进行任何操作,只是返回了对象本身。
    • allot initnew底层实现一致,都是调用callAllocinit。所以如果自定义了init方法调用两者效果相同。

    objc_alloc_initobjc_opt_new的绑定与objc_alloc的实现相同。同样的实现绑定的还有:

    const char *AppleObjCTrampolineHandler::g_opt_dispatch_names[] = {
    "objc_alloc",//alloc
    "objc_autorelease",//autorelease
    "objc_release",//release
    "objc_retain",//retain
    "objc_alloc_init",// alloc init
    "objc_allocWithZone",//allocWithZone
    "objc_opt_class",//class
    "objc_opt_isKindOfClass",//isKindOfClass
    "objc_opt_new",//new
    "objc_opt_respondsToSelector",//respondsToSelector
    "objc_opt_self",//self
    };

    总结

    alloc调用过程:

    • objc_alloc
      • alloc底层首先调用的是objc_alloc
      • objc_allocalloc是在llvm编译阶段进行关联的。苹果会对系统特殊函数做hook进行标记。
    • callAlloc判断应该初始化的分支。
    • _class_createInstanceFromZone进行真正的开辟和关联操作:
      • instacneSize计算应该开辟的内存空间。
        • alignedInstanceSize内部进行字节对齐。
        • fastInstanceSize内部会进行内存对齐。
      • calloc开辟内存空间。
      • initInstanceIsa关联isa与创建的对象。
    • init & new
      • init方法内部默认没有进行任何操作,只是为了方便扩展。
      • allot initnew底层实现一致,都是调用callAllocinit



    作者:HotPotCat
    链接:https://www.jianshu.com/p/884275c811d5


    收起阅读 »

    OC alloc 底层探索

    一、alloc对象的指针地址和内存有如下代码://alloc后分配了内存,有了指针。 //init所指内存地址一样,init没有对指针进行操作。 HPObject *hp1 = [HPObject alloc]; HPObject *hp2 = [hp1 in...
    继续阅读 »

    一、alloc对象的指针地址和内存

    有如下代码:

    //alloc后分配了内存,有了指针。
    //init所指内存地址一样,init没有对指针进行操作。
    HPObject *hp1 = [HPObject alloc];
    HPObject *hp2 = [hp1 init];
    HPObject *hp3 = [hp1 init];
    NSLog(@"%@-%p",hp1,hp1);
    NSLog(@"%@-%p",hp2,hp2);
    NSLog(@"%@-%p",hp3,hp3);

    输出:

    <HPObject: 0x600000f84330>-0x600000f84330
    <HPObject: 0x600000f84330>-0x600000f84330
    <HPObject: 0x600000f84330>-0x600000f84330


    说明alloc后进行了内存分配有了指针,而init后所指内存地址一致,所以init没有对指针进行操作。
    修改NSLog内容如下:

    NSLog(@"%@-%p &p:%p",hp1,hp1,&hp1);
    NSLog(@"%@-%p &p:%p",hp2,hp2,&hp2);
    NSLog(@"%@-%p &p:%p",hp3,hp3,&hp3);

    输出:

    <HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d8
    <HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d0
    <HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40c8


    这就说明hp1hp2hp3都指向堆空间的一块区域。而3个指针本身是在栈中连续开辟的空间,从高地址->低地址。
    那么alloc是怎么开辟的内存空间呢?


    二、底层探索思路


    1. 断点结合Step into instruction进入调用堆栈找到关键函数:


    找到了最中调用的是libobjc.A.dylibobjc_alloc:`。

    下断点后通过汇编查看调用流程Debug->Debug workflow->Always Show Disassembly通过已知符号断点确定未知符号。

    直接alloc下符号断点跟踪:


    三、alloc源码分析

    通过上面的分析已经能确定allocobjc框架中,正好苹果开源了这块代码,源码:objc源码地址:Source Browser
    最好是自己能编译一份能跑通的源码(也可以直接github上找别人编译好的)。当然也可以根据源码下符号断点跟踪调试。由于objc4-824目前下载不了,这里以objc4-824.2为例进行调试。

    HPObject定义如下:


    @interface HPObject : NSObject

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

    @end

    3.1 alloc

    直接搜索alloc函数的定义发现在NSObject.mm 2543,通过断点调试类。
    调用alloc会首先调用objc_alloc:

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


    callAlloc会走到调用alloc分支。

    + (id)alloc {
    return _objc_rootAlloc(self);
    }

    alloc直接调用了_objc_rootAlloc

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

    • _objc_rootAlloc传递参数checkNilfalseallocWithZonetrue直接调用了callAlloc
    • 在调用objc_alloc的时候传递的checkNiltrueallocWithZonefalse

    这里没什么好说的只是方法的一些封装,具体实现要看callAlloc



    3.2 callAlloc

    static ALWAYS_INLINE id
    callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
    {
    #if __OBJC2__
    //表示值为假的可能性更大。即执行else里面语句的机会更大
    if (slowpath(checkNil && !cls)) return nil;
    //hasCustomAWZ方法判断是否实现自定义的allocWithZone方法,如果没有实现就调用系统默认的allocWithZone方法。
    //表示值为真的可能性更大;即执行if里面语句的机会更大
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
    return _objc_rootAllocWithZone(cls, nil);
    }
    #endif

    // No shortcuts available.
    if (allocWithZone) {
    return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
    }

    slowpath:表示值为假的可能性更大。即执行else里面语句的机会更大。
    fastpath:表示值为真的可能性更大;即执行if里面语句的机会更大。
    OBJC2:是因为有两个版本。Legacy版本(早期版本,对应Objective-C 1.0) 和 Modern版本(现行版本Objective-C 2.0)。

    • 在首次调用的时候会走alloc分支进入到alloc逻辑。
    • hasCustomAWZ意思是hasCustomAllocWithZone有没有自定义实现AllocWithZone。没有实现就走(这里进行了取反)_objc_rootAllocWithZone,实现了走allocWithZone:
    • 第二次调用直接走callAlloc的其它分支不会调用到alloc

    ⚠️:自己实现一个类的allocWithZone alloc分支就每次都被调用了


    3.3 _objc_rootAllocWithZone

    NEVER_INLINE
    id
    _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
    {
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
    OBJECT_CONSTRUCT_CALL_BADALLOC);
    }

    _objc_rootAllocWithZone直接调用了_class_createInstanceFromZone

    3.4 allocWithZone

    // Replaced by ObjectAlloc
    + (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
    }


    _objc_rootAllocWithZone直接调用了_objc_rootAllocWithZone,与上面的3.3中的逻辑汇合了。

    3.5 _class_createInstanceFromZone

    最终会调用_class_createInstanceFromZone进程内存的计算和分配。


    static ALWAYS_INLINE id
    _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
    int construct_flags = OBJECT_CONSTRUCT_NONE,
    bool cxxConstruct = true,
    size_t *outAllocatedSize = nil)
    {
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    //判断当前class或者superclass是否有.cxx_construct构造方法的实现
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    //判断当前class或者superclass是否有.cxx_destruct析构方法的实现
    bool hasCxxDtor = cls->hasCxxDtor();
    //标记类是否支持优化的isa
    bool fast = cls->canAllocNonpointer();
    size_t size;
    //通过内存对齐得到实例大小,extraBytes是由对象所拥有的实例变量决定的。
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    //对象分配空间
    if (zone) {
    obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
    obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
    if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
    return _objc_callBadAllocHandler(cls);
    }
    return nil;
    }
    //初始化实例isa指针
    if (!zone && fast) {
    obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
    // Use raw pointer isa on the assumption that they might be
    // doing something weird with the zone or RR.
    obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
    return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
    }
  • 调用instanceSize计算空间大小。
  • 根据zone是否有值调用malloc_zone_calloccalloc进行内存分配。
    calloc之前分配的obj是一块脏内存,执行calloc后才会真正分配内存。执行前后内存地址发生了变化。

  • 根据!zone && fast分别调用initInstanceIsainitIsa进行isa实例化。
    • 执行完initInstanceIsa后再次打印就有类型了。
    • 根据是否有hasCxxCtor分别返回obj和调用object_cxxConstructFromClass

    3.6 instanceSize 申请内存

    在这个函数中调用了instanceSize计算实例大小:


    inline size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
    return cache.fastInstanceSize(extraBytes);
    }

    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
    }
    • 没有缓存的话会调用alignedInstanceSize,如果最终的size < 16会返回16
    • 有缓存则调用fastInstanceSize
    • 正常情况下缓存是在_read_images的时候生成的。所以这里一般会走fastInstanceSize分支。

    3.6.1 alignedInstanceSize



    #ifdef __LP64__
    # define WORD_MASK 7UL
    #else
    # define WORD_MASK 3UL

    uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
    }

    static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
    }

    uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
    }

    xunalignedInstanceSize获取。读取的是data()->ro()->instanceSize实例变量的大小。由ivars决定。这里为8,因为默认有个isaisaClass ,Classobjc_class struct *类型。

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    • 字节对齐算法为:(x + WORD_MASK) & ~WORD_MASKWORD_MASK 64位下为732位下为3
      那么对于HPObject对象计算方法如下:
      根据公式可得1:(8 + 7) & ~7 等价于 (8 + 7) >>3 << 3
      根据1可得2:15 & ~7
      转换为二进制:0000 1111 & ~0000 0111 = 0000 1111 & 1111 1000
      计算可得:00001000 = 8
      所以alignedInstanceSize计算就是以8字节对齐取8的倍数(算法中是往下取,对于内存分配来讲是往上取)。

    那么为什么以8字节对齐,最后最小分配16呢?
    分配16是为了做容错处理。以8字节对齐(选择8字节是因为8字节类型是最常用最多的)是以空间换取时间,提高CPU读取速度。当然这过程中会做一定的优化。

    3.6.2 fastInstanceSize

    bool hasFastInstanceSize(size_t extra) const
    {
    if (__builtin_constant_p(extra) && extra == 0) {
    return _flags & FAST_CACHE_ALLOC_MASK16;
    }
    return _flags & FAST_CACHE_ALLOC_MASK;
    }

    size_t fastInstanceSize(size_t extra) const
    {
    ASSERT(hasFastInstanceSize(extra));

    if (__builtin_constant_p(extra) && extra == 0) {
    return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
    size_t size = _flags & FAST_CACHE_ALLOC_MASK;
    // remove the FAST_CACHE_ALLOC_DELTA16 that was added
    // by setFastInstanceSize
    return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
    }

    • fastInstanceSize中会调用align16,实现如下(16字节对齐):
    static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
    }

    void setInstanceSize(uint32_t newSize) {
    ASSERT(isRealized());
    ASSERT(data()->flags & RW_REALIZING);
    auto ro = data()->ro();
    if (newSize != ro->instanceSize) {
    ASSERT(data()->flags & RW_COPIED_RO);
    *const_cast<uint32_t *>(&ro->instanceSize) = newSize;
    }
    cache.setFastInstanceSize(newSize);
    }

    size变化只会走会更新在缓存中。那么调用setInstanceSize的地方如下:

    • realizeClassWithoutSwift:类加载的时候计算。这里包括懒加载和非懒加载。这里会调用方法,根据类的实例变量进行size计算。这里是在_read_images的时候调用。
    • class_addIvar:动态添加属性的时候会重新计算实例大小。
    • objc_initializeClassPair_internal:动态添加类相关的初始化。

    instanceSize对于HPObject而言分配内存大小应该为8(isa) + 8(name)+4(age)= 20根据内存对齐应该分配24字节。


    3.7 initInstanceIsa

    inline void 
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
    }

    initInstanceIsa最终会调用initIsainitIsa最后会对isa进行绑定:

    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
    {
    ASSERT(!isTaggedPointer());

    isa_t newisa(0);

    if (!nonpointer) {
    newisa.setClass(cls, this);
    } else {
    ASSERT(!DisableNonpointerIsa);
    ASSERT(!cls->instancesRequireRawIsa());


    #if SUPPORT_INDEXED_ISA
    ASSERT(cls->classArrayIndex() > 0);
    newisa.bits = ISA_INDEX_MAGIC_VALUE;
    // isa.magic is part of ISA_MAGIC_VALUE
    // isa.nonpointer is part of ISA_MAGIC_VALUE
    newisa.has_cxx_dtor = hasCxxDtor;
    newisa.indexcls = (uintptr_t)cls->classArrayIndex();
    #else
    newisa.bits = ISA_MAGIC_VALUE;
    // isa.magic is part of ISA_MAGIC_VALUE
    // isa.nonpointer is part of ISA_MAGIC_VALUE
    # if ISA_HAS_CXX_DTOR_BIT
    newisa.has_cxx_dtor = hasCxxDtor;
    # endif
    newisa.setClass(cls, this);
    #endif
    newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
    }
    • isa_t是一个union
    • nonpointer表示是否进行指针优化。不优化直接走setClass逻辑,优化走else逻辑。


    作者:HotPotCat
    链接:https://www.jianshu.com/p/884275c811d5



    收起阅读 »

    React的路由,怎么开发得劲儿

    首先确定业务场景如果我们把场景设定在开发一个pc端管理后台的话,那么很常见的需求就是根据不同用户,配置不同的权限,显示不同的菜单项目,渲染不同的路由。那权限到底归谁管一般来说都是后台配置权限,然后驱动前端显示菜单,但我觉得这样不太好,加一个menu就要向后台申...
    继续阅读 »

    首先确定业务场景

    如果我们把场景设定在开发一个pc端管理后台的话,那么很常见的需求就是根据不同用户,配置不同的权限,显示不同的菜单项目,渲染不同的路由。

    那权限到底归谁管

    一般来说都是后台配置权限,然后驱动前端显示菜单,但我觉得这样不太好,加一个menu就要向后台申请,太不灵活,费劲儿。

    我觉得应该也给前台一定程度的权利,让其可以“绕过”后台主导一部分菜单项和路由项的渲染.

    __一言以蔽之__:

    前后台协同把事情办了,后台为主,前端为辅。

    基于以上分析,制定了一个解决方案

    首先列出一下“出场角色”:

    动态结构数据 :通过前后台协同创建数据,其描述的是一种树状关系。

    静态内容数据 :渲染路由和菜单项的基本数据信息。

    菜单项和其关联的路由 :根据以上数据驱动显示。

    静态内容配置

    主要为两个成员:
    • 路由配置:routesMap
    • 菜单项配置:menusMap

      二者相关性太高,故在一起进行管理。

    路由配置:routesMap

    作用:

    每一个路由都是一个单体对象,通过注册routesMap内部来进行统一管理。

    结构:
    {
    ...
    {
    name: "commonTitle_nest", //国际化单位ID
    icon: "thunderbolt", //antd的icon
    path: "/pageCenter/nestRoute", //路径规则
    exact: true, //是否严格匹配
    component: lazyImport(() =>
    import('ROUTES/login')
    ), //组件
    key: uuid() //唯一标识
    }
    ...
    }


    个体参数一览:
    参数类型说明默认值
    namestring国际化的标识ID_
    iconstringantd的icon标识-
    pathstring路径规则-
    exactboolan是否严格匹配false
    componentstring渲染组件-
    keystring唯一标识-
    redirectstring重定向路由地址-
    searchobject"?="-
    paramstringnumber"/*"-
    isNoFormatboolean标识拒绝国际化false

    基本是在react-router基础上进行扩展的,保留了其配置项。

    菜单项配置:menusMap

    作用:

    每个显示在左侧的菜单项目都是一个单体对象,菜单单体内容与路由对象进行关联,并通过注册routesToMenuMap内部来进行统一管理。

    结构:
    {
    ...
    [LIGHT_ID]: {
    ...routesMap.lightHome,
    routes: [
    routesMap.lightAdd,
    routesMap.lightEdit,
    routesMap.lightDetail,
    ],
    }
    ...
    }


    个体参数一览:
    参数类型说明默认值
    routesarray转载路由个体_

    该个体主要关联路由个体,故其参数基本与之一致

    动态结构配置

    主要为两个类别:
    • __menuLocalConfig.json__:前端期望的驱动数据。
    • __menuRemoteConfig.json__:后端期望的驱动数据。
    作用:

    __动静结合,驱动显示__:两文件融合作为动态数据,去激活静态数据(菜单项menusMap)来驱动显示菜单项目和渲染路由组件。

    强调:
    • __menuLocalConfig.json__:是动态数据的组成部份,是“动”中的“静”,完全由前端主导配置。
    • __menuRemoteConfig.json__:应该由后台配置权限并提供,前端配置该数据文件,目的是在后台未返回数据作默认配置,还有模拟mock开发使用。
    结构:
    [   
    ...
    {
    "menuId": 2001,
    "parentId": 1001
    }
    ...
    ]

    简单,直接地去表示结构的数据集合

    动态配置的解释:

    简单讲,对于驱动菜单项和路由的渲染,无论后台配置权限控制前端也好,前端想绕过后端主导显示也好,都是一种期望(种因)。二者协商,结合,用尽可能少的信息描述一个结构(枝繁),从而让静态数据对其进行补充(叶茂),然后再用形成的整体去驱动(结果)。

    快速上手

    注册路由个体

    位置在/src/routes/config.js,栗:

    /* 路由的注册数据,新建路由在这配置 */
    export const routesMap = {
    ...
    templates: {
    name: "commonTitle_nest",
    icon: "thunderbolt",
    path: "/pageCenter/nestRoute",
    exact: true,
    redirect: "/pageCenter/light",
    key: uuid()
    }
    ...
    }


    详:/路由相关/配置/静态内容配置

    决定该路由个体的“出场”

    位置同上,栗:

    /* 路由匹配menu的注册数据,新建后台驱动的menu在这配置 */
    export const menusMap = {
    ...
    [LIGHT_ID]: {
    ...routesMap.lightHome, //“主角”
    routes: [
    routesMap.lightAdd, //“配角”
    routesMap.lightEdit,
    routesMap.lightDetail,
    ],
    },
    ...
    }


    解:首先路由个体出现在该配置中,就说明出场(驱动渲染route)了,但是出场又分为两种:

    类别驱动显示了左侧 MenuItem可以跳转么
    主角可以
    配角没有可以

    以上就已经完成了静态数据的准备,接下来就等动态结构数据类激活它了。

    配置动态结构数据

    后台配置的权限:
    [
    { "menuId": 1002, "parentId": 0 },
    { "menuId": 1001, "parentId": 0 }
    ]

    主导

    前端自定义的权限:
    [
    { "menuId": 2002, "parentId": 1001 },
    { "menuId": 2001, "parentId": 1001 },
    { "menuId": 2003, "parentId": 0 },
    { "menuId": 2004, "parentId": 1002 },
    { "menuId": 2005, "parentId": 1002 }
    ]


    补充

    解:1***2***分别是后台和前台的命名约定(能区分就行,怎么定随意),通过以上数据不难看出二者结合描述了一个树状关系,进而去激活静态数据以驱动渲染页面的菜单和路由。

    简单讲:就是动态数据描述结构,静态数据描述内容,结构去和内容进行匹配,有就显示,没有也不会出问题,二者配合驱动显示。

    至此配置基本完成,可以通过直接修改该文件的方式进行开发和调整,也可以可视化操作。

    配置调整费劲?拖拽吧

    操作后自动刷新。

    自动生成文件
    menuLocalConfig.json

    menuRemoteConfig.json

    总结:

    这样我觉得react的路由开发起来得劲儿了不少,整体的解决方案已经确定,供参考。

    收起阅读 »

    宝, 来学习一下CSS中的宽高比,让 h5 开发更想你的夜!

    在图像和其他响应式元素的宽度和高度之间有一个一致的比例是很重要的。在CSS中,我们使用padding hack已经很多年了,但现在我们在CSS中有了原生的长宽比支持。在这篇文章中,我们将讨论什么是宽高比,我们过去是怎么做的,新的做法是什么。当然,也会有一些用例...
    继续阅读 »

    在图像和其他响应式元素的宽度和高度之间有一个一致的比例是很重要的。在CSS中,我们使用padding hack已经很多年了,但现在我们在CSS中有了原生的长宽比支持。

    在这篇文章中,我们将讨论什么是宽高比,我们过去是怎么做的,新的做法是什么。当然,也会有一些用例,对它们进行适当的回退。

    什么是高宽比

    根据维基百科的说法:

    在数学上,比率表示一个数字包含另一个数字的多少倍。例如,如果一碗水果中有八个橙子和六个柠檬,那么橙子和柠檬的比例是八比六(即8∶6,相当于比值4∶3)。

    在网页设计中,高宽比的概念是用来描述图像的宽度和高度应按比例调整。

    考虑下图

    比率是4:3,这表明苹果和葡萄的比例是4:3

    换句话说,我们可以为宽高比为4:3的最小框是4px * 3px框。 当此盒式高度按比例调整为其宽度时,我们将有一个致宽尺寸的框。

    考虑下图。

    盒子被按比例调整大小,其宽度和高度之间的比例是一致的。现在,让我们想象一下,这个盒子里有一张重要的图片,我们关心它的所有细节。

    请注意,无论大小如何,图像细节都被保留。通过拥有一致的高宽比,我们可以获得以下好处

    • 整个网站的图像将在不同的视口大小上保持一致。
    • 我们也可以有响应式的视频元素。
    • 它有助于设计师创建一个图像大小的清晰指南,这样开发者就可以在开发过程中处理它们。

    计算宽高比

    为了测量宽高比,我们需要将宽度除以如下图所示的高度。

    宽度和高度之间的比例是1.33。这意味着这个比例应该得到遵守。请考虑

    注意右边的图片,宽度÷高度的值是 1.02,这不是原来的长宽比(1.33或4:3)。

    你可能在想,如何得出4:3这个数值?嗯,这被称为最接近的正常长宽比,有一些工具可以帮助我们找到它。在进行UI设计时,强烈建议你确切地知道你所使用的图像的宽高比是多少。使用这个网址可以帮我们快速计算。

    网址地址:http://lawlesscreation.github...

    在 CSS 中实现宽高比

    我们过去是通过在CSS中使用百分比padding 来实现宽高比的。好消息是,最近,我们在所有主要的浏览器中都得到了aspect-ratio的原生支持。在深入了解原生方式之前,我们先首先解释一下好的老方法。

    当一个元素有一个垂直百分比的padding时,它将基于它的父级宽度。请看下图。

    当标题有padding-top: 50%时,该值是根据其父元素的宽度来计算的。因为父元素的宽度是200px,所以padding-top会变成100px

    为了找出要使用的百分比值,我们需要将图像的高度除以宽度。得到的数字就是我们要使用的百分比。

    假设图像宽度为260px,高度为195px

    Percentage padding = height / width

    195/260的结果为 0.75(或75%)。

    我们假设有一个卡片的网格,每张卡片都有一个缩略图。这些缩略图的宽度和高度应该是相等的。

    由于某些原因,运营上传了一张与其他图片大小不一致的图片。注意到中间那张卡的高度与其他卡的高度不一样。

    你可能会想,这还不容易解决?我们可以给图片加个object-fit: cover。问题解决了,对吗?不是这么简单滴。这个解决方案在多种视口尺寸下都不会好看。

    注意到在中等尺寸下,固定高度的图片从左边和右边被裁剪得太厉害,而在手机上,它们又太宽。所有这些都是由于使用了固定高度的原因。我们可以通过不同的媒体查询手动调整高度,但这不是一个实用的解决方案。

    我们需要的是,无论视口大小如何,缩略图的尺寸都要一致。为了实现这一点,我们需要使用百分比padding来实现一个宽高比。

    HTML

    <article class="card">
    <div class="card__thumb">
    <img src="thumb.jpg" alt="" />
    </div>
    <div class="card__content">
    <h3>Muffins Recipe</h3>
    <p>Servings: 3</p>
    </div>
    </article>

    CSS

    .card__thumb {
    position: relative;
    padding-top: 75%;
    }

    .card__thumb img {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    }


    通过上述,我们定义了卡片缩略图包装器(.card__thumb)的高度取决于其宽度。另外,图片是绝对定位的,它有它的父元素的全部宽度和高度,有object-fit: cover,用于上传不同大小的图片的情况。请看下面的动图。

    请注意,卡片大小的变化和缩略图的长宽比没有受到影响。

    aspect-ratio 属性

    今年早些时候,Chrome、Safari TP和Firefox Nightly都支持aspect-ratio CSS 属性。最近,它在Safari 15的官方版本中得到支持。

    我们回到前面的例子,我们可以这样改写它。

    /* 上面的方式 */
    .card__thumb {
    position: relative;
    padding-top: 75%;
    }

    /* 使用 aspect-ratio 属性 */
    .card__thumb {
    position: relative;
    aspect-ratio: 4/3;
    }


    请看下面的动图,了解宽高比是如何变化的。

    Demo 地址:https://codepen.io/shadeed/pe...

    有了这个,让我们探索原始纵横比可以有用的一些用例,以及如何以逐步增强的方法使用它。

    渐进增强

    我们可以通过使用CSS @supports和CSS变量来使用CSS aspect-ratio

    .card {
    --aspect-ratio: 16/9;
    padding-top: calc((1 / (var(--aspect-ratio))) * 100%);
    }

    @supports (aspect-ratio: 1) {
    .card {
    aspect-ratio: var(--aspect-ratio);
    padding-top: initial;
    }
    }


    Logo Images

    来看看下面的 logo

    你是否注意到它们的尺寸是一致的,而且它们是对齐的?来看看幕后的情况。

    // html
    <li class="brands__item">
    <a href="#">
    <img src="assets/batch-2/aanaab.png" alt="" />
    </a>
    </li>
    .brands__item a {
    padding: 1rem;
    }

    .brands__item img {
    width: 130px;
    object-fit: contain;
    aspect-ratio: 2/1;
    }


    我添加了一个130px的基本宽度,以便有一个最小的尺寸,而aspect-ratio会照顾到高度。

    蓝色区域是图像的大小,object-fit: contain是重要的,避免扭曲图像。

    Responsive Circles

    你是否曾经需要创建一个应该是响应式的圆形元素?CSS aspect-ratio是这种使用情况的最佳选择。

    .person {
    width: 180px;
    aspect-ratio: 1;
    }

    如果宽高比的两个值相同,我们可以写成aspect-ratio: 1而不是aspect-ratio: 1/1。如果你使用flexboxgrid ,宽度将是可选的,它可以被添加作为一个最小值。

    ~完,我是小智,宝,你学会了吗~


    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    原文:https://ishadeed.com/article/...


    收起阅读 »

    前端白屏监控探索

    背景不知从什么时候开始,前端白屏问题成为一个非常普遍的话题,'白屏' 甚至成为了前端 bug 的代名词:_喂,你的页面白了。_而且,'白' 这一现象似乎对于用户体感上来说更加强,回忆起 windows 系统的崩溃 '蓝屏'。可以说是非常相似了,甚至能明白了白屏...
    继续阅读 »

    背景

    不知从什么时候开始,前端白屏问题成为一个非常普遍的话题,'白屏' 甚至成为了前端 bug 的代名词:_喂,你的页面白了。_而且,'白' 这一现象似乎对于用户体感上来说更加强,回忆起 windows 系统的崩溃 '蓝屏'。
    可以说是非常相似了,甚至能明白了白屏这个词汇是如何统一出来的。那么,体感如此强烈的现象势必会给用户带来一些不好的影响,如何能尽早监听,快速消除影响就显得很重要了。

    为什么单独监控白屏

    不光光是白屏,白屏只是一种现象,我们要做的是精细化的异常监控。异常监控各个公司肯定都有自己的一套体系,集团也不例外,而且也足够成熟。但是通用的方案总归是有缺点的,如果对所有的异常都加以报警和监控,就无法区分异常的严重等级,并做出相应的响应,所以在通用的监控体系下定制精细化的异常监控是非常有必要的。这就是本文讨论白屏这一场景的原因,我把这一场景的边界圈定在了 “白屏” 这一现象。

    方案调研

    白屏大概可能的原因有两种:

    1. js 执行过程中的错误
    2. 资源错误

    这两者方向不同,资源错误影响面较多,且视情况而定,故不在下面方案考虑范围内。为此,参考了网上的一些实践加上自己的一些调研,大概总结出了一些方案:

    一、onerror + DOM 检测

    原理很简单,在当前主流的 SPA 框架下,DOM 一般挂载在一个根节点之下(比如 <div id="root"></div> )发生白屏后通常现象是根节点下所有 DOM 被卸载,该方案就是通过监听全局的 onerror 事件,在异常发生时去检测根节点下是否挂载 DOM,若无则证明白屏。
    我认为是非常简单暴力且有效的方案。但是也有缺点:其一切建立在 **白屏 === 根节点下 DOM 被卸载** 成立的前提下,实际并非如此比如一些微前端的框架,当然也有我后面要提到的方案,这个方案和我最终方案天然冲突。

    二、Mutation Observer Api

    不了解的可以看下文档
    其本质是监听 DOM 变化,并告诉你每次变化的 DOM 是被增加还是删除。为其考虑了多种方案:

    1. 搭配 onerror 使用,类似第一个方案,但很快被我否决了,虽然其可以很好的知道 DOM 改变的动向,但无法和具体某个报错联系起来,两个都是事件监听,两者是没有必然联系的。
    2. 单独使用判断是否有大量 DOM 被卸载,缺点:白屏不一定是 DOM 被卸载,也有可能是压根没渲染,且正常情况也有可能大量 DOM 被卸载。完全走不通。
    3. 单独使用其监听时机配合 DOM 检测,其缺点和方案一一样,而且我觉得不如方案一。因为它没法和具体错误联系起来,也就是没法定位。当然我和其他团队同学交流的时候他们给出了其他方向:通过追踪用户行为数据来定位问题,我觉得也是一种方法。

    一开始我认为这就是最终答案,经过了漫长的心里斗争,最终还是否定掉了。不过它给了一个比较好的监听时机的选择。

    三、饿了么-Emonitor 白屏监控方案

    饿了么的白屏监控方案,其原理是记录页面打开 4s 前后 html 长度变化,并将数据上传到饿了么自研的时序数据库。如果一个页面是稳定的,那么页面长度变化的分布应该呈现「幂次分布」曲线的形态,p10、p20 (排在文档前 10%、20%)等数据线应该是平稳的,在一定的区间内波动,如果页面出现异常,那么曲线一定会出现掉底的情况。

    其他

    其他都大同小样,其实调研了一圈下来发现无非就是两点

    1. 监控时机:调研下来常见的就三种:

      • onerror
      • mutation observer api
      • 轮训
    2. DOM 检测:这个方案就很多了,除了上述的还可以:

      • elementsFromPoint api 采样
      • 图像识别
      • 基于 DOM 的各种数据的各种算法识别
      • ...

    改变方向

    几番尝试下来几乎没有我想要的,其主要原因是准确率 -- 这些方案都不能保证我监听到的是白屏,单从理论的推导就说不通。他们都有一个共同点:监听的是'白屏'这个现象,从现象去推导本质虽然能成功,但是不够准确。所以我真正想要监听的是造成白屏的本质。

    那么回到最开始,什么是白屏?他是如何造成的?是因为错误导致的浏览器无法渲染?不,在这个 spa 框架盛行的现在实际上的白屏是框架造成的,本质是由于错误导致框架不知道怎么渲染所以干脆就不渲染。由于我们团队 React 技术栈居多,我们来看看 React 官网的一段话

    React 认为把一个错误的 UI 保留比完全移除它更糟糕。我们不讨论这个看法的正确与否,至少我们知道了白屏的原因:渲染过程的异常且我们没有捕获异常并处理。

    反观目前的主流框架:我们把 DOM 的操作托管给了框架,所以渲染的异常处理不同框架方法肯定不一样,这大概就是白屏监控难统一化产品化的原因。但大致方向肯定是一样的。

    那么关于白屏我认为可以这么定义:异常导致的渲染失败

    那么白屏的监控方案即:监控渲染异常。那么对于 React 而言,答案就是: Error Boundaries

    Error Boundaries

    我们可以称之为错误边界,错误边界是什么?它其实就是一个生命周期,用来监听当前组件的 children 渲染过程中的错误,并可以返回一个 降级的 UI 来渲染:

    class ErrorBoundary extends React.Component {
    constructor(props) {
    super(props);
    this.state = { hasError: false };
    }

    static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
    // 我们可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
    }

    render() {
    if (this.state.hasError) {
    // 我们可以自定义降级后的 UI 并渲染
    return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
    }
    }


    一个有责任心的开发一定不会放任错误的发生。错误边界可以包在任何位置并提供降级 UI,也就是说,一旦开发者'有责任心' 页面就不会全白,这也是我之前说的方案一与之天然冲突且其他方案不稳定的情况。
    那么,在这同时我们上报异常信息,这里上报的异常一定会导致我们定义的白屏,这一推导是 100% 正确的。

    100% 这个词或许不够负责,接下来我们来看看为什么我说这一推导是 100% 准确的:

    React 渲染流程

    我们来简单回顾下从代码到展现页面上 React 做了什么。
    我大致将其分为几个阶段:render => 任务调度 => 任务循环 => 提交 => 展示
    我们举一个简单的例子来展示其整个过程(任务调度不再本次讨论范围故不展示):

    const App = ({ children }) => (
    <>
    <p>hello</p>
    { children }
    </>
    );
    const Child = () => <p>I'm child</p>

    const a = ReactDOM.render(
    <App><Child/></App>,
    document.getElementById('root')
    );


    首先浏览器是不认识我们的 jsx 语法的,所以我们通过 babel 编译大概能得到下面的代码:

    var App = function App(_ref2) {
    var children = _ref2.children;
    return React.createElement("p", null, "hello"), children);
    };

    var Child = function Child() {
    return React.createElement("p", null, "I'm child");
    };

    ReactDOM.render(React.createElement(App, null, React.createElement(Child, null)), document.getElementById('root'));

    babel 插件将所有的 jsx 都转成了 createElement 方法,执行它会得到一个描述对象 ReactElement 大概长这样子:

    {
    $$typeof: Symbol(react.element),
    key: null,
    props: {}, // createElement 第二个参数 注意 children 也在这里,children 也会是一个 ReactElement 或 数组
    type: 'h1' // createElement 的第一个参数,可能是原生的节点字符串,也可能是一个组件对象(Function、Class...)
    }


    所有的节点包括原生的 <a></a> 、 <p></p> 都会创建一个 FiberNode ,他的结构大概长这样:

    FiberNode = {
    elementType: null, // 传入 createElement 的第一个参数
    key: null,
    type: HostRoot, // 节点类型(根节点、函数组件、类组件等等)
    return: null, // 父 FiberNode
    child: null, // 第一个子 FiberNode
    sibling: null, // 下一个兄弟 FiberNode
    flag: null, // 状态标记
    }


    你可以把它理解为 Virtual Dom 只不过多了许多调度的东西。最开始我们会为根节点创建一个 FiberNodeRoot 如果有且仅有一个 ReactDOM.render 那么他就是唯一的根,当前有且仅有一个 FiberNode 树。

    我只保留了一些渲染过程中重要的字段,其他还有很多用于调度、判断的字段我这边就不放出来了,有兴趣自行了解

    render

    现在我们要开始渲染页面,是我们刚才的例子,执行 ReactDOM.render 。这里我们有个全局 workInProgress 对象标志当前处理的 FiberNode

    1. 首先我们为根节点初始化一个 FiberNodeRoot ,他的结构就如上面所示,并将 workInProgress= FiberNodeRoot
    2. 接下来我们执行 ReactDOM.render 方法的第一个参数,我们得到一个 ReactElement :
    ReactElement = {
    $$typeof: Symbol(react.element),
    key: null,
    props: {
    children: {
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ƒ Child(),
    }
    }
    ref: null,
    type: f App()
    }


    该结构描述了 <App><Child /></App>

    1. 我们为 ReactElement 生成一个 FiberNode 并把 return 指向父 FiberNode ,最开始是我们的根节点,并将 workInProgress = FiberNode
    {
    elementType: f App(), // type 就是 App 函数
    key: null,
    type: FunctionComponent, // 函数组件类型
    return: FiberNodeRoot, // 我们的根节点
    child: null,
    sibling: null,
    flags: null
    }


    1. 只要workInProgress 存在我们就要处理其指向的 FiberNode 。节点类型有很多,处理方法也不太一样,不过整体流程是相同的,我们以当前函数式组件为例子,直接执行 App(props) 方法,这里有两种情况

      • 该组件 return 一个单一节点,也就是返回一个 ReactElement 对象,重复 3 - 4 的步骤。并将当前 节点的 child 指向子节点 CurrentFiberNode.child = ChildFiberNode 并将子节点的 return 指向当前节点 ChildFiberNode.return = CurrentFiberNode
      • 该组件 return 多个节点(数组或者 Fragment ),此时我们会得到一个 ChildiFberNode 的数组。我们循环他,每一个节点执行 3 - 4 步骤。将当前节点的 child 指向第一个子节点 CurrentFiberNode.child = ChildFiberNodeList[0] ,同时每个子节点的 sibling 指向其下一个子节点(如果有) ChildFiberNode[i].sibling = ChildFiberNode[i + 1] ,每个子节点的 return 都指向当前节点 ChildFiberNode[i].return = CurrentFiberNode

    如果无异常每个节点都会被标记为待布局 FiberNode.flags = Placement

    1. 重复步骤直到处理完全部节点 workInProgress 为空。

    最终我们能大概得到这样一个 FiberNode 树:

    FiberNodeRoot = {
    elementType: null,
    type: HostRoot,
    return: null,
    child: FiberNode<App>,
    sibling: null,
    flags: Placement, // 待布局状态
    }

    FiberNode<App> {
    elementType: f App(),
    type: FunctionComponent,
    return: FiberNodeRoot,
    child: FiberNode<p>,
    sibling: null,
    flags: Placement // 待布局状态
    }

    FiberNode<p> {
    elementType: 'p',
    type: HostComponent,
    return: FiberNode<App>,
    sibling: FiberNode<Child>,
    child: null,
    flags: Placement // 待布局状态
    }

    FiberNode<Child> {
    elementType: f Child(),
    type: FunctionComponent,
    return: FiberNode<App>,
    child: null,
    flags: Placement // 待布局状态
    }


    提交阶段

    提交阶段简单来讲就是拿着这棵树进行深度优先遍历 child => sibling,放置 DOM 节点并调用生命周期。

    那么整个正常的渲染流程简单来讲就是这样。接下来看看异常处理

    错误边界流程

    刚刚我们了解了正常的流程现在我们制造一些错误并捕获他:

    const App = ({ children }) => (
    <>
    <p>hello</p>
    { children }
    </>
    );
    const Child = () => <p>I'm child {a.a}</p>

    const a = ReactDOM.render(
    <App>
    <ErrorBoundary><Child/></ErrorBoundary>
    </App>,
    document.getElementById('root')
    );


    执行步骤 4 的函数体是包裹在 try...catch 内的如果捕获到了异常则会走异常的流程:

    do {
    try {
    workLoopSync(); // 上述 步骤 4
    break;
    } catch (thrownValue) {
    handleError(root, thrownValue);
    }
    } while (true);

    执行步骤 4 时我们调用 Child 方法由于我们加了个不存在的表达式 {a.a} 此时会抛出异常进入我们的 handleError 流程此时我们处理的目标是 FiberNode<Child> ,我们来看看 handleError :

    function handleError(root, thrownValue): void {
    let erroredWork = workInProgress; // 当前处理的 FiberNode 也就是异常的 节点
    throwException(
    root, // 我们的根 FiberNode
    erroredWork.return, // 父节点
    erroredWork,
    thrownValue, // 异常内容
    );
    completeUnitOfWork(erroredWork);
    }

    function throwException(
    root: FiberRoot,
    returnFiber: Fiber,
    sourceFiber: Fiber,
    value: mixed,
    ) {
    // The source fiber did not complete.
    sourceFiber.flags |= Incomplete;

    let workInProgress = returnFiber;
    do {
    switch (workInProgress.tag) {
    case HostRoot: {
    workInProgress.flags |= ShouldCapture;
    return;
    }
    case ClassComponent:
    // Capture and retry
    const ctor = workInProgress.type;
    const instance = workInProgress.stateNode;
    if (
    (workInProgress.flags & DidCapture) === NoFlags &&
    (typeof ctor.getDerivedStateFromError === 'function' ||
    (instance !== null &&
    typeof instance.componentDidCatch === 'function' &&
    !isAlreadyFailedLegacyErrorBoundary(instance)))
    ) {
    workInProgress.flags |= ShouldCapture;
    return;
    }
    break;
    default:
    break;
    }
    workInProgress = workInProgress.return;
    } while (workInProgress !== null);
    }


    代码过长截取一部分
    先看 throwException 方法,核心两件事:

    1. 将当前也就是出问题的节点状态标志为未完成 FiberNode.flags = Incomplete
    2. 从父节点开始冒泡,向上寻找有能力处理异常( ClassComponent )且的确处理了异常的(声明了 getDerivedStateFromError 或 componentDidCatch 生命周期)节点,如果有,则将那个节点标志为待捕获 workInProgress.flags |= ShouldCapture ,如果没有则是根节点。

    completeUnitOfWork 方法也类似,从父节点开始冒泡,找到 ShouldCapture 标记的节点,如果有就标记为已捕获 DidCapture  ,如果没找到,则一路把所有的节点都标记为 Incomplete 直到根节点,并把 workInProgress 指向当前捕获的节点。

    之后从当前捕获的节点(也有可能没捕获是根节点)开始重新走流程,由于其状态 react 只会渲染其降级 UI,如果有 sibling 节点则会继续走下面的流程。我们看看上述例子最终得到的 FiberNode 树:

    FiberNodeRoot = {
    elementType: null,
    type: HostRoot,
    return: null,
    child: FiberNode<App>,
    sibling: null,
    flags: Placement, // 待布局状态
    }

    FiberNode<App> {
    elementType: f App(),
    type: FunctionComponent,
    return: FiberNodeRoot,
    child: FiberNode<p>,
    sibling: null,
    flags: Placement // 待布局状态
    }

    FiberNode<p> {
    elementType: 'p',
    type: HostComponent,
    return: FiberNode<App>,
    sibling: FiberNode<ErrorBoundary>,
    child: null,
    flags: Placement // 待布局状态
    }

    FiberNode<ErrorBoundary> {
    elementType: f ErrorBoundary(),
    type: ClassComponent,
    return: FiberNode<App>,
    child: null,
    flags: DidCapture // 已捕获状态
    }

    FiberNode<h1> {
    elementType: f ErrorBoundary(),
    type: ClassComponent,
    return: FiberNode<ErrorBoundary>,
    child: null,
    flags: Placement // 待布局状态
    }


    如果没有配置错误边界那么根节点下就没有任何节点,自然无法渲染出任何内容。

    ok,相信到这里大家应该清楚错误边界的处理流程了,也应该能理解为什么我之前说由 ErrorBoundry 推导白屏是 100% 正确的。当然这个 100% 指的是由 ErrorBoundry 捕捉的异常基本上会导致白屏,并不是指它能捕获全部的白屏异常。以下场景也是他无法捕获的:

    • 事件处理
    • 异步代码
    • SSR
    • 自身抛出来的错误

    React SSR 设计使用流式传输,这意味着服务端在发送已经处理好的元素的同时,剩下的仍然在生成 HTML,也就是其父元素无法捕获子组件的错误并隐藏错误的组件。这种情况似乎只能将所有的 render 函数包裹 try...catch ,当然我们可以借助 babel 或 TypeScript 来帮我们简单实现这一过程,其最终得到的效果是和 ErrorBoundry 类似的。

    而事件和异步则很巧,虽说 ErrorBoundry 无法捕获他们之中的异常,不过其产生的异常也恰好不会造成白屏(如果是错误的设置状态,间接导致了白屏,刚好还是会被捕获到)。这就在白屏监控的职责边界之外了,需要别的精细化监控能力来处理它。

    总结

    那么最后总结下本文的出的几个结论:
    我对白屏的定义:异常导致的渲染失败
    对应方案是:资源监听 + 渲染流程监听

    在目前 SPA 框架下白屏的监控需要针对场景做精细化的处理,这里以 React 为例子,通过监听渲染过程异常能够很好的获得白屏的信息,同时能增强开发者对异常处理的重视。而其他框架也会有相应的方法来处理这一现象。

    当然这个方案也有弱点,由于是从本质推导现象其实无法 cover 所有的白屏的场景,比如我要搭配资源的监听来处理资源异常导致的白屏。当然没有一个方案是完美的,我这里也是提供一个思路,欢迎大家一起讨论。


    收起阅读 »

    面试官问我会canvas? 我可以绘制一个烟花?动画

    在我们日常开发中贝塞尔曲线无处不在:svg 中的曲线(支持 2阶、 3阶)canvas 中绘制贝塞尔曲线几乎所有前端2D或3D图形图表库(echarts,d3,three.js)都会使用到贝塞尔曲线所以掌握贝塞尔曲线势在必得。 这篇文章主要是实战篇,不会介绍和...
    继续阅读 »

    在我们日常开发中贝塞尔曲线无处不在:

    1. svg 中的曲线(支持 2阶、 3阶)
    2. canvas 中绘制贝塞尔曲线
    3. 几乎所有前端2D或3D图形图表库(echarts,d3,three.js)都会使用到贝塞尔曲线

    所以掌握贝塞尔曲线势在必得。 这篇文章主要是实战篇,不会介绍和贝塞尔相关的知识, 如果有同学对贝塞尔曲线不是很清楚的话:可以查看我这篇文章——深入理解SVG

    绘制贝塞尔曲线

    第一步我们先创建ctx, 用ctx 画一个二阶贝塞尔曲线看下。二阶贝塞尔曲线有1个控制点,一个起点,一个终点。

    const canvas = document.getElementById( 'canvas' );
    const ctx = canvas.getContext( '2d' );
    ctx.beginPath();
    ctx.lineWidth = 2;
    ctx.strokeStyle = '#000';
    ctx.moveTo(100,100)
    ctx.quadraticCurveTo(180,50, 200,200)
    ctx.stroke();


    这样我们就画好了一个贝塞尔曲线了。

    绘制贝塞尔曲线动画

    画一条线谁不会哇?接下来文章的主体内容。 首先试想一下动画我们肯定一步步画出曲线? 但是这个ctx给我们全部画出来了是不是有点问题。我们重新看下二阶贝塞尔曲线的实现过程动画,看看是否有思路。

    从图中可以分析得出贝塞尔上的曲线是和t有关系的, t的区间是在0-1之间,我们是不是可以通过二阶贝塞尔的曲线方程去算出每一个点呢,这个专业术语叫离散化,但是这样的得出来的点的信息是不太准的,我们先这样实现。

    先看下方程:

    我们模拟写出代码如如下:

    //这个就是二阶贝塞尔曲线方程
    function twoBezizer(p0, p1, p2, t) {
    const k = 1 - t
    return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2
    }

    //离散
    function drawWithDiscrete(ctx, start, control, end,percent) {
    for ( let t = 0; t <= percent / 100; t += 0.01 ) {
    const x = twoBezizer(start[0], control[0], end[0], t)
    const y = twoBezizer(start[1], control[1], end[1], t)
    ctx.lineTo(x, y)
    }
    }


    我们看下效果:

    和我们画的几乎是一模一样,接下啦就用requestAnimationFrame 开始我们的动画给出以下代码:

    let percent = 0
    function animate() {
    ctx.clearRect( 0, 0, 800, 800 );
    ctx.beginPath();
    ctx.moveTo(100,100)
    drawWithDiscrete(ctx,[100,100],[180,50],[200,200],percent)
    ctx.stroke();
    percent = ( percent + 1 ) % 100;
    id = requestAnimationFrame(animate)
    }
    animate()


    这里有两个要注意的是, 我是是percent 不断加1 和100 求余,所以呢 percent 会不断地从1-100 这样往复,OK所以我们必须要动画之前做一次区域清理, ctx.clearRect( 0, 0, 800, 800 ); 这样就可以不断的从开始到结束循环往复,我们看下效果:

    看着样子是不是还不错哈哈哈😸。

    绘制贝塞尔曲线动画方法2

    你以为这样就结束了? 当然不是难道我们真的没有办法画出某一个t的贝塞尔曲线了? 当前不是,这里放一下二阶贝塞尔方程的推导过程:

    二阶贝塞尔曲线上的任意一点,都是可以通过同样比例获得。 在两点之间的任意一点,其实满足的一阶贝塞尔曲线, 一阶贝塞尔曲线满足的其实是线性变化。我给出以下方程

     function oneBezizer(p0,p1,t) {
    return p0 + (p1-p0) * t
    }

    从我画的图可以看出,我们只要 不断求A点 和C点就可以画出在某一时间段的贝塞尔了。

    我给出以下代码和效果图:

    function drawWithDiscrete2(ctx, start, control, end,percent) {
    const t = percent/ 100;
    // 求出A点
    const A = [];
    const C = [];
    A[0] = oneBezizer(start[0],control[0],t);
    A[1] = oneBezizer(start[1],control[1],t);
    C[0] = twoBezizer(start[0], control[0], end[0], t)
    C[1] = twoBezizer(start[1], control[1], end[1], t)
    ctx.quadraticCurveTo(
    A[ 0 ], A [ 1 ],
    C[ 0 ], C[ 1 ]
    );
    }


    礼花🎉动画

    上文我们实现了一条贝塞尔线,我们将这条贝塞尔的曲线的开始点作为一个圆的圆心,然后按照某个次数求出不同的结束点。 再写一个随机颜色,礼花效果就成了, 直接上代码,

    for(let i=0; i<count; i++) {
    const angle = Math.PI * 2 / count * i;
    const x = center[ 0 ] + radius * Math.sin( angle );
    const y = center[ 1 ] + radius * Math.cos( angle );
    ctx.strokeStyle = colors[ i ];
    ctx.beginPath();
    drawWithDiscrete(ctx, center,[180,50],[x,y],percent)
    ctx.stroke();
    }

    function getRandomColor(colors, count) {
    // 生成随机颜色
    for ( let i = 0; i < count; i++ ) {
    colors.push(
    'rgb( ' +
    ( Math.random() * 255 >> 0 ) + ',' +
    ( Math.random() * 255 >> 0 ) + ',' +
    ( Math.random() * 255 >> 0 ) +
    ' )'
    );
    }
    }


    我们看下动画吧:



    收起阅读 »

    在 React 应用中展示报表数据

    创建 React 应用创建 React 应用 参考链接, 如使用npx 包运行工具:npx create-react-app arjs-react-viewer-app如果您使用的是yarn,执行命令:yarn create react-app arjs-re...
    继续阅读 »

    创建 React 应用

    创建 React 应用 参考链接, 如使用npx 包运行工具:

    npx create-react-app arjs-react-viewer-app
    如果您使用的是yarn,执行命令:

    yarn create react-app arjs-react-viewer-app
    更多创建 React方法可参考 官方文档

    安装 ActivereportsJS NPM 包

    React 报表 Viewer 组件已经放在了npm @grapecity/activereports-react npm 中。 @grapecity/activereports 包提供了全部的核心功能。

    运行以下命令安装包:

    npm install @grapecity/activereports-react @grapecity/activereports
    或使用yarn命令

    yarn add @grapecity/activereports-react @grapecity/activereports

    导入 ActiveReportsJS 样式

    打开 src\App.css 文件并添加以下代码,导入Viewer 的默认样式,定义了元素宿主的样式React Report Viewer 控件:

    @import "@grapecity/activereports/styles/ar-js-ui.css";
    @import "@grapecity/activereports/styles/ar-js-viewer.css";

    viewer-host {

    width: 100%;
    height: 100vh;
    }

    添加 ActiveReportsJS 报表

    ActiveReportsJS 使用 JSON格式和 rdlx-json扩展报表模板文件。在应用程序的public文件夹中,创建名为 report.rdlx-json 的新文件,并在该文件中插入以下JSON内容:

    {
    "Name": "Report",
    "Body": {

    "ReportItems": [
    {
    "Type": "textbox",
    "Name": "TextBox1",
    "Value": "Hello, ActiveReportsJS Viewer",
    "Style": {
    "FontSize": "18pt"
    },
    "Width": "8.5in",
    "Height": "0.5in"
    }
    ]
    }
    }


    添加 React 报表 Viewer 控件

    修改 src\App.js代码:

    import React from "react";
    import "./App.css";
    import { Viewer } from "@grapecity/activereports-react";

    function App() {
    return (

    <div id="viewer-host">
    <Viewer report={{ Uri: 'report.rdlx-json' }} />
    </div>
    );
    }


    export default App;

    运行和调试

    使用 npm start 或 yarn start 命令运行项目,如果编译失败了,报以下错误,请删除node_modules 文件夹并重新运行 npm install 或 yarn命令来重新安装需要的包文件。

    react-scripts start

    internal/modules/cjs/loader.js:883
    throw err;
    ^

    Error: Cannot find module 'react'
    当应用程序启动时,ActiveReportsJS Viewer组件将出现在页面上。Viewer将显示显示“ Hello,ActiveReportsJS Viewer”文本的报表。您可以通过使用工具栏上的按钮或将报表导出为可用格式之一来测试。

    原文:https://segmentfault.com/a/1190000040257641

    收起阅读 »

    【开源项目】声网Agora+环信IM实现的社交APP---CircleLive

    CircleLive分享,遇见,Live分享自己絮语,心情,碎碎念,在Live中遇见共鸣。技术支持1、Agora互动直播SDK多人音视频互动2、Agora云信令实时消息通信,Live过程的消息分发3、环信IM 支持,建立好有关系,会话主要功能1、发布和浏览心情...
    继续阅读 »

    CircleLive
    分享,遇见,Live

    分享自己絮语,心情,碎碎念,在Live中遇见共鸣。

    技术支持
    1、Agora互动直播SDK
    多人音视频互动

    2、Agora云信令
    实时消息通信,Live过程的消息分发

    3、环信
    IM 支持,建立好有关系,会话

    主要功能
    1、发布和浏览心情动态
    2、Live
      -发布Live
      -订阅共鸣Live
      -多人音视频Live
      -Live心情滤镜
    3、IM
      -加好友(自动创建会话)
      -基本IM(文字,语言,图片,视频,表情)


    视频Demo




    安装包
    Apk下载:链接: https://pan.baidu.com/s/111xZY2ANYCNZORt0ydLg9Q 密码: voil


    视频和APK链接如果失效,请email我 stonelavender@hotmail.com

    测试账号
    也可以新注册

    欢天喜地
    你好
    灰色头像
    密码均为:123456

    收起阅读 »

    【开源项目】用环信IM实现的公益APP--宝贝回家Baby back home

    项目背景有时,只一瞬间没回头,生命中的最重要就消失不见。 这是电影《亲爱的》中一句最经典的台词,看完整个电影,就会明白失去孩子的父母有多无助,拐卖孩子的人贩子有多可恨。 当今社会通过网络平台,短视频,404页面等寻亲信息曝光,很多热心群众都自发参与帮助走失儿童...
    继续阅读 »

    项目背景
    有时,只一瞬间没回头,生命中的最重要就消失不见。 这是电影《亲爱的》中一句最经典的台词,看完整个电影,就会明白失去孩子的父母有多无助,拐卖孩子的人贩子有多可恨。 当今社会通过网络平台,短视频,404页面等寻亲信息曝光,很多热心群众都自发参与帮助走失儿童寻找家人。但还没有发现一个功能实用,寻亲信息集中的移动端公益平台。 本项目是一个为丢失家人的家庭提供发布寻人信息和搜集线索的移动端平台,借助网络传播的力量,通过环信IM一对一实时消息,声网音视频的高效连接,帮助走失的宝贝尽快回归家庭。

    项目说明
    本项目使用OC开发
    接入声网SDK AgoraRtcEngine_iOS
    接入环信SDK EaseCallKit

    运行说明
    本项目使用iOS设备运行
    在注册页面注册账号后就能正常登录

    开发环境
    Xcode 12.1

    运行环境
    iOS 11.0

    功能介绍
    0, 注册登录
    本项目登录注册功能全部采用环信sdk提供的登录注册功能

    1, 首页
    首页主要是展示寻人信息,点击列表可以进入查看详情并私信发布人,利用环信一对一实时通讯让有线索的人更高效顺畅的与寻人者建立通讯。

    2, 寻找
    寻找页可以按条件筛选丢失人员的信息,除内容关键词搜索外,还可按地区,按发布时间,按性别分类筛选,快速查找到符合的信息。

    3,消息
    可以接收到有线索人的私信,点击头像添加好友,进行聊天。 界面右上角 “+” 号可以搜索添加好友,左上角点击进入联系人界面。

    4,我的
    个人信息管理。可更换头像,修改昵称,查看我发布的信息,以及退出登录

    5,发布
    发布寻人信息,输入被寻人相关信息,丢失原因,时间,地址,姓名,性别,联系方式等。发布后还可以将本条信息转发至qq群,微信群,微信好友,借助网友的力量一起寻找。 因没有服务器,使用的本地数据库。发布后可以在首页查看


    项目截图:





    iOS端源码下载地址:https://hub.fastgit.org/AgoraIO-Community/RTE-2021-Innovation-Challenge/tree/master/Application-Challenge/%E3%80%90%E5%BC%A0%E5%AE%87%E3%80%91%E5%AE%9D%E8%B4%9D%E5%9B%9E%E5%AE%B6/Baby%20back%20home


    欢迎添加环信冬冬微信,联系该项目作者

    收起阅读 »

    OC 对象、位域、isa

    一、对象的本质1.1 clang1.1.1clang 概述Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexp...
    继续阅读 »

    一、对象的本质

    1.1 clang

    1.1.1clang 概述

    Clang是一个C语言C++Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

    Clang是一个由Apple主导编写,基于LLVMC/C++/Objective-C编译器。
    它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC

    1.1.2 clang与xcrun命令

    1.1.2.1 clang

    把目标文件编译成c++文件,最简单的方式:

    clang -rewrite-objc main.m -o main.cpp

    如果包含其它SDK,比如UIKit则需要指定isysroot

    clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m -o main.cpp

    • 如果找不到Foundation则需要排查clang版本设置是否正确。使用which clang可以直接查看路径。有些公司会使用clang-format来进行代码格式化,需要排查环境变量中是否导出了相关路径(如果导出先屏蔽掉)。正常路径为/usr/bin/clang
    • isysroot也可以导出环境变量进行配置方便使用。

    1.1.2.2 xcrun(推荐)

    xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了 一些封装,比clang更好用。


    模拟器命令:

    xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64simulator.cpp

    真机命令:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

    1.2 对象c++代码分析

    main.m文件如下,直接生成对应的.cpp文件对HotpotCat进行分析。

    #import <Foundation/Foundation.h>

    @interface HotpotCat : NSObject

    @end

    @implementation HotpotCat

    @end

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
    }

    1.2.1 对象在底层是结构体

    直接搜索HotpotCat

    可以看到生成了HotpotCat_IMPL是一个结构体,那么HotpotCat_IMPL就是HotpotCat的底层实现么?对HotpotCat增加属性hp_name:

    @property(nonatomic, copy) NSString *hp_name;

    重新生成.cpp文件:

    这也就验证了HotpotCat_IMPL就是HotpotCat的底层实现,那么说明: 对象在底层的本质就是结构体

    HotpotCat_IMPL结构体中又嵌套了NSObject_IMPL结构体,这可以理解为继承。
    NSObject_IMPL定义如下:


    struct NSObject_IMPL {
    Class isa;
    };

    所以NSObject_IVARS就是成员变量isa

    1.2.2 objc_object & objc_class

    HotpotCat_IMPL上面有如下代码:

    typedef struct objc_object HotpotCat;

    为什么HotpotCatobjc_object类型?这是因为NSObject的底层实现就是objc_object

    同样的Class定义如下:

    typedef struct objc_class *Class;

    objc_class类型的结构体指针。

    同样可以看到idobjc_object结构体类型指针。

    typedef struct objc_object *id;

    这也就是id声明的时候不需要*的原因。


    1.2.3 setter & getter

    .cpp文件中有以下代码:


    // @property(nonatomic, copy) NSString *hp_name;


    /* @end */


    // @implementation HotpotCat

    //这里是setter和getter 参数self _cmd 隐藏参数
    static NSString * _I_HotpotCat_hp_name(HotpotCat * self, SEL _cmd) {
    //return self + 成员变量偏移
    return (*(NSString **)((char *)self + OBJC_IVAR_$_HotpotCat$_hp_name));
    }
    extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

    static void _I_HotpotCat_setHp_name_(HotpotCat * self, SEL _cmd, NSString *hp_name) {
    objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct HotpotCat, _hp_name), (id)hp_name, 0, 1);
    }
    // @end
    • 根据系统默认注释和函数名称确认这里是hp_namesettergetter方法。
    • getter方法返回hp_name是通过self + 成员变量偏移获取的。getter同理。

    二、位域

    struct Direction {
    BOOL left;
    BOOL right;
    BOOL front;
    BOOL back;
    };

    上面是一个记录方向的结构体。这个结构体占用4字节32位:00000000 00000000 00000000 00000000。但是对于BOOL值只有两种情况YES/NO。那么如果能用40000来代替前后左右,就只需要0.5个字节就能表示这个数据结构了(虽然只需要0.5字节,但是数据单元最小为1字节)。那么Direction的实现显然浪费了3倍的空间。有什么优化方式呢?位域

    2.1 结构体位域

    修改DirectionHPDirection:


    struct HPDirection {
    BOOL left : 1;
    BOOL right : 1;
    BOOL front : 1;
    BOOL back : 1;
    };

    格式为:数据类型 位域名称:位域长度

    验证:

    struct Direction dir;
    dir.left = YES;
    dir.right = YES;
    dir.front = YES;
    dir.back = YES;
    struct HPDirection hpDir;
    hpDir.left = YES;
    hpDir.right = YES;
    hpDir.front = YES;
    hpDir.back = YES;
    printf("\nDirection size:%zu\nHPDirection size:%zu\n",sizeof(dir),sizeof(hpDir));



    2.2 联合体

    2.2.1结构体&联合体对比


    //结构体联合体对比
    //共存
    struct HPStruct {
    char *name;
    int age;
    double height;
    };

    //互斥
    union HPUnion {
    char *name;
    int age;
    double height;
    };


    void testStructAndUnion() {
    struct HPStruct s;
    union HPUnion u;
    s.name = "HotpotCat";
    u.name = "HotpotCat";
    s.age = 18;
    u.age = 18;
    s.height = 180.0;
    u.height = 180.0;
    }
    分别定义了HPStruct结构体和HPUnion共用体,在整个赋值过程中变化如下:

    总结:

    • 结构体(struct)中所有变量是“共存”的。
      优点:“有容乃大”, 全面;
      缺点:struct内存空间的分配是粗放的,不管用不用全分配。
    • 联合体/共用体(union)中是各变量是“互斥”的。
      缺点:不够“包容”;
      优点:内存使用更为精细灵活,节省了内存空间。
    • 联合体在未进行赋值前数据成员会存在脏数据。

    2.2.2 联合体位域

    HPDirectionItem.h

    @interface HPDirectionItem : NSObject

    @property (nonatomic, assign) BOOL left;
    @property (nonatomic, assign) BOOL right;
    @property (nonatomic, assign) BOOL front;
    @property (nonatomic, assign) BOOL back;

    @end
    HPDirectionItem.m:

    #define HPDirectionLeftMask   (1 << 0)
    #define HPDirectionRightMask (1 << 1)
    #define HPDirectionFrontMask (1 << 2)
    #define HPDirectionBackMask (1 << 3)

    #import "HPDirectionItem.h"

    @interface HPDirectionItem () {
    //这里bits和struct用任一一个就可以,结构体相当于是对bits的解释。因为是共用体用同一块内存。
    union {
    char bits;
    //位域,这里是匿名结构体(anonymous struct)
    struct {
    char left : 1;
    char right : 1;
    char front : 1;
    char back : 1;
    };
    }_direction;
    }

    @end

    @implementation HPDirectionItem

    - (instancetype)init {
    self = [super init];
    if (self) {
    _direction.bits = 0b00000000;
    }
    return self;
    }

    - (void)setLeft:(BOOL)left {
    if (left) {
    _direction.bits |= HPDirectionLeftMask;
    } else {
    _direction.bits &= ~HPDirectionLeftMask;
    }
    }

    - (BOOL)left {
    return _direction.bits & HPDirectionLeftMask;
    }
    //……
    //其它方向设置同理
    //……
    @end

    • HPDirectionItem是一个方向类,类中有一个_direction的共用体。
    • _direction中有bitsanonymous struct,这里anonymous struct相当于是对bits的一个解释(因为是共用体,同一个字节内存。下面的调试截图很好的证明力这一点)。
    • 通过对bits位移操作来进行数据的存储,其实就相当于对结构体位域的操作。连这个可以互相操作。

    所以可以将settergetter通过结构体去操作,效果和操作bits相同:

    - (void)setLeft:(BOOL)left {

        _direction.left = left;
    }

    - (BOOL)left {
    return _direction.left;
    }

    当然也可以两者混用:

    - (void)setLeft:(BOOL)left {
    _direction.left = left;
    }

    - (BOOL)left {
    return _direction.bits & HPDirectionLeftMask;
    }

    根本上还是对同一块内存空间进行操作。

    调用:

    void testUnionBits() {
    HPDirectionItem *item = [HPDirectionItem alloc];
    item.left = 1;
    item.right = 1;
    item.front = 1;
    item.back = 1;
    item.right = 0;
    item.back = 0;
    NSLog(@"testUnionBits");
    }



    这样整个赋值流程就符合预期满足需求了。

    • 联合体位域作用:优化内存空间和访问速度。

    三、 isa

    alloc分析的文章中已经了解到执行完initIsa后将alloc开辟的内存与类进行了关联。在initIsa中首先创建了isa_t也就是isa,去掉方法后它的主要结构如下:

    union isa_t {
    //……
    uintptr_t bits;
    private:
    Class cls;
    public:
    #if defined(ISA_BITFIELD)
    struct {
    ISA_BITFIELD; // defined in isa.h
    };
    //……
    #endif
    //……
    };

    它是一个union,包含了bitscls(私有)和一个匿名结构体,所以这3个其实是一个内容,不同表现形式罢了。这个结构似曾相识,与2.2.2中联合体位域一样。不同的是isa_t占用8字节64位。

    没有关联类时isa分布(默认都是0,没有指向):





    bitscls分析起来比较困难,既然三者一样,那么isa_t的核心就是ISA_BITFIELD


    作者:HotPotCat
    链接:https://www.jianshu.com/p/84749f140139

    收起阅读 »

    objc_msgSend cache查找

    分析objc_msgSend中缓存的查找逻辑以及汇编代码是如何进入c/c++代码的。一、CacheLookup 查找缓存1.1 CacheLookup源码分析传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncache...
    继续阅读 »

    分析objc_msgSend中缓存的查找逻辑以及汇编代码是如何进入c/c++代码的。

    一、CacheLookup 查找缓存

    1.1 CacheLookup源码分析

    传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncached


    //NORMAL, _objc_msgSend, __objc_msgSend_uncached

    .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    // requirements:
    // //缓存不存在返回NULL,x0设置为0
    // GETIMP:
    // The cache-miss is just returning NULL (setting x0 to 0)
    // 参数说明
    // NORMAL and LOOKUP:
    // - x0 contains the receiver
    // - x1 contains the selector
    // - x16 contains the isa
    // - other registers are set as per calling conventions
    //
    //调用过来的p16存储的是cls,将cls存储在x15.
    mov x15, x16 // stash the original isa
    //_objc_msgSend
    LLookupStart\Function:
    // p1 = SEL, p16 = isa
    //arm64 64 OSX/SIMULATOR
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //isa->cache,首地址也就是_bucketsAndMaybeMask
    ldr p10, [x16, #CACHE] // p10 = mask|buckets
    //lsr逻辑右移 p11 = _bucketsAndMaybeMask >> 48 也就是 mask
    lsr p11, p10, #48 // p11 = mask
    //p10 = _bucketsAndMaybeMask & 0xffffffffffff = buckets(保留后48位)
    and p10, p10, #0xffffffffffff // p10 = buckets
    //x12 = cmd & mask w1为第二个参数cmd(self,cmd...),w11也就是p11 也就是执行cache_hash。这里没有>>7位的操作
    and w12, w1, w11 // x12 = _cmd & mask
    //arm64 64 真机这里p11计算后是_bucketsAndMaybeMask
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
    //arm64 + iOS + !模拟器 + 非mac应用
    #if CONFIG_USE_PREOPT_CACHES
    //iphone 12以后指针验证
    #if __has_feature(ptrauth_calls)
    //tbnz 测试位不为0则跳转。与tbz对应。 p11 第0位不为0则跳转 LLookupPreopt\Function。
    tbnz p11, #0, LLookupPreopt\Function
    //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff // p10 = buckets
    #else
    //p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
    and p10, p11, #0x0000fffffffffffe // p10 = buckets
    //p11 第0位不为0则跳转 LLookupPreopt\Function。
    tbnz p11, #0, LLookupPreopt\Function
    #endif
    //eor 逻辑异或(^) 格式为:EOR{S}{cond} Rd, Rn, Operand2
    //p12 = selector ^ (selector >> 7) select 右移7位&自己给到p12
    eor p12, p1, p1, LSR #7
    //p12 = p12 & (_bucketsAndMaybeMask >> 48) = index & mask值 = buckets中的下标
    and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else
    //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff // p10 = buckets
    //p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下标
    and p12, p1, p11, LSR #48 // x12 = _cmd & mask
    #endif // CONFIG_USE_PREOPT_CACHES
    //arm64 32
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //后4位为mask前置0的个数的case
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
    and p10, p11, #~0xf // p10 = buckets 相当于后4位置为0,取前32位
    and p11, p11, #0xf // p11 = maskShift 取的是后4位,为mask前置位的0的个数
    mov p12, #0xffff
    lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
    and p12, p1, p11 // x12 = _cmd & mask
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    //通过上面的计算 p10 = buckets,p11 = mask(arm64真机是_bucketsAndMaybeMask), p12 = index
    // p13(bucket_t) = buckets + 下标 << 4 PTRSHIFT arm64 为3. <<4 位为16字节 buckets + 下标 *16 = buckets + index *16 也就是直接平移到了第几个元素的地址。
    add p13, p10, p12, LSL #(1+PTRSHIFT)
    // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    //这里就直接遍历查找了,因为arm64下cache_next相当于遍历(这里只扫描了前面)
    // do {
    //p17 = imp, p9 = sel
    1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
    //sel - _cmd != 0 则跳转 3:,也就意味着没有找到就跳转到__objc_msgSend_uncached
    cmp p9, p1 // if (sel != _cmd) {
    b.ne 3f // scan more
    // } else {
    //找到则调用或者返回imp,Mode为 NORMAL
    2: CacheHit \Mode // hit: call or return imp 命中
    // }
    //__objc_msgSend_uncached
    //缓存中找不到方法就走__objc_msgSend_uncached逻辑了。
    //cbz 为0跳转 sel == nil 跳转 \MissLabelDynamic
    3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; 有空位没有找到说明没有缓存
    //bucket_t - buckets 由于是递减操作
    cmp p13, p10 // } while (bucket >= buckets) //⚠️ 这里一直是往前找,后面的元素在后面还有一次循环。
    //无符号大于等于 则跳转1:f b 分别代表front与back
    b.hs 1b

    //没有命中cache 查找 p13 = mask对应的元素,也就是倒数第二个
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //p13 = buckets + (mask << 4) 平移找到对应mask的bucket_t。UXTW 将w11扩展为64位后左移4
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
    // p13 = buckets + (mask << 1+PTRSHIFT)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //p13 = buckets + (mask >> 44) 这里右移44位,少移动4位就不用再左移了。因为maskZeroBits的存在 就找到了mask对应元素的地址
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
    // p13 = buckets + (mask << 1+PTRSHIFT)
    // see comment about maskZeroBits
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //p13 = buckets + (mask << 4) 找到对应mask的bucket_t。
    add p13, p10, p11, LSL #(1+PTRSHIFT)
    // p13 = buckets + (mask << 1+PTRSHIFT)
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    //p12 = buckets + (p12<<4) index对应的bucket_t
    add p12, p10, p12, LSL #(1+PTRSHIFT)
    // p12 = first probed bucket

    //之前已经往前查找过了,这里从后往index查找
    // do {
    //p17 = imp p9 = sel
    4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
    //sel - _cmd
    cmp p9, p1 // if (sel == _cmd)
    //sel == _cmd跳转CacheHit
    b.eq 2b // goto hit
    //sel != nil
    cmp p9, #0 // } while (sel != 0 &&
    //
    ccmp p13, p12, #0, ne // bucket > first_probed)
    //有值跳转4:
    b.hi 4b

    LLookupEnd\Function:
    LLookupRecover\Function:
    //仍然没有找到缓存,缓存彻底不存在 __objc_msgSend_uncached()
    b \MissLabelDynamic

    核心逻辑:

    • 根据不同架构找到bucketssel对应的indexp10 = buckets,p11 = mask / _bucketsAndMaybeMask(arm64_64 是 _bucketsAndMaybeMask),p12 = index
      • arm64_64的情况下如果_bucketsAndMaybeMask0位为1则执行LLookupPreopt\Function
    • p13 = buckets + index << 4找到cls对应的buckets地址,地址平移找到对应bucket_t
    • do-while循环扫描buckets[index]的前半部分(后半部分逻辑不在这里)。
      • 如果存在sel为空,则说明是没有缓存的,就直接__objc_msgSend_uncached()
      • 命中直接CacheHit \Mode,这里ModeNORMAL
    • 平移获得p13 = buckets[mask]对应的元素,也就是倒数第二个(倒数第一个为buckets地址)。
    • p13 = buckets + mask << 4找到mask对应的buckets地址,地址平移找到对应bucket_t
    • do-while循环扫描buckets[mask]的前面元素,直到index(不包含index)。
      • 命中CacheHit \Mode
      • 如果存在sel为空,则说明是没有缓存的,就直接结束循环。
    • 最终仍然没有找到则执行__objc_msgSend_uncached()
    1. CACHEcache_t相对isa的偏移。 #define CACHE (2 * __SIZEOF_POINTER__)
    2. maskZeroBits始终是40p13 = buckets + (_bucketsAndMaybeMask >> 44)右移44位后就不用再<<4找到对应bucket_t的地址了。这也是maskZeroBitsarm64_64下存在的意义。
    3. f b 分别代表frontback,往下往上的意思。

    1.2 CacheLookup 伪代码实现


    //NORMAL, _objc_msgSend, __objc_msgSend_uncached

    void CacheLookup(Mode,Function,MissLabelDynamic,MissLabelConstant) {
    //1. 根据架构不同集算sel在buckets中的index
    if (arm64_64 && OSX/SIMULATOR) {
    p10 = isa->cache //_bucketsAndMaybeMask
    p11 = _bucketsAndMaybeMask >> 48//mask
    p10 = _bucketsAndMaybeMask & 0xffffffffffff//buckets
    x12 = sel & mask //index 也就是执行cache_hash
    } else if (arm64_64) {//真机 //这个分支下没有计算mask
    p11 = isa->cache //_bucketsAndMaybeMask
    if (arm64 + iOS + !模拟器 + 非mac应用) {
    if (开启指针验证 ) {
    if (_bucketsAndMaybeMask 第0位 != 0) {
    goto LLookupPreopt\Function
    } else
    {
    p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff//buckets
    }
    } else {
    p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe //buckets
    if (_bucketsAndMaybeMask 第0位 != 0) {
    goto LLookupPreopt\Function
    }
    }
    //计算index
    p12 = selector ^ (selector >> 7)
    p12 = p12 & (_bucketsAndMaybeMask & 48) = p12 & mask//index
    } else
    {
    p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff //buckets
    p12 = selector & (_bucketsAndMaybeMask >>48) //index
    }
    } else if (arm64_32) {
    p11 = _bucketsAndMaybeMask
    p10 = _bucketsAndMaybeMask &(~0xf//buckets 相当于后4位置为0,取前32位
    p11 = _bucketsAndMaybeMask & 0xf //mask前置位0的个数
    p11 = 0xffff >> p11 //获取到mask的值
    x12 = selector & mask //index
    } else {
    #error Unsupported cache mask storage for ARM64.
    }

    //通过上面的计算 p10 = buckets,p11 = mask/_bucketsAndMaybeMask, p12 = index
    p13 = buckets + index << 4 //找到cls对应的buckets地址。地址平移找到对应bucket_t。

    //2.找缓存(这里只扫描了前面)
    do {
    p13 = *bucket-- //赋值后指向前一个bucket
    p17 = bucket.imp
    p9 = bucket.sel
    if (p9 != selector) {
    if (p9 == 0) {//说明没有缓存
    __objc_msgSend_uncached()
    }
    } else {//缓存命中,走命中逻辑 call or return imp
    CacheHit \Mode
    }
    } while(bucket >= buckets) //buckets是首地址,bucket是index对应的buckct往前移动

    //查找完后还没有缓存?
    //查找 p13 = mask对应的元素,也就是倒数第二个
    if (arm64_64 && OSX/SIMULATOR) {
    p13 = buckets + (mask << 4)
    } else if (arm64_64) {//真机
    p13 = buckets + (_bucketsAndMaybeMask >> 44)//这里右移44位,少移动4位就不用再左移了。这里就找到了对应index的bucket_t。
    } else if (arm64_32) {
    p13 = buckets + (mask << 4)
    } else {
    #error Unsupported cache mask storage for ARM64.
    }

    //index的bucket_t 从mask对应的buckets开始再往前找
    p12 = buckets + (index<<4)
    do {
    p17 = imp;
    p9 = sel;
    *p13--;
    if (p9 == selector) {//命中
    CacheHit \Mode
    }
    } while (p9 != nil && bucket > p12)//从后往前 p9位nil则证明没有存,也就不存在缓存了。

    //仍然没有找到缓存,缓存彻底不存在。
    __objc_msgSend_uncached()
    }

    二、LLookupPreopt\Function

    arm64_64真机的情况下,如果_bucketsAndMaybeMask的第0位为1则会执行LLookupPreopt\Function的逻辑。简单看了下汇编发现与cache_t 中的_originalPreoptCache有关。

    2.1 LLookupPreopt\Function 源码分析

    LLookupPreopt\Function:

    #if __has_feature(ptrauth_calls)
    //p10 = _bucketsAndMaybeMask & 0x007ffffffffffffe = buckets
    and p10, p11, #0x007ffffffffffffe // p10 = x
    //buckets x16为cls 验证
    autdb x10, x16 // auth as early as possible
    #endif

    // x12 = (_cmd - first_shared_cache_sel)
    //(_cmd >> 12 + PAGE) << 12 + PAGEOFF 第一个sel
    adrp x9, _MagicSelRef@PAGE
    ldr p9, [x9, _MagicSelRef@PAGEOFF]
    //差值index
    sub p12, p1, p9

    // w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
    #if __has_feature(ptrauth_calls)
    // bits 63..60 of x11 are the number of bits in hash_mask
    // bits 59..55 of x11 is hash_shift

    // 取到 hash_shift...
    lsr x17, x11, #55 // w17 = (hash_shift, ...)
    //w9 = index >> hash_shift
    lsr w9, w12, w17 // >>= shift
    //x17 = _bucketsAndMaybeMask >>60 //mask_bits
    lsr x17, x11, #60 // w17 = mask_bits
    mov x11, #0x7fff
    //x11 = 0x7fff >> mask_bits //mask
    lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
    //x9 = x9 & mask
    and x9, x9, x11 // &= mask
    #else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
    lsr w9, w12, w17 // >>= shift
    and x9, x9, x11, LSR #53 // &= mask
    #endif
    //x17 = el_offs | (imp_offs << 32)
    ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
    // cmp x12 x17 是否找到sel
    cmp x12, w17, uxtw

    .if \Mode == GETIMP
    b.ne \MissLabelConstant // cache miss
    //imp = isa - (sel_offs >> 32)
    sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
    //注册imp
    SignAsImp x0
    ret
    .else
    b.ne 5f // cache miss
    //imp(x17) = (isa - sel_offs>> 32)
    sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
    .if \Mode == NORMAL
    //跳转imp
    br x17
    .elseif \Mode == LOOKUP
    //x16 = isa | 3 //这里为或的意思
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    //注册imp
    SignAsImp x17
    ret
    .else
    .abort unhandled mode \Mode
    .endif
    //x9 = buckets-1
    5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
    //计算回调isa x16 = x16 + x9
    add x16, x16, x9 // compute the fallback isa
    //使用新isa重新查找缓存
    b LLookupStart\Function // lookup again with a new isa
    .endif
    • 找到imp就跳转/返回。
    • 没有找到返回下一个isa重新CacheLookup

    ⚠️@TODO 真机调试的时候进不到这块流程,这块分析的还不是很透彻,后面再补充。

    三、CacheHit

    在查找缓存命中后会执行CacheHit

    3.1 CacheHit源码分析

    #define NORMAL 0

    #define GETIMP 1
    #define LOOKUP 2

    // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
    .macro CacheHit
    //这里传入的为NORMAL
    .if $0 == NORMAL
    //调用imp TailCallCachedImp(imp,buckets,sel,isa)
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
    .elseif $0 == GETIMP
    //返回imp
    mov p0, p17
    //imp == nil跳转9:
    cbz p0, 9f // don't ptrauth a nil imp
    //有imp执行AuthAndResignAsIMP(imp,buckets,sel,isa)最后给到x0返回。
    AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
    9: ret // return IMP
    .elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    //找imp(imp,buckets,sel,isa)
    AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
    //isa与x15比较
    cmp x16, x15
    //cinc如果相等 就将x16+1,否则就设成0.
    cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
    ret // return imp via x17
    .else
    .abort oops
    .endif
    .endmacro
    • 这里其实走的是NORMAL逻辑,NORMALcase直接验证并且跳转imp
    • TailCallCachedImp内部执行的是imp^cls,对imp进行了解码。
    • GETIMP返回imp
    • LOOKUP查找注册imp并返回。

    3.1 CacheHit伪代码实现

    //x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa

    void CacheHit(Mode) {
    if (Mode == NORMAL) {
    //imp = imp^cls 解码
    TailCallCachedImp x17, x10, x1, x16 // 解码跳转imp
    } else if (Mode == GETIMP) {
    p0 = IMP
    if (p0 == nil) {
    return
    } else {
    AuthAndResignAsIMP(imp,buckets,sel,isa)//resign cached imp as IMP
    }
    } else if (Mode == LOOKUP) {
    AuthAndResignAsIMP(x17, buckets, sel, isa)//resign cached imp as IMP
    if (isa == x15) {
    x16 += 1
    } else {
    x16 = 0
    }
    } else {
    .abort oops//报错
    }
    }

    四、__objc_msgSend_uncached

    在缓存没有命中的情况下会走到__objc_msgSend_uncached()的逻辑:

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    //查找imp
    MethodTableLookup
    //跳转imp
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
    • MethodTableLookup查找imp
    • TailCallFunctionPointer跳转imp

    MethodTableLookup

    .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
    • 调用_lookUpImpOrForward查找imp。这里就调用到了c/c++的代码了:
    • IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

    最终会调用_lookUpImpOrForward进入c/c++环境逻辑。

    对于架构的一些理解
    LP64 //64位
    x86_64 // interl 64位
    i386 // intel 32位
    arm // arm指令 32 位
    arm64 //arm64指令
    arm64 && LP64 //arm64 64位
    arm64 && !LP64 //arm64 32 位

    五、 objc_msgSend流程图

    objc_msgSend流程图

    总结

    • 判断receiver是否存在。
    • 通过isa获取cls
    • cls内存平移0x10获取cache也就是_bucketsAndMaybeMask
    • 通过buckets & bucketsMask获取buckets`地址。
    • 通过bucketsMask >> maskShift获取mask
    • 通过sel & mask获取第一次查找的index
    • buckets + index << 4找到index对应的地址。
    • do-while循环判断找缓存,这次从[index~0]查找imp
    • 取到buckets[mask]继续do-while循环,从[mask~index)查找imp。两次查找过程中如果有sel为空则会结束查找。走__objc_msgSend_uncached的逻辑。
    • 找到imp就解码跳转imp


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

    收起阅读 »

    Core Image 和视频

    在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。总览当...
    继续阅读 »

    在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。

    总览

    当涉及到处理视频的时候,性能就会变得非常重要。而且了解黑箱下的原理 —— 也就是 Core Image 是如何工作的 —— 也很重要,这样我们才能达到足够的性能。在 GPU 上面做尽可能多的工作,并且最大限度的减少 GPU 和 CPU 之间的数据传送是非常重要的。之后的例子中,我们将看看这个细节。

    优化资源的 OpenGL ES

    CPU 和 GPU 都可以运行 Core Image,在这个例子中,我们要使用 GPU,我们做如下几样事情。

    我们首先创建一个自定义的 UIView,它允许我们把 Core Image 的结果直接渲染成 OpenGL。我们可以新建一个 GLKView 并且用一个 EAGLContext 来初始化它。我们需要指定 OpenGL ES 2 作为渲染 API,在这两个例子中,我们要自己触发 drawing 事件 (而不是在 -drawRect: 中触发),所以在初始化 GLKView 的时候,我们将 enableSetNeedsDisplay 设置为 false。之后我们有可用新图像的时候,我们需要主动去调用 -display

    在这个视图里,我们保持一个对 CIContext 的引用,它提供一个桥梁来连接我们的 Core Image 对象和 OpenGL 上下文。我们创建一次就可以一直使用它。这个上下文允许 Core Image 在后台做优化,比如缓存和重用纹理之类的资源等。重要的是这个上下文我们一直在重复使用。

    上下文中有一个方法,-drawImage:inRect:fromRect:,作用是绘制出来一个 CIImage。如果你想画出来一个完整的图像,最容易的方法是使用图像的 extent。但是请注意,这可能是无限大的,所以一定要事先裁剪或者提供有限大小的矩形。一个警告:因为我们处理的是 Core Image,绘制的目标以像素为单位,而不是点。由于大部分新的 iOS 设备配备 Retina 屏幕,我们在绘制的时候需要考虑这一点。如果我们想填充整个视图,最简单的办法是获取视图边界,并且按照屏幕的 scale 来缩放图片 (Retina 屏幕的 scale 是 2)。


    从相机获取像素数据

    对于 AVFoundation 如何工作的概述,我们想从镜头获得 raw 格式的数据。我们可以通过创建一个 AVCaptureDeviceInput 对象来选定一个摄像头。使用 AVCaptureSession,我们可以把它连接到一个 AVCaptureVideoDataOutput。这个 data output 对象有一个遵守 AVCaptureVideoDataOutputSampleBufferDelegate 协议的代理对象。这个代理每一帧将接收到一个消息:

    func captureOutput(captureOutput: AVCaptureOutput!,
    didOutputSampleBuffer: CMSampleBuffer!,
    fromConnection: AVCaptureConnection!) {

    我们将用它来驱动我们的图像渲染。在我们的示例代码中,我们已经将配置,初始化以及代理对象都打包到了一个叫做 CaptureBufferSource 的简单接口中去。我们可以使用前置或者后置摄像头以及一个回调来初始化它。对于每个样本缓存区,这个回调都会被调用,并且参数是缓冲区和对应摄像头的 transform:

    source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
    (buffer, transform) in
    ...
    }

    我们需要对相机返回的数据进行变换。无论你如何转动 iPhone,相机的像素数据的方向总是相同的。在我们的例子中,我们将 UI 锁定在竖直方向,我们希望屏幕上显示的图像符合照相机拍摄时的方向,为此我们需要后置摄像头拍摄出的图片旋转 -π/2。前置摄像头需要旋转 -π/2 并且加一个镜像效果。我们可以用一个 CGAffineTransform 来表达这种变换。请注意如果 UI 是不同的方向 (比如横屏),我们的变换也将是不同的。还要注意,这种变换的代价其实是非常小的,因为它是在 Core Image 渲染管线中完成的。

    接着,要把 CMSampleBuffer 转换成 CIImage,我们首先需要将它转换成一个 CVPixelBuffer。我们可以写一个方便的初始化方法来为我们做这件事:

    extension CIImage {
    convenience init(buffer: CMSampleBuffer) {
    self.init(CVPixelBuffer: CMSampleBufferGetImageBuffer(buffer))
    }
    }

    现在我们可以用三个步骤来处理我们的图像。首先,把我们的 CMSampleBuffer 转换成 CIImage,并且应用一个形变,使图像旋转到正确的方向。接下来,我们用一个 CIFilter 滤镜来得到一个新的 CIImage 输出。我们使用了 Florian 的文章 提到的创建滤镜的方式。在这个例子中,我们使用色调调整滤镜,并且传入一个依赖于时间而变化的调整角度。最终,我们使用之前定义的 View,通过 CIContext 来渲染 CIImage。这个流程非常简单,看起来是这样的:

    source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
    [unowned self] (buffer, transform) in
    let input = CIImage(buffer: buffer).imageByApplyingTransform(transform)
    let filter = hueAdjust(self.angleForCurrentTime)
    self.coreImageView?.image = filter(input)
    }

    当你运行它时,你可能会因为如此低的 CPU 使用率感到吃惊。这其中的奥秘是 GPU 做了几乎所有的工作。尽管我们创建了一个 CIImage,应用了一个滤镜,并输出一个 CIImage,最终输出的结果是一个 promise:直到实际渲染才会去进行计算。一个 CIImage 对象可以是黑箱里的很多东西,它可以是 GPU 算出来的像素数据,也可以是如何创建像素数据的一个说明 (比如使用一个滤镜生成器),或者它也可以是直接从 OpenGL 纹理中创建出来的图像。

    下面是演示视频

    从影片中获取像素数据

    我们可以做的另一件事是通过 Core Image 把这个滤镜加到一个视频中。和实时拍摄不同,我们现在从影片的每一帧中生成像素缓冲区,在这里我们将采用略有不同的方法。对于相机,它会推送每一帧给我们,但是对于已有的影片,我们使用拉取的方式:通过 display link,我们可以向 AVFoundation 请求在某个特定时间的一帧。

    display link 对象负责在每帧需要绘制的时候给我们发送消息,这个消息是按照显示器的刷新频率同步进行发送的。这通常用来做 自定义动画,但也可以用来播放和操作视频。我们要做的第一件事就是创建一个 AVPlayer 和一个视频输出:

    player = AVPlayer(URL: url)
    videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBufferDict)
    player.currentItem.addOutput(videoOutput)

    接下来,我们要创建 display link。方法很简单,只要创建一个 CADisplayLink 对象,并将其添加到 run loop。

    let displayLink = CADisplayLink(target: self, selector: "displayLinkDidRefresh:")
    displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)

    现在,唯一剩下的就是在 displayLinkDidRefresh: 调用的时候获取视频每一帧。首先,我们获取当前的时间,并且将它转换成当前播放项目里的时间比。然后我们询问 videoOutput,如果当前时间有一个可用的新的像素缓存区,我们把它复制一下并且调用回调方法:

    func displayLinkDidRefresh(link: CADisplayLink) {
    let itemTime = videoOutput.itemTimeForHostTime(CACurrentMediaTime())
    if videoOutput.hasNewPixelBufferForItemTime(itemTime) {
    let pixelBuffer = videoOutput.copyPixelBufferForItemTime(itemTime, itemTimeForDisplay: nil)
    consumer(pixelBuffer)
    }
    }

    我们从一个视频输出获得的像素缓冲是一个 CVPixelBuffer,我们可以把它直接转换成 CIImage。正如上面的例子,我们会加上一个滤镜。在这个例子里,我们将组合多个滤镜:我们使用一个万花筒的效果,然后用渐变遮罩把原始图像和过滤图像相结合,这个操作是非常轻量级的。

    创意地使用滤镜

    大家都知道流行的照片效果。虽然我们可以将这些应用到视频,但 Core Image 还可以做得更多。

    Core Image 里所谓的滤镜有不同的类别。其中一些是传统的类型,输入一张图片并且输出一张新的图片。但有些需要两个 (或者更多) 的输入图像并且混合生成一张新的图像。另外甚至有完全不输入图片,而是基于参数的生成图像的滤镜。

    通过混合这些不同的类型,我们可以创建意想不到的效果。

    混合图片

    在这个例子中,我们使用这些东西:

    Combining filters

    上面的例子可以将图像的一个圆形区域像素化。

    它也可以创建交互,我们可以使用触摸事件来改变所产生的圆的位置。

    Core Image Filter Reference 按类别列出了所有可用的滤镜。请注意,有一部分只能用在 OS X。

    生成器和渐变滤镜可以不需要输入就能生成图像。它们很少自己单独使用,但是作为蒙版的时候会非常强大,就像我们例子中的 CIBlendWithMask 那样。

    混合操作和 CIBlendWithAlphaMask 还有 CIBlendWithMask 允许将两个图像合并成一个。

    CPU vs. GPU

    iOS 和 OS X 的图形栈。需要注意的是 CPU 和 GPU 的概念,以及两者之间数据的移动方式。

    在处理实时视频的时候,我们面临着性能的挑战。

    首先,我们需要能在每一帧的时间内处理完所有的图像数据。我们的样本中采用 24 帧每秒的视频,这意味着我们有 41 毫秒 (1/24 秒) 的时间来解码,处理以及渲染每一帧中的百万像素。

    其次,我们需要能够从 CPU 或者 GPU 上面得到这些数据。我们从视频文件读取的字节数最终会到达 CPU 里。但是这个数据还需要移动到 GPU 上,以便在显示器上可见。

    避免转移

    一个非常致命的问题是,在渲染管线中,代码可能会把图像数据在 CPU 和 GPU 之间来回移动好几次。确保像素数据仅在一个方向移动是很重要的,应该保证数据只从 CPU 移动到 GPU,如果能让数据完全只在 GPU 上那就更好。

    如果我们想渲染 24 fps 的视频,我们有 41 毫秒;如果我们渲染 60 fps 的视频,我们只有 16 毫秒,如果我们不小心从 GPU 下载了一个像素缓冲到 CPU 里,然后再上传回 GPU,对于一张全屏的 iPhone 6 图像来说,我们在每个方向将要移动 3.8 MB 的数据,这将使帧率无法达标。

    当我们使用 CVPixelBuffer 时,我们希望这样的流程:

    Flow of image data

    CVPixelBuffer 是基于 CPU 的 (见下文),我们用 CIImage 来包装它。构建滤镜链不会移动任何数据;它只是建立了一个流程。一旦我们绘制图像,我们使用了基于 EAGL 上下文的 Core Image 上下文,而这个 EAGL 上下文也是 GLKView 进行图像显示所使用的上下文。EAGL 上下文是基于 GPU 的。请注意,我们是如何只穿越 GPU-CPU 边界一次的,这是至关重要的部分。

    工作和目标

    Core Image 的图形上下文可以通过两种方式创建:使用 EAGLContext 的 GPU 上下文,或者是基于 CPU 的上下文。

    这个定义了 Core Image 工作的地方,也就是像素数据将被处理的地方。与工作区域无关,基于 GPU 和基于 CPU 的图形上下文都可以通过执行 createCGImage(…)render(_, toBitmap, …) 和 render(_, toCVPixelBuffer, …),以及相关的命令来向 CPU 进行渲染。

    重要的是要理解如何在 CPU 和 GPU 之间移动像素数据,或者是让数据保持在 CPU 或者 GPU 里。将数据移过这个边界是需要很大的代价的。

    缓冲区和图像

    在我们的例子中,我们使用了几个不同的缓冲区图像。这可能有点混乱。这样做的原因很简单,不同的框架对于这些“图像”有不同的用途。下面有一个快速总览,以显示哪些是以基于 CPU 或者基于 GPU 的:

    描述
    CIImage它们可以代表两种东西:图像数据或者生成图像数据的流程。
    CIFilter 的输出非常轻量。它只是如何被创建的描述,并不包含任何实际的像素数据。
    如果输出时图像数据的话,它可能是纯像素的 NSData,一个 CGImage, 一个 CVPixelBuffer,或者是一个 OpenGL 纹理
    CVImageBuffer这是 CVPixelBuffer (CPU) 和 CVOpenGLESTexture (GPU) 的抽象父类.
    CVPixelBufferCore Video 像素缓冲 (Pixel Buffer) 是基于 CPU 的。
    CMSampleBufferCore Media 采样缓冲 (Sample Buffer) 是 CMBlockBuffer 或者 CVImageBuffer 的包装,也包括了元数据。
    CMBlockBufferCore Media 区块缓冲 (Block Buffer) 是基于 GPU 的

    需要注意的是 CIImage 有很多方便的方法,例如,从 JPEG 数据加载图像或者直接加载一个 UIImage 对象。在后台,这些将会使用一个基于 CGImage 的 CIImage 来进行处理。

    结论

    Core Image 是操纵实时视频的一大利器。只要你适当的配置下,性能将会是强劲的 —— 只要确保 CPU 和 GPU 之间没有数据的转移。创意地使用滤镜,你可以实现一些非常炫酷的效果,神马简单色调,褐色滤镜都弱爆啦。所有的这些代码都很容易抽象出来,深入了解下不同的对象的作用区域 (GPU 还是 CPU) 可以帮助你提高代码的性能。


    原文:http://www.objc.io/issue-23/core-image-video.html

    译者:考高这点小事

    高考这件小事


    收起阅读 »

    使用 Swift 进行函数式信号处理

    作为一个和 Core Audio 打过很长时间交道的工程师,苹果发布 Swift 让我感到兴奋又疑惑。兴奋是因为 Swift 是一个为性能打造的现代编程语言,但是我又不是非常确定函数式编程是否可以应用到 “我的世界”。幸运的是,很多人已经探索和克服了这些问题,...
    继续阅读 »

    作为一个和 Core Audio 打过很长时间交道的工程师,苹果发布 Swift 让我感到兴奋又疑惑。兴奋是因为 Swift 是一个为性能打造的现代编程语言,但是我又不是非常确定函数式编程是否可以应用到 “我的世界”。幸运的是,很多人已经探索和克服了这些问题,所以我决定将我从这些项目中学习到的东西应用到 Swift 编程语言中去。


    信号

    信号处理的基本当然是信号。在 Swift 中,我可以这样定义信号:

    public typealias Signal = Int -> SampleType

    你可以把 Signal 类想象成一个离散时间函数,这个函数会返回一个时间点上的信号值。在大多数信号处理的教科书中,这个会被写做 x[t], 这样一来它就很符合我的世界观了。

    现在我们来定义一个给定频率的正弦波:

    public func sineWave(sampleRate: Int, frequency: ParameterType) -> Signal {
    let phi = frequency / ParameterType(sampleRate)
    return { i in
    return SampleType(sin(2.0 * ParameterType(i) * phi * ParameterType(M_PI)))
    }
    }

    sineWave 函数会返回一个 SignalSignal 本身是一个将采样点的索引映射为输出样点的函数。我将这些不需要“输入”的信号称为信号发生器,因为它们不需要任何其他的东西就能创造信号。

    但是我们正在讨论信号处理。那么如何更改一个信号呢?

    任何关于信号处理的高层面的讨论,都不可能离开一个基础,那就是如何控制增益 (或者音量):

    public func scale(s: Signal, amplitude: ParameterType) -> Signal {
    return { i in
    return SampleType(s(i) * SampleType(amplitude))
    }
    }

    scale 函数接受一个名为 s 的 Signal 作为输入,然后返回一个施加了标量之后的新 Signal。每次调用这个经过 scale 后的信号,返回的值都是对应的 s(i) 然后通过所提供的 amplitude 进行加成,来作为输出。很容易对吧?但是很快这些构件就会变得混乱起来。来看看以下的例子:

    public func mix(s1: Signal, s2: Signal) -> Signal {
    return { i in
    return s1(i) + s2(i)
    }
    }

    这让我们能够将两个信号混合成一个信号。我们甚至可以混合任意多个信号:

    public func mix(signals: [Signal]) -> Signal {
    return { i in
    return signals.reduce(SampleType(0)) { $0 + $1(i) }
    }
    }

    这可以让我们干很多事情;但是一个 Signal 仅仅限于一个单一的音频频道,有些音效需要复杂的操作的组合同时发生才能做到。

    处理 Block

    我们如何才能以更灵活的方式在信号和处理器之间建立联系,来让信号处理更接近于我们所想呢?有很多流行的环境,比如说 Max 和 PureData,这些环境会建立信号处理的 “blocks”,并以此来创造强大的音效和演奏工具。

    Faust 是一个为此设计出来的函数式编程语言,它是一个用来编写高度复杂 (而且高性能) 的信号处理代码的强大工具。Faust 定义了一系列运算符来让你建立 blocks (处理器),这和信号流图像很相似。

    类似地,我用同样的方式建立了一个可以高效工作的环境。

    使用我们之前定义的 Signal,我们可以基于这个概念进行扩展。

    public protocol BlockType {
    typealias SignalType
    var inputCount: Int { get }
    var outputCount: Int { get }
    var process: [SignalType] -> [SignalType] { get }

    init(inputCount: Int, outputCount: Int, process: [SignalType] -> [SignalType])
    }

    一个 Block 有多个输入,多个输出,和一个 process 函数,这个函数将信号从输入集合转换成输出集合。Blocks 可以有零个或多个输入,也可以有零个或多个输出。

    你可以用以下的方法来建立串行的 blocks。

    public func serial<B: BlockType>(lhs: B, rhs: B) -> B {
    return B(inputCount: lhs.inputCount, outputCount: rhs.outputCount, process: { inputs in
    return rhs.process(lhs.process(inputs))
    })
    }

    这个函数将 lhs block 的输出当做 rhs block 的输入,然后返回结果。就好像在两个 blocks 中间连起一根线一样。当你想要并行地执行多个 blocks 的时候,事情就变得有意思起来:

    public func parallel<B: BlockType>(lhs: B, rhs: B) -> B {
    let totalInputs = lhs.inputCount + rhs.inputCount
    let totalOutputs = lhs.outputCount + rhs.outputCount

    return B(inputCount: totalInputs, outputCount: totalOutputs, process: { inputs in
    var outputs: [B.SignalType] = []

    outputs += lhs.process(Array(inputs[0..<lhs.inputCount]))
    outputs += rhs.process(Array(inputs[lhs.inputCount..<lhs.inputCount+rhs.inputCount]))

    return outputs
    })
    }

    一组并行运行的 blocks 将输入和输出结合在一起,并创建了一个更大的 block。比如一对产生的正弦波的 Block 组合在一起可以创建一个 DTMF 音调,或者两个单频延迟的 Block 可以组成一个立体延迟 Block等。这个概念在实践中是非常强大的。

    那么混合器呢?我们如何从多个输入得到一个单频道的结果?我们可以用如下函数来将多个 block 合并在一起:

    public func merge<B: BlockType where B.SignalType == Signal>(lhs: B, rhs: B) -> B {
    return B(inputCount: lhs.inputCount, outputCount: rhs.outputCount, process: { inputs in
    let leftOutputs = lhs.process(inputs)
    var rightInputs: [B.SignalType] = []

    let k = lhs.outputCount / rhs.inputCount
    for i in 0..<rhs.inputCount {
    var inputsToSum: [B.SignalType] = []
    for j in 0..<k {
    inputsToSum.append(leftOutputs[i+(rhs.inputCount*j)])
    }
    let summed = inputsToSum.reduce(NullSignal) { mix($0, $1) }
    rightInputs.append(summed)
    }

    return rhs.process(rightInputs)
    })
    }

    从 Faust 借用一个惯例,输入的混合是这样进行的:右手边 block 的输入来自于左手边对输入取模后的输出。举个例子,将六个频道的三个立体声轨变成一个立体输出的 block:输出频道 0,2,4 被混合 (比如相加) 进输入频道 0,然后输出频道 1,3,5 会被混合进输入频道 1。

    同样的,你可以用相反的方法将 block 的输出分开。

    public func split<B: BlockType>(lhs: B, rhs: B) -> B {
    return B(inputCount: lhs.inputCount, outputCount: rhs.outputCount, process: { inputs in
    let leftOutputs = lhs.process(inputs)
    var rightInputs: [B.SignalType] = []

    // 从 lhs 将频道逐个复制输入中
    let k = lhs.outputCount
    for i in 0..<rhs.inputCount {
    rightInputs.append(leftOutputs[i%k])
    }

    return rhs.process(rightInputs)
    })
    }

    对于输出我们也使用一个类似的惯例,一个立体声 block 作为三个立体声 block 的输入 (总共接受六个声道),也就是说,频道 0 作为输入 0,2,4,而频道 1 作为 1,3,5 的输入。

    我们当然不想被这些很长的函数束缚住手脚,所以我写了这些运算符:

    // 并行
    public func |-<B: BlockType>(lhs: B, rhs: B) -> B

    // 串行
    public func --<B: BlockType>(lhs: B, rhs: B) -> B

    // 分割
    public func -<<B: BlockType>(lhs: B, rhs: B) -> B

    // 合并
    public func >-<B: BlockType where B.SignalType == Signal>(lhs: B, rhs: B) -> B

    (我觉得“并行”运算符的定义并不是特别好,因为它看上去和几何中的“垂直”尤其相似,但是现在就这样,非常欢迎大家的意见)

    现在有了这些运算符,你可以建立一些有趣的 blocks “图”。比如说 DTMF 音调发生器:

    let dtmfFrequencies = [
    ( 941.0, 1336.0 ),

    ( 697.0, 1209.0 ),
    ( 697.0, 1336.0 ),
    ( 697.0, 1477.0 ),

    ( 770.0, 1209.0 ),
    ( 770.0, 1336.0 ),
    ( 770.0, 1477.0 ),

    ( 852.0, 1209.0 ),
    ( 852.0, 1336.0 ),
    ( 852.0, 1477.0 ),
    ]

    func dtmfTone(digit: Int, sampleRate: Int) -> Block {
    assert( digit < dtmfFrequencies.count )
    let (f1, f2) = dtmfFrequencies[digit]

    let f1Block = Block(inputCount: 0, outputCount: 1, process: { _ in [sineWave(sampleRate, f1)] })
    let f2Block = Block(inputCount: 0, outputCount: 1, process: { _ in [sineWave(sampleRate, f2)] })

    return ( f1Block |- f2Block ) >- Block(inputCount: 1, outputCount: 1, process: { return $0 })
    }

    dtmfTone 函数处理两个并行的正弦发生器,然后将它们融合成一个 “单位元 block”,这个 block 只是将自己的输入复制到输出。记住这个函数的返回值本身就是一个 block,所以你可以在更大的系统中使用这个block。

    可以看得出来这个想法蕴含了很多的潜力。通过创建可以使用更紧凑和容易理解的 DSL (domain specific language) 来描述复杂系统的环境,我们可以花更少的时间来思考单个 block 的细节,并轻易地把所有东西组合到一起。

    实践

    如果我今天要开始做一个要求最高性能以及丰富功能的新项目,我会毫不犹豫的使用 Faust。如果你对函数式音频编程感兴趣的话,我极力推荐 Faust。

    话虽如此,我一上提到想法的可行性很大程度上依赖于苹果对编译器的改进,编译器需要具有能识别我们定义在 block 中的模式,并输出更智能的代码的能力。也就是说,苹果需要像编译 Haskell 一样来编译 Swift。在 Haskell 中函数式编程模式会被压缩成某一个目标 CPU 的矢量运算。

    说实话,我觉得 Swift 在苹果的管理下是很好的,我们也会在将来看见我在以上呈现的想法会变得很常见,而且性能也会变得非常好。


    原文链接:http://www.objc.io/issue-24/functional-signal-processing.html

    译者:李子轩



    收起阅读 »

    【开源项目】用环信IM实现的一款教学助手

    教学助手开发环境:Tools : Android Studio 4.1.2os : windows 10code : kotlin配置文件:appId 目录 com.kangaroo.studentedu.app.appIdappCe(证书) 目录 com.k...
    继续阅读 »

    教学助手

    开发环境:

    • Tools : Android Studio 4.1.2
    • os : windows 10
    • code : kotlin

    配置文件:

    • appId 目录 com.kangaroo.studentedu.app.appId
    • appCe(证书) 目录 com.kangaroo.studentedu.app.appCe

    运行环境:

    • os : Android 5.0 +

    项目包含内容:

    • Android project
    • 安装包

    第三方:

    • 声网灵动课堂 sdk
    • 声网直播 sdk
    • 环信IM sdk

    项目背景

    疫情期间在线教育火了,各种直播教育软件都开始推广。教育软件开始了火热。就拿我公司来说,我公司是数据化教育行业的一员。在疫情期间帮助学校进行了作业发布,作业批改的业务,帮助学校提升了疫情期间的教学质量。 就拿普通中小学来举例: 互动白板功能非常重要,老师上课会用互动白板功能,老师录课也会用互动白板功能。除了上课和录课,老师还可以通过设备来下发电子考题,考试题目。考试完毕后还可以统计到本堂课的上课质量。

    • 拿一些特殊课堂举例: 艺术课,体育课,音乐课等,这些课堂有时可能不需要白板这样的功能,互动直播功能又变得比较适合。
    • 拿一些校外辅导举例: 校外辅导的校长需要了解当前学校老师教学情况的数据,了解学生上课的数据,了解学校招生的数据。通过这些数据来提升学校的应受。

    运行说明

    本项目登录功能全部采用环信sdk提供的登录功能呢,支持单设备登录,互踢功能。 由于本项目没有后台,很多功能和数据都是在本地做的处理

    安装包:安装包

    一个app提供两种身份登录(学生,老师),两种权限(学生,老师)


    老师分为2种类型

    1. 普通老师(带有白板功能)
    2. 体育老师等艺术类老师(带有直播功能)

    学生端


    主功能界面


    学生主要有4种功能

    签到

    学生地理位置的签到,老师会收到学生签到的通知,那么进一步老师会在考勤上记录学生的情况

    课堂点评

    学生会对老师当堂课程进行点评,打分,可以发送图片内容。点评的数据,在数据统计里展示,学校管理员,或校长,会直接看到,那么校长会知道教学质量

    写作业

    老师端会给当天课程进行布置作业,布置一些图片作业,或者文字作业。学生要写作业

    数据统计

    学生的数据主要针对各科的数据进行统计,直观的看自己的平均发展。查漏补缺,提升自己薄弱的方面



    课程表

    课程表会展示 管理员在后台给老师和学生排的课程,下方课程便是今天学生该上的课程(直播课,或白板课)。

    通讯录

    教学互动,老师和学生可以直接交流。对今天不会的课程进行答疑

    消息列表

    消息列表

    我的

    退出登录,等基本展示

    老师端




    主功能界面

    老师主要有6种功能

    学员考勤

    勾选今日到校的学生进行考勤管理。

    课堂点评

    老师可以查看学生对自己的评价,提升自我教学质量

    布置作业

    老师端会给当天课程进行布置作业,布置一些图片作业,或者文字作业。学生要写作业

    批改作业

    老师会对发上的作业,进行及时批改。

    我的班级

    可以查看当前班级,查看学生数据

    数据统计

    老师关心的班级男女比例,出勤率,课堂评价,作业提交率等,根据数据来对自己的教学质量更改

    课程表

    课程表会展示 管理员在后台给老师和学生排的课程,下方课程便是今天老师要教的(直播课,或白板课)。

    通讯录

    教学互动,老师和学生可以直接交流。对今天不会的课程进行答疑

    消息列表

    消息列表

    我的

    退出登录,等基本展示


    github地址:https://github.com/smartbackme/RTE-2021-Innovation-Challenge/tree/master/Application-Challenge/%5B%E5%8F%B2%E5%A4%A7%E4%BC%9F%5D%20%E6%95%99%E5%AD%A6%E5%8A%A9%E6%89%8B

    安装包下载地址:com.kangaroo.studentedu-release-v1.0.0-20210528152329L.apk


    欢迎添加环信冬冬微信,联系该项目作者

    收起阅读 »

    Kotlin 源码 | 降低代码复杂度的法宝

    随着码龄增大,渐渐意识到团队代码中的最大的敌人是“复杂度”。不合理的复杂度是降低代码质量,增加沟通成本的元凶。Kotlin 在降低代码复杂度方面有着诸多法宝。这一篇就以两个常见的业务场景来剖析下简单和复杂的关系。若要用一句话概括这关系,我最喜欢这一句:“一切简...
    继续阅读 »

    随着码龄增大,渐渐意识到团队代码中的最大的敌人是“复杂度”。不合理的复杂度是降低代码质量,增加沟通成本的元凶。

    Kotlin 在降低代码复杂度方面有着诸多法宝。这一篇就以两个常见的业务场景来剖析下简单和复杂的关系。若要用一句话概括这关系,我最喜欢这一句:“一切简单的背后都蕴藏着复杂”。

    启动线程和读取文件容是 Android 开发中两个颇为常见的场景。分别给出 Java 和 Kotlin 的实现,在惊叹两种语言表达力上悬殊的差距的同时,逐层剖析 Kotlin 语法简单背后的复杂。

    启动线程

    先看一个简单的业务场景,在 java 中用下面的代码启动一个新线程:

     Thread thread = new Thread() {
    @Override
    public void run() {
    doSomething() // 业务逻辑
    super.run();
    }
    };
    thread.setDaemon(false);
    thread.setPriority(-1);
    thread.setName("thread");
    thread.start();

    启动线程是一个常用操作,其中除了 doSomething() 之外的其他代码都具有通用性。难道每次启动线程时都复制粘贴这一坨代码吗?不优雅!得抽象成一个静态方法以便到处调用:

    public class ThreadUtil {
    public static Thread startThread(Callback callback) {
    Thread thread = new Thread() {
    @Override
    public void run() {
    if (callback != null) callback.action();
    super.run();
    }
    };
    thread.setDaemon(false);
    thread.setPriority(-1);
    thread.setName("thread");
    thread.start();
    return thread;
    }

    public interface Callback {
    void action();
    }
    }

    仔细分析下这里引入的复杂度,一个新的类ThreadUtil及静态方法startThread(),还有一个新的接口Callback

    然后就可以像这样构建线程了:

    ThreadUtil.startThread( new Callback() {
    @Override
    public void action() {
    doSomething();
    }
    })

    对比下 Kotlin 的解决方案thread()

    public fun thread(
    start: Boolean = true,
    isDaemon: Boolean = false,
    contextClassLoader: ClassLoader? = null,
    name: String? = null,
    priority: Int = -1,
    block: () -> Unit
    ): Thread {
    val thread = object : Thread() {
    public override fun run() {
    block()
    }
    }
    if (isDaemon)
    thread.isDaemon = true
    if (priority > 0)
    thread.priority = priority
    if (name != null)
    thread.name = name
    if (contextClassLoader != null)
    thread.contextClassLoader = contextClassLoader
    if (start)
    thread.start()
    return thread
    }

    thread()方法把构建线程的细节全都隐藏在方法内部。

    然后就可以像这样启动一个新线程:

    thread { doSomething() }

    这简洁的背后是一系列语法特性的支持:

    1. 顶层函数

    Kotlin 中把定义在类体外,不隶属于任何类的函数称为顶层函数thread()就是这样一个函数。这样定义的好处是,可以在任意位置,方便地访问到该函数。

    Kotlin 的顶层函数被编译成 java 代码后就变成一个类中的静态函数,类名是顶层函数所在文件名+Kt 后缀。

    2. 高阶函数

    若函数的参数或者返回值是 lambda 表达式,则称该函数为高阶函数

    thread()方法的最后一个参数是 lambda 表达式。在 Kotlin 中当调用函数只传入一个 lambda 类型的参数时,可以省去括号。所以就有了thread { doSomething() }这样简洁的调用。

    3. 参数默认值 & 命名参数

    thread()函数包含了 6 个参数,为啥在调用时可以只传最后一个参数?因为其余的参数都在定义时提供了默认值。这个语法特性叫参数默认值

    当然也可以忽略默认值,重新为参数赋值:

    thread(isDaemon = true) { doSomething() }

    当只想重新为某一个参数赋值时,不用将其余参数都重写一遍,只需用参数名 = 参数值,这个语法特性叫命名参数

    逐行读取文件内容

    再看一个稍复杂的业务场景:“读取文件中每一行的内容并打印”,用 Java 实现的代码如下:

    File file = new File(path)
    BufferedReader bufferedReader = null;
    try {
    bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
    String line;
    // 循环读取文件中的每一行并打印
    while ((line = bufferedReader.readLine()) != null) {
    System.out.println(line);
    }
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    // 关闭资源
    if (bufferedReader != null) {
    try {
    bufferedReader.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    对比一下 Kotlin 的解决方案:

    File(path).readLines().foreach { println(it) }

    一句话搞定,就算没学过 Kotlin 也能猜到这是在干啥,语义是如此简洁清晰。这样的代码写的时候畅快,读的时候悦目。

    之所以简单,是因为 Kotlin 通过各种语法特性将复杂度分层并隐藏在了后背。

    1. 扩展方法

    拨开简单的面纱,探究背后隐藏的复杂:

    // 为 File 扩展方法 readLines()
    public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
    // 构建字符串列表
    val result = ArrayList<String>()
    // 遍历文件的每一行并将内容添加到列表中
    forEachLine(charset) { result.add(it) }
    // 返回列表
    return result
    }

    扩展方法是 Kotlin 在类体外给类新增方法的语法,它用类名.方法名()表达。

    把 Kotlin 编译成 java,扩展方法就是新增了一个静态方法:

    final class FilesKt__FileReadWriteKt {
    // 静态函数的第一个参数是 File
    public static final List readLines(@NotNull File $this$readLines, @NotNull Charset charset) {
    Intrinsics.checkNotNullParameter($this$readLines, "$this$readLines");
    Intrinsics.checkNotNullParameter(charset, "charset");
    final ArrayList result = new ArrayList();
    FilesKt.forEachLine($this$readLines, charset, (Function1)(new Function1() {
    public Object invoke(Object var1) {
    this.invoke((String)var1);
    return Unit.INSTANCE;
    }

    public final void invoke(@NotNull String it) {
    Intrinsics.checkNotNullParameter(it, "it");
    result.add(it);
    }
    }));
    return (List)result;
    }
    }

    静态方法中的第一个参数是被扩展对象的实例,所以在扩展方法中可以通过this访问到类实例及其公共方法。

    File.readLines() 的语义简单明了:遍历文件的每一行,将其添加到列表中并返回。

    复杂度都被隐藏在了forEachLine(),它也是 File 的扩展方法,此处应该是this.forEachLine(charset) { result.add(it) },this 通常可以省略。forEachLine()是个好名字,一眼看去就知道是在遍历文件的每一行。

    public fun File.forEachLine(charset: Charset = Charsets.UTF_8, action: (line: String) -> Unit): Unit {
    BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(action)
    }

    forEachLine()中将 File 层层包裹最终形成一个 BufferReader 实例,并且调用了 Reader 的扩展方法forEachLine()

    public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
    useLines { it.forEach(action) }

    forEachLine()调用了同是 Reader 的扩展方法useLines(),从名字细微的差别就可以看出uselines()完成了文件所有行内容的整合,而且这个整合的结果是可以被遍历的。

    2. 泛型

    哪个类能整合一组元素,并可以被遍历?沿着调用链继续往下:

    public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T =
    buffered().use { block(it.lineSequence()) }

    Reader 在useLines()中被缓冲化:

    public inline fun Reader.buffered(bufferSize: Int = DEFAULT_BUFFER_SIZE): BufferedReader =
    // 如果已经是 BufferedReader 则直接返回,否则再包一层
    if (this is BufferedReader) this else BufferedReader(this, bufferSize)

    紧接着调用了use(),使用 BufferReader:

    // Closeable 的扩展方法
    public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var exception: Throwable? = null
    try {
    // 触发业务逻辑(扩展对象实例被传入)
    return block(this)
    } catch (e: Throwable) {
    exception = e
    throw e
    } finally {
    // 无论如何都会关闭资源
    when {
    apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
    this == null -> {}
    exception == null -> close()
    else ->
    try {
    close()
    } catch (closeException: Throwable) {}
    }
    }
    }

    这次的扩展函数不是一个具体类,而是一个泛型,并且该泛型的上界是Closeable,即为所有可以被关闭的类新增一个use()方法。

    use()扩展方法中,lambda 表达式block代表了业务逻辑,扩展对象作为实参传入其中。业务逻辑在try-catch代码块中被执行,最后在finally中关闭了资源。上层可以特别省心地使用这个扩展方法,因为不再需要在意异常捕获和资源关闭。

    3. 重载运算符 & 约定

    读取文件内容的场景中,use() 中的业务逻辑是将BufferReader转换成LineSequence,然后遍历它。这里的遍历和类型转换分别是怎么实现的?

    // 将 BufferReader 转化成 Sequence
    public fun BufferedReader.lineSequence(): Sequence<String> =
    LinesSequence(this).constrainOnce()

    还是通过扩展方法,直接构造了LineSequence对象并将BufferedReader传入。这种通过组合方式实现的类型转换和装饰者模式颇为类似(关于装饰者模式的详解可以点击使用组合的设计模式 | 美颜相机中的装饰者模式

    LineSequence 是一个 Sequence:

    // 序列
    public interface Sequence<out T> {
    // 定义如何构建迭代器
    public operator fun iterator(): Iterator<T>
    }

    // 迭代器
    public interface Iterator<out T> {
    // 获取下一个元素
    public operator fun next(): T
    // 判断是否有后续元素
    public operator fun hasNext(): Boolean
    }

    Sequence是一个接口,该接口需要定义如何构建一个迭代器iterator。迭代器也是一个接口,它需要定义如何获取下一个元素及是否有后续元素。

    2 个接口中的 3 个方法都被保留词operator修饰,它表示重载运算符,即重新定义运算符的语义。Kotlin 中预定义了一些函数名和运算符的对应关系,称为约定。当前这个约定就是iterator() + next() + hasNext()for循环的约定。

    for 循环在 Kotlin 中被定义为“遍历迭代器提供的元素”,需要和in保留词一起使用:

    public inline fun <T> Sequence<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
    }

    Sequence 有一个扩展法方法forEach()来简化遍历语法,内部就使用了“for + in”来遍历序列中所有的元素。

    所以才可以在Reader.forEachLine()中用如此简单的语法实现遍历文件中的所有行。

    public fun Reader.forEachLine(action: (String) -> Unit): Unit = 
    useLines { it.forEach(action) }

    关于 Sequence 的用法实例可以点击Kotlin 基础 | 望文生义的 Kotlin 集合操作

    LineSequence 的语义是 Sequence 中每一个元素都是文件中的一行,它在内部实现iterator()接口,构造了一个迭代器实例:

    // 行序列:在 BufferedReader 外面包一层 LinesSequence
    private class LinesSequence(private val reader: BufferedReader) : Sequence<String> {
    override public fun iterator(): Iterator<String> {
    // 构建迭代器
    return object : Iterator<String> {
    private var nextValue: String? = null // 下一个元素值
    private var done = false // 迭代是否结束

    // 判断迭代器中是否有下一个元素,并顺便获取下一个元素存入 nextValue
    override public fun hasNext(): Boolean {
    if (nextValue == null && !done) {
    // 下一个元素是文件中的一行内容
    nextValue = reader.readLine()
    if (nextValue == null) done = true
    }
    return nextValue != null
    }

    // 获取迭代器中下一个元素
    override public fun next(): String {
    if (!hasNext()) {
    throw NoSuchElementException()
    }
    val answer = nextValue
    nextValue = null
    return answer!!
    }
    }
    }
    }

    LineSequence 内部的迭代器在hasNext()中获取了文件中一行的内容,并存储在nextValue中,完成了将文件中每一行的内容转换成 Sequence 中的一个元素。

    当在 Sequence 上遍历时,文件中每一行的内容就一个个出现在迭代中。这样做的好处是对内存更加友好,LineSequence 并没有持有文件中所有行的内容,它只是定义了如何获取文件中下一行的内容,所有的内容只有等待遍历时,才一个个地浮现出来。

    用一句话总结 Kotlin 逐行读取文件内容的算法:用缓冲流(BufferReader)包裹文件,再用行序列(LineSequence)包裹缓冲流,序列迭代行为被定义为读取文件中一行的内容。遍历序列时,文件内容就一行行地被添加到列表中。

    总结

    顶层函数、高阶函数、默认参数、命名参数、扩展方法、泛型、重载运算符,Kotlin 利用了这些语法特性隐藏了实现常用业务功能的复杂度,并且在内部将复杂度分层。

    分层是降低复杂度的惯用手段,它不仅让复杂度分散,使得同一时刻只需面对有限的复杂度,并且可以通过对每一层取一个好名字来概括本层的语义。除此之外,它还有助于定位问题(缩小问题范围)并增加代码可复用性(每层单独复用)。

    是不是也可以效仿这种分层的思想方法,在写代码之前,琢磨一下,复杂度是不是太高了?可以运用那些语言特性实现合理的抽象将复杂度分层?以避免复杂度在一个层次被铺开。

    收起阅读 »

    Kotlin 协程 | CoroutineContext 为什么要设计成 indexed set?(一)

    CoroutineContext是 Kotlin 协程中的核心概念,它是用来干嘛的?它由哪些元素组成?它为什么要这样设计?这篇试着分析源码以回答这些问题。 indexed set 既是 set 又是 map? CoroutineContext的定义如下: /*...
    继续阅读 »

    CoroutineContext是 Kotlin 协程中的核心概念,它是用来干嘛的?它由哪些元素组成?它为什么要这样设计?这篇试着分析源码以回答这些问题。


    indexed set 既是 set 又是 map?


    CoroutineContext的定义如下:


    /**
    * Persistent context for the coroutine. It is an indexed set of [Element] instances.
    * An indexed set is a mix between a set and a map.
    * Every element in this set has a unique [Key].
    */
    public interface CoroutineContext { ... }

    暂且把CoroutineContext译成协程上下文,简称上下文。


    从注解来看,上下文是一个Element的集合,这种集合被称为indexed set。它是介于 set 和 map 之间的一种结构。set 意味着其中的元素有唯一性,map 意味着每个元素都对应一个键。


    public interface CoroutineContext {
    // Element 也是一个上下文
    public interface Element : CoroutineContext { ... }
    }

    没想到Element也是一个上下文,所以协程上下文是包含了一系列上下文的集合(自己包含自己)。暂且称在协程上下文内部的一系列上下文为子上下文


    上下文如何保证子上下文各自的唯一性?


    public interface CoroutineContext {
    public interface Key<E : Element>
    }

    上下文为每个子上下文分配了一个Key,它是一个带有类型信息的接口。这个接口通常被实现为companion object


    // 子上下文:Job
    public interface Job : CoroutineContext.Element {
    // Job 的静态 Key
    public companion object Key : CoroutineContext.Key<Job> { ... }
    }

    // 子上下文:拦截器
    public interface ContinuationInterceptor : CoroutineContext.Element {
    // 拦截器的静态 Key
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
    }

    // 子上下文:协程名
    public data class CoroutineName( val name: String ) : AbstractCoroutineContextElement(CoroutineName) {
    // 协程名的静态 Key
    public companion object Key : CoroutineContext.Key<CoroutineName>
    }

    // 子上下文:异常处理器
    public interface CoroutineExceptionHandler : CoroutineContext.Element {
    // 异常处理器的静态 Key
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
    }

    列举了若干源码中定义的子上下文,它们有一个共性,都会在内部声明一个静态的Key,类内部的静态变量意味着被所有类实例共享,即全局唯一的 Key 实例可以对应多个子上下文实例。然而在一个类似 map 的结构中,每个键必须是唯一的,因为对相同的键 put 两次值,新值会代替旧值。如此一来,键的唯一性这就保证了上下文中的所有子上下文实例都是唯一的。这就是indexed set集合的内涵。


    做个阶段性总结:





    1. 协程上下文是一个元素的集合,单个元素本身也是一个上下文,所以协程上下文的定义是递归的,自包含的(自己包含若干个自己)。




    2. 协程上下文这个集合有点像 set 结构,因为其中的元素都是唯一的,不重复的。为了做到这一点,每一个元素都配有一个静态的键实例,构成一组键值对,这使得它又有点像 map 结构。这种介于 set 和 map 之间的结构称为indexed set





    从 indexed set 获取元素


    集合必然提供了存取其中元素的方法,CoroutineContextElement元素的集合,取元素的方法定义如下:


    public interface CoroutineContext {
    // 根据 key 在上下文中查找元素
    public operator fun <E : Element> get(key: Key<E>): E?
    }

    get()方法输入 Key 返回 Element。CoroutineContext 的子类Element有一个get()的实现:


    public interface CoroutineContext {
    // 元素
    public interface Element : CoroutineContext {
    // 元素的键
    public val key: Key<*>
    public override operator fun <E : Element> get(key: Key<E>): E? =
    // 如果给定键和元素本身键相同,则返回当前元素,否则返回空
    if (this.key == key) this as E else null
    }
    }

    协程上下文是元素的集合,而元素也是一个上下文,所以元素也是一个元素的集合(解释递归的定义有点像绕口令)。只不过这个元素集合有一点特别,它只包含一个元素,即它本身。这从Element.get()方法的实现中也可以看出:当从 Element 的元素集合中获取元素时,要么返回自身,要么返回空。


    协程上下文还有一个实现类叫CombinedContext混合上下文,它的get()实现如下:


    // 混合上下文(大蒜)
    internal class CombinedContext(
    // 左上下文
    private val left: CoroutineContext,
    // 右元素
    private val element: Element
    ) : CoroutineContext, Serializable {
    // 根据 key 在上下文中查找元素
    override fun <E : Element> get(key: Key<E>): E? {
    var cur = this
    while (true) {
    // 如果输入 key 和右元素的 key 相同,则返回右元素(剥去大蒜的一片)
    cur.element[key]?.let { return it }
    // 若右元素不匹配,则向左继续查找
    val next = cur.left
    // 如果左上下文是混合上下文,则开始向左递归(剥去一片后还是一个大蒜,继续剥)
    if (next is CombinedContext) {
    cur = next
    }
    // 若左上下文不是混合上下文,则结束递归
    else {
    return next[key]
    }
    }
    }
    }

    CombinedContext.get() 用 while 循环实现了类似递归的效果。CombinedContext的定义本身就是递归的,它包含两个成员:leftelement,其中left是一个协程上下文,若left实例是另一个CombinedContext,就发生了自己包含自己的递归情况,这结构非常像大蒜:left是“蒜体”,element是“蒜皮”。当剥开一片蒜皮后,发现还是一颗大蒜,只是变小了而已。


    CombinedContext.get() 这个算法就好比是“找到一棵大蒜中指定的一片蒜皮”,每剥去一片,都检查一下是不是想要的那一片,若不是就继续剥下一片,就这样递归地进行下去,直到命中了指定片或大蒜被剥空了。


    CombinedContext这颗大蒜还是偏心的,即它的最后一片不在正中心,而是在最左边(当left的类型不再是CombinedContext时),但遍历这颗大蒜是从最右边开始向左进行的,这使得每一片蒜皮拥有不同的优先级,越早被遍历到,优先级越高。


    做一个阶段性总结:



    CombinedContext是协程上下文的一个具体实现,就像协程上下文一样,它也包含了一组元素,这组元素被组织成 “偏心大蒜” 这种自包含的结构。偏心大蒜也是 indexed set 的一种具体实现,即它用唯一键对应唯一值的方式保证了集合中元素的唯一性。但和 set 和 map 这种“平”的结构不同的是,偏心大蒜内元素天然是有层级的,遍历大蒜结构是从外层向内(从右到左)进行的,越先被遍历到的元素自然具有较高的优先级。



    向 indexed set 追加元素


    说完取元素操作,接着说存元素:


    public interface CoroutineContext {
    // 重载操作符
    public operator fun plus(context: CoroutineContext): CoroutineContext =
    // 若追加上下文是空的(等于啥也没追加),则直接返回当前山下文(高性能返回)
    if (context === EmptyCoroutineContext) this else
    // 以当前上下文为初始值进行累加
    context.fold(this) { acc, element -> // 累加算法 }
    }

    CoroutineContext 使用operator保留词重载了plus操作符,即重新定义运算符的语义。Kotlin 中预定义了一些函数名和运算符的对应关系,称为约定。当前这个就是plus()+的约定。当两个 CoroutineContext 实例通过+相连时,就等价于调用了plus()方法,这样做的目的是增加代码可读性。


    plus() 的返回值是CoroutineContext,这使得c1 + c2 + c3这样的链式调用变得方便。


    EmptyCoroutineContext是一个特殊的上下文,它不包含任何元素,这从它的get()方法的实现中可见一斑:


    // 空协程上下文
    public object EmptyCoroutineContext : CoroutineContext, Serializable {
    // 返回空元素
    public override fun <E : Element> get(key: Key<E>): E? = null
    ...
    }

    plus() 中调用的CoroutineContext.fold()是将协程上下文中元素进行累加的接口:


    public interface CoroutineContext {
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    }

    fold() 需要输入一个累加初始值initial和累加算法operation。先来看看 plus() 方法中定义的累加算法:


    public interface CoroutineContext {
    public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) this else
    // 以当前上下文为初始值进行累加
    context.fold(this) { acc, element ->
    // 将追加的元素抽出以便将其重定位
    val removed = acc.minusKey(element.key)
    // 若集合中只包含追加元素,则不需要重定位,直接返回
    if (removed === EmptyCoroutineContext) element else {
    // 获取元素集合中的 Interceptor
    val interceptor = removed[ContinuationInterceptor]
    // 如果元素集合中不包含 Interceptor 则将追加元素作为最外层蒜皮
    if (interceptor == null) CombinedContext(removed, element) else {
    // 如果元素集合中包含 Interceptor 则将其抽出以便将其重定位
    val left = removed.minusKey(ContinuationInterceptor)
    // 元素集合中只包含 Interceptor 和追加元素
    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
    // 将 Interceptor 作为最外层蒜皮,追加元素作为次外层蒜皮
    CombinedContext(CombinedContext(left, element), interceptor)
    }
    }
    }
    }

    累加算法有两个输入参数,一个代表当前累加值acc,另一个代表新追加的元素element。上述算法可以概括为:“当向协程上下文中追加元素时,总是会将所有元素重定位。定位原则如下:将 Interceptor 和新追加的元素依次放在偏心大蒜的最外层和次外层。”


    minusKey()


    其中minusKey()也是协程上下文的一个接口:


    public interface CoroutineContext {
    public fun minusKey(key: Key<*>): CoroutineContext
    }

    minusKey()返回一个协程上下文,该上下文的元素集合中去掉了 key 对应的元素。Element 对该接口的实现如下:


    public interface Element : CoroutineContext {
    public override fun minusKey(key: Key<*>): CoroutineContext =
    if (this.key == key) EmptyCoroutineContext else this
    }

    因为 Element 只包含一个元素,如果要去掉的元素就是它自己,则返回一个空上下文,否则返回自己。


    CombineContext 对 minusKey() 的实现如下:


    internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
    ) : CoroutineContext, Serializable {
    public override fun minusKey(key: Key<*>): CoroutineContext {
    // 1. 如果最外层就是要去掉的元素,则直接返回左上下文
    element[key]?.let { return left }
    // 2. 在左上下文中去掉对应元素
    val newLeft = left.minusKey(key)
    return when {
    // 2.1 左上下文中也不包含对应元素
    newLeft === left -> this
    // 2.2 左上下文中除了对应元素外不包含任何元素,返回右元素
    newLeft === EmptyCoroutineContext -> element
    // 2.3 将移除了对应元素的左上下文和右元素组合成新得混合上下文
    else -> CombinedContext(newLeft, element)
    }
    }

    可以总结为:在偏心大蒜结构中找到对应的蒜皮,并把它剔除,然后将剩下的所有蒜皮按原来的顺序重新组合成偏心大蒜结构。


    Element.fold()


    分析完累加算法之后,看看Elementfold()的实现:


    public interface CoroutineContext {
    public interface Element : CoroutineContext {
    public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    operation(initial, this)
    }

    Element 在这个方法中将自己作为追加值。结合上面的累加算法,可以这样理解 Element 累加:“Element 总是将自己作为被追加的元素,即 Element 总是会出现在偏心大蒜的最外层。”


    举个例子:


    val e1 = Element()
    val e2 = Element()
    val context = e1 + e2

    上述代码中的 context 是一个什么结构?推理如下:



    • e1 + e2 等价于e2.fold(e1)

    • 因为 e2 是 Element 类型,所以调用 Element.fold(),等价于operation(e1, e2)

    • operation 就是上述累加算法,结合累加算法,最终得出 context = CombinedContext(e1, e2)


    再举一个更复杂的例子:


    val e1 = Element()
    val e2 = Element()
    val e3 = Element()
    val c = CombinedContext(e1, e2)
    val context = c + e3

    上述代码中的 context 是一个什么结构?推理如下:



    • c + e3 等价于e3.fold(c)

    • 因为 e3 是 Element 类型,所以调用 Element.fold(),等价于operation(c, e3)

    • operation 就是上述累加算法,结合累加算法,最终得出 context = CombinedContext(c, e2)

    • 将 context 完全展开如下:CombinedContext(CombinedContext(e1, e2), e3)


    做一个阶段性总结:



    两个协程上下文做加法运算意味着将它们的元素合并形成一个新的更大的偏心大蒜。若被加数是 Element 类型的,即被加数中只包含一个元素,则该元素总是被追加到偏心的大蒜的最外层。



    CombinedContext.fold()


    再来看看CombinedContextfold()的实现:


    internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
    ) : CoroutineContext, Serializable {
    public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    operation(left.fold(initial, operation), element)
    }

    这就比 Element 的复杂多了,因为有递归。


    还是举一个例子:


    val e1 = Element()
    val e2 = Element()
    val e3 = Element()
    val c = CombinedContext(e1, e2)
    val context = e3 + c // 和上一个例子几乎是一样的,只是换了下加数与被加数的位置

    上述代码中的 context 是一个什么结构?推理如下:



    • e3 + c 等价于c.fold(e3)

    • 因为 c 是 CombinedContext 类型,所以调用 CombinedContext.fold(),等价于operation(e1.fold(e3), e2)

    • 其中e1.fold(e3)等价于operation(e3, e1),它的值为 CombinedContext(e3, e1)

    • 将第三步结果代入第二步,最终得出 context = CombinedContext(CombinedContext(e3, e1), e2)


    再做一个阶段性总结:



    两个协程上下文做加法运算意味着将它们的元素合并形成一个新的更大的偏心大蒜。若被加数是 CombinedContext 类型的,即被加数包含一个左侧的蒜体和一个右侧的蒜皮,则蒜皮还是在原来的位置待着,蒜体会和加数融合成新的偏心大蒜结构。



    总结


    这一篇介绍了 CoroutineContext 的数据结构,它包含如下特征:



    1. 协程上下文是一个元素的集合,单个元素本身也是一个上下文,所以协程上下文的定义是递归的,自包含的(自己包含若干个自己)。

    2. 协程上下文这个集合有点像 set 结构,因为其中的元素都是唯一的,不重复的。为了做到这一点,每一个元素都配有一个静态的键实例,构成一组键值对,这使得它又有点像 map 结构。这种介于 set 和 map 之间的结构称为indexed set

    3. CombinedContext是协程上下文的一个具体实现,就像协程上下文一样,它也包含了一组元素,这组元素被组织成 “偏心大蒜” 这种自包含的结构。偏心大蒜也是 indexed set 的一种具体实现,即它用唯一键对应唯一值的方式保证了集合中元素的唯一性。但和 set 和 map 这种“平”的结构不同的是,偏心大蒜内元素天然是有层级的,遍历大蒜结构是从外层向内(从右到左)进行的,越先被遍历到的元素自然具有较高的优先级。

    4. 两个协程上下文做加法运算意味着将它们的元素合并形成一个新的更大的偏心大蒜。若被加数是 Element 类型的,即被加数中只包含一个元素,则该元素总是被追加到偏心的大蒜的最外层。若被加数是 CombinedContext 类型的,即被加数包含一个左侧的蒜体和一个右侧的蒜皮,则蒜皮还是在原来的位置待着,蒜体会和加数融合成新的偏心大蒜结构。

    作者:唐子玄
    链接:https://juejin.cn/post/6978613779252641799
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    iOS开发笔记(十一)— UITableView、ARC、xcconfig、Push

    前言分享iOS开发中遇到的问题,和相关的一些思考,本次内容包括:UITableView滚动问题、ARC、xcconfig、Push证书。正文UITableViewUITableView在reloadData 的时候,如果height的高度发生较大变化,cont...
    继续阅读 »

    前言

    分享iOS开发中遇到的问题,和相关的一些思考,本次内容包括:UITableView滚动问题、ARC、xcconfig、Push证书。

    正文

    UITableView

    UITableView在reloadData 的时候,如果height的高度发生较大变化,contentOffset无法保持原来的大小时,会发生滚动的效果。如果直接reloadData再setContentOffset:设置位置,仍会出现滚动的效果。
    如果需要去除该滚动效果,可以在reloadData之后,调用scrollToRowAtIndexPath并设置animated:NO,最后再用setContentOffset:微调位置。
    同理,如果需要在reloadData后,手动scroll到header时,可用同上的解决方案。

    UITableView还有类似的问题,如果列表项过多时,scrollToRowAtIndexPath有时并不准确,比如有1000行时滚动到第500行,此时可能会出现滚到501或者499行的情况。
    究其原因,是因为UITableView不会调用1~499行所有的heightFor和cellFor方法,所以无法准确计算出来位置。
    从这里去分析,如果需要滚动到准确的位置,可以用estimatedRowHeight的属性,设置和行高一样的高度;在行高各不相同的场景,可以设置estimatedRowHeight为大致的数字,在scrollToRowAtIndexPath之后通过setContentOffset:微调位置。

    // 解决部分 UITableView不滚动的问题,实现的效果是某个cell在点击后就扩展高度
    - (void)onMoreContentClick:(SSBookDetailContentCell *)cell {
    ++self.showNums;
    CGPoint offset = self.contentTableView.contentOffset;
    [self.contentTableView beginUpdates];
    [self.contentTableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
    [self.contentTableView endUpdates];
    [self.contentTableView.layer removeAllAnimations];
    [self.contentTableView setContentOffset:offset animated:NO];
    }

    ARC

    Automatic Reference Counting(ARC)是编译器特性,由编译器插入对象内存管理代码,实现内存管理。
    如果仅仅是retain/release的管理,非常容易理解,但是插入的代码如何实现weak、strong这些运行时特性?
    最近同事遇到一个问题,以下代码会crash:
    他实现了一个editingButton的getter,同时在dealloc的时候将其移除;
    如果editingButton在整个生命周期都没有初始化时,则在dealloc使用getter会触发初始化,然后在下面的weakify(self);这一行crash。

    - (void)dealloc
    {
    [self.editingView removeFromSuperview];
    [self.editingButton removeFromSuperview]; // crash
    }

    - (UIButton *)editingButton
    {
    if (!_editingButton)
    {
    _editingButton = [UIButton buttonWithType:UIButtonTypeCustom];
    ......
    weakify(self); // CRASH
    [_editingButton ss_addEventHandler:^(id _Nonnull sender) {
    ......
    } forControlEvents:UIControlEventTouchDown];
    }
    return _editingButton;
    }

    闪退的堆栈如下


    在ARC的文档中找到闪退的方法,其中有一段描述如下:


    当dealloc开始的时候,weakSelf的指针应该都已经被重置为nil;如果在dealloc的函数中再次初始化weakSelf指针会出现异常。

    另外,在dealloc方法执行属性的getter方法也是不合理,因为属性的getter方法大都包括如果未创建就创建并初始化的逻辑。
    ARC的文档 这份文档也是非常好的ARC学习资料。

    xcconfig

    xcconfig是用来保存build setting键值对的文件,里面是一些纯文本;

    //:configuration = Debug
    PRODUCT_BUNDLE_IDENTIFIER = com.loyinglin.dev
    DISPLAY_NAME = 测试标题
    PRODUCT_NAME = Learning
    GCC_TREAT_WARNINGS_AS_ERRORS = YES

    //:configuration = Debug
    GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 SSDEBUG=1

    比如这里配置是一份debug的xcconfig,其中PRODUCT_BUNDLE_IDENTIFIER = com.loyinglin.dev的键值会在编译的时候生效。
    xcconfig有什么用?
    一个Xcode工程,一定会有Debug的开发环境和Release的发布环境,可能会有Testflight的灰度环境、DailyBuild的持续集成环境、XXLanguage的多语言环境、TestCoverage的覆盖率测试环境、IAP的内购测试环境等;每个环境所用的证书不同,APP安装后显示的名字不同,provision file也不同等等。
    一种方案是使用Target来解决,公用的部分设置在project,每个环境根据各自特点自定义某些设置;这样带来的后果是target数量增多明显,而target增多带来的后果是当需要新增extension的时候会工作量巨大,并且多环境的管理难度加剧。
    另外一种方案是使用Configuration来区分环境,而xcconfig就是用来管理Configuration的文件。

    如何创建和使用xcconfig?

    1、在Xcode中新建文件,输入config,选择configuration settings file;这一步是创建xcconfig的文件。


    2、在Xcode中选中工程,在configurations中选择需要配置的选项,这里以debug为例,点击后选择刚刚已经创建的xcconfig,则可以把xcconfig和debug的编译选项绑定在一起。


    如果你用了cocoaPod,你会发现这一项已经有了CocoaPod创建xcconfig,如果选择了自己新建的xcconfig,则会编译失败;
    此时可以在自己新建的xcconfig头文件中加入以下代码:

    #include "Pods/Target Support Files/Pods-YourName/Pods-YourName.debug.xcconfig"

    注意需要修改成自己的工程名。

    3、在build setting选中某个配置项,cmd+c复制然后到xcconfig的文件中,cmd+v就可以复制配置项到xcconfig中。
    注意如果这个配置项在build setting已经有自定义值,需要将其删除,原因下面解释。


    xcconifg的配置和工程默认配置、手动在build setting配置有什么区别?

    配置的结果优先级不同,我的理解是:
    a、project默认配置是最低优先级,因为是最基础的配置;
    b、target配置基于project,但target默认会添加一些配置,优先级比上面高;
    c、xcconfig的配置是target某个config的配置,优先级比上面高;
    d、target的build setting中直接添加的配置项,优先级比上面高;


    知道上面的关系后,我们可以解决使用xcconifg时,CI 打包xcconifg配置项不生效的问题:
    检查是否对应配置项是否在target的build setting中直接添加;

    如果需要新增某个configuration,可以直接duplicate已有的configuration,但是如果使用Pods需要重新pod install,以生成对应的pod工程的配置项,否则会出现下图的报错:


    Push 证书

    .p12是连接苹果APNs服务器的证书(公钥+私钥);
    .cer 是苹果的证书文件(公钥);
    .pem是OpenSSL的证书文件(公钥+私钥);
    当我们生成push证书时,其实就是将我们本地的p12通过脚本,导出对应的pem文件;
    下面是一段常用的脚本:

    P12_CERT=AppStorePush.p12 # p12证书文件
    PASSWD=loying # p12密码

    EXPORT_CERT=AppStorePush.pem # 导出pem证书
    EXPORT_KEY=AppStorePushWithKey.pem # 导出的pem私钥,有密码
    EXPORT_KEY_UNENCRY=AppStorePushWithoutKey.pem # 导出的pem私钥,无密码
    EXPORT_KEY_AND_CERT=AppStore_ck.pem # 含有证书和私钥的pem

    openssl pkcs12 -clcerts -nokeys -out ${EXPORT_CERT} -in ${P12_CERT} -passin pass:${PASSWD} # 导出证书

    openssl pkcs12 -nocerts -passout pass:${PASSWD} -out ${EXPORT_KEY} -in ${P12_CERT} -passin pass:${PASSWD} #导出私钥,有密码

    openssl rsa -in ${EXPORT_KEY} -passin pass:${PASSWD} -out ${EXPORT_KEY_UNENCRY} # 导出私钥,无密码

    cat ${EXPORT_CERT} ${EXPORT_KEY_UNENCRY} > ${EXPORT_KEY_AND_CERT} # 证书和私钥合起来

    openssl s_client -connect gateway.push.apple.com:2195 -cert ${EXPORT_CERT} -key ${EXPORT_KEY_UNENCRY} # 测试 push证书

    # gateway.push.apple.com
    # gateway.sandbox.push.apple.com

    在调试Push的时候,以下这个软件(App Store可以下载)非常便捷:


    使用时配置好证书(可以点击connect验证是否连接APNs成功),再从iPhone获取到deviceToken添加到设备列表,便可以使用推送。

    总结

    这些都是在项目中遇到的一些问题,UITableView这个是老生常谈,ARC那篇文档是很好的学习资料,xcconfig需要多研究,未来随着版本和渠道增多会越来越复杂,Push在Easy APNs Provider这个软件出来后就很好测试,再也不用登录信鸽去手动配置Push。
    新的一年,继续搬砖和学习。

    链接:https://www.jianshu.com/p/0093cb8c5a35

    收起阅读 »

    iOS股票K线图、分时图绘制

    介绍:1、这是以雪球APP为原型,基于 iOS的K线开源项目。2、该项目整体设计思路已经经过某成熟证券APP的商业认证。3、本项目将K线业务代码尽可能缩减,保留核心功能,可流畅、高效实现手势交互。4、K线难点在于手势交互和数据动态刷新上,功能并不复杂,关键在于...
    继续阅读 »

    介绍:

    1、这是以雪球APP为原型,基于 iOS的K线开源项目。
    2、该项目整体设计思路已经经过某成熟证券APP的商业认证。
    3、本项目将K线业务代码尽可能缩减,保留核心功能,可流畅、高效实现手势交互。
    4、K线难点在于手势交互和数据动态刷新上,功能并不复杂,关键在于设计思路。

    演示:


    建议:

    如果搭建K线为公司业务,不建议采用集成度高的开源代码。庞大臃肿,纵然短期匆忙上线,难以应付后期灵活需求变更。
    Objective-C版请移步 https://github.com/cclion/CCLKLineChartView
    Swift版请移步 https://github.com/cclion/KLineView

    设计思路&&难点:

    K线难点在于手势的处理上,捏合、长按、拖拽都需要展示不同效果。以下是Z君当时做K线时遇到的问题的解决方案;

    1. 捏合手势需要动态改变K线柱的宽度,对应的增加或减少当前界面K线柱的展示数量,并且根据当前展示的数据计算出当前展示数据的极值。
    采用UITableView类实现,将K线柱封装在cell中,在tableview中监听捏合手势,对应改变cell的高度,同时刷新cell中K线柱的布局来实现动态改变K线柱的宽度。

    采用UITableView还有一个好处就是可以采用cell的重用机制降低内存。

    注意:因为UITableView默认是上下滑动,而K线柱是左右滑动,Z君这里将UITableView做了一个顺时针90°的旋转。


    2. K线柱绘制

    K线柱采用CAShapeLayer配合UIBezierPath绘制,内存低,效率高,棒棒哒!

    关于CAShapeLayer的使用大家可以看这篇 https://zsisme.gitbooks.io/ios-/content/chapter6/cashapelayer.html
    (现在的google、baidu,好文章都搜不到,一搜全是简单调用两个方法就发的博客,还是翻了两年前的收藏才找到这个网站,强烈推荐大家)

    3. 捏合时保证捏合中心点不变,两边以捏合中间点为中心进行收缩或扩散

    因为UITableView在改变cell的高度时,默认时不会改变偏移量,所以不能保证捏合的中心点不变,这里我们的小学知识就会用上了。


    我们可以通过变量定义控件间距离。


    保证捏合中心点的中K线柱的中心点还在捏合前,就需要c1 = c2 ,计算出O2,在捏合完,设置偏移量为O2即可。


    4. K线其他线性指标如何绘制

    在K线中除了K线柱之外,还有其他均线指标,连贯整个数据显示区。


    由图可以看出均线指标由每个cell中心点的数据连接相邻的cell中心点的数据。我们依旧将绘制放在cell中,将相连两个cell的线分割成两段,分别在各自所属的cell中绘制。

    需要我们做的就是就是在cell中传入相邻的cell的soureData,计算出相邻中点的位置,分为两段绘制。


    大家针对K线有什么问题都可以在下面留言,会第一时间解答。
    未完待续

    转自:https://www.jianshu.com/p/104857287bc4

    收起阅读 »

    Babel配置傻傻看不懂?

    1.2 AST 是什么玩意?👨‍🎓 啊斌同学: 上面说到的抽象语法树AST又是什么玩意?答:我们上文提到,Babel在解析是时候会通过将code转换为AST抽象语法树,本质上是代码语法结构的一种抽象表示,通过以树🌲形的结构形式表现出它的语法结构,抽象在于它的语...
    继续阅读 »

    前沿:文章起源在于,朋友跟树酱说在解决项目兼容IE11浏览器过程中,遇到“眼花缭乱”的babel配置和插件等,傻傻分不清配置间的区别、以及不了解如何引用babel插件才能让性能更佳,如果你也有这方面的疑虑,这篇文章可能适合你

    1.babel

    babel是个什么玩意? Babel本质上是一个编辑器,也就是个“翻译官”的角色,比如树酱听不懂西班牙语,需要别人帮我翻译成为中文,我才晓得。那么Babel就是帮助浏览器翻译的,让web应用能够运行旧版本的浏览器中,比如IE11浏览器不支持Promise等ES6语法,那这个时候在IE11打开你写的web应用,应用就无法正常运行,这时候就需要Babel来“翻译”成为IE11能读懂的

    1.1 Babel是怎么工作的?

    本质上单独靠Babel是无法完成“翻译”,比如官网的例子const babel = code => code;不借助Babel插件的前提,输出是不会把箭头函数“翻译”的,如果想完成就需要用到插件,更多概念点点击 官方文档

    Babel工作原理本质上就是三个步骤:解析、转换、输出,如下👇所示,

    1.2 AST 是什么玩意?

    👨‍🎓 啊斌同学: 上面说到的抽象语法树AST又是什么玩意?

    答:我们上文提到,Babel在解析是时候会通过将code转换为AST抽象语法树,本质上是代码语法结构的一种抽象表示,通过以树🌲形的结构形式表现出它的语法结构,抽象在于它的语言形态不会体现在原始代码code中

    下面介绍下在前端项目开发中一些AST的应用场景:

    • Vue模版解析: 我们平时写的.vue文件通过vue-template-compiler解析,.vue文件处理为一个AST
    • Babel的“翻译” : 如将ES6转换为ES5过程中转为AST
    • webpack的插件UglifyJS: uglifyjs-webpack-plugin用来压缩资源,uglifyjs会遇到需要解析es6语法,这个过程中本质上也是借助babel-loader

    你可以安装通过本地安装babel-cli做个验证,通过babel-cli编译js文件,玩玩“翻译”

    🌲推荐阅读:

    1.3 开发自己的babel插件需要了解什么?

    👨‍🎓 啊可同学: 树酱,我想自己使用AST开发一个babel插件需要使用到哪些东西呢?

    答:我们上一节中提到babel不借助“外援”的话,自己是无法完成翻译,而一个完整的“翻译”的过程是需要走完解析、转换、输出才能完成整个闭环,而这其中的每个环节都需要借助babel以下这些API

    • @babel/parser: babel解析器将源代码code解析成 AST
    • @babel/generator: 将AST解码生成js代码 new Code
    • @babel/traverse : 用来遍历AST树,可以用来改造AST~,如替换或添加AST原始节点
    • @babel/core:包括了整个babel工作流

    下面是一个简单“翻译”的demo~

    👦:啊宽同学:你不是说@babel/parser是也将源代码code解析成 AST吗?为啥@babel/core也是?

    答:@babel/core包含的是整个babel工作流,在开发插件的过程中,如果每个API都单独去引入岂不是蒙蔽了来吧~于是就有了@babel/core插件,顾名思义就是核心插件,他将底层的插件进行封装(包含了parser、generator等),提高原有的插件开发效率,简化过程,好一个“🍟肯德基全家桶”

    🌲推荐阅读:

    1.4 Babel插件相关

    讲完Babel的基本使用,接下来聊聊插件,上文提到单独靠babel是“难成大器”的,需要插件的辅助才能实现霸业,那插件是怎么搞的呢?

    通过第一节的学习我们知道完成第一步骤解析完AST后,接下来是进入转换,插件在这个阶段就起到关键作用了。

    1.4.1 插件的使用

    告诉Babel该做什么之前,我们需要创建一个配置文件.babelrc或者babel.config.js文件

    如果我想把es2015的语法转化为es5 及支持es2020的链式写法,我可以这样写

    上图所示👆,我们可以看到我们配置两个东西 presentplugin

    👨‍🎓 啊可同学:babel不是只需要plugin来帮忙翻译吗,这个present又是什么玩意?

    答:presets是预设,举个例子:有一天树酱要去肯德基买鸡翅、薯条、可乐、汉堡。然后我发现有个套餐A包含了(薯条、可乐、汉堡),那这个present就相当于套餐A,它包含了一些插件集合,一个大套餐,这样我就只需要一个套餐A+鸡翅就搞定了,不用配置很多插件。

    就好比上面的es2015“套餐”,其实就是Babel团队将同属ES2015相关的很多个plugins集合到babel-preset-es2015一个preset中去

    👧 啊琪同学:@babel/preset-env这个是什么?我看很多babel的配置都有

    答:@babel/preset-env这个是一个present预设,换句话说就是“豪华大礼包”,包括一系列插件的集合,包含了我们常用的es2015,es2016, es2017等最新的语法转化插件,允许我们使用最新的js语法,比如 let,const,箭头函数等等,但不包括stage-x阶段的插件。换句话说,他包含了我们上文提到了es2015,是个“全家桶”了,而不仅是个套餐了。

    1.4.2 自定义 present

    👦 啊斌同学:树酱,那我是不是可以自己搞一个预设present?

    答: 可以的,但是你可以以 babel-preset-* 的命名规范来创建一个新项目,然后创建一个packjson并安装好定影的依赖和一个index.js 文件用于导出 .babelrc,最终发布到npm中,如下所示

    1.4.3 关于 polyfill

    比如我们在开发中使用,会使用到一些es6的新特征比如Array.from等,但不是所有的 JavaScript 环境都支持 Array.from,这个时候我们可以使用 Polyfill(代码填充,也可译作兼容性补丁)的“黑科技”,因为babel只转换新的js语法,如箭头函数等,但不转换新的API,比如Symbol、Promise等全局对象,这时候需要借助@babel/polyfill,把es的新特性都装进来,使用步骤如下
    • npm 安装 : npm install --save @babel/polyfill
    • 文件顶部导入 polyfillimport @babel/polyfilll

    🙅‍♂️:缺点:全局引入整个 polyfill包,如promise会被全局引入,污染全局环境,所以不建议使用,那有没有更好的方式?可以直接使用@babel/preset-env并修改配置,因为@babel/preset-env包含了@babel/polyfill插件,看下一节

    1.4.4 如何通过修改@babel/preset-env配置优化

    完成上面的配置,然后用Babel编译代码,我们会发现有时候打出的包体积很大,因为@babel/polyfill有些会被全局引用,那你要弄清楚@babel/preset-env的配置

    @babel/preset-env 中与 @babel/polyfill 的相关参数有两个如下:

    • targets: 支持的目标浏览器的列表
    • useBuiltIns: 参数有 “entry”、”usage”、false 三个值。默认值是false,此参数决定了babel打包时如何处理@babel/polyfilll 语句

    主要聊聊关于useBuiltIns的不同配置如下:

    • entry: 去掉目标浏览器已支持的polyfilll 模块,将浏览器不支持的都引入对应的polyfilll 模块。
    • usage: 打包时会自动根据实际代码的使用情况,结合 targets 引入代码里实际用到部分 polyfilll模块
    • false: 不会自动引入 polyfilll 模块,对polyfilll模块屏蔽

    🌲建议:使用 useBuiltIns: usage来根据目标浏览器的支持情况,按需引入用到的 polyfill 文件,这样打包体积也不会过大

    1.4.5 webpack打包如何使用babel?

    对于@babel/core@babel/preset-env 、@babel/polyfill等这些插件,当我们在使用webpack进行打包的时候,如何让webpack知道按这些规则去编译js。这时就需要babel-loader了,它相当于一个中间桥梁,通过调用babel/core中的API来告知webpack要如何处理。

    1.4.6 开发工具库,涉及到babel使用怎么避免污染环境?

    👦 啊斌同学:我开发了一个工具库,也使用了babel,如果引用polyfill,如何避免使用导致的污染环境?

    答:在开发工具库或者组件库时,就不能再使用babel-polyfill了,否则可能会造成全局污染,可以使用@babel/runtime。它不会污染你的原有的方法。遇到需要转换的方法它会另起一个名字,否则会直接影响使用库的业务代码,使用@babel/runtime主要在于

    • 可以减小库和工具包的体积,规避babel编译的工具函数在每个模块里都重复出现的情况
    • 在没有使用 @babel/runtime 之前,库和工具包一般不会直接引入 polyfill。否则像 Promise 这样的全局对象会污染全局命名空间,这就要求库的使用者自己提供 polyfill。这些 polyfill 一般在库和工具的使用说明中会提到,比如很多库都会有要求提供 es5 的 polyfill。在使用 babel-runtime 后,库和工具只要在 package.json 中增加依赖 babel-runtime,交给 babel-runtime 去引入 polyfill就可以了

    如何使用 @babel/runtime

    • 1.npm安装
    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime
    • 2.配置

    1.5 关于babel容易混淆的点

    1.5.1 babel-core和@babel/core 区别

    👦:啊呆同学:babel-core和@babel/core是什么区别?

    答;@babel是在babel7中版本提出来的,就类似于 vue-cli 升级后使用@vue/cli一样的道理,所以babel7以后的版本都是使用 @babel 开头声明作用域,


    收起阅读 »

    如何用 JS 一次获取 HTML 表单的所有字段 ?

    问:如何用 JS 一次获取 HTML 表单的所有字段 ?考虑一个简单的 HTML 表单,用于将任务保存在待办事项列表中:<form> <label for="name">用户名</label> <input...
    继续阅读 »

    问:如何用 JS 一次获取 HTML 表单的所有字段 ?

    考虑一个简单的 HTML 表单,用于将任务保存在待办事项列表中:

    <form>
    <label for="name">用户名</label>
    <input type="text" id="name" name="name" required>

    <label for="description">简介</label>
    <input type="text" id="description" name="description" required>

    <label for="task">任务</label>
    <textarea id="task" name="task" required></textarea>

    <button type="submit">提交</button>
    </form>


    上面每个字段都有对应的的typeID和 name属性,以及相关联的label。 用户单击“提交”按钮后,我们如何从此表单中获取所有数据?

    有两种方法:一种是用黑科技,另一种是更清洁,也是最常用的方法。为了演示这种方法,我们先创建form.js,并引入文件中。

    从事件 target 获取表单字段

    首先,我们在表单上为Submit事件注册一个事件侦听器,以停止默认行为(它们将数据发送到后端)。

    然后,使用this.elementsevent.target.elements访问表单字段:

    相反,如果需要响应某些用户交互而动态添加更多字段,那么我们需要使用FormData

    使用 FormData

    首先,我们在表单上为submit事件注册一个事件侦听器,以停止默认行为。接着,我们从表单构建一个FormData对象:

    const form = document.forms[0];

    form.addEventListener("submit", function(event) {
    event.preventDefault();
    const formData = new FormData(this);
    });

    除了append()delete()get()set()之外,FormData 还实现了Symbol.iterator。这意味着它可以用for...of 遍历:

    const form = document.forms[0];

    form.addEventListener("submit", function(event) {
    event.preventDefault();
    const formData = new FormData(this);

    for (const formElement of formData) {
    console.log(formElement);
    }
    })


    除了上述方法之外,entries()方法获取表单对象形式:

    const form = document.forms[0];

    form.addEventListener("submit", function(event) {
    event.preventDefault();
    const formData = new FormData(this);
    const entries = formData.entries();
    const data = Object.fromEntries(entries);
    });


    这也适合Object.fromEntries() (ECMAScript 2019)

    为什么这有用?如下所示:

    const form = document.forms[0];

    form.addEventListener("submit", function(event) {
    event.preventDefault();
    const formData = new FormData(this);
    const entries = formData.entries();
    const data = Object.fromEntries(entries);

    // send out to a REST API
    fetch("https://some.endpoint.dev", {
    method: "POST",
    body: JSON.stringify(data),
    headers: {
    "Content-Type": "application/json"
    }
    })
    .then(/**/)
    .catch(/**/);
    });


    一旦有了对象,就可以使用fetch发送有效负载。

    小心:如果在表单字段上省略name属性,那么在FormData对象中刚没有生成。

    总结

    要从HTML表单中获取所有字段,可以使用:

    • this.elementsevent.target.elements,只有在预先知道所有字段并且它们保持稳定的情况下,才能使用。

    使用FormData构建具有所有字段的对象,之后可以转换,更新或将其发送到远程API。*


    原文:https://www.valentinog.com/bl...

    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    收起阅读 »

    自动化注册组件,自动化注册路由--懒人福利(vue,react皆适用)

    我是一个react主义者,这次因为项目组关系必须用vue,作为vue小白就记录一下开发过程中的一些骚想法。正文1. 对于路由的操作可能用过umi的同学知道,umi有一套约定式路由的系统,开发过程中可以避免每写一个页面就去手动import到路由的数组中,你只需要...
    继续阅读 »

    我是一个react主义者,这次因为项目组关系必须用vue,作为vue小白就记录一下开发过程中的一些骚想法。

    正文

    1. 对于路由的操作

    可能用过umi的同学知道,umi有一套约定式路由的系统,开发过程中可以避免每写一个页面就去手动import到路由的数组中,你只需要按照规则,就可以自动化的添加路由。

    完美,我们今天就简单实现一个约定式路由的功能。

    首先把vue自己的路由注释掉

    // const routes: Array = [
    // {
    // path: "/login",
    // name: "login",
    // component: Login,
    // },
    // // {
    // // path: "/about",
    // // name: "About",
    // // // route level code-splitting
    // // // this generates a separate chunk (about.[hash].js) for this route
    // // // which is lazy-loaded when the route is visited.
    // // component: () =>
    // // import(/* webpackChunkName: "about" */ "../views/About.vue"),
    // // },
    // ];


    可以看到代码非常的多,随着页面的增加也会越来越多。当然vue的这种方式也有很多好处:比如支持webpack的魔法注释,支持懒加载

    接下来就去实现我们的约定式路由吧!

    我们这次用到的API是require.context,大家可能以为需要安装什么包,不用不用!这是webpack的东西!具体API的介绍大家可以自行百度了

    首先用这玩意去匹配对应规则的页面,然后提前创好我们的路由数组以便使用。

    const r = require.context("../views", true, /.vue/);
    const routeArr: Array = [];

    接下来就是进行遍历啦,匹配了../views文件下的页面,遍历匹配结果,如果是按照我们的规则创建的页面就去添加到路由数组中

    比如我现在的views文件夹里是这样的

    // 遍历
    r.keys().forEach((key) => {
    console.log(key) //这里的匹配结果就是 ./login/index.vue ./product/index.vue
    const keyArr = key.split(".");
    if (key.indexOf("index") > -1) {
    // 约定式路由构成方案,views文件夹下的index.vue文件都会自动化生成路由
    // 但是我不想在路由中出现index,我只想要login,product,于是对path进行改造。
    // 这部其实是有很多优化空间的。大家可以自己试着用正则去提取
    const pathArr = keyArr[1].split("/");
    routeArr.push({
    name: pathArr[1],
    path: "/" + pathArr[1],
    component: r(key).default, // 这是组件
    });
    }
    });


    一起来看一下自动匹配出来的路由数组是什么模样

    完美🚖达成了我们的需求。去页面看一看!

    完美实现! 最后把全部代码送上。这样就实现了约定式自动注册路由,避免了手动添加的烦恼,懒人必备

    import Vue from "vue";
    import VueRouter, { RouteConfig } from "vue-router";
    const r = require.context("../views", true, /.vue/);
    const routeArr: Array = [];
    r.keys().forEach((key) => {
    const keyArr = key.split(".");
    if (key.indexOf("index") > -1) {
    // 约定式路由构成方案,views文件夹下的index.vue文件都会自动化生成路由
    const pathArr = keyArr[1].split("/");
    routeArr.push({
    name: pathArr[1],
    path: "/" + pathArr[1],
    component: r(key).default, // 这是组件
    });
    }
    });
    Vue.use(VueRouter);

    const router = new VueRouter({
    mode: "history",
    base: process.env.BASE_URL,
    routes: routeArr,
    });

    export default router;


    2.组件

    经过上一章的操作,我们可以写页面了,然后就写到了组件。我发现每次使用组件都要在使用的页面去import,非常的麻烦。

    通过上一章的想法,我们是不是也可以自动化导入组件呢?

    我的想法是:

    • 通过一个方法把components文件下的所有组件进行统一的管理
    • 需要的页面可以用这个方法传入对应的规则,统一返回组件
    • 这个方法可以手动导入,也可以全局挂载。

    先给大家看一下我的components文件夹

    再看一下现在的页面长相

    ok。我们开始在index.ts里撸代码吧

    首先第一步一样的去匹配,这里只需要匹配当前文件夹下的所有vue文件

    const r = require.context("./"true/.vue/);

    然后声明一个方法,这个方法可以做到fn('规则')返回对应的组件,代码如下。

    function getComponent(...names: string[]): any {
    const componentObj: any = {};
    r.keys().forEach((key) => {
    const name = key.replace(/(\.\/|\.vue)/g, "");
    if (names.includes(name)) {
    componentObj[name] = r(key).default;
    }
    });
    return componentObj;
    }
    export { getComponent };

    我们一起来看看调用结果吧

    打印结果:

    看到这个结果不难想象页面的样子吧! 当然跟之前一样啦!当然实现啦!

    非常的完美!

    最后

    由于项目比较急咯,我还有一些骚想法没有时间去整理去查资料实现,暂时先这样吧~

    如果文内有错误,敬请大家帮我指出!(反正我也不一定改哈哈)

    最后!谢谢!拜拜!

    收起阅读 »

    ES6 中 module 备忘清单,你可能知道 module 还可以这样用!

    这是一份备忘单,展示了不同的导出方式和相应的导入方式。 它实际上可分为3种类型:名称,默认值和列表 ?// 命名导入/导出 export const name = 'value'import { name } from '...'// 默认导出/导入expor...
    继续阅读 »

    这是一份备忘单,展示了不同的导出方式和相应的导入方式。 它实际上可分为3种类型:名称,默认值和列表 ?

    // 命名导入/导出 
    export const name = 'value'
    import { name } from '...'

    // 默认导出/导入
    export default 'value'
    import anyName from '...'

    // 重命名导入/导出
    export { name as newName }
    import { newName } from '...'

    // 命名 + 默认 | Import All
    export const name = 'value'
    export default 'value'
    import * as anyName from '...'

    // 导出列表 + 重命名
    export {
    name1,
    name2 as newName2
    }
    import {
    name1 as newName1,
    newName2
    } from '...'


    接下来,我们来一个一个的看?

    命名方式

    这里的关键是要有一个name

    export const name = 'value';
    import { name } from 'some-path/file';

    console.log(name); // 'value'

    大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

    默认方式

    使用默认导出,不需要任何名称,所以我们可以随便命名它?

    export default 'value'
    import anyName from 'some-path/file'

    console.log(anyName) // 'value'

    ❌ 默认方式不用变量名

    export default const name = 'value';  
    // 不要试图给我起个名字!

    命名方式 和 默认方式 一起使用

    命名方式 和 默认方式 可以同个文件中一起使用?

    eport const name = 'value'
    eport default 'value'
    import anyName, { name } from 'some-path/file'

    导出列表

    第三种方式是导出列表(多个)

    const name1 = 'value1'
    const name2 = 'value2'

    export {
    name1,
    name2
    }
    import {name1, name2 } from 'some-path/file'

    console.log(
    name1, // 'value1'
    name2, // 'value2'
    )

    需要注意的重要一点是,这些列表不是对象。它看起来像对象,但事实并非如此。我第一次学习模块时,我也产生了这种困惑。真相是它不是一个对象,它是一个导出列表

    // ❌ Export list ≠ Object
    export {
    name: 'name'
    }

    重命名的导出

    对导出名称不满意?问题不大,可以使用as关键字将其重命名。

    const name = 'value'

    export {
    name as newName
    }
    import { newName } from 'some-path/file'

    console.log(newName); // 'value'

    // 原始名称不可访问
    console.log(name); // ❌ undefined

    ❌ 不能将内联导出与导出列表一起使用

    export const name = 'value'

    // 你已经在导出 name ☝️,请勿再导出我
    export {
    name
    }

    大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

    重命名导入

    同样的规则也适用于导入,我们可以使用as关键字重命名它。

    const name1 = 'value1'
    const name2 = 'value2'

    export {
    name1,
    name2 as newName2
    }
    import {
    name1 as newName1,
    newName2
    } from '...'

    console.log(newName1); // 'value1'
    console.log(newName2); // 'value2'


    name1; // undefined
    name2; // undefined

    导入全部

    export const name = 'value'

    export default 'defaultValue'
    import * as anyName from 'some-path/file'

    console.log(anyName.name); // 'value'
    console.log(anyName.default); // 'defaultValue'

    命名方式 vs 默认方式

    是否应该使用默认导出一直存在很多争论。 查看这2篇文章。

    就像任何事情一样,答案没有对错之分。正确的方式永远是对你和你的团队最好的方式。

    命名与默认导出的非开发术语

    假设你欠朋友一些钱。 你的朋友说可以用现金或电子转帐的方式还钱。 通过电子转帐付款就像named export一样,因为你的姓名已附加在交易中。 因此,如果你的朋友健忘,并开始叫你还钱,说他没收到钱。 这里,你就可以简单地向他们显示转帐证明,因为你的名字在付款中。 但是,如果你用现金偿还了朋友的钱(就像default export一样),则没有证据。 他们可以说当时的 100 块是来自小红。 现金上没有名称,因此他们可以说是你本人或者是任何人?

    那么采用电子转帐(named export)还是现金(default export)更好?

    这取决于你是否信任的朋友?, 实际上,这不是解决这一难题的正确方法。 更好的解决方案是不要将你的关系置于该位置,以免冒险危及友谊,最好还是相互坦诚。 是的,这个想法也适用于你选择named export还是default export。 最终还是取决你们的团队决定,哪种方式对团队比较友好,就选择哪种,毕竟不是你自己一个人在战斗,而是一个团体?

    原文:https://segmentfault.com/a/1190000040187607

    收起阅读 »

    20个 Javascript 技巧,提高我们的摸鱼时间!

    使用方便有用的方法,以减少代码行数,提高我们的工作效率,增加我们的摸鱼时间。在我们的日常任务中,我们需要编写函数,如排序、搜索、寻找惟一值、传递参数、交换值等,所以在这里分享一下我工作多年珍藏的几个常用技巧和方法,以让大家增加摸鱼的时间。这些方法肯定会帮助你:...
    继续阅读 »

    使用方便有用的方法,以减少代码行数,提高我们的工作效率,增加我们的摸鱼时间。

    在我们的日常任务中,我们需要编写函数,如排序、搜索、寻找惟一值、传递参数、交换值等,所以在这里分享一下我工作多年珍藏的几个常用技巧和方法,以让大家增加摸鱼的时间。

    这些方法肯定会帮助你:

    • 减少代码行
    • Coding Competitions
    • 增加摸鱼的时间

    1.声明和初始化数组

    我们可以使用特定的大小来初始化数组,也可以通过指定值来初始化数组内容,大家可能用的是一组数组,其实二维数组也可以这样做,如下所示:

    const array = Array(5).fill(''); 
    // 输出
    (5) ["", "", "", "", ""]

    const matrix = Array(5).fill(0).map(() => Array(5).fill(0))
    // 输出
    (5) [Array(5), Array(5), Array(5), Array(5), Array(5)]
    0: (5) [0, 0, 0, 0, 0]
    1: (5) [0, 0, 0, 0, 0]
    2: (5) [0, 0, 0, 0, 0]
    3: (5) [0, 0, 0, 0, 0]
    4: (5) [0, 0, 0, 0, 0]
    length: 5

    2. 求和,最小值和最大值

    我们应该利用 reduce 方法快速找到基本的数学运算。

    const array = [5,4,7,8,9,2];

    求和

    array.reduce((a,b) => a+b);
    // 输出: 35

    最大值

    array.reduce((a,b) => a>b?a:b);
    // 输出: 9

    最小值

    array.reduce((a,b) => a<b?a:b);
    // 输出: 2

    3.排序字符串,数字或对象等数组

    我们有内置的方法sort()reverse()来排序字符串,但是如果是数字或对象数组呢

    字符串数组排序

    const stringArr = ["Joe", "Kapil", "Steve", "Musk"]
    stringArr.sort();
    // 输出
    (4) ["Joe", "Kapil", "Musk", "Steve"]

    stringArr.reverse();
    // 输出
    (4) ["Steve", "Musk", "Kapil", "Joe"]

    数字数组排序

    const array  = [40, 100, 1, 5, 25, 10];
    array.sort((a,b) => a-b);
    // 输出
    (6) [1, 5, 10, 25, 40, 100]

    array.sort((a,b) => b-a);
    // 输出
    (6) [100, 40, 25, 10, 5, 1]

    对象数组排序

    const objectArr = [ 
    { first_name: 'Lazslo', last_name: 'Jamf' },
    { first_name: 'Pig', last_name: 'Bodine' },
    { first_name: 'Pirate', last_name: 'Prentice' }
    ];
    objectArr.sort((a, b) => a.last_name.localeCompare(b.last_name));
    // 输出
    (3) [{…}, {…}, {…}]
    0: {first_name: "Pig", last_name: "Bodine"}
    1: {first_name: "Lazslo", last_name: "Jamf"}
    2: {first_name: "Pirate", last_name: "Prentice"}
    length: 3

    4.从数组中过滤到虚值

    像 0undefinednullfalse""''这样的假值可以通过下面的技巧轻易地过滤掉。

    const array = [3, 0, 6, 7, '', false];
    array.filter(Boolean);


    // 输出
    (3) [3, 6, 7]

    5. 使用逻辑运算符处理需要条件判断的情况

    function doSomething(arg1){ 
    arg1 = arg1 || 10;
    // 如果arg1没有值,则取默认值 10
    }

    let foo = 10;
    foo === 10 && doSomething();
    // 如果 foo 等于 10,刚执行 doSomething();
    // 输出: 10

    foo === 5 || doSomething();
    // is the same thing as if (foo != 5) then doSomething();
    // Output: 10

    6. 去除重复值

    const array  = [5,4,7,8,9,2,7,5];
    array.filter((item,idx,arr) => arr.indexOf(item) === idx);
    // or
    const nonUnique = [...new Set(array)];
    // Output: [5, 4, 7, 8, 9, 2]

    7. 创建一个计数器对象或 Map

    大多数情况下,可以通过创建一个对象或者Map来计数某些特殊词出现的频率。

    let string = 'kapilalipak';

    const table={};
    for(let char of string) {
    table[char]=table[char]+1 || 1;
    }
    // 输出
    {k: 2, a: 3, p: 2, i: 2, l: 2}

    或者

    const countMap = new Map();
    for (let i = 0; i < string.length; i++) {
    if (countMap.has(string[i])) {
    countMap.set(string[i], countMap.get(string[i]) + 1);
    } else {
    countMap.set(string[i], 1);
    }
    }
    // 输出
    Map(5) {"k" => 2, "a" => 3, "p" => 2, "i" => 2, "l" => 2}

    8. 三元运算符很酷

    function Fever(temp) {
    return temp > 97 ? 'Visit Doctor!'
    : temp < 97 ? 'Go Out and Play!!'
    : temp === 97 ? 'Take Some Rest!': 'Go Out and Play!';;
    }

    // 输出
    Fever(97): "Take Some Rest!"
    Fever(100): "Visit Doctor!"

    9. 循环方法的比较

    • for 和 for..in 默认获取索引,但你可以使用arr[index]
    • for..in也接受非数字,所以要避免使用。
    • forEachfor...of 直接得到元素。
    • forEach 也可以得到索引,但 for...of 不行。

    10. 合并两个对象

    const user = { 
    name: 'Kapil Raghuwanshi',
    gender: 'Male'
    };
    const college = {
    primary: 'Mani Primary School',
    secondary: 'Lass Secondary School'
    };
    const skills = {
    programming: 'Extreme',
    swimming: 'Average',
    sleeping: 'Pro'
    };

    const summary = {...user, ...college, ...skills};

    // 合并多个对象
    gender: "Male"
    name: "Kapil Raghuwanshi"
    primary: "Mani Primary School"
    programming: "Extreme"
    secondary: "Lass Secondary School"
    sleeping: "Pro"
    swimming: "Average"

    11. 箭头函数

    箭头函数表达式是传统函数表达式的一种替代方式,但受到限制,不能在所有情况下使用。因为它们有词法作用域(父作用域),并且没有自己的thisargument,因此它们引用定义它们的环境。

    const person = {
    name: 'Kapil',
    sayName() {
    return this.name;
    }
    }
    person.sayName();
    // 输出
    "Kapil"

    但是这样:

    const person = {
    name: 'Kapil',
    sayName : () => {
    return this.name;
    }
    }
    person.sayName();
    // Output
    "

    13. 可选的链

    const user = {
    employee: {
    name: "Kapil"
    }
    };
    user.employee?.name;
    // Output: "Kapil"
    user.employ?.name;
    // Output: undefined
    user.employ.name
    // 输出: VM21616:1 Uncaught TypeError: Cannot read property 'name' of undefined

    13.洗牌一个数组

    利用内置的Math.random()方法。

    const list = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    list.sort(() => {
    return Math.random() - 0.5;
    });
    // 输出
    (9) [2, 5, 1, 6, 9, 8, 4, 3, 7]
    // 输出
    (9) [4, 1, 7, 5, 3, 8, 2, 9, 6]

    14.双问号语法

    const foo = null ?? 'my school';
    // 输出: "my school"

    const baz = 0 ?? 42;
    // 输出: 0

    剩余和展开语法

    function myFun(a,  b, ...manyMoreArgs) {
    return arguments.length;
    }
    myFun("one", "two", "three", "four", "five", "six");

    // 输出: 6

    const parts = ['shoulders', 'knees']; 
    const lyrics = ['head', ...parts, 'and', 'toes'];

    lyrics;
    // 输出:
    (5) ["head", "shoulders", "knees", "and", "toes"]

    16.默认参数

    const search = (arr, low=0,high=arr.length-1) => {
    return high;
    }
    search([1,2,3,4,5]);

    // 输出: 4

    17. 将十进制转换为二进制或十六进制

    const num = 10;

    num.toString(2);
    // 输出: "1010"
    num.toString(16);
    // 输出: "a"
    num.toString(8);
    // 输出: "12"

    18. 使用解构来交换两个数

    let a = 5;
    let b = 8;
    [a,b] = [b,a]

    [a,b]
    // 输出
    (2) [8, 5]

    19. 单行的回文数检查

    function checkPalindrome(str) {
    return str == str.split('').reverse().join('');
    }
    checkPalindrome('naman');
    // 输出: true

    20.将Object属性转换为属性数组

    const obj = { a: 1, b: 2, c: 3 };

    Object.entries(obj);
    // Output
    (3) [Array(2), Array(2), Array(2)]
    0: (2) ["a", 1]
    1: (2) ["b", 2]
    2: (2) ["c", 3]
    length: 3

    Object.keys(obj);
    (3) ["a", "b", "c"]

    Object.values(obj);
    (3) [1, 2, 3]



    原文:https://dev.to/techygeeky/top...


    收起阅读 »

    Compose Column控件讲解并且实现一个淘宝商品item的效果

    前情提要本篇文章主要对 Compose 中的 Column 进行使用解析,文章结束会使用 Column 和 Row 配合实现一个淘宝商品 Item 的效果,最终效果预览:如果您对 Column 的用法比较娴熟,可以直接看最后一节的内容Column 简单说明Co...
    继续阅读 »


    前情提要

    本篇文章主要对 Compose 中的 Column 进行使用解析,文章结束会使用 Column 和 Row 配合实现一个淘宝商品 Item 的效果,

    最终效果预览:

    如果您对 Column 的用法比较娴熟,可以直接看最后一节的内容

    Column 简单说明

    Column 对应于我们开发中的 LinearLayout.vertical,可以垂直的摆放内部控件

    因为 Row 和 Column 是想通的,只不过 Column 是垂直方向布局的,而 Row 是水平方向布局。所以讲完了 Column 你只需要把例子代码中的 Column 换成 Row 就可以自行查看 Row 的效果了

    Column 参数介绍

    modifier

    用来定义 Column 的各种属性,比如可以定义宽度、高度、背景等

    1. 示例代码

      设置 modifier 的时候可以链式调用

    @Composable
    fun DefaultPreview() {
    Column(modifier = Modifier
    .width(300.dp)
    .height(200.dp)
    .background(color = Color.Green)) {

    }
    }
    1. 实现效果

    展示了一个绿色填充的矩形

    verticalArrangement

    实现内部元素的竖直对齐效果

    关于 verticalArrangement 我们的示例代码如下

    后面介绍每种效果的时候会更改 verticalArrangement 的值进行展示

    Row() {
    Spacer(modifier = Modifier.width(100.dp))
    Column(
    modifier = Modifier
    .width(50.dp)
    .height(200.dp)
    .background(color = Color.Green),
    // verticalArrangement = Arrangement.SpaceAround
    ) {
    Image(modifier = Modifier.size(20.dp),painter = painterResource(id = R.drawable.apple), contentDescription = null)
    Image(modifier = Modifier.size(20.dp),painter = painterResource(id = R.drawable.apple), contentDescription = null)
    Image(modifier = Modifier.size(20.dp),painter = painterResource(id = R.drawable.apple), contentDescription = null)
    Image(modifier = Modifier.size(20.dp),painter = painterResource(id = R.drawable.apple), contentDescription = null)
    }
    }
    不设置该属性的效果
    1. 效果

    2. 结论

      不设置该属性的时候,内部元素贴着顶部紧凑排列

    Arrangement.Center
    1. 效果

    1. 结论

      所有元素垂直居中,紧凑排列

    Arrangement.SpaceBetween
    1. 效果

    1. 结论

      元素之间均分空间,与顶部和底部之间无间距

    SpaceAround 效果
    1. 效果

    1. 结论

      内部元素等分空间,并且顶部和底部留间距(顶部元素距离顶部的距离和底部元素距离底部的距离与元素等分的长度不一致)

    Arrangement.SpaceEvenly
    1. 效果

    1. 结论

      所有元素均分空间(顶部元素距离顶部的距离和底部元素距离底部的距离与元素等分的长度一致)

    Arrangement.Bottom
    1. 效果

    1. 结论

      所有元素靠近底部,紧凑排列

    Arrangement.spacedBy(*.dp)

    可以设置元素间的等分距离

    比如我们设置 20dp,Arrangement.spacedBy(20.dp)

    1. 效果

    1. 结论

    元素之间距离为 20dp,靠近顶部排列

    horizontalAlignment

    实现 Column 的水平约束

    Alignment.Start 居开始的位置对齐
    1. 效果

    1. 结论

      当前模拟器 Start 就是 Right,所以内部元素居左侧对齐

    Alignment.CenterHorizontally 水平居中
    1. 效果

    2. 结论

      内部元素水平居中对齐

    Alignment.End
    1. 效果

    1. 结论

      当前模拟器 End 就是 Left,所以内部元素居右侧对齐

    content

    关于这个属性,注释中都没写他我也就先不研究了

    使用 Column 实现淘宝商品 item 布局

    1. 本例中目标效果图如下

    1. 代码
    @Composable
    fun DefaultPreview2() {
    Row(
    modifier = Modifier.fillMaxSize(1f).background(color= Color.Gray),
    verticalAlignment = Alignment.CenterVertically,
    horizontalArrangement = Arrangement.Center
    ) {
    Column(
    modifier = Modifier
    .width(200.dp)
    .background(color = Color.White)
    .padding(all = 10.dp)
    ) {
    Image(
    painter = painterResource(id = R.drawable.apple),
    contentDescription = null,
    modifier = Modifier.size(180.dp).clip(RoundedCornerShape(10.dp))
    )
    Text(
    text = "当天发,不要钱",
    fontSize = 20.sp,
    style = TextStyle(fontWeight = FontWeight.Bold),
    modifier = Modifier.padding(vertical = 2.dp)
    )
    Row(
    modifier = Modifier.padding(vertical = 2.dp),
    verticalAlignment = Alignment.CenterVertically
    ) {
    Text(
    text = "¥说了不要钱",
    fontSize = 14.sp,
    color = Color(0xff9f8722)
    )
    Text(text = "23人免费拿", fontSize = 12.sp)
    }
    Row(
    modifier = Modifier
    .width(200.dp)
    .fillMaxWidth()
    .padding(vertical = 2.dp),
    verticalAlignment = Alignment.CenterVertically
    ) {
    Text(text = "不要钱")
    Spacer(modifier = Modifier.weight(1f))//通过设置weight让Spacer把Row撑开,实现后面的图片居右对齐的效果
    Image(
    painter = painterResource(id = android.R.drawable.btn_star_big_on),
    contentDescription = null,
    )
    }
    }
    }
    }
    1. 实现说明

    本商品 item 分为四部分:

    第一部分:图片,我们使用 Image 实现

    第二部分:商品描述,使用一个 Text

    第三部分:价格,使用 Row 套两个 Text 实现

    第四部分:分期情况,使用 Row 套一个 Text 和 Image 完成,注意因为图片要居右对齐,所以中间需要使用一个 Spacer 挤满剩余宽度。

    淘宝商品 item 实现要点

    1. 我们可以使用 modifier = Modifier .width(200.dp) 设置 Column 的宽度
    2. Modifier.padding(all = 10.dp)可以设置四个方向的内边距
    3. modifier = Modifier.size(180.dp).clip(RoundedCornerShape(10.dp)可以设置圆角,因为本例中图片背景和控件背景都是白色,所以看不出来效果
    4. 最底部的控件需要让收藏按钮贴近父控件右侧对齐,使用 Modifier.weight 实现: Spacer(modifier = Modifier.weight(1f))
    收起阅读 »

    手把手教集成EaseIMKit源码

    准备工作我们已经安装了cocoapods (如果没有安装,请百度搜索安装cocoapods教程,并安装)下载EaseIM源码: 源码地址:http://docs-im.easemob.com/im/ios/other/easeimkitEaseIMKit 使用...
    继续阅读 »

    准备工作

    我们已经安装了cocoapods (如果没有安装,请百度搜索安装cocoapods教程,并安装)

    下载EaseIM源码: 源码地址:http://docs-im.easemob.com/im/ios/other/easeimkit

    EaseIMKit 使用指南 -> 简介 -> EaseIMKit 源码地址 EaseIMKit工程

    下载完成后,如下目录 (其中红框内的两个文件夹是我们需要的文件夹)




    一.创建工程 + 放入相关文件夹 + 创建Podfile文件


    在这里,我创建了一个叫showDemo的工程,将第零步下载的源码文件中,红框圈住的两个文件夹复制,粘贴入新建的工程文件夹内

    创建Podfile文件

    如下:




    二.修改Podfile文件内容

    其中红框圈住部分为重要部分



    注:最下面红框 为生成Framework而加入.
    //================================
    platform :ios, '11.0'
    workspace 'appName.xcworkspace'
    use_frameworks!
    target 'appName' do
    # pod 'MBProgressHUD'
    # pod 'Masonry'
    # pod 'MJRefresh'
    # pod 'SDWebImage'
    # pod 'AFNetworking'
    #以上为常用第三方库,根据实际情况添加.(井号#代表注释)
    #添加环信的SDK
    pod 'HyphenateChat'
    #加入EaseIMKit源码
    pod 'EaseIMKit', :path => './EaseIMKit/EaseIMKit.podspec'
    #若需要添加音视频功能,则需要集成如下SDK
    # pod 'AgoraRtcEngine_iOS', '3.3.1' #添加声网SDK
    # pod 'EaseCallKit' #添加环信CallKit
    end
    target 'EaseIMKit' do
    project './EaseIMKit/EaseIMKit.xcodeproj'
    pod 'HyphenateChat'
    pod 'EMVoiceConvert'
    end
    //================================


    三.执行pod install

    四.打开项目

    打开工作空间,工作空间文件如下图所示,右键打开.




    打开之后,整体目录如下:



    这里需要注意:

    EaseIMKit.framework(EaseIMKit -> Products -> EaseIMKit.framework)的名字应该是黑色的.

    如果是红色的,代表文件不存在,解决方法:如上图,标记为2的地方,按照图示选项,运行一次,文件即可变黑.

    五.加入framework并运行

    加入Framework



    运行起来吧.

    六.举个例子

    (建议command + b先进行编译一下)在需要引入头文件的地方,加入相关头文件,并写代码,举例说明:



    七.常见报错

    如果工程报错,信息如下:




    而且重新pod install也没有用.
    解决方案:
    我们需要清理掉之前所有pod的第三方,重新pod.
    清理可使用cocoapods-clean
    由于cocoapods-clean并非cocoapods自带,我们需要额外安装
    终端输入命令:
    sudo gem install cocoapods-clean
    并回车,进行安装cocoapods-clean



    cd到工程文件夹目录下,如下:



    先执行
    pod clean
    完成后,再执行
    pod install

    --- end ---



    收起阅读 »

    从 Flutter 和前端角度出发,聊聊单线程模型下如何保证 UI 流畅性

    一、单线程模型的设计1. 最基础的单线程处理简单任务假设有几个任务:任务1: "姓名:" + "杭城小刘"任务2: "年龄:" + "1995" + "02" + "20"任务3: "大小:" + (2021 - 1995 + 1)任务4: 打印任务1、2、3...
    继续阅读 »

    一、单线程模型的设计

    1. 最基础的单线程处理简单任务

    假设有几个任务:

    • 任务1: "姓名:" + "杭城小刘"
    • 任务2: "年龄:" + "1995" + "02" + "20"
    • 任务3: "大小:" + (2021 - 1995 + 1)
    • 任务4: 打印任务1、2、3 的结果

    在单线程中执行,代码可能如下:

    //c
    void mainThread () {
    string name = "姓名:" + "杭城小刘";
    string birthday = "年龄:" + "1995" + "02" + "20"
    int age = 2021 - 1995 + 1;
    printf("个人信息为:%s, %s, 大小:%d", name.c_str(), birthday.c_str(), age);
    }

    线程开始执行任务,按照需求,单线程依次执行每个任务,执行完毕后线程马上退出。

    2. 线程运行过程中来了新的任务怎么处理?

    问题1 介绍的线程模型太简单太理想了,不可能从一开始就 n 个任务就确定了,大多数情况下,会接收到新的 m 个任务。那么 section1 中的设计就无法满足该需求。

    要在线程运行的过程中,能够接受并执行新的任务,就需要有一个事件循环机制。最基础的事件循环可以想到用一个循环来实现。

    // c++
    int getInput() {
    int input = 0;
    cout<< "请输入一个数";
    cin>>input;
    return input;
    }

    void mainThread () {
    while(true) {
    int input1 = getInput();
    int input2 = getInput();
    int sum = input1 + input2;
    print("两数之和为:%d", sum);
    }
    }

    相较于第一版线程设计,这一版做了以下改进:

    • 引入了循环机制,线程不会做完事情马上退出。
    • 引入了事件。线程一开始会等待用户输入,等待的时候线程处于暂停状态,当用户输入完毕,线程得到输入的信息,此时线程被激活。执行相加的操作,最终输出结果。不断的等待输入,并计算输出。

    3. 处理来自其他线程的任务

    真实环境中的线程模块远远没有这么简单。比如浏览器环境下,线程可能正在绘制,可能会接收到1个来自用户鼠标点击的事件,1个来自网络加载 css 资源完成的事件等等。第二版线程模型虽然引入了事件循环机制,可以接受新的事件任务,但是发现没?这些任务之来自线程内部,该设计是无法接受来自其他线程的任务的。

    从上图可以看出,渲染主线程会频繁接收到来自于 IO 线程的一些事件任务,当接受到的资源加载完成后的消息,则渲染线程会开始 DOM 解析;当接收到来自鼠标点击的消息,渲染主线程则会执行绑定好的鼠标点击事件脚本(js)来处理事件。

    需要一个合理的数据结构,来存放并获取其他线程发送的消息?

    消息队列这个词大家都听过,在 GUI 系统中,事件队列是一个通用解决方案。

    消息队列(事件队列)是一种合理的数据结构。要执行的任务添加到队列的尾部,需要执行的任务,从队列的头部取出。

    有了消息队列之后,线程模型得到了升级。如下:

    可以看出改造分为3个步骤:

    • 构建一个消息队列
    • IO 线程产生的新任务会被添加到消息队列的尾部
    • 渲染主线程会循环的从消息队列的头部读取任务,执行任务

    伪代码。构造队列接口部分

    class TaskQueue {
    public:
    Task fetchTask (); // 从队列头部取出1个任务
    void addTask (Task task); // 将任务插入到队列尾部
    }

    改造主线程

    TaskQueue taskQueue;
    void processTask ();
    void mainThread () {
    while (true) {
    Task task = taskQueue.fetchTask();
    processTask(task);
    }
    }

    IO 线程

    void handleIOTask () {
    Task clickTask;
    taskQueue.addTask(clickTask);
    }

    Tips: 事件队列是存在多线程访问的情况,所以需要加锁。

    4. 处理来自其他线程的任务

    浏览器环境中, 渲染进程经常接收到来自其他进程的任务,IO 线程专门用来接收来自其他进程传递来的消息。IPC 专门处理跨进程间的通信。

    5. 消息队列中的任务类型

    消息队列中有很多消息类型。内部消息:如鼠标滚动、点击、移动、宏任务、微任务、文件读写、定时器等等。

    消息队列中还存在大量的与页面相关的事件。如 JS 执行、DOM 解析、样式计算、布局计算、CSS 动画等等。

    上述事件都是在渲染主线程中执行的,因此编码时需注意,尽量减小这些事件所占用的时长。

    6. 如何安全退出

    Chrome 设计上,确定要退出当前页面时,页面主线程会设置一个退出标志的变量,每次执行完1个任务时,判断该标志。如果设置了,则中断任务,退出线程

    7. 单线程的缺点

    事件队列的特点是先进先出,后进后出。那后进的任务也许会被前面的任务因为执行时间过长而阻塞,等待前面的任务执行完毕才可以执行后面的任务。这样存在2个问题。

    • 如何处理高优先级的任务

      假如要监控 DOM 节点的变化情况(插入、删除、修改 innerHTML),然后触发对应的逻辑。最基础的做法就是设计一套监听接口,当 DOM 变化时,渲染引擎同步调用这些接口。不过这样子存在很大的问题,就是 DOM 变化会很频繁。如果每次 DOM 变化都触发对应的 JS 接口,则该任务执行会很长,导致执行效率的降低

      如果将这些 DOM 变化做为异步消息,假如消息队列中。可能会存在因为前面的任务在执行导致当前的 DOM 消息不会被执行的问题,也就是影响了监控的实时性

      如何权衡效率和实时性?微任务 就是解决该类问题的。

      通常,我们把消息队列中的任务成为宏任务,每个宏任务中都包含一个微任务队列,在执行宏任务的过程中,假如 DOM 有变化,则该变化会被添加到该宏任务的微任务队列中去,这样子效率问题得以解决。

      当宏任务中的主要功能执行完毕欧,渲染引擎会执行微任务队列中的微任务。因此实时性问题得以解决

    • 如何解决单个任务执行时间过长的问题

      可以看出,假如 JS 计算超时导致动画 paint 超时,会造成卡顿。浏览器为避免该问题,采用 callback 回调的设计来规避,也就是让 JS 任务延后执行。

    二、 flutter 里的单线程模型

    1. event loop 机制

    Dart 是单线程的,也就是代码会有序执行。此外 Dart 作为 Flutter 这一 GUI 框架的开发语言,必然支持异步。

    一个 Flutter 应用包含一个或多个 isolate,默认方法的执行都是在 main isolate 中;一个 isolate 包含1个 Event loop 和1个 Task queue。其中,Task queue 包含1个 Event queue 事件队列和1个 MicroTask queue 微任务队列。如下:

    为什么需要异步?因为大多数场景下 应用都并不是一直在做运算。比如一边等待用户的输入,输入后再去参与运算。这就是一个 IO 的场景。所以单线程可以再等待的时候做其他事情,而当真正需要处理运算的时候,再去处理。因此虽是单线程,但是给我们的感受是同事在做很多事情(空闲的时候去做其他事情)

    某个任务涉及 IO 或者异步,则主线程会先去做其他需要运算的事情,这个动作是靠 event loop 驱动的。和 JS 一样,dart 中存储事件任务的角色是事件队列 event queue。

    Event queue 负责存储需要执行的任务事件,比如 DB 的读取。

    Dart 中存在2个队列,一个微任务队列(Microtask Queue)、一个事件队列(Event Queue)。

    Event loop 不断的轮询,先判断微任务队列是否为空,从队列头部取出需要执行的任务。如果微任务队列为空,则判断事件队列是否为空,不为空则从头部取出事件(比如键盘、IO、网络事件等),然后在主线程执行其回调函数,如下:

    2. 异步任务

    微任务,即在一个很短的时间内就会完成的异步任务。微任务在事件循环中优先级最高,只要微任务队列不为空,事件循环就不断执行微任务,后续的事件队列中的任务持续等待。微任务队列可由 scheduleMicroTask 创建。

    通常情况,微任务的使用场景比较少。Flutter 内部也在诸如手势识别、文本输入、滚动视图、保存页面效果等需要高优执行任务的场景用到了微任务。

    所以,一般需求下,异步任务我们使用优先级较低的 Event Queue。比如 IO、绘制、定时器等,都是通过事件队列驱动主线程来执行的。

    Dart 为 Event Queue 的任务提供了一层封装,叫做 Future。把一个函数体放入 Future 中,就完成了同步任务到异步任务的包装(类似于 iOS 中通过 GCD 将一个任务以同步、异步提交给某个队列)。Future 具备链式调用的能力,可以在异步执行完毕后执行其他任务(函数)。

    看一段具体代码:

    void main() {
    print('normal task 1');
    Future(() => print('Task1 Future 1'));
    print('normal task 2');
    Future(() => print('Task1 Future 2'))
    .then((value) => print("subTask 1"))
    .then((value) => print("subTask 2"));
    }
    //
    lbp@MBP  ~/Desktop  dart index.dart
    normal task 1
    normal task 2
    Task1 Future 1
    Task1 Future 2
    subTask 1
    subTask 2

    main 方法内,先添加了1个普通同步任务,然后以 Future 的形式添加了1个异步任务,Dart 会将异步任务加入到事件队列中,然后理解返回。后续代码继续以同步任务的方式执行。然后再添加了1个普通同步任务。然后再以 Future 的方式添加了1个异步任务,异步任务被加入到事件队列中。此时,事件队列中存在2个异步任务,Dart 在事件队列头部取出1个任务以同步的方式执行,全部执行(先进先出)完毕后再执行后续的 then。

    Future 与 then 公用1个事件循环。如果存在多个 then,则按照顺序执行。

    例2:

    void main() {
    Future(() => print('Task1 Future 1'));
    Future(() => print('Task1 Future 2'));

    Future(() => print('Task1 Future 3'))
    .then((_) => print('subTask 1 in Future 3'));

    Future(() => null).then((_) => print('subTask 1 in empty Future'));
    }
    lbp@MBP  ~/Desktop  dart index.dart
    Task1 Future 1
    Task1 Future 2
    Task1 Future 3
    subTask 1 in Future 3
    subTask 1 in empty Future

    main 方法内,Task 1 添加到 Future 1中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 2中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 3中,被 Dart 添加到 Event Queue 中,subTask 1 和 Task 1 共用 Event Queue。Future 4中任务为空,所以 then 里的代码会被加入到 Microtask Queue,以便下一轮事件循环中被执行。

    综合例子

    void main() {
    Future(() => print('Task1 Future 1'));
    Future fx = Future(() => null);
    Future(() => print("Task1 Future 3")).then((value) {
    print("subTask 1 Future 3");
    scheduleMicrotask(() => print("Microtask 1"));
    }).then((value) => print("subTask 3 Future 3"));

    Future(() => print("Task1 Future 4"))
    .then((value) => Future(() => print("sub subTask 1 Future 4")))
    .then((value) => print("sub subTask 2 Future 4"));

    Future(() => print("Task1 Future 5"));

    fx.then((value) => print("Task1 Future 2"));

    scheduleMicrotask(() => print("Microtask 2"));

    print("normal Task");
    }
    lbp@MBP  ~/Desktop  dart index.dart
    normal Task
    Microtask 2
    Task1 Future 1
    Task1 Future 2
    Task1 Future 3
    subTask 1 Future 3
    subTask 3 Future 3
    Microtask 1
    Task1 Future 4
    Task1 Future 5
    sub subTask 1 Future 4
    sub subTask 2 Future 4

    解释:

    • Event Loop 优先执行 main 方法同步任务,再执行微任务,最后执行 Event Queue 的异步任务。所以 normal Task 先执行
    • 同理微任务 Microtask 2 执行
    • 其次,Event Queue FIFO,Task1 Future 1 被执行
    • fx Future 内部为空,所以 then 里的内容被加到微任务队列中去,微任务优先级最高,所以 Task1 Future 2 被执行
    • 其次,Task1 Future 3 被执行。由于存在2个 then,先执行第一个 then 中的 subTask 1 Future 3,然后遇到微任务,所以 Microtask 1 被添加到微任务队列中去,等待下一次 Event Loop 到来时触发。接着执行第二个 then 中的 subTask 3 Future 3。随着下一次 Event Loop 到来,Microtask 1 被执行
    • 其次,Task1 Future 4 被执行。随后的第一个 then 中的任务又是被 Future 包装成一个异步任务,被添加到 Event Queue 中,第二个 then 中的内容也被添加到 Event Queue 中。
    • 接着,执行 Task1 Future 5。本次事件循环结束
    • 等下一轮事件循环到来,打印队列中的 sub subTask 1 Future 4、sub subTask 1 Future 5.

    3. 异步函数

    异步函数的结果在将来某个时刻才返回,所以需要返回一个 Future 对象,供调用者使用。调用者根据需求,判断是在 Future 对象上注册一个 then 等 Future 执行体结束后再进行异步处理,还是同步等到 Future 执行结束。Future 对象如果需要同步等待,则需要在调用处添加 await,且 Future 所在的函数需要使用 async 关键字。

    await 并不是同步等待,而是异步等待。Event Loop 会将调用体所在的函数也当作异步函数,将等待语句的上下文整体添加到 Event Queue 中,一旦返回,Event Loop 会在 Event Queue 中取出上下文代码,等待的代码继续执行。

    await 阻塞的是当前上下文的后续代码执行,并不能阻塞其调用栈上层的后续代码执行

    void main() {
    Future(() => print('Task1 Future 1'))
    .then((_) async => await Future(() => print("subTask 1 Future 2")))
    .then((_) => print("subTask 2 Future 2"));
    Future(() => print('Task1 Future 2'));
    }
    lbp@MBP  ~/Desktop  dart index.dart
    Task1 Future 1
    Task1 Future 2
    subTask 1 Future 2
    subTask 2 Future 2

    解析:

    • Future 中的 Task1 Future 1 被添加到 Event Queue 中。其次遇到第一个 then,then 里面是 Future 包装的异步任务,所以 Future(() => print("subTask 1 Future 2")) 被添加到 Event Queue 中,所在的 await 函数也被添加到了 Event Queue 中。第二个 then 也被添加到 Event Queue 中
    • 第二个 Future 中的 'Task1 Future 2 不会被 await 阻塞,因为 await 是异步等待(添加到 Event Queue)。所以执行 'Task1 Future 2。随后执行 "subTask 1 Future 2,接着取出 await 执行 subTask 2 Future 2

    4. Isolate

    Dart 为了利用多核 CPU,将 CPU 层面的密集型计算进行了隔离设计,提供了多线程机制,即 Isolate。每个 Isolate 资源隔离,都有自己的 Event Loop 和 Event Queue、Microtask Queue。Isolate 之间的资源共享通过消息机制通信(和进程一样)

    使用很简单,创建时需要传递一个参数。

    void coding(language) {
    print("hello " + language);
    }
    void main() {
    Isolate.spawn(coding, "Dart");
    }
    lbp@MBP  ~/Desktop  dart index.dart
    hello Dart

    大多数情况下,不仅仅需要并发执行。可能还需要某个 Isolate 运算结束后将结果告诉主 Isolate。可以通过 Isolate 的管道(SendPort)实现消息通信。可以在主 Isolate 中将管道作为参数传递给子 Isolate,当子 Isolate 运算结束后将结果利用这个管道传递给主 Isolate

    void coding(SendPort port) {
    const sum = 1 + 2;
    // 给调用方发送结果
    port.send(sum);
    }

    void main() {
    testIsolate();
    }

    testIsolate() async {
    ReceivePort receivePort = ReceivePort(); // 创建管道
    Isolate isolate = await Isolate.spawn(coding, receivePort.sendPort); // 创建 Isolate,并传递发送管道作为参数
    // 监听消息
    receivePort.listen((message) {
    print("data: $message");
    receivePort.close();
    isolate?.kill(priority: Isolate.immediate);
    isolate = null;
    });
    }
    lbp@MBP  ~/Desktop  dart index.dart
    data: 3

    此外 Flutter 中提供了执行并发计算任务的快捷方式-compute 函数。其内部对 Isolate 的创建和双向通信进行了封装。

    实际上,业务开发中使用 compute 的场景很少,比如 JSON 的编解码可以用 compute。

    计算阶乘:

    int testCompute() async {
    return await compute(syncCalcuateFactorial, 100);
    }

    int syncCalcuateFactorial(upperBounds) => upperBounds < 2
    ? upperBounds
    : upperBounds * syncCalcuateFactorial(upperBounds - 1);

    总结:

    • Dart 是单线程的,但通过事件循环可以实现异步
    • Future 是异步任务的封装,借助于 await 与 async,我们可以通过事件循环实现非阻塞的同步等待
    • Isolate 是 Dart 中的多线程,可以实现并发,有自己的事件循环与 Queue,独占资源。Isolate 之间可以通过消息机制进行单向通信,这些传递的消息通过对方的事件循环驱动对方进行异步处理。
    • flutter 提供了 CPU 密集运算的 compute 方法,内部封装了 Isolate 和 Isolate 之间的通信
    • 事件队列、事件循环的概念在 GUI 系统中非常重要,几乎在前端、Flutter、iOS、Android 甚至是 NodeJS 中都存在。
    收起阅读 »

    JavaScript中关于null的一切

    JavaScript有2种类型:基本类型(string, booleans number, symbol)和对象。对象是复杂的数据结构,JS 中最简单的对象是普通对象:一组键和关联值:let myObject = { name...
    继续阅读 »

    JavaScript有2种类型:基本类型(stringbooleans numbersymbol)和对象。

    对象是复杂的数据结构,JS 中最简单的对象是普通对象:一组键和关联值:

    let myObject = {
    name: '前端小智'
    }

    但是在某些情况下无法创建对象。 在这种情况下,JS 提供一个特殊值null —表示缺少对象。

    let myObject = null

    在本文中,我们将了解到有关JavaScript中null的所有知识:它的含义,如何检测它,nullundefined之间的区别以及为什么使用null造成代码维护困难。

    1. null的概念

    JS 规范说明了有关null的信息:

    值 null 特指对象的值未设置,它是 JS 基本类型 之一,在布尔运算中被认为是falsy

    例如,函数greetObject()创建对象,但是在无法创建对象时也可以返回null

    function greetObject(who) {
    if (!who) {
    return null;
    }
    return { message: `Hello, ${who}!` };
    }

    greetObject('Eric'); // => { message: 'Hello, Eric!' }
    greetObject(); // => null

    但是,在不带参数的情况下调用函数greetObject() 时,该函数返回null。 返回null是合理的,因为who参数没有值。

    2. 如何检查null

    检查null值的好方法是使用严格相等运算符:

    const missingObject = null;
    const existingObject = { message: 'Hello!' };

    missingObject === null; // => true
    existingObject === null; // => false

    missingObject === null的结果为true,因为missingObject变量包含一个null 值。

    如果变量包含非空值(例如对象),则表达式existObject === null的计算结果为false

    2.1 null 是虚值

    nullfalse0''undefinedNaN都是虚值。如果在条件语句中遇到虚值,那么 JS 将把虚值强制为false

    Boolean(null); // => false

    if (null) {
    console.log('null is truthy')
    } else {
    console.log('null is falsy')
    }

    2.2 typeof null

    typeof value运算符确定值的类型。 例如,typeof 15是'number'typeof {prop:'Value'}的计算结果是'object'

    有趣的是,type null的结果是什么

    typeof null// => 'object'

    为什么是'object'typoef nullobject是早期 JS 实现中的一个错误。

    要使用typeof运算符检测null值。 如前所述,使用严格等于运算符myVar === null

    如果我们想使用typeof运算符检查变量是否是对象,还需要排除null值:

    function isObject(object) {
    return typeof object === 'object' && object !== null;
    }

    isObject({ prop: 'Value' }); // => true
    isObject(15); // => false
    isObject(null); // => false

    3. null 的陷阱

    null经常会在我们认为该变量是对象的情况下意外出现。然后,如果从null中提取属性,JS 会抛出一个错误。

    再次使用greetObject() 函数,并尝试从返回的对象访问message属性:

    let who = '';

    greetObject(who).message;
    // throws "TypeError: greetObject() is null"

    因为who变量是一个空字符串,所以该函数返回null。 从null访问message属性时,将引发TypeError错误。

    可以通过使用带有空值合并的可选链接来处理null:

    let who = ''

    greetObject(who)?.message ?? 'Hello, Stranger!'
    // => 'Hello, Stranger!'

    4. null 的替代方法

    当无法构造对象时,我们通常的做法是返回null,但是这种做法有缺点。在执行堆栈中出现null时,刚必须进行检查。

    尝试避免返回 null 的做法:

    • 返回默认对象而不是null
    • 抛出错误而不是返回null

    回到开始返回greeting对象的greetObject()函数。缺少参数时,可以返回一个默认对象,而不是返回null

    function greetObject(who) {
    if (!who) {
    who = 'Stranger';
    }
    return { message: `Hello, ${who}!` };
    }

    greetObject('Eric'); // => { message: 'Hello, Eric!' }
    greetObject(); // => { message: 'Hello, Stranger!' }

    或者抛出一个错误:

    function greetObject(who) {
    if (!who) {
    throw new Error('"who" argument is missing');
    }
    return { message: `Hello, ${who}!` };
    }

    greetObject('Eric'); // => { message: 'Hello, Eric!' }
    greetObject(); // => throws an error

    这两种做法可以避免使用 null

    5. null vs undefined

    undefined是未初始化的变量或对象属性的值,undefined是未初始化的变量或对象属性的值。

    let myVariable;

    myVariable; // => undefined

    nullundefined之间的主要区别是,null表示丢失的对象,而undefined表示未初始化的状态。

    严格的相等运算符===区分nullundefined :

    null === undefined // => false

    而双等运算符==则认为nullundefined 相等

    null == undefined // => true

    我使用双等相等运算符检查变量是否为null 或undefined:

    function isEmpty(value) {
    return value == null;
    }

    isEmpty(42); // => false
    isEmpty({ prop: 'Value' }); // => false
    isEmpty(null); // => true
    isEmpty(undefined); // => true

    6. 总结

    null是JavaScript中的一个特殊值,表示丢失的对象,严格相等运算符确定变量是否为空:variable === null

    typoef运算符对于确定变量的类型(numberstringboolean)很有用。 但是,如果为null,则typeof会产生误导:typeof null的值为'object'

    nullundefined在某种程度上是等价的,但null表示缺少对象,而undefined未初始化状态。


    原文:https://segmentfault.com/a/1190000040222768

    收起阅读 »

    Web 动画原则及技巧浅析

    在 Web 动画方面,有一套非常经典的原则 -- Twelve basic principles of animation,也就是关于动画的 12 个基本原则(也称之为迪士尼动画原则),网上对它的解读延伸的文章也非常之多:Animation Prin...
    继续阅读 »

    在 Web 动画方面,有一套非常经典的原则 -- Twelve basic principles of animation,也就是关于动画的 12 个基本原则(也称之为迪士尼动画原则),网上对它的解读延伸的文章也非常之多:

    其中使用的示例 DEMO 属于比较简单易懂,但是没有很好地体现在实际生产中应该如何灵活运用。今天本文将带大家再次复习复习,并且替换其中的最基本的 DEMO,换成一些到今天非常实用,非常酷炫的动画 DEMO 效果。

    Squash and stretch -- 挤压和拉伸

    挤压和拉伸的目的是为绘制的对象赋予重量感和灵活性。它可以应用于简单的物体,如弹跳球,或更复杂的结构,如人脸的肌肉组织。

    应用在动画中,这一原则最重要的方面是对象的体积在被挤压或拉伸时不会改变。如果一个球的长度被垂直拉伸,它的宽度(三个维度,还有它的深度)需要相应地水平收缩。

    看看上面这张图,很明显右边这个运动轨迹要比左边的真实很多。

    原理动画如下:

    类似的一些比较有意思的 Web 动画 DEMO:

    CodePen Demo -- CSS Flippy Loader 🍳 By Jhey

    仔细看上面这个 Loading 动画,每个块在跳起之前都会有一个压缩准备动作,在压缩的过程中高度变低,宽度变宽,这就是挤压和拉伸,让动画看上去更加真实。

    OK,再看两个类似的效果,加深下印象:

    CodePen Demo -- CSS Loading Animation

    CodePen Demo -- CSS Animation Loader - Jelly Box

    简单总结一下,挤压和拉伸的核心在于保持对象的体积一致,当拉伸元素时,它的宽度需要变薄,而当挤压元素时,它的宽度需要变宽。

    Anticipation -- 预备动作

    准备动作用于为主要的动画动作做好准备,并使动作看起来更逼真。

    譬如从地板上跳下来的舞者必须先弯曲膝盖,挥杆的高尔夫球手必须先将球杆向后挥动。

    原理动画如下,能够看到滚动之前的一些准备动作:

    看看一些实际应用的chang场景,下面这个动画效果:

    CodePen Demo -- Never-ending box By Pawel

    小球向上滚动,但是仔细看的话,每次向上滚动的时候都会先向后摆一下,可以理解为是一个蓄力动作,也就是我们说的准备动作。

    类似的,看看这个购物车动画,运用了非常多的小技巧,其中之一就是,车在向前冲之前会后退一点点进行一个蓄力动作,整个动画的感觉明显就不一样,它让动画看起来更加的自然:

    Staging -- 演出布局

    Staging 意为演出布局,它的目的是引导观众的注意力,并明确一个场景中什么是最重要的。

    可以通过多种方式来完成,例如在画面中放置角色、使用光影,或相机的角度和位置。该原则的本质是关注核心内容,避免其他不必要的细节吸引走用户的注意力。

    原理动画如下:

    上述 Gif 原理图效果不太明显,看看示例效果:

    CodePen Demo -- CSS Loading Animation

    该技巧的核心就是在动画的过程中把主体凸显,把非主体元素通过模糊、变淡等方式弱化其效果,降低用户在其之上的注意力。

    Straight-Ahead Action and Pose-to-Pose -- 连续运动和姿态对应

    其实表示的就是逐帧动画和补间动画:

    • FrameAnimation(逐帧动画):将多张图片组合起来进行播放,可以利用 CSS Aniation 的 Steps,画面由一帧一帧构成,类似于漫画书
    • TweenAnimation(补间动画):补间动画是在时间帧上进行关键帧绘制,不同于逐帧动画的每一帧都是关键帧,补间动画可以在一个关键帧上绘制一个基础形状,然后在时间帧上对另一个关键帧进行形状转变或绘制另一个形状等,然后中间的动画过程是由计算机自动生成。

    这个应该是属于最基础的了,在不同场景下有不同的妙用。我们在用 CSS 实现动画的过程中,使用的比较多的应该是补间动画,逐帧动画也很有意思,譬如设计师设计好的复杂动画,利用多张图片拼接成逐帧动画也非常不错。

    逐帧动画和补间动画适用在不同的场合,没有谁更好,只有谁更合适,比较下面两个时钟动画,其中一个的秒针运用的是逐帧动画,另外一个则是补间动画:

    • 时钟秒针运用的是逐帧动画:

    CodePen Demo -- CSS3 Working Clock By Ilia

    • 时钟秒针运用的是补间动画:

    CodePen Demo -- CSS Rotary Clock By Jake Albaugh

    有的时候一些复杂动画无法使用 CSS 直接实现的,也会利用逐帧的效果近似实现一个补间动画,像是苹果这个耳机动画,就是实际逐帧动画,但是看起来是连续的:

    CodePen Demo -- Apple AirPods Pro Animation (final demo) By Blake Bowen

    这里其实是多张图片的快速轮播,每张图片表示一个关键帧。

    Follow through and overlapping action 跟随和重叠动作

    跟随和重叠动作是两种密切相关的技术的总称,它们有助于更真实地渲染运动,并有助于给人一种印象,即运动的元素遵循物理定律,包括惯性原理。

    • 跟随意味着在角色停止后,身体松散连接的部分应该继续移动,并且这些部分应该继续移动到角色停止的点之外,然后才被拉回到重心或表现出不同的程度的振荡阻尼;
    • 重叠动作是元素各部分以不同速率移动的趋势(手臂将在头部的不同时间移动等等);
    • 第三种相关技术是拖动,元素开始移动,其中一部分需要几帧才能追上。

    要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。

    原理示意图:

    看看下面这个购物车动画,仔细看购物车,在移动到停止的过程中,有个很明显的刹车再拉回的感觉,这里运用到了跟随的效果,让动画更加生动真实:

    Slow In and Slow Out -- 缓入缓出

    现实世界中物体的运动,如人体、动物、车辆等,需要时间来加速和减速。

    真实的运动效果,它的缓动函数一定不是 Linear。出于这个原因,运动往往是逐步加速并在停止前变慢,实现一个慢进和慢出的效果,以贴近更逼真的动作。

    示意图:

    这个还是很好理解的。真实世界中,很少有缓动函数是 Linear 的运动。

    Arc -- 弧线运动

    大多数自然动作倾向于遵循一个拱形轨迹,动画应该遵循这个原则,遵循隐含的弧形以获得更大的真实感。

    原理示意图:

    嗯哼,在很多动画中,使用弧线代替直线,能够让动画效果更佳的逼真。看看下面这个烟花粒子动画:

    CodePen Demo -- Particles, humankind's only weakness By Rik Schennink

    整个烟花粒子动画看上去非常的自然,因为每个粒子的下落都遵循了自由落体的规律,它们的运动轨迹都是弧线而不是直线。

    Secondary Action -- 次要动作

    将次要动作添加到主要动作可以使场景更加生动,并有助于支持主要动作。走路的人可以同时摆动手臂或将手臂放在口袋里,说话或吹口哨,或者通过面部表情来表达情绪。

    原理示意图:

    简单的一个应用实例,看看下面这个动画:

    CodePen Demo -- Submarine Animation (Pure CSS) By Akhil Sai Ram

    这里实现了一个潜艇向前游动的画面,动画本身还有很多可以优化的地方。但也有一些值得学习肯定的地方,动画使用了尾浆转动和气泡和海底景物移动。

    同时,值得注意的是,窗口的反光也是一个很小的细节,表示船体在移动,这个就属于一个次要动作,衬托出主体的移动。

    再看看下面这打印动画,键盘上按键的上上下下模拟了点击效果,其实也是个次要动作,衬托主体动画效果:

    ![Secondary Action - CodePen Home
    CSS Typewriter](https://p3-juejin.byteimg.com...

    CodePen Demo -- CSS Typewriter By Aaron Iker

    Timing -- 时间节奏

    时间是指给定动作的绘图或帧数,它转化为动画动作的速度。

    在纯粹的物理层面上,正确的计时会使物体看起来遵守物理定律。例如,物体的重量决定了它对推动力的反应,因为重量轻的物体会比重量大的物体反应更快。

    同一个动画,使用不同的速率展示,其效果往往相差很多。对于 Web 动画而言,可能只需要调整 animation-duration 或 transition-duration 的值。

    原理示意图:

    可以看出,同个动画,不同的缓动函数,或者赋予不同的时间,就能产生很不一样的效果。

    当然,时间节奏可以运用在很多地方,譬如在一些 Loading 动画中:

    CodePen Demo -- Only Css 3D Cube By Hisami Kurita

    又或者是这样,同个动画,不同的速率:

    CodePen Demo -- Rotating Circles Preloader

    也可以是同样的延迟、同样的速率,但是不同的方向:

    CodePen Demo -- 2020 SVG Animation By @keyframers

    Exaggeration -- 夸张手法

    夸张是一种对动画特别有用的效果,因为力求完美模仿现实的动画动作可能看起来是静态和沉闷的。

    使用夸张时,一定程度的克制很重要。如果一个场景包含多个元素,则应平衡这些元素之间的关系,以避免混淆或吓倒观众。

    原理示意图:

    OK,不同程度的展现对效果的感官是不一样的,对比下面两个故障艺术动画:

    轻微晃动故障:

    严重晃动故障:

    CodePen Demo -- Glitch Animation

    可以看出,第二个动画明显能感受到比第一个更严重的故障。

    过多的现实主义会毁掉动画,或者说让它缺乏吸引力,使其显得静态和乏味。相反,为元素对象添加一些夸张,使它们更具活力,能够让它们更吸引眼球。

    Solid drawing -- 扎实的描绘

    这个原则表示我们的动画需要尊重真实性,譬如一个 3D 立体绘图,就需要考虑元素在三维空间中的形式。

    了解掌握三维形状、解剖学、重量、平衡、光影等的基础知识。有助于我们绘制出更为逼真的动画效果。

    原理示意图:

    再再看看下面这个动画,名为 Close the blinds -- 关上百叶窗:

    CodePen Demo -- Close the blinds By Chance Squires

    hover 的时候有一个关上动画,使用多块 div 模拟了百叶窗的落下,同时配合了背景色从明亮到黑暗的过程,很好的利用了色彩光影辅助动画的展示。

    再看看这个摆锤小动画,也是非常好的使用了光影、视角元素:

    CodePen Demo -- The Three-Body Problem By Vian Esterhuizen

    最后这个 Demo,虽然是使用 CSS 实现的,但是也尽可能的还原模拟了现实中纸张飞舞的形态,并且对纸张下方阴影的变化也做了一定的变化:

    CodePen Demo -- D CSS-only flying page animation tutorial By @keyframers

    好的动画,细节是经得起推敲的。

    Appeal -- 吸引力

    一反往常,精美的细节往往能非常好的吸引用户的注意力。

    吸引力是艺术作品的特质,而如何实现有吸引力的作品则需要不断的尝试。

    原理示意图:

    我觉得这一点可能是 Web 动画的核心,一个能够吸引人的动画,它肯定是有某些特质的,让我们一起来欣赏下。

    CodePen Demo -- Download interaction By Milan Raring

    通过一连串的动作,动画展开、箭头移动、进度条填满、数字变化,把一个下载动画展示的非常 Nice,让人在等待的过程并不觉得枯燥。

    再来看看这个视频播放的效果:

    CodePen Demo -- Video button animation - Only CSS

    通过一个遮罩 hover 放大,再到点击全屏的变化,一下子就将用户的注意力给吸引了过来。

    Web 动画的一些常见误区

    当然,上述的一些技巧源自于迪士尼动画原则,我们可以将其中的一些思想贯穿于我们的 Web 动画的设计之中。

    但是,必须指出的是,Web 动画本身在使用的时候,也有一些原则是我们需要注意的。主要有下面几点:

    • 增强动画与页面元素之间的关联性
    • 不要为了动画而动画,要有目的性
    • 动画不要过于缓慢,否则会阻碍交互

    增强动画与页面元素之间的关联性

    这个是一个常见的问题,经常会看到一些动画与主体之间没有关联性。关联性背后的逻辑,能帮助用户在界面布局中理解刚发生的变化,是什么导致了变化。

    好的动画可以做到将页面的多个环节或者场景有效串联。

    比较下面两个动画,第二个就比第一个更有关联性:

    没有强关联性的:

    有关联性的:

    很明显,第二个动画比第一个动画更能让用户了解页面发生的变化。

    不要为了动画而动画,要有目的性

    这一点也很重要,不要为了动画而动画,要有目的性,很多时候很多页面的动画非常莫名其妙。

    emm,简单一点来说就是单纯的为了炫技而存在的动画。这种动画可以存在于你的 Demo,你的个人网站中,但不太适合用于线上业务页面中。

    使用动画应该有明确的目的性,譬如 Loading 动画能够让用户感知到页面正在发生变化,正在加载内容。

    在我们的交互过程中,适当的增加过渡与动画,能够很好的让用户感知到页面的变化。类似的还有一些滚动动画。丝滑的滚动切换比突兀的内容明显是更好的体验。

    动画不要过于缓慢,否则会阻碍交互

    缓慢的动画,它产生了不必要的停顿。

    一些用户会频繁看到它们的过渡动画,尽可能的保持简短。让动画持续时间保持在 300ms 或更短。

    比较下面两个动画,第一次可能会让用户耳目一新,但是如果用户在浏览过程中频繁出现通过操作,过长的转场动画会消耗用户大量不必要的时间:

    过长的转场动画:

    缩短转场动画时间,保持恰当的时长:

    结合产品及业务的创意交互动画

    这一点是比较有意思的。我个人认为,Web 动画做得好用的妙,是能非常好的提升用户体验,提升品牌价值的。

    结合产品及业务的创意动画,是需要挖掘,不断打磨的不断迭代的。譬如大家津津乐道的 BiliBili 官网,它的顶部 Banner,配合一些节日、活动,经常就会有出现一些有意思的创意交互动画。简单看两个:

    以及这个:

    我非常多次在不同地方看到有人讨论 Bilibili 的顶部 banner 动画,可见它这块的动画是成功的。很好的结合了一些节日、实事、热点,当成了一种比较固定的产品去不断推陈出新,在不同时候给与用户不同的体验。

    考虑动画的性价比

    最后一条,就是动画虽好,但是打磨一个精品动画是非常耗时的,尤其是在现在非常多的产品业务都是处于一种敏捷开发迭代之下。

    一个好的 Web 动画从构思到落地,绝非前端一个人的工作,需要产品、设计、前端等等相关人员公共努力, 不断修改才能最终呈现比较好的效果。所以在项目初期,一定需要考虑好性价比,是否真的值得为了一个 Web 动画花费几天时间呢?当然这是一个非常见仁见智的问题。

    参考文章

    最后

    想使用 Web 技术绘制生动有趣的动画并非易事,尤其在现在国内的大环境下,鲜有人会去研究动画原则,并运用于实践生产之中。但是它本身确实是个非常有意思有技术的事情。希望本文能给大伙对 Web 动画的认知带来一些提升和帮助,在后续的工作中多少运用一些。

    原文:https://segmentfault.com/a/1190000040223372

    收起阅读 »

    这个vue3的应用框架你学习了吗?

    vue
    1.新项目初期当我们开始一个新项目的筹备的时候(这里特指中后台应用),项目初始化往往我们可能会考虑以下几个问题如何统一做权限管理?如何统一对请求库比如基于 Axios做封装(取消重复请求、请求节流、错误异常处理等统一处理)如何作为子应用嵌入到微前端体系(假设基...
    继续阅读 »

    1.新项目初期

    当我们开始一个新项目的筹备的时候(这里特指中后台应用),项目初始化往往我们可能会考虑以下几个问题
    • 如何统一做权限管理?
    • 如何统一对请求库比如基于 Axios做封装(取消重复请求、请求节流、错误异常处理等统一处理)
    • 如何作为子应用嵌入到微前端体系(假设基于qiankun)
    • 如何共享响应式数据?
    • 配置信息如何管理?

    1.1 你可能会这样做

    如果每次新建一个项目得时候,我们都得手动去处理以上这些问题,那么将是一个重复性操作,而且还要确保团队一致,那么还得考虑约束能力

    在没有看到这个Fes.js这个解决方案之前,对于上述问题,我的解决方式就是
    • 通过维护一个公共的工具库来封装,比如axios的二次封装
    • 开发一个简易的脚手架,把这些东西集成到一个模板中,再通过命令行去拉取
    • 直接通过vue-cli生成模板再进行自定义配置修改等等,简单就是用文档,工具,脚手架来赋能

      但其实有没有更好的解决方案?

    图片引自文章《蚂蚁前端研发最佳实践》

    1.2 其他解决方式 - 框架(插件化)

    学习react的童鞋都知道,在react社区有个插件化的前端应用框架 UmiJS,而vue的世界中并不存在,而接下来我们要分享的 Fes.js就是vue中的 UmiJS, Fes.js 很多功能是借鉴 UmiJS 做的, UmiJS 内置了路由、构建、部署、测试等,还支持插件和插件集,以满足功能和垂直域的分层需求。

    本质上是为了更便捷、更快速地开发中后台应用。框架的核心是插件管理,提供的内置插件封装了大量构建相关的逻辑,并且有着丰富的插件生态,业务中需要处理的脏活累活靠插件来解决,而用户只需要简单配置或者按照规范使用即可

    甚至你还可以将插件做聚合成插件集,类似 babel 的 plugin 和 preset,或者 eslint 的 rule 和 config。通过插件和插件集来满足不同场合的业务

    通过插件扩展 import from UmiJS 的能力,比如类似下图,是不是很像vue 3Composition API设计

    拓展阅读:

    • UmiJS 插件体系的一些初步理解

      2. Fes.js

      官方介绍: Fes.js 是一个好用的前端应用解决方案。 Fes.js 2.0 以Vue 3.0和路由为基础,同时支持配置式路由和约定式路由,并以此进行功能扩展。匹配了覆盖编译时和运行时生命周期完善的插件体系,支持各种功能扩展和业务需求。

    2.1 支持约定式路由

    约定式路由是个啥? 约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,现在越来越多框架支持约定式路由,包括上文提到的 UmiJS,还有SSR的nuxt等等,节省我们手动配置路由的时间. 关于fes更多的路由配置看路由文档

    2.2 插件化支持

    本质上一个插件是就是一个npm包, 通过插件扩展Fes.js的功能,目前 Fes.js已经有多个插件开源。而且插件可以管理项目的编译时和运行时 插件文档

    插件源码地址 链接。fesjs也支持开发者自定义插件,详情看插件化开发文档

    彬彬同学: 那什么叫支持编译时和运行时?

    可以这样理解: 如果是编译时的配置,就是打包的时候,就根据配置完成相应的代码构建,而运行时的配置,则是代码在浏览器执行时,才会根据读取的配置去做相应处理,如果感兴趣,可以深入理解下fesjs的插件源码,了解如何根据编译时和运行时做处理 fes-plugin-access 源码链接

    2.3 Fes.js 如何使用

    Fes.js 提供了命令行工具 create-fes-app, 全局安装后直接通过该命令创建项目模板,项目结构如下所示

    然后运行 npm run dev 就可以开启你的fes之路, 如下图所示

    2.4 为啥选择 Fes.js

    像vue-cli 只能解决我们项目中开发,构建,打包等基本问题,而 Fes.js可以直接解决大部分常规中后台应用的业务场景的问题,包括如下
    • 配置化布局:解决布局、菜单 、导航等配置问题,类似low-code机制
    • 权限控制:通过内置的access插件实现站点复杂权限管理
    • 请求库封装:通过内置的request插件,内置请求防重、请求节流、错误处理等功能
    • 微前端集成:通过内置qiankun插件,快速集成到微前端中体系

    期待更多的插件可以赋能中后台应用业务场景

    3.回顾 vue 3

    3.1 新特征

    vue3.0 相对于 vue2.0变更几个比较大的点包括如下

    • 性能提升: 随着主流浏览器对es6的支持,es module成为可以真正落地的方案,也进一步优化了vue的性能
    • 支持typescript: 通过ts其类型检查机制,可避免我们在重构过程中引入意外的错误
    • 框架体积变小:框架体积优化后,一方面是因为引入Composition API的设计,同时支持tree-shaking树摇,按需引入模块API,将无用模块都会最终被摇掉,使得最终打包后的bundle的体积更小
    • 更优的虚拟Dom方案实现 : 添加了标记flag,Vue2的Virtual DOM不管变动多少整个模板会进行重新的比对, 而vue3对动态dom节点进行了标记PatchFlag ,只需要追踪带有PatchFlag的节点。并且当节点的嵌套层级多的情况,动态节点都是直接跟根节点直接绑定的,也就是说当diff算法走到了根dom节点的时候,就会直接定位动态变化的节点,并不会去遍历静态dom节点,以此提升了效率
    • 引入Proxy特性: 取代了vue2的Object.defineProperty来实现双向绑定,因为其本身的局限性,只能劫持对象的属性,如果对象属性值是对象,还需要进行深度遍历,才能做到劫持,并不能真正意义上的完整劫持整个对象,而proxy可以完整劫持整个对象

    3.2 关于 Composition API

    vue3 取代了原本vue2通过Options API来构建组件设计(强制我们进行代码分层),而采用了类似React Hooks的设计,通过可组组合式的、低侵入式的、函数式的 API,使得我们构建组件更加灵活。官方定义:一组基于功能的附加API,可以灵活地组合组件逻辑

    通过上图的对比,我们可以看出Composition API 与 Options API在构建组件的差别,很明显基于Composition API构建会更加清晰明了。我们会发现vue3几个不同的点:

    • vue3提供了两种数据响应式监听APIrefreactive,这两者的区别在 reactive主要用于定义复杂的数据类型比如对象,而ref则用于定义基本类型比如字符串
    • vue3 提供了setup(props, context)方法,这是使用Composition API 的前提入口,相当于 vue2.x 在 生命周期beforeCreate 之后 created 之前执行,方法中的props参数是用来获取在组件中定义的props的,需要注意的是props是响应式的, 并不能使用es6解构(它会消除prop的响应性),如果需要监听响应还需要使用wacth。而context参数来用来获取attribute,获取插槽,或者发送事件,比如 context.emit,因为在setup里面没有this上下文,只能使用context来获取山下文

    关于vue3的更多实践后期会继续更新,本期主要是简单回顾

    你好,我是🌲 树酱,请你喝杯🍵 记得三连哦~

    1.阅读完记得点个赞哦,有👍 有动力

    2.关注公众号前端那些趣事,陪你聊聊前端的趣事

    3.文章收录在Github frontendThings 感谢Star✨

    原文:https://segmentfault.com/a/1190000040236420



    收起阅读 »

    Esbuild 为什么那么快

    Esbuild 是什么Esbuild 是一个非常新的模块打包工具,它提供了与 Webpack、Rollup、Parcel 等工具相似的资源打包能力,却有着高的离谱的性能优势:下面展开细讲。为什么快语言优势大多数前端打包工具都是基于 JavaScript 实现的...
    继续阅读 »

    Esbuild 是什么

    Esbuild 是一个非常新的模块打包工具,它提供了与 Webpack、Rollup、Parcel 等工具相似的资源打包能力,却有着高的离谱的性能优势:

    从上到下,耗时逐步上升达到数百倍的差异,这个巨大的性能优势使得 Esbuild 在一众基于 Node 的构建工具中迅速蹿红,特别是 Vite 2.0 宣布使用 Esbuild 预构建依赖后,前端社区关于它的讨论热度逐渐上升。

    那么问题来了,这是怎么做到的?我翻阅了很多资料后,总结了一些关键因素:

    下面展开细讲。

    为什么快

    语言优势

    大多数前端打包工具都是基于 JavaScript 实现的,而 Esbuild 则选择使用 Go 语言编写,两种语言各自有其擅长的场景,但是在资源打包这种 CPU 密集场景下,Go 更具性能优势,差距有多大呢?比如计算 50 次斐波那契数列,JS 版本:

    function fibonacci(num) {
    if (num < 2) {
    return 1
    }
    return fibonacci(num - 1) + fibonacci(num - 2)
    }

    (() => {
    let cursor = 0;
    while (cursor < 50) {
    fibonacci(cursor++)
    }
    })()

    Go 版本:

    package main

    func fibonacci(num int) int{
    if num<2{
    return 1
    }

    return fibonacci(num-1) + fibonacci(num-2)
    }

    func main(){
    for i := 0; i<50; i++{
    fibonacci(i)
    }
    }

    JavaScript 版本执行耗时大约为 332.58s,Go 版本执行耗时大约为 147.08s,两者相差约 1.25 倍,这个简单实验并不能精确定量两种语言的性能差别,但感官上还是能明显感知 Go 语言在 CPU 密集场景下会有更好的性能表现。

    归根到底,虽然现代 JS 引擎与10年前相比有巨大的提升,但 JavaScript 本质上依然是一门解释型语言,JavaScript 程序每次执行都需要先由解释器一边将源码翻译成机器语言,一边调度执行;而 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只需要直接执行这些机器码即可。也就意味着,Go 语言编写的程序比 JavaScript 少了一个动态解释的过程。

    这种语言层面的差异在打包场景下特别突出,说的夸张一点,JavaScript 运行时还在解释代码的时候,Esbuild 已经在解析用户代码;JavaScript 运行时解释完代码刚准备启动的时候,Esbuild 可能已经打包完毕,退出进程了!

    所以在编译运行层面,Go 前置了源码编译过程,相对 JavaScript 边解释边运行的方式有更高的执行性能。

    多线程优势

    Go 天生具有多线程运行能力,而 JavaScript 本质上是一门单线程语言,直到引入 WebWorker 规范之后才有可能在浏览器、Node 中实现多线程操作。

    我曾经研读过 Rollup、Webpack 的代码,就我熟知的范围内两者均未使用 WebWorker 提供的多线程能力。反观 Esbuild,它最核心的卖点就是性能,它的实现算法经过非常精心的设计,尽可能饱和地使用各个 CPU 核,特别是打包过程的解析、代码生成阶段已经实现完全并行处理。

    除了 CPU 指令运行层面的并行外,Go 语言多个线程之间还能共享相同的内存空间,而 JavaScript 的每个线程都有自己独有的内存堆。这意味着 Go 中多个处理单元,例如解析资源 A 的线程,可以直接读取资源 B 线程的运行结果,而在 JavaScript 中相同的操作需要调用通讯接口 woker.postMessage 在线程间复制数据。

    所以在运行时层面,Go 拥有天然的多线程能力,更高效的内存使用率,也就意味着更高的运行性能。

    节制

    对,没错,节制!

    Esbuild 并不是另一个 Webpack,它仅仅提供了构建一个现代 Web 应用所需的最小功能集合,未来也不会大规模加入我们业已熟悉的各类构建特性。最新版本 Esbuild 的主要功能特性有:

    • 支持 js、ts、jsx、css、json、文本、图片等资源
    • 增量更新
    • Sourcemap
    • 开发服务器支持
    • 代码压缩
    • Code split
    • Tree shaking
    • 插件支持

    可以看到,这份列表中支持的资源类型、工程化特性非常少,甚至并不足以支撑一个大型项目的开发需求。在这之外,官网明确声明未来没有计划支持如下特性:

    • ElmSvelteVueAngular 等代码文件格式
    • Ts 类型检查
    • AST 相关操作 API
    • Hot Module Replace
    • Module Federation

    而且,Esbuild 所设计的插件系统也无意覆盖以上这些场景,这就意味着第三方开发者无法通过插件这种无侵入的方式实现上述功能,emmm,可以预见未来可能会出现很多魔改版本。

    Esbuild 只解决一部分问题,所以它的架构复杂度相对较小,相对地编码复杂度也会小很多,相对于 Webpack、Rollup 等大一统的工具,也自然更容易把性能做到极致。节制的功能设计还能带来另外一个好处:完全为性能定制的各种附加工具。

    定制

    回顾一下,在 Webpack、Rollup 这类工具中,我们不得不使用很多额外的第三方插件来解决各种工程需求,比如:

    • 使用 babel 实现 ES 版本转译
    • 使用 eslint 实现代码检查
    • 使用 TSC 实现 ts 代码转译与代码检查
    • 使用 less、stylus、sass 等 css 预处理工具

    我们已经完全习惯了这种方式,甚至觉得事情就应该是这样的,大多数人可能根本没有意识到事情可以有另一种解决方案。Esbuild 起了个头,选择完全!完全重写整套编译流程所需要用到的所有工具!这意味着它需要重写 js、ts、jsx、json 等资源文件的加载、解析、链接、代码生成逻辑。

    开发成本很高,而且可能被动陷入封闭的风险,但收益也是巨大的,它可以一路贯彻原则,以性能为最高优先级定制编译的各个阶段,比如说:

    • 重写 ts 转译工具,完全抛弃 ts 类型检查,只做代码转换
    • 大多数打包工具把词法分析、语法分析、符号声明等步骤拆解为多个高内聚低耦合的处理单元,各个模块职责分明,可读性、可维护性较高。而 Esbuild 则坚持性能第一原则,不惜采用反直觉的设计模式,将多个处理算法混合在一起降低编译过程数据流转所带来的性能损耗
    • 一致的数据结构,以及衍生出的高效缓存策略,下一节细讲

    这种深度定制一方面降低了设计成本,能够保持编译链条的架构一致性;一方面能够贯彻性能第一的原则,确保每个环节以及环节之间交互性能的最优。虽然伴随着功能、可读性、可维护性层面的的牺牲,但在编译性能方面几乎做到了极致。

    结构一致性

    上一节我们讲到 Esbuild 选择重写包括 js、ts、jsx、css 等语言在内的转译工具,所以它更能保证源代码在编译步骤之间的结构一致性,比如在 Webpack 中使用 babel-loader 处理 JavaScript 代码时,可能需要经过多次数据转换:

    • Webpack 读入源码,此时为字符串形式
    • Babel 解析源码,转换为 AST 形式
    • Babel 将源码 AST 转换为低版本 AST
    • Babel 将低版本 AST generate 为低版本源码,字符串形式
    • Webpack 解析低版本源码
    • Webpack 将多个模块打包成最终产物

    源码需要经历 string => AST => AST => string => AST => string ,在字符串与 AST 之间反复横跳。

    而 Esbuild 重写大多数转译工具之后,能够在多个编译阶段共用相似的 AST 结构,尽可能减少字符串到 AST 的结构转换,提升内存使用效率。

    总结

    单纯从编译性能的维度看,Esbuild 确实完胜世面上所有打包框架,差距甚至能在百倍之大:

    耗时性能差异速度产物大小
    esbuild0.11s1x1198.5 kloc/s0.97mb
    esbuild (1 thread)0.40s4x329.6 kloc/s0.97mb
    webpack 419.14s174x6.9 kloc/s1.26mb
    parcel 122.41s204x5.9 kloc/s1.56mb
    webpack 525.61s233x5.1 kloc/s1.26mb
    parcel 231.39s285x4.2 kloc/s0.97mb

    但这是有代价的,刨除语言层面的天然优势外,在功能层面它直接放弃对 less、stylus、sass、vue、angular 等资源的支持,放弃 MF、HMR、TS 类型检查等功能,正如作者所说:

    This will involve saying "no" to requests for adding major features to esbuild itself. I don't think esbuild should become an all-in-one solution for all frontend needs\!

    在我看来,Esbuild 当下与未来都不能替代 Webpack,它不适合直接用于生产环境,而更适合作为一种偏底层的模块打包工具,需要在它的基础上二次封装,扩展出一套既兼顾性能又有完备工程化能力的工具链,例如 SnowpackViteSvelteKitRemix Run 等。

    总的来说,Esbuild 提供了一种新的设计思路,值得学习了解,但对大多数业务场景还不适合直接投入生产使用。

    原文:https://segmentfault.com/a/1190000040243093
    收起阅读 »

    萌新贴

    安卓开发同学,初来匝道,谢谢关照

    安卓开发同学,初来匝道,谢谢关照

    Event Loop 和 JS 引擎、渲染引擎的关系

    安卓就是这样的架构,在主线程里面完成 ui 的更新,事件的绑定,其他逻辑可以放到别的线程,然后完成以后在消息队列中放一个消息,主线程不断循环的取消息来执行。 electron ui 架构 开发过 electron 应用的同学会知道,electron 中分为了...
    继续阅读 »


    安卓就是这样的架构,在主线程里面完成 ui 的更新,事件的绑定,其他逻辑可以放到别的线程,然后完成以后在消息队列中放一个消息,主线程不断循环的取消息来执行。



    electron ui 架构


    开发过 electron 应用的同学会知道,electron 中分为了主进程和渲染进程,window 相关的操作只能在主线程,由渲染进程向主进程发消息。


    image.png


    从上面两个案例我们可以总结出,所有的 ui 系统的设计,如果使用了多线程(进程)的架构,基本都是 ui 只能在一个线程(进程)中操作,由别的线程(进程)来发消息到这边来更新,如果多个线程,会有一个消息队列和 looper。消息队列的生产者是各个子线程(进程),消费者是主线程(进程)。


    而且,不只是 ui 架构是这样,后端也大量运用了消息队列的概念,


    后端的消息队列



    后端因为不同服务负载能力不一样,所以中间会加一个消息队列来异步处理消息,和前端客户端的 ui 架构不同的是,后端的消息队列中间件会有多个消费者、多个队列,而 ui 系统的消息队列只有一个队列,一个消费者(主线程、主进程)


    在一个线程做 ui 操作,其他线程做逻辑计算的架构很普遍,会需要一个消息队列来做异步消息处理。 网页中后来有了 web worker,也是这种架构的实现,但是最开始并不是这样的。


    单线程


    因为 javascript 最开始只是被设计用来做表单处理,那么就不会有特别大的计算量,就没有采用多线程架构,而是在一个线程内进行 dom 操作和逻辑计算,渲染和 JS 执行相互阻塞。(后来加了 web worker,但不是主流)


    我们知道,JS 引擎只知道执行 JS,渲染引擎只知道渲染,它们两个并不知道彼此,该怎么配合呢?


    答案就是 event loop。


    宿主环境


    JS 引擎并不提供 event loop(可能很多同学以为 event loop 是 JS 引擎提供的,其实不是),它是宿主环境为了集合渲染和 JS 执行,也为了处理 JS 执行时的高优先级任务而设计的机制。


    宿主环境有浏览器、node、跨端引擎等,不同的宿主环境有一些区别:


    注入的全局 api 不同


    • node 会注入一些全局的 require api,同时提供 fs、os 等内置模块

    • 浏览器会注入 w3c 标准的 api

    • 跨端引擎会注入设备的 api,同时会注入一套操作 ui 的 api(可能是对标 w3c 的 api 也可能不是)


    event loop 的实现不同

    上文说过,event loop 是宿主环境提供了,不同的宿主环境有不同的需要调度的任务,所以也会有不同的设计:



    • 浏览器里面主要是调度渲染和 JS 执行,还有 worker

    • node 里面主要是调度各种 io

    • 跨端引擎也是调度渲染和 JS 执行


    这里我们只关心浏览器里面的 event loop。


    浏览器的 event loop


    check

    浏览器里面执行一个 JS 任务就是一个 event loop,每个 loop 结束会检查下是否需要渲染,是否需要处理 worker 的消息,通过这种每次 loop 结束都 check 的方式来综合渲染、JS 执行、worker 等,让它们都能在一个线程内得到执行(渲染其实是在别的线程,但是会和 JS 线程相互阻塞)。



    这样就解决了渲染、JS 执行、worker 这三者的调度问题。


    但是这样有没有问题?


    我们会在任务队列中不断的放新的任务,这样如果有更高优的任务是不是要等所有任务都执行完才能被执行。如果是“急事”呢?


    所以这样还不行,要给 event loop 加上“急事”处理的快速通道,这就是微任务 micro tasks。


    micro tasks


    任务还是每次取一个执行,执行完检查下要不要渲染,处理下 worker 消息,但是也给高优先级的“急事”加入了插队机制,会在执行完任务之后,把所有的急事(micro task)全部处理完。


    这样,event loop 貌似就挺完美的了,每次都会检查是否要渲染,也能更快的处理 JS 的“急事”。


    requestAnimationFrame


    JS 执行完,开始渲染之前会有一个生命周期,就是 requestAnimationFrame,在这里面做一些计算最合适了,能保证一定是在渲染之前做的计算。


    image.png


    如果有人问 requestAnimationFrame 是宏任务还是微任务,就可以告诉他:requestAnimationFrame 是每次 loop 结束发现需要渲染,在渲染之前执行的一个回调函数,不是宏微任务。


    event loop 的问题


    上文聊过,虽然后面加入了 worker,但是主流的方式还是 JS 计算和渲染相互阻塞,这样就导致了一个问题:


    每一帧的计算和渲染是有固定频率的,如果 JS 执行时间过长,超过了一帧的刷新时间,那么就会导致渲染延迟,甚至掉帧(因为上一帧的数据还没渲染到界面就被覆盖成新的数据了),给用户的感受就是“界面卡了”。


    什么情况会导致帧刷新拖延甚至帧数据被覆盖(丢帧)呢?每个 loop 在 check 渲染之前的每一个阶段都有可能,也就是 task、microtask、requestAnimationFrame、requestIdleCallback 都有可能导致阻塞了 check,这样等到了 check 的时候发现要渲染了,再去渲染的时候就晚了。


    所以主线程 JS 代码不要做太多的计算(不像安卓会很自然的起一个线程来做),要做拆分,这也是为啥 ui 框架要做计算的 fiber 化,就是因为处理交互的时候,不能让计算阻塞了渲染,要递归改循环,通过链表来做计算的暂停恢复。


    除了 JS 代码本身要注意之外,如果浏览器能够提供 API 就是在每帧间隔来执行,那样岂不是就不会阻塞了,所以后来有了 requestIdeCallback。


    requestIdleCallback


    requestIdleCallback 会在每次 check 结束发现距离下一帧的刷新还有时间,就执行一下这个。如果时间不够,就下一帧再说。


    如果每一帧都没时间呢,那也不行,所以提供了 timeout 的参数可以指定最长的等待时间,如果一直没时间执行这个逻辑,那么就算拖延了帧渲染也要执行。



    这个 api 对于前端框架来说太需要了,框架就是希望计算不阻塞渲染,也就是在每一帧的间隔时间(idle时间)做计算,但是这个 api 毕竟是最近加的,有兼容问题,所以 react 自己实现了类似 idle callback 的fiber 机制,在执行逻辑之前判断一下离下一帧刷新还有多久,来判断是否执行逻辑。


    总结


    总之,浏览器里有 JS 引擎做 JS 代码的执行,利用注入的浏览器 API 完成功能,有渲染引擎做页面渲染,两者都比较纯粹,需要一个调度的方式,就是 event loop。


    event loop 实现了 task 和 急事处理机制 microtask,而且每次 loop 结束会 check 是否要渲染,渲染之前会有 requestAnimationFrames 生命周期。


    帧刷新不能被拖延否则会卡顿甚至掉帧,所以就需要 JS 代码里面不要做过多计算,于是有了 requestIdleCallback 的 api,希望在每次 check 完发现还有时间就执行,没时间就不执行(这个deadline的时间也作为参数让 js 代码自己判断),为了避免一直没时间,还提供了 timeout 参数强制执行。


    防止计算时间过长导致渲染掉帧是 ui 框架一直关注的问题,就是怎么不阻塞渲染,让逻辑能够拆成帧间隔时间内能够执行完的小块。浏览器提供了 idelcallback 的 api,很多 ui 框架也通过递归改循环然后记录状态等方式实现了计算量的拆分,目的只有一个:loop 内的逻辑执行不能阻塞 check,也就是不能阻塞渲染引擎做帧刷新。所以不管是 JS 代码宏微任务、 requestAnimationCallback、requestIdleCallback 都不能计算时间太长。这个问题是前端开发的持续性阵痛。


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

    收起阅读 »

    浏览器原理 之 页面渲染的原理和性能优化篇

    001 浏览器的底层渲染页面篇 浏览器中的5个进程 浏览器在获取服务器的资源后将 html 解析成 DOM 树,CSS 计算成 CSSOM 树,将两者合成 render tree。具体如下浏览器根据 render tree 布局生成一个页面。需要理解的...
    继续阅读 »

    001 浏览器的底层渲染页面篇



    浏览器中的5个进程



    浏览器进程.jpg



    浏览器在获取服务器的资源后将 html 解析成 DOM 树,CSS 计算成 CSSOM 树,将两者合成 render tree。具体如下浏览器根据 render tree 布局生成一个页面。需要理解的是浏览器从服务器获取回来的资源是一个个的字节码3C 6F 6E 62 ....等,浏览器会按照一套规范W3C将字节码最后解析一个个的代码字符串才成为我们看到的代码



    浏览器加载资源的机制



    • 浏览器会开辟一个 GUI 渲染线程,自上而下执行代码,专门用于渲染渲染页面的线程。


    遇到 CSS 资源



    • 遇到 <style> 内联标签会交给 GUI 渲染线程解析,但是遇到 <link> 标签会异步处理,浏览器会开辟一个 HTTP 请求处理的线程,GUI 渲染线程继续往下执行

    • 如果遇到@import 时,也会开辟一个新的 HTTP 请求线程处理,由于 @import 是同步的 GUI 渲染线程会阻塞等待请求的结果。



    需要注意 chrome 中,同一个源下,最多同时开辟 6-7 和 HTTP 请求线程。



    遇到 JS 资源


    GUI渲染遇到script.jpg



    最底部的线表示 GUI 线程的过程,渲染线程遇到不同情况下的script资源,有不同的处理。




    • 遇到 <script></script> 资源,默认是同步的。 此时 GUI 渲染线程会阻塞。等待 JS 渲染线程渲染结束后,GUI 线程才会继续渲染。

    • 如果遇到 <script async></script> 那么资源是异步的 async,浏览器也会开辟一个 HTTP 请求线程加载资源,这时 GUI 渲染线程会继续向下渲染,请求的资源回来后 JS 渲染线程开始执行,GUI 线程再次被阻塞。

    • 如果遇到 <script defer></script> 和 async 类似都会开辟一个新的HTTP线程,GUI 继续渲染。和 async 不一样的地方在于,defer 请求回来的资源需要等待 GUI 同步的代码执行结束后才执行 defer 请求回来的代码。



    async 不存在资源的依赖关系先请求回来的先执行。defer 需要等待所有的资源请求回来以后,按照导入的顺序/依赖关系依次执行。



    图片或音频



    • 遇到 <img/> 异步,也会开辟一个新的 HTTP 线程请求资源。GUI 继续渲染,当 GUI 渲染结束后,才会处理请求的资源。


    需要注意的是:假设某些资源加载很慢,浏览器会忽略这些资源接着渲染后面的代码,在chrome浏览器中会先使用预加载器html-repload-scanner先扫描节点中的 src,link等先进行预加载,避免了资源加载的时间


    浏览解析资源的机制



    • 浏览器是怎样解析加载回来的资源文件的? 页面自上而下渲染时会确定一个 DOM树CSSOM树,最后 DOM树CSSOM树 会被合并成 render 树,这些所谓的树其实都是js对象,用js对象来表示节点,样式,节点和样式之间的关系。


    DOM 树



    所谓的 DOM 树是确定好节点之间的父子、兄弟关系。这是 GUI 渲染线程自上而下渲染结束后生成的,等到 CSS 资源请求回来以后会生成 CSSOM 样式树。



    DOM树.jpg


    CSSOM 树



    CSSOM(CSS Object Model), CSS 资源加载回来以后会被 GUI 渲染成 样式树



    样式树.jpg


    Render tree 渲染树



    浏览器根据 Render tree 渲染页面需要经历下面几个步骤。注意 display:node 的节点不会被渲染到 render tree 中



    renderTree.jpg



    • layout 布局,根据渲染树 计算出节点在设备中的位置和大小

    • 分层处理。按照层级定位分层处理

    • painting 绘制页面


    layout2.jpg



    上面的图形就是浏览器分成处理后的显示效果



    002 浏览器的性能优化篇



    前端浏览器的性能优化,可以从CRP: 关键渲染路径入手



    DOM Tree



    • 减少 DOM 的层级嵌套

    • 不要使用被标准标签


    CSSOM



    • 尽量不要使用 @import,会阻碍GUI渲染线程。

    • CSS 代码量少可以使用内嵌式的style标签,减少请求。

    • 减少使用link,可以减少 HTTP 的请求数量。

    • CSS 选择器链尽可能短,因为CSS选择器的渲染时从右到左的。

    • 将写入的 link 请求放入到<head></head> 内部,一开始就可以请求资源,GUI同时渲染。


    其他资源



    • <script></script> 中的同步 js 资源尽可能的放入到页面的末尾,防止阻碍GUI的渲染。如果遇到 <script async/defer></script> 的异步资源,GUI 渲染不会中断,但是JS资源请求回来以后会中断 GUI 的渲染。

    • <img /> 资源使用懒加载,懒加载:第一次加载页面时不要加载图片,因为图片也会占据 HTTP 的数量。还可以使用图片 base64,代表图片。


    003 回流和重绘篇



    layout 阶段就是页面的回流期,painting 就是重绘阶段。第一次加载页面时必有一次回流和重绘。




    • 浏览器渲染页面的流程



    浏览器会先把 HTML 解析成 DOM树 计算 DOM 结构;然后加载 CSS 解析成 CSSOM;最后将 DOM 和 CSSOM 合并生成渲染树 Render Tree,浏览器根据页面计算 layout(重排阶段);最后浏览器按照 render tree 绘制(painting,重绘阶段)页面。



    重排(DOM 回流)



    重排是指 render tree 某些 DOM 大小和位置发生了变化(页面的布局和几何信息发生了变化),浏览器重新渲染 DOM 的这个过程就是重排(DOM 回流),重排会消耗页面很大的性能,这也是虚拟 DOM 被引入的原因。



    发生重排的情况



    • 第一次页面计算 layout 的阶段

    • 添加或删除DOM节点,改变了 render tree

    • 元素的位置,元素的字体大小等也会导致 DOM 的回流

    • 节点的几何属性改变,比如width, height, border, padding,margin等被改变

    • 查找盒子属性的 offsetWidth、offsetHeight、client、scroll等,浏览器为了得到这些属性会重排操作。

    • 框架中 v-if 操作也会导致回流的发生。

    • 等等


    一道小题,问下面的代码浏览器重排了几次(chrome新版浏览器为主)?


    box.style.width = "100px";
    box.style.width = "100px";
    box.style.position = "relative";
    复制代码


    你可能会觉得是3次,但是在当代浏览器中,浏览器会为上面的样式代码开辟一个渲染队列,将所有的渲染代码放入到队列里面,最后一次更新,所以重排的次数是1次。 问下面的代码会导致几次重排



    box.style.width = "100px";
    box.style.width = "100px";
    box.offsetWidth;
    box.style.position = "relative";
    复制代码


    答案是2次,因为 offsetWidth 会导致渲染队列的刷新,才可以获取准确的 offsetWidth 值。最后 position 导致元素的位子发生改变也会触发一次回流。所以总共有2次。



    重绘



    重绘是指 页面的样式发生了改变但是 DOM 结构/布局没有发生改变。比如颜色发生了变化,浏览器就会对需要的颜色进行重新绘制。



    发生重绘的情况



    • 第一次页面 painting 绘制的阶段

    • 元素颜色的 color 发生改变


    直接合成



    如果我们更改了一个不影响布局和绘制的属性,浏览器的渲染引擎会跳过重排和重绘的阶段,直接合成




    • 比如我们使用了CSS 的 transform 属性,浏览器的可以师姐合成动画效果。


    重排一定会引发重绘,但是重绘不一定会导致重排


    重排 (DOM回流)和重绘吗?说一下区别



    思路:先讲述浏览器的渲染机制->重排和重绘的概念->怎么减少重排和重绘。。。



    区别



    重排会导致 DOM结构 发生改变,浏览器需要重新渲染布局生成页面,但是重绘不会引发 DOM 的改变只是样式上的改变,前者的会消耗很大的性能。



    如何减少重排和重绘





      1. 避免使用 table 布局,因为 table 布局计算的时间比较长耗性能;





      1. 样式集中改变,避免频繁使用 style,而是采用修改 class 的方式。





      1. 避免频繁操作 DOM,使用vue/react。





      1. 样式的分离读写。设置样式style和读取样式的offset等分离开,也可以减少回流次数。





      1. 将动画效果设计在文档流之上即 position 属性的 absolutefixed 上。使用 GPU 加速合成。




    参考


    《浏览器工作原理与实践》


    Render Tree页面渲染


    结束


    浏览器原理篇:本地存储和浏览器缓存


    Vue 原理篇:Vue高频原理详细解答


    webpack原理篇: 编写loader和plugin


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

    收起阅读 »

    这些node开源工具你值得拥有

    前言:文章的灵感来源于,社群中某大佬分享一个自己耗时数月维护的github项目 awesome-nodejs 。或许你跟我一样会有一个疑惑,github上其实已经有个同类型的awesome-nodejs库且还高达41k⭐,重新维护一个新的意义何在? 当你深入对...
    继续阅读 »

    前言:文章的灵感来源于,社群中某大佬分享一个自己耗时数月维护的github项目 awesome-nodejs 。或许你跟我一样会有一个疑惑,github上其实已经有个同类型的awesome-nodejs库且还高达41k⭐,重新维护一个新的意义何在? 当你深入对比后,本质上还是有差别的,一个是分类体系粒度更细,其次是对中文更友好的翻译维护,也包括了对国内一些优秀的开源库的收录。最后我个人认为通过自己梳理,也能更好地做复盘和总结



    image.png


    通过阅读 awesome-nodejs 库的收录,我抽取其中一些应用场景比较多的分类,通过分类涉及的应用场景跟大家分享工具


    1.Git


    1.1 应用场景1: 要实现git提交前 eslint 校验和 commit 信息的规范校验?


    可以使用以下工具:



    • husky - 现代化的本地Git钩子使操作更加轻松

    • pre-commit - 自动在您的git储存库中安装git pre-commit脚本,该脚本在pre-commit上运行您的npm test。

    • yorkie 尤大改写的yorkie,yorkie实际是fork husky,让 Git 钩子变得简单(在 vue-cli 3x 中使用)


    1.2 应用场景2: 如何通过node拉取git仓库?(可用于开发脚手架)


    可以使用以下工具:



    1.3 应用场景3: 如何在终端看git 流程图?


    可以使用以下工具:



    • gitgraph - 在 Terminal 绘制 git 流程图(支持浏览器、React)。


    1.4 其他



    2.环境


    2.1 应用场景1: 如何根据不同环境写入不同环境变量?


    可以使用以下工具:



    • cross-env - 跨平台环境脚本的设置,你可以通过一个简单的命令(设置环境变量)而不用担心设置或者使用环境变量的平台。

    • dotenv - 从 .env文件 加载用于nodejs项目的环境变量。

    • vue-cli --mode - 可以通过传递 --mode 选项参数为命令行覆写默认的模式


    3.NPM


    3.1 应用场景1: 如何切换不同npm源?


    可以使用以下工具:



    • nrm - 快速切换npm注册服务商,如npm、cnpm、nj、taobao等,也可以切换到内部的npm源

    • pnpm - 可比yarn,npm 更节省了大量与项目和依赖成比例的硬盘空间


    3.2 应用场景2: 如何读取package.json信息?


    可以使用以下工具:



    3.3 应用场景3:如何查看当前package.json依赖允许的更新的版本


    可以使用以下工具:



    image.png


    3.4 应用场景4:如何同时运行多个npm脚本



    通常我们要运行多脚本或许会是这样npm run build:css && npm run build:js ,设置会更长通过&来拼接



    可以使用以下工具:



    • npm-run-all - 命令行工具,同时运行多个npm脚本(并行或串行)


    npm-run-all提供了三个命令,分别是 npm-run-all run-s run-p,后两者是 npm-run-all 带参数的简写,分别对应串行和并行。而且还支持匹配分隔符,可以简化script配置


    或者使用



    • concurrently - 并行执行命令,类似 npm run watch-js & npm run watch-less但更优。(不过它只能并行)


    3.5 应用场景5:如何检查NPM模块未使用的依赖。


    可以使用以下工具:



    • depcheck - 检查你的NPM模块未使用的依赖。


    image.png


    3.6 其他:



    • npminstall - 使 npm install 更快更容易,cnpm默认使用

    • semver - NPM使用的JavaScript语义化版本号解析器。


    关于npm包在线查询,推荐一个利器 npm.devtool.tech


    image.png


    4.文档生成


    4.1 应用场景1:如何自动生成api文档?



    • docsify - API文档生成器。

    • jsdoc - API文档生成器,类似于JavaDoc或PHPDoc。


    5.日志工具


    5.1 应用场景1:如何实现日志分类?



    • log4js-nodey - 不同于Java log4j的日志记录库。

    • consola - 优雅的Node.js和浏览器日志记录库。

    • winston - 多传输异步日志记录库(古老)


    6.命令行工具


    6.1 应用场景1: 如何解析命令行输入?



    我们第一印象会想到的是process.argv,那么还有什么工具可以解析吗?



    可以使用以下工具:



    • minimist - 命令行参数解析引擎

    • arg - 简单的参数解析

    • nopt - Node/npm 参数解析


    6.2 应用场景2:如何让用户能与命令行进行交互?


    image.png


    可以使用以下工具:



    • Inquirer.js - 通用可交互命令行工具集合。

    • prompts - 轻量、美观、用户友好的交互式命令行提示。

    • Enquirer - 用户友好、直观且易于创建的时尚CLI提示。


    6.3 应用场景3: 如何在命令行中显示进度条?


    image.png
    可以使用以下工具:



    6.4 应用场景4: 如何在命令行执行多任务?


    image.png


    可以使用以下工具:



    • listr - 命令行任务列表。


    6.5 应用场景5: 如何给命令行“锦上添花”?


    image.png


    可以使用以下工具:



    • chalk - 命令行字符串样式美化工具。

    • ora - 优雅的命令行loading效果。

    • colors.js - 获取Node.js控制台的颜色。

    • qrcode-terminal - 命令行中显示二维码。

    • treeify - 将javascript对象漂亮地打印为树。

    • kleur - 最快的Node.js库,使用ANSI颜色格式化命令行文本。



    感兴趣的童鞋可以参考树酱的从0到1开发简易脚手架,其中有实践部分工具



    7.加解密



    一般为了项目安全性考虑,我们通常会对账号密码进行加密,一般会通过MD5、AES、SHA1、SM,那开源社区有哪些库可以方便我们使用?



    可以使用以下工具:



    • crypto-js - JavaScript加密标准库。支持算法最多

    • node-rsa - Node.js版Bcrypt。

    • node-md5 - 一个JavaScript函数,用于使用MD5对消息进行哈希处理。

    • aes-js - AES的纯JavaScript实现。

    • sm-crypto - 国密sm2, sm3, sm4的JavaScript实现。

    • sha.js - 使用纯JavaScript中的流式SHA哈希。


    8.静态网站生成 & 博客



    一键生成网站不香吗~ 基于node体系快速搭建自己的博客网站,你值得拥有,也可以作为组件库文档展示



    image.png


    可以使用以下工具:



    • hexo - 使用Node.js的快速,简单,强大的博客框架。

    • vuepress - 极简的Vue静态网站生成工具。(基于nuxt SSR)

    • netlify-cms - 基于Git的静态网站生成工具。

    • vitepress - Vite & Vue.js静态网站生成工具。


    9.数据校验工具



    数据校验,离我们最近的就是表单数据的校验,在平时使用的组件库比如element、iview等我们会看到使用了一个开源的校验工具async-validator , 那还有其他吗?



    可以使用以下工具:



    • validator.js - 字符串校验库。

    • joi - 基于JavaScript对象的对象模式描述语言和验证器。

    • async-validator - 异步校验。

    • ajv - 最快的JSON Schema验证器

    • superstruct - 用简单和可组合的方式在JavaScript和TypeScript中校验数据。


    10.解析工具


    10.1应用场景1: 如何解析markdown?


    可以使用以下工具:



    • marked - Markdown解析器和编译器,专为提高速度而设计。

    • remark - Markdown处理工具。

    • markdown-it -支持100%通用Markdown标签解析的扩展&语法插件。


    10.2应用场景2: 如何解析csv?


    可以使用以下工具:



    • PapaParse - 快速而强大的 CSV(分隔文本)解析器,可以优雅地处理大文件和格式错误的输入。

    • node-csv - 具有简单api的全功能CSV解析器,并针对大型数据集进行了测试。

    • csv-parser -旨在比其他任何人都快的流式CSV解析器。


    10.3应用场景3: 如何解析xml?


    可以使用以下工具:



    最后



    如果你喜欢这个库,也给作者huaize2020 一个star 仓库地址:awesome-nodejs



    昨天看到一段话想分享给大家


    对于一个研发测的日常:



    • 1.开始工作的第一件事,规划今日的工作内容安排 (建议有清晰的ToDolist,且按优先级排序)

    • 2.确认工作量与上下游关联风险(如依赖他人的,能否按时提供出来);有任何风险,尽早暴露

    • 3.注意时间成本、不是任何事情都是值得你用尽所有时间去做的,分清主次关系

    • 4.协作任务,明确边界责任,不要出现谁都不管,完成任务后及时同步给相关人

    • 5.及时总结经验,沉淀技术产出实现能力复用,同类型任务,不用从零开始,避免重复工作


    往期热门文章📖:



    • 链接:https://juejin.cn/post/6972124481053523999

    收起阅读 »

    NodeJS使用Koa框架开发对接QQ登陆功能

    开发准备 注册开发者账号 首先我们需要先去腾讯开发者平台认证注册成为个人开发者 输入网址:https://open.tencent.com/ 然后 点击 QQ开放平台——然后点击顶部的 应用管理会提示你登陆,使用自己的QQ账号登陆后,如果是新用户会提示你注...
    继续阅读 »

    开发准备



    • 注册开发者账号


    首先我们需要先去腾讯开发者平台认证注册成为个人开发者 输入网址:https://open.tencent.com/ 然后 点击 QQ开放平台——然后点击顶部的 应用管理会提示你登陆,使用自己的QQ账号登陆后,如果是新用户会提示你注册成为开发者,这里我已经注册并认证成功了,所以我就可以直接创建应用了,我这里是网站使用的,所以我就创建的网站Web'应用,APP小程序申请移动端的进行了 下面看我的截图
    image.png


    image.png


    image.png


    image.png


    image.png


    到这一步基本上就创建完成了一个应用,会有7天的等待,官方会审核检查你填写的信息是否准确,如果都是真实有效的用不了几天审核通过了,就申请到了appid和appkey的。



    • 接入QQ登录时,网站需要不停的和Qzone进行交互,发送请求和接受响应。



      1. 对于PC网站:请在你的服务器上ping graph.qq.com ,保证连接畅通。



    • 2.移动应用无需此步骤


    放置“QQ登录”按钮_OAuth2.0


    image.png


    这里说一下我碰到的几个坑



    1. 网站名称我没有填写我到时候域名备案写的网站名称,于是出了一次错误被驳回

    2. 网站的备案号格式:(地区)蜀ICP备XXXXX号 我填写的格式不正确又一次被驳回

    3. 就是大家可能都比较容易犯错误的,回调地址的填写,刚开始我一直卡这里,总共的填写后面我也会反复给大家强调,在这里就是Api接口地址可以这样去理解,(目前我这样理解,有更好意见的欢迎反馈评论给我) 如我的网址是:lovehaha.cn 我的api接口是 lovehaha.cn/test 那么我在后端写了一个专门处理腾讯qq返回的数据的路由,是 /qqauthor 那么我的回调地址就是: lovehaha/test/qqauthor

    4. 审核的时候,网站需要可以访问,同时需要查看QQ图标的位置是否正确,应在登陆页或首页,同时回调地址的路由可以正常收到腾讯返回的数据。


    代码部署


    前面都顺顺利利成功了后,需要到开发者平台应用管理哪里先填写个QQ调试账号然后就开始我们的代码配置部署吧!


    后端使用的是Node的Koa框架 框架的安装配置很简单(首先肯定需要大家有node环境 我这里是v14.16.1版本的,安装了Node 版本大于10还是几就自带npm了)


    命令:



    • npm install koa-generator -g (全局安装koa-generator是koa框架的生成器)

    • koa 文件名称 创建项目

    • npm install 安装依赖包

    • npm run dev 就可以运行了默认应该是3000端口访问


    在这里我就简单介绍一下,下面介绍我的后端代码处理逻辑


    整体逻辑:



    • 获取Authorization Code

    • 通过Authorization Code 获取 Access Token (Code ————> 换 Token)

    • 通过Access Token 获取 用户的Openid

    • 最后通过获取的 Token 和 Openid 获取用户的信息


    PS:(可选)权限自动续期,获取Access Token
    Access_Token的有效期默认是3个月,过期后需要用户重新授权才能获得新的Access_Token。本步骤可以实现授权自动续期,避免要求用户再次授权的操作,提升用户体验。(官网文档有教程,我这里没用)

    /**
    * QQ登陆授权判断
    * code 是前端点击QQ登陆按钮图标然后请求,然后请求这个回调地址 返回的
    * 我这里就可以取到了
    */
    router.get('/qqauthor', async (ctx, next) => {
    const { code } = ctx.request.query
    console.log("code", code) // 打印查看是否获取到
    let userinfo
    let openid
    let item
    if (code) {

    let token = await QQgetAccessToken(code) // 获取token 函数 返回 token 并存储
    console.log('返回的token',token)
    openid = await getOpenID(token) // 获取 Openid 函数 返回 Openid 并存储
    console.log('返回的openid', openid)
    if (openid && token) {
    userinfo = await QQgetUserInfO(token, openid) // 如果都获取到了,获取用户信息
    console.log("返回的结果", userinfo)
    }

    }

    // 封装:
    if (userinfo) {
    let obj = {
    nickname: userinfo.nickname,
    openid: openid,
    gender: userinfo.gender === '男' ? 1 : 2,
    province: userinfo.province,
    city: userinfo.city,
    year: userinfo.year,
    avatar: userinfo.figureurl_qq_2 ? userinfo.figureurl_qq_2 : userinfo.figureurl_qq_1
    }
    console.log('封装的obj', obj)
    item = await register({ userInfo: obj, way: 'qq' })
    /** 从这里到封装 都是改变我获取的用户信息存储到数据库里面,根据数据库的存储,创建新用户,如果有
    * 用户我就查询并获取用户的id 然后返回给前端 用户的 id
    */
    ctx.state = {
    id: item.data.id
    }
    await ctx.render('login', ctx.state) // 如果获取到用户 id 返回 前端一个页面并携带参数 用户ID
    }
    })


    /**
    *
    * @param {string} code
    * @param {string} appId 密钥
    * @param {string} appKey key
    * @param {string} state client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回
    * @param {string} redirectUrl (回调地址)
    * @returns
    */
    async function QQgetAccessToken(code) {
    let result
    let appId = '申请成功就有了'
    let appKey = '申请成功就有了'
    let state = '自定义'
    let redirectUrl = 'https://xxxxx/qqauthor' // 回调地址是一样的 我这里就是我的获取登陆接口的地址

    // 安装了 axios 请求 接口 获取返回的token
    await axios({
    url:`https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=${appId}&client_secret=${appKey}&code=${code}&state=${state}&redirect_uri=${redirectUrl}&fmt=json`,
    method:'GET'
    }).then(res =>{
    console.log(res.data)
    result = res.data.access_token
    // res.data.access_token
    }).catch(err => {
    console.log(err)
    result = err
    })

    return result
    }


    /**
    * 根据Token获取Openid
    * @param {string} accessToken token 令牌
    * @returns
    */
    async function getOpenID(accessToken) {
    let result

    // 跟上面差不多就不解释了
    await axios({
    url: `https://graph.qq.com/oauth2.0/me?access_token=${accessToken}&fmt=json`,
    method: 'GET'
    }).then(res => {
    // 获取到了OpenID
    result = res.data.openid
    }).catch(err => {
    result = err
    })

    return result
    }


    /**
    * 根据Openid 和 Token 获取用户的信息
    * @param {string} accessToken
    * @param {string} openid
    * @returns
    */
    async function QQgetUserInfO (token, openid) {
    let result
    await axios({
    url: `https://graph.qq.com/user/get_user_info?access_token=${token}&oauth_consumer_key=101907569&openid=${openid}`,
    method: 'GET'
    }).then(res => {
    result = res.data
    }).catch(err => {
    console.log(err)
    result = err
    })

    return result
    }

    前后端调试

    前端我这里使用的是Vue2.0的语法去写的上login.vue 页面代码

    <template>
    <div class="icon" @click="qqAuth">
    <img src="@/static/img/qq48-48.png" alt="" />
    <span>QQ账号登陆</span>
    </div>
    </template>

    // 这里我就直接写
    <script>
    export default {
    methods: {
    // 简单粗暴
    qqAuth () {
    const appId = 申请就有了
    const redirectUrl = 'https://xxx/qqauthor' // 回调地址 我这里路由是/qqauthor 你的是什么填什么
    const state = 'ahh' // 可自定义
    const display = '' // 可不传仅PC网站接入时使用。用于展示的样式。
    const scope = '' // 请求用户授权时向用户显示的可进行授权的列表。 可不填
    const url = `
    https://graph.qq.com/oauth2.0/authorize?
    response_type=code&
    client_id=${appId}&
    redirect_uri=${redirectUrl}
    &state=${state}
    &scope=${scope}
    `
    window.open(url, '_blank') // 开始访问请求 ,这个时候用户点击登陆,就会跳转到qq登陆界面,
    登陆后会返回code 到最开始我们写好的后端接口也就是回调地址哪里,开始操作
    },
    }
    </script>

    这个时候用户点击登陆触发qqAuth事件,就会跳转到qq登陆界面,登陆成功后会返回code到最开始我们写好的后端接口也就是回调地址哪里,我们把获取Code操作最后获取用户信息存储并返回一个登陆成功的页面携带用户的ID,这个返回的页面,我写了一个 a 标签 携带着 返回的 用户ID


    image.png


    我这里的href地址是我自己可以访问并且在线上真实的地址,跳转到了首页,我在这个页面的Mounth 写了一个事件
    页面加载的时候获取当前页面的URL如果,并且分割URL字符串,判断是否存在ID,存在ID证明是用户登陆成功返回的,获取当前用户的ID,然后再通过ID请求后端,查找到了用户的数据,缓存,完成整个QQ登陆逻辑功能
    image.png


    完成开发


    开发完成了就上线了,但肯定我的这个是存在更优的解决办法,我记录下来,供大家提供一种思路,希望大家可以喜欢,返回页面是使用的Koa的njk框架,比较方便。


    链接:https://juejin.cn/post/6977399909532041247
    收起阅读 »

    Docker 快速部署 Node express 项目

    前言 本文章讲解如何简单快速部署 node API 项目。可作为docker入门学习。 Node 项目基于 express+sequelize 框架。 数据库使用 mysql。 Docker 安装 Docker 官方下载地址:docs.docker.com/g...
    继续阅读 »

    前言


    本文章讲解如何简单快速部署 node API 项目。可作为docker入门学习。


    Node 项目基于 express+sequelize 框架。


    数据库使用 mysql。


    Docker 安装


    Docker 官方下载地址:docs.docker.com/get-docker


    检查 Docker 安装版本:$ docker --version


    Dockerfile



    Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。

    Dockerfile 学习地址:http://www.runoob.com/docker/dock…



    在项目根目录下编写 Dockerfile 文件:


    7231624506430_.pic.jpg


    FROM node:12.1    :基于 node:12.1 的定制镜像
    LABEL maintainer="kingwyh1993@163.com" :镜像作者
    COPY . /home/funnyService :制文件到容器里指定路径
    WORKDIR /home/funnyService :指定工作目录为,RUN/CMD 在工作目录运行
    ENV NODE_ENV=production :指定环境变量 NODE_ENV 为 production
    RUN npm install yarn -g :安装 yarn
    RUN yarn install :初始化项目
    EXPOSE 3000 :声明端口
    CMD [ "node", "src/app.js" ] :运行 node 项目 `$ node src/app.js`

    注:CMD 在docker run 时运行。RUN 是在 docker build。
    复制代码

    docker-compose



    Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。

    docker-compose 学习地址:http://www.runoob.com/docker/dock…



    在根目录下编写 docker-compose.yml 文件:


    7241624516284_.pic.jpg


    container_name: 'funny-app'  :指定容器名称 funny-app
    build: . :指定构建镜像上下文路径,依据 ./Dockerfile 构建镜像
    image: 'funny-node:2.0' :指定容器运行的镜像,名称设置为 funny-node:2.0
    ports: :映射端口的标签,格式为 '宿主机端口:容器端口'
    - '3000:3000' :这里 node 项目监听3000端口,映射到宿主机3000端口

    复制代码

    本地调试


    项目根目录下执行 $ docker-compose up -d


    查看构建的镜像 $ docker images 检查有上述 node、funny-node 镜像则构建成功


    查看运行的容器 $ docker ps 检查有 funny-app 容器则启动成功


    调试接口 http://127.0.0.1:3000/test/demo 成功:


    image.png


    服务器部署运行


    在服务器 git pull 该项目


    执行 $ docker-compose up -d


    使用 $ docker images $ docker ps 检查是否构建和启动成功


    调试接口 http://服务器ip:3000/test/demo



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

    收起阅读 »

    [react-native]JSX和RN样式以及和web的不同之处

    全屏状态栏 import { View, Text, Image, StatusBar } from 'react-native' <StatusBar backgroundColor="transparent" translucent={ true }...
    继续阅读 »

    全屏状态栏


    import { View, Text, Image, StatusBar } from 'react-native'
    <StatusBar backgroundColor="transparent" translucent={ true } />


    JSX:React中写组件的代码格式, 全称是JavaScript xml


    import React from 'react'
    import { View, Text } from 'react-native'

    const App = () => <View>
    <Text>JSX Hello World</Text>
    </View>

    export default App


    RN样式(主要讲解和web开发的不同之处)


    image.png


    #屏幕宽度和高度
    import { Dimensions } from 'react-native'
    const screenWidth = Math.round(Dimensions.set('window').width)
    const screenHeight = Math.round(Dimensions.get('window').height)

    #变换
    <Text style={{ transform: [{translateY: 300}, {scale: 2}] }}>变换</Text>


    标签



    1. View

    2. Text

    3. TouchableOpacity

    4. Image

    5. ImageBackground

    6. TextInput

    7. 其他 =>

      1. button

      2. FlatList

      3. ScrollView

      4. StatusBar

      5. TextInput




    View



    1. 相当于以前web中的div

    2. 不支持设置字体大小, 字体颜色等

    3. 不能直接放文本内容

    4. 不支持直接绑定点击事件(一般使用TouchableOpactiy 来代替)


    Text



    1. 文本标签,可以设置字体颜色、大小等

    2. 支持绑定点击事件


    TouchableOpacity (onpress => 按下事件 onclick=> 点击事件)


    可以绑定点击事件的块级标签



    1. 相当于块级的容器

    2. 支持绑定点击事件 onPress

    3. 可以设置点击时的透明度


    import React from 'react'
    import {TouchableOpacity, Text} from 'react-native'

    const handlePress = () => {
    alert('111')
    }

    const App = () =>
    <TouchableOpacity activeOpacity={0} onPress={ handlePress }>
    <Text>点击事件</Text>
    </TouchableOpacity>

    export default App


    Image图片渲染


    1.渲染本地图片时


    <Image source={ require("../gril.png") } />


    2.渲染网络图片时, 必须加入宽度和高度


    <Image source={{ uri: 'https://timgsa.baidu.com/xxx.png }} style={{ width: 200, height: 300 }} />


    3.在android上支持GIF和WebP格式图片


    默认情况下Android是不支持gif和webp格式的, 只需要在 android/app/build.gradle 文件中根据需要手动添加


    以下模块:


    dependencies {
    // 如果你需要支持android4.0(api level 14)之前的版本
    implementation 'com.facebook.fresco:animated-base-support:1.3.0'

    // 如果你需要支持GIF动画
    implementation 'com.facebook.fresco:animated-gif:2.0.0'

    // 如果你需要支持webp格式,包括webp动图
    implementation 'com.facebook.fresco:animated-webp:2.1.0'
    implementation 'com.facebook.fresco:webpsupport:2.0.0'

    // 如果只需要支持webp格式而不需要动图
    implementation 'com.facebook.fresco:websupport:2.0.0'
    }


    ImageBackground


    一个可以使用图片当做背景的容器,相当于以前的 div + 背景图片


    import React from 'react'
    import { Text, ImageBackground } from 'react-native'

    const App = () =>
    <ImageBackground source={require('./assets/logo.png')} style={{ width: 200, height: 200 }}>
    <Text>Inside</Text>
    </ImageBackground>

    export default App


    TextInput输入框组件


    可以通过 onChangeText 事件来获取输入框的值
    语法:



    1. 组件

    2. 插值表达式

    3. 状态state

    4. 属性props

    5. 调试

    6. 事件

    7. 生命周期


    import React from 'react'
    import { TextInput } from 'react-native'

    const handleChangeText = (text) => {
    alert(text)
    }

    #onChangeText => 获取输入的值
    const App = () => <TextInput onChangeText={ handleChangeText } />

    export default App


    花括号{}里面可以直接添加JS代码的


    组件: 函数组件, 类组件


    函数组件



    1. 没有state(通过hooks可以有)

    2. 没有生命周期(通过hooks可以有)

    3. 适合简单的场景


    类组件



    1. 适合复杂的场景

    2. 有state

    3. 有生命周期


    属性props (父子组件的传递)和插槽slot


    import React from 'react'
    import { View, Text } from 'react-native'

    const App = () => (
    <View>
    <Text>==========</Text>
    <Sub color="red">
    <View><Text>1234</Text></View>
    </Sub>
    <Text>==========</Text>
    </View>
    )

    // 子组件 props
    const Sub = (props) =>
    (<View><Text style={{ color: props.color }}>{ props.children }</Text></View>)

    // 插槽类似于 vue中的slot
    export default App



    人懒,不想配图,都是自己的博客内容(干货),望能帮到大家




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

    收起阅读 »