从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空
- 判断是否为元类
class_isMetaClass(objcL);
let button = document.getElemetById('button')
button.onclick = function () {
console.log(this) //button对象
}
全局环境
浏览器环境下
console.log(this) // window
node环境下
console.log(this) // module.exports
var object = {
name: 'object',
getName: function() {
console.log(this)
}
}
var bar = object.getName // 只是函数声明并未调用
object.getName() // object对象
window.object.getName() // object对象
/* 函数被多层对象所包含,如果函数被最外层对象调用,this指向
的也只是它上一级的对象。*/
bar() // window对象
let fn = function(){
this.id = 'xiaoMing'
console.log(this.id)
}
let fn1 = new fn() //this指向fn1对象
new 的内部原理
【1】创建一个空对象 obj;
【2】把 child 的__proto__ 指向构造函数 parent 的原型对象 prototype,此时便建立了 obj 对象的原型链:child ->parent.prototype->Object.prototype->null
【3】在 child 对象的执行环境调用 parent 函数并传递参数。
【4】考察第 3 步的返回值,如果无返回值 或者 返回一个非对象值,则将 child 作为新对象返回;否则会将 result 作为新对象返回。this绑定的是返回的对象。
function fn(){
this.num = 10;
}
fn.num = 20;
fn.prototype.num = 30;
fn.prototype.method = function() {
console.log(this.num);
}
var prototype = fn.prototype
var method = prototype.method
new fn().method() // 10
prototype.method() // 30
method() // undefined
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。
除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。
不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
func.bind(this, arg1, arg2)()
原文链接:https://blog.csdn.net/weixin_45495667/article/details/108801000
收起阅读 »最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。
主要实现图中所示的功能,话不多少搞起!
第一次写大神们多包涵呀
下面这个方法计算传进来的字符串数组,实现每个字符串长度的计算,并做换行判断,每一行存进统计数组中
//将标签数组根据type以及其他参数进行分组装入数组
- (void)disposeTags:(NSArray *)aryName aryCount:(NSArray *)aryCount{
NSMutableArray *tags = [NSMutableArray new];//纵向数组
NSMutableArray *subTags = [NSMutableArray new];//横向数组
float originX = _tagOriginX;
for (NSString *tagTitle in aryName) {
NSUInteger index = [aryName indexOfObject:tagTitle];
//计算每个tag的宽度
CGSize contentSize = [tagTitle fdd_sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(self.frame.size.width-_tagOriginX*2, MAXFLOAT)];
NSMutableDictionary *dict = [NSMutableDictionary new];
dict[@"tagTitle"] = tagTitle;//标签标题
dict[@"tagCount"] =aryCount[index];
dict[@"viewWith"] = [NSString stringWithFormat:@"%f",contentSize.width+_tagSpace+30];//标签的宽度
if (index == 0) {
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
} else {
if (originX + contentSize.width > self.frame.size.width-_tagOriginX*2) {
//当前标签的X坐标+当前标签的长度>屏幕的横向总长度则换行
[tags addObject:subTags];
//换行标签的起点坐标初始化
originX = _tagOriginX;
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
subTags = [NSMutableArray new];
[subTags addObject:dict];
} else {
//如果没有超过屏幕则继续加在前一个数组里
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
}
}
if (index +1 == aryName.count) {
//最后一个标签加完将横向数组加到纵向数组中
[tags addObject:subTags];
disposeAry = tags;
}
//标签的X坐标每次都是前一个标签的宽度+标签左右空隙+标签距下个标签的距离
originX += contentSize.width+_tagHorizontalSpace+_tagSpace+30;
}
}
下面这个方法是计算字符串长度的封装方法,只限字符串计算,用的话可以直接搬走
#pragma mark - 扩展方法
@implementation NSString (FDDExtention)
- (CGSize)fdd_sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
CGSize resultSize;
if ([self respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSDictionary *attributes = @{ NSFontAttributeName:font };
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin;
NSStringDrawingContext *context;
[invocation setArgument:&size atIndex:2];
[invocation setArgument:&options atIndex:3];
[invocation setArgument:&attributes atIndex:4];
[invocation setArgument:&context atIndex:5];
[invocation invoke];
CGRect rect;
[invocation getReturnValue:&rect];
resultSize = rect.size;
} else {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(sizeWithFont:constrainedToSize:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(sizeWithFont:constrainedToSize:)];
[invocation setArgument:&font atIndex:2];
[invocation setArgument:&size atIndex:3];
[invocation invoke];
[invocation getReturnValue:&resultSize];
}
return resultSize;
}
字符串长度计算完成了,下面就可以进行所有字符串排列的高度了。其实这个蛮简单的 ,统计数组有多少个元素就代表标签排布有多少行。
//获取处理后的tagsView的高度根据标签的数组
(float)getDisposeTagsViewHeight:(NSArray *)ary {
float height = 0;
if (disposeAry.count > 0) {
height = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);
}
return height;
}
下面这个将标签加载到view上了,并实现赋值。
-(void)setTagAryName:(NSArray *)tagAryName aryCount:(NSArray *)aryCount delegate:(id)delegate{
_tagDelegate=delegate;
[self disposeTags:tagAryName aryCount:aryCount];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, 0, 200, 30)];
label.text=@"事件类别统计:";
label.textColor=_titleColor;
label.textAlignment=NSTextAlignmentLeft;
label.adjustsFontSizeToFitWidth=YES;
[self addSubview:label];
//遍历标签数组,将标签显示在界面上,并给每个标签打上tag加以区分
for (NSArray *iTags in disposeAry) {
NSUInteger i = [disposeAry indexOfObject:iTags];
for (NSDictionary *tagDic in iTags) {
NSUInteger j = [iTags indexOfObject:tagDic];
NSString *tagTitle = tagDic[@"tagTitle"];
float originX = [tagDic[@"originX"] floatValue];
float viewWith = [tagDic[@"viewWith"] floatValue];
NSString *count = tagDic[@"tagCount"];
UIView *NCView=[[UIView alloc]initWithFrame:CGRectMake(originX, _tagOriginY+i*(_tagHeight+_tagVerticalSpace), viewWith, _tagHeight)];
[self addSubview:NCView];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame=CGRectMake(0, 0, viewWith-30, _tagHeight);
button.layer.borderColor = _borderColor.CGColor;
button.layer.borderWidth = _borderWidth;
button.layer.masksToBounds = _masksToBounds;
button.layer.cornerRadius = _cornerRadius;
button.titleLabel.font = [UIFont systemFontOfSize:_titleSize];
[button setTitle:tagTitle forState:UIControlStateNormal];
[button setTitleColor:_titleColor forState:UIControlStateNormal];
[button setBackgroundImage:_normalBackgroundImage forState:UIControlStateNormal];
[button setBackgroundImage:_highlightedBackgroundImage forState:UIControlStateHighlighted];
button.tag = i*iTags.count+j;
[button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(viewWith-30, 0, 30, _tagHeight)];
label.text=[NSString stringWithFormat:@"X%@",count];
label.textAlignment=NSTextAlignmentCenter;
label.adjustsFontSizeToFitWidth=YES;
label.textColor=_titleColor;
[NCView addSubview:button];
[NCView addSubview:label];
}
}
self.countLabel=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, [self getDisposeTagsViewHeight:disposeAry], 100, 30)];
self.countLabel.text=[NSString stringWithFormat:@"总计:%ld次",_times];
self.countLabel.textAlignment=NSTextAlignmentLeft;
self.countLabel.adjustsFontSizeToFitWidth=YES;
self.countLabel.textColor=_titleColor;
[self addSubview:self.countLabel];
if (disposeAry.count > 0) {
float contentSizeHeight = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);
self.contentSize = CGSizeMake(self.frame.size.width,contentSizeHeight);
}
if (self.frame.size.height <= 0) {
self.frame = CGRectMake(CGRectGetMinX([self frame]), CGRectGetMinY([self frame]), CGRectGetWidth([self frame]), [self getDisposeTagsViewHeight:disposeAry]+30);
}
}
说了这么多,如何调用呢? 那么亮点来了 只需要传进来数组就实现图中的功能。
//计算出全部展示的高度,让maxHeight等于计算出的高度即可,初始化不需要设置高度
NSArray *tagAryCount=@[@"10",@"8",@"7",@"9",@"2",@"4",@"5",@"3",@"4"];
NSArray* tagAryName = @[@"黑色玫瑰",@"比尔沃吉特",@"钢铁烈阳",@"德玛西亚",@"祖安",@"巨神峰",@"雷瑟守备祖安",@"诺克萨斯暗影岛",@"弗雷尔卓德"];
ZHLbView * tagsView = [[ZHLbView alloc] initWithFrame:CGRectMake(0, 200, self.view.frame.size.width, 0)];
[tagsView setTagAryName:tagAryName aryCount:tagAryCount delegate:self];
[self.view addSubview:tagsView];
补充,在demo编写的过程中 ,遇到了一些小问题,就是scrollView如果是VC的第一个子视图的话 其总是要有一个64高度的空白区。
示例图:
一个实用的多条件筛选菜单,在很多App上都能看到这个效果,如美团,爱奇艺电影票等
我的博客 自己造轮子--android常用多条件帅选菜单实现思路(类似美团,爱奇艺电影票下拉菜单)
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
dependencies {
compile 'com.github.dongjunkun:DropDownMenu:1.0.4'
}
添加DropDownMenu 到你的布局文件,如下
<com.yyydjk.library.DropDownMenu
android:id="@+id/dropDownMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:ddmenuTextSize="13sp" //tab字体大小
app:ddtextUnselectedColor="@color/drop_down_unselected" //tab未选中颜色
app:ddtextSelectedColor="@color/drop_down_selected" //tab选中颜色
app:dddividerColor="@color/gray" //分割线颜色
app:ddunderlineColor="@color/gray" //下划线颜色
app:ddmenuSelectedIcon="@mipmap/drop_down_selected_icon" //tab选中状态图标
app:ddmenuUnselectedIcon="@mipmap/drop_down_unselected_icon"//tab未选中状态图标
app:ddmaskColor="@color/mask_color" //遮罩颜色,一般是半透明
app:ddmenuBackgroundColor="@color/white" //tab 背景颜色
app:ddmenuMenuHeightPercent="0.5" 菜单的最大高度,根据屏幕高度的百分比设置
...
/>
我们只需要在java代码中调用下面的代码
//tabs 所有标题,popupViews 所有菜单,contentView 内容
mDropDownMenu.setDropDownMenu(tabs, popupViews, contentView);
原文链接:https://github.com/dongjunkun/DropDownMenu
类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用
@interface Person : NSObject
-(void)perMethod;
+(void)perEat;
@end
@implementation Person
-(void)perMethod{
}
+(void)perEat{
}
@end
/// student继承自Person
@interface Student : Person
-(void)StudentMethod;
+(void)StudentEat;
@end
@implementation Student
-(void)StudentMethod{
}
+(void)StudentEat{
}
Student *student = [[Student alloc]init];
[student StudentMethod]
Student *student = [[Student alloc]init];
[student perMethod];当子类调用父类的实例方法的时候,子类的class类对象的superclass指针指向父类,直至基类(NSObject)找到方法并执行(注意,这里指的是实例方法,也就是减号方法)
三.元类对象的superclass 指针
当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用
[Student perEat];
总的来说,isa,superclass的的关系可以用一副经典的图来表示
function debounce(callback, wait) {
let timer = null;
return function(){
let _this = this,
arg = arguments;
if(!!timer){
clearTimeout(timer)
timer = null
}
timer = setTimeout(function(){
callback.call(_this,arg)
},wait)
}
}
let _debounce = debounce(function() {
console.log('dshdihdi')
}, 1000)
// 函数节流,时间戳版本
function throttle(callback, wait) {
let time = new Date().getTime();
return function() {
let _this = this,
arg = arguments;
let nowTime = new Date().getTime();
if (nowTime - time >= wait){
callback.call(_this,...arg)
time = nowTime
}
}
}
let _throttle = throttle(function(e) {
console.log(e)
console.log(arguments)
}, 1000)
收起阅读 »
OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)
NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];
NSLog(@"instance----%p %p",objc1,objc2);
instance实例对象存储的信息:
1.isa指针
Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);
打印结果
2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个
类对象存储的信息:
1.isa指针
2.superclass指针
3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)
1.元类对象的获取
Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);
打印指针地址
NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0
获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个
Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];
无论class几次,它返回的始终是类对象
2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140
元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)
从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空
将原来的load方法换成initialize
相信大家在想什么叫第一次接收消息了,我们回到main()
从输出结果可以看到没有任何关于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发送消息的输出结果
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的调用
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
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
#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
#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
new 操作符做的事情 - 01
new 操作符做的事情 - 02
- 创建一个全新的对象 (无原型的Object.create(null)) 。
- 目的是保存 new 出来的实例的所有属性。
- 将构造函数的原型赋值给新创建的对象的原型。
- 目的是将构造函数原型上的属性继承下来。
- 调用构造函数,并将 this 指向新建的对象。
- 目的是让构造函数内的属性全部转交到该对象上,使得 this 指向改变,方法有三 : apply、call、bind 。
- 判断构造函数调用的方式,如果是 new 的调用方式,则返回经过加工后的新对象,如果是普通调用方式,则直接返回构造函数调用时的返回值。
function myNew(Constructor, ...args) {
// 判断 Constructor 参数是否是函数
if (typeof Constructor !== 'function') {
return 'Constructor.apply is not a function';
};
// 1、创建了一个全新的对象。
let newObject = {};
// 2、将对象链接到这个函数的 prototype 对象上。
newObject.__proto__ = Constructor.prototype;
// 此处是把 1 / 2 步结合到一起
// const newObject = Object.create(Constructor.prototype);
// 3、执行构造函数,
// 并将 this 绑定到新创建的对象上。
let result = Constructor.apply(newObject, args);
// 4. 判断构造函数执行返回的结果是否是引用数据类型,
// 若是则返回构造函数执行的结果,
// 否则返回创建的对象。
if ((result !== null && typeof result === 'object') || (typeof result === 'function')) {
return result;
} else {
return newObject;
};
};
// 需要被 new 的函数
function NewTest(args) {
this.dataValue = args;
return this;
};
// 定义参数
let dataObj = {
sname: '杨万里',
number: 3,
web: 'vue'
};
let dataArray = [5, 'uniApp', '范仲淹'];
// 执行 myNew 函数
let test = myNew(NewTest, 1, 'JavaScript', '辛弃疾', dataObj, dataArray);
console.log(test); // NewTest {dataValue: Array(5)}
本文链接:https://blog.csdn.net/weixin_51157081/article/details/115577751
#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
#import
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里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出结果:
从输出结果我们可以抛光,三个负载方法都被调用
#import
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
分类重写测试
#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
#import
#import "TCPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
}
return 0;
}
我们打印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;
}
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函数指针直接调用
函数指针直接调用
其分类load方法调用也是一样
UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车
,视频收藏
等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。
tableView.editing = YES
。// 取消
[self.tableView setEditing:YES animated:NO];
UITableViewDelegate
中的- tableview:editingStyleForRowAtIndexPath:
方法,在里面返回删除模式。如果不实现,默认返回的就是删除模式。-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 删除
return UITableViewCellEditingStyleDelete;
}
UITableViewDelegate
中的- tableview:commitEditingStyle:editing StyleForRowAtIndexPath:
方法。只要实现此方法,即默认实现了系统横扫出现删除按钮的删除方法。-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
//只要实现这个方法,就实现了默认滑动删除!!!!!
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// 删除数据
[self _deleteSelectIndexPath:indexPath];
}
}
-tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:
方法。-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return @"删除";
}
删除模式
无效,反而成为多选模式
。tableView.allowsMultipleSelection = NO;
tableView.allowsSelectionDuringEditing = NO;
tableView.allowsMultipleSelectionDuringEditing = NO;
编辑
按钮的bug。一、OpenGL中的坐标系
1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)
2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形
3. OpenGL的投影方式有:
透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果
正投影:只能用来设置平面图形
4.坐标系的分类:
① 惯性坐标系:没有什么参考价值,只要用来平移到世界坐标系
② 世界坐标系:大环境中的位置,系统的绝对坐标
③ 物体空间坐标系(局部空间):局部空间中的位置
④ 摄像机坐标系:观察空间(观察者)
5.坐标系之间的变换
① 物体坐标系,模型变换
② 转换到世界坐标系,视变换
③ 转换到观察者坐标系/摄像机坐标系,投影变换
④ 转换到裁剪坐标系,透视除法
⑤ 转换到规范化设备坐标,视口变换
⑥ 转换到屏幕坐标
注:① - ④ 是可以由开发者自定义完成的,⑤ - ⑥是有系统OpenGL来完成的
二、着色器的渲染流程
1.顶点数据
2.顶点着色器:接收顶点数据,单独处理每个顶点
3.细分着色器:
① 可选:描述物体形状,在管线中生成新的饿几何平面模型,生成最终形态
② 组分控制着色器/细分计算着色器:对所有的图形进行修改几何图元类型或者放弃所有图像
4.几何着色器
5.图元设置
6.剪切:剪切窗口之外的绘制
7.光栅化:输入图元的数学描述,转化为与屏幕对应位置像素片元,简称光栅化
8.片元着色器:片元着色器以及深度值,然后传递到片元测试和混合模块
9.效果
三、补充几个知识点
1.图片的渲染流程:
① GPU解码图片
② GPU纹理混合、顶点计算、像素点填充计算、渲染到帧缓冲区
③ 时钟信号:垂直同步、水平同步
④ iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制
2.MVP矩阵:
① model:物体的一些变化
② view:观察
③ projection:3D矩阵
3.强制解压缩:对图片进行重新绘制,得到一张新的压缩后的位图,其中用到的最核心的函数是:GCBitmapContextCreate
收起阅读 »需求, 使用canvas绘制图片和文字, 生成图片
<!--pages/canvas/canvas.wxml-->
<view>
<view class="container">
<button bind:tap="start" class="start" size="mini">选择图片</button>
<button bind:tap="downloadCanvas" class="downloadCanvas" size="mini">下载</button>
<button bind:tap="share" class="share" size="mini">分享</button>
<!-- 分享图片功能 -->
</view>
<text>canvas区域</text>
<canvas style='width:{{canvasWidth}}px;height:{{canvasHeight}}px;border: 1px solid grey;' canvas-id='myCanvas'></canvas>
<image v-if="previewImage" :src="{{canvasImage}}"></image>
</view>
先设置canvas区域的宽度和高度
js代码
data: {
title: 'canvas绘制图片',
canvasWidth: '', // canvas宽度
canvasHeight: '', // canvas高度
imagePath: '', // 分享的图片路径
leftMargin: 0,
topMargin: 0,
imgInfo: {},
ctx: [],
canvasImage: '',
previewImage: false,
imgProportion: 0.8, // 图片占canvas画布宽度百分比
imgToTop: 100 // 图片到canvas顶部的距离
},
onLoad: function(options) {
var that = this
var sysInfo = wx.getSystemInfo({
success: function(res) {
that.setData({
canvasWidth: res.windowWidth,
// 我这里选择canvas高度是系统高度的80%
canvasHeight: res.windowHeight * 0.8
})
// 根据图片比例, 使图片居中
let leftMargin = (res.windowWidth * (1 - that.data.imgProportion)) / 2
that.setData({
leftMargin
})
}
})
}
接下来下选择图片
// 点击选择图片按钮触发
start: function() {
let that = this
let ctx = wx.createCanvasContext('myCanvas')
// 设置canvas背景色, 否则制作的图片是透明的
ctx.setFillStyle('#f8f8f8')
ctx.fillRect(0, 0, that.data.canvasWidth, that.data.canvasHeight)
this.addImage(ctx)
},
// 添加图片
addImage: function(ctx) {
var that = this;
let imgInfo = that.data.imgInfo
var path
wx.chooseImage({
count: '1',
success(res) {
wx.getImageInfo({
src: res.tempFilePaths[0],
success: function(response) {
// 返回的response里有图片的临时路径和图片信息(高度/宽度)
that.setData({
imgInfo: response,
path: response.path
})
that.drawImage(ctx)
}
})
}
})
this.addTitle(ctx)
},
绘制文字和图
// 绘制图片
drawImage(ctx) {
let that = this
let imgInfo = that.data.imgInfo
let path = that.data.path
// 计算图片宽度 宽度固定 高度等比缩放
let imgWidth = that.data.canvasWidth * that.data.imgProportion
let imgHeight = imgInfo.height / imgInfo.width * imgWidth
// drawImage参数, 下面会说明
ctx.drawImage(path, 0, 0, imgInfo.width, imgInfo.height, that.data.leftMargin, that.data.imgToTop, imgWidth, imgHeight)
ctx.draw()
that.data.previewImage = true
},
//绘制文字
addTitle: function(ctx) {
var str = this.data.title
ctx.font = 'normal bold 16px sans-serif';
ctx.setTextAlign('center'); // 文字居中
ctx.setFillStyle("#222222");
ctx.fillText(str, this.data.canvasWidth / 2, 45) // 文字位置
},
drawImage 方法允许在 canvas 中插入其他图像( img 和 canvas 元素) 。drawImage函数有三种函数原型:
drawImage(image, dx, dy) 在画布指定位置绘制原图
drawImage(image, dx, dy, dw, dh) 在画布指定位置上按原图大小绘制指定大小的图
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) 剪切图像,并在画布上定位被剪切的部分 从 1.9.0 起支持
image
所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载)
s
需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 x 坐标
sy
需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 y 坐标
sWidth
需要绘制到画布中的,image的矩形(裁剪)选择框的宽度
sHeight
需要绘制到画布中的,image的矩形(裁剪)选择框的高度
dx
image的左上角在目标 canvas 上 x 轴的位置
dy
image的左上角在目标 canvas 上 y 轴的位置
dWidth
在目标画布上绘制imageResource的宽度,允许对绘制的image进行缩放
dHeight
在目标画布上绘制imageResource的高度,允许对绘制的image进行缩放
//点击下载按钮保存canvas图片
downloadCanvas: function() {
let that = this;
// 判断用户是否选择了图片
if (that.data.previewImage) {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success(res) {
console.log(res, '保存')
}
})
}
});
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}
// 分享图片
share() {
let that = this
if (that.data.previewImage) {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.showToast({
icon: 'none',
title: '长按图片分享',
duration: 1500
})
setTimeout(
() => {
wx.previewImage({
urls: [res.tempFilePath]
})
}, 1000)
}
})
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}
原文链接:https://www.jianshu.com/p/1e54146b8a26
收起阅读 »<script src="mshare.js"></script>//引进mshare.js
<button data-mshare="0">点击弹出原生分享面板</button>
<button data-mshare="1">点击触发朋友圈分享</button>
<button data-mshare="2">点击触发发送给微信朋友</button>
<script>
var mshare = new mShare({
title: 'Lorem ipsum dolor sit.',
url: 'http://m.ly.com',
desc: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat inventore minima voluptates.',
img: 'http://placehold.it/150x150'
});
$('button').click(function () {
// 1 ==> 朋友圈 2 ==> 朋友 0 ==> 直接弹出原生
mshare.init(+$(this).data('mshare'));
});
</script>
/**
* 此插件主要作用是在UC和QQ两个主流浏览器
* 上面触发微信分享到朋友圈或发送给朋友的功能
*/
'use strict';
var UA = navigator.appVersion;
/**
* 是否是 UC 浏览器
*/
var uc = UA.split('UCBrowser/').length > 1 ? 1 : 0;
/**
* 判断 qq 浏览器
* 然而qq浏览器分高低版本
* 2 代表高版本
* 1 代表低版本
*/
var qq = UA.split('MQQBrowser/').length > 1 ? 2 : 0;
/**
* 是否是微信
*/
var wx = /micromessenger/i.test(UA);
/**
* 浏览器版本
*/
var qqVs = qq ? parseFloat(UA.split('MQQBrowser/')[1]) : 0;
var ucVs = uc ? parseFloat(UA.split('UCBrowser/')[1]) : 0;
/**
* 获取操作系统信息 iPhone(1) Android(2)
*/
var os = (function () {
var ua = navigator.userAgent;
if (/iphone|ipod/i.test(ua)) {
return 1;
} else if (/android/i.test(ua)) {
return 2;
} else {
return 0;
}
}());
/**
* qq浏览器下面 是否加载好了相应的api文件
*/
var qqBridgeLoaded = false;
// 进一步细化版本和平台判断
if ((qq && qqVs < 5.4 && os == 1) || (qq && qqVs < 5.3 && os == 1)) {
qq = 0;
} else {
if (qq && qqVs < 5.4 && os == 2) {
qq = 1;
} else {
if (uc && ((ucVs < 10.2 && os == 1) || (ucVs < 9.7 && os == 2))) {
uc = 0;
}
}
}
/**
* qq浏览器下面 根据不同版本 加载对应的bridge
* @method loadqqApi
* @param {Function} cb 回调函数
*/
function loadqqApi(cb) {
// qq == 0
if (!qq) {
return cb && cb();
}
var script = document.createElement('script');
script.src = (+qq === 1) ? '//3gimg.qq.com/html5/js/qb.js' : '//jsapi.qq.com/get?api=app.share';
/**
* 需要等加载过 qq 的 bridge 脚本之后
* 再去初始化分享组件
*/
script.onload = function () {
cb && cb();
};
document.body.appendChild(script);
}
/**
* UC浏览器分享
* @method ucShare
*/
function ucShare(config) {
// ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID']
// 关于platform
// ios: kWeixin || kWeixinFriend;
// android: WechatFriends || WechatTimeline
// uc 分享会直接使用截图
var platform = '';
var shareInfo = null;
// 指定了分享类型
if (config.type) {
if (os == 2) {
platform = config.type == 1 ? 'WechatTimeline' : 'WechatFriends';
} else if (os == 1) {
platform = config.type == 1 ? 'kWeixinFriend' : 'kWeixin';
}
}
shareInfo = [config.title, config.desc, config.url, platform, '', '', ''];
// android
if (window.ucweb) {
ucweb.startRequest && ucweb.startRequest('shell.page_share', shareInfo);
return;
}
if (window.ucbrowser) {
ucbrowser.web_share && ucbrowser.web_share.apply(null, shareInfo);
return;
}
}
/**
* qq 浏览器分享函数
* @method qqShare
*/
function qqShare(config) {
var type = config.type;
//微信好友 1, 微信朋友圈 8
type = type ? ((type == 1) ? 8 : 1) : '';
var share = function () {
var shareInfo = {
'url': config.url,
'title': config.title,
'description': config.desc,
'img_url': config.img,
'img_title': config.title,
'to_app': type,
'cus_txt': ''
};
if (window.browser) {
browser.app && browser.app.share(shareInfo);
} else if (window.qb) {
qb.share && qb.share(shareInfo);
}
};
if (qqBridgeLoaded) {
share();
} else {
loadqqApi(share);
}
}
/**
* 对外暴露的接口函数
* @method mShare
* @param {Object} config 配置对象
*/
function mShare(config) {
this.config = config;
this.init = function (type) {
if (typeof type != 'undefined') this.config.type = type;
try {
if (uc) {
ucShare(this.config);
} else if (qq && !wx) {
qqShare(this.config);
}
} catch (e) {}
}
}
// 预加载 qq bridge
loadqqApi(function () {
qqBridgeLoaded = true;
});
if (typeof module === 'object' && module.exports) {
module.exports = mShare;
} else {
window.mShare = mShare;
}
收起阅读 »
在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案:
plist文件(序列化)
preference(偏好设置)
NSKeyedArchiver(归档)
SQLite3
FMDB
CoreData
每个APP的沙盒下面都有相似目录结构,如图
下面的代码得到的是应用程序目录的路径,在该目录下有三个文件夹:Documents、Library、temp以及一个.app包!该目录下就是应用程序的沙盒,应用程序只能访问该目录下的文件夹!!!
NSString *path = NSHomeDirectory();
1、Documents 目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。
2、AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
3、Library 目录:这个目录下有两个子目录:
Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.
Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。
4、tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。
// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
// 获取Documents目录路径
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library的目录路径
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// 获取Caches目录路径
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取tmp目录路径
NSString *tmpDir = NSTemporaryDirectory();
//获取应用程序程序包中资源文件路径的方法
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"png"];
UIImage *appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];
可以被序列化的类型只有如下几种:
NSArray; //数组
NSMutableArray; //可变数组
NSDictionary; //字典
NSMutableDictionary; //可变字典
NSData; //二进制数据
NSMutableData; //可变二进制数据
NSString; //字符串
NSMutableString; //可变字符串
NSNumber; //基本数据
NSDate; //日期
数据存储与读取的实例:
/**
写入数据到plist
*/
- (void)writeToPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"写入数据地址%@",path);
NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
NSArray *array = @[@"123", @"王佳佳", @"iOS"];
//序列化,把数组存入plist文件
[array writeToFile:fileName atomically:YES];
NSLog(@"写入成功");
}
/**
从plist读取数据
@return 读出数据
*/
- (NSArray *)readFromPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"读取数据地址%@",path);
NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
//反序列化,把plist文件数据读取出来,转为数组
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
return result;
}
存储时使用writeToFile:atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。
Preference通常用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。
数据存储与读取的实例:
- (void)writeToPreference{
//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向偏好设置中写入内容
[userDefaults setObject:@"wangjiajia" forKey:@"name"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
NSString *path = NSHomeDirectory();
}
- (void)readFromPreference{
//获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//读取偏好设置
NSString *name = [userDefaults objectForKey:@"name"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
}
使用偏好设置对数据进行保存,它保存的时间是不确定的,会在将来某一时间自动将数据保存到 Preferences 文件夹下,如果需要即刻将数据存储,使用 [defaults synchronize]。
Preference(偏好设置)
和plist文件(序列化)
都是保存在plist
文件中,但是plist文件(序列化)
操作读取时需要把整个plist文件都进行读取,而Preference(偏好设置)
可以直接通过key-value
单个读取。
要使用归档,其归档对象必须实现NSCoding协议
。
NSCoding协议声明的两个方法都必须实现。
encodeWithCoder:用来说明如何将对象编码到归档中。
initWithCoder:用来说明如何进行解档来获取一个新对象。
数据存储与读取的实例:
/**
归档
*/
- (void)keyedArchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.name = @"wangjiajia";
[NSKeyedArchiver archiveRootObject:person toFile:file];
}
/**
解档
*/
- (void)keyedUnarchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
NSLog(@"name:%@",person.name);
}
}
以下代码块将会介绍sqlite3.h
中主要的API。
/* 打开数据库 */
int sqlite3_open(
const char *filename, /* 数据库路径(UTF-8) */
sqlite3 **pDb /* 返回的数据库句柄 */
);
/* 执行没有返回的SQL语句 */
int sqlite3_exec(
sqlite3 *db, /* 数据库句柄 */
const char *sql, /* SQL语句(UTF-8) */
int (*callback)(void*,int,char**,char**), /* 回调的C函数指针 */
void *arg, /* 回调函数的第一个参数 */
char **errmsg /* 返回的错误信息 */
);
/* 执行有返回结果的SQL语句 */
int sqlite3_prepare_v2(
sqlite3 *db, /* 数据库句柄 */
const char *zSql, /* SQL语句(UTF-8) */
int nByte, /* SQL语句最大长度,-1表示SQL支持的最大长度 */
sqlite3_stmt **ppStmt, /* 返回的查询结果 */
const char **pzTail /* 返回的失败信息*/
);
/* 关闭数据库 */
int sqlite3_close(sqlite3 *db);
处理SQL返回结果的一些API
#pragma mark - 定位记录的方法
/* 在查询结果中定位到一条记录 */
int sqlite3_step(sqlite3_stmt *stmt);
/* 获取当前定位记录的字段名称数目 */
int sqlite3_column_count(sqlite3_stmt *stmt);
/* 获取当前定位记录的第几个字段名称 */
const char * sqlite3_column_name(sqlite3_stmt *stmt, int iCol);
# pragma mark - 获取字段值的方法
/* 获取二进制数据 */
const void * sqlite3_column_blob(sqlite3_stmt *stmt, int iCol);
/* 获取浮点型数据 */
double sqlite3_column_double(sqlite3_stmt *stmt, int iCol);
/* 获取整数数据 */
int sqlite3_column_int(sqlite3_stmt *stmt, int iCol);
/* 获取文本数据 */
const unsigned char * sqlite3_column_text(sqlite3_stmt *stmt, int iCol);
由于其他API相对来说比较简单,这里就只给出执行有返回结果的SQL语句的实例
/* 执行有返回值的SQL语句 */
- (NSArray *)executeQuery:(NSString *)sql{
NSMutableArray *array = [NSMutableArray array];
sqlite3_stmt *stmt; //保存查询结果
//执行SQL语句,返回结果保存在stmt中
int result = sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL);
if (result == SQLITE_OK) {
//每次从stmt中获取一条记录,成功返回SQLITE_ROW,直到全部获取完成,就会返回SQLITE_DONE
while( SQLITE_ROW == sqlite3_step(stmt)) {
//获取一条记录有多少列
int columnCount = sqlite3_column_count(stmt);
//保存一条记录为一个字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
//获取第i列的字段名称
const char *name = sqlite3_column_name(stmt, i);
//获取第i列的字段值
const unsigned char *value = sqlite3_column_text(stmt, i);
//保存进字典
NSString *nameStr = [NSString stringWithUTF8String:name];
NSString *valueStr = [NSString stringWithUTF8String:(const char *)value];
dict[nameStr] = valueStr;
}
[array addObject:dict];//添加当前记录的字典存储
}
sqlite3_finalize(stmt);//stmt需要手动释放内存
stmt = NULL;
NSLog(@"Query Stmt Success");
return array;
}
NSLog(@"Query Stmt Fail");
return nil;
}
在使用数据库存储时主要存在以下步骤。
1、创建数据库
2、创建数据表
3、数据的“增删改查”操作
4、关闭数据库
在使用sqlite3时,数据表的创建及数据的增删改查都是通过sql语句实现的。下面是一些常用的SQL语句。
创建表:
create table 表名称(字段1,字段2,……,字段n,[表级约束])[TYPE=表类型];
插入记录:
insert into 表名(字段1,……,字段n) values (值1,……,值n);
删除记录:
delete from 表名 where 条件表达式;
修改记录:
update 表名 set 字段名1=值1,……,字段名n=值n where 条件表达式;
查看记录:
select 字段1,……,字段n from 表名 where 条件表达式;
FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便。
FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。
FMResultSet :执行查询一个FMDatabase结果集。
FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。
创建数据库
打开数据库、关闭数据库
执行更新的SQL语句
执行查询的SQL语句
/**
创建数据库
*/
- (void)createDatabase{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
NSLog(@"数据库路径:%@",filePath);
/**
1. 如果该路径下已经存在该数据库,直接获取该数据库;
2. 如果不存在就创建一个新的数据库;
3. 如果传@"",会在临时目录创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
4. 如果传nil,会在内存中临时创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
*/
self.database = [FMDatabase databaseWithPath:filePath];
}
/* 打开数据库,成功返回YES,失败返回NO */
- (BOOL)open;
/* 关闭数据库,成功返回YES,失败返回NO */
- (BOOL)close;
在FMDB里除了查询操作,其他数据库操作都称为更新。而更新操作FMDB给出以下四种方法。
/* 1. 直接使用完整的SQL更新语句 */
[self.database executeUpdate:@"insert into mytable(num,name,sex) values(0,'wangjiajia1','m');"];
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
/* 2. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要后面的参数进行替代 */
[self.database executeUpdate:sql,@1,@"wangjiajia2",@"m"];
/* 3. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要数组参数里面的参数进行替代 */
[self.database executeUpdate:sql
withArgumentsInArray:@[@2,@"wangjiajia3",@"m"]];
/* 4. SQL语句字符串可以使用字符串格式化,这种我们应该比较熟悉 */
[self.database executeUpdateWithFormat:@"insert into mytable(num,name,sex) values(%d,%@,%@);",4,@"wangjiajia4",@"m"];
查询方法与更新方法类似,只不过查询方法存在返回值,可以通过返回值给出的相应方法获取需要的数据。
/* 执行查询SQL语句,返回FMResultSet查询结果 */
- (FMResultSet *)executeQuery:(NSString*)sql, ... ;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... ;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
执行查询语句实例如下
- (NSArray *)getResultFromDatabase{
//执行查询SQL语句,返回查询结果
FMResultSet *result = [self.database executeQuery:@"select * from mytable"];
NSMutableArray *array = [NSMutableArray array];
//获取查询结果的下一个记录
while ([result next]) {
//根据字段名,获取记录的值,存储到字典中
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
int num = [result intForColumn:@"num"];
NSString *name = [result stringForColumn:@"name"];
NSString *sex = [result stringForColumn:@"sex"];
dict[@"num"] = @(num);
dict[@"name"] = name;
dict[@"sex"] = sex;
//把字典添加进数组中
[array addObject:dict];
}
return array;
}
由于在多线程同时操作FMDatabase对象时,会造成数据混乱的问题,FMDB提供了一个可以确保线程安全的类(FMDatabaseQueue)。
FMDatabaseQueue的使用比较简单
//创建多线程安全队列对象
self.queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
//在block块内自行相关数据库操作即可
[self.queue inDatabase:^(FMDatabase * _Nonnull db) {
}];
事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。
比如要更新数据库的大量数据,我们需要确保所有的数据更新成功,才采取这种更新方案,如果在更新期间出现错误,就不能采取这种更新方案了,这就是事务的用处。只有事务提交了,开启事务期间的操作才会生效。
以下是事务的例子
//事务
-(void)transaction {
// 开启事务
[self.database beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i--) {
NSNumber *num = @(i+6);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [self.database executeUpdate:sql,num,name,sex];
if ( !result ) {
NSLog(@"插入失败!");
isRollBack = YES;
return;
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
@finally {
if (!isRollBack) {
NSLog(@"插入成功,事务提交");
//事务提交
[self.database commit];
}else{
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
}
}
//多线程安全事务实例
- (void)transactionByQueue {
//开启事务
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
BOOL isRollBack = NO;
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if ( !result ) {
isRollBack = YES;
return;
}
}
//当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
*rollback = isRollBack;
}];
}
以下是CoreData常用类的作用描述
PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。
ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。
PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。
ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。
CoreData主要工作原理如下
读取数据库的数据时,数据库数据先进入数据解析器,根据对应的模板,生成对应的关联对象。
向数据库插入数据时,对象管理器先根据实体描述创建一个空对象,对该对象进行初始化,然后经过数据解析器,根据对应的模板,转化为数据库的数据,插入数据库中。
更新数据库数据时,对象管理器需要先读取数据库的数据,拿到相互关联的对象,对该对象进行修改,修改的数据通过数据解析器,转化为数据库的更新数据,对数据库更新。
CoreData的使用步骤如下
1.添加框架。
2.数据模板和对象模型。
3.创建对象管理上下文。
4.数据的增删改查操作。
在其中第二步的时候要注意,Xcode 8.0 之后和之前的Xcode 版本是有一些区别的。在 8.0之后创建.xcdatamodeld文件之后,添加实体之后会自动生成对应的对应类文件。
#import
NS_ASSUME_NONNULL_BEGIN
@interface TCPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
当点击屏幕的时候,控制台输出:
2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}
在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:
- (void)setName:(NSString *)name{
_name = name;
}
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}
/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];
self.person2.name = @"ttttttttt";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
#import "NSKVONotifying_TCPerson.h"
@implementation NSKVONotifying_TCPerson
//NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
- (void)setName:(NSString *)name{
_NSSetIntVaueAndNotify();
}
//改变过程
void _NSSetIntVaueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
//通知观察者
- (void)didChangeValueForKey:(NSString *key){
[observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context];
}
@end
首先了解一下程序、进程和线程
程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。所以程序运行的时候,需要一个机制,让线程能随时处理事件但不退出。
以前写游戏的时候就写过这样的东西,通常是一个 do while 循环,让程序一直运转,直到接收到退出信息。
而 RunLoop 就是让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时默认是没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
他们的层级关系为,一个 RunLoop 对象包含若干个 Mode 对象,每个 Mode 又包含若干个 Source/Timer/Observer,RunLoop 在运作的时候,一次只能运作与一个 Mode 之下,如果需要切换 Mode,需要退出 Loop 才能重新指定一个 Mode。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
而一个 Source 对象是一个事件,Source 有两个版本:Source0 和 Source1,Source0 只包含一个函数指针,并不能主动触发,需要将 Source0 标记为待处理,在 RunLoop 运转的时候,才会处理这个事件(如果 RunLoop 处于休眠状态,则不会被唤醒去处理),而 Source1 包含了一个 mach_port 和一个函数指针,mach_port 是 iOS 系统提供的基于端口的输入源,可用于线程或进程间通讯。而 RunLoop 支持的输入源类型中就包括基于端口的输入源,可以做到对 mach_port 端口源事件的监听。所以监听到 source1 端口的消息时,RunLoop 就会自己醒来去执行 Source1 事件(也能称为被消息唤醒)。也就是 Source0 是直接添加给 RunLoop 处理的事件,而 Source1 是基于端口的,进程或线程之间传递消息触发的事件(为什么要 0 和 1 来命名,每次都记不住,GG)。
Timer 是基于时间的触发器,CFRunLoopTimerRef 和 NSTimer 可以通过 Toll-free bridging 技术混用,Toll-free bridging 是一种允许某些 ObjC 类与其对应的 CoreFoundation 类之间可以互换使用的机制,当将 Timer 加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行 Timer 回调。
Observer(观察者)都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
也就是可以在这几个时机去安排 RunLoop 执行一些其他的任务。
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
RunLoop 的 CFRunLoopMode 和 CFRunLoop 的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。
如上文说的 RunLoop 一次循环只能运行在一个 Mode 下,是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。但如果一个 Source/Timer/Observer 想在多个 Mode 下运作,则可以分别加入到多个 Mode,或者给两个 Mode 添加 "Common" 标记,再将 Source/Timer/Observer 加入到 RunLoop 的 "commonModeItems" 。
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,如果想让它回调则可以将这个 Timer 分别加入这两个 Mode。或将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 。
CFRunLoop 对外暴露的管理 Mode 接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
可以注意到 mode 并不像 Source/Timer/Observer 一样有 Remove 方法,所以 mode 只能增加,不能减少。
当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。
接下来看 RunLoop 的执行逻辑
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
其中第 2 步虽然通知 Observer 即将处理 Timer,但其实并没有真的即将处理 Timer 回调,这个通知每次 Loop 循环都会调用,但 Timer 只有在注册的时间到了才会在第 9 步去执行,第 4 步处理运行的 mode 里待处理的 Source0,其中第 5 步会判断 mode item 里是否有 Source1 处于 ready 状态(也就是 Source1 的端口已经收到消息),有的话跳到第 9 步,处理 Source1 事件,然后进入下一个循环,没有的话说明 mode item 里的事件都处理完毕,线程进入休眠状态,等待 Source1,Timer 或者外部手动将 RunLoop 唤醒(上文说 Source0 并不能唤醒 RunLoop,所以一般会通过手动唤醒 RunLoop,来让 RunLoop 处理新加入进去的 Source0)。
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,去执行加入到 RunLoop 里的 Source0,Timer 的回调,以及 Observer 回调,以及用于线程或进程间通讯的 Source1,当所有的都处理完之后,结束一次循环,进入休眠状态,休眠的时候等待 Timer 注册的时间点或者 Source1 唤醒 RunLoop(也可以手动唤醒)。
从上面我们可以了解,线程和进程之间的通讯是基于 mach port 传递消息实现的,这也是 RunLoop 的核心。有必要了解一下 mach port, OSX/iOS 的系统架构分为 4 层,从外到内为应用层,应用框架层,核心框架层,Darwin。应用层包括用户能接触到的图形应用,应用框架层即开发人员接触到的 Cocoa 等框架,核心框架层包括各种核心框架、OpenGL 等内容,Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容。
其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
在 Mach 中,进程、线程和虚拟内存都被称为"对象"。Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
Mach 中的对象通过一个 Mach 端口发送一个消息,消息中会携带目标端口,这个消息会从用户空间传递到内核空间,再由内核空间传递到目标端口,实现线程或进程之间的通讯。(也就是线程或进程之间的通讯不能绕过系统内核)。目标端口接收到消息,因为 RunLoop 会对 mach_port 端口源进行监听,如果 RunLoop 此时处于休眠状态,则被唤醒,便可以处理已经接收到消息的 source1 事件。
启动后,系统默认注册了5个Mode:
1:kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
2:UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3:UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4:GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5:kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()(因为需要设置不同的优先级,所以注册两个)。
第一个 Observer 监视的事件是 Entry(即将进入Loop),用来创建自动释放池,且设置的优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时去释放旧的池并创建新池;Exit(即将退出Loop) 时释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的事件传递。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。也就是 UI 是在界面 RunLoop 休眠之前更新的,所以如果想在 UI 更新之后做一些事情,可以注册一个 Observer 监听 kCFRunLoopAfterWaiting(刚从休眠中唤醒)。
定时器
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
网络请求
iOS 中,关于网络请求的接口自下至上有如下几层:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了多个需要手动触发的 Source0。
当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 基于 mach port 的 Source1 接收来自底层 CFSocket 的消息,当收到消息后,其会在合适的时机将 Source0 标记为待处理,同时唤醒 Delegate 线程的 RunLoop 来让其处理 Source0。完成一个由 CFSocket 线程到网络请求所在线程的数据处理。
AFNetworking
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 创建了一个线程,并在这个线程中启动了一个 RunLoop,RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。当需要这个后台线程执行网络请求任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了该线程的 RunLoop 中。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。
排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor 等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务(需要将 Node 的属性同步到主线程的 UIView/CALayer 去的任务),然后一一执行。
总结:
ibireme 的 深入理解RunLoop 前前后后我看了好几遍,每次都看的不深入,总是瞬时了解,睡一觉就记不清楚细节了,这次自己逐行去理解,并写下来,加上点自己的理解,也写了点代码去观察 RunLoop,算是对 RunLoop 的理解更加深入了。
Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。
也就是 Runtime 涉及三个点,
面向对象
消息分发
消息转发
。面向对象:
Objective-C 的对象是基于 Runtime 创建的结构体。先从代码层面分析一下。
Class *class = [[Class alloc] init];
alloc 方法会为对象分配一块内存空间,空间的大小为 isa_t(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化 isa_t ,而 isa_t 是一个 union 类型的结构体(或者称之为联合体),它的结构是在 Runtime 里被定义的。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
从 isa_t 的结构可以看出,isa_t 可以存储 struct,uintptr_t 或者 Class 类型
。
init 方法就直接返回了初始化好的对象,class 指针指向这个初始化好的对象。
也就是在 Runtime 的协助之下,一个对象完成了创建。
你可能想知道,这个对象只存放了一个 isa_t 结构体和成员变量,对象的方法在哪里?
在编译的时候,类在内存中的位置就已经确定,而在 main 方法之前,Runtime 将可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)加载到内存中,由 Runtime 管理,这里也包括了也是一个对象的类。
类对象里储存着一个 isa_t 的结构体,super_class 指针,cache_t 结构体,class_data_bits_t 指针。
struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
class_data_bits_t 指向类对象的数据区域,数据区域存放着这个类的实例方法链表。而类方法存在元类对象的数据区域。也就是有对象,类对象,元类对象三个概念,对象是在运行时动态创建的,可以有无数个,类对象和元类对象在 main 方法之前创建的,分别只会有一个。
消息分发:
在 Objective-C 中的“方法调用”其实应该叫做消息传递,[object message] 会被编译器翻译为 objc_msgSend(object, @selector(message)),这是一个 C 方法,首先看它的两个参数,第一个是 object ,既方法调用者,第二个参数称为选择子 SEL,Objective-C 为我们维护了一个巨大的选择子表,在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中,在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。
通过第一个参数 object,可以找到 object 对象的 isa_t 结构体,从上文中能看 isa_t 结构体的结构,在 isa_t 结构体中,shiftcls 存放的是一个 33 位的地址,用于指向 object 对象的类对象,而类对象里有一个 cache_t 结构体,来看一下 cache_t 的具体代码,
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
_mask:分配用来缓存 bucket 的总数。
_occupied:表明目前实际占用的缓存 bucket 的个数。
_buckets:一个散列表,用来方法缓存,bucket_t 类型,包含 key 以及方法实现 IMP。
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
objc_msgSend() 方法会先从缓存表里,查找是否有该 SEL 对应的 IMP,有的话算命中缓存,直接通过函数指针 IMP ,找到方法的具体实现函数,执行。
当然缓存表里可能并不会命中,则此时会根据类对象的 class_data_bits_t 指针找到数据区域,数据区域里用链表存放着类的实例方法,实例方法也是一个结构体,其结构为:
struct method_t {
SEL name;
const char *types;
IMP imp;
};
编译器将每个方法的返回值和参数类型编码为一个字符串,types 指向的就是这样一个字符串,objc_msgSend() 会在类对象的方法链表里按链表顺序去匹配 SEL,匹配成功则停止,并将此方法加入到类对象的 _buckets 里缓存起来。如果没找到则会通过类对象的 superclass 指针找到其父类,去父类的方法列表里寻找(也会从父类的方法缓存列表开始)。
如果继续没有找到会一直向父类寻找,直到遇见 NSObject,NSObject 的 superclass 指向 nil。也就意味着寻找结束,并没有找到实现方法。(如果这个过程找到了,也同样会在 object 的类对象的 _buckets 里缓存起来)。
选择子在当前类和父类中都没有找到实现,就进入了方法决议(method resolve),首先判断当前 object 的类对象是否实现了 resolveInstanceMethod: 方法,如果实现的话,会调用 resolveInstanceMethod:方法,这个时候我们可以在 resolveInstanceMethod:方法里动态的添加该 SEL 对应的方法(也可以去做点别的,比如写入日志)。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现 resolveInstanceMethod: 方法,Runtime 还有另一套机制,消息转发。
消息转发:
消息转发分为以下几步:
1.调用 forwardingTargetForSelector: 方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了 nil,继续下面的动作。
2.调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。
3.调用 forwardInvocation: 方法,将第 2 步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了。
以上三个方法都可以通过在 object 的类对象里实现, forwardingTargetForSelector: 可以通过对参数 SEL 的判断,返回一个可以响应该消息的对象。这样则会重新从该对象开始执行查找方法实现的流程,找到了也同样会在 object 的类对象的 _buckets 里缓存起来。而 2,3 方法则一般是配套使用,实现 methodSignatureForSelector: 方法根据参数 SEL ,做相应处理,返回 NSMethodSignature (方法签名) 对象,NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。
上面是讲的是实例方法,类方法没什么区别,类方法储存在元类对象的数据区域里,通过类对象的 isa_t 找到元类对象,执行查找方法实现的流程,元类对象的 superclass 最终也会指向 NSObject。没找到的话,也会有方法决议以及消息转发。
runtime 可以做什么:
实现多继承:从 forwardingTargetForSelector: 方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。
Method m1 = class_getInstanceMethod([M1 class], @selector(hello1));
Method m2 = class_getInstanceMethod([M2 class], @selector(hello2));
method_exchangeImplementations(m2, m1);
关联对象:
通过下面两个方法,可以给 category 实现添加成员变量的效果。
objc_setAssociatedObject
objc_getAssociatedObject
动态添加类和方法:
objc_allocateClassPair 函数与 objc_registerClassPair 函数可以完成一个新类的添加,class_addMethod 给类添加方法,class_addIvar 添加成员变量,objc_registerClassPair 来注册类,其中成员变量的添加必须在类注册之前,类注册后就可以创建该类的对象了,而再添加成员变量就会破坏创建的对象的内存结构。
用到了 Runtime 获取某一个类的全部属性的名字,以及 Runtime 获取属性的类型。
Measure过程要分情况来看,如果只是原始的View,那么通过measure()
方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure()
,下面分两种情况进行讨论。
View的measure过程就是会调用View的measure()
方法,它是一个final类型的方法,在measure()
方法还会调用View的onMeasure()
方法,我们看这个方法写了什么。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
代码很少,这里的setMeasuredDimension()
方法会设置View的宽和高的测量值,因此我们只要看getDefaultSize()
即可。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
从源码可以看出,逻辑还是很简单的,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况,简单理解,其实getDefaultSize()
返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终得大小是在layout阶段确定的,所以这里必须要区分,但几乎所有情况下View的测量大小和最终大小是相等的。
至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize()
的第一个参数size,即宽和高为getSuggestedMinimunWidth()
和getSuggestedMinimunHeight()
的返回值:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这里就只分析getSuggestedMinimumWidth()
方法的实现,getSuggestedMinimunHeight()
和它的实现原理是一样的,从代码中可以看出,如果View没有设置背景,那么宽度就为mMinWidth,而mMinWidth对应android:Width这个属性所指定的值。如果不指定,那么mMinWidth则默认为0,如果设置了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth()。那mBackground.getMinimumWidth()
是什么呢?
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出,getMinimumWidth()
返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。这里说明一下,ShapeDrawable无原始宽和高,BitmapDrawab有原始宽和高。
从getDefaultSize可以看出,View的宽和高由specSize决定。直接继承View的自定义控件需要重写onMeasure()
方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent。
对于ViewGroup来说,除了完成自己的mesaure过程以外,还会遍历去调用所有子元素的measure()
方法,各个子元素在递归的去执行这个过程,和View不同的是,ViewGroup时候是一个抽象类,因此它没有重写View的onMeasure()
,但是它提供了一个叫measureChildren()
的方法。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChildren()
会调用measureChild()
方法,在measureChild()
方法里,就是去除子元素的LayoutParams,然后在通过getChildMeasureSpec()
来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure()
方法进行测量。
我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure()
方法需要由各个子类去具体实现,比如LinearLayout、RelativeLayout等,ViewGroup不做统一的onMeasure()
实现,是因为不同的ViewGroup子类有不同的布局特性,导致它们的测量细节个不不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因为无法做统一,以后会专门写一篇文章来讲解每个layout的布局是怎么实现的。
Layout的作用就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用layout()
方法,在layout()
方法中onLayout()
方法又会被调用。layout过程相比measure过程就简单多了,layout()
方法确定View本身的位置,而onLayout()
方法则会确定所有子元素的位置,先看View的layout()
方法。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
首先会通过setFrame()
方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接着就会调用onLayout()
方法,这个方法的用途是父容器确定子元素的位置,和onMeasure()
类似,onLayout()
的具体实现同样和具体的布局有关,所以View和ViewGroup都没有真正实现onLayout()
方法。
Draw过程就简单很多了,作用就是将View会知道屏幕上面。View的绘制过程遵循着如下几步。
(1)background.draw(canvas)
- 绘制背景
(2)onDraw()
- 绘制自己
(3)dispatchDraw()
- 绘制children
(4)onDrawScrollBars()
- 绘制装饰
绘制流程从ViewRootImpl的performDraw()
开始,performDraw()
方法在类 ViewRootImpl内,其核心代码如下:
private void performDraw() {
boolean canUseAsync = draw(fullRedrawNeeded);
}
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
然后查看View的draw()
方法的源代码:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas); //这里进行传递的实现!!!
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
......
}
View的绘制过程的传递是通过dispatchDraw()
来实现的,dispatchDraw()
会遍历调用所有子元素的draw()
方法,如此一来draw事件就一层层的传递了下去。这里补充一下View还有一个特殊的方法叫setWillNotDraw()
,代码如下:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化,默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个标记位。这个标记位对开发的实际意义是:当我们的自定义控件继承与ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位进行后续的优化。当然如果确定ViewGroup要绘制内容的话,就要关闭这个标记位。
作者:居居居居居居x
链接:https://www.jianshu.com/p/f4afebd50a2b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK为为Android 9.0版本。
在了解绘制流程之前,我们首先要了解Activity的构成,我们都知道Activity要用setcontentView()
来加载布局,但是这个方法具体是怎么实现的呢,如下所示:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
这里有一个getWindow()
,返回的是一个mWindow,那这个是什么呢,我们接在在Activity原来里的attach()
方法里可以看到如下代码:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
......
}
我们发现了原来mWindow实例化了PhoneWindow,那意思就是说Activity通过setcontentView()
加载布局的方法其实就是调用了PhoneWindow的setcontentView()
方法,那我们接着往下看PhoneWindow类里面具体是怎么样的。
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor(); //在这里
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
点进PhoneWindow这个类我们可以知道这里的PhoneWindow是继承Window的,Window是一个抽象类。然后我们看里面的一些关键地方,比如installDecor()
方法,我们看看里面具体做了啥。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1); //注释1
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor); //注释2
......
}
}
里面有一个标记注释为1的generateDecor()
方法,我们看看具体做了啥。
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
发现创建了一个DecorView,而这个DecorView继承了Framelayout,DecorView就是Activity的根View,接着我们回到标记注释2的generateLayout()
方法,看看做了啥。
protected ViewGroup generateLayout(DecorView decor) {
......
// Inflate the window decor.
//根据不同情况加载不同的布局给layoutResource
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title; //在这里!!!!!!!
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
......
mDecor.finishChanging();
return contentParent;
}
generateLayout()
方法代码比较长,这里截取了一部分,主要内容就是根据不同的情况加载不同的布局给layoutresource,在这里面有一个基础的XML,就是R.layout.screen_title。这个XMl文件里面有一个ViewStub和两个FrameLayout,ViewStub主要是显示Actionbar的,两个FarmeLayout分别显示TitleView和ContentView,也就是我们的标题和内容。看到这里我们也就知道,一个Activity包含一个Window对象,而Window是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又分为两个区域,分别就是TitleView和ContentView,平常所显示的布局就是在ContentView中。
上一节讲了Activity的构成,最后讲到了DecorView的创建和加载它的资源,但是这个时候DecorView的内容还无法显示,因为还没用加载到Window中,接下来我们看它是怎么被加载到Window中。
还是老样子,DecorView创建完毕的时并且要加载到Window中,我们还是要先了解Activity的创建流程,当我们使用Activit的startActivity()
方法时,最终就是调用ActivityThread的handleLaunchActivity
方法来创建Activity。而绘制会从根视图ViewRootImpl的performTraversals()
方法开始从上到下遍历整个视图树,每个 View 控制负责绘制自己,而ViewGroup还需要负责通知自己的子 View进行绘制操作。视图操作的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)。
ViewRootImpl是WindowManager和DecorView的纽带,View的三大流程均是通过这里来完成的,在ActivityThread中,当Activity对象被创建,会将DecorView加入到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
刚刚也说过了,View的绘制流程是从ViewRootImpl类的performTraversals()
方法开始的,它会依次调用performMeasure()
、performLayout()
和performDraw()
三个方法。其中在performTraversals()
调用measure()
方法,在measure()
方法又会调用onMeasure()
方法,在onMeasure()
方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了依次measure过程,接着子元素会重复父容器的measure过程,如此返回完成了整个View数的遍历。
为了理解View的测量过程,我们还需理解MeasureSpec,它在很大程度上决定了一个View的尺寸规格,而且也参与到了View的measure过程中。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据这个measureSpec来测量出View的宽和高。
MeasureSpec是View的内部类,它代表哦啦一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小,如以下代码所示:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
......
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
......
}
MeasureSpec通过SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,而也可以通过解包得到SpecMode和SpecSize。这里说一下打包成的MeasureSpec是一个int值,不是这个类本身。
对于SpecMode,有三种类型:
UPSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?
我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据
1.传递数据处理的流程:顶点着色器--->光栅化/图元装配--->片元着色器--->渲染完成
2.TextureData(纹理)、Uniforms:可以直接的传递到顶点着色器或者片元着色器中。
Attributes(属性):只能传递到顶点着色器中,进过处理后的数据可以传递到片元着色器中
3.着色器中的是我们可以控制的,但是光栅化/图元装配是由系统来完成的,不可控
三种传输方式详解
1.Attributes:只能传递到顶点着色器中,通过处理可以传递到片元着色器中
使用场景:当数据不停的进行变换时
经常传递的数据:颜色数据、顶点数据、纹理坐标、关照法线
2.Uniform:可以直接传递数据到顶点/片元着色器中
使用场景:比较统一的处理方式,不会发生太多变化时
顶点着色器处理的场景:图形的旋转操作,每个顶点乘以旋转矩阵来完成(旋转矩阵基本是不怎么改变的)
片元着色器处理场景:处理视频,视频解码之后是由一帧帧的图片来组成的,对视频的颜色空间进行渲染处理(在视频中常使用的颜色空间为YUV)将YUV颜色空间乘以矩阵转化为RGB颜色来处理视频
3.TextureData(纹理):颜色的填充,视频的处理。一般这里的数据不会传递到顶点着色器的,这里主要是用来处理图片的,一般不涉及到顶点数据的处理
收起阅读 »在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要一个新进程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时候就需要线程池来对线程进行管理。在Java 1.5中提供了Executor框架用于把任务的提交和执行解耦。任务的提交交给RUnnable或者Callable,而Executor框架用来处理任务。Executor框架中最核心的成员就是ThreadPoolExecutor,它是线程池的核心实现类。本篇文章就着重讲解ThreadPoolExecutor。
可以通过ThreadPoolExecutor开创建一个线程池,ThreadPoolExecutor类一共有四个构造方法。下面展示的都是拥有最多参数的的构造方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:
核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于corePoolSize,则会创建新线程来处理任务;如果等于或者多于corePoolSize,则不会创建,如果调用线程池的prestartAllcoreThread()
方法,线程池会提前创建并启动所有核心线程来等待任务。
maximumPoolSize:
线程池允许创建的最大线程数,如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍旧会创建新的线程来处理任务。
keepAliveTime:
非核心线程闲置的超时时间,超过这个事件则回收,如果任务很多,并且每个任务的执行事件很短,则可以调用keepAliveTime来提高线程的利用率。另外,如果设置allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上。
TimeUnit:
keepAliveTime参数的时间单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、秒(SECONDS)、毫秒(MILLOSECONDS)等。
BlockingQueue
任务队列,如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockiingQueue类型,也就是阻塞队列。
ThreadFactory:
线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置参数。
RejectedExecutionHandler :
饱和策略,这是当任务队列中和线程池都满了时所采取的对应策略,默认是ABordPolicy,表示无法处理新任务,并抛出RejetctedExecutionException异常。此外还有3种策略,它们分别如下:
(1)CallerRunsPolicy:用调用者所在的线程来处理任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
(2)DiscardPolicy:不能执行的任务,并将该任务删除。
(3)DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。
package com.ju.executordemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity{
private Button btnStart;
private final int CORE_POOL_SIZE = 4;//核心线程数
private final int MAX_POOL_SIZE = 5;//最大线程数
private final long KEEP_ALIVE_TIME = 10;//空闲线程超时时间
private ThreadPoolExecutor executorPool;
private int songIndex = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
//创建线程池
initExec();
}
private void initView() {
btnStart = findViewById(R.id.btn_start);
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
begin();
}
});
}
public void begin() {
songIndex++;
try {
executorPool.execute(new WorkerThread("歌曲" + songIndex));
} catch (Exception e) {
Log.e("threadtest", "AbortPolicy...已超出规定的线程数量,不能再增加了....");
}
// 所有任务已经执行完毕,我们在监听一下相关数据
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(20 * 1000);
} catch (Exception e) {
}
sout("monitor after");
}
}).start();
}
private void sout(String msg) {
Log.i("threadtest", "monitor " + msg
+ " CorePoolSize:" + executorPool.getCorePoolSize()
+ " PoolSize:" + executorPool.getPoolSize()
+ " MaximumPoolSize:" + executorPool.getMaximumPoolSize()
+ " ActiveCount:" + executorPool.getActiveCount()
+ " TaskCount:" + executorPool.getTaskCount()
);
}
private void initExec() {
executorPool = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingDeque(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
}
class WorkerThread implements Runnable {
private String threadName;
public WorkerThread (String name){
threadName = name;
}
@Override
public void run() {
boolean flag = true;
try {
while (flag){
String tn = Thread.currentThread().getName();
//模拟耗时操作
Random random = new Random();
long time = (random.nextInt(5) + 1) * 1000;
Thread.sleep(time);
Log.e("threadtest","线程\"" + tn + "\"耗时了(" + time / 1000 + "秒)下载了第<" + threadName + ">");
//下载完毕跳出循环
flag = false;
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
execute()
方法,然后他需要一个Runnable的参数来进行启动。通过直接或者间接地配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor,其中有 4 种线程池比较常用,它们分别是 FixedThreadPool、CachedThreadPool、SingleThreadExecutor和 ScheduledThreadPool。下面分别介绍这4种线程池。
FixedThreadPool 是可重用固定线程数的线程池。在 Executors 类中提供了创建FixedThreadPool的方法, 如下所示:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
FixedThreadPool的corePoolSize和maximumPoolSize都设置为创建FixedThreadPool指定的参数nThreads,也就意味着FixedThreadPool只有核心线程,并且数量是固定的,没有非核心线程。keepAliveTime设置为0L 意味着多余的线程会被立即终止。因为不会产生多余的线程,所以keepAliveTime是无效的参数。另外,任 务队列采用了无界的阻塞队列LinkedBlockingQueue。
execute()
方法时,如果当前运行的线程未达到corePoolSize(核心线程数)时 就创建核心线程来处理任务,如果达到了核心线程数则将任务添加到LinkedBlockingQueue中。 FixedThreadPool就是一个有固定数量核心线程的线程池,并且这些核心线程不会被回收。当线程数超过corePoolSize时,就将任务存储在任务队列中;当线程池有空闲线程时,则从任务队列中去取任务执行CachedThreadPool是一个根据需要创建线程的线程池,创建CachedThreadPool的代码如下所示:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
CachedThreadPool的corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,这意味着 CachedThreadPool没有核心线程,非核心线程是无界的。keepAliveTime设置为60L,则空闲线程等待新任务 的最长时间为 60s。在此用了阻塞队列 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作 必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。
execute()
方法时,首先会执行SynchronousQueue的offer()
方法来提交任务,并且查询线程池中是否有空闲的线程执行SynchronousQueue的poll()
方法来移除任务。如果有则配对成功,将任务交给这个空闲的线程处理;如果没有则配对失败,创建新的线程去处理任务。当线程池中的线程空闲时,它会执行 SynchronousQueue的poll()
方法,等待SynchronousQueue中新提交的任务。如果超过 60s 没有新任务提交到 SynchronousQueue,则这个空闲线程将终止。因为maximumPoolSize 是无界的,所以如果提交的任务大于线 程池中线程处理任务的速度就会不断地创建新线程。另外,每次提交任务都会立即有线程去处理。所以,CachedThreadPool比较适于大量的需要立即处理并且耗时较少的任务。SingleThreadExecutor是使用单个工作线程的线程池,其创建源码如下所示:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
corePoolSize和maximumPoolSize都为1,意味着SingleThreadExecutor只有一个核心线程,其他的参数都 和FixedThreadPool一样,这里就不赘述了。SingleThreadExecutor的execute()
方法的执行示意图如图5所示。
execute()
方法时,如果当前运行的线程数未达到核心线程数,也就是当前没有运行的线程,则创建一个新线程来处理任务。如果当前有运行的线程,则将任务添加到阻塞队列LinkedBlockingQueue中。因此,SingleThreadExecutor能确保所有的任务在一个线程中按照顺序逐一执行。ScheduledThreadPool是一个能实现定时和周期性任务的线程池,它的创建源码如下所示
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
这里创建了ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,它主要用于给定延时之后的运行任务或者定期处理任务。ScheduledThreadPoolExecutor 的构造方法如下所示:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
从上面的代码可以看出,ScheduledThreadPoolExecutor 的构造方法最终调用的是ThreadPoolExecutor的 构造方法。corePoolSize是传进来的固定数值,maximumPoolSize的值是Integer.MAX_VALUE。因为采用的 DelayedWorkQueue是无界的,所以maximumPoolSize这个参数是无效的。
scheduleAtFixedRate()
或者scheduleWithFixedDelay()
方法时,会向DelayedWorkQueue添加一个 实现RunnableScheduledFuture接口的ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到corePoolSize。如果没有则新建线程并启动它,但并不是立即去执行任务,而是去DelayedWorkQueue中取ScheduledFutureTask,然后去执行任务。如果运行的线程达到了corePoolSize时,则将任务添加到DelayedWorkQueue中。DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面。其跟此前介绍的线程池不同的是,当执行完任务后,会将ScheduledFutureTask中time变量改为下次要执行的时间并放回到DelayedWorkQueue中。示例图:
安卓选择器类库,包括日期及时间选择器(可设置范围)、单项选择器(可用于性别、职业、学历、星座等)、城市地址选择器(分省级、地级及县级)、数字选择器(可用于年龄、身高、体重、温度等)、双项选择器、颜色选择器、文件及目录选择器等……
具体步骤如下: 第一步,在项目根目录下的build.gradle里加:
repositories {
maven {
url "https://jitpack.io"
}
}
第二步,在项目的app模块下的build.gradle里加: 滚轮选择器:
dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:版本号'
}
文件目录选择器:
dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:FilePicker:版本号'
}
颜色选择器:
dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:ColorPicker:版本号'
}
注:Support版本截止1.5.6,从2.0.0开始为AndroidX版本。
Support版本依赖:
dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:1.5.6.20181018'
}
AndroidX版本依赖:
dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:Common:2.0.0'
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:2.0.0'
}
由于地址选择器使用了fastjson来解析,混淆时候需要加入以下类似的规则,不混淆Province、City等实体类。
-keepattributes InnerClasses,Signature
-keepattributes *Annotation*
-keep class cn.qqtheme.framework.entity.** { *;}
各种设置方法:
picker.setXXX(...);
如:
设置选项偏移量,可用来要设置显示的条目数,范围为1-5,1显示3行、2显示5行、3显示7行……
picker.setOffset(...);
设置启用循环
picker.setCycleDisable(false);
设置每项的高度,范围为2-4
picker.setLineSpaceMultiplier(...);
picker.setItemHeight(...);
设置文字颜色、字号、字体等
picker.setTextColor(...);
picker.setTextSize(...);
picker.setTextPadding(...);
picker.setTextSizeAutoFit(...);
picker.setTypeface(...);
设置单位标签
picker.setLabel(...);
picker.setOnlyShowCenterLabel(...))
设置默认选中项
picker.setSelectedItem(...);
picker.setSelectedIndex(...);
设置滚轮项填充宽度,分割线最长
picker.setUseWeight(true);
picker.setDividerRatio(WheelView.DividerConfig.FILL);
设置触摸弹窗外面是否自动关闭
picker.setCanceledOnTouchOutside(...);
设置分隔线配置项,设置null将隐藏分割线及阴影
picker.setDividerConfig(...);
picker.setDividerColor(...);
picker.setDividerRatio(...);
picker.setDividerVisible(...);
设置内容边距
picker.setContentPadding(...);
设置选中项背景色
picker.setShadowColor(...)
自定义顶部及底部视图
picker.setHeaderView(...);
picker.setFooterView(...);
获得内容视图(不要调用picker.show()方法),可以将其加入到其他容器视图(如自定义的Dialog的视图)中
picker.getContentView();
获得按钮视图(需要先调用picker.show()方法),可以调用该视图相关方法,如setVisibility()
picker.getCancelButton();
picker.getSubmitButton();
自定义选择器示例:
CustomHeaderAndFooterPicker picker = new CustomHeaderAndFooterPicker(this);
picker.setOnOptionPickListener(new OptionPicker.OnOptionPickListener() {
@Override
public void onOptionPicked(int position, String option) {
showToast(option);
}
});
picker.show();
核心滚轮控件为WheelView,可以参照SinglePicker、DateTimePicker及LinkagePicker自行扩展。
原文链接:https://github.com/gzu-liyujiang/AndroidPicker
收起阅读 »LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 LiveData 的通知机制是它们的主要责任。所以,就这个机会我们也来分析一下 LiveData 通知的实现过程。
// 类 A
public class A extends Fragment {
private boolean loading = false;
private MyViewModel vm;
// ......
/**
* Register load observer.
*/
public void registerObservers() {
vm.getData().observe(this, resources -> {
loading = false;
// ... show in list
})
}
/**
* Load data from server.
*/
public void loadData() {
if (loading) return;
loading = true;
vm.load();
}
/**
* On receive message.
*/
public void onReceive() {
loadData();
}
}
public class B extends Activity {
public void doBusiness1() {
sendMessage(MSG); // Send message when on foreground.
}
@Override
public void onBackpressed() {
// ....
sendMessage(MSG); // Send message when back
}
}
public class MyViewModel extends ViewModel {
private MutableLiveData> data;
public MutableLiveData> getData() {
if (data == null) {
data = new MutableLiveData<>();
}
return data;
}
public void load() {
Object result = AsyncGetData.getData(); // Get data
if (data != null) {
data.setValue(Resouces.success(result));
}
}
}
A 打开了 B 之后,A 处于后台,B 处于前台。此时,B 调用 doBusiness1() 发送了一条消息 MSG,A 中在 onReceive() 中收到消息,并调用 loadData() 加载数据。然后,B 处理完了业务,准备退出的时候发现其他数据发生了变化,所以又发了一条消息,然后 onReceive() 中收到消息,并调用 loadData(). 但此时发现 loading 为 true. 所以,我们后来对数据的修改没有体现到列表上面。
// 类 A
public class A extends Fragment {
private boolean dataChanged;
/**
* On receive message.
*/
public void onReceive() {
dataChanged = true;
}
@Override
public void onResume() {
// ...
if (dataChanged) {
loadData();
}
}
}
对于上面的问题,当我们调用了 setValue() 之后将调用到 LiveData 类的 setValue() 方法,
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
这里表明该方法必须在主线程中被调用,最终事件的分发将会交给 dispatchingValue() 方法来执行:
private void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
// 发送事件
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}
然后,会调用 considerNotify() 方法来最终将事件传递出去,
private void considerNotify(ObserverWrapper observer) {
// 这里会因为当前的 Fragment 没有处于 active 状态而退出方法
if (!observer.mActive) {
return;
}
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}
这里会因为当前的 Fragment 没有处于 active 状态而退出 considerNotify() 方法,从而消息无法被传递出去。
private SafeIterableMap, ObserverWrapper> mObservers = new SafeIterableMap<>();
每当我们调用一次 observe() 方法的时候就会有一个映射关系被加入到哈希表中,
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// 持有者当前处于被销毁状态,因此可以忽略此次观察
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}
从上面的代码我们可以看出,添加到映射关系中的类会先被包装成 LifecycleBoundObserver 对象。然后使用该对象对 owner 的生命周期进行监听。
这的 LifecycleBoundObserver 和 ObserverWrapper 两个类的定义如下,
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
@NonNull final LifecycleOwner mOwner;
LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer observer) {
super(observer);
mOwner = owner;
}
@Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}
@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}
@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}
}
private abstract class ObserverWrapper {
final Observer mObserver;
boolean mActive;
int mLastVersion = START_VERSION;
ObserverWrapper(Observer observer) {
mObserver = observer;
}
abstract boolean shouldBeActive();
boolean isAttachedTo(LifecycleOwner owner) {
return false;
}
void detachObserver() {}
void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
dispatchingValue(this);
}
}
}
上面的类中我们先来关注 LifecycleBoundObserver 中的 onStateChanged() 方法。该方法继承自 LifecycleObserver. 这里的 Lifecycle.Event 是一个枚举类型,定义了一些与生命周期相关的枚举值。所以,当 Activity 或者 Fragment 的生命周期发生变化的时候会回调这个方法。从上面我们也可以看出,该方法内部又调用了基类的 activeStateChanged() 方法,该方法主要用来更新当前的 Observer 是否处于 Active 的状态。我们上面无法通知也是因为在这个方法中 mActive 被置为 false 造成的。
继续看 activeStateChanged() 方法,我们可以看出在最后的几行中,它调用了 dispatchingValue(this) 方法。所以,当 Fragment 从处于后台切换到前台之后,会将当前缓存的值通知给观察者。
那么值是如何缓存的,以及缓存了多少值呢?回到之前的 setValue() 和 dispatchingValue() 方法中,我们发现值是以一个单独的变量进行缓存的,
private volatile Object mData = NOT_SET;
因此,在我们的示例中,当页面从后台切换到前台的时候,只能将最后一次缓存的结果通知给观察者就真相大白了。
private ReceiptViewerViewModel viewModel;
@Override
protected void doCreateView(Bundle savedInstanceState) {
viewModel = ViewModelProviders.of(this).get(ReceiptViewerViewModel.class); // 1
handleIntent(savedInstanceState);
// ...
}
private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}
在方法 doCreateView() 中我获取了 viewModel 实例,然后在 handleIntent() 方法中从 Intent 中取出传入的参数。当然,还要使用 viewModel 的 getter 方法从其中取出 loadingStatus 并使用。在使用的时候抛了空指针。
显然,一般情况下是不会出现问题的,但是如果 Activity 在后台被销毁了,那么再重建的时候就会出现空指针异常。
解决方法也比较简单,在 onSaveInstanceState() 方法中将数据缓存起来即可,即:
private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
} else {
loadingStatus = (LoadingStatus) savedInstanceState.get(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(Router.RECEIPT_VIEWER_LOADING_STATUS, viewModel.getLoadingStatus());
}
现在的问题是 ViewModel 的生命周期问题,有人说在 doCreateView() 方法的 1 处得到的不是之前的 ViewModel 吗,数据不是之前已经设置过了吗?所以,这牵扯 ViewModel 是在什么时候被销毁和重建的问题。
// ViewModelProviders#of()
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
ViewModelProvider.AndroidViewModelFactory factory =
ViewModelProvider.AndroidViewModelFactory.getInstance(activity);
return new ViewModelProvider(ViewModelStores.of(activity), factory); // 1
}
// ViewModelProvider#get()
public T get(@NonNull String key, @NonNull Class modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
我们回到上述 of() 方法的 1 处,来看下 ViewModelStores.of() 方法,其定义如下:
// ViewModelStores#of()
public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) {
return ((ViewModelStoreOwner) activity).getViewModelStore();
}
return holderFragmentFor(activity).getViewModelStore();
}
// HolderFragment#holderFragmentFor()
public static HolderFragment holderFragmentFor(FragmentActivity activity) {
return sHolderFragmentManager.holderFragmentFor(activity);
}
这里会从 holderFragmentFor() 方法中获取一个 HolderFragment 实例,它是一个 Fragment 的实现类。然后从该实例中获取 ViewModelStore 的实例。所以,ViewModel 对生命周期的管理与 Glide 和 RxPermission 等框架的处理方式一致,就是使用一个空的 Fragment 来进行生命周期管理。
对于 HolderFragment,其定义如下。从下面的代码我们可以看出,上述用到的 ViewModelStore 实例就是 HolderFragment 的一个局部变量。所以,ViewModel 使用空的 Fragment 管理生命周期实锤了。
public class HolderFragment extends Fragment implements ViewModelStoreOwner {
private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
private ViewModelStore mViewModelStore = new ViewModelStore();
public HolderFragment() {
setRetainInstance(true);
}
// ...
}
此外,我们注意到上面的 HolderFragment 的构造方法中还调用了 setRetainInstance(true) 这一行代码。我们进入该方法看它的注释:Control whether a fragment instance is retained across Activityre-creation (such as from a configuration change). This can onlybe used with fragments not in the back stack. If set, the fragmentlifecycle will be slightly different when an activity is recreated:
就是说,当 Activity 被重建的时候该 Fragment 会被保留,然后传递给新创建的 Activity. 但是,这只适用于不处于后台的 Fragment. 所以,如果 Activity 处于后台的时候,Fragment 不会保留,那么它得到的 ViewModelStore 实例就不同了。
所以,总结下来,准确地讲:当 Activity 处于前台的时候被销毁了,那么得到的 ViewModel 是之前实例过的 ViewModel;如果 Activity 处于后台时被销毁了,那么得到的 ViewModel 不是同一个。举例说,如果 Activity 因为配置发生变化而被重建了,那么当重建的时候,ViewModel 是之前的实例;如果因为长期处于后台而被销毁了,那么重建的时候,ViewModel 就不是之前的实例了。
回到之前的 holderFragmentFor() 方法,我们看下这里具体做了什么,其定义如下。
// HolderFragmentManager#holderFragmentFor()
HolderFragment holderFragmentFor(FragmentActivity activity) {
// 使用 FragmentManager 获取 HolderFragment
FragmentManager fm = activity.getSupportFragmentManager();
HolderFragment holder = findHolderFragment(fm);
if (holder != null) {
return holder;
}
// 从哈希表中获取 HolderFragment
holder = mNotCommittedActivityHolders.get(activity);
if (holder != null) {
return holder;
}
if (!mActivityCallbacksIsAdded) {
mActivityCallbacksIsAdded = true;
activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
}
holder = createHolderFragment(fm);
// 将新的实例放进哈希表中
mNotCommittedActivityHolders.put(activity, holder);
return holder;
}
首先,尝试使用 FragmentManager 来获取 HolderFragment,如果获取不到就从 mNotCommittedActivityHolders 中进行获取。这里的 mNotCommittedActivityHolders 是一个哈希表,每次实例化的新的 HolderFragment 会被添加到哈希表中。
另外,上面的方法中还使用了 ActivityLifecycleCallbacks 对 Activity 的生命周期进行监听。其定义如下,
private ActivityLifecycleCallbacks mActivityCallbacks =
new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
}
};
当 Activity 被销毁的时候会从哈希表中移除映射关系。所以,每次 Activity 被销毁的时候哈希表中的映射关系都不存在了。而之所以 ViewModel 能够实现在 Activity 配置发生变化的时候获取之前的 ViewModel 是通过上面的 setRetainInstance(true) 和 findHolderFragment(fm) 来实现的。
总结
以上就是 ViewModel 的生命周期的总结。我们只是通过对主流程的分析研究了它的生命周期的流程,实际上内部还有许多小细节,逻辑也比较简单,我们就不一一说明了。
viewmodel-lifecycle
这里使用了 Activity rotated,也就是 Activity 处于前台的时候配置发生变化的情况,而不是处于后台,不知道你之前有没有注意这一点呢?
原文链接:揭开 ViewModel 的生命周期控制的神秘面纱
收起阅读 »MVC、MVP 和 MVVM 是常见的三种架构设计模式,当前 MVP 和 MVVM 的使用相对比较广泛,当然 MVC 也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和前面三种架构设计不是一个层次的。它们之间的关系是,组件化的各个组件可以使用前面三种架构设计。我们只有了解了这些架构设计的特点之后,才能在进行开发的时候选择适合自己项目的架构模式,这也是本文的目的。
public interface HomeContract {
interface IView extends BaseView {
void setFirstPage(List itemLists);
void setNextPage(List itemLists);
void onError(String msg);
}
interface IPresenter extends BasePresenter {
void requestFirstPage();
void requestNextPage();
}
}
HomeContract 用来规定 View 和 Presenter 应该具有的操作,在这里它用来指定主页的 View 和 Presenter 的方法。从上面我们也可以看出,这里的 IView 和 IPresenter 分别实现了 BaseView 和 BasePresenter。
上面,我们定义了 V 和 P 的规范,MVP 中还有一项 Model,它用来从网络中获取数据。这里我们省去网络相关的具体的代码,你只需要知道 APIRetrofit.getEyepetizerService() 是用来获取 Retrofit 对应的 Service,而 getMoreHomeData() 和 getFirstHomeData() 是用来从指定的接口中获取数据就行。下面是 HomeModel 的定义:
public class HomeModel {
public Observable getFirstHomeData() {
return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis());
}
public Observable getMoreHomeData(String url) {
return APIRetrofit.getEyepetizerService().getMoreHomeData(url);
}
}
OK,上面我们已经完成了 Model 的定义和 View 及 Presenter 的规范的定义。下面,我们就需要具体去实现 View 和 Presenter。
首先是 Presenter,下面是我们的 HomePresenter 的定义。在下面的代码中,为了更加清晰地展示其中的逻辑,我删减了一部分无关代码:
public class HomePresenter implements HomeContract.IPresenter {
private HomeContract.IView view;
private HomeModel homeModel;
private String nextPageUrl;
// 传入View并实例化Model
public HomePresenter(HomeContract.IView view) {
this.view = view;
homeModel = new HomeModel();
}
// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调
@Override
public void requestFirstPage() {
Disposable disposable = homeModel.getFirstHomeData()
// ....
.subscribe(itemLists -> { view.setFirstPage(itemLists); },
throwable -> { view.onError(throwable.toString()); });
}
// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调
@Override
public void requestNextPage() {
Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)
// ....
.subscribe(itemLists -> { view.setFirstPage(itemLists); },
throwable -> { view.onError(throwable.toString()); });
}
}
从上面我们可以看出,在 Presenter 需要将 View 和 Model 建立联系。我们需要在初始化的时候传入 View,并实例化一个 Model。Presenter 通过 Mode l获取数据,并在拿到数据的时候,通过 View 的方法通知给 View 层。
然后,就是我们的 View 层的代码,同样,我对代码做了删减:
@Route(path = BaseConstants.EYEPETIZER_MENU)
public class HomeActivity extends CommonActivity implements HomeContract.IView {
// 实例化Presenter
private HomeContract.IPresenter presenter;
{
presenter = new HomePresenter(this);
}
@Override
protected int getLayoutResId() {
return R.layout.activity_eyepetizer_menu;
}
@Override
protected void doCreateView(Bundle savedInstanceState) {
// ...
// 使用Presenter请求数据
presenter.requestFirstPage();
loading = true;
}
private void configList() {
// ...
getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 请求下一页的数据
presenter.requestNextPage();
}
}
});
}
// 当请求到结果的时候在页面上做处理,展示到页面上
@Override
public void setFirstPage(List itemLists) {
loading = false;
homeAdapter.addData(itemLists);
}
// 当请求到结果的时候在页面上做处理,展示到页面上
@Override
public void setNextPage(List itemLists) {
loading = false;
homeAdapter.addData(itemLists);
}
@Override
public void onError(String msg) {
ToastUtils.makeToast(msg);
}
// ...
}
从上面的代码中我们可以看出实际在 View 中也要维护一个 Presenter 的实例。当需要请求数据的时候会使用该实例的方法来请求数据,所以,在开发的时候,我们需要根据请求数据的情况,在 Presenter 中定义接口方法。
实际上,MVP 的原理就是 View 通过 Presenter 获取数据,获取到数据之后再回调 View 的方法来展示数据。
另外一个值得注意的地方就是,在实际的使用过程中,尤其是进行异步请求的时候,为了给 Model 和 View 之间解耦,我们会在 Presenter 中使用 Handler 收发消息来建立两者之间的桥梁。
public class GuokrViewModel extends ViewModel {
public LiveData> getGuokrNews(int offset, int limit) {
MutableLiveData> result = new MutableLiveData<>();
GuokrRetrofit.getGuokrService().getNews(offset, limit)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer() {
@Override
public void onError(Throwable e) {
result.setValue(Resource.error(e.getMessage(), null));
}
@Override
public void onComplete() { }
@Override
public void onSubscribe(Disposable d) { }
@Override
public void onNext(GuokrNews guokrNews) {
result.setValue(Resource.success(guokrNews));
}
});
return result;
}
}
这里的 ViewModel 来自 android.arch.lifecycle.ViewModel,所以,为了使用它,我们还需要加入下面的依赖:
api "android.arch.lifecycle:runtime:$archVersion"
api "android.arch.lifecycle:extensions:$archVersion"
annotationProcessor "android.arch.lifecycle:compiler:$archVersion"
在 ViewModel 的定义中,我们直接使用 Retrofit 来从网络中获取数据。然后当获取到数据的时候,我们使用 LiveData 的方法把数据封装成一个对象返回给 View 层。在 View 层,我们只需要调用该方法,并对返回的 LiveData 进行"监听"即可。这里,我们将错误信息和返回的数据信息进行了封装,并且封装了一个代表当前状态的枚举信息,你可以参考源代码来详细了解下这些内容。
上面我们定义完了 Model 和 ViewModel,下面我们看下 View 层的定义,以及在 View 层中该如何使用 ViewModel。
@Route(path = BaseConstants.GUOKR_NEWS_LIST)
public class NewsListFragment extends CommonFragment {
private GuokrViewModel guokrViewModel;
private int offset = 0;
private final int limit = 20;
private GuokrNewsAdapter adapter;
@Override
protected int getLayoutResId() {
return R.layout.fragment_news_list;
}
@Override
protected void doCreateView(Bundle savedInstanceState) {
// ...
guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);
fetchNews();
}
private void fetchNews() {
guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {
if (guokrNewsResource == null) {
return;
}
switch (guokrNewsResource.status) {
case FAILED:
ToastUtils.makeToast(guokrNewsResource.message);
break;
case SUCCESS:
adapter.addData(guokrNewsResource.data.getResult());
adapter.notifyDataSetChanged();
break;
}
});
}
}
以上就是我们的 View 层的定义,这里我们先使用了
这里的view.fragment包下面的类对应于实际的页面,这里我们 ViewModelProviders 的方法来获取我们需要使用的 ViewModel,然后,我们直接使用该 ViewModel 的方法获取数据,并对返回的结果进行“监听”即可。
以上就是 MVVM 的基本使用,当然,这里我们并没有使用 DataBinding 直接与返回的列表信息进行绑定,它被更多的用在了整个 Fragment 的布局中。
dependencies {
api fileTree(include: ['*.jar'], dir: 'libs')
// ...
// router
api 'com.alibaba:arouter-api:1.3.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
// walle
api 'com.meituan.android.walle:library:1.1.6'
// umeng
api 'com.umeng.sdk:common:1.5.3'
api 'com.umeng.sdk:analytics:7.5.3'
api files('libs/pldroid-player-1.5.0.jar')
}
isGuokrModuleApp=false
isLiveModuleApp=false
isLayoutModuleApp=false
isLibraryModuleApp=false
isEyepetizerModuleApp=false
if (isEyepetizerModuleApp.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
if (isEyepetizerModuleApp.toBoolean()) {
manifest.srcFile "src/main/debug/AndroidManifest.xml"
} else {
manifest.srcFile "src/main/AndroidManifest.xml"
}
}
}
// use data binding
dataBinding {
enabled = true
}
// use java 8 language
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
这是一款仿iOS的PickerView控件,有时间选择器和选项选择器,新版本的详细特性如下:
——TimePickerView 时间选择器,支持年月日时分,年月日,年月,时分等格式。
——OptionsPickerView 选项选择器,支持一,二,三级选项选择,并且可以设置是否联动 。
compile 'com.contrarywind:Android-PickerView:4.1.9'
或者
com.contrarywind
Android-PickerView
4.1.9
pom
//时间选择器
TimePickerView pvTime = new TimePickerBuilder(MainActivity.this, new OnTimeSelectListener() {
@Override
public void onTimeSelect(Date date, View v) {
Toast.makeText(MainActivity.this, getTime(date), Toast.LENGTH_SHORT).show();
}
}).build();
//条件选择器
OptionsPickerView pvOptions = new OptionsPickerBuilder(MainActivity.this, new OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3 ,View v) {
//返回的分别是三个级别的选中位置
String tx = options1Items.get(options1).getPickerViewText()
+ options2Items.get(options1).get(option2)
+ options3Items.get(options1).get(option2).get(options3).getPickerViewText();
tvOptions.setText(tx);
}
}).build();
pvOptions.setPicker(options1Items, options2Items, options3Items);
pvOptions.show();
Calendar selectedDate = Calendar.getInstance();
Calendar startDate = Calendar.getInstance();
//startDate.set(2013,1,1);
Calendar endDate = Calendar.getInstance();
//endDate.set(2020,1,1);
//正确设置方式 原因:注意事项有说明
startDate.set(2013,0,1);
endDate.set(2020,11,31);
pvTime = new TimePickerBuilder(this, new OnTimeSelectListener() {
@Override
public void onTimeSelect(Date date,View v) {//选中事件回调
tvTime.setText(getTime(date));
}
})
.setType(new boolean[]{true, true, true, true, true, true})// 默认全部显示
.setCancelText("Cancel")//取消按钮文字
.setSubmitText("Sure")//确认按钮文字
.setContentSize(18)//滚轮文字大小
.setTitleSize(20)//标题文字大小
.setTitleText("Title")//标题文字
.setOutSideCancelable(false)//点击屏幕,点在控件外部范围时,是否取消显示
.isCyclic(true)//是否循环滚动
.setTitleColor(Color.BLACK)//标题文字颜色
.setSubmitColor(Color.BLUE)//确定按钮文字颜色
.setCancelColor(Color.BLUE)//取消按钮文字颜色
.setTitleBgColor(0xFF666666)//标题背景颜色 Night mode
.setBgColor(0xFF333333)//滚轮背景颜色 Night mode
.setDate(selectedDate)// 如果不设置的话,默认是系统时间*/
.setRangDate(startDate,endDate)//起始终止年月日设定
.setLabel("年","月","日","时","分","秒")//默认设置为年月日时分秒
.isCenterLabel(false) //是否只显示中间选中项的label文字,false则每项item全部都带有label。
.isDialog(true)//是否显示为对话框样式
.build();
pvOptions = new OptionsPickerBuilder(this, new OptionsPickerView.OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3 ,View v) {
//返回的分别是三个级别的选中位置
String tx = options1Items.get(options1).getPickerViewText()
+ options2Items.get(options1).get(option2)
+ options3Items.get(options1).get(option2).get(options3).getPickerViewText();
tvOptions.setText(tx);
}
}) .setOptionsSelectChangeListener(new OnOptionsSelectChangeListener() {
@Override
public void onOptionsSelectChanged(int options1, int options2, int options3) {
String str = "options1: " + options1 + "\noptions2: " + options2 + "\noptions3: " + options3;
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
}
})
.setSubmitText("确定")//确定按钮文字
.setCancelText("取消")//取消按钮文字
.setTitleText("城市选择")//标题
.setSubCalSize(18)//确定和取消文字大小
.setTitleSize(20)//标题文字大小
.setTitleColor(Color.BLACK)//标题文字颜色
.setSubmitColor(Color.BLUE)//确定按钮文字颜色
.setCancelColor(Color.BLUE)//取消按钮文字颜色
.setTitleBgColor(0xFF333333)//标题背景颜色 Night mode
.setBgColor(0xFF000000)//滚轮背景颜色 Night mode
.setContentTextSize(18)//滚轮文字大小
.setLinkage(false)//设置是否联动,默认true
.setLabels("省", "市", "区")//设置选择的三级单位
.isCenterLabel(false) //是否只显示中间选中项的label文字,false则每项item全部都带有label。
.setCyclic(false, false, false)//循环与否
.setSelectOptions(1, 1, 1) //设置默认选中项
.setOutSideCancelable(false)//点击外部dismiss default true
.isDialog(true)//是否显示为对话框样式
.isRestoreItem(true)//切换时是否还原,设置默认选中第一项。
.build();
pvOptions.setPicker(options1Items, options2Items, options3Items);//添加数据源
// 注意:自定义布局中,id为 optionspicker 或者 timepicker 的布局以及其子控件必须要有,否则会报空指针
// 具体可参考demo 里面的两个自定义布局
pvCustomOptions = new OptionsPickerBuilder(this, new OptionsPickerView.OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3, View v) {
//返回的分别是三个级别的选中位置
String tx = cardItem.get(options1).getPickerViewText();
btn_CustomOptions.setText(tx);
}
})
.setLayoutRes(R.layout.pickerview_custom_options, new CustomListener() {
@Override
public void customLayout(View v) {
//自定义布局中的控件初始化及事件处理
final TextView tvSubmit = (TextView) v.findViewById(R.id.tv_finish);
final TextView tvAdd = (TextView) v.findViewById(R.id.tv_add);
ImageView ivCancel = (ImageView) v.findViewById(R.id.iv_cancel);
tvSubmit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pvCustomOptions.returnData(tvSubmit);
}
});
ivCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pvCustomOptions.dismiss();
}
});
tvAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getData();
pvCustomOptions.setPicker(cardItem);
}
});
}
})
.build();
pvCustomOptions.setPicker(cardItem);//添加数据
compile 'com.contrarywind:wheelview:4.1.0'
xml布局:
android:id="@+id/wheelview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Java 代码:
WheelView wheelView = findViewById(R.id.wheelview);
wheelView.setCyclic(false);
final List mOptionsItems = new ArrayList<>();
mOptionsItems.add("item0");
mOptionsItems.add("item1");
mOptionsItems.add("item2");
wheelView.setAdapter(new ArrayWheelAdapter(mOptionsItems));
wheelView.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(int index) {
Toast.makeText(MainActivity.this, "" + mOptionsItems.get(index), Toast.LENGTH_SHORT).show();
}
});
代码下载:Android-PickerView-master.zip
原文链接:https://github.com/Bigkoo/Android-PickerView
收起阅读 »我在前面两篇文章中详细介绍了 今日头条适配方案 和 SmallestWidth 限定符适配方案 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 Android 屏幕适配方案,而且它们都已经拥有了一定的用户基数
但是对于一些才接触这两个方案的朋友,肯定或多或少还是不知道如何选择这两个方案,我虽然在之前的文章中给出了它们各自的优缺点,但是并没有用统一的标准对它们进行更细致的对比,所以也就没办法更形象的体现它们的优劣,那下面我就用统一的标准对它们进行对比,看看它们的对比情况
我始终坚定地认为在这两个方案中,并不能以单个标准就能评判出谁一定比谁好,因为它们都有各自的优缺点,都不是完美的,从更客观的角度来看,它们谁都不能成为最好的那个,只有可能明确了它们各自的优缺点,知道在它们的优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,从而选择出一个最适合自己项目的屏幕适配方案
单纯的争论谁是最好的 Android 屏幕适配方案没有任何意义,每个人的需求不一样,站的角度不一样,评判标准也不一样,你能接受的东西他不一定能接受,你觉得不可接受的东西他却觉得可以接受,你有你的理由,他有他的理由,想让一个观点让所有人都能接受太难了!所以我在这里只是列出它们的对比项和对比结果,尽可能的做到客观,最后的选择结果请自行决定,如果还有什么遗漏的对比项,请补充!
对比项目 | 对比对象 A | 对比结果 | 对比对象 B |
---|---|---|---|
适配效果(越高越好) | 今日头条适配方案 | ≈ | SW 限定符适配方案(在未覆盖的机型上会存在一定的误差) |
稳定性(越高越好) | 今日头条适配方案 | < | SW 限定符适配方案 |
灵活性(越高越好) | 今日头条适配方案 | > | SW 限定符适配方案 |
扩展性(越高越好) | 今日头条适配方案 | > | SW 限定符适配方案 |
侵入性(越低越好) | 今日头条适配方案 | < | SW 限定符适配方案 |
使用成本(越低越好) | 今日头条适配方案 | < | SW 限定符适配方案 |
维护成本(越低越好) | 今日头条适配方案 | < | SW 限定符适配方案 |
性能损耗 | 今日头条适配方案没有性能损耗 | = | SW 限定符适配方案没有性能损耗 |
副作用 | 今日头条适配方案会影响一些三方库和系统控件 | ≈ | SW 限定符适配方案会影响 App 的体积 |
可以看到 SmallestWidth 限定符适配方案 和 今日头条适配方案 的适配效果其实都是差不多的,我在前面的文章中也通过公式计算过它们的精确度,SmallestWidth 限定符适配方案 运行在未覆盖的机型上虽然也可以适配,但是却会出现一定的误差,所以 今日头条适配方案 的适配精确度确实要比 SmallestWidth 限定符适配方案 略高的,不过只要 SmallestWidth 限定符适配方案 合理的分配资源文件,适配效果的差距应该也不大
SmallestWidth 限定符适配方案 主打的是稳定性,在运行过程中极少会出现安全隐患,适配范围也可控,不会产生其他未知的影响,而 今日头条适配方案 主打的是降低开发成本、提高开发效率,使用上更灵活,也能满足更多的扩展需求,简单一句话概括就是,这两兄弟,一个求稳,一个求快,好了,我就介绍这么多了,自己选择吧!
下面就开始介绍我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize,大家千万不要认为,我推出的屏幕适配框架 AndroidAutoSize 是根据 今日头条屏幕适配方案 优化的,我本人就一定支持 今日头条屏幕适配方案 是最好的 Android 屏幕适配方案这个观点,它确实很优秀,但同样也有很多不足,我最真实的观点在上面就已经表述咯,至于我为什么要根据 今日头条屏幕适配方案 再封装一个屏幕适配框架,无外乎就以下几点原因:
SmallestWidth 限定符适配方案 已经有多个优秀的开源解决方案了,它们已经能满足我们日常开发中的所有需求
今日头条 官方技术团队只公布了 今日头条屏幕适配方案 的 文章 以及核心代码,但并没有在 Github 上创建公开的仓库,一个新的方案必定要有一个成长迭代的过程,在此期间,一定需要一个可以把所有使用者聚集起来的公共社区,可以让所有使用该方案的使用者在上面交流,大家一起总结、一起填坑,这样才能让该方案更成熟稳定,这就是开源的力量
今日头条 官方技术团队公布的核心代码并不能满足我的所有需求,已经开源的其他基于 今日头条屏幕适配方案 的开源项目以及解决方案也不能满足我的所有需求,而我有更好的实现想法
MVPArms 需要一个适配效果还不错并且切换维护成本也比较低的屏幕适配框架,以帮助使用者用较低的成本、工作量将已经停止维护的 AndroidAutoLayout 快速替换掉
我建议大家都可以去实际体验一下 今日头条屏幕适配方案 和 SmallestWidth 限定符适配方案,感受下它们的异同,我给的建议是,可以在项目中先使用 今日头条屏幕适配方案,感受下它的使用方式以及适配效果,今日头条屏幕适配方案 的侵入性非常低,如果在使用过程中遇到什么不能解决的问题,马上可以切换为其他的屏幕适配方案,在切换的过程中也花费不了多少工作量,试错成本非常低
但如果你在项目中先使用 SmallestWidth 限定符适配方案,之后在使用的过程中再遇到什么不能解决的问题,这时想切换为其他的屏幕适配方案,这工作量可就大了,每个 Layout 文件都含有大量的 dimens 引用,改起来这工作量得有多大,想想都觉得后怕,这就是侵入性太高导致的最致命的问题
AndroidAutoSize 与 今日头条屏幕适配方案 的关系,相当于汽车和发动机的关系,今日头条屏幕适配方案 官方公布的代码,只实现了修改系统 density 的相关逻辑,这的确在屏幕适配中起到了最关键的作用,但这还远远还不够
要想让使用者能够更傻瓜式的使用该方案,并且能够应对日常开发中的所有复杂需求,那在架构框架时,还需要考虑 API 的易用性以及合理性、框架的扩展性以及灵活性、功能的全面性、注释和文档的易读性等多个方面的问题
于是我带着我的这些标准在网上搜寻了很久,发现并没有任何一个开源框架或解决方案能够达到我的所有标准,它们大多数还只是停留在将 今日头条屏幕适配方案 封装成工具类来引入项目的阶段,这样在功能的扩展上有限制,并且对用户的使用体验也不好,而我想做的是一个全面性的产品级屏幕适配框架,这离我最初的构想,差距还非常大,于是我只好自己动手,将我的所有思想实现,这才有了 AndroidAutoSize
写完 AndroidAutoSize 框架后,因为对 今日头条屏幕适配方案 有了更加深入的理解,所以才写了 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案,以帮助大家更清晰的理解 今日头条屏幕适配方案
AndroidAutoSize 因为名字和 鸿神 的 AndroidAutoLayout 非常相似,并且在填写设计图尺寸的方式上也极为相似,再加上我写的屏幕适配系列的文章也发布在了 鸿神 的公众号上,所以很多人以为 AndroidAutoSize 是 鸿神 写的 AndroidAutoLayout 的升级版,这里我哭笑不得 😂,我只好在这里说一句,大家好,我叫 JessYan,的确可以理解为 AndroidAutoSize 是 AndroidAutoLayout 的升级版,但是它是我写的,关注一波呗
但 AndroidAutoSize 和 AndroidAutoLayout 的原理,却天差地别,比如 AndroidAutoLayout 只能使用 px 作为布局单位,而 AndroidAutoSize 恰好相反,在布局中 dp、sp、pt、in、mm 所有的单位都能支持,唯独不支持 px,但这也意味着 AndroidAutoSize 和 AndroidAutoLayout 在项目中可以共存,互不影响,所以使用 AndroidAutoLayout 的老项目也可以放心的引入 AndroidAutoSize,慢慢的完成屏幕适配框架的切换
之所以将框架取名为 AndroidAutoSize,第一,是想致敬 AndroidAutoLayout 对 Android 屏幕适配领域的贡献,第二,也想成为在 Android 屏幕适配领域有重要影响力的框架
我在上面就已经说了很多开源框架以及解决方案,只是把 今日头条屏幕适配方案 简单的封装成一个工具类然后引入项目,这时很多人就会说了 今日头条屏幕适配方案 官方公布的全部代码都只有 30 行不到,你不把它封装成工具类,那封装成什么?该怎么封装?下面就来看看 AndroidAutoSize 的整体结构
├── external
│ ├── ExternalAdaptInfo.java
│ ├── ExternalAdaptManager.java
│── internal
│ ├── CancelAdapt.java
│ ├── CustomAdapt.java
│── unit
│ ├── Subunits.java
│ ├── UnitsManager.java
│── utils
│ ├── AutoSizeUtils.java
│ ├── LogUtils.java
│ ├── Preconditions.java
│ ├── ScreenUtils.java
├── ActivityLifecycleCallbacksImpl.java
├── AutoAdaptStrategy.java
├── AutoSize.java
├── AutoSizeConfig.java
├── DefaultAutoAdaptStrategy.java
├── DisplayMetricsInfo.java
├── FragmentLifecycleCallbacksImpl.java
├── InitProvider.java
AndroidAutoSize 根据 今日头条屏幕适配方案 官方公布的 30 行不到的代码,经过不断的优化和扩展,发展成了现在拥有 18 个类文件,上千行代码的全面性屏幕适配框架,在迭代的过程中完善和优化了很多功能,相比 今日头条屏幕适配方案 官方公布的原始代码,AndroidAutoSize 更加稳定、更加易用、更加强大,欢迎阅读源码,注释非常详细哦!
AndroidAutoSize 在使用上非常简单,只需要填写设计图尺寸这一步即可接入项目,但需要注意的是,AndroidAutoSize 有两种类型的布局单位可以选择,一个是 主单位 (dp、sp),一个是 副单位 (pt、in、mm),两种单位面向的应用场景都有不同,也都有各自的优缺点
主单位: 使用 dp、sp 为单位进行布局,侵入性最低,会影响其他三方库页面、三方库控件以及系统控件的布局效果,但 AndroidAutoSize 也通过这个特性,使用 ExternalAdaptManager 实现了在不修改三方库源码的情况下适配三方库的功能
副单位: 使用 pt、in、mm 为单位进行布局,侵入性高,对老项目的支持比较好,不会影响其他三方库页面、三方库控件以及系统控件的布局效果,可以彻底的屏蔽修改 density 所造成的所有未知和已知问题,但这样 AndroidAutoSize 也就无法对三方库进行适配
大家可以根据自己的应用场景在 主单位 和 副单位 中选择一个作为布局单位,建议想引入老项目并且注重稳定性的人群使用 副单位,只是想试试本框架,随时可能切换为其他屏幕适配方案的人群使用 主单位
其实 AndroidAutoSize 可以同时支持 主单位 和 副单位,但 AndroidAutoSize 可以同时支持 主单位 和 副单位 的目的,只是为了让使用者可以在 主单位 和 副单位 之间灵活切换,因为切换单位的工作量可能非常巨大,不能立即完成,但领导又要求马上打包上线,这时就可以起到一个很好的过渡作用
主单位 的 Demo 在 demo
将 AndroidAutoSize 引入项目后,只要在 app 的 AndroidManifest.xml 中填写上设计图尺寸,无需其他过多配置 (如果你没有其他自定义需求的话),AndroidAutoSize 即可自动运行,像下面这样👇
在使用主单位时,design_width_in_dp
和 design_height_in_dp
的单位必须是 dp,如果设计师给你的设计图,只标注了 px 尺寸 (现在已经有很多 UI 工具可以自动标注 dp 尺寸了),那请自行根据公式 dp = px / (DPI / 160) 将 px 尺寸转换为 dp 尺寸,如果你不知道 DPI 是多少?那请以自己测试机的 DPI 为准,如果连怎么得到设备的 DPI 都不知道?百度吧好伐,如果你实在找不到设备的 DPI 那就直接将 px 尺寸除以 3 或者 2 也是可以的
如果你只是想使用 AndroidAutoSize 的基础功能,AndroidAutoSize 的使用方法在这里就结束了,只需要上面这一步,即可帮助你以最简单的方式接入 AndroidAutoSize,但是作为一个全面性的屏幕适配框架,在保证基础功能的简易性的同时,也必须保证复杂的需求也能在框架内被解决,从而达到一个小闭环,所以下面介绍的内容全是前人踩坑踩出来的一些必备功能,如果你没这个需求,或者觉得麻烦,可以按需查看或者跳过,下面的内容建议和 Demo 配合起来阅读,效果更佳
design_width_in_dp
和 design_height_in_dp
虽然都需要填写,但是 AndroidAutoSize 只会将高度和宽度其中的一个作为基准进行适配,一方作为基准,另一方就会变为备用,默认以宽度为基准进行适配,可以通过 AutoSizeConfig#setBaseOnWidth(Boolean) 不停的切换,这意味着最后运行到设备上的布局效果,在高度和宽度中只有一方可以和设计图上一模一样,另外一方会和设计图出现偏差,为什么不像 AndroidAutoLayout 一样,高和宽都以设计图的效果等比例完美呈现呢?这也很简单,你无法保证所有设备的高宽比例都和你设计图上的高宽比例一致,特别是在现在全面屏全面推出的情况下,如果这里不这样做的话,当你的项目运行在与设计图高宽比例不一致的设备上时,布局会出现严重的变形,这个几率非常大,详情请看 这里很多人有疑惑,为什么使用者只需要在 AndroidManifest.xml 中填写一下 meta-data 标签,其他什么都不做,AndroidAutoSize 就能自动运行,并在 App 启动时自动解析 AndroidManifest.xml 中填写的设计图尺寸,这里很多人不敢相信,问我真的只需要填写下设计图尺寸框架就可以正常运行吗?难道使用了什么 黑科技?
其实这里并没有用到什么 黑科技,原理反而非常简单,只需要声明一个 ContentProvider,在它的 onCreate 方法中启动框架即可,在 App 启动时,系统会在 App 的主进程中自动实例化你声明的这个 ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,可以做一些初始化的工作,get 到了吗?
这里需要注意的是,如果你的项目拥有多进程,系统只会在主进程中实例化一个你声明的 ContentProvider,并不会在其他非主进程中实例化 ContentProvider,如果在当前进程中 ContentProvider 没有被实例化,那 ContentProvider#onCreate 就不会被调用,你的初始化代码在当前进程中也就不会执行,这时就需要在 Application#onCreate 中调用下 ContentProvider#query 执行一下查询操作,这时 ContentProvider 就会在当前进程中实例化 (每个进程中只会保证有一个实例),所以应用到框架中就是,如果你需要在多个进程中都进行屏幕适配,那就需要在 Application#onCreate 中调用 AutoSize#initCompatMultiProcess 方法
虽然 AndroidAutoSize 不需要其他过多的配置,只需要在 AndroidManifest.xml 中填写下设计图尺寸就能正常运行,但 AndroidAutoSize 还是为大家准备了很多可配置选项,尽最大可能满足大家日常开发中的所有扩展需求
所有的全局配置选项在 Demo 中都有介绍,每个 API 中也都有详细的注释,在这里就不过多介绍了
在 AndroidManifest.xml 中填写的设计图尺寸,是整个项目的全局设计图尺寸,但是如果某些 Activity 页面由于某些原因,设计师单独出图,这个页面的设计图尺寸和在 AndroidManifest.xml 中填写的设计图尺寸不一样该怎么办呢?不要急,AndroidAutoSize 已经为你考虑好了,让这个页面的 Activity 实现 CustomAdapt 接口即可实现你的需求,CustomAdapt 接口的第一个方法可以修改当前页面的设计图尺寸,第二个方法可以切换当前页面的适配基准,下面的注释都解释的很清楚
public class CustomAdaptActivity extends AppCompatActivity implements CustomAdapt {
/**
* 是否按照宽度进行等比例适配 (为了保证在高宽比不同的屏幕上也能正常适配, 所以只能在宽度和高度之中选择一个作为基准进行适配)
*
* @return {@code true} 为按照宽度进行适配, {@code false} 为按照高度进行适配
*/
@Override
public boolean isBaseOnWidth() {
return false;
}
/**
* 这里使用 iPhone 的设计图, iPhone 的设计图尺寸为 750px * 1334px, 高换算成 dp 为 667 (1334px / 2 = 667dp)
*
* 返回设计图上的设计尺寸, 单位 dp
* {@link #getSizeInDp} 须配合 {@link #isBaseOnWidth()} 使用, 规则如下:
* 如果 {@link #isBaseOnWidth()} 返回 {@code true}, {@link #getSizeInDp} 则应该返回设计图的总宽度
* 如果 {@link #isBaseOnWidth()} 返回 {@code false}, {@link #getSizeInDp} 则应该返回设计图的总高度
* 如果您不需要自定义设计图上的设计尺寸, 想继续使用在 AndroidManifest 中填写的设计图尺寸, {@link #getSizeInDp} 则返回 {@code 0}
*
* @return 设计图上的设计尺寸, 单位 dp
*/
@Override
public float getSizeInDp() {
return 667;
}
}
如果某个 Activity 想放弃适配,让这个 Activity 实现 CancelAdapt 接口即可,比如修改 density 影响到了老项目中的某些 Activity 页面的布局效果,这时就可以让这个 Activity 实现 CancelAdapt 接口
public class CancelAdaptActivity extends AppCompatActivity implements CancelAdapt {
}
Fragment 的自定义方式和 Activity 是一样的,只不过在使用前需要先在 App 初始化时开启对 Fragment 的支持
AutoSizeConfig.getInstance().setCustomFragment(true);
实现 CustomAdapt
public class CustomAdaptFragment extends Fragment implements CustomAdapt {
@Override
public boolean isBaseOnWidth() {
return false;
}
@Override
public float getSizeInDp() {
return 667;
}
}
实现 CancelAdapt
public class CancelAdaptFragment extends Fragment implements CancelAdapt {
}
在使用主单位时可以使用 ExternalAdaptManager 来实现在不修改三方库源码的情况下,适配三方库的所有页面 (Activity、Fragment)
由于 AndroidAutoSize 要求需要自定义适配参数或取消适配的页面必须实现 CustomAdapt、CancelAdapt,这时问题就来了,三方库是通过远程依赖的,我们无法修改它的源码,这时我们怎么让三方库的页面也能实现自定义适配参数或取消适配呢?别急,这个需求 AndroidAutoSize 也已经为你考虑好了,当然不会让你将三方库下载到本地然后改源码!
通过 ExternalAdaptManager#addExternalAdaptInfoOfActivity(Class, ExternalAdaptInfo) 将需要自定义的类和自定义适配参数添加进方法即可替代实现 CustomAdapt 的方式,这里 展示了使用方式,以及详细的注释
通过 ExternalAdaptManager#addCancelAdaptOfActivity(Class) 将需要取消适配的类添加进方法即可替代实现 CancelAdapt 的方式,这里 也展示了使用方式,以及详细的注释
需要注意的是 ExternalAdaptManager 的方法虽然可以添加任何类,但是只能支持 Activity、Fragment,并且 ExternalAdaptManager 是支持链式调用的,以便于持续添加多个页面
当然 ExternalAdaptManager 不仅可以对三方库的页面使用,也可以让自己项目中的 Activity、Fragment 不用实现 CustomAdapt、CancelAdapt 即可达到自定义适配参数和取消适配的功能
前面已经介绍了 副单位 的应用场景,这里就直接介绍 副单位 如何使用,副单位 的 Demo 在 demo-subunits
首先和 主单位 一样也需要先在 app 的 AndroidManifest.xml 中填写上设计图尺寸,但和 主单位 不一样的是,当在使用 副单位 时 design_width_in_dp
和 design_height_in_dp
的单位不需要一定是 dp,可以直接填写设计图的 px 尺寸,在布局文件中每个控件的大小也可以直接填写设计图上标注的 px 尺寸,无需再将 px 转换为 dp,这是 副单位的 特性之一,可以帮助大家提高开发效率
由于 AndroidAutoSize 提供了 pt、in、mm 三种类型的 副单位 供使用者选择,所以在使用 副单位 时,还需要在 APP 初始化时,通过 UnitsManager#setSupportSubunits(Subunits) 方法选择一个你喜欢的副单位,然后在布局文件中使用这个副单位进行布局,三种类型的副单位,其实效果都是一样,大家按喜欢的名字选择即可
由于使用副单位是为了彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,所以在使用副单位时建议调用 UnitsManager#setSupportDP(false) 和 UnitsManager#setSupportSP(false),关闭 AndroidAutoSize 对 dp 和 sp 的支持,AndroidAutoSize 为什么不在使用 副单位 时默认关闭对 dp、sp 的支持?因为允许同时支持 主单位 和 副单位 可以帮助使用者在 主单位 和 副单位 之间切换时更好的过渡,这点在前面就已经提到过
UnitsManager 的详细使用方法,在 demo-subunits 中都有展示,注释也十分详细
在使用 副单位 时自定义 Activity 和 Fragment 的方式是和 主单位 是一样的,这里就不再过多介绍了
如果你的项目在使用 副单位 并且关闭了对 主单位 (dp、sp) 的支持,这时 ExternalAdaptManager 对三方库的页面是不起作用的,只对自己项目中的页面起作用,除非三方库的页面也使用了副单位 (pt、in、mm) 进行布局
其实 副单位 之所以能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,就是因为三方库页面、三方库控件以及系统控件基本上使用的都是 dp、sp 进行布局,所以只要 AndroidAutoSize 关闭了对 dp、sp 的支持,转而使用 副单位 进行布局,就能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响
但这也同样意味着使用 副单位 就不能适配三方库的页面了,ExternalAdaptManager 也就对三方库的页面不起作用了
在开发阶段布局时的实时预览是一个很重要的环节,很多情况下 Android Studio 提供的默认预览设备并不能完全展示我们的设计图,所以我们就需要自己创建模拟设备,dp、pt、in、mm 这四种单位的模拟设备创建方法请看 这里
AndroidAutoSize 在经历了 240+ commit、60+ issues、6 个版本 的洗礼后,逐渐的稳定了下来,已经在上个星期发布了首个正式版,在这里要感谢将 AndroidAutoSize 接入到自己项目中的上千个使用者,感谢他们的信赖,AndroidAutoSize 创建的初衷就是为了让所有使用 今日头条屏幕适配方案 的使用者能有一个可以一起交流、沟通的聚集地,所以后面也会持续的收集并解决 今日头条屏幕适配方案的常见问题,让 今日头条屏幕适配方案 变得更加成熟、稳定
至此本系列的第三篇文章也就完结了,这也预示着这个系列连载的终结,这篇文章建议结合系列的第一篇文章 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案 一起看,这样可以对 今日头条屏幕适配方案 有一个更深入的理解,如果你能将整个系列的文章都全部认真看完,那你对 Android 屏幕适配领域的相关知识绝对会有一个飞速的提升!
当你的项目需要切换某个框架时,你会怎么去考察、分析、对比现有的开源方案,并有足够的理由去选择或优化一个最适合自己项目的方案呢?其实整个系列文章可以看作是我怎么去选择同类型开源方案的过程,你以后当遇到同样的选择也可以参照我的思维方式去处理,当然如果以后面试官问到你屏幕适配相关的问题,你能将我如何选择、分析、对比已有方案的过程以及文章中的核心知识点告诉给面试官,那肯定比你直接说一句我使用的是某某开源库有价值得多
ok,根据上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 smallestWidth 限定符屏幕适配方案
了解我的朋友一定知道,MVPArms 一直使用的是 鸿神 的 AndroidAutoLayout 屏幕适配方案,得益于 AndroidAutoLayout 的便捷,所以我对屏幕适配领域研究的不是很多,AndroidAutoLayout 停止维护后,我也一直在找寻着替代方案,直到 今日头条屏幕适配方案 刷屏,后来又无意间看到了 smallestWidth 限定符屏幕适配方案,这才慢慢的将研究方向转向了屏幕适配领域
最近一个月才开始慢慢恶补 Android 屏幕适配的相关知识,对这两个方案也进行了更深入的研究,可以说从一个小白慢慢成长而来,所以我明白小白的痛,因此在上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 中,把 今日头条屏幕适配方案 讲得非常的细,尽量把每一个知识点都描述清晰,深怕小白漏掉每一个细节,这篇文章我也会延续上一篇文章的优良传统,将 smallestWidth 限定符屏幕适配方案 的每一个知识点都描述清晰
顺便说一句,感谢大家对 AndroidAutoSize 的支持,我只是在上一篇文章中提了一嘴我刚发布的屏幕适配框架 AndroidAutoSize,还没给出详细的介绍和原理剖析 (原计划在本系列的第三篇文章中发布),AndroidAutoSize 就被大家推上了 Github Trending,一个多星期就拿了 2k+ stars,随着关注度的增加,我在这段时间里也累坏了,issues 就没断过,不到半个月就提交了 200 多次 commit,但累并快乐着,在这里要再次感谢大家对 AndroidAutoSize 的认可
大家要注意了!这些观点其实针对的是所有以百分比缩放布局的库,而不只是今日头条屏幕适配方案,所以这些观点也同样适用于 smallestWidth 限定符屏幕适配方案,这点有很多人存在误解,所以一定要注意!
Android 的 系统碎片化、机型以及屏幕尺寸碎片化、屏幕分辨率碎片化 有多严重大家可以通过 友盟指数 了解一下,有些时候在某些事情的决断标准上,并不能按照事情的对错来决断,大多数情况还是要分析成本,收益等多种因素,通过利弊来决断,每个人的利弊标准又都不一样,所以每个人的观点也都会有差别,但也都应该得到尊重,所以我只是说说自己的观点,也不否认任何人的观点
方案是死的人是活的,在某些大屏手机或平板电脑上,您也可以采用其他适配方案和百分比库结合使用,比如针对某个屏幕区间的设备单独出一套设计图以显示比小屏幕手机更多更精细的内容,来达到与百分比库互补的效果,没有一个方案可以说自己是完美的,但我们能清晰的认识到不同方案的优缺点,将它们的优点相结合,才能应付更复杂的开发需求,产出最好的产品
友情提示: 下面要介绍的 smallestWidth 限定符屏幕适配方案,原理也同样是按照百分比缩放布局,理论上也会存在上面所说的 大屏手机和小屏手机显示的内容相同 的问题,选择与否请仔细斟酌
这个方案的的使用方式和我们平时在布局中引用 dimens 无异,核心点在于生成 dimens.xml 文件,但是已经有大神帮我们做了这 一步
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440
如果有人还记得上面这种 宽高限定符屏幕适配方案 的话,就可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,smallestWidth 限定符屏幕适配方案 长这样👇
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
│ ├── ├──...
│ ├── ├──values-sw600dp
│ ├── ├──values-sw640dp
其实 smallestWidth 限定符屏幕适配方案 的原理也很简单,开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-sw
如果系统根据当前设备屏幕的 最小宽度 (smallestWidth) 没找到对应的 values-sw
smallestWidth 翻译为中文的意思就是 最小宽度,那这个 最小宽度 是什么意思呢?
系统会根据当前设备屏幕的 最小宽度 来匹配 values-sw
这就要说到,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度,这个 最小宽度 是根据屏幕来定的,是固定不变的,意思是不管您怎么旋转屏幕,只要这个屏幕的高度大于宽度,那系统就只会认定宽度的值为 最小宽度,反之如果屏幕的宽度大于高度,那系统就会认定屏幕的高度的值为 最小宽度
如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?可以再根据 values-w
如果想区分屏幕的方向来做适配该怎么办呢?那就只有再根据 屏幕方向限定符 生成一套资源文件咯,后缀加上 -land 或 -port 即可,像这样,values-sw400dp-land (最小宽度 400 dp 横向),values-sw400dp-port (最小宽度 400 dp 纵向)
要先算出当前设备的 smallestWidth 值我们才能知道当前设备该匹配哪个 values-sw
ok,还是按照上一篇文章的叙述方式,现在来举栗说明,帮助大家更好理解
我们假设设备的屏幕信息是 1920 * 1080、480 dpi
根据上面的规则我们要在屏幕的高度和宽度中选择值最小的一方作为最小宽度,1080 < 1920,明显 1080 px 就是我们要找的 最小宽度 的值,但 最小宽度 的单位是 dp,所以我们要把 px 转换为 dp
帮助大家再巩固下基础,下面的公式一定不能再忘了!
px / density = dp,DPI / 160 = density,所以最终的公式是 px / (DPI / 160) = dp
所以我们得到的 最小宽度 的值是 360 dp (1080 / (480 / 160) = 360)
现在我们已经算出了当前设备的最小宽度是 360 dp,我们晓得系统会根据这个 最小宽度 帮助我们匹配到 values-sw360dp 文件夹下的 dimens.xml 文件,如果项目中没有 values-sw360dp 这个文件夹,系统才会去匹配相近的 values-sw
dimens.xml 文件是整个方案的核心所在,所以接下来我们再来看看 values-sw360dp 文件夹中的这个 dimens.xml 是根据什么原理生成的
因为我们在项目布局中引用的 dimens 的实际值,来源于根据当前设备屏幕的 最小宽度 所匹配的 values-sw
说到 dimens.xml 的生成,就要涉及到两个因数,第一个因素是 最小宽度基准值,第二个因素就是您的项目需要适配哪些 最小宽度,通俗理解就是需要生成多少个 values-sw
最小宽度基准值 是什么意思呢?简单理解就是您需要把设备的屏幕宽度分为多少份,假设我们现在把项目的 最小宽度基准值 定为 360,那这个方案就会理解为您想把所有设备的屏幕宽度都分为 360 份,方案会帮您在 dimens.xml 文件中生成 1 到 360 的 dimens 引用,比如 values-sw360dp 中的 dimens.xml 是长这样的
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_7">7dp</dimen>
<dimen name="dp_8">8dp</dimen>
<dimen name="dp_9">9dp</dimen>
<dimen name="dp_10">10dp</dimen>
...
<dimen name="dp_356">356dp</dimen>
<dimen name="dp_357">357dp</dimen>
<dimen name="dp_358">358dp</dimen>
<dimen name="dp_359">359dp</dimen>
<dimen name="dp_360">360dp</dimen>
</resources>
values-sw360dp 指的是当前设备屏幕的 最小宽度 为 360dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 360dp),把屏幕宽度分为 360 份,刚好每份等于 1dp,所以每个引用都递增 1dp,值最大的 dimens 引用 dp_360 值也是 360dp,刚好覆盖屏幕宽度
下面再来看看将 最小宽度基准值 定为 360 时,values-sw400dp 中的 dimens.xml 长什么样
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.1111dp</dimen>
<dimen name="dp_2">2.2222dp</dimen>
<dimen name="dp_3">3.3333dp</dimen>
<dimen name="dp_4">4.4444dp</dimen>
<dimen name="dp_5">5.5556dp</dimen>
<dimen name="dp_6">6.6667dp</dimen>
<dimen name="dp_7">7.7778dp</dimen>
<dimen name="dp_8">8.8889dp</dimen>
<dimen name="dp_9">10.0000dp</dimen>
<dimen name="dp_10">11.1111dp</dimen>
...
<dimen name="dp_355">394.4444dp</dimen>
<dimen name="dp_356">395.5556dp</dimen>
<dimen name="dp_357">396.6667dp</dimen>
<dimen name="dp_358">397.7778dp</dimen>
<dimen name="dp_359">398.8889dp</dimen>
<dimen name="dp_360">400.0000dp</dimen>
</resources>
values-sw400dp 指的是当前设备屏幕的 最小宽度 为 400dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 400dp),把屏幕宽度同样分为 360份,这时每份就等于 1.1111dp 了,每个引用都递增 1.1111dp,值最大的 dimens 引用 dp_360 同样刚好覆盖屏幕宽度,为 400dp
通过两个 dimens.xml 文件的比较,dimens.xml 的生成原理一目了然,方案会先确定 最小宽度基准值,然后将每个 values-sw
这样就能保证不管将项目运行到哪个设备上,只要当前设备能匹配到对应的 values-sw
说到这里,那大家就应该就会明白我为什么会说 smallestWidth 限定符屏幕适配方案 的原理也同样是按百分比进行布局,如果在布局中,一个 View 的宽度引用 dp_100,那不管运行到哪个设备上,这个 View 的宽度都是当前设备屏幕总宽度的 360分之100,前提是项目提供有当前设备屏幕对应的 values-sw
其实 smallestWidth 限定符屏幕适配方案 的原理和 今日头条屏幕适配方案 挺像的,今日头条屏幕适配方案 是根据屏幕的宽度或高度动态调整每个设备的 density (每 dp 占当前设备屏幕多少像素),而 smallestWidth 限定符屏幕适配方案 同样是根据屏幕的宽度动态调整每个设备 每份占的 dp 值
第二个因数是需要适配哪些 最小宽度?比如您想适配的 最小宽度 有 320dp、360dp、400dp、411dp、480dp,那方案就会为您的项目生成 values-sw320dp、values-sw360dp、values-sw400dp、values-sw411dp、values-sw480dp 这几个资源文件夹,像这样👇
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
方案会为您需要适配的 最小宽度,在项目中生成一系列对应的 values-sw
那是不是 values-sw
也不是,因为每个 values-sw
所以一定要合理分配 values-sw
原理讲完了,我们还是按照老规矩,来验证一下这个方案是否可行?
假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133)
在使用 smallestWidth 限定符屏幕适配方案 时,需要提供 最小宽度基准值 和需要适配哪些 最小宽度,我们就把 最小宽度基准值 设置为 375 (和 设计图 一致),这时方案就会为我们需要适配的 最小宽度 生成对应的 values-sw
下面就来验证下在使用 smallestWidth 限定符屏幕适配方案 的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致
设备 1 的屏幕总宽度为 1080 px,屏幕总高度为 1920 px,DPI 为 480
设备 1 的屏幕高度大于屏幕宽度,所以 设备 1 的 最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 1 的 最小宽度 的值为 360 dp (1080 / (480 / 160) = 360)
根据 设备 1 的 最小宽度 应该匹配的是 values-sw360dp 这个文件夹,假设 values-sw360dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值 为 375 生成的,360 / 375 = 0.96,所以每份占的 dp 值为 0.96,dimens.xml 里面的内容是长下面这样的
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">0.96dp</dimen>
<dimen name="dp_2">1.92dp</dimen>
<dimen name="dp_3">2.88dp</dimen>
<dimen name="dp_4">3.84dp</dimen>
<dimen name="dp_5">4.8dp</dimen>
...
<dimen name="dp_50">48dp</dimen>
...
<dimen name="dp_371">356.16dp</dimen>
<dimen name="dp_372">357.12dp</dimen>
<dimen name="dp_373">358.08dp</dimen>
<dimen name="dp_374">359.04dp</dimen>
<dimen name="dp_375">360dp</dimen>
</resources>
可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw360dp 中定格在了 48 dp,所以这个 View 在 设备 1 上的高宽都为 48 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 144 px (48 * (480 / 160) = 144)
144 / 1080 = 0.133,View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放
某些设备的高宽是和 设备 1 相同的,但是 DPI 可能不同,而由于 smallestWidth 限定符屏幕适配方案 并没有像 今日头条屏幕适配方案 一样去自行修改 density,所以系统就会使用默认的公式 DPI / 160 求出 density,density 又会影响到 dp 和 px 的换算,因此 DPI 的变化,是有可能会影响到 smallestWidth 限定符屏幕适配方案 的
所以我们再来试试在这种特殊情况下 smallestWidth 限定符屏幕适配方案 是否也能完成适配
设备 2 的屏幕总宽度为 1080 px,屏幕总高度为 1920 px,DPI 为 420
设备 2 的屏幕高度大于屏幕宽度,所以 设备 2 的 最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 2 的 最小宽度 的值为 411.429 dp (1080 / (420 / 160) = 411.429)
根据 设备 2 的 最小宽度 应该匹配的是 values-sw411dp 这个文件夹,假设 values-sw411dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值 为 375 生成的,411 / 375 = 1.096,所以每份占的 dp 值为 1.096,dimens.xml 里面的内容是长下面这样的👇
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.096dp</dimen>
<dimen name="dp_2">2.192dp</dimen>
<dimen name="dp_3">3.288dp</dimen>
<dimen name="dp_4">4.384dp</dimen>
<dimen name="dp_5">5.48dp</dimen>
...
<dimen name="dp_50">54.8dp</dimen>
...
<dimen name="dp_371">406.616dp</dimen>
<dimen name="dp_372">407.712dp</dimen>
<dimen name="dp_373">408.808dp</dimen>
<dimen name="dp_374">409.904dp</dimen>
<dimen name="dp_375">411dp</dimen>
</resources>
可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw411dp 中定格在了 54.8dp,所以这个 View 在 设备 2 上的高宽都为 54.8 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 143.85 px (54.8 * (420 / 160) = 143.85)
143.85 / 1080 = 0.133,View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放
虽然 View 在 设备 2 上的高宽是 143.85 px,比 设备 1 的 144 px 少了 0.15 px,但是误差非常小,整体的比例并没有发生太大的变化,是完全可以接受的
这个误差是怎么引起的呢,因为 设备 2 的 最小宽度 的实际值是 411.429 dp,但是匹配的 values-sw411dp 舍去了小数点后面的位数 (切记!系统会去寻找小于或等于 411.429 dp 的 values-sw
可以看到即使在高宽一样但 DPI 不一样的设备上,smallestWidth 限定符屏幕适配方案 也能完成等比例适配,证明这个方案是可行的,如果大家还心存疑虑,也可以再试试其他分辨率的设备,其实到最后得出的比例都是在 0.133 左右,唯一的变数就是第二个因数,如果您生成的 values-sw
非常稳定,极低概率出现意外
不会有任何性能的损耗
适配范围可自由控制,不会影响其他三方库
在插件的配合下,学习成本低
在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦
侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂
无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择
如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积
不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-w
不能以高度为基准进行适配,考虑到这个方案的名字本身就叫 最小宽度限定符适配方案,所以在使用这个方案之前就应该要知道这个方案只能以宽度为基准进行适配,为什么现在的屏幕适配方案只能以高度或宽度其中的一个作为基准进行适配,请看 这里
这时有人就会问了,设计师给的设计图只标注了 px,使用这个方案时,那不是还要先将 px 换算成 dp?
其实也可以不用换算的,那这是什么骚操作呢?
很简单,你把设计图的 px 总宽度设置成 最小宽度基准值 就可以了,还是以前面验证可行性的例子
我们在前面验证可行性时把 最小宽度基准值 设置成了 375,为什么是 375 呢?因为设计图的总宽度为 375 dp,如果换算成 px,总宽度就是 750 px,我们这时把 最小宽度基准值 设置成 750,然后看看 values-sw360dp 中的 dimens.xml 长什么样👇<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="px_1">0.48dp</dimen>
<dimen name="px_2">0.96dp</dimen>
<dimen name="px_3">1.44dp</dimen>
<dimen name="px_4">1.92dp</dimen>
<dimen name="px_5">2.4dp</dimen>
...
<dimen name="px_50">24dp</dimen>
...
<dimen name="px_100">48dp</dimen>
...
<dimen name="px_746">358.08dp</dimen>
<dimen name="px_747">358.56dp</dimen>
<dimen name="px_748">359.04dp</dimen>
<dimen name="px_749">359.52dp</dimen>
<dimen name="px_750">360dp</dimen>
</resources>
360 dp 被分成了 750 份,相比之前的 375 份,现在 每份占的 dp 值 正好减少了一半,还记得在验证可行性的例子中那个 View 的尺寸是多少吗?50dp * 50dp,如果设计图只标注 px,那这个 View 在设计图上的的尺寸应该是 100px * 100px,那我们直接根据设计图上标注的 px,想都不用想直接在布局中引用 px_100 就可以了,因为在 375 份时的 dp_50 刚好等于 750 份时的 px_100 (值都是 48 dp),所以这时的适配效果和之前验证可行性时的适配效果没有任何区别
看懂了吗?直接将 最小宽度基准值 和布局中的引用都以 px 作为单位就可以直接填写设计图上标注的 px!
关于文中所列出的优缺点,列出的缺点数量确实比列出的优点数量多,但 缺点 3,缺点 4,缺点 5 其实都可以归纳于 占用 App 体积 这一个缺点,因为他们都可以通过增加资源文件来解决问题,而 缺点 6 则是这个方案的特色,只能以宽度为基准进行适配,这个从这个方案的名字就能看出
请大家千万不要曲解文章的意思,不要只是单纯的对比优缺点的数量,缺点的数量大于优点的数量就一定是这个方案不行?没有一个方案是完美的,每个人的需求也都不一样,作为一篇科普类文章我只可能把这个方案描述得尽可能的全面
这个方案能给你带来什么,不能给你带来什么,我必须客观的描述清楚,这样才有助你做出决定,你应该注重的是在这些优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,而不只是单纯的去看数量,这样毫无意义,有些人就是觉得稳定性最重要,其他的都可以做出妥协,那其他缺点对于他来说都是无所谓的
好了,这个系列的第二篇文章讲完了,这篇文章也是按照上篇文章的优良传统,写的非常详细,哪怕是新手我相信也应该能看懂,为什么这么多人都不知道自己该选择什么样的方案,就是因为自己都没搞懂这些方案的原理,懂了原理过后才知道这些方案是否是自己想要的
接下来的第三篇文章会详细讲解两个方案的深入对比以及该如何选择,并剖析我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize 的原理,敬请期待
如果大家想使用 smallestWidth 限定符屏幕适配方案,可以参考 这篇文章,里面提供有自动生成资源文件的插件和 Demo,由于我并没有在项目中使用 smallestWidth 限定符屏幕适配方案,所以如果在文章中有遗漏的知识点请谅解以及补充,感谢!
这个月在 Android 技术圈中 屏幕适配 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案
上上个星期 Blankj 老师发表了一篇力挺今日头条屏幕适配方案的 文章,提出了很多优化的方案,并开源了相关源码
上个星期 拉丁吴 老师在 鸿神 的公众号上发布了一篇 文章,详细描述了市面上主流的几种屏幕适配方案,并发布了他的 smallestWidth 限定符适配方案和相关源码 (其实早就发布了),文章写的很好,建议大家去看看
其实大家最关注的不是市面上有多少种屏幕适配方案,而是自己的项目该选择哪种屏幕适配方案,可以看出两位老师最终选择的屏幕适配方案都是不同的
我下面就来分析分析,我作为一个才接触这两个屏幕适配方案的吃瓜群众,我是怎么来验证这两种屏幕适配方案是否可行,以及怎样根据它们的优缺点来选择一个最适合自己项目的屏幕适配方案
在 拉丁吴 老师的文章中谈到了两个比较经典的屏幕适配方案,在我印象中十分深刻,我想大多数兄弟都用过,在我的开发生涯里也是有很长一段时间都在用这两种屏幕适配方案
第一种就是宽高限定符适配,什么是宽高限定符适配呢
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440
就是这种,在资源文件下生成不同分辨率的资源文件,然后在布局文件中引用对应的 dimens,大家一定还有印象
第二种就是 鸿神 的 AndroidAutoLayout
这两种方案都已经逐渐退出了历史的舞台,为什么想必大家都知道,不知道的建议看看 拉丁吴 老师的文章,所以这两种方案我在文章中就不在阐述了,主要讲讲现在最主流的两种屏幕适配方案,今日头条适配方案 和 smallestWidth 限定符适配方案
建议大家不清楚这两个方案的先看看这两篇文章,才清楚我在讲什么,后面我要讲解它们的原理,以及验证这两种方案是否真的可行,最后对他们进行深入对比,对于他们的一些缺点给予对应的解决方案,绝对干货
上面已经告知,不了解这两个方案的先看看上面的两篇文章,所以这里我就假设大家已经看了上面的文章或者之前就了解过这两个方案,所以在本文中我就不再阐述 DPI、Density 以及一些比较基础的知识点,上面的文章已经阐述的够清楚了
今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
density 的意思就是 1 dp 占当前设备多少像素
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为 px,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为 px 的
所以我们常用的 px 转 dp 的公式 dp = px / density,就是根据上面的方法得来的,density 在公式的运算中扮演着至关重要的一步
要看懂下面的内容,还得明白,今日头条的适配方式,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配,为什么不像 AndroidAutoLayout 一样,高以高为基准,宽以宽为基准,同时进行适配呢
这就引出了一个现在比较棘手的问题,大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致
这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题
明白这个后,我再来说说 density,density 在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
设备 1,屏幕宽度为 1080px,480DPI,屏幕总 dp 宽度为 1080 / (480 / 160) = 360dp
设备 2,屏幕宽度为 1440,560DPI,屏幕总 dp 宽度为 1440 / (560 / 160) = 411dp
可以看到屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的
这会导致什么呢?假设我们布局中有一个 View 的宽度为 100dp,在设备 1 中 该 View 的宽度占整个屏幕宽度的 27.8% (100 / 360 = 0.278)
但在设备 2 中该 View 的宽度就只能占整个屏幕宽度的 24.3% (100 / 411 = 0.243),可以看到这个 View 在像素越高的屏幕上,dp 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 dp 的屏幕适配方式产生了较大的误差
这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的
这时我们该怎么做呢?改变每个 View 的 dp 值?不现实,在每个设备上都要通过代码动态计算 View 的 dp 值,工作量太大
如果每个 View 的 dp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp 值
屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
在这个公式中我们要保证 屏幕的总 dp 宽度 和 设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配
上面已经把原理分析的很清楚了,很多文章只是一笔带过这个公式,公式虽然很简单但我们还是想晓得这是怎么来的,所以我就反向推理了一遍,如果还是看不懂,那我只能说我尽力了,原理讲完了,那我们再来现场验证一下这个方案是否可行?
假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致
屏幕总宽度为 1080 px,根据今日头条的的公式求出 density,1080 / 375 = 2.88 (density)
这个 50dp * 50dp 的 View,系统最后会将高宽都换算成 px,50dp * 2.88 = 144 px (根据公式 dp * density = px)
144 / 1080 = 0.133,View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放
某些设备总宽度为 1080 px,但是 DPI 可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据 DPI 求出 density,是根据自己的公式求出的 density,所以这对今日头条的方案没有影响
上面只能确定在所有屏幕总宽度为 1080 px 的设备上能完成等比例适配,那我们再来试试其他分辨率的设备
屏幕总宽度为 1440 px,根据今日头条的的公式求出 density,1440 / 375 = 3.84 (density)
这个 50dp * 50dp 的 View,系统最后会将高宽都换算成 px,50dp * 3.84 = 192 px (根据公式 dp * density = px)
192 / 1440 = 0.133,View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以也完成了等比例缩放
两个不同分辨率的设备都完成了等比例缩放,证明今日头条屏幕适配方案在不同分辨率的设备上都是有效的,如果大家还心存疑虑,可以再试试其他分辨率的设备,其实到最后得出的比例不会有任何偏差, 都是 0.133
使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案
侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0
可适配三方库的控件和系统的控件(不止是 Activity 和 Fragment,Dialog、Toast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益
不会有任何性能的损耗
暂时没发现其他什么很明显的缺点,已知的缺点有一个,那就是第三个优点,它既是这个方案的优点也同样是缺点,但是就这一个缺点也是非常致命的
只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的
这样不是很好吗?这样本来是很好的,但是应用到这个方案是就不好了,因为我上面的原理也分析了,这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样
当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重
假设一个三方库的 View,作者在设计时,把它设计为 100dp * 100dp,设计图的最大宽度为 1000dp,这个 View 在设计图中的比例是 100 / 1000 = 0.1,意思是这个 View 的宽度在设计图中占整个宽度的 10%,如果我们要完成等比例适配,那这个三方库 View 在所有的设备上与屏幕的总宽度的比例,都必须保持在 10%
这时在一个使用今日头条屏幕适配方案的项目上,设置的设计图最大宽度如果是 1000dp,那这个三方库 View,与项目自身都可以完美的适配,但当我们项目自身的设计图最大宽度不是 1000dp,是 500dp 时,100 / 500 = 0.2,可以看到,比例发生了较大的变化,从 10% 上升为 20%,明显这个三方库 View 高于作者的预期,比之前更大了
这就是两个设计图尺寸不一致导致的非常严重的问题,当两个设计图尺寸差距越大,那适配的效果也就天差万别了
调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配
这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变
但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 Activity 为单位修改设计图尺寸,相当于每个 Activity 都可以自定义设计图尺寸,因为有些 Activity 不会使用三方库 View,也就不需要自定义尺寸,所以每个 Activity 都有控制权的话,这也是最灵活的
但这也有个问题,当一个 Activity 使用了多个设计图尺寸不一样的三方库 View,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题
第二个方案是最简单的,也是按 Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案
有些文章中提到了今日头条屏幕适配方案可以将设计图尺寸填写成以 px 为单位的宽度和高度,这样我们在布局文件中,也就能直接填写设计图上标注的 px 值,省掉了将 px 换算为 dp 的时间 (大部分公司的设计图都只标注 px 值),而且照样能完美适配
但是我建议大家千万不要这样做,还是老老实实的以 dp 为单位填写 dp 值,为什么呢?
直接填写 px 虽然刚开始布局的时候很爽,但是这个坑就已经埋上了,会让你后面很爽,有哪些坑?
这样无疑于使项目强耦合于这个方案,当你遇到无法解决的问题想切换为其他屏幕适配方案的时候,layout 文件里曾经填写的 px 值都会作为 dp
比如你的设计图实际宽度为 1080px,你不换算为 360dp (1080 / 3 = 360),却直接将 1080px 作为这个方案的设计图尺寸,那你在 layout 文件中,填写的也都是设计图上标注的 px 值,但是单位却是 dp
一个在设计图上 300px * 300px 的 View,你可以直接在 layout 文件中填写为 300dp,而由于这个方案可以动态改变 density 的原因还是可以做到等比例适配,非常爽!
但你不要忘了,这样你就强耦合于这个方案了,因为当你不使用这个方案时,density 是不可变的!
使用这个方案时,在屏幕宽度为 1080px 的设备上,将设计图宽度直接填写为 1080,根据今日头条公式
当前设备屏幕总宽度 / 设计图总宽度 = density
这时得出 density 为 1 (1080 / 1080 = 1),所以你在 layout 文件中你填写的 300dp 最后转换为 px 也是 300px (300dp * 1 = 300px 根据公式 dp * density = px)
在这个方案的帮助下非常完美,和设计图一模一样完成了适配
但当你不使用这个方案时,density 的换算公式就变为官方的 DPI / 160 = density,
在这个屏幕宽度为 1080px,480dpi 的设备上,density 就固定为 3 (480 / 160 = 3)
这时再来看看你之前在 layout 文件中填写的 dp,换算成 px 为 900 px (300dp * 3 = 900px 根据公式 dp * density = px)
原本在在设计图上为 300px 的 View,这时却达到了惊人的 900px,3倍的差距,恭喜你,你已经强耦合于这个方案了,你要不所有 layout 文件都改一遍,要不继续使用这个方案
第二个坑其实就是刚刚在上面说的今日头条适配方案的缺点,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重
你如果直接填写以 px 为设计图的尺寸,这不用想,肯定和所有的三方库以及系统控件的设计图尺寸都不一样,而且差距都非常之大,至少两三倍的差距,这时你在当前页面弹个 Toast 就可以明显看到,比之前小很多,可以说是天差万别,用其他三方库 View,也是一样的,会小很多
因为你以 px 为单位填写设计图尺寸,人家却用的 dp,差距能不大吗,你如果老老实实用 dp,哪怕三方库的设计图尺寸和你项目自身的设计图尺寸不一样,那也差距不大,小到一定程度,基本都不用调整,可以忽略不计,而且很多三方库的设计图尺寸其实也都是那几个大众尺寸,很大可能和你项目自身的设计图尺寸一样
可以看到我讲的非常详细,可以说比今日头条官方以及任何博客写的都清楚,从原理到优缺点再到解决方案一应俱全,因为篇幅有限,如果我还想把 smallestWidth 限定符适配方案写的这么详细,那估计这篇文章得有一万字了
所以我把这次的屏幕适配文章归位一个系列,一共分为三篇,第一篇详细的讲 今日头条屏幕适配方案,第二篇详细的讲 smallestWidth 限定符适配方案,第三篇详细讲两个方案的深入对比以及如何选择,并发布我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize
今日头条屏幕适配方案 官方公布的核心源码只有 30 行不到,但我这个框架的源码有 1500 行以上,在保留原有特性的情况下增加了不少功能和特性,功能增加了不少,但是使用上却变简单了
只要这一步填写了设计图的高宽以 dp 为单位,你什么都不做,框架就开始适配了
大家可以提前看看我是怎么封装和优化的,我后面的第三篇文章会给出这个框架的原理分析,敬请期待
关于大家的评论以及关注的问题,我在这里统一回复一下:
感谢,大家的关注和回复,我介绍这个今日头条的屏幕适配方案并不是说他有多么完美,只是他确实有效而且能帮我们减少很多开发成本
对于很多人说的 DPI 的存在,不就是为了让大屏能显示更多的内容,如果一个大屏手机和小屏手机,显示的内容都相同,那用户买大屏手机又有什么意义呢,我觉得大家对 DPI 的理解是对的,这个观点我也是认同的,Google 设计 DPI 时可能也是这么想的,但是有一点大家没考虑到,Android 的碎片化太严重了
为什么 Android 诞生这么多年,Android 的百分比库层出不穷,按理说他们都违背了上面说的这个理念,但为什么还有这么多人去研究百分比库通过各种方式去实现百分比布局 (谷歌官方也曾出过百分比库)?为什么?很简单,因为需求啊!为什么需求,因为开发成本低啊!为什么今日头条的这个屏幕适配方案现在能这么火,因为他的开发成本是目前所有屏幕适配方案中最低的啊!
DPI 的意义谁又不懂呢?难道就你懂,今日头条这种大公司的程序员不懂这个道理吗?今日头条这么大的公司难道不想把每个机型每个版本的设备都适配完美,让自己的 App 体验更好,哪怕付出更大的成本,有些时候想象是美好的,但是这个投入的成本,谁又能承担呢,连今日头条这么大的公司,这么雄厚的资本都没选择投入更大的成本,对每个机型进行更精细化的适配,难道市面上的中小型公司又有这个能力投入这么大的成本吗?
鱼和熊掌不可兼得,DPI 的意义在 Google 的设计理念中是完全正确的,但不是所有公司都能承受这个成本,想必今日头条的程序员,也是因为今日头条 App 的用户量足够多,机型分布足够广,也是被屏幕适配这个问题折磨的不要不要的,才想出这么个不这么完美但是却很有效的方案
Android屏幕适配方案,直接填写设计图上的像素尺寸即可完成适配。
将autolayout引入
dependencies {
compile project(':autolayout')
}
也可以直接
dependencies {
compile 'com.zhy:autolayout:1.4.5'
}
建议使用As,方便版本更新。实在不行,只有复制粘贴源码了。
在你的项目的AndroidManifest中注明你的设计稿
的尺寸。
<meta-data android:name="design_width" android:value="768">
meta-data>
<meta-data android:name="design_height" android:value="1280">
meta-data>
让你的Activity继承自AutoLayoutActivity
.
非常简单的两个步骤,你就可以开始愉快的编写布局了,详细可以参考sample。
如果你不希望继承AutoLayoutActivity
,可以在编写布局文件时,将
这样也可以完成适配。
默认使用的高度是设备的可用高度,也就是不包括状态栏和底部的操作栏的,如果你希望拿设备的物理高度进行百分比化:
可以在Application的onCreate方法中进行设置:
public class UseDeviceSizeApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
AutoLayoutConifg.getInstance().useDeviceSize();
}
}
对于其他继承系统的FrameLayout、LinearLayout、RelativeLayout的控件,比如CardView
,如果希望再其内部直接支持"px"百分比化,可以自己扩展,扩展方式为下面的代码,也可参考issue#21:
package com.zhy.sample.view;
import android.content.Context;
import android.support.v7.widget.CardView;
import android.util.AttributeSet;
import com.zhy.autolayout.AutoFrameLayout;
import com.zhy.autolayout.utils.AutoLayoutHelper;
/**
* Created by zhy on 15/12/8.
*/
public class AutoCardView extends CardView
{
private final AutoLayoutHelper mHelper = new AutoLayoutHelper(this);
public AutoCardView(Context context)
{
super(context);
}
public AutoCardView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public AutoCardView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
@Override
public AutoFrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new AutoFrameLayout.LayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (!isInEditMode())
{
mHelper.adjustChildren();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
sample中包含ListView、RecyclerView例子,具体查看sample
对于ListView这类控件的item,默认根局部写“px”进行适配是无效的,因为外层非AutoXXXLayout,而是ListView。但是,不用怕,一行代码就可以支持了:
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder holder = null;
if (convertView == null)
{
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
convertView.setTag(holder);
//对于listview,注意添加这一行,即可在item上使用高度
AutoUtils.autoSize(convertView);
} else
{
holder = (ViewHolder) convertView.getTag();
}
return convertView;
}
注意 AutoUtils.autoSize(convertView);
这行代码的位置即可。demo中也有相关实例。
public ViewHolder(View itemView)
{
super(itemView);
AutoUtils.autoSize(itemView);
}
//...
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View convertView = LayoutInflater.from(mContext).inflate(R.layout.recyclerview_item, parent, false);
return new ViewHolder(convertView);
}
一定要记得LayoutInflater.from(mContext).inflate
使用三个参数的方法!
由于该库的特点,布局文件中宽高上的1px是不相等的,于是如果需要宽高保持一致的情况,布局中使用属性:
app:layout_auto_basewidth="height"
,代表height上编写的像素值参考宽度。
app:layout_auto_baseheight="width"
,代表width上编写的像素值参考高度。
如果需要指定多个值参考宽度即:
app:layout_auto_basewidth="height|padding"
用|隔开,类似gravity的用法,取值为:
设计稿一般只会标识一个字体的大小,比如你设置textSize="20px",实际上TextView所占据的高度肯定大于20px,字的上下都会有一定的间隙,所以一定要灵活去写字体的高度,比如对于text上下的margin可以选择尽可能小一点。或者选择别的约束条件去定位(比如上例,选择了marginBottom)
###(1)导入后出现org/gradle/api/publication/maven/internal/DefaultMavenFactory
最简单的方式,通过compile 'com.zhy:autolayout:x.x.x'
进行依赖使用,
###(2)RadioGroup,Toolbar等控件中的子View无法完成适配
这个其实上文已经提到过了,需要自己扩展。不过这个很多使用者贡献了他们的扩展类可以直接使用, 参考autolayout-widget, 如果没有发现你需要的容器类,那么你就真的需要自行扩展了,当然如果你完成了扩展,可以给我发个PR,或者让我知道,我可以加入到 autolayout-widget
中方便他人,ps:需要用到哪个copy就好了,不要直接引用autolayout-widget
,因为其引用了大量的库,可能很多 库你是用不到的。
###(3)java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
这个问题是因为默认AutoLayoutActivity会继承自AppCompatActivity,所以默认需要设置 Theme.AppCompat的theme;
如果你使用的依旧是FragmentActivity等,且不考虑使用AppCompatActivity, 你可以选择自己编写一个MyAutoLayoutActivity extends 目前你使用的Activity基类
,例如 MyAutoLayoutActivity extends FragmentActivity
,然后将该库中AutoLayoutActivity中的逻辑 拷贝进去即可,以后你就继承你的MyAutoLayoutActivity
就好了。
ps:还是建议尽快更新SDK版本使用AppCompatActivity
.
代码下载:AndroidAutoLayout-master.zip
原文链接:https://github.com/hongyangAndroid/AndroidAutoLayout
mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
t1.setText("Header 1");
TextView t2 = new TextView(this);
mHeaderAndFooterWrapper.addHeaderView(t2);
在不改变原有的Adapter基础上去增强其功能。
public class HeaderAndFooterWrapper extends RecyclerView.Adapter
{
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;
private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat mFootViews = new SparseArrayCompat<>();
private RecyclerView.Adapter mInnerAdapter;
public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
{
mInnerAdapter = adapter;
}
private boolean isHeaderViewPos(int position)
{
return position < getHeadersCount();
}
private boolean isFooterViewPos(int position)
{
return position >= getHeadersCount() + getRealItemCount();
}
public void addHeaderView(View view)
{
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
public void addFootView(View view)
{
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}
public int getHeadersCount()
{
return mHeaderViews.size();
}
public int getFootersCount()
{
return mFootViews.size();
}
}
并且可以看到我们对每个HeaderView,都有一个特定的key与其对应,第一个headerView对应的是BASE_ITEM_TYPE_HEADER,第二个对应的是BASE_ITEM_TYPE_HEADER+1;
为什么要这么做呢?
这两个问题都需要到复写onCreateViewHolder的时候来说明。
public class HeaderAndFooterWrapper extends RecyclerView.Adapter
{
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
if (mHeaderViews.get(viewType) != null)
{
ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType));
return holder;
} else if (mFootViews.get(viewType) != null)
{
ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType));
return holder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
@Override
public int getItemViewType(int position)
{
if (isHeaderViewPos(position))
{
return mHeaderViews.keyAt(position);
} else if (isFooterViewPos(position))
{
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
}
return mInnerAdapter.getItemViewType(position - getHeadersCount());
}
private int getRealItemCount()
{
return mInnerAdapter.getItemCount();
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (isHeaderViewPos(position))
{
return;
}
if (isFooterViewPos(position))
{
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}
@Override
public int getItemCount()
{
return getHeadersCount() + getFootersCount() + getRealItemCount();
}
}
getItemViewType
由于我们增加了headerView和footerView首先需要复写的就是getItemCount和getItemViewType。
getItemCount很好理解;
对于getItemType,可以看到我们的返回值是mHeaderViews.keyAt(position),这个值其实就是我们addHeaderView时的key,footerView是一样的处理方式,这里可以看出我们为每一个headerView创建了一个itemType。
onCreateViewHolder
可以看到,我们分别判断viewType,如果是headview或者是footerview,我们则为其单独创建ViewHolder,这里的ViewHolder是我之前写的一个通用的库里面的类,文末有链接。当然,你也可以自己写一个ViewHolder的实现类,只需要将对应的headerView作为itemView传入ViewHolder的构造即可。
这个方法中,我们就可以解答之前的问题了:
为什么我要用SparseArrayCompat而不是List?
为什么我要让每个headerView对应一个itemType,而不是固定的一个?
对于headerView假设我们有多个,那么onCreateViewHolder返回的ViewHolder中的itemView应该对应不同的headerView,如果是List,那么不同的headerView应该对应着:list.get(0),list.get(1)等。
但是问题来了,该方法并没有position参数,只有itemType参数,如果itemType还是固定的一个值,那么你是没有办法根据参数得到不同的headerView的。
所以,我利用SparseArrayCompat,将其key作为itemType,value为我们的headerView,在onCreateViewHolder中,直接通过itemType,即可获得我们的headerView,然后构造ViewHolder对象。而且我们的取值是从100000开始的,正常的itemType是从0开始取值的,所以正常情况下,是不可能发生冲突的。
需要说明的是,这里的意思并非是一定不能用List,通过一些特殊的处理,List也能达到上述我描述的效果。
onBindViewHolder
onBindViewHolder比较简单,发现是HeaderView或者FooterView直接return即可,因为对于头部和底部我们仅仅做展示即可,对于事件应该是在addHeaderView等方法前设置。
这样就初步完成了我们的装饰类,我们分别添加两个headerView和footerView:
大家都知道RecyclerView比较强大,可以设置不同的LayoutManager,那么我们换成GridLayoutMananger再看看效果。
好像发现了不对的地方,我们的headerView果真被当成普通的Item处理了,不过由于我们的编写方式,出现上述情况是可以理解的。
那么我们该如何处理呢?让每个headerView独立的占据一行?
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView)
{
innerAdapter.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager)
{
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
{
@Override
public int getSpanSize(int position)
{
int viewType = getItemViewType(position);
if (mHeaderViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
} else if (mFootViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
}
if (oldLookup != null)
return oldLookup.getSpanSize(position);
return 1;
}
});
gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
}
}
当发现layoutManager为GridLayoutManager时,通过设置SpanSizeLookup,对其getSpanSize方法,返回值设置为layoutManager.getSpanCount();
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
{
mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderViewPos(position) || isFooterViewPos(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams)
{
StaggeredGridLayoutManager.LayoutParams p =
(StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
这样就完成了对StaggeredGridLayoutManager的处理,效果图就不贴了。
到此,我们就完成了整个HeaderAndFooterWrapper的编写,可以在不改变原Adapter代码的情况下,为其添加一个或者多个headerView或者footerView,以及完成了如何让HeaderView或者FooterView适配各种LayoutManager。
源码地址:https://github.com/hongyangAndroid/baseAdapter
————————————————
版权声明:本文为CSDN博主「鸿洋_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin 与 Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空处理。
先扯一扯 Kotlin 学习本身。
之前各种听人说上手容易,但真要切换到另一门语言,难免还是会踌躇是否有这个必要。现在因为工作关系直接上手 Kotlin,感受是 真香(上手的确容易)。
首先在代码阅读层面,对于有 Java 基础的程序员来说阅读 Kotlin 代码基本无障碍,除去一些操作符、一些顺序上的变化,整体上可以直接阅读。
其次在代码编写层面,仅需要改变一些编码习惯。主要是:语句不要写分号、变量需要用 var 或 val 声明、类型写在变量之后、实例化一个对象时不用 "new" …… 习惯层面的改变只需要多写代码,自然而然就适应了。
最后在学习方式层面,由于 Kotlin 最终都会被编译成字节码跑在 JVM
上,所以初入手时完全可以用 Java 作为对比。比如你可能不知道
Kotlin 里 companion object
是什么意思,但你知道既然
Kotlin 最终会转成 jvm 可以跑的字节码,那 Java 里必然可以找到与之对应的东西。
Android Studio 也提供了很方便的工具。选择菜单 Tools -> Kotlin -> Show
Kotlin Bytecode 即可看到 Kotlin 编译成的字节码,点击窗口上方的 "Decompile" 即可看到这份字节码对应的 Java 代码。 —— 这个工具特别重要,假如一段
Kotlin 代码让你看得云里雾里,看一下它对应的 Java 代码你就能知道它的含义。
当然这里仅仅是说上手或入门(仅入门的话可以忽略诸如协程等高级特性),真正熟练应用乃至完全掌握肯定需要一定时间。
有些文章说 Kotlin 帮开发者解决了 NPE(NullPointerException),这个说法是不对的。在我看来,Kotlin 没有帮开发者解决了 NPE (Kotlin: 臣妾真的做不到啊),而是通过在语言层面增加各种强规则,强制开发者去自己处理可能的空指针问题,达到尽量减少(只能减少而无法完全避免)出现 NPE 的目的。
那么 Kotlin 具体是怎么做的呢?别着急,我们可以先回顾一下在 Java 中我们是怎么处理空指针问题的。
Java 中对于空指针的处理总体来说可以分为“防御式编程”和“契约式编程”两种方案。
“防御式编程”大家应该不陌生,核心思想是不信任任何“外部”输入 —— 不管是真实的用户输入还是其他模块传入的实参,具体点就是各种判空。创建一个方法需要判空,创建一个逻辑块需要判空,甚至自己的代码内部也需要判空(防止对象的回收之类的)。示例如下:
public void showToast(Activity activity) {
if (activity == null) {
return;
}
......
}
另一种是“契约式编程”,各个模块之间约定好一种规则,大家按照规则来办事,出了问题找没有遵守规则的人负责,这样可以避免大量的判空逻辑。Android 提供了相关的注解以及最基础的检查来协助开发者,示例如下:
public void showToast(@NonNull Activity activity) {
......
}
在示例中我们给 Activity 增加了 @NonNull
的注解,就是向所有调用这个方法的人声明了一个约定,调用方应该保证传入的 activity 非空。当然聪明的你应该知道,这是一个很弱的限制,调用方没注意或者不理会这个注解的话,程序就依然还有 NPE 导致的 crash 的风险。
回过头来,对于 Kotlin,我觉得就是一种把契约式编程和防御式编程相结合且提升到语言层面的处理方式。(听起来似乎比 Java 中各种判空或注解更麻烦?继续看下去,你会发现的确是更麻烦……)
在 Kotlin 中,有以下几方面约束:
1.
在声明阶段,变量需要决定自己是否可为空,比如 var time: Long?
可接受 null,而 var time: Long
则不能接受 null。
2.
在变量传递阶段,必须保持“可空性”一致,比如形参声明是不为空的,那么实参必须本身是非空或者转为非空才能正常传递。示例如下:
fun main() {
......
// test(isOpen) 直接这样调用,编译不通过
// 可以是在空检查之内传递,证明自己非空
isOpen?.apply {
test(this)
}
// 也可以是强制转成非空类型
test(isOpen!!)
}
private fun test(open: Boolean) {
......
}
3.
在使用阶段,需要严格判空:
var time: Long? = 1000
//尽管你才赋值了非空的值,但在使用过程中,你无法这样:
//time.toInt()
//必须判空
time?.toInt()
总的来说 Kotlin 为了解决 NPE 做了大量语言层级的强限制,的确可以做到减少 NPE 的发生。但这种既“契约式”(判空)又“防御式”(声明空与非空)的方案会让开发者做更多的工作,会更“麻烦”一点。
当然,Kotlin 为了减少麻烦,用 "?" 简化了判空逻辑 —— "?" 的实质还是判空,我们可以通过工具查看 time?.toInt()
的 Java 等价代码是:
if (time != null) {
int var10000 = (int)time;
}
这种简化在数据层级很深需要写大量判空语句时会特别方便,这也是为什么虽然逻辑上 Kotlin 让开发者做了更多工作,但写代码过程中却并没有感觉到更麻烦。
在 Kotlin 这么严密的防御之下,NPE 问题是否已经被终结了呢?答案当然是否定的。 在实践过程中我们发现主要有以下几种容易导致 NPE 的场景:
例如从后端拿 json 数据的场景,后端的哪个字段可能会传空是客户端无法控制的,这种情况下我们的预期必须是每个字段都可能为空,这样转成 json object 时才不会有问题:
data class User(
var id: Long?,
var gender: Long?,
var avatar: String?)
假如有一个字段忘了加上"?",后端没传该值就会抛出空指针异常。
private lateinit var mUser: User
...
private fun initView() {
mUser = intent.getParcelableExtra<User>("key_user")
}
在 Kotlin 的体系中久了会过分依赖于
Android Studio 的空值检查,在代码提示中
Intent 的 getParcelableExtra
方法返回的是非空,因此这里你直接用方法结果赋值不会有任何警告。但点击进 getParcelableExtra
方法内部你会发现它的实现是这样的:
public <T extends Parcelable> T getParcelableExtra(String name) {
return mExtras == null ? null : mExtras.<T>getParcelable(name);
}
内部的其他代码不展开了,总之它是可能会返回 null 的,直接赋值显然会有问题。
我理解这是 Kotlin 编译工具对 Java 代码检查的不足之处,它无法准确判断 Java 方法是否会返回空就选择无条件信任,即便方法本身可能还声明了 @Nullable
。
这点与第一、第二点都很类似,主要是使用过程中一定要进一步思考传递过来的值是否真的非空。
有人可能会说,那我全部都声明为可空类型不就得了么 —— 这样做会让你在使用该变量的所有地方都需要判空,Kotlin 本身的便利性就荡然无存了。
我的观点是不要因噎废食,使用时多注意点就可以避免大部分问题。
当将可空类型赋值给非空类型时,需要有对空类型的判断,确保非空才能赋值(Kotlin 的约束)。
我们使用!!
可以很方便得将“可空”转为“非空”,但可空变量值为 null,则会 crash。
因此使用上建议在确保非空时才用 !!
:
param!!
否则还是尽量放在判空代码块里:
param?.let {
doSomething(it)
}
从 Java 的空处理转到 Kotlin 的空处理,我们可能会下意识去寻找对标 Java 的判空写法:
if (n != null) {
//非空如何
} else {
//为空又如何
}
在 Kotlin 中类似的写法的确有,那就是结合高阶函数 let
、
apply
、
run ……
来处理判空,比如上述 Java 代码就可以写成:
n?.let {
//非空如何
} ?: let {
//为空又如何
}
但这里有几个小坑。
假如是 Java 的写法,那么不管 n 的值怎样,两个代码块都是互斥的,也就是“非黑即白”。但 Kotlin 的这种写法不是(不确定这种写法是否是最佳实践,假如有更好的方案可以留言指出)。
?:
这个操作符可以理解为 if (a != null) a else b
,也就是它之前的值非空返回之前的值,否则返回之后的值。
假如用的是 let, 注意看它的返回值是“指定 return 或函数里最后一行”,那么碰到以下情况:
val n = 1
var a = 0
n?.let {
a++
...
null //最后一行为 null
} ?: let {
a++
}
你会很神奇地发现 a 的值是 2,也就是既执行了前一个代码块,也执行了后一个代码块。
上面这种写法你可能不以为然,因为很明显地提醒了诸位需要注意最后一行,但假如是之前没注意这个细节或者是下面这种写法呢?
n?.let {
...
anMap.put(key, value) // anMap 是一个 HashMap
} ?: let {
...
}
应该很少人会注意到 Map 的 put 方法是有返回值的,且可能会返回 null。那么这种情况下很容易踩坑。
以 let 为例,在 let 代码块里可以用 it 指代该对象(其他高阶函数可能用
this,类似的),那么我们在写如下代码时可能会顺手这样写:
activity {
n?.let {
it.hashCode() // it 为 n
} ?: let {
it.hashCode() // it 为 activity
}
}
结果自然会发现值不一样。前一个代码块 it 指代的是 n,而后一个代码块里 it 指代的是整个代码块指向的
this。
原因是 ?:
与 let 之间是没有 .
的,也就是说后一个代码块调用 let 的对象并不是被判空的对象,而是 this
。(不过这种场景会出错的概率不大,因为在后一个代码块里很多对象 n 的方法用不了,就会注意到问题了)
总的来说切换到 Kotlin 还是比预期顺利和舒服,写惯了
Kotlin 后再回去写 Java 反倒有点不习惯。今天先写这点,后面有其他需要总结的再分享。
Activity启动流程很多文章都已经说过了,这里说一下自己的理解。
Activity启动流程分两种,一种是启动正在运行的app的Activity,即启动子Activity。如无特殊声明默认和启动该activity的activity处于同一进程。如果有声明在一个新的进程中,则处于两个进程。另一种是打开新的app,即为Launcher启动新的Activity。后边启动Activity的流程是一样的,区别是前边判断进程是否存在的那部分。
Activity启动的前提是已经开机,各项进程和AMS等服务已经初始化完成,在这里也提一下那些内容。
Activity启动之前
init进程:init是所有linux程序的起点,是Zygote的父进程。解析init.rc孵化出Zygote进程。
Zygote进程:Zygote是所有Java进程的父进程,所有的App进程都是由Zygote进程fork生成的。
SystemServer进程:System
Server是Zygote孵化的第一个进程。SystemServer负责启动和管理整个Java framework,包含AMS,PMS等服务。
Launcher:Zygote进程孵化的第一个App进程是Launcher。
1.init进程是什么?
Android是基于linux系统的,手机开机之后,linux内核进行加载。加载完成之后会启动init进程。
init进程会启动ServiceManager,孵化一些守护进程,并解析init.rc孵化Zygote进程。
2.Zygote进程是什么?
所有的App进程都是由Zygote进程fork生成的,包括SystemServer进程。Zygote初始化后,会注册一个等待接受消息的socket,OS层会采用socket进行IPC通信。
3.为什么是Zygote来孵化进程,而不是新建进程呢?
每个应用程序都是运行在各自的Dalvik虚拟机中,应用程序每次运行都要重新初始化和启动虚拟机,这个过程会耗费很长时间。Zygote会把已经运行的虚拟机的代码和内存信息共享,起到一个预加载资源和类的作用,从而缩短启动时间。
Activity启动阶段
涉及到的概念
进程:Android系统为每个APP分配至少一个进程
IPC:跨进程通信,Android中采用Binder机制。
涉及到的类
ActivityStack:Activity在AMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。
ActivitySupervisor:管理 activity 任务栈
ActivityThread:ActivityThread
运行在UI线程(主线程),App的真正入口。
ApplicationThread:用来实现AMS和ActivityThread之间的交互。
ApplicationThreadProxy:ApplicationThread
在服务端的代理。AMS就是通过该代理与ActivityThread进行通信的。
IActivityManager:继承与IInterface接口,抽象出跨进程通信需要实现的功能
AMN:运行在server端(SystemServer进程)。实现了Binder类,具体功能由子类AMS实现。
AMS:AMN的子类,负责管理四大组件和进程,包括生命周期和状态切换。AMS因为要和ui交互,所以极其复杂,涉及window。
AMP:AMS的client端代理(app进程)。了解Binder知识可以比较容易理解server端的stub和client端的proxy。AMP和AMS通过Binder通信。
Instrumentation:仪表盘,负责调用Activity和Application生命周期。测试用到这个类比较多。
流程图
这个图来源自网上,之前也看过很多类似讲流程的文章,但是大都是片段的。这个图是目前看到的最全的,自己去画一下也应该不会比这个全了,所以在这里直接引用一下,可以去浏览器上放大看。
涉及到的进程
·
Launcher所在的进程
·
AMS所在的SystemServer进程
·
要启动的Activity所在的app进程
如果是启动根Activity,就涉及上述三个进程。
如果是启动子Activity,那么就只涉及AMS进程和app所在进程。
具体流程
1. Launcher:Launcher通知AMS要启动activity。
·
startActivitySafely->startActivity->Instrumentation.execStartActivity()(AMP.startActivity)->AMS.startActivity
2. AMS:PMS的resoveIntent验证要启动activity是否匹配。如果匹配,通过ApplicationThread发消息给Launcher所在的主线程,暂停当前Activity(即Launcher)。
3. 暂停完,在该activity还不可见时,通知AMS,根据要启动的Activity配置ActivityStack。然后判断要启动的Activity进程是否存在?
·
存在:发送消息LAUNCH_ACTIVITY给需要启动的Activity主线程,执行handleLaunchActivity
·
不存在:通过socket向zygote请求创建进程。进程启动后,ActivityThread.attach
4. 判断Application是否存在,若不存在,通过LoadApk.makeApplication创建一个。在主线程中通过thread.attach方法来关联ApplicationThread。
5. 在通过ActivityStackSupervisor来获取当前需要显示的ActivityStack。
6. 继续通过ApplicationThread来发送消息给主线程的Handler来启动Activity(handleLaunchActivity)。
7. handleLauchActivity:调用了performLauchActivity,里边Instrumentation生成了新的activity对象,继续调用activity生命周期。
IPC过程:
双方都是通过对方的代理对象来进行通信。
1.app和AMS通信:app通过本进程的AMP和AMS进行Binder通信
2.AMS和新app通信:通过ApplicationThreadProxy来通信,并不直接和ActivityThread通信
参考函数流程
Activity启动流程(从Launcher开始):
第一阶段: Launcher通知AMS要启动新的Activity(在Launcher所在的进程执行)
·
Launcher.startActivitySafely //首先Launcher发起启动Activity的请求
·
Activity.startActivity
·
Activity.startActivityForResult
·
Instrumentation.execStartActivity //交由Instrumentation代为发起请求
·
ActivityManager.getService().startActivity
//通过IActivityManagerSingleton.get()得到一个AMP代理对象
·
ActivityManagerProxy.startActivity
//通过AMP代理通知AMS启动activity
第二阶段:AMS先校验一下Activity的正确性,如果正确的话,会暂存一下Activity的信息。然后,AMS会通知Launcher程序pause Activity(在AMS所在进程执行)
·
ActivityManagerService.startActivity
·
ActivityManagerService.startActivityAsUser
·
ActivityStackSupervisor.startActivityMayWait
·
ActivityStackSupervisor.startActivityLocked
:检查有没有在AndroidManifest中注册
·
ActivityStackSupervisor.startActivityUncheckedLocked
·
ActivityStack.startActivityLocked :判断是否需要创建一个新的任务来启动Activity。
·
ActivityStack.resumeTopActivityLocked :获取栈顶的activity,并通知Launcher应该pause掉这个Activity以便启动新的activity。
·
ActivityStack.startPausingLocked
·
ApplicationThreadProxy.schedulePauseActivity
第三阶段: pause Launcher的Activity,并通知AMS已经paused(在Launcher所在进程执行)
·
ApplicationThread.schedulePauseActivity
·
ActivityThread.queueOrSendMessage
·
H.handleMessage
·
ActivityThread.handlePauseActivity
·
ActivityManagerProxy.activityPaused
第四阶段:检查activity所在进程是否存在,如果存在,就直接通知这个进程,在该进程中启动Activity;不存在的话,会调用Process.start创建一个新进程(执行在AMS进程)
·
ActivityManagerService.activityPaused
·
ActivityStack.activityPaused
·
ActivityStack.completePauseLocked
·
ActivityStack.resumeTopActivityLocked
·
ActivityStack.startSpecificActivityLocked
·
ActivityManagerService.startProcessLocked
·
Process.start //在这里创建了新进程,新的进程会导入ActivityThread类,并执行它的main函数
第五阶段: 创建ActivityThread实例,执行一些初始化操作,并绑定Application。如果Application不存在,会调用LoadedApk.makeApplication创建一个新的Application对象。之后进入Loop循环。(执行在新创建的app进程)
·
ActivityThread.main
·
ActivityThread.attach(false) //声明不是系统进程
·
ActivityManagerProxy.attachApplication
第六阶段:处理新的应用进程发出的创建进程完成的通信请求,并通知新应用程序进程启动目标Activity组件(执行在AMS进程)
·
ActivityManagerService.attachApplication //AMS绑定本地ApplicationThread对象,后续通过ApplicationThreadProxy来通信。
·
ActivityManagerService.attachApplicationLocked
·
ActivityStack.realStartActivityLocked //真正要启动Activity了!
·
ApplicationThreadProxy.scheduleLaunchActivity
//AMS通过ATP通知app进程启动Activity
第七阶段: 加载MainActivity类,调用onCreate声明周期方法(执行在新启动的app进程)
·
ApplicationThread.scheduleLaunchActivity
//ApplicationThread发消息给AT
·
ActivityThread.queueOrSendMessage
·
H.handleMessage //AT的Handler来处理接收到的LAUNCH_ACTIVITY的消息
·
ActivityThread.handleLaunchActivity
·
ActivityThread.performLaunchActivity
·
Instrumentation.newActivity //调用Instrumentation类来新建一个Activity对象
·
Instrumentation.callActivityOnCreate
·
MainActivity.onCreate
·
ActivityThread.handleResumeActivity
·
AMP.activityResumed
·
AMS.activityResumed(AMS进程)
参考文章
http://gityuan.com/2016/03/12/start-activity/
https://blog.csdn.net/luoshengyang/article/details/6689748
示例图:
一个Android TabLayout库,目前有3个TabLayout
SlidingTabLayout:参照PagerSlidingTabStrip进行大量修改.
/** 关联ViewPager,用于不想在ViewPager适配器中设置titles数据的情况 */
public void setViewPager(ViewPager vp, String[] titles)
/** 关联ViewPager,用于连适配器都不想自己实例化的情况 */
public void setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList<Fragment> fragments)
CommonTabLayout:不同于SlidingTabLayout对ViewPager依赖,它是一个不依赖ViewPager可以与其他控件自由搭配使用的TabLayout.
/** 关联数据支持同时切换fragments */
public void setTabData(ArrayList<CustomTabEntity> tabEntitys, FragmentManager fm, int containerViewId, ArrayList<Fragment> fragments)
SegmentTabLayout
dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.flyco.roundview:FlycoRoundView_Lib:1.1.2@aar'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:1.5.0@aar'
}
After v2.0.0(support 2.2+)
dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.0.0@aar'
}
After v2.0.2(support 3.0+)
dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.1.2@aar'
}
代码下载:FlycoTabLayout-master.zip
原文链接:https://github.com/H07000223/FlycoTabLayout
收起阅读 »OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)
NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];
NSLog(@"instance----%p %p",objc1,objc2);
输出结果:instance实例对象存储的信息:
1.isa指针
Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);
打印结果
2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
类对象存储的信息:
1.isa指针
2.superclass指针
3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)
1.元类对象的获取
Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);
打印指针地址
NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0
获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个
Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];
无论class几次,它返回的始终是类对象
元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)
从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空
是否为元类 class_isMetaClass(objcL);
编译
和链接
过程。我们平常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
复制代码
其实上述过程可以分解为四步:
首先是源代码文件hello.c
和相关的头文件(如stdio.h
等)被预编译器cpp
预编译成一个.i
文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):
$ gcc –E hello.c –o hello.i
复制代码
还可以下面的表达
$ cpp hello.c > hello.i
复制代码
预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define
等,主要处理规则如下:
#define
删除,并展开所有的宏定义#if,#ifdef,#elif,#else,#endif
#include
预编译指令,将被包含的文件插入到该预编译指令的位置。//
和/**/
#2 “hello.c” 2。
#pragma
编译器指令截图个大家看看效果
经过预编译后的文件(.i文件)
不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件
中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
编译过程就是把预处理完的文件进行一系列的:词法分析
、语法分析
、语义分析
及优化后生产相应的汇编代码文件
,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:
$ gcc –S hello.i –o hello.s
hello.s
.汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。
所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:
as hello.s –o hello.o
复制代码
或者
gcc –c hello.s –o hello.o
复制代码
或者使用gcc
命令从C源代码文件
开始,经过预编译、编译和汇编直接输出目标文件:
gcc –c hello.c –o hello.o
复制代码
链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld
才可以产生一个能够正常运行的Hello World
程序:
注意默认情况没有gcc / 记得 :
$ brew install gcc
复制代码
链接相应的库
主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化
到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看
iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM
LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。
在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。
gcc
编译一模模一样样!.app
包,后面编译后的文件都会被放入包中;Cocoapods
会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases
中可以看到;Mach-O
,这过程 LLVM
的完整流程,前端、优化器、后端;storyboard
文件:storyboard
文件也是会被编译的;storyboard
文件:将编译后的 storyboard
文件链接成一个文件;Asset
文件:我们的图片如果使用 Assets.xcassets
来管理图片,那么这些图片将会被编译成机器码,除了 icon
和 launchImage
;Cocoapods
脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。.app
包Swift
标准库拷贝到包中Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App
我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:
一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。
避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。
通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect。
证书的同步更新,管理,在新电脑能够迅速具备项目打包环境。
那既然说Fastlane是一套工具的集合,那认识并了解其中的工具的作用是必不可少的环节。按照功能属性Fastlane对工具进行了如下分类(链接至官网详细介绍):
分类下对应的就是具体的每一个工具的介绍,在这里每一个工具Fastlane叫做action,下文我们也统一叫action。这里我会列举一些我认为常用的action,其他的大家可以去官网看下
gym:是fastlane提供的打包工具
snapshot: 生成多个设备的截图文件
frameit :对截图加一层物理边框
increment_build_number:自增build number 然后与之对应的get_build_number。Version number同理。
cert:创建一个新的代码签名证书
sigh:生成一个provisioning profile并保存打当前文件
pem:确保当前的推送证书是活跃的,如果没有会帮你生成一个新的
match:在团队中同步证书和描述文件。(这是一种管理证书的全新方式,需要重点关注下)
testflight:上传ipa到testflight
deliver:上传ipa到AppStore
当然官网里面其实是有很多可以划等号的Action,大家在看的时候注意下。Actions官网关于Action的介绍
当前最新版本是2.8.4
xcode-select --install
,如果没有安装,会弹出对话框,点击安装。如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates
表示已经安装
sudo gem install fastlane -NV
或是brew cask install fastlane
我这里使用gem安装的
安装完了执行fastlane --version
,确认下是否安装完成和当前使用的版本号。
cd到你的项目目录执行
fastlane init
这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。
如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane
文件夹,重新fastlane init
一下就不会报错了。
接着会提示你输入开发者账号和密码。
[20:48:55]: Please enter your Apple ID developer credentials
[20:48:55]: Apple ID Username:
登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。
如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。
初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。
其中metadata和screenshots分别对应App元数据和商店应用截图。
Appfile主要存放App的apple_id team_id app_identifier等信息
Deliverfile中为发布的配置信息,一般情况用不到。
Fastfile是我们最应该关注的文件,也是我们的工作文件。
之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。
这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。
蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。
打开终端输入fastlane add_plugin pgyer
desc "打包到pgy"
lane :test do |options|
gym(
clean:true, #打包前clean项目
export_method: "ad-hoc", #导出方式
scheme:"shangshaban", #scheme
configuration: "Debug",#环境
output_directory:"./app",#ipa的存放目录
output_name:get_build_number()#输出ipa的文件名为当前的build号
)
#蒲公英的配置 替换为自己的api_key和user_key
pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
end
这样一个打包到蒲公英的lane就完成了。
option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台
在工作目录的终端执行
fastlane test desc:测试打包
然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置。
其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解。
开头已经说了,match是一种全新的证书同步管理机制,也是我认为在fastlane中相对重要的一环,介于篇幅这篇就不细说了,有兴趣的可以去官网看下,有机会我也会更新一篇关于match的文章。match
可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来。
如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。
如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。
如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据
skip_screenshots:true,#不上传屏幕截图
其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!
效果图:
1.支持拖动,提供自动贴边等动画
2.内部自动进行权限申请操作
3.可自由指定要显示悬浮窗的界面
4.应用退到后台时,悬浮窗会自动隐藏
5.除小米外,4.4~7.0 无需权限申请
6.位置及宽高可设置百分比值,轻松适配各分辨率
7.支持权限申请结果、位置等状态监听
8.链式调用,简洁清爽
第 1 步、在工程的 build.gradle 中添加:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
第 2 步、在应用的 build.gradle 中添加:
dependencies {
compile 'com.github.yhaolpz:FloatWindow:1.0.9'
}
0.声明权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
1.基础使用
FloatWindow
.with(getApplicationContext())
.setView(view)
.setWidth(100) //设置控件宽高
.setHeight(Screen.width,0.2f)
.setX(100) //设置控件初始位置
.setY(Screen.height,0.3f)
.setDesktopShow(true) //桌面显示
.setViewStateListener(mViewStateListener) //监听悬浮控件状态改变
.setPermissionListener(mPermissionListener) //监听权限申请结果
.build();
宽高及位置可设像素值或屏幕宽/高百分比,默认宽高为 wrap_content;默认位置为屏幕左上角,x、y 为偏移量。
2.指定界面显示
.setFilter(true, A_Activity.class, C_Activity.class)
此方法表示 A_Activity、C_Activity 显示悬浮窗,其他界面隐藏。
.setFilter(false, B_Activity.class)
此方法表示 B_Activity 隐藏悬浮窗,其他界面显示。
注意:setFilter 方法参数可以识别该 Activity 的子类
也就是说,如果 A_Activity、C_Activity 继承自 BaseActivity,你可以这样设置:
.setFilter(true, BaseActivity.class)
3.可拖动悬浮窗及回弹动画
.setMoveType(MoveType.slide)
.setMoveStyle(500, new AccelerateInterpolator()) //贴边动画时长为500ms,加速插值器
共提供 4 种 MoveType :
MoveType.slide : 可拖动,释放后自动贴边 (默认)
MoveType.back : 可拖动,释放后自动回到原位置
MoveType.active : 可拖动
MoveType.inactive : 不可拖动
setMoveStyle 方法可设置动画效果,只在 MoveType.slide 或 MoveType.back 模式下设置此项才有意义。默认减速插值器,默认动画时长为 300ms。
4.后续操作
//手动控制
FloatWindow.get().show();
FloatWindow.get().hide();
//修改显示位置
FloatWindow.get().updateX(100);
FloatWindow.get().updateY(100);
//销毁
FloatWindow.destroy();
以上操作应待悬浮窗初始化后进行。
5.多个悬浮窗
FloatWindow
.with(getApplicationContext())
.setView(imageView)
.build();
FloatWindow
.with(getApplicationContext())
.setView(button)
.setTag("new")
.build();
FloatWindow.get("new").show();
FloatWindow.get("new").hide();
FloatWindow.destroy("new");
创建第一个悬浮窗不需加 tag,之后再创建就需指定唯一 tag ,以此区分,方便进行后续操作。
原文链接:https://github.com/yhaolpz/FloatWindow
收起阅读 »功能强大的 iOS 富文本编辑与显示框架
YYText 和 TextKit 架构对比
// YYLabel (和 UILabel 用法一致)
YYLabel *label = [YYLabel new];
label.frame = ...
label.font = ...
label.textColor = ...
label.textAlignment = ...
label.lineBreakMode = ...
label.numberOfLines = ...
label.text = ...
// YYTextView (和 UITextView 用法一致)
YYTextView *textView = [YYTextView new];
textView.frame = ...
textView.font = ...
textView.textColor = ...
textView.dataDetectorTypes = ...
textView.placeHolderText = ...
textView.placeHolderTextColor = ...
textView.delegate = ...
// 1. 创建一个属性文本
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text, blabla..."];
// 2. 为文本设置属性
text.yy_font = [UIFont boldSystemFontOfSize:30];
text.yy_color = [UIColor blueColor];
[text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
text.yy_lineSpacing = 10;
// 3. 赋值到 YYLabel 或 YYTextView
YYLabel *label = [YYLabel new];
label.frame = ...
label.attributedString = text;
YYTextView *textView = [YYTextView new];
textView.frame = ...
textView.attributedString = text;
你可以用一些已经封装好的简便方法来设置文本高亮:
[text yy_setTextHighlightRange:range
color:[UIColor blueColor]
backgroundColor:[UIColor grayColor]
tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
NSLog(@"tap text range:...");
}];
你可以用一些已经封装好的简便方法来设置文本高亮:
[text yy_setTextHighlightRange:range
color:[UIColor blueColor]
backgroundColor:[UIColor grayColor]
tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
NSLog(@"tap text range:...");
}];
或者用更复杂的办法来调节文本高亮的细节:
// 1. 创建一个"高亮"属性,当用户点击了高亮区域的文本时,"高亮"属性会替换掉原本的属性
YYTextBorder *border = [YYTextBorder borderWithFillColor:[UIColor grayColor] cornerRadius:3];
YYTextHighlight *highlight = [YYTextHighlight new];
[highlight setColor:[UIColor whiteColor]];
[highlight setBackgroundBorder:highlightBorder];
highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
NSLog(@"tap text range:...");
// 你也可以把事件回调放到 YYLabel 和 YYTextView 来处理。
};
// 2. 把"高亮"属性设置到某个文本范围
[attributedText yy_setTextHighlight:highlight range:highlightRange];
// 3. 把属性文本设置到 YYLabel 或 YYTextView
YYLabel *label = ...
label.attributedText = attributedText
YYTextView *textView = ...
textView.attributedText = ...
// 4. 接受事件回调
label.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
NSLog(@"tap text range:...");
};
label.highlightLongPressAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
NSLog(@"long press text range:...");
};
@UITextViewDelegate
- (void)textView:(YYTextView *)textView didTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
NSLog(@"tap text range:...");
}
- (void)textView:(YYTextView *)textView didLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
NSLog(@"long press text range:...");
}
NSMutableAttributedString *text = [NSMutableAttributedString new];
UIFont *font = [UIFont systemFontOfSize:16];
NSMutableAttributedString *attachment = nil;
// 嵌入 UIImage
UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"];
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
[text appendAttributedString: attachment];
// 嵌入 UIView
UISwitch *switcher = [UISwitch new];
[switcher sizeToFit];
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
[text appendAttributedString: attachment];
// 嵌入 CALayer
CASharpLayer *layer = [CASharpLayer layer];
layer.path = ...
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
[text appendAttributedString: attachment];
NSAttributedString *text = ...
CGSize size = CGSizeMake(100, CGFLOAT_MAX);
YYTextLayout *layout = [YYTextLayout layoutWithContainerSize:size text:text];
// 获取文本显示位置和大小
layout.textBoundingRect; // get bounding rect
layout.textBoundingSize; // get bounding size
// 查询文本排版结果
[layout lineIndexForPoint:CGPointMake(10,10)];
[layout closestLineIndexForPoint:CGPointMake(10,10)];
[layout closestPositionToPoint:CGPointMake(10,10)];
[layout textRangeAtPoint:CGPointMake(10,10)];
[layout rectForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];
[layout selectionRectsForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];
// 显示文本排版结果
YYLabel *label = [YYLabel new];
label.size = layout.textBoundingSize;
label.textLayout = layout;
部分效果展示
pod 'YYText'
。pod install
或 pod update
。github "ibireme/YYText"
。carthage update --platform ios
并将生成的 framework 添加到你的工程。YYText.h
。常见问题及源码下载:点击这里
demo:YYText.zip
高性能 iOS 缓存框架。
NSCache
保持一致, 所有方法都是线程安全的。pod 'YYCache'
。pod install
或 pod update
。github "ibireme/YYCache"
。carthage update --platform ios
并将生成的 framework 添加到你的工程。YYCache.h
。常见问题与源码下载:点击这里
代码示例:YYCache.zip
pod 'YYImage'
。pod install
或 pod update
。pod 'YYImage/WebP'
。github "ibireme/YYImage"
。carthage update --platform ios
并将生成的 framework 添加到你的工程。YYImage.h
。Vendor/WebP.framework
(静态库) 加入你的工程。// 文件: ani@3x.gif
UIImage *image = [YYImage imageNamed:@"ani.gif"];
UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
// 文件: frame1.png, frame2.png, frame3.png
NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
NSArray *times = @[@0.1, @0.2, @0.1];
UIImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
UIImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
YYAnimatedImageView *imageView = ...;
// 暂停:
[imageView stopAnimating];
// 播放:
[imageView startAnimating];
// 设置播放进度:
imageView.currentAnimatedImageIndex = 12;
// 获取播放状态:
image.currentIsPlayingAnimation;
//上面两个属性都支持 KVO。
// 解码单帧图片:
NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
// 渐进式图片解码 (可用于图片下载显示):
NSMutableData *data = [NSMutableData new];
YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
while(newDataArrived) {
[data appendData:newData];
[decoder updateData:data final:NO];
if (decoder.frameCount > 0) {
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
// progressive display...
}
}
[decoder updateData:data final:YES];
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
// final display...
// 编码静态图 (支持各种常见图片格式):
YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
jpegEncoder.quality = 0.9;
[jpegEncoder addImage:image duration:0];
NSData jpegData = [jpegEncoder encode];
// 编码动态图 (支持 GIF/APNG/WebP):
YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
webpEncoder.loopCount = 5;
[webpEncoder addImage:image0 duration:0.1];
[webpEncoder addImage:image1 duration:0.15];
[webpEncoder addImage:image2 duration:0.2];
NSData webpData = [webpEncoder encode];
// 获取图片类型
YYImageType type = YYImageDetectType(data);
if (type == YYImageTypePNG) ...
常见问题及源码:点击这里
demo下载:YYImage.zip
// JSON:
{
"uid":123456,
"name":"Harry",
"created":"1965-07-31T00:00:00+0000"
}
// Model:
@interface User : NSObject
@property UInt64 uid;
@property NSString *name;
@property NSDate *created;
@end
@implementation User
@end
// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
User *user = [User yy_modelWithJSON:json];
// 将 Model 转换为 JSON 对象:
NSDictionary *json = [user yy_modelToJSONObject];
当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。
JSON/Dictionary | Model |
---|---|
NSString | NSNumber,NSURL,SEL,Class |
NSNumber | NSString |
NSString/NSNumber | 基础类型 (BOOL,int,float,NSUInteger,UInt64,...) NaN 和 Inf 会被忽略 |
NSString | NSDate 以下列格式解析: yyyy-MM-dd yyyy-MM-dd HH:mm:ss yyyy-MM-dd'T'HH:mm:ss yyyy-MM-dd'T'HH:mm:ssZ EEE MMM dd HH:mm:ss Z yyyy |
NSDate | NSString 格式化为 ISO8601: "YYYY-MM-dd'T'HH:mm:ssZ" |
NSValue | struct (CGRect,CGSize,...) |
NSNull | nil,0 |
"no","false",... | @(NO),0 |
"yes","true",... | @(YES),1 |
// JSON:
{
"n":"Harry Pottery",
"p": 256,
"ext" : {
"desc" : "A book written by J.K.Rowing."
},
"ID" : 100010
}
// Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"page" : @"p",
@"desc" : @"ext.desc",
@"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end
你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。
在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。
// JSON
{
"author":{
"name":"J.K.Rowling",
"birthday":"1965-07-31T00:00:00+0000"
},
"name":"Harry Potter",
"pages":256
}
// Model: 什么都不用做,转换会自动完成
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Author
@end
@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author; //Book 包含 Author 属性
@end
@implementation Book
@end
pod 'YYModel'
。pod install
或 pod update
。github "ibireme/YYModel"
。carthage update --platform ios
并将生成的 framework 添加到你的工程。YYModel.h
。常见问题及demo:点击这里
源码下载:YYModel.zip
环信作为中国即时通讯云的开创者一直致力于为开发者提供简单/易用/稳定/完美的产品和服务。我们深知产品和服务没有极限和尽头,为了给开发者提供超出预期的产品和服务,我们诚邀小伙伴们积极参与和帮助。所谓人人为我,我为人人,让我们一起来找bug,一起提建议,一起来打造世界上最好用的即时通讯云sdk叭!
4月1日-4月30日
下载最新版环信IM SDK (https://www.easemob.com/download/im) ,依照开发文档进行模拟操作,找出开发文档或SDK或Demo中的bug或建议,即可参与活动!
集成成功的截图和产品建议请回复本帖,bug等问题发送邮件至:market@easemob.com
| 参与方式 | 描述及示例 | 奖励 |
1 | 成功集成环信IM SDK! | 在本帖回复成功集成SDK的截图,并进官方红包群。 如:iOS 开发者:截图环信后台AppKey + 环信SDK初始化代码 (截到KEY 和 编译成功) + 项目的Bundle identifier Android开发者截图环信后台APPKey+项目集成的KEY +项目的applicationId 其他客户端以此类推。 | 50%中奖率抽50元京东卡,不定时群红包 |
2 | 一般功能bug EaseUIKit UI 及兼容性问题 | 例如:某功能没有达到预期效果(iOS设置头像圆角不生效)或响应结果与文档描述不符等情况; 界面(EaseUIKit)样式错乱,或在不同机型上出现的特异性问题; | 50元京东卡 |
3 | 重大bug发现 | 使用环信SDK时出现异常崩溃 崩溃信息指向sdk需提供异常信息log日志 SDK某个API调用设置不生效/部分监听回调不执行等 | 200元京东卡 |
4 | 用户体验型建议(含Demo) | 页面提示不友好等用户体验问题,对操作和引导不产生影响,属于使用建议。用户体验型问题(包括Demo应用体验) | IMGeek定制T恤 |
5 | 对环信IM SDK提出有效的产品建议 | 提出具体的产品建议信息,可以是交互形式,产品新功能等 | IMGeek定制T恤
|
6 | 开发文档修正建议 | 包括错误的内容、不合理的文档结构等 例如:参数错误、文档接口过时或文档接口与SDK不符等 | IMGeek定制T恤 |
7 | 其他回帖内容 | 除以上回帖内容外,其他回帖根据内容酌情奖励 |
|
1、集成成功的截图和产品建议请回复本帖~!经确认符合活动要求,将私信联系你领奖。
2、发现bug等问题,请提供demo与复现步骤,将问题发送邮件至 market@easemob.com,邮件内容经官方确认属实,邮件联系你领取奖励;如不是产品bug,会给予回复或解决方法;
3、如果有相同的bug提交 ,按收到邮件先后顺序奖励较早的提交者;
4、反馈多条问题时,定制T恤奖励不叠加,京东卡可叠加至200元上限;
5、本次活动不限客户端;
新集成环信SDK的用户请留意私信,冬冬将拉你进抽奖群。
PS:如果推荐朋友集成环信IM SDK,推荐人与被推荐人都可进群参与抽奖哦!
开发者帐号注册地址:https://console.easemob.com/user/register
登录console地址:https://console.easemob.com/user/login
IM SDK及Demo下载:https://www.easemob.com/download/im
安卓端开发文档:http://docs-im.easemob.com/im/android/sdk/import
iOS端开发文档:http://docs-im.easemob.com/im/ios/sdk/prepare
WEB端开发文档:http://docs-im.easemob.com/im/web/intro/start
小程序端开发文档:http://docs-im.easemob.com/im/applet/solution
桌面端开发文档:http://docs-im.easemob.com/im/pc/intro/integration
Linux端开发文档:http://docs-im.easemob.com/im/linux/integration
服务端开发文档:http://docs-im.easemob.com/im/server/ready/intro
常见开发场景说明:http://docs-im.easemob.com/im/other/integrationcases/live-chatroo
*活动最终解释权归环信IMGeek社区所有。
收起阅读 »AAChartKit
AAChartKit 项目,是AAInfographics的 Objective-C
语言版本,是在流行的开源前端图表库Highcharts的基础上,封装的面向对象的,一组简单易用,极其精美的图表绘制控件.可能是这个星球上 UI 最精致的第三方 iOS 开源图表库了(✟我以无神论者的名义向上帝起誓
iOS 8+
, 支持iOS
、 iPad OS
、macOS
, 支持 Objective-C
语言, 同时更有 Swift
语言版本 AAInfographics 、 Java
语言版本 AAChartCore 、Kotlin
语言版本 AAChartCore-Kotlin 可供使用, 配置导入工程简单易操作. 支持的所有语言版本及连接,参见此列表.柱状图
、条形图
、折线图
、曲线图
、折线填充图
、曲线填充图
、雷达图
、极地图
、扇形图
、气泡图
、散点图
、区域范围图
、柱形范围图
、面积范围图
、面积范围均线图
、直方折线图
、直方折线填充图
、箱线图
、瀑布图
、热力图
、桑基图
、金字塔图
、漏斗图
、等二十几种类型的图形,不可谓之不多.主标题
、副标题
、X 轴
、Y 轴
自不必谈, 从纵横的交互准星线
、跟手的浮动提示框
, 到切割数值的值域分割线
、值域分割颜色带
, 再到细小的线条
类型,标记点
样式, 各种细微的图形子组件, 应有尽有. 以至于不论是极简
、抽象的小清新风格, 还是纷繁复杂
的严肃商业派头, 均可完美驾驭.动画
效果细腻精致, 流畅优美. 有三十多种以上渲染动画效果可供选择, 用户可自由设置渲染图形时的动画时间和动画类型, 关于图形渲染动画类型,具体参见 AAChartKit 动画类型.AAChartView + AAChartModel = Chart
,在 AAChartKit 图表框架当中,遵循这样一个极简主义公式:图表视图控件 + 图表模型 = 你想要的图表
. 同另一款强大而又精美的图表库AAInfographics完全一致.链式编程语法
, 一行代码即可配置完成 AAChartModel
模型对象实例.AAChartModel
实例对象时, 无论你写多少行代码, 理论上只能算作是一行). Podfile
中添加以下内容pod 'AAChartKit', :git => 'https://github.com/AAChartModel/AAChartKit.git'
pod install
或 pod update
Demo
中的文件夹AAChartKitLib
拖入到所需项目中..pch
全局宏定义文件中添加#import "AAGlobalMacro.h"
ViewController
视图控制器文件中添加#import "AAChartKit.h"
AAChartView
CGFloat chartViewWidth = self.view.frame.size.width;
CGFloat chartViewHeight = self.view.frame.size.height - 250;
_aaChartView = [[AAChartView alloc]init];
_aaChartView.frame = CGRectMake(0, 60, chartViewWidth, chartViewHeight);
////禁用 AAChartView 滚动效果(默认不禁用)
//self.aaChartView.scrollEnabled = NO;
[self.view addSubview:_aaChartView];
AAChartModel
AAChartModel *aaChartModel= AAObject(AAChartModel)
.chartTypeSet(AAChartTypeArea)//设置图表的类型(这里以设置的为折线面积图为例)
.titleSet(@"编程语言热度")//设置图表标题
.subtitleSet(@"虚拟数据")//设置图表副标题
.categoriesSet(@[@"Java",@"Swift",@"Python",@"Ruby", @"PHP",@"Go",@"C",@"C#",@"C++"])//图表横轴的内容
.yAxisTitleSet(@"摄氏度")//设置图表 y 轴的单位
.seriesSet(@[
AAObject(AASeriesElement)
.nameSet(@"2017")
.dataSet(@[@7.0, @6.9, @9.5, @14.5, @18.2, @21.5, @25.2, @26.5, @23.3, @18.3, @13.9, @9.6]),
AAObject(AASeriesElement)
.nameSet(@"2018")
.dataSet(@[@0.2, @0.8, @5.7, @11.3, @17.0, @22.0, @24.8, @24.1, @20.1, @14.1, @8.6, @2.5]),
AAObject(AASeriesElement)
.nameSet(@"2019")
.dataSet(@[@0.9, @0.6, @3.5, @8.4, @13.5, @17.0, @18.6, @17.9, @14.3, @9.0, @3.9, @1.0]),
AAObject(AASeriesElement)
.nameSet(@"2020")
.dataSet(@[@3.9, @4.2, @5.7, @8.5, @11.9, @15.2, @17.0, @16.6, @14.2, @10.3, @6.6, @4.8]),
])
;
AAChartView
实例对象后,首次绘制图形调用此方法)/*图表视图对象调用图表模型对象,绘制最终图形*/
[_aaChartView aa_drawChartWithChartModel:aaChartModel];
typedef NSString *AAChartType;
AACHARTKIT_EXTERN AAChartType const AAChartTypeColumn; //柱形图
AACHARTKIT_EXTERN AAChartType const AAChartTypeBar; //条形图
AACHARTKIT_EXTERN AAChartType const AAChartTypeArea; //折线区域填充图
AACHARTKIT_EXTERN AAChartType const AAChartTypeAreaspline; //曲线区域填充图
AACHARTKIT_EXTERN AAChartType const AAChartTypeLine; //折线图
AACHARTKIT_EXTERN AAChartType const AAChartTypeSpline; //曲线图
AACHARTKIT_EXTERN AAChartType const AAChartTypeScatter; //散点图
AACHARTKIT_EXTERN AAChartType const AAChartTypePie; //扇形图
AACHARTKIT_EXTERN AAChartType const AAChartTypeBubble; //气泡图
AACHARTKIT_EXTERN AAChartType const AAChartTypePyramid; //金字塔图
AACHARTKIT_EXTERN AAChartType const AAChartTypeFunnel; //漏斗图
AACHARTKIT_EXTERN AAChartType const AAChartTypeColumnrange; //柱形范围图
AACHARTKIT_EXTERN AAChartType const AAChartTypeArearange; //区域折线范围图
AACHARTKIT_EXTERN AAChartType const AAChartTypeAreasplinerange; //区域曲线范围图
AACHARTKIT_EXTERN AAChartType const AAChartTypeBoxplot; //箱线图
AACHARTKIT_EXTERN AAChartType const AAChartTypeWaterfall; //瀑布图
AACHARTKIT_EXTERN AAChartType const AAChartTypePolygon; //多边形图
typedef NSString *AAChartZoomType;
AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeNone; //禁用手势缩放功能(默认禁用手势缩放)
AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeX; //支持图表 X轴横向缩放
AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeY; //支持图表 Y轴纵向缩放
AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeXY; //支持图表等比例缩放
AAChartModel
属性配置列表AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, title) //标题文本内容
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, titleFontSize) //标题字体尺寸大小
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, titleFontColor) //标题字体颜色
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, titleFontWeight) //标题字体粗细
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitle) //副标题文本内容
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, subtitleFontSize) //副标题字体尺寸大小
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitleFontColor) //副标题字体颜色
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitleFontWeight) //副标题字体粗细
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, backgroundColor) //图表背景色(必须为十六进制的颜色色值如红色"#FF0000")
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray <NSString *>*, colorsTheme) //图表主题颜色数组
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray <NSString *>*, categories) //x轴坐标每个点对应的名称(注意:这个不是用来设置 X 轴的值,仅仅是用于设置 X 轴文字内容的而已)
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, series) //图表的数据列内容
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartSubtitleAlignType, subtitleAlign) //图表副标题文本水平对齐方式。可选的值有 “left”,”center“和“right”。 默认是:center.
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartType, chartType) //图表类型
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartStackingType, stacking) //堆积样式
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartSymbolType, markerSymbol) //折线曲线连接点的类型:"circle", "square", "diamond", "triangle","triangle-down",默认是"circle"
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AAChartSymbolStyleType, markerSymbolStyle)
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartZoomType, zoomType) //缩放类型 AAChartZoomTypeX 表示可沿着 x 轴进行手势缩放
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AAChartAnimation, animationType) //设置图表的渲染动画类型
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, animationDuration) //设置图表的渲染动画时长(动画单位为毫秒)
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, inverted) //x 轴是否垂直,默认为否
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, gradientColorsThemeEnabled) //是否将常规主题颜色数组 colorsTheme 自动转换为半透明渐变效果的颜色数组(设置后就不用自己再手动去写渐变色字典,相当于是设置渐变色的一个快捷方式,当然了,如果需要细致地自定义渐变色效果,还是需要自己手动配置渐变颜色字典内容,具体方法参见图表示例中的`颜色渐变条形图`示例代码),默认为否
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, polar) //是否极化图形(变为雷达图),默认为否
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, dataLabelEnabled) //是否显示数据,默认为否
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, dataLabelFontColor) //Datalabel font color
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, dataLabelFontSize) //Datalabel font size
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, dataLabelFontWeight) //Datalabel font weight
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisVisible) //x 轴是否可见(默认可见)
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisReversed) // x 轴翻转,默认为否
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisLabelsEnabled) //x 轴是否显示文字
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisLabelsFontSize) //x 轴文字字体大小
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, xAxisLabelsFontColor) //x 轴文字字体颜色
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartFontWeightType, xAxisLabelsFontWeight) //x 轴文字字体粗细
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisGridLineWidth) //x 轴网格线的宽度
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisTickInterval) //x轴刻度点间隔数(设置每隔几个点显示一个 X轴的内容)
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisCrosshairWidth) //设置 x 轴准星线的宽度
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, xAxisCrosshairColor) //设置 x 轴准星线的颜色
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AALineDashSyleType, xAxisCrosshairDashStyleType) //设置 x 轴准星线的线条样式类型
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisVisible) //y 轴是否可见(默认可见)
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisReversed) //y 轴翻转,默认为否
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisLabelsEnabled) //y 轴是否显示文字
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisLabelsFontSize) //y 轴文字字体大小
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisLabelsFontColor) //y 轴文字字体颜色
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartFontWeightType , yAxisLabelsFontWeight) //y 轴文字字体粗细
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisTitle) //y 轴标题
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisLineWidth) //y y-axis line width
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisGridLineWidth) //y轴网格线的宽度
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisAllowDecimals) //是否允许 y 轴显示小数
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, yAxisPlotLines) //y 轴基线的配置
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisMax) //y 轴最大值
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisMin) //y 轴最小值(设置为0就不会有负数)
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisTickInterval)
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, yAxisTickPositions) //自定义 y 轴坐标(如:[@(0), @(25), @(50), @(75) , (100)])
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisCrosshairWidth) //设置 y 轴准星线的宽度
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisCrosshairColor) //设置 y 轴准星线的颜色
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AALineDashSyleType, yAxisCrosshairDashStyleType) //设置 y 轴准星线的线条样式类型
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, tooltipEnabled) //是否显示浮动提示框(默认显示)
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, tooltipShared)//是否多组数据共享一个浮动提示框
AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, tooltipValueSuffix) //浮动提示框单位后缀
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, connectNulls) //设置折线是否断点重连(是否连接空值点)
AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, legendEnabled) //是否显示图例 lengend(图表底部可点按的圆点和文字)
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, borderRadius) //柱状图长条图头部圆角半径(可用于设置头部的形状,仅对条形图,柱状图有效)
AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, markerRadius) //折线连接点的半径长度
常见问题及详细说明:点击这里
效果预览
# 将以下内容添加到您的Podfile中:
# 不使用网络图片功能
pod 'HXPhotoPicker', '~> 3.1.9'
# 使用SDWebImage加载网络图片
pod 'HXPhotoPicker/SDWebImage', '~> 3.1.9'
# 使用YYWebImage加载网络图片
pod 'HXPhotoPicker/YYWebImage', '~> 3.1.9'
# 搜索不到库或最新版时请执行
pod repo update 或 rm ~/Library/Caches/CocoaPods/search_index.json
# 将以下内容添加到您的Cartfile中:
github "SilenceLove/HXPhotoPicker"
手动导入:将项目中的“HXPhotoPicker”文件夹拖入项目中
使用前导入头文件 "HXPhotoPicker.h"
要求 - Requirements
跳转相册选择照片
// 懒加载 照片管理类
- (HXPhotoManager *)manager {
if (!_manager) {
_manager = [[HXPhotoManager alloc] initWithType:HXPhotoManagerSelectedTypePhotoAndVideo];
}
return _manager;
}
// 方法一:
HXWeakSelf
[self hx_presentSelectPhotoControllerWithManager:self.manager didDone:^(NSArray *allList, NSArray *photoList, NSArray *videoList, BOOL isOriginal, UIViewController *viewController, HXPhotoManager *manager) {
weakSelf.total.text = [NSString stringWithFormat:@"总数量:%ld ( 照片:%ld 视频:%ld )",allList.count, photoList.count, videoList.count];
weakSelf.original.text = isOriginal ? @"YES" : @"NO";
NSSLog(@"block - all - %@",allList);
NSSLog(@"block - photo - %@",photoList);
NSSLog(@"block - video - %@",videoList);
} cancel:^(UIViewController *viewController, HXPhotoManager *manager) {
NSSLog(@"block - 取消了");
}];
// 方法二:
// 照片选择控制器
HXCustomNavigationController *nav = [[HXCustomNavigationController alloc] initWithManager:self.manager delegate:self];
[self presentViewController:nav animated:YES completion:nil];
// 通过 HXCustomNavigationControllerDelegate 代理返回选择的图片以及视频
/**
点击完成按钮
@param photoNavigationViewController self
@param allList 已选的所有列表(包含照片、视频)
@param photoList 已选的照片列表
@param videoList 已选的视频列表
@param original 是否原图
*/
- (void)photoNavigationViewController:(HXCustomNavigationController *)photoNavigationViewController didDoneAllList:(NSArray *)allList photos:(NSArray *)photoList videos:(NSArray *)videoList original:(BOOL)original;
/**
点击取消
@param photoNavigationViewController self
*/
- (void)photoNavigationViewControllerDidCancel:(HXCustomNavigationController *)photoNavigationViewController;
单独使用HXPhotoPreviewViewController预览图片
HXCustomAssetModel *assetModel1 = [HXCustomAssetModel assetWithLocaImageName:@"1" selected:YES];
// selected 为NO 的会过滤掉
HXCustomAssetModel *assetModel2 = [HXCustomAssetModel assetWithLocaImageName:@"2" selected:NO];
HXCustomAssetModel *assetModel3 = [HXCustomAssetModel assetWithNetworkImageURL:[NSURL URLWithString:@"http://tsnrhapp.oss-cn-hangzhou.aliyuncs.com/1466408576222.jpg"] selected:YES];
// selected 为NO 的会过滤掉
HXCustomAssetModel *assetModel4 = [HXCustomAssetModel assetWithNetworkImageURL:[NSURL URLWithString:@"http://tsnrhapp.oss-cn-hangzhou.aliyuncs.com/0034821a-6815-4d64-b0f2-09103d62630d.jpg"] selected:NO];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"QQ空间视频_20180301091047" withExtension:@"mp4"];
HXCustomAssetModel *assetModel5 = [HXCustomAssetModel assetWithLocalVideoURL:url selected:YES];
HXPhotoManager *photoManager = [HXPhotoManager managerWithType:HXPhotoManagerSelectedTypePhotoAndVideo];
photoManager.configuration.saveSystemAblum = YES;
photoManager.configuration.photoMaxNum = 0;
photoManager.configuration.videoMaxNum = 0;
photoManager.configuration.maxNum = 10;
photoManager.configuration.selectTogether = YES;
photoManager.configuration.photoCanEdit = NO;
photoManager.configuration.videoCanEdit = NO;
HXWeakSelf
// 长按事件
photoManager.configuration.previewRespondsToLongPress = ^(UILongPressGestureRecognizer *longPress,
HXPhotoModel *photoModel,
HXPhotoManager *manager,
HXPhotoPreviewViewController *previewViewController) {
hx_showAlert(previewViewController, @"提示", @"长按事件", @"确定", nil, nil, nil);
};
// 跳转预览界面时动画起始的view
photoManager.configuration.customPreviewFromView = ^UIView *(NSInteger currentIndex) {
HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
return viewCell;
};
// 跳转预览界面时展现动画的image
photoManager.configuration.customPreviewFromImage = ^UIImage *(NSInteger currentIndex) {
HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
return viewCell.imageView.image;
};
// 退出预览界面时终点view
photoManager.configuration.customPreviewToView = ^UIView *(NSInteger currentIndex) {
HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
return viewCell;
};
[photoManager addCustomAssetModel:@[assetModel1, assetModel2, assetModel3, assetModel4, assetModel5]];
[self hx_presentPreviewPhotoControllerWithManager:photoManager
previewStyle:HXPhotoViewPreViewShowStyleDark
currentIndex:0
photoView:nil];
UIViewController+HXExtension.h
/// 跳转预览照片界面
/// @param manager 照片管理者
/// @param previewStyle 预览样式
/// @param currentIndex 当前预览的下标
/// @param photoView 照片展示视图 - 没有就不传
- (void)hx_presentPreviewPhotoControllerWithManager:(HXPhotoManager *)manager
previewStyle:(HXPhotoViewPreViewShowStyle)previewStyle
currentIndex:(NSUInteger)currentIndex
photoView:(HXPhotoView * _Nullable)photoView;
使用如何保存草稿
通过 HXPhotoManager 对象进行存储
/// 获取保存在本地文件的模型数组
- (NSArray *)getLocalModelsInFile;
/// 将模型数组保存到本地文件
- (BOOL)saveLocalModelsToFile;
/// 将保存在本地文件的模型数组删除
- (BOOL)deleteLocalModelsInFile;
/// 将本地获取的模型数组添加到manager的数据中
/// @param models 在本地获取的模型数组
- (void)addLocalModels:(NSArray *)models;
/// 将本地获取的模型数组添加到manager的数据中
- (void)addLocalModels;
demo:HXPhotoPicker.zip
常见问题及源码地址:点击这里
SDWebImage 一款超级好用的网络图片加载库
集成方式
pod 'SDWebImage', '~> 5.0'
使用方式
#import <SDWebImage/SDWebImage.h>
[imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址"]
placeholderImage:[UIImage imageNamed:@"占位图名字"]];
加载gif
SDAnimatedImageView *imageView = [SDAnimatedImageView new];
SDAnimatedImage *animatedImage = [SDAnimatedImage imageNamed:@"image.gif"];
imageView.image = animatedImage;
使用Blocks,采用这个方案可以在网络图片加载过程中得知图片的下载进度和图片加载成功与否
[imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址"] placeholderImage:[UIImage imageNamed:@"占位图"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { //... completion code here ... }];
[SDImageCache sharedImageCache] getSize];
[[SDImageCache sharedImageCache] clearMemory];
推荐一个好用的iOS 提示框库
集成方式
pod 'MBProgressHUD', '~> 1.2.0'
或者直接将附件拖入项目内
导入#import "MBProgressHUD.h"
在这里顺便分享一下使用小技巧
我们可以将hud定义成宏 .
#pragma mark - hud 提示
/**
默认请求开始的hud
*/
#define HudShow MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\
hud.label.text = NSLocalizedString(@"请求中...", @"HUD loading title");
/**
自定义title的请求开始hud
*/
#define HudShowStr(str) MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\
hud.label.text = NSLocalizedString(str, @"HUD loading title");
/**
移除hud
*/
#define HudHidden [hud hideAnimated:YES];
/**
自定义title 的提示hud
*/
#define HudMessageStr(str) hud.mode = MBProgressHUDModeText;\
hud.label.text = NSLocalizedString(str, @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\
/**
提示的hud .title来自json
*/
#define HudMessage hud.mode = MBProgressHUDModeText;\
hud.label.text = NSLocalizedString(@"根据需求传值例如取服务器json的某个值", @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\
/**
请求错误时的hud
*/
#define HudError hud.mode = MBProgressHUDModeText;\
hud.label.text = NSLocalizedString(@"网络差", @"HUD message title"); \
[hud hideAnimated:YES afterDelay:1.f];
/**
alert效果的hud
*/
#define alertHudShow(str) MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\
hud.mode = MBProgressHUDModeText;\
hud.label.text = NSLocalizedString(str, @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\
/**
请求时错误才提示的hud
*/
#define RequestErrorHud MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\
hud.mode = MBProgressHUDModeText;\
hud.label.text = NSLocalizedString(@"网络差", @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\
源码下载: Hud.zip
常见问题及demo:点击这里
收起阅读 »android studio
// 基础依赖包,必须要依赖
implementation 'com.gyf.immersionbar:immersionbar:3.0.0'
// fragment快速实现(可选)
implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0'
// kotlin扩展(可选)
implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0'
android.useAndroidX=true
android.enableJetifier=true
在manifest加入如下配置,四选其一,或者都写
① 在manifest的Application节点下加入
<meta-data
android:name="android.max_aspect"
android:value="2.4" />
② 在manifest的Application节点中加入
android:resizeableActivity="true"
③ 在manifest的Application节点中加入
android:maxAspectRatio="2.4"
④ 升级targetSdkVersion为25以上版本
在manifest的Application节点下加入,vivo和oppo没有找到相关配置信息
<meta-data
android:name="android.notch_support"
android:value="true"/>
<meta-data
android:name="notch.config"
android:value="portrait|landscape" />
基础用法
ImmersionBar.with(this).init();
高级用法(每个参数的意义)
ImmersionBar.with(this)
.transparentStatusBar() //透明状态栏,不写默认透明色
.transparentNavigationBar() //透明导航栏,不写默认黑色(设置此方法,fullScreen()方法自动为true)
.transparentBar() //透明状态栏和导航栏,不写默认状态栏为透明色,导航栏为黑色(设置此方法,fullScreen()方法自动为true)
.statusBarColor(R.color.colorPrimary) //状态栏颜色,不写默认透明色
.navigationBarColor(R.color.colorPrimary) //导航栏颜色,不写默认黑色
.barColor(R.color.colorPrimary) //同时自定义状态栏和导航栏颜色,不写默认状态栏为透明色,导航栏为黑色
.statusBarAlpha(0.3f) //状态栏透明度,不写默认0.0f
.navigationBarAlpha(0.4f) //导航栏透明度,不写默认0.0F
.barAlpha(0.3f) //状态栏和导航栏透明度,不写默认0.0f
.statusBarDarkFont(true) //状态栏字体是深色,不写默认为亮色
.navigationBarDarkIcon(true) //导航栏图标是深色,不写默认为亮色
.autoDarkModeEnable(true) //自动状态栏字体和导航栏图标变色,必须指定状态栏颜色和导航栏颜色才可以自动变色哦
.autoStatusBarDarkModeEnable(true,0.2f) //自动状态栏字体变色,必须指定状态栏颜色才可以自动变色哦
.autoNavigationBarDarkModeEnable(true,0.2f) //自动导航栏图标变色,必须指定导航栏颜色才可以自动变色哦
.flymeOSStatusBarFontColor(R.color.btn3) //修改flyme OS状态栏字体颜色
.fullScreen(true) //有导航栏的情况下,activity全屏显示,也就是activity最下面被导航栏覆盖,不写默认非全屏
.hideBar(BarHide.FLAG_HIDE_BAR) //隐藏状态栏或导航栏或两者,不写默认不隐藏
.addViewSupportTransformColor(toolbar) //设置支持view变色,可以添加多个view,不指定颜色,默认和状态栏同色,还有两个重载方法
.titleBar(view) //解决状态栏和布局重叠问题,任选其一
.titleBarMarginTop(view) //解决状态栏和布局重叠问题,任选其一
.statusBarView(view) //解决状态栏和布局重叠问题,任选其一
.fitsSystemWindows(true) //解决状态栏和布局重叠问题,任选其一,默认为false,当为true时一定要指定statusBarColor(),不然状态栏为透明色,还有一些重载方法
.supportActionBar(true) //支持ActionBar使用
.statusBarColorTransform(R.color.orange) //状态栏变色后的颜色
.navigationBarColorTransform(R.color.orange) //导航栏变色后的颜色
.barColorTransform(R.color.orange) //状态栏和导航栏变色后的颜色
.removeSupportView(toolbar) //移除指定view支持
.removeSupportAllView() //移除全部view支持
.navigationBarEnable(true) //是否可以修改导航栏颜色,默认为true
.navigationBarWithKitkatEnable(true) //是否可以修改安卓4.4和emui3.x手机导航栏颜色,默认为true
.navigationBarWithEMUI3Enable(true) //是否可以修改emui3.x手机导航栏颜色,默认为true
.keyboardEnable(true) //解决软键盘与底部输入框冲突问题,默认为false,还有一个重载方法,可以指定软键盘mode
.keyboardMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) //单独指定软键盘模式
.setOnKeyboardListener(new OnKeyboardListener() { //软键盘监听回调,keyboardEnable为true才会回调此方法
@Override
public void onKeyboardChange(boolean isPopup, int keyboardHeight) {
LogUtils.e(isPopup); //isPopup为true,软键盘弹出,为false,软键盘关闭
}
})
.setOnNavigationBarListener(onNavigationBarListener) //导航栏显示隐藏监听,目前只支持华为和小米手机
.setOnBarListener(OnBarListener) //第一次调用和横竖屏切换都会触发,可以用来做刘海屏遮挡布局控件的问题
.addTag("tag") //给以上设置的参数打标记
.getTag("tag") //根据tag获得沉浸式参数
.reset() //重置所以沉浸式参数
.init(); //必须调用方可应用以上所配置的参数
java用法
ImmersionBar.with(this).init();
kotlin用法
immersionBar {
statusBarColor(R.color.colorPrimary)
navigationBarColor(R.color.colorPrimary)
}
①结合dialogFragment使用,可以参考demo中的BaseDialogFragment这个类
ImmersionBar.with(this).init();
②其他dialog,关闭dialog的时候必须调用销毁方法
ImmersionBar.with(this, dialog).init();
销毁方法:
java中
ImmersionBar.destroy(this, dialog);
kotlin中
destroyImmersionBar(dialog)
重点是调用以下方法,但是此方法会导致有导航栏的手机底部布局会被导航栏覆盖,还有底部输入框无法根据软键盘弹出而弹出,具体适配请参考demo。
popupWindow.setClippingEnabled(false);
① 使用dimen自定义状态栏高度,不建议使用,因为设备状态栏高度并不是固定的
在values-v19/dimens.xml文件下
<dimen name="status_bar_height">25dpdimen>
在values/dimens.xml文件下
<dimen name="status_bar_height">0dpdimen>
然后在布局界面添加view标签,高度指定为status_bar_height
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/darker_gray"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="@dimen/status_bar_height"
android:background="@color/colorPrimary" />
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:title="方法一"
app:titleTextColor="@android:color/white" />
LinearLayout>
② 使用系统的fitsSystemWindows属性,使用该属性不会导致输入框与软键盘冲突问题,不要再Fragment使用该属性,只适合纯色状态栏
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
LinearLayout>
然后使用ImmersionBar时候必须指定状态栏颜色
ImmersionBar.with(this)
.statusBarColor(R.color.colorPrimary)
.init();
③ 使用ImmersionBar的fitsSystemWindows(boolean fits)方法,只适合纯色状态栏
ImmersionBar.with(this)
.fitsSystemWindows(true) //使用该属性,必须指定状态栏颜色
.statusBarColor(R.color.colorPrimary)
.init();
④ 使用ImmersionBar的statusBarView(View view)方法,可以用来适配渐变色状态栏、侧滑返回
在标题栏的上方增加View标签,高度指定为0dp
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/darker_gray"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/colorPrimary" />
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:title="方法四"
app:titleTextColor="@android:color/white" />
LinearLayout>
然后使用ImmersionBar的statusBarView方法,指定view就可以啦
ImmersionBar.with(this)
.statusBarView(view)
.init();
//或者
//ImmersionBar.setStatusBarView(this,view);
⑤ 使用ImmersionBar的titleBar(View view)方法,原理是设置paddingTop,可以用来适配渐变色状态栏、侧滑返回
ImmersionBar.with(this)
.titleBar(view) //可以为任意view,如果是自定义xml实现标题栏的话,标题栏根节点不能为RelativeLayout或者ConstraintLayout,以及其子类
.init();
//或者
//ImmersionBar.setTitleBar(this, view);
⑥ 使用ImmersionBar的titleBarMarginTop(View view)方法,原理是设置marginTop,只适合纯色状态栏
ImmersionBar.with(this)
.titleBarMarginTop(view) //可以为任意view
.statusBarColor(R.color.colorPrimary) //指定状态栏颜色,根据情况是否设置
.init();
//或者使用静态方法设置
//ImmersionBar.setTitleBarMarginTop(this,view);
ImmersionBar.with(this)
.keyboardEnable(true) //解决软键盘与底部输入框冲突问题
// .keyboardEnable(true, WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
// | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) //软键盘自动弹出
.init();
ImmersionBar.with(this)
.statusBarDarkFont(true, 0.2f) //原理:如果当前设备支持状态栏字体变色,会设置状态栏字体为黑色,如果当前设备不支持状态栏字体变色,会使当前状态栏加上透明度,否则不执行透明度
.init();
public static boolean hasNavigationBar(Activity activity)
判断是否存在导航栏
public static int getNavigationBarHeight(Activity activity)
获得导航栏的高度
public static int getNavigationBarWidth(Activity activity)
获得导航栏的宽度
public static boolean isNavigationAtBottom(Activity activity)
判断导航栏是否在底部
public static int getStatusBarHeight(Activity activity)
获得状态栏的高度
public static int getActionBarHeight(Activity activity)
获得ActionBar的高度
public static boolean hasNotchScreen(Activity activity)
是否是刘海屏
public static boolean getNotchHeight(Activity activity)
获得刘海屏高度
public static boolean isSupportStatusBarDarkFont()
判断当前设备支不支持状态栏字体设置为黑色
public static boolean isSupportNavigationIconDark()
判断当前设备支不支持导航栏图标设置为黑色
public static void hideStatusBar(Window window)
隐藏状态栏
-keep class com.gyf.immersionbar.* {*;}
-dontwarn com.gyf.immersionbar.**
代码下载 :ImmersionBar-master.zip
原文链接:https://github.com/gyf-dev/ImmersionBar
material风格(v7支持包中的),ios风格,自动获取顶层activity,可在任意界面弹出,可在任意线程弹出
注意使用的场景:
第一此进入页面,用layout内部的loadingview,有很多statelayout框架,
再次刷新,用刷新头显示刷新状态
局部刷新或点击某按钮访问网络,用loading dialog,不影响页面本身状态,类似web中的ajax请求.
Step 1. Add the JitPack repository to your build file
Add it in your root build.gradle at the end of repositories:
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
Step 2. Add the dependency
dependencies {
compile ('com.github.hss01248:DialogUtil:lastest release'){
exclude group: 'com.android.support'
}
compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support:recyclerview-v7:26.1.0'
compile 'com.android.support:design:26.1.0'
//将26.1.0: 改为自己项目中一致的版本
}
lastest release: https://github.com/hss01248/DialogUtil/releases
//在Application的oncreate方法里:
传入context
StyledDialog.init(this);
在activity生命周期callback中拿到顶层activity引用:
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
ActivityStackManager.getInstance().addActivity(activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
ActivityStackManager.getInstance().removeActivity(activity);
}
});
//使用默认样式时,无须.setxxx:
StyledDialog.buildLoading().show();
//自定义部分样式时:
StyledDialog.buildMdAlert("title", msg, new MyDialogListener() {
@Override
public void onFirst() {
showToast("onFirst");
}
@Override
public void onSecond() {
showToast("onSecond");
}
@Override
public void onThird() {
showToast("onThird");
}
})
.setBtnSize(20)
.setBtnText("i","b","3")
.show();
public abstract void onFirst();//md-确定,ios-第一个
public abstract void onSecond();//md-取消,ios-第二个
public void onThird(){}//md-netural,ios-第三个
public void onCancle(){}
/**
* 提供给Input的回调
* @param input1
* @param input2
*/
public void onGetInput(CharSequence input1,CharSequence input2){
}
/**
* 提供给MdSingleChoose的回调
* @param chosen
* @param chosenTxt
*/
public void onGetChoose(int chosen,CharSequence chosenTxt){
}
/**
* 提供给MdMultiChoose的回调
* @param states
*/
public void onChoosen( List selectedIndex, List selectedStrs,boolean[] states){
}
/**
* IosSingleChoose,BottomItemDialog的点击条目回调
* @param text
* @param position
*/
public abstract void onItemClick(CharSequence text, int position);
/**
* BottomItemDialog的底部按钮(经常是取消)的点击回调
*/
public void onBottomBtnClick(){}
StyledDialog.dismiss(DialogInterface... dialogs);
StyledDialog.dismissLoading();
/**
* 可以在任何线程调用
* @param dialog 传入show方法返回的对象
* @param progress
* @param max
* @param msg 如果是转圈圈,会将msg变成msg:78%的形式.如果是水平,msg不起作用
* @param isHorizontal 是水平线状,还是转圈圈
*/
public static void updateProgress( Dialog dialog, int progress, int max, CharSequence msg, boolean isHorizontal)
代码下载: DialogUtil-master.zip
原文链接:https://github.com/hss01248/DialogUtil
收起阅读 »