注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

【iOS】自动布局之Purelayout

masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎...
继续阅读 »

masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。
因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎么不在了?

于是在github上搜索到另外一个较出名的布局,便有了这段Purelayout的尝试。

生成一个UIView:

UIView *view = [UIView newAutoLayoutView];
+ (instancetype)newAutoLayoutView
{
ALView *view = [self new];
view.translatesAutoresizingMaskIntoConstraints = NO;
return view;
}

newAutoLayoutView是UIView的一个扩展方法,其实达到的目的就是生成一个UIView实例,并把该实例的translatesAutoresizingMaskIntoConstraints属性置为NO。这个属性值在默认情况下是YES,如果设置为 NO,那么在运行时,程序不会自动将AutoresizingMask转化成 Constraint。

1.view相对于父容器间距的位置

[view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10];//相对于父容器顶部距离10
[view autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:10];//相对于父容器左部距离10
[view autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:10];//相对于父容器右部距离10
[view autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10];//相对于父容器底部距离10

值得注意的是Purelayout对UILabel做了一些人性化的处理:
在有的国家地区文字是从右至左的,以下代码就是将label的起始位置距离父容器10

[label autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:10];

2.相对于父容器的中心位置:

[view autoCenterInSuperview];//view在父容器中心位置
[view autoAlignAxisToSuperviewAxis:ALAxisHorizontal];//view在父容器水平中心位置
[view autoAlignAxisToSuperviewAxis:ALAxisVertical];//view在父容器垂直中心位置

3.设置大小

[view autoSetDimensionsToSize:CGSizeMake(300, 300)];//设置view的大小为300*300
[view autoSetDimension:ALDimensionHeight toSize:300];//设置view的高度为300
[view autoSetDimension:ALDimensionWidth toSize:300];//设置view的宽度为300

4.相对位置
NSLayoutRelation是一个枚举类型:

typedef NS_ENUM(NSInteger, NSLayoutRelation) {
NSLayoutRelationLessThanOrEqual = -1,
NSLayoutRelationEqual = 0,
NSLayoutRelationGreaterThanOrEqual = 1,
};

见名知意,你懂的。

[view1 autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:view2 withOffset:20 relation:NSLayoutRelationEqual];//view1的顶部在view2的底部的20像素的位置

5.中心对齐

[view1 autoAlignAxis:ALAxisVertical toSameAxisOfView:view2];//view1相对于view2保持在同一个垂直中心上

view1相对于view2保持在同一个垂直中心上

6.相对大小

[view1 autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:view2];

view1的宽度和view2的宽度相等

在使用purelayout的时候值得注意:
1.purelayout提供的方法有些是只支持iOS8及以上的,如果iOS7及以下的调用了是会奔溃的,本人就是因为这个被搞得欲仙欲死。好在purelayout在方法中都有介绍。以上介绍的几种使用场景的方法,也都是支持iOS7及以下系统的。
2.在view父容器为nil的时候,执行purelayout的方法会崩溃。

有兴趣的可以直接去github下载官方的demo,写的也是相当ok的。

持续更新~~~

链接:https://www.jianshu.com/p/15bb1bfec5e9

收起阅读 »

【开源项目】使用环信IM开发的一款仿微信APP

项目背景:为了让更多的小伙伴们能够使用环信快速开发出一款自己的社交通讯APP,现进行开源 产品功能:易用IM是一款仿微信APP,包含以下主要功能:1. 单聊,群聊,群聊天中可发随机红包2. 通讯录:管理好友和群组3. 朋友圈:展示自己和好友发的全部可见的动态,...
继续阅读 »

项目背景

为了让更多的小伙伴们能够使用环信快速开发出一款自己的社交通讯APP,现进行开源

 

产品功能:

易用IM是一款仿微信APP,包含以下主要功能:

1. 单聊群聊,群聊天中可发随机红包

2. 通讯录:管理好友和群组

3. 朋友圈展示自己和好友发的全部可见的动态,可点赞、评论、回复和收藏

4. 支付宝充值余额、提现

5. 余额充值提现功能

6. 表情商店:后台维护表情包,用户可一键添加到自己的聊天中

 

软件架构

1. 使用ThinkPHP3.2.3框架开发

2. 数据库mysql5.7

3. IM功能集成环信即时通讯

4. 集成极光推送、阿里云OSS

5. 百度地图



资源地址:

服务端 https://gitee.com/491290710/EasyIM_Service.git

安卓端 https://gitee.com/491290710/EasyIM_Android.git

IOS端 https://gitee.com/491290710/EasyIM_IOS.git 

 

安装教程

1. 服务器建议使用centos7+,运行环境使用lnmp1.5-1.6一键安装

2. 第三方开发参数请在Application/Common/Conf/config.php中进行配置

3. WEB端代码在layim目录中,访问方式为 您的域名/layim

4. 推荐使用阿里云服务器ECS,优惠购买请点击

https://partner.aliyun.com/shop/20690101/newusers?marketer=286

 

使用说明:

1. WEB端体验地址 http://weixin.pro2.liuniukeji.net/layim

2. 可自行注册账号,注册时验证码输入 654321

 

 

项目截图:

 

 

 

 

 

安卓端下载地址:



本开源项目仅做个人学习使用如需商业合作,请联系:

电话: 18660911357

微信  liuniukeji-js

公司官网: https://www.liuniukeji.com/index/easemob

收起阅读 »

SVProgressHUD简单使用以及自定义动画

SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.MBProgressHUD和SVProgressHUD的区别...
继续阅读 »

SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。
常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.
MBProgressHUD和SVProgressHUD的区别:
svprogresshud 使用起来很方便,但 可定制 差一些,看它的接口貌似只能添加一个全屏的HUD,不能把它添加到某个视图上面去.
MBProgressHUD 功能全一些,可定制 高一些,而且可以指定加到某一个View上去.用起来可能就没上面那个方便了.
具体还要看你的使用场景.
附上GitHub源码地址:
SVProgressHUD:https://github.com/SVProgressHUD/SVProgressHUD
MBProgressHUD:https://github.com/jdg/MBProgressHUD
今天我们不对二者的区别做详解,有空我会专门写文章对它们的区别做一个详解.
今天我们主要简单介绍一下SVProgressHUD的使用.


安装

通过CocoaPods安装,在Podfile中加入pod 'SVProgressHUD',这里不多做介绍.可以参考文章: CocoaPods的简单使用

使用

SVProgressHUD是已经被创建为单例的,所以不需要被实例化了,可以直接使用.调用它的方法[SVProgressHUD method].

[SVProgressHUD show ];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^ {
//耗时的任务
dispatch_async(dispatch_get_main_queue(),^ {
[SVProgressHUD dismiss ];
});
});

显示HUD

可以在下拉刷新或者执行其他耗时任务的时候,使用下面方法之一,来显示不确定任务的状态:

+ (void)show;
+ (void)showWithStatus:(NSString*)string;

效果图分别为:



如果你希望HUD反应任务的进度,可以使用下面方法的其中一个:

+ (void)showProgress:(CGFloat)progress;
+ (void)showProgress:(CGFloat)progress status:(NSString*)status;

通过其他方式可以实现进度条的速度把控.比如:

- (IBAction)clickButtonsShowWithProgress:(id)sender {
progress = 0.0f;
[SVProgressHUD showProgress:0 status:@"Loading"];
[self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
}

- (void)increaseProgress {
progress += 0.05f;
[SVProgressHUD showProgress:progress status:@"xuanhe Loading"];

if(progress < 1.0f){
[self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
} else {
[self performSelector:@selector(dismiss) withObject:nil afterDelay:0.4f];
}
}

效果如下


还有其他常用的语法:

+(void)showInfoWithStatus :( NSString *)string;
+(void)showSuccessWithStatus :( NSString *)string;
+(void)showErrorWithStatus :( NSString *)string;
+(void)showImage:(UIImage *)image status :( NSString *)string;

取消HUD

HUD可以使用以下方式解除:

+(void)dismiss;
+(void)dismissWithDelay :( NSTimeInterval)delay;
+ (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion;

可以对这些代码进行改进,比如,在弹框结束后执行其他操作.可以封装一个方法,弹框结束后,执行Block.

定制

SVProgressHUD 可以通过以下方法定制:

+ (void)setDefaultStyle:(SVProgressHUDStyle)style;                  // default is SVProgressHUDStyleLight
+ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType; // default is SVProgressHUDMaskTypeNone
+ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type; // default is SVProgressHUDAnimationTypeFlat
+ (void)setContainerView:(UIView*)containerView; // default is window level
+ (void)setMinimumSize:(CGSize)minimumSize; // default is CGSizeZero, can be used to avoid resizing
+ (void)setRingThickness:(CGFloat)width; // default is 2 pt
+ (void)setRingRadius:(CGFloat)radius; // default is 18 pt
+ (void)setRingNoTextRadius:(CGFloat)radius; // default is 24 pt
+ (void)setCornerRadius:(CGFloat)cornerRadius; // default is 14 pt
+ (void)setBorderColor:(nonnull UIColor*)color; // default is nil
+ (void)setBorderWidth:(CGFloat)width; // default is 0
+ (void)setFont:(UIFont*)font; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]
+ (void)setForegroundColor:(UIColor*)color; // default is [UIColor blackColor], only used for SVProgressHUDStyleCustom
+ (void)setBackgroundColor:(UIColor*)color; // default is [UIColor whiteColor], only used for SVProgressHUDStyleCustom
+ (void)setBackgroundLayerColor:(UIColor*)color; // default is [UIColor colorWithWhite:0 alpha:0.4], only used for SVProgressHUDMaskTypeCustom
+ (void)setImageViewSize:(CGSize)size; // default is 28x28 pt
+ (void)setInfoImage:(UIImage*)image; // default is the bundled info image provided by Freepik
+ (void)setSuccessImage:(UIImage*)image; // default is bundled success image from Freepik
+ (void)setErrorImage:(UIImage*)image; // default is bundled error image from Freepik
+ (void)setViewForExtension:(UIView*)view; // default is nil, only used if #define SV_APP_EXTENSIONS is set
+ (void)setGraceTimeInterval:(NSTimeInterval)interval; // default is 0 seconds
+ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval; // default is 5.0 seconds
+ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval; // default is CGFLOAT_MAX
+ (void)setFadeInAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
+ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
+ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel; // default is UIWindowLevelNormal
+ (void)setHapticsEnabled:(BOOL)hapticsEnabled; // default is NO

样式

作为标准SVProgressHUD提供两种预先配置的样式:

SVProgressHUDStyleLight白色背景黑色图标和文字
SVProgressHUDStyleDark黑色背景与白色图标和文本
如果要使用自定义颜色使用setForegroundColor和setBackgroundColor:。这些方法将HUD的风格置为SVProgressHUDStyleCustom。

触觉反馈

对于具有较新设备的用户(从iPhone 7开始),SVProgressHUD可以根据显示的HUD来自动触发触觉反馈。反馈图如下:

showSuccessWithStatus: < - > UINotificationFeedbackTypeSuccess

showInfoWithStatus: < - > UINotificationFeedbackTypeWarning

showErrorWithStatus: < - > UINotificationFeedbackTypeError

要启用此功能,请使用setHapticsEnabled: 。

具有iPhone 7之前的设备的用户将不会改变功能。

通知

SVProgressHUD发布四个通知,NSNotificationCenter以响应被显示/拒绝:

SVProgressHUDWillAppearNotification 提示框即将出现
SVProgressHUDDidAppearNotification 提示框已经出现
SVProgressHUDWillDisappearNotification 提示框即将消失
SVProgressHUDDidDisappearNotification 提示框已经消失

每个通知通过一个userInfo保存HUD状态字符串(如果有的话)的字典,可以通过检索SVProgressHUDStatusUserInfoKey。

SVProgressHUD SVProgressHUDDidReceiveTouchEventNotification当用户触摸整个屏幕或SVProgressHUDDidTouchDownInsideNotification用户直接触摸HUD时也会发布。由于此通知userInfo未被传递,而对象参数包含UIEvent与触摸相关的参数。

应用扩展

这里对这个功能不做详解.自行摸索.

自定义动画

SVProgressHUD提供了方法可以自定义图片.但是不支持gif格式,直接利用下面的方法依然显示一张静态的图片

[SVProgressHUD showImage:[UIImage imageNamed:@"loading.gif"] status:@"加载中..."];

我们可以把gif转化为一个动态的image.
下面是我在百度上搜的一个方法.仅供参考.

#import <UIKit/UIKit.h>

typedef void (^GIFimageBlock)(UIImage *GIFImage);
@interface UIImage (GIFImage)

/** 根据本地GIF图片名 获得GIF image对象 */
+ (UIImage *)imageWithGIFNamed:(NSString *)name;

/** 根据一个GIF图片的data数据 获得GIF image对象 */
+ (UIImage *)imageWithGIFData:(NSData *)data;

/** 根据一个GIF图片的URL 获得GIF image对象 */
+ (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock;

下面是.m的方法实现.

#import "UIImage+GIFImage.h"
#import <ImageIO/ImageIO.h>
@implementation UIImage (GIFImage)
+ (UIImage *)imageWithGIFData:(NSData *)data{

if (!data) return nil;
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
UIImage *animatedImage;
if (count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
} else {
NSMutableArray *images = [NSMutableArray array];
NSTimeInterval duration = 0.0f;
for (size_t i = 0; i < count; i++) {
// 拿出了Gif的每一帧图片
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
//Learning... 设置动画时长 算出每一帧显示的时长(帧时长)
NSTimeInterval frameDuration = [UIImage sd_frameDurationAtIndex:i source:source];
duration += frameDuration;
// 将每帧图片添加到数组中
[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
// 释放真图片对象
CFRelease(image);
}
// 设置动画时长
if (!duration) {
duration = (1.0f / 10.0f) * count;
}
animatedImage = [UIImage animatedImageWithImages:images duration:duration];
}

// 释放源Gif图片
CFRelease(source);
return animatedImage;
}
+ (UIImage *)imageWithGIFNamed:(NSString *)name{
NSUInteger scale = (NSUInteger)[UIScreen mainScreen].scale;
return [self GIFName:name scale:scale];
}

+ (UIImage *)GIFName:(NSString *)name scale:(NSUInteger)scale{
NSString *imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
if (!imagePath) {
(scale + 1 > 3) ? (scale -= 1) : (scale += 1);
imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
}
if (imagePath) {
// 传入图片名(不包含@Nx)
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
return [UIImage imageWithGIFData:imageData];
} else {
imagePath = [[NSBundle mainBundle] pathForResource:name ofType:@"gif"];
if (imagePath) {
// 传入的图片名已包含@Nx or 传入图片只有一张 不分@Nx
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
return [UIImage imageWithGIFData:imageData];
} else {
// 不是一张GIF图片(后缀不是gif)
return [UIImage imageNamed:name];
}
}
}
+ (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock{
NSURL *GIFUrl = [NSURL URLWithString:url];
if (!GIFUrl) return;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSData *CIFData = [NSData dataWithContentsOfURL:GIFUrl];
// 刷新UI在主线程
dispatch_async(dispatch_get_main_queue(), ^{
gifImageBlock([UIImage imageWithGIFData:CIFData]);
});
});
}
#pragma mark - <关于GIF图片帧时长(Learning...)>
+ (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
float frameDuration = 0.1f;
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
if (delayTimeUnclampedProp) {
frameDuration = [delayTimeUnclampedProp floatValue];
}
else {
NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
if (delayTimeProp) {
frameDuration = [delayTimeProp floatValue];
}
}
// Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
// We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
// a duration of <= 10 ms. See and
// for more information.
if (frameDuration < 0.011f) {
frameDuration = 0.100f;
}
CFRelease(cfFrameProperties);
return frameDuration;
}
@end

这个是UIimage的分类,在用到的控制器里面调用代码方法即可.这个分类实现我也不太懂.只会用.

_imgView1.image = [UIImage imageWithGIFNamed:@"xuanxuan"];

NSString *path = [[NSBundle mainBundle] pathForResource:@"xuanxuan" ofType:@"gif"];
NSData *imgData = [NSData dataWithContentsOfFile:path];
_imgView2.image = [UIImage imageWithGIFData:imgData];


[UIImage imageWithGIFUrl:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1495708809771&di=da92fc5cf3bdd684711ab5124ee43183&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%253D580%2Fsign%3D91bd6cd2d42a60595210e1121835342d%2F212eb9389b504fc215d0301ee6dde71190ef6d1a.jpg" and:^(UIImage *GIFImage) {
_imgView3.image = GIFImage;
}];

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

收起阅读 »

IPFS对标HTTP,IPFS的优势是什么?

FIL
区块链技术的高速发展,离不开底层技术的支持,而且肯定先于区块链技术的发展。目前来看,IPFS—Filecoin是最有可能成为区块链底层基础设施的技术。这也表明IPFS—Filecoin必然会随之快速发展。造成这一现象的原因之一在于区块链技术本身的限制,它不能存...
继续阅读 »

区块链技术的高速发展,离不开底层技术的支持,而且肯定先于区块链技术的发展。目前来看,IPFS—Filecoin是最有可能成为区块链底层基础设施的技术。这也表明IPFS—Filecoin必然会随之快速发展。造成这一现象的原因之一在于区块链技术本身的限制,它不能存储存储数据,这也是自区块链技术诞生之后限制区块链技术发展的重要原因之一。IPFS矿机布局,避免踩坑(FIL37373)

Filecoin与IPFS(InterPlanetary File System,星际文件系统)是近两年来非常热门的概念。所谓IPFS是一个基于内容寻址的、分布式的、新型超媒体传输协议。IPFS支持创建完全分布式的应用。它旨在使用网络更快、更安全、更开放。IPFS是一个分布式文件系统,它的目标是将所有计算设备连接到同一个文件系统,从而成为一个全球统一的储存系统。而Filecoin是IPFS的激励层。

IPFS对标HTTP,IPFS的优势是什么?

IPFS星际文件存储系统,是一种p2p协议。相对于传统云存储有以下几个优点:

1. 便宜。IPFS存储空间不由服务商提供,而是接入网络的节点来提供,可以说是任何人都可以成为节点的一部分,所以非常便宜。

2. 速度快。IPFS协议下,文件冗余存储在世界各地,类似于CDN一样。当用户发起下载请求时,附近的借点都会收到信息并传送文件给你,而你只接收最先到达的文件。而传统云服务依赖于中心服务器到你的主机的线路和带宽。IPFS矿机布局,避免踩坑(FIL37373)

3. 安全性高。目前没有任何云存储敢保证自己的服务器不会遭到黑客袭击并保证数据安全。但是IPFS协议下文件在上传的时候会在每个节点保留其记录,系统检测单到文件丢失的时候会自动恢复。且由于其分布性存储的特征,黑客无法同时攻击所有节点。IPFS矿机布局,避免踩坑(FIL37373)

4.隐私保护。对于加密文件的上传使用非对称加密的方式,即除非对方掌握了私钥,否则无法破解。

IPFS分布式存储结构,各项数值优于HTTP,且发布区块链项目Filecoin,能够为IPFS技术存储提供足够的微型存储空间(节点),IPFS,与Filecoin即形成紧密的共生关系,相辅相成。

IPFS网络要想稳定运行需要用户贡献他们的存储空间、网络带宽,如果没有恰当的奖励机制,那么巨大的资源开销很难维持网络持久运转。受到比特币网络的启发,将Filecoin作为IPFS的激励层就是一种解决方案了。对于用户而言,Filecoin能够提高存取速度和效率,能带来去中心化的应用;对于矿工,贡献网络资源可以获得一笔不错的收益。

收起阅读 »

iOS缓存设计(YYCache思路)

iOS缓存设计(YYCache思路)前言:前段时间业务有缓存需求,于是结合YYCache和业务需求,做了缓存层(内存&磁盘)+ 网络层的方案尝试由于YYCache 采用了内存缓存和磁盘缓存组合方式,性能优良,这里拿它的原理来说下如何设计一套缓存的思路,...
继续阅读 »

iOS缓存设计(YYCache思路)

前言:
前段时间业务有缓存需求,于是结合YYCache和业务需求,做了缓存层(内存&磁盘)+ 网络层的方案尝试
由于YYCache 采用了内存缓存和磁盘缓存组合方式,性能优良,这里拿它的原理来说下如何设计一套缓存的思路,并结合网络整理一套完整流程

目录

初步认识缓存
如何优化缓存(YYCache设计思想)
网络和缓存同步流程
一、初步认识缓存

1. 什么是缓存?

我们做一个缓存前,先了解它是什么,缓存是本地数据存储,存储方式主要包含两种:磁盘储存和内存存储

1.1 磁盘存储

磁盘缓存,磁盘也就是硬盘缓存,磁盘是程序的存储空间,磁盘缓存容量大速度慢,磁盘是永久存储东西的,iOS为不同数据管理对存储路径做了规范如下:
1、每一个应用程序都会拥有一个应用程序沙盒。
2、应用程序沙盒就是一个文件系统目录。
沙盒根目录结构:Documents、Library、temp。

磁盘存储方式主要有文件管理和数据库,其特性:


1.2 内存存储

内存缓存,内存缓存是指当前程序运行空间,内存缓存速度快容量小,它是供cpu直接读取,比如我们打开一个程序,他是运行在内存中的,关闭程序后内存又会释放。
iOS内存分为5个区:栈区,堆区,全局区,常量区,代码区

栈区stack:这一块区域系统会自己管理,我们不用干预,主要存一些局部变量,以及函数跳转时的现场保护。因此大量的局部变量,深递归,函数循环调用都可能导致内存耗尽而运行崩溃。
堆区heap:与栈区相对,这一块一般由我们自己管理,比如alloc,free的操作,存储一些自己创建的对象。
全局区(静态区static):全局变量和静态变量都存储在这里,已经初始化的和没有初始化的会分开存储在相邻的区域,程序结束后系统会释放
常量区:存储常量字符串和const常量
代码区:存储代码

在程序中声明的容器(数组 、字典)都可看做内存中存储,特性如下:


2. 缓存做什么?

我们使用场景比如:离线加载,预加载,本地通讯录...等,对非网络数据,使用本地数据管理的一种,具体使用场景有很多

3. 怎么做缓存?

简单缓存可以仅使用磁盘存储,iOS主要提供四种磁盘存储方式:

NSKeyedArchiver: 采用归档的形式来保存数据, 该数据对象需要遵守NSCoding协议, 并且该对象对应的类必须提供encodeWithCoder:和initWithCoder:方法.

//自定义Person实现归档解档
//.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCoding>
@property(nonatomic,copy) NSString * name;

@end

//.m文件
#import "Person.h"
@implementation Person
//归档要实现的协议方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_name forKey:@"name"];
}
//解档要实现的协议方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
@end

使用归档解档

// 将数据存储在path路径下归档文件
[NSKeyedArchiver archiveRootObject:p toFile:path];
// 根据path路径查找解档文件
Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

缺点:归档的形式来保存数据,只能一次性归档保存以及一次性解压。所以只能针对小量数据,如果想改动数据的某一小部分,需要解压整个数据或者归档整个数据。

NSUserDefaults: 用来保存应用程序设置和属性、用户保存的数据。用户再次打开程序或开机后这些数据仍然存在。
NSUserDefaults可以存储的数据类型包括:NSData、NSString、NSNumber、NSDate、NSArray、 NSDictionary。

// 以键值方式存储
[[NSUserDefaults standardUserDefaults] setObject:@"value" forKey:@"key"];
// 以键值方式读取
[[NSUserDefaults standardUserDefaults] objectForKey:@"key"];

Write写入方式:永久保存在磁盘中。具体方法为:

//将NSData类型对象data写入文件,文件名为FileName
[data writeToFile:FileName atomically:YES];
//从FileName中读取出数据
NSData *data=[NSData dataWithContentsOfFile:FileName options:0 error:NULL];

SQLite:采用SQLite数据库来存储数据。SQLite作为⼀一中小型数据库,应用ios中跟其他三种保存方式相比,相对复杂一些

//打开数据库
if (sqlite3_open([databaseFilePath UTF8String], &database)==SQLITE_OK) {
NSLog(@"sqlite dadabase is opened.");
} else { return;}//打开不成功就返回

//在打开了数据库的前提下,如果数据库没有表,那就开始建表了哦!
char *error;
const char *createSql="create table(id integer primary key autoincrement, name text)"; if (sqlite3_exec(database, createSql, NULL, NULL, &error)==SQLITE_OK) {
NSLog(@"create table is ok.");
} else {
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下⼀一次使用
}

// 建表完成之后, 插入记录
const char *insertSql="insert into a person (name) values(‘gg’)";
if (sqlite3_exec(database, insertSql, NULL, NULL, &error)==SQLITE_OK) {
NSLog(@"insert operation is ok.");
} else {
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下一次使用
}

上面提到的磁盘存储特性,具备空间大、可持久、但是读取慢,面对大量数据频繁读取时更加明显,以往测试中磁盘读取比内存读取保守测量低于几十倍,那我们怎么解决磁盘读取慢的缺点呢? 又如何利用内存的优势呢?

二、 如何优化缓存(YYCache设计思想)

YYCache背景知识:
源码中由两个主要类构成


YYMemoryCache (内存缓存)
操作YYLinkedMap中数据, 为实现内存优化,采用双向链表数据结构实现 LRU算法,YYLinkedMapItem 为每个子节点
YYDiskCache (磁盘缓存)
不会直接操作缓存对象(sqlite/file),而是通过 YYKVStorage 来间接的操作缓存对象。
容量管理:

ageLimit :时间周期限制,比如每天或每星期开始清理
costLimit: 容量限制,比如超出10M后开始清理内存
countLimit : 数量限制, 比如超出1000个数据就清理
这里借用YYCache设计, 来讲述缓存优化

1. 磁盘+内存组合优化
利用内存和磁盘特性,融合各自优点,整合如下:


APP会优先请求内存缓冲中的资源
如果内存缓冲中有,则直接返回资源文件, 如果没有的话,则会请求资源文件,这时资源文件默认资源为本地磁盘存储,需要操作文件系统或数据库来获取。
获取到的资源文件,先缓存到内存缓存,方便以后不再重复获取,节省时间。
然后就是从缓存中取到数据然后给app使用。
这样就充分结合两者特性,利用内存读取快特性减少读取数据时间,

YYCache 源码解析:

- (id<NSCoding>)objectForKey:(NSString *)key {
// 1.如果内存缓存中存在则返回数据
id<NSCoding> object = [_memoryCache objectForKey:key];
if (!object) {
// 2.若不存在则查取磁盘缓存数据
object = [_diskCache objectForKey:key];
if (object) {
// 3.并将数据保存到内存中
[_memoryCache setObject:object forKey:key];
}
}
return object;
}

2. 内存优化-- 提高内存命中率

但是我们想在基础上再做优化,比如想让经常访问的数据保留在内存中,提高内存的命中率,减少磁盘的读取,那怎么做处理呢? -- LRU算法


LRU算法:我们可以将链表看成一串数据链,每个数据是这个串上的一个节点,经常访问的数据移动到头部,等数据超出容量后从链表后面的一些节点销毁,这样经常访问数据在头部位置,还保留在内存中。

链表实现结构图:


YYCache 源码解析

/**
A node in linked map.
Typically, you should not use this class directly.
*/
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
/**
A linked map used by YYMemoryCache.
It's not thread-safe and does not validate the parameters.
Typically, you should not use this class directly.
*/
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}

/// Insert a node at head and update the total cost.
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
- (void)removeAll;

@end

_YYLinkedMapNode *_prev 为该节点的头指针,指向前一个节点
_YYLinkedMapNode *_next为该节点的尾指针,指向下一个节点
头指针和尾指针将一个个子节点串连起来,形成双向链表

来看下bringNodeToHead:的源码实现,它是实现LRU算法主要方法,移动node子结点到链头。

(详细已注释在代码中)

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
if (_head == node) return; // 如果当前节点是链头,则不需要移动

// 链表中存了两个指向链头(_head)和链尾(_tail)的指针,便于链表访问
if (_tail == node) {
_tail = node->_prev; // 若当前节点为链尾,则更新链尾指针
_tail->_next = nil; // 链尾的尾节点这里设置为nil
} else {
// 比如:A B C 链表, 将 B拿走,将A C重新联系起来
node->_next->_prev = node->_prev; // 将node的下一个节点的头指针指向node的上一个节点,
node->_prev->_next = node->_next; // 将node的上一个节点的尾指针指向node的下一个节点
}
node->_next = _head; // 将当前node节点的尾指针指向之前的链头,因为此时node为最新的第一个节点
node->_prev = nil; // 链头的头节点这里设置为nil
_head->_prev = node; // 之前的_head将为第二个节点
_head = node; // 当前node成为新的_head
}

其他方法就不挨个举例了,具体可翻看源码,这些代码结构清晰,类和函数遵循单一职责,接口高内聚,低耦合,是个不错的学习示例!

3. 磁盘优化 - 数据分类存储

YYDiskCache 是一个线程安全的磁盘缓存,基于 sqlite 和 file 来做的磁盘缓存,我们的缓存对象可以自由的选择存储类型,
下面简单对比一下:

sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。
file: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。
所以 YYDiskCache 使用两者配合,灵活的存储以提高性能。

另外:
YYDiskCache 具有以下功能:

它使用 LRU(least-recently-used) 来删除对象。
支持按 cost,count 和 age 进行控制。
它可以被配置为当没有可用的磁盘空间时自动驱逐缓存对象。
它可以自动抉择每个缓存对象的存储类型(sqlite/file)以便提供更好的性能表现。
YYCache源码解析

// YYKVStorageItem 是 YYKVStorage 中用来存储键值对和元数据的类
// 通常情况下,我们不应该直接使用这个类
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; ///< key
@property (nonatomic, strong) NSData *value; ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size; ///< value's size in bytes
@property (nonatomic) int modTime; ///< modification unix timestamp
@property (nonatomic) int accessTime; ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end


/**
YYKVStorage 是基于 sqlite 和文件系统的键值存储。
通常情况下,我们不应该直接使用这个类。

@warning
这个类的实例是 *非* 线程安全的,你需要确保
只有一个线程可以同时访问该实例。如果你真的
需要在多线程中处理大量的数据,应该分割数据
到多个 KVStorage 实例(分片)。
*/
@interface YYKVStorage : NSObject

#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path; /// storage 路径
@property (nonatomic, readonly) YYKVStorageType type; /// storage 类型
@property (nonatomic) BOOL errorLogsEnabled; /// 是否开启错误日志

#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;

#pragma mark - Save Items
- (BOOL)saveItem:(YYKVStorageItem *)item;
...

#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
...

#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
...

#pragma mark - Get Storage Status
- (BOOL)itemExistsForKey:(NSString *)key;
- (int)getItemsCount;
- (int)getItemsSize;

@end

我们只需要看一下 YYKVStorageType 这个枚举,它决定着 YYKVStorage 的存储类型。

YYKVStorageType

/**
存储类型,指示“YYKVStorageItem.value”存储在哪里。

@discussion
通常,将数据写入 sqlite 比外部文件更快,但是
读取性能取决于数据大小。在测试环境 iPhone 6s 64G,
当数据较大(超过 20KB)时从外部文件读取数据比 sqlite 更快。
*/
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0, // value 以文件的形式存储于文件系统
YYKVStorageTypeSQLite = 1, // value 以二进制形式存储于 sqlite
YYKVStorageTypeMixed = 2, // value 将根据你的选择基于上面两种形式混合存储
};

总结:

这里说了YYCache几个主要设计优化之处,其实细节上也有很多不错的处理,比如:

线程安全
如果说 YYCache 这个类是一个纯逻辑层的缓存类(指 YYCache 的接口实现全部是调用其他类完成),那么 YYMemoryCache 与 YYDiskCache 还是做了一些事情的(并没有 YYCache 当甩手掌柜那么轻松),其中最显而易见的就是 YYMemoryCache 与 YYDiskCache 为 YYCache 保证了线程安全。
YYMemoryCache 使用了 pthread_mutex 线程锁来确保线程安全,而 YYDiskCache 则选择了更适合它的 dispatch_semaphore,上文已经给出了作者选择这些锁的原因。

性能

YYCache 中对于性能提升的实现细节:

异步释放缓存对象
锁的选择
使用 NSMapTable 单例管理的 YYDiskCache
YYKVStorage 中的 _dbStmtCache
甚至使用 CoreFoundation 来换取微乎其微的性能提升

3. 网络和缓存同步流程

结合网络层和缓存层,设计了一套接口缓存方式,比较灵活且速度得到提升; 比如首页界面可能由多个接口提供数据,没有采用整块存储而是将存储细分到每个接口中,有API接口控制,基本结构如下:

主要分为:

应用层 :显示数据
管理层: 管理网络层和缓存层,为应用层提供数据支持
网络层: 请求网络数据
缓存层: 缓存数据
层级图:


服务端每套数据对应一个version (或时间戳),若后台数据发生变更,则version发生变化,在返回客户端数据时并将version一并返回。
当客户端请求网络时,将本地上一次数据对应version上传。
服务端获取客户端传来得version后,与最新的version进行对比,若version不一致,则返回最新数据,若未发生变化,服务端不需要返回全部数据只需返回304(No Modify) 状态值
客户端接到服务端返回数据,若返回全部数据非304,客户端则将最新数据同步到本地缓存中;客户端若接到304状态值后,表示服务端数据和本地数据一致,直接从缓存中获取显示
这也是ETag的大致流程;详细可以查看 https://baike.baidu.com/item/ETag/4419019?fr=aladdin

源码示例

- (void)getDataWithPage:(NSNumber *)page pageSize:(NSNumber *)pageSize option:(DataSourceOption)option completion:(void (^)(HomePageListCardModel * _Nullable, NSError * _Nullable))completionBlock {
NSString *cacheKey = CacheKey(currentUser.userId, PlatIndexRecommendation);// 全局静态常量 (userid + apiName)
// 根据需求而定是否需要缓存方式,网络方式走304逻辑
switch (option) {
case DataSourceCache:
{
if ([_cache containsObjectForKey:cacheKey]) {
completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
} else {
completionBlock(nil, LJDError(400, @"缓存中不存在"));
}
}
break;
case DataSourceNetwork:
{
[NetWorkServer requestDataWithPage:page pageSize:pageSize completion:^(id _Nullable responseObject, NSError * _Nullable error) {
if (responseObject && !error) {
HomePageListCardModel *model = [HomePageListCardModel yy_modelWithJSON:responseObject];
if (model.errnonumber == 304) { //取缓存数据
completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
} else {
completionBlock(model, error);
[self->_cache setObject:model forKey:cacheKey]; //保存到缓存中
}
} else {
completionBlock(nil, error);
}
}];
}
break;

default:
break;
}
}

这样做好处:

对于不频繁更新数据的接口,节省了大量JSON数据转化时间
节约流量,节省加载时长
用户界面显示加快
总结:项目中并不一定完全这样做,有时候过渡设计也是一种浪费,多了解其他设计思路后,针对项目找到适合的才是最好的!

参考文献:
YYCache: https://github.com/ibireme/YYCache
YYCache 设计思路 :https://blog.ibireme.com/2015/10/26/yycache/

链接:https://www.jianshu.com/p/b592ee20f09a

收起阅读 »

iOS进阶:WebViewJavascriptBridge源码解读

WebViewJavascriptBridge GitHub地址jsBridge框架是解决客户端与网页交互的方法之一。最主要的实现思路是客户端在webivew的代理方法中拦截url,根据url的类型来做不同处理。接下去会以jsBridge提供demo中的为例,...
继续阅读 »

WebViewJavascriptBridge GitHub地址

jsBridge框架是解决客户端与网页交互的方法之一。最主要的实现思路是客户端在webivew的代理方法中拦截url,根据url的类型来做不同处理。接下去会以jsBridge提供demo中的为例,从使用的角度,一步步分析它是如何实现的。

注:在iOS8后,苹果推出了WKWebView。对于UIWebView和WKWebView,jsBridge都能实现客户端与网页交互,且实现的方式类似,因此本文会以UIWebView为例来分析。

本文会通过以下几点来介绍框架的实现:

框架结构
WebViewJavascriptBridge_JS
WebViewJavascriptBridge WKWebViewJavascriptBridge
WebViewJavascriptBridgeBase
网页通知客户端的实现
客户端通知网页的实现
js环境注入问题
总结

框架结构


WebViewJavascriptBridge_JS

WebViewJavascriptBridge_JS 简单的说就是网页的js环境,需要客户端在网页初始化的时候注入到网页中去。如果不注入就无法实现网页与客户端的交互。该类只有一个返回值为NSString 的方法:NSString * WebViewJavascriptBridge_js(); 。

至于究竟何时注入,如何注入,会在接下去的分析中写到。

WebViewJavascriptBridge WKWebViewJavascriptBridge

这两个类分别对应UIWebView和WKWebView。看名字就可以知道这两个类是交互的桥梁,不管是网页同时客户端还是客户端通知网页,都是通过这两个类来完成通知的。

WebViewJavascriptBridgeBase

WebViewJavascriptBridgeBase个人认为类似数据处理工具类。

该类中存着客户端注册的方法以及对应实现:@property (strong, nonatomic) NSMutableDictionary* messageHandlers;

也存着客户端通知网页后的回调实现:@property (strong, nonatomic) NSMutableDictionary* responseCallbacks;

同时,该类还实现了之前提的网页js环境注入方法:-(void)injectJavascriptFile;

还有一些url类别判断方法,这里不一一举例了。

网页通知客户端的实现

要让客户端能够响应网页的通知,首先必须使用桥梁注册方法名和实现,然后存起来,等待网页的通知。

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];

客户端注册方法时,bridge做了些什么事情呢?其实bridge只是简单地将方法名和实现block分别作为键值存到了messageHandlers属性中。

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}

接下来,网页想要调用客户端的testObjcCallback方法了。网页上有一个按钮,点击后调用客户端方法,网页的js代码如下:

var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
callbackButton.innerHTML = 'Fire testObjcCallback'
callbackButton.onclick = function(e) {
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
log('JS got response', response)
})
}

这里网页调用的方法为bridge.callHandler,这里你可能会有疑问,为什么bridge对象哪来的,callHandler方法又是哪来的。关于这个,这边先简单的说一下:这个bridge其实就是我们之前提到的js环境提供的,callHandler方法也是环境中的代码实现的,如果没有js环境,网页就拿不到bridge,也就无法成功调起客户端的方法。这边可以简单的理解为这个环境就相当于是我们客户端的WebViewJavascriptBridge框架,客户端如果不导入,也就无法使用jsbridge。网页也是类似,如果不注入,就无法使用jsbridge。而区别就在于,客户端的这个框架是运行前导入的,而网页这个环境是由客户端加载到该网页时,动态注入的。

至于详细的注入,会在下文中分析说明。

js环境文件中,bridge.callHandler方法实现:

function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName:handlerName, data:data }, responseCallback);
}


function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

由于本质上网页处理发送通知的思路和客户端的一致,而我们队客户端的oc代码更好理解,因此我打算将这段代码的分析跳过,等到分析客户端通知网页时,再仔细讲。这边只需要知道

1.字典中加了一个callbackId字段,这个字段是用来等客户端调用完方法后,网页能找到对应的实现的。同时网页将实现存到了它管理的字典中:responseCallbacks[callbackId] = responseCallback;

2.网页最终将字典压到了sendMessageQueue中,并调用了messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';

3.字典中的数据是:

{   
handlerName : "testObjcCallback",
data : {'foo': 'bar'},
callbackId : 'cb_'+(uniqueId++)+'_'+new Date().getTime()
}

这时,客户端的webview代码方法就能拦截到url:


正是网页调用的:https://__wvjb_queue_message__/。然后客户端是如果去判断url并做相应处理呢?下面为拦截的源码:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }

NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}

这时,由于传过来的是https://__wvjb_queue_message__/,会进[_base isQueueMessageURL:url]的判断中,然后做以下处理:

NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];

第一行代码为从网页的sendMessageQueue中获取到数据,还记得之前网页把调用的相关数据存到了sendMessageQueue中吗?这个时候,客户端又把它取出来了。然后第二行代码,客户端开始处理这个数据:

- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}

id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];

NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}

WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}

handler(message[@"data"], responseCallback);
}
}
}

这段代码有点多,核心思路是将获得的数据转换成字典,然后从客户端的messageHandlers中取出方法名对应的block,并调用:handler(message[@"data"], responseCallback);


这边还需要特别注意的是,callbackId问题。在这个例子中,是存在callbackId的,因为网页是有写调用完客户端后的回调的,所以这边做了处理,如果有callbackId的话,再创建一个responseCallback,等客户端调用完网页通知的方法后再调用。

还记得当初客户端注册方法时的代码吗:

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"testObjcCallback called: %@", data);
responseCallback(@"Response from testObjcCallback");
}];

这边就将这个handler的block取出来,然后将message[@"data"]和responseCallback作为参数调用。调用完后又调用了responseCallback,将数据又发回网页去。这边具体的发送会在下文客户端通知网页分析中写到。这边这需要知道,如果存在callbackId,就会将callbackId和数据又发回网页。

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];

以上就是网页通知客户端的大致实现。

客户端通知网页

其实客户端通知网页的大致思路是和上文类似的。在客户端调用之前,网页肯定是已经注册好了客户端要调用的方法,就如上文中,客户端也已经注册好了网页通知的方法一样。下面为网页注册的代码:

bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
log('ObjC called testJavascriptHandler with', data)
var responseData = { 'Javascript Says':'Right back atcha!' }
log('JS responding with', responseData)
responseCallback(responseData)
})

看看registerHandler方法如何实现:

function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}

恩,是不是和客户端的注册非常相似?

接下来再看看客户端是如何调用的:

- (void)callHandler:(id)sender {
id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
NSLog(@"testJavascriptHandler responded: %@", response);
}];
}

callHandler方法实现:

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

sendData实现:

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];

if (data) {
message[@"data"] = data;
}

if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}

if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}

客户端将数据封装成一个字段,这时这个字典的值为:

{
callbackId = "objc_cb_1";
data = {
greetingFromObjC = "Hi there, JS!";
};
handlerName = testJavascriptHandler;
}

还是和网页的处理非常一致。下面看看客户端是如何通知网页的:

- (void)_queueMessage:(WVJBMessage*)message {
if (self.startupMessageQueue) {
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}

- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];

NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];

} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}

客户端将字段转成js字符串,然后注入到网页中实现通知。具体方法是调用了js环境中的_handleMessageFromObjC方法,参数为字典转换后的字符串。下面看看_handleMessageFromObjC方法的实现:

function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}

function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}

function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;

if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
}

var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}

这边的处理其实和上文客户端处理message字典时没什么区别的。

这边要提一下的是这个responseId的判断逻辑,还记得网页通知客户端分析中,由于网页有实现通知完客户端后的代码,所以客户端将网页传递过来的callbackId作为responseId参数又传回去了:

WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];

这边网页的处理是,从responseCallbacks中根据这个"responseId":callbackId字段取出block并调用,代码如下:

if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
}

如果看到这里有点乱了,可以再看看网页通知客户端时对于字典的处理部分。

以上就是客户端通知网页的大致实现。

js环境注入问题

上文一提到这个,就说下文讲解,现在终于可以分析这一块了。

其实这个比较简单,本质上就是网页调用了一个特殊的,jsbridge规定的url,使得客户端可以拦截到并分析出是需要注入js环境的通知。然后客户端开始注入。

网页部分的代码:

WVJBIframe.src = 'https://__bridge_loaded__';

一般这个是放在网页代码的最前面的。这样做可以让客户端在最早的情况下将环境注入到网页中。

而客户端是如何处理的呢?

if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
}
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
}

看到了吧,客户端调用WebViewJavascriptBridge_JS类的唯一的方法:NSString * WebViewJavascriptBridge_js(); ,然后通过_evaluateJavascript注入。

总结

以网页通知客户端为例:客户端会将要被调用的方法存到字典中,同时拦截网页的调用,当网页调用时,从字典中取出方法并调用。调用完后,判断网页是否有调用完的回调,如果有,再将回调的id和参数通过客户端调用网页的方式通知过去。这就完成了网页通知客户端的总体流程。

最后

这个框架是在去年就已经看完了,由于忙+懒,拖到今天才终于准备写一下。花了一下午的时间,将大体的逻辑理清楚并用文字的方式表达出来,但是由于昨晚没睡舒服,现在脑子还是有点乱,所以文章中应该有部分错别字,麻烦看到了指出一下方便我改正。还有一点,对于之前没接触过的同学,由于在调用时有responseId和callbackId,会比较乱,在此建议多看几遍。如果实在理解不了,可以评论或加我微信,我会尽我努力让你理解。最后,谢谢你的耐心阅读😆😆

链接:https://www.jianshu.com/p/7bd7260daf94

收起阅读 »

Flutter IM跨端架构设计和实现

作者:闲鱼技术——祈晴1. 闲鱼IM现状闲鱼IM框架构建于2016-2017年,期间多次迭代升级导致历史包袱累积多,后经IM界面Flutter化,造成架构更复杂,开发层面总结闲鱼当前架构主要存在如下几个问题:•研发效率较低:当前架构开发需求涉及到Android...
继续阅读 »

作者:闲鱼技术——祈晴

1. 闲鱼IM现状

闲鱼IM框架构建于2016-2017年,期间多次迭代升级导致历史包袱累积多,后经IM界面Flutter化,造成架构更复杂,开发层面总结闲鱼当前架构主要存在如下几个问题:

•研发效率较低:当前架构开发需求涉及到Android/iOS双端的逻辑代码以及Flutter的UI界面代码,定位问题往往只能从Flutter UI表相追查到Native逻辑漏洞;•架构层次较差:架构设计上分层不清晰,业务逻辑夹杂在核心的逻辑层致使代码变更风险大;•性能测试略差:核心数据源存储Native内存,需经Flutter Plugin将数据源序列化上抛Flutter侧,在大批量数据源情况下性能表现较差;

从舆情层面总结闲鱼IM当前架构的主要问题如下:

•定位问题困难:线上舆情反馈千奇百怪,测试始终无法复现相关场景,因此很多时候只能靠现象猜测本质;•疑难杂症较多:架构不稳定性造成出现的问题反复出现,当前疑难杂症主要包括未读红点计数,iPhone5C低端机器架构,以及多媒体发送等多个问题;•问题差异性大:Android和iOS两端逻辑代码差异大,包括现存埋点逻辑都不尽相同,因此排查问题根源时候双端都会有不同问题根因,解决问题方案也不相同;

2.业界跨端方案

为解决当前IM痛点,闲鱼今年特起关于IM架构升级项目,重在解决客户端中双端一致性痛点,初步设想方案就是实现跨端统一的Android/iOS逻辑架构;在当前行业内跨端方案可初步归类如下图架构,在GUI层面的跨端方案有Weex,ReactNative,H5,Uni-APP等,其内存模型大多需要通过桥接到Native模式存储;在逻辑层面的跨端方案大致有C/C++等与虚拟机无关语言实现跨端,当然汇编语言也可行;此外有两个独立于上述体系之外的架构就是Flutter和KMM(谷歌基于Kotlin实现类似Flutter架构),其中Flutter运行特定DartVM,将内存数据挂载其自身的isolate中;undefined

考虑闲鱼是Flutter的前沿探索者,方案上优先使用Flutter;然而Flutter的isolate更像一个进程的概念(底层实现非使用进程模式),相比Android,同一进程场景中,Android的Dalvik虚拟机多个线程运行共享一个内存Heap,而DartVM的Isolate运行隔离各自的Heap,因而isolate之间通讯方式比较繁琐(需经过序列化反序列化过程);整个模型如下图所示:undefined

若按官方混合架构实现Flutter应用,开启多个FlutterAcitivty/FlutterController,底层会生成多个Engine,对应会存在多个isolate,而isolate通讯类似于进程通讯(类似socket或AIDL),这里借鉴闲鱼FlutterBoost的设计理念,FlutterIM架构将多个页面的Engine共享,则内存模型就天然支持共享读取,原理图如下:

undefined

3.Flutter IM架构设计

3.1 新老架构对比

如下图是一个老架构方案,其核心问题主要集中于Native逻辑抽象差,其中逻辑层面还设计到多线程并发使得问题倍增,Android/iOS/Flutter交互繁杂,开发维护成本高,核心层耦合较为严重,无插拔式概念;undefined

考虑到历史架构的问题,演进如下新架构设计undefined

架构从上至下依次为业务层分发层逻辑层以及数据源层,数据源层来源于推送或网络请求,其封装于Native层,通过Flutter插件将消息协议数据上抛到Flutter侧的核心逻辑层,处理完成后变成Flutter DB的Enitity实体,实体中挂载一些消息协议实体;核心逻辑层将繁杂数据扁平化打包挂载到分发层中的会话内存模型数据或消息内存模型数据,最后通过观察者模式的订阅分发到业务逻辑中;Flutter IM重点集中改造逻辑层和分发层,将IM核心逻辑和业务层面数据模型进行封装隔离,核心逻辑层和数据库交互后将数据封装到分发层的moduleData中,通过订阅方式分发到业务层数据模型中;此外在IM模型中DB也是重点依赖的,个人对DB数据库管理进行全面封装解,实现一种轻量级,性能佳的Flutter DB管理框架;

3.2 DB存储模型

Flutter IM架构的DB存储依赖数据库插件,目前主流插件是Sqflite,其存储模型如下:undefined依据上图Sqflite插件的DB存储模型会有2个等待队列,一个是Flutter层同步执行队列,一个是Native层的线程执行队列,其Android实现机制是HandlerThread,因此Query/Save读写在会同一线程队列中,导致响应速度慢,容易造成DB SQL堆积,此外缺失缓存模型,于是个人定制如下改进方案undefinedFlutter侧通过表的主键设计查询时候会优先从Entity Cache层去获取,若缓存不存在,则通过Sqflite插件查询,同时改造Sqflite插件成支持sync/Async同步异步两种方式操作,对应到Native侧也会有同步线程队列和异步线程队列,保证数据吞吐率;但是这里建议查询使用异步,存储使用同步更稳妥,主要怕出现多个相同的数据元model同一时间进入异步线程池中,存储先后顺序无法有效的保证;

3.3 ORM数据库方案

IM架构重度依赖DB数据库,而当前业界还没有一个完备的数据库ORM管理方案,参考了Android的OrmLite/GreenDao,个人自行设计一套Flutter ORM数据库管理方案,其核心思想如下:undefined由于Flutter不支持反射,因此无法直接像Android的开源数据库方式操作,但可通过APT方式,将Entity和Orm Entity绑定于一身,操作OrmEntity即操作Entity,整个代码风格设计也和OrmLite极其相似,参考代码如下:

undefined

3.4 IM内存数据模型

FlutterIM架构在内存数据模型主要划分为会话和消息两个颗粒度,会话内存数据模型交托于SessionModuleData,消息内存数据模型交托于MessageModuleData;会话内存数据有一个根节点RootNotice,然后其挂载PSessionMessageNotice(这里PSessionMessageNotice是ORM映射的会话DB表模型)子节点集合;消息内存数据会有一个MessageConatiner容器管理,其内部挂载此会话中的PMessage(PMessage是ORM映射的消息DB表模型)消息集合;

依据上一章节,PSessionMessageNotice设计了一个OrmEnitity Cache,考虑到IM中会话数是有限的,因此PSessionMessageNotice都是直接缓存到Cache中,这种做法的好处是各地去拿会话数据元时候都是缓存中同一个对象,容易保证多次重复读写的数据一致性;而PSessionMessageNotice考虑到其数量可以无限多的特殊性,因此这里将其挂载到MessageContainer的内存管理中,在退出会话的时机会校验容器中PMessage集合的数量,适当缩容可以减少内存开销,模型如下图所示:undefined

3.5 状态管理方案

Flutter IM状态管理方案比较简单,对数据源Session/Message维度使用观察者模式的订阅分发方式实现,架构类似于EventBus模式,页面级的状态管理无论使用fish-redux,scopeModel或者provider几乎影响面不大,核心还是需保留一种插拔式抽象更重要;架构如下图:undefined

3.6 IM同步模型方案

如下是当前现状的消息同步模型,模型中存在ACCS Thread/Main Thread/Region Thread等多线程并发场景,导致易出现多线程高并发的问题;native的推送和网络请求同步的隔离方案通过Lock的锁机制,并且通过队列降频等方式处理,流程繁琐且易出错。整体通过Region Version Gap去判断是否有域空洞,进而执行域同步补充数据。undefined改进的同步模型如下,在Flutter侧天然没多线程场景,通过一种标记位的转化同步异步实现类似Handler消息队列,架构清晰简约了很多,避免锁带来的开销以及同步问题,undefined

4.进展以及性能对比

•针对架构层面:在FlutterIM架构中,重点将双端逻辑差异性统一成同一份Dart代码,完全磨平Android/iOS的代码差异性带来的问题,降低开发维护,测试回归,视觉验收的一半成本,极大提高研发效率;架构上进行重构分层,实现一种解耦合,插拔式的IM架构;同时Native到Flutter侧的大量数据上抛序列化过程改造程Flutter引用传递,解决极限测试场景下的私聊卡顿问题;•针对线上舆情:补齐UT和TLog的集团日志方式做到可追踪,可排查;另外针对于很多现存的疑难杂症重点集中专项解决,比如iphone5C的架构在Flutter侧统一规划,未读红点计数等问题也在架构模型升级中修复,此外多媒体音视频发送模块进行改造升级;•性能数据对比:当IM架构的逻辑层和UI层都切换成Flutter后,和原先架构模式初步对比,整体内存水位持平,其中私聊场景下小米9测试结构内存下降40M,功耗降低4mah,CPU降低1%;极限测试场景下新架构内存数据相比于旧架构有一个较为明显的改观,主要由于两个界面都使用Flutter场景下,页面切换的开销降低很多;

5.展望

JS跨端不安全,C++跨端成本有点高,Flutter会是一个较好选择;彼时闲鱼FlutterIM架构升级根本目的从来不是因Flutter而Flutter,是由于历史包袱的繁重,代码层面的维护成本高,新业务的扩展性差,人力配比不协调以及疑难杂症的舆情持续反馈等等因素造成我们不得不去探索新方案。经过闲鱼IM超复杂业务场景验证Flutter模式的逻辑跨端可行性,闲鱼在Flutter路上会一直保持前沿探索,最后能反馈到生态圈;总结一句话,探索过程在于你勇于迈出第一步,后面才会不断惊喜发现

收起阅读 »

Jetpack—架构组件—App Startup

App Startup介绍作用这是官网的截图,大意就是 App Startup 是一种用来在 app 启动时候规范初始化数据的 library。同时使用 App Startup 可以解决我们平时滥用 ContentProvider 导致的启动变慢问题。还有一点...
继续阅读 »

App Startup

介绍作用

这是官网的截图,大意就是 App Startup 是一种用来在 app 启动时候规范初始化数据的 library。同时使用 App Startup 可以解决我们平时滥用 ContentProvider 导致的启动变慢问题。

还有一点,App Startup 可以用于 app 开发,也可以用来进行 sdk 开发

App Startup 的优势

  1. 平时使用 ContentProvider 自动获取 ApplicationContext 的方式管理混乱,并且多个 ContentProvider 初始化的方式也无法保证初始化的顺序

  2. 统一管理的方式可以明显提升 app 初始化速度,注:仅限于用较多 ContentProvider 来初始化应用的 app,反之不是不能用,只是没有优化效果

依赖

dependencies {
implementation("androidx.startup:startup-runtime:1.0.0")
}
复制代码

使用 AppStartup 初始化全局单例对象(main 分支)

  1. Car 对象
class Car(private val name: String) {
companion object {
var instance: Car? = null
fun getInstance(name: String): Car {
if (instance == null) {
instance = Car(name)
}
return instance!!
}
}

override fun toString(): String {
return "$name ${Random.nextInt(100)}"
}
}
复制代码
  1. 首先需要实现一个 Initializer
class AndroidInitializer : Initializer<Car> {
override fun create(context: Context): Car {
return Car.getInstance("出租车")
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码
  1. 在代码中注册 AndroidInitializer
 <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
>
<meta-data
android:name="com.ananananzhuo.appstartupdemo.AndroidInitializer"
android:value="androidx.startup" />
</provider>
复制代码
  1. 分析

本例中 Car 对象,Car 对象内部维护了一个全局单例方法 getInstance。

前面说了,AppStartup 是用来维护全局单例的,那么实际上这个单例的初始化就是通过我们定义的 AndroidInitializer 对象 create 方法来初始化的。

  1. 我们会在 MainActivity 中调用 Car 的 toString 方法,代码如下
 logEE(Car.getInstance("小汽车:").toString())
logEE(Car.getInstance("小汽车:").toString())
logEE(Car.getInstance("小汽车:").toString())
复制代码

我们调用了,三次 toString 方法

代码输出如下:

我们 MainActivity 中代码 getInstance 传入的参数是 "小汽车",但是打印的却是 "出租车"。查看 AndroidInitializer 中的代码发现,我们在 AndroidInitializer 中的 create 方法中创建对象的参数是 "出租车"。

由此可以证明,我们的全局 Car 单例在 AndroidInitializer 中就已经初始化完成了。

手动初始化组件

上一节中我们使用在 Manifest 中注册组件的方式实现 Car 对象的自动初始化。

但是,实际上我们是可以不在 Manifest 中注册的方式实现初始化的,手动初始化的方式如下:

 AppInitializer.getInstance(this)
.initializeComponent(AndroidInitializer::class.java)
复制代码

这种方式的弊端是一次只能初始化一个组件

实现相互依赖的多实例的初始化(分支:multimodule)

通过上一节的学习,你可能会有这样的疑问:AppStartup 啥用没有吧,我直接在 Application 中一行代码初始化不香吗,非要用你这种方式???

那么现在我就要用 AppStartup 实现多实例的初始化,让你进一步了解 AppStartup 的应用

我们这一节的逻辑先描述一下:

本例中我们需要创建两个对象,Person 和 Noodle,两者都是全局单例的。

Person 持有 Noodle 对象的引用,

Person 中有一个 eat 方法,本例中我们的 eat 会输出一行 "某某人" 吃 "面条" 的日志

废话不多说,上代码:

不要嫌代码长,都是一看就懂的逻辑

  1. Person 和 Noodle
class Person(val name:String) {
private var noodle: Noodle? = null
companion object {
private var instance: Person? = null
fun getInstance(name:String): Person {
if (instance == null) {
instance = Person(name)
}
return instance!!
}
}

fun addNoodle(paramsnoodle: Noodle) {
noodle = paramsnoodle
}


fun eat() {
logEE("${name} 吃 ${noodle?.name}")
}
}
复制代码
class Noodle {
val name = "面条"

companion object {
private var instance: Noodle? = null
fun getInstance(): Noodle {
if (instance == null) {
instance = Noodle()
}
return instance!!
}
}
}
复制代码
  1. PersonInitializer、NoodleInitializer
class PersonInitializer : Initializer<Person> {
override fun create(context: Context): Person {
return Person.getInstance("李白").apply {
addNoodle(Noodle.getInstance())
}
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf(NoodleInitializer::class.java)
}
}
复制代码

class NoodleInitializer:Initializer<Noodle> {
override fun create(context: Context): Noodle {
return Noodle.getInstance()
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码

这两个组件中 PersonInitializer 的 create 方法中创建了 Person 的实例,并向里面添加 Noodle 的实例。

划重点:

PersonInitializer 的 dependencies 方法中返回了 mutableListOf(NoodleInitializer::class.java)。这句代码的意思是在 PersonInitializer 中的 Person 初始化之前会先初始化 NoodleInitializer 中的 Noodle 实例,然后当 PersonInitializer 中 addNoodle 的时候 Noodle 全局单例已经创建好了。

  1. 调用吃面条方法
Person.getInstance("杜甫").eat()
复制代码
  1. 打印日志输出

日志输出符合我们的预期

多实例的注册组件方式如下,我们将 PersonInitializer、NoodleInitializer 都被注册到 meta-data 中了。

实际上,NoodleInitializer 的组件是完全可以不注册的,因为在 PersonInitializer 的 dependencies 中已经声明了 NoodleInitializer 组件。

  <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false">
<meta-data
android:name="com.ananananzhuo.appstartupdemo.PersonInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.ananananzhuo.appstartupdemo.NoodleInitializer"
android:value="androidx.startup" />
</provider>
复制代码

使用 AppStartup 进行 sdk 开发(分支:sdk_develop)

本例介绍 sdk 开发中 AppStartup 的使用,实际上与应用开发是一样的,但是感觉还是有必要说一下。

在本例中我们新建了一个 library 的 module,在 library 里面编写了我们的 AppStartup 的代码逻辑,然后将 Library 打包成 arr,集成到 app 模块中,在 app 的 Manifest 中注册组件,并调用组件的相关方法。

  1. aar 集成 

  2. library 中的代码

class LibraryInitializer:Initializer<Student> {
override fun create(context: Context): Student {
return Student.getInstance()
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
复制代码
class Student(val name: String) {
companion object {
private val student = Student("安安安安卓")
fun getInstance(): Student {
return student
}
}

fun study() {
Log.e("tag", "${name} 好好学习")
}
}
复制代码
  1. Manifest 中注册组件
 <provider
android:name="androidx.startup.InitializationProvider"
android:authorities="com.ananananzhuo.appstartupdemo.androidx-startup"
android:exported="false"
>
<meta-data
android:name="com.ananananzhuo.library.LibraryInitializer"
android:value="androidx.startup" />
</provider>
复制代码
  1. 日志打印

  1. 结论

通过这种方式,第三方 sdk 只需要定义自己的 AppStartup 组件就可以,我们在注册组件的时候在 manifest 中添加第三方组件的信息就可以完成第三方组件的初始化了。

这极大的避免了某些自以为是的 sdk,打着方便我们集成的名义搞 ContentProvider 初始化恶心我们

以后如果你合作的第三方 sdk 提供方再出现 ContentProvider 的初始化方式恶心你,那么拿出我的文章好好教他做人。

收起阅读 »

SpannableStringBuiler封装Kotlin

前言SpannableStringBuilder和SpannableString功能基本一样,不过SpannableStringBuilder可以拼接,主要是通过setSpan来实现各种效果,主要的方法如下:start: 指定Span的开始位置 end: 指定...
继续阅读 »

前言

SpannableStringBuilder和SpannableString功能基本一样,不过SpannableStringBuilder可以拼接,主要是通过setSpan来实现各种效果,主要的方法如下:

start: 指定Span的开始位置
end: 指定Span的结束位置,并不包括这个位置。
flags:取值有如下四个
Spannable. SPAN_INCLUSIVE_EXCLUSIVE:前面包括,后面不包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本不会应用该样式
Spannable. SPAN_INCLUSIVE_INCLUSIVE:前面包括,后面包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本也会应用该样式
Spannable. SPAN_EXCLUSIVE_EXCLUSIVE:前面不包括,后面不包括
Spannable. SPAN_EXCLUSIVE_INCLUSIVE:前面不包括,后面包括
what: 对应的各种Span,不同的Span对应不同的样式。已知的可用类有:
BackgroundColorSpan : 文本背景色
ForegroundColorSpan : 文本颜色
MaskFilterSpan : 修饰效果,如模糊(BlurMaskFilter)浮雕
RasterizerSpan : 光栅效果
StrikethroughSpan : 删除线
SuggestionSpan : 相当于占位符
UnderlineSpan : 下划线
AbsoluteSizeSpan : 文本字体(绝对大小)
DynamicDrawableSpan : 设置图片,基于文本基线或底部对齐。
ImageSpan : 图片
RelativeSizeSpan : 相对大小(文本字体)
ScaleXSpan : 基于x轴缩放
StyleSpan : 字体样式:粗体、斜体等
SubscriptSpan : 下标(数学公式会用到)
SuperscriptSpan : 上标(数学公式会用到)
TextAppearanceSpan : 文本外貌(包括字体、大小、样式和颜色)
TypefaceSpan : 文本字体
URLSpan : 文本超链接
ClickableSpan : 点击事件

简单使用示例

初始化SpannableString或SpannableStringBuilder,然后设置对应的setPan就可以实现对应的效果。

SpannableString spannableString = new SpannableString("要设置的内容");
ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#009ad6"));
spannableString.setSpan(colorSpan, 0, 8, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
((TextView)findViewById(R.id.mode1)).setText(spannableString);

具体使用详情可以参考:强大的SpannableStringBuilder

封装使用

对很多功能都可以封装,简化使用,这里使用了扩展函数,更方便在Kotlin中使用,不过在Java中也可以使用,使用方法如下:

第一种情况,要设置的内容已经是一段完整的内容

注意:链式调用时,只需要初始化第一个src就可以了,后续都会默认使用第一个,如果后续继续初始化src, 会导致前面的设置无效,只有最后一个生效。target和range都是为了确定要改变的文字的范围,两个初始化一个即可。

  1. 对整个字符串设置效果

    src 和target默认等于TextView的text

    //对整个 text 设置方式一,textView已经设置过内容,可以不用初始化src
    tvTvOne.sizeSpan(textSize = 20f)
    //对整个 text 设置方式二
    tvTvOne2.typeSpan(src = "全部文字加粗",target = "全部文字加粗",
    type = SsbKtx.type_bold)
  2. 设置部分文字效果

    type 有3个,对应加粗,倾斜,加粗倾斜

    //设置部分文字效果
    //tvTv2.typeSpan(range = 2..4,type = SsbKtx.type_bold)
    tvTv2.typeSpan(target = "部分",type = SsbKtx.type_bold)
    //设置加粗倾斜效果
    tvTv3.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
  3. 对同一个文字设置多个效果

    对同一个部分做多种效果,只能第一个设置 src, 后续设置会导致前面的无效。

    //        tvTv4.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
    // .foregroundColorIntSpan(range = 0..4,color = Color.GREEN)
    // .strikethroughSpan(range = 0..4)
    tvTv4.typeSpan(src = "只能这个可以设置 src,后面的再设置会导致前面效果无效",
    range = 0..4,type = SsbKtx.type_bold_italic)
    .foregroundColorIntSpan(range = 0..4,color = Color.GREEN)
    .strikethroughSpan(range = 0..4)
  4. 对多个不同的文字分别设置不同的效果

     tvTv5.typeSpan(range = 0..4,type = SsbKtx.type_bold_italic)
    .foregroundColorIntSpan(range = 7..11,color = Color.BLUE)
  5. 设置部分点击

    tvTv6.clickIntSpan(range = 0..4){
    Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show()
    }
  6. 设置部分超链接

    tvTv7.urlSpan(range = 0..4,url = "https://www.baidu.com")

第二种情况,拼接成一个完整的字符串

  1. 拼接成完整的内容

     tvTv8.text = "拼接一段文字"
    tvTv8.appendTypeSpan("加粗",SsbKtx.type_bold)
    .strikethroughSpan(target = "加粗")//对同一部分文字做多个效果
    .appendForegroundColorIntSpan("改变字体颜色",Color.RED)

    如果想对拼接的内容做多个效果,可以在其后面调用对应的方法,只要traget或是range正确即可。

完整代码

object SsbKtx {
const val flag = SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
const val type_bold = Typeface.BOLD
const val type_italic = Typeface.ITALIC
const val type_bold_italic = Typeface.BOLD_ITALIC

}
//-------------------CharSequence相关扩展-----------------------
/**
*CharSequence不为 null 或者 empty
*/
fun CharSequence?.isNotNullOrEmpty() = !isNullOrEmpty()

/**
*获取一段文字在文字中的范围
* @param target
* @return
*/
fun CharSequence.range(target: CharSequence): IntRange {
val start = this.indexOf(target.toString())
return start..(start + target.length)
}

/**
*将一段指定的文字改变大小
* @return
*/
fun CharSequence.sizeSpan(range: IntRange, textSize: Int): CharSequence {
return SpannableStringBuilder(this).apply {
setSpan(AbsoluteSizeSpan(textSize), range.first, range.last, SsbKtx.flag)
}
}


/**
*设置文字颜色
* @param range
* @return
*/
fun CharSequence.foregroundColorSpan(range: IntRange, color: Int = Color.RED): CharSequence {
return SpannableStringBuilder(this).apply {
setSpan(ForegroundColorSpan(color), range.first, range.last, SsbKtx.flag)
}
}

/**
*设置click,将一段文字中指定range的文字添加颜色和点击事件
* @param range
* @return
*/
fun CharSequence.clickSpan(
range: IntRange,
color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): CharSequence {
return SpannableString(this).apply {
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
clickAction()
}

override fun updateDrawState(ds: TextPaint) {
ds.color = color
ds.isUnderlineText = isUnderlineText
}
}
setSpan(clickableSpan, range.first, range.last, SsbKtx.flag)
}
}


//-------------------TextView相关扩展--------------------------
/**
*设置目标文字大小, src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.sizeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
@DimenRes textSize: Int
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
textSize == 0 -> this
range != null -> {
text = src.sizeSpan(range, ResUtils.getDimensionPixelSize(textSize))
this
}
target.isNotNullOrEmpty() -> {
text = src.sizeSpan(src.range(target!!), ResUtils.getDimensionPixelSize(textSize))
this
}
else -> this
}
}

/**
*设置目标文字大小, src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.sizeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
textSize: Float
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
textSize == 0f -> this
range != null -> {
text = src.sizeSpan(range, DensityUtils.dp2px(textSize))
this
}
target.isNotNullOrEmpty() -> {
text = src.sizeSpan(src.range(target!!), DensityUtils.dp2px(textSize))
this
}
else -> this
}
}

/**
*追加内容设置字体大小
* @param str
* @param textSize
* @return
*/
fun TextView?.appendSizeSpan(str: String?, textSize: Float): TextView? {
str?.let {
this?.append(it.sizeSpan(0..it.length, DensityUtils.dp2px(textSize)))
}
return this
}

fun TextView?.appendSizeSpan(str: String?, @DimenRes textSize: Int): TextView? {
str?.let {
this?.append(it.sizeSpan(0..it.length, ResUtils.getDimensionPixelSize(textSize)))
}
return this
}

/**
*设置目标文字类型(加粗,倾斜,加粗倾斜),src,target 为空时,默认设置整个 text
* @return
*/
fun TextView?.typeSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
type: Int
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.typeSpan(range, type)
this
}
target.isNotNullOrEmpty() -> {
text = src.typeSpan(src.range(target!!), type)
this
}
else -> this
}
}

fun TextView?.appendTypeSpan(str: String?, type: Int): TextView? {
str?.let {
this?.append(it.typeSpan(0..it.length, type))
}
return this
}

/**
*设置目标文字下划线
* @return
*/
fun TextView?.underlineSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.underlineSpan(range)
this
}
target.isNotNullOrEmpty() -> {
text = src.underlineSpan(src.range(target!!))
this
}
else -> this
}
}


/**
*设置目标文字对齐方式
* @return
*/
fun TextView?.alignSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
align: Layout.Alignment
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
text = src.alignSpan(range, align)
this
}
target.isNotNullOrEmpty() -> {
text = src.alignSpan(src.range(target!!), align)
this
}
else -> this
}
}

fun TextView?.appendAlignSpan(str: String?, align: Layout.Alignment): TextView? {
str?.let {
this?.append(it.alignSpan(0..it.length, align))
}
return this
}

/**
*设置目标文字超链接
* @return
*/
fun TextView?.urlSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
url: String
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
text = src.urlSpan(range, url)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
text = src.urlSpan(src.range(target!!), url)
this
}
else -> this
}
}

fun TextView?.appendUrlSpan(str: String?, url: String): TextView? {
str?.let {
this?.append(it.urlSpan(0..it.length, url))
}
return this
}

/**
*设置目标文字点击
* @return
*/
fun TextView?.clickIntSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(range, color, isUnderlineText, clickAction)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(src.range(target!!), color, isUnderlineText, clickAction)
this
}
else -> this
}
}

fun TextView?.appendClickIntSpan(
str: String?, color: Int = Color.RED,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
str?.let {
this?.append(it.clickSpan(0..it.length, color, isUnderlineText, clickAction))
}
return this
}

/**
*设置目标文字点击
* @return
*/
fun TextView?.clickSpan(
src: CharSequence? = this?.text,
target: CharSequence? = this?.text,
range: IntRange? = null,
@ColorRes color: Int,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
return when {
this == null -> this
src.isNullOrEmpty() -> this
target.isNullOrEmpty() && range == null -> this
range != null -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(range, ResUtils.getColor(color), isUnderlineText, clickAction)
this
}
target.isNotNullOrEmpty() -> {
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT // remove click bg color
text = src.clickSpan(
src.range(target!!),
ResUtils.getColor(color),
isUnderlineText,
clickAction
)
this
}
else -> this
}
}

fun TextView?.appendClickSpan(
str: String?,
@ColorRes color: Int,
isUnderlineText: Boolean = false,
clickAction: () -> Unit
): TextView? {
str?.let {
this?.append(
it.clickSpan(
0..it.length,
ResUtils.getColor(color),
isUnderlineText,
clickAction
)
)
}
return this
}

里面的ResUtils只是简单的获取资源文件,如果想直接引入,可以参考Github直接使用gradle依赖。

收起阅读 »

iOS组件化开发实践

目录:1.组件化需求来源2.组件化初识3.组件化必备的工具使用4.模块拆分5.组件工程兼容swift环境6.组件之间的通讯7.组件化后的资源加载8.OC工程底层换swift代码9.总结1. 组件化需求来源起初的这个项目,App只有一条产品线,代码逻辑相对比较清...
继续阅读 »

目录:

1.组件化需求来源
2.组件化初识
3.组件化必备的工具使用
4.模块拆分
5.组件工程兼容swift环境
6.组件之间的通讯
7.组件化后的资源加载
8.OC工程底层换swift代码
9.总结

1. 组件化需求来源

起初的这个项目,App只有一条产品线,代码逻辑相对比较清晰,后期随着公司业务的迅速发展,现在App里面承载了大概五六条产品线,每个产品线的流程有部分是一样的,也有部分是不一样的,这就需要做各种各样的判断及定制化需求。大概做了一年多后,出现了不同产品线提过来的需求,开发人员都需要在主工程中开发,但是开发人员开发的是不同的产品线,也得将整个工程跑起来,代码管理、并行开发效率、分支管理、上线时间明显有所限制。大概就在去年底,我们的领导提出了这个问题,希望作成组件化,将代码重构拆分成模块,在主工程中组装拆分的模块,形成一个完整的App。

2. 组件化初识

随着业务线的增多,业务的复杂度增加,App的代码逻辑复杂度也增加了,后期的开发维护成本也增加了,为什么这么说呢?业务逻辑没有分类,查找问题效率降低(针对新手),运行也好慢哦,真的好烦哦......我们要改变这种局面。而组件化开发,就是将一个臃肿,复杂的单一工程的项目, 根据功能或者属性进行分解,拆分成为各个独立的功能模块或者组件 ; 然后根据项目和业务的需求,按照某种方式, 任意组织成一个拥有完整业务逻辑的工程。

组件化开发的缺点:

1、代码耦合严重
2、依赖严重
3、其它app接入某条产品线难以集成
4、项目复杂、臃肿、庞大,编译时间过长
5、难以做集成测试
6、对开发人员,只能使用相同的开发模式
......
组件化开发的优点:

1、项目结构清晰
2、代码逻辑清晰
3、拆分粒度小
4、快速集成
5、能做单元测试
6、代码利用率高
7、迭代效率高
......
组件化的实质:就是对现有项目或新项目进行基础、功能及业务逻辑的拆分,形成一个个的组件库,使宿主工程能在拆分的组件库里面查找需要的功能,组装成一个完整的App。

3. 组件化必备的工具使用

组件的存在方式是以每个pod库的形式存在的。那么我们组合组件的方法就是通过利用CocoaPods的方式添加安装各个组件,我们就需要制作CocoaPods远程私有库,将其发不到公司的gitlab或GitHub,使工程能够Pod下载下来。

Git的基础命令:

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master

CocoaPods远程私有库制作:
1、Create Component Project

pod lib create ProjectName

2、Use Git

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master

3、Edit podspec file

vim CoreLib.podspec
Pod::Spec.new do |s|
s.name = '组件工程名'
s.version = '0.0.1'
s.summary = 'summary'

s.description = <<-DESC
description
DESC

s.homepage = '远程仓库地址'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '作者' => '作者' }
s.source = { :git => '远程仓库地址', :tag => s.version.to_s }

s.ios.deployment_target = '8.0'

s.source_files = 'Classes/**/*.{swift,h,m,c}'
s.resources = 'Assets/*'

s.dependency 'AFNetworking', '~> 2.3'
end

4、Create tag

//create local tag
git tag '0.0.1'

git tag 0.0.1

//local tag push to remote
git push --tags

git push origin 0.0.1

//delete local tag
git tag -d 0.0.1

//delete remote tag
git tag origin :0.0.1

5、Verify Component Project

pod lib lint --allow-warnings --no-clean

6、Push To CocoaPods

pod repo add CoreLib git@git.test/CoreLib.git
pod repo push CoreLib CoreLib.podspec --allow-warnings

4. 模块拆分


基础组件库:
基础组件库放一些最基础的工具类,比如金额格式化、手机号/shenfen证/邮箱的有效校验,实质就是不会依赖业务,不会和业务牵扯的文件。

功能组件库:
分享的封装、图片的轮播、跑马灯功能、推送功能的二次封装,即开发一次,以后都能快速集成的功能。

业务组件库:
登录组件、实名组件、消息组件、借款组件、还款组件、各条产品线组件等。

中间件(组件通讯):
各个业务组件拆分出来后,组件之间的通讯、传参、回调就要考虑了,此时就需要一个组件通讯的工具类来处理。

CocoaPods远程私有库:
每个拆分出去的组件存在的形式都是以Pod的形式存在的,并能达到单独运行成功。

宿主工程:
宿主工程就是一个壳,在组件库中寻找这个工程所需要的组件,然后拿过来组装成一个App。

5. 组件工程兼容swift环境

在做组件化之前,这个项目使用的是Objective-C语言写的,还没有支持在项目里面使用Swift语言的能力,考虑到后期肯定会往Swift语言切过去的,于是借着这次重构的机会,创建的组件工程都是swift工程。

Podfile文件需要添加==use_frameworks!==

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
inhibit_all_warnings!
use_frameworks!

target 'CoreLib_Example' do
pod 'CoreLib', :path => '../'
end

这里其实有个大坑需要特别注意,在支持Swift环境后,部分Objective-C语言的三方库采用的是==静态库==,在OC文件中引用三方库头文件,会一直报头文件找不到,我们在遇到这个问题时找遍了百度,都没找到解决方案,整整花了一个星期的时间尝试。

解决方案:我们对这些三方库(主要有:UMengAnalytics、Bugly、AMapLocation-NO-IDFA)再包一层,使用CocoaPods远程私有库管理,对外暴露我们写的文件,引用我们写的头文件,就能调用到。

Pod::Spec.new do |s|
s.name = ''
s.version = '0.0.1'
s.summary = '包装高德地图、分享、友盟Framework.'

s.description = <<-DESC
DESC

s.homepage = ''
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '' => '' }
s.source = { :git => '', :tag => s.version.to_s }

s.ios.deployment_target = '8.0'

s.source_files = ['Classes/UMMob/**/*.{h,m}','Classes/Bugly/**/*.{h,m}','Classes/AMap/**/*.{h,m}']
s.public_header_files = ['Classes/*.h']
s.libraries = 'sqlite3', 'c++', 'z', 'z.1.1.3', 'stdc++', 'stdc++.6.0.9'
s.frameworks = 'SystemConfiguration', 'CoreTelephony', 'JavaScriptcore', 'CoreLocation', 'Security', 'Foundation'
s.vendored_frameworks = 'Frameworks/**/*.framework'
s.xcconfig = { "FRAMEWORK_SEARCH_PATHS" => "Pods/WDContainerLib/Frameworks" }

s.requires_arc = true
end

6. 组件之间的通讯

在将业务控制器拆分出去后,如果一个组件要调用另一个组件里面的控制器,平常的做法是直接==#import "控制器头文件"==,现在在不同的组件里面是无法import的,那该怎么做呢?答案就是使用==消息发送机制==。

思路:

1.每个业务组件库里面会有一个控制器的配置文件(路由配置文件),标记着每个控制器的key;
2.在App每次启动时,组件通讯的工具类里面需要解析控制器配置文件(路由配置文件),将其加载进内存;
3.在内存中查询路由配置,找到具体的控制器并动态生成类,然后使用==消息发送机制==进行调用函数、传参数、回调,都能做到。

((id (*)(id, SEL, NSDictionary *)) objc_msgSend)((id) cls, @selector(load:), param);
((void(*)(id, SEL,NSDictionary*))objc_msgSend)((id) vc, @selector(callBack:), param);

Or

[vc performSelector:@selector(load:) withObject:param];
[vc performSelector:@selector(callBack:) withObject:param];

好处:

解除了控制器之间的依赖;
使用iOS的消息发送机制进行传参数、回调参数、透传参数;
路由表配置文件,能实现界面动态配置、动态生成界面;
路由表配置文件放到服务端,还可以实现线上App的跳转逻辑;
将控制器的key提供给H5,还可以实现H5跳转到Native界面;

7. 组件化后的资源加载

新项目已开始就采用组件化开发,还是特别容易的,如果是老项目重构成组件化,那就比较悲剧了,OC项目重构后,app包里面会有一个==Frameworks==文件夹,所有的组件都在这个文件夹下,并且以==.framework==(比如:WDComponentLogin.framework)结尾。在工程中使用的==xib、图片==,使用正常的方式加载,是加载不到的,原因就是xib、图片的路径==(工程.app/Frameworks/WDComponentLogin.framework/LoginViewController.nib、工程.app/Frameworks/WDComponentLogin.framework/login.png)==发生了变化。

以下是在组件库中加载nib文件/图片文件的所有情况:

/**
从主工程mainBundle或从所有的组件(组件名.framework)中加载图片

@param imageName 图片名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName;

/**
从指定的组件中加载图片,主要用于从当前组件加载其他组件中的图片

@param imageName 图片名称
@param frameworkName 组件名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName frameworkName:(NSString *_Nonnull)frameworkName;

/**
从指定的组件的Bundle文件夹中加载图片,主要用于从当前组件加载其他组件Bundle文件夹中的图片

@param imageName 图片名称
@param bundleName Bundle文件夹名
@param frameworkName 组件名称
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName frameworkName:(NSString *_Nonnull)frameworkName;

/**
从主工程mainBundle的指定Bundle文件夹中去加载图片

@param imageName 图片名称
@param bundleName Bundle文件夹名
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName;

/**
从指定的组件(组件名.framework)中加载图片
说明:加载组件中的图片,必须指明图片的全名和图片所在bundle的包名

@param imageName 图片名称
@param targetClass 当前类
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName targetClass:(Class _Nonnull)targetClass;

/**
从指定的组件(组件名.framework)中的Bundle文件夹中加载图片
说明:加载组件中的图片,必须指明图片的全名和图片所在bundle的包名

@param imageName 图片名称
@param bundleName Bundle文件夹名
@param targetClass 当前类
@return 返回查找的图片结果
*/
+ (UIImage *_Nullable)loadImageNamed:(NSString *_Nonnull)imageName bundleName:(NSString *_Nonnull)bundleName targetClass:(Class _Nonnull)targetClass;

/**
加载工程中的nib文件
eg:[_tableview registerNib:[WDLoadResourcesUtil loadNibClass:[WDRepaymentheaderView class]] forHeaderFooterViewReuseIdentifier:kWDRepaymentheaderView]
@param class nib文件名
@return 返回所需要的nib对象
*/
+ (UINib *_Nullable)loadNibClass:(NSObject *_Nonnull)targetClass;

控制器加载方式:

@implementation WDBaseViewController

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
NSString *classString = [[NSStringFromClass(self.class) componentsSeparatedByString:@"."] lastObject];
if ([[NSBundle bundleForClass:[self class]] pathForResource:classString ofType:@"nib"] != nil) {
//有xib
return [super initWithNibName:classString bundle:[NSBundle bundleForClass:[self class]]];
}else if ([[NSBundle mainBundle] pathForResource:classString ofType:@"nib"] == nil) {
//没有xib
return [super initWithNibName:nil bundle:nibBundleOrNil];
} else {
return [super initWithNibName:(nibNameOrNil == nil ? classString : nibNameOrNil) bundle:nibBundleOrNil];
}
}
@end

UIView视图加载方式:

OC版本

+ (id)loadFromNIB {
if ([[NSFileManager defaultManager] fileExistsAtPath:[NSBundle bundleForClass:[self class]].bundlePath]) {
return [[[NSBundle bundleForClass:[self class]] loadNibNamed:[self description]
owner:self
options:nil] lastObject];
}else{
return [[[NSBundle mainBundle] loadNibNamed:[self description] owner:self options:nil] lastObject];
}

}

+ (id)loadFromNIB:(NSInteger)index {
if ([[NSFileManager defaultManager] fileExistsAtPath:[NSBundle bundleForClass:[self class]].bundlePath]) {
return [[NSBundle bundleForClass:[self class]] loadNibNamed:[self description]
owner:self
options:nil][index];
}else{
return [[NSBundle mainBundle] loadNibNamed:[self description] owner:self options:nil][index];
}

}

Swift版本

// MARK: - 通过nib加载视图
@objc public static func loadFromNIB() -> UIView! {
return (Bundle(for: self.classForCoder()).loadNibNamed(self.description().components(separatedBy: ".")[1], owner: self, options: nil)?.first as? UIView)!
}

8. OC工程底层换swift代码

目前正在做OC底层的统一,换成swift写的代码。

1、控制器Base、Web控制器Base使用OC代码,因为OC控制器不能继承Swift,而Swift控制器可以继承OC写的控制器。
2、导航栏、工具栏、路由、基础组件、功能组件、混合开发插件都是用Swift语言。
3、Swift移动组件大部分完成,OC工程、Swift工程都统一使用开发的移动组件库。

9. 总结

经过半年的努力重构,终于将工程拆分成组件化开发了,也从中学到了很多,希望自己能再接再厉和同事一起进步。

链接:https://www.jianshu.com/p/196ec57cdc75

收起阅读 »

APP路由框架与组件化简析

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。 路由...
继续阅读 »

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。


路由的概念


路由这个词本身应该是互联网协议中的一个词,维基百科对此的解释如下:


路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

个人理解,在前端开发中,路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme,并生成路由表,然后,能够根据外部输入字符串在路由表中匹配到对应的页面或者服务,进行跳转或者调用,并提供会获取返回值等,示意如下


image.png


所以一个基本路由框架要具备如下能力:





    1. APP路由的扫描及注册逻辑




    1. 路由跳转target页面能力




    1. 路由调用target服务能力



APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。


三方路由框架是否是APP强需求


答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。


Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。


原生路由的限制:功能单一,扩展灵活性差,不易协同


传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:


<!--1 导入依赖-->
import com.snail.activityforresultexample.test.SecondActivity;

public class MainActivity extends AppCompatActivity {

void jumpSecondActivityUseClassName(){
<!--显示的引用Activity类-->
Intent intent =new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}


显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。


第一步:manifest中配置activity的intent-filter,至少要配置一个action


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.snail.activityforresultexample">
<application
...
<activity android:name=".test.SecondActivity">
<intent-filter>
<!--隐式调用必须配置android.intent.category.DEFAULT-->
<category android:name="android.intent.category.DEFAULT"/>
<!--至少配置一个action才能通过隐式调用-->
<action android:name="com.snail.activityforresultexample.SecondActivity" />
<!--可选-->
<!-- <data android:mimeType="video/mpeg" android:scheme="http" ... />-->
</intent-filter>
</activity>
</application>
</manifest>

第二步:调用


void jumpSecondActivityUseFilter() {
Intent intent = new Intent();
intent.setAction("com.snail.activityforresultexample.SecondActivity");
startActivity(intent);
}

如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:



  • 首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。

  • 其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。

  • 最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。


可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的


APP三方路由框架需具备的能力


目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:



  • 路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑

  • scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离

  • 基础路由跳转能力 :页面跳转能力的支持

  • 服务类组件的支持 :如去某个服务组件获取一些配置等

  • [扩展]路由拦截逻辑:比如登陆,统一鉴权

  • 可定制的降级逻辑:找不到组件时的兜底


可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,


	@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {
...
}

build阶段会根据注解搜集路由scheme,生成路由表。第二步使用


        ARouter.getInstance()
.build("/test/activity2")
.navigation(this);

如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。


APP路由框架的实现


路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询


路由表的自动生成


生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,


image.png


不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:


image.png


其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。



JavaPoet如何搜集并生成路由表集合?



以ARouter框架为例,先定义Router框架需要的注解如:


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

/**
* Path of route
*/
String path();

该注解用于标注需要路由的组件,用法如下:


@Route(path = "/test/activity1", name = "测试用 Activity")
public class Test1Activity extends BaseActivity {
@Autowired
int age = 10;

之后利用APT扫描所有被注解的类,生成路由表,实现参考如下:


@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {
<!--获取所有被Route.class注解标注的集合-->
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
<!--解析并生成表-->
this.parseRoutes(routeElements);
...
return false;
}

<!--生成中间路由表Java类-->
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
...
// Generate groups
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);

产物如下:包含路由表,及局部注册入口。


image.png



自动注册:ASM搜集上述路由表并聚合插入Init代码区



为了能够插入到Init代码区,首先需要预留一个位置,一般定义一个空函数,以待后续填充:


	public class RouterInitializer {

public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
...
loadRouterTables();
}
//自动注册代码
public static void loadRouterTables() {

}
}

首先利用AOP工具,遍历上述APT中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,



  • 搜集目标,聚合路由表

      /**扫描jar*/
    fun scanJar(jarFile: File, dest: File?) {

    val file = JarFile(jarFile)
    var enumeration = file.entries()
    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    if (jarEntry.name.endsWith("XXRouterTable.class")) {
    val inputStream = file.getInputStream(jarEntry)
    val classReader = ClassReader(inputStream)
    if (Arrays.toString(classReader.interfaces)
    .contains("IHTRouterTBCollect")
    ) {
    tableList.add(
    Pair(
    classReader.className,
    dest?.absolutePath
    )
    )
    }
    inputStream.close()
    } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
    registerInitClass = dest
    }
    }
    file.close()
    }

  • 对目标Class注入路由表初始化代码

      fun asmInsertMethod(originFile: File?) {

    val optJar = File(originFile?.parent, originFile?.name + ".opt")
    if (optJar.exists())
    optJar.delete()
    val jarFile = JarFile(originFile)
    val enumeration = jarFile.entries()
    val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    val entryName = jarEntry.getName()
    val zipEntry = ZipEntry(entryName)
    val inputStream = jarFile.getInputStream(jarEntry)
    //插桩class
    if (entryName.endsWith("RouterInitializer.class")) {
    //class文件处理
    jarOutputStream.putNextEntry(zipEntry)
    val classReader = ClassReader(IOUtils.toByteArray(inputStream))
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
    classReader.accept(cv, EXPAND_FRAMES)
    val code = classWriter.toByteArray()
    jarOutputStream.write(code)
    } else {
    jarOutputStream.putNextEntry(zipEntry)
    jarOutputStream.write(IOUtils.toByteArray(inputStream))
    }
    jarOutputStream.closeEntry()
    }
    //结束
    jarOutputStream.close()
    jarFile.close()
    if (originFile?.exists() == true) {
    Files.delete(originFile.toPath())
    }
    optJar.renameTo(originFile)
    }


最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码:


 public static void loadRouterTables() {

<!---->
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
...
}

如此就完成了路由表的搜集与注册,大概的流程就是如此。当然对于支持服务、Fragment等略有不同,但大体类似。


Router框架对服务类组件的支持


通过路由的方式获取服务属于APP路由比较独特的能力,比如有个用户中心的组件,我们可以通过路由的方式去查询用户是否处于登陆状态,这种就不是狭义上的页面路由的概念,通过一串字符串如何查到对应的组件并调用其方法呢?这种的实现方式也有多种,每种实现方式都有自己的优劣。



  • 一种是可以将服务抽象成接口,沉到底层,上层实现通过路由方式映射对象

  • 一种是将实现方法直接通过路由方式映射


先看第一种,这种事Arouter的实现方式,它的优点是所有对外暴露的服务都暴露接口类【沉到底层】,这对于外部的调用方,也就是服务使用方非常友好,示例如下:



先定义抽象服务,并沉到底层



image.png


public interface HelloService extends IProvider {
void sayHello(String name);
}


实现服务,并通过Router注解标记



@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
Context mContext;

@Override
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}


使用:利用Router加scheme获取服务实例,并映射成抽象类,然后直接调用方法。



  ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

这种实现方式对于使用方其实是很方便的,尤其是一个服务有多个可操作方法的时候,但是缺点是扩展性,如果想要扩展方法,就要改动底层库。


再看第二种:将实现方法直接通过路由方式映射


服务的调用都要落到方法上,参考页面路由,也可以支持方法路由,两者并列关系,所以组要增加一个方法路由表,实现原理与Page路由类似,跟上面的Arouter对比,不用定义抽象层,直接定义实现即可:



定义Method的Router



	public class HelloService {

<!--参数 name-->
@MethodRouter(url = {"arouter://sayhello"})
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}


使用即可



 RouterCall.callMethod("arouter://sayhello?name=hello");

上述的缺点就是对于外部调用有些复杂,尤其是处理参数的时候,需要严格按照协议来处理,优点是,没有抽象层,如果需要扩展服务方法,不需要改动底层。


上述两种方式各有优劣,不过,如果从左服务组件的初衷出发,第一种比较好:对于调用方比较友好。另外对于CallBack的支持,Arouter的处理方式可能也会更方便一些,可以比较方便的交给服务方定义。如果是第二种,服务直接通过路由映射的方式,处理起来就比较麻烦,尤其是Callback中的参数,可能要统一封装成JSON并维护解析的协议,这样处理起来,可能不是很好。


路由表的匹配


路由表的匹配比较简单,就是在全局Map中根据String输入,匹配到目标组件,然后依赖反射等常用操作,定位到目标。


组件化与路由的关系


组件化是一种开发集成模式,更像一种开发规范,更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件,这些组件之间可能是不同的团队,处于不同的目的在各自维护,甚至是需要代码隔离,如果牵扯到组件间的调用与通信,就不可避免的借助路由,因为实现隔离的,只能采用通用字符串scheme进行通信,这就是路由的功能范畴。


组件化需要路由支撑的根本原因:组件间代码实现的隔离


总结



  • 路由不是一个APP的必备功能,但是大型跨团队的APP基本都需要

  • 路由框架的基本能力:路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能

  • 组件化与路由的关系:组件化的代码隔离导致路由框架成为必须




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

浅谈Android插件化

一、认识插件化 1.1 插件化起源 插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。 想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才...
继续阅读 »

一、认识插件化


1.1 插件化起源


插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。


想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已。


常见的应用安装目录有:



  • /system/app:系统应用

  • /system/priv-app:系统应用

  • /data/app:用户应用


那可能大家会想问,既然安装这个过程如此简单,Android 是怎么运行应用中的代码的呢,我们先看 Apk 的构成,一个常见的 Apk 会包含如下几个部分:



  • classes.dexJava 代码字节码

  • res:资源文件

  • libso 文件

  • assets:静态资产文件

  • AndroidManifest.xml:清单文件


其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已。


那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?


1.2 插件化优点


插件化让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益:



  • 减少安装Apk的体积、按需下载模块

  • 动态更新插件

  • 宿主和插件分开编译,提升开发效率

  • 解决方法数超过65535的问题


想象一下,你的应用拥有 Native 应用一般极高的性能,又能获取诸如 Web 应用一样的收益。


嗯,理想很美好不是嘛?


1.3 与组件化的区别



  • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。

  • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。


二、插件化的技术难点


? 想让插件的Apk真正运行起来,首先要先能找到插件Apk的存放位置,然后我们要能解析加载Apk里面的代码。


? 但是光能执行Java代码是没有意义的,在Android系统中有四大组件是需要在系统中注册的,具体来说是在 Android 系统的 ActivityManagerService (AMS)PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMSPMS,如何欺骗系统,让他承认一个未安装的 Apk 中的组件,如何让宿主动态加载执行插件Apk中 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等是插件化最大的难点。


? 另外,应用资源引用(特指 R 中引用的资源,如 layoutvalues 等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。


总结一下,其实做到插件化的要点就这几个:



  • 如何加载并执行插件 Apk 中的代码(ClassLoader Injection

  • 让系统能调用插件 Apk 中的组件(Runtime Container

  • 正确识别插件 Apk 中的资源(Resource Injection


当然还有其他一些小问题,但可能不是所有场景下都会遇到,我们后面再单独说。


三、ClassLoader Injection


ClassLoader 是插件化中必须要掌握的,因为我们知道Android 应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk,系统不会处理其中的类,所以需要使用 ClassLoader 加载 Apk,然后反射里面的代码。


3.1 java 中的 ClassLoader



  • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等


  • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包


  • AppClassLoader 负责加载 classpath 里的 jar 包和目录



3.2 android 中的 ClassLoader


在Android系统中ClassLoader是用来加载dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件



  • PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

    public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
    ClassLoader parent)
    {
    super(dexPath, null, libraryPath, parent);
    }
    }

  • DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

    public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
    String libraryPath, ClassLoader parent)
    {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
    }


我们在插件化中一般使用的是 DexClassLoader。


3.3 双亲委派机制


每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。


    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 先从父类加载器中进行加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 没有找到,再自己加载
c = findClass(name);
}
}
return c;
}
复制代码

3.4 如何加载插件中的类


要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要哪些参数。


public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
// ...
}
}
复制代码

构造函数需要四个参数: dexPath 是需要加载的 dex / apk / jar 文件路径 optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置 librarySearchPath 是 native 依赖的位置 parent 就是父类加载器,默认会先从 parent 加载对应的类


创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:


    // 从 assets 中拿出插件 apk 放到内部存储空间
private fun extractPlugin() {
var inputStream = assets.open("plugin.apk")
File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
}

private fun init() {
extractPlugin()
pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用来加载插件类
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
}


3.5 执行插件类的方法


通过反射来执行类的方法


val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test",null).invoke(loadClass)

我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。


四、Runtime Container


我们之前说到 Activity 插件化最大的难点是如何欺骗系统,让他承认一个未安装的 Apk 中的组件。 因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。 如果直接把插件的 Activity 注册到宿主 Manifest 里就失去了插件化的动态特性,因为每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。


4.1 为什么没有注册的 Activity 不能和系统交互


这里的不能直接交互的含义有两个



  1. 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:

    android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?



这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:


public class Instrumentation {
public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}

switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
...
}
}
}



  1. Activity 的生命周期无法被调用,其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。


4.2 运行时容器技术


由于Android中的组件(Activity,Service,BroadcastReceiver和ContentProvider)是由系统创建的,并且由系统管理生命周期。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。插件化如何支持组件生命周期的管理。 大致分为两种方式:



  • 运行时容器技术(ProxyActivity代理)

  • 预埋StubActivity,hook系统启动Activity的过程


我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。


它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:



  • pluginName

  • pluginApkPath

  • pluginActivityName


等,其实最重要的就是 pluginApkPathpluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:



  • 转发所有来自系统的生命周期回调至插件 Activity

  • 接受 Activity 方法的系统调用,并转发回系统


我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity


public class ContainerActivity extends Activity {
private PluginActivity pluginActivity;

@Override
protected void onCreate(Bundle savedInstanceState) {
String pluginActivityName = getIntent().getString("pluginActivityName", "");
pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
if (pluginActivity == null) {
super.onCreate(savedInstanceState);
return;
}

pluginActivity.onCreate();
}

@Override
protected void onResume() {
if (pluginActivity == null) {
super.onResume();
return;
}
pluginActivity.onResume();
}

@Override
protected void onPause() {
if (pluginActivity == null) {
super.onPause();
return;
}
pluginActivity.onPause();
}

// ...
}

public class PluginActivity {
private ContainerActivity containerActivity;

public PluginActivity(ContainerActivity containerActivity) {
this.containerActivity = containerActivity;
}

@Override
public <T extends View> T findViewById(int id) {
return containerActivity.findViewById(id);
}
// ...
}

// 插件 `Apk` 中真正写的组件
public class TestActivity extends PluginActivity {
// ......
}

是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。


4.3 字节码替换


该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。


class TestActivity extends Activity {}
->
class TestActivity extends PluginActivity {}

有没有什么办法能让插件组件的编写与原来没有任何差别呢?


Shadow 的做法是字节码替换插件,这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。


实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:


class TestActivity extends Activity {}

然后完成编译后,最后的字节码中,显示的却是:


class TestActivity extends PluginActivity {}

到这里基本的框架就差不多结束了。


五、Resource Injection


最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id


资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:



  • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 ApkPackageInfo

  • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例


我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 ApkPackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:


PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
pluginApkPath,
PackageManager.GET_ACTIVITIES
| PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

Resources injectResources = null;
try {
injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
// ...
}

拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:


public class PluginResources extends Resources {
private Resources hostResources;
private Resources injectResources;

public PluginResources(Resources hostResources, Resources injectResources) {
super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
this.hostResources = hostResources;
this.injectResources = injectResources;
}

@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return injectResources.getString(id, formatArgs);
} catch (NotFoundException e) {
return hostResources.getString(id, formatArgs);
}
}

// ...
}

然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:


public class ContainerActivity extends Activity {
private Resources pluginResources;

@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
// ...
}

@Override
public Resources getResources() {
if (pluginActivity == null) {
return super.getResources();
}
return pluginResources;
}
}

这样就完成了资源的注入。



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

二阶贝塞尔仿微信扔炸弹动画

前言 新出来的微信炸屎动画很多人都玩过了,所以先仿照一个微信扔炸弹的动画,在后续有时间会做一个完整的,效果如下: 具体实现 其中最麻烦的就是绘制抛物线了,爆炸的效果只是播放了一个动画,另外微信貌似都是通过代码绘制的,可能不是动画,奈何没有人家那技术,...
继续阅读 »

前言


新出来的微信炸屎动画很多人都玩过了,所以先仿照一个微信扔炸弹的动画,在后续有时间会做一个完整的,效果如下:



具体实现


其中最麻烦的就是绘制抛物线了,爆炸的效果只是播放了一个动画,另外微信貌似都是通过代码绘制的,可能不是动画,奈何没有人家那技术,只能找一张动画来凑合。


二阶贝塞尔曲线


抛物线在这里是通过二阶贝塞尔曲线来完成,所以先来了解下什么是二阶贝塞尔曲线,从下图中可以发现,二阶贝塞尔曲线有三个关键点,我们可以称作起点坐标、终点坐标,还有控制点。


录屏_选择区域_20210615170032.gif


起点和终点坐标好理解,控制点可以理解成开始下降的转折点,而古老的数学大神早就提供好了公式,我们只需要向这个公式提供这几个参数即可得到x、y,当然还有个参数是时间,有了时间控制,我们可以在指定秒内把他平滑的绘制完成。


公式如下:


x = (1 - t)^2 * 0 + 2 t (1 - t) * 1 + t^2 * 1 = 2 t (1 - t) + t^2
y= (1 - t)^2 * 1 + 2 t (1 - t) * 1 + t^2 * 0 = (1 - t)^2 + 2 t (1 - t)

自定义二阶贝塞尔曲线计算器


提到动画,首先可能会想到ObjectAnimator类,没错,抛物线也是通过ObjectAnimator来完成的,只不过我们需要自定义一个TypeEvaluator,用来提供二阶贝塞尔曲线的x和y。


TypeEvaluator只有一个方法,定义如下:


public abstract T evaluate (float fraction, 
T startValue,
T endValue)



fraction表示开始值和结束值之间的比例,startValue、endValue分别是开始值和结束值,这个比例也可以当作是时间,可能官方一点叫比例,他会自动计算,值的范围是0-1,比如取值0.5的时候就是动画完成了一半,1的时候动画完成。


所以套入二阶贝塞尔曲线公式得到如下代码:


class PointFTypeEvaluator(var control: PointF) : TypeEvaluator<PointF> {
override fun evaluate(fraction: Float, startValue: PointF, endValue: PointF): PointF {
return getPointF(startValue, endValue, control, fraction)
}

private fun getPointF(start: PointF, end: PointF, control: PointF, t: Float): PointF {
val pointF = PointF()
pointF.x = (1 - t) * (1 - t) * start.x + 2 * t * (1 - t) * control.x + t * t * end.x
pointF.y = (1 - t) * (1 - t) * start.y + 2 * t * (1 - t) * control.y + t * t * end.y
return pointF
}

}

播放动画


然后使用ObjectAnimator进行播放。


 val animator = ObjectAnimator.ofObject(activityMainBinding.boom, "mPointF",
PointFTypeEvaluator(controlP), startP, endP)

注意的是这个View需要有point方法,参数是PointF,方法内主要完成x和y的设置。


 public void setPoint(PointF pointF) {
setX(pointF.x);
setY(pointF.y);
}

当然微信炸弹落地的位置是随机的,我们也加个随机。


class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding;

private fun getRandom(max: Int, min: Int): Int {
val random = java.util.Random()
return random.nextInt(max - min + 1) + min
}

private fun getRandomPointF():PointF{
val outMetrics = DisplayMetrics()
val offset = 100
windowManager.defaultDisplay.getMetrics(outMetrics)
val width = outMetrics.widthPixels
val height = outMetrics.heightPixels
return PointF(getRandom(width / 2 + offset, width / 2 - offset).toFloat(), getRandom(height / 2 + offset, height / 2 - offset).toFloat())
}


override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);



binding.button.setOnClickListener {
binding!!.boom.visibility = View.VISIBLE
val startP = PointF()
val endP = PointF()
val controlP = PointF()
val randomPointF = getRandomPointF()
startP.x = 916f
startP.y = 1353f
endP.x = randomPointF.x
endP.y = randomPointF.y
controlP.x = randomPointF.x + getRandom(200, 50)
controlP.y = randomPointF.y - getRandom(200, 50)
val animator = ObjectAnimator.ofObject(binding.boom, "point",
PointFTypeEvaluator(controlP), startP, endP)

animator.start()
}
}

}

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">


<data>

</data>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">



<com.airbnb.lottie.LottieAnimationView
android:visibility="gone"
android:id="@+id/lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="boom.json">
</com.airbnb.lottie.LottieAnimationView>

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始">
</Button>

<com.example.kotlindemo.widget.MyImageView
android:id="@+id/boom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_boom"
android:visibility="gone">
</com.example.kotlindemo.widget.MyImageView>
</RelativeLayout>
</layout>

效果如下:


录屏_选择区域_20210615174149.gif


爆炸效果


爆炸效果是使用的动画,用的lottie框架,这里提供爆炸文件的下载地址。


https://lottiefiles.com/download/public/9990-explosion

有了结束的坐标点,只需要吧LottieAnimationView移动到对应位置进行播放即可,播放后隐藏,完整代码如下:


package com.example.kotlindemo

import android.animation.Animator
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.PointF
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.example.kotlindemo.databinding.ActivityMainBinding
import com.example.kotlindemo.widget.PointFTypeEvaluator
import meow.bottomnavigation.MeowBottomNavigation
import kotlin.random.Random


class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding;

private fun getRandom(max: Int, min: Int): Int {
val random = java.util.Random()
return random.nextInt(max - min + 1) + min
}

private fun getRandomPointF():PointF{
val outMetrics = DisplayMetrics()
val offset = 100
windowManager.defaultDisplay.getMetrics(outMetrics)
val width = outMetrics.widthPixels
val height = outMetrics.heightPixels
return PointF(getRandom(width / 2 + offset, width / 2 - offset).toFloat(), getRandom(height / 2 + offset, height / 2 - offset).toFloat())
}


override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);


binding!!.button.setOnClickListener {
binding!!.boom.visibility = View.VISIBLE
val startP = PointF()
val endP = PointF()
val controlP = PointF()
val randomPointF = getRandomPointF()
startP.x = 916f
startP.y = 1353f
endP.x = randomPointF.x
endP.y = randomPointF.y
controlP.x = randomPointF.x + getRandom(200, 50)
controlP.y = randomPointF.y - getRandom(200, 50)
val animator = ObjectAnimator.ofObject(binding.boom, "point",
PointFTypeEvaluator(controlP), startP, endP)
animator.duration = 600
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
val measuredHeight = binding.lottie.measuredHeight
val measuredWidth = binding.lottie.measuredWidth
binding.lottie.x = randomPointF.x - measuredWidth / 2
binding.lottie.y = randomPointF.y - measuredHeight / 2
binding.lottie.visibility = View.VISIBLE
binding.boom.visibility = View.GONE
binding.lottie.playAnimation()
binding.lottie.addAnimatorListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
binding.lottie.visibility = View.GONE
}

override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
}

override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
animator.start()
}

}

}




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

Android App唤醒丶保活详解 , 以及代码展示

安卓进程进程保活分为: 黑色保活,白色保活,灰色保活 黑色保活: 可以说黑色保活,可以通过网络切换,拍照,拍视频,开机,利用系统产生的广播唤醒app,接入三方的sdk也会唤醒一些app,如支付宝,微信..........这样的话,这样的话,不敢想象系统存...
继续阅读 »

安卓进程进程保活分为:


黑色保活,白色保活,灰色保活


黑色保活:


可以说黑色保活,可以通过网络切换,拍照,拍视频,开机,利用系统产生的广播唤醒app,接入三方的sdk也会唤醒一些app,如支付宝,微信..........这样的话,这样的话,不敢想象系统存活会给系统带来多大的负担,所以我们的安卓手机也变得卡了,google官方可能也认识了这么一点,所以取消了


ACTION_NEW_PICTURE(拍照),ACTION_NEW_VIDEO(拍视频),CONNECTIVITY_ACTION(网络切换)


app也会随着做一点改变,(不过sdk的使用还是会通过一个app启动相关的一些app , 黑色保活我个人认为不推荐使用,毕竟为了我们广大安卓用户。)


白色保活:


白色保活手段非常简单,就是调用系统api启动一个前台的Service进程,这样会在系统的通知栏生成一个Notification,用来让用户知道有这样一个app在运行着,哪怕当前的app退到了后台。




不过用户看到这个图标的时候,都会把它清空的。。。。



灰色保活:


可以说,灰色保活是用的最多,当用户不知不觉中这个app程序已经在后台运行了。


它是利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。这样做带来的好处就是,用户无法察觉到你运行着一个前台进程(因为看不到Notification),但你的进程优先级又是高于普通后台进程的。API < 18,启动前台Service时直接传入new Notification();API >= 18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理;


安卓app唤醒:


其实app唤醒的介绍很好说,app唤醒就是当打开一个app的时候,另一个app里有对应刚打开那个app的属性标志,根据你想要的唤醒方式,执行不同的代码操作,这样就可以唤醒另一个没打开的app了。(代码在最下面)


下面我展示一下这几种状态下的代码:


这个是xml布局,主要是为了展示我所介绍的几种保活方式:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">

<Button
android:id="@+id/mBtn_white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="白色保活" />

<Button
android:id="@+id/mBtn_gray"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="灰色保活" />

<Button
android:id="@+id/mBtn_black"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="黑色保活(发广播)" />

<Button
android:id="@+id/mBtn_background_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="普通后台 Service 进程" />

</LinearLayout>

下面是主要实现类:


WakeReceiver


import android.app.Notification;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

public class WakeReceiver extends BroadcastReceiver {
private final static String TAG = WakeReceiver.class.getSimpleName();
private final static int WAKE_SERVICE_ID = -1111;
/**
* 灰色保活手段唤醒广播的action
*/
public final static String GRAY_WAKE_ACTION = "com.wake.gray";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GRAY_WAKE_ACTION.equals(action)) {
Log.i(TAG, "wake !! wake !! ");

Intent wakeIntent = new Intent(context, WakeNotifyService.class);
context.startService(wakeIntent);
}
}
/**
* 用于其他进程来唤醒UI进程用的Service
*/
public static class WakeNotifyService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "WakeNotifyService->onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "WakeNotifyService->onStartCommand");
if (Build.VERSION.SDK_INT < 18) {
startForeground(WAKE_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标
} else {
Intent innerIntent = new Intent(this, WakeGrayInnerService.class);
startService(innerIntent);
startForeground(WAKE_SERVICE_ID, new Notification());
}
return START_STICKY;
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "WakeNotifyService->onDestroy");
super.onDestroy();
}
}

/**
* 给 API >= 18 的平台上用的灰色保活手段
*/
public static class WakeGrayInnerService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "InnerService -> onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "InnerService -> onStartCommand");
startForeground(WAKE_SERVICE_ID, new Notification());
//stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "InnerService -> onDestroy");
super.onDestroy();
}
}
}

BackGroundService


import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

/**
* 普通的后台Service进程
*
* @author clock
* @since 2016-04-12
*/
public class BackgroundService extends Service {

private final static String TAG = BackgroundService.class.getSimpleName();

@Override
public void onCreate() {
Log.i(TAG, "onCreate");
super.onCreate();
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
super.onDestroy();
}
}

GrayService


import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import com.example.renzheng.receiver.WakeReceiver;

/**
* 灰色保活手法创建的Service进程
*
* @author Clock
* @since 2016-04-12
*/
public class GrayService extends Service {

private final static String TAG = GrayService.class.getSimpleName();
/**
* 定时唤醒的时间间隔,5分钟
*/
private final static int ALARM_INTERVAL = 5 * 60 * 1000;
private final static int WAKE_REQUEST_CODE = 6666;

private final static int GRAY_SERVICE_ID = -1001;

@Override
public void onCreate() {
Log.i(TAG, "GrayService->onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "GrayService->onStartCommand");
if (Build.VERSION.SDK_INT < 18) {
startForeground(GRAY_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标
} else {
Intent innerIntent = new Intent(this, GrayInnerService.class);
startService(innerIntent);
startForeground(GRAY_SERVICE_ID, new Notification());
}

//发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(WakeReceiver.GRAY_WAKE_ACTION);
PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_INTERVAL, operation);

return START_STICKY;
}

@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "GrayService->onDestroy");
super.onDestroy();
}

/**
* 给 API >= 18 的平台上用的灰色保活手段
*/
public static class GrayInnerService extends Service {

@Override
public void onCreate() {
Log.i(TAG, "InnerService -> onCreate");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "InnerService -> onStartCommand");
startForeground(GRAY_SERVICE_ID, new Notification());
//stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "InnerService -> onDestroy");
super.onDestroy();
}
}
}

WhileService


import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
import com.example.renzheng.MainActivity;
import com.example.renzheng.R;
/**
* 正常的系统前台进程,会在系统通知栏显示一个Notification通知图标
*
* @author clock
* @since 2016-04-12
*/
public class WhiteService extends Service {

private final static String TAG = WhiteService.class.getSimpleName();
private final static int FOREGROUND_ID = 1000;

@Override
public void onCreate() {
Log.i(TAG, "WhiteService->onCreate");
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "WhiteService->onStartCommand");
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("Foreground");
builder.setContentText("I am a foreground service");
builder.setContentInfo("Content Info");
builder.setWhen(System.currentTimeMillis());
Intent activityIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
Notification notification = builder.build();
startForeground(FOREGROUND_ID, notification);
return super.onStartCommand(intent, flags, startId);
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onDestroy() {
Log.i(TAG, "WhiteService->onDestroy");
super.onDestroy();
}
}

MainActivity


import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import com.example.renzheng.service.BackgroundService;
import com.example.renzheng.service.GrayService;
import com.example.renzheng.service.WhiteService;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private final static String TAG = MainActivity.class.getSimpleName();
/**
* 黑色唤醒广播的action
*/
private final static String BLACK_WAKE_ACTION = "com.wake.black";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.mBtn_white).setOnClickListener(this);
findViewById(R.id.mBtn_gray).setOnClickListener(this);
findViewById(R.id.mBtn_black).setOnClickListener(this);
findViewById(R.id.mBtn_background_service).setOnClickListener(this);
}

@Override
public void onClick(View v) {
int viewId = v.getId();
if (viewId == R.id.mBtn_white) { //系统正常的前台Service,白色保活手段
Intent whiteIntent = new Intent(getApplicationContext(), WhiteService.class);
startService(whiteIntent);

} else if (viewId == R.id.mBtn_gray) {//利用系统漏洞,灰色保活手段(API < 18 和 API >= 18 两种情况)
Intent grayIntent = new Intent(getApplicationContext(), GrayService.class);
startService(grayIntent);

} else if (viewId == R.id.mBtn_black) { //拉帮结派,黑色保活手段,利用广播唤醒队友
Intent blackIntent = new Intent();
blackIntent.setAction(BLACK_WAKE_ACTION);
sendBroadcast(blackIntent);

} else if (viewId == R.id.mBtn_background_service) {//普通的后台进程
Intent bgIntent = new Intent(getApplicationContext(), BackgroundService.class);
startService(bgIntent);
}
}
}

代码注册权限:


 


<receiver
android:name=".receiver.WakeReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.wake.gray" />
</intent-filter>
</receiver>

<service
android:name=".service.WhiteService"
android:enabled="true"
android:exported="false"
android:process=":white" />
<service
android:name=".service.GrayService"
android:enabled="true"
android:exported="false"
android:process=":gray" />
<service
android:name=".service.GrayService$GrayInnerService"
android:enabled="true"
android:exported="false"
android:process=":gray" />
<service
android:name=".service.BackgroundService"
android:enabled="true"
android:exported="false"
android:process=":bg" />
<service
android:name=".receiver.WakeReceiver$WakeNotifyService"
android:enabled="true"
android:exported="false" />

<service
android:name=".receiver.WakeReceiver$WakeGrayInnerService"
android:enabled="true"
android:exported="false" />

 


下面是app唤醒代码:


有2个APP,分别为A和B,当A活着的时候,试着开启B的后台服务,将原本杀死的B的后台服务程序活起来。反之也一样。


1.先看B的代码:


创建一个服务B,给服务添加一个process属性,设置action。


 

<service
android:name=".B"
android:process=":test">
<intent-filter>
<action android:name="yangyang" />
</intent-filter>
</service>

B的代码,在onStartCommand方法中弹出toast:


public class B extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {

Toast.makeText(this, "B 已经唤醒", Toast.LENGTH_SHORT).show();
return START_STICKY;
}
}

2.看A的代码,在MainActivity中点击开启B应用的B服务的代码:


public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

Button btn = (Button) findViewById(R.id.btn);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendService();
}
});
}

private void sendService() {
boolean find = false;

ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
Intent serviceIntent = new Intent();

for (ActivityManager.RunningServiceInfo runningServiceInfo : mActivityManager.getRunningServices(100)) {
if (runningServiceInfo.process.contains(":test")) {//判断service是否在运行
Log.e("zhang", "process:" + runningServiceInfo.process);
find = true;
}
}
//判断服务是否起来,如果服务没起来,就唤醒
if (!find) {
serviceIntent.setPackage("com.example.b);
serviceIntent.setAction("yangyang");
startService(serviceIntent);
Toast.makeText(this, "开始唤醒 B", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(this, "B 不用唤醒", Toast.LENGTH_SHORT).show();
}
}
}

这里只是写了A启动B服务的代码,反之也是一样的。被启动应用的Servcie在AndroidMainfest.xml中注册时注意,添加process属性,和设置action匹配规则。


————————————————
版权声明:本文为CSDN博主「看美丽风晴」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/nazicsdn/article/details/79752617

收起阅读 »

有“声”聚一堂|RTE 2021 编程挑战赛圆满收官啦!

6 月 12 日,由声网Agora 与环信联合主办的“RTE 2021 编程挑战赛”圆满落幕。从 200+ 支参赛队伍中冲出重围的 46 支决赛队伍用精彩的答辩为历时 2 个多月的大赛划下了圆满的句号。今年的“RTE 2021 创新编程挑战赛”共分为 2 个赛...
继续阅读 »

6 月 12 日,由声网Agora 与环信联合主办的“RTE 2021 编程挑战赛”圆满落幕。从 200+ 支参赛队伍中冲出重围的 46 支决赛队伍用精彩的答辩为历时 2 个多月的大赛划下了圆满的句号。

今年的“RTE 2021 创新编程挑战赛”共分为 2 个赛道:应用创新赛道延续了「使用声网Agora SDK 开发应用」的赛题;技术创新赛道开发者可以「利用声网云市场插件接口,开发自研插件与功能演示 Demo」。


尽管此次的赛制与赛题对参赛队伍提出了更高的要求,但同时也为大家提供了独有的技术创新空间。相较去年而言,两个赛道的报名队伍及提交作品几乎都是去年的两倍。

本次大赛的决赛和颁奖都是通过 Agora Video Call App 在线上进行的,全程通过 B 站进行了直播。 最终,决赛共诞生了应用创新赛道的一、二、三等奖团队各一名,“环信专项奖”一名,以及“优秀奖” 六名;技术创新赛道“技术创新专项奖”一名,“优秀奖”一名。



应用创新赛道

一等奖:Agora Home AI

随着智能设备性能提升和网络的快速发展,以音视频为基础的智能硬件也正在蓬勃发展中。跨品牌、跨产品的设备管理也成为萦绕在用户日常使用中绕不开的一个话题。

「Agora Home AI」 系统以智能家居为主题,使用云信令 SDK 实现了IoT 设备远程控制。同时,通过声网Agora RTC SDK 实现人与机器的 1V1 视频,将机器人端采集到的视频发送至 PC 控制中心,进行 AI 智能检测,触发事件响应。


系统采用开源了 Yolo V3 算法进行各种视频数据的处理,支持 C#、C++ 调用;Unity 3D、VS 系列开发。目前已支持 Yolo 基础 80 种物体识别、安全帽识别、冰球识别文件等。采用声网提供的云信令 SDK 进行远程设备控制,构建群组房间进行消息实时通信,支持通过自定义协议进行智能硬件的控制。

「Agora Home AI」可以帮助用户实现可穿戴设备、智能家具设备、视频监控设备接入何控制。包括智能灯光、智能门窗、智能门锁、智能安防、智能手环监测、智能家电控制等配套产品,让用户实现多种品牌的智能设备在统一的交互平台内互联互通、统一管理、智能联动。为给用户创造更舒适、更安全、更节能的家居生活环境。


二等奖:Agora FIow

获得第二名的作品「Agora Flow」是一个基于声网+环信 SDK 搭建的音视频 Low Code Web 共享编辑器。

作品的灵感来源于在使用声网Agora SDK 的过程中,创作者一直在思考关于音视频服务除了以 SDK 的形式来提供服务和为开发者赋能外,还有没有别的形式呢?Low Code 就是这样一个可能的解决方案。将音视频相关功能进行模块化集成,提供一个图形化界面,让开发者可以用做 PPT 的形式来完成想要实现的功能。
作品通过声网的音视频传输及云信令 SDK 产品,提供了基于 Web 的集成了 RTC Chat SDK 的模版工程,通过 CodeGen 来生成配置项。实现了在线流程图编辑器 Low Code 项目的自动生成。作品中的一切的操作几乎都可以通过拖拽来完成。


有开发者开玩笑说,这次的大赛作品很多都是开发了一个 App,而「Agora Flow」则是做了一个帮助开发者能更好开发 App 的项目。



三等奖:都市探险家

「都市探险家」项目是一款利用地图 LBS + 云信令 SDK + 实时音视频构建的社交产品。这款产品为想要寻找共同爱好的新朋友并一起在都市进行旅游、探索的小伙伴而设计。


产品的使用十分简单便捷,用户注册登录后,通过 LBS 地图会自动更新用户所在位置,只要点击“发起任务”并选择“探险”人数,用户就可以与小伙伴进行一次全新的都市探险啦。
对于 RTE 场景而言,产品中实现了多人语聊房场景。并且,通过云信令 SDK 的使用结合了实际的业务场景,对于当下的语聊房场景进行了拓展。 产品未来也会接入视频聊天的功能,让没有办法即时出行的小伙伴也能共同参与到城市的探险当中。



环信专项奖:忘忧馆

「忘忧馆」是一个很有温度的作品,希望可以帮助现代生活中的人们通过彼此倾诉忘掉烦恼、解除忧愁,传播正能量。


这是一款陌生人社交 App,包含信息流。结合了几种最常见的社交产品形态,包括文字聊天,通话等等。让一些不方便与亲人和朋友诉说的烦恼,可以在和陌生人交流时找到共鸣与安慰。


优秀奖:Vchat

「Vchat」利用人脸骨骼识别和云信令 SDK 实现了虚拟 3D 角色的实时通话。使用 tensorflow.js 的 WebGL 引擎作为后端,使用现有开源的人脸识别模型通过摄像头识别人脸的位置以及五官的状态。再通过 Three.js 和 Vrm.js 将人脸数据实时更新到虚拟的 3D 模型上。


在视频部分,通过实时消息 RTM SDK 将人脸骨骼数据实时传输到频道中让其他用户订阅还原人脸。而语音部分则是通过 RTC SDK 将声音进行实时传输并让用户进行订阅。可实现同步换脸、变声聊天等功能。
除了上述的「Vchat」以外,还有「灵动课堂答题组件」、「Agora X-Runtime」、「Weln」、「欢信(bla-bla.app)」、「智能AR毛笔临摹教学系统/CopyTeachWorks」作品获得了此次大赛“应用创新赛道”的优秀奖。关于这些优秀的作品可能没有办法在这里跟大家一一呈现,感兴趣的小伙伴可以前往参赛作品的 Github 仓库进行查看:https://github.com/AgoraIO-Community/RTE-2021-Innovation-Challenge/tree/master/Application-Challenge


技术创新赛道

技术创新专项奖:人脸识别

「技术创新专项奖」是为“技术创新赛道”专门设置的一个奖项。获奖作品是一个在 iOS 平台上使用使用 AgoraEngineKit2 开发接入一个基于 C++ 语言封装的「人脸识别」插件。

作品通过 TYSMExtensionManger 类与对外交互,对内则处理插件实现的相关逻辑。将 IExtensionProvider、IVideoFilter 和自己的开发的 Processer 都放在同一个地方。用 framework 方式对外公开两个文件,既方便开发者查阅,同时也可作为作为参数传递,增强代码可阅读性。 


该插件可以支持人脸检测、追踪、以及多脸的追踪识别,对脸部轮廓、眼睛、眉毛、鼻子、嘴巴等识别到的区域以 3D 点状作出反馈。



优秀奖:Water Mask

「Water Mask」项目是“技术赛道”中的参赛作品,通过在声网 SDK 的视频采集或者播放环节,在 YUV 域上或者编码后添加图片或文字类型的隐性水印。

隐性水印(盲水印)添加后,用户不能直接看到视频中的水印信息。在保护视频发布者版权的同时,也保障了用户的视频观看体验。未来,「Water Mask」还希望在音频处理上,可以扩展声纹水印,在视频版权追溯、认证防伪等场景为行业带来更多、更好的体验。
以上就是本届 「RTE 2021 编程挑战赛」的部分获奖作品及团队情况。关于本次挑战赛的更多作品情况将开源在 Github,感兴趣的小伙伴可前往进行查看:

https://github.com/AgoraIO-Community/RTE-2021-Innovation-Challenge



收起阅读 »

ios中应用Lottie解决动画问题

Lottie的简单介绍:使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份J...
继续阅读 »

Lottie的简单介绍:

使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份JSON文件, 解析动画结构和参数信息并渲染。

Lottie的优点:

1、设计即所见: 设计师用AE设计好动画后直接导出Json文件,Lottie 解析Json文件后调Core Animation的API绘制渲染。还原度更好,开发成本更低。
2、跨平台: 支持iOS、Android、React Native。
3、性能:Lottie对于从AE导出的Json文件,用Core Animation做矢量动画, 性能较佳。Lottie 对解析后的数据模型有内存缓存。但是对多图片帧动画,性能比较差。
支持动画属性多:比起脸书的Keyframes,Lottie支持了更多AE动画属性,比如Mask, Trim Paths,Stroke (shape layer)等。
4、包大小,相比动辄上百K的帧动画,Json文件包大小很小。有图片资源的情况下,同一张图片也可以被多个图层复用,而且运行时内存中只有一个UIImage对象(iOS)。

Lottie在iOS中的使用

1、pod 'lottie-ios' 使用cocoaPods来加载Lottie。
2、在使用的界面添加头文件#import <Lottie/Lottie.h>
3、简单的使用介绍(要想深入学习,还需要自己点击进入源代码中去深究每一个方法和属性,在此就不一一列举了)

LOTAnimationView * animation = [LOTAnimationView animationNamed:@"HappyBirthday"];
animation.loopAnimation = YES; //是否是循环播放
animation.frame = self.view.bounds;
[self.view addSubview:animation];
animation.backgroundColor = [UIColor whiteColor];
[animation playWithCompletion:^(BOOL animationFinished) {
//播放完成,循环播放则不进入此方法
}];
//可以以动画为北京来添加子控件
UILabel * newV = [[UILabel alloc]initWithFrame:CGRectMake(100,100,200,100)];
newV.backgroundColor = [UIColor clearColor];
newV.textColor = [UIColor blackColor];
newV.text = @"Lottie的使用教程";
[animation addSubview:newV];

另外的创建方法

/// Load animation by name from the default bundle, Images are also loaded from the bundle
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));

/// Loads animation by name from specified bundle, Images are also loaded from the bundle
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle NS_SWIFT_NAME(init(name:bundle:));

/// Creates an animation from the deserialized JSON Dictionary
+ (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));

/// Loads an animation from a specific file path. WARNING Do not use a web URL for file path.
+ (nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath NS_SWIFT_NAME(init(filePath:));

/// Creates an animation from the deserialized JSON Dictionary, images are loaded from the specified bundle
+ (nonnull instancetype)animationFromJSON:(nullable NSDictionary *)animationJSON inBundle:(nullable NSBundle *)bundle NS_SWIFT_NAME(init(json:bundle:));

/// Creates an animation from the LOTComposition, images are loaded from the specified bundle
- (nonnull instancetype)initWithModel:(nullable LOTComposition *)model inBundle:(nullable NSBundle *)bundle;

/// Loads animation asynchrounously from the specified URL
- (nonnull instancetype)initWithContentsOfURL:(nonnull NSURL *)url;

LOTAnimationView的属性

/// Flag is YES when the animation is playing
@property (nonatomic, readonly) BOOL isAnimationPlaying;

/// Tells the animation to loop indefinitely.
@property (nonatomic, assign) BOOL loopAnimation;

/// The animation will play forward and then backwards if loopAnimation is also YES
@property (nonatomic, assign) BOOL autoReverseAnimation;

/// Sets a progress from 0 - 1 of the animation. If the animation is playing it will stop and the compeltion block will be called.
/// The current progress of the animation in absolute time.
/// e.g. a value of 0.75 always represents the same point in the animation, regardless of positive
/// or negative speed.
@property (nonatomic, assign) CGFloat animationProgress;

/// Sets the speed of the animation. Accepts a negative value for reversing animation.
@property (nonatomic, assign) CGFloat animationSpeed;

/// Read only of the duration in seconds of the animation at speed of 1
@property (nonatomic, readonly) CGFloat animationDuration;

/// Enables or disables caching of the backing animation model. Defaults to YES
@property (nonatomic, assign) BOOL cacheEnable;

/// Sets a completion block to call when the animation has completed
@property (nonatomic, copy, nullable) LOTAnimationCompletionBlock completionBlock;

/// Set the amimation data
@property (nonatomic, strong, nullable) LOTComposition *sceneModel;

4、简单应用的场景:(1)App的动画引导页。(2)一些特定的动画界面。(3)来作为Tabbar来使用。
5、这里来介绍下作为Tabbar的使用gitHub上原作者
6、Lottie动画资源网站
7、后续有新的学习会更新的。

链接:https://www.jianshu.com/p/7af085a6a20a

收起阅读 »

iOS - Block 准备面试必须了解的东西

一.Block的本质        block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。查看Block源码:struct __block_impl {    void*isa;    int Fla...
继续阅读 »

一.Block的本质

        block本质是一个OC对象,它里面有个isa指针,封装了函数调用环境的OC对象,封装了函数调用上下文的OC对象。


查看Block源码:

struct __block_impl {

    void*isa;

    int Flags;

    int Reserved;

    void *FuncPtr;

};

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct__main_block_desc_0* Desc;

  // 构造函数(类似于OC的init方法),返回结构体对象

  __main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,intflags=0) {

    impl.isa = &_NSConcreteStackBlock;

    impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;

  }

};

// 封装了block执行逻辑的函数

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);

        }

static struct __main_block_desc_0 {

  size_treserved;

  size_tBlock_size;

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(intargc,constchar* argv[]) {

    /* @autoreleasepool */{__AtAutoreleasePool__autoreleasepool;

        // 定义block变量

        void(*block)(void) = &__main_block_impl_0(

                                                   __main_block_func_0,

                                                   &__main_block_desc_0_DATA

                                                   );

        // 执行block内部的代码

        block->FuncPtr(block);

    }

    return0;

}

说明:FuncPtr:指向调用函数的地址,__main_block_desc_0 :block描述信息,Block_size:block的大小

二.Block变量的捕获

2.1局部变量的捕获

        对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

int age=10;

void(^Block)(void)=^{

NSLog(@"age:%d",age);

};

age=20;

Block();

2.2__block 修饰的外部变量

        对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值

__block int age=10;

myBlock block=^{

NSLog(@"age = %d",age);

};

age=18;

block();

输出:18;

auto int age=10;

static int num=25;

void(^Block)(void)=^{

NSLog(@"age:%d,num:%d",age,num);

};

age=20;

num=11;

Block();

        输出结果为:age:10,num:11,auto变量block访问方式是值传递,也就是当block定义的时候,值已经传到block里面了,static变量block访问方式是指针传递,auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可,block不需要对全局变量捕获,都是直接采用取值的,局部变量的捕获是因为考虑作用域的问题,需要跨函数访问,就需要捕获,当出了作用域,局部变量已经被销毁,这时候如果block访问,就会出问题。

2.2.block变量捕获机制




 block里访问self,self是当调用block函数的参数,参数是局部变量,self指向调用者,所以它也会捕获self,block里访问成员,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量。

3.3Block的类型

        block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

__NSGlobalBlock __ ( _NSConcreteGlobalBlock )全局block即数据区

__NSStackBlock __ ( _NSConcreteStackBlock )堆区block

__NSMallocBlock __ ( _NSConcreteMallocBlock )栈区block

        说明:堆区,程序员自己控制,程序员自己管理,栈区,系统自动控制,一般我们使用最多的是堆区Block,判断类型的根据是没有访问auto变量的block是__NSGlobalBlock __ ,放在数据段访问了auto变量的block是__NSStackBlock __;[__NSStackBlock __ copy]操作就变成了__NSMallocBlock __,__NSGlobalBlock __ 调用copy操作后,什么也不做__NSStackBlock __ 调用copy操作后,复制效果是:从栈复制到堆;副本存储位置是堆__NSMallocBlock __ 调用copy操作后,复制效果是:引用计数增加;副本存储位置是堆,在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况是:

                1.block作为函数返回值时

                2.将block赋值给__strong指针时

                3.block作为Cocoa API中方法名含有usingBlock的方法参数时

                4.block作为GCD API的方法参数时

三.对象类型的auto变量

typedefvoid(^XBTBlock)(void);

XBTBlock block;

{

Person*p=[[Person alloc]init];

p.age=10;

block=^{

NSLog(@"======= %d",p.age);

};}

Person.m

-(void)dealloc{

NSLog(@"Person - dealloc");

}

        说明:block为堆block,block里面有一个Person指针,Person指针指向Person对象。只要block还在,Person就还在。block强引用了Person对象。在MRC下,就会打印,因为堆空间的block会对Person对象retain操作,拥有一次Person对象。无论MRC还是ARC,栈空间上的block,不会持有对象;堆空间的block,会持有对象。

特别说明:block内部访问了对象类型的auto变量时,是否会强引用?

栈block

a) 如果block是在栈上,将不会对auto变量产生强引用

b) 栈上的block随时会被销毁,也没必要去强引用其他对象

堆block

1.如果block被拷贝到堆上:

a) 会调用block内部的copy函数

b) copy函数内部会调用_Block_object_assign函数

c) _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

2.如果block从堆上移除

a) 会调用block内部的dispose函数

b) dispose函数内部会调用_Block_object_dispose函数

c) _Block_object_dispose函数会自动释放引用的auto变量(release)

正确答案:

如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象

如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

3.2gcd的block中引用 Person对象什么时候销毁?

eg:-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{

    Person*person = [[Personalloc]init];

    person.age=10;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"age:%d",person.age);

    });

    NSLog(@"touchesBegan");

}

输出:touchesBegan

            age:10

            Person-dealloc

        说明:gcd的block默认会做copy操作,即dispatch_after的block是堆block,block会对Person强引用,block销毁时候Person才会被释放,如果上诉Person用__weak。即添加代码为__weak Person*weakPerson=person;,在Block中变成NSLog(@"age:%p",weakPerson);,它就不输出age,使用__weak修饰过后的对象,堆block会采用弱引用,无法延时Person的寿命,所以在touchesBegan函数结束后,Person就会被释放,gcd就无法捕捉到Person,gcd内部只要有强引用Person,Person就会等待执行完再销毁!如果gcd内部先强引用后弱引用,Person会等待强引用执行完毕后释放,只要强引用执行完,就不会等待后执行的弱引用,会直接释放的

eg:-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{

    Person*person = [[Personalloc]init];

    person.age=10;

    __weakPerson*weakPerson = person;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),

                   dispatch_get_main_queue(), ^{

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            NSLog(@"2-----age:%p",weakPerson);

        });

        NSLog(@"1-----age:%p",person);

    });

    NSLog(@"touchesBegan");

}

四.Block的修饰符

        block在修改NSMutableArray,不需要加__block,auto修饰变量,block无法修改,因为block使用的时候是内部创建了变量来保存外部的变量的值,block只有修改内部自己变量的权限,无法修改外部变量的权限。

        static修饰变量,block可以修改,因为block把外部static修饰变量的指针存入,block直接修改指针指向变量值,即可修改外部变量值。全局变量值,全局变量无论哪里都可以修改,当然block内部也可以修改。

eg:__block int age = 10,系统做了哪些---》编译器会将__block变量包装成一个对象

__block 修饰符作用:

        __block可以用于解决block内部无法修改auto变量值的问题

        __block不能修饰全局变量、静态变量(static)

        编译器会将__block变量包装成一个对象

        __block修改变量:age->__forwarding->age        

        __Block_byref_age_0结构体内部地址和外部变量age是同一地址

        __block的内存管理---->当block在栈上时,并不会对__block变量产生强引用

block的属性修饰词为什么是copy?

        block一旦没有进行copy操作,就不会在堆上

        block在堆上,程序员就可以对block做内存管理等操作,可以控制block的生命周期,会调用block内部的copy函数

        copy函数内部会调用_Block_object_assign函数

        _Block_object_assign函数会对__block变量形成强引用(retain)

        对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用,当block从堆中移除时,会调用block内部的dispose函数dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的__block变量(release),当block在栈上时,对它们都不会产生强引用,当block拷贝到堆上时,都会通过copy函数来处理它们,对于__block 修饰的变量 assign函数对其强引用;对于外部对象 assign函数根据外部如何引用而引用

__block的__forwarding指针说明:

        栈上__block的__forwarding指向本身

        栈上__block复制到堆上后,栈上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身

五. block循环引用

        1.ARC下如何解决block循环引用的问题?

        三种方式:__weak、__unsafe_unretained、__block

        1)第一种方式:__weak

        Person*person=[[Person alloc]init];

        // __weak Person *weakPerson = person;

        __weaktypeof(person)weakPerson=person;

        person.block=^{

            NSLog(@"age is %d",weakPerson.age);

        };

        2)第二种方式:__unsafe_unretained

        __unsafe_unretained Person*person=[[Person alloc]init];

        person.block=^{

            NSLog(@"age is %d",weakPerson.age);

        };

        3)第三种方式:__block

        __block Person*person=[[Person alloc]init];

        person.block=^{

            NSLog(@"age is %d",person.age);

            person=nil;

        };

        person.block();

三种方法比较:__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil,__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,__block:必须把引用对象置位nil,并且要调用该block









作者:枫紫
链接:https://www.jianshu.com/p/4bde3936b154






收起阅读 »

iOS - Metal的认识

一.Metal 简介        在 WWDC 2014 上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲...
继续阅读 »

一.Metal 简介

        在 WWDC 2014 上,Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲染性能,并支持大家熟悉的游戏引擎及公司。

        Metal 是一种低层次的渲染应用程序编程接口,提供了软件所需的最低层,保证软件可以运行在不同的图形芯片上。Metal 提升了 A7 与 A8 处理器效能,让其性能完全发挥。

        Metal,充分利用GPU的运算能力,在现阶段,AVFoundation ⼈脸识别/.... 等大量需要显示计算的时候,苹果采用了硬件加速器驱动GPU工作,在音视频方面,⾳频编码/解码 / 视频编码/解码 ->压缩任务 ->都与硬件加速器分不开,苹果提供的Metal,能发挥GPU/CPu的最大性能,并且管理我们的资源。

二.Metal的渲染流程

        Metal的渲染流程借鉴了OpenGLES的流程,它通过控制顶点着色器/片元着色器(Metal里面叫顶点函数/片元函数),交给帧缓冲区,最后显示到屏幕上





值得注意的是,在OpenGlES中,图元装配有9中,在Metal中,图元装配只有五种,他们分别是:

                 MTLPrimitiveTypePoint = 0, 点

                 MTLPrimitiveTypeLine = 1, 线段

                 MTLPrimitiveTypeLineStrip = 2, 线环

                 MTLPrimitiveTypeTriangle = 3,  三角形

                 MTLPrimitiveTypeTriangleStrip = 4, 三角型扇

三.Metal的初级准备工作

3.1Metal的注意事项

        在讲Metal的初级使用之前,我们先来看看苹果爸爸给我们的建议,首先,苹果建议我们Separate Your Rendering Loop,即分离我们渲染,Metal给我们提供了一个View,叫MTKView,它继承自UiView,它主要的渲染是通过MTKViewDelegate协议回调实现,两个重要的协议方法是:

        1)当MTKView视图发生大小改变时调用

        /*!

         @method mtkView:drawableSizeWillChange:

         @abstract Called whenever the drawableSize of the view will change

         @discussion Delegate can recompute view and projection matricies or regenerate any buffers to be compatible with the new view size or resolution

         @paramviewMTKView which called this method

         @paramsizeNew drawable size in pixels

         */

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size;

        2)每当视图需要渲染时调用

        /*!

         @method drawInMTKView:

         @abstract Called on the delegate when it is asked to render into the view

         @discussion Called on the delegate when it is asked to render into the view

         */

        - (void)drawInMTKView:(nonnullMTKView*)view;

    3.2  Metal是如何驱动GPU工作的?



相关对应代码:在ViewController中,我们把当前的View变成MTKView,当然你也可以用self.view添加一个子视图View,CCRenderer是自定义的一个类,主要是分离MTview的渲染,

         _view.device = MTLCreateSystemDefaultDevice();一个MTLDevice 对象就代表这着一个GPU,通常我们可以调用方法MTLCreateSystemDefaultDevice()来获取代表默认的GPU单个对象.

        在CCRenderer中的初始化方法中- (id)initWithMetalKitView:(MTKView *)mtkView我们拿到device,创建newCommandQueue队列:

                _commandQueue = [_device newCommandQueue];

        所有应用程序需要与GPU交互的第一个对象是一个对象->MTLCommandQueue. 你使用MTLCommandQueue 去创建对象,并且加入MTLCommandBuffer 对象中.确保它们能够按照正确顺序发送到GPU.对于每一帧,一个新的MTLCommandBuffer 对象创建并且填满了由GPU执行的命令.

        在CCRenderer中,我们实现了MTKView的协议代理方法,在- (void)drawInMTKView:(nonnullMTKView*)view中,我们通过创建好的队列再创建命令缓冲区并且加入到MTCommandBuffer对象中去:

                id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

        值得注意的是,在创建好命令缓冲区后,Metal提出了一个概念叫渲染描述符:(个人理解这个渲染描述符是给每个命令打上一个标记,GPU在工作的时候通过这个渲染描述符取出相应的命令,如果说的不对,请大神指点)从视图绘制中,获得渲染描述符:

                MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

通过渲染描述符renderPassDescriptor创建MTLRenderCommandEncoder                

                id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];

        最后 [renderEncoderendEncoding];

当编码器结束之后,命令缓存区就会接受到2个命令.

         1) present

         2) commit

         因为GPU是不会直接绘制到屏幕上,因此你不给出去指令.是不会有任何内容渲染到屏幕上.

        [commandBuffer presentDrawable:view.currentDrawable];

        [commandBuffercommit];

        至此,Metal的准备工作已经完成

四.用Metal渲染一个简单的三角形

在做好上面的准备的准备工作后:


//初始化MTKView

- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView

{

    self= [superinit];

    if(self)

    {

        NSError*error =NULL;


        //1.获取GPU 设备

        _device= mtkView.device;

        //2.在项目中加载所有的(.metal)着色器文件

        // 从bundle中获取.metal文件

        id defaultLibrary = [_devicenewDefaultLibrary];

        //从库中加载顶点函数

        id vertexFunction = [defaultLibrarynewFunctionWithName:@"vertexShader"];

        //从库中加载片元函数

        id fragmentFunction = [defaultLibrarynewFunctionWithName:@"fragmentShader"];

        //3.配置用于创建管道状态的管道

        MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];

        //管道名称

        pipelineStateDescriptor.label=@"Simple Pipeline";

        //可编程函数,用于处理渲染过程中的各个顶点

        pipelineStateDescriptor.vertexFunction= vertexFunction;

        //可编程函数,用于处理渲染过程中各个片段/片元

        pipelineStateDescriptor.fragmentFunction= fragmentFunction;

        //一组存储颜色数据的组件

        pipelineStateDescriptor.colorAttachments[0].pixelFormat= mtkView.colorPixelFormat;


        //4.同步创建并返回渲染管线状态对象

        _pipelineState= [_devicenewRenderPipelineStateWithDescriptor:pipelineStateDescriptorerror:&error];

        //判断是否返回了管线状态对象

        if (!_pipelineState)

        {


            //如果我们没有正确设置管道描述符,则管道状态创建可能失败

            NSLog(@"Failed to created pipeline state, error %@", error);

            returnnil;

        }

        //5.创建命令队列

        _commandQueue = [_device newCommandQueue];

    }

    return self;

}

//每当视图需要渲染帧时调用

- (void)drawInMTKView:(nonnullMTKView*)view

{

    //1. 顶点数据/颜色数据

    staticconstCCVertextriangleVertices[] =

    {

        //顶点,    RGBA 颜色值

        { {  0.5, -0.25,0.0,1.0}, {1,0,0,1} },

        { { -0.5, -0.25,0.0,1.0}, {0,1,0,1} },

        { { -0.0f,0.25,0.0,1.0}, {0,0,1,1} },

    };

    //2.为当前渲染的每个渲染传递创建一个新的命令缓冲区

    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

    //指定缓存区名称

    commandBuffer.label=@"MyCommand";


    //3.

    // MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。

    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

    //判断渲染目标是否为空

    if(renderPassDescriptor !=nil)

    {

        //4.创建渲染命令编码器,这样我们才可以渲染到something

        id renderEncoder =[commandBufferrenderCommandEncoderWithDescriptor:renderPassDescriptor];

        //渲染器名称

        renderEncoder.label=@"MyRenderEncoder";

        //5.设置我们绘制的可绘制区域

        /*

        typedef struct {

            double originX, originY, width, height, znear, zfar;

        } MTLViewport;

         */

        //视口指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域

        //为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。

        MTLViewportviewPort = {

            0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0

        };

        [renderEncodersetViewport:viewPort];

        //[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];


        //6.设置当前渲染管道状态对象

        [renderEncodersetRenderPipelineState:_pipelineState];



        //7.从应用程序OC 代码 中发送数据给Metal 顶点着色器 函数

        //顶点数据+颜色数据

        //  1) 指向要传递给着色器的内存的指针

        //  2) 我们想要传递的数据的内存大小

        //  3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。

        [renderEncodersetVertexBytes:triangleVertices

                               length:sizeof(triangleVertices)

                              atIndex:CCVertexInputIndexVertices];

        //viewPortSize 数据

        //1) 发送到顶点着色函数中,视图大小

        //2) 视图大小内存空间大小

        //3) 对应的索引

        [renderEncodersetVertexBytes:&_viewportSize

                               length:sizeof(_viewportSize)

                              atIndex:CCVertexInputIndexViewportSize];



        //8.画出三角形的3个顶点

        // @method drawPrimitives:vertexStart:vertexCount:

        //@brief 在不使用索引列表的情况下,绘制图元

        //@param 绘制图形组装的基元类型

        //@param 从哪个位置数据开始绘制,一般为0

        //@param 每个图元的顶点个数,绘制的图型顶点数量

        /*

         MTLPrimitiveTypePoint = 0, 点

         MTLPrimitiveTypeLine = 1, 线段

         MTLPrimitiveTypeLineStrip = 2, 线环

         MTLPrimitiveTypeTriangle = 3,  三角形

         MTLPrimitiveTypeTriangleStrip = 4, 三角型扇

         */


        [renderEncoderdrawPrimitives:MTLPrimitiveTypeTriangle

                          vertexStart:0

                          vertexCount:3];

        //9.表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离

        [renderEncoderendEncoding];

        //10.一旦框架缓冲区完成,使用当前可绘制的进度表

        [commandBufferpresentDrawable:view.currentDrawable];

    }

    //11.最后,在这里完成渲染并将命令缓冲区推送到GPU

    [commandBuffercommit];

}



 

Metal文件:(语法下篇介绍)

#include 

//使用命名空间 Metal

using namespace metal;

// 导入Metal shader 代码和执行Metal API命令的C代码之间共享的头

#import "CCShaderTypes.h"

// 顶点着色器输出和片段着色器输入

//结构体

typedef struct

{

    //处理空间的顶点信息

    float4clipSpacePosition [[position]];

    //颜色

    float4color;

} RasterizerData;

//顶点着色函数

vertex RasterizerData

vertexShader(uintvertexID [[vertex_id]],

             constantCCVertex*vertices [[buffer(CCVertexInputIndexVertices)]],

             constantvector_uint2*viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])

{

    /*

     处理顶点数据:

        1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.

        2) 将顶点颜色值传递给返回值

     */


    //定义out

    RasterizerDataout; 

//    //初始化输出剪辑空间位置

//    out.clipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0);

//

//    // 索引到我们的数组位置以获得当前顶点

//    // 我们的位置是在像素维度中指定的.

//    float2 pixelSpacePosition = vertices[vertexID].position.xy;

//

//    //将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型

//    vector_float2 viewportSize = vector_float2(*viewportSizePointer);

//

//    //每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.

//    //计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.

//    out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);

    out.clipSpacePosition= vertices[vertexID].position;

    //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.

    out.color= vertices[vertexID].color;

    //完成! 将结构体传递到管道中下一个阶段:

    returnout;

}

//当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.

// 片元函数

//[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.

//一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.

//被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.

fragmentfloat4fragmentShader(RasterizerDatain [[stage_in]])

{

    //返回输入的片元颜色

    returnin.color;

}

用于OC和Metal桥接的文件:

/*

 介绍:

 头文件包含了 Metal shaders 与C/OBJC 源之间共享的类型和枚举常数

*/

#ifndef CCShaderTypes_h

#define CCShaderTypes_h

// 缓存区索引值 共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用

typedef enum CCVertexInputIndex

{

    //顶点

    CCVertexInputIndexVertices    =0,

    //视图大小

    CCVertexInputIndexViewportSize =1,

} CCVertexInputIndex;

//结构体: 顶点/颜色值

typedef struct

{

    // 像素空间的位置

    // 像素中心点(100,100)

    vector_float4 position;

    // RGBA颜色

    vector_float4 color;

} CCVertex;

#endif


作者:枫紫
链接:https://www.jianshu.com/p/a6f3c90d6ba5





收起阅读 »

iOS KVO底层原理&&KVO的isa指向

一.简单复习一下KVO的使用定义一个类,继承自NSObject,并添加一个name的属性#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface TCPerson ...
继续阅读 »

一.简单复习一下KVO的使用

  • 定义一个类,继承自NSObject,并添加一个name的属性
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END

  • 在ViewController我们简单的使用一下KVO
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end


当点击屏幕的时候,控制台输出:

2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到<TCPerson: 0x600003444d10>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

二.深入剖析KVO的底层

  • 在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.name = @"cang lao shi";
    }我们知道self.person1.name的本质是[self.person1 setName:@"cang lao shi"];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}
在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:

- (void)setName:(NSString *)name{
_name = name;
}

  • 在ViewController我们新建一个person2,代码变成了:
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];

self.person2.name = @"ttttttttt";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

  • 注意:当我们点击屏幕的时候输出的结果是:

2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到<TCPerson: 0x600002ce8230>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

  • 既然我们改变name的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?



三.KVO的isa指向

  • 上篇文章中我分析了实例对象,类对象,元类对象的isa,既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:
    打开lldb

(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
  • 从上面的打印我们看到 self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson
  • NSKVONotifying_TCPerson是runtime动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面代码不能在xcode中运行):

  • #import "NSKVONotifying_TCPerson.h"

    @implementation NSKVONotifying_TCPerson
    //NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
    - (void)setName:(NSString *)name{
    _NSSetIntVaueAndNotify();
    }
    //改变过程
    void _NSSetIntVaueAndNotify(){
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
    }
    //通知观察者
    - (void)didChangeValueForKey:(NSString *key){
    [observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context];
    }
    @end
    未添加观察self.person2实例对象的isa指向流程图:



    添加观察self.person1实例对象的isa指向流程图:




    所以KVO其本质是动态生成一个NSKVONotifying_TCPerson
    类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/0b6083b91b04
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。







    收起阅读 »

    View系列:事件分发(二)

    滑动冲突常见场景:内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)内外层滑动方向一致(如:RecyclerView嵌套)一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截父View事件发送方,父...
    继续阅读 »

    滑动冲突

    常见场景:

    1. 内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)
    2. 内外层滑动方向一致(如:RecyclerView嵌套)

    image-20210602150942026

    一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截

    父View

    事件发送方,父View拦截。

    父View根据自己的需求,选择在何时给onInterceptTouchEvent返回true,使事件直接分发给自己处理(前提:子View未设置requestDisallowInteceptTouchEvent(true),否则根本就不会经过onInterceptTouchEvent方法)。

    • DOWN不要拦截,否则根据事件分发逻辑,事件直接给父View自己处理了
    • UP不要拦截,否则子View无法出发click事件,无法移除longClick消息
    • 在MOVE中根据逻辑需求判断是否拦截
        public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
    intercepted = false;
    break;
    }
    case MotionEvent.ACTION_UP: {
    intercepted = false;
    break;
    }
    case MotionEvent.ACTION_MOVE: {
    if (满足父容器的拦截要求) {
    intercepted = true;
    } else {
    intercepted = false;
    }
    break;
    }
    }
    return intercepted;
    }

    子View

    事件接收方,内部拦截

    事件已经传递到子View,子View只有选择是否消费该事件,或者向父View申请拦截事件。

    注意:申请拦截事件,不代表就以后就收不到事件了。request只是会清除FLAG_DISALLOW_INTERCEPT标记,导致父View检查onInterceptTouchEvent方法,仅此而已(恢复到默认状态)。主要看父View.onInterceptTouchEvent中的返回值。

        public boolean dispatchTouchEvent(MotionEvent event) {//或 onTouchEvent
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
    parent.requestDisallowInterceptTouchEvent(true);//不许拦截
    break;
    }
    case MotionEvent.ACTION_MOVE: {
    int deltaX = x - mLastX;
    int deltaY = y - mLastY;
    if (父容器需要此类点击事件) {
    parent.requestDisallowInterceptTouchEvent(false);//申请拦截
    }
    break;
    }
    case MotionEvent.ACTION_UP: {
    break;
    }
    }
    return super.dispatchTouchEvent(event);
    }

    :cry:多点触控

    安卓自定义View进阶-多点触控详解

    自由地对图片进行缩放和移动

    多点触控相关的事件:

    事件简介
    ACTION_DOWN第一个 手指 初次接触到屏幕 时触发。
    ACTION_MOVE手指 在屏幕上滑动 时触发,会多次触发(单个或多个手指)。
    ACTION_UP最后一个 手指 离开屏幕时触发。
    ACTION_POINTER_DOWN有非主要的手指按下(即按下之前已经有手指在屏幕上)。
    ACTION_POINTER_UP有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。
    以下事件类型不推荐使用---以下事件在2.0开始,在 2.2 版本以上被废弃---
    ACTION_POINTER_1_DOWN第 2 个手指按下,已废弃,不推荐使用。
    ACTION_POINTER_2_DOWN第 3 个手指按下,已废弃,不推荐使用。
    ACTION_POINTER_3_DOWN第 4 个手指按下,已废弃,不推荐使用。
    ACTION_POINTER_1_UP第 2 个手指抬起,已废弃,不推荐使用。
    ACTION_POINTER_2_UP第 3 个手指抬起,已废弃,不推荐使用。
    ACTION_POINTER_3_UP第 4 个手指抬起,已废弃,不推荐使用。

    多点触控相关的方法:

    方法简介
    getActionMasked()与 getAction() 类似,多点触控需要使用这个方法获取事件类型
    getActionIndex()获取该事件是哪个指针(手指)产生的。
    getPointerCount()获取在屏幕上手指的个数。
    getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
    findPointerIndex(int pointerId)通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。
    getX(int pointerIndex)获取某一个指针(手指)的X坐标
    getY(int pointerIndex)获取某一个指针(手指)的Y坐标

    index和pointId

    在 2.2 版本以上,我们可以通过getActionIndex() 轻松获取到事件的索引(Index),Index 变化有以下几个特点:

    1、从 0 开始,自动增长。 2、之前落下的手指抬起,后面手指的 Index 会随之减小。 (0、1、2 --> 第2个手指抬起 --> 第三个手指变为1 --> 0、1) 3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。 4、对 move 事件无效。 **getActionIndex()**获取到的始终是数值 0

    相同点不同点
    1. 从 0 开始,自动增长。
    2. 落下手指时优先填补空缺(填补之前抬起手指的编号)。
    Index 会变化,pointId 始终不变。

    pointerIndex 与 pointerId

    pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。

    类型简介
    pointerIndex用于获取具体事件,可能会随着其他手指的抬起和落下而变化
    pointerId用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变

    这两个数值使用以下两个方法相互转换:

    方法简介
    getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
    findPointerIndex(int pointerId)通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。

    自定义View示例

    img
    /**
    * Created by Varmin
    * on 2017/7/5 16:16.
    * 文件描述:left,content,right三个tag,在布局中给每个部分设置该tag。用于该ViewGroup内部给子View排序。
    * 功能:默认全部关闭左右滑动。分别设置打开
    */
    public class SlideView extends ViewGroup implements View.OnClickListener, View.OnLongClickListener {
    private static final String TAG = "SlideView";
    public final String LEFT = "left";
    public final String CONTENT = "content";
    public final String RIGHT = "right";
    private Scroller mScroller;
    /**
    * scroller滑动时间。默认250ms
    */
    public static final int DEFAULT_TIMEOUT = 250;
    public static final int SLOW_TIMEOUT = 500;
    /**
    * 左右View的宽度
    */
    private int leftWidth;
    private int rightWidth;
    private GestureDetector mGesture;
    private ViewConfiguration mViewConfig;

    public SlideView(Context context) {
    super(context);
    init(context);
    }

    public SlideView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
    }

    public SlideView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
    }

    private void init(Context context) {
    mScroller = new Scroller(context);
    //都是自己处理的,这里没有用到该手势方法
    //缺点:误差有些大。这种精确滑动的,最好自己判断
    mGesture = new GestureDetector(context, new SlideGestureDetector());
    mViewConfig = ViewConfiguration.get(context);
    //默认false
    setClickable(true);
    }

    /**
    * 所有的子View都映射完xml,该方法最早能获取到childCount
    * 在onMeasuer/onLayout中获取,注册监听的话,会多次被调用
    * 在构造方法中,不能获取到childCount。
    */
    @Override
    protected void onFinishInflate() {
    super.onFinishInflate();
    initListener();
    }

    private void initListener() {
    for (int i = 0; i < getChildCount(); i++) {
    View childView = getChildAt(i);
    childView.setClickable(true);
    childView.setOnClickListener(this);
    if (CONTENT.equals(childView.getTag())) {
    childView.setOnLongClickListener(this);
    }
    }

    }
    @Override
    public void onClick(View v) {
    String tag = (String) v.getTag();
    switch (tag) {
    case LEFT:
    Toast.makeText(getContext(), "Left", Toast.LENGTH_SHORT).show();
    break;
    case CONTENT:
    Toast.makeText(getContext(), "Content", Toast.LENGTH_SHORT).show();
    closeAll(SLOW_TIMEOUT);
    break;
    case RIGHT:
    Toast.makeText(getContext(), "Right", Toast.LENGTH_SHORT).show();
    break;
    }
    }

    @Override
    public boolean onLongClick(View v) {
    Toast.makeText(getContext(), "Content_LongClick", Toast.LENGTH_SHORT).show();
    return true;
    }

    /**
    * 每个View的大小都是由父容器给自己传递mode来确定。
    * 每个View的位置都是由父容器给自己设定好自己在容器中的左上右下来确定位置。
    * 所以,继承至ViewGroup的容器,要在自己内部实现对子View大小和位置的确定。
    */

    /**
    * 子View不会自己测量自己的,所以在这里测量各个子View大小
    * 另外,处理自己是wrap的情况,给自己一个确定的值。
    */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    //测量子View
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    //测量自己
    //默认是给该ViewGroup设置固定宽高,假设不纯在wrap情况,onlayout中也不考虑此情况
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
    View childView = getChildAt(i);
    int childWidth = childView.getMeasuredWidth();
    int childHeight = childView.getMeasuredHeight();
    String tag = (String) childView.getTag();
    switch (tag) {
    case LEFT:
    leftWidth = childWidth;
    childView.layout(-childWidth, 0, 0, childHeight);
    break;
    case CONTENT:
    childView.layout(0, 0, childWidth, childHeight);
    break;
    case RIGHT:
    rightWidth = childWidth;
    childView.layout(getMeasuredWidth(), 0,
    getMeasuredWidth() + childWidth, childHeight);
    break;
    }
    }

    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean handled = super.onInterceptTouchEvent(ev);
    if (handled) {
    return true;
    }
    switch (ev.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
    mInitX = (int) ev.getX();
    mInitY = (int) ev.getY();
    break;
    case MotionEvent.ACTION_MOVE:
    int offsetX = (int) (ev.getX() - mInitX);
    int offsetY = (int) (ev.getY() - mInitY);
    /**
    * 判断可以横向滑动了
    * 1,拦截自己的子View接收事件
    * 2,申请父ViewGroup不要看拦截事件。
    */
    if ((Math.abs(offsetX) - Math.abs(offsetY)) > mViewConfig.getScaledTouchSlop()) {
    requestDisallowInterceptTouchEvent(true);
    return true;
    }
    break;
    case MotionEvent.ACTION_UP:
    //重置回ViewGroup默认的拦截状态
    requestDisallowInterceptTouchEvent(false);
    break;
    }
    return handled;
    }

    private int mInitX;
    private int mOffsetX;
    private int mInitY;
    private int mOffsetY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    boolean handled = false;
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
    break;
    case MotionEvent.ACTION_MOVE:
    mOffsetX = (int) (event.getX() - mInitX);
    mOffsetY = (int) (event.getY() - mInitY);
    if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > 0) {//横向触发条件
    //预估,偏移offsetX后的大小
    int mScrollX = getScrollX() + (-mOffsetX);
    if (mScrollX <= 0) {//向右滑动,显示leftView:110
    //上面的是预估,如果预估大于目标:你不能return放弃了,要调整mOffsetX的值使其刚好等于目标
    if (Math.abs(mScrollX) > leftWidth) {
    mOffsetX = leftWidth - Math.abs(getScrollX());
    //return true;
    }
    }else {//向左滑动,显示rightView:135
    if (mScrollX > rightWidth) {
    mOffsetX = getScrollX() - rightWidth;
    //return true;
    }
    }
    this.scrollBy(-mOffsetX,0);
    mInitX = (int) event.getX();
    mInitY = (int) event.getY();
    return true;
    }

    break;
    case MotionEvent.ACTION_UP:
    int upScrollX = getScrollX();
    if (upScrollX > 0) {//向左滑动,显示rightView
    if (upScrollX >= (rightWidth/2)) {
    mOffsetX = upScrollX - rightWidth;
    }else {
    mOffsetX = upScrollX;
    }
    }else {//向右,显示leftView
    if (Math.abs(upScrollX) >= (leftWidth/2)) {
    mOffsetX = leftWidth - Math.abs(upScrollX);
    }else {
    mOffsetX = upScrollX;
    }
    }
    // this.scrollBy(-mOffsetX,0);//太快
    // startScroll(-mOffsetX, 0, 1000);//直接放进去,不行?
    /**
    * 注意startX。dx表示的是距离,不是目标位置
    */
    mScroller.startScroll(getScrollX(), getScrollY(), -mOffsetX, 0,SLOW_TIMEOUT);
    invalidate();

    break;
    }

    if (!handled) {
    handled = super.onTouchEvent(event);
    }
    return handled;
    }


    @Override
    public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
    scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    invalidate();
    }
    }


    /**
    * 虽然传入的dx、dy并不是scrollTo实际要到的点,dx,dy只是一小段距离。
    * 但是computeScroll()我们scrollTo的是:现在位置+dx的距离 = 目标位置
    *
    * @param dx //TODO *距离!距离!并不是说要到达的目标。*
    * @param dy
    * @param duration 默认的滑动时间是250,复位的时候如果感觉太快可以自己设置事件.
    *
    */
    private void startScroll(int dx, int dy, int duration) {
    mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
    //mScroller.extendDuration(duration); 在250ms基础上增加。构造函数传入的话,就是duration的时间。
    invalidate();
    }


    /**
    * 是否打开,ListView中复用关闭
    * @return
    */
    public boolean isOpened(){
    return getScrollX() != 0;
    }
    public void closeAll(int duration){
    mScroller.startScroll(getScrollX(), getScrollY(), (-getScrollX()), 0, duration);
    invalidate();
    }
    }

    Tips

    scrollTo/By

    通过三种方式可以实现View的滑动:

    1. 通过View本身提供的scrollTo/scrollBy方法;

    2. 通过动画使Veiw平移。

    3. 通过改变View的LayoutParams属性值。

    **setScrollX/Y、scrollTo: **移动到x,y的位置

    **scrollBy: **移动x,y像素的距离

        public void setScrollX(int value) {
    scrollTo(value, mScrollY);
    }

    public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
    }

    public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
    int oldX = mScrollX;
    int oldY = mScrollY;
    mScrollX = x;
    mScrollY = y;
    invalidateParentCaches();
    onScrollChanged(mScrollX, mScrollY, oldX, oldY);
    }
    }

    **注意:**假如scrollTo(30,10),按照View右下正,左上负的概念,因该是向右滑动30,向下滑动10。


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

    收起阅读 »

    View系列:事件分发(一)

    基础相关View坐标系MotionEvent当用户触摸屏幕的时候,产生Touch事件,事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象事件类型具体动作MotionEvent.ACTION_DOWN按下View(所有事件的开始)Moti...
    继续阅读 »

    基础相关

    View坐标系

    View坐标系

    MotionEvent

    当用户触摸屏幕的时候,产生Touch事件,事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象

    image-20210531100221285

    image-20210531100250617

    事件类型具体动作
    MotionEvent.ACTION_DOWN按下View(所有事件的开始)
    MotionEvent.ACTION_MOVE滑动View
    MotionEvent.ACTION_UP抬起View(与DOWN对应)
    MotionEvent.ACTION_CANCEL结束事件
    MotionEvent.ACTION_OUTSIDE事件发生在视图范围外

    辅助类

    辅助类-dev

    View触摸相关工具类全解

    ViewConfiguration

    获取 Android 系统常用的距离、速度、时间等常量

    VelocityTracker

    跟踪触摸事件的速度。此设置对于手势标准中包含速度的手势(例如滑动)非常有用。

    GestureDetector

    手势检测,该类支持的一些手势包括 onDown()、onLongPress()、onFling() 等。可以将 GestureDetector 与onTouchEvent() 方法结合使用。

    OverScroller

    回弹工具类,不同的回弹效果可以自定义不同的动画插值器

    TouchDelegate

    扩展子视图的可轻触区域

    img

    view1.post(new Runnable() {
    @Override
    public void run() {
    Rect bounds = new Rect();
    // 获取View2占据的矩形区域在其父View(也就是View1)中的相对坐标
    view2.getHitRect(bounds);
    // 计算扩展后的矩形区域Bounds相对于View1的坐标
    bounds.left -= 100;
    bounds.top -= 50;
    bounds.right += 100;
    bounds.bottom += 50;
    TouchDelegate touchDelegate = new TouchDelegate(bounds, view2);
    // 为View1设置TouchDelegate
    view1.setTouchDelegate(touchDelegate);
    }
    });

    事件处理

    image-20210531100411928

    • 每一个DOWN / MOVE / UP / CANCLE都是一个事件,并不是连起来才是一个事件
    • 事件的消费,是看返回true/false,而不是看有没有处理操作
    • Activity、ViewGroup、View
      • 都有分发、消费事件的能力
      • 只有ViewGroup有拦截事件的能力

    事件分发

    window中的View是树形结构,可能会重叠在一起,当我们点击的区域有多个View都可以响应的时候,事件分发机制决定了这个点击事件应该给谁处理。

    分发机制类似洋葱模型、责任链模式、冒泡...

    分发:Activity -> PhoneWindow -> DecorView -> ViewGroup ->  @1 -> ... -> View
    消费:Activity <- PhoneWindow <- DecorView <- ViewGroup <- @1 <- ... <- View
    • 如果事件被消费,就意味着事件信息传递终止 如果在@1处消费事件,就不在往下传递了,直接返回
    • 如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃事

    image

    View

    优先级:

    1. OnTouchListener.onTouch
    2. onTouchEven

    注意:OnTouchListener.onTouch返回false,并不代表该View不消费事件了,得看dispatchTouchEvent返回的结果

    public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    // 被遮盖,不响应事件
    if (onFilterTouchEventForSecurity(event)) {
    ...
    //setOnTouchListener设置的监听,优先级高
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
    && (mViewFlags & ENABLED_MASK) == ENABLED
    && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
    }

    // 系统已实现好的,优先级低。
    if (!result && onTouchEvent(event)) {
    result = true;
    }
    }
    ...
    return result;
    }

    onTouchEvent:

    • View即使设置了setEnable(false),只要是可点击状态就会消费事件,只是不做出回应
    • 只要进入CLICKABLE判断,就返回true消费时间
    事件处理
    DOWN发送LongClick延迟消息,过期触发
    MOVE移除LongClick消息
    CANCLE移除LongClick消息
    UP移除LongClick消息
    触发Click事件
    <!--只关注事件的分发,不关注其它状态的变化-->
    public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int action = event.getAction();

    //View被禁用的话,如果是可以点击的,一样返回true,表示消费了事件。只是不作出回应。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
    return (((viewFlags & CLICKABLE) == CLICKABLE
    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    // 委托:扩大点击事件、委托其它处理
    if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
    return true;
    }
    }

    /**
    * 只要进入该if,就返回true,消费事件
    */

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
    case MotionEvent.ACTION_DOWN:
    if (isInScrollingContainer) {
    } else {
    //长按事件,发送延时消息到队列
    checkForLongClick(0, x, y);
    }
    break;
    case MotionEvent.ACTION_MOVE:
    if (!pointInView(x, y, mTouchSlop)) {
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
    //移除长按事件的消息。
    removeLongPressCallback();
    setPressed(false);
    }
    }
    break;
    case MotionEvent.ACTION_UP:
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    // 移除长按事件的消息
    removeLongPressCallback();

    //点击事件: 可知onclick事件是在UP的时候触发
    if (!focusTaken) {
    if (!post(mPerformClick)) {
    performClick();
    }
    }
    }
    }
    break;
    case MotionEvent.ACTION_CANCEL:
    //移除长按事件
    removeLongPressCallback();
    mHasPerformedLongPress = false;
    break;
    }
    return true;
    }

    return false;
    }

    ViewGroup

    1. DOWN事件:
      • 清除之前状态,mFirstTouchTarget = null
      • 进入逻辑1、2寻找接收事件的子View
        • mFirstTouchTarget = null,进入逻辑3
        • mFirstTouchTarget != null, 进入逻辑4
    2. MOVE/UP事件:
      • mFirstTouchTarget = null,注释1处不满足逻辑1判断条件,进入逻辑3
      • mFirstTouchTarget != null,不满足逻辑2判断条件,进入逻辑4
    3. CANCLE事件:
      • mFirstTouchTarget = null,注释2处不满足逻辑1判断条件,进入逻辑3
      • mFirstTouchTarget != null,注释2处不满足逻辑1判断条件,进入逻辑4

    总结,

    • DOWN事件就是用来清理状态、寻找新接收事件子View的

    • DOWN事件的后续事件:

      • 未找到子View接收情况下,直接自己处理
      • 找到子View接收的情况下,直接给子View
        @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    ....
    // 如果该View被遮蔽,并且在被遮蔽时不响应点击事件,则不分发该触摸事件,即返回false。
    if (onFilterTouchEventForSecurity(ev)) {
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    /**
    * step1:DOWN事件的时候,表示最初开始事件,清除之前的状态。
    */

    if (actionMasked == MotionEvent.ACTION_DOWN) {
    // 关键:每次DOWN的时候,清除前一个手势的mFirstTouchTarget = null
    cancelAndClearTouchTargets(ev);
    // 清除状态
    resetTouchState();
    }


    /**
    * step2:拦截判断
    */

    final boolean intercepted;
    // ACTION_DOWN(初始状态)或 有子View处理事件:判断是否拦截
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    //默认没有该标记位,返回false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
    //默认返回false,并不是每次都会调用
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action);
    } else {//requestDisallowInterceptTouchEvent(true)
    intercepted = false;
    }
    } else {
    //[注释1],没有子View接收事件,拦截
    intercepted = true;
    }


    /**
    * step3:找能接收事件的子View,并赋值给mFirstTouchTarget
    */

    final boolean canceled = resetCancelNextUpFlag(this)
    || actionMasked == MotionEvent.ACTION_CANCEL; //[注释2]
    // *****每次都会初始化这两个变量****
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    //如果在这一层不满足判断条件,直接就到[逻辑3,4]了。
    //[逻辑1]


    /*
    step4:到这,已经跳出了上面的大嵌套判断!--上面的大嵌套就是用来找接收事件的子View的。
    一旦确定找到了或者没有接收者,后面的事件:
    1. 检查intercepte状态。
    2. 进入下面的逻辑,后面的事件直接确定分发给谁
    */

    // 没有找到接收事件的View,以后的move/up也通过这一步给ViewGroup
    [逻辑3] if (mFirstTouchTarget == null) {
    //没有接收事件的子View,调用自己的dispatchTouchEvent
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
    TouchTarget.ALL_POINTER_IDS);
    [逻辑4] } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    final TouchTarget next = target.next;
    // 在DOWN找到接受事件的子View时,赋值alreadyDispatchedToNewTouchTarget = true
    // 此时已经消费了事件,所以直接返回true
    // 后面的其它事件中,alreadyDispatchedToNewTouchTarget被重置,不在满足该条件
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted;
    // 子View消费事件
    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    }
    }
    }
    }
    return handled;
    }

    Activity

    Touch事件先是传递到Activity,接着由Activity传递到最外层布局,然后一层层遍历循环到View

        public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    // 交互 空实现
    onUserInteraction();
    }
    // DecorView实际是ViewGroup的dispatchTouchEvent方法
    if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
    }
    // down点击到外部区域,消费事件,finish
    return onTouchEvent(ev);
    }

    onUserInteraction()

    这是一个空实现,用的也比较少,不深究: 此方法是activity的方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。

    onTouchEvent(event)

        public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
    finish();
    return true;
    }
    return false;
    }

    mWindow即使PhoneWindow,该方法是@hide,并且在Window类中定义。

        /** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
    && isOutOfBounds(context, event) && peekDecorView() != null) {
    return true;
    }
    return false;
    }
    • mCloseOnTouchOutside是一个boolean变量,它是由Window的android:windowCloseOnTouchOutside属性值决定。
    • isOutOfBounds(context, event)是判断该event的坐标是否在context(对于本文来说就是当前的Activity)之外。是的话,返回true;否则,返回false。
    • peekDecorView()则是返回PhoneWindow的mDecor。

    总的来说:如果设置了android:windowCloseOnTouchOutside为true,并且是DOWN事件点击了Activity外部区域(比如Activity是一个Dialog),返回true,消费事件,并且finish。

    ACTION_CANCEL

    子View在接收事件过程中,被中断,父View会传给子View一个CANCEL事件

     [逻辑4]      } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted; //注释1

    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    //...
    }
    }
    }


    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
    //发送CANCEL事件给子View
    handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
    }
    //...
    }

    ACTION_OUTSIDE

    设置了FLAG_WATCH_OUTSIDE_TOUCH,事件发生在当前视图的范围之外

    例如,点击音量键之外的区域取消音量键显示:

    //frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java        
    // 给音量键Window设置FLAG_WATCH_OUTSIDE_TOUCH
    mDialog = new CustomDialog(mContext);
    mWindow = mDialog.getWindow();
    mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
    | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH //设置Window Flag
    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
    ......

    // 重写onTouchEvent并处理ACTION_OUTSIDE事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (mShowing) {
    if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
    dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
    return true;
    }
    }
    return false;
    }

    事件拦截

    一文解决Android View滑动冲突

    只有ViewGroup有事件拦截的能力,View可根据情况申请父View进行拦截

    image-20210531100411928

    View

    View没有拦截事件的能力,只能根据不同需求调用mParent.requestDisallInterceptTouchEvent(true/false) 申请父View是否进行拦截。

    注意:如果在子View接收事件的过程中被父View拦截,父View会给子View一个CANCEL事件,注意处理相关逻辑。

    ViewGroup

    onInterceptTouchEvent
    • 设置了FLAG_DISALLOW_INTERCEPT标记时,不会调用
    • 其它时候都会调用
        /**
    * ViewGroup事件分发时的拦截检查机制
    */

    //默认没有该标记位,返回false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//注释1
    if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
    intercepted = onInterceptTouchEvent(ev);//默认返回false
    } else {
    intercepted = false;//requestDisallowInterceptTouchEvent(true)
    }


    /**
    * 默认返回false
    */

    public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
    && ev.getAction() == MotionEvent.ACTION_DOWN
    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
    return true;
    }
    return false;
    }

    /*
    * disallowIntercept = true时,不允许拦截,注释1为true
    * disallowIntercept = false时,允许拦截,注释1为false
    */

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    // We're already in this state, assume our ancestors are too
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
    return;
    }

    if (disallowIntercept) {
    mGroupFlags |= FLAG_DISALLOW_INTERCEPT;// 添加标记,使得注释1为true
    } else {
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;// 清除标记,使得注释1为false
    }

    if (mParent != null) {
    mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
    }
    requestDisallowInterceptTouchEvent
    • true,不允许拦截,注释1为true,不会调用onInterceptTouchEvent
    • false,允许拦截,注释1为false(默认),调用onInterceptTouchEvent

    注意:调用requestDisallowInterceptTouchEvent(false)申请拦截,并不会真的就被父View拦截了。它只是一个标记,使得父View会检查onInterceptTouchEvent这个方法(默认也会调用)。 它只会影响 mGroupFlags & FLAG_DISALLOW_INTERCEPT值,真正决定要不要被拦截是看 onInterceptTouchEvent的返回值。如果为true:

    在注释1处cancelChild = true,会导致给子类发送CANCEL事件,然后修改mFirstTouchTarget,不再给子View传递事件。

    [逻辑4]      } else {//找到了接收事件的View
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    } else {
    // 判断是否 收到CANCEL事件 或 需要拦截事件
    final boolean cancelChild = resetCancelNextUpFlag(target.child)
    || intercepted; //注释1
    // 子View消费事件
    //如果cancelChild为true,给子View发送cancle事件
    [逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
    get.child, target.pointerIdBits)) {
    handled = true;
    }
    // 修改mFirstTouchTarget,使原来的子View不再接收事件
    if (cancelChild) {
    if (predecessor == null) {
    mFirstTouchTarget = next;
    } else {
    predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
    }
    }
    }
    }

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
    handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
    }
    //...
    }

    Activity

    Activity没有onInterceptTouchEvent方法,也没有mParent,不具备主动或被动拦截能力

    收起阅读 »

    View系列:动画

    View Animation(视图动画)最大的特点是:并没有改变目标实际的属性(宽高/位置等)。例如:移动后,点击原来的位置出发点击事件;移动后再旋转,还是回到原来的位置旋转。Tween Animation(补间动画)锚点可以是数值、百分数、百分数p三种样式,...
    继续阅读 »

    View Animation(视图动画)

    最大的特点是:并没有改变目标实际的属性(宽高/位置等)。例如:移动后,点击原来的位置出发点击事件;移动后再旋转,还是回到原来的位置旋转。

    Tween Animation(补间动画)

    锚点

    可以是数值、百分数、百分数p三种样式,比如50、50%、50%p。[不是只有pivotx/y才可以用这3中样式,其它变换的属性也可以]

    • 当为数值时,表示在当前View的左上角,即原点处加上50px,做为起始缩放点;
    • 如果是50%,表示在当前控件的左上角加上自己宽度的50%做为起始点;
    • 如果是50%p,那么就是表示在当前的左上角加上父控件宽度的50%做为起始点x轴坐标(是在目标的左上角原点加上相对于父控件宽度的距离,不是锚点在父控件的那个位置)。

    fromX/toX等等类型的数据也可以用上面的3中数据 类型,只不过有的不适合。比如scale用%p就没意义了。养成好习惯,只在锚点的属性上随便用这3中类型,from/to属性分清类型用相应的数值(浮点倍数/角度...)。

    从Animation继承的属性
    android:duration 动画持续时间,以毫秒为单位 
    android:fillAfter 如果设置为true,控件动画结束时,将保持动画最后时的状态
    android:fillBefore 如果设置为true,控件动画结束时,还原到开始动画前的状态
    android:fillEnabled 与android:fillBefore 效果相同,都是在动画结束时,将控件还原到初始化状态
    android:repeatCount 重复次数
    android:repeatMode 重复类型,有reverse和restart两个值,reverse表示倒序回放,restart表示重新放一遍,必须与repeatCount一起使用才能看到效果。因为这里的意义是重复的类型,即回放时的动作。
    android:interpolator 设定插值器,其实就是指定的动作效果,比如弹跳效果等,不在这小节中讲解,后面会单独列出一单讲解。
    scale
    <?xml version="1.0" encoding="utf-8"?>
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:duration="700"
    android:fromXScale="50%" //也可以用上面的3中类型
    android:fromYScale="50%"
    android:toXScale="200%"
    android:toYScale="200%"
    android:pivotX="0.5"
    android:pivotY="0.5"
    android:repeatCount = "2"
    android:repeatMode = "reverse"
    android:fillAfter = "true"
    />

    alpha
    <?xml version="1.0" encoding="utf-8"?>
    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:fromAlpha="0.1"
    android:toAlpha="1"
    android:duration="1500"
    android:repeatMode = "reverse"
    android:repeatCount = "2"
    android:fillAfter = "true"
    >

    </alpha>
    rotate
    <?xml version="1.0" encoding="utf-8"?>
    <rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:fromDegrees="0"
    android:toDegrees="270"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="700"
    android:repeatMode = "reverse"
    android:repeatCount = "3"
    android:fillAfter = "true"
    >

    </rotate>
    translate
    <?xml version="1.0" encoding="utf-8"?>
    <translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:duration="700"
    android:fillAfter="true"
    android:fromXDelta="50"
    android:fromYDelta="50%p"
    android:repeatCount="3"
    android:repeatMode="reverse"
    android:toXDelta="70%p"
    android:toYDelta="80%p">

    </translate>
    AnimationSet animSet = new AnimationSet(false);
    Animation scaleAnim = AnimationUtils.loadAnimation(this, R.anim.scale_anim); //资源文件
    Animation rotateAnim = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
    AlphaAnimation alphaAnim = new AlphaAnimation(0.2f, 1.0f); //代码生成
    //valueType 3中类型的数据(px, 自身%, 父类%p),这里已自身为参照物。
    TranslateAnimation traslateAnim = new TranslateAnimation(
    Animation.RELATIVE_TO_SELF, 0.2f,
    Animation.RELATIVE_TO_SELF, 3.0f,
    Animation.RELATIVE_TO_SELF, 0f,
    Animation.RELATIVE_TO_SELF, 1.0f);
    ivTarget.startAnimation(animSet);
    自定义Animation
    private class MoveAnimation extends Animation {
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
    super.applyTransformation(interpolatedTime, t);
    mInterpolatedTime = interpolatedTime;
    invalidate();
    }
    }

    Frame Animation(逐帧动画)

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    //false 一直重复执行,true执行一次。
    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_1"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_2"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_3"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_4"/>

    <item
    android:duration="200"
    android:drawable="@drawable/frame_anim_4"/>

    </animation-list>
    • 需要注意的是,动画的启动需要在view和window建立连接后才可以绘制,比如上面代码是在用户触摸后启动。如果我们需要打开界面就启动动画的话,则可以在Activity的onWindowFocusChanged()方法中启动。

    Property Animation(属性动画)

    属性动画是指通过改变View属性来实现动画效果,包括:ValueAnimator、ObjectAnimator、TimeAnimator

    ValueAnimator

    该类主要针对数值进行改变,不对View进行操作

    ValueAnimator animator = ValueAnimator.ofInt(0,400);  
    animator.setDuration(1000);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    //拿到监听结果,自己处理。
    int curValue = (int)animation.getAnimatedValue();
    tvTextView.layout(curValue,curValue,curValue+tv.getWidth(),curValue+tv.getHeight());
    }
    });
    animator.setInterpolator(new LinearInterpolator());
    animator.start();

    监听:

    /**
    * 监听器一:监听动画变化时的实时值
    * 添加方法为:public void addUpdateListener(AnimatorUpdateListener listener)
    */

    public static interface AnimatorUpdateListener {
    void onAnimationUpdate(ValueAnimator animation);
    }
    /**
    * 监听器二:监听动画变化时四个状态
    * 添加方法为: public void addListener(AnimatorListener listener)
    */

    public static interface AnimatorListener {
    void onAnimationStart(Animator animation);
    void onAnimationEnd(Animator animation);
    void onAnimationCancel(Animator animation);
    void onAnimationRepeat(Animator animation);
    }


    /**
    * 移除AnimatorUpdateListener
    */

    void removeUpdateListener(AnimatorUpdateListener listener);
    void removeAllUpdateListeners();
    /**
    * 移除AnimatorListener
    */

    void removeListener(AnimatorListener listener);
    void removeAllListeners();

    ObjectAnimator

    ValueAnimator只能对数值进行计算,不能直接操作View,需要我们在监听器中自己去操作控件。这样就有点麻烦了,于是Google在ValueAmimator的基础上又派生出了ObjerctAnimator类,让动画直接与控件关联起来。

     	ObjectAnimator rotateObject = ObjectAnimator.ofFloat(tvPropertyTarget, 
    "Rotation",
    0, 20, -20, 40, -40, 0);
    rotateObject.setDuration(2000);
    rotateObject.start();
    setter/getter 属性名

    在View中已经实现了一些属性的setter/getter方法,在构造动画时可以直接对控件使用。

    • 要使用一个属性,必须在控件中有对应的setter/getter方法,属性setter/getter方法的命名必须以驼峰方式
    • ObjectAnimator在使用该属性的时候,会把setter/getter和属性第一个字母大写转换后的字段拼接成方法名,通过反射的方式调用该方法传值。 所以,上文中"Rotation/rotation"可以首字母可以大小写都行
    //1、透明度:alpha  
    public void setAlpha(float alpha)

    //2、旋转度数:rotation、rotationX、rotationY
    public void setRotation(float rotation) //围绕Z轴旋转
    public void setRotationX(float rotationX)
    public void setRotationY(float rotationY)

    //3、平移:translationX、translationY
    public void setTranslationX(float translationX)
    public void setTranslationY(float translationY)

    //缩放:scaleX、scaleY
    public void setScaleX(float scaleX)
    public void setScaleY(float scaleY)

    image-20210603130556023

    自定义属性做动画
    public class PointView extends View {
    private float mRadius = 0;
    public PointView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
    }

    public void setRadius(float radius){
    this.mRadius = radius;
    invalidate();
    }

    public float getRadius(){
    return mRadius;
    }
    }

    //radius属性首字母大小写无所谓,最后都是要转成大些的。
    ObjectAnimator pointAnim = ObjectAnimator.ofFloat(pointPropertyAnim,
    "Radius",
    10, 40, 40, 80, 60, 100, 80, 120,60);
    pointAnim.start();

    什么时候需要用到get方法呢? 前面构造动画时传入的取值范围都是多个参数,Animator知道是从哪个值变化到哪个值。当只传入一个参数的时候,Animator怎么知道哪里是起点?这时通过get方法找到初始值。 如果没有找到get方法,会用该参数类型的默认初始值复制。如:ofInt方法传入一个值,找不到get方法时,默认给的初始值是Int类型的初始值0.

    原理

    image-20210603131108900ObjectAnimator的方便之处在于:

    ValueAnimator只负责把数值给监听器,ObjectAnimator只负责调用set方法。至于实现,都是靠我们自己或者set中的方法。

    插值器

    设置动画运行过程中的进度比例,类似匀速变化、加速变化、回弹等

    • 参数input:是一个float类型,它取值范围是0到1,表示当前动画的进度,取0时表示动画刚开始,取1时表示动画结束,取0.5时表示动画中间的位置,其它类推。
    • 返回值:表示当前实际想要显示的进度。取值可以超过1也可以小于0,超过1表示已经超过目标值,小于0表示小于开始位置。(给估值器使用
    • 插值器默认每10ms刷新一次
    public class PointInterpolator implements Interpolator {
    /**
    * input 是实际动画执行的时间比例 0~1
    * newInput 你想让动画已经执行的比例 0~1。
    * 注意:都是比例,而不是实际的值。
    *
    * setDuration(1000)情况下:前200ms走了3/4的路程比例,后800ms走了1/4的路程比例。
    */

    @Override
    public float getInterpolation(float input) {
    if (input <= 0.2) {//后1/4的时间,输出3/4的比例
    float newInput = input*4;
    return newInput;
    }else {//后3/4的时间,输出1/4的比例
    float newInput = (float) (input - 0.2)/4 + 0.8f;
    return newInput;
    }
    }
    }

    使用方式和默认插值器

    在xml和代码中使用插值器,省略代码中使用方式

    <?xml version="1.0" encoding="utf-8"?>
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
    // 通过资源ID设置插值器
    android:interpolator="@android:anim/overshoot_interpolator"
    android:duration="3000"
    android:fromXScale="0.0"
    android:fromYScale="0.0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toXScale="2"
    android:toYScale="2" />

    内置插值器动画展示

    Android动画之Interpolator

    Android动画插值器

    作用资源ID对应的Java类
    动画加速进行@android:anim/accelerte_interpolatorAcceleraterplator
    快速完成动画,超出再回到到结束样式@android:anim/overshoot_interpolatorOvershootInterpolator
    先加速再减速@android:anim/accelerate_decelerate_interpolatorAccelerateDecelerateInterpolator
    先退后再加速前进@android:anim/anticipate_interpolatorAnticipateInterpolator
    先退后再加速前进,超出终点后再回终点@android:anim/anticipate_overshoot_interpolatorAnticipateOvershootInterpolator
    最后阶段弹球效果@android:anim/bounce_interpolatorBounceInterpolator
    周期运动@android:anim/cycle_interpolatorCycleInterpolator
    减速@android:anim/decelerate_interpolatorDecelerateInterpolator
    匀速@android:anim/linear_interpolatorLinearInterpolator

    估值器

    设置 属性值 从初始值过渡到结束值 的变化具体数值

    • 参数fraction: 表示当前动画的进度(插值器返回值
    • 返回值:表示当前对应类型的取值,也就是UpdateListener接口方法中传入的值
    public class PointEvaluator implements TypeEvaluator<Point> {
    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
    int radius = (int) (startValue.getRadius() +
    fraction*(endValue.getRadius() - startValue.getRadius()));
    return new Point(radius);
    }
    }

    自定义插值器、估值器、属性的使用:

    public void doAnimation(){
    //ObjectAnimator animator = ObjectAnimator.ofInt(mView, "Radius", 20, 80);
    ValueAnimator animatior = new ValueAnimator();
    animatior.setObjectValues(new Point(20), new Point(80));
    animatior.setInterpolator(new PointInterpolator());
    animatior.setEvaluator(new PointEvaluator());

    animatior.setDuration(2000);
    animatior.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    mPoint = (Point) animation.getAnimatedValue();
    invalidate();
    }
    });
    animatior.start();
    }

    PropertyValuesHolder

    它其中保存了动画过程中所需要操作的属性和对应的值

    通过ObjectAnimator.ofFloat(Object target, String propertyName, float… values)构造的动画,ofFloat()的内部实现其实就是将传进来的参数封装成PropertyValuesHolder实例来保存动画状态,后期的各种操作也是以PropertyValuesHolder为主。

    //将需要操作的多个属性和值封装起来,一起放到ObjectAnimator中,相当于set操作。
    PropertyValuesHolder rotateHolder = PropertyValuesHolder.ofFloat("Rotation", 0, 360, 0);
    PropertyValuesHolder scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", 1, 2, 1,2,1);
    PropertyValuesHolder scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", 1, 2, 1,2,1);
    ObjectAnimator objectAnim = ObjectAnimator.ofPropertyValuesHolder(ivHolderTarget,
    rotateHolder,
    scaleXHolder,
    scaleYHolder);
    objectAnim.setDuration(2000);
    objectAnim.setInterpolator(new LinearInterpolator());
    objectAnim.start();

    KeyFrame(主要帧)

    如果想要更精确的控制动画,想要控制整个动画过程的某个点或某个时段达到的值,可以通过自定义插值器或估值器来实现,但是那样又有些费事,并且不容易计算这段时间内值的变化。 这时可以用Keyframe来实现,即设置好某个时间点和值,系统会自动计算该点和上个点之间,值的变化。

    /***
    * 实现左右摇晃,每边最后有震动的效果。
    * 摇晃角度100度:0.2f/0.2~0.4/0.4~0.5,分别设置不同的角度和加速器。
    * 每个比例点达到哪个角度,这在估值器中也能做到,但是需要自己算每个时间段内值的变化过程。
    * KeyFrame可以设置好 比例-值 以后,系统根据默认或设置的加速器改变:上个点和该点内的值如何变换。
    * 这样可以更精确的控制动画过程,同时也不用自己费劲去计算值因该如何变换。
    */

    Keyframe kfRotation1 = Keyframe.ofFloat(0, 0); //第一帧,如果没有该帧,会直接跳到第二帧开始动画。
    //第二帧 0.2f时达到60度,线性加速应该作用于从0~0.2f的这段时间,而不是作用在0.2~0.4f这段。因为已经定好60度是要的结果了,那么实现就应该在前面这段。
    Keyframe kfRotation2 = Keyframe.ofFloat(0.2f, 60);
    kfRotation2.setInterpolator(new LinearInterpolator());
    Keyframe kfRotation3 = Keyframe.ofFloat(0.4f, 100);
    kfRotation3.setInterpolator(new BounceInterpolator());
    Keyframe kfRotation4 = Keyframe.ofFloat(0.5f, 0);
    kfRotation4.setInterpolator(new LinearInterpolator()); //最少有2帧
    Keyframe kfRotation5 = Keyframe.ofFloat(0.7f, -60);
    kfRotation5.setInterpolator(new LinearInterpolator());
    Keyframe kfRotation6 = Keyframe.ofFloat(0.9f, -100);
    kfRotation6.setInterpolator(new BounceInterpolator());
    Keyframe kfRotation7 = Keyframe.ofFloat(1f, 0);//最后一帧,如果没有该帧,会以最后一个KeyFrame做结尾
    kfRotation7.setInterpolator(new LinearInterpolator());

    Keyframe kfScaleX1 = Keyframe.ofFloat(0, 1);
    Keyframe kfScaleX2 = Keyframe.ofFloat(0.01f,2.8f);
    Keyframe kfScaleX3 = Keyframe.ofFloat(0.8f,2.0f);
    Keyframe kfScaleX4 = Keyframe.ofFloat(1f,1.0f);

    Keyframe kfScaleY1 = Keyframe.ofFloat(0, 1);
    Keyframe kfScaleY2 = Keyframe.ofFloat(0.01f,2.8f);
    Keyframe kfScaleY4 = Keyframe.ofFloat(0.8f,2.0f);
    Keyframe kfScaleY5 = Keyframe.ofFloat(1f,1.0f);

    PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofKeyframe("rotation", kfRotation1, kfRotation2, kfRotation3,kfRotation4, kfRotation5, kfRotation6, kfRotation7);
    PropertyValuesHolder scaleXHolder = PropertyValuesHolder.ofKeyframe("scaleX", kfScaleX1, kfScaleX2, kfScaleX3, kfScaleX4);
    PropertyValuesHolder scaleYHolder = PropertyValuesHolder.ofKeyframe("scaleY", kfScaleY1, kfScaleY2, kfScaleY4, kfScaleY5);


    ObjectAnimator objectAnim = ObjectAnimator.ofPropertyValuesHolder(ivHolderTarget,
    rotationHolder,
    scaleXHolder,
    scaleYHolder);
    objectAnim.setDuration(1500);

    AnimatorSet

    AnimatorSet针对ValueAnimator和ObjectAnimator都是适用的,但一般而言,我们不会用到ValueAnimator的组合动画。

    playTogether/playSequentially

    无论是playTogether还是playSequentially方法,它们只是,仅仅是激活了动画什么时候开始,并不参与动画的具体操作。 例如:如果是playTogether,它只负责这个动画什么时候一起激活,至于anim1/anim2/anim3...哪个马上开始,哪个有延迟,哪个会无限重复,set都不管,只负责一起激活。 如果是playSequentially,它只负责什么时候开始激活第一个(因为有可能set设置延迟),并在第一个动画结束的时候,激活第二个,以此类推。

    ObjectAnimator anim1 = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);

    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    anima2.setStartDelay(2000);
    anima2.setRepeatCount(ValueAnimator.INFINITE);

    ObjectAnimator anim3 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim3.setStartDelay(2000);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.playTogether(anim1, anim2, anim3);//playSequentially(按次序播放)
    animatorSet.setDuration(2000);
    animatorSet.setStartDelay(2000);
    animatorSet.start();
    play(x).with(x)
    • play(anim1).with(anim2):2000ms后set开始激活动画,anim1启动,再过2000ms后anim2启动。
    • play(anim2).with(anim1):2000ms后set开始激活动画,再过2000ms后启动anim2,并且启动anim1.
    set监听

    addListener监听的是AnimatorSet的start/end/cacle/repeat。不会监听anim1/anim2的动画状态的。

    联合动画XML实现
    单独设置和Set中设置
    • 以set为准:
    //设置单次动画时长
    public AnimatorSet setDuration(long duration);
    //设置加速器
    public void setInterpolator(TimeInterpolator interpolator)
    //设置ObjectAnimator动画目标控件
    public void setTarget(Object target)
    ObjectAnimator anim1 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    anim1.setDuration(500000000);

    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim2.setDuration(3000);//每次3000,而不是3次3000ms
    anim2.setRepeatCount(3);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.play(tv2TranslateY).with(tv1TranslateY);
    animatorSet.setDuration(2000);//以Set为准
    animatorSet.start();

    setDuration()是指单个动画的时间,并不是指总共做完这个动画过程的时间。比如:anim2中设置了3000ms,重复3次。是指每次3000ms,不是3次3000ms。
    另外animatorSet设置了时间以后,anim1/anim2虽然也设置了,但是这时以set为准。即,anim1/anim2的单个动画时间为2000ms。只不过anim2是每次2000ms,重复3次,共6000ms。

    • 不以set为准:setStartDelay
    ObjectAnimator anim1 = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);
    ObjectAnimator anim2 = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);
    anim2.setStartDelay(2000);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.addListener(new Animator.AnimatorListener(){...});
    animatorSet.play(anim1).with(anim2);
    animatorSet.setStartDelay(3000);//指的是Set的激活延迟,而不是动画延迟
    animatorSet.setDuration(2000);
    animatorSet.start();

    setStartDelay不会覆盖单个动画的该方法,只会延长set的激活时间。所以,上面代码中动画的启动过程是:3000ms后set开始激活动画,anim1启动,再过2000ms后anim2启动。

    ViewPropertyAnimator

    属性动画已不再是针对于View而进行设计的了,而是一种对数值不断操作的过程,我们将属性动画对数值的操作过程设置到指定对象的属性上来,从而形成一种动画的效果。 虽然属性动画给我们提供了ValueAnimator类和ObjectAnimator类,在正常情况下,基本都能满足我们对动画操作的需求,但ValueAnimator类和ObjectAnimator类本身并不是针对View对象的而设计的,而我们在大多数情况下主要都还是对View进行动画操作的。

    因此Google官方在Android 3.1系统中补充了ViewPropertyAnimator类,这个类便是专门为View动画而设计的。

    • 专门针对View对象动画而操作的类
    • 更简洁的链式调用设置多个属性动画,这些动画可以同时进行
    • 拥有更好的性能,多个属性动画是一次同时变化,只执行一次UI刷新(也就是只调用一次invalidate,而n个ObjectAnimator就会进行n次属性变化,就有n次invalidate)
    • 每个属性提供两种类型方法设置。scaleX()/scaleXBy()
    • 该类只能通过View的animate()获取其实例对象的引用
    • 自动调用start
    btn.animate()
    .alpha(0.5f)
    .rotation(360)
    .scaleX(1.5f).scaleY(1.5f)
    .translationX(50).translationY(50)
    .setDuration(5000);

    image-20210604100429893

    layoutAnimation

    布局动画,api1,该属性只对创建ViewGroup时,对其子View有动画。已经创建过了该ViewGroup的话,再向其添加子View不会有动画。

    • onCreat创建加载布局时:
    //anim -> rotate_anim.xml
    <?xml version="1.0" encoding="utf-8"?>


    // layoutAnimation标签
    <?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="1"
    android:animationOrder="normal"
    android:animation="@anim/rotate_anim">

    </layoutAnimation>

    //定义在LinearLayout上,在该界面生成时,Button显示动画。但是,后面在LinearLayout中添加Button时,不再有动画。
    <LinearLayout
    android:id="@+id/ll_tips_target_animation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layoutAnimation="@anim/layout_animation"
    android:tag="在xml中设置的layoutAnimation"
    android:orientation="vertical">

    <Button
    style="@style/base_button"
    android:text="ViewGroup初始化时,子View有动画"/>

    </LinearLayout>
    • 代码中动态设置layoutAnimation,添加View
            //代码生成ViewGroup
    LinearLayout linear = new LinearLayout(this);

    Animation animation = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
    LayoutAnimationController controller = new LayoutAnimationController(animation);
    controller.setDelay(1);
    //动画模式,正常/倒叙/随机
    controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
    //设置layoutAnimation
    linear.setLayoutAnimation(controller);
    linear.setLayoutAnimationListener(new Animation.AnimationListener() {

    });

    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
    LinearLayout.LayoutParams.MATCH_PARENT,
    LinearLayout.LayoutParams.WRAP_CONTENT);
    linear.setLayoutParams(params);
    //给该ViewGroup添加子View,子View会有动画。
    addVeiw(linear,null);
    llTargetAnim.addView(linear, 0);

    使用场景:

    该属性只有ViewGroup创建的时候才能有效果,所以不适合动态添加子View的操作显示动画。一般做界面显示的时候的入场动画,比如打开一个界面,多个固定不变的item有动画的显示出来。(进入设置界面,信息展示界面)。

    android:animateLayoutChanges属性:

    Api11后,添加/移除子View时所带的默认动画,在Xml中设置。不能自定义动画,只能使用默认的。所以,使用范围较小。

    <LinearLayout
    android:animateLayoutChanges="true"
    />

    image


    收起阅读 »

    「Java 路线」| 关于泛型能问的都在这里了(含Kotlin)

    前言 泛型(Generic Type) 无论在哪一门语言里,都是最难语法的存在,细节之繁杂、理解之困难,令人切齿; 在这个系列里,我将总结Java & Kotlin中泛型的知识点,带你从 语法 & 原理 全面理解泛型。追求简单易懂又...
    继续阅读 »

    前言



    • 泛型(Generic Type) 无论在哪一门语言里,都是最难语法的存在,细节之繁杂、理解之困难,令人切齿;

    • 在这个系列里,我将总结Java & Kotlin中泛型的知识点,带你从 语法 & 原理 全面理解泛型。追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!

    • 首先,尝试回答这些面试中容易出现的问题,相信看完这篇文章,这些题目都难不倒你:


    1、下列代码中,编译出错的是:
    public class MyClass<T> {
    private T t0; // 0
    private static T t1; // 1
    private T func0(T t) { return t; } // 2
    private static T func1(T t) { return t; } // 3
    private static <T> T func2(T t) { return t; } // 4
    }
    2、泛型的存在是用来解决什么问题?
    3、请说明泛型的原理,什么是泛型擦除机制,具体是怎样实现的?



    目录





    1. 泛型基础




    • 问:什么是泛型,有什么作用?


    答:在定义类、接口和方法时,可以附带类型参数,使其变成泛型类、泛型接口和泛型方法。与非泛型代码相比,使用泛型有三大优点:更健壮(在编译时进行更强的类型检查)、更简洁(消除强转,编译后自动会增加强转)、更通用(代码可适用于多种类型)



    • 问:什么是类型擦除机制?


    答:泛型本质上是 Javac 编译器的一颗 语法糖,这是因为:泛型是 JDK1.5 中引进的新特性,为了 向下兼容,Java 虚拟机和 Class 文件并没有提供泛型的支持,而是让编译器擦除 Code 属性中所有的泛型信息,需要注意的是,泛型信息会保留在类常量池的属性中。



    • 问:类型擦除的具体步骤?


    答:类型擦除发生在编译时,具体分为以下 3 个步骤:



    • 1:擦除所有类型参数信息,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object

    • 2:(必要时)插入类型转换,以保持类型安全

    • 3:(必要时)生成桥接方法以在子类中保留多态性


    举个例子:


    源码:
    public class Parent<T> {
    public void func(T t){
    }
    }

    public class Child<T extends Number> extends Parent<T> {
    public T get() {
    return null;
    }
    public void func(T t){
    }
    }

    void test(){
    Child<Integer> child = new Child<>();
    Integer i = child.get();
    }
    ---------------------------------------------------------
    字节码:
    public class Parent {
    public void func(Object t){
    }
    }

    public class Child extends Parent {
    public Number get() {
    return null;
    }
    public void func(Number t) {
    }

    桥方法 - synthetic
    public void func(Object t){
    func((Number)t);
    }
    }

    void test() {
    Child<Integer> child = new Child();
    // 插入强制类型转换
    Integer i = (Integer) child.get();
    }

    步骤1:Parent 中的类型参数 T 被擦除为 Object,而 Child 中的类型参数 T 被擦除为 Number;


    步骤2:child.get(); 插入了强制类型转换


    步骤3:在 Child 中生成桥方法,桥方法是编译器生成的,所以会带有 synthetic 标志位。为什么子类中需要增加桥方法呢,可以先思考这个问题:假如没有桥方法,会怎么样?你可以看看下列代码调用的是子类还是父类方法:


    Parent<Integer> child = new Child<>();
    Parent<Integer> parent = new Parent<>();

    child.func(1); // Parent#func(Object);
    parent.func(1); // Parent#func(Object);

    这两句代码都会调用到 Parent#func(),如果你看过之前我写过的一篇文章,相信难不到你:《Java | 深入理解方法调用的本质(含重载与重写区别)》。在这里我简单分析下:



    1、方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)


    2、这两句代码调用的方法符号引用为:


    child.func(new Object()) => com/xurui/Child.func(Object)


    parent.func(new Object()) => com/xurui/Parent.func(Object)


    3、这两句方法调用的字节码指令为 invokevirtual


    4、类加载解析阶段解析类的继承关系,生成类的虚方法表


    5、调用阶段(动态分派):Child 没有重写 func(Object),所以 Child 的虚方法表中存储的是Parent#func(Object);Parent 的虚方法表中存储的是Parent#func(Object);



    可以看到,即使使用对象的实际类型为 Child ,这里调用的依旧是父类的方法。这样就 失去了多态性。 因此,才需要在泛型子类中添加桥方法。



    • 问:为什么擦除后,反编译还是看到类型参数 T ?


    反编译Parent.class,可以看到 T ,不是已经擦除了吗?

    public class Parent<T> {
    public Parent() {
    }

    public void func(T t) {
    }
    }

    答:泛型中所谓的类型擦除,其实只是擦除Code 属性中的泛型信息,在类常量池属性(Signature 属性、LocalVariableTypeTable 属性)中其实还保留着泛型信息,这也是在运行时可以反射获取泛型信息的根本依据,我在第 4 节说。



    • 问:泛型的限制 & 类型擦除会带来什么影响?


    由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。为了避免程序的运行结果与程序员语义不一致的情况,泛型在使用上存在一些限制。好处是类型擦除不会为每种参数化类型创建新的类,因此泛型不会增大内存消耗。


    泛型的限制




    2. Kotlin的实化类型参数


    前面我们提到,由于类型擦除的影响,在运行期是不清楚类型实参的实际类型的。例如下面的代码是不合法的,因为T并不是一个真正的类型,而仅仅是一个符号:


    在这个函数里,我们传入一个List,企图从中过滤出 T 类型的元素:

    Java:
    <T> List<T> filter(List list) {
    List<T> result = new ArrayList<>();
    for (Object e : list) {
    if (e instanceof T) { // compiler error
    result.add(e);
    }
    }
    return result;
    }
    ---------------------------------------------------
    Kotlin:
    fun <T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
    if (e is T) { // cannot check for instance of erased type: T
    result.add(e)
    }
    }
    return result
    }

    Kotlin中,有一种方法可以突破这种限制,即:带实化类型参数的内联函数


    Kotlin:
    inline fun <reified T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
    if (e is T) {
    result.add(e)
    }
    }
    return result
    }

    关键在于inlinereified,这两者的语义是:



    • inline(内联函数): Kotlin编译器将内联函数的字节码插入到每一次调用方法的地方

    • reified(实化类型参数): 在插入的字节码中,使用类型实参的确切类型代替类型实参


    规则很好理解,对吧。很明显,当发生方法内联时,方法体字节码就变成了:


    调用:
    val list = listOf("", 1, false)
    val strList = filter<String>(list)
    ---------------------------------------------------
    内联后:
    val result = ArrayList<String>()
    for (e in list) {
    if (e is String) {
    result.add(e)
    }
    }

    需要注意的是,内联函数整个方法体字节码会被插入到调用位置,因此控制内联函数体的大小。如果函数体过大,应该将不依赖于T的代码抽取到单独的非内联函数中。



    注意,无法从 Java 代码里调用带实化类型参数的内联函数



    实化类型参数的另一个妙用是代替 Class 对象引用,例如:


    fun Context.startActivity(clazz: Class<*>) {
    Intent(this, clazz).apply {
    startActivity(this)
    }
    }

    inline fun <reified T> Context.startActivity() {
    Intent(this, T::class.java).apply {
    startActivity(this)
    }
    }

    调用方:
    context.startActivity(MainActivity::class.java)
    context.startActivity<MainActivity>() // 第二种方式会简化一些



    3. 变型:协变 & 逆变 & 不变


    变型(Variant)描述的是相同原始类型的不同参数化类型之间的关系。说起来有点绕,其实就是说:IntegerNumber的子类型,问你List<Integer>是不是List<Number>的子类型?


    变型的种类具体分为三种:协变型 & 逆变型 & 不变型



    • 协变型(covariant): 子类型关系被保留

    • 逆变型(contravariant): 子类型关系被翻转

    • 不变型(invariant): 子类型关系被消除


    在 Java 中,类型参数默认是不变型的,例如:


    List<Number> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // compiler error

    相比之下,数组是支持协变型的:


    Number[] nums;
    Integer[] ints = new Integer[10];
    nums = ints; // OK 协变,子类型关系被保留

    那么,当我们需要将List<Integer>类型的对象,赋值给List<Number>类型的引用时,应该怎么做呢?这个时候我们需要限定通配符



    • <? extends> 上界通配符


    要想类型参数支持协变,需要使用上界通配符,例如:


    List<? extends Number> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK

    但是这会引入一个编译时限制:不能调用参数包含类型参数 E 的方法,也不能设置类型参数的字段,简单来说,就是只能访问不能修改(非严格):


    // ArrayList.java
    public boolean add(E e) {
    ...
    }

    l1.add(1); // compiler error


    • <? super> 下界通配符


    要想类型参数支持逆变,需要使用下界通配符,例如:


    List<? super Integer> l1;
    List<Number> l2 = new ArrayList<>();
    l1 = l2; // OK

    同样,这也会引入一个编译时限制,但是与协变相反:不能调用返回值为类型参数的方法,也不能访问类型参数的字段,简单来说,就是只能修改不能访问(非严格):


    // ArrayList.java
    public E get(int index) {
    ...
    }

    Integer i = l1.get(0); // compiler error


    • <?> 无界通配符

    其实很简单,很多资料其实都解释得过于复杂了。 < ?> 其实就是 的缩写。例如:
    List<?> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK

    理解了这点,这个问题就很好回答了:



    • 问:List 与 List<?>有什么区别?


    答:List 是原生类型,可以添加或访问元素,不具备编译期安全性,而 List 其实是 List的缩写,是协变型的(可引出协变型的特点与限制);从语义上,List 表明使用者清楚变量是类型安全的,而不是因为疏忽而使用了原生类型 List。



    泛型代码的设计,应遵循PECS原则(Producer extends Consumer super):



    • 如果只需要获取元素,使用 <? extends T>

    • 如果只需要存储,使用<? super T>


    举例:


    // Collections.java public static void copy(List<? super T> dest, List<? extends T> src) { }



    在 Kotlin 中,变型写法会有些不同,但是语义是完全一样的:


    协变:
    val l0: MutableList<*> 相当于MutableList<out Any?>
    val l1: MutableList<out Number>
    val l2 = ArrayList<Int>()
    l0 = l2 // OK
    l1 = l2 // OK
    ---------------------------------------------------
    逆变:
    val l1: MutableList<in Int>
    val l2 = ArrayList<Number>()
    l1 = l2 // OK

    另外,Kotlin 的in & out不仅仅可以用在类型实参上,还可以用在泛型类型声明的类型参数上。其实这是一种简便写法,表示类设计者知道类型参数在整个类上只能协变或逆变,避免在每个使用的地方增加,例如 Kotlin 的List被设计为不可修改的协变型:


    public interface List<out E> : Collection<E> {
    ...
    }


    注意:在 Java 中,只支持使用点变型,不支持 Kotlin 类似的声明点变型



    小结一下:





    4. 使用反射获取泛型信息


    前面提到了,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。


    获取泛型类型实参:需要利用Type体系


    4.1 获取泛型类 & 泛型接口声明


    TypeVariable ParameterizedType GenericArrayType WildcardType


    Gson TypeToken


    Editting....




    5. 总结



    • 应试建议

      • 1、第 1 节非常非常重点,着重记忆:泛型的本质和设计缘由、泛型擦除的三个步骤、限制和优点,已经总结得很精华了,希望能帮到你;

      • 2、着重理解变型(Variant)的概念,以及各种限定符的含义;

      • 3、Kotlin 相关的部分,作为知识积累和思路扩展为主,非应试重点。







    作者:彭丑丑
    链接:https://juejin.cn/post/6888345234653052941
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    「Java 路线」| 反射机制(含 Kotlin)

    前言 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注! 目录 1. 类型系统...
    继续阅读 »

    前言



    • 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。

    • 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!




    目录



    1. 类型系统的基本概念


    首先,梳理一一下类型系统的基础概念:



    • 问:什么是强 / 弱类型语言?


    答:强 / 弱类型语言的区分,关键在于变量是否 (倾向于) 类型兼容。例如,Java 是强类型语言,变量有固定的类型,以下代码在 Java 中是非法的:


    public class MyRunnable {
    public abstract void run();
    }

    // 编译错误:Incompatible types
    java.lang.Runnable runnable = new MyRunnable() {
    @Override
    public void run() {

    }
    }
    runnable.run(); // X

    相对地,JavaScript 是弱类型语言,一个变量没有固定的类型,允许接收不同类型的值:


    function MyRunnable(){
    this.run = function(){
    }
    }
    function Runnable(){
    this.run = function(){
    }
    }
    var ss = new MyRunnable();
    ss.run(); // 只要对象有相同方法签名的方法即可
    ss = new Runnable();
    ss.run();

    更具体地描述,Java的强类型特性体现为:变量仅允许接收相同类型或子类型的值。 嗯(黑人问号脸)?和你的理解一致吗?请看下面代码,哪一行是有问题的:


    注意,请读者假设 1 ~ 4 号代码是单独运行的

    long numL = 1L;
    int numI = 0;
    numL = numI; // 1
    numI = (int)numL; // 2

    Integer integer = new Integer(0);
    Object obj = new Object();
    integer = (Integer) obj; // 3 ClassCastException
    obj = integer; // 4

    在这里,第 3 句代码会发生运行时异常,结论:



    • 1:调用字节码指令 i2l,将 int 值转换为 long 值。(此时,numL 变量接收的是相同类型的值,命题正确)


    • 2:调用字节码指令 l2i,将 long 值转换为 int 值。(此时,numI 变量接收的是相同类型的值,命题正确)


    • 3:调用字节码指令 checkcast,发现 obj 变量的值不是 Integer 类型,抛出 ClassCastException。(此时,Integer 变量不允许接收 Object 对象,命题正确)


    • 4:integer 变量的值是 obj 变量的子类型,可以接收。(此时,Object 变量允许接收 Integer 对象,命题正确)



    用一张图概括一下:






    • 问:什么是静态 / 动态类型语言?


    答:静态 / 动态类型语言的区分,关键在于类型检查是否 (倾向于) 编译时执行。例如, Java & C/C++ 是静态类型语言,而 JavaScript 是动态类型语言。需要注意的是,这个定义并不是绝对的,例如 Java 也存在运行时类型检查的方式,例如上面提到的 checkcast 指令本质上是在运行时检查变量的类型与对象的类型是否相同。 那么 Java 是如何在运行时获得类型信息的呢?这就是我们下一节要讨论的问题。




    2. 反射的基本概念



    • 问:什么是反射?为什么要使用反射?


    答:反射(Reflection)是一种在运行时 动态访问类型信息 的机制。Java 是静态强类型语言,它倾向于在编译时进行类型检查,因此当我们访问一个类时,它必须是编译期已知的,而使用反射机制可以解除这种限制,赋予 Java 语言动态类型的特性。例如:


    void func(Object obj) {
    try {
    Method method = obj.getClass().getMethod("run",null);
    method.invoke(obj,null);
    }
    ... 省略 catch
    }
    func(runnable); 调用 Runnale#run()
    func(myRunnable); 调用 MyRunnale#run()


    • 问:Java 运行时类型信息是如何表示的?


    所有的类在第一次使用时动态加载到内存中,并构造一个 Class 对象,其中包含了与类有关的所有信息,Class 对象是运行时访问类型信息的入口。需要注意的是,每个类 / 内部类 / 接口都拥有各自的 Class 对象。



    • 问:获取 Class 对象有几种方式,有什么区别?


    答:获取 Class 对象是反射的起始步骤,具体来说,分为以下三种方式:



    • 问:为什么反射性能差,怎么优化?


    答:主要有以下原因:


    性能差原因优化方法
    产生大量中间变量缓存元数据对象
    增加了检查可见性操作调用Method#setAccessible(true),减少不必要的检查
    Inflation 机制会生成字节码,而这段字节码没有经过优化/
    缺少编译器优化,普通调用有一系列优化手段,例如方法内联,而反射调用无法应用此优化/
    增加了装箱拆箱操作,反射调用需要构建包装类/


    3. 反射调用的 Inflation 机制


    反射调用是反射的一个较为常用的场景,这里我们来分析下反射调用的源码。反射调用需要使用Method#invoke(...),源码如下:


    Method.java


    public Object invoke(Object obj, Object... args) {
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
    ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
    }

    NativeMethodAccessorImpl.java


    class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
    this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) {
    1. 检查调用次数是否超过阈值
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
    2. ASM 生成新类
    MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
    3. 设置为代理
    this.parent.setDelegate(var3);
    }
    4. 调用 native 方法
    return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
    this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
    }

    ReflectionFactory.java


    public class ReflectionFactory {

    private static int inflationThreshold = 15;

    static int inflationThreshold() {
    return inflationThreshold;
    }
    }

    可以看到,反射调用最终会委派给 NativeMethodAccessorImpl ,要点如下:



    • 当反射调用执行次数较少时,直接通过 native 方法调用;

    • 当反射调用执行次数较多时,则通过 ASM 字节码生成技术生成新的类,以后的反射调用委派给新生成的类来处理。



    提示: 为什么不一开始就生成新类呢?因为生成字节码的时间成本高于执行一次 native 方法的时间成本,所以在反射调用执行次数较少时,就直接调用 native 方法了。





    4. 反射的应用场景


    4.1 类型判断



    4.2 创建对象



    • 1、使用 Class.newInstance(),适用于类拥有无参构造方法


    Class classType = Class.forName("java.lang.String");
    String str= (String) classType.newInstance();


    • 2、Constructor.newInstance(),适用于使用带参数的构造方法


    Class classType = Class.forName("java.lang.String");
    Constructor constructor = classType.getConstructor(new Class[]{String.class});
    constructor.setAccessible(true);
    String employee3 = (String) constructor.newInstance(new Object[]{"123"});

    4.3 创建数组


    创建数组需要元素的 Class 对象作为 ComponentType:



    • 1、创建一维数组


    Class classType = Class.forName("java.lang.String");
    String[] array = (String[]) Array.newInstance(classType, 5); 长度为5
    Array.set(array, 3, "abc"); 设置元素
    String string = (String) Array.get(array,3); 读取元素


    • 2、创建多维数组


    Class[] dimens = {3, 3};
    Class[][] array = (Class[][]) Array.newInstance(int.class, dimens);

    4.3 访问字段、方法


    Editting...


    4.4 获取泛型信息


    我们知道,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!


    4.5 获取运行时注解信息


    注解是一种添加到声明上的元数据,而RUNTIME注解在类加载后会保存在 Class 对象,可以反射获取。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!






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

    Java | JDK 动态代理的原理其实很简单

    前言 代理模式(Proxy Pattern)也称委托模式(Delegate Pattern),是一种结构型设计模式,也是一项基础设计技巧; 其中,动态代理有很多有意思的应用场景,比如 AOP、日志框架、全局性异常处理、事务处理等。这篇文章,我们主要...
    继续阅读 »

    前言



    • 代理模式(Proxy Pattern)也称委托模式(Delegate Pattern),是一种结构型设计模式,也是一项基础设计技巧;

    • 其中,动态代理有很多有意思的应用场景,比如 AOP、日志框架、全局性异常处理、事务处理等。这篇文章,我们主要讨论最基本的 JDK 动态代理。




    目录





    前置知识


    这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~





    1. 概述



    • 什么是代理 (模式)? 代理模式 (Proxy Pattern) 也称委托模式 (Deletage Pattern),属于结构型设计模式,也是一项基本的设计技巧。通常,代理模式用于处理两种问题:

      • 1、控制对基础对象的访问

      • 2、在访问基础对象时增加额外功能



    这是两种非常朴素的场景,正因如此,我们常常会觉得其它设计模式中存在代理模式的影子。UML 类图和时序图如下:





    • 代理的基本分类: 静态代理 + 动态代理,分类的标准是 “代理关系是否在编译期确定;


    • 动态代理的实现方式: JDK、CGLIB、Javassist、ASM





    2. 静态代理


    2.1 静态代理的定义


    静态代理是指代理关系在编译期确定的代理模式。使用静态代理时,通常的做法是为每个业务类抽象一个接口,对应地创建一个代理类。举个例子,需要给网络请求增加日志打印:


    1、定义基础接口
    public interface HttpApi {
    String get(String url);
    }

    2、网络请求的真正实现
    public class RealModule implements HttpApi {
    @Override
    public String get(String url) {
    return "result";
    }
    }

    3、代理类
    public class Proxy implements HttpApi {
    private HttpApi target;

    Proxy(HttpApi target) {
    this.target = target;
    }

    @Override
    public String get(String url) {
    // 扩展的功能
    Log.i("http-statistic", url);
    // 访问基础对象
    return target.get(url);
    }
    }

    2.2 静态代理的缺点



    • 1、重复性: 需要代理的业务或方法越多,重复的模板代码越多;

    • 2、脆弱性: 一旦改动基础接口,代理类也需要同步修改(因为代理类也实现了基础接口)。




    3. 动态代理


    3.1 动态代理的定义


    动态代理是指代理关系在运行时确定的代理模式。需要注意,JDK 动态代理并不等价于动态代理,前者只是动态代理的实现之一,其它实现方案还有:CGLIB 动态代理、Javassist 动态代理和 ASM 动态代理等。因为代理类在编译前不存在,代理关系到运行时才能确定,因此称为动态代理。


    3.2 JDK 动态代理示例


    我们今天主要讨论JDK 动态代理(Dymanic Proxy API),它是 JDK1.3 中引入的特性,核心 API 是 Proxy 类和 InvocationHandler 接口。它的原理是利用反射机制在运行时生成代理类的字节码。


    我们继续用打印日志的例子,使用动态代理时:


    public class ProxyFactory {
    public static HttpApi getProxy(HttpApi target) {
    return (HttpApi) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new LogHandler(target));
    }

    private static class LogHandler implements InvocationHandler {
    private HttpApi target;

    LogHandler(HttpApi target) {
    this.target = target;
    }
    // method底层的方法无参数时,args为空或者长度为0
    @Override
    public Object invoke(Object proxy, Method method, @Nullable Object[] args)
    throws Throwable
    {
    // 扩展的功能
    Log.i("http-statistic", (String) args[0]);
    // 访问基础对象
    return method.invoke(target, args);
    }
    }
    }

    如果需要兼容多个业务接口,可以使用泛型:


    public class ProxyFactory {
    @SuppressWarnings("unchecked")
    public static T getProxy(T target) {
    return (T) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new LogHandler(target));
    }

    private static class LogHandler implements InvocationHandler {
    // 同上
    }
    }

    客户端调用:


    HttpAPi proxy = ProxyFactory.getProxy(target);
    OtherHttpApi proxy = ProxyFactory.getProxy(otherTarget);

    通过泛型参数传递不同的类型,客户端可以按需实例化不同类型的代理对象。基础接口的所有方法都统一到 InvocationHandler#invoke() 处理。静态代理的两个缺点都得到解决:



    • 1、重复性:即使有多个基础业务需要代理,也不需要编写过多重复的模板代码;

    • 2、脆弱性:当基础接口变更时,同步改动代理并不是必须的。


    3.3 静态代理 & 动态代理对比



    • 共同点:两种代理模式实现都在不改动基础对象的前提下,对基础对象进行访问控制和扩展,符合开闭原则。

    • 不同点:静态代理存在重复性和脆弱性的缺点;而动态代理(搭配泛型参数)可以实现了一个代理同时处理 N 种基础接口,一定程度上规避了静态代理的缺点。从原理上讲,静态代理的代理类 Class 文件在编译期生成,而动态代理的代理类 Class 文件在运行时生成,代理类在 coding 阶段并不存在,代理关系直到运行时才确定。




    4. JDK 动态代理源码分析


    这一节,我们来分析 JDK 动态代理的源码,核心类是 Proxy,主要分析 Proxy 如何生成代理类,以及如何将方法调用统一分发到 InvocationHandler 接口。


    4.1 API 概述


    Proxy 类主要包括以下 API:



























    Proxy 描述
    getProxyClass(ClassLoader, Class...) : Class 获取实现目标接口的代理类 Class 对象
    newProxyInstance(ClassLoader,Class[],InvocationHandler) : Object 获取实现目标接口的代理对象
    isProxyClass(Class) : boolean 判断一个 Class 对象是否属于代理类
    getInvocationHandler(Object) : InvocationHandler 获取代理对象内部的 InvocationHandler

    4.2 核心源码


    Proxy.java


    1、获取代理类 Class 对象
    public static Class getProxyClass(ClassLoader loader,Class... interfaces){
    final Class[] intfs = interfaces.clone();
    ...
    1.1 获得代理类 Class 对象
    return getProxyClass0(loader, intfs);
    }

    2、实例化代理类对象
    public static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h){
    ...
    final Class[] intfs = interfaces.clone();
    2.1 获得代理类 Class对象
    Class cl = getProxyClass0(loader, intfs);
    ...
    2.2 获得代理类构造器 (接收一个 InvocationHandler 参数)
    // private static final Class[] constructorParams = { InvocationHandler.class };
    final Constructor cons = cl.getConstructor(constructorParams);
    final InvocationHandler ih = h;
    ...
    2.3 反射创建实例
    return newInstance(cons, ih);
    }

    可以看到,实例化代理对象也需要先通过 getProxyClass0(...) 获取代理类 Class 对象,而 newProxyInstance(...) 随后会获取参数为 InvocationHandler 的构造函数实例化一个代理类对象。


    我们先看下代理类 Class 对象是如何获取的:


    Proxy.java


    -> 1.12.1 获得代理类 Class对象
    private static Class getProxyClass0(ClassLoader loader,Class... interfaces) {
    ...
    从缓存中获取代理类,如果缓存未命中,则通过ProxyClassFactory生成代理类
    return proxyClassCache.get(loader, interfaces);
    }

    private static final class ProxyClassFactory implements BiFunction[], Class>{

    3.1 代理类命名前缀
    private static final String proxyClassNamePrefix = "$Proxy";

    3.2 代理类命名后缀,从 0 递增(原子 Long)
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class apply(ClassLoader loader, Class[] interfaces)
    {
    Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
    3.3 参数校验
    for (Class intf : interfaces) {
    // 验证参数 interfaces 和 ClassLoder 中加载的是同一个类
    // 验证参数 interfaces 是接口类型
    // 验证参数 interfaces 中没有重复项
    // 否则抛出 IllegalArgumentException
    }
    // 验证所有non-public接口来自同一个包

    3.4(一般地)代理类包名
    // public static final String PROXY_PACKAGE = "com.sun.proxy";
    String proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";

    3.5 代理类的全限定名
    long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    3.6 生成字节码数据
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);

    3.7 从字节码生成 Class 对象
    return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
    }
    }

    -> 3.6 生成字节码数据
    public static byte[] generateProxyClass(final String var0, Class[] var1) {
    ProxyGenerator var2 = new ProxyGenerator(var0, var1);
    ...
    final byte[] var3 = var2.generateClassFile();
    return var3;
    }

    ProxyGenerator.java


    private byte[] generateClassFile() {
    3.6.1 只代理Object的hashCode、equals和toString
    this.addProxyMethod(hashCodeMethod, Object.class);
    this.addProxyMethod(equalsMethod, Object.class);
    this.addProxyMethod(toStringMethod, Object.class);

    3.6.2 代理接口的每个方法
    ...
    for(var1 = 0; var1 < this.interfaces.length; ++var1) {
    ...
    }

    3.6.3 添加带有 InvocationHandler 参数的构造器
    this.methods.add(this.generateConstructor());
    var7 = this.proxyMethods.values().iterator();
    while(var7.hasNext()) {
    ...
    3.6.4 在每个代理的方法中调用InvocationHandler#invoke()
    }

    3.6.5 输出字节流
    ByteArrayOutputStream var9 = new ByteArrayOutputStream();
    DataOutputStream var10 = new DataOutputStream(var9);
    ...
    return var9.toByteArray();
    }

    以上代码已经非常简化了,主要关注核心流程:JDK 动态代理生成的代理类命名为 com.sun.proxy$Proxy[从0开始的数字](例如:com.sun.proxy$Proxy0),这个类继承自 java.lang.reflect.Proxy。其内部还有一个参数为 InvocationHandler 的构造器,对于代理接口的方法调用都会分发到 InvocationHandler#invoke()。


    UML 类图如下,需要注意图中红色箭头,表示代理类和 HttpApi 接口的代理关系在运行时才确定:




    提示: Android 系统中生成字节码和从字节码生成 Class 对象的步骤都是 native 方法:



    • private static native Class generateProxy(…)

    • 对应的native方法:dalvik/vm/native/java_lang_reflect_Proxy.cpp



    4.3 查看代理类源码


    可以看到,ProxyGenerator#generateProxyClass() 其实是一个静态 public 方法,所以我们直接调用,并将代理类 Class 的字节流写入磁盘文件,使用 IntelliJ IDEA 的反编译功能查看源代码。


    输出字节码:


    byte[] classFile = ProxyGenerator.generateProxyClass("$proxy0",new Class[]{HttpApi.class});
    // 直接写入项目路径下,方便使用IntelliJ IDEA的反编译功能
    String path = "/Users/pengxurui/IdeaProjects/untitled/src/proxy/HttpApi.class";
    try(FileOutputStream fos = new FileOutputStream(path)){
    fos.write(classFile);
    fos.flush();
    System.out.println("success");
    } catch (Exception e){
    e.printStackTrace();
    System.out.println("fail");
    }

    反编译结果:


    public final class $proxy0 extends Proxy implements HttpApi {
    //反射的元数据Method存储起来,避免重复创建
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $proxy0(InvocationHandler var1) throws {
    super(var1);
    }

    /**
    * Object#hashCode()
    * Object#equals(Object)
    * Object#toString()
    */


    // 实现了HttpApi接口
    public final String get() throws {
    try {
    //转发到Invocation#invoke()
    return (String)super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
    throw var2;
    } catch (Throwable var3) {
    throw new UndeclaredThrowableException(var3);
    }
    }

    static {
    try {
    //Object#hashCode()
    //Object#equals(Object)
    //Object#toString()
    m3 = Class.forName("HttpApi").getMethod("get");
    } catch (NoSuchMethodException var2) {
    throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
    throw new NoClassDefFoundError(var3.getMessage());
    }
    }
    }

    4.4 常见误区



    • 基础对象必须实现基础接口,否则不能使用动态代理


    这个想法可能来自于一些没有实现任何接口的类,因此就没有办法得到接口的Class对象作为Proxy#newProxyInstance() 的参数,这确实会带来一些麻烦,举个例子:


    package com.domain;
    public interface HttpApi {
    String get();
    }

    // 另一个包的non-public接口
    package com.domain.inner;
    /**non-public**/interface OtherHttpApi{
    String get();
    }

    package com.domain.inner;
    // OtherHttpApiImpl类没有实现HttpApi接口或者没有实现任何接口
    public class OtherHttpApiImpl /**extends OtherHttpApi**/{
    public String get() {
    return "result";
    }
    }

    // Client:
    HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // TODO:扩展的新功能
    // IllegalArgumentException: object is not an instance of declaring class
    return method.invoke(impl,args);
    }
    });
    api.get();

    在这个例子里,OtherHttpApiImpl 类因为历史原因没有实现 HttpApi 接口,虽然方法签名与 HttpApi 接口的方法签名完全相同,但是遗憾,无法完成代理。也有补救的办法,找到 HttpApi 接口中签名相同的 Method,使用这个 Method 来转发调用。例如:


    HttpApi api = (HttpApi) Proxy.newProxyInstance(...}, new InvocationHandler() {
    OtherHttpApiImpl impl = new OtherHttpApiImpl();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // TODO:扩展的新功能
    if (method.getDeclaringClass() != impl.getClass()) {
    // 找到相同签名的方法
    Method realMethod = impl.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
    return realMethod.invoke(impl, args);
    }else{
    return method.invoke(impl,args);
    }
    }
    });



    5. 总结


    今天,我们讨论了静态代理和动态代理两种代理模式,静态代理在设计模式中随处可见,但存在重复性和脆弱性的缺点,动态代理的代理关系在运行时确定,可以实现一个代理处理 N 种基础接口,一定程度上规避了静态代理的缺点。在我们熟悉的一个网络请求框架中,就充分利用了动态代理的特性,你知道是在说哪个框架吗?




    参考资料







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

    这一次,彻底搞懂SparseArray实现原理

    最近在整理SparseArray这一知识点的时候,发现网上大多数SparseArray原理分析的文章都存在很多问题(可以说很多作者并没有读懂SparseArray的源码),也正因此,才有了这篇文章。我们知道,SparseArray与ArrayMap是Andro...
    继续阅读 »

    最近在整理SparseArray这一知识点的时候,发现网上大多数SparseArray原理分析的文章都存在很多问题(可以说很多作者并没有读懂SparseArray的源码),也正因此,才有了这篇文章。我们知道,SparseArray与ArrayMap是Android中高效存储K-V的数据结构,也是是Android面试中的常客,弄懂它们的实现原理是很有必要的,本篇文章就以SparseArray的源码为例进行深入分析。


    一、SparseArray的类结构


    SparseArray可以翻译为稀疏数组,从字面上可以理解为松散不连续的数组。虽然叫做Array,但它却是存储K-V的一种数据结构。其中Key只能是int类型,而Value是Object类型。我们来看下它的类结构:


    public class SparseArray<E> implements Cloneable {
    // 用来标记此处的值已被删除
    private static final Object DELETED = new Object();
    // 用来标记是否有元素被移除
    private boolean mGarbage = false;
    // 用来存储key的集合
    private int[] mKeys;
    // 用来存储value的集合
    private Object[] mValues;
    // 存入的元素个数
    private int mSize;

    // 默认初始容量为10
    public SparseArray() {
    this(10);
    }

    public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
    mKeys = EmptyArray.INT;
    mValues = EmptyArray.OBJECT;
    } else {
    mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
    mKeys = new int[mValues.length];
    }
    mSize = 0;
    }

    // ...省略其他代码

    }

    可以看到SparseArray仅仅实现了Cloneable接口并没有实现Map接口,并且SparseArray内部维护了一个int数组和一个Object数组。在无参构造方法中调用了有参构造,并将其初始容量设置为了10。


    二、SparseArray的remove()方法


    是不是觉得很奇怪?作为一个容器类,不先讲put方法怎么先将remove呢?这是因为remove方法的一些操作会影响到put的操作。只有先了解了remove才能更容易理解put方法。我们来看remove的代码:



    // SparseArray
    public void remove(int key) {
    delete(key);
    }

    public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    mValues[i] = DELETED;
    mGarbage = true;
    }
    }
    }

    可以看到remove方法直接调用了delete方法。而在delete方法中会先通过二分查找(二分查找代码后边分析)找到key所在的位置,然后将这一位置的value值置为DELETE,注意,这里还将mGarbage设置为了true来标记集合中存在删除元素的情况。想象一下,在删除多个元素后这个集合中是不是就可能会出现不连续的情况?大概这也是SparseArray名字的由来吧。


    三、SparseArray的put()方法


    作为一个存储K-V类型的数据结构,put方法是key和value的入口。也是SparseArray中最重要的一个方法。先来看下put方法的代码:


    // SparseArray
    public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) { // 意味着之前mKeys中已经有对应的key存在了,第i个位置对应的就是key。
    mValues[i] = value; // 直接更新value
    } else { // 返回负数说明未在mKeys中查找到key

    // 取反得到待插入key的位置
    i = ~i;

    // 如果插入位置小于size,并且这个位置的value刚好是被删除掉的,那么直接将key和value分别插入mKeys和mValues的第i个位置
    if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
    }
    // mGarbage为true说明有元素被移除了,此时mKeys已经满了,但是mKeys内部有被标记为DELETE的元素
    if (mGarbage && mSize >= mKeys.length) {
    // 调用gc方法移动mKeys和mValues中的元素,这个方法可以后边分析
    gc();

    // 由于gc方法移动了数组,因此插入位置可能有变化,所以需要重新计算插入位置
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
    }
    // GrowingArrayUtils的insert方法将会将插入位置之后的所有数据向后移动一位,然后将key和value分别插入到mKeys和mValue对应的第i个位置,如果数组空间不足还会开启扩容,后边分析这个insert方法
    mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
    mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
    mSize++;
    }
    }

    虽然这个方法只有寥寥数行,但是想要完全理解却并非易事,即使写了很详细的注释也不容易读懂。我们不妨来详细分析一下。第一行代码通过二分查找得到了一个index。看下二分查找的代码:


    // ContainerHelpers
    static int binarySearch(int[] array, int size, int value) {
    int lo = 0;
    int hi = size - 1;

    while (lo <= hi) {
    final int mid = (lo + hi) >>> 1;
    final int midVal = array[mid];

    if (midVal < value) {
    lo = mid + 1;
    } else if (midVal > value) {
    hi = mid - 1;
    } else {
    return mid; // value found
    }
    }
    return ~lo; // value not present
    }

    关于二分查找相信大家都是比较熟悉的,这一算法用于在一组有序数组中查找某一元素所在位置的。如果数组中存在这一元素,则将这个元素对应的位置返回。如果不存在那么此时的lo就是这个元素的最佳存储位置。上述代码中将lo取反作为了返回值。因为lo一定是大于等于0的数,因此取反后的返回值必定小于等于0.明白了这一点,再来看put方法中的这个if...else是不是很容易理解了?


    // SparseArray
    public void put(int key, E value) {

    if (i >= 0) {
    mValues[i] = value; // 直接更新value
    } else {
    i = ~i;
    // ... 省略其它代码
    }
    }

    如果i>=0,意味着当前的这个key已经存在于mKeys中了,那么此时put只需要将最新的value更新到mValues中即可。而如果i<=0就意味着mKeys中之前没有对应的key。因此就需要将key和value分别插入到mKeys和mValues中。而插入的最佳位置就是对i取反。


    得到插入位置之后,如果这个位置是被标记为删除的元素,那么久可以直接将其覆盖掉了,因此有以下代码:


    public void put(int key, E value) {
    // ...
    if (i >= 0) {
    // ...
    } else {
    // 如果i对应的位置是被删除掉的,可以直接将其覆盖
    if (i < mSize && mValues[i] == DELETED) {
    mKeys[i] = key;
    mValues[i] = value;
    return;
    }
    // ...
    }

    }

    如果上边条件不满足,那么继续往下看:


    public void put(int key, E value) {
    // ...
    if (i >= 0) {
    // ...
    } else {
    // mGarbage为true说明有元素被移除了,此时mKeys已经满了,但是mKeys内部有被标记为DELETE的元素
    if (mGarbage && mSize >= mKeys.length) {
    // 调用gc方法移动mKeys和mValues中的元素,这个方法可以后边分析
    gc();

    // 由于gc方法移动了数组,因此插入位置可能有变化,所以需要重新计算插入位置
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
    }
    // ...
    }

    }

    上边我们已经知道,在remove元素的时候mGarbage会被置为true,这段代码意味着有被移除的元素,被移除的位置并不是要插入的位置,并且如果mKeys已经满了,那么就调用gc方法来移动元素填充被移除的位置。由于mKeys中元素位置发生了变化,因此key插入的位置也可能改变,因此需要再次调用二分法来查找key的插入位置。


    以上代码最终会确定key被插入的位置,接下来调用GrowingArrayUtils的insert方法来进行key的插入操作:


    // SparseArray
    public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    // ...
    } else {
    // ...

    // GrowingArrayUtils的insert方法将会将插入位置之后的所有数据向后移动一位,然后将key和value分别插入到mKeys和mValue对应的第i个位置,如果数组空间不足还会开启扩容,后边分析这个insert方法
    mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
    mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
    mSize++;
    }
    }

    GrowingArrayUtils的insert方法代码如下:


    // GrowingArrayUtils
    public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    assert currentSize <= array.length;
    // 如果插入后数组size小于数组长度,能进行插入操作
    if (currentSize + 1 <= array.length) {
    // 将index之后的所有元素向后移动一位
    System.arraycopy(array, index, array, index + 1, currentSize - index);
    // 将key插入到index的位置
    array[index] = element;
    return array;
    }

    // 来到这里说明数组已满,需需要进行扩容操作。newArray即为扩容后的数组
    T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
    growSize(currentSize));
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
    }

    // 返回扩容后的size
    public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
    }

    insert方法的代码比较容易理解,如果数组容量足够,那么就将index之后的元素向后移动一位,然后将key插入index的位置。如果数组容量不足,那么则需要进行扩容,然后再进行插入操作。


    四、SparseArray的gc()方法


    这个方法其实很容易理解,我们知道Java虚拟机在内存不足时会进行GC操作,标记清除法在回收垃圾对象后为了避免内存碎片化,会将存活的对象向内存的一端移动。而SparseArray中的这个gc方法其实就是借鉴了垃圾收集整理碎片空间的思想。


    关于mGarbage这个参数上边已经有提到过了,这个变量会在删除元素的时候被置为true。如下:


    // SparseArray中所有移除元素的方法中都将mGarbage置为true

    public E removeReturnOld(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    final E old = (E) mValues[i];
    mValues[i] = DELETED;
    mGarbage = true;
    return old;
    }
    }
    return null;
    }

    public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
    if (mValues[i] != DELETED) {
    mValues[i] = DELETED;
    mGarbage = true;
    }
    }
    }


    public void removeAt(int index) {
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
    throw new ArrayIndexOutOfBoundsException(index);
    }
    if (mValues[index] != DELETED) {
    mValues[index] = DELETED;
    mGarbage = true;
    }
    }



    而SparseArray中所有插入和查找元素的方法中都会判断如果mGarbage为true,并且mSize >= mKeys.length时调用gc,以append方法为例,代码如下:


    public void append(int key, E value) {

    if (mGarbage && mSize >= mKeys.length) {
    gc();
    }

    // ... 省略无关代码
    }

    源码中调用gc方法的地方多达8处,都是与添加和查找元素相关的方法。例如put()、keyAt()、setValueAt()等方法中。gc的实现其实比较简单,就是将删除位置后的所有数据向前移动一下,代码如下:


    private void gc() {
    // Log.e("SparseArray", "gc start with " + mSize);

    int n = mSize;
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
    Object val = values[i];

    if (val != DELETED) {
    if (i != o) {
    keys[o] = keys[i];
    values[o] = val;
    values[i] = null;
    }

    o++;
    }
    }

    mGarbage = false;
    mSize = o;

    // Log.e("SparseArray", "gc end with " + mSize);
    }

    五、SparseArray的get()方法


    这个方法就比较简单了,因为put的时候是维持了一个有序数组,因此通过二分查找可以直接确定key在数组中的位置。


    public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i < 0 || mValues[i] == DELETED) {
    return valueIfKeyNotFound;
    } else {
    return (E) mValues[i];
    }
    }

    六、总结


    可见SparseArray是一个使用起来很简单的数据结构,但是它的原理理解起来似乎却没那么容易。这也是网上大部分文章对应SparseArray的解析都是含糊不清的原因。相信通过本篇文章的学习一定对SparseArray的实现有了新的认识!


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

    iOS 音视频编解码----H264-I(关键)帧,B/P(参考)帧

    内容元素1.图像(image)2.音频(Audio)3.元素信息(Meta-data)编码格式1.Video:H2642.Audio:AAC3.容器封装:MP4/MOV/FLV/RM/RMVB/AVIH264当我们需要对发送的视频文件进行编码时,只要是H264...
    继续阅读 »




  • 内容元素

    1.图像(image)


    2.音频(Audio)


    3.元素信息(Meta-data)


  • 编码格式

    1.Video:H264


    2.Audio:AAC


    3.容器封装:MP4/MOV/FLV/RM/RMVB/AVI


  • H264

    当我们需要对发送的视频文件进行编码时,只要是H264文件,AVFoundation都提供视频编解码器支持,这个标准被广泛应用于消费者视频摄像头捕捉到的资源并成为网页流媒体视频最主要的格式。H264规范是MPEG4定义的一部分,H264遵循早期的MPEG-1/MPEG-2标准,但是在以更低比特率得到 更高图片质量方面有了进步。


  • 编码的本质






    • 为什么要编码?

    举个例子: (具体大小我没数过,只做讲解参考)
    你的老公,Helen,将于明天晚上6点零5份在重庆的江北机场接你
    ----------------------23 * 2 + 10 = 56个字符--------------------------


    你的老公将于明天晚上6点零5分在江北机场接你
    ----------------------20 * 2 + 2 = 42个字符----------------------------


    Helen将于明天晚上6点在机场接你
    ----------------------10 * 2 + 2 = 26个字符----------------------------


    相信大家看了这个例子之后,心里应该大概明白编码的本质:只要不接收方不会产生误解,就可以产生数据的承载量,编码视频的本质也是如此,编码的本质就是减少数据的冗余


    • 在引入I(关键)帧,B/P(参考)帧之前,我们先来了解一下人眼和视频摄像头的对帧的识别

      我们人眼看实物的时候,一般的一秒钟只要是连续的16帧以上,我们就会认为事物是动的,对于摄像头来说,一秒钟所采集的图片远远高于了16帧,可以达到几十帧,对于一些高质量的摄像头,一秒钟可以达到60帧,对于一般的事物来说,你一秒钟能改变多少个做动作?所以,当摄像头在一秒钟内采集视频的时候,前后两帧的图片数据里有大量的相同数据,对于这些数据我们该怎么处理了?当然前后两帧也有不同





  • I帧(I-frames,也叫关键帧)


    • 我们知道在视频的传输过程中,它是分片段传输的,这个片段的第一帧,也就是第一张图片,就是I帧,也就是传说中的关键帧
    • I帧:也就是关键帧,帧内压缩(也就是压缩独立视频帧,被称为帧内压缩),帧内压缩通过消除包含在每个独立视频帧内的色彩以及结构中的冗余信息来进行压缩,因此可在不降低图片适量的情况下尽可能的缩小尺寸,这类似于JEPG压缩的原理,帧内压缩也可以称为有损压缩算法,但通常用于对原因图片的一部分进行处理,以生成极高质量的照片,通过这一过程的创建的帧称为I-frames;将第一帧完整的保存下来.如果没有这个关键帧后面解码数据,是完成不了的.所以I帧特别关键.

  • P帧(P-frames,又称为预测帧)

    • P帧:向前参考帧(在I帧(关键帧)后,P帧参考关键帧,保存下一帧和前一帧的不同数据).压缩时只参考前一个帧.属于帧间压缩技术.
    • 帧间压缩技术:很多帧被组合在一起作为一组图片(简称GOP),对于GOP所存在的时间维度的冗余可以被消除,如果想象视频文件中的典型场景,就会有一些特定的运动元素的概念,比如行驶中的汽车或者街道上行走的路人,场景的背景环信通道是固定的,或者在一定的时间内,有些元素的改变很小或者不变,这些数据就称为时间上的冗余,这些数据就可以通过帧间压缩的方式进行消除,也就是帧间压缩,视频的第一帧会被作为关键帧完整保存下来.而后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面所有的帧只存储于前一帧的差异.这样就能将数据大大的减少.从而达到一个高压缩率的效果.这就是P帧,保存前后两帧不通的数据

  • B帧(B-frames,又称为双向帧)

    • B帧,又叫双向帧,它是基于使用前后两帧进行编码后得到的帧,几乎不需要存储空间,但是解压过程会消耗较长的时间,因为它依赖周围其他的帧
    • B帧的特点:B帧使得视频的压缩率更高.存储的数据量更小.如果B帧的数量越多,你的压缩率就越高.这是B帧的优点,但是B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.如果网络状态很好的话,解码会比较快,如果网络不好时解码会稍微慢一些.丢包时还需要重传.对实时互动的直播,一般不会使用B帧.如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧.如果我们在实时互动的直播,我们需要提高时效性,这时就不能使用B帧了.

  • GOP一组帧的理解

    • 如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组.

    • 什么叫一组帧?
      就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧.我们称为GOP






    • 视频花屏/卡顿原因

      • 我们平常在观看视频的时候,出现视频的花屏或者卡顿,第一反应就是我们的网络出现了问题,其实我们的网络没有问题,是我们在解码的时候I帧,B/P帧出现了丢失
      • 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误.
      • 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像.
      • 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因.

    所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.



    作者:枫紫
    链接:https://www.jianshu.com/p/94d2a8bbc3ac





    收起阅读 »

    iOS 音视频编解码基本概念

    内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC容器封装: • MP4/MOV/FLV/RM/RMVB/AVI.视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面...
    继续阅读 »



    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 容器封装: • MP4/MOV/FLV/RM/RMVB/AVI

    • .视频相关基础概念

      • 1.视频文件格式

        相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.jpg等,我们常见的视频文件后缀有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。这些后缀名通常在操作系统上用相应的应用程序打开,比如.doc用word打开。对于视频来说,为什么会有这么多的文件格式了,那是因为通过了不同的方式来实现了视频这件事---------视频的封装格式。
    • 2.视频的封装格式

      视频封装格式,通常我们把它称作为视频格式,它相当于一种容器,比如可乐的瓶子,矿泉水瓶等等。它里面包含了视频的相关信息(视频信息,音频信息,解码方式等等),一种封装格式直接反应了视频的文件格式,封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.




    • 相关视频封装格式的优缺点:

      • 1.AVI 格式:这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。
      • 2.WMV 格式:可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。
      • 3.MPEG 格式:为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。
      • 4.Matroska 格式:是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。
      • 5.Real Video 格式:用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。
      • 6.QuickTime File Format 格式:是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。
      • 7.Flash Video 格式: Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。
    • 视频的编码格式

    • 视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:

      • 视频的质量、
      • 用来表示视频所需要的数据量(通常称之为码率)、
      • 编码算法和解码算法的复杂度
      • 针对数据丢失和错误的鲁棒性(Robustness)
      • 编辑的方便性
      • 随机访问
      • 编码算法设计的完美性
      • 端到端的延时以及其它一些因素
    • 常见的编码方式:

    • H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

      • H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
      • H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
      • H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
      • H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
    • 当前不建议用H.265是因为太过于消耗CPU,而且目前H.264已经满足了大多的视频需求,虽然H.265是H.264的升级版,期待后续硬件跟上

    • MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

      • MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
      • MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
      • MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
        其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。
    • 可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

    • 音频编码方式

      • 视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

      • AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。

      • MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。

      • WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

    直播/小视频中的编码格式

    • 视频编码格式

      • H264编码的优势:
        低码率
        高质量的图像
        容错能力强
        网络适应性强
    • 总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
      举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
      音频编码格式:

    • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

    • LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)

    • HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)

    • 优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

    关于H264

    • H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

    • 图像

      • H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

    当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

    当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

    「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好




    • 片(Slice),每一帧图像可以分为多个片

    网络提取层单元(NALU, Network Abstraction Layer Unit),
    NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

    宏块(Macroblock),分片是由宏块组成。




    作者:枫紫
    链接:https://www.jianshu.com/p/9602f3c9b82b


    收起阅读 »

    iOS 特效 - iCarousel

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其...
    继续阅读 »

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其他“CoverFlow”库不同,iCarousel 可以处理任何类型的视图,而不仅仅是图像,因此它非常适合在您的应用程序中以流畅且令人印象深刻的方式呈现分页数据。它还使得以最少的代码更改在不同的轮播效果之间切换变得非常容易。

    支持的操作系统和 SDK 版本

    • 支持的构建目标 - iOS 10.0 / Mac OS 10.12(Xcode 8.0,Apple LLVM 编译器 8.0)
    • 最早支持的部署目标 - iOS 5.0 / Mac OS 10.7
    • 最早的兼容部署目标 - iOS 4.3 / Mac OS 10.6

    注意:“支持”表示该库已经过此版本的测试。“兼容”意味着库应该在这个操作系统版本上工作(即它不依赖于任何不可用的 SDK 功能)但不再进行兼容性测试,可能需要调整或错误修复才能正确运行。

    ARC兼容性

    从 1.8 版开始,iCarousel 需要 ARC。如果您希望在非 ARC 项目中使用 iCarousel,只需将 -fobjc-arc 编译器标志添加到 iCarousel.m 类。为此,请转到目标设置中的 Build Phases 选项卡,打开 Compile Sources 组,双击列表中的 iCarousel.m 并在弹出窗口中键入 -fobjc-arc。

    如果您希望将整个项目转换为 ARC,请在 iCarousel.m 中注释掉 #error 行,然后在 Xcode 中运行 Edit > Refactor > Convert to Objective-C ARC... 工具并确保您希望转换的所有文件使用 ARC 进行(包括 iCarousel.m)检查。

    线程安全

    iCarousel 派生自 UIView 并且 - 与所有 UIKit 组件一样 - 它只能从主线程访问。您可能希望使用线程来加载或更新轮播内容或项目,但始终确保一旦您的内容加载完毕,您就可以在更新轮播前切换回主线程。

    安装

    要在应用程序中使用 iCarousel 类,只需将 iCarousel 类文件(不需要演示文件和资产)拖到您的项目中并添加 QuartzCore 框架。您也可以使用 Cocoapods 以正常方式安装它。


    轮播类型

    iCarousel 支持以下内置显示类型:

    • iCarouselTypeLinear
    • iCarouselTypeRotary
    • iCarouselTypeInvertedRotary
    • iCarouselTypeCylinder
    • iCarouselTypeInvertedCylinder
    • iCarouselTypeWheel
    • iCarouselTypeInvertedWheel
    • iCarouselTypeCoverFlow
    • iCarouselTypeCoverFlow2
    • iCarouselTypeTimeMachine
    • iCarouselTypeInvertedTimeMachine

    您还可以使用iCarouselTypeCustomcarousel:itemTransformForOffset:baseTransform:委托方法实现自己的定制轮播样式

    注意:iCarouselTypeCoverFlowiCarouselTypeCoverFlow2类型之间的区别非常微妙,但是 for 的逻辑要iCarouselTypeCoverFlow2复杂得多。如果您轻弹转盘,它们基本上是相同的,但是如果您用手指缓慢拖动转盘,则差异应该很明显。iCarouselTypeCoverFlow2旨在尽可能接近地模拟标准 Apple CoverFlow 效果,并且将来可能会为了该目标而进行微妙的更改。

    显示类型可视化示例

    线性

    线性

    旋转式

    旋转式


    倒转

    倒转


    圆筒

    圆筒

    倒置气缸

    倒置气缸

    Cover Flow功能

    Cover Flow功能



    特性

    iCarousel 具有以下属性(注意:对于 Mac OS,在使用属性时将 NSView 替换为 UIView):

    @property (nonatomic, weak) IBOutlet id dataSource;

    一个支持 iCarouselDataSource 协议并可以提供视图来填充轮播的对象。

    @property (nonatomic, weak) IBOutlet id delegate;

    一个支持 iCarouselDelegate 协议并且可以响应轮播事件和布局请求的对象。

    @property (nonatomic, assign) iCarouselType type;

    用于切换轮播显示类型(详见上文)。

    @property (nonatomic, assign) CGFloat perspective;

    用于调整各种 3D 轮播视图的透视缩短效果。应为负值,小于 0 且大于 -0.01。超出此范围的值将产生非常奇怪的结果。默认值为 -1/500 或 -0.005;

    @property (nonatomic, assign) CGSize contentOffset;

    此属性用于调整轮播项目视图相对于轮播中心的偏移。它默认为 CGSizeZero,这意味着轮播项目居中。更改此值会移动轮播项目而不改变其视角,即消失点随轮播项目移动,因此如果您将轮播项目向下移动,则不会看起来好像您在俯视轮播。

    @property (nonatomic, assign) CGSize viewpointOffset;

    此属性用于调整相对于轮播项目的用户视角。它与调整 contentOffset 有相反的效果,即如果您向上移动视点,则轮播似乎向下移动。与 contentOffset 不同,移动视点也会改变相对于旋转木马项目的透视消失点,因此如果您向上移动视点,它会看起来好像您在俯视旋转木马。

    @property (nonatomic, assign) CGFloat decelerationRate;

    旋转木马在轻弹时减速的速率。较高的值意味着较慢的减速。默认值为 0.95。值应在 0.0(释放时旋转木马立即停止)到 1.0(旋转木马无限期地继续而不减速,除非它到达终点)的范围内。

    @property (nonatomic, assign) BOOL bounces;

    设置旋转木马是应该弹过终点并返回,还是停止不动。请注意,这对设计为包装的轮播类型或 carouselShouldWrap 委托方法返回 YES 的类型没有影响。

    @property (nonatomic, assign) CGFloat bounceDistance;

    未包裹的传送带越过末端时反弹的最大距离。这是以 itemWidth 的倍数来衡量的,因此值 1.0 表示轮播将反弹整个项目宽度,值 0.5 表示项目宽度的一半,依此类推。默认值为 1.0;

    @property (nonatomic, assign, getter = isScrollEnabled) BOOL scrollEnabled;

    启用和禁用用户滚动轮播。如果此属性设置为 NO,则仍然可以通过编程方式滚动轮播。

    @property (nonatomic, readonly, getter = isWrapEnabled) BOOL wrapEnabled;

    如果启用包装,则返回 YES,否则返回 NO。此属性是只读的。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 返回一个值iCarouselOptionWrap

    @property (nonatomic, assign, getter = isPagingEnabled) BOOL pagingEnabled;

    启用和禁用分页。启用分页后,轮播将在用户滚动时在每个项目视图处停止,这与 UIScrollView 的 pagingEnabled 属性非常相似。

    @property (nonatomic, readonly) NSInteger numberOfItems;

    轮播中的项目数(只读)。要设置它,请实现numberOfItemsInCarousel:dataSource 方法。请注意,并非所有这些项目视图都会在给定的时间点加载或可见 - 轮播在滚动时按需加载项目视图。

    @property (nonatomic, readonly) NSInteger numberOfPlaceholders;

    要在轮播中显示的占位符视图的数量(只读)。要设置它,请实现numberOfPlaceholdersInCarousel:dataSource 方法。

    @property (nonatomic, readonly) NSInteger numberOfVisibleItems;

    屏幕上同时显示的轮播项目视图的最大数量(只读)。此属性对于性能优化很重要,并且会根据轮播类型和视图框架自动计算。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 iCarouselOptionVisibleItems 返回一个值。

    @property (nonatomic, strong, readonly) NSArray *indexesForVisibleItems;

    一个数组,包含当前加载和在轮播中可见的所有项目视图的索引,包括占位符视图。该数组包含 NSNumber 对象,其整数值与视图的索引匹配。项目视图的索引从零开始并匹配传递给 dataSource 以加载视图的索引,但是任何可见占位符视图的索引要么是负数(小于零)要么大于或等于numberOfItems此数组中占位符视图的索引等同于与 dataSource 一起使用的占位符视图索引。

    @property (nonatomic, strong, readonly) NSArray *visibleItemViews;

    当前显示在轮播中的所有项目视图的数组(只读)。这包括任何可见的占位符视图。此数组中的视图索引与项目索引不匹配,但是这些视图的顺序与 visibleItemIndexes 数组属性的顺序匹配,即您可以通过从visibleItemIndexes 数组(或者,您可以只使用该indexOfItemView:方法,这要容易得多)。

    @property (nonatomic, strong, readonly) UIView *contentView;

    包含轮播项目视图的视图。如果您想将它们与轮播项目散布,您可以向此视图添加子视图。如果您希望视图出现在所有轮播项目的前面或后面,您应该将其直接添加到 iCarousel 视图本身。请注意,当应用程序运行时, contentView 中的视图顺序会经常发生且未记录的更改。添加到 contentView 的任何视图都应将其 userInteractionEnabled 属性设置为 NO 以防止与 iCarousel 的触摸事件处理发生冲突。

    @property (nonatomic, assign) CGFloat scrollOffset;

    这是轮播的当前滚动偏移量,是 itemWidth 的倍数。这个值,四舍五入到最接近的整数,是 currentItemIndex 值。您可以使用此值在轮播移动时定位其他屏幕元素。如果您希望以编程方式将轮播滚动到特定偏移量,也可以设置该值。如果您希望禁用内置手势处理并提供您自己的实现,这可能很有用。

    @property (nonatomic, readonly) CGFloat offsetMultiplier;

    这是用户用手指拖动轮播时使用的偏移乘数。它不影响编程滚动或减速速度。对于大多数轮播类型,这默认为 1.0,但对于 CoverFlow 风格的轮播默认为 2.0,以补偿它们的项目间隔更近的事实,因此必须进一步拖动以移动相同的距离。您不能直接设置此属性,但可以通过实现carouselOffsetMultiplier:委托方法来覆盖默认值

    @property (nonatomic, assign) NSInteger currentItemIndex;

    轮播中当前居中项目的索引。设置此属性等效于scrollToItemAtIndex:animated:将动画参数设置为 NO进行调用

    @property (nonatomic, strong, readonly) UIView *currentItemView;

    轮播中当前居中的项目视图。此视图的索引匹配currentItemIndex

    @property (nonatomic, readonly) CGFloat itemWidth;

    轮播中项目的显示宽度(只读)。这是从使用carousel:viewForItemAtIndex:reusingView:dataSource 方法传递给轮播的第一个视图自动派生的您还可以使用carouselItemWidth:委托方法覆盖此值,这将更改为轮播项目分配的空间(但不会调整项目视图的大小或缩放)。

    @property (nonatomic, assign) BOOL centerItemWhenSelected;

    当设置为 YES 时,点击轮播中除与 currentItemIndex 匹配的项目之外的任何项目都会使其平滑地动画到中心。点击当前选定的项目将不起作用。默认为是。

    @property (nonatomic, assign) CGFloat scrollSpeed;

    这是用户用手指轻弹轮播时的滚动速度倍增器。默认为 1.0。

    @property (nonatomic, readonly) CGFloat toggle;

    此属性用于iCarouselTypeCoverFlow2轮播变换。它是公开的,以便您可以使用carousel:itemTransformForOffset:baseTransform:委托方法实现自己的 CoverFlow2 样式变体

    @property (nonatomic, assign) BOOL stopAtItemBoundary;

    默认情况下,轮播将在轻弹时停在确切的项目边界处。如果将此属性设置为 NO,它将自然停止,然后 - 如果 scrollToItemBoundary 设置为 YES - 向后或向前滚动到最近的边界。

    @property (nonatomic, assign) BOOL scrollToItemBoundary;

    默认情况下,当轮播停止移动时,它会自动滚动到最近的项目边界。如果将此属性设置为 NO,则轮播在停止后将不会滚动并停留在它所在的位置,即使它在当前索引上没有完全对齐。例外情况是,如果 wrapping 被禁用并bounces设置为 YES,那么无论此设置如何,如果轮播结束后停止,轮播将自动滚动回第一个或最后一个项目索引。

    @property (nonatomic, assign, getter = isVertical) BOOL vertical;

    此属性切换轮播是在屏幕上水平显示还是垂直显示。所有内置的轮播类型都适用于两个方向。切换到垂直会更改轮播的布局以及屏幕上滑动检测的方向。请注意,自定义轮播变换不受此属性影响,但滑动手势方向仍会受到影响。

    @property (nonatomic, readonly, getter = isDragging) BOOL dragging;

    如果用户已开始滚动轮播但尚未释放它,则返回 YES。

    @property (nonatomic, readonly, getter = isDecelerating) BOOL decelerating;

    如果用户不再拖动轮播,但它仍在移动,则返回 YES。

    @property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;

    如果当前正在以编程方式滚动轮播,则返回 YES。

    @property (nonatomic, assign) BOOL ignorePerpendicularSwipes;

    如果是,则轮播将忽略与轮播方向垂直的滑动手势。所以对于水平轮播,垂直滑动不会被拦截。这意味着您可以在旋转木马项目视图中拥有一个垂直滚动的 scrollView,它仍然可以正常工作。默认为是。

    @property (nonatomic, assign) BOOL clipsToBounds;

    这实际上不是 iCarousel 的属性,而是继承自 UIView。它包含在此处是因为它是一个经常被遗漏的功能。将此设置为 YES 以防止轮播项目视图溢出其边界。您可以通过勾选“剪辑子视图”选项在界面生成器中设置此属性。默认为否。

    @property (nonatomic, assign) CGFloat autoscroll;

    此属性可用于设置轮播以恒定速度滚动。值为 1.0 将以每秒一项的速度向前滚动轮播。自动滚动值可以为正也可以为负,默认为 0.0(固定)。如果用户与轮播交互,自动滚动将停止,并在他们停止时恢复。

    方法

    iCarousel 类具有以下方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (void)scrollToItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将使轮播在指定的项目上居中,无论是立即还是平滑的动画。对于包裹式轮播,轮播将自动确定要滚动的最短(直接或环绕)距离。如果您需要控制滚动方向,或者想要滚动一圈以上,请改用 scrollByNumberOfItems 方法。

    - (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)scrollDuration;

    此方法允许您控制轮播滚动到指定索引所需的时间。

    - (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration;

    此方法允许您将轮播滚动固定距离,以轮播项目宽度为单位。可以为 itemCount 指定正值或负值,具体取决于您希望滚动的方向。iCarousel 优雅地处理边界问题,因此如果您指定的距离大于轮播中项目的数量,滚动将在到达轮播结束时被限制(如果环绕被禁用)或无缝环绕。

    - (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollToItemAtIndex:,但允许您滚动到小数偏移量。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则调用此方法后,轮播将自动滚动到最近的项目索引。反正。

    - (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollByNumberOfItems:,但允许您滚动项目的小数部分。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则无论如何调用此方法后,轮播都会自动滚动到最近的项目索引。

    - (void)reloadData;

    这将从数据源重新加载所有轮播视图并刷新轮播显示。

    - (UIView *)itemViewAtIndex:(NSInteger)index;

    返回具有指定索引的可见项视图。请注意,索引与轮播中的位置有关,而不是在visibleItemViews数组中的位置,这可能会有所不同。传递负索引或大于或等于的索引numberOfItems以检索占位符视图。该方法仅适用于可见的项目视图,如果指定索引处的视图尚未加载,或者索引超出范围,则返回 nil。

    - (NSInteger)indexOfItemView:(UIView *)view;

    轮播中给定项目视图的索引。适用于项目视图和占位符视图,但是占位符视图索引与数据源使用的索引不匹配,并且可能为负数(indexesForVisibleItems有关更多详细信息,请参阅上面的属性)。此方法仅适用于可见的项目视图,并且将为当前未加载的视图返回 NSNotFound。要获取所有当前加载的视图的列表,请使用该visibleItemViews属性。

    - (NSInteger)indexOfItemViewOrSubview:(UIView *)view

    此方法为您提供传递的视图或包含作为参数传递的视图的视图的项目索引。它的工作方式是从传递的视图开始沿着视图层次结构向上移动,直到找到一个项目视图并在轮播中返回其索引。如果未找到当前加载的项目视图,则返回 NSNotFound。此方法对于处理嵌入在项目视图中的控件上的事件非常有用。这允许您将所有项目控件绑定到视图控制器上的单个操作方法,然后确定触发操作的控件与哪个项目相关。您可以在Controls Demo示例项目中看到此技术的示例。

    - (CGFloat)offsetForItemAtIndex:(NSInteger)index;

    itemWidth以中心位置的倍数返回指定项索引的偏移量这与用于计算视图变换和 alpha 的值相同,可用于根据它们在轮播中的位置自定义项目视图。每当carouselDidScroll:调用委托方法时,每个视图的这个值都会发生变化

    - (UIView *)itemViewAtPoint:(CGPoint)point;

    返回轮播边界内指定点的最前面的项目视图。用于实现您自己的点击检测。

    - (void)removeItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将从轮播中删除一个项目。其余项目将滑过以填补空白。请注意,调用此方法时数据源不会自动更新,因此后续调用 reloadData 将恢复已删除的项目。

    - (void)insertItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这会将一个项目插入到轮播中。新的item会从dataSource中请求,所以在调用这个方法之前要确保新的item已经添加到数据源data中,否则会在carousel中得到重复的item,或者其他怪事。

    - (void)reloadItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    此方法将重新加载指定的项目视图。将从数据源请求新项目。如果动画参数为 YES,它将从旧项目视图交叉淡入淡出到新项目视图,否则将立即交换。

    协议

    iCarousel 通过提供两个协议接口 iCarouselDataSource 和 iCarouselDelegate 来遵循 Apple 的数据驱动视图约定。iCarouselDataSource 协议具有以下必需的方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel;

    返回轮播中的项目(视图)数。

    - (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示在轮播中指定索引处的视图。reusingView参数的工作方式类似于 UIPickerView,其中先前显示在轮播中的视图被传递回要回收的方法。如果这个参数不是 nil,你可以设置它的属性并返回它,而不是创建一个新的视图实例,这会稍微提高性能。与 UITableView 不同,没有用于区分不同轮播视图类型的重用标识符,因此如果您的轮播包含多个不同的视图类型,那么您应该忽略此参数并在每次调用该方法时返回一个新视图。您应该确保每次carousel:viewForItemAtIndex:reusingView: 方法被调用时,它要么返回 reusingView 要么返回一个全新的视图实例,而不是维护自己的可回收视图池,因为为不同的轮播项目索引返回同一视图的多个副本可能会导致轮播显示问题。

    iCarouselDataSource 协议有以下可选方法:

    - (NSUInteger)numberOfPlaceholdersInCarousel:(iCarousel *)carousel;

    返回要在轮播中显示的占位符视图的数量。当轮播中的项目数量太少而无法填充轮播宽度,并且您希望在空白空间中显示某些内容时,将使用占位符视图。它们与轮播一起移动并且行为与任何其他轮播项目一样,但它们不计入 numberOfItems 值,并且不能设置为当前选定的项目。启用换行时,占位符会隐藏。占位符出现在轮播项目的两侧。对于 n 个占位符视图,前 n/2 个项目将出现在项目视图的左侧,接下来的 n/2 个项目将出现在右侧。您可以有奇数个占位符,在这种情况下,轮播将是不对称的。

    - (UIView *)carousel:(iCarousel *)carousel placeholderViewAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示为占位符视图的视图。工作方式与carousel:viewForItemAtIndex:reusingView:占位符 reusingViews 与用于常规轮播的 reusingViews 存储在单独的池中,因此如果您的占位符视图与项目视图不同,这不是问题。

    iCarouselDelegate 协议具有以下可选方法:

    - (void)carouselWillBeginScrollingAnimation:(iCarousel *)carousel;

    每当轮播开始动画滚动时,都会调用此方法。这可以在用户完成滚动轮播后以编程方式或自动触发,因为轮播会重新对齐自身。

    - (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel;

    当轮播结束动画滚动时调用此方法。

    - (void)carouselDidScroll:(iCarousel *)carousel;

    每当滚动轮播时都会调用此方法。无论轮播是通过编程还是通过用户交互滚动,它都会被调用。

    - (void)carouselCurrentItemIndexDidChange:(iCarousel *)carousel;

    每当轮播滚动到足以改变 currentItemIndex 属性时,就会调用此方法。无论项目索引是以编程方式更新还是通过用户交互更新,都会调用它。

    - (void)carouselWillBeginDragging:(iCarousel *)carousel;

    当用户开始拖动轮播时调用此方法。如果用户点击/点击轮播,或者轮播以编程方式滚动,它不会触发。

    - (void)carouselDidEndDragging:(iCarousel *)carousel willDecelerate:(BOOL)decelerate;

    当用户停止拖动轮播时调用此方法。willDecelerate 参数指示转盘是否行得足够快以至于它在停止之前需要减速(即当前索引不一定是它将停止的索引),或者它是否会在它所在的位置停止。请注意,即使 willDecelerate 为 NO,轮播仍会自动滚动,直到它与当前索引完全对齐。如果您需要知道它何时完全停止移动,请使用 carouselDidEndScrollingAnimation 委托方法。

    - (void)carouselWillBeginDecelerating:(iCarousel *)carousel;

    当轮播开始减速时调用此方法。它通常会在 carouselDidEndDragging:willDecelerate: 方法之后立即调用,假设 willDecelerate 为 YES。

    - (void)carouselDidEndDecelerating:(iCarousel *)carousel;

    当轮播完成减速时调用此方法,您可以假设此时的 currentItemIndex 是最终停止值。与以前的版本不同,在大多数情况下,轮播现在将准确地停在最终索引位置。唯一的例外是启用了弹跳的非包裹式转盘,如果最终停止位置超出转盘的末端,则转盘将自动滚动,直到它与结束索引完全对齐。为了向后兼容,轮播将始终scrollToItemAtIndex:animated:在完成减速后调用如果您需要确定轮播何时完全停止移动,请使用carouselDidEndScrollingAnimation委托方法。

    - (CGFloat)carouselItemWidth:(iCarousel *)carousel;

    返回轮播中每个项目的宽度 - 即每个项目视图的间距。如果未实现该方法,则默认为carousel:viewForItemAtIndex:reusingView:dataSource 方法返回的第一个项目视图的宽度如果从返回的视图carousel:viewForItemAtIndex:reusingView:不正确(例如,如果视图大小不同,或者在其背景图像中包含影响其大小的投影或外部发光),则此方法应仅用于裁剪或填充项目视图- 如果您只是想要将视图隔开一点,那么最好使用该iCarouselOptionSpacing值。

    - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform;

    此方法可用于为每个轮播视图提供自定义转换。offset 参数是视图与旋转木马中间的距离。当前居中的项目视图的偏移量为 0.0,右侧的偏移值为 1.0,左侧的偏移值为 -1.0,依此类推。要实现线性轮播样式,您只需将偏移值乘以项目宽度并将其用作变换的 x 值。仅当轮播类型为 iCarouselTypeCustom 时才会调用此方法。

    - (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value;

    该方法用于自定义标准轮播类型的参数。通过实施此方法,您可以调整选项,例如圆形转盘中显示的项目数量,或coverflow 转盘中的倾斜量,以及转盘是否应环绕以及是否应在末端淡出等. 对于任何您不想调整的选项,只需返回默认值即可。这些选项的含义在下面的iCarouselOption 值下列出检查选项演示以获取使用此方法的高级示例。

    - (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。如果用户点击当前选定视图中的控件(即作为 UIControl 子类的任何视图),则不会触发此方法。

    - (BOOL)carousel:(iCarousel *)carousel shouldSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。方法的目的是让您有机会忽略轮播上的点击。如果你从方法中返回 YES,或者没有实现它,tap 将正常处理并carousel:didSelectItemAtIndex:调用方法。如果您返回 NO,轮播将忽略点击并继续向上传播视图层次结构。这是防止轮播拦截打算由另一个视图处理的点击事件的好方法。


    检测项目视图上的点击

    在 iOS 上的 iCarousel 中检测点击视图有两种基本方法。第一种方法是简单地使用carousel:didSelectItemAtIndex:委托方法,每次点击项目时都会触发方法。如果您只对点击当前居中的项目感兴趣,您可以将该currentItemIndex属性与此方法的 index 参数进行比较

    或者,如果您想要更多控制,您可以提供 UIButton 或 UIControl 作为项目视图并自己处理触摸交互。有关如何完成此操作的示例,请参阅按钮演示示例项目(不适用于 Mac OS;见下文)。

    您还可以在您的项目视图中嵌套 UIControls,这些将按预期接收触摸(请参阅Controls Demo示例项目以获取示例)。

    如果您希望检测其他类型的交互,例如滑动、双击或长按,最简单的方法是将 UIGestureRecognizer 附加到您的项目视图或其子视图,然后再将其传递给轮播。

    请注意,除了当前选定的项目视图之外,任何项目视图上的点击和手势都将被忽略,除非您将该centerItemWhenSelected属性设置为 NO。

    在 Mac OS 上,目前没有简单的方法可以在 iCarousel 项目视图中嵌入控件。您不能只在项目视图中或在项目视图中提供 NSButton,因为应用于项目视图的转换意味着命中检测无法正常工作。我正在研究可能的解决方案(如果您知道解决此问题的好方法,请与我们联系,或在 github 上 fork 项目)。

    demo及常见问题:https://github.com/nicklockwood/iCarousel

    源码下载:iCarousel-master.zip


    收起阅读 »

    iOS 应用分享平台fir使用遇到的一些坑

    前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分...
    继续阅读 »

    前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分享出来给大家,希望能对你有所帮助。

    首先要确定你们使用平台的需求,我这里有蒲公英(fir同类型网站)对于应用分享需求的介绍


    如果只是小范围的几个人来安装,使用Ad-hoc方式,去一个个添加UDID就好了,好处是使用你自己的免费证书也可以申请。
    如果是想做线下推广,没办法及时获取添加目标UDID的话,最好还是要使用In-house方式,不过装机数量苹果好像还是有一定限制,这个具体政策不太清楚。

    我的目的是给测试团队装机,所以选择Ad-hoc方式做。

    简化下来一共需要三大步
    1 . 在你的Apple Developer 页面的Devices中添加目标的(于我就是“测试团队”)苹果手机UDID。(关于UDID的获取看这里 http://fir.im/udid 这个网址使用苹果手机的Safari浏览器访问)


    在这里点击“+”输入用户的UDID(name是你自己定的,建议起个和此UDID手机拥有者相关的名字,后面会用到),点击下方的注册,会跳转确认注册页面


    确认账号无误后可以点击下方的确定,目标UDID就乖乖加入到你的Devices列表中了😊。

    注意:这里就会有一个坑,我导入的第一个UDID出现这种情况


    你会发现这个缺少了Model:这一项,目前我没有发现是因为什么(隐约赶脚是因为录入这个UDID时,网络或者苹果官网之类的问题😊)。这种账号是无法添加进描述文件的,添加进去也无法识别和使用。

    还有一种情况是你添加了目标UDID,在Devices列表中找不到,再次注册该UDID又会提示它不是有效的,多次尝试无果也只好作罢。

    2 . 在Distribution中添加一个用于测试的描述文件,并在此步骤中添加目标手机到描述文件中。


    在此点击“+”,添加一个新的描述文件。


    选择你需要的方式,我的是Ad-hoc


    然后是选择自己项目


    选择开发者(或团队)


    选择你要添加的目标UDID(此时使用的是你创建Device时的名字)


    给你的描述文件命名(项目中添加Provisioning Profile时使用这个名字)

    creat之后点击下载,描述文件就会下载到电脑。

    这里倒是没有什么坑,就是苹果官网如果访问起来困难,部分页面会不显示你已有的一些资料,会提示要你新建一个项目。如果你确定自己有项目的话,刷新一下就好了。

    3 . 将描述文件添加到Xcode,然后在项目中选择相应的打包选项,生成.ipa文件。然后大功告成,将其上传到fir平台后点击“预览”会自动生成一个带有二维码的网址。(需要使用iphone自带的safari浏览器访问该链接)

    现在可以关掉万恶的苹果官网,来到桌面上,建议先彻底关闭Xcode,然后双击一下你下载下来的描述文件,Xcode会自动打开,此时描述文件就已经添加好了。


    在 Xcode 中点击project图标,在info这个tab下找到configuration设置,里面默认的是debug和release。点击+,选择Duplicate the “Release configuration”,给生成的新东西起个名字,推荐使用ad hoc distribution


    点击targets图标,在build settings这个tab下,找到code signing部分。将Code Signing Identity中的ad hoc distribution证书设置为刚刚导入到 Xcode 中对应测试应用的证书。注意不要改动Debug和Release中的证书。
    在下方的Provisioning Profile中选择你下载下来的描述文件。
    保证target中info这个tab下的bundle indentifier里面有预设值,其必须和provision portal输入匹配。这个很重要,否则将来会出错。


    在Xcode左上角run按钮右侧有一个下拉菜单,选择device或者simulator,点击菜单下方的edit schema。保证Archive中Build Configuration中的值是ad hoc distribution


    配置工作到此结束。点击Product中的Archive,程序开始编译,编译完成后弹出设置框,点选"Export" 然后选"Save for Ad Hoc Develoyment"

    按操作提示就会生成一个.ipa文件。此.ipa可以被安装到之前设置的测试应用设备中。

    然后创建一个fir账号,在其上发布就好了。

    本文借鉴于http://blog.csdn.net/yuanbohx/article/details/9213879
    该博客6楼指出其在文章中的错误,实测6楼所说是正确的。

    链接:https://www.jianshu.com/p/cbda1e434add

    收起阅读 »

    超强的游戏模拟器, 做游戏开发必备 - OpenEmu

    OpenEmuOpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 S...
    继续阅读 »

    OpenEmu

    alt text


    OpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 Sparkle,它用于自动更新。OpenEmu 使用模块化架构,允许使用游戏引擎插件,允许 OpenEmu 支持大量不同的仿真引擎和后端,同时保留熟悉的 macOS 原生前端。

    目前 OpenEmu 可以加载以下游戏引擎作为插件:



    最低要求

    macOS 10.14


    demo及常见问题:https://github.com/OpenEmu/OpenEmu

    源码下载:OpenEmu-master.zip





    收起阅读 »

    Apple 的xcodebuild的扩展!

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。特征xctool是替代品,xcodebuild test它增加了一些额外的功能:更快的并行测试运行。xctool可以选择并行运行所有测...
    继续阅读 »

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。

    特征

    xctool是替代品,xcodebuild test它增加了一些额外的功能:

    • 更快的并行测试运行。

      xctool可以选择并行运行所有测试包,从而显着加快测试运行速度。在 Facebook,通过并行运行,我们看到了 2 倍和 3 倍的加速。

      使用-parallelize带有run-teststest选项来启用。有关详细信息,请参阅并行化测试运行

    • 测试结果的结构化输出。

      xctool将所有测试结果捕获为结构化 JSON 对象。如果您正在构建一个持续集成系统,这意味着您不再需要正则表达式解析xcodebuild输出。

      尝试使用Reporters之一自定义输出或使用该-reporter json-stream选项获取完整的事件流

    • 人性化的 ANSI 颜色输出。

      xcodebuild非常冗长,为每个源文件打印完整的编译命令和输出。默认情况下,xctool仅在出现问题时才详细说明,从而更容易确定问题所在。

    • 用Objective-C编写。

      xctool是用 Objective-C 编写的。Mac OS X 和 iOS 开发人员可以轻松提交新功能并修复他们可能遇到的任何错误,而无需学习新语言。我们非常欢迎拉取请求!

    注意:不推荐使用 xctool 构建项目,并且不会更新以支持 Xcode 的未来版本。我们建议移动到 xcodebuild(使用xcpretty)来满足简单的需求,或者使用xcbuild来满足更多的需求。xctool 将继续支持测试(见上文)。

    要求

    • Xcode 7 或更高版本
    • 您需要安装 Xcode 的命令行工具。从 Xcode,通过Xcode → Preferences → Downloads安装

    安装

    brew install xctool

    xctool 的命令和选项主要是 xcodebuild 的超集。在大多数情况下,您只需将xcodebuildxctool交换,事情就会按预期运行,但输出更具吸引力。

    您始终可以通过以下方式获得帮助和完整的选项列表:

    path/to/xctool.sh -help


    在运行测试之前,您需要构建它们。您可以使用xcodebuild、 xcbuildBuck来做到这一点。

    例如:

    xcodebuild \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-for-testing


    如果您使用 Xcode 7 进行构建,您可以继续使用 xctool 使用 build-tests 构建测试,或者仅使用测试操作来运行测试。

    例如:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-tests

    并行化测试运行

    xctool可以选择并行运行单元测试,从而更好地利用其他空闲的 CPU 内核。在 Facebook,通过并行化我们的测试运行,我们已经看到了 2 倍和 3 倍的收益。

    要允许测试包同时运行,请使用以下-parallelize 选项:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    run-tests -parallelize


    常见问题及demo下载:https://github.com/facebookarchive/xctool

    源码下载:xctool-master.zip



    收起阅读 »

    DKNightVersion 的实现 --- 如何为 iOS 应用添加夜间模式

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。在正式介绍目前版本的实现之前,我会先简单介绍...
    继续阅读 »

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。

    其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。

    在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。

    我们会以对 backgroundColor 为例说明整个框架的工作原理。

    方法调剂的版本

    如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。

    其核心思路就是使用方法调剂修改 backgroundColor 的存取方法。

    使用 nightBackgroundColor

    在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在分类中添加 nightBackgroundColor 属性,并且使用方法调剂改变 backgroundColor 的 setter 方法。

    在当前主题为 DKThemeVersionNormal 时,将颜色保存至 normalBackgroundColor 中,然后再调用原 backgroundColor 的 setter 方法,更新视图的颜色。

    DKNightVersionManager

    这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。

    整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要工作就是负责改变应用的主题、并在主题改变时通知其它视图更新颜色:

    - (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {
    if ([object respondsToSelector:@selector(changeColor)]) {
    [object changeColor];
    }
    if ([object respondsToSelector:@selector(subviews)]) {
    if (![object subviews]) {
    // Basic case, do nothing.
    return;
    } else {
    for (id subview in [object subviews]) {
    // recursive darken all the subviews of current view.
    [self changeColor:subview];
    if ([subview respondsToSelector:@selector(changeColor)]) {
    [subview changeColor];
    }
    }
    }
    }
    }

    如果主题更新,那么就会递归地调用 changeColor 方法,刷新全部的视图颜色,而这个方法的实现比较简单:

    - (void)changeColor {
    if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
    self.backgroundColor = self.normalBackgroundColor;
    } else {
    self.backgroundColor = self.nightBackgroundColor;
    }
    }

    上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:

    1、在高速滚动的 scrollView 上面来回切换夜间模式,会出现颜色错乱的问题
    2、由于对 backgroundColor 属性进行不合适的方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
    3、无法适配第三方 UI 控件

    使用色表的版本

    为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 nightBackgroundColor 的使用,并且重新设计底层的实现,转而使用更为稳定、安全的方法实现夜间模式,先看一下效果图:

    <em>新的实现不仅能够支持夜间模式,而且能够支持多主题。</em>

    DKColorPicker

    与上一个版本实现上的不同,在 2.0 中删除了全部的 nightBackgroundColor,使用一个名为 dk_backgroundColorPicker 的属性取代它。

    @property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;

    这个属性其实就是一个 block,它接收参数 DKThemeVersion *themeVersion,但是会返回一个 UIColor *:

    在第一次传入 picker 或者每次主题改变时,都会将当前主题 DKThemeVersion 传入 picker 并执行,然后,将得到的 UIColor 赋值给对应的属性 backgroundColor 更新视图颜色。

    typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);

    比如下面使用 DKColorPickerWithRGB 创建一个临时的 DKColorPicker:

    1、在 DKThemeVersionNormal 时返回 0xffffff
    2、在 DKThemeVersionNight 时返回 0x343434
    3、在自定义的主题下返回 0xfafafa (这里的顺序与色表中主题的顺序有关)

    cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);

    同时,每一个对象还持有一个 pickers 数组,来存储自己的全部 DKColorPicker:

    @interface NSObject ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    在第一次使用这个属性时,当前对象注册为 DKNightVersionThemeChangingNotificaiton 通知的观察者。

    在每次收到通知时,都会调用 night_update 方法,将当前主题传入 DKColorPicker,并再次执行,并将结果传入对应的属性 [self performSelector:sel withObject:result]。

    - (void)night_updateColor {
    [self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull selector, DKColorPicker _Nonnull picker, BOOL * _Nonnull stop) {
    SEL sel = NSSelectorFromString(selector);
    id result = picker(self.dk_manager.themeVersion);
    [UIView animateWithDuration:DKNightVersionAnimationDuration
    animations:^{
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:sel withObject:result];
    #pragma clang diagnostic pop
    }];
    }];
    }

    也就是说,在每次改变主题的时候,都会发出通知。

    DKColorTable

    虽然我们在上面临时创建了一些 DKColorPicker。不过在 DKNightVersion 中,我更推荐使用色表,来减少相同的 DKColorPicker 的创建,并且能够更好地管理整个应用中的颜色:

    NORMAL   NIGHT    RED
    #ffffff #343434 #fafafa BG
    #aaaaaa #313131 #aaaaaa SEP
    #0000ff #ffffff #fa0000 TINT
    #000000 #ffffff #000000 TEXT
    #ffffff #444444 #ffffff BAR

    上面就是默认色表文件 DKColorTable.txt 中的内容,其中,第一行表示主题,NORMAL 主题必须存在,而且必须为第一列,而最右面的 BG、SEP 就是对应 DKColorPicker 的 key。

    self.tableView.dk_backgroundColorPicker =  DKColorPickerWithKey(BG);

    在使用时,上面的代码就相当于返回了一个在 NORMAL 时返回 #ffffff、NIGHT 时返回 #343434 以及 RED 时返回 #fafafa 的 DKColorPicker。

    pickerify

    虽然说,我们使用色表以及 DKColorPicker 解决了,但是,到目前为止我们还没有解决第三方框架的问题。

    比如我们使用了某个第三方框架,或者自己添加了某个 color 属性,比如说:

    @interface DKView ()

    @property (nonatomic, strong) UIColor *weirdColor;

    @end

    weirdColor 并没有对应的 DKColorPicker,但是,我们可以通过 pickerify 在想要使用 dk_weirdColorPicker 的地方生成这个对应的 picker:

    @pickerify(DKView, weirdColor);

    然后,我们就可以使用 dk_weirdColorPicker 属性了:

    view.dk_weirdColorPicker = DKColorPickerWithKey(BG);

    pickerify 其实是一个宏:

    #define pickerify(KLASS, PROPERTY) interface \
    KLASS (Night) \
    @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \
    @end \
    @interface \
    KLASS () \
    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers; \
    @end \
    @implementation \
    KLASS (Night) \
    - (DKColorPicker)dk_ ## PROPERTY ## Picker { \
    return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \
    } \
    - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \
    objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \
    [self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\
    [self.pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \
    } \
    @end

    这个宏根据传入的类和属性名,为我们生成了对应 picker 的存取方法,它也可以说是一种元编程的手段。

    这里生成的 setter 方法不是标准意义上的驼峰命名法 dk_setweirdColorPicker:,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。

    嵌入式 Ruby
    由于框架中很多的代码,都是重复的,所以在这里使用了嵌入式 Ruby 模板来生成对应的文件 color.m.irb:

    //
    // <%= klass.name %>+Night.m
    // <%= klass.name %>+Night
    //
    // Copyright (c) 2015 Draveness. All rights reserved.
    //
    // These files are generated by ruby script, if you want to modify code
    // in this file, you are supposed to update the ruby code, run it and
    // test it. And finally open a pull request.

    #import "<%= klass.name %>+Night.h"
    #import "DKNightVersionManager.h"
    #import <objc/runtime.h>

    @interface <%= klass.name %> ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    @implementation <%= klass.name %> (Night)

    <% klass.properties.each do |property| %><%= """
    - (DKColorPicker)dk_#{property.name}Picker {
    return objc_getAssociatedObject(self, @selector(dk_#{property.name}Picker));
    }

    - (void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker {
    objc_setAssociatedObject(self, @selector(dk_#{property.name}Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
    self.#{property.name} = picker(self.dk_manager.themeVersion);
    [self.pickers setValue:[picker copy] forKey:@\"#{property.setter}\"];
    }
    """ %><% end %>

    @end

    这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 generator 文件夹,其中包含了代码生成器的全部代码。

    小结

    如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 README 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,AFNetworking、 BlocksKit 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。

    Git仓库地址

    转自:https://draveness.me/night/

    收起阅读 »

    iOS-单元测试汇总

    前言:对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。我看...
    继续阅读 »

    前言:
    对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。

    我看了几篇单元测试的文章,其中写到单元测试多数用于:

    1.调试接口是否正常使用。比如要测试一个网络接口,通常每次都要重新启动,经过繁复的操作之后,才能测试到网络接口。要是用单元测试,就可以直接测试那个方法,相对方便很多。

    2.比如由于修改较多,想测试分享功能是否正常,(而不是重新启动程序,进入到分享界面,点击分享,填写分享内容。),在单元测试通过了,直接用到相应的地方。

    3.自动发布、自动测试(特别在一些大的项目,以防止程序被误改或引起新的问题)。
    4.用户注册/登陆等

    了解一下单元测试:
    单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

    通常来说,程序员每修改一次代码就会修改某个单元,那我们就可以对这个单元做修改的验证(单元测试),在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书(产品需求)要求的工作目标,而且没有程序错误。

    每个理想的测试案例独立于其它case,测试时需隔离模块。单元测试通常由软件开发人员编写,用于确保所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是手动的,或是构建自动化的一部分。

    单元测试允许程序员在未来重构代码,且确保模块依然工作正确。这个过程是为所有方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。

    可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。在连续的单元测试环境,通过其固有的持续维护工作,单元测试可以延续用于准确反映当任何变更发生时可执行程序和代码的表现。借助于上述开发实践和单元测试的覆盖,可以总是维持准确性。

    了解一下单元测试目的:
    保证代码的质量 (帮助你编写高质量代码、减少bu)
    代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,尤其包含了许多单元分支的情况下,单元测试可以保证代码的行为和结果与我们的预期和需求一致。在测试某段代码的行为是否和你的期望一致时,你需要确认,在任何情况下,这段代码是否都和你的期望一致,譬如参数可能为空,可能的异步操作等。

    有一部分bug的原因是开发人员在编写工作代码的时候没有考虑到某些case或者边际条件。造成这种问题的原因很多,其中很重要的一个原因是我们对工作代码所要完成的功能思考不足,而编写单元测试,特别是先写单元测试再写工作代码就可以帮助开发人员思考编写的代码到底要实现哪些功能。例如实现一个简单的用户注册功能的业务类方法,用单元测试再写工作代码的方式来工作的话开发人员就会先考虑各种场景相关,例如正常注册、用户名重复、没有满足必要的填写内容......等等,之后就会编写相关的测试用例。编写单元测试代码的过程就是促使开发人员思考工作代码实现内容和逻辑的过程,之后实现工作代码的时候,开发人员思路会更清晰,实现代码的质量也会有相应的提升。

    保证代码的可维护性 (提升代码的反馈速度,减少重复工作,保证你最后的代码修改不会破坏之前代码的功能)
    保证原有单元测试正确的情况下,无论如何修改单元内部代码,测试的结果应该是正确的,且修改后不会影响到其他的模块。

    开发人员实现某个功能或者修补了某个bug,如果有相应的单元测试支持的话,开发人员可以马上通过运行单元测试来验证之前完成的代码是否正确,而不需要反复通过编译运行simulator、等待应用启动、通过输入数据等繁琐的步骤来验证所完成的功能。用单元测试代码来验证代码和通过发布应用以人工的方式来验证代码这两者的效率差很多,所以单元测试其实还能节约人力成本。

    项目越做越大,代码越来越多,特别涉及到一些公用接口之类的代码或是底层的基础库,谁也不敢保证这次修改的代码不会破坏之前的功能,所以与此相关的需求会被搁置或推迟,由于不敢改进代码,代码也变得越来越难以维护,质量也越来越差。而单元测试就是解决这种问题的很好方法(不敢说最好的)。由于代码的历史功能都有相应的单元测试保证,修改了某些代码以后,通过运行相关的单元测试就可以验证出新调整的功能是否有影响到之前的功能。当然要实现到这种程度需要很大的付出,不但要能够达到比较高的测试覆盖率,而且单元测试代码的编写质量也要有保证。

    保证代码的可扩展性
    为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行,说明代码的依赖性很高。

    了解一下单元测试的本质:
    是一种验证行为
    单元测试在开发前期检验了代码逻辑的正确性,开发后期,无论是修改代码内部抑或重构,测试的结果为这一切提供了可量化的保障。

    是一种设计行为
    为了可进行单元测试,尤其是先写单元测试(TDD),我们将从调用者思考,从接口上思考,我们必须把程序单元设计成接口功能划分清晰的,易于测试的,且与外部模块耦合性尽可能小。

    是一种快速回归的方式
    在原代码基础上开发及修改功能时,单元测试是一种快捷,可靠的回归。

    除了那些大拿们编写的代码,我相信很多易于维护、设计良好的代码都是通过不断的重构才得到的。虽然说单元测试本身不能直接改进生产代码的质量,但它为生产代码提供了“安全网”,让开发人员可以勇敢地改进代码,从而让代码的clean和beautiful不再是梦想。

    是程序优良的文档
    从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。

    由于给代码写很多单元测试,相当于给代码加上了规格说明书,开发人员通过读单元测试代码也能够帮助开发人员理解现有代码。很有Open Source的项目(如,AFNetworking, FMDB,喵神的VVDoucment等)都有相当量的单元测试代码,通过读这些测试代码会有助于理解生产源代码。

    两种测试思想
      测试驱动开发(Test-driven development,TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。

    行为驱动开发(Behavior-driven development,BDD)是一种敏捷软件开发的技术,BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了 测试驱动开发方法(TDD)。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。

    在iOS单元测试框架中,kiwi是BDD的代表。

    介绍
    OCUnit(即用XCTest进行测试)其实就是苹果自带的测试框架。

    GHUnit是一个可视化的测试框架。
    有了它,你可以点击APP来决定测试哪个方法,并且可以点击查看测试结果等。

    OCMock就是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法A,方法A里面调用到了方法B,而且方法B是有参数传入,但又不是方法A所提供。这时候,你可以使用OCMock来模拟方法B返回的值。(在不影响测试的情况下,就可以这样去模拟。)除了这些,在没有网络的情况下,也可以通过OCMock模拟返回的数据。

    UITests就是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UITests就可以帮助解决这个问题了。

    案例 1

    简单的单元测试
    1-1 创建一个新的项目


    1-2点开测试文件,进入到这个类

    setUp       :每个测试方法调用前执行
    tearDown :每个测试方法调用后执行
    testExample :是测试方法,和我们新建的没有差别。
    测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法
    测试方法的执行顺序: 字典序排序。
    快捷键:Command + U进行单元测试,这个快捷键是全部测试。


    1-3在testExample方法中输入如下:

    NSLog(@"自定义测试testExample");
    int a= 3;
    XCTAssertTrue(a == 0,"a 不能等于 0");


    备注:红色的叉子:代表测试未通过。绿色叉子:代表测试通过。

    案例 2

    iOS-Main - 单元测试 &基本体验

    案例 3
    进行网络请求的测试
    使用CocoaPods安装AFNetworking和STAlertView(CocoaPods安装和使用教程 )
    Pofile:

    platform :ios, '7.0'
    target 'UnitTestDemoTests' do
    pod 'AFNetworking', '~> 2.5.0'
    pod 'STAlertView', '~> 1.0.0'
    end
    target 'UnitTestDemoTestsTests' do
    pod 'AFNetworking', '~> 2.5.0'
    pod 'STAlertView', '~> 1.0.0'
    end

    iOS9的http安全问题:现在进行异步请求的网络测试,由于测试方法主线程执行完就会结束,所以需要设置一下,否则没法查看异步返回结果。
    也可以在方法结束前设置等待,调回回来的时候再让它继续执行。(另一种异步函数的单元测试)定义宏如下:

    //waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。
    #define WAIT do {\
    [self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\
    [self waitForExpectationsWithTimeout:30 handler:nil];\
    } while (0);

    #define NOTIFY \
    [[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil];

    增加测试方法:

    -(void)testRequest{
    // 1.获得请求管理者
    AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
    mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil];

    // 2.发送GET请求
    [mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"responseObject:%@",responseObject);
    XCTAssertNotNil(responseObject, @"返回出错");
    NOTIFY //继续执行
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"error:%@",error);
    XCTAssertNil(error, @"请求出错");
    NOTIFY //继续执行
    }];
    WAIT //暂停
    }

    有时候我们想测试一下整个流程是否可以跑通,比如获取验证码、登录、上传头像,查询个人资料。其实只要输入验证码就可以完成整个测试。这时候就需要用到输入框了,以便程序继续执行。使用了一个第三方的弹出输入框STAlertView,前面已经设置。
    STAlertView的使用方法:

    - (void)testAlertView
    {

    self.stAlertView = [[STAlertView alloc]initWithTitle:@"验证码" message:nil textFieldHint:@"请输入手机验证码" textFieldValue:nil cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancelButtonBlock:^{
    //点击取消返回后执行
    [self testAlertViewCancel];
    NOTIFY //继续执行
    } otherButtonBlock:^(NSString *b) {
    //点击确定后执行
    [self alertViewComfirm:b];
    NOTIFY //继续执行
    }];

    [self.stAlertView show];
    WAIT //设置等待时间
    }

    案例 4
    测试的执行顺序


    通过上述测试得出结论:
    可以看到无论我们怎样调换test方法的书写顺序,其测试顺序都是不变的。
    目前初步的结论:测试方法执行的顺序与方法名中test后面的字符大小有关,小者优先,例如testA,testB1,testB2三个方法相继执行。
    案例 5
    Xcode集成了对单元测试的支持,XCode4.x集成的是OCUnit,到了XCode5.x时代就升级为了XCTest,XCode7.x时代XCtest还可以进行UI测试。下面我们简单介绍下XCTest的使用。

    在xcode新建项目中,默认会建一个单元测试的target,并建立一个继承于XCTestCase的测试用例类


     本例实现了一个个税计算方法,在测试用例中测试输入后输出是否符合结果。
    创建一个名为ASRevenueBL的 .h .m文件,如下面所示:


    ASRevenueBL.h

    #import <Foundation/Foundation.h>
    @interface ASRevenueBL : NSObject
    - (double)calculate:(double)revenue;
    @end

    ASRevenueBL.m

    import "ASRevenueBL.h"

    #define baseNum 3500.0 // 起征点

    @implementation ASRevenueBL

    /*
    * method:传入收入计算税值
    * revenue:收入
    */
    - (double)calculate:(double)revenue
    {
    double tax = 0.0; // 税
    // 应纳税所得额 = 工资收入金额 - 各项社会保险费 - 起征点(3500元)
    // 应纳税额 = 应纳税所得额 x 税率 - 速算扣除数
    double dbTaxRevenue = revenue - baseNum;
    if(dbTaxRevenue <= 1500){
    tax = dbTaxRevenue * 0.03;
    } else if (dbTaxRevenue > 1500 && dbTaxRevenue <= 4500){
    tax = dbTaxRevenue *0.1 -105;
    } else if (dbTaxRevenue > 4500 && dbTaxRevenue <= 9000){
    tax = dbTaxRevenue * 0.2 - 555;
    }else if (dbTaxRevenue > 9000 && dbTaxRevenue <= 35000) {
    tax = dbTaxRevenue * 0.25 - 1005;
    } else if (dbTaxRevenue > 35000 && dbTaxRevenue <= 55000) {
    tax = dbTaxRevenue * 0.3 - 2755;
    } else if (dbTaxRevenue > 55000 && dbTaxRevenue <= 80000) {
    tax = dbTaxRevenue * 0.35 - 5505;
    } else if (dbTaxRevenue > 80000) {
    tax = dbTaxRevenue * 0.45 - 13505;
    }
    return tax;
    }

    导入测试方法所在的类的头文件,并创建一个类,在测试方法调用前,初始化类对象,测试完毕后,将对象置nil,其方法测试如下方测试代码:

    #import <XCTest/XCTest.h>
    #import "ASRevenueBL.h"

    @interface UnitTestsTwoTests : XCTestCase
    @property (nonatomic, strong) ASRevenueBL *revenueBL;
    @end

    @implementation UnitTestsTwoTests

    - (void)setUp {
    [super setUp];

    self.revenueBL = [[ASRevenueBL alloc] init];
    }

    - (void)tearDown {
    self.revenueBL = nil;
    [super tearDown];
    }

    - (void)testLevel1
    {
    double revenue = 5000;
    double tax = [self.revenueBL calculate:revenue];
    XCTAssertEqual(tax, 45.0,@"测试案例1失败");
    XCTAssertTrue(tax == 45.0);
    }

    - (void)testLevel2 {
    XCTestExpectation *exp = [self expectationWithDescription:@"超时"];
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{
    double revenue = 1500;
    double tax = [self.revenueBL calculate:revenue];
    sleep(1);
    NSLog(@"%f",tax);
    XCTAssertEqual(tax, 45, @"用例2测试失败");
    [exp fulfill]; // exp结束
    }];

    [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
    if (error) {
    NSLog(@"Timeout Error: %@", error);
    }
    }];
    }


    - (void)testExample {

    }

    - (void)testPerformanceExample {

    [self measureBlock:^{
    for (int a = 0; a<10; a+=a) {
    NSLog(@"%zd", a);
    }
    }];

    }
    @end


    testLevel1通过revenueBL计算出来的tax与预期相同,测试通过;testLevel2通过revenueBL计算出来的tax与预期不同,测试不通过,反映出了程序一些逻辑漏洞;testPerformanceExample中的平均执行时间比基准值低,测试通过。

    案例 6 命令行测试
    在命令行中也可以启动测试,便于持续集成。

    Assuner$ cd Desktop/
    Desktop Assuner$ cd ASUnitTestFirstDemo/
    ASUnitTestFirstDemo Assuner$ xcodebuild test -project ASUnitTestFirstDemo.xcodeproj -scheme ASUnitTestFirstDemo -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
    // 可以有多个destination

    结果

    Test Suite 'All tests' started at 2017-09-11 11:12:16.348
    Test Suite 'ASUnitTestFirstDemoTests.xctest' started at 2017-09-11 11:12:16.349
    Test Suite 'ASUnitTestFirstDemoTests' started at 2017-09-11 11:12:16.349
    Test Case '-[ASUnitTestFirstDemoTests testLevel1]' started.
    Test Case '-[ASUnitTestFirstDemoTests testLevel1]' passed (0.001 seconds).
    Test Case '-[ASUnitTestFirstDemoTests testLevel2]' started.
    /Users/liyongguang-eleme-iOS-Development/Desktop/ASUnitTestFirstDemo/ASUnitTestFirstDemoTests/ASUnitTestFirstDemoTests.m:46: error: -[ASUnitTestFirstDemoTests testLevel2] : ((tax) equal to (45.0)) failed: ("-60") is not equal to ("45") - 用例2测试失败
    Test Case '-[ASUnitTestFirstDemoTests testLevel2]' failed (1.007 seconds).
    Test Suite 'ASUnitTestFirstDemoTests' failed at 2017-09-11 11:12:17.358.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.009) seconds
    Test Suite 'ASUnitTestFirstDemoTests.xctest' failed at 2017-09-11 11:12:17.359.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.010) seconds
    Test Suite 'All tests' failed at 2017-09-11 11:12:17.360.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.012) seconds
    Failing tests:
    -[ASUnitTestFirstDemoTests testLevel2]
    ** TEST FAILED **

    如果是workspace

    xcodebuild -workspace ASKiwiTest.xcworkspace -scheme ASKiwiTest-Example -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' test

    每个test方法都会跑一遍,并给出结果描述。

    案例 7 代码的执行时间测试-(性能测试)
    性能测试主要使用 measureBlock 方法 ,用于测试一组方法的执行时间,通过设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。


    假如直接执行方法,因为block中没有内容,所以方法的执行时间为0.0s,如果我们把baseline设成0.05,偏差10%,是可以通过的测试的。但是如果设置如果我们把baseline为1,偏差10%,那测试会失败,因为不满足条件。
    如上图所示,这个方法是用来测试block内代码的执行时间的,我们可以通过打印很清楚的看到它其实执行了10次,用处也很宽广,比如想测试shenfenzheng的识别时间,请求的时间,转模型的速度等等都可以通过它来测试,这里只是举个简单的例子.

    我们可以看下打印发现他确实是执行了十次.


    再来看看左边的执行代码相关信息,这里由于打印"1"执行的太快无法看出效果,所以我将测试内容换成了使用for循环打印1-9999,看看他们的执行时间.


    可以很清楚的看到,10次的平均时间是1.382秒,第一次时间是1.85秒,并且可以看到第一次执行时间超过了平均时间33%,这里的测试结果都是和机器性能有关系的.

    案例 8 登陆模块测试


    案例 9 加法测试

    - (void)testExample {
    //设置变量和设置预期值
    NSUInteger a = 10;NSUInteger b = 15;
    NSUInteger expected = 24;
    //执行方法得到实际值
    NSUInteger actual = [self add:a b:b];
    //断言判定实际值和预期是否符合
    XCTAssertEqual(expected, actual,@"add方法错误!");
    }

    -(NSUInteger)add:(NSUInteger)a b:(NSUInteger)b{
    return a+b;
    }

    从这也能看出一个测试用例比较规范的写法,1:定义变量和预期,2:执行方法得到实际值,3:断言

    案例 10 代码来自于AFNetworking,用于测试backgroundImageForState方法

    - (void)testThatBackgroundImageChanges {
    XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);
    NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIButton * _Nonnull button, NSDictionary<NSString *,id> * _Nullable bindings) {
    return [button backgroundImageForState:UIControlStateNormal] != nil;
    }];

    [self expectationForPredicate:predicate
    evaluatedWithObject:self.button
    handler:nil];
    [self waitForExpectationsWithTimeout:20 handler:nil];
    }

    利用谓词计算,button是否正确的获得了backgroundImage,如果正确20秒内正确获得则通过测试,否则失败。

    expectationForNotification 方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。

    - (void)testAsynExample1 {
    [self expectationForNotification:(@"监听通知的名称xxx") object:nil handler:nil];
    [[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx" object:nil];

    //设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:3 handler:nil];
    }

    这个例子也可以用expectationWithDescription实现,只是多些很多代码而已,但是这个可以帮助你更好的理解 expectationForNotification 方法和 expectationWithDescription的区别。同理,expectationForPredicate方法也可以使用expectationWithDescription实现。

    func testAsynExample1() {
    let expectation = expectationWithDescription("监听通知的名称xxx")
    let sub = NSNotificationCenter.defaultCenter().addObserverForName("监听通知的名称xxx", object: nil, queue: nil) { (not) -> Void in
    expectation.fulfill()
    }

    NSNotificationCenter.defaultCenter().postNotificationName("监听通知的名称xxx", object: nil)
    waitForExpectationsWithTimeout(1, handler: nil)
    NSNotificationCenter.defaultCenter().removeObserver(sub)
    }

    XCTest常见的断言

    XCTFail(format...)  生成一个失败的测试
    XCTAssertNil(a1,format...)为空判断, a1为空时通过,反之不通过;
    XCTAssertNotNil(a1,format...) 不为空判断,a1不为空时通过,反之不通过;
    XCTAssert(expression,format...) 当expression求值为true时通过;
    XCTAssertTrue(expression,format...) 当expression求值为true时通过;
    XCTAssertFalse(expression,format...) 当expression求值为False时通过;
    XCTAssertEqualObjects(a1, a2,format...) 判断相等 [a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
    XCTAssertNotEqualObjects(a1, a2,format...) 判断不等,[a1 isEqual:a2]值为False时通过;
    XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
    XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
    XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
    XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
    XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态)
    XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
    XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
    XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
    XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

    特别注意下XCTAssertEqualObjects和XCTAssertEqual。 XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES

    备注:

    1.关于私有方法的测试,只能通过扩展来实现

    2.关于case的方法名字,一定要以test开头并注意驼峰命名法,且不能加入参数。

    3.单元测试类继承自XCTestCase,他有一些重要的方法,其中最重要的有3个,setUp ,tearDown,measureBlock.

    4.md + 5切换到测试选项卡后会看到很多小箭头,点击可以单独或整体测试.

    5.cmd + U运行整个单元测试

    6.使用pod的项目中,在XC测试框架中测试内容包括第三方包时,需要手动去设置Header Search Paths才能找到头文件 ,还需要设置test target的PODS_ROOT。

    7.xcode7要使用真机做跑测试时,证书必须配对,否则会报错exc_breakpoint错误

    链接:https://www.jianshu.com/p/4001e06b150e

    收起阅读 »

    OpenGLES/(GLKit/CoreAnimation正方体的渲染+旋转)

    一.Hello--OpenGLES                 OpenGL可用于渲染...
    继续阅读 »

    一.Hello--OpenGLES 

                    OpenGL可用于渲染2D和3D图像,是一个多用途的开源图形库。OpenGL设计用来将函数命令转换成图形命令,发送到GPU中。GPU正是被设计用来处理图形命令的,所以OpenGL的绘制非常高效。

                    OpenGLES是OpenGL的简化版本,抛弃了冗余的文件及命令,使之专用于嵌入式设备。OpenGLES使得移动APP能充分利用GPU的强大运算能力。iOS设备上的GPU能执行更精确的2D和3D绘制,以及更加复杂的针对每个像素的图形脚本(shader)计算。⽀持的平台: iOS, Andriod , BlackBerry ,bada ,Linux ,Windows。

    1.1准备工程

                    iOS新建工程,@interface ViewController : UIViewController改成-->@interface ViewController : GLKViewController,.h文件导入#import,.m导入#import#import,最后在Main.storyboard中将view改成GLVIew






    GLView

    1.2EAGLContext(OpenGL 上下文)

                    EAGLContext对象管理着OpenGLES的渲染context,即所有绘制的状态,命令及资源信息,并控制GPU去执行渲染运算。 绘制如textures及renderbuffers的过程,是由一个与context绑定的EAGLSharegroup对象来管理的。当初始化一个EAGLContext对象的时候,可选择新建一个sharegroup,或者使用已有的,这一点我们往往采用系统默认即可。在绘制到context之前,我们要先绑定一个完整的framebuffer对象到context中。






    Hello-OpenGLES

                    1)初始化上写文:context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];(参数知识选择版本)

                    2)设置当前上下文:[EAGLContext setCurrentContext:context];

                    3)GLView绑定上下文:GLKView *view =(GLKView *) self.view;  view.context=context;

                    注意:在使用GLview中,我们必须实现它的协议:GLKViewDelegate--->- (void)glkView:(GLKView*)viewdrawInRect:(CGRect)rect,GLKView对象使其OpenGL ES上下文成为当前上下文,并将其framebuffer绑定为OpenGL ES呈现命令的目标。然后,委托方法应该绘制视图的内容。我们给GLview设置颜色,看一下效果:glClearColor(1, 0, 0, 1.0);






    Hello--OpenGlES

    二.显示图片







    加载图片

    2.1设置顶点坐标/纹理坐标

                     在OpenGl中我们显示一张图片,首先我们设置顶点数组,绑定纹理,在OpenGLES中,我们一样这么设置:

                                    GLfloatvertexData[] = {

                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            0.5,0.5,  0.0f,    1.0f,1.0f,//右上

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上


                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上

                                                                            -0.5, -0.5,0.0f,  0.0f,0.0f,//左下

                                                                        };

                      在OpenGL中我们提到了图形绘制是点,线,三角形,正方形由两个三角形组成,就是六个顶点,而我们知道,纹理的坐标范围是(0,1),其原点是在左下角,所以坐标(0,0)是原点,右上角(1,1);

    2.2开辟顶点缓存区并把数据存到缓中区

                    (1).创建顶点缓存区标识符ID

                            GLuint  bufferID;

                            glGenBuffers(1, &bufferID);(分配纹理)

                    (2).绑定顶点缓存区.(明确作用)

                            glBindBuffer(GL_ARRAY_BUFFER, bufferID);

                    (3).将顶点数组的数据copy到顶点缓存区中(GPU显存中)

                            glBufferData(GL_ARRAY_BUFFER,sizeof(vertexData), vertexData,GL_STATIC_DRAW);

                    (4).打开读取通道.

                            1)顶点坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribPosition);

                                glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 0);

                            2)纹理坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

                                glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 3);

    特别说明:

                        (1)在iOS中, 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的.意味着,顶点数据在着色器端(服务端)是不可用的. 即使你已经使用glBufferData方法,将顶点数据从内存拷贝到顶点缓存区中(GPU显存中).所以, 必须由glEnableVertexAttribArray 方法打开通道.指定访问属性.才能让顶点着色器能够访问到从CPU复制到GPU的数据.

                         注意: 数据在GPU端是否可见,即,着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。

                        (2)方法简介

                            glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)    功能: 上传顶点数据到显

    存的方法(设置合适的方式从buffer里面读取数据)

             参数列表:

                            index,指定要修改的顶点属性的索引值,例如 size, 每次读取数量。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a),纹理则是2个,type,指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。 normalized,指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)stride,指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0 ,ptr指定一个指针,指向数组中第一个顶点属性的第一个组件。初始值为0







    参数流程说明

    2.2获取纹理







    纹理

                    1)路径:NSString *filePath = [[NSBundle mainBundle]pathForResource:@"kunkun" ofType:@"jpg"];

                    2)参数:NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];

                                    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];

                    说明:纹理坐标原点是左下角,但是图片显示原点应该是左上角.我们要设置图片绘制从左上角开始绘制GLKTextureLoaderOriginBottomLeft;

                    3)cEffect:你可以把它理解成UIimageVIew,用于显示图片的控件,iOS提供GLKBaseEffect 完成着色器工作(顶点/片元)

                                cEffect = [[GLKBaseEffect alloc]init];

                                cEffect.texture2d0.enabled = GL_TRUE;

                                cEffect.texture2d0.name= textureInfo.name;

                    最后在GLVIew的delegate中:

                                                    1.清除颜色缓冲区

                                                    glClear(GL_COLOR_BUFFER_BIT);

                                                    2.准备绘制

                                                    [cEffect prepareToDraw];

                                                    3.开始绘制

                                                    glDrawArrays(GL_TRIANGLES, 0, 6);







    效果

    三.OpenGLES绘制立方体

            在OpenGLES绘制立方体,相当于绘制六个面,十二个三角形,60个数据(当然你在图元连接方式那里可以选择平面,GL_TRIANGLE_FAN,这样就会少设置一点数据,这里我选择GL_TRIANGLES)

    GLfloatvertexData[] = {

            //第一个面

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  -0.5f,    1.0f,1.0f,//右上

            -0.5,0.5, -0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5, -0.5f,  0.0f,0.0f,//左下

            0.5, -0.5,0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  0.5f,    1.0f,1.0f,//右上

            -0.5,0.5,0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5,0.5f,  0.0f,0.0f,//左下

    //

            //2

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5, -0.5,0.5f,    0.0f,1.0f,//右下

            -0.5, -0.5, -0.5f,    0.0f,0.0f,//右下

            0.5,0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            //3

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            0.5,0.5,0.5f,    0.0f,1.0f,//右下

            0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            -0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            -0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

        };







    效果


    四。CoreAnimation正方体的大体原理就是一个VIew上放六个imageVIew,并设置imageVIew旋转组成一个立方体,一共6个,最后添加定时器控制view的layer转动,达到效果,因为比较简单,这里不做展示



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/035061d80d5c
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    OpenGl纹理相关常用API

    一.原始图像数据1.像素包装:                    图像存储空间=图像的宽度*图像的高度*每个像素的字节数二.相关函数(加粗部分表示常用)2....
    继续阅读 »

    一.原始图像数据

    1.像素包装:

                        图像存储空间=图像的宽度*图像的高度*每个像素的字节数

    二.相关函数(加粗部分表示常用)

    2.1  改变像素存储方式----->void glPixelStorei(GLenum pname,GLint param);

            恢复像素存储⽅式----->void glPixelStoref(GLenum pname,GLfloat param);

    参数说明:

                    //参数1:GL_UNPACK_ALIGNMENT 指定OpenGL 如何从数据缓存区中解包图像数据

                    //参数2:表示参数GL_UNPACK_ALIGNMENT 设置的值

                   //GL_UNPACK_ALIGNMENT 指内存中每个像素⾏起点的排列请求

                    允许设置为1 (byte排列)

                                        2(排列为偶数byte的⾏)

                                        4(字word排列)

                                        8(⾏从双字节边界开始)

             举例: glPixelStorei(GL_UNPACK_ALIGNMENT,1);

    2.2  从颜⾊缓存区内容作为像素图直接读取

    void glReadPixels(GLint x,GLint y,GLSizei width,GLSizei height, GLenum format, GLenum type,const void * pixels);

    参数说明:

                    //参数1:x,矩形左下⻆的窗⼝坐标

                    //参数2:y,矩形左下⻆的窗⼝坐标

                    //参数3:width,矩形的宽,以像素为单位

                    //参数4:height,矩形的⾼,以像素为单位

                    //参数5:format,OpenGL 的像素格式,参考 表6-1

                    //参数6:type,解释参数pixels指向的数据,告诉OpenGL 使⽤缓存区中的什么数据类型来存储颜⾊分量,像素数据的数据类型,参考 表6-2

                    //参数7:pixels,指向图形数据的指针

                    glReadBuffer(mode);—> 指定读取的缓存

                    glWriteBuffer(mode);—> 指定写⼊的缓存

    2.3载⼊纹理

                    void glTexImage1D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLint border,GLenum format,GLenum type,void *data);

                    void glTexImage2D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLsizei height,GLint border,GLenum format,GLenum type,void * data);(这个是比较常用的)

                    void glTexImage3D(GLenum target,GLint level,GLint internalformat,GLSizei width,GLsizei height,GLsizei depth,GLint border,GLenum format,GLenum type,void *data);

    参数说明:

                        * target:`GL_TEXTURE_1D`、`GL_TEXTURE_2D`、`GL_TEXTURE_3D`。 

                        * Level:指定所加载的mip贴图层次。⼀般我们都把这个参数设置为0。

                        * internalformat:每个纹理单元中存储多少颜⾊成分。

                        * width、height、depth参数:指加载纹理的宽度、⾼度、深度。==注意!==这些值必须是2的整数次⽅。(这是因为OpenGL 旧版本上的遗留下的⼀个要求。当然现在已经可以⽀持不是2的整数次⽅。但是开发者们还是习惯使⽤以2的整数次⽅去设置这些参数。)

                        * border参数:允许为纹理贴图指定⼀个边界宽度。

                        * format、type、data参数:与我们在讲glDrawPixels 函数对于的参数相同

    2.4更新纹理

                    void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);

    参数说明:同载入纹理一样

    2.5插入替换纹理

                    void glCopyTexSubImage1D(GLenum target,GLint level,GLint xoffset,GLint x,GLint y,GLsizei width);

                    void glCopyTexSubImage2D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint x,GLint y,GLsizei width,GLsizei height);

                    void glCopyTexSubImage3D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint zOffset,GLint x,GLint y,GLsizei width,GLsizei height);

    参数说明:同载入纹理一样

    2.6使⽤颜⾊缓存区加载数据,形成新的纹理使⽤

                    void glCopyTexImage1D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLint border);

                    void glCopyTexImage2D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLsizei height,GLint border);

    特别说明:x,y 在颜⾊缓存区中指定了开始读取纹理数据的位置;缓存区⾥的数据,是源缓存区通过glReadBuffer设置的。注意:不存在glCopyTextImage3D ,因为我们⽆法从2D 颜⾊缓存区中获取体积 数据

    三.纹理对象

    3.1使⽤函数分配纹理对象&&指定纹理对象的数量 和 指针(指针指向⼀个⽆符号整形数组,由纹理对象标识符填充)。

                    void glGenTextures(GLsizei n,GLuint * textTures);

    3.2绑定纹理状态

                    void glBindTexture(GLenum target,GLunit texture);

    参数说明:

                    参数target:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数texture:需要绑定的纹理对象

    3.2删除纹理对象

                    void glDeleteTextures(GLsizei n,GLuint *textures);

                    参数说明:同分配纹理对象一样

    3.3测试纹理对象是否有效

                    GLboolean glIsTexture(GLuint texture);

                    说明:如果texture是⼀个已经分配空间的纹理对象,那么这个函数会返回GL_TRUE,否则会返回GL_FALSE。

    3.4设置纹理参数

                    glTexParameterf(GLenum target,GLenum pname,GLFloat param);

                    glTexParameteri(GLenum target,GLenum pname,GLint param);

                    glTexParameterfv(GLenum target,GLenum pname,GLFloat *param);

                    glTexParameteriv(GLenum target,GLenum pname,GLint *param);

    参数说明:

                    参数1:target,指定这些参数将要应⽤在那个纹理模式上,⽐如GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D。

                    参数2:pname,指定需要设置那个纹理参数

                    参数3:param,设定特定的纹理参数的值

    3.5过滤方式

            1)邻近过滤(GL_NEAREST)


      说明:当一像素点靠近A时,返回离这个点最近的像素值

            2)线性过滤(GL_LINEAR)













    说明:两种过滤效果本质上没有多大区别,肉眼很难区分的出来,只有当图片放大后,可惜清晰的看清楚两种过滤方式的差别,一般情况下,glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR) 纹理放⼤时,使⽤线性过滤

    3.6设置环绕⽅式

    当纹理坐标超出默认范围时,每个选项都有不同的输出效果



    设置环绕方式;

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_S,GL_CLAMP_TO_EDGE);

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_T,GL_CLAMP_TO_EDGE);

    参数说明:

                    参数1:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数2:GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,针对s,t,r坐标(s->x,t->y,r->z)

                    参数3:GL_REPEAT、GL_CLAMP、GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER

                    GL_REPEAT:OpenGL 在纹理坐标超过1.0的⽅向上对纹理进⾏重复;

                    GL_CLAMP:所需的纹理单元取⾃纹理边界或TEXTURE_BORDER_COLOR.

                    GL_CLAMP_TO_EDGE环绕模式强制对范围之外的纹理坐标沿着合法的纹理单元的最后⼀⾏或者最后⼀列来进⾏采样。

                    GL_CLAMP_TO_BORDER:在纹理坐标在0.0到1.0范围之外的只使⽤边界纹理单元。边界纹理单元是作为围绕基本图像的额外的⾏和列,并与基本纹理图像⼀起加载的。

    3.7OpenGL 像素格式

    常量说明

    GL_RGB                                                         描述红、绿、蓝顺序排列的颜⾊

    GL_RGBA                                                      按照红、绿、蓝、Alpha顺序排列的颜⾊

    GL_BGR                                                         按照蓝、绿、红顺序排列颜⾊

    GL_BGRA                                                       按照蓝、绿、红、Alpha顺序排列颜⾊

    GL_RED                                                         每个像素只包含了⼀个红⾊分量

    GL_GREEN                                                    每个像素只包含了⼀个绿⾊分量

    GL_BLUE                                                       每个像素只包含了⼀个蓝⾊分量

    GL_RG                                                           每个像素依次包含了一个红色和绿色的分量

    GL_RED_INTEGER                                        每个像素包含了一个整数形式的红⾊分量

    GL_GREEN_INTEGER                                   每个像素包含了一个整数形式的绿色分量

    GL_BLUE_INTEGER                                     每个像素包含了一个整数形式的蓝色分量

    GL_RG_INTEGER                                          每个像素依次包含了一个整数形式的红⾊、绿⾊分量

    GL_RGB_INTEGER                                       每个像素包含了一个整数形式的红⾊、蓝⾊、绿色分量

    GL_RGBA_INTEGER                                     每个像素包含了一个整数形式的红⾊、蓝⾊、绿⾊、Alpah分量

    GL_BGR_INTEGER                                        每个像素包含了一个整数形式的蓝⾊、绿⾊、红色分量

    GL_BGRA_INTEGER                                     每个像素包含了一个整数形式的蓝⾊、绿⾊、红色、Alpah分量

    GL_STENCIL_INDEX                                    每个像素只包含了一个模板值

    GL_DEPTH_COMPONENT                          每个像素只包含一个深度值

    GL_DEPTH_STENCIL                                 每个像素包含一个深度值和一个模板值

    3.8像素数据的数据类型

    GL_UNSIGNED_BYTE                        每种颜色分量都是一个8位无符号整数

    GL_BYTE                                            8位有符号整数

    GL_UNSIGNED_SHORT                    16位无符号整数

    GL_SHORT                                         16位有符号整数

    CL_UNSIGNED_INT                            32位无符号整数

    GL_INT                                               32位有符号整数

    GL_FLOAT                                        单精度浮点数

    GL_HALF_FLOAT                                半精度浮点数

    GL_UNSIGNED_BYTE_3_2_3            包装的RGB值

    GL_UNSIGNED_BYTE_2_3_3_REV    包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5         包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5_REV  包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4      包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4_REV   包装的RGB值

    GL_UNSIGNED_SHORT_5_5_5_1        包装的RGB值

    GL_UNSIGNED_SHORT_1_5_5_5_REV   包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8               包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8_REV      包装的RGB值

    GL_UNSIGNED_INT_10_10_10_2       包装的RGB值

    GL_UNSIGNED_INT_2_10_10_10_REV   包装的RGB值

    GL_UNSIGNED_INT_24_8                   包装的RGB值

    GL_UNSIGNED_INT_10F_11F_REV       包装的RGB值

    GL_FLOAT_24_UNSIGNED_INT_24_8_REV     包装的RGB值




    作者:枫紫
    链接:https://www.jianshu.com/p/bea1fd229b18


    收起阅读 »

    iOS---webView相关及原生和web的交互

    webView的基本应用,监听加载进度,返回上一页,异常处理web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView原生调用web:获取webView的标题等web原生互相调用:web获取a...
    继续阅读 »


  • webView的基本应用,监听加载进度,返回上一页,异常处理
  • web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView
  • 原生调用web:获取webView的标题等
  • web原生互相调用:web获取app当前的id、token等用户信息
  • 微信web里打开原生app
  • 一、webView的基本应用

    现在基本每个app都会或多或少用到web来实现快速迭代。正常都会将其封装在一个控制器里,以使其样式、功能统一
    (iOS8引入了WKWebView,使用独立的进程渲染web,解决了之前UIWebView内存泄漏和crash率高等被诟病已久的问题,所以现在基本都是用WKWebView了)


        //如果不考虑和原生的交互
    _webView = [[WKWebView alloc] initWithFrame:CGRectZero];
    [self.view addSubview:_webView];
    [_webView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(self.view);
    }];
    _webView.UIDelegate = self;
    _webView.navigationDelegate = self;
    [_webView loadRequest:[NSURLRequest requestWithURL:URL]];//这里的url是经过校检的

    如果要监听webview的加载进度

        //kvo监听
    [_webView addObserver:self forKeyPath:@"estimatedProgress" options:0 context:nil];

    //创建加载进度条UIProgressView
    {
    init progressView
    }

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]) {
    self.progressView.alpha = 1.0f;
    BOOL animated = _webView.estimatedProgress > self.progressView.progress;
    [self.progressView setProgress:_webView.estimatedProgress animated:animated];

    if (_webView.estimatedProgress >= 1.0f) {
    [UIView animateWithDuration:0.3f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^{
    self.progressView.alpha = 0.0f;
    } completion:^(BOOL finished) {}];
    }
    } else {
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    }
    返回上一页

            //kvo监听
    [_webView addObserver:self forKeyPath:@"canGoBack" options:0 context:nil];//监听是否有上一页

    //configBackButton里判断canGoBack,如果不可以返回就将按钮置灰或者隐藏
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqual: @"canGoBack"]) {
    [self configBackButton];
    }
    }
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    //按钮事件
    if ([weakSelf.webView canGoBack]) {
    [weakSelf.webView goBack];
    }
    当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用下面的回调函数,我们在这里执行[webView reload]解决白屏问题

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

    [webView reload];
    }

    二、web调用原生

    1.这三个代理方法是可以接收到web的调用比如 window.prompt("xxx")

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
    1. 在发送请求之前,决定是否跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    NSURL *url = navigationAction.request.URL;
    //可以在这里处理一些跳转,比如通过scheme来处理跳转到指定的原生页面(xxx://xxx),拦截跳转其他app,添加app白名单,拦截通用链接跳转等等

    //比如
    if ([@"mailto" isEqualToString:url.scheme] || [@"tel" isEqualToString:url.scheme]) {//系统scheme
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
    [[UIApplication sharedApplication] openURL:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
    } if ([@"xxxx" isEqualToString:url.scheme]) {
    // 如果该scheme是你定义好的scheme,可以根据后面的参数去处理跳转到app内的指定页面,或者其他操作
    decisionHandler(WKNavigationActionPolicyCancel);
    }else if ([scheme白名单 containsObject:url.scheme]) {//白名单
    // 打开scheme
    [[UIApplication sharedApplication] openURL:url];
    decisionHandler(WKNavigationActionPolicyCancel);
    } else {
    BOOL canOpenUniversalUrl = NO;
    for (NSString *str in universalLink白名单) {
    if ([url.absoluteString rangeOfString:str].location != NSNotFound) {
    canOpenUniversalUrl = YES;
    break;
    }
    }
    if (canOpenUniversalUrl) {
    // 打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow);
    } else {
    // Default 可以正常访问网页,但禁止打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow+2);
    }
    }
    }
    web只需
    window.location.href = "xxx"//这里的地址就是上方代理方法的url
    WKWebView可以使用WKScriptMessageHandler来实现JS调用原生方法
    首先初始化的时候,这里拿最常用的web调用关闭webView:xxx_close举例(也可以用上边的href的scheme方式实现,但不太合理)
        _webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:[self configWKConfiguration]];

    // config,js注入
    - (WKWebViewConfiguration *)configWKConfiguration {
    WKWebViewConfiguration* webViewConfig = [WKWebViewConfiguration new];
    WKUserContentController *userContentController = [WKUserContentController new];
    //这里如果用的不多,可以不用单独写一个js文件,直接用字符串就行了
    NSString *jsStr = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"js文件地址"] encoding:NSUTF8StringEncoding error:nil];
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
    [userContentController addUserScript:userScript];
    [userContentController addScriptMessageHandler:self name:"closeWebView"];

    webViewConfig.userContentController = userContentController;
    webViewConfig.preferences = [[WKPreferences alloc] init];
    webViewConfig.preferences.javaScriptEnabled = YES;
    return webViewConfig;
    }

    //app里的js文件里实现
    var xxx = {
    close: function() {
    window.webkit.messageHandlers.closeWebView.postMessage(null);
    },
    }

    //在这里能收到回调
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([message.name isEqualToString:@"closeWebView"]) {
    // 关闭页面
    [self.navigationController popViewControllerAnimated:YES];
    }
    }

    web中只需
          try {
    window.xxx.close();
    } catch (err) {}

    三、原生调用web,就是app调用web里的js方法

    1.比较常用的一种,获取webView的标题
    //也可以用正则去获取标题、图片之类的

        [webView evaluateJavaScript:@"document.title" completionHandler:^(id result, NSError * _Nullable error) {
    }];

    四. web原生互相调用

    比如一个场景,在web里获取app当前登录的账号id

    1. 首先像上边一样,通过js注入的方式web向app发送getUserId请求,app也要同步处理

    //web代码

          try {
    window.xxx.getUserId();//这里可以直接加返回值,但是app内的js没办法直接去获取原生用户信息这些变量,所以还是要通过原生的代理去实现
    } catch (err) {}
    1. 这时候app接收到这个请求,但还要将userId告诉web

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:"getUserId"]){
    NSDictionary *dict = xxx;//因为这个过程是异步的,所以这里最好多加一点信息,以便于web确认该结果是上边请求的返回
    NSString * jsMethod = [NSString stringWithFormat:@"window.getUserId(%@)",[dict yy_modelToJSONString]];
    [webView evaluateJavaScript:@"xxx" completionHandler:^(id result, NSError * _Nullable error) {
    }];
    }
    }

    1. web需要将getUserId方法挂载到window上,算是自定义回调,将上一步生成的用户信息dic当成参数传进来,然后去处理接收到的信息

    //web代码

        window["getUserId"] = function(value) {
    //在这里解析处理接收到的用户信息
    };

    五. web如何在微信里打开原生?

    普通的scheme跳转被微信给禁了,所以现在基本都是通过universalLink通用链接的方式,设置universalLink的方式网上有好多,另外通用链接可以设置多个,最好设置两个以上(因为这里有个隐藏的坑:web的域名不能和universalLink一样,否则无法跳转)
    web代码:


    window.location.href = '通用链接://具体落地页'//可以通过参数跳转到具体的页面
    作者:Theendisthebegi
    链接:https://www.jianshu.com/p/d66d694b762f










    收起阅读 »

    iOS开发宏定义整理

    宏定义今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能下面分开介绍各种宏:Macros.h这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK#...
    继续阅读 »

    宏定义

    今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能

    下面分开介绍各种宏:

    • Macros.h

    这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK
    #import "DimensMacros.h"
    #import "UtilsMacros.h"
    #import "PathMacros.h"
    #import "NotificationMacros.h"
    #import "APIStringMacros.h"

    • APIStringMacros_h(服务端API接口的宏)

    这里面主要放一些API相关的东西:比如你请求网络的接口hostname,port还有一些第三方的关键字段:极光推送的appkey....

    • DimensMacros.h (定义尺寸类的宏)

    这里面定义一些尺寸相关的宏:

    #pragma mark - 系统UI
    #define kNavigationBarHeight 44
    #define kStatusBarHeight 20
    #define kTopBarHeight 64
    #define kToolBarHeight 44
    #define kTabBarHeight 49
    #define kiPhone4_W 320
    #define kiPhone4_H 480
    #define kiPhone5_W 320
    #define kiPhone5_H 568
    #define kiPhone6_W 375
    #define kiPhone6_H 667
    #define kiPhone6P_W 414
    #define kiPhone6P_H 736
    /*** 当前屏幕宽度 */
    #define kScreenWidth [[UIScreen mainScreen] bounds].size.width
    /*** 当前屏幕高度 */
    #define kScreenHeight [[UIScreen mainScreen] bounds].size.height
    /*** 普通字体 */
    #define kFont(size) [UIFont systemFontOfSize:size]
    /*** 粗体 */
    #define kBoldFont(size) [UIFont boldSystemFontOfSize:size]
    #define kLineHeight (1 / [UIScreen mainScreen].scale)
    • NotificationMacros.h(通知Notification相关宏)

    这里面放一些关于通知定义的宏

    #define TNCancelFavoriteProductNotification     @"TNCancelFavoriteProductNotification"      //取消收藏时
    #define TNMarkFavoriteProductNotification @"TNMarkFavoriteProductNotification" //标记收藏时

    #define kNotficationDownloadProgressChanged @"kNotficationDownloadProgressChanged" //下载进度变化
    #define kNotificationPauseDownload @"kNotificationPauseDownload" //暂停下载
    #define kNotificationStartDownload @"kNotificationStartDownload" //开始下载

    #define kNotificationDownloadSuccess @"kNotificationDownloadSuccess" //下载成功
    #define kNotificationDownloadFailed @"kNotificationDownloadFailed" //下载失败
    #define kNotificationDownloadNewMagazine @"kNotificationDownloadNewMagazine"
    • UtilsMacros_h(工具类的宏)

    这里面存放一些方便开发的工具:颜色,打印,单利,版本...
    // 日志输出
    #ifdef DEBUG
    #define LMLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTIONLINE, ##VA_ARGS);
    #else
    #define LMLog(...)
    #endif

    #define WeakSelf(weakSelf)  __weak __typeof(&*self)weakSelf = self;

    #pragma mark - 颜色
    #define kWhiteColor [UIColor whiteColor]
    #define kBlackColor [UIColor blackColor]
    #define kDarkGrayColor [UIColor darkGrayColor]
    #define kLightGrayColor [UIColor lightGrayColor]
    #define kGrayColor [UIColor grayColor]
    #define kRedColor [UIColor redColor]
    #define kGreenColor [UIColor greenColor]
    #define kBlueColor [UIColor blueColor]
    #define kCyanColor [UIColor cyanColor]
    #define kYellowColor [UIColor yellowColor]
    #define kMagentaColor [UIColor magentaColor]
    #define kOrangeColor [UIColor orangeColor]
    #define kPurpleColor [UIColor purpleColor]
    #define kBrownColor [UIColor brownColor]
    #define kClearColor [UIColor clearColor]

    //16进制
    #define LMColorFromHex(s) [UIColor colorWithRed:(((s & 0xFF0000) >> 16))/255.0green:(((s &0xFF00) >>8))/255.0blue:((s &0xFF))/255.0alpha:1.0]
    //RGB
    #define kRGBAColor(r,g,b,a) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a]
    #define kRGBColor(r,g,b) kRGBAColor(r,g,b,1.0f)
    #define kSeperatorColor kRGBColor(234,237,240)
    #define kBgColor kRGBColor(243,245,247)

    #define krgbaColor(r,g,b,a) [UIColor colorWithRed:r green:g blue:b alpha:a]
    #define krgbColor(r,g,b) krgbColor(r,g,b,1.0f)

    #define kCommonHighLightRedColor krgbColor(1.00f,0.49f,0.65f)
    #define kCommonGrayTextColor krgbColor(0.63f,0.63f,0.63f)
    #define kCommonRedColor krgbColor(0.91f,0.33f,0.33f)
    #define kCommonBlackColor krgbColor(0.17f,0.23f,0.28f)
    #define kCommonTintColor krgbColor(0.42f,0.33f,0.27f)
    #define kCommonBgColor krgbColor(0.86f,0.85f,0.80f)
    #define kDetailTextColor krgbColor(0.56f,0.60f,0.62f)
    #define kLineBgColor krgbColor(0.86f,0.88f,0.89f)
    #define kTextColor krgbColor(0.32f,0.36f,0.40f)


    #define kVersion [NSString stringWithFormat:@"%@",[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]

    //System version utils

    #define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
    #define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
    #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

    //大于等于7.0的ios版本
    #define iOS7_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")

    //大于等于8.0的ios版本
    #define iOS8_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")

    //iOS6时,导航VC中view的起始高度
    #define YH_HEIGHT (iOS7_OR_LATER ? 64:0)

    //获取系统时间戳
    #define getCurentTime [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]]

    #define kWindow [UIApplication sharedApplication].keyWindow //主窗口
    #define kUserDefault [NSUserDefaults standardUserDefaults]

    #pragma mark - 字符串转化
    #define kEmptyStr @""
    #define kIntToStr(i) [NSString stringWithFormat: @"%d", i]
    #define kIntegerToStr(i) [NSString stringWithFormat: @"%ld", i]
    #define kValidStr(str) [NHUtils validString:str]

    #pragma mark - 单利
    #define SingletonH(methodName) + (instancetype)shared##methodName;
    // .m文件的实现
    #if __has_feature(objc_arc) // 是ARC
    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    #else // 不是ARC

    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    \
    - (oneway void)release \
    { \
    \
    } \
    \
    - (id)retain \
    { \
    return self; \
    } \
    \
    - (NSUInteger)retainCount \
    { \
    return 1; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    *PathMacros.h(沙河路径宏)

    这里面是一些沙河路径,还有一些plist路径
    //文件目录
    #define kPathTemp NSTemporaryDirectory()
    #define kPathDocument [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathCache [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathSearch [kPathDocument stringByAppendingPathComponent:@"Search.plist"]

    #define kPathMagazine               [kPathDocument stringByAppendingPathComponent:@"Magazine"]
    #define kPathDownloadedMgzs [kPathMagazine stringByAppendingPathComponent:@"DownloadedMgz.plist"]
    #define kPathDownloadURLs [kPathMagazine stringByAppendingPathComponent:@"DownloadURLs.plist"]
    #define kPathOperation [kPathMagazine stringByAppendingPathComponent:@"Operation.plist"]

    #define kPathSplashScreen [kPathCache stringByAppendingPathComponent:@"splashScreen"]

    这样导入宏,简单明了



    作者:Cooci
    链接:https://www.jianshu.com/p/db4f67e56214

    收起阅读 »

    iOS开发必备 - iOS 的锁

    这次主要想解决这些疑问:锁是什么?为什么要有锁?锁的分类问题为什么 OSSpinLock 不安全?解决自旋锁不安全问题有几种方式为什么换用其它的锁,可以解决 OSSpinLock 的问题?自旋锁和互斥锁的关系是平行对立的吗?信号量和互斥量的关系信号量和条件变量...
    继续阅读 »

    这次主要想解决这些疑问:

      1. 锁是什么?
      1. 为什么要有锁?
      1. 锁的分类问题
      1. 为什么 OSSpinLock 不安全?
      1. 解决自旋锁不安全问题有几种方式
      1. 为什么换用其它的锁,可以解决 OSSpinLock 的问题?
      1. 自旋锁和互斥锁的关系是平行对立的吗?
      1. 信号量和互斥量的关系
      1. 信号量和条件变量的区别


    锁是什么

    锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

    为什么要有锁?

    前面说到了,锁是用来保护线程安全的工具。

    可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。

    当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:

    程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。

    所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization)

    属性设置 atomic

    上面提到了原子性,我马上想到了属性关键字里, atomic 的作用。

    设置 atomic 之后,默认生成的 getter 和 setter 方法执行是原子的。

    但是它只保证了自身的读/写操作,却不能说是线程安全。

    如下情况:

    //thread A
    for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
    self.arr = @[@"1", @"2", @"3"];
    }else {
    self.arr = @[@"1"];
    }
    NSLog(@"Thread A: %@\n", self.arr);
    }

    //thread B
    if (self.arr.count >= 2) {
    NSString* str = [self.arr objectAtIndex:1];
    }

    就算在 thread B 中针对 arr 数组进行了大小判断,但是仍然可能在 objectAtIndex: 操作时被改变数组长度,导致出错。这种情况声明为 atomic 也没有用。

    而解决方式,就是进行加锁。

    需要注意的是,读/写的操作都需要加锁,不仅仅是对一段代码加锁。

    锁的分类

    锁的分类方式,可以根据锁的状态,锁的特性等进行不同的分类,很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。关于锁的分类,可以参考 Java中的锁分类 看一下。

    自旋锁和互斥锁的关系

    很多谈论锁的文章,都会提到互斥锁,自旋锁。很少有提到它们的关系,其实自旋锁,也是互斥锁的一种实现,而 spin lock和 mutex 两者都是为了解决某项资源的互斥使用,在任何时刻只能有一个保持者。

    区别在于 spin lock和 mutex 调度机制上有所不同。

    OSSpinLock

    OSSpinLock 是一种自旋锁。它的特点是在线程等待时会一直轮询,处于忙等状态。自旋锁由此得名。

    自旋锁看起来是比较耗费 cpu 的,然而在互斥临界区计算量较小的场景下,它的效率远高于其它的锁。

    因为它是一直处于 running 状态,减少了线程切换上下文的消耗。

    为什么 OSSpinLock 不再安全?

    关于 OSSpinLock 不再安全,原因就在于优先级反转问题。

    优先级反转(Priority Inversion)

    什么情况叫做优先级反转?

    wikipedia 上是这么定义的:

    优先级倒置,又称优先级反转、优先级逆转、优先级翻转,是一种不希望发生的任务调度状态。在该种状态下,一个高优先级任务间接被一个低优先级任务所抢先(preemtped),使得两个任务的相对优先级被倒置。 这往往出现在一个高优先级任务等待访问一个被低优先级任务正在使用的临界资源,从而阻塞了高优先级任务;同时,该低优先级任务被一个次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,该次高优先级任务获得执行权。

    再消化一下

    有:高优先级任务A / 次高优先级任务B / 低优先级任务C / 资源Z 。
    A 等待 C 执行后的 Z
    而 B 并不需要 Z,抢先获得时间片执行
    C 由于没有时间片,无法执行(优先级相对没有B高)。
    这种情况造成 A 在C 之后执行,C在B之后,间接的高优先级A在次高优先级任务B 之后执行, 使得优先级被倒置了。(假设: A 等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源。此时 C 无法与 A 争夺 CPU 时间,从而 C 无法执行,进而无法释放资源。造成的后果,就是 A 无法获得 Z 而继续推进。)

    而 OSSpinLock 忙等的机制,就可能造成高优先级一直 running ,占用 cpu 时间片。而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

    优先级反转的解决方案

    关于优先级反转一般有以下三种解决方案

    优先级继承

    优先级继承,故名思义,是将占有锁的线程优先级,继承等待该锁的线程高优先级,如果存在多个线程等待,就取其中之一最高的优先级继承。

    优先级天花板

    优先级天花板,则是直接设置优先级上限,给临界区一个最高优先级,进入临界区的进程都将获得这个高优先级。

    如果其他试图进入临界区的进程的优先级,都低于这个最高优先级,那么优先级反转就不会发生。

    禁止中断

    禁止中断的特点,在于任务只存在两种优先级:可被抢占的 / 禁止中断的 。

    前者为一般任务运行时的优先级,后者为进入临界区的优先级。

    通过禁止中断来保护临界区,没有其它第三种的优先级,也就不可能发生反转了。

    为什么使用其它的锁,可以解决优先级反转?

    我们看到很多本来使用 OSSpinLock 的知名项目,都改用了其它方式替代,比如 pthread_mutex 和 dispatch_semaphore 。

    那为什么其它的锁,就不会有优先级反转的问题呢?如果按照上面的想法,其它锁也可能出现优先级反转。

    原因在于,其它锁出现优先级反转后,高优先级的任务不会忙等。因为处于等待状态的高优先级任务,没有占用时间片,所以低优先级任务一般都能进行下去,从而释放掉锁。

    线程调度

    为了帮助理解,要提一下有关线程调度的概念。

    无论多核心还是单核,我们的线程运行总是 "并发" 的。

    当 cpu 数量大于等于线程数量,这个时候是真正并发,可以多个线程同时执行计算。

    当 cpu 数量小于线程数量,总有一个 cpu 会运行多个线程,这时候"并发"就是一种模拟出来的状态。操作系统通过不断的切换线程,每个线程执行一小段时间,让多个线程看起来就像在同时运行。这种行为就称为 "线程调度(Thread Schedule)"

    线程状态

    在线程调度中,线程至少拥有三种状态 : 运行(Running),就绪(Ready),等待(Waiting)

    处于 Running的线程拥有的执行时间,称为 时间片(Time Slice),时间片 用完时,进入Ready状态。如果在Running状态,时间片没有用完,就开始等待某一个事件(通常是 IO 或 同步 ),则进入Waiting状态。

    如果有线程从Running状态离开,调度系统就会选择一个Ready的线程进入 Running 状态。而Waiting的线程等待的事件完成后,就会进入Ready状态。

    dispatch_semaphore

    dispatch_semaphore 是 GCD 中同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。

    信号量机制

    信号量中,二元信号量,是一种最简单的锁。只有两种状态,占用和非占用。二元信号量适合唯一一个线程独占访问的资源。而多元信号量简称 信号量(Semaphore)。

    信号量和互斥量的区别

    信号量是允许并发访问的,也就是说,允许多个线程同时执行多个任务。信号量可以由一个线程获取,然后由不同的线程释放。

    互斥量只允许一个线程同时执行一个任务。也就是同一个程获取,同一个线程释放。

    之前我对,互斥量只由一个线程获取和释放,理解的比较狭义,以为这里的获取和释放,是系统强制要求的,用 NSLock 实验发现它可以在不同线程获取和释放,感觉很疑惑。

    实际上,的确能在不同线程获取/释放同一个互斥锁,但互斥锁本来就用于同一个线程中上锁和解锁。这里的意义更多在于代码使用的层面。

    关键在于,理解信号量可以允许 N 个信号量允许 N 个线程并发地执行任务。

    @synchonized

    @synchonized 是一个递归锁。

    递归锁

    递归锁也称为可重入锁。互斥锁可以分为非递归锁/递归锁两种,主要区别在于:同一个线程可以重复获取递归锁,不会死锁; 同一个线程重复获取非递归锁,则会产生死锁。

    因为是递归锁,我们可以写类似这样的代码:


    - (void)testLock{
    if(_count>0){
    @synchronized (obj) {
    _count = _count - 1;
    [self testLock];
    }
    }
    }

    而如果换成NSLock,它就会因为递归发生死锁了。

    实际使用问题

    如果obj 为 nil,或者 obj地址不同,锁会失效。

    所以我们要防止如下的情况:

    @synchronized (obj) {
    obj = newObj;
    }

    这里的 obj 被更改后,等到其它线程访问时,就和没加锁一样直接进去了。

    另外一种情况,就是 @synchonized(self). 不少代码都是直接将self传入@synchronized当中,而 self 很容易作为一个外部对象,被调用和修改。所以它和上面是一样的情况,需要避免使用。

    正确的做法是什么?obj 应当传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的,不被随便修改的。

    pthread_mutex

    pthread定义了一组跨平台的线程相关的 API,其中可以使用 pthread_mutex作为互斥锁。

    pthread_mutex 不是使用忙等,而是同信号量一样,会阻塞线程并进行等待,调用时进行线程上下文切换。

    pthread_mutex` 本身拥有设置协议的功能,通过设置它的协议,来解决优先级反转:

    pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol)

    其中协议类型包括以下几种:

    • PTHREAD_PRIO_NONE:线程的优先级和调度不会受到互斥锁拥有权的影响。
    • PTHREAD_PRIO_INHERIT:当高优先级的等待低优先级的线程锁定互斥量时,低优先级的线程以高优先级线程的优先级运行。这种方式将以继承的形式传递。当线程解锁互斥量时,线程的优先级自动被降到它原来的优先级。该协议就是支持优先级继承类型的互斥锁,它不是默认选项,需要在程序中进行设置。
    • PTHREAD_PRIO_PROTECT:当线程拥有一个或多个使用 PTHREAD_PRIO_PROTECT初始化的互斥锁时,此协议值会影响其他线程(如 thrd2)的优先级和调度。thrd2 以其较高的优先级或者以thrd2 拥有的所有互斥锁的最高优先级上限运行。基于被thrd2拥有的任一互斥锁阻塞的较高优先级线程对于 thrd2的调度没有任何影响。

    设置协议类型为 PTHREAD_PRIO_INHERIT ,运用优先级继承的方式,可以解决优先级反转的问题。

    而我们在 iOS 中使用的 NSLock,NSRecursiveLock等都是基于pthread_mutex 做实现的。

    NSLock

    NSLock属于 pthread_mutex的一层封装, 设置了属性为 PTHREAD_MUTEX_ERRORCHECK 。

    它会损失一定性能换来错误提示。并简化直接使用 pthread_mutex 的定义。

    NSCondition

    NSCondition是通过pthread中的条件变量(condition variable) pthread_cond_t来实现的。

    条件变量

    在线程间的同步中,有这样一种情况: 线程 A 需要等条件 C 成立,才能继续往下执行.现在这个条件不成立,线程 A 就阻塞等待. 而线程 B 在执行过程中,使条件 C 成立了,就唤醒线程 A 继续执行。

    对于上述情况,可以使用条件变量来操作。

    条件变量,类似信号量,提供线程阻塞与信号机制,可以用来阻塞某个线程,等待某个数据就绪后,随后唤醒线程。

    一个条件变量总是和一个互斥量搭配使用。

    NSCondition其实就是封装了一个互斥锁和条件变量,互斥锁的lock/unlock方法和后者的wait/signal统一封装在 NSCondition对象中,暴露给使用者。

    用条件变量控制线程同步,最为经典的例子就是 生产者-消费者问题。

    生产者-消费者问题

    生产者消费者问题,是一个著名的线程同步问题,该问题描述如下:

    有一个生产者在生产产品,这些产品将提供给若干个消费者去消费。要求让生产者和消费者能并发执行,在两者之间设置一个具有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经放入产品的缓冲区中再次投放产品。

    我们可以刚好可以使用 NSCondition解决生产者-消费者问题。具体的代码放置在文末的 Demo 里了。

    if(count==0){
    [condition wait];
    }

    上面这样是不能保证消费者是线程安全的。

    因为NSCondition可以给每个线程分别加锁,但加锁后不影响其他线程进入临界区。所以 NSCondition使用 wait并加锁后,并不能真正保证线程的安全。

    当一个signal操作发出时,如果有两个线程都在做 消费者 操作,那同时都会消耗掉资源,于是绕过了检查。

    例如我们的条件是,count == 0 执行等待。

    假设当前 count = 0,线程A 要判断到 count == 0,执行等待;

    线程B 执行了count = 1,并唤醒线程A 执行 count - 1,同时线程C 也判断到 count > 0 。因为处在不同的线程锁,同样判断执行了 count - 1。2 个线程都会执行count - 1,但是 count = 1,实际就出现count = -1的情况。

    所以为了保证消费者操作的正确,使用 while 循环中的判断,进行二次确认:


     while (count == 0) {
    [condition wait];
    }

    条件变量和信号量的区别

    每个信号量有一个与之关联的值,发出时+1,等待时-1,任何线程都可以发出一个信号,即使没有线程在等待该信号量的值。

    可是对于条件变量,例如 pthread_cond_signal发出信号后,没有任何线程阻塞在 pthread_cond_wait 上,那这个条件变量上的信号会直接丢失掉。

    NSConditionLock

    NSConditionLock称为条件锁,只有 condition 参数与初始化时候的 condition相等,lock才能正确进行加锁操作。

    这里分清两个概念:

    • unlockWithCondition:,它是先解锁,再修改 condition 参数的值。 并不是当 condition 符合某个件值去解锁。
    • lockWhenCondition:,它与 unlockWithCondition: 不一样,不会修改 condition 参数的值,而是符合 condition 的值再上锁。

    在这里可以利用 NSConditionLock实现任务之间的依赖.

    NSRecursiveLock

    NSRecursiveLock 和前面提到的 @synchonized一样,是一个递归锁。

    NSRecursiveLock 与 NSLock 的区别在于内部封装的pthread_mutex_t 对象的类型不同,NSRecursiveLock 的类型被设置为 PTHREAD_MUTEX_RECURSIVE

    NSDistributedLock

    这里顺带提一下 NSDistributedLock, 是 macOS 下的一种锁.

    苹果文档 对于NSDistributedLock 的描述是:

    A lock that multiple applications on multiple hosts can use to restrict access to some shared resource, such as a file

    意思是说,它是一个用在多个主机间的多应用的锁,可以限制访问一些共享资源,例如文件。

    按字面意思翻译,NSDistributedLock 应该就叫做 分布式锁。但是看概念和资料,在 解决NSDistributedLock进程互斥锁的死锁问题(一) 里面看到,NSDistributedLock 更类似于文件锁的概念。 有兴趣的可以看一看 Linux 2.6 中的文件锁

    其它保证线程安全的方式

    除了用锁之外,有其它方法保证线程安全吗?

    使用单线程访问

    首先,尽量避免多线程的设计。因为多线程访问会出现很多不可控制的情况。有些情况即使上锁,也无法保证百分之百的安全,例如自旋锁的问题。

    不对资源做修改

    而如果还是得用多线程,那么避免对资源做修改。

    如果都是访问共享资源,而不去修改共享资源,也可以保证线程安全。

    比如NSArry作为不可变类是线程安全的。然而它们的可变版本,比如 NSMutableArray 是线程不安全的。事实上,如果是在一个队列中串行地进行访问的话,在不同线程中使用它们也是没有问题的。

    总结

    如果实在要使用多线程,也没有必要过分追求效率,而更多的考虑线程安全问题,使用对应的锁。

    对于平时编写应用里的多线程代码,还是建议用 @synchronized,NSLock 等,可读性和安全性都好,多线程安全比多线程性能更重要。



    作者:Cooci
    链接:https://www.jianshu.com/p/c557308c0ec5




    收起阅读 »

    iOS开发堆栈你理解多少?

    浅谈堆栈理解Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。1、栈区(stack):...
    继续阅读 »

    浅谈堆栈理解
    Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;

    栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。

    1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。
    2、堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样,其类是与链表。

    操作系统iOS 中应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在几个个不同的内存区域 。


    栈区(stack):
    1、存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;
    2、程序猿不需要管理栈区变量的内存; 栈区地址从高到低分配;

    堆区(heap):
    1、堆区的内存分配使用的是alloc;
    2、需要程序猿管理内存;
    3、ARC的内存的管理,是编译器再编译的时候自动添加 retain、release、autorelease;
    4、堆区的地址是从低到高分配)

    全局区/静态区(static):
    包括两个部分:未初始化过 、初始化过; 也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域; eg:int a;未初始化的。int a = 10;已初始化的。

    常量区:常量字符串cString等就是放在这里;

    代码区:存放App代码;

    例子:

    int a = 10;  全局初始化区
    char *p; 全局未初始化区

    main{
    int b; 栈区
    char s[] = "abcdef" 栈
    char *p1; 栈
    char *p2 = "qwerty"; \\\\qwerty在常量区,p2在栈上。
    static int c =0; 全局(静态)初始化区
    leap1 = (char *)malloc(100);
    leap2 = (char *)malloc(200);
    分配得来得100和200字节的区域就在堆区。
    }

    “stack”
    局部变量、参数、返回值都存在这里,函数调用开始会参数入栈、局部变量入栈;调用结束依次出栈。

    正如名称所示,stack 是后进先出(LIFO )结构。当函数调用其他的函数时,stack frame会被创建;当其他函数退出后,这个frame会自动被破坏。

    “heap”

    动态内存区域,使用alloc或new申请的内存;为了访问你创建在heap 中的数据,你最少要求有一个保存在stack中的指针,因为你要通过stack中的指针访问heap 中的数据。

    你可以认为stack 中的一个指针仅仅是一个整型变量,保存了heap 中特定内存地址的数据。实际上,它有一点点复杂,但这是它的基本结构。

    简而言之,操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。

    在iOS 操作系统的stack 段和heap 段中,一般来说你都可以创建数据对象。

    stack 对象的优点主要有两点,一是创建速度快,二是管理简单,它有严格的生命周期。stack 对象的缺点是它不灵活。创建时长度是多大就一直是多 大,创建时是哪个函数创建的,它的owner 就一直是它。不像heap 对象那样有多个owner ,其实多个owner 等同于引用计数。只有 heap 对象才是采用“引用计数”方法管理它。

    堆空间和栈空间的大小是可变的,堆空间从下往上生长,栈空间从上往下生长。

    stack 对象的创建

    只要栈的剩余空间大于 stack 对象申请创建的空间,操作系统就会为程序提供这段内存空间,否则将报异常提示栈溢出。

    heap 对象的创建

    操作系统对于内存heap 段是采用链表进行管理的。操作系统有一个记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找第一个空间大于所申请的heap 节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。

    例如:

    NSString 的对象就是 stack 中的对象,NSMutableString 的对象就是heap 中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,可有多个owner, 适用于计数管理内存管理模式。

    两类对象的创建方法也不同,前者直接创建 NSString * str1=@"welcome"; ,而后者需要先分配再初始化 NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];。

    引用计数是放在堆内存中的一个整型,对象alloc开辟堆内存空间后,引用计数自动置1;

    NSString直接赋值是创建在_TEXT段中,_TEXT段是在编译时保存程序代码段的机器码,也就是说NSString会以字符串的形式保存起来,只要字符串名称相同,其地址就相同,就算在新建一个名字一样的NSString,还是原来那个;顺便讲一下_DATA段,他是保存全局变量和静态变量的值的)

    _TEXT段:整个程序的代码,以及所有的常量。这部分内存是是固定大小的,只读的。
    _DATA段:初始化为非零值的全局变量。
    BSS段:初始化为0或未初始化的全局变量和静态变量。
    更多细节我后面会讲一篇Mach-O内核方面的文章;

    静态和全局的区别

    static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;

    static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;

    static函数与普通函数有什么区别:static函数与普通函数作用域不同,只在定义该变量的源文件内有效;

    全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。

    补充:内存引用计数的实现

    GNUstep的实现是将引用计数保存在对象占用内存块头部的变量中

    好处是:

    少量的代码即可完成。

    能够统一管理引用计数内存块和对象引用计数内存块

    苹果的实现是保存在引用计数hash表中

    好处是:

    对象用内存块的分配无需考虑内存块的头部

    引用计数表各记录中存有内存块地址,可以从各个记录追溯到各对象的内存块,这点对调试非常重要

    weak对象释放是自动致nil实现:

    也是通过一个weakhash表实现的,将weak的对象地址注册到weakhash表中,如果该对象被destroy销毁,则在weak表中将该对象地址致nil,并清除记录

    链接:https://www.jianshu.com/p/1f075bdc2e29

    收起阅读 »

    浅谈Android插件化

    一、认识插件化1.1 插件化起源插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。想必大家都知道,在 Android ...
    继续阅读 »

    一、认识插件化

    1.1 插件化起源

    插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。

    想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已。

    常见的应用安装目录有:

    • /system/app:系统应用
    • /system/priv-app:系统应用
    • /data/app:用户应用

    那可能大家会想问,既然安装这个过程如此简单,Android 是怎么运行应用中的代码的呢,我们先看 Apk 的构成,一个常见的 Apk 会包含如下几个部分:

    • classes.dexJava 代码字节码
    • res:资源文件
    • libso 文件
    • assets:静态资产文件
    • AndroidManifest.xml:清单文件

    其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已。

    那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?

    1.2 插件化优点

    插件化让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益:

    • 减少安装Apk的体积、按需下载模块
    • 动态更新插件
    • 宿主和插件分开编译,提升开发效率
    • 解决方法数超过65535的问题

    想象一下,你的应用拥有 Native 应用一般极高的性能,又能获取诸如 Web 应用一样的收益。

    嗯,理想很美好不是嘛?

    1.3 与组件化的区别

    • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。
    • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。

    二、插件化的技术难点

    想让插件的Apk真正运行起来,首先要先能找到插件Apk的存放位置,然后我们要能解析加载Apk里面的代码。

    但是光能执行Java代码是没有意义的,在Android系统中有四大组件是需要在系统中注册的,具体来说是在 Android 系统的 ActivityManagerService (AMS) 和 PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMS 和 PMS,如何欺骗系统,让他承认一个未安装的 Apk 中的组件,如何让宿主动态加载执行插件Apk中 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等是插件化最大的难点。

    另外,应用资源引用(特指 R 中引用的资源,如 layoutvalues 等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。

    总结一下,其实做到插件化的要点就这几个:

    • 如何加载并执行插件 Apk 中的代码(ClassLoader Injection
    • 让系统能调用插件 Apk 中的组件(Runtime Container
    • 正确识别插件 Apk 中的资源(Resource Injection

    当然还有其他一些小问题,但可能不是所有场景下都会遇到,我们后面再单独说。

    三、ClassLoader Injection

    ClassLoader 是插件化中必须要掌握的,因为我们知道Android 应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk,系统不会处理其中的类,所以需要使用 ClassLoader 加载 Apk,然后反射里面的代码。

    3.1 java 中的 ClassLoader

    • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等

    • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包

    • AppClassLoader 负责加载 classpath 里的 jar 包和目录

    3.2 android 中的 ClassLoader

    在Android系统中ClassLoader是用来加载dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件

    • PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

      public class PathClassLoader extends BaseDexClassLoader {
      public PathClassLoader(String dexPath, ClassLoader parent) {
      super(dexPath, null, null, parent);
      }

      public PathClassLoader(String dexPath, String libraryPath,
      ClassLoader parent) {
      super(dexPath, null, libraryPath, parent);
      }
      }

    • DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

      public class DexClassLoader extends BaseDexClassLoader {
      public DexClassLoader(String dexPath, String optimizedDirectory,
      String libraryPath, ClassLoader parent) {
      super(dexPath, new File(optimizedDirectory), libraryPath, parent);
      }
      }

    我们在插件化中一般使用的是 DexClassLoader。

    3.3 双亲委派机制

    每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。

        protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
    try {
    if (parent != null) {
    // 先从父类加载器中进行加载
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }

    if (c == null) {
    // 没有找到,再自己加载
    c = findClass(name);
    }
    }
    return c;
    }

    3.4 如何加载插件中的类

    要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要哪些参数。

    public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    // ...
    }
    }

    构造函数需要四个参数: dexPath 是需要加载的 dex / apk / jar 文件路径 optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置 librarySearchPath 是 native 依赖的位置 parent 就是父类加载器,默认会先从 parent 加载对应的类

    创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:

        // 从 assets 中拿出插件 apk 放到内部存储空间
    private fun extractPlugin() {
    var inputStream = assets.open("plugin.apk")
    File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init() {
    extractPlugin()
    pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
    nativeLibDir = File(filesDir, "pluginlib").absolutePath
    dexOutPath = File(filesDir, "dexout").absolutePath
    // 生成 DexClassLoader 用来加载插件类
    pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
    }

    3.5 执行插件类的方法

    通过反射来执行类的方法

    val loadClass = pluginClassLoader.loadClass(activityName)
    loadClass.getMethod("test",null).invoke(loadClass)

    我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。

    四、Runtime Container

    我们之前说到 Activity 插件化最大的难点是如何欺骗系统,让他承认一个未安装的 Apk 中的组件。 因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。 如果直接把插件的 Activity 注册到宿主 Manifest 里就失去了插件化的动态特性,因为每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。

    4.1 为什么没有注册的 Activity 不能和系统交互

    这里的不能直接交互的含义有两个

    1. 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:

      android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?


    这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:

    public class Instrumentation {
    public static void checkStartActivityResult(int res, Object intent) {
    if (!ActivityManager.isStartResultFatalError(res)) {
    return;
    }

    switch (res) {
    case ActivityManager.START_INTENT_NOT_RESOLVED:
    case ActivityManager.START_CLASS_NOT_FOUND:
    if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
    throw new ActivityNotFoundException(
    "Unable to find explicit activity class "
    + ((Intent)intent).getComponent().toShortString()
    + "; have you declared this activity in your AndroidManifest.xml?");
    throw new ActivityNotFoundException(
    "No Activity found to handle " + intent);
    ...
    }
    }
    }


    1. Activity 的生命周期无法被调用,其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。

    4.2 运行时容器技术

    由于Android中的组件(Activity,Service,BroadcastReceiver和ContentProvider)是由系统创建的,并且由系统管理生命周期。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。插件化如何支持组件生命周期的管理。 大致分为两种方式:

    • 运行时容器技术(ProxyActivity代理)
    • 预埋StubActivity,hook系统启动Activity的过程

    我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。

    它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:

    • pluginName
    • pluginApkPath
    • pluginActivityName

    等,其实最重要的就是 pluginApkPath 和 pluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:

    • 转发所有来自系统的生命周期回调至插件 Activity
    • 接受 Activity 方法的系统调用,并转发回系统

    我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity

    public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    String pluginActivityName = getIntent().getString("pluginActivityName", "");
    pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
    if (pluginActivity == null) {
    super.onCreate(savedInstanceState);
    return;
    }

    pluginActivity.onCreate();
    }

    @Override
    protected void onResume() {
    if (pluginActivity == null) {
    super.onResume();
    return;
    }
    pluginActivity.onResume();
    }

    @Override
    protected void onPause() {
    if (pluginActivity == null) {
    super.onPause();
    return;
    }
    pluginActivity.onPause();
    }

    // ...
    }

    public class PluginActivity {

    private ContainerActivity containerActivity;

    public PluginActivity(ContainerActivity containerActivity) {
    this.containerActivity = containerActivity;
    }

    @Override
    public T findViewById(int id) {
    return containerActivity.findViewById(id);
    }
    // ...
    }

    // 插件 `Apk` 中真正写的组件
    public class TestActivity extends PluginActivity {
    // ......
    }

    是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。

    4.3 字节码替换

    该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。

    class TestActivity extends Activity {}
    ->
    class TestActivity extends PluginActivity {}

    有没有什么办法能让插件组件的编写与原来没有任何差别呢?

    Shadow 的做法是字节码替换插件,这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。

    实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:

    class TestActivity extends Activity {}
    然后完成编译后,最后的字节码中,显示的却是:
    class TestActivity extends PluginActivity {}

    到这里基本的框架就差不多结束了。

    五、Resource Injection

    最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id

    资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:

    • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的 PackageInfo
    • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例

    我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:

    PackageManager packageManager = getPackageManager();
    PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,
    PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
    );
    packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
    packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

    Resources injectResources = null;
    try {
    injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
    } catch (PackageManager.NameNotFoundException e) {
    // ...
    }

    拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:

    public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources, Resources injectResources) {
    super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
    this.hostResources = hostResources;
    this.injectResources = injectResources;
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
    try {
    return injectResources.getString(id, formatArgs);
    } catch (NotFoundException e) {
    return hostResources.getString(id, formatArgs);
    }
    }

    // ...
    }

    然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:

    public class ContainerActivity extends Activity {
    private Resources pluginResources;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    // ...
    pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
    // ...
    }

    @Override
    public Resources getResources() {
    if (pluginActivity == null) {
    return super.getResources();
    }
    return pluginResources;
    }
    }

    这样就完成了资源的注入。

    收起阅读 »

    APP路由框架与组件化简析

    前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。路由的概...
    继续阅读 »

    前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。

    路由的概念

    路由这个词本身应该是互联网协议中的一个词,维基百科对此的解释如下:

    路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

    个人理解,在前端开发中,路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme,并生成路由表,然后,能够根据外部输入字符串在路由表中匹配到对应的页面或者服务,进行跳转或者调用,并提供会获取返回值等,示意如下

    image.png

    所以一个基本路由框架要具备如下能力:

      1. APP路由的扫描及注册逻辑
      1. 路由跳转target页面能力
      1. 路由调用target服务能力

    APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。

    三方路由框架是否是APP强需求

    答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。

    Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。

    原生路由的限制:功能单一,扩展灵活性差,不易协同

    传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:


    import com.snail.activityforresultexample.test.SecondActivity;

    public class MainActivity extends AppCompatActivity {

    void jumpSecondActivityUseClassName(){

    Intent intent =new Intent(MainActivity.this, SecondActivity.class);
    startActivity(intent);
    }

    显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。

    第一步:manifest中配置activity的intent-filter,至少要配置一个action
















    第二步:调用

    void jumpSecondActivityUseFilter() {
    Intent intent = new Intent();
    intent.setAction("com.snail.activityforresultexample.SecondActivity");
    startActivity(intent);
    }

    如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:

    • 首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。
    • 其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。
    • 最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。

    可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的

    APP三方路由框架需具备的能力

    目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:

    • 路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑
    • scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离
    • 基础路由跳转能力 :页面跳转能力的支持
    • 服务类组件的支持 :如去某个服务组件获取一些配置等
    • [扩展]路由拦截逻辑:比如登陆,统一鉴权
    • 可定制的降级逻辑:找不到组件时的兜底

    可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,

    	@Route(path = "/test/activity2")
    public class Test2Activity extends AppCompatActivity {
    ...
    }

    build阶段会根据注解搜集路由scheme,生成路由表。第二步使用

            ARouter.getInstance()
    .build("/test/activity2")
    .navigation(this);

    如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。

    APP路由框架的实现

    路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询

    路由表的自动生成

    生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,

    image.png

    不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:

    image.png

    其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。

    JavaPoet如何搜集并生成路由表集合?

    以ARouter框架为例,先定义Router框架需要的注解如:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.CLASS)
    public @interface Route {

    /**
    * Path of route
    */

    String path();

    该注解用于标注需要路由的组件,用法如下:

    @Route(path = "/test/activity1", name = "测试用 Activity")
    public class Test1Activity extends BaseActivity {
    @Autowired
    int age = 10;

    之后利用APT扫描所有被注解的类,生成路由表,实现参考如下:

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
    if (CollectionUtils.isNotEmpty(annotations)) {

    Set routeElements = roundEnv.getElementsAnnotatedWith(Route.class);

    this.parseRoutes(routeElements);
    ...
    return false;
    }


    private void parseRoutes(Set routeElements) throws IOException {
    ...
    // Generate groups
    String groupFileName = NAME_OF_GROUP + groupName;
    JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
    TypeSpec.classBuilder(groupFileName)
    .addJavadoc(WARNING_TIPS)
    .addSuperinterface(ClassName.get(type_IRouteGroup))
    .addModifiers(PUBLIC)
    .addMethod(loadIntoMethodOfGroupBuilder.build())
    .build()
    ).build().writeTo(mFiler);

    产物如下:包含路由表,及局部注册入口。

    image.png

    自动注册:ASM搜集上述路由表并聚合插入Init代码区

    为了能够插入到Init代码区,首先需要预留一个位置,一般定义一个空函数,以待后续填充:

    	public class RouterInitializer {

    public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
    ...
    loadRouterTables();
    }
    //自动注册代码
    public static void loadRouterTables() {

    }
    }

    首先利用AOP工具,遍历上述APT中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,

    • 搜集目标,聚合路由表

        /**扫描jar*/
      fun scanJar(jarFile: File, dest: File?) {

      val file = JarFile(jarFile)
      var enumeration = file.entries()
      while (enumeration.hasMoreElements()) {
      val jarEntry = enumeration.nextElement()
      if (jarEntry.name.endsWith("XXRouterTable.class")) {
      val inputStream = file.getInputStream(jarEntry)
      val classReader = ClassReader(inputStream)
      if (Arrays.toString(classReader.interfaces)
      .contains("IHTRouterTBCollect")
      ) {
      tableList.add(
      Pair(
      classReader.className,
      dest?.absolutePath
      )
      )
      }
      inputStream.close()
      } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
      registerInitClass = dest
      }
      }
      file.close()
      }

    • 对目标Class注入路由表初始化代码

        fun asmInsertMethod(originFile: File?) {

      val optJar = File(originFile?.parent, originFile?.name + ".opt")
      if (optJar.exists())
      optJar.delete()
      val jarFile = JarFile(originFile)
      val enumeration = jarFile.entries()
      val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

      while (enumeration.hasMoreElements()) {
      val jarEntry = enumeration.nextElement()
      val entryName = jarEntry.getName()
      val zipEntry = ZipEntry(entryName)
      val inputStream = jarFile.getInputStream(jarEntry)
      //插桩class
      if (entryName.endsWith("RouterInitializer.class")) {
      //class文件处理
      jarOutputStream.putNextEntry(zipEntry)
      val classReader = ClassReader(IOUtils.toByteArray(inputStream))
      val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
      val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
      classReader.accept(cv, EXPAND_FRAMES)
      val code = classWriter.toByteArray()
      jarOutputStream.write(code)
      } else {
      jarOutputStream.putNextEntry(zipEntry)
      jarOutputStream.write(IOUtils.toByteArray(inputStream))
      }
      jarOutputStream.closeEntry()
      }
      //结束
      jarOutputStream.close()
      jarFile.close()
      if (originFile?.exists() == true) {
      Files.delete(originFile.toPath())
      }
      optJar.renameTo(originFile)
      }

    最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码:

     public static void loadRouterTables() {


    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
    ...
    }

    如此就完成了路由表的搜集与注册,大概的流程就是如此。当然对于支持服务、Fragment等略有不同,但大体类似。

    Router框架对服务类组件的支持

    通过路由的方式获取服务属于APP路由比较独特的能力,比如有个用户中心的组件,我们可以通过路由的方式去查询用户是否处于登陆状态,这种就不是狭义上的页面路由的概念,通过一串字符串如何查到对应的组件并调用其方法呢?这种的实现方式也有多种,每种实现方式都有自己的优劣。

    • 一种是可以将服务抽象成接口,沉到底层,上层实现通过路由方式映射对象
    • 一种是将实现方法直接通过路由方式映射

    先看第一种,这种事Arouter的实现方式,它的优点是所有对外暴露的服务都暴露接口类【沉到底层】,这对于外部的调用方,也就是服务使用方非常友好,示例如下:

    先定义抽象服务,并沉到底层

    image.png

    public interface HelloService extends IProvider {
    void sayHello(String name);
    }

    实现服务,并通过Router注解标记

    @Route(path = "/yourservicegroupname/hello")
    public class HelloServiceImpl implements HelloService {
    Context mContext;

    @Override
    public void sayHello(String name) {
    Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }

    使用:利用Router加scheme获取服务实例,并映射成抽象类,然后直接调用方法。

      ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

    这种实现方式对于使用方其实是很方便的,尤其是一个服务有多个可操作方法的时候,但是缺点是扩展性,如果想要扩展方法,就要改动底层库。

    再看第二种:将实现方法直接通过路由方式映射

    服务的调用都要落到方法上,参考页面路由,也可以支持方法路由,两者并列关系,所以组要增加一个方法路由表,实现原理与Page路由类似,跟上面的Arouter对比,不用定义抽象层,直接定义实现即可:

    定义Method的Router

    	public class HelloService {


    @MethodRouter(url = {"arouter://sayhello"})
    public void sayHello(String name) {
    Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }

    使用即可

     RouterCall.callMethod("arouter://sayhello?name=hello");

    上述的缺点就是对于外部调用有些复杂,尤其是处理参数的时候,需要严格按照协议来处理,优点是,没有抽象层,如果需要扩展服务方法,不需要改动底层。

    上述两种方式各有优劣,不过,如果从左服务组件的初衷出发,第一种比较好:对于调用方比较友好。另外对于CallBack的支持,Arouter的处理方式可能也会更方便一些,可以比较方便的交给服务方定义。如果是第二种,服务直接通过路由映射的方式,处理起来就比较麻烦,尤其是Callback中的参数,可能要统一封装成JSON并维护解析的协议,这样处理起来,可能不是很好。

    路由表的匹配

    路由表的匹配比较简单,就是在全局Map中根据String输入,匹配到目标组件,然后依赖反射等常用操作,定位到目标。

    组件化与路由的关系

    组件化是一种开发集成模式,更像一种开发规范,更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件,这些组件之间可能是不同的团队,处于不同的目的在各自维护,甚至是需要代码隔离,如果牵扯到组件间的调用与通信,就不可避免的借助路由,因为实现隔离的,只能采用通用字符串scheme进行通信,这就是路由的功能范畴。

    组件化需要路由支撑的根本原因:组件间代码实现的隔离

    总结

    • 路由不是一个APP的必备功能,但是大型跨团队的APP基本都需要
    • 路由框架的基本能力:路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能
    • 组件化与路由的关系:组件化的代码隔离导致路由框架成为必须
    收起阅读 »

    MVVMFrame for Android 是一个基于Google官方推出的JetPack(Lifecycle,LiveData,ViewModel,Room)构建的快速开发框架,从此构建一个MVVM模式的项目变得快捷简单。

    MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开...
    继续阅读 »

    MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开发框架。有了 MVVMFrame 的加持,从此构建一个 MVVM 模式的项目变得快捷简单。

    架构

    Image

    Android version

    引入

    由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

    1. 在Project的 build.gradle 里面添加远程仓库
    allprojects {
    repositories {
    //...
    mavenCentral()
    }
    }
    1. 在Module的 build.gradle 里面添加引入依赖项

    v2.x(使用 Hilt 简化 Dagger2 依赖注入用法)

    //AndroidX 版本
    implementation 'com.github.jenly1314:mvvmframe:2.1.0'

    以前发布至JCenter的版本

    v2.0.0(使用 Hilt 简化 Dagger2 依赖注入用法)

    //AndroidX 版本
    implementation 'com.king.frame:mvvmframe:2.0.0'

    v1.x 以前版本(使用 Dagger2)

    //AndroidX 版本
    implementation 'com.king.frame:mvvmframe:1.1.4'

    //Android Support版本
    implementation 'com.king.frame:mvvmframe:1.0.2'

    Dagger和 Room 的相关注解处理器

    你需要引入下面的列出的编译时的注解处理器,用于自动生成相关代码。其它对应版本具体详情可查看 Versions

    v2.x 版本($versions 相关可查看Versions

    你需要在项目根目录的 build.gradle 文件中配置 Hilt 的插件路径:

    buildscript {
    ...
    dependencies {
    ...
    classpath "com.google.dagger:hilt-android-gradle-plugin:$versions.daggerHint"
    }
    }

    接下来,在 app/build.gradle 文件中,引入 Hilt 的插件和相关依赖:

    ...
    apply plugin: 'dagger.hilt.android.plugin'

    dependencies{
    ...

    //AndroidX ------------------ MVVMFrame v2.x.x
    //lifecycle
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle"
    //room
    annotationProcessor "androidx.room:room-compiler:$versions.room"
    //hilt
    implementation "com.google.dagger:hilt-android:$versions.daggerHint"
    annotationProcessor "com.google.dagger:hilt-android-compiler:$versions.daggerHint"

    //从2.1.0以后已移除
    // implementation "androidx.hilt:hilt-lifecycle-viewmodel:$versions.hilt"
    // annotationProcessor "androidx.hilt:hilt-compiler:$versions.hilt"
    }

    v1.x 以前版本,建议 查看分支版本

    在 app/build.gradle 文件中引入 Dagger 和 Room 相关依赖:


    dependencies{
    ...

    //AndroidX ------------------ MVVMFrame v1.1.4
    //dagger
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.30.1'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.30.1'
    //room
    annotationProcessor 'androidx.room:room-compiler:2.2.5'
    }

    dependencies{
    ...

    // Android Support ------------------ MVVMFrame v1.0.2
    //dagger
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.19'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
    //room
    annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
    }

    如果你的项目使用的是 Kotlin,记得加上 kotlin-kapt 插件,并需使用 kapt 替代 annotationProcessor

    MVVMFrame引入的库(具体对应版本请查看 Versions

        //appcompat
    compileOnly deps.appcompat

    //retrofit
    api deps.retrofit.retrofit
    api deps.retrofit.gson
    api deps.retrofit.converter_gson

    //retrofit-helper
    api deps.jenly.retrofit_helper

    //lifecycle
    api deps.lifecycle.runtime
    api deps.lifecycle.extensions
    annotationProcessor deps.lifecycle.compiler

    //room
    api deps.room.runtime
    annotationProcessor deps.room.compiler

    //hilt
    compileOnly deps.dagger.hilt_android
    annotationProcessor deps.dagger.hilt_android_compiler

    compileOnly deps.hilt.hilt_viewmodel
    annotationProcessor deps.hilt.hilt_compiler

    //log
    api deps.timber

    示例

    集成步骤代码示例 (示例出自于app中)

    Step.1 启用DataBinding,在你项目中的build.gradle的android{}中添加配置:

    Android Studio 4.x 以后版本

    buildFeatures{
    dataBinding = true
    }

    Android Studio 4.x 以前版本

    dataBinding {
    enabled true
    }

    Step.2 使用JDK8编译(v1.1.2新增),在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    Step.3 自定义全局配置(继承MVVMFrame中的FrameConfigModule)(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

    /**
    * 自定义全局配置
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    public class AppConfigModule extends FrameConfigModule {
    @Override
    public void applyOptions(Context context, ConfigModule.Builder builder) {
    builder.baseUrl(Constants.BASE_URL)//TODO 配置Retrofit中的baseUrl
    .retrofitOptions(new RetrofitOptions() {
    @Override
    public void applyOptions(Retrofit.Builder builder) {
    //TODO 配置Retrofit
    //如想使用RxJava
    //builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    }
    })
    .okHttpClientOptions(new OkHttpClientOptions() {
    @Override
    public void applyOptions(OkHttpClient.Builder builder) {
    //TODO 配置OkHttpClient
    }
    })
    .gsonOptions(new GsonOptions() {
    @Override
    public void applyOptions(GsonBuilder builder) {
    //TODO 配置Gson
    }
    })
    .roomDatabaseOptions(new RoomDatabaseOptions<RoomDatabase>() {
    @Override
    public void applyOptions(RoomDatabase.Builder<RoomDatabase> builder) {
    //TODO 配置RoomDatabase
    }
    });
    }
    }

    Step.4 在你项目中的AndroidManifest.xml中通过配置meta-data来自定义全局配置(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

    <!-- MVVMFrame 全局配置 -->
    <meta-data android:name="com.king.mvvmframe.config.AppConfigModule"
    android:value="FrameConfigModule"/>

    Step.5 关于Application

    2.x版本 因为从2.x开始使用到了Hilt,所以你自定义的Application需加上 @HiltAndroidApp 注解,这是使用Hilt的一个必备前提。示例如下:

       @HiltAndroidApp
    public class YourApplication extends Application {

    }

    1.x版本 将你项目的 Application 继承MVVMFrame中的 BaseApplication

    /**
    * MVVMFrame 框架基于Google官方的Architecture Components dependencies 构建,在使用MVVMFrame时,需遵循一些规范:
    * 1.你的项目中的Application中需初始化MVVMFrame框架相关信息,有两种方式处理:
    * a.直接继承本类{@link BaseApplication}即可;
    * b.如你的项目中的Application本身继承了其它第三方的Application,因为Java是单继承原因,导致没法继承本类,可参照{@link BaseApplication}类,
    * 将{@link BaseApplication}中相关代码复制到你项目的Application中,在相应的生命周期中调用即可。
    *
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    public class App extends BaseApplication {

    @Override
    public void onCreate() {
    //TODO 如果默认配置已经能满足你的需求,你不需要自定义配置,可以通过下面注释掉的方式设置 BaseUrl,从而可以省略掉 step3 , setp4 两个步骤。
    // RetrofitHelper.getInstance().setBaseUrl(baseUrl);
    super.onCreate();
    //开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
    ApplicationComponent appComponent = DaggerApplicationComponent.builder()
    .appComponent(getAppComponent())
    .build();
    //注入
    appComponent.inject(this);

    }


    }

    其他

    关于v2.x

    因为v2.x版本 使用了 Hilt 的缘故,简化了之前 Dagger2 的用法,建议在新项目中使用。如果是从 v1.x 升级到 v2.x,集成步骤稍有变更,详情请查看 Step.5,并且可能还需要删除以前 @Component@Module等注解桥接层相关的逻辑代码,因为从v2.x开始,这些桥接逻辑无需自己编写,全部交由 Hilt 处理。

    关于使用 Hilt

    Hilt 是JetPack中新增的一个依赖注入库,其基于Dagger2研发(后面统称为Dagger),但它不同于Dagger。对于Android开发者来说,Hilt可以说专门为Android 打造。

    之前使用的Dagger for Android虽然也是针对于Android打造,也能通过 @ContributesAndroidInjector 来通过生成简化一部分样板代码,但是感觉还不够彻底。因为 Component 层相关的桥接还是要自己写。Hilt的诞生改善了这些问题。

    Hilt 大幅简化了Dagger 的用法,使得我们不用通过 @Component 注解去编写桥接层的逻辑,但是也因此限定了注入功能只能从几个 Android 固定的入口点开始,

    Hilt 一共支持 6 个入口点,分别是:

    Application

    Activity

    Fragment

    View

    Service

    BroadcastReceiver

    其中,只有 Application 这个入口点是使用 @HiltAndroidApp 注解来声明,示例如下

    Application 示例

       @HiltAndroidApp
    public class YourApplication extends Application {

    }

    其他的所有入口点,都是用 @AndroidEntryPoint 注解来声明,示例如下

    Activity 示例

       @AndroidEntryPoint
    public class YourActivity extends BaseActivity {

    }

    Fragment 示例

       @AndroidEntryPoint
    public class YourFragment extends BaseFragment {

    }

    Service 示例

       @AndroidEntryPoint
    public class YourService extends BaseService {

    }

    BroadcastReceiver 示例

       @AndroidEntryPoint
    public class YourBroadcastReceiver extends BaseBroadcastReceiver {

    }

    其它示例

    BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在类上添加 @HiltViewModel 并在构造函数上添加 @Inject 注解)

       @HiltViewModel
    public class YourViewModel extends BaseViewModel<YourModel> {
    @Inject
    public DataViewModel(@NonNull Application application, YourModel model) {
    super(application, model);
    }
    }

    BaseModel 示例 (如果您继承使用了BaseModel或其子类,你需要参照如下方式在构造函数上添加 @Inject 注解)

       public class YourModel extends BaseModel {
    @Inject
    public BaseModel(IDataRepository dataRepository){
    super(dataRepository);
    }
    }

    如果使用的是 v2.0.0 版本 (使用 androidx.hilt:hilt-lifecycle-viewmodel 的方式)

    BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在构造函数上添加 @ViewModelInject 注解)

       public class YourViewModel extends BaseViewModel<YourModel> {
    @ViewModelInject
    public DataViewModel(@NonNull Application application, YourModel model) {
    super(application, model);
    }
    }

    关于使用 Dagger

    之所以特意说 Dagger 是因为Dagger的学习曲线相对陡峭一点,没那么容易理解。

    1. 如果你对 Dagger 很了解,那么你将会更加轻松的去使用一些注入相关的骚操作。

    因为 MVVMFrame 中使用到了很多 Dagger 注入相关的一些操作。所以会涉及Dagger相关技术知识。

    但是并不意味着你一定要会使用 Dagger,才能使用MVVMFrameComponent

    如果你对 Dagger 并不熟悉,其实也是可以用的,因为使用 Dagger 全局注入主要都已经封装好了。你只需参照Demo 中的示例,照葫芦画瓢。 主要关注一些继承了BaseActivityBaseFragmentBaseViewModel等相关类即可。

    这里列一些主要的通用注入参照示例:(下面Dagger相关的示例仅适用于v1.x版本,因为v2.x已基于Hilt编写,简化了Dagger依赖注入桥接层相关逻辑)

    直接或间接继承了 BaseActivity 的配置示例:

    /**
    * Activity模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(subcomponents = BaseActivitySubcomponent.class)
    public abstract class ActivityModule {

    @ContributesAndroidInjector
    abstract MainActivity contributeMainActivity();

    }

    直接或间接继承了 BaseFragment 的配置示例:

    /**
    * Fragment模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(subcomponents = BaseFragmentSubcomponent.class)
    public abstract class FragmentModule {

    @ContributesAndroidInjector
    abstract MainFragment contributeMainFragment();

    }

    直接或间接继承了 BaseViewModel 的配置示例:

    /**
    * ViewModel模块统一管理:通过{@link Binds}和{@link ViewModelKey}绑定关联对应的ViewModel
    * ViewModelModule 例子
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module
    public abstract class ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(MainViewModel.class)
    abstract ViewModel bindMainViewModel(MainViewModel viewModel);
    }

    ApplicationModule 的配置示例

    /**
    * Application模块:为{@link ApplicationComponent}提供注入的各个模块
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(includes = {ViewModelFactoryModule.class,ViewModelModule.class,ActivityModule.class,FragmentModule.class})
    public class ApplicationModule {

    }

    ApplicationComponent 的配置示例

    /**
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @ApplicationScope
    @Component(dependencies = AppComponent.class,modules = {ApplicationModule.class})
    public interface ApplicationComponent {
    //指定你的 Application 继承类
    void inject(App app);
    }

    通过上面的通用配置注入你所需要的相关类之后,如果配置没什么问题,你只需 执行Make Project 一下,或通过 Make Project 快捷键 Ctrl + F9 ,就可以自动生产相关代码。 比如通过 ApplicationComponent 生成的 DaggerApplicationComponent 类。

    然后在你的 Application 集成类 App 中通过 DaggerApplicationComponent 构建 ApplicationComponent,然后注入即可。

        //开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
    ApplicationComponent appComponent = DaggerApplicationComponent.builder()
    .appComponent(getAppComponent())
    .build();
    //注入
    appComponent.inject(this);

    你也可以直接查看app中的源码示例

    关于设置 BaseUrl

    目前通过设置 BaseUrl 的入口主要有两种:

    1.一种是通过在 Manifest 中配置 meta-data 的来自定义 FrameConfigModule,在里面 通过 {@link ConfigModule.Builder#baseUrl(String)}来配置 BaseUrl。(一次设置,全局配置)

    2.一种就是通过RetrofitHelper {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来配置 BaseUrl。(可多次设置,动态全局配置,有前提条件)

    以上两种配置 BaseUrl 的方式都可以达到目的。但是你可以根据不同的场景选择不同的配置方式。

    主要场景与选择如下:

    一般场景:对于只使用单个不变的 BaseUrl的

    场景1:如果本库的默认已满足你的需求,无需额外自定义配置的。
         选择:建议你直接使用 {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来初始化 BaseUrl,切记在框架配置初始化 BaseUrl之前,建议在你自定义的 {@link Application#onCreate()}中初始化。
    场景2:如果本库的默认配置不满足你的需求,你需要自定义一些配置的。(比如需要使用 RxJava相关)
         选择:建议你在自定义配置中通过 {@link ConfigModule.Builder#baseUrl(String)} 来初始化 BaseUrl。

    二般场景:对于只使用单个 BaseUrl 但是,BaseUrl中途会变动的。

    场景3:和一般场景一样,也能分两种,所以选择也和一般场景也可以是一样的。
         选择:两种选择都行,但当 BaseUrl需要中途变动时,还需将 {@link RetrofitHelper#setDynamicDomain(boolean)} 设置为 {@code true} 才能支持动态改变 BaseUrl。

    特殊场景:对于支持多个 BaseUrl 且支持动态可变的。

       选择:这个场景的选择,主要涉及到另外的方法,请查看 {@link RetrofitHelper#putDomain(String, String)} 和 {@link RetrofitHelper#putDomain(String, HttpUrl)}相关详情

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档


    代码下载:MVVMFrame.zip

    收起阅读 »

    RetrofitHelper是一个支持配置多个BaseUrl,支持动态改变BaseUrl,动态配置超时时长的Retrofit帮助类

    RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。 支持配置多个BaseUrl 支持动态改变BaseUrl 支持动态配置超时时长 支持添加公...
    继续阅读 »


    RetrofitHelper

    RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。

    主要功能介绍

    •  支持配置多个BaseUrl
    •  支持动态改变BaseUrl
    •  支持动态配置超时时长
    •  支持添加公共请求头

    Gif 展示

    Image

    引入

    由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

    1. 在Project的 build.gradle 里面添加远程仓库
    allprojects {
    repositories {
    //...
    mavenCentral()
    }
    }
    1. 在Module的 build.gradle 里面添加引入依赖项
    //AndroidX 版本
    implementation 'com.github.jenly1314:retrofit-helper:1.0.1'

    RetrofitHelper引入的库(具体对应版本请查看 Versions

        compileOnly "androidx.appcompat:appcompat:$versions.appcompat"
    compileOnly "com.squareup.retrofit2:retrofit:$versions.retrofit"

    因为 RetrofitHelper 的依赖只在编译时有效,并未打入包中,所以您的项目中必须依赖上面列出相关库

    示例

    主要集成步骤代码示例

    Step.1 需使用JDK8编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    Step.2 通过RetrofitUrlManager初始化OkHttpClient,进行初始化配置

    //通过RetrofitHelper创建一个支持多个BaseUrl的 OkHttpClient
    //方式一
    val okHttpClient = RetrofitHelper.getInstance()
    .createClientBuilder()
    //...你自己的其他配置
    .build()
    //方式二
    val okHttpClient = RetrofitHelper.getInstance()
    .with(builder)
    //...你自己的其他配置
    .build()
    //完整示例
    val okHttpClient = RetrofitHelper.getInstance()
    .createClientBuilder()
    .addInterceptor(LogInterceptor())
    .build()
    val retrofit = Retrofit.Builder()
    .baseUrl(Constants.BASE_URL)
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create(Gson()))
    .build()

    Step.3 定义接口时,通过注解标记对应接口,支持动态改变 BaseUrl相关功能

     interface ApiService {

    /**
    * 接口示例,没添加任何标识,和常规使用一致
    * @return
    */
    @GET("api/user")
    fun getUser(): Call<User>


    /**
    * Retrofit默认返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
    * @return
    */
    @DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
    @Timeout(connectTimeout = 15,readTimeout = 15,writeTimeout = 15,timeUnit = TimeUnit.SECONDS) //超时标识,用于自定义超时时长
    @GET("api/user")
    fun getUser(): Call<User>

    /**
    * 动态改变 BaseUrl
    * @return
    */
    @BaseUrl(baseUrl) //baseUrl 标识,用于支持指定 BaseUrl
    @GET("api/user")
    fun getUser(): Call<User>


    //--------------------------------------

    /**
    * 使用RxJava返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
    * @return
    */
    @DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
    @Timeout(connectTimeout = 20,readTimeout = 10) //超时标识,用于自定义超时时长
    @GET("api/user")
    fun getUser(): Observable<User>

    }

    Step.4 添加多个 BaseUrl 支持

            //添加多个 BaseUrl 支持 ,domainName为域名别名标识,domainUrl为域名对应的 BaseUrl,与上面的接口定义表示一致即可生效
    RetrofitHelper.getInstance().putDomain(domainName,domainUrl)
            //添加多个 BaseUrl 支持 示例
    RetrofitHelper.getInstance().apply {
    //GitHub baseUrl
    putDomain(Constants.DOMAIN_GITHUB,Constants.GITHUB_BASE_URL)
    //Google baseUrl
    putDomain(Constants.DOMAIN_GOOGLE,Constants.GOOGLE_BASE_URL)
    }

    RetrofitHelper

    /**
    * Retrofit帮助类
    *


    * 主要功能介绍:
    * 1.支持管理多个 BaseUrl,且支持运行时动态改变
    * 2.支持接口自定义超时时长,满足每个接口动态定义超时时长
    * 3.支持添加公共请求头
    *


    *
    * RetrofitHelper中的核心方法
    *
    * {@link #createClientBuilder()} 创建 {@link OkHttpClient.Builder}初始化一些配置参数,用于支持多个 BaseUrl
    *
    * {@link #with(OkHttpClient.Builder)} 传入 {@link OkHttpClient.Builder} 配置一些参数,用于支持多个 BaseUrl
    *
    * {@link #setBaseUrl(String)} 和 {@link #setBaseUrl(HttpUrl)} 主要用于设置默认的 BaseUrl。
    *
    * {@link #putDomain(String, String)} 和 {@link #putDomain(String, HttpUrl)} 主要用于支持多个 BaseUrl,且支持 BaseUrl 动态改变。
    *
    * {@link #setDynamicDomain(boolean)} 设置是否支持 配置多个BaseUrl,且支持动态改变,一般会通过其他途径自动开启,此方法一般不会主动用到,只有在特殊场景下可能会有此需求,所以提供此方法主要用于提供更多种可能。
    *
    * {@link #setHttpUrlParser(HttpUrlParser)} 设置 HttpUrl解析器 , 当前默认采用的 {@link DomainParser} 实现类,你也可以自定义实现 {@link HttpUrlParser}
    *
    * {@link #setAddHeader(boolean)} 设置是否添加头,一般会通过{@link #addHeader(String, String)}相关方法自动开启,此方法一般不会主动用到,只有特殊场景下会有此需求,主要用于提供统一控制。
    *
    * {@link #addHeader(String, String)} 设置头,主要用于添加公共头消息。
    *
    * {@link #addHeaders(Map)} 设置头,主要用于设置公共头消息。
    *
    * 这里只是列出一些对外使用的核心方法,和相关的简单说明。如果想了解更多,可以查看对应的方法和详情。
    *
    *


    *
    * @author Jenly
    */
    public final class RetrofitHelper{
    //...
    }

    特别说明

            //通过setBaseUrl可以动态改变全局的 BaseUrl,优先级比putDomain(domainName,domainUrl)低,谨慎使用
    RetrofitHelper.getInstance().setBaseUrl(dynamicUrl)

    更多使用详情,请查看Demo中的源码使用示例或直接查看API帮助文档


    代码下载:RetrofitHelper.zip

    收起阅读 »

    LLDB调试利器及高级用法

    LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我...
    继续阅读 »

    LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我会把重点放在常用命令的使用方式和技巧上。

    一、 LLDB常用调试命令
    ❶ p、po及 image命令
    1、是打印对象,是打印对象的description,演示如下:


    2、p命令修改变量,演示如下:


    3、imagelookup -a用于寻找栈地址对应的代码位置,演示如下:


    3.1 从上图中我们可以看到当程序崩溃时并不能定位到指定的代码位置,使用image寻址命令可以定位到具体的崩溃位置在viewDidLoad方法中的第51行。


    3.2 这里说明为什么是程序的名称,因为LLDBDebug在编译后就是一个Macho的可执行文件,也可以理解为镜像文件,image并不是图像的意思,而是代表镜像。这里跟上我们自己的工程名,即用image定位寻址才是寻找我们自己的代码。
    ❷ bt及frame命令
    1、使用命令可以查看函数调用堆栈,然后用 命令即可查看对应函数详细,演示如下:


    1.1 上面函数执行的顺序如下:点击登录按钮--验证手机号--验证密码--开始登录。

    - (IBAction)login:(UIButton *)sender {

    [self validationPhone];
    }
    #pragma mark --验证手机号
    -(void)validationPhone{

    [self validationPwd];
    }
    #pragma mark --验证密码
    -(void)validationPwd{

    [self startLogin];
    }
    #pragma mark --开始登陆
    -(void)startLogin{

    NSLog(@"------开始登录...------");
    }

    1.2 从bt命令的打印信息中,我们可以很清楚看到函数调用顺序,如下图:


    1.3 接下来我们执行 frame select命令即可以查看函数相关信息,同时配合up和down命令追踪函数的调用和被调用关系,演示如下:


    1.4 同时可以使用frame variable很方便的查方法的调用者及方法名称,如下图:


    ❸ breakpoint命令
    1、b命令给函数下断点,演示如下图


    1.1 当我们的断点下成功后,控制台会打印如下信息:
    Breakpoint 1: where = LLDBDebug`-[ViewController login:] at ViewController.m:53, address = 0x00000001034fb0a0

    1.2 我们可以看到断点的位置在.m文件的53行,Breakpoint 1这里的1代表的是编号为1的组断点。
    使用 我们可以看到断点的数量,同时使用 后面跟上组号,即可删除,演示如下:


    3、\color{red}{breakpoint}的\color{red}{c},\color{red}{n},\color{red}{s}以及\color{red}{finish}命令,对应关系如下图:


    3.1 我们执行\color{red}{c},\color{red}{n},\color{red}{s}及\color{red}{finish}命令演示如下:


    ❹ breakpoint命令
    1.target stop-hook add -o "frame variable"每次进入断点都会自动打印详细的参数信息,演示如下:


    二、 LLDB高级用法
    ❶ 我们先来简单看下\color{red}{menthods}和\color{red}{pviews}命令的执行效果,演示如下图:


    1.1 \color{red}{menthods}命令可以打印当前对象的属性和方法,如下所示:

    (lldb) methods p1
    <Person: 0x60000003eac0>:
    in Person:
    Properties:
    @property (copy, nonatomic) NSString* name; (@synthesize name = _name;)
    @property (nonatomic) long age; (@synthesize age = _age;)
    Instance Methods:
    - (void) eat; (0x1098bf3e0)
    - (void) .cxx_destruct; (0x1098bf4f0)
    - (id) description; (0x1098bf410)
    - (id) name; (0x1098bf430)
    - (void) setName:(id)arg1; (0x1098bf460)
    - (void) setAge:(long)arg1; (0x1098bf4c0)
    - (long) age; (0x1098bf4a0)
    (NSObject ...)

    1.2 \color{red}{pviews}命令可以打印当前视图的层级结构,如下所示:

    (lldb) pviews
    <UIWindow: 0x7fd1719060a0; frame = (0 0; 414 736); gestureRecognizers = <NSArray: 0x60c000058660>; layer = <UIWindowLayer: 0x60c0000364c0>>
    | <UIView: 0x7fd16fc06d10; frame = (0 0; 414 736); alpha = 0.8; autoresize = W+H; layer = <CALayer: 0x60000003e7e0>>
    | | <UIButton: 0x7fd16fe0b520; frame = (54 316; 266 53); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x60400003b040>>
    | | | <UIButtonLabel: 0x7fd16fe023f0; frame = (117.667 17.6667; 30.6667 18); text = '登录'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60400008ac80>>
    | | | | <_UILabelContentLayer: 0x600000220260> (layer)
    | | <UILabel: 0x7fd16fc04a60; frame = (164 225; 80 47); text = 'Qinz'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000088fc0>>
    (lldb)

    1.3 如果你在原生的XCode中,是敲不出这些命令的,上面只是演示了两个常见的LLDB插件命令的用法,更加高级的用法下面会详细说明。不过在这之前,我们要安装两个插件,接下来先讲解环境的配置。
    ❷ LLDB插件配置:chisel及LLDB
    2.1 chisel是facebook开源的一款LLDB插件,里面封装了很多好用的命令,当然这些命令都是基于苹果提供的api。chisel下载
    2.2 这里建议使用包管理工具Homebrew来安装,然后配置脚本路径,演示如下:


    2.3 然后在lldb窗口执行命令,演示如下:


    2.4 看到输出"command script import /usr/local/opt/chisel/libexec/fblldb.py"即代表安装成功,这里还会看到一个"command script import /opt/LLDB/lldb_commands/dslldb.py
    "路径,这是我们接下来要安装的第二个插件

    Executing commands in '/Users/Qinz/.lldbinit'.
    command script import /usr/local/opt/chisel/libexec/fblldb.py
    command script import /opt/LLDB/lldb_commands/dslldb.py
    (lldb)

    2.5 这个插件的名称也叫LLDB,LLDB下载。我们先clone文件,我这里放置在opt文件夹下,你可以选择自己的文件目录放置,然后依次找到dslldb文件,在~/.initlldb文件中配置路径,演示如下:


    2.6 接下来依然在lldb窗口执行 command source ~/.lldbinit命令。到此LLDB插件的配置环境完成,接下来我们讲解这些插件的实用命令。
    ❸ lldb高级用法
    1. 搭配,让你快速找准控件,演示如下:


    1.1 taplog是点击控件,会打印控件的地址,大小及透明度等信息,我们拿到地址后执行flicker 0x7fd321e09710命令,此时控件会进行闪烁,这里动态图显示的闪烁效果明显。
    2. 和显示和隐藏控件,演示如下:


    \color{red}{vs}命令方便动态查看控件的层级关系,演示如下:


    3.1 当我们执行\color{red}{vs}命令后会进入动态调试阶段,会出现以下五个命令,每个命令我做了详细注释如下:

    (lldb) vs 0x7fe73550a090
    Use the following and (q) to quit.
    (w) move to superview //移动到父视图
    (s) move to first subview //移动到第一个子视图
    (a) move to previous sibling //移动上一个兄弟视图
    (d) move to next sibling //移动下一个兄弟视图
    (p) print the hierarchy //打印视图层级结构

    \color{red}{pactions}直接打印对象调用者及方法,演示如下:


    \color{red}{border}&\color{red}{unborder}给控件增加和去除边框,演示如下:


    5.1 这里的-c即是color,-w即设置边框的宽度。通过这个命令我们可以很方便的查看边框的边缘的问题,而不需要每次重启运行。
    6.\color{red}{pclass}打印对象的继承关系,演示如下图:


    \color{red}{presponder}命令打印响应链,演示如下图:


    \color{red}{caflush}这个命令会重新渲染,即可以重新绘制界面, 相当于执行了 [CATransaction flush] 方法,演示如下:


    \color{red}{search}搜索已经存在于栈中的控件及其子控件,演示如下:


    \color{red}{lookup}搜索,可执行正则表达式。演示如下:


    10.1 上面的搜索会搜索所用镜像模块,我们重点看与我们工程名字相同的模块,即可查看哪些地方调用了这些方法。
    11. \color{red}{pbundlepath}打印app路径及\color{red}{pdocspath}打印文档路径,演示如下:


    总结:上面详细讲解了LLDB常用命令及高级命令的用法,熟练掌握可大幅度提高Debug能力和开发效率。

    我是Qinz,希望我的文章对你有帮助。

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

    收起阅读 »

    UIViewController解耦---浅析Three20架构

    前言Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在...
    继续阅读 »

    前言
    Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在诟病Three20各个模块之间的耦合度太高,而且更多人在抱怨Three20极少的开发文档,我想这些大概也是Three20在苹果发布iOS6之后就停止了更新维护的原因吧。大神Joe Hewitt创建的在Github上的源码早已删除,目前只有少数人在GitHub上为自己的项目维护。而我也是有幸在某个项目中见识到了曾经耳闻,却未目睹的Three20框架,因此才有了这篇文章。

    架构
    最近大家都在讨论MVC、MVVM以及MVP三种在移动端开发中常用到的架构模式,究竟是哪种架构最强大,最适合移动开发者使用。这里笔者也阐述一下个人意见,有句方言叫“树挪死,人挪活”,个人认为,架构是死的,开发者是活的,我们不需要局限于哪一种架构的模式之下,看到大家都在用MVVM,于是花大成本将MVC架构模式的老项目重构成了MVVM架构,这种重构个人看来其实并没有意义。更多的架构话题就不想在这里讨论了,笔者推荐几篇大神们关于架构的见解。

    1、被误解的 MVC 和被神化的 MVVM
    这是一篇被早已被翻烂了的文章,起码我个人反复阅读了数次,由家喻户晓的唐巧大神编写。
    2、iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构
    最近在Cocoa China上发表的一篇译文,笔者之前看过俩次原文,讲的比较形象。
    3、MVC,MVP 和 MVVM 的图示
    大神阮一峰的博文,以图形展示的方式使得各层结构更加清晰明了。
    4、猿题库 iOS 客户端架构设计
    猿题库 iOS客户端开发者蓝晨钰的博文,以实际项目猿题库详解了架构设计

    UIViewController瘦身

    架构模式并不是限制思维,相反应该是发散思维,我们并不应该为了架构而架构,架构应该是服务于我们的代码逻辑,打造更具有扩展性和健壮的代码结构。就比如,大多数开发者都会遇到一个同样的问题,随着项目一天天的壮大,功能越来越多,需求越来越多,而我们的UIViewController也变得越来越臃肿。在上面推荐的博文中,笔者们都或多或少的阐述了如何打造更轻量级的UIViewController,大都列举了一些共性策略:

    1、将一个界面中的数据获取抽象成一个类,这里面细分一下,包括了网络请求和数据库缓存,我们可以针对这俩点再次封装成俩个类。
    2、将一个界面中的数据处理逻辑抽象成一个类,这里面包含了各种数据转换和算法逻辑,比如数据检索,数据遍历等。
    3、将一个界面中数据传递到UIView视图的过程抽象成一个模型类,这里面就包含了对应到UIView视图的每一个数据的传递,比如icon图标,title标题,comment评论内容等。
    4、将一个界面中所有展示的UIView视图的添加和渲染抽象成一个类,这里包含了添加控件,自定义动画等。这个对视图的封装仍然可以细分,每一个自定义控件都可以单独封装,因为这样可以完美的在其他的UIViewController达到复用的目的。

    而完成了上述抽象之后,就会发现我们需要在UIViewController中完成的工作仅仅是处理视图交互逻辑和数据传递逻辑,这样我们的UIViewController就比较容易维护了。

    Three20架构
    每一种框架的兴起和衰落都有其相应的时势和必然性。虽然Three20饱受诟病,早已跌落神坛,但是它的存在是有一定道理的。虽然它在模块之间的耦合度较高,但是个人认为它对UIViewController的抽象和封装也是一个非常好的借鉴。在这里以Three20中对TTTableViewController的解耦为例,先上图看一下TTTableViewController包含的模块:


    这里根据上面的结构图具体地解释一下解耦的设计方式。TTTableViewController的设计遵从了经典的MVC模式,TTModel负责数据的获取和处理逻辑,TTTableView负责视图展示,TTTableViewController负责TTModel与TTTableView之间的通信逻辑和界面的控件添加渲染。而TTTableViewController在顺应了MVC模式的前提下,也做了一些扩展,它将TTTableViewDatasource接收数据传递的逻辑抽象出来封装成了TTTableItem。而TTTableItem就是关联TTModel传递数据的过程,因而我们也可以把这一层称作是MVVM架构模式中的ViewModel

    根据上面的图示,我们可以看到获取数据的逻辑都在TTModel中,而且界面控件添加和动画渲染这些逻辑仍然都在TTTableViewController中,因此我根据大神们的一些建议,对项目中的Three20进行了一下强化,先上图看一下增加的结构:


    可以清晰地看到,我将TTModel中处理缓存数据的逻辑抽象出来,单独放在了TTCacheModel中,此外还将TTTableViewController中添加控件和渲染动画的逻辑抽象出来,放到了TTViewRender中,这样TTTableViewController就只关心界面交互以及TTModel和TTTableItem之间的数据传递逻辑。

    链接:https://www.jianshu.com/p/a748f6fd99dd

    收起阅读 »

    iOS RESideMenu 侧滑 第三方类库

    下载地址:https://github.com/romaonthego/RESideMenu效果如下:官方案例自己的实现效果具体代码下:AppDelegate.m文件中- (BOOL)application:(UIApplication *)applicati...
    继续阅读 »

    下载地址:https://github.com/romaonthego/RESideMenu
    效果如下:官方案例


    自己的实现效果


    具体代码下:

    AppDelegate.m文件中

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法

    DEMOLeftMenuViewController *leftMenuViewController = [[DEMOLeftMenuViewController alloc] init];

    RESideMenu *sideMenuViewController = [[RESideMenu alloc] initWithContentViewController:[[MainTabBarController alloc]init] leftMenuViewController:leftMenuViewController rightMenuViewController:[UINavigationController new]];
    sideMenuViewController.backgroundImage = [UIImage imageNamed:@"005.jpg"];
    sideMenuViewController.menuPreferredStatusBarStyle = 1; // UIStatusBarStyleLightContent
    sideMenuViewController.delegate = self;
    // sideMenuViewController.parallaxContentMaximumRelativeValue=100;
    // sideMenuViewController.bouncesHorizontally=YES;
    sideMenuViewController.contentViewShadowColor = [UIColor blackColor];
    sideMenuViewController.contentViewShadowOffset = CGSizeMake(0, 0);
    sideMenuViewController.contentViewShadowOpacity = 0.6;
    sideMenuViewController.contentViewShadowRadius = 12;
    // sideMenuViewController.contentViewShadowEnabled = YES;
    // sideMenuViewController.panFromEdge=NO;
    self.window.rootViewController = sideMenuViewController;

    左侧的控制器DEMOLeftMenuViewController.h和DEMOLeftMenuViewController.m

    #import <UIKit/UIKit.h>
    #import "RESideMenu.h"

    @interface DEMOLeftMenuViewController : UIViewController<UITableViewDataSource, UITableViewDelegate, RESideMenuDelegate>


    @end
    #import "DEMOLeftMenuViewController.h"
    #import "HomeViewController.h"
    #import "UIViewController+RESideMenu.h"
    #import "LoginViewController.h"
    #import "resigeViewController.h"

    @interface DEMOLeftMenuViewController ()
    @property (strong, readwrite, nonatomic) UITableView *tableView;

    @end

    @implementation DEMOLeftMenuViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    self.navigationController.title=@"登陆";
    self.tableView = ({
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, (self.view.frame.size.height - 54 * 5) / 2.0f, self.view.frame.size.width, 54 * 5) style:UITableViewStylePlain];
    tableView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth;
    tableView.delegate = self;
    tableView.dataSource = self;
    tableView.opaque = NO;
    tableView.backgroundColor = [UIColor clearColor];
    tableView.backgroundView = nil;
    tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    tableView.bounces = NO;
    tableView.scrollsToTop = NO;
    tableView;
    });
    [self.view addSubview:self.tableView];
    }

    #pragma mark -
    #pragma mark UITableView Delegate

    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    switch (indexPath.row) {
    case 0:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[LoginViewController alloc] init]] animated:YES completion:nil];
    break;
    case 1:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[resigeViewController alloc] init]] animated:YES completion:nil];
    break;
    default:
    break;
    }
    }

    #pragma mark -
    #pragma mark UITableView Datasource

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    return 54;
    }

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    return 1;
    }

    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)sectionIndex
    {
    return 5;
    }

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *cellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    cell.backgroundColor = [UIColor clearColor];
    cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:21];
    cell.textLabel.textColor = [UIColor whiteColor];
    cell.textLabel.highlightedTextColor = [UIColor lightGrayColor];
    cell.selectedBackgroundView = [[UIView alloc] init];
    }

    NSArray *titles = @[@"Home", @"Calendar", @"Profile", @"Settings", @"Log Out"];
    NSArray *images = @[@"IconHome", @"IconCalendar", @"IconProfile", @"IconSettings", @"IconEmpty"];
    cell.textLabel.text = titles[indexPath.row];
    cell.imageView.image = [UIImage imageNamed:images[indexPath.row]];

    return cell;
    }


    @end

    主页HomeViewController.h和HomeViewController.m实现侧滑的关键代码

    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"个人中心"
    style:UIBarButtonItemStylePlain
    target:self
    action:@selector(presentLeftMenuViewControl

    这个第三番可以实现很多效果

    总结

    优点:

    1.里面的文件较少,不需要使用cocoapods即可运行。

    2.里面自定义API也比较多,可以设置变小的抽屉效果或者不变小。

    3.里面有两个事例程序,一个是纯手码,一个是Storyboard得。可见作者也非常喜欢IB开发,此框架用IB开发应该可以完美兼容。

    4.可以使用手势拖来拖去。

    5.项目里各个文件不需要继承,导入头文件就行。

    缺点:

    1.左边显示的菜单可选项是固定的几个button,暂时想把左边换成tableView还不知道可不可行。

    2.不能实现状态栏右移。

    3.暂时没找到两边控制器的占比怎么自定义。

    转自:https://www.cnblogs.com/qianLL/p/5425738.html

    收起阅读 »

    PNChart:一个简单漂亮的iOS图表库

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。要求PNChart 适用...
    继续阅读 »

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。

    要求

    PNChart 适用于 iOS 7.0 或更高版本,与 ARC 项目兼容。如果需要支持 iOS 6 ,请使用 0.8.1 版本之前的 PNChart 。注意 0.8.2 版本仅支持 iOS 8.0+ ,0.8.3 及更新版本支持 iOS 7.0+ 。

    PNChart 依赖于下列框架,这些框架已经嵌入了 Xcode 开发工具:

    Foundation.framework

    UIKit.framework

    CoreGraphics.framework

    QuartzCore.framework

    你需要 LLVM 3.0 或更高版本来建立 PNChart 。

    安装

    通过CocoaPods安装(推荐):

    1、在你的 Podfile 文件中添加pod 'PNChart'。

    2、运行pod install进行安装。

    3、按需导入头文件#import "PNChart.h"。

    手动安装:

    拷贝PNChart文件夹到你的工程中。

    使用

    #import "PNChart.h"

    //For Line Chart

    PNLineChart*lineChart=[[PNLineChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [lineChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    // Line Chart No.1

    NSArray*data01Array=@[@60.1,@160.1,@126.4,@262.2,@186.2];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=lineChart.xLabels.count;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart No.2

    NSArray*data02Array=@[@20.1,@180.1,@26.4,@202.2,@126.2];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=lineChart.xLabels.count;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    lineChart.chartData=@[data01,data02];

    [lineChartstrokeChart];
    #import "PNChart.h"

    //For BarC hart

    PNBarChart*barChart=[[PNBarChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [barChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    [barChartsetYValues:@[@1,@10,@2,@6,@3]];

    [barChartstrokeChart];



    ``` Objective-C

    #import "PNChart.h"

    //For Circle Chart

    PNCircleChart*circleChart=[[PNCircleChartalloc]initWithFrame:CGRectMake(0,80.0,SCREEN_WIDTH,100.0)total:[NSNumbernumberWithInt:100]current:[NSNumbernumberWithInt:60]clockwise:NOshadow:NO];

    circleChart.backgroundColor=[UIColorclearColor];

    [circleChartsetStrokeColor:PNGreen];

    [circleChart strokeChart];



    ```Objective-C

    # import "PNChart.h"

    //For Pie Chart

    NSArray*items=@[[PNPieChartDataItemdataItemWithValue:10color:PNRed],

    [PNPieChartDataItemdataItemWithValue:20color:PNBluedescription:@"WWDC"],

    [PNPieChartDataItemdataItemWithValue:40color:PNGreendescription:@"GOOL I/O"],

    ];

    PNPieChart*pieChart=[[PNPieChartalloc]initWithFrame:CGRectMake(40.0,155.0,240.0,240.0)items:items];

    pieChart.descriptionTextColor=[UIColorwhiteColor];

    pieChart.descriptionTextFont=[UIFontfontWithName:@"Avenir-Medium"size:14.0];

    [pieChartstrokeChart];
    # import "PNChart.h"

    //For Scatter Chart

    PNScatterChart*scatterChart=[[PNScatterChartalloc]initWithFrame:CGRectMake(SCREEN_WIDTH/6.0-30,135,280,200)];

    [scatterChartsetAxisXWithMinimumValue:20andMaxValue:100toTicks:6];

    [scatterChartsetAxisYWithMinimumValue:30andMaxValue:50toTicks:5];

    NSArray*data01Array=[selfrandomSetOfObjects];

    PNScatterChartData*data01=[PNScatterChartDatanew];

    data01.strokeColor=PNGreen;

    data01.fillColor=PNFreshGreen;

    data01.size=2;

    data01.itemCount=[[data01ArrayobjectAtIndex:0]count];

    data01.inflexionPointStyle=PNScatterChartPointStyleCircle;

    __blockNSMutableArray*XAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:0]];

    __blockNSMutableArray*YAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:1]];

    data01.getData=^(NSUIntegerindex){

    CGFloatxValue=[[XAr1objectAtIndex:index]floatValue];

    CGFloatyValue=[[YAr1objectAtIndex:index]floatValue];

    return[PNScatterChartDataItemdataItemWithX:xValueAndWithY:yValue];

    };

    [scatterChartsetup];

    self.scatterChart.chartData=@[data01];

    /***

    this is for drawing line to compare

    CGPoint start = CGPointMake(20, 35);

    CGPoint end = CGPointMake(80, 45);

    [scatterChart drawLineFromPoint:start ToPoint:end WithLineWith:2 AndWithColor:PNBlack];

    ***/

    scatterChart.delegate=self;

    图例

    PNChart 允许在折线图和饼状图中添加图例,图例可以竖向堆叠布置或者横向并列布置。

    #import "PNChart.h"

    //For Line Chart

    //Add Line Titles for the Legend

    data01.dataTitle=@"Alpha";

    data02.dataTitle=@"Beta Beta Beta Beta";

    //Build the legend

    self.lineChart.legendStyle=PNLegendItemStyleSerial;

    self.lineChart.legendFontSize=12.0;

    UIView*legend=[self.lineChartgetLegendWithMaxWidth:320];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(100,400,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    //For Pie Chart

    //Build the legend

    self.pieChart.legendStyle=PNLegendItemStyleStacked;

    self.pieChart.legendFontSize=12.0;

    UIView*legend=[self.pieChartgetLegendWithMaxWidth:200];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(130,350,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    更新数据

    实时更新数据也非常简单。

    Objective-C

    if([self.titleisEqualToString:@"Line Chart"]){

    // Line Chart #1

    NSArray*data01Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=data01Array.count;

    data01.inflexionPointStyle=PNLineChartPointStyleTriangle;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart #2

    NSArray*data02Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=data02Array.count;

    data02.inflexionPointStyle=PNLineChartPointStyleSquare;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    [self.lineChartsetXLabels:@[@"DEC 1",@"DEC 2",@"DEC 3",@"DEC 4",@"DEC 5",@"DEC 6",@"DEC 7"]];

    [self.lineChartupdateChartData:@[data01,data02]];

    }

    elseif([self.titleisEqualToString:@"Bar Chart"])

    {

    [self.barChartsetXLabels:@[@"Jan 1",@"Jan 2",@"Jan 3",@"Jan 4",@"Jan 5",@"Jan 6",@"Jan 7"]];

    [self.barChartupdateChartData:@[@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30)]];

    }

    elseif([self.titleisEqualToString:@"Circle Chart"])

    {

    [self.circleChartupdateChartByCurrent:@(arc4random()0)];

    }

    代理回调

    Objective-C

    #import "PNChart.h"

    //For LineChart

    lineChart.delegate=self;

    动画

    默认绘制图表时使用动画,可以通过设置displayAnimation = NO来禁止动画。

    Objective-C


    #import "PNChart.h"

    //For LineChart

    lineChart.displayAnimation=NO;

    ```Objective-C



    //For DelegateMethod

    -(void)userClickedOnLineKeyPoint:(CGPoint)pointlineIndex:(NSInteger)lineIndexpointIndex:(NSInteger)pointIndex{

    NSLog(@"Click Key on line %f, %f line index is %d and point index is %d",point.x,point.y,(int)lineIndex,(int)pointIndex);

    }

    -(void)userClickedOnLinePoint:(CGPoint)pointlineIndex:(NSInteger)lineIndex{

    NSLog(@"Click on line %f, %f, line index is %d",point.x,point.y,(int)lineIndex);

    }

    开源协议

    PNChart 在MIT开源协议下可以使用,也就是说,只要在项目副本中包含了版权声明和许可声明,用户就可以使用 PNChart 做任何想做的事情,而 PNChart 也无需承担任何责任。可以通过查看 LICENSE 文件来获取更多相关信息。

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

    链接:https://www.jianshu.com/p/9c162d6f8f14

    收起阅读 »

    Android原生绘图进度条+简单自定义属性代码生成器

    先一下效果:一、简单自定义属性生成器1.玩安卓的应该都写过自定义控件的自定义属性:如下我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?2.通过attrs.xml自动生成相应代码秉承着能用代码解决的问题,绝对不动手。能够靠...
    继续阅读 »

    先一下效果:

    圆形进度条.gif

    横向进度条.gif

    一、简单自定义属性生成器

    1.玩安卓的应该都写过自定义控件的自定义属性:如下

    自定义控件.png

    我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?

    2.通过attrs.xml自动生成相应代码

    秉承着能用代码解决的问题,绝对不动手。能够靠智商解决的问题,绝对不靠体力的大无畏精神:

    写了一个小工具,将代码里的内容自动生成一下:基本上就是字符串的切割和拼装,工具附在文尾

    使用方法与注意点:

    1.拷贝到AndroidStudio的test里,将attrs.xml的文件路径设置一下,运行
    2.自定义必须符合命名规则,如z_pb_on_height,专属前缀如z_,单词间下划线连接即可
    3.它并不是什么高大上的东西,只是简单的字符串切割拼组,只适用简单的自定义属性[dimension|color|boolean|string](不过一般的自定义属性也够用了)

    自动生成.png

    在开篇之前:先看一下Android系统内自定义控件的书写风格,毕竟跟原生看齐没有什么坏处

    看一下LinearLayout的源码:

    1.构造方法使用最多参数的那个,其他用this(XXX)调用

     public LinearLayout(Context context) {
    this(context, null);
    }

    public LinearLayout(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
    }

    public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    ...
    }

    2.自定义属性的书写

    1).先将自定义属性的成员变量定义好

    2).如果自定义属性不是很多,一个一个a.getXXX,默认值直接写在后面就行了
    3).看了一下TextView的源码,自定义属性很多,它是先定义默认值的变量,再使用,而且用switch来对a.getXXX进行赋值

    final TypedArray a = context.obtainStyledAttributes(
    attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);

    int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
    if (index >= 0) {
    setOrientation(index);
    }

    index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
    if (index >= 0) {
    setGravity(index);
    }

    boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
    if (!baselineAligned) {
    setBaselineAligned(baselineAligned);
    }
    ......
    a.recycle();

    一、水平的进度条

    条形进度条分析.png

    1.自定义控件属性:values/attrs.xml

        






















    2.初始代码:将进行一些常规处理

    public class TolyProgressBar extends ProgressBar {

    private Paint mPaint;
    private int mPBWidth;
    private RectF mRectF;
    private Path mPath;
    private float[] mFloat8Left;//左边圆角数组
    private float[] mFloat8Right;//右边圆角数组

    private float mProgressX;//进度理论值
    private float mEndX;//进度条尾部
    private int mTextWidth;//文字宽度
    private boolean mLostRight;//是否不画右边
    private String mText;//文字

    private int mPbBgColor = 0xffC9C9C9;
    private int mPbOnColor = 0xff54F340;
    private int mPbOnHeight = dp(6);
    private int mPbBgHeight = dp(6);
    private int mPbTxtColor = 0xff525252;
    private int mPbTxtSize = sp(10);
    private int mPbTxtOffset = sp(10);
    private boolean mPbTxtGone= false;

    public TolyProgressBar(Context context) {
    this(context, null);
    }

    public TolyProgressBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TolyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyProgressBar);
    mPbOnHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_on_height, mPbOnHeight);
    mPbTxtOffset = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_offset, mPbTxtOffset);
    mPbOnColor = a.getColor(R.styleable.TolyProgressBar_z_pb_on_color, mPbOnColor);
    mPbTxtSize = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_size, mPbTxtSize);
    mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
    mPbBgColor = a.getColor(R.styleable.TolyProgressBar_z_pb_bg_color, mPbBgColor);
    mPbTxtGone = a.getBoolean(R.styleable.TolyProgressBar_z_pb_txt_gone, mPbTxtGone);
    a.recycle();

    init();
    }

    private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setTextSize(mPbTxtSize);
    mPaint.setColor(mPbOnColor);
    mPaint.setStrokeWidth(mPbOnHeight);

    mRectF = new RectF();
    mPath = new Path();


    mFloat8Left = new float[]{//仅左边两个圆角--为背景
    mPbOnHeight / 2, mPbOnHeight / 2,//左上圆角x,y
    0, 0,//右上圆角x,y
    0, 0,//右下圆角x,y
    mPbOnHeight / 2, mPbOnHeight / 2//左下圆角x,y
    };

    mFloat8Right = new float[]{
    0, 0,//左上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
    0, 0//左下圆角x,y
    };
    }

    }

    private int sp(int sp) {
    return (int) TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    private int dp(int dp) {
    return (int) TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }


    2.测量:

        @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = measureHeight(heightMeasureSpec);
    setMeasuredDimension(width, height);
    mPBWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//进度条实际宽度
    }

        /**
    * 测量高度
    *
    * @param heightMeasureSpec
    * @return
    */

    private int measureHeight(int heightMeasureSpec) {
    int result = 0;
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    int size = MeasureSpec.getSize(heightMeasureSpec);

    if (mode == MeasureSpec.EXACTLY) {
    //控件尺寸已经确定:如:
    // android:layout_height="40dp"或"match_parent"
    result = size;
    } else {
    int textHeight = (int) (mPaint.descent() - mPaint.ascent());
    result = getPaddingTop() + getPaddingBottom() + Math.max(
    Math.max(mPbBgHeight, mPbOnHeight), Math.abs(textHeight));

    if (mode == MeasureSpec.AT_MOST) {//最多不超过
    result = Math.min(result, size);
    }
    }
    return result;
    }
    复制代码

    3.绘制:

        @Override
    protected synchronized void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.translate(getPaddingLeft(), getHeight() / 2);

    parseBeforeDraw();//1.绘制前对数值进行计算以及控制的flag设置

    if (getProgress() == 100) {//进度达到100后文字消失
    whenOver();//2.
    }
    if (mEndX > 0) {//当进度条尾部>0绘制
    drawProgress(canvas);//3.
    }
    if (!mPbTxtGone) {//绘制文字
    mPaint.setColor(mPbTxtColor);
    int y = (int) (-(mPaint.descent() + mPaint.ascent()) / 2);
    canvas.drawText(mText, mProgressX, y, mPaint);
    } else {
    mTextWidth = 0 - mPbTxtOffset;
    }
    if (!mLostRight) {//绘制右侧
    drawRight(canvas);/4.
    }

    canvas.restore();
    }

    1).praseBeforeDraw()

    /**
    * 对数值进行计算以及控制的flag设置
    */

    private void parseBeforeDraw() {
    mLostRight = false;//lostRight控制是否绘制右侧
    float radio = getProgress() * 1.f / getMax();//当前百分比率
    mProgressX = radio * mPBWidth;//进度条当前长度
    mEndX = mProgressX - mPbTxtOffset / 2; //进度条当前长度-文字间隔的左半
    mText = getProgress() + "%";
    if (mProgressX + mTextWidth > mPBWidth) {
    mProgressX = mPBWidth - mTextWidth;
    mLostRight = true;
    }
    //文字宽度
    mTextWidth = (int) mPaint.measureText(mText);
    }

    2).whenOver()

    /**
    * 当结束是执行:
    */

    private void whenOver() {
    mPbTxtGone = true;
    mFloat8Left = new float[]{//只有进度达到100时让进度圆角是四个
    mPbBgHeight / 2, mPbBgHeight / 2,//左上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2//左下圆角x,y
    };
    }

    3).drawProgress()

    /**
    * 绘制左侧:(进度条)
    *
    * @param canvas
    */

    private void drawProgress(Canvas canvas) {
    mPath.reset();
    mRectF.set(0, mPbOnHeight / 2, mEndX, -mPbOnHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Left, Path.Direction.CW);//顺时针画
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPbOnColor);
    canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
    }

    4).drawRight()

    /**
    * 绘制左侧:(背景)
    *
    * @param canvas
    */

    private void drawRight(Canvas canvas) {
    float start = mProgressX + mPbTxtOffset / 2 + mTextWidth;
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeWidth(mPbBgHeight);
    mPath.reset();
    mRectF.set(start, mPbBgHeight / 2, mPBWidth, -mPbBgHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Right, Path.Direction.CW);//顺时针画
    canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
    }

    xml里使用:


    三、圆形进度条
    1.自定义属性





    2.代码实现:

    /**
    * 作者:张风捷特烈


    * 时间:2018/11/9 0009:11:49


    * 邮箱:1981462002@qq.com


    * 说明:圆形进度条
    */

    public class TolyRoundProgressBar extends TolyProgressBar {

    private int mPbRadius = dp(30);//进度条半径
    private int mMaxPaintWidth;

    public TolyRoundProgressBar(Context context) {
    this(context, null);
    }

    public TolyRoundProgressBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TolyRoundProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyRoundProgressBar);
    mPbRadius = (int) a.getDimension(R.styleable.TolyRoundProgressBar_z_pb_radius, mPbRadius);
    mPbOnHeight = (int) (mPbBgHeight * 1.8f);//让进度大一点
    a.recycle();

    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setDither(true);
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    mMaxPaintWidth = Math.max(mPbBgHeight, mPbOnHeight);
    int expect = mPbRadius * 2 + mMaxPaintWidth + getPaddingLeft() + getPaddingRight();
    int width = resolveSize(expect, widthMeasureSpec);
    int height = resolveSize(expect, heightMeasureSpec);
    int realWidth = Math.min(width, height);
    mPaint.setStrokeCap(Paint.Cap.ROUND);

    mPbRadius = (realWidth - getPaddingLeft() - getPaddingRight() - mMaxPaintWidth) / 2;
    setMeasuredDimension(realWidth, realWidth);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {

    String txt = getProgress() + "%";
    float txtWidth = mPaint.measureText(txt);
    float txtHeight = (mPaint.descent() + mPaint.ascent()) / 2;
    canvas.save();
    canvas.translate(getPaddingLeft() + mMaxPaintWidth / 2, getPaddingTop() + mMaxPaintWidth / 2);
    drawDot(canvas);
    mPaint.setStyle(Paint.Style.STROKE);
    //背景
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeWidth(mPbBgHeight);
    canvas.drawCircle(mPbRadius, mPbRadius, mPbRadius, mPaint);
    //进度条
    mPaint.setColor(mPbOnColor);
    mPaint.setStrokeWidth(mPbOnHeight);
    float sweepAngle = getProgress() * 1.0f / getMax() * 360;//完成角度
    canvas.drawArc(
    0, 0, mPbRadius * 2, mPbRadius * 2,
    -90, sweepAngle, false, mPaint);
    //文字
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPbTxtColor);
    canvas.drawText(txt, mPbRadius - txtWidth / 2, mPbRadius - txtHeight / 2, mPaint);
    canvas.restore();
    }

    /**
    * 绘制一圈点
    *
    * @param canvas
    */

    private void drawDot(Canvas canvas) {
    canvas.save();
    int num = 40;
    canvas.translate(mPbRadius, mPbRadius);
    for (int i = 0; i < num; i++) {
    canvas.save();
    int deg = 360 / num * i;
    canvas.rotate(deg);
    mPaint.setStrokeWidth(dp(3));
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    if (i * (360 / num) < getProgress() * 1.f / getMax() * 360) {
    mPaint.setColor(mPbOnColor);
    }
    canvas.drawLine(0, mPbRadius * 3 / 4, 0, mPbRadius * 4 / 5, mPaint);
    canvas.restore();
    }
    canvas.restore();
    }
    }




    附录:简单自定义属性生成器

    public class Attrs2Code {
    @Test
    public void main() {
    File file = new File("C:\\Users\\Administrator\\Desktop\\attrs.xml");
    initAttr("z_", file);
    }

    public static void initAttr(String preFix, File file) {
    HashMap format = format(preFix, file);
    String className = format.get("className");
    String result = format.get("result");
    StringBuilder sb = new StringBuilder();
    sb.append("TypedArray a = context.obtainStyledAttributes(attrs, R.styleable." + className + ");\r\n");
    format.forEach((s, s2) -> {
    String styleableName = className + "_" + preFix + s;
    if (s.contains("_")) {
    String[] partStrArray = s.split("_");
    s = "";
    for (String part : partStrArray) {
    String partStr = upAChar(part);
    s += partStr;
    }
    }
    if (s2.equals("dimension")) {
    // mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
    sb.append("m" + s + " = (int) a.getDimension(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("color")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getColor(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("boolean")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getBoolean(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("string")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getString(R.styleable." + styleableName + ");\r\n");
    }
    });
    sb.append("a.recycle();\r\n");
    System.out.println(result);
    System.out.println(sb.toString());
    }

    /**
    * 读取文件+解析
    *
    * @param preFix 前缀
    * @param file 文件路径
    */

    public static HashMap format(String preFix, File file) {
    HashMap container = new HashMap<>();
    if (!file.exists() && file.isDirectory()) {
    return null;
    }
    FileReader fr = null;
    try {
    fr = new FileReader(file);
    //字符数组循环读取
    char[] buf = new char[1024];
    int len = 0;
    StringBuilder sb = new StringBuilder();
    while ((len = fr.read(buf)) != -1) {
    sb.append(new String(buf, 0, len));
    }
    String className = sb.toString().split(""));
    container.put("className", className);
    String[] split = sb.toString().split("<");
    String part1 = "private";
    String type = "";//类型
    String name = "";
    String result = "";
    String def = "";//默认值

    StringBuilder sb2 = new StringBuilder();
    for (String s : split) {
    if (s.contains(preFix)) {
    result = s.split(preFix)[1];
    name = result.substring(0, result.indexOf("\""));
    type = result.split("format=\"")[1];
    type = type.substring(0, type.indexOf("\""));
    container.put(name, type);
    if (type.contains("color") || type.contains("dimension") || type.contains("integer")) {
    type = "int";
    def = "0";
    }
    if (result.contains("fraction")) {
    type = "float";
    def = "0.f";
    }
    if (result.contains("string")) {
    type = "String";
    def = "\"toly\"";
    }
    if (result.contains("boolean")) {
    type = "boolean";
    def = "false";

    }
    if (name.contains("_")) {
    String[] partStrArray = name.split("_");
    name = "";
    for (String part : partStrArray) {
    String partStr = upAChar(part);
    name += partStr;
    }
    sb2.append(part1 + " " + type + " m" + name + "= " + def + ";\r\n");
    }
    container.put("result", sb2.toString());
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    try {
    if (fr != null) {
    fr.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    return container;
    }

    /**
    * 将字符串仅首字母大写
    *
    * @param str 待处理字符串
    * @return 将字符串仅首字母大写
    */

    public static String upAChar(String str) {
    String a = str.substring(0, 1);
    String tail = str.substring(1);
    return a.toUpperCase() + tail;
    }
    }

    代码下载:bobing107-IPhoneWatch_progressbar-master.zip

    收起阅读 »