注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS——SDWebImage加载WebP图片

1.确定第三方库首先直接去SDWebImage的仓库,里面直接就有关于WebP的仓库地址也就是SDWebImageWebPCoder,直接pod 'SDWebImageWebPCoder'就行。(如果项目里没有SDWebImage,需要pod 'SDWebIm...
继续阅读 »

1.确定第三方库

首先直接去SDWebImage的仓库,里面直接就有关于WebP的仓库地址



也就是SDWebImageWebPCoder,直接pod 'SDWebImageWebPCoder'就行。(如果项目里没有SDWebImage,需要pod 'SDWebImage')

这里要注意!!!是pod 'SDWebImageWebPCoder'

我搜索SDWebImage加载WebP,权重高的答案都是pod 'SDWebImage/WebP',但是这个仓库我在SDWebImage的repositories里搜索不到,也就是说没有这个仓库,结果如图。


猜测可能之前的旧仓库是这个名字,那些文章也一直没更新,但是权重又高,不免误人子弟了一番。

2.导入SDWebImageWebPCoder

大概率会在pod install时报错,因为libwebp这个仓库的地址连接不上。

1、在终端输入pod repo 查看 cocoapods 在本机的PATH,每个人的路径都可能不一样


2、复制trunk的path,command + shift + G 输入上一步的地址,依次点击Specs-->1-->9-->2-->libwebp。(这里要注意有可能你的路径是cocoapods的path)

3、选择报错的版本打开,将source下git地址更改为

https://github.com/webmproject/libwebp.git


4、pod install(如果还报一样的错,那么是第2步出了问题,去另一个路径改source-git的地址即可)

3.使用SDWebImageWebPCoder

SDImageWebPCoder *webPCoder = [SDImageWebPCoder sharedCoder];
[[SDImageCodersManager sharedManager] addCoder:webPCoder];

NSData *webpData;
UIImage *wimage = [[SDImageWebPCoder sharedCoder] decodedImageWithData:webpData options:nil];
NSData *webpData;
[UIImage sd_imageWithWebPData:webpData];

经测试以上两种写法都能成功加载webp图片

转自:https://www.jianshu.com/p/74fab9c7de77

收起阅读 »

iOS dispatch_semaphore信号量的使用(for循环请求网络时,使用信号量导致死锁)

有的时候我们会遇到这样的需求:循环请求网络,但是在循环的过程中,必须上一个网络回调完成后才能请求下一个网络即进行下一个循环,也就是所谓的多个异步网络做同步请求,首先想到的就是用信号量拦截,但是发现AFNetWorking配合信号量使用时,网络不回调了,是什么原...
继续阅读 »

有的时候我们会遇到这样的需求:
循环请求网络,但是在循环的过程中,必须上一个网络回调完成后才能请求下一个网络即进行下一个循环,也就是所谓的多个异步网络做同步请求,首先想到的就是用信号量拦截,但是发现AFNetWorking配合信号量使用时,网络不回调了,是什么原因引起的网络无法回调。下面我们模拟下正常使用过程并分析,如下:

-(void)semaphoreTest{

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

for (int i = 0; i<10; i++) {
[self semaphoreTestBlock:^(NSString *TNT) {
NSLog(@"任务完成 %d",i);
dispatch_semaphore_signal(semaphore);
}];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"信号量限制 %d",i);
}
}

//这里用延迟模拟异步网络请求
-(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{
/*
queue 的类型无论是串行队列还是并行队列并不影响最终结果
如果 queue = dispatch_get_main_queue() 将会堵塞组线程,造成死锁
*/
dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
block(@"完成");
});
}

这段代码的输出结果为:

2019-10-11 14:40:23.961328+0800 LJC[9013:1358198] 任务完成 0
2019-10-11 14:40:23.961751+0800 LJC[9013:1356826] 信号量限制 0
2019-10-11 14:40:25.061312+0800 LJC[9013:1358198] 任务完成 1
2019-10-11 14:40:25.061673+0800 LJC[9013:1356826] 信号量限制 1
2019-10-11 14:40:26.062082+0800 LJC[9013:1356931] 任务完成 2
2019-10-11 14:40:26.062381+0800 LJC[9013:1356826] 信号量限制 2
2019-10-11 14:40:27.062883+0800 LJC[9013:1356931] 任务完成 3
2019-10-11 14:40:27.063275+0800 LJC[9013:1356826] 信号量限制 3
2019-10-11 14:40:28.160535+0800 LJC[9013:1356931] 任务完成 4
2019-10-11 14:40:28.160988+0800 LJC[9013:1356826] 信号量限制 4
2019-10-11 14:40:29.161327+0800 LJC[9013:1356931] 任务完成 5
2019-10-11 14:40:29.161512+0800 LJC[9013:1356826] 信号量限制 5
2019-10-11 14:40:30.161756+0800 LJC[9013:1356931] 任务完成 6
2019-10-11 14:40:30.161989+0800 LJC[9013:1356826] 信号量限制 6
2019-10-11 14:40:31.261507+0800 LJC[9013:1356931] 任务完成 7
2019-10-11 14:40:31.261912+0800 LJC[9013:1356826] 信号量限制 7
2019-10-11 14:40:32.361503+0800 LJC[9013:1356931] 任务完成 8
2019-10-11 14:40:32.361870+0800 LJC[9013:1356826] 信号量限制 8
2019-10-11 14:40:33.461544+0800 LJC[9013:1358198] 任务完成 9
2019-10-11 14:40:33.461953+0800 LJC[9013:1356826] 信号量限制 9

如果我们把
dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
替换成
dispatch_queue_t queue = dispatch_get_main_queue()
发现输出结果为空

为什么呢?
首先我们要知道
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
他怎么才能实现锁的功能,他的锁其实是针对线程的,我们当前任务是在主线程执行的,我们就需要在主线程上锁。
完成任务我们去将信号量+1,即执行
dispatch_semaphore_signal(semaphore)
这个时候发现你的回调也是在主线程触发的,但是此时主线程上锁,已经卡住了,是不能让你在主线程做任务的,这就形成了相互等待,卡死了,所以我们需要将回调任务放在非主线程中(以目前这个例子来说,就是非主线程,其实我们最终调整的目的是让执行任务和回调任务不在同一线程即可)。

那我们如果将任务(for循环)在子线程中执行,回调在主线程中是否可以呢?下面我们修改代码

-(void)semaphoreTest{

dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

for (int i = 0; i<10; i++) {
[self semaphoreTestBlock:^(NSString *TNT) {
NSLog(@"任务完成 %d",i);
dispatch_semaphore_signal(semaphore);
}];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"信号量限制 %d",i);
}
});
}

-(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{

// dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
block(@"完成");
});
}

输出结果:

2019-10-11 14:51:00.224109+0800 LJC[9063:1362953] 任务完成 0
2019-10-11 14:51:00.224486+0800 LJC[9063:1363099] 信号量限制 0
2019-10-11 14:51:01.325117+0800 LJC[9063:1362953] 任务完成 1
2019-10-11 14:51:01.325493+0800 LJC[9063:1363099] 信号量限制 1
2019-10-11 14:51:02.425129+0800 LJC[9063:1362953] 任务完成 2
2019-10-11 14:51:02.425491+0800 LJC[9063:1363099] 信号量限制 2
2019-10-11 14:51:03.524266+0800 LJC[9063:1362953] 任务完成 3
2019-10-11 14:51:03.524715+0800 LJC[9063:1363099] 信号量限制 3
2019-10-11 14:51:04.625254+0800 LJC[9063:1362953] 任务完成 4
2019-10-11 14:51:04.625659+0800 LJC[9063:1363099] 信号量限制 4
2019-10-11 14:51:05.725228+0800 LJC[9063:1362953] 任务完成 5
2019-10-11 14:51:05.725573+0800 LJC[9063:1363099] 信号量限制 5
2019-10-11 14:51:06.726094+0800 LJC[9063:1362953] 任务完成 6
2019-10-11 14:51:06.726442+0800 LJC[9063:1363099] 信号量限制 6
2019-10-11 14:51:07.825270+0800 LJC[9063:1362953] 任务完成 7
2019-10-11 14:51:07.825613+0800 LJC[9063:1363099] 信号量限制 7
2019-10-11 14:51:08.925323+0800 LJC[9063:1362953] 任务完成 8
2019-10-11 14:51:08.925674+0800 LJC[9063:1363099] 信号量限制 8
2019-10-11 14:51:10.025359+0800 LJC[9063:1362953] 任务完成 9
2019-10-11 14:51:10.025722+0800 LJC[9063:1363099] 信号量限制 9

这就验证了我们的想法, 执行任务和任务回调是不能在一个线程中的

整理

在使用信号量的时候,需要注意 dispatch_semaphore_wait 需要和 任务 放在同一线程,在任务执行异步回调的时候,需要将回调放在与执行任务不同的线程中,因为如果在同一线程中 dispatch_semaphore_wait 操作会造成相互等待导致死锁问题,我们在使用 AFNetWorking 的时候,他默认的回调是在 主线程中,所以我们在配合 AFNetWorking 使用信号量的时候可以指定 AFNetWorking 的回调线程,或者我们在执行任务的时候,将任务放在其他线程

注释:
写这篇文章是因为我在用信号量配合AFNetWorking做网路任务的时候发现一只卡死,在网上找的都说指定AFNetWorking 的 completionQueue ,然后我更改了代码,request是我们网络对AFNetWorking的封装对象实例,按理来说是没问题的,但是不知道为什么还是会造成死锁。目前原因没找到。所以我将for循环再放了子线程中

request.sessionManager.completionQueue = dispatch_get_global_queue(0, 0);

如发现理解错误,望指出 ^_^ THANKS

转自:https://www.jianshu.com/p/91e9e38e3f51

收起阅读 »

iOS 登录接口封装实践

登录。。。基本所有APP都少不了,开始写APP,可能首先就是从登录开始我也一样,我手上有一个封装了所有账户体系相关接口的SDK,运行良好但也遇到一些烦心事,就拿登录来说说吧。首先有如下相关封装,很常见,也无需太多解释:import Foundationpubl...
继续阅读 »

登录。。。基本所有APP都少不了,开始写APP,可能首先就是从登录开始
我也一样,我手上有一个封装了所有账户体系相关接口的SDK,运行良好但也遇到一些烦心事,就拿登录来说说吧。

首先有如下相关封装,很常见,也无需太多解释:

import Foundation

public typealias Response = (_ json: String?, _ error: Error?) -> Void

// 账户体系管理器
public class AccountMgr: NSObject {
private override init() {}
public static let shared = AccountMgr()
}

public extension AccountMgr {
/// 登录
/// - Parameters:
/// - accountType: 账户类型 see `AccountType`
/// - password: 密码
/// - res: 请求结果
func login(by accountType: AccountType, password: String, res: Response?) {
var params = [String: Any]()
switch accountType {
case let .email(email):
params["type"] = "email"
params["email"] = email
case let .mobile(mobile, mobileArea):
params["type"] = "mobile"
params["mobile"] = mobile
params["mobileArea"] = mobileArea
}

params["password"] = password
//网络请求,并回调
//request(type: .post, api: .login, params: params, res: res)
}
}

/// 账号类型
public enum AccountType {
/// 手机号
/// - mobile: 手机号
/// - mobileArea: 国家区号(中国 86)
case mobile(_ phoneNumber: String, mobileArea: String = "86")
/// 邮箱
case email(_ email: String)
}

使用也很方便:

// 分开使用
AccountMgr.shared.login(by: .email(""), password: "", res: nil)
AccountMgr.shared.login(by: .mobile("", mobileArea: ""), password: "", res: nil)

// 合并使用
var loginType: AccountType
if inputEmail {
loginType = .email("test@weixian.com")
} else {
loginType = .mobile("18000000000", mobileArea: "86")
}
AccountMgr.shared.login(by: loginType, password: "xxxxx", res: nil)

无论是邮箱,手机号登录分开逻辑登录,还是统一的登录管理器登录都能胜任,并且只有两种登录,分开写也不会多很多代码。

有一天,这个SDK需要在OC项目中使用

感觉没爱了,懒得想太多,直接废弃了Swift 枚举的便利性,写成了两个方法:

public class AccountMgr: NSObject {
private override init() {}
@objc(shareInstance)
public static let shared = AccountMgr()
}

@objc func loginBy(email: String, password: String, res: Response?)

@objc func loginBy(mobile: String, mobilArea: String, password: String, res: Response?)

之所以写成loginBy(email:)而不是login(by email:),主要是为了swift 转 OC 后使用的时候能直接看懂,也不需要去查看定义,看如下截图就能明白了:


第一个方法不看定义,应该没办法了解参数应该填什么了。

就这样,我的SDK又运行了一段时间,看起来也没什么大问题,无非是手机登录和邮箱登录一定要分开调用罢了

又有一天,这个登录方法要增加用户账号登录

依样画葫芦,我又增加了一个接口~~~,只是这样,那故事就结束了。

可惜,我还有第三方绑定接口,即微信登录后绑定手机,邮箱,或账号、、、、这里又三个接口,还有查询账号信息又三个,还有。。。又三个。。。,还有。。。又三个。。。

这个时候我又开始怀念第一版的接口了,其实这很容易解决,只要一个整型枚举,然后把多出来的参数设置为可选,虽然使用的时候会有点奇怪,但是很好的解决了问题。并且最终我也是这么做的,可我还是想在Swift中能够更好的使用Swfit特性,写出更简洁的代码。。所以我写了两套接口。。。。,一套OC使用,一套Swfit使用,因为我总觉得在不久的将来,我就不需要支持OC了:

首先增加了一个OC的类型枚举:

@objc public enum AccountType_OC: Int {
case mobile
case email
case userId
}

然后增加了一个只有OC可用的方法:

@available(swift 10.0)
@objc func loginBy(accountType: AccountType_OC, account: String, password: String, mobileArea: String?, res: Response?) {
let type = getSwiftAccountType(accountType: accountType, account: account, mobileArea: mobileArea)
login(by: type, password: password, res: res)
}

private func getSwiftAccountType(accountType: AccountType_OC, account: String, mobileArea: String?) -> AccountType {
var type: AccountType
switch accountType {
case .mobile:
guard let mobileArea = mobileArea else { fatalError("need mobile area") }
type = .mobile(account, mobileArea: mobileArea)
case .email:
type = .email(account)
case .userId:
type = .userId(account)
}
return type
}

OC中没办法给参数赋默认值,即类似mobileArea: String = "86" 这种,完全没有用。。。

私有类型转换的方法的封装,使得所有其他方法可以快速转换,关于@available(swift 10.0) 意思就是说只有Swift 版本10.0只后才可以使用。。即变相达到了,在Swift 代码中不会出现这个方法,只有下面方法可以使用:

func login(by accountType: AccountType, password: String, res: Response?)

基本就是这样了,看起来很麻烦,也确实挺麻烦,其实完全可以只保留OC使用的方法,这完全归于我的代码洁癖,以及我自己在使用Swift和对于日后去掉OC支持时我可以快乐的删代码的白日幻想。

当然,如果你只是在自己的混编APP内部封装一些接口,那一套接口应该是比较好的,如果你的是SDK,同时你也不是很怕麻烦,像我这样写也许会有一些意外的收获。

链接:https://www.jianshu.com/p/247c1e923c5c

收起阅读 »

iOS自定义键盘-简单版

为什么说是简单版,因为这里只说一个数字键盘。一,怎么自定义键盘随便一个view都可以作为键盘,主要代码是为你的输入框指定inputView,这个inputView就是键盘,键盘具体什么样子都可以。kfZNumberKeyBoard * mkb = [kfZNu...
继续阅读 »

为什么说是简单版,因为这里只说一个数字键盘。
一,怎么自定义键盘
随便一个view都可以作为键盘,主要代码是为你的输入框指定inputView,这个inputView就是键盘,键盘具体什么样子都可以。

kfZNumberKeyBoard * mkb = [kfZNumberKeyBoard moneyKeyBoardBuyer];

UITextField * field = [[UITextField alloc]init];
field.backgroundColor = [UIColor cyanColor];
field.inputView = mkb;
[self.view addSubview:field];
field.frame = CGRectMake(20, NavBottom + 50, DEF_SCREEN_WIDTH - 40, 40);

二,自定义键盘怎么实现各种输入
这里千万不要自己拼接字符串太容易出问题了,用系统自带的方法。我们发现不管UITextField还是UITextView都遵循UITextInput协议,这个协议又遵循UIKeyInput协议,我们用的就是UIKeyInput协议中的方法。

- (void)insertText:(NSString *)text;//插入文字,不用处理光标位置
- (void)deleteBackward;//删除,不用处理光标位置

用这两个方法是不是事情就特别简单了,其实说到这里已经可以了,怎么做都说完了。不过我还是推销一下我写的数字键盘吧。最后面我会贴出代码用的可以拷贝改一下。

三,数字键盘
先看效果图:


a.UI布局上,删除和确定是单独的按键,其他部分我用了collectionView,想着之后做的乱序加密效果好做,打乱数据源刷新一下就行(当然现在没有,不是懒,过渡开发是病)
b.获取当前输入框,这里为了不在外面传,直接在内部监听了输入框开始输入和结束输入。
c.加了几个输入限制:
1.有小数点不能在输入小数点
2.内容为空输入小数点时,前面自动补0
3.最大小数位数限制(测试不多可能有bug哦)
4.移除焦点时小数点前面没东西自动补0
5.输入框有内容确定可以点击,输入框没内容确定不能点击。

下面是代码了:

@interface kfZNumberKeyBoard : UIView

/** 确认按键 */
@property (nonatomic, strong) UIButton * returnButton;
/** 有没有小数点 */
@property (nonatomic, assign) BOOL hiddenPoint;
/** 小数位数,为0不限制,不需要小数时请使用hiddenPoint隐藏点 默认是2 */
@property (nonatomic, assign) NSUInteger decimalCount;
/** 整体高度 */
@property (nonatomic, assign, readonly) CGFloat KFZNumberKeyBoardHeight;

+(instancetype)moneyKeyBoardBuyer;
+(instancetype)moneyKeyBoardSeller;

-(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint;

@end
#import "kfZNumberKeyBoard.h"
#import "KFZKeyBoardCell.h"
@interface kfZNumberKeyBoard ()

@property(nonatomic, weak) UIView * textInputView;

/** 删除按键 */
@property (nonatomic, strong) UIButton * deleteButton;

@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) NSArray *dataSource;

/** 间隔 */
@property (nonatomic, assign) CGFloat KFZNumberKeyBoardSpace;
/** 数字按键高度 */
@property (nonatomic, assign) CGFloat KFZNumberKeyBoardItemHeight;

@end

@implementation kfZNumberKeyBoard

+(instancetype)moneyKeyBoardBuyer{
kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
return keyBoard;
}

+(instancetype)moneyKeyBoardSeller{
kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
keyBoard.returnButton.backgroundColor = [UIColor maintonal_sellerMain];
return keyBoard;
}

-(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint{
self = [super init];
if (self) {
_hiddenPoint = hiddenPoint;
_KFZNumberKeyBoardItemHeight = 50.f;
_KFZNumberKeyBoardSpace = 0.5;
_KFZNumberKeyBoardHeight = _KFZNumberKeyBoardItemHeight * 4 + _KFZNumberKeyBoardSpace * 5 + HOMEINDICATOR_HEIGHT;
_decimalCount = 2;

self.frame = CGRectMake(0, 0, DEF_SCREEN_WIDTH, _KFZNumberKeyBoardHeight);

_deleteButton = [[UIButton alloc]init];
_deleteButton.backgroundColor = [UIColor color_FAFAFA];
[_deleteButton setImage:[UIImage imageNamed:@"keyboard_icon_backspace"] forState:UIControlStateNormal];
[_deleteButton addTarget:self action:@selector(deleteEvent) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_deleteButton];
[_deleteButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(_KFZNumberKeyBoardSpace);
make.right.mas_equalTo(0.f);
make.width.equalTo(self).multipliedBy(0.25);
}];

_returnButton = [[UIButton alloc]init];
[_returnButton setTitle:@"确定" forState:UIControlStateNormal];
[_returnButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_returnButton.titleLabel.font = [UIFont custemFontOfSize:20 weight:UIFontWeightRegular];
_returnButton.backgroundColor = [UIColor mainTonal_main];
[_returnButton addTarget:self action:@selector(returnEvent) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_returnButton];
[_returnButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_deleteButton.mas_bottom);
make.right.equalTo(_deleteButton);
make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
make.height.equalTo(_deleteButton);
make.width.equalTo(_deleteButton).offset(_KFZNumberKeyBoardSpace);
}];

//101对应小数点 102对应收起键盘 修改的话其他的判断逻辑也要修改
_dataSource = @[@(1), @(2), @(3), @(4), @(5), @(6), @(7), @(8), @(9), @(101), @(0), @(102)];

UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init];
layout.itemSize = CGSizeMake((DEF_SCREEN_WIDTH * 3.f/4.f - _KFZNumberKeyBoardSpace*3)/3.f, (_KFZNumberKeyBoardHeight - HOMEINDICATOR_HEIGHT - _KFZNumberKeyBoardSpace*5)/4.f);
layout.sectionInset = UIEdgeInsetsMake(_KFZNumberKeyBoardSpace, 0, _KFZNumberKeyBoardSpace, _KFZNumberKeyBoardSpace);
layout.minimumLineSpacing = _KFZNumberKeyBoardSpace;
layout.minimumInteritemSpacing = _KFZNumberKeyBoardSpace;

_collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView.dataSource = self;
_collectionView.delegate = self;
[_collectionView registerClass:[KFZKeyBoardCell class] forCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class])];
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.scrollEnabled = NO;
[self addSubview:_collectionView];
[_collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.mas_equalTo(0.f);
make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
make.right.equalTo(_deleteButton.mas_left);
}];


[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
}
return self;
}

-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidEndEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
}

#pragma mark - response

-(void)textInputWithNumber:(NSNumber *)number{
NSString *strValue = [self inputViewString];

if ([number isEqualToNumber:@(101)]) {
if ([strValue containsString:@"."]){
return;
}else{
if ([strValue length] <= 0)
[self.textInputView insertText:@"0."];
else
[self.textInputView insertText:@"."];
}
}else{
if ([strValue containsString:@"."] && _decimalCount > 0) {
NSInteger pointLocation = [strValue rangeOfString:@"."].location;
NSInteger curDecimalCount = strValue.length - pointLocation - 1;
if (curDecimalCount >= _decimalCount) {
NSInteger cursorLocation = [self inputViewSelectRangeLocation];
if (cursorLocation <= pointLocation) {
[_textInputView insertText:number.stringValue];
}
}else{
[_textInputView insertText:number.stringValue];
}
}else{
[_textInputView insertText:number.stringValue];
}
}
[self freshReturnButtonEnabled];
}

-(void)deleteEvent{
[_textInputView deleteBackward];
[self freshReturnButtonEnabled];
}

-(void)returnEvent{
[_textInputView resignFirstResponder];
}

-(void)textInputViewDidBeginEditing:(NSNotification*)notification{
_textInputView = notification.object;
[self freshReturnButtonEnabled];
}

-(void)textInputViewDidEndEditing:(NSNotification*)notification{
NSString *strValue = [self inputViewString];
if ([strValue startsWithString:@"."]) {
strValue = [NSString stringWithFormat:@"0%@", strValue];
[self setInputViewString:strValue];
}
_textInputView = nil;

}

-(NSString *)inputViewString{
NSString *strValue = @"";
if ([self.textInputView isKindOfClass:[UITextView class]]){
strValue = ((UITextView *)self.textInputView).text;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
strValue = ((UITextField *)self.textInputView).text;
}
return strValue;
}

-(void)setInputViewString:(NSString *)string{
if ([self.textInputView isKindOfClass:[UITextView class]]){
((UITextView *)self.textInputView).text = string;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
((UITextField *)self.textInputView).text = string;
}
}

-(NSInteger)inputViewSelectRangeLocation{
NSInteger location = 0;
if ([self.textInputView isKindOfClass:[UITextView class]]){
UITextView * textView = (UITextView *)self.textInputView;
location = textView.selectedRange.location;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
UITextField *textField = (UITextField *)self.textInputView;
UITextPosition* beginning = textField.beginningOfDocument;
UITextRange* selectedRange = textField.selectedTextRange;
UITextPosition* selectionStart = selectedRange.start;
location = [textField offsetFromPosition:beginning toPosition:selectionStart];
}
return location;
}

-(void)freshReturnButtonEnabled{
NSString *strValue = [self inputViewString];
if (strValue.length == 0) {
_returnButton.enabled = NO;
_returnButton.alpha = 0.6;
}else{
_returnButton.enabled = YES;
_returnButton.alpha = 1.f;
}
}

#pragma mark -- Delegate
#pragma mark - UICollectionViewDataSource

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.dataSource.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
KFZKeyBoardCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class]) forIndexPath:indexPath];
NSNumber * number = self.dataSource[indexPath.row];
if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
cell.textLabel.text = @"";
}else{
cell.textNumber = number;
}
return cell;
}

#pragma mark - UICollectionViewDelegate

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
NSNumber * number = self.dataSource[indexPath.row];
if ([number isEqualToNumber:@(102)]) {
[_textInputView resignFirstResponder];
return;
}
if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
return;
}
[self textInputWithNumber:number];
}

#pragma mark - init

-(void)setHiddenPoint:(BOOL)hiddenPoint{
_hiddenPoint = hiddenPoint;
[_collectionView reloadData];
}

@end

这个是里面cell的:

@interface KFZKeyBoardCell : UICollectionViewCell
/** 文字 */
@property (nonatomic, strong) UILabel * textLabel;
/** 图片 */
@property (nonatomic, strong) UIImageView * imageIcon;

/** 设置值 */
@property (nonatomic, strong) NSNumber * textNumber;
@end
#import "KFZKeyBoardCell.h"

@implementation KFZKeyBoardCell

- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor color_FAFAFA];

[self.contentView addSubview:self.textLabel];
[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.left.right.mas_equalTo(0.f);
}];

self.imageIcon.hidden = YES;
[self.contentView addSubview:self.imageIcon];
[self.imageIcon mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(CGPointZero);
make.size.mas_equalTo(CGSizeMake(24.f, 22.f));
}];
}
return self;
}
-(void)prepareForReuse{
self.textLabel.hidden = NO;
self.imageIcon.hidden = YES;
}
- (void)setTextNumber:(NSNumber *)textNumber{
_textNumber = textNumber;
if ([textNumber isEqualToNumber:@(101)]) {
self.textLabel.text = @"·";
}
else if ([textNumber isEqualToNumber:@(102)]){
self.textLabel.hidden = YES;
self.imageIcon.hidden = NO;
self.imageIcon.image = [UIImage imageNamed:@"keyboard_icon_smallkb"];
}
else{
self.textLabel.text = textNumber.stringValue;
}
}

- (UILabel *)textLabel{
if (!_textLabel) {
_textLabel = [[UILabel alloc]init];
_textLabel.font = [UIFont KFZSpecial_DINAlternateBoldWithFontSize:24.f];
_textLabel.textAlignment = NSTextAlignmentCenter;
_textLabel.userInteractionEnabled = NO;
_textLabel.backgroundColor = UIColor.clearColor;
}
return _textLabel;
}

-(UIImageView *)imageIcon{
if (!_imageIcon) {
_imageIcon = [[UIImageView alloc]init];
}
return _imageIcon;
}

@end

转自:https://www.jianshu.com/p/226f67166770

收起阅读 »

iOS 设备信息获取

1.获取电池电量(一般用百分数表示,大家自行处理就好)-(CGFloat)getBatteryQuantity{ return [[UIDevice currentDevice] batteryLevel];}2.获取电池状态(UIDeviceBatte...
继续阅读 »

1.获取电池电量(一般用百分数表示,大家自行处理就好)

-(CGFloat)getBatteryQuantity
{
return [[UIDevice currentDevice] batteryLevel];
}

2.获取电池状态(UIDeviceBatteryState为枚举类型)

-(UIDeviceBatteryState)getBatteryStauts
{
return [UIDevice currentDevice].batteryState;
}

3.获取总内存大小

-(long long)getTotalMemorySize
{
return [NSProcessInfo processInfo].physicalMemory;
}

4.获取当前可用内存

-(long long)getAvailableMemorySize
{
vm_statistics_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
if (kernReturn != KERN_SUCCESS)
{
return NSNotFound;
}
return ((vm_page_size * vmStats.free_count + vm_page_size * vmStats.inactive_count));
}

5.获取已使用内存

- (double)getUsedMemory
{
task_basic_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(),
TASK_BASIC_INFO,
(task_info_t)&taskInfo,
&infoCount);

if (kernReturn != KERN_SUCCESS
) {
return NSNotFound;
}

return taskInfo.resident_size;
}

6.获取总磁盘容量

include 
-(long long)getTotalDiskSize
{
struct statfs buf;
unsigned long long freeSpace = -1;
if (statfs("/var", &buf) >= 0)
{
freeSpace = (unsigned long long)(buf.f_bsize * buf.f_blocks);
}
return freeSpace;
}

7.获取可用磁盘容量

-(long long)getAvailableDiskSize
{
struct statfs buf;
unsigned long long freeSpace = -1;
if (statfs("/var", &buf) >= 0)
{
freeSpace = (unsigned long long)(buf.f_bsize * buf.f_bavail);
}
return freeSpace;
}

8.容量转换

-(NSString *)fileSizeToString:(unsigned long long)fileSize
{
NSInteger KB = 1024;
NSInteger MB = KB*KB;
NSInteger GB = MB*KB;

if (fileSize < 10) {
return @"0 B";
}else if (fileSize < KB) {
return @"< 1 KB";
}else if (fileSize < MB) {
return [NSString stringWithFormat:@"%.1f KB",((CGFloat)fileSize)/KB];
}else if (fileSize < GB) {
return [NSString stringWithFormat:@"%.1f MB",((CGFloat)fileSize)/MB];
}else {
return [NSString stringWithFormat:@"%.1f GB",((CGFloat)fileSize)/GB];
}
}

9.型号

#import 

+ (NSString *)getCurrentDeviceModel:(UIViewController *)controller
{
int mib[2];
size_t len;
char *machine;

mib[0] = CTL_HW;
mib[1] = HW_MACHINE;
sysctl(mib, 2, NULL, &len, NULL, 0);
machine = malloc(len);
sysctl(mib, 2, machine, &len, NULL, 0);

NSString *platform = [NSString stringWithCString:machine encoding:NSASCIIStringEncoding];
free(machine);

if ([platform isEqualToString:@"iPhone3,1"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,2"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,3"]) return @"iPhone 4 (A1349)";
if ([platform isEqualToString:@"iPhone4,1"]) return @"iPhone 4s (A1387/A1431)";
if ([platform isEqualToString:@"iPhone5,1"]) return @"iPhone 5 (A1428)";
if ([platform isEqualToString:@"iPhone5,2"]) return @"iPhone 5 (A1429/A1442)";
if ([platform isEqualToString:@"iPhone5,3"]) return @"iPhone 5c (A1456/A1532)";
if ([platform isEqualToString:@"iPhone5,4"]) return @"iPhone 5c (A1507/A1516/A1526/A1529)";
if ([platform isEqualToString:@"iPhone6,1"]) return @"iPhone 5s (A1453/A1533)";
if ([platform isEqualToString:@"iPhone6,2"]) return @"iPhone 5s (A1457/A1518/A1528/A1530)";
if ([platform isEqualToString:@"iPhone7,1"]) return @"iPhone 6 Plus (A1522/A1524)";
if ([platform isEqualToString:@"iPhone7,2"]) return @"iPhone 6 (A1549/A1586)";
if ([platform isEqualToString:@"iPhone8,1"]) return @"iPhone 6s";
if ([platform isEqualToString:@"iPhone8,2"]) return @"iPhone 6s Plus";
if ([platform isEqualToString:@"iPhone8,4"]) return @"iPhone SE";
if ([platform isEqualToString:@"iPhone9,1"]) return @"国行、日版、港行iPhone 7";
if ([platform isEqualToString:@"iPhone9,2"]) return @"港行、国行iPhone 7 Plus";
if ([platform isEqualToString:@"iPhone9,3"]) return @"美版、台版iPhone 7";
if ([platform isEqualToString:@"iPhone9,4"]) return @"美版、台版iPhone 7 Plus";
if ([platform isEqualToString:@"iPhone10,1"]) return @"国行(A1863)、日行(A1906)iPhone 8";
if ([platform isEqualToString:@"iPhone10,4"]) return @"美版(Global/A1905)iPhone 8";
if ([platform isEqualToString:@"iPhone10,2"]) return @"国行(A1864)、日行(A1898)iPhone 8 Plus";
if ([platform isEqualToString:@"iPhone10,5"]) return @"美版(Global/A1897)iPhone 8 Plus";
if ([platform isEqualToString:@"iPhone10,3"]) return @"国行(A1865)、日行(A1902)iPhone X";
if ([platform isEqualToString:@"iPhone10,6"]) return @"美版(Global/A1901)iPhone X";

if ([platform isEqualToString:@"iPod1,1"]) return @"iPod Touch 1G (A1213)";
if ([platform isEqualToString:@"iPod2,1"]) return @"iPod Touch 2G (A1288)";
if ([platform isEqualToString:@"iPod3,1"]) return @"iPod Touch 3G (A1318)";
if ([platform isEqualToString:@"iPod4,1"]) return @"iPod Touch 4G (A1367)";
if ([platform isEqualToString:@"iPod5,1"]) return @"iPod Touch 5G (A1421/A1509)";

if ([platform isEqualToString:@"iPad1,1"]) return @"iPad 1G (A1219/A1337)";
if ([platform isEqualToString:@"iPad2,1"]) return @"iPad 2 (A1395)";
if ([platform isEqualToString:@"iPad2,2"]) return @"iPad 2 (A1396)";
if ([platform isEqualToString:@"iPad2,3"]) return @"iPad 2 (A1397)";
if ([platform isEqualToString:@"iPad2,4"]) return @"iPad 2 (A1395+New Chip)";
if ([platform isEqualToString:@"iPad2,5"]) return @"iPad Mini 1G (A1432)";
if ([platform isEqualToString:@"iPad2,6"]) return @"iPad Mini 1G (A1454)";
if ([platform isEqualToString:@"iPad2,7"]) return @"iPad Mini 1G (A1455)";

if ([platform isEqualToString:@"iPad3,1"]) return @"iPad 3 (A1416)";
if ([platform isEqualToString:@"iPad3,2"]) return @"iPad 3 (A1403)";
if ([platform isEqualToString:@"iPad3,3"]) return @"iPad 3 (A1430)";
if ([platform isEqualToString:@"iPad3,4"]) return @"iPad 4 (A1458)";
if ([platform isEqualToString:@"iPad3,5"]) return @"iPad 4 (A1459)";
if ([platform isEqualToString:@"iPad3,6"]) return @"iPad 4 (A1460)";

if ([platform isEqualToString:@"iPad4,1"]) return @"iPad Air (A1474)";
if ([platform isEqualToString:@"iPad4,2"]) return @"iPad Air (A1475)";
if ([platform isEqualToString:@"iPad4,3"]) return @"iPad Air (A1476)";
if ([platform isEqualToString:@"iPad4,4"]) return @"iPad Mini 2G (A1489)";
if ([platform isEqualToString:@"iPad4,5"]) return @"iPad Mini 2G (A1490)";
if ([platform isEqualToString:@"iPad4,6"]) return @"iPad Mini 2G (A1491)";
if ([platform isEqualToString:@"iPad4,7"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad4,8"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad4,9"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad5,1"]) return @"iPad Mini 4 (WiFi)";
if ([platform isEqualToString:@"iPad5,2"]) return @"iPad Mini 4 (LTE)";
if ([platform isEqualToString:@"iPad5,3"]) return @"iPad Air 2";
if ([platform isEqualToString:@"iPad5,4"]) return @"iPad Air 2";
if ([platform isEqualToString:@"iPad6,3"]) return @"iPad Pro 9.7";
if ([platform isEqualToString:@"iPad6,4"]) return @"iPad Pro 9.7";
if ([platform isEqualToString:@"iPad6,7"]) return @"iPad Pro 12.9";
if ([platform isEqualToString:@"iPad6,8"]) return @"iPad Pro 12.9";
if ([platform isEqualToString:@"iPad6,11"]) return @"iPad 5 (WiFi)";
if ([platform isEqualToString:@"iPad6,12"]) return @"iPad 5 (Cellular)";
if ([platform isEqualToString:@"iPad7,1"]) return @"iPad Pro 12.9 inch 2nd gen (WiFi)";
if ([platform isEqualToString:@"iPad7,2"]) return @"iPad Pro 12.9 inch 2nd gen (Cellular)";
if ([platform isEqualToString:@"iPad7,3"]) return @"iPad Pro 10.5 inch (WiFi)";
if ([platform isEqualToString:@"iPad7,4"]) return @"iPad Pro 10.5 inch (Cellular)";

if ([platform isEqualToString:@"AppleTV2,1"]) return @"Apple TV 2";
if ([platform isEqualToString:@"AppleTV3,1"]) return @"Apple TV 3";
if ([platform isEqualToString:@"AppleTV3,2"]) return @"Apple TV 3";
if ([platform isEqualToString:@"AppleTV5,3"]) return @"Apple TV 4";

if ([platform isEqualToString:@"i386"]) return @"iPhone Simulator";
if ([platform isEqualToString:@"x86_64"]) return @"iPhone Simulator";
return platform;
}

10.IP地址

#import 和#import 

- (NSString *)deviceIPAdress {
NSString *address = @"an error occurred when obtaining ip address";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;

success = getifaddrs(&interfaces);

if (success == 0) { // 0 表示获取成功

temp_addr = interfaces;
while (temp_addr != NULL) {
if( temp_addr->ifa_addr->sa_family == AF_INET) {
// Check if interface is en0 which is the wifi connection on the iPhone
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// Get NSString from C String
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}

temp_addr = temp_addr->ifa_next;
}
}

freeifaddrs(interfaces);
return address;
}

11.当前手机连接的WIFI名称(SSID)

需要#import 

- (NSString *)getWifiName
{
NSString *wifiName = nil;

CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
if (!wifiInterfaces) {
return nil;
}

NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;

for (NSString *interfaceName in interfaces) {
CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));

if (dictRef) {
NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;

wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];

CFRelease(dictRef);
}
}

CFRelease(wifiInterfaces);
return wifiName;
}

12.当前手机系統版本

[[[UIDevice currentDevice] systemVersion] floatValue] ;


摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/b25cdf09ece2

收起阅读 »

WKWebView的特性及原理

WKWebView是在Apple的WWDC 2014随iOS 8和OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。使用UIWebView加载网页的时候,我们会发现内存会无限增长,还有内存泄漏的问题存在。WebKit中更新的...
继续阅读 »

WKWebView是在Apple的WWDC 2014随iOS 8和OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。

使用UIWebView加载网页的时候,我们会发现内存会无限增长,还有内存泄漏的问题存在。

WebKit中更新的WKWebView控件的新特性与使用方法,它很好的解决了UIWebView存在的内存、加载速度等诸多问题。

一、WKWebView新特性

在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存);

允许JavaScript的Nitro库加载并使用(UIWebView中限制);

支持了更多的HTML5特性;

高达60fps的滚动刷新率以及内置手势;

将UIWebViewDelegate与UIWebView重构成了14类与3个协议查看苹果官方文档

二、WebKit框架概览


如上图所示,WebKit框架中最核心的类应该属于WKWebView了,这个类专门用来渲染网页视图,其他类和协议都将基于它和服务于它。

WKWebView:网页的渲染与展示,通过WKWebViewConfiguration可以进行自定义配置

WKWebViewConfiguration:这个类专门用来配置WKWebView。

WKPreference:这个类用来进行相关webView设置。

WKProcessPool:这个类用来配置进程池,与网页视图的资源共享有关。

WKUserContentController:这个类主要用来做native与JavaScript的交互管理。

WKUserScript:用于进行JavaScript注入。

WKScriptMessageHandler:这个类专门用来处理JavaScript调用native的方法。

WKNavigationDelegate:网页跳转间的导航管理协议,这个协议可以监听网页的活动

WKNavigationAction:网页某个活动的示例化对象。

WKUIDelegate:用于交互处理JavaScript中的一些弹出框。

WKBackForwardList:堆栈管理的网页列表。

WKBackForwardListItem:每个网页节点对象。

三、WKWebView的属性

/// webView的自定义配置
@property (nonatomic,readonly, copy) WKWebViewConfiguration *configuration;
/// 导航代理
@property (nullable, nonatomic, weak)id navigationDelegate;
/// UI代理
@property (nullable, nonatomic, weak)id UIDelegate;
/// 访问过网页历史列表
@property (nonatomic,readonly, strong) WKBackForwardList *backForwardList;

/// 自定义初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
/// url加载webView视图
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
/// 文件加载webView视图
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));
/// HTMLString字符串加载webView视图
- (nullable WKNavigation *)loadHTMLString:(NSString *)stringbaseURL:(nullable NSURL *)baseURL;
/// NSData数据加载webView视图
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0));
/// 返回上一个网页节点
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

/// 网页的标题
@property (nullable, nonatomic,readonly, copy) NSString *title;
/// 网页的URL地址
@property (nullable, nonatomic,readonly, copy) NSURL *URL;
/// 网页是否正在加载
@property (nonatomic,readonly, getter=isLoading) BOOL loading;
/// 加载的进度 范围为[0, 1]
@property (nonatomic,readonly)double estimatedProgress;
/// 网页链接是否安全
@property (nonatomic,readonly) BOOL hasOnlySecureContent;
/// 证书服务
@property (nonatomic,readonly, nullable) SecTrustRef serverTrust API_AVAILABLE(macosx(10.12), ios(10.0));
/// 是否可以返回
@property (nonatomic,readonly) BOOL canGoBack;
/// 是否可以前进
@property (nonatomic,readonly) BOOL canGoForward;

/// 返回到上一个网页
- (nullable WKNavigation *)goBack;
/// 前进到下一个网页
- (nullable WKNavigation *)goForward;
/// 重新加载
- (nullable WKNavigation *)reload;
/// 忽略缓存 重新加载
- (nullable WKNavigation *)reloadFromOrigin;
/// 停止加载
- (void)stopLoading;
/// 执行JavaScript
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^ _Nullable)(_Nullableid, NSError * _Nullable error))completionHandler;

/// 是否允许左右滑动,返回-前进操作 默认是NO
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
/// 自定义代理字符串
@property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));
/// 在iOS上默认为NO,标识不允许链接预览
@property (nonatomic) BOOL allowsLinkPreview API_AVAILABLE(macosx(10.11), ios(9.0));
/// 滚动视图
@property (nonatomic,readonly, strong) UIScrollView *scrollView;
/// 是否支持放大手势,默认为NO
@property (nonatomic) BOOL allowsMagnification;
/// 放大因子,默认为1
@property (nonatomic) CGFloat magnification;
/// 据设置的缩放因子来缩放页面,并居中显示结果在指定的点

- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;/// 证书列表@property (nonatomic,readonly, copy) NSArray *certificateChain API_DEPRECATED_WITH_REPLACEMENT("serverTrust", macosx(10.11,10.12), ios(9.0,10.0));

四、WKWebView的使用
简单使用,直接加载url地址

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://developer.apple.com/reference/webkit"]]];
[self.view addSubview:webView];

自定义配置
再WKWebView里面注册供JS调用的方法,是通过WKUserContentController类下面的方法:

- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;

// 创建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];

// 创建UserContentController(提供JavaScript向webView发送消息的方法)
WKUserContentController* userContent = [[WKUserContentController alloc] init];

// 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
[userContent addScriptMessageHandler:self name:@"NativeMethod"];

// 将UserConttentController设置到配置文件
config.userContentController = userContent;

// 高端的自定义配置创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
// 设置访问的
URLNSURL *url = [NSURL URLWithString:@"https://developer.apple.com/reference/webkit"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
[self.view addSubview:webView];

// 实现WKScriptMessageHandler协议方法

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

  // 判断是否是调用原生的
if([@"NativeMethod" isEqualToString:message.name]) {
// 判断message的内容,然后做相应的操作
if([@"close" isEqualToString:message.body]) {
}
}
}

注意:上面将当前ViewController设置为MessageHandler之后需要在当前ViewController销毁前将其移除,否则会造成内存泄漏。

[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];

五、WKNavigationDelegate代理方法
如果实现了代理方法,一定要在decidePolicyForNavigationAction和decidePolicyForNavigationResponse方法中的回调设置允许跳转。

typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {

WKNavigationActionPolicyCancel, // 取消跳转

WKNavigationActionPolicyAllow, // 允许跳转

} API_AVAILABLE(macosx(10.10), ios(8.0));

1.在发送请求之前,决定是否跳转

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler {

NSLog(@"1-------在发送请求之前,决定是否跳转 -->%@",navigationAction.request);

decisionHandler(WKNavigationActionPolicyAllow);
}

2. 页面开始加载时调用

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@"2-------页面开始加载时调用");
}

3.在收到响应后,决定是否跳转

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisionHandler {
/// 在收到服务器的响应头,根据response相关信息,决定是否跳转。decisionHandler必须调用,来决定是否跳转,参数WKNavigationActionPolicyCancel取消跳转,WKNavigationActionPolicyAllow允许跳转    NSLog(@"3-------在收到响应后,决定是否跳转");

decisionHandler(WKNavigationResponsePolicyAllow);

4. 当内容开始返回时调用

- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {

NSLog(@"4-------当内容开始返回时调用");
}

5 页面加载完成之后调用

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

NSLog(@"5-------页面加载完成之后调用");
}

6 页面加载失败时调用

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@"6-------页面加载失败时调用");
}

7.接收到服务器跳转请求之后调用

- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@"-------接收到服务器跳转请求之后调用");
}

8.数据加载发生错误时调用

- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {

NSLog(@"----数据加载发生错误时调用");
}

9.需要响应身份验证时调用 同样在block中需要传入用户身份凭证

- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {

//用户身份信息 NSLog(@"----需要响应身份验证时调用 同样在block中需要传入用户身份凭证");

NSURLCredential *newCred = [NSURLCredential credentialWithUser:@"" password:@"" persistence:NSURLCredentialPersistenceNone];

// 为 challenge 的发送方提供 credential [[challenge sender] useCredential:newCred forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);

}

10.进程被终止时调用

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

NSLog(@"----------进程被终止时调用");
}

六、WKUIDelegate代理方法

/**
* web界面中有弹出警告框时调用
*
* @param webView 实现该代理的webview
* @param message 警告框中的内容
* @param completionHandler 警告框消失调用
*/

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(void(^)())completionHandler {

NSLog(@"-------web界面中有弹出警告框时调用");
}


* 创建新的webView时调用的方法

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

NSLog(@"-----创建新的webView时调用的方法");

return webView;

}

// 关闭webView时调用的方法

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

NSLog(@"----关闭webView时调用的方法");

}

// 下面这些方法是交互JavaScript的方法

// JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去

-(void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(BOOL))completionHandler {

NSLog(@"%@",message);

completionHandler(YES);

}

// JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入

-(void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSString * _Nullable))completionHandler{

NSLog(@"%@",prompt);

completionHandler(@"123");

}

// 默认预览元素调用

- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {

NSLog(@"-----默认预览元素调用");

return YES;

}

// 返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。

- (nullable UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray> *)previewActions {

NSLog(@"----返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。");

return self;

}

// 允许应用程序向它创建的视图控制器弹出

- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController {

NSLog(@"----允许应用程序向它创建的视图控制器弹出");

}

// 显示一个文件上传面板。completionhandler完成处理程序调用后打开面板已被撤销。通过选择的网址,如果用户选择确定,否则为零。如果不实现此方法,Web视图将表现为如果用户选择了取消按钮。

- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSArray * _Nullable URLs))completionHandler {

NSLog(@"----显示一个文件上传面板");

}


摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/1fd78ec144bb

收起阅读 »

taro-ui实现省市区三级联动

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

        <Picker

  mode="multiSelector" // 多列选择
onChange={this.onChange} // change事件
onColumnChange={this.onColumnChange} // 某列改变的事件
range={rangeData} //需要展示的数据
value={rangeKey} // 选择的下标
>
<View className="picker">
<Text className="label">所在地址:</Text>
{formData.province && (
<Text>
{formData.province}
{formData.city}
{formData.country}
</Text>
)} // 主要是数据回显加的代码,
{!formData.province && (
<Text className="placeholder">请选择省/市/区</Text>
)}
</View>
</Picker>


上述代码其实taro-ui官方文档都有具体的事例,这里就不多解释了。

相信每个的省市区结构都不一样,现在贴一部分自己项目的省市区结构

[{
provinceName: '北京市',
provinceCode: '11',
cities: [
{
cityName: '市辖区',
cityCode: '1101',
countries: [
{
countryCode: "110101"
countryName: "东城区"
}
]
}
]
}]

现在开始处理数据,因为rangeData是所有数据,省市区,我们需要把数据转换成[‘省’, ‘市’, ‘区’]。

handleCityData = key => {
// 处理数据。
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange, // 省市区所有的数据
rangeKey: key // key是多列选择器需要展示的下标,因为是初始化,所以我们传入[0,0,0]
});
};

数据处理代码有点丑,欢迎大家提意见。因babel没升级到7版本,所以if判断有点繁琐。

数据处理完了之后,我们需要开始处理每列的值改变,数据联动了,那么我们需要列联动事件。

onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) { // 根据改变不同的列,来显示不同的数据
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};

到这里的话,就基本实现了省市区三级联动。

下面说一哈,省市区数据回显的代码,不需要的朋友也可以了解一哈。
数据回显,其实很简单,只要找到对应的省市区的下标,就可以回显了。下面是具体实现代码:

getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
};

通过上面的循环找出对应省市区的下标,就可以实现省市区的数据回显。

噢,还忘了多列选择器的change事件,这个的话,根据自己项目需要返回的是code还是name,这块就自己处理了,我这边讲的主要是省市区的三级联动。
我是把省市区写成一个组件,然后在父节点传入对应的数据以及事件就可以在一个项目中多次用到了。

下面是该组件的所有代码

import Taro, { Component } from "@tarojs/taro";
import { View, Text, Image, ScrollView, Picker } from "@tarojs/components";
import { connect } from "@tarojs/redux";
import * as actions from "@actions/address";
// import { dispatchCartNum } from '@actions/cart';
import "./index.scss";

@connect(state => state.address, { ...actions })
class ChangeCity extends Component {
static defaultProps = {
detailAddress: {}
};
constructor(props) {
super(props);
this.state = {
addressData: [],
rangeKey: [0, 0, 0],
rangeData: [[], [], []],
formData: {
province: "",
city: "",
country: ""
}
};
}

componentDidMount() {
this.getAddress();
}
getAddress = () => {
this.props.dispatchAddressChina().then(res => {
let addressData = [...res.data];
this.setState(
{
addressData: addressData
},
() => {
let { detailAddress } = this.props;
if (!detailAddress.province) {
this.handleCityData([0, 0, 0]);
} else {
this.getRangeKey(detailAddress);
}
}
);
});
};
getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
this.setState({
rangeKey: rangeKey
});
};
handleCityData = key => {
// 处理数据
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange,
rangeKey: key
});
};
onChange = e => {
let { value } = e.detail;
this.getAddressName(value);
};
getAddressName = value => {
// 这里是转化用户选择的地址数据
let { addressData } = this.state;
let formData = {
province: "",
city: "",
country: ""
};
let payload = {
province: "",
city: "",
country: ""
};
if (addressData[value[0]]) {
formData.province = addressData[value[0]].provinceName; // 省名称
payload.province = addressData[value[0]].provinceCode; // 省code
if (
addressData[value[0]].cities &&
addressData[value[0]].cities[value[1]]
) {
formData.city = addressData[value[0]].cities[value[1]].cityName;
payload.city = addressData[value[0]].cities[value[1]].cityCode;
if (
addressData[value[0]].cities[value[1]].countries &&
addressData[value[0]].cities[value[1]].countries[value[2]]
) {
formData.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryName;
payload.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryCode;
}
}
}
// console.log(formData, "formData");
this.setState({
formData: formData
});
this.props.onChangeAddress(payload, formData);
};
onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) {
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};
render() {
const { formData, rangeData, rangeKey } = this.state;
return (


mode="multiSelector"
onChange={this.onChange}
onColumnChange={this.onColumnChange}
range={rangeData}
value={rangeKey}
>

所在地址:
{formData.province && (

{formData.province}
{formData.city}
{formData.country}

)}
{!formData.province && (
请选择省/市/区
)}




);
}
}
export default ChangeCity;

样式自己处理一下子就好了

本文链接:https://blog.csdn.net/weixin_42381896/article/details/106854708


EaseIMKit如何设置昵称、头像

参考截图:
1、聊天页面




2、会话列表




"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(4)

五、Groovy数据结构 通过这个模块的学习,我会结合具体的例子来说明如何查阅文档来确定闭包中的参数,在讲 Map 的时候我会讲到 Groovy 常用的数据结构有如下 四种: 1)、数组 2)、List 3)、Map 4)、Range 1、数组 在 Gro...
继续阅读 »

五、Groovy数据结构


通过这个模块的学习,我会结合具体的例子来说明如何查阅文档来确定闭包中的参数,在讲 Map 的时候我会讲到


Groovy 常用的数据结构有如下 四种:



  • 1)、数组

  • 2)、List

  • 3)、Map

  • 4)、Range


1、数组


在 Groovy 中使用 [ ] 表示的是一个 List 集合,如果要定义 Array 数组,我们就必须强制指定为一个数组的类型

//在 Java 中,我们一般会这样去定义一个数组
String[] javaArray = ["Java", "Groovy", "Android"]

//在 Groovy 中,我们一般会使用 as 关键字定义数组
def groovyArray = ["Java", "Groovy", "Android"] as String[]

2、List


1)、列表集合定义


1、List 即列表集合,对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类


2、定义一个列表集合的方式有点像 Java 中定义数组一样


3、集合元素可以接收任意的数据类型

//在 Groovy 中定义的集合默认就是对应于 Java 中 ArrayList 集合
def list1 = [1,2,3]
//打印 list 类型
print list1.class
//打印结果
class java.util.ArrayList

//集合元素可以接收任意的数据类型
def list2 = ['erdai666', 1, true]

那么问题来了,如果我想定义一个 LinkedList 集合,要怎么做呢?有两种方式:


1、通过 Java 的强类型方式去定义


2、通过 as 关键字来指定

//方式1:通过 Java 的强类型方式去定义
LinkedList list3 = [4, 5, 6]

//方式2:通过 as 关键字来指定
def list4 = [1, 2, 3] as LinkedList

2)、列表集合增删改查

def list = [1,2,3]
//-------------------------- 增加元素 ---------------------------------
//有以下几种方式
list.add(20)
list.leftShift(20)
list << 20

//-------------------------- 删除元素 ---------------------------------
//根据下标移除元素
list.remove(0)

//-------------------------- 修改元素 ---------------------------------
//根据下标修改元素
list[0] = 100

//-------------------------- 查询元素 ---------------------------------
//调用闭包的 find 方法,方法中接收一个闭包,闭包的参数就是 list 中的元素
list.find {
println it
}

列表集合 Api 挺多的,对于一些其他Api,使用到的时候自行查阅文档就好了,我会在下面讲 Map 的时候演示查阅 Api 文档确定闭包的参数


3、Map


1)、定义


1、Map 表示键-值表,其底层对应 Java 中的 LinkedHashMap


2、Map 变量由[:]定义,冒号左边是 key,右边是 Value。key 必须是字符串,value 可以是任何对象


3、Map 的 key 可以用 '' 或 "" 或 ''' '''包起来,也可以不用引号包起来

def map = [a: 1, 'b': true, "c" : "Groovy", '''d''' : '''ddd''']

2)、Map 常用操作


这里列举一些 Map 的常用操作,一些其他的 Api 使用到的时候自行查阅文档就好了

//---------------------------- Map 中元素访问操作 ----------------
/**
* 有如下三种方式:
* 1、map.key
* 2、map[key]
* 3、map.get(ket)
*/
println map.a
println map['b']
println map.get('c')
//打印结果
1
true
Groovy

//---------------------------- Map 中添加和修改元素 -------------------
//如果当前 key 在 map 中不存在,则添加该元素,如果存在则修改该元素
map.put('key','value')
map['key'] = "value"

3)、Map 遍历,演示查阅官方文档


现在我要去遍历 map 中的元素,但是我不知道它的 Api 是啥,那这个时候就要去查官方 Api 文档了:


http://docs.groovy-lang.org/latest/html/groovy-jdk/java/util/Map.html



通过官方文档我们可以发现: each 和 eachWithIndex 的闭包参数还是不确定的,如果我们使用 each 方法,如果传递给闭包是一个参数,那么它就把 entry 作为参数,如果我们传递给闭包是两个参数,那么它就把 key 和 value 作为参数,eachWithIndex 比 each 多了个 index 下标而已.


那么我们现在就使用以下这两个 Api :

//下面为了打印输出的格式清晰,做了一些额外的操作
def map = [a: 1, 'b': true, "c" : "Groovy", '''d''' : '''ddd''']

map.each {
print "$it.key $it.value \t"
}
println()

map.each {key,value ->
print "$key $value \t"
}
println()

map.eachWithIndex {entry,index ->
print "$entry.key $entry.value $index \t"
}
println()

map.eachWithIndex { key,value,index ->
print "$key $value $index \t"
}
//打印结果
a 1 b true c Groovy d ddd
a 1 b true c Groovy d ddd
a 1 0 b true 1 c Groovy 2 d ddd 3
a 1 0 b true 1 c Groovy 2 d ddd 3

4、Range


Range 表示范围,它其实是 List 的一种拓展。其由 begin 值 + 两个点 + end 值表示。如果不想包含最后一个元素,则 begin 值 + 两个点 + < + end 表示。我们可以通过 aRange.from 与 aRange.to 来获对应的边界元素,实际操作感受一下:

//定义一个两端都是闭区间的范围
def range = 1..10
range.each {
print it + " "
}
//打印值
1 2 3 4 5 6 7 8 9 10

//如果不想包含最后一个元素
def range1 = 1..<10
range1.each {
print it + " "
}
//打印结果
1 2 3 4 5 6 7 8 9

//打印头尾边界元素
println "$range1.from $range1.to"
//打印结果
1 9

六、Groovy 文件处理


1、IO


下面我们开始来操作这个文件,为了闭包的可读性,我会在闭包上加上类型和参数:

//-------------------------------1、文件定位 --------------------------------
def file = new File('testFile.txt')

//-----------------------2、使用 eachLine Api 每次读取一行, 闭包参数是每一行的字符串------------
file.eachLine { String line ->
println line
}
//打印结果
erdai666
erdai777
erdai888

//------------------------3、获取输入流,输出流读文件和写文件---------------------------------
//获取输入流读取文件的每一行
//1
file.withInputStream { InputStream inputStream ->
inputStream.eachLine { String it ->
println it
}
}

//2
file.withReader { BufferedReader it ->
it.readLines().each { String it ->
println it
}
}

//打印结果
erdai666
erdai777
erdai888

//获取输出流将字符串写入文件 下面这两种方式写入的文件内容会把之前的内容给覆盖
//1
file.withOutputStream { OutputStream outputStream ->
outputStream.write("erdai999".getBytes())
}

//2
file.withWriter { BufferedWriter it ->
it.write('erdai999')
}

//------------------------4、通过输入输出流实现文件拷贝功能---------------------------------
//1、通过 withOutputStream withInputStream 实现文件拷贝
def targetFile = new File('testFile1.txt')
targetFile.withOutputStream { OutputStream outputStream ->
file.withInputStream { InputStream inputStream ->
outputStream << inputStream
}
}

//2、通过 withReader、withWriter 实现文件拷贝
targetFile.withWriter {BufferedWriter bufferedWriter ->
file.withReader {BufferedReader bufferedReader ->
bufferedReader.eachLine {String line ->
bufferedWriter.write(line + "\r\n")
}
}
}

2、XML 文件操作


1)、解析 XML 文件

//定义一个带格式的 xml 字符串
def xml = '''
<response>
<value>
<books id="1" classification="android">
<book available="14" id="2">
<title>第一行代码</title>
<author id="2">郭霖</author>
</book>
<book available="13" id="3">
<title>Android开发艺术探索</title>
<author id="3">任玉刚</author>
</book>
</books>
</value>
</response>
'''
//创建 XmlSlurper 类对象,解析 XML 文件主要借助 XmlSlurper 这个类
def xmlSlurper = new XmlSlurper()
//解析 mxl 返回 response 根结点对象
def response = xmlSlurper.parseText(xml)
//打印一些结果
println response.value.books[0].book[0].title.text()
println response.value.books[0].book[0].author.text()
//打印结果
第一行代码
郭霖

//1、使用迭代器解析
response.value.books.each{ books ->
books.book.each{ book ->
println book.title
println book.author
}
}
//打印结果
第一行代码
郭霖
Android开发艺术探索
任玉刚

//2、深度遍历 XML 数据
def str1 = response.depthFirst().findAll { book ->
return book.author == '郭霖'
}
println str1
//打印结果
[第一行代码郭霖]

//3、广度遍历 XML 数据
def str2 = response.value.books.children().findAll{ node ->
node.name() == 'book' && node.@id == '2'
}.collect { node ->
"$node.title $node.author"
}
println str2
//打印结果
[第一行代码 郭霖]

2)、生成 XML 文件


上面我们使用 XmlSlurper 这个类解析了 XML,现在我们借助 MarkupBuilder 来生成 XML ,代码如下:

/**
* <response>
* <value>
* <books id="1" classification="android">
* <book available="14" id="2">
* <title>第一行代码</title>
* <author id="2">郭霖</author>
* </book>
* <book available="13" id="3">
* <title>Android开发艺术探索</title>
* <author id="3">任玉刚</author>
* </book>
* </books>
* </value>
* </response>
*/
//方式1:通过下面这种方式 就可以实现上面的效果,但是这种方式有个弊端,数据都是写死的
def sw = new StringWriter()
def xmlBuilder = new MarkupBuilder(sw)
xmlBuilder.response{
value{
books(id: '1',classification: 'android'){
book(available: '14',id: '2'){
title('第一行代码')
author(id: '2' ,'郭霖')
}
book(available: '13',id: '3'){
title('Android开发艺术探索')
author(id: '3' ,'任玉刚')
}
}
}
}
println sw

//方式2:将 XML 数据对应创建相应的数据模型,就像我们解析 Json 创建相应的数据模型是一样的
//创建 XML 对应数据模型
class Response {

def value = new Value()

class Value {

def books = new Books(id: '1', classification: 'android')

class Books {
def id
def classification
def book = [new Book(available: '14', id: '2', title: '第一行代码', authorId: 2, author: '郭霖'),
new Book(available: '13', id: '3', title: 'Android开发艺术探索', authorId: 3, author: '任玉刚')]

class Book {
def available
def id
def title
def authorId
def author
}
}
}
}

//创建 response 对象
def response = new Response()
//构建 XML
xmlBuilder.response{
value{
books(id: response.value.books.id,classification: response.value.books.classification){
response.value.books.book.each{
def book1 = it
book(available: it.available,id: it.id){
title(book1.title)
author(authorId: book1.authorId,book1.author)
}
}
}
}
}
println sw

3、Json 解析


Json解析主要是通过 JsonSlurper 这个类实现的,这样我们在写插件的时候就不需要额外引入第三方的 Json 解析库了,其示例代码如下所示:

//发送请求获取服务器响应的数据
def response = getNetWorkData("https://www.wanandroid.com/banner/json")
println response.data[0].desc
println response.data[0].imagePath

def getNetWorkData(String url){
def connect = new URL(url).openConnection()
connect.setRequestMethod("GET")
//这个会阻塞线程 在Android中不能这样操作 但是在桌面程序是可以的
connect.connect()
def response = connect.content.text

//json转实体对象
def jsonSlurper = new JsonSlurper()
jsonSlurper.parseText(response)
}
//打印结果
扔物线
https://wanandroid.com/blogimgs/8a0131ac-05b7-4b6c-a8d0-f438678834ba.png

7、总结


在本篇文章中,我们主要介绍了以下几个部分:


1、一些关于 Gradle ,Groovy 的问题


2、搭建 Groovy 开发环境,创建一个 Groovy 工程


3、讲解了 Groovy 的一些基础语法


4、对闭包进行了深入的讲解


5、讲解了 Groovy 中的数据结构和常用 Api 使用,并以 Map 举例,查阅官方文档去确定 Api 的使用和闭包的参数


6、讲解了 Groovy 文件相关的处理


学习了 Groovy ,对于我们后续自定义 Gradle 插件迈出了关键的一步。其次如果你学习过 Kotlin ,你会发现,它们的语法非常的类似,因此对于后续学习 Kotlin 我们也可以快速去上手。


作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(3)

四、Groovy 闭包 在 Groovy 中,闭包非常的重要,因此单独用一个模块来讲 1、闭包定义 引用 Groovy 官方对闭包的定义:A closure in Groovy is an open, anonymous, block of code that...
继续阅读 »

四、Groovy 闭包


在 Groovy 中,闭包非常的重要,因此单独用一个模块来讲


1、闭包定义


引用 Groovy 官方对闭包的定义:A closure in Groovy is an open, anonymous, block of code that can take arguments, return a value and be assigned to a variable. 翻译过来就是:Groovy 中的闭包是一个开放的、匿名的代码块,它可以接受参数、返回值并将值赋给变量。 通俗的讲,闭包可以作为方法的参数和返回值,也可以作为一个变量而存在,闭包本质上就是一段代码块,下面我们就由浅入深的来学习闭包


2、闭包声明


1、闭包基本的语法结构:外面一对大括号,接着是申明参数,参数类型可省略,在是一个 -> 箭头号,最后就是闭包体里面的内容


2、闭包也可以不定义参数,如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it

//1
{ params ->
//do something
}

//2
{
//do something
}

3、闭包调用


1、闭包可以通过 .call 方法来调用


2、闭包可以直接用括号+参数来调用

//定义一个闭包赋值给 closure 变量
def closure = { params1,params2 ->
params1 + params2
}

//闭包调用方式1: 闭包可以通过 .call 方法来调用
def result1 = closure('erdai ','666')
//闭包调用方式2: 闭包可以直接用括号+参数来调用
def result2 = closure.call('erdai ','777')
//打印值
println result1
println result2
//打印结果
erdai 666
erdai 777

//定义一个无参闭包
def closure1 = {
println('无定义参数闭包')
}
closure1() //或者调用 closure1.call()
//打印结果
无定义参数闭包

4、闭包进阶


1)、闭包中的关键变量


每个闭包中都含有 this、owner 和 delegate 这三个内置对象,那么这三个三个内置对象有啥区别呢?我们用代码去验证一下


注意


1、getThisObject() 方法 和 thisObject 属性等同于 this


2、getOwner() 方法 等同于 owner


3、getDelegate() 方法 等同于 delegate


这些去看闭包的源码你就会有深刻的体会


1、我们在 GroovyGrammar.groovy 这个脚本类中定义一个闭包打印这三者的值看一下:

//定义一个闭包
def outerClosure = {
println "this: " + this
println "owner: " + owner
println "delegate: " + delegate
}
//调用闭包
outerClosure.call()
//打印结果
this: variable.GroovyGrammar@39dcf4b0
owner: variable.GroovyGrammar@39dcf4b0
delegate: variable.GroovyGrammar@39dcf4b0
//证明当前三者都指向了GroovyGrammar这个脚本类对象

2、我们在这个 GroovyGrammar.groovy 这个脚本类中定义一个类,类中定义一个闭包,打印看下结果:

//定义一个 OuterClass 类
class OuterClass {
//定义一个闭包
def outerClosure = {
println "this: " + this
println "owner: " + owner
println "delegate: " + delegate
}
}

def outerClass = new OuterClass()
outerClass.outerClosure.call()

//打印结果如下:
this: variable.OuterClass@1992eaf4
owner: variable.OuterClass@1992eaf4
delegate: variable.OuterClass@1992eaf4
//结果证明这三者都指向了当前 OuterClass 类对象

3、我们在 GroovyGrammar.groovy 这个脚本类中,定义一个闭包,闭包中在定义一个闭包,打印看下结果:

def outerClosure = {
def innerClosure = {
println "this: " + this
println "owner: " + owner
println "delegate: " + delegate
}
innerClosure.call()

}
println outerClosure
outerClosure.call()

//打印结果如下
variable.GroovyGrammar$_run_closure4@64beebb7
this: variable.GroovyGrammar@5b58ed3c
owner: variable.GroovyGrammar$_run_closure4@64beebb7
delegate: variable.GroovyGrammar$_run_closure4@64beebb7
//结果证明 this 指向了当前GroovyGrammar这个脚本类对象 owner 和 delegate 都指向了 outerClosure 闭包对象

我们梳理一下上面的三种情况:


1、闭包定义在GroovyGrammar.groovy 这个脚本类中 this owner delegate 就指向这个脚本类对象


2、我在这个脚本类中创建了一个 OuterClass 类,并在他里面定义了一个闭包,那么此时 this owner delegate 就指向了 OuterClass 这个类对象


3、我在 GroovyGrammar.groovy 这个脚本类中定义了一个闭包,闭包中又定义了一个闭包,this 指向了当前GroovyGrammar这个脚本类对象, owner 和 delegate 都指向了 outerClosure 闭包对象


因此我们可以得到结论:


1、this 永远指向定义该闭包最近的类对象,就近原则,定义闭包时,哪个类离的最近就指向哪个,我这里的离得近是指定义闭包的这个类,包含内部类


2、owner 永远指向定义该闭包的类对象或者闭包对象,顾名思义,闭包只能定义在类中或者闭包中


3、delegate 和 owner 是一样的,我们在闭包的源码中可以看到,owner 会把自己的值赋给 delegate,但同时 delegate 也可以赋其他值


注意:在我们使用 this , owner , 和 delegate 的时候, this 和 owner 默认是只读的,我们外部修改不了它,这点在源码中也有体现,但是可以对 delegate 进行操作


2)、闭包委托策略


下面我们就来对修改闭包的 delegate 进行实操:

//创建一个香蕉类
class Banana{
def name
}

//创建一个橘子类
class Orange{
def name
}

//定义一个香蕉对象
def banana = new Orange(name: '香蕉')
//定义一个橘子对象
def orange = new Orange(name: '橘子')
//定义一个闭包对象
def closure = {
//打印值
println delegate.name
}
//调用闭包
closure.call()

//运行一下,发现结果报错了,如下
Caught: groovy.lang.MissingPropertyException: No such property: name for class: variable.GroovyGrammar
//大致意思就是GroovyGrammar这个脚本类对象没有这个 name 对象

我们来分析下报错的原因原因,分析之前我们要明白一个知识点:


闭包的默认委托策略是 OWNER_FIRST,也就是闭包会先从 owner 上寻找属性或方法,找不到则在 delegate 上寻找


1、closure 这个闭包是生明在 GroovyGrammar 这个脚本类当中


2、根据我们之前学的知识,在不改变 delegate 的情况下 delegate 和 owner 是一样的,都会指向 GroovyGrammar 这个脚本类对象


3、GroovyGrammar 这个脚本类对象,根据闭包默认委托策略,找不到 name 这个属性


因此报错了,知道了报错原因,那我们就修改一下闭包的 delegate , 还是上面那段代码,添加如下这句代码:

//修改闭包的delegate
closure.delegate = orange
//我们在运行一下,打印结果:
橘子

此时闭包的 delegate 指向了 orange ,因此会打印 orange 这个对象的 name ,那么我们把 closure 的 delegate 改为 banana,肯定就会打印香蕉了

//修改闭包的delegate
closure.delegate = banana
//我们在运行一下,打印结果:
香蕉

3)、深入闭包委托策略

//定义一个 ClosureDepth 类
class ClosureDepth{
//定义一个变量 str1 赋值为 erdai666
def str1 = 'erdai666'
//定义一个闭包
def outerClosure = {
//定义一个变量 str2 赋值为 erdai777
def str2 = 'erdai777'
//打印str1 分析1
println str1

//闭包中在定义一个闭包
def innerClosure = {
//分析2
println str1
println str2
}
//调用内部这个闭包
innerClosure.call()
}
}

//创建 ClosureDepth 对象
def closureDepth = new ClosureDepth()
//调用外部闭包
closureDepth.outerClosure.call()
//运行程序,打印结果如下
erdai666
erdai666
erdai777

上面代码注释写的很清楚,现在我们来重点分析下分析1和分析2处的打印值:


分析1:


分析1处打印了 str1 , 它处于 outerClosure 这个闭包中,此时 outerClosure 这个闭包的 owner , delegate 都指向了 ClosureDepth 这个类对象,因此 ClosureDepth 这个类对象的属性和方法我们就都能调用到,因此分析1处会打印 erdai666


分析2:


分析2处打印了 str1和 str2,它处于 innerClosure 这个闭包中,此时 innerClosure 这个闭包的 owner 和 delegate 会指向 outerClosure 这个闭包对象,我们会发现 outerClosure 有 str2 这个属性,但是并没有 str1 这个属性,因此 outerClosure 这个闭包会向它的 owner 去寻找,因此会找到 ClosureDepth 这个类对象的 str1 属性,因此打印的 str1 是ClosureDepth 这个类对象中的属性,打印的 str2 是outerClosure 这个闭包中的属性,所以分析2处的打印结果分别是 erdai666 erdai777


上面的例子中没有显式的给 delegate 设置一个接收者,但是无论哪层闭包都能成功访问到 str1、str2 值,这是因为默认的解析委托策略在发挥作用,Groovy 闭包的委托策略有如下几种:




  1. OWNER_FIRST:默认策略,首先从 owner 上寻找属性或方法,找不到则在 delegate 上寻找


  2. DELEGATE_FIRST:和上面相反,首先从 delegate 上寻找属性或者方法,找不到则在 owner 上寻找


  3. OWNER_ONLY:只在 owner 上寻找,delegate 被忽略


  4. DELEGATE_ONLY:和上面相反,只在 delegate 上寻找,owner 被忽略


  5. TO_SELF:高级选项,让开发者自定义策略,必须要自定义实现一个 Closure 类,一般我们这种玩家用不到


下面我们就来修改一下闭包的委托策略,加深理解:

class People1{
def name = '我是People1'

def action(){
println '吃饭'
}

def closure = {
println name
action()
}
}

class People2{
def name = '我是People2'

def action(){
println '睡觉'
}
}

def people1 = new People1()
def people2 = new People2()
people1.closure.delegate = people2
people1.closure.call()
//运行下程序,打印结果如下:
我是People1
吃饭

what? 这是啥情况,我不是修改了 delegate 为 people2 了,怎么打印结果还是 people1 的?那是因为我们忽略了一个点,没有修改闭包委托策略,他默认是 OWNER_FIRST ,因此我们修改一下就好了,还是上面这段代码,添加一句代码如下:

people1.closure.resolveStrategy = Closure.DELEGATE_FIRST
//运行下程序,打印结果如下:
我是People2
睡觉

到这里,相信你对闭包了解的差不多了,下面我们在看下闭包的源码就完美了


4)、闭包 Closure 类源码


仅贴出关键源码

public abstract class Closure<V> extends GroovyObjectSupport implements Cloneable, Runnable, GroovyCallable<V>, Serializable {
/**
* 熟悉的一堆闭包委托代理策略
*/
public static final int OWNER_FIRST = 0;
public static final int DELEGATE_FIRST = 1;
public static final int OWNER_ONLY = 2;
public static final int DELEGATE_ONLY = 3;
public static final int TO_SELF = 4;
/**
* 闭包对应的三个委托对象 thisObject 对应的就是 this 属性,都是用 private 修饰的,外界访问不到
*/
private Object delegate;
private Object owner;
private Object thisObject;
/**
* 闭包委托策略
*/
private int resolveStrategy;

/**
* 在闭包的构造方法中:
* 1、将 resolveStrategy 赋值为0,也是就默认委托策略OWNER_FIRST
* 2、thisObject ,owner ,delegate都会被赋值,delegate 赋的是 owner的值
*/
public Closure(Object owner, Object thisObject) {
this.resolveStrategy = 0;
this.owner = owner;
this.delegate = owner;
this.thisObject = thisObject;
CachedClosureClass cachedClass = (CachedClosureClass)ReflectionCache.getCachedClass(this.getClass());
this.parameterTypes = cachedClass.getParameterTypes();
this.maximumNumberOfParameters = cachedClass.getMaximumNumberOfParameters();
}

/**
* thisObject 只提供了 get 方法,且 thisObject 是用 private 修饰的,因此 thisObject 即 this 只读
*/
public Object getThisObject() {
return this.thisObject;
}

/**
* owner 只提供了 get 方法,且 owner 是用 private 修饰的,因此 owner 只读
*/
public Object getOwner() {
return this.owner;
}

/**
* delegate 提供了 get 和 set 方法,因此 delegate 可读写
*/
public Object getDelegate() {
return this.delegate;
}

public void setDelegate(Object delegate) {
this.delegate = delegate;
}

/**
* 熟悉的委托策略设置
*/
public void setResolveStrategy(int resolveStrategy) {
this.resolveStrategy = resolveStrategy;
}
public int getResolveStrategy() {
return resolveStrategy;
}
}

到这里闭包相关的知识点就都讲完了,但是,但是,但是,重要的事情说三遍:我们使用闭包的时候,如何去确定闭包的参数呢?,这个真的很蛋疼,作为 Android 开发者,在使用 AndroidStudio 进行 Gradle 脚本编写的时候,真的是非常不友好,上面我讲了可以使用一个小技巧去解决这个问题,但是这种情况是在你知道要使用一个 Api 的情况下,比如你知道 Map 的 each 方法可以遍历,但是你不知道参数,这个时候就可以去使用。那如果你连 Api 都不知道使用,那就更加不知道闭包的参数了,因此要解决这种情况,我们就必须去查阅 Groovy 官方文档:


http://www.groovy-lang.org/api.html


http://docs.groovy-lang.org/latest/html/groovy-jdk/index-all.html



作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(2)

三、Groovy 基础语法 再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的,既然如此,Groovy 的优势在哪里呢? 在于 Groovy 提供了更加灵活简单的语法,大量的语法糖以及闭包特性可以让你用更少的代码来实现和...
继续阅读 »

三、Groovy 基础语法


再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的,既然如此,Groovy 的优势在哪里呢?


在于 Groovy 提供了更加灵活简单的语法,大量的语法糖以及闭包特性可以让你用更少的代码来实现和Java同样的功能。比如解析xml文件,Groovy 就非常方便,只需要几行代码就能搞定,而如果用 Java 则需要几十行代码。


1、支持动态类型,使用 def 关键字来定义一个变量


在 Groovy 中可以使用 def 关键字定义一个变量,当然 Java 里面定义数据类型的方式,在 Groovy 中都能用

//Java 中,我们一般会这么定义
int age = 16
String name = "erdai"

//Groovy 中,我们可以这样定义,在变量赋值后,Groovy 编译器会推断出变量的实际类型
def age = 16
def name = 'erdai'

2、不用写 ; 号


现在比较新的语言都不用写,如 Kotlin

def age = 16
def name = 'erdai'

3、没有基本数据类型了,全是引用类型


上面说到,定义一个变量使用 def 关键字,但是 Groovy 是基于 Java 扩展的,因此我们也可以使用 Java 里面的类型,如 Java 中8大基本类型:byte , short , int , long , float , double ,char,boolean

//定义8大基本类型
byte mByte = 1
short mShort = 2
int mInt = 3
long mLong = 4
float mFloat = 5
double mDouble = 6
char mChar = 'a'
boolean mBoolean = true
//对类型进行打印
println(mByte.class)
println(mShort.class)
println(mInt.class)
println(mLong.class)
println(mFloat.class)
println(mDouble.class)
println(mChar.class)
println(mBoolean.class)

//打印结果如下:
class java.lang.Byte
class java.lang.Short
class java.lang.Integer
class java.lang.Long
class java.lang.Float
class java.lang.Double
class java.lang.Character
class java.lang.Boolean

因此我们可以得出结论:Groovy中没有基本数据类型,全是引用类型,即使定义了基础类型,也会被转换成对应的包装类


4、方法变化


1、使用 def 关键字定义一个方法,方法不需要指定返回值类型,参数类型,方法体内的最后一行会自动作为返回值,而不需要return关键字


2、方法调用可以不写 () ,最好还是加上 () 的好,不然可读性不好


3、定义方法时,如果参数没有返回值类型,我们可以省略 def,使用 void 即可


4、实际上不管有没有返回值,Groovy 中返回的都是 Object 类型


5、类的构造方法,避免添加 def 关键字

def sum(a,b){
a + b
}
def sum = sum(1,2) //还可以写成这样,但是可读性不好 def sum = sum 1,2
println(sum)

//打印结果
3

//如果方法没有返回值,我们可以这样写:
void doSomething(param1, param2) {

}

//类的构造方法,避免添加 def 关键字
class MyClass {
MyClass() {

}
}

5、字符串变化


在 Groovy 中有三种常用的字符串定义方式,如下所示:


这里先解释一下可扩展字符串的含义,可扩展字符串就是字符串里面可以引用变量,表达式等等


1 、单引号 '' 定义的字符串为不可扩展字符串


2 、双引号 "" 定义的字符串为可扩展字符串,可扩展字符串里面可以使用 ${} 引用变量值,当 {} 里面只有一个变量,非表达式时,{}也可以去掉


3 、三引号 ''' ''' 定义的字符串为输出带格式的不可扩展字符串

def age = 16
def name = 'erdai'
//定义一个不可扩展字符串,和我门在Java中使用差不多
def str1 = 'hello ' + name
//定义可扩展字符串,字符串里面可以引用变量值,当 {} 里面只有一个变量时,{}也可以去掉
def str2 = "hello $name ${name + age}"
//定义带输出格式的不可扩展字符串 使用 \ 字符来分行
def str3 = '''
\
hello
name
'''
//打印类型和值 下面代码我省略了 println 方法的(),上面有讲到这种语法也是允许的
println 'str1类型: ' + str1.class
println 'str1输出值: ' + str1
println 'str2类型: ' + str2.class
println 'str2输出值: ' + str2
println 'str3类型: ' + str3.class
println 'str3输出值: ' + str3

//打印结果
str1类型: class java.lang.String
str1输出值: hello erdai
str2类型: class org.codehaus.groovy.runtime.GStringImpl
str2输出值: hello erdai erdai16
str3类型: class java.lang.String
str3输出值:
hello
name

从上面代码我们可以看到,str2 是 GStringImpl 类型的,而 str1 和 str3 是 String 类型的,那么这里我就会有个疑问,这两种类型在相互赋值的情况下是否需要强转呢?我们做个实验在测试下:

//定义一个 String 类型的变量接收 GStringImpl 类型的变量,并没有强转
String str4 = str2
println 'str4类型: ' + str4.class
println 'str4输出值: ' + str4

//打印类型和值
str4类型: class java.lang.String
str4输出值: hello erdai erdai16

因此我们可以得出结论:编码的过程中,不需要特别关注 String 和 GString 的区别,编译器会帮助我们自动转换类型


6. 不用写 get 和 set 方法


1、在我们创建属性的时候,Groovy会帮我们自动创建 get 和 set 方法


2、当我们只定义了一个属性的 get 方法,而没有定义这个属性,默认这个属性只读


3、我们在使⽤对象 object.field 来获取值或者使用 object.field = value 来赋值的时候,实际上会自动转而调⽤ object.getField() 和 object.setField(value) 方法,如果我们不想调用这个特殊的 get 方法时则可以使用 .@ 直接域访问操作符访问属性本身


我们来模拟1,2,3这三种情况

//情况1:在我们创建属性的时候,Groovy会帮我们自动创建 get 和 set 方法
class People{
def name
def age
}

def people = new People()
people.name = 'erdai'
people.age = 19
println "姓名: $people.name 年龄: $people.age"
//打印结果
姓名: erdai 年龄: 19

//情况2 当我们定义了一个属性的 get 方法,而没有定义这个属性,默认这个属性只读
//我们修改一下People类
class People{
def name
def getAge(){
12
}
}

def people = new People()
people.name = 'erdai'
people.age = 19
println "姓名: $people.name 年龄: $people.age"
//运行一下代码 打印结果报错了,如下:
Caught: groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: age for class: variable.People
//大概错误意思就是我们不能修改一个只读的属性

//情况3: 如果我们不想调用这个特殊的 get 方法时则可以使用 .@ 直接域访问操作符访问属性本身
class People{
def name
def age

def getName(){
"My name is $name"
}
}
//这里使用了命名的参数初始化和默认的构造器创建people对象,后面会讲到
def people = new People(name: 'erdai666')
people.age = 19
def myName = people.@name

//打印值
println myName
println "姓名: $people.name 年龄: $people.age"

//打印结果
erdai666
姓名: My name is erdai666 年龄: 19
//看到区别了吗?使用 people.name 则会去调用这个属性的get方法,而 people.@name 则会访问这个属性本身

7、Class 是一等公民,所有的 Class 类型可以省略 .Class

//定义一个Test类
class Test{

}

//定义一个测试class的方法,从前面的语法我们知道,方法的参数类型是可以省略的
def testClass(myClass){

}

//测试
testClass(Test.class)
testClass(Test)

8、== 和 equals


在 Groovy 中,== 就相当于 Java 的 equals,如果需要比较两个对象是否是同一个,需要使用 .is()

class People{
def name
def age
}

def people1 = new People(name: 'erdai666')
def people2 = new People(name: 'erdai666')

println("people1.name == people2.name is: " + (people1.name == people2.name))
println("people1 is people2 is: " + people1.is(people2))

//打印结果
people1.name == people2.name is: true
people1 is people2 is: false

9、使用 assert 来设置断言,当断言的条件为 false 时,程序将会抛出异常

assert  2 ** 4 == 15
//运行程序,报错了,结果如下:
Caught: Assertion failed:
assert 2 ** 4 == 15
| |
16 false

10、支持 ** 次方运算符

assert  2 ** 4 == 16

11、简洁的三元表达式

//在java中,我们会这么写
String str = obj != null ? obj : ""

//在Groovy中,我们可以这样写,?: 操作符表示如果左边结果不为空则取左边的值,否则取右边的值
String str = obj ?: ""

12、简洁的非空判断

//在java中,我们可能会这么写
if(obj != null){
if(obj.group != null){
if(obj.group.artifact != null){
//do something
}
}
}

//在Groovy中,我们可以这样写 ?. 操作符表示如果当前调用对象为空就不执行了
obj?.group?.artifact


13、强大的 Switch


在 Groovy 中,switch 方法变得更加灵活,强大,可以同时支持更多的参数类型,比在 Java 中增强了很多

def result = 'erdai666'
switch (result){
case [1,2,'erdai666']:
println "匹配到了result"
break
default:
println 'default'
break
}
//打印结果
匹配到了result

14、判断是否为 null 和 非运算符


在 Groovy 中,所有类型都能转成布尔值,比如 null 就相当于0或者相当于false,其他则相当于true

//在 Java 中,我们会这么用
if (name != null && name.length > 0) {

}

//在 Groovy 中,可以这么用,如果name为 null 或 0 则返回 false,否则返回true
if(name){

}

//非运算符 erdai 这个字符串为非 null ,因此为true,而 !erdai 则为false
assert (!'erdai') = false

15、可以使用 Number 类去替代 float、double 等类型,省去考虑精度的麻烦


16、默认是 public 权限


默认情况下,Groovy 的 class 和 方法都是 public 权限,所以我们可以省略 public 关键字,除非我们想使用 private 修饰符

class Server { 
String toString() { "a server" }
}

17、使用命名的参数初始化和默认的构造器


Groovy中,我们在创建一个对象实例的时候,可以直接在构造方法中通过 key value 的形式给属性赋值,而不需要去写构造方法,说的有点抽象,上代码感受一下:

//定义一个people
class People{
def name
def age
}

//我们可以通过以下几种方式去实例化一个对象,注意我们People类里面没有写任何一个构造方法哦
def people1 = new People()
def people1 = new People(age: 15)
def people2 = new People(name: 'erdai')
def people3 = new People(age: 15,name: 'erdai')

18、使用 with 函数操作同一个对象的多个属性和方法


with 函数接收一个闭包,闭包下面会讲,闭包的参数就是当前调用的对象

class People{
def name
def age

void running(){
println '跑步'
}
}
//定义一个 people 对象
def people = new People()
//调用 with 函数 闭包参数即为peopeo 如果闭包不写参数,默认会有一个 it 参数
people.with{
name = "erdai"
age = 19
println "$name $age"
running()
}
//打印结果
erdai 19
跑步

19、异常捕获


如果你实在不想关心 try 块里抛出何种异常,你可以简单的捕获所有异常,并且可以省略异常类型:

//在 java 中我们会这样写
try {
// ...
} catch (Exception e) {
// do something
}

//在 Groovy 中,我们可以这样写
try {
// ...
} catch (any) {
// do something
}


上面 Groovy 的写法其实就是省略了参数类型,实际上 any 的参数类型也是 Exception, 并不包括 Throwable ,如果你想捕获所有的异常,你可以明确捕获异常的参数类型



作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

"Gradle"系列: 一、Gradle相关概念理解,Groovy基础(1)

前言 引用 Gradle 官方一段对Gradle的介绍:Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build sc...
继续阅读 »

前言


引用 Gradle 官方一段对Gradle的介绍:Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL.翻译过来就是:Gradle 是一个开源的自动化构建工具,专注于灵活性和性能。Gradle 构建脚本是使用 Groovy 或 Kotlin DSL 编写的。 之前官网的介绍是说 Gradle 是基于 Groovy 的 DSL,为啥现在又多了个 Kotlin 呢?因为 Gradle 从5.0开始,开始支持了 Kotlin DSL,现在已经发展到了6.8.3,因此我们可以使用 Groovy 或者 Kotlin 来编写 Gradle脚本。Kotlin 现作为 Android 第一开发语言,重要性不言而喻,作为一个 Android开发者,Kotlin 是必学的,后续我也会出个 Kotlin 系列文章。今天我们的重点是介绍一些 Gradle 的相关概念,以及对 Groovy 语言的学习


一、问题


我学习知识喜欢以问题为导向,这样可以让我明确学习的目的,提高学习效率,下面也是我在学习 Gradle 的过程中,由浅入深所产生的一些疑问,我们都知道,Android 应用是用 Gradle 构建的,在刚开发 Android 的时候我会想:


1、什么是自动化构建工具?


2、Gradle 是什么?


3、什么是 DSL?


4、什么是 Groovy?


5、Gradle 和 Groovy 有什么区别?


6、静态编程语言和动态编程语言有什么区别?


带着这些疑问,我们继续学习


1、自动化构建工具


在 Android 上的体现,简单的说就是自动化的编译、打包程序


在上大学学习Java那会,老师为了让我们深刻的体验撸码的魅力,都是通过文本直接敲代码的,敲完之后把扩展名改成.java后缀,然后通过javac命令编译,编译通过后,在执行java命令去运行,那么这种文件一多,我们每次都得手动去操作,效率会大大的降低,这个时候就出现了自动化编译工具,我们只需要在编译工具中,点击编译按钮,编译完成后,无需其他手动操作,程序就可以直接运行了,自动化编译工具就是最早的自动化构建工具。那么随着业务功能的不断扩展,我们的产品需要加入多媒体资源,需要打不同的渠道包发布到不同的渠道,那就必须依靠自动化构建工具,要能支持平台、需求等方面的差异、能添加自定义任务、专门的用来打包生成最终产品的一个程序、工具,这个就是自动化构建工具。自动化构建工具本质上还是一段代码程序。这就是自动化构建工具的一个发展历程,自动化构建工具在这个过程中不断的发展和优化


2、Gradle 是什么?


理解了自动化构建工具,那么理解 Gradle 就比较简单了,还是引用官方的那一段话:


Gradle 是一个开源的自动化构建工具,专注于灵活性和性能。Gradle 构建脚本是使用 Groovy 或 Kotlin DSL 编写的。


Gradle 是 Android 的默认构建工具,Android 项目这么多东西,既有我们自己写的 java、kotlin、C++、Dart 代码,也有系统自己的 java、C,C++ 代码,还有引入的第三方代码,还有多媒体资源,这么多代码、资源打包成 APK 文件肯定要有一个规范,干这个活的就是我们熟悉的 gradle 了,总而言之,Gradle就是一个帮我们打包 APK 的工具


3、什么是DSL?


DSL英文全称:domain specific language,中文翻译即领域特定语言,例如:HTML,XML等 DSL 语言


特点



  • 解决特定领域的专有问题

  • 它与系统编程语言走的是两个极端,系统编程语言是希望解决所有的问题,比如 Java 语言希望能做 Android 开发,又希望能做后台开发,它具有横向扩展的特性。而 DSL 具有纵向深入解决特定领域专有问题的特性。


总的来说,DSL 的核心思想就是:“求专不求全,解决特定领域的问题”。


4、什么是 Groovy?


Groovy 是基于 JVM 的脚本语言,它是基于Java扩展的动态语言


基于 JVM 的语言有很多种,如:Groovy,Kotlin,Java,Scala等等,他们都拥有一个共同的特性:最终都会编译生成 Java 字节码文件并在 JVM 上运行。


因为 Groovy 就是对 Java 的扩展,所以,我们可以用学习 Java 的方式去学习 Groovy 。 学习成本相对来说还是比较低的,即使开发过程中忘记 Groovy 语法,也可以用 Java 语法继续编码


5、Gradle 和 Groovy 有什么区别?


Gradle是基于 Groovy 的一种自动化构建工具,是运行在JVM上的一个程序,Groovy是基于JVM的一种语言,Gradle 和 Groovy 的关系就像 Android 和 Java 的关系一样


6、静态编程语言和动态编程语言有什么区别?


静态编程语言是在编译期就要确定变量的数据类型,而动态编程语言则是在运行期确定变量的数据类型。就像静态代理和动态代理一样,一个强调的是编译期,一个强调的是运行期,常见的静态编程语言有Java,Kotlin等等,动态编程语言有Groovy,Python等语言。

二、Groovy 开发环境搭建与工程创建

1、到官网下载JDK安装,并配置好 JDK 环境



2、到官网下载好 Groovy SDK,并解压到合适的位置



3、配置 Groovy 环境变量



4、到官网下载 IntelliJ IDEA 开发工具并安装



5、创建 Groovy 工程即可


小技巧: 作为 Android 开发者,我们一般都是使用 AndroidStudio 进行开发的,但是 AndroidStudio 对于 Groovy 支持不是很友好,各种没有提示,涉及到闭包,你也不知道闭包的参数是啥?因此这个时候,你就可以使用 IntelliJ IDEA 先弄好,在复制过去,IntelliJ IDEA 对Groovy 的支持还是很友好的


作者:妖孽那里逃
链接:https://www.jianshu.com/p/124effa509bb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Fastlane 自动打包技术

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布AppGithub官网文档我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们...
继续阅读 »

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App

Github

官网

文档

我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:

1.一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。

2.避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。

3.通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect

4.证书的同步更新,管理,在新电脑能够迅速具备项目打包环境。

如果你也有上述需求,那我相信Fastlane是一个好的选择。

多说无益,开始上手

一、安装xcode命令行工具
xcode-select --install,如果没有安装,会弹出对话框,点击安装。

如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates表示已经安装

二、安装Fastlane
sudo gem install fastlane -NV或是brew cask install fastlane我这里使用gem安装的

安装完了执行fastlane --version,确认下是否安装完成和当前使用的版本号。

三、初始化Fastlane
cd到你的项目目录执行

fastlane init

这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。

如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。


接着会提示你输入开发者账号和密码。

[20:48:55]: Please enter your Apple ID developer credentials
[20:48:55]: Apple ID Username:

登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。

如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。

四、文件系统

初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。

其中metadata和screenshots分别对应App元数据和商店应用截图。

Appfile主要存放App的apple_id team_id app_identifier等信息

Deliverfile中为发布的配置信息,一般情况用不到。

Fastfile是我们最应该关注的文件,也是我们的工作文件。

Fastfile


之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。

打包到蒲公英
这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。

蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。

打开终端输入fastlane add_plugin pgyer

更多信息查看蒲公英文档

新建一个lane

desc "打包到pgy"
lane :test do |options|
gym(
clean:true, #打包前clean项目
export_method: "ad-hoc", #导出方式
scheme:"shangshaban", #scheme
configuration: "Debug",#环境
output_directory:"./app",#ipa的存放目录
output_name:get_build_number()#输出ipa的文件名为当前的build号
)
#蒲公英的配置 替换为自己的api_key和user_key
pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
end

这样一个打包到蒲公英的lane就完成了。

option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台

执行

在工作目录的终端执行

fastlane test desc:测试打包


然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置。

其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解

其他的一些小提示

1.可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来。

2.如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。

3.如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。

4.如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据 skip_screenshots:true,#不上传屏幕截图

关于fastlane的一些想法
其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!

摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/59725c52e0fa

收起阅读 »

iOS 常见面试题总结及答案(4)

一.OC对象的内存管理机制?在iOS中,使用引用计数来管理OC对象的内存一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1内存管...
继续阅读 »

一.OC对象的内存管理机制?

在iOS中,使用引用计数来管理OC对象的内存

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结

当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况

extern void _objc_autoreleasePoolPrint(void);

二.内存区域分布

在iOS开发过程中,为了合理的分配有限的内存空间,将内存区域分为五个区,由低地址向高地址分类分别是:代码区、常量区、全局静态区、堆、栈。

代码段 -- 程序编译产生的二进制的数据
常量区 -- 存储常量数据,通常程序结束后由系统自动释放
全局静态区 -- 全局区又可分为未初始化全局区:.bss段和初始化全局区:data段。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,在程序结束后有系统释放。
堆(heap) -- 程序运行过程中,动态分配的内存
栈(stack) -- 存放局部变量,临时变量

三.堆区和栈取的区别

按管理方式分

对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
按分配方式分

堆是动态分配和回收内存的,没有静态分配的堆
栈有两种分配方式:静态分配和动态分配
静态分配是系统编译器完成的,比如局部变量的分配
动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动 态分配也由系统编译器进行释放,不需要程序员手动管理

四.怎么保证多人开发进行内存泄露的检查

1.使用Analyze进行代码的静态分析
2.使用leaks 进行内存泄漏检测
3.使用一些三方工具(DoraemonKit/WithMLeaksFinder)

五.内存泄漏可能会出现的几种原因?

第一种可能:第三方框架不当使用;
第二种可能:block循环引用;
第三种可能:delegate循环引用;
第四种可能:NSTimer循环引用
第五种可能:非OC对象内存处理
第六种可能:地图类处理
第七种可能:大次数循环内存暴涨

六.什么是Tagged Pointer?

1.从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
2.使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
3.当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

七.copy和mutableCopy区别


八.AutoreleasePoolPage的结构?以及如何 push 和 pop 的

AutoreleasePool(自动释放池)其实并没有自身的结构,他是基于多个AutoreleasePoolPage(一个C++类)以双向链表组合起来的结构; 可以通过 push操作添加对象,pod 操作弹出对象,以及通过 release 操作释放对象;


调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

id *next指向了下一个能存放autorelease对象地址的区域

九.Autoreleasepool 与 Runloop 的关系

主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理
iOS在主线程的Runloop中注册了2个Observer

第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

十.什么是多线程?

多线程是指实现多个线程并发执行的技术,进而提升整体处理性能。

同一时间,CPU 只能处理一条线程,多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换)如果 CPU 调度线程的时间足够快, 就造成了多线程并发执行的假象

主线程的栈区 空间大小为1M,非常非常宝贵

子线程的栈区 空间大小为512K内存空间

优势
充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“并行计算”状态

弊端
新线程会消耗内存控件和cpu时间,线程太多会降低系统运行性能。

十一.iOS的多线程方案有哪几种?


十二,讲一下GCD

GCD(Grand Central Dispatch), 又叫做大中央调度, 它对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口, 使用更加高效,也是苹果推荐的使用方式.

GCD 的队列

1.并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效

2.串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务),按照FIFO顺序执行.

同步和异步任务

GCD多线程经常会使用 dispatch_sync和dispatch_async函数向指定队列添加任务,分别是同步和异步

同步指阻塞当前线程,既要等待添加的耗时任务块Block完成后,函数才能返回,后面的代码才能继续执行

异步指将任务添加到队列后,函数立即返回,后面的代码不用等待添加的任务完成后即可执行,异步提交无法确定任务执行顺序

相关常用函数使用:

1.dispatch_after使用 (通过该函数可以让提交的任务在指定时间后开始执行,也就是延迟执行;)

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"10秒后开始执行")
});

2.dispatch_group_t (组调度)的使用 (组调度可以实现等待一组操都作完成后执行后续任务.)

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求3
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});

3.dispatch_semaphore (信号量)如何使用?

用于控制最大并发数     可以防止资源抢夺

与他相关的共有三个函数,分别是:

dispatch_semaphore_create,  // 创建最大并发数
dispatch_semaphore_wait。 // -1 开始执行 (0则等待)
dispatch_semaphore_signal, // +1

4.dispatch_barrier_(a)sync使用?

一个dispatch barrier 允许在一个并发队列中创建一个同步点。当在并发队列中遇到一个barrier, 他会延迟执行barrier的block,等待所有在barrier之前提交的blocks执行结束。 这时,barrier block自己开始执行。 之后, 队列继续正常的执行操作。

十三.什么是NSOperation?

1.NSOperation是基于GCD的上封装,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强

例如:
可以加入操作依赖控制执行顺序,设置操作队列最大并发数,取消操作等

2.NSOperation如何实现操作依赖

通过任务间添加依赖,可以为任务设置执行的先后顺序。接下来通过一个案例来展示设置依赖的效果。

NSOperationQueue *queue=[[NSOperationQueue alloc] init];
//创建操作
NSBlockOperation *operation1=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第1次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation2=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第2次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation3=[NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行第3次操作,线程:%@",[NSThread currentThread]);
}];
//添加依赖
[operation1 addDependency:operation2];
[operation2 addDependency:operation3];
//将操作添加到队列中去
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];

十四.在项目什么时候选择使用 GCD,什么时候选 择 NSOperation

项目中使用 NSOperation 的优点是 NSOperation 是对线程的高度抽象,在项目中使 用它,会使项目的程序结构更好,子类化 NSOperation 的设计思路,是具有面向对 象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中 使用。

项目中使用 GCD 的优点是 GCD 本身非常简单、易用,对于不复杂的多线程操 作,会节省代码量,而 Block 参数的使用,会是代码更为易读,建议在简单项目中 使用。

区别,以及各自的优势

GCD是纯C语⾔言的API,NSOperationQueue是基于GCD的OC版本封装

GCD只⽀支持FIFO的队列列,NSOperationQueue可以很⽅方便便地调整执⾏行行顺 序、设 置最⼤大并发数量量

NSOperationQueue可以在轻松在Operation间设置依赖关系,⽽而GCD 需要写很 多的代码才能实现

NSOperationQueue⽀支持KVO,可以监测operation是否正在执⾏行行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld)

GCD的执⾏行行速度⽐比NSOperationQueue快 任务之间不不太互相依赖:GCD 任务之间 有依赖\或者要监听任务的执⾏行行情况:NSOperationQueue

十五.线程安全的处理手段有哪些,线程锁都有哪些?

1.加锁

2.同步执行

线程锁 (我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁 应运而生。)

1.OSSpinLock (自旋锁)

注:苹果爸爸已经在iOS10.0以后废弃了这种锁机制,使用os_unfair_lock 替换,顾名思义能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题.

2.os_unfair_lock(自旋锁)

3.dispatch_semaphore (信号量)

4.pthread_mutex(互斥锁)

5.NSLock(互斥锁、对象锁)

6.NSCondition(条件锁、对象锁)

7.NSConditionLock(条件锁、对象锁)

8.NSRecursiveLock(递归锁、对象锁)

9.@synchronized(条件锁)

10.pthread_mutex(recursive)(递归锁) 

注.递归锁可以被同一线程多次请求,而不会引起死锁。即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。这主要是用在循环或递归操作中

性能图


十六.HTTPS连接过程简述

1.客户端向服务端发起 https 请求

2.服务器(需要申请 ca 证书),返回证书(包含公钥)给客户端

3.客户端使用根证书验证 服务器证书的有效性,进行身份确认

4.客户端生成对称密钥,通过公钥进行密码,发送给服务器

5.服务器使用私钥进行 解密,获取对称密钥

6.双方使用对称加密的数据进行通信

十七.http 与https区别

HTTPS和HTTP的区别主要为以下四点:

1.https协议需要到ca申请证书,一般免费证书很少,需要交费。

2.http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。

3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4.http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全

十八.什么是DNS?DNS劫持问题?

域名系统(DomainNameSystem,缩写:DNS)是[互联网]的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问[互联网]

DNS劫持又称(域名劫持), 是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。

解决办法: 使用HTTPDNS

十九.网络七层是什么?

OSI模型有7层结构,每层都可以有几个子层。 OSI的7层从上到下分别是 7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层 ;其中高层(即7、6、5、4层)定义了应用程序的功能,下面3层(即3、2、1层)主要面向通过网络的端到端的数据流。

1.应用层
网络服务与最终用户的一个接口。
协议有:HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP

2.表示层
数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层)
格式有,JPEG、ASCll、DECOIC、加密格式等

3 .会话层
建立、管理、终止会话。(在五层模型里面已经合并到了应用层)
对应主机进程,指本地主机与远程主机正在进行的会话

4.传输层
定义传输数据的协议端口号,以及流控和差错校验。
协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层

5.网络层
进行逻辑地址寻址,实现不同网络之间的路径选择。
协议有:ICMP IGMP IP(IPV4 IPV6) ARP RARP

6.数据链路层
建立逻辑连接、进行硬件地址寻址、差错校验 [2] 等功能。(由底层网络定义协议)
将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。

7.物理层
建立、维护、断开物理连接。(由底层网络定义协议)

二十.项目中网络层如何做安全处理

1.尽量使用https

2.不要传输明文密码

3.Post并不比Get安全

4.不要使用301跳转

5.http请求都带上MAC

6.http请求使用临时密钥

7.AES使用CBC模式







收起阅读 »

ios加固,ios代码混淆,ios代码混淆工具, iOS源码混淆使用说明详解

ios加固,ios代码混淆,ios代码混淆工具,iOS源码混淆产品是一款纯离线的源码加密工具,主要用于保护iOS项目中的核心代码,避免因逆向工程或破解,造成核心技术被泄漏、代码执行流程被分析等安全问题。该加密工具和普通编译器相似,基于项目源代码可将Object...
继续阅读 »

ios加固,ios代码混淆,ios代码混淆工具,iOS源码混淆产品是一款纯离线的源码加密工具,主要用于保护iOS项目中的核心代码,避免因逆向工程或破解,造成核心技术被泄漏、代码执行流程被分析等安全问题。该加密工具和普通编译器相似,基于项目源代码可将Objective-C、Swift、C、C++代码编译成二进制代码,不同之处在于,加密工具在编译时,能够对代码采取混淆、字符串加密等安全措施。从而避免攻击者通过IDA Pro等逆向工具反编译二进制代码,分析业务代码执行流程,进一步篡改或窃取核心技术。

概述

本文主要介绍iOS源码混淆产品之Xcode插件的使用方式,阅读者需具备iOS开发经验,否则使用可能存在困难。

安装插件

v13.0.2-20190703及其之前的版本为替换clang编译器的模式,之后版本为切换Xcode -> Toolchains的模式,后者可以在Xcode中快速切换编译器。

Xcode插件通过执行python install.py 命令安装编译器,使用完成后执行 python uninstal.py 即可卸载编译器。如下图:

(备注:如果有多个Xcode版本,请修改configuration.txt文件中Xcode默认的路径。)


执行安装会提示输入密码,输入电脑开机密码即可,Xcode插件安装成功后会有Install Success提示,如下图:


引入头文件

将include目录下的KiwiOBF.h头文件拷贝到iOS项目中,并在需的地方进行引用即可。

添加KIWIOBF标签

对需要进行混淆保护的函数,添加KIWIOBF标签,以告知编译器该函数需要进行混淆编译。如下图:


设置参数

全编译器有默认混淆参数,如不能满足需求,可以自定义配置参数
加密参数说明


iOS项目的混淆参数在 Other C Flags,Other C++ Flags,Other Swift Flags中设置,如下图:


卸载插件

Xcode插件:执行 python uninstall.py 即可卸载编译器。

友情告知地址,ios代码混淆,ios加固:https://www.kiwisec.com/product/compiler-ios.html

转自:https://www.jianshu.com/p/7fdb4544c916

收起阅读 »

iOS 常见面试题总结及答案(3)

一.列举出延迟调用的几种方法?1.performSelector方法 [self performSelector:@selector(Delay) withObject:nil afterDelay:3.0f];2.NSTimer定时器  [NSTimer s...
继续阅读 »

一.列举出延迟调用的几种方法?

1.performSelector方法 

[self performSelector:@selector(Delay) withObject:nil afterDelay:3.0f];

2.NSTimer定时器  

[NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:@selector(Delay) userInfo:nil repeats:NO];

3.sleepForTimeInterval

[NSThread sleepForTimeInterval:3.0f];

4.GCD方式

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self Delay];
});
- (void)Delay {
NSLog(@"执行");
}

二.NSCache 和NSDictionary 区别?

NSCache可以提供自动删减缓存功能,而且保证线程安全,与字典不同,不会拷贝键。
NSCache可以设置缓存上限,限制对象个数和总缓存开销。定义了删除缓存对象的时机。这个机制只对NSCache起到指导作用,不会一定执行。
NSPurgeableData搭配NSCache使用,可以自动清除数据。
只有那种“重新计算很费劲”的数据才值得放入缓存。

三.NSArray 和 NSSet区别

NSSet和NSArray功能性质一样,用于存储对象,属于集合。
NSSet属于 “无序集合”,在内存中存储方式是不连续
NSArray是 “有序集合” 它内存中存储位置是连续的。
NSSet,NSArray都是类,只能添加对象,如果需要加入基本数据类型(int,float,BOOL,double等),需要将数据封装成NSNumber类型。
由于NSSet是用hash实现的所以就造就了它查询速度比较快,但是我们不能把某某对象存在第几个元素后面之类的有关下标的操作。

四.什么是分类?

分类: 在不修改原有类代码的情况下,可以给类添加方法
Categroy 给类扩展方法,或者关联属性, Categroy底层结构也是一个结构体:内部存储这结构体的名字,那个类的分类,以及对象和类方法列表,协议,属性信息
通过Runtime加载某个类的所有Category数据
把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面
将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

五.为什么说OC是一门动态语言?

动态语言:是指程序在运行时可以改变其结构,新的函数可以被引进,已有的函数可以被删除等在结构上的变化
动态类型语言: 就是类型的检查是在运行时做的。
OC的动态特性可从三方面:

动态类型(Dynamic typing):最终判定该类的实例类型是在运行期间
动态绑定(Dynamic binding):在运行时确定调用的方法
动态加载(Dynamic loading):在运行期间加载需要的资源或可执行代码

六.什么是动态绑定?

动态绑定 将调用方法的确定也推迟到运行时。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去,这就是动态绑定。
在编译时,方法的 调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,

七.什么是谓词?

谓词(NSPredicate)是OC针对数据集合的一种逻辑帅选条件,类似一个过滤器,简单实实用代码如下:

Person * p1 = [Person personWithName:@"alex" Age:20];
Person * p2 = [Person personWithName:@"alex1" Age:30];
Person * p3 = [Person personWithName:@"alex2" Age:10];
Person * p4 = [Person personWithName:@"alex3" Age:40];
Person * p5 = [Person personWithName:@"alex4" Age:80];

NSArray * persons = @[p1, p2, p3, p4, p5];
//定义谓词对象,谓词对象中包含了过滤条件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age < 30"];
//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];

八.什么是类工厂方法?

类工厂方法就是用来快速创建对象的类方法, 他可以直接返回一个初始化好的对象,具备以下特征:

一定是类方法
返回值需要是 id/instancetype 类型
规范的方法名说说明类工厂方法返回的是一个什么对象,一般以类名首字母小写开始;
比如系统 UIButton 的buttonWithType 就是一个类工厂方法:

// 类工厂方法
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
// 使用
+ UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];

九.简要说明const,宏,static,extern区分以及使用?

1.const

const常量修饰符,经常使用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量。

- const 作用:限制类型
- 使用const修饰基本变量, 两种写法效果一致 , b都是只读变量
const int b = 5;
int const b = 5;
- 使用const修饰指针变量的变量
第一种: const int *p = &a 和 int const *q = &a; 效果一致,*p 的值不能改,p 的指向可以改;
第二种: int * const p = &a; 表示 p 的指向不能改,*p 的值可以改
第三种:
const int * const p = &a; *p 值和 p 的指向都不能改

const 在*左边, 指向可变, 值不可变
const 在*的右边, 指向不可变, 值可变
const 在*的两边, 都不可变

2.

* 基本概念:宏是一种批量处理的称谓。一般说来,宏是一种规则或模式,或称语法替换 ,用于说明某一特定输入(通常是字符串)如何根据预定义的规则转换成对应的输出(通常也是字符串)。这种替换在预编译时进行,称作宏展开。编译器会在编译前扫描代码,如果遇到我们已经定义好的宏那么就会进行代码替换,宏只会在内存中copy一份,然后全局替换,宏一般分为对象宏和函数宏。 宏的弊端:如果代码中大量的使用宏会使预编译时间变长。

const与宏的区别?

* 编译检查 宏没有编译检查,const有编译检查;
* 宏的好处 定义函数,方法 const不可以;
* 宏的坏处 大量使用宏,会导致预编译时间过长

3.static

* 修饰局部变量: 被static修饰局部变量,延长生命周期,跟整个应用程序有关,程序结束才会销毁,被 static 修饰局部变量,只会分配一次内存
* 修饰全局变量: 被static修饰全局变量,作用域会修改,也就是只能在当前文件下使用

4.extern

声明外部全局变量(只能用于声明,不能用于定义)

常用用法(.h结合extern联合使用)
如果在.h文件中声明了extern全局变量,那么在同一个类中的.m文件对全局变量的赋值必须是:数据类型+变量名(与声明一致)=XXXX结构。并且在调用的时候,必须导入.h文件。代码如下:

.h
@interface ExternModel : NSObject
extern NSString *lhString;
@end
.m
@implementation ExternModel
NSString *lhString=@"hello";
@end

调用的时候:例如:在viewController.m中调用,则可以引入:ExternModel.h,否则无法识别全局变量。当然也可以通过不导入头文件的方式进行调用(通过extern调用)。

十.id类型, nil , Nil ,NULL和NSNULL的区别?

id类型: 是一个独特的数据类型,可以转换为任何数据类型,id类型的变量可以存放任何数据类型的对象,在内部处理上,这种类型被定义为指向对象的指针,实际上是一个指向这种对象的实例变量的指针; id 声明的对象具有运行时特性,既可以指向任意类型的对象
nil 是一个实例对象值;如果我们要把一个对象设置为空的时候,就用nil
Nil 是一个类对象的值,如果我们要把一个class的对象设置为空的时候,就用Nil
NULL 指向基本数据类型的空指针(C语言的变量的指针为空)
NSNull 是一个对象,它用在不能使用nil的场合

十一.C和 OC 如何混编&&Swift 和OC 如何调用?

1.xcode可以识别一下几种扩展名文件:

.m文件,可以编写 OC语言 和 C 语言代码
.cpp: 只能识别C++ 或者C语言(C++兼容C)
.mm: 主要用于混编 C++和OC代码,可以同时识别OC,C,C++代码

2.Swift 调用 OC代码

需要创建一个 Target-BriBridging-Header.h 的桥文件,在乔文件导入需要调用的OC代码头文件即可

3.OC 调用 Swift代码
直接导入 Target-Swift.h文件即可, Swift如果需要被OC调用,需要使用@objc 对方法或者属性进行修饰

十二.OC与 JS交互方式有哪些?

1.通过拦截URL

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = request.URL.absoluteString;
if ([url rangeOfString:@"需要跳转源生界面的URL判断"].location != NSNotFound) {
//跳转原生界面
return NO;
}
return YES;
}

2.使用MessageHandler(WKWebView)

当JS端想传一些数据给iOS.那它们会调用下方方法来发送.
window.webkit.messageHandlers.<方法名>.postMessage(<数据>)上方代码在JS端写会报错,导致网页后面业务不执行.可使用try-catch执行.
那么在OC中的处理方法如下.它是WKScriptMessageHandler的代理方法.name和上方JS中的方法名相对应.

- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;

3.JavaScriptCore (UIWebView)
使用三方库WebViewJavascriptBridge,可提供 js 调OC,以及OC掉JS

1. 设置 webViewBridge
_bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.webView];
[_bridge setWebViewDelegate:self];
2. 注册handler方法,需要和 前段协商好 方法名字,是供 JS调用Native 使用的。
[_bridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
// OC调用
NSString *scanResult = @"http://www.baidu.com";
// js 回调传参
responseCallback(scanResult);
}];
3. OC掉用JS
[_bridge callHandler:@"testJSFunction" data:@"一个字符串" responseCallback:^(id responseData) {
NSLog(@"调用完JS后的回调:%@",responseData);
}];

4.OC调用JS代码

// 直接运行 使用 
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];

// 使用JavaScriptCore框架
#import
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//获取webview中的JS内容
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSString *runJS = @"执行的JS代码";
//准备执行的JS代码
[context evaluateScript:runJS];
}

十三.编译过程做了哪些事情

Objective,Swift都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。Objective,Swift二者的编译都是依赖于Clang + LLVM. OC和Swift因为原理上大同小异,知道一个即可!
1.iOS编译 不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。
2.编译器前端 :编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行
3.编译器后端 :编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。LVVM优化器会进行BitCode的生成,链接期优化等等,LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码。

十四.Category的实现原理&&使用场合&&Class Extension的区别

1.Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
2.在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

使用场合:

在不修改原有类代码的情况下,为类添对象方法或者类方法
或者为类关联新的属性
分解庞大的类文件

添加实例方法
添加类方法
添加协议
添加属性
关联成员变量

区别

Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中。

十五.Category能否添加成员变量?如果可以,如何给Category添加成员变量?

不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构)
可以通过 runtime 动态的关联属性

十六.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

十七.initialize方法如何调用,以及调用时机

当类第一次收到消息的时候会调用类的initialize方法
是通过 runtime 的消息机制 objc_msgSend(obj,@selector()) 进行调用的
优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的

十八.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

load 是类加载到内存时候调用, 优先父类->子类->分类
initialize 是类第一次收到消息时候调用,优先分类->子类->父类
同级别和编译顺序有关系
load 方法是在 main 函数之前调用的

十九.什么是Runtime?平时项目中有用过么?

Objective-C runtime是一个运行时库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码,类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用。通过了解runtime以及源码,可以更加深入的了解OC其特性和原理

OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行

OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数

平时编写的OC代码,底层都是转换成了Runtime API进行调用

具体应用

利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题

二十.讲一下 OC 的消息机制

1.OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
2.objc_msgSend底层有3大阶段   消息发送(当前类、父类中查找)、动态方法解析、消息转发

消息发送流程

当我们的一个 receiver(实例对象)收到消息的时候, 会通过 isa 指针找到 他的类对象, 然后在类对象方法列表中查找 对应的方法实现,如果 未找到,则会通过 superClass 指针找到其父类的类对象, 找到则返回,未找打则会一级一级往上查到,最终到NSObject 对象, 如果还是未找到就会进行动态方法解析
类方法调用同上,只不过 isa 指针找到元类对象;

动态方法解析机制&&消息转发机制流程

当我们发送消息未找到方法实现,就会进入第二步,动态方法解析: 代码实现如下

//  动态方法绑定- 实例法法调用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(run)) {
Method method = class_getInstanceMethod(self, @selector(test));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 类方法调用
+(BOOL) resolveClassMethod:(SEL)sel....

未找到动态方法绑定,就会进行消息转发阶段

// 快速消息转发- 指定消息处理对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
return [Student new];
}
return [super forwardingTargetForSelector:aSelector];
}

// 标准消息转发-消息签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector == @selector(run))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//内部逻辑自己处理
}

答案摘自作者:iOS猿_员

原贴链接:https://www.jianshu.com/p/4aaf45c11082

收起阅读 »

TCP、UDP协议和IP协议

一、TCP定义TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。面向连接意味着两个使用TCP的进程(一个客户和一个服务器)在交换数据之前必须先建立好连接,然后才能开始传输数据。建立连接时采用客户服务器模式,其中主动发起连接建立的进程叫做客户(Clie...
继续阅读 »
一、TCP
  1. 定义

    TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。面向连接意味着两个使用TCP的进程(一个客户和一个服务器)在交换数据之前必须先建立好连接,然后才能开始传输数据。建立连接时采用客户服务器模式,其中主动发起连接建立的进程叫做客户(Client),被动等待连接建立的进程叫做服务器(Server)。

  2. 端对端

    TCP提供全双工的数据传输服务,这意味着建立了TCP连接的主机双方可以同时发送和接收数据。这样,接收方收到发送方消息后的确认可以在反方向的数据流中进行捎带。“端到端”的TCP通信意味着TCP连接发生在两个进程之间,一个进程发送数据,只有一个接收方,因此TCP不支持广播和组播。

  3. 面向字节

    TCP连接面向字节流,字节流意味着用户数据没有边界,例如,发送进程在TCP连接上发送了2个512字节的数据,接收方接收到的可能是2个512字节的数据,也可能是1个1024字节的数据。因此,接收方若要正确检测数据的边界,必须由发送方和接收方共同约定,并且在用户进程中按这些约定来实现。

  4. 位于传输层
    TCP接收到数据包后,将信息送到更高层的应用程序,如FTP的服务程序和客户程序。应用程序处理后,再轮流将信息送回传输层,传输层再将它们向下传送到网际层,最后到接收方。


二、UDP

UDP与TCP位于同一层,但与TCP不同

  • UDP协议提供的是一种无连接的、不可靠的传输层协议,只提供有限的差错检验功能。

  • 它在IP层上附加了简单的多路复用功能,提供端到端的数据传输服务。

  • 设计UDP的目的是为了以最小的开销在可靠的或者是对数据可靠性要求不高的环境中进行通信,

  • 由于无连接,UDP支持广播和组播,这在多媒体应用中是非常有用的。


三、IP协议

  1. 定义

    IP(网际)协议是TCP/IP模型的核心,也是网络层最重要的协议。

  2. 功能

    网际层接收来自网络接口层的数据包,并将数据包发送到传输层;相反,也将传输层的数据包传送到网络接口层。
    IP协议主要包括无连接数据报传送,数据报路由器选择以及差错处理等功能。

  3. 局限及对策

    由于网络拥挤、网络故障等问题可能导致数据报无法顺利通过传输层。IP协议具有有限的报错功能,不能有效处理数据报延迟,不按顺序到达和数据报出错,所以IP协议需要与另外的协议配套使用,包括地址解析协议ARP、逆地址解析协议RARP、因特网控制报文协议ICMP、因特网组管理协议IGMP等。
    IP数据包中含有源地址(发送它的主机地址)和目的地址(接收它的主机地址)。

  4. 意义

    IP协议对于网络通信而言有着重要的意义。由于网络中的所有计算机都安装了IP软件,使得许许多多的局域网构成了庞大而严密的通信系统,才形成了如今的Internet。其实,Internet并非一个真实存在的网络,而是一个虚拟网络,只不过是利用IP协议把世界上所有愿意接入Internet的计算机局域网络连接起来,使之能够相互通信。

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

收起阅读 »

iOS 一键返回首页

在APP的开发中,我们难免会遇到这种情况,一层层的打开下一级控制,这时,我们再想回到原始控制器时,一级级返回不太现实,所以我们需要一种方法,来一次性返回首页从App的rootViewController开始,找到所有presentedController,然后...
继续阅读 »

在APP的开发中,我们难免会遇到这种情况,一层层的打开下一级控制,这时,我们再想回到原始控制器时,一级级返回不太现实,所以我们需要一种方法,来一次性返回首页

从App的rootViewController开始,找到所有presentedController,然后逆序dismiss这些Controller,最后pop to rootViewController就可以了。

- (void)backToHomePage
{
UIWindow *window = [(AppDelegate *)[UIApplication sharedApplication].delegate window];
UIViewController *presentedController = nil;

UIViewController *rootController = [window rootViewController];
if ([rootController isKindOfClass:[UITabBarController class]]) {
rootController = [(UITabBarController *)rootController selectedViewController];
}
presentedController = rootController;
//找到所有presented的controller,包括UIViewController和UINavigationController
NSMutableArray *presentedControllerArray = [[NSMutableArray alloc] init];
while (presentedController.presentedViewController) {
[presentedControllerArray addObject:presentedController.presentedViewController];
presentedController = presentedController.presentedViewController;
}
if (presentedControllerArray.count > 0) {
//把所有presented的controller都dismiss掉
[self dismissControllers:presentedControllerArray topIndex:presentedControllerArray.count - 1 completion:^{
[self popToRootViewControllerFrom:rootController];
}];
} else {
[self popToRootViewControllerFrom:rootController];
}
}
- (void)dismissControllers:(NSArray *)presentedControllerArray topIndex:(NSInteger)index completion:(void(^)(void))completion
{
if (index < 0) {
completion();
} else {
[presentedControllerArray[index] dismissViewControllerAnimated:NO completion:^{
[self dismissControllers:presentedControllerArray topIndex:index - 1 completion:completion];
}];
}
}
- (void)popToRootViewControllerFrom:(UIViewController *)fromViewController
{
//pop to root
if ([fromViewController isKindOfClass:[UINavigationController class]]) {
[(UINavigationController *)fromViewController popToRootViewControllerAnimated:YES];
}
if (fromViewController.navigationController) {
[fromViewController.navigationController popToRootViewControllerAnimated:YES];
}
}

参考这个思路可以做一些其他非常规页面跳转,跳转到我们想要跳转的指定界面去

原文链接:https://blog.csdn.net/yinyignfenlei/article/details/86167245

收起阅读 »

Node交互式命令行工具开发——自动化文档工具

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。  npm上命令行开发相关包很多,例如minimist、optimist、nopt、commander.js...
继续阅读 »

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。
  npm上命令行开发相关包很多,例如minimistoptimistnoptcommander.jsyargs等等,使用方法和效果类似。其中用得比较多的是TJ大神的commanderyargs,本文以commander为基础讲述,可以参考这篇教程,yargs教程可以参考阮大神的或者这一篇
  另外,一个完整的命令行工具开发,还需要了解processshelljspathlinebyline等模块,这些都是node基础模块或一些简单模块,非常简单,就不多说了,另外如果你不想用回调函数处理异步还需要了解一下PromiseGenerator函数。这是教程:i5ting大神的《深入浅出js(Node.js)异步流程控制》和阮大神的异步编程教程以及promise小人书,另外想尝试ES7 stage3阶段的async/await异步解决方案,可参考这篇教程async/await解决方案需要babel转码,这是教程。本人喜欢async/await(哪个node开发者不喜欢呢?)但不喜欢倒腾,况且async/await本身就是Promise的语法糖,所以没选择使用,据江湖消息,nodejs将在今年晚些时候(10月份?)支持async/await,很是期待。
  以下是文章末尾实例用到的一些依赖。

"dependencies": {
"bluebird": "^3.4.1",
"co": "^4.6.0",
"colors": "^1.1.2",
"commander": "^2.9.0",
"dox": "^0.9.0",
"handlebars": "^4.0.5",
"linebyline": "^1.3.0",
"mkdirp": "^0.5.1"
}

 其中bluebird用于Promise化,TJ大神的co用于执行Generator函数,handlebars是一种模板,linebyline用于分行读取文件,colors用于美化输出,mkdirp用于创建目录,另外教程中的示例是一款工具,可以自动化生成数据库和API接口的markdown文档,并通过修改git hooks,使项目的每次commit都会自动更新文档,借助了TJ大神的dox模块。
  <span style="color:rgb(0, 136, 204)">所有推荐教程/教材,仅供参考,自行甄选阅读。</span>

安装Node

  各操作系统下安装见Nodejs官网,安装完成之后用node -v或者which node等命令测试安装是否成功。which在命令行开发中是一个非常有用的命令,使用which命令确保你的系统中不存在名字相同的命令行工具,例如which commandName,例如which testdev命令返回空白那么说明testdev命令名称还没有被使用。

初始化

  1. 新建一个.js文件,即是你的命令要执行的主程序入口文件,例如testdev.js。在文件第一行加入#!/usr/bin/env node指明系统在运行这个文件的时候使用node作为解释器,等价于node testdev.js命令。
  2. 初始化package.json文件,使用npm init命令根据提示信息创建,也可以是使用npm init -y使用默认设置创建。创建完成之后需要修改package.json文件内容加入"bin": {"testdev": "./testdev.js"}这条信息用于告诉npm你的命令(testdev)要执行的脚本文件的路径和名字,这里我们指定testdev命令的执行文件为当前目录下的testdev.js文件。
  3. 为了方便测试在testdev.js文件中加入代码console.log('hello world');,这里只是用于测试环境是否搭建成功,更加复杂的程序逻辑和过程需要按照实际情况进行编写

测试

  使用npm link命令,可以在本地安装刚刚创建的包,然后就可以用testdev来运行命令了,如果正常的话在控制台会打印出hello world

commander

  TJ的commander非常简洁,README.md已经把使用方法写的非常清晰。下面是例子中的代码:

const program = require('commander'),
co = require('co');

const appInfo = require('./../package.json'),
asyncFunc = require('./../common/asyncfunc.js');

program.allowUnknownOption();
program.version(appInfo.version);

program
.command('init')
.description('初始化当前目录doc.json文件')
.action(() => co(asyncFunc.initAction));

program
.command('show')
.description('显示配置文件状态')
.action(() => co(asyncFunc.showAction));

program
.command('run')
.description('启动程序')
.action(() => co(asyncFunc.runAction));

program
.command('modifyhook')
.description('修改项目下的hook文件')
.action(() => co(asyncFunc.modifyhookAction));

program
.command('*')
.action((env) => {
console.error('不存在命令 "%s"', env);
});

program.on('--help', () => {
console.log(' Examples:');
console.log('');
console.log(' $ createDOC --help');
console.log(' $ createDOC -h');
console.log(' $ createDOC show');
console.log('');
});

program.parse(process.argv);

 定义了四个命令和个性化帮助说明。

交互式命令行process

  commander只是实现了命令行参数与回复一对一的固定功能,也就是一个命令必然对应一个回复,那如何实现人机交互式的命令行呢,类似npm init或者eslint --init这样的与用户交互,交互之后根据用户的不同需求反馈不同的结果呢。这里就需要node内置的process模块。
  这是我实现的一个init命令功能代码:

exports.initAction = function* () {
try {
var docPath = yield exists(process.cwd() + '/doc.json');
if (docPath) {
func.initRepl(config.coverInit, arr => {
co(newDoc(arr));
})
} else {
func.initRepl(config.newInit, arr => {
co(newDoc(arr));
})
}
} catch (err) {
console.warn(err);
}

首先检查doc.json文件是否存在,如果存在执行覆盖交互,如果不存在执行生成交互,try...catch捕获错误。
  交互内容配置如下:

newInit:
[
{
title:'initConfirm',
description:'初始化createDOC,生成doc.json.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],
coverInit:[
{
title:'modifyConfirm',
description:'doc.json已存在,初始化将覆盖文件.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],

人机交互部分代码也就是initRepl函数内容如下:

//初始化命令,人机交互控制
exports.initRepl = function (init, func) {
var i = 1;
var inputArr = [];
var len = init.length;
process.stdout.write(init[0].description);
process.stdin.resume();
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk) => {
chunk = chunk.replace(/[\s\n]/, '');
if (chunk !== 'y' && chunk !== 'Y' && chunk !== 'n' && chunk !== 'N') {
console.log(config.colors.red('您输入的命令是: ' + chunk));
console.warn(config.colors.red('请输入正确指令:y/n'));
process.exit();
}
if (
(init[i - 1].title === 'modifyConfirm' || init[i - 1].title === 'initConfirm') &&
(chunk === 'n' || chunk === 'N')
) {
process.exit();
}
var inputJson = {
title: init[i - 1].title,
value: chunk,
};
inputArr.push(inputJson);
if ((len--) > 1) {
process.stdout.write(init[i++].description)
} else {
process.stdin.pause();
func(inputArr);
}
});
}

人机交互才用向用户提问根据用户不同输入产生不同结果的形式进行,顺序读取提问列表并记录用户输入结果,如果用户输入n/N则终止交互,用户输入非法字符(除y/Y/n/N以外)提示输入命令错误。

文档自动化

  文档自动化,其中数据库文档自动化,才用依赖sequelize的方法手写(根据需求不同自行编写逻辑),API文档才用TJ的dox也很简单。由于此处代码与命令行功能相关度不大,请读者自行去示例地址查看代码。

示例地址

github地址
npm地址

原文链接:https://segmentfault.com/a/1190000039749423

收起阅读 »

JS前端面试总结

ES5的继承和ES6的继承有什么区别ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制完全不同,实质上是先创建父类的实...
继续阅读 »

ES5的继承和ES6的继承有什么区别

ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。
ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。

如何实现一个闭包?闭包的作用有哪些

在一个函数里面嵌套另一个函数,被嵌套的那个函数的作用域是一个闭包。
作用:创建私有变量,减少全局变量,防止变量名污染。可以操作外部作用域的变量,变量不会被浏览器回收,保存变量的值。

介绍一下 JS 有哪些内置对象

Object 是 JavaScript 中所有对象的父对象
数据封装类对象:Object、Array、Boolean、Number、String
其他对象:Function、Argument、Math、Date、RegExp、Error

new 操作符具体干了什么呢

(1)创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
(2)属性和方法被加入到 this 引用的对象中。
(3)新创建的对象由 this 所引用,并且最后隐式的返回 this 。

同步和异步的区别

同步的概念应该是来自于操作系统中关于同步的概念:不同进程为协同完成某项工作而在先后次序上调整(通过阻塞,唤醒等方式)。
同步强调的是顺序性,谁先谁后;异步则不存在这种顺序性。

同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作。

异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容。

异步解决方式优缺点

回调函数(callback)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return
优点:解决了同步的问题

Promise

Promise就是为了解决callback的问题而产生的。
回调地狱的根本问题在于:

缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
嵌套函数过多的多话,很难处理错误

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题
缺点:无法取消 Promise ,错误需要通过回调函数来捕获

Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

Async/await

async、await 是异步的终极解决方案

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

null 和 undefined 的区别

null: null表示空值,转为数值时为0;
undefined:undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

• 变量被声明了,但没有赋值时,就等于undefined。
• 对象没有赋值的属性,该属性的值为undefined。
• 函数没有返回值时,默认返回undefined。

JavaScript 原型,原型链 ? 有什么特点?

JavaScript 原型: 每创建一个函数,函数上都有一个属性为 prototype,它的值是一个对象。 这个对象的作用在于当使用函数创建实例的时候,那么这些实例都会共享原型上的属性和方法。

原型链: 在 JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接(proto)。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向)。这种一级一级的链结构就称为原型链(prototype chain)。 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止;到查找到达原型链的顶部(Object.prototype),仍然没有找到指定的属性,就会返回 undefined

如何获取一个大于等于0且小于等于9的随机整数

function randomNum(){
return Math.floor(Math.random()*10)
}

想要去除一个字符串的第一个字符,有哪些方法可以实现str.slice(1)

 str.substr(1)
str.substring(1)
str.replace(/./,'')
str.replace(str.charAt(0),'')

JavaScript的组成

JavaScript 由以下三部分组成:

ECMAScript(核心):JavaScript 语言基础
DOM(文档对象模型):规定了访问HTML和XML的接口
BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法

到底什么是前端工程化、模块化、组件化

前端工程化就是用做工程的思维看待和开发自己的项目,
而模块化和组件化是为工程化思想下相对较具体的开发方式,因此可以简单的认为模块化和组件化是工程化的表现形式。
模块化和组件化一个最直接的好处就是复用,同时我们也应该有一个理念,模块化和组件化除了复用之外还有就是分治,我们能够在不影响其他代码的情况下按需修改某一独立的模块或是组件,因此很多地方我们及时没有很强烈的复用需要也可以根据分治需求进行模块化或组件化开发。
模块化开发的4点好处:

  1 避免变量污染,命名冲突
  2 提高代码复用率
  3 提高维护性
4 依赖关系的管理

前端模块化实现的过程如下:
一 函数封装
我们在讲到函数逻辑的时候提到过,函数一个功能就是实现特定逻辑的一组语句打包,在一个文件里面编写几个相关函数就是最开始的模块了

function m1(){
    //...
  }

  function m2(){
    //...
  }

这样做的缺点很明显,污染了全局变量,并且不能保证和其他模块起冲突,模块成员看起来似乎没啥关系
二 对象
为了解决这个问题,有了新方法,将所有模块成员封装在一个对象中

var module = new Object({

_count:0,

m1:function (){ ``` },

m2:function (){ ``` }

})

这样 两个函数就被包在这个对象中, 嘿嘿 看起来没毛病是吗 继续往下:
当我们要使用的时候,就是调用这个对象的属性
module.m1()
诶嘿 那么问题来了 这样写法会暴露全部的成员,内部状态可以被外部改变,比如外部代码可直接改变计数器的值
//坏人的操作

module._count = 10;

最后的最后,聪明的人类找到了究极新的方法——立即执行函数,这样就可以达到不暴露私有成员的目的

var module = (function (){

var _count = 5;

var m1 = function (){ ``` };

var m2 = function (){ ``` };

return{
m1:m1,
m2:m2
}

})()

面向对象与面向过程

  1. 什么是面向过程与面向对象?

• 面向过程就是做围墙的时候,由你本身操作,叠第一层的时候:放砖头,糊水泥,放砖头,糊水泥;然后第二层的时候,继续放砖头,糊水泥,放砖头,糊水泥……
• 面向对象就是做围墙的时候,由他人帮你完成,将做第一层的做法抽取出来,就是放砖头是第一个动作,糊水泥是第二个动作,然后给这两个动作加上步数,最后告诉机器人有 n 层,交给机器人帮你工作就行了。

  1. 为什么需要面向对象写法?

• 更方便
• 可以复用,减少代码冗余度
• 高内聚低耦合
简单来说,就是增加代码的可复用性,减少咱们的工作,使代码更加流畅。

事件绑定和普通事件有什么区别

普通添加事件的方法:

var btn = document.getElementById("hello");
btn.onclick = function(){
alert(1);
}
btn.onclick = function(){
alert(2);
}

执行上面的代码只会alert 2

事件绑定方式添加事件:

var btn = document.getElementById("hello");
btn.addEventListener("click",function(){
alert(1);
},false);
btn.addEventListener("click",function(){
alert(2);
},false);

垃圾回收

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
  现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
1、标记清除
  这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
  垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
关于这一块,建议读读Tom大叔的几篇文章,关于作用域链的一些知识详解,读完差不多就知道了,哪些变量会被做标记。

2、引用计数
  另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。


原文链接:https://segmentfault.com/a/1190000018077712


收起阅读 »

面向面试编程,面向掘金面试

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣关于前端,后端,移动端的面试,这里统...
继续阅读 »

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣

关于前端,后端,移动端的面试,这里统统都有,希望可以在面试的过程中帮助到你。另外我也有一个仓库 日问 来记录前后端以及 devops 一些有意思的问题,欢迎交流

前端

后端

Android/IOS

原文:https://segmentfault.com/a/1190000021037487

收起阅读 »

通用的广告栏控件-ConvenientBanner

demo:ConvenientBanner通用的广告栏控件,让你轻松实现广告头效果。支持无限循环,可以设置自动翻页和时间(而且非常智能,手指触碰则暂停翻页,离开自动开始翻页。你也可以设置在界面onPause的时候不进行自动翻页,onResume之后继续自动翻页...
继续阅读 »

demo:



ConvenientBanner

通用的广告栏控件,让你轻松实现广告头效果。支持无限循环,可以设置自动翻页和时间(而且非常智能,手指触碰则暂停翻页,离开自动开始翻页。你也可以设置在界面onPause的时候不进行自动翻页,onResume之后继续自动翻页),并且提供多种翻页特效。 对比其他广告栏控件,大多都需要对源码进行改动才能加载网络图片,或者帮你集成不是你所需要的图片缓存库。而这个库能让有代码洁癖的你欢喜,不需要对库源码进行修改你就可以使用任何你喜欢的网络图片库进行配合。

demo是用Module方式依赖,你也可以使用gradle 依赖:

    implementation 'com.bigkoo:convenientbanner:2.1.5'//地址变小写了,额。。。
implementation 'androidx.recyclerview:recyclerview:1.0.0+'

// compile 'com.bigkoo:ConvenientBanner:2.1.4'//地址变ConvenientBanner 大写了,额。。。
//compile 'com.bigkoo:convenientbanner:2.0.5'旧版
Config in xml
<com.bigkoo.convenientbanner.ConvenientBanner
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/convenientBanner"
android:layout_width="match_parent"
android:layout_height="200dp"
app:canLoop="true" //控制循环与否
/>

config in java code

//自定义你的Holder,实现更多复杂的界面,不一定是图片翻页,其他任何控件翻页亦可。
convenientBanner.setPages(
new CBViewHolderCreator() {
@Override
public LocalImageHolderView createHolder(View itemView) {
return new LocalImageHolderView(itemView);
}

@Override
public int getLayoutId() {
return R.layout.item_localimage;
}
}, localImages)
//设置两个点图片作为翻页指示器,不设置则没有指示器,可以根据自己需求自行配合自己的指示器,不需要圆点指示器可用不设
// .setPageIndicator(new int[]{R.drawable.ic_page_indicator, R.drawable.ic_page_indicator_focused})
.setOnItemClickListener(this);
//设置指示器的方向
// .setPageIndicatorAlign(ConvenientBanner.PageIndicatorAlign.ALIGN_PARENT_RIGHT)
// .setOnPageChangeListener(this)//监听翻页事件
;

public class LocalImageHolderView implements Holder<Integer>{
private ImageView imageView;
@Override
public View createView(Context context) {
imageView = new ImageView(context);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
return imageView;
}

@Override
public void UpdateUI(Context context, final int position, Integer data) {
imageView.setImageResource(data);
}
}


原文链接:https://github.com/saiwu-bigkoo/Android-ConvenientBanner

代码下载:Android-ConvenientBanner-master.zip

收起阅读 »

iOS面试题(二)

数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t 对象,类对象,元类对象 消息传递 消息转发 一、数据结构:objc_object,objc_class,isa,class...
继续阅读 »
  • 数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t

  • 对象,类对象,元类对象

  • 消息传递

  • 消息转发



一、数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t







  • objc_object(id)
    isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关

  • objc_class (class) 继承自objc_object

  • isa指针,共用体isa_t


  • isa指向

    关于对象,其指向类对象。

    关于类对象,其指向元类对象。

    实例--(isa)-->class--(isa)-->MetaClass

  • cache_t

    用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用


 struct cache_t {
struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
mask_t _mask;//分配用来缓存bucket的总数
mask_t _occupied;//表明目前实际占用的缓存bucket的个数

struct bucket_t {
private:
cache_key_t _key;
IMP _imp;

复制代码


  • class_data_bits_t:对class_rw_t的封装


struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

复制代码

Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议


struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};
复制代码


  • method_t

    函数四要素:名称,返回值,参数,函数体


struct method_t {
SEL name; //名称
const char *types;//返回值和参数
IMP imp; //函数体

复制代码

二、 对象,类对象,元类对象



  • 类对象存储实例方法列表等信息。

  • 元类对象存储类方法列表等信息。


  • superClass是一层层集成的,到最后NSObject的superClass是nil.而NSObject的isa指向根元类,这个根元类的isa指向它自己,而它的superClass是NSObject,也就是最后形成一个环,

    三、消息传递


    void objc_msgSend(void /* id self, SEL op, ... */ )

    void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

    struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message.
    #if !defined(__cplusplus) && !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
    #else
    __unsafe_unretained _Nonnull Class super_class;
    #endif
    /* super_class is the first class to search */
    };
    复制代码



消息传递的流程:缓存查找-->当前类查找-->父类逐级查找



  • 调用方法之前,先去查找缓存,看看缓存中是否有对应选择器的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)

  • 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法 ,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)

  • 如果当前类对象的方法列表没有,就会逐级父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程)

  • 如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递


四、消息转发


+ (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
+ (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
复制代码





那么最后消息未能处理的时候,还会调用到

- (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来

方法交换(Method-Swizzling)
+ (void)load
{
Method test = class_getInstanceMethod(self, @selector(test));

Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

method_exchangeImplementations(test, otherTest);
}

应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息

动态添加方法

class_addMethod(self, sel, testImp, "v@:");

void testImp (void)
{
NSLog(@"testImp");
}

  • @dynamic 动态方法解析

    动态运行时语言将函数决议推迟到运行时

    编译时语言在编译期进行函数决议


  • [obj foo]和objc_msgSend()函数之间有什么关系?

    objc_msgSend()是[obj foo]的具体实现。在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。


  • runtime是如何通过selector找到对应的IMP地址的?

    缓存查找-->当前类查找-->父类逐级查找


  • 能否向编译后的类中增加实例变量?

    不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。

    但可以向动态添加的类中增加实例变量。


链接:https://juejin.cn/post/6844904039004504072 收起阅读 »

iOS面试题(一)

字符串反转链表反转有序数组合并Hash算法查找两个子视图的共同父视图求无序数组当中的中位数一、字符串反转给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh- (void)charReverse { NSString ...
继续阅读 »
  • 字符串反转
  • 链表反转
  • 有序数组合并
  • Hash算法
  • 查找两个子视图的共同父视图
  • 求无序数组当中的中位数

一、字符串反转
给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh

- (void)charReverse
{
NSString * string = @"hello,world";

NSLog(@"%@",string);

NSMutableString * reverString = [NSMutableString stringWithString:string];

for (NSInteger i = 0; i < (string.length + 1)/2; i++) {

[reverString replaceCharactersInRange:NSMakeRange(i, 1) withString:[string substringWithRange:NSMakeRange(string.length - i - 1, 1)]];

[reverString replaceCharactersInRange:NSMakeRange(string.length - i - 1, 1) withString:[string substringWithRange:NSMakeRange(i, 1)]];
}

NSLog(@"reverString:%@",reverString);

//C
char ch[100];

memcpy(ch, [string cStringUsingEncoding:NSUTF8StringEncoding], [string length]);

//设置两个指针,一个指向字符串开头,一个指向字符串末尾
char * begin = ch;

char * end = ch + strlen(ch) - 1;

//遍历字符数组,逐步交换两个指针所指向的内容,同时移动指针到对应的下个位置,直至begin>=end
while (begin < end) {

char temp = *begin;

*(begin++) = *end;

*(end--) = temp;
}

NSLog(@"reverseChar[]:%s",ch);
}
复制代码

二、链表反转
反转前:1->2->3->4->NULL
反转后:4->3->2->1->NULL

/**  定义一个链表  */
struct Node {

NSInteger data;

struct Node * next;
};

- (void)listReverse
{
struct Node * p = [self constructList];

[self printList:p];

//反转后的链表头部
struct Node * newH = NULL;
//头插法
while (p != NULL) {

//记录下一个结点
struct Node * temp = p->next;
//当前结点的next指向新链表的头部
p->next = newH;
//更改新链表头部为当前结点
newH = p;
//移动p到下一个结点
p = temp;
}

[self printList:newH];
}
/**
打印链表

@param head 给定链表
*/

- (void)printList:(struct Node *)head
{
struct Node * temp = head;

printf("list is : ");

while (temp != NULL) {

printf("%zd ",temp->data);

temp = temp->next;
}

printf("\n");
}


/** 构造链表 */
- (struct Node *)constructList
{
//头结点
struct Node *head = NULL;
//尾结点
struct Node *cur = NULL;

for (NSInteger i = 0; i < 10; i++) {

struct Node *node = malloc(sizeof(struct Node));

node->data = i;

//头结点为空,新结点即为头结点
if (head == NULL) {

head = node;

}else{
//当前结点的next为尾结点
cur->next = node;
}

//设置当前结点为新结点
cur = node;
}

return head;
}

复制代码

三、有序数组合并
将有序数组 {1,4,6,7,9} 和 {2,3,5,6,8,9,10,11,12} 合并为
{1,2,3,4,5,6,6,7,8,9,9,10,11,12}

- (void)orderListMerge
{
int aLen = 5,bLen = 9;

int a[] = {1,4,6,7,9};

int b[] = {2,3,5,6,8,9,10,11,12};

[self printList:a length:aLen];

[self printList:b length:bLen];

int result[14];

int p = 0,q = 0,i = 0;//p和q分别为a和b的下标,i为合并结果数组的下标

//任一数组没有达到s边界则进行遍历
while (p < aLen && q < bLen) {

//如果a数组对应位置的值小于b数组对应位置的值,则存储a数组的值,并移动a数组的下标与合并结果数组的下标
if (a[p] < b[q]) result[i++] = a[p++];

//否则存储b数组的值,并移动b数组的下标与合并结果数组的下标
else result[i++] = b[q++];
}

//如果a数组有剩余,将a数组剩余部分拼接到合并结果数组的后面
while (++p < aLen) {

result[i++] = a[p];
}

//如果b数组有剩余,将b数组剩余部分拼接到合并结果数组的后面
while (q < bLen) {

result[i++] = b[q++];
}

[self printList:result length:aLen + bLen];
}
- (void)printList:(int [])list length:(int)length
{
for (int i = 0; i < length; i++) {

printf("%d ",list[i]);
}

printf("\n");
}
复制代码

四、HASH算法

  • 哈希表
    例:给定值是字母a,对应ASCII码值是97,数组索引下标为97。
    这里的ASCII码,就算是一种哈希函数,存储和查找都通过该函数,有效地提高查找效率。
  • 在一个字符串中找到第一个只出现一次的字符。如输入"abaccdeff",输出'b'

    字符(char)是一个长度为8的数据类型,因此总共有256种可能。每个字母根据其ASCII码值作为数组下标对应数组种的一个数字。数组中存储的是每个字符出现的次数。
- (void)hashTest
{
NSString * testString = @"hhaabccdeef";

char testCh[100];

memcpy(testCh, [testString cStringUsingEncoding:NSUTF8StringEncoding], [testString length]);

int list[256];

for (int i = 0; i < 256; i++) {

list[i] = 0;
}

char *p = testCh;

char result = '\0';

while (*p != result) {

list[*(p++)]++;
}

p = testCh;

while (*p != result) {

if (list[*p] == 1) {

result = *p;

break;
}

p++;
}

printf("result:%c",result);
}
复制代码

五、查找两个子视图的共同父视图
思路:分别记录两个子视图的所有父视图并保存到数组中,然后倒序寻找,直至找到第一个不一样的父视图。

- (void)findCommonSuperViews:(UIView *)view1 view2:(UIView *)view2
{
NSArray * superViews1 = [self findSuperViews:view1];

NSArray * superViews2 = [self findSuperViews:view2];

NSMutableArray * resultArray = [NSMutableArray array];

int i = 0;

while (i < MIN(superViews1.count, superViews2.count)) {

UIView *super1 = superViews1[superViews1.count - i - 1];

UIView *super2 = superViews2[superViews2.count - i - 1];

if (super1 == super2) {

[resultArray addObject:super1];

i++;

}else{

break;
}
}

NSLog(@"resultArray:%@",resultArray);

}
- (NSArray *)findSuperViews:(UIView *)view
{
UIView * temp = view.superview;

NSMutableArray * result = [NSMutableArray array];

while (temp) {

[result addObject:temp];

temp = temp.superview;
}

return result;
}
复制代码

六、求无序数组中的中位数
中位数:当数组个数n为奇数时,为(n + 1)/2,即是最中间那个数字;当n为偶数时,为(n/2 + (n/2 + 1))/2,即是中间两个数字的平均数。
首先要先去了解一些几种排序算法:iOS排序算法
思路:

  • 1.排序算法+中位数
    首先用冒泡排序、快速排序、堆排序、希尔排序等排序算法将所给数组排序,然后取出其中位数即可。
  • 2.利用快排思想
链接:https://juejin.cn/post/6844904038996279309
收起阅读 »

vue 自动化路由实现

1、需求描述在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我...
继续阅读 »

1、需求描述

在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示


那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我们几乎都是一个文件夹下有很多子文件,子文件中一般包含index.vue, detail.vue, edit.vue分别对应的事列表页,详情页和编辑页。


 上图是我们的文件目录,views文件夹中存放的是所有的页面,goodsPlanning是一级目录,onNewComplete和thirdGoods是二级目录,二级目录中存放的是具体的页面,indexComponents中存放的是index.vue的文件,editComponents也是同样的道理。index.vue对应的路由是/goodsPlanning/onNewComplete, edit.vue对应的路由是/goodsPlanning/onNewComplete/edit,detail.vue也是同样的道理。所以我们的文件夹和路由是完全能够对应上的,只要知道路由,就能很快的找到对应的文件。那么有没有办法能够读取我们二级目录下的所有文件,然后根据文件名来生成路由呢?答案是有的


2 、require.context介绍

简单说就是:有了require.context,我们可以得到指定文件夹下的所有文件

require.context(directory, useSubdirectories = false, regExp = /^\.\//);

require.context有三个参数:

  • directory:说明需要检索的目录
  • useSubdirectories:是否检索子目录
  • regExp: 匹配文件的正则表达式

require.context()的返回值,有一个keys方法,返回的是个数组

let routers = require.context('VIEWS', true).keys()
console.log(routers)



 通过上面的代码,我们打印出了所有的views文件夹下的所有文件和文件夹,我们只要写好正则就能找到我们所需要的文件


3、 直接上代码

import Layout from 'VIEWS/layout/index'

/**
* 正则 首先匹配./ ,然后一级目录,不包含components的二级目录,以.vue结尾的三级目录
*/
let routers = require.context('VIEWS', true, /\.\/[a-z]+\/(?!components)[a-z]+\/[a-z]+\.vue$/i).keys()
let indexRouterMap = {} // 用来存储以index.vue结尾的文件,因为index.vue是列表文件,需要加入layout(我们的菜单),需要keepAlive,需要做权限判断
let detailRouterArr = [] // 用来存储以非index.vue结尾的vue文件,此类目前不需要layout
routers.forEach(item => {
const paths = item.match(/[a-zA-Z]+/g) //paths中存储了一个目录,二级目录,文件名
const routerChild = { //定义路由对象
path: paths[1],
name: `${paths[0]}${_.upperFirst(paths[1])}`, //upperFirst,lodash 首字母大写方法
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
}
if (/index\.vue$/.test(item)) { //判断是否以indexvue结尾
if (indexRouterMap[paths[0]]) { //判断一级路由是否存在,存在push二级路由,不存在则新建
indexRouterMap[paths[0]].children.push(routerChild)
} else {
indexRouterMap[paths[0]] = {
path: '/' + paths[0],
component: Layout,
children: [routerChild]
}
}
} else { //不以index.vue结尾的,直接添加到路由中
detailRouterArr.push({
path: item.slice(1, -4), //渠道最前面的 . 和最后的.vue
name: `${paths[0]}${_.upperFirst(paths[1])}${_.upperFirst(paths[2])}`,
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
meta: {
noCache: true, //不keepAlive
noVerify: true //不做权限验证
}
})
}
})

export default [
...Object.values(indexRouterMap),
...detailRouterArr,
/**
* dashboard单独处理下
*/
{
path: '',
component: Layout,
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: () => import('VIEWS/dashboard/index'),
name: 'dashboard',
meta: { title: '首页', noCache: true, noVerify: true }
}
]
},
]

简简单单的几十行代码就实现了所有的路由功能,再也不用一行一行的写路由文件了。可能你的文件管理方式和我的不一样,但是只要稍微改改正则就行了。


4、 注意

  1. 不能用import引入路由因为用import引入不支持变量
  2. 不能用别名找了半天问题,才知道用变量时也不能用别名,所以我用的都是相对路径


5、 好处

  • 不用在添加路由了,这个就不说了,明眼人都看得出来
  • 知道了路由,一个能找到对应的文件,以前我们团队就出现过,乱写path的情况
  • 更好的控制验证和keepAlive

原文链接:https://www.cnblogs.com/mianbaodaxia/p/11452123.html

收起阅读 »

iOS基础之Category(一)

一、简介 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要...
继续阅读 »

一、简介


  1. 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要的 category,比如 SDWebImage 中 UIImageView+WebCache 和 UIButton+WebCache,根据不同需求加载不同的 category。


二、Extension 和 Category 对比



  • extension 是在编译器决定的,它就是类的一部分,在编译期和头文件里的 @interface 和 实现文件里的 @implementation形成一个完整的类,它伴随类的的产生而产生,随着类的消亡而消亡。extension 一般用来隐藏类的私有信息,必须有类的源码才可以为一个类添加 extension。所以无法为系统的类添加 extension

  • category 是在运行期决定的,category 是无法添加实例变量的,extension 是可以添加的。


三、Category 的本质

3.1 Category的基本使用



我们首先来看以下 category的基本使用:


// Person+Eat.h

#import "Person.h"

@interface Person (Eat)

- (void)eatBread;

+ (void)eatFruit;

@property (nonatomic, assign) int count;

@end

// Person+Eat.m

#import "Person+Eat.h"

@implementation Person (Eat)

- (void)eatBread {
NSLog(@"eatBread");
}

+ (void)eatFruit {
NSLog(@"eatFruit");
}

@end
复制代码


  • 创建了一个 Person 的分类,专门实现吃这个功能

  • 这个分类遵守了2个协议,分别为 NSCopyingNSCoding

  • 声明了2个方法,一个实例方法,一个类方法

  • 定义一个 count 属性


3.2 编译期的 Category


我们通过 clang 编译器来观察一下在编译期这些代码的本质是什么?


xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MyClass.m -o MyClass-arm64.cpp
复制代码

编译之后,我们可以发现 category 的本质是结构体 category_t,无论我们创建了多少个 category 最终都会生成 category_t 这个结构体,并且 category 中的方法、属性、协议都是存储在这个结构体里的。也就是说在编译期,分类中成员是不会和类合并在一起的


struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
复制代码


  • name:类的名字

  • cls:类

  • instanceMethodscategory 中所有给类添加的实例方法的列表

  • classMethodscategory 中所有给类添加的类方法的列表

  • protocolscategory 中实现的所有协议的列表

  • instancePropertiescategory 中添加的所有属性


category 的定义中可以看到我们可以 添加实例方法,添加类方法,可以实现协议,可以添加属性。


不可以添加实例变量


我们继续研究下面的编译后的代码:


static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatBread", "v16@0:8", (void *)_I_Person_Eat_eatBread}}
};

static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatFruit", "v16@0:8", (void *)_C_Person_Eat_eatFruit}}
};

static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};

static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"count","Ti,N"}}
};

static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{

"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat,
};
复制代码


  • 首先看一下 _OBJC_$_CATEGORY_Person_$_Eat 结构体变量中的值,就是分别对应 category_t 的成员,第1个成员就是类名,因为我们声明了实例方法,类方法,遵守了协议,定义了属性,所以我们的结构体变量中这些都会有值。

  • _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat 结构体表示实例方法列表,里面包含了 eatBread 实例方法

  • _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat 结构体包含了 eatFruit 类方法

  • _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat 结构体包含了 NSCopingNSCoding 协议

  • _OBJC_$_PROP_LIST_Person_$_Eat 结构体包含了 count 属性


3.3 运行期的 Category


在研究完编译时期的 category 后,我们进而研究运行时期的 category


objc-runtime-new.mm 的源码中,我们可以最终找到如何将 category 中的方法列表,属性列表,协议列表添加到类中。


static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)

{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}

/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/

constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];

uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();

for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if ( ) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}

protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}

if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}

rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
复制代码


  • rwe->methods.attachLists(mlists, mcount);

  • rwe->protocols.attachLists(protolists, protocount);

  • rwe->properties.attachLists(proplists, propcount);


以上三个函数就是把 category 中的方法、属性和协议列表添加到类中的函数。


继续查看 attchLists 函数的实现:


void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
复制代码


  • 在这段源码中,主要关注2个函数 memmovememcpy

  • memmove 函数的作用是移动内存,将之前的内存向后移动,将原来的方法列表往后移

  • memcpy 函数的作用是内存的拷贝,将 category 中的方法列表复制到上一步移出来的位置。


从上述源码中,可以发现 category 的方法并没有替换原来类已有的方法,如果 category 和原来类中都有某个同名方法,只不过 category 中的方法被放到了新方法列表的前面,在运行时查找方法的时候是按照顺序查找的,一旦找到该方法,就不会向下继续查找了,产生了 category 会覆盖原类方法的假象。



所以我们在 category 定义方法的时候都要加上前缀,以避免意外的重名把类本身的方法”覆盖“掉。




  • 如果多个 category 中存在同名的方法,运行时最终调用哪个方法是由编译器决定的,最后一个参与编译的方法将会先被调用

链接:https://juejin.cn/post/6950833332422705165

收起阅读 »

iOS 常见面试题总结及答案(2)

一.App启动过慢,你可能想到的因素有哪些?1.解析Info.plist   加载相关信息,例如如闪屏 沙箱建立、权限检查2.Mach-O加载 如果是胖二进制文件,寻找合适当前CPU类别的部分加载所有依赖的Mach-O...
继续阅读 »

一.App启动过慢,你可能想到的因素有哪些?

1.解析Info.plist  

 加载相关信息,例如如闪屏 沙箱建立、权限检查

2.Mach-O加载 

如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为attribute((constructor))的C函数
加载类扩展(Category)中的方法

3.程序执行

调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching

影响启动性能的因素

main()函数之前耗时的影响因素
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load越多,启动越慢
main()函数之后耗时的影响因素
执行main()函数的耗时
执行applicationWillFinishLaunching的耗时
rootViewController及其childViewController的加载、view及其subviews的加载

优化

纯代码方式而不是storyboard加载首页UI。
对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载。
对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。


二.单例的利弊

优点:
1:一个类只被实例化一次,提供了对唯一实例的受控访问。
2:节省系统资源
3:允许可变数目的实例。

缺点:
1:一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”。
2:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
3:滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。


三.TCP和UDP的区别于联系

TCP为传输控制层协议,为面向连接、可靠的、点到点的通信;
UDP为用户数据报协议,非连接的不可靠的点到多点的通信;
TCP侧重可靠传输,UDP侧重快速传输


四.TCP连接的三次握手

第一次握手:客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;

第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开 TCP 连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)



五.假如Controller太臃肿,如何优化?

1.将网络请求抽象到单独的类中

方便在基类中处理公共逻辑;
方便在基类中处理缓存逻辑,以及其它一些公共逻辑;
方便做对象的持久化。

2.将界面的封装抽象到专门的类中
构造专门的 UIView 的子类,来负责这些控件的拼装。这是最彻底和优雅的方式,不过稍微麻烦一些的是,你需要把这些控件的事件回调先接管,再都一一暴露回 Controller。

3.构造 ViewModel
借鉴MVVM。具体做法就是将 ViewController 给 View 传递数据这个过程,抽象成构造 ViewModel 的过程

4.专门构造存储类
专门来处理本地数据的存取。

5.整合常量


六.对程序性能的优化你有什么建议?

1.使用复用机制

2.尽可能设置 View 为不透明

3.避免臃肿的 XIB 文件

4.不要阻塞主线程

5.图片尺寸匹配 UIImageView

6.选择合适的容器

7.启用 GZIP 数据压缩

8.View 的复用和懒加载机制

9、缓存

服务器的响应信息(response)。图片。计算值。比如:UITableView 的 row heights。

10.关于图形绘制

11.处理 Memory Warnings

在 AppDelegate 中实现 - [AppDelegate applicationDidReceiveMemoryWarning:] 代理方法。
在 UIViewController 中重载 didReceiveMemoryWarning 方法。
监听 UIApplicationDidReceiveMemoryWarningNotification 通知。

12.复用高开销的对象

13.减少离屏渲染(设置圆角和阴影的时候可以选用绘制的方法)

14.优化 UITableView

通过正确的设置 reuseIdentifier 来重用 Cell。
尽量减少不必要的透明 View。
尽量避免渐变效果、图片拉伸和离屏渲染。
当不同的行的高度不一样时,尽量缓存它们的高度值。
如果 Cell 展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的 response。
使用 shadowPath 来设置阴影效果。
尽量减少 subview 的数量,对于 subview 较多并且样式多变的 Cell,可以考虑用异步绘制或重写 drawRect。
尽量优化 - [UITableView tableView:cellForRowAtIndexPath:] 方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。
选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
对于 rowHeight、sectionFooterHeight、sectionHeaderHeight 尽量使用常量。

15.选择合适的数据存储方式

在 iOS 中可以用来进行数据持有化的方案包括:
NSUserDefaults。只适合用来存小数据。
XML、JSON、Plist 等文件。JSON 和 XML 文件的差异在「选择正确的数据格式」已经说过了。
使用 NSCoding 来存档。NSCoding 同样是对文件进行读写,所以它也会面临必须加载整个文件才能继续的问题。
使用 SQLite 数据库。可以配合 FMDB 使用。数据的相对文件来说还是好处很多的,比如可以按需取数据、不用暴力查找等等。
使用 CoreData。也是数据库技术,跟 SQLite 的性能差异比较小。但是 CoreData 是一个对象图谱模型,显得更面向对象;SQLite 就是常规的 DBMS。

16.减少应用启动时间

快速启动应用对于用户来说可以留下很好的印象。尤其是第一次使用时。
保证应用快速启动的指导原则:
尽量将启动过程中的处理分拆成各个异步处理流,比如:网络请求、数据库访问、数据解析等等。
避免臃肿的 XIB 文件,因为它们会在你的主线程中进行加载。重申:Storyboard 没这个问题,放心使用。
注意:在测试程序启动性能的时候,最好用与 Xcode 断开连接的设备进行测试。因为 watchdog 在使用 Xcode 进行调试的时候是不会启动的。

17.使用 Autorelease Pool (内存释放池)

18.imageNamed 和 imageWithContentsOfFile


七.使用drawRect有什么影响?

drawRect方法依赖Core Graphics框架来进行自定义的绘制

缺点:它处理touch事件时每次按钮被点击后,都会用setNeedsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了

这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制


八.基于CTMediator的组件化方案,有哪些核心组成?

假如主APP调用某业务A,那么需要以下组成部分:

CTMediator类,该类提供了函数 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
这个函数可以根据targetName生成对象,根据actionName构造selector,然后可以利用performSelector:withObject:方法,在目标上执行动作。

业务A的实现代码,另外要加一个专门的类,用于执行Target Action
类的名字的格式:Target_%@,这里就是Target_A。
这个类里面的方法,名字都以Action_开头,需要传参数时,都统一以NSDictionary*的形式传入。
CTMediator类会创建Target类的对象,并在对象上执行方法。

业务A的CTMediator扩展
扩展里声明了所有A业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。
在扩展的实现里,对Target, Action需要通过硬编码进行指定。由于扩展的负责方和业务的负责方是相同的,所以这个不是问题。


九.为什么CTMediator方案优于基于Router的方案?

Router的缺点:

在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。

在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。

为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的

CTMediator的优点:

调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。

组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。

方便传递各种类型的参数。


十.内存的使用和优化的注意事项

重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用;

尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;

不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多;

选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。

延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。

数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。

处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。

避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要;

使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;

正确选择图片加载方式:UIImage加载方式


摘自作者:iOS猿_员
原贴链接:https://www.jianshu.com/p/4b4bd4e3feff

收起阅读 »

前端自测清单(前端八股文)

缘起这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。先从性能优化开始吧。性能优化大体可以分为两个,运行时优化加载时优化加载时优化网络优化dns寻址过程tcp的三次握手和四次挥手,以及为何要三次和为何要四次https的握手过程,以及对称加密和非...
继续阅读 »

/zi-ce-qing-dan/featured-image.jpg

缘起

这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。

先从性能优化开始吧。性能优化大体可以分为两个,

  • 运行时优化
  • 加载时优化

加载时优化

网络优化

  • dns寻址过程
  • tcp的三次握手和四次挥手,以及为何要三次和为何要四次
  • https的握手过程,以及对称加密和非对称加密的区别,什么是中间人劫持,ca证书包括哪些内容
  • http1.0,http1.1以及http2.0的区别,多路复用具体指的是什么,keep-alive具体如何体现
  • cdn的原理,cdn什么情况下会回源,cdn的适用场景
  • 浏览器缓存有哪几种,它们的区别是什么,什么时候发生缓存,如何决定缓存哪些文件
  • 了解过websocket么,解释一下websocket的作用

渲染优化

  • 关键渲染路径优化,什么是关键渲染路径,分别如何优化
  • 优化体积,webpack的分包策略,如何配置优化,如何提高构建速度,tree-shaking是什么
  • cssom 的优化,以及html解析过程中,遇到哪些tag会阻塞渲染
  • 雅虎军规说,css尽量放到head里,js放到下方,那么移动端适配的flexiblejs为何要放到css上方呢
  • 影响回流重绘的因素有哪些,如何避免回流,以及bfc是什么,bfc有什么特性,清除浮动的原理是什么

场景:如何优化首屏

除了上以及下面说到的,这里也是分两个层面,

  • 加载时优化
  • 运行时优化

加载

  • 首屏请求和非首屏请求拆分
  • 图片都应该使用懒加载的形式加载
  • 使用preload预加载技术,以及prefetch的dns预解析
  • 与首屏无关的代码可以加async甚至是defer等待网页加载完成后运行

运行

这里跟加载的异常耦合,另作分析吧。

运行时优化

  • 虚拟长列表渲染
  • 图片懒加载
  • 使用事件委托
  • react memo以及pureComponent
  • 使用SSR
  • 。。。

以及一些比较骚的操作,只能特定场景使用,

  • serviceWorker劫持页面
  • 利用worker

更新一波,性能优化之外的面试题,

底层

  • V8是如何实现GC的
  • JS的let,const,call stack,function context,global context。。。的区别
  • this的指向,箭头函数中this和function里的this有什么区别
  • 原型链是什么,继承呢,有几种继承方式,如何实现es6的class
  • eventloop是什么,浏览器的eventloop和nodejs的eventloop有什么区别,nexttick是什么
  • commonjs和AMD,CMD的区别,以及跟ES MODULE的区别
  • 说说require.cache
  • 了解过,洋葱模型没有,它是如何实现的
  • 说说nodejs中的流,也就是stream
  • 你用过ts,说说你常用的ts中的数据类型
  • js的数据类型,weakMap,weakSet和Map以及Set的区别是什么
  • 为何0.1+0.2 不等于0.3,如何解决这个问题
  • js的类型转换
  • 正则表达式
  • 对象循环引用会发生什么问题
  • 如何捕获异步的异常,你能说出几种方案

CSS相关

  • position有哪几种属性,它们的区别是什么
  • 如何实现垂直居中,移动端的呢
  • margin设置百分比,是依据谁的百分比,padding呢
  • 怪异盒模型和一般盒模型有什么区别
  • flex:1代表什么,flex-shrink和flex-grow有什么区别
  • background-size origin基准点在哪里
  • 移动端1px解决方案,以及为何会产生这个问题
  • 移动端高清屏图片的解决方案
  • 说说GPU加速

跨端

  • RN 实现原理
  • 小程序实现原理
  • webview跟h5有什么区别
  • RPC 是什么
  • JSBridge 原理是什么
  • 网页唤起app的原理是什么

服务端

  • oauth2了解过没有,sso呢
  • JWT 如何实现的

网络

除了之前提到的网络问题,当然还有很多,比如

  • 为何使用1x1的gif进行请求埋点
  • TCP 如何进行拥塞控制

安全

  • csrf是什么,防范措施是什么
  • xss如何防范

浏览器相关

  • 跨域是如何产生的,如何解决
  • 如何检查性能瓶颈
  • 打开页面白屏,如何定位问题,或者打开页面CPU100%,如何定位问题
  • jsonp是什么,为何能解决跨域,使用它可能会产生什么问题
  • base64会产生什么问题
  • event.target和event.currTarget有什么区别

框架相关

  • react和vue的区别
  • react的调度原理
  • setstate为何异步
  • key的作用是什么,为何说要使用唯一key,react的diff算法是如何实现的,vue的呢
  • react的事件系统是如何实现的
  • react hook是如何实现的
  • react的通信方式,hoc的使用场景
  • 听过闭包陷阱么,为何会出现这种现象,如何避免
  • vue的响应式原理
  • 为何vue3.x用的是proxy而不是object.defineProperties
  • vue是如何实现对数据的监听的,对数组呢
  • vue中的nexttick是如何实现的
  • fiber是什么,简单说说时间切片如何实现,为何vue不需要时间切片
  • webpack是如何实现的,HMR是如何实现的,可以写个简单的webpack么,webpack的执行流程是怎样的
  • koa源码实现,洋葱模型原理,get/post等这些方法如何set入koa里的,ctx.body为何能直接改变response的body
  • 你简历上写的了解过webpack源码,到哪种程度了(实话说没写koa简单。。

算法相关

  • js大整数加法
  • 双指针
  • 经典排序
  • 动态规划
  • 贪心算法
  • 回溯法
  • DFS
  • BFS
  • 链表操作
  • 线性求值
  • 预处理,前缀和

项目相关

  • 项目中遇到的最大问题是什么,如何解决的
  • nodejs作为中间层的作用是什么

场景题(机试)

  • 如何实现直播上的弹幕组件,要求不能重叠,仿照b站上的弹幕
  • 如何实现动态表单,仿照antd上的form组件
  • 实现一个promise(一般不会这样问)
  • 实现一个限制请求数量的方法
  • 如何实现一个大文件的上传
  • 实现一个eventEmitter
  • 实现一个new,call,bind,apply
  • 实现一个throttle,debound
  • 实现promise.then,finally,all
  • 实现继承,寄生组合继承,instanceof
  • 实现Generator,Aynsc

20.11.20 更新来了

  • React 生命周期,(分三个阶段进行回答,挂载阶段,更新阶段以及卸载阶段)
  • Vue 生命周期 以及其父子组件的生命周期调度顺序
  • 如果让你用强缓存或者协商缓存来缓存资源的话,你会如何使用
  • 作用域是什么,作用域链呢?(这题我想了下,不会利用语言去表达这个东西。。)

目前暂时想到这些,后来有想到的会补充上去。

原文链接:https://steinw.cc/zi-ce-qing-dan/


收起阅读 »

iOS 常见面试题总结及答案(1)

一.    Runloop和线程的关系?1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建2.runloop在第一次获取时创建,在线程结束时销毁.1.NSTimer在子线程开启一个定时器;控制定...
继续阅读 »

一.    Runloop和线程的关系?

1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建

2.runloop在第一次获取时创建,在线程结束时销毁.

runloop 的运行逻辑就是 do-while 循环下运用观察者模式(或者说是消息发送),根据7种状态的变化,处理事件输入源和定时器。如下图


runloop的应用:

1.NSTimer在子线程开启一个定时器;控制定时器在特定模式下执行

2.imageView的显示

3.performSelector

4.常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)

5.自动释放池

二.自动释放池什么时候释放?

第一次创建:启动runloop时候

最后一次销毁:runloop退出的时候

其他时候的创建和销毁:当runloop即将睡眠时销毁之前的释放池,重建一个新的

三.什么时候使用weak关键字,和assign的区别?

1.arc中有可能出现循环引用的地方,比如delegate属性

2.自定义IBOutlet空间属性一般也是使用weak

区别:weak表明一种非持有关系,必须用于oc对象;assign用于基本数据类型

weak修饰的指针默认是nil,如果用assign修饰对象,在对象被销毁时,会产生野指针,容易发生崩溃

四.objc中向一个nil对象发送消息将会发生什么?

在oc中向nil发送消息是完全有效的,只是在运行时不会有任何作用,如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil),如果向一个nil对象发送消息,首先寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误.

五.runtime如何实现weak变量的自动置nil

runtime对注册的类,会进行布局,对于weak对象会放入一个hash表中,用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil

六.runtime如何通过selector找到对应的IMP地址?

每一个类对象都有一个方法列表,方法列表中记录着方法名称.方法实现.参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现

七.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

1.不能向编译后得到的类中增加实例变量,可以向运行时创建的类中添加实例变量

原因: (1)因为编译后的类已经注册在runtime中,类结构中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同事runtime会调用class_setIvarLayout 或者class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量

(2)运行时创建的的类可以添加实例变量,调用class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上

八.kvo的实现原理?

当你观察一个对象时,一个新的类会被动态创建,这个类继承自该对象的原本的类,并重写了被观察属性的setter方法,重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值得更改,最后通过isa混写,把这个对象的isa指针(isa指针告诉runtime系统这个对象的类是什么)指向这个新创建的子类,对象就神奇的变成了新创建子类的实例

实现原理如下图:


九.谈谈你对kvc的理解

kvc可以通过key直接访问对象属性,或者给对象的属性赋值,这样可以在运行时动态访问或者改变对象的属性值

当调用setValue:属性值forKey:@"name"的代码时,底层执行机制如下:

1.程序优先调用set:属性值方法,代码通过setter方法完成设置,注意,这里的是指成员变量名,首字母大小写要符合kvc的命名规则

2.如果没有找到setName:方法,kvc机制会检查+(Bool)accessInstanceVariablesDiretly方法,有没有返回yes,默认是返回yes,如果重写了该方法切返回NO的话,那么这一步会直接执行setValue:forUnderfindKey:方法, 如果返回Yes,那么kvc机制会搜索该类里面有没有名为的成员变量,不论该变量是在类接口处定义,还是在类实现处定义,也无论用什么样的访问修饰符只要存在以命名的变量,kvc都可以对该变量赋值

3.如果该类即没有set方法,也没有成员变量,kvc机制会搜索_is的成员变量

4.和上面一样,如果该类即没有set:方法,也没有_和_is成员变量,kvc机制会继续搜索和is的成员变量,再给它赋值

5.如果以上列出的方法和成员变量都不存在,系统将执行setValue:forUnderfindKey:方法,默认是抛出异常.

十.Notification和KVO的区别

1.KVO提供一种机制,当指定的被观察对象属性被修改后,kvo会自动通知响应的观察者

2.通知:是一种广播机制,在事件发生的时候,通过通知中心对象,一个对象能够为所关心这个事件发生的对象发送消息,两者都是观察者模式,不同在于kvo是被观察者直接发送消息给观察者,是对象间的直接交互.通知则是两者都和通知中心对象交互,对象之间不知道彼此

3.本质区别,底层原理不一样,kvo基于runtime,通知则有个通知中心来进行通知

十一.如果让你设计一个通知中心,设计思路

1.创建通知中心单例类,并在里面有一个保存全局NSDictionary

2.对于注册通知的类,将注册通知名作为key,执行的方法和类,以及一些参数作为一个数组为值

3.发送通知可以调用通知中心,通过字典key(通知名)找到对应的类和方法进行调用传值

十二.atomic和nonatomic区别,以及作用?

atomic与nonatom的主要区别就是系统自动生成的getter/setter方法不一样 ,atomic系统自动生成的getter/setter方法会进行加锁操作,nonatomic系统自动生成的getter/setter方法不会进行加锁操作

atomic不是线程安全的

系统生成的getter/setter方法会进行加锁操作,注意:这个锁仅仅保证了getter和setter存取方法的线程安全.因为getter/setter方法有加锁的缘故,故在别的线程来读写这个属性之前,会先执行完当前操作

atomic可以保证多线程访问时候,对象是未被其他线程销毁(比如:如果当一个线程正在get或set时,又有另一个线程同时在进行release操作,可能会直接crash)

十三.说一下静态库和动态库之间的区别

静态库:以.a 和 .framework为文件后缀名。链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。

动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存 

静态库.a 和 framework区别.a 主要是二进制文件,不包含资源,需要自己添加头文件.framework 可以包含头文件+资源信息

十四.遇到过BAD_ACCESS的错误吗?你是怎样调试的?

BAD_ACCESS 报错属于内存访问错误,会导致程序崩溃,错误的原因是访问了野指针(悬挂指针)。

常规操作如下:

设置全局断点快速定位问题代码所在行。

开启僵尸对象诊断
Analyze分析
重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object。
Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer

十五.说一下iOS 中的APNS,远程推送原理?

Apple push Notification Service,简称 APNS,是苹果的远程消息推送,原理如下:
iOS 系统向APNS服务器请求手机端的deviceToken
App 接收到手机端的 deviceToken,然后传给 App 对应的服务器.
App 服务端需要发送推送消息时, 需要先通过 APNS 服务器
然后根据对应的 deviceToken 发送给对应的手机

十六.UITableView的优化

1.重用cell

2.缓存行高(在请求到数据的时候提前计算好行高,用字典缓存好高度)

3.加载网络图片,使用异步加载,并缓存,下载的图片根据显示大小切成合适大小的图,查看大图时再显示大图,服务端最好处理好大图和小图,延时加载,当滚动很快时避免频繁请求,可通过runloop设置defultMode状态下渲染请求

4.局部刷新,减少全局刷新

5.渲染,尽量不要使用透明图层,将cell的opaque值设为Yes,背景色和子View不要使用透明色,减少阴影渐变,圆角等

6.少用addSubview给cell动态添加子View,初始化时直接设置好,通过hidden控制显示隐藏,布局在初始化直接布局好,避免cell的重新布局

7.按需加载cell,滚动很快时,只加载范围内的cell,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后定制n行加载,按需加载,提高流畅性方法如下


8.遇到复杂界面,需要异步绘制,给自定义的cell添加draw方法,在方法中利用GCD异步绘制,或者直接重写drawRect方法,此外,绘制cell不建议使用UIView,建议使用CALayer,UIView的绘制是建立在CoreGraphic上的,使用的是cpu,CALayer使用的是core Animation,CPU.GPU通吃.由系统决定使用哪一个,view的绘制使用的自下向上的一层层的绘制,然后渲染layer处理的是Texure,利用GPU的Texure Cache和独立的浮点数计算单元加速纹理的处理,GPU不喜欢透明,所以绘图一定要弄成不透明,对于圆角和阴影截一个伪透明的小图绘制上去,在layer回调里一定只做绘图,不做计算

cell被重用时,内部绘制的内容并不会自动清除,因此需要调用setNeedsDisplay或者setNeedsDisplayLayInRect:方法

十七.离屏渲染

下面的情况或操作会引发离屏渲染:

1.为图层设置遮罩(layer.mask)
2.将图层的layer.masksToBounds/view.clipsToBounds属性设置为ture
3.将图层layer,allowsGroupOpacity属性设置为Yes和layer.opacity小于1.0
4.给图层设置阴影(layer.shadow)
5.为图层设置layer.shouldRasterize=Yes(光栅化)
6.具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层(圆角,抗锯齿)
7.使用CGContext在drawRect:方法中绘制大部分情况会导致离屏渲染,甚至是一个空的实现

优化方案

圆角优化:使用CAShapeLayer和UIBezierPath设置圆角;直接中间覆盖一张为圆形的透明图片
shadow优化:使用shadowPath指定layer阴影效果路径,优化性能
使用异步进行layer渲染(Facebook开源异步绘制框架AsncDisplayKit)
设置layer的opaque值为Yes,减少复杂图层合成
尽量使用不包含透明(alpha)通道的图片资源
尽量设置layer的大小为整型值

十八.UIView和CALayer区别

1.UIView可以响应事件,CALayer不可以,UIView继承自UIResponder,在UIResponder中定义了处理各种事件的事件传递接口。而CALayer直接继承NSObject,并没有相应的处理事件接口。
2.一个CALayer的frame是由它的anchorPoint(锚点),position,bounds,和transform共同决定,而一个view的frame只是简单的返回layer的frame,同样view的center和bounds也是返回layer的一些属性
3..UIView主要是对显示内容的管理,而CALayer主要是侧重显示内容的绘制。UIView是CALayer的CALayerDelegate。
4.每一个view内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的CALayer提供,两者都有树状层级结构,layer内部有subLayers,view内部有subviews。
5.两者最明显的区别是view可以接受并处理事件,而Layer不可以。View是Layer的代理Delegate。

十九.iOS应用程序生命周期

ios程序启动原理(过程):如下:


二十.view视图生命周期



























收起阅读 »

协程+Retrofit 让你的代码足够优雅

目标 简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。 如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。 一、引入依赖 协程和 Retrofit 的版本:dependen...
继续阅读 »

目标


简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。


如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。


一、引入依赖


协程和 Retrofit 的版本:

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.0.0'
}

二、使用Retrofit


创建一个 interface

interface GithubApi {
@GET("search/repositories?sort=stars")
suspend fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): RepoSearchResponse
}

和我们平时使用 Retrofit 有两点不一样:



  1. 需要在函数前加上 suspend 修饰符

  2. 直接使用我们需要返回的类型,不需要包上 Call<T> 或者 Observable<T>


RepoSearchResponse 是返回的数据:

data class RepoSearchResponse(
@SerializedName("total_count") val total: Int = 0,
@SerializedName("items") val items: List<Repo> = emptyList()
)

data class Repo(
@SerializedName("id") val id: Long,
@SerializedName("name") val name: String,
@SerializedName("full_name") val fullName: String,
@SerializedName("description") val description: String?,
@SerializedName("html_url") val url: String,
@SerializedName("stargazers_count") val stars: Int,
@SerializedName("forks_count") val forks: Int,
@SerializedName("language") val language: String?
)

之后的步骤就和我们平常使用 Retrofit 一致:



  1. 创建一个 OkHttpClient

  2. 创建一个 Retrofit

  3. 返回上面创建的接口


代码:

interface GithubApi {
//... 代码省略

companion object {
private const val BASE_URL = "https://api.github.com/"

fun createGithubApi(): GithubApi {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC

val client = OkHttpClient.Builder()
.addInterceptor(logger)
.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(),
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String
) {}
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String
) {}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
})
.hostnameVerifier(SSLSocketClient.getHostnameVerifier())
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GithubApi::class.java)
}
}
}

因为接口是 Https 请求,所以需要加上忽略 SSL 的验证,其他都一样了。


三、使用协程去请求


初始化 RecyclerView 的代码就不放了,比较简单:

class MainActivity : AppCompatActivity() {
val scope = MainScope()
private val mAdapter by lazy {
MainAdapter()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// ... 初始化RecyclerView
fetchData()
}

private fun fetchData(){
scope.launch {
try {
val result = GithubApi.createGithubApi().searchRepos("Android", 0, 20)
if(result != null && !result.items.isNullOrEmpty()){
mAdapter.submitList(result.items)
}
}catch (e: Exception){
e.printStackTrace()
}
}
}

override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}

协程中最方便的还是省去切线程的步骤,用同步代码处理耗时的异步网络请求


需要注意的是,由于没有使用 LifecycleKTX 的扩展库,所以协程作用域的生命周期得我们自己去释放,在上面的代码中,我是在 onCreate 方法中启动了一个协程,然后在 onDestroy 方法中去取消了正在执行的任务,以防内存泄漏。


总结


协程 + Retrofit 的方便之处在于:使用同步代码处理异步的网络请求,减去 Retrofit 中网络回调或者 RxJava + Retrofit 的请求回调


作者:九心_
链接:https://www.jianshu.com/p/dd3a9323b81a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Jetpack新成员,Paging3从吐槽到真香

各位小伙伴们大家早上好。 随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。 现在,关于App Start和Hilt的文章我都...
继续阅读 »

各位小伙伴们大家早上好。


随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。


现在,关于App Start和Hilt的文章我都已经写完了,请参考 Jetpack新成员,App Startup一篇就懂 和 Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 。


那么本篇文章,我们要学习的自然就是Paging 3了。


Paging 3简介


Paging是Google推出的一个应用于Android平台的分页加载库。


事实上,Paging并不是现在才刚刚推出的,而是之前就已经推出过两个版本了。


但Paging 3和前面两个版本的变化非常大,甚至可以说是完全不同的东西了。所以即使你之前没有学习过Paging的用法也没有关系,把Paging 3当成是一个全新的库去学习就可以了。


我相信一定会有很多朋友在学习Paging 3的时候会产生和我相同的想法:本身Android上的分页功能并不难实现,即使没有Paging库我们也完全做得出来,但为什么Paging 3要把一个本来还算简单的功能设计得如此复杂呢?


是的,Paging 3很复杂,至少在你还不了解它的情况下就是如此。我在第一次学习Paging 3的时候就直接被劝退了,心想着何必用这玩意委屈自己呢,自己写分页功能又不是做不出来。


后来本着拥抱新技术的态度,我又去学习了一次Paging 3,这次算是把它基本掌握了,并且还在我的新开源项目 Glance 当中应用了Paging 3的技术。


如果现在再让我来评价一下Paging 3,那么我大概是经历了一个由吐槽到真香的过程。理解了Paging 3之后,你会发现它提供了一套非常合理的分页架构,我们只需要按照它提供的架构去编写业务逻辑,就可以轻松实现分页功能。我希望大家在看完这篇文章之后,也能觉得Paging 3香起来。


不过,本篇文章我不能保证它的易懂性。虽然很多朋友都觉得我写的文章简单易懂,但Paging 3的复杂性在于它关联了太多其他的知识,如协程、Flow、MVVM、RecyclerView、DiffUtil等等,如果你不能将相关联的这些知识都有所了解,那么想要掌握Paging 3就会更有难度。


另外,由于Paging 3是Google基于Kotlin协程全新重写的一个库,所以它主要是应用于Kotlin语言(Java也能用,但是会更加复杂),并且以后这样的库会越来越多,比如Jetpack Compose等等。如果你对于Kotlin还不太了解的话,可以去参郭霖的新书《第一行代码 Android 第3版》。


上手Paging 3


经过我自己的总结,我发现如果零散去介绍一些Paging 3的知识点是很难能掌握得了这个库的。最好的学习方式就是直接上手,用Paging 3去做一个项目,项目做完了,你也基本就掌握了。本篇文章中我们就会采用这种方式来学习。


另外,我相信大家之前应该都做过分页功能,正如我所说,这个功能并不难实现。但是现在,请你完全忘掉过去你所熟知的分页方案,因为它不仅对理解Paging 3没有帮助,反而在很大程度上会影响你对Paging 3的理解。


是的,不要想着去监听列表滑动事件,滑动到底部的时候发起一个网络请求加载下一页数据。Paging 3完全不是这么用的,如果你还保留着这种过去的实现思路,在学习Paging 3的时候会很受阻。


那么现在就让我们开始吧。


首先新建一个Android项目,这里我给它起名为Paging3Sample。


接下来,我们在build.gradle的dependencies当中添加必要的依赖库:

dependencies {
...
implementation 'androidx.paging:paging-runtime:3.0.0-beta01'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

注意虽然我刚才说,Paging 3是要和很多其他关联库结合到一起工作的,但是我们并不需要将这些关联库一一手动引入,引入了Paging 3之后,所有的关联库都会被自动下载下来。


另外这里还引入了Retrofit的库,因为待会我们会从网络上请求数据,并通过Paging 3进行分页展示。


那么在正式开始涉及Paging 3的用法之前,让我们先来把网络相关的代码搭建好,方便为Paging 3提供分页数据。


这里我准备采用GitHub的公开API来作为我们这个项目的数据源,请注意GitHub在国内虽然一般都是可以访问的,但有时接口并不稳定,如果你无法正常请求到数据的话,请自行科学上网。


我们可以尝试在浏览器中请求如下接口地址:

https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1

这个接口表示,会返回GitHub上所有Android相关的开源库,以Star数量排序,每页返回5条数据,当前请求的是第一页。


服务器响应的数据如下,为了方便阅读,我对响应数据进行了简化:

{
"items": [
{
"id": 31792824,
"name": "flutter",
"description": "Flutter makes it easy and fast to build beautiful apps for mobile and beyond.",
"stargazers_count": 112819,
},
{
"id": 14098069,
"name": "free-programming-books-zh_CN",
"description": ":books: 免费的计算机编程类中文书籍,欢迎投稿",
"stargazers_count": 76056,
},
{
"id": 111583593,
"name": "scrcpy",
"description": "Display and control your Android device",
"stargazers_count": 44713,
},
{
"id": 12256376,
"name": "ionic-framework",
"description": "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.",
"stargazers_count": 43041,
},
{
"id": 55076063,
"name": "Awesome-Hacking",
"description": "A collection of various awesome lists for hackers, pentesters and security researchers",
"stargazers_count": 42876,
}
]
}

简化后的数据格式还是非常好理解的,items数组中记录了第一页包含了哪些库,其中name表示该库的名字,description表示该库的描述,stargazers_count表示该库的Star数量。


那么下面我们就根据这个接口来编写网络相关的代码吧,由于这部分都是属于Retrofit的用法,我会介绍的比较简略。


首先根据服务器响应的Json格式定义对应的实体类,新建一个Repo类,代码如下所示:

data class Repo(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("description") val description: String?,
@SerializedName("stargazers_count") val starCount: Int
)

然后定义一个RepoResponse类,以集合的形式包裹Repo类:

class RepoResponse(
@SerializedName("items") val items: List<Repo> = emptyList()
)

接下来定义一个GitHubService用于提供网络请求接口,如下所示:

interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse

companion object {
private const val BASE_URL = "https://api.github.com/"

fun create(): GitHubService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GitHubService::class.java)
}
}

}

这些都是Retrofit的标准用法,现在当调用searchRepos()函数时,Retrofit就会自动帮我们向GitHub的服务器接口发起一条网络请求,并将响应的数据解析到RepoResponse对象当中。


好了,现在网络相关的代码都已经准备好了,下面我们就开始使用Paging 3来实现分页加载功能。


Paging 3有几个非常关键的核心组件,我们需要分别在这几个核心组件中按部就班地实现分页逻辑。


首先最重要的组件就是PagingSource,我们需要自定义一个子类去继承PagingSource,然后重写load()函数,并在这里提供对应当前页数的数据。


新建一个RepoPagingSource继承自PagingSource,代码如下所示:

class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
return try {
val page = params.key ?: 1 // set page 1 as default
val pageSize = params.loadSize
val repoResponse = gitHubService.searchRepos(page, pageSize)
val repoItems = repoResponse.items
val prevKey = if (page > 1) page - 1 else null
val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
LoadResult.Page(repoItems, prevKey, nextKey)
} catch (e: Exception) {
LoadResult.Error(e)
}
}

override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null

}

这段代码并不长,但却需要好好解释一下。


在继承PagingSource时需要声明两个泛型类型,第一个类型表示页数的数据类型,我们没有特殊需求,所以直接用整型就可以了。第二个类型表示每一项数据(注意不是每一页)所对应的对象类型,这里使用刚才定义的Repo。


然后在load()函数当中,先通过params参数得到key,这个key就是代表着当前的页数。注意key是可能为null的,如果为null的话,我们就默认将当前页数设置为第一页。另外还可以通过params参数得到loadSize,表示每一页包含多少条数据,这个数据的大小我们可以在稍后设置。


接下来调用刚才在GitHubService中定义的searchRepos()接口,并把page和pageSize传入,从服务器获取当前页所对应的数据。


最后需要调用LoadResult.Page()函数,构建一个LoadResult对象并返回。注意LoadResult.Page()函数接收3个参数,第一个参数传入从响应数据解析出来的Repo列表即可,第二和第三个参数分别对应着上一页和下一页的页数。针对于上一页和下一页,我们还额外做了个判断,如果当前页已经是第一页或最后一页,那么它的上一页或下一页就为null。


这样load()函数的作用就已经解释完了,可能你会发现,上述代码还重写了一个getRefreshKey()函数。这个函数是Paging 3.0.0-beta01版本新增的,以前的alpha版中并没有。它是属于Paging 3比较高级的用法,我们本篇文章涉及不到,所以直接返回null就可以了。


PagingSource相关的逻辑编写完成之后,接下来需要创建一个Repository类。这是MVVM架构的一个重要组件,还不了解的朋友可以去参考《第一行代码 Android 第3版》第15章的内容。

object Repository {

private const val PAGE_SIZE = 50

private val gitHubService = GitHubService.create()

fun getPagingData(): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(PAGE_SIZE),
pagingSourceFactory = { RepoPagingSource(gitHubService) }
).flow
}

}

这段代码虽然很短,但是却不易理解,因为用到了协程的Flow。我无法在这里展开解释Flow是什么,你可以简单将它理解成协程中对标RxJava的一项技术。


当然这里也没有用到什么复杂的Flow技术,正如你所见,上面的代码很简短,相比于理解,这更多是一种固定的写法。


我们定义了一个getPagingData()函数,这个函数的返回值是Flow<PagingData<Repo>>,注意除了Repo部分是可以改的,其他部分都是固定的。


在getPagingData()函数当中,这里创建了一个Pager对象,并调用.flow将它转换成一个Flow对象。在创建Pager对象的时候,我们指定了PAGE_SIZE,也就是每页所包含的数据量。又指定了pagingSourceFactory,并将我们自定义的RepoPagingSource传入,这样Paging 3就会用它来作为用于分页的数据源了。


将Repository编写完成之后,我们还需要再定义一个ViewModel,因为Activity是不可以直接和Repository交互的,要借助ViewModel才可以。新建一个MainViewModel类,代码如下所示:

class MainViewModel : ViewModel() {

fun getPagingData(): Flow<PagingData<Repo>> {
return Repository.getPagingData().cachedIn(viewModelScope)
}

}

代码很简单,就是调用了Repository中定义的getPagingData()函数而已。但是这里又额外调用了一个cachedIn()函数,这是用于将服务器返回的数据在viewModelScope这个作用域内进行缓存,假如手机横竖屏发生了旋转导致Activity重新创建,Paging 3就可以直接读取缓存中的数据,而不用重新发起网络请求了。


写到这里,我们的这个项目已经完成了一大半了,接下来开始进行界面展示相关的工作。


由于Paging 3是必须和RecyclerView结合使用的,下面我们定义一个RecyclerView的子项布局。新建repo_item.xml,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:orientation="vertical">

<TextView
android:id="@+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#5194fd"
android:textSize="20sp"
android:textStyle="bold" />

<TextView
android:id="@+id/description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:maxLines="10"
android:ellipsize="end" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="end"
tools:ignore="UseCompoundDrawables">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="5dp"
android:src="@drawable/ic_star"
tools:ignore="ContentDescription" />

<TextView
android:id="@+id/star_count_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />

</LinearLayout>

</LinearLayout>

这个布局中使用到了一个图片资源,可以到本项目的源码中去获取,源码地址见文章最底部。


接下来定义RecyclerView的适配器,但是注意,这个适配器也比较特殊,必须继承自PagingDataAdapter,代码如下所示:

class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.ViewHolder>(COMPARATOR) {

companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem == newItem
}
}
}

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name_text)
val description: TextView = itemView.findViewById(R.id.description_text)
val starCount: TextView = itemView.findViewById(R.id.star_count_text)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val repo = getItem(position)
if (repo != null) {
holder.name.text = repo.name
holder.description.text = repo.description
holder.starCount.text = repo.starCount.toString()
}
}

}

相比于一个传统的RecyclerView Adapter,这里最特殊的地方就是要提供一个COMPARATOR。因为Paging 3在内部会使用DiffUtil来管理数据变化,所以这个COMPARATOR是必须的。如果你以前用过DiffUtil的话,对此应该不会陌生。


除此之外,我们并不需要传递数据源给到父类,因为数据源是由Paging 3在内部自己管理的。同时也不需要重写getItemCount()函数了,原因也是相同的,有多少条数据Paging 3自己就能够知道。


其他部分就和普通的RecyclerView Adapter没什么两样了,相信大家都能够看得明白。


接下来就差最后一步了,让我们把所有的一切都集成到Activity当中。


修改activity_main.xml布局,在里面定义一个RecyclerView和一个ProgressBar:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

</FrameLayout>

然后修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {

private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }

private val repoAdapter = RepoAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = repoAdapter
lifecycleScope.launch {
viewModel.getPagingData().collect { pagingData ->
repoAdapter.submitData(pagingData)
}
}
repoAdapter.addLoadStateListener {
when (it.refresh) {
is LoadState.NotLoading -> {
progressBar.visibility = View.INVISIBLE
recyclerView.visibility = View.VISIBLE
}
is LoadState.Loading -> {
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.INVISIBLE
}
is LoadState.Error -> {
val state = it.refresh as LoadState.Error
progressBar.visibility = View.INVISIBLE
Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
}
}
}
}

}

这里最重要的一段代码就是调用了RepoAdapter的submitData()函数。这个函数是触发Paging 3分页功能的核心,调用这个函数之后,Paging 3就开始工作了。


submitData()接收一个PagingData参数,这个参数我们需要调用ViewModel中返回的Flow对象的collect()函数才能获取到,collect()函数有点类似于Rxjava中的subscribe()函数,总之就是订阅了之后,消息就会源源不断往这里传。


不过由于collect()函数是一个挂起函数,只有在协程作用域中才能调用它,因此这里又调用了lifecycleScope.launch()函数来启动一个协程。


其他地方应该就没什么需要解释的了,都是一些传统RecyclerView的用法,相信大家都能看得懂。


好了,这样我们就把整个项目完成了,在正式运行项目之前,别忘了在你的AndroidManifest.xml文件中添加网络权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.paging3sample">

<uses-permission android:name="android.permission.INTERNET" />
...

</manifest>



可以看到,GitHub上Android相关的开源库已经成功显示出来了。并且你可以不断往下滑,Paging 3会自动加载更多的数据,仿佛让你永远也滑不到头一样。


如次一来,使用Paging 3来进行分页加载的效果也就成功完成了。


总结一下,相比于传统的分页实现方案,Paging 3将一些琐碎的细节进行了隐藏,比如你不需要监听列表的滑动事件,也不需要知道知道何时应该加载下一页的数据,这些都被Paging 3封装掉了。我们只需要按照Paging 3搭建好的框架去编写逻辑实现,告诉Paging 3如何去加载数据,其他的事情Paging 3都会帮我们自动完成。


在底部显示加载状态


根据Paging 3的设计,其实我们理论上是不应该在底部看到加载状态的。因为Paging 3会在列表还远没有滑动到底部的时候就提前加载更多的数据(这是默认属性,可配置),从而产生一种好像永远滑不到头的感觉。


然而凡事总有意外,比如说当前的网速不太好,虽然Paging 3会提前加载下一页的数据,但是当滑动到列表底部的时候,服务器响应的数据可能还没有返回,这个时候就应该在底部显示一个正在加载的状态。


另外,如果网络条件非常糟糕,还可能会出现加载失败的情况,此时应该在列表底部显示一个重试按钮。


那么接下来我们就来实现这个功能,从而让项目变得更加完善。


创建一个footer_item.xml布局,用于显示加载进度条和重试按钮:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">

<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

<Button
android:id="@+id/retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Retry" />

</FrameLayout>

然后创建一个FooterAdapter来作为RecyclerView的底部适配器,注意它必须继承自LoadStateAdapter,如下所示:

class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)
val retryButton: Button = itemView.findViewById(R.id.retry_button)
}

override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_item, parent, false)
val holder = ViewHolder(view)
holder.retryButton.setOnClickListener {
retry()
}
return holder
}

override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
holder.progressBar.isVisible = loadState is LoadState.Loading
holder.retryButton.isVisible = loadState is LoadState.Error
}

}

这仍然是一个非常简单的Adapter,需要注意的地方大概只有两点。


第一点,我们使用Kotlin的高阶函数来给重试按钮注册点击事件,这样当点击重试按钮时,构造函数中传入的函数类型参数就会被回调,我们待会将在那里加入重试逻辑。


第二点,在onBindViewHolder()中会根据LoadState的状态来决定如何显示底部界面,如果是正在加载中那么就显示加载进度条,如果是加载失败那么就显示重试按钮。


最后,修改MainActivity中的代码,将FooterAdapter集成到RepoAdapter当中:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })
...
}

}

代码非常简单,只需要改动一行,调用RepoAdapter的withLoadStateFooter()函数即可将FooterAdapter集成到RepoAdapter当中。


另外注意这里使用Lambda表达式来作为传递给FooterAdapter的函数类型参数,在Lambda表示式中,调用RepoAdapter的retry()函数即可重新加载。


这样我们就把底部显示加载状态的功能完成了,现在来测试一下吧,效果如下图所示。




可以看到,首先我在设备上开启了飞行模式,这样当滑动到列表底部时就会显示重试按钮。


然后把飞行模式关闭,并点击重试按钮,这样加载进度条就会显示出来,并且成功加载出新的数据了。


最后


本文到这里就结束了。


不得不说,我在文章中讲解的这些知识点仍然只是Paging 3的基本用法,还有许多高级用法文中并没有涵盖。当然,这些基本用法也是最最常用的用法,所以如果你并不打算成为Paging 3大师,掌握文中的这些知识点就已经足够应对日常的开发工作了。


如果你还想要进一步进阶学习Paging 3,可以参考Google官方的Codelab项目,地址是:

https://developer.android.com/codelabs/android-paging


我们刚才一起编写的Paging3Sample项目其实就是从Google官方的Codelab项目演化而来的,我根据自己的理解重写了这个项目并进行了一定的简化。直接学习原版项目,你将能学到更多的知识。


最后,如果你需要获取Paging3Sample项目的源码,请访问以下地址:

https://github.com/guolindev/Paging3Sample

作者:飞鱼_9d08
链接:https://www.jianshu.com/p/588562fbd19d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 常见的错误日志及相应的解决方案总结

之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步) android.view.WindowMana...
继续阅读 »

之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步)


android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

at android.view.ViewRootImpl.setView(ViewRootImpl.java:635)
at android.view.ColorViewRootImpl.setView(ColorViewRootImpl.java:60)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:321)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:86)
at android.widget.PopupWindow.invokePopup(PopupWindow.java:1262)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1110)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1069)

以上bug出现的原因是因为PopupWindow需要依附在一个创建好的Activity上,那么出现这个异常就说明此时你的Activity还没有创建好,出现这种情况,很可能是在onCreate()或者是onStart()中调用导致的。

下面有两种方法可以解决这个问题:



方法一:重载Activity的onWindowFocusChanged方法,然后在里面实现相应的逻辑如下:

public void onWindowFocusChanged(boolean hasFocus) {  
super.onWindowFocusChanged(hasFocus);
if(hasFocus) {
//执行PopupWindow相应的操作
}
}

下面给大家看下这个方法的源码,有兴趣的小伙伴可以看看

 /**
* Called when the current {@link Window} of the activity gains or loses
* focus. This is the best indicator of whether this activity is visible
* to the user. The default implementation clears the key tracking
* state, so should always be called.
*
* <p>Note that this provides information about global focus state, which
* is managed independently of activity lifecycles. As such, while focus
* changes will generally have some relation to lifecycle changes (an
* activity that is stopped will not generally get window focus), you
* should not rely on any particular order between the callbacks here and
* those in the other lifecycle methods such as {@link #onResume}.
*
* <p>As a general rule, however, a resumed activity will have window
* focus... unless it has displayed other dialogs or popups that take
* input focus, in which case the activity itself will not have focus
* when the other windows have it. Likewise, the system may display
* system-level windows (such as the status bar notification panel or
* a system alert) which will temporarily take window input focus without
* pausing the foreground activity.
*
* @param hasFocus Whether the window of this activity has focus.
*
* @see #hasWindowFocus()
* @see #onResume
* @see View#onWindowFocusChanged(boolean)
*/
public void onWindowFocusChanged(boolean hasFocus) {
}


方法二:上面的那种方法是需要实现Activity的一个方法并在方法中做操作,一般我们在项目中会在一些逻辑里面showPopupWindow或者其他的,那这样就会影响一些,然后我们就针对这个源码,追溯一下会发现另外一个方法:hasWindowFocus

/**
* Returns true if this activity's <em>main</em> window currently has window focus.
* Note that this is not the same as the view itself having focus.
*
* @return True if this activity's main window currently has window focus.
*
* @see #onWindowAttributesChanged(android.view.WindowManager.LayoutParams)
*/
public boolean hasWindowFocus() {
Window w = getWindow();
if (w != null) {
View d = w.getDecorView();
if (d != null) {
return d.hasWindowFocus();
}
}
return false;
}

查看上面的源码,我们会发现,我们可以直接使用hasWindowFocus来判断当前的Activity有没有创建好,再去做其他操作;以上就是这个错误日志相应的解决方案,如果还有其他的希望大家补充。


java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity


这个问题是在使用Glide的时候生产上面爆出来的,如果遇到其他相似的错误也可以试一下,以下有两种解决方案:



方法一:参考博文

在使用Glide的地方加上这个判断;Util是系统自带的;

if(Util.isOnMainThread()) {
Glide.with(AppUtil.getContext()).load``(R.mipmap.iclunch).error(R.mipmap.cuowu).into(imageView);
}

在使用的Glide的界面的生命周期onDestroy中添加如下代码:

@Override
protected void onDestroy() {
super.onDestroy();
if(Util.isOnMainThread()) {
Glide.with(this).pauseRequests();
}
}

上面Destroy中with(this),改成with(AppUtil.getContext());

不然会报: java.lang.IllegalStateException: Activity has been destroyed

扩展:

Glide.with(AppUtil.getContext()).resumeRequests()和 Glide.with(AppUtil.getContext()).pauseRequests()的区别:

1.当列表在滑动的时候,调用pauseRequests()取消请求;

2.滑动停止时,调用resumeRequests()恢复请求;

另外Glide.clear():当你想清除掉所有的图片加载请求时,这个方法可以用到。

ListPreloader:如果你想让列表预加载的话,可以试试这个类。

请记住一句话:不要再非主线程里面使用Glide加载图片,如果真的使用了,请把context参数换成getApplicationContext;



方法二:使用Activity提供的isFinishing和isDestroyed方法,来判断当前的Activity是不是已经销毁了或者说正在finishing,下面贴出来相应的源码:

/**
* Check to see whether this activity is in the process of finishing,
* either because you called {@link #finish} on it or someone else
* has requested that it finished. This is often used in
* {@link #onPause} to determine whether the activity is simply pausing or
* completely finishing.
*
* @return If the activity is finishing, returns true; else returns false.
*
* @see #finish
*/
public boolean isFinishing() {
return mFinished;
}

/**
* Returns true if the final {@link #onDestroy()} call has been made
* on the Activity, so this instance is now dead.
*/
public boolean isDestroyed() {
return mDestroyed;
}

附上项目相应的源码,希望有所帮助:

final WeakReference<ImageView> imgBankLogoWeakReference = new WeakReference<>(imgBankLogo);
final WeakReference<ImageView> imgBankBgWeakReference = new WeakReference<>(imgBankBg);
ImageView imgBankLogoTarget = imgBankLogoWeakReference.get();
ImageView imgBankBgTarget = imgBankBgWeakReference.get();
if (imgBankLogoTarget != null && imgBankBgTarget != null) {
if (!(isFinishing() || isDestroyed())) {
Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankLogo())
.centerCrop().into(imgBankLogoTarget);
Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankBg())
.centerCrop().into(imgBankBgTarget);
}
}

java.lang.IllegalStateException: Could not find a method OnButtonClick(View) in the activity class android.view.ContextThemeWrapper for onClick handler on view class android.widget.ImageView with id 'img_apply_result'
at android.view.View$1.onClick(View.java:4061)
at android.view.View.performClick(View.java:4848)
at android.view.View$PerformClick.run(View.java:20262)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5714)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:984)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
Caused by: java.lang.NoSuchMethodException: OnButtonClick [class android.view.View]
at java.lang.Class.getMethod(Class.java:664)
at java.lang.Class.getMethod(Class.java:643)
at android.view.View$1.onClick(View.java:4054)


以上的错误日志出现的有点low,但是呢有时候部分人还是容易忽略:我们一般在Activity或fragment等Java代码中使用资源文件时,例如:在Java代码中对一个Imageview附一张图片,我们不能img.setImageResource(图片相应的资源ID);需要img.setImageResource(context.getResources().getDrawable(图片相应的资源ID));需要先获取文件资源,再去拿图片,但是刚刚写的那个方法现在已经过时了,下面我贴出Google官方给出的最新的方法img.setImageDrawable(ContextCompat.getDrawable(context, imgResId));其实setImageDrawable是最省内存高效的,如果担心图片过大或者图片过多影响内存和加载效率,可以自己解析图片然后通过调用setImageDrawable方法进行设置


java.lang.NoClassDefFoundError: android.app.AppOpsManager


Appops是Application Operations的简称,是关于应用权限管理的一套方案,但这里的应用指的是系统应用,这些API不对第三方应用开放。Appops的两个重要组成部分是AppOpsManager和AppOpsService,它们是典型的客户端和服务端设计,通过Binder跨进程调用。AppOpsManager提供标准的API供APP调用,但google有明确说明,大部分只针对系统应用。AppOpsService是做最终检查的系统服务,它的注册名字是appops, 应用可以类似于

mAppOps=(AppOpsManager)getContext().getSystemService(Context.APP_OPS_SERVICE);的方式来获取这个服务。


解决方法:这个api是在19新加入的,所以要注意加个判断,参考项目代码如下:



判断是否开启通知权限(解释比较好的博客推荐

private boolean isNotificationEnabled(Context context) {

String CHECK_OP_NO_THROW = "checkOpNoThrow";
String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
if (Build.VERSION.SDK_INT < 19) {
return true;
}
try {
AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
ApplicationInfo appInfo = context.getApplicationInfo();
String pkg = context.getApplicationContext().getPackageName();
int uid = appInfo.uid;
Class appOpsClass = null;
/* Context.APP_OPS_MANAGER */
appOpsClass = Class.forName(AppOpsManager.class.getName());
Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE,
String.class);
Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);

int value = (Integer) opPostNotificationValue.get(Integer.class);
return ((Integer) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED);

} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoClassDefFoundError e) {
e.printStackTrace();
}
return false;
}

java.lang.SecurityException: getDeviceId: Neither user 10185 nor current process has android.permission.READ_PHONE_STATE.


这里的getDeviceId可能是获取系统状态或内容的操作,需要授予android.permission.READ_PHONE_STATE 权限,首先我们来看一下危险权限组



我们会发现android.permission.READ_PHONE_STATE 这个权限在PHONE组里面,在Android M版本及以后,当你的应用运行在Android6.0系统上如果设置targetSdkVersion小于23的时候,它也会默认采用以前的权限管理机制,当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,它会采用新的这套权限管理机制。相关动态权限爬坑这块可以看一下之前的博文(传送门)

,当你配置了targetSdkVersion>=23时,默认第一次安装会打开android.permission.READ_PHONE_STATE这个权限,部分手机亲测,那样依旧可以获取getDeviceId,但这个权限是可见的,用户在后续是可以关闭的。当用户关闭了这个权限,下次进来会动态弹出授权页面提醒用户授权,如果用户依旧关闭权限将获取不到DeviceId。但是国产手机的各种自定义导致部分手机会出现动态权限返回0,(PS:当用户禁止了权限,返回回调还是为已授权,例如:OPPO meizu等兼容),这样就尴尬了,如果我们拿到用户已经授权(但实际上是禁止的)就去调用

TelephonyManager tm = (TelephonyManager) getApplicationContext()
.getSystemService(Context.TELEPHONY_SERVICE);
_clientInfo.setDeviceId(tm.getDeviceId());

就会闪退,目前这边处理的思路为:第一次如果拿到就放在SharedPreferences里面存起来,当下次用户再次关闭权限也不用担心报错;


java.util.ConcurrentModificationExceptionat java.util.ArrayList$ArrayListIterator.next(ArrayList.java:578)
at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:637)
at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:624)
at com.google.gson.JsonSerializationVisitor.findAndInvokeCustomSerializer(JsonSerializationVisitor.java:184)
at com.google.gson.JsonSerializationVisitor.visitUsingCustomHandler(JsonSerializationVisitor.java:160)
at com.google.gson.ObjectNavigator.accept(ObjectNavigator.java:101)
at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:62)
at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:53)
at com.google.gson.Gson.toJsonTree(Gson.java:220)
at com.google.gson.Gson.toJson(Gson.java:260)
at com.google.gson.Gson.toJson(Gson.java:240)



在ArrayList.addAll()中对传进来的参数没有做null判断,于是,在调用collection.toArray()函数的时候就抛异常了,activity就崩溃了


在使用ArrayList.addAll()的时候一定要注意传入的参数会不会出现为null的情况,如果有,那么我们可以做以下判断

if (collection!= null)
mInfoList.addAll(Collection<? extends E> collection);

如果为null,就不执行下面的了,我们也不能确保是不是存在null的情况,所以为了确保不会出错,在前面加个判断是一个有经验的程序员该做的。以上错误日志的原因,可以看下源码大家就可以理解了:(这个问题虽小但容易忽略,希望各位注意)

/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the
* specified collection's Iterator. The behavior of this operation is
* undefined if the specified collection is modified while the operation
* is in progress. (This implies that the behavior of this call is
* undefined if the specified collection is this list, and this
* list is nonempty.)
*
* @param c collection containing elements to be added to this list
* @return <tt>true</tt> if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

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


以上错误日志出现的背景是调用系统自带的浏览器出现的,原因是因为部分手机设备商更改Android原生自带的com.android.browser/com.android.browser.BrowserActivity自己弄了一个其他的,例如,生产就出现一款手机 HTC 802t,这款手机自带浏览器的代码包名为:com.htc.sense.browser,看到这块是不是想吐槽一下,所以说如果直接写以下代码,就会出现以上错误日志:

 Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
intent.setData(contentUri);
intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));


解决方案(PS:获取系统安装的所有的浏览器应用 过滤):

Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
intent.setData(contentUri);
// HTC com.htc.sense.browser
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);//通过查询,获得所有ResolveInfo对象.
for (ResolveInfo resolveInfo : resolveInfos) {
browsers.add(resolveInfo.activityInfo.packageName);
System.out.println(resolveInfo.activityInfo.packageName);
}
if (browsers.contains("com.android.browser")) {
intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));
}
context.startActivity(intent);

android.os.FileUriExposedException/NullPointerException: Attempt to invoke virtual method 'java.lang.String android.net.Uri.getPath()' on a null object reference:


上述错误日志是Android 7.0应用间共享文件(FileProvider)兼容的问题,后续会出一篇博文来讲解:

下面提供代码:

/**
* 适配7.0及以上
*
* @param context
* @param file
* @return
*/
private static Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(context.getApplicationContext(), "xxx.fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}

AndroidManifest中配置provider:

<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.crfchina.market.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>

下面是很久之前的备忘的,也贴出来给大家分享一下。可能涉及到其他博文的内容,如有发现,麻烦私信,我后续加上 ……


android java.net.UnknownHostException: Unable to resolve host "...": No address associated 错误



解决方法:




  • (1)手机3G或者WIFI没有开启



  • (2).Manifest文件没有标明网络访问权限

    如果确认网络已经正常连接并且还是出这种错误的话,那么请看下你的Manifest文件是否标明应用需要网络访问权限,如果没标明的话,也访问不了网络,也会造成这种情况的.


    //网络访问权限


    <uses-permission android:name="android.permission.INTERNET" />




java.lang.NullPointerException: missing IConnectivityManager

at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:52)
at android.net.ConnectivityManager.<init>(ConnectivityManager.java:919)
at android.app.ContextImpl$11.createService(ContextImpl.java:387)
at android.app.ContextImpl$ServiceFetcher.getService(ContextImpl.java:278)
at android.app.ContextImpl.getSystemService(ContextImpl.java:1676)
at android.content.ContextWrapper.getSystemService(ContextWrapper.java:540)
at com.crfchina.market.util.NetUtil.getNetworkState(NetUtil.java:28)


错误日志产生原因:



Android里面内存泄漏问题最突出的就是Activity的泄漏,而泄漏的根源大多在于单例的使用,也就是一个静态实例持有了Activity的引用。静态变量的生命周期与应用(Application)是相同的,而Activity生命周期通常比它短,也就会造成在Activity生命周期结束后,还被引用导致无法被系统回收释放。

生成静态引用内存泄漏可能有两种情况:

1. 应用级:应用程序代码实现的单例没有很好的管理其生命周期,导致Activity退出后仍然被引用。
2. 系统级:Android系统级的实现的单例,被应用不小心错误调用(当然你也可以认为是系统层实现地不太友好)。

这个主要讲下系统级的情况,这样的情况可能也有很多,举个最近发现的问题ConnectivityManager。

通常我们获取系统服务时采用如下方式:

context.getSystemService()

在Android6.0系统上,如果这里的Context如果是Activity的实例,那么即使你什么也不干也会造成内存泄漏。

这个Context在ConnectivityManager 创建时传入,这个Context在StaticOuterContextServiceFetcher中由ContextImpl对象转换为OuterContext,与就是Activity对象,所以最终ConnectivityManager的单实例持有了Activity的实例引用。这样即使Activity退出后仍然无法释放,导致内存泄漏。

这个问题仅在6.0上出现,在5.1上ConnectivityManager实现为单例但不持有Context的引用,在5.0有以下版本ConnectivityManager既不为单例,也不持有Context的引用。

其他服务没认真研究,不确定有没有这个问题。不过为了避免类似的情况发生,

最好的解决办法就是:



解决方案:



获取系统服务getSystemService时使用ApplicationContext

context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);


java.lang.IllegalArgumentException: View not attached to window manager



错误日志产生原因:



在延时线程里调用了ProgressDialog.dismiss,但此时主Activity已经destroy了。于是应用崩溃,我写了一个 SafeProgressDialog 来避免这个问题,主要原理是覆写dismiss方法,在ProgressDialog.dismiss之前判断Activity是否存在。



解决方案:

class SafeProgressDialog extends ProgressDialog
{
Activity mParentActivity;
public SafeProgressDialog(Context context)
{
super(context);
mParentActivity = (Activity) context;
}

@Override
public void dismiss()
{
if (mParentActivity != null && !mParentActivity.isFinishing())
{
super.dismiss(); //调用超类对应方法
}
}
}

Android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.test.testFragment: make sure class name exists, is public, and has an empty constructor that is public



错误日志产生原因及解决方案:



根据报错提示 “make sure class name exists, is public, and has an empty constructor that is public” ,若Fragement定义有带参构造函数,则一定要定义public的默认的构造函数。即可解决此问题。

除了他说的public.还有一个就是弄个空的构造函数。

例如我是这样定义了我的fragment。带有了构造函数

public TestFragment(int index){
mIndex = index;
}

然后我添加了一个这样的构造函数

public TestFragment(){
}

java.lang.IllegalStateException: Unable to get package info for com.crfchina.market; is package not installed?



错误日志产生原因:



简单的卸载app 没有卸载干净,然后再次运行,当dalvik重新安装。apk文件并试图重用以前的活动从同一个包




好了目前就总结这么多,后续还会继续更新补充!毕竟太长也没有人愿意耐下心去看,以上也是曾经遇到过坑,希望有遇到的兄弟能从中受益!欢迎大家贴一些内容作为补充,相互学习共同进步……



作者:大荣言午
链接:https://www.jianshu.com/p/dd9714beb7ea
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

错过了金三银四,还不赶紧准备金九银十?这份Android大厂面试大纲静下心应对,九月就是你的战场!

感悟这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。而面试官也一样,他也可能只掌握了20%的知识,而且一个面...
继续阅读 »

感悟

这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。

而面试官也一样,他也可能只掌握了20%的知识,而且一个面试也不会有足够多的时间给你展示你全部的知识,而往往只会注意开发中最常遇到的20%。

这时候,你对这些问题理解的深度就显得尤为重要。回答了10个问题,而每个问题都只是浅显分析,还没有你将一个问题讲得透彻、全面更能让面试官加分。

当然这并不意味着当你要准备跳槽,要做面试准备的时候,你就只盯着几个自己感兴趣的课题,使劲背,使劲学,而其他的知识点就完全不学了。

想要面试的时候完胜面试官,最简便的,最稳妥的办法就是将一套完整系统的面试题全部刷完,然后再进行自我总结。

我知道有很多人最近都在为跳槽换工作面试做准备,所以在这里,我把我所收集到的面试大纲,分享给大家。

  • 阿里巴巴

  • LRUCache原理
  • 图片加载原理
  • 模块化实现(好处,原因)
  • JVM
  • 视频加密传输
  • 统计启动时长,标准
  • 如何保持应用的稳定性
  • ThreadLocal 原理
  • 谈谈classloader
  • 动态布局
  • 热修复,插件化
  • HashMap源码,SpareArray原理
  • 性能优化,怎么保证应用启动不卡顿
  • 怎么去除重复代码
  • SP是进程同步的吗?有什么方法做到同步
  • 介绍下SurfView
  • HashMap实现原理,ConcurrentHashMap 的实现原理
  • BroadcastReceiver,LocalBroadcastReceiver 区别
  • Bundle 机制
  • Handler 机制
  • android 事件传递机制
  • 线程间 操作 List
  • App启动流程,从点击桌面开始
  • 动态加载
  • 类加载器
  • OSGI
  • Https请求慢的解决办法,DNS,携带数据,直接访问IP
  • GC回收策略
  • 画出 Android 的大体架构图
  • 描述清点击 Android Studio 的 build 按钮后发生了什么
  • 大体说清一个应用程序安装到手机上时发生了什么;
  • 对 Dalvik、ART 虚拟机有基本的了解;
  • Android 上的 Inter-Process-Communication 跨进程通信时如何工作的;
  • App 是如何沙箱化,为什么要这么做;
  • 权限管理系统(底层的权限是如何进行 grant 的)
  • 进程和 Application 的生命周期;
  • 系统启动流程 Zygote进程 –> SystemServer进程 –> 各种系统服务 –> 应用进程
  • recycleview listview 的区别,性能
  • 排序,快速排序的实现
  • 树:B+树的介绍
  • 图:有向无环图的解释
  • TCP/UDP的区别
  • synchronized与Lock的区别
  • volatile
  • Java线程池
  • Java中对象的生命周期
  • 类加载机制
  • 双亲委派模型
  • Android事件分发机制
  • MVP模式
  • RxJava
  • 抽象类和接口的区别
  • 集合 Set实现 Hash 怎么防止碰撞
  • JVM 内存区域 开线程影响哪块内存
  • 垃圾收集机制 对象创建,新生代与老年代
  • 二叉树 深度遍历与广度遍历
  • B树、B+树
  • 消息机制
  • 进程调度
  • 进程与线程
  • 死锁
  • 进程状态
  • JVM内存模型
  • 并发集合了解哪些
  • ConCurrentHashMap实现
  • CAS介绍
  • 开启线程的三种方式,run()和start()方法区别
  • 线程池
  • 常用数据结构简介
  • 判断环(猜测应该是链表环)
  • 排序,堆排序实现
  • 链表反转

  • 腾讯

  • synchronized用法
  • volatile用法
  • 动态权限适配方案,权限组的概念
  • 网络请求缓存处理,okhttp如何处理网络缓存的
  • 图片加载库相关,bitmap如何处理大图,如一张30M的大图,如何预防OOM
  • 进程保活
  • listview图片加载错乱的原理和解决方案
  • https相关,如何验证证书的合法性,https中哪里用了对称加密,哪里用了非对称加密,对加密算法(如RSA)等是否有了解

  • 滴滴

  • MVP
  • 广播(动态注册和静态注册区别,有序广播和标准广播)
  • service生命周期
  • handler实现机制(很多细节需要关注:如线程如何建立和退出消息循环等等)
  • 多线程(关于AsyncTask缺陷引发的思考)
  • 数据库数据迁移问题
  • 设计模式相关(例如Android中哪里使用了观察者模式,单例模式相关)
  • x个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完
  • TCP与UDP区别与应用(三次握手和四次挥手)涉及到部分细节(如client如何确定自己发送的消息被server收到) HTTP相关 提到过Websocket 问了WebSocket相关以及与socket的区别
  • 是否熟悉Android jni开发,jni如何调用java层代码
  • 进程间通信的方式
  • java注解
  • 计算一个view的嵌套层级
  • 项目组件化的理解
  • 多线程断点续传原理
  • Android系统为什么会设计ContentProvider,进程共享和线程安全问题
  • jvm相关
  • Android相关优化(如内存优化、网络优化、布局优化、电量优化、业务优化)
  • EventBus实现原理

  • 美团

  • static synchronized 方法的多线程访问和作用,同一个类里面两个synchronized方法,两个线程同时访问的问题
  • 内部类和静态内部类和匿名内部类,以及项目中的应用
  • handler发消息给子线程,looper怎么启动
  • View事件传递
  • activity栈
  • 封装view的时候怎么知道view的大小
  • arraylist和linkedlist的区别,以及应用场景
  • 怎么启动service,service和activity怎么进行数据交互
  • 下拉状态栏是不是影响activity的生命周期,如果在onStop的时候做了网络请求,onResume的时候怎么恢复
  • view渲染

  • 今日头条

  • 数据结构中堆的概念,堆排序
  • 死锁的概念,怎么避免死锁
  • ReentrantLock 、synchronized和volatile(n面)
  • HashMap
  • singleTask启动模式
  • 用到的一些开源框架,介绍一个看过源码的,内部实现过程。
  • 消息机制实现
  • ReentrantLock的内部实现
  • App启动崩溃异常捕捉
  • 事件传递机制的介绍
  • ListView的优化
  • 二叉树,给出根节点和目标节点,找出从根节点到目标节点的路径
  • 模式MVP,MVC介绍
  • 断点续传的实现
  • 集合的接口和具体实现类,介绍
  • TreeMap具体实现
  • synchronized与ReentrantLock
  • 手写生产者/消费者模式
  • 逻辑地址与物理地址,为什么使用逻辑地址
  • 一个无序,不重复数组,输出N个元素,使得N个元素的和相加为M,给出时间复杂度、空间复杂度。手写算法
  • Android进程分类
  • 前台切换到后台,然后再回到前台,Activity生命周期回调方法。弹出Dialog,生命值周期回调方法。
  • Activity的启动模式

  • 爱奇艺

  • RxJava的功能与原理实现
  • RecycleView的使用,原理,RecycleView优化
  • ANR的原因
  • 四大组件
  • Service的开启方式
  • Activity与Service通信的方式
  • Activity之间的通信方式
  • HashMap的实现,与HashSet的区别
  • JVM内存模型,内存区域
  • Java中同步使用的关键字,死锁
  • MVP模式
  • Java设计模式,观察者模式
  • Activity与Fragment之间生命周期比较
  • 广播的使用场景

  • 百度

  • Bitmap 使用时候注意什么?
  • Oom 是否可以try catch ?
  • 内存泄露如何产生?
  • 适配器模式,装饰者模式,外观模式的异同?
  • ANR 如何产生?
  • String buffer 与string builder 的区别?
  • 如何保证线程安全?
  • java四中引用
  • Jni 用过么?
  • 多进程场景遇见过么?
  • 关于handler,在任何地方new handler 都是什么线程下
  • sqlite升级,增加字段的语句
  • bitmap recycler 相关
  • 强引用置为null,会不会被回收?
  • glide 使用什么缓存?
  • Glide 内存缓存如何控制大小?
  • 如何保证多线程读写文件的安全?

  • 携程

  • Activity启动模式
  • 广播的使用方式,场景
  • App中唤醒其他进程的实现方式
  • AndroidManifest的作用与理解
  • List,Set,Map的区别
  • HashSet与HashMap怎么判断集合元素重复
  • Java中内存区域与垃圾回收机制
  • EventBus作用,实现方式,代替EventBus的方式
  • Android中开启摄像头的主要步骤

  • 网易

  • 集合
  • concurrenthashmap
  • volatile
  • synchronized与Lock
  • Java线程池
  • wait/notify
  • NIO
  • 垃圾收集器
  • Activity生命周期
  • AlertDialog,popupWindow,Activity区别

  • 小米

  • String 为什么要设计成不可变的?
  • fragment 各种情况下的生命周期
  • Activity 上有 Dialog 的时候按 home 键时的生命周期
  • 横竖屏切换的时候,Activity 各种情况下的生命周期
  • Application 和 Activity 的 context 对象的区别
  • 序列化的作用,以及 Android 两种序列化的区别。
  • List 和 Map 的实现方式以及存储方式。
  • 静态内部类的设计意图。
  • 线程如何关闭,以及如何防止线程的内存泄漏

  • 360

  • 软引用、弱引用区别
  • 垃圾回收
  • 多线程:怎么用、有什么问题要注意;Android线程有没有上限,然后提到线程池的上限
  • JVM
  • OOM,内存泄漏
  • ANR怎么分析解决
  • LinearLayout、RelativeLayout、FrameLayout的特性、使用场景
  • 如何实现Fragment的滑动
  • ViewPager使用细节,如何设置成每次只初始化当前的Fragment,其他的不初始化
  • ListView重用的是什么
  • 进程间通信的机制
  • AIDL机制
  • AsyncTask机制
  • 如何取消AsyncTask
  • 序列化
  • Android为什么引入Parcelable
  • 有没有尝试简化Parcelable的使用
  • AIDL机制
  • 项目:拉活怎么做的
  • 应用安装过程
  • 某海外直播公司
  • 线程和进程的区别?
  • 为什么要有线程,而不是仅仅用进程?
  • 算法判断单链表成环与否?
  • 如何实现线程同步?
  • hashmap数据结构?
  • arraylist 与 linkedlist 异同?
  • object类的equal 和hashcode 方法重写,为什么?
  • hashmap如何put数据(从hashmap源码角度讲解)?
  • 简述IPC?
  • fragment之间传递数据的方式?
  • 简述tcp四次挥手?
  • threadlocal原理
  • 内存泄漏的可能原因?
  • 用IDE如何分析内存泄漏?
  • OOM的可能原因?
  • 线程死锁的4个条件?
  • 差值器&估值器
  • 简述消息机制相关
  • 进程间通信方式?
  • Binder相关?
  • 触摸事件的分发?
  • 简述Activity启动全部过程?
  • okhttp源码?
  • RxJava简介及其源码解读?
  • 性能优化如何分析systrace?
  • 广播的分类?
  • 点击事件被拦截,但是相传到下面的view,如何操作?
  • Glide源码?
  • ActicityThread相关?
  • volatile的原理
  • synchronize的原理
  • lock原理
  • 翻转一个单项链表
  • string to integer
  • 合并多个单有序链表(假设都是递增的)
  • 其他公司
  • 四大组件
  • Android中数据存储方式
  • 微信主页面的实现方式
  • 微信上消息小红点的原理
  • 两个不重复的数组集合中,求共同的元素。
  • 上一问扩展,海量数据,内存中放不下,怎么求出。
  • Java中String的了解。
  • ArrayList与LinkedList区别
  • 堆排序过程,时间复杂度,空间复杂度
  • 快速排序的时间复杂度,空间复杂度
  • RxJava的作用,与平时使用的异步操作来比,优势
  • Android消息机制原理
  • Binder机制介绍
  • 为什么不能在子线程更新UI
  • JVM内存模型
  • Android中进程内存的分配,能不能自己分配定额内存
  • 垃圾回收机制与调用System.gc()区别
  • Android事件分发机制
  • 断点续传的实现
  • RxJava的作用,优缺点

版权声明:本文为CSDN博主「Young-G2333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/YoungOne2333/article/details/115679291

收起阅读 »

Flutter踩坑:Android sdkmanager tool not found

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误: Android...
继续阅读 »

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误:



Android sdkmanager tool not found

(/Users/xx/android-sdk/tools/bin/sdkmanager).

Try re-installing or updating your Android SDK,

visit https://flutter.io/setup/#android-setup for detailed instructions.



解决步骤:

看字面意思问题应该是在“/Users/xx/android-sdk/tools/bin/sdkmanager”,但是我尝试了一下发现根本SDK文件夹下根本没有Tools文件夹
百度了一圈,网上给的解决方案,都是将emulator目录下的sdkmanager移动到 tools目录下,可是我根本就没有这个文件夹。
后来在Stack Overflow上找到了原因:Android Studio最新版本中,默认情况下是不会安装Android SDK Tools的,我的版本是3.6。



找到了原因就好解决了:




  • 在窗口左上角andriod studio-偏好设置中找到SDKTools






按图操作就好.png

继续在终端执行

flutter doctor --android-licenses (之后一路选Y就行了)






PS:VScode和AS都要记得装flutter插件,AS还要另外装dart插件


链接:https://www.jianshu.com/p/3237ea28793c 收起阅读 »

UITableViewCell嵌套WKWebView

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在找到了解决方案,废话不多说,直接看解决方案。正文1. 构建WKWebViewself.webView = [[WKWeb...
继续阅读 »
前言

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在stackoverflow找到了解决方案,废话不多说,直接看解决方案。

正文

1. 构建WKWebView

self.webView = [[WKWebView alloc] init];
// 创建请求
NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.jianshu.com"]];
// 加载网页
[self.webView loadRequest:request];

self.webView.scrollView.scrollEnabled = NO;
self.webView.scrollView.bounces = NO;
self.webView.scrollView.showsVerticalScrollIndicator = NO;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

// 将webView添加到界面
[self.contentView addSubview:self.webView];

2. cell高度适应WKWebView的内容

cell.webView.navigationDelegate = self;

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

[webView evaluateJavaScript:@"document.body.offsetHeight" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 计算webView高度
self.webViewCellHeight = [result doubleValue];
// 刷新tableView
[self.tableView reloadData];
}];
}

3. 解决加载空白问题
原因:由于WKWebView采用的lazy加载模式,所在的scrollView的滚动被禁用,导致被嵌套的WKCompositingView不进行数据加载。
详细细节请参考:WKWebView刷新机制小探

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 判断webView所在的cell是否可见,如果可见就layout
NSArray *cells = self.tableView.visibleCells;
for (UITableViewCell *cell in cells) {
if ([cell isKindOfClass:[TraitWebViewCell class]]) {
TraitWebViewCell *webCell = (TraitWebViewCell *)cell;

[webCell.webView setNeedsLayout];
}
}

}


链接:https://www.jianshu.com/p/8cdad2282d24
收起阅读 »

Material Design实战之可折叠式标题栏

CollapsingToolbarLayout1.基本介绍CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果注意:Collapsin...
继续阅读 »

CollapsingToolbarLayout

1.基本介绍

CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果

注意:
CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout的子布局。。

2.具体使用

①我们需要一个具体展示水果详情的页面

在这里就新建一个FruitActivity。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FruitActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mCollapsingToolbarLayout"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/purple_500"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fruitImageView"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>



app:contentScrim用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色。
app:layout_scrollFlags的exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
里面的ImageView和Toolbar就是标题栏的具体内容。
app:layout_collapseMode表示设置折叠过程中的折叠样式。
然后我们在加一个NestedScrollView,它不仅允许使用滚动来查看屏幕以外的数据,而且还增加了嵌套响应滚动事件的功能。
布局文件全部代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FruitActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mCollapsingToolbarLayout"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/purple_500"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fruitImageView"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginTop="35dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
app:cardCornerRadius="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruitContentText"
android:layout_margin="10dp"
/>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>



很好理解,就不解释了

②编写功能逻辑

public class FruitActivity extends AppCompatActivity {
static String FRUIT_NAME = "fruit_name";
static String FRUIT_IMAGE_ID = "fruit_image_id";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fruit);
String fruitName = getIntent ().getStringExtra(FRUIT_NAME);
String fruitImageId = getIntent ().getStringExtra(FRUIT_IMAGE_ID);

Toolbar toolbar = findViewById (R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

CollapsingToolbarLayout collapsingToolbarLayout = findViewById (R.id.mCollapsingToolbarLayout);
collapsingToolbarLayout.setTitle(fruitName);
Glide.with(this).load(fruitImageId).into((ImageView) findViewById (R.id.fruitImageView));

TextView textView = findViewById (R.id.fruitContentText);
}
}


textView.setText("声卡的那句阿奎那飞机咔叽脑筋那就是可能安东尼上级领导那就ask的年纪ask" +
"打卡时间开机卡死的你课件撒的就看撒贷记卡十多年按实际困难贷记卡大卡司你可记得是" +
"多久啊是当年就卡死的你叫阿三的");

toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();

}
});



没啥难度,不说了

③在RecyclerView的适配器中增加点击事件

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.MyViewHolder> {
List<Fruit> fruits = new ArrayList<>();
Context context;
FruitAdapter(Context context){
this.context = context;
for (int i = 0; i < 30; i++) {
fruits.add(new Fruit("香蕉",R.mipmap.banana));
}
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
MyViewHolder holder = new MyViewHolder(view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit = fruits.get(position);
Intent intent = new Intent(context, FruitActivity.class);
intent.putExtra(FruitActivity.FRUIT_NAME,fruit.name);
intent.putExtra(FruitActivity.FRUIT_IMAGE_ID,fruit.imageId);
context.startActivity(intent);
}
});
return holder;
}
}


————————————————
版权声明:本文为CSDN博主「独饮敌敌畏丶」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/afdafvdaa/article/details/115583226

收起阅读 »

前端如何进行用户权限管理

【前端如何进行用户权限管理】1:问题:假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?2:要不要给老师和学生各自设计一...
继续阅读 »

【前端如何进行用户权限管理】

1:问题:
假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。


现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?

2:要不要给老师和学生各自设计一套页面?这样工作量是不是太大了,并且如果还要加入其它角色的话,难道每个角色对应一套代码?

所以我们需要用一套页面适应各种用户角色,并根据身份赋予他们不同权限

3:权限设计与管理是一个很复杂的问题,涉及的东西很多,相比前端,更偏向于后端,在搜集相关资料的过程中,发现掺杂了许多数据库之类的知识,以及几个用于权限管理的java框架,比如spring,比如shiro等等,都属于后端的工作

4:那我们前端能做什么呢?

权限的设计中比较常见的就是RBAC基于角色的访问控制,基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。

一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

在Angular构建的单页面应用中,要实现这样的架构我们需要额外多做一些事.从整体项目上来讲,大约有3处地方,前端工程师需要进行处理.

1. UI处理(根据用户拥有的权限,判断页面上的一些内容是否显示)

2. 路由处理(当用户访问一个它没有权限访问的url时,跳转到一个错误提示的页面)

3. HTTP请求处理(当我们发送一个数据请求,如果返回的status是401或者401,则通常重定向到一个错误提示的页面)

如何实现?
首先需要在Angular启动之前就获取到当前用户的所有的permissions,然后比较优雅的方式是通过一个service存放这个映射关系.对于UI处理一个页面上的内容是否根据权限进行显示,我们应该通过一个directive来实现.当处理完这些,我们还需要在添加一个路由时额外为其添加一个"permission"属性,并为其赋值表明拥有哪些权限的角色可以跳转这个URL,然后通过Angular监听routeChangeStart事件来进行当前用户是否拥有此URL访问权限的校验.最后还需要一个HTTP拦截器监控当一个请求返回的status是401或者403时,跳转页面到一个错误提示页面.

大致上的工作就是这些,看起来有些多,其实一个个来还是挺好处理的.

在Angular运行之前获取到permission的映射关系



Angular项目通过ng-app启动,但是一些情况下我们是希望Angular项目的启动在我们的控制之中.比如现在这种情况下,我就希望能获取到当前登录用户的所有permission映射关系后,再启动Angular的App.幸运的是Angular本身提供了这种方式,也就是angular.bootstrap().看的仔细的人可能会注意到,这里使用的是$.get(),没有错用的是jQuery而不是Angular的$resource或者$http,因为在这个时候Angular还没有启动,它的function我们还无法使用.

进一步使用上面的代码可以将获取到的映射关系放入一个service作为全局变量来使用.


在取得当前用户的权限集合后,我们将这个集合存档到对应的一个service中,然后又做了2件事:

(1) 将permissions存放到factory变量中,使之一直处于内存中,实现全局变量的作用,但却没有污染命名空间.

(2) 通过$broadcast广播事件,当权限发生变更的时候.

如何确定UI组件的依据权限进行显隐




这里我们需要自己编写一个directive,它会依据权限关系来进行显示或者隐藏元素.

这里看到了比较理想的情况是通关一个has-permission属性校验permission的name,如果当前用户有则显示,没有则隐藏.




扩展一下之前的factory:




路由上的依权限访问
这一部分的实现的思路是这样: 当我们定义一个路由的时候增加一个permission的属性,属性的值就是有哪些权限才能访问当前url.然后通过routeChangeStart事件一直监听url变化.每次变化url的时候,去校验当前要跳转的url是否符合条件,然后决定是跳转成功还是跳转到错误的提示页面.

router.js:






mainController.js 或者 indexController.js (总之是父层Controller)





这里依然用到了之前写的hasPermission,这些东西都是高度可复用的.这样就搞定了,在每次view的route跳转前,在父容器的Controller中判断一些它到底有没有跳转的权限即可.



HTTP请求处理
这个应该相对来说好处理一点,思想的思路也很简单.因为Angular应用推荐的是RESTful风格的接口,所以对于HTTP协议的使用很清晰.对于请求返回的status code如果是401或者403则表示没有权限,就跳转到对应的错误提示页面即可.





当然我们不可能每个请求都去手动校验转发一次,所以肯定需要一个总的filter.代码如下:

写到这里我们就基本实现了在这种前后端分离模式下,前端部分的权限管理和控制。

原文链接:https://blog.csdn.net/jnshu_it/article/details/77511588


收起阅读 »

Android图片轮播-banner

使用步骤以下提供的是最简单的步骤,需要复杂的样式自己可以自定义Step 1.依赖bannerGradledependencies{ compile 'com.youth.banner:banner:2.1.0' }Step 2.添加权限到你的 An...
继续阅读 »

使用步骤

以下提供的是最简单的步骤,需要复杂的样式自己可以自定义

Step 1.依赖banner

Gradle

dependencies{
compile 'com.youth.banner:banner:2.1.0'
}

Step 2.添加权限到你的 AndroidManifest.xml


<uses-permission android:name="android.permission.INTERNET" />

Step 3.在布局文件中添加Banner,可以设置自定义属性

!!!此步骤可以省略,可以直接在Activity或者Fragment中new Banner();

<com.youth.banner.Banner
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="高度自己设置" />

Step 4.继承BannerAdapter,和RecyclerView的Adapter一样(如果你只是图片轮播也可以使用默认的)

!!!此步骤可以省略,图片轮播提供有默认适配器,其他的没有提供是因为大家的可变性要求不确定,所以直接自定义的比较好。

/**
* 自定义布局,下面是常见的图片样式,更多实现可以看demo,可以自己随意发挥
*/
public class ImageAdapter extends BannerAdapter<DataBean, ImageAdapter.BannerViewHolder> {

public ImageAdapter(List<DataBean> mDatas) {
//设置数据,也可以调用banner提供的方法,或者自己在adapter中实现
super(mDatas);
}

//创建ViewHolder,可以用viewType这个字段来区分不同的ViewHolder
@Override
public BannerViewHolder onCreateHolder(ViewGroup parent, int viewType) {
ImageView imageView = new ImageView(parent.getContext());
//注意,必须设置为match_parent,这个是viewpager2强制要求的
imageView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
return new BannerViewHolder(imageView);
}

@Override
public void onBindView(BannerViewHolder holder, DataBean data, int position, int size) {
holder.imageView.setImageResource(data.imageRes);
}

class BannerViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;

public BannerViewHolder(@NonNull ImageView view) {
super(view);
this.imageView = view;
}
}
}

Step 5.Banner具体方法调用

public class BannerActivity extends AppCompatActivity {
public void useBanner() {
//--------------------------简单使用-------------------------------
banner.addBannerLifecycleObserver(this)//添加生命周期观察者
.setAdapter(new BannerExampleAdapter(DataBean.getTestData()))
.setIndicator(new CircleIndicator(this));

//—————————————————————————如果你想偷懒,而又只是图片轮播————————————————————————
banner.setAdapter(new BannerImageAdapter<DataBean>(DataBean.getTestData3()) {
@Override
public void onBindView(BannerImageHolder holder, DataBean data, int position, int size) {
//图片加载自己实现
Glide.with(holder.itemView)
.load(data.imageUrl)
.apply(RequestOptions.bitmapTransform(new RoundedCorners(30)))
.into(holder.imageView);
}
})
.addBannerLifecycleObserver(this)//添加生命周期观察者
.setIndicator(new CircleIndicator(this));
//更多使用方法仔细阅读文档,或者查看demo
}
}

Banner使用中优化体验

如果你需要考虑更好的体验,可以看看下面的代码

Step 1.(可选)生命周期改变时

public class BannerActivity {

//方法一:自己控制banner的生命周期

@Override
protected void onStart() {
super.onStart();
//开始轮播
banner.start();
}

@Override
protected void onStop() {
super.onStop();
//停止轮播
banner.stop();
}

@Override
protected void onDestroy() {
super.onDestroy();
//销毁
banner.destroy();
}

//方法二:调用banner的addBannerLifecycleObserver()方法,让banner自己控制

protected void onCreate(Bundle savedInstanceState) {
//添加生命周期观察者
banner.addBannerLifecycleObserver(this);
}
}

常见问题(收录被反复询问的问题)

  • 网络图片加载不出来?

    banner本身不提供图片加载功能,首先确认banner本身使用是否正确,具体参考demo, 然后请检查你的图片加载框架或者网络请求框架,服务端也可能加了https安全认证,是看下是否报有证书相关错误

  • 怎么实现视频轮播?

    demo中有实现类似淘宝商品详情的效果,第一个放视频,后面的放的是图片,并且可以设置首尾不能滑动。 因为大家使用的播放器不一样业务环境也不同,具体情况自己把握,demo就是给一个思路哈!可以参考和修改

  • 我想指定轮播开始的位置?

    现在提供了setStartPosition()方法,在sheAdapter和setDatas直接调用一次就行了,当然setAdapter后通过setCurrentItem设置也行

  • 父控件滑动时,banner切换会获取焦点,然后自动全部显示。不想让banner获取焦点可以给父控件加上:

        //banner也一定要用最新版哦!
    android:focusable="true"
    android:focusableInTouchMode="true"


代码下载:banner-master.zip

原文链接:https://github.com/SenhLinsh/Android-Hot-Libraries

收起阅读 »

彻底解决小程序无法触发SESSION问题

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来wx.request({ ...... success: function(res) { console.log(res.header); //set-co...
继续阅读 »

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来

wx.request({
......
success: function(res) {
console.log(res.header);
//set-cookie:PHPSESSID=ic4vj84aaavqgb800k82etisu0; path=/; domain=.fengkui.net

// 登录成功,获取第一次的sessionid,存储起来
// 注意:Set-Cookie(开发者工具中调试全部小写)(远程调试和线上首字母大写)
wx.setStorageSync("sessionid", res.header["Set-Cookie"]);
}
})

二、请求时带上将sessionid放入request的header头中传到服务器,服务器端可直接在cookie中获取

wx.request({
......
header: {
'content-type': 'application/json', // 默认值
'cookie': wx.getStorageSync("sessionid")
//读取sessionid,当作cookie传入后台将PHPSESSID做session_id使用
},
success: function(res) {
console.log(res)
}
})

三、后台获取cookie中的PHPSESSID,将PHPSESSID当作session_id使用

<?php
// 判断$_COOKIE['PHPSESSID']是否存在,存在则作session_id使用
if ($_COOKIE['PHPSESSID']) {
session_id($_COOKIE['PHPSESSID']);
}

session_start();
echo session_id();


原文链接:https://blog.csdn.net/qq_41654694/article/details/85991846

收起阅读 »

iOS 一个OC对象在内存中的布局&&占用多少内存

一.先来看看我们平时接触的NSObject NSObject *objc = [[NSObject alloc]init]的本质 在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为: struct NSObject_I...
继续阅读 »

一.先来看看我们平时接触的NSObject



  • NSObject *objc = [[NSObject alloc]init]的本质

    在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为:


struct NSObject_IMPL {
Class isa;
};

在64位机中,一个isa占8个字节,在32位机中,一个isa占4个字节(当然苹果后面的机型都是64位的,这里我们着重讲解64位机)

  • 我们先来看看这个创建好的objc占多少个字节


int main(int argc, char * argv[]) {

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
//定义一个objc
NSObject *objc = [[NSObject alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([NSObject class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));
}

}




其打印结果为:



objc打印结果





  • 为什么一个是8一个是16
    • 我们先来认识一下class_getInstanceSize、malloc_size的区别

      1.class_getInstanceSize:是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)即实例对象的大小->结构体内存对齐

      2.创建的对象【至少】需要的内存大小不考虑malloc函数的话,内存对齐一般是以【8】对齐

      3.#import <objc/runtime.h>使用这个函数时倒入runtime运行时



    • malloc_size:堆空间【实际】分配给对象的内存大小 -系统内存对齐



      1. 在Mac、iOS中的malloc函数分配的内存大小总是【16】的倍数 即指针指向的内存大小

      2. import <malloc/malloc.h>使用时倒入这个框架





  • sizeof:是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等),这些数值在程序编译时就转成常数,程序运行时是直接获取的
  • 看到上面对两个函数的认识,应该知道为什么输出的一个是8,一个是16了吧,当内存申请<16时,在底层分配的时候,系统会默认最低16个字节,系统给objc16个字节,而objc用到的是8个字节(没添加任何成员变量之前)

二.内存对齐



  • 在上面的基础上我们新建一个类Student继承NSObject,那么对于student的底层C++编译实现就变成了:


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
};


也就是说,继承关系,子类直接将父类的isa引用进来




  • 对于class_getInstanceSize(也就是类本质的内存对其)

    1.在student中创建成员变量:
@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;
}
@end

其底层C++编译结构体就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
};



  • 打印结果:


 //定义一个objc
Student *objc = [[Student alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([Student class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));

2020-09-08 12:35:27.158568+0800 OC底层[1549:79836] tu-24

2020-09-08 12:35:27.159046+0800 OC底层[1549:79836] tu-32




  • 先来说说24的由来





由于创建对象的时候,内存是以8对齐,上面我们讲到一个对象里面包含了一个isa占8个字节,对于student来说它有四个成员变量,isa,age,no,tc,共占8+4+4+4=20字节,但是由于内存以8对齐的原因,我们看到的输出是24,

所以class_getInstanceSize在计算实例大小的时候就是24,其白色区域表示空出了四个字节

再来看看32的由来
上面我们说到malloc_size指的是实际堆分配的空间,它以16字节对齐
可以看到,空白的区域为空出了12个字节,总共为32个字节

三.添加属性


  • 添加属性


@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;

}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSArray *array;
@end

其在底层C++编译就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
NSString _name;
NSArray _array;
};


默认的会将属性生成的_name添加进结构体中,计算相应的大小



总结:所以在实际计算类的占用空间大小的时候,根据添加的成员变量就可以计算出一个实例占用的内存大小(即计算出结构体的大小24,然后告诉系统,系统调用calloc分配内存的时候按照16对齐原则分配)

收起阅读 »

vue 重复点击菜单,路由重复报错

报错信息vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。报错原因vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果...
继续阅读 »

报错信息

vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。


报错原因
vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果没有捕获到错误,控制台始终会出现如上图的报错
node_module/vue-router/dist/vue-router.js 搜VueRouter.prototype.push

解决方法

1.降低vue-router的版本

npm i vue-router@3.0 -S

2.在vue.use(Router)使用路由插件之前插入如下代码

//获取原型对象上的push函数
const originalPush = Router.prototype.push
//修改原型对象中的push方法
Router.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => err)
}

3.捕获异常

// 捕获router.push异常
this.$router.push(route).catch(err => {
console.log('输出报错',err)

4.补齐router第三个参数

// 补齐router.push()的第三个参数
this.$router.push(route, () => {}, (e) => {
console.log('输出报错',e)
})

本文链接:https://blog.csdn.net/pinbolei/article/details/115620529


收起阅读 »

深入理解vue中的slot与slot-scope

写在前面vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明...
继续阅读 »

写在前面

vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明文档。

实际上,插槽的概念很简单,下面通过分三部分来讲。这个部分也是按照vue说明文档的顺序来写的。

进入三部分之前,先让还没接触过插槽的同学对什么是插槽有一个简单的概念:插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示、以及怎样显示由父组件来决定。 实际上,一个slot最核心的两个问题这里就点出来了,是显示不显示怎样显示

由于插槽是一块模板,所以,对于任何一个组件,从模板种类的角度来分,其实都可以分为非插槽模板插槽模板两大类。
非插槽模板指的是html模板,指的是‘div、span、ul、table’这些,非插槽模板的显示与隐藏以及怎样显示由插件自身控制;插槽模板是slot,它是一个空壳子,因为它显示与隐藏以及最后用什么样的html模板显示由父组件控制。但是插槽显示的位置确由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块

单个插槽 | 默认插槽 | 匿名插槽

首先是单个插槽,单个插槽是vue的官方叫法,但是其实也可以叫它默认插槽,或者与具名插槽相对,我们可以叫它匿名插槽。因为它不用设置name属性。

单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(name属性)不同就可以了。

下面通过一个例子来展示。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
<h3>这里是子组件</h3>
<slot></slot>
</div>
</template>

在这个例子里,因为父组件在<child></child>里面写了html模板,那么子组件的匿名插槽这块模板就是下面这样。也就是说,子组件的匿名插槽被使用了,是被下面这块模板使用了。

<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>

最终的渲染结果如图所示:


注:所有demo都加了样式,以方便观察。其中,父组件以灰色背景填充,子组件都以浅蓝色填充。

具名插槽

匿名插槽没有name属性,所以是匿名插槽,那么,插槽加了name属性,就变成了具名插槽。具名插槽可以在一个组件中出现N次。出现在不同的位置。下面的例子,就是一个有两个具名插槽单个插槽的组件,这三个插槽被父组件用同一套css样式显示了出来,不同的是内容上略有区别。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl" slot="up">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
<div class="tmpl" slot="down">
<span>菜单-1</span>
<span>菜单-2</span>
<span>菜单-3</span>
<span>菜单-4</span>
<span>菜单-5</span>
<span>菜单-6</span>
</div>
<div class="tmpl">
<span>菜单->1</span>
<span>菜单->2</span>
<span>菜单->3</span>
<span>菜单->4</span>
<span>菜单->5</span>
<span>菜单->6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
// 具名插槽
<slot name="up"></slot>
<h3>这里是子组件</h3>
// 具名插槽
<slot name="down"></slot>
// 匿名插槽
<slot></slot>
</div>
</template>

显示结果如图:



可以看到,父组件通过html模板上的slot属性关联具名插槽。没有slot属性的html模板默认关联匿名插槽。

作用域插槽 | 带数据的插槽

最后,就是我们的作用域插槽。这个稍微难理解一点。官方叫它作用域插槽,实际上,对比前面两种插槽,我们可以叫它带数据的插槽。什么意思呢,就是前面两种,都是在组件的template里面写

匿名插槽
<slot></slot>
具名插槽
<slot name="up"></slot>

但是作用域插槽要求,在slot上面绑定数据。也就是你得写成大概下面这个样子。

<slot name="up" :data="data"></slot>
export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
},
}

我们前面说了,插槽最后显示不显示是看父组件有没有在child下面写模板,像下面那样。

<child>
html模板
</child>

写了,插槽就总得在浏览器上显示点东西,东西就是html该有的模样,没写,插槽就是空壳子,啥都没有。
OK,我们说有html模板的情况,就是父组件会往子组件插模板的情况,那到底插一套什么样的样式呢,这由父组件的html+css共同决定,但是这套样式里面的内容呢?

正因为作用域插槽绑定了一套数据,父组件可以拿来用。于是,情况就变成了这样:样式父组件说了算,但内容可以显示子组件插槽绑定的。

我们再来对比,作用域插槽和单个插槽和具名插槽的区别,因为单个插槽和具名插槽不绑定数据,所以父组件是提供的模板要既包括样式由包括内容的,上面的例子中,你看到的文字,“菜单1”,“菜单2”都是父组件自己提供的内容;而作用域插槽,父组件只需要提供一套样式(在确实用作用域插槽绑定的数据的前提下)。

下面的例子,你就能看到,父组件提供了三种样式(分别是flex、ul、直接显示),都没有提供数据,数据使用的都是子组件插槽自己绑定的那个人名数组。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<!--第一次使用:用flex展示数据-->
<child>
<template slot-scope="user">
<div class="tmpl">
<span v-for="item in user.data">{{item}}</span>
</div>
</template>

</child>

<!--第二次使用:用列表展示数据-->
<child>
<template slot-scope="user">
<ul>
<li v-for="item in user.data">{{item}}</li>
</ul>
</template>

</child>

<!--第三次使用:直接显示数据-->
<child>
<template slot-scope="user">
{{user.data}}
</template>

</child>

<!--第四次使用:不使用其提供的数据, 作用域插槽退变成匿名插槽-->
<child>
我就是模板
</child>
</div>
</template>

子组件:

<template>
<div class="child">

<h3>这里是子组件</h3>
// 作用域插槽
<slot :data="data"></slot>
</div>
</template>

export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
}
}

结果如图所示:



github

以上三个demo就放在GitHub了,有需要的可以去取。使用非常方便,是基于vue-cli搭建工程。

https://github.com/cunzaizhuyi/vue-slot-demo

转载地址:https://segmentfault.com/a/1190000012996217

收起阅读 »

iOS之解决崩溃Collection <__NSArrayM: 0xb550c30> was mutated while being enumerated.

崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x14df0bd0> was ...
继续阅读 »

崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x14df0bd0> was mutated while being enumerated.'



当程序出现这个提示的时候,是因为你一边便利数组,又同时修改这个数组里面的内容,导致崩溃,网上的方法如下:

NSMutableArray * arrayTemp = xxx;

NSArray * array = [NSArray arrayWithArray: arrayTemp];

for (NSDictionary * dic in array) {

if (condition){

[arrayTemp removeObject:dic];

}

}

这种方法就是在定义一个一模一样的数组,便利数组A然后操作数组B

今天终于找到了一个更快接的删除数组里面的内容以及修改数组里面的内容的方法:

NSMutableArray *tempArray = [[NSMutableArray alloc]initWithObjects:@"12",@"23",@"34",@"45",@"56", nil];

[tempArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

if ([obj isEqualToString:@"34"]) {

*stop = YES;

if (*stop == YES) {

[tempArray replaceObjectAtIndex:idx withObject:@"3333333"];

}

}

if (*stop) {

NSLog(@"array is %@",tempArray);

}

}];



利用block来操作,根据查阅资料,发现block便利比for便利快20%左右,这个的原理是这样的:

找到符合的条件之后,暂停遍历,然后修改数组的内容

转自:https://www.cnblogs.com/rglmuselily/p/6249015.html

收起阅读 »

Android Handler消息传递机制

Android中只允许UI线程(也就是主线程)修改Activity里的UI组件。实际开发中,新启动的线程需要周期性地改变界面组件的属性值就需要借助Handler的消息传递机制。Handler类Handler类的主要作用:在新启动的线程中发送消息在主线程中获取、...
继续阅读 »

Android中只允许UI线程(也就是主线程)修改Activity里的UI组件。实际开发中,新启动的线程需要周期性地改变界面组件的属性值就需要借助Handler的消息传递机制。

Handler类

Handler类的主要作用:

在新启动的线程中发送消息
在主线程中获取、处理消息
Handler类包含如下方法用于发送、处理消息。

handleMessage(Message msg):处理消息的方法。该方法通常用于被重写。
hasMessages(int what):检查消息队列中是否包含what属性为指定值的消息。
hasMessages(int what,Object object):检查消息队列中是否包含what属性为指定值且object属性为指定对象的消息。
多个重载的 Message obtainMessage():获取消息。
sendEmptyMessage(int what):发送空消息。
sendEmptyMessageDelayed(int what,long delayMillis):指定多少毫秒之后发送空消
sendMessage(Message msg):立即发送消息。
sendMessageDelayed(Message msg,long delayMillis):指定多少毫秒之后发送消息。
借助于上面这些方法,程序可以方便地利用Handler来进行消息传递。
关于Handler的源码解读,可参考别人写的《Android 多线程之 Handler 源码分析》

实例:自动轮播图片

本实例通过一个新线程来周期性的修改ImageView所显示的图片(因为不允许其他线程访问Activity的界面组件,故在程序中发送消息通知系统更新ImageView组件,故不需要实例Looper),布局文件非常简单,故直接给程序代码:

package com.example.testapp1.activity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.example.testapp1.R;
import com.example.testapp1.control.RoundImageView;

import java.lang.ref.WeakReference;
import java.util.Timer;
import java.util.TimerTask;

public class NextActivity extends AppCompatActivity {
private RoundImageView imageShow;

static class ImageHandler extends Handler {
private WeakReference nextActivityWeakReference;

public ImageHandler(WeakReference nextActivityWeakReference) {
this.nextActivityWeakReference = nextActivityWeakReference;
}

private int[] imageIds = new int[]{R.drawable.a383f7735d8cd09fb81ff979b2f3d599
, R.drawable.b6ab4abe4db592b27ea678345b0c3416
, R.mipmap.head1
, R.drawable.b6ab4abe4db592b27ea678345b0c3416};
private int currentImageId = 0;

@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == 0x1233) {
nextActivityWeakReference.get().imageShow.setImageResource(imageIds[currentImageId++ % imageIds.length]);
}
}
}

ImageHandler imageHandler = new ImageHandler(new WeakReference<>(this));

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.next);
imageShow = findViewById(R.id.headImg);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
imageHandler.sendEmptyMessage(0x1233);
}
}, 0, 2000);
}
}




上述代码中,TimeTask对象的本质就是启动一条新线程。

Handler、Loop、MessageQueue的工作原理

Message: Handler接收和处理的消息对象。
Looper:每个线程只能拥有一个Looper。它的loop方法负责读取 MessageQueue中的消息,读到信息之后就把消息交给发送该消息的Handler进行处理。
MessageQueue:消息队列,它采用先进先出的方式来管理Message。程序创建Looper对象时,会在它的构造器中创建MessageQueue对象。Looper的构造器源代码如下:

private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}



该构造器使用了private修饰,表明程序员无法通过构造器创建Looper对象。从上面的代码不难看出,程序在初始化Looper时会创建一个与之关联的 MessagQueue,这个MessageOuee就负责管理消息。

Handler:它的作用有两个,即发送消息和处理消息,程序使用Handler发送消息,由Handler发送的消息必须被送到指定的MessageQueue。也就是说,如果希望Handler正常工作,必须在当前线程中有一个MessageQueue;否则消息就没有 MessageQueue进行保存了。不过MessageQueue是由Looper负责管理的,也就是说,如果希望Handler正常工作,必须在当前线程中有一个Looper对象。为了保证当前线程中有Looper对象,可以分如下两种情况处理。
在主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可通过Handler来发送消息、处理消息了。
程序员自己启动的子线程,必须自己创建一个Looper对象,并启动它。创建 Looper对象调用它的prepare(方法即可。
prepare()方法保证每个线程最多只有一个Looper对象。prepare()方法的源代码如下:

public static void prepare() {
prepare(true);
}

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}



接下来调用Looper的静态loop()方法来启动它。loop()方法使用一个死循环不断取出MessageQueue中的消息,并将取出的消息分给该消息对应的Handler进行处理。下面是Looper类的loop()方法的源代码:

/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
if (me.mInLoop) {
Slog.w(TAG, "Loop again would have the queued messages be executed"
+ " before this one completed.");
}

me.mInLoop = true;
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);

boolean slowDeliveryDetected = false;

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;

final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;

if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}

final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();
}
}


归纳起来,Looper、MessageQueue、Handler各自的作用如下:

Looper:每个线程只有一个Looper,它负责管理MessageQueue,会不断地从MessageQueag中取出消息,并将消息分给对应的Handler处理。
MessageQueue:由Looper负责管理。它采用先进先出的方式来管理Message。
Handler:它能把消息发送给 Looper管理的MessageQueue,并负责处理 Looper分给它的消息。

在线程中使用Handler的步骤如下:

调用Looper的 prepare()方法为当前线程创建Looper对象,创建Looper对象时,它的构造器会创建与之配套的MessageQueue。
有了Looper之后,创建 Handler子类的实例,重写 handleMessage(方法,该方法负责处理来自其他线程的消息。
调用Looper的loopO方法启动Looper。

实例:使用新线程实现点击图片弹出图片内容

1.布局文件:


android:id="@+id/constraintlayout2"
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"
android:layout_width="match_parent"
android:layout_gravity="center"
android:layout_height="wrap_content">

android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_launcher_foreground" />

android:id="@+id/textView3"
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/constraintlayout2"
tools:text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
tools:visibility="visible" />


布局文件比较简单,就是使用约束布局,在其中放入一个图片控件和文本控件(不展示)。
JAVA代码:

private ImageThread imageThread;

class ImageHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if(msg.what == 0x123){
String imageText = msg.getData().getString("ImageText");
Toast.makeText(mContext, imageText, Toast.LENGTH_LONG).show();
}
}
}

class ImageThread extends Thread {
private Handler mHandler;

@Override
public void run() {
Looper.prepare();
mHandler = new ImageHandler();
Looper.loop();
}
}



上述代码定义了一个线程的子类和Handler的子类,在Android Studio的比较新的版本不能直接使用Handler类实例对象并重新handleMessage(已废弃,旧版本可以),必须通过Handler子类实例对象
在Activity的onCreate()或者Fragment的onCreateView()方法中加入以下代码:启动新线程,监听图片的点击事件,向新线程中的Handler发送消息。

imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Message msg = new Message();
msg.what = 0x123;
Bundle bundle = new Bundle();
bundle.putString("ImageText", imageData.getImageText());
msg.setData(bundle);
imageThread.mHandler.sendMessage(msg);
}
});
imageThread = new ImageThread();
imageThread.start();


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

收起阅读 »

Android三方库glide的使用

glideGlide 是一个图片加载库,跟它同类型的库还有 Picasso、Fresco、Universal-Image-Loader 等。glide库的优点:加载类型多样化:Glide 支持 Gif、WebP 等格式的图片。生命周期的绑定:图片请求与页面生命...
继续阅读 »

glide

Glide 是一个图片加载库,跟它同类型的库还有 Picasso、Fresco、Universal-Image-Loader 等。

glide库的优点:

加载类型多样化:Glide 支持 Gif、WebP 等格式的图片。
生命周期的绑定:图片请求与页面生命周期绑定,避免内存泄漏。
使用简单(链式调用),且提供丰富的 Api 功能 (如: 图片裁剪等功能)。
高效的缓存策略:
支持多种缓存策略 (Memory 和 Disk 图片缓存)。
根据 ImageView 的大小来加载相应大小的图片尺寸。
内存开销小,默认使用 RGB_565 格式 (3.x 版本)。
使用 BitmapPool 进行 Bitmap 的复用。
首先,使用glide需要添加依赖,在当前项目的build.gradle下加入以下代码:

implementation 'com.github.bumptech.glide:glide:4.8.0'
其次,在加载图片时,若需要网络请求或者本地内存的访问,需要在当前项目的AndroidManifest.xml中加入请求权限代码:

//用于网络请求

//它可以监听用户的连接状态并在用户重新连接到网络时重启之前失败的请求

//用于硬盘缓存和读取

glide的使用

Glide.with(MainActivity) .load(R.mipmap.image) .into(imageView);

with()方法可以接收Context、Activity或者Fragment类型的参数。
load()方法中不仅可以传入图片地址,还可以传入图片文件File,resource,图片的byte数组等。
into()参数可以直接写图片控件,如需要给其他控件添加背景图片,则需要:

.into(new SimpleTarget(){
@Override
public void onResourceReady(Bitmap resource, Transition transition) {
Drawable drawable = new BitmapDrawable(resource);
mConstraintLayout.setBackground(drawable);
}
});

加载本地图片:

File file = new File(getExternalCacheDir() + "/image.jpg");
Glide.with(this).load(file).into(imageView);

加载应用资源:

int resource = R.drawable.image;
Glide.with(this).load(resource).into(imageView);

加载二进制流:

byte[] image = getImageBytes();
Glide.with(this).load(image).into(imageView);

加载Uri对象:

Uri imageUri = getImageUri();
Glide.with(this).load(imageUri).into(imageView);

注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。
取消图片:Glide.with(this).load(url).clear();
————————————————
版权声明:本文为CSDN博主「maisomgan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45828419/article/details/115632155

收起阅读 »

Android三方库OKHTTP请求的使用

okhttpOkhttp是网络请求框架。OkHttp主要有Get请求、Post请求等功能。使用前,需要添加依赖,在当前项目的build.gradle下加入以下代码:implementation 'com.squareup.okhttp3:okhttp:3.5....
继续阅读 »

okhttp

Okhttp是网络请求框架。OkHttp主要有Get请求、Post请求等功能。
使用前,需要添加依赖,在当前项目的build.gradle下加入以下代码:

implementation 'com.squareup.okhttp3:okhttp:3.5.0'

Okhttp的Get请求
使用OkHttp进行Get请求只需要完成以下四步:

获取OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();

构造Request对象
Request request = new Request.Builder() .get() .url("https://v0.yiketianqi.com/api?version=v62&appid=12646748&appsecret=SLB1jIr8&city=北京") .build();

将Request封装为Call
Call call = okHttpClient.newCall(request);

根据需要调用同步或者异步请求方法
//同步调用,返回Response,会抛出IO异常
Response response = call.execute();

//异步调用,并设置回调函数

call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Toast.makeText(OkHttpActivity.this, "get failed", Toast.LENGTH_SHORT).show();
}

@Override
public void onResponse(Call call, final Response response) throws IOException {
final String res = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(res);
}
});
}
});



OkHttp进行Post请求
使用OkHttp进行Post请求和进行Get请求很类似,只需要以下五步:

获取OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
1
构建FormBody或RequestBody或构架我们自己的RequestBody,传入参数

//OkHttp进行Post请求提交键值对
FormBody formBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "admin")
.build();

//OkHttp进行Post请求提交字符串
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:admin}");

//OkHttp进行Post请求上传文件
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
if (!file.exists()){
Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
}else{
RequestBody requestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file);
}

//OkHttp进行Post请求提交表单
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
if (!file.exists()){
Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
return;
}
RequestBody muiltipartBody = new MultipartBody.Builder()
//一定要设置这句
.setType(MultipartBody.FORM)
.addFormDataPart("username", "admin")//
.addFormDataPart("password", "admin")//
.addFormDataPart("myfile", "1.png", RequestBody.create(MediaType.parse("application/octet-stream"), file))
.build();
构建Request,将FormBody作为Post方法的参数传入
final Request request = new Request.Builder()
.url("http://www.jianshu.com/")
.post(formBody)
.build();

将Request封装为Call
Call call = okHttpClient.newCall(request);
1
调用请求,重写回调方法
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Toast.makeText(OkHttpActivity.this, "Post Failed", Toast.LENGTH_SHORT).show();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
final String res = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(res);
}
});
}
});


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

收起阅读 »

即学即用Android Jetpack - Navigation

前言 即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇。 记得去年第一次参加谷歌开发者大会的时候,就被Navigation的...
继续阅读 »

前言


即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇。


记得去年第一次参加谷歌开发者大会的时候,就被Navigation的图形导航界面给迷住了,一句卧槽就代表了小王的全部心情~


目录


一、简介


1. 定义


Navigation是什么呢?谷歌的介绍视频上说:



Navigation是一个可简化Android导航的库和插件



更确切的来说,Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见App的交互流程。这完美的契合了Jake Wharton大神单Activity的建议。


2. 优点



  • 处理Fragment的切换(上文已说过)

  • 默认情况下正确处理Fragment的前进和后退

  • 为过渡和动画提供标准化的资源

  • 实现和处理深层连接

  • 可以绑定ToolbarBottomNavigationViewActionBar


  • SafeArgs(Gradle插件) 数据传递时提供类型安全性


  • ViewModel支持


3. 准备


如果想要进行下面的学习,你需要 3.2 或者更高的Android studio


4. 学习方式


最好的学习方式仍然是通过官方文档,下面是官方的学习地址:

谷歌官方教程:Navigation Codelab

谷歌官方文档:Navigation

官方Demo:Demo地址


二、实战

可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph看作一个地图,NavHostFragment看作一个车,以及把NavController看作车中的方向盘,Navigation Graph中可以看出各个地点(Destination)和通往各个地点的路径,NavHostFragment可以到达地图中的各个目的地,但是决定到什么目的地还是方向盘NavController,虽然它取决于开车人(用户)。



第一步 添加依赖


模块层的build.gradle文件需要添加:

ext.navigationVersion = "2.0.0"
dependencies {
//...
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
}

如果你要使用SafeArgs插件,还要在项目目录下的build.gradle文件添加:

buildscript {
ext.navigationVersion = "2.0.0"
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
}
}

以及模块下面的build.gradle文件添加:

apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'

第二步 创建navigation导航



  1. 创建基础目录:资源文件res目录下创建navigation目录 -> 右击navigation目录New一个Navigation resource file

  2. 创建一个Destination,如果说navigation是我们的导航工具,Destination是我们的目的地,在此之前,我已经写好了一个WelcomeFragmentLoginFragmentRegisterFragment

除了可视化界面之外,我们仍然有必要看一下里面的内容组成,login_navigation.xml

<navigation
...
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">

<fragment
android:id="@+id/login"
android:name="com.joe.jetpackdemo.ui.fragment.login.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_login"
/>

<fragment
android:id="@+id/welcome"
android:name="com.joe.jetpackdemo.ui.fragment.login.WelcomeFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_welcome">
<action
.../>
<action
.../>
</fragment>

<fragment
android:id="@+id/register"
android:name="com.joe.jetpackdemo.ui.fragment.login.RegisterFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_register"
>

<argument
.../>
</fragment>
</navigation>



我在这里省略了一些不必要的代码


第三步 建立NavHostFragment


我们创建一个新的LoginActivity,在activity_login.xml文件中:

    ...>

android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/login_navigation"
app:defaultNavHost="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>





第四步 界面跳转、参数传递和动画

WelcomeFragment中,点击登录和注册按钮可以分别跳转到LoginFragmentRegisterFragment中。

这里我使用了两种方式实现:


方式一 利用ID导航

目标:WelcomeFragment携带keyname的数据跳转到LoginFragmentLoginFragment接收后显示。

Have a account ? Login按钮的点击事件如下:

btnLogin.setOnClickListener {
// 设置动画参数
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
// 参数设置
val bundle = Bundle()
bundle.putString("name","TeaOf")
findNavController().navigate(R.id.login, bundle,navOption)
}

后续LoginFragment的接收代码比较简单,直接获取Fragment中的Bundle即可,这里不再出示代码。

方式二 利用Safe Args

目标:WelcomeFragment通过Safe Args将数据传到RegisterFragmentRegisterFragment接收后显示。

再看一下已经展示过的login_navigation.xml

<navigation
...
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">

<fragment
android:id="@+id/login"
android:name="com.joe.jetpackdemo.ui.fragment.login.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_login"
/>

<fragment
android:id="@+id/welcome"
android:name="com.joe.jetpackdemo.ui.fragment.login.WelcomeFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_welcome">
<action
.../>
<action
.../>
</fragment>

<fragment
android:id="@+id/register"
android:name="com.joe.jetpackdemo.ui.fragment.login.RegisterFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_register"
>

<argument
.../>
</fragment>
</navigation>



细心的同学可能已经观察到navigation目录下的login_navigation.xml资源文件中的action标签和argument标签,这里需要解释一下:
点击Android studio中的Make Project按钮,可以发现系统为我们生成了两个类



WelcomeFragment中的JOIN US按钮点击事件:
btnRegister.setOnClickListener {
val action = WelcomeFragmentDirections
.actionWelcomeToRegister()
.setEMAIL("TeaOf1995@Gamil.com")
findNavController().navigate(action)
}

RegisterFragment中的接收:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// ...
val safeArgs:RegisterFragmentArgs by navArgs()
val email = safeArgs.email
mEmailEt.setText(email)
}
需要提及的是,如果不用Safe Argsaction可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)方式生成,感兴趣的同学可以自行编写。


三、更多


Navigation可以绑定menusdrawersbottom navigation,这里我们以bottom navigation为例,我先在navigation目录下新创建了main_navigation.xml,接着新建了MainActivity,下面则是activity_main.xml:

<LinearLayout
...>

<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
app:navGraph="@navigation/main_navigation"
app:defaultNavHost="true"
android:layout_height="0dp"
android:layout_weight="1"/>

<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:itemIconTint="@color/colorAccent"
app:itemTextColor="@color/colorPrimary"
app:menu="@menu/menu_main"/>

</LinearLayout>



MainActivity中的处理也十分简单:

class MainActivity : AppCompatActivity() {

lateinit var bottomNavigationView: BottomNavigationView

override fun onCreate(savedInstanceState: Bundle?) {
//...
val host: NavHostFragment = supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
val navController = host.navController
initWidget()
initBottomNavigationView(bottomNavigationView,navController)
}

private fun initBottomNavigationView(bottomNavigationView: BottomNavigationView, navController: NavController) {
bottomNavigationView.setupWithNavController(navController)
}

private fun initWidget() {
bottomNavigationView = findViewById(R.id.navigation_view)
}
}


作者:九心_
链接:https://www.jianshu.com/p/66b93df4b7a6
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

检测项目中是否包含UIWebView

苹果最近废弃了UIWebView的使用,所以要把工程中引用UIWebView的地方全换掉,不然每次提交审核都会发警告邮件,如下: ITMS-90809: Deprecated API Usage - App updates that use UIWebView...
继续阅读 »

苹果最近废弃了UIWebView的使用,所以要把工程中引用UIWebView的地方全换掉,不然每次提交审核都会发警告邮件,如下:

ITMS-90809: Deprecated API Usage - App updates that use UIWebView will no longer be accepted as of December 2020\. Instead, use WKWebView for improved security and reliability. Learn more https://developer.apple.com/documentation/uikit/uiwebview

After you’ve corrected the issues, you can upload a new binary to App Store Connect.


用到UIWebView的场景如下(包括字符串):


1.自己代码中使用了UIWebView控件。
2.第三方库中使用:
1). README.md等资源文件中使用。
这些文件是没有引入项目的,要在pod库里找到相应的库文件夹,然后 show in finder便能找到。
2). 第三方库的注释里有使用UIWebView字眼。
3). 第三方framewok、.a文件等包含UIWebView,都是二进制文件(Binary file),这种情况只能等第三方库更新SDK了。
3.工程的一些本地配置里包含了UIWebView
搜索结果: UserInterfaceState.xcuserstate matches
1.UserInterfaceState.xcuserstate是什么?
该文件为xcode默认自带文件,是xcode的配置信息,git会用这个文件记录下来。 比如:手动删除此文件,退出xcode后重启xcode,此文件会自动创建并跟踪, git push的时候一般忽略此文件



解决:
场景1:
直接搜索替换成WKWebView即可
场景2:
注释和README文件里使用的UIWebView字眼应该是没影响的。至于frameword和.a文件中包含的引用只能等第三方库更新了,例如Twitter的SDK。
场景3:
xcode的配置信息文件,对于打出来的包应该页没啥影响。为了保险起见,还是删掉此文件,然后让Xcode重新生成一个新的。


检测项目中是否包含UIWebView
1.打开终端,cd + 把项目的工程文件所在文件夹拖入终端(即 得到项目的工程文件所在的路径)
2.输入以下命令(注意最后有个点号,而且点号和 UIWebView 之间必须有一个空格):
grep -r UIWebView .
3.以上操作都正确的话,会马上出现工程中带有 UIWebView 的文件的列表(包括在工程中无法搜索到的 .a 文件中对UIWebView 的引用),如下:



替换TwitterKit
在pod文件中,把 pod 'TwitterKit' 替换为 pod 'TwitterKit5'
进入TwitterSDK的github地址 https://github.com/twitter-archive/twitter-kit-ios/issues/120,可以看到如下信息:



链接:https://www.jianshu.com/p/9c1507509896
收起阅读 »

iOS之切换UITabBar再次加载网络数据

我们在开发中,常常遇到这样的问题,点击某一个TabBar后,本TabBar上的控制器页面数据不刷新,原因是因为在App启动之后,第一次点击本TabBar后页面已经走了viewDidLoad,所以除了重新启动不会再次走viewDidLoad,如果把请求方法写在v...
继续阅读 »

我们在开发中,常常遇到这样的问题,点击某一个TabBar后,本TabBar上的控制器页面数据不刷新,原因是因为在App启动之后,第一次点击本TabBar后页面已经走了viewDidLoad,所以除了重新启动不会再次走viewDidLoad,如果把请求方法写在viewDidLoad中,当然不会再次触发啦,但是,苹果早考虑到这个问题,不用咱们写通知事件什么的,废话有点多了,看代码详解:

首先需要在本控制器签订TabBar的协议

UITabBarControllerDelegate

一定要看清楚协议,如果警报 Assigning to 'id<UITabBarControllerDelegate> _Nullable' from incompatible type 'RCFollowOrderViewController *const __strong'那么就证明你的协议签成了UITabBarDelegate

在viewDidLoad 请求一次 

[self requestdata];

协议方法:


//点击的时候触发的方法


-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController{


    if (self.tabBarController.selectedIndex==1) {


        [self requestdata];


    }


}



//防止同一个页面一直点击tabbar 的方法


-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController{


    UIViewController *tbselect=tabBarController.selectedViewController;


    if([tbselect isEqual:viewController]){


        returnNO;


    }


    returnYES;


}


如果想要点击TabBar一次,就刷新一次界面,就不写防止重复点击的代理方法,试下效果,搞定!


转自:https://www.jianshu.com/p/f52013ef1eea
收起阅读 »

iOS app唤起微信进行分享时出现“未验证应用”

昨天领导反馈app微信分享到朋友圈出现“未验证应用”的提示信息。通过追踪找到了解决办法。问题的原因由于苹果iOS 13系统版本安全升级,为此openSDK在1.8.6版本进行了适配。 1.8.6版本支持Universal Links方式跳转,对openSDK分...
继续阅读 »

昨天领导反馈app微信分享到朋友圈出现“未验证应用”的提示信息。通过追踪找到了解决办法。

问题的原因

由于苹果iOS 13系统版本安全升级,为此openSDK在1.8.6版本进行了适配。 1.8.6版本支持Universal Links方式跳转,对openSDK分享进行合法性校验。

PS:现在openSDK出了最新的版本1.8.7,新增了自检函数checkUniversalLinkReady:,可以帮助开发者排查SDK接入过程中遇到的问题,在哪一步出错了(共7步,见接入文档https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4)。

问题的解决办法

1.配置Universal Links

1)常见并编辑一个名为apple-app-site-association,无需后缀名,务必符合标准的json格式,格式如下:

{

  "applinks": {

"apps":[],

"details": [

  {

"appID": "你的app的teamID + Bundle Identifier",

"paths": ["*"]

  }

]

  }

}

2)将apple-app-site-association文件发给服务器端的同事,让他上传到域名的根目录下或者.well-known的子目录下(这里的域名必须要是可访问的域名,由服务器端的同事给到)。

2.在app里面配置通用链接

1)首先检查一下Xcode-Targets-Signing&Capabilities 是否有Associated Domains,如果没有,需要去开发者账号在identifer里选择跟当前Xcode所用bundle identifier相同的那一组,进去之后,将Associated Domains前面的方框打上勾,如果已经打勾了,配置如下:




2)实现AppDelegate里支持通用链接的实现方法

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler {

  return [WXApi handleOpenUniversalLink:userActivity delegate:self];

}

3)修改微信注册方法

由[WXApi registerApp:kAppid]改为[WXApi registerApp:kAppid universalLink:kUniversalLinks],这里的universal links为第一步第2)里服务器同事给的链接地址

4)配置info.plist

这里如果是从旧版更新WechatOpenSDK1.8.6/7版本的话,需要在这个里面调用微信里的这个方法,并且在Xcode中,选择你的工程设置项,选中“TARGETS”一栏,在 “info”标签栏的“LSApplicationQueriesSchemes“添加weixin和weixinULAPI

5) 微信开放平台配置Universal links

需要把服务器同事给的地址填写到app iOS信息Universal Links,同时app的下载地址也一定要填写app在app store的地址,填写好后保存,开放平台需要审核,可能要等一段时间Universal Links才能生效,我就是昨天下午设置好没生效,今天早上来才生效。

3.检查是否配置好的Universal links是否生效

生效的标准结合这两个文档https://docs.qq.com/doc/DZHNvSGJLS3pqbHNl的步骤和https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4提供的自检过程

4.如果以上步骤都已经完成,并且自检正确,再分享出去,就不会再出现“未验证的应用”字样了。

PS:上周五(8-7)按照上述步骤,不会出现“未验证的应用”字样,到了周一(8-11)分享,发现又会出现,本人手机系统iOS13.5.1,微信7.0.14;换了同事的手机iOS13.1.2,微信7.0.14发现她的就正常,这个问题需要持续关注,个人觉得是微信sdk的bug;打包上线后,等到用户用一段时间后,新版本放量上去让整体错误率下降到90%以下才会从未验证应用中移除,问题得到彻底解决。


转自:https://www.jianshu.com/p/8e2f06d8f45a 收起阅读 »