Dart语法简介

目录

  • 参考资料
  • 语言特性
  • 关键字
  • 变量与常量
  • 数据类型
  • 运算符 operators
  • 控制流程语句
  • 异常 Exceptions
  • 函数 Function
  • 类 Class
  • 类-方法
  • 类-抽象类
  • 类-隐式接口
  • 类-扩展一个类(重写)
  • 库和可见性
  • 异步支持

参考资料

语言特性

  • Dart 所有的东西都是对象, 即使是数字 numbers、函数 function、null 也都是对象,所有的对象都继承自 Object 类。

  • Dart 动态类型语言, 尽量给变量定义一个类型,会更安全,没有显示定义类型的变量在 debug 模式下会类型会是 dynamic(动态的)。

  • Dart 在 running 之前解析你的所有代码,指定数据类型和编译时的常量,可以提高运行速度。

  • Dart 中的类和接口是统一的,类即接口,你可以继承一个类,也可以实现一个类(接口),自然也包含了良好的面向对象和并发编程的支持。

  • Dart 提供了顶级函数(如:main())。

  • Dart 没有 public、private、protected 这些关键字,变量名以"_"开头意味着对它的 lib 是私有的。

  • 没有初始化的变量都会被赋予默认值 null。

  • final 的值只能被设定一次。const 是一个编译时的常量,可以通过 const 来创建常量值,var c=const[];,这里 c 还是一个变量,只是被赋值了一个常量值,它还是可以赋其它值。实例变量可以是 final,但不能是 const。

  • 编程语言并不是孤立存在的,Dart 也是这样,他由语言规范、虚拟机、类库和工具等组成:

    • SDK:SDK 包含 Dart VM、dart2js、Pub、库和工具。
    • Dartium:内嵌 Dart VM 的 Chromium ,可以在浏览器中直接执行 dart 代码。
    • Dart2js:将 Dart 代码编译为 JavaScript 的工具。
    • Dart Editor:基于 Eclipse 的全功能 IDE,并包含以上所有工具。支持代码补全、代码导航、快速修正、重构、调试等功能。

关键字(56 个)

关键字 - - -
abstract do import super
as dynamic in switch
assert else interface sync*
enum implements is this
async* export library throw
await external mix-in true
break extends new try
case factory null typedef
catch false operator var
classfinal part void const
finally rethrow while continue
for return with covariant
get set yield* default
if static deferred

变量与常量

  1. 变量声明与初始化

    • 调用的变量 name 包含对 String 值为“张三” 的对象的引用,name 推断变量的类型是 String,但可以通过指定它来更改该类型,如果对象不限于单一类型(没有明确的类型),请使用 Object 或 dynamic 关键字。
    // 没有明确类型,编译的时候根据值明确类型
    var name = ‘Bob’;
    Object name = '张三';
    dynamic name = '李四';
    // 显示声明将被推断类型, 可以使用 String 显示声明字符串类型
    String name = 'Bob' ;
  2. 默认值

    • 未初始化的变量的初始值为 null(包括数字),因此数字、字符串都可以调用各种方法
    //测试 数字类型的初始值是什么?
    int lineCount;
    // 为 false 的时候抛出异常
    assert(lineCount == null);
    print(lineCount); //打印结果为 null,证明数字类型初始化值是 null
  3. final and const

    • 如果您从未打算更改一个变量,那么使用 final 或 const,不是 var,也不是一个类型。

      一个 final 变量只能被初始化一次; const 变量是一个编译时常量,(Const 变量是隐式的 final)

      final 的顶级或类变量在第一次使用时被初始化。

    • 被 final 修饰的顶级变量或类变量在第一次声明的时候就需要初始化。

    // The final variable 'outSideFinalName' must be initialized.
    final String outSideFinalName
    
    • 被 final 或者 const 修饰的变量,变量类型可以省略,建议指定数据类型。

      //可以省略 String 这个类型声明
      final name = "Bob";
      final String name1 = "张三";
      const name2 = "alex";
      const String name3 = "李四";
    • 被 final 或 const 修饰的变量无法再去修改其值。

      final String outSideFinalName = "Alex";
      // outSideFinalName', a final variable, can only be set once
      // 一个 final 变量,只能被设置一次。
      outSideFinalName = "Bill";
      const String outSideName = 'Bill';
      // 这样写,编译器提示:Constant variables can't be assigned a value
      // const 常量不能赋值
      // outSideName = "小白";
    • flnal 或者 const 不能和 var 同时使用

      // Members can't be declared to be both 'const' and 'var'
      const var String outSideName = 'Bill';
      // Members can't be declared to be both 'final' and 'var'
      final var String name = 'Lili';
    • 常量如果是类级别的,请使用 static const

      // 常量如果是类级别的,请使用 static const
      static const String name3 = 'Tom';
      // 这样写保存
      // Only static fields can be declared as const
      // 只有静态字段可以声明为 const
      //const String name3 = 'Tom';
    • 常量的运算

    const speed = 100; //速度(km/h)
    const double distance = 2.5 _ speed; // 距离 = 时间 _ 速度
    final speed2 = 100; //速度(km/h)
    final double distance2 = 2.5 _ speed2; // 距离 = 时间 _ 速度
    • const 关键字不只是声明常数变量,您也可以使用它来创建常量值,以及声明创建常量值的构造函数,任何变量都可以有一个常量值。
    // 注意: [] 创建的是一个空的 list 集合
    // const []创建一个空的、不可变的列表(EIL)。
    var varList = const []; // varList 当前是一个 EIL
    final finalList = const []; // finalList 一直是 EIL
    const constList = const []; // constList 是一个编译时常量的 EIL
    // 可以更改非 final,非 const 变量的值
    // 即使它曾经具有 const 值
    varList = ["haha"];
    // 不能更改 final 变量或 const 变量的值
    // 这样写,编译器提示:a final variable, can only be set once
    // finalList = ["haha"];
    // 这样写,编译器提示:Constant variables can't be assigned a value
    // constList = ["haha"];
    • 在常量表达式中,该运算符的操作数必须为'bool'、'num'、'String'或'null', const 常量必须用 conat 类型的值初始化。
    const String outSideName = 'Bill';
    final String outSideFinalName = 'Alex';
    const String outSideName2 = 'Tom';
    const aConstList = const ['1', '2', '3'];
    // In constant expressions, operands of this operator must be of type 'bool', 'num', 'String' or 'null'
    // 在常量表达式中,该运算符的操作数必须为'bool'、'num'、'String'或'null'。
    const validConstString = '$outSideName $outSideName2 \$aConstList';
    // Const variables must be initialized with a constant value
    // const 常量必须用 conat 类型的值初始化
    const validConstString = '$outSideName $outSideName2 \$outSideFinalName';
    var outSideVarName='Cathy';
    // Const variables must be initialized with a constant value.
    // const 常量必须用 conat 类型的值初始化
    const validConstString = '$outSideName $outSideName2 \$outSideVarName';
    // 正确写法
    const String outSideConstName = 'Joy';
    const validConstString = '$outSideName $outSideName2 \$outSideConstName';

数据类型

  1. num

    • num 是数字类型的父类,有两个子类 int 和 double。

    • int 根据平台的不同,整数值不大于 64 位。在 Dart VM 上,值可以从-263 到 263 - 1,编译成 JavaScript 的 Dart 使用 JavaScript 代码,允许值从-253 到 253 - 1。

    • double 64 位(双精度)浮点数,如 IEEE 754 标准所规定。

int a = 1;
print(a);

double b = 1.12;
print(b);

// String -> int
int one = int.parse('1');
// 输出3
print(one + 2);

// String -> double
var onePointOne = double.parse('1.1');
// 输出3.1
print(onePointOne + 2);

// int -> String
String oneAsString = 1.toString();
// The argument type 'int' can't be assigned to the parameter type 'String'
//print(oneAsString + 2);
// 输出 1 + 2
print('$oneAsString + 2');
// 输出 1 2
print('$oneAsString 2');

// double -> String 注意括号中要有小数点位数,否则报错
String piAsString = 3.14159.toStringAsFixed(2);
// 截取两位小数, 输出3.14
print(piAsString);

String aString = 1.12618.toStringAsFixed(2);
// 检查是否四舍五入,输出1.13,发现会做四舍五入
print(aString);
  1. String

    • Dart 里面的 String 是一系列 UTF-16 代码单元。
    • 您可以使用单引号或双引号来创建一个字符串。
    • 单引号或者双引号里面嵌套使用引号。
    • 用 或{} 来计算字符串中变量的值,需要注意的是如果是表达式需要${表达式}
String singleString = 'abcdddd';
String doubleString = "abcsdfafd";

String sdString = '$singleString a "bcsd" ${singleString}';
String dsString = "abc 'aaa' \$sdString";
print(sdString);
print(dsString);

String singleString = 'aaa';
String doubleString = "bbb";
// 单引号嵌套双引号
String sdString = '$singleString a "bbb" ${doubleString}';
// 输出 aaa a "bbb" bbb
print(sdString);

// 双引号嵌套单引号
String dsString = "${singleString.toUpperCase()} abc 'aaa' $doubleString.toUpperCase()";
// 输出 AAA abc 'aaa' bbb.toUpperCase(),
可以看出 ”\$doubleString.toUpperCase()“ 没有加“{}“,导致输出结果是”bbb.toUpperCase()“
print(dsString);
  1. bool

  • Dart 是强 bool 类型检查,只有 bool 类型的值是 true 才被认为是 true。
  • 只有两个对象具有 bool 类型:true 和 false,它们都是编译时常量。
  • Dart 的类型安全意味着您不能使用 if(nonbooleanValue)assert(nonbooleanValue) 等代码, 相反 Dart 使用的是显式的检查值。
  • assert 是语言内置的断言函数,仅在检查模式下有效
    在开发过程中, 除非条件为真,否则会引发异常。(断言失败则程序立刻终止)。
// 检查是否为空字符串
var fullName = '';
assert(fullName.isEmpty);

// 检查 0
var hitPoints = 0;
assert(hitPoints <= 0);

// 检查是否为 null
var unicorn;
assert(unicorn == null);

// 检查是否为 NaN
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);
  1. List 集合

  • 在 Dart 中,数组是 List 对象,因此大多数人只是将它们称为 List。
    Dart list 文字看起来像 JavaScript 数组文字
//创建一个 int 类型的 list
List list = [10, 7, 23];
// 输出[10, 7, 23]
print(list);

// 使用 List 的构造函数,也可以添加 int 参数,表示 List 固定长度,不能进行添加 删除操作
var fruits = new List();

// 添加元素
fruits.add('apples');

// 添加多个元素
fruits.addAll(['oranges', 'bananas']);

List subFruits = ['apples', 'oranges', 'banans'];
// 添加多个元素
fruits.addAll(subFruits);

// 输出: [apples, oranges, bananas, apples, oranges, banans]
print(fruits);

// 获取 List 的长度
print(fruits.length);

// 获取第一个元素
print(fruits.first);

// 获取元素最后一个元素
print(fruits.last);

// 利用索引获取元素
print(fruits[0]);

// 查找某个元素的索引号
print(fruits.indexOf('apples'));

// 删除指定位置的元素,返回删除的元素
print(fruits.removeAt(0));

// 删除指定元素,成功返回 true,失败返回 false
// 如果集合里面有多个“apples”, 只会删除集合中第一个改元素
fruits.remove('apples');

// 删除最后一个元素,返回删除的元素
fruits.removeLast();

// 删除指定范围(索引)元素,含头不含尾
fruits.removeRange(start,end);

// 删除指定条件的元素(这里是元素长度大于 6)
fruits.removeWhere((item) => item.length >6);

// 删除所有的元素
fruits.clear();
  • 注意事项:

    1. 可以直接打印 list 包括 list 的元素,list 也是一个对象。但是 java 必须遍历才能打印 list,直接打印是地址值。

    2. 和 java 一样 list 里面的元素必须保持类型一致,不一致就会报错。

    3. 和 java 一样 list 的角标从 0 开始。

    4. 如果集合里面有多个相同的元素“X”, 只会删除集合中第一个改元素

  1. Map 集合

  • 一般来说,map 是将键和值相关联的对象。键和值都可以是任何类型的对象。

    每个键只出现一次,但您可以多次使用相同的值。Dart 支持 map 由 map 文字和 map 类型提供。

  • 初始化 Map 方式一: 直接声明,用{}表示,里面写 key 和 value,每组键值对中间用逗号隔开。

// Two keys in a map literal can't be equal.
// Map companys = {'Alibaba': '阿里巴巴', 'Tencent': '腾讯', 'baidu': '百度', 'Alibaba': '钉钉', 'Tenect': 'qq-music'};

Map companys = {'Alibaba': '阿里巴巴', 'Tencent': '腾讯', 'baidu': '百度'};
// 输出:{Alibaba: 阿里巴巴, Tencent: 腾讯, baidu: 百度}
print(companys);
  • 创建 Map 方式二:先声明,再去赋值。
Map schoolsMap = new Map();
schoolsMap['first'] = '清华';
schoolsMap['second'] = '北大';
schoolsMap['third'] = '复旦';
// 打印结果 {first: 清华, second: 北大, third: 复旦}
print(schoolsMap);

var fruits = new Map();
fruits["first"] = "apple";
fruits["second"] = "banana";
fruits["fifth"] = "orange";
//换成双引号,换成 var 打印结果 {first: apple, second: banana, fifth: orange}
print(fruits);
  • Map API
// 指定键值对的参数类型
var aMap = new Map<int, String>();

// Map 的赋值,中括号中是 Key,这里可不是数组
aMap[1] = '小米';

//Map 中的键值对是唯一的
//同 Set 不同,第二次输入的 Key 如果存在,Value 会覆盖之前的数据
aMap[1] = 'alibaba';

// map 里面的 value 可以相同
aMap[2] = 'alibaba';

// map 里面 value 可以为空字符串
aMap[3] = '';

// map 里面的 value 可以为 null
aMap[4] = null;

print(aMap);

// 检索 Map 是否含有某 Key
assert(aMap.containsKey(1));

//删除某个键值对
aMap.remove(1);

print(aMap);
  • 注意事项

    1. map 的 key 类型不一致也不会报错。

    2. 添加元素的时候,会按照你添加元素的顺序逐个加入到 map 里面,哪怕你的 key,比如分别是 1,2,4,看起来有间隔,事实上添加到 map 的时候是{1:value,2:value,4:value} 这种形式。

    3. map 里面的 key 不能相同。但是 value 可以相同,value 可以为空字符串或者为 null。

运算符

描述 操作符
一元后置操作符 expr++ expr-- () [] . ?.
一元前置操作符 expr !expr ~expr ++expr --expr
乘除 * / % ~/
加减 + -
位移 << >>
按位与 &
按位或
按位异或 ^
逻辑与 &&
逻辑或
关系和类型判断 >= > <= < as is is!
== !=
如果为空 ??
条件表达式 expr1 ? expr2 : expr3
赋值 = *= /= ~/= %= += -= <<= >>= &= ^= = ??=
级联 ..

流程控制语句(Control flow statements)

  • if...else
  • for
  • while do-whild
  • break continue
  • switch...case
  • assert(仅在 checked 模式有效)

异常(Exceptions)

  1. throw

  • 抛出固定类型的异常

    throw new FormatException('Expected at least 1 section');

  • 抛出任意类型的异常

    throw 'Out of llamas!';

  • 因为抛出异常属于表达式,可以将 throw 语句放在=>语句中,或者其它可以出现表达式的地方

    distanceTo(Point other) =>
       throw new UnimplementedError();
    
  • catch

  • 将可能出现异常的代码放置到 try 语句中,可以通过 on 语句来指定需要捕获的异常类型,使用 catch 来处理异常。

    try {
    breedMoreLlamas();
    } on OutOfLlamasException {
    // A specific exception
    buyMoreLlamas();
    } on Exception catch (e) {
    // Anything else that is an exception
    print('Unknown exception: $e');
    } catch (e, s) {
    print('Exception details:\n $e');
    print('Stack trace:\n \$s');
    }
  • rethrow

  • rethrow 语句用来处理一个异常,同时希望这个异常能够被其它调用的部分使用。

 final foo = '';

 void misbehave() {
    try {
      foo = "1";
    } catch (e) {
      print('2');
      rethrow;// 如果不重新抛出异常,main函数中的catch语句执行不到
    }
 }

 void main() {
    try {
      misbehave();
    } catch (e) {
      print('3');
    }
 }

  1. finally

    • Dart 的 finally 用来执行那些无论异常是否发生都执行的操作。
      final foo = '';
      void misbehave() {
    try {
    foo = "1";
    } catch (e) {
    print('2');
    }
    }
    void main() {
    try {
    misbehave();
    } catch (e) {
    print('3');
    } finally {
    print('4'); // 即使没有rethrow最终都会执行到
    }
    }

函数 Function

  • 以下是一个实现函数的例子:
  bool isNoble(int atomicNumber) {
  return \_nobleGases[atomicNumber] != null;
  }
  1. main()函数

    • 每个应用程序都必须有一个顶层 main()函数,它可以作为应用程序的入口点。该 main()函数返回 void 并具有 List参数的可选参数。
      void main() {
      querySelector('#sample_text_id')
    ..text = 'Click me!'
    ..onClick.listen(reverseText);
    }
    • 级联符号..允许您在同一个对象上进行一系列操作。除了函数调用之外,还可以访问同一对象上的字段。这通常会为您节省创建临时变量的步骤,并允许您编写更流畅的代码。
      querySelector('#confirm') // Get an object.
      ..text = 'Confirm' // Use its members.
    ..classes.add('important')
    ..onClick.listen((e) => window.alert('Confirmed!'));
    • 上述例子相对于:
      var button = querySelector('#confirm');
      button.text = 'Confirm';
    button.classes.add('important');
    button.onClick.listen((e) => window.alert('Confirmed!'));
    • 级联符号也可以嵌套使用。 例如:
      final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
    ..email = 'jenny@example.com'
    ..phone = (PhoneNumberBuilder()
    ..number = '415-555-0100'
    ..label = 'home')
    .build())
    .build();
    • 当返回值是 void 时不能构建级联。 例如,以下代码失败:
      var sb = StringBuffer();
      sb.write('foo') // 返回 void
    ..write('bar'); // 这里会报错
    • 注意: 严格地说,级联的..符号不是操作符。它只是 Dart 语法的一部分。
  2. 可选参数

    • 可选的命名参数, 定义函数时,使用{param1, param2, …},用于指定命名参数。例如:
      //设置[bold]和[hidden]标志
      void enableFlags({bool bold, bool hidden}) {
    // ...
    }
    enableFlags(bold: true, hidden: false);
    • 可选的位置参数,用[]它们标记为可选的位置参数:
      String say(String from, String msg, [String device]) {
      var result = '$from says $msg';
    if (device != null) {
    result = '$result with a $device';
    }
    return result;
    }
    • 下面是一个不带可选参数调用这个函数的例子:

      say('Bob', 'Howdy'); //结果是: Bob says Howdy

    • 下面是用第三个参数调用这个函数的例子:

      say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal

  3. 默认参数

    • 函数可以使用=为命名参数和位置参数定义默认值。默认值必须是编译时常量。如果没有提供默认值,则默认值为 null。

    • 下面是为命名参数设置默认值的示例:

      // 设置 bold 和 hidden 标记的默认值都为false
      void enableFlags2({bool bold = false, bool hidden = false}) {
    // ...
    }
    // 调用的时候:bold will be true; hidden will be false.
    enableFlags2(bold: true);
    • 下一个示例显示如何为位置参数设置默认值:
      String say(String from, String msg,
      [String device = 'carrier pigeon', String mood]) {
    var result = '$from says $msg';
    if (device != null) {
    result = '$result with a $device';
    }
    if (mood != null) {
    result = '$result (in a $mood mood)';
    }
    return result;
    }
    //调用方式:
    say('Bob', 'Howdy'); //结果为:Bob says Howdy with a carrier pigeon;
    • 您还可以将 list 或 map 作为默认值传递。下面的示例定义一个函数 doStuff(),该函数指定列表参数的默认 list 和 gifts 参数的默认 map。

      // 使用 list 或者 map 设置默认值
      void doStuff(
      {List list = const [1, 2, 3],
      Map gifts = const {'first': 'paper',
      'second': 'cotton', 'third': 'leather'
      }}) {
      print('list: \(list');
      print('gifts: \)gifts');
      }

  4. 作为一个类对象的功能

  • 您可以将一个函数作为参数传递给另一个函数。

    void printElement(int element) {
    print(element);
    }

    var list = [1, 2, 3];

    // 把 printElement 函数作为一个参数传递进来
    list.forEach(printElement);

  • 您也可以将一个函数分配给一个变量。

    var loudify = (msg) => '!!! \${msg.toUpperCase()} !!!';
    assert(loudify('hello') == '!!! HELLO !!!');
    
  1. 匿名函数

    • 大多数函数都能被命名为匿名函数,如 main() 或 printElement()。您还可以创建一个名为匿名函数的无名函数,有时也可以创建 lambda 或闭包。您可以为变量分配一个匿名函数,例如,您可以从集合中添加或删除它。

    • 一个匿名函数看起来类似于一个命名函数 - 0 或更多的参数,在括号之间用逗号和可选类型标注分隔。

    • 下面的代码块包含函数的主体:

      ([[Type] param1[, …]]) {
         codeBlock;
    };
    • 下面的示例定义了一个具有无类型参数的匿名函数 item,该函数被 list 中的每个 item 调用,输出一个字符串,该字符串包含指定索引处的值。
      var list = ['apples', 'bananas', 'oranges'];
      list.forEach((item) {
    print('${list.indexOf(item)}: $item');
    });
    • 如果函数只包含一条语句,可以使用箭头符号=>来缩短它, 比如上面的例 2 可以简写成:

      list.forEach((item) => print('${list.indexOf(item)}: $item'));

  1. 返回值

  • 所有函数都返回一个值,如果没有指定返回值,则语句 return null,隐式地附加到函数体。
  foo() {}
  assert(foo() == null);

类(Classes)

  1. 对象

  • Dart 是一种面向对象的语言,并且支持基于 mixin 的继承方式。
  • Dart 语言中所有的对象都是某一个类的实例,所有的类有同一个基类--Object。
  • 基于 mixin 的继承方式具体是指:一个类可以继承自多个父类。

  • 使用 new 语句来构造一个类,构造函数的名字可能是 ClassName,也可以是 ClassName.identifier, 例如:

  var jsonData = JSON.decode('{"x":1, "y":2}');

  // Create a Point using Point().
  var p1 = new Point(2, 2);

  // Create a Point using Point.fromJson().
  var p2 = new Point.fromJson(jsonData);
  • 使用.(dot)来调用实例的变量或者方法。
  var p = new Point(2, 2);

  // Set the value of the instance variable y.
  p.y = 3;

  // Get the value of y.
  assert(p.y == 3);

  // Invoke distanceTo() on p.
  num distance = p.distanceTo(new Point(4, 4));
  • 使用?.来确认前操作数不为空, 常用来替代. , 避免左边操作数为 null 引发异常。

    // If p is non-null, set its y value to 4.
    p?.y = 4;
    
  • 使用 const 替代 new 来创建编译时的常量构造函数。

    var p = const ImmutablePoint(2, 2);
    
  • 使用 runtimeType 方法,在运行中获取对象的类型。该方法将返回 Type 类型的变量。

    print('The type of a is ${a.runtimeType}');
    
  1. 实例化变量(Instance variables)

  • 在类定义中,所有没有初始化的变量都会被初始化为 null。
class Point {
num x; // Declare instance variable x, initially null.
num y; // Declare y, initially null.
num z = 0; // Declare z, initially 0.
}
  • 类定义中所有的变量, Dart 语言都会隐式的定义 setter 方法,针对非空的变量会额外增加 getter 方法。
  class Point {
      num x;
      num y;
  }

  main() {
      var point = new Point();
      point.x = 4; // Use the setter method for x.
      assert(point.x == 4); // Use the getter method for x.
      assert(point.y == null); // Values default to null.
  }
  1. 构造函数(Constructors)

  • 声明一个和类名相同的函数,来作为类的构造函数。
  class Point {
    num x;
    num y;
   
    Point(num x, num y) {
      // There's a better way to do this, stay tuned.
      this.x = x;
      this.y = y;
    }
  }
  • this 关键字指向了当前类的实例, 上面的代码可以简化为:
  class Point {
    num x;
    num y;

    // Syntactic sugar for setting x and y
    // before the constructor body runs.
    Point(this.x, this.y);
  }
  1. 构造函数不能继承(Constructors aren’t inherited)

  • Dart 语言中,子类不会继承父类的命名构造函数。如果不显式提供子类的构造函数,系统就提供默认的构造函数。

  1. 命名的构造函数(Named constructors)

  • 使用命名构造函数从另一类或现有的数据中快速实现构造函数。

    class Point {
    num x;
    num y;
    Point(this.x, this.y);
    // 命名构造函数 Named constructor
    Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
    }
    }
  • 构造函数不能被继承,父类中的命名构造函数不能被子类继承。如果想要子类也拥有一个父类一样名字的构造函数,必须在子类是实现这个构造函数。

  1. 调用父类的非默认构造函数

  • 默认情况下,子类只能调用父类的无名,无参数的构造函数; 父类的无名构造函数会在子类的构造函数前调用; 如果 initializer list 也同时定义了,则会先执行 initializer list 中的内容,然后在执行父类的无名无参数构造函数,最后调用子类自己的无名无参数构造函数。即下面的顺序:

    1. initializer list(初始化列表)
    2. super class’s no-arg constructor(父类无参数构造函数)
    3. main class’s no-arg constructor (主类无参数构造函数)
  • 如果父类不显示提供无名无参数构造函数的构造函数,在子类中必须手打调用父类的一个构造函数。这种情况下,调用父类的构造函数的代码放在子类构造函数名后,子类构造函数体前,中间使用:(colon) 分割。

  class Person {
     String firstName;

     Person.fromJson(Map data) {
         print('in Person');
     }
  }

  class Employee extends Person {
     // 父类没有无参数的非命名构造函数,必须手动调用一个构造函数
     super.fromJson(data)
     Employee.fromJson(Map data) : super.fromJson(data) {
        print('in Employee');
     }
  }

  main() {
     var emp = new Employee.fromJson({});

     // Prints:
     // in Person
     // in Employee
     if (emp is Person) {
       // Type check
       emp.firstName = 'Bob';
     }
     (emp as Person).firstName = 'Bob';
  }
  1. 初始化列表

    • 除了调用父类的构造函数,也可以通过初始化列表在子类的构造函数体前(大括号前)来初始化实例的变量值,使用逗号,分隔。如下所示:
      class Point {
        num x;
    num y;
    Point(this.x, this.y);
    // 初始化列表在构造函数运行前设置实例变量。
    Point.fromJson(Map jsonMap)
    : x = jsonMap['x'],
    y = jsonMap['y'] {
    print('In Point.fromJson(): ($x, $y)');
    }
    }

    注意:上述代码,初始化程序无法访问 this 关键字。

  2. 静态构造函数

    • 如果你的类产生的对象永远不会改变,你可以让这些对象成为编译时常量。为此,需要定义一个 const 构造函数并确保所有的实例变量都是 final 的。
      class ImmutablePoint {
        final num x;
    final num y;
    const ImmutablePoint(this.x, this.y);
    static final ImmutablePoint origin = const ImmutablePoint(0, 0);
    }
  3. 重定向构造函数

    • 有时候构造函数的目的只是重定向到该类的另一个构造函数。重定向构造函数没有函数体,使用冒号:分隔。
      class Point {
          num x;
    num y;
    // 主构造函数
    Point(this.x, this.y) {
    print("Point($x, $y)");
    }
    // 重定向构造函数,指向主构造函数,函数体为空
    Point.alongXAxis(num x) : this(x, 0);
    }
    void main() {
    var p1 = new Point(1, 2);
    var p2 = new Point.alongXAxis(4);
    }
  4. 常量构造函数

  • 如果类的对象不会发生变化,可以构造一个编译时的常量构造函数。定义格式如下:

    • 定义所有的实例变量是 final。
    • 使用 const 声明构造函数。
    class ImmutablePoint {
    final num x;
    final num y;
    const ImmutablePoint(this.x, this.y);
    static final ImmutablePoint origin = const ImmutablePoint(0, 0);
    }
  • 工厂构造函数

  • 当实现一个使用 factory 关键词修饰的构造函数时,这个构造函数不必创建类的新实例。例如,工厂构造函数可能从缓存返回实例,或者它可能返回子类型的实例。 下面的示例演示一个工厂构造函数从缓存返回的对象:

  class Logger {
    final String name;
    bool mute = false;

    // _cache 是一个私有库,幸好名字前有个 _ 。
    static final Map<String, Logger> \_cache = <String, Logger>{};

    factory Logger(String name) {
        if (\_cache.containsKey(name)) {
            return \_cache[name];
        } else {
            final logger = new Logger.\_internal(name);
            \_cache[name] = logger;
            return logger;
        }
    }

    Logger._internal(this.name);
    void log(String msg) {
        if (!mute) {
            print(msg);
         }
      }
  }

注意:工厂构造函数不能用 this。

方法

  • 方法就是为对象提供行为的函数。

  1. 实例方法

    • 对象的实例方法可以访问实例变量和 this 。以下示例中的 distanceTo() 方法是实例方法的一个例子:
      import 'dart:math';
      class Point {
    num x;
    num y;
    Point(this.x, this.y);
    num distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx _ dx + dy _ dy);
    }
    }
  2. setters 和 Getters

    • 是一种提供对方法属性读和写的特殊方法。每个实例变量都有一个隐式的 getter 方法,合适的话可能还会有 setter 方法。你可以通过实现 getters 和 setters 来创建附加属性,也就是直接使用 get 和 set 关键词:
      class Rectangle {
          num left;
    num top;
    num width;
    num height;
    Rectangle(this.left, this.top, this.width, this.height);
    // 定义两个计算属性: right and bottom.
    num get right => left + width;
    set right(num value) => left = value - width;
    num get bottom => top + height;
    set bottom(num value) => top = value - height;
    }
    main() {
    var rect = new Rectangle(3, 4, 20, 15);
    assert(rect.left == 3);
    rect.right = 12;
    assert(rect.left == -8);
    }
    • 借助于 getter 和 setter ,你可以直接使用实例变量,并且在不改变客户代码的情况下把他们包装成方法。

    • 注: 不论是否显式地定义了一个 getter,类似增量(++)的操作符,都能以预期的方式工作。为了避免产生任何向着不期望的方向的影响,操作符一旦调用 getter ,就会把他的值存在临时变量里。

3) 抽象方法
- Instance , getter 和 setter 方法可以是抽象的,也就是定义一个接口,但是把实现交给其他的类。要创建一个抽象方法,使用分号(;)代替方法体:

```
  abstract class Doer {
    // ...定义实例变量和方法...
    void doSomething(); // 定义一个抽象方法。
  }

  class EffectiveDoer extends Doer {
        void doSomething() {
        // ...提供一个实现,所以这里的方法不是抽象的...
        }
  }
```

4) 枚举类型

  • 枚举类型,通常被称为 enumerations 或 enums ,是一种用来代表一个固定数量的常量的特殊类。

  • 声明一个枚举类型需要使用关键字 enum :

enum Color {
  red,
  green,
  blue
}
  • 在枚举中每个值都有一个 index getter 方法,它返回一个在枚举声明中从 0 开始的位置。例如,第一个值索引值为 0 ,第二个值索引值为 1 。
  assert(Color.red.index == 0);
  assert(Color.green.index == 1);
  assert(Color.blue.index == 2);
  • 要得到枚举列表的所有值,可使用枚举的 values 常量。

    List<Color> colors = Color.values;
    assert(colors[2] == Color.blue);
    
    • 你可以在 switch 语句 中使用枚举。如果 e 在 switch (e) 是显式类型的枚举,那么如果你不处理所有的枚举值将会弹出警告:
    enum Color {
      red,
    green,
    blue
    }
    // ...
    Color aColor = Color.blue;
    switch (aColor) {
    case Color.red:
    print('Red as roses!');
    break;
    case Color.green:
    print('Green as grass!');
    break;
    default: // Without this, you see a WARNING.
    print(aColor); // 'Color.blue'
    }

    枚举类型有以下限制

    • 你不能在子类中混合或实现一个枚举。
    • 你不能显式实例化一个枚举。
  1. 为类添加特征:mixins

    • mixins 是一种多类层次结构的类的代码重用。

    • 要使用 mixins ,在 with 关键字后面跟一个或多个 mixin 的名字。下面的例子显示了两个使用 mixins 的类:

     class Musician extends Performer with Musical {
          // ...
    }
    class Maestro extends Person with Musical,
    Aggressive, Demented {
    Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
    }
    }
  2. 要实现 mixin ,就创建一个继承 Object 类的子类,不声明任何构造函数,不调用 super 。例如:

abstract class Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

 void entertainMe() {
   if (canPlayPiano) {
       print('Playing piano');
   } else if (canConduct) {
       print('Waving hands');
   } else {
       print('Humming to self');
   }
 }
}
  1. 类的变量和方法

    • 使用 static 关键字来实现类变量和类方法。

    • 只有当静态变量被使用时才被初始化。

    • 静态变量, 静态变量(类变量)对于类状态和常数是有用的:

    class Color {
       static const red = const Color('red'); // 一个恒定的静态变量
    final String name; // 一个实例变量。
    const Color(this.name); // 一个恒定的构造函数。
    }
    main() {
    assert(Color.red.name == 'red');
    }
    • 静态方法, 静态方法(类方法)不在一个实例上进行操作,因而不必访问 this 。例如:
      import 'dart:math';
      class Point {
    num x;
    num y;
    Point(this.x, this.y);
    static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
    }
    }
    main() {
    var a = new Point(2, 2);
    var b = new Point(4, 4);
    var distance = Point.distanceBetween(a, b);
    assert(distance < 2.9 && distance > 2.8);
    }
    • 注:考虑到使用高阶层的方法而不是静态方法,是为了常用或者广泛使用的工具和功能。

    • 你可以将静态方法作为编译时常量。例如,你可以把静态方法作为一个参数传递给静态构造函数。

抽象类

  • 使用 abstract 修饰符来定义一个抽象类,该类不能被实例化。抽象类在定义接口的时候非常有用,实际上抽象中也包含一些实现。如果你想让你的抽象类被实例化,请定义一个 工厂构造函数 。

  • 抽象类通常包含 抽象方法。下面是声明一个含有抽象方法的抽象类的例子:

    // 这个类是抽象类,因此不能被实例化。
    abstract class AbstractContainer {
    // ...定义构造函数,域,方法...

     void updateChildren(); // 抽象方法。
    

    }

  • 下面的类不是抽象类,因此它可以被实例化,即使定义了一个抽象方法:

    class SpecializedContainer extends AbstractContainer {
    // ...定义更多构造函数,域,方法...

      void updateChildren() {
        // ...实现 updateChildren()...
    }
    // 抽象方法造成一个警告,但是不会阻止实例化。
    void doSomething();

    }

类-隐式接口

  • 每个类隐式的定义了一个接口,含有类的所有实例和它实现的所有接口。如果你想创建一个支持类 B 的 API 的类 A,但又不想继承类 B ,那么,类 A 应该实现类 B 的接口。

  • 一个类实现一个或更多接口通过用 implements 子句声明,然后提供 API 接口要求。例如:

    // 一个 person ,包含 greet() 的隐式接口。
    class Person {
    // 在这个接口中,只有库中可见。
    final _name;
    // 不在接口中,因为这是个构造函数。
    Person(this._name);
    // 在这个接口中。
    String greet(who) => 'Hello, $who. I am $_name.';
    }
    // Person 接口的一个实现。
    class Imposter implements Person {
    // 我们不得不定义它,但不用它。
    final _name = "";
    String greet(who) => 'Hi $who. Do you know who I am?';
    }
    greetBob(Person person) => person.greet('bob');
    main() {
    print(greetBob(new Person('kathy')));
    print(greetBob(new Imposter()));
    }
    • 这里是具体说明一个类实现多个接口的例子:
     class Point implements Comparable, Location {
        // ...
    }

    类-扩展一个类

  • 使用 extends 创建一个子类,同时 supper 将指向父类:

class Television {
  void turnOn() {
     _illuminateDisplay();
      _activateIrSensor();
  }
  // ...
}

class SmartTelevision extends Television {

  void turnOn() {
     super.turnOn();
     _bootNetworkInterface();
     _initializeMemory();
     _upgradeApps();
  }
  // ...
}
  • 子类可以重载实例方法, getters 方法, setters 方法。下面是个关于重写 Object 类的方法 noSuchMethod() 的例子,当代码企图用不存在的方法或实例变量时,这个方法会被调用。
class A {
  // 如果你不重写 noSuchMethod 方法, 就用一个不存在的成员,会导致NoSuchMethodError 错误。
  void noSuchMethod(Invocation mirror) {
      print('You tried to use a non-existent member:' +
          '${mirror.memberName}');
   }
}
  • 你可以使用 @override 注释来表明你重写了一个成员。
class A {
  @override
  void noSuchMethod(Invocation mirror) {
     // ...
  }
}
  • 如果你用 noSuchMethod() 实现每一个可能的 getter 方法,setter 方法和类的方法,那么你可以使用 @proxy 标注来避免警告。
  • @proxy
    class A {
    void noSuchMethod(Invocation mirror) {
    // ...
    }
    }

    库和可见性

  1. import,part,library 指令可以帮助创建一个模块化的,可共享的代码库。库不仅提供了 API,还提供隐私单元:以下划线(_)开头的标识符只对内部库可见。每个 Dartapp 就是一个库,即使它不使用库指令。

  2. 库可以分布式使用包。见 Pub Package and Asset Manager 中有关 pub(SDK 中的一个包管理器)。

  3. 使用库

  • 使用 import 来指定如何从一个库命名空间用于其他库的范围。

  • 例如,Dart Web 应用一般采用这个库 dart:html,可以这样导入:

    import 'dart:html';
    
  • 唯一需要 import 的参数是一个指向库的 URI。对于内置库,URI 中具有特殊 dart:scheme。对于其他库,你可以使用文件系统路径或 package:scheme。包 package:scheme specifies libraries ,如 pub 工具提供的软件包管理器库。例如:

    import 'dart:io';
    import 'package:mylib/mylib.dart';
    import 'package:utils/utils.dart';
  • 指定库前缀

  • 如果导入两个库是有冲突的标识符,那么你可以指定一个或两个库的前缀。例如,如果 library1 和 library2 都有一个元素类,那么你可能有这样的代码:

    import 'package:lib1/lib1.dart';
    import 'package:lib2/lib2.dart' as lib2;
    // ...
    var element1 = new Element(); // 使用lib1里的元素
    var element2 =
    new lib2.Element(); // 使用lib2里的元素
  1. 导入部分库

    • 如果想使用的库一部分,你可以选择性导入库。例如:
      // 只导入 foo 库
      import 'package:lib1/lib1.dart' show foo;
    //导入所有除了 foo
    import 'package:lib2/lib2.dart' hide foo;
  2. 延迟加载库

    • 延迟(deferred)加载(也称为延迟(lazy)加载)允许应用程序按需加载库。下面是当你可能会使用延迟加载某些情况:

      • 为了减少应用程序的初始启动时间;
      • 执行 A / B 测试-尝试的算法的替代实施方式中;
      • 加载很少使用的功能,例如可选的屏幕和对话框。
    • 为了延迟加载一个库,你必须使用 deferred as 先导入它。

      import 'package:deferred/hello.dart' deferred as hello;
      
    • 当需要库时,使用该库的调用标识符调用 LoadLibrary()。

       greet() async {
         await hello.loadLibrary();
    hello.printGreeting();
    }
    • 在前面的代码,在库加载好之前,await 关键字都是暂停执行的。有关 async 和 await 见 asynchrony support 的更多信息。

    • 您可以在一个库调用 LoadLibrary() 多次都没有问题。该库也只被加载一次。

    • 当您使用延迟加载,请记住以下内容:

      • 延迟库的常量在其作为导入文件时不是常量。记住,这些常量不存在,直到迟库被加载完成。
      • 你不能在导入文件中使用延迟库常量的类型。相反,考虑将接口类型移到同时由延迟库和导入文件导入的库。
      • Dart 隐含调用 LoadLibrary()插入到定义 deferred as namespace。在调用 LoadLibrary()函数返回一个 Future。
  1. 库的实现

    • 用 library 来来命名库,用 part 来指定库中的其他文件。 注意:不必在应用程序中(具有顶级 main()函数的文件)使用 library,但这样做可以让你在多个文件中执行应用程序。
  2. 声明库

    • 利用 library identifier(库标识符)指定当前库的名称:
    // 声明库,名ballgame
    library ballgame;
    // 导入html库
    import 'dart:html';
    // ...代码从这里开始...

9) 关联文件与库
- 添加实现文件,把 part fileUri 放在有库的文件,其中 fileURI 是实现文件的路径。然后在实现文件中,添加部分标识符(part of identifier),其中标识符是库的名称。下面的示例使用的一部分,在三个文件来实现部分库。
- 第一个文件,ballgame.dart,声明球赛库,导入其他需要的库,并指定 ball.dart 和 util.dart 是此库的部分:
```
library ballgame;

import 'dart:html';
// ...其他导入在这里...

part 'ball.dart';
part 'util.dart';

// ...代码从这里开始...
```
  • 第二个文件 ball.dart,实现了球赛库的一部分:
    part of ballgame;
    // ...代码从这里开始...
  • 第三个文件,util.dart,实现了球赛库的其余部分:
   part of ballgame;
   // ...Code goes here...
  1. 重新导出库(Re-exporting libraries)

    • 可以通过重新导出部分库或者全部库来组合或重新打包库。例如,你可能有实现为一组较小的库集成为一个较大库。或者你可以创建一个库,提供了从另一个库方法的子集。

      // In french.dart:
      library french;
      hello() => print('Bonjour!');
      goodbye() => print('Au Revoir!');
      // In togo.dart:
      library togo;
      import 'french.dart';
      export 'french.dart' show hello;
      // In another .dart file:
      import 'togo.dart';
      void main() {
      hello(); //print bonjour
      goodbye(); //FAIL
      }

异步的支持

  1. Dart 添加了一些新的语言特性用于支持异步编程。最通常使用的特性是 async 方法和 await 表达式。Dart 库大多方法返回 Future 和 Stream 对象。这些方法是异步的:它们在设置一个可能的耗时操作(比如 I/O 操作)之后返回,而无需等待操作完成

  2. 当你需要使用 Future 来表示一个值时,你有两个选择。

    • 使用 async 和 await
    • 使用 Future API
  3. 同样的,当你需要从 Stream 获取值的时候,你有两个选择。

    • 使用 async 和一个异步的 for 循环 (await for)
    • 使用 Stream API
  4. 使用 async 和 await 的代码是异步的,不过它看起来很像同步的代码。比如这里有一段使用 await 等待一个异步函数结果的代码:

await lookUpVersion()

  1. 要使用 await,代码必须用 await 标记

     checkVersion() async {
        var version = await lookUpVersion();
    if (version == expectedVersion) {
    // Do something.
    } else {
    // Do something else.
    }
    }
  2. 你可以使用 try, catch, 和 finally 来处理错误并精简使用了 await 的代码。

     try {
        server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4044);
    } catch (e) {
    // React to inability to bind to the port...
    }
  3. 声明异步函数

  • 一个异步函数是一个由 async 修饰符标记的函数。虽然一个异步函数可能在操作上比较耗时,但是它可以立即返回-在任何方法体执行之前。
checkVersion() async {
  // ...
}

lookUpVersion() async => /* ... */;
  • 在函数中添加关键字 async 使得它返回一个 Future,比如,考虑一下这个同步函数,它将返回一个字符串。

  • String lookUpVersionSync() => '1.0.0';

  • 如果你想更改它成为异步方法-因为在以后的实现中将会非常耗时-它的返回值是一个 Future 。

  • Future lookUpVersion() async => '1.0.0';

  • 请注意函数体不需要使用 Future API,如果必要的话 Dart 将会自己创建 Future 对象

  1. 使用带 future 的 await 表达式

  • 一个 await 表达式具有以下形式

    await expression

  • 在异步方法中你可以使用 await 多次。比如,下列代码为了得到函数的结果一共等待了三次。

   var entrypoint = await findEntrypoint();
   var exitCode = await runExecutable(entrypoint, args);
   await flushThenExit(exitCode);
  • await 表达式中, 表达式 的值通常是一个 Future 对象;如果不是,那么这个值会自动转为 Future。这个 Future 对象表明了表达式应该返回一个对象。await 表达式 的值就是返回的一个对象。在对象可用之前,await 表达式将会一直处于暂停状态。

  • 如果 await 没有起作用,请确认它是一个异步方法。比如,在你的 main() 函数里面使用 await,main() 的函数体必须被 async 标记:

      main() async {
         checkVersion();
    print('In main: version is ${await lookUpVersion()}');
    }
  1. 结合 streams 使用异步循环

    • 一个异步循环具有以下形式:
      await for (variable declaration in expression) {
      // Executes each time the stream emits a value.
    }
    • 表达式 的值必须有 Stream 类型(流类型)。执行过程如下:

      • 在 stream 发出一个值之前等待
      • 执行 for 循环的主体,把变量设置为发出的值。
      • 重复 1 和 2,直到 Stream 关闭
    • 如果要停止监听 stream ,你可以使用 break 或者 return 语句,跳出循环并取消来自 stream 的订阅 。

    • 如果一个异步 for 循环没有正常运行,请确认它是一个异步方法。 比如,在应用的 main() 方法中使用异步的 for 循环时,main() 的方法体必须被 async 标记。

    • 更多关于异步编程的信息,请看 dart:async 库部分的介绍。你也可以看文章 [Dart Language Asynchrony Support: Phase 1 ] (https://www.dartlang.org/articles/await-async/) 和 Dart Language Asynchrony Support: Phase 2the Dart language specification

2019/12/24 posted in  Flutter

[Flutter]Image图片缓存

源码分析

class Image extends StatefulWidget {

   Image.network(String src, {

   Key key,

   double scale = 1.0,

   this.width,

   this.height,

   this.color,

   this.colorBlendMode,

   this.fit,

   this.alignment = Alignment.center,

   this.repeat = ImageRepeat.noRepeat,

   this.centerSlice,

   this.matchTextDirection = false,

   this.gaplessPlayback = false,

   Map<String, String> headers,

     }) : image = new NetworkImage(src, scale: scale, headers: headers),

      assert(alignment != null),

      assert(repeat != null),

      assert(matchTextDirection != null),

      super(key: key);

  ......

我们看到Image是一个StatefulWidget对象,可以直接放到Container或者Column等容器里,其属性解释如下:

width:widget的宽度

height:widget的高度

color:与colorBlendMode配合使用,将此颜色用BlendMode方式混合图片

colorBlendMode:混合模式算法

fit:与android:scaletype一样,控制图片如何resized/moved来匹对Widget的size

alignment:widget对齐方式

repeat:如何绘制未被图像覆盖的部分

centerSlice:支持9patch,拉伸的中间的区域

matchTextDirection:绘制图片的方向:是否从左到右

gaplessPlayback:图片变化的时候是否展示老图片或者什么都不展示

headers:http请求头

image:一个ImageProvide对象,在调用的时候已经实例化,这个类主要承担了从网络加载图片的功能。它是加载图片的最重要的方法,不同的图片加载方式(assert文件加载、网络加载等等)也就是重写ImageProvider加载图片的方法(load())。

Image是一个StatefulWidget对象,所以我们看它的State对象:

2019/12/18 posted in  Flutter

"Authenticating with the iTunes store” 或 “正在通过 iTunes Store进行鉴定”的完美解决办法

用 Xcode 提交 app 到 iTunes store 时总是卡死在 “Authenticating with the iTunes store”。改DNS、用VPN都不行,全局、自动代理模式全都试过了。
网上找解决办法,有很多人推荐使用“Application Loader”上传:
但是使用 Application Loader 提交时又卡死在 “正在通过 iTunes Store进行鉴定” 。

$ cd ~    #进入用户根目录
$ mv .itmstransporter/ .old_itmstransporter/    #将名为itmstransporter的隐藏文件重命名为old_itmstransporter
$ /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter     #运行Application Loader.app包里的iTMSTransporter程序

当3条命令都执行完成后再用,Xcode 或 Application Loader上传。

2019/6/21 posted in  Xcode

马蜂窝 iOS App 启动治理:回归用户体验

From:https://mp.weixin.qq.com/s?__biz=Mzg5MTA4Mzg5NA==&mid=2247483816&idx=1&sn=588981334dd8c797ce2e4395db6ca68d&utm_source=tuicool&utm_medium=referral

增长、活跃、留存是移动 App 的常见核心指标,直接反映一款 App 甚至一个互联网公司运行的健康程度和发展动能。启动流程的体验决定了用户的第一印象,在一定程度上影响了用户活跃度和留存率。因此,确保启动流程的良好体验至关重要。

「马蜂窝旅游」App 是马蜂窝为用户提供服务的主要阵地,其承载的 业务模块不断丰富和完善,产品功能日趋复杂, 已经逐渐成长为一个集合旅行信息、出行决策、自由行产品及服务交易的一站式移动平台。

「马蜂窝旅游」iOS App 历经几十个版本的开发迭代,在启动流程上积累了一定的技术债务。为了带给用户更流畅的使用体验,我们团队实施了数月的专项治理,也总结出一些 iOS 启动治理方面的实践经验,借由本文和大家分享。

0X0

如何定义「启动」

要分析和解决启动问题,我们首先需要界定启动的内涵和边界,从哪开始、到哪结束,中间经历了哪些阶段和过程。以不同视角去观察时,可以得出不同结论。

技术视角

App 启动原本就是程序启动的技术过程。作为开发人员,我们很自然地更愿意从技术阶段去看待和定义启动的流程。

App 启动的方式分为 冷启动 和 热启动 两种。简单来说,冷启动发生时后台是没有这个应用的进程的,程序需要从头开始,经过漫长的准备和加载过程,最终运行起来。而热启动则是在后台已有该应用进程的情况下发生的,系统不需要重新创建和初始化。因此,从技术视角讨论启动治理时,主要针对冷启动。

从技术视角出发,分析 iOS 的启动过程,主要分为两个阶段:

pre-main: main() 函数是程序执行入口,从进程创建到进入 main 函数称为 premain 阶段, 主要包括了环境准备、资源加载等操作;

post-main: main() 函数到-didFinishLaunchWithOptions:方法执行结束。该阶段已获得代码执行控制权,是我们治理的主要部分。

 <premain>                  <postmain>

+----------------X------------------------------------X--------->

start main -didFinishLaunchWithOptions:

用户视角

iOS App 是面向终端用户的产品,因此衡量启动的最终标准还是要从用户视角出发。

从用户视角定义启动,主要以用户主观视觉为依据,以页面流程为标准。这样看来,常见的 App 启动可以分为三个阶段:

T1:闪屏页
闪屏页是启动过程中的静态展示页。在冷启动的过程中,App 还没有运行起来,需要经历环境准备和初始化的过程。这个过渡阶段需要展示一些视图,供阻塞等待中的用户浏览。

iOS 系统 (SpringBoard) 根据 App Bundle 目录下的 Info.plist 中"Launch screen interface file base name"字段的值,找到所指定的 xib 文件,加载渲染展示该视图。

闪屏页的展示是系统行为,因此无法控制;加载的是 xib 描述文件,无法定制动态展示逻辑,因此是静态展示。

对应技术启动阶段的 pre-main 阶段

T2(可选):欢迎页(广告)
App 运行后根据特定的业务逻辑展示的第一个页面。常见的有广告页和装机引导流程。

欢迎页是业务定制的,因此可根据业务需要优化展示策略,该阶段本身也是可选的。

T3:目标页 (落地页)
App 启动的目标页。

可以是首页或特定的落地页

目标页的加载渲染渲染完成标志着 T3 阶段的结束,也标志着启动流程的结束。

启动治理的最终目标是提升用户体验,在这样的思想下,本文关于启动流程的讨论主要围绕用户视角进行。

0X1

方法论及关键指标

APM 方法论

对 iOS 启动的治理,本质上是对应用性能优化 (App Performance Management) 的过程,其基本的方法论可以归纳为:

界定问题
准确描述现象,确定问题的边界

确定量化评价手段,明确关键指标

分析问题
分析问题产生的主要原因,根本原因

确定问题的重要性,优先级

性能问题可能是单点的短板,也可能是复杂的系统性问题,切忌「头痛医头,脚痛医脚」。要严谨全面地分析问题,找到主要原因、根本原因予以优先解决

解决问题
确定解题的具体技术方案

根据关键指标量化成果

对问题进行总结,积累沉淀

持续监控
性能问题是持续的,长期的

对关键技术指标建立长效的监控机制,确保增量能被及时反馈,予以处理

关键指标

  1. 启动耗时
    启动耗时是衡量启动性能的核心指标,因为它直接影响了用户体验并对用户转化率产生影响。

对启动耗时指标的拆解有助于细粒度地监控启动过程,帮助找到问题环节。具体可以拆解为:

技术启动耗时指标
pre-main

core-postmain

主观启动耗时指标
T1_duration : 从程序运行起点到主视窗可见

T2_duration

T3_duration

total_duration

根据对马蜂窝 App 用户的行为数据分析确认,我们得到以下结论:

启动耗时和启动流失率正相关

启动耗时和次日留存负相关

2.启动流失率
1). 如何定义启动流失
用户视角的启动流程完成前(即目标页渲染完成前),用户主动离开 App(进入后台,杀死 App, 切换到其他 App 等),记做 一次启动流失 。

启动流失率计算公式为:

启动 PV 流失率: 启动流失 PV / App 首次进入前台 PV

启动 UV 流失率: 启动流失 UV / DAU

UV 绝对流失率: 当日仅进入前台一次且流失的 UV / DAU

2) 如何定义首次进入前台
我们先来区分下 冷启动,热启动和首次进入前台 的概念:

iOS App 有后台机制,App 可在某些条件下,在用户不感知的情况下在后台启动(如后台刷新)。 由于用户不感知,如果当日该用户没有主动进入前台,则不会记作活跃用户。因此,单纯的后台启动不是启动流失率的分母。

但是当 iOS App 从后台启动,并留在内存中没有被操作系统清除,而一段时间后,用户触发 App 进入前台,这种情况虽然是热启动,但应被看作「首次进入前台」。

3) 如何定位流失的时机
根据定义,用户主动离开 App 则记作一次流失。从技术角度可以找到两个点:

applicationdidEnterBackground

applicaitonWillTerminate

但在实践的典型场景中我们发现,从用户点击 Home 键到程序接收到-applicationdidEnterBackground 回调存在一定的时间差,该时间差会影响到流失率的判断。

例如,用户在时刻 0.0s 启动 app,启动总时长为 4.0s。用户在时刻 3.8s 点击了 home 键离开 App,则应该记作 launch_leave = true。而程序在时刻 4.3s 接收到了-applicationDidEnterBackground 回调,此时启动已经结束,获得了启动耗时 4.0s。通过比较 Tleave > Tlaunch_total,则错误地记为 launch_leave = false。

由此推测,这里的 delay 是设置灵敏度阻尼,消除用户决策的摆动。这个延时大约在 0.5s 左右。

为了避免这个误差,我们的解决方案是利用 inactive 状态,找到准确的用户决策起点:

用户即将离开前台时,会先进入 inactive 状态,通过-appWillResignActive:拿到决策起点的时间戳 Tdetermine

根据用户最终决策行为,是否确实离开,再决定决策 Tdetermine 是否有效

最终根据有效的 Tdetermine 作为判断流失行为的标准,而不是-applicationdidEnterBackground 的时间点

  1. 启动广告曝光率
    广告是 App 盈利的主要手段之一。广告曝光率直接决定了广告点击消费率;而广告曝光 PV 和加载 PV 直接影响了广告售价。

我们定义:启动广告曝光率 = 启动广告曝光 PV / 启动广告加载 PV。

其中广告素材需要下载,素材渲染需要一定耗时,这些都会对广告曝光率产生影响。进一步来说,启动广告的曝光率会受到 App 启动性能的影响,但更主要的是受缓存和曝光策略的影响,详细阐述在下文「精细化策略」部分介绍。

0X2

iOS App 启动优化

以上,我们对 iOS App 启动治理的思路和关键指标进行了分析和拆解,下面来说一下从技术层面和业务层面,我们对启动性能的优化和流程治理分别做了哪些事情。

一、技术启动优化

  1. 优化pre-main

1). pre-main 主要流程分析
在进行该阶段的优化前,我们需要对 Pre-Main 阶段的过程有所了解,网上的文章较多,这里主要推荐两篇 WWDC 参考文章:

App Startup Time: Past, Present, and Future (https://developer.apple.com/videos/play/wwdc2017/413/)

Optimizing App Startup Time (https://developer.apple.com/videos/play/wwdc2016/406/)

总结来看,pre-main 主要流程包括:

fork 进程

加载 executable

加载 DYLD

分析依赖,迭代加载动态库

rebase

rebind

耗时多

  1. 准备环境

准备 OC 运行时

准备 C++环境

  1. main 函数

2). 优化建议
尽量少使用动态库

尽量编译到静态库中,减少 rebase,rebind 耗时

尽量合并动态库,减轻依赖关系

控制 Class 类的数量规模

由于 selector 需要在初始化时做唯一性检查,应尽量减少使用

少用 initializers

严格控制 +load 方法使用

多用 Swift

Swift 没有运行时

Swift 没有 initializers

Swift 没有数据不对齐问题

3). 性能监控:如何获取启动起点
启动的结束时间相对来说是比较好确定的,但如何定位启动的起点,是启动监控的一个难点。

对于开发环境,可以通过 Xcode 配置启动参数,获得 pre-main 的启动报告:

DYLD_PRINT_STATICS = 1
对于线上环境,根据 premain 主要流程的分析,我们的解决方案是:

创建动态库 ABootMonitor.dylib

ABootMonitor.dylib 实现+load 方法,记录启动起点时间

将 ABootMonitor.dylib 放在 executable 动态库依赖的头部

通过上述方法,可以在线上环境尽量地模拟出最早的启动时间点,从而更好地监测优化效果。

  1. 优化post-main
    post-main 阶段的技术优化主要针对两个方法的执行耗时来进行:

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:

  • (void)applicationDidBecomeActive:(UIApplication *)application;

为什么包含 2,需要我们对 iOS App 生命周期有一定理解。从操作系统的视角来看,iOS App 本质上是一个进程。对于 Mac OS/iOS 系统,进程的生命周期状态包括了:

not-running

running

进程激活,可以运行的状态

suspend

进程被挂起,不可以执行代码,通常在 UIApplication 进入后台后一段时间被系统挂起

zombie

进程回收前的临时状态,很短暂

terminated

进程终止,并被清理

而对于 UIApplication,定义了生命周期状态:

//  UIApplication.h

typedef NS_ENUM(NSInteger, UIApplicationState) {
    UIApplicationStateActive,     // 前台, UIApplication响应事件
    UIApplicationStateInactive,   // 前台, UIApplication不响应事件
    UIApplicationStateBackground  // 后台, UIApplication不在屏幕上显示
} NS_ENUM_AVAILABLE_IOS(4_0);

组合起来的状态机如下图:

通过上面的讨论,我们可以分析出以下问题:

UIApplication 会因为某种原因,在用户不感知的情况下被唤起,进程进入 running 状态,但停留在 iOS 的 background 状态

每次冷启动都会执行- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:,但未必进入前台

在 didFinishLaunchingWithOptions 中进行大量 UI 和网络请求等操作是不合理

post-main 优化思路和建议
整理拆分启动项,以启动项为粒度进行测量

启动项执行尽量在背景线程

启动的过程 CPU 占用较高,占用主线程会导致卡顿,耗时延长,用户体验不佳

启动项并发执行

启动项延迟执行

当 CPU 时间片跑满时,使用多线程并发不能提高性能,反而会因为频繁的线程上下文切换,造成 overhead 耗时增长

尽可能将启动项延迟执行,在时间轴上平滑,降低 CPU 利用率峰值

启动项分组

-didFinishLaunchingWithOptions 只执行必要的核心启动项

其他启动项,在首次调用-applicationDidBecomeActive:后执行

二、精细化策略

  1. 交互优化
    通过技术的实现手段,我们可以从客观上减少启动的绝对耗时。而从用户视角来看,对于启动是否流畅会受到很多心理因素的主观影响。因此从另一方面,我们可以从优化交互的角度提升用户体验。

避免阻塞等待

我们都希望用户可以尽快地使用 App,不要出现流失。但在快消费的时代,用户的耐心是极其有限的。

因此,如果有理由需要用户进行等待,就应该注意尽量避免产品流程是阻塞的。即使有更充足的理由必须让用户在阻塞状态原地等待,也应该给用户提供可响应的交互。

例如,在 T2 欢迎/广告页阶段,为了避免用户阻塞等待,应该提供明显的「跳过」按钮,允许用户进行跳过操作。

如果非要用户在这个阶段等待不可,也可以花一些小心思提供可响应的交互,比如点击触发视觉的变化等,不要让用户除了等待无事可做。

增加视觉信息量

增加屏幕上视图的信息量提供给用户消费,转移其注意力,降低用户对等待的感受。

例如,在 T1 闪屏页阶段,用户处于阻塞等待的状态,无法跳过。而且闪屏页是系统渲染的静态视图,我们无法提供动态响应。那么,我们可以通过在静态视图上提供更多信息量,给等待中的用户消费。

主观感受对比如下图:

合理的动态提示

合适的动画
事实上,早期在部分高性能 Android 设备上,App 的启动比同水平 iDevice 要快。但由于 iOS 设计了符合神经认知学的交互动画,使得主观感受到的时间缩短。

动画是否「合适」,关键在于对场景的选择和数量的把握。一个常见的动画耗时约为 0.25s,对于启动流程来说,已经可以解决或掩盖不少问题了。

合适的提示信息
好的交互体验和产品流程,至少应该是符合用户预期的。给以合适的动态提示,让用户知道此刻使用的 App 正在发生什么,可以极大地提升用户体验。

例如在 T2 广告页阶段,广告需要占时 3 秒钟的时间。交互上建议给与广告消失的倒计时提示:

一方面,倒计时提示可以有动态 loading 的视觉效果,展现 App 的良好运行;

另一方面,倒计时可以让用户安心,主观上耗时减少,情绪上不至于焦虑和退出。

  1. 基于场景的启动会话
    根据对启动过程的定义,我们可以列举出一些启动的「起点」和「终点」,比如:

启动触发点:
点击 App 图标正常启动

初次安装

点击 PUSH 进入

应用间跳转

3DTouch

Siri 唤起

其他

启动终点--目标页:
应用首页

指定的落地页

可以看出,启动的起点和终点多种多样,而对于启动流程的设定,很多都是和业务场景强相关的,比如:

初次安装需要进入装机引导流程

正常启动需要展示广告

PUSH 进入可以不展示广告,直达落地页

其他

如何才能维护这些复杂的启动关系,提高业务承载能力呢?我们的优化思路是基于场景创建启动会话:

由启动参数和其他条件确定启动场景

根据启动场景创建具体的启动会话

启动会话接管之后的启动流程

  1. 启动广告曝光和缓存策略
    广告曝光主要流程为: 请求广告接口 —> 准备广告素材 —> 展示广告页,进行曝光。

在准备广告素材环节,我们会判断广告素材是否命中缓存。如果命中则直接使用缓存,这样可以明显缩短广告加载的时间。如果没有命中,则开始下载广告素材。当广告素材超过设定的准备时长,则此次曝光不显示。

通过以往数据量化分析,我们发现通常情况下,广告未曝光的主要原因是由于广告素材准备超时,且素材体积和广告曝光率是负相关的。 为了保证广告的曝光率,我们应该尽量减少广告素材的体积,并且提高广告素材缓存的命中率。

下面分别介绍下我们的启动广告预缓存策略和启动广告曝光策略。

启动广告预缓存策略
广告素材接口和广告曝光接口分离

在可能的合适时机,下载广告素材

例如后台启动,后台刷新等

尽可能地提前下发广告素材

拉长广告素材投放的时间窗口

常见地可提前半月下发广告素材

对于 「 双十一等大促活动,应尽早地下发素材

启动广告曝光策略
分级的广告曝光QoS策略
若业务许可,可对广告优先级进行分级

对于低优先级,应用 cache-only 的曝光策略

对于普通优先级,应用 max-wait 的曝光策略

对于高优先级,应用 max-retry 的曝光策略

灵活的曝光时机选择
通常我们仅在首次进入前台时,进行广告曝光,但这有一定的缺陷:

启动耗时长了,用户体验差,启动流失率高

对于当日只有一次启动且启动流失的用户,丢了这个 DAU

我们可以在 App 首次进入前台,和热启动切回前台时选择时机,进行有策略的曝光

可依据策略,在首启时不展示广告页,提升用户体验,DAU,减少启动流失

可在 App 切回时展示,提升广告曝光 PV,和曝光率。

由于 App 之前已经启动,此时大概率已经缓存了广告素材

由于 App 一次生命周期存在多次切回前台,曝光 PV 可以得到提升

根据马蜂窝 App 的统计分析,在激进策略下可提升曝光 PV 约 4 倍

三、合理利用平台机制

iOS 经过多年的迭代,提供了很多智能的平台机制。合理利用这些机制,可以强化 App 的功能和性能。

  1. 内存保活
    我们已经讨论了冷启动和热启动的区别:

冷启动是进程并不存在的状态,一切需要从 0 开始。

热启动是指进程在内存中(iOS 不支持 SWAP),此时可能处于 background 的 running 状态或 suspend 状态,用户唤起进去前台。

热启动可以极大地减少 T1 闪屏页时间,从而减少启动耗时。

因此,我们应该尽量增加热启动概率,并且尽量减少 App 在后台被系统回收的概率。

iOS App 生命周期中关于系统内回收策略如下:

App 进入后台后,进程会活跃一段时间后,会被操作系统挂起,进入 suspend 状态。除非在 info.plist 指定进入后台即退出。

前台运行的 App 拥有内存的优先使用权

当前台的 App 需要更多物理内存时,系统根据一定策略,将一部分挂起的 App 进行释放

系统优先选择占用内存多的 App 进行释放

优化思路:
App 进入后台时,应该将内存资源竟可能的释放,尽量在内存中保活

尤其对于可重得的图片,文件等资源进行释放

对于可持久化的非重要内存,也可做持久化后释放

对于线上,应利用后台进程激活状态,加强对后台内存使用的监控

  1. 后台拉起
    iOS 系统提供了一些机制,可以帮助我们实现在用户不感知的情况下拉起 App。合适的拉起策略,可以优化 App 性能和功能表现,比如提升当日首启热启动的概率;在后台准备更新一些数据,如更新 PUSH token、准备启动广告素材等。

iOS 常见的后台拉起机制包括:

Background-fetch 后台刷新

需要权限

在某特定时机拉起,智能策略

PUSH

静默推送

远端推送

aps 中指定 "content-available = 1"

App 实现相关处理方法

地理围栏

后台网络任务 NSURLBackgroundSession

VOIP 等其他

使用后台机制时,有以下几点需要注意:

常见的后台机制需要 entitlement 声明和用户授权

部分节能模式会使部分拉起机制失效,节能模式不可用

拉起策略参考用户意图,用户主动杀死 App,会使部分拉起机制失效

正常进入后台,该 App 会向系统应用 「 AppSwitcher 」 注册,并受其管理

如果用户主动杀死 App,该 App 不会向 「 AppSwitcher 」 注册

后台拉起时,主要从 AppSwitcher 的注册列表选择 App 进行操作。 例如,后台刷新会根据某种策略排序,依此拉起 AppSwitcher 中注册的部分 App

批量拉起会导致服务端接口压力过大

例如使用 PUSH 拉起,则短时间内可能有数千万的 App 被拉起,此时接口请求不亚于一次针对服务端的 DDOS 攻击,需要整理和优化

四、结构化定制

页面栈/树优化
App 通过页面进行组织,在启动过程中,我们需要构建根页面栈。

由上分析我们知道,App 存在后台拉起,我们建议在首次进入前台时才进行页面渲染操作。但另一方面,根页面栈是 App 的基本结构,应该作为核心启动流程。因此我们提出以下解决方案:

涉及启动的页面,如首页、落地页等,应将页面栈创建、数据请求、页面渲染分离

在核心启动流程 (didFinishLaunch) 创建核心页面栈

在即将进入前台时,异步请求数据

在目标页即将展示时,进行渲染

例如,在广告页消失前的 1s,通知首页进行渲染,如下图

由于目标页可能和 T2 等启动阶段重叠,应特别注意页面加载的性能问题,避免交叉影响

0x3

结语

经过团队 3 个月的持续优化治理,马蜂窝 iOS App 的启动优化取得了一些成果:

启动耗时: 约 3.6s,减少约 50%

PV启动流失率: 降低约 30%

启动广告曝光率: 大幅提升

ios App 的启动治理乃至性能管理,是一个长期且艰巨的过程,需要各位开发同学具备良好的对平台和对代码性能的理解意识。其次,性能问题也常常是一个复杂的系统性问题,需要严谨地分析和推理,在此感谢支持以上工作的马蜂窝数据分析师。最后,这项工作需要建立完善的性能监控机制,持续跟踪,主动解决。

One

More Thing

我们计划于近期将马蜂窝 iOS 的启动框架开源,欢迎持续关注马蜂窝公众号动态。期待和大家交流。

本文作者:许旻昊,马蜂窝 iOS 研发技术专家。

2019/5/16 posted in  苹果开发

Python并发工具

转载于:http://mingxinglai.com/cn/2016/09/python-concurrent/

相信每一个接触Python并发的同学,刚开始都会有一段困惑时间:Python的并发看起来杂乱无章. 大家使用最多的是threading,但是,Python中由于GIL的存在,又没有实现真正的并发.因此, 在寻求其他并发工具的时候,Python往往由于资料太多,而大家又没有系统性的学习并发,导致 丈二的和尚摸不着头脑.

本文将分别讲解Python中常用的几个并发工具:

  1. 线程
  2. 进程
  3. 线程池/进程池
  4. 并发执行框架
  5. Python 3的asyncio
  6. 开源的gevent

1. Python的threading库

Python通过threading库提供了并发支持,有两种方式可以使用多线程:

新建Thread对象
继承Thread,然后实现run方法
Python中的多线程使用示例:

#!/usr/bin/python
#-*- coding: UTF-8 -*-
import os
from threading import Thread

def get_data(num):
    print('sum to {0} with pid {1}'.format(num, os.getpid()))
    return sum([i for i in range(num)])

def main():
    data = [ i for i in range(10) ]
    threads = []
    for num in data:
        thread = Thread(target=get_data, args=(num,))
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()

if __name__ == '__main__':
    main()

2. Python的multiprocessing库

multiprocessing is a package that supports spawning processes using an API similar to the threading module.

可以看到,为了让用户方便的使用多进程,减少用户代码的修改,multiprocessing的API与 threading的API是保持一致的.

例如,对于上面这个并发的例子,只需要将导入语句修改为下面的语句,就能够正常使用多进程:

from multiprocessing import Process as Thread

当然,实际使用中不推荐大家import Process as Thread,这里只是为了说明,这两个包的接口 是一样的,很方便大家进行切换.

3. 线程池/进程池

我们知道,Python里面有一个内置的map函数,该函数是函数式编程语言里面的概念,也是 map-reduce中的map,它的作用就是对一个序列中的每一个元素进行某种操作,例如,求整数的平方:

map(lambda x: x*x, [ i for i in range(5))
内置的map是单线程运行的,如果涉及到网络请求或者大量的cpu计算,则速度相对会慢很多,因此, 出现了并发的map,如下所示:

import requests
from multiprocessing import Pool

def get_website_data(url):
    r = requests.get(url)
    return r.url

def main():
    urls = ['http://mingxinglai.com',
            'http://www.baidu.com',
            'http://163.com']
    pool = Pool(2)
    print pool.map(get_website_data, urls)

main()

为了与线程兼容,该模块还提供了multiprocessing.dummy,用以提供线程实现,如下所示:

from multiprocessing.dummy import Pool

4. 并发执行框架

用过java的同学应该知道,java中有ThreadPoolExecutor框架,这类框架特别适合执行一些大批量 的异步任务.

Python3的concurrent.futures模块,提供了Executor框架,类似于java中的ThreadPoolExecutor, 不过,Python中支持两种Executor,分别是:

ThreadPoolExecutor
ProcessPoolExecutor
示例如下:

import concurrent.futures
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __name__ == '__main__':
    main()

如果要使用ThreadPoolExecutor,只需要修改一行代码即可:

with ThreadPoolExecutor(max_workers=1) as executor:

5. asyncio

Python 3.4 中asyncio被纳入了标准库,它提供了使用协程编写单线程并发代码,通过IO多路复用 技术访问套接字和其他资源,Python 3.5中添加了async和awit这两个关键字.自此,协程成为新的 语法,而不再是一种生成器类型.

import asyncio

async def slow_operation(n):
    await asyncio.sleep(1)
    print('Slow operation {} complete'.format(n))

async def main():
    await asyncio.wait([
        slow_operation(1),
        slow_operation(2),
        slow_operation(3),
        ])

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

6. gevent

gevent是一个基于微线程库greenlet的并发框架,其基本思想是:当一个greenlet遇到IO操作时, 比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行. 由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有 greenlet在运行,而不是等待IO.

虽然与直接使用greenlet、eventlet相比性能略低,但是,它提供了和线程模型编程相仿的接口, 而且提供了Monkey Patch方法,可以在运行时动态修改标准库里大部分的阻塞式系统调用, 如socket, threading, select等模块.

gevent的好处是Python 2和Python 3都可以使用,不像asyncio只能在python 3中使用.Gevent的使用示例如下:

import gevent

def foo():
    print('Running in foo')
    gevent.sleep(0)
    print('Explicit context switch to foo again')

def bar():
    print('Explicit context to bar')
    gevent.sleep(0)
    print('Implicit context switch back to bar')

gevent.joinall([
    gevent.spawn(foo),
    gevent.spawn(bar),
])

再强调一遍: 当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet, 等到IO操作完成,再在适当的时候切换回来继续执行.由于IO操作非常耗时,经常使程序处于等待 状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO.

所以上段代码的执行流程如下:

总结
本文概要的介绍了Python中的6个并发工具,大家可以通过本文,快速的了解各个工具的使用,选择 适合的工作.

2018/12/5 posted in  Python

API Star:一个 Python 3 的 API 框架

为了在 Python 中快速构建 API,我主要依赖于 Flask。最近我遇到了一个名为 “API Star” 的基于 Python 3 的新 API 框架。由于几个原因,我对它很感兴趣。首先,该框架包含 Python 新特点,如类型提示和 asyncio。而且它再进一步为开发人员提供了很棒的开发体验。我们很快就会讲到这些功能,但在我们开始之前,我首先要感谢 Tom Christie,感谢他为 Django REST Framework 和 API Star 所做的所有工作。

现在说回 API Star —— 我感觉这个框架很有成效。我可以选择基于 asyncio 编写异步代码,或者可以选择传统后端方式就像 WSGI 那样。它配备了一个命令行工具 —— apistar 来帮助我们更快地完成工作。它支持 Django ORM 和 SQLAlchemy,这是可选的。它有一个出色的类型系统,使我们能够定义输入和输出的约束,API Star 可以自动生成 API 的模式(包括文档),提供验证和序列化功能等等。虽然 API Star 专注于构建 API,但你也可以非常轻松地在其上构建 Web 应用程序。在我们自己构建一些东西之前,所有这些可能都没有意义的。

开始

我们将从安装 API Star 开始。为此实验创建一个虚拟环境是一个好主意。如果你不知道如何创建一个虚拟环境,不要担心,继续往下看。

pip install apistar

(译注:上面的命令是在 Python 3 虚拟环境下使用的)

如果你没有使用虚拟环境或者你的 Python 3 的 pip 名为 pip3,那么使用 pip3 install apistar 代替。

一旦我们安装了这个包,我们就应该可以使用 apistar 命令行工具了。我们可以用它创建一个新项目,让我们在当前目录中创建一个新项目。

apistar new .

现在我们应该创建两个文件:app.py,它包含主应用程序,然后是 test.py,它用于测试。让我们来看看 app.py 文件:

from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls
def welcome(name=None):
    if name is None:
        return {'message': 'Welcome to API Star!'}
    return {'message': 'Welcome to API Star, %s!' % name}
routes = [
    Route('/', 'GET', welcome),
    Include('/docs', docs_urls),
    Include('/static', static_urls)
]
app = App(routes=routes)
if __name__ == '__main__':
    app.main()

在我们深入研究代码之前,让我们运行应用程序并查看它是否正常工作。我们在浏览器中输入 http://127.0.0.1:8080/,我们将得到以下响应:

{"message": "Welcome to API Star!"}

如果我们输入:http://127.0.0.1:8080/?name=masnun

{"message": "Welcome to API Star, masnun!"}

同样的,输入 http://127.0.0.1:8080/docs/,我们将看到自动生成的 API 文档。

现在让我们来看看代码。我们有一个 welcome 函数,它接收一个名为 name 的参数,其默认值为 None。API Star 是一个智能的 API 框架。它将尝试在 url 路径或者查询字符串中找到 name 键并将其传递给我们的函数,它还基于其生成 API 文档。这真是太好了,不是吗?

然后,我们创建一个 Route 和 Include 实例的列表,并将列表传递给 App 实例。Route 对象用于定义用户自定义路由。顾名思义,Include 包含了在给定的路径下的其它 url 路径。

路由

路由很简单。当构造 App 实例时,我们需要传递一个列表作为 routes 参数,这个列表应该有我们刚才看到的 Route 或 Include 对象组成。对于 Route,我们传递一个 url 路径,http 方法和可调用的请求处理程序(函数或者其他)。对于 Include 实例,我们传递一个 url 路径和一个 Routes 实例列表。

路径参数

我们可以在花括号内添加一个名称来声明 url 路径参数。例如 /user/{user_id} 定义了一个 url,其中 user_id 是路径参数,或者说是一个将被注入到处理函数(实际上是可调用的)中的变量。这有一个简单的例子:

from apistar import Route
from apistar.frameworks.wsgi import WSGIApp as App
def user_profile(user_id: int):
    return {'message': 'Your profile id is: {}'.format(user_id)}
routes = [
    Route('/user/{user_id}', 'GET', user_profile),
]
app = App(routes=routes)
if __name__ == '__main__':
    app.main()

如果我们访问 http://127.0.0.1:8080/user/23,我们将得到以下响应:

{"message": "Your profile id is: 23"}

但如果我们尝试访问 http://127.0.0.1:8080/user/some_string,它将无法匹配。因为我们定义了 user_profile 函数,且为 user_id 参数添加了一个类型提示。如果它不是整数,则路径不匹配。但是如果我们继续删除类型提示,只使用 user_profile(user_id),它将匹配此 url。这也展示了 API Star 的智能之处和利用类型和好处。

包含/分组路由

有时候将某些 url 组合在一起是有意义的。假设我们有一个处理用户相关功能的 user 模块,将所有与用户相关的 url 分组在 /user 路径下可能会更好。例如 /user/new、/user/1、/user/1/update 等等。我们可以轻松地在单独的模块或包中创建我们的处理程序和路由,然后将它们包含在我们自己的路由中。

让我们创建一个名为 user 的新模块,文件名为 user.py。我们将以下代码放入这个文件:

from apistar import Route
def user_new():
    return {"message": "Create a new user"}
def user_update(user_id: int):
    return {"message": "Update user #{}".format(user_id)}
def user_profile(user_id: int):
    return {"message": "User Profile for: {}".format(user_id)}
user_routes = [
    Route("/new", "GET", user_new),
    Route("/{user_id}/update", "GET", user_update),
    Route("/{user_id}/profile", "GET", user_profile),
]

现在我们可以从 app 主文件中导入 user_routes,并像这样使用它:

from apistar import Include
from apistar.frameworks.wsgi import WSGIApp as App
from user import user_routes
routes = [
    Include("/user", user_routes)
]
app = App(routes=routes)
if __name__ == '__main__':
    app.main()

现在 /user/new 将委托给 user_new 函数。

访问查询字符串/查询参数
查询参数中传递的任何参数都可以直接注入到处理函数中。比如 url /call?phone=1234,处理函数可以定义一个 phone 参数,它将从查询字符串/查询参数中接收值。如果 url 查询字符串不包含 phone 的值,那么它将得到 None。我们还可以为参数设置一个默认值,如下所示:

def welcome(name=None):
    if name is None:
        return {'message': 'Welcome to API Star!'}
    return {'message': 'Welcome to API Star, %s!' % name}

在上面的例子中,我们为 name 设置了一个默认值 None。

注入对象
通过给一个请求程序添加类型提示,我们可以将不同的对象注入到视图中。注入请求相关的对象有助于处理程序直接从内部访问它们。API Star 内置的 http 包中有几个内置对象。我们也可以使用它的类型系统来创建我们自己的自定义对象并将它们注入到我们的函数中。API Star 还根据指定的约束进行数据验证。

让我们定义自己的 User 类型,并将其注入到我们的请求处理程序中:

from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar import typesystem
class User(typesystem.Object):
    properties = {
    'name': typesystem.string(max_length=100),
    'email': typesystem.string(max_length=100),
    'age': typesystem.integer(maximum=100, minimum=18)
    }
    required = ["name", "age", "email"]
def new_user(user: User):
    return user
routes = [
    Route('/', 'POST', new_user),
]
app = App(routes=routes)
if __name__ == '__main__':
    app.main()

现在如果我们发送这样的请求:

curl -X POST \
  http://127.0.0.1:8080/ \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{"name": "masnun", "email": "masnun@gmail.com", "age": 12}'

猜猜发生了什么?我们得到一个错误,说年龄必须等于或大于 18。类型系允许我们进行智能数据验证。如果我们启用了 docs url,我们还将自动记录这些参数。

发送响应

如果你已经注意到,到目前为止,我们只可以传递一个字典,它将被转换为 JSON 并作为默认返回。但是,我们可以使用 apistar 中的 Response 类来设置状态码和其它任意响应头。这有一个简单的例子:

from apistar import Route, Response
from apistar.frameworks.wsgi import WSGIApp as App
def hello():
    return Response(
    content="Hello".encode("utf-8"),
    status=200,
    headers={"X-API-Framework": "API Star"},
    content_type="text/plain"
    )
routes = [
    Route('/', 'GET', hello),
]
app = App(routes=routes)
if __name__ == '__main__':
    app.main()

它应该返回纯文本响应和一个自定义标响应头。请注意,content 应该是字节,而不是字符串。这就是我编码它的原因。

继续

我刚刚介绍了 API Star 的一些特性,API Star 中还有许多非常酷的东西,我建议通过 Github Readme 文件来了解这个优秀框架所提供的不同功能的更多信息。我还将尝试在未来几天内介绍关于 API Star 的更多简短的,集中的教程。

via: http://polyglot.ninja/api-star-python-3-api-framework/

作者:MASNUN 译者:MjSeven 校对:wxy

2018/12/3 posted in  Python

树莓派定时任务

https://www.kawabangga.com/posts/1398

Linux下有一个定时运行的程序命令叫“crontab”,是任务调度的crond常驻命令,是Linux系统下的定时任务触发器 。
限制用户使用crontab的文件有:/etc/cron.allow /etc/cron.deny 。
当使用crontab建立工作排程后,将被记录到/var/spool/cron里。
cron执行的每一项工作都被记录到/varlog/cron里去。
crontab参数:
-u:只有root才可能,帮其他用户建立或移除工作排程。
-l:查阅crontab的工作内容
-r:移除所有的crontab的工作内容,移除一项,用-e编辑。
每项工作有六个字段分别是:
分钟 小时 日期 月份 周 指令
0-59 0-23 1-31 1-12 0-7 指令 #0和7都代表星期天
辅助特殊字符:

  • (星号)代表任何时刻
    ,(逗号)代表分隔时候。如3点与6点 就是3,6
    -(减号)代表一段时间范围内。如:3点到6点 就是3-6
    /n(斜线)n代表数字,即每隔n单位。如每隔五分钟,/5
    以下我们举个例子,比如每晚定时23:50分需要关机
    1.编辑crontab 任务:
    $crontab -e
    2.再文件的最后一行添加以下这行内容:
    50 23 * * * /sbin/shutdown -h now

注:50 23 代表 每天的23:50,执行的命令就是“shutdown -h now”

crontab命令
crontab命令常见于Unix和类Unix的操作系统之中,用于设置周期性被执行的指令。该命令从标准输入设备读取指令,并将其存放于“crontab”文件中,以供之后读取和执行。该词来源于希腊语 chronos(χρνο),原意是时间。常,crontab储存的指令被守护进程激活, crond常常在后台运行,每一分钟检查是否有预定的作业需要执行。这类作业一般称为cron jobs。

使用说明
只有root用户和crontab文件的所有者才能编辑定时任务,因此如果以pi用户登录,不要忘记加上sudo。-e参数表示编辑(edit)。
sudo crontab -e
进入编辑以后需要按照一定的格式写入所需执行的命令和重复的时间。格式如下:
m h dom mon dow command
依次是分钟(minute)、小时(hour)、几号(day of month)、月份(month)、星期几(day of week)、命令。
时间可以是一个数字,表示在这个时刻执行,也可以是星号(*),表示不做限制、在任意时刻都执行。
查看所有的定时任务可以使用-l参数,表示列出(list)的含义
crontab -l

用法举例
每天0点1分执行贴吧签到脚本
1 0 * * * python qiandao.py
在每周日的7点更新系统
0 7 * * 1 apt-get update && sudo apt-get upgrade -y

2018/11/28 posted in  Raspbian

Mac OSX下给树莓派安装Raspbian系统

2018/11/26 posted in  Raspbian

iOS获取App ipa包以及资源文件

LINK:https://www.jianshu.com/p/fdb50d303ad6

要获得线上APP的ipa文件,现在有以下几种方案
1.通过PP助手下载安装到手机的应用
2.通过iTools助手下载安装到手机的应用
3.通过Apple Configurator 2(Mac商店)获取
前两种方案网上的教程很多,这里只介绍第三种方案

首先 去Mac上的App Store下载Apple Configurator 2。
然后把iphone连接上Mac,点击Apple Configurator 2 菜单中->账户->登陆(用连接设备的Apple ID)

打开登录.png

备份iPhone的内容(避免数据丢失,非必选)

所有设备->选中当前iPhone->添加->应用,找到您想要ipa的那个应用->添加

添加应用.png

添加.png

下载ipa包中.png
因为你手机中已经存在了当前应用,所以会提示,该应用已经存在, 是否需要替换?
此时,不要点任何按钮!不要点任何按钮!不要点任何按钮!

不要点击任何按钮.png
不要操作Apple Configurator 2,让它保持上图的状态,然后打开Finder前往文件夹,或者直接快捷键command+shift+G
并输入下面路径
~/Library/Group Containers/K36BKF7T3D.group.com.apple.configurator/Library/Caches/Assets/TemporaryItems/MobileApps/

前往文件夹.png

前往文件夹2.png
点击前往,打开ipa包所在文件。将ipa文件copy出来。

获取ipa包所在文件夹.png
这时候别忘了点击Apple Configurator 2窗口中的停止,你会发现刚才目录下的文件也消失了

拿到ipa文件后,你可以将后缀.ipa改为.zip,然后解压


修改文件类型.png

就可以看到Payload下的包,显示包内容可以看到部分APP的资源以及Assets.car。

解压.png
如果你要解压Assets.car, 可以使用github上的工具https://github.com/pcjbird/AssetsExtractor

最后,提取出来的资源文件,大家要注意版权,仅供参考,不要直接拿来商业使用。

2018/11/13 posted in  杂七杂八

iOS RSA加密 以及生成公钥 秘钥 pem文件

在iOS中使用RSA加密解密,需要用到.der和.p12后缀格式的文件,其中.der格式的文件存放的是公钥(Public key)用于加密,.p12格式的文件存放的是私钥(Private key)用于解密. 首先需要先生成这些文件,然后再将文件导入工程使用,不多说,开始做!
一、使用openssl生成所需秘钥文件
  生成环境是在mac系统下,使用openssl进行生成,首先打开终端,按下面这些步骤依次来做:

  1. 生成模长为1024bit的私钥文件private_key.pem

openssl genrsa -out private_key.pem 1024

  1. 生成证书请求文件rsaCertReq.csr
    openssl req -new -key private_key.pem -out rsaCerReq.csr
    注意:这一步会提示输入国家、省份、mail等信息,可以根据实际情况填写,或者全部不用填写,直接全部敲回车.
  2. 生成证书rsaCert.crt,并设置有效时间为10年
    openssl x509 -req -days 3650 -in rsaCerReq.csr -signkey private_key.pem -out rsaCert.crt
  3. 生成供iOS使用的公钥文件public_key.der
    openssl x509 -outform der -in rsaCert.crt -out public_key.der
  4. 生成供iOS使用的私钥文件private_key.p12
    openssl pkcs12 -export -out private_key.p12 -inkey private_key.pem -in rsaCert.crt
    注意:这一步会提示给私钥文件设置密码,直接输入想要设置密码即可,然后敲回车,然后再验证刚才设置的密码,再次输入密码,然后敲回车,完毕!
    在解密时,private_key.p12文件需要和这里设置的密码配合使用,因此需要牢记此密码.
  5. 生成供Java使用的公钥rsa_public_key.pem
    openssl rsa -in private_key.pem -out rsa_public_key.pem -pubout
  6. 生成供Java使用的私钥pkcs8_private_key.pem
    openssl pkcs8 -topk8 -in private_key.pem -out pkcs8_private_key.pem -nocrypt
    全部执行成功后,会生成如下文件,其中public_key.der和private_key.p12就是iOS需要用到的文件
2018/10/10 posted in  Crypto