python 虚拟环境pyenv

From: https://strikefreedom.top/management-multi-pythons

::提前安装 tcl-tk => brew install tcl-tk : 一个UI绘制的库, 先安装后安装 python
虚拟环境的python 会编译这个UI库

系统版本:Mac OS X El Capitan(10.13)
预先安装:homebrew 安装方法:运行ruby脚本:

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

pyenv依赖:python 2.5+ , git

推荐使用pyenv-installer这个插件安装pyenv,这种方式安装会多安装几个是实用的插件,比如:

pyenv-virtualenv 用于整合virtualenv

pyenv-pip-rehash 用于使用pip安装包之后自动执行rehash

pyenv-update 用于升级pyenv

使用python-installer方式安装:确保你的电脑可以访问Github,然后在终端运行:


curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash

即可自动安装pyenv,安装完成后,pyenv默认安装地址是:/.pyenv,但是如果你的系统的环境变量中存在PYENV_ROOT则会被安装到PYENV_ROOT目录下(当使用homebrew等其他工具安装后)信息,在你电脑使用的对应的shell的配置文件中添加环境变量(如使用的shell是bash则在~/.bashrc中添加,若是zsh则在~/.zshrc中添加,etc):

export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
#正确的 在切换到虚拟环境后 执行source ~/.bashrc 后python 版本会切换成功
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv virtualenv-init -)"

注意更新 source ~/.bashrc

至此,完成pyenv的安装和基本配置。

ps:pyenv默认安装地址是~/.pyenv,但是如果你的系统的环境变量中存在PYENV_ROOT则会被安装到PYENV_ROOT目录下(当使用homebrew等其他工具安装后)

系统运行python的时候会检测环境变量PATH, 如在以下路径查找:

/usr/local/bin:/usr/bin:/bin

系统会检测/usr/local/bin是否存在python,如果不存在则继续搜索/usr/bin,以及类推 pyenv的工作原理是在PATH中添加shims目录:

~/.pyenv/shims:/usr/local/bin:/usr/bin:/bin

pyenv的卸载比较简单,只需要删除对应的目录即可,默认目录是:~/.pyenv。

查看可安装的包:

$ pyenv install -l

查看已安装的python:

versions和version命令

versions命令列出你已经安装的Python版本以及当前使用的版本

pyenv versions

版本名称为system代表的是系统预装的Python版本。

安装3.9.0:

$ pyenv install 3.9.0
$ pyenv rehash (这一步是更新pyenv中的python版本数据,每次安装新的python版本后需执行此命令。)

安装指定版本的python只需使用pyenv install 版本号即可。

卸载python版本:

使用pyenv uninstall {版本号}

1pyenv rehash

切换当前目录的python版本:

切换和使用指定的版本Python版本有3种方法:

global和local命令:

global命令和local命令都是用来切换当前Python版本的命令。不同之处在于,global的切换是全局的,而local的切换是局部的。

pyenv local 3.9.0

以上命令:会在当前目录下创建一个.pyenv-version文件,文件内容为3.9.0,pyenv通过这种形式,标记当前目录使用Python-3.9.0。如果其子目录下面没有.pyenv-version文件,那么此版本讲继承到子目录。

pyenv global 3.9.0

以上命令:会修改$PYENV_HOME/version文件的内容,标记全局Python版本,如何理解全局Python版本,可以认为全局版本是一个默认值,当一个目录及其父目录下面都没有.python-version文件的时候,会使用全局版本。

一般的,我们不修改全局版本,而使用期默认值system,因为在unix系统上,很多系统工具依赖于Python,如果我们修改了Python的版本,会造成绝大多数的依赖Python的系统工具无法使用,如果你不小心修改了,也不要紧张,使用global命令修改回来就可以了,有时候,你发现部分系统工具无法使用,你也可以看看你当前的Python版本。

系统全局用系统默认的Python比较好,不建议直接对其操作

pyenv global system

用local进行指定版本切换,一般开发环境使用。

pyenv local 3.9.0

对当前用户的临时设定Python版本,退出后失效

pyenv shell 3.5.1

取消某版本切换

pyenv local 3.5.1 --unset

优先级关系:shell —> local —> global

virtualenv用于创建独立的Python环境,多个Python相互独立,互不影响,它能够:

  • 在没有权限的情况下安装新套件
  • 不同应用可以使用不同的套件版本
  • 套件升级不影响其他应用

Virtaulenvwrapper是virtualenv的扩展包,用于更方便管理虚拟环境,它可以做:

  • 将所有虚拟环境整合在一个目录下
  • 管理(新增,删除,复制)虚拟环境
  • 切换虚拟环境

1、安装和使用方法

安装

pip install virtualenv
pip install virtualenvwrapper

创建虚拟环境

mkvirtualenv \[虚拟环境名称\]

此时还不能使用virtualenvwrapper,默认virtualenvwrapper安装在/usr/local/bin下面,实际上你需要运行virtualenvwrapper.sh文件才行,先别急,打开这个文件看看,里面有安装步骤,我们照着操作把环境设置好。

如果你使用的bash或者zsh

创建目录用来存放虚拟环境

mkdir $HOME/.virtualenvs

在. bash_profile 或者 .zshrc 追加两行:

export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh

运行:

source ~/.bashrc
source ~/.zshrc

此时virtualenvwrapper就可以使用了。

2、创建虚拟环境例如:

在当前的环境的Python版本下创建名称为py3dev的虚拟环境。

mkvirtualenv py3dev

默认情况下,虚拟环境会依赖系统环境中的site packages,就是说系统中已经安装好的第三方package也会安装在虚拟环境中,如果不想依赖这些package,那么可以加上参数 –no-site-packages建立虚拟环境

例如:

mkvirtualenv --no-site-packages \[虚拟环境名称\]
mkvirtualenv --no-site-packages py3dev

3、查看创建的虚拟环境:

 λ ~/ lsvirtualenv

 py2dev

 ======

 py3dev

 ======

λ ~/ workon

py2dev

py3dev

4、启动某虚拟环境:

works \[虚拟环境名称\]
workon py3dev

5、删除某个虚拟环境:

rmvirtualenv \[虚拟环境名称\]
rmvirtualenv py3dev

6、删除某个虚拟环境需要先退出这个环境:

deactivate

如何对Python3.9.0和Python3.5.1版本分别创建虚拟环境?

有这三个工具其实非常简单,主要是确保环境切换成功后在创建虚拟环境。

为确保切换成功,我建议 source .zshrc 一下在切换(如果是使用pyenv-installer安装的,则不需要,若是直接安装的pyenv则需要,具体原因仍不清楚。)

 #安装全新的Python3.9.0版本
 pyenv install 3.9.0
 pyenv rehash

 #切换到刚安装的这个版本
 pyenv local 3.9.0
 #确保切换成功
 source .zshrc
 #验证一下版本,pip发现里面包很少
pip list
#验证版本
python -V
#务必在这个新的3.9.0中安装
pip install virtualenv
pip install virtualenvwrapper
#务必
source .zshrc
#创建3.9.0的开发环境
mkvirtualenv py2dev
#创建完某版本的开发环境后务必退出,当前虚拟环境,不然就是虚拟环境中在创建了。
deactivate
#退出3.9.0环境
pyenv local --unset 3.9.0
source .zshrc

3.5.1的虚拟环境创建也是一样,因此验证3.9.0和3.5.1的虚拟环境

(py3dev) ~  deactivate
~  workon py2dev
(py2dev)~  python -V
Python 3.9.0
(py2dev)~  deactivate
~  workon py3dev
(py3dev)~  python -V
Python 3.5.1
(py3dev)~  deactivate
~

#愉快无痛升级,一键升级所有PIP包

pip list --outdated | grep '^\[a-z\]* (' | cut -d " " -f 1 | xargs pip install -U

pyenv通过插件,可以很好的和virtualenv一起工作,通过整合virtualenv,pyenv实现了真正意义上的环境隔离,每个项目都相当于使用一个单独的解释器。

通过pyenv-installer安装的pyenv,已经安装好virtualenv插件了,如果不是通过pyenv-installer安装的pyenv,你可能需要自己安装virtualenv插件,安装方法也很简单:

cd $PYENV_ROOT/plugins
git clone https://github.com/yyuu/pyenv-virtualenv.git

直接把插件clone下来就安装完成了。
安装完成之后,我们可以通过virtualenv命令即可创建虚拟环境,virtualenv的一般用法如下:

pyenv virtualenv \[-f|--force\] \[-u|--upgrade\] \[VIRTUALENV_OPTIONS\] <version> <virtualenv-name>

选项-f表示强制的,也就是如果已经存在这个虚拟环境,那么将会覆盖这个虚拟环境 选项-u表示upgrade,用于修改已经存在的虚拟环境的Python版本 VIRTUALENV_OPTIONS 是传递给virtualenv的选项,可以通过virtualenv的帮助获取选项的含义 version 表示Python版本 virtualenv-name 是我们给虚拟环境指定的名字

例如:

pyenv virtualenv 3.9.0 my_project

以上命令就创建了一个基于Python-3.9.0,名为my_project的虚拟环境。创建好的虚拟环境犹如一个单独Python版本一样,我们可以通过local或者global命令切换过去。

由于每个解释器间是完全隔离的,所以强烈建议你的每个项目,都放置在单独的虚拟环境中。

virtualenv插件还提供了virtualenvs命令,用于列出所有已经创建的虚拟环境,

pyenv virtualenvs

以上命令列出我们所有已经创建的虚拟环境,已经虚拟环境基于那个Python版本。
当我们的一个项目生命周期结束的时候,我们或许会想要删除虚拟环境以释放我们的硬盘空间,删除虚拟环境非常简单,直接用uninstall命令像删除正常的Python版本一样就可以了。

事实上,虚拟环境一旦创建,你就可以把他当成一个独立的版本来使用和维护了。

删除、设置当前目录python以及取消设置版本使用下面的命令

# 创建一个基于`python 2.7.10`版本的名字叫做`env27`的虚拟环境, 使用`--no-site-packages`参数来隔离系统的site-packages
$ pyenv virtualenv 2.7.10 env27 --no-site-packages
# 指定目录使用evn27的虚拟环境
$ cd code
$ pyenv local env27
# 取消设定
$ pyenv local env27 --unset
# 删除
$ pyenv uninstall env27

记录一次pod 私有组件,导入第三方framework的问题

记录一次pod 私有组件,导入第三方framework的问题

1 起因: UM 的超链裂变是beta 版本 需要文件导入, 本身项目是一个组件化项目,所以um的framework 不能导入在主项目中,

按照调理接入

1
2
3
4
5
6
7
8
# 私有库加入
s.vendored_frameworks = [
"xxx/Source/UM/common/common_ios_7.2.7/UMCommon.framework",
"xxx/Source/UM/common/common_ios_7.2.7/UMRemoteConfig.framework",
"xxx/Source/UM/common/common_ios_7.2.7/UMDevice.framework",
"xxx/Source/UM/link/link_ios_1.0.0/UMLink.framework",
] #自己的framework在工程中的路径

然后发现头文件重复导入, 和一些奇怪的找不到的问题 然后去处理头文件, 后发现 -umbrella.h 把 一些头文件按照”xxx.h” 方式导入,造成找不到的问题, 然后使用

1
s.private_header_files = ['xxx/Source/UM/common/common_ios_7.2.7/**/*.h','xxx/Source/UM/**/*.h']

私有头文件, -umbrella.h 中不会有多余的头文件了, 运行, 成功, 但是 主要是因为 主项目用的 use_modular_headers!
非 use_frameworks! 如果是 use_frameworks! 还需要做一个头文件搜索路径

https://www.jianshu.com/p/dfe9a1e1db7f 这个地址有详细说明, 后续没有继续去处理

flutter 基础知识

From: https://www.jianshu.com/p/1481fce28a02

一. Dart入口方法

每一个flutter项目的lib目录里面都有一个main.dart。这个文件就是flutter的入口文件,其中的main方法是dart的入口方法。
runApp返回的是组件。MyApp是自定义的一个组件。在flutter里面万物皆组件。

# void是当前函数的返回值 可以省略
void main() {//程序执行的入口函数 有且仅有一个 必须的
   runApp(MyApp)
   print('Hello World!')//dart通过print方法打印数据
}
//如果runApp后面只有一行代码也可以这样写
void main() => runApp(MyApp());


//以模块接入原生的, 可以用 在入口文件中新建新的入口函数, 前面加入  @pragma('vm:entry-point')
例:
 @pragma('vm:entry-point') void mainWallet(){} 

二. Dart变量,常量和命名规则

  1. 变量
    dart是一个强大的脚本类语言,可以不预先定义变量类型,dart会通过类型推断 type inference(swift也是)自动判断当前变量的类型。dart中定义变量是通过var关键字或者通过类型来声明变量。

    var str = ‘这是var变量’;
    String str1 = ‘这是类型声明的变量’;
    int number = 123;
    注意: var 后不要写类型,写了类型不要写var 写了会报错 var a int = 5

  1. 常量

常量通过final或者const修饰。

差异之处:const修饰的常量在创建时就需要赋值(编译时的常量)赋值后不可改变。final不仅有const的编译时的常量特性,最重要的是它的运行时常量。并且final是惰性初始化,即在运行时第一次使用前才初始化。如果是不改量的量,请使用final或者cont修饰它,而不是使用var或者其他变量类型。

final name = ‘huang’;
final String rename = 'hai';
const bar = 100000; 
cont double atm = 1.01325 * bar 
  1. dart的命名规则
    • 变量等名称必须由数字、字母、下划线和美元符号($)组成。
    • 注意:标识符开头不能是数字。
    • 标识符不能使保留字或者关键字。
    • 变量的名称是区分大小写的如:age和Age是不同的变量。在实际的运用中,也建议不要用一个
    • 标识符(变量名称)一定要见名思意 :变量名称建议用名称,方法名称建议用动词

三. Dart基础数据类型

  • 字符串(String)

① 字符串创建方式
通过var关键字或者String单引号、双引号、三个单引号或者三个双引号来包裹字符组成字符串变量或者常量。
②字符串拼接,通过+号或者通过 $加变量 $str或者${表达式}

对应的①
/单引号 '' 或者 双引号 "" 来包裹字符组成字符串
  String str1 = '这是字符串';
  String str2 = "这是字符串";
  //使用 + 链接两个字符串
  String str3 = str1 + str2;
  print(str3);
  //多行字符串使用三重单引号 '''字符串'''
  //三重双引号 """字符串"""
  String str4 = """ 1111
  22222
  333333""";
  print(str4);
  //使用r前缀 可以使字符串里面的特殊字符作为普通字符串
//  \n 换行符 特殊字符
  String str5 = r'这是特殊字符\n现在不会换行';
  String str6 = '这是特殊字符\n现在会换行';
  print(str5);
  print(str6);
对应的②
var str4 = str + str1;
print('$str4 ${str4.length}');
// dart判断数据类型 is关键词用来判断数据类型
  if(str4 is String){
    // print()
  }
  • num(数字)

num是数字类型的父类,有两个子类intdouble

* int 整型 数值范围在-2的53次方到2的53次方减1
* double双精度浮点型
* int 必须是整型 double 既可以接收整型也可以接收浮点型

//print函数是控制套输出函数
  print('a 是 $a');
  print('你好dart');
  double c = 10;//double可以包含整型 整型 dart sdk 2.1 之前不能使用,会报错
  double d = 10.1;
  print('d = $d');

  num aa = -3; //num 是number类型的简写  abs()取绝对值
  print('asdasdas' + aa.abs().toString());
//ceil() 带小数就向前进一位取大于或者等于表达式的最小整数
  num h = 8.3;
  num i = h.ceil();
  print(i);
//floor()舍掉小数位 不管他多大
num j = 10.9;
num k = j.floor();
//round()四舍五入
  • boolean

    • 布尔 关键字 bool 值 true 或者 false
    • 布尔类型的合法值只有两个 true 和 false 在条件判断语句中dart不会对条件语句进行类型转换
    • 与其他语言不同 例如 JavaScript 中 布尔是非空都为true dart中只有值为true才可以 比如就算一个字符串不是空 但是他的返回值不是bool值 所以是false

    bool value = true;
    // bool value2 = ‘asda’;//不合法
    bool value2 = false;
    if(value){

    print(value);
    

    }

四. Dart集合

  • List集合(数组)

    //第一种定义List的方式
    var list222 = [“asdsa”,”asdasd”,”asdsadff”];
    list222.length; 获取长度
    list222[0];获取第一个值
    //第二种定义List的方式
    var list111 = List();
    list111.add(‘asdsdddda’);//增加新值
    list111.addAll([‘hahah’,’asdasd’]); 可以添加新的数组
    //在定义List时 直接指定其类型
    var List2223 = List();//定义了一个只能放String类型的数组 调用其构造函数创建数组
    //数组 里面的元素(值)可以重复 值的类型可以不同
    List list1 = [1,2,3,4,5,6,7,8];
    List list2 = [1,true,’qwe’,1];

  • List里面常用的属性和方法 Map Set差不多通用

    //常用属性
    length 长度
    reversed 数组翻转 对列表倒序排序
    isEmpty 是否为空
    isNoTEmpty 是否不为空
    //常用方法
    add 增加元素 增加一个
    addAll 拼接数组
    indexOf 查找数据 传入具体值 查到了返回元素所在位置索引 查找不到返回-1
    remove 删除 传入具体值
    removeAt 删除 传入索引值
    fillRange 修改数据 传入开始索引和结束索引 然后传入要添加的值(1,10 ,’修改的值’);
    insert(index,value); 指定位置插值 (1 要添加的位置在索引为1的元素前添加 ,’要添加的值‘)
    insertAll(index,List); 指定位置插入List 和上面的一样 只是传入的是数组
    toList() 其他类型转换为List
    join() List转换为字符串 传入变成字符串后的分割元素之间的符号
    split() 字符串转换为List 传入通过哪些符号来分割字符串 变成List
    forEach
    map
    where
    any
    every

  • Maps(字典) 也可以叫对象是无序的键值对

    • 常用属性:
      • keys 获取所以key值
      • values 获取所有Value值
      • isEmpty 是否为空
      • isNotEmpty是否不为空
    • 常用方法
      • remove(key) 删除指定key的数据
      • addAll({….}) 合并映射字典 给映射增加属性
      • containsValue 查看字典内是否有某个值 返回true false

    //将key和Value相关联的对象
    //key和Value都可以是任何对象
    //定义方式 map字面量来定义
    var person = {
    “name” : “zhangsna”,
    “age” : 20,
    “work” : [‘haha’,’hehe’]
    };
    //可以直接添加或者修改值
    person[“name”]; //取值
    var per = Map();
    // Map类型
    Map dic = {‘name’: ‘zhangsna’, ‘age’: 20};
    Map dic2 = new Map();//dart2.0版本后可以不用写new关键字
    var dic3 = new Map();
    //从map中取值 如果map对象中没有对应的键 返回null
    print(tempMap2[‘sencond’].toString());
    // .length 取得map对象的长度

  • Set

集合里面的值不可以重复,值的类型必须统一。最主要的功能是去除数组中重复的内容。Set是没有顺序且不能重复的集合,所以不能通过索引去获取值。

定义有两种形式 Set字面量 Set类型
  var s = Set();
  var set1 = {'value1','value2'};//Set字面量
  //Set类型
  //变量类型 决定
  Set set2 = {};
  //类型参数
  var set3 = <String>{};//指定元素为String类型的Set集合
  Set<String> set4 = {};
  //.add()添加至到集合
  set4.add('value1');
  print(set4);
//  .addAll 将元素为同类型的集合添加到现有集合
  set4.addAll(set1);
  print(set4);
// .length 得到集合的长度 里面多少个元素或者值
  print(set4.length.toString());
  • 常用方法

①. forEach() 传入函数表达式 遍历

list.forEach((value){ 遍历数组 会把数组的每一个元素赋值给Value
     print("$value");
});

②. map方法和JS里map方法很像,遍历一个数组同时返回一个数组,遍历同时会给出每个元素value。

var newList = lsit.map((value){
   return value*2;
})

③. where方法遍历数组得到元素同时可以加判断语句

var newList = list.where((value){
    return value>5;
})

④. any方法遍历一个数组是否存在某个元素或者符合某些条件 返回 true false 只要集合里面有满足条件就返回true

var newList = list.any((value){
    return value>5;
})

⑤. every方法遍历一个数组是否存在某个元素 或者 符合某些条件 每一个都满足条件返回true否则返回false

var newList = list.every((value){
     return value>5;
})

五. 运算符

  • 算术运算符 + 、- 、* 、/ 、~/取整 、 %取余、 ++ 、 – 、 += 、+-

    a = b++; 会先把b赋值给a 然后在加 ++ – 标识自增 自减 1
    a = ++b; 把b加上1 在赋值给a
    //在赋值运算里面如果++ – 写在前面 这个时候先运算 在赋值 如果++ – 写在后面 先赋值后运算
    int aaaa = 13;
    int bbbb = 5;
    print(aaaa+bbbb);
    print(aaaa-bbbb);
    print(aaaa*bbbb);
    print(aaaa/bbbb);
    print(aaaa~/bbbb);
    print(aaaa%bbbb);

  • 关系运算符 == != > < >= <= 二元运算符

    print(aaaa == bbbb);
    print(aaaa != bbbb);
    print(aaaa > bbbb);
    print(aaaa < bbbb);
    print(aaaa <= bbbb);
    print(aaaa >= bbbb);

  • 逻辑运算符 ! 取反 && 并且 || 与

    • && 并且 条件全部为true 为true 否则false
    • || 或者 或 全部为false 为false 否则 true
  • 基础赋值运算符 = ??= 赋值

    b ??= 23; 表示如果b为空的话把23赋值给b

  • 复合赋值运算符 += 、 -= 、 *= 、 %= 、 ~/=

  • 三目运算符 它是唯一有3个操作数的运算符,也叫三元运算符。一般来说,三目运算符的结合性是右结合的。

    var flag = true;
    var ccc = flag ? ‘正确’ : ‘错误’;

  • ?? 运算符

    var aaa = 222;
    var bbb = a ?? 333; //当a的值为空的时候把333赋值给bbb

六. 类型转换

  • Number和String类型之间的转换

    • Number类型转换为String类型用toSting()
    • String转换为Number 用parse 通过int调用parse函数把要转换的字符串传进去

    String ahahah = ‘123’;
    var nnnnn = int.parse(ahahah);
    var nnnnn = double.parse(ahahah);//转换为double
    //如果传入的值为空 可以通过 try catch来判断
    try{

    var nnnnn = int.parse(ahahah);
    

    }catch (err){

    print('error');
    

    //如果转换失败会跳到catch中
    }
    var aaasaa = 222;
    aaasaa.isNaN //判断是否为空
    print(aaaa.toString());

七. 循环语句 流程控制语句(控制流)

  • for循环
  • 流程:

    1. 声明变量int i = 0
    2. 判断i <= 100
    3. print(i);
    4. i++
      5.从第二步再来,直到判断为false

      for(int i = 0 ; i <= 100 ; i++ ){
      print(i);
      }
      // 打印0到50所有的偶数
      for(int i = 0 ; i <= 50 ; i++ ){
      if(i % 2 == 0){

      print(i);
      

      }
      }
      //求1+2+3+4+5+++100的和
      var sum = 0;
      for(int i = 1 ; i <= 100 ; i++ ){

      sum += i;
      }
      //5050;

  • while 循环 和 do while 循环

    • 语法格式
    • 注意点
      • 最后的分号不要忘记
      • 循环条件中使用的变量需要经过初始化
      • 循环体中,应有结束循环的条件否则会死循环
      • while语句可能一次都不执行 do while 肯定会执行一次
    • 不同点 第一次循环条件不成立的情况下 while不执行循环体 do while肯定会执行一次

      while(表达式/循环条件){
      循环体
      }

      do{
      语句/循环体
      }while(表达式/循环条件)

  • if else

  • switch case

  • break语句

    1. 在switch语句中使流程跳出switch结构
    2. 在循环语句中使流程跳出当前循环,遇到break 循环终止,后面的代码不会执行
  • break语句注意点
    1.如果在循环中已经执行了break语句,就不能执行循环体中位于break后的语句。
    2.在多层循环中,一个break语句只能向外跳出一层。
    3.break可以用在switch case 中也可以用在for循环和while循环中。

  • ontinue语句

    • [注]只能在循环语句中使用,使本次循环结束,既跳过循环体重下面尚未执行的语句,接着进行下continue可以用在for循环以及while循环中,但是不建议用在while循环中,不小心容易死循环

八. Dart函数

dart中的函数 函数的定义 可选参数 默认参数 命名参数 箭头函数 匿名函数 闭包等 函数也叫方法 在类外面叫函数 在类内部叫方法 这个都无所谓 都可以叫函数 也可以叫方法

  • 自定义方法函数:

    自定义方法的基本格式
    返回类型 方法名称(形式参数1,形式参数2,……){
    方法体 具体执行逻辑
    return 返回值;
    }
    print();//内置方法/函数

  • 定义一个带可选参数的方法

    String method (String name ,[int age,String sex]){
    //形参 可选参数放到参数后面 用[]中括号包裹 用,逗号隔开
    }

  • 定义一个带默认参数的方法

    String method (String name ,[String sex = ‘男’,int age]){
    //形参 如果有默认参数 建议放到 不带默认参数的可选参数前面
    }

  • 定义一个命名参数的方法

    String method (String name ,{String sex = ‘男’,int age}){
    //参数带名称的参数 需要用大括号包裹{}并且里面也可以设置默认参数
    }

  • 实现一个把函数当做参数的方法

    fn1(){
    print(‘fn1’);
    }
    fn2(fn){
    fn();
    }
    fn2(fn1);
    //把方法函数fan1当做另一个方法fan2的形式参数传进去 然后执行

  • 匿名函数 没有名字的函数

    var fn = (){//没有方法名称
    print(‘我是一个匿名方法’);
    }//直接通过变量fn调用方法
    var printNumm = (){
    //表示把一个函数赋值给了printNumm这个变量 调用方式和普通调用方法一样 传值方式和普通方法一样
    }

  • 箭头函数 箭头函数后面只能写一句代码

    list.forEach((valye) => print(value));
    list.forEach((value) => {//里面也只能写一句代码
    print(value)//不用写分号
    })

  • 自执行方法 不主动去调用 方法自己去执行

    ((int n){
    //方法在创建好后会自动执行 因为方法后面有括号会直接调用这个方法 可以传入参数 和 接收参数 还可以指定类型
    print(‘我是自执行方法’);
    })(12);
    //就相当于在函数外面包裹了一个函数

  • 方法的递归 一个方法可以调用自己 记得写判断语句 当符合条件后跳出 否则死循环

    var sum = 1;
    fn(int n ){

    sum*=n;
    if(n == 1){
      return;
    }
    fn(n-1);//关键这句 在符合条件后在此执行当前方法 
    

    }
    fn(10);

  • 闭包:函数嵌套函数,内部函数会调用外部函数的变量或参数.

    1. 全局变量特点:全局变量常驻内存,全局变量污染全局
    2. 局部变量的特点:不会常驻内存 会被垃圾回收机制回收,不会污染全局
  • 通过闭包可以实现:常驻内存、不污染全局,产生了闭包,闭包可以解决这个问题。

闭包写法:函数嵌套函数,并return 里面的函数,这样就行成了闭包

fn(){ 
 var a = 123; /*不会污染全局 常驻内存 *//
 return (){
 a++;
 print(a);
 }
}
print(fn());

九. 类

dart所有的东西都是对象,所有的对象都继承自Object类。是一门使用类和单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是Object的子类。

一个类通常由属相和方法组成

1. 定义Person类
class Person{//类名首字母大写
  String name = '张三';
  int age = 23;
 //dart里面的构造函数可以写多个但是默认构造函数只能有一个
 Person(String name,int age){
  this.name = name;
  this.age = age;
  print('这是构造函数里面的内容,这个方法在实例化的时候触发')
 }
 //默认构造函数简写
 Person(this.name,this.age);
 //命名构造函数 可以有多个
 Person.now(){
    print('我是命名构造函数');
 }
  void getInfo(){
  print("${this.name}--$age");//this指当前类 类似self 通过this.需要用{}大括号包裹
}
}
2. 实例化类
var p1 = Person();//2.0.0后不用new关键字 写也可以 推荐不写
Person p2 = Person();//默认实例化类的时候调用的是默认构造函数
Person p3 = Person.now();//命名构造函数
var time = DateTime.now(); //实例化datetime 调用它的命名构造函数
//dart和其他面向对象语言不一样 dart中没有public private protected这些访问修饰符
但是我们可以使用 “_” 下划线 把一个属性或者方法定义成私有
String _name;//私有属性 私有属性可以通过共有的方法来访问 间接访问私有属性
_run(){//私有方法
    print('这是一个私有方法');//也可以通过公有方法来间接调用私有方法
}
alert(){
    this._run();//通过公有方法访问私有方法 私有的不能直接访问
}
3. 类中的getter和setter修饰符
get 名称{ //getter方法 也就方法获取数据
   return "返回值"
}
set 名称(形式参数){
//参数名称 = 形式参数;
}
4. 类中的初始化列表

dart中我们也可以在构造函数体运行之前初始化实例变量

int height;
int width
Rect():height = 2 , width= 3{//在实例化之前的操作

}
5. 静态成员

dart类中的静态成员:

  1. 使用static 关键字来实现类级别的变量和函数
  2. 静态方法不能访问非静态成员,非静态方法可以访问静态成员
  3. 静态方法成员变量不能在通过类的实例化对象访问 直接通过类来访问

    static String name = “zhangsan”;

    static void show(){

    }

    void getInfo(){//非静态方法可以访问静态成员以及非静态成员

    }

6. dart中的对象操作符
  • ? 条件运算符
  • as 类型转换
  • is 类型判断
  • .. 级联操作(连缀)

    p1?.方法 如果p1对象不存在会自动返回 如果存在会访问方法
    (p1 as Person).方法或者属性 //类型转换 转换为自己需要的类型
    Person p1 = Person();
    p1..name = “hhhh”;
    ..age = 35;//连缀操作符 访问属性或者方法不用对象名可以直接访问

7. dart中的类的继承

通过super关键字来继承父类的属性和方法
重写父类方法是时@override关键字 建议加 可以不加
super.父类里面的方法 通过super调用父类的方法

  1. 子类使用extends类关键字来继承父类
  2. 子类会集成父类中可见的属性和方法 但是不会继承构造函数
  3. 子类能重写父类的方法getter和setter方法
8. dart中的抽象类 多态 和接口

dart抽象类主要用于定义标准,子类可以继承抽象类,可以实现抽象类的接口

  1. 抽象类通过abstract关键字来定义
  2. dart中的抽象方法不能用abstract声明,dart中没有方法体的方法我们称为抽象方法
  3. 如果子类继承抽象类必须得实现里面的抽象方法
  4. 如果把抽象类当做接口实现的话必须得实现抽象类里面定义的所有属性和方法
  5. 抽象类不能被实例化,只有继承它的子类可以

继承抽象类extends和implements关键字的区别

  1. 如果复用抽象类的方法,并且要用抽象方法约束自类的话我们就要用extends继承抽象类
  2. 如果只是把抽象类当做标准的话我们就用implements实现抽象类、

接口:就是约定规范

首先dart的接口没有interface关键字定义接口,而是普通类或者抽象类都可以作为接口被实现
同样适用implements关键字进行实现
但是dart的接口有点奇怪如果实现的类是普通类 会将普通类和抽象中的属性的方法全部需要重写一遍
而因为抽象类可以定义抽象方法,普通类不可以,所以一般如果要实现像Java接口那样的方式,一般会使用抽象类。
建议使用抽象类定义接口

abstract class Db{
    add();//只写方法不实现 继承它的子类需要实现它的方法属性
}
class Mysql implements Db{
    @override
    add(){

    }
}

dart中一个类实现多个接口 以及dart中的Mixins混入

abstract class a{
  add();//只写方法不实现 继承它的子类需要实现它的方法属性
}
abstract class b{
  remove();//只写方法不实现 继承它的子类需要实现它的方法属性
}
class implements a,b{
  //需要实现上面俩个抽象类的方法和属性 这叫一个类实现多个接口
}

Mixins的中文意思是混入 就是类中混入其他功能,在dart中可以使用mixins实现类似多继承的功能,因为mixins使用条件 随着dart的版本一直在变 这里讲的是dart2.x中使用mixins的条件

  1. 作为mixins的类只能继承自object 不能继承其他类
  2. 作为mixins的类不能有构造函数
  3. 一个类可以mixins多个mixins类

mixins不是继承也不是接口 而是一种全新的特性,mixins的实例类型就是其超类的子类型 c 混入a b 就是ab的子类型。通过with关键字实现class c with a,b{}c继承了a和b class c extends Person with a,b {}c继承于Person类同时混入了a b 如果继承的有同样的方法和属性 后面的会覆盖前面的

9. 泛型 泛型方法 泛型类 泛型接口

通俗理解:泛型就是解决 类 接口 方法的复用性,以及对不特定数据类型的支持(类型效验)

泛型一般在方法前面加T 啥都行 说明是泛型

①. 泛型方法
T getDate<T>(T value){
return value;
}

调用:

一般 getDate(123) 这个没有类型校验 传啥返回啥。
类型校验 getData(‘123213’); 这个String会传给尖括号的T代表这个方法的返回值是String 接收值也是String

②. 泛型类
class Person<T>{
    List list = List<T>();
    void ad(T value){
        this.list.add(value);
    }
}

dart中的泛型接口:

  1. 定义一个泛型接口 约束实现它的子类必须有getByKey(Key) 和 setBuKey(key,value)
    2.要求setByKey的时候value的类型和实例化子类的时候指定的类型一致

    abstract class Cache{
    getByKey(steing key);
    void setByKey(string key ,T value);

    }
    class FlieCache implements Cache{
    //继承泛型类 把当前类定义的泛型传给父类的泛型
    void setByKey(string key ,T value){

    }
    }

③. async和await
  • 只有async方法才能使用await关键字调用方法,如果调用别的async方法必须使用await关键字
    • async是让方法变成异步
    • await是等待异步方法执行完成

记录:

函数如果有一行的话可以使用箭头函数 只要超过一行就不能使用箭头函数 (value) => print(value);
dart中所有的类都继承于Object类
$ 符 字符串插值运算符 $+变量名

//引入别的类
import 'package:async/async.dart';

Rune 符号文件 用来表达Unicode字符
Unicode为所有世界写作系统中使用的每个字母、数字和符号定义了唯一的数值。
Unicode采用UTF-32位编码 dart采用了UTF-64编码
为了在字符串中表达32位的Unicode值需要用到特殊语法
\uXXXX 以\u开始后面跟着4个十六进制数(XXXX) , x的取值范围是0到f

var tempStr = '\u0F00';
print(tempStr);

当到指定多于或者少于4个十六进制数字是,使用{}包裹该值

var smeil = '\u{1F600}';
print(smeil);

全局函数 可以在main方法中调用 全局作用域 方法参数被称为形式参数 形参 调用方法传入的参数被称为实际参数 实参

//自定义方法
void printInfo(){ 
  print('我是一个自定义方法');
  int getNum(){//方法里面还可以嵌套方法 一个返回值为int的方法 这个方法只能在 当前函数体内调用 局部作用域
    var myNum = 111;
    return myNum;
  }
}

Dart重要概念:1,在变量中可以放置的所有东西都是对象,而每个对象都是类的实例。无论数字、函数、和null都是对…


From: https://blog.csdn.net/IT_Boy_/article/details/106380206

1.获取状态栏高度

1.第一种,注意:这里需要导入 ‘dart:ui’ 包

import 'dart:ui';
MediaQueryData.fromWindow(window).padding.top

2.第二种,

MediaQuery.of(context).padding.top

说到状态栏,就要说个安全区域的概念:所谓安全区域,就是适配现在一些刘海屏之类的非常规显示屏,在flutter中除了根据上面的方法获取到状态栏高度,给页面加对应的状态栏高度padding,还有一个专门的widget用来显示安全区域内容:SafeArea

2.获取appBar高度

位于 Dart Packages/flutter/src/material/constans.dart

///  * [kMinInteractiveDimensionCupertino]
///  * The Material spec on touch targets at <https://material.io/design/usability/accessibility.html#layout-typography>.
const double kMinInteractiveDimension = 48.0;

/// The height of the toolbar component of the [AppBar].
const double kToolbarHeight = 56.0;

/// The height of the bottom navigation bar.
const double kBottomNavigationBarHeight = 56.0;

3.获取手机屏幕宽高

Material 设计规范中 状态栏、导航栏、ListTile高度分别为 24、56、56

MediaQuery.of(context).size.width
MediaQuery.of(context).size.height

我把 MediaQuery.of(context) 的值输出来了,不同的机型有些值是不同的, 其实 MediaQuery.of(context) 输出的内容和 MediaQueryData.fromWindow(window) 输出的内容是一样的

MediaQueryData(
  size: Size(360.0, 592.0),
  devicePixelRatio: 2.0,
  textScaleFactor: 1.0,
  platformBrightness: Brightness.light,
  padding: EdgeInsets(0.0, 24.0, 0.0, 0.0),
  viewPadding: EdgeInsets(0.0, 24.0, 0.0, 0.0),
  viewInsets: EdgeInsets.zero,
  physicalDepth: 1.7976931348623157e+308,
  alwaysUse24HourFormat: true,
  accessibleNavigation: false,
  disableAnimations: false,
  invertColors: false,
  boldText: false,
);

flutter 中线条,
1 主要是利用DecoratedBox的decoration属性:

1
Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ FlatButton( child: Text('打开'), ), //垂直分割线 SizedBox( width: 1, height: 12, child: DecoratedBox( decoration: BoxDecoration(color: Colors.grey), ), ), FlatButton( child: Text('关闭'), ) ], );

2 或者更简单的,直接使用VerticalDivider:

1
VerticalDivider( color: Colors.grey, width: 1, )

3 部分需求中 Container 的外边框 属性中

1
2
3
4
5
6
decoration: new BoxDecoration(
color: Colors.white,
border: new Border(bottom: BorderSide(width: 1,color: ColorTool.grayE0(),style:BorderStyle.solid )),
//设置Border属性给容器添加边框
)

flutter 点击空白处回收键盘

1
2
3
4
5
6
7
8
9
10
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
// 触摸收起键盘
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
padding: EdgeInsets.fromLTRB(0, 0, 0, 0),
child: XXXX);
`

Include of non-modular header inside framework module

From: https://alanli7991.github.io/2017/07/21/%E6%A8%A1%E5%9D%97%E5%8C%9621Framework%E4%B8%8E%E6%A8%A1%E5%9D%97%E5%A4%96nonmodular%E5%A4%B4%E6%96%87%E4%BB%B6/

Include of non-modular header inside framework module

在进行Framework化的过程中,一旦引用了某些Framework其使用者Project,就会报错

HttpHelper.h:10:9: error : include of non-modular header inside framework module 'Ware.HttpHelper': '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator10.3.sdk/usr/include/CommonCrypto/CommonHMAC.h'  

[-Werror,-Wnon-modular-include-in-framework-module] #import <CommonCrypto/CommonHMAC.h>  

提醒你 在Framework Module中加载了非当前Module的头文件

处理方法

处理方法有两种,一种是基于LLVM的配置,一种是手动梳理头文件

Allow Non-modular Include

根据StackOverflow的回答可以通过

  • Build Setting — Apple LLVM 8.1 Language Modules — Allow Non-modular Includes In Framework Modules

设置为YES,则可以在Framework中使用模块外的Include,不过这种过于粗暴

检查头文件引用关系

根据下文产生原因的分析,其本质原因类似C++文件file not found的加载与编译,是因为不同语言之间的头文件引用混乱产生的

手工检查头文件引用关系,特别是 Umbrella Header File中 不要引用Include 了 C/C++ 头文件的OC头文件 作为子模块放出

产生原因

关注点集中于 #import 可以发现其 CommonCrypto 模块为一个系统库

与UIKit这样的Framework不同,CommonCrypto 是 C模块 ,也就是说其头文件 CommonHMAC.h 不应该伴随 SomeHeader.h 头文件对外暴露

  1. Target.app 引用了 SharedFrame.framework的Umbrella头文件 SharedFrame.h
  2. Umbrella头文件中包含了 SomeHeader.h
  3. HttpHelper.h 中暴露了 CommonCrypto 的 CommonHMAC.h
  4. 对于 SharedFrame.framework的Scope, 内部二进制包含有 CommonCrypto 模块的代码,CommonHMAC.h有效
  5. 对于 Target.app的Scope, 不确定SharedFrame.h内部引用 HttpHelper.h 是否包含CommonCrypto 模块的代码
  6. Target.app 判定 为模块外Header File报错

修改方法:

在 SharedFrame.h 中删除 #import , 仅在 Target.app 的.m文件中使用时加载

iOS动态调用类方法

From: https://www.jianshu.com/p/a43057a8d474

iOS动态调用类方法(不带参数)

Class class = NSClassFromString(@"MyClass");
if (class) {
    id obj = [[class alloc] init];
    SEL sel = NSSelectorFromString(@"myMethod");
    //检查是否有"myMethod"这个名称的方法
    if ([obj respondsToSelector:sel]) {
        [obj performSelector:sel];
    }
}

iOS动态调用类方法(带参数)

Class class = NSClassFromString(@"MyClass");
if (class) {
    id obj = [[class alloc] init];
    SEL sel = NSSelectorFromString(@"myMethod:"); //方法带有参数需要加冒号:
    //检查是否有"myMethod"这个名称的方法
    if ([obj respondsToSelector:sel]) {
        [obj performSelector:sel withObject:param]; //方法有多个参数时使用多个withObject传递参数
    }
}

iOS动态调用类方法(有返回值)

Class class = NSClassFromString(@"MyClass");
if (class) {
    id obj = [[class alloc] init];
    SEL sel = NSSelectorFromString(@"myMethod");
    //检查是否有"myMethod"这个名称的方法
    if ([obj respondsToSelector:sel]) {
        id = [obj performSelector:sel];
        //然后将id转换为类方法实际返回的数据类型。
        //假设为NSString *类型
        NSString *str = (NSString *)id;
        NSLog(@"str: %@", str);
    }
}

优点

  1. 弱化连接,因此并不会把没有的Framework也link到程序中。
  2. 不需要使用import,因为类是动态加载的,只要存在就可以加载。因此如果你的toolchain中没有某个类的头文件定义,而你确信这个类是可以用的,那么就可以用这种方法。

问题

采用这种方式,在Xcode中会报以下警告信息:

"performSelector may cause a leak because its selector is unknown"(因为performSelector的选择器未知可能会引起泄漏)

原因

在ARC下调一个方法,runtime需要知道对于返回值该怎么办。返回值可能有各种类型:voidintcharNSString *id等等。ARC 一般是根据返回值的头文件来决定该怎么办的,一共有以下4种情况:

  1. 直接忽略(如果是基本类型比如 voidint这样的)。
  2. 把返回值先retain,等到用不到的时候再release(最常见的情况)。
  3. retain,等到用不到的时候直接release(用于 initcopy 这一类的方法,或者标注ns_returns_retained的方法)。
  4. 什么也不做,默认返回值在返回前后是始终有效的(一直到最近的release pool结束为止,用于标注ns_returns_autoreleased的方法)。

而调performSelector:的时候,系统会默认返回值并不是基本类型,但也不会retainrelease,也就是默认采取第 4 种做法。所以如果那个方法本来应该属于前3种情况,都有可能会造成内存泄漏。

对于返回void或者基本类型的方法,就目前而言你可以忽略这个warning,但这样做不一定安全。我看过Clang在处理返回值这块的几次迭代演进。一旦开着ARC,编译器会觉得从performSelector:返回的对象没理由不能retain,不能release。在编译器眼里,它就是个对象。所以,如果返回值是基本类型或者void,编译器还是存在会retainrelease它的可能,然后直接导致crash。

解决办法

1. 使用宏忽略警告(不推荐)

#define SuppressPerformSelectorLeakWarning(Stuff) \
do {\
    _Pragma("clang diagnostic push") \
    _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
    Stuff; \
    _Pragma("clang diagnostic pop") 
\} while (0)

在产生警告也就是performSelector的地方用使用该宏,如:

SuppressPerformSelectorLeakWarning([obj performSelector:sel]);

1. 使用函数指针方式(推荐)

Class class = NSClassFromString(@"MyClass");
if (class) {
    id obj = [[class alloc] init];
    SEL sel = NSSelectorFromString(@"myMethod");
    IMP imp = [obj methodForSelector:sel];
    void (*function)(id, SEL) = (void *)imp;
    function(obj, sel);
}

这一堆代码在做的事情其实是向obj请求那个方法对应的C函数指针。所有的NSObject都能响应methodForSelector:这个方法,不过也可以用Objective-C runtime里的class_getMethodImplementation(只在protocol的情况下有用,id<SomeProto>这样的)。这种函数指针叫做IMP,就是typedef过的函数指针(id (*IMP)(id, SEL, ...))。它跟方法签名(signature)比较像,虽然可能不是完全一样。

得到IMP之后,还需要进行转换,转换后的函数指针包含ARC所需的那些细节(比如每个OC方法调用都有的两个隐藏参数self_cmd)。这就是代码第6行干的事(右边的那个(void *)只是告诉编译器,不用报类型强转的warning)。

最后一步,调用函数指针。

如果selector接收参数,或者有返回值,代码就需要改改:

Class class = NSClassFromString(@"MyClass");
if (class) {
    id obj = [[class alloc] init];
    SEL sel = NSSelectorFromString(@"myMethod:");
    IMP imp = [obj methodForSelector:sel];
    NSString *(*function)(id, SEL, NSString *) = (void *)imp;
    NSString *result = function(obj, sel);
}
部分引用内容出处如下:

链接:[https://www.jianshu.com/p/a9569a9c9a63][1]
链接:[https://www.jianshu.com/p/6517ab655be7][2]

Flutter Dio包网络请求抓包解决方案

From: https://segmentfault.com/a/1190000023654714

发布于 8月17日

在Flutter中进行网络请求时,我们可以使用的库有3个,即Http请求库、HttpClient请求库和Dio请求库(详细介绍请参考:Flutter开发之Http网络请求),使用得最多的就是Dio请求库。因为相比Http请求库和HttpClient请求库,Dio库不仅支持常见的网络请求,还支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等操作。

不过,默认情况下,Dio进行网络请求时是不支持抓包的,所以如果要进行抓包,就需要对Dio进行请求封装,并编写代理代码。下面是代理的几种写法:

方法一

我们可以直接在Dio里面设置ip以及端口,通过硬编码的方式进行代理,代码如下:

(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
          (client) {
        //这一段是解决安卓https抓包的问题
        client.badCertificateCallback =
            (X509Certificate cert, String host, int port) {
          return Platform.isAndroid;
        };
        client.findProxy = (uri) {
          return "PROXY 代理ip:代理port";
        };
      };

不过,这种硬编码方式,写得太死,不够灵活,每次更改代理都需要打包。

方法二

直接在原生插件获取手代理ip和代理端口,不过Android比较难,下面是iOS的实现。

//自动获取手机代理
 NSDictionary *proxySettings = (__bridge NSDictionary *)(CFNetworkCopySystemProxySettings());
          NSArray *proxies = (__bridge NSArray *)(CFNetworkCopyProxiesForURL((__bridge CFURLRef _Nonnull)([NSURL URLWithString:call.arguments]), (__bridge CFDictionaryRef _Nonnull)(proxySettings)));
          NSString *hostName = proxySettings[@"HTTPSProxy"];
          NSString *portName = [NSString stringWithFormat:@"%@",proxySettings[@"HTTPPort"]];
          long HTTPEnable = [proxySettings[@"HTTPEnable"] longValue];
          if (HTTPEnable==0) {
              hostName = @"";
          }

方法三

除了上面的硬编码方式外,我们还可以采用scheme协议的方式传入代理ip和代理端口。此方法的步骤如下:
1,注册自己的URL Scheme,例如:scheme://
2,定义参数规则,例如:scheme://tiaoshi?host=10.0.206.163
3,引入flutter插件:uni_links: ^0.2.0
4,flutter监听解析参数,并在dio里面设置代理
5,使用[草料]https://cli.im生成一个二维码:内容:scheme://tiaoshi?host=10.0.206.163
6,使用原生相机扫码进入app就可以抓包

下面是涉及的代码,Flutter代码如下:

Future<Null> initUniLinks() async {

    // 监听插件scheme数据
      getLinksStream().listen((String link) {
        link =  Uri.decodeComponent(link);
        if(link.contains("scheme://")){
          String type = getTypeStr(link);
          String param = link.replaceAll("scheme://$type?", "");
          Map dict = getUrlParams(param);
          if(type=="tiaoshi"){//设置抓包代理
            String host = dict["host"];
            String port = dict["port"];
            //这里是网络请求封装
            Net.setHttpProxy(host,port==null?"8888":port);
           }
        }
      // Parse the link and warn the user, if it is not correct
    }, onError: (err) {
      // Handle exception by warning the user their action did not succeed
    });
  }

//获取scheme 要处理的业务类型
  String getTypeStr(String link){
    List params = link.split("?");
    String typeStr = params[0];
    typeStr =  typeStr.replaceAll("scheme://", "");
    return typeStr;
  }

//url参数转map
  Map getUrlParams(String paramStr) {
    Map map = Map();
    List params = paramStr.split("&");
    for(int i=0;i<params.length;i++){
      String str = params[i];
      List arr = str.split("=");
      map[arr[0]]= arr[1];
    }
    return map;
  }

代理层代码:

static void setHttpProxy(String host,String port) {
    Application.httpProxy = host+':'+port;
    _initDio();
  }

static Future<void> _initDio() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    if (Platform.isAndroid) {
      _androidInfo = await deviceInfo.androidInfo;
    } else if (Platform.isIOS) {
      _iosInfo = await deviceInfo.iosInfo;
    }

    _dio = Dio(BaseOptions(
      contentType: 'application/json',
      baseUrl: Config.BASE_URL,
    ));
    _dio.options.receiveTimeout = 5000;
    _dio.options.connectTimeout = 10000;

    if (Application.httpProxy.length != 0) {
      (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
          (client) {
        //这一段是解决安卓https抓包的问题
        client.badCertificateCallback =
            (X509Certificate cert, String host, int port) {
          return Platform.isAndroid;
        };
       //这是抓包代理
        client.findProxy = (uri) {
          return "PROXY ${Application.httpProxy}";
        };
      };
    }
    _dio.interceptors.addAll([
      InterceptorsWrapper(
        onRequest: (Options options) {
          options.headers['DeviceName'] = 'xxxx';
          return options;
        },
        onResponse: (Response res) {
          try {

          ...

            return res;
          } catch (e) {
            return res;
          }
        },
        onError: (DioError e) {
          print(e);
         }
              break;
            default:
          }
          return e;
        },
      ),
    ]);
  }

static Future<ResponseModel> get(
    String path, {
    Map<String, dynamic> queryParameters,
    Options options,
    CancelToken cancelToken,
    void Function(int, int) onReceiveProgress,
  }) async {
    if (_dio == null) {
      await _initDio();
    }

    final res = await _dio.get<ResponseModel>(
      path,
      queryParameters: queryParameters,
      options: options,
      cancelToken: cancelToken,
      onReceiveProgress: onReceiveProgress,
    );

    return res.data;
  }

  static Future<ResponseModel> post(
    String path, {
    dynamic data,
    Map<String, dynamic> queryParameters,
    Options options,
    CancelToken cancelToken,
    void Function(int, int) onSendProgress,
    void Function(int, int) onReceiveProgress,
  }) async {
    if (_dio == null) {
      await _initDio();
    }
    final res = await _dio.post<ResponseModel>(
      path,
      data: data,
      queryParameters: queryParameters,
      options: options,
      cancelToken: cancelToken,
      onSendProgress: onSendProgress,
      onReceiveProgress: onReceiveProgress,
    );

    return res.data;
  }

阅读 779 发布于 8月17日

本作品系原创, 采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


Flutter 记录1

使用 json_serializable 时 执行命名报错

1
2
Could not find package "build_runner". Did you forget to add a dependency?
pub finished with exit code 65

解决 加入 build_runner: ^1.10.0 依赖

https://caijinglong.github.io/json2dart/index_ch.html

https://pub.dev/packages/g_json


flutter 在xcode 上调试

在项目文件下 执行(App 的FlutterEdgin 执行后)

1
2
3
4
5
6
//
flutter attach
// 调试模式
flutter attach --isolate-filter='debug'
// 加上调试的
flutter attach --app-id com.hzzhebei.zbios

单例

Flutter中,dart的单例模式设计
创建一个单例的Manager类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Manager {
// 工厂模式
factory Manager() =>_getInstance()
static Manager get instance => _getInstance();
static Manager _instance;
Manager._internal() {
// 初始化
}
static Manager _getInstance() {
if (_instance == null) {
_instance = new Manager._internal();
}
return _instance;
}
}

调用
// 无论如何初始化,取到的都是同一个对象

1
2
Manager manager = new Manager();
Manager manager2 = Manager.instance;

—–main 函数之前执行一些函数报错—

If you’re running an application and need to access the binary messenger before runApp() has been called..

1
2
// main 函数第一行加入
WidgetsFlutterBinding.ensureInitialized();

—- 出现json 自动化生成错误, 报错non-nullable 一直循环, 清理项目解决

1
2
3
flutter clean
flutter packages pub upgrade
flutter pub run build_runner build

From: https://blog.csdn.net/worship_kill/article/details/102892349

在使用Flutter的过程中,想去自己封装一个转model的类,后来写着写着发现根本没法弄,于是就去中文官网看看。

接着就看到了json_serializable,然后就照着做呗,导入package,导入头文件,声明@JsonSerializable(),创建类,属性。然后

flutter packages pub run build_runner build,就没然后了,

没有生成.g.dart,命令行到最后还提醒Succeeded,我就蒙蔽了。

于是反复去工程里查找,就是没找到,于是乎,我又重新创建一个新的工程,再导入一遍,还是不行,我就去github上json_serializable找源码查看,然后试着将

part ‘model.g.dart’;

敲上去,发现代码是报错的,这时候再运行一次flutter packages pub run build_runner build

于是乎就在Model.dart文件下生成了.g.dart文件,欲哭无泪。

附带一个标准生成.g.dart的写法:

首先在pubspec.yaml文件里面导入依赖库,Ctrl + S或者点击一下右上角的下载箭头(VS Code),getPackage一下,注意文字的对齐

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  json_serializable: ^2.0.0
  build_runner: ^1.0.0

然后来到你的model类里面

复制过去(注意现在代码是报错的),只用改类名和属性名,其他不用管

运行一次

import 'package:json_annotation/json_annotation.dart';

part '你的类名.g.dart';

@JsonSerializable()

class  你的类名 {
  var name;
  var age;
  你的类名(this.name,this.age);
  factory 你的类名.fromJson(Map<String,dynamic> json) => _$你的类名FromJson(json);
  Map<String,dynamic> toJson() => _$你的类名ToJson(this);

}

cd 你的工程目录

flutter packages pub run build_runner build //使用 build_runner 生成 .g.dart 文件

flutter packages pub run build_runner watch //监控生成文件,如果有改动时自动生成/更新 .g.dart 文件

就ok了

还没有生成.g.dart文件或者报错的运行下面的命令

cd 你的工程目录

flutter packages pub run build_runner build –delete-conflicting-outputs //删除并重新创建.g.dart文件

没有生成的再运行一下

flutter packages pub run build_runner build


Fastlane 实践(一):自动化打包和发布

遇到问题, 多了一个扩展打包失败,顺便看到了这个文章觉得不错,转载记录一下,
xcode 12 自动打包问题,dev 有arm64 兼容问题,切换ad-hoc 自动打包上传解决

From: http://chaosky.tech/2020/05/04/fastlane-in-action-1/

fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. 🚀 It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.

fastlane 是自动化Beta部署和发布iOS和Android应用程序最简单方法。它可以处理所有繁琐的任务,例如生成屏幕截图,处理代码签名以及发布应用程序。

Fastlane 安装

安装 Xcode command line tools

$ xcode-select --install  

安装 Homebrew

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"  

安装 RVM

$ curl -sSL https://get.rvm.io | bash -s stable --auto-dotfiles  

$ source ~/.rvm/scripts/rvm  

修改 RVM 的 Ruby 安装源到 Ruby China 的 Ruby 镜像服务器,这样能提高安装速度。

$ echo "ruby_url=https://cache.ruby-china.org/pub/ruby" > ~/.rvm/user/db  

安装Ruby 2.6.5

$ rvm install 2.6.5  

$ rvm use 2.6.5 --default  

更新 RubyGems 镜像

$ gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/  

$ gem sources -l  

https://gems.ruby-china.org  

# 确保只有 gems.ruby-china.org  

bundle config mirror.https://rubygems.org https://gems.ruby-china.org  

安装 CocoaPods 和 Fastlane

$ gem install cocoapods  

$ gem install fastlane -NV  

$ gem install bundle  

快速开始

  1. 进入 iOS App 的目录并运行:

    1

fastlane init  

fastlane 会自动自动识别你的项目,并询问任何缺失的信息。

  1. fastlane Getting Started guide for iOS
  1. fastlane Getting Started guide for Android

Fastlane 进阶用法

随着公司项目的增多,每次都运行重复的Fastlane 命令进行配置会低效很多,所以急需一套可以满足所有App需求的配置。

Fastlane 是由Ruby开发,所以也支持 dotenv 的功能。

最终Fastlane生成目录结构如下:

├── .env
├── Appfile
├── Deliverfile
├── Fastfile
├── Matchfile
├── Pluginfile
├── README.md
├── Scanfile
├── metadata
│ ├── app_icon.jpg
│ ├── copyright.txt
│ ├── primary_category.txt
│ ├── primary_first_sub_category.txt
│ ├── primary_second_sub_category.txt
│ ├── review_information
│ │ ├── demo_password.txt
│ │ ├── demo_user.txt
│ │ ├── email_address.txt
│ │ ├── first_name.txt
│ │ ├── last_name.txt
│ │ ├── notes.txt
│ │ └── phone_number.txt
│ ├── secondary_category.txt
│ ├── secondary_first_sub_category.txt
│ ├── secondary_second_sub_category.txt
│ ├── trade_representative_contact_information
│ │ ├── address_line1.txt
│ │ ├── address_line2.txt
│ │ ├── address_line3.txt
│ │ ├── city_name.txt
│ │ ├── country.txt
│ │ ├── email_address.txt
│ │ ├── first_name.txt
│ │ ├── is_displayed_on_app_store.txt
│ │ ├── last_name.txt
│ │ ├── phone_number.txt
│ │ ├── postal_code.txt
│ │ ├── state.txt
│ │ └── trade_name.txt
│ └── zh-Hans
│ ├── apple_tv_privacy_policy.txt
│ ├── description.txt
│ ├── keywords.txt
│ ├── marketing_url.txt
│ ├── name.txt
│ ├── privacy_url.txt
│ ├── promotional_text.txt
│ ├── release_notes.txt
│ ├── subtitle.txt
│ └── support_url.txt
└── pem
├── development_xxx.xxx.xxx.p12
├── development_xxx.xxx.xxx.pem
├── development_xxx.xxx.xxx.pkey
├── production_xxx.xxx.xxx.p12
├── production_xxx.xxx.xxx.pem
├── production_xxx.xxx.xxx.pkey

.env

这个文件中放入的是需要引用的环境变量。

FASTLANE_SKIP_UPDATE_CHECK=true  

FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120  



APPLE_ID="xxxx"    

TEAM_ID="xxxx"         

FASTLANE_PASSWORD="xxx"      

FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="qwwe-tdpp-hdpc-fgzy"   

ITC_TEAM_ID="xxxx"       



APP_IDENTIFIER="xxx.xxx.xxx"  

SCHEME_NAME="XXX"  

WORKSPACE_NAME="XXX.xcworkspace"  

XCODEPROJ_NAME="XXX.xcodeproj"  





DEV_APP_IDENTIFIER="xxx.xxx.dev.xxx"  

DEV_APP_NAME="XXX测试版"  





PROD_APP_IDENTIFIER="xxx.xxx.xxx"  

PROD_APP_NAME="XXX"  



MATCH_GIT_BRANCH="XXX"  



DELIVER_METADATA_PATH="./fastlane/metadata"  

DOWNLOAD_METADATA_PATH="./metadata"  

Appfile

app_identifier "#{ENV["APP_IDENTIFIER"]}"   

apple_id "#{ENV["APPLE_ID"]}"   



team_id "#{ENV["TEAM_ID"]}"   

itc_team_id "#{ENV["ITC_TEAM_ID"]}"   

Deliverfile

app_identifier "#{ENV["APP_IDENTIFIER"]}"   

username "#{ENV["APPLE_ID"]}"   

Fastfile

fastlane_require "spaceship"  



fastlane_version "2.89.0"  



default_platform :ios  



platform :ios do  



  base_path = Pathname::new(File::dirname(__FILE__)).realpath.parent  



  before_all do  







  end  



  desc "生成 adhoc 测试版本,提交到蒲公英,参数 => type:'adhoc/development',默认adhoc"  

  lane :pgyer_beta do |options|  



    type = String(options[:type] || "adhoc")  



    if type == "adhoc"  

      export_method = "ad-hoc"  

      match_type = "adhoc"  

      match_type_name = "AdHoc"  

    else  

      export_method = "development"  

      match_type = "development"  

      match_type_name = "Development"  

    end  



    git_reversion = sh("git log -1 --pretty=format:'%h'")  

    version_number = get_info_plist_value(path: "#{ENV["SCHEME_NAME"]}/Info.plist", key: "CFBundleShortVersionString")  

    build_number = number_of_commits(all: false)  





    git_log = sh("git log --no-merges -1 --pretty=format:'# %ai%n# %B by %an'")  

    build_time = Time.new.strftime("%Y-%m-%d_%H.%M.%S")  





    output_dir = "#{base_path}/Output/adhoc/#{build_time}"  

    output_name = "#{ENV["SCHEME_NAME"]}_v#{version_number}(#{build_number}).ipa"  





    add_badge(shield: "#{version_number}-#{build_number}-orange")  





    increment_build_number(build_number: build_number)  





    update_app_identifier(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",  

      app_identifier: "#{ENV["DEV_APP_IDENTIFIER"]}"  

    )  



    update_info_plist(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",  

      block: proc do |plist|  

        plist["CFBundleDisplayName"] = "#{ENV["DEV_APP_NAME"]}"  

        plist["CFBundleName"] = "#{ENV["DEV_APP_NAME"]}"  

        plist["GIT_REVISION"] = git_reversion  

        plist["BUILD_TIME"] = build_time  

        plist["APP_CHANNEL"] = "pgyer"  

        urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "weixin"}  

        urlScheme[:CFBundleURLSchemes] = ["#{ENV["DEV_WEIXIN_APPID"]}"]  

      end  

    )  





    update_app_identifier(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}/Info.plist",  

      app_identifier: "#{ENV["DEV_NOTIFICATION_SERVICE"]}"  

    )  



    match(  

      type: "#{match_type}",   

      app_identifier: ["#{ENV["DEV_APP_IDENTIFIER"]}", "#{ENV["DEV_NOTIFICATION_SERVICE"]}"],   

      readonly: true  

    )  



    gym(  

      export_method: "#{export_method}",  

      include_bitcode: false,  

      scheme: "#{ENV["SCHEME_NAME"]}",   

      configuration: "AdHoc",  

      export_options: {  

        compileBitcode: false,  

        uploadBitcode: false,  

        provisioningProfiles: {  

          "#{ENV["DEV_APP_IDENTIFIER"]}" => "match #{match_type_name} #{ENV["DEV_APP_IDENTIFIER"]}",  

          "#{ENV["DEV_NOTIFICATION_SERVICE"]}" => "match #{match_type_name} #{ENV["DEV_NOTIFICATION_SERVICE"]}"  

        }  

      },  

      output_directory: output_dir,  

      output_name: output_name  

    )  



    upload_ipa(type: 'gxm', log: git_log)  





    bugly(app_id: "#{ENV["DEV_BUGLY_APPID"]}",  

      app_key:"#{ENV["DEV_BUGLY_APPKEY"]}",  

      symbol_type: 2,  

      bundle_id: "#{ENV["DEV_APP_IDENTIFIER"]}",  

      product_version: "#{version_number}(#{build_number})",  

      channel: 'pgyer'  

    )  



    copy_dsym(tpye: 'adhoc')  

  end  



  desc "生成 adhoc 预发版本,提交到蒲公英"  

  lane :pgyer_release do  



    git_reversion = sh("git log -1 --pretty=format:'%h'")  

    build_time = Time.new.strftime("%Y-%m-%d_%H.%M.%S")  

    version_number = get_info_plist_value(path: "#{ENV["SCHEME_NAME"]}/Info.plist", key: "CFBundleShortVersionString")  

    build_number = number_of_commits(all: false)  

    git_log = sh("git log --no-merges -1 --pretty=format:'# %ai%n# %B by %an'")  





    output_dir = "#{base_path}/Output/release/#{build_time}"  

    output_name = "#{ENV["SCHEME_NAME"]}_v#{version_number}(#{build_number}).ipa"  





    add_badge(shield: "#{version_number}-#{build_number}-orange", alpha: true)  





    increment_build_number(build_number: build_number)  



    update_app_identifier(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",  

      app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}"  

    )  



    update_info_plist(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",  

      block: proc do |plist|  

        plist["CFBundleDisplayName"] = "#{ENV["PROD_APP_NAME"]}"  

        plist["CFBundleName"] = "#{ENV["PROD_APP_NAME"]}"  

        plist["GIT_REVISION"] = git_reversion  

        plist["BUILD_TIME"] = build_time  

        plist["APP_CHANNEL"] = "pgyer"  

        urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "weixin"}  

        urlScheme[:CFBundleURLSchemes] = ["#{ENV["PROD_WEIXIN_APPID"]}"]  

      end  

    )  





    update_app_identifier(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}/Info.plist",  

      app_identifier: "#{ENV["PROD_NOTIFICATION_SERVICE"]}"  

    )  



    match(  

      type: "adhoc",   

      app_identifier: ["#{ENV["PROD_APP_IDENTIFIER"]}", "#{ENV["PROD_NOTIFICATION_SERVICE"]}"],   

      readonly: true  

    )  



    update_project_provisioning(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      target_filter: "#{ENV["SCHEME_NAME"]}",  

      profile:ENV["sigh_#{ENV["PROD_APP_IDENTIFIER"]}_adhoc_profile-path"],  

      build_configuration: "Release"  

    )  



    update_project_provisioning(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      target_filter: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}",  

      profile:ENV["sigh_#{ENV["PROD_NOTIFICATION_SERVICE"]}_adhoc_profile-path"],  

      build_configuration: "Release"  

    )  



    gym(  

      export_method: "ad-hoc",   

      scheme: "#{ENV["SCHEME_NAME"]}",   

      configuration: "Release",  

      export_options: {  

        compileBitcode: false,  

        uploadBitcode: false,  

        provisioningProfiles: {  

          "#{ENV["PROD_APP_IDENTIFIER"]}" => "match AdHoc #{ENV["PROD_APP_IDENTIFIER"]}",  

          "#{ENV["PROD_NOTIFICATION_SERVICE"]}" => "match AdHoc #{ENV["PROD_NOTIFICATION_SERVICE"]}"  

        }  

      },  

      output_directory: output_dir,  

      output_name: output_name  

    )  







    upload_ipa(type: 'gxm', log: "App Store 包上传:#{version_number}(#{build_number})")  





    bugly(app_id: "#{ENV["PROD_BUGLY_APPID"]}",  

      app_key:"#{ENV["PROD_BUGLY_APPKEY"]}",  

      symbol_type: 2,  

      bundle_id: "#{ENV["PROD_APP_IDENTIFIER"]}",  

      product_version: "#{version_number}(#{build_number})",  

      channel: 'pgyer'  

    )  



    copy_dsym(tpye: 'release')  

  end  



  desc "生成 appstore 版本,发布到 App Store"  

  lane :appstore_release do  



    git_reversion = sh("git log -1 --pretty=format:'%h'")  

    build_time = Time.new.strftime("%Y-%m-%d_%H.%M.%S")  

    version_number = get_info_plist_value(path: "#{ENV["SCHEME_NAME"]}/Info.plist", key: "CFBundleShortVersionString")  

    build_number = number_of_commits(all: false)  





    output_dir = "#{base_path}/Output/appstore/#{build_time}"  

    output_name = "#{ENV["SCHEME_NAME"]}_v#{version_number}(#{build_number}).ipa"  



    clear_derived_data  





    increment_build_number(build_number: build_number)  



    update_app_identifier(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",  

      app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}"  

    )  



    update_info_plist(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",  

      block: proc do |plist|  

        plist["CFBundleDisplayName"] = "#{ENV["PROD_APP_NAME"]}"  

        plist["CFBundleName"] = "#{ENV["PROD_APP_NAME"]}"  

        plist["GIT_REVISION"] = git_reversion  

        plist["BUILD_TIME"] = build_time  

        plist["APP_CHANNEL"] = "appstore"  

        urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "weixin"}  

        urlScheme[:CFBundleURLSchemes] = ["#{ENV["PROD_WEIXIN_APPID"]}"]  

      end  

    )  





    update_app_identifier(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      plist_path: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}/Info.plist",  

      app_identifier: "#{ENV["PROD_NOTIFICATION_SERVICE"]}"  

    )  



    match(  

      type: "appstore",   

      app_identifier: ["#{ENV["PROD_APP_IDENTIFIER"]}", "#{ENV["PROD_NOTIFICATION_SERVICE"]}"],   

      readonly: true  

    )  



    update_project_provisioning(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      target_filter: "#{ENV["SCHEME_NAME"]}",  

      profile:ENV["sigh_#{ENV["PROD_APP_IDENTIFIER"]}_appstore_profile-path"],  

      build_configuration: "AppStore"  

    )  



    update_project_provisioning(  

      xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",  

      target_filter: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}",  

      profile:ENV["sigh_#{ENV["PROD_NOTIFICATION_SERVICE"]}_appstore_profile-path"],  

      build_configuration: "AppStore"  

    )  







    gym(  

      export_method: "app-store",   

      scheme: "#{ENV["SCHEME_NAME"]}",   

      configuration: "AppStore",  

      export_options: {  

        provisioningProfiles: {  

          "#{ENV["PROD_APP_IDENTIFIER"]}" => "match AppStore #{ENV["PROD_APP_IDENTIFIER"]}",  

          "#{ENV["PROD_NOTIFICATION_SERVICE"]}" => "match AppStore #{ENV["PROD_NOTIFICATION_SERVICE"]}"  

        }  

      },  

      output_directory: output_dir,  

      output_name: output_name  

    )  





    bugly(app_id: "#{ENV["PROD_BUGLY_APPID"]}",  

      app_key:"#{ENV["PROD_BUGLY_APPKEY"]}",  

      symbol_type: 2,  

      bundle_id: "#{ENV["PROD_APP_IDENTIFIER"]}",  

      product_version: "#{version_number}(#{build_number})",  

      channel: 'appstore'  

    )  





    upload_ipa(type: 'gxm', log: "App Store 包上传:#{version_number}(#{build_number})")  



    copy_dsym(type: 'appstore')  



    deliver(  

      metadata_path: "#{ENV["DELIVER_METADATA_PATH"]}",  

      force: true  

    )  



  end  



  desc "上传 AppStore DSYM 文件到 Bugly,参数 => version:[latest]"  

  lane :upload_appstore_dsyms do |options|  

    version = String(options[:version] || "latest")  

    download_dsyms(version: version)  

    dsym_paths = lane_context[SharedValues::DSYM_PATHS]  

    for dsym_path in dsym_paths  



      split_strs = dsym_path.split(/\//).last.split(/-/)  

      version_number = split_strs[1]  

      build_number = split_strs[2].split(/\./)[0]  



      bugly(app_id: "#{ENV["PROD_BUGLY_APPID"]}",  

        app_key:"#{ENV["PROD_BUGLY_APPKEY"]}",  

        symbol_type: 2,  

        bundle_id: "#{ENV["PROD_APP_IDENTIFIER"]}",  

        product_version: "#{version_number}(#{build_number})",  

        channel: 'appstore',  

        dsym: dsym_path  

      )  

    end  

    clean_build_artifacts  

  end  



  desc "手动批量添加设备到profile"  

  lane :add_devices_manual do  



    UI.header "Add Device"  

    device_hash = {}  

    device_sum = UI.input("Device Sum: ").to_i  

    if device_sum == 0  

      next  

    end  

    index = 0  

    while index < device_sum do  

      device_name = UI.input("Device Name: ")  

      device_udid = UI.input("Device UDID: ")  

      device_hash[device_name] = device_udid  

      index += 1  

    end  



    register_devices(  

        devices: device_hash  

    )  

    refresh_profiles  

  end  



  desc "文件批量添加设备到profile"  

  lane :add_devices_file do  

    register_devices(  

      devices_file: "fastlane/devices.txt"  

    )  

    refresh_profiles  

  end  



  desc "批量导出设备"  

  lane :export_devices do  

    password = UI.password("输入 #{ENV["APPLE_ID"]} 账号密码: ")  

    Spaceship::Portal.login("#{ENV["APPLE_ID"]}", password)  

    Spaceship::Portal.select_team(team_id: "#{ENV["TEAM_ID"]}")  

    devices = Spaceship.device.all  

    File.open("#{base_path}/fastlane/devices.txt", "wb") do |f|  

      f.puts "Device ID\tDevice Name"  

      devices.each do |device|  

        f.puts "#{device.udid}\t#{device.name}"  

      end  

    end  

  end  





  desc "更新 provisioning profiles"  

  lane :refresh_profiles do  

    match(  

      type: "development",  

      force: true,  

      force_for_new_devices: true  

    )  

    match(  

      type: "adhoc",  

      force: true,  

      force_for_new_devices: true  

    )  

    match(  

      type: "appstore",  

      force: true,  

      force_for_new_devices: true  

    )  

  end  



  desc "同步 certificates 和 provisioning profiles"  

  lane :sync_cert_profiles do  

    match(  

      type: "development",  

      readonly: true  

    )  

    match(  

      type: "adhoc",  

      readonly: true  

    )  

    match(  

      type: "appstore",  

      readonly: true  

    )  

  end  



  desc "移除本地描述文件"  

  lane :remove_local_profiles do  

    app_identifiers = ["#{ENV["DEV_APP_IDENTIFIER"]}", "#{ENV["DEV_NOTIFICATION_SERVICE"]}", "#{ENV["PROD_APP_IDENTIFIER"]}", "#{ENV["PROD_NOTIFICATION_SERVICE"]}"]  

    types = ["development", "adhoc", "appstore"]  

    app_identifiers.each do |app_identifier|  

      types.each do |type|  

        remove_provisioning_profile(app_identifier: app_identifier, type: type)      

      end  

    end  

  end  



  desc "revoke 证书和描述文件"  

  private_lane :revoke_cert_profiles do  

    ENV["MATCH_SKIP_CONFIRMATION"] = "1"  

    sh("fastlane match nuke development")  

    sh("fastlane match nuke distribution")  

  end  



  desc "生成APNs证书"  

  lane :generate_apns_cert do  

    pem(  

      development: true,   

      force: true,   

      app_identifier: "#{ENV["DEV_APP_IDENTIFIER"]}",   

      p12_password: "GXM", output_path: "fastlane/pem"  

    )  



    pem(  

      development: false,   

      force: true,   

      app_identifier: "#{ENV["DEV_APP_IDENTIFIER"]}",   

      p12_password: "GXM", output_path: "fastlane/pem"  

    )  



    pem(  

      development: true,   

      force: true,   

      app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}",   

      p12_password: "GXM", output_path: "fastlane/pem"  

    )  



    pem(  

      development: false,   

      force: true,   

      app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}",   

      p12_password: "GXM", output_path: "fastlane/pem"  

    )  

  end  



  desc "同步 metadata"  

  lane :sync_metadata do  

    ENV["DELIVER_FORCE_OVERWRITE"] = "1"  

    sh("fastlane deliver download_metadata --metadata_path #{ENV["DOWNLOAD_METADATA_PATH"]}")  

  end  



  desc "拷贝 dSYM"  

  private_lane :copy_dsym do |options|  

    type = String(options[:type] || "adhoc")  

    dsym_path = lane_context[SharedValues::DSYM_OUTPUT_PATH]  

    share_dir = File.join(ENV['HOME'],'/Public/iOS', "#{ENV["SCHEME_NAME"]}", "#{type}")  

    FileUtils.mkdir_p(share_dir)  

    FileUtils.cp_r(File.join(dsym_path), share_dir)  

  end  



  desc "上传 ipa,type: [pgyer,gxm], log: desc"  

  private_lane :upload_ipa do |options|  

    type = options[:type] || 'pgyer'  

    log = options[:log] || ''  

    log = String  

    if type == "pgyer"  

      pgyer(  

        api_key: '0098b94391ff417d86837343597789a9',  

        user_key: '4ca1278171177f624ba3f3cc39eb2d73',  

        update_description: log  

      )  

    else  

      sh("curl -X 'POST' 'https://fabu.guoxiaomei.com/api/apps/5dca5121f3920d001f71e42d/upload' -H 'Content-Type: multipart/form-data' -H 'accept: application/json' -H 'apikey: 07a0840834294e7b89c41ab9c302c852' -F 'file=@#{lane_context[SharedValues::IPA_OUTPUT_PATH]}'")  

    end  

  end  



  after_all do |lane|  











  end  



  error do |lane, exception|  









  end  

end  

以上 fastlane 满足基本的功能需求。

iOS应用架构谈 组件化方案2

#
在现有工程中实施基于CTMediator的组件化方案

From: https://casatwy.com/modulization_in_action.html

国内业界大家对组件化的讨论从今年年初开始到年尾,不外乎两个方案:URL/protocol注册调度,runtime调度。

我之前批评过URL注册调度是错误的组件化实施方案,在所有的基于URL注册调度的方案中,存在两个普遍问题:

  1. 命名域渗透
  2. 因注册是不必要的,而带来同样不必要的注册列表维护成本

其它各家的基于URL注册的不同方案在这两个普遍问题上还有各种各样的其他问题,例如FRDIntent库中的FRDIntent对象其本质是鸡肋对象、原属于响应者的业务被渗透到调用者的业务中、组件化实施方案的过程中会产生对原有代码的侵入式修改等问题。

另外,我也发现还是有人在都没有理解清楚的前提下就做出了自己的解读,流毒甚广。我之前写过关于CTMediator比较理论的描述,也有Demo,但惟独没有写实践方面的描述。我本来以为Demo就足够了,可现在看来还是要给一篇实践的文章的。

在更早之前,卓同学的swift老司机群里也有人提出因为自己并没有理解透彻CTMediator方案,所以不敢贸然直接在项目中应用。所以这篇文章的另一个目的也是希望能够让大家明白,基于CTMediator的组件化方案实施其实非常简单,而且也是有章法可循的。这篇文章可能会去讨论一些理论的东西,但主要还会是以实践为主。争取做到能够让大家看完文章之后就可以直接在自己的项目中顺利实施组件化。

最后,我希望这篇文章能够终结业界持续近一年的关于组件化方案的无谓讨论和错误讨论。

我在github上开了一个orgnization,里面有一个主工程:MainProject,我们要针对这个工程来做组件化。组件化实施完毕之后的主工程就是ModulizedMainProject了。抽出来的独立Pod、私有Pod源也都会放在这个orgnization中去。

在一个项目实施组件化方案之前,我们需要做一个准备工作,建立自己的私有Pod源和快手工具脚本的配置:

  1. 先去开一个repo,这个repo就是我们私有Pod源仓库
  2. pod repo add [私有Pod源仓库名字] [私有Pod源的repo地址]
  3. 创立一个文件夹,例如Project。把我们的主工程文件夹放到Project下:~/Project/MainProject
  4. 在~/Project下clone快速配置私有源的脚本repo:git clone git@github.com:casatwy/ConfigPrivatePod.git
  5. 将ConfigPrivatePod的template文件夹下Podfile中source 'https://github.com/ModulizationDemo/PrivatePods.git'改成第一步里面你自己的私有Pod源仓库的repo地址
  6. 将ConfigPrivatePod的template文件夹下upload.sh中PrivatePods改成第二步里面你自己的私有Pod源仓库的名字

最后你的文件目录结构应该是这样:

Project
├── ConfigPrivatePod
└── MainProject

到此为止,准备工作就做好了。

MainProject是一个非常简单的应用,一共就三个页面。首页push了AViewController,AViewController里又push了BViewController。我们可以理解成这个工程由三个业务组成:首页、A业务、B业务。

我们这一次组件化的实施目标就是把A业务组件化出来,首页和B业务都还放在主工程。

因为在实际情况中,组件化是需要循序渐进地实施的。尤其是一些已经比较成熟的项目,业务会非常多,一时半会儿是不可能完全组件化的。CTMediator方案在实施过程中,对主工程业务的影响程度极小,而且是能够支持循序渐进地改造方式的。这个我会在文章结尾做总结的时候提到。

既然要把A业务抽出来作为组件,那么我们需要为此做两个私有Pod:A业务Pod(以后简称A Pod)、方便其他人调用A业务的CTMediator category的Pod(以后简称A_Category Pod)。这里多解释一句:A_Category Pod本质上只是一个方便方法,它对A Pod不存在任何依赖。

我们先创建A Pod

  1. 新建Xcode工程,命名为A,放到Projects下
  2. 新建Repo,命名也为A,新建好了之后网页不要关掉

此时你的文件目录结构应该是这样:

Project
├── ConfigPrivatePod
├── MainProject
└── A

然后cd到ConfigPrivatePod下,执行./config.sh脚本来配置A这个私有Pod。脚本会问你要一些信息,Project Name就是A,要跟你的A工程的目录名一致。HTTPS RepoSSH Repo网页上都有,Home Page URL就填你A Repo网页的URL就好了。

这个脚本是我写来方便配置私有库的脚本,pod lib create也可以用,但是它会直接从github上拉一个完整的模版工程下来,只是国内访问github其实会比较慢,会影响效率。而且这个配置工作其实也不复杂,我就索性自己写了个脚本。

这个脚本要求私有Pod的文件目录要跟脚本所在目录平级,也会在XCode工程的代码目录下新建一个跟项目同名的目录。放在这个目录下的代码就会随着Pod的发版而发出去,这个目录以外的代码就不会跟随Pod的版本发布而发布,这样子写用于测试的代码就比较方便。

然后我们在主工程中,把属于A业务的代码拎出来,放到新建好的A工程的A文件夹里去,然后拖放到A工程中。原来主工程里面A业务的代码直接删掉,此时主工程和A工程编译不过都是正常的,我们会在第二步中解决主工程的编译问题,第三步中解决A工程的编译问题。

此时你的主工程应该就没有A业务的代码了,然后你的A工程应该是这样:

A
├── A
|   ├── A
|   │   ├── AViewController.h
|   │   └── AViewController.m
|   ├── AppDelegate.h
|   ├── AppDelegate.m
|   ├── ViewController.h
|   ├── ViewController.m
|   └── main.m
└── A.xcodeproj

我们再创建A_Category Pod

同样的,我们再创建A_Category,因为它也是个私有Pod,所以也照样子跑一下config.sh脚本去配置一下就好了。最后你的目录结构应该是这样的:

Project
├── A
│   ├── A
│   │   ├── A
│   │   ├── AppDelegate.h
│   │   ├── AppDelegate.m
│   │   ├── Assets.xcassets
│   │   ├── Info.plist
│   │   ├── ViewController.h
│   │   ├── ViewController.m
│   │   └── main.m
│   ├── A.podspec
│   ├── A.xcodeproj
│   ├── FILE_LICENSE
│   ├── Podfile
│   ├── readme.md
│   └── upload.sh
├── A_Category
│   ├── A_Category
│   │   ├── A_Category
│   │   ├── AppDelegate.h
│   │   ├── AppDelegate.m
│   │   ├── Info.plist
│   │   ├── ViewController.h
│   │   ├── ViewController.m
│   │   └── main.m
│   ├── A_Category.podspec
│   ├── A_Category.xcodeproj
│   ├── FILE_LICENSE
│   ├── Podfile
│   ├── readme.md
│   └── upload.sh
├── ConfigPrivatePod
│   ├── config.sh
│   └── templates
└── MainProject
    ├── FILE_LICENSE
    ├── MainProject
    ├── MainProject.xcodeproj
    ├── MainProject.xcworkspace
    ├── Podfile
    ├── Podfile.lock
    ├── Pods
    └── readme.md

然后去A_Category下,在Podfile中添加一行pod "CTMediator",在podspec文件的后面添加s.dependency "CTMediator",然后执行pod install --verbose

接下来打开A_Category.xcworkspace,把脚本生成的名为A_Category的空目录拖放到Xcode对应的位置下,然后在这里新建基于CTMediator的Category:CTMediator+A。最后你的A_Category工程应该是这样的:

A_Category
├── A_Category
|   ├── A_Category
|   │   ├── CTMediator+A.h
|   │   └── CTMediator+A.m
|   ├── AppDelegate.h
|   ├── AppDelegate.m
|   ├── ViewController.h
|   └── ViewController.m
└── A_Category.xcodeproj

到这里为止,A工程和A_Category工程就准备好了。

去主工程的Podfile下添加pod "A_Category", :path => "../A_Category"来本地引用A_Category。

然后编译一下,说找不到AViewController的头文件。此时我们把头文件引用改成#import <A_Category/CTMediator+A.h>

然后继续编译,说找不到AViewController这个类型。看一下这里是使用了AViewController的地方,于是我们在Development Pods下找到CTMediator+A.h,在里面添加一个方法:

- (UIViewController *)A_aViewController;

再去CTMediator+A.m中,补上这个方法的实现,把主工程中调用的语句作为注释放进去,将来写Target-Action要用:

- (UIViewController *)A_aViewController
{
    /*
        AViewController *viewController = [[AViewController alloc] init];
     */
    return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO];
}

补充说明一下,performTarget:@"A"中给到的@"A"其实是Target对象的名字。一般来说,一个业务Pod只需要有一个Target就够了,但一个Target下可以有很多个Action。Action的名字也是可以随意命名的,只要到时候Target对象中能够给到对应的Action就可以了。

关于Target-Action我们会在第三步中去实现,现在不实现Target-Action是不影响主工程编译的。

category里面这么写就已经结束了,后面的实施过程中就不会再改动到它了。

然后我们把主工程调用AViewController的地方改为基于CTMediator Category的实现:

UIViewController *viewController = [[CTMediator sharedInstance] A_aViewController];
[self.navigationController pushViewController:viewController animated:YES];

再编译一下,编译通过。

到此为止主工程就改完了,现在跑主工程点击这个按钮跳不到A页面是正常的,因为我们还没有在A工程中实现Target-Action。

而且此时主工程中关于A业务的改动就全部结束了,后面的组件化实施过程中,就不会再有针对A业务线对主工程的改动了。

此时我们关掉所有XCode窗口。然后打开两个工程:A_Category工程和A工程。

我们在A工程中创建一个文件夹:Targets,然后看到A_Category里面有performTarget:@"A",所以我们新建一个对象,叫做Target_A

然后又看到对应的Action是viewController,于是在Target_A中新建一个方法:Action_viewController。这个Target对象是这样的:

头文件:
#import <UIKit/UIKit.h>

@interface Target_A : NSObject

- (UIViewController *)Action_viewController:(NSDictionary *)params;

@end

实现文件:
#import "Target_A.h"
#import "AViewController.h"

@implementation Target_A

- (UIViewController *)Action_viewController:(NSDictionary *)params
{
    AViewController *viewController = [[AViewController alloc] init];
    return viewController;
}

@end

这里写实现文件的时候,对照着之前在A_Category里面的注释去写就可以了。

因为Target对象处于A的命名域中,所以Target对象中可以随意import A业务线中的任何头文件。

另外补充一点,Target对象的Action设计出来也不是仅仅用于返回ViewController实例的,它可以用来执行各种属于业务线本身的任务。例如上传文件,转码等等各种任务其实都可以作为一个Action来给外部调用,Action完成这些任务的时候,业务逻辑是可以写在Action方法里面的。

换个角度说就是:Action具备调度业务线提供的任何对象和方法来完成自己的任务的能力。它的本质就是对外业务的一层服务化封装。

现在我们这个Action要完成的任务只是实例化一个ViewController并返回出去而已,根据上面的描述,Action可以完成的任务其实可以更加复杂。

然后我们再继续编译A工程,发现找不到BViewController。由于我们这次组件化实施的目的仅仅是将A业务线抽出来,BViewController是属于B业务线的,所以我们没必要把B业务也从主工程里面抽出来。但为了能够让A工程编译通过,我们需要提供一个B_Category来使得A工程可以调度到B,同时也能够编译通过。

B_Category的创建步骤跟A_Category是一样的,不外乎就是这几步:新建Xcode工程、网页新建Repo、跑脚本配置Repo、添加Category代码。

B_Category添加好后,我们同样在A工程的Podfile中本地指过去,然后跟在主工程的时候一样。

所以B_Category是这样的:

头文件:
#import <CTMediator/CTMediator.h>
#import <UIKit/UIKit.h>

@interface CTMediator (B)

- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;

@end

实现文件:
#import "CTMediator+B.h"

@implementation CTMediator (B)

- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText
{
    /*
        BViewController *viewController = [[BViewController alloc] initWithContentText:@"hello, world!"];
     */
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"contentText"] = contentText;
    return [self performTarget:@"B" action:@"viewController" params:params shouldCacheTarget:NO];
}

@end

然后我们对应地在A工程中修改头文件引用为#import <B_Category/CTMediator+B.h>,并且把调用的代码改为:

UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"hello, world!"];
[self.navigationController pushViewController:viewController animated:YES];

此时再编译一下,编译通过了。注意哦,这里A业务线跟B业务线就已经完全解耦了,跟主工程就也已经完全解耦了。

此时还有一个收尾工作是我们给B业务线创建了Category,但没有创建Target-Action。所以我们要去主工程创建一个B业务线的Target-Action。创建的时候其实完全不需要动到B业务线的代码,只需要新增Target_B对象即可:

Target_B头文件:
#import <UIKit/UIKit.h>

@interface Target_B : NSObject

- (UIViewController *)Action_viewController:(NSDictionary *)params;

@end

Target_B实现文件:
#import "Target_B.h"
#import "BViewController.h"

@implementation Target_B

- (UIViewController *)Action_viewController:(NSDictionary *)params
{
    NSString *contentText = params[@"contentText"];
    BViewController *viewController = [[BViewController alloc] initWithContentText:contentText];
    return viewController;
}

@end

这个Target对象在主工程内不存在任何侵入性,将来如果B要独立成一个组件的话,把这个Target对象带上就可以了。

收尾工作就到此结束,我们创建了三个私有Pod:A、A_Category、B_Category。

接下来我们要做的事情就是给这三个私有Pod发版,发版之前去podspec里面确认一下版本号和dependency。

Category的dependency是不需要填写对应的业务线的,它应该是只依赖一个CTMediator就可以了。其它业务线的dependency也是不需要依赖业务线的,只需要依赖业务线的Category。例如A业务线只需要依赖B_Category,而不需要依赖B业务线或主工程。

发版过程就是几行命令:

git add .
git commit -m "版本号"
git tag 版本号
git push origin master --tags
./upload.sh

命令行cd进入到对应的项目中,然后执行以上命令就可以了。

要注意的是,这里的版本号要和podspec文件中的s.version给到的版本号一致。upload.sh是配置私有Pod的脚本生成的,如果你这边没有upload.sh这个文件,说明这个私有Pod你还没用脚本配置过。

最后,所有的Pod发完版之后,我们再把Podfile里原来的本地引用改回正常引用,也就是把:path...那一段从Podfile里面去掉就好了,改动之后记得commit并push。

组件化实施就这么三步,到此结束。

hard code

这个组件化方案的hard code仅存在于Target对象和Category方法中,影响面极小,并不会泄漏到主工程的业务代码中,也不会泄漏到业务线的业务代码中。

而且在实际组件化的实施中,也是依据category去做业务线的组件化的。所以先写category里的target名字,action名字,param参数,到后面在业务线组件中创建Target的时候,照着category里面已经写好的内容直接copy到Target对象中就肯定不会出错(仅Target对象,并不会牵扯到业务线本身原有的对象)。

如果要消除这一层hard code,那么势必就要引入一个第三方pod,然后target对象所在的业务线和category都要依赖这个pod。为了消除这种影响面极小的hard code,而且只要按照章法来就不会出错。为此引入一个新的依赖,其实是不划算的。

命名域问题

在这个实践中,响应者的命名域并没有泄漏到除了响应者以外的任何地方,这就带来一个好处,迁移非常方便。

比如我们的响应者是一个上传组件。这个上传组件如果要替换的话,只需要在它外面包一个Target-Action,就可以直接拿来用了。而且包Target-Action的过程中,不会产生任何侵入性的影响。

例如原来是你自己基于AFNetworking写的上传组件,现在用了七牛SDK上传,那么整个过程你只需要提供一个Target-Action封装一下七牛的上传操作即可。不需要改动七牛SDK的代码,也不需要改动调用方的代码。倘若是基于URL注册的调度,做这个事情就很蛋疼。

服务管理问题

由于Target对象处于响应者的命名域中,Target对象就可以对外提供除了页面实例以外的各种Action。

而且,由于其本质就是针对响应者对外业务逻辑的Action化封装(其实就是服务化封装),这就能够使得一个响应者对外提供了哪些Action(服务)Action(服务)的实现逻辑是什么得到了非常好的管理,能够大大降低将来工程的维护成本。然后Category解决了服务应该怎么调用的问题。

但在基于URL注册机制和Protocol共享机制的组件化方案中,由于服务散落在响应者各处,服务管理就显得十分困难。如果还是执念于这样的方案,大家只要拿上面提到的三个问题,对照着URL注册机制和Protocol共享机制的组件化方案比对一下,就能明白了。

另外,如果这种方案把所有的服务归拢到一个对象中来达到方便管理的目的的话,其本质就已经变成了Target-Action模式,Protocol共享机制其实就已经没有存在意义了。

高内聚

基于protocol共享机制的组件化方案导致响应者业务逻辑泄漏到了调用者业务逻辑中,并没有做到高内聚

如果这部分业务在其他地方也要使用,那么代码就要重新写一遍。虽然它可以提供一个业务高内聚的对象来符合这个protocol,但事实上这就又变成了Target-Action模式,protocol的存在意义就也没有了。

侵入性问题

正如你所见,CTMediator组件化方案的实施非常安全。因为它并不存在任何侵入性的代码修改。

对于响应者来说,什么代码都不用改,只需要包一层Target-Action即可。例如本例中的B业务线作为A业务的响应者时,不需要修改B业务的任何代码。

对于调用者来说,只需要把调用方式换成CTMediator调用即可,其改动也不涉及原有的业务逻辑,所以是十分安全的。

另外一个非侵入性的特征体现在,基于CTMediator的组件化方案是可以循序渐进地实施的。这个方案的实施并不要求所有业务线都要被独立出来成为组件,实施过程也并不会修改未组件化的业务的代码。

在独立A业务线的过程中如果涉及其它业务线(B业务线)的调用,就只需要给到Target对象即可,Target对象本身并不会对未组件化的业务线(B业务线)产生任何的修改。而且将来如果对应业务线需要被独立出去的时候,也仅需要把Target对象一起复制过去就可以了。

但在基于URL注册和protocol共享的组件化方案中,都必须要在未组件化的业务线中写入注册代码和protocol声明,并分配对应的URL和protocol到具体的业务对象上。这些其实都是不必要的,无端多出了额外维护成本。

注册问题

CTMediator没有任何注册逻辑的代码,避免了注册文件的维护和管理。Category给到的方法很明确地告知了调用者应该如何调用。

例如B_Category给到的- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;方法。这能够让工程师一眼就能够明白使用方式,而不必抓瞎拿着URL再去翻文档。

这可以很大程度提高工作效率,同时降低维护成本。

实施组件化方案的时机

MVP阶段过后,越早实施越好。

这里说的MVP不是一种设计模式,而是最小价值产品的意思,它是产品演进的第一个阶段。

一般来说天使轮就是用于MVP验证的,在这个阶段产品闭环尚未确定,因此产品本身的逻辑就会各种变化。但是过了天使轮之后,产品闭环已经确定,此时就应当实施组件化,以应对A轮之后的产品拓张。

有的人说我现在项目很小,人也很少,所以没必要实施组件化。确实,把一个小项目组件化之后,跟之前相比并没有多大程度的改善,因为本来小项目就不复杂,改成组件化之后,也不会更简单。

但这其实是一种很短视的认知。

组件化对于一个小项目而言,真正发挥优势的地方是在未来的半年甚至一年之后。

因为趁着人少项目小,实施组件化的成本就也很小,三四天就可以实施完毕。于是等将来一年之后业务拓张到更大规模时,就不会束手束脚了。

但如果等到项目大了,人手多了再去实施组件化,那时候实施组件化的复杂度肯定比现在规模还很小的时候的复杂度要大得多,三四天肯定搞不定,而且实施过程还会非常艰辛。到那时你就后悔为什么当初没有早早实施组件化了。

Swift工程怎么办?

其实只要Target对象继承自NSObject就好了,然后带上@objc(className)。action的参数名永远只有一个,且名字需要固定为params,其它照旧。具体swift工程中target的写法参见A_swift

因为Target对象是游离于业务实现的,所以它去继承NSObject完全没有任何问题。完整的SwiftDemo在这里。


本文Demo

WKWebView的图片二维码使用

From: https://www.cnblogs.com/gongyuhonglou/p/8888487.html

(未验证, 看到此文,后续有需要求再看)

WKWebView的图片二维码使用:

1.长按手势识别二维码并保存
2.识别二维码跳转;不是链接显示内容点击网址跳转
3.解决url包含中文不能编码的问题
4.文字带链接网址,点击跳转
5.纯文本-文字html展示可拷贝,查询
6.解决html页面适配屏幕宽度的问题和保留源文件的格式
7.判断是web网页图片否存在二维码并进行识别

代码:

// 添加长按手势识别二维码

    [self WKWebViewHandleLongPress:_detailWebView];



    // 识别二维码跳转;不是链接显示内容点击网址跳转

    if ([self.m_url hasPrefix:@"http"]) {

//        NSString *urlStr = [self.m_url stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

        // 解决url包含中文不能编码的问题

        NSString *urlStr = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,(CFStringRef)self.m_url,(CFStringRef)@"!$&'()*+,-./:;=?@_~%#[]",NULL,kCFStringEncodingUTF8));

        NSURL *url = [NSURL URLWithString:urlStr];

        NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url];

        [request setHTTPShouldHandleCookies:YES];

        [_detailWebView loadRequest:request];

    } else {

        self.topTitleLabel.text = @"扫描结果";

        QRCodeLabel = [[ZXLabel alloc]initWithFrame:CGRectMake(10, 10, SCREENWIDTH-20, 50) fontSize:16 text:@"" textColor:[UIColor colorWithHexString:@"#000000"] textAlignment:NSTextAlignmentLeft numberOfLines:0];

        [_detailWebView addSubview:QRCodeLabel];



        if ([self urlValidation:[NSString stringWithFormat:@"%@",self.m_url]]==YES) {//网址,点击跳转

            [self textColour];

            QRurlStr = [NSString stringWithFormat:@"http://%@",[NSString stringWithFormat:@"%@",self.m_url]];

        } else {



    // 文字html可拷贝,查询

    //            "<html><head><meta charset='UTF-8'><meta name='viewport content=initial-scale=1,maximum-scale=1, minimum-scale=1'><meta name='apple-mobile-web-app-capable' content='yes'><meta name='apple-mobile-web-app-status-bar-style' content='black'><meta name='format-detection' content='telephone=no'><title></title><style>*{margin: 0;padding: 0;}html,body {width: 100%;height: 100%;}p {width: 90%;font-size: 16px;color: #333;text-align: justify;}</style></head><body><p></p ></body></html>"



    //            @"<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'><meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no'><style>*{margin:5px;padding:0px;}</style><title></title></head<body><pre>%@</pre></body></html>"



    //            @"<html><head><meta charset='UTF-8'><meta name='viewport' content='initial-scale=1,maximum-scale=1, minimum-scale=1'><meta name='apple-mobile-web-app-capable' content='yes'><meta name='apple-mobile-web-app-status-bar-style' content='black'><meta name='format-detection' content='telephone=no'><title></title><style>*{margin: 0;padding: 0;box-sizing: border-box;}p {width: 6.4rem;padding: .2rem;font-size: .2rem;color: #333;text-align: justify;}</style></head><body><p>%@</p ></body><script>(function() { function getViewPort()  {if(document.compatMode == 'BackCompat') { return {width: document.body.clientWidth,height: document.body.clientHeight}; } else {return {width: document.documentElement.clientWidth,height: document.documentElement.clientHeight};} }function screenZoom() {var _obj = getViewPort(); var Width = _obj.width;var Height = _obj.height;if (Width>640) { Width = 640;}document.documentElement.style.fontSize = Width/6.4 + 'px';}screenZoom();window.onresize = function() {screenZoom();};})();</script></html>"

            // 解决html页面适配屏幕宽度的问题和保留源文件的格式

            QRurlStr = [NSString stringWithFormat:

                        @"<html><head><meta charset='UTF-8'><meta name='viewport' content='initial-scale=1,maximum-scale=1, minimum-scale=1'><meta name='apple-mobile-web-app-capable' content='yes'><meta name='apple-mobile-web-app-status-bar-style' content='black'><meta name='format-detection' content='telephone=no'><title></title><style>*{margin: 0;padding: 0;box-sizing: border-box;}pre {/* width: 6.4rem; */padding: 10px;font-size: 15px;color: #333;text-align: justify;white-space: pre-wrap; /*css-3*/white-space: -moz-pre-wrap; /*Mozilla,since1999*/ white-space: -pre-wrap; /*Opera4-6*/white-space: -o-pre-wrap; /*Opera7*/word-wrap: break-word; /*InternetExplorer5.5+*/ }</style></head><body><pre>%@</pre></body><script>(function() { function getViewPort()  {if(document.compatMode == 'BackCompat') { return {width: document.body.clientWidth,height: document.body.clientHeight}; } else {return {width: document.documentElement.clientWidth,height: document.documentElement.clientHeight};} }function screenZoom() {var _obj = getViewPort(); var Width = _obj.width;var Height = _obj.height;if (Width>640) { Width = 640;}document.documentElement.style.fontSize = Width/6.4 + 'px';}screenZoom();window.onresize = function() {screenZoom();};})();</script></html>",[NSString stringWithFormat:@"%@",self.m_url]];

            [_detailWebView loadHTMLString:QRurlStr  baseURL:Nil];

        }

    }





#pragma mark -- 识别图中二维码

// app内部识别二维码

/**

 *  网址正则验证

 *

 *  @param string 要验证的字符串

 *

 *  @return 返回值类型为BOOL

 */

- (BOOL)urlValidation:(NSString *)string {

    NSError *error;

    NSString *regulaStr = @"((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$";

    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regulaStr options:NSRegularExpressionCaseInsensitive error:&error];

    NSArray *arrayOfAllMatches = [regex matchesInString:string options:0 range:NSMakeRange(0, [string length])];

    for (NSTextCheckingResult *match in arrayOfAllMatches){

        NSString* substringForMatch = [string substringWithRange:match.range];

        NSLog(@"匹配--%@",substringForMatch);

        return YES;

    }

    return NO;

}



- (void)textColour {

    NSMutableAttributedString *abs = [[NSMutableAttributedString alloc]initWithString:[NSString stringWithFormat:@"%@",self.m_url]];

    [abs beginEditing];

    //字体大小

    //        [abs addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:20.0] range:NSMakeRange(0, 2)];

    //字体颜色

    [abs addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor]  range:NSMakeRange(0, [NSString stringWithFormat:@"%@",self.m_url].length)];

    //下划线

    [abs addAttribute:NSUnderlineStyleAttributeName  value:@(NSUnderlineStyleSingle) range:NSMakeRange(0, [NSString stringWithFormat:@"%@",self.m_url].length)];

    QRCodeLabel.attributedText = abs;

    UITapGestureRecognizer *LabelTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(capchaBtn:)];

    QRCodeLabel.userInteractionEnabled = YES;

    [QRCodeLabel addGestureRecognizer:LabelTap];

}



// 链接跳转

- (void)capchaBtn:(UITapGestureRecognizer *)sendr{

    NSLog(@"跳转网页~~");

    [QRCodeLabel removeFromSuperview];

    NSString *urlStr = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,(CFStringRef)QRurlStr,(CFStringRef)@"!$&'()*+,-./:;=?@_~%#[]",NULL,kCFStringEncodingUTF8));

    NSURL *url =[NSURL URLWithString:urlStr];

    NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url];

    [request setHTTPShouldHandleCookies:YES];

    [_detailWebView loadRequest:request];

}







#pragma mark -- common WKWebView

-(void)WKWebViewHandleLongPress:(WKWebView *)webView {

    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(webViewHandleLongPress:)];

    longPress.minimumPressDuration = 0.2;

    longPress.delegate = self;

    m_webView = webView;

    [webView addGestureRecognizer:longPress];

}



- (void)webkitTouchCallout:(WKWebView *)webView

{

    // 不执行前段界面弹出列表的JS代码

    [webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil];

    [webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none'" completionHandler:nil];

}



// 是否允许支持多个手势,默认是不支持:NO

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{

    return YES;

}



// 网页内长按识别二维码

- (void)webViewHandleLongPress:(UILongPressGestureRecognizer *)sender{

    if (sender.state == UIGestureRecognizerStateBegan) {



        CGPoint touchPoint = [sender locationInView:m_webView];

        // 获取长按位置对应的图片url的JS代码

        NSString *imgJS = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", touchPoint.x, touchPoint.y];

        // 执行对应的JS代码 获取url

        [m_webView evaluateJavaScript:imgJS completionHandler:^(id _Nullable imgUrl, NSError * _Nullable error) {

            if (imgUrl) {

                NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imgUrl]];

                UIImage *image = [UIImage imageWithData:data];

                if (!image) {

                    NSLog(@"读取图片失败");

                    return;

                }

                _saveImage = image;



                // 禁用选中效果

                [self webkitTouchCallout:m_webView];



                if ([self isAvailableQRcodeIn:image]) {

                    [self filterPopViewWithTag:100002 WithTitleArray:[NSMutableArray arrayWithObjects:@"保存图片",@"识别图中二维码",nil]];

                } else {

                    [self filterPopViewWithTag:100001 WithTitleArray:[NSMutableArray arrayWithObjects:@"保存图片",nil]];

                }



            } else {

                // 选中效果

                [m_webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='text'" completionHandler:nil];

                [m_webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='text'" completionHandler:nil];

            }

        }];

    }

}



#pragma mark -- RomAlertViewDelegate 弹框识别图中二维码

// 判断是web网页图片否存在二维码

- (BOOL)isAvailableQRcodeIn:(UIImage *)img {



    //方法:一

//    UIGraphicsBeginImageContextWithOptions(img.size, NO, 3);//0,获取当前屏幕分辨率[UIScreen mainScreen].scale

//    CGContextRef context = UIGraphicsGetCurrentContext();

//    [self.view.layer renderInContext:context];

//    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

//    UIGraphicsEndImageContext();



    //方法:二

    UIImage *image = [self snapshot:self.view];



    //方法:三

//    UIImage *image = [self imageByInsetEdge:UIEdgeInsetsMake(-20, -20, -20, -20) withColor:[UIColor lightGrayColor] withImage:img];





    CIImage *ciImage = [[CIImage alloc] initWithCGImage:image.CGImage options:nil];

    CIContext *ciContext = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @(YES)}]; // 软件渲染

    CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:ciContext options:@{CIDetectorAccuracy : CIDetectorAccuracyHigh}];// 二维码识别



    NSArray *features = [detector featuresInImage:ciImage];

    if (features.count > 0) {

        //        for (CIQRCodeFeature *feature in features) {

        //        NSLog(@"qrCodeUrl = %@",feature.messageString); // 打印二维码中的信息

        //        qrCodeUrl = feature.messageString;

        //    }

        CIQRCodeFeature *feature = [features objectAtIndex:0];

        qrCodeUrl = [feature.messageString copy];

        NSLog(@"二维码信息:%@", qrCodeUrl);

        return YES;

    } else {

        NSLog(@"图片中没有二维码");

        return NO;

    }

}



// you can also implement by UIView category

- (UIImage *)snapshot:(UIView *)view

{

    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 3);//view.bounds.size, YES, view.window.screen.scale



    if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {

        [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];

    }

    UIImage* image = UIGraphicsGetImageFromCurrentImageContext();



    UIGraphicsEndImageContext();



    return image;

}

// you can also implement by UIImage category

- (UIImage *)imageByInsetEdge:(UIEdgeInsets)insets withColor:(UIColor *)color withImage:(UIImage *)image

{

    CGSize size = image.size;

    size.width -= insets.left + insets.right;

    size.height -= insets.top + insets.bottom;

    if (size.width <= 0 || size.height <= 0) {

        return nil;

    }

    CGRect rect = CGRectMake(-insets.left, -insets.top, image.size.width, image.size.height);

    UIGraphicsBeginImageContextWithOptions(size, NO, image.scale);

    CGContextRef context = UIGraphicsGetCurrentContext();

    if (color) {

        CGContextSetFillColorWithColor(context, color.CGColor);

        CGMutablePathRef path = CGPathCreateMutable();

        CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));

        CGPathAddRect(path, NULL, rect);

        CGContextAddPath(context, path);

        CGContextEOFillPath(context);

        CGPathRelease(path);

    }

    [image drawInRect:rect];

    UIImage *insetEdgedImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();



    return insetEdgedImage;

}



// 网页内部识别二维码

- (void)alertview:(RomAlertView *)alertview didSelectWebRowAtIndexPath:(NSIndexPath *)indexPath

{

    if (alertview.tag == 100001) {

        if ([alertview.otherTitles[indexPath.row]  isEqualToString:@"保存图片"]) {

            NSLog(@"保存图片");

//            UIImageWriteToSavedPhotosAlbum(_saveImage, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);

            [self saveWebLongPressed];



        }

    } else if (alertview.tag == 100002) {

        if ([alertview.otherTitles[indexPath.row]  isEqualToString:@"保存图片"]) {

            NSLog(@"保存图片");

//            UIImageWriteToSavedPhotosAlbum(_saveImage, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);

            [self saveWebLongPressed];



        }else if ([alertview.otherTitles[indexPath.row] isEqualToString:@"识别图中二维码"]){

            NSLog(@"识别图中二维码");



            ADWebViewViewController *controller = [[ADWebViewViewController alloc] init];

            controller.m_url = qrCodeUrl;

            controller.hidesBottomBarWhenPushed = YES;

            [self.navigationController pushViewController:controller animated:YES];

        }

    }

}



//- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{

//    NSString *message = @"Succeed";

//    if (error) {

//        message = @"Fail";

//    }

//    NSLog(@"save result :%@", message);

//}



#pragma mark --web保存图片

//保存

- (void)saveWebLongPressed {

//    if (webPhotoSave == YES) { // 图片已经保存到相册 提示

//        [self.view makeToast:@"该图片已经保存到相册" duration:2 position:CSToastPositionCenter];

//        return;

//    }

    [self saveWebPhoto];

}



- (void)saveWebPhoto {



    PHAuthorizationStatus oldStatus = [PHPhotoLibrary authorizationStatus];

    [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {

        dispatch_async(dispatch_get_main_queue(), ^{

            switch (status) {

                case PHAuthorizationStatusAuthorized: {

                    //  保存图片到相册

                    [self saveWebImageIntoAlbum];

                    break;

                }

                case PHAuthorizationStatusDenied: {

                    if (oldStatus == PHAuthorizationStatusNotDetermined) return;

                    NSLog(@"提醒用户打开相册的访问开关");

                    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"无法保存"        message:@"请在iPhone的“设置-隐私-照片”选项中,允许访问你的照片。" delegate:self  cancelButtonTitle:@"确定" otherButtonTitles:nil];

                    [alert show];

                    break;

                }

                case PHAuthorizationStatusRestricted: {

                    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"无法保存"        message:@"因系统原因,无法访问相册!" delegate:self  cancelButtonTitle:@"确定" otherButtonTitles:nil];

                    [alert show];

                    break;

                }

                default:

                    break;

            }

        });

    }];

}





// 获得刚才添加到【相机胶卷】中的图片

- (PHFetchResult<PHAsset *> *)createdAssets {



    __block NSString *createdAssetId = nil;

    // 添加图片到【相机胶卷】

    [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{

        createdAssetId = [PHAssetChangeRequest creationRequestForAssetFromImage:_saveImage].placeholderForCreatedAsset.localIdentifier;

    } error:nil];

    if (createdAssetId == nil) return nil;

    // 在保存完毕后取出图片

    return [PHAsset fetchAssetsWithLocalIdentifiers:@[createdAssetId] options:nil];

}



//获得【自定义相册】

-(PHAssetCollection *)createdCollection {

    // 获取软件的名字作为相册的标题(如果需求不是要软件名称作为相册名字就可以自己把这里改成想要的名称)

    NSString *title = [NSBundle mainBundle].infoDictionary[(NSString *)kCFBundleNameKey];

    // 获得所有的自定义相册

    PHFetchResult<PHAssetCollection *> *collections = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];

    for (PHAssetCollection *collection in collections) {

        if ([collection.localizedTitle isEqualToString:title]) {

            return collection;

        }

    }

    // 代码执行到这里,说明还没有自定义相册

    __block NSString *createdCollectionId = nil;

    // 创建一个新的相册

    [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{

        createdCollectionId = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:title].placeholderForCreatedAssetCollection.localIdentifier;

    } error:nil];

    if (createdCollectionId == nil) return nil;

    // 创建完毕后再取出相册

    return [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[createdCollectionId] options:nil].firstObject;

}



//保存图片到相册

- (void)saveWebImageIntoAlbum {

    // 获得相片

    PHFetchResult<PHAsset *> *createdAssets = self.createdAssets;

    // 获得相册

    PHAssetCollection *createdCollection = self.createdCollection;

    if (createdAssets == nil || createdCollection == nil) {



        [self.view makeToast:@"图片保存失败!" duration:2 position:CSToastPositionCenter];

        return;

    }

    // 将相片添加到相册

    NSError *error = nil;

    [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{

        PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:createdCollection];

        [request insertAssets:createdAssets atIndexes:[NSIndexSet indexSetWithIndex:0]];

    } error:&error];

    // 保存结果

    NSString *msg = nil ;

    if(error){

        msg = @"图片保存失败!";

        [self.view makeToast:msg duration:2 position:CSToastPositionCenter];

    }else{

        msg = @"已成功保存到系统相册";

//        webPhotoSave = YES;

        [self.view makeToast:msg duration:2 position:CSToastPositionCenter];

    }

}

PS:WKWebView官网