注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

移动架构 (一) 架构第一步,学会画各种 UML 图

注意: 文章中 UML 图开始用是 Windows PowerDesigner 工具,后来换电脑了用的 StarUML。 UML 定义 UML 是统一建模语言, 是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开...
继续阅读 »

注意: 文章中 UML 图开始用是 Windows PowerDesigner 工具,后来换电脑了用的 StarUML。


UML


定义


UML 是统一建模语言, 是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法。


作用



  1. 帮组开发团队以一种可视化的方式理解系统的功能需求。

  2. 有利于开发团队队员之间在各个开发环节间确立沟通的标准,便于系统文档的制定和项目的管理。因为 UML 的简单、直观和标准性,在一个团队中用 UML 来交流比用文字说明的文档要好的多。

  3. UML 为非专业编程人士理解软件的功能和构造,提供了一种直白、简单、通俗的方法。

  4. 使用 UML 可以方便的理解各种框架的设计方式。


面向对象模型


用例图 (User Case Diagram)


概述



  • 用例图主要模拟系统中的动态行为,并且描述了用户、需求、以及系统功能单元之间的关系。

  • 用例图由参与者 (用户) ,用例 (功能) 和它们之间的关系组成。


目的



  1. 用来收集系统的要求。

  2. 用于获取系统的外观图。

  3. 识别外部和内部影响因素。

  4. 显示要求之间的相互作用是参与者。


构成元素















































组成元素 说明 符号表示
参与者 (Actor) 表示与你自己的程序或者系统进行正在交互的动作。用一个小人表示
用例 (User Case) 表示在一个系统或者程序中某个功能的描述。用一个椭圆代表
关联关系 (Association) 表示参与者与用例之间的关系。用一个箭头表示
包含关系 (Include) 表示一个大的功能分解成多个小模块的动作。用一个带包含文字的虚线箭头表示
扩展关系 (Extend) 表示用例功能的延伸,相当于是为用例提供附加功能。用一个带扩展文字的虚线箭头表示
依赖 (dependency) 表示一个用例依赖于另一个用例(相当于程序里面的一个类引用另一个类的关系)。用一个带依赖文字的虚线箭头表示
泛化 (Generalization) 相当于程序里面的继承关系。用一个箭头表示

用例图例子


需求: 以一个登录的例子来画一个用例图



  1. 包含 登录/注册/

  2. 登录/注册 支持手机号码、第三方 QQ/weichat/GitHub 登录注册


效果图:



提供的登录用例基本上已经包含了刚刚所学的组成元素部分。


结构图


类图 (Class Diagram)


概念

类图 (Class Diagram) 是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其它类的关系等。


类图不显示暂时性的信息,类图是面向对象建模的主要组成部分。它即用于应用程序的系统分类的一般概念建模,也用于详细建模,将模型转换成编程代码。


构成元素









































构成元素 说明 表示符号
泛化 (Generalization) 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征和行为。用一个带三角箭头的实线,箭头指向父类表示
实现 (Realization) 是一种类与接口的关系, 表示类是接口所有特征和行为的实现。用一个带三角箭头的虚线,箭头指向接口表示.
关联 (Association) 1. 是一种拥有的关系, 它使一个类知道另一个类的属性和方法. 2.关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头。用一个带普通箭头的实心线,指向被拥有者
依赖 (Dependency) 是一种使用的关系, 即一个类的实现需要另一个类的协助, 所以要尽量不使用双向的互相依赖.用一个带箭头的虚线,指向被使用者
聚合 (Aggregation) 聚合是一种特殊的关联 (Association) 形式,表示两个对象之间的所属 (has-a) 关系。所有者对象称为聚合对象,它的类称为聚合类;从属对象称为被聚合对象,它的类称为被聚合类。例如,一个公司有很多员工就是公司类 Company 和员工类Employee 之间的一种聚合关系。被聚合对象和聚合对象有着各自的生命周期,即如果公司倒闭并不影响员工的存在。用一个带空心菱形的实心线,菱形指向整体
VXn1BV.png
组合 (Composition) 是整体与部分的关系, 但部分不能离开整体而单独存在. 如公司和部门是整体和部分的关系, 没有公司就不存在部门。用一个带实心菱形的实线,菱形指向整体表示。

类图例子

需求: 基于 google 官方 MVP 架构 绘制一个基本的 MVP 类图架构


ZIm7Fg.jpg


组合结构图 (Composite Structure Diagram)


概念

用来显示组合结构或部分系统的内部构造,包括类、接口、包、组件、端口和连接器等元素。比类图更抽象的表示,一般来说先画组合结构图,再画类图。


构成元素





























































构成元素 说明 表示符号
类 (Class) 表示对某件事物的描述
class.jpg
接口 (Interface) 表示用于对 Class 的说明
25a5bf4ec2a49f23bbc971fb55242484.jpg
端口 (port) 表示部件和外部环境的交互点
958803fcc1ecf9dd710a7fa4d3d7f284.jpg
部件 (part) 表示被描述事物所拥有的内部成分
388d69ae3fb52b2777f1efa2051e2d03.jpg
泛化 (Generalication) 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征和行为。用一个带三角箭头的实线,箭头指向父类表示
实现 (Realization) 是一种类与接口的关系, 表示类是接口所有特征和行为的实现。用一个带三角箭头的虚线,箭头指向接口表示.
关联 (Association) 1. 是一种拥有的关系, 它使一个类知道另一个类的属性和方法. 2.关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头。用一个带普通箭头的实心线,指向被拥有者
依赖 (Dependency) 是一种使用的关系, 即一个类的实现需要另一个类的协助, 所以要尽量不使用双向的互相依赖.用一个带箭头的虚线,指向被使用者
聚合 (Aggregation) 聚合是一种特殊的关联 (Association) 形式,表示两个对象之间的所属 (has-a) 关系。所有者对象称为聚合对象,它的类称为聚合类;从属对象称为被聚合对象,它的类称为被聚合类。例如,一个公司有很多员工就是公司类 Company 和员工类Employee 之间的一种聚合关系。被聚合对象和聚合对象有着各自的生命周期,即如果公司倒闭并不影响员工的存在。用一个带空心菱形的实心线,菱形指向整体
VXn1BV.png
组合 (Composition) 是整体与部分的关系, 但部分不能离开整体而单独存在. 如公司和部门是整体和部分的关系, 没有公司就不存在部门。用一个带实心菱形的实线,菱形指向整体表示。

注意事项

侧重类的整体特性,就用类图;侧重类的内部结构,就使用组合结构图。


组合结构图例子

Composite-Structures1-.md.png


对象图 (Object Diagram)


概念

显示某时刻对象和对象之间的关系


构成元素


























构成元素 说明 表示符号
对象 (Object) 代表某个事物
class.jpg
实例链接 (Instance Link) 链是类之间关系的实例
-3c9ce1846469aa82.jpg
依赖 (Dependency) 想当于 A 对象使用 B 对象里面的属性

对象图例子


包图 (Package Diagram)


概念

包与包的之间的关系


构成元素


























构成元素 说明 表示符号
包 (Package) 当对一个比较复杂的软件系统进行建模时,会有大量的类、接口、组件、节点和图需要处理;如果放在同一个地方的话,信息量非常的大,显得很乱,不方便查询,所以就对这些信息进行分组,将语义或者功能相同的放在同一个包中,这样就便于理解和处理整个模型
PackageDiagram1.png
泛化 (Generalization) 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征和行为。用一个带三角箭头的实线,箭头指向父类表示
依赖 (Dependency) 是一种使用的关系, 即一个类的实现需要另一个类的协助, 所以要尽量不使用双向的互相依赖.用一个带箭头的虚线,指向被使用者

包图例子

PackageDiagram2.md.png


动态图


时序图 (Sequence Diagram)


概念

时序图(Sequence Diagram) , 又名序列图、循序图、顺序图,是一种UML交互图。


它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。


它可以表示用例的行为顺序,当执行一个用例行为时,其中的每条消息对应一个类操作或状态机中引起转换的触发事件。


构成元素































构成元素 说明 表示符号
参与者 (Actor) 表示与你自己的程序或者系统进行正在交互的动作。用一个小人表示
对象 (Object) 代表某个事物
class.jpg
控制焦点 (Activation) 控制焦点是顺序图中表示时间段的符号,在这个时间段内对象将执行相应的操作。用小矩形表示
1563183352712.jpg
消息 (Message) 消息一般分为同步消息(Synchronous Message),异步消息(Asynchronous Message)和返回消息(Return Message)
1563183471285.jpg

时序图例子

需求:这里为了简单就用一个登陆的时序图为参考


SequenceDiagram_.png


通讯图 (Communication Diagram)


概念

顺序图强调先后顺序,通信图则是强调相互之间的关系。顺序图和通信图基本同构,但是很少使用通信图,因为顺序图更简洁,更直观。


构成元素































构成元素 说明 表示符号
参与者 (Actor) 表示与你自己的程序或者系统进行正在交互的动作。用一个小人表示
对象 (Object) 代表某个事物
class.jpg
实例链接 (Instance Link) 链是类之间关系的实例
-3c9ce1846469aa82.jpg
消息 (Message) 消息一般分为同步消息(Synchronous Message),异步消息(Asynchronous Message)和返回消息(Return Message)
1563183471285.jpg

通讯图例子


活动图 (Activity Diagram)


概念

活动图是 UML 用于对系统的动态行为建模的另一种常用工具,它描述活动的顺序,展现从一个活动到另一个活动的控制流。活动图在本质上是一种流程图。活动图着重表现从一个活动到另一个活动的控制流,是内部处理驱动的流程。


构成元素









































构成元素 说明 表示符号
活动 (Activity) 活动状态用于表达状态机中的非原子的运行
1563204107193.jpg
对象节点 (Object Node) 某件事物的具体代表
1563204175149.jpg
判断 (Decision) 对某个事件进行判断
1563204318945.jpg
同步 (synchronization) 指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程;
1563204447903.jpg
开始 (final) 表示成实心黑色圆点
1563204607110.jpg
结束 (Flow Final) 分为活动终止节点(activity final nodes)和流程终止节点(flow final nodes)。而流程终止节点表示是子流程的结束。
1563204701765.jpg

活动图例子

需求: 点开直播 -> 观看直播的动作


cdcf1ece24ce8cf2829939376955d829.jpg


状态图 (Statechart Diagram)


概念

描述了某个对象的状态和感兴趣的事件以及对象响应该事件的行为。转换 (transition) 用标记有事件的箭头表示。状态(state)用圆角矩形表示。通常的做法会包含一个初始状态,当实例创建时,自动从初始状态转换到另外一个状态。


状态图显示了对象的生命周期:即对象经历的事件、对象的转换和对象在这些事件之间的状态。当然,状态图不必要描述所有的事件。


构成元素




































构成元素 说明 表示符号
开始 (final) 表示成实心黑色圆点
1563204607110.jpg
结束 (Flow Final) 分为活动终止节点(activity final nodes)和流程终止节点(flow final nodes)。而流程终止节点表示是子流程的结束。
1563204701765.jpg
状态 (state) 某一时刻变化的记录
1563205220353.jpg
过渡 (Transition) 相当于 A 点走向 B 点的过渡
1563205289524.jpg
同步 (synchronization) 共同执行一个指令
1563204447903.jpg

状态图例子

需求: 这里直接借鉴 Activity 官方状态图


Activity-.jpg


交错纵横图 (Interaction overview Diagram)


概念

用来表示多张图之间的关联


构成元素




































构成元素 说明 表示符号
开始 (final) 表示成实心黑色圆点
1563204607110.jpg
结束 (Flow Final) 分为活动终止节点(activity final nodes)和流程终止节点(flow final nodes)。而流程终止节点表示是子流程的结束。
1563204701765.jpg
同步 (synchronization) 共同执行一个指令
1563204447903.jpg
判断 (Decision) 对某个事件进行判断
1563204318945.jpg
流 (Flow) 事件流的走向 可以参考,开始跟结束

交错纵横图例子

82ea391d3a841e9097d737c315be3879.png


交互图


组件图 (Component Diagram)


概念

组件图(component diagram)是用来反映代码的物理结构。从组件图中,您可以了解各软件组件(如源代码文件或动态链接库)之间的编译器和运行时依赖关系。使用组件图可以将系统划分为内聚组件并显示代码自身的结构


构成元素





















构成元素 说明 表示符号
组件 (Component) 组件用一个左侧带有突出两个小矩形的矩形来表示
35397ded31ae2f0576de21395a532b6c.jpg
接口 (Interface) 接口由一组操作组成,它指定了一个契约,这个契约必须由实现和使用这个接口的构件的所遵循
1aa3560034ea28d1a9f621bb59d3cc5f.jpg

组件图例子

810389027b2bc0e8a2bd6432147372a8.png


部署图 (Deployment Diagram)


概念

部署图可以用于描述规范级别的架构,也可以描述实例级别的架构。这与类图和对象图有点类似,做系统集成很方便。


构成元素


























构成元素 说明 表示符号
节点 (node) 结点是存在与运行时的代表计算机资源的物理元素,可以是硬件也可以是运行其上的软件系统
node.jpg
节点实例 (Node Instance) 与结点的区别在于名称有下划线
466029a462fe609fa554ddfe910c6050.jpg
物件(Artifact) 物件是软件开发过程中的产物,包括过程模型(比如用例图、设计图等等)、源代码、可执行程序、设计文档、测试报告、需求原型、用户手册等等。
5adb2c458e0218a5094b76d8b0564101.jpg

部署图例子

617cd1f8a76331806f29f144ee9b5912.png


经典例子


微信支付时序图



总结


只要掌握常用的几种图 (用例图、类图、时序图、活动图) ,就已经迈向架构第一步了,加油!


作者:DevYK
链接:https://juejin.cn/post/6844903891067207693
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS- 多页面嵌套(JXPagerView、JXCategoryView)

目录 1. 示例 2. 详细说明Podfile中导入 pod 'JXPagingView/Pager' pod 'JXCategoryView'1. 示例VC// 头部View高#define JXTableHeaderViewHeight (kIs_...
继续阅读 »
目录
1. 示例
2. 详细说明
Podfile中导入
pod 'JXPagingView/Pager'
pod 'JXCategoryView'
1. 示例


VC

// 头部View高
#define JXTableHeaderViewHeight (kIs_iPhoneX?200+44:200)
// 菜单项View高
#define JXheightForHeaderInSection 40


#import <JXPagingView/JXPagerView.h>
#import <JXCategoryView/JXCategoryView.h>
<JXPagerViewDelegate, JXCategoryViewDelegate>

/**
顶部View(自定义View)
*/
@property (nonatomic,strong) ZYTeamplayerHeadView *teamplayerHeadV;
/**
菜单项View
*/
@property (nonatomic,strong) JXCategoryTitleView *categoryView;
/**
内容View
*/
@property (nonatomic, strong) JXPagerView *pagingView;
/**
内容View,建议这里使用控制器
*/
@property (nonatomic, strong) NSArray <ZYTeamplayerContentView *> *listViewArray;
/**
菜单项标题数组
*/
@property (nonatomic,copy) NSArray *itemArr;

-(void)viewDidLoad{
[super viewDidLoad];
[self.view addSubview:self.pagingView];
}

#pragma mark - JXPagingViewDelegate
/**
自定义头部视图
*/
- (UIView *)tableHeaderViewInPagerView:(JXPagerView *)pagerView {
return self.teamplayerHeadV;
}
/**
自定义头部视图高

@param pagerView pagerView
@return 头部视图高
*/
- (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView {
return JXTableHeaderViewHeight;
}
/**
菜单项View

@param pagerView pagerView
@return 菜单项View
*/
- (UIView *)viewForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
return self.categoryView;
}
/**
菜单项View高

@param pagerView pagerView
@return 菜单项View高
*/
- (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
return JXheightForHeaderInSection;
}
/**
内容子视图数组

@param pagerView pagerView
@return 内容子视图数组
*/
- (NSArray<UIView<JXPagerViewListViewDelegate> *> *)listViewsInPagerView:(JXPagerView *)pagerView {
return self.listViewArray;
}
/**
上下滚动后调用
*/
- (void)mainTableViewDidScroll:(UIScrollView *)scrollView {
//计算偏移量
CGFloat P = scrollView.contentOffset.y/(JXTableHeaderViewHeight-kNavBarAndStatusBarHeight);
}

#pragma mark - JXCategoryViewDelegate
/**
选中菜单项后调用

@param categoryView 菜单项View
@param index 下表
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didSelectedItemAtIndex:(NSInteger)index {
self.navigationController.interactivePopGestureRecognizer.enabled = (index == 0);
}
/**
滑动并切换内容视图后调用

@param categoryView 菜单项View
@param index 下表
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didScrollSelectedItemAtIndex:(NSInteger)index{
}


#pragma mark 懒加载
/**
总视图
*/
-(JXPagerView *)pagingView{
if(!_pagingView){
//
_pagingView = [[JXPagerView alloc] initWithDelegate:self];
_pagingView.frame = self.view.bounds;
}
return _pagingView;
}
/**
自定义头部视图
*/
-(ZYTeamplayerHeadView *)teamplayerHeadV{
if(!_teamplayerHeadV){
_teamplayerHeadV=[ZYTeamplayerHeadView new];
[_teamplayerHeadV setFrame:CGRectMake(0, 0, kScreenWidth, JXTableHeaderViewHeight)];
}
return _teamplayerHeadV;
}
/**
菜单项视图View
*/
-(JXCategoryTitleView *)categoryView{
if(!_categoryView){
//
_categoryView = [[JXCategoryTitleView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, JXheightForHeaderInSection)];
// dele
_categoryView.delegate = self;
// 设置菜单项标题数组
_categoryView.titles = self.itemArr;
// 背景色
_categoryView.backgroundColor = [UIColor whiteColor];
// 标题色、标题选中色、标题字体、标题选中字体
_categoryView.titleColor = kTitleColor;
_categoryView.titleSelectedColor = kTintClolor;
_categoryView.titleFont=kFont(16);
_categoryView.titleSelectedFont=kFontBold(16);
// 标题色是否渐变过渡
_categoryView.titleColorGradientEnabled = YES;

// 下划线
JXCategoryIndicatorLineView *lineView = [[JXCategoryIndicatorLineView alloc] init];
// 下划线颜色
lineView.indicatorLineViewColor = kTintClolor;
// 下划线宽度
lineView.indicatorLineWidth = 35;
_categoryView.indicators = @[lineView];

// 联动(categoryView和pagingView)
_categoryView.contentScrollView = self.pagingView.listContainerView.collectionView;
// 返回上一页侧滑手势(仅在index==0时有效)
self.navigationController.interactivePopGestureRecognizer.enabled = (_categoryView.selectedIndex == 0);
}

return _categoryView;
}
/**
内容视图数组
*/
-(NSArray<ZYTeamplayerContentView *> *)listViewArray{
if(!_listViewArray){
// 内容视图(通过PageType属性区分页面)
CGRect rect=CGRectMake(0, 0, kScreenWidth, kScreenHeight-kNavBarAndStatusBarHeight-JXTableHeaderViewHeight-JXheightForHeaderInSection);
ZYTeamplayerContentView *playerView = [[ZYTeamplayerContentView alloc] initWithFrame:rect];
[playerView setPageType:ZYTeamplayerContentViewTypePlayer];
ZYTeamplayerContentView *infoView = [[ZYTeamplayerContentView alloc] initWithFrame:rect];
[infoView setPageType:ZYTeamplayerContentViewTypeTeam];
_listViewArray = @[playerView, infoView];
}
return _listViewArray;
}
/**
菜单项标题数组
*/
-(NSArray *)itemArr{
if(!_itemArr){
_itemArr=@[@"球员",@"信息"];
}
return _itemArr;
}
添加下拉刷新

__weak typeof(self)weakSelf = self;
self.pagingView.mainTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 修改
// [self.categoryView reloadData];
// [self.pagingView reloadData];
[weakSelf.pagingView.mainTableView.mj_header endRefreshing];
});
}];

自定义内容视图View

#import "JXPagerView.h"

typedef enum{
ZYTeamplayerContentViewTypePlayer, // 球员
ZYTeamplayerContentViewTypeTeam, // 信息
}ZYTeamplayerContentViewType;

<JXPagerViewListViewDelegate>
/**
页面类型
*/
@property (nonatomic,assign) ZYTeamplayerContentViewType pageType;

@property (nonatomic, copy) void(^scrollCallback)(UIScrollView *scrollView);
// 必须加(用于联动)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
self.scrollCallback(scrollView);
}


#pragma mark - JXPagingViewListViewDelegate
- (UIView *)listView {
return self;
}
/**
返回一个可滚动的视图
*/
- (UIScrollView *)listScrollView {
return self.contentTableView;
}
/**
用于联动
*/
- (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback {
self.scrollCallback = callback;
}


-(void)layoutSubviews{
[self.contentTableView setFrame:self.bounds];
}
-(UITableView *)contentTableView{
if(!_contentTableView){
_contentTableView=[[UITableView alloc]initWithFrame:CGRectZero style:UITableViewStyleGrouped];
[_contentTableView setDelegate:self];
[_contentTableView setDataSource:self];
[_contentTableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
[_contentTableView setBackgroundColor:[UIColor whiteColor]];
[_contentTableView setContentInset:UIEdgeInsetsMake(0, 0, kNavBarAndStatusBarHeight, 0)]; //
[self addSubview:_contentTableView];
[_contentTableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.bottom.mas_equalTo(0);
}];
}
return _contentTableView;
}
2. 详细说明
菜单项

JXCategoryTitleView 文本菜单项

@interface JXCategoryTitleView : JXCategoryIndicatorView
/**
菜单项标题数组
*/
@property (nonatomic, strong) NSArray <NSString *>*titles;

/**
标题项标题行数 (默认:1)
*/
@property (nonatomic, assign) NSInteger titleNumberOfLines;
/**
标题项标题颜色
默认:[UIColor blackColor]
*/
@property (nonatomic, strong) UIColor *titleColor;
/**
标题项标题字体
默认:[UIFont systemFontOfSize:15]
*/
@property (nonatomic, strong) UIFont *titleFont;
/**
标题项标题选中颜色
默认:[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *titleSelectedColor;
/**
标题项标题选中字体
默认:[UIFont systemFontOfSize:15]
*/
@property (nonatomic, strong) UIFont *titleSelectedFont;

/**
默认:NO,title的颜色是否渐变过渡
*/
@property (nonatomic, assign) BOOL titleColorGradientEnabled;
/**
默认:NO,titleLabel是否遮罩过滤。
*/
@property (nonatomic, assign) BOOL titleLabelMaskEnabled;
//---------------titleLabelZoomEnabled(忽略选中后字体)------------------//

/**
默认为NO。
为YES时titleSelectedFont失效,以titleFont为准。
*/
@property (nonatomic, assign) BOOL titleLabelZoomEnabled;
/**
默认1.2。
titleLabelZoomEnabled为YES才生效。
是对字号的缩放,比如titleFont的pointSize为10,放大之后字号就是10*1.2=12。
*/
@property (nonatomic, assign) CGFloat titleLabelZoomScale; //
/**
手势滚动中,是否需要更新zoom状态。默认为YES
*/
@property (nonatomic, assign) BOOL titleLabelZoomScrollGradientEnabled;
//---------------titleLabelStrokeWidth(忽略选中后字体)--------------------//

/**
是否使用Stroke,用于控制字体的粗细(底层通过NSStrokeWidthAttributeName实现)
默认:NO
*/
@property (nonatomic, assign) BOOL titleLabelStrokeWidthEnabled;
/**
默认:-3。
使用该属性,务必让titleFont和titleSelectedFont设置为一样的!!!
*/
@property (nonatomic, assign) CGFloat titleLabelSelectedStrokeWidth;
//----------------------titleLabel缩放锚点中心位置-----------------------//

/**
titleLabel锚点位置(用于调整titleLabel缩放时的基准位置)

typedef NS_ENUM(NSUInteger, JXCategoryTitleLabelAnchorPointStyle) {
JXCategoryTitleLabelAnchorPointStyleCenter, 默认
JXCategoryTitleLabelAnchorPointStyleTop,
JXCategoryTitleLabelAnchorPointStyleBottom,
};
*/
@property (nonatomic, assign) JXCategoryTitleLabelAnchorPointStyle titleLabelAnchorPointStyle;
/**
titleLabel锚点垂直方向的位置偏移,数值越大越偏离中心,默认为:0
*/
@property (nonatomic, assign) CGFloat titleLabelVerticalOffset;
@end

JXCategoryImageView 图片菜单项

@interface JXCategoryImageView : JXCategoryIndicatorView

/**
未选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*imageNames;
/**
未选中图片源(url)
*/
@property (nonatomic, strong) NSArray <NSURL *>*imageURLs;
/**
选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*selectedImageNames;
/**
选中图片源(url)
*/
@property (nonatomic, strong) NSArray <NSURL *>*selectedImageURLs;
/**
使用imageURL从远端下载图片进行加载,建议使用SDWebImage等第三方库进行下载。
*/
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL);

/**
图片大小
默认CGSizeMake(20, 20)
*/
@property (nonatomic, assign) CGSize imageSize;
/**
图片圆角
*/
@property (nonatomic, assign) CGFloat imageCornerRadius;
/**
是否使用缩放效果
默认为NO
*/
@property (nonatomic, assign) BOOL imageZoomEnabled;
/**
缩放比例
默认1.2,
imageZoomEnabled为YES才生效
*/
@property (nonatomic, assign) CGFloat imageZoomScale;
@end

JXCategoryTitleImageView 文本+图片 菜单项

@interface JXCategoryTitleImageView : JXCategoryTitleView
/**
未选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*imageNames;
/**
选中图片源(本地)
*/
@property (nonatomic, strong) NSArray <NSString *>*selectedImageNames;
/**
未选中图片源(url)
通过loadImageCallback回调加载
*/
@property (nonatomic, strong) NSArray <NSURL *>*imageURLs;
/**
选中图片源(url)
通过loadImageCallback回调加载
*/
@property (nonatomic, strong) NSArray <NSURL *>*selectedImageURLs;
/**
图片源为url时使用
*/
@property (nonatomic, copy) void(^loadImageCallback)(UIImageView *imageView, NSURL *imageURL);

/**
默认@[JXCategoryTitleImageType_LeftImage...]
*/
@property (nonatomic, strong) NSArray <NSNumber *> *imageTypes;
/**
图片大小
默认CGSizeMake(20, 20)
*/
@property (nonatomic, assign) CGSize imageSize;
/**
titleLabel和ImageView的间距,默认5
*/
@property (nonatomic, assign) CGFloat titleImageSpacing;
/**
图片是否缩放。默认为NO
*/
@property (nonatomic, assign) BOOL imageZoomEnabled;
/**
图片缩放的最大scale。默认1.2,
imageZoomEnabled为YES才生效
*/
@property (nonatomic, assign) CGFloat imageZoomScale;
@end

JXCategoryNumberView 文本+数字 菜单项

@interface JXCategoryNumberView : JXCategoryTitleView
/**
需要与titles的count对应
*/
@property (nonatomic, strong) NSArray <NSNumber *> *counts;
/**
block内默认不会格式化数字,直接转成字符串显示。
如果业务需要数字超过999显示999+,可以通过该block实现。
*/
@property (nonatomic, copy) NSString *(^numberStringFormatterBlock)(NSInteger number);

/**
numberLabel的font
默认:[UIFont systemFontOfSize:11]
*/
@property (nonatomic, strong) UIFont *numberLabelFont;
/**
数字的背景色
默认:[UIColor colorWithRed:241/255.0 green:147/255.0 blue:95/255.0 alpha:1]
*/
@property (nonatomic, strong) UIColor *numberBackgroundColor;
/**
数字的title颜色
默认:[UIColor whiteColor]
*/
@property (nonatomic, strong) UIColor *numberTitleColor;
/**
numberLabel的宽度补偿,默认:10
总宽度=文字内容的宽度+补偿的宽度
*/
@property (nonatomic, assign) CGFloat numberLabelWidthIncrement;
/**
numberLabel的高度
默认:14
*/
@property (nonatomic, assign) CGFloat numberLabelHeight;
@end

JXCategoryDotView 文本+小角标 菜单项

@interface JXCategoryDotView : JXCategoryTitleView
/**
相对于titleLabel的位置,
默认:JXCategoryDotRelativePosition_TopRight
*/
@property (nonatomic, assign) JXCategoryDotRelativePosition relativePosition;

/**
@[@(布尔值)]数组,控制红点是否显示
*/
@property (nonatomic, strong) NSArray <NSNumber *> *dotStates;
/**
红点的尺寸。
默认:CGSizeMake(10, 10)
*/
@property (nonatomic, assign) CGSize dotSize;
/**
红点的圆角值。
默认:JXCategoryViewAutomaticDimension(self.dotSize.height/2)
*/
@property (nonatomic, assign) CGFloat dotCornerRadius;
/**
红点的颜色。
默认:[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *dotColor;
@end

JXCategoryIndicatorView 基类

@interface JXCategoryIndicatorView : JXCategoryBaseView
/**
下划线
*/
@property (nonatomic, strong) NSArray <UIView<JXCategoryIndicatorProtocol> *> *indicators;

//----------------------菜单项背景色-----------------------//
/**
是否开启背景色
默认:NO
*/
@property (nonatomic, assign) BOOL cellBackgroundColorGradientEnabled;
/**
未选中背景色
默认:[UIColor clearColor]
前提:cellBackgroundColorGradientEnabled为true
*/
@property (nonatomic, strong) UIColor *cellBackgroundUnselectedColor;
/**
选中背景色
默认:[UIColor grayColor]
前提:cellBackgroundColorGradientEnabled为true
*/
@property (nonatomic, strong) UIColor *cellBackgroundSelectedColor;

//----------------------separatorLine-----------------------//
/**
是否显示分割线。默认为NO
*/
@property (nonatomic, assign) BOOL separatorLineShowEnabled;
/**
分割线颜色。默认为[UIColor lightGrayColor]
前提;separatorLineShowEnabled为true
*/
@property (nonatomic, strong) UIColor *separatorLineColor;
/**
分割线的size
默认为CGSizeMake(1/[UIScreen mainScreen].scale, 20)
前提;separatorLineShowEnabled为true
*/
@property (nonatomic, assign) CGSize separatorLineSize;

/**
当contentScrollView滚动时候,处理跟随手势的过渡效果。
根据cellModel的左右位置、是否选中、ratio进行过滤数据计算。

@param leftCellModel 左边的cellModel
@param rightCellModel 右边的cellModel
@param ratio 从左往右方向计算的百分比
*/
- (void)refreshLeftCellModel:(JXCategoryBaseCellModel *)leftCellModel rightCellModel:(JXCategoryBaseCellModel *)rightCellModel ratio:(CGFloat)ratio NS_REQUIRES_SUPER;
@end

JXCategoryBaseView 基类

@interface JXCategoryBaseView : UIView
/**
菜单项视图
*/
@property (nonatomic, strong, readonly) JXCategoryCollectionView *collectionView;
/**
需要关联的内容视图
*/
@property (nonatomic, strong) UIScrollView *contentScrollView;

@property (nonatomic, strong) NSArray <JXCategoryBaseCellModel *> *dataSource;
/**
dele<JXCategoryViewDelegate>
*/
@property (nonatomic, weak) id<JXCategoryViewDelegate> delegate;
/**
初始化选中index
*/
@property (nonatomic, assign) NSInteger defaultSelectedIndex; //
/**
当前选中index(只读)
*/
@property (nonatomic, assign, readonly) NSInteger selectedIndex;
/**
默认为YES,
只有当delegate未实现`- (void)categoryView:(JXCategoryBaseView *)categoryView didClickedItemContentScrollViewTransitionToIndex:(NSInteger)index`代理方法时才有效
*/
@property (nonatomic, assign) BOOL contentScrollViewClickTransitionAnimationEnabled;

/**
整体左边距
默认JXCategoryViewAutomaticDimension(等于cellSpacing)
*/
@property (nonatomic, assign) CGFloat contentEdgeInsetLeft;
/**
整体右边距
默认JXCategoryViewAutomaticDimension(等于cellSpacing)
*/
@property (nonatomic, assign) CGFloat contentEdgeInsetRight;
/**
菜单项之间的间距
默认20
*/
@property (nonatomic, assign) CGFloat cellSpacing;
/**
当collectionView.contentSize.width小于JXCategoryBaseView的宽度,是否将cellSpacing均分。
默认为YES。
*/
@property (nonatomic, assign) BOOL averageCellSpacingEnabled;
/**
菜单项宽度
默认:JXCategoryViewAutomaticDimension
*/
@property (nonatomic, assign) CGFloat cellWidth;
/**
菜单项宽度补偿(总宽度=宽度+k补偿宽度)
默认:0
*/
@property (nonatomic, assign) CGFloat cellWidthIncrement;


//----------------cellWidthZoomEnabled(菜单项缩放)---------------//
/**
菜单项的宽度是否缩放
默认为NO
*/
@property (nonatomic, assign) BOOL cellWidthZoomEnabled;
/**
默认1.2,
cellWidthZoomEnabled为YES才生效
*/
@property (nonatomic, assign) CGFloat cellWidthZoomScale;
/**
手势滚动过程中,是否需要更新菜单项的宽度。
默认为YES
*/
@property (nonatomic, assign) BOOL cellWidthZoomScrollGradientEnabled;
/**
是否开启选中动画。
默认为NO。
自定义的菜单项选中动画需要自己实现。
*/
@property (nonatomic, assign) BOOL selectedAnimationEnabled;
/**
菜单项选中动画的时间。
默认0.25
*/
@property (nonatomic, assign) NSTimeInterval selectedAnimationDuration;
/**
选中目标index的item

@param index 目标index
*/
- (void)selectItemAtIndex:(NSInteger)index;
/**
初始化的时候无需调用。
重新配置categoryView,需要调用该方法进行刷新。
*/
- (void)reloadData;
/**
刷新指定的index的菜单项
内部会触发`- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index`方法进行cellModel刷新

@param index 指定cell的index
*/
- (void)reloadCellAtIndex:(NSInteger)index;


#pragma mark - Subclass use
- (CGRect)getTargetCellFrame:(NSInteger)targetIndex;
#pragma mark - Subclass Override
- (void)initializeData NS_REQUIRES_SUPER;
- (void)initializeViews NS_REQUIRES_SUPER;

/**
reloadData方法调用,重新生成数据源赋值到self.dataSource
*/
- (void)refreshDataSource;
/**
reloadData方法调用,根据数据源重新刷新状态;
*/
- (void)refreshState NS_REQUIRES_SUPER;
/**
reloadData时,返回每个菜单项的宽度

@param index 目标index
@return cellWidth
*/
- (CGFloat)preferredCellWidthAtIndex:(NSInteger)index;
/**
refreshState时调用,重置cellModel的状态

@param cellModel 待重置的cellModel
@param index cellModel在数组中的index
*/
- (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index;
/**
选中某个item时,刷新将要选中与取消选中的cellModel

@param selectedCellModel 将要选中的cellModel
@param unselectedCellModel 取消选中的cellModel
*/
- (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel NS_REQUIRES_SUPER;
/**
关联的contentScrollView的contentOffset发生了改变时调用

@param contentOffset 偏移量
*/
- (void)contentOffsetOfContentScrollViewDidChanged:(CGPoint)contentOffset NS_REQUIRES_SUPER;
/**
选中某一个item的时候调用,该方法用于子类重载。
如果外部要选中某个index,请使用`- (void)selectItemAtIndex:(NSUInteger)index;`

@param index 选中的index
@param selectedType JXCategoryCellSelectedType
@return 返回值为NO,表示触发内部某些判断(点击了同一个cell),子类无需后续操作。
*/
- (BOOL)selectCellAtIndex:(NSInteger)index selectedType:(JXCategoryCellSelectedType)selectedType NS_REQUIRES_SUPER;


/**
返回自定义菜单项的class

@return cell class
*/
- (Class)preferredCellClass;
@end

JXCategoryViewDelegate 协议

@protocol JXCategoryViewDelegate <NSObject>
@optional
/**
点击选中或者滚动选中都会调用该方法。
适用于只关心选中事件,不关心具体是点击还是滚动选中的。

@param categoryView categoryView description
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didSelectedItemAtIndex:(NSInteger)index;
/**
点击选中的情况才会调用该方法

@param categoryView categoryView description
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didClickSelectedItemAtIndex:(NSInteger)index;
/**
滚动选中的情况才会调用该方法

@param categoryView categoryView description
@param index 选中的index
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView didScrollSelectedItemAtIndex:(NSInteger)index;
/**
正在滚动中的回调

@param categoryView categoryView description
@param leftIndex 正在滚动中,相对位置处于左边的index
@param rightIndex 正在滚动中,相对位置处于右边的index
@param ratio 从左往右计算的百分比
*/
- (void)categoryView:(JXCategoryBaseView *)categoryView scrollingFromLeftIndex:(NSInteger)leftIndex toRightIndex:(NSInteger)rightIndex ratio:(CGFloat)ratio;
@end
下划线

JXCategoryIndicatorLineView 直线

@interface JXCategoryIndicatorLineView : JXCategoryIndicatorComponentView
/**
lineStyle

JXCategoryIndicatorLineStyle_Normal = 0,默认
JXCategoryIndicatorLineStyle_Lengthen = 1,
JXCategoryIndicatorLineStyle_LengthenOffset = 2,
*/
@property (nonatomic, assign) JXCategoryIndicatorLineStyle lineStyle;
/**
line滚动时x的偏移量,默认为10;
lineStyle为JXCategoryIndicatorLineStyle_LengthenOffset有用;
*/
@property (nonatomic, assign) CGFloat lineScrollOffsetX;
/**
lineView的高度。
默认:3
*/
@property (nonatomic, assign) CGFloat indicatorLineViewHeight;
/**
lineView的宽度。
默认JXCategoryViewAutomaticDimension(与cellWidth相等)
*/
@property (nonatomic, assign) CGFloat indicatorLineWidth;
/**
lineView的圆角值。
默认JXCategoryViewAutomaticDimension (等于self.indicatorLineViewHeight/2)
*/
@property (nonatomic, assign) CGFloat indicatorLineViewCornerRadius;
/**
lineView的颜色。
默认为[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *indicatorLineViewColor;
@end

JXCategoryIndicatorTriangleView 三角形

@interface JXCategoryIndicatorTriangleView : JXCategoryIndicatorComponentView
/**
三角形的尺寸。
默认:CGSizeMake(14, 10)
*/
@property (nonatomic, assign) CGSize triangleViewSize;
/**
三角形的颜色值。
默认:[UIColor redColor]
*/
@property (nonatomic, strong) UIColor *triangleViewColor;
@end



收起阅读 »

美团外卖Android Crash治理之路

Crash率是衡量一个App好坏的重要指标之一,如果你忽略了它的存在,它就会愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失。本文讲述美团外卖Android客户端团队在将App的Crash率从千分之三做到万分之二过程中所做的大量实践工作,抛砖引玉...
继续阅读 »

Crash率是衡量一个App好坏的重要指标之一,如果你忽略了它的存在,它就会愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失。本文讲述美团外卖Android客户端团队在将App的Crash率从千分之三做到万分之二过程中所做的大量实践工作,抛砖引玉,希望能够为其他团队提供一些经验和启发。


面临的挑战和成果


面对用户使用频率高,外卖业务增长快,Android碎片化严重这些问题,美团外卖Android App如何持续的降低Crash率,是一项极具挑战的事情。通过团队的全力全策,美团外卖Android App的平均Crash率从千分之三降到了万分之二,最优值万一左右(Crash率统计方式:Crash次数/DAU)。


美团外卖自2013年创建以来,业务就以指数级的速度发展。美团外卖承载的业务,从单一的餐饮业务,发展到餐饮、超市、生鲜、果蔬、药品、鲜花、蛋糕、跑腿等十多个大品类业务。目前美团外卖日完成订单量已突破2000万,成为美团点评最重要的业务之一。美团外卖客户端所承载的业务模块越来越多,产品复杂度越来越高,团队开发人员日益增加,这些都给App降低Crash率带来了巨大的挑战。


Crash的治理实践


对于Crash的治理,我们尽量遵守以下三点原则:



  • 由点到面。一个Crash发生了,我们不能只针对这个Crash的去解决,而要去考虑这一类Crash怎么去解决和预防。只有这样才能使得这一类Crash真正被解决。

  • 异常不能随便吃掉。随意的使用try-catch,只会增加业务的分支和隐蔽真正的问题,要了解Crash的本质原因,根据本质原因去解决。catch的分支,更要根据业务场景去兜底,保证后续的流程正常。

  • 预防胜于治理。当Crash发生的时候,损失已经造成了,我们再怎么治理也只是减少损失。尽可能的提前预防Crash的发生,可以将Crash消灭在萌芽阶段。


常规的Crash治理


常规Crash发生的原因主要是由于开发人员编写代码不小心导致的。解决这类Crash需要由点到面,根据Crash引发的原因和业务本身,统一集中解决。常见的Crash类型包括:空节点、角标越界、类型转换异常、实体对象没有序列化、数字转换异常、Activity或Service找不到等。这类Crash是App中最为常见的Crash,也是最容易反复出现的。在获取Crash堆栈信息后,解决这类Crash一般比较简单,更多考虑的应该是如何避免。下面介绍两个我们治理的量比较大的Crash。


NullPointerException


NullPointerException是我们遇到最频繁的,造成这种Crash一般有两种情况:



  • 对象本身没有进行初始化就进行操作。

  • 对象已经初始化过,但是被回收或者手动置为null,然后对其进行操作。


针对第一种情况导致的原因有很多,可能是开发人员的失误、API返回数据解析异常、进程被杀死后静态变量没初始化导致,我们可以做的有:



  • 对可能为空的对象做判空处理。

  • 养成使用@NonNull和@Nullable注解的习惯。

  • 尽量不使用静态变量,万不得已使用SharedPreferences来存储。

  • 考虑使用Kotlin语言。


针对第二种情况大部分是由于Activity/Fragment销毁或被移除后,在Message、Runnable、网络等回调中执行了一些代码导致的,我们可以做的有:



  • Message、Runnable回调时,判断Activity/Fragment是否销毁或被移除;加try-catch保护;Activity/Fragment销毁时移除所有已发送的Runnable。

  • 封装LifecycleMessage/Runnable基础组件,并自定义Lint检查,提示使用封装好的基础组件。

  • 在BaseActivity、BaseFragment的onDestory()里把当前Activity所发的所有请求取消掉。


IndexOutOfBoundsException


这类Crash常见于对ListView的操作和多线程下对容器的操作。


针对ListView中造成的IndexOutOfBoundsException,经常是因为外部也持有了Adapter里数据的引用(如在Adapter的构造函数里直接赋值),这时如果外部引用对数据更改了,但没有及时调用notifyDataSetChanged(),则有可能造成Crash,对此我们封装了一个BaseAdapter,数据统一由Adapter自己维护通知, 同时也极大的避免了The content of the adapter has changed but ListView did not receive a notification,这两类Crash目前得到了统一的解决。


另外,很多容器是线程不安全的,所以如果在多线程下对其操作就容易引发IndexOutOfBoundsException。常用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap,同时也要注意有一些类的内部实现也是用的线程不安全的容器,如Bundle里用的就是ArrayMap。


系统级Crash治理


众所周知,Android的机型众多,碎片化严重,各个硬件厂商可能会定制自己的ROM,更改系统方法,导致特定机型的崩溃。发现这类Crash,主要靠云测平台配合自动化测试,以及线上监控,这种情况下的Crash堆栈信息很难直接定位问题。下面是常见的解决思路:



  1. 尝试找到造成Crash的可疑代码,看是否有特异的API或者调用方式不当导致的,尝试修改代码逻辑来进行规避。

  2. 通过Hook来解决,Hook分为Java Hook和Native Hook。Java Hook主要靠反射或者动态代理来更改相应API的行为,需要尝试找到可以Hook的点,一般Hook的点多为静态变量,同时需要注意Android不同版本的API,类名、方法名和成员变量名都可能不一样,所以要做好兼容工作;Native Hook原理上是用更改后方法把旧方法在内存地址上进行替换,需要考虑到Dalvik和ART的差异;相对来说Native Hook的兼容性更差一点,所以用Native Hook的时候需要配合降级策略。

  3. 如果通过前两种方式都无法解决的话,我们只能尝试反编译ROM,寻找解决的办法。


我们举一个定制系统ROM导致Crash的例子,根据Crash平台统计数据发现该Crash只发生在vivo V3Max这类机型上,Crash堆栈如下:


java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:304)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
at java.util.concurrent.FutureTask.run(FutureTask.java:242)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
at android.widget.AbsListView$UpdateBottomFlagTask.isSuperFloatViewServiceRunning(AbsListView.java:7689)
at android.widget.AbsListView$UpdateBottomFlagTask.doInBackground(AbsListView.java:7665)
at android.os.AsyncTask$2.call(AsyncTask.java:292)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
... 4 more
复制代码

我们发现原生系统上对应系统版本的AbsListView里并没有UpdateBottomFlagTask类,因此可以断定是vivo该版本定制的ROM修改了系统的实现。我们在定位这个Crash的可疑点无果后决定通过Hook的方式解决,通过源码发现AsyncTask$SerialExecutor是静态变量,是一个很好的Hook的点,通过反射添加try-catch解决。因为修改的是final对象所以需要先反射修改accessFlags,需要注意ART和Dalvik下对应的Class不同,代码如下:


  public static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field artField = Field.class.getDeclaredField("artField");
artField.setAccessible(true);
Object artFieldValue = artField.get(field);
Field accessFlagsFiled = artFieldValue.getClass().getDeclaredField("accessFlags");
accessFlagsFiled.setAccessible(true);
accessFlagsFiled.setInt(artFieldValue, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}

复制代码
private void initVivoV3MaxCrashHander() {
if (!isVivoV3()) {
return;
}
try {
setFinalStatic(AsyncTask.class.getDeclaredField("SERIAL_EXECUTOR"), new SafeSerialExecutor());
Field defaultfield = AsyncTask.class.getDeclaredField("sDefaultExecutor");
defaultfield.setAccessible(true);
defaultfield.set(null, AsyncTask.SERIAL_EXECUTOR);
} catch (Exception e) {
L.e(e);
}
}
复制代码

美团外卖App用上述方法解决了对应的Crash,但是美团App里的外卖频道因为平台的限制无法通过这种方式,于是我们尝试反编译ROM。
Android ROM编译时会将framework、app、bin等目录打入system.img中,system.img是Android系统中用来存放系统文件的镜像 (image),文件格式一般为yaffs2或ext。但Android 5.0开始支持dm-verity后,system.img不再提供,而是提供了三个文件system.new.dat,system.patch.dat,system.transfer.list,因此我们首先需要通过上述的三个文件得到system.img。但我们将vivo ROM解压后发现厂商将system.new.dat进行了分片,如下图所示:



经过对system.transfer.list中的信息和system.new.dat 1 2 3 ... 文件大小对比研究,发现一些共同点,system.transfer.list中的每一个block数*4KB 与对应的分片文件的大小大致相同,故大胆猜测,vivo ROM对system.patch.dat分片也只是单纯的按block先后顺序进行了分片处理。所以我们只需要在转化img前将这些分片文件合成一个system.patch.dat文件就可以了。最后根据system.img的文件系统格式进行解包,拿到framework目录,其中有framework.jar和boot.oat等文件,因为Android4.4之后引入了ART虚拟机,会预先把system/framework中的一些jar包转换为oat格式,所以我们还需要将对应的oat文件通过ota2dex将其解包获得dex文件,之后通过dex2jarjd-gui查看源码。


OOM


OOM是OutOfMemoryError的简称,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草。
导致OOM的原因大部分如下:



  • 内存泄漏,大量无用对象没有被及时回收导致后续申请内存失败。

  • 大内存对象过多,最常见的大对象就是Bitmap,几个大图同时加载很容易触发OOM。


内存泄漏
内存泄漏指系统未能及时释放已经不再使用的内存对象,一般是由错误的程序代码逻辑引起的。在Android平台上,最常见也是最严重的内存泄漏就是Activity对象泄漏。Activity承载了App的整个界面功能,Activity的泄漏同时也意味着它持有的大量资源对象都无法被回收,极其容易造成OOM。
常见的可能会造成Activity泄漏的原因有:



  • 匿名内部类实现Handler处理消息,可能导致隐式持有的Activity对象无法回收。

  • Activity和Context对象被混淆和滥用,在许多只需要Application Context而不需要使用Activity对象的地方使用了Activity对象,比如注册各类Receiver、计算屏幕密度等等。

  • View对象处理不当,使用Activity的LayoutInflater创建的View自身持有的Context对象其实就是Activity,这点经常被忽略,在自己实现View重用等场景下也会导致Activity泄漏。


对于Activity泄漏,目前已经有了一个非常好用的检测工具:LeakCanary,它可以自动检测到所有Activity的泄漏情况,并且在发生泄漏时给出十分友好的界面提示,同时为了防止开发人员的疏漏,我们也会将其上报到服务器,统一检查解决。另外我们可以在debug下使用StrictMode来检查Activity的泄露、Closeable对象没有被关闭等问题。


大对象
在Android平台上,我们分析任一应用的内存信息,几乎都可以得出同样的结论:占用内存最多的对象大都是Bitmap对象。随着手机屏幕尺寸越来越大,屏幕分辨率也越来越高,1080p和更高的2k屏已经占了大半份额,为了达到更好的视觉效果,我们往往需要使用大量高清图片,同时也为OOM埋下了祸根。
对于图片内存优化,我们有几个常用的思路:



  • 尽量使用成熟的图片库,比如Glide,图片库会提供很多通用方面的保障,减少不必要的人为失误。

  • 根据实际需要,也就是View尺寸来加载图片,可以在分辨率较低的机型上尽可能少地占用内存。除了常用的BitmapFactory.Options#inSampleSize和Glide提供的BitmapRequestBuilder#override之外,我们的图片CDN服务器也支持图片的实时缩放,可以在服务端进行图片缩放处理,从而减轻客户端的内存压力。
    分析App内存的详细情况是解决问题的第一步,我们需要对App运行时到底占用了多少内存、哪些类型的对象有多少个有大致了解,并根据实际情况做出预测,这样才能在分析时做到有的放矢。Android Studio也提供了非常好用的Memory Profiler堆转储分配跟踪器功能可以帮我们迅速定位问题。


AOP增强辅助


AOP是面向切面编程的简称,在Android的Gradle插件1.5.0中新增了Transform API之后,编译时修改字节码来实现AOP也因为有了官方支持而变得非常方便。
在一些特定情况下,可以通过AOP的方式自动处理未捕获的异常:



  • 抛异常的方法非常明确,调用方式比较固定。

  • 异常处理方式比较统一。

  • 和业务逻辑无关,即自动处理异常后不会影响正常的业务逻辑。典型的例子有读取Intent Extras参数、读取SharedPreferences、解析颜色字符串值和显示隐藏Window等等。


这类问题的解决原理大致相同,我们以Intent Extras为例详细介绍一下。读取Intent Extras的问题在于我们非常常用的方法 Intent#getStringExtra 在代码逻辑出错或者恶意攻击的情况下可能会抛出ClassNotFoundException异常,而我们平时在写代码时又不太可能给所有调用都加上try-catch语句,于是一个更安全的Intent工具类应运而生,理论上只要所有人都使用这个工具类来访问Intent Extras参数就可以防止此类型的Crash。但是面对庞大的旧代码仓库和诸多的业务部门,修改现有代码需要极大成本,还有更多的外部依赖SDK基本不可能使用我们自己的工具类,此时就需要AOP大展身手了。
我们专门制作了一个Gradle插件,只需要配置一下参数就可以将某个特定方法的调用替换成另一个方法:


WaimaiBytecodeManipulator {
replacements(
"android/content/Intent.getIntExtra(Ljava/lang/String;I)I=com/waimai/IntentUtil.getInt(Landroid/content/Intent;Ljava/lang/String;I)I",
"android/content/Intent.getStringExtra(Ljava/lang/String;)Ljava/lang/String;=com/waimai/IntentUtil.getString(Landroid/content/Intent;Ljava/lang/String;)Ljava/lang/String;",
"android/content/Intent.getBooleanExtra(Ljava/lang/String;Z)Z=com/waimai/IntentUtil.getBoolean(Landroid/content/Intent;Ljava/lang/String;Z)Z",
...)
}
}
复制代码

上面的配置就可以将App代码(包括第三方库)里所有的Intent.getXXXExtra调用替换成IntentUtil类中的安全版实现。当然,并不是所有的异常都只需要catch住就万事大吉,如果真的有逻辑错误肯定需要在开发和测试阶段及时暴露出来,所以在IntentUtil中会对App的运行环境做判断,Debug下会将异常直接抛出,开发同学可以根据Crash堆栈分析问题,Release环境下则在捕获到异常时返回对应的默认值然后将异常上报到服务器。


依赖库的问题


Android App经常会依赖很多AAR, 每个AAR可能有多个版本,打包时Gradle会根据规则确定使用的最终版本号(默认选择最高版本或者强制指定的版本),而其他版本的AAR将被丢弃。如果互相依赖的AAR中有不兼容的版本,存在的问题在打包时是不能发现的,只有在相关代码执行时才会出现,会造成NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError等异常。如图所示,order和store两个业务库都依赖了platform.aar,一个是1.0版本,一个是2.0版本,默认最终打进APK的只有platform 2.0版本,这时如果order库里用到的platform库里的某个类或者方法在2.0版本中被删除了,运行时就可能发生异常,虽然SDK在升级时会尽量做到向下兼容,但很多时候尤其是第三方SDK是没法得到保证的,在美团外卖Android App v6.0版本时因为这个原因导致热修复功能丧失,因此为了提前发现问题,我们接入了依赖检查插件Defensor。




Defensor在编译时通过DexTask获取到所有的输入文件(也就是被编译过的class文件),然后检查每个文件里引用的类、字段、方法等是否存在。



除此之外我们写了一个Gradle插件SVD(strict version dependencies)来对那些重要的SDK的版本进行统一管理。插件会在编译时检查Gradle最终使用的SDK版本是否和配置中的一致,如果不一致插件会终止编译并报错,并同时会打印出发生冲突的SDK的所有依赖关系。


Crash的预防实践


单纯的靠约定或规范去减少Crash的发生是不现实的。约定和规范受限于组织架构和具体执行的个人,很容易被忽略,只有靠工程架构和工具才能保证Crash的预防长久的执行下去。


工程架构对Crash率的影响


在治理Crash的实践中,我们往往忽略了工程架构对Crash率的影响。Crash的发生大部分原因是源于程序员的不合理的代码,而程序员工作中最直接的接触的就是工程架构。对于一个边界模糊,层级混乱的架构,程序员是更加容易写出引起Crash的代码。在这样的架构里面,即使程序员意识到导致某种写法存在问题,想要去改善这样不合理的代码,也是非常困难的。相反,一个层级清晰,边界明确的架构,是能够大大减少Crash发生的概率,治理和预防Crash也是相对更容易。这里我们可以举几个我们实践过的例子阐述。


业务模块的划分
原来我们的Crash基本上都是由个别同学关注解决的,团队里的每个同学都会提交可能引起Crash的代码,如果负责Crash的同学因为某些事情,暂时没有关注App的Crash率,那么造成Crash的同学也不会知道他的代码引起了Crash。


对于这个问题,我们的做法是App的业务模块化。业务模块化后,每个业务都有都有唯一包名和对应的负责人。当某个模块发生了Crash,可以根据包名提交问题给这个模块的负责人,让他第一时间进行处理。业务模块化本身也是工程架构优先需要考虑的事情之一。


页面跳转路由统一处理页面跳转
对外卖App而言,使用过程中最多的就是页面间的跳转,而页面间跳转经常会造成ActivityNotFoundException,例如我们配了一个scheme,但对方的scheme路径已经发生了变化;又例如,我们调用手机上相册的功能,而相册应用已被用户自己禁用或移除了。解决这一类Crash,其实也很简单,只需要在startActivity增加ActivityNotFoundException异常捕获即可。但一个App里,启动Activity的地方,几乎是随处可见,无法预测哪一处会造成ActivityNotFoundException。
我们的做法是将页面的跳转,都通过我们封装的scheme路由去分发。这样的好处是,通过scheme路由,在工程架构上所有业务都是解耦,模块间不需要相互依赖就可以实现页面的跳转和基本类型参数的传递;同时,由于所有的页面跳转都会走scheme路由,我们只需要在scheme路由里一处加上ActivityNotFoundException异常捕获即可解决这种类型的Crash。路由设计示意图如下:



网络层统一处理API脏数据
客户端的很大一部分的Crash是因为API返回的脏数据。比如当API返回空值、空数组或返回不是约定类型的数据,App收到这些数据,就极有可能发生空指针、数组越界和类型转换错误等Crash。而且这样的脏数据,特别容易引起线上大面积的崩溃。
最早我们的工程的网络层用法是:页面监听网络成功和失败的回调,网络成功后,将JSON数据传递给页面,页面解析Model,初始化View,如图所示。这样的问题就是,网络虽然请求成功了,但是JSON解析Model这个过程可能存在问题,例如没有返回数据或者返回了类型不对的数据,而这个脏数据导致问题会出现在UI层,直接反应给用户。



根据上图,我们可以看到由于网络层只承担了请求网络的职责,没有承担数据解析的职责,数据解析的职责交给了页面去处理。这样使得我们一旦发现脏数据导致的Crash,就只能在网络请求的回调里面增加各种判断去兼容脏数据。我们有几百个页面,补漏完全补不过来。通过几个版本的重构,我们重新划分了网络层的职责,如图所示:



从图上可以看出,重构后的网络层负责请求网络和数据解析,如果存在脏数据的话,在网络层就会发现问题,不会影响到UI层,返回给UI层的都是校验成功的数据。这样改造后,我们发现这类的Crash率有了极大的改善。


大图监控


上面讲到大对象是导致OOM的主要原因之一,而Bitmap是App里最常见的大对象类型,因此对占用内存过大的Bitmap对象的监控就很有必要了。
我们用AOP方式Hook了三种常见图片库的加载图片回调方法,同时监控图片库加载图片时的两个维度:



  1. 加载图片使用的URL。外卖App中除静态资源外,所有图片都要求发布到专用的图片CDN服务器上,加载图片时使用正则表达式匹配URL,除了限定CDN域名之外还要求所有图片加载时都要添加对应的动态缩放参数。

  2. 最终加载出的图片结果(也就是Bitmap对象)。我们知道Bitmap对象所占内存和其分辨率大小成正比,而一般情况下在ImageView上设置超过自身尺寸的图片是没有意义的,所以我们要求显示在ImageView中的Bitmap分辨率不允许超过View自身的尺寸(为了降低误报率也可以设定一个报警阈值)。


开发过程中,在App里检测到不合规的图片时会立即高亮出错的ImageView所在的位置并弹出对话框提示ImageView所在的Activity、XPath和加载图片使用的URL等信息,如下图,辅助开发同学定位并解决问题。在Release环境下可以将报警信息上报到服务器,实时观察数据,有问题及时处理。


Lint检查


我们发现线上的很多Crash其实可以在开发过程中通过Lint检查来避免。Lint是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题,提醒开发人员及早修正,提高代码质量。


但是Android原生提供的Lint规则(如是否使用了高版本API)远远不够,缺少一些我们认为有必要的检测,也不能检查代码规范。因此我们开始开发自定义Lint,目前我们通过自定义Lint规则已经实现了Crash预防、Bug预防、提升性能/安全和代码规范检查这些功能。如检查实现了Serializable接口的类,其成员变量(包括从父类继承的)所声明的类型都要实现Serializable接口,可以有效的避免NotSerializableException;强制使用封装好的工具类如ColorUtil、WindowUtil等可以有效的避免因为参数不正确产生的IllegalArgumentException和因为Activity已经finish导致的BadTokenException。


Lint检查可以在多个阶段执行,包括在本地手动检查、编码实时检查、编译时检查、commit时检查,以及在CI系统中提Pull Request时检查、打包时检查等,如下图所示。更详细的内容可参考《美团外卖Android Lint代码检查实践》



资源重复检查


在之前的文章《美团外卖Android平台化架构演进实践》中讲述了我们的平台化演进过程,在这个过程中大家很大的一部分工作是下沉,但是下沉不完全就会导致一些类和资源的重复,类因为有包名的限制不会出现问题。但是一些资源文件如layout、drawable等如果同名则下层会被上层覆盖,这时layout里view的id发生了变化就可能导致空指针的问题。为了避免这种问题,我们写了一个Gradle插件通过hook MergeResource这个Task,拿到所有library和主库的资源文件,如果检查到重复则会中断编译过程,输出重复的资源名及对应的library name,同时避免有些资源因为样式等原因确实需要覆盖,因此我们设置了白名单。同时在这个过程中我们也拿到了所有的的图片资源,可以顺手做图片大小的本地监控,如下图所示:


Crash的监控&止损的实践


监控


在经过前面提到的各种检查和测试之后,应用便开始发布了。我们建立了如下图的监控流程,来保证异常发生时能够及时得到反馈并处理。首先是灰度监控,灰度阶段是增量Crash最容易暴露的阶段,如果这个阶段没有很好的把握住,会使得增量变存量,从而导致Crash率上升。如果条件允许的话,可以在灰度期间制定一些灰度策略去提高这个阶段Crash的暴露。例如分渠道灰度、分城市灰度、分业务场景灰度、新装用户的灰度等等,尽量覆盖所有的分支。灰度结束之后便开始全量,在全量的过程中我们还需要一些日常Crash监控和Crash率的异常报警来防止突发情况的发生,例如因为后台上线或者运营配置错误导致的线上Crash。除此之外还需要一些其他的监控,例如,之前提到的大图监控,来避免因为大图导致的OOM。具体的输出形式主要有邮件通知、IM通知、报表。



止损


尽管我们在前面做了那么多,但是Crash还是无法避免的,例如,在灰度阶段因为量级不够,有些Crash没有被暴露出来;又或者某些功能客户端比后台更早上线,而这些功能在灰度阶段没有被覆盖到;这些情况下,如果出现问题就需要考虑如何止损了。


问题发生时首先需要评估重要性,如果问题不是很严重而且修复成本较高可以考虑在下个版本再修复,相反如果问题比较严重,对用户体验或下单有影响时就必须要修复。修复时首先考虑业务降级,主要看该部分异常的业务是否有兜底或者A/B策略,这样是最稳妥也是最有效的方式。如果业务不能降级就需要考虑热修复了,目前美团外卖Android App接入的热修复框架是自研的Robust,可以修复90%以上的场景,热修成功率也达到了99%以上。如果问题发生在热修复无法覆盖的场景,就只能强制用户升级。强制升级因为覆盖周期长,同时影响用户的体验,只在万不得已的情况下才会使用。


展望


Crash的自我修复


我们在做新技术选型时除了要考虑是否能满足业务需求、是否比现有技术更优秀和团队学习成本等因素之外,兼容性和稳定性也非常重要。但面对国内非富多彩的Android系统环境,在体量百万级以上的的App中几乎不可能实现毫无瑕疵的技术方案和组件,所以一般情况下如果某个技术实现方案可以达到0.01‰以下的崩溃率,而其他方案也没有更好的表现,我们就认为它是可以接受的。但是哪怕仅仅十万分之一的崩溃率,也代表还有用户受到影响,而我们认为Crash对用户来说是最糟糕的体验,尤其是涉及到交易的场景,所以我们必须本着每一单都很重要的原则,尽最大努力保证用户顺利执行流程。


实际情况中有一些技术方案在兼容性和稳定性上做了一定妥协的场景,往往是因为考虑到性能或扩展性等方面的优势。这种情况下我们其实可以再多做一些,进一步提高App的可用性。就像很多操作系统都有“兼容模式”或者“安全模式”,很多自动化机械机器都配套有手动操作模式一样,App里也可以实现备用的降级方案,然后设置特定条件的触发策略,从而达到自动修复Crash的目的。


举例来讲,Android 3.0中引入了硬件加速机制,虽然可以提高绘制帧率并且降低CPU占用率,但是在某些机型上还是会有绘制错乱甚至Crash的情况,这时我们就可以在App中记录硬件加速相关的Crash问题或者使用检测代码主动检测硬件加速功能是否正常工作,然后主动选择是否开启硬件加速,这样既可以让绝大部分用户享受硬件加速带来的优势,也可以保障硬件加速功能不完善的机型不受影响。
还有一些类似的可以做自动降级的场景,比如:



  • 部分使用JNI实现的模块,在SO加载失败或者运行时发生异常则可以降级为Java版实现。

  • RenderScript实现的图片模糊效果,也可以在失败后降级为普通的Java版高斯模糊算法。

  • 在使用Retrofit网络库时发现OkHttp3或者HttpURLConnection网络通道失败率高,可以主动切换到另一种通道。


这类问题都需要根据具体情况具体分析,如果可以找到准确的判定条件和稳定的修复方案,就可以让App稳定性再上一个台阶。


特定Crash类型日志自动回捞


外卖业务发展迅速,即使我们在开发时使用各种工具、措施来避免Crash的发生,但Crash还是不可避免。线上某些怪异的Crash发生后,我们除了分析Crash堆栈信息之外,还可以使用离线日志回捞、下发动态日志等工具来还原Crash发生时的场景,帮助开发同学定位问题,但是这两种方式都有它们各自的问题。离线日志顾名思义,它的内容都是预先记录好的,有时候可能会漏掉一些关键信息,因为在代码中加日志一般只是在业务关键点,在大量的普通方法中不可能都加上日志。动态日志(Holmes)存在的问题是每次下发只能针对已知UUID的一个用户的一台设备,对于大量线上Crash的情况这种操作并不合适,因为我们并不能知道哪个发生Crash的用户还会再次复现这次操作,下发配置充满了不确定性。


我们可以改造Holmes使其支持批量甚至全量下发动态日志,记录的日志等到发生特定类型的Crash时才上报,这样一来可以减少日志服务器压力,同时也可以极大提高定位问题的效率,因为我们可以确定上报日志的设备最后都真正发生了该类型Crash,再来分析日志就可以做到事半功倍。


总结


业务的快速发展,往往不可能给团队充足的时间去治理Crash,而Crash又是App最重要的指标之一。团队需要由一个个Crash个例,去探究每一个Crash发生的最本质原因,找到最合理解决这类Crash的方案,建立解决这一类Crash的长效机制,而不能饮鸩止渴。只有这样,随着版本的不断迭代,我们才能在Crash治理之路上离目标越来越近。


作者:美团技术团队
链接:https://juejin.cn/post/6844903620920492046
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin集成Arouter

使用场景 为了软件间的解耦操作,确保模块之间Activity的相互跳转不受影响。故引用阿里巴巴的Arouter。但官网上还没有针对Kotlin的集成说明,故在此记录下来 如何使用 gradle配置 目录配置,常量类配置 在Application中进行Ar...
继续阅读 »

使用场景



为了软件间的解耦操作,确保模块之间Activity的相互跳转不受影响。故引用阿里巴巴的Arouter。但官网上还没有针对Kotlin的集成说明,故在此记录下来



如何使用



  1. gradle配置

  2. 目录配置,常量类配置

  3. Application中进行Arouter初始化

  4. Activity的配置


1. gradle配置


注意需要在两个地方进行配置


1.根目录下的build.gradle中配置,在dependencies中增加arouter-register引用


 dependencies {
classpath "com.alibaba:arouter-register:1.0.2"
}
复制代码

2.在模块所在的build.gradle中添加引用及编译配置


plugins {
// 1.增加kotlin-kapt引用
id 'kotlin-kapt'
}



android {

// 2.增加Arouter编译配置,注意顺序。此处应该在android{}中
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}

dependencies {

// 3. 添加gradle引用
implementation 'com.alibaba:arouter-api:1.5.1'
kapt "com.alibaba:arouter-compiler:1.5.1"

}
复制代码

注意:此处的与官网教程不一样。官网的配置是针对java的,所以我没有使用
javaCompileOptionsannotationProcessor'com.alibaba:arouter-compiler:1.5.1'这两个配置对kotlin不生效。


2. 目录配置,常量类配置


新建一个ui包用于存放需要跳转的Activity,随后新建一个ConstantObject文件。添加Activity的常量资源


常量类Constants


object Constants {
object Activitys{
const val RECYCLELIST_ACTIVITY = "/ui/RecycleListActivity"
}
}
复制代码

目录结构


目录结构


3. 在Application中进行Arouter初始化


class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG){
ARouter.openLog()
ARouter.openDebug()
}
ARouter.init(this)

}

override fun onTerminate() {
super.onTerminate()
ARouter.getInstance().destroy()
}
}
复制代码

**注意:**此处有两个小坑。



  1. 重写的APP类需要在Manifest中进行添加,否则不会执行。(只需要在application节点中添加name并指向这个类即可)

  2. 注意BuildConfig这个类是引用谁的,因为Arouter本身也有BuildConfig此处需要引用AnroidBuildConfig。博主引用错了后,一直无法跳转。而且也一直没有报错,坑了很久


4. Activity的配置


以上工作做完后,就可以在需要跳转的Activity进行配置了。


跳转到的Activity,增加@Route注解


@Route(path = Constants.Activitys.RECYCLELIST_ACTIVITY)
class RecycleListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycle_list)
}
}
复制代码

需要进行跳转的Activity,调用Arouter单例进行跳转


 mBtnList.setOnClickListener {
ARouter.getInstance().build(Constants.Activitys.RECYCLELIST_ACTIVITY).navigation()
}

作者:约束№证
链接:https://juejin.cn/post/6955465825809760286
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Object类和Any详解

Any Any类是kotlin类结构的跟,每个kotlin都继承或间接继承于Any类 /** * The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superc...
继续阅读 »

Any


Any类是kotlin类结构的跟,每个kotlin都继承或间接继承于Any类


/**
* The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
*/
public open class Any {
// kotlin的函数可以没有函数体,其不是Abstract方法,所以子类不必重写。
public open operator fun equals(other: Any?): Boolean
public open fun hashCode(): Int
public open fun toString(): String
}
复制代码

里面有三个open的方法equals、hashCode和toString,其中equals和hashCode如果需要修改就必须同时修改。


Object


同样java中Object也是class结构的根,每个类继承或者间接继承于Object


package java.lang;

public class Object {
public Object() {
}

private static native void registerNatives();

public final native Class<?> getClass();

public native int hashCode();

public boolean equals(Object var1) {
return this == var1;
}

protected native Object clone() throws CloneNotSupportedException;

public String toString() {
return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
}

public final native void notify();

public final native void notifyAll();

public final native void wait(long var1) throws InterruptedException;

public final void wait(long var1, int var3) throws InterruptedException {
if (var1 < 0L) {
throw new IllegalArgumentException("timeout value is negative");
} else if (var3 >= 0 && var3 <= 999999) {
if (var3 > 0) {
++var1;
}

this.wait(var1);
} else {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
}

public final void wait() throws InterruptedException {
this.wait(0L);
}

protected void finalize() throws Throwable {
}

static {
registerNatives();
}
}
复制代码

相比于Kotlin,java中的class方法丰富的多,十二个。其中7个本地方法包含一个静态本地方法,5个可以被子类覆盖的方法


private static native void registerNatives();

static {
registerNatives();
}
复制代码

静态本地方法在类加载时执行。该方法的作用是通过类加载器加载一些本地方法到JVM中。Object类在被加载时,会加载一些methods中的本地方法到JVM中如下:


static JNINativeMethod methods[] = {
{“hashCode”, “()I”, (void *)&JVM_IHashCode},
{“wait”, “(J)V”, (void *)&JVM_MonitorWait},
{“notify”, “()V”, (void *)&JVM_MonitorNotify},
{“notifyAll”, “()V”, (void *)&JVM_MonitorNotifyAll},
{“clone”, “()Ljava/lang/Object;”, (void *)&JVM_Clone},
};
复制代码

@Contract(pure = true) public final native Class<?> getClass();

返回该对象的类的Class对象。Class对象可以用于反射等场景。


public native int hashCode();

返回对象的哈希值,主要用于HashMap的hash tables。


哈希需要注意的几点:




  • 相等的对象必须要有相同的哈希码




  • 不相等的对象一定有着不同的哈希码——错!




  • 有同一个哈希值的对象一定相等——错!




  • 重写equals时必须重写hashCode




equals

 public boolean equals(Object var1) {
return this == var1;
}
复制代码

判断引用是否指向同一个地址,就是判断两个引用指向的对象是否是同一个对象, String重写了该方法,判断字符串是否相等。


protected native Object clone() throws CloneNotSupportedException;

该方法用于拷贝。要调用该方法需要类实现Cloneable接口,否则抛出CloneNotSupportedException。


浅拷贝,重写clone方法,调用super.clone():


public class TestOne implements Cloneable {
@NonNull
@Override
protected TestOne clone() {
TestOne obj = null;
try {
obj = (TestOne) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
复制代码

深拷贝


public class TestTwo implements Cloneable {
public TestOne var;
@NonNull
@Override
protected TestTwo clone() {
TestTwo obj = null;
try {
obj = (TestTwo) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
obj.var = obj.var.clone();
return obj;
}
}
复制代码

public String toString()

返回类名和对象的哈希值的十六进制字符串,推荐子类重写该方法。


notify()、notifyAll()、wait()方法

从1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。


public synchronize void method(){
method body
}
等同于
public void method(){
this.intrinsicLock.lock();
try{
method body
}
finally{ this.intrinsicLock.unlock();}
}
复制代码

从这个示例我们即可看出上面几个方法的作用。


public final native void notify();

随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法或同步块中调用。如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常。


public final native void notifyAll();

解除那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常。


public final void wait() throws InterruptedException

使线程进入等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常。


public final void wait(long millis, int nanos) throws InterruptedException

public final native void wait(long millis) throws InterruptedException;

参数:millis 毫秒数 nanos 纳秒数 < 1000 000

使线程进入等待状态直到它被通知或者经过指定的时间。这些方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者该方法抛出一个IllegalMonitorStateException异常。


protected void finalize() throws Throwable

当一个堆空间中的对象没有被栈空间变量指向的时候,这个对象会等待被java回收。


GC特点:



  • 当对象不再被程序所使用的时候,垃圾回收器将会将其回收

  • 垃圾回收是在后台运行的,我们无法命令垃圾回收器马上回收资源,但是我们可以告诉他可以尽快回收资源(System.gc()和Runtime.getRuntime().gc())

  • 垃圾回收器在回收某个对象的时候,首先会调用该对象的finalize()方法

  • GC主要针对堆内存

  • 单例模式的缺点


Any和Object相同点


Kotlin中的Any只存在于编译期,运行期就不存在了。


val any = Any()
println("any:$any ")
println("anyClass:${any.javaClass} ")

val obj = any as Object
synchronized(obj){
obj.wait()
}
println("obj:$obj ")
println("obj:${any.`class`} ")

I/System.out: any:java.lang.Object@d12ebc1
I/System.out: anyClass:class java.lang.Object
I/System.out: obj:java.lang.Object@d12ebc1
I/System.out: obj:class java.lang.Object
复制代码

从上面的示例可以看出在runtime,Any变成了Object,在kotlin中也可以将Any强转为Object。


从Kolitn的官方文档 kotlinlang.org/docs/java-i… 可以看到Object对应的就是Any


Kotlin专门处理一些Java类型。这些类型不是按原样从Java加载的,而是映射到相应的Kotlin类型。映射只在编译时起作用,运行时表示保持不变。Java的原语类型映射到相应的Kotlin类型(保持平台类型)


从上面的示例可以看出obj可以any混用,any强转后不仅可以使用notify()等方法,还可以使用Any的扩展方法,如使用 obj.apply { }


作者:zbt
链接:https://juejin.cn/post/6955413784143855629
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS-TZImagePickerController获取图片视频

TZImagePickerControllerDemo项目介绍TZImagePickerControllerDemoPodfile新增行 pod 'TZImagePickerController'使用之前导入头文件 #import <...
继续阅读 »

TZImagePickerControllerDemo

项目介绍

TZImagePickerControllerDemo

Podfile新增行 pod 'TZImagePickerController'

使用之前导入头文件 #import <TZImagePickerController.h>

使用说明

1. 定义 类变量:
  UIImagePickerController* picker_library_;
2.实现 UIImagePickerControllerDelegate 这个delegate,还需要UINavigationControllerDelegate 这个代理
3. 以模态的方式,显示 图片选取器

picker_library_ = [[UIImagePickerController alloc] init];  
picker_library_.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker_library_.allowsEditing = YES;
picker_camera_.allowsImageEditing=YES;
picker_library_.delegate = self;
[self presentModalViewController: picker_library_
animated: YES];

其中,sourceType 指定了 几种 图片的来源:
UIImagePickerControllerSourceTypePhotoLibrary:表示显示所有的照片
UIImagePickerControllerSourceTypeCamera:表示从摄像头选取照片
UIImagePickerControllerSourceTypeSavedPhotosAlbum:表示仅仅从相册中选取照片。
allowEditing和allowsImageEditing 设置为YES,表示 允许用户编辑图片,否则,不允许用户编辑。

选照片

//MaxImagesCount  可以选着的最大条目数
TZImagePickerController *imagePicker = [[TZImagePickerController alloc] initWithMaxImagesCount:1 delegate:self];

// 是否显示可选原图按钮
imagePicker.allowPickingOriginalPhoto = NO;
// 是否允许显示视频
imagePicker.allowPickingVideo = NO;
// 是否允许显示图片
imagePicker.allowPickingImage = YES;

// 这是一个navigation 只能present
[self presentViewController:imagePicker animated:YES completion:nil];

选择照片的回调

// 选择照片的回调
-(void)imagePickerController:(TZImagePickerController *)picker
didFinishPickingPhotos:(NSArray<UIImage *> *)photos
sourceAssets:(NSArray *)assets
isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto{

}

选视频

//MaxImagesCount  可以选着的最大条目数
TZImagePickerController *imagePicker = [[TZImagePickerController alloc] initWithMaxImagesCount:2 delegate:self];

// 是否显示可选原图按钮
imagePicker.allowPickingOriginalPhoto = NO;
// 是否允许显示视频
imagePicker.allowPickingVideo = YES;
// 是否允许显示图片
imagePicker.allowPickingImage = NO;

// 这是一个navigation 只能present
[self presentViewController:imagePicker animated:YES completion:nil];

选择视频的回调

// 选择视频的回调
-(void)imagePickerController:(TZImagePickerController *)picker
didFinishPickingVideo:(UIImage *)coverImage
sourceAssets:(PHAsset *)asset{

}
收起阅读 »

Android修炼系列(五),写一篇超全面的annotation讲解(2)

自定义编译期注解(CLASS) 为什么要最后说编译期注解呢,因为相对前面的自定义注解来说,编译期注解有些难度,涉及到的东西比较多,但却是平时用到的最多的注解,因为编译期注解不存在反射,所以对性能没有影响。 本来也想用绑定 View 的例子讲解,但是现在这样的 ...
继续阅读 »

自定义编译期注解(CLASS)


为什么要最后说编译期注解呢,因为相对前面的自定义注解来说,编译期注解有些难度,涉及到的东西比较多,但却是平时用到的最多的注解,因为编译期注解不存在反射,所以对性能没有影响。


本来也想用绑定 View 的例子讲解,但是现在这样的 demo 网上各种泛滥,而且还有各路大牛写的,所以我就没必要班门弄斧了。在这里以跳转界面为例:


    Intent intent = new Intent (this, NextActivity.class);
startActivity (intent);
复制代码

本着方便就是改进的原则,让我们定义一个编译期注解,来自动生成上述的代码,想想每次需要的时候只需要一个注解就能跳转到想要跳转的界面是不是很刺激。


1.首先新建一个 android 项目,在创建两个 java module(File -> New -> new Module ->java Module),因为有的类在android项目中不支持,建完后项目结构如下:


这里写图片描述


其中 annotation 中盛放自定义的注解,annotationprocessor 中创建注解处理器并做相关处理,最后的 app 则为我们的项目。


注意:MyFirstProcessor类为上文讲解 AbstractProcessor 所建的类,可以删去,跟本项目没有关系。


2.处理各自的依赖


annotation


processor


app


3.编写自定义注解,这是一个应用到字段之上的注解,被注解的字段为传递的参数


/**
* 这是一个自定义的跳转传值所用到的注解。
* value 表示要跳转到哪个界面activity的元素,传入那个界面的名字。
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface IntentField {
String value () default " ";
}
复制代码

4.自定义注解处理器,获取被注解元素的类型,进行相应的操作。


@AutoService(javax.annotation.processing.Processor.class)
public class MyProcessot extends AbstractProcessor{

private Map<Element, List<VariableElement>> items = new HashMap<>();
private List<Generator> generators = new LinkedList<>();

// 做一些初始化工作
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
Utils.init();
generators.add(new ActivityEnterGenerator());
generators.add(new ActivityInitFieldGenerator());
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

// 获取所有注册IntentField注解的元素
for (Element elem : roundEnvironment.getElementsAnnotatedWith(IntentField.class)) {
// 主要获取ElementType 是不是null,即class,interface,enum或者注解类型
if (elem.getEnclosingElement() == null) {
// 直接结束处理器
return true;
}

// 如果items的key不存在,则添加一个key
if (items.get(elem.getEnclosingElement()) == null) {
items.put(elem.getEnclosingElement(), new LinkedList<VariableElement>());
}

// 我们这里的IntentField是应用在一般成员变量上的注解
if (elem.getKind() == ElementKind.FIELD) {
items.get(elem.getEnclosingElement()).add((VariableElement)elem);
}
}

List<VariableElement> variableElements;
for (Map.Entry<Element, List<VariableElement>> entry : items.entrySet()) {
variableElements = entry.getValue();
if (variableElements == null || variableElements.isEmpty()) {
return true;
}
// 去通过自动javapoet生成代码
for (Generator generator : generators) {
generator.genetate(entry.getKey(), variableElements, processingEnv);
generator.genetate(entry.getKey(), variableElements, processingEnv);
}
}
return false;
}

// 指定当前注解器使用的Java版本
@Override public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

// 指出注解处理器 处理哪种注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>(2);
annotations.add(IntentField.class.getCanonicalName());
return annotations;
}
}
复制代码

5.这是一个工具类方法,提供了本 demo 中所用到的一些方法,其实实际里面的方法都很常见,只不过做了一个封装而已.


public class Utils {

private static Set<String> supportTypes = new HashSet<>();

/** 当getIntent的时候,每种类型写的方式都不一样,所以把每种方式都添加到了Set容器中。*/
static void init() {
supportTypes.add(int.class.getSimpleName());
supportTypes.add(int[].class.getSimpleName());
supportTypes.add(short.class.getSimpleName());
supportTypes.add(short[].class.getSimpleName());
supportTypes.add(String.class.getSimpleName());
supportTypes.add(String[].class.getSimpleName());
supportTypes.add(boolean.class.getSimpleName());
supportTypes.add(boolean[].class.getSimpleName());
supportTypes.add(long.class.getSimpleName());
supportTypes.add(long[].class.getSimpleName());
supportTypes.add(char.class.getSimpleName());
supportTypes.add(char[].class.getSimpleName());
supportTypes.add(byte.class.getSimpleName());
supportTypes.add(byte[].class.getSimpleName());
supportTypes.add("Bundle");
}

/** 获取元素所在的包名。*/
public static String getPackageName(Element element) {
String clazzSimpleName = element.getSimpleName().toString();
String clazzName = element.toString();
return clazzName.substring(0, clazzName.length() - clazzSimpleName.length() - 1);
}


/** 判断是否是String类型或者数组或者bundle,因为这三种类型getIntent()不需要默认值。*/
public static boolean isElementNoDefaultValue(String typeName) {
return (String.class.getName().equals(typeName) || typeName.contains("[]") || typeName.contains("Bundle"));
}

/**
* 获得注解要传递参数的类型。
* @param typeName 注解获取到的参数类型
*/
public static String getIntentTypeName(String typeName) {
for (String name : supportTypes) {
if (name.equals(getSimpleName(typeName))) {
return name.replaceFirst(String.valueOf(name.charAt(0)), String.valueOf(name.charAt(0)).toUpperCase())
.replace("[]", "Array");
}
}
return "";
}

/**
* 获取类的的名字的字符串。
* @param typeName 可以是包名字符串,也可以是类名字符串
*/
static String getSimpleName(String typeName) {
if (typeName.contains(".")) {
return typeName.substring(typeName.lastIndexOf(".") + 1, typeName.length());
}else {
return typeName;
}
}


/** 自动生成代码。*/
public static void writeToFile(String className, String packageName, MethodSpec methodSpec, ProcessingEnvironment processingEnv, ArrayList<FieldSpec> listField) {
TypeSpec genedClass;
if(listField == null) {
genedClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(methodSpec).build();
}else{
genedClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(methodSpec)
.addFields(listField).build();
}
JavaFile javaFile = JavaFile.builder(packageName, genedClass)
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}

}

复制代码

6.自定义一个接口,把需要自动生成的每个java文件的方法都独立出去。


public interface Generator {
void genetate(Element typeElement
, List<VariableElement> variableElements
, ProcessingEnvironment processingEnv);

}
复制代码

7.编写自动生成文件的格式,生成后的类格式如下:


跳转类格式


上图为本例中的MainActivity$Enter类,如果你想生成一个类,那么这个类的格式和作用肯定已经在你的脑海中有了定型,如果你自己都不知道想要生成啥,那还玩啥。


/**
* 这是一个要自动生成跳转功能的.java文件类
* 主要思路:1.使用javapoet生成一个空方法
* 2.为方法加上实参
* 3.方法的里面的代码拼接
* 主要需要:获取字段的类型和名字,获取将要跳转的类的名字
*/
public class ActivityEnterGenerator implements Generator{

private static final String SUFFIX = "$Enter";

private static final String METHOD_NAME = "intentTo";

@Override
public void genetate(Element typeElement, List<VariableElement> variableElements, ProcessingEnvironment processingEnv) {
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.returns(void.class);
// 设置生成的METHOD_NAME方法第一个参数
methodBuilder.addParameter(Object.class, "context");
methodBuilder.addStatement("android.content.Intent intent = new android.content.Intent()");

// 获取将要跳转的类的名字
String name = "";

// VariableElement 主要代表一般字段元素,是Element的一种
for (VariableElement element : variableElements) {
// Element 只是一种语言元素,本身并不包含信息,所以我们这里获取TypeMirror
TypeMirror typeMirror = element.asType();
// 获取注解在身上的字段的类型
TypeName type = TypeName.get(typeMirror);
// 获取注解在身上字段的名字
String fileName = element.getSimpleName().toString();
// 设置生成的METHOD_NAME方法第二个参数
methodBuilder.addParameter(type, fileName);
methodBuilder.addStatement("intent.putExtra(\"" + fileName + "\"," +fileName + ")");
// 获取注解上的元素
IntentField toClassName = element.getAnnotation(IntentField.class);
String name1 = toClassName.value();
if(null != name && "".equals(name)){
name = name1;
}
// 理论上每个界面上的注解value一样,都是要跳转到的那个类名字,否则提示错误
else if(name1 != null && !name1.equals(name)){
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "同一个界面不能跳转到多个活动,即value必须一致");
}
}
methodBuilder.addStatement("intent.setClass((android.content.Context)context, " + name +".class)");
methodBuilder.addStatement("((android.content.Context)context).startActivity(intent)");

/**
* 自动生成.java文件
* 第一个参数:要生成的类的名字
* 第二个参数:生成类所在的包的名字
* 第三个参数:javapoet 中提供的与自动生成代码的相关的类
* 第四个参数:能够为注解器提供Elements,Types和Filer
*/
Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX, Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv,null);
}

}
复制代码

当我们定义了跳转的类,那么接下来肯定就是在另一个界面获取传递过来的数据了,参考格式如下,这是本demo中自动生成的MainActivity$Init 类。


获取参数格式


/**
* 要生成一个.Java文件,在这个Java文件里生成一个获取上个界面传递过来数据的方法
* 主要思路:1.使用Javapoet生成一个空的的方法
* 2.为方法添加需要的形参
* 3.拼接方法内部的代码
* 主要需要:获取传递过来字段的类型
*/
public class ActivityInitFieldGenerator implements Generator {

private static final String SUFFIX = "$Init";

private static final String METHOD_NAME = "initFields";

@Override
public void genetate(Element typeElement, List<VariableElement> variableElements, ProcessingEnvironment processingEnv) {

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PROTECTED)
.returns(Object.class);

final ArrayList<FieldSpec> listField = new ArrayList<>();

if (null != variableElements && variableElements.size() != 0) {
VariableElement element = variableElements.get(0);
// 当前接收数据的字段的名字
IntentField currentClassName = element.getAnnotation(IntentField.class);
String name = currentClassName.value();

methodBuilder.addParameter(Object.class, "currentActivity");
methodBuilder.addStatement(name + " activity = (" + name + ")currentActivity");
methodBuilder.addStatement("android.content.Intent intent = activity.getIntent()");
}

for (VariableElement element : variableElements) {

// 获取接收字段的类型
TypeName currentTypeName = TypeName.get(element.asType());
String currentTypeNameStr = currentTypeName.toString();
String intentTypeName = Utils.getIntentTypeName(currentTypeNameStr);

// 字段的名字,即key值
Name filedName = element.getSimpleName();

// 创建成员变量
FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(element.asType()),filedName+"")
.addModifiers(Modifier.PUBLIC)
.build();
listField.add(fieldSpec);

// 因为String类型的获取 和 其他基本类型的获取在是否需要默认值问题上不一样,所以需要判断是哪种
if (Utils.isElementNoDefaultValue(currentTypeNameStr)) {
methodBuilder.addStatement("this."+filedName+"= intent.get" + intentTypeName + "Extra(\"" + filedName + "\")");
} else {
String defaultValue = "default" + element.getSimpleName();
if (intentTypeName == null) {
// 当字段类型为null时,需要打印错误信息
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "the type:" + element.asType().toString() + " is not support");
} else {
if ("".equals(intentTypeName)) {
methodBuilder.addStatement("this." + filedName + "= (" + TypeName.get(element.asType()) + ")intent.getSerializableExtra(\"" + filedName + "\")");
} else {
methodBuilder.addParameter(TypeName.get(element.asType()), defaultValue);
methodBuilder.addStatement("this."+ filedName +"= intent.get"
+ intentTypeName + "Extra(\"" + filedName + "\", " + defaultValue + ")");
}
}
}
}
methodBuilder.addStatement("return this");
Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX, Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv, listField);
}
}
复制代码

8、在Activity中使用刚才的自定义注解。


public class MainActivity extends AppCompatActivity {

@IntentField("NextActivity")
int count = 10;
@IntentField("NextActivity")
String str = "编译器注解";
@IntentField("NextActivity")
StuBean bean = new StuBean(1,"No1");

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
addOnclickListener();
}

public void addOnclickListener() {
findViewById(R.id.tvnext).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 从哪个界面进行跳转,则以哪个界面打头,enter 结尾
// 例如 MainActivity$Enter
new MainActivity$Enter()
.intentTo(MainActivity.this, count, str, bean);
}
});
}
}
复制代码

9.这是实体bean


public class StuBean implements Serializable{
public StuBean(int id , String name) {
this.id = id;
this.name = name;
}
//学号
public int id;
//姓名
public String name;
}
复制代码

10、在NextActivity接收并打印数据:


public class NextActivity extends AppCompatActivity {

private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);
textView = (TextView) findViewById(R.id.tv);

// 想获取从哪个界面传递过来的数据,就已哪个类打头,init结尾
// 例如 MainActivity$Init
MainActivity$Init formIntent = (MainActivity$Init)new MainActivity$Init().initFields(this,0);
textView.setText(formIntent.count + "---" + formIntent.str + "---" +formIntent.bean.name);

// 打印上个界面传递过来的数据
Log.i("Tag",formIntent.count + "---" + formIntent.str + "---" + formIntent.bean.name);
}
}
复制代码

11.运行结果:


这里写图片描述


总结


好了,看到这里,你应该对注解有所了解了,但是看的再懂也不如自己动手练一下。如果你仔细研究了,你会发现一个非常奇怪的事情,当我们设置 RetentionPolicy.CLASS 级别的时候,仍能通过反射获取注解信息,当我们设置 RetentionPolicy.SOURCE 级别的时候,仍能走通编译期注解,是不是非常迷惑。


之后只能又找了一些资料(非权威),看到了一个比较受认同的解释:这个属性主要给IDE 或者编译器开发者准备的,一般应用级别上不太会用到。



好了,本文到这里就结束了,关于注解的讲解应该非常全面了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考 1、B.E,Java编程思想:机械工业出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6936609673416015908
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(五),写一篇超全面的annotation讲解(1)

不学注解,也许是因为平时根本不需要没事自定义个这玩意玩,可随着Android形势越来越内卷,不学点东西是真不行了。而通过本文的学习,可以让你对于注解有个全面的认识,你会发现,小小的注解,大有可为,编不下去了.. 注解不同于注释,注释的作用是为了方便自己或者别...
继续阅读 »

不学注解,也许是因为平时根本不需要没事自定义个这玩意玩,可随着Android形势越来越内卷,不学点东西是真不行了。而通过本文的学习,可以让你对于注解有个全面的认识,你会发现,小小的注解,大有可为,编不下去了..



注解不同于注释,注释的作用是为了方便自己或者别人的阅读,能够利用 javadoc 提取源文件里的注释来生成人们所期望的文档,对于代码本身的运行是没有任何影响的。


而注解的功能就要强大很多,不但能够生成描述符文件,而且有助于减轻编写“样板”代码的负担,使代码干净易读。通过使用扩展的注解(annotation)API 我们能够在 编译期运行期 对代码进行操控。



注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后的某个时刻非常方便的使用这些数据。 —Jeremy Meyer



本文主要对于下面几个方面进行讲解,篇幅很长,建议收藏查看:



Java 最初内置的三种标准注解


注解是 java SE5中的重要的语言变化之一,你可能对注解的原理不太理解,但你每天的开发中可能无时无刻不在跟注解打交道,最常见的就是 @Override 注解,所以注解并没有那么神秘,也没有那么冷僻,不要害怕使用注解(虽然使用的注解大部分情况都是根据需要自定义的注解),用的多了自然就熟了。为什么说最初的三种标准注解呢,因为在后续的 java 版本中又陆陆续续的增加了一些注解,不过原理都是一样的。























java SE5内置的标准注解含义
@Override表示当前的方法定义将覆盖超类中的方法,如果方法拼写错误或者方法签名不匹配,编译器便会提出错误提示
@Deprecated表示当前方法已经被弃用,如果开发者使用了注解为它的元素,编译器便会发出警告信息
@SuppressWarnings可以关闭不当的编译器警告信息

Java 提供的四种元注解和一般注解


所谓元注解(meta-annotation)也是一种注解,只不过这种注解负责注解其他的注解。所以再说元注解之前我们来看一下普通的注解:



public @interface LogClassMessage {}



这是一个最普通的注解,注解的定义看起来很像一个接口,在 interface 前加上 @ 符号。事实上在语言级别上,注解也和 java 中的接口、类、枚举是同一个级别的,都会被编译成 class 文件。而前面提到的元注解存在的目的就是为了修饰这些普通注解,但是要明确一点,元注解只是给普通注解提供了作用,并不是必须存在的。



























java 提供的元注解作用
@Target定义你的注解应用到什么地方(详见下文解释)
@Retention定义该注解在哪个级别可用(详见下文解释)
@Documented将此注解包含在 javadoc 中
@Inherited允许子类继承超类中的注解

〔1〕@Target使用的时候添加一个 ElementType 参数,表示当前注解可以应用到什么地方,即可以指定一种,也可以同时指定多种,使用方法如下:


    // 表示当前的注解只能应用到类、接口(包括注解)、enum上面
@Target(ElementType.TYPE)
public @interface LogClassMessage {}
复制代码

    // 表示当前的注解只能应用到方法和成员变量上面
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {}
复制代码

下面来看一下 ElementType 的全部参数含义:







































ElementType 参数说明
ElementType.CONSTRUCTOR构造器的声明
ElementType.FIELD域的声明(包括enum的实例)
ElementType.LOCATION_VARLABLE局部变量的声明
ElementType.METHOD方法的声明
ElementType.PACKAGE包的声明
ElementType.PARAMETER参数的声明
ElementType.TYPE类、接口(包括注解类型)、enum声明

〔2〕@Retention用来注解在哪一个级别可用,需要添加一个 RetentionPolicy 参数,用来表示在源代码中(SOURCE),在类文件中(CLASS)或者运行时(RUNTIME):


    // 表示当前注解运行时可用
@Retention(RetentionPolicy.RUNTIME)
public @interface LogClassMessage {}
复制代码

下面来看一下 RetentionPolicy 的全部参数含义:























RetentionPolicy 参数说明
RetentionPolicy.SOURCE注解将被编译器丢弃,只能存于源代码中
RetentionPolicy.CLASS注解在class文件中可用,能够存于编译之后的字节码之中,但会被VM丢弃
RetentionPolicy.RUNTIMEVM在运行期也会保留注解,因此运行期注解可以通过反射获取注解的相关信息

在注解中,一般都会包含一些元素表示某些值,并且可以为这些元素设置默认值,没有元素的注解也称为标记注解(marker annotation)


    @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {
public int id () default -1;
public String message() default "";
}
复制代码

注:虽然上面的 id 和 message 定义和接口的方法定义很类似,但是在注解中将 id 和 message 称为:int 元素 id , String 元素 message。而且注解元素的类型是有限制的,并不是任何类型都可以,主要包括:基本数据类型(理论上是没有基本类型的包装类型的,但是由于自动封装箱,所以也不会报错)、String 类型、enum 类型、Class 类型、Annotation 类型、以及以上类型的数组,(没有等字,说明目前注解的元素类型只支持上面列出的这几种),否则编译器便会提示错误。



invalid type 'void ' for annotation member // 例如注解类型为void的错误信息



对于默认值限制 ,Bruce Eckel 在其书中是这样描述的:编译器对元素的默认值有些过分挑剔,首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供注解的值。其次,对于非基本类型的元素,无论在源代码声明中,或者在注解接口中定义默认值时,都不能以 null 作为其值。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有元素都存在,并且都具有相应的值。为了绕开这个约束,我们只能自己定义一些特殊的值,例如空字符串或者负数,以此表示某个元素的不存在,这算得上是一个习惯用法。


参考系统的标准注解


怎么说呢,接触一种知识的途径有很多,可能每一种的结果都是大同小异的,都能让你学到东西,但是实现的方式、实现过程中的规范、方法和思路却并不一定是最佳的。


上文讲到的是注解的基本语法,那么系统是怎么用的呢?首先让我们来看一下使用频率最高的 @Override :


    @Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}
复制代码

〔1〕首先系统定义一个没有元素的标记注解 Override ,随后使用元注解 @Target 指明 Override 注解只能应用于方法之上(你可以细想想,是不是在我们实际使用这个注解的时候,只能是重写方法,没有见过重写类或者字段的吧),使用注解 @Retention 表示当前注解只能存在源代码中,并不会出现在编译之后的 class 文件之中。


    @Override
protected void onResume() {
super.onResume();
}
复制代码

〔2〕如在 Activity 中我们可以重写 onResume() 方法,添加注解 @override 之后编译器便会去检查父类中是否存在相同方法,如果不存在便会报错。


〔3〕也许到这里你会感到很疑惑,注解到底是怎么工作的,怎么系统这样定义一个注解 Override 它就能工作了?黑魔法吗,擦擦,完成看不到实现过程嘛(泪流满面),经过查阅了一些资料(非权威)了解到,其实处理过程都编写在了编译器里面,也就是说编译器已经给我们写好了处理方法,当编译器进行检查的时候就会调用相应的处理方法。


注解处理器


介绍之前,先引用 Jeremy Meyer 的一段话:如果没有用来读取注解的工具,那么注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5 扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具 apt帮助程序员解析带有注解的 java 源代码。


根据上面描述我们可以知道,注解处理器并不是一个特定格式,并不是只有继承了 AbstractProcessor 这个抽象类才叫注解处理器,凡是根据相关API 来读取注解的类或者方法都可以称为注解处理器。


反射机制下的处理器


最简单的注解处理器莫过于,直接使用反射机制的 getDeclaredMethods 方法获取类上所有方法(字段原理是一样的),再通过调用 getAnnotation 获取每个方法上的特定注解,有了注解便可以获取注解之上的元素值,方法如下:


    public void getAnnoUtil(Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
LogClassMessage logClassMessage = m.getAnnotation(LogClassMessage .class);
if(null != logClassMessage) {
int id = logClassMessage.id();
String method = logClassMessage.message();
}
}
}
复制代码

由于反射对性能会有一定的损耗,所以上述类型的注解处理器并不占主流,现在使用最多的还是 AbstractProcessor 自定义注解处理器,因为后者并不需要通过反射实现,效率和直接调用普通方法没有区别,这也是为什么编译期注解比运行时注解更受欢迎。


但是并不是说为了性能运行期注解就不能用了,只能说不能滥用,要在性能方面给予考虑。目前主要的用到运行期注解的框架差不多都有缓存机制,只有在第一次使用时通过反射机制,当再次使用时直接从缓存中取出。


好了,说着说着就跑题,还是来聊一下这个 AbstractProcessor 类吧,到底有何魅力让这么多人为她沉迷,方法如下:



public class MyFirstProcessor extends AbstractProcessor {

/**
* 做一些初始化工作,注释处理工具框架调用了这个方法,给我们传递一个 ProcessingEnvironment 类型的实参。
*
* <p>如果在同一个对象多次调用此方法,则抛出IllegalStateException异常。
*
* @param processingEnvironment 这个参数里面包含了很多工具方法
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {

// 返回用来在元素上进行操作的某些工具方法的实现
Elements es = processingEnvironment.getElementUtils();
// 返回用来创建新源、类或辅助文件的Filer
Filer filer = processingEnvironment.getFiler();
// 返回用来在类型上进行操作的某些实用工具方法的实现
Types types = processingEnvironment.getTypeUtils();

// 这是提供给开发者日志工具,我们可以用来报告错误和警告以及提示信息
// 注意 message 使用后并不会结束过程,Kind 参数表示日志级别
Messager messager = processingEnvironment.getMessager();
messager.printMessage(Diagnostic.Kind.ERROR, "例如当默认值为空则提示一个错误");
// 返回任何生成的源和类文件应该符合的源版本
SourceVersion version = processingEnvironment.getSourceVersion();

super.init(processingEnvironment);
}

/**
* @return 如果返回true 不要求后续Processor处理它们,反之,则继续执行处理。
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

/**
* TypeElement 这表示一个类或者接口元素集合常用方法不多,TypeMirror getSuperclass()返回直接超类。
*
* <p>详细介绍下 RoundEnvironment 这个类,常用方法:
* boolean errorRaised() 如果在以前的处理round中发生错误,则返回true
* Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)
* 这里的 a 即你自定义的注解class类,返回使用给定注解类型注解的元素的集合
* Set<? extends Element> getElementsAnnotatedWith(TypeElement a)
*
* <p>Element 的用法:
* TypeMirror asType() 返回此元素定义的类型 如int
* ElementKind getKind() 返回元素的类型 如 e.getkind() = ElementKind.FIELD 字段
* boolean equals(Object obj) 如果参数表示与此元素相同的元素,则返回true
* Name getSimpleName() 返回此元素的简单名称
* List<? extends Elements> getEncloseElements 返回元素直接封装的元素
* Element getEnclosingElements 返回此元素的最里层元素,如果这个元素是个字段等,则返回为类
*/

return false;
}

/**
* 指出注解处理器 处理哪种注解
* 在 jdk1.7 中,我们可以使用注解 {@SupportedAnnotationTypes()} 代替
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}

/**
* 指定当前注解器使用的Jdk版本。
* 在 jdk1.7 中,我们可以使用注解{@SupportedSourceVersion()}代替
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}

复制代码

自定义运行期注解(RUNTIME)


我们在开发中经常会需要计算一个方法所要执行的时间,以此来直观的比较哪个实现方式最优,常用方法是开始结束时间相减



System.currentTimeMillis()



但是当方法多的时候,是不是减来减去都要减的怀疑人生啦,哈哈,那么下面我就来写一个运行时注解来打印方法执行的时间。


1.首先我们先定义一个注解,并给注解添加我们需要的元注解:


/**
* 这是一个自定义的计算方法执行时间的注解。
* 只能作用于方法之上,属于运行时注解,能被VM处理,可以通过反射得到注解信息。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CalculateMethodRunningTime {

// 要计算时间的方法的名字
String methodName() default "no method to set";
}
复制代码

2.利用反射方法在程序运行时,获取被添加注解的类的信息:


public class AnnotationUtils {

// 使用反射通过类名获取类的相关信息。
public static void getClassInfo(String className) {
try {
Class c = Class.forName(className);
// 获取所有公共的方法
Method[] methods = c.getMethods();
for (Method m : methods) {
Class<CalculateMethodRunningTime> ctClass = CalculateMethodRunningTime.class;
if (m.isAnnotationPresent(ctClass)) {
CalculateMethodRunningTime anno = m.getAnnotation(ctClass);
// 当前方法包含查询时间的注解时
if (anno != null) {
final long beginTime = System.currentTimeMillis();
m.invoke(c.newInstance(), null);
final long time = System.currentTimeMillis() - beginTime;
Log.i("Tag", anno.methodName() + "方法执行所需要时间:" + time + "ms");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码

3.在 activity 中使用注解,注意咱们的注解是作用于方法之上的:


public class ActivityAnnotattion extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anno);
AnnotationUtils.getClassInfo("com.annotation.zmj.annotationtest.ActivityAnnotattion");
}

@CalculateMethodRunningTime(methodName = "method1")
public void method1() {
long i = 100000000L;
while (i > 0) { i--; }
}

}
复制代码

4.运行结果:


这里写图片描述



作者:矛盾的阿呆i
链接:https://juejin.cn/post/6936609673416015908
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(四),谈起泛型,大家都很佛性

当我们new了一个对象,会发生什么呢?来段代码: public class Tested { public static int T = 10; public int c = 1; } 复制代码 类初始化 在编译期,编译器会将 Tested.j...
继续阅读 »

当我们new了一个对象,会发生什么呢?来段代码:


public class Tested {
public static int T = 10;
public int c = 1;
}
复制代码

类初始化


在编译期,编译器会将 Tested.java类转换成 Tested.class 字节码文件。当虚拟机接收到new 字节码指令时,如果此时类还未被初始化,则虚拟机会先进行类的初始化过程。



在类加载完成后。虚拟机会为new Tested() 的Tested对象,在java堆中分配内存。而对象所需要的内存大小在类加载完成后就被确定了。


指针碰撞


如果 java 中的内存是规整的,即使用过的放在一边,空闲的在另一边,中间放着指针作为分界点的指示器。那所分配的内存就仅仅是将指针像空闲空间挪动一段与对象大小相等的距离。这种方式内称为指针碰撞。


空闲列表


如果 java 中的内存是不工整的,使用过的和空闲的内存相互交错,那么虚拟机就必须维护一个列表记录哪些内存是可用的。在分配的时候从列表中找到一块足够大的空间给对象示例,并更新表的记录。这种分配方式称为空闲列表。


对象初始化


当我们的对象内存被分配完毕后,虚拟机就会对对象进行初始化操作。



此时Tested 对象在我们眼里就算出生了,在虚拟机眼里就是真正可用的了。可对象的生命并不是无穷的,它也会经历自己的死亡。


可达性分析


在主流实现中,我们通过可达性分析来判断一个对象是否存活。实现思路是:通过一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始像下搜索,搜索所走的路径被称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。见图:


在这里插入图片描述


即使 Obj5 与 Obj4 由于与 GC Roots 没有引用链相连,所以我们称 GC Roots 到对象 Obj4 和 Obj5 不可达。所以 Obj4 和 Obj5 就是可回收的。


既然Obj4 和 Obj5 是可回收的,那么是否一定会被回收呢?不一定。此时虚拟机会进行第一次的标记过程。因为 java 内能够重写 finalize() 方法(在这里只是分析特例,不推荐在任何情况下使用此方法),当对象重写了此方法,并且 finalize() 方法还未被虚拟机调用,那么虚拟机就会将此对象放入一个专门的F-Queue队列,由一个单独的 Finalizer 线程去执行它,如果队列中对象的 finalize() 方法在虚拟机第二次标记之前执行,并在此次执行过程中又将自己与GC Roots 引用链相连,那么虚拟机在进行第二次标记时,就会将该对象从 F-Queue队列移除,否则就宣告该对象死亡。注意:finalize() 方法只会被执行一次,所以一个对象一生只有一次机会进入F-Queue队列,有机会逃脱本此死亡。


如果对象已经宣告死亡了,那么虚拟机怎么来回收它吗?


标记-清除算法


这是最基础的收集算法,主要分为标记和清除两个阶段。首先标记出所以需要回收的对象,在标记完成后统一回收所有被标记的对象。可以参考上面的空闲列表。其有两点不足:


a. 效率问题,标记和清除两个过程效率都不高。


b. 空间问题,因为堆中的内存不是规整的,已使用的和空闲的内存相互交错,这也就导致了每次GC回收后,产生大量的内存碎片,而当再次分配一个大对象时,如果无法找到足够的连续内存,又会再此触发GC回收。


复制算法


复制算法是将堆内存分成大小相等的两块,每次只使用其中一块,这样内存就是规整的了,参考指针碰撞。每当一块内存使用完了,就将该块内存中存活的对象复制到另一边,随后将该块内存一次清理掉。


现在的虚拟机都采用这种方式来回收新生代,只是并不是按照1:1的比例来划分内存,而是将内存分为一块较大的 Eden 空间,和两块较小的 Survivor 空间(HotSpot虚拟机默认Eden:Survivor = 8 :1)。每次只使用 Eden 和 其中一块 Survivor 空间,当回收时,将 Eden 空间和当前正使用的 Survivor 空间内存活的对象复制到另一块空闲的 Survivor空间,随后清空 Eden 和 刚才用过的 Survivor 内存。


注意:由于我们无法保证每次 存活的对象所占内存一直都不大于 Survivor 内存值,所以就会有溢出风险。所以在 分代收集算法 中,虚拟机会将内存先划分为一块新生代内存和一块为老年代内存。而在新生代内存中,会采用这种8:1:1的内存分配方式,如果溢出了,就将该情况下的存活对象全部放在老年代内存里,说白了就是一种兜底策略。这里要注意的是,不是溢出的那部分,而是全部的存活对象。


标记-整理算法


标记-整理算法中的标记过程,与标记-清除算法中的标记过程一样,不同的是,当标记完成并清理回收完对象后,会将当前不连续的碎片内存就行整理,即存活的对象都移到一端,来保证接下来要分配的内存的规整性。我们的 分代收集算法 中的老年代内存块,就是采用的该算法(当然也可以是标记-清除算法,不同虚拟机的策略不同)。所以就不再对分代收集算法就行赘述了。



好了,本文到这里,关于“对象”的生命周期的讲解就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考
1、周志明,深入理解JAVA虚拟机:机械工业出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6935481800365981727
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS-MBprogressHUD的使用

看开发文档中,涉及到六种基础的提示框typedef NS_ENUM(NSInteger, MBProgressHUDMode) { /**使用UIActivityIndicatorView显示进度。这是菊花默认值。 */ MBProgressHUDModeIn...
继续阅读 »

看开发文档中,涉及到六种基础的提示框

typedef NS_ENUM(NSInteger, MBProgressHUDMode) {
/**使用UIActivityIndicatorView显示进度。这是菊花默认值。 */
MBProgressHUDModeIndeterminate,
/** 使用圆形的饼图来显示进度。 */
MBProgressHUDModeDeterminate,
/** 使用水平进度条显示进度 */
MBProgressHUDModeDeterminateHorizontalBar,
/** 使用圆环进度视图显示进度。*/
MBProgressHUDModeAnnularDeterminate,
/** 自定义的view*/
MBProgressHUDModeCustomView,
/** 仅显示标签 */
MBProgressHUDModeText
};

使用函数

+ (void)showToast:(NSString *)title withView:(UIView *)view {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.mode = MBProgressHUDModeIndeterminate;
hud.labelText = title;
[hud hide:YES afterDelay:1];
}

运行例子:


//1,设置背景框的透明度  默认0.8
hud.opacity = 1;

//2,设置背景框的背景颜色和透明度, 设置背景颜色之后opacity属性的设置将会失效
hud.color = [UIColor redColor];
hud.color = [HUD.color colorWithAlphaComponent:1];

//3,设置背景框的圆角值,默认是10
hud.cornerRadius = 20.0;

//4,设置提示信息 信息颜色,字体
hud.labelColor = [UIColor blueColor];
hud.labelFont = [UIFont systemFontOfSize:13];
hud.labelText = @"Loading...";

//5,设置提示信息详情 详情颜色,字体
hud.detailsLabelColor = [UIColor blueColor];
hud.detailsLabelFont = [UIFont systemFontOfSize:13];
hud.detailsLabelText = @"LoadingLoading...";

//6,设置菊花颜色 只能设置菊花的颜色
hud.activityIndicatorColor = [UIColor blackColor];

//7,设置一个渐变层
hud.dimBackground = YES;

//9,设置提示框的相对于父视图中心点的便宜,正值 向右下偏移,负值左上
hud.xOffset = -80;
hud.yOffset = -100;

//10,设置各个元素距离矩形边框的距离
hud.margin = 0;

//11,背景框的最小大小
hud.minSize = CGSizeMake(50, 50);

//12设置背景框的实际大小 readonly
CGSize size = HUD.size;

//13,是否强制背景框宽高相等
hud.square = YES;


收起阅读 »

WKWebView 使用问题整理

一. WKWebView处理window.open问题WKWebView加载页面, 当页面使用window.open跳转时候, 无响应, 需要实现WKUIDelegate协议实现-(WKWebView *)webView:(WKWebView *)webVie...
继续阅读 »

一. WKWebView处理window.open问题

  • WKWebView加载页面, 当页面使用window.open跳转时候, 无响应, 需要实现WKUIDelegate协议实现

-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{

WKFrameInfo *frameInfo = navigationAction.targetFrame;
if (![frameInfo isMainFrame]) {
//1. 本页跳转
[webView loadRequest:navigationAction.request];

//2. 获取url 打开新的 vc 实现跳转到新页面
//NSString *urlStr = [[navigationAction.request URL] absoluteString];
}
return nil;
}

注意 :
1- 使用 window.open 在移动端可能引发兼容问题, 建议前端对移动端标签使用location.href处理
2- ajax 处理window.open时候, 同步时可以响应跳转, 异步时不会响应跳转

$.ajax({
url: '',
async: true,
complete: function (xhr) {
window.open("http://www.baidu.com");
}
});

二. WKWebView处理a标签问题

方案1: 不建议使用

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
// 将a标签 跳转方式全部改为本页
[webView evaluateJavaScript:@"var aArr = document.getElementsByTagName('a');for(var i=0;i}
方案2: WKNavigationDelegate协议实现

-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// webview 本页重新加载
if (navigationAction.targetFrame == nil) {
[webView loadRequest:navigationAction.request];
}
decisionHandler(WKNavigationActionPolicyAllow);
return;

}
方案3: WKUIDelegate协议实现

-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{

WKFrameInfo *frameInfo = navigationAction.targetFrame;
if (![frameInfo isMainFrame]) {

// 可创建新页面打开 [WebView new]
// 也可重新加载本页面 [webView loadRequest:navigationAction.request];

}
return nil;

}

注意 : 如果方案2与方案3 代码中均实现, 程序会先执行方案2

三. WKWebView处理alert 问题

  • WKWebView加载页面, 当页面使用alert()、confirm()和prompt(),默认无响应. 若要正常使用这三个方法,需要实现WKUIDelegate中的三个方法模拟JS的这三个方法

JS 处理实现方法
function showAlert() {
alert("js_alertMessage");
}

function showConfirm() {
confirm("js_confirmMessage");
}

function showPrompt() {
prompt("js_prompt", "js_prompt_defaultMessage");
}

App 处理
//! alert(message)
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
completionHandler();
}

//! confirm(message)
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
completionHandler();
}

//! prompt(prompt, defaultText)
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {
completionHandler();
}

注意: completionHandler();需要被执行, 不然会引发crash.

四. WKWebView与JS简单交互

  • -WKWebView加载页面, 当需要给js简单交互, 可如下处理

// JS 处理
document.getElementById("btn").onclick = function () {

var url = "APP://action?params";
window.location.href = url;
}

// App 处理
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{

if ([navigationAction.request.URL.scheme caseInsensitiveCompare:@"APP"] == NSOrderedSame) {
// 进行业务处理
decisionHandler(WKNavigationActionPolicyCancel);
}else{
if (navigationAction.targetFrame == nil) {
[webView loadRequest:navigationAction.request];
}
decisionHandler(WKNavigationActionPolicyAllow);
}
return;
}
// App 处理
NSString *func = [NSString stringWithFormat:@"loadData('%@', '%@')", @"aaa", @"bbb"];
[webView evaluateJavaScript:func completionHandler:nil];

// JS 处理
function loadData(action, params){

document.getElementById("returnValue").innerHTML = action + '?' + params;
}

注意:
1 webView调用 evaluateJavaScript:completionHandler:方法, 要确保前端的JS方法不在闭包中, 如window.onload = function() {} 中的方法就无法调用.
2 如果交互复杂 可以使用 WebViewJavascriptBridge 实现

五. WKWebView相关文档

WKWebView 那些坑

让 WKWebView 支持 NSURLProtocol

转自:https://www.jianshu.com/p/b9a88a537d87



收起阅读 »

iOS面试题(四)

1. OC 的消息机制消息机制可以分为三个部分1. 消息传递当我么调用方法的时候,方法的调用都会转化为objc_msgSend这样来传递。第一步会根据对象的isa指针找到所属的类(也就是类对象)第二步,会根据类对象里面的catch里面查找。catch是个散列表...
继续阅读 »

1. OC 的消息机制

消息机制可以分为三个部分

1. 消息传递

  • 当我么调用方法的时候,方法的调用都会转化为objc_msgSend这样来传递。

  • 第一步会根据对象的isa指针找到所属的类(也就是类对象)

  • 第二步,会根据类对象里面的catch里面查找。catch是个散列表,是根据@selector(方法名)来获取对应的IMP,从而开始调用

  • 第三步,如果第二步没有找到,会继续查找到类对象里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面

  • 第四步,如果第三部也没有找到,会根据类对象里面的superclass指针,查找super的catch,如果也是没有查找,会继续查找到superclass里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面

  • 第五步,如果第四部还是没有查找到,此时会根据类的superclass,继续第四部操作

.......

  • 第六步。如果一直查找到基类都没有找到响应的方法,则会进入动态解析里面

2. 动态解析

  • 当消息传递,没有找到对应的IMP的时候,会进入的动态解析中

  • 此时会根据方法是类方法,还是实例方法分别调用+(BOOL)resolveClassMethod:(SEL)sel、+(BOOL)resolveInstanceMethod:(SEL)sel

  • 我们可以实现这两个方法,使用Runtime的class_addMethod来添加对应的IMP

  • 如果添加后,返回true,没有添加则调用父类方法

  • 注意:其实返回true或者false,结果都是一样的,再次掉消息传递步骤

3. 消息转发

  • 如果我们没有实现动态解析方法,就会走到消息转发这里

  • 第一步,会调用-(id)forwardingTargetForSelector:(SEL)aSelector方法,我们可以在这里,返回一个响应aSelector的对象。当返回不为nil时候,系统会继续再次走消息转发,继续查找对应的IMP

  • 第二步,如果第一步返回nil或者self(自己),此时系统会继续走这里-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,需要返回aSelector的一个签名

  • 第三步,如果返回了签名,就会到这里-(void)forwardInvocation:(NSInvocation *)anInvocation,相应的我们可以根据anInvocation,可以获取到参数、target、方法名等,再次操作的空间就很多了,看你需求喽。此时我们什么都不操作也是没问题的,

  • 注意:当我们是类方法的时候,其实我们可以将以上方法的-改为+,即可实现了类方法的转发


2.weak表是如何存储__weak指针的

  • weak关键字,我们都知道,当对象销毁的时候,也会将指针赋值为nil,而weak的底层也是将指针和对象以键值对的形式存储在哈希表里面

  • 当使用__weak修饰的时候,底层会调用id objc_storeWeak(id *location, id newObj)传递两个参数

        第一个参数为指针,第二个参数为所指向的对象

  • 第二步,继续调用storeWeak(location, (objc_object *)newObj)

     1. 第一个参数是指针,第二个参数是对象的地址

     2. 再次方法里面会根据对象地址生成一个SideTables对象

  • 第三步,调用id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating)

     1. weak_table则为SideTables的一个属性,referent_id为对象,referrer_id则为那个弱引用的指针

     2. 在此里面会根据对象地址和指针生成一个weak_entry_t

  • 第四步,会继续调用static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)

     重点:在此方法里面会根据对象 & weak_table->mask(表示weak表里面可以存储的大小减一,例如:表可以存储10个对象,那么mask就是9), 生成对应的index,如果index对应已经存储上对象,则会index++的方式找到未存储的对应,并将new_entry存储进去,储存在weak_table里的weak_entries属                    性里面

  • 注意:当一个对象多个weak指针指向的时候,生成的也是一个entry,多个指针时保存在entry里面referrers属性里面

  • 以下为简易的源码:

id
objc_storeWeak(id *location, id newObj)
{
return storeWeak
(location, (objc_object *)newObj);
}
static id
storeWeak(id *location, objc_object *newObj) {
// 根据对象生成新的SideTable
SideTable *newTable = &SideTables()[newObj];
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
}
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating){
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

// 根据对象和指针生成一个entry
weak_entry_t new_entry(referent, referrer);
// 检查是是否该去扩容
weak_grow_maybe(weak_table);
// 将新的entry 插入到表里面
weak_entry_insert(weak_table, &new_entry);
}
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;

size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;
}
weak_entries[index] = *new_entry;
weak_table->num_entries++;
}

weak_table的扩容,根据存储条数 >= 最大存储条数的3/4时,就会按照两倍的方式进行扩容,并且会将已经有的条目再次生成新的index(因为扩容后,weak_table的mask发生了改变)。进行保存

  • 以下为简易的源码:

static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = (weak_table->mask ? weak_table->mask + 1 : 0);
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
size_t old_size = TABLE_SIZE(weak_table);
weak_entry_t *old_entries = weak_table->weak_entries;
// calloc 分配新的控件
weak_entry_t *new_entries = (weak_entry_t *)
calloc(new_size, sizeof(weak_entry_t));
// mask 就是大小减一
weak_table->mask = new_size - 1;
weak_entry_t *entry;
weak_entry_t *end = old_entries + old_size;
for (entry = old_entries; entry < end; entry++) {
if (entry->referent) {
weak_entry_insert(weak_table, entry);
}
}
}

3. 方法catch表是如何存储方法的

  • 我们都是知道调用方法的时候,会根据对象的isa查找到对象类对象,并开始在catch表里面查询对应的IMP

  • 其实catch是个散列表,是根据方法的@selector(方法名) & catch->mask(catck表最大数量 - 1)得到index,如果index已经存储了新的方法,那么就会index++,如果index对应的值为nil时,将响应的方法,插入到catch表里面

  • 核心代码

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) {
// 获取类对象的catch地址
cache_t *cache = &cls->cache
// 获取key
cache_key_t key = (cache_key_t)sel;
// 找到bucket
bucket_t *bucket = cache->find(key, receiver);
}

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
// catch表的buckets属性
bucket_t *b = buckets();
// catch 表示的mask 最大值 - 1
mask_t m = mask();

mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}

注意:catch表的扩容,同样也是和weak_table一样按照2倍的方式进行扩容,但是注意:扩容后,以前缓存的方法则会被删除掉。

简易代码

void cache_t::expand() {
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
reallocate(oldCapacity, newCapacity);
}

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
// 获取旧的oldBuckets
bucket_t *oldBuckets = buckets();
// 重新分配新的
bucket_t *newBuckets = allocateBuckets(newCapacity);
// free 掉旧的
cache_collect_free(oldBuckets, oldCapacity);
}

4. 优化后isa指针是什么样的?存储都有哪些内容?

  • 最新的Objective-C的对象里面的isa指针已经不是单单的指向所属类的地址了的指针了,而时变成了一个共用体,并且使用位域来存储更多的信息


5. App启动流程,以及如何优化?

  • 启动顺序

     1. dyld,Apple的动态连接器,可以用来装载Mach-O文件(可执行文件、动态库)

       1.1、装载App的可执行文件,同事递归加载所有依赖的动态库

       1.2 、当dyld把可执行文件、动态库装载完毕后,会通知Runtime进行下一步的处理

  • Runtime

     1. 调用map_images进行可执行文件内容的解析和处理

     2. 在load_images里面调用call_load_methods,调用所有class和category的+load方法

     3. 进行各种objc结构的初始化(注册Objc类,初始化类对象等等)

     4. 到目前未知,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP..)都已经按照格式成功加载到内存中,被runtime管理

  • main函数调用

     1. 所有初始化工作结束后,dyld就会调用main函数

     2. 截下来就是UIApplicationMan函数,AppDelegate的application:didFinishLaunchingWithOptions:的

  • App启动速度优化

      1. dyld

       1.1、减少动态库,合并一些自定义的动态库,以及定期清理一些不需要的动态库

       1.2、较少Objc类、category的数量、以及定期清理一些不必要的类和分类

       1.3、Swift尽量使用struct

     2. Runtime

       2.1、使用+initialize和dispatch_once取代Objc的+load方法、C++的静态构造器

     3. main

       3.1、再不印象用户体验的情况下面,尽可能的将一些操作延迟,不要全部放到finishLaunching

       3.2、一些网络请求

       3.3、一些第三方的注册

       3.4、以及window的rootViewController 的viewDidload方法,也别做耗时操作

     4. 注意:我们可以添加环境变量可以打印出App的启动时间分析(Edit scheme -> Run -> Arguments)

       4.1、DYLD_PRINT_STATISTICS设置为1,可以打印出来每个阶段的时间

       4.2、如果需要更详细的信息,那就设置DYLD_PRINT_STATISTICS_DETAILS为1


6. App瘦身

  • 资源(图片、音频、视频等)

    1. 可以采取无损压缩

     2. 使用LSUnusedResources去除没有用的资源 LSUnusedResources

  • 可执行文件瘦身

     1. Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为true

     2. 去掉一些异常支持 Enable C++ Exceptions、Enable Objective-C Exceptions设置为false

     3. 使用AppCode检测未使用的代码:菜单栏 -> Code -> Inspect Code,等编译完成后,会看到未使用的类

  • 生成LinkMap文件,可以查看可执行文件的具体组成

     1. 可借助第三方工具解析LinkMap文件LinkMap


     Link Map解析结果




收起阅读 »

iOS面试题(三)

1. ARC帮我们做了什么?使用LLVM + Runtime 结合帮我管理对象的生命周期LLVM 帮我们在代码合适的地方添加release、retarn、autorelease等添加计数器或者减少计数器操作Runtime 帮我们像__weak、copy等关键字...
继续阅读 »

1. ARC帮我们做了什么?

  • 使用LLVM + Runtime 结合帮我管理对象的生命周期

  • LLVM 帮我们在代码合适的地方添加release、retarn、autorelease等添加计数器或者减少计数器操作

  • Runtime 帮我们像__weak、copy等关键字的操作

2.initialize和load是如何调用的?它们会多次调用吗?

  • load方法说在应用加载的时候,Runtime直接拿到load的IMP直接去调用的,而不是像其他方式根据objc_msgSend(消息机制)来调用方法的

  • load方法调用的顺序是根据类的加载的前后进行调用的,但是每个类调用的顺序是superclass->class->category顺序调用的,每个load方法只会调用一次(手动调用不算)

  • 一下为Runtime源码的主要代码:

load_images(const char *path __unused, const struct mach_header *mh) {
// 准备classcategory
prepare_load_methods((const headerType *)mh);
// 调用load方法
call_load_methods();
}

void prepare_load_methods(const headerType *mhdr) {
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
add_category_to_loadable_list(cat);
}
}

static void schedule_class_load(Class cls) {
// 开始递归,加载superclass
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
}

void call_load_methods(void) {
do {
while (loadable_classes_used > 0) {
call_class_loads();
}
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
}

static void call_class_loads(void) {
// 在此add_class_to_loadable_list 里面准备了所有重写load的方法的类
struct loadable_class *classes = loadable_classes;
// Call all +loads for the detached list.
for ( int i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 获取到load 方法的imp
load_method_t load_method = (load_method_t)classes[i].method;
// 调用laod 方法
(*load_method)(cls, SEL_load);
}
}

static bool call_category_loads(void) {
// 在prepare_load_methods 方法里面准备了所有重新load方法的category
struct loadable_category *cats = loadable_categories;
for (int i = 0; i < used; i++) {
// 获取到catgegory
Category cat = cats[i].cat;
// 获取category 的load 方法的IMP实现
load_method_t load_method = (load_method_t)cats[i].method;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
// 调用load方法
(*load_method)(cls, SEL_load);
}
}
}
  • initialize方法的调用其实和其他方法调用一样的,objc_msgSend(消息机制)来调用的。调用的数序是:没有初始话的superclass -> 实现initialize的categort 或者 实现了initialize的class,如果class没有实现initialize 方法,则会调用superclass的initialize,因为initialize的底层是使用了objc_msgSend

  • 看下Runtime底层调用_class_initialize的源码

void _class_initialize(Class cls) {
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
// 又是个递归
_class_initialize(supercls);
}
// 调用 initialize方法
callInitialize(cls);
}
// objc_msgSend 调用 initialize 方法
void callInitialize(Class cls) {
// **注意:因为使用了objc_msgSend,有可能调用class的 initialize **
objc_msgSend(cls, SEL_initialize);
}

总结:
load方法一个类只会调用一次(除去手动调用),而调用的数序是,从superclass -> class -> category,category里面的顺序是先编译,先调用
initialize方法,一个类可能会调用多次,如果子类没有实现initialize方法,当第一次使用此类的时候,会调用superclass。而调用的顺序是,superclass -> 实现initialize的category 或者 实现了initialize方法(没有category实现initialize) 或者 superclass的initialize (没有子类和category实现initialize方法)

3.说下autoreleasepool

  • 在MRC下,当对象调用autorerelease方法时候,会将对象加入到对象前面的哪一个autoreleasepool里面,并且当autoreleasepool作用域释放的时候,会对里面的所有的对象进行一次release操作。

  • autoreleasepool底层是使用了AutoreleasePoolPage对象来管理的,AutoreleasePoolPage是一个双向的链表,每个AutoreleasePoolPage都有4096个字节,除了用来存放内部的成员变量,剩下的控件都会用来存放autorelease对象的地址

/// AutoreleasePoolPage 的简化的结构
class AutoreleasePoolPage {
magic_t const magic;
// 下一次可以存储对象的地址
id *next;
pthread_t const thread;
// 标识上一个page对象
AutoreleasePoolPage * const parent;
// 标识下一个page对昂
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
  • 当autoreleasepool开始的时候,会调用AutorelasePoolPage的push方法,会讲一个标识POOL_BOUNDARY添加到AutoreleasePoolPage对象里面,并且返回POOL_BOUNDARY的地址r1(暂且这样叫)

  • 当对像进行relase的时候,会将对象的地址添加到当前AutorelasePoolPage里面,依次添加。

  • 当autoreleasepool作用域结束的时候,会调用AutorelasePoolPage的pop(r1)方法(r1为当前aotoreleasepool开始的加入标识POOL_BOUNDARY的地址),AutorelasePoolPage则会将里面保存的对象的从左后一个开始进行release操作,当碰到r1时候,标识当前那个autoreleasepool里面所有的对象都进行了一次release操作。

@autoreleasepool {
// 此处会调用
void *ctxt = AutoreleasePoolPage::push();
// 添加到最近的一个autoreleasepool中
[[[NSObject alloc]init] autorelease];
//移除作用域的时候调用
AutoreleasePoolPage:pop(ctxt)
}
// autoreleasepool 作用域开始会调用AutoreleasePoolPage::push()
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {
// 创建一个心的page对象
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
// 已经有了page对象,讲`pool_boundary`添加进去
dest = autoreleaseFast(POOL_BOUNDARY);
}
}
static inline id *autoreleaseFast(id obj)
{
// 获取正在使用的page对昂
AutoreleasePoolPage *page = hotPage();
// page还没有装满
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
// 已经添加满了
return autoreleaseFullPage(obj, page);
} else {
// 没有page对象,创建心的page对象
return autoreleaseNoPage(obj);
}
}
// 对象调用release 的简介源码
id objc_object::rootAutorelease2() {
return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj) {
// 同样也是添加进去
id *dest = autoreleaseFast(obj);
return obj;
}
// page调用pop简介源码 *token 表示结束的标识
static inline void pop(void *token) {
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
page->releaseUntil(stop);
}
// 释放对象的源码
void releaseUntil(id *stop) {
// next 标识当前page可以存储对象的下一个地址
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
// 因为page是个双向链表,当page为空的时候,需要往上查找parent的page对象里面存储的睇相
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
id obj = *--page->next;
if (obj != POOL_BOUNDARY) {// obj 不是刚开始传入的POOL_BOUNDARY及表示对象,所以需要调用一次操作
objc_release(obj);
}
}
}


autoreleasepool和runloop的关系

  • runloop里面会注册两个Observer来监听runloop的状态变化

  • 其中一个Observer监听的状态为kCFRunLoopEntry进入runloop的状态,则会调用AutoreleasePoolPage::push()方法

  • 另外中一个Observer监听的状态为kCFRunLoopBeforeWaiting、kCFRunLoopExit,即将休眠和退出当前的runloop。

  • 在kCFRunLoopBeforeWaiting的回掉里面会调用AutoreleasePoolPage::pop(ctxt)和AutoreleasePoolPage::(push)方法,释放上一个autoreleasepool里面添加的对象,并开启下一个autoreleasepool。

  • 在kCFRunLoopExit的Observer回掉里面会调用AutoreleasePoolPage::(push)释放autoreleasepool里面的对象

4.category属性是存储在那里?

  • 我们都知道可以使用Runtime的objc_setAssociatedObject、objc_getAssociatedObject两个方法给category的属性重写get、set方法,而此属性的值是存储在那里呢?

  • 其实此属性的值保存在一个AssociationsManager里面。

  • 我们也是可以根据源码看一下

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// 一下为精简的代码
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
}
}
}


5.category方法是如何添加的?

  • 当我们给分类添加相同的方法的时候,会调用category里面的方法,而不是调用我们class里面的方法

  • 当编译器编译的时候,编译器会将category编译成category_t这样的结构体,等类初始化的时候,会将分类的信息同步到class_rw_t里面,包含:method、property、protocol等,同步的时候会将category里面的信息添加到class的前面(而不是替换掉class里面的方法),而方法调用的时候,而是遍历class_rw_t里面的方法,所以找到分类里面的IMP则返回。 

  • 使用memmove,将类方法移动到后面
  • 使用memcpy,将分类的方法copy到前面
  • 当多个分类有相同的方法的时候,调用的顺序是后编译先调用
  • 当类初始化同步category的时候,会使用while(i--)的倒序循环,将后编译的category添加到最前面。
         
收起阅读 »

react中的组件设计

react的组件设计有很多模式。下面列举几种常见的:完全受控组件这种组件设计的特点是,组件的所有state和action都来自props,组件自身没有状态,只负责展示UI层。model层完全交给全局状态管理库比如redux或mobx。不推荐这种组件设计,因为后...
继续阅读 »

react的组件设计有很多模式。下面列举几种常见的:

完全受控组件

这种组件设计的特点是,组件的所有state和action都来自props,组件自身没有状态,只负责展示UI层。model层完全交给全局状态管理库比如redux或mobx。不推荐这种组件设计,因为后期不好维护。这是典型的滥用全局状态管理库的现象。

什么叫滥用全局状态管理库?

就是没有认识到状态管理库的作用,或者说我们什么时候需要状态管理库?

拿 react 来说,react 是有组内状态的,状态可以通过 props 传递。但是,但当 app 比较庞大的时候,兄弟组件,远亲组件这些的交流就变得困难起来,
它们必须依赖相同的父组件来完成信息的传递。这时,就是我们使用状态管理库的时候。

但是,很多人把所有状态都往 redux 里面丢,虽然这方便了开发,但缺点却很明显:

  1. 组件很难复用:因为状态只有一份。
  2. 耦合度高:根据高内聚低耦合的设计原则,一个模块应该有独立的功能,不依赖外部,在内部实现复杂度,只暴露接口来与外界交流。但如果把组内的一些状态放在全局 model,就提供了让其他组件修改的能力,并且代码没有内聚。

非受控组件

划分好状态的等级,尽量把状态放在组件内。当遇到共享组内状态困难的场景时,提升状态到全局状态管理库。

这种组件,有view层、model层、services层。因为它是有独立功能的,然后通过向外界暴露api来提供自己的能力,同时把复杂度隐藏在内部。

例如一个列表组件:

// 方案一
// ListDemo.jsx
import React,{useEffect} from 'react';
import {getData} from 'services/api';

export default function ListDemo({requestId}){
// model
const [data,setData] = useState([]);
const [visible,setVisible] = useState(false);

useEffect(()=>{
// services 层
getData().then(data=>{
setData(data)
});
/**
* 当requestId变化时,列表会重新请求
* 这里的requestId是组件向外界暴露的一个api
**/
},[requestId])

useEffect(()=>{
if(visible===true){
// clearState
setVisible(false);
}
},[requestId])

return (
// view
<div>
{
data.map(item=><li>{item}</li>
}
{
visible && (
<div>
this is a modal
</div>
)
}
</div>
)
)
}

// app.jsx
<ListDemo />

这种组件设计的特点是,组件可以重置自身状态的时机是由自身控制的。如果你觉得这样麻烦,你可以把重置自身状态的时机交给外部,通过key来 “销毁组件”=>“重新渲染组件”。上面的代码可以简化成:

// 方案二
// ListDemo.jsx
import React,{useEffect} from 'react';
import {getData} from 'services/api';

export default function ListDemo({requestId}){
// model
const [data,setData] = useState([]);
const [visible,setVisible] = useState(false);

useEffect(()=>{
// services 层
getData().then(data=>{
setData(data)
});
},[])

return (
// view
<div>
{
data.map(item=><li>{item}</li>
}
{
visible && (
<div>
this is a modal
</div>
)
}
</div>
)
)
}

// app.jsx
/*
*当requestId变化时,ListDemo会重新渲染
*/
<ListDemo key={requestId} />

方案二的代码比较整洁,且出错率比方案一低,但是方案二存在重新渲染组件的一个环节,性能开支会比方案一多一点点(大部分情况你都可以忽略不计)。

很多情况下,我们应该采用方案二。

原文:https://zhuanlan.zhihu.com/p/88593781

收起阅读 »

如何用webpack优化moment.js的体积

本篇为转译,原出处。当你在代码中写了var moment = require('moment') 然后再用webpack打包, 打出来的包会比你想象中的大很多,因为打包结果包含了各地的local文件.解决方案是下面的两个webpack插件,任选其一:...
继续阅读 »

本篇为转译,原出处

当你在代码中写了var moment = require('moment') 然后再用webpack打包, 打出来的包会比你想象中的大很多,因为打包结果包含了各地的local文件.


解决方案是下面的两个webpack插件,任选其一:

  1. IgnorePlugin
  2. ContextReplacementPlugin

方案一:使用 IgnorePlugin插件

IgnorePlugin的原理是会移除moment的所有本地文件,因为我们很多时候在开发中根本不会使用到。 这个插件的使用方式如下:

const webpack = require('webpack');
module.exports = {
//...
plugins: [
// 忽略 moment.js的所有本地文件
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
};

那么你可能会有疑问,所有本地文件都被移除了,但我想要用其中的一个怎么办。不用担心,你依然可以在代码中这样使用:

const moment = require('moment');
require('moment/locale/ja');

moment.locale('ja');
...

这个方案被用在 create-react-app.

方案二:使用 ContextReplacementPlugin

这个方案其实跟方案一有点像。原理是我们告诉webpack我们会使用到哪个本地文件,具体做法是在插件项中这样添加ContextReplacementPlugin

const webpack = require('webpack');
module.exports = {
//...
plugins: [
// 只加载 `moment/locale/ja.js` 和 `moment/locale/it.js`
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /ja|it/),
],
};

值得注意的是,这样你就不需要在代码中再次引入本地文件了:


const moment = require('moment');
// 不需要
moment.locale('ja');
...

体积对比

对比条件:

  • webpack: v3.10.0
  • moment.js: v2.20.1

具体表现:


可见,处理后的体积小了很多。

原文链接:https://zhuanlan.zhihu.com/p/90748774

收起阅读 »

git 撤销对文件的追踪

Git
撤销暂存区(index)区的track当我们新增加文件时,使用git status会打印出:Untracked files: (use "git add ..." to include in what will be committed) ...
继续阅读 »

撤销暂存区(index)区的track

当我们新增加文件时,使用git status会打印出:

Untracked files:
(use "git add ..." to include in what will be committed)
hello.txt

nothing added to commit but untracked files present (use "git add" to track)

可见,git add 命令可以用来追踪文件。


当我们使用 git add hello.txt后,再使用git status后,会打印出:

Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: hello.txt

可见,文件已经被追踪了,只是还没提交到本地仓库。此时可以使用git restore来撤销这个追踪。

> git restore hello.txt --staged
> git status
On branch master
Your branch is up to date with 'origin/master'.

Untracked files:
(use "git add ..." to include in what will be committed)
hello.txt


撤销“已经提交到本地仓库的文件”的追踪

当一个文件(例如hello.txt)已经提交到本地仓库时。后续你再往.gitignore添加它,也不会起作用。怎么解除这种追踪呢?最常见的做法是直接删除这个文件,流程是:本地删除,提交删除这个commit到仓库。

但这样本地的也会被删除。有时我们只是想删除仓库的副本,可以使用git rm --cachedgit rm经常被用来删除工作区和暂存区的文件。它可以携带一个cache参数,作用如下(摘自文档):

git rm --cached
Use this option to unstage and remove paths only from the index. Working tree files, whether modified or not, will be left alone.
使用这个项来解除暂存区的缓存,工作区的文件将保持不动。

意思就是不会在实际上删掉这个文件,只是解除它的追踪关系。

举例:

> git rm --cached hello.txt
// rm 'hello.txt'
> git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
(use "git restore --staged ..." to unstage)
deleted: hello.txt

工作区的hello.txt还在,但已经没有被git追踪了。之后,只要我们把hello.txt添加到.gitignore后,修改hello.txt并不会产生改动。

接下来我们提交下这个改动。

git commit -m 'delete hello.txt'
[master d7a2e3e] delete hello.txt
1 files changed, 17 deletions(-)
delete mode 100644 hello.txt

使用rm这个命令时,我们经常会用到-r这个命令。-r是递归的意思,表示删除整个文件夹,包括它的子文件夹。


原文:https://zhuanlan.zhihu.com/p/139950341

收起阅读 »

web前端常见的三种manifest文件

manifest.jsonmanifest.json经常被用在PWA,用来 告知浏览器 关于PWA应用的一些信息如应用图标、启动应用的画面。举例:{ "short_name": "React App", "name": "Creat...
继续阅读 »

manifest.json

manifest.json经常被用在PWA,用来 告知浏览器 关于PWA应用的一些信息如应用图标、启动应用的画面。举例:

{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

assets-manifest.json

assets-manifest.json经常会在create-react-app这个脚手架的打包文件上看到,由webpack-manifest-plugin这个webpack插件产生。举例:

{
"files": {
"main.css": "/static/css/main.491bee12.chunk.css",
"main.js": "/static/js/main.14bfbead.chunk.js",
"main.js.map": "/static/js/main.14bfbead.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.e89362ac.js",
"runtime-main.js.map": "/static/js/runtime-main.e89362ac.js.map",
"static/js/2.017bb613.chunk.js": "/static/js/2.017bb613.chunk.js",
"static/js/2.017bb613.chunk.js.map": "/static/js/2.017bb613.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.33b41575e0c64a21bca1a6091e8a5c6d.js": "/precache-manifest.33b41575e0c64a21bca1a6091e8a5c6d.js",
"service-worker.js": "/service-worker.js",
"static/css/main.491bee12.chunk.css.map": "/static/css/main.491bee12.chunk.css.map",
"static/media/logo.svg": "/static/media/logo.25bf045c.svg"
},
"entrypoints": [
"static/js/runtime-main.e89362ac.js",
"static/js/2.017bb613.chunk.js",
"static/css/main.491bee12.chunk.css",
"static/js/main.14bfbead.chunk.js"
]
}

wepack-mainfest-plugin对它自身的介绍是:

This will generate amanifest.jsonfile in your root output directory with a mapping of all source file names to their corresponding output file。

意思就是assets-manifest.json其实只是源文件和加哈希后文件的一个对比表,仅此而已。它不会对应用的运行产生任何影响,浏览器也不会去请求它。


precache-manifest.js

这个文件由workbox-webpack-plugin插件生成, 用来告诉workbox哪些静态文件可以缓存。例如:

/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

其中self.__precacheManifest的值就是precache-manifest.js的内容。

 原文链接:https://zhuanlan.zhihu.com/p/90829472

收起阅读 »

iOS-一些常用第三方资源

一:第三方插件1:基于响应式编程思想的oc地址:https://github.com/ReactiveCocoa/ReactiveCocoa2:hud提示框地址:https://github.com/jdg/MBProgressHUD3:XML/HTML解析地...
继续阅读 »

一:第三方插件

1:基于响应式编程思想的oc

地址:https://github.com/ReactiveCocoa/ReactiveCocoa

2:hud提示框

地址:https://github.com/jdg/MBProgressHUD

3:XML/HTML解析

地址:https://github.com/topfunky/hpple

4:有文字输入时,能根据键盘是否弹出来调整自身显示内容的位置

地址:https://github.com/michaeltyson/TPKeyboardAvoiding

5:状态栏提示框

地址:https://github.com/jaydee3/JDStatusBarNotification

6:block工具包。将很多需要用delegate实现的方法整合成了block的形式

地址:https://github.com/zwaldowski/BlocksKit

7:图片加载

地址:https://github.com/rs/SDWebImage

8:正则表达式

地址:https://github.com/wezm/RegexKitLite

9:Masonry代码布局

地址:https://github.com/SnapKit/Masonry

10:弹出窗

地址:https://github.com/sberrevoets/SDCAlertView

11:Button的样式

地址:https://github.com/mattlawer/BButton

12:验证网络连接状态

地址:https://github.com/tonymillion/Reachability

13:自动计算表格行高

地址:https://github.com/forkingdog/UITableView-FDTemplateLayoutCell

14:动画效果的启动页

地址:https://github.com/IFTTT/JazzHands

15:iOS快速简单集成国内三大平台分享

地址:https://github.com/xumeng/XMShareModule

16:五项能力值展示的五边形

地址:https://github.com/dsxNiubility/SXFiveScoreShow

17:自动识别网址号码邮箱和表情的label

地址:https://github.com/molon/MLEmojiLabel

18:IM对话功能的封装

地址:https://github.com/ZhipingYang/UUChatTableView

19:字典转模型框架

地址:https://github.com/CoderMJLee/MJExtension

20:下拉上拉刷数据

地址:https://github.com/CoderMJLee/MJRefresh

21:表格行左右划动菜单

地址:https://github.com/MortimerGoro/MGSwipeTableCell

22:图文混搭

地址:https://github.com/zhouande/TLAttributedLabel

23:可以简单展示在UINavigationBar下方,类似Music app的播放列表视图,弹出菜单视图

地址:https://github.com/DrummerB/BFNavigationBarDrawer

24:比如筛选、模糊、优化、蒙版、调整大小、旋转以及保存等等。同时还提供了一个UIImageView子类从URL异步加载图片,并在下载完毕时展示图片。

地址:https://github.com/Nyx0uf/NYXImagesKit

25:底部TabBar

地址:https://github.com/robbdimitrov/RDVTabBarController

26:表情面版

地址:https://github.com/ayushgoel/AGEmojiKeyboard

27:记录框架

地址:https://github.com/CocoaLumberjack/CocoaLumberjack

28:IOS与javascript交互

地址:https://github.com/marcuswestin/WebViewJavascriptBridge

29:图表统计展示

地址:https://github.com/kevinzhow/PNChart

30:appStore评分

地址:https://github.com/arashpayan/appirater

31:iOS-Categories 扩展类大全

地址:https://github.com/shaojiankui/IOS-Categories

32:扫描二维码,仿微信效果,带有扫描条

地址:https://github.com/JxbSir/JxbScanQR

33:动效弹出视图(弹出窗里面为文字,可以定义弹出的方向,及显示的时间)--AMPopTip

地址:https://github.com/andreamazz/AMPopTip

34:基于Masonry自动计算行高扩展

地址:https://github.com/632840804/HYBMasonryAutoCellHeight

 35:模仿新浪微博弹出菜单

地址:https://github.com/wwdc14/HyPopMenuView

 36:搜索历史标签

地址:https://github.com/zhiwupei/SearchHistory

 37:快速集成新手引导的类库

地址:https://github.com/StrongX/XSportLight

38:设置页面的封装

地址:https://github.com/renzifeng/ZFSetting

39:带箭头的弹出视图插件

地址:https://github.com/xiekw2010/DXPopover

40:下拉菜单插件

地址:https://github.com/dopcn/DOPDropDownMenu/

41:表格空白提示插件

地址:https://github.com/dzenbot/DZNEmptyDataSet

42:给任意UIView视图四条边框加上阴影,可以自定义阴影的颜色、粗细程度、透明程度以及位置(上下左右边框)

地址:https://github.com/Seitk/UIView-Shadow-Maker

43:不错的日期时间插件

地址:https://github.com/CoderXL/UUDatePicker

44:底部弹出选择

地址:https://github.com/skywinder/ActionSheetPicker-3.0

45:比较不错的引导页面插件

地址:https://github.com/ealeksandrov/EAIntroView

46:两个APP跳转的插件

地址:https://github.com/usebutton/DeepLinkKit

47:本地存取NSUserDefaults插件

地址:https://github.com/gangverk/GVUserDefaults

48:NSArray 和 NSDictionary关于LINQ的操作方式,封装一些常用的操作

地址:https://github.com/ColinEberhardt/LinqToObjectiveC

49:可以监控网络请求的内容

地址:https://github.com/coderyi/NetworkEye

50:时间帮助插件,可以快速获取时间,比较,增加等操作

地址:https://github.com/MatthewYork/DateTools

51: 不错的链式动作

地址:https://github.com/jhurray/JHChainableAnimations

52:弹出层视图,背景效果(可以自定义视图的内容)

地址:https://github.com/HJaycee/JCAlertView

53:圆形进度条的显示,中间可显示值

地址:https://github.com/mdinacci/MDRadialProgress

54:很帅的数据加载动画(可以用于数据列表加载的展现)

地址:https://github.com/NghiaTranUIT/FeSpinner 

55:一个开源的AFnetworking上层的封装(猿题库等运用)

地址:https://github.com/yuantiku/YTKNetwork

56:CBStoreHouseRefreshControl:一个效果很酷炫的下拉刷新控件

地址:https://github.com/coolbeet/CBStoreHouseRefreshControl

57:AFNetworking-RACExtensions:针对ReactiveCocoa的AF封装

地址:https://github.com/CodaFi/AFNetworking-RACExtensions

58:模糊效果(毛玻璃)

地址:https://github.com/nicklockwood/FXBlurView

二:源代码实例

1:Coding.net客户端

地址:https://coding.net/u/coding/p/Coding-iOS/git

2:高仿美团iOS版

地址:https://github.com/lookingstars/meituan

3:模仿网易新闻做的精仿网易新闻

地址:https://github.com/dsxNiubility/SXNews

4:支付宝高仿版

地址:https://github.com/gsdios/GSD_ZHIFUBAO

5:高仿百度传课iOS版

地址:https://github.com/lookingstars/chuanke

6:模仿一元云购

地址:https://github.com/JxbSir/YiYuanYunGou

7:wordpress源代码

地址:https://github.com/wordpress-mobile/WordPress-iOS

8:v2ex源代码(文章类型,若报SVProgressHUD错,则把Podfile中的SVProgressHUD移除)

地址:https://github.com/singro/v2ex

9:PHPHub客户端(IOS8.0以上)

地址:https://github.com/Aufree/phphub-ios

10:快速搭建项目源代码

地址:https://github.com/wujunyang/MobileProject

三:辅助软件

1:XCODE文档注解插件VVDocumenter

地址:https://github.com/onevcat/VVDocumenter-Xcode

2:将JSON格式化输出为模型的属性

地址:https://github.com/EnjoySR/ESJsonFormat-Xcode

3:图片提示插件

地址:https://github.com/ksuther/KSImageNamed-Xcode

4:图片转换插件

地址:https://github.com/rickytan/RTImageAssets


收起阅读 »

ios-本地存储的五种方式

ios数据存储的5种方式NSUserDefaults(Preference偏好设置)plist存储归档SQLite3CoreData应用沙盒Document:适合存储重要的数据, iTunes同步应用时会同步该文件下的内容,(比如游戏中的存档)Library/...
继续阅读 »

ios数据存储的5种方式

  1. NSUserDefaults(Preference偏好设置)
  2. plist存储
  3. 归档
  4. SQLite3
  5. CoreData

应用沙盒

Document:适合存储重要的数据, iTunes同步应用时会同步该文件下的内容,(比如游戏中的存档)
Library/Caches:适合存储体积大,不需要备份的非重要数据,iTunes不会同步该文件
Library/Preferences:通常保存应用的设置信息, iTunes会同步
tmp:保存应用的临时文件,用完就删除,系统可能在应用没在运行时删除该目录下的文件,iTunes不会同步

获取沙盒路径

Document:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject;

NSuserDefault

NSuserDefault适合存储轻量级的本地数据,支持的数据类型有:NSNumber,NSString,NSDate,NSArray,NSDictionary,BOOL,NSData

沙盒路径为 Library/Preferences
文件格式为 .plist

优点:

  1. 不需要关心文件名
  2. 快速进行键值对存储
  3. 直接存储基本数据类型

缺点:

  1. 不能存储自定义数据
  2. 取出的数据都是不可变的
- (IBAction)userDefaultSave:(id)sender {
NSArray *testArray = @[@"test1", @"test2", @"test3"];
[[NSUserDefaults standardUserDefaults] setObject:testArray forKey:@"arrayKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (IBAction)userDefaultLoad:(id)sender {
NSArray *testArray = [[NSUserDefaults standardUserDefaults] objectForKey:@"arrayKey"];
NSLog(@"%@", testArray);
}

plist存储

plist支持的数据类型:

NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;
不支持BOOL
而且最外层好像要用`NSArray 或 NSDictionary,偷个懒还没验证

- (IBAction)plistSave:(id)sender {
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [cachePath stringByAppendingPathComponent:@"testPlist.plist"];

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@"ran" forKey:@"name"];
[dict setObject:@"18" forKey:@"age"];
[dict writeToFile:filePath atomically:YES];
}

- (IBAction)plistLoad:(id)sender {
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [cachePath stringByAppendingPathComponent:@"testPlist.plist"];

NSDictionary *t = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(@"%@",t);
}

归档

存储自定义对象

  1. 首先新建Person类,并遵守NSCoding协议
@interface Person : NSObject<NSCoding>

@property(nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSString *age;

@end

实现协议方法:

@implementation Person

- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
_name = [coder decodeObjectForKey:@"name"];
_age = [coder decodeObjectForKey:@"age"];
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{

[coder encodeObject:self.name forKey:@"name"];
[coder encodeObject:self.age forKey:@"age"];

}
@end

归档解档

- (IBAction)archive:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];

Person *p1 = [[Person alloc] init];
p1.name = @"ran";
p1.age = @"18";

[NSKeyedArchiver archiveRootObject:p1 toFile:filePath];
}

- (IBAction)unarchive:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];

Person *p1 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath] ;

NSLog(@"%@", p1.name);
NSLog(@"%@", p1.age);
}

但是这种方法只能存储一个对象,存储多个对象要采用如下的方法:

- (IBAction)archiveManyObject:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];

NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; //将数据区连接到NSKeyedArchiver对象

Person *p1 = [[Person alloc] init];
p1.name = @"ran1";
p1.age = @"18";
[archiver encodeObject:p1 forKey:@"person1"];

Person *p2 = [[Person alloc] init];
p2.name = @"ran2";
p2.age = @"19";
[archiver encodeObject:p2 forKey:@"person2"];

[archiver finishEncoding];

[data writeToFile:filePath atomically:YES];
}

- (IBAction)unarchiveManyObject:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];
NSData *data = [NSData dataWithContentsOfFile:filePath];

NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *p1 = [unarchiver decodeObjectForKey:@"person1"];
Person *p2 = [unarchiver decodeObjectForKey:@"person2"];
[unarchiver finishDecoding];

NSLog(@"%@", p1.name);
NSLog(@"%@", p2.name);
}

SQLite3

数据库(splite):
splite是一个轻量级,跨平台的小型数据库,可移植性比较高,有着和MySpl几乎相同的数据库语句,以及无需服务器即可使用的优点:

数据库的优点:

  1. 该方案可以存储大量的数据,存储和检索的速度非常快.
  2. 能对数据进行大量的聚合,这样比起使用对象来讲操作要快.

数据库的缺点:

  1. 它没有提供数据库的创建方式
  2. 它的底层是基于C语言框架设计的, 没有面向对象的API, 用起来非常麻烦
  3. 发杂的数据模型的数据建表,非常麻烦
    在实际开发中我们都是使用的是FMDB第三方开源的数据库,该数据库是基于splite封装的面向对象的框架.
#import "SqliteVC.h"
#import "Person.h"
@interface SqliteVC() {

sqlite3 *_db;

}
@end

@implementation SqliteVC

- (void)viewDidLoad {
[super viewDidLoad];

NSString *fileName = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"student.sqlite"];
NSLog(@"fileName = %@",fileName);

int result = sqlite3_open(fileName.UTF8String, &_db); //创建(打开)数据库,如果数据库不存在,会自动创建 数据库文件的路径必须以C字符串(而非NSString)传入

if (result == SQLITE_OK) {
NSLog(@"成功打开数据库");

char *errorMesg = NULL;
const char *sql = "create table if not exists t_person (id integer primary key autoincrement, name text, age integer);";
int result = sqlite3_exec(_db, sql, NULL, NULL, &errorMesg); //sqlite3_exec()可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据

if (result == SQLITE_OK) {
NSLog(@"成功创建t_person表");
} else {
NSLog(@"创建t_person表失败:%s",errorMesg);
}

} else {
NSLog(@"打开数据库失败");
}
}
- (IBAction)insert:(id)sender {
for (int i = 0; i < 30; i++) {

NSString *name = [NSString stringWithFormat:@"person-%d",arc4random()0];
int age = arc4random() % 100;

char *errorMesg = NULL;
NSString *sql = [NSString stringWithFormat:@"insert into t_person (name,age) values ('%@',%d);",name, age];
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errorMesg);

if (result == SQLITE_OK) {
NSLog(@"添加数据成功");
} else {
NSLog(@"添加数据失败");
}
}
}

- (IBAction)delete:(id)sender {
char *errorMesg = NULL;
NSString *sql = @"delete from t_person where age >= 0";
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errorMesg);

if (result == SQLITE_OK) {
NSLog(@"删除成功");
}else {
NSLog(@"删除失败");
}
}

- (IBAction)query:(id)sender {
const char *sql = "select id, name, age from t_person;"; //"select id, name, age from t_person where age >= 50;"
sqlite3_stmt *stmt = NULL; //定义一个stmt存放结果集
int result = sqlite3_prepare_v2(_db, sql, -1, &stmt, NULL); //检测SQL语句的合法性

if (result == SQLITE_OK) {
NSLog(@"查询语句合法");

while (sqlite3_step(stmt) == SQLITE_ROW) {

int ID = sqlite3_column_int(stmt, 0);
const unsigned char *sname = sqlite3_column_text(stmt, 1);
NSString *name = [NSString stringWithUTF8String:(const char *)sname];
int age = sqlite3_column_int(stmt, 2);

NSLog(@"%d %@ %d",ID, name, age);
}
} else {
NSLog(@"查询语句非法");
}
}

- (IBAction)update:(id)sender {
NSString *sql = @"update t_person set name = '哈哈' where age > 60";
char *errorMesg = NULL;
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errorMesg);

if (result == SQLITE_OK) {
NSLog(@"更改成功");
}else {

NSLog(@"更改失败");
}
}

coreData

coreData是苹果官方在iOS5之后推出的综合性数据库,其使用了对象关系映射技术,将对象转换成数据,将数据存储在本地的数据库中
coreData为了提高效率,需要将数据存储在不同的数据库中,比如:在使用的时候,最好是将本地的数据保存到内存中,这样的目的是访问速度比较快.

CoreData与SQLite进行对比

SQLite
1、基于C接口,需要使用SQL语句,代码繁琐
2、在处理大量数据时,表关系更直观
3、在OC中不是可视化,不易理解


CoreData
1、可视化,且具有undo/redo能力
2、可以实现多种文件格式:
* NSSQLiteStoreType
* NSBinaryStoreType
* NSInMemoryStoreType
* NSXMLStoreTyp
3、苹果官方API支持,与iOS结合更紧密

CoreData核心类与结构

NSManagedObjectContext(数据上下文)

  • 对象管理上下文,负责数据的实际操作(重要)
  • 作用:插入数据,查询数据,删除数据,更新数据

NSPersistentStoreCoordinator(持久化存储助理)

  • 相当于数据库的连接器
  • 作用:设置数据存储的名字,位置,存储方式,和存储时机

NSManagedObjectModel(数据模型)

  • 数据库所有表格或数据结构,包含各实体的定义信息
  • 作用:添加实体的属性,建立属性之间的关系
  • 操作方法:视图编辑器,或代码

NSManagedObject(被管理的数据记录)

  • 数据库中的表格记录

NSEntityDescription(实体结构)

  • 相当于表格结构

NSFetchRequest(数据请求)

  • 相当于查询语句

后缀为.xcdatamodeld的包

  • 里面是.xcdatamodel文件,用数据模型编辑器编辑
  • 编译后为.momd或.mom文件

类关系图

开始创建coredata

步骤:
1.创建模型文件 [相当于一个数据库]
2.添加实体 [一张表]
3.创建实体类 [相当模型--表结构]
4.生成上下文 关联模型文件生成数据库

1.创建模型文件

New File -> iOS -> Core Data ->Data Model

2.创建实体

Codegen

3.创建实体类

创建结果如图所示:

1.生成上下文 关联模型文件生成数据库,进行增删查改操作

#import "coredataVC.h"
#import <CoreData/CoreData.h>
#import "Student+CoreDataProperties.h"

@interface coredataVC ()

@property(nonatomic, strong)NSManagedObjectContext *context;

@end

@implementation coredataVC

- (void)viewDidLoad {
[super viewDidLoad];

//entity 记得勾选 language:objective-c 和 codegen:manual/none
[self createSql];
}

- (void)createSql {
//获取模型路径
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Person" withExtension:@"momd"];
//根据模型文件创建模型对象
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

//利用模型对象创建持久化存储助理
NSPersistentStoreCoordinator *store = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];

//数据库的名称和路径
NSString *docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *sqlPath = [docStr stringByAppendingPathComponent:@"coreData.sqlite"];
NSURL *sqlUrl = [NSURL fileURLWithPath:sqlPath];
NSLog(@"数据库 path = %@", sqlPath);

NSError *error = nil; //设置数据库相关信息 添加一个持久化存储库并设置类型和路径,NSSQLiteStoreType:SQLite作为存储库
[store addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sqlUrl options:nil error:&error];

if (error) {
NSLog(@"添加数据库失败:%@",error);
} else {
NSLog(@"添加数据库成功");
}

//3、创建上下文 保存信息 对数据库进行操作 关联持久化助理
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.persistentStoreCoordinator = store;
_context = context;
}


- (IBAction)insertClick:(id)sender {
Student * student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:_context];
student.name = [NSString stringWithFormat:@"stu-%d",arc4random()0];
student.age = arc4random()%30;

NSError *error = nil;
if ([_context save:&error]) {
NSLog(@"数据插入到数据库成功");
}else{
NSLog(@"数据插入到数据库失败");
}
}


- (IBAction)deleteClick:(id)sender {
//创建删除请求
NSFetchRequest *deleRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"];

//删除条件 没有任何条件就是读取所有的数据
//NSPredicate *pre = [NSPredicate predicateWithFormat:@"age < %d", 10];
//deleRequest.predicate = pre;

//返回需要删除的对象数组
NSArray *deleArray = [_context executeFetchRequest:deleRequest error:nil];

//从数据库中删除
for (Student *stu in deleArray) {
[_context deleteObject:stu];
}

NSError *error = nil;
if ([_context save:&error]) {
NSLog(@"删除数据成功");
}else{
NSLog(@"删除数据失败, %@", error);
}
}


- (IBAction)queryClick:(id)sender {
//创建查询请求
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];

//查询条件 没有任何条件就是读取所有的数据
NSPredicate *pre = [NSPredicate predicateWithFormat:@"age >= 0"];
request.predicate = pre;

// 从第几页开始显示 通过这个属性实现分页
//request.fetchOffset = 0;
// 每页显示多少条数据
//request.fetchLimit = 6;

//发送查询请求
NSArray *resArray = [_context executeFetchRequest:request error:nil];

//打印查询结果
for (Student *stu in resArray) {
NSLog(@"name=%@, age=%d",stu.name, stu.age);
}
}


- (IBAction)updateClick:(id)sender {
//创建查询请求
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];

NSPredicate *pre = [NSPredicate predicateWithFormat:@"age >= 0"];
request.predicate = pre;

//发送请求
NSArray *resArray = [_context executeFetchRequest:request error:nil];

//修改
for (Student *stu in resArray) {
stu.name = @"ran";
}

NSError *error = nil;
if ([_context save:&error]) {
NSLog(@"更新数据成功");
}else{
NSLog(@"更新数据失败, %@", error);
}
}

@end


转自:https://blog.csdn.net/u013712343/article/details/106698848

收起阅读 »

iOS 显示动态图、GIF图方法总结

一、WebView加载可以通过WebView加载本地Gif图和网络Gif图,但图片大小不能自适应控件大小,也不能设置Gif图播放时间。使用如下:// 1、WebView加载- (void)webViewShowGif { UIWebView *webVi...
继续阅读 »

一、WebView加载

可以通过WebView加载本地Gif图和网络Gif图,但图片大小不能自适应控件大小,也不能设置Gif图播放时间。使用如下:

// 1、WebView加载
- (void)webViewShowGif {
UIWebView *webView = self.viewArr[0];

// 本地地址
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"gif"];
// 网路地址
// NSString *imagePath = @"http://qq.yh31.com/tp/zjbq/201711092144541829.gif";

NSURL *imageUrl = [NSURL URLWithString:imagePath];
NSURLRequest *request = [NSURLRequest requestWithURL:imageUrl];
[webView loadRequest:request];
}

二、UIImageView加载多图动画

把动态图拆分成一张张图片,将一系列帧添加到animationImages数组里面,然后设置animation一系列属性,如动画时间,动画重复次数。例:

// 2、UIImageView加载多张图片,播放
- (void)imageViewStartAnimating {
UIImageView *imageView = self.viewArr[1];

NSMutableArray *imageArr = [NSMutableArray arrayWithCapacity:3];
for (int i = 0; i<3; i++) {
NSString *imageStr = [NSString stringWithFormat:@"import_progress%d",i + 1];
UIImage *image = [UIImage imageNamed:imageStr];
[imageArr addObject:image];
}
imageView.animationImages = imageArr;
imageView.animationDuration = 2;
[imageView startAnimating];
}

三、SDWebImage加载本地GIF

在SDWebImage这个库里有一个UIImage+GIF的类别,使用sd_animatedGIFWithData方法可以将GIF图片数据专为图片。例:

// 3、SDWebImage加载本地GIF
- (void)imageViewLocalGif {
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"happy" ofType:@"gif"];
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
UIImage *image = [UIImage sd_animatedGIFWithData:imageData];

UIImageView *imageView = self.viewArr[2];
imageView.image = image;
}

四、SDWebImage加载网络GIF

首先将网络gif图下载到本地,然后再用sd_animatedGIFWithData方法,转为可用的图片,下载gif图的方式有两种

方式一:采用SDWebImageDownloader下载,回调里面会有NSData。只是,你会发现采用SDWebImageDownloader下载,界面显示就是没有sd_setImageWithURL方法流畅,这是因为sd_setImageWithURL里面对cache和线程做了很多处理,保证了UI的流畅。

NSString *imageStr = @"http://qq.yh31.com/tp/zjbq/201711142021166458.gif";
NSURL *imgeUrl = [NSURL URLWithString:imageStr];
SDWebImageDownloaderOptions options = 0;
UIImageView *imageView = self.viewArr[3];

// 方法一 SDWebImageDownloader下载
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imgeUrl
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {

} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
imageView.image = [UIImage sd_animatedGIFWithData:data];
}];

方式二、sd_setImageWithURL下载,回调的时候不用image,去直接读cache。(首先要了解sd_setImageWithURL里的内部逻辑,下载完之后先入cache,再执行block,这才保证外面可以直接读取到),取出来的就是NSData。首次下载成功时,可能获取data失败,因为这次图片可能还没存储成功,有延迟。

// 方法二 sd_setImageWithURL下载
SDWebImageOptions opt = SDWebImageRetryFailed | SDWebImageAvoidAutoSetImage;
[imageView sd_setImageWithURL:imgeUrl
placeholderImage:nil
options:opt
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {

if (image.images && image.images.count) {
NSString *path = [[SDImageCache sharedImageCache] defaultCachePathForKey:imageURL.absoluteString];
NSData *data = [NSData dataWithContentsOfFile:path];
UIImage *gifImage = [UIImage sd_animatedGIFWithData:data];
imageView.image = gifImage;
}
}];

五、FLAnimatedImage使用  

FLAnimatedImage 是由Flipboard开源的iOS平台上播放GIF动画的一个优秀解决方案,在内存占用和播放体验都有不错的表现。FLAnimatedImage项目的流程比较简单,FLAnimatedImage就是负责GIF数据的处理,然后提供给FLAnimatedImageView一个UIImage对象。FLAnimatedImageView拿到UIImage对象显示出来就可以了。 例:

// 5、FLAnimatedImage使用
- (void)animatedImageViewShowGif {
FLAnimatedImageView *imageView = self.viewArr[4];

NSURL *url = [[NSBundle mainBundle] URLForResource:@"weiwei" withExtension:@"gif"];
NSData *data = [NSData dataWithContentsOfURL:url];
FLAnimatedImage *animatedImage = [FLAnimatedImage animatedImageWithGIFData:data];
imageView.animatedImage = animatedImage;
}

六、YYImage使用

1.显示本地gif 

//load loacle gif image
- (void)loadLocaleGifImage{
//yyImage show gif image
[self labelFactoryWithFrame:CGRectMake(0, kScreenHeight/2 - 20, kScreenWidth, 20) title:@"yyImage"];
YYImage *yyimage = [YYImage imageNamed:@"test.gif"];
YYAnimatedImageView *yyImageView = [[YYAnimatedImageView alloc] initWithImage:yyimage];
yyImageView.frame = CGRectMake(0, kScreenHeight/2, kScreenWidth, kScreenHeight/3);
[self.view addSubview:yyImageView];
}

 2.加载网络gif图

//download network gif image
- (void)downloadNetworkGifImage{

//yyImage show gif image
[self labelFactoryWithFrame:CGRectMake(0, kScreenHeight/2 - 20, kScreenWidth, 20) title:@"yyImage"];
YYImage *yyimage = [YYImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://photocdn.sohu.com/20151214/mp48444247_1450092561460_10.gif"]]];
YYAnimatedImageView *yyImageView = [[YYAnimatedImageView alloc] initWithImage:yyimage];
yyImageView.frame = CGRectMake(0, kScreenHeight/2, kScreenWidth, kScreenHeight/3);
[self.view addSubview:yyImageView];
}

- (void)labelFactoryWithFrame:(CGRect)frame title:(NSString *)title{

UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.textAlignment = NSTextAlignmentCenter;
label.textColor = [UIColor blackColor];
label.font = [UIFont systemFontOfSize:14];
label.text = title;
[self.view addSubview:label];
}


收起阅读 »

iOS -Masonry详解

现在iPhone手机屏幕越来越多, 屏幕适配也越来越重要. Masonry就是为屏幕适配而生的三方框架.Masonry基础APImas_makeConstraints() 添加约束mas_remakeConstraints() 移除之前的约束,重新添加...
继续阅读 »

现在iPhone手机屏幕越来越多, 屏幕适配也越来越重要. Masonry就是为屏幕适配而生的三方框架.

Masonry基础API

mas_makeConstraints()    添加约束
mas_remakeConstraints() 移除之前的约束,重新添加新的约束
mas_updateConstraints() 更新约束,写哪条更新哪条,其他约束不变

equalTo() 参数是对象类型,一般是视图对象或者mas_width这样的坐标系对象
mas_equalTo() 和上面功能相同,参数可以传递基础数据类型对象,可以理解为比上面的API更强大

width() 用来表示宽度,例如代表view的宽度
mas_width() 用来获取宽度的值。和上面的区别在于,一个代表某个坐标系对象,一个用来获取坐标系对象的值

更新约束和布局

Masonry本质上就是对系统AutoLayout进行的封装,包括里面很多的API,都是对系统API进行了一次二次包装。
typedef NS_OPTIONS(NSInteger, MASAttribute) {
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
MASAttributeRight = 1 << NSLayoutAttributeRight,
MASAttributeTop = 1 << NSLayoutAttributeTop,
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
};

Masonry示例代码

Masonry本质上就是对系统AutoLayout进行的封装,包括里面很多的API,都是对系统API进行了一次二次包装。
typedef NS_OPTIONS(NSInteger, MASAttribute) {
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
MASAttributeRight = 1 << NSLayoutAttributeRight,
MASAttributeTop = 1 << NSLayoutAttributeTop,
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
};

常用方法

设置内边距

/** 
设置yellow视图和self.view等大,并且有10的内边距。
注意根据UIView的坐标系,下面right和bottom进行了取反。所以不能写成下面这样,否则right、bottom这两个方向会出现问题。
make.edges.equalTo(self.view).with.offset(10);

除了下面例子中的offset()方法,还有针对不同坐标系的centerOffset()、sizeOffset()、valueOffset()之类的方法。
*/
[self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).with.offset(10);
make.top.equalTo(self.view).with.offset(10);
make.right.equalTo(self.view).with.offset(-10);
make.bottom.equalTo(self.view).with.offset(-10);
}];

通过insets简化设置内边距的方式

// 下面的方法和上面例子等价,区别在于使用insets()方法。
[self.blueView mas_makeConstraints:^(MASConstraintMaker *make) {
// 下、右不需要写负号,insets方法中已经为我们做了取反的操作了。
make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(10, 10, 10, 10));
}];

更新约束

// 设置greenView的center和size,这样就可以达到简单进行约束的目的
[self.greenView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
// 这里通过mas_equalTo给size设置了基础数据类型的参数,参数为CGSize的结构体
make.size.mas_equalTo(CGSizeMake(300, 300));
}];

// 为了更清楚的看出约束变化的效果,在显示两秒后更新约束。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 指定更新size,其他约束不变。
[self.greenView mas_updateConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(100, 100));
}];
});

大于等于和小于等于某个值的约束

[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
// 设置宽度小于等于200
make.width.lessThanOrEqualTo(@200);
// 设置高度大于等于10
make.height.greaterThanOrEqualTo(@(10));
}];

self.textLabel.text = @"这是测试的字符串。能看到1、2、3个步骤,第一步当然是上传照片了,要上传正面近照哦。上传后,网站会自动识别你的面部,如果觉得识别的不准,你还可以手动修改一下。左边可以看到16项修改参数,最上面是整体修改,你也可以根据自己的意愿单独修改某项,将鼠标放到选项上面,右边的预览图会显示相应的位置。";

textLabel只需要设置一个属性即可

self.textLabel.numberOfLines = 0;

使用基础数据类型当做参数

/** 
如果想使用基础数据类型当做参数,Masonry为我们提供了"mas_xx"格式的宏定义。
这些宏定义会将传入的基础数据类型转换为NSNumber类型,这个过程叫做封箱(Auto Boxing)。

"mas_xx"开头的宏定义,内部都是通过MASBoxValue()函数实现的。
这样的宏定义主要有四个,分别是mas_equalTo()、mas_offset()和大于等于、小于等于四个。
*/
[self.redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.width.mas_equalTo(100);
make.height.mas_equalTo(100);
}];

设置约束优先级

/** 
Masonry为我们提供了三个默认的方法,priorityLow()、priorityMedium()、priorityHigh(),这三个方法内部对应着不同的默认优先级。
除了这三个方法,我们也可以自己设置优先级的值,可以通过priority()方法来设置。
*/
[self.redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.width.equalTo(self.view).priorityLow();
make.width.mas_equalTo(20).priorityHigh();
make.height.equalTo(self.view).priority(200);
make.height.mas_equalTo(100).priority(1000);
}];

Masonry也帮我们定义好了一些默认的优先级常量,分别对应着不同的数值,优先级最大数值是1000。
static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;
static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500;
static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow;
static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel;

设置约束比例

// 设置当前约束值乘以多少,例如这个例子是redView的宽度是self.view宽度的0.2倍。
[self.redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.height.mas_equalTo(30);
make.width.equalTo(self.view).multipliedBy(0.2);
}];

小练习

子视图等高/等宽练习

/**
下面的例子是通过给equalTo()方法传入一个数组,设置数组中子视图及当前make对应的视图之间等高。

需要注意的是,下面block中设置边距的时候,应该用insets来设置,而不是用offset。
因为用offset设置right和bottom的边距时,这两个值应该是负数,所以如果通过offset来统一设置值会有问题。
*/

CGFloat padding = 10;

UIView *redView = [[UIView alloc]init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];

UIView *blueView = [[UIView alloc]init];
blueView.backgroundColor = [UIColor blueColor];
[self.view addSubview:blueView];

UIView *yellowView = [[UIView alloc]init];
yellowView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:yellowView];

/********** 等高 ***********/
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, 0, padding));
make.bottom.equalTo(blueView.mas_top).offset(-padding);
}];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, 0, padding));
make.bottom.equalTo(yellowView.mas_top).offset(-padding);
}];

/**
下面设置make.height的数组是关键,通过这个数组可以设置这三个视图高度相等。其他例如宽度之类的,也是类似的方式。
*/
[yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, padding, padding));
make.height.equalTo(@[blueView, redView]);
}];

/********** 等宽 ***********/
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, padding, 0));
make.right.equalTo(blueView.mas_left).offset(-padding);
}];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(padding, 0, padding, 0));
make.right.equalTo(yellowView.mas_left).offset(-padding);
}];
[yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.right.equalTo(self.view).insets(UIEdgeInsetsMake(padding, 0, padding, padding));
make.width.equalTo(@[redView, blueView]);
}];


子视图垂直居中练习

CGFloat padding = 10;

UIView *redView = [[UIView alloc]init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];

UIView *blueView = [[UIView alloc]init];
blueView.backgroundColor = [UIColor blueColor];
[self.view addSubview:blueView];

[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.left.equalTo(self.view).mas_offset(padding);
make.right.equalTo(blueView.mas_left).mas_offset(-padding);
//make.width.equalTo(blueView);
make.height.mas_equalTo(150);
}];

[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.right.equalTo(self.view).mas_offset(-padding);
make.width.equalTo(redView);
make.height.mas_equalTo(150);
}];



转自:https://www.jianshu.com/p/587efafdd2b3

收起阅读 »

谈谈react hooks的优缺点

谈一下个人认为的react hooks的优缺点,通过和传统的React.Component进行对比得出。#优点一、更容易复用代码这点应该是react hooks最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。hooks...
继续阅读 »

谈一下个人认为的react hooks的优缺点,通过和传统的React.Component进行对比得出。

#优点

一、更容易复用代码

这点应该是react hooks最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。hooks是怎么解决这个复用的问题呢,具体如下:

  1. 每调用useHook一次都会生成一份独立的状态,这个没有什么黑魔法,函数每次调用都会开辟一份独立的内存空间。
  2. 虽然状态(from useState)和副作用(useEffect)的存在依赖于组件,但它们可以在组件外部进行定义。这点是class component做不到的,你无法在外部声明state和副作用(如componentDidMount)。

上面这两点,高阶组件和renderProps也同样能做到。但hooks实现起来的代码量更少,以及更直观(代码可读性)。

举个例子,经常使用的antd-table,用的时候经常需要维护一些状态 ,并在合适的时机去更改它们:

componentDidMount(){
this.loadData();
}

loadData = ()=>{
this.setState({
current: xxx,
total: xxx,
pageSize: xxx,
dataSource: xxx[]
})
}

onTableChange = ()=>{
this.setState({
current: xxx,
total: xxx,
pageSize: xxx,
})
}

render(){
const {total,pageSize,current,dataSource} = this.state;
return <Table
dataSource={dataSource}
pagination={{total,pageSize,current}
onChange={this.onTableChange}
/>
}

每个table都要写一些这种逻辑,那还有啥时间去摸鱼。这些高度类似的逻辑,可以通过封装一个高阶组件来抽象它们。这个高阶组件自带这些状态,并可以自动调用server去获取remote data。

用高阶组件来实现的话会是这样:

import { Table } from 'antd'
import server from './api'

function useTable(server) {
return function (WrappedComponent) {
return class HighComponent extends React.Component {
state = {
tableProps: xxx,
};
render() {
const { tableProps } = this.state;
return <WrappedComponent tableProps={tableProps} />;
}
};
};
}


@useTable(server)
class App extends Component{
render(){
/**
* 高阶组件/renderProps是通过增强组件的props(赋予一个新的属性或者方法到组件的props属性),
* 实现起来比较隐式。你难以区分这个props是来自哪个高阶组件(特别是使用了较多的高阶组件时),
* 或者还是来自业务的父组件。
*/
const { tableProps } = this.props;
return (
<Table
columns={[...]}
// tableProps包含pagination, onChange, dataSource等属性。
{...tableProps}
/>
)
}
}

如果用hooks来实现的话,会是:

import { Table } from 'antd'
import server from './api'

function useTable(server) {
const [tableProps, setTableProps] = useState(xxx);
return tableProps;
}

function App {
const { tableProps } = useTable();
return (
<Table
columns={[...]}
// tableProps包含pagination, onChange, dataSource等属性
{...tableProps}
/>
)
}
/*
相对比高阶组件“祖父=>父=>子”的层层嵌套,
hooks是这样的:
const { brother1 } = usehook1;
const { brother2} = usehook2;
*/

可以看到,hooks的逻辑更清晰,可读性更好。

二、清爽的代码风格+代码量更少

1. 函数式编程风格,函数式组件、状态保存在运行环境、每个功能都包裹在函数中,整体风格更清爽,更优雅。

2. 对IDE更友好,对比类组件,函数组件里面的unused状态和unused-method更容易被编辑器发现。

3. 使用typescript的话,类型声明也变得更容易。

class Example{
hello: string;
constructor(){
this.hello = 'hello world'
}
}

// 代码量更少
function Example(){
const hello:string = 'hello world'
}


4. 向props或状态取值更加方便,函数组件的取值都从当前作用域直接获取变量,而类组件需要先访问实例this,再访问其属性或者方法,多了一步。

5. 更改状态也变得更加简单, `this.setState({ count:xxx })`变成 `setCount(xxx)`。


因为减少了很多模板代码,特别是小组件写起来更加省事,人们更愿意去拆分组件。而组件粒度越细,被复用的可能性越大。所以,hooks也在不知不觉中改变人们的开发习惯,提高项目的组件复用率。


#缺点

一、响应式的useEffect

写函数组件时,你不得不改变一些写法习惯。你必须清楚代码中useEffectuseCallback的“依赖项数组”的改变时机。有时候,你的useEffect依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,你的useEffect就被意外地触发了,如果你的useEffect是幂等的操作,可能带来的是性能层次的问题,如果是非幂等,那就糟糕了。

所以,对比componentDidmountcomponentDidUpdate,useEffect带来的心智负担更大。

二、hooks不擅长异步的代码(旧引用问题)

函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面。当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。比如下面的一个例子(codesandbox):

import React, { useState } from "react";

const Counter = () => {
const [counter, setCounter] = useState(0);

const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counter);
}, 3000);
};

return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};

export default Counter;

当你点击Show me the value in 3 seconds的后,紧接着点击Click me使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。

这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxxthis.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。

其实解决这个hooks的问题也可以参照类的instance。用useRef返回的immutable RefObject(把值保存在current属性上)来保存state,然后取值方式从counter变成了: counterRef.current。如下:

import React, { useState, useRef, useEffect } from "react";

const Counter = () => {
const [counter, setCounter] = useState(0);
const counterRef = useRef(counter);

const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counterRef.current);
}, 3000);
};

useEffect(() => {
counterRef.current = counter;
});

return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};

export default Counter;

结果如我们所期待,alert的是当前的值1。

我们可以把这个过程封装成一个custom hook,如下:

import { useEffect, useRef, useState } from "react";

const useRefState = <T>(
initialValue: T
): [T, React.MutableRefObject<T>, React.Dispatch<React.SetStateAction<T>>] => {
const [state, setState] = useState<T>(initialValue);
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state;
}, [state]);
return [state, stateRef, setState];
};

export default useRefState;

尽管这个问题被巧妙地解决了,但它不优雅、hack味道浓,且丢失了函数编程风格。


三、custom hooks有时严重依赖参数的不可变性

import {useState, useEffect} from 'react'

export function() useData(api){
const [data, setDate] = useState([]);
useEffect(()=>{
api().then(res=>setData(res.data)) ;
// 这里要求传入的api是immutable的,被useCallback/useMemo所包裹。不然每次api一变,
// 都会非预期地多调用一次useEffect。
},[api])
}

对于这点,除了在团队约定参数的不可变性,还可以对useCallback/useMemo进行魔改:

import React from "react";

let useCallback = React.useCallback;

if (__DEV__) {
useCallback = (fn, arr) => {
fn.__useCallback__ = true;
return useCallback(fn, arr);
};
}

export default useCallback;

然后在run-time中去检查是否存在__useCallback__这个属性:

import {useState, useEffect} from 'react'

function checkFn(fn){
if(__DEV__){
if(!fn.__useCallback__){
throw Error('请用团队封装的useCallback来包裹fn')
}
}
}

export function() useData(api){
const [data, setDate] = useState([]);

checkFn(api);

useEffect(()=>{
api().then(res=>setData(res.data)) ;
// 这里要求传入的api是immutable的,被useCallback/useMemo所包裹。不然每次api一变,
// 都会非预期地多调用一次useEffect。
},[api])
}

也有其他的方案:比如用eslint插件去检查。


#怎么避免react hooks的常见问题

  1. 不要在useEffect里面写太多的依赖项,划分这些依赖项成多个单一功能的useEffect。其实这点是遵循了软件设计的“单一职责模式”。
  2. 如果你碰到状态不同步的问题,可以考虑下手动传递参数到函数。如:
   // showCount的count来自父级作用域 
const [count,setCount] = useState(xxx);
function showCount(){ console.log(count) }

// showCount的count来自参数
const [count,setCount] = useState(xxx);
function showCount(c){ console.log(c) }

但这个也只能解决一部分问题,很多时候你不得不使用上述的useRef方案。

3. 拆分组件,细化组件的粒度。复杂业务场景中使用hooks,应尽可能地细分组件,使得组件的功能尽可能单一,这样的hooks组件更好维护。


#感想

hooks很好用很强大,但它不擅长异步。但在有太多异步逻辑的代码时,class比它更适合、更稳、更好维护。


原文链接:https://zhuanlan.zhihu.com/p/88593858

收起阅读 »

iOS动态换肤-支持暗夜模式

适配暗夜模式iOS13新出现了暗夜模式,苹果新增了一些API方便我们来做适配。这里不做深入,只是稍微总结下。适配暗夜模式,无非就是界面显示上的一些变化,暗夜模式下,主题由默认的白色调变为了深色调,相应的,我们的APP在显示上也需要做相应调整。主要包括两个方面:...
继续阅读 »

适配暗夜模式

iOS13新出现了暗夜模式,苹果新增了一些API方便我们来做适配。这里不做深入,只是稍微总结下。

适配暗夜模式,无非就是界面显示上的一些变化,暗夜模式下,主题由默认的白色调变为了深色调,相应的,我们的APP在显示上也需要做相应调整。主要包括两个方面:颜色的变化(视图颜色色,字体颜色等)和图片的改变;

  • 关于颜色改变:UIcolor新增了一个分类\color{red}{UIColor (DynamicColors)},提供了动态color的API。通过特征收集器traitCollection,可以动态判断当前手机的一些界面特征信息。

/*使用时可以做下进一步封装。*/
[UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
UIColor *color = [UIColor lightGrayColor];
if (@available(iOS 13.0,*)) {
if (traitCollection.userInterfaceStyle ==UIUserInterfaceStyleDark ) {
color =[UIColor blackColor];//dark
}else if(traitCollection.userInterfaceStyle ==UIUserInterfaceStyleLight){
color =[UIColor lightGrayColor];//light
}
}
return color;
}];
  • 关于图片:可以在Assets.xcassets中给每一套图片设置对应的模式,系统自动根据当前的模式取用相应的图片;


  • 监听模式的改变:做好以上两点只能部分满足需求,很多时候我们需要确切的知道当前的模式,并且知道用户什么时候切换的模式;

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{
[super traitCollectionDidChange:previousTraitCollection];
if ([UITraitCollection currentTraitCollection].userInterfaceStyle !=previousTraitCollection.userInterfaceStyle ) {
NSLog(@"用户切换了模式,在这里做适配工作");
}else{
NSLog(@"用户没有切换模式");
}
}

关于这种适配方式的几点看法:

  • 工作繁琐,代码杂乱。界面如果需要做一些定制化的改变,就需要监听模式的改变,可能出现一个界面适配的代码出现在好几个地方。

  • 扩展性不高。现在出现了一个暗夜模式,将来会不会再有其他模式?如果APP本来就有几套主题,那么适配起来更加繁琐杂乱。

  • 细细想来,适配暗夜模式,不就是切换主题吗,单独给暗夜模式弄一套对应皮肤就完了。下面看看,如何给APP便捷高效、扩展性性高地换肤。

动态换肤(DynamicSkin)
代码简洁,便于维护;
自动适配暗夜模式,不需要自己每个界面去监听模式的切换;

使用步骤
1.引入框架,导入头文件
手动引入或者通过CocoaPods

pod 'DynamicSkin'
#import "DPDynamicTheme.h"

2.配置模型
继承DPThemeConfig,根据自己的需求,配置相应字段即可。

#import "DPThemeConfig.h"

NS_ASSUME_NONNULL_BEGIN

@interface TestConfig : DPThemeConfig
@property(nonatomic,copy)NSString*color1;
@property(nonatomic,copy)NSString*color2;
@property(nonatomic,copy)NSString*img1;
@property(nonatomic,copy)NSString*tabOne;
@property(nonatomic,copy)NSString*tabTwo;
@property(nonatomic,copy)NSString*tabThree;
@property(nonatomic,copy)NSString*tabTextColorNormal;
@property(nonatomic,copy)NSString*tabTextColorSelect;
@property(nonatomic,copy)NSString*state;
@end

NS_ASSUME_NONNULL_END

3.设置默认主题

__weak typeof (self)weakSelf = self;
//用户切换暗夜模式,或则主动切换pushCurrentThemme:,会触发该回调
[self tz_dynamicTheme:^(TestConfig * _Nullable config) {
[weakSelf.image sd_setImageWithURL:[NSURL URLWithString:config.img1]];
weakSelf.statelabel.text = config.state;
} WithIdentifier:NSStringFromClass([self class])];
}

4.数据绑定

__weak typeof (self)weakSelf = self;
//用户切换暗夜模式,或则主动切换pushCurrentThemme:,会触发该回调
[self tz_dynamicTheme:^(TestConfig * _Nullable config) {
[weakSelf.image sd_setImageWithURL:[NSURL URLWithString:config.img1]];
weakSelf.statelabel.text = config.state;
} WithIdentifier:NSStringFromClass([self class])];
}

5.销毁不需要的回调

-(void)dealloc{
//identifer需要和当前界面绑定的保持一致
[[DPThemeManager manager] removeUpdateWithIdentifer:NSStringFromClass([self class])];
}

转自:https://www.jianshu.com/p/50f24d5af4cd

收起阅读 »

React Hooks究竟是什么呢?

我们大部分 React 类组件可以保存状态,而函数组件不能? 并且类组件具有生命周期,而函数组件却不能?React 早期版本,类组件可以通过继承PureComponent来优化一些不必要的渲染,相对于函数组件,React 官网没有提供对应的方法来缓存函数组件以...
继续阅读 »

我们大部分 React 类组件可以保存状态,而函数组件不能? 并且类组件具有生命周期,而函数组件却不能?

React 早期版本,类组件可以通过继承PureComponent来优化一些不必要的渲染,相对于函数组件,React 官网没有提供对应的方法来缓存函数组件以减少一些不必要的渲染,直接 16.6 出来的 React.memo函数。

React 16.8 新出来的Hook可以让React 函数组件具有状态,并提供类似 componentDidMountcomponentDidUpdate等生命周期方法。

类被会替代吗?

Hooks不会替换类,它们只是一个你可以使用的新工具。React 团队表示他们没有计划在React中弃用类,所以如果你想继续使用它们,可以继续用。

我能体会那种总有新东西要学的感觉有多痛苦,不会就感觉咱们总是落后一样。Hooks 可以当作一个很好的新特性来使用。当然没有必要用 Hook 来重构原来的代码, React团队也建议不要这样做。

Go Go

来看看Hooks的例子,咱们先从最熟悉的开始:函数组件。

以下 OneTimeButton 是函数组件,所做的事情就是当我们点击的时候调用 sayHi 方法。

import React from 'react';
import { render } from 'react-dom';

function OneTimeButton(props) {
return (
<button onClick={props.onClick}>
点我点我
</button>
)
}

function sayHi() {
console.log('yo')
}

render(
<OneTimeButton onClick={sayHi}/>,
document.querySelector('#root')
)

我们想让这个组件做的是,跟踪它是否被点击,如果被点击了,禁用按钮,就像一次性开关一样。

但它需要一个state,因为是一个函数,它不可能有状态(React 16.8之前),所以需要重构成类。

函数组件转换为类组件的过程中大概有5个阶段:

  • 否认:也许它不需要是一个类,我们可以把 state 放到其它地方。

  • 实现: 废话,必须把它变成一个class,不是吗?

  • 接受:好吧,我会改的。

  • 努力加班重写:首先 写 class Thing extends React.Component,然后 实现 render等等 。

  • 最后:添加state。


class OneTimeButton extends React.Component {
state = {
clicked: false
}

handleClick = () => {
this.props.onClick();

// Ok, no more clicking.
this.setState({ clicked: true });
}

render() {
return (
<button
onClick={this.handleClick}
disabled={this.state.clicked}
>
You Can Only Click Me Once
</button>
);
}
}

这是相当多的代码,组件的结构也发生了很大的变化, 我们需要多个小的功能,就需要改写很多。

使用 Hook 轻松添加 State

接下来,使用新的 useState hook向普通函数组件添加状态:

import React, { useState } from 'react'

function OneTimeButton(props) {
const [clicked, setClicked] = useState(false)

function doClick() {
props.onClick();
setClicked(true)
}

return (
<button
onClick={clicked ? undefined : doClick}
disabled={clicked}
>
点我点我
</button>
)
}

这段代码是如何工作的

这段代码的大部分看起来像我们一分钟前写的普通函数组件,除了useState

useState是一个hook。 它的名字以“use”开头(这是Hooks的规则之一 - 它们的名字必须以“use”开头)。

useState hook 的参数是 state 的初始值,返回一个包含两个元素的数组:当前state和一个用于更改state 的函数。

类组件有一个大的state对象,一个函数this.setState一次改变整个state对象。

函数组件根本没有状态,但useState hook允许我们在需要时添加很小的状态块。 因此,如果只需要一个布尔值,我们就可以创建一些状态来保存它。

由于Hook以某种特殊方式创建这些状态,并且在函数组件内也没有像setState函数来更改状态,因此 Hook 需要一个函数来更新每个状态。 所以 useState 返回是一对对应关系:一个值,一个更新该值函数。 当然,值可以是任何东西 - 任何JS类型 - 数字,布尔值,对象,数组等。

现在,你应该有很多疑问,如:

  • 当组件重新渲染时,每次都不会重新创建新的状态吗? React如何知道旧状态是什么?

  • 为什么hook 名称必须以“use”开头? 这看起来很可疑。

  • 如果这是一个命名规则,那是否意味着我可以自定义 Hook。

  • 如何存储更复杂的状态,很多场景不单单只有一个状态值这么简单。

Hooks 的魔力

将有状态信息存储在看似无状态的函数组件中,这是一个奇怪的悖论。这是第一个关于钩子的问题,咱们必须弄清楚它们是如何工作的。

原作者得的第一个猜测是某种编译器的在背后操众。搜索代码useWhatever并以某种方式用有状态逻辑替换它。

然后再听说了调用顺序规则(它们每次必须以相同的顺序调用),这让我更加困惑。这就是它的工作原理。

React第一次渲染函数组件时,它同时会创建一个对象与之共存,该对象是该组件实例的定制对象,而不是全局对象。只要组件存在于DOM中,这个组件的对象就会一直存在。

使用该对象,React可以跟踪属于组件的各种元数据位。

请记住,React组件甚至函数组件都从未进行过自渲染。它们不直接返回HTML。组件依赖于React在适当的时候调用它们,它们返回的对象结构React可以转换为DOM节点。

React有能力在调用每个组件之前做一些设置,这就是它设置这个状态的时候。

其中做的一件事设置 Hooks 数组。 它开始是空的, 每次调用一个hook时,React 都会向该数组添加该 hook

为什么顺序很重要

假设咱们有以下这个组件:

function AudioPlayer() {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);

.....
}

因为它调用useState 3次,React 会在第一次渲染时将这三个 hook 放入 Hooks 数组中。

下次渲染时,同样的3hooks以相同的顺序被调用,所以React可以查看它的数组,并发现已经在位置0有一个useState hook ,所以React不会创建一个新状态,而是返回现有状态。

这就是React能够在多个函数调用中创建和维护状态的方式,即使变量本身每次都超出作用域。

多个useState 调用示例

让咱们更详细地看看这是如何实现的,第一次渲染:

  1. React 创建组件时,它还没有调用函数。React 创建元数据对象和Hooks的空数组。假设这个对象有一个名为nextHook的属性,它被放到索引为0的位置上,运行的第一个hook将占用位置0
  1. React 调用你的组件(这意味着它知道存储hooks的元数据对象)。
  1. 调用useState,React创建一个新的状态,将它放在hooks数组的第0位,并返回[volume,setVolume]对,并将volume 设置为其初始值80,它还将nextHook索引递增1。

  2. 再次调用useState,React查看数组的第1位,看到它是空的,并创建一个新的状态。 然后它将nextHook索引递增为2,并返回[position,setPosition]

  3. 第三次调用useState。 React看到位置2为空,同样创建新状态,将nextHook递增到3,并返回[isPlaying,setPlaying]

现在,hooks 数组中有3个hook,渲染完成。 下一次渲染会发生什么?

  1. React需要重新渲染组件, 由于 React 之前已经看过这个组件,它已经有了元数据关联。

  2. ReactnextHook索引重置为0,并调用组件。

  3. 调用useState,React查看索引0处的hooks数组,并发现它已经在该槽中有一个hook。,所以无需重新创建一个,它将nextHook推进到索引1并返回[volume,setVolume],其中volume仍设置为80

  4. 再次调用useState。 这次,nextHook1,所以React检查数组的索引1。同样,hook 已经存在,所以它递增nextHook并返回[position,setPosition]

  5. 第三次调用useState,我想你知道现在发生了什么。

就是这样了,知道了原理,看起来也就不那么神奇了, 但它确实依赖于一些规则,所以才有使用 Hooks 规则。

Hooks 的规则

自定义 hooks 函数只需要遵守规则 3 :它们的名称必须以“use”为前缀。

例如,我们可以从AudioPlayer组件中将3个状态提取到自己的自定义钩子中:

function AudioPlayer() {
// Extract these 3 pieces of state:
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);

// < beautiful audio player goes here >
}

因此,咱们可以创建一个专门处理这些状态的新函数,并使用一些额外的方法返回一个对象,以便更容易启动和停止播放,例如:

function usePlayerState(lengthOfClip) {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);

const stop = () => {
setPlaying(false);
setPosition(0);
}

const start = () => {
setPlaying(true);
}

return {
volume,
position,
isPlaying,
setVolume,
setPosition,
start,
stop
};
}

像这样提取状态的一个好处是可以将相关的逻辑和行为组合在一起。可以提取一组状态和相关事件处理程序以及其他更新逻辑,这不仅可以清理组件代码,还可以使这些逻辑和行为可重用。

另外,通过在自定义hooks中调用自定义hooks,可以将hooks组合在一起。hooks只是函数,当然,函数可以调用其他函数。

总结

Hooks 提供了一种新的方式来处理React中的问题,其中的思想是很有意思且新奇的。

React团队整合了一组很棒的文档和一个常见问题解答,从是否需要重写所有的类组件到钩Hooks是否因为在渲染中创建函数而变慢? 以及两者之间的所有东西,所以一定要看看。

原文:https://daveceddia.com/intro-to-hooks/

收起阅读 »

当面试官问Webpack的时候他想知道什么

前言在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花...
继续阅读 »

前言

在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。

说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花八门的功能感到陌生。尤其当我们使用诸如umi.js之类的应用框架还帮我们把webpack配置再封装一层的时候,webpack的本质似乎离我们更加遥远和深不可测了。

当面试官问你是否了解webpack的时候,或许你可以说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那我们今天就一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒做API工程师。


你知道webpack的作用是什么吗?

从官网上的描述我们其实不难理解,webpack的作用其实有以下几点:

  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

说一下模块打包运行原理?

如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?

首先我们应该简单了解一下webpack的整个打包流程:

  • 1、读取webpack的配置参数;
  • 2、启动webpack,创建Compiler对象并开始解析项目;
  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。

compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。

而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。

最终Webpack打包出来的bundle文件是一个IIFE的执行函数。

// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})

webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。

其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。

你知道sourceMap是什么吗?

提到sourceMap,很多小伙伴可能会立刻想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含义。除了知道不同参数之间的区别以及性能上的差异外,我们也可以一起了解一下sourceMap的实现方式。

sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap

既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:

{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}

其中mappings数据有如下规则:

  • 生成文件中的一行的每个组用“;”分隔;
  • 每一段用“,”分隔;
  • 每个段由1、4或5个可变长度字段组成;

有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效:

//# sourceURL=/path/to/file.js.map

有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。

如果我们仔细查看webpack打包出来的bundle文件,就可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。

sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴可以看看以下文章,帮助理解soucrMap的原理实现:

Source Map的原理探究[1]

Source Maps under the hood – VLQ, Base64 and Yoda[2]

是否写过Loader?简单描述一下编写loader的思路?

从上面的打包代码我们其实可以知道,Webpack最后打包出来的成果是一份Javascript代码,实际上在Webpack内部默认也只能够处理JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因此当项目存在非JS类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader机制存在的意义。

Loader的配置使用我们应该已经非常的熟悉:

// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}

通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作,同时在开发上需要严格遵循“单一职责”,只关心loader的输出以及对应的输出。

loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有兴趣的小伙伴可以自行阅读源码。

module.exports = function(source) {
const content = doSomeThing2JsString(source);

// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;

// 可以用作解析其他模块路径的上下文
console.log('this.context');

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}

更详细的开发文档可以直接查看官网的 Loader API[3]。

是否写过Plugin?简单描述一下编写plugin的思路?

如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。

上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。

既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks[4]),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks[5])。

Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案(github[6])

// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}

/* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:

  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;
  • 传给每个插件的 compiler 和 compilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;

了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。

class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);

// do something...
})
}
}

最后

本文也是结合一些优秀的文章和webpack本身的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还需要结合源码去阅读和慢慢理解。

Webpack作为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念可以借鉴,我们自然也不应该仅仅停留在API的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让我们学到更多知识,理解得更加深刻,在项目中才能游刃有余的应用。

参考资料

[1]Source Map的原理探究: https://blog.fundebug.com/201...

[2]Source Maps under the hood – VLQ, Base64 and Yoda: *https://docs.microsoft.com/zh...

[3]Loader API: *https://www.webpackjs.com/api...

[4]compiler-hooks: https://webpack.js.org/api/co...

[5]Compilation Hooks: https://webpack.js.org/api/co...

[6]github: https://github.com/webpack/ta...

[7]Plugin API: https://www.webpackjs.com/api...

原文地址(前端大全)

收起阅读 »

几个优雅的JavaScript运算符使用技巧

ECMAScript发展进程中,会有很多功能的更新,比如销毁,箭头功能,模块,它们极大的改变JavaScript编写方式,可能有些人喜欢,有些人不喜欢,但像每个新功能一样,我们最终会习惯它们。新版本的ECMAScript引入了三个新的逻辑赋值运算符:空运算符,...
继续阅读 »

ECMAScript发展进程中,会有很多功能的更新,比如销毁,箭头功能,模块,它们极大的改变JavaScript编写方式,可能有些人喜欢,有些人不喜欢,但像每个新功能一样,我们最终会习惯它们。新版本的ECMAScript引入了三个新的逻辑赋值运算符:空运算符,AND和OR运算符,这些运算符的出现,也是希望让我们的代码更干净简洁,下面分享几个优雅的JavaScript运算符使用技巧

一、可选链接运算符【?.】

可选链接运算符(Optional Chaining Operator) 处于ES2020提案的第4阶段,因此应将其添加到规范中。它改变了访问对象内部属性的方式,尤其是深层嵌套的属性。它也可以作为TypeScript 3.7+中的功能使用。

相信大部分开发前端的的小伙伴们都会遇到null和未定义的属性。JS语言的动态特性使其无法不碰到它们。特别是在处理嵌套对象时,以下代码很常见:

if (data && data.children && data.children[0] && data.children[0].title) {
// I have a title!
}

上面的代码用于API响应,我必须解析JSON以确保名称存在。但是,当对象具有可选属性或某些配置对象具有某些值的动态映射时,可能会遇到类似情况,需要检查很多边界条件。

这时候,如果我们使用可选链接运算符,一切就变得更加轻松了。它为我们检查嵌套属性,而不必显式搜索梯形图。我们所要做的就是使用“?” 要检查空值的属性之后的运算符。我们可以随意在表达式中多次使用该运算符,并且如果未定义任何项,它将尽早返回。

对于静态属性用法是:

object?.property

对于动态属性将其更改为:

object?.[expression]

上面的代码可以简化为:

let title = data?.children?.[0]?.title;

然后,如果我们有:


let data;
console.log(data?.children?.[0]?.title) // undefined

data = {children: [{title:'codercao'}]}
console.log(data?.children?.[0]?.title) // codercao

这样写是不是更加简单了呢? 由于操作符一旦为空值就会终止,因此也可以使用它来有条件地调用方法或应用条件逻辑


const conditionalProperty = null;
let index = 0;

console.log(conditionalProperty?.[index++]); // undefined
console.log(index); // 0

对于方法的调用你可以这样写

object.runsOnlyIfMethodExists?.()

例如下面的parent对象,如果我们直接调用parent.getTitle(),则会报Uncaught TypeError: parent.getTitle is not a function错误,parent.getTitle?.()则会终止不会执行

let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
console.log(this.name)
}
};

parent.getName?.() // parent
parent.getTitle?.() //不会执行

与无效合并一起使用

提供了一种方法来处理未定义或为空值和表达提供默认值。我们可以使用??运算符,为表达式提供默认值

console.log(undefined ?? 'codercao'); // codercao

因此,如果属性不存在,则可以将无效的合并运算符与可选链接运算符结合使用以提供默认值。

let title = data?.children?.[0]?.title ?? 'codercao';
console.log(title); // codercao

二、逻辑空分配(?? =)

expr1 ??= expr2

逻辑空值运算符仅在空值(空值或未定义undefined)时才将值分配给expr1,表达方式:

x ??= y

可能看起来等效于:

x = x ?? y;

但事实并非如此!有细微的差别。

空的合并运算符(??)从左到右操作,如果x不为空,则短路。因此,如果x不为null或未定义,则永远不会对表达式y进行求值。因此,如果y是一个函数,它将根本不会被调用。因此,此逻辑赋值运算符等效于

x ?? (x = y);

三、逻辑或分配(|| =)

此逻辑赋值运算符仅在左侧表达式为 falsy值时才赋值。Falsy值与null有所不同,因为falsy值可以是任何一种值:undefined,null,空字符串(双引号""、单引号’’、反引号``),NaN,0。IE浏览器中的 document.all,也算是一个。

语法

x ||= y

等同于

x || (x = y)

在我们想要保留现有值(如果不存在)的情况下,这很有用,否则我们想为其分配默认值。例如,如果搜索请求中没有数据,我们希望将元素的内部HTML设置为默认值。否则,我们要显示现有列表。这样,我们避免了不必要的更新和任何副作用,例如解析,重新渲染,失去焦点等。我们可以简单地使用此运算符来使用JavaScript更新HTML:

document.getElementById('search').innerHTML ||= '<i>No posts found matching this search.</i>'

四、逻辑与分配(&& =)

可能你已经猜到了,此逻辑赋值运算符仅在左侧为真时才赋值。因此:

x &&= y

等同于

x && (x = y)
最后

本次分享几个优雅的JavaScript运算符使用技巧,重点分享了可选链接运算符的使用,这样可以让我们不需要再编写大量我们例子中代码即可轻松访问嵌套属性。但是IE不支持它,因此,如果需要支持该版本或更旧版本的浏览器,则可能需要添加Babel插件。对于Node.js,需要为此升级到Node 14 LTS版本,因为12.x不支持该版本。

如果你也有优雅的优雅的JavaScript运算符使用技巧,请不要吝惜,在评论区一起交流~

原文链接:https://segmentfault.com/a/1190000039885243


收起阅读 »

移动应用遗留系统重构(6)- 测试篇

前言 上一篇移动应用遗留系统重构(5)- 重构方法篇我们分享了进行依赖解除的重构流程。主要为4个操作步骤,识别内聚包、解除依赖、移动、验收。同时最后也提出了一个问题,重构时如何保证功能的正确性,不会修改出新问题? 其实这个问题容易但又不简单。容易的是把修改得功...
继续阅读 »

前言


上一篇移动应用遗留系统重构(5)- 重构方法篇我们分享了进行依赖解除的重构流程。主要为4个操作步骤,识别内聚包、解除依赖、移动、验收。同时最后也提出了一个问题,重构时如何保证功能的正确性,不会修改出新问题?


其实这个问题容易但又不简单容易的是把修改得功能仔细测一篇保证所有功能正常就可以了。不简单的是如何全面、高效、可重复的执行这个过程。我们很容易联想到的方案就是自动化测试。但最大的问题是,对大部分遗留系统来说都是没有任何自动化测试。而且大量的坏味道代码,可测试性低,我们也很难补充充分的自动化测试。那么我们有什么折中的策略吗?


测试策略


我们先来看看Google Android开发者官网上对于测试的介绍,将不同的类型的测试分为三类测试(即小型、中型和大型测试)。




图片来源developer.android.com




  • 小型测试是指单元测试,用于验证应用的行为,一次验证一个类。

  • 中型测试是指集成测试,用于验证模块内堆栈级别之间的互动或相关模块之间的互动。

  • 大型测试是指端到端测试,用于验证跨越了应用的多个模块的用户操作流程。


前面提到对于遗留单体系统来说通常没有任何自动化测试,并且通常内部结构耦合严重,所以实施中小型的成本非常高。显然对于遗留系统,测试金字塔模型适用度较低。 所以对于遗留系统,可能比较适合的策略模型如下:



对于遗留单体系统,一个可行的思路是先补充中大型的测试,作为基本的冒烟测试,重构优化内部结构后再及时补充中小型测试。


CloudDisk示例


对于我们这个浓缩版的CloudDisk,界面上也比较简单。主要是有一个主界面,主界面上主要为文件、动态、用户。(后续的MV*重构篇会持续补充页面交互及逻辑)



我们可以设计一组UI的测试验证基本的功能。主要的几个测试点如下:



  1. 主界面能正常运行并显示3个Fragment

  2. 3个Fragment能正常显示

  3. 点击登录按钮,能够跳转到登录页面


测试设计的用例如下:


@RunWith(AndroidJUnit4.class)
@LargeTest
public class SmokeTesting
{

@Test
public void should_show_fragment_list_when_activity_launch() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_user)).perform(click());
//then
List fragments = activity.getSupportFragmentManager().getFragments();
assertThat(fragments.size() == 3);
assertThat(fragments.get(0) instanceof FileFragment);
assertThat(fragments.get(1) instanceof DynamicFragment);
assertThat(fragments.get(2) instanceof UserCenterFragment);
});
}

@Test
public void show_show_file_ui_when_click_tab_file() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_file)).perform(click());
//then
onView(withText("Hello file fragment")).check(matches(isDisplayed()));
});
}

@Test
public void show_show_dynamic_ui_when_click_tab_dynamic() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_dynamic)).perform(click());
//then
onView(withText("Hello dynamic fragment")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
});
}

@Test
public void show_show_user_center_ui_when_click_tab_dynamic() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
//when
onView(withText(R.string.tab_user)).perform(click());
//then
onView(withText("Hello user center fragment")).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)));
});
}

@Test
public void show_show_login_ui_when_click_login_button() {
//given
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
Intents.init();
//when
onView(withId(R.id.fab)).perform(click());
//then
intended(IntentMatchers.hasComponent("com.cloud.disk.platform.login.LoginActivity"));
Intents.release();
});
}
}
复制代码

详细代码见Github提交


我们可以将用例运行在Robolectric上,提高反馈的速度,执行命令如下:


./gradlew testDebug --tests SmokeTesting
复制代码

测试执行结果如下:




当然实际的项目里情况更复杂,数据可能来自网络服务、数据库等等。我们还需要进行Mock。后续的MV*重构篇会持续补充常见坏味道示例代码及更多的自动化测试用例。



更多测试框架及设计可以参考Google官方
在 Android 平台上测试应用


总结


这一篇我们介绍了常用的测试分类及遗留系统的测试策略,对于遗留单体系统,一个可行的思路是先补充中大型的测试,作为基本的冒烟测试,重构优化内部结构后再及时补充中小型测试。同时也给CloudDisk补充了一组基础的大型测试作为冒烟测试,作为后续重构的基本守护测试。


下一篇移动应用遗留系统重构(7)- 解耦重构演示篇(一) 我们将基于方法篇的流程开始对CloudDisk进行重构的改造,具体的解耦操作会以视频的方式展示。


参考资料


developer.android.com


CloudDisk示例代码


CloudDisk


系列链接


移动应用遗留系统重构(1)- 开篇


移动应用遗留系统重构(2)-架构篇


移动应用遗留系统重构(3)-示例篇


移动应用遗留系统重构(4)-分析篇


移动应用遗留系统重构(5)- 重构方法篇


大纲



作者:JunBin
链接:https://juejin.cn/post/6954635678982340622
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(5)- 重构方法篇

前言 上一篇 移动应用遗留系统重构(4)-分析篇  我们根据CloudDisk未来的架构,借助ArchUnit进行架构测试守护以及Intellij的Dependendencies分析出了按照未来的架构设计需要解决的异常依赖。 这一篇开始我们将分享进...
继续阅读 »

前言


上一篇

移动应用遗留系统重构(4)-分析篇  我们根据CloudDisk未来的架构,借助ArchUnit进行架构测试守护以及Intellij的Dependendencies分析出了按照未来的架构设计需要解决的异常依赖。


这一篇开始我们将分享进行依赖解除的重构流程、方法以及常用的工具使用。


重构流程


1.识别一个内聚的包
2.解除该包的异常依赖
3.移动该包对应的代码及资源到新的模块
4.包解耦验收

1.识别内聚的包


对于移动应用通常我们可以通过产品的业务划分进行领域的识别划分。例如CloudDisk这个产品的相对还是比较清晰,业务上主要分为文件、动态及个人中心。


对于部分遗留系统来说,旧代码可能散落在不同的包下,或者原先的代码组织方式是以功能划分,而非业务划分。就像CloudDisk的代码一样,第一步我们得先把相关的业务代码组织到同一包下,这个阶段我们可以先不管是否存在异常依赖,因为只有先组织到一个内聚的包下才方便我们进行依赖分析及代码重构。


2.解除异常依赖


这里我们将介绍几种通用的依赖解除手法。包含下沉、接口提取、路由跳转。



后续的演示篇会通过视频进行具体的操作演示
























依赖解除手法使用场景
下沉原本类功能属于Library或者Platform的,直接下沉。例如LogUtil 或 DateUtil等
接口提取适用于Bundle间有数据或者行为依赖。例如某个BundleA中的classA需要触发BundleB的某个业务行为
路由跳转适用于UI页面间跳转。例如某个BundleA中的Activity1,需要跳转到BundleB的Activity2

重构手法:



  1. 类下沉




  • 具体类移动到适当的 Lib 模块中




  • 在调用模块增加对该 Lib 的依赖





  1. 接口提取



  • 在适当的公用模块中创建空的接口

  • 将调用具体页面类的跳转代码块所在的包中建立新的实现类实现该接口

  • (自动)将调用代码块通过 Extract method 提取成新方法



如已经是独立方法跳过此步




  • (自动)在原调用逻辑所属的类中增加实现类成员变量作为delegate



需要预留 Inject 接口,建议采用 Constructor Inject,静态成员提供setter




  • (自动)将新方法调用转移到delegate



如果是静态方法先通过 Change Method Signature 将 delegate 作为参数传给该方法




  • (自动)将新方法 Pull up 到接口

  • (自动)将实现类移动到壳程序中

  • 在壳程序中实现实现类的Inject



  1. 路由跳转



  • 在跳转类定义对应的映射Path

  • 在调用处使用对应的path进行跳转


3.移动代码及资源


当包的异常依赖全部解耦完后,就可以直接进行移动了。这里我们分享2中常用的代码移动方式。



  1. Move


这种方式大家应该比较常用,选择一个File或者Directory,按下F6选择希望移动后的目录则可。



但是这种方式会存在一个问题,就是被移动的类如果依赖了其他的类或者资源,移动后会出现依赖异常。


适用场景:移动的File或Directory没有其他的依赖



  1. Modularize


Modularize能够分析出移动的File存在的相关依赖,并一起关联移动,很好解决Move的痛点,非常适用于跨Module的移动。



选择移动的Module后点击Preview。



这里注意,有一些划线的文件,那是因为这个文件同时被多处引用,如果跟随一起移动,那么其他的地方会报错。所以我们需要将划线的文件先移动至公用的合适位置。待Preview没有任何的文件划线时,就可以进行移动。


4.包解耦验收



  • 所有模块编译通过

  • 所有新增模块符合模块依赖规则

  • 通过架构守护测试


总结


这一篇我们分享了进行依赖解除的重构流程,主要为4个操作步骤,识别内聚包、解除依赖、移动、验收。同时也介绍了Intellij中非常好用的Modularize功能。接下来我们就可以开始动手进行代码重构,但此时我们又面临着另外一个问题,也是很多同学在做重构时经常担心的一个问题。重构时如何保证功能的正确性,不会修改出新问题。


下一篇移动应用遗留系统重构(6)- 测试篇,我们将分享对于单体移动应用遗留系统,如何制定测试策略及有效补充自动化测试,更好为重构保驾护航。


系列链接


移动应用遗留系统重构(1)- 开篇

移动应用遗留系统重构(2)-架构篇

移动应用遗留系统重构(3)-示例篇

移动应用遗留系统重构(4)-分析篇



大纲



作者:JunBin
链接:https://juejin.cn/post/6952298178095874055
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(4)-分析篇

前言 上一篇移动应用遗留系统重构(3)-示例篇我们介绍了CloudDisk的业务及代码现状。分享了“理想”(未来的架构设计)与“现实”(目前的代码现状),接下来在我们开始动手进行重构时,我们首先得知道往理想的设计架构演化,中间存在多少问题。一方面作为开始重构的...
继续阅读 »

前言


上一篇移动应用遗留系统重构(3)-示例篇我们介绍了CloudDisk的业务及代码现状。分享了“理想”(未来的架构设计)与“现实”(目前的代码现状),接下来在我们开始动手进行重构时,我们首先得知道往理想的设计架构演化,中间存在多少问题。一方面作为开始重构的输入,另外一方面我们有数据指标,也能更好评估工作量及衡量进度。


接下来我们将根据架构篇团队采用的架构设计,结合目前的代码,总结分析工具及方法。


架构设计


我们先回忆一下架构篇里团队采用的架构设计。




  1. 代码复用



  • 公共能力复用,有一层专门统一管理应用公用的基础能力,如图片、网络、存储能力、安全等

  • 公用业务能力复用,有一层专门统一管理应用的业务通用组件,如分享、推送、登录等



  1. 低耦合



  • 业务模块间通过API方式依赖,不依赖具体的模块实现

  • 依赖方向清晰,上层模块依赖下层模块



  1. 并行研发



  • 业务模块支持独立编译调试

  • 业务模块独立发布


结合该4层架构、已有的代码,以及业务的后续演化,团队设计的新架构如下



分析工具


ArchUnit


有了架构设计后,我们就能识别代码的边界,这里我们可以通过Archunit进行边界约束描述。我们可以得到2条通用的守护规则。



  1. 垂直方向,下层模块不能反向依赖上层

  2. 横向方向,组件之间不能存在相互的依赖


转化为ArchUnit的测试用例如下:


  @ArchTest
public static final ArchRule architecture_layer_should_has_right_dependency =layeredArchitecture()
.layer("Library").definedBy("..cloud.disk.library..")
.layer("PlatForm").definedBy("..cloud.disk.platform..")
.layer("FileBundle").definedBy("..cloud.disk.bundle.file..")
.layer("DynamicBundle").definedBy("..cloud.disk.bundle.dynamic..")
.layer("UserBundle").definedBy("..cloud.disk.bundle.user..")
.layer("AllBundle").definedBy("..cloud.disk.bundle..")
.layer("App").definedBy("..cloud.disk.app..")
.whereLayer("App").mayOnlyBeAccessedByLayers()
.whereLayer("FileBundle").mayOnlyBeAccessedByLayers("App")
.whereLayer("DynamicBundle").mayOnlyBeAccessedByLayers("App")
.whereLayer("UserBundle").mayOnlyBeAccessedByLayers("App")
.whereLayer("PlatForm").mayOnlyBeAccessedByLayers("App","AllBundle")
.whereLayer("Library").mayOnlyBeAccessedByLayers("App","AllBundle","PlatForm");
复制代码

当然这个用例的执行是失败的,因为我们基本的包结构还没有调整。但有了架构守护测试用例,我们就可以逐步把代码移动到对应的Package中,直到守护用例运行通过为止。


接下来我们先运用IDE工具进行基础的包结构调整,调整后的结构如下



调整后运行ArchUnit测试运行结果如下


这些异常的提示就是我们需要处理的异常依赖。但是ArchUnit的这个提示比较不不友好,接下来我们介绍另外一种分析异常依赖的方式,使用Intellij Dependencies 。


Intellij Dependencies


我们选择对应的Package,选择Analyze菜单,点击Dependencies,可以找出该Package所依赖的相关类。


我们选择dynamic Package进行分析后,发现根据现有的架构约束,存在横向的Bundle依赖需要进行解除依赖。



我是在实际重构过程中,我们可以频繁借助该功能验证耦合解除情况,并且同时通过ArchUnit测试做好守护。


详细代码见Cloud Disk


总结


这一篇我们分享了如何借助工具进行异常依赖的分析。当我们有了未来的架构设计后,可以借助ArchUnit进行架构测试守护,通过Intellij的Dependendencies 我们可以方便以Package或者Class为单位进行依赖分析。


当我们已经分析出需要处理的异常依赖,接下来我们就可以逐步进行重构。下一篇,我们将给大家分享实践总结的一些重构套路,移动应用遗留系统重构(5)- 重构方法篇。


系列链接


大纲




作者:JunBin
链接:https://juejin.cn/post/6950077521790500894
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(3)-示例篇

前言 上一篇移动应用遗留系统重构(2)-架构篇我们介绍了业内的优秀架构实践以及CloudDisk团队根据业务情况设计的分层架构。 这一篇我们将介绍一个浓缩版的示例,示例中我们设计了一些常见的异常依赖,后续的重构篇我们也将基于这个示例进行操作演示。为了简化代码及...
继续阅读 »

前言


上一篇移动应用遗留系统重构(2)-架构篇我们介绍了业内的优秀架构实践以及CloudDisk团队根据业务情况设计的分层架构。


这一篇我们将介绍一个浓缩版的示例,示例中我们设计了一些常见的异常依赖,后续的重构篇我们也将基于这个示例进行操作演示。为了简化代码及对业务上下文的理解,示例中的部分实现都是空实现,重点体现异常的耦合依赖。


仓库地址:CloudDisk


CloudDisk示例


项目概述


CloudDisk是一个类似于Google Drive的云存储应用。该应用主要拥有3大核心业务模块。



  1. 文件模块:用于管理用户云端文件系统。用户能够上传、下载、浏览文件。

  2. 动态模块:类似微信朋友圈,用于可以在动态上分享信息及文件

  3. 个人中心模块:用于管理用户个人信息


问题说明


该项目已经维护超过10年以上,目前有用开发人员100+。代码在一个大单体模块中,约30w行左右,编译时间5分钟以上。团队目前主要面临几个问题。



  1. 开发效率低,编译时间长,经常出现代码合并冲突

  2. 代码质量差,经常修改出新问题

  3. 市场响应慢,需要对齐各个模块进行整包发布


代码分析


代码在一个Module中,且在一个Git仓中管理。采用"MVC"结构,按功能进行划分Package。


包结构如下:



主要包说明:



































包名功能说明
adapterViewPager RecycleView等适配器类
callback接口回调
controller主要的业务逻辑
model数据模型
uiActivity、Fragment相关界面
util公用工具类

主要类说明:







































类名功能说明
MainActivity应用主界面,用于加载显示各个模块的Fragment
CallBack网络接口操作回调
DynamicController动态模块主要业务逻辑,包含发布及获取列表
FileController文件模块主要业务逻辑,主要包含上传、下载、获取文件列表
UserController用户模块主要业务逻辑,主要包含登录,获取用户信息
HttpUtils网络请求,用于发送get及post请求
LogUtils主要用于进行日志记录

详细源码见CloudDisk



为了简化业务上下文理解,代码都是空实现,只体现模块的异常依赖,后续的MV*重构篇会持续补充常见坏味道示例代码。



总结


CloudDisk在业务最初发展的时候,采用了单一Module及简单“MVC”架构很好的支持了业务的发展,但随着业务的演化及人员膨胀,这样的模式已经很难高效的支持业务及团队的发展。


前面我们已经分享了“理想”(未来的架构设计)与“现实”(目前的代码现状),接下来在我们开始动手进行重构时,我们首先得知道往理想的设计架构演化,中间存在多少问题。一方面作为开始重构的输入,另外一方面我们有数据指标,也能评估工作量及衡量进度。


下一篇,我们将给大家分享移动应用遗留系统重构(4)-分析篇。介绍常用的分析工具及框架,并对CloudDisk团队目前的代码进行分析。


系列链接


大纲



作者:JunBin
链接:https://juejin.cn/post/6947855094272491556
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(2)-架构篇

前言 上一篇移动应用遗留系统重构(1)- 开篇我们分享了移动应用遗留系统常见的问题。那么好的实践或者架构设计是怎样的呢? 这一篇我们将整理业内优秀的移动应用架构设计,包含微信、淘宝、支付宝以及美团外卖。其中的部分产品也经历过遗留系统的重构改造,具有非常好的参考...
继续阅读 »

前言


上一篇移动应用遗留系统重构(1)- 开篇我们分享了移动应用遗留系统常见的问题。那么好的实践或者架构设计是怎样的呢?


这一篇我们将整理业内优秀的移动应用架构设计,包含微信、淘宝、支付宝以及美团外卖。其中的部分产品也经历过遗留系统的重构改造,具有非常好的参考意义。


优秀实践


微信


从微信对外分享的架构演进文章中可知,微信应用其实也是经历了从大单体到模块化的演进。




图片来源 微信Android模块化架构重构实践



我们看下介绍中后续改造后的架构设计。




图片来源 微信Android模块化架构重构实践



设计中提到重构主要3个目标



  • 改变通信方式 (API化)

  • 重新设计模块 (中心化业务代码回归各自业务)

  • 约束代码边界 (pins工程结构,更细粒度管控边界)


我们可以发现重构后架构比原来的单体应用的一些变化。



  1. 业务模块独立编译调试,耦合度低

  2. 代码复用高,有统一公共的组件库及kernel

  3. 模块职责、代码边界清晰,强约束


更多信息可阅读原文,微信Android模块化架构重构实践


手淘


从手机淘宝客户端架构探索实践的分享中介绍到手机淘宝从1.0用单工程编写开始,东西非常简陋;到2.0为索引许多三方库的庞大的单工程;再到3.0打破了单工程开发模式实现业务复用。




图片来源 手机淘宝客户端架构探索实践



淘宝架构主要分为四层,最上层是组件Bundle(业务组件),往下是容器(核心层),中间件Bundle(功能封装),基础库Bundle(底层库)。


文章提到架构演化的一些优点及变化很值得深思。



  1. 业务复用,减少人力

  2. 基础复用,做深做精

  3. 敏捷开发,快速试错


支付宝


在支付宝mPass实践讨论分析一文中,提到支付宝客户端的总体架构图如下。




图片来源 开篇 | 模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨



分享文章中介绍到5层架构设计如下:




  • 最底层是支付宝框架的容器层,包括类加载资源加载和安全模块;




  • 第二层是我们抽离出来的组件层,包括网络库,日志库,缓存库,多媒体库,日志等等,简单说这些是一些通用的能力层;




  • 第三层是我们定制的框架层,这是关键部分,是我们得以实现上千人,上千多个工程共同开发一个 App 的基础。




  • 第四层是基于框架封装出来的业务服务层;




  • 第五层便是具体的业务模块,其中每一个模块都是一个或多个具体的工程;




文章中介绍到关于工程之间的依赖关系的处理比较特别。


在支付宝的架构里,编译参与的部分是和运行期参与的部分是分离的:编译期使用 bundle 的接口包,运行期使用 bundle 包本身。bundle 的接口包是 bundle 包的一部分,即刚才说的 bundle 的代码部分。bundle 的资源包同时打进接口包,在编译期提供给另一个 bundle 引用。


更多信息可阅读原文,开篇 | 模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨


美团


最后看另外一个跨平台技术架构相关的分享,在外卖客户端容器化架构的演进分享中提到了美团外包的整体架构如下。




图片来源 外卖客户端容器化架构的演进



特别的一点是是采用了容器化架构,根据业务场景及PV,支持多种容器技术。在文末的总结提到,容器化架构相对于传统的移动端架构而言,充分地利用了现在的跨端技术,将动态化的能力最大化的赋予业务。通过动态化,带来业务迭代周期缩短、编译的加速、开发效率的提升等好处。同时,也解决了面临着的多端复用、平台能力、平台支撑、单页面多业务团队、业务动态诉求强等业务问题。但对线上的可用性、容器的可用性、支撑业务的线上发布上提出了更加严格的要求。


更多信息可阅读原文,外卖客户端容器化架构的演进


总结


架构是为了解决业务的问题,没有银弹。 但通过这些业内的优秀实践分享,我们可以发现一些优秀的设计范式。



  1. 代码复用



  • 公共能力复用,有专门统一管理应用公用的基础能力,如图片、网络、存储能力、安全等

  • 公用业务能力复用,有专门统一管理应用的业务通用组件,如分享、推送、登录等



  1. 低耦合,高内聚



  • 业务模块间通过API方式依赖,不依赖具体的模块实现

  • 依赖方向清晰,上层模块依赖下层模块



  1. 并行研发



  • 业务模块支持独立编译调试

  • 业务模块独立发布


结合这些特点及CloudDisk团队的业务,团队采用的架构设计如下。



下一篇,移动应用遗留系统重构(3)- 示例篇,我们将继续介绍CloudDisk的业务及团队问题,分析现有的代码。


参考


微信Android模块化架构重构实践


手机淘宝客户端架构探索实践



参考来自阿里云开发者社区,但链接已失效



开篇 | 模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨


外卖客户端容器化架构的演进


系列链接


大纲





作者:JunBin
链接:https://juejin.cn/post/6945313969556946980
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

移动应用遗留系统重构(1)- 开篇

前言 2008年9月22日,谷歌正式对外发布第一款Android手机。苹果公司最早于2007年1月9日的MacWorld大会上公布IOS系统。移动应用领域的发展已经超过10年。在App Annie 最新的移动市场报告中分享2020应用下载量已经达到2180亿次...
继续阅读 »

前言


2008年9月22日,谷歌正式对外发布第一款Android手机。苹果公司最早于2007年1月9日的MacWorld大会上公布IOS系统。移动应用领域的发展已经超过10年。在App Annie 最新的移动市场报告中分享2020应用下载量已经达到2180亿次,同比增加7%。根据Statista的统计,2020年度Google Play的应用数量为3148932个。


在移动互联网的高速发展及竞争中,更快及更高质量的交付用户,显然尤为重要。但很多产品随着移动互联网的发展,已经迭代超过10年。在这个过程中人员流动、技术债务累计、技术生态更新,使得产生了大量的遗留系统。就像一辆低排量的破旧汽车,再大的马路,技术再好的驾驶员,达到车辆本身的系统瓶颈,速度就很难再提升起来。


遗留系统



在以往的项目中,遇到了大量的这种遗留系统。这些系统具有以下一些特点。



  • 大泥球架构,代码量上百万行,开发人员超过100+

  • 内部耦合高,代码修改维护牵一发动全身,质量低

  • 编译集成调试慢,没有任何自动化测试,开发效率低

  • 技术栈陈旧,祖传代码,无人敢动


在这样的背景下,个别少的团队选择重写,当然没有良好的过程管理及方法,好多重写完又成了新的遗留系统。也有的团队选择重构,但目前相关的方法及教程比较少。这里推荐一下《重构(第2版)》,书中有基本的重构手法。另外一本《修改代码的艺术》,书中有很多基于遗留代码开发的示例。但对于开发人员来说,缺少比较贴近移动应用项目实战的重构指导及系统方法。很多团队依旧没有解决遗留系统根本的原因,仅靠不断的堆人,恶性循环。


CloudDisk 演示示例


CloudDisk是一个类似于Google Drive的云存储应用。该应用主要拥有3大核心业务模块,文件、动态及个人中心。


该项目已经维护超过10年以上,目前有用开发人员100+。目前代码在一个大模块中,约30w行左右,编译时间10分钟以上。团队目前主要还面临几个问题。



  1. 开发效率低



编译时间长,经常出现代码合并冲突。遗留大量技术债务,团队疲于交付需求




  1. 代码质量差



经常修改出新问题,版本提测问题多,没有任何自动化测试




  1. 版本发布周期长



往往需要1个月以上,市场反馈响应慢



我们希望通过一个更贴近实际工程项目的浓缩版遗留系统示例,持续解决团队在产品不断迭代中遇到的问题。从架构设计与分析、安全重构、基础生态设施、流水线、编译构建等方面,一步一步介绍如何进行持续演化。我们将通过文章及视频演示的方式进行分享,希望通过这个系列文章,大家可以更系统的掌握移动应用项目中实战的重构技巧及落地方法。


大纲



作者:JunBin
链接:https://juejin.cn/post/6943470229905211422
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

这15个Android开源库,只有经常逛Github的才知道!

哈喽,大家好,我是西哥! 又到了大家最喜欢了的环节--开源库推荐,前面为大家推荐了我收藏的一些非常酷的开源库,受到大家一致好评,还没看过的,请移步至: 【Android珍藏】推荐10个炫酷的开源库 【开源推荐】进阶实战,从一款音乐播放器开始 【2020年Git...
继续阅读 »

哈喽,大家好,我是西哥!


又到了大家最喜欢了的环节--开源库推荐,前面为大家推荐了我收藏的一些非常酷的开源库,受到大家一致好评,还没看过的,请移步至:


【Android珍藏】推荐10个炫酷的开源库


【开源推荐】进阶实战,从一款音乐播放器开始


【2020年GitHub 上那些优秀Android开源库,这里是Top10!】


本期又为大家带来了哪些有趣的库呢?本期为大家精选了15个有趣又有用的开源,排名不分先后,一起来看看吧!


1. Coil



Coil是Android上的一个全新的图片加载框架,它的全名叫做 coroutine image loader,即协程图片加载库。与传统的图片加载库Glide,Picasso或Fresco等相比。该具有轻量(只有大约1500个方法)、快、易于使用、更现代的API等优势。


它支持GIF和SVG,并且可以执行四个默认转换:模糊圆形裁剪灰度圆角


示例如下:


imageView.load(“https://www.example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}
复制代码

并且是全用Kotlin编写,如果你是纯Kotlin项目的话,那么这个库应该是你的首选。


Github地址:github.com/coil-kt/coi…


2. MultiSearchView



该库具有一个非常酷的Search View 动画!


使用非常简单,并且可以自定义,你可以在在styles.xml下添加自定义样式。


示例代码:


<com.iammert.library.ui.multisearchviewlib.MultiSearchView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
复制代码

multiSearchView.setSearchViewListener(object : MultiSearchView.MultiSearchViewListener{
override fun onItemSelected(index: Int, s: CharSequence) {
}

override fun onTextChanged(index: Int, s: CharSequence) {
}

override fun onSearchComplete(index: Int, s: CharSequence) {
}

override fun onSearchItemRemoved(index: Int) {
}

})
复制代码

自定义样式:


  <!-- Search Text Style. -->
<style name="SearchTextStyle">
<!-- Custom values write to here for SearchEditText. -->
<item name="android:focusable">true</item>
<item name="android:focusableInTouchMode">true</item>
<item name="android:enabled">true</item>
<item name="android:hint">Search</item>
<item name="android:imeOptions">actionSearch</item>
<item name="android:textSize">18sp</item>
<item name="android:maxLength">15</item>
<item name="android:inputType">textCapSentences</item>
<item name="android:textColorHint">#80999999</item>
<item name="android:textColor">#000</item>
</style>
复制代码

然后,您应该将样式设置为MultiSearchView下的app:searchTextStyle


Github地址:github.com/iammert/Mul…


3. CalendarView



CalendarView是一个高度可定制化的日历组件库,用recycleView实现。


它有如下特性:



  • 单一或范围选择

  • 周历或者月历模式

  • 边界日期

  • 自定义日历视图

  • 水平或者垂直滚动模式

  • 完全可定制的视图


该库的文档也非常全面,并包含许多示例。此外,还有一个示例应用程序展示了库的所有功能。


它是用纯Kotlin编写的,并在MIT许可下发布。如果您需要在应用程序中使用日历视图,这是一个不错的选择。



注意:该库通过Java 8+ API使用了java.time类,以便向后兼容,因为这些类是在Java 8中添加的。



因此,需要在app的build.gradle 中添加如下配置:


android {
defaultConfig {
// Required ONLY when setting minSdkVersion to 20 or lower
multiDexEnabled true
}

compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:<latest-version>'
}
复制代码

Github: github.com/kizitonwose…


4. Bubble Navigation















FloatingTopBarActivityTopBarActivity














BottomBarActivityEqualBottomBarActivity

Bubble Navigation是一个轻巧的库,可通过大量自定义选项轻松制作精美的导航栏。


它有很多非常的特性:




  • 针对不同用例的两种类型的NavigationViews




    • BubbleNavigationConstraintView(支持spreadspreadinside, 和 packed莫斯)




    • BubbleNavigationLinearView(允许平均分配,使用权重或packed模式)






  • 高度可定制化




  • 您可以添加小红点,它具有BubbleToggleView来创建新的UI组件,而不仅仅是导航




示例:


<com.gauravk.bubblenavigation.BubbleNavigationConstraintView
android:id="@+id/top_navigation_constraint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="380dp"
android:background="@color/white"
android:elevation="4dp"
android:padding="12dp"
app:bnc_mode="spread">

<com.gauravk.bubblenavigation.BubbleToggleView
android:id="@+id/c_item_rest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:bt_active="true"
app:bt_colorActive="@color/search_active"
app:bt_colorInactive="@color/search_inactive"
app:bt_icon="@drawable/ic_restaurant"
app:bt_shape="@drawable/transition_background_drawable_restaurant"
app:bt_title="@string/restaurant"
app:bt_padding="@dimen/internal_padding"
app:bt_titlePadding="@dimen/title_padding" />

<!-- Add more child items here - max upto 5 -->

</com.gauravk.bubblenavigation.BubbleNavigationConstraintView>
复制代码

Github文档很完善,有很多示例,更多用法和属性可去Github了解。


Github:github.com/gauravk95/b…


5. FabFilter



这是一个有趣的项目,它不是一个直接可用的库,而是一个示例应用程序,展示了使用不使用 MotionLayout两种方式来实现的高级UI动画。


详细的实现细节可以看看Medium上的系列文章:


“Complex UI/Animations on Android”


“Complex UI/Animations on Android — featuring MotionLayout”


Github:github.com/nikhilpanju…


6.android-showcase



android-showcase是一个非常优秀的开源项目,它是一个展示应用程序,展示了如何使用Kotlin和最新的Jetpack 技术栈来开发一个APP。


该项目为您带来了一系列最佳实践,工具和解决方案:



  • 100% Kotlin

  • 现代架构 (feature modules, clean architecture, Model-View-ViewModel, Model-View-Intent)

  • Android Jetpack组件

  • 单Activity模式,使用Navigation导航


看完这个项目,在模块化,Clean体系结构,测试、设置CI / CD工具,等方面,你将会受到启发。感谢作者的开源。


Github:github.com/igorwojda/a…


7. Croppy



Croppy是一个Android图片裁剪库。


它有很多强大的特性:



  • 双指缩放

  • 裁剪任意大小

  • 按照长宽比例裁剪

  • 显示裁剪后的Bitmap

  • 自动居中裁剪

  • 全面的动画使用体验


更多使用细节请看Github。


Github: github.com/lyrebirdstu…


8. RubberPicker



一个炫酷的、有趣的SeekBar动画库。


RubberPicker库包含RubberSeekBarRubberRangePicker,其灵感来自CubertoiOS橡胶范围选择器


使用示例:


布局文件中配置


<com.jem.rubberpicker.RubberSeekBar
...
app:minValue="20"
app:maxValue="80"
app:elasticBehavior="cubic"
app:dampingRatio="0.3"
app:stiffness="300"
app:stretchRange="24dp"
app:defaultThumbRadius="16dp"
app:normalTrackWidth="4dp"
app:highlightTrackWidth="8dp"
app:normalTrackColor="#AAAAAA"
app:highlightTrackColor="#BA1F33"
app:defaultThumbInsideColor="#FFF"
app:highlightDefaultThumbOnTouchColor="#CD5D67"/>

<!-- Similar attributes can be applied for RubberRangePicker as well-->
<com.jem.rubberpicker.RubberRangePicker
...
app:minValue="0"
app:maxValue="100"
app:elasticBehavior="linear"
app:dampingRatio="0.4"
app:stiffness="400"
app:stretchRange="36dp"
app:defaultThumbRadius="16dp"
app:normalTrackWidth="4dp"
app:highlightTrackWidth="8dp"
app:normalTrackColor="#AAAAAA"
app:highlightTrackColor="#BA1F33"
app:defaultThumbInsideColor="#CFCD5D67"
app:highlightDefaultThumbOnTouchColor="#CD5D67"/>
复制代码

或者,在代码中动态配置:


val rubberSeekBar = RubberSeekBar(this)
rubberSeekBar.setMin(20)
rubberSeekBar.setMax(80)
rubberSeekBar.setElasticBehavior(ElasticBehavior.CUBIC)
rubberSeekBar.setDampingRatio(0.4F)
rubberSeekBar.setStiffness(1000F)
rubberSeekBar.setStretchRange(50f)
rubberSeekBar.setThumbRadius(32f)
rubberSeekBar.setNormalTrackWidth(2f)
rubberSeekBar.setHighlightTrackWidth(4f)
rubberSeekBar.setNormalTrackColor(Color.GRAY)
rubberSeekBar.setHighlightTrackColor(Color.BLUE)
rubberSeekBar.setHighlightThumbOnTouchColor(Color.CYAN)
rubberSeekBar.setDefaultThumbInsideColor(Color.WHITE)

val currentValue = rubberSeekBar.getCurrentValue()
rubberSeekBar.setCurrentValue(currentValue + 10)
rubberSeekBar.setOnRubberSeekBarChangeListener(object : RubberSeekBar.OnRubberSeekBarChangeListener {
override fun onProgressChanged(seekBar: RubberSeekBar, value: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(seekBar: RubberSeekBar) {}
override fun onStopTrackingTouch(seekBar: RubberSeekBar) {}
})


//Similarly for RubberRangePicker
val rubberRangePicker = RubberRangePicker(this)
rubberRangePicker.setMin(20)
...
rubberRangePicker.setHighlightThumbOnTouchColor(Color.CYAN)

val startThumbValue = rubberRangePicker.getCurrentStartValue()
rubberRangePicker.setCurrentStartValue(startThumbValue + 10)
val endThumbValue = rubberRangePicker.getCurrentEndValue()
rubberRangePicker.setCurrentEndValue(endThumbValue + 10)
rubberRangePicker.setOnRubberRangePickerChangeListener(object: RubberRangePicker.OnRubberRangePickerChangeListener{
override fun onProgressChanged(rangePicker: RubberRangePicker, startValue: Int, endValue: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
override fun onStopTrackingTouch(rangePicker: RubberRangePicker, isStartThumb: Boolean) {}
})
复制代码

更多、更详细的使用请看Github。


Github:github.com/Chrisvin/Ru…


9. Switcher



一个炫酷的Switcher 切换动画库,真是的太可爱了,我前面也写过文章专门介绍过:


炫酷!从未见过如此Q弹的Switcher


它的灵感来自于 Dribble上Oleg Frolov 的设计。


Github: github.com/bitvale/Swi…


10. StfalconImageViewer



StfalconImageViewer是一个图片查看库,
该库简单且可定制。它包含一个全屏图像查看器具有共享的图像过渡支持捏合缩放功能以及滑动手势来关闭手势。


Github文档说明了如何使用每个功能。同样值得注意的是:该库与所有最受欢迎的图像处理库(例如Picasso,Glide等)兼容。


所有可配置项如下:


StfalconImageViewer.Builder<String>(this, images, ::loadImage)
.withStartPosition(startPosition)
.withBackgroundColor(color)
//.withBackgroundColorResource(R.color.color)
.withOverlayView(view)
.withImagesMargin(R.dimen.margin)
//.withImageMarginPixels(margin)
.withContainerPadding(R.dimen.padding)
//.withContainerPadding(R.dimen.paddingStart, R.dimen.paddingTop, R.dimen.paddingEnd, R.dimen.paddingBottom)
//.withContainerPaddingPixels(padding)
//.withContainerPaddingPixels(paddingStart, paddingTop, paddingEnd, paddingBottom)
.withHiddenStatusBar(shouldHideStatusBar)
.allowZooming(isZoomingAllowed)
.allowSwipeToDismiss(isSwipeToDismissAllowed)
.withTransitionFrom(targeImageView)
.withImageChangeListener(::onImageChanged)
.withDismissListener(::onViewerDismissed)
.withDismissListener(::onViewerDismissed)
复制代码

更详细的使用请看Github。


Github: github.com/stfalcon-st…


11. Broccoli



Broccoli是一个show View Loading 库,也就是我常说的骨架屏,在内容加载的时候,显示一个占位符。


该库带有很平滑的动画效果,你可以配合RecyclerView一起使用,等待加载内容时,再也不枯燥了。


示例:


Broccoli broccoli = new Broccoli();

//add the default style placeholder
broccoli.addPlaceholders('activity', 'view_id', 'view_id');

or
//add the default style placeholder
broccoli.addPlaceholders('view1', 'view2', 'view3');

or

//add the custom style placeholder
broccoli.addPlaceholder(new PlaceholderParameter.Builder()
.setView('view')
.setAnimation('scaleAnimation');
.setDrawable(DrawableUtils.createRectangleDrawable(placeHolderColor, 0))
.build());

or
//add the custom style placeholder with gradient animation
broccoli.addPlaceholder(new PlaceholderParameter.Builder()
.setView('view')
.setDrawable(new BroccoliGradientDrawable(Color.parseColor("#DDDDDD"),
Color.parseColor("#CCCCCC"), 0, 1000, new LinearInterpolator())
.build());
broccoli.show();
复制代码

更多使用请看Github。


Github: github.com/samlss/Broc…


12. Orbit MVI



这是一个用于Kotlin和Android的Model-View-Intent (MVI)框架。它的灵感来自Jake Wharton,RxFeedback和Mosby的“Managing State with RxJava”。


根据ReadMe所说:



Orbit在您的redux实现周围提供了尽可能小的结构,以使其易于使用,但您仍可以使用RxJava的强大功能。



redux系统可能如下所示:


data class State(val total: Int = 0)

data class AddAction(val number: Int)

sealed class SideEffect {
data class Toast(val text: String) : SideEffect()
}

class CalculatorViewModel : OrbitViewModel<State, SideEffect> (State(), {

perform("addition")
.on<AddAction>()
.sideEffect { post(SideEffect.Toast("Adding ${event.number}")) }
.reduce {
currentState.copy(currentState.total + event.number)
}

...
})
复制代码

activity / fragment 中:


// Example of injection using koin, your DI system might differ
private val viewModel by viewModel<CalculatorViewModel>()

override fun onCreate() {
...
addButton.setOnClickListener { viewModel.sendAction(AddAction) }
}

override fun onStart() {
viewModel.connect(this, ::handleState, ::handleSideEffect)
}

private fun handleState(state: State) {
...
}

private fun handleSideEffect(sideEffect: SideEffect) {
when (sideEffect) {
is SideEffect.Toast -> toast(sideEffect.text)
}
}
复制代码

详细使用请看Github。


Github: github.com/babylonheal…


13. IndicatorScrollView















IndicatorScrollViewIndicatorScrollView

该库为NestedScrollView添加了逻辑,使它可以在滚动时,更改对指示器进行动态响应。


README文件包含开始项目所需的所有信息,例如如何使用IndicatorScrollViewIndicatorViewIndicatorItem。目前,它的版本为1.0.2,是根据Apache 2.0许可发布的。它支持API 16及更高版本。


文档示例很详细,更多使用相关请看Github。


Github: github.com/skydoves/In…


14. Cyanea



Cyanea 是一个Android 主题引擎库。


它允许那你动态更换应用主题,它内置了很多主题如:



  • Theme.Cyanea.Dark

  • Theme.Cyanea.Dark.LightActionBar

  • Theme.Cyanea.Dark.NoActionBar

  • Theme.Cyanea.Light

  • Theme.Cyanea.Light.DarkActionBar

  • Theme.Cyanea.Light.NoActionBar


更多详细信息请看Github。


Github: github.com/jaredrummle…


15. Android MotionLayout Carousel



这是一个示例项目,它展示了如何使用MotionLayout来实现一个炫酷的轮播图。


文档几乎没有任何说明,但是如果你最近也在探索MotionLayout,这将是一个很好示例。


Github: github.com/faob-dev/Mo…


总结


以上就是本期的开源项目推荐,如果你也有好玩的、有趣的、强大的开源项目,也可以推荐给西哥,欢迎评论区留言讨论。

作者:依然范特稀西

链接:https://juejin.cn/post/6856637313964441614
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

7个你应该知道的Gradle实用技巧

前言 Gradle在android开发中应用地十分广泛,但相信有很多同学并不很了解gradle 本文主要介绍了使用gradle的一些实用技巧,帮助读者增进对这个熟悉的陌生人的了解 主要包括以下内容 1.Gradle依赖树查询 2.使用循环优化Gradle依赖...
继续阅读 »

前言


Gradleandroid开发中应用地十分广泛,但相信有很多同学并不很了解gradle

本文主要介绍了使用gradle的一些实用技巧,帮助读者增进对这个熟悉的陌生人的了解

主要包括以下内容



  • 1.Gradle依赖树查询

  • 2.使用循环优化Gradle依赖管理

  • 3.支持代码提示的Gradle依赖管理

  • 4.Gradle模块化

  • 5.Library模块Gradle代码复用

  • 6.资源文件分包

  • 7.AAR依赖与源码依赖快速切换


1.Gradle依赖树查询


有时我们在分析依赖冲突时,需要查看依赖树,我们常用的查看依赖树的命令为


gradlew app:dependencies
复制代码

不过这种命令行方式查看依赖树出来的信息太多,看的有些费劲

所以官方又推出了Scan工具来帮助我们更加方便地查看依赖树

在项目根目录位置下运行gradle build --scan即可,然后会生成 HTML 格式的分析文件的分析文件


分析文件会直接上传到Scan官网,命令行最后会给出远程地址

第一次跑会让你在 Scan 官网注册一下,邮件确认后就能看了

scan 工具是按照依赖变体挨个分类的,debugCompileClassPath 就是 dedug 打包中的依赖包了


如上,使用这种方式分析依赖树更加方便简洁


2.使用循环优化Gradle依赖管理


如下所示,我们常常使用ext来管理依赖


    dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation rootProject.ext.dependencies["appcompat-v7"]
implementation rootProject.ext.dependencies["cardview-v7"]
implementation rootProject.ext.dependencies["design"]
implementation rootProject.ext.dependencies["constraint-layout"]
annotationProcessor rootProject.ext.dependencies["glide_compiler"]
...
}
复制代码

这样虽然实现了依赖的统一管理,但是随着项目越来越大,依赖也会越来越多,常常会有几十甚至上百行,导致build.gradle越来越长


有没有一种好的方式不在 build.gradle 中写这么多的依赖配置?

有,就是使用循环遍历依赖。

示例如下,首先添加config.gradle


ext{
dependencies = [
// base
"appcompat-v7" : "com.android.support:appcompat-v7:${version["supportLibraryVersion"]}",
...
]

annotationProcessor = [
"glide_compiler" : "com.github.bumptech.glide:compiler:${version["glideVersion"]}",
...
]

apiFileDependencies = [
"launchstarter" :"libs/launchstarter-release-1.0.0.aar"
]

debugImplementationDependencies = [
"MethodTraceMan" : "com.github.zhengcx:MethodTraceMan:1.0.7"
]

...

implementationExcludes = [
"com.android.support.test.espresso:espresso-idling-resource:3.0.2" : [
'com.android.support' : 'support-annotations'
]
]

...
}
复制代码

然后在build.gradle中配置如下:


apply from config.gradle
...

def implementationDependencies = project.ext.dependencies
def processors = project.ext.annotationProcesso
def implementationExcludes = project.ext.implementationExcludes
dependencies{
// 处理所有的 xxximplementation 依赖
implementationDependencies.each { k, v -> implementation v }
// 处理 annotationProcessor 依赖
processors.each { k, v -> annotationProcessor v }
// 处理所有包含 exclude 的依赖
implementationExcludes.each { entry ->
implementation(entry.key) {
entry.value.each { childEntry ->
exclude(group: childEntry)
}
}
}
...

}
复制代码

这样做的优点在于

1.后续添加依赖不需要改动build.gradle,直接在config.gradle中添加即可

2.精简了build.gradle的长度


3.支持代码提示的Gradle依赖管理


上面介绍了通过config.gradle管理依赖的方法

在我们添加Gradle依赖时,还有一些痛点

1.不支持代码提示

2.不支持单击跳转

3.多模块开发时,不同模块相同的依赖需要复制粘贴


使用buildSrc+kotlin可以解决这个问题

效果如下:



由于buildSrc是对全局的所有module的配置,所以可以在所有module中直接使用


这里就不多介绍了,详细开发及引入buildSrc的过程可见:

[译]Kotlin + buildSrc:更好的管理Gadle依赖


buildSrc vs includeBuild


上面介绍的方法使用的是buildSrc,使用起来比较方便

不过它的缺点在于构建速度上会慢一些,使用includeBuild可以实现同样的效果

两者实现的最终效果是差不多的

详细实现可见:【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本?巧用 includeBuild


4.Gradle模块化


我们在开发中,引入一些插件时,有时需要在build.gradle中引入一些配置,比如greendao,推送,tinker

这些其实是可以封装在相应gradle文件中,然后通过apply from引入

举个例子,例如在我们使用greendao数据库时,需要在build.gradle中指定版本


这种时候应该新建一个greendao-config.gradle


apply plugin: 'org.greenrobot.greendao'

//greenDao指定版本和路劲等
greendao {
//数据库的schema版本,也可以理解为数据库版本号
schemaVersion 1
//设置DaoMaster、DaoSession、Dao包名,也就是要放置这些类的包的全路径。
daoPackage 'com.example.ausu.big_progect.dao'
//设置DaoMaster、DaoSession、Dao目录
targetGenDir 'src/main/java'
}
复制代码

然后再在build.gradle中引入


apply from 'greendao-config.gradle'
复制代码

这样做主要有2个优点

1.单一职责原则,将greendao的相关配置封装在一个文件里,不与其他文件混淆

2.精简了build.gradle的代码,同时后续修改数据库相关时不需要修改build.gradle的代码


5.Library模块Gradle代码复用


随着我们项目的越来越大,Library Module也越建越多,每个Module都有自己的build.gradle

但其实每个build.gradle的内容都差不多,我们能不能将重复的部分封装起来复用?


我们可以做一个 basic 抽取,同样将共有参数/信息提取到 basic.gradle 中,每个 module apply,这样就是减少了不少代码量


apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
// 指定用于编译项目的 API 级别
compileSdkVersion Versions.compileSDK
// 指定在生成项目时要使用的 SDK 工具的版本,Android Studio 3.0 后不需要手动配置。
buildToolsVersion Versions.buildTools

// 指定 Android 插件适用于所有构建版本的版本属性的默认值
defaultConfig {
minSdkVersion Versions.minSDK
targetSdkVersion Versions.targetSDK
versionCode 1
versionName "1.0"
}

// 配置 Java 编译(编码格式、编译级别、生成字节码版本)
compileOptions {
encoding = 'utf-8'
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

lintOptions {
// lint 异常后继续执行
abortOnError false
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
}
复制代码

然后在相应的模块的build.gradle中引入即可


apply from:"../basic.gradle"

dependencies {
api Deps.constraintLayout
api Deps.retrofit
}
复制代码

这样是不是简洁很多?读者可根据项目实际情况判断是否适合抽取basic.gradle使用


6.资源文件分包


随着项目越来越大,项目中的资源文件也越来越大,比如layoutdrawable文件夹下的文件数量常常可达几百甚至上千个

我们能不能像代码一样,对资源文件进行分包呢?


答案是可以的,主要是利用gradlesourceSets属性

我们可以将资源文件像代码一样按业务分包,具体操作如下


1.新建res_xxx目录


main 目录下新建 res_core, res_feed(根据业务模块命名)等目录,在res_core中新建res目录中相同的文件夹如:layoutdrawable-xxhdpivalues等。


2.在gradle中配置res_xx目录


android {
//...
sourceSets {
main {
res.srcDirs(
'src/main/res',
'src/main/res_core',
'src/main/res_feed',
)
}
}
}
复制代码

以上就完成了资源文件分包,这样做主要有几点好处

1.按业务分包查找方便,结构清晰

2.strings.xmlkey-value型文件多人修改可以减少冲突

3.当删除模块或做组件化改造时资源文件删除或迁移方便,不必像以前一样一个个去找


7.AAR依赖与源码依赖快速切换


当我们的项目中Module越来越多,为了加快编译速度,常常把Module发布成AAR,然后在项目中直接依赖AAR

但是我们有时候又需要修改AAR,就需要依赖于源码

所以我们需要一个可以快速地切换依赖AAR与依赖源码的方式


我们下面举个例子,以retrofit为例

假如我们要修改retrofit的源码,修改步骤如下:

1.首先下载retrofit,可以放到和项目同级的目录,并修改目录名为retrofit-source,以便区分

2.在settings.gradle文件中添加需要修改的aar库的源码project


include ':retrofit-source'
project(':retrofit-source').projectDir = new File("../retrofit-source")
复制代码

3.替换aar为源码

build.gradle(android) 脚本中添加替换策略


allprojects {
repositories {
...
}

configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute module( "com.squareup.retrofit2:retrofit") with project(':retofit-source')
}
}
}
}
复制代码

如上几步,就可以比较方便地实现aar依赖与源码依赖间的互换了

这样做的主要优点在于

1.不需要修改原有的依赖配置,而是通过全局的配置,利用本地的源码替换掉aar,侵入性低

2.如果有多个Module依赖于同一个aar,不需要重复修改,只需在根目录build.gradle中修改一处


总结


本文主要介绍了几个实用的Gradle技巧,如果觉得有所帮助,可以帮忙点赞

如果发现本文还有什么不足,欢迎在评论区指出~


作者:RicardoMJiang
链接:https://juejin.cn/post/6947675376835362846
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一文读懂 View.Post 的原理及缺陷

很多开发者都了解这么一个知识点:在 Activity 的 onCreate 方法里我们无法直接获取到 View 的宽高信息,但通过 View.post(Runnable)这种方式就可以,那背后的具体原因你是否有了解过呢? 读者可以尝试以下操作。可以发现,除了通...
继续阅读 »

很多开发者都了解这么一个知识点:在 Activity 的 onCreate 方法里我们无法直接获取到 View 的宽高信息,但通过 View.post(Runnable)这种方式就可以,那背后的具体原因你是否有了解过呢?


读者可以尝试以下操作。可以发现,除了通过 View.post(Runnable)这种方式可以获得 View 的真实宽高外,其它方式取得的值都是 0


/**
* 作者:leavesC
* 时间:2020/03/14 11:05
* 描述:
* GitHub:https://github.com/leavesC
*/
class MainActivity : AppCompatActivity() {

private val view by lazy {
findViewById<View>(R.id.view)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getWidthHeight("onCreate")
view.post {
getWidthHeight("view.Post")
}
Handler().post {
getWidthHeight("handler")
}
}

override fun onResume() {
super.onResume()
getWidthHeight("onResume")
}

private fun getWidthHeight(tag: String) {
Log.e(tag, "width: " + view.width)
Log.e(tag, "height: " + view.height)
}

}
复制代码

github.leavesc.view E/onCreate: width: 0
github.leavesc.view E/onCreate: height: 0
github.leavesc.view E/onResume: width: 0
github.leavesc.view E/onResume: height: 0
github.leavesc.view E/handler: width: 0
github.leavesc.view E/handler: height: 0
github.leavesc.view E/view.Post: width: 263
github.leavesc.view E/view.Post: height: 263
复制代码

从这就可以引申出几个疑问:



  • View.post(Runnable) 为什么可以得到 View 的真实宽高

  • Handler.post(Runnable)View.post(Runnable)有什么区别

  • onCreateonResume 函数中为什么无法直接得到 View 的真实宽高

  • View.post(Runnable) 中的 Runnable 是由谁来执行的,可以保证一定会被执行吗


后边就来一一解答这几个疑问,本文基于 Android API 30 进行分析


一、View.post(Runnable)


看下 View.post(Runnable) 的方法签名,可以看出 Runnable 的处理逻辑分为两种:



  • 如果 mAttachInfo 不为 null,则将 Runnable 交由mAttachInfo内部的 Handler 进行处理

  • 如果 mAttachInfo 为 null,则将 Runnable 交由 HandlerActionQueue 进行处理


    public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
复制代码

1、AttachInfo


先来看View.post(Runnable)的第一种处理逻辑


AttachInfo 是 View 内部的一个静态类,其内部持有一个 Handler 对象,从注释可知它是由 ViewRootImpl 提供的


final static class AttachInfo {

/**
* A Handler supplied by a view's {@link android.view.ViewRootImpl}. This
* handler can be used to pump events in the UI events queue.
*/
@UnsupportedAppUsage
final Handler mHandler;

AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
···
mHandler = handler;
···
}

···
}
复制代码

查找 mAttachInfo 的赋值时机可以追踪到 View 的 dispatchAttachedToWindow 方法,该方法被调用就意味着 View 已经 Attach 到 Window 上了


	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
···
}
复制代码

再查找dispatchAttachedToWindow 方法的调用时机,可以跟踪到 ViewRootImpl 类。ViewRootImpl 内就包含一个 Handler 对象 mHandler,并在构造函数中以 mHandler 作为构造参数之一来初始化 mAttachInfo。ViewRootImpl 的performTraversals()方法就会调用 DecorView 的 dispatchAttachedToWindow 方法并传入 mAttachInfo,从而层层调用整个视图树中所有 View 的 dispatchAttachedToWindow 方法,使得所有 childView 都能获取到 mAttachInfo 对象


	final ViewRootHandler mHandler = new ViewRootHandler();

public ViewRootImpl(Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
···
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
···
}

private void performTraversals() {
···
if (mFirst) {
···
host.dispatchAttachedToWindow(mAttachInfo, 0);
···
}
···
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
···
}
复制代码

此外,performTraversals()方法也负责启动整个视图树的 Measure、Layout、Draw 流程,只有当 performLayout 被调用后 View 才能确定自己的宽高信息。而 performTraversals()本身也是交由 ViewRootHandler 来调用的,即整个视图树的绘制任务也是先插入到 MessageQueue 中,后续再由主线程取出任务进行执行。由于插入到 MessageQueue 中的消息是交由主线程来顺序执行的,所以 attachInfo.mHandler.post(action)就保证了 action 一定是在 performTraversals 执行完毕后才会被调用,因此我们就可以在 Runnable 中获取到 View 的真实宽高了


2、HandlerActionQueue


再来看View.post(Runnable)的第二种处理逻辑


HandlerActionQueue 可以看做是一个专门用于存储 Runnable 的任务队列,mActions 就存储了所有要执行的 Runnable 和相应的延时时间。两个post方法就用于将要执行的 Runnable 对象保存到 mActions中,executeActions就负责将mActions中的所有任务提交给 Handler 执行


public class HandlerActionQueue {

private HandlerAction[] mActions;
private int mCount;

public void post(Runnable action) {
postDelayed(action, 0);
}

public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}

public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}

mActions = null;
mCount = 0;
}
}

private static class HandlerAction {
final Runnable action;
final long delay;

public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}

public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}

···

}
复制代码

所以说,getRunQueue().post(action)只是将我们提交的 Runnable 对象保存到了 mActions 中,还需要外部主动调用 executeActions方法来执行任务


而这个主动执行任务的操作也是由 View 的 dispatchAttachedToWindow来完成的,从而使得 mActions 中的所有任务都会被插入到 mHandler 的 MessageQueue 中,等到主线程执行完 performTraversals() 方法后就会来执行 mActions,所以此时我们依然可以获取到 View 的真实宽高


	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
···
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
···
}
复制代码

二、Handler.post(Runnable)


Handler.post(Runnable)View.post(Runnable)有什么区别呢?


从上面的源码分析就可以知道,View.post(Runnable)之所以可以获取到 View 的真实宽高,主要就是因为确保了获取 View 宽高的操作一定是在 View 绘制完毕之后才被执行,而 Handler.post(Runnable)之所以不行,就是其无法保证这一点


虽然这两种post(Runnable)的操作都是往同个 MessageQueue 插入任务,且最终都是交由主线程来执行。但绘制视图树的任务是在onResume被回调后才被提交的,所以我们在onCreate中用 Handler 提交的任务就会早于绘制视图树的任务被执行,因此也就无法获取到 View 的真实宽高了


三、onCreate & onResume


onCreateonResume 函数中为什么无法也直接得到 View 的真实宽高呢?


从结果反推原因,这说明当 onCreateonResume被回调时 ViewRootImpl 的 performTraversals()方法还未执行,那么performTraversals()方法的具体执行时机是什么时候呢?


这可以从 ActivityThread -> WindowManagerImpl -> WindowManagerGlobal -> ViewRootImpl 这条调用链上找到答案


首先,ActivityThread 的 handleResumeActivity 方法就负责来回调 Activity 的 onResume 方法,且如果当前 Activity 是第一次启动,则会向 ViewManager(wm)添加 DecorView


	@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
···
//Activity 的 onResume 方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
···
if (r.window == null && !a.mFinished && willBeVisible) {
···
ViewManager wm = a.getWindowManager();
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//重点
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
···
}
复制代码

此处的 ViewManager 的具体实现类即 WindowManagerImpl,WindowManagerImpl 会将操作转交给 WindowManagerGlobal


    @UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
复制代码

WindowManagerGlobal 就会完成 ViewRootImpl 的初始化并且调用其 setView 方法,该方法内部就会再去调用 performTraversals 方法启动视图树的绘制流程


public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
···
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
···
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
复制代码

所以说, performTraversals 方法的调用时机是在 onResume 方法之后,所以我们在 onCreateonResume 函数中都无法获取到 View 的实际宽高。当然,当 Activity 在单次生命周期过程中第二次调用onResume 方法时自然就可以获取到 View 的宽高属性


四、View.post(Runnable) 的兼容性


从以上分析可以得出一个结论:由于 View.post(Runnable)最终都是往和主线程关联的 MessageQueue 中插入任务且最终由主线程来顺序执行,所以即使我们是在子线程中调用View.post(Runnable),最终也可以得到 View 正确的宽高值


但该结论也只在 API 24 及之后的版本上才成立,View.post(Runnable) 方法也存在着一个版本兼容性问题,在 API 23 及之前的版本上有着不同的实现方式


	//Android API 24 及之后的版本
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

//Android API 23 及之前的版本
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
复制代码

在 Android API 23 及之前的版本上,当 attachInfo 为 null 时,会将 Runnable 保存到 ViewRootImpl 内部的一个静态成员变量 sRunQueues 中。而 sRunQueues 内部是通过 ThreadLocal 来保存 RunQueue 的,这意味着不同线程获取到的 RunQueue 是不同对象,这也意味着如果我们在子线程中调用View.post(Runnable) 方法的话,该 Runnable 永远不会被执行,因为主线程根本无法获取到子线程的 RunQueue


    static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();

static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}
复制代码

此外,由于sRunQueues 是静态成员变量,主线程会一直对应同一个 RunQueue 对象,如果我们是在主线程中调用View.post(Runnable)方法的话,那么该 Runnable 就会被添加到和主线程关联的 RunQueue 中,后续主线程就会取出该 Runnable 来执行


即使该 View 是我们直接 new 出来的对象(就像以下的示例),以上结论依然生效,当系统需要绘制其它视图的时候就会顺便取出该任务,一般很快就会执行到。当然,由于此时 View 并没有 attachedToWindow,所以获取到的宽高值肯定也是 0


        val view = View(Context)
view.post {
getWidthHeight("view.Post")
}
复制代码

View.post(Runnable)方法的兼容性问题做下总结:



  • 当 API < 24 时,如果是在主线程进行调用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 均会被执行。但只有在 View 被 AttachedToWindow 的情况下才可以获取到 View 的真实宽高

  • 当 API < 24 时,如果是在子线程进行调用,那么不管 View 是否有 AttachedToWindow,提交的 Runnable 都将永远不会被执行

  • 当 API >= 24 时,不管是在主线程还是子线程进行调用,只要 View 被 AttachedToWindow 后,提交的 Runnable 都会被执行,且都可以获取到 View 的真实宽高值。如果没有被 AttachedToWindow 的话,Runnable 也将永远不会被执行


五、总结


Activity 的 onResume 方法在第一次被调用后,绘制视图树的 Runnable 才会被 Post 到和主线程关联的 MessageQueue 中,虽然该 Runnable 和回调 Activity 的 onResume 方法的操作都是在主线程中执行的,但是该 Runnable 只有等到主线程后续将其从 MessageQueue 取出来后才会被执行,所以这两者其实是构成了异步行为,因此我们在onCreateonResume 这两个方法里才无法直接获取到 View 的宽高大小


当 View 还未绘制完成时,通过 View.post(Runnable)提交的 Runnable 会等到 View.dispatchAttachedToWindow方法被调用后才会被保存到 MessageQueue 中,这样也依然保证了该 Runnable 一定是会在 View 绘制完成后才会被执行,所以此时我们才能获取到 View 的宽高大小


除了View.post(Runnable)外,我们还可以通过 OnGlobalLayoutListener 来获取 View 的宽高属性,onGlobalLayout 方法会在视图树发生变化的时候被调用,在该方法中我们就可以来获取 View 的宽高大小


        view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
val width = view.width
}
})
复制代码

按照我自己的想法,系统提供View.post(Runnable)这个方法的目的不仅仅是为了用来获取 View 的宽高等属性这么简单,有可能是为了提供一种优化手段,使得我们可以在整个视图树均绘制完毕后才去执行一些不紧急又必须执行的操作,使得整个视图树可以尽快地呈现出来,以此优化用户体验


作者:业志陈
链接:https://juejin.cn/post/6939763855216082974
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS -SDWebImage的使用和底层原理

一、SDWebImage的使用1、SDWebImage的安装集成有2种方式:(1)直接到github地址下载,链接https://github.com/rs/SDWebImage        (2)用coco...
继续阅读 »

一、SDWebImage的使用

1、SDWebImage的安装集成有2种方式:

(1)直接到github地址下载,链接https://github.com/rs/SDWebImage        

(2)用cocoapods安装,在文件夹生成的podfile文件中添加pod 'SDWebImage' ,终端cd + 文件位置,然后pod install即可

2、UITableView中导入头文件UIImageView+WebCache.h

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

如果在加载完图片后,需要做些其他操作,可以使用block回调

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
... completion code here ...
}];

3、SDWebImageManager的使用

UIImageView(WebCache) 分类的核心在于 SDWebImageManager 的下载和缓存处理,SDWebImageManager将图片下载和图片缓存组合起来了。SDWebImageManager也可以单独使用。

SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
// do something with image
}
}];

4、单独使用SDWebImageDownloader异步下载图片

我们还可以单独使用 SDWebImageDownloader 来下载图片,但是图片内容不会缓存。

SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
// do something with image
}
}];

5、单独使用SDImageCache异步缓存图片

SDImageCache 支持内存缓存和异步的磁盘缓存(可选),如果你想单独使用 SDImageCache 来缓存数据的话,可以使用单例,也可以创建一个有独立命名空间的 SDImageCache 实例。

添加缓存的方法:

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法:

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];

读取缓存时可以使用 queryDiskCacheForKey:done: 方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。

SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
// image is not nil if image was found
}];

6、自定义缓存key

有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的 key。

SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
};

二、使用过程中常见问题

问题 1:使用 UITableViewCell 中的 imageView 加载不同尺寸的网络图片时会出现尺寸缩放问题。

解决方案: 

自定义 UITableViewCell,重写 -layoutSubviews 方法,调整位置尺寸; 
或者直接弃用 UITableViewCell 的 imageView,自己添加一个 imageView 作为子控件。

问题 2:图片刷新问题:SDWebImage 在进行缓存时忽略了所有服务器返回的 caching control 设置,并且在缓存时没有做时间限制,这也就意味着图片 URL 必须是静态的了,要求服务器上一个 URL 对应的图片内容不允许更新。但是如果存储图片的服务器不由自己控制,也就是说 图片内容更新了,URL 却没有更新,这种情况怎么办?

解决方案:在调用 sd_setImageWithURL: placeholderImage: options:方法时设置 options 参数为 SDWebImageRefreshCached,这样虽然会降低性能,但是下载图片时会照顾到服务器返回的 caching control。

问题 3:在加载图片时,如何添加默认的 progress indicator ? 

解决方案:在调用 -sd_setImageWithURL:方法之前,先调用下面的方法:

[imageView sd_setShowActivityIndicatorView:YES]; 

[imageView sd_setIndicatorStyle:UIActivityIndicatorViewStyleGray];

问题4:如果在加载图片的过程中出现程序报错(App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.)

你需要操作如下--------
(1)、在Info.plist中添加 NSAppTransportSecurity 类型 Dictionary ;

(2)、在 NSAppTransportSecurity 下添加 NSAllowsArbitraryLoads 类型Boolean ,值设为 YES;

三、SDWebImage底层原理


1)当我门需要获取网络图片的时候,我们首先需要的便是URL,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URl相关的图片,如果有则直接返回image,如果没有则进行下一步。

2)当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。

3)如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。

4)刷新UI(当然根据实际情况操作)将image存入沙盒缓存。

四、SDWebImage源码实现步骤

常见的四种加载方式

1、无占位图直接加载(如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

2、有占位图直接加载(如果URL加载不到则展示占位图,如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

3、有占位图直接加载,并且实现图片加载完之后的Block可以继续完成下一步操作(如果URL加载不到则展示占位图,如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

4、可以选择options的形式加载图片,(如果URL加载不到则展示占位图,如果缓存中存在改图片则直接获取无需重新下载增加磁盘缓存)

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

/*使用可更换optionsType的加载方式

-------------Options 枚举下的加载方式-----------
SDWebImageRetryFailed 默认情况下,当URL无法下载时,URL就会被列入黑名单,这样库就不会继续尝试了。此标记禁用此黑名单。
SDWebImageLowPriority 默认情况下,图像下载是在UI交互过程中启动的,这标志禁用该特性,导致在UIScrollView减速方面延迟下载。
SDWebImageCacheMemoryOnly 此标记禁用磁盘缓存
SDWebImageProgressiveDownload 此标志可以进行渐进式下载,在下载过程中,图像会逐步显示,就像浏览器所做的那样。默认情况下,图像只显示一次完全下载。
SDWebImageRefreshCached 即使缓存了映像,也要尊重HTTP响应缓存控制,并在需要的情况下从远程位置刷新映像。磁盘缓存将由NSURLCache来处理,而不是使用SDWebImage,这会导致轻微的性能下降。这个选项有助于处理在同一个请求URL后面更改的图像,例如Facebook图形api概要图。如果刷新了缓存的图像,那么完成块就会被缓存的图像和最后的图像再次调用一次。只有当你不能用嵌入的缓存破坏参数使你的url静态时,才使用这个标志。
SDWebImageContinueInBackground 在iOS 4+中,如果应用程序进入后台,可以继续下载图片。这是通过请求系统在后台获得额外的时间来完成请求完成的。如果后台任务过期,操作将被取消。
SDWebImageHandleCookies 通过设置NSMutableURLRequest来处理存储在NSHTTPCookieStore中的cookie。HTTPShouldHandleCookies =是的;
SDWebImageAllowInvalidSSLCertificates 启用不受信任的SSL证书。用于测试目的。在生产中使用谨慎。
SDWebImageHighPriority 默认情况下,图像按顺序装载在队列中。这个标志把它们移到队列的前面。
SDWebImageDelayPlaceholder 默认情况下,在图像加载时加载占位符图像。此标志将延迟加载占位符图像,直到图像完成加载。
SDWebImageTransformAnimatedImage 我们通常不会在动画图像上调用transformdownloade昏暗委托方法,因为大多数转换代码会把它搞砸。无论如何,使用这个标志来转换它们。* /
SDWebImageAvoidAutoSetImage 默认情况下,图像会在下载后添加到imageView中。但是在某些情况下,我们想要在设置图像之前有手(例如,应用一个过滤器或将它添加到交叉衰减动画中)使用这个标记如果你想在成功完成时手工设置图像
SDWebImageScaleDownLargeImages 默认情况下,图像会被解码,以尊重它们原来的大小。在iOS上,这一标志将把图像缩小到与设备受限内存兼容的大小。*如果“SDWebImageProgressiveDownload”标志设置禁用缩减。
*/

以上四个常用方法,点击进去查看内部实现代码时,你会发现所有方法都指向------>


源码注释解释的含义是

用url、占位符和自定义选项设置imageView图像。下载是异步的和缓存的。
@param url是图像的url。
@param占位符将首先设置的图像,直到图像请求完成。
@param选择在下载图像时使用的选项。
@参见SDWebImageOptions用于可能的值。
@param progressBlock在下载@note时,在后台队列
@param completedBlock的后台进程中执行进程块,该块是在操作完成时被调用的。这个块没有返回值,并将所请求的UIImage作为第一个参数。在出现错误时,图像参数为nil,第二个参数可能包含一个NSError。第三个参数是一个布尔值,指示是否从本地缓存或网络检索图像。第四个参数是原始图像url。

下面是图解(上面展示了每句话的备注)

1、设置展位图,并且取消当前下载任务


2、创建一个新的下载操作


3、下载操作代码(判断流是否存在,如果不存在则将其存在失效列表中,防止重复下载无效流)-----在这里他对NSString和NSURL的转换做了判断。原因是(非常常见的错误是使用NSString对象而不是NSURL发送URL。出于某种奇怪的原因,Xcode不会对这种类型的不匹配发出任何警告。在这里,我们通过允许url作为NSString传递来确保这个错误。)括号当中是文档给出的解释,所以这里做了强制转换。


4、利用唯一生成的key,到缓存--->内存---->磁盘中分别寻找。


5、寻找的顺序 缓存---->磁盘---->在没有就下载


下载流程之后就是清理缓存(种类) 1、清理所有内存缓存镜像 2、清理所有磁盘缓存镜像3、清理过期的缓存映像从磁盘中删除

/*
异步清除所有磁盘缓存映像。非阻塞方法-立即返回。@param完成一个应该在缓存过期后执行的块(可选)

注意:这里要注意[[SDImageCache sharedImageCache] clearDisk];方法会报错,下面clearDiskOnCompletion的方法会替代上面的方法
*/
[[SDImageCache sharedImageCache] clearDiskOnCompletion:^{

}];

/*
Clear all memory cached images --->清除所有缓存镜像
*/
[[SDImageCache sharedImageCache] clearMemory];

/*
异步将所有过期的缓存映像从磁盘中删除。非阻塞方法-立即返回。@param completionBlock在缓存过期后执行(可选)--->故名思义他是不能删除你当前缓存的大小的
*/
[[SDImageCache sharedImageCache] deleteOldFilesWithCompletionBlock:^{

}];

五、总结

SDWebImage加载图片的流程:
1. 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage显示,然后 SDWebImageManager 根据 URL 开始处理图片。

2. 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

3. 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

4. SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache等前端展示图片。

5. 如果内存缓存中没有,生成 NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存。

6. 根据 URLKey在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

9. 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

10. 图片下载由 NSURLConnection来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

11. connection:didReceiveData: 中利用 ImageIO做了按图片下载进度加载效果。

12. connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

13. 图片解码处理在一个 NSOperationQueue完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

14. 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

15. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager告知图片下载完成

16. 通知所有的 downloadDelegates下载完成,回调给需要的地方展示图片。

17. 将图片保存到 SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

18. SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

19. SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

20. SDWebImagePrefetcher 可以预先下载图片,方便后续使用。



原文链接:https://blog.csdn.net/qq_16146389/article/details/88355852


收起阅读 »

一文读懂 View & Window 机制(二)

一文读懂 View & Window 机制(一)六、DecorView DecorView 是 FrameLayout 的子类,其 onResourcesLoaded 方法在拿到 PhoneWindow 传递过来的 layoutResource 后,就...
继续阅读 »

一文读懂 View & Window 机制(一)

六、DecorView


DecorView 是 FrameLayout 的子类,其 onResourcesLoaded 方法在拿到 PhoneWindow 传递过来的 layoutResource 后,就会生成对应的 View 并添加为自己的 childView,就像普通的 ViewGroup 通过 addView 方法来添加 childView 一样,该 childView 就对应 mContentRoot,我们可以在 Activity 中通过(window.decorView as ViewGroup).getChildAt(0)来获取到 mContentRoot


所以 DecorView 可以看做是 Activity 中整个视图树的根布局


public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

@UnsupportedAppUsage
private PhoneWindow mWindow;

ViewGroup mContentRoot;

DecorView(Context context, int featureId, PhoneWindow window,
WindowManager.LayoutParams params) {
···
}

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}

}
复制代码

七、ActivityThread


完成以上步骤后,此时其实还只是完成了 Activity 整个视图树的加载工作,虽然 Activity 的 attach方法已经创建了 Window 对象,但还需要将 DecorView 提交给 WindowManager 后才能正式将视图树展示到屏幕上


DecorView 具体的提交时机还需要看 ActivityThread 的 handleResumeActivity 方法,该方法用于回调 Activity 的 onResume 方法,里面就会回调到 Activity 的makeVisible 方法,从方法名就可以猜出来makeVisible就用于令 Activity 变为可见状态


	@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
···
r.activity.makeVisible();
···
}
复制代码

makeVisible 方法会判断当前 Activity 是否已经将 DecorView 提交给 WindowManager 了,如果还没的话就进行提交,最后将 DecorView 的可见状态设为 VISIBLE,至此才建立起 Activity 和 WindowManager 之间的关联关系,Activity 也才正式变为可见状态


    void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
复制代码

八、做下总结


对以上流程做下总结



  1. 每个 Activity 内部都包含一个 Window 对象,该对象的具体实现类是 PhoneWindow。Activity 的 setContentViewfindViewById 等操作都会交由 Window 来实现,Window 是 Activity 和整个 View 系统交互的入口

  2. PhoneWindow 根据 theme 和 features 得知 Activity 的基本视图属性,由此来选择合适的根布局文件 layoutResource,每种 layoutResource虽然在布局结构上略有不同,但是均会包含一个 ID 名为content的 FrameLayout,ContentParent 即该 FrameLayout。我们可以通过 Window.ID_ANDROID_CONTENT来拿到该 ID,也可以在 Activity 中通过 findViewById<View>(Window.ID_ANDROID_CONTENT) 来获取到ContentParent

  3. PhoneWindow 并不直接管理视图树,而是交由 DecorView 去管理。DecorView 会根据layoutResource来生成对应的 rootView 并将开发者指定的 ContentView 添加为ContentParent的 childView,所以可以将 DecorView 看做是视图树的根布局。正因为如此,Activity 的 findViewById 操作实际上会先交由 Window,Window 再交由 DecorView 去完成,因为 DecorView 才是实际持有 ContentView 的容器类

  4. PhoneWindow 是 Window 这个抽象类的的唯一实现类,Activity 和 Dialog 内部其实都是使用 PhoneWindow 来加载视图树,因此 PhoneWindow 成为了上层类和视图树系统之间的交互入口,从而也将 Activity 和 Dialog 的共同视图逻辑给抽象出来了,减轻了上层类的负担,这也是 Window 机制存在的好处之一

  5. Activity 的视图树是在makeVisible 方法里提交给 WindowManager 的,之后 WindowManagerImpl 会通过 ViewRootImpl 来完成整个视图树的绘制流程,至此 Activity 才对用户可见

  6. View 通过 Canvas 绘制自身,定义了具体的 UI 效果。View 和 ViewGroup 共同组成一个具体的视图树,视图树的根布局则是 DecorView,DecorView 的存在使得视图树有了一个统一的容器,有利于统一系统的主题样式并对所有 childView 进行统一管理。Activity 通过 Window 和视图树进行交互,将具体的视图树处理逻辑抽取给 PhoneWindow 实现,减轻了自身负担。PhoneWindow 拿到 DecorView 后,又通过 ViewRootImpl 来对 DecorView 进行管理,由其来完成整个视图树的 Measure、Layout、Draw 流程。当整个视图树绘制完成后,就将 DecorView 提交给 WindowManager,从而将 Activity 显示到屏幕上


九、一个 Demo


这里我也提供一个自定义 Window 的 Demo,实现了基本的拖拽移动和点击事件,代码点击这里:AndroidOpenSourceDemo



十、一文系列


最近比较倾向于只用一篇文章来写一个知识点,也懒得总是想文章标题,就一直沿用一开始用的一文读懂XXX,写着写着也攒了蛮多篇文章了,之前也已经写了几篇关于 View 系统的文章,希望对你有所帮助 😇😇


作者:业志陈
链接:https://juejin.cn/post/6942303848996274213
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一文读懂 View & Window 机制(一)

Android 系统中,Window 在代码层次上是一个抽象类,在概念上表示的是一个窗口。Android 中所有的视图都是通过 Window 来呈现的,例如 Activity、Dialog 和 Toast 等,它们实际上都是挂载在 Window 上的。大部分情...
继续阅读 »

Android 系统中,Window 在代码层次上是一个抽象类,在概念上表示的是一个窗口。Android 中所有的视图都是通过 Window 来呈现的,例如 Activity、Dialog 和 Toast 等,它们实际上都是挂载在 Window 上的。大部分情况下应用层开发者很少需要来和 Window 打交道,Activity 已经隐藏了 Window 的具体实现逻辑了,但我觉得来了解 Window 机制的一个比较大的好处是可以加深我们对 View 绘制流程以及事件分发机制的了解,这两个操作就涉及到我们的日常开发了,实现自定义 View 和解决 View 的滑动冲突时都需要我们掌握这方面的知识点,而这两个操作和 Window 机制有很大的关联。视图树只有被挂载到 Window 后才会触发视图树的绘制流程,之后视图树才有机会接收到用户的触摸事件。所以说,视图树被挂载到了 Window 上是 Activity 和 Dialog 等视图能够展示到屏幕上且和用户做交互的前置条件


本文就以 Activity 为例子,展开讲解 Activity 是如何挂载到 Window 上的,基于 Android API 30 进行分析,希望对你有所帮助 😇😇


一、Window


Window 存在的意义是什么呢?


大部分情况下,用户都是在和应用的 Activity 做交互,应用在 Activity 上接收用户的输入并在 Activity 上向用户做出交互反馈。例如,在 Activity 中显示了一个 Button,当用户点击后就会触发 OnClickListener,这个过程中用户就是在和 Activity 中的视图树做交互,此时还没有什么问题。可是,当需要在 Activity 上弹出 Dialog 时,系统需要确保 Dialog 是会覆盖在 Activity 之上的,有触摸事件时也需要确保 Dialog 是先于 Activity 接收到的;当启动一个新的 Activity 时又需要覆盖住上一个 Activity 显示的 Dialog;在弹出 Toast 时,又需要确保 Toast 是覆盖在 Dialog 之上的


这种种要求就涉及到了一个层次管理问题,系统需要对当前屏幕上显示的多个视图树进行统一管理,这样才能来决定不同视图树的显示层次以及在接收触摸事件时的优先级。系统就通过 Window 这个概念来实现上述目的


想要在屏幕上显示一个 Window 并不算多复杂,代码大致如下所示


	private val windowManager by lazy {
context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}

private val floatBallView by lazy {
FloatBallView(context)
}

private val floatBallWindowParams: WindowManager.LayoutParams by lazy {
WindowManager.LayoutParams().apply {
width = FloatBallView.VIEW_WIDTH
height = FloatBallView.VIEW_HEIGHT
gravity = Gravity.START or Gravity.CENTER_VERTICAL
flags =
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
} else {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
}
}

fun showFloatBall() {
windowManager.addView(floatBallView, floatBallWindowParams)
}
复制代码

显示一个 Window 最基本的操作流程有:



  1. 声明希望显示的 View,即本例子中的 floatBallView,其承载了我们希望用户看到的视图界面

  2. 声明 View 的位置参数和交互逻辑,即本例子中的 floatBallWindowParams,其规定了 floatBallView 在屏幕上的位置,以及和用户之间的交互逻辑

  3. 通过 WindowManager 来添加 floatBallView,从而将 floatBallView 挂载到 Window 上,WindowManager 是外界访问 Window 的入口


当中,WindowManager.LayoutParams 的 flags 属性就用于控制 Window 的显示特性和交互逻辑,常见的有以下几个:




  1. WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE。表示当前 Window 不需要获取焦点,也不需要接收各种按键输入事件,按键事件会直接传递给下层具有焦点的 Window




  2. WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL。表示当前 Window 区域的单击事件希望自己处理,其它区域的事件则传递给其它 Window




  3. WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED。表示当前 Window 希望显示在锁屏界面




此外,WindowManager.LayoutParams 的 type 属性就用于表示 Window 的类型。Window 有三种类型:应用 Window、子 Window、系统 Window。应用类Window 对应 Activity。子 Window 具有依赖关系,不能单独存在,需要附属在特定的父 Window 之中,比如 Dialog 就是一个子 Window。系统 Window 是需要声明权限才能创建的 Window,比如 Toast 和 statusBar 都是系统 Window


从这也可以看出,系统 Window 是处于最顶层的,所以说 type 属性也用于控制 Window 的显示层级,显示层级高的 Window 就会覆盖在显示层级低的 Window 之上。应用 Window 的层级范围是 1~99,子 Window 的层级范围是 1000~1999,系统 Window 的层级范围是 2000~2999。如果想要让我们创建的 Window 位于其它 Window 之上,那么就需要使用比较大的层级值了,但想要显示自定义的系统级 Window 的话就必须向系统动态申请权限


WindowManager.LayoutParams 内就声明了这些层级值,我们可以择需选取。例如,系统状态栏本身也是一个 Window,其 type 值就是 TYPE_STATUS_BAR


    public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {

public int type;

//应用 Window 的开始值
public static final int FIRST_APPLICATION_WINDOW = 1;
//应用 Window 的结束值
public static final int LAST_APPLICATION_WINDOW = 99;

//子 Window 的开始值
public static final int FIRST_SUB_WINDOW = 1000;
//子 Window 的结束值
public static final int LAST_SUB_WINDOW = 1999;

//系统 Window 的开始值
public static final int FIRST_SYSTEM_WINDOW = 2000;
//系统状态栏
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
//系统 Window 的结束值
public static final int LAST_SYSTEM_WINDOW = 2999;

}
复制代码

二、WindowManager


每个 Window 都会关联一个 View,想要显示 Window 也离不开 WindowManager,WindowManager 就提供了对 View 进行操作的能力。WindowManager 本身是一个接口,其又继承了另一个接口 ViewManager,WindowManager 最基本的三种操作行为就由 ViewManager 来定义,即添加 View、更新 View、移除 View


public interface ViewManager {
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
复制代码

WindowManager 的实现类是 WindowManagerImpl,其三种基本的操作行为都交由了 WindowManagerGlobal 去实现,这里使用到了桥接模式


public final class WindowManagerImpl implements WindowManager {

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}

}
复制代码

这里主要看下 WindowManagerGlobal 是如何实现 addView 方法的即可


首先,WindowManagerGlobal 会对入参参数进行校验,并对 LayoutParams 做下参数调整。例如,如果当前要显示的是子 Window 的话,那么就需要使其 LayoutParams 遵循父 Window 的要求才行


	public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
···
}
复制代码

之后就会为当前的视图树(即 view)构建一个关联的 ViewRootImpl 对象,通过 ViewRootImpl 来绘制视图树并完成 Window 的添加过程。ViewRootImpl 的 setView方法会触发启动整个视图树的绘制流程,即完成视图树的 Measure、Layout、Draw 流程,具体流程可以看我的另一篇文章:一文读懂 View 的 Measure、Layout、Draw 流程


	public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
···

ViewRootImpl root;
View panelParentView = null;

···

root = new ViewRootImpl (view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

// do this last because it fires off messages to start doing things
try {
//启动和 view 关联的整个视图树的绘制流程
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
复制代码

ViewRootImpl 内部最终会通过 WindowSession 来完成 Window 的添加过程,mWindowSession 是一个Binder对象,真正的实现类是 Session,也就是说,Window 的添加过程涉及到了 IPC 调用。后面就比较复杂了,能力有限就不继续看下去了


        mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
adjustLayoutParamsForCompatibility(mWindowAttributes);
res = mWindowSession.addToDisplayAsUser(
mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls
);
setFrame(mTmpFrame);
复制代码

需要注意的是,这里所讲的视图树代表的是很多种不同的视图形式。我们知道,在启动一个 Activity 或者显示一个 Dialog 的时候,都需要为它们指定一个布局文件,布局文件会通过 LayoutInflater 加载映射为一个具体的 View 对象,即最终 Activity 和 Dialog 都会被映射为一个 View 类型的视图树,它们都会通过 WindowManager 的 addView 方法来显示到屏幕上,WindowManager 对于 Activity 和 Dialog 来说具有统一的操作行为入口


三、Activity & Window


这里就以 Activity 为例子来展开讲解 Window 相关的知识点,所以也需要先对 Activity 的组成结构做个大致的介绍。Activity 和 Window 之间的关系可以用以下图片来表示





  1. 每个 Activity 均包含一个 Window 对象,即 Activity 和 Window 是一对一的关系




  2. Window 是一个抽象类,其唯一的实现类是 PhoneWindow




  3. PhoneWindow 内部包含一个 DecorView,DecorView 是 FrameLayout 的子类,其内部包含一个 LinearLayout,LinearLayout 中又包含两个自上而下的 childView,即 ActionBar 和 ContentParent。我们平时在 Activity 中调用的 setContentView 方法实际上就是在向 ContentParent 执行 addView 操作




Window 这个抽象类里定义了多个和 UI 操作相关的方法,我们平时在 Activity 中调用的setContentViewfindViewById方法都会被转交由 Window 来实现,Window 是 Activity 和视图树系统交互的入口。例如,其 getDecorView() 方法就用于获取内嵌的 DecorView,findViewById() 方法就会将具体逻辑转交由 DecorView 来实现,因为 DecorView 才是真正包含 contentView 的容器类


public abstract class Window {

public Window(Context context) {
mContext = context;
mFeatures = mLocalFeatures = getDefaultFeatures(context);
}

public abstract void setContentView(@LayoutRes int layoutResID);

@Nullable
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}

public abstract void setTitle(CharSequence title);

public abstract @NonNull View getDecorView();

···

}
复制代码

四、Activity # setContentView


每个 Activity 内部都包含一个 Window 对象 mWindow,在 attach 方法中完成初始化,这说明 Activity 和 Window 是一对一的关系。mWindow 对象对应的是 PhoneWindow 类,这也是 Window 的唯一实现类


public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {

@UnsupportedAppUsage
private Window mWindow;

@UnsupportedAppUsage
private WindowManager mWindowManager;

@UnsupportedAppUsage
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

//初始化 mWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
···
}

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

}
复制代码

Activity 的attach 方法又是在 ActivityThread 的 performLaunchActivity 方法中被调用的,在通过反射生成 Activity 实例后就会调用attach 方法,且可以看到该方法的调用时机是早于 Activity 的 onCreate 方法的。所以说,在生成 Activity 实例后不久其 Window 对象就已经被初始化了,而且早于各个生命周期回调函数


	private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
···
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}

···

activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);

···

if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
return activity;
}
复制代码

此外,从 Activity 的setContentView 的方法签名来看,具体逻辑都交由了 Window 的同名方法来实现,传入的 layoutResID 就是我们希望在屏幕上呈现的布局,那么 PhoneWindow 自然就需要去加载该布局文件生成对应的 View。而为了能够有一个对 View 进行统一管理的入口,View 应该要包含在一个指定的 ViewGroup 中才行,该 ViewGroup 指的就是 DecorView


下面就再来看下 PhoneWindow 是如何处理这一个流程的


五、PhoneWindow # setContentView


PhoneWindow 的 setContentView 方法的逻辑可以总结为:



  1. PhoneWindow 内部包含一个 DecorView 对象 mDecor。DecorView 是 FrameLayout 的子类,其内部包含两个我们经常会接触到的 childView:actionBar 和 contentParent,actionBar 即 Activity 的标题栏,contentParent 即 Activity 的视图内容容器

  2. 如果 mContentParent 为 null 的话则调用 installDecor() 方法来初始化 DecorView,从而同时初始化 mContentParent;不为 null 的话则移除 mContentParent 的所有 childView,为 layoutResID 腾出位置(不考虑转场动画,实际上最终的操作都一样)

  3. 通过LayoutInflater.inflate生成 layoutResID 对应的 View,并将其添加到 mContentParent 中,从而将我们的目标视图挂载到一个统一的容器中(不考虑转场动画,实际上最终的操作都一样)

  4. 当 ContentView 添加完毕后会回调 Callback.onContentChanged 方法,我们可以通过重写 Activity 的该方法从而得到布局内容改变的通知


所以说,Activity 的 setContentView 方法实际上就是在向 DecorView 的 mContentParent 执行 addView 操作,所以该方法才叫setContentView而非setView


public class PhoneWindow extends Window implements MenuBuilder.Callback {

private DecorView mDecor;

ViewGroup mContentParent;

@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将 layoutResID 对应的 View 添加到 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
}

mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回调通知 contentView 发生变化了
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);

// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeFrameworkOptionalFitsSystemWindows();

final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);

if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
···
} else {
···
}
···
}
}

}
复制代码

mContentParent 通过 generateLayout 方法来完成初始化,该方法主要完成的操作有两个:



  1. 读取我们为 Activity 设置的 theme 属性,以此配置基础的 UI 风格。例如,如果我们设置了 <item name="windowNoTitle">true</item>的话,那么就会执行 requestFeature(FEATURE_NO_TITLE) 来隐藏标题栏

  2. 根据 features 来选择合适的布局文件,得到 layoutResource。之所以会有多种布局文件,是因为不同的 Activity 会有不同的显示要求,有的要求显示 title,有的要求显示 leftIcon,而有的可能全都不需要,为了避免控件冗余就需要来选择合适的布局文件。而虽然每种布局文件结构上略有不同,但均会包含一个 ID 名为content的 FrameLayout,mContentParent 就对应该 FrameLayout。DecorView 会拿到 layoutResource 并生成对应的 View 对象(对应 DecorView 中的 mContentRoot),并将其添加为mContentParent的 childView


	protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.

TypedArray a = getWindowStyle();

···

//第一步
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}

···

// Inflate the window decor.

//第二步
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
···
//交由 DecorView 去生成 layoutResource 对应的 View
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

//正常来说每种 layoutResource 都会包含一个 ID 为 ID_ANDROID_CONTENT 的 ViewGroup
//如果找不到的话就直接抛出异常
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}

···
return contentParent;
}
复制代码


作者:业志陈
链接:https://juejin.cn/post/6942303848996274213
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS -AFN实现原理&&面试

AFNetworking是封装的NSURLSession的网络请求。AFNetworking由五个模块组成:分别由NSURLSession,Security,Reachability,Serialization,UIKit五部分组成NSURLSession:网...
继续阅读 »

AFNetworking是封装的NSURLSession的网络请求。

AFNetworking由五个模块组成:

分别由NSURLSession,Security,Reachability,Serialization,UIKit五部分组成

NSURLSession:网络通信模块(核心模块) 对应 AFNetworking中的 AFURLSessionManager和对HTTP协议进行特化处理的AFHTTPSessionManager,AFHTTPSessionManager是继承于AFURLSessionmanager的
Security:网络通讯安全策略模块  对应 AFSecurityPolicy
Reachability:网络状态监听模块 对应AFNetworkReachabilityManager
Seriaalization:网络通信信息序列化、反序列化模块 对应 AFURLResponseSerialization
UIKit:对于IOSUIKit的扩展库

网络请求的过程:

创建NSURLSessionConfig对象--用创建的config对象配置初始化NSURLSession--创建NSURLSessionTask对象并resume执行,用delegate或者block回调返回数据。

AFURLSessionManager封装了上述网络交互功能

AFURLSessionManager请求过程

1.初始化AFURLSessionManager。

2.获取AFURLSessionManager的Task对象

3.启动Task

AFURLSessionManager会为每一个Task创建一个AFURLSessionmanagerTaskDelegate对象,manager会让其处理各个Task的具体事务,从而实现了manager对多个Task的管理

初始化好manager后,获取一个网络请求的Task,生成一个Task对象,并创建了一个AFURLSessionmanagerTaskDelegate并将其关联,设置Task的上传和下载delegate,通过KVO监听download进度和upload进度

NSURLSessionDelegate的响应

因为AFURLSessionmanager所管理的AFURLSession的delegate指向其自身,因此所有的NSURLSessiondelegate的回调地址都是AFURLSessionmanager,而AFURLSessionmanager又会根据是否需要具体处理会将AFdelegate所响应的delegate,传递到对应的AFdelegate去

面试相关:

AFN调用流程分析:

AFHTTPSessionManager: 发起网络请求(例如GET);
AFHTTPSessionManager内部调用dataTaskWithHTTPMethod:方法(内部处理requestSerializer);
dataTaskWithHTTPMethod内部调用父类AFURLSessionManager的dataTaskWithRequest: uploadProgress: downloadProgress: completionHandler方法;
AFURLSessionManager中的dataTaskWithRequest方法内部设置全局session和创建task;
AFURLSessionManager中的dataTaskWithRequest方法内部给task设置delegate(AFURLSessionManagerTaskDelegate);
taskDelegate代理的初始化: 绑定task / 存储task下载的数据 / 下载或上传进度 / 进度与task同步(KVO)
task对应的AFURLSessionManagerTaskDelegate实现对进度处理、Block调用、Task完成返回数据的拼装的功能等;
setDelegate: forTask: 加锁设置通过一个字典处理Task与之代理方法关联; 添加对Task开始、重启、挂起状态的通知的接收.
[downloadTask resume]后执行开始, 走代理回调方法(内部其实是NSURLSession的各种代理的实现);
task完成后走URLSession: task: didCompleteWithError: 回调对返回的数据进行封装;
同时移除对应的task; removeDelegateForTask: 加锁移除8中的字典和通知;

AFN请求过程梳理

首先我们是初始化了AFHTTPSessionManager类(往往创建单例)初始化时候指定请求回调的代理是父类(AFURLSessionManager)。之后当我们发出一个请求后,先创建一个AFURLSessionManagerTaskDelegate对象来保存请求结果回调。并把该对象放到一个全局字典中来保存(以task.taskIdentifier为key),再启动请求。当AFURLSessionManager类收到了请求结果后根据task.taskIdentifier从全局字典中取出当前请求的AFURLSessionManagerTaskDelegate对象。然后调用AFURLSessionManagerTaskDelegate的对象方法处理请求,完成回调。之后再从全局字典中移除该AFURLSessionManagerTaskDelegate对象。

AFN是怎样来解决循环引用的

首先我们用AFN时候往往是用单例,因此调用类不会直接持有该AFHTTPSessionManager对象。
该AFHTTPSessionManager对象持有block,该AFHTTPSessionManager对象持有全局字典,该全局字典持有AFURLSessionManagerTaskDelegate对象,该AFURLSessionManagerTaskDelegate对象持有block,这是一个循环引用。
当AFURLSessionManagerTaskDelegate对象block进行回调后,从全局字典中移除该对象。从而打破引用环。

1、AFN2.x为什么添加一条常驻线程?

AFN2.0里面把每一个网络请求的发起和解析都放在了一个线程里执行。正常来说,一个线程执行完任务后就退出了。开启runloop是为了防止线程退出。一方面避免每次请求都要创建新的线程;另一方面,因为connection的请求是异步的,如果不开启runloop,线程执行完代码后不会等待网络请求完的回调就退出了,这会导致网络回调的代理方法不执行。
这是一个单例,用NSThread创建了一个线程,并且为这个线程添加了一个runloop,并且加了一个NSMachPort,来防止runloop直接退出。 这条线程就是AF用来发起网络请求,并且接受网络请求回调的线程,仅仅就这一条线程

2、AFN3.x为什么不再需要常驻线程?

NSURLConnection的一大痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。
苹果也是明白了这一痛点,从iOS9.0开始 deprecated 了NSURLConnection。 替代方案就是NSURLSession。

3、为什么AF3.0中需要设置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0却不需要?

功能不一样:AF3.0的operationQueue是用来接收NSURLSessionDelegate回调的,鉴于一些多线程数据访问的安全性考虑,设置了maxConcurrentOperationCount = 1来达到串行回调的效果。
而AF2.0的operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。

AFNetworking3.0

在AFNetworking 3.0之前,底层是通过封装NSURLConnection来实现的。
在AFNetworking 3.0之后,也就是在iOS 9.0 之后,NSURLConnection被弃用,苹果推荐使用NSURLSession来管理网络请求,所以AFNetworking 3.0之后,底层是通过封装NSURLSession来实现的。

从AFNetworking 3.0中之后,下面三个方法被弃用了。
AFURLConnectionOperation
AFHTTPRequestOperation
AFHTTPRequestOperationManager

依次被下面三个类代替了,同时请求方法也跟着改变了,所以AFNetworking 3.0以后发生了很大的变化。
AFURLSessionManager
AFHTTPSessionManager
AFNetworkReachabilityManager

参考链接:https://blog.csdn.net/songzhuo1991/article/details/104883981

参考链接:https://blog.csdn.net/weixin_39638526/article/details/111748124

收起阅读 »

Android修炼系列(三),一个对象在JVM的生死存亡

当我们new了一个对象,会发生什么呢?来段代码: public class Tested { public static int T = 10; public int c = 1; } 复制代码 类初始化 在编译期,编译器会将 Tested.j...
继续阅读 »

当我们new了一个对象,会发生什么呢?来段代码:


public class Tested {
public static int T = 10;
public int c = 1;
}
复制代码

类初始化


在编译期,编译器会将 Tested.java类转换成 Tested.class 字节码文件。当虚拟机接收到new 字节码指令时,如果此时类还未被初始化,则虚拟机会先进行类的初始化过程。



在类加载完成后。虚拟机会为new Tested() 的Tested对象,在java堆中分配内存。而对象所需要的内存大小在类加载完成后就被确定了。


指针碰撞


如果 java 中的内存是规整的,即使用过的放在一边,空闲的在另一边,中间放着指针作为分界点的指示器。那所分配的内存就仅仅是将指针像空闲空间挪动一段与对象大小相等的距离。这种方式内称为指针碰撞。


空闲列表


如果 java 中的内存是不工整的,使用过的和空闲的内存相互交错,那么虚拟机就必须维护一个列表记录哪些内存是可用的。在分配的时候从列表中找到一块足够大的空间给对象示例,并更新表的记录。这种分配方式称为空闲列表。


对象初始化


当我们的对象内存被分配完毕后,虚拟机就会对对象进行初始化操作。



此时Tested 对象在我们眼里就算出生了,在虚拟机眼里就是真正可用的了。可对象的生命并不是无穷的,它也会经历自己的死亡。


可达性分析


在主流实现中,我们通过可达性分析来判断一个对象是否存活。实现思路是:通过一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始像下搜索,搜索所走的路径被称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。见图:


在这里插入图片描述


即使 Obj5 与 Obj4 由于与 GC Roots 没有引用链相连,所以我们称 GC Roots 到对象 Obj4 和 Obj5 不可达。所以 Obj4 和 Obj5 就是可回收的。


既然Obj4 和 Obj5 是可回收的,那么是否一定会被回收呢?不一定。此时虚拟机会进行第一次的标记过程。因为 java 内能够重写 finalize() 方法(在这里只是分析特例,不推荐在任何情况下使用此方法),当对象重写了此方法,并且 finalize() 方法还未被虚拟机调用,那么虚拟机就会将此对象放入一个专门的F-Queue队列,由一个单独的 Finalizer 线程去执行它,如果队列中对象的 finalize() 方法在虚拟机第二次标记之前执行,并在此次执行过程中又将自己与GC Roots 引用链相连,那么虚拟机在进行第二次标记时,就会将该对象从 F-Queue队列移除,否则就宣告该对象死亡。注意:finalize() 方法只会被执行一次,所以一个对象一生只有一次机会进入F-Queue队列,有机会逃脱本此死亡。


如果对象已经宣告死亡了,那么虚拟机怎么来回收它吗?


标记-清除算法


这是最基础的收集算法,主要分为标记和清除两个阶段。首先标记出所以需要回收的对象,在标记完成后统一回收所有被标记的对象。可以参考上面的空闲列表。其有两点不足:


a. 效率问题,标记和清除两个过程效率都不高。


b. 空间问题,因为堆中的内存不是规整的,已使用的和空闲的内存相互交错,这也就导致了每次GC回收后,产生大量的内存碎片,而当再次分配一个大对象时,如果无法找到足够的连续内存,又会再此触发GC回收。


复制算法


复制算法是将堆内存分成大小相等的两块,每次只使用其中一块,这样内存就是规整的了,参考指针碰撞。每当一块内存使用完了,就将该块内存中存活的对象复制到另一边,随后将该块内存一次清理掉。


现在的虚拟机都采用这种方式来回收新生代,只是并不是按照1:1的比例来划分内存,而是将内存分为一块较大的 Eden 空间,和两块较小的 Survivor 空间(HotSpot虚拟机默认Eden:Survivor = 8 :1)。每次只使用 Eden 和 其中一块 Survivor 空间,当回收时,将 Eden 空间和当前正使用的 Survivor 空间内存活的对象复制到另一块空闲的 Survivor空间,随后清空 Eden 和 刚才用过的 Survivor 内存。


注意:由于我们无法保证每次 存活的对象所占内存一直都不大于 Survivor 内存值,所以就会有溢出风险。所以在 分代收集算法 中,虚拟机会将内存先划分为一块新生代内存和一块为老年代内存。而在新生代内存中,会采用这种8:1:1的内存分配方式,如果溢出了,就将该情况下的存活对象全部放在老年代内存里,说白了就是一种兜底策略。这里要注意的是,不是溢出的那部分,而是全部的存活对象。


标记-整理算法


标记-整理算法中的标记过程,与标记-清除算法中的标记过程一样,不同的是,当标记完成并清理回收完对象后,会将当前不连续的碎片内存就行整理,即存活的对象都移到一端,来保证接下来要分配的内存的规整性。我们的 分代收集算法 中的老年代内存块,就是采用的该算法(当然也可以是标记-清除算法,不同虚拟机的策略不同)。所以就不再对分代收集算法就行赘述了。



好了,本文到这里,关于“对象”的生命周期的讲解就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考
1、周志明,深入理解JAVA虚拟机:机械工业出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6935481800365981727
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android修炼系列(二),Class类加载过程与类加载器

在说类加载器和双亲委派模型之前,我们先来梳理下Class类文件的加载过程,JAVA虚拟机为了保证 实现语言的无关性,是将虚拟机只与“Class 文件”字节码 这种特定形式的二进制文件格式 相关联,而不是与实现语言绑定。类加载过程Class类...
继续阅读 »


在说类加载器和双亲委派模型之前,我们先来梳理下Class类文件的加载过程,JAVA虚拟机为了保证 实现语言的无关性,是将虚拟机只与“Class 文件”字节码 这种特定形式的二进制文件格式 相关联,而不是与实现语言绑定。

类加载过程

Class类从被加载到虚拟机内存开始,到卸载出内存为止,其生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中加载过程见下:

在这里插入图片描述

加载阶段

加载阶段做了什么?过程见下图。其中类的全限定名是Class文件(JAVA由编译器自动生成)内的代表常量池内的16进制值所代表的特定符号引用。因为Class文件格式有其自己的一套规范,如第1-4字节代表魔数,第5-6字节代表次版本,第7-8字节代表主版本号等等。

说白了就是,虚拟机不关心我们的这种“特定二进制流”从哪里来的,从本地加载也好,从网上下载的也罢,都没关系。虚拟机要做的就是将该二进制流写在自己的内存中并生成相应的Class对象(并不是在堆中)。在这个阶段,我们能够通过我们自定义类加载器来控制二进制流的获取方式。

验证阶段

验证阶段,正因为加载阶段虚拟机不介意二进制的来源,所以就可能存在着影响虚拟机正常运行的安全隐患。所以虚拟机对于该二进制流的校验工作非常重要。校验方式包括但不限于:

准备阶段

准备阶段在此阶段将正式为类变量分配内存并设置变量的初始化值。注意的是,类变量是指 static 的静态变量,是分配在方法区之中的,而不像对象变量,分配在堆中。还有一点需要注意,final 常量在此阶段就已经被赋值了。如下:

    public static int SIZE = 10; // 初始化值 == 0
public static final int SIZE = 10; // 初始化值 == 10
复制代码

解析阶段

解析阶段是将常量池内的符号引用替换为直接引用的过程。符号引用就是上文说的Class文件格式标准所规定的特定字面量,而直接引用就是我们说的指针,内存引用等概念

初始化阶段

到了初始化阶段,就开始真正执行我们的字节码程序了。也可以理解成:类初始化阶段就是虚拟机内部执行类构造 < clinit >() 方法的过程。注意,这个类构造方法可不是虚拟机内部生成的,而是我们的编译器自动生成的,是编译器自动收集类中的所有类变量的 赋值动作 和静态语句块(static{}块)中的语句合并产生的,具体分析见下。

注意,这里说的是类变量赋值动作,即static 并且具有赋值操作,如果无赋值操作,那么在准备阶段进行的方法区初始化就算完成了。为何还要加上static{} 呢?我们可以把static{} 理解成:是由多个静态初始化动作组织成的一个特殊的“静态子句”,与其他的静态初始化动作一样。这也是为何 static {} 只会执行一遍并在对象构造方法之前执行的原因。如下代码:

public class Tested {
public static int T;
// public static int V; // 无赋值,不在类构造中再次初始化
public int c = 1; // 不会在类构造中

static {
T = 10;
}
}
复制代码

还有一点,编辑器收集类变量的顺序,也就是虚拟机在此初始化阶段的执行顺序,这个顺序就是变量在类中语句定义的先后顺序,如上面的:语句 2 : T 在 6 : T 之前,这是两个独立的语句。类构造< clinit >的其他特点如下:

编译期的< clinit >

我们将流程回溯到编译期阶段,以刚刚的Tested 类代码为例。通过 javap -c /Tested.class (注意:/../Tested 绝对路径),获取Class文件:

public class com.tencent.lo.Tested {
public static int T;

public int c;

public com.tencent.lo.Tested();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field c:I
9: return

static {};
Code:
0: bipush 10
2: putstatic #3 // Field T:I
5: return
}
复制代码

在Class 文件中我们能很明显的看到 invokespecial 对应的对象构造 "< init >" : () V ,那为什么没有看到< clinit > 类构造方法呢?其实上面的 static {} 就是。我们来看下OpenJDK源码 Constants接口,此接口定义了在编译器中所用到的常量,这是一个自动生成的类。

public interface Constants extends RuntimeConstants {
public static final boolean tracing = true;

Identifier idClassInit = Identifier.lookup("<clinit>");
Identifier idInit = Identifier.lookup("<init>");
}
复制代码

MemberDefinition类 中,判断是否为类构造器字符:

    public final boolean isInitializer() {
return getName().equals(idClassInit); // 类构造
}
public final boolean isConstructor() {
return getName().equals(idInit); // 对象构造
}
复制代码

而在MemberDefinition 的 toString() 方法中,我们能够看到,当类构造时,会输出特定字符,而不会像对象构造那样输出规范的字符串。

    public String toString() {
Identifier name = getClassDefinition().getName();
if (isInitializer()) { // 类构造
return isStatic() ? "static {}" : "instance {}";
} else if (isConstructor()) { // 对象构造
StringBuffer buf = new StringBuffer();
buf.append(name);
buf.append('(');
Type argTypes[] = getType().getArgumentTypes();
for (int i = 0 ; i < argTypes.length ; i++) {
if (i > 0) {
buf.append(',');
}
buf.append(argTypes[i].toString());
}
buf.append(')');
return buf.toString();
} else if (isInnerClass()) {
return getInnerClass().toString();
}
return type.typeString(getName().toString());
}
复制代码

类加载器

“虚拟机将类加载阶段中的“通过一个全限定名来获取描述此类的二进制字节流”这个动作放到了外部来实现,以便开发者可以自己决定如何获取所需的类文件,而实现这个动作的代码模块就被称为类加载器。对于任意一个类来说,只有在类加载器相同的情况下比较两者是否相同才有意义,否则即使是同个文件,在不同加载器下,在虚拟机看来其仍然是不同的,是两个独立的类。我们可以将类加载器分为三类”:

双亲委派

而所谓的双亲委派模型就是:“如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把加载的操作委托给父类加载器去完成,每一层次加载器都是如此,因此所有的加载请求都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围没有找到所需的类,因为上面所说的启动类加载器和扩展类加载器,只能加载特定目录之下的,或被-x参数所指定的类库),子类才会尝试自己加载”。注意这里说的父类只是形容层次结构,其并不是直接继承关系,而是通过组合方式来复用父类的加载器的。

在这里插入图片描述

“双亲委派的好处就是,使加载器也具备了优先级的层次结构。例如,java.lang.Object存放在< JAVA_HOME>\lib 下的rt.jar包内,无论哪个类加载器要加载这个类,最终都会委派给最顶层的启动类加载器,所以保证了Object类在各类加载器环境中都是同一个类。相反,如果没有双亲委派模型,如果用户编写了一个java.lang.Object类,并放在程序的ClassPath下,那么系统将会出现多个不同的Object类”。

为何?因为每个加载器各自为政,不会委托给父构造器,如上面所说,只要加载器不同,即使类Class文件相同,其也是独立的。

试想如果自己在项目中编写了一个java.lang.Object 类(当然不能放入rt.jar类库中替换掉同名Object文件,这样做没有意义,如果虚拟机加载校验能通过的话,只是相当于改了源码嘛),我们通过自定义的构造器来加载这个类可以吗?理论上来说,虽然这两个类都是java.lang.Object,但由于构造器不同,对于虚拟机来说这是不同的Class文件,当然可以。但是实际上呢?来段代码见下:

    public void loadPathName(String classPath) throws ClassNotFoundException {
new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
InputStream is = getClass().getResourceAsStream(name);
if (is == null)
return super.loadClass(name);
byte[] b;
try {
b = new byte[is.available()];
is.read(b);
} catch (Exception e) {
return super.loadClass(name);
}
return defineClass(name, b, 0, b.length);
}
}.loadClass(classPath);
}
复制代码

实际的执行逻辑是 defineClass 方法。可以发现,自定义加载器是无法加载以 java. 开头的系统类的。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError {

protectionDomain = preDefineClass(name, protectionDomain);
... // 略

return c;
}

private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// 在这里能看到系统类,自定义的加载器是不能加载的
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
... // 略

return pd;
}
复制代码

如果你用AS直接查看,你会发现,defineClass 内部是没有具体实现的,源码见下。可这并不代表android 的 defineClass 方法实现与 java 不同,因为都是引用的 java.lang 包下的ClassLoader 类,逻辑肯定都是一样的。之所以看到的源码不一样,这是由于SDK和JAVA源码包的区别导致的。SDK内的源码是谷歌提供给我们方便开发查看的,并不完全等同于源码。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
throw new UnsupportedOperationException("can't load this type of class file");
}
复制代码

好了,本文到这里就结束了,关于类加载过程的讲解也应该够用了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。

参考 1、周志明,深入理解JAVA虚拟机:机械工业出版社

收起阅读 »

iOS -YYModel的底层实现原理

一. YYModel逻辑结构 实际使用时,需要对其遍历,取出容器中得字典,然后继续字典转模型(YYModel的核心是通过runtime获取结构体中得Ivars的值,将此值定义为key,然后给key赋value值,所以我们需要自己遍历容器(N...
继续阅读 »

一. YYModel逻辑结构 


实际使用时,需要对其遍历,取出容器中得字典,然后继续字典转模型
(YYModel的核心是通过runtime获取结构体中得Ivars的值,将此值定义为key,然后给key赋value值,所以我们需要自己遍历容器(NSArray,NSSet,NSDictionary),获取每一个值,然后KVC进行处理)。

1.Model 属性名和 JSON 中的 Key 不相同

// JSON:
{
"n":"Harry Pottery",
"p": 256,
"ext" : {
"desc" : "A book written by J.K.Rowing."
},
"ID" : 100010
}

// Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"page" : @"p",
@"desc" : @"ext.desc",
@"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end

你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。

在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。

2.Model 包含其他 Model

// JSON
{
"author":{
"name":"J.K.Rowling",
"birthday":"1965-07-31T00:00:00+0000"
},
"name":"Harry Potter",
"pages":256
}

// Model: 什么都不用做,转换会自动完成
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Author
@end

@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author; //Book 包含 Author 属性
@end
@implementation Book
@end

3.容器类属性

@class Shadow, Border, Attachment;

@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array
@property NSSet *borders; //Set
@property NSMutableDictionary *attachments; //Dict
@end

@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{@"shadows" : [Shadow class],
@"borders" : Border.class,
@"attachments" : @"Attachment" };
}
@end

在实际使用过过程中,[Shadow class]Border.class@"Attachment"没有明显的区别。
这里仅仅是创建作者有说明,实际使用时,需要对其遍历,取出容器中得字典,然后继续字典转模型。

YYModel的核心是通过runtime获取结构体中得Ivars的值,将此值定义为key,然后给keyvalue值,所以我们需要自己遍历容器(NSArrayNSSetNSDictionary),获取每一个值,然后KVC)。

具体的代码实现如下:

NSDictionary *json =[self getJsonWithJsonName:@"ContainerModel"];
ContainerModel *containModel = [ContainerModel yy_modelWithDictionary:json];
NSDictionary *dataDict = [containModel valueForKey:@"data"];
//定义数组,接受key为list的数组
self.listArray = [dataDict valueForKey:@"list"];
//遍历数组
[self.listArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSDictionary *listDict = obj;
//获取数组中得字典
List *listModel = [List yy_modelWithDictionary:listDict];
//获取count 和 id
NSString *count = [listModel valueForKey:@"count"];
NSString *id = [listModel valueForKey:@"id"];

4.黑名单与白名单

@interface User
@property NSString *name;
@property NSUInteger age;
@end

@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
return @[@"name"];
}
@end

5.数据校验与自定义转换

实际这个分类的目的比较简单和明确。
就是对判断是否为时间戳,然后对时间戳进行处理,调用
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
获取时间。

// JSON:
{
"name":"Harry",
"timestamp" : 1445534567 //时间戳
}

// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end

@implementation User
// JSON 转为 Model 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
NSNumber *timestamp = dic[@"timestamp"];
if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
return YES;
}

// Model 转为 JSON 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
if (!_createdAt) return NO;
dic[@"timestamp"] = @(n.timeIntervalSince1970);
return YES;
}
@end

  • 需要注意的时,如果用插件,对时间戳类型或默认创建为NSUInteger类型,需要将其更改为NSDate类型。

6.Coding/Copying/hash/equal/description

以下方法都是YYModel的简单封装,实际使用过程和系统方法区别不大。对其感兴趣的可以点进方法内部查看。

@interface YYShadow :NSObject 
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGSize size;
@end

@implementation YYShadow
// 直接添加以下代码即可自动完成
- (void)encodeWithCoder:(NSCoder *)aCoder {
[self yy_modelEncodeWithCoder:aCoder];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
return [self yy_modelInitWithCoder:aDecoder];
}
- (id)copyWithZone:(NSZone *)zone {
return [self yy_modelCopy];
}
- (NSUInteger)hash {
return [self yy_modelHash];
}
- (BOOL)isEqual:(id)object {
return [self yy_modelIsEqual:object];
}
- (NSString *)description {
return [self yy_modelDescription];
}
@end


原文链接:https://blog.csdn.net/huhui168/article/details/80541387

收起阅读 »

Android修炼系列(一),写一篇易懂的动态代理讲解

在说动态代理之前,先来简单看下代理模式。代理是最基本的设计模式之一。它能够插入一个用来替代“实际”对象的“代理”对象,来提供额外的或不同的操作。这些操作通常涉及与“实际”对象的通信,因此“代理”对象通常充当着中间人的角色。 代理模式 代理对象为“实际”对象提供...
继续阅读 »

在说动态代理之前,先来简单看下代理模式。代理是最基本的设计模式之一。它能够插入一个用来替代“实际”对象的“代理”对象,来提供额外的或不同的操作。这些操作通常涉及与“实际”对象的通信,因此“代理”对象通常充当着中间人的角色。


代理模式


代理对象为“实际”对象提供一个替身或占位符以控制对这个“实际”对象的访问。被代理的对象可以是远程的对象,创建开销大的对象或需要安全控制的对象。来看下类图:


代理模式


再来看下类图对应代码,这是IObject接口,真实对象RealObj和代理对象ObjProxy都实现此接口:


/**
* 为实际对象Tested和代理对象TestedProxy提供对外接口
*/
public interface IObject {
void request();
}
复制代码

RealObj是实际处理request() 逻辑的对象,但是出于设计的考量,需要对RealObj内部的方法调用进行控制访问


public class RealObject implements IObject {

@Override
public void request() {
// 模拟一些操作
}
}
复制代码

ObjProxy是RealObj的代理类,其同样实现了IObject接口,所以具有相同的对外方法。客户端与RealObj的所有交互,都必须通过ObjProxy。


public class ObjProxy implements IObject {
IObject realT;

public ObjProxy(IObject t) {
realT = t;
}

@Override
public void request() {
if (isAllow()) realT.request();
}

private boolean isAllow() {
return true;
}
}
复制代码

番外


代理模式和装饰者模式不管是在类图,还是在代码实现上,几乎是一样的,但我们为何还要进行划分呢?其实学设计模式,不能拘泥于格式,不能死记形式,重要的是要理解模式背后的意图,意图只有一个,但实现的形式却可能多种多样。这也就是为何那么多变体依然属于xx设计模式的原因。


代理模式的意图是替代真正的对象以实现访问控制,而装饰者模式的意图是为对象加入额外的行为。


动态代理


Java的动态代理可以动态的创建代理并动态的处理所代理方法的调用,在动态代理上所做的所以调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的策略。类图见下:


动态代理


还以上面的代码为例,这是对外的接口IObject:


public interface IObject {
void request();
}
复制代码

这是 InvocationHandler 的实现类,类图中 Proxy 的方法调用都会被系统传入此类,即 invoke 方法,而 ObjProxyHandler 又持有着 RealObject 实例,所以 ObjProxyHandler 是“真正”对 RealObject 对象进行访问控制的代理类。


public class ObjProxyHandler implements InvocationHandler {
IObject realT;

public ObjProxyHandler(IObject t) {
realT = t;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// request方法时,进行校验
if (method.getName().equals("request") && !isAllow())
return null;
return method.invoke(realT, args);
}

private boolean isAllow() {
return false;
}
}
复制代码

RealObj是实际处理request() 逻辑的对象。


public class RealObject implements IObject {
@Override
public void request() {
// 模拟一些操作
}
}
复制代码

动态代理的使用方法如下:我们通过 Proxy.newProxyInstance 静态方法来创建代理,其参数如下,一个类加载器、一个代理实现的接口列表、一个 InvocationHandler 的接口实现。


    public void startTest() {
IObject proxy = (IObject) Proxy.newProxyInstance(
IObject.class.getClassLoader(),
new Class[]{IObject.class},
new ObjProxyHandler(new RealObject()));
proxy.request(); // ObjProxyHandler的invoke方法会被调用
}
复制代码

Proxy源码


来看下Proxy 源码,当我们 newProxyInstance(...) 时,首先系统会进行判空处理,之后获取我们实际的 Proxy 代理类 Class 对象,再通过一个参数的构造方法生成我们的代理对象 p(p : 返回值),这里能看出来 p 是持有我们的对象 h 的。注意 cons.setAccessible(true) 表示,即使是 cl 是私有构造,也可以获得对象。源码见下:


public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();

/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
...
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
cons.setAccessible(true);
// END Android-removed: Excluded AccessController.doPrivileged call.
}
return cons.newInstance(new Object[]{h});
...
}
复制代码

其中 getProxyClass0(...) 是用来检查并获取实际代理对象的。首先会有一个65535的接口限制检测,随后从代理缓存proxyClassCache 中获取代理类,如果给定的接口不存在,则通过 ProxyClassFactory 新建。见下:


    private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
复制代码

存放代理 Proxy.class 的缓存 proxyClassCache,是一个静态常量,所以在我们类加载时,其就已经被初始化完毕了。见下:


private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
复制代码

Proxy 提供的 getInvocationHandler(Object proxy)方法和 invoke(...) 方法很重要。分别为获取当前代理关联的调用处理器对象 InvocationHandler,并将当前Proxy方法调用 调度给 InvocationHandler。是不是与上面的代理思维很像,至于这两个方法何时被调用的,推测是写在了本地方法内,当我们调用proxy.request 方法时(系统创建Proxy时,会自动 implements 用户传递的接口,可以为多个),系统就会调用Proxy invoke 方法,随后proxy 将方法调用传递给 InvocationHandler。


public static InvocationHandler getInvocationHandler(Object proxy)
throws IllegalArgumentException
{
/*
* Verify that the object is actually a proxy instance.
*/
if (!isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy instance");
}
final Proxy p = (Proxy) proxy;
final InvocationHandler ih = p.h;

return ih;
}

// Android-added: Helper method invoke(Proxy, Method, Object[]) for ART native code.
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = proxy.h;
return h.invoke(proxy, method, args);
}
复制代码

ProxyClassFactory


重点是ProxyClassFactory 类,这里的逻辑不少,所以我将ProxyClassFactory 单独抽出来了。能看到,首先其会检测当前interface 是否已被当前类加载器所加载。


        Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
复制代码

之后会进行判断是否为接口。这也是我们说的第二个参数为何不能传基类或抽象类的原因。


        if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
复制代码

之后判断当前 interface 是否已经存在于缓存cache内了。


        if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
复制代码

检测非 public 修饰符的 interface 是否在是同一个包名,如果不是则抛出异常


    for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
...

复制代码

检验通过后,会 getMethods(...) 获取接口内的全部方法。


随后会对methords进行一个排序。具体的代码我就不贴了,排序规则是:如果方法相等(返回值和方法签名一样)或同是一个接口内方法,则当前顺序不变,如果两个方法所在的接口存在继承关系,则父类在前,子类在后。


之后 validateReturnTypes(...) 判断 methords 是否存在方法签名相同并且返回值类型也相同的methord,如果有则抛出异常。


之后通过 deduplicateAndGetExceptions(...) 方法,将 methords 方法内的相同方法的父类方法剔除掉,并将 methord 保存在数组中。


转成一维数组和二维数组,Method[] methodsArray,Class< ? >[][] exceptionsArray,随后给当前代理类命名:包名 + “$Proxy” + num


最后调用系统提供的 native 方法 generateProxy(...) 。这是真正的代理类创建方法。感兴趣的可以查看下java_lang_reflect_Proxy.cc源码
class_linker.cc源码


        List<Method> methods = getMethods(interfaces);
Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE);
validateReturnTypes(methods);
List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods);

Method[] methodsArray = methods.toArray(new Method[methods.size()]);
Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

return generateProxy(proxyName, interfaces, loader, methodsArray,
exceptionsArray);
复制代码


好了,本文到这里就结束了,关于动态代理的讲解也应该够用了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考
1、Head First 设计模式:中国电力出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6935029399125262349
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

研究Android音视频-2-MediaCodec使用:YUV码流编码位MP4的示例

本文解决的问题 查看编解码器 录制YUV文件 将YUV文件编码为MP4视频格式 官方的示意图 数据流 input:给解码器输入需要解码或需要编码的数据流 output:解码器输出解码好或编码好的数据给客户端 MediaCodec内部采用异步的方式处理数...
继续阅读 »

本文解决的问题



  1. 查看编解码器

  2. 录制YUV文件

  3. 将YUV文件编码为MP4视频格式


官方的示意图


数据流


input:给解码器输入需要解码或需要编码的数据流


output:解码器输出解码好或编码好的数据给客户端



MediaCodec内部采用异步的方式处理数据,将处理好的数据写入缓冲区,客户端从缓冲区取数据使用,使用后必须手动释放缓冲区,否则无法继续处理数据



状态



  • Stopped

    • Error

    • Uninitialized:新建MediaCodec后,会进入该状态

    • Configured:调用configured方法后,进入该状态



  • Executing

    • Flushed:调用start方法后,进入该状态

    • Running:调用dequeueInputBuffer方法后,进入该状态

    • End of Stream



  • Released


功能描述


打印设备支持的编解码选项


val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)//创建查看设备可以使用的编解码器
val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS)//创建查看所有编解码器

fun printCodecInfo() {
val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)//创建查看设备可以使用的编解码器
val codecInfos = mediaCodecList.codecInfos
codecInfos.forEach { codecInfo ->
if (codecInfo.isEncoder)
println(
"encoder name: ${codecInfo.name} \n" +
" canonicalName: ${codecInfo.canonicalName} \n" +
" isAlias: ${codecInfo.isAlias} \n" +
" isSoftwareOnly: ${codecInfo.isSoftwareOnly} \n" +
" supportedTypes: ${
codecInfo.supportedTypes.map {
println("encoder: $it")
}
} \n" +
" isVendor: ${codecInfo.isVendor} \n" +
" isHardwareAccelerated: ${codecInfo.isHardwareAccelerated}" +
""
)
}

codecInfos.forEach { codecInfo ->
if (!codecInfo.isEncoder)
println(
"decoder name: ${codecInfo.name} \n" +
" canonicalName: ${codecInfo.canonicalName} \n" +
" isAlias: ${codecInfo.isAlias} \n" +
" isSoftwareOnly: ${codecInfo.isSoftwareOnly} \n" +
" supportedTypes: ${
codecInfo.supportedTypes.map {
println("decoder: $it")
}
} \n" +
" isVendor: ${codecInfo.isVendor} \n" +
" isHardwareAccelerated: ${codecInfo.isHardwareAccelerated}" +
""
)
}
}

/*
打印示例
"video/avc":H.264硬件编码器
encoder name: OMX.qcom.video.encoder.avc
canonicalName: OMX.qcom.video.encoder.avc
isAlias: false
isSoftwareOnly: false
supportedTypes: [kotlin.Unit]
isVendor: true
isHardwareAccelerated: true
"video/hevc":H.265软解编码器
encoder name: c2.android.hevc.encoder
canonicalName: c2.android.hevc.encoder
isAlias: false
isSoftwareOnly: true
supportedTypes: [kotlin.Unit]
isVendor: false
isHardwareAccelerated: false
*/

/*查找指定的编解码器*/
val codec = findCodec("video/avc", false, true)//查找H.264硬解码器
fun findCodec(
mimeType: String,
isEncoder: Boolean,
isHard: Boolean = true
): MediaCodecInfo? {
val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecInfos = mediaCodecList.codecInfos
return codecInfos.find {
it.isEncoder == isEncoder && !it.isSoftwareOnly == isHard && hasThisCodec(
it,
mimeType
)
}
}
private fun hasThisCodec(codecInfo: MediaCodecInfo, mimeType: String): Boolean {
return codecInfo.supportedTypes.find { it.equals(mimeType) } != null
}
复制代码

录制yuv文件


打开Camera,通过回调流保存原始YUV数据。简单存几秒即可,1080p下存了225帧,存储占用1080*1920*225*3/2=667.4M字节


示例工程: 地址


示例代码:


 val fos = FileOutputStream("$filesDir/test.yuv")
cameraView = findViewById(R.id.cameraview)
cameraView.cameraParams.facing = Camera.CameraInfo.CAMERA_FACING_BACK
cameraView.cameraParams.isFilp = false
cameraView.cameraParams.isScaleWidth = true
cameraView.cameraParams.previewSize.previewWidth = 1920
cameraView.cameraParams.previewSize.previewHeight = 1080
cameraView.addPreviewFrameCallback(object : PreviewFrameCallback {
override fun analyseData(data: ByteArray): Any {
fos.write(data)
return 0
}

override fun analyseDataEnd(t: Any) {}
})
addLifecycleObserver(cameraView)
复制代码

YUV视频流编码为h.264码流并通过MediaMuxer保存为mp4文件


编码流程:



  1. 查询编码队列是否空闲

  2. 将需要编码的数据复制到编码队列

  3. 查询编码完成队列是否有完成的数据

  4. 将已编码完成的数据复制到cpu内存


单线程示例代码:


//TODO YUV视频流编码为H.264/H.265码流并通过MediaMuxer保存为mp4文件
fun convertYuv2Mp4(context: Context) {
val yuvPath = "${context.filesDir}/test.yuv"
val saveMp4Path = "${context.filesDir}/test.mp4"
File(saveMp4Path).deleteOnExit()

val mime = "video/avc" //若设备支持H.265也可以使用'video/hevc'编码器
val format = MediaFormat.createVideoFormat(mime, 1920, 1080)
format.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
)
//width*height*frameRate*[0.1-0.2]码率控制清晰度
format.setInteger(MediaFormat.KEY_BIT_RATE, 1920 * 1080 * 3)
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
//每秒出一个关键帧,设置0为每帧都是关键帧
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
format.setInteger(
MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR//遵守用户设置的码率
)

//定义并启动编码器
val videoEncoder = MediaCodec.createEncoderByType(mime)
videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
videoEncoder.start()

// 当前编码帧信息
val bufferInfo = MediaCodec.BufferInfo()

//定义混合器:输出并保存h.264码流为mp4
val mediaMuxer =
MediaMuxer(
"${context.filesDir}/test.mp4",
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
);
var muxerTrackIndex = -1
val byteArray = ByteArray(1920 * 1080 * 3 / 2)
var read = 0
var inputEnd = false//数据读取完毕,并且全部都加载至编码器
var pushEnd = false //数据读取完毕,并且成功发出eof信号
val presentTimeUs = System.nanoTime() / 1000


//从文件中读取yuv码流,模拟输入流
FileInputStream("${context.filesDir}/test.yuv").use { fis ->
loop1@ while (true) {
//step1 将需要编码的数据逐帧送往编码器
if (!inputEnd) {
//step1.1 查询编码器队列是否空闲
val inputQueueIndex = videoEncoder.dequeueInputBuffer(30);
if (inputQueueIndex > 0) {
read = fis.read(byteArray)
if (read == byteArray.size) {
//默认从Camera中保存的YUV NV21,编码后颜色成反,手动转为NV12后,颜色正常
val convertCost = measureTimeMillis {
val start = 1920 * 1080
val end = 1920 * 1080 / 4 - 1
for (i in 0..end) {
val temp = byteArray[2 * i + start]
byteArray[2 * i + start] = byteArray[2 * i + start + 1]
byteArray[2 * i + start + 1] = temp
}
}
//step1.2 将数据送往编码器,presentationTimeUs为送往编码器的跟起始值的时间差,单位为微妙
val inputBuffer =
videoEncoder.getInputBuffer(inputQueueIndex)
inputBuffer?.clear()
inputBuffer?.put(byteArray)
videoEncoder.queueInputBuffer(
inputQueueIndex,
0,
byteArray.size,
System.nanoTime() / 1000 - presentTimeUs,
0
)
} else {
inputEnd = true//文件读取结束标记
}
}
}

//step2 将结束标记传给编码器
if (inputEnd && !pushEnd) {
val inputQueueIndex = videoEncoder.dequeueInputBuffer(30);
if (inputQueueIndex > 0) {
val pts: Long = System.nanoTime() / 1000 - presentTimeUs
videoEncoder.queueInputBuffer(
inputQueueIndex,
0,
byteArray.size,
pts,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
pushEnd = true
println("数据输入完成,成功发出eof信号")
}
}

//step3 从编码器中取数据,不及时取出,缓冲队列被占用,编码器将阻塞不进行编码工作
val outputQueueIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 30)
when (outputQueueIndex) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
//step3.1 标记新的解码数据到来,在此添加视频轨道到混合器
muxerTrackIndex = mediaMuxer.addTrack(videoEncoder.outputFormat)
mediaMuxer.start()
}
MediaCodec.INFO_TRY_AGAIN_LATER -> {
}
else -> {
when (bufferInfo.flags) {
MediaCodec.BUFFER_FLAG_CODEC_CONFIG -> {
// SPS or PPS, which should be passed by MediaFormat.
}
MediaCodec.BUFFER_FLAG_END_OF_STREAM -> {
bufferInfo.set(0, 0, 0, bufferInfo.flags)
videoEncoder.releaseOutputBuffer(outputQueueIndex, false)
println("数据解码并获取完成,成功发出eof信号")
break@loop1
}
else -> {
mediaMuxer.writeSampleData(
muxerTrackIndex,
videoEncoder.getOutputBuffer(outputQueueIndex)!!,
bufferInfo
)
}
}
videoEncoder.releaseOutputBuffer(outputQueueIndex, false)
}
}
}

//释放应该释放的具柄
mediaMuxer.release()
videoEncoder.stop()
videoEncoder.release()
}
}
复制代码


作者:君子陌路
链接:https://juejin.cn/post/6955080139885838372
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android选择图片库-TakePhoto

TakePhoto是一款用于在Android设备上获取照片(拍照或从相册、文件中选择)、裁剪图片、压缩图片的开源工具库,目前最新版本4.1.0。 3.0以下版本及API说明,详见TakePhoto2.0+。TakePhoto交流平台:QQ群:556387607...
继续阅读 »

TakePhoto是一款用于在Android设备上获取照片(拍照或从相册、文件中选择)、裁剪图片、压缩图片的开源工具库,目前最新版本4.1.0。 3.0以下版本及API说明,详见TakePhoto2.0+

TakePhoto交流平台:QQ群:556387607(群1,未满)

V4.0

  • 支持通过相机拍照获取图片
  • 支持从相册选择图片
  • 支持从文件选择图片
  • 支持批量图片选取
  • 支持图片压缩以及批量图片压缩
  • 支持图片裁切以及批量图片裁切
  • 支持照片旋转角度自动纠正
  • 支持自动权限管理(无需关心SD卡及摄像头权限等问题)
  • 支持对裁剪及压缩参数个性化配置
  • 提供自带裁剪工具(可选)
  • 支持智能选取及裁剪异常处理
  • 支持因拍照Activity被回收后的自动恢复
  • 支持Android8.1
  • +支持多种压缩工具
  • +支持多种图片选择工具

目录

安装说明

Gradle:

    compile 'com.jph.takephoto:takephoto_library:4.1.0'

Maven:

<dependency>
<groupId>com.jph.takephoto</groupId>
<artifactId>takephoto_library</artifactId>
<version>4.1.0</version>
<type>pom</type>
</dependency>

使用说明

使用TakePhoto有以下两种方式:

方式一:通过继承的方式

  1. 继承TakePhotoActivityTakePhotoFragmentActivityTakePhotoFragment三者之一。
  2. 通过getTakePhoto()获取TakePhoto实例进行相关操作。
  3. 重写以下方法获取结果
 void takeSuccess(TResult result);
void takeFail(TResult result,String msg);
void takeCancel();

此方式使用简单,满足的大部分的使用需求,具体使用详见simple。如果通过继承的方式无法满足实际项目的使用,可以通过下面介绍的方式。

方式二:通过组装的方式

可参照:TakePhotoActivity,以下为主要步骤:

1.实现TakePhoto.TakeResultListener,InvokeListener接口。

2.在 onCreate,onActivityResult,onSaveInstanceState方法中调用TakePhoto对用的方法。

3.重写onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults),添加如下代码。

  @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//以下代码为处理Android6.0、7.0动态权限所需
TPermissionType type=PermissionManager.onRequestPermissionsResult(requestCode,permissions,grantResults);
PermissionManager.handlePermissionsResult(this,type,invokeParam,this);
}

4.重写TPermissionType invoke(InvokeParam invokeParam)方法,添加如下代码:

 @Override
public TPermissionType invoke(InvokeParam invokeParam) {
TPermissionType type=PermissionManager.checkPermission(TContextWrap.of(this),invokeParam.getMethod());
if(TPermissionType.WAIT.equals(type)){
this.invokeParam=invokeParam;
}
return type;
}

5.添加如下代码获取TakePhoto实例:

   /**
* 获取TakePhoto实例
* @return
*/
public TakePhoto getTakePhoto(){
if (takePhoto==null){
takePhoto= (TakePhoto) TakePhotoInvocationHandler.of(this).bind(new TakePhotoImpl(this,this));
}
return takePhoto;
}

自定义UI

TakePhoto不仅支持对相关参数的自定义,也支持对UI的自定义,下面就像大家介绍如何自定义TakePhoto的相册与裁剪工具的UI。

自定义相册

如果TakePhoto自带相册的UI不符合你应用的主题的话,你可以对它进行自定义。方法如下:

自定义Toolbar

在“res/layout”目录中创建一个名为“toolbar.xml”的布局文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:theme="@style/CustomToolbarTheme"
android:background="#ffa352">
</android.support.v7.widget.Toolbar>

在“toolbar.xml”文件中你可以指定TakePhoto自带相册的主题以及Toolbar的背景色。

自定义状态栏

在“res/values”目录中创建一个名为“colors.xml”的资源文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="multiple_image_select_primaryDark">#212121</color>
</resources>

通过上述方式便可以自定义状态栏的颜色。

自定义提示文字

在“res/values”目录的“string.xml”文件冲添加如下代码:

<resources>    
<string name="album_view">选择图片</string>
<string name="image_view">单击选择</string>
<string name="add">确定</string>
<string name="selected">已选</string>
<string name="limit_exceeded">最多能选 %d 张</string>
</resources>

重写上述代码,便可以自定义TakePhoto自带相册的提示文字。

自定义裁切工具

在“res/layout”目录中创建一个名为“crop__activity_crop.xml”与“crop__layout_done_cancel.xml”的布局文件,内容如下:

crop__activity_crop.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.soundcloud.android.crop.CropImageView
android:id="@+id/crop_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:background="@drawable/crop__texture"
android:layout_above="@+id/done_cancel_bar" />
<include
android:id="@+id/done_cancel_bar"
android:layout_alignParentBottom="true"
layout="@layout/crop__layout_done_cancel"
android:layout_height="50dp"
android:layout_width="match_parent" />
</RelativeLayout>

crop__layout_done_cancel.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Crop.DoneCancelBar">
<FrameLayout
android:id="@+id/btn_cancel"
style="@style/Crop.ActionButton">
<TextView style="@style/Crop.ActionButtonText.Cancel" />
</FrameLayout>
<FrameLayout
android:id="@+id/btn_done"
style="@style/Crop.ActionButton">
<TextView style="@style/Crop.ActionButtonText.Done" />
</FrameLayout>
</LinearLayout>

重写上述代码,便可以自定义TakePhoto裁切工具的UI。

API

获取图片

TakePhoto提供拍照,从相册选择,从文件中选择三种方式获取图片。

API:

/**
* 从文件中获取图片(不裁剪)
*/
void onPickFromDocuments();
/**
* 从相册中获取图片(不裁剪)
*/
void onPickFromGallery();
/**
* 从相机获取图片(不裁剪)
* @param outPutUri 图片保存的路径
*/
void onPickFromCapture(Uri outPutUri);
/**
* 图片多选
* @param limit 最多选择图片张数的限制
**/
void onPickMultiple(int limit);

以上三种方式均提供对应的裁剪API,详见:裁剪图片
注:
由于不同Android Rom厂商对系统有不同程度的定制,有可能导致某种选择图片的方式不支持,所以为了提高TakePhoto的兼容性,当某种选的图片的方式不支持时,TakePhoto会自动切换成使用另一种选择图片的方式进行图片选择。

裁剪图片

API

TakePhoto支持对图片进行裁剪,无论是拍照的照片,还是从相册、文件中选择的图片。你只需要调用TakePhoto的相应方法即可:

/**
* 从相机获取图片并裁剪
* @param outPutUri 图片裁剪之后保存的路径
* @param options 裁剪配置
*/
void onPickFromCaptureWithCrop(Uri outPutUri, CropOptions options);
/**
* 从相册中获取图片并裁剪
* @param outPutUri 图片裁剪之后保存的路径
* @param options 裁剪配置
*/
void onPickFromGalleryWithCrop(Uri outPutUri, CropOptions options);
/**
* 从文件中获取图片并裁剪
* @param outPutUri 图片裁剪之后保存的路径
* @param options 裁剪配置
*/
void onPickFromDocumentsWithCrop(Uri outPutUri, CropOptions options);
/**
* 图片多选,并裁切
* @param limit 最多选择图片张数的限制
* @param options 裁剪配置
* */
void onPickMultipleWithCrop(int limit, CropOptions options);

对指定图片进行裁剪

另外,TakePhoto也支持你对指定图片进行裁剪:

/**
* 裁剪图片
* @param imageUri 要裁剪的图片
* @param outPutUri 图片裁剪之后保存的路径
* @param options 裁剪配置
*/
void onCrop(Uri imageUri, Uri outPutUri, CropOptions options)throws TException;
/**
* 裁剪多张图片
* @param multipleCrop 要裁切的图片的路径以及输出路径
* @param options 裁剪配置
*/
void onCrop(MultipleCrop multipleCrop, CropOptions options)throws TException;

CropOptions

CropOptions是用于裁剪的配置类,通过它你可以对图片的裁剪比例,最大输出大小,以及是否使用TakePhoto自带的裁剪工具进行裁剪等,进行个性化配置。

Usage:

 CropOptions cropOptions=new CropOptions.Builder().setAspectX(1).setAspectY(1).setWithOwnCrop(true).create();  
getTakePhoto().onPickFromDocumentsWithCrop(imageUri,cropOptions);
//
getTakePhoto().onCrop(imageUri,outPutUri,cropOptions);

注:
由于不同Android Rom厂商对系统有不同程度的定制,有可能系统中没有自带或第三方的裁剪工具,所以为了提高TakePhoto的兼容性,当系统中没有自带或第三方裁剪工具时,TakePhoto会自动切换到使用TakePhoto自带的裁剪工具进行裁剪。

另外TakePhoto4.0+支持指定使用TakePhoto自带相册,如:takePhoto.setTakePhotoOptions(new TakePhotoOptions.Builder().setWithOwnGallery(true).create()); 详情可参考:Demo

压缩图片

你可以选择是否对图片进行压缩处理,你只需要告诉它你是否要启用压缩功能以及CompressConfig即可。

API

 /**
* 启用图片压缩
* @param config 压缩图片配置
* @param showCompressDialog 压缩时是否显示进度对话框
* @return
*/
void onEnableCompress(CompressConfig config,boolean showCompressDialog);

Usage:

TakePhoto takePhoto=getTakePhoto();
takePhoto.onEnableCompress(compressConfig,true);
takePhoto.onPickFromGallery();

如果你启用了图片压缩,TakePhoto会使用CompressImage对图片进行压缩处理,CompressImage目前支持对图片的尺寸以及图片的质量进行压缩。默认情况下,CompressImage开启了尺寸与质量双重压缩。

对指定图片进行压缩

另外,你也可以对指定图片进行压缩:
Usage:

new CompressImageImpl(compressConfig,result.getImages(), new CompressImage.CompressListener() {
@Override
public void onCompressSuccess(ArrayList<TImage> images) {
//图片压缩成功
}
@Override
public void onCompressFailed(ArrayList<TImage> images, String msg) {
//图片压缩失败
}
}).compress();

CompressConfig

CompressConfig是用于图片压缩的配置类,你可以通过CompressConfig.Builder对图片压缩后的尺寸以及质量进行相关设置。如果你想改变压缩的方式可以通过CompressConfig.Builder进行相关设置。
Usage:

CompressConfig compressConfig=new CompressConfig.Builder().setMaxSize(50*1024).setMaxPixel(800).create();

指定压缩工具

使用TakePhoto压缩工具进行压缩:

CompressConfig config=new CompressConfig.Builder()
.setMaxSize(maxSize)
.setMaxPixel(width>=height? width:height)
.create();
takePhoto.onEnableCompress(config,showProgressBar);

使用Luban进行压缩:

LubanOptions option=new LubanOptions.Builder()
.setGear(Luban.CUSTOM_GEAR)
.setMaxHeight(height)
.setMaxWidth(width)
.setMaxSize(maxSize)
.create();
CompressConfig config=CompressConfig.ofLuban(option);
takePhoto.onEnableCompress(config,showProgressBar);

详情可参考Demo:CustomHelper.java


原文链接:https://github.com/crazycodeboy/TakePhoto

代码下载:TakePhoto-master.zip

收起阅读 »

iOS-数据结构初探

数据结构的分类数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成 简单来说:数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数...
继续阅读 »

数据结构的分类

数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成

简单来说:数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数据结构,才能在处理实际问题时选取最合适的数据结构。

常用的数据结构有:数组,栈,链表,队列,树,图,堆,散列表等

1、数组

数组是可以再内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始

NSArray *array = [NSArray arrayWithObjects:@"1",@"2",@"3",@"4", nil];
// NSArray *array = @[@"1",@"2",@"3",@"4"];
NSLog(@"%@",array[0]);

优点:

  • 1、按照索引查询元素速度快
  • 2、按照索引遍历数组方便

缺点:

  • 1、数组的大小固定后就无法扩容了
  • 2、数组只能存储一种类型的数据
  • 3、添加,删除的操作慢,因为要移动其他的元素。

适用场景:

  • 频繁查询,对存储空间要求不大,很少增加和删除的情况。

2、栈

栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈

线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。

线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)

3、队列

队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素,也就是:先进先出。从一端放入元素的操作称为入队,取出元素为出队


4、链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

根据指针的指向,链表能形成不同的结构,例如单链表双向链表循环链表等。




双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。


链表的优点:

  • 链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
  • 添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;

缺点:

  • 因为含有大量的指针域,占用空间较大;
  • 查找元素需要遍历链表来查找,非常耗时。

适用场景:

  • 数据量较小,需要频繁增加,删除操作的场景

5、树

树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • 每个节点有零个或多个子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点
  • 除了根节点外,每个子节点可以分为多个不相交的子树;

在日常的应用中,我们讨论和用的更多的是树的其中一种结构,就是二叉树


二叉树是树的特殊一种,具有如下特点:

  • 1、每个结点最多有两颗子树,结点的度最大为2。
  • 2、左子树和右子树是有顺序的,次序不能颠倒。
  • 3、即使某结点只有一个子树,也要区分左右子树。

二叉树是一种比较有用的折中方案,它添加,删除元素都很快,并且在查找方面也有很多的算法优化,所以,二叉树既有链表的好处,也有数组的好处,是两者的优化方案,在处理大批量的动态数据方面非常有用。

二叉树有很多扩展的数据结构,包括平衡二叉树红黑树B+树等,这些数据结构二叉树的基础上衍生了很多的功能,在实际应用中广泛用到,例如mysql的数据库索引结构用的就是B+树,还有HashMap的底层源码中用到了红黑树。这些二叉树的功能强大,但算法上比较复杂,想学习的话还是需要花时间去深入的。

6、散列表

散列表,也叫哈希表,是根据关键码值 (key和value) 直接进行访问的数据结构,通过keyvalue来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。

记录的存储位置=f(key)

  • 这里的对应关系f 成为散列函数,又称为哈希 (hash函数),而散列表就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字
  • 然后就将该数字对数组长度进行取余,取余结果就当作数组的下标
  • 将value存储在以该数字为下标的数组空间里
  • 这种存储空间可以充分利用数组的查找优势来查找元素,所以查找的速度很快。

哈希表在应用中也是比较常见的,就如Java中有些集合类就是借鉴了哈希原理构造的,例如HashMapHashTable等,利用hash表的优势,,对于集合的查找元素时非常方便的,然而,因为哈希表是基于数组衍生的数据结构,在添加删除元素方面是比较慢的,所以很多时候需要用到一种数组链表来做,也就是拉链法。拉链法是数组结合链表的一种结构,较早前的hashMap底层的存储就是采用这种结构,直到jdk1.8之后才换成了数组加红黑树的结构.iOSweak表(弱引用表)就是典型的哈希表


  • 左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,
  • 当然这个链表可能为空,也可能元素很多。
  • 我们根据元素的一些特征把元素分配到不同的链表中去,
  • 也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
哈希表的应用场景很多,当然也有很多问题要考虑,比如哈希冲突的问题,如果处理的不好会浪费大量的时间,导致应用崩溃。

7、堆

堆是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆斐波那契堆等。



堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。

(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2),
满足前者的表达式的成为小顶堆,满足后者表达式的为大顶堆,这两者的结构图可以用完全二叉树排列出来

8、图

图型结构也称图案,指个体目标重复排列的空间形式。图案反映了地物的空间分布特征,它可以是自然的,也可以是人为构造的 [1] 图形结构,简称“图”,是一种复杂的数据结构。图形结构中,每个结点的前驱结点数和后续结点数可以任意多个。


数据元素间的关系是任意的。其他数据结构(如树、线性表等)都有明确的条件限制,而图形结构中任意两个数据元素间均可相关联。常用来研究生产流程、施工计划、各种网络建设等问题。


转自:https://www.jianshu.com/p/4013774d929d
收起阅读 »

面试之链表

前言这一篇博客是很早之前写的,是关于一些链表和二叉树面试相关的问题,算是整理吧,网上这部分的答案也很多,希望能给大家一些帮助。注意:本文中一些异常情况都是没有做处理的,例如NULL等等,只是给出了基本的解决方案.大家参考一下.链表部分问题:定义并且创建一个链表...
继续阅读 »

前言
这一篇博客是很早之前写的,是关于一些链表和二叉树面试相关的问题,算是整理吧,网上这部分的答案也很多,希望能给大家一些帮助。
注意:本文中一些异常情况都是没有做处理的,例如NULL等等,只是给出了基本的解决方案.大家参考一下.

链表部分
问题:定义并且创建一个链表.
解题方案:
我们首先要如何定义一个结构体,下面的构造方案我是使用的递归的形式来构造一个结构体,注意不要忘记分配内存.其他的方面都比较简单,难度较低.
代码示例:

#include <stdio.h>

typedef struct ListNode {
int data;
struct ListNode*nextNode;
}ListNode;

ListNode* createListNodeAction(int *listArray, int index, int length) {
ListNode *listNode = (ListNode *) malloc(sizeof (ListNode) );
ListNode *nextNode = NULL;
int i = listArray[index];
listNode->data = i;
if (index != length - 1) {
nextNode = (ListNode*) malloc(sizeof (ListNode));
nextNode = createListNodeAction(listArray, index + 1, length);
}
listNode->nextNode = nextNode;
return listNode;
}

问题:不通过遍历删除链表中的非尾节点.

解题方案:
首先我们要知道我们如何通过遍历删除链表中的某个节点? 通过遍历我们可以知道要删除的链表节点前驱(也就是前一个节点),然后我们把前驱的nextNode指向要删除的节点的nextNode,释放要删除的节点即可.示意图如下所示.


那么我们对于上面的那个题目,我们该如何解决呢?由于前驱不通过遍历我们是拿不到的,所以我们只能通过覆盖的形式,用nextNode节点的属性覆盖掉需要删除的节点,然后释放nextNode节点,这样就完成了删除工作,由于前驱的nextNode指针属性不通过遍历修改不了,所以不能删除尾节点.否则就会有野指针问题出现.

void deleteListNodeNotTail(ListNode *deleteNode) {

ListNode *deleteNextNode = deleteNode->nextNode;
deleteNode->data = deleteNextNode->data;
deleteNode->nextNode = deleteNextNode->nextNode;
free(deleteNextNode);
}

问题:只遍历一次就找到链表中的中间节点.

解题方案:
撇开题目不谈,我们首先要清楚如何确定链表中的中间节点?由于链表没有长度的属性,所以暴力法的做法就是先遍历一次确定链表的长度,然后再次遍历链表找到中间节点.时间复杂度为O(logn+n).
那么如何通过一次遍历来找到链表中的中间节点呢?我们的解决方案是我们需要一快一慢两个移动节点fathNode和slowNode,fathNode的偏移速度是slowNode的两倍,,所以当fathNode == NULL,slowNode正好处于中心节点上.时间复杂度为O(logn).
代码示例:

ListNode* getListHalfNode(ListNode *listNode) {

ListNode *fathNode = listNode->nextNode;
ListNode *slowNode = listNode;

while (fathNode) {
fathNode = fathNode->nextNode->nextNode;
slowNode = slowNode->nextNode;
}
return slowNode;
}

问题:如何找到单向链表中的倒数第i个节点(i >= 1).

解题方案:
暴力法该如何解决这种问题呢?我们先遍历一遍确定链表的长度length,再次遍历链表取得下标位置在length-1-k的节点就是我们要的节点.时间复杂度为O(logn+n).
那没有有没有优化方式呢?这是有的,仍然借助上一个问题的解决方案,我们需要一快一慢两个移动节点fathNode和slowNode,fathNode先偏移i个位置,然后两个节点同时进行移动,所以当fathNode == NULL,slowNode正好处于倒数.时间复杂度为O(2logn).
代码示例:

ListNode* getListNodeWithLast(ListNode *listNode,int i) {

ListNode *fathNode = listNode;
ListNode *slowNode = listNode;

while (i) {
fathNode = fathNode->nextNode;
i--;
}

while (fathNode) {
fathNode = fathNode->nextNode;
slowNode = slowNode->nextNode;
}
return slowNode;
}

问题:删除倒数第i个结点(i>=1),不能用替换删除法.

解题方案:
上面我们已经了解了替换删除法,不需要知道前驱,我们就可以使用覆盖替换的方式删除节点,而这次我们可以是知道前驱节点的,而且结合上一次的快慢节点的方式,我们只需要先找到前驱节点即可.也就是fathNode节点需要先移动i + 1 次,具体代码如下所示.
代码示例:

void deleteListNodeWithLast(ListNode *listNode,int i) {

ListNode *fathNode = listNode;
ListNode *slowNode = listNode;

while (i + 1) {
fathNode = fathNode->nextNode;
i--;
}

while (fathNode) {
fathNode = fathNode->nextNode;
slowNode = slowNode->nextNode;
}

ListNode *deleteNode = slowNode->nextNode;
ListNode *deleteNextNode = deleteNode->nextNode;
slowNode->nextNode = deleteNextNode;
free(deleteNode);
}

问题:约瑟夫问题

约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解。
解题方案:
使用链表该如何解决约瑟夫问题呢?我们需要把链表做成一个环,也就是我们需要遍历一遍找到尾节点,并且制定尾节点的nextNode指针指向链表的第一个节点,这样我们就把链表做成了一个环.
然后我们假设每i次删除一个节点,这样返回的删除,直到只剩最后一个节点就是我们要求的解.
代码示例:

ListNode* JocephCircle(ListNode *firstNode, int k) {

ListNode *endNode = firstNode;
ListNode *resultNode = firstNode;
ListNode *deleteNode = NULL;

// 做环
while (endNode->nextNode) {
endNode = endNode->nextNode;
}
endNode->nextNode = firstNode;

// 自身的nextNode指向自身的时候,就只剩下一个元素了
while (resultNode->nextNode != resultNode) {

//删除节点 ,先找到前驱节点,然后找到删除节点
//由于先执行赋值操作,再进行i-1操作,所以k-1,由于是找删除节点的前驱节点,所以还需要-1.
int i = (k-1)-1;
while (i) {
resultNode = resultNode->nextNode;
i--;
}

// 重新指向并且释放删除节点
deleteNode = resultNode->nextNode;
resultNode->nextNode = resultNode->nextNode->nextNode;
free(deleteNode);
resultNode = resultNode->nextNode;
}

return resultNode;
}

问题:单链表的冒泡排序问题

解题方案:
仿照普通的数组遍历,这里两个while进行实现简单的冒泡排序.判断条件为nextNode节点是否为NULL,即可知道是否已经到达了单链表的尾节点.这个问题如果不做任何优化的话就如同下面代码演示的即可.其他优化方式就不过多阐述,上网查询即可.
代码示例:

void sortNodeListAction(ListNode *firstNode) {

ListNode *nowNode = firstNode;
ListNode *exchangeNode = (ListNode *)malloc(sizeof(ListNode));

while (nowNode->nextNode) {
ListNode *nowNextNode = nowNode;
while (nowNextNode) {
if (nowNextNode->data < nowNode->data) {
exchangeNode->data = nowNextNode->data;
nowNextNode->data = nowNode->data;
nowNode->data = exchangeNode->data;
}
nowNextNode = nowNextNode->nextNode;
if (!nowNextNode) {
continue;
}
}
nowNode = nowNode->nextNode;
}
free(exchangeNode);
}

问题:判断链表是否带环;若带环,求环的长度和入口点

解题方案:
这里我们要首先明白什么叫做带环,如下图所示,不管是哪种表现形式,我们都说当前链表是带环的链表.


我们了解了什么叫链表带环.在代码中,我们该如何判断当前的链表是否带环呢?网上有一种方案就是使用快慢节点解决,设置fathNode和slowNode,fathNode的偏移速度是slowNode的两倍,所以当fathNode == NULL,那么可以断定链表不带环,假设在某一个时刻fathNode==slowNode,说明两个节点重合,也就是说链表带环.
那么带环的链表我们该如何判断其环的长度呢?首先我们要知道fathNode偏移速度是slowNode的两倍,也就是说相同时间内,fathNode偏移距离是slowNode的2倍.
我们要说明两个节点交汇的情况,两者的情况肯定是慢节点在换上走不到一圈就会进行交汇,有人会问这是为什么呢?因为fathNode偏移速度是slowNode的两倍,所以在两者起点相同的情况下slowNode走完一圈fathNode走完两圈内,两者是必然相交的.
根据上面的两种情形,如下图所示.当两点相交时,我们有以下的结论,fathNode走过的路程为L + (C + A) + A,slowNode走过的路程为L + A, 我们得出 (L + A) x 2 = L + (C + A) + A;所以L = C.这时候我们继续定义一个新的节点enterNode从头开始出发,slowNode同时出发,两者速度相同,同时L = C;所以我们知道两者相交的节点必然是环的入口点.这时候enterNode再走到b点,就可以计算出环的长度了.

代码示例:

// 判断是否有环
bool isExistLoop(ListNode* firstNode) {
ListNode *fastNode;
ListNode * slowNode;
fastNode = slowNode = firstNode;
while (slowNode != NULL && fastNode -> next != NULL) {
slowNode = slowNode -> next ;
fastNode = fastNode -> next -> next ;
if (slowNode == fastNode)
return true ;
}
return false ;
}
// 判断环的长度
int getLoopLength(ListNode* firstNode){
ListNode* slowNode = firstNode;
ListNode* fastNode = firstNode;
while ( fastNode && fastNode ->next ){
slowNode = slowNode->next;
fastNode = fastNode->next->next;
if ( slowNode== fastNode) {
break;
}
}
slowNode= slowNode->next;
fastNode = fastNode->next->next;
int length = 1;
while ( fastNode != slowNode)
{
slowNode = slowNode->next;
fastNode = fastNode->next->next;
length ++;
}
return length;
}
// 找到环中的相遇节点
ListNode* getMeetingNode(ListNode* firstNode) {
ListNode* fastNode;
ListNode* slowNode;
slowNode = fastNode = firstNode;
while (slowNode != NULL && fastNode-> next != NULL) {
slowNode = slowNode-> next ;
fastNode = fastNode-> next -> next ;
if (slowNode == fastNode)
return slowNode;
}

//到达末尾仍然没有相遇,则不存在环
return NULL ;
}
// 找出环的入口节点
ListNode* getEntryNodeOfLoop(ListNode* firstNode) {
ListNode* meetingNode = getMeetingNode(firstNode); // 先找出环中的相遇节点
if (meetingNode == NULL)
return NULL;
ListNode* p1 = meetingNode;
ListNode* p2 = pHead;
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
return p1;
}

如果可以使用字典或者集合的话,那就更简单了;数组也是可以解决,但是效率不是太高.需要多次遍历.

总结

OK,写到这里基本上就结束了,先整理这些后期会持续更新,欢迎大家指导批评,谢谢。。。

转自:https://www.jianshu.com/p/cf89d05c8f30

收起阅读 »

Flutter集成到Swift老项目 使用pod接入flutter

Xcode:Version 11.3.1 (11C504)Swift:5.0iOS项目地址Flutter项目创建cd some/path/flutter create --template module flutter_yyframework文件路径如下:cd...
继续阅读 »

Xcode:Version 11.3.1 (11C504)
Swift:5.0
iOS项目地址
Flutter项目创建

cd some/path/
flutter create --template module flutter_yyframework

文件路径如下:


cd 到你要混编的项目(YYFramework)同一个路径下 ,执行如下:

flutter create -t module flutter_yyframework

Podfile 文件

#注意路径和文件夹名字正确无误 最后有一个反斜杠
flutter_application_path = '/Users/houjianan/Documents/GitHub/iOS/flutter_yyframework/'
load File.join(flutter_application_path, 'YYFramework', 'Flutter', 'podhelper.rb')

target 'YYFramework' do
install_all_flutter_pods(flutter_application_path)

end

注:YYFramework 是iOS项目的文件名
添加好之后

pod install

注意,如下错误:[!] InvalidPodfilefile: No such file or directory @ rb_sysopen - ./flutter_yyframework/.ios/Flutter/podhelper.rb.
需要在flutter_yyframework文件夹下执行以下命令,把.ios和.android等flutter配置生成出来。(打开模拟器。链接真机都可以。)

open -a Simulator
flutter run

注意,如下错误是因为路径不对。

[!] Invalid `Podfile` file: cannot load such file -- path/to/flutter_yyframework/.ios/Flutter/podhelper.rb.

# from /Users/houjianan/Documents/GitHub/iOS/YYFramework/Podfile:7
# -------------------------------------------
# flutter_application_path = 'path/to/flutter_yyframework/'
> load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
#
# -------------------------------------------
houjianan:YYFramework> pod install
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Installing flutter_yyframework (0.0.1)
Generating Pods project
Integrating client project
Pod installation complete! There are 42 dependencies from the Podfile and 51 total pods installed.
houjianan:YYFramework>

iOS Swift代码

//
// AppDelegate.swift
// YYFramework
//
// Created by houjianan on 2018/8/11.
// Copyright © 2018年 houjianan. All rights reserved.
//

import UIKit
import SwiftTheme
import PLShortVideoKit
import Flutter
import FlutterPluginRegistrant // Used to connect plugins.

@UIApplicationMain
// 集成FlutterAppDelegate之后代理方法要override
class AppDelegate: FlutterAppDelegate {

lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(NSHomeDirectory())
flutter_run()
return true
}

}
//
// AppDelegate+Flutter.swift
// YYFramework
//
// Created by houjianan on 2020/1/20.
// Copyright © 2020 houjianan. All rights reserved.
//

import Foundation
import Flutter
import FlutterPluginRegistrant // Used to connect plugins.

extension AppDelegate {

func flutter_run() {
flutterEngine.run()
GeneratedPluginRegistrant.register(with: self.flutterEngine)
}
}
//
// GAFlutterRooterViewController.swift
// YYFramework
//
// Created by houjianan on 2020/1/20.
// Copyright © 2020 houjianan. All rights reserved.
//

import UIKit
import Flutter

class GAFlutterRooterViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

}

@IBAction func bAction(_ sender: Any) {
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
present(flutterViewController, animated: true, completion: nil)
}

@IBAction func cAction(_ sender: Any) {
let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)

flutterViewController.setInitialRoute("MyApp")

let channel = FlutterMethodChannel(name: "com.pages.your/native_get", binaryMessenger: flutterViewController as! FlutterBinaryMessenger)
channel.setMethodCallHandler { (call, result) in
print("method = ", call.method, "arguments = ", call.arguments ?? "argumentsNULL", result)

let method = call.method
if method == "FlutterPopIOS" {
print("FlutterPopIOS:返回来传的参数是 == ", call.arguments ?? "argumentsNULL")
self.navigationController?.popViewController(animated: true)
} else if method == "FlutterCickedActionPushIOSNewVC" {
print("FlutterCickedActionPushIOSNewVC:返回来传的参数是 == ", call.arguments ?? "argumentsNULL")
let vc = GAVerificationCodeViewController(nibName: "GAVerificationCodeViewController", bundle: nil)
self.navigationController?.pushViewController(vc, animated: true)
} else if method == "FlutterGetIOSArguments" {
let dic = ["a":"value"]
print("传参给Flutter:", dic)
result(dic)
} else {

}

}
self.navigationController?.pushViewController(flutterViewController, animated: true)
}
}

Flutter代码

import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:bot_toast/bot_toast.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
return BotToastInit(
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorObservers: [BotToastNavigatorObserver()],
home: MyHomePage(title: '1235777'),
),
);
}
}


class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

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

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
String _textString = "00";

void _incrementCounter() {
setState(() {
_counter++;
});
}

// 创建一个给native的channel (类似iOS的通知)
static const MethodChannel methodChannel = MethodChannel('com.pages.your/native_get');

_iOSPushToVC() async {
await methodChannel.invokeMethod('FlutterPopIOS', '参数');
}

void _backAction() {
_iOSPushToVC();
}

void _pushIOSNewVC() async {
Map<String, dynamic> map = {"code": "200", "data":[1,2,3]};

await methodChannel.invokeMethod('FlutterCickedActionPushIOSNewVC', map);
}

Future<void> _FlutterGetIOSArguments(para) async {
BotToast.showText(text:"_FlutterGetIOSArguments");
try {
final result = await methodChannel.invokeMethod('FlutterGetIOSArguments', para);


BotToast.showText(text:result["a"]);
_textString = result["a"];
} on PlatformException catch (error) {
print(error);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times1:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
FloatingActionButton(
onPressed: _backAction,
child: Icon(Icons.accessibility),
),
FloatingActionButton(
onPressed: _pushIOSNewVC,
child: Icon(Icons.accessibility),
),
FloatingActionButton(
onPressed:() {
_FlutterGetIOSArguments("flutter传值");
// 刷新界面
setState(() {});
},
child: Icon(Icons.accessibility),
),
Text(_textString),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

官网Integrate a Flutter module into your iOS project
很久之前写的一篇《Flutter和原生iOS交互》

过年了,有点时间玩Flutter!
Flutter统治全世界。

转自:https://www.jianshu.com/p/351bd8ecbc79

收起阅读 »

iOS 音频播放iOS13上远程控制设置控制方法崩溃

使用MPRemoteCommandCenter 处理远程音频事件的播放的时候,有些同学会用[pauseCommand addTarget:self action:@selector(remotePauseEvent)]这个方法来处理,但是在iOS13后苹果官方...
继续阅读 »

使用MPRemoteCommandCenter 处理远程音频事件的播放的时候,
有些同学会用[pauseCommand addTarget:self action:@selector(remotePauseEvent)]这个方法来处理,但是在iOS13后苹果官方在这个方法有要求了,官方文档这么写的

// Target-action style for adding handlers to commands.
// Actions receive an MPRemoteCommandEvent as the first parameter.
// Targets are not retained by addTarget:action:, and should be removed from the
// command when the target is deallocated.
//
// Your selector should return a MPRemoteCommandHandlerStatus value when
// possible. This allows the system to respond appropriately to commands that
// may not have been able to be executed in accordance with the application's
// current state
翻译一下其实意思就是 建议用addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler; 这个方法来为其添加本地事件处理,但是也可以用- (void)addTarget:(id)target action:(SEL)action;方法来处理,用- (void)addTarget:(id)target action:(SEL)action; 方法处理时候需要返回MPRemoteCommandHandlerStatus这个值.

意思就是这样了,根据这样的翻译可以很明确知道该怎么解决,要不换- (void)addTarget:(id)target action:(SEL)action;方法为- (id)addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler;要不就在- (void)addTarget:(id)target action:(SEL)action;的引用方法里添加返回值,例如:

- (MPRemoteCommandHandlerStatus)remotePauseEvent {

return MPRemoteCommandHandlerStatusSuccess;
}

参考至这里


转自:https://www.jianshu.com/p/40cd3e7b05bb

收起阅读 »

iOS _OBJC_CLASS_$_NSEntityDescription报错

最近项目中有使用到,MJ相关系列的库,结果出现了报错,如下:Undefined symbol: _OBJC_CLASS_$_NSEntityDescriptionUndefined symbol: _OBJC_CLASS_$_NSManagedObject通过...
继续阅读 »

最近项目中有使用到,MJ相关系列的库,结果出现了报错,如下:

Undefined symbol: _OBJC_CLASS_$_NSEntityDescription

Undefined symbol: _OBJC_CLASS_$_NSManagedObject


通过网上查资料,才知道,自己缺少了coredata库文件,所以才会报这个错误,在项目的这个地方引入,coredata库,即可解决此问题

引入coredata库

收起阅读 »