0%

机器编程

处理器和架构集的历史

Inter x86处理器

  • 精简指令集计算机RISC (Reduced instruction set computer)

  • 复杂指令集计算机 CISC(Complex instruction set computer)

  • RISC出现前,称以前的指令集为CISC(是一种嘲讽)

    截屏2021-06-03 上午10.01.42

C, 汇编,机器码

截屏2021-06-03 上午10.29.32

  • c源文件(.c) – 编译器(Compiler)–> 汇编文件(.s) –汇编器 (Assembler)–>目标文件(.o)–链接器(Linker) + 静态库(Static libraries)–> 可执行文件

将c源文件转化成汇编

1
gcc -Og -S 指针.cpp

机器指令示例:

  • C语言
1
*dest = t;
  • 汇编
1
movq %rax, (%rbx)

t: %rax

dest: %rbx

*dest: M[%rbx]

  • 指令编码
1
0x40059e: 48 89 03

3byte指令

指令存储在0x40059e处

2.9 段寄存器

段地址存储在段寄存器

8086CPU有4个段寄存器

CS、DS、SS、ES

当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址

2.10 CS和IP

CS和IP是8086CPU中最关键的寄存器,他们指示当前要读取指令的地址。

CS:代码段寄存器

IP:指令指针寄存器

  1. 从CS:IP指向内存单元读取指令,读取的指令进入指令缓存器;
  2. IP = IP + 所读取指令的长度,从而指向下一条指令;
  3. 执行指令。转到步骤1,重复这个过程。

8086PC工作过程的简要描述

  • 在8086CPU加电或复位后(CPU刚开始工作时)CP和IP被设置为CS=FFFFH,IP=0000H。
  • 即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行。
  • FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
  • 在任何时候,CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,到内存中读取指令码,再执行。
  • 如果说,内存中的一段信息曾被CPU执行过,那么它所在的内存单元必然被CS:IP指向过。

2.11 修改CS、IP的指令

  • 在CPU中,程序员能够用指令读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制。
  • CPU从何处执行指令是由CS、IP中的内容决定的,程序员可以通过改变CS、IP中的内容来控制CPU执行目标指令。

如何修改AX中的值?

mov指令可以改变8086CPU大部分寄存器的值:传送指令

  • mov指令不能用于设置CS、IP的值,8086CPU没有提供这样的功能
  • jmp段地址:偏移地址

    同时修改CS、IP的内容

    功能:用指令中给出的段地址修改CS,偏移地址修改IP。

    jmp 2AE3:3 >> 2AE33H

    jmp 3:0B16 >> 0B46H

  • 仅修改IP的内容

    jmp某一合法寄存器

    jmp ax (类似于mov IP, ax)

    功能:用寄存器中的值修改IP

问题分析:CPU运行的流程

(初始:CS=2000H,IP=0000H)

截屏2021-06-02 下午11.08.40

  1. mov ax, 6622H ;ax=6622H
  2. jmp 1000:3 ;CS=1000,IP=3
  3. mov ax,0000 ;ax=0000H
  4. mov bx,ax ;bx=0000H
  5. jum bx ;IP=0000H
  6. mov ax,0123H ;ax=0123H
  7. 转到第三步执行(死循环 )

2.12 代码段

对于8086PC机,可以将长度为N(N<=64kb)的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元,这段内存是用来存放代码的,从而定义了一个代码段。

例如:

这段长度为10字节的字节的指令,存在从123B0H123B9H的一组内存单元中,我们就可以认为,123B0H123B9H这段内存单元是用来存放代码的,是一个代码段,它的段地址为123BH,长度为10字节。

截屏2021-06-02 下午11.24.18

检测点2.3:

下面的3条指令执行后,CPU几次修改IP?都是什么时候?最后IP中的值是多少?

mov ax,bx

sub ax, ax

jmp ax

4次(第三次:读取jmp ax之后;第四次:执行jmp ax修改IP), ax=0000H,IP=ax;

指向指针的指针

1
2
3
int ival = 1024;
int *pi = &ival; // pi points to an int
int **ppi = &pi; // ppi points to a pointer to an int

截屏2021-06-02 上午10.14.16

指向指针的引用

1
2
3
4
5
int i = 42;
int *p;//p isa pointer to int
int *&r = p;// r is a reference to the pointer p
r = &i; // r refers to a pointer; assigning &i to r makes p point to i
*r = 0; // dereferencing r yields i, the object to which p points; changes i to 0
  • 如何理解int *&r = p;

    从右往左看,最靠近r的是&,说明r是一个引用,然后是*,说明r指向指针,最后是int,说明r是一个指向int类型指针的引用。

Exercises Section 2.3.3
Exercise 2.25:
Determine the types and values of each of the following variables.

(a) int* ip, &r = ip;

(b) int i, *ip = 0;

(c) int* ip, ip2;

(a) ip:int类型的指针,r:指向int类型指针的引用

(b) i:int类型的变量,ip:int类型的指针

(c)ip:int类型的指针,ip2:int类型的变量

常量限定符(const)

  • const修饰的对象不可修改
1
2
const int bufSize = 512; // input buffer size
bufSize = 512; // error: attempt to write to const object
  • const修饰的对象必须初始化
1
2
3
const int i = get_size(); // ok: initialized at run time
const int j = 42; // ok: initialized at compile time
const int k; // error: k is uninitialized const
  • 在多个文件中使用const修饰的对象
1
2
3
4
// file_1.cc defines and initializes a const that is accessible to other files 
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize; // same bufSize as defined in file_1.cc

Exercises Section 2.4
Exercise 2.26:
Which of the following are legal? For those that are illegal,

explain why.

(a) const int buf;

(b) int cnt = 0;

(c) const int sz = cnt;

(d) ++cnt; ++sz;

(a):❌ (d):++cnt;(✅)++sz;(❌)

常量引用

1
2
3
4
const int ci = 1024; 
const int &r1 = ci; // ok: both reference and underlying object are const
r1 = 42; // error: r1 is a reference to const
int &r2 = ci; // error: non const reference to a const object
  • 常量引用不可修改值
  • 非常量引用不可指向常量对象
1
2
3
4
int i = 42;
const int &r1 = i; // we can bind a const int& to a plain int object const int &r2 = 42; // ok: r1 is a reference to const
const int &r3 = r1 * 2; // ok: r3 is a reference to const
int &r4 = r * 2; // error: r4 is a plain, non const reference

常量引用可以指向 非常量对象字面量通用表达式

1
2
double dval = 3.14; 
const int &ri = dval;
  • 常量引用会自动强制类型转化
1
2
const int temp = dval; // create a temporary const int from the double 
const int &ri = temp; // bind ri to that temporary

常量指针(pointer to const)

指向常量的指针,指针地址可以修改,但地址存放的值不可以修改

1
2
3
4
const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = &pi; // error: ptr is a plain pointer
const double *cptr = &pi; // ok: cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr
  • 指向常量的指针地址存放的值不可修改
1
2
double dval = 3.14; // dval is a double; its value can be changed
cptr = &dval; // ok: but can't change dval through cptr
  • 指向常量的指针可以指向非常量对象

指针常量(const Pointers)

1
2
3
4
int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb const
double pi = 3.14159;
const double *const pip = &pi; // pip is a const pointer to a const object

从右往左看:curErrconst修饰,不可修改

1
2
3
4
*pip = 2.72; // error: pip is a pointer to const
// if the object to which curErr points (i.e., errNumb) is nonzero
if (*curErr) {
*curErr = 0; // ok: reset the value of the object to which curErr is bound }

Exercises Section 2.4.2
Exercise 2.27:
Which of the following initializations are legal? Explain why.

(a) int i = -1, &r = 0;
(b) int const p2 = &i2;
(c) const int i = -1, &r = 0;
(d) const int *const p3 = &i2;
*
(e)** const int *p1 = &i2;
(f) const int &const r2;
(g) const int i2 = i, &r = i;

(a)❌ 引用必须指向一个对象

(f) ❌常量必须初始化

Exercise 2.28: Explain the following definitions. Identify any that are illegal.

(a) int i, *const cp;
(b) int *p1, *const p2;

(c) const int ic, &r = ic;

(d) const int *const p3;

(e) const int *p;

原因:常量初始化没有赋值

常量表达式

常量表达式是指能够在编译时确定值

1
2
3
4
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression

尽管sz是一个常量,但是它的值只有在运行时才能确定,因此sz不是一个常量表达式

constexpr

1
2
3
constexpr int mf = 20; // 20 is a constant expression 
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function

constexpr避免了出现上述sz的情况

1
2
const int *p = nullptr; // p is a pointer to a const int 
constexpr int *q = nullptr; // q is a const pointer to int

constexpr是top-level const,即指针本身是常量(low-level const是指针指向常量对象)

1
2
3
4
5
6
constexpr int *np = nullptr; // np is a constant pointer to int that is null
int j = 0;
constexpr int i = 42; // type of i is const int
// i and j must be defined outside any function
constexpr const int *p = &i; // p is a constant pointer to the const int i
constexpr int *p1 = &j; // p1 is a constant pointer to the int j

2.4 物理地址

CPU访问内存单元时要给出内存单元的地址。我们将这个唯一的地址称为物理地址。

2.5 16位结构的CPU

16位结构描述了一个CPU具有以下几个方面特征:

  • 运算器一次最多可以处理16位的数据
  • 寄存器的最大宽度为16位
  • 寄存器和运算器之间的通路是16位的

2.6 8086CPU给出物理地址的方法

8086有20位地址总线,可传送20位地址,寻址能力为1M

8086内部为16位结构,只能传送16位的地址,寻址能力却只有64k。

8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理空间

  1. cpu中的相关部件提供两个16位的地址,一个称为断地址,一个称为偏移地址;
  2. 段地址和偏移地址通过内部总线送入一个称为 地址加法器 的部件
  3. 地址加法器将两个16位地址合并成一个20位的地址

截屏2021-06-02 上午12.02.00

地址加法器的工作原理

截屏2021-06-02 上午12.06.06

段地址 * 16,即数据左移4位

  • 一个数据的十六进制形式左移1位,相当于乘以16;
  • 一个数据的十进制形式左移1位,相当于乘以10;
  • 一个数据的X进制形式左移1位,相当于乘以X;

2.7 段地址 ✖️16 + 偏移地址 = 物理地址的本质含义

  • 第一个比喻:

    截屏2021-06-02 上午12.09.59

    • 从学校走2826m到图书馆。这2826可以认为是图书馆的物理地址。
    • 可以分解为从学校走2000m到体育馆,从体育馆再走826m到图书馆
      • 第一个距离2000m是相当于起点的基础地址
      • 第二个距离826m是相对于基础地址的偏移地址
  • 第二个比喻:

    用纸条通讯,询问图书馆的地址(目前只有两张可以容纳3位数据的纸条)

    • 第一张:200

    • 第二张:826

      规则:第一张 ✖️10 + 第二张,即可得到地址。

2.8 段的概念

错误认识:

内存被划分成了一个一个的段,每一个段有一个段地址

其实:

内存并没有分段,段的划分来自于CPU,由于8086CPU用“段地址 ✖️ 16 + 偏移地址 = 物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。

  • 段地址 ✖️ 16必然是16的倍数,所以一个段的起始地址也一定是16的倍数;
  • 偏移地址为16,16位的地址的寻址能力为64k,所以一个段的长度最大为64k。

如果给定一个段地址,仅通过变化偏移地址来进行寻址,最多可以定位多少内存单元?

结论:偏移地址16位,变化范围为0~FFFFH,仅用偏移地址来寻址最多可寻64k个内存单元。

比如:给定段地址1000H,用偏移地址寻址,CPU的寻址范围为:10000H ~ 1FFFFH。

数据在21F60H内存中。对于8086PC机的两种描述:

  1. 数据存在内存2000:1F60单元中;
  2. 数据存在内存的2000段中的1F60H单元中。

注:段是可以人为定义的,可根据需要,将地址连续、起始地址为16的倍数的一组内存单元定义为一个段。

标准输入输出

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
std::cout << "Enter two numbers:" << std::endl;
int v1 = 0, v2 = 0;
std::cin >> v1 >> v2;
std::cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << std::endl;
return 0;
}
  • cin(see-in):标准输入

    1
    2
    3
    // 输入v1的值
    int v1 = 0;
    std::cin >> v1;
  • cout(see-out):标准输出

    1
    2
    // 输出字符串"Enter two numbers:"
    std::cout << "Enter two numbers:" << std::endl;

    命名空间(namespace)

命名空间的出现是为了避免如下的问题:

我们声明了同名函数dance,那么如何调用指定的某个函数呢?

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

namespace Somebody {
void dance() {
std::cout<<"Somebody dances"<<std::endl;
}
}

void dance() {
std::cout<<"I dance"<<std::endl;
}

使用命名空间,指定调用的函数:

1
2
3
4
5
int main() {
dance(); // 输出:I dance
Somebody::dance(); // 输出: Somebody dances
return 0;
}

引用(References)

引用就是一个对象的别名

A reference is not an object. Instead, a reference is just another name for an already existing object.

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
int main() {
int ival = 1024;
int &refVal = ival;
refVal = 512;
cout << ival << endl; // 512
int &refVal2; // error: references must be initialized.
return 0;
}
  • 引用必须初始化
  • 一旦初始化,不可以再将引用指向其他对象
1
2
3
int &refVal4 = 10; // error: initializer must be an object 
double dval = 3.14;
int &refVal5 = dval; // error: initializer must be an int object

Exercises Section 2.3.1

Exercise 2.15: Which of the following definitions, if any, are invalid? Why?

(a) int ival = 1.01;

(b) int &rval1 = 1.01;

(c) int &rval2 = ival;

(d) int &rval3;

(a) ✅(b)❌ 初始化赋值必须是int对象

(c) ✅ (d)❌ 引用必须被初始化

指针(Pointers)

1
2
int *ip1, *ip2; // both ip1 and ip2 are pointers to int 
double dp, *dp2; // dp2 is a pointer to double; dp is a double
  • 与引用不同,指针本身就是一个对象
  • 在指针的生命周期中,可以指向不同的对象
1
2
int ival = 42;
int *p = &ival; // p holds the address of ival; p is a pointer to ival
  • 指针存放的值是另一个对象的地址
1
2
3
4
5
double dval;
double *pd = &dval; // ok: initializer is the address of a double
double *pd2 = pd; // ok: initializer is a pointer to double
int *pi = pd; // error: types of pi and pd differ
pi = &dval; // error: assigning the address of a double to a pointer to int
  • 指针类型必须和其指向的地址中存放的对象的类型相同

注:& 和 * 有多种含义

&、*可以当做声明使用,比如:

1
2
3
4
int i = 42;
int&r=i; //& *followsatypeandispartofadeclaration; r is a *reference*

int *p; // * *follows a type and is part of a declaration; p is a *pointer*

&也可以作取地址符使用:

1
p = &i; // & is used in an expression as the address-of operator.

*可以作取值符号使用:

1
2
*p = i; // * is used in an expression as the dereference operator
int &r2 = *p; // & is part of the declaration; * is the dereference operator
  1. 使用指针访问一个对象

    1
    2
    3
    int ival = 42;
    int *p = &ival; // p holds the address of ival; p is a pointer to ival
    cout << *p; // * yields the object to which p points; prints 42
  2. 使用指针指向空指针(Null Pointers)

    1
    2
    3
    4
    int *p1 = nullptr; // equivalent to int *p1 = 0;
    int *p2 = 0; // directly initializes p2 from the literal constant 0
    // must #include cstdlib
    int *p3 = NULL; // equivalent to int *p3 = 0;

void指针(void* Pointers)

void指针能够指向任意类型的对象

1
2
3
4
double obj = 3.14, *pd = &obj;
// ok: void* can hold the address value of any data pointer type
void *pv = &obj; // obj can be an object of any type
pv = pd; // pv can hold a pointer to any type

Exercise 2.20: What does the following program do?

int i = 42;
int *p1 = &i;

*p1 = *p1 * *p1;

*p1 = 42 * 42

前言

笔者学习AppleScript的起因,是因为本人键盘修饰键的使用习惯和其他同事不同,笔者习惯于将「大小写修饰键」和「Ctrl键」互调位置,以至于习惯默认修饰键配置的同事在我的电脑上进行调试,总是忍不住感叹一声:
“这TM是人用的吗?”
相信了解iOS开发的小伙伴们都知道,Ctrl + Command + 上下,可以切换.h和.m文件,但是在笔者的键盘上,执行这一操作时,不但没有任何的效果,还会把键盘设置成了大写模式,在一次又一次,同事怒吼的咆哮后,笔者痛定思通,决心要解决这个问题,在如何不改变自己键盘使用习惯的前提下,让来调试代码的同事们也感到如使用自己键盘般的流畅和丝滑呢?

于是,就有就有了这篇聊一聊脚本语言:AppleScript

AppleScript的实践应用

实践:还原修饰键默认设置

本文下面比较详细地介绍了AppleScript基础知识,在学习它们之前,只需要简单了解AppleScript中常用的几个概念,就可以实现GUI Scripting的神奇功效,我们先来看看AppleScript中的常用概念

AppleScript的常用概念

这是一个神奇的传送门☞脚本编辑器(本文所有代码均可直接在IDE中执行)

我们先了解下我们的IDE工具,打开脚本编辑器

1.png

点击图片中的按钮,可以看到上面除了「结果」选项外,还多出了「信息」、「事件」和「回复」三个选项,在这里面可以看到更为详细的打印信息!

2.png

AppleScript的必须掌握的命令:

  • tell someone do something

AppleScript的语法十分接近自然语言,想要操作一个对象执行某些操作,只需要使用”tell命令 +对象类型 + 对象名”,在之后执行end tell,结束当前的回话

激活终端

1
2
3
tell application "Terminal"
activate -- 告诉 终端,执行激活命令
end tell

关闭终端

1
2
3
tell application "Terminal"
quit -- 告诉 终端,执行退出命令
end tell

System Events是系统应用,我们有时寄期望于在系统应用中找到某个正在执行的进程(Process)

1
2
3
4
tell application "System Events"
tell process "Terminal"
end tell
end tell

在执行这段代码后,你会发现,其实什么都没有执行,不用慌,这是因为你并没有告诉”终端”要做些什么,那么我们通过entire contents 命令获取Terminal内的所有UI元素

1
2
3
4
5
6
7
#执行下面代码前,先激活"终端"

tell application "System Events"
tell process "Terminal"
entire contents --获取区域内所有的UI元素
end tell
end tell

执行后,会得到”终端”内的所有UI元素的完整描述

3.png

选择日志中的一条进行分析

button 1 of window “终端 — -zsh — 80×24” of application process “Terminal” of application “System Events”

其实这其中的从属关系已经十分明确了,翻译过来就是

应用System Events的应用进程Terminal窗口按钮1

  • 模拟点击

button 1的描述,我们已经很清楚了,那button 1到底对应”终端”中的哪个按钮呢?其实单从名字上看,很难看出button 1对应的是哪个按钮,那么我们用一个很简单的方法,确定它的位置,让button 1执行点击操作,通过它的点击效果反推它的位置!

1
2
3
4
5
6
7
8
9
10
11
12
13
#执行下面代码前,先激活"终端"

tell application "System Events"

tell process "Terminal"
(*
注意:由于环境的差异性,直接复制这段代码到你的脚本编辑器,可能得不到想要的效果
这里一定要输入你上面entire contents后,打印的"button 1"的完整结果
*)
click button 1 of window "终端 — -zsh — 80×24" of application process "Terminal" of application "System Events"
end tell

end tell

执行上述操作后,发现”终端”被关闭了,原来button 1对应的就是”终端”左上角的关闭按钮

请注意:并不是所有的button 1对应的都是应用的关闭按钮,还要具体情况具体分析

  • 模拟输入

我们打开了终端,想看当前目录下存有哪些文件,该如何操作呢?

想一下这个问题,我们都知道在终端中查看目录下文件的命令是ls,那么就可以将这个问题拆分为键入’ls’和点击回车两个步骤

在AppleScript中,输入字符串的命令是keystroke,你也可以用key code实现单键点击

我们完善一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
tell application "Terminal"
activate
end tell

tell application "System Events"

tell process "Terminal"
keystroke "ls"
delay 1 -- 延时一秒后执行
key code 36 -- 回车的键位码为36
end tell

end tell

完整的键位码传送门☞键位码

我们有时需要通过组合按键的方式执行某一操作,通过在键入命令后添加using {按键A down, 按键B down}的方式,如:

1
2
#强制关闭应用快捷键
key code 53 using {command down, option down} -- Esc的键位码为53
  • 执行shell命令

在终端中通过执行shell命令的方式可以达到和模拟键入的同样的效果

1
2
3
4
tell application "Terminal"
activate
do script "ls"
end tell

当然也可打开某路径下的文件,这里我们尝试打开”系统偏好设置”中的”键盘偏好设置”

1
2
3
4
5
tell application "Terminal"
activate
#打开键盘偏好设置
do script "open . '/System/Library/PreferencePanes/Keyboard.prefPane'"
end tell

我们简单回顾一下,现在我们已经了解了:

  • 如何激活应用,并在当前的系统进程中找到它

  • 如何获取某一区域内的全部UI控件,并尝试点击其中的某一个单独控件

  • 如何实现模拟字符串输入、单键输入和组合输入

  • 如何关闭应用

此外,我们再多了解两个帮助我们编写代码的两个小知识点:

1.注释

AppleScript中的注释分为单行注释和多行注释

单行注释:#--

多行注释 (* 中间的部分都是注释 *)

(上文中三种注释均有用到)

2.输出日志

log命令,可以通过打印辅助信息,来帮助我们编写代码

至此,你已经掌握了实现恢复系统默认修饰键的全部知识,小伙伴们完全可以自己尝试写下代码


核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#激活
tell application "Terminal"
activate
#打开键盘偏好设置
do script "open . '/System/Library/PreferencePanes/Keyboard.prefPane'"
end tell

tell application "System Events"

tell process "系统偏好设置"
#点击修饰键
click button "修饰键…" of tab group 1 of window "键盘" -- of application process "System Preferences" of application "System Events"

#更改为默认状态
click button "修饰键…" of tab group 1 of window "键盘" -- of application process "System Preferences" of application "System Events"
click button "恢复成默认" of sheet 1 of window "键盘" -- of application process "System Preferences" of application "System Events"

click button "好" of sheet 1 of window "键盘" -- of application process "System Preferences" of ®application "System Events"

end tell

end tell

编写完成后,选择导出:

笔者在这里文件格式选择的是应用程序,并设置了对应的配合快捷键,实现快捷调用~

完整代码传送门,欢迎Star☞ASShortcutkey

AppleScript基础知识

1.基本模块

语法

1
2
3
#符号'¬'(option+'L') 用来将语句延续到第二行
display dialog "This is just a test." buttons {"Great", "OK"} ¬
default button "OK" giving up after 3 --result:调出弹窗,默认键是OK,3秒后消失

变量和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#变量赋值
set myName to "John"
copy 22 to myAge

#局部变量
local windowCount
local agentName,agentNumber,agentHireDate

#全局变量
global gAgentCount
global gStatementDate,gNextAgentNumber

#属性
property defaultClientName : "Mary Smith"

#字符串中引用变量
set aPerson to "GCS"
display dialog "Hello " & aPerson & "!"

类型转换

1
set myText to 2 as text

运算符

1
3 * 7 - "1" --result 20

List(数组)

1
2
3
4
5
6
#初始化数组
set myList to {1, "what", 3} --result: {1, "what", 3}
set beginning of myList to 0 --首位设置为0
set end of myList to "four" --末位设置为"four"
set item 2 of myList to 4 --第二位设置为4
myList --result: {0, 4, "what", 3, "four"}

Record(键值对)

1
2
3
4
#存值
set myFullName to {firstName:"John", lastName:"Chapman"}
#取值
set myLastName to lastName of myFullName --result "Chapman"

2.控制语句

considering/ignoring

1
2
3
4
5
6
7
8
9
"Hello Bob" = "HelloBob" --result: false
ignoring white space --忽略空格
"Hello Bob" = "HelloBob" --result: true
end ignoring

"BOB" = "bob" --result: true
considering case --考虑大小写
"BOB" = "bob" --result: false
end considering

try-error

1
2
3
4
5
try
word 5 of "one two three"
on error
error "There are not enough words."
end try

if

1
2
3
4
5
6
7
8
9
set currentTemp to 10
if currentTemp < 60 then
set response to "It's a little chilly today."
else if currentTemp > 80 then
set response to "It's getting hotter today."
else
set response to "It's a nice day today."
end if
display dialog response

repeat-exit

1
2
3
4
5
6
7
8
9
10
set num to 0
repeat
-- perform operations
if num < 5 then
set num to num + 1
else
display dialog num
exit repeat
end if
end repeat

repeat (number) times

1
2
3
4
5
6
7
8
set x to 3
set power to 3

set returnVal to x
repeat power - 1 times
set returnVal to returnVal * x
end repeat
return returnVal

repeat while(当型循环)

1
2
3
4
5
6
7
8
9
set userNotDone to true
repeat while userNotDone
set userNotDone to enterDataRecord()
end repeat

on enterDataRecord()
delay 3
false
end enterDataRecord

数值循环

1
2
3
4
5
6
set n to 3
set returnVal to 0
repeat with i from 0 to n
set returnVal to returnVal + I
end repeat
return returnVal

(数组遍历)

1
2
3
4
5
6
7
8
9
10
11
set peopleList to {"Chris", "David", "Sal", "Ben"}

#方法一
repeat with aPerson in peopleList
display dialog "Hello " & aPerson & "!"
end repeat

#方法二
repeat with i from 1 to (number of items in peopleList)
display dialog "Hello " & item i of peopleList & "!"
end repeat

String to List

1
2
3
4
5
6
7
set wordList to words in "Where is the hammer?"
repeat with currentWord in wordList
log currentWord
if (contents of currentWord) is equal to "hammer" then
display dialog "I found the hammer!"
end if
end repeat

record遍历

1
2
3
4
5
6
7
8
9
10
11
12
#在文档中没有找到遍历record的方法,不知道是不是不小心遗漏了
#但是在Stack Overflow中看有如下方法,引入了OC中的Foundation库,实现了record的遍历
use framework "Foundation"
set testRecord to {a:"aaa", b:"bbb", c:"ccc"}

set objCDictionary to current application's NSDictionary's dictionaryWithDictionary:testRecord
set allKeys to objCDictionary's allKeys()

repeat with theKey in allKeys
log theKey as text
log (objCDictionary's valueForKey:theKey) as text
end repeat

3.函数构造

1
2
3
4
5
on greetClient(nameOfClient)
display dialog ("Hello " & nameOfClient & "!")
end greetClient

greetClient("GCS_DEVELOPER")

4.脚本对象

  • 定义/执行脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
script John
property HowManyTimes : 0

to sayHello to someone
set HowManyTimes to HowManyTimes + 1
return "Hello " & someone
end sayHello

end script

tell John to sayHello to "Herb"
#John's sayHello to "Jake" --result: "Hello Jake"
#sayHello of John to "Jake" --result: "Hello Jake"
  • 初始化脚本
1
2
3
4
5
6
7
8
9
10
11
on makePoint(x, y)
script thePoint
property xCoordinate:x
property yCoordinate:y
end script
return thePoint
end makePoint

set myPoint to makePoint(10,20)
get xCoordinate of myPoint --result: 10
get yCoordinate of myPoint --result: 20
  • 继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
script Alex
on sayHello()
return "Hello, " & getName()
end sayHello
on getName()
return "Alex"
end getName
end script

script AlexJunior
property parent : Alex
on getName()
return "Alex Jr"
end getName
end script

-- Sample calls to handlers in the script objects:
tell Alex to sayHello() --result: "Hello, Alex"
tell AlexJunior to sayHello() --result: "Hello, Alex Jr."

tell Alex to getName() --result: "Alex"
tell AlexJunior to getName() --result: "Alex Jr"

写在结尾

本文记录的只是AppleScript中的一小部分内容,笔者在网上暂时还没有找到十分详细的中文文档和介绍,所以想要更加深入的了解和学习AppleScript还是查看官方文档!

一手资料☞AppleScript Language Guide

参考资料:

手把手教你用 AppleScript 模拟鼠标键盘操作,实现 macOS 系统的自动化操作

一:前言

之前也陆陆续续看过一些介绍App Clip的技术文章,发现这些文章几乎千篇一律,差不多都是「苹果技术文档」和「WWDC视频截图」的拼装,以及「WWDC视频讲解」和「网上同类文章」的复述,却很少谈及App Clip真正落地的业务场景和涉及的技术问题。

App Clip发布后,我开始负责App Clip的技术调研工作,也是沾团队的光,约到了一个App Clip lab,感受了一把白天看WWDC,晚上和WWDC讲解人一对一交流的体验,虽然我们团队最后决定先暂停App Clip的开发,但是在调研App Clip的过程中,也确实发现了很多值得思考之处。

先说一个结论:不要因为看了WWDC和一些技术文章,就对App Clip有过分美好的幻想,至少从目前来看,App Clip的坑还很多。

二:业务原因

2.1 业务场景少

  1. 线下支付

    看过WWDC视频的小伙伴们可能已经察觉到了,视频中介绍的场景90%以上都是线下支付的场景,不知道苹果的本意是不是希望通过App Clip去抢占o2o市场,虽然说的是「不用安装商家App」即可通过App Clip享受便捷服务,快速完成下单。可在中国,用户绝大部分的时候本来就不需要下载商家App,难道小程序不香嘛?况且就算真的有这种需求,目前能够满足的也仅仅是iOS 14系统以上的iOS用户。所以这种线下支付的场景,至少在目前看来,App Clip甚至称不上现有方案的一个有效替代方案

  2. 线上拉新

    既然线下支付推进困难重重,那肯定很多人会把目光重新看向线上场景。因为App Clip具备「及时可用」的特点,让用户不用下载完整的App即可享受到App的部分功能,那不妨就把App Clip做为一个拉新的入口,把Clip当做「试用装」吸引用户,等用户习惯了,再引导用户下载App。理想虽然很美好,可现实中怎么把「试用装」交到用户手中呢?我们知道,App Clip的调用场景目前主要就5种,二维码、Safari Smart Banner、NFC、Map和Message。

    其中NFC需要物理介质,Map需要实体店,它们对于纯线上的App来说很难找到应用的场景。

    Message更不用说了,首先国人对Message聊天的需求本来就不大,也总不能群发带Clip的短信给用户吧?

    对二维码,我们是抱有美好的期待的,希望用户通过第三方SDK将二维码分享给好友,然后好友通过二维码调起App Clip,逻辑满分。但尴尬的是,App Clip的二维码,只能通过「系统相机」才能调起。我相信绝大部分的人看到二维码,第一反应都是拿微信去扫一扫,想要培养用户使用「系统相机」扫码的习惯恐怕很难。

    那最后剩下的就是Safari这种方式了,这就需要我们想方设法的让用户在Safari里打开这个链接,注意,必须是Safari里打开,在第三方App里的WebView是没有办法显示Smart Banner的,自然也就没办法调起App Clip。

2.2 操作路径长

即便Safari貌似是纯线上App最适用的场景了,但是这种方式还存在一个问题,那就是用户的操作路径过长。以第三方分享为例,用户在第三方App接受到好友分享的链接后,打开Clip最短的操作路径可能如下:

  • 打开好友分享的链接,进入到内容页;
  • 点击更多按钮,选择在Safari中打开;
  • 点击Safari中Smart Banner上的Open按钮,调起Clip Card;
  • 点击Clip Card上的Open按钮,调起App Clip;

在引导用户通过Safari打开这步估计就足够劝退大部分的用户了…

三:技术原因

即便我们不考虑业务场景,单纯从技术角度看,实现一个App Clip本身恐怕也不会是一个轻松的过程。

  1. 代码拆分

    苹果希望App Clip和App提供一致的体验,刚看完WWDC的视频,一开始真的会给人一种实现一个App Clip 「So Easy」的感觉,只要复用原有的工程,随便勾勾选选,把文件加到Clip的Target下,一个精美小巧的App Clip就呈现在眼前了。

    可事实上呢?通常我们需要尽可能复用现有的业务来开发Clip,可除非你所在的项目业务足够简单,或者组件化做的足够彻底,否则你会发现摘Clip的代码是一件几乎无法实现的事情,很可能需要先做大量的技术重构。当然你也可以将原有代码复制出来,重写一份,可是这样下来,为了保持与完整App的功能一致性,就需要付出维护两套代码的时间成本。

  2. 包大小限制

    10M是App Clip包大小的上限,对于现在动辄一二百M的应用来说,如何用10M来完成目标功能,也是一个不小的挑战。一方面还是上面提到的代码拆分的问题,如果组件化时,基础层划分的颗粒度不够细致,很可能导致上层业务依赖的基础组件过多,从而影响包大小。另一方面,如果App Clip的功能一直是增量的,那必然会导致未来的某一天会超过10M的限制,为避免这一问题,还需要根据新增功能对包大小的影响,配合产品完成现有业务功能的调整。

  3. 后台活动

    App Clip不支持后台处理活动,这里的后台活动不单指beginBackgroundTaskWithExpirationHandler:这种后台任务,经过实际测试,甚至连「正常的网络请求」以及「NSData转 NSString」等操作,在后台执行都是不是完全可靠的,再结合视频里说的:

    In addition, app Clip can’t perform background activity, such as doing background networking with URLSession or maintaining Bluetooth connections when the app clip isn’t in use.

    对于不能执行的background activity目前包括什么,咱无从得知,但从安全的角度看,尽量不在后台搞事情,肯定是第一原则,只是不知道把后台的监听都去掉,你的项目是否还能work~

  4. 唯一标识

    如果想把App Clip当做拉新的手段,那可能就会涉及到一个问题:如果识别出当前App用户曾是App Clip用户?

    我们知道如果用户手机上存在App Clip的情况下,又下载了对应的App,是可以进行数据迁移的。那如果用户的Clip因为一段时间未使用被系统自动删除了,此后用户又下载了App,该怎么设置用户的唯一标识呢?

    要知道Clip在被系统删除时,也会清空其存储在Keychain中的数据,至于IDFA,Shared Keychain那就更别想了,Clip压根不开放这个能力。当然肯定还是有一些其他方式能够对用户进行唯一标识,比如有些防作弊的SDK,就可以标识唯一的设备ID,这就看大家各显神通了。当然,如果不关心这种case的话就另说了。

  5. 第三方App调起

    App Clip可以通过Url Scheme或者Universal Link调起第三方App,但是第三方App却不能通过Url Scheme和Universal Link调起App Clip,说白了就是能调出去但是回不来。第三方登录、第三方分享、第三方支付可能都会受到影响。

  6. 不支持企业包

    企业证书是没法配置App Clip的,如果项目是使用同一个工程,通过不同的configuration切换企业版和正式版,可能还需要注意一个额外的问题,App Clip是通过Build Phases中的Embed App Clip集成到主Target中的,而Build Phases又没办法区分configuration,如果想区分版本去添加Embed App Clip,就只能打企业包时,单独去修改Project文件了。

  7. Pods

    可能目前CocoaPods对App Clip的支持还不够完善,写Demo时发现如果App Clip需要依赖Pods的话,pod install后,在Build Phases中是不会自动生成Embed Pods Frameworks的,这就会导致编译时报image not found的错误,目前解决的办法就是手动添加一下Embed Pods Frameworks

四:结语

当然,对于App Clip的最终落地来说,技术原因终归是次要的,最关键还要看如何找到适合自己产品的业务场景。在苹果线上培训的时候,认识了一个团队的开发,说他们想用App Clip做AR,当时我就觉得这个想法还挺好的。可能对于很多产品来说没有这么新颖的玩法,其实倒也不必太在意,对于苹果来说他们也很清楚目前的现状,据他们自己说也在和美国总部沟通,当然结果如何,咱不得而知,但毕竟留个念想嘛,万一未来App Clip的入口放开了呢~