组件的使用-Picker

为了统一,各视图分别使用如下组件

二、选择器Picker

1、事项选择器:ItemPickerUtil

事项选择器
1
2
3
4
5
6
7
8
9
10
import 'package:flutter_datepicker/flutter_datepicker.dart';

ItemPickerUtil.chooseItem(
context,
title: '更换头像',
itemTitles: ['拍照上传', '从相册选择'],
onConfirm: (int selectedIndex) {
dealAvatar(selectedIndex);
},
);

2、日期选择器DatePickerUtil

日期选择器
1
2
3
4
5
6
7
8
9
10
11
import 'package:flutter_datepicker/flutter_datepicker.dart';

String currentBirthday = '2000-01-01';

DatePickerUtil.chooseBirthday(context,
title: '选择你的生日', selectedyyyyMMddDateString: currentBirthday,
onConfirm: (String yyyyMMddDateStirng) {
setState(() {
currentBirthday = yyyyMMddDateStirng;
});
});

页面多入口优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class WishPublishPage extends BJHBasePage {
bool isUpdateBusiness;
Wish_detail_model wishDetailModel;

WishPublishPage({
Key key,
this.isUpdateBusiness = false,
this.wishDetailModel,
}) : super(key: key);

WishPublishPage.fromTemplatePage({
Key key,
this.isUpdateBusiness = false,
WishTemplateModel templateModel,
}) : super(key: key) {
wishDetailModel =
Wish_detail_model.fromTemplateJson(templateModel.toJson());
}

WishPublishPage.fromOtherUser({
Key key,
List<Items> selectGoodsList,
}) : super(key: key) {
isUpdateBusiness = false;
wishDetailModel.items = selectGoodsList;
}

@override
_WishPublishPageState createState() => _WishPublishPageState();
}

框架的重要性

一、ppt

详看:框架的重要性.pptx

总框架结构图.graffle

二、内容说明

框架是什么?

框架是解决问题的具体实现方法,能直接执行或复用。

为什么需要框架?

简单的讲是为了约束和统一。

举个例子:我们想要让所有的页面支持在无网络的时候,都有个缺省页。

如果没有一个框架,则我们每个页面都得进行很多判断和视图操作。

且后期如果需要修改,也会导致工作量巨大。

框架的好处是什么?

我们先从常见的几个工作中实际场景介绍:

网络请求:不进行框架/封装处理的话,可能遇到的问题:

①、每次进行网络请求,都得写一堆代码才能完成最基础的请求操作;

②、如果还要求对每个请求都添加一些公共参数、错误码处理,则又得每个请求添加一遍;

页面的缺省:不进行框架/封装处理的话,可能遇到的问题:

①、无网络等状态下,直接无缺省页,显示成白屏,体验极差;

②、有设置缺省页,但无进行框架化,导致每个页面都得堆一堆代码来实现缺省页功能;

③、有设置缺省页,但无进行框架化,后期需要修改时候,每个页面都修改,维护成本巨大;

视图控件(如按钮):不进行框架/封装处理的话,可能遇到的问题:

①、代码实现特长;

②、还没有点击效果,如果要添加又要一堆代码;

③、后期需要修改时候,每个页面都修改,维护成本巨大;

测试框架:

①、开发联调接口,无法设置代理抓包查看;

②、用户反馈问题,无法提供问题出现的版本等信息;

③、已发布包程序出现异常,开发无法定位;

场景问题解决要点/框架能解决的问题:

①、不用堆一堆类似代码,几行代码就实现

②、规范变化的时候,不要我关心和修改

③、增加通用功能的时候,不需要自己再去实现一遍

④、app异常时候的监测和反馈

框架化后,以上这些问题都能够得到解决。

所以,框架的好处有统一设计,

建立框架的意义:

好的框架能够保证和提升项目的可维护性,扩展性,健壮性。

能够提高工作效率

能够让风格更统一

各框架提供的功能

缺省页框架:

解决可能的初始”白屏”

无网络情况下的”空白页”

无数据情况下的”空白页”

为页面提供网络异常页并伴有刷新恢复重试

测试框架:

新增抓包设置(代理)

新增查看版本页面,方便对应反馈的问题出现的版本(防止是旧包)。

程序异常(上报+提示)

视图控件框架:

定义加载动画loading

愿望灯动画效果优化(底部下沉)

框架的使用

使用前(纯代码,未有任何封装):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
GestureDetector(
onTap: () => _shareWechatfriend(),
child: Container(
height: 38.h,
width: 260.w,
decoration: BoxDecoration(
color: color_theme,
borderRadius: BorderRadius.circular(19.h),
border: Border.all(color: Colors.white, width: 1),
),
child: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"images/wish/yaoqing_icon.png",
width: 18.w,
height: 18.h,
),
SizedBox(width: 5.w),
Text(
"邀请好友许愿",
style: TextStyle(color: Colors.white, fontSize: 15.sp),
)
],
),
),
),
),

框架化后:

1
2
3
4
5
6
7
8
9
10
11
ThemeBGButton(
width: 260,
height: 38,
bgColorType: ThemeBGType.pink, // 此参数,让你可以直接切换到其他主题样式
needHighlight: true, // 添加此参数即会有高亮效果,而不需要再写一堆代码
title: '邀请好友许愿',
titleStyle: ButtonThemeUtil.PingFang_FontSize_Bold(15.0),
imageWidget: ImageAsset('images/wish/yaoqing_icon.png', width: 18, height: 18),
cornerRadius: 19,
onPressed: () => _shareWechatfriend(),
),

基础框架

框架优化/升级方案

为了有更好的用户体验,我们提出以下优化和升级方案。

白屏体验

网络恢复

一、可能的初始”白屏”(可选)

1、场景

页面无任何占位视图。如页面本身就是一个列表。

页面数据来源于网络,网络请求回来前,空。

2、处理方案及实现方式

主要有以下两种方案,各自独立,分场景使用。

①界面框架预染页(美团/饿了么的灰底效果)

②数据预加载(首页)

2.1、处理方案:界面框架的预染页:美团/饿了么的灰底效果(如商品详情页)

2.1.1、方案描述

美团/饿了么的灰底效果(如商品详情页)

2.1.2、实现方式
1
2
3
4
5
6
7
8
9
// bad
return realWidgets(data: netdata); // netdata 可能为空,导致页面空白

// good
if (data == null) {
return nodataWidgets(); // 页面预览页
} else {
return withdataWidgets(data: data);
}

2.2、数据预加载(根据需要)

当realWidgets中的数据全部来自网络时候,也有白屏的可能,如列表,如需确实有界面框架显示,使用如下方式:

  • 初次冷启动:

    使用与产品约定的默认数据来加载

  • 其他:

    保存缓存数据,下次使用缓存数据来加载

3、工期预算

框架接入 2d

二、请求结束后的”白屏”(必须)

1、场景

②处理请求后的场景:文字提示+页面展示(无数据+无网络)

2、处理方案及实现方式

2、网络异常:

为页面提供网络异常页并伴有刷新恢复重试

3、工期预算

框架接入 2d

三、网络异常(提示+恢复)

2.1、文字提示

根据情况,弹出易于用户理解的错误提示

  • 接口问题:服务器开小差了,请稍后重试

2.2、界面提示(实现网络恢复后能再显示正确界面)

1
2
3
4
5
6
7
8
9
10
11
// bad
return successWidget();

// good
if (ApiResult.type == error) {
return errorWidget();
} else if (ApiResult.type == nodata) {
return nodataWidget();
} else {
return successWidget();
}

四、程序异常(上报+提示)

接入bugly服务+自定义异常页

接入第三方bugly华为AGC的崩溃服务

默认的程序异常页 VS 处理后的程序异常页

默认的程序异常页 处理后的程序异常页

程序异常上报后的后台:

程序异常上报后的后台

工期预算

异常上报 1d

五、测试问题反馈与排查优化

1、新增查看版本页面

新增查看版本页面,方便对应反馈的问题出现的版本(防止是旧包)。

2、新增抓包设置(代理)

新增代理设置,方便接口人员抓包排查

六、视觉体验优化

定义加载动画loading

愿望灯动画效果优化(底部下沉)

1、Loading自定义

UI提供 images 或 json,自定义loading动画

2、愿望灯

采用上层愿望灯不变,底部进行下沉动画来实现愿望灯上飘的效果

3、Toast自定义

图层

七、开发规范优化

1、用户操作/页面形态展示优化

1、入口的点击

内部界面

  • 开发中/已实现:

    点击后直接进入,并展示

  • 待开发:

    点击后,弹出”将进入XXX功能,其待开发中”

  • 产品未规划:

    不显示该入口

2、h5白屏

重定向

域名切换:tke

八、接口请求优化:接口拆分

通过页面分析,与产品和后台确认哪些页面需要进行接口拆分。

目前可能需要处理的有:

  • 商品详情页

框架实现拆分项

1、是否显示加载动画isLoading

1.1、loading动画(images 或 json)

使用 image 或 json 实现loading动画

1.2、含loading显示控制的页面组件

1
static void showLoading(bool show);

2、页面状态PageType

2.1、加载结束不同页面状态页面组件

编号 页面状态PageType 功能 人日
1 加载失败 errorWidget 0.25
2 加载成功,但无数据 nodataWidget(含”重新加载”操作) 0.5
3 加载成功,且有数据 successWidget 0

2.2、含页面状态控制的视图组件

通过传入不同的状态,显示不同状态下的页面

3、请求

5、含状态管理的页面基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int currentPageType = successWidget; // 默认的状态页面
bool showLoading = false; // 是否显示加载动画

return pageTypeWidget(pageType: currentPageType, showLoading: showLoading);


// 开始请求
showLoading = true;
setState(() {});
apiRequest.then((value) {
_xxxBean = value;
showLoading = false;
if (value == null) {
currentPageType = nodata;
} else {
currentPageType = success;
}

// setState(() {
//
// });
disabledLoading();
}).catchError((onError) {
currentPageType = error;
});


// 请求结束
showLoading = false
if (ApiResult.type == error) {
currentPageType = error;
} else if (ApiResult.type == nodata) {
currentPageType = nodata;
} else {
currentPageType = success;
}
setState(() {});

待安排优化

1、网络库升级

重构及增加错误码友好提示处理 1d

2、基类新增处理

1
2
3
4
5
6
7
Visibility 隐藏/可见,能保存组件的状态;Offstate不能保存组件的状态,组件重新加载
* 控制child是否显示
*
当offstage为true,控件隐藏; 当offstage为false,显示;
当Offstage不可见的时候,如果child有动画等,需要手动停掉,Offstage并不会停掉动画等操作。

const Offstage({ Key key, this.offstage = true, Widget child })
  • 网页加载库替换,新增错误码监听,统一js
  • 基础框架新增背景设置及修复有背景时候界面显示问题,error/nodata新增color设置,用于设置透明
  • 基础框架新增unkonw类型,兼容有无初始显示界面的设置
  • 基础框架优化appbar设置,及提供最简易常用的导航栏
  • Mock api 新增缓存功能
  • Bruno接入及编译修复
  • 版本升级管理
  • 数据缓存处理
  • 精选愿望单原型开发完成,待联调
  • 新增百愿清单原型开发完成,待联调

3、App快速实现置灰样式

App快速实现置灰样式

iOS整个APP实现灰色主题的示例代码

白屏:

①处理请求前的场景:数据预加载+框架预染页(美团/饿了么的灰底效果)

②处理请求后的场景:文字提示+页面展示(无数据+无网络)

2、网络异常:

为页面提供网络异常页并伴有刷新恢复重试

3、程序异常(上报+提示):

接入bugly服务+自定义异常页

4、视觉体验优化

定义加载动画loading

愿望灯动画效果优化(底部下沉)

5、测试优化

新增查看版本页面,方便对应反馈的问题

新增代理设置,方便接口人员抓包排查

6、其他

AA送礼:提测,待测试验证后,如有问题修复

开发者账号申请审核未通过,正在处理。 (因为域名问题被拒,新域名已经准备好了,在跟apple沟通中)

下周:

继续我的模块开发,联调接口。

在完成”我的”模块或等待接口情况下,评估及开发产品规划的其他功能。

目前:

框架升级(白屏、预览页灰底、网络刷新恢复页,异常上报及异常页):今天

愿望灯 ok
许个愿 ok
我的:部分在联调,部分在等接口
AA送礼:提测,待测试验证后,如有问题修复

loading
开发者账号申请审核未通过,正在处理。 (因为域名问题被拒,新域名已经准备好了,在跟apple沟通中)

组件的使用-基础框架BasePage

框架的接入

为了有更好的用户体验,我们对页面框架进行如下优化。

页面框架

1、页面头appbar、页面身successWidget

1、错误页和空白页的页面,需要自定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import 'package:flutter_effect/flutter_effect.dart';

// 修改地方1.1继承: BJHBasePage
//class TSBasePage extends StatefulWidget {
class TSBasePage extends BJHBasePage {

}

// 修改地方1.2继承: BJHBasePageState
//class _TSBasePageState extends State<TSBasePage> {
class _TSBasePageState extends BJHBasePageState<TSBasePage> {

// 修改地方2.0:删除/注释掉原来的 Widget build(BuildContext context){}
//@override
//Widget build(BuildContext context) {
// return Scaffold(
// backgroundColor: Color(0xffF0F0F0),
// appBar: appBarWidget,
// body: bodyWidgets,
// );
//}
// 修改地方2.1:将其中的导航栏 appBarWidget 通过 PreferredSizeWidget appBar() {} 返回
// 当你的导航栏是使用系统的AppBar时候,重写appBar()方法,如
@override
PreferredSizeWidget appBar() {
return AppBar(
title: Text("导航栏标题"),
);
}
// 当你的导航栏不是使用系统的AppBar,而是自己在page上添加的Widget的时候,重写 Widget appBarWidget(BuildContext context)方法,如
@override
Widget appBarWidget(BuildContext context) {
return EasyAppBar(
title: '我是成功页面的标题',
);
return CommonAppBar(
title: AppBarTitleWidget(text: '我是成功页面的标题'),
leading: AppBarBackWidget(
onPressed: () {
Navigator.pop(context);
},
),
);
}

// @override
// Color backgroundColor() {
// return Color(0xFFF0F0F0);
// }

// 修改地方2.2:将其中的 bodyWidgets 通过 Widget buildSuccessWidget(BuildContext context) {} 返回,则当前调用 updateWidgetType(WidgetType.SuccessWithData); 的时候,其会将视图更新为该方法返回的样式
@override
Widget buildSuccessWidget(BuildContext context) {
return bodyWidgets;
}

// 修改地方2.2:自定义【请求成功,但无数据】的界面
@override
Widget buildNodataWidget(BuildContext context) {
return StateNodataWidget(
image: AssetImage('assets/images/nodata.png')
mainTitle: '我是【请求成功,但无数据】的界面',
subTitle: '',
);
}

// 修改地方2.3:自定义【请求失败】的界面
@override
Widget buildErrorWidget(BuildContext context) {
return StateErrorWidget(
//color: Colors.transparent, //可设置背景色,常用语本page有设置背景图片的时候
errorRetry: getData,
);
}


// 请求网络的方法名可任意(不再需要保证为getData,因为错误页和空白页的重新加载需要在本dart文件设置)
void getData() {
...

// 修改地方4:对返回的数据进行判空及更新状态
Api.getGoodsInfoList({}).then((ResultData data) {
if (data.isSuccess) {
...

// 修改地方4.1:对请求到数据,且状态正确的数据进行判空及更新状态
if (bean == null) {
updateWidgetType(WidgetType.SuccessNoData);
} else {
updateWidgetType(WidgetType.SuccessWithData);
}

} else {
// 修改地方4.2:对请求到数据,但状态错误的处理
//updateWidgetType(WidgetType.ErrorBusiness);
}
}).catchError(onError) {
// 修改地方4.3:对请求失败,如服务器崩溃,无网络等的处理
updateWidgetType(WidgetType.ErrorNetwork);
};
}

}

2、初始界面buildInitWidget

其他,如果你还想设置初始页面,目前初始视图默认是空白视图

1
2
3
4
5
6
7
8
9
10
11
12
@override
Widget buildInitWidget(BuildContext context) {
return Container(
color: Colors.green,
height: 100,
child: Text(
'我是初始视图...',
style: TextStyle(color: Colors.blue, fontSize: 24),
textAlign: TextAlign.center,
),
);
}

3、背景色、背景图片

设置背景图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 背景视图(常用来设置背景图片)
@override
Widget backgroundWidget(BuildContext context) {
// 设置背景色
return Container(
color: Color(0xFFF0F0F0),
);

// eg1:设置铺满的背景图片
// return Container(
// alignment: Alignment.topCenter,
// //color: Colors.yellow,
// constraints: const BoxConstraints(
// minWidth: double.infinity,
// minHeight: double.infinity,
// ),
// child: Image.asset(
// "images/wish/bg_icon.png",
// fit: BoxFit.fitWidth,
// ),
// );

// eg2:设置绝对定位的背景图片
// return Positioned(
// top: 0,
// right: 0,
// left: 0,
// height: Adapt.px(678),
// child: Image.asset(
// "images/wish/bg_icon.png",
// fit: BoxFit.fitWidth,
// ),
// );
}

End

Git使用

详细而全的文档:Pro Git(中文版)

Git回滚代码到某个commit

回退命令:

1
2
3
4
5
6
7
8
在git push的时候,有时候我们会想办法撤销git commit的内容 
1、找到之前提交的git commit的id
git log
找到想要撤销的id
2、git reset –hard id
完成撤销,同时将代码恢复到前一commit_id 对应的版本
3、git reset id
完成Commit命令的撤销,但是不对代码修改进行撤销,可以直接通过git commit 重新提交对本地代码的修改
1
2
3
4
5
6
$ git reset HEAD^         			回退到上个版本,代码还在

注意:如果加上使用--hard会导致这里虽然回到了上个版本,但是commit上去,未push出去的就没法找回了
$ git reset --hard HEAD^ 回退到上个版本
$ git reset --hard HEAD~3 回退到前3次提交之前,以此类推,回退到n次提交之前
$ git reset --hard commit_id 退到/进到 指定commit的sha码

当你有多处备份的时候,你可以强制性的执行

git reset –hard HEAD^

但是你刚commit上去,未push出去的就没法找回了。

pod install报错:ArgumentError - Malformed version number string

网上的参考:pod install报错:ArgumentError - Malformed version number string

实际:通过执行sudo gem install cocoapods命令重装cocoapods即解决了。

其他CocoaPods那些错

GitHub 出现 POST git-receive-pack (chunked) 解决方案详解

出现 POST git-receive-pack (chunked) 的原因就是 当使用 HTTPS 提交到 Git 上时使用不检查加密要是东西过多将导致提交停止。

解决方法:

1
2
3
4
5
6
7
方案1>  使用 Git 提交代码
进入到要提交的代码的目录,里面包含 .git 文件夹,输入指令 git config http.postBuffer 524288000

方案2> 使用 SourceTreee 提交代码
如图按照顺序依次点击在最后一步增加
[http]
postBuffer = 524288000

POST git-receive-pack (chunked).png)

再次提交将会成功。

相关参考:GitHub 出现 POST git-receive-pack (chunked) 解决方案详解

设计模式-②单例模式

精选文章

以下目录内容全部摘自:iOS中的单例模式

# 目录 * [一、什么是单例模式](#)
1
...

## 一、单例 > [< 返回目录](#目录)

1、单例:dispatch_once (使用dispatch_once时,不用使用@synchronized)
单例是一种用于实现单例的数学概念,即将类的实例化限制成仅一个对象的设计模式。
或者我的理解是:单例是一种类,该类只能实例化一个对象。

实现单例模式的函数就是void dispatch_once( dispatch_once_t *predicate, dispatch_block_t block);
该函数接收一个dispatch_once用于检查该代码块是否已经被调度的谓词(是一个长整型,实际上作为BOOL使用)。它还接收一个希望在应用的生命周期内仅被调度一次的代码块,对于本例就用于shared实例的实例化。
dispatch_once不仅意味着代码仅会被运行一次,而且还是线程安全的,这就意味着你不需要使用诸如@synchronized之类的来防止使用多个线程或者队列时不同步的问题。
Apple的GCD Documentation证实了这一点:
如果被多个线程调用,该函数会同步等等直至代码块完成。

示例:在整个应用中访问某个类的共享实例

1
2
3
4
5
6
7
8
9
10
11
+ (NetworkManager *)sharedInstance
{
static NetworkManager *sharedManager;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[NetworkManager alloc] init];
});

return sharedManager;
}

就这些,你现在在应用中就有一个共享的实例,该实例只会被创建一次。
下次你任何时候访问共享实例,需要做的仅是:NetworkManager *networkManager = [NetworkManager sharedInstance];

2、线程的同步执行@synchronized
为了防止多个线程同时执行同一个代码块,OC提供了@synchronized()指令。使用@synchronized()指令可以锁住在线程中执行的某一个代码块。存在被保护(即被锁住)的代码块的其他线程,将被阻塞,这也就意味着,他们将在@synchronized()代码块的最后一条语句执行结束后才能继续执行。
@synchronized()指令的唯一参数可以使用任何OC对象,包括self。这个对象就是我们所谓的信号量。

## END [< 返回目录](#目录)

设计模式-⑤多代理模式

前言

1、什么是多代理

引文①、环信SDK

用过环信SDK的同学应该对多代理不陌生了,请看下面代码:

1
2
3
4
5
6
7
8
9
/*
@method
@brief 注册一个监听对象到监听列表中
@discussion 把监听对象添加到监听列表中准备接收相应的事件
@param delegate 需要注册的监听对象
@param queue 通知监听对象时的线程
@result
*/
- (void)addDelegate:(id<EMChatManagerDelegate>)delegate delegateQueue:(dispatch_queue_t)queue;

平时我们写得比较多的代理: @property (nonatomic,weak) id<EMChatManagerDelegate>delegate; 写了上面属性后系统会默认生成set方法: - (void)setDelegate:(id<EMChatManagerDelegate>)delegate; 通过对两个接口的比较就不难看出:单代理只能设置一个,而多代理可以设置多个,准确来说应该是多代理可以添加多个

以上摘自:iOS 实现多代理的方法及实例代码

引文②、XMPP

XMPP以及类似IM框架里通常会有这种需求:打开多个聊天窗口,和多个人聊天。然而框架底层消息转发管理器却只有一个。通常是这两个窗口都要收到消息回调,然后取自己有用的消息。。。
大概就这么个意思,我两年前用了下,具体也解释不清楚,欢迎指点交流,反正就是有一个需求需要多代理回调,这种IM框架通常有这种方法
[xmppRoom addDelegate:self delegateQueue:dispatch_get_main_queue()];

以上摘自:iOS 多代理的实现

引文③、类似IM库,当接受到消息,在几个不同地方做回调,比如改变消息数,显示小红点等

参考文章:iOS实现多重代理及应用场景

2、为什么不用NSNotificationCenter

系统不是已经有通知中心NSNotificationCenter了吗?为什么还要自己实现一个呢?

举个例子,现在我们有一个模块需要抛一个通知出来,通知其它模块用户名改变了,我们来看代码大致是怎么写的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 发通知一方
NSString *const kUserNameChangedNotification = @"UserNameChangedNotification";

NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:UserNameChangedNotification object:nil
userInfo:@{@"oldName":@"zhangsan","newName":"lisi"}];

// 接收通知的一方可以是
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(UserNameChanged1:)
name:kUserNameChangedNotification object:nil];
// 也可以是
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(UserNameChanged2:)
name:kUserNameChangedNotification object:nil];

从例子中可以看到有的缺点:
1.对于接收同一个事件的通知,不同的人可能会用不同的方法名来执行(例子中是UserNameChanged1UserNameChanged1),无法统一。
2.对于多参数支持不方便。

所以本文我们的优化的是统一接收方的执行方法,并为该方法提供明确的参数。

CJProtocolCenter

CJProtocolCenter.m

项目背景举例:

背景描述如下:

vc中所做的某个操作(指一个操作),想要发送给多个人,让他们接收到信息后,自己处理。

我们假设接收者为

1
2
3
4
DelegateReceivedViewModel1 *delegateReceiver1 = [[DelegateReceivedViewModel1 alloc] init];
DelegateReceivedViewModel2 *delegateReceiver2 = [[DelegateReceivedViewModel2 alloc] init];
self.delegateReceiver1 = delegateReceiver1;
self.delegateReceiver2 = delegateReceiver2;

一、正常操作

发送状态变化的信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// OneToManyDelegateNormalViewController1.h
#import "CJUIKitBaseHomeViewController.h"
#import "TSDelegate.h"

@interface OneToManyDelegateNormalViewController1 : CJUIKitBaseHomeViewController {

}
@property (nonatomic, weak) id<TSDelegate> delegate1; // 提供给外部设置
@property (nonatomic, weak) id<TSDelegate> delegate2; // 提供给外部设置

@end


// OneToManyDelegateNormalViewController1.m
loginModule.actionBlock = ^{
[self.delegate1 delegate_didUpdateLoginState:YES];
[self.delegate2 delegate_didUpdateLoginState:YES];
}

②设置代理的地方会是如下:

1
2
3
4
OneToManyDelegateNormalViewController1 *viewController = [[OneToManyDelegateNormalViewController1 alloc] init];
viewController.delegate1 = delegateReceiver1;
viewController.delegate2 = delegateReceiver2;
[self.navigationController pushViewController:viewController animated:YES];

二、优化delegate

依赖注入

依赖注入:把做的事情交给某人。至于依赖的某人有什么能力,依靠外部决定。

参考文档:

注入可以通过对象的初始化(或者可以说构造器)或者通过特性(setter),即构造器注入和setter方法注入。

OneToManyDelegateNormalMediator21

1
2
3
4
5
6
7
8
9
10
11
OneToManyDelegateNormalMediator21 *delegateMediator

// 方法1:构造器注入
- (instancetype)initWithDelegate1:(Delegate1 *)delegate1
delegate2:(Delegate2 *)delegate2;
@property (nonatomic, weak, readonly) Delegate1 *delegate1;
@property (nonatomic, weak, readonly) Delegate2 *delegate2;

// 方法2:Setter 注入
@property (nonatomic, weak) id<Delegate1> delegate1;
@property (nonatomic, weak) id<Delegate2> delegate2;

参考的其他文档:

EXTConcreteProtocol 虽然没有直接叫做依赖注入,而是叫做混合协议,但是充分使用了 OC 动态语言的特性,不侵入项目,高度自动化,框架十分轻量,使用非常简单。

1、优化思路一:从vc中抽离delegate到Mediator

为了优化代码,我们从vc中抽离delegate,则此时

①设置代理的地方会是如下:

1
2
3
4
5
6
7
OneToManyDelegateNormalMediator21 *delegateMediator = [[OneToManyDelegateNormalMediator21 alloc] init];
delegateMediator.delegate1 = self.delegateReceiver1;
delegateMediator.delegate2 = self.delegateReceiver2;

OneToManyDelegateNormalViewController21 *viewController = [[OneToManyDelegateNormalViewController21 alloc] init];
viewController.delegateMediator = delegateMediator;
[self.navigationController pushViewController:viewController animated:YES];

②发送状态变化的信息如下:

1
2
3
4
5
6
// OneToManyDelegateNormalViewController21.m
loginModule.actionBlock = ^{
//[self.delegate1 delegate_didUpdateLoginState:YES];
//[self.delegate2 delegate_didUpdateLoginState:YES];
[self.delegateMediator delegate_didUpdateLoginState:YES];
};

③此时delegateMediator中的delegate_didUpdateLoginState:方法如下:

1
2
3
4
5
6
7
8
9
10
#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
if (self.delegate1 && [self.delegate1 respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegate1 delegate_didUpdateLoginState:YES];
}

if (self.delegate2 && [self.delegate2 respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegate2 delegate_didUpdateLoginState:YES];
}
}

2、优化思路二:在Mediator中管理delegate数组

为了减少设置代理的变量,我们使用delegate数组,则此时

①设置代理的地方会是如下:

1
2
3
4
5
6
7
OneToManyDelegateArrayMediator31 *delegateMediator = [[OneToManyDelegateArrayMediator31 alloc] init];
[delegateMediator addDelegate:self.delegateReceiver1];
[delegateMediator addDelegate:self.delegateReceiver2];

OneToManyDelegateArrayViewController31 *viewController = [[OneToManyDelegateArrayViewController31 alloc] init];
viewController.delegateMediator = delegateMediator;
[self.navigationController pushViewController:viewController animated:YES];

②发送状态变化的信息如下:不变

③此时delegateMediator中的delegate_didUpdateLoginState:方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  OneToManyDelegateArrayMediator31.m
_delegates = [NSPointerArray weakObjectsPointerArray];

- (void)addDelegate:(id)delegate {
[_delegates addPointer:(__bridge void*)delegate];
}

#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if (delegate && [delegate respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[delegate delegate_didUpdateLoginState:YES];
}
}
}

三、优化状态变化的发送方法

1、未优化时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vc.m
loginModule.actionBlock = ^{
if (self.delegateMediator && [self.delegateMediator respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegateMediator delegate_didUpdateLoginState:YES];
}
};

// Mediator.m
#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if (delegate && [delegate respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[delegate delegate_didUpdateLoginState:YES];
}
}
}

为了能够在Mediator中减少我们要实现每个代理的每个方法,我们进行如下优化:

2、增加个发送的封装方法,将协议要执行的方法改为参数等传进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// vc.m
loginModule.actionBlock = ^{
[self.delegateMediator broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};

// Mediator.m
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];
if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

3、如果你不用上述的封装方法,你还可以利用消息转发

对Mediator对象调用delegate_didUpdateLoginState方法时,因为含delegates的Mediator对象并没有实现协议中的方法,如delegate_didUpdateLoginState,所以,我们只能要么补充实现,要么不实现的话,就利用消息转发,将协议中的方法转发到自己delegate链中的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// vc.m
loginModule.actionBlock = ^{
if (self.delegateMediator && [self.delegateMediator respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegateMediator delegate_didUpdateLoginState:YES];
}
};

// Mediator.m
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (signature) {
return signature;
}
[_delegates addPointer:nil]; //不加上这句的话,直接调用compact,并不能清除 _delegates 数组中的 nil 对象。
[_delegates compact]; //注意 [_delegates compact],这个方法可以帮助你去掉数组里面的野指针,避免你在快速遍历的时候拿到一个指向不存在对象的地址

for (id delegate in _delegates) {
if (!delegate) {
continue;
}
signature = [delegate methodSignatureForSelector:aSelector];
if (signature) {
break;
}
}
return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
BOOL responded = NO;
for (id delegate in _delegates) {
if (delegate && [delegate respondsToSelector:selector]) {
[anInvocation invokeWithTarget:delegate];
responded = YES;
}
}
if (!responded) {
[self doesNotRecognizeSelector:selector];
}
}

此方式的问题:由于[self.delegateMediator delegate_didUpdateLoginState:YES];,所以delegateMediator需要在@interface支持,同样的因为这个支持,我们需要在.m文件中去实现它里面的方法,否则会有警告。除非说协议里的那些方法全部都是设置为了@optional

结论:基于此方式的问题,所以为了减少对其他的约束,我们这里最后采用2的优化方式。

四、发现潜在问题

1、如果一个类同时依赖两个protocal,且刚好两个protocal中有同名方法怎么办?

举例:在vc.m中想要告诉大家去实现协议1中的showMessage方法,但是如果不说明是协议1中的方法,可能有个协议2也有同样的方法,导致执行的方法错了。

分析:这种情况是可能出现的,但是这种情况出现的时候,不会被引用到同一个监听者,不然分不清delegateReceiver中现在执行的是哪一个。

如原本名字不一样,我们区分得开。

1
2
3
4
5
6
7
8
9
10
// viewModel.m
#pragma mark - TSUserDelegate
- (void)userDelegate_didUpdateLoginState:(BOOL)loginState {
NSLog(@"vm收到userDelegate:登录状态发生变化,您已%@", loginState ? @"登录" : @"登出");
}

#pragma mark - TSMessageDelegate
- (void)messageDelegate_didUpdateMessageState:(BOOL)messageState {
NSLog(@"vm收到messageDelegate:信息状态发生变化");
}

我们直接在上述原本实现的基础上看问题,修改后为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// vc.m
loginModule.actionBlock = ^{
[self.delegateMediator broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};

// Mediator.m
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

同理:添加delegate的时候,也

1
2
3
if (![delegateReceiver conformsToProtocol:protocol]) {  // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
// continue or return or ...;
}

2、如何保证delegates操作的数据安全

场景:B页面发送一个信号,B页面的viewModel会去执行,当然还有其他地方也有可能执行。

那么当B发送信号出去,还在遍历执行的时候,突然某个从B页面返回A页面,B页面释放,B的viewModel也跟着释放,即这个信号的其中一个接收者B的viewModel被释放了,那么当执行的时候相当于给nil发送信息,不会崩溃。

1
2
3
4
5
6
7
8
9
10
11
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}

需要注意数据安全的地方,应该是避免重复添加和重复删除

所以,我们使用@synchronized来保护。

1
2
3
@synchronized(xxx) {

}

五、delegateMediator升级为中心单例

发送时候的核心代码,大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// vc.m
loginModule.actionBlock = ^{
[[TSNotificationCenter defaultCenter] broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};


// 该发送方法在`TSNotificationCenter.m`中的代码如下
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

接收时候的核心代码,大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// receiver.m
[[TSNotificationCenter defaultCenter] addDelegate:self.diffDelegateReceiver
forProtocol:@protocol(TSUserDelegate)];
[[TSNotificationCenter defaultCenter] addDelegate:self.diffDelegateReceiver
forProtocol:@protocol(TSMessageDelegate)];

// 该添加方法在`TSNotificationCenter.m`中的代码如下
- (void)addDelegate:(id __nonnull)listener forProtocol:(Protocol * _Nonnull )protocol {
NSAssert(listener, @"listener is nil");
NSAssert(protocol, @"Protocol is nil");
#ifdef DEBUG
NSAssert([listener conformsToProtocol:protocol], @"This listener does not conform to the protocol");
#endif

@synchronized(protocol) {
[self.hashTable addObject:listener];
}
}

说说上面我们应该用什么来管理遵守协议的类(NSHashTable)?

答:因为delegate本身为了避免循环应用,所以其是弱引用对象。而要保存弱引用对象,我们不能够用NSArray,因为NSArray是强引用,而应该用NSHashTable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方法1:NSPointerArray
_delegates = [NSPointerArray weakObjectsPointerArray];
- (void)addDelegate:(id)delegate {
[_delegates addPointer:(__bridge void*)delegate];
}

// 方法2:NSHashTable
_hashTable = [NSHashTable weakObjectsHashTable];
- (void)addDelegate:(id)delegate {
[_hashTable addObject:delegate];
}


@property (nonatomic, strong, readonly) NSMutableSet *listenerLists; // 保存多个NSHashTable,即其是数组的数组,里面的元素是 NSHashTable *listeners

这边用了NSHashTable来存储遵守协议的类,NSHashTable和NSSet类似,但又有所不同,总的来说有这几个特点:

NSHashTable中的元素可以通过Hashable协议来判断是否相等.

NSHashTable中的元素如果是弱引用,对象销毁后会被移除,可以避免循环引用.

附一:block 或 delegate 等弱引用对象用什么保存

如果你需要保存一堆block,并且希望它们能够响应特定的事件或操作,你通常会使用数组来存储这些block。然而,由于block在Objective-C中是对象,并且默认情况下它们会被强引用,你需要考虑循环引用的问题,特别是当block内部捕获了它们的捕获环境(例如,捕获了它们的创建者对象的强引用)时。

以下是几种处理block存储的方法:

  1. 使用__weakweak修饰符:如果你的block捕获了它们的创建者对象的引用,你可以在block内部使用__weak(在Objective-C中)或weak(在Swift中)来避免强引用循环。
  2. **使用NSHashTableNSPointerArray**:如果你需要存储block的弱引用,可以使用NSHashTableNSPointerArray,并设置它们的pointerFunctions属性以使用NSPointerFunctionsWeakMemory,这样它们就会存储block的弱引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@property (nonatomic, strong) NSPointerArray *blocks;

- (void)addBlock:(void (^)(void))block {
if (!self.blocks) {
self.blocks = [NSPointerArray weakObjectsPointerArray];
}
[self.blocks addPointer:(__bridge void *)(block ? block : [NSNull null])];
}

- (void)executeBlocks {
for (NSUInteger i = 0; i < self.blocks.count; i++) {
void *pointer = [self.blocks pointerAtIndex:i];
if (pointer != [NSNull null]) {
void (^block)(void) = (__bridge void (^)(void))pointer;
block();
}
}
}

附:不常见集合NSHashTable和NSMapTable

不常见集合NSHashTable和NSMapTable

5框架设计模式-⑦组件化.md

END

其他参考文章:

iOS 实现多代理的方法及实例代码