注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

你确定你会写代码---iOS规范补充

Pod update注意1、先执行pod repo update 公司内部库specs2、再执行pod update --no-repo-update这样就不会update github_specs,速度快JSONSerialization涉及到JSON Ob...
继续阅读 »

Pod update注意

1、先执行pod repo update 公司内部库specs
2、再执行pod update --no-repo-update这样就不会update github_specs,速度快

JSONSerialization

涉及到JSON Object<->NSData数据转换的地方,注意对NSError的处理和JSON Object合法性的校验,如:

BOOL validate = [NSJSONSerialization isValidJSONObject:parament];
if (!validate) {
// 对不是合法的JSON对象错误进行处理
return;
}
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parament options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
// 对数据转换错误进行处理
return;
}

合法JSON对象满足:
1、Top level object is an NSArray or NSDictionary
2、All objects are NSString, NSNumber, NSArray, NSDictionary, or NSNull
3、All dictionary keys are NSStrings
4、NSNumbers are not NaN or infinity

补充一些代码规范、开发约定

写if else语句时可以else不换行紧跟if的}括号,但写if else if时,为了保持条件{}的可读性,务必请换行书写:

// if else
BOOL flag = YES;
if (flag) {

} else {

}

// if else if
BOOL flag = YES;
BOOL elseIfFlag = (1+1-1+2 == 5);
if (flag) {

}
// 这里换行书写
else if(elseIfFlag) {

}

对于@property声明的属性,如果初始化设置复杂,请采用懒加载getters方式,对于简单初始化的,应在.m文件中提供统一的-initData初始化数据的方法。

/// 懒加载方式-内部配置
@property(nonatomic, strong)UIView *redView;
/// 统一初始化
@property(nonatomic, strong)NSMutableArray *dataSourceArray;

/// 统一数据初始化
- (void)initData{
_dataSourceArray = [NSMutableArray new];
}

/// 懒加载
- (UIView *)redView{
if (!_redView) {
_redView = [UIView new];
_redView.backgroundColor = UIColor.redColor;
}
return _redView;
}

对于NSDictionary、NSArray等的初始化,为提高可读性起见,建议采用语法糖的初始化方式:

_dataSourceArray = [@[@"1", @"2"] mutableCopy];
_parameters = [@{@"action": @"add", @"id": @"22"} mutableCopy];

// X: 不推荐这样做
_dataSourceArray = [NSMutableArray new];
[_dataSourceArray addObject:@"1"];
[_dataSourceArray addObject:@"2"];

_parameters = [NSMutableDictionary new];
[_parameters setValue:@"add" forKey:@"action"];
[_parameters setValue:@"22" forKey:@"id"];

对于Category中的对外公有方法,务必采用categoryName_funcName的命名方式,以区别于主类里没有前缀的方法:

// TALPlayer+LogReport.h

/// 加载播放器
- (void)logReport_loadPlayer;
/// 开始播放
- (void)logReport_startPlay;

对于Category里的私有同名方法,可采用下划线方式如_mainClassFuncName以区别.
对于主类里的私有属性,在多个Category访问时,可采用属性中间件的方式,拆出一个独立的MainClass+InternalProperty来提供一些getters方法:

// TALPlayer+InternalProperty.h

- (TALPlayerLogModel *)internalProperty_logModel;
- (TALPlayerStaticsModel *)internalProperty_staticsModel;


// TALPlayer+InternalProperty.m

- (TALPlayerLogModel *)internalProperty_logModel{
// 这里为方便以后调试断点用,建议拆开2行写
id value = [self valueForKey:@"logModel"];
return value;
}

对于需要跟服务器交互的网络请求参数字符串,务必独立出对应Category的DataInfoKeys扩展文件,方便查询、注释、全局引用、修改和拼写纠错:

// TALPlayer+LogReportDataInfoKeys.h

/// action
extern NSString *const LogReportDataInfoActionKey;
/// 心跳
extern NSString *const LogReportDataInfoActionHeartBeatKey;
/// 严重卡顿
extern NSString *const LogReportDataInfoActionSeriousBufferKey;


// TALPlayer+LogReportDataInfoKeys.m

// action
NSString *const LogReportDataInfoActionKey = @"action";
/// 心跳
NSString *const LogReportDataInfoActionHeartBeatKey = @"heartbeat";
/// 严重卡顿
NSString *const LogReportDataInfoActionSeriousBufferKey = @"seriousbuffer";


// 使用
#import "TALPlayerLogReportDataInfoKeys.h"

NSMutableDictionary *info = [NSMutableDictionary new];
info[LogReportDataInfoActionKey] = LogReportDataInfoActionHeartBeatKey;
// info[XXXKey] = value;

对于.h及.m文件中默认#pragma mark的规范,推荐如下:

// XXX.h

#pragma mark - Protocol

#pragma mark - Properties

#pragma mark - Methods

// XXX.m

#pragma mark - Consts

#pragma mark - UI Components

#pragma mark - Data Properties

#pragma mark - Initial Methods

#pragma mark - Lifecycle Methods

#pragma mark - Override Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - XXXDelegate

#pragma mark - Getters

#pragma mark - Setters

如上相关#pragma字符在Xcode中的自动配置,有机会我会单独分享给大家。
Xcode FileTemplate路径:Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/Source/Cocoa Touch Class.xctemplate/


对于OC文件中注释规范说明:

//MARK: mark here(类似于#pragma mark,只不过没有横线)

//TODO: todo here

//FIXME: fix me later

//???: What is this shit

//!!!: DO NOT TOUCH MY CODE

说明:
对于单行注释,尽量用//代替/**/格式,,务必请在//后加一个空格,再进行内容补充,如:// 这是单行注释而不要写成//这是单行注释
对于SDK内部私有方法,如果无参数,则采用/// 这是无参数方法注释格式;
对于SDK需要向外暴露的接口方法注释,请务必按照AppleDoc编写,写明@param、@return等:

/**
* 这是一个demo方法
* @param param1 第一个参数
* @param param2 第二个参数
* @return BOOL值
*/
- (BOOL)thisIsADemoFuncWithParam1: (NSString *)param1
param2: (NSInteger)param2{
return NO;
}

1、对于@property申明的SDK公开属性,务必写成/* 这是SDK公开属性注释 */,方便调用时Xcode提示;

2、对于SDK内部使用的属性,最好写成/// 这是属性注释而不是/**/;

3、另外,务必让对于>=2个参数的方法,各个参数折行对齐,务必保持.h和.m方法参数格式一致;

4、对于方法名的统一性说明:

  4.1 如果方法是功能性的,处理某些事件、计算、数据处理等的私有方法,则可定义方法名为handleXXX:,如-handleRedBtnClick:、-handleResponsedData:、-handlePlayerEvent:等;

  4.2 对于一些需要暴露的公有方法,则命名最好按照n的功能命名,如对于一个TALPlayer它可以play、stop、resume等;

  4.3 对于可以switch两种状态切换的状态方法,最好命名为toggleXXX:(BOOL)on,如- (void)toggleMute:(BOOL)on;

5、对于一些状态描述性的属性,可以用needs、is、should、has+adj/vb组合形式,如needsAutoDisplay、shouldAutoRotateOrientation、isFinished、hasData或hasNoData等;

对于一些NS_ENUM枚举定义,务必遵循统一前缀方式:

typedef enum : NSUInteger {
TALPlayerEventA = 0,
TALPlayerEventB,
TALPlayerEventC,
} TALPlayerEvent;
// 或者
typedef NS_ENUM(NSUInteger, MyEnum) {
MyEnumValueA,
MyEnumValueB,
MyEnumValueC,
};

6、对于一些全局宏的定义,务必SDK前缀全大写_NAME_组合如TALPLAYER_GLOBAL_MACRO_NAME,对于const类型的常量,务必加上k前缀,命名为kConstValue;

7、对于一些typedef的Block,命名最好指明Block的类别+用途,如TALPlayerLogReportHandler,如果有功能性区分的话,则可以定义为TALPlayerLogReportCompletionHandler、TALPlayerLogReportSuccessHandler、TALPlayerLogReportFailureHandler注意是名词组合形式;

8、调用Block时,一定要对block对象进行nil值判断,防止崩溃handler ? handler() : nil;

9、所有对于NSString校验的地方,都应该校验其length > 0,而不是!str;

10、所有对于NSURL校验的地方,都应该校验其[URL.scheme.lowercaseString isEqualToString: @"https"]方式,而不是!URL;

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

收起阅读 »

iOS Cateogry的深入理解&&initialize方法调用理解(二)

上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调...
继续阅读 »
  • 上一篇文章我们讲到了load方法,今天我们来看看initialize

新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下

将原来的load方法换成initialize






先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize

    1. 相信大家在想什么叫第一次接收消息了,我们回到main()





说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码

从输出结果可以看到没有任何关于initialize的打印,程序直接退出

  • 2.initialize的打印

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
}
return 0;
}
2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印

load是直接函数指针直接调用,类,分类,继承等等

[TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用


多次向TCPerson发送消息的输出结果

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}
2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

initialize只会调用一次

我们再来看看继承关系中,initialize的调用

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCStudent alloc];

}
return 0;
}

输出结果:

2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0


从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}
020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

#import "TCStudent.h"

@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}
2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0
如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

#import "TCStudent.h"

@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"

@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"

@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end
2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0




作者:枫紫_6174
链接:https://www.jianshu.com/p/f0150edc0f42


收起阅读 »

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

首先先看几个面试问题Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类2.新建一个TCStudent类继承自TCPerson,并且给T...
继续阅读 »

首先先看几个面试问题

  • Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?

1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类


2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类


Cateogry里面有load方法么?

  • 答:分类里面肯定有load

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{

}
@end
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{

}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{

}
@end

load方法什么时候调用?

load方法在runtime加载类和分类的时候调用load

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

}
return 0;
}

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
@end


@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
@end
@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
@end
可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出

从输出结果我们可以看出,三个load方法都被调用

问题:分类重写方法,真的是覆盖原有类的方法么?如果不是,到底分类的方法是怎么调用的?

  • 首先我们在TCPerson申明一个方法+ (void)test并且在它的两个分类都重写+ (void)test


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject
+ (void)test;
@end

NS_ASSUME_NONNULL_END

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
+ (void)test{
NSLog(@"TCPerson +test");
}
@end
分类重写test
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest1) +test1");
}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest2) +test2");
}
@end

在main里面我们调用test

#import <Foundation/Foundation.h>
#import "TCPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
}
return 0;
}

从输出结果中我们可以看到,只有分类2中的test被调用,为什么只调用分类2中的test了?





因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(拖动文件就行了)




细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test覆盖了,所以才出现这种情况么?

我们打印TCPerson的类方法

void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);

// 存储方法名
NSMutableString *methodNames = [NSMutableString string];

// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}

// 释放
free(methodList);

// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
printMethodNamesOfClass(object_getClass([TCPerson class]));
}
return 0;
}


可以看到,TCPerson的所有类方法名,并不是覆盖,三个load,三个test,方法都在

load源码分析:查看objc底层源码我们可以看到:

void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}
load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用
我们查看call_class_loads()方法

static void call_class_loads(void)
{
int I;

// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}

// Destroy the detached list.
if (classes) free(classes);
}
其通过的是load_method_t函数指针直接调用
函数指针直接调用
typedef void(*load_method_t)(id, SEL);

其分类load方法调用也是一样

static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;

// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;

cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}

为什么test不一样了

因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的

load只在加载类的时候调用一次,且先调用类的load,再调用分类的

load的继承关系调用
首先我们先看TCStudent
#import "TCStudent.h"

@implementation TCStudent

@end

不写load方法调用

TCStudent写上load


从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的

总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



作者:枫紫
链接:https://www.jianshu.com/p/f66921e24ffe









收起阅读 »

在iOS中运用React Component的思路,效率更高的开发UI,更好的复用UI组件

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺...
继续阅读 »

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺带着把ES6的一些新特性也了解了一下,更好的函数调用方式,Class的引入,Promise的运用等等,其实最吸引我的还是在用了Weex之后,感受到了Component带来的UI复用,高效开发的快感。Weex是运用Vue.js来调用,渲染native控件,来达到one code, run everywhere。不管是Vue.js,还是React,最终都是朝着W3C WebComponent的标准走了(今年会发布的Vue 3.0在组件上的语法基本上跟React一样了)。这篇就来讲讲我对React Component的理解,还有怎么把这个标准也能在native上面做运用

demo源码

iOS UI开发的痛点

对iOS开发来说,最常用的UI组件就是UICollectionView了,就是所谓的一个列表页,现在的app大部分页面都是由一个列表来呈现内容的。对iOS开发者来说,我们可以封装每个UICollectionViewCell,从而可以在每个页面的UICollectionView中能够复用,但是痛点是,这个复用仅仅是UI上的复用,在每写一个新的页面(UIViewController)的时候,还是需要新建一个UICollectionView,然后再把UICollectionView的DataSource和Delegate方法再实现一遍,把这些Cell再在这些方法里重新生成一遍,才能让列表展现出来。比方说我们首页列表底部有猜你喜欢的cell,个人中心页面底部也有猜你喜欢的cell,这两个页面,都需要在自己拥有的UICollectionView中注册这个猜你喜欢的cell,返回这个猜你喜欢cell的高度,设置这个cell的model并刷新数据,如果有Header或者Footer的话,还得重新设置这些Header跟Footer。所以新写一个列表页面,对iOS开发者来说,还是很麻烦。

使用Weex或者RN开发原生列表页

使用Weex开发列表页的时候,我们组内的小伙伴都觉得很爽,很高效,基本上几行代码就能绘制出一个列表页,举个RN和weex的例子

// React
render() {
const cells = this.state.items.map((item, index) => {
if (item.cellType === 'largeCell') {
return <LargeCell cellData={item.entity}></LargeCell>
} else if (item.cellType === 'mediumCell') {
return <MediumCell cellData={item.entity}></MediumCell>
} else if (item.cellType === 'smallCell') {
return <SmallCell cellData={item.entity}></SmallCell>
}
});

return(
<Waterfall>
{ cells }
</Waterfall>
);
}

// Vue
<template>
<waterfall>
<cell v-for="(item, index) in itemsArray" :key="index">
<div class="cell" v-if="item.cellType === 'largeCell'">
<LargeCell :cellData="item.entity"></LargeCell>
</div>
<div class="cell" v-if="item.cellType === 'mediumCell'">
<MediumCell :cellData="item.entity"></MediumCell>
</div>
<div class="cell" v-if="item.cellType === 'smallCell'">
<SmallCell :cellData="item.entity"></SmallCell>
</div>
</cell>
</waterfall>
</template>

const

waterfall对应的就是iOS中的UICollectionView,waterfall这个组件中有cell的子组件,这些cell的子组件可以是我们自己定义的不同类型样式的cell组件。LargeCell,MediumCell,SmallCell对应的就是原生中的我们自定义的UICollectionViewCell。这些Cell子组在任何waterfall组件下面都可以使用,在一个waterfall组件下面,我们只需要把我们把在这个列表中需要展示的cell放进来,通过props把数据传到cell组件中即可。这种方式对iOS开发者来说,真的是太舒服了。在觉得开发很爽的同时,我也在思考,既然这种Component的方式用起来很爽,那么能不能也运用到原生开发中呢?毕竟我们大部分的业务需求还是基于原生来开发的。

React的核心思想

1、先来解释下React中的React Element和React Component
1.1、React Elements

const element = <div id='login-button>Login</div>

这段JSX表达式返回的就是一个React Element,React element描述了用户将在屏幕上看到的那个UI,跟DOM elements不一样的是,React elements是一个单纯的对象,仅仅是对将要呈现到屏幕上的UI的一个描述,并不是真正渲染好的UI,创建一个React element开销是极其小的,渲染的事情是由背后的React DOM来处理的。上面的那段代码相当于:

const element = React.createElement(
'div',
{id: 'login-button'},
'Login'
)

返回的React element对象相当于 =>

{
type: 'div',
props: {
children: 'Login',
id: 'login-button'
}
}

1.2 React Components

React中最核心的一个思想就是Component了,官方的解释是Component允许我们将UI拆分为独立可复用的代码片段,组件中可以包含多个其他组件,这样将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性(Readability)、可维护性(Maintainability)、可复用性(Reusability)和可测试性(Testability)。这也是 React 里用 Component 抽象所有 UI 的意义所在。

class Button extends React.Component {
render() {
const element = <div id='login-button>{ this.props.title }</div>
return (
<div>
{ element }
</div>
)
}

这段代码中Button就是一个React Component,这个component接受一个叫props的参数,返回描述UI的React element。

2、可以看出React Component接受props是一个对象,也就是所谓的一种数据结构,返回React Element也是一种对象,所谓的另外一种数据结构,所以我认为的React Component其实就是一个function,这个function的主要功能就是将一种数据结构(描述原始数据)转换成另外一种数据结构(描述UI)。React element仅仅是一个描述UI的对象,可以认为是一个中间状态,我们可以用最小的开销来创建或者销毁element对象。

3、React的核心思想总结下来就是这样的一个流程
1、原始数据到UI数据的转化 props -> React Component -> React Element
2、React Element的作用是将Component的创建跟描述状态分离,Component内部主要负责这个Component的构建,React Element主要用来做描述这个Component的状态
3、多个Component返回的多个Elements,这个流程是进行UI组合
4、React Element并不是一个渲染结果,React DOM的作用是将UI的状态(即Element)和UI的渲染分离,React DOM负责element的渲染
5、最后一个流程就是UI渲染了
上述这几个流程基本上代表了React的核心概念

怎么在iOS中运用React Component概念

说了这么多,其实iOS中缺少的就是这个Component概念,iOS原生的流程是原始数据到UI布局,再到UI绘制。复用的只是UI绘制结果的那个view(e.g. UICollectionViewCell)

在使用UICollectionView的时候,我们的数据都是通过DataSource方法返回给UICollectionView,UICollectionView拿到这些数据之后,就直接去绘制UICollectionViewCell了。所以每个列表页都得重新建一个UICollectionView,再引入自定义的UICollectionViewCell来绘制列表,所有的DataSource跟Delegate方法都得走一遍。所以我在想,我们可以按照React的那种方式来绘制列表么?将一个个UI控件抽象成一个个组件,再将这些组件组合到一起,绘制出最后的页面,React或者Weex的绘制列表其实就是waterfall这个列表component里面按照列表顺序插入自定义的cell component(组合)。那么我们其实可以在iOS中也可以有这个waterfall的component,这个component支持一个insertChildComponent:的方法,这个方法里就是插入自定义的CellComponent到waterfall这个组件中,并通过传入props来创建这个component。所以我就先定义了一个组件的基类BaseComponent

@protocol ComponentProtocol <NSObject>

/**
* 绘制组件
*
* @param view 展示该组件的view
*/
- (void)drawComponentInView:(UIView *)view withProps:(id)props;

/**
* 组件的尺寸
*
* @param props 该component的数据model
* @return 该组件的size
*/
+ (CGSize)componentSize:(id)props;

@end

@interface BaseComponent : NSObject <ComponentProtocol>

- (instancetype)initWithProps:(id)props;

@property (nonatomic, strong, readonly) id props;

所有的Component的创建都是通过传入props参数,来返回一个组件实例,每个Component还遵守一个ComponentProtocol的协议,协议里两个方法:

1、- (void)drawComponentInView:(UIView *)view withProps:(id)props; 每个component通过这个方法来进行native控件的绘制,参数中view是将会展示该组件的view,比方说WaterfallComponent中的该方法view为UIViewController的view,因为UIViewController的view会用来展示WaterfallComponent这个组件,'props'是该组件创建时传入的参数,这个参数用来告诉组件应该怎样绘制UI
2、+ (CGSize)componentSize:(id)props; 来描述组件的尺寸。

有了这个Component概念之后,我们原生的绘制流程就变成

1、创建Component,传入参数props
2、Component内部执行创建代码,保存props
3、当页面需要绘制的时候(React中的render命令),component内部会执行- (void)drawComponentInView:(UIView *)view withProps:(id)props;方法来描述并绘制UI

原生代码中想实现React element,其实不是一件简单的事情,因为原生没有类似JSX这种语言来生成一套只用来描述UI,并不绘制UI的中间状态的对象(可以做,比方说自己定义一套语法来描述UI),所以目前我的做法是在component内部,等到绘制命令来了之后,通过在- (void)drawComponentInView:(UIView *)view withProps:(id)props方法中,调用原生自定义的UIKit控件,通过props来绘制该UIKit

所以将通过封装component的方式,我们之前UIKit代表的UI组件转换成组件,把这些组件一个个单独抽离出来,再通过搭积木的方式,将各种组件一个个组合到一起,怎么绘制交给component内部去描述,而不是交给每个页面对应的UIViewController

Demo

Demo中,我会创建一个WaterfallComponent组件,还有多个CellComponent来绘制列表页,每个不一样列表页面(UIViewController)都可以创建一个WaterfallComponent组件,然后将不一样的CellComponent按照顺序插入到WaterfallComponent组件中,即可完成绘制列表,不需要每个页面再去处理UICollectionView的DataSource,Delegate方法。


WaterfallComponent内部会有一个UICollectionView,WaterfallComponent的insertChildComponent方法中,会创建一个dataController来管理数据源,并用来跟UICollectionView的DataSource方法进行交互从而绘制出列表页,最终UIViewController中绘制列表的方法如下:

self.waterfallComponent = [[WaterfallComponent alloc] initWithProps:nil];

for (NSDictionary *props in datas) {
if ([props[@"type"] isEqualToString:@"1"]) {
FirstCellComponent *cellComponent = [[FirstCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
} else if ([props[@"type"] isEqualToString:@"2"]) {
SecondCellComponent *cellComponent = [[SecondCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
}
}
[self.waterfallComponent drawComponentInView:self.view withProps:nil];

这样,每个我们自定义的Cell就可以以CellComponent的形式,被按照随意顺序插入到WaterfallComponent,从而做到了真正意义上的复用,Demo已上传到GitHub上,有兴趣的可以看

总结

React的核心思想是将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性、可维护性、可复用性和可测试性。这也是 React 里用 Component 抽象所有 UI 的意义所在。
原生开发中,使用Component的概念,用Component去抽象UIKit控件,也能达到同样的效果,这样也能统一每个开发使用UICollectionView时候的规范,也能统一对所有列表页的数据源做一些统一处理,比方说根据一个逻辑,统一在所有列表页,插入一个广告cell,这个逻辑完全可以在WaterfallComponent里统一处理。

思考

目前我们只用到了Component这个概念,其实React中,React Element的概念也是非常核心的,React Element隔离了UI描述跟UI绘制的逻辑,通过JSX来描述UI,并不去生成,绘制UI,这样我们能够以最小的代价来生成或者销毁React Elements,然后在交付给系统绘制elements里描述的UI,那么如果原生里也有这一套模板语言,那么我们就能真正做到在Component里,传入props,返回一个element描述UI,然后再交给系统去绘制,这样还能省去cell的创建,只创建CellComponent即可。其实我们可以通过定义一套语义去描述UI布局,然后通过解析这套语义,通过Core Text去做绘制,这一套还是值得我再去思考的。

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

收起阅读 »

Swift 5.0 值得关注的特性:增加 Result<T, E: Error> 枚举类型

HackingSwift: What’s new in Swift 5.0Result<T> 还是 Result<T, E: Error>背景在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 er...
继续阅读 »

HackingSwift: What’s new in Swift 5.0
Result<T> 还是 Result<T, E: Error>

背景

在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 error。因为数据可能获取成功,也可能失败。因此回调中的数据和错误都是 optional 类型。
比如 CloudKit 中保存数据的一个函数就是这样:

func save(_ record: CKRecord, completionHandler: @escaping (CKRecord?, Error?) -> Void)

这种形式的缺点是没有体现出两种结果的互斥关系:如果数据成功获取到了,那么 error 一定为空。如果 error 有值,数据一定是获取失败了。

Swift 中枚举的能力相比 OC 有着很大的进步,每个枚举值除了可以是常规的基础类型,还可以是一个关联的类型。有了这样的特性后用枚举来优化返回结果的数据结构显得水到渠成:

enum Result<Success, Failure> where Failure : Error {

/// A success, storing a `Success` value.
case success(Success)

/// A failure, storing a `Failure` value.
case failure(Failure)
}

基本用法

定义异步返回结果是 Int 类型的函数:

func fetchData(_ completionHandler: @escaping (Result<Int, Error>) -> Void) {
DispatchQueue.global().async {
let isSuccess = true
if isSuccess {
let resultValue = 6
return completionHandler(.success(resultValue))
} else {
let error = NSError(domain: "custom error", code: -1, userInfo: nil)
return completionHandler(.failure(error))
}
}
}

返回值的类型通过泛型进行约束,Result 第一个泛型类型表示返回值的类型,第二个类型表示错误的类型。对 Result 赋值和常规的枚举一样:

let valueResult: Result<Int, CustomError> = Result.success(4)

// 因为 swift 中会进行类型推断,编译器在确认返回的是 `Result` 类型后,可以省略枚举类型的声明
let errorResult = .failure(CustomError.inputNotValid)

取出 Result 值和获取普通的关联类型枚举是一样的:

fetchData { (result) in
switch result {
case .success(let value):
print(value)
case .failure(let error)
print(error.localizedDescription)
}
}

如果你只想要获取其中一项的值,也可以直接用 if case 拆包:

fetchDate { (result) in
if case .success(let value) = result {
print(value)
}
}

可以判等

Enum 是一个值类型,是一个值就应该可以判断是否相等。如果 Result 的成功和失败的类型都是 Equatable,那么 Result就可以判等,源码如下:

extension Result : Equatable where Success : Equatable, Failure : Equatable { }

类似的,如果是成功和失败的类型都是 Hashable,那么 Result 也是 Hashable:

extension Result : Hashable where Success : Hashable, Failure : Hashable { }

如果实现了 Hashable ,可以用来当做字典的 key。

辅助的 API

map、mapError
与 Dictionary 类似,Swift 为 Result 提供了几个 map value 和 error 的方法。

let intResult: Result<Int, Error> = Result.success(4)
let stringResult = x.map { (value) -> Result<String, Error> in
return .success("map")
}

let originError = NSError(domain: "origin error", code: -1, userInfo: nil)
let errorResult: Result<Int, Error> = .failure(originError)
let newErrorResult = errorResult.mapError { (error) -> Error in
let newError = NSError(domain: "new error", code: -2, userInfo: nil)
return newError
}

flatMap、flatMapError
map 返回的是具体的结果和错误, flatMap 闭包中返回的是 Result 类型。如果 Result 中包含的是数据,效果和 map 一致,替换数据;如果 Result 中包含的是错误,那么不替换结果。

let intResult: Result<Int, Error> = Result.success(4)

// 替换成功
let flatMapResult = intResult.flatMap { (value) -> Result<String, Error> in
return .success("flatMap")
}

// 没有执行替换操作,flatMapIntResult 值还是 intResult
let flatMapIntResult = intResult.flatMap { (value) -> Result<String, Error> in
return .failure(NSError(domain: "origin error", code: -1, userInfo: nil))
}

get
很多时候只关心 Result 的值,Swift 提供了 get() 函数来便捷的直接获取值,需要注意的是这个函数被标记为 throws,使用时语句前需要加上 try:

let intResult: Result<Int, Error> = Result.success(4)

let value = try? intResult.get()

可抛出异常的闭包初始化器

很多时候获取返回值的闭包中可能会发生异常代表获取失败的错误,基于这个场景 Swift 提供了一个可抛出异常的闭包初始化器:

enum CustomError: Error, Equatable {
case inputNotValid
}

let fetchInt = { () -> Int in
if true {
return 4
} else {
throw CustomError.inputNotValid
}
}

let result: Result<Int, Error> = Result { try fetchInt() }

需要提醒是通过这种方式声明的 Result 的 error 类型只能是 Error,不能指定特定的 Error。

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

收起阅读 »

运行时Hook所有Block方法调用的技术实现

1.方法调用的几种Hook机制iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。当我们想Hook一...
继续阅读 »

1.方法调用的几种Hook机制

iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。

当我们想Hook一个OC类的某些具体的方法时可以通过Method Swizzling技术来实现、当我们想Hook动态库中导出的某个C函数时可以通过修改导入函数地址表中的信息来实现(可以使用开源库fishhook来完成)、当我们想Hook所有OC类的方法时则可以通过替换objc_msgSend系列函数来实现。。。

那么对于Block方法呢而言呢?

2.Block的内部实现原理和实现机制简介

这里假定你对Block内部实现原理和运行机制有所了解,如果不了解则请参考文章《深入解构iOS的block闭包实现原理》或者自行通过搜索引擎搜索。

源程序中定义的每个Block在编译时都会转化为一个和OC类对象布局相似的对象,每个Block也存在着isa这个数据成员,根据isa指向的不同,Block分为__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三种类型。也就是说从某种程度上Block对象也是一种OC对象。下面的类图描述了Block类的层次结构。


Block类以及其派生类在CoreFoundation.framework中被定义和实现,并且没有对外公开。

每个Block对象在内存中的布局,也就是Block对象的存储结构被定义如下(代码出自苹果开源出来的库实现libclosure中的文件Block_private.h):

//需要注意的是下面两个只是模板,具体的每个Block定义时总是按这个模板来定义的。

//Block描述,每个Block一个描述并定义在全局数据段
struct Block_descriptor_1 {
uintptr_t reserved; //记住这个变量和结构体,它很重要!!
uintptr_t size;
};

//Block对象的内存布局
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1 *descriptor;
// imported variables,这里是每个block对象的特定数据成员区域
};

这里要关注一下struct Block_descriptor_1中的reserved这个数据成员,虽然系统没有用到它,但是下面就会用到它而且很重要!

在了解了Block对象的类型以及Block对象的内存布局后,再来考察一下一个Block从定义到调用是如何实现的。就以下面的源代码为例:

int main(int argc, char *argv[])
{
//定义
int a = 10;
void (^testblock)(void) = ^(){
NSLog(@"Hello world!%d", a);
};

//执行
testblock();

return 0;
}

在将OC代码翻译为C语言代码后每个Block的定义和调用将变成如下的伪代码:

//testblock的描述信息
struct Block_descriptor_1_fortestblock {
uintptr_t reserved;
uintptr_t size;
};

//testblock的布局存储结构体
struct Block_layout_fortestblock {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1_fortestblock *descriptor;
int m_a; //外部的传递进来的数据。
};

//testblock函数的实现。
void main_invoke_fortestblock(struct Block_layout_fortestblock *cself)
{
NSLog(@"Hello world!%d", cself->m_a);
}

//testblock对象描述的实例,存储在全局内存区
struct Block_descriptor_1_fortestblock _testblockdesc = {0, sizeof(struct Block_layout_fortestblock)};

int main(int argc, char *argv[])
{
//定义部分
int a = 10;
struct Block_layout_fortestblock testblock = {
.isa = __NSConcreteStackBlock,
.flags =0,
.reserved = 0,
.invoke = main_invoke_fortestblock,
.descriptor = & _testblockdesc,
.m_a = a
};

//调用部分
testblock.invoke();

return 0;
}

可以看出Block对象的生成和调用都是在编译期间就已经固定在代码中了,它不像其他OC对象调用方法时需要通过runtime来执行间接调用。并且线上程序中所有关于Block的符号信息都会被strip掉。所以上述的所介绍的几种Hook方法都无法Hook住一个Block对象的函数调用。

如果想要Hook住系统的所有Block调用,需要解决如下几个问题:
a. 如何在运行时将所有的Block的invoke函数替换为一个统一的Hook函数。
b. 这个统一的Hook函数如何调用原始Block的invoke函数。
c. 如何构建这个统一的Hook函数。

3.实现Block对象Hook的方法和原理

一个OC类对象的实例通过引用计数来管理对象的生命周期。在MRC时代当对象进行赋值和拷贝时需要通过调用retain方法来实现引用计数的增加,而在ARC时代对象进行赋值和拷贝时就不再需要显示调用retain方法了,而是系统内部在编译时会自动插入相应的代码来实现引用计数的添加和减少。不管如何只要是对OC对象执行赋值拷贝操作,最终内部都会调用retain方法。

Block对象也是一种OC对象!!

每当一个Block对象在需要进行赋值或者拷贝操作时,也会激发对retain方法的调用。因为Block对象赋值操作一般是发生在Block方法执行之前,因此我们可以通过Method Swizzling的机制来Hook 类的retain方法,然后在重写的retain方法内部将Block对象的invoke数据成员替换为一个统一的Hook函数!

通过考察__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三个类的实现发现这三个类都重载了NSObject的retain方法,这样在执行Method Swizzling时就不需要对NSObject的retain方法执行替换,而只要对上述三个类的retain执行替换即可。

你可以说出为什么这三个派生类都会对retain方法进行重载吗?答案可以从这三种Block的类型定义以及所表示的意义中去寻找。

Block技术不仅可以用在OC语言中,LLVM对C语言进行的扩展也能使用Block,比如gcd库中大量的使用了Block。在C语言中如果对一个Block进行赋值或者拷贝系统需要通过C库函数:

//函数声明在Block.h头文件汇总
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

来实现,这个函数定义在libsystem_blocks.dylib库中,并且库实现已经开源:libclosure。因此可以借助fishhook库来对__Block_copy这个函数进行替换处理,然后在替换的函数函数中将一个Block的原始的invoke函数替换为统一的Hook函数。

另外一个C语言函数objc_retainBlock,也是实现了对Block进行赋值时的引用计数增加,这个函数内部就是简单的调用__Block_copy方法。因此我们也可以添加对objc_retainBlock的替换处理。

解决了第一个问题后,接下来再解决第二个问题。还记得上面提到过的struct Block_descriptor_1中的reserved这个数据成员吗? 当我们通过上述的方法对所有Block对象的invoke成员替换为一个统一的Hook函数前,可以将Block对象的原始invoke函数保存到这个保留字段中去。然后就可以在统一的Hook函数内部读取这个保留字段中的保存的原始invoke函数来执行真实的方法调用了。

因为一个Block对象函数的第一个参数其实是一个隐藏的参数,这个隐藏的参数就是Block对象本身,因此很容易就可以从隐藏的参数中来获取到对应的保留字段。

下面的代码将展示通过方法交换来实现Hook处理的伪代码

struct Block_descriptor {
void *reserved;
uintptr_t size;
};

struct Block_layout {
void *isa;
int32_t flags; // contains ref count
int32_t reserved;
void *invoke;
struct Block_descriptor *descriptor;
};

//统一的Hook函数,这里以伪代码的形式提供
void blockhook(void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}
//模拟器下如果返回类型是结构体并且大于16字节那么第一个参数是返回值保存的内存地址,block对象变为第二个参数
void blockhook_stret(void *pret, void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}

//执行Block对象的方法替换处理
void replaceBlockInvokeFunction(const void *blockObj)
{
struct Block_layout *layout = (struct Block_layout*)blockObj;
if (layout != NULL && layout->descriptor != NULL){
int32_t BLOCK_USE_STRET = (1 << 29); //如果模拟器下返回的类型是一个大于16字节的结构体,那么block的第一个参数为返回的指针,而不是block对象。
void *hookfunc = ((layout->flags & BLOCK_USE_STRET) == BLOCK_USE_STRET) ? blockhook_stret : blockhook;
if (layout->invoke != hookfunc){
layout->descriptor->reserved = layout->invoke;
layout->invoke = hookfunc;
}
}
}

void *(*__NSStackBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSStackBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSStackBlock_retain_old(obj, cmd);
}

void *(*__NSMallocBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSMallocBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSMallocBlock_retain_old(obj, cmd);
}

void *(*__NSGlobalBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSGlobalBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSGlobalBlock_retain_old(obj, cmd);
}

int main(int argc, char *argv[])
{
//因为类名和方法名都不能直接使用,所以这里都以字符串的形式来转换获取。
__NSStackBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSStackBlock"), sel_registerName("retain"), (IMP)__NSStackBlock_retain_new, nil);
__NSMallocBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSMallocBlock"), sel_registerName("retain"), (IMP)__NSMallocBlock_retain_new, nil);
__NSGlobalBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSGlobalBlock"), sel_registerName("retain"), (IMP)__NSGlobalBlock_retain_new, nil);

return 0;
}

解决了第二个问题后,就需要解决第三个问题。上面的统一Hook函数blockhook和block_stret只是伪代码实现,因为任何一个Block中的函数的参数类型和个数是不一样的,而且统一Hook函数也需要在适当的时候调用原始的默认Block函数实现,并且不能破坏参数信息。为了解决这些问题就使得这个统一的Hook函数不能用高级语言来实现,而只能用汇编语言来实现。下面就是在arm64位体系下的实现代码:

.text
.align 5
.private_extern _blockhook
_blockhook:
//为了不破坏原有参数,这里将所有参数压入栈中
stp q6, q7, [sp, #-0x20]!
stp q4, q5, [sp, #-0x20]!
stp q2, q3, [sp, #-0x20]!
stp q0, q1, [sp, #-0x20]!
stp x6, x7, [sp, #-0x10]!
stp x4, x5, [sp, #-0x10]!
stp x2, x3, [sp, #-0x10]!
stp x0, x1, [sp, #-0x10]!
stp x8, x30, [sp, #-0x10]!

//这里可以添加任意逻辑来进行hook处理。

//这里将所有参数还原
ldp x8, x30, [sp], #0x10
ldp x0, x1, [sp], #0x10
ldp x2, x3, [sp], #0x10
ldp x4, x5, [sp], #0x10
ldp x6, x7, [sp], #0x10
ldp q0, q1, [sp], #0x20
ldp q2, q3, [sp], #0x20
ldp q4, q5, [sp], #0x20
ldp q6, q7, [sp], #0x20

ldr x16, [x0, #0x18] //将block对象的descriptor数据成员取出
ldr x16, [x16] //获取descriptor中的reserved成员
br x16 //执行reserved中保存的原始函数指针。
LExit_blockhook:

对于x86_64/arm32位系统来说,如果block函数的返回是一个结构体并且长度超过16字节(arm32是8字节)。那么block对象里面的flags属性就会设置为BLOCK_USE_STRET。而x86_64/arm32位系统对于这种返回类型的函数就会将返回值存放到第一个参数所指向的内存中,同时会把原本的block对象变化为第二个参数,因此需要对这种情况进行特殊处理。

关于在运行时Hook所有Block方法调用的技术实现原理就介绍到这里了。当然一个完整的系统可能需要其他一些能力:

1、如果你只想Hook可执行程序中定义的Block,那么请参考我的文章:深入iOS系统底层之映像操作API介绍 中的内容来实现Hook函数的过滤处理。
2、如果你不想借助Block_descriptor中的reserved来保存原始的invoke函数,那么可以参考我的文章:Thunk程序的实现原理以及在iOS中的应用(二)中介绍的技术来实现统一Hook函数以及完成对原始invoke函数的调用技术。

具体完整的代码可以访问我的github中的项目:YSBlockHook。这个项目以AOP的形式实现了真机arm64位模式下对可执行程序中所有定义的Block进行Hook的方法,Hook所做的事情就是在所有Block调用前,打印出这个Block的符号信息。

链接:https://www.jianshu.com/p/0a3d00485c7f

收起阅读 »

性能超高的UI库-AsyncDisplayKit

AsyncDisplayKit 已移动并重命名:Texture性能提升AsyncDisplayKit 的基本单位是node. ASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer。与只能在主线程上使用的视图不同,节...
继续阅读 »

AsyncDisplayKit 已移动并重命名:Texture

性能提升

AsyncDisplayKit 的基本单位是nodeASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer与只能在主线程上使用的视图不同,节点是线程安全的:您可以在后台线程上并行实例化和配置它们的整个层次结构。

为了保持其用户界面流畅和响应迅速,您的应用程序应以每秒 60 帧的速度呈现——这是 iOS 的黄金标准。这意味着主线程有六十分之一秒来推动每一帧。执行所有布局和绘图代码需要 16 毫秒!并且由于系统开销,您的代码在导致丢帧之前的运行时间通常不到 10 毫秒。

AsyncDisplayKit 允许您将图像解码、文本大小调整和渲染、布局和其他昂贵的 UI 操作移出主线程,以保持主线程可用于响应用户交互。


随着框架的发展,添加了许多功能,通过消除现代 iOS 应用程序中常见的样板样式结构,可以为开发人员节省大量时间。如果您曾经处理过单元格重用错误,尝试为页面或滚动样式界面高效地预加载数据,或者甚至只是试图防止您的应用丢失太多帧,您都可以从集成 ASDK 中受益。


详细的api介绍:

https://texturegroup.org/appledocs.html


常见问题及demo下载:

https://github.com/facebookarchive/AsyncDisplayKit


源码下载:




收起阅读 »

java设计模式:享元模式

前言在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。 例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些...
继续阅读 »

前言

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。


例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。


定义

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。


优点

相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。


缺点

为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
读取享元模式的外部状态会使得运行时间稍微变长。

享元模式的结构与实现

享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。

比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。


享元模式的本质是缓存共享对象,降低内存消耗。


结构

抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

享元模式的实现

应用实例的话,其实上面的模板就已经是一个很好的例子了,类似于String常量池,没有的对象创建后存在池中,若池中存在该对象则直接从池中取出。


  为了更好的理解享元模式,这里再举一个实例,比如接了我一个小型的外包项目,是做一个产品展示网站,后来他的朋友们也希望做这样的网站,但要求都有些不同,我们当然不能直接复制粘贴再来一份,有任希望是新闻发布形式的,有人希望是博客形式的等等,而且因为经费原因不能每个网站租用一个空间。


  其实这里他们需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这是造成服务器的大量资源浪费。如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源;而对于代码,由于是一份实例,维护和扩展都更加容易。


  那么此时就可以用到享元模式了。UML图如下:
  在这里插入图片描述
网站抽象类
 

 public abstract class WebSite {

public abstract void use();

}

具体网站类


public class ConcreteWebSite extends WebSite {

private String name = "";

public ConcreteWebSite(String name) {
this.name = name;
}

@Override
public void use() {
System.out.println("网站分类:" + name);
}

}

网络工厂类
  这里使用HashMap来作为池,通过put和get方法实现加入池与从池中取的操作。

public class WebSiteFactory {

private HashMap<String, ConcreteWebSite> pool = new HashMap<>();

//获得网站分类
public WebSite getWebSiteCategory(String key) {
if(!pool.containsKey(key)) {
pool.put(key, new ConcreteWebSite(key));
}

return (WebSite)pool.get(key);
}

//获得网站分类总数
public int getWebSiteCount() {
return pool.size();
}

}

Client客户端
  这里测试用例给了两种网站,原先我们需要做三个产品展示和三个博客的网站,也即需要六个网站类的实例,但其实它们本质上都是一样的代码,可以利用用户ID号的不同,来区分不同的用户,具体数据和模板可以不同,但代码核心和数据库却是共享的。

public class Client {

public static void main(String[] args) {
WebSiteFactory factory = new WebSiteFactory();

WebSite fx = factory.getWebSiteCategory("产品展示");
fx.use();

WebSite fy = factory.getWebSiteCategory("产品展示");
fy.use();

WebSite fz = factory.getWebSiteCategory("产品展示");
fz.use();

WebSite fa = factory.getWebSiteCategory("博客");
fa.use();

WebSite fb = factory.getWebSiteCategory("博客");
fb.use();

WebSite fc = factory.getWebSiteCategory("博客");
fc.use();

System.out.println("网站分类总数为:" + factory.getWebSiteCount());
}

}

源码中的享元模式

享元模式很重要,因为它能帮你在一个复杂的系统中大量的节省内存空间。在JAVA语言中,String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a=”abc”,其中”abc”就是一个字符串常量。


熟悉java的应该知道下面这个例子:


Stringa="hello";
Stringb="hello";
if(a==b)
 System.out.println("OK");
else
 System.out.println("Error");

输出结果是:OK。可以看出if条件比较的是两a和b的地址,也可以说是内存空间 核心总结,可以共享的对象,也就是说返回的同一类型的对象其实是同一实例,当客户端要求生成一个对象时,工厂会检测是否存在此对象的实例,如果存在那么直接返回此对象实例,如果不存在就创建一个并保存起来,这点有些单例模式的意思。通常工厂类会有一个集合类型的成员变量来用以保存对象,如hashtable,vector等。在java中,数据库连接池,线程池等即是用享元模式的应用。


首先String不属于8种基本数据类型,String是一个对象。
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
new String()和new String(“”)都是申明一个新的空字符串,是空串不是null;
String str=”kvill”;
String str=new String (“kvill”);的区别:
在这里,我们不谈堆,也不谈栈,只先简单引入常量池这个简单的概念。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
看例1:

String s0=”kvill”; 

String s1=”kvill”;

String s2=”kv” + “ill”;

System.out.println( s0==s1 );

System.out.println( s0==s2 );

结果为:


true 

true

首先,我们要知结果为道Java会确保一个字符串常量只有一个拷贝。
因为例子中的s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”kvill”的一个引用。

所以我们得出s0==s1==s2;


用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。


看例2:


String s0=”kvill”; 

String s1=new String(”kvill”);

String s2=”kv” + new String(“ill”);

System.out.println( s0==s1 );

System.out.println( s0==s2 );

System.out.println( s1==s2 );

结果为:


false 

false

false

例2中s0还是常量池中”kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分new String(“ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。


String.intern():

再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看例3就清楚了


例3:


String s0= “kvill”; 

String s1=new String(”kvill”);

String s2=new String(“kvill”);

System.out.println( s0==s1 );

System.out.println( “**********” );

s1.intern();

s2=s2.intern(); //把常量池中“kvill”的引用赋给s2

System.out.println( s0==s1);

System.out.println( s0==s1.intern() );

System.out.println( s0==s2 );

结果为:


false 

**********

false //虽然执行了s1.intern(),但它的返回值没有赋给s1

true //说明s1.intern()返回的是常量池中”kvill”的引用

true

最后我再破除一个错误的理解:


有人说,“使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中“如果我把他说的这个全局的String表理解为常量池的话,他的最后一句话,“如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:


看例4:


String s1=new String("kvill"); 

String s2=s1.intern();

System.out.println( s1==s1.intern() );

System.out.println( s1+" "+s2 );

System.out.println( s2==s1.intern() );

结果:


false 

kvill kvill

true

在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。


s1==s1.intern()为false说明原来的“kvill”仍然存在;


s2现在为常量池中“kvill”的地址,所以有s2==s1.intern()为true。


关于equals()和==:

这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。


关于String是不可变的

这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”;


就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” “ 生成 ”kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。


okhttp3 kotlin ConnectionPool 源码分析

ConnectionPool的说明:
管理http和http/2的链接,以便减少网络请求延迟。同一个address将共享同一个connection。该类实现了复用连接的目标。

class RealConnectionPool(
/** 每个address的最大空闲连接数 */
private val maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
//这是一个用于清楚过期链接的线程池,每个线程池最多只能运行一个线程,并且这个线程池允许被垃圾回收
private val executor = ThreadPoolExecutor(
0, // corePoolSize.
Int.MAX_VALUE, // maximumPoolSize.
60L, TimeUnit.SECONDS, // keepAliveTime.
SynchronousQueue(),
threadFactory("OkHttp ConnectionPool", true)
)
//双向队列
private val connections = ArrayDeque<RealConnection>()
//路由的数据库
val routeDatabase = RouteDatabase()
//清理任务正在执行的标志
var cleanupRunning: Boolean = false


  1. 主要就是connections,可见ConnectionPool内部以队列方式存储连接;
  2. routDatabase是一个黑名单,用来记录不可用的route,但是看代码貌似ConnectionPool并没有使用它。所以此处不做分析。
  3. 剩下的就是和清理有关了,所以executor是清理任务的线程池,cleanupRunning是清理任务的标志,cleanupRunnable是清理任务。

class ConnectionPool(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
//创建一个适用于单个应用程序的新连接池。
//该连接池的参数将在未来的okhttp中发生改变
//目前最多可容乃5个空闲的连接,存活期是5分钟
constructor() : this(5, 5, TimeUnit.MINUTES)
}

init {
//保持活着的时间,否则清理将旋转循环
require(keepAliveDuration > 0L) { "keepAliveDuration <= 0: $keepAliveDuration" }
}

通过这个构造器我们知道了这个连接池最多维持5个连接,且每个链接最多活5分钟。并且包含一个线程池包含一个清理任务。
所以maxIdleConnections和keepAliveDurationNs则是清理中淘汰连接的的指标,这里需要说明的是maxIdleConnections是值每个地址上最大的空闲连接数。所以OkHttp只是限制与同一个远程服务器的空闲连接数量,对整体的空闲连接并没有限制。

这时候说下ConnectionPool的实例化的过程,一个OkHttpClient只包含一个ConnectionPool,其实例化也是在OkHttpClient的过程。这里说一下ConnectionPool各个方法的调用并没有直接对外暴露,而是通过OkHttpClient的Internal接口统一对外暴露。


然后我们来看下他的transmitterAcquirePooledConnection(获取连接)和put方法


fun transmitterAcquirePooledConnection(
address: Address,
transmitter: Transmitter,
routes: List<Route>?,
requireMultiplexed: Boolean
): Boolean {
//断言,判断线程是不是被自己锁住了
assert(Thread.holdsLock(this))
// 遍历已有连接集合
for (connection in connections) {
if (requireMultiplexed && !connection.isMultiplexed) continue
//如果connection和需求中的"地址"和"路由"匹配
if (!connection.isEligible(address, routes)) continue
//复用这个连接
transmitter.acquireConnectionNoEvents(connection)

return true
}
return false
}

put方法更为简单,就是异步触发清理任务,然后将连接添加到队列中


  fun put(connection: RealConnection) {
assert(Thread.holdsLock(this))
if (!cleanupRunning) {
cleanupRunning = true
executor.execute(cleanupRunnable)
}
connections.add(connection)
}

private val cleanupRunnable = object : Runnable {
override fun run() {
while (true) {
val waitNanos = cleanup(System.nanoTime())
if (waitNanos == -1L) return
try {
this@RealConnectionPool.lockAndWaitNanos(waitNanos)
} catch (ie: InterruptedException) {
// Will cause the thread to exit unless other connections are created!
evictAll()
}
}
}
}
这个逻辑也很简单,就是调用cleanup方法执行清理,并等待一段时间,持续清理,其中cleanup方法返回的值来来决定而等待的时间长度。那我们继续来看下cleanup函数:
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE

// Find either a connection to evict, or the time that the next eviction is due.
synchronized(this) {
for (connection in connections) {
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
continue
}
//统计空闲连接数量
idleConnectionCount++

// If the connection is ready to be evicted, we're done.
val idleDurationNs = now - connection.idleAtNanos
if (idleDurationNs > longestIdleDurationNs) {
//找出空闲时间最长的连接以及对应的空闲时间
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
}
}

when {
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
// We've found a connection to evict. Remove it from the list, then close it below
// (outside of the synchronized block).
//在符合清理条件下,清理空闲时间最长的连接
connections.remove(longestIdleConnection)
}
idleConnectionCount > 0 -> {
// A connection will be ready to evict soon.
//不符合清理条件,则返回下次需要执行清理的等待时间,也就是此连接即将到期的时间
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
// All connections are in use. It'll be at least the keep alive duration 'til we run
// again.
//没有空闲的连接,则隔keepAliveDuration(分钟)之后再次执行
return keepAliveDurationNs
}
else -> {
// No connections, idle or in use.
cleanupRunning = false
return -1
}
}
}
//关闭socket资源
longestIdleConnection!!.socket().closeQuietly()

// Cleanup again immediately.
//这里是在清理一个空闲时间最长的连接以后会执行到这里,需要立即再次执行清理
return 0
}

这里的首先统计空闲连接数量,然后通过for循环查找最长空闲时间的连接以及对应空闲时长,然后判断是否超出最大空闲连接数(maxIdleConnections)或者或者超过最大空闲时间(keepAliveDurationNs),满足其一则清除最长空闲时长的连接。如果不满足清理条件,则返回一个对应等待时间。
这个对应等待的时间又分二种情况:


  1. 有连接则等待下次需要清理的时间去清理:keepAliveDurationNs-longestIdleDurationNs;
  2. 没有空闲的连接,则等下一个周期去清理:keepAliveDurationNs
    如果清理完毕返回-1。

综上所述,我们来梳理一下清理任务,清理任务就是异步执行的,遵循两个指标,最大空闲连接数量和最大空闲时长,满足其一则清理空闲时长最大的那个连接,然后循环执行,要么等待一段时间,要么继续清理下一个连接,知道清理所有连接,清理任务才结束,下一次put的时候,如果已经停止的清理任务则会被再次触发


private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
val references = connection.transmitters
var i = 0
//遍历弱引用列表
while (i < references.size) {
val reference = references[i]
//若StreamAllocation被使用则接着循环
if (reference.get() != null) {
i++
continue
}

// We've discovered a leaked transmitter. This is an application bug.
val transmitterRef = reference as TransmitterReference
val message = "A connection to ${connection.route().address.url} was leaked. " +
"Did you forget to close a response body?"
Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace)
//若StreamAllocation未被使用则移除引用,这边注释为泄露
references.removeAt(i)
connection.noNewExchanges = true
//如果列表为空则说明此连接没有被引用了,则返回0,表示此连接是空闲连接
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs
return 0
}
}

return references.size
}

pruneAndGetAllocationCount主要是用来标记泄露连接的。内部通过遍历传入进来的RealConnection的StreamAllocation列表,如果StreamAllocation被使用则接着遍历下一个StreamAllocation。如果StreamAllocation未被使用则从列表中移除,如果列表中为空则说明此连接连接没有引用了,返回0,表示此连接是空闲连接,否则就返回非0表示此连接是活跃连接。
接下来让我看下ConnectionPool的connectionBecameIdle()方法,就是当有连接空闲时,唤起cleanup线程清洗连接池

fun connectionBecameIdle(connection: RealConnection): Boolean {
assert(Thread.holdsLock(this))
//该连接已经不可用
return if (connection.noNewExchanges || maxIdleConnections == 0) {
connections.remove(connection)
true
} else {
// Awake the cleanup thread: we may have exceeded the idle connection limit.
//欢迎clean 线程
this.notifyAll()
false
}
}

connectionBecameIdle标示一个连接处于空闲状态,即没有流任务,那么久需要调用该方法,由ConnectionPool来决定是否需要清理该连接。
再来看下evictAll()方法

fun evictAll() {
val evictedConnections = mutableListOf<RealConnection>()
synchronized(this) {
val i = connections.iterator()
while (i.hasNext()) {
val connection = i.next()
if (connection.transmitters.isEmpty()) {
connection.noNewExchanges = true
evictedConnections.add(connection)
i.remove()
}
}
}

for (connection in evictedConnections) {
connection.socket().closeQuietly()
}
}

该方法是删除所有空闲的连接,比较简单,不说了


Integer中的享元模式

那么我们来看看Integer中的享元模式具体是怎么样的吧。
通过如下代码了解一下integer的比较

public static void main(String[] args)
{
Integer integer1 = 9;
Integer integer2 = 9;
System.out.println(integer1==integer2);

Integer integer3 = 129;
Integer integer4 = 129;
System.out.println(integer3==integer4);
}

输出:


true
false

在通过等号赋值的时候,实际上是通过调用valueOf方法的返回一个对象。然后我们观察一下这个方法的源码。


public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private final int value;
public Integer(int value) {
this.value = value;
}
}

上面是我简化了的Integer类。平常在使用Integer类的时候。你是否思考过用valueOf还是用new创建Integer对象。看完源码就会发现在valueOf这个方法中它会先判断传进去的值是否在IntegerCache中,如果不在就创建新的对象,在就直接返回缓存池里的对象。这个valueOf方法就用到享元模式。它将-128到127的Integer对象先在缓存池里创建好,等我们需要的时候直接返回即可。所以在-128到127中的数值我们用valueOf创建会比new更快。因此我们在使用Integer对象的时候,也一定要记住使用equals(),而不是单纯的使用”==”,否则有可能出现不相等的情况。


收起阅读 »

java设计模式:桥接模式

桥接模式的定义与特点桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。 通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了...
继续阅读 »

桥接模式的定义与特点

桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。


通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。


优点


  • 抽象与实现分离,扩展能力强
  • 符合开闭原则
  • 符合合成复用原则
  • 其实现细节对客户透明

缺点

由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。


桥接模式的结构与实现

可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。


模式的结构

抽象化角色:定义抽象类,并包含一个对实现化对象的引用。
扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
实现化角色:定义实现化角色的接口,供扩展抽象化角色调用。
具体实现化角色:给出实现化角色接口的具体实现。

桥接模式的应用场景

当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。


桥接模式通常适用于以下场景。



  1. 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  2. 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  3. 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。

桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。


示例代码:
在这里插入图片描述


//抽象类:建筑
public abstract class Building {
protected Paint paint;
public Building(Paint paint) {
this.paint = paint;
}
public abstract void decorate();
}
//接口:油漆
public interface Paint {
void decorateImpl();
}
//教学楼
public class TeachingBuilding extends Building {
public TeachingBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的教学楼");
paint.decorateImpl();
}
}
//实验楼
public class LaboratoryBuilding extends Building {
public LaboratoryBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的实验楼");
paint.decorateImpl();
}
}
public class RedPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被红色油漆装饰过。");
}
}
public class GreenPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被绿色油漆装饰过。");
}
}
public class BulePaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被蓝色油漆装饰过。");
}
}
public class BridgePatternDemo {
public static void main(String[] args) {
//普通的教学楼被红色油漆装饰。
Building redTeachingBuilding=new TeachingBuilding(new RedPaint());
redTeachingBuilding.decorate();
//普通的教学楼被绿色油漆装饰。
Building greenTeachingBuilding1=new TeachingBuilding(new GreenPaint());
greenTeachingBuilding1.decorate();
//普通的实验楼被红色油漆装饰。
Building redLaboratoryBuilding=new LaboratoryBuilding(new RedPaint());
redLaboratoryBuilding.decorate();
//普通的实验楼被绿色油漆装饰。
Building greenLaboratoryBuilding=new LaboratoryBuilding(new GreenPaint());
greenLaboratoryBuilding.decorate();
//普通的实验楼被蓝色油漆装饰。
Building blueLaboratoryBuilding=new LaboratoryBuilding(new BulePaint());
blueLaboratoryBuilding.decorate();
}
}

运行结果:
普通的教学楼被红色油漆装饰过。
普通的教学楼被绿色油漆装饰过。
普通的实验楼被红色油漆装饰过。
普通的实验楼被绿色油漆装饰过。
普通的实验楼被蓝色油漆装饰过。

桥接模式与装饰模式对比:

两个模式都是为了解决子类过多问题, 但他们的诱因不同:



  1. 桥接模式对象自身有 沿着多个维度变化的趋势 , 本身不稳定;
  2. 装饰者模式对象自身非常稳定, 只是为了增加新功能/增强原功能。

收起阅读 »

你有原则么?懂原则么?想了解么?快看设计模式原则篇,让你做个有原则的程序员

前言无论做啥,要想好设计,就得多扩展,少修改 开闭原则此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modificatio...
继续阅读 »

前言

无论做啥,要想好设计,就得多扩展,少修改



开闭原则

此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modification”。就是说模块应对扩展开放,而对修改关闭。模块应尽量在不修改原(是”原”,指原来的代码)代码的情况下进行扩展


开闭原则的含义

当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。


开闭原则的作用


  1. 对软件测试的影响
    软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
  2. 可以提高代码的可复用性
    粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
  3. 可以提高软件的可维护性
    遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。

里氏替换原则

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。


里氏替换原则的作用

它克服了继承中重写父类造成的可复用性变差的缺点。
它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

里氏替换原则的实现方法

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。


依赖倒置原则

要面向接口编程,不要面向实现编程。


依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。


由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。


使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。


依赖、倒置原则的作用


  • 依赖倒置原则的主要作用如下。
  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性。
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

单一职责原则

单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分


单一职责原则的优点

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。



  • 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
  • 提高类的可读性。复杂性降低,自然其可读性会提高。
  • 提高系统的可维护性。可读性提高,那自然更容易维护了。
  • 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

接口隔离原则

尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
一个类对另一个类的依赖应该建立在最小的接口上

要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。


接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:



  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

    接口隔离原则的优点

    接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。


  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  3. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
  4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  5. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

迪米特法则

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。


迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。


  • 降低了类之间的耦合度,提高了模块的相对独立性。
  • 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。


合成复用原则

要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。


如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。


合成复用原则的重要性

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。



  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。



  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

收起阅读 »

华为手机升级HarmonyOS全攻略:公测&内测&线下升级

写在前面:本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)必要说明:所有消费者公测渠道最终都会跳转到花粉俱乐部;初期申请量巨大...
继续阅读 »

写在前面:

本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!

目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)

必要说明:

所有消费者公测渠道最终都会跳转到花粉俱乐部;

初期申请量巨大,花粉俱乐部很容易就挂掉,心急的小伙伴可尝试线下渠道或者多次尝试或者深夜(两点以后)申请;

申请前务必将“花粉俱乐部”、“我的华为”、“会员中心”升级到最新版本,尤其是“花粉俱乐部”。

消费者公测

包含机型:

Mate X2

Mate40、Mate40E、Mate 40 Pro、Mate 40 Pro+、Mate 40 RS 保时捷设计

P40 5G、P40 4G、P40 Pro、P40 Pro+

Mate 30 4G、Mate 30 Pro 4G、Mate 30 5G、Mate 30 Pro 5G、Mate 30 RS保时捷设计、Mate 30E Pro 5G

MatePad Pro、MatePad Pro 5G

我的华为/花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开“我的华为”,点击“升级尝鲜” / 打开“花粉俱乐部”,点击“公测尝鲜”。

△我的华为

△花粉俱乐部

2.页面加载完成后,点击“公测尝鲜”下的“立即尝鲜”按钮。(花粉俱乐部进入的请忽略此步骤,直接进入下步)

3.接下来在列表中找到当前手机型号对应的公测活动,点击“报名公测”。由于不同手机对应的系统版本不一样,请务必仔细核实你的机器型号。

4.此处会跳转到“花粉论坛”的一篇帖子,划到这篇帖子的末尾,点击“参加公测活动”。接下来系统会引导用户签订《华为公测协议》和《华为公测与隐私声明》,等待10秒点击通过。

5.通过两个协议后,系统会引导你下载协议文件。这个过程会验证你的机型是否符合要求,且下载的文件也是将来升级为正式版的必要文件,如果找到(反正我是没找到)请勿删除!!

6.下载并提示安装完描述文件后,就可以去检测系统更新,下载并更新HarmonyOS了。(P40系列当前版本116)

消费者内测

包含机型:

Mate XS、Mate 20、Mate 20 Pro、Mate 20 RS(保时捷)、Mate 20 X(4G)

nova 8、nova 8 Pro、nova 8 SE、nova 7 5G、nova 7 Pro 5G、nova 7 SE 5G、nova 7 SE 5G活力版、nova 7 SE 5G乐活版、nova 6、nova 6 5G、nova 6 SE

华为畅享20 Plus 5G、华为畅享Z 5G、华为畅享20 Pro5G

华为麦芒9 5G

MatePad 10.8、MatePad 5G 10.4、MatePad 10.4

内测时间:6月2日~6月9日上午10:00

渠道一

会员中心:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开会员中心,首页上找到“体验先锋”,点击进入。

2.点击顶部的HarmonyOS 2升级尝鲜。

3.进入页面点击报名。如果机型不符合,会弹出提示框。

4.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

渠道二

花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.进入首页点击内测报名

2.跳转后,点击立即报名

3.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

HarmonyOS体验官(线下)

包含机型:

我的华为:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

在APP首页点击“HarmonyOS体验官”海报,经过简单的互动问答即可参加。期间需要提交信息、预约门店时间和信息,最终会生成一张包含数字的海报,用户需要保存此海报才可参与活动。

活动仅在部分门店进行,具体店面和城市请在活动页面查询。到店会提供礼品,并可在线下由店面工作人员协助完成升级。

重要的补充说明

1.消费者公测仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

2.消费者内测仍然会存在审核机制,但仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

3.最终稳定的系统版本号预计为:HarmonyOS 2.0.0.116(以实际推送版本号为准!)

4.老荣耀系列机型不在本次消费者公测列表中。

收起阅读 »

ZFPlayer 3.0解析

详细介绍一下ZFPlayer 3.0的用法,如果你有什么问题或者建议可联系我。在3.0之前版本使用ZFPlayer,是不是在烦恼播放器SDK自定义、控制层自定义等问题。作者公司多个项目分别使用不同播放器SDK以及每个项目控制层都不一样,但是为了统一管理、统一调...
继续阅读 »

详细介绍一下ZFPlayer 3.0的用法,如果你有什么问题或者建议可联系我。在3.0之前版本使用ZFPlayer,是不是在烦恼播放器SDK自定义、控制层自定义等问题。作者公司多个项目分别使用不同播放器SDK以及每个项目控制层都不一样,但是为了统一管理、统一调用,我特意写了这个播放器壳子。播放器SDK只要遵守ZFPlayerMediaPlayback协议,控制层只要遵守ZFPlayerMediaControl协议,可以实现自定义播放器和控制层。

目前支持的功能如下:

1、普通模式的播放,类似于腾讯视频、爱奇艺等APP;
2、列表普通模式的播放,包括手动点击播放、滑动到屏幕中间自动播放,wifi网络智能播放等等;
3、列表的亮暗模式播放,类似于微博、UC浏览器视频列表等APP;
4、列表视频滑出屏幕后停止播放、滑出屏幕后小窗播放;
5、优雅的全屏,支持横屏和竖屏全屏模式;

播放器的主要类为ZFPlayerController,具体API请看下边这张图吧,后边我也会详细介绍。在之前版本收到好多开发朋友的Issues建议也好bug也好,ZFPlayer也是致力于解决这些问题和满足各位的建议。

ZFPlayerController(播放器的主要类)

初始化方式:

/// 普通播放的初始化
+ (instancetype)playerWithPlayerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

/// 普通播放的初始化
- (instancetype)initWithPlayerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

/// UITableView、UICollectionView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerViewTag:(NSInteger)containerViewTag;

/// UITableView、UICollectionView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerViewTag:(NSInteger)containerViewTag;

/// UIScrollView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

/// UIScrollView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

属性

/// 初始化时传递的容器视图,用来显示播放器view,和播放器view同等大小
@property (nonatomic, strong) UIView *containerView;

/// 初始化时传递的播放器manager,必须遵守`ZFPlayerMediaPlayback`协议
@property (nonatomic, strong) id<ZFPlayerMediaPlayback> currentPlayerManager;

/// 此属性是设置显示的控制层,自定义UIView遵守`ZFPlayerMediaControl`协议,实现相关协议就可以满足自定义控制层的目的。
@property (nonatomic, strong) UIView<ZFPlayerMediaControl> *controlView;

/// 通知的管理类
@property (nonatomic, strong, readonly) ZFPlayerNotification *notification;

/// 容器的类型(cell和普通View)
@property (nonatomic, assign, readonly) ZFPlayerContainerType containerType;

/// 播放器小窗的容器View
@property (nonatomic, strong, readonly) ZFFloatView *smallFloatView;

/// 播放器小窗是否正在显示
@property (nonatomic, assign, readonly) BOOL isSmallFloatViewShow;

ZFPlayerController (ZFPlayerTimeControl)

/// 0...1.0,调节系统的声音,要是调节播放器声音可以使用播放器管理类设置
@property (nonatomic) float volume;

/// 系统静音,要是调节播放器静音可以使用播放器管理类设置
@property (nonatomic, getter=isMuted) BOOL muted;

// 0...1.0, 系统屏幕亮度
@property (nonatomic) float brightness;

/// 移动网络下自动播放, default is NO.
@property (nonatomic, getter=isWWANAutoPlay) BOOL WWANAutoPlay;

/// 当前播放的下标,只适用于设置了`assetURLs`
@property (nonatomic) NSInteger currentPlayIndex;

/// 在 `assetURLs`中是否是最后一个
@property (nonatomic, readonly) BOOL isLastAssetURL;

/// 在 `assetURLs`中是否是第一个
@property (nonatomic, readonly) BOOL isFirstAssetURL;

/// 当退到后台后是否暂停播放,前提是支持后台播放器模式,default is YES.
@property (nonatomic) BOOL pauseWhenAppResignActive;

/// 当播放器在玩播放时,它会被一些事件暂停,而不是用户点击暂停。
/// 例如,应用程序进入后台或者push到另一个视图控制器
@property (nonatomic, getter=isPauseByEvent) BOOL pauseByEvent;

/// 当前播放器控制器消失,而不是dealloc
@property (nonatomic, getter=isViewControllerDisappear) BOOL viewControllerDisappear;

/// 自定义AVAudioSession, default is NO.
@property (nonatomic, assign) BOOL customAudioSession;

/// 当播放器Prepare时候调用
@property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

///当播放器准备开始播放时候调用
@property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// 当播放进度改变时候调用.
@property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval currentTime, NSTimeInterval duration);

/// 当缓冲进度改变时候调用
@property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval bufferTime);

/// 当播放状态改变时候调用
@property (nonatomic, copy, nullable) void(^playerPlayStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerPlaybackState playState);

/// 当加载状态改变时候调用.
@property (nonatomic, copy, nullable) void(^playerLoadStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerLoadState loadState);

/// 当播放失败时候调用.
@property (nonatomic, copy, nullable) void(^playerPlayFailed)(id<ZFPlayerMediaPlayback> asset, id error);

/// 当播放状态完成时候调用.
@property (nonatomic, copy, nullable) void(^playerDidToEnd)(id<ZFPlayerMediaPlayback> asset);

// 当播放器view的尺寸改变时候调用.
@property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id<ZFPlayerMediaPlayback> asset, CGSize size);

/// 播放下一个,只适用于设置了`assetURLs`
- (void)playTheNext;

/// 播放上一个,只适用于设置了`assetURLs`
- (void)playThePrevious;

/// 播放某一个,只适用于设置了`assetURLs`
- (void)playTheIndex:(NSInteger)index;

/// 停止播放,并且把播放器view和相关通知移除
- (void)stop;

/// 切换当前的PlayerManager,适用场景:播放某一个视频时候使用特定的播放器管理类
- (void)replaceCurrentPlayerManager:(id<ZFPlayerMediaPlayback>)manager;

/**
添加播放器view到cell上
*/
- (void)addPlayerViewToCell;

/**
添加播放器view到容器view上.
*/
- (void)addPlayerViewToContainerView:(UIView *)containerView;

/**
添加播放器到主window上.
*/
- (void)addPlayerViewToKeyWindow;

/**
停止当前在view上的播放并移除播放器view.
*/
- (void)stopCurrentPlayingView;

/**
停止当前在cell上的播放并移除播放器view.
*/
- (void)stopCurrentPlayingCell;

ZFPlayerController (ZFPlayerOrientationRotation)

/// 屏幕旋转管理类
@property (nonatomic, readonly) ZFOrientationObserver *orientationObserver;

///是否支持自动屏幕旋转。
/// iOS8.1~iOS8.3的值为YES,其他iOS版本的值为NO。
///这个属性用于UIViewController ' shouldAutorotate '方法的返回值。
@property (nonatomic, readonly) BOOL shouldAutorotate;

///是否允许视频方向旋转。
///默认值是YES。
@property (nonatomic) BOOL allowOrentitaionRotation;

/// 是否是全屏状态,当ZFFullScreenMode == ZFFullScreenModeLandscape,当currentOrientation是LandscapeLeft或者LandscapeRight,这个值是YES
/// 当ZFFullScreenMode == ZFFullScreenModePortrait,当视频全屏后,这个值是YES
@property (nonatomic, readonly) BOOL isFullScreen;

/// 锁定当前的屏幕方向,目的是禁止设备自动旋转
@property (nonatomic, getter=isLockedScreen) BOOL lockedScreen;

/// 隐藏系统的状态栏
@property (nonatomic, getter=isStatusBarHidden) BOOL statusBarHidden;

/// 使用设备方向旋转屏幕, default NO.
@property (nonatomic, assign) BOOL forceDeviceOrientation;

/// 播放器view当前方向
@property (nonatomic, readonly) UIInterfaceOrientation currentOrientation;

/// 当即将全屏时候会调用
@property (nonatomic, copy, nullable) void(^orientationWillChange)(ZFPlayerController *player, BOOL isFullScreen);

/// 当已经全屏时候会调用
@property (nonatomic, copy, nullable) void(^orientationDidChanged)(ZFPlayerController *player, BOOL isFullScreen);

/// 添加设备方向的监听
- (void)addDeviceOrientationObserver;

/// 移除设备方向的监听
- (void)removeDeviceOrientationObserver;

/// 当 ZFFullScreenMode == ZFFullScreenModeLandscape使用此API设置全屏切换
- (void)enterLandscapeFullScreen:(UIInterfaceOrientation)orientation animated:(BOOL)animated;

/// 当 ZFFullScreenMode == ZFFullScreenModePortrait使用此API设置全屏切换
- (void)enterPortraitFullScreen:(BOOL)fullScreen animated:(BOOL)animated;

/// 内部根据ZFFullScreenMode的值来设置全屏切换
- (void)enterFullScreen:(BOOL)fullScreen animated:(BOOL)animated;

ZFPlayerController (ZFPlayerViewGesture)

/// 手势的管理类
@property (nonatomic, readonly) ZFPlayerGestureControl *gestureControl;

/// 禁用哪些手势,默认支持单击、双击、滑动、缩放手势
@property (nonatomic, assign) ZFPlayerDisableGestureTypes disableGestureTypes;

///不支持的平移手势移动方向
@property (nonatomic) ZFPlayerDisablePanMovingDirection disablePanMovingDirection;

ZFPlayerController (ZFPlayerScrollView)

/// 初始化时候设置的scrollView
@property (nonatomic, readonly, nullable) UIScrollView *scrollView;

/// 只适用于列表播放时候是否自动播放,default is YES.
@property (nonatomic) BOOL shouldAutoPlay;

/// 移动网络自动播放,只有当“shouldAutoPlay”为YES时才支持,默认为NO
@property (nonatomic, getter=isWWANAutoPlay) BOOL WWANAutoPlay;

/// 当前播放的indexPath
@property (nonatomic, nullable) NSIndexPath *playingIndexPath;

/// 初始化时候设置的containerViewTag,根据此tag在cell上找到播放器view显示的位置
@property (nonatomic) NSInteger containerViewTag;

/// 滑出屏幕后是否停止播放,如果设置为NO,滑出屏幕后则会小窗播放,defalut is YES.
@property (nonatomic) BOOL stopWhileNotVisible;

/**
当前播放器滚动滑出屏幕的百分比。
当`stopWhileNotVisible`为YES时使用的属性,停止当前正在播放的播放器。
当`stopWhileNotVisible`为NO时使用的属性,当前正在播放的播放器添加到小容器视图。
范围是0.0~1.0,defalut是0.5。
0.0是player将会消失。
1.0是player消失了。
*/
@property (nonatomic) CGFloat playerDisapperaPercent;

/**
当前播放器滚动到屏幕百分比来播放视频。
范围是0.0~1.0,defalut是0.0。
0.0是玩家将会出现。
1.0是播放器确实出现了。
*/
@property (nonatomic) CGFloat playerApperaPercent;

/// 如果列表播放时候有多个区,使用此API
@property (nonatomic, copy, nullable) NSArray <NSArray <NSURL *>*>*sectionAssetURLs;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath
@param scrollToTop使用动画将当前单元格滚动到顶部。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath
@param assetURL播放器URL。
@param scrollToTop使用动画将当前单元格滚动到顶部。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath assetURL:(NSURL *)assetURL scrollToTop:(BOOL)scrollToTop;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath
@param scrollToTop使用动画将当前单元格滚动到顶部。
@param completionHandler滚动完成回调。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop completionHandler:(void (^ __nullable)(void))completionHandler;

ZFPlayerMediaPlayback—播放器SDK遵守的协议

1、枚举类型:

///  播放状态:未知、播放中、暂停、失败、停止
typedef NS_ENUM(NSUInteger, ZFPlayerPlaybackState) {
ZFPlayerPlayStateUnknown = 0,
ZFPlayerPlayStatePlaying,
ZFPlayerPlayStatePaused,
ZFPlayerPlayStatePlayFailed,
ZFPlayerPlayStatePlayStopped
};
///  加载状态:未知、就绪、可以播放、自动播放、播放暂停
typedef NS_OPTIONS(NSUInteger, ZFPlayerLoadState) {
ZFPlayerLoadStateUnknown = 0,
ZFPlayerLoadStatePrepare = 1 << 0,
ZFPlayerLoadStatePlayable = 1 << 1,
ZFPlayerLoadStatePlaythroughOK = 1 << 2,
ZFPlayerLoadStateStalled = 1 << 3,
};
///  播放画面拉伸模式:无拉伸、等比例拉伸不裁剪、部分内容裁剪按比例填充、非等比例填满
typedef NS_ENUM(NSInteger, ZFPlayerScalingMode) {
ZFPlayerScalingModeNone,
ZFPlayerScalingModeAspectFit,
ZFPlayerScalingModeAspectFill,
ZFPlayerScalingModeFill
};

2、协议属性:

///  播放器视图继承于ZFPlayerView,处理一些手势冲突
@property (nonatomic) ZFPlayerView *view;

/// 0...1.0,播放器音量,不影响设备的音量大小
@property (nonatomic) float volume;

/// 播放器是否静音,不影响设备静音
@property (nonatomic, getter=isMuted) BOOL muted;

/// 0.5...2,播放速率,正常速率为 1
@property (nonatomic) float rate;

/// 当前播放时间
@property (nonatomic, readonly) NSTimeInterval currentTime;

/// 播放总时间
@property (nonatomic, readonly) NSTimeInterval totalTime;

/// 缓冲时间
@property (nonatomic, readonly) NSTimeInterval bufferTime;

/// 视频播放定位时间
@property (nonatomic) NSTimeInterval seekTime;

/// 视频是否正在播放中
@property (nonatomic, readonly) BOOL isPlaying;

/// 视频播放视图的填充模式,默认不做任何拉伸
@property (nonatomic) ZFPlayerScalingMode scalingMode;

/// 检查视频播放是否准备就绪,返回YES,调用play方法直接播放视频;返回NO,调用play方法内部自动调用prepareToPlay方法进行视频播放准备工作
@property (nonatomic, readonly) BOOL isPreparedToPlay;

/// 媒体播放资源URL
@property (nonatomic) NSURL *assetURL;

/// 视频的尺寸
@property (nonatomic, readonly) CGSize presentationSize;

/// 视频播放状态
@property (nonatomic, readonly) ZFPlayerPlaybackState playState;

/// 视频的加载状态
@property (nonatomic, readonly) ZFPlayerLoadState loadState;

///------------------------------------
///如果没有指定controlView,可以调用以下块。
///如果你指定了controlView,下面的代码块不能在外部调用,只能用于“ZFPlayerController”调用。
///------------------------------------

/// 准备播放
@property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// 开始播放了
@property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// 播放进度改变
@property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval currentTime, NSTimeInterval duration);

/// 视频缓冲进度改变
@property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval bufferTime);

/// 视频播放状态改变
@property (nonatomic, copy, nullable) void(^playerPlayStatChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerPlaybackState playState);

/// 视频加载状态改变
@property (nonatomic, copy, nullable) void(^playerLoadStatChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerLoadState loadState);

/// 视频播放已经结束
@property (nonatomic, copy, nullable) void(^playerDidToEnd)(id<ZFPlayerMediaPlayback> asset);

// 视频的尺寸改变了
@property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id<ZFPlayerMediaPlayback> asset, CGSize size);

///------------------------------------
/// end
///------------------------------------

3、协议方法:

///  视频播放准备,中断除non-mixible之外的任何音频会话
- (void)prepareToPlay;

/// 重新进行视频播放准备
- (void)reloadPlayer;

/// 视频播放
- (void)play;

/// 视频暂停
- (void)pause;

/// 视频重新播放
- (void)replay;

/// 视频播放停止
- (void)stop;

/// 视频播放当前时间的画面截图
- (UIImage *)thumbnailImageAtCurrentTime;

/// 替换当前媒体资源地址
- (void)replaceCurrentAssetURL:(NSURL *)assetURL;

/// 调节播放进度
- (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;

ZFPlayerMediaControl—控制层遵守的协议

1、视频状态相关

///  视频播放准备就绪
- (void)videoPlayer:(ZFPlayerController *)videoPlayer prepareToPlay:(NSURL *)assetURL;

/// 视频播放状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer playStateChanged:(ZFPlayerPlaybackState)state;

/// 视频加载状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer loadStateChanged:(ZFPlayerLoadState)state;

2、播放进度

///  视频播放时间进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
currentTime:(NSTimeInterval)currentTime
totalTime:(NSTimeInterval)totalTime;

/// 视频缓冲进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
bufferTime:(NSTimeInterval)bufferTime;

/// 视频定位播放时间
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
draggingTime:(NSTimeInterval)seekTime
totalTime:(NSTimeInterval)totalTime;

/// 视频播放结束
- (void)videoPlayerPlayEnd:(ZFPlayerController *)videoPlayer;

3、锁屏

/// 设置播放器锁屏时的协议方法
- (void)lockedVideoPlayer:(ZFPlayerController *)videoPlayer lockedScreen:(BOOL)locked;

4、屏幕旋转

///  播放器全屏模式即将改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationWillChange:(ZFOrientationObserver *)observer;

/// 播放器全屏模式已经改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationDidChanged:(ZFOrientationObserver *)observer;

/// 当前网络状态发生变化
- (void)videoPlayer:(ZFPlayerController *)videoPlayer reachabilityChanged:(ZFReachabilityStatus)status;

5、手势方法

///  相关手势设置
- (BOOL)gestureTriggerCondition:(ZFPlayerGestureControl *)gestureControl
gestureType:(ZFPlayerGestureType)gestureType
gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
touch:(UITouch *)touch;

/// 单击
- (void)gestureSingleTapped:(ZFPlayerGestureControl *)gestureControl;

/// 双击
- (void)gestureDoubleTapped:(ZFPlayerGestureControl *)gestureControl;

/// 开始拖拽
- (void)gestureBeganPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location;

/// 拖拽中
- (void)gestureChangedPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location
withVelocity:(CGPoint)velocity;

/// 拖拽结束
- (void)gestureEndedPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location;

/// 捏合手势变化
- (void)gesturePinched:(ZFPlayerGestureControl *)gestureControl
scale:(float)scale;

6、scrollView上的播放器视图方法

/**
scrollView中的播放器视图将要出现的回调
*/
- (void)playerWillAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图已经出现的回调
*/
- (void)playerDidAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图即将消失的回调
*/
- (void)playerWillDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图已经消失的回调
*/
- (void)playerDidDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图正在显示的回调
*/
- (void)playerAppearingInScrollView:(ZFPlayerController *)videoPlayer playerApperaPercent:(CGFloat)playerApperaPercent;

/**
scrollView中的播放器视图正在消失的回调
*/
- (void)playerDisappearingInScrollView:(ZFPlayerController *)videoPlayer playerDisapperaPercent:(CGFloat)playerDisapperaPercent;

/**
小窗视图显示隐藏的回调
*/
- (void)videoPlayer:(ZFPlayerController *)videoPlayer floatViewShow:(BOOL)show;

代码传送门:https://github.com/renzifeng/ZFPlayer

转自:https://www.jianshu.com/p/90e55deb4d51

收起阅读 »

Android微信工具包,你想要的这里都有~

wxlibrary aar文件使用说明APP 使用示例项目,libs下含有以编译最新的aar资源。wxlibrary arr资源项目,需要引入的资源包项目。aar文件生成,在工具栏直接Gradle - (项目名) - wxlibrary - Tasks - b...
继续阅读 »

wxlibrary aar文件使用说明

一、项目介绍

  1. APP 使用示例项目,libs下含有以编译最新的aar资源。
  2. wxlibrary arr资源项目,需要引入的资源包项目。
  3. aar文件生成,在工具栏直接Gradle - (项目名) - wxlibrary - Tasks - build - assemble,直到编译完成
  4. aar文件位置,打开项目所在文件夹,找到 wxlibrary\build\outputs\aar 下。

二、工程引入工具包

下载项目,可以在APP项目的libs文件下找到*.aar文件(已编译为最新版),选择其中一个引入自己的工程

引入微信工具包及微信SDK

dependencies {
//引入wxlibrary.aar资源
implementation files('libs/wxlibrary-release.aar')
//引入wxlibrary.aar的依赖资源,以下2个
implementation 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.6.5'
//eventbus,引入后你的项目将支持EventBus,EventBus是一种用于Android的事件发布-订阅总线,替代广播的传值方式,使用方法可以度娘查询。
implementation 'org.greenrobot:eventbus:3.1.1'
...
}

三、工具包初始准备工作

  • 工程继承WxApplication 或者 application 的 onCreate 下使用,获取 APPkey 和AppSecret需要使用mete-data方式获取。 isCheckSignature() 与 isNowRegister() 默认即可
    WxApiUtil.getInstance().init(getApplicationContext(), true, true);
  • APPkeyAppSecret,需要使用mete-data方式进行赋值

方式一,manifest下覆盖mete-data资源

 
...>



android:name="WX_LIBRARY_WX_APP_KEY"
android:value="123456s"
tools:replace="android:value"/>


android:name="WX_LIBRARY_WX_APP_SECRET"
android:value="567890a"
tools:replace="android:value"/>



方式二,manifest下不覆盖mete-data资源,在gradle(app)下赋值

android {
...
defaultConfig {
...

//todo 微信appKey和appSecret赋值的方法二,2个参数都需要赋值,secret不需要时赋值为空字符串即可
manifestPlaceholders = [
WX_LIBRARY_WX_APP_KEY: '',
WX_LIBRARY_WX_APP_SECRET: ''
]
}
...
}

四、登录、分享和支付的使用,链式写法一句搞定

1. 登录使用

// 注意以下注册回调事件不注册则不会触发
WxLoginUtil.newInstance()
.setSucceed((code) -> {
// 登录过程回调成功 code为微信返回的code
// 如果需要在app获取openID,则在此处使用code向微信服务器请求获取openID。
// 使用WxApiGlobal.getInstance().getAppKey()和WxApiGlobal.getInstance().getAppSecret()获取微信的必要参数,使用前请确保已填写正确参数
return;
})
.setNoInstalled((() -> {
// 微信客户端未安装
return;
}))
.setUserCancel(() -> {
// 用户取消
return;
})
.setFail((errorCode, errStr) -> {
// 其他类型错误, errorCode为微信返回的错误码
return;
})
//发起登录请求
.logIn();
2. 分享使用,注意由于微信分享变更,分享时只要唤起微信客户端,无论是否真正分享,都会返回成功

// 注意以下注册回调事件不注册则不会触发
WxShareUtil.newInstance()
.setSucceed(() -> {
// 分享过程回调成功
})
.setNoInstalled((() -> {
// 微信客户端未安装
}))
.setUserCancel(() -> {
// 用户取消,由于微信调整,用户取消状态不会触发
})
.setFail((errorCode, errStr) -> {
// 其他类型错误, errorCode为微信返回的错误码
})
//发起分享请求
.shareTextMessage("内容", "标题", "描述", ShareType.WXSceneTimeline);
3. 支付使用

// req.appId = json.getString("appid");
// req.partnerId = json.getString("partnerid");
// req.prepayId = json.getString("prepayid");
// req.nonceStr = json.getString("noncestr");
// req.timeStamp = json.getString("timestamp");
// req.packageValue = json.getString("package");
// req.sign = json.getString("sign");
// 此json文本需要包含以上所需字段,或者使用实体方式,不列举
// 注意以下注册回调事件不注册则不会触发
WxPayUtil.newInstance()
.setSucceed(() -> {
// sdk支付成功,向微信服务器查询下具体结果吧
})
.setNoInstalled((() -> {
// 微信客户端未安装
}))
.setUserCancel(() -> {
// 用户取消
})
.setFail((errorCode, errStr) -> {
// 其他类型错误, errorCode为微信返回的错误码
})
//发起分享请求
.payWeChat("json文本");

五、测试说明

由于微信需要在后台配置签名信息,而测试时不能修改一次打包一次进行测试,所以配置项目的签名信息即可在debug模式下使用正式版签名信息。

android {
signingConfigs {
release {
storeFile file('key文件位置,可写相对位置。默认是相对于app的文件夹下')
storePassword 'key文件密码'
keyAlias = '打包别名'
keyPassword '别名密码'
}
}
...
buildTypes {
debug {
signingConfig signingConfigs.release
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}

}


代码下载:mjsoftking-wxlibraryapp-master.zip

收起阅读 »

Android仿微信录制音视频的管理工具

ecorderManager因为在项目中经常需要使用音视频录制,所以写了一个公共库RecorderManager,欢迎大家使用。最新0.4.0-beta.3版本: 1.升级依赖 2.移除EasyPermissions和废弃方法,使用新API registerF...
继续阅读 »

ecorderManager

因为在项目中经常需要使用音视频录制,所以写了一个公共库RecorderManager,欢迎大家使用。

最新0.4.0-beta.3版本: 1.升级依赖 2.移除EasyPermissions和废弃方法,使用新API registerForActivityResult,请采用Java1.8以上版本 3.重构框架,优化代码 4.库调用做部分调整,详见下方文档说明 5.欢迎大家测试反馈完善功能

0.3.2版本:1.移除strings.xml中app_name 2.升级kotlin

0.3.1版本更新:详情见文档 1.新增最小录制时间设置RecordVideoOption.setMinDuration(//最小录制时长(秒数,最小是1,会自动调整不大于最大录制时长)) 2.优化代码

0.3-beta.2版本更新: 1.重构项目代码,kotlin改写部分功能 2.移除rxjava库,减少依赖 3.升级最新SDK 4.新增闪光灯功能,增加计时前提示文本设置 5.增加国际化支持,英文和中文 6.修复已知问题,优化代码 7.对外用户调用API改动较少,主要为内部调整,见下方文档,欢迎大家测试反馈完善功能

0.2.29版本更新: 1.新增圆形进度按钮配置功能 2.新增指定前后置摄像头功能 3.优化代码,调整启动视频录制配置项

0.2.28版本更新: 1.优化视频录制结果获取方式 2.优化代码

0.2.27版本更新: 1.视频录制界面RecordVideoRequestOption新增RecorderOption和hideFlipCameraButton配置 2.优化代码

0.2.26版本更新: 1.项目迁移至AndroidX, 引入Kotlin

0.2.25版本更新: 1.优化权限自动申请,可自动调起视频录制界面 2.规范图片资源命名

一.效果展示

仿微信界面视频录制

2.音频录制界面比较简单,就不放图了

二.引用

1.Add it in your root build.gradle at the end of repositories

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

2.Add the dependency

dependencies {
implementation 'com.github.MingYueChunQiu:RecorderManager:0.3.2'
}

三.使用

1.音频录制

采用默认配置录制

mRecorderManager.recordAudio(mFilePath);

自定义配置参数录制

mRecorderManager.recordAudio(new RecorderOption.Builder()
.setAudioSource(MediaRecorder.AudioSource.MIC)
.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
.setAudioSamplingRate(44100)
.setBitRate(96000)
.setFilePath(path)
.build());

2.视频录制

(1).可以直接使用RecordVideoActivity,实现了仿微信风格的录制界面

从0.2.18开始改为类似

RecorderManagerFactory.getRecordVideoRequest().startRecordVideo(MainActivity.this, 0);

从0.4.0-beta版本开始,因为采用registerForActivityResult,所以直接传入结果回调

      RecorderManagerProvider.getRecordVideoRequester().startRecordVideo(MainActivity.this, new RMRecordVideoResultCallback() {
@Override
public void onResponseRecordVideoResult(@NonNull RecordVideoResultInfo info) {
Log.e("MainActivity", "onActivityResult: " + info.getDuration() + " " + info.getFilePath());
Toast.makeText(MainActivity.this, info.getDuration() + " " + info.getFilePath(), Toast.LENGTH_SHORT).show();
}

@Override
public void onFailure(@NonNull RecorderManagerException e) {
Log.e("MainActivity", "onActivityResult: " + e.getErrorCode() + " " + e.getMessage());
}
});

从0.4.0-beta版本开始:RecorderManagerFactory重命名为RecorderManagerProvider RecorderManagerFactory中可以拿到IRecordVideoPageRequester,在IRecordVideoPageRequester接口中

/**
* 以默认配置打开录制视频界面
*
* @param activity Activity
* @param requestCode 请求码
*/
void startRecordVideo(@NonNull FragmentActivity activity, int requestCode);

/**
* 以默认配置打开录制视频界面
*
* @param fragment Fragment
* @param requestCode 请求码
*/
void startRecordVideo(@NonNull Fragment fragment, int requestCode);

/**
* 打开录制视频界面
*
* @param activity Activity
* @param requestCode 请求码
* @param option 视频录制请求配置信息类
*/
void startRecordVideo(@NonNull FragmentActivity activity, int requestCode, @Nullable RecordVideoRequestOption option);

/**
* 打开录制视频界面
*
* @param fragment Fragment
* @param requestCode 请求码
* @param option 视频录制请求配置信息类
*/
void startRecordVideo(@NonNull Fragment fragment, int requestCode, @Nullable RecordVideoRequestOption option);

从0.4.0-beta版本开始:

public interface IRecordVideoPageRequester extends IRMRequester {

/**
* 以默认配置打开录制视频界面
*
* @param activity Activity
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

/**
* 以默认配置打开录制视频界面
*
* @param fragment Fragment
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull Fragment fragment, @NonNull RMRecordVideoResultCallback callback);

/**
* 打开录制视频界面
*
* @param activity Activity
* @param option 视频录制请求配置信息类
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull FragmentActivity activity, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);

/**
* 打开录制视频界面
*
* @param fragment Fragment
* @param option 视频录制请求配置信息类
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull Fragment fragment, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);
}

RecordVideoRequestOption可配置最大时长(秒)和文件保存路径

public class RecordVideoRequestOption implements Parcelable {

private String filePath;//文件保存路径
private int maxDuration;//最大录制时间(秒数)
private RecordVideoOption recordVideoOption;//录制视频配置信息类(里面配置的filePath和maxDuration会覆盖外面的)
}

RecordVideoActivity里已经配置好了默认参数,可以直接使用,然后在onActivityResult里拿到视频路径的返回值 返回值为RecordVideoResultInfo

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK && requestCode == 0) {
RecordVideoResultInfo info = data.getParcelableExtra(EXTRA_RECORD_VIDEO_RESULT_INFO);

//从0.2.28版本开始可以使用下面这种方式,更安全更灵活,兼容性强
RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResult(data);
//从0.3版本开始
RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResultParser().parseRecordVideoResult(data);

if (info != null) {
Log.e("MainActivity", "onActivityResult: " + " "
+ info.getDuration() + " " + info.getFilePath());
}
}
}

从0.4.0-beta.1版本开始: 由于采用Android新API registerForActivityResult,所以startActivityForResult等相关方法皆已废弃,相关回调将直接通过RMRecordVideoResultCallback传递

interface RMRecordVideoResultCallback {

fun onResponseRecordVideoResult(info: RecordVideoResultInfo)

fun onFailure(e: RecorderManagerException)
}

通过下列IRecordVideoPageRequester相关方法,调用时同时传入响应结果回调
void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

(2).如果想要界面一些控件的样式,可以继承RecordVideoActivity,里面提供了几个protected方法,可以拿到界面的一些控件

/**
* 获取计时控件
*
* @return 返回计时AppCompatTextView
*/
protected AppCompatTextView getTimingView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getTimingView();
}

/**
* 获取圆形进度按钮
*
* @return 返回进度CircleProgressButton
*/
protected CircleProgressButton getCircleProgressButton() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getCircleProgressButton();
}

/**
* 获取翻转摄像头控件
*
* @return 返回翻转摄像头AppCompatImageView
*/
public AppCompatImageView getFlipCameraView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getFlipCameraView();
}

/**
* 获取播放控件
*
* @return 返回播放AppCompatImageView
*/
protected AppCompatImageView getPlayView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getPlayView();
}

/**
* 获取取消控件
*
* @return 返回取消AppCompatImageView
*/
protected AppCompatImageView getCancelView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getCancelView();
}

/**
* 获取确认控件
*
* @return 返回确认AppCompatImageView
*/
protected AppCompatImageView getConfirmView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getConfirmView();
}

/**
* 获取返回控件
*
* @return 返回返回AppCompatImageView
*/
protected AppCompatImageView getBackView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getBackView();
}

想要替换图标资源的话,提供下列名称图片

rm_record_video_flip_camera.png
rm_record_video_cancel.png
rm_record_video_confirm.png
rm_record_video_play.png
rm_record_video_pull_down.png
rm_record_video_flashlight_turn_off.png
rm_record_video_flashlight_turn_on.png

(3).同时提供了对应的RecordVideoFragment,实现与RecordVideoActivity同样的功能,实际RecordVideoActivity就是包裹了一个RecordVideoFragment

1.创建RecordVideoFragment

/**
* 获取录制视频Fragment实例(使用默认配置项)
*
* @param filePath 存储文件路径
* @return 返回RecordVideoFragment
*/
public static RecordVideoFragment newInstance(@Nullable String filePath) {
return newInstance(filePath, 30);
}

/**
* 获取录制视频Fragment实例(使用默认配置项)
*
* @param filePath 存储文件路径
* @param maxDuration 最大时长(秒数)
* @return 返回RecordVideoFragment
*/
public static RecordVideoFragment newInstance(@Nullable String filePath, int maxDuration) {
return newInstance(new RecordVideoOption.Builder()
.setRecorderOption(new RecorderOption.Builder().buildDefaultVideoBean(filePath))
.setMaxDuration(maxDuration)
.build());
}

/**
* 获取录制视频Fragment实例
*
* @param option 录制配置信息对象
* @return 返回RecordVideoFragment
*/
public static RecordVideoFragment newInstance(@Nullable RecordVideoOption option) {
RecordVideoFragment fragment = new RecordVideoFragment();
Bundle args = new Bundle();
args.putParcelable(BUNDLE_EXTRA_RECORD_VIDEO_OPTION, option == null ? new RecordVideoOption() : option);
fragment.setArguments(args);
return fragment;
}

2.然后添加RecordVideoFragment到自己想要的地方就可以了 3.可以设置OnRecordVideoListener,拿到各个事件的回调

public class RecordVideoOption:

private RecorderOption recorderOption;//录制配置信息
private RecordVideoButtonOption recordVideoButtonOption;//录制视频按钮配置信息类
private int minDuration;//最小录制时长(秒数,最小是1,会自动调整不大于最大录制时长),可以和timingHint配合使用
private int maxDuration;//最大录制时间(秒数)
private RecorderManagerConstants.CameraType cameraType;//摄像头类型
private boolean hideFlipCameraButton;//隐藏返回翻转摄像头按钮
private boolean hideFlashlightButton;//隐藏闪光灯按钮
private String timingHint;//录制按钮上方提示语句(默认:0:%s),会在计时前显示
private String errorToastMsg;//录制发生错误Toast(默认:录制时间小于1秒,请重试)

原OnRecordVideoListener现已改为RMOnRecordVideoListener,并从RecordVideoOption中移除,主要用于用户自己activity或fragment实现此接口,用于承载RecordVideoFragment,获取相关步骤回调

interface RMOnRecordVideoListener {

/**
* 当完成一次录制时回调
*
* @param filePath 视频文件路径
* @param videoDuration 视频时长(毫秒)
*/
fun onCompleteRecordVideo(filePath: String?, videoDuration: Int)

/**
* 当点击确认录制结果按钮时回调
*
* @param filePath 视频文件路径
* @param videoDuration 视频时长(毫秒)
*/
fun onClickConfirm(filePath: String?, videoDuration: Int)

/**
* 当点击取消按钮时回调
*
* @param filePath 视频文件路径
* @param videoDuration 视频时长(毫秒)
*/
fun onClickCancel(filePath: String?, videoDuration: Int)

/**
* 当点击返回按钮时回调
*/
fun onClickBack()
}

4.RecordVideoButtonOption是圆形进度按钮配置类

	private @ColorInt
int idleCircleColor;//空闲状态内部圆形颜色
private @ColorInt
int pressedCircleColor;//按下状态内部圆形颜色
private @ColorInt
int releasedCircleColor;//释放状态内部圆形颜色
private @ColorInt
int idleRingColor;//空闲状态外部圆环颜色
private @ColorInt
int pressedRingColor;//按下状态外部圆环颜色
private @ColorInt
int releasedRingColor;//释放状态外部圆环颜色
private int idleRingWidth;//空闲状态外部圆环宽度
private int pressedRingWidth;//按下状态外部圆环宽度
private int releasedRingWidth;//释放状态外部圆环宽度
private int idleInnerPadding;//空闲状态外部圆环与内部圆形之间边距
private int pressedInnerPadding;//按下状态外部圆环与内部圆形之间边距
private int releasedInnerPadding;//释放状态外部圆环与内部圆形之间边距
private boolean idleRingVisible;//空闲状态下外部圆环是否可见
private boolean pressedRingVisible;//按下状态下外部圆环是否可见
private boolean releasedRingVisible;//释放状态下外部圆环是否可见

5.RecorderOption是具体的录制参数配置类

	private int audioSource;//音频源
private int videoSource;//视频源
private int outputFormat;//输出格式
private int audioEncoder;//音频编码格式
private int videoEncoder;//视频编码格式
private int audioSamplingRate;//音频采样频率(一般44100)
private int bitRate;//视频编码比特率
private int frameRate;//视频帧率
private int videoWidth, videoHeight;//视频宽高
private int maxDuration;//最大时长
private long maxFileSize;//文件最大大小
private String filePath;//文件存储路径
private int orientationHint;//视频录制角度方向

(4).如果想自定义自己的界面,可以直接使用RecorderManagerable类

1.通过RecorderManagerFactory获取IRecorderManager 从0.4.0-beta版本开始:RecorderManagerFactory重命名为RecorderManagerProvider

public class RecorderManagerFactory {

private RecorderManagerFactory() {
}

/**
* 创建录制管理类实例(使用默认录制类)
*
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance() {
return newInstance(new RecorderHelper());
}

/**
* 创建录制管理类实例(使用默认录制类)
*
* @param intercept 录制管理器拦截器
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance(@NonNull IRecorderManagerInterceptor intercept) {
return newInstance(new RecorderHelper(), intercept);
}

/**
* 创建录制管理类实例
*
* @param helper 实际录制类
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance(@NonNull IRecorderHelper helper) {
return newInstance(helper, null);
}

/**
* 创建录制管理类实例
*
* @param helper 实际录制类
* @param intercept 录制管理器拦截器
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance(@NonNull IRecorderHelper helper, @Nullable IRecorderManagerInterceptor intercept) {
return new RecorderManager(helper, intercept);
}

@NonNull
public static IRecordVideoRequest getRecordVideoRequest() {
return new RecordVideoPageRequest();
}

//0.3之后版本通过解析器来进行处理数据
@NonNull
public static IRecordVideoResultParser getRecordVideoResultParser() {
return new RecordVideoResultParser();
}
}

它们返回的都是IRecorderManager 接口类型,RecorderManager 是默认的实现类,RecorderManager 内持有一个真正进行操作的RecorderHelper。

public interface IRecorderManager extends IRecorderHelper {

/**
* 设置录制对象
*
* @param helper 录制对象实例
*/
void setRecorderHelper(@NonNull IRecorderHelper helper);

/**
* 获取录制对象
*
* @return 返回录制对象实例
*/
@NonNull
IRecorderHelper getRecorderHelper();

/**
* 初始化相机对象
*
* @param holder Surface持有者
* @return 返回初始化好的相机对象
*/
@Nullable
Camera initCamera(@NonNull SurfaceHolder holder);

/**
* 初始化相机对象
*
* @param cameraType 指定的摄像头类型
* @param holder Surface持有者
* @return 返回初始化好的相机对象
*/
@Nullable
Camera initCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

/**
* 打开或关闭闪光灯
*
* @param turnOn true表示打开,false关闭
*/
boolean switchFlashlight(boolean turnOn);

/**
* 翻转摄像头
*
* @param holder Surface持有者
* @return 返回翻转并初始化好的相机对象
*/
@Nullable
Camera flipCamera(@NonNull SurfaceHolder holder);

/**
* 翻转到指定类型摄像头
*
* @param cameraType 摄像头类型
* @param holder Surface持有者
* @return 返回翻转并初始化好的相机对象
*/
@Nullable
Camera flipCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

/**
* 获取当前摄像头类型
*
* @return 返回摄像头类型
*/
@NonNull
RecorderManagerConstants.CameraType getCameraType();

/**
* 释放相机资源
*/
void releaseCamera();

}

RecorderManagerIntercept实现IRecorderManagerInterceptor接口,用户可以直接继承RecorderManagerIntercept,它里面所有方法都是空实现,可以自己改写需要的方法

public interface or extends ICameraInterceptor {}

IRecorderHelper是一个接口类型,由实现IRecorderHelper的子类来进行录制操作,默认提供的是RecorderHelper,RecorderHelper实现了IRecorderHelper。

public interface IRecorderHelper {

/**
* 录制音频
*
* @param path 文件存储路径
* @return 返回是否成功开启录制,成功返回true,否则返回false
*/
boolean recordAudio(@NonNull String path);

/**
* 录制音频
*
* @param option 存储录制信息的对象
* @return 返回是否成功开启录制,成功返回true,否则返回false
*/
boolean recordAudio(@NonNull RecorderOption option);

/**
* 录制视频
*
* @param camera 相机
* @param surface 表面视图
* @param path 文件存储路径
* @return 返回是否成功开启录制,成功返回true,否则返回false
*/
boolean recordVideo(@Nullable Camera camera, @Nullable Surface surface, @Nullable String path);

/**
* 录制视频
*
* @param camera 相机
* @param surface 表面视图
* @param option 存储录制信息的对象
* @return 返回是否成功开启视频录制,成功返回true,否则返回false
*/
boolean recordVideo(@Nullable Camera camera, @Nullable Surface surface, @Nullable RecorderOption option);

/**
* 释放资源
*/
void release();

/**
* 获取录制器
*
* @return 返回实例对象
*/
@NonNull
MediaRecorder getMediaRecorder();

/**
* 获取配置信息对象
*
* @return 返回实例对象
*/
@Nullable
RecorderOption getRecorderOption();
}

2.拿到后创建相机对象

		if (mCamera == null) {
mCamera = mManager.initCamera(mCameraType, svVideoRef.get().getHolder());
mCameraType = mManager.getCameraType();
}

3.录制

isRecording = mManager.recordVideo(mCamera, svVideoRef.get().getHolder().getSurface(), mOption.getRecorderOption());

4.释放

	    mManager.release();
mManager = null;
mCamera = null;

代码下载:MingYueChunQiu-RecorderManager-master.zip
收起阅读 »

kotlin编写的 Android 开源播放器, 开箱即用

介绍功能特性1、通过 dependence 引入MXVideo2、页面集成3、开始播放MXPlaySource 可选参数说明:4、监听播放进度5、全屏返回 + 释放资源功能相关

MXVideo

介绍

基于饺子播放器、kotlin编写的 Android 开源播放器, 开箱即用,欢迎提 issue 和 pull request 最新版本:

功能特性

  • 任意播放器内核(包含开源IJK、谷歌Exo、阿里云等等)
  • 单例播放,只能同时播放一个节目
  • 0代码集成全屏功能
  • 可以调节音量、屏幕亮度
  • 可以注册播放状态监听回调
  • 播放器高度可以根据视频高度自动调节
  • 播放器支持设置宽高比,设置宽高比后,高度固定。
  • 自动保存与恢复播放进度(可关闭)
  • 支持循环播放、全屏时竖屏模式、可关闭快进快退功能、可关闭全屏功能、可关闭非WiFi环境下流量提醒

1、通过 dependence 引入MXVideo

    dependencies {
implementation 'com.gitee.zhangmengxiong:MXVideo:x.x.x'
}

2、页面集成

        
android:id="@+id/mxVideoStd"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

3、开始播放
// 设置播放占位图
Glide.with(this).load("http://www.xxx.com/xxx.png").into(mxVideoStd.getPosterImageView())

// 默认从上一次进度播放
mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"))
mxVideoStd.startPlay()

// 从头开始播放
mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"), seekTo = 0)
mxVideoStd.startPlay()

// 从第10秒开始播放
mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"), seekTo = 10)
mxVideoStd.startPlay()


MXPlaySource 可选参数说明:

参数说明默认值
title标题""
headerMap网络请求头部null
changeOrientationWhenFullScreen全屏时是否需要变更Activity方向,如果 = null,会自动根据视频宽高来判断null
isLooping是否循环播放false
enableSaveProgress是否存储、读取播放进度true
isLiveSource是否直播源,当时直播时,不显示进度,无法快进快退暂停false

4、监听播放进度

mxVideoStd.addOnVideoListener(object : MXVideoListener() {
// 播放状态变更
override fun onStateChange(state: MXState) {
}

// 播放时间变更
override fun onPlayTicket(position: Int, duration: Int) {
}
})

5、全屏返回 + 释放资源

这里MXVideo默认持有当前播放的MXVideoStd,可以使用静态方法操作退出全屏、释放资源等功能。

也可以直接使用viewId:mxVideoStd.isFullScreen(),mxVideoStd.isFullScreen(),mxVideoStd.release() 等方法。

    override fun onBackPressed() {
if (MXVideo.isFullScreen()) {
MXVideo.gotoNormalScreen()
return
}
super.onBackPressed()
}

override fun onDestroy() {
MXVideo.releaseAll()
super.onDestroy()
}

功能相关

  • 切换播放器内核
// 默认MediaPlayer播放器,库默认内置
com.mx.video.player.MXSystemPlayer

// 谷歌的Exo播放器
com.mx.mxvideo_demo.player.MXExoPlayer

// IJK播放器
com.mx.mxvideo_demo.player.MXIJKPlayer

// 设置播放源是可以设置内核,默认 = MXSystemPlayer
mxVideoStd.setSource(MXPlaySource(Uri.parse("xxx"), "xxx"), player = MXSystemPlayer::class.java)
  • 视频渲染旋转角度
// 默认旋转角度 = MXOrientation.DEGREE_0
mxVideoStd.setOrientation(MXOrientation.DEGREE_90)
  • 视频填充规则
// 强制填充宽高 MXScale.FILL_PARENT
// 根据视频大小,自适应宽高 MXScale.CENTER_CROP

// 默认填充规则 = MXScale.CENTER_CROP
mxVideoStd.setScaleType(MXScale.CENTER_CROP)
  • MXVideoStd 控件宽高约束

在页面xml中添加,layout_width一般设置match_parent,高度wrap_content

    
android:id="@+id/mxVideoStd"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

可以设置任意宽高比,如果设置宽高比,则控件高度需要设置android:layout_height="wrap_content",否则不生效。

当取消约束、MXVideo高度自适应、填充规则=MXScale.CENTER_CROP时,控件高度会自动根据视频宽高自动填充高度

// MXVideoStd控件设置宽高比= 16:9
mxVideoStd.setDimensionRatio(16.0 / 9.0)

// MXVideoStd控件设置宽高比= 4:3
mxVideoStd.setDimensionRatio(4.0 / 3.0)

// 取消约束
mxVideoStd.setDimensionRatio(0.0)
  • 进度跳转
// 进度单位:秒  可以在启动播放后、错误或播完之前调用
mxVideoStd.seekTo(55)
  • 设置不能快进快退
// 播放前设置 默认=true
mxVideoStd.getConfig().canSeekByUser = false
  • 设置不能全屏
// 播放前设置 默认=true
mxVideoStd.getConfig().canFullScreen = false
  • 设置不显示控件右上角时间
// 播放前设置 默认=true
mxVideoStd.getConfig().canShowSystemTime = false
  • 设置不显示控件右上角电量图
// 播放前设置 默认=true
mxVideoStd.getConfig().canShowBatteryImg = false
  • 设置关闭WiFi环境播放前提醒
// 播放前设置 默认=true
mxVideoStd.getConfig().showTipIfNotWifi = false
  • 设置播放完成后自动退出全屏
// 播放前设置 默认=true
mxVideoStd.getConfig().gotoNormalScreenWhenComplete = true
  • 设置播放错误后自动退出全屏
// 播放前设置 默认=true
mxVideoStd.getConfig().gotoNormalScreenWhenError = true
  • 设置屏幕方向根据重力感应自动进入全屏、小屏模式
// 播放前设置 默认=false
mxVideoStd.getConfig().autoRotateBySensor = true


代码下载:zhangmengxiong-MXVideo-master.zip

iOS逆向(8)-Monkey、Logos

由于最近微信大佬发飙,罚了红包外挂5000万大洋,这就让人很慌了,别说罚我5000万,5000块我都吃不消。所以笔者决定以后不用微信做例子了。换成优酷了😈。本文会对优酷的设置页面增加一个开启/关闭屏蔽广告的Cell(仅UI)。效果可见下文配图。在之前的几篇文章...
继续阅读 »

由于最近微信大佬发飙,罚了红包外挂5000万大洋,这就让人很慌了,别说罚我5000万,5000块我都吃不消。所以笔者决定以后不用微信做例子了。换成优酷了😈。

本文会对优酷的设置页面增加一个开启/关闭屏蔽广告的Cell(仅UI)。效果可见下文配图。

在之前的几篇文章里已经介绍了APP重签名,代码注入,Hook原理,可以发现,将工程建好,脚本写好,我们就可以以代价非常小的方式对一个第三方的APP进行分析。
那么是否一种工具,可以将重签名,代码注入,Hook源代码,class-dump,Cydia Substrate,甚至是恢复符号表这些功能,集成在一个工程里面,让真正的逆向小白也能享受逆向的乐趣呢?
答案是肯定的,Monkey就是这样的一个非越狱插件开发集成神器!

老规矩,片头先上福利:点击下载demo
这篇文章会用到的工具有:

1、MonkeyDev
2、博主自己砸壳的优酷ipa包 提取码: xtua
3、砸壳后的SimpleAppDemo.ipa 提取码: afnc

一、Monkey

什么是Monkey?
原有iOSOpenDev的升级,非越狱插件开发集成神器!

可以使用Xcode开发CaptainHook Tweak、Logos Tweak 和 Command-line Tool,在越狱机器开发插件,这是原来iOSOpenDev功能的迁移和改进。

1、只需拖入一个砸壳应用,自动集成class-dump、restore-symbol、* Reveal、Cycript和注入的动态库并重签名安装到非越狱机器。
2、支持调试自己编写的动态库和第三方App
3、支持通过CocoaPods第三方应用集成SDK以及非越狱插件,简单来说就是通过CocoaPods搭建了一个非越狱插件商店。

环境要求

使用工具前确保如下几点:

1、安装最新的theos

sudo git clone --recursive https://github.com/theos/theos.git /opt/theos

2、安装ldid(如安装theos过程安装了ldid,跳过)

brew install ldid

安装

你可以通过以下命令选择指定的Xcode进行安装:

sudo xcode-select -s /Applications/Xcode-beta.app

默认安装的Xcode为:

xcode-select -p

执行安装命令:

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-install)"

卸载

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-uninstall)"

更新
如果没有发布特殊说明,使用如下命令更新即可:

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-update)"

安装/更新之后重启下Xcode再新建项目。如果看到如下选项,即代表安装成功,如果没有,重复上面步骤再来一遍。


二、Logos

Logos是Thoes开发的一套组件,可非常方便用于的Hook OC代码。

接下来我们就介绍下Logos的简单用法,最后运用Monkey和Logos给优酷增加一点UI。

1、创建一个简单的工程
创建工程SimpleAppDemo,里面只有一个按钮,点击按钮弹出一个Alert。 点击下载:SimpleAppDemo
按钮对应的方法为:

- (IBAction)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来啦" message:@"老弟😁😁😁" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
}

2、砸壳
对SimpleAppDemo参数的ipa文件进行砸壳,砸壳过程就不在这详细描述了,这里有笔者已经砸壳好的ipa:SimpleAppDemo.ipa 提取码: afnc

3、新建一个Monkey工程
取名LogosDemo,将下面下载好的SimpleAppDemo.ipa,放到工程对应的目录下:


配好证书(随意一个能在手机上运行的证书即可),Run。运行成功~

4、玩转Logos
在上一步建好的Monkey工程中,可以发现在目录有一个Logos目录:


默认有两个文件LogosDemoDylib.xm和LogosDemoDylib.mm。
其中Logos语句就是写在LogosDemoDylib.xm中的,LogosDemoDylib.mm是根据LogosDemoDylib.xm中的内容自动生成的。
接下来,咱们根据几个需求来介绍Logos的一些常用的用法。

1、更改点击按钮的弹框内容(hook)
由于需要更改弹窗,所以首先导入UIKit框架。

#import <UIKit/UIKit.h>

由于咱们手上有源码,所以可以直接跳过动态分析的这一步,直接就知道按钮所处的页面是叫做ViewController,按钮的响应方法是:

- (IBAction)tapAction:(id)sender

利用hook命令:

#import <UIKit/UIKit.h>

// hook + 类名
%hook ViewController
// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
}

%end

运行项目,发现按钮已经被成功hook了。


2、调用原方法(orig)

#import <UIKit/UIKit.h>

%hook ViewController

- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
// 调用原方法
%orig;
}

%end

3、新增一个方法,并且调用(new)

由于在Monkey工程里面是编译不到源码的,所以无论是新增的方法,还是调用原工程中的方法,都是无法通过编译的,所以都需要使用interface申明每一个方法。

#import <UIKit/UIKit.h>

// 这里只是为了申明
@interface ViewController

- (void)newFunC;

@end

%hook ViewController

// 新增方法关键字new
%new
- (void)newFunC{
NSLog(@"newFunC");
}

// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
[self newFunC];
// 调用原方法关键字orig
%orig;
}

%end

文中所有的Demo都在这可以下载到:Dmoe

Logos除了以上hook,end,orig,new这几种关键字,还有:
%subclass:增加一个类
%log:打印,类似NSLog
%group: 给代码分组,可以用于在不同环境加载不同的代码,比如iOS8加载group1,iOS9加载group2,如果部分中,默认所有代码在名为「_ungrouped」的隐藏分组中。
...

所有的Logos语法都可以在官方文档中查询得到。

5、给优酷加UI

首先在这里下载笔者自己砸壳后优酷ipa包(arm64架构的):优酷(砸壳).ipa 提取码: xtua

Step 1、新建工程YouKu

同样的新建一个Monkey工程,取名YouKu,将下载好的ipa包放入工程对应的TargetApp目录下。Run。同样是重签名成功。

在上面的Demo中,我们是对我们直接的工程进行HOOK,由于我们手上有源码,所以我们越过了最难的一个步骤:动态分析。
而我们现在要对优酷进行Hook,但我们手上是没有优酷的源码的,所以此时此刻就需要对其进行动态分析了。
下面我将结合Xcode和class dump对优酷的设置页面简单的进行分析。

Step 2、class dump

class-dump is a command-line utility for examining the Objective-C segment of Mach-O files. It generates declarations for the classes, categories and protocols. This is the same information provided by using 'otool -ov', but presented as normal Objective-C declarations.
简单说就是一个可以导出一个MachO文件的所有头文件信息(包括Extension)

在文首有提到Monkey除了重签名,还集成了class dump的功能,所以我们需要做的就仅仅是开启这个功能:


Run!成功之后可以发现在工程目录下多了一个文件夹Youkui4Phone_Headers,其中就是优酷的所有的头文件了。


Step 3、分析优酷设置页面

工程Run成功后,点击进入设置页面(不用登录),如下图:


我们现在要做的就是在这个页面的TableView的最后一行加上Cell,里面有个Switch,用于打开/关闭屏蔽广告功能(只是UI,这篇文章不牵扯到屏蔽广告的具体实现,如果你需要,点个小心心,持续关注我哦😀😀😀)。

利用伟大的Xcode我们可以非常清晰的看到,设置页面的DataSource和Delegate都是在SettingViewController中,


咱们就找到Hook的类名:SettingViewController
需要Hook的方法自然就是TableView的那些DataSource和Delegate了。

这里需要额外提到的一点是,在文章开始的时候就说了Monkey已经将Cydia Substrate集成进去了,所以我们可以直接使用Cydia Substrate的相关功能了。

在这里我们需要拿到这个页面TableView的对应的变量,我们就需要使用到Cydia Substrate的功能了。打开上文中获取到优酷的所有的头文件,所有SettingViewController,发现其只有一个TableView变量:_tabview。
那么毫无疑问,就是他了!
而获取它的方法是:

MSHookIvar <UITableView *>(self,"_tabview")

一个reloadData的简单使用:

[MSHookIvar <UITableView *>(self,"_tabview") reloadData];

其他的UI代码在这里就不一一解释了,全部代码如下,当然在Demo中也是有的,其中包括了数据的简单持久化功能:

#import <UIKit/UIKit.h>
#define FYDefaults [NSUserDefaults standardUserDefaults]
#define FYSwitchUserDefaultsKey @"FYSwitchUserDefaultsKey"

@interface SettingViewController
- (long long)numberOfSectionsInTableView:(id)arg1;
@end

%hook SettingViewController

%new
-(void)switchChangeAction:(UISwitch *)switchView{
[FYDefaults setBool:switchView.isOn forKey:FYSwitchUserDefaultsKey];
[FYDefaults synchronize];
[MSHookIvar <UITableView *>(self,"_tabview") reloadData];
}

//多少组
- (long long)numberOfSectionsInTableView:(id)arg1{
UITableView * tableView = MSHookIvar <UITableView *>(self,"_tabview");
NSLog(@"fy_numberOfSectionsInTableView:");
// 额外增加一个
return %orig+1;
}

//每组多少行
- (long long)tableView:(UITableView *)tableView numberOfRowsInSection:(long long)section{
NSLog(@"fy_numberOfRowsInSection:");
//定位设置界面,并且是最后一个
if(section == [self numberOfSectionsInTableView:tableView]-1){
return 1;
}
else{
return %orig;
}
}

//返回高度
- (double)tableView:
(UITableView *)tableView heightForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_heightForRowAtIndexPath:");
//定位设置界面,并且是最后一个
if([indexPath section] ==[self numberOfSectionsInTableView:tableView]-1){
return 44;
}
else{
return %orig;
}
}


//每一个Cell
- (id)tableView:(UITableView *)tableView cellForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_cellForRowAtIndexPath:");
//定位设置界面,并且是最后一组
if([indexPath section] == [self numberOfSectionsInTableView:tableView]-1){
UITableViewCell * cell = nil;
if([indexPath row] == 0){
static NSString *swCell = @"SwCellIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:swCell];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:nil];
}
cell.textLabel.text = @"免广告";
// 免广告开关
UISwitch *switchView = [[UISwitch alloc] init];
switchView.on = [FYDefaults boolForKey:FYSwitchUserDefaultsKey];
[switchView addTarget:self action:@selector(switchChangeAction:) forControlEvents:(UIControlEventValueChanged)];
cell.accessoryView = switchView;
cell.imageView.image = [UIImage imageNamed:([FYDefaults boolForKey:FYSwitchUserDefaultsKey] == 1) ? @"unlocked" : @"locked"];
}
cell.backgroundColor = [UIColor whiteColor];
return cell;

}else{
return %orig;
}
}

%end

最后的效果


6、为什么Monkey这么牛逼

查看重新编译后的app文件,可以发现其中的Framework多了很多东西:


从这可以得知,原来Monkey其实也是通过将诸多的动态库(包括自己的工程)注入的形式,实现了这些功能。

三、总结

在这片文章中主要介绍了Monkey的一些用法已经Logos的基本语法。而在上一篇其实留了一个小尾巴,就是Cycript,笔者将要在下一篇文章中重点讲解Cycript的安装,基础用法和高级用法。之所以放在下一篇,是因为Cycript配合Monkey将会有事半功倍的效果。

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

收起阅读 »

iOS 优秀框架之TYAttributedLabel(基于coreText的图文混排)

TYAttributedLabel1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数3、支持添加高度自定义文本属性4、支持添加属性文本,自定义链接,...
继续阅读 »

TYAttributedLabel

1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)
2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数
3、支持添加高度自定义文本属性
4、支持添加属性文本,自定义链接,新增高亮效果显示(文字和背景)
5、支持添加UIImage和UIView控件

demo演示


重点类简介

TYAttributedLabel

创建label(可接受文本及富文本)
设置字体间距
设置行间距
设置字体大小
设置view的位置和宽,会自动计算高度
设置链接文本,并用代理(TYAttributedLabelDelegate)方法完成点击后需完成的任务

TYImageStorage

可创建一个append在TYAttributedLabel后的图片控件,可自定义图片大小,及对齐样式

TYTextStorage

文本文件,可设置文本大小及字体颜色

TYTextContainer

属性文本生成器(使用 RegexKitLite)
具体代码及使用细节请看作者的demo(作者是华人),讲的很详细,这里就不再赘述

链接:TYAttributedLabel

链接:https://www.jianshu.com/p/5d81bf7e79c8

收起阅读 »

如何让10万条数据的小程序列表如丝般顺滑

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。一...
继续阅读 »

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:

‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。

一.小程序页面限制多少个wxml节点?

写了个小dome做了个测试。 listData的数据结构为:

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//20条数据
]
}]

页面渲染效果:




{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



2.dome2,删除了不必要的dom嵌套



{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



通过大致计算,一个小程序页面大概可以渲染2万个wxml节点 而小程序官方的性能测评得分条件为少于1000个wxml节点官方链接



二.列表页面优化

1.减少不必要的标签嵌套


由上面的测试dome可知,在不影响代码运行和可读性的前提下,尽量减少标签的嵌套,可以大幅的增加页面数据的列表条数,毕竟公司不是按代码行数发工资的。如果你的列表数据量有限,可以用这种方法来增加列表渲染条数。如果数据量很大,再怎么精简也超过2万的节点,这个方法则不适用。


2.优化setData的使用


图五所示,小程序setDate的性能会受到setData数据量大小和调用频率限制。所以要围绕减少每一次setData数据量大小,降低setData调用频率进行优化。
#####(1)删除冗余字段
后端的同事经常把数据从数据库中取出就直接返回给前端,不经过任何处理,所以会导致数据大量的冗余,很多字段根本用不到,我们需要把这些字段删除,减少setDate的数据大小。
#####(2)setData的进阶用法
通常,我们对data中数据的增删改操作,是把原来的数据取出,处理,然后用setData整体去更新,比如我们列表中使用到的上拉加载更多,需要往listData尾部添加数据:

newList=[{...},{...}];
this.setData({
listData:[...this.data.listData,...newList]
})

这样会导致 setDate的数据量越来越大,页面也越来越卡。

setDate的正确使用姿势

  • setDate修改数据

比如我们要修改数组listData第一个元素的isDisplay属性,我们可以这样操作:

let index=0;
this.setData({
[`listData[${index}].isDisplay`]:false,
})

如果我们想同时修改数组listData中下标从0到9的元素的isDisplay属性,那要如何处理呢?你可能会想到用for循环来执行setData

for(let index=0;index<10;index++){
this.setData({
[`listData[${index}].isDisplay`]:false,
})
}

那么这样就会导致另外一个问题,那就是listData的调用过于频繁,也会导致性能问题,正确的处理方式是先把要修改的数据先收集起来,然后调用setData一次处理完成:

let changeData={};
for(let index=0;index<10;index++){
changeData[[`listData[${index}].isDisplay`]]=false;
}
this.setData(changeData);



这样我们就把数组listData中下标从0到9的元素的isDisplay属性改成了false

  • setDate往数组末尾添加数据

如果只添加一条数据

let newData={...};
this.setData({
[`listData[${this.data.listData.length}]`]:newData
})

如果是添加多条数据

let newData=[{...},{...},{...},{...},{...},{...}];
let changeData={};
let index=this.data.listData.length
newData.forEach((item) => {
changeData['listData[' + (index++) + ']'] = item //赋值,索引递增
})
this.setData(changeData)

三.使用自定义组件

可以把列表的一行或者多行封装到自定义组件里,在列表页使用一个组件,只算一个节点,这样你的列表能渲染的数据可以成倍数的增加。组件内的节点数也是有限制的,但是你可以一层层嵌套组件实现列表的无限加载,如果你不怕麻烦的话

四.使用虚拟列表

经过上面的一系列操作后,列表的性能会得到很大的提升,但是如果数据量实在太大,wxml节点数也会超出限制,导致页面发生错误。我们的处理方法是使用虚拟列表,页面只渲染当前可视区域以及可视区域上下若干条数据的节点,通过isDisplay控制节点的渲染。

  • 可视区域上方:above
  • 可视区域:screen
  • 可视区域下方:below

1.listData数组的结构

使用二维数组,因为如果是一维数组,页面滚动需要用setData设置大量的元素isDispaly属性来控制列表的的渲染。而二维数组可以这可以一次调用setData控制十条,二十条甚至更多的数据的渲染。

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//二维数组中的条数根据项目实际情况
]
}]

2.必要的参数

data{
itemHeight:4520,//列表第一层dom高度,单位为rpx
itemPxHeight:'',//转化为px高度,因为小程序获取的滚动条高度单位为px
aboveShowIndex:0,//已渲染数据的第一条的Index
belowShowNum:0,//显示区域下方隐藏的条数
oldSrollTop:0,//记录上一次滚动的滚动条高度,判断滚动方向
prepareNum:5,//可视区域上下方要渲染的数量
throttleTime:200,//滚动事件节流的时间,单位ms
}

3.wxml的dom结构






{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}




4.获取列表第一层dom的px高度

let query = wx.createSelectorQuery();
query.select('.content').boundingClientRect(rect=>{
let clientWidth = rect.width;
let ratio = 750 / clientWidth;
this.setData({
itemPxHeight:Math.floor(this.data.itemHeight/ratio),
})
}).exec();

5.页面滚动时间节流

function throttle(fn){
let valid = true
return function() {
if(!valid){
return false
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn.call(this,arguments);
valid = true;
}, this.data.throttleTime)
}
}

6.页面滚动事件处理

onPageScroll:throttle(function(e){
let scrollTop=e[0].scrollTop;//滚动条高度
let itemNum=Math.floor(scrollTop/this.data.itemPxHeight);//计算出可视区域的数据Index
let clearindex=itemNum-this.data.prepareNum+1;//滑动后需要渲染数据第一条的index
let oldSrollTop=this.data.oldSrollTop;//滚动前的scrotop,用于判断滚动的方向
let aboveShowIndex=this.data.aboveShowIndex;//获取已渲染数据第一条的index
let listDataLen=this.data.listData.length;
let changeData={}
//向下滚动
if(scrollTop-oldSrollTop>0){
if(clearindex>0){
//滚动后需要变更的条数
for(let i=aboveShowIndex;i changeData[[`listData[${i}].isDisplay`]]=false;
let belowShowIndex=i+2*this.data.prepareNum;
if(i+2*this.data.prepareNum changeData[[`listData[${belowShowIndex}].isDisplay`]]=true;
}
}
}
}else{//向上滚动
if(clearindex>=0){
let changeData={}
for(let i=aboveShowIndex-1;i>=clearindex;i--){
let belowShowIndex=i+2*this.data.prepareNum
if(i+2*this.data.prepareNum<=listDataLen-1){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=false;
}
changeData[[`listData[${i}].isDisplay`]]=true;
}
}else{
if(aboveShowIndex>0){
for(let i=0;i this.setData({
[`listData[${i}].isDisplay`]:true,
})
}
}
}
}
clearindex=clearindex>0?clearindex:0
if(clearindex>=0&&!(clearindex>0&&clearindex==this.data.aboveShowIndex)){
changeData.aboveShowIndex=clearindex;
let belowShowNum=this.data.listData.length-(2*this.data.prepareNum+clearindex)
belowShowNum=belowShowNum>0?belowShowNum:0
if(belowShowNum>=0){
changeData.belowShowNum=belowShowNum
}
this.setData(changeData)
}
this.setData({
oldSrollTop:scrollTop
})
}),

经过上面的处理后,页面的wxml节点数量相对稳定,可能因为可视区域数据的index计算误差,页面渲染的数据有小幅度的浮动,但是已经完全不会超过小程序页面的节点数量的限制。理论上100万条数据的列表也不会有问题,只要你有耐心和精力一直划列表加载这么多数据。

7.待优化事项



  • 列表每一行的高度需要固定,不然会导致可视区域数据的index的计算出现误差

  • 渲染玩列表后往回来列表,如果手速过快,会导致above,below区域的数据渲染不过来,会出现短暂的白屏,白屏问题可以调整 prepareNum, throttleTime两个参数改善,但是不能完全解决(经过测试对比发现,即使不对列表进行任何处理,滑动速度过快也会发生短暂白屏的情况)。

  • 如果列表中有图片,above,below区域重新渲染时,图片虽然以经缓存在本地,不需要重新去服务器请求,但是重新渲染还是需要时间,尤其当你手速特别快时。可以根据上面的思路, isDisplay时只销毁非的节点,这样重新渲染就不需要渲染图片,但是这样节点数还是会增加,不过应该能满足大部分项目需求了,看自己项目怎么取舍。



原文:https://juejin.cn/post/6966904317148299271


收起阅读 »

iOS 使用Moya网络请求

Moya最新版本11.0.2由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。Moya11.0.2DemoMoya简介Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)...
继续阅读 »

Moya最新版本11.0.2

由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。
Moya11.0.2Demo

Moya简介

Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)安放网络请求,Moya 替你管理。

Moya有几个比较好的特性:

1、编译时检查正确的API端点访问.

2、使你定义不同端点枚举值对应相应的用途更加明晰.

3、提高测试地位从而使单元测试更加容易.

Swift我们用Alamofire来做网络库.而Moya在Alamofire的基础上又封装了一层,如下流程图说明Moya的简单工作流程图:


** Moya**的官方下载地址点我强大的Moya,有具体的使用方法在demo里面有说明。

本文主要介绍一下Moya的用法

1、设置请求头部信息
2、设置超时时间
3、自定义插件
4、自签名证书
注意:以下所出现的NetAPIManager跟官网上demo的** GitHub**是一样类型的文件,都是这个enum实现一个协议TargetType,点进去可以看到TargetType定义了我们发送一个网络请求所需要的东西,什么baseURL,parameter,method等一些计算性属性,我们要做的就是去实现这些东西,当然有带默认值的我们可以不去实现,但是设置头部信息跟超时时间就要修改这些系统默认设置了。

为了看得更加清楚,贴上NetAPIManager文件的内容

//
// NetAPIManager.swift
// NN110
//
// Created by 陈亦海 on 2017/5/12.
// Copyright © 2017年 陈亦海. All rights reserved.
//

import Foundation
import Moya


enum NetAPIManager {
case Show
case upload(bodyData: Data)
case download
case request(isTouch: Bool, body: Dictionary<String, Any>? ,isShow: Bool)
}


extension NetAPIManager: TargetType {
var baseURL: URL {//服务器地址

switch self {
case .request( _, _, _):
return URL(string: "https://www.pmphmall.com")!
default:
return URL(string: "https://httpbin.org")!
}


}

var path: String {//具体某个方法的路径
switch self {
case .Show:
return ""
case .upload(_):
return ""
case .request(_, _, _):
return "/app/json.do"
case .download:
return ""
}
}

var method: Moya.Method {//请求的方法 get或者post之类的
switch self {
case .Show:
return .get
case .request(_, _, _):
return .post
default:
return .post
}
}

var parameters: [String: Any]? {//请求的get post给服务器的参数
switch self {
case .Show:
return nil
case .request(_, _, _):
return ["msg":"H4sIAAAAAAAAA11SSZJFIQi7EqPAEgTvf6TP62W7sMoSQhKSWDrs6ZUKVWogLwYV7RjHFBZJlNlzloN6LVqID4a+puxqRdUKVNLwE1TRcZIC/fjF2rPotuXmb84r1gMXbiASZIZbhQdKEewJlz41znDkujCHuQU3dU7G4/PmVRnwArMLXukBv0J23XVahNO3VX35wlgce6TLUzzgPQJFuHngAczl6VhaNXpmRLxJBlMml6gdLWiXxTdO7I+iEyC7XuTirCQXOk4dotgArgkH/InxVjfNTnE/uY46++hyAiLFuFL4cv1Z8WH5DgB2GnvFXMh5gm53Tr13vqqrEYtcdXfkNsMwKB+9sAQ77grNJmquFWOhfXA/DELlMB0KKFtHOc/ronj1ml+Z7qas82L3VWiCVQ+HEitjTVzoFw8RisFN/jJxBY4awvq427McXqnyrfCsl7oeEU6wYgW9yJtj1lOkx0ELL5Fw4z071NaVzRA9ebxWXkFyothgbB445cpRmTC+//F73r1kOyQ3lTpec12XNDR00nnq5/YmJItW3+w1z27lSOLqgVctrxG4xdL9WVPdkH1tkiZ/pUKBGhADAAA="]
default:
return nil

}
}

var sampleData: Data { //编码转义
return "{}".data(using: String.Encoding.utf8)!
}

var task: Task { //一个请求任务事件

switch self {


case let .upload(data):
return .upload(.multipart([MultipartFormData(provider: .data(data), name: "file", fileName: "gif.gif", mimeType: "image/gif")]))

default:
return .request

}

}

var parameterEncoding: ParameterEncoding {//编码的格式
switch self {
case .request(_, _, _):
return URLEncoding.default
default:
return URLEncoding.default
}

}
//以下两个参数是我自己写,用来控制网络加载的时候是否允许操作,跟是否要显示加载提示,这两个参数在自定义插件的时候会用到
var touch: Bool { //是否可以操作

switch self {
case .request(let isTouch, _, _):
return isTouch
default:
return false
}

}

var show: Bool { //是否显示转圈提示

switch self {
case .request( _, _,let isShow):
return isShow
default:
return false
}

}


}

如何设置Moya请求头部信息

头部信息的设置在开发过程中很重要,如服务器生成的token,用户唯一标识等
我们直接上代码,不说那么多理论的东西,哈哈

// MARK: - 设置请求头部信息
let myEndpointClosure = { (target: NetAPIManager) -> Endpoint<NetAPIManager> in


let url = target.baseURL.appendingPathComponent(target.path).absoluteString
let endpoint = Endpoint<NetAPIManager>(
url: url,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
parameters: target.parameters,
parameterEncoding: target.parameterEncoding
)

//在这里设置你的HTTP头部信息
return endpoint.adding(newHTTPHeaderFields: [
"Content-Type" : "application/x-www-form-urlencoded",
"ECP-COOKIE" : ""
])

}

如何设置请求超时时间

// MARK: - 设置请求超时时间
let requestClosure = { (endpoint: Endpoint<NetAPIManager>, done: @escaping MoyaProvider<NetAPIManager>.RequestResultClosure) in

guard var request = endpoint.urlRequest else { return }

request.timeoutInterval = 30 //设置请求超时时间
done(.success(request))
}

自定义插件

自定义插件必须PluginType协议的两个方法willSend与didReceive

//
// MyNetworkActivityPlugin.swift
// NN110
//
// Created by 陈亦海 on 2017/5/10.
// Copyright © 2017年 CocoaPods. All rights reserved.
//

import Foundation
import Result
import Moya


/// Network activity change notification type.
public enum MyNetworkActivityChangeType {
case began, ended
}

/// Notify a request's network activity changes (request begins or ends).
public final class MyNetworkActivityPlugin: PluginType {



public typealias MyNetworkActivityClosure = (_ change: MyNetworkActivityChangeType, _ target: TargetType) -> Void
let myNetworkActivityClosure: MyNetworkActivityClosure

public init(newNetworkActivityClosure: @escaping MyNetworkActivityClosure) {
self.myNetworkActivityClosure = newNetworkActivityClosure
}

// MARK: Plugin

/// Called by the provider as soon as the request is about to start
public func willSend(_ request: RequestType, target: TargetType) {
myNetworkActivityClosure(.began,target)
}

/// Called by the provider as soon as a response arrives, even if the request is cancelled.
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
myNetworkActivityClosure(.ended,target)
}
}

使用自定义插件方法

// MARK: - 自定义的网络提示请求插件
let myNetworkPlugin = MyNetworkActivityPlugin { (state,target) in
if state == .began {
// SwiftSpinner.show("Connecting...")

let api = target as! NetAPIManager
if api.show {
print("我可以在这里写加载提示")
}

if !api.touch {
print("我可以在这里写禁止用户操作,等待请求结束")
}

print("我开始请求\(api.touch)")

UIApplication.shared.isNetworkActivityIndicatorVisible = true
} else {
// SwiftSpinner.show("request finish...")
// SwiftSpinner.hide()
print("我结束请求")
UIApplication.shared.isNetworkActivityIndicatorVisible = false

}
}

自签名证书

在16年的WWDC中,Apple已表示将从2017年1月1日起,所有新提交的App必须强制性应用HTTPS协议来进行网络请求。默认情况下非HTTPS的网络访问是禁止的并且不能再通过简单粗暴的向Info.plist中添加NSAllowsArbitraryLoads
设置绕过ATS(App Transport Security)的限制(否则须在应用审核时进行说明并很可能会被拒)。所以还未进行相应配置的公司需要尽快将升级为HTTPS的事项提上进程了。本文将简述HTTPS及配置数字证书的原理并以配置实例和出现的问题进行说明,希望能对你提供帮助。(比心~)

HTTPS:
简单来说,HTTPS就是HTTP协议上再加一层加密处理的SSL协议,即HTTP安全版。相比HTTP,HTTPS可以保证内容在传输过程中不会被第三方查看、及时发现被第三方篡改的传输内容、防止身份冒充,从而更有效的保证网络数据的安全。
HTTPS客户端与服务器交互过程:
1、 客户端第一次请求时,服务器会返回一个包含公钥的数字证书给客户端;
2、 客户端生成对称加密密钥并用其得到的公钥对其加密后返回给服务器;
3、 服务器使用自己私钥对收到的加密数据解密,得到对称加密密钥并保存;
4、 然后双方通过对称加密的数据进行传输。


数字证书:
在HTTPS客户端与服务器第一次交互时,服务端返回给客户端的数字证书是让客户端验证这个数字证书是不是服务端的,证书所有者是不是该服务器,确保数据由正确的服务端发来,没有被第三方篡改。数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方身份。证书由公钥、证书主题(Subject)、数字签名(digital signature)等内容组成。其中数字签名就是证书的防伪标签,目前使用最广泛的SHA-RSA加密。
证书一般分为两种:

1、一种是向权威认证机构购买的证书,服务端使用该种证书时,因为苹果系统内置了其受信任的签名根证书,所以客户端不需额外的配置。为了证书安全,在证书发布机构公布证书时,证书的指纹算法都会加密后再和证书放到一起公布以防止他人伪造数字证书。而证书机构使用自己的私钥对其指纹算法加密,可以用内置在操作系统里的机构签名根证书来解密,以此保证证书的安全。
2、另一种是自己制作的证书,即自签名证书。好处是不需要花钱购2买,但使用这种证书是不会受信任的,所以需要我们在代码中将该证书配置为信任证书.

自签名证书具体实现:

我们在使用自签名证书来实现HTTPS请求时,因为不像机构颁发的证书一样其签名根证书在系统中已经内置了,所以我们需要在App中内置自己服务器的签名根证书来验证数字证书。首先将服务端生成的.cer格式的根证书添加到项目中,注意在添加证书要一定要记得勾选要添加的targets。这里有个地方要注意:苹果的ATS要求服务端必须支持TLS 1.2或以上版本;必须使用支持前向保密的密码;证书必须使用SHA-256或者更好的签名hash算法来签名,如果证书无效,则会导致连接失败。由于我在生成的根证书时签名hash算法低于其要求,在配置完请求时一直报NSURLErrorServerCertificateUntrusted = -1202错误,希望大家可以注意到这一点。

那么如何在Moya中使用自签名的证书来实现HTTPS网络请求呢,请期待下回我专门分享......需要自定义一个Manager管理

综合使用的方法如下

定义一个公用的Moya请求服务对象

let MyAPIProvider = MoyaProvider<NetAPIManager>(endpointClosure: myEndpointClosure,requestClosure: requestClosure, plugins: [NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter),myNetworkPlugin])

// MARK: -创建一个Moya请求
func sendRequest(_ postDict: Dictionary<String, Any>? = nil,
success:@escaping (Dictionary<String, Any>)->(),
failure:@escaping (MoyaError)->()) -> Cancellable? {

let request = MyAPIProvider.request(.Show) { result in
switch result {
case let .success(moyaResponse):


do {
let any = try moyaResponse.mapJSON()
let data = moyaResponse.data
let statusCode = moyaResponse.statusCode
MyLog("\(data) --- \(statusCode) ----- \(any)")

success(["":""])


} catch {

}



case let .failure(error):

print(error)
failure(error)
}
}

return request
}

取消所有的Moya请求

// MARK: -取消所有请求
func cancelAllRequest() {
// MyAPIProvider.manager.session.invalidateAndCancel() //取消所有请求
MyAPIProvider.manager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
}

//let sessionManager = Alamofire.SessionManager.default
//sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
// dataTasks.forEach { $0.cancel() }
// uploadTasks.forEach { $0.cancel() }
// downloadTasks.forEach { $0.cancel() }
//}

}

转自:https://www.jianshu.com/p/38fbc22a1e2b

收起阅读 »

一行代码完成http请求,bitmap异步加载,数据库增删查改!

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.##Welike带来了哪些特征?WelikeAndroid...
继续阅读 »

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,
使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.

##Welike带来了哪些特征?

WelikeAndroid目前包含五个大模块:

  • 异常安全隔离模块(实验阶段):当任何线程抛出任何异常,我们的异常隔离机制都会让UI线程继续运行下去.
  • Http模块: 一行代码完成POST、GET请求和Download,支持上传, 高度优化Disk的缓存加载机制,
    自由设置缓存大小、缓存时间(也支持永久缓存和不缓存).
  • Bitmap模块: 一行代码完成异步显示图片,无需考虑OOM问题,支持加载前对图片做自定义处理.
  • Database模块: 支持NotNull,Table,ID,Ignore等注解,Bean无需Getter和Setter,一键式部署数据库.
  • ui操纵模块: 我们为Activity基类做了完善的封装,继承基类可以让代码更加优雅.
  • :请不要认为功能相似,框架就不是原创,源码摆在眼前,何不看一看?

使用WelikeAndroid需要以下权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

##下文将教你如何圆润的使用WelikeAndroid:
###通过WelikeContext在任意处取得上下文:

  • WelikeContext.getApplication(); 就可以取得当前App的上下文
  • WelikeToast.toast("你好!"); 简单一步弹出Toast.

##WelikeGuard(异常安全隔离机制用法):

  • 第一步,开启异常隔离机制:
WelikeGuard.enableGuard();
  • 第二步,注册一个全局异常监听器:

WelikeGuard.registerUnCaughtHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {

WelikeGuard.newThreadToast("出现异常了: " + ex.getMessage() );

}
});
  • 你也可以自定义异常:

/**
*
* 自定义的异常,当异常被抛出后,会自动回调onCatchThrowable函数.
*/
@Catch(process = "onCatchThrowable")
public class CustomException extends IllegalAccessError {

public static void onCatchThrowable(Thread t){
WeLog.e(t.getName() + " 抛出了一个异常...");
}
}
  • 另外,继承自UncaughtThrowable的异常我们不会对其进行拦截.

使用Welike做屏幕适配:

Welike的ViewPorter类提供了屏幕适配的Fluent-API,我们可以通过一组流畅的API轻松做好屏幕适配.

        ViewPorter.from(button).ofScreen().divWidth(2).commit();//宽度变为屏幕的二分之一
ViewPorter.from(button).of(viewGroup).divHeight(2).commit();//高度变为viewGroup的二分之一
ViewPorter.from(button).div(2).commit();//宽度和高度变为屏幕的四分之一
ViewPorter.from(button).of(this).fillWidth().fillHeight().commit();//宽度和高度铺满Activity
ViewPorter.from(button).sameAs(imageView).commit();//button的宽度和高度和imageView一样

WelikeHttp入门:

首先来看看框架的调试信息,是不是一目了然. DEBUG DEBUG2

  • 第一步,取得WelikeHttp默认实例.
WelikeHttp welikeHttp = WelikeHttp.getDefault();
  • 第二步,发送一个Get请求.
HttpParams params = new HttpParams();
params.putParams("app","qr.get",
"data","Test");//一次性放入两对 参数 和 值

//发送Get请求
HttpRequest request = welikeHttp.get("http://api.k780.com:88", params, new HttpResultCallback() {
@Override
public void onSuccess(String content) {
super.onSuccess(content);
WelikeToast.toast("返回的JSON为:" + content);
}

@Override
public void onFailure(HttpResponse response) {
super.onFailure(response);
WelikeToast.toast("JSON请求发送失败.");
}

@Override
public void onCancel(HttpRequest request) {
super.onCancel(request);
WelikeToast.toast("请求被取消.");
}
});

//取消请求,会回调onCancel()
request.cancel();

当然,我们为满足需求提供了多种扩展的Callback,目前我们提供以下Callback供您选择:

  • HttpCallback(响应为byte[]数组)
  • FileUploadCallback(仅在上传文件时使用)
  • HttpBitmapCallback(建议使用Bitmap模块)
  • HttpResultCallback(响应为String)
  • DownloadCallback(仅在download时使用)

如需自定义Http模块的配置(如缓存时间),请查看HttpConfig.

WelikeBitmap入门:

  • 第一步,取得默认的WelikeBitmap实例:

//取得默认的WelikeBitmap实例
WelikeBitmap welikeBitmap = WelikeBitmap.getDefault();
  • 第二步,异步加载一张图片:
BitmapRequest request = welikeBitmap.loadBitmap(imageView,
"http://img0.imgtn.bdimg.com/it/u=937075122,1381619862&fm=21&gp=0.jpg",
android.R.drawable.btn_star,//加载中显示的图片
android.R.drawable.ic_delete,//加载失败时显示的图片
new BitmapCallback() {

@Override
public Bitmap onProcessBitmap(byte[] data) {
//如果需要在加载时处理图片,可以在这里处理,
//如果不需要处理,就返回null或者不复写这个方法.
return null;
}

@Override
public void onPreStart(String url) {
super.onPreStart(url);
//加载前回调
WeLog.d("===========> onPreStart()");
}

@Override
public void onCancel(String url) {
super.onCancel(url);
//请求取消时回调
WeLog.d("===========> onCancel()");
}

@Override
public void onLoadSuccess(String url, Bitmap bitmap) {
super.onLoadSuccess(url, bitmap);
//图片加载成功后回调
WeLog.d("===========> onLoadSuccess()");
}

@Override
public void onRequestHttp(HttpRequest request) {
super.onRequestHttp(request);
//图片需要请求http时回调
WeLog.d("===========> onRequestHttp()");
}

@Override
public void onLoadFailed(HttpResponse response, String url) {
super.onLoadFailed(response, url);
//请求失败时回调
WeLog.d("===========> onLoadFailed()");
}
});
  • 如果需要自定义Config,请看BitmapConfig这个类.

##WelikeDAO入门:

  • 首先写一个Bean.

/*表名,可有可无,默认为类名.*/
@Table(name="USER",afterTableCreate="afterTableCreate")
public class User{
@ID
public int id;//id可有可无,根据自己是否需要来加.

/*这个注解表示name字段不能为null*/
@NotNull
public String name;

public static void afterTableCreate(WelikeDao dao){
//在当前的表被创建时回调,可以在这里做一些表的初始化工作
}
}
  • 然后将它写入到数据库
WelikeDao db = WelikeDao.instance("Welike.db");
User user = new User();
user.name = "Lody";
db.save(user);
  • 从数据库取出Bean

User savedUser = db.findBeanByID(1);
  • SQL复杂条件查询
List<User> users = db.findBeans().where("name = Lody").or("id = 1").find();
  • 更新指定ID的Bean
User wantoUpdateUser = new User();
wantoUpdateUser.name = "NiHao";
db.updateDbByID(1,wantoUpdateUser);
  • 删除指ID定的Bean
db.deleteBeanByID(1);
  • 更多实例请看DEMO和API文档.

##十秒钟学会WelikeActivity

  • 我们将Activity的生命周期划分如下:

=>@initData(所有标有InitData注解的方法都最早在子线程被调用)
=>initRootView(bundle)
=>@JoinView(将标有此注解的View自动findViewByID和setOnClickListener)
=>onDataLoaded(数据加载完成时回调)
=>点击事件会回调onWidgetClick(View Widget)

###关于@JoinView的细节:

  • 有以下三种写法:
@JoinView(name = "welike_btn")
Button welikeBtn;
@JoinView(id = R.id.welike_btn)
Button welikeBtn;
@JoinView(name = "welike_btn",click = false)
Button welikeBtn;
  • clicktrue时会自动调用view的setOnClickListener方法,并在onWidgetClick回调.
  • 当需要绑定的是一个Button的时候, click属性默认为true,其它的View则默认为false.
收起阅读 »

一个简洁而优雅的Android原生UI框架,解放你的双手!

一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、...
继续阅读 »

一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!

涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、ProgressBar、LoadingView、StateLayout、FlowLayout、Switch、Actionbar、TabBar、Banner、GuideView、BadgeView、MarqueeView、WebView、SearchView等一系列的组件和丰富多彩的样式主题。

在提issue前,请先阅读【提问的智慧】,并严格按照issue模板进行填写,节约大家的时间。

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

X系列库快速集成

为了方便大家快速集成X系列框架库,我提供了一个空壳模版供大家参考使用: https://github.com/xuexiangjys/TemplateAppProject

除此之外,我还特别制作了几期视频教程供大家学习参考.


特征

  • 简洁优雅,尽可能少得引用资源文件的数量,项目库整体大小不足1M(打包后大约644k)。

  • 组件丰富,提供了绝大多数我们在开发者常用的功能组件。

  • 使用简单,为方便快速开发,提高开发效率,对api进行了优化,提供一键式接入。

  • 样式统一,框架提供了一系列统一的样式,使UI整体看上去美观和谐。

  • 兼容性高,框架还提供了3种不同尺寸设备的样式(4.5英寸、7英寸和10英寸),并且最低兼容到Android 17, 让UI兼容性更强。

  • 扩展性强,各组件提供了丰富的属性和样式API,可以通过设置不同的样式属性,构建不同风格的UI。


如何使用

在决定使用XUI前,你必须明确的一点是,此框架给出的是一整套UI的整体解决方案,如果你只是想使用其中的几个控件,那大可不必引入如此庞大的一个UI库,Github上会有更好的组件库。如果你是想拥有一套可以定制的、统一的UI整体解决方案的话,那么你就继续往下看吧!

添加Gradle依赖

1.先在项目根目录的 build.gradle 的 repositories 添加:

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

2.然后在dependencies添加:

dependencies {
...
//androidx项目
implementation 'com.github.xuexiangjys:XUI:1.1.7'

implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
}

【注意】如果你的项目目前还未使用androidx,请使用如下配置:

dependencies {
...
//support项目
implementation 'com.github.xuexiangjys:XUI:1.0.9-support'

implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.github.bumptech.glide:glide:4.8.0'
}

初始化XUI设置

1.调整应用的基础主题(必须)

必须设置应用的基础主题,否则组件将无法正常使用!必须保证所有用到XUI组件的窗口的主题都为XUITheme的子类,这非常重要!!!

基础主题类型:

  • 大平板(10英寸, 240dpi, 1920*1200):XUITheme.Tablet.Big

  • 小平板(7英寸, 320dpi, 1920*1200):XUITheme.Tablet.Small

  • 手机(4.5英寸, 320dpi, 720*1280):XUITheme.Phone

<style name="AppTheme" parent="XUITheme.Phone">

 <!-- 自定义自己的主题样式 -->

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

</style>

当然也可以在Activity刚开始时调用如下代码动态设置主题

@Override
protected void onCreate(Bundle savedInstanceState) {
XUI.initTheme(this);
super.onCreate(savedInstanceState);
...
}

2.调整字体库(对字体无要求的可省略)

(1)设置你需要修改的字体库路径(assets下)

//设置默认字体为华文行楷,这里写你的字体库
XUI.getInstance().initFontStyle("fonts/hwxk.ttf");

(2)在项目的基础Activity中加入如下代码注入字体.

注意:1.1.4版本之后使用如下设置注入

@Override
protected void attachBaseContext(Context newBase) {
//注入字体
super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase));
}

注意:1.1.3版本及之前的版本使用如下设置注入

@Override
protected void attachBaseContext(Context newBase) {
//注入字体
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}

混淆配置

-keep class com.xuexiang.xui.widget.edittext.materialedittext.** { *; }


代码下载:XUI.zip

收起阅读 »

Android支付组件

接入指南:1、导入libSdk 依赖工程2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)2....
继续阅读 »



接入指南:

1、导入libSdk 依赖工程

2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)

  • 2.1 拷贝assets/data.bin 文件到 项目中

3、项目中实际使用支付:具体使用看下文 ---> 调起支付 。


请配置正确的参数,否则支付宝和微信 会出现无法调起的情况。

//配置 AndroidManifest

    <activity
android:name="net.lbh.pay.PaymentActivity"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />

<activity-alias
android:name=".wxapi.WXPayEntryActivity"
android:exported="true"
android:targetActivity="net.lbh.pay.PaymentActivity" />
<!-- 微信支付 end -->


<!-- 支付宝 begin -->
<activity
android:name="com.alipay.sdk.app.H5PayActivity"
android:configChanges="orientation|keyboardHidden|navigation"
android:exported="false"
android:screenOrientation="behind"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!-- 支付宝 end -->


<!-- 银联支付 begin -->

<activity
android:name="com.unionpay.uppay.PayActivity"
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />

<activity
android:name="com.unionpay.UPPayWapActivity"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />

<!-- 银联支付 end -->


<!-- 微信 广播 start -->
<receiver android:name="net.lbh.pay.wxpay.AppRegister" >
<intent-filter>
<action android:name="com.tencent.mm.plugin.openapi.Intent.ACTION_REFRESH_WXAPP" />
</intent-filter>
</receiver>
<!-- 微信 广播 end -->


<!-- 微信支付 参数 appid, 需要替换成你自己的 -->
<meta-data
android:name="WXPAY_APP_ID"
android:value="替换成自己的 app id" >
</meta-data>
<meta-data
android:name="WXPAY_MCH_ID"
android:value="替换成自己的 MCH_ID" >
</meta-data>
<meta-data
android:name="WXPAY_API_KEY"
android:value="替换成自己的 api key" >
</meta-data>
<!-- 微信支付 参数 end 需要替换成你自己的 -->


<!-- 支付宝 参数 appid, 需要替换成你自己的 --> //如果是 超过10位数字,要在前边加 ,Eg: \0223987667567887653
<meta-data
android:name="ALIPAY_PARTNER_ID"
android:value="替换成自己的 partenr id" >
</meta-data>
<meta-data
android:name="ALIPAY_SELLER_ID"
android:value="替换成自己的 seller id" >
</meta-data>

<meta-data
android:name="ALIPAY_PRIVATE_KEY"
android:value="替换成自己的 private key" >
</meta-data>

<meta-data
android:name="ALIPAY_PUBLIC_KEY"
android:value="替换成自己的 public key" >
</meta-data>
<!-- 支付宝 参数 end 需要替换成你自己的 -->

// 初始化支付组件

	PayAgent payAgent = PayAgent.getInstance();
payAgent.setDebug(true);

// 代码初始化 参数, 此处针对场景,所有参数有 自己app server保管的时候,动态的支付配置下发参数
payAgent.initAliPayKeys(partnerId, sellerId, privateKey, publicKey);
payAgent.initWxPayKeys(appId, mchId, appKey)
// 初始化 银联支付 所需的 验签 参数
//payAgent.initUpPayKeys(PublicKeyPMModulus, publicExponent, PublicKeyProductModulus);
// 代码动态初始化为 可选

payAgent.initPay(this);

// 调起支付

    PayAgent.getInstance().onPay(payType, this, payInfo,
new OnPayListener() {

@Override
public void onStartPay() {

progressDialog.setTitle("加载中。。。");
progressDialog.show();
}

@Override
public void onPaySuccess() {

Toast.makeText(MainActivity.this,"支付成功!", 1).show();

if (null != progressDialog) {
progressDialog.dismiss();
}

}

@Override
public void onPayFail(String code, String msg) {
Toast.makeText(MainActivity.this,
"code:" + code + "msg:" + msg, 1).show();
Log.e(getClass().getName(), "code:" + code + "msg:" + msg);

if (null != progressDialog) {
progressDialog.dismiss();
}
}
});

支付参数说明:

PayType: 支付的支付方式,目前支持:

  • 1、PayAgent.PayType.WECHATPAY(微信支付);
  • 2、PayAgent.PayType.ALIPAY(支付宝);
  • 3、PayAgent.PayType.UPPAY(银联)。

Activity: 调起支付的 Activity

PayInfo:

/** 商品名称*/
private String subject;

/** 商品详细信息 商品的标题/交易标题/订单标题/订单关键字等。该参数最长为128个汉字*/
private String body;

/** 商品价格*/
private String price;

/** 商品订单号*/
private String orderNo;

/** 支付通知地址*/
private String notifyUrl;

OnPayListener: 支付监听器:

  • onStartPay() 开始支付,可以在此做 支付前准备提示
  • onPaySuccess(); 支付成功
  • onPayFail(String code, String msg); 支付失败,code和msg 均为第三方原样返回

配置第三方参数说明:

  • 1、支付宝:

注意:

  • 1、支付宝支付,调用支付宝时, 所有参数为必须向
  • 2、微信支付,orderNo 为必须项
  • 3、银联支付时,orderNo 为必须项 -4、关于支付后,通知回调,只有支付宝是 在客户端手动设置,其余都是在 后台配置。

注意事项:

  • 1、当测试时,可以使用Debug模式,开启方式为: PayAgent payAgent = PayAgent.getInstance(); payAgent.setDebug(true);

  • 2、调试模式(非正式环境,目前只有 银联): PayAgent payAgent = PayAgent.getInstance(); payAgent.setOnlieMode(false);

版本說明:

  • 1、银联支付:3.3.2
  • 2、支付宝:
  • 3、微信:

更新日志:

  • 2016.04.15更新:

  • 1、2016.4.14 银联更新sdk,更新银联支付控件为3.3.3

  • 2、去除银联客户端验签;添加银联需要权限(nfc等)

  • 1、更新银联支付控件为3.3.2

  • 2、添加调试模式(非正式环境模式、主要正对银联支付)

payAgent.setOnlieMode(false);

  • 3、添加银联 验证签名,初始化签名参数
  • 4、修改Demo ,测试 Demo能正常运行。

其他说明:



收起阅读 »

日志管理工具 - CocoaLumberjack

CocoaLumberjackCocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。首先,通过CocoaPods、Carthage、Swift Package Manager或手动安...
继续阅读 »

CocoaLumberjack

CocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。

首先,通过CocoaPodsCarthageSwift Package Manager或手动安装 CocoaLumberjack 然后使用DDOSLoggeriOS 10 及更高版本,或DDTTYLoggerDDASLLogger早期版本开始记录消息。

CocoaPods

platform :ios, '9.0'

target 'SampleTarget' do
use_frameworks!
pod 'CocoaLumberjack/Swift'
end


platform :ios, '9.0'

target 'SampleTarget' do
pod 'CocoaLumberjack'
end

Carthage

github "CocoaLumberjack/CocoaLumberjack"

用法

如果您使用 Lumberjack 作为框架,则可以@import CocoaLumberjack;除此以外,#import
[DDLog addLogger: [DDOSLogger sharedInstance ]]; //使用 os_log

DDFileLogger *fileLogger = [[DDFileLogger
alloc ] init ]; //文件记录器
fileLogger.rollingFrequency =
60 * 60 * 24 ; // 24 小时滚动
fileLogger.logFileManager.maximumNumberOfLogFiles =
7 ;
[DDLog
addLogger: fileLogger];

...


DDLogVerbose ( @"详细" );
DDLogDebug ( @"调试" );
DDLogInfo ( @"信息" );
DDLogWarn ( @"警告" );
DDLogError ( @"错误" );

Objective-C ARC 语义问题

将 Lumberjack 集成到现有的 Objective-C 中时,可能会遇到Multiple methods named 'tag' found with mismatched result, parameter type or attributes构建错误。

#define DD_LEGACY_MESSAGE_TAG 0在导入 CocoaLumberjack 之前添加或添加#define DD_LEGACY_MESSAGE_TAG 0或添加-DDD_LEGACY_MESSAGE_TAG=0Xcode 项目中的其他 C 标志OTHER_CFLAGS

快速日志后端

CocoaLumberjack 还附带了swift-log的后端实现只需将 CocoaLumberjack 作为依赖项添加到您的 SPM 目标(见上文),并将CocoaLumberjackSwiftLogBackend产品作为依赖项添加到您的目标。

然后,您可以将DDLogHandler其用作 swift-log 的后端,它将所有消息转发到 CocoaLumberjack 的DDLog您仍将通过 配置您想要的记录器和日志格式化程序DDLog,但将使用Loggerswift-log完成写入日志消息

在您自己的日志格式化程序中,您可以使用swiftLogInfoon 属性DDLogMessage来检索通过 swift-log 记录的消息的详细信息。


在大多数情况下,它比 NSLog 快一个数量级。

当您的应用程序启动时,只需一行代码即可配置 lumberjack。然后只需将您的 NSLog 语句替换为 DDLog 语句即可。(而且 DDLog 宏的格式和语法与 NSLog 完全相同,因此非常简单。)

一个日志语句可以发送到多个记录器,这意味着您可以同时记录到一个文件和控制台。想要更多?创建您自己的记录器(这很容易)并通过网络发送您的日志语句。或者到数据库或分布式文件系统。天空才是极限。

根据需要配置您的日志记录。更改每个文件的日志级别(非常适合调试)。更改每个记录器的日志级别(详细控制台,但简洁的日志文件)。更改每个 xcode 配置的日志级别(详细调试,但简洁发布)。将您的日志语句从发布版本中编译出来。为您的应用程序自定义日志级别的数量。添加您自己的细粒度日志记录。在运行时动态更改日志级别。选择您希望日志文件滚动的方式和时间。将您的日志文件上传到中央服务器。压缩归档日志文件以节省磁盘空间...


在以下情况下,此框架适合您:

  • 您正在寻找一种方法来追踪该领域不断出现的无法重现的错误。
  • 您对 iPhone 上超短的控制台日志感到沮丧。
  • 您希望将您的应用程序在支持和稳定性方面提升到一个新的水平。
  • 您正在为您的应用程序(Mac 或 iPhone)寻找企业级日志记录解决方案。

要求

当前版本的 Lumberjack 要求:

  • Xcode 12 或更高版本
  • Swift 5.3 或更高版本
  • iOS 9 或更高版本
  • macOS 10.10 或更高版本
  • watchOS 3 或更高版本
  • tvOS 9 或更高版本



收起阅读 »

视图添加闪烁效果的简单方法 - Shimmer

ShimmerShimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。Shimmer 最初是为了在Paper 中显示加载状态而开发的。用法要使用 Shimmer,请创建一个FBShimmeringView或FBS...
继续阅读 »

Shimmer

Shimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。

Shimmer 最初是为了在Paper 中显示加载状态而开发的

用法

要使用 Shimmer,请创建一个FBShimmeringViewFBShimmeringLayer并添加您的内容。要开始闪烁,请将shimmering属性设置YES

使标签闪烁的示例:

FBShimmeringView *shimmeringView = [[FBShimmeringView alloc ] initWithFrame: self .view.bounds];
[
self .view addSubview: shimmeringView];

UILabel *loadingLabel = [[UILabel
alloc ] initWithFrame: shimmeringView.bounds];
loadingLabel.textAlignment = NSTextAlignmentCenter;

loadingLabel.text = NSLocalizedString(
@" Shimmer " , nil );
shimmeringView.contentView = loadingLabel;


//开始闪烁。
shimmeringView.shimmering =
YES ;

还有一个示例项目。在示例中,您可以水平和垂直滑动以尝试各种闪烁参数,或点击以开始或停止闪烁。(要在本地构建示例,您需要打开FBShimmering.xcworkpace而不是.xcodeproj.)

安装

有两种选择:

  1. 微光ShimmerCocoapods 中可用
  2. 手动将文件添加到您的 Xcode 项目中。稍微简单一点,但更新也是手动的。

Shimmer 需要 iOS 6 或更高版本。


这个怎么运作

Shimmer 使用该-[CALayer mask]属性来启用闪烁,类似于 John Harper 2009 年 WWDC 演讲中所描述的内容(不幸的是不再在线)。Shimmer 使用 CoreAnimation 的计时功能在启动和停止微光时平滑过渡“节拍”。


demo及常见问题:https://github.com/facebookarchive/Shimmer

源码下载:Shimmer-master.zip



收起阅读 »

还不会用coil,你就out了

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语...
继续阅读 »

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语言,将协程、OKHttp、OKIO和AndroidX作为一等公民,以期打造成一个更加轻快、现代化的图片加载库。具体而言包含以下几个方面:



  • 发挥Kotlin的语言特性,利用扩展函数、内联、lambda参数以及密封类来创建简单优雅的API。

  • 利用了Kotlin协程强大的 可取消的非阻塞式异步编程和对线程最大化利用的特性。

  • 使用现代化的依赖库:OKHttp、OKIO基本上已经是目前大部分app的事实“标准”库,它们强大的特性让Coil避免了重复实现磁盘缓存和缓冲流;类似的,AndroidX-LifeCycle也是官方推荐的,Coil目前是唯一一个对其支持的图片加载库。

  • 轻量:Coil项目的代码量几乎只有Glide的1/8,更是远远小于Fresco;并且对APK仅增加了大约1500个方法(对于那些已经依赖的OKHttp和协程的app来说),和Picasso相当并显著低于Glide和Fresco。

  • 支持扩展:Coil的image-pipline主要由 Mappers , Fetchers , 和 Decoders 三个类组成,可以方便地用于自定义:扩展或覆盖默认行为,或增加对新的文件类型的支持。

  • 测试友好化:Coil的基础服务类是 ClassLoader ,它是一个接口,可以方便地编写对应的实现类进行测试;并且Coil同时提供了单例和非单例对象来支持依赖注入。

  • 没有annotation processing:annotation processing一般会降低编译速度,Coil通过Kotlin扩展函数来避免。


Coil目前支持其它图片加载库所包含的所有功能,除此之外它还有一个独特的特性:动态采样(Dynamic image sampling),简而言之就是可以在内存中只缓存了一个低质量的图片而此时需要显示同一个高质量的图片时,Coil可以先把低质量的图片作为 ImageView 的 placeHolder 并同时去磁盘缓存中读取对应的高质量图片最后以“渐进式”的方式替换并最终显示到视图中,例如最常见的从图片列表到预览大图的场景。
以上就是Coil目前的大致介绍,下面我们对Coil的API进行一个简单的使用预览和介绍。


API预览



// 加载一个基本的url(利用了扩展函数,对target无任何侵入)
imageView.load("https://www.website.com/image.jpg")

// Coil 支持加载 urls, uris, resources, drawables, bitmaps, files 等等
imageView.load(R.drawable.image)
imageView.load(File("/path/to/image.jpg"))
imageView.load(Uri.parse("content://com.android.externalstorage/image.jpg"))

// Requests的配置项可以通过load的lambda参数方式实现
imageView.load("https://www.website.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}

// 自定义targets,包含开始、成功和失败的回调
Coil.load(context, "https://www.website.com/image.jpg") {
target { drawable ->
// Handle the successful result.
}
}

// 通过使用挂起函数get来直接获取图片对象
val drawable = Coil.get("https://www.website.com/image.jpg")

github地址:https://github.com/coil-kt/coil

下载地址:main.zip

收起阅读 »

最快的图像加载库-FastImageCache

FastImageCache快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。对于像Path这样的图形丰富的应用程序,性能...
继续阅读 »

FastImageCache

快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。

对于像Path这样的图形丰富的应用程序,性能的一个重大负担是图像加载。从磁盘加载单个图像的传统方法太慢了,尤其是在滚动时。Fast Image Cache 就是专门为解决这个问题而创建的。

快速图像缓存的作用

  • 将相似大小和样式的图像存储在一起
  • 将图像数据保存到磁盘
  • 比传统方法更快地将图像返回给用户
  • 根据使用情况自动管理缓存过期
  • 利用基于模型的方法来存储和检索图像
  • 允许在将图像存储到缓存之前按模型处理图像


事实证明,从压缩的磁盘图像数据到用户可以实际看到的渲染核心动画层的过程非常昂贵。随着要显示的图像数量的增加,这种成本很容易导致帧速率显着下降。可滚动视图进一步加剧了这种情况,因为内容可以快速变化,需要快速处理时间才能保持 60FPS 的流畅。1

考虑从磁盘加载图像并将其显示在屏幕上时发生的工作流程:

  1. +[UIImage imageWithContentsOfFile:]使用Image I/OCGImageRef从内存映射数据创建一个此时,图像还没有被解码。
  2. 返回的图像被分配给一个UIImageView.
  3. 隐式CATransaction捕获这些层树修改。
  4. 在主运行循环的下一次迭代中,Core Animation 提交隐式事务,这可能涉及创建已设置为图层内容的任何图像的副本。根据图像,复制它涉及以下部分或全部步骤:2
    1. 缓冲区被分配用于管理文件 IO 和解压操作。
    2. 文件数据从磁盘读取到内存中。
    3. 压缩的图像数据被解码为其未压缩的位图形式,这通常是一个非常占用 CPU 的操作。3
    4. 然后 Core Animation 使用未压缩的位图数据来渲染图层。

这些成本很容易累积并扼杀感知的应用程序性能。特别是在滚动时,给用户呈现的用户体验不尽如人意,与 iOS 的整体体验不符。


解决方案

快速图像缓存使用各种技术最大限度地减少(或完全避免)上述大部分工作:

映射内存

快速图像缓存工作原理的核心是图像表。图像表类似于精灵表,通常用于 2D 游戏。图像表将相同尺寸的图像打包到一个文件中。该文件只打开一次,只要应用程序保留在内存中,就保持打开状态以供读取和写入。

图像表使用mmap系统调用将文件数据直接映射到内存中。没有memcpy发生。这个系统调用只是在磁盘上的数据和内存区域之间创建一个映射。

当请求图像缓存返回特定图像时,图像表(以恒定时间)在它维护的文件中找到所需图像数据的位置。该文件数据区域被映射到内存中,并创建一个新CGImageRef的后备存储映射的文件数据。

当返回的CGImageRef(包装成 a UIImage)准备好被绘制到屏幕上时,iOS 的虚拟内存系统页面中的实际文件数据。这是使用映射内存的另一个好处;VM 系统会自动为我们处理内存管理。此外,映射内存“不计入”应用程序的实际内存使用量。

以类似的方式,当图像数据被存储在图像表中时,会创建一个内存映射位图上下文。与原始图像一起,此上下文被传递到图像表的相应实体对象。该对象负责将图像绘制到当前上下文中,可选地进一步配置上下文(例如,将上下文裁剪为圆角矩形)或进行任何额外的绘制(例如,在原始图像上绘制叠加图像)。mmap将绘制的图像数据编组到磁盘,因此不会在内存中分配图像缓冲区。

未压缩的图像数据

为了避免昂贵的图像解压缩操作,图像表将未压缩的图像数据存储在它们的文件中。如果源图像被压缩,则必须首先解压缩图像表才能使用它。这是一次性成本。此外,可以利用图像格式系列为一组相似的图像格式只执行一次这种解压缩。

然而,这种方法有明显的后果。未压缩的图像数据需要更多的磁盘空间,压缩和未压缩文件大小之间的差异可能很大,尤其是对于 JPEG 等图像格式。出于这个原因,快速图像缓存最适用于较小的图像,尽管没有强制执行此操作的 API 限制。

字节对齐

对于高性能滚动,Core Animation 能够使用图像而无需首先创建副本是至关重要的。Core Animation 创建图像副本的原因之一是图像底层CGImageRef正确对齐的每行字节值必须是 的倍数8 pixels × bytes per pixel对于典型的 ARGB 图像,对齐的每行字节值是 64 的倍数。每个图像表都配置为从一开始就为 Core Animation 始终正确地对每个图像进行字节对齐。因此,当从图像表中检索图像时,它们已经处于 Core Animation 可以直接使用的形式,而无需创建副本。

为了便于项目集成,Fast Image Cache 可作为CocoaPod 使用

手动

创建图像格式

每个图像格式对应一个图像缓存将使用的图像表。可以使用相同的源图像来渲染它们存储在图像表中的图像的图像格式应该属于相同的图像格式系列有关如何确定适当的最大计数的更多信息,请参阅图像表大小

静态 NSString *XXImageFormatNameUserThumbnailSmall = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailSmall " ;
静态 NSString *XXImageFormatNameUserThumbnailMedium = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailMedium " ;
静态 NSString *XXImageFormatFamilyUserThumbnails = @" com.mycompany.myapp.XXImageFormatFamilyUserThumbnails " ;

FICImageFormat *smallUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
smallUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailSmall;

smallUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

smallUserThumbnailImageFormat.style = FICImageFormatStyle16BitBGR;

smallUserThumbnailImageFormat.imageSize = CGSizeMake(
50 , 50 );
smallUserThumbnailImageFormat.maximumCount =
250 ;
smallUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

smallUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


FICImageFormat *mediumUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
mediumUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailMedium;

mediumUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

mediumUserThumbnailImageFormat.style = FICImageFormatStyle32BitBGRA;

mediumUserThumbnailImageFormat.imageSize = CGSizeMake(
100 , 100 );
mediumUserThumbnailImageFormat.maximumCount =
250 ;
mediumUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

mediumUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


NSArray *imageFormats = @[smallUserThumbnailImageFormat, mediumUserThumbnailImageFormat];

配置图像缓存

一旦定义了一种或多种图像格式,就需要将它们分配给图像缓存。除了分配图像缓存的委托之外,没有其他可以在图像缓存本身上配置的内容。

FICImageCache *sharedImageCache = [FICImageCache sharedImageCache ];
sharedImageCache.delegate = self;

sharedImageCache.formats = imageFormats;


实体是符合FICEntity协议的对象实体唯一标识图像表中的条目,并且它们还负责绘制它们希望存储在图像缓存中的图像。已经定义了模型对象(可能由 Core Data 管理)的应用程序通常是合适的候选实体。

@interface  XXUser : NSObject 

@property ( nonatomic , assign , getter = isActive) BOOL active;
@property ( nonatomic , copy ) NSString *userID;
@属性非原子副本NSURL * userPhotoURL;

@结尾

这是该FICEntity协议的示例实现

- ( NSString *)UUID {
CFUUIDBytes UUIDBytes = FICUUIDBytesFromMD5HashOfString (_userID);
NSString *UUID = FICStringWithUUIDBytes (UUIDBytes);

返回UUID;
}


- (
NSString *)sourceImageUUID {
CFUUIDBytes sourceImageUUIDBytes = FICUUIDBytesFromMD5HashOfString ([_userPhotoURL absoluteString ]);
NSString *sourceImageUUID = FICStringWithUUIDBytes (sourceImageUUIDBytes);

返回sourceImageUUID;
}


- (
NSURL *)sourceImageURLWithFormatName:( NSString *)formatName {
return _sourceImageURL;
}


- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(
NSString *)formatName {
FICEntityImageDrawingBlockdrawingBlock = ^(
CGContextRef context, CGSize contextSize) {
CGRect contextBounds = CGRectZero ;
上下文边界。
大小= 上下文大小
CGContextClearRect(上下文,contextBounds);

//剪辑中等缩略图,使它们具有圆角
if ([formatName isEqualToString: XXImageFormatNameUserThumbnailMedium]) {
UIBezierPath clippingPath = [
self _clippingPath ];
[剪辑
路径添加剪辑];
}


UIGraphicsPushContext(上下文);
[图像
drawInRect: contextBounds];
UIGraphicsPopContext ();
};


返回绘图块;
}

理想情况下,实体的UUID永远不应该改变。这就是为什么在应用程序使用从 API 检索的资源的情况下,它与模型对象的服务器生成的 ID 很好地对应。

一个实体的可以改变。例如,如果用户更新了他们的个人资料照片,则该照片的 URL 也应更改。保持不变,标识相同的用户,但改变了个人资料照片网址将表明,有一个新的源图像。sourceImageUUID UUID

注意:通常,最好对用于定义UUID和 的任何标识符进行哈希处理sourceImageUUIDFast Image Cache 提供了实用功能来执行此操作。由于散列可能很昂贵,因此建议仅计算一次散列(或仅在标识符更改时)并存储在实例变量中。

当要求图像缓存为特定实体和格式名称提供图像时,该实体负责提供 URL。URL 甚至不需要指向实际资源——例如,URL 可能由自定义 URL 方案构成——但它必须是一个有效的 URL。

图像缓存仅使用这些 URL 来跟踪哪些图像请求已经在进行中;正确处理对同一图像的图像缓存的多个请求,而不会浪费任何精力。选择使用 URL 作为键控图像缓存请求的基础实际上补充了许多实际应用程序设计,其中图像资源(而不是图像本身)的 URL 包含在服务器提供的模型数据中。

注意:快速图像缓存不提供任何网络请求机制。这是图像缓存委托的责任。

最后,一旦源图像可用,实体就会被要求提供一个绘图块。将存储最终图像的图像表设置文件映射位图上下文并调用实体的绘图块。这使得每个实体可以方便地决定如何处理特定图像格式的源图像。

从图像缓存中请求图像

快速图像缓存在 Cocoa 常见的按需、延迟加载设计模式下工作。

XXUser *user = [self _currentUser];
NSString *formatName = XXImageFormatNameUserThumbnailSmall;
FICImageCacheCompletionBlock completionBlock = ^(id <FICEntity> entity, NSString *formatName, UIImage *image) {
_imageView.image = image;
[_imageView.layer addAnimation:[CATransition animation] forKey:kCATransition];
};

BOOL imageExists = [sharedImageCache retrieveImageForEntity:user withFormatName:formatName completionBlock:completionBlock];

if (imageExists == NO) {
_imageView.image = [self _userPlaceholderImage];
}

统计数据

以下统计数据是从演示应用程序的运行中测得的:

方法滚动性能磁盘使用情况RPRVT 1
传统的~35FPS568KB2.40MB1.06MB+1.34MB
快速图像缓存~59FPS2.2MB1.15MB1.06MB+0.09MB


demo下载及常见问题:https://github.com/path/FastImageCache

源码下载:FastImageCache-master.zip


收起阅读 »

项目想美观么?试试它吧!!自定义加载视图:mkloader

美丽流畅的自定义加载视图 使用<com.tuyenmonkey.mkloader.MKLoader android:layout_width="wrap_content" android:layout_heigh...
继续阅读 »

美丽流畅的自定义加载视图



使用

<com.tuyenmonkey.mkloader.MKLoader
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mk_type="<loading_type>" (Optional. Default is ClassicSpinner)
app:mk_color="<color>" (Optional. Default is #ffffff)
/>

类型


  • Sharingan
  • TwinFishesSpinner
  • ClassicSpinner
  • LineSpinner
  • FishSpinner
  • PhoneWave
  • ThreePulse
  • FourPulse
  • FivePulse
  • Worm
  • Whirlpool
  • Radar

安装

Gradle


dependencies {
compile 'com.tuyenmonkey:mkloader:1.4.0'
}

github地址:https://github.com/nntuyen/mkloader


下载地址:master.zip


收起阅读 »

iOS应用程序瘦身的静态库解决方案

为什么要给程序瘦身?随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的...
继续阅读 »

为什么要给程序瘦身?

随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的移动网络流量等等。因此在这些众多的问题下需要对应用进行瘦身处理。

一个应用程序由众多资源文件和可执行程序文件组成,资源文件的优化不在本文探讨范围。本文主要讨论对可执行程序代码瘦身的方法。

对可执行程序代码瘦身主要就是想办法让程序中不会被调用的源代码不参与编译或链接。我们可以通过一些源代码分析工具来查找哪些函数或者类方法没有被调用并从代码中删除掉来解决编译链接前的瘦身问题。这些分析工具也不在本文的讨论范围内。应用程序在编译时会对工程中的所有代码都执行编译处理并生成目标文件。而在链接阶段则会根据程序代码中对符号的引用关系来将所有相关的目标文件链接为一个大的可执行程序文件,并且在链接阶段链接器会优化掉所有没被调用的C/C++函数代码,但是对于OC类中的没有调用的方法则不会被优化掉。所以为了对可执行程序在编译链接阶段进行瘦身处理就需要了解源代码的编译链接规则。这也是本文所要介绍的针对工程通过静态库的形式进行编译和链接的方式来减少可执行程序代码的尺寸。您可以从文章:《深入iOS系统底层之静态库介绍》中详细的了解到静态库的编译链接过程,以及相关的技术细节。

一个瘦身的例子!

为了验证和具体的实践,我在github上建立了一个项目:YSAppSizeTest。您可以从这个项目中看到如何对工程进行构建以实现程序的瘦身处理。

在示例项目中同一个Workspace中分别建立ThinApp和FatApp两个工程,这两个工程实现的功能是一样。在整个应用程序中分别定义了CA、CB、CC、CD、CE一共5个OC类,定义了一个UIView(Test)分类,还有定义了两个C函数:libFoo1和libFoo1。

整个应用程序中只使用了CA和CC两个OC类,以及调用了UIView(Test)分类方法,以及调用了libFoo1函数,并且同时都采用导入静态库的形式。因为这两个工程对文件的定义和分布策略不同使得两个应用程序的最终可执行代码的尺寸是不相同的。

FatApp中的文件定义和分布策略

1、FatApp工程依赖并导入了FatAppLib静态库工程。
2、CA,CB两个类都定义在主程序工程中。
3、CC,CD,CE三个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在FatAppLib静态库工程中。
4、CC,CD两个类定义在同一个文件中,CE类则定义在单独的文件中。
5、FatApp工程的Other Linker Flags中设置了 -ObjC选项。

ThinApp中的文件定义和分布策略

1、ThinApp工程依赖并导入了ThinAppLib静态库工程。
2、主程序工程就是一个壳工程。
3、CA,CB,CC,CD,CE5个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在ThinAppLib静态库工程中。
4、上述的5个类都分别定义在不同的文件中。
5、ThinApp工程的Other Linker Flags中没有设置-ObjC选项。

上述两个工程的程序被Archive出来后,FatApp可执行程序的尺寸是367KB,而ThinApp可执行程序的尺寸是334KB。通过一些工具比如Mach-O View或者 IDA可以看出:FatApp中5个OC类的代码以及libFoo1函数还有UIView(Test)分类的代码都被链接进可执行程序中;而ThinApp中则只有CA,CC两个类以及libFoo1函数还有UIView(Test)分类的代码被链接进可执行程序中。在ThinApp中虽然没有使用-Objc链接选项,但是静态库中的分类也被链接进可执行程序中。

应用程序工程构建规则

根据对项目中的文件定义和引用策略以及相关的理论基础我们可以按照如下的规则来构建您的应用程序:

1、尽量将所有代码都移植到静态库中,而主程序则保留为一个壳程序。具体操作方法是建立一个Workspace,然后主程序工程就只有默认创建工程时的代码,所有新加入的代码都建立并存放到静态库工程中去,然后通过工程依赖来引入这些静态库工程,或者借助一些工程化工具比如Cocoapods来实现这种拆分和引用处理。主程序工程中只保留AppDelegate的代码,其他代码都一致到静态库中。然后在AppDelegate中的相关代码处调用静态库中定义的业务代码。

2、按业务组件对工程进行解耦每个组件是一个静态库工程。静态库中的每一个文件中最好只有一个类的实现,并且类的分类实现最好和类实现编写在同一个文件中,相同功能的代码以及可能都会被调用的代码尽量存放在一个文件中。

3、不要在主程序工程中使用-ObjC和-all_load两个选项而改为用-force_load 来单独指定要执行加载的静态库。-ObjC和-all_load选项会把主程序工程以及所依赖的所有静态库中的工程中的全部代码都链接到可执行程序中而不管代码是否有被调用过或者使用过。而force_load则只会将指定的静态库中的所有代码链接到可执行程序中,当然force_load如果没有必要也尽量不要使用。

4、尽量减少在静态库中定义OC类的分类方法,如果一定要定义分类方法则可以将分类方法定义在和类定义相同的文件中,或者将分类方法定义在一个一定会被调用和引用的实现文件中。因为根据链接规则静态库中的分类是不会被链接进可执行程序中的,除非使用了上述的三个链接选项。如果将分类代码单独的定义在一个文件中的话则可以通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。

//分类文件的头文件UIView+XXX.h
@interface UIView (XXX)

//分类中定义的方法

@end

/*
通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把
整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。
而在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能
将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。
*/
extern void _cat_UIView_XXX_Impl(void);
inline void _cat_UIView_XXX_Decl(void){_cat_UIView_XXX_Impl();}


------------------------------------------------------------
//分类文件的实现文件UIView+XXX.m
#import "UIView+XXX.h"

@implementation UIView (XXX)

//分类的实现代码

@end

void _cat_UIView_XXX_Impl(void){}


---------------------------------------------------------------
//最后把这个分类头文件放入到某个对外暴露的头文件中,比如本例中将分类代码放入到了ThinAppLib.h文件中
//ThinAppLib.h

#import "UIView+XXX.h"
//其他头文件

5、除了可以通过-force_load来加载指定静态库中的所有代码外。我们还可以在构建静态库时,在静态库的工程的Build Settings中将Perform Single-Object Prelink 中的开关选项打开。当这个开关打开时,系统会对生成的静态库的所有目标文件执行预链接操作,预链接操作会将所有的目标文件组合成为一个单独的大的目标文件。这样根据以文件为单位的链接规则就会将静态库中的所有代码全部都链接进可执行程序中去,但是这样带来的问题就是最后在dead code stripping时删除不掉已经链接进来的那些没有被任何地方使用过的OC类了。

6、对于引入的一些第三方静态库或者第三方的开源库来说因为我们无法去改变其实现逻辑。如果这个静态库中没有任何分类代码的定义则正常引用即可,如果静态库中有分类方法的定义则单独对这个静态库采用-force_load选项。

总之一句话:为了让你的程序瘦身,尽量将代码放到静态库中,不要使用-Objc和-all_load选项

为了验证上述方法的有效性,笔者对项目中的应用做了一个测试:分别是有带-ObjC选项和没有带-ObjC选项的情况下的应用程序包中可执行程序的大小从115M减少到95M,减少了20M的尺寸。

链接:https://www.jianshu.com/p/2078e00891fd

收起阅读 »

日!!聊天页面还能这么简单??ChatKit

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集...
继续阅读 »

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集成 IM 服务,轻松实现聊天功能。
+


ChatKit 开源且提供完全自由的授权协议,开发者可以对其进行任意修改、扩展和二次封装,但是 LeanCloud 并不对 ChatKit 的二次开发提供技术支持。

+


获取项目


git clone git@github.com:leancloud/LeanCloudChatKit-Android.git

运行 Demo


获取源代码之后,用 Android Studio 打开项目,左侧的 Project 视图显示为:

+




「ChatKit-Android」Project 包含两个模块:

+



  • leancloudchatkit

    是一个封装了 LeanCloud 即时通讯的 UI lib,其目的是让开发者更快速地接入 LeanCloud 即时通讯的功能。

  • chatkitapplication

    为 Demo 项目,它是一个简单的示例项目,用来指导开发者如何使用 leancloudchatkit。


然后,请确保电脑上已经连接了一台真机或者虚拟机用作调试。

+


点击 Debug 或者 Run,第一次启动会运行 Gradle Build。建议打开全局网络代理,否则 Gradle Build 可能会因为网络原因无法完成。

+


使用方法


开发者可以将 ChatKit 导入到自己的 Project 中使用。下面我们将新建一个 Project(名为 ChatDemo) 用以导入 ChatKit。导入方式推荐通过源代码导入

+


源代码导入



  1. 浏览器访问 https://github.com/leancloud/LeanCloudChatKit-Android

  2. 执行以下命令行,将项目 clone 到本地(如 ChatKit 文件夹中,或者直接下载 zip 包自行解压缩到此文件夹下):
    git clone https://github.com/leancloud/LeanCloudChatKit-Android.git`


  3. 将文件夹 leancloudchatkit 复制到 ChatDemo 根目录;

  4. 修改 ChatDemo/settings.gradle 加入 include ':leancloudchatkit'

  5. 修改 ChatDemo/app/build.gradle,在 dependencies 中添加 compile project(":leancloudchatkit")


最后只要 Sync Project,这样 ChatKit 就算是导入到项目中了。

+


自定义使用


一、实现自己的 Application

+


ChatKit 在使用之前需要进行初始化,就像直接使用 LeanCloud 基础 SDK 时需要调用 AVOSCloud.initialize(appId, appKey) 一样。初始化逻辑应该放在 Application.onCreate 方法中实现。

+


ChatDemo 中新建一个 Class,名字叫做 ChatDemoApplication,让它继承自 Application 类,代码如下:

+


public class ChatDemoApplication extends Application {

// appId、appKey 可以在「LeanCloud 控制台 > 设置 > 应用 Key」获取
private final String APP_ID = "********";
private final String APP_KEY = "********";

@Override
public void onCreate() {
super.onCreate();
// 关于 CustomUserProvider 可以参看后面的文档
LCChatKit.getInstance().setProfileProvider(CustomUserProvider.getInstance());
LCChatKit.getInstance().init(getApplicationContext(), APP_ID, APP_KEY);
}
}

二、在 AndroidMainfest.xml 中配置 ChatDemoApplication

+


<application
...
android:name=".ChatDemoApplication" >
...
</application>

三、实现自己的用户体系

+


一般来说,聊天界面要相对复杂一些,不但要支持文字、表情、图片、语音等消息格式,还要展示用户信息,比如用户的头像、昵称等。而 LeanCloud 的消息流中只包含用户的 clientId 这一唯一标识,所以要获取头像这类额外的用户信息,就需要开发者接入自己的用户系统来实现。

+


为了保证通用性和扩展性,让开发者可以更容易将聊天界面嵌入自己的应用中,ChatKit 在设计上抽象出了一个「用户体系」的接口,需要开发者自己提供用户信息的获取方式。该接口只有一个方法需要开发者实现:

+


/**
* 用户体系的接口,开发者需要实现此接口来接入 LCChatKit
*/

public interface LCChatProfileProvider {
// 根据传入的 clientId list,查找、返回用户的 Profile 信息(id、昵称、头像)
public void fetchProfiles(List<String> userIdList, LCChatProfilesCallBack profilesCallBack);
}

为此,我们在 ChatDemo 中新建一个 Class,名字叫做 CustomUserProvider,代码如下:

+


public class CustomUserProvider implements LCChatProfileProvider {

private static CustomUserProvider customUserProvider;

public synchronized static CustomUserProvider getInstance() {
if (null == customUserProvider) {
customUserProvider
= new CustomUserProvider();
}
return customUserProvider;
}

private CustomUserProvider() {
}

private static List<LCChatKitUser> partUsers = new ArrayList<LCChatKitUser>();

// 此数据均为模拟数据,仅供参考
static {
partUsers
.add(new LCChatKitUser("Tom", "Tom", "http://www.avatarsdb.com/avatars/tom_and_jerry2.jpg"));
partUsers
.add(new LCChatKitUser("Jerry", "Jerry", "http://www.avatarsdb.com/avatars/jerry.jpg"));
partUsers
.add(new LCChatKitUser("Harry", "Harry", "http://www.avatarsdb.com/avatars/young_harry.jpg"));
partUsers
.add(new LCChatKitUser("William", "William", "http://www.avatarsdb.com/avatars/william_shakespeare.jpg"));
partUsers
.add(new LCChatKitUser("Bob", "Bob", "http://www.avatarsdb.com/avatars/bath_bob.jpg"));
}

@Override
public void fetchProfiles(List<String> list, LCChatProfilesCallBack callBack) {
List<LCChatKitUser> userList = new ArrayList<LCChatKitUser>();
for (String userId : list) {
for (LCChatKitUser user : partUsers) {
if (user.getUserId().equals(userId)) {
userList
.add(user);
break;
}
}
}
callBack
.done(userList, null);
}

public List<LCChatKitUser> getAllUsers() {
return partUsers;
}
}

当用户昵称和头像需要更新时,需要覆盖旧的 LCChatKitUser 对象并更新本地缓存:

+


    LCChatKitUser user = new LCChatKitUser("唯一 userId 不可变", "要变更的昵称", "要变更的 avatarURL");
LCIMProfileCache.getInstance().cacheUser(user);

四、打开即时通讯,并且跳转到聊天页面

+


我们支持通过两种方式来打开聊天界面:

+



  1. 通过指定另一个参与者的 clientId 的方式,开启一对一的聊天;

    此时,通过调用 intent.putExtra(LCIMConstants.PEER_ID, "peermemberId") 来传递另一参与者的 clientId。

  2. 通过指定一个已经存在的 AVIMConversation id 的方式,开启单人、多人或者开放式聊天室;

    此时,通过调用 LCIMConstants.CONVERSATION_ID, "particularConversationId") 来传递特定对话 Id。


下面的代码展示了如何通过第一种方式来开启聊天界面:

+


LCChatKit.getInstance().open("Tom", new AVIMClientCallback() {
@Override
public void done(AVIMClient avimClient, AVIMException e) {
if (null == e) {
finish
();
Intent intent = new Intent(MainActivity.this, LCIMConversationActivity.class);
intent
.putExtra(LCIMConstants.PEER_ID, "Jerry");
startActivity
(intent);
} else {
Toast.makeText(MainActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
}
}
});

这样,Tom 就可以和 Jerry 愉快地聊天了。

+


接口以及组件


以下介绍在 ChatKit 中开发者常需要关注的业务逻辑组件(Interface)和界面组件(UI)。

+


用户


LCChatKitUser 是 ChatKit 封装的参与聊天的用户,它提供了如下属性:

2+























名称 描述
userId 用户在单个应用内唯一的 ID,也是调用 LCChatKit.open 时传入的 userId。
avatarUrl 用户头像的 URL
name 用户名

使用这些默认的属性基本可以满足一个聊天应用的需求,同时开发者可以通过继承 LCChatKitUser 类实现更多属性。具体用法请参考 Demo 中的 MembersAdapter.java

+


用户信息管理类


LCChatProfileProvider 接口需要用户 implements 后实现 fetchProfiles 函数,以使 ChatKit 在需要显示的时候展示用户相关的信息。

+


例如 Demo 中的 CustomUserProvider 这个类,它实现了 LCChatProfileProvider 接口,在 fetchProfiles 方法里加载了 Tom、Jerry 等人的信息。

+


核心类


LCChatKit 是 ChatKit 的核心类,具体逻辑可以参看代码,注意以下几个主要函数:

+



public void init(Context context, String appId, String appKey)

此函数用于初始化 ChatKit 的相关设置,此函数要在 Application 的 onCreate 中调用,否则可能会引起异常。

public void setProfileProvider(LCChatProfileProvider profileProvider)

此函数用于设置用户体系,因为 LeanCloud 即时通讯功能已经实现了完全剥离用户体系的功能,这里接入已有的用户体系会很方便。

public void open(final String userId, final AVIMClientCallback callback)

此函数用于开始即时通讯,open 成功后可以执行自己的逻辑,或者跳转到聊天页面。



对话列表界面


对话 AVIMConversation 是 LeanMessage 封装的用来管理对话中的成员以及发送消息的载体,不论是群聊还是单聊都是在一个对话当中;而对话列表可以作为聊天应用默认的首页显示出来,主流的社交聊天软件,例如微信,就是把最近的对话列表作为登录后的首页。

+


因此,我们也提供了对话列表 LCIMConversationListFragment 页面供开发者使用,在 Demo 项目中的 MainActivity 中的 initTabLayout 方法中演示了如何引入对话列表页面:

+


  private void initTabLayout() {
String[] tabList = new String[]{"会话", "联系人"};
final Fragment[] fragmentList = new Fragment[] {new LCIMConversationListFragment(),
new ContactFragment()};
// 以上这段代码为新建了一个 Fragment 数组,并且把 LCIMConversationListFragment 作为默认显示的第一个 Tab 页面

tabLayout
.setTabMode(TabLayout.MODE_FIXED);
for (int i = 0; i < tabList.length; i++) {
tabLayout
.addTab(tabLayout.newTab().setText(tabList[i]));
}
...
}

具体的显示效果如下:

+




聊天界面


聊天界面是显示频率最高的前端页面,ChatKit 通过 LCIMConversationFragmentLCIMConversationActivity 来实现这一界面。在 Demo 的 ContactItemHolder 界面包含了使用 LCIMConversationActivity 的实例:

+


  public void initView() {
nameView
= (TextView)itemView.findViewById(R.id.tv_friend_name);
avatarView
= (ImageView)itemView.findViewById(R.id.img_friend_avatar);

itemView
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击联系人,直接跳转进入聊天界面
Intent intent = new Intent(getContext(), LCIMConversationActivity.class);
// 传入对方的 Id 即可
intent
.putExtra(LCIMConstants.PEER_ID, lcChatKitUser.getUserId());
getContext
().startActivity(intent);
}
});
}

具体的显示效果如下:

+




联系人列表页面


因为 ChatKit 是与应用的用户体系完全解耦的,所以 ChatKit 中并没有包含联系人列表页面,但部分开发者可能有此需求,所以 chatkitapplication 实现了一个基于 LCChatProfileProvider 的联系人列表页面,具体代码可以参考 ContactFragment.java
具体效果如下:

+




常见问题


ChatKit 组件收费么?

ChatKit 是完全开源并且免费给开发者使用,使用聊天所产生的费用以账单为准。

+


接入 ChatKit 有什么好处?

它可以减轻应用或者新功能研发初期的调研成本,直接引入使用即可。ChatKit 从底层到 UI 提供了一整套的聊天解决方案。




收起阅读 »

罕见!!上弹窗:Alerter

alerter克服了toast和snackbar的缺点,并布局很简单 生成为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。 可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。 安装配置allprojects {...
继续阅读 »

alerter克服了toast和snackbar的缺点,并布局很简单


生成

为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。


可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。


安装配置

allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}

build.gradle


dependencies {
implementation 'com.github.tapadoo:alerter:$current-version'
}

使用


Activity -


Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

Fragment -


Alerter.create(activity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

展示 -


Alerter.isShowing()

隐藏 -


Alerter.hide()

定制

背景颜色

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setBackgroundColorRes(R.color.colorAccent) // or setBackgroundColorInt(Color.CYAN)
.show()


图标

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.setIcon(R.drawable.alerter_ic_mail_outline)
.setIconColorFilter(0) // Optional - Removes white tint
.setIconSize(R.dimen.custom_icon_size) // Optional - default is 38dp
.show()


自定义屏幕持续时间(毫秒)

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.show()

无标题

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.show()


添加 Click Listener

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnClickListener(View.OnClickListener {
Toast.makeText(this@DemoActivity, "OnClick Called", Toast.LENGTH_LONG).show();
})
.show()


长的文本

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text.")
.show()


自定义进入/退出动画

  Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setEnterAnimation(R.anim.alerter_slide_in_from_left)
.setExitAnimation(R.anim.alerter_slide_out_to_right)
.show()

可见性回调

 Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnShowListener(OnShowAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Show Alert", Toast.LENGTH_LONG).show()
})
.setOnHideListener(OnHideAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Hide Alert", Toast.LENGTH_LONG).show()
})
.show()

自定义字体和文本外观

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setTitleAppearance(R.style.AlertTextAppearance_Title)
.setTitleTypeface(Typeface.createFromAsset(getAssets(), "Pacifico-Regular.ttf"))
.setText("Alert text...")
.setTextAppearance(R.style.AlertTextAppearance_Text)
.setTextTypeface(Typeface.createFromAsset(getAssets(), "ScopeOne-Regular.ttf"))
.show()


退出

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableSwipeToDismiss()
.show()


Progress Bar

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableProgress(true)
.setProgressColorRes(R.color.colorAccent)
.show()


带按钮

 Alerter.create(this@KotlinDemoActivity)
.setTitle(R.string.title_activity_example)
.setText("Alert text...")
.addButton("Okay", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "Okay Clicked", Toast.LENGTH_LONG).show()
})
.addButton("No", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "No Clicked", Toast.LENGTH_LONG).show()
})
.show()


自定义layout

 Alerter.create(this@KotlinDemoActivity, R.layout.custom_layout)
.setBackgroundColorRes(R.color.colorAccent)
.also { alerter ->
val tvCustomView = alerter.getLayoutContainer()?.tvCustomLayout
tvCustomView?.setText(R.string.with_custom_layout)
}
.show()


github地址:https://github.com/Tapadoo/Alerter
下载地址:master.zip


收起阅读 »

iOS开发你不知道的事-编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。

编译流程分析
现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}

在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

其实上述过程可以分解为四步:

1、预处理(Prepressing)
2、编译(Compilation)
3、汇编(Assembly)
4、链接(Linking)


预编译
首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i

还可以下面的表达

$ cpp hello.c > hello.i

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

1、将所有的#define删除,并展开所有的宏定义
2、处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
3、处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
4、删除所有的注释//和/**/
5、添加行号和文件名标识,比如#2 “hello.c” 2。
6、保留所有的#pragma编译器指令
截图个大家看看效果


经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)
编译过程就是把预处理完的文件进行一系列的:词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s


通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o

链接(linking)
链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc

链接相应的库


下面在贴出我们的写出的源代码是如何变成目标代码的流程图:


主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看


iOS的编译器
iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

1.LLVM核心库:
LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)。LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。。

2.LLVM IR 生成器Clang: Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。

3.LLDB项目:
LLDB项目以LLVM和Clang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST和表达式解析器,LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。

4.libc和libc++:
libc 和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。

5.lld项目:
lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。


编译器前端的任务是进行:

1、语法分析
2、语义分析
3、生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。


以上图解内容所做的是事情和gcc编译一模模一样样!

iOS程序-详细编译过程
1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
4.链接文件:将项目中的多个可执行文件合并成一个文件;
5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
6.编译 storyboard 文件:storyboard 文件也是会被编译的;
7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
10.生成 .app 包
11.将 Swift 标准库拷贝到包中
12.对包进行签名
13.完成打包

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

收起阅读 »

测试 View Controllers

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。让事情保持简单测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动...
继续阅读 »

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。

让事情保持简单

测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动开发(简称 TDD),有些人喜欢它,有些人则不喜欢。我们在这里不深入讨论,只是如果用 TDD,你得在写代码之前先写好测试。如果你好奇的话,可以去找 Wikipedia 上的文章看看。同时,我们也认为重构和测试可以很好地结合在一起。

测试 UI 部分通常很麻烦,因为它们包含太多活动部件。通常,view controller 需要和大量的 model 和 view 类交互。为了使 view controller 便于测试,我们要让任务尽量分离。

幸好,我们在更轻量的 view controller 这篇文章中的阐述的技术可以让测试更加简单。通常,如果你发现有些地方很难做测试,这就说明你的设计出了问题,你应该重构它。你可以重新参考更轻量的 view controller 这篇文章来获得一些帮助。总的目标就是有清晰的关注点分离。每个类只做一件事,并且做好。这样就可以让你只测试这件事。

记住:测试越多,回报的增长趋势越慢。首先你应该做简单的测试。当你觉得满意时,再加入更多复杂的测试。

Mocking

当你把一个整体拆分成小零件(比如更小的类)时,我们可以针对每个小的类来进行测试。但由于我们测试的类会和其他类交互,这里我们用一个所谓的 mock 或 stub 来绕开它。把 mock 对象看成是一个占位符,我们测试的类会跟这个占位符交互,而不是真正的那个对象。这样,我们就可以针对性地测试,并且保证不依赖于应用程序的其他部分。

在示例程序中,我们有个包含数组的 data source 需要测试。这个 data source 会在某个时候从 table view 中取出(dequeue)一个 cell。在测试过程中,还没有 table view,但是我们传递一个 mock 的 table view,这样即使没有 table view,也可以测试 data source,就像下面你即将看到的。起初可能有点难以理解,多看几次后,你就能体会到它的强大和简单。

Objective-C 中有个用来 mocking 的强大工具叫做 OCMock。它是一个非常成熟的项目,充分利用了 Objective-C 运行时强大的能力和灵活性。它使用了一些很酷的技巧,让通过 mock 对象来测试变得更加有趣。

本文后面有 data source 测试的例子,它更加详细地展示了这些技术如何工作在一起。

SenTestKit

编者注 这一节有一些过时了。在 Xcode 5 中 SenTestingKit 已经被 XCTest 完全取代,不过两者使用上没有太多区别,我们可以通过 Xcode 的 Edit -> Refactor -> Convert to XCTest 选项来切换到新的测试框架

我们将要使用的另一个工具是一个测试框架,开发者工具的一部分:Sente 的 SenTestingKit。这个上古神器从 1997 年起就伴随在 Objective-C 开发者左右,比第一款 iPhone 发布还早 10 年。现在,它已经集成到 Xcode 中了。SenTestingKit 会运行你的测试。通过 SenTestingKit,你将测试组织在类中。你需要给每一个你想测试的类创建一个测试类,类名以 Tests 结尾,它反应了这个类是干什么的。

这些测试类里的方法会做具体的测试工作。方法名必须以 test 开头来作为触发一个测试运行的条件。还有特殊的 -setUp 和 -tearDown 方法,你可以重载它们来设置各个测试。记住,你的测试类就是个类而已:只要对你有帮助,可以按需求在里面加 properties 和辅助方法。

做测试时,为测试类创建基类是个不错的模式。把通用的逻辑放到基类里面,可以让测试更简单和集中。可以通过示例程序中的例子来看看这样带来的好处。我们没有使用 Xcode 的测试模板,为了让事情简单有效,我们只创建了单独的 .m 文件。通过把类名改成以 Tests 结尾,类名可以反映出我们在对什么做测试。

编者注 Xcode 5 中 默认的测试模板也不再会自动创建 .h 文件了

与 Xcode 集成

测试会被 build 成一个 bundle,其中包含一个动态库和你选择的资源文件。如果你要测试某些资源文件,你得把它们加到测试的 target 中,Xcode 就会将它们打包到一个 bundle 中。接着你可以通过 NSBundle 来定位这些资源文件,示例项目实现了一个 -URLForResource:withExtension: 方法来方便的使用它。

Xcode 中的每个 scheme 定义了相应的测试 bundle 是哪个。通过 ⌘-R 运行程序,⌘-U 运行测试。

测试的运行依附于程序的运行,当程序运行时,测试 bundle 将被注入(injected)。测试时,你可能不想让你的程序做太多的事,那样会对测试造成干扰。可以把下面的代码加到 app delegate 中:

static BOOL isRunningTests(void) __attribute__((const));

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (isRunningTests()) {
return YES;
}

//
// Normal logic goes here
//

return YES;
}

static BOOL isRunningTests(void)
{
NSDictionary* environment = [[NSProcessInfo processInfo] environment];
NSString* injectBundle = environment[@"XCInjectBundle"];
return [[injectBundle pathExtension] isEqualToString:@"octest"];
}

编辑 Scheme 给了你极大的灵活性。你可以在测试之前或之后运行脚本,也可以有多个测试 bundle。这对大型项目来说很有用。最重要的是,可以打开或关闭个别测试,这对调试测试非常有用,只是要记得之后再把它们重新全部打开。

还要记住你可以为测试代码下断点,当测试执行时,调试器会在断点处停下来。

测试 Data Source
好了,让我们开始吧。我们已经通过拆分 view controller 让测试工作变得更轻松了。现在我们要测试 ArrayDataSource。首先我们新建一个空的,基本的测试类。我们把接口和实现都放到一个文件里;也没有哪个地方需要包含 @interface,放到一个文件会显得更加漂亮和整洁。

#import "PhotoDataTestCase.h"

@interface ArrayDataSourceTest : PhotoDataTestCase
@end

@implementation ArrayDataSourceTest
- (void)testNothing;
{
STAssertTrue(YES, @"");
}
@end

这个类没做什么事,只是展示了基本的设置。当我们运行这个测试时,-testNothing 方法将会运行。特别地,STAssert宏将会做琐碎的检查。注意,前缀 ST 源自于 SenTestingKit。这些宏和Xcode 集成,会把失败显示到侧边面板的Issues导航栏中。

第一个测试

我们现在把 testNothing 替换成一个简单、真正的测试:

- (void)testInitializing;
{
STAssertNil([[ArrayDataSource alloc] init], @"Should not be allowed.");
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){};
id obj1 = [[ArrayDataSource alloc] initWithItems:@[]
cellIdentifier:@"foo"
configureCellBlock:block];
STAssertNotNil(obj1, @"");
}

接着,我们想测试ArrayDataSource实现的方法:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath;

为此,我们创建一个测试方法:

- (void)testCellConfiguration;

首先,创建一个 data source:

__block UITableViewCell *configuredCell = nil;
__block id configuredObject = nil;
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){
configuredCell = a;
configuredObject = b;
};
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:block];

注意,configureCellBlock 除了存储对象以外什么都没做,这可以让我们可以更简单地测试它。

然后,我们为 table view 创建一个 mock 对象:

id mockTableView = [OCMockObject mockForClass:[UITableView class]];

Data source 将在传进来的 table view 上调用 -dequeueReusableCellWithIdentifier:forIndexPath: 方法。我们将告诉 mock object 当它收到这个消息时要做什么。首先创建一个 cell,然后设置 mock。

UITableViewCell *cell = [[UITableViewCell alloc] init];
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[[mockTableView expect] andReturn:cell]
dequeueReusableCellWithIdentifier:@"foo"
forIndexPath:indexPath];

第一次看到它可能会觉得有点迷惑。我们在这里所做的,是让 mock 记录特定的调用。Mock 不是一个真正的 table view;我们只是假装它是。-expect 方法允许我们设置一个 mock,让它知道当这个方法调用时要做什么。

另外,-expect 方法也告诉 mock 这个调用必须发生。当我们稍后在 mock 上调用 -verify 时,如果那个方法没有被调用过,测试就会失败。相应地,-stub 方法也用来设置 mock 对象,但它不关心方法是否被调用过。

现在,我们要触发代码运行。我们就调用我们希望测试的方法。

NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
id result = [dataSource tableView:mockTableView
cellForRowAtIndexPath:indexPath];

然后我们测试是否一切正常:

STAssertEquals(result, cell, @"Should return the dummy cell.");
STAssertEquals(configuredCell, cell, @"This should have been passed to the block.");
STAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block.");
[mockTableView verify];

STAssert宏测试值的相等性。注意,前两个测试,我们通过比较指针来完成;我们不使用-isEqual:,是因为我们实际希望测试的是result,cell和configuredCell都是同一个对象。第三个测试要用 -isEqual:,最后我们调用 mock的 -verify方法。

注意,在示例程序中,我们是这样设置 mock 的:

id mockTableView = [self autoVerifiedMockForClass:[UITableView class]];

这是我们测试基类中的一个方便的封装,它会在测试最后自动调用 -verify 方法。

测试 UITableViewController
下面,我们转向PhotosViewController。它是个 UITableViewController 的子类,它使用了我们刚才测试过的 data source。View controller 剩下的代码已经相当简单了。

我们想测试点击 cell 后把我们带到详情页面,即一个 PhotoViewController的实例被 push 到 navigation controller 里面。我们再次使用 mocking 来让测试尽可能不依赖于其他部分。

首先我们创建一个 UINavigationController 的 mock:

id mockNavController = [OCMockObject mockForClass:[UINavigationController class]];

接下来,我们要使用部分 mocking。我们希望 PhotosViewController 实例的navigationController 返回 mockNavController。我们不能直接设置 navigation controller,所以我们简单地用 stub 来替换掉 PhotosViewController实例这个方法,让它返回mockNavController 就可以了。

PhotosViewController *photosViewController = [[PhotosViewController alloc] init];
id photosViewControllerMock = [OCMockObject partialMockForObject:photosViewController];
[[[photosViewControllerMock stub] andReturn:mockNavController] navigationController];

现在,任何时候对 photosViewController 调用 -navigationController 方法,都会返回 mockNavController。这是个强大的技巧,OCMock 就有这样的本领。

接下来,我们要告诉 navigation controller mock 我们调用的期望,即,一个 photo 不为 nil 的 detail view controller。

UIViewController* viewController = [OCMArg checkWithBlock:^BOOL(id obj) {
PhotoViewController *vc = obj;
return ([vc isKindOfClass:[PhotoViewController class]] &&
(vc.photo != nil));
}];
[[mockNavController expect] pushViewController:viewController animated:YES];

现在,我们触发 view 加载,并且模拟一行被点击:

UIView *view = photosViewController.view;
STAssertNotNil(view, @"");
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[photosViewController tableView:photosViewController.tableView
didSelectRowAtIndexPath:indexPath];

最后我们验证 mocks 上期望的方法被调用过:

[mockNavController verify];
[photosViewControllerMock verify];

现在我们有了一个测试,用来测试和 navigation controller 的交互,以及正确 view controller 的创建。

又一次地,我们在示例程序中使用了便捷的方法:

- (id)autoVerifiedMockForClass:(Class)aClass;
- (id)autoVerifiedPartialMockForObject:(id)object;

于是,我们不需要记住调用-verify。

进一步探索

就像你从上面看到的那样,部分 mocking 非常强大。如果你看看 -[PhotosViewController setupTableView]方法的源码,你就会看到它是如何从 app delegate 中取出 model 对象的。

NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;

上面的测试依赖于这行代码。打破这种依赖的一种方式是再次使用 部分 mocking,让 app delegate 返回预定义的数据,就像这样:

id storeMock; // 假设我们已经设置过了
id appDelegate = [AppDelegate sharedDelegate]
id appDelegateMock = [OCMockObject partialMockForObject:appDelegate];
[[[appDelegateMock stub] andReturn:storeMock] store];

现在,无论何时调用[AppDelegate sharedDelegate].store,它将返回 storeMock。将这个技术使用好的话,可以确保让你的测试恰到好处地在保持简单和应对复杂之间找到平衡。

需要记住的事

部分 mock 技术将会在 mocks 的存在期间替换并保持被 mocking 的对象,并且一直有效。你可以通过提前调用[aMock stopMocking]来终于这种行为。大多数时候,你希望 部分 mock 在整个测试期间都保持有效。如果要提前终止,请确保在测试方法最后放置[aMock verify]。否则 ARC 会过早释放这个 mock,这样你就不能 -verify了,这不太可能是你想要的结果。

测试 NIB 加载

PhotoCell设置在一个 NIB 中,我们可以写一个简单的测试来检查 outlets 设置得是否正确。我们来回顾一下PhotoCell类:

@interface PhotoCell : UITableViewCell

+ (UINib *)nib;

@property (weak, nonatomic) IBOutlet UILabel* photoTitleLabel;
@property (weak, nonatomic) IBOutlet UILabel* photoDateLabel;

@end

我们的简单测试的实现看上去是这样:

@implementation PhotoCellTests

- (void)testNibLoading;
{
UINib *nib = [PhotoCell nib];
STAssertNotNil(nib, @"");

NSArray *a = [nib instantiateWithOwner:nil options:@{}];
STAssertEquals([a count], (NSUInteger) 1, @"");
PhotoCell *cell = a[0];
STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

// 检查 outlet 是否正确设置
STAssertNotNil(cell.photoTitleLabel, @"");
STAssertNotNil(cell.photoDateLabel, @"");
}

@end

非常基础,但是能出色完成工作。

值得一提的是,当有发生改变时,我们需要同时更新测试以及相应的类或 nib 。这是事实。你需要考虑改变类或者 nib 文件时可能会打破原有的 outlets 连接。如果你用了 .xib 文件,你可能要注意了,这是经常发生的事。

关于 Class 和 Injection
我们已经从与 Xcode 集成得知,测试 bundle 会注入到应用程序中。省略注入的如何工作的细节(它本身是个巨大的话题),简单地说:注入是把待注入的 bundle(我们的测试 bundle)中的 Objective-C 类添加到运行的应用程序中。这很好,因为这样允许我们运行测试了。

还有一件事会很让人迷惑,那就是如果我们同时把一个类添加到应用程序和测试 bundle中。如果在上面的示例程序中,我们(不小心)把 PhotoCell 类同时添加到测试 bundle 和应用程序里的话,在测试 bundle 中调用 [PhotoCell class]会返回一个不同的指针(你应用程序中的那个类)。于是我们的测试将会失败:

STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

再一次声明:注入很复杂。你应该确认的是:不要把应用程序中的 .m 文件添加到测试 target 中。否则你会得到预想不到的行为。

额外的思考

如果你使用一个持续集成 (CI) 的解决方案,让你的测试启动和运行是一个好主意。详细的描述超过了本文的范围。这些脚本通过 RunUnitTests 脚本触发。还有个 TEST_AFTER_BUILD 环境变量。

另一种有趣的选择是创建单独的测试 bundle 来自动化性能测试。你可以在测试方法里做任何你想做的。定时调用一些方法并使用 STAssert 来检查它们是否在特定阈值里面是其中一种选择。

链接:https://www.jianshu.com/p/733fa8bbca95

收起阅读 »

基于环信MQTT消息云,Java版MQTT客户端快速实现消息收发

本文介绍Java版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。 一、前提条件1.部署Java开发环境安装IDE。您可以使用IntelliJ IDEA或者Eclipse,本文以IntelliJ IDEA为例。下载安装JDK。&n...
继续阅读 »

本文介绍Java版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。

 

一、前提条件

1.部署Java开发环境

安装IDE。您可以使用IntelliJ IDEA或者Eclipse,本文以IntelliJ IDEA为例。
下载安装
JDK 

2.导入项目依赖

IntelliJ IDEA中创建工程,并确认pom.xml中包含以下依赖。


commons-codec
commons-codec
1.10


org.eclipse.paho
org.eclipse.paho.client.mqttv3
1.2.2


org.apache.httpcomponents
httpclient
4.5.2


com.alibaba
fastjson
1.2.76

二、实现流程

1、获取鉴权

     为保障客户安全性需求,环信MQTT消息云服务为客户提供【token+clientID】方式实现鉴权认证,其中AppID(clientID中的关键字段)及token标识获取流程如下:

【登录console】
     欢迎您登录环信云console控制台,在此控制台中,为您提供应用列表、解决方案、DEMO体验以及常见问题等功能。
     在应用列表中,若您未在APP中开通MQTT业务,可参见APP  MQTT开通流程
     若APP已开通MQTT业务,可在应用列表中选中Appname,点击【查看】操作,进入应用详情。


【获取AppID及连接地址】 
      进入【查看】后,点击左侧菜单栏【MQTT】->【服务概览】,在下图红色方框内获取当前AppID及服务器连接地址。

【获取token】
     为实现对用户管控及接入安全性,环信云console提供用户认证功能,支持对用户账户的增、删、改、查以及为每个用户账户分配唯一token标识,获取token标识可选择以下两种形式。
  形式一:console控制台获取(管理员视角)
  * 点击左侧菜单栏【应用概览】->【用户认证】页面,点击【创建IM用户】按钮,增添新的账户信息(包  括用户名及密码)。
  * 创建成功后,在【用户ID】列表中选中账户,点击【查看token】按钮获取当前账户token信息。


  形式二:客户端代码获取(客户端视角)
  * 获取域名:点击左侧菜单栏【即时通讯】->【服务概览】页面,查看下图中token域名、org_name、app_name。


  * 拼接URL:获取token URL格式为:http:/ /token域名/org_name/app_name/token。 
  * 用户名/密码:使用【用户ID】列表中已有账户的用户名及密码,例“用户名:test/密码:test123”。

客户端获取token代码示例如下:

public static void main() 
{
// 获取token的URL
http://{token域名}/{org_name}/{app_name}/token
// 获取token
String token = "";
// 取token
try (final CloseableHttpClient httpClient = HttpClients.createDefault())
{
final HttpPost httpPost = new HttpPost("http://{token域名}/{org_name}/{app_name}/token");
Map params = new HashMap<>();
params.put("grant_type", "password");
params.put("username", "test");
params.put("password", "test123");
//设置请求体参数
StringEntity entity = new StringEntity(JSONObject.toJSONString(params), Charset.forName("utf-8"));
entity.setContentEncoding("utf-8");
httpPost.setEntity(entity);
//设置请求头部
httpPost.setHeader("Content-Type", "application/json");
//执行请求,返回请求响应
try (final CloseableHttpResponse response = httpClient.execute(httpPost)
{
//请求返回状态码
int statusCode = response.getStatusLine().getStatusCode();
//请求成功
if (statusCode == HttpStatus.SC_OK && statusCode <= HttpStatus.SC_TEMPORARY_REDIRECT)
{
//取出响应体
final HttpEntity entity2 = response.getEntity();
//从响应体中解析出token
String responseBody = EntityUtils.toString(entity2, "utf-8");
JSONObject jsonObject = JSONObject.parseObject(responseBody);
token = jsonObject.getString("access_token");
}
else
{
//请求失败
throw new ClientProtocolException("请求失败,响应码为:" + statusCode);
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
}

返回结果

 {
"access_token": "YWMtN8a0oqV3EeuF0AmiqRgEh-grzF8zZk2Wp8GS3pF-orDW_F-gj3kR6os3h_oz3ROQAwMAAAF5BxhGlwBPGgAvTR8vDrdVsDPNZMQj0fFjv7EaohgZhzMHM9ncVLE30g",
"expires_in": 5184000,
"user":
{
"uuid": "d6fc5fa0-8f79-11ea-8b37-87fa33dd1390",
"type": "user",
"created": 1588756404898,
"modified": 1588756404898,
"username": "test",
"activated": true
}
}
access_token即为要获取的token

2、初始化

      在IntelliJ IDEA工程中创建MQTT客户端,客户端初始配置包括创建clientID,topic名称,QoS质量,连接地址等信息。

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.eclipse.paho.client.mqttv3.MqttConnectOptions.MQTT_VERSION_3_1_1;
public class MqttDemoStarter 
{
public static void main(String[] args) throws MqttException, InterruptedException {
/**
* 用户指定
* /
String deviceId = "xxxxx-xxxx-xxxxx-xxxxx-xxxxx";
/**
* 从console控制台获取
* /
String appId = "1NQ1E9";
/**
* 设置接入点,进入console控制台获取
*/
String endpoint = "1NQ1E9.sandbox.mqtt.chat";
/**
* MQTT客户端ID,由业务系统分配,需要保证每个TCP连接都不一样,保证全局唯一,如果不同的客户端对象(TCP连接)使用了相同的clientId会导致之前的连接断开。
* clientId由两部分组成,格式为DeviceID@appId,其中DeviceID由业务方自己设置,appId在console控制台创建,clientId总长度不得超过64个字符。
*/
String clientId = deviceId + "@" + appId;

/**
* 需要订阅或发送消息的topic名称
* 如果使用了没有创建或者没有被授权的Topic会导致鉴权失败,服务端会断开客户端连接。
*/
final String myTopic = "myTopic";

/**
* QoS参数代表传输质量,可选0,1,2。详细信息,请参见名词解释。
*/
final int qosLevel = 0;
final MemoryPersistence memoryPersistence = new MemoryPersistence();

/**
* 客户端协议和端口。客户端使用的协议和端口必须匹配,如果是ws或者wss,使用http://;如果是mqtt或者mqtts,使用tcp://
*/
final MqttClient mqttClient = new MqttClient("tcp://" + endpoint + ":1883", clientId, memoryPersistence);
/**
* 设置客户端发送超时时间,防止无限阻塞。
*/
mqttClient.setTimeToWait(5000);

final ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
}

3、连接服务器

    配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
/**
* 用户名,在console中注册
*/
mqttConnectOptions.setUserName("test");
/**
* 用户密码为第一步中申请的token
*/
mqttConnectOptions.setPassword(token.toCharArray());
mqttConnectOptions.setCleanSession(true);
mqttConnectOptions.setKeepAliveInterval(90);
mqttConnectOptions.setAutomaticReconnect(true);
mqttConnectOptions.setMqttVersion(MQTT_VERSION_3_1_1);
mqttConnectOptions.setConnectionTimeout(5000);
mqttClient.connect(mqttConnectOptions);
//暂停1秒钟,等待连接订阅完成
Thread.sleep(1000);

4、订阅【subscribe】

【订阅主题】

    当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。

mqttClient.setCallback(new MqttCallbackExtended() {
/**
* 连接完成回调方法
* @param b
* @param s
*/
@Override
public void connectComplete(boolean b, String s) {
/**
* 客户端连接成功后就需要尽快订阅需要的Topic。
*/
System.out.println("connect success");
executorService.submit(() -> {
try {
final String[] topicFilter = {myTopic};
final int[] qos = {qosLevel};
mqttClient.subscribe(topicFilter, qos);
} catch (Exception e) {
e.printStackTrace();
}
});
}
});

【取消订阅】

mqttClient.unsubscribe(new String[]{myTopic});

【接收消息】

    配置接收消息回调方法,从环信MQTT消息云接收订阅消息。

mqttClient.setCallback(new MqttCallbackExtended() {
/**
* 接收消息回调方法
* @param s
* @param mqttMessage
*/
@Override
public void messageArrived(String s, MqttMessage mqttMessage) {
System.out.println("receive msg from topic " + s + " , body is " + new String(mqttMessage.getPayload()));
}
});

5、发布【publish】

    配置发送消息回调方法,向环信MQTT消息云中指定topic发送消息。

for (int i = 0; i < 10; i++) {
/**
* 构建一个Mqtt消息
*/
MqttMessage message = new MqttMessage("hello world pub sub msg".getBytes());
//设置传输质量
message.setQos(qosLevel);
/**
* 发送普通消息时,Topic必须和接收方订阅的Topic一致,或者符合通配符匹配规则。
*/
mqttClient.publish(myTopic, message);
}

6、结果验证

connect success
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg

三、更多信息

  * 完整demo示例,请参见demo下载

或直接下载:MQTTChatDemo- Java.zip

  * 目前MQTT客户端支持多种语言,请参见 SDK下载
  * 如果您在使用环信MQTT消息云服务中,有任何疑问和建议,欢迎您联系我们

 

收起阅读 »

一个围绕 CFNetwork API的网络通讯库,断点续传神器!

ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。它适用于执行...
继续阅读 »

ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。

它适用于执行基本的HTTP请求并与基于REST的服务(GET / POST / PUT / DELETE交互包含的 ASIFormDataRequest 子类使得使用 multipart/form-data提交POST数据和文件变得容易

它提供:

  • 一个简单的界面,用于向网络服务器提交数据和从网络服务器获取数据
  • 将数据下载到内存或直接下载到磁盘上的文件
  • 在本地驱动器上提交文件作为POST数据的一部分,与HTML文件输入机制兼容
  • 将请求正文直接从磁盘传输到服务器,以节省内存
  • 恢复部分下载
  • 轻松访问请求和响应HTTP标头
  • 进度委托(NSProgressIndicators 和 UIProgressViews)显示有关下载上传进度的信息
  • 操作队列上传下载进度指示器的自动魔法管理
  • 基本、摘要 + NTLM身份验证支持,凭据在会话期间自动重复使用,并且可以存储在钥匙串中以备日后使用。
  • 饼干支持
  • []当您的应用程序移至后台时,请求可以继续运行(iOS 4+)
  • GZIP支持响应数据请求正文
  • 包含的 ASIDownloadCache 类允许请求透明地缓存响应,并且即使在没有可用网络的情况下也允许对缓存数据的请求成功
  • [] ASIWebPageRequest – 下载完整的网页,包括图像和样式表等外部资源。即使没有网络连接,任何大小的页面都可以无限期地缓存,并显示在 UIWebview/WebView 中。
  • 易于使用的 Amazon S3 支持 – 无需自己动手签署请求!
  • 完全支持 Rackspace 云文件
  • []客户端证书支持
  • 支持手动和自动检测代理、验证代理和PAC文件自动配置。内置的登录对话框让您的 iPhone 应用程序可以透明地使用身份验证代理,无需任何额外工作。
  • 带宽限制支持
  • 支持持久连接
  • 支持同步和异步请求
  • 通过委托或 [ NEW ] 块获取有关请求状态更改的通知(Mac OS X 10.6、iOS 4 及更高版本)
  • 带有广泛的单元测试

ASIHTTPRequest 兼容 Mac OS 10.5 或更高版本,以及 iOS 3.0 或更高版本。


创建同步请求

使用 ASIHTTPRequest 的最简单方法。发送startSynchronous 消息将在同一线程中执行请求,并在完成(成功或失败)时返回控制权。

通过检查error属性来检查问题

要以字符串形式获取响应,请调用responseString方法。不要将它用于二进制数据 - 使用responseData获取 NSData 对象,或者,对于较大的文件,将您的请求设置为使用downloadDestinationPath属性下载到文件

- (IBAction)grabURL:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request startSynchronous];
NSError *error = [request error];
if (!error) {
NSString *response = [request responseString];
}
}

与前面的示例执行相同的操作,但请求在后台运行。

- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
}
 
- (void)requestFinished:(ASIHTTPRequest *)request
{
// Use when fetching text data
NSString *responseString = [request responseString];
 
// Use when fetching binary data
NSData *responseData = [request responseData];
}
 
- (void)requestFailed:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}

请注意,我们设置了请求的委托属性,以便我们可以在请求完成或失败时收到通知。

这是创建异步请求的最简单方法,它将在后台运行在全局 NSOperationQueue 中。对于更复杂的操作(例如跟踪多个请求的进度),您可能希望创建自己的队列,这就是我们接下来将介绍的内容。

使用块

从 v1.8 开始,我们可以在支持块的平台上使用块来做同样的事情:

- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
// Use when fetching text data
NSString *responseString = [request responseString];
 
// Use when fetching binary data
NSData *responseData = [request responseData];
}];
[request setFailedBlock:^{
NSError *error = [request error];
}];
[request startAsynchronous];
}

请注意在我们声明请求时使用__block限定符,这很重要!它告诉块不要保留请求,这对于防止保留循环很重要,因为请求将始终保留块。

使用队列

这个例子再次做同样的事情,但我们为我们的请求创建了一个 NSOperationQueue。

使用您自己创建的 NSOperationQueue(或 ASINetworkQueue,见下文)可以让您更好地控制异步请求。使用队列时,只能同时运行一定数量的请求。如果您添加的请求多于队列的maxConcurrentOperationCount属性,则请求将在开始之前等待其他人完成。

- (IBAction)grabURLInTheBackground:(id)sender
{
if (![self queue]) {
[self setQueue:[[[NSOperationQueue alloc] init] autorelease]];
}
 
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setDidFinishSelector:@selector(requestDone:)];
[request setDidFailSelector:@selector(requestWentWrong:)];
[[self queue] addOperation:request]; //queue is an NSOperationQueue
}
 
- (void)requestDone:(ASIHTTPRequest *)request
{
NSString *response = [request responseString];
}
 
- (void)requestWentWrong:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}


demo及常见问题:https://github.com/paytronix/ASIHTTPRequest

源码下载:ASIHTTPRequest-master.zip


收起阅读 »

扁平化 UI 的 iOS 组件-FlatUIKit

FlatUIKitFlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合。它的设计灵感来自于Flat UI和Kyle Miller。样式是通过替换现有 UIKit 组件的类别来实现的,因此将其...
继续阅读 »


FlatUIKit

FlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合它的设计灵感来自于Flat UIKyle Miller样式是通过替换现有 UIKit 组件的类别来实现的,因此将其集成到您的项目中非常简单。

安装

FlatUIKit 可以通过CocoaPods安装只需添加

pod 'FlatUIKit'

组件

FUIButton 是 UIButton 的一个嵌入式子类,它公开了额外的属性 buttonColor、shadowColor、cornerRadius 和 shadowHeight。请注意,如果您设置了其中任何一个,则必须设置所有这些。

myButton.buttonColor = [UIColor turquoiseColor ];
myButton.shadowColor = [UIColor
greenSeaColor ];
myButton.shadowHeight =
30f ;
myButton.cornerRadius =
60f ;
myButton.titleLabel.font = [UIFont
boldFlatFontOfSize: 16 ];
[myButton
setTitleColor: [UIColor cloudColor ] forState: UIControlStateNormal];
[myButton
setTitleColor: [UIColor cloudColor ] forState: UIControlStateHighlighted];

FUITextField 是 UITextField 的一个嵌入式子类,它公开了附加属性 edgeInsets、textFieldColor、borderColor、borderWidth 和 cornerRadius。请注意,如果您设置了其中任何一个,则必须设置所有这些。

myTextField.font = [UIFont flatFontOfSize: 16 ];
myTextField.backgroundColor = [UIColor
clearColor ];
myTextField.edgeInsets = UIEdgeInsetsMake(
4 . 0f , 15 . 0f , 4 . 0f , 15 . 0f );
myTextField.textFieldColor = [UIColor
whiteColor ];
myTextField.borderColor = [UIColor
turquoiseColor ];
myTextField.borderWidth =
20f ;
myTextField.cornerRadius =
30f ;

FUISegmentedControl 是 UISegmentedControl 的一个嵌入式子类,它公开了附加属性 selectedColor、deselectedColor、selectedFont、deselectedFont、selectedFontColor、deselectedFontColor、dividerColor 和 cornerRadius。请注意,如果您设置了其中任何一个,建议您设置所有这些。

mySegmentedControl.selectedFont = [UIFont boldFlatFontOfSize: 16 ];
mySegmentedControl.selectedFontColor = [UIColor
cloudColor ];
mySegmentedControl.deselectedFont = [UIFont
flatFontOfSize: 16 ];
mySegmentedControl.deselectedFontColor = [UIColor
cloudColor ];
mySegmentedControl.selectedColor = [UIColor
amethystColor ];
mySegmentedControl.deselectedColor = [UIColor
silverColor ];
mySegmentedControl.dividerColor = [的UIColor
midnightBlueColor ];
mySegmentedControl.cornerRadius =
5.0 ;

FUISwitch 不是 UISwitch 的子类(UISwitch 的子类太不灵活了),而是一个重新实现,它暴露了 UISwitch 的所有方法。此外,它还提供对其底层开/关 UILabels 和其他子视图的访问。

mySwitch.onColor = [UIColor turquoiseColor ];
mySwitch.offColor = [UIColor
cloudColor ];
mySwitch.onBackgroundColor = [的UIColor
midnightBlueColor ];
mySwitch.offBackgroundColor = [UIColor
silverColor ];
mySwitch.offLabel.font = [UIFont
boldFlatFontOfSize: 14 ];
mySwitch.onLabel.font = [UIFont
boldFlatFontOfSize: 14 ];

与 FUISwitch 类似,FUIAlertView 是 UIAlertView 的重新实现,它公开了 UIAlertView 的所有方法(和委托方法,使用 FUIAlertViewDelegate 协议),但在 UI 定制方面具有更大的灵活性。它的所有子 UILabels、UIViews 和 FUIButtons 都可以随意定制。

FUIAlertView *alertView = [[FUIAlertView alloc ] initWithTitle: @" Hello "
message: @" This is an alert view "
delegate: nil cancelButtonTitle: @" Dismiss "
otherButtonTitles: @" Do Something " , nil ];
alertView.titleLabel.textColor = [UIColor
cloudColor ];
alertView.titleLabel.font = [UIFont
boldFlatFontOfSize: 16 ];
alertView.messageLabel.textColor = [UIColor
cloudColor ];
alertView.messageLabel.font = [UIFont
flatFontOfSize: 14 ];
alertView.backgroundOverlay.backgroundColor = [[UIColor
cloudColor ] colorWithAlphaComponent: 0.8 ];
alertView.alertContainer.backgroundColor = [的UIColor
midnightBlueColor ];
alertView.defaultButtonColor = [UIColor
cloudColor ];
alertView.defaultButtonShadowColor = [UIColor
asbestosColor ];
alertView.defaultButtonFont = [UIFont
boldFlatFontOfSize: 16 ];
alertView.defaultButtonTitleColor = [UIColor
asbestosColor ];

为了提供平面 UISlider、UIProgressViews 和 UISteppers,我们只需在 UISlider/ProgressView/UIStepper 上提供类别,以使用适当的颜色/角半径自动配置它们的外观。这有助于与您现有的项目零摩擦集成:

[mySlider configureFlatSliderWithTrackColor: [UIColor silverColor ]
progressColor: [UIColor alizarinColor ]
thumbColor: [UIColor pomegranateColor ]];

FUIS滑块

[myProgressView configureFlatProgressViewWithTrackColor: [UIColor silverColor ]
progressColor: [UIColor alizarinColor ]];

[myStepper
configureFlatStepperWithColor: [的UIColor wisteriaColor ]
highlightedColor: [的UIColor wisteriaColor ]
disabledColor: [的UIColor amethystColor ]
iconColor通过: [的UIColor cloudsColor ]];

要为整个应用程序自定义栏按钮项(包括后退按钮),UIBarButtonItem+FlatUI 提供了一个类方法,该方法利用 UIBarButtonItem 外观代理一步完成此操作:

[UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
highlightColor: [UIColor belizeHoleColor ]
cornerRadius: 3 ];

但是,这可能会导致从操作表、共享表或 web 视图中的链接推送的控制器出现渲染问题。为防止这种行为,请将自定义栏按钮项的范围限定到您的控制器:

[UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
highlightColor: [UIColor belizeHoleColor ]
cornerRadius: 3
whenContainedIn: [YourViewController class ]];

您可以修改 UITableViewCell 的 backgroundColor 和 selectedBackgroundColor 而不会丢失圆角。单元格将复制 UITableView 的分隔符颜色。分隔符高度显示为 separatorHeight,半径显示为 cornerRadius。

UITableViewCell *cell = ...;
[cell
configureFlatCellWithColor: [UIColor greenSeaColor ]
selectedColor: [UIColor cloudColor ]
roundingCorners: corners];

cell.cornerRadius =
50f ; //可选
cell.separatorHeight =
2 . 0f ; //可选

demo下载及常见问题:https://github.com/Grouper/FlatUIKit
源码下载:FlatUIKit.zip


收起阅读 »

模型处理工具不仅仅只有YYModel,还有更强的Mantle

Mantle 使为您的 Cocoa 或 Cocoa Touch 应用程序编写简单的模型层变得容易Let's use the GitHub API for demonstration~!typedef enum : NSUInteger { ...
继续阅读 »

Mantle 使为您的 Cocoa 或 Cocoa Touch 应用程序编写简单的模型层变得容易

Let's use the GitHub API for demonstration~!

typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;

@interface GHIssue : NSObject <NSCoding, NSCopying>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

- (id)initWithDictionary:(NSDictionary *)dictionary;

@end


typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;

@interface GHIssue : NSObject <NSCoding, NSCopying>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

- (id)initWithDictionary:(NSDictionary *)dictionary;

@end
@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}

- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [self init];
if (self == nil) return nil;

_URL = [NSURL URLWithString:dictionary[@"url"]];
_HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];

if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}

_title = [dictionary[@"title"] copy];
_retrievedAt = [NSDate date];
_body = [dictionary[@"body"] copy];
_reporterLogin = [dictionary[@"user"][@"login"] copy];
_assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];

_updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];

return self;
}

- (id)initWithCoder:(NSCoder *)coder {
self = [self init];
if (self == nil) return nil;

_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_retrievedAt = [NSDate date];
_body = [coder decodeObjectForKey:@"body"];
_reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
_assignee = [coder decodeObjectForKey:@"assignee"];
_updatedAt = [coder decodeObjectForKey:@"updatedAt"];

return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];

[coder encodeUnsignedInteger:self.state forKey:@"state"];
}

- (id)copyWithZone:(NSZone *)zone {
GHIssue *issue = [[self.class allocWithZone:zone] init];
issue->_URL = self.URL;
issue->_HTMLURL = self.HTMLURL;
issue->_number = self.number;
issue->_state = self.state;
issue->_reporterLogin = self.reporterLogin;
issue->_assignee = self.assignee;
issue->_updatedAt = self.updatedAt;

issue.title = self.title;
issue->_retrievedAt = [NSDate date];
issue.body = self.body;

return issue;
}

- (NSUInteger)hash {
return self.number.hash;
}

- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) return NO;

return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body];
}

@end

哇,对于这么简单的事情来说,这是很多样板!而且,即便如此,这个例子也没有解决一些问题:

  • 无法GHIssue使用来自服务器的新数据更新 a 
  • 没有办法将 a 变GHIssue JSON。
  • GHIssueState不应按原样编码。如果将来枚举更改,现有存档可能会中断。
  • 如果未来的界面发生GHIssue变化,现有的档案可能会中断。


为什么不使用核心数据?

Core Data 很好地解决了某些问题。如果您需要对数据执行复杂的查询,处理具有大量关系的巨大对象图,或支持撤消和重做,Core Data 非常适合。

然而,它确实有几个痛点:

  • 仍然有很多样板。托管对象减少了上面看到的一些样板,但 Core Data 有很多自己的样板。正确设置 Core Data 堆栈(带有持久存储和持久存储协调器)并执行提取可能需要多行代码。
  • 很难做对。即使是有经验的开发人员在使用 Core Data 时也可能会犯错误,而且该框架是不可原谅的。

如果您只是想访问一些 JSON 对象,那么 Core Data 可能会做大量工作,但收获甚微。

尽管如此,如果您已经在应用程序中使用或想要使用 Core Data,Mantle 仍然可以作为 API 和托管模型对象之间的方便转换层。


MTL模型

输入 MTLModelGHIssue看起来像继承自MTLModel

typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;

@interface GHIssue : MTLModel

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *updatedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@end


@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"URL": @"url",
@"HTMLURL": @"html_url",
@"number": @"number",
@"state": @"state",
@"reporterLogin": @"user.login",
@"assignee": @"assignee",
@"updatedAt": @"updated_at"
};
}

+ (NSValueTransformer *)URLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}

+ (NSValueTransformer *)assigneeJSONTransformer {
return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
}

+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
return [self.dateFormatter dateFromString:dateString];
} reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
return [self.dateFormatter stringFromDate:date];
}];
}

- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
self = [super initWithDictionary:dictionaryValue error:error];
if (self == nil) return nil;

// Store a value that needs to be determined locally upon initialization.
_retrievedAt = [NSDate date];

return self;
}

@end


值得注意的是,从这个版本是缺席的实现, -isEqual:,和-hash通过检查@property 您在子类中声明,MTLModel可以为所有这些方法提供默认实现。

原始示例的问题也都被修复了:

无法GHIssue使用来自服务器的新数据更新 a 

MTLModel有一个可扩展的-mergeValuesForKeysFromModel:方法,可以很容易地指定应该如何集成新的模型数据。

没有办法将 a 变GHIssue JSON。

这是可逆变压器真正派上用场的地方。+[MTLJSONAdapter JSONDictionaryFromModel:error:]可以将任何符合 的模型对象转换 回 JSON 字典。+[MTLJSONAdapter JSONArrayFromModels:error:]是相同的,但将模型对象数组转换为字典的 JSON 数组。

如果未来的界面发生GHIssue变化,现有的档案可能会中断。

MTLModel自动保存用于存档的模型对象的版本。取消归档时,-decodeValueForKey:withCoder:modelVersion:如果被覆盖将被调用,为您提供一个方便的挂钩来升级旧数据。


Mantle 不会自动为您保留对象。但是,MTLModel 确实符合,因此可以使用 将模型对象存档到磁盘 NSKeyedArchiver

如果你需要更强大的东西,或者想要避免将整个模型一次保存在内存中,Core Data 可能是更好的选择。

Carthage

github "Mantle/Mantle"


CocoaPods

target 'MyAppOrFramework' do
pod 'Mantle'
end


demo下载及常见问题:https://github.com/Mantle/Mantle

源码:Mantle-master.zip



收起阅读 »

Android 通知栏封装方案

BaseNotification获取此框架 allprojects { repositories { ... maven { url 'https://jitpack.io' } }}dependencies ...
继续阅读 »

BaseNotification

获取此框架 

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.Dboy233:BaseNotification:2.0'
}


如何使用此封装框架?

1.自定义你的Notification继承BaseNotification

public abstract class ChatChannelNotify<ChatData> extends BaseNotification<ChatData> {


public ChatChannelNotify(@NotNull ChatData mData) {
super(mData);
//使用add*函数添加对应自定义的布局 然后使用get*
addContentRemoteViews(R.layout.notify_comm_chat_layout);
}
/**
*渠道名字
*/
@NotNull
@Override
public String getChannelName() {
return NotificationConfig.CHANNEL_ID_CHAT;
}
/**
*渠道id
*/
@NotNull
@Override
public String getChannelId() {
return NotificationConfig.CHANNEL_ID_CHAT;
}

@Override
@RequiresApi(api = Build.VERSION_CODES.O)
public void configureChannel(@NotNull NotificationChannel notificationChannel) {
//配置渠道信息。如果没有这个渠道会创建这个渠道,如果有了,这个函数是不会被调用的
notificationChannel.setLockscreenVisibility(NotificationCompat.VISIBILITY_PUBLIC);
notificationChannel.setImportance(NotificationManager.IMPORTANCE_HIGH);
}


@Override
public void configureNotify(@NotNull NotificationCompat.Builder mBuilder) {
//配置你的通知属性
mBuilder.setShowWhen(true)
.setSmallIcon(getData().getSmallIcon())
.setContentTitle(getData().getContentTitle())
.setContentText(getData().getContentText())
.setTicker(getData().getContentTitle())
.setContentInfo(getData().getContentText())
.setAutoCancel(true)
.setSubText(getData().getContentText())
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setGroup("chat");
}

/**
*如果你在构造函数使用了add**的函数添加自定义布局 在这里配置你布局展示的数据
*使用**2结尾的函数可链式调用 点击事件做了单独封装
*/
@Override
public void convert(@NotNull BaseRemoteViews mBaseRemoteViews, @NotNull ChatData data) {
ContentRemote contentRemote = mBaseRemoteViews.getContentRemote();
if (contentRemote != null) {
contentRemote
.setImageViewResource2(R.id.notify_chat_head_img, data.getIcon())
.setTextViewText2(R.id.notify_chat_title, data.getContentTitle())
.setTextViewText2(R.id.notify_chat_subtitle, data.getContentText())
.setOnClickPendingIntent2(getNotificationId(), R.id.notify_chat_layout);
}

}
/**
*设置你的通知id
*/
@Override
public int getNotificationId() {
return NotificationConfig.NOTIFICATION_ID;
}

}


2.实例化你的通知并展示

ChatChannelNotify chat=new ChatChannelNotify(new Chat())
chat.show()//显示我们的通知
//更新内容使用 chat.show(new Chat())
//取消通知 chat.cancel()


3.设置点击事件

class ChatActivity : AppCompatActivity(), PendingIntentListener {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//设置点击事件
NotificationControl.addPendingIntentListener(this)
}

/**
*通知点击事件回调
*notifId通知的id
*ViewId点击的viewId
*/
override fun onClick(notifyId: Int, viewId: Int) {
Toast.makeText(this,
"点击事件: notifyId:" + notifyId + " ViewId:" + viewId, Toast.LENGTH_LONG)
.show()
}

override fun onDestroy() {
super.onDestroy()
//销毁点击事件
NotificationControl.removePendingIntentListener(this)
}
}

原文链接:https://github.com/Dboy233/BaseNotification

代码下载:BaseNotification-master.zip



收起阅读 »

Android增量更新

APP自动增量更新抽取的Android自动更新库,目的是几行代码引入更新功能,含服务端代码,欢迎Star,欢迎Fork,谢谢~目录功能介绍流程图效果图与示例apk如何引入更新清单文件简单使用详细说明差分包生成(服务端)依赖License功能介绍 支持...
继续阅读 »

APP自动增量更新

抽取的Android自动更新库,目的是几行代码引入更新功能,含服务端代码,欢迎Star,欢迎Fork,谢谢~

目录

功能介绍

  •  支持全量更新apk,直接升级到最新版本
  •  支持增量更新,只下载补丁包升级
  •  设置仅在wifi环境下更新
  •  支持外部注入网络框架(库默认使用okhttp)
  •  支持前台或后台自动更新
  •  支持基于版本的强制更新
  •  支持对外定制更新提示和更新进度界面
  •  含发布功能后台服务端github (Node.js实现)

流程图

效果图与示例apk

示例1 示例2


如何引入

Gradle引入

step 1

Add the JitPack repository to your build file

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.itlwy:AppSmartUpdate:v1.0.7'
}


Step 2

Add the dependency

更新清单文件

该清单放置在静态服务器以供App访问,主要用于判断最新的版本,及要更新的版本资源信息等(示例见仓库根目录下的resources目录或直接访问后台代码 github),清单由服务端程序发布apk时生成,详见后台示例:github

{
"minVersion": 100, // app最低支持的版本代码(包含),低于此数值的app将强制更新
"minAllowPatchVersion": 100, // 最低支持的差分版本(包含),低于此数值的app将采取全量更新,否则采用差量
"newVersion": 101, // 当前最新版本代码
"tip": "test update", // 更新提示
"size": 1956631, // 最新apk文件大小
"apkURL": "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/app/smart-update.apk", // 最新apk 绝对url地址,也可用相对地址,如下方的"patchURL"字段
"hash": "ea97c8efa490a2eaf7d10b37e63dab0e", // 最新apk文件的md5值
"patchInfo": { // 差分包信息
"v100": { // v100表示-版本代码100的apk需要下载的差分包
"patchURL": "v100/100to101.patch", //差分包地址,相对此UpdateManifest.json文件的地址,也可用绝对地址
"tip": "101 version", // 提示
"hash": "ea97c8efa490a2eaf7d10b37e63dab0e", // 合成后apk(即版本代码101)的文件md5值
"size": 1114810 // 差分包大小
}
}
}


简单使用

1.初始化

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
//推荐在Application中初始化
Config config = new Config.Builder()
.isDebug(true)
.build(this);
UpdateManager.getInstance().init(config);
}
}


2.调用

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mUpdateBtn;
private String manifestJsonUrl = "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/UpdateManifest.json";
private IUpdateCallback mCallback;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUpdateBtn = (Button) findViewById(R.id.update_btn);
mUpdateBtn.setOnClickListener(this);

}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.update_btn:
UpdateManager.getInstance().update(this, manifestJsonUrl, null);
break;

}
}
}


详细说明

注册通知回调

  • 其他activity界面需要获知后台更新情况
public void register(IUpdateCallback callback) {...}

public void unRegister(IUpdateCallback callback) {...}

public interface IUpdateCallback {

/**
* 通知无新版本需要更新,运行在主线程
*/
void noNewApp();

/**
* 自动更新准备开始时回调,运行在主线程,可做一些提示等
*/
void beforeUpdate();

/**
* 自动更新的进度回调(分增量和全量更新),运行在主线程
*
* @param percent 当前总进度百分比
* @param totalLength 更新总大小(全量为apk大小,增量为全部补丁大小和)
* @param patchIndex 当前更新的补丁索引(从1开始)
* @param patchCount 需要更新的总补丁数(当为0时表示是增量更新)
*/
void onProgress(int percent, long totalLength, int patchIndex, int patchCount);

/**
* 下载完成,准备更新,运行在主线程
*/
void onCompleted();

/**
* 异常回调,运行在主线程
*
* @param error 异常信息
*/
void onError(String error);

/**
* 用户取消了询问更新对话框
*/
void onCancelUpdate();

/**
* 取消了更新进度对话框,压入后台自动更新,此时由通知栏通知进度
*/
void onBackgroundTrigger();
}


网络框架注入

默认使用okhttp,也可由外部注入,只需实现如下的IHttpManager接口,然后通过new Config.Builder().httpManager(new OkhttpManager())注入即可

public interface IHttpManager {


IResponse syncGet(@NonNull String url, @NonNull Map<String, String> params) throws IOException;

/**
* 异步get
*
* @param url get请求地址
* @param params get参数
* @param callBack 回调
*/
void asyncGet(@NonNull String url, @NonNull Map<String, String> params, @NonNull Callback callBack);


/**
* 异步post
*
* @param url post请求地址
* @param params post请求参数
* @param callBack 回调
*/
void asyncPost(@NonNull String url, @NonNull Map<String, String> params, @NonNull Callback callBack);

/**
* 下载
*
* @param url 下载地址
* @param path 文件保存路径
* @param fileName 文件名称
* @param callback 回调
*/
void download(@NonNull String url, @NonNull String path, @NonNull String fileName, @NonNull FileCallback callback);
}


定制更新交互界面

每个应用的风格都可能是不一样的,因此这里也支持自定义弹出的提示框和进度框,详细见如下代码示例:

  1. 初始化config时需要将内部默认的弹框屏蔽掉

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
Config config = new Config.Builder()
.isShowInternalDialog(false)
.build(this);
UpdateManager.getInstance().init(config);
}
}


  1. 自定义对话框,如下(详细代码在MainActivity.java里):

public void registerUpdateCallbak() {
mCallback = new IUpdateCallback() {
@Override
public void noNewApp() {
Toast.makeText(MainActivity.this, "当前已是最新版本!", Toast.LENGTH_LONG).show();
}

@Override
public void hasNewApp(AppUpdateModel appUpdateModel, UpdateManager updateManager, final int updateMethod) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
mDialog = builder.setTitle("自动更新提示")
.setMessage(appUpdateModel.getTip())
.setPositiveButton("更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
UpdateManager.getInstance().startUpdate(updateMethod);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
}).create();
mDialog.show();
}

@Override
public void beforeUpdate() {
// 更新开始
mProgressDialog = new ProgressDialog(MainActivity.this);
mProgressDialog.setTitle("更新中...");
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setMessage("正在玩命更新中...");
mProgressDialog.setMax(100);
mProgressDialog.setProgress(0);
mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// 退到后台自动更新,进度由通知栏显示
if (UpdateManager.getInstance().isRunning()) {
UpdateManager.getInstance().onBackgroundTrigger();
}
}
});
mProgressDialog.show();
}

@Override
public void onProgress(int percent, long totalLength, int patchIndex, int patchCount) {
String tip;
if (patchCount > 0) {
tip = String.format("正在下载补丁%d/%d", patchIndex, patchCount);
} else {
tip = "正在下载更新中...";
}
mProgressDialog.setProgress(percent);
mProgressDialog.setMessage(tip);
}

@Override
public void onCompleted() {
mProgressDialog.dismiss();
}

@Override
public void onError(String error) {
Toast.makeText(MainActivity.this, error, Toast.LENGTH_LONG).show();
mProgressDialog.dismiss();
}

@Override
public void onCancelUpdate() {

}

@Override
public void onBackgroundTrigger() {
Toast.makeText(MainActivity.this, "转为后台更新,进度由通知栏提示!", Toast.LENGTH_LONG).show();
}
};
UpdateManager.getInstance().register(mCallback);
}


差分包合成(jni)

此部分采用的差分工具为开源bsdiff,用于生成.patch补丁文件,采用jni方式封装一个.so库供java调用,详见"smartupdate"库里的main/cpp目录源码,过程比较简单,就是写个jni的方法来直接调用bsdiff库,目录结构如下:

main
    -cpp
        -bzip2
-CMakeLists.txt
-patchUtils.c
-patchUtils.h
-update-lib.cpp

因为bsdiff还依赖了bzip2,所以这里涉及多个源文件编译链接问题,需要在CMakeLists.txt稍作修改:

# 将当前 "./src/main/cpp" 目录下的所有源文件保存到 "NATIVE_SRC" 中,然后在 add_library 方法调用。
aux_source_directory( . NATIVE_SRC )
# 将 "./src/main/cpp/bzip2" 目录下的子目录bzip2保存到 "BZIP2_BASE" 中,然后在 add_library 方法调用。
aux_source_directory( ./bzip2 BZIP2_BASE )
# 将 BZIP2_BASE 增加到 NATIVE_SRC 中,这样目录的源文件也加入了编译列表中,当然也可以不加到 NATIVE_SRC,直接调用add_library。
list(APPEND NATIVE_SRC ${BZIP2_BASE})

add_library( # Sets the name of the library.
update-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${NATIVE_SRC})


差分包生成

服务端见github ,使用时将manifestJsonUrl改成部署的服务器地址即可,如下示例代码片段的注释处

public class MainActivity extends AppCompatActivity {
private String manifestJsonUrl = "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/UpdateManifest.json";
// private String manifestJsonUrl = "http://192.168.2.107:8000/app/UpdateManifest.json";
...
}


依赖

  • okhttp : com.squareup.okhttp3:okhttp:3.11.0
  • gson : com.google.code.gson:gson:2.8.0
收起阅读 »

OpenCV二维码扫码优化

说明在介绍二维码的优化前,可以参考二维码基础原理了解二维码识别的相关知识。 作者相关博客Android二维码扫描优化Contents 目录概述普通优化解码优化优化相机设置难点灰色角度光照优化尝试1尝试2尝试3项目说明概述随着二维码的流行,几乎所有手持设备都支持...
继续阅读 »

说明

在介绍二维码的优化前,可以参考二维码基础原理了解二维码识别的相关知识。 作者相关博客Android二维码扫描优化

Contents 目录

概述

随着二维码的流行,几乎所有手持设备都支持二维码的扫描识别。对于大部分普通的情景下,使用zxing和zbar就可以很好的完成二维码的识别,但是如果碰到一些特殊的情景,zxing和zbar的识别率并不高。这些特殊的情景包括:

  1. 二维码是灰色的
  2. 大角度斜扫二维码
  3. 由光源引发的摄像头干扰,比如手机扫描屏幕上的二维码会出现条纹或者噪点

普通优化

在介绍难点优化之前,先介绍网上常见的优化点。这些优化点同样被使用在了作者的项目中,具体包括:

解码优化

  1. 减少解码格式
  2. 解码算法优化
  3. 减少解码数据
  4. 解码库Zbar与Zxing融合

优化相机设置

  1. 选择最佳预览尺寸/图片尺寸
  2. 设置适合的相机放大倍数
  3. 调整聚焦时间
  4. 设置自动对焦区域
  5. 调整合理扫描区域

以上普通优化均参考自智能设备上的二维码解码优化, 不过作者在实现过程中,对该链接所对应的项目实现做了一些修改,修改点包括:

  1. 旋转android后置摄像头preview数据时,根据yuv NV21数据的特点进行旋转
  2. 摄像头预览数据中,对应预览框的数据的截取,并非通过PlanarYUVLuminanceSource创建时提供的rect进行截取。而是通过opencv,将yuv数据转换成rgb之后,在进行截取
  3. 摄像头最佳预览尺寸选取时,按照设备屏宽,屏高最接近的方式选取,并没有通过屏幕宽高比进行筛选。因为部分android手机,屏幕的宽高比和摄像头所支持最接近的size的宽高比并不一致(ps:比如nexus 4,屏幕宽高为720/1280, 但是摄像头支持的最接近屏幕宽高比的preview size,都要少几个像素,印象中,好像是 716/1276, 具体多少记不清了)
  4. 支持用户双指对摄像头进行缩放,并未采用链接引用项目的zoom策略
  5. 添加zbar结合zxing一起检测的代码

难点

整个二维码扫描识别过程中,个人认为最关键的一步在于图片的二值化处理。所谓二值化,可以简单理解为给定一个阈值,低于阈值的像素标记为白色,高于阈值的像素标记为黑色。一旦进行二值化后,图片形成黑白两色图,再利用二维码原理进行定位查询并识别解码并不困难。因此能否对一个图片进行正确的二值化极大影响二维码识别率。而以下特殊情况很大程度上会干扰扫描过程中二值化过程:

灰色二维码

如上图,某些二维码是灰色的,或者由于光照的情况下色调比较淡。灰色二维码识别难度在于背景往往比二维码的灰点色度更黑,当用手机对这类灰色二维码进行扫描时,总是或多或少会有一些非二维码的背景显示在扫描框中,而这部分背景由于色调比灰点深,这会造成zxing,或者zbar二值化过程中选择阈值受到干扰,从而无法识别二维码

大角度对二维码进行扫描识别

在这种情况下如果是纯黑色的二维码影响不会太大,但如果是灰色的二维码,就非常容易受到光照的印象。 由于是斜着扫二维码,那么对于摄像头来说,二维码的两边所带来的亮度是不同的。

 

如上图例子,如果是从左侧进行扫描二维码,那么左边由于距离摄像头更近,因此更亮,而右边由于距离摄像头更远,会比较暗,此时整个预览框进行二值化时,非常容易造成右边暗的部分,全部超过阈值而被判断为黑色,进而极大影响二维码的识别

由光源引发的摄像头干扰,比如手机扫描屏幕上的二维码会出现条纹或者噪点

如果二维码位于电脑屏幕中,或者位于其他投影设备里,或者当时处于白炽灯等灯光照耀的情况下,进行扫描,摄像头有时候会受到光源频率的干扰,从而形成条纹状。而某些情况下,当摄像头zoom到一定程度是,形成的早点也会对二维码识别造成极大干扰,如下图所示作者遇到的情况:

 

这类情况,如果二维码是黑色的还好,如果二维码是灰色的,则条纹会对二维码的识别起到很大的干扰,如上边右侧图像所示

优化

针对上述难点,作者尝试通过opencv对相机的preview数据进行预处理,从而提升二维码的扫描识别,作者进行过的尝试如下:

尝试1:通过opencv方法进行预览数据的降噪

作者本想通过opencv的各种降噪滤波算法(例如mediaBlur等),去光照算法(例如RGB归一化等)等对camera预览数据进行处理。可能是作者对于opencv算法理解还不足,也可能是其他原因,测试下来效果都不太好。最好作者选择mediaBlur作为降噪算法进行保留,对于一些椒盐噪点的去除还挺有帮助

尝试2:缩小二值化阈值计算范围

考虑到灰色二维码之所以难以识别是因为背景的色调过暗,拉低了二值化阈值的计算,导致整个二维码的难以识别。因此作者将二值化阈值的计算区域缩小到预览框的2/5,然后再通过计算的阈值对整个预览区域的数据进行二值化。这个效果对于灰色二维码的识别提升很大。可惜的是,只对正面扫描的情况下有用,一旦斜着超过一定角度扫描灰色二维码,往往识别不出。

尝试3:分块对预览区进行二值化

考虑到斜扫二维码不好识别因为矩形预览区域各个部分的亮度不统一,因此作者下一步尝试是,将矩形预览框再次分成4个块,每个块取其靠近中心部分的1/5区域进行阈值计算,最后分块进行二值化后,拼接而成。具体如下示意图所示

之所以分块进行计算阈值,是考虑到不同区域的亮度是不同的,如果整体进行计算的话,会丢失部分有效信息。而之所以选取靠近中心的小部分进行阈值化计算,是因为用户行为通常会将二维码对准预览框的中心,因此中心部分,包含有效亮度信息更为精确,减少背景亮度对整体二值化的影响。这一步尝试之后,大大提升了特殊情景二维码的识别概率。至此,opencv预处理部分到此结束了。

项目说明

项目分成3个module

  • app app module提供demo activity

  • library_qrscan 二维码扫码核心功能,集成zxing,zbar,opencv预处理等功能

  • library_opencv 该module作为隐藏module,setting配置并没有include它,是方便调试opencv功能。该module可以用于生成libProcessing.so,集成在library_qrscan module中。如果用户想使用该module,请按照以下做法:

    1. 打开setting中配置,include该module
    2. 下载opencv for android版本到本地目录,并解压
    3. 修改library_opencv/src/main/jni/Android.mk 中 12行include对应本地opencv的mk路径
    4. 删除library_qrscan/libs/armeabi-v7a/libProcessing.so


收起阅读 »

图片选择器:Matisse

Matisse 是一个为Android精心设计的本地图像和视频选择器。你可以 在活动或片段中使用它 选择包含JPEG、PNG、GIF的图像和包含MPEG、MP4的视频 应用不同的主题,包括两个内置主题和自定义主题 不同的图像加载器 定义自定义筛选规则 ...
继续阅读 »

Matisse 是一个为Android精心设计的本地图像和视频选择器。你可以



  • 在活动或片段中使用它


  • 选择包含JPEG、PNG、GIF的图像和包含MPEG、MP4的视频


  • 应用不同的主题,包括两个内置主题和自定义主题


  • 不同的图像加载器


  • 定义自定义筛选规则















Zhihu Style Dracula Style Preview




下载使用

Gradle:


repositories {
jcenter()
}

dependencies {
implementation 'com.zhihu.android:matisse:$latest_version'
}

混淆

如果用的是 Glide
添加规则:

-dontwarn com.squareup.picasso.**

如果用的是 Picasso 添加规则:


-dontwarn com.bumptech.glide.**

怎样使用他呢?

权限


  • android.permission.READ_EXTERNAL_STORAGE
  • android.permission.WRITE_EXTERNAL_STORAGE

所以,如果您的目标是android6.0+,那么您需要在下一步之前处理运行时权限请求。


Simple usage snippet



启动 MatisseActivity 从 活动的 Activity or Fragment:


Matisse.from(MainActivity.this)
.choose(MimeType.allOf())
.countable(true)
.maxSelectable(9)
.addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
.gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
.thumbnailScale(0.85f)
.imageEngine(new GlideEngine())
.showPreview(false) // Default is `true`
.forResult(REQUEST_CODE_CHOOSE);

主题

有两个内置的主题可以用来开始 MatisseActivity:



  • R.style.Matisse_Zhihu (白天模式)
  • R.style.Matisse_Dracula (夜间模式)

你也可以随心所欲地定义自己的主题。


接收结果

onActivityResult() 回调 Activity or Fragment:


List<Uri> mSelected;

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHOOSE && resultCode == RESULT_OK) {
mSelected = Matisse.obtainResult(data);
Log.d("Matisse", "mSelected: " + mSelected);
}
}

github地址:https://github.com/zhihu/matisse
下载地址:matisse-master.zip


收起阅读 »

秀!秀!秀!优秀的富文本库:XRichText

一个Android富文本类库,支持图文混排,支持编辑和预览,支持插入和删除图片。 实现的原理: 使用ScrollView作为最外层布局包含LineaLayout,里面填充TextView和ImageView。删除的时候,根据光标的位置,删除TextView和I...
继续阅读 »

一个Android富文本类库,支持图文混排,支持编辑和预览,支持插入和删除图片。


实现的原理:


  • 使用ScrollView作为最外层布局包含LineaLayout,里面填充TextView和ImageView。
  • 删除的时候,根据光标的位置,删除TextView和ImageView,文本自动合并。
  • 生成的数据为list集合,可自定义处理数据格式。

注意事项


  • V1.4版本开放了图片点击事件接口和删除图片接口,具体使用方式可以参考后面的文档说明,也可以参考Demo实现。
  • V1.6版本升级RxJava到2.2.3版本,RxAndroid到2.1.0版本。设置字体大小时需要带着单位,如app:rt_editor_text_size=”16sp”。
  • V1.9.3及后续版本,xrichtext库中已去掉Glide依赖,开放接口可以自定义图片加载器。具体使用方式可以参考后面的文档说明,也可以参考Demo实现。
  • Demo中图片选择器为知乎开源库Matisse,适配Android 7.0系统使用FileProvider获取图片路径。
  • 开发环境更新为 AS 3.4.2 + Gradle 4.4 + compileSDK 28 + support library 28.0.0,导入项目报版本错误时,请手动修改为自己的版本。
  • 请参考Demo的实现,进行了解本库。可以使用Gradle引入,也可以下载源码进行修改。
  • 如有问题,欢迎提出。欢迎加入QQ群交流:745215148。

截图预览

笔记列表
文字笔记详情
编辑笔记
图片笔记详情

使用方式

1. 作为module导入

把xrichtext作为一个module导入你的工程。


2. gradle依赖

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.sendtion:XRichText:1.9.4'
}

如果出现support版本不一致问题,请排除XRichText中的support库,或者升级自己的support库为28.0.0版本。
使用方式:

implementation ('com.github.sendtion:XRichText:1.9.4') {
exclude group: 'com.android.support'
}

具体使用

在xml布局中添加基于EditText编辑器(可编辑)


<com.sendtion.xrichtext.RichTextEditor
android:id="@+id/et_new_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rt_editor_text_line_space="6dp"
app:rt_editor_image_height="500"
app:rt_editor_image_bottom="10"
app:rt_editor_text_init_hint="在这里输入内容"
app:rt_editor_text_size="16sp"
app:rt_editor_text_color="@color/grey_900"/>

在xml布局中添加基于TextView编辑器(不可编辑)


<com.sendtion.xrichtext.RichTextView
android:id="@+id/tv_note_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rt_view_text_line_space="6dp"
app:rt_view_image_height="0"
app:rt_view_image_bottom="10"
app:rt_view_text_size="16sp"
app:rt_view_text_color="@color/grey_900"/>

自定义属性

具体参考Demo



  • RichTextView


    rt_view_image_height        图片高度,默认为0自适应,可以设置为固定数值,如500、800等
    rt_view_image_bottom 上下两张图片的间隔,默认10
    rt_view_text_size 文字大小,使用sp单位,如16sp
    rt_view_text_color 文字颜色,使用color资源文件
    rt_view_text_line_space 字体行距,跟TextView使用一样,比如6dp
  • RichTextEditor


    rt_editor_image_height      图片高度,默认为500,可以设置为固定数值,如500、800等,0为自适应高度
    rt_editor_image_bottom 上下两张图片的间隔,默认10
    rt_editor_text_init_hint 默认提示文字,默认为“请输入内容”
    rt_editor_text_size 文字大小,使用sp单位,如16sp
    rt_editor_text_color 文字颜色,使用color资源文件
    rt_editor_text_line_space 字体行距,跟TextView使用一样,比如6dp

生成数据

我把数据保存为了html格式,生成字符串存储到了数据库。


String noteContent = getEditData();

private String getEditData() {
List<RichTextEditor.EditData> editList = et_new_content.buildEditData();
StringBuffer content = new StringBuffer();
for (RichTextEditor.EditData itemData : editList) {
if (itemData.inputStr != null) {
content.append(itemData.inputStr);
} else if (itemData.imagePath != null) {
content.append("<img src=\"").append(itemData.imagePath).append("\"/>");
}
}
return content.toString();
}

显示数据

et_new_content.post(new Runnable() {
@Override
public void run() {
showEditData(content);
}
});

protected void showEditData(String content) {
et_new_content.clearAllLayout();
List<String> textList = StringUtils.cutStringByImgTag(content);
for (int i = 0; i < textList.size(); i++) {
String text = textList.get(i);
if (text.contains("<img")) {
String imagePath = StringUtils.getImgSrc(text);
int width = ScreenUtils.getScreenWidth(this);
int height = ScreenUtils.getScreenHeight(this);
et_new_content.measure(0,0);
Bitmap bitmap = ImageUtils.getSmallBitmap(imagePath, width, height);
if (bitmap != null){
et_new_content.addImageViewAtIndex(et_new_content.getLastIndex(), bitmap, imagePath);
} else {
et_new_content.addEditTextAtIndex(et_new_content.getLastIndex(), text);
}
et_new_content.addEditTextAtIndex(et_new_content.getLastIndex(), text);
}
}
}

图片点击事件

tv_note_content.setOnRtImageClickListener(new RichTextView.OnRtImageClickListener() {
@Override
public void onRtImageClick(String imagePath) {
ArrayList<String> imageList = StringUtils.getTextFromHtml(myContent, true);
int currentPosition = imageList.indexOf(imagePath);
showToast("点击图片:"+currentPosition+":"+imagePath);
// TODO 点击图片预览
}
});

图片加载器使用

请在Application中设置,经测试在首页初始化会出现问题。Demo仅供参考,具体实现根据您使用的图片加载器而变化。


XRichText.getInstance().setImageLoader(new IImageLoader() {
@Override
public void loadImage(String imagePath, ImageView imageView, int imageHeight) {
//如果是网络图片
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")){
Glide.with(getApplicationContext()).asBitmap().load(imagePath).dontAnimate()
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
if (imageHeight > 0) {//固定高度
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, imageHeight);//固定图片高度,记得设置裁剪剧中
lp.bottomMargin = 10;//图片的底边距
imageView.setLayoutParams(lp);
Glide.with(getApplicationContext()).asBitmap().load(imagePath).centerCrop()
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(imageView);
} else {//自适应高度
Glide.with(getApplicationContext()).asBitmap().load(imagePath)
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(new TransformationScale(imageView));
}
}
});
} else { //如果是本地图片
if (imageHeight > 0) {//固定高度
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, imageHeight);//固定图片高度,记得设置裁剪剧中
lp.bottomMargin = 10;//图片的底边距
imageView.setLayoutParams(lp);

Glide.with(getApplicationContext()).asBitmap().load(imagePath).centerCrop()
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(imageView);
} else {//自适应高度
Glide.with(getApplicationContext()).asBitmap().load(imagePath)
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(new TransformationScale(imageView));
}
}
}
});

GitHub地址:https://github.com/sendtion/XRichText
下载地址:XRichText-master.zip


收起阅读 »

FBKVOController - 面试聊到KVO如何有效的怒怼面试官!

1.系统KVO的问题2.FBKVOController优点3.FBKVOController的架构设计图4.FBKVOController源码详读5.FBKVOController总结一.系统KVO的问题当观察者被销毁之前,需要手动移除观察者,否则会出现程序异...
继续阅读 »
  • 1.系统KVO的问题
  • 2.FBKVOController优点
  • 3.FBKVOController的架构设计图
  • 4.FBKVOController源码详读
  • 5.FBKVOController总结

一.系统KVO的问题

  • 当观察者被销毁之前,需要手动移除观察者,否则会出现程序异常(向已经销毁的对象发送消息);
  • 可能会对同一个被监听的属性多次添加监听,这样我们会接收到多次监听的回调结果;
  • 当观察者对多个对象的不同属性进行监听,处理监听结果时,需要在监听回调的方法中,作出大量的if判断;
  • 当对同一个被监听的属性进行两次removeObserver时,会导致程序crash。这种情况通常出现在父类中有一个KVO,在父类的dealloc中remove一次,而在子类中再次remove。

二. FBKVOController优点

  • 可以同时对一个对象的多个属性进行监听,写法简洁;
  • 通知不会向已释放的观察者发送消息;
  • 增加了block和自定义操作对NSKeyValueObserving回调的处理支持;
  • 不需要在dealloc 方法中手动移除观察者,而且移除观察者不会抛出异常,当FBKVOController对象被释放时, 观察者被隐式移除;

三.FBKVOController架构设计图






四.FBKVOController源码详解

FBKVOController源码详解分四部分:

  • 私有类_FBKVOInfo,
  • 私有类_FBKVOSharedController
  • FBKVOController,
  • NSObject+FBKVOController的源码解读:
(一)FBKVOController

首先我们创建一个FBKVOController的实例对象时,有以下三种方法,一个类方法和两个对象方法,

//该方法是一个全能初始化的对象方法,其他初始化方法内部均调用该方法
//参数:observer是观察者,retainObserved:表示是否强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer;

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
+ (instancetype)controllerWithObserver:(nullable id)observer;
NS_DESIGNATED_INITIALIZER;

我们先来看全能初始化方法内部的实现,

  • 该方法对三个实例变量_observer(观察者)
  • _objectInfosMap(NSMapTable,被监听对象->被监听属性集合之间的映射关系)
  • pthread_mutex_init(互斥锁)

//全能初始化方法
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {

//观察者
_observer = observer;

//NSMapTable中的key可以为对象,而且可以对其中的key和value弱引用
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];

//对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER
//对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy
pthread_mutex_init(&_lock, NULL);
}
return self;
}

这里请先思考以下问题:

  • 属性observer为何使用weak,它和哪个对象之间会导致循环引用问题,是如何导致循环引用问题的?
  • 为何不使用字典来保存被监听对象和被监听属性集合之间的关系?
  • NSDictionary的局限性有哪些?NSMapTable相对字典,有哪些优点?
  • 互斥锁是为了保证哪些数据的线程安全?

带着这些问题我们来看FBKVOController内部是如何实现监听的,这里我们只看带Block回调的一个监听方法,其他几个方法和这个方法内部实现是相同的。下面的方法内部做了如下工作:

  • 1.传入的参数keyPath,block为空时,程序闪退,同时报出误提示;
  • 2.对传入参数为空的判读;
  • 3.利用传入的参数创建_FBKVOInfo对象;
  • 4.调用内部私有方法实现注册监听;
//观察者监听object中健值路径(keyPath)所对应属性的变化
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//NSAssert是一个预处理宏, 它可以让开发者比较便捷的捕获错误, 让程序闪退, 同时报出错误提示
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);

//首先判断被监听的对象是否为空,被监听的健值路径是否为空,回调的block是否为空
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}

// 根据传进来的参数创建_FBKVOInfo对象,将这些参数封装到_FBKVOInfo对象中
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

// 监听对象object的属性信息(_FBKVOInfo对象)
[self _observe:object info:info];
}

该私有方法内部并没有实现真正的注册监听,这里使用NSMapTable保存了被监听对象object-> _FBKVOInfo对象集合的关系,具体的监听是在_FBKVOSharedController类中实现的。观察者可以监听多个对象,而每个对象中可能有多个属性被监听


内部实现思路:

  • 对当前线程访问的数据_objectInfosMap进行加锁;
  • 根据被监听对象object到_objectInfosMap取出被监听的属性信息对象集合infos;
  • 判断被监听的属性对象info是否存在集合中;
  • 如果已经存在,则不需要再次添加监听,防止多次监听;
  • 如果获取的集合infos为空,则建存放_FBKVOInfo对象的集合infos,保存映射关系:object->infos;
  • 将被监听的信息_FBKVOInfo对象存到集合infos中;
  • 解锁,其他线程可以访问该数据;
  • 调用_FBKVOSharedController 的方法实现监听;
//该方法是内部私有方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
//先加锁,访问_objectInfosMap
pthread_mutex_lock(&_lock);

//到_objectInfosMap中根据key(被监听的对象)获取被监听的属性信息集合
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

//判断infos集合中是否存在被监听属性信息对象info
_FBKVOInfo *existingInfo = [infos member:info];

//被监听对象的属性已经存在,不需要再次监听,防止多次添加监听
if (nil != existingInfo) {

//解锁,其他线程可以再次访问_objectInfosMap中的数据
pthread_mutex_unlock(&_lock);
return;
}

//根据被监听对象在_objectInfosMap获取的被监听属性信息的集合为空
if (nil == infos) {
//懒加载创建存放_FBKVOInfo对象的set集合infos
infos = [NSMutableSet set];

//保存被监听对象和被监听属性信息的映射关系object->infos
[_objectInfosMap setObject:infos forKey:object];
}

// 将被监听的信息_FBKVOInfo对象存到集合infos中
[infos addObject:info];

//解锁
pthread_mutex_unlock(&_lock);

//最终的监听方法是通过_FBKVOSharedController中的方法来实现
//_FBKVOSharedController内部实现系统KVO方法
[[_FBKVOSharedController sharedController] observe:object info:info];
}
(二)_FBKVOInfo

_FBKVOInfo私有类的内部很简单,没有任何业务逻辑,只是一个简单的Model,主要是将以下的实例变量封装到对象中,方便访问:

{
@public
//weak,防止循环引用
__weak FBKVOController *_controller;
//被监听属性的健值路径
NSString *_keyPath;

//NSKeyValueObservingOptionNew:观察修改前的值
// NSKeyValueObservingOptionOld:观察修改后的值
//NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
//NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(一次修改有两次触发)
NSKeyValueObservingOptions _options;

//被监听属性值变化时的回调方法
SEL _action;

//上下文信息(void * 任何类型)
void *_context;
//被监听属性值变化时的回调block
FBKVONotificationBlock _block;
//监听状态
_FBKVOInfoState _state;
}

_FBKVOInfo私有类提供了一个全能初始化方法,来初始化以上实例变量。其他几个部分初始化方法内部均调用该全能初始化方法。

//全能初始化方法
- (instancetype)initWithController:(FBKVOController *)controller
keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
block:(nullable FBKVONotificationBlock)block
action:(nullable SEL)action
context:(nullable void *)context
{
self = [super init];
if (nil != self) {
_controller = controller;
_block = [block copy];
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}


优化判断对象相等性的效率:
  • 1.首先判断hash值是否相等,若相等则进行第2步;若不等,则直接判断不等;hash值是对象判等的必要非充分条件;(即没它一定不行,有它不一定行)
  • 2.在hash值相等的情况下,再进行对象判等, 作为判等的结果
//当重写hash方法时,我们可以将关键属性的hash值进行位或运算来作为hash值
- (NSUInteger)hash
{
return [_keyPath hash];
}

/**
对于基本类型, ==运算符比较的是值;
对于对象类型, ==运算符比较的是对象的地址(即是否为同一对象)
*/

- (BOOL)isEqual:(id)object
{
//判断对象是否为空,若为空,则不相等
if (nil == object) {
return NO;
}

//判断对象的地址是否相等,若相等,则为同一个对象(即是否为同一个对象)
if (self == object) {
return YES;
}

//判断是否是同一类型,这样可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险
if (![object isKindOfClass:[self class]]) {
return NO;
}

//对各个属性分别使用默认判等方法进行判断
//返回所有属性判等的与结果
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

//输出对象的调试信息

//description: 使用NSLog从控制台输出对象的信息
//debugDescription:通过断点po打印输出对象的信息
- (NSString *)debugDescription
{
NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
if (0 != _options) {
[s appendFormat:@" options:%@", describe_options(_options)];
}
if (NULL != _action) {
[s appendFormat:@" action:%@", NSStringFromSelector(_action)];
}
if (NULL != _context) {
[s appendFormat:@" context:%p", _context];
}
if (NULL != _block) {
[s appendFormat:@" block:%p", _block];
}
[s appendString:@">"];
return s;
}

  • 请分析如果将实例变量__weak FBKVOController *_controller前的 __weak去掉,它和_FBKVOInfo对象之间的循环引用环是如何形成的?
(三)_FBKVOSharedController

_FBKVOSharedController私有类内部实现了系统KVO的方法,用来接收和转发KVO的通知。接口中提供了监听和移除监听的方法。其接口如下:


@interface _FBKVOSharedController : NSObject

// 单例初始化方法
+ (instancetype)sharedController;

// 监听object的属性
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info;

//移除对object中属性的监听
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;

// 移除对object中多个属性的监听
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;

@end
_FBKVOSharedController私有类内部有两个私有成员变量,_infos是用来存放_FBKVOInfo对象,_infos可以对其中的成员变量弱引用,这也是为何使用NSHashTable,而不使用NSSet来存放_FBKVOInfo对象的原因。_mutex是互斥锁:

{
//存放被监听属性的信息对象
NSHashTable<_FBKVOInfo *> *_infos;
//互斥锁
pthread_mutex_t _mutex;
}

_FBKVOSharedController私有类的初始化方法,支持iOS 系统和Mac系统,初始化实例变量_infos,指定了_infos对存放在其中的成员变量弱引用,及判等性方式:

//提供全局的单例初始化方法,该单例对象的生命周期与程序的生命周期相同
+ (instancetype)sharedController
{
static _FBKVOSharedController *_controller = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_controller = [[_FBKVOSharedController alloc] init];
});
return _controller;
}
//初始化成员变量_infos和_mutex
- (instancetype)init
{
self = [super init];
if (nil != self) {
//初始化实例变量
NSHashTable *infos = [NSHashTable alloc];

// iOS 系统下:hashTable中的对象是弱引用,对象的判等方式:位移指针的hash值和直接判等
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

//MAC系统下
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}

#endif
//初始化互斥锁
pthread_mutex_init(&_mutex, NULL);
}
return self;
}

- (void)dealloc
{
//对象被销毁时,销毁互斥锁
pthread_mutex_destroy(&_mutex);
}
_FBKVOSharedController在这个方法中,调用系统KVO方法,将自己注册为观察者,思路如下:
1.首先将被监听的信息对象_FBKVOInfo保存到_infos中;
2.然后调用系统KVO方法将自己注册为被监听对象object的观察者;
3.最后修改监听的状态;当不再监听时,安全移除观察者;

//添加监听
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
//被监听的属性信息_FBKVOInfo对象为空时,直接返回
if (nil == info) {
return;
}

// 加锁,防止多线程访问时,出现数据竞争
pthread_mutex_lock(&_mutex);

// 将被监听的属性信息info对象添加到_infos中,_infos对成员变量info是弱引用
[_infos addObject:info];

//添加完成之后,解锁,其他线程可以访问
pthread_mutex_unlock(&_mutex);

// 添加监听
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

//修改监听状态
if (info->_state == _FBKVOInfoStateInitial) {

info->_state = _FBKVOInfoStateObserving;

} else if (info->_state == _FBKVOInfoStateNotObserving) {

//不再监听时安全移除观察者
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
实现系统KVO监听回调的方法

//被监听属性更改时的回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

_FBKVOInfo *info;
{
pthread_mutex_lock(&_mutex);
//确定_infos是否包含给定的对象context,若存在返回该对象,否则返回nil;
//所使用的相等性比较取决于所选择的选项
//例如,使用NSPointerFunctionsObjectPersonality选项将使用isEqual:方法来判断相等。
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}

//通过上下文参数context传过来的被监听的_FBKVOInfo对象,已经存在_infos中
if (nil != info) {

//_FBKVOSharedController对象强引用FBKVOController对象,防止被提前释放
//因为在_FBKVOInfo中,对FBKVOController对象是弱引用
FBKVOController *controller = info->_controller;
if (nil != controller) {

//强引用观察者,在FBKVOController中,FBKVOController对象弱引用观察者observer,防止在使用时已经被释放
id observer = controller.observer;
if (nil != observer) {

//使用自定义block回传监听结果
if (info->_block) {

NSDictionary<NSString *, id> *changeWithKeyPath = change;

//将keyPath添加到字典中以便在观察多个keyPath时,能够清晰知道监听的是哪个keyPath
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);

} else if (info->_action) {
//使用自定义方法回传监听结果
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
//使用系统默认方法回传监听结果
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}

_FBKVOSharedController实现了移除观察者的方法,思路如下:

  • 1.首先从_infos中移除被监听的属性信息对象info;
  • 2.然后根据监听状态,通过调用系统的方法,移除正在被监听的属性信息对象info;
  • 3.最后修改监听状态;

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}

//先从HashTable中移除被监听的属性信息对象
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);

// 当正在监听时,则移除监听
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
//修改被监听的状态
info->_state = _FBKVOInfoStateNotObserving;
}

(四)NSObject+FBKVOController

NSObject+FBKVOController 分类比较简单,它主要通过runtime方法,以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。



@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end

五.FBKVOController总结

FBKVOController是线程安全的,相对于系统的KVO而言,使用起来更方便,安全,简洁。

  • 1.NSHashTable和NSMapTable的使用;
  • 2.互斥锁pthread_mutex_t的使用
  • 3.FBKVOController和Observer之间循环引用的形成和解决;
  • 4.FBKVOController和_FBKVOInfo之间循环引用的形成和解决;


作者:Cooci
链接:https://www.jianshu.com/p/65e345a7aa21

收起阅读 »

没对象么?那就来了解Java创建对象详解

对象是对类的实例化。对象具有状态和行为,变量用来表明对象的状态,方法表明对象所具有的行为。Java 对象的生命周期包括创建、使用和清除,本文详细介绍对象的创建 Java虚拟机内存架构模型详解 1.使用new创建对象 使用new关键字创建对象应该是最常见的一种...
继续阅读 »

对象是对类的实例化。对象具有状态和行为,变量用来表明对象的状态,方法表明对象所具有的行为。Java 对象的生命周期包括创建、使用和清除,本文详细介绍对象的创建



Java虚拟机内存架构模型详解


1.使用new创建对象


使用new关键字创建对象应该是最常见的一种方式,但我们应该知道,使用new创建对象会增加耦合度。无论使用什么框架,都要减少new的使用以降低耦合度。


public class Hello
{
public void sayWorld()
{
System.out.println("Hello world!");
}

}
public class NewClass
{
public static void main(String[] args)
{
Hello h = new Hello();
h.sayWorld();
}
}
复制代码

2.使用反射的机制创建对象


使用Class类的newInstance方法


  Hello类的代码不变,NewClass类的代码如下:


public class NewClass
{
public static void main(String[] args)
{
try
{
Class heroClass = Class.forName("yunche.test.Hello");
Hello h =(Hello) heroClass.newInstance();
h.sayWorld();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}

}
}
复制代码

使用Constructor类的newInstance方法


public class NewClass
{
public static void main(String[] args)
{
try
{
//获取类对象
Class heroClass = Class.forName("yunche.test.Hello");

//获取构造器
Constructor constructor = heroClass.getConstructor();
Hello h =(Hello) constructor.newInstance();
h.sayWorld();
}
catch (NoSuchMethodException e)
{
e.printStackTrace();
}
catch (InvocationTargetException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}

}
}
复制代码

3.采用clone


  clone时,需要已经有一个分配了内存的源对象,创建新对象时,首先应该分配一个和源对象一样大的内存空间。


  要调用clone方法需要实现Cloneable接口,由于clone方法是protected的,所以修改Hello类。


public class Hello implements Cloneable
{
public void sayWorld()
{
System.out.println("Hello world!");

}

public static void main(String[] args)
{
Hello h1 = new Hello();
try
{
Hello h2 = (Hello)h1.clone();
h2.sayWorld();
}
catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
}
}
复制代码

4.采用序列化机制


  使用序列化时,要实现实现Serializable接口,将一个对象序列化到磁盘上,而采用反序列化可以将磁盘上的对象信息转化到内存中。


public class Serialize
{
public static void main(String[] args)
{
Hello h = new Hello();

//准备一个文件用于存储该对象的信息
File f = new File("hello.obj");

try(FileOutputStream fos = new FileOutputStream(f);
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream(f);
ObjectInputStream ois = new ObjectInputStream(fis)
)
{
//序列化对象,写入到磁盘中
oos.writeObject(h);
//反序列化对象
Hello newHello = (Hello)ois.readObject();

//测试方法
newHello.sayWorld();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
}

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

iOS必备装X技能-NSOperationQueue 控制串行执行、并发执行

 NSOperationQueue 控制串行执行、并发执行NSOperationQueue 创建的自定义队列同时具有串行、并发功能,那么他的串行功能是如何实现的?这里有个关键属性 maxConcurrentOperationCount,叫做...
继续阅读 »

 NSOperationQueue 控制串行执行、并发执行

NSOperationQueue 创建的自定义队列同时具有串行、并发功能,那么他的串行功能是如何实现的?


这里有个关键属性 maxConcurrentOperationCount,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。

注意:这里 maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行。


最大并发操作数:maxConcurrentOperationCount
  • maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。

/**
* 设置 MaxConcurrentOperationCount(最大并发操作数)
*/

- (void)setMaxConcurrentOperationCount {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 串行队列
// queue.maxConcurrentOperationCount = 2; // 并发队列
// queue.maxConcurrentOperationCount = 8; // 并发队列

// 3.添加操作
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}

当最大并发操作数为1时,操作是按顺序串行执行的,并且一个操作完成之后,下一个操作才开始执行。当最大操作并发数为2时,操作是并发执行的,可以同时执行两个操作。而开启线程数量是由系统决定的,不需要我们来管理

这样看来,是不是比 GCD 还要简单了许多

 NSOperation 操作依赖

NSOperation、NSOperationQueue 最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。NSOperation 提供了3个接口供我们管理和查看依赖。

  • - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
  • - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。

当然,我们经常用到的还是添加依赖操作。现在考虑这样的需求,比如说有 A、B 两个操作,其中 A 执行完操作,B 才能执行操作。

如果使用依赖来处理的话,那么就需要让操作 B 依赖于操作 A。具体代码如下:


/**
* 操作依赖
* 使用方法:addDependency:
*/

- (void)addDependency {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];

// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2

// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}

通过添加操作依赖,无论运行几次,其结果都是 op1 先执行,op2 后执行

NSOperation 优先级

NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。


// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。

那么,什么样的操作才是进入就绪状态的操作呢?

  • 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子,现在有4个优先级都是 NSOperationQueuePriorityNormal(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这4个操作添加到队列中并发执行。

  • 因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。
  • 而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。

理解了进入就绪状态的操作,那么我们就理解了queuePriority 属性的作用对象。

  • queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。
  • 如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。


 NSOperation、NSOperationQueue 线程间的通信


在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯

/**
* 线程间通信
*/

- (void)communication {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];

// 2.添加操作
[queue addOperationWithBlock:^{
// 异步进行耗时操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}

// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}];
}
通过线程间的通信,先在其他线程中执行操作,等操作执行完了之后再回到主线程执行主线程的相应操作

NSOperation、NSOperationQueue 线程同步和线程安全

  • 线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
  • 线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

下面,我们模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。
场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。


NSOperation、NSOperationQueue 非线程安全

先来看看不考虑线程安全的代码:

/**
* 非线程安全:不使用 NSLock
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/

- (void)initTicketStatusNotSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程

self.ticketSurplusCount = 50;

// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;

// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;

// 3.创建卖票操作 op1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketNotSafe];
}];

// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketNotSafe];
}];

// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}

/**
* 售卖火车票(非线程安全)
*/

- (void)saleTicketNotSafe {
while (1) {

if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有火车票均已售完");
break;
}
}
}

在不考虑线程安全,不使用 NSLock 情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题

NSOperation、NSOperationQueue 线程安全

线程安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各种方式。这里我们使用 NSLock 对象来解决线程同步问题。NSLock 对象可以通过进入锁时调用 lock 方法,解锁时调用 unlock 方法来保证线程安全。

考虑线程安全的代码:


/**
* 线程安全:使用 NSLock 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/


- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程

self.ticketSurplusCount = 50;

self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象

// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;

// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;

// 3.创建卖票操作 op1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketSafe];
}];

// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketSafe];
}];

// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}

/**
* 售卖火车票(线程安全)
*/

- (void)saleTicketSafe {
while (1) {

// 加锁
[self.lock lock];

if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}

// 解锁
[self.lock unlock];

if (self.ticketSurplusCount <= 0) {
NSLog(@"所有火车票均已售完");
break;
}
}
}

在考虑了线程安全,使用 NSLock 加锁、解锁机制的情况下,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题

NSOperation 常用属性和方法

  1. 取消操作方法
    • - (void)cancel; 可取消操作,实质是标记 isCancelled 状态。
  2. 判断操作状态方法
    • - (BOOL)isFinished; 判断操作是否已经结束。
    • - (BOOL)isCancelled; 判断操作是否已经标记为取消。
    • - (BOOL)isExecuting; 判断操作是否正在在运行。
    • - (BOOL)isReady; 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。
  3. 操作同步
    • - (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。
    • - (void)setCompletionBlock:(void (^)(void))block; completionBlock 会在当前操作执行完毕时执行 completionBlock。
    • - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
    • - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
    • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。


 NSOperationQueue 常用属性和方法

  • 取消/暂停/恢复操作
    • - (void)cancelAllOperations; 可以取消队列的所有操作。
    • - (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。
    • - (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
  • 操作同步
    • - (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕。
  • 添加/获取操作`
    • - (void)addOperationWithBlock:(void (^)(void))block; 向队列中添加一个 NSBlockOperation 类型操作对象。
    • - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束
    • - (NSArray *)operations; 当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。
    • - (NSUInteger)operationCount; 当前队列中的操作数。
  • 获取队列
    • + (id)currentQueue; 获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。
    • + (id)mainQueue; 获取主队列。


  • 注意:

    1. 这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
    2. 暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。


    作者:Cooci
    链接:https://www.jianshu.com/p/5ee0aa045127





    收起阅读 »

    iOS面试-与面试官盘NSOperation、NSOperationQueue

    NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。为什么...
    继续阅读 »

    NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

    为什么要使用 NSOperation、NSOperationQueue?

    1. 可添加完成的代码块,在操作完成后执行。
    2. 添加操作之间的依赖关系,方便的控制执行顺序。
    3. 设定操作执行的优先级。
    4. 可以很方便的取消一个操作的执行。
    5. 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。

    2. NSOperation、NSOperationQueue 操作和操作队列

    既然是基于 GCD 的更高一层的封装。那么,GCD 中的一些概念同样适用于 NSOperation、NSOperationQueue。在 NSOperation、NSOperationQueue 中也有类似的任务(操作)队列(操作队列)的概念。

    • 操作(Operation):
      • 执行操作的意思,换句话说就是你在线程中执行的那段代码。
      • 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperationNSBlockOperation,或者自定义子类来封装操作。
    • 操作队列(Operation Queues):
      • 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
      • 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
      • NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。

    3. NSOperation、NSOperationQueue 使用步骤

    NSOperation 需要配合 NSOperationQueue 来实现多线程。因为默认情况下,NSOperation 单独使用时系统同步执行操作,配合 NSOperationQueue 我们能更好的实现异步执行。

    NSOperation 实现多线程的使用步骤分为三步:

    1. 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
    2. 创建队列:创建 NSOperationQueue 对象。
    3. 将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。

    之后呢,系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。

    下面我们来学习下 NSOperation 和 NSOperationQueue 的基本使用。

    4. NSOperation 和 NSOperationQueue 基本使用

    4.1 创建操作

    NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。

    1. 使用子类 NSInvocationOperation
    2. 使用子类 NSBlockOperation
    3. 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。

    在不使用 NSOperationQueue,单独使用 NSOperation 的情况下系统同步执行操作,下面我们学习以下操作的三种创建方式。


    4.1.1 使用子类 NSInvocationOperation

    /**
    * 使用子类 NSInvocationOperation
    */

    - (void)useInvocationOperation {

    // 1.创建 NSInvocationOperation 对象
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

    // 2.调用 start 方法开始执行操作
    [op start];
    }

    /**
    * 任务1
    */

    - (void)task1 {
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }
    输出结果:

    • 可以看到:在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
    如果在其他线程中执行操作,则打印结果为其他线程。
    // 在其他线程使用子类 NSInvocationOperation
    [NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

    • 可以看到:在其他线程中单独使用子类 NSInvocationOperation,操作是在当前调用的其他线程执行的,并没有开启新线程。

    下边再来看看 NSBlockOperation。


    4.1.2 使用子类 NSBlockOperation

    /**
    * 使用子类 NSBlockOperation
    */

    - (void)useBlockOperation {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.调用 start 方法开始执行操作
    [op start];
    }

    • 可以看到:在没有使用 NSOperationQueue、在主线程中单独使用 NSBlockOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

    注意:和上边 NSInvocationOperation 使用一样。因为代码是在主线程中调用的,所以打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程。

    但是,NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。

    如果添加的操作多的话,blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。(可以使用 addExecutionBlock: 多添加几个操作试试)。


    /**
    * 使用子类 NSBlockOperation
    * 调用方法 AddExecutionBlock:
    */

    - (void)useBlockOperationAddExecutionBlock {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.添加额外的操作
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.调用 start 方法开始执行操作
    [op start];
    }

    /**
    * 使用子类 NSBlockOperation
    * 调用方法 AddExecutionBlock:
    */

    - (void)useBlockOperationAddExecutionBlock {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.添加额外的操作
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.调用 start 方法开始执行操作
    [op start];
    }

    • 可以看出:使用子类 NSBlockOperation,并调用方法 AddExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中异步执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。

    一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

    4.1.3 使用自定义继承自 NSOperation 的子类

    如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。重写main方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished。当 main 执行完返回的时候,这个操作就结束了。

    先定义一个继承自 NSOperation 的子类,重写main方法。

    // YSCOperation.h 文件
    #import <Foundation/Foundation.h>

    @interface YSCOperation : NSOperation

    @end

    // YSCOperation.m 文件
    #import "YSCOperation.h"

    @implementation YSCOperation

    - (void)main {
    if (!self.isCancelled) {
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2];
    NSLog(@"1---%@", [NSThread currentThread]);
    }
    }
    }

    @end
    /**
    * 使用自定义继承自 NSOperation 的子类
    */

    - (void)useCustomOperation {
    // 1.创建 YSCOperation 对象
    YSCOperation *op = [[YSCOperation alloc] init];
    // 2.调用 start 方法开始执行操作
    [op start];
    }


    • 可以看出:在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类的情况下,是在主线程执行操作,并没有开启新线程。

    下边我们来讲讲 NSOperationQueue 的创建。

    4.2 创建队列

    NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。

    • 主队列
      • 凡是添加到主队列中的操作,都会放到主线程中执行(注:不包括操作使用addExecutionBlock:添加的额外操作,额外操作可能在其他线程执)。


    // 主队列获取方法
    NSOperationQueue *queue = [NSOperationQueue mainQueue];

    • 自定义队列(非主队列)
      • 添加到这种队列中的操作,就会自动放到子线程中执行。
      • 同时包含了:串行、并发功能。
    // 自定义队列创建方法
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    4.3 将操作加入到队列中

    上边我们说到 NSOperation 需要配合 NSOperationQueue 来实现多线程。

    那么我们需要将创建好的操作加入到队列中去。总共有两种方法:

    1. - (void)addOperation:(NSOperation *)op;
      • 需要先创建操作,再将创建好的操作加入到创建好的队列中去。
    /**
    * 使用 addOperation: 将操作加入到操作队列中
    */

    - (void)addOperationToQueue {

    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.创建操作
    // 使用 NSInvocationOperation 创建操作1
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

    // 使用 NSInvocationOperation 创建操作2
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];

    // 使用 NSBlockOperation 创建操作3
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op3 addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.使用 addOperation: 添加所有操作到队列中
    [queue addOperation:op1]; // [op1 start]
    [queue addOperation:op2]; // [op2 start]
    [queue addOperation:op3]; // [op3 start]
    }
    • 使用 NSOperation 子类创建操作,并使用 addOperation: 将操作加入到操作队列后能够开启新线程,进行并发执行。
    1. - (void)addOperationWithBlock:(void (^)(void))block;
      • 无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中。


    /**
    * 使用 addOperationWithBlock: 将操作加入到操作队列中
    */


    - (void)addOperationWithBlockToQueue {
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.使用 addOperationWithBlock: 添加操作到队列中
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    }
    使用 addOperationWithBlock: 将操作加入到操作队列后能够开启新线程,进行并发执行


    作者:Cooci
    链接:https://www.jianshu.com/p/5ee0aa045127








    收起阅读 »

    iOS标准库中常用数据结构和算法之cache

    📝缓存Cache缓存是以键值对的形式进行数据的存储和检索,内部采用哈希表实现。当系统出现内存压力时则会释放掉部分缓存的键值对。 iOS系统提供了一套基于OC语言的高级缓存库NSCache,同时也提供一套基于C语言实现的缓存库libcache.dylib,其中N...
    继续阅读 »

    📝缓存Cache

    缓存是以键值对的形式进行数据的存储和检索,内部采用哈希表实现。当系统出现内存压力时则会释放掉部分缓存的键值对。 iOS系统提供了一套基于OC语言的高级缓存库NSCache,同时也提供一套基于C语言实现的缓存库libcache.dylib,其中NSCache是基于libcache.dylib实现的高级类库,并且这两个库都是线程安全的。 本文主要介绍基于C语言的缓存库的各种API函数。

    头文件: #include <cache.h>, #include <cache_callbacks.h>
    平台: iOS系统

    一、缓存对象的创建和关闭

    功能:创建或者销毁一个缓存对象。

    函数签名:

    int cache_create(const char *name, cache_attributes_t *attrs, cache_t **cache_out);
    int cache_destroy(cache_t *cache);

    参数:

    name:[in] 创建缓存时用来指定缓存的字符串名称,不能为空。
    attrs: [in] 设置缓存的属性。不能为空。
    cache_out: [out] 返回创建的缓存对象。
    return: [out] 成功操作返回0,否则返回非0

    描述:

    缓存对象是一个容器对象,其缓存的内容是一个个键值对,至于这些键值对是什么类型的数据,如何控制键值对的数据的生命周期,如何判断两个键是否是相同的键等等这些信息缓存对象本身是无法得知,因此需要我们明确的告诉缓存对象如何去操作这些键值信息。这也就是为什么在创建缓存对象时需要指定属性这个参数了。属性的参数类型是一个cache_attributes_t结构体。这个结构体的大部分数据成员都是函数指针,这些函数指针就是用来实现对键值进行操作的各种策略。

    struct cache_attributes_s {
    uint32_t version; //缓存对象的版本信息
    cache_key_hash_cb_t key_hash_cb; //对键执行hash计算的函数,不能为空
    cache_key_is_equal_cb_t key_is_equal_cb; //判断两个键是否相等的函数,不能为空

    cache_key_retain_cb_t key_retain_cb; //键加入缓存时调用,用于增加键的引用计数或者进行内存拷贝。
    cache_release_cb_t key_release_cb; //键的释放处理函数,用于对键的内存管理使用。
    cache_release_cb_t value_release_cb; //值的释放处理函数,用于对值的内存管理使用。

    cache_value_make_nonpurgeable_cb_t value_make_nonpurgeable_cb; //当值的引用计数从0变为1时对值内存进行非purgeable的处理函数。
    cache_value_make_purgeable_cb_t value_make_purgeable_cb; //当值的引用计数变为0时对值进行purgeable的处理函数。这个函数的作用是为了解决当内存吃紧时自动释放所分配的内存。

    void *user_data; //附加数据,这个附加数据会在所有的这些回调函数中出现。

    // Added in CACHE_ATTRIBUTES_VERSION_2
    cache_value_retain_cb_t value_retain_cb; //值增加引用计数的函数,用于对值的内存管理使用。
    };
    typedef struct cache_attributes_s cache_attributes_t;

    上述的各种回调函数的格式都在cache.h中有明确的定义,因此这里就不再展开介绍了。一般情况下我们通常都会将字符串或者整数来作为键使用,因此当你采用字符串或者整数作为键时,系统预置了一系列缓存对象属性的默认实现函数。这些函数的声明在cache_callbacks.h文件中

    /*
    * Pre-defined callback functions.
    */

    //用于键进行哈希计算的预置函数
    CACHE_PUBLIC_API uintptr_t cache_key_hash_cb_cstring(void *key, void *unused);
    CACHE_PUBLIC_API uintptr_t cache_key_hash_cb_integer(void *key, void *unused);
    //用于键进行相等比较的预置函数
    CACHE_PUBLIC_API bool cache_key_is_equal_cb_cstring(void *key1, void *key2, void *unused);
    CACHE_PUBLIC_API bool cache_key_is_equal_cb_integer(void *key1, void *key2, void *unused);

    //键值进行释放的函数,这函数默认实现就是调用free函数,因此如果采用这个函数进行释放处理则键值需要从堆中进行内存分配。
    CACHE_PUBLIC_API void cache_release_cb_free(void *key_or_value, void *unused);

    //对值进行purgeable处理的预置函数。
    CACHE_PUBLIC_API void cache_value_make_purgeable_cb(void *value, void *unused);
    CACHE_PUBLIC_API bool cache_value_make_nonpurgeable_cb(void *value, void *unused);

    示例代码:

    //下面代码用于创建一个以字符串为键的缓存对象,其中的缓存对象的属性中的各个成员函数采用的是系统默认预定的函数。
    #include <cache.h>
    #include <cache_callbcaks.h>

    cache_t *im_cache;
    cache_attributes_t attrs = {
    .version = CACHE_ATTRIBUTES_VERSION_2,
    .key_hash_cb = cache_key_hash_cb_cstring,
    .key_is_equal_cb = cache_key_is_equal_cb_cstring,
    .key_retain_cb = my_copy_string,
    .key_release_cb = cache_release_cb_free,
    .value_release_cb = cache_release_cb_free,
    };
    cache_create("com.acme.im_cache", &attrs, &im_cache);

    二、缓存对象中键值对的设置和获取以及删除

    功能:用于处理键值对在缓存中的添加、获取和删除操作。
    函数签名:

    //将键值对添加到缓存,或者替换掉原有的键值对。
    int cache_set_and_retain(cache_t *cache, void *key, void *value, size_t cost);

    //从缓存中根据键获取值
    int cache_get_and_retain(cache_t *cache, void *key, void **value_out);

    //将缓存中的值引用计数减1,当引用计数为0时则清理值分配的内存或者销毁值分配的内存。
    int cache_release_value(cache_t *cache, void *value);

    //从缓存中删除键值。
    int cache_remove(cache_t *cache, void *key);

    参数:
    cache:[in] 缓存对象。
    key:[in] 添加或者获取或者删除时的键。
    cost:[in] 添加缓存时的成本代价,值越大键值在缓存中保留的时间就越长久。
    value:[in] 添加时的值。
    value_out: [out] 用于值获取时的输出。

    描述:
    1、cache_set_and_retain 函数用于将键值对放入缓存中,并指定cost值。当将一个键添加到缓存时,系统内部分别会调用缓存属性cache_attributes_t结构体中的key_retain_cb来实现对键的内存的管理,如果这个函数设置为NULL的话那就表明我们需要自己负责键的生命周期的管理。因为缓存对象内部是通过哈希表来进行数据的存储和检索的,所以在将键值对加入缓存时,还需要提供对键进哈希计算和比较的属性函数key_hash_cb,key_is_equal_cb。 而对于值来说,当值加入缓存时系统会将值的引用计数设置为1,如果我们想自行处理值在缓存中的内存保存则需要指定缓存属性中的value_retain_cb来实现。加入缓存中的值是可以为NULL的。最后的cost参数用于指定这个键值对的成本值,值越小在缓存中保留的时间就越少,反之亦然。

    2、cache_get_and_retain函数用来根据键获取对应的值,如果缓存中没有保存对应的键值对,或者键值对被丢弃,或者值所分配的内存被清除则value_out返回NULL,并且函数返回特殊的值ENOENT。每调用一次值的获取,缓存对象都会增加值的引用计数。因此当我们不再需要访问返回的值时则需要调用手动调用cache_release_value函数来减少缓存对象中值的引用计数。而当值的引用计数变为0时则值分配的内存会设置为可清理(purgeable)或者被销毁掉。

    3、cache_remove函数用于删除缓存中的键值对。当删除缓存中的键值对时,缓存对象会调用属性结构体中的key_release_cb函数进行键的内存销毁,以及如果值的引用计数变为0时则会调用value_release_cb函数进行值的内存销毁。

    示例代码:

    #include <cache.h>
    #include <cache_callbacks.h>

    void main()
    {
    cache_attributes_t attr;
    attr.key_hash_cb = cache_key_hash_cb_cstring;
    attr.key_is_equal_cb = cache_key_is_equal_cb_cstring;
    attr.key_retain_cb = NULL;
    attr.key_release_cb = cache_release_cb_free;
    attr.version = CACHE_ATTRIBUTES_VERSION_2;
    attr.user_data = NULL;
    attr.value_retain_cb = NULL;
    attr.value_release_cb = cache_release_cb_free;
    attr.value_make_purgeable_cb = cache_value_make_purgeable_cb;
    attr.value_make_nonpurgeable_cb = cache_value_make_nonpurgeable_cb;

    //创建缓存
    cache_t *cache = NULL;
    int ret = cache_create("com.test", &attr, &cache);

    //将键值对放入缓存
    char *key = malloc(4);
    strcpy(key, "key");
    char *val = malloc(4);
    strcpy(val, "val");
    ret = cache_set_and_retain(cache, key, val, 0);
    ret = cache_release_value(cache, val);
    //获取键值对,使用后要释放。
    char *val2 = NULL;
    ret = cache_get_and_retain(cache, key, (void**)&val2);
    ret = cache_release_value(cache, val2);

    //删除键值
    cache_remove(cache, key);

    //销毁缓存。
    cache_destroy(cache);
    }

    三、缓存中键值对的清理策略和值的访问策略

    缓存的作用是会对保存在里面的键值对进行丢弃,这取决于当前内存的使用情况以及其他一些场景。在调用cache_set_and_retain将键值对添加到缓存时还会指定一个cost值,值越大被丢弃的可能性就越低。在上面的介绍中有说明缓存会对键值对中的值进行引用计数管理。当调用cache_set_and_retain时值引用计数将设置为1,调用cache_get_and_retain函数获取值时如果键值对在缓存中则会增加值的引用计数。而不需要访问和操作值时我们需要调用cache_release_value函数来将引用计数减1。当值的引用计数变为0时就会立即或者以后发生如下的事情:

    1、如果我们在缓存的属性结构体中设置了value_make_purgeable_cb函数则会调用这个函数表明此时值是可以被清理的。被清理的意思是说为值分配的物理内存随时有可能会被回收。因此当值被设置为可被清理状态时就不能继续去直接访问值所分配的内存了。
    2、如果在此之前键值对因为函数cache_remove的调用而被从缓存中删除,则会调用属性结构体中的value_release_cb函数来执行值内存的销毁处理。
    3、如果因为系统内存的压力导致需要丢弃缓存中的键值对时,就会把值引用计数为0的键值对丢弃掉!注意:只有值引用计数为0时才会缓存被丢弃。

    每次对缓存中的值的访问都需要通过cache_get_and_retain函数来执行,当调用cache_get_and_retain函数时会发生如下事情:

    1、判断当前键值对是否在缓存中,如果不再则值返回NULL。
    2、如果键值对在缓存中。并且值的引用计数为0,就会判断缓存的结构体属性中是否存在value_make_nonpurgeable_cb函数,如果存在则会调用value_make_nonpurgeable_cb函数将值的内存设置为不可清理,如果设置为不可清理返回为false则表明此时值的内存已经被清理了,这时候键值对将会从缓存中丢弃,并且cache_get_and_retain函数将返回值为NULL。当然如果value_make_nonpurgeable_cb函数为空则不会发生这一步。
    3、增加值的引用计数,并返回值。

    链接:https://www.jianshu.com/p/2f58f165bf1a

    收起阅读 »

    iOS开发 - 面试被问到内存概念怎么办?

    在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。这样也就是单运行的时候没有什么问题!可是,计算机会有多到程序、分时系统和多任务,当我们能够同时运行多个程序时,CPU的利用率将会比较高。那么有一个非常严重的问题:如何...
    继续阅读 »
    在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。这样也就是单运行的时候没有什么问题!可是,计算机会有多到程序、分时系统和多任务,当我们能够同时运行多个程序时,CPU的利用率将会比较高。那么有一个非常严重的问题:如何将计算机的有限的物理内存分配给多个程序使用

    假设我们计算有128MB内存,程序A需要10MB,程序B需要100MB,程序C需要20MB。如果我们需要同时运行程序A和B,那么比较直接的做法是将内存的前10MB分配给程序A,10MB~110MB分配给B。




    但这样做,会造成以下问题:

    • 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。

    • 进程地址空间不隔离,由于程序是直接访问物理内存的,所以每一个进程都可以修改其他进程的内存数据,设置修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏

    • 内存使用效率低 内存空间不足,就需要将其他程序展示拷贝到硬盘当中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率会非常低

    • 程序运行的地址不确定;因为内存地址是随机分配的,所以程序运行的地址也是不正确的

    解决这几个问题的思路就是使用我们非常牛逼的方法:增加中间层 - 即使用一种间接的地址访问方式。


    把程序给出的地址看做是一种虚拟地址,然后通过某种映射,将这个虚拟地址转化到实际的物理地址。这样,只需要控制好映射过程,就能保证程序所能访问的物理内存区域跟别的程序不重叠,达到空间隔离的效果。

    隔离

    普通的程序它只需要一个简单的执行环境一个单一的地址空间有自己的CPU
    地址空间比较抽象,如果把它想象成一个数组,每一个数组是一字节,数组大小就是地址空间的长度,那么32位的地址空间大小就是2^32=4294967296字节,即4G,地址空间有效位是0x00000000~0xFFFFFFFF
    地址空间分为两种:

    • 物理空间:就是物理内存。32位的机器,地址线就有32条,物理空间4G,但如果只装有512M的内存,那么实际有效的空间地址就是0x00000000~0x1FFFFFFF,其他部分都是无效的。

    • 虚拟空间:每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的空间地址,这样就有效的做到了进程隔离。

    分段

    基本思路: 把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。虚拟空间的每个字节对应物理空间的每个字节。这个映射过程由软件来完成。

    比如A需要10M,就假设有0x00000000 到0x00A00000大小的虚拟空间,然后从物理内存分配一个相同大小的空间,比如是0x001000000x00B00000。操作系统来设置这个映射函数,实际的地址转换由硬件完成。如果越界,硬件就会判断这是一个非法访问,拒绝这个地址请求,并上报操作系统或监控程序。





    这样一来利用分段的方式可以解决之前的地址空间不隔离程序运行地址不确定

    • 首先做到了地址隔离,因为A和B被映射到了两块不同的物理空间,它们之间没有任何重叠,如果A访问虚拟空间的地址超过了0x00A00000这个范围,硬件就会判断这是一个非法的访问,并将这个请求报告给操作系统或者监控程序,由它决定如何处理。

    • 再者,对于每个程序来说,无论它们被分配到地址空间的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只要按照从地址0x000000000x00A00000来编写程序、放置变量,所以程序不需要重定位。

    第二问题内存使用效率问题依旧没有解决。

    但是分段的方法没有解决内存使用效率的问题。分段对于内存区域的映射还是按照程序为单位,如果内存不足,被换入换出的磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上根据程序的局部性原理,当一个程序正在运行时,在某个时间段内,它只是频繁用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内是不会被用到的。人们很自然地想到了更小粒度的内存分割和映射方法,使得程序的局部性原理得到充分利用,大大提高了内存的使用率。这种方法就是分页。

    分页

    分页的基本方法是把地址空间人为得等分成固定大小的页,每一个页的大小由硬件决定,或硬件支持多种页的大小,由操作系统选择决定页的大小。 目前几乎所有PC的操作系统都是用4KB大小的页。我们使用的PC机是32位虚拟地址空间,也就是4GB,按4KB分页,总共有1048576个页。

    那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它们从磁盘里取出即可。图中的线表示映射关系,我们可以看到虚拟空间有些页被映射到同一个物理页,这样就可以实现内存共享。
    虚拟页,物理页,磁盘页根据内存空间不一样而区分

    我们可以看到Process 1 的VP2和VP3不在内存中,但是当进程需要用到这两个页的时候,硬件就会捕获到这个消息,就是所谓的页错误(Page Fault),然后操作系统接管进程,负责将VP2和VP3从磁盘读取出来装入内存,然都将内存中的这两个页和VP2和VP3建立映射关系。以页为单位存取和交换数据非常方便,硬件本身就支持这种以页为单位的操作方式。


  • 保护页也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问,而且只有操作系统有权修改这些属性,那么操作系统就可以做到保护自己和保护进程。

  • 虚拟存储的实现需要硬件支持,几乎所有CPU都采用称为MMU的部件来进行页的映射:




  • 在页映射模式下,CPU发出的是Virtual Address,即我们程序看到的是虚拟地址。经过MMU转换以后就变成了Physical Address。一般MMU集成在CPU内部,不会以独立的部件存在。

    作者:Cooci
    链接:https://www.jianshu.com/p/1ad04daa1b8a











    收起阅读 »

    多线程安全-iOS开发注意咯

    正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势资源利用率更好程序设计在某些情况下更简单程序响应更快但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础原子操作...
    继续阅读 »



    正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势

    • 资源利用率更好
    • 程序设计在某些情况下更简单
    • 程序响应更快

    但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础


    原子操作

    自旋锁其实就是封装了一个spinlock_t自旋锁

    自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。自旋锁下面还会展开来介绍

    互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态bool lock = false; // 一开始没有锁上,任何线程都可以申请锁

    do {
    while(test_and_set(&lock); // test_and_set 是一个原子操作
    Critical section // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
    Reminder section // 不需要锁保护的代码
    }


    操作在底层会被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码,而我们的原子性的单条指令的执行是不会被打断的,所以保证了安全.

    自旋锁的BUG

    尽管原子操作非常的简单,但是它只适合于比较简单特定的场合。在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作指令就力不从心了,

    如果临界区的执行时间过长,使用自旋锁不是个好主意。之前我们介绍过时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的

    下面开始我们又爱又恨的


    iOS锁

    锁并是一种非强制机制,每一个现货出呢个在访问数据或资源之前视图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁已经被占用的时候试图获取锁,线程会等待,知道锁重新可用!

    信号量

    二元信号量(Binary Semaphore)只有两种状态:占用与非占用。它适合被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放

    现在我们在这个基础上,我们把学习的思维由二元->多元的时候,我们的信号量由此诞生,多元信号量简称信号量

    • 将信号量的值减1

    • 如果信号量的值小于0,则进入等待状态,否则继续执行。访问玩资源之后,线程释放信号量,进行如下操作

    • 将信号量的值加1

    • 如果信号量的值小于1,唤醒一个等待中的线程


    let sem = DispatchSemaphore(value: 1)

    for index in 1...5 {
    DispatchQueue.global().async {
    sem.wait()
    print(index,Thread.current)
    sem.signal()
    }
    }

    输出结果:
    1 <NSThread: 0x600003fa8200>{number = 3, name = (null)}
    2 <NSThread: 0x600003f90140>{number = 4, name = (null)}
    3 <NSThread: 0x600003f94200>{number = 5, name = (null)}
    4 <NSThread: 0x600003fa0940>{number = 6, name = (null)}
    5 <NSThread: 0x600003f94240>{number = 7, name = (null)}

    互斥量


    互斥量(Mutex)又叫互斥锁和二元信号量很类似,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放;也就是说哪个线程锁的,要哪个线程释放锁。

    Mutex可以分为递归锁(recursive mutex)非递归锁(non-recursive mutex)。 递归锁也叫可重入锁(reentrant mutex),非递归锁也叫不可重入锁(non-reentrant mutex)
    二者唯一的区别是:
    • 同一个线程可以多次获取同一个递归锁,不会产生死锁。
    • 如果一个线程多次获取同一个非递归锁,则会产生死锁。

    NSLock 是最简单额互斥锁!但是是非递归的!直接封装了pthread_mutex 用法非常简单就不做赘述
    @synchronized 是我们互斥锁里面用的最频繁的,但是性能最差!

    int main(int argc, const char * argv[]) {
    NSString *obj = @"Iceberg";
    @synchronized(obj) {
    NSLog(@"Hello,world! => %@" , obj);
    }
    }

    底层clang

    int main(int argc, const char * argv[]) {

    NSString *obj = (NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_0;

    {
    id _rethrow = 0;
    id _sync_obj = (id)obj;
    objc_sync_enter(_sync_obj);
    try {
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {
    objc_sync_exit(sync_exit);
    }
    id sync_exit;
    } _sync_exit(_sync_obj);

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_1 , obj);

    } catch (id e) {
    _rethrow = e;
    }

    {
    struct _FIN {
    _FIN(id reth) : rethrow(reth) {}
    ~_FIN() {
    if (rethrow)
    objc_exception_throw(rethrow);
    }
    id rethrow;
    } _fin_force_rethow(_rethrow);
    }
    }

    }
    我们发现objc_sync_enter函数是在try语句之前调用,参数为需要加锁的对象。因为C++中没有try{}catch{}finally{}语句,所以不能在finally{}调用objc_sync_exit函数。因此objc_sync_exit是在_SYNC_EXIT结构体中的析构函数中调用,参数同样是当前加锁的对象。这个设计很巧妙,原因在_SYNC_EXIT结构体类型的_sync_exit是一个局部变量,生命周期为try{}语句块,其中包含了@sychronized{}代码需要执行的代码,在代码完成后,_sync_exit局部变量出栈释放,随即调用其析构函数,进而调用objc_sync_exit函数。即使try{}语句块中的代码执行过程中出现异常,跳转到catch{}语句,局部变量_sync_exit同样会被释放,完美的模拟了finally的功能。


    int objc_sync_enter(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
    SyncData* data = id2data(obj, ACQUIRE);
    require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");

    result = recursive_mutex_lock(&data->mutex);
    require_noerr_string(result, done, "mutex_lock failed");
    } else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
    _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    objc_sync_nil();
    }

    done:
    return result;
    }


    从上面的源码中我们可以得出你调用sychronized的每个对象,Objective-C runtime都会为其分配一个递归锁并存储在哈希表中。完美

    其实如果大家觉得@sychronized性能低的话,完全可以用NSRecursiveLock现成的封装好的递归锁

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
    [lock lock];
    if (value > 0) {
    NSLog(@"value:%d", value);
    RecursiveBlock(value - 1);
    }
    [lock unlock];
    };
    RecursiveBlock(2);
    });

    2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2
    2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1

    条件变量

    条件变量(Condition Variable)作为一种同步手段,作用类似一个栅栏。对于条件变量,现成可以有两种操作:

    • 首先线程可以等待条件变量,一个条件变量可以被多个线程等待
    • 其次线程可以唤醒条件变量。此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。

    换句话说:使用条件变量可以让许多线程一起等待某个时间的发生,当某个时间发生时,所有的线程可以一起恢复执行!

    相信仔细的大家肯定在锁的用法里面见过NSCondition,就是封装了条件变量pthread_cond_t和互斥锁

    - (void) signal { 
    pthread_cond_signal(&_condition);
    }
    // 其实这个函数是通过宏来定义的,展开后就是这样
    - (void) lock {
    int err = pthread_mutex_lock(&_mutex);
    }
    NSConditionLock借助 NSCondition来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个NSCondition对象,以及 _condition_value属性,在初始化时就会对这个属性进行赋值:

    // 简化版代码
    - (id) initWithCondition: (NSInteger)value {
    if (nil != (self = [super init])) {
    _condition = [NSCondition new]
    _condition_value = value;
    }
    return self;
    }

    临界区

    比互斥量更加严格的同步手段。在术语中,把临界区的获取称为进入临界区,而把锁的释放称为离开临界区。与互斥量和信号量的区别:

    • (1)互斥量和信号量字系统的任何进程都是可见的。
    • (2)临界区的作用范围仅限于本进程,其他进程无法获取该锁。

    // 临界区结构对象
    CRITICAL_SECTION g_cs;
    // 共享资源
    char g_cArray[10];
    UINT ThreadProc10(LPVOID pParam)
    {
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
    g_cArray[i] = a;
    Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
    }
    UINT ThreadProc11(LPVOID pParam)
    {
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
    g_cArray[10 - i - 1] = b;
    Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
    }
    ……
    void CSample08View::OnCriticalSection()
    {
    // 初始化临界区
    InitializeCriticalSection(&g_cs);
    // 启动线程
    AfxBeginThread(ThreadProc10, NULL);
    AfxBeginThread(ThreadProc11, NULL);
    // 等待计算完毕
    Sleep(300);
    // 报告计算结果
    CString sResult = CString(g_cArray);
    AfxMessageBox(sResult);
    }

    读写锁

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);

    ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。


    #include <pthread.h>      //多线程、读写锁所需头文件
    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //定义和初始化读写锁

    写模式:
    pthread_rwlock_wrlock(&rwlock); //加写锁
    写写写……
    pthread_rwlock_unlock(&rwlock); //解锁

    读模式:
    pthread_rwlock_rdlock(&rwlock); //加读锁
    读读读……
    pthread_rwlock_unlock(&rwlock); //解锁


    • 用条件变量实现读写锁

    这里用条件变量+互斥锁来实现。注意:条件变量必须和互斥锁一起使用,等待、释放的时候都需要加锁。

    #include <pthread.h> //多线程、互斥锁所需头文件

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义和初始化互斥锁
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //定义和初始化条件变量


    写模式:
    pthread_mutex_lock(&mutex); //加锁
    while(w != 0 || r > 0)
    {
    pthread_cond_wait(&cond, &mutex); //等待条件变量的成立
    }
    w = 1;

    pthread_mutex_unlock(&mutex);
    写写写……
    pthread_mutex_lock(&mutex);
    w = 0;
    pthread_cond_broadcast(&cond); //唤醒其他因条件变量而产生的阻塞
    pthread_mutex_unlock(&mutex); //解锁


    读模式:
    pthread_mutex_lock(&mutex);
    while(w != 0)
    {
    pthread_cond_wait(&cond, &mutex); //等待条件变量的成立
    }
    r++;
    pthread_mutex_unlock(&mutex);
    读读读……
    pthread_mutex_lock(&mutex);
    r- -;
    if(r == 0)
    pthread_cond_broadcast(&cond); //唤醒其他因条件变量而产生的阻塞
    pthread_mutex_unlock(&mutex); //解锁
    • 用互斥锁实现读写锁

    这里使用2个互斥锁+1个整型变量来实现

    #include <pthread.h> //多线程、互斥锁所需头文件
    pthread_mutex_t r_mutex = PTHREAD_MUTEX_INITIALIZER; //定义和初始化互斥锁
    pthread_mutex_t w_mutex = PTHREAD_MUTEX_INITIALIZER;
    int readers = 0; //记录读者的个数

    写模式:
    pthread_mutex_lock(&w_mutex);
    写写写……
    pthread_mutex_unlock(&w_mutex);


    读模式:
    pthread_mutex_lock(&r_mutex);

    if(readers == 0)
    pthread_mutex_lock(&w_mutex);
    readers++;
    pthread_mutex_unlock(&r_mutex);
    读读读……
    pthread_mutex_lock(&r_mutex);
    readers- -;
    if(reader == 0)
    pthread_mutex_unlock(&w_mutex);
    pthread_mutex_unlock(&r_mutex);


    • 用信号量来实现读写锁

    这里使用2个信号量+1个整型变量来实现。令信号量的初始数值为1,那么信号量的作用就和互斥量等价了。

    #include <semaphore.h>     //线程信号量所需头文件

    sem_t r_sem; //定义信号量
    sem_init(&r_sem, 0, 1); //初始化信号量

    sem_t w_sem; //定义信号量
    sem_init(&w_sem, 0, 1); //初始化信号量
    int readers = 0;

    写模式:
    sem_wait(&w_sem);
    写写写……
    sem_post(&w_sem);


    读模式:
    sem_wait(&r_sem);
    if(readers == 0)
    sem_wait(&w_sem);
    readers++;
    sem_post(&r_sem);
    读读读……
    sem_wait(&r_sem);
    readers- -;
    if(readers == 0)
    sem_post(&w_sem);
    sem_post(&r_sem);



    线程的安全是现在各个领域在多线程开发必须要掌握的基础!只有对底层有所掌握,才能在真正的实际开发中游刃有余!


    收起阅读 »

    iOS标准库中常用数据结构和算法之内存池

    ⛲️内存池内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。而如果是采用内存池则可以很轻松解决这个问题:你只需要从内存池中申请这块内存,设置完内容后当...
    继续阅读 »

    ⛲️内存池

    内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。而如果是采用内存池则可以很轻松解决这个问题:你只需要从内存池中申请这块内存,设置完内容后当不需要用时你可以将这块内存放入内存池中,供其他地方在申请时进行复用,而当你再次需要时则只需要重新申请即可。内存池提供了内存分配编号而且设置脏标志的概念,当你把分配的内存放入内存池并设置脏标志后,系统就会在适当的时候将这块内存的内容写回到磁盘,这样当你再次根据内存编号来访问内存时,系统就又会从磁盘中将内容读取到内存中去。

    功能:在iOS中提供了一套内存池管理的API,你可以用这套API来实现上述的功能,而且系统内部很多功能也是借助内存池来实现内存复用和磁盘存储的。
    头文件: #include <mpool.h>, #include <db.h>
    平台: BSD系统,linux系统

    一、内存池的创建、同步和关闭

    功能:创建和关闭一个内存池对象并和磁盘文件绑定以便进行同步操作。
    函数签名:

    //创建一个内存池对象
    MPOOL * mpool_open(void *key, int fd, pgno_t pagesize, pgno_t maxcache);

    //将内存池中的脏数据同步写回到磁盘文件中
    int mpool_sync(MPOOL *mp);

    //关闭和销毁内存池对象。
    int mpool_close(MPOOL *mp);

    参数:

    key:[in] 保留字段,暂时没有用处,传递NULL即可。
    fd:[in] 内存池关联的磁盘文件句柄,文件句柄需要用open函数来打开。
    pagesize:[in] 内存池中每次申请和分配的内存的尺寸大小,单位是字节。
    maxcache:[in] 内存池中内存页的最大缓存数量。如果池中缓存的内存数量超过了最大缓存的数量就会复用已经存在的内存,而不是每次都分配新的内存。
    return:[out] 返回一个新创建的内存池对象,其他两个函数成功返回0,失败返回非0.

    描述:

    1、内存池中的内存的分配和获取是以页为单位进行的,每次分配的页的尺寸大小由pagesize指定,同时内存池也指定了最大的缓存页数量maxcache。每次从内存池中分配一页内存时,除了会返回分配的内存地址外,还会返回这一页内存的编号。这个编号对于内存池中的内存页来说是唯一的。因为内存池中的内存是可以被复用的,因此就有可能是不同的编号的内存页所得到的内存地址是相同的。

    2、每一个内存池对象都会要和一个文件关联起来,以便能够实现内存数据的永久存储和内存的复用。文件句柄必须用open函数来打开,比如下面的例子:

    int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

    3、当我们不需要使用某个内存页时或者内存页的内容有改动则我们需要将这个内存页放入回内存池中,并将页标志为脏(DIRTY)。这样系统就会在适当的时候将此内存页的数据写回到磁盘文件中,同时此内存页也会在后续被重复利用。

    4、当我们想将所有设置为脏标志的内存页立即写入磁盘时则需要调用mpool_sync函数进行同步处理。

    5、当我们不再需要内存池时,则可以通过mpool_close来关闭内存池对象,需要注意的是关闭内存池并不会将内存中的数据回写到磁盘中去。

    二、内存池中内存的获取

    功能: 从内存池中申请分配一页新的内存或者获取现有缓存中的内存。
    函数签名:

    //从内存池中申请分配一页新的内存
    void * mpool_new(MPOOL *mp, pgno_t *pgnoaddr);
    //根据内存编号页获取对应的内存。
    void * mpool_get(MPOOL *mp, pgno_t pgno, u_int flags);

    参数:

    mp:[in] 内存池对象。
    pgnoaddr:[out] 用于mpool_new函数,用于保存新分配的内存页编号。
    pngno:[in] 用于mpool_get函数,指定要获取的内存页的编号。
    flags:[in] 此参数暂时无用。
    return:[out] 返回分配或者获取的内存地址。如果分配或者获取失败则返回NULL。

    描述:

    1、无论是new还是get每次从内存池里面分配或者获取的内存页的大小都是由上述mpool_open函数中的pagesize参数指定的大小。
    2、系统内部分配的内存是用calloc函数实现的,但是我们不需要手动调用free来对内存进行释放处理。
    3、每个内存页都有一个唯一的页编号,而且每次分配的页编号也会一直递增下去。
    4、mpool_new函数申请分配新的内存时,如果当前缓存中的内存页小于maxcache数量则总是分配新的内存,只有当缓存数量大于maxcache时才会从现有的缓存中寻找一页可以被重复利用的内存页,如果没有可以重复利用的页面,则会继续分配新的内存页。
    5、mpool_get函数则根据内存页的编号获取对应的内存页。如果编号不存在则返回NULL。需要注意的是一般在获取了某一页内存后,不要进行重复获取操作,否则在DEBUG状态下会返回异常。另外一个情况是有可能相同的页编号下两次获取的内存地址是不一样的,因为系统实现内部有内存复用的机制。

    三、内存池中内存的放回

    功能:将分配或者申请的内存页放回到内存池中去,以便进行重复利用。
    函数签名:

    int  mpool_put(MPOOL *mp, void *pgaddr, u_int flags);

    参数:

    mp: [in] 内存池对象。
    pgaddr:[in] 要放入缓存的内存页地址。这个地址由mpool_get/new两个函数返回。
    flags:[in] 放回的属性,一般设置为0或者MPOOL_DIRTY。
    return:[in] 函数调用成功返回0,失败返回非0

    描述:

    1、这个函数用来将内存页放入回内存池缓存中,以便对内存进行重复利用。当将某个内存地址放入回缓存后,将不能再次访问这个内存地址了。如果要想继续访问内存中的数据则需要借助上述的mpool_get/new函数来重新获取。
    2、flags:属性如果指定为0时,表明放弃这次内存中的内容的修改,系统不会将内存中的内容写入到磁盘中,而只是将内存放入缓存中供其他地方重复使用。而如果设置为MPOOL_DIRTY时,则表明将这页内存中的数据设置为脏标志,除了同样将内存放入缓存中重复利用外,则会在适当的时候将内存中的数据写入到磁盘中,以便下次进行读取。

    四、内存池磁盘读写通知

    功能:注册回调函数,当某页内存要写回到磁盘或者要从磁盘中读取时就会调用指定的回调函数。
    函数签名:

    void mpool_filter(MPOOL *mp, void (*pgin)(void *, pgno_t, void *),
    void (*pgout)(void *, pgno_t, void *), void *pgcookie);

    参数:

    mp:[in] 内存池对象.
    pgin: [in]: 回调函数,当某个内存页的数据需要从磁盘读取时,会在读取完成后调用这个回调函数。
    pgout:[in]: 回调函数,当某个内存页的数据要到磁盘时,会在写入完成后调用这个回调函数。
    pgcookie: [in] 上述两个回调函数的附加参数。

    描述:

    因为内存池中的内存页会进行复用,以及会在适当的时候将内容同步到磁盘中,或者从磁盘中将内容读取到内存中,因此可以借助这个函数来监控这些磁盘文件和内存之间的读写操作。pgin和pgout函数的格式定义如下:

    //pgin和pgout回调函数的格式。
    //pgcookie:是mpool_filter函数中传入的参数。
    //pgno: 要进行读写的内存页编号
    //pageaddr: 要进行读写的内存地址。
    void (*pgcallback)(void *pgcookie, pgno_t pgno, void *pageaddr);

    五、实例代码

    #include <mpool.h>
    #include <db.h>

    //创建并打开一个文件。
    int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

    //创建一个内存池对象,每页的内存100个字节,最大的缓存数量为4
    MPOOL *pool = mpool_open(NULL, fd, 100, 4);


    //从内存池中分配一个新的内存页,这里对返回的内存填写数据。
    pgno_t pidx1, pidx2 = 0;
    char *mem1 = (char*)mpool_new(pool, &pidx1);
    memcpy(mem1, "aaa", 4);

    char *mem2 = (char*)mpool_new(pool, &pidx2);
    memcpy(mem2, "bbb", 4);

    //将分配的内存mem1放回内存池中,但是内容不保存到磁盘
    mpool_put(pool, mem1, 0);
    //将分配的内存mem2放回内存池中,但是内容保存到磁盘。
    mpool_put(pool, mem2, MPOOL_DIRTY);

    //经过上面的操作后mem1,mem2将不能继续再访问了,需要访问时需要再次调用mpool_get。
    mem1 = (char*)mpool_get(pool, pidx1, 0);
    mem2 = (char*)mpool_get(pool, pidx2, 0);

    //上面的mem1和mem2可能和前面的new返回的地址是不一样的。因此在内存池中不能通过地址来做唯一比较,而应该将编号来进行比较。

    //将所有设置为脏标志的内存也写回到磁盘中去。
    mpool_sync(pool);

    mpool_close(pool); //关闭内存池。

    close(fd); //关闭文件。

    内存池为iOS系统底层开发提供了一个非常重要的能力,我们可以好好利用内存池来对内存进行管理,以及一些需要进行持久化的数据也可以借助内存池来进行保存,通过内存池提高内存的重复利用率。

    转自:https://www.jianshu.com/p/34bd3e5c5b4e

    收起阅读 »

    css 加载阻塞问题,无废话

    一、问题 & 结论1. css 加载会阻塞 DOM 树的解析渲染吗 ?css 并不会阻塞 dom 树的解析css 会阻塞 dom 树的渲染2. css 加载会阻塞 js 运行吗 ?css 加载会阻塞后面 js 语句的执行二、造成的结果以及优化方案1. ...
    继续阅读 »

    一、问题 & 结论

    1. css 加载会阻塞 DOM 树的解析渲染吗 ?
    • css 并不会阻塞 dom 树的解析
    • css 会阻塞 dom 树的渲染
    2. css 加载会阻塞 js 运行吗 ?
    • css 加载会阻塞后面 js 语句的执行

    二、造成的结果以及优化方案

    1. 造成的结果
    • css 加载缓慢会造成长时间的白屏

    2. 优化方案
    1. CDN 加速:CDN 会根据网络状况,挑选一个最近的具有缓存内容的节点提供资源,减少加载时间
    2. 对 css 进行压缩:使用打包工具 webpack、gulp 等,开启 gzip 压缩
    3. 合理的使用缓存:强缓存、协商缓存等策略
    4. 减少 http 请求次数,合并 css 文件,或者干脆写成内联样式(缺点:不能缓存)

    三、原理解析

    1. 浏览器的渲染过程如下图所示:


    2. 结论如下:


    • DOM 解析和 css 解析是两个独立并行的进行,所以 css 的加载不会阻塞 DOM 的解析

    • 由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。因此,CSS 加载是会阻塞 Dom 的渲染的

    • 由于 js 可能会操作之前的 Dom 节点和 css 样式,因此浏览器会维持 html 中 css 和 js 的顺序。因此,样式表会在后面的 js 执行前先加载执行完毕。所以 css 会阻塞后面 js 的执行

    四、实际场景


    1. 页面加载的两个事件


    • onLoad:等待页面的所有资源都加载完成才会触发,这些资源包括 css、js、图片视频等

    • DOMContentLoaded:就是当页面的内容解析完成后,则触发该事件


    2. css 加载的影响


    • 如果页面中同时存在 css 和 js,并且存在 js 在 css 后面,则 DOMContentLoaded 事件会在 css 加载完后才执行

    • 其他情况下,DOMContentLoaded 都不会等待 css 加载,并且 DOMContentLoaded 事件也不会等待图片、视频等其他资源加载

    链接:https://juejin.cn/post/6967530166520119332





    收起阅读 »