注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

12个出现频率最高的iOS技术面试题及答案

这篇文章给大家总结了在iOS面试的时候可能会遇到的12个技术面试题,以及这些面试题但答案,这些答案只是给大家一些参考,大家可以再结合自己理解进行回答,有需要的朋友们下面来一起看看吧。前言随着移动互联网科技不断的发展和创新,如今无论是公司还是开发者或设计师个人而...
继续阅读 »

这篇文章给大家总结了在iOS面试的时候可能会遇到的12个技术面试题,以及这些面试题但答案,这些答案只是给大家一些参考,大家可以再结合自己理解进行回答,有需要的朋友们下面来一起看看吧。

前言

随着移动互联网科技不断的发展和创新,如今无论是公司还是开发者或设计师个人而言,面试都是一项耗时耗钱的项目,而面对iOS开发者及设计师在面试时可能会遇到的问题进行了筛选与汇总。下面我们一起来一下看看吧。

一、如何绘制UIView?

绘制一个UIView最灵活的方法就是由它自己完成绘制。实际上你不是绘制一个UIView,而是子类化一个UIView并赋予绘制自己的能力。当一个UIView需要执行绘制操作时,drawRect:方法就会被调用,覆盖此方法让你获得绘图操作的机会。当drawRect:方法被调用,当前图形的上下文也被设置为属于视图的图形上下文,你可以使用Core Graphic或者UIKit提供的方法将图形画在该上下文中。

二、什么是MVVM?主要目的是什么?优点有哪些?

MVVM即 Model-View-ViewModel

1.View主要用于界面呈现,与用户输入设备进行交互、

2.ViewModel是MVVM架构中最重要的部分,ViewModel中包含属性,方法,事件,属性验证等逻辑,负责ViewModel之间的通讯

3.Model就是我们常说的数据模型,用于数据的构造,数据的驱动,主要提供基础实体的属性。

MVVM主要目的是分离视图和模型

MVVM优点:低耦合,可重用性,独立开发,可测试

三、get请求与post请求的区别

1.get是向服务器发索取数据的一种请求,而post是向服务器提交数据的一种请求

2.get没有请求体,post有请求体

3.get请求的数据会暴露在地址栏中,而post请求不会,所以post请求的安全性比get请求号

4.get请求对url长度有限制,而post请求对url长度理论上是不会收限制的,但是实际上各个服务器会规定对post提交数据大小进行限制。

四、谈谈你对多线程开发的理解?ios中有几种实现多线程的方法?

好处:

1.使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片,视频的下载;

2.发挥多核处理器的优势,并发执行让系统运行的更快,更流畅,用户体验更好;

缺点:

1.大量的线程降低代码的可读性;

2.更多的线程需要更多的内存空间;

3当多个线程对同一个资源出现争夺的时候要注意线程安全的问题。

ios有3种多线程编程的技术:1.NSThread,2.NSOperationQueue,3.gcd;

五、XMPP工作原理;xmpp系统特点

原理:

1.所有从一个client到另一个clientjabber消息和数据都要通过xmpp server

2.client链接到server

3.server利用本地目录系统的证书对其认证

4.server查找,连接并进行相互认证

5.client间进行交互

特点:1)客户机/服务器通信模式;2)分布式网络;3)简单的客户端;4)XML的数据格式

六、地图的定位是怎么实现的?

1.导入了CoreLocation.framework

2.ios8以后,如果需要使用定位功能,就需要请求用户授权,在首次运行时会弹框提示

3.通过本机自带的gps获取位置信息(即经纬度)

七、苹果内购实现流程

程序通过bundle存储的plist文件得到产品标识符的列表。

程序向App Store发送请求,得到产品的信息。

App Store返回产品信息。

程序把返回的产品信息显示给用户(App的store界面)

用户选择某个产品

程序向App Store发送支付请求

App Store处理支付请求并返回交易完成信息。

App获取信息并提供内容给用户。

八、支付宝,微信等相关类型的sdk的集成

1.在支付宝开发平台创建应用并获取APPID

2.配置密钥

3.集成并配置SDK

4.调用接口(如交易查询接口,交易退款接口)

九、 gcd产生死锁的原因及解锁的方法

产生死锁的必要条件:1.互斥条件,2.请求与保持条件,3.不剥夺条件,4.循环等待条件。

解决办法:采用异步执行block。

十、生成二维码的步骤

1.使用CIFilter滤镜类生成二维码

2.对生成的二维码进行加工,使其更清晰

3.自定义二维码背景色、填充色

4.自定义定位角标

5.在二维码中心插入小图片


十一、在使用XMPP的时候有没有什么困难

发送附件(图片,语音,文档...)时比较麻烦

XMPP框架没有提供发送附件的功能,需要自己实现

实现方法,把文件上传到文件服务器,上传成功后获取文件保存路径,再把附件的路径发送给好友



十二、是否使用过环信,简单的说下环信的实现原理

环信是一个即时通讯的服务提供商

环信使用的是XMPP协议,它是再XMPP的基础上进行二次开发,对服务器Openfire和客户端进行功能模型的添加和客户端SDK的封装,环信的本质还是使用XMPP,基于Socket的网络通信

环信内部实现了数据缓存,会把聊天记录添加到数据库,把附件(如音频文件,图片文件)下载到本地,使程序员更多时间是花到用户体验体验上。



作者:iOS鑫
链接:https://www.jianshu.com/p/d95967869aed
收起阅读 »

最新iOS开发常见面试题-基础篇

1.iOS线程与进程的区别和联系?进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而...
继续阅读 »

1.iOS线程与进程的区别和联系?

进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。

程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

2.iOS 如何找到最合适的控件来处理事件?

自己是否能接收触摸事件?
触摸点是否在自己身上?
从后往前遍历子控件,重复前面的两个步骤
如果没有符合条件的子控件,那么就自己最适合处理

3.iOS static 关键字的作用?

(1)函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,

因此其值在下次调用时仍维持上次的值;

(2)在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;

(3)在模块内的 static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明

它的模块内;

(4)在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

(5)在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的static 成员变量。

4.iOS UIEvent对象的作用与常见属性?

每产生一个事件,就会产生一个UIEvent对象

UIEvent : 称为事件对象,记录事件产生的时刻和类型
常见属性 

  //事件类型
//@property(nonatomic,readonly) UIEventType type;
//@property(nonatomic,readonly) UIEventSubtype subtype;
//事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)

4.ViewController 的 loadView, viewDidLoad, viewDidUnload 分别是在什么时候调用的?在自定义ViewController的时候这几个函数里面应该做什么工作?

viewDidLoad在view 从nib文件初始化时调用,loadView在controller的view为nil时调用。此方法在编程实现view时调用,view 控制器默认会注册memory warning notification,当view controller的任何view 没有用的时候,viewDidUnload会被调用,在这里实现将retain 的view release,如果是retain的IBOutlet view 属性则不要在这里release,IBOutlet会负责release 。

5.object-c 的优缺点 ?

objc优点:

  1. Cateogies

  2. Posing

  3. 动态识别

  4. 指标计算

5)弹性讯息传递

  1. 不是一个过度复杂的 C 衍生语言

  2. Objective-C 与 C++ 可混合编程

缺点:

  1. 不支援命名空间

  2. 不支持运算符重载

3)不支持多重继承

4)使用动态运行时类型,所有的方法都是函数调用,所以很多编译时优化方法都用不到。(如内联函数等),性能低劣。

6.iOS引用与指针有什么区别?

1.引用必须被初始化,指针不必。
2.引用初始化以后不能被改变,指针可以改变所指的对象。
3.不存在指向空值的引用,但是存在指向空值的指针。

7.iOS堆和栈的区别 ?

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

申请大小:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

8.什么时候用delegate,什么时候用Notification?

delegate针对one-to-one关系,并且reciever可以返回值 给sender,notification 可以针对one-to-one/many/none,reciever无法返回值给sender.所以,delegate用于sender希望接受到 reciever的某个功能反馈值,notification用于通知多个object某个事件。

9.iOS UITouch对象的作用与常见属性?

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

UITouch的作用:

保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
当手指离开屏幕时,系统会销毁相应的UITouch对象
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;

UITouch的常见方法

   //返回值表示触摸在view上的位置
//这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(UIView *)view;


10.object-c 的内存管理 ?

如果您通过分配和初始化(比如[[MyClass alloc] init])的方式来创建对象,您就拥有这个对象,需要负责该对象的释放。这个规则在使用NSObject的便利方法new 时也同样适用。

如果您拷贝一个对象,您也拥有拷贝得到的对象,需要负责该对象的释放。

如果您保持一个对象,您就部分拥有这个对象,需要在不再使用时释放该对象。

反过来,如果您从其它对象那里接收到一个对象,则您不拥有该对象,也不应该释放它(这个规则有少数的例外)

11.iOS单件实例是什么 ?

Foundation 和 Application Kit 框架中的一些类只允许创建单件对象,即这些类在当前进程中的唯一实例。举例来说,NSFileManager 和NSWorkspace 类在使用时都是基于进程进行单件对象的实例化。当向这些类请求实例的时候,它们会向您传递单一实例的一个引用,如果该实例还不存在,则首先进行实例的分配和初始化。单件对象充当控制中心的角色,负责指引或协调类的各种服务。如果类在概念上只有一个实例(比如NSWorkspace),就应该产生一个单件实例,而不是多个实例;如果将来某一天可能有多个实例,您可以使用单件实例机制,而不是工厂方法或函数。

12.iOS类工厂方法是什么 ?

类工厂方法的实现是为了向客户提供方便,它们将分配和初始化合在一个步骤中,返回被创建的对象,并进行自动释放处理。这些方法的形式是+ (type)className...(其中 className不包括任何前缀)。

工厂方法可能不仅仅为了方便使用。它们不但可以将分配和初始化合在一起,还可以为初始化过程提供对象的分配信息,类工厂方法的另一个目的是使类(比如NSWorkspace)提供单件实例。虽然init...方法可以确认一个类在每次程序运行过程只存在一个实例,但它需要首先分配一个“生的”实例,然后还必须释放该实例,工厂方法则可以避免为可能没有用的对象盲目分配内存。

13.一个指针可以是volatile 吗?解释为什么。

是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

14.iOS 类别的局限性 ?

有两方面局限性:

(1)无法向类中添加新的实例变量,类别没有位置容纳实例变量。

(2)名称冲突,即当类别中的方法与原始类方法名称冲突时,类别具有更高的优先级。类别方法将完全取代初始方法从而无法再使用初始方法。

无法添加实例变量的局限可以使用字典对象解决

15.什么是iOS键-值,键路径是什么 ?

模型的性质是通过一个简单的键(通常是个字符串)来指定的。视图和控制器通过键来查找相应的属性值。在一个给定的实体中,同一个属性的所有值具有相同的数据类型。键-值编码技术用于进行这样的查找—它是一种间接访问对象属性的机制。

键路径是一个由用点作分隔符的键组成的字符串,用于指定一个连接在一起的对象性质序列。第一个键的性质是由先前的性质决定的,接下来每个键的值也是相对于其前面的性质。键路径使您可以以独立于模型

实现的方式指定相关对象的性质。通过键路径,您可以指定对象图中的一个任意深度的路径,使其指向相关对象的特定属性。

16.iOS 类别的作用 ?

类别主要有3个作用:

(1)将类的实现分散到多个不同文件或多个不同框架中。

(2)创建对私有方法的前向引用。

(3)向对象添加非正式协议。

17.sprintf,strcpy,memcpy使用上有什么要注意的地方 ?

strcpy是一个字符串拷贝的函数,它的函数原型为strcpy(char dst, ct char *src);

将 src开始的一段字符串拷贝到dst开始的内存中去,结束的标志符号为'\0',由于拷贝的长度不是由我们自己控制的,所以这个字符串拷贝很容易出错。具备字符串拷贝功能的函数有memcpy,这是一个内存拷贝函数,它的函数原型为memcpy(char dst, c*t char src, unsigned int len);

将长度为len的一段内存,从src拷贝到dst中去,这个函数的长度可控。但是会有内存叠加的问题。

sprintf是格式化函数。将一段数据通过特定的格式,格式化到一个字符串缓冲区中去。sprintf格式化的函数的长度不可控,有可能格式化后的字符串会超出缓冲区的大小,造成溢出。

14答案是:

a) int a; // An integer

b) int *a; // A pointer to an integer

c) int **a; // A pointer to a pointer to an integer

d) int a[10]; // An array of 10 integers

e) int *a[10]; // An array of 10 pointers to integers

f) int (*a)[10]; // A pointer to an array of 10 integers

g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer

h) int (a[10])(int); // An array of 10 pointers to functi that take an integer argument and return an integer

18.readwrite,readonly,assign,retain,copy,nonatomic属性的作用

@property是一个属性访问声明,扩号内支持以下几个属性:

1,getter=getterName,setter=setterName,设置setter与getter的方法名

2,readwrite,readonly,设置可供访问级别

2,assign,setter方法直接赋值,不进行任何retain操作,为了解决原类型与环循引用问题

3,retain,setter方法对参数进行release旧值再retain新值,所有实现都是这个顺序(CC上有相关资料)

4,copy,setter方法进行Copy操作,与retain处理流程一样,先旧值release,再Copy出新的对象,retainCount为1。这是为了减少对上下文的依赖而引入的机制。copy是在你不希望a和b共享一块内存时会使用到。a和b各自有自己的内存。

5,nonatomic,非原子性访问,不加同步,多线程并发访问会提高性能。注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。锁被加到所属对象实例级(我是这么理解的...)。

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。

19"NSMutableString *"这个数据类型则是代表"NSMutableString"对象本身,这两者是有区别的。

NSString只是对象的指针而已。

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。

面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。


作者:iOS鑫
链接:https://www.jianshu.com/p/48a5b53c63e8

收起阅读 »

iOS面试备战-网络篇

计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及并用到的知识点,可想而知它的重要性。所以它也成为了iOS面试中经常被问及的问题。准备面试的话,网络相关的知识点一定不能错过。这里总结了一些我认为有用的和最近面试遇到的网络相关知识点。计算机...
继续阅读 »
计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及并用到的知识点,可想而知它的重要性。所以它也成为了iOS面试中经常被问及的问题。准备面试的话,网络相关的知识点一定不能错过。这里总结了一些我认为有用的和最近面试遇到的网络相关知识点。


计算机网络是如何分层的

网络有两种分层模型,一种是ISO(国际标准化组织)制定的OSI(Open System Interconnect)模型,它将网络分为七层。一种是TCP/IP的四层网络模型。OSI是一种学术上的国际标准,理想概念,TCP/IP是事实上的国际标准,被广泛应用于现实生活中。两者的关系可以看这个图:




注:也有说五层模型的,它跟四层模型的区别就是,在OSI模型中的数据链路层和物理层,前者将其作为两层,后者将其合并为一层称为网络接口层。一般作为面试题的话都是需要讲出OSI七层模型的。

各个分层的含义以及它们之间的关系用这张图表示:



Http协议

http协议特性

  • HTTP 协议构建于 TCP/IP 协议之上,是一个应用层协议,默认端口号是 80
  • 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
  • 无状态:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。
  • 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传。

请求方法

  • GET:请求获取Request-URI标识的资源,请求参数附加在url上,明文展示。

  • POST:在Request-URI所标识的资源后附加新的数据,常用于修改服务器资源或者提交资源到服务器。POST请求体是放到body中的,可以指定编码方式,更加安全。

  • HEAD:请求获取由Request-URI所标识的资源的响应消息报头。

  • PUT:请求服务器存储一个资源,并用Request-URI作为其标识。

  • DELETE:请求服务器删除Request-URI所标识的资源。

  • TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断。

  • OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求。

请求和响应报文

在Chrome查看其请求的Headers信息。

General

这里标记了请求的URL,请求方法为GET。状态码为304,代表文件未修改,可以直接使用缓存的文件。远程地址为185.199.111.153:443,此IP为Github 服务器地址,是因为我的博客是部署在GitHub上的。

除了304还有别的状态码,分别是:

  • 200 OK 客户端请求成功
  • 301 Moved Permanently 请求永久重定向
  • 302 Moved Temporarily 请求临时重定向
  • 304 Not Modified 文件未修改,可以直接使用缓存的文件。
  • 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
  • 401 Unauthorized 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因
  • 404 Not Found 请求的资源不存在,例如,输入了错误的URL
  • 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
  • 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。

Response Headers


content-encoding:用于指定压缩算法

content-length:资源的大小,以十进制字节数表示。

content-type:指示资源的媒体类型。图中所示内容类型为html的文本类型,文字编码方式为utf-8

last-modified:上次内容修改的日期,为6月8号

status:304 文件未修改状态码

注:其中content-type在响应头中代表,需要解析的格式。在请求头中代表上传到服务器的内容格式。

Request Headers




method:GET请求

:path:url路径

:scheme:https请求

accept:通知服务器可以返回的数据类型。

accept-encoding:编码算法,通常是压缩算法,可用于发送回的资源

accept-language:通知服务器预期发送回的语言类型。这是一个提示,并不一定由用户完全控制:服务器应该始终注意不要覆盖用户的显式选择(比如从下拉列表中选择语言)。

cookie:浏览器cookie

user-agent:用户代理,标记系统和浏览器内核

TCP三次握手和四次挥手的过程以及为什么要有三次和四次

在了解TCP握手之前我们先看下TCP的报文样式:

TCP三次握手

示意图如下:



三次握手是指建立一个TCP连接时,需要客户端和服务器总共发送3个数据包。

1、第一次握手(SYN=1, seq=x)

客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。

发送完毕后,客户端进入 SYN_SEND 状态。

2、第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1)

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

3、第三次握手(ACK=1, ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

问题一:为什么需要三次握手呢?

在谢希仁著的《计算机网络》里说,『为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误』。怎么理解呢,我们假设一种情况,有一个建立连接的第一次握手的报文段因为滞留到网络中过了较长时间才发送到服务端。这时服务器是要做ACK应答的,如果只有两次握手就代表连接建立,那服务器此时就要等待客户端发送建立连接之后的数据。而这只是一个因滞留而废弃的请求,是不是白白浪费了很多服务器资源。

从另一个角度看这个问题,TCP是全双工的通信模式,需要保证两端都已经建立可靠有效的连接。在三次握手过程中,我们可以确认的状态是:

第一次握手:服务器确认自己接收OK,服务端确认客户端发送OK。

第二次握手:客户端确认自己发送OK,客户端确认自己接收OK,客户端确认服务器发送OK,客户端确认服务器接收OK。

第三次握手:服务器确认自己发送OK,服务器确认客户端接收OK。

只有握手三次才能达到全双工的目的:确认自己和对方都能够接收和发送消息。

TCP四次挥手

示意图如下:


四次挥手表示要发送四个包,挥手的目的是断开连接。

1、第一次挥手(FIN=1, seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。

发送完毕后,客户端进入 FIN_WAIT_1 状态。

2、第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

3、第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

4、第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。

问题一:为什么挥手需要四次呢?为什么不能将ACK和FIN报文一起发送?

当服务器收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端『你发的FIN我收到了』。只有等到服务端所有的报文都发送完了,才能发FIN报文,所以要将ACK和FIN分开发送,这就导致需要四次挥手。

问题二:为什么TIMED_WAIT之后要等2MSL才进入CLOSED状态?

MSL是TCP报文的最大生命周期,因为TIME_WAIT持续在2MSL就可以保证在两个传输方向上的尚未接收到或者迟到的报文段已经消失,同时也是在理论上保证最后一个报文可靠到达。假设最后一个ACK丢失,那么服务器会再重发一个FIN,这是虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK。

HTTPS的流程

HTTPS = HTTP + TLS/SSL,它使用的端口默认为443,它的建立可以用下图表示:



1、客户端首次请求服务器,告诉服务器自己支持的协议版本,支持的加密算法及压缩算法,并生成一个随机数(client random)告知服务器。

2、服务器确认双方使用的加密方法,并返回给客户端证书以及一个服务器生成的随机数(server random)

3、客户端收到证书后,首先验证证书的有效性,然后生成一个新的随机数(premaster secret),并使用数字证书中的公钥,加密这个随机数,发送给服务器。

4、服务器接收到加密后的随机数后,使用私钥进行解密,获取这个随机数(premaster secret

5、服务器和客户端根据约定的加密方法,使用前面的三个随机数(client random, server random, premaster secret),生成『对话密钥』(session key),用来加密接下来的整个对话过程(对称加密)。

问题一:为什么握手过程需要三个随机数,而且安全性只取决于第三个随机数?

前两个随机数是明文传输,存在被拦截的风险,第三个随机数是通过证书公钥加密的,只有它是经过加密的,所以它保证了整个流程的安全性。前两个随机数的目的是为了保证最终对话密钥的『更加随机性』。

问题二:Charles如何实现HTTPS的拦截?

Charles要实现对https的拦截,需要在客户端安装Charles的证书并信任它,然后Charles扮演中间人,在客户端面前充当服务器,在服务器面前充当客户端。

问题三:为什么有些HTTPS请求(例如微信)抓包结果仍是加密的,如何实现的?


我在聊天过程中并没有抓到会话的请求,在小程序启动的时候到是抓到了一个加密内容。我手动触发该链接会下载一个加密文件,我猜测这种加密是内容层面的加密,它的解密是由客户端完成的,而不是在HTTPS建立过程完成的。

另外在研究这个问题的过程中,又发现了一些有趣的问题:

1、图中所示的三个https请求分别对应三个不同类型的图标,它们分别代表什么意思呢?

感谢iOS憨憨的回答。 第一个图标含义是HTTP/2.0,第二个图标含义是HTTP/1.1,第三个图标加锁是因为我用charles只抓取了443端口的请求,该请求端口为5228,所以不可访问。

2、第三个请求https://mtalk.google.com:5228图标和请求内容都加了锁,这个加锁是在https之上又加了一层锁吗?

这些问题暂时没有确切的答案,希望了解的小伙伴告知一下哈。

DNS解析流程

DNS(Domain name system)域名系统。DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户通过域名访问到对应的服务器(IP地址)。具体的解析流程是这样的:

1、浏览器中输入想要访问的网站域名,操作系统会检查本地hosts文件是否有这个网址的映射关系,如果有就调用这个IP地址映射,完成域名解析。没有的话就走第二步。

2、客户端回向本地DNS服务器发起查询,如果本地DNS服务器收到请求,并可以在本地配置区域资源中查到该域名,就将对应结果返回为给客户端。如果没有就走第三步。

3、根据本地DNS服务器的设置,采用递归或者迭代查询,直至解析完成。

其中递归查询和迭代查询可以用如下两图表示。

递归查询

如图所示,递归查询是由DNS服务器一级一级查询传递的。


迭代查询

如果所示,迭代查询是找到指定DNS服务器,由客户端发起查询。



DNS劫持

DNS劫持发生在DNS服务器上,当客户端请求解析域名时将其导向错误的服务器(IP)地址。

常见的解决办法是使用自己的解析服务器或者是将域名以IP地址的方式发出去以绕过DNS解析。

Cookie和Session的区别

HTTP 是无状态协议,说明它不能以状态来区分和管理请求和响应。也就是说,服务器单从网络连接上无从知道客户身份。

可是怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。

  • Cookie:Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,实际上Cookie是服务器在本地机器上存储的一小段文本,并随着每次请求发送到服务器。Cookie技术通过请求和响应报文中写入Cookie信息来控制客户端的状态。

  • Session:Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。当有用户请求创建一个session时,服务器会先检查这个客户端里是否已经包含了一个Session标识(session id),如果有就通过session id把session检索出来。如果没有就创建一个对应此Session的session id。这个session id会在本次响应中返回给客户端。

两者有以下区别:

1、存储位置:Cookie存放在客户端上,Session数据存放在服务器上。

2、Session 的运行依赖 session id,而 session id 是存在 Cookie 中的,也就是说,如果浏览器禁用了 Cookie ,同时 Session 也会失效

3、安全性:Cookie存在浏览器中,可能会被一些程序复制,篡改;而Session存在服务器相对安全很多。

4、性能:Session会在一定时间内保存在服务器上,当访问增多,会对服务器造成一定的压力。考虑到减轻服务器压力,应当使用Cookie

CDN是干什么用的

CDN(Content Delivery Network),根本作用是将网站的内容发布到最接近用户的网络『边缘』,以提高用户访问速度。概括的来说:CDN = 镜像(Mirror) + 缓存(Cache) + 整体负载均衡(GSLB)。

目前CDN都以缓存网站中的静态数据为主,如CSS、JS、图片和静态网页等数据。用户在从主站服务器请求到动态内容后再从CDN上下载这些静态数据,从而加速网页数据内容的下载速度,如淘宝有90%以上的数据都是由CDN来提供的。

CDN工作流程

一个用户访问某个静态文件(如CSS),这个静态文件的域名假如是http://www.baidu.com,而这个域名最终会被指向CDN全局中CDN负载均衡服务器,再由这个负载均衡服务器来最终分配是哪个地方的访问用户,返回给离这个访问用户最近的CDN节点。之后用户就直接去这个CDN节点访问这个静态文件了,如果这个节点中请求的文件不存在,就会再回到源站去获取这个文件,然后再返回给用户。


Socket的作用

socket位于应用层和传输层之间:


它的作用是为了应用层能够更方便的将数据经由传输层来传输。所以它的本质就是对TCP/IP的封装,然后应用程序直接调用socket API即可进行通信。上文中说的三次握手和四次挥手即是通过socket完成的。

我们可以从iOS中网络库分层找到BSD Sockets,它是位于CFNetwork之下。在CFNetwork中还有一个CFSocket,推测是对BSD Sockets的封装。


WebRTC是干什么用的

WebRTC

是一个可以用在视频聊天,音频聊天或P2P文件分享等Web App中的 API。借助WebRTC,你可以在基于开放标准的应用程序中添加实时通信功能。它支持在同级之间发送视频,语音和通用数据,从而使开发人员能够构建功能强大的语音和视频通信解决方案。该技术可在所有现代浏览器以及所有主要平台的本机客户端上使用。WebRTC项目是开源的并得到Apple,Google,Microsoft和Mozilla等的支持。

如果某一请求只在某一地特定时刻失败率较高,会有哪些原因

这个是某公司二面时的问题,是一个开放性问题,我总结了以下几点可能:

1、该时刻请求量过大

2、该地的网络节点较不稳定

3、用户行为习惯,比如该时刻为上班高峰期,或者某个群体的特定习惯



作者:iOS鑫
链接:https://www.jianshu.com/p/6b16f40d7354



收起阅读 »

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。

Lobster一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!一、功能介绍1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例; 2.组件中...
继续阅读 »


Lobster

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!

一、功能介绍

1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例;
2.组件中自己创建的Application生命周期方法伴随壳子工程Application生命周期调用而调用;
3.组件中自己创建的Application可以配置优先级,用于优先或延后执行。

二、应用场景

组件化框架中,各组件有时需要持有Application的实例,但很多做法是在公共库中创建BaseApplication,  
让壳子工程的Application去继承BaseApplication,进而组件去持有BaseApplication的实例达到使用的目的,
然而这样会加剧组件对公共库的过分依赖,项目较大时,就会造成一定的耦合,可能会出现改一处而动全身的场景。
因此,在组件化当中,各组件应该像一个应用一样维护一个自己的Application,使用时拿的是自己Application的实例,
与其他组件隔离,也与公共库隔离,降低耦合!

三、使用方式

1.需要在壳子工程和其他module中添加如下依赖:

android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [LOBSTER_MODULE_NAME: project.getName()]
}
}
}
}

dependencies {
implementation project(path: ':lobster-annotation')
annotationProcessor project(path: ':lobster-compiler')
...
}

2.在壳子工程和其他Module中的Application中添加注解: ShellApp注解作用于壳子工程(主工程)Application,一般来说只有一个,ModuleApp注解作用于组件Application,可以设置优先级,AppInstance注解作用于组件Application的实例。

// 壳子工程的Application
@ShellApp
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
}
}
// 其他Module的Application
@ModuleApp(priority = 1)
public class Module1App extends Application {
private static final String TAG = "Lobster";

@AppInstance
public static Application mApplication1;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module1App->onCreate");
}
}
// 其他Module的Application
@ModuleApp(priority = 2)
public class Module2App extends Application {
private static final String TAG = "Lobster";
@AppInstance
public static Application mApplication2;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module2App->onCreate");
Toast.makeText(mApplication2, "I come from Module2App", Toast.LENGTH_SHORT).show();
}
}

3.没有了,可以开始耍了!

代码下载:Lobster.zip

收起阅读 »

Android自定义搜索控件 KSearchView

KSearchView自定义搜索控件布局示例代码 <com.kacent.widget.view.KingSearchView android:id="@+id/search_view" android:layout_wi...
继续阅读 »

KSearchView

自定义搜索控件


布局示例代码

 <com.kacent.widget.view.KingSearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
<!-提示文本->
app:hint_text="输入搜索内容"
<!-icon的padding->
app:icon_padding_bottom="5dp"
<!-searchView输入框的padding->
app:search_padding_bottom="10dp"
app:search_padding_start="30dp"
app:search_padding_top="10dp"
<!-searchView背景设置->
app:search_view_background="@drawable/my_search_shape"
app:text_size="8sp" />

设置搜索监听器

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val searchView = findViewById<KingSearchView>(R.id.search_view)
searchView.setQueryListener(object : KingSearchView.OnQueryListener {
override fun onQuery(value: String) {
if (TextUtils.isEmpty(value)) {
Toast.makeText(this@MainActivity, "没有输入相关搜索内容", Toast.LENGTH_SHORT).show()
}
Log.e("搜索内容", value)
}
})
}
}


代码下载:KingSearchView-master.zip

收起阅读 »

Android基础到进阶UI爸爸级TextView介绍+实例

TextView是什么 向用户显示文本,并可选择允许他们编辑文本。TextView是一个完整的文本编辑器,但是基类为不允许编辑;其子类EditText允许文本编辑。 咱们先上一个图看看TextView的继承关系: 从上图可以看出TxtView继承了Vi...
继续阅读 »

TextView是什么


向用户显示文本,并可选择允许他们编辑文本。TextView是一个完整的文本编辑器,但是基类为不允许编辑;其子类EditText允许文本编辑。


咱们先上一个图看看TextView的继承关系:


从上图可以看出TxtView继承了View,它还是Button、EditText等多个组件类的父类。咱们看看这些子类是干嘛的。



  • Button:用户可以点击或单击以执行操作的用户界面元素。

  • CheckedTextView:TextView支持Checkable界面和显示的扩展。

  • Chronometer:实现简单计时器的类。

  • DigitalClock:API17已弃用可用TextClock替代。

  • EditText:用于输入和修改文本的用户界面元素。

  • TextClock:可以将当前日期和/或时间显示为格式化字符串。


看看他的儿子都这么牛掰,何况是爸爸,今天咱就看看这个爸爸级组件:TextView


使用TextView


1.在xml中创建并设置属性



咱们看上图说话。上图的文字显示多种多样,但是也仅包含TextView的部分功能,看看这多种多样的显示也是比较有意思的。


下面咱看看代码实践:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/dimen_20"
    android:orientation="vertical">

    <!--在Design中表示可从左侧控件展示处拖拽至布局文件上,创建简单一个TextView。-->
    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView" />

    <!--修改颜色、大小-->
    <!--设置颜色 @color/color_ff0000位置:app/values/colors-->
    <!--设置大小 @dimen/text_size_18位置:app/values/dimens-->
    <!--设置内容 @string/str_setting_color_size位置:app/values/strings-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/str_setting_color_size"
        android:layout_marginTop="@dimen/dimen_10"
        android:textColor="@color/color_ff0000"
        android:textSize="@dimen/text_size_20" />

    <!--添加图片和使用阴影-->
    <!--添加图片:drawableTop、drawableBottom、drawableLeft(drawableStart)、drawableRight(drawableEnd)-->
    <!--使用阴影:shadowColor(阴影颜色)、shadowDx(tv_2位置为基准,数字越大越往右)、
    shadowDy(tv_2位置为基准,数字越大越往下)、shadowRadius(数字越大越模糊)-->

    <!--图片 @mipmap/ic_launcher 位置:app/mipmap/任意一个目录能找到即可-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@mipmap/ic_launcher"
        android:layout_marginTop="@dimen/dimen_10"
        android:gravity="center_vertical"
        android:shadowColor="@color/color_FF773D"
        android:shadowDx="30"
        android:shadowDy="-20"
        android:shadowRadius="2"
        android:text="右侧添加图片和使用阴影"
        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_20" />

    <!--对电话和邮件增加链接-->
    <!--autoLink对文本内容自动添加E-mail地址、电话号码添加超级链接-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autoLink="email|phone"
        android:gravity="center_vertical"
        android:layout_marginTop="@dimen/dimen_10"
        android:text="可点击跳转邮件:SCC5201314@qq.com\n可点击跳转电话:0215201314"
        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_14" />

    <!--内容过多-->
    <!--maxLength最多显示几行,单行也可用android:singleline="true"-->
    <!--ellipsize,内容显示不下时,显示...(位置最前、中间、最后都可以),这里要加行数限制才行-->
    <!--lineSpacingMultiplier,行距-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:gravity="center_vertical"
        android:lineSpacingMultiplier="1.2"
        android:layout_marginTop="@dimen/dimen_10"
        android:maxLength="2"
        android:text="TxtView继承了View,它还是Button、EditText两个UI组件类的父类。它的作用是在用户界面上显示文本素。从功能上来看TextView就是个文本编辑器,只不过Android关闭的它的可编辑功能。如果需要一个可编辑的文本框,就要使用到它的子类Editext了,Editext允许用户编辑文本框中的内容。TextView和Editext它俩最大的区别就在于TextView不允许用户编辑文本内容,Editext允许用户编辑文本内容。
下面咱写几个实例来详细了解一下TextView的。"

        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_14" />

    <!--background设置背景色-->
    <!--padding内边距(边到可用范围的距离)-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/color_ff0000"
        android:layout_marginTop="@dimen/dimen_10"
        android:padding="10dp"
        android:text="背景色红色的文本"
        android:textColor="@color/white" />


    <!--带边框的文本-->
    <!--layout_margin外边距(TextView到其他控件的距离)-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimen_10"
        android:background="@drawable/bg_tv_frame_red"
        android:padding="10dp"
        android:text="带着红色边框的文本" />

    <!--带边框的文本背景色渐变-->
    <!--代码可实现文本的渐变-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimen_10"
        android:background="@drawable/bg_tv_frame_gradient"
        android:padding="10dp"
        android:textColor="@color/white"
        android:text="带着边框和背景色渐变的文本" />

    
</LinearLayout>

background设置边框的文件 android:background="@drawable/bg_tv_frame_red"


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!--radius四个圆角统一设置,也可以单独对某一个圆角设置。例:topLeftRadius-->
    <corners android:radius="2dp"/>
    <!--边框宽度width、颜色color-->
    <stroke android:width="4px" android:color="@color/color_ff0000" />
</shape>

带着边框和背景色渐变 android:background="@drawable/bg_tv_frame_gradient"


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!--radius四个圆角统一设置,也可以单独对某一个圆角设置。例:topLeftRadius-->
    <corners android:radius="8dp"/>
    <!--边框宽度width、颜色color-->
    <stroke android:width="1dp" android:color="@color/color_ff0000" />
    <!--渐变的颜色设置开始到结束-->
    <gradient
        android:startColor="@color/color_188FFF"
        android:centerColor="@color/color_FF773D"
        android:endColor="@color/color_ff0000"
        android:type="linear"
        />

</shape>

2.在xml中创建,在代码中设置属性




  • 布局文件


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_margin="@dimen/dimen_20"
   android:orientation="vertical">
   <TextView
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="下面是用代码实现效果"
       android:textSize="@dimen/text_size_18"
       android:layout_marginTop="@dimen/dimen_20"
       android:layout_marginBottom="@dimen/dimen_10"
       android:textColor="@color/black"
       android:textStyle="bold" />

   <TextView
       android:id="@+id/tv_flag"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textColor="@color/color_188FFF"
       android:layout_marginTop="@dimen/dimen_10"
       android:text="给文本加划线"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_gradient"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:textColor="@color/white"
       android:text="文字渐变是不是很神奇"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_bg"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:padding="10dp"
       android:text="设置背景色"
       android:textColor="@color/white"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_size"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:textColor="@color/color_ff0000"
       android:text="文字特别大小不一致" />

   <TextView
       android:id="@+id/tv_onclick"
       android:layout_width="match_parent"
       android:layout_marginTop="@dimen/dimen_10"
       android:layout_height="wrap_content"
       android:textSize="@dimen/dimen_20"
       android:text="可点击可长按" />
</LinearLayout>


  • 运行结果




  • 在代码中实现


        //下划线并加清晰
        tv_flag.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
        tv_flag.getPaint().setAntiAlias(true);//抗锯齿

        int[] colors = {0xff188fff0xffff773D0xffff0000};//颜色的数组
        LinearGradient mLinearGradient = new LinearGradient(000
                tv_gradient.getPaint().getTextSize(), colors, null, Shader.TileMode.CLAMP);
        tv_gradient.getPaint().setShader(mLinearGradient);
        tv_gradient.invalidate();

        int fillColor = Color.parseColor("#ff0000");//内部填充颜色
        GradientDrawable gd = new GradientDrawable();//创建drawable
        gd.setColor(fillColor);//设置背景色
        gd.setCornerRadius(10);//设置圆角
        tv_bg.setBackground(gd);//设置背景

        Spannable wordtoSpan = new SpannableString(tv_size.getText().toString());
        //setSpan:参数1,设置文字大小;参数2,开始的文字位置;参数3,结束改变文字位置不包含这个位置
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this18)), 02, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this24)), 25, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this10)), 5, tv_size.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv_size.setText(wordtoSpan);

        //TextView其实也是有点击事件的毕竟它的爸爸Veiew
        tv_onclick.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MLog.e("这里是点击事件");
                Toast.makeText(TextViewActivity.this,"这里是点击事件",Toast.LENGTH_SHORT).show();
            }
        });
        tv_onclick.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                MLog.e("这里长按事件");
                Toast.makeText(TextViewActivity.this,"这里长按事件",Toast.LENGTH_SHORT).show();
                //true表示事件已消费
                return true;
            }
        });


  • 运行结果分析



    • TextView的属性在xml中可以使用的大部分在代码中也是可以实现的,看个人喜好怎么去使用。

    • 因TextView继承View,所以可以使用View的方法。如View.OnClickListener()和View.OnLongClickListener()还有去慢慢探索吧。



3.在代码中创建并设置属性



  • 先看效果图:




  • 下面是实现所用的代码:


  //ll_act_tv布局文件根布局id
  LinearLayout ll_act_tv = findViewById(R.id.ll_act_tv);
  TextView textView = new TextView(this);//创建控件
  textView.setText("蠢代码写的哦");//设置控件内容
  textView.setTextColor(Color.RED);//设置控件颜色
  textView.setTextSize(DensityUtil.dip2px(this20));//设置控件字体大小
  ll_act_tv.addView(textView);

TextView今天就聊到这里,后面还有它的子类,比较子类也是比较厉害的不可能一文搞定。你学会了吗?嘿嘿嘿


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

Android-自定义拼图验证码

废话不多说,先上图: 从效果图开始"临摹" 分析 从上面的效果图中,我们可以很直观的看出一共包含三个元素:背景图、空缺部分、填充部分,需要注意的是: 1. 空缺部分缺失的图片刚好是填充部分 2. 我们把填充部分位置固定在左侧,而随机生成空缺部分在右侧...
继续阅读 »

废话不多说,先上图:


1.gif


从效果图开始"临摹"


分析


从上面的效果图中,我们可以很直观的看出一共包含三个元素:背景图、空缺部分、填充部分,需要注意的是:
1. 空缺部分缺失的图片刚好是填充部分
2. 我们把填充部分位置固定在左侧,而随机生成空缺部分在右侧,增加验证难度


思路



  1. 准备背景图片,通过canvas.drawBitmap() 方法画出背景图

  2. 计算View宽高,随机生成空缺部分的x坐标在(width/3, width)范围,固定填充部分的x左边在(0,width/3)范围内,保证填充部分和空缺部分在初始化时没有重叠。(不严谨,具体数值还要结合空缺部分/填充部分尺寸详细计算,仅提供思路)。

  3. 先随机生成空缺部分,然后根据空缺部分在原来Bitmap上的左边生成一样大小一样形状的图片,用于填充部分。

  4. 然后重写onTouchEvent方法,处理拖动时填充部分的位移,在MotionEvent.ACTION_UP条件下,计算填充部分和空缺部分在画布中的x坐标差值,判断当差值小于阙值 dx 时,则认为通过验证,否则调用 invalidate() 方法重新生成验证码。


主要代码分析


这里重写了onMeasure方法,根据我们准备的原图片尺寸设置View宽高,并且重新生成和View一样尺寸的背景图newBgBitmap,统一尺寸以便后面我们对左边的转化。(这里曾经有些地方参照画布尺寸计算,有些地方参照背景图bitmap尺寸计算,导致填充部分和空缺部分没有吻合)。


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int minimumWidth = getSuggestedMinimumWidth();
/*根据原背景图宽高比设置画布尺寸*/
width = measureSize(minimumWidth, widthMeasureSpec);
float scale = width / (float) bgBitmap.getWidth();
height = (int) (bgBitmap.getHeight() * scale);
setMeasuredDimension(width, height);

/*根据画布尺寸生成相同尺寸的背景图*/
newBgBitmap = clipBitmap(bgBitmap, width, height);
/*根据新的背景图生成填充部分*/
srcBitmap = createSmallBitmap(newBgBitmap);
}


设置画笔的混合模式,生成一张自定义形状的图片供填充部分使用


    public Bitmap createSmallBitmap(Bitmap var) {
Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
/*设置混合模式*/
paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

/*在指定范围随机生成空缺部分坐标,保证空缺部分出现在View右侧*/
int min = width / 3;
int max = width - shadowSize / 2 - padding;
Random random = new Random();
shadowLeft = random.nextInt(max) % (max - min + 1) + min;
Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
canvas1.drawBitmap(var, rect, rectF, paintSrc);
paintSrc.setXfermode(null);
return bitmap;
}

在onDraw()方法中依次画出背景图、空缺部分、填充部分,注意先后顺序(具体细节自行处理,例如阴影、凹凸感等等)


    @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, width, height);
/*画背景图*/
canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

bgPaint.setColor(Color.parseColor("#000000"));
/*画空缺部分周围阴影*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画空缺部分*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

bgPaint.setColor(Color.parseColor("#FFFFFF"));
/*画填充部分周围阴影*/
canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画填充部分*/
canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
}

草纸代码参考


随写随发布?


package com.example.qingfengwei.myapplication;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

import java.util.Random;


public class SlidingVerificationView extends View {

private Bitmap bgBitmap;
private Bitmap newBgBitmap;
private Bitmap srcBitmap;

private Paint paintShadow;
private Paint paintSrc;
private float curX;
private float lastX;

private int dx;
private int shadowSize = dp2px(60);
private int padding = dp2px(40);
private int shadowLeft;
private int srcLeft = padding;

private int width, height;

private Paint bgPaint;

private OnVerifyListener listener;

public SlidingVerificationView(Context context) {
this(context, null);
}

public SlidingVerificationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public SlidingVerificationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paintShadow = new Paint();
paintShadow.setAntiAlias(true);
paintShadow.setColor(Color.parseColor("#AA000000"));


paintSrc = new Paint();
paintSrc.setAntiAlias(true);
paintSrc.setFilterBitmap(true);
paintSrc.setStyle(Paint.Style.FILL_AND_STROKE);
paintSrc.setColor(Color.WHITE);

bgPaint = new Paint();
bgPaint.setMaskFilter(new BlurMaskFilter(5, BlurMaskFilter.Blur.OUTER));
bgPaint.setAntiAlias(true);
bgPaint.setStyle(Paint.Style.FILL);

bgBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.syzt);
}

public void setVerifyListener(OnVerifyListener listener) {
this.listener = listener;
}

public Bitmap clipBitmap(Bitmap bm, int newWidth, int newHeight) {
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
}


public Bitmap createSmallBitmap(Bitmap var) {
Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
/*设置混合模式*/
paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));


/*在指定范围随机生成空缺部分坐标,保证空缺部分出现在View右侧*/
int min = width / 3;
int max = width - shadowSize / 2 - padding;
Random random = new Random();
shadowLeft = random.nextInt(max) % (max - min + 1) + min;
Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
canvas1.drawBitmap(var, rect, rectF, paintSrc);
paintSrc.setXfermode(null);
return bitmap;
}


@Override
public boolean onTouchEvent(MotionEvent event) {
curX = event.getRawX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
dx = (int) (curX - lastX);
srcLeft = dx + padding;
invalidate();
break;
case MotionEvent.ACTION_UP:

boolean isSuccess = Math.abs(srcLeft - shadowLeft) < 8;

if (isSuccess) {
Toast.makeText(getContext(), "验证成功!", Toast.LENGTH_SHORT).show();
Log.d("w", "check success!");
} else {
Toast.makeText(getContext(), "验证失败!", Toast.LENGTH_SHORT).show();
Log.d("w", "check fail!");
srcBitmap = createSmallBitmap(newBgBitmap);
srcLeft = padding;
invalidate();
}

if (listener != null) {
listener.onResult(isSuccess);
}
break;
}

return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int minimumWidth = getSuggestedMinimumWidth();
/*根据原背景图宽高比设置画布尺寸*/
width = measureSize(minimumWidth, widthMeasureSpec);
float scale = width / (float) bgBitmap.getWidth();
height = (int) (bgBitmap.getHeight() * scale);
setMeasuredDimension(width, height);

/*根据画布尺寸生成相同尺寸的背景图*/
newBgBitmap = clipBitmap(bgBitmap, width, height);
/*根据新的背景图生成填充部分*/
srcBitmap = createSmallBitmap(newBgBitmap);

}

private int measureSize(int defaultSize, int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
int result = defaultSize;
switch (mode) {
case MeasureSpec.UNSPECIFIED:
result = defaultSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = size;
break;
}
return result;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, width, height);
/*画背景图*/
canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

bgPaint.setColor(Color.parseColor("#000000"));
/*画空缺部分周围阴影*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画空缺部分*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

bgPaint.setColor(Color.parseColor("#FFFFFF"));
/*画填充部分周围阴影*/
canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画填充部分*/
canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
}

public static int dp2px(float dp) {
float density = Resources.getSystem().getDisplayMetrics().density;
return (int) (density * dp + 0.5f);
}
}



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

手把手教你搭建AndroidJenkins环境及一键自动构建打包

前言: 之前看到后端的同事,在服务器上刷刷的敲命令觉得很酷,给我的感觉是Linux很难上手。自从公司给我配了台服务器后,操作了一下,感觉和想的不是那么一回事,所以还是得多动手,不要空想。 正文开始: 看到网上说Linux安装软件基本是用yum,所以这边也是...
继续阅读 »

前言: 之前看到后端的同事,在服务器上刷刷的敲命令觉得很酷,给我的感觉是Linux很难上手。自从公司给我配了台服务器后,操作了一下,感觉和想的不是那么一回事,所以还是得多动手,不要空想。


正文开始:


看到网上说Linux安装软件基本是用yum,所以这边也是使用yum,从0-1搭建Linux Jenkins,实现Android 一键自动化构建部署打包。


步骤一:安装JDK环境


1.查看jdk的版本:


# yum -y list java*


2.选择要安装的JDK版本(带devel是JDK)


# yum install -y java-1.8.0-openjdk-devel.x86_64


3.安装完后,查看是否安装成功(看到版本号表示安装成功,不需要像Windows配置java_home环境变量)


# java -version


image.png


步骤二:安装Jenkins


1.yum安装Jenkins


# yum install jenkins


如果没有Jenkins的repos,按照官网提示再操作:


# sudo wget -O /etc/yum.repos.d/jenkins.repo  https://pkg.jenkins.io/redhat-stable/jenkins.repo
# sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

2.修改Jenkins默认的配置(怕端口冲突,改成你想要名字和端口):


# cd /etc/sysconfig/
# vi jenkins

image.png


3.启动Jenkins


# service jenkins start


按照提示,到指定目录复制密码,下一步是选择安装默认的插件(推荐),注册一个账户,最后就会来到这个界面,表示Jenkins安装成功。


image.png


步骤三:安装gradle


1.安装gradle gradle.org/releases/


image.png 拿到安装包的下载链接: 创建一个安装的目录,我在新建了一个文件夹/opt/gradle/下 下载


# cd /opt
# mkdir gradle
# cd /opt/gradle
# wget https://downloads.gradle-dn.com/distributions/gradle-6.5-all.zip
# unzip /opt/gradle gradle-6.5-all.zip

步骤四:command tools 下载



  1. Command line tools only linux版本


developer.android.com/studio


# cd /opt
# mkdir android
# cd /opt/android
# wget https://dl.google.com/android/repository/commandlinetools-linux-7302050_latest.zip
# unzip /opt/android commandlinetools-linux-7302050_latest.zip

步骤五:配置gradle和Android SDK 环境变量


# cd /etc
# /etc/profile

image.png 在最后追加环境变量,保存


export ANDROID_HOME="/opt/android"
export GRADLE_HOME="/opt/gradle"
export PATH="$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$GRADLE_HOME/gradle-4.9/bin:$PATH"


刷新当前的shell环境


# source /etc/profile


配置gradle全局代理


def repoConfig = {
all { ArtifactRepository repo ->
if (repo instanceof MavenArtifactRepository) {
def url = repo.url.toString()
if (url.contains('repo1.maven.org/maven2') || url.contains('jcenter.bintray.com')) {
println "gradle 初始化: (${repo.name}: ${repo.url}) 移除"
remove repo
}
}
}
maven { url 'https://maven.aliyun.com/repository/central' }
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
mavenLocal()
mavenCentral()
}

allprojects {
buildscript {
repositories repoConfig
}

repositories repoConfig
}

这里因为项目里面配了一个google的maven地址,导致一直gradle构建超半个小时,排查了很久。


项目builid.gradle里面千万不要配google的maven地址
项目builid.gradle里面千万不要配google的maven地址
项目builid.gradle里面千万不要配google的maven地址
就是他,浪费了我几天时间排查,正常用
google()
jcenter()
就可以了

image.png


查看gradle是否配置成功
# gradle -version

image.png


步骤六:sdkmanager方式安装SDK


因为是sdkmanager访问的是google,所以配置了谷歌代理才可以访问,当时折腾了很久才找到的dl.google.com代理


# cd /etc/
# vi hosts

203.208.40.36 dl.google.com


image.png


# cd /opt/android/cmdline-tools


在里面创建一个latest,将文件夹里面文件全部放到latest里面。


# cd /opt/android/cmdline-tools/latest/bin


查看版本
# ./sdkmanager --list
安装想要的SDK版本
# ./sdkmanager --install "platforms;android-29"
# ./sdkmanager --install "build-tools;29.0.2"

步骤六:安装git


1.下载最新的git github.com/git/git/rel…


2.我通过xftp copy在/root目录下,解压


安装依赖库
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker

# cd /root/git-2.32.0
make prefix=/usr/local all
make prefix=/usr/local install

最后测试下git clone 你的git地址试试能不能拉代码

步骤七:最后一步,Jenkins上创建项目


1.在plugins.jenkins.io/ 下载插件


构建化插件:extended-choice-parameter


image.png


点击高级-上传插件-等待安装成功即可


image.png


2.新建任务


image.png


构建化参数添加参考


image.png


我这个教程,Jenkins里面是零配置,直接执行shell脚本,很方便


具体的脚本自己根据实际需求来编写,我这里就不举例了。


gradle clean
gradle assembleRelease--stacktrace -info

结语: 搭建Jenkins的服务,我深有体会,由于没有服务器,首先在自己电脑搭建了一套Windows的Jenkins,也是遇到了各种疑难问题,最后还是解决了。然后,在Linux上部署这套Jenkins环境,就变得很轻松。这次最大的收获就是熟悉了Linux的操作,实现了Android 一键自动打包上传到服务器,减少了打包,上传服务器繁琐的操作。


如果本文对你有帮助,请帮忙对个赞或者留言,你的鼓励是我分享的动力。




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

Android BaseDialog(开发必备)动画、加载进度、阴影

GitHubAPK使用方法将libray模块复制到项目中,或者直接在build.gradle中依赖:allprojects { repositories { maven { url 'https://jitpack.io' } } } ...
继续阅读 »

GitHub

APK

使用方法

将libray模块复制到项目中,或者直接在build.gradle中依赖:

allprojects {
repositories {

maven { url 'https://jitpack.io' }
}
}
dependencies {
compile 'com.github.AnJiaoDe:BaseDialog:V1.1.8'
}

1.Center


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:background="@drawable/white_shape"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="确定删除吗?"
android:textSize="16sp"
android:gravity="center"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/line"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp"

android:text="取消"
android:id="@+id/tv_cancel"
android:gravity="center"
android:textSize="16sp"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/line"/>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/tv_confirm"
android:layout_height="48dp"
android:text="确定"
android:gravity="center"

android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>

2.Left


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

3.Top


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

4.Right


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

5.Bottom


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

6.Progress


public class MainActivity extends BaseActivity {
private BaseDialog dialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_center).setOnClickListener(this);
findViewById(R.id.btn_left).setOnClickListener(this);
findViewById(R.id.btn_top).setOnClickListener(this);
findViewById(R.id.btn_right).setOnClickListener(this);
findViewById(R.id.btn_bottom).setOnClickListener(this);
findViewById(R.id.btn_progress).setOnClickListener(this);

}

@Override
public void onClick(View v) {

switch (v.getId()) {
case R.id.btn_center:
dialog = new BaseDialog(this);
dialog.contentView(R.layout.dialog_center)
.canceledOnTouchOutside(true).show();
dialog.findViewById(R.id.tv_confirm).setOnClickListener(this);
dialog.findViewById(R.id.tv_cancel).setOnClickListener(this);

break;
case R.id.btn_left:
BaseDialog dialog_left = new BaseDialog(this);

dialog_left.contentView(R.layout.dialog_left)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))
.dimAmount(0.5f)
.gravity(Gravity.LEFT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.LEFT)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_top:
BaseDialog dialog_top = new BaseDialog(this);

dialog_top.contentView(R.layout.dialog_photo)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
.dimAmount(0.5f)
.gravity(Gravity.TOP)
.offset(0, ScreenUtils.dpInt2px(this, 48))
.animType(BaseDialog.AnimInType.TOP)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_right:
BaseDialog dialog_right = new BaseDialog(this);

dialog_right.contentView(R.layout.dialog_right)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))

.gravity(Gravity.RIGHT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.RIGHT)
.offset(20, 0)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_bottom:
BaseDialog dialog_bottom = new BaseDialog(this);

dialog_bottom.contentView(R.layout.dialog_photo)
.gravity(Gravity.BOTTOM)
.animType(BaseDialog.AnimInType.BOTTOM)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_progress:

ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.color_iv(0xffffffff)
.color_bg_progress(0xffffffff)
.colors_progress(0xff2a5caa).show();
break;
case R.id.tv_confirm:
dialog.dismiss();
break;
case R.id.tv_cancel:
dialog.dismiss();
break;
}

}
}

源码:

BaseDialog

public class BaseDialog extends Dialog {

public BaseDialog(Context context) {
this(context, 0);

}


public BaseDialog(Context context, int themeResId) {
super(context, themeResId);

requestWindowFeature(Window.FEATURE_NO_TITLE);// 去除对话框的标题
GradientDrawable gradientDrawable = new GradientDrawable();
gradientDrawable.setColor(0x00000000);
getWindow().setBackgroundDrawable(gradientDrawable);//设置对话框边框背景,必须在代码中设置对话框背景,不然对话框背景是黑色的

dimAmount(0.2f);
}

public BaseDialog contentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
return this;
}


public BaseDialog contentView(@NonNull View view) {
getWindow().setContentView(view);
return this;
}

public BaseDialog contentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
return this;
}
public BaseDialog layoutParams(@Nullable ViewGroup.LayoutParams params) {
getWindow().setLayout(params.width, params.height);
return this;
}


/**
* 点击外面是否能dissmiss
*
* @param canceledOnTouchOutside
* @return
*/
public BaseDialog canceledOnTouchOutside(boolean canceledOnTouchOutside) {
setCanceledOnTouchOutside(canceledOnTouchOutside);
return this;
}

/**
* 位置
*
* @param gravity
* @return
*/
public BaseDialog gravity(int gravity) {

getWindow().setGravity(gravity);

return this;

}

/**
* 偏移
*
* @param x
* @param y
* @return
*/
public BaseDialog offset(int x, int y) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.x = x;
layoutParams.y = y;

return this;
}

/*
设置背景阴影,必须setContentView之后调用才生效
*/
public BaseDialog dimAmount(float dimAmount) {

WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.dimAmount = dimAmount;
return this;
}


/*
动画类型
*/
public BaseDialog animType(BaseDialog.AnimInType animInType) {


switch (animInType.getIntType()) {
case 0:
getWindow().setWindowAnimations(R.style.dialog_zoom);

break;
case 1:
getWindow().setWindowAnimations(R.style.dialog_anim_left);

break;
case 2:
getWindow().setWindowAnimations(R.style.dialog_anim_top);

break;
case 3:
getWindow().setWindowAnimations(R.style.dialog_anim_right);

break;
case 4:
getWindow().setWindowAnimations(R.style.dialog_anim_bottom);

break;
}
return this;
}


/*
动画类型
*/
public enum AnimInType {
CENTER(0),
LEFT(1),
TOP(2),
RIGHT(3),
BOTTOM(4);

AnimInType(int n) {
intType = n;
}

final int intType;

public int getIntType() {
return intType;
}
}
}

ProgressDialog

public class ProgressDialog extends BaseDialog {

private MaterialProgressDrawable progress;

private ValueAnimator valueAnimator;
private CircleImageView imageView;

public ProgressDialog(Context context) {
super(context);
setCanceledOnTouchOutside(false);

FrameLayout frameLayout = new FrameLayout(context);

imageView = new CircleImageView(context);

progress = new MaterialProgressDrawable(getContext(), imageView);


//设置圈圈的各种大小
progress.updateSizes(MaterialProgressDrawable.DEFAULT);

progress.showArrow(false);
imageView.setImageDrawable(progress);

frameLayout.addView(imageView);


valueAnimator = valueAnimator.ofFloat(0f, 1f);

valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float n = (float) animation.getAnimatedValue();
//圈圈的旋转角度
progress.setProgressRotation(n * 0.5f);
//圈圈周长,0f-1F
progress.setStartEndTrim(0f, n * 0.8f);
//箭头大小,0f-1F
progress.setArrowScale(n);
//透明度,0-255
progress.setAlpha((int) (255 * n));
}
});

getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
setContentView(frameLayout);

setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {

if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
hide();
return true;
}
return false;
}
});


}


public ProgressDialog duration(long duration) {
valueAnimator.setDuration(duration);

return this;
}


public ProgressDialog radius_iv(float radius_iv) {
imageView.radius(radius_iv);

return this;
}

public ProgressDialog color_iv(int color_iv) {
imageView.color(color_iv);

return this;
}

public ProgressDialog color_bg_progress(int color_bg_progress) {
progress.setBackgroundColor(color_bg_progress);

return this;
}

/**
* //圈圈颜色,可以是多种颜色
*
* @param colors_progress
* @return
*/
public ProgressDialog colors_progress(int... colors_progress) {
progress.setColorSchemeColors(colors_progress);

return this;
}

@Override
public void show() {
super.show();
if (progress == null) return;
progress.start();
if (valueAnimator == null) return;
valueAnimator.start();


}

@Override
public void hide() {
super.hide();
if (progress == null) return;
progress.stop();
if (valueAnimator == null) return;
valueAnimator.cancel();


}
}

参考:Android Dialog

GitHub

APK

收起阅读 »

一个Android文字展示动画框架:TextSurface

文字表面一个小动画框架,可以帮助您以漂亮的方式显示消息。用法创建TextSurface实例或将其添加到您的布局中。创建Text具有TextBuilder定义文本外观和位置的实例:Text textDaai = TextBuilder .create("Daa...
继续阅读 »

文字表面

一个小动画框架,可以帮助您以漂亮的方式显示消息。


用法

  1. 创建TextSurface实例或将其添加到您的布局中。
  2. 创建Text具有TextBuilder定义文本外观和位置的实例

Text textDaai = TextBuilder
.create("Daai")
.setSize(64)
.setAlpha(0)
.setColor(Color.WHITE)
.setPosition(Align.SURFACE_CENTER).build();

  1. 创建动画并将它们传递给TextSurface实例:

textSurface.play(
new Sequential(
Slide.showFrom(Side.TOP, textDaai, 500),
Delay.duration(500),
Alpha.hide(textDaai, 1500)
)
);


调整动画

  • 要按顺序播放动画,请使用 Sequential.java

  • 要同时播放动画,请使用 Parallel.java

  • 动画/效果可以这样组合:

    new Parallel(Alpha.show(textA, 500), ChangeColor.to(textA, 500, Color.RED))

    即文本的 alpha 和颜色将在 500 毫秒内同时更改

添加您自己的动画/效果

您可以扩展两个基本类来添加自定义动画:

Proguard 配置

该框架基于reflection广泛使用的标准 android 动画类为避免混淆,您需要排除框架的类:

-keep class su.levenetc.android.textsurface.** { *; }


下载

repositories {
maven { url "https://jitpack.io" }
}
//...
dependencies {
//...
compile 'com.github.elevenetc:textsurface:0.9.1'
}




github地址:https://github.com/elevenetc/TextSurface
下载地址:master.zip
收起阅读 »

基础篇 - 从构建层面看 import 和 require 的区别

前言 一切的一切,都是因为群里的一个问题 虽然说最近在做 webpack 相关的事情,但是也没有对着干问题做过相关的研究,网上很多文章包括 vue 都介绍了建议使用 import ,但是没有说为什么要使用 import,对于开发者来说,调用的方式是没有区别的...
继续阅读 »

前言


一切的一切,都是因为群里的一个问题


image.png


虽然说最近在做 webpack 相关的事情,但是也没有对着干问题做过相关的研究,网上很多文章包括 vue 都介绍了建议使用 import ,但是没有说为什么要使用 import,对于开发者来说,调用的方式是没有区别的,那么为什么 import 的包就要比 require 的包小呢


这里暂时就不说什么调用方式了,什么动态加载(require)、静态编译(import)的,这个网上都有,这篇文章就是分析一下为什么要用 import,而不用 require


正文


首先本地先基于 webpack 搭建一个环境只是为了测试,不需要搭建太复杂的内容


基础文件内容


// webpack.config.js
module.exports = {
mode: 'development',
entry: './src/index.js'
}

index.js 内添加两种调用方式


function test() {
const { b } = import('./importtest')
console.log(b())
}
test()

// or

function test() {
const { b } = require('./requiretest')
console.log(b())
}
test()

importtest.js 中也是简单输出一下


// importtest.js
export default {
b: function () {
return {
name: 'zhangsan'
}
}
}

requiretest.js 也是如此


// requiretest.js
module.exports = {
b: function() {
return {
name: 'lisi'
}
}
}

上述的方式分别执行 webpack 后,输出的内容分别如下


import 输出


image.png


在打包时一共输出了两个文件:main.jssrc_importtest_js.jsmain.js 里面输出的内容如下


image.png


main.js 里面就是 index.js 里面的内容,importtest 里面的内容,是通过一个索引的方式引用过来的,引用的地址就是 src_importtest_js.js


require 输出


image.png


require 打包时,直接输出了一个文件,就只有一个 main.jsmain.js 里面输出的内容如下


image.png


main.js 里面的内容是 index.jsrequiretest.js 里面的所有内容


综上所述,我们从数据角度来看 import 的包是要大于 require 的,但通过打包文件来看,由业务代码导致的文件大小其实 import 是要小于 require 的
复制代码

多引用情况下导致的打包变化


这个时候我们大概知道了 importrequire 打包的区别,接下来我们可以模拟一下一开始那位同学的问题,直接修改一下 webpack.config.js 的入口即可


module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
index1: './src/index1.js'
}
}
复制代码

这里直接保证 index.jsindex1.js 的内容一样即可,还是先测试一下 import 的打包


image.png


这里的内容和单入口时打包的 import 基本一致,里面出了本身的内容外,都是引用的 src_importtest_js 的地址,那么在看看 require 的包


image.png


这里内容和单入口打包的 require 基本一致,都是把 requiretest 的内容复制到了对应的文件内


虽然我们现在看的感觉多入口打包,还是 import 的文件要比 require 的文件大,但是核心问题在于测试案例的业务代码量比较少,所以看起来感觉 import 要比 require 大,当我们的业务代码量达到实际标准的时候,区别就看出来了


总结


import: 打包的内容是给到一个路径,通过该路径来访问对应的内容


require: 把当前访问资源的内容,打包到当前的文件内


到这里就可以解释为什么 vue 官方和网上的文章说推荐 import 而不推荐 require,因为每一个使用 require 的文件会把当前 require 的内容打包到当前文件内,所以导致了文件的过大,使用 import,抛出来的是一个索引,所以不会导致重复内容的打包,就不会出现包大的情况


当然这也不是绝对的,就好像上述案例那种少量的业务代码,使用 import 的代码量其实要比 require 大,所以不建议大家直接去确定某一种方式是最好的,某一种方式就是不行的,依场景选择方法


尾声


这篇文章就是一个简单的平时技术方面基础研究的简介,不是特别高深的东西,还希望对大家有所帮助,如果有覆盖面不够,或者场景不全面的情况,还希望大家提出,我在继续补充


这种类型的文章不是我擅长的方向,还是喜欢研究一些新的东西,欢迎大家指教:


链接:https://juejin.cn/post/6987219589612601357

收起阅读 »

小程序页面返回传值四种解决方案总结

使用场景 小程序从A页面跳转到B页面,在B页面选择一个值后返回到A页面,在A页面使用在B页面选中的值。例如:在购买订单页面跳转到地址列表,选择完地址以后回退到订单页面,订单页面的配送地址需要同步更新。 解决方案 常见的比容要容易解决的方案是使用小程序的全局存储...
继续阅读 »

使用场景


小程序从A页面跳转到B页面,在B页面选择一个值后返回到A页面,在A页面使用在B页面选中的值。例如:在购买订单页面跳转到地址列表,选择完地址以后回退到订单页面,订单页面的配送地址需要同步更新。


解决方案


常见的比容要容易解决的方案是使用小程序的全局存储globalData、本地缓存storage、获取小程序的页面栈,调用上一个Page的setData方法、以及利用wx.navigateTo的events属性监听被打开页面发送到当前页面的数据。下面给大家简单对比下四种方法的优缺点:


1、使用globalData实现


//page A
const app = getApp() //获取App.js实例
onShow() {//生命周期函数--监听页面显示
if (app.globalData.backData) {
this.setData({ //将B页面更新完的值渲染到页面上
backData: app.globalData.backData
},()=>{
delete app.globalData.backData //删除数据 避免onShow重复渲染
})
}
}
//page B
const app = getApp() //获取App.js实例
changeBackData(){
app.globalData.backData = '我被修改了'
wx.navigateBack()
}

2、使用本地缓存Storage实现


//page A
onShow: function () {
let backData = wx.getStorageSync('backData')
if(backData){
this.setData({
backData
},()=>{
wx.removeStorageSync('backData')
})
}
},
//page B
changeBackData(){
wx.setStorageSync('backData', '我被修改了')
wx.navigateBack()
},

3、使用小程序的Page页面栈实现


使小程序的页面栈,比其他两种方式会更方便一点而且渲染的会更快一些,不需要等回退到A页面上再把数据渲染出来,在B页面上的直接就会更新A页面上的值,回退到A页面的时候,值已经被更新了。globalData和Storage实现的原理都是在B页面上修改完值以后,回退到A页面,触发onShow生命周期函数,来更新页面渲染。


//page B
changeBackData(){
const pages = getCurrentPages();
const beforePage = pages[pages.length - 2]
beforePage.setData({ //会直接更新A页面的数据,A页面不需要其他操作
backData: "我被修改了"
})

4、使用wx.navigateTo API的events实现


wx.navigateTo的events的实现原理是利用设计模式的发布订阅模式实现的,有兴趣的同学可以自己动手实现一个简单的,也可以实现相同的效果。


//page A
goPageB() {
wx.navigateTo({
url: 'B',
events: {
getBackData: res => { //在events里面添加监听事件
this.setData({
backData: res.backData
})
},
},
})
},
//page B
changeBackData(){
const eventChannel = this.getOpenerEventChannel()
eventChannel.emit('getBackData', {
backData: '我被修改了'
});
wx.navigateBack()
}

总结


1和2两种方法在页面渲染效果上比后面两种稍微慢一点,3和4两种方法在B页面回退到A页面之前已经触发了更新,而1和2两种方法是等返回到A页面之后,在A页面才触发更新。并且1和2两种方式,要考虑到A页面更新完以后要删除globalData和Storage的数据,避免onShow方法里面重复触发setData更新页面,所以个人更推荐大家使用后面的3和4两种方式。


链接:https://juejin.cn/post/6986556857703727117

收起阅读 »

腾讯面试官:兄弟,你说你会Webpack,那说说他的原理?

原理图解 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser 2、然后使用@babel/traverse去找出入口文件所有依赖模块 3、然后使用@babel/core+@babel/preset-env将入...
继续阅读 »

image.png


原理图解



  • 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser

  • 2、然后使用@babel/traverse去找出入口文件所有依赖模块

  • 3、然后使用@babel/core+@babel/preset-env将入口文件的AST转为Code

  • 4、将2中找到的入口文件的依赖模块,进行遍历递归,重复执行1,2,3

  • 5。重写require函数,并与4中生成的递归关系图一起,输出到bundle


截屏2021-07-21 上午7.39.26.png


代码实现


webpack具体实现原理是很复杂的,这里只是简单实现一下,让大家粗略了解一下,webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!


image.png


目录


截屏2021-07-21 上午7.47.33.png


config.js


这个文件中模拟webpack的配置


const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}

入口文件


src/index.js是入口文件


// src/index
import { age } from './aa.js'
import { name } from './hh.js'

console.log(`${name}今年${age}岁了`)

// src/aa.js
export const age = 18

// src/hh.js
console.log('我来了')
export const name = '林三心'


1. 定义Compiler类


// index.js
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {}
// 重写 require函数,输出bundle
generate() {}
}

2. 解析入口文件,获取 AST


我们这里使用@babel/parser,这是babel7的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树


const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const ast = Parser.getAst(this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

3. 找出所有依赖模块


Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

4. AST 转换为 code


AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
const code = getCode(ast)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

5. 递归解析所有依赖项,生成依赖关系图


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

6. 重写 require 函数,输出 bundle


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
this.generate(dependencyGraph)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
// 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
generate(code) {
// 输出文件路径
const filePath = path.join(this.output.path, this.output.filename)
// 懵逼了吗? 没事,下一节我们捋一捋
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})`

// 把文件内容写入到文件系统
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}

new Compiler(options).run()

7. 看看main里的代码


实现了上面的代码,也就实现了把打包后的代码写到main.js文件里,咱们来看看那main.js文件里的代码吧:


(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('./src/index.js')
})({
"./src/index.js": {
"dependecies": {
"./aa.js": "./src\\aa.js",
"./hh.js": "./src\\hh.js"
},
"code": "\"use strict\";\n\nvar _aa = require(\"./aa.js\");\n\nvar _hh = require(\"./hh.js\");\n\nconsole.log(\"\".concat(_hh.name, \"\\u4ECA\\u5E74\").concat(_aa.age, \"\\u5C81\\u4E86\"));"
},
"./src\\aa.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.age = void 0;\nvar age = 18;\nexports.age = age;"
},
"./src\\hh.js": {
"dependecies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nconsole.log('我来了');\nvar name = '林三心';\nexports.name = name;"
}
})

大家可以执行一下main.js的代码,输出结果是:


我来了
林三心今年18岁了

image.png


结语


webpack具体实现原理是很复杂的,这里只是简单实现一下,让大家粗略了解一下,webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!



链接:https://juejin.cn/post/6987180860852142093

收起阅读 »

Electron上手指南

前置 配置代理,解决网络问题: npm set electron_mirror https://npm.taobao.org/mirrors/electron/ # electron 二进制包镜像 npm set ELECTRON_MIRROR https:/...
继续阅读 »

前置


配置代理,解决网络问题:


npm set electron_mirror https://npm.taobao.org/mirrors/electron/ # electron 二进制包镜像
npm set ELECTRON_MIRROR https://cdn.npm.taobao.org/dist/electron/ # electron 二进制包镜像

安装:


npm install electron --save-dev

使用


和开发 Web 应用非常类似。


index.html


<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  <title>Hello World!</title>
</head>
<body>
  <h1>Hello World!</h1>
  We are using Node.js <span id="node-version"></span>,
  Chromium <span id="chrome-version"></span>,
  and Electron <span id="electron-version"></span>.
</body>
</html>

main.js


const { app, BrowserWindow } = require('electron')

function createWindow() {
const win = new BrowserWindow({
  width: 800,
  height: 600
})

win.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()
})

甚至可以直接加载一个现成的线上应用:


const { app, BrowserWindow } = require('electron')

function createWindow() {
const win = new BrowserWindow({
  width: 800,
  height: 600
})

win.loadURL('https://www.baidu.com/')
}

app.whenReady().then(() => {
createWindow()
})

package.json


{
"name": "electron-demo",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
  "start": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
  "electron": "^13.1.7"
}
}

执行:


npm start

打包构建


npm install --save-dev @electron-forge/cli
npx electron-forge import

npm run make

流程模型


Electron 与 Chrome 类似采用多进程架构。作为 Electron 应用开发者,可以控制着两种类型的进程:主进程和渲染器。


主进程


每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。


窗口管理


主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。
BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 可从主进程用 window 的 webContent 对象与网页内容进行交互。


const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)
复制代码

应用程序生命周期


主进程还能通过 Electron 的app 模块来控制应用程序的生命周期。 该模块提供了一整套的事件和方法,可以添加自定义的应用程序行为 ( 例如:以编程方式退出您的应用程序、修改程序坞或显示关于面板 ) 。


// 当 macOS 无窗口打开时退出应用
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})

渲染器进程


每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。


预加载脚本


预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。


预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。


const { BrowserWindow } = require('electron')
//...
const win = new BrowserWindow({
preload: 'path/to/preload.js'
})
//...

由于预加载脚本与渲染器共享同一个全局 Window 接口,并且可以访问 Node.js API,因此它通过在 window 全局中暴露任意的网络内容来增强渲染器。



链接:https://juejin.cn/post/6987310547133792286

收起阅读 »

你真的了解package.json吗?来看看吧,这可能是最全的package解析

1. 概述 从我们接触前端开始,每个项目的根目录下一般都会有一个package.json文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。 当运行npm install命令的时候,会根据package.json文件中...
继续阅读 »

1. 概述


从我们接触前端开始,每个项目的根目录下一般都会有一个package.json文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。


当运行npm install命令的时候,会根据package.json文件中的配置自动下载所需的模块,也就是配置项目所需的运行和开发环境。


比如下面这个文件,只存在简单的项目名称和版本号。


{
"name" : "yindong",
"version" : "1.0.0",
}

package.json文件是一个JSON对象,这从他的后缀名.json就可以看出来,该对象的每一个成员就是当前项目的一项设置。比如name就是项目名称,version是版本号。


当然很多人其实并不关心package.json的配置,他们应用的更多的是dependencies或devDependencies配置。


下面是一个更完整的package.json文件,详细解释一下每个字段的真实含义。


{
"name": "yindong",
"version":"0.0.1",
"description": "antd-theme",
"keywords":["node.js","antd", "theme"],
"homepage": "https://zhiqianduan.com",
"bugs":{"url":"http://path/to/bug","email":"yindong@xxxx.com"},
"license": "ISC",
"author": "yindong",
"contributors":[{"name":"yindong","email":"yindong@xxxx.com"}],
"files": "",
"main": "./dist/default.js",
"bin": "",
"man": "",
"directories": "",
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"scripts": {
"start": "webpack serve --config webpack.config.dev.js --progress"
},
"config": { "port" : "8080" },
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"babel-plugin-import": "^1.13.3",
"glob": "^7.1.7",
"less": "^3.9.0",
"less-loader": "^9.0.0",
"style-loader": "^2.0.0",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"peerDependencies": {
"tea": "2.x"
},
"bundledDependencies": [
"renderized", "super-streams"
],
"engines": {"node": "0.10.x"},
"os" : [ "win32", "darwin", "linux" ],
"cpu" : [ "x64", "ia32" ],
"private": false,
"publishConfig": {}
}


2. name字段


package.json文件中最重要的就是nameversion字段,这两项是必填的。名称和版本一起构成一个标识符,该标识符被认为是完全唯一的。对包的更改应该与对版本的更改一起进行。


name必须小于等于214个字符,不能以._开头,不能有大写字母,因为名称最终成为URL的一部分因此不能包含任何非URL安全字符。
npm官方建议我们不要使用与核心节点模块相同的名称。不要在名称中加jsnode。如果需要可以使用engines来指定运行环境。


该名称会作为参数传递给require,因此它应该是简短的,但也需要具有合理的描述性。


3. version字段


version一般的格式是x.x.x, 并且需要遵循该规则。


package.json文件中最重要的就是nameversion字段,这两项是必填的。名称和版本一起构成一个标识符,该标识符被认为是完全唯一的。每次发布时version不能与已存在的一致。


4. description字段


description是一个字符串,用于编写描述信息。有助于人们在npm库中搜索的时候发现你的模块。


5. keywords字段


keywords是一个字符串组成的数组,有助于人们在npm库中搜索的时候发现你的模块。


6. homepage字段


homepage项目的主页地址。


7. bugs字段


bugs用于项目问题的反馈issue地址或者一个邮箱。


"bugs": { 
"url" : "https://github.com/owner/project/issues",
"email" : "project@hostname.com"
}

8. license字段


license是当前项目的协议,让用户知道他们有何权限来使用你的模块,以及使用该模块有哪些限制。


"license" : "BSD-3-Clause"

9. author字段 contributors字段


author是具体一个人,contributors表示一群人,他们都表示当前项目的共享者。同时每个人都是一个对象。具有name字段和可选的urlemail字段。


"author": {
"name" : "yindong",
"email" : "yindong@xx.com",
"url" : "https://zhiqianduan.com/"
}

也可以写成一个字符串


"author": "yindong yindong@xx.com (https://zhiqianduan.com/)"

10. files字段


files属性的值是一个数组,内容是模块下文件名或者文件夹名,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)


可以在模块根目录下创建一个.npmignore文件,写在这个文件里边的文件即便被写在files属性里边也会被排除在外,这个文件的写法与.gitignore类似。


11. main字段


main字段指定了加载的入口文件,require导入的时候就会加载这个文件。这个字段的默认值是模块根目录下面的index.js


12. bin字段


bin项用来指定每个内部命令对应的可执行文件的位置。如果你编写的是一个node工具的时候一定会用到bin字段。


当我们编写一个cli工具的时候,需要指定工具的运行命令,比如常用的webpack模块,他的运行命令就是webpack


"bin": {
"webpack": "bin/index.js",
}

当我们执行webpack命令的时候就会执行bin/index.js文件中的代码。


在模块以依赖的方式被安装,如果存在bin选项。在node_modules/.bin/生成对应的文件,
Npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。


所有node_modules/.bin/目录下的命令,都可以用npm run [命令]的格式运行。在命令行下,键入npm run,然后按tab键,就会显示所有可以使用的命令。


13. man字段


man用来指定当前模块的man文档的位置。


"man" :[ "./doc/calc.1" ]

14. directories字段


directories制定一些方法来描述模块的结构, 用于告诉用户每个目录在什么位置。


15. repository字段


指定一个代码存放地址,对想要为你的项目贡献代码的人有帮助


"repository" : {
"type" : "git",
"url" : "https://github.com/npm/npm.git"
}

16. scripts字段


scripts指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令。


"scripts": {
"start": "node ./start.js"
}

使用scripts字段可以快速的执行shell命令,可以理解为alias


scripts可以直接使用node_modules中安装的模块,这区别于直接运行需要使用npx命令。


"scripts": {
"build": "webpack"
}

// npm run build
// npx webpack

17. config字段


config字段用于添加命令行的环境变量。


{
"name" : "yindong",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}

然后,在server.js脚本就可以引用config字段的值。


console.log(process.env.npm_package_config_port); // 8080

用户可以通过npm config set来修改这个值。


npm config set yindong:port 8000

18. dependencies字段, devDependencies字段


dependencies字段指定了项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。


它们的值都是一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。


当安装依赖的时候使用--save参数表示将该模块写入dependencies属性,--save-dev表示将该模块写入devDependencies属性。


"devDependencies": {
"webpack": "^5.38.1",
}

对象的每一项通过一个键值对表示,前面是模块名称,后面是对应模块的版本号。版本号遵循“大版本.次要版本.小版本”的格式规定。



版本说明



固定版本: 比如5.38.1,安装时只安装指定版本。
波浪号: 比如~5.38.1, 表示安装5.38.x的最新版本(不低于5.38.1),但是不安装5.39.x,也就是说安装时不改变大版本号和次要版本号。
插入号: 比如ˆ5.38.1, ,表示安装5.x.x的最新版本(不低于5.38.1),但是不安装6.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
latest: 安装最新版本。




19. peerDependencies字段


当我们开发一个模块的时候,如果当前模块与所依赖的模块同时依赖一个第三方模块,并且依赖的是两个不兼容的版本时就会出现问题。


比如,你的项目依赖A模块和B模块的1.0版,而A模块本身又依赖B模块的2.0版。


大多数情况下,这不构成问题,B模块的两个版本可以并存,同时运行。但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。


最典型的场景就是插件,比如A模块是B模块的插件。用户安装的B模块是1.0版本,但是A插件只能和2.0版本的B模块一起使用。这时,用户要是将1.0版本的B的实例传给A,就会出现问题。因此,需要一种机制,在模板安装的时候提醒用户,如果A和B一起安装,那么B必须是2.0模块。


peerDependencies字段,就是用来供插件指定其所需要的主工具的版本。可以通过peerDependencies字段来限制,使用myless模块必须依赖less模块的3.9.x版本.


{
"name": "myless",
"peerDependencies": {
"less": "3.9.x"
}
}

注意,从npm 3.0版开始,peerDependencies不再会默认安装了。就是初始化的时候不会默认带出。


20. bundledDependencies字段


bundledDependencies指定发布的时候会被一起打包的模块.


21. optionalDependencies字段


如果一个依赖模块可以被使用, 同时你也希望在该模块找不到或无法获取时npm继续运行,你可以把这个模块依赖放到optionalDependencies配置中。这个配置的写法和dependencies的写法一样,不同的是这里边写的模块安装失败不会导致npm install失败。


22. engines字段


engines字段指明了该模块运行的平台,比如Node或者npm的某个版本或者浏览器。


{ "engines" : { "node" : ">=0.10.3 <0.12", "npm" : "~1.0.20" } }

23. os字段


可以指定你的模块只能在哪个操作系统上运行


"os" : [ "darwin", "linux", "win32" ]

24. cpu字段


限制模块只能在某种架构的cpu下运行


"cpu" : [ "x64", "ia32" ]

25. private字段


如果这个属性被设置为truenpm将拒绝发布它,这是为了防止一个私有模块被无意间发布出去。


"private": true

26. publishConfig字段


这个配置是会在模块发布时生效,用于设置发布用到的一些值的集合。如果你不想模块被默认标记为最新的,或者默认发布到公共仓库,可以在这里配置tag或仓库地址。


通常publishConfig会配合private来使用,如果你只想让模块被发布到一个特定的npm仓库,如一个内部的仓库。


"private": true,
"publishConfig": {
"tag": "1.0.0",
"registry": "https://registry.npmjs.org/",
"access": "public"
}

27. preferGlobal字段


preferGlobal的值是布尔值,表示当用户不将该模块安装为全局模块时(即不用–global参数),要不要显示警告,表示该模块的本意就是安装为全局模块。


"preferGlobal": false

28. browser字段


browser指定该模板供浏览器使用的版本。Browserify这样的浏览器打包工具,通过它就知道该打包那个文件。


"browser": {
"tipso": "./node_modules/tipso/src/tipso.js"
},


链接:https://juejin.cn/post/6987179395714646024


收起阅读 »

单独维护图片选择开源库ImagePicker,便于根据个人业务需要进行二次开发的要求

演示1.用法使用前,对于Android Studio的用户,可以选择添加: compile 'com.lzy.widget:imagepicker:0.6.1' //指定版本2.功能和参数含义温馨提示:目前库中的预览界面有个原图的复选框,暂时只做了UI,还没...
继续阅读 »

演示

imageimageimageimage

1.用法

使用前,对于Android Studio的用户,可以选择添加:

	compile 'com.lzy.widget:imagepicker:0.6.1'  //指定版本

2.功能和参数含义

温馨提示:目前库中的预览界面有个原图的复选框,暂时只做了UI,还没有做压缩的逻辑

配置参数参数含义
multiMode图片选着模式,单选/多选
selectLimit多选限制数量,默认为9
showCamera选择照片时是否显示拍照按钮
crop是否允许裁剪(单选有效)
style有裁剪时,裁剪框是矩形还是圆形
focusWidth矩形裁剪框宽度(圆形自动取宽高最小值)
focusHeight矩形裁剪框高度(圆形自动取宽高最小值)
outPutX裁剪后需要保存的图片宽度
outPutY裁剪后需要保存的图片高度
isSaveRectangle裁剪后的图片是按矩形区域保存还是裁剪框的形状,例如圆形裁剪的时候,该参数给true,那么保存的图片是矩形区域,如果该参数给fale,保存的图片是圆形区域
imageLoader需要使用的图片加载器,自需要实现ImageLoader接口即可

3.代码参考

更多使用,请下载demo参看源代码

  1. 首先你需要继承 com.lzy.imagepicker.loader.ImageLoader 这个接口,实现其中的方法,比如以下代码是使用 Picasso 三方加载库实现的
public class PicassoImageLoader implements ImageLoader {

@Override
public void displayImage(Activity activity, String path, ImageView imageView, int width, int height) {
Picasso.with(activity)//
                   .load(Uri.fromFile(new File(path)))//
.placeholder(R.mipmap.default_image)//
.error(R.mipmap.default_image)//
.resize(width, height)//
.centerInside()//
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)//
.into(imageView);
}

@Override
public void clearMemoryCache() {
//这里是清除缓存的方法,根据需要自己实现
}
}
  1. 然后配置图片选择器,一般在Application初始化配置一次就可以,这里就需要将上面的图片加载器设置进来,其余的配置根据需要设置
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_picker);

ImagePicker imagePicker = ImagePicker.getInstance();
imagePicker.setImageLoader(new PicassoImageLoader()); //设置图片加载器
imagePicker.setShowCamera(true); //显示拍照按钮
imagePicker.setCrop(true); //允许裁剪(单选才有效)
imagePicker.setSaveRectangle(true); //是否按矩形区域保存
imagePicker.setSelectLimit(9); //选中数量限制
imagePicker.setStyle(CropImageView.Style.RECTANGLE); //裁剪框的形状
imagePicker.setFocusWidth(800); //裁剪框的宽度。单位像素(圆形自动取宽高最小值)
imagePicker.setFocusHeight(800); //裁剪框的高度。单位像素(圆形自动取宽高最小值)
imagePicker.setOutPutX(1000);//保存文件的宽度。单位像素
imagePicker.setOutPutY(1000);//保存文件的高度。单位像素
}
  1. 以上配置完成后,在适当的方法中开启相册,例如点击按钮时
public void onClick(View v) {
Intent intent = new Intent(this, ImageGridActivity.class);
startActivityForResult(intent, IMAGE_PICKER);
}
}
  1. 如果你想直接调用相机
Intent intent = new Intent(this, ImageGridActivity.class);
intent.putExtra(ImageGridActivity.EXTRAS_TAKE_PICKERS,true); // 是否是直接打开相机
startActivityForResult(intent, REQUEST_CODE_SELECT);
  1. 重写onActivityResult方法,回调结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == ImagePicker.RESULT_CODE_ITEMS) {
if (data != null && requestCode == IMAGE_PICKER) {
ArrayList<ImageItem> images = (ArrayList<ImageItem>) data.getSerializableExtra(ImagePicker.EXTRA_RESULT_ITEMS);
MyAdapter adapter = new MyAdapter(images);
gridView.setAdapter(adapter);
} else {
Toast.makeText(this, "没有数据", Toast.LENGTH_SHORT).show();
}
}
}

代码下载:ImagePicker-master.zip

收起阅读 »

用Activity实现的锁屏程序,可有效的屏蔽Home键,Recent键,通知栏

功能目前市面上大部分锁屏应用都是用悬浮窗实现,而不用Activity。因为用Activity实现的锁屏应用,很大的问题就是Activity能被各种办法关闭或者绕过,所以本项目参考了一些前人的经验,也反编了一些现有锁屏应用的包,最后终于基本解决了所有能绕过Act...
继续阅读 »

功能

目前市面上大部分锁屏应用都是用悬浮窗实现,而不用Activity。因为用Activity实现的锁屏应用,很大的问题就是Activity能被各种办法关闭或者绕过,所以本项目参考了一些前人的经验,也反编了一些现有锁屏应用的包,最后终于基本解决了所有能绕过Activity锁屏的场景,让Activity实现的锁屏也能安安全全的挡在屏幕前。

  1. 亮屏自动启动锁屏Activity

  2. 锁屏界面屏蔽Home键,back键,recent键,防止将Activity退到后台

  3. 锁屏界面禁用通知栏下拉,防止点击通知跳到第三方应用,锁屏被绕过

  4. 最近列表中排除锁屏Activity,防止锁屏Activity在不正常的场景出现

设置说明

  1. 请先设置"我的锁屏"为默认的Launcher程序(桌面应用),才可以正常使用所有功能
  2. 第三方应用无权限禁用系统的锁屏,所以如果设置了密码锁,会出现双重锁屏情况,测试时请先禁用系统锁屏
  3. 来电和闹铃等场景会自动解除锁屏,但是来电和闹铃亮屏后,过程中按电源键关闭屏幕,再打开,锁屏界面会出现在来电或者闹铃界面之上,造成覆盖,需要另做特殊处理
收起阅读 »

Android仿ButterKnife,实现自己的BindView

仿ButterKnife,实现自己的BindViewButterKnife插件的出现让Android程序员从繁琐的findViewById重复代码中解放出来,尤其搭配各种自动生成代码的Android Studio插件,更是如虎添翼。 ButterKnife的实...
继续阅读 »


仿ButterKnife,实现自己的BindView

ButterKnife插件的出现让Android程序员从繁琐的findViewById重复代码中解放出来,尤其搭配各种自动生成代码的Android Studio插件,更是如虎添翼。 ButterKnife的实现原理,大家应该都有所耳闻,利用AbstractProcess,在编译时候为BindView注解的控件自动生成findViewById代码,ButterKnife#bind(Activity)方法,实质就是去调用自动生成的这些findViewById代码。 然而,当我需要去了解这些实现细节的时候,我决定去看看ButterKnife的源码。ButterKnife整个项目涵盖的注解有很多,看起来可能会消耗不少的时间,笔者基于这些天的摸索的该项目的思路,实现了自己的一个BindView注解的使用,来帮助大家了解。

GitHub链接

笔者实现的项目已经上传到Github,欢迎大家star。点击查看MyButterKnife

项目结构

Annotation module

我们需要处理的BindView注解,就声明在这个module里,简单不多说。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value() default -1;
}

Target为FIELD类型,表示这个注解用于类内属性的声明;Retention为CLASS,表示这个注解在项目编译时起作用,如果为RUNTIME则表示在运行时起作用,RUNTIME的注解都是结合反射使用的,所以执行效率上有所欠缺,应该尽量避免使用RUNTIME类注解。 BindView内的value为int类型,正是R.id对应的类型,方便我们直接对View声明其绑定的id:

@BindView(R.id.btn)
protected Button mBtn;

Compiler module

这个module是自动生成findViewById代码的重点,这里只有一个类,继承于AbstractProcessor。

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindProcess extends AbstractProcessor{
private Elements mElementsUtil;

/**
* key: eclosed elemnt
* value: inner views with BindView annotation
*/
private Map<TypeElement,Set<Element>> mElems;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementsUtil = processingEnv.getElementUtils();
mElems = new HashMap<>();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(BindView.class.getCanonicalName());
return types;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("Process start !");

initBindElems(roundEnv.getElementsAnnotatedWith(BindView.class));
generateJavaClass();

System.out.println("Process finish !");
return true;
}

private void generateJavaClass() {
for (TypeElement enclosedElem : mElems.keySet()) {
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(ClassName.get(enclosedElem.asType()),"activity")
.returns(TypeName.VOID);
for (Element bindElem : mElems.get(enclosedElem)) {
methodSpecBuilder.addStatement(String.format("activity.%s = (%s)activity.findViewById(%d)",bindElem.getSimpleName(),bindElem.asType(),bindElem.getAnnotation(BindView.class).value()));
}
TypeSpec typeSpec = TypeSpec.classBuilder("Bind"+enclosedElem.getSimpleName())
.superclass(TypeName.get(enclosedElem.asType()))
.addModifiers(Modifier.FINAL,Modifier.PUBLIC)
.addMethod(methodSpecBuilder.build())
.build();
JavaFile file = JavaFile.builder(getPackageName(enclosedElem),typeSpec).build();
try {
file.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
}

private void initBindElems(Set<? extends Element> bindElems) {
for (Element bindElem : bindElems) {
TypeElement enclosedElem = (TypeElement) bindElem.getEnclosingElement();
Set<Element> elems = mElems.get(enclosedElem);
if (elems == null){
elems= new HashSet<>();
mElems.put(enclosedElem,elems);
System.out.println("Add enclose elem "+enclosedElem.getSimpleName());
}
elems.add(bindElem);
System.out.println("Add bind elem "+bindElem.getSimpleName());
}
}

private String getPackageName(TypeElement type) {
return mElementsUtil.getPackageOf(type).getQualifiedName().toString();
}
}

类注解@AutoServic用于自动生成META-INF信息,对于AbstractProcessor的继承类,需要声明在META-INF里,才能在编译时生效。有了AutoService,可以自动把注解的类加入到META-INF里。使用AutoService需要引入如下包:

compile 'com.google.auto.service:auto-service:1.0-rc2'

然后编译时就会执行proces方法来生成代码,参数annotautions是一个集合,由于上面getSupportedAnnotationTypes返回的是@BindView注解,所以annotations参数里包含所有被@BindView注解的元素。把各元素按照所在类来分组,放入map中,然后generateJavaClass方法中用该map来生成代码,这里使用了javapoet包里的类,能很方便的生成各种java类,方法,修饰符等等。方法体类代码看似复杂,但稍微学一下javapoet包的使用,就可以很快熟练该方法的作用,以下是编译后生成出来的java类代码:

package top.lizhengxian.apt_sample;

public final class BindMainActivity extends MainActivity {
public static void bindView(MainActivity activity) {
activity.mBtn = (android.widget.Button)activity.findViewById(2131427422);
activity.mTextView = (android.widget.TextView)activity.findViewById(2131427423);
}
}

而被注解的原类如下:

public class MainActivity extends AppCompatActivity {

@BindView(R.id.btn)
protected Button mBtn;

@BindView(R.id.text)
protected TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindMainActivity.bindView(this)
}
}

生成的java类位于如下位置: 

mybutterknife module

按理说,上面已经完成了整个findViewById的代码生成,在MainActivity的onCreat方法里,调用完setContentView后,就可以直接调用BindMainActivity.bindView(this)来完成各个View和id的绑定和实例化了。 但是我们观察ButterKnife中的实现,不管是哪个Activity类,都是调用ButterKnife.bindView(this)方法来注入的。而在本项目的代码中,不同的类,就会生成不同名字继承类,比如,如果另有一个HomeActivity类,那注入就要使用BindHomeActivity.bindView(this)来实现。 怎样实现ButterKnife那样统一方法来注入呢? 还是查看源码,可以发现,ButterKnife.bindView方法使用的还是反射来调用生成的类中的方法,也就是说,ButterKnife.bindView只是提供了统一入口。 对照于此,在mybutterknife module里,我们也可以用反射实现类似的方法路由,统一所有的注入方法入口:

public class MyButterKnife {
private static Map<Class,Method> classMethodMap = new HashMap<>();
public static void bindView(Activity target){
if (target != null){
Method method = classMethodMap.get(target.getClass());
try {
if (method == null) {
String bindClassName = target.getPackageName() + ".Bind" + target.getClass().getSimpleName();
Class bindClass = Class.forName(bindClassName);
method = bindClass.getMethod("bindView", target.getClass());
classMethodMap.put(target.getClass(), method);
}
method.invoke(null, target);
}catch (Exception e){
e.printStackTrace();
}
}
}
}

sample module

综上,轻轻松松实现了我们自己的BindView注解,使用方式如下:

public class MainActivity extends AppCompatActivity {

@BindView(R.id.btn)
protected Button mBtn;

@BindView(R.id.text)
protected TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bindView(this);
mBtn.setText("changed");
mTextView.setText("changed too");
}
}

运行代码,完美!


代码下载:MyButterKnife-master.zip

收起阅读 »

快速使用Windows版的MQTT 客户端实现消息收发

    在环信MQTT消息云产品上线之后,很多小伙伴对这个产品都跃跃欲试。从业务方面,比如想要应用在实时位置共享、实时数据传输以及IoT设备管理等业务中;从开发平台,涵盖了C、C#、Android、iOS以及Java等开发语言;综上所述,我...
继续阅读 »

    在环信MQTT消息云产品上线之后,很多小伙伴对这个产品都跃跃欲试。从业务方面,比如想要应用在实时位置共享、实时数据传输以及IoT设备管理等业务中;从开发平台,涵盖了C、C#、Android、iOS以及Java等开发语言;综上所述,我们的小Q真的是功能丰富,搭建简单,人见人爱。

   那为了让大家更快的体验我们小Q收发消息的特性,除了已提供的各端SDK外,我们今天也特地整理了Windows版的MQTT客户端收发消息流程,大家阅后赶快整起吧~

一、环境配置

1、根据电脑配置,下载eclipse安装包,点击下载

2、根据电脑配置,安装JDK,点击下载

3、本次电脑配置为Windows10,64位系统,

(1)eclipse选择为:org.eclipse.paho.ui.app-1.0.2-win32.win32.x86_64.zip;

(2)JDK选择为:jdk-8u291-windows-x64.exe。

上述安装包已上传至百度网盘,下载链接: https://pan.baidu.com/s/1m1q1HX6oTvrSPLFBwW9TBQ 密码: rhe3


二、操作流程

1、安装eclipse后,双击启动应用程序:


2、进入程序页面,点击【+】图标创建会话


3、输入连接信息,包括服务器地址、端口、clientID、用户名和密码

(获取方式见链接:https://docs-im.easemob.com/mqtt/qsquick



4、切换回【MQTT】页面,点击连接


5、添加订阅主题,选择订阅栏下的【+】图标,输入主题名称,点击【订阅】按钮,订阅成功后,可以在右侧的【历史记录】中查看;


6、发送消息,在发布窗口填写主题、QoS、以及消息体内容(消息负载支持json、XML和RAW格式),内容输入完成后,点击【发布】按钮;

7、由于发布主题与订阅主题相同,所以【历史记录】中存在已发布记录和已接收记录(可根据需要,创建不同的客户端实现消息收发)



收起阅读 »

Android 选择图片、上传图片之PictureSelector

效果图: 【注意】Demo已更新到最新版本,并稍作调整。(2019-07-05) 之前出过一篇 Android 选择图片、上传图片之ImagePicker,这个是okgo作者出的,就一般需求来讲是够了,但是没有压缩,需要自己去搞。 后来业务需求...
继续阅读 »


效果图:
这里写图片描述这里写图片描述这里写图片描述这里写图片描述


【注意】Demo已更新到最新版本,并稍作调整。(2019-07-05)





之前出过一篇 Android 选择图片、上传图片之ImagePicker,这个是okgo作者出的,就一般需求来讲是够了,但是没有压缩,需要自己去搞。
后来业务需求提升,页面要美,体验要好,便不是那么满足需求了,所幸在github上找到PictureSelector(然后当时没多久Matisse就开源了…可以看这里Android 选择图片、上传图片之Matisse),也不用自己再撸一个了,下面来介绍介绍PictureSelector



github



https://github.com/LuckSiege/PictureSelector


目前是一直在维护的,支持从相册或拍照选择图片或视频、音频,支持动态权限获取、裁剪(单图or多图裁剪)、压缩、主题自定义配置等功能、适配android 6.0+系统,而且你能遇到的问题,README文档都有解决方案。



功能特点


功能齐全,且兼容性好,作者也做了兼容测试



1.适配android6.0+系统
2.解决部分机型裁剪闪退问题
3.解决图片过大oom闪退问题
4.动态获取系统权限,避免闪退
5.支持相片or视频的单选和多选
6.支持裁剪比例设置,如常用的 1:1、3:4、3:2、16:9 默认为图片大小
7.支持视频预览
8.支持gif图片
9.支持.webp格式图片
10.支持一些常用场景设置:如:是否裁剪、是否预览图片、是否显示相机等
11.新增自定义主题设置
12.新增图片勾选样式设置
13.新增图片裁剪宽高设置
14.新增图片压缩处理
15.新增录视频最大时间设置
16.新增视频清晰度设置
17.新增QQ选择风格,带数字效果
18.新增自定义 文字颜色 背景色让风格和项目更搭配
19.新增多图裁剪功能
20.新增LuBan多图压缩
21.新增单独拍照功能
22.新增压缩大小设置
23.新增Luban压缩档次设置
24.新增圆形头像裁剪
25.新增音频功能查询



主题配置


这个就想怎么改就怎么改了


<!--默认样式 注意* 样式只可修改,不能删除任何一项 否则报错-->
<style name="picture.default.style" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<!--标题栏背景色-->
<item name="colorPrimary">@color/bar_grey</item>
<!--状态栏背景色-->
<item name="colorPrimaryDark">@color/bar_grey</item>
<!--是否改变图片列表界面状态栏字体颜色为黑色-->
<item name="picture.statusFontColor">false</item>
<!--返回键图标-->
<item name="picture.leftBack.icon">@drawable/picture_back</item>
<!--标题下拉箭头-->
<item name="picture.arrow_down.icon">@drawable/arrow_down</item>
<!--标题上拉箭头-->
<item name="picture.arrow_up.icon">@drawable/arrow_up</item>
<!--标题文字颜色-->
<item name="picture.title.textColor">@color/white</item>
<!--标题栏右边文字-->
<item name="picture.right.textColor">@color/white</item>
<!--图片列表勾选样式-->
<item name="picture.checked.style">@drawable/checkbox_selector</item>
<!--开启图片列表勾选数字模式-->
<item name="picture.style.checkNumMode">false</item>
<!--选择图片样式0/9-->
<item name="picture.style.numComplete">false</item>
<!--图片列表底部背景色-->
<item name="picture.bottom.bg">@color/color_fa</item>
<!--图片列表预览文字颜色-->
<item name="picture.preview.textColor">@color/tab_color_true</item>
<!--图片列表已完成文字颜色-->
<item name="picture.complete.textColor">@color/tab_color_true</item>
<!--图片已选数量圆点背景色-->
<item name="picture.num.style">@drawable/num_oval</item>
<!--预览界面标题文字颜色-->
<item name="picture.ac_preview.title.textColor">@color/white</item>
<!--预览界面已完成文字颜色-->
<item name="picture.ac_preview.complete.textColor">@color/tab_color_true</item>
<!--预览界面标题栏背景色-->
<item name="picture.ac_preview.title.bg">@color/bar_grey</item>
<!--预览界面底部背景色-->
<item name="picture.ac_preview.bottom.bg">@color/bar_grey_90</item>
<!--预览界面状态栏颜色-->
<item name="picture.status.color">@color/bar_grey_90</item>
<!--预览界面返回箭头-->
<item name="picture.preview.leftBack.icon">@drawable/picture_back</item>
<!--是否改变预览界面状态栏字体颜色为黑色-->
<item name="picture.preview.statusFontColor">false</item>
<!--裁剪页面标题背景色-->
<item name="picture.crop.toolbar.bg">@color/bar_grey</item>
<!--裁剪页面状态栏颜色-->
<item name="picture.crop.status.color">@color/bar_grey</item>
<!--裁剪页面标题文字颜色-->
<item name="picture.crop.title.color">@color/white</item>
<!--相册文件夹列表选中图标-->
<item name="picture.folder_checked_dot">@drawable/orange_oval</item>
</style>

功能配置


// 进入相册 以下是例子:用不到的api可以不写
PictureSelector.create(MainActivity.this)
.openGallery()//全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()、音频.ofAudio()
.theme()//主题样式(不设置为默认样式) 也可参考demo values/styles下 例如:R.style.picture.white.style
.maxSelectNum()// 最大图片选择数量 int
.minSelectNum()// 最小选择数量 int
.imageSpanCount(4)// 每行显示个数 int
.selectionMode()// 多选 or 单选 PictureConfig.MULTIPLE or PictureConfig.SINGLE
.previewImage()// 是否可预览图片 true or false
.previewVideo()// 是否可预览视频 true or false
.enablePreviewAudio() // 是否可播放音频 true or false
.isCamera()// 是否显示拍照按钮 true or false
.imageFormat(PictureMimeType.PNG)// 拍照保存图片格式后缀,默认jpeg
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true
.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
.setOutputCameraPath("/CustomPath")// 自定义拍照保存路径,可不填
.enableCrop()// 是否裁剪 true or false
.compress()// 是否压缩 true or false
.glideOverride()// int glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
.withAspectRatio()// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
.hideBottomControls()// 是否显示uCrop工具栏,默认不显示 true or false
.isGif()// 是否显示gif图片 true or false
.compressSavePath(getPath())//压缩图片保存地址
.freeStyleCropEnabled()// 裁剪框是否可拖拽 true or false
.circleDimmedLayer()// 是否圆形裁剪 true or false
.showCropFrame()// 是否显示裁剪矩形边框 圆形裁剪时建议设为false true or false
.showCropGrid()// 是否显示裁剪矩形网格 圆形裁剪时建议设为false true or false
.openClickSound()// 是否开启点击声音 true or false
.selectionMedia()// 是否传入已选图片 List<LocalMedia> list
.previewEggs()// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false
.cropCompressQuality()// 裁剪压缩质量 默认90 int
.minimumCompressSize(100)// 小于100kb的图片不压缩
.synOrAsy(true)//同步true或异步false 压缩 默认同步
.cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效 int
.rotateEnabled() // 裁剪是否可旋转图片 true or false
.scaleEnabled()// 裁剪是否可放大缩小图片 true or false
.videoQuality()// 视频录制质量 0 or 1 int
.videoMaxSecond(15)// 显示多少秒以内的视频or音频也可适用 int
.videoMinSecond(10)// 显示多少秒以内的视频or音频也可适用 int
.recordVideoSecond()//视频秒数录制 默认60s int
.isDragFrame(false)// 是否可拖动裁剪框(固定)
.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code

集成方式


compile引入


dependencies {
implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.2.3'
}

build.gradle加入


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

使用


使用非常简单,你想要的基本上都有



package com.yechaoa.pictureselectordemo;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;

import com.luck.picture.lib.PictureSelector;
import com.luck.picture.lib.config.PictureConfig;
import com.luck.picture.lib.config.PictureMimeType;
import com.luck.picture.lib.entity.LocalMedia;
import com.luck.picture.lib.permissions.Permission;
import com.luck.picture.lib.permissions.RxPermissions;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.functions.Consumer;

public class MainActivity extends AppCompatActivity {

private int maxSelectNum = 9;
private List<LocalMedia> selectList = new ArrayList<>();
private GridImageAdapter adapter;
private RecyclerView mRecyclerView;
private PopupWindow pop;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mRecyclerView = findViewById(R.id.recycler);

initWidget();
}

private void initWidget() {
FullyGridLayoutManager manager = new FullyGridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(manager);
adapter = new GridImageAdapter(this, onAddPicClickListener);
adapter.setList(selectList);
adapter.setSelectMax(maxSelectNum);
mRecyclerView.setAdapter(adapter);
adapter.setOnItemClickListener(new GridImageAdapter.OnItemClickListener() {
@Override
public void onItemClick(int position, View v) {
if (selectList.size() > 0) {
LocalMedia media = selectList.get(position);
String pictureType = media.getPictureType();
int mediaType = PictureMimeType.pictureToVideo(pictureType);
switch (mediaType) {
case 1:
// 预览图片 可自定长按保存路径
//PictureSelector.create(MainActivity.this).externalPicturePreview(position, "/custom_file", selectList);
PictureSelector.create(MainActivity.this).externalPicturePreview(position, selectList);
break;
case 2:
// 预览视频
PictureSelector.create(MainActivity.this).externalPictureVideo(media.getPath());
break;
case 3:
// 预览音频
PictureSelector.create(MainActivity.this).externalPictureAudio(media.getPath());
break;
}
}
}
});
}

private GridImageAdapter.onAddPicClickListener onAddPicClickListener = new GridImageAdapter.onAddPicClickListener() {

@SuppressLint("CheckResult")
@Override
public void onAddPicClick() {
//获取写的权限
RxPermissions rxPermission = new RxPermissions(MainActivity.this);
rxPermission.requestEach(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.subscribe(new Consumer<Permission>() {
@Override
public void accept(Permission permission) {
if (permission.granted) {// 用户已经同意该权限
//第一种方式,弹出选择和拍照的dialog
showPop();

//第二种方式,直接进入相册,但是 是有拍照得按钮的
// showAlbum();
} else {
Toast.makeText(MainActivity.this, "拒绝", Toast.LENGTH_SHORT).show();
}
}
});
}
};

private void showAlbum() {
//参数很多,根据需要添加
PictureSelector.create(MainActivity.this)
.openGallery(PictureMimeType.ofImage())// 全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()、音频.ofAudio()
.maxSelectNum(maxSelectNum)// 最大图片选择数量
.minSelectNum(1)// 最小选择数量
.imageSpanCount(4)// 每行显示个数
.selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选PictureConfig.MULTIPLE : PictureConfig.SINGLE
.previewImage(true)// 是否可预览图片
.isCamera(true)// 是否显示拍照按钮
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true
//.setOutputCameraPath("/CustomPath")// 自定义拍照保存路径
.enableCrop(true)// 是否裁剪
.compress(true)// 是否压缩
//.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
.glideOverride(160, 160)// glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
.withAspectRatio(1, 1)// 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
//.selectionMedia(selectList)// 是否传入已选图片
//.previewEggs(false)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中)
//.cropCompressQuality(90)// 裁剪压缩质量 默认100
//.compressMaxKB()//压缩最大值kb compressGrade()为Luban.CUSTOM_GEAR有效
//.compressWH() // 压缩宽高比 compressGrade()为Luban.CUSTOM_GEAR有效
//.cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效
.rotateEnabled(false) // 裁剪是否可旋转图片
//.scaleEnabled()// 裁剪是否可放大缩小图片
//.recordVideoSecond()//录制视频秒数 默认60s
.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code
}

private void showPop() {
View bottomView = View.inflate(MainActivity.this, R.layout.layout_bottom_dialog, null);
TextView mAlbum = bottomView.findViewById(R.id.tv_album);
TextView mCamera = bottomView.findViewById(R.id.tv_camera);
TextView mCancel = bottomView.findViewById(R.id.tv_cancel);

pop = new PopupWindow(bottomView, -1, -2);
pop.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
pop.setOutsideTouchable(true);
pop.setFocusable(true);
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 0.5f;
getWindow().setAttributes(lp);
pop.setOnDismissListener(new PopupWindow.OnDismissListener() {

@Override
public void onDismiss() {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 1f;
getWindow().setAttributes(lp);
}
});
pop.setAnimationStyle(R.style.main_menu_photo_anim);
pop.showAtLocation(getWindow().getDecorView(), Gravity.BOTTOM, 0, 0);

View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.tv_album:
//相册
PictureSelector.create(MainActivity.this)
.openGallery(PictureMimeType.ofImage())
.maxSelectNum(maxSelectNum)
.minSelectNum(1)
.imageSpanCount(4)
.selectionMode(PictureConfig.MULTIPLE)
.forResult(PictureConfig.CHOOSE_REQUEST);
break;
case R.id.tv_camera:
//拍照
PictureSelector.create(MainActivity.this)
.openCamera(PictureMimeType.ofImage())
.forResult(PictureConfig.CHOOSE_REQUEST);
break;
case R.id.tv_cancel:
//取消
//closePopupWindow();
break;
}
closePopupWindow();
}
};

mAlbum.setOnClickListener(clickListener);
mCamera.setOnClickListener(clickListener);
mCancel.setOnClickListener(clickListener);
}

public void closePopupWindow() {
if (pop != null && pop.isShowing()) {
pop.dismiss();
pop = null;
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
List<LocalMedia> images;
if (resultCode == RESULT_OK) {
if (requestCode == PictureConfig.CHOOSE_REQUEST) {// 图片选择结果回调

images = PictureSelector.obtainMultipleResult(data);
selectList.addAll(images);

//selectList = PictureSelector.obtainMultipleResult(data);

// 例如 LocalMedia 里面返回三种path
// 1.media.getPath(); 为原图path
// 2.media.getCutPath();为裁剪后path,需判断media.isCut();是否为true
// 3.media.getCompressPath();为压缩后path,需判断media.isCompressed();是否为true
// 如果裁剪并压缩了,以取压缩路径为准,因为是先裁剪后压缩的
adapter.setList(selectList);
adapter.notifyDataSetChanged();
}
}
}

}





Demo:https://github.com/yechaoa/PictureSelectorDemo



收起阅读 »

Android7.0拍照以及使用uCrop裁剪

一、引入 Android在7.0中修改了文件权限,所以从Android7.0开始要使用FileProvider来处理uri,从网上找了好多文章,解决了在7.0下拍照及相册选图的问题,但是参照网上的解决方案前切图片一直搞不定,最终使用了UCrop进行剪切...
继续阅读 »

一、引入



  1. Android在7.0中修改了文件权限,所以从Android7.0开始要使用FileProvider来处理uri,从网上找了好多文章,解决了在7.0下拍照及相册选图的问题,但是参照网上的解决方案前切图片一直搞不定,最终使用了UCrop进行剪切图片并返回文件地址,便于与服务器交互。

  2. 本文主要介绍在Android7.0上进行拍照,相册选图以及相应的图片剪切,当然也会向下兼容,同时我也在Android4.3的手机上进行了测试,在文章最后我会附上源码,会有我自认为详细的注释哈哈。








二、拍照及相册



  1. FileProvider

    想必FileProvider大家都很熟悉了,但是想了一下感觉还是写一下比较好。



    1. 在manifest中配置

       <application

      ... ...

      <provider
      android:name="android.support.v4.content.FileProvider"
      android:authorities="com.sdwfqin.sample.fileprovider"
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/file_paths_public"/>
      </provider>
      </application>

    2. 在 res 目录下新建文件夹 xml 然后创建资源文件 file_paths_public(名字随意,但是要和manifest中的名字匹配)

       <?xml version="1.0" encoding="utf-8"?>
      <paths>
      <!--照片-->
      <external-path
      name="my_images"
      path="Pictures"/>

      <!--下载-->
      <paths>
      <external-path
      name="download"
      path=""/>

      </paths>
      </paths>


  2. 调用相机拍照

     // 全局变量
    public static final int RESULT_CODE_1 = 201;
    // 7.0 以上的uri
    private Uri mProviderUri;
    // 7.0 以下的uri
    private Uri mUri;
    // 图片路径
    private String mFilepath = SDCardUtils.getSDCardPath() + "AndroidSamples";
    -----------
    /**
    * 拍照
    */

    private void camera() {
    File file = new File(mFilepath, System.currentTimeMillis() + ".jpg");
    if (!file.getParentFile().exists()) {
    file.getParentFile().mkdirs();
    }
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // Android7.0以上URI
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    //通过FileProvider创建一个content类型的Uri
    mProviderUri = FileProvider.getUriForFile(this, "com.sdwfqin.sample.fileprovider", file);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, mProviderUri);
    //添加这一句表示对目标应用临时授权该Uri所代表的文件
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    } else {
    mUri = Uri.fromFile(file);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);
    }
    try {
    startActivityForResult(intent, RESULT_CODE_1);
    } catch (ActivityNotFoundException anf) {
    ToastUtils.showShort("摄像头未准备好!");
    }
    }

  3. 相册选图

     // 全局变量
    public static final int RESULT_CODE_2 = 202;
    ----------
    private void selectImg() {
    Intent pickIntent = new Intent(Intent.ACTION_PICK,
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    pickIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
    startActivityForResult(pickIntent, RESULT_CODE_2);
    }

  4. onActivityResult

    需要注意的是拍照没有返回数据,用之前的uri就可以,从相册查找图片会返回uri

     case RESULT_CODE_1:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    // 调用裁剪方法
    cropRawPhoto(mProviderUri);
    } else {
    cropRawPhoto(mUri);
    }
    break;
    case RESULT_CODE_2:
    Log.i(TAG, "onActivityResult: " + data.getData());
    cropRawPhoto(data.getData());
    break;


三、图片剪裁(重点)



  1. 因为用原生的一直是各种报错,所以我这里用的是UCrop,大家可能都见过官方的展示图,界面可能在有些需求下显得过于复杂,但是真正使用起来感觉有很多都是可以修改的哈哈哈!推荐大家看一下官方的例子。项目地址:github.com/Yalantis/uC…


  2. 简单说一下引入方法但是并不能保证是最新的



    1. 依赖

       compile 'com.github.yalantis:ucrop:2.2.1'

    2. 在AndroidManifest中添加Activity

       <activity
      android:name="com.yalantis.ucrop.UCropActivity"
      android:screenOrientation="portrait"
      android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>


  3. 剪切图片

     public void cropRawPhoto(Uri uri) {

    // 修改配置参数(我这里只是列出了部分配置,并不是全部)
    UCrop.Options options = new UCrop.Options();
    // 修改标题栏颜色
    options.setToolbarColor(getResources().getColor(R.color.colorPrimary));
    // 修改状态栏颜色
    options.setStatusBarColor(getResources().getColor(R.color.colorPrimaryDark));
    // 隐藏底部工具
    options.setHideBottomControls(true);
    // 图片格式
    options.setCompressionFormat(Bitmap.CompressFormat.JPEG);
    // 设置图片压缩质量
    options.setCompressionQuality(100);
    // 是否让用户调整范围(默认false),如果开启,可能会造成剪切的图片的长宽比不是设定的
    // 如果不开启,用户不能拖动选框,只能缩放图片
    options.setFreeStyleCropEnabled(true);

    // 设置源uri及目标uri
    UCrop.of(uri, Uri.fromFile(new File(mFilepath, System.currentTimeMillis() + ".jpg")))
    // 长宽比
    .withAspectRatio(1, 1)
    // 图片大小
    .withMaxResultSize(200, 200)
    // 配置参数
    .withOptions(options)
    .start(this);
    }

  4. 剪切完图片的回掉

     if (resultCode == UCrop.RESULT_ERROR){
    mCameraTv.setText(UCrop.getError(data) + "");
    showMsg("图片剪裁失败");
    return;
    }
    if (resultCode == RESULT_OK) {
    switch (requestCode) {
    case UCrop.REQUEST_CROP:
    // 成功(返回的是文件地址)
    Log.i(TAG, "onActivityResult: " + UCrop.getOutput(data));
    mCameraTv.setText(UCrop.getOutput(data) + "");
    // 使用Glide显示图片
    Glide.with(this)
    .load(UCrop.getOutput(data))
    .crossFade()
    .into(mCameraImg);
    break;
    }
    }

  5. 完整的onActivityResult,包含拍照的回掉

     @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == UCrop.RESULT_ERROR){
    mCameraTv.setText(UCrop.getError(data) + "");
    showMsg("图片剪裁失败");
    return;
    }
    if (resultCode == RESULT_OK) {
    switch (requestCode) {
    case RESULT_CODE_1:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    cropRawPhoto(mProviderUri);
    } else {
    cropRawPhoto(mUri);
    }
    break;
    case RESULT_CODE_2:
    Log.i(TAG, "onActivityResult: " + data.getData());
    cropRawPhoto(data.getData());
    break;
    case UCrop.REQUEST_CROP:
    Log.i(TAG, "onActivityResult: " + UCrop.getOutput(data));
    mCameraTv.setText(UCrop.getOutput(data) + "");
    Glide.with(this)
    .load(UCrop.getOutput(data))
    .crossFade()
    .into(mCameraImg);
    break;
    }
    }
    }

    ```



四、源码


源码地址:github.com/sdwfqin/And…   

收起阅读 »

巨大图片显示 Subsampling Scale Image View

适用于 Android 的自定义图像视图,专为照片画廊而设计,无需OutOfMemoryErrors即可显示巨大的图像(例如地图和建筑计划)。包括捏缩放、平移、旋转和动画支持,并允许轻松扩展,因此您可以添加自己的覆盖和触摸事件检测。该视图可选地使用二次采样和图...
继续阅读 »

适用于 Android 的自定义图像视图,专为照片画廊而设计,无需OutOfMemoryErrors即可显示巨大的图像(例如地图和建筑计划)包括捏缩放、平移、旋转和动画支持,并允许轻松扩展,因此您可以添加自己的覆盖和触摸事件检测。

该视图可选地使用二次采样和图块来支持非常大的图像 - 加载低分辨率基础层,当您放大时,它会与可见区域的较小高分辨率图块重叠。这避免了在内存中保存过多数据。它非常适合显示大图像,同时允许您放大高分辨率细节。您可以禁用较小图像的平铺以及显示位图对象时。禁用平铺有一些优点和缺点,以便决定哪个最好,请参阅wiki

演示


特征

图像显示

  • 显示来自资产、资源、文件系统或位图的图像
  • 根据 EXIF 自动旋转文件系统(例如相机或图库)中的图像
  • 以 90° 为增量手动旋转图像
  • 显示源图像的一个区域
  • 在加载大图像时使用预览图像
  • 在运行时交换图像
  • 使用自定义位图解码器

启用平铺:

  • 显示巨大的图像,大于可以加载到内存中
  • 在放大时显示高分辨率细节
  • 测试高达 20,000x20,000 像素,但较大的图像速度较慢

手势检测

  • 一指平底锅
  • 两指捏合放大
  • 快速缩放(一指缩放)
  • 缩放时平移
  • 在平移和缩放之间无缝切换
  • 平移后抛出动量
  • 双击可放大和缩小
  • 禁用平移和/或缩放手势的选项

动画片

  • 为比例和中心设置动画的公共方法
  • 可定制的持续时间和缓动
  • 可选的不间断动画

可覆盖的事件检测

  • 支持OnClickListenerOnLongClickListener
  • 支持使用GestureDetector拦截事件OnTouchListener
  • 扩展以添加您自己的手势

轻松集成

  • 在 a 内使用ViewPager以创建照片库
  • 屏幕旋转后轻松恢复比例、中心和方向
  • 可以扩展以添加随图像移动和缩放的叠加图形
  • 处理视图调整大小和wrap_content布局

快速开始

1)将此库添加为应用程序的 build.gradle 文件中的依赖项。

dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
}

如果您的项目使用 AndroidX,请按如下方式更改工件名称:

dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
}

2)将视图添加到您的布局 XML。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>

3a)现在,在您的片段或活动中,设置图像资源、资产名称或文件路径。

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.resource(R.drawable.monkey));
// ... or ...
imageView.setImage(ImageSource.asset("map.png"))
// ... or ...
imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG"));

3b)或者,如果您Bitmap在内存中有一个对象,请将其加载到视图中。这不适合大图像,因为它绕过了子采样 - 您可能会得到一个OutOfMemoryError.

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.bitmap(bitmap));


github地址:https://github.com/davemorrissey/subsampling-scale-image-view

下载地址:master.zip

收起阅读 »

PhotoView 图片展示

PhotoView 旨在帮助生成一个易于使用的缩放 Android ImageView 实现。依赖将此添加到您的根build.gradle文件(不是您的模块build.gradle文件)中:allprojects { repositories { ...
继续阅读 »

PhotoView 旨在帮助生成一个易于使用的缩放 Android ImageView 实现。


依赖

将此添加到您的根build.gradle文件(不是您的模块build.gradle文件)中:

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

buildscript {
repositories {
maven { url "https://www.jitpack.io" }
}
}

然后,将库添加到您的模块中 build.gradle

dependencies {
implementation 'com.github.chrisbanes:PhotoView:latest.release.here'
}

特征

  • 开箱即用的缩放,使用多点触控和双击。
  • 滚动,平滑滚动。
  • 在滚动父级(例如 ViewPager)中使用时效果很好。
  • 允许在显示的矩阵更改时通知应用程序。当您需要根据当前缩放/滚动位置更新 UI 时很有用。
  • 允许在用户点击照片时通知应用程序。

用法

提供示例展示了如何以更高级的方式使用库,但为了完整起见,以下是让 PhotoView 工作所需的全部内容:

<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageResource(R.drawable.image);

就是这样!

视图组的问题

有一些 ViewGroups(使用 onInterceptTouchEvent 的那些)在放置 PhotoView 时抛出异常,最显着的是ViewPagerDrawerLayout这是一个尚未解决的框架问题。为了防止此异常(通常在缩小时发生),请查看HackyDrawerLayout,您可以看到解决方案是简单地捕获异常。任何使用 onInterceptTouchEvent 的 ViewGroup 也需要扩展并捕获异常。使用HackyDrawerLayout作为如何执行此操作的模板。基本实现是:

public class HackyProblematicViewGroup extends ProblematicViewGroup {

public HackyProblematicViewGroup(Context context) {
super(context);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
//uncomment if you really want to see these errors
//e.printStackTrace();
return false;
}
}
}

与 Fresco 一起使用

由于 Fresco 的复杂性,该库目前不支持 Fresco。这个项目作为一种替代解决方案。


github地址:https://github.com/Baseflow/PhotoView

下载地址:master.zip

收起阅读 »

Android agp 对 R 文件内联支持

agp
本文作者:郑超 背景 最近团队升级静态代码检测能力,依赖的相关编译检测能力需要用到较新的agp,而且目前云音乐agp版本用的是 3.5.0,对比现在 4.2.0 有较大差距,所以我们集中对 agp 进行了一次升级。在升级前通过官方文档,发现在 agp3...
继续阅读 »

本文作者:郑超



背景


最近团队升级静态代码检测能力,依赖的相关编译检测能力需要用到较新的agp,而且目前云音乐agp版本用的是 3.5.0,对比现在 4.2.0 有较大差距,所以我们集中对 agp 进行了一次升级。在升级前通过官方文档,发现在 agp3.6.0 和 4.1.0 版本分别对 R 文件的处理方式进行了相应的升级,具体升级如下。


agp 3.6.0 变更


Simplified R class generation


The Android Gradle plugin simplifies the compile classpath by generating only one R class for each library module in your project and sharing those R classes with other module dependencies. This optimization should result in faster builds, but it requires that you keep the following in mind:



  • Because the compiler shares R classes with upstream module dependencies, it’s important that each module in your project uses a unique package name.

  • The visibility of a library's R class to other project dependencies is determined by the configuration used to include the library as a dependency. For example, if Library A includes Library B as an 'api' dependency, Library A and other libraries that depend on Library A have access to Library B's R class. However, other libraries might not have access to Library B's R class If Library A uses the implementation dependency configuration. To learn more, read about dependency configurations.


从字面意思理解 agp3.6.0 简化了 R 的生成过程,每一个 module 直接生成 R.class (在 3.6.0 之前 R.class 生成的过程是为每个 module 先生成 R.java -> 再通过 javac 生成 R.class ,现在是省去了生成 R.java 和通过 javac 生成 R.class)


现在我们来验证一下这个结果,建一个工程,工程中会建立 android library module。分别用 agp3.5.0 和 agp3.6.0 编译,然后看构建产物。


agp 3.5.0 构建产物如下:


image


agp 3.6.0 构建产物如下:


image


从构建产物上来看也验证了这个结论,agp 3.5.0 到 3.6.0 通过减少 R 生成的中间过程,来提升 R 的生成效率(先生成 R.java 再通过 javac 生成 R.class 变为直接生成 R.class);


agp 4.1.0升级如下:


App size significantly reduced for apps using code shrinking


Starting with this release, fields from R classes are no longer kept by default, which may result in significant APK size savings for apps that enable code shrinking. This should not result in a behavior change unless you are accessing R classes by reflection, in which case it is necessary to add keep rules for those R classes.


从标题看 apk 包体积有显著减少(这个太有吸引力了),通过下面的描述,大致意思是不再保留 R 的 keep 规则,也就是 app 中不再包括 R 文件?(要不怎么减少包体积的)


在分析这个结果之前先介绍下 apk 中,R 文件冗余的问题;


R 文件冗余问题


android 从 ADT 14 开始为了解决多个 library 中 R 文件中 id 冲突,所以将 Library 中的 R 的改成 static 的非常量属性。


在 apk 打包的过程中,module 中的 R 文件采用对依赖库的R进行累计叠加的方式生成。如果我们的 app 架构如下:


image


编译打包时每个模块生成的 R 文件如下:



  1. R_lib1 = R_lib1;

  2. R_lib2 = R_lib2;

  3. R_lib3 = R_lib3;

  4. R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1本身的R)

  5. R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2本身的R)

  6. R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app本身R)


在最终打成 apk 时,除了 R_app(因为 app 中的 R 是常量,在 javac 阶段 R 引用就会被替换成常量,所以打 release 混淆时,app 中的 R 文件会被 shrink 掉),其余的 R 文件全部都会打进 apk 包中。这就是 apk 中 R 文件冗余的由来。而且如果项目依赖层次越多,上层的业务组件越多,将会导致 apk 中的 R 文件将急剧的膨胀。


R 文件内联(解决冗余问题)


系统导致的冗余问题,总不会难住聪明的程序员。在业内目前已经有一些R文件内联的解决方案。大致思路如下:



由于 R_app 是包括了所有依赖的的 R,所以可以自定义一个 transform 将所有 library module 中 R 引用都改成对 R_app 中的属性引用,然后删除所有依赖库中的 R 文件。这样在 app 中就只有一个顶层 R 文件。(这种做法不是非常彻底,在 apk 中仍然保留了一个顶层的 R,更彻底的可以将所有代码中对 R 的引用都替换成常量,并在 apk 中删除顶层的 R )



agp 4.1.0 R 文件内联


首先我们分别用 agp 4.1.0 和 agp 3.6.0 构建 apk 进行一个对比,从最终的产物来确认下是否做了 R 文件内联这件事。 测试工程做了一些便于分析的配置,配置如下:



  1. 开启 proguard


buildTypes {
release {
minifyEnabled true // 打开
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}


  1. 关闭混淆,仅保留压缩和优化(避免混淆打开,带来的识别问题)


// proguard-rules.pro中配置
-dontobfuscate

构建 release 包。 先看下 agp 3.6.0 生成的 apk:


image


从图中可以看到 bizlib module 中会有 R 文件,查看 SecondActivity 的 byte code ,会发现内部有对 R 文件的引用。


接着再来看 agp 4.1.0 生成的 apk:


image


可以看到,bizlib module 中已经没有 R 文件,并且查看 SecondActivity 的 byte code ,会发现内部的引用已经变成了一个常量。


由此可以确定,agp 4.1.0 是做了对 R 文件的内联,并且做的很彻底,不仅删除了冗余的 R 文件,并且还把所有对 R 文件的引用都改成了常量。


具体分析


现在我们来具体分析下 agp 4.1.0 是如何做到 R 内联的,首先我们大致分析下,要对 R 做内联,基本可以猜想到是在 class 到 dex 这个过程中做的。确定了大致阶段,那接下看能不能从构建产物来缩小相应的范围,最好能精确到具体的 task。(题外话:分析编译相关问题一般四板斧:1. 先从 app 的构建产物里面分析相应的结果;2.涉及到有依赖关系分析的可以将所有 task 的输入输出全部打印出来;3. 1、2满足不了时,会考虑去看相应的源码;4. 最后的大招就是调试编译过程;)


首先我们看下构建产物里面的 dex,如下图:


image


接下来在 app module 中增加所有 task 输入输出打印的 gradle 脚本来辅助分析,相关脚本如下:


gradle.taskGraph.afterTask { task ->
try {
println("---- task name:" + task.name)
println("-------- inputs:")
task.inputs.files.each { it ->
println(it.absolutePath)
}
println("-------- outputs:")
task.outputs.files.each { it ->
println(it.absolutePath)
}
} catch (Exception e) {

}
}

minifyReleaseWithR8 相应的输入输出如下:


image


从图中可以看出,输入有整个 app 的 R 文件的集合(R.jar),所以基本明确 R 的内联就是在 minifyReleaseWithR8 task 中处理的。


接下来我们就具体分析下这个 task。 具体的逻辑在 R8Task.kt 里面.


创建 minifyReleaseWithR8 task 代码如下:


class CreationAction(
creationConfig: BaseCreationConfig,
isTestApplication: Boolean = false
) : ProguardConfigurableTask.CreationAction<R8Task, BaseCreationConfig>(creationConfig, isTestApplication) {
override val type = R8Task::class.java
// 创建 minifyReleaseWithR8 task
override val name = computeTaskName("minify", "WithR8")
.....
}

task 执行过程如下(由于代码过多,下面仅贴出部分关键节点):


    // 1. 第一步,task 具体执行
override fun doTaskAction() {
......
// 执行 shrink 操作
shrink(
bootClasspath = bootClasspath.toList(),
minSdkVersion = minSdkVersion.get(),
......
)
}

// 2. 第二步,调用 shrink 方法,主要做一些输入参数和配置项目的准备
companion object {
fun shrink(
bootClasspath: List<File>,
......
)
{
......
// 调用 r8Tool.kt 中的顶层方法,runR8
runR8(
filterMissingFiles(classes, logger),
output.toPath(),
......
)
}
// 3. 第三步,调用 R8 工具类,执行混淆、优化、脱糖、class to dex 等一系列操作
fun runR8(
inputClasses: Collection<Path>,
......
)
{
......
ClassFileProviderFactory(libraries).use { libraryClasses ->
ClassFileProviderFactory(classpath).use { classpathClasses ->
r8CommandBuilder.addLibraryResourceProvider(libraryClasses.orderedProvider)
r8CommandBuilder.addClasspathResourceProvider(classpathClasses.orderedProvider)
// 调用 R8 工具类中的run方法
R8.run(r8CommandBuilder.build())
}
}
}

至此可以知道实际上 agp 4.1.0 中是通过 R8 来做到 R 文件的内联的。那 R8 是如果做到的呢?这里简要描述下,不再做具体代码的分析:



R8 从能力上是包括了 Proguard 和 D8(java脱糖、dx、multidex),也就是从 class 到 dex 的过程,并在这个过程中做了脱糖、Proguard 及 multidex 等事情。在 R8 对代码做 shrink 和 optimize 时会将代码中对常量的引用替换成常量值。这样代码中将不会有对 R 文件的引用,这样在 shrink 时就会将 R 文件删除。



当然要达到这个效果 agp 在 4.1.0 版本里面对默认的 keep 规则也要做一些调整,4.1.0 里面删除了默认对 R 的 keep 规则,相应的规则如下:



-keepclassmembers class **.R$* {
public static <fields>;
}


总结



  1. 从 agp 对 R 文件的处理历史来看,android 编译团队一直在对R文件的生成过程不断做优化,并在 agp 4.1.0 版本中彻底解决了 R 文件冗余的问题。


  2. 编译相关问题分析思路:



    1. 先从 app 的构建产物里面分析相应的结果;

    2. 涉及到有依赖关系分析的可以将所有 task 的输入输出全部打印出来;

    3. 1、2满足不了时,会考虑去看相应的源码;

    4. 最后的大招就是调试编译过程;


  3. 从云音乐 app 这次 agp 升级的效果来看,app 的体积降低了接近 7M,编译速度也有很大的提升,特别是 release 速度快了 10 分钟+(task 合并),整体收益还是比较可观的。



文章中使用的测试工程


参考资料



  1. Shrink, obfuscate, and optimize your app

  2. r8

  3. Android Gradle plugin release notes



本文发布自 网易云音乐大前端团队,文章未经授权禁止任何形式的转载。我们常年招收前端、iOS、Android,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!


收起阅读 »

教你使用whistle工具捉小程序包

介绍 我们说起捉包工具,可能大家比较熟悉的Fiddler工具,它是通过断点修改请求响应的方式,平时使用测试捉包也是很方便的,今天主角介绍另一个捉包工具whistle,这个工具比较轻,无需安装客户端只需通过终端node即可跑起捉取数据 whistle用的是类似...
继续阅读 »

介绍



  • 我们说起捉包工具,可能大家比较熟悉的Fiddler工具,它是通过断点修改请求响应的方式,平时使用测试捉包也是很方便的,今天主角介绍另一个捉包工具whistle,这个工具比较轻,无需安装客户端只需通过终端node即可跑起捉取数据

  • whistle用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能,更多内容介绍请查看官方文档


安装



  1. 安装node 文档地址


$ node -v  // 查看node版本号
v12.0.0 //(建议12版本以上,不然手机捉包会有点问题)


  1. 安装whistle


npm install -g whistle
或者直接指定镜像安装:
npm install whistle -g --registry=https://registry.npm.taobao.org


  1. 使用whistle

    • 启动whistle: (以下指令,window系统不需要$符号)


    $ w2 start


    • 重启whsitle:


    $ w2 restart


    • 停止whistle:3


    $ w2 stop


    • 调试模式启动whistle(主要用于查看whistle的异常及插件开发):


    $ w2 run

    w2 start启动完即可查看本地ip,把ip拷贝到浏览器即可


image.png
在浏览器显示效果
image.png
4. 配置代理 更多配置请查看官方文档

抓取 Https 请求需要配置



  • 电脑上安装根证书(现在安装证书也没那么麻烦,下载完直接点安装一步下一步就行)


   下载根证书:Whistle 监控界面 -> HTTPS -> Download RootCA

   下载完根证书后点击rootCA.crt文件,弹出根证书安装对话框。

   Windows 安装方法:

image.png



  • 移动端需要在设置中配置当前Wi-Fi的代理,以 harmonyOS 为例:


image.png



  • 手机上安装根证书


   iOS:

   Safari 地址栏输入 rootca.pro,按提示安装证书。  

   iOS 10.3 之后需要手动信任自定义根证书,设置路径:设置 --> 通用 --> 关于本机 --> 证书信任设置


   Android:

   用浏览器扫描 whistle 监控界面 HTTPS 的二维码下载安装,或者浏览器地址栏 rootca.pro 按提示安装。

   ca 证书安装完后可以在 Android 手机的“设置” -》“安全和隐私” -》“受信任的凭证” 里查看手机上有没有安装成功。

   部分浏览器不会自动识别 ca 证书,可以通过 Android Chrome 来完成安装。



  • 电脑选择配置勾选捉取https:


image.png



  • 最后捉取得效果图:


image.png


总结



  • 其实使用并不难,按上面安装步骤来即可,这个捉包方法适用于捉取小程序体验版或测试版,不支持小程序正式版本,如果打开小程序正式版本,整个小程序请求接口都会异常无法请求;如果你的体验版小程序无法捉取,请尝试打开调试工具;(本文仅限学习,方便测试使用,还有更多好玩的东西,请移步到官方文档

链接:https://juejin.cn/post/6986888917622456351

收起阅读 »

面试了十几个高级前端,竟然连(扁平数据结构转Tree)都写不出来

前言 招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。 我们看下题目:打平的数据内容如下: let arr = [ {id: 1, name: '部门1...
继续阅读 »

前言


招聘季节一般都在金三银四,或者金九银十。最近在这五六月份,陆陆续续面试了十几个高级前端。有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。


我们看下题目:打平的数据内容如下:


let arr = [
{id: 1, name: '部门1', pid: 0},
{id: 2, name: '部门2', pid: 1},
{id: 3, name: '部门3', pid: 1},
{id: 4, name: '部门4', pid: 3},
{id: 5, name: '部门5', pid: 4},
]

输出结果


[
{
"id": 1,
"name": "部门1",
"pid": 0,
"children": [
{
"id": 2,
"name": "部门2",
"pid": 1,
"children": []
},
{
"id": 3,
"name": "部门3",
"pid": 1,
"children": [
// 结果 ,,,
]
}
]
}
]

我们的要求很简单,可以先不用考虑性能问题。实现功能即可,回头分析了面试的情况,结果使我大吃一惊。


10%的人没思路,没碰到过这种结构


60%的人说用过递归,有思路,给他个笔记本,但就是写不出来


20%的人在引导下,磕磕绊绊能写出来


剩下10%的人能写出来,但性能不是最佳


感觉不是在招聘季节遇到一个合适的人真的很难。


接下来,我们用几种方法来实现这个小算法


什么是好算法,什么是坏算法


判断一个算法的好坏,一般从执行时间占用空间来看,执行时间越短,占用的内存空间越小,那么它就是好的算法。对应的,我们常常用时间复杂度代表执行时间,空间复杂度代表占用的内存空间。


时间复杂度



时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。



随着n的不断增大,时间复杂度不断增大,算法花费时间越多。 常见的时间复杂度有



  • 常数阶O(1)

  • 对数阶O(log2 n)

  • 线性阶O(n)

  • 线性对数阶O(n log2 n)

  • 平方阶O(n^2)

  • 立方阶O(n^3)

  • k次方阶O(n^K)

  • 指数阶O(2^n)


计算方法



  1. 选取相对增长最高的项

  2. 最高项系数是都化为1

  3. 若是常数的话用O(1)表示


举个例子:如f(n)=3*n^4+3n+300 则 O(n)=n^4


通常我们计算时间复杂度都是计算最坏情况。计算时间复杂度的要注意的几个点



  • 如果算法的执行时间不随n增加增长,假如算法中有上千条语句,执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。 举例如下:代码执行100次,是一个常数,复杂度也是O(1)


    let x = 1;
while (x <100) {
x++;
}


  • 多个循环语句时候,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的方法决定的。举例如下:在下面for循环当中,外层循环每执行一次内层循环要执行n次,执行次数是根据n所决定的,时间复杂度是O(n^2)


  for (i = 0; i < n; i++){
for (j = 0; j < n; j++) {
// ...code
}
}


  • 循环不仅与n有关,还与执行循环判断条件有关。举例如下:在代码中,如果arr[i]不等于1的话,时间复杂度是O(n)。如果arr[i]等于1的话,循环不执行,时间复杂度是O(0)


    for(var i = 0; i<n && arr[i] !=1; i++) {
// ...code
}

空间复杂度



空间复杂度是对一个算法在运行过程中临时占用存储空间的大小。



计算方法:



  1. 忽略常数,用O(1)表示

  2. 递归算法的空间复杂度=(递归深度n)*(每次递归所要的辅助空间)


计算空间复杂度的简单几点



  • 仅仅只复制单个变量,空间复杂度为O(1)。举例如下:空间复杂度为O(n) = O(1)。


   let a = 1;
let b = 2;
let c = 3;
console.log('输出a,b,c', a, b, c);


  • 递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1) = O(n)。


    function fun(n) {
let k = 10;
if (n == k) {
return n;
} else {
return fun(++n)
}
}

不考虑性能实现,递归遍历查找


主要思路是提供一个递getChildren的方法,该方法递归去查找子集。
就这样,不用考虑性能,无脑去查,大多数人只知道递归,就是写不出来。。。


/**
* 递归查找,获取children
*/
const getChildren = (data, result, pid) => {
for (const item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []};
result.push(newItem);
getChildren(data, newItem.children, item.id);
}
}
}

/**
* 转换方法
*/
const arrayToTree = (data, pid) => {
const result = [];
getChildren(data, result, pid)
return result;
}

从上面的代码我们分析,该实现的时间复杂度为O(2^n)


不用递归,也能搞定


主要思路是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //

// 先转成map存储
for (const item of items) {
itemMap[item.id] = {...item, children: []}
}

for (const item of items) {
const id = item.id;
const pid = item.pid;
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n),需要一个Map把数据存储起来,空间复杂度O(n)


最优性能


主要思路也是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储。不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。


function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //
for (const item of items) {
const id = item.id;
const pid = item.pid;

if (!itemMap[id]) {
itemMap[id] = {
children: [],
}
}

itemMap[id] = {
...item,
children: itemMap[id]['children']
}

const treeItem = itemMap[id];

if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}

}
return result;
}

从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为O(n),需要一个Map把数据存储起来,空间复杂度O(n)


链接:https://juejin.cn/post/6983904373508145189

收起阅读 »

前端是不是又要回去操作真实dom年代?

写在开头 近期我有写两篇文章,一篇是:petite-vue源码解析和掘金编辑器的源码解析,发现里面用到了Svelte这个框架 加上最近React17,vite大家也在逐步的用在生产环境中,我于是有了今天的思考 看前端的技术演进 原生Javascript ...
继续阅读 »

写在开头



  • 近期我有写两篇文章,一篇是:petite-vue源码解析和掘金编辑器的源码解析,发现里面用到了Svelte这个框架

  • 加上最近React17,vite大家也在逐步的用在生产环境中,我于是有了今天的思考


看前端的技术演进



  • 原生Javascript - Jquery为代表的时代,例如,引入Jquery只要


<script src="cdn/jquery.min,js"></script>


  • 接着便又有了gulp webpack等构建工具出现,React和Vue也在这个时候开始火了起来,随即而来的是一大堆工程化的辅助工具,例如babel,还有提供整套服务的create-react-app等脚手架

  • 这也带来了问题,当然这个是npm的问题,每次启动项目前,都要安装大量的依赖,即便出现了yarn pnpm`等优化的依赖管理工具,但是这个问题根源不应该使用工具解决,而是问题本质是依赖本地化,代码和依赖需要工具帮助才能运行在浏览器中



总结就是:现有的开发模式,让项目太重,例如我要使用某个脚手架,我只想写一个helloworld演示下,结果它让我装500mb的依赖,不同的脚手架产物,配置不同,产物也不同



理想的开发模式




  • 1.不需要辅助的工具配置,我不需要webpack这类帮我打包的工具,模块化浏览器本身就支持,而且是一个规范。例如vite号称不打包,用的是浏览器本身支持的esm模块化,但是它没有解决依赖的问题,因为依赖问题本身是依赖的问题,而不是工具的问题




  • 2.不需要安装依赖,一切都可以import from remote,我觉得webpack5Module Federation设计,就考虑到了这一点,下面是官方的解释:




    • 多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。




    • 这通常被称作微前端,但并不仅限于此。







但是这可能并不是最佳实践,目前是有import from http,例如



import lodash from 'https://unpackage/lodash/es'


  • 这里又会有人问,那你不都是要发请求吗,都是要每次启动的时候去远程拉取,还不如在本地呢。import from http我想只是解决了一个点的问题,就是不用手动安装依赖到本地磁盘

  • 前段时间我写过,在浏览器中本地运行Node.js




这个技术叫WebContainers技术,感兴趣的可以去翻翻我公众号之前的文章




  • 等等,别急。这些仅仅开了个头,新的技术往往要探索才能实现价值最大化,我想此处应该可以彻底颠覆现有的开发模式,而且应该就在3-5年内。


将几个新的前端技术理念融合?



  • vite的不打包理念:直接使用浏览器支持的esm模块化

  • WebContainers技术:让浏览器直接运行node.js

  • import from remote,从一个个远程地址直接引入可以使用的依赖

  • 现在很火的webIDE:类似remix编辑器,直接全部可以在云端搞定

  • 浏览器的优化,天然有缓存支持


会发生什么变化?



  • 我们所有的一切开始,都直接启动一个浏览器即可

  • 浏览器中的webIDE,可以直接引入远程依赖,浏览器可以运行Node.js,使用的都是esm模块化,不需要打包工具,项目启动的时间和热更新时间都非常短,构建也是直接可以在浏览器中构建



这些看似解决了我们之前提出的大部分问题,回到今天的主题





回到主题



  • 前端会不会回到操作原生dom的时代?

  • 我觉得,有这个趋势,例如petite-vue,还有Svelte



因为之前写过petite-vue源码解析了,我们今天就讲讲Svelte



Svelte



Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 在浏览器中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。




  • 与使用虚拟(virtual)DOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM






  • 上面是官方的介绍,我们看看知乎这篇文章https://zhuanlan.zhihu.com/p/97825481,感觉他写得很好,这里照搬一些过来吧直接




  • React和Vue都是基于runtime的框架。所谓基于runtime的框架就是框架本身的代码也会被打包到最终的bundle.js并被发送到用户浏览器。




  • 当用户在你的页面进行各种操作改变组件的状态时,框架的runtime会根据新的组件状态(state)计算(diff)出哪些DOM节点需要被更新





可是,这些被打包进去的框架,实在太大了。



(今天还在跟同事说,前年写的登录站点,纯原生手工打造,性能无敌)



  • 100kb对于一个弱网环境来说,很要命,我们看看svelte减少了多少体积:



科普



  • 虚拟dom并没有加快用户操作浏览器响应的速度,只是说,方便用于数据驱动视图,更便于管理而已,并且在一定程度上,更慢。真正最快的永远是:


currentDom.innerHtml = '前端巅峰';


所以Svelte并不是说多好,而是它的这种理念,可能未来会越来越成为主流



React17的改变



  • 大家应该都知道,现有的浏览器都是无法直接解译JSX的,所以大多数React用户都需要使用Babel或者TypeScript之类的编译器来将JSX转换为浏览器能够理解的JavaScript语言。许多预配置的工具箱(如:Create React App 或者Next.js)内部也有JSX的转换。

  • React 17.0,尽管React团队想对JSX的转换进行改进,但React团队不想打破现有的配置。这就是为什么React团队与Babel合作,为想要升级的开发者提供了一个全新的JSX转换的重写版本。

  • 通过全新的转换,你可以单独使用JSX而无需引入React.



我猜想,或许React团队有意将jsx语法推动到成为es标准语法中去,剥离开来希望会大大提升。



重点



  • 说了这么多,大家可能没理解到重点,那就是:大家都在想着减轻自身的负重,把丢下来的东西标准化,交给浏览器处理,这也是在为未来的只需要打开一个浏览器,就可以完成所有的事情做铺垫

  • 而我,相信这一天应该不远了,据我所知已经有不少顶尖的团队在研发这种产品



链接:https://juejin.cn/post/6986613468975595556

收起阅读 »

面试官:你知道git xx 和git xx的区别吗?看完这篇Git指南后直呼:内行!

Git
前言 作为一名工程师,既然想要加入一个团队,并肩作战地协同开发项目,就必不可少要学会Git基本操作。面试过程中,面试官不止是考察1-3年的工程师,高级岗位也同样会考察团队协作的能力。相信小伙伴们经常会在面试中被问到以下问题吧,可以帮助你测试一下你的Git基础牢...
继续阅读 »

前言


作为一名工程师,既然想要加入一个团队,并肩作战地协同开发项目,就必不可少要学会Git基本操作。面试过程中,面试官不止是考察1-3年的工程师,高级岗位也同样会考察团队协作的能力。相信小伙伴们经常会在面试中被问到以下问题吧,可以帮助你测试一下你的Git基础牢不牢固。



  • 代码开发到一半,需要紧急切换分支修复线上问题,该怎么办?

  • 代码合并有几种模式?分别有什么优缺点?

  • git fetchgit pull有什么区别,有合并操作吗?

  • git mergegit rebase有什么区别,它们的应用场景有哪些?

  • git resetgit revert有什么区别,该如何选择,回滚后的<commit-id>还能找到吗?


如果你心中已有答案,那么可以选择跳过这篇文章啦,愉快地继续摸鱼~


如果你对这些概念还有些模糊,或者没有实际操作过,那么就需要好好阅读本篇文章啦!


接下来马上进入正文啦,本文分四个部分介绍,大家可以自由选择阅读。



  • Git的区域示例图,帮助大家理解Git的结构。

  • Git的基本使用场景,介绍一些常用git命令。

  • Git的进阶使用场景,介绍一些高频出现的面试题目以及应用场景。

  • 最后介绍Git的下载地址、基本配置和工具推荐。


Git的区域


画了一个简单的示意图,供大家参考


yuque_diagram.jpg



  • 远程仓库(Remote):在远程用于存放代码的服务器,远程仓库的内容能够被分布其他地方的本地仓库修改。

  • 本地仓库(Repository):在自己电脑上的仓库,平时我们用git commit 提交到暂存区,就会存入本地仓库。

  • 暂存区(Index):执行 git add 后,工作区的文件就会被移入暂存区,表示哪些文件准备被提交,当完成某个功能后需要提交代码,可以通过 git add 先提交到暂存区。

  • 工作区(Workspace):工作区,开发过程中,平时打代码的地方,看到是当前最新的修改内容。


Git的基本使用场景


以下命令远程主机名默认为origin,如果有其他远程主机,将origin替换为其他即可。


git fetch


# 获取远程仓库特定分支的更新
git fetch origin <分支名>

# 获取远程仓库所有分支的更新
git fetch --all

git pull


# 从远程仓库拉取代码,并合并到本地,相当于 git fetch && git merge 
git pull origin <远程分支名>:<本地分支名>

# 拉取后,使用rebase的模式进行合并
git pull --rebase origin <远程分支名>:<本地分支名>

注意



  • 直接git pull 不加任何选项,等价于git fetch + git merge FETCH_HEAD,执行效果就是会拉取所有分支信息回来,但是只合并当前分支的更改。其他分支的变更没有执行合并。

  • 使用git pull --rebase 可以减少冲突的提交点,比如我本地已经提交,但是远程其他同事也有新的代码提交记录,此时拉取远端其他同事的代码,如果是merge的形式,就会有一个merge的commit记录。如果用rebase,就不会产生该合并记录,是将我们的提交点挪到其他同事的提交点之后。


git branch


# 基于当前分支,新建一个本地分支,但不切换
git branch <branch-name>

# 查看本地分支
git branch

# 查看远程分支
git branch -r

# 查看本地和远程分支
git branch -a

# 删除本地分支
git branch -D <branch-name>

# 基于旧分支创建一个新分支
git branch <new-branch-name> <old-branch-name>

# 基于某提交点创建一个新分支
git branch <new-branch-name> <commit-id>

# 重新命名分支
git branch -m <old-branch-name> <new-branch-name>

git checkout


# 切换到某个分支上
git checkout <branch-name>

# 基于当前分支,创建一个分支并切换到新分支上
git checkout -b <branch-name>

git add


# 添把当前工作区修改的文件添加到暂存区,多个文件可以用空格隔开
git add xxx

# 添加当前工作区修改的所有文件到暂存区
git add .

git commit


# 提交暂存区中的所有文件,并写下提交的概要信息
git commit -m "message"

# 相等于 git add . && git commit -m
git commit -am

# 对最近一次的提交的信息进行修改,此操作会修改commit的hash值
git commit --amend

git push


# 推送提交到远程仓库
git push

# 强行推送到远程仓库
git push -f

git tag


# 查看所有已打上的标签
git tag

# 新增一个标签打在当前提交点上,并写上标签信息
git tag -a <version> -m 'message'

# 为指定提交点打上标签
git tag -a <version> <commit-id>

# 删除指定标签
git tag -d <version>

Git的进阶使用场景



HEAD表示最新提交 ;HEAD^表示上一次; HEAD~n表示第n次(从0开始,表示最近一次)



正常协作



  • git pull 拉取远程仓库的最新代码

  • 工作区修改代码,完成功能开发

  • git add . 添加修改的文件到暂存区

  • git commit -m 'message' 提交到本地仓库

  • git push将本地仓库的修改推送到远程仓库


代码合并


git merge


自动创建一个新的合并提交点merge-commit,且包含两个分支记录。如果合并的时候遇到冲突,仅需要修改解决冲突后,重新commit。



  • 场景:如dev要合并进主分支master,保留详细的合并信息

  • 优点:展示真实的commit情况

  • 缺点:分支杂乱


git checkout master
git merge dev

rf1o2b6eduboqwkigg3w.gif


git merge 的几种模式



  • git merge --ff (默认--ff,fast-farward)

    • 结果:被merge的分支和当前分支在图形上并为一条线,被merge的提交点commit合并到当前分支,没有新的提交点merge

    • 缺点:代码合并不冲突时,默认快速合并,主分支按时间顺序混入其他分支的零碎commit点。而且删除分支,会丢失分支信息。



  • git merge --no-ff(不快速合并、推荐)

    • 结果:被merge的分支和当前分支不在一条线上,被merge的提交点commit还在原来的分支上,并在当前分支产生一个新提交点merge

    • 优点:代码合并产生冲突就会走这个模式,利于回滚整个大版本(主分支自己的commit点)



  • git merge --squash(把多次分支commit历史压缩为一次)

    • 结果:把多次分支commit历史压缩为一次




image.png


git rebase



  • 不产生merge commit,变换起始点位置,“整理”成一条直线,且能使用命令合并多次commit。

  • 如在develop上git rebase master 就会拉取到master上的最新代码合并进来,也就是将分支的起始时间指向master上最新的commit上。自动保留的最新近的修改,不会遇到合并冲突。而且可交互操作(执行合并删除commit),可通过交互式变基来合并分支之前的commit历史git rebase -i HEAD~3

  • 场景:主要发生在个人分支上,如 git rebase master整理自己的dev变成一条线。频繁进行了git commit提交,可用交互操作drop删除一些提交,squash提交融合前一个提交中。

  • 优点:简洁的提交历史

  • 缺点:发生错误难定位,解决冲突比较繁琐,要一个一个解决。


git checkout dev
git rebase master

dwyukhq8yj2xliq4i50e.gifmsofpv7k6rcmpaaefscm.gif


git merge和git rebase的区别



  • merge会保留两个分支的commit信息,而且是交叉着的,即使是ff模式,两个分支的commit信息会混合在一起(按真实提交时间排序),多用于自己dev合并进master。

  • rebase意思是变基,改变分支的起始位置,在dev上git rebase master,将dev的多次commit一起拉到要master最新提交的后面(时间最新),变成一条线,多用于整理自己的dev提交历史,然后把master最新代码合进来。

  • 使用rebase还是merge更多的是管理风格的问题,有个较好实践:

    • 就是dev在merge进主分支(如master)之前,最好将自己的dev分支给rebase到最新的主分支(如master)上,然后用pull request创建普通merge请求。

    • 用rebase整理成重写commit历史,所有修改拉到master的最新修改前面,保证dev运行在当前最新的主branch的代码。避免了git历史提交里无意义的交织。



  • 假设场景:从 dev 拉出分支 feature-a。

    • 那么当 dev 要合并 feature-a 的内容时,使用 git merge feature-a

    • 反过来当 feature-a 要更新 dev 的内容时,使用 git rebase dev



  • git merge和git rebase 两者对比图

    • git merge图示 image.png

    • git rebase图示 image.png




取消合并


# 取消merge合并
git merge --abort
# 取消rebase合并
git rebase --abort

代码回退


代码回退的几种方式



  • git checkout

  • git reset

    • --hard:硬重置,影响【工作区、暂存区、本地仓库】

    • --mixed:默认,影响【暂存区、本地仓库】,被重置的修改内容还留在工作区

    • --soft:软重置,影响 【本地仓库】,被重置的修改内容还留在工作区和暂存区



  • git revert


# 撤回工作区该文件的修改,多个文件用空格隔开
git checkout -- <file-name>
# 撤回工作区所有改动
git checkout .

# 撤回已经commit到暂存区的文件
git reset <file-name>
# 撤回已经commit到暂存区的所有文件
git reset .
# 丢弃已commit的其他版本,hard参数表示同时重置工作区的修改
git reset --hard <commit-id>
# 回到上一个commit的版本,hard参数表示同时重置工作区的修改
git reset --hard HEAD^

# 撤销0ffaacc这次提交
git revert 0ffaacc
# 撤销最近一次提交
git revert HEAD
# 撤销最近2次提交,注意:数字从0开始
git revert HEAD~1

# 回退后要执行强制推送远程分支
git push -f

git reset和git revert的区别



  • reset是根据来移动HEAD指针,在该次提交点后面的提交记录会丢失。


hlh0kowt3hov1xhcku38.gif



  • revert会产生新的提交,来抵消选中的该次提交的修改内容,可以理解为“反做”,不会丢失中间的提交记录。


3kkd2ahn41zixs12xgpf.gif



  • 使用建议

    • 公共分支回退使用git revert,避免丢掉其他同事的提交。

    • 自己分支回退可使用git reset,也可以使用git revert,按需使用。




挑拣代码


git cherry-pick



  • “挑拣”提交,单独抽取某个分支的一个提交点,将这个提交点的所有修改内容,搬运到你的当前分支。

  • 如果我们只想将其他分支的某个提交点合并进来,不想用git merge将所有提交点合并进来,就需要使用这个git cherry-pick


git cherry-pick <commit-id>

2dkjx4yeaal10xyvj29v.gif


暂存代码


git stash



  • 当我们想要切换去其他分支修复bug,此时当前的功能代码还没修改完整,不想commit,就需要暂存当前修改的文件,然后切换到hotfix分支修复bug,修复完成再切换回来,将暂存的修改提取出来,继续功能开发。

  • 还有另一种场景就是,同事在远程分支上推送了代码,此时拉下来有冲突,可以将我们自己的修改stash暂存起来,然后先拉最新的提交代码,再pop出来,这样可以避免一个冲突的提交点。


# 将本地改动的暂存起来
git stash
# 将未跟踪的文件暂存(另一种方式是先将新增的文件添加到暂存区,使其被git跟踪,就可以直接git stash)
git stash -u
# 添加本次暂存的备注,方便查找。
git stash save "message"
# 应用暂存的更改
git stash apply
# 删除暂存
git stash drop
# 应用暂存的更改,然后删除该暂存,等价于git stash apply + git stash drop
git stash pop
# 删除所有缓存
git stash clear

打印日志



  1. git log


可以显示所有提交过的版本信息,如果感觉太繁琐,可以加上参数  --pretty=oneline,只会显示版本号和提交时的备注信息。



  1. git reflog


git reflog 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作),例如执行 git reset --hard HEAD~1,退回到上一个版本,用git log是看不出来被删除的,用git reflog则可以看到被删除的,我们就可以买后悔药,恢复到被删除的那个版本。


Git的下载、配置、工具推荐



  • Git下载地址


  • 两种拉取代码的方式

    • https:每次都要手动输入用户名和密码

    • ssh :自动使用本地私钥+远程的公钥验证是否为一对秘钥



  • 配置ssh

    • ssh-keygen -t rsa -C "邮箱地址"

    • cd ~/.ssh切换到home下面的ssh目录、cat id_rsa.pub命令查看公钥的内容,然后复制

    • github的settings -> SSH and GPG keys-> 复制刚才的内容贴入 -> Add SSH key

    • 全局配置一下Git用户名和邮箱

      • git config --global user.name "xxx"

      • git config --global user.email "xxx@xx.com"

      • image.png





  • Git 相关工具推荐

    • 图形化工具 SourceTree :可视化执行git命令,解放双手

    • VSCode插件 GitLens:可以在每行代码查看对应git的提交信息,而且提供每个提交点的差异对比




结尾


阅读到这里,是不是感觉对Git相关概念更加清晰了呢,那么恭喜你,再也不怕因为误操作,丢失同事辛辛苦苦写的代码了,而且将在日常工作的协同中游刃有余。



  • 💖建议收藏文章,工作中有需要的时候翻出来看一看~

  • 📃创作不易,如果我的文章对你有帮助,辛苦大佬们点个赞👍🏻,支持我一下~

  • 📌如果有错漏,欢迎大佬们指正~

  • 👏欢迎转载分享,请注明出处,谢谢~

链接:https://juejin.cn/post/6986868722136776718

收起阅读 »

为了让她10分钟入门canvas,我熬夜写了3个小项目和这篇文章

1. canvas实现时钟转动 实现以下效果,分为几步: 1、找到canvas的中心,画出表心,以及表框 2、获取当前时间,并根据时间画出时针,分针,秒针,还有刻度 3、使用定时器,每过一秒获取新的时间,并重新绘图,达到时钟转动的效果 1.1 表心,表框...
继续阅读 »

image.png


1. canvas实现时钟转动


实现以下效果,分为几步:



  • 1、找到canvas的中心,画出表心,以及表框

  • 2、获取当前时间,并根据时间画出时针,分针,秒针,还有刻度

  • 3、使用定时器,每过一秒获取新的时间,并重新绘图,达到时钟转动的效果


截屏2021-07-19 下午8.52.15.png


1.1 表心,表框


画表心,表框有两个知识点:



  • 1、找到canvas的中心位置

  • 2、绘制圆形


//html

<canvas id="canvas" width="600" height="600"></canvas>

// js

// 设置中心点,此时300,300变成了坐标的0,0
ctx.translate(300, 300)
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
// 执行画线段的操作stroke
ctx.stroke()

让我们来看看效果,发现了,好像不对啊,我们是想画两个独立的圆线,怎么画出来的两个圆连到一起了


截屏2021-07-19 下午9.10.07.png
原因是:上面代码画连个圆时,是连着画的,所以画完大圆后,线还没斩断,就接着画小圆,那肯定会大圆小圆连一起,解决办法就是:beginPath,closePath


ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0

// 画大圆
+ ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
+ ctx.closePath()

// 画小圆
+ ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
+ ctx.closePath()

1.2 时针,分针,秒针


画这三个指针,有两个知识点:



  • 1、根据当前时,分,秒计算角度

  • 2、在计算好的角度上去画出时针,分针,秒针


如何根据算好的角度去画线呢,比如算出当前是3点,那么时针就应该以12点为起始点,顺时针旋转2 * Math.PI / 12 * 3 = 90°,分针和秒针也是同样的道理,只不过跟时针不同的是比例问题而已,因为时在表上有12份,而分针和秒针都是60份


截屏2021-07-19 下午10.07.19.png


这时候又有一个新问题,还是以上面的例子为例,我算出了90°,那我们怎么画出时针呢?我们可以使用moveTo和lineTo去画线段。至于90°,我们只需要将x轴顺时针旋转90°,然后再画出这条线段,那就得到了指定角度的指针了。但是上面说了,是要以12点为起始点,我们的默认x轴确是水平的,所以我们时分秒针算出角度后,每次都要减去90°。可能这有点绕,我们通过下面的图演示一下,还是以上面3点的例子:


截屏2021-07-19 下午10.30.23.png


截屏2021-07-19 下午10.31.02.png
这样就得出了3点指针的画线角度了。


又又又有新问题了,比如现在我画完了时针,然后我想画分针,x轴已经在我画时针的时候偏转了,这时候肯定要让x轴恢复到原来的模样,我们才能继续画分针,否则画出来的分针是不准的。这时候save和restore就派上用场了,save是把ctx当前的状态打包压入栈中,restore是取出栈顶的状态并赋值给ctxsave可多次,但是restore取状态的次数必须等于save次数


截屏2021-07-19 下午10.42.06.png


懂得了上面所说,剩下画刻度了,起始刻度的道理跟时分秒针道理一样,只不过刻度是死的,不需要计算,只需要规则画出60个小刻度,和12个大刻度就行


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
// 把状态保存起来
+ ctx.save()

// 画大圆
ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
ctx.closePath()

// 画小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()

----- 新加代码 ------

// 获取当前 时,分,秒
let time = new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 时针
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo设置画线起点
ctx.moveTo(-10, 0)
// lineTo设置画线经过点
ctx.lineTo(40, 0)
// 设置线宽
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
// 恢复成上一次save的状态
ctx.restore()
// 恢复完再保存一次
ctx.save()

// 分针
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.strokeStyle = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

//秒针
ctx.rotate(2 * Math.PI / 60 * sec - - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
ctx.stroke()
ctx.closePath()
}

ctx.restore()

截屏2021-07-19 下午10.53.53.png


最后一步就是更新视图,使时钟转动起来,第一想到的肯定是定时器setInterval,但是注意一个问题:每次更新视图的时候都要把上一次的画布清除,再开始画新的视图,不然就会出现千手观音的景象


截屏2021-07-19 下午10.57.05.png


附上最终代码:


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

setInterval(() => {
ctx.save()
ctx.clearRect(0, 0, 600, 600)
ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
ctx.save()

// 画大圆
ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
ctx.closePath()

// 画小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()

// 获取当前 时,分,秒
let time = new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 时针
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo设置画线起点
ctx.moveTo(-10, 0)
// lineTo设置画线经过点
ctx.lineTo(40, 0)
// 设置线宽
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 分针
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.strokeStyle = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

//秒针
ctx.rotate(2 * Math.PI / 60 * sec - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
// ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
}

ctx.restore()
ctx.restore()
}, 1000)

效果 very good啊:


clock的副本.gif


2. canvas实现刮刮卡


小时候很多人都买过充值卡把,懂的都懂啊哈,用指甲刮开这层灰皮,就能看底下的答案了。
截屏2021-07-19 下午11.02.09.png


思路是这样的:



  • 1、底下答案是一个div,顶部灰皮是一个canvascanvas一开始盖住div

  • 2、鼠标事件,点击时并移动时,鼠标经过的路径都画圆形开路,并且设置globalCompositeOperationdestination-out,使鼠标经过的路径都变成透明,一透明,自然就显示出下方的答案信息。


关于fill这个方法,其实是对标stroke的,fill是把图形填充,stroke只是画出边框线


// html
<canvas id="canvas" width="400" height="100"></canvas>
<div class="text">恭喜您获得100w</div>
<style>
* {
margin: 0;
padding: 0;
}
.text {
position: absolute;
left: 130px;
top: 35px;
z-index: -1;
}
</style>


// js
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// 填充的颜色
ctx.fillStyle = 'darkgray'
// 填充矩形 fillRect(起始X,起始Y,终点X,终点Y)
ctx.fillRect(0, 0, 400, 100)
ctx.fillStyle = '#fff'
// 绘制填充文字
ctx.fillText('刮刮卡', 180, 50)

let isDraw = false
canvas.onmousedown = function () {
isDraw = true
}
canvas.onmousemove = function (e) {
if (!isDraw) return
// 计算鼠标在canvas里的位置
const x = e.pageX - canvas.offsetLeft
const y = e.pageY - canvas.offsetTop
// 设置globalCompositeOperation
ctx.globalCompositeOperation = 'destination-out'
// 画圆
ctx.arc(x, y, 10, 0, 2 * Math.PI)
// 填充圆形
ctx.fill()
}
canvas.onmouseup = function () {
isDraw = false
}

效果如下:


guaguaka.gif


3. canvas实现画板和保存


框架:使用vue + elementUI


其实很简单,难点有以下几点:



  • 1、鼠标拖拽画正方形和圆形

  • 2、画完一个保存画布,下次再画的时候叠加

  • 3、保存图片


第一点,只需要计算出鼠标点击的点坐标,以及鼠标的当前坐标,就可以计算了,矩形长宽计算:x - beginX, y - beginY,圆形则要利用勾股定理:Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))


第二点,则要利用canvas的getImageDataputImageData方法


第三点,思路是将canvas生成图片链接,并赋值给具有下载功能的a标签,并主动点击a标签进行图片下载


看看效果吧:


截屏2021-07-19 下午11.16.24.png


截屏2021-07-19 下午11.17.41.png


具体代码我就不过多讲解了,说难也不难,只要前面两个项目理解了,这个项目很容易就懂了:


<template>
<div>
<div style="margin-bottom: 10px; display: flex; align-items: center">
<el-button @click="changeType('huabi')" type="primary">画笔</el-button>
<el-button @click="changeType('rect')" type="success">正方形</el-button>
<el-button
@click="changeType('arc')"
type="warning"
style="margin-right: 10px"
>圆形</el-button
>
<div>颜色:</div>
<el-color-picker v-model="color"></el-color-picker>
<el-button @click="clear">清空</el-button>
<el-button @click="saveImg">保存</el-button>
</div>
<canvas
id="canvas"
width="800"
height="400"
@mousedown="canvasDown"
@mousemove="canvasMove"
@mouseout="canvasUp"
@mouseup="canvasUp"
>
</canvas>
</div>
</template>

<script>
export default {
data() {
return {
type: "huabi",
isDraw: false,
canvasDom: null,
ctx: null,
beginX: 0,
beginY: 0,
color: "#000",
imageData: null,
};
},
mounted() {
this.canvasDom = document.getElementById("canvas");
this.ctx = this.canvasDom.getContext("2d");
},
methods: {
changeType(type) {
this.type = type;
},
canvasDown(e) {
this.isDraw = true;
const canvas = this.canvasDom;
this.beginX = e.pageX - canvas.offsetLeft;
this.beginY = e.pageY - canvas.offsetTop;
},
canvasMove(e) {
if (!this.isDraw) return;
const canvas = this.canvasDom;
const ctx = this.ctx;
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
this[`${this.type}Fn`](ctx, x, y);
},
canvasUp() {
this.imageData = this.ctx.getImageData(0, 0, 800, 400);
this.isDraw = false;
},
huabiFn(ctx, x, y) {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
},
rectFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.rect(beginX, beginY, x - beginX, y - beginY);
ctx.stroke();
ctx.closePath();
},
arcFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
this.isDraw && ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.arc(
beginX,
beginY,
Math.round(
Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
),
0,
2 * Math.PI
);
ctx.stroke();
ctx.closePath();
},
saveImg() {
const url = this.canvasDom.toDataURL();
const a = document.createElement("a");
a.download = "sunshine";
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
clear() {
this.imageData = null
this.ctx.clearRect(0, 0, 800, 400)
}
},
};
</script>

<style lang="scss" scoped>
#canvas {
border: 1px solid black;
}
</style>

结语


链接:https://juejin.cn/post/6986785259966857247

收起阅读 »

一个酷炫的 android 粒子动画库

一、灵感做这个粒子动画库的灵感来自于 MIUI 卸载应用时的动画:这个爆炸的粒子效果看起来很酷炫,而且粒子颜色是从 icon 中拿到的。最开始我简单实现了类似爆炸的效果,后来想到可以直接扩展一下,写一个通用的粒子动画库。二、使用项目地址:github.com/...
继续阅读 »


一、灵感

做这个粒子动画库的灵感来自于 MIUI 卸载应用时的动画:

这个爆炸的粒子效果看起来很酷炫,而且粒子颜色是从 icon 中拿到的。

最开始我简单实现了类似爆炸的效果,后来想到可以直接扩展一下,写一个通用的粒子动画库。

二、使用

项目地址:github.com/ultimateHan…

Particle 是一个使用 kotlin 编写的粒子动画库,可以用几行代码轻松搞定一个粒子动画。同时也支持高度自定义的粒子动画轨迹,可以打造出非常炫酷的自定义动画。这个项目发布了 0.1 版本在 JitPack 上,按如下操作引入:

在根目录的 build.gradle 中的 allprojects 中添加(注意不是 buildScript):

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

然后在你的项目中引入依赖即可。

implementation 'com.github.ultimateHandsomeBoy666:Particle:0.1'

在引入了 Particle 之后,只需要下面几行简单的代码,就可以实现上面的粒子爆炸效果:

Particles.with(context, container) // container 是粒子动画的宿主父 ViewGroup
.colorFromView(button)// 从 button 中采样颜色
.particleNum(200)// 一共 200 个粒子
.anchor(button)// 把 button 作为动画的锚点
.shape(Shape.CIRCLE)// 粒子形状是圆形
.radius(2, 6)// 粒子随机半径 2~6
.anim(ParticleAnimation.EXPLOSION)// 使用爆炸动画
.start()

三、粒子形状

粒子的形状支持圆形、三角形、矩形、五角星以及矢量图形及位图,并且支持多种图形粒子混合

下面详细说明。

Shape.CIRCLE 和 Shape.HOLLOWCIRCLE

  • 圆形和空心圆

  • 使用 radius 定义圆的大小。空心圆使用 strokeWidth 定义粗细。

Shape.TRIANGLE 和 Shape.HOLLOWTRIANGLE

  • 实心三角形和空心三角形

  • 使用 width 和 height 定义三角形的大小。空心三角形使用 strokeWidth 定义粗细。

Shape.RECTANGLE 和 Shape.HOLLOWRECTANGLE

  • 实心矩形和空心矩形。

  • 使用 width 和 height 定义矩形的大小。空心矩形使用 strokeWidth 定义粗细。

Shape.PENTACLE 和 Shape.HOLLOWPENTACLE

  • 实心五角星和空心五角星

  • 使用 radius 定义五角星外接圆的大小。空心五角星使用 strokeWidth 定义粗细。

Shape.BITMAP

  • 支持位图。

  • 支持矢量图,只需要把矢量图 xml 的资源 id 传入即可。

  • 图片粒子不受 color 设置的影响。

除了上述单种图形以外,还支持多种图形的混合粒子,如下:

四、粒子动画

动画控制

粒子的动画使用 ValueAnimator 来控制,可以自行定义 animator 来控制动画的行为,包括动画时长、Interpolater、重复、开始结束的监听等等。

粒子特效

目前仅支持粒子在运动过程中的旋转,如下。后续会增加更多效果

粒子轨迹

粒子轨迹的控制使用 IPathGenerator 接口的派生类来完成。库中自带四种轨迹动画,分别是:

  • ParticleAnimation.EXPLOSION 爆炸💥效果
  • ParticleAnimation.RISE 粒子上升
  • ParticleAnimation.FALL 粒子下降
  • ParticleAnimation.FIREWORK 烟花🎇效果

如果想要自定义粒子运动轨迹的话,可以继承 IPathGenerator 接口,复写生成粒子坐标的方法:

private fun createPathGenerator(): IPathGenerator {
// LinearPathGenerator 库中自带
return object : LinearPathGenerator() {
val cos = Random.nextDouble(-1.0, 1.0)
val sin = Random.nextDouble(-1.0, 1.0)

override fun getCurrentCoord(progress: Float, duration: Long): Pair<Int, Int> {
// 在这里写你想要的粒子轨迹
val originalX = distance * progress
val originalY = 100 * sin(originalX / 50)
val x = originalX * cos - originalY * sin
val y = originalX * sin + originalY * cos
return Pair((0.01 * x * originalY).toInt(), (0.008 * y * originalX).toInt())
}
}
}

然后把这个返回 IPathGenerator 的方法通过高阶函数的形式传入即可:

particleManager!!.colorFromView(button)
.particleNum(300)
.anchor(it)
.shape(Shape.CIRCLE, Shape.BITMAP)
.radius(8, 12)
.strokeWidth(10f)
.size(20, 20)
.rotation(Rotation(600))
.bitmap(R.drawable.ic_thumbs_up)
.anim(ParticleAnimation.with({
// 控制动画的animator
createAnimator()
}, {
// 粒子运动的轨迹
createPathGenerator()
})).start()

上述代码中的 ParticleAnimation.with 方法接受两个高阶函数分别生成动画控制和粒子轨迹。

fun with(animator: () -> ValueAnimator = DEFAULT_ANIMATOR_LAMBDA,
generator: () -> IPathGenerator)
: ParticleAnimation {
return ParticleAnimation(generator, animator)
}

终于,经过上面的折腾,可以得到下面的酷炫动画:

当然,只要你想要,可以构造出无限多的粒子动画轨迹,不过这可能要求一点数学功底🐶。

在 github.com/ultimateHan… 目录下有一份我之前试验的比较酷炫的轨迹公式合集,可以参考。

五、注意事项

  • 粒子动画比较消耗内存和 CPU,所以粒子数目太多,比如超过 1000 的话,可能会有卡顿。
  • 默认在动画结束的时候,粒子是不会消失的。如果要让粒子在动画结束时消失,可以自定义 ValueAnimator 监听动画结束,在结束时调用 ParticleManager.hide() 方法来隐藏粒子。
  • 如果需要反复触发粒子动画,比如按一次按钮触发一次,可以使用一个全局的 particleManager 变量来启动和取消粒子动画,可以避免内存消耗和内存抖动。比如:
particleManager = Particles.with(this, container)
button.setOnClickListener {

particleManager!!.colorFromView(button)
.particleNum(300)
.anchor(it)
.shape(Shape.CIRCLE, Shape.BITMAP)
.radius(8, 12)
.rotation(Rotation(600))
.anim(ParticleAnimation.EXPLOSION)

particleManager!!.start()
}

代码下载:ChipsLayoutManager-master.zip

收起阅读 »

Android 可扩展视图设计

前言问题飞书团队在去年对Chat页面进行了布局优化,在优化的时候发现了一个现象:很多布局(特别是RootView)往往会被附加非常多的功能(输入法监控、渲染耗时统计 、侧边栏滑出抽屉等),而且这些功能在很多场景下都会被用到。当时面临一个问题:如何优雅地扩展一个...
继续阅读 »

前言

问题

飞书团队在去年对Chat页面进行了布局优化,在优化的时候发现了一个现象:很多布局(特别是RootView)往往会被附加非常多的功能(输入法监控、渲染耗时统计 、侧边栏滑出抽屉等),而且这些功能在很多场景下都会被用到。

当时面临一个问题:如何优雅地扩展一个View的功能?

常用方案

对于View的功能扩展,一般有三条路可走:

  1. 一个自定义View的无限膨胀
  2. 多层自定义View
  3. 多重继承自定义View

但是,这三个方案都有问题:

  1. 一个自定义View,会完全没有可复用性,可维护性差
  2. 多层自定义View,会有过度绘制问题(增加了视图层级)
  3. 多重继承自定义View,会有耦合性问题,因为如果有N个功能自由组合,使用继承的方式来实现,最终自定义View的个数会是:C(N,1)+C(N,2)+...+C(N,N)

一个想法

我们知道,在软件设计中有一对非常重要的概念:is-a 和 has-a  简单理解,is-a表示继承关系,has-a是组合关系,而has-a要比is-a拥有更好的可扩展性。

那么在扩展视图功能的时候,是不是也可以用has-a(组合)代替常用的is-a(继承)?

答案是可以的,而且我们可以使用委托模式来实现它,委托模式天然适合这个工作:设计的出发点就是为has-a替代is-a提供解决方案的, 而Kotlin在语言层面对委托模式提供了非常优雅的支持,在这种场景下可以使用它的by接口委托 

探索

概念定义

  • Widget: 系统View / ViewGroup、自定义View / ViewGroup。
  • WidgetPlus: 委托者。继承自Widget,并可通过register()的方式has some items。
  • DelegateItem: 被委托者。接受来自WidgetPlus的委托,负责业务逻辑的具体实现。
  • IDelegate: 被委托者接口。

不支持在 Docs 外粘贴 block

流程设计

无法复制加载中的内容

角色转换

在被委托接口IDelagate的“润滑”下,Widget、WidgetPlus和Item相互之间是可以做到无缝转换的

  • Widget -> WidgetPlus

    • 简单描述:一个视图可以改造为功能可扩展的视图(可双向
    • 转换方法:实现IDelegate接口、支持item注册
  • Widget -> DelegateItem

    • 简单描述:自定义视图可以被改造为一个功能项,供其它可扩展视图动态配置(可双向
    • 转换方法:自定义Widget移除对Widget的继承,实现IDelegate接口
  • WidgetPlus -> DelegateItem

    • 简单描述:一个可扩展视图(本身带有一部分功能),可被改造为功能项(可双向
    • 转换方法:移除对Widget的继承,保留IDelegate接口的实现

无法复制加载中的内容

通信和调用

  • 可扩展视图和扩展项应该支持双向通信:

    • WidgetPlus -> DelegateItem

      • 这个比较简单,WidgetPlus会用组合的方式持有Item,在收到业务或系统的请求时,委托Item去执行具体的实现逻辑。
    • DelegateItem -> WidgetPlus

      • 在Item初始化的时候,需要传入WidgetPlus的相关信息(widgetPlus、context、attrs、defStyleAttr、defStyleRes)
  • WidgetPlus跟Items拥有相同的API,需要设置调用原则:

    • 所有公共方法,一律使用WidgetPlus对象来触发(无论是在外部代码还是Item内部)
    • Item私有方法,使用Item对象来触发

竞争机制

一个WidgetPlus同时持有多个Item的时候,如果这些Item被委托实现了相同的方法,那么就会出现Item的内部竞争问题。这里,可以根据方法类别来分别处理:

  1. 无返回值方法

    1. 比如onMeasure(),按照Item注册列表顺序执行
  2. 有返回值方法

  • 比如onTouchEvent():Boolean,这里出现了功能冲突,因为不可能同时返回多个值,只能取第一个返回值作为WidgetPlus的返回值。
  • 对于这种情形,可以打印日志以便Develop时就被发现,解决方法有两种:
  1. 合而为一,即把两个Item合并,在一个Item中处理冲突;
  2. 分而治之,即把其中一个Item转换为WidgetPlus,创建两级视图。

关键点

1:1

  • 一个WidgetPlus可以无限扩展Item功能项,但是对一种Item功能项只能持有一个对象。
  • 但是,由于外部调用具有不可控性,所以register()的入参应该是Item的Class对象,在WidgetPlus内部反射调用Item的构造来生成对象。

Center

WidgetPlus中还是有一部分代码量的,为了减少Widget的转换成本、增加后续的可维护性,可以在WidgetPlus和Item直接再加一层DelegateCenter,由它来统一管理。

无法复制加载中的内容

Super

  • 问题:在重写Widget的系统方法时,是需要执行superMethod的,而Item在进行业务实现时,无法直接触发到这个superMethod的。
  • 有两个解决方案:
  1. 把Widget的method拆分为methodBefore()、methodAfter()、isHasSuper(),分别委托Item实现
  2. 把superMethod作为委托参数,这里可以使用Kotlin的方法类型参数

很显然,第二种方案要更好。

示意代码

 /**

* Widget

*/

package android.widget;

public class LinearLayout extends ViewGroup {

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

}



/**

* WidgetPlus

*/

class LinearLayoutPlus() : LinearLayout(), IDelegate by DelegateCenter() {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

onDelegateMeasure(widthMeasureSpec, heightMeasureSpec) { _, _ ->

super.onMeasure(widthMeasureSpec, heightMeasureSpec)}

}

}



/**

* Center

*/

class DelegateCenter() : IDelegate {



private val itemList = mutableListOf<IItem>()



fun register(item: Class<IDelegate>) {

plusList.add(item.newInstance())

}



fun unRegister(item: Class<IDelegate>) {

plusList.remove(item)

}



override fun onDelegateMeasure(

widthMeasureSpec: Int,

heightMeasureSpec: Int,

superMethod: (Int, Int) -> Unit) {

for (item in itemList) {

item.onDelegateMeasure(widthMeasureSpec, heightMeasureSpec,superMethod)

}

}

}



/**

* delegate interface

*/

interface IDelegate : IItem {



fun register(item: Class<IDelegate>)



fun unRegister(item: Class<IDelegate>)

}



/**

* Item interface

*/

interface IItem{

fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int,

superMethod: (Int, Int) -> Unit)

}



/**

* Item1

*/

class Item1() : IItem() {

override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: I nt, superMethod: (Int, Int) -> Unit) {}

}



/**

* Item2

*/

class Item2() : IItem() {

override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int, superMethod: (Int, Int) -> Unit) {}

}



/**

* main

*/

fun main() {

val plus = LinearLayoutPlus(context, attrs)

plus.register(Item1::class.java)

plus.register(Item2::class.java)

}
复制代码

背景知识

类与类之间的关系

  • 类与类之间有六种关系:
关系描述耦合度语义代码层面
继承继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己新功能的能力☆☆☆☆☆☆is-a在Java中继承关系通过关键字extends明确标识
实现实现指的是一个类实现接口(可以是多个)的功能☆☆☆☆☆is-a在Java中实现关系通过关键字implements明确标识
组合它体现整体与部分间的关系,而且具有不可分割性,生命周期是一致的☆☆☆☆contains-a类B作为类A的成员变量,只能从语义上来区别聚合和关联
聚合它体现整体与部分间的关系,它们是可分离的,各有自己的生命周期☆☆☆has-a类B作为类A的成员变量,只能从语义上来区别组合和关联
关联这种使用关系具有长期性,而且双方的关系一般是平等的☆☆has-a类B作为类A的成员变量,只能从语义上来区别组合和聚合
依赖这种使用关系具有临时性,非常的脆弱use-a类B作为入参,在类A的某个方法中被使用
  • 继承和实现体现的一种纵向关系,一般是明确无异议的。而组合、聚合、关联和依赖体现的是横向关系,它们之间就比较难区分了,这几种关系都是语义级别的,从代码层面并不能完全区分。

委托模式

  • 定义:有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。
  • 能力: 是一种基础模式,状态模式、策略模式、访问者模式等在本质上就是在特殊场合采用了委托模式,委托模式使得我们可以用组合、聚合、关联来替代继承。
  • 委托模式不能等价于代理模式: 虽然它们都是把业务需要实现的逻辑交给一个目标实现类来完成,但是使用代理模式的目的在于提供一种代理以控制对这个对象的访问,但是委托模式的出发点是将某个对象的请求拜托给另一个对象。
  • 委托模式是可以自由切换被委托者,委托者甚至可以自实现业务逻辑,例如Java ClassLoader的双亲委派模型中,在委托父加载器加载失败的情况下,可以切换为自己去加载。

收起阅读 »

深入解析dio(一) Socket 编程实现本地多端群聊

深入解析dio(一) Socket 编程实现本地多端群聊引言无论你是否用过, wendux 大佬开源的 dio 项目,应该是目前 Flutter 中最 🔥 的网络请求库,在 github 上接近 1W 的 star。但其...
继续阅读 »

深入解析dio(一) Socket 编程实现本地多端群聊

引言

无论你是否用过, wendux 大佬开源的 dio 项目,应该是目前 Flutter 中最 🔥 的网络请求库,在 github 上接近 1W 的 star。

但其实 Dart 中已经有 dart:io 库为我们提供了网络服务,为何 Dio 又如此受到开发者青睐?背后有哪些优秀的设计值得我们学习?

这个系列预计会花 6 期左右从计算机网络原理,到 Dart 中的网络编程,最后再到 Dio 的架构设计,通过原理分析 + 练习的方式,带大家由浅入深的掌握 Dart 中的网络编程与 Dio 库的设计。

本期,我们会通过编写一个简单的本地群聊服务一起学习计算机网络基础知识与 Dart 中的 Socket 编程


Socket 是什么

想要了解 Socket 是什么,需要先复习一下网络基础。

无论微信聊天,观看视频或者打开网页,当我们通过网络进行一次数据传输时。数据根据网络协议进行传输, 在 TCP/IP 协议中,经历如下的流转:

image.png

TCP/IP 定义了四层结构,每一层都是为了完成一种功能,为了完成这些功能,需要遵循一些规则,这些规则就是协议,每一层都定义了一些协议。

  • 应用层

应用层决定了向用户提供应用服务时通信的活动。TCP/IP 协议族内预存了各类通用的应用服务。比如,FTP(FileTransfer Protocol,文件传输协议)和 DNS(Domain Name System,域名系统)服务就是其中两类。HTTP 协议也处于该层。

  • 传输层

传输层对上层应用层,提供处于网络连接中的两台计算机之间端到端的数据传输。在传输层有两个性质不同的协议:TCP(Transmission ControlProtocol,传输控制协议)和UDP(User Data Protocol,用户数据报协议)。

  • 网络层(又名网络互连层)

网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所起的作用就是在众多的选项内选择一条传输路线。

  • 网络访问层(又名链路层)

用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。

今天的主角 Socket 是应用层 与 TCP/IP 协议族通信的中间软件抽象层,表现为一个封装了 TCP / IP协议族 的编程接口(API)

image.png

为什么我们一开始要了解 Socket 编程,因为比起直接使用封装好的网络接口,Socket 能让我们更接近接近网络的本质,同时不用关心底层链路的细节。


如何使用 Dart 中的 Socket

dart:io 库中提供了两个类,第一个是 Socket,我们可以用它作为客户端与服务器建立连接。 第二个是 ServerSocket,我们将使用它创建一个服务器,并与客户端进行连接。

1、Socket 客户端

本系列代码均上传,可直接运行:io_practice/socket_study

Socket 类中有一个静态方法 connect(host, int port) 。第一个参数 host 可以是一个域名或者 IP 的 String,也可以是 InternetAddress 对象。

connect 返回一个 Future<Socket> 对象,当 socket 与 host 完成连接时 Future 对象回调。

// socket_pratice1.dart
void main() {
Socket.connect("www.baidu.com", 80).then((socket) {
print('Connected to: '
'${socket.remoteAddress.address}:${socket.remotePort}');
socket.destroy();
});
}
复制代码

这个 case 中,我们通过 80 端口(为 HTTP 协议开放)与 http://www.baidu.com 连接。连接到服务器之后,打印出连接的 IP 地址和端口,最后通过 socket.destroy() 关闭连接。在命令行中 执行 dart socket_pratice1.dart 可以看到如下输出:

➜  socket_study dart socket_pratice1.dart 
socket_pratice2.dart: Warning: Interpreting this as package URI, 'package:io_pratice/socket_study/socket_pratice2.dart'.
Connected to: 220.181.38.149:80
复制代码

通过简单的函数调用,Dart 为我们完成了 http://www.baidu.com 的 IP 查找与 TCP 建立连接,我们只需要等待即可。 在连接建立之后,我们可以和服务端进行数据交互,为此我们需要做两件事。

1、发起请求 2、响应接受数据

对应 Socket 中提供的两个方法 Socket.write(String data) 和 Socket.listen(void onData(data)) 。

// socket_pratice2.dart
void main() {
String indexRequest = 'GET / HTTP/1.1\nConnection: close\n\n';

//与百度通过 80 端口连接
Socket.connect("www.baidu.com", 80).then((socket) {
print('Connected to: '
'${socket.remoteAddress.address}:${socket.remotePort}');

//监听 socket 的数据返回
socket.listen((data) {
print(new String.fromCharCodes(data).trim());
}, onDone: () {
print("Done");
socket.destroy();
});

//发送数据
socket.write(indexRequest);
});
}
复制代码

运行这段代码可以看到 HTTP/1.1 请求头,以及页面数据。这是学习 web 协议很好的一个工具,我们还可以看到设 cookie 等值。(一般不用这种方式连接 HTTP 服务器,Dart 中提供了 HttpClient 类,提供更多能力)

➜  socket_study dart socket_pratice2.dart 
socket_pratice2.dart: Warning: Interpreting this as package URI, 'package:io_pratice/socket_study/socket_pratice2.dart'.
Connected to: 220.181.38.150:80
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-Length: 14615
Content-Type: text/html
...
...
(headers and HTML code)
...
</script></body></html>
Done
复制代码

2、ServerSocket

使用 Socket 可以很容易的与服务器连接,同样我们可以使用 ServerSocket 对象创建一个可以处理客户端请求的服务器。 首先我们需要绑定到一个特定的端口并进行监听,使用 ServerSocket.bind(address,int port) 方法即可。这个方法会返回 Future<ServerSocket> 对象,在绑定成功后返回 ServerSocket 对象。之后 ServerSocket.listen(void onData(Socket event)) 方法注册回调,便可以得到客户端连接的 Socket 对象。注意,端口号需要大于 1024 (保留范围)。

// serversocket_pratice1.dart
void main() {
ServerSocket.bind(InternetAddress.anyIPv4, 4567)
.then((ServerSocket server) {
server.listen(handleClient);
});
}

void handleClient(Socket client) {
print('Connection from '
'${client.remoteAddress.address}:${client.remotePort}');
client.write("Hello from simple server!\n");
client.close();
}
复制代码

与客户端不同的是,在 ServerSocket.listen 中我们监听的不是二进制数据,而是客户端连接。 当客户端发起连接时,我们可以得到一个表示客户端连接的 Socket 对象。作为参数调用 handleClient(Socket client) 函数。通过这个 Socket 对象,我们可以获取到客户端的 IP 端口等信息,并且可以与其通信。运行这个程序后,我们需要一个客户端连接服务器。可以将上一个案例中 conect 的地址改为 127.0.0.0.1,端口改为 4567,或者使用 telnet 作为客户端发起。

运行服务端程序:

➜  socket_study dart serversocket_pratice1.dart 
serversocket_pratice1.dart: Warning: Interpreting this as package URI, 'package:io_pratice/socket_study/serversocket_pratice1.dart'.
Connection from 127.0.0.1:54555 // 客户端连接之后打印其 ip 与端口
复制代码

客户端使用 telnet 请求:

➜  io_pratice telnet localhost 4567
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello from simple server! // 来自服务端的消息
Connection closed by foreign host.
复制代码

即使客户端关闭连接,服务器程序仍然不会退出,继续等待下一个连接,Dart 已经为我们处理好了一切。

实战:本地群聊服务

1、聊天服务器

有了上面的实践,我们可以尝试编写一个简单的群聊服务。当某个客户端发送消息时,其他所有连接的客户端都可以收到这条消息,并且能优雅的处理错误和断开连接。

image.png

如图,我们的三个客户端与服务器保持连接,当其中一个发送消息时,由服务端将消息分发给其他连接者。 所以我们创建一个集合来存储每一个客户端连接对象

List<ChatClient> clients = [];
复制代码

每一个 ChatClient 表示一个连接,我们通过对 Socket 进行简单的封装,提供基本的消息监听,退出与异常处理:

class ChatClient {
Socket _socket;
String _address;
int _port;

ChatClient(Socket s){
_socket = s;
_address = _socket.remoteAddress.address;
_port = _socket.remotePort;

_socket.listen(messageHandler,
onError: errorHandler,
onDone: finishedHandler);
}

void messageHandler(List data){
String message = new String.fromCharCodes(data).trim();
// 接收到客户端的套接字之后进行消息分发
distributeMessage(this, '${_address}:${_port} Message: $message');
}

void errorHandler(error){
print('${_address}:${_port} Error: $error');
// 从保存过的 Client 中移除
removeClient(this);
_socket.close();
}

void finishedHandler() {
print('${_address}:${_port} Disconnected');
removeClient(this);
_socket.close();
}

void write(String message){
_socket.write(message);
}
}
复制代码

当服务端接受到某个客户端发送的消息时,需要转发给聊天室的其他客户端。

image.png

我们通过 messageHandler 中的 distributeMessage 进行消息分发:

...
void distributeMessage(ChatClient client, String message){
for (ChatClient c in clients) {
if (c != client){
c.write(message + "\n");
}
}
}
...
复制代码

最后我们只需要监听每一个客户端的连接,将其添加至 clients 集合中即可:

// chatroom.dart

ServerSocket server;

void main() {
ServerSocket.bind(InternetAddress.ANY_IP_V4, 4567)
.then((ServerSocket socket) {
server = socket;
server.listen((client) {
handleConnection(client);
});
});
}

void handleConnection(Socket client){
print('Connection from '
'${client.remoteAddress.address}:${client.remotePort}');

clients.add(new ChatClient(client));

client.write("Welcome to dart-chat! "
"There are ${clients.length - 1} other clients\n");
}
复制代码

直接运行程序

➜ dart chatroom.dart
复制代码

使用 telnet 测试服务器连接:

➜  socket_study telnet localhost 4567 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Welcome to dart-chat! There are 0 other clients
复制代码

2、聊天客户端

聊天客户端会简单很多,他只需要连接到服务器并接受消息;以及读取用户的输入信息并将其发送至客户端的方法。

前面我们已经实践过如何从服务器接收数据,所以我们只需实现发送消息即可。

通过 dart:io 中的 stdin 能帮助我们轻松的读取键盘输入:

// chatclient.dart

Socket socket;

void main() {
Socket.connect("localhost", 4567)
.then((Socket sock) {
socket = sock;
socket.listen(dataHandler,
onError: errorHandler,
onDone: doneHandler,
cancelOnError: false);
})
.catchError((AsyncError e) {
print("Unable to connect: $e");
exit(1);
});

// 监听键盘输入,将数据发送至服务端
stdin.listen((data) =>
socket.write(
new String.fromCharCodes(data).trim() + '\n'));
}

void dataHandler(data){
print(new String.fromCharCodes(data).trim());
}

void errorHandler(error, StackTrace trace){
print(error);
}

void doneHandler(){
socket.destroy();
exit(0);
}
复制代码

之后运行服务器,并通过多个命令行运行多个客户端程序。你可以在某个客户端中输入消息,之后在其他客户端接收到消息。

image.png

如果你有多个设备,也可以通过 Socket.connect(host, int port) 与服务器进行连接,当然这需要你提供每个设备的 IP 地址,这该如何做到?下一期我会通过 UDP 与组播协议进一步完善群聊服务。

收起阅读 »

【开源项目】集成环信IM开发的一款社交app---共享影院

项目介绍该项目旨在嵌入当今已经较为成熟的视频播放行业,让用户可以创建一个观影房,与远端的其他用户进行视频通讯并且同时观看同一视频。做到相隔万里,依然可以零距离互动,感受视频所带来的乐趣。项目结构share-cinema: 共享影院前端源码video-backe...
继续阅读 »

项目介绍

该项目旨在嵌入当今已经较为成熟的视频播放行业,让用户可以创建一个观影房,与远端的其他用户进行视频通讯并且同时观看同一视频。做到相隔万里,依然可以零距离互动,感受视频所带来的乐趣。

项目结构

  • share-cinema: 共享影院前端源码
  • video-backend: 共享影院后端源码
  • 演示视频.mp4: 共享影院功能演示视频


相关技术

  • Agora Video SDK :实现高清、稳定、流畅的及时通讯
  • 环信IM SDK :实现安全、简单的文字聊天
  • Socket.io :实现聊天室中同步播放、暂停以及拖动进度条
  • Celery + Flower : 实现后台的用户画像及模型更新
  • 图计算 :生成用户推荐模型


作品背景

《共享影院》项目的设计灵感来自当下非常流行的一种视频形式:Reaction Video(反应视频)。反应视频,顾名思义,就是记录下人们对事情做出反应的视频。在表现形式上,画面由两个部分组成,包括观看的视频资源,以及观看者本人的反应。这有点像观看体育比赛时电视台邀请的实况解说。
2013年,美剧《权利的游戏》第三季热播,大量的油管网友录制自己或朋友看剧时的激烈反应,引发了全球观众的集体共鸣,反应视频由此走入大众视野。目前,这类反应视频已经在多个视频平台成为了一类发展成熟且庞大的分支。Youtube上最火的Reaction类频道,目前已累积90亿次播放量,收获了1970万订阅。
Reaction Video之所以会如此成功,有以下两部分的因素:

  • 认同感:对于观众来说,他们希望在看视频的时候可以找到与自己有相同关注点的人,也期待着他人在看视频时会不会产生与自己相近的反应。
  • 分享感:对于反应视频的制作者来说,他们希望与大家分享自己在看视频时的喜怒哀乐。

这两种心理因素同样适应于如今非常流行的弹幕文化,但是其中仍然存在一些缺点。对于反应视频的制作者,或者发送弹幕的人来说,他们发表了自己的观点,收获了分享的满足感,却很难得到及时的反馈;对于观众而言,他们看到了弹幕或视频制作者的反应,从中找到了认同感,却难以与其分享自己的感受。
同时我们注意到,从以前“看完视频写评论”到“看视频时发弹幕”、“直播互动聊天”等新型视频形式,随着现在人们生活节奏的加快,人们越来越需要在相同的时间内获得更多的信息。
因此,我们提出《共享影院》这个项目,从“视频+音视频通讯+文字聊天”的形式上,将认同感与分享感合二为一,为看视频的用户提供及时地、双向地、新颖地视频娱乐体验。

创新性

  • 2020年,网易云音乐推出了《一起听音乐》,可以与好友一起同步听歌曲。
  • 2020年,BiliBili推出了《一起看》功能,提供了同步观影,语音消息等功能。
  • 2021年,抖音推出了《一起看》功能,提供和好友一起刷短视频的功能。
  • 我们的《共享影院》不但提供了与好友一起看视频的功能、实现了聊天室内的同步观影,而且与这些产品不同的是,首次结合了视频通讯,将Reaction Video的理念加入进来。不仅可以让已经观看过视频的人,将感兴趣的视频推荐给好友,再次观看进而与好友更加深入的交流;还可以在赛事直播以及新品发布时,与好友第一时间面对面见证历史时刻。此外,我们还引入了“陌生人匹配”,在孤独的时候为用户推荐最符合用户画像的“熟悉的陌生人”。
一起看时进行音视频通讯
一起看时进行文字聊天
观看精彩影视作品
观看赛事直播与发布会
陌生人匹配


潜在商业价值

反应类视频所带来的市场需求

  • 给用户带来认同感与分享感
  • 为用户提供一个私密的共享空间
  • 彼此之间的互动体验

文化推广,促进营销

该项目为用户之间提供了更多的讨论机会,这种“一起观看”的形式并不只停留在用户的首次观影,许多用户会为了与他人分享而将同一作品进行多次观看。同时,用户邀请和分享给好友视频资源,这对视频本身的内容营销有着颇多利好。可以快速带动视频内容的宣传,同时增加点播数,创造更多收益。

会员制度

部分电视剧、电影、综艺,需要开通会员才能观看。共同观影要求所有用户都满足权限才能进行观看。因此,在视频得到推广的同时,视频平台也将从中获益。

快速接入 《共享影院》的核心内容简单,可变性强

可以快速与已经成熟的视频平台对接,迅速投入商业化使用。视频资源不但可以是影视剧集,还可以是实况球赛、网络直播、新品发布会等各式各样的视频类型,上升空间大。
趣味性

借助音视频通讯来吸引用户观看视频。使用Agora Video SDK可以快速提升音视频通讯技术,提供美颜、变声、AR Face等多种玩法,增加视频生活的趣味性,提高用户黏性。


运行说明

前端

  • 《共享影院》项目前端由vue.cli 4.x搭建,启动前请按以下步骤执行
  • 安装依赖

npm install

  • 本地启动

npm run serve

运行后通过 https://localhost:8020/ 进行访问

后端

  • 《共享影院》项目后端由python3+ Flask框架搭建,运行前请按以下步骤操作
  • 请在requirements.txt所在目录下执行
  • 请在环信IM管理控制台手动创建一个名为superadmin的管理员用户,用于在后台创建聊天室
  • 请在环信IM管理控制台手动将用户注册方式修改为开放注册,用于实现用户注册

pip install -r requirements.txt

  • 添加声网RTC所需的相关配置


在app.py同一路径下创建config.py文件

文件中添加agora_token相关信息

# 声网SDK配置

appid = ""

appsecret = ""#

环信IM配置

url = "http://a1.easemob.com/"

orgname = ""

appname = ""

clientid = ""

clientsecret = ""

  • 启动后端


python app.py


操作指南

  • 前后端均正常运行之后,使用https://localhost:8020进入主页
  • 用户A选择主页中的任一视频,将自动创建观影房,观影房将有一个独立的房间号
  • 用户B从主页左上角的输入框输入用户A的房间号,将进入用户A的房间
  • 此时双方在观影房内的播放、暂停、拖动进度条均保持同步
  • 观影房中右侧的三个按钮分别是:禁止麦克风、退出房间、禁止摄像头

注意事项

  • 部分后台数据与模型储存在服务器上,为方便用户浏览项目中的全部功能,我们提供了本地运行版本,部分数据已经进行了模拟。
  • 视频资源因空间较大,我们只上传了部分视频资源,方便演示播放功能。
  • 项目中的图片以及视频资源均来源于网络。


收起阅读 »

Android资源管理及资源的编译和打包过程分析

前言在工作和学习中,我们除了要写一些业务代码,还要对项目的编译和打包流程有一定的认识,才能在遇到相关问题的时候能有所头绪。在这个过程中,我们往往会忽略掉资源文件是如何被添加进去的,Android的资源管理框架是一个很庞大和复杂的框架,资源编译打包的过程也很复杂...
继续阅读 »

前言

在工作和学习中,我们除了要写一些业务代码,还要对项目的编译和打包流程有一定的认识,才能在遇到相关问题的时候能有所头绪。在这个过程中,我们往往会忽略掉资源文件是如何被添加进去的,Android的资源管理框架是一个很庞大和复杂的框架,资源编译打包的过程也很复杂和繁琐,本文就来浅谈一下Android的资源文件是如何编译和打包的吧,除了当做一个自我总结,也希望能对看到本文的你有所帮助和启发。当然了文章比较长,希望你能耐心的看完。

编译打包流程

Android一个包中,除了代码以外,还有很多的资源文件,这些资源文件在apk打包的过程中,通过AAPT工具,打包到apk中。我们首先看一下apk的打包流程图,

image.png

概述一下这张图,打包主要有一下几个步骤:

  • 打包资源文件:通过aapt工具将res目录下的文件打包生成R.java文件和resources.arsc资源文件,比如AndroidManifest.xml和xml布局文件等。
  • 处理aidl files:如果有aidl接口,通过aidl工具打包成java接口类
  • java Compiler:javac编译,将R.java,源码文件,aidl.java编译为class文件
  • dex:源码.class,第三方jar包等class文件通过dx工具生成dex文件
  • apkbuilder:apkbuilder将所有的资源编译过的和不需要编译的,dex文件,arsc资源文件打包成一个完整的apk文件
  • jarsigner:以上生成的是一个没有签名的apk文件,这里通过jarsigner工具对该apk进行签名,从而得到一个带签名的apk文件
  • zipalign:对齐,将apk包中所有的资源文件距离文件起始偏移为4的整数倍,这样运行时可以减少内存的开销

资源分类

asset目录

存放原始资源文件,系统在编译时不会编译该目录下的资源文件,所以不能通过id的方式访问,如果要访问这些文件,需要指定文件名来访问。可以通过AssetManager访问原始文件,它允许你以简单的字节流的形式打开和读取和应用程序绑定在一起的原始资源文件。以下是一个从assets中读取本地的json文件的实例:

        StringBuilder sb = new StringBuilder();
AssetManager assets = getAssets();
try {
InputStream open = assets.open(“xxx.json”);
//使用一个转换流转换为字符流进行读取
InputStreamReader inputStreamReader = new InputStreamReader(open);
//缓冲字符流
BufferedReader reader = new BufferedReader(inputStreamReader);
String readLine;
while((readLine = reader.readLine())!=null){
sb.append(readLine);
}
String s = sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
复制代码

来看看一般项目中asset目录下会放些什么东东吧

image.png

res目录

存放可编译的资源文件(raw除外),编译时,系统会自动在R.java文件中生成资源文件的id,访问这种资源可以通过R.xxx.id即可。

目录资源类型
animator/用于定义属性动画的xml
anim/用于定义补间动画的xml(属性动画也可以在这里定义)
color/用于颜色状态列表的xml
drawable/位图文件(.9.png、.png、.jpg、.gif)
mipmap/适用于不同启动器图标密度的可绘制对象文件
layout/用于定义用户界面布局的 XML 文件
menu/用于定义应用菜单(如选项菜单、上下文菜单或子菜单)的 XML 文件
values/包含字符串、整型数和颜色等简单值的 XML 文件
XML/可在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(如可搜索配置)都必须保存在此处。
font/带有扩展名的字体文件(如 .ttf、.otf 或 .ttc),或包含 元素的 XML 文件
raw/需以原始形式保存的任意文件

编译资源文件的结果

好处

对资源进行编译有以下两点好处

  • 空间占用小:二进制xml文件占用的空间更小,因为所有的xml文件的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串池中。有了这个字符串池,原来使用字符串的地方就可以使用一个整数索引代替,从而可以减少文件的大小
  • 解析速度快:二进制的xml文件解析的速度更快,xml文件中不在包含字符串值,所以就省去了解析字符串的时间,从而提高了速度。

编译完成之后,除了assets资源之外,会给其他所有的资源生成一个id,根据这些id,打包工具会生成一个资源索引表resources.arsc以及R.java文件。资源索引表会记录所有资源的信息,根据资源id和设备信息,快速的匹配最合适的资源,R文件则记录各个资源的id常量。

生成资源索引表

首先来看一张图,这是resources.arsc的结构图 20160623160331859.png

整个resources.arsc是由一系列的chunk组成的,每一个chunk都有一个头,用来描述chunk的元数据。

  • header:每个chunk的头部用来描述该chunk的元信息,包括当前chunk的类型,头大小,块大小等
  • Global String Pool:全局字符串池,将所有字符串放到这个池子中,大家都复用这个池子中的数据,什么样的字符串会放到这个池子中呢?所有资源的文件的路径名,以及资源文件中所定义的资源的值,所以这个池子也可以叫做资源项的值字符串资源池,包含了所有在资源包里定义的资源项的值字符串,比如下面代码中ABC就存放在这里
  • package数据块:
    • package header:记录包的元数据,包名、大小、类型等
    • 资源类型字符串池:存储所有类型相关的字符串,如:attr、drawable、layout、anim等
    • 资源项名称字符串池:存储应用所有资源文件中资源项名称相关的字符串,比如下边的app_name就存放在这里。
    • Type Spec:类型规范数据块,用来描述资源项的配置差异性,通过这个差异性描述,我们就可以知道每一个资源项的配置状况。Android设备众多,为了使得应用程序支持不同的大小、密度、语言,Android将资源组织为18个维度,每一个资源类都对应一组配置列表,配置这个资源类的不同维度,最后再使用一套匹配算法来为应用程序在资源目录中选择最合适的资源。
    • config list:上边说到,每个type spec是一个类型的描述,每个类型会有多个维度,config list就是由多个ResTable_type结构来描述的,每一个ResTable_type描述的就是一个维度。
 <resources>    
    <string name="app_name">ABC</string>    
</resources>
复制代码

生成R文件和资源id

image.png

首先看一下R文件的结构图,每一种资源文件都对应一个静态内部类,对照前面所说的res文件目录结构,其中每个静态内部类中的一个静态常量分别定义一条资源标识符

image.png

或者这样:

    public static final class layout {
        public static final int main=0x7f030000;
    }
复制代码

public static final int main=0x7f030000;就表示layout目录下的main.xml文件。id中最高字节代表package的id,次高字节代表type的id,最后的字节代表当前类型中出现的序号。

  • package id:相当于一个命名空间,限定资源的来源,Android系统当前定义了两个资源命令空间,其中系统资源命令空间是0x01,另外一个应用程序资源命令空间为0x7f,所有位于 0x01到0x7f 之间的packageid都是合法的。
  • type id:指资源的类型id,如anim、color、layout、raw...等,每一种资源都对应一个type id
  • entry id:指每一个资源在其所属资源类型中出现的次序,不同资源类型的entry id是有可能相同的,但是由于他们的type id不同,所以一样可以进行区分。

资源文件只能以小写字母和下划线作为首字母,随后的名字中只能出现a-z或者0-9或者_.这些字符,否则会报错。

当我们在相应的res的资源目录中添加资源文件时,便会在相应的R文件中的静态内部类中自动生成一条静态的常量,对添加的文件进行索引。

在布局文件中当我们需要为组件添加id属性时,可以使用@+id/idname,+表示在R文件的名为id的内部类中添加一条记录。如果这个id不存在,则会首先生成它。

资源文件打包流程

说完了资源文件的一些基本信息以后,相信你对apk包内的资源文件有了一个更加明确的认识了吧,接下来我们就来讲一讲资源文件是如何打包到apk中的,这个过程非常复杂,需要好好的理解和记忆。

Android资源打包工具在编译应用程序资源之前,会创建资源表ResourceTable,当应用程序资源编译完之后,这个资源表就包含了资源的所有信息,然后就可以根据这个资源表来生成资源索引文件resources.arsc了。

解析AndroidManifest.xml

获取要编译资源的应用程序的包名、minSdkVersion等,有了包名就可以创建资源表了,也就是ResourceTable。

添加被引用的资源包

通常在编译一个apk包的时候,至少会涉及到两个资源包,一个是被引用的系统资源包,里面有很多系统级的资源,比如我们熟知的四大布局 LinearLayout、FrameLayout等以及一些属性layout_width、layout_height、layout_oritation等,另一个就是当前正在编译的应用程序的资源包。

收集资源文件

在编译应用程序资源之前,aapt会创建AaptAssets对象,用来收集当前需要编译的资源文件,这些资源文件被保存在AaptAssets类的成员变量mRes中。

将收集到的资源增加到资源表ResourceTable

之前将资源添加到了AaptAssets中,这一步将资源添加到ResourceTable中,我们最后要根据这个资源表来生成resources.arsc资源索引表,回头看看arsc文件的结构图,它也有一个resourceTable。

这一步收集到资源表的资源是不包括values的,因为values资源需要经过编译后,才能添加到资源表中

编译values资源

values资源描述的是一些比较简单的轻量级资源,如strings/colors/dimen等,这些资源是在编译的过程中进行收集的

给bag资源分配id

values资源下,除了string之外,还有其他的一些特殊资源,这些资源给自己定义一些专用的值,比如LinearLayout的orientation属性,它的取值范围为 vertical 和 horizontal,这就相当于定义了vertical和horizontal两个bag。

在编译其他非values资源之前,我们需要给之前收集到的bag资源分配资源id,因为它可能会被其它非values类资源所引用。

编译xml文件

之前的六步为编译xml文件做好了准备,收集到了xml所需要用到的所有资源,现在可以开始编译xml文件了,比如layout、anims、animators等。编译xml文件又可以分为四个步骤

解析xml文件

这一步会将xml文件转化为一系列树形结构的XMLNode,每一个XMLNode都表示一个xml元素,解析完成之后,就可以得到一个根节点XMLNode,然后就可以根据这个根节点来完成下边的操作

赋予属性名称id

这一步为每个xml元素的属性名称都赋予资源id,比如一个控件TextView,它有layout_width和layout_height两个属性,这里就要给这些属性名称赋予一个资源id。对系统资源包来说,这些属性名称都是它定义好的一些列bag资源,在编译的时候,就已经分配好了资源id了。

对于每一个xml文件都是从根节点开始给属性名称赋予资源id,然后再递归的给每一个子节点属性名称赋予资源id,一直到每一个节点的属性名称都有了资源id为止。

解析属性值

这一步是上一步的进一步深化,上一步为每个属性赋值id,这一步对属性对应的值进行解析,比如对于刚才的TextView,就会对其width和height的值进行解析,可能是match_parent也可能是warp_content.

压平xml文件

将xml文件进行扁平化处理,将其变为二进制格式,有如下几个步骤

  1. 收集有资源id的属性名称字符串,并将它们放在一个数组里。这些收集到的属性名称字符串保存在字符串资源池中,与收集到的资源id数组是一一对应的。
  2. 收集xml文件中其他所有的字符串,也就是没有资源id的字符串
  3. 写入xml文件头,最终编译出来的xml二进制文件是一系列的chunk组成的,每一个chunk都有一个头部,来描述元信息。
  4. 写入资源池字符串,将第一步和第二步收集到的内容写入Global String pool中,也就是之前所说的arsc文件结构里的全局字符串资源池中
  5. 写入资源id,将所有的资源id收集起来,生成package时要用到,对应arsc文件的结构的package。
  6. 压平xml文件,就是将各个xml元素中的字符串都替换掉,这些字符串或者被替换为到字符串资源池的一个索引,或者被替换为一个具有类型的其他值

给资源生成资源符号

这里生成资源符号为之后生成R文件做准备,之前的操作将所有收集到的资源文件都按照类型保存在资源表中,也就是ResourceTable对象。aapt在这里只需要遍历每一个package里面的type,然后取出每一个entry的名称,在根据其在相应的type中出现的次序,就可以计算出相应的资源id了,然后就能得到其资源符号。资源符号=名称+资源id

根据资源id生成资源索引表

在这里我们将生成resources.arsc,对其生成的步骤再次进行拆解

  1. 按照package收集类型字符串,如drawable、string、layout、id等,当前被编译的应用程序有几个package,就对应几组类型字符串,每一组类型字符串保存在其所属的package中。
  2. 收集资源型名称字符串,还是以package为单位,比如在string.xml中,<resources>    <string name="app_name">ABC</string>  </resources>就可以收集其中的属性app_name
  3. 收集资源项值字符串,还是上面的string.xml就可以收集到ABC
  4. 生成package数据块,就是按照之前说的resources.arsc文件格式中package的格式进行一步步的解析和收集
  5. 写入资源索引表头部,也就是ResTable_header
  6. 写入资源项的值字符串资源池,上面的第3步,将所有的值字符串收集起来了,这里直接写入就好了
  7. 写入package数据块,将第4步收集到的package数据块写入到资源索引表中。

经过以上几步,资源项索引表resources.arsc就生成好了。

编译AndroidManifest.xml文件

经过以上的几个步骤,应用程序的所有资源就编译完成了,这里就将应用程序的配置文件AndroidManifest.xml也编译为二进制文件。

生成R文件

到这里,我们已经知道了所有的资源以及其对应的id,然后就可以愉快的写入到R文件了,根据不同的type写到不同的静态内部类中,就像之前所描述的R文件的格式那样。

打包到APK

所有的资源文件都编译以及生成完之后,就可以将其打包到apk中了

  • assets目录
  • res目录,除了values之外,因为values目录下的资源文件经过编译以后,已经直接写入到资源索引表中去了
  • 资源索引表resources.arsc
  • 除了资源文件之外的其他文件(dex、AndroidManifest.xml、签名信息等)

结语

终于捋完了,整个资源文件的编译打包过程真的是很复杂又很繁琐的一个过程,在阅读的过程中要时刻对照着那几张机构图才能更好地对这些文件有更清晰的认识。资源文件在Android的学习和工作中是非常重要的,很多时候这些知识会被忽略掉,但是如果有时间好好捋一捋这些知识对于自身是一个很大的提升。

画个流程图

最后再用一张流程图来回顾一个整个流程

image.png


收起阅读 »

Android高手笔记 - 网络优化

一文带你了解android中对注入框架的检测。(以下的检测来源于对某APP进行逆向分析得出的情况)1.检测栈信息2.检测包名信息public static boolean xp1(Context context) {         boolean scan...
继续阅读 »

一文带你了解android中对注入框架的检测。

(以下的检测来源于对某APP进行逆向分析得出的情况)

1.检测栈信息

image.png

2.检测包名信息

public static boolean xp1(Context context) {

        boolean scanPackage = scanPackage(context, new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5pbnN0YWxsZXI=", 2)));

        MLog.b("attack", "Installed xposed:" + scanPackage);

        return scanPackage;

}

解密
ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5pbnN0YWxsZXI= = de.robv.android.xposed.installer
 

 

 public static boolean xp2(Context context) {

        StackTraceElement[] stackTrace;

        context.getFilesDir();

        try {

            throw new Exception("凸一_一凸");

        } catch (Exception e) {

            MLog.a("attack", e.getMessage());

            boolean z = false;

            for (StackTraceElement stackTraceElement : e.getStackTrace()) {

                if (stackTraceElement.getClassName().equals(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2))) && stackTraceElement.getMethodName().equals(new String(Base64.decode("bWFpbg==", 2)))) {

                    z = true;

                }

                if (stackTraceElement.getClassName().equals(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2))) && stackTraceElement.getMethodName().equals(new String(Base64.decode("aGFuZGxlSG9va2VkTWV0aG9k", 2)))) {

                    z = true;

                }

            }

            MLog.b("attack", "Exception hit:" + z);

            return z;

        }

    }

 

解密:

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=de.robv.android.xposed.XposedBridge


aGFuZGxlSG9va2VkTWV0aG9k = handleHookedMethod

bWFpbg==main
 

 ```

```C++


  public static String xp3(Context context) {

        String str;

        context.getFilesDir();

        try {

            Field declaredField = DexAOPEntry.java_lang_ClassLoader_loadClass_proxy(ClassLoader.getSystemClassLoader(), new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRIZWxwZXJz", 2))).getDeclaredField(new String(Base64.decode("ZmllbGRDYWNoZQ==", 2)));

            declaredField.setAccessible(true);

            Map map = (Map) declaredField.get(null);

            ArrayList arrayList = new ArrayList();

            arrayList.addAll(map.keySet());

            str = new JSONArray(arrayList).toString();

        } catch (Exception e) {

            str = null;

        }

        MLog.b("attack", "FieldInHook msg:" + str);

        return str;

    }


解密:

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRIZWxwZXJz =de.robv.android.xposed.XposedHelpers

ZmllbGRDYWNoZQ== fieldCache


 public static String xp4(Context context) {

        String str;

        context.getFilesDir();

        PackHookPlugin packHookPlugin = new PackHookPlugin(1);

        try {

            Field declaredField = DexAOPEntry.java_lang_ClassLoader_loadClass_proxy(ClassLoader.getSystemClassLoader(), new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2))).getDeclaredField(new String(Base64.decode("c0hvb2tlZE1ldGhvZENhbGxiYWNrcw==", 2)));

            declaredField.setAccessible(true);

            Map map = (Map) declaredField.get(null);

            Class java_lang_ClassLoader_loadClass_proxy = DexAOPEntry.java_lang_ClassLoader_loadClass_proxy(ClassLoader.getSystemClassLoader(), new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2UkQ29weU9uV3JpdGVTb3J0ZWRTZXQ=", 2)));

            Method declaredMethod = java_lang_ClassLoader_loadClass_proxy.getDeclaredMethod(new String(Base64.decode("Z2V0U25hcHNob3Q=", 2)), new Class[0]);

            for (Entry entry : map.entrySet()) {

                Member member = (Member) entry.getKey();

                Object value = entry.getValue();

                String a = ScanMethod.a(member.toString());

                if (!"".equals(a) && java_lang_ClassLoader_loadClass_proxy.isInstance(value)) {

                    for (Object obj : (Object[]) declaredMethod.invoke(value, new Object[0])) {

                        String[] split = obj.getClass().getClassLoader().toString().split("\"");

                        if (split.length > 1) {

                            packHookPlugin.a(StringTool.a(split, 1), a);

                        }

                    }

                }

            }

            JSONArray a2 = packHookPlugin.a();

            JSONArray methodToNative = methodToNative();

            if (a2 != null) {

                if (methodToNative != null) {

                    for (int i = 0; i < methodToNative.length(); i++) {

                        a2.put(methodToNative.getJSONObject(i));

                    }

                }

                str = a2.toString();

            } else {

                if (methodToNative != null) {

                    str = methodToNative.toString();

                }

                str = null;

            }

        } catch (Exception e) {

        }

        MLog.b("attack", "MethodInHook msg:" + str);

        return str;

}

解密:

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=de.robv.android.xposed.XposedBridge

 

c0hvb2tlZE1ldGhvZENhbGxiYWNrcw== sHookedMethodCallbacks

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2UkQ29weU9uV3JpdGVTb3J0ZWRTZXQ= de.robv.android.xposed.XposedBridge$CopyOnWriteSortedSet

Z2V0U25hcHNob3Q=getSnapshot

 ```

```C++


 public static boolean xp5(Context context) {

        try {

            Throwable th = new Throwable();

            th.setStackTrace(new StackTraceElement[]{new StackTraceElement(new String(Base64.decode("U2NhbkF0dGFjaw==", 2)), "", "", 0), new StackTraceElement(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2)), "", "", 0)});

            StackTraceElement[] stackTrace = th.getStackTrace();

            if (stackTrace.length != 2 || !stackTrace[1].getClassName().equals(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U=", 2)))) {

                return true;

            }

            return false;

        } catch (Exception e) {

            return false;

        }

    }

解密:

U2NhbkF0dGFjaw== ScanAttack

ZGUucm9idi5hbmRyb2lkLnhwb3NlZC5YcG9zZWRCcmlkZ2U= de.robv.android.xposed.XposedBridge


    public static boolean xp6(Context context) {

        try {

            StringWriter stringWriter = new StringWriter();

            new Throwable().printStackTrace(new PrintWriter(stringWriter));

            if (stringWriter.toString().contains(new String(Base64.decode("ZGUucm9idi5hbmRyb2lkLnhwb3NlZA==", 2)))) {

                return true;

            }

            return false;

        } catch (Exception e) {

            return false;

        }

    }

解密:ZGUucm9idi5hbmRyb2lkLnhwb3NlZA==de.robv.android.xposed

收起阅读 »

Android基础到进阶UI祖宗级 View介绍+实用

View的继承关系在Android系统中,任何可视化控件都需要从android.view.View类继承。而任何从android.view.View继承的类都可以称为视图(View)。Android的绝大部分UI组件都放在android.widget包及其子包...
继续阅读 »

View的继承关系

在Android系统中,任何可视化控件都需要从android.view.View类继承。而任何从android.view.View继承的类都可以称为视图(View)。Android的绝大部分UI组件都放在android.widget包及其子包,下图就是android.widget包中所有View及其子类的继承关系:

从上图看,有很多布局类等为什么没有在上图看到,在这里要说明一下这里仅是android.widget包的,还有其他视图的虽然也继承View但是他们不属于android.widget包例如下面两个组件:

RecyclerView继承ViewGroup,但是属于androidx.recyclerview.widget包的。

ConstraintLayout继承ViewGroup,但是属于androidx.constraintlayout.widget包的;

其他还有很多其他包、或自定义控件,这里就不做过多描述了。

Android中的视图类可分为3种:布局(Layout)类视图容器(View Container)类视图类(例TextView),这3种类都是android.view.View的子类。ViewGroup是一个容器类,该类也是View的重要子类,所有的布局类和视图容器类都是ViewGroup的子类,而视图类直接继承自View类。 下图描述了View、ViewGroup、视图容器类及视图类的继承关系。

从上图所示的继承关系可以看出:

  • Button、TextView、EditText都是视图类,TextView是Button和EditText的父类,TextView直接继承自View类。
  • GridView和ListView是ViewGroup的子类,但并不是直接子类,GridView、ListView继承自AbsListView继承自AdapterView继承自ViewGroup,从而形成了视图容器类的层次结构。
  • 布局视图虽然也属于容器视图,但由于布局视图具有排版功能,所以将这类视图置为布局类

对于一个Android应用的图形用户界面来说,ViewGroup作为容器来装其他组件,而ViewGroup里除了可以包含普通View组件之外,还可以再次包含ViewGroup组件。

创建View对象

使用XML布局定义View,再用代码控制View

XML布局文件是Android系统中定义视图的常用方法,所有的XML布局文件必须保存在res/layout目录中。XML布局文件的命名及定义需要注意如下几点:

  • XML布局文件的扩展名必须是xml。
  • 由于aapt会根据每一个XML布局文件名在R类的内嵌类中生成一个int类型的变量,这个变量名就是XML布局文件名,因此,XML布局文件名(不包含扩展名)必须符合Java变量名的命名规则,例如,XML布局文件名不能以数字开头。
  • 每一个XML布局文件的根节点可以是任意的视图标签,如< LinearLayout >,< TextView >。
  • XML布局文件的根节点必须包含android命名空间,而且命名空间的值必须是android="schemas.android.com/apk/res/and…
  • 为XML布局文件中的标签指定ID时需要使用这样的格式:@+id/tv_xml,其实@+id就是在R.java文件里新增一个id名称,在同一个xml文件中确保ID唯一。
  • 由于每一个视图ID都会在R.id类中生成与之相对应的变量,因此,视图ID的值也要符合Java变量的命名规则,这一点与XML布局文件名的命名规则相同。

举例

1.创建activity_view.xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color_666666"
    android:orientation="vertical">
    <TextView
        android:id="@+id/tv_xml"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dimen_20"
        android:background="@color/color_188FFF"
        android:padding="@dimen/dimen_10"
        android:text="XML设置TextView"
        android:textColor="@color/white"
        android:textSize="@dimen/text_size_18" />
    <Button
        android:id="@+id/btn_xml"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dimen_20"
        android:background="@color/color_188FFF"
        android:padding="@dimen/dimen_10"
        android:text="按钮"
        android:textColor="@color/white"
        android:textSize="@dimen/text_size_18" />
</LinearLayout>

2.加载布局文件、关联控件

如果要使用上面的XML布局文件(activity_view.xml),通常需要在onCreate方法中使用setContentView方法指定XML布局文件的资源lD,并获取在activity_view.xml文件中定义的某个View,代码如下:

public class ViewActivity extends AppCompatActivity{
    private Button btnXml;
    private TextView tvXml;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //加载布局文件
        setContentView(R.layout.activity_view);
        //如果想获得在activity_view.xml文件中定义的某个View
        //关联控件:R.id.tv_xml是tvXml的ID,确保这个ID在R.layout.activity_view中
        tvXml = findViewById(R.id.tv_xml);
        //关联控件:R.id.btn_xml是btnXml的ID,确保这个ID在R.layout.activity_view中
        btnXml = findViewById(R.id.btn_xml);
    }
}

3.在获得XML布局文件中的视图对象时需要注意下面几点:

  • 先使用setContentView方法装载XML布局文件,再使用findViewByld方法,否则findViewByld方法会由于未找到控件而产生空指针异常,导致应用崩溃。

  • 虽然所有的XML布局文件中的视图ID都在R.id类中生成了相应的变量,但使用findViewByld方法只能获得已经装载的XML布局文件中的视图对象。

    • 例,activity_view.xml中TextView的对应R.id.tv_xml;
    • 其他XML文件中有TextView的R.id.tv_shuaiciid,tv_shuaici不在activity_view.xml中如果使用 tvXml = findViewById(R.id.tv_shuaici);
    • 结果应用崩溃。原因:在activity_view.xml中找不到ID为tv_shuaici的视图对象。

4.用代码控制视图

虽然使用XML布局文件可以非常方便地对控件进行布局,但若想控制这些控件的行为,仍然需要编写Java代码。在上面介绍了使用findViewByld方法获得指定的视图对象,当获得视图对象后,就可以使用代码来控制这些视图对象了。例如,下面的代码获得了一个TextView对象,并修改了TextView的文本。

TextView tvXml = findViewById(R.id.tv_xml);
//直接使用字符串来修改TextView的文本
tvXml.setText("帅次");
//使用字符串资源(res/values/strings.xml)
//其中R.string.str_tv_shuaici是字符串资源ID,系统会使用这个ID对应的字符串设置TextView的文本。
tvXml.setText(R.string.str_tv_shuaici);

选择其中一样即可,如果同时设置,最后一次设置为最终结果。

使用代码的方式来创建View对象

在更高级的Android应用中,往往需要动态添加视图。要实现这个功能,最重要的是获得当前的视图容器对象,这个容器对象所对应的类需要继承ViewGroup类。 将其他的视图添加到当前的容器视图中需要如下几步:

  • 第1步,获得当前的容器视图对象;
  • 第2步,获得或创建待添加的视图对象;
  • 第3步,将相应的视图对象添加到容器视图中。

实例

1.获得当前的容器视图对象

//1、获取activity_view.xml中LinearLayout对象
 //2、也可以给LinearLayout添加@+id/,然后通过findViewById关联控件也能获取LinearLayout对象
LinearLayout linearLayout =
        (LinearLayout)getLayoutInflater().inflate(R.layout.activity_view,null);
//加载布局文件
setContentView(linearLayout);

2.获得或创建待添加的视图对象

EditText editText = new EditText(this);
editText.setHint("请输入内容");

3.将相应的视图对象添加到容器视图中

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //1、获取activity_view.xml中LinearLayout对象
        //2、也可以给LinearLayout添加@+id/,然后通过findViewById关联控件也能获取LinearLayout对象
        LinearLayout linearLayout =
                (LinearLayout)getLayoutInflater().inflate(R.layout.activity_view,null);
        //加载布局文件
        setContentView(linearLayout);
        EditText editText = new EditText(this);
        editText.setHint("请输入内容");
        linearLayout.addView(editText);
    }

效果图如下:

总结

  • 实际上不管使用那种方式,他们创建Android用户界面行为的本质是完全一样的。大部分时候,设置UI组件的XML属性还有对应的方法。
  • 对于View类而言,它是所有UI组件的基类,因此它包含的XML属性和方法是所有组件都可以使用的。

自定义View

为什么要自定义View

Android系统提供了一系列的原生控件,但这些原生控件并不能够满足我们的需求时,我们就需要自定义View了。

自定义View的基本方法

自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。

  • 测量:onMeasure()决定View的大小;
  • 布局:onLayout()决定View在ViewGroup中的位置;
  • 绘制:onDraw()决定绘制这个View。

需要用到的两个对象

  • Canvas(画布),可在画布上面绘制东西,绘制的内容取决于所调用的方法。如drawCircle方法,用来绘制圆形,需要我们传入圆心的x和y坐标,以及圆的半径。
  • Paint(画笔),用来告诉画布,如何绘制那些要被绘制的对象。

这两个方法暂时了解就行,如果拓展开,这不够写,后面可能会针对这两个对象单独拉一个章节出来。

自绘控件View实例

1、直接继承View类

自绘View控件时,最主要工作就是绘制出丰富的内容,这一过程是在重写的onDraw方法中实现的。由于是View,它没有子控件了,所以重写onLayout没有意义。onMeasure的方法可以根据自己的需要来决定是否需要重写,很多情况下,不重写该方法并不影响正常的绘制。

/**
 * 创建人:scc
 * 功能描述:自定义View
 */

public class CustomView extends View {
    private Paint paint;
    //从代码创建视图时使用的简单构造函数。
    public CustomView(Context context) {
        super(context);
    }
    //从XML使用视图时调用的构造函数。
    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    //View的绘制工作
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //实例化画笔对象
        paint = new Paint();
        //给画笔设置颜色
        paint.setColor(Color.RED);
        //设置画笔属性
        //paint.setStyle(Paint.Style.FILL);//画笔属性是实心圆
        paint.setStyle(Paint.Style.STROKE);//画笔属性是空心圆
        paint.setStrokeWidth(10);//设置画笔粗细
        //cx:圆心的x坐标;cy:圆心的y坐标;参数三:圆的半径;参数四:定义好的画笔
        canvas.drawCircle(getWidth() / 4, getHeight() / 4150, paint);
    }
}

2、在布局 XML 文件中使用自定义View

<com.scc.demo.view.CustomView
        android:id="@+id/view_circle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

3、实现效果:

性能优化

在自定义View时需要注意,避免犯以下的性能错误:

  • 在非必要时,对View进行重绘。
  • 绘制一些不被用户所看到的的像素,也就是过度绘制。(被覆盖的地方)
  • 在绘制期间做了一些非必要的操作,导致内存资源的消耗。

可进一步了解和优化:

  • View.invalite()是最最广泛的使用操作,因为在任何时候都是刷新和更新视图最快的方式。

在自定义View时要小心避免调用非必要的方法,因为这样会导致重复强行绘制整个视图层级,消耗宝贵的帧绘制周期。检查清楚View.invalite()和View.requestLayout()方法调用时间位置,因为这会影响整个UI,导致GPU和它的帧速率变慢。

  • 避免过渡重绘。为了避免过渡重绘,我们可以利用Canvas方法,只绘制控件中所需要的部分。整个一般在重叠部分或控件时特别有用。相应的方法是Canvas.clipRect()(指定要被绘制的区域);
  • 在实现View.onDraw()方法中,不应该在方法内及调用的方法中进行任何的对象分配。在该方法中进行对象分配,对象会被创建和初始化。而当View.onDraw()方法执行完毕时。垃圾回收器会释放内存。如果View带动画,那么View在一秒内会被重绘60次。所以要避免在View.onDraw()方法中分配内存。

永远不要在View.onDraw()方法中及调用的方法中进行内存分配,避免带来负担。垃圾回收器多次释放内存,会导致卡顿。最好的方式就是在View被首次创建出来时,实例化这些对象。

到这里View基本差不多了,还有其他属性、方法、事件等,在后面的TexView、Button、Layout等中慢慢了解。

收起阅读 »

iOS开发常见面试题(底层篇)

1.iOS 类(class)和结构体(struct)有什么区别?Swift 中,类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型的区别。举个简单的例子,代码如下clas...
继续阅读 »

1.iOS 类(class)和结构体(struct)有什么区别?

Swift 中,类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型的区别。

举个简单的例子,代码如下

class Temperature {
var value: Float = 37.0
}

class Person {
var temp: Temperature?

func sick() {
temp?.value = 41.0
}
}

let A = Person()
let B = Person()
let temp = Temperature()

A.temp = temp
B.temp = temp

A.sick() 上面这段代码,由于 Temperature 是 class ,为引用类型,故 A 的 temp 和 B 的 temp指向同一个对象。A 的 temp修改了,B 的 temp 也随之修改。这样 A 和 B 的 temp 的值都被改成了41.0。如果将 Temperature 改为 struct,为值类型,则 A 的 temp 修改不影响 B 的 temp。

内存中,引用类型诸如类是在堆(heap)上,而值类型诸如结构体实在栈(stack)上进行存储和操作。相比于栈上的操作,堆上的操作更加复杂耗时,所以苹果官方推荐使用结构体,这样可以提高 App 运行的效率。

class有这几个功能struct没有的:

class可以继承,这样子类可以使用父类的特性和方法 类型转换可以在runtime的时候检查和解释一个实例的类型 可以用deinit来释放资源 一个类可以被多次引用 struct也有这样几个优势:

结构较小,适用于复制操作,相比于一个class的实例被多次引用更加安全。 无须担心内存memory leak或者多线程冲突问题


2.iOS自动释放池是什么,如何工作 ?

当您向一个对象发送一个autorelease消息时,Cocoa就会将该对象的一个引用放入到最新的自动释放池。它仍然是个正当的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放。

1.object-c 是通过一种"referring counting"(引用计数)的方式来管理内存的, 对象在开始分配内存(alloc)的时候引用计数为一,以后每当碰到有copy,retain的时候引用计数都会加一, 每当碰到release和autorelease的时候引用计数就会减一,如果此对象的计数变为了0, 就会被系统销毁.

2.NSAutoreleasePool 就是用来做引用计数的管理工作的,这个东西一般不用你管的.

3.autorelease和release没什么区别,只是引用计数减一的时机不同而已,autorelease会在对象的使用真正结束的时候才做引用计数减一.

3.iOS你在项目中用过 runtime 吗?举个例子

Objective-C 语言是一门动态语言,编译器不需要关心接受消息的对象是何种类型,接收消息的对象问题也要在运行时处理。

pragramming 层面的 runtime 主要体现在以下几个方面:

1.关联对象 Associated Objects
2.消息发送 Messaging
3.消息转发 Message Forwarding
4.方法调配 Method Swizzling
5.“类对象” NSProxy Foundation | Apple Developer Documentation
6.KVC、KVO About Key-Value Coding

4.KVC /KVO的底层原理和使用场景

1 KVC(KeyValueCoding)
1.1 KVC 常用的方法

(1)赋值类方法
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

(2)取值类方法
// 能取得私有成员变量的值
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

1.2 KVC 底层实现原理

当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作:
1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值
2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(_key).如果有,直接给该成员属性进行赋值
3.如果没有成员属性_key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值
4.如果都没有,就会调用 valueforUndefinedKey 和setValue:forUndefinedKey:方法
1.3 KVC 的使用场景
1.3.1 赋值
(1) KVC 简单属性赋值

Person *p = [[Person alloc] init];
// p.name = @"jack";
// p.money = 22.2;
使用setValue: forKey:方法能够给属性赋值,等价于直接给属性赋值
[p setValue:@"rose" forKey:@"name"];
[p setValue:@"22.2" forKey:@"money"];

(2) KVC复杂属性赋值

//给Person添加 Dog属性
Person *p = [[Person alloc] init];
p.dog = [[Dog alloc] init];
// p.dog.name = @"阿黄";

1)setValue: forKeyPath: 方法的使用
//修改p.dog 的name 属性
[p.dog setValue:@"wangcai" forKeyPath:@"name"];
[p setValue:@"阿花" forKeyPath:@"dog.name"];

2)setValue: forKey: 错误用法
[p setValue:@"阿花" forKey:@"dog.name"];
NSLog(@"%@", p.dog.name);

3)直接修改私有成员变量
[p setValue:@"旺财" forKeyPath:@"_name"];

(3) 添加私有成员变量

Person 类中添加私有成员变量_age
[p setValue:@"22" forKeyPath:@"_age"];

1.3.2 字典转模型

(1)简单的字典转模型
+(instancetype)videoWithDict:(NSDictionary *)dict
{
JLVideo *videItem = [[JLVideo alloc] init];
//以前
// videItem.name = dict[@"name"];
// videItem.money = [dict[@"money"] doubleValue] ;

//KVC,使用setValuesForKeysWithDictionary:方法,该方法默认根据字典中每个键值对,调用setValue:forKey方法
// 缺点:字典中的键值对必须与模型中的键值对完全对应,否则程序会崩溃
[videItem setValuesForKeysWithDictionary:dict];
return videItem;
}

(2)复杂的字典转模型
注意:复杂字典转模型不能直接通过KVC 赋值,KVC只能在简单字典中使用,比如:
NSDictionary *dict = @{
@"name" : @"jack",
@"money": @"22.2",
@"dog" : @{
@"name" : @"wangcai",
@"money": @"11.1",

}

};
JLPerson *p = [[JLPerson alloc]init]; // p是一个模型对象
[p setValuesForKeysWithDictionary:dict];
内部转换原理:
// [p setValue:@"jack" forKey:@"name"];
// [p setValue:@"22.2" forKey:@"money"];
// [p setValue:@{
// @"name" : @"wangcai",
// @"money": @"11.1",
//
// } forKey:@"dog"]; //给 dog赋值一个字典肯定是不对的

(3)KVC解析复杂字典的正确步骤
NSDictionary *dict = @{
@"name" : @"jack",
@"money": @"22.2",
@"dog" : @{
@"name" : @"wangcai",
@"price": @"11.1",
},
//人有好多书
@"books" : @[
@{
@"name" : @"5分钟突破iOS开发",
@"price" : @"19.8"
},
@{
@"name" : @"3分钟突破iOS开发",
@"price" : @"24.8"
},
@{
@"name" : @"1分钟突破iOS开发",
@"price" : @"29.8"
}
]
};

XMGPerson *p = [[XMGPerson alloc] init];
p.dog = [[XMGDog alloc] init];
[p.dog setValuesForKeysWithDictionary:dict[@"dog"]];

//保存模型的可变数组
NSMutableArray *arrayM = [NSMutableArray array];

for (NSDictionary *dict in dict[@"books"]) {
//创建模型
Book *book = [[Book alloc] init];
//KVC
[book setValuesForKeysWithDictionary:dict];
//将模型保存
[arrayM addObject:book];
}

p.books = arrayM;

备注:
(1)当字典中的键值对很复杂,不适合用KVC;
(2)服务器返还的数据,你可能不会全用上,如果在模型一个一个写属性非常麻烦,所以不建议使用KVC字典转模型

1.3.3 取值
(1) 模型转字典

 Person *p = [[Person alloc]init];
p.name = @"jack";
p.money = 11.1;
//KVC取值
NSLog(@"%@ %@", [p valueForKey:@"name"], [p valueForKey:@"money"]);

//模型转字典, 根据数组中的键获取到值,然后放到字典中
NSDictionary *dict = [p dictionaryWithValuesForKeys:@[@"name", @"money"]];
NSLog(@"%@", dict);

(2) 访问数组中元素的属性值

Book *book1 = [[Book alloc] init];
book1.name = @"5分钟突破iOS开发";
book1.price = 10.7;

Book *book2 = [[Book alloc] init];
book2.name = @"4分钟突破iOS开发";
book2.price = 109.7;

Book *book3 = [[Book alloc] init];
book3.name = @"1分钟突破iOS开发";
book3.price = 1580.7;

// 如果valueForKeyPath:方法的调用者是数组,那么就是去访问数组元素的属性值
// 取得books数组中所有Book对象的name属性值,放在一个新的数组中返回
NSArray *books = @[book1, book2, book3];
NSArray *names = [books valueForKeyPath:@"name"];
NSLog(@"%@", names);

//访问属性数组中元素的属性值
Person *p = [[Person alloc]init];
p.books = @[book1, book2, book3];
NSArray *names = [p valueForKeyPath:@"books.name"];
NSLog(@"%@", names);

2 KVO (Key Value Observing)
2.1 KVO 的底层实现原理

(1)KVO 是基于 runtime 机制实现的
(2)当一个对象(假设是person对象,对应的类为 JLperson)的属性值age发生改变时,系统会自动生成一个继承自JLperson的类NSKVONotifying_JLPerson,在这个类的 setAge 方法里面调用
[super setAge:age];
[self willChangeValueForKey:@"age"];
[self didChangeValueForKey:@"age"];
三个方法,而后面两个方法内部会主动调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context方法,在该方法中可以拿到属性改变前后的值.

2.2 KVO的作用
作用:能够监听某个对象属性值的改变

// 利用KVO监听p对象name 属性值的改变
Person *p = [[XMGPerson alloc] init];
p.name = @"jack";

/* 对象p添加一个观察者(监听器)
Observer:观察者(监听器)
KeyPath:属性名(需要监听哪个属性)
*/

[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];

/**
* 利用KVO 监听到对象属性值改变后,就会调用这个方法
*
* @param keyPath 哪一个属性被改了
* @param object 哪一个对象的属性被改了
* @param change 改成什么样了
*/

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
// NSKeyValueChangeNewKey == @"new"
NSString *new = change[NSKeyValueChangeNewKey];
// NSKeyValueChangeOldKey == @"old"
NSString *old = change[NSKeyValueChangeOldKey];

NSLog(@"%@-%@",new,old);
}

5.iOS中持久化方式有哪些?

属性列表文件 -- NSUserDefaults 的存储,实际是本地生成一个 plist 文件,将所需属性存储在 plist 文件中

对象归档 -- 本地创建文件并写入数据,文件类型不限

SQLite 数据库 -- 本地创建数据库文件,进行数据处理

CoreData -- 同数据库处理思想相同,但实现方式不同

6.什么是KVC和KVO?

KVC(Key-Value-Coding)内部的实现:一个对象在调用setValue的时候
(1)首先根据方法名找到运行方法的时候所需要的环境参数。
(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。
(3)再直接查找得来的具体的方法实现。KVO(Key-Value- Observing):当观察者为一个对象的属性进行了注册,被观察对象的isa指针被修改的时候,isa指针就会指向一个中间类,而不是真实的类。所以 isa指针其实不需要指向实例对象真实的类。所以我们的程序最好不要依赖于isa指针。在调用类的方法的时候,最好要明确对象实例的类名

7.iOS中属性修饰符的作用?

ios5之前是MRC,内存需要程序员进行管理,ios5之后是ARC,除非特殊情况,比如C框架或者循环引用,其他时候是不需要程序员手动管理内存的。 ios中当我们定义属性@property的时候就需要属性修饰符,下面我们就看一下不同属性修饰符的作用。有错误和不足的地方还请大家谅解并批评指正。

主要的属性修饰符有下面几种:

  • copy
  • assign
  • retain
  • strong
  • weak
  • readwrite/readonly (读写策略、访问权限)
  • nonatomic/atomic (安全策略)

如果以MRC和ARC进行区分修饰符使用情况,可以按照如下方式进行分组:

 1. MRC: assign/ retain/ copy/  readwritereadonly/ nonatomic、atomic  等。
2. ARC: assign/ strong/ weak/ copy/ readwritereadonly/ nonatomic、atomic 等。

属性修饰符对retainCount计数的影响。

  1. alloc为对象分配内存,retainCount 为1 。
  2. retain MRC下 retainCount + 1。
  3. copy 一个对象变成新的对象,retainCount为 1, 原有的对象计数不变。
  4. release 对象的引用计数 -1。
  5. autorelease 对象的引用计数 retainCount - 1,如果为0,等到最近一个pool结束时释放。

不管MRC还是ARC,其实都是看reference count是否为0,如果为0那么该对象就被释放,不同的地方是MRC需要程序员自己主动去添加retain 和 release,而ARC apple已经给大家做好,自动的在合适的地方插入retain 和 release类似的内存管理代码,具体原理如下,图片摘自官方文档。



MRC 和 ARC原理

下面就详述上所列的几种属性修饰符的使用场景,应用举例和注意事项。

8.iOS atomatic nonatomic区别和理解

第一种

atomic和nonatomic区别用来决定编译器生成的getter和setter是否为原子操作。atomic提供多线程安全,是描述该变量是否支持多线程的同步访问,如果选择了atomic 那么就是说,系统会自动的创建lock锁,锁定变量。nonatomic禁止多线程,变量保护,提高性能。

atomic:默认是有该属性的,这个属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题。

nonatomic:如果该对象无需考虑多线程的情况,请加入这个属性,这样会让编译器少生成一些互斥加锁代码,可以提高效率。

atomic的意思就是setter/getter这个函数,是一个原语操作。如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,可以保证数据的完整性。nonatomic不保证setter/getter的原语行,所以你可能会取到不完整的东西。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。

比如setter函数里面改变两个成员变量,如果你用nonatomic的话,getter可能会取到只更改了其中一个变量时候的状态,这样取到的东西会有问题,就是不完整的。当然如果不需要多线程支持的话,用nonatomic就够了,因为不涉及到线程锁的操作,所以它执行率相对快些。

下面是载录的网上一段加了atomic的例子:




{lock}
if (property != newValue) {
[property release];
property = [newValue retain];
}
{unlock}

可以看出来,用atomic会在多线程的设值取值时加锁,中间的执行层是处于被保护的一种状态,atomic是oc使用的一种线程保护技术,基本上来讲,就是防止在写入未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

第二种

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。

atomic
设置成员变量的@property属性时,默认为atomic,提供多线程安全。

在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。加了atomic,setter函数会变成下面这样:


                    {lock}
if (property != newValue) {
[property release];
property = [newValue retain];
}
{unlock}

nonatomic
3禁止多线程,变量保护,提高性能。

3atomic是Objc使用的一种线程保护技术,基本上来讲,是防止在写未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

3指出访问器不是原子操作,而默认地,访问器是原子操作。这也就是说,在多线程环境下,解析的访问器提供一个对属性的安全访问,从获取器得到的返回值或者通过设置器设置的值可以一次完成,即便是别的线程也正在对其进行访问。如果你不指定 nonatomic ,在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值。

9.iOS UIViewController的完整生命周期

UIViewController的完整生命周期

-[ViewControllerinitWithNibName:bundle:];

-[ViewControllerinit];

-[ViewControllerloadView];

-[ViewControllerviewDidLoad];

-[ViewControllerviewWillDisappear:];

-[ViewControllerviewWillAppear:];

-[ViewControllerviewDidAppear:];

-[ViewControllerviewDidDisappear:];

1、 alloc 创建对象,分配空间

2、init(initWithNibName) 初始化对象,初始化数据

3、loadView 从nib载入视图 ,通常这一步不需要去干涉。除非你没有使用xib文件创建视图

4、viewDidLoad 载入完成,可以进行自定义数据以及动态创建其他控件

5、viewWillAppear 视图将出现在屏幕之前,马上这个视图就会被展现在屏幕上了

6、viewDidAppear 视图已在屏幕上渲染完成

当一个视图被移除屏幕并且销毁的时候的执行顺序,这个顺序差不多和上面的相反

1、viewWillDisappear 视图将被从屏幕上移除之前执行

2、viewDidDisappear 视图已经被从屏幕上移除,用户看不到这个视图了

3、dealloc 视图被销毁,此处需要对你在init和viewDidLoad中创建的对象进行释放

ViewController 的 loadView,、viewDidLoad,、viewDidUnload 分别是在什么时候调用的?

viewDidLoad在view从nib文件初始化时调用,loadView在controller的view为nil时调用。

此方法在编程实现view时调用,view控制器默认会注册memory warning notification,当view controller的任何view没有用的时候,viewDidUnload会被调用,在这里实现将retain的view release,如果是retain的IBOutlet view属性则不要在这里release,IBOutlet会负责release。

10.ios7 层协议,tcp四层协议及如何对应的?


11.iOS应用导航模式有哪些?

平铺模式,一般由scrollView和pageControl组合而成的展示方式。手机自带的天气比较典型。

标签模式,tabBar的展示方式,这个比较常见。

树状模式,tableView的多态展示方式,常见的9宫格、系统自带的邮箱等展现方式。

12.一个参数既可以是const还可以是volatile吗?解释为什么。

• 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

13.iOS 响应者链的事件传递过程?

如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图

在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

如果window对象也不处理,则其将事件或消息传递给UIApplication对象

如果UIApplication也不能处理该事件或消息,则将其丢弃

14.iOS 请说明并比较以下关键词:weak,block

weak与weak基本相同。前者用于修饰变量(variable),后者用于修饰属性(property)。weak 主要用于防止block中的循环引用。 block也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。block用于修饰某些block内部将要修改的外部变量。 weak和block的使用场景几乎与block息息相关。而所谓block,就是Objective-C对于闭包的实现。闭包就是没有名字的函数,或者理解为指向函数的指针。

15.iOS UIView的Touch事件注意点

如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件(掌握)
UIView不接收触摸事件的三种情况:
不接收用户交互 : userInteractionEnabled = NO
隐藏 : hidden = YES
透明 : alpha = 0.0 ~ 0.01
UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的

16.iOS 说明并比较关键词:strong, weak, assign, copy等等

strong表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。

weak表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。

assign主要用于修饰基本数据类型,如NSInteger和CGFloat,这些数值主要存在于栈上。

weak 一般用来修饰对象,assign一般用来修饰基本数据类型。原因是assign修饰的对象被释放后,指针的地址依然存在,造成野指针,在堆上容易造成崩溃。而栈上的内存系统会自动处理,不会造成野指针。

copy与strong类似。不同之处是strong的复制是多个指针指向同一个地址,而copy的复制每次会在内存中拷贝一份对象,指针指向不同地址。copy一般用在修饰有可变对应类型的不可变对象上,如NSString, NSArray, NSDictionary。

Objective-C 中,基本数据类型的默认关键字是atomic, readwrite, assign;普通属性的默认关键字是atomic, readwrite, strong。

1、属性readwrite,readonly,assign,retain,copy,nonatomic 各自什么作用,他们在那种情况下用?

readwrite:默认的属性,可读可写,生成settergetter方法。

readonly:只读,只生成getter方法,也就是说不能修改变量。

assign:用于声明基本数据类型(intfloat)仅设置变量,是赋值属性。

retain:持有属性,setter方法将传入的参数先保留,再赋值,传入的参数 引用计数retaincount 会加1

在堆上开辟一块空间,用指针a指向,然后将指针a赋值(assign)给指针b,等于是a和b同时指向这块堆空间,当a不使用这块堆空间的时候,是否要释放这块堆空间?答案是肯定要的,但是这件堆空间被释放后,b就成了野指针。

如何避免这样的问题? 这就引出了引用计数器,当a指针这块堆空间的时候,引用计数器+1,当b也指向的时候,引用计数器变成了2,当a不再指向这块堆空间时,release-1,引用计数器为1,当b也不指向这块堆空间时,release-1,引用计数器为0,调用dealloc函数,空间被释放

总结:当数据类型为int,float原生类型时,可以使用assign。如果是上面那种情况(对象)就是用retain。

copy:是赋值特性,setter方法将传入对象赋值一份;需要完全一份新的变量时,直接从堆区拿。

当属性是 NSString、NSArray、NSDictionary时,既可以用strong 修饰,也可以用copy修饰。当用strong修饰的NSString 指向一个NSMutableString时,如果在不知情的情况下这个NSMutableString的别的引用修改了值,就会出现:一个不可变的字符串却被改变了的情况, 使用copy就不会出现这种情况。


nonatomic:非原子性,可以多线程访问,效率高。
atomic:原子性,属性安全级别的表示,同一时刻只有一个线程访问,具有资源的独占性,但是效率很低。
strong:强引用,引用计数+ 1,ARC下,一个对象如果没有强引用,系统就会释放这个对象。
weak:弱引用,不会使引用计数+1.当一个指向对象的强引用都被释放时,这块空间依旧会被释放掉。

使用场景:在ARC下,如果使用XIB 或者SB 来创建控件,就使用 weak。纯代码创建控件时,用strong修饰,如果想用weak 修饰,就需要先创建控件,然后赋值给用weak修饰的对象。

查找了一些资料,发现主要原因是,controller需要拥有它自己的view(这个view是所以子控件的父view),因此viewcontroller对view就必须是强引用(strong reference),得用strong修饰view。对于lable,它的父view是view,view需要拥有label,但是controller是不需要拥有label的。如果用strong修饰,在view销毁的情况下,label还仍然占有内存,因为controller还对它强引用;如果用wak修饰,在view销毁的时label的内存也同时被销毁,避免了僵尸指针出现。

用引用计数回答就是:因为Controller并不直接“拥有”控件,控件由它的父view“拥有”。使用weak关键字可以不增加控件引用计数,确保控件与父view有相同的生命周期。控件在被addSubview后,相当于控件引用计数+1;父view销毁后,所有的子view引用计数-1,则可以确保父view销毁时子view立即销毁。weak的控件在removeFromSuperview后也会立即销毁,而strong的控件不会,因为Controller还保有控件强引用。

总结归纳为:当控件的父view销毁时,如果你还想继续拥有这个控件,就用srtong;如果想保证控件和父view拥有相同的生命周期,就用weak。当然在大多数情况下用两个都是可以的。

使用weak的时候需要特别注意的是:先将控件添加到superview上之后再赋值给self,避免控件被过早释放。

17.iOS里什么是响应链,它是怎么工作的?

第一反应就是,响应链就是响应链啊,由一串UIResponder对象链接,收到响应事件时由上往下传递,直到能响应事件为止。

但其中却大有文章...

1.由一串UIResponder对象链接 ?

我们知道UIResponder类里有个属性:

@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

如果我们对响应链原理不清楚的话,会很容易的认为,这条链是由 nextResponder 指针连接起来的,在寻找响应者的时候是顺着这个指针找下去直到找到响应者为止的,但这是错误的认为。 举个例子: 现在我们有这样一个场景:

AppDelegate上的Window上有一个UIViewController *ViewController, 然后在ViewController.view 上按顺序添加viewA和viewB,viewB稍微覆盖viewA一部分用来测试, 给viewA,viewB 分别添加点击手势tapA 和 tapB,然后把viewB.userInteractionEnabled = NO,让viewB不能响应点击。

然后我们点击重复的那块区域,会发现viewA响应了tap手势,执行了tapA的事件。 我们知道viewB设置了viewB.userInteractionEnabled = NO,不响应tap手势是正常的,但怎么会透过viewB,viewA响应了手势?

我们知道nextResponder指针指向的规则:

  • UIView
  • 如果 view 是一个 view controller 的 root view,nextResponder 是这个 view controller.
  • 如果 view 不是 view controller 的 root view,nextResponder 则是这个 view 的 superview
  • UIViewController
  • 如果 view controller 的 view 是 window 的 root view, view controller 的 nextResponder 是这个 window
  • 如果 view controller 是被其他 view controller presented调起来的,那么 view controller 的 nextResponder 就是发起调起的那个 view controller
  • UIWindow
  • window 的 nextResponder 是 UIApplication 对象.
  • UIApplication
  • UIApplication 对象的 nextResponder 是 app delegate, 但是 app delegate 必须是 UIResponder 对象,并且不能使 view ,view controller 或 UIApplication 对象他本身.

那么上述情况下,viewB所在的响应者链应该是: viewB -> ViewController.view -> ViewController -> Window -> Application 这种情况下怎么也轮不到viewA去响应啊。

所以,当有事件需要响应时,nextResponder 并不是链接响应链的那根绳子,响应链的工作方式另有别的方式

2. 那么响应链是如何工作,正确找到应该响应该事件的响应者的?

UIKit使用基于视图的hit-testing来确定touch事件发生的位置。具体解释就是,UIKit将touch的位置和视图层级中的view的边界进行了比较,UIView的方法 hitTest:withEvent: 在视图层级中进行,寻找包含指定touch的最深子视图。这个视图成为touch事件的第一个响应者。

说白了就是,当有touch事件来的时候,会从最下面的视图开始执行 hitTest:withEvent: ,如果符合成为响应者的条件,就会继续遍历它的 subviews 继续执行 hitTest:withEvent: ,直到找到最合适的view成为响应者。

这里要注意几个点:

  • 符合响应者的条件包括
  • touch事件的位置在响应者区域内
  • 响应者 hidden 属性不为 YES
  • 响应者 透明度 不是 0
  • 响应者 userInteractionEnabled 不为 NO
  • 遍历 subview 时,是从上往下顺序遍历的,即 view.subviews 的 lastObject 到 firstObject 的顺序,找到合适的响应者view,即停止遍历.

所以再回看上面的例子,当我们点击中间的重复区域时,流程其实是这样:

  • AppDelegate 的 window 收到事件,并开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
  • window 上只有 viewcontroller.view ,所以viewcontroller.view 开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
  • viewcontroller.view 有两个子view, viewA 和 viewB ,但是viewB 在 viewA 上边,所以先 viewB 执行 hitTest:withEvent: ,结果发现viewB 不符合要求,因为viewB 的 userInteractionEnabled 为 NO.
  • 接下来 viewA 执行 hitTest:withEvent: ,发现符合条件,并且viewA 也没有子view可去遍历,于是返回viewA.
  • viewA成了最终事件的响应者.

这样就完美解释了,最开始例子的响应状况.

那么如果 viewB 的 userInteractionEnabled 属性为YES的话,是怎么样的呢?

如果 viewB 的 userInteractionEnabled 属性为YES,上面流程的第三部就会发现viewB是符合要求的,而直接返回viewB作为最终响应者,中断子view的遍历,viewA都不会被遍历到了.

这就是响应链相关的点,如果有什么不对的请留言提示,然后有什么别的需要补充的我会及时补充~

18.什么是iOS的动态绑定 ?

—在运行时确定要调用的方法

动态绑定将调用方法的确定也推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,您的代码每次执行都可以得到不同的结果。运行时因子负责确定消息的接收者和被调用的方法。运行时的消息分发机制为动态绑定提供支持。当您向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的isa指针定位对象的类,并以此为起点确定被调用的方法,方法和消息是动态绑定的。而且,您不必在Objective-C 代码中做任何工作,就可以自动获取动态绑定的好处。您在每次发送消息时,

特别是当消息的接收者是动态类型已经确定的对象时,动态绑定就会例行而透明地发生。

19.iOS单元测试框架有哪些?

OCUnit 是 OC 官方测试框架, 现在被 XCTest 所取代。 XCTest 是与 Foundation 框架平行的测试框架。 GHUnit 是第三方的测试框架。github地址OCMock都是第三方的测试框架。

20.iOS ARC全解?

考查点

我记得在刚接触iOS的时候对这个ARC和MRC就讨论颇深,认为ARC是对程序员的一种福利,让我们节省了大量的代码,那么ARC是什么呢?

ARC 是苹果在 WWDC 2011 提出来的技术,因此很多新入行的同学可能对此技术细节并不熟悉。但是,虽然 ARC 极大地简化了我们的内存管理工作,但是引用计数这种内存管理方案如果不被理解,那么就无法处理好那些棘手的循环引用问题。所以,这道面试题其实是考查同学对于 iOS 程序内存管理的理解深度。 答案

自动的引用计数(Automatic Reference Count 简称 ARC),是苹果在 WWDC 2011 年大会上提出的用于内存管理的技术。

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt) 等语言也提供了基于引用计数的内存管理方式。

引用计数这种内存管理方式虽然简单,但是手工写大量的操作引用计数的代码不但繁琐,而且容易被遗漏。于是苹果在 2011 年引入了 ARC。ARC 顾名思义,是自动帮我们填写引用计数代码的一项功能。

ARC 的想法来源于苹果在早期设计 Xcode 的 Analyzer 的时候,发现编译器在编译时可以帮助大家发现很多内存管理中的问题。后来苹果就想,能不能干脆编译器在编译的时候,把内存管理的代码都自动补上,带着这种想法,苹果修改了一些内存管理代码的书写方式(例如引入了 @autoreleasepool 关键字)后,在 Xcode 中实现了这个想法。

ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:

编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。

相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1 的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

但是也有人认为,ARC 也附带有运行期的一些机制来使 ARC 能够更好的工作,他们主要是指 weak 关键字。weak 变量能够在引用计数为 0 时被自动设置成 nil,显然是有运行时逻辑在工作的。我通常并没有把这个算在 ARC 的概念当中,当然,这更多是一个概念或定义上的分歧,因为除开 weak 逻辑之外,ARC 核心的代码都是在编译期填充的。

21.iOS内存的使用和优化的注意事项

重用问题:

如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews

设置正确的reuseIdentifier,充分重用;

尽量把views设置为不透明:

当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;

不要使用太复杂的XIB/Storyboard:

载入时就会将XIB/storyboard需要的所有资源,

包括图片全部载入内存,即使未来很久才会使用。

那些相比纯代码写的延迟加载,性能及内存就差了很多;

选择正确的数据结构:

学会选择对业务场景最合适的数组结构是写出高效代码的基础。

比如,数组: 有序的一组值。

使用索引来查询很快,使用值查询很慢,插入/删除很慢。

字典: 存储键值对,用键来查找比较快。

集合: 无序的一组值,用值来查找很快,插入/删除很快。

gzip/zip压缩:

当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。

延迟加载:

对于不应该使用的数据,使用延迟加载方式。

对于不需要马上显示的视图,使用延迟加载方式。

比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。

数据缓存:

对于cell的行高要缓存起来,使得reload数据时,效率也极高。

而对于那些网络数据,不需要每次都请求的,应该缓存起来,

可以写入数据库,也可以通过plist文件存储。

处理内存警告:

一般在基类统一处理内存警告,将相关不用资源立即释放掉

重用大开销对象:

一些objects的初始化很慢,

比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。

通常是作为属性存储起来,防止反复创建。

避免反复处理数据:

许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。

在服务器端和客户端使用相同的数据结构很重要;

使用Autorelease Pool:

在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;

22.什么是iOS的目标-动作机制 ?

目标是动作消息的接收者。一个控件,或者更为常见的是它的单元,以插座变量(参见"插座变量"部分)

的形式保有其动作消息的目标。

动作是控件发送给目标的消息,或者从目标的角度看,它是目标为了响应动作而实现的方法。

程序需要某些机制来进行事件和指令的翻译。这个机制就是目标-动作机制。

23.iOS 事件传递的完整过程?

先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。

调用最合适控件的touches….方法

如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者
接着就会调用上一个响应者的touches….方法

如何判断上一个响应者:
如果当前这个view是控制器的view,那么控制器就是上一个响应者
如果当前这个view不是控制器的view,那么父控件就是上一个响应者

24.什么是iOS的响应者链?

  • 响应者链条:是由多个响应者对象连接起来的链条
  • 作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
  • 响应者对象:能处理事件的对象



25.iOS UIView的Touch事件有哪几种触摸事件?

处理事件的方法

UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件

  //一根或者多根手指开始触摸view
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
//一根或者多根手指在view上移动
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
//一根或者多根手指离开view
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

26.iOS开发:Objective-C中通知与协议的区别?

what is difference between NSNotification and protocol? (通知和协议的不同之处?)

我想大家都知道这个东西怎么用,但是更深层次的思考可能就比较少了吧,众所周知就是代理是一对一的,但是通知是可以多对多的.但是为什么是这个样子,有没有更深的思考过这个问题?

今天看了下网上的几个视频教程,KVO、KVC、谓词、通知,算是开发中的高级点的东西了。通知和协议都是类似于回调一样,于是就在思考通知和协议到底有什么不同,或者说什么时候该用通知,什么时候该用协议。

下面是网上摘抄的一段解释:

协议有控制链(has-a)的关系,通知没有。首先我一开始也不太明白,什么叫控制链(专业术语了~)。但是简单分析下通知和代理的行为模式,我们大致可以有自己的理解简单来说,通知的话,它可以一对多,一条消息可以发送给多个消息接受者。代理按我们的理解,到不是直接说不能一对多,比如我们知道的明星经济代理人,很多时候一个经济人负责好几个明星的事务。只是对于不同明星间,代理的事物对象都是不一样的,一一对应,不可能说明天要处理A明星要一个发布会,代理人发出处理发布会的消息后,别称B的发布会了。但是通知就不一样,他只关心发出通知,而不关心多少接收到感兴趣要处理。因此控制链(has-a从英语单词大致可以看出,单一拥有和可控制的对应关系。

1.通知:

通知需要有一个通知中心:NSNotificationCenter,自定义通知的话需要给一个名字,然后监听。

优点:通知的发送者和接受者都不需要知道对方。可以指定接收通知的具体方法。通知名可以是任何字符串。

缺点:较键值观察(KVO)需要多点代码,在删掉前必须移除监听者。

2.协议

通过setDelegate来设置代理对象,最典型的例子是常用的TableView.

优点:支持它的类有详尽和具体信息。

缺点:该类必须支持委托。某一时间只能有一个委托连接到某一对象。

相信看到这些东西,认真思考一下,就可以知道在那种情况下使用通知,在那种情况下使用代理了吧.

27.写一个NSString类的实现

 + (id)initWithCString:(c*****t char *)nullTerminatedCString  encoding:(NSStringEncoding)encoding;** 

+ (id) stringWithCString: (c*****t char*)nullTerminatedCString

encoding: (NSStringEncoding)encoding

{

NSString *obj;

obj = [self allocWithZone: NSDefaultMallocZone()];

obj = [obj initWithCString: nullTerminatedCString encoding: encoding];

return AUTORELEASE(obj);

}

28.iOS 事件的产生和传递流程

发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理 touchesBegan… touchesMoved… touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递(不实现touches方法,系统会自动向上一个响应者传递),将事件交给上一个响应者进行处理
如果一个事件既想自己处理也想交给上一个响应者处理,那么自己实现touches方法,并且调用super的touches方法,[super touches、、、];

29.关键字volatile有什么含意?并给出三个不同的例子?

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到

这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

• 并行设备的硬件寄存器(如:状态寄存器)

• 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

• 多线程应用中被几个任务共享的变量

30.iOS hitTest方法&pointInside方法

hitTest方法
当事件传递给控件的时候,就会调用控件的这个方法,去寻找最合适的view
point:当前的触摸点,point这个点的坐标系就是方法调用者

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
pointInside方法
作用:判断当前这个点在不在方法调用者(控件)上

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent:的实现原理

- (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.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;

for (NSInteger 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;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}


作者:iOS鑫
链接:https://www.jianshu.com/p/2cc5d8b4e8d3








收起阅读 »

iOS面试题快来来来(内存方向)

1.形成tableView卡顿的缘由有哪些?1.最经常使用的就是cell的重用, 注册重用标识符若是不重用cell时,每当一个cell显示到屏幕上时,就会从新建立一个新的cellhtml若是有不少数据的时候,就会堆积不少cell。ios若是重用cell,为ce...
继续阅读 »

1.形成tableView卡顿的缘由有哪些?

  • 1.最经常使用的就是cell的重用, 注册重用标识符

    若是不重用cell时,每当一个cell显示到屏幕上时,就会从新建立一个新的cellhtml

    若是有不少数据的时候,就会堆积不少cell。ios

    若是重用cell,为cell建立一个ID,每当须要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,若是没有再从新建立cellc++

  • 2.避免cell的从新布局

    cell的布局填充等操做 比较耗时,通常建立时就布局好面试

    如能够将cell单独放到一个自定义类,初始化时就布局好swift

  • 3.提早计算并缓存cell的属性及内容

    当咱们建立cell的数据源方法时,编译器并非先建立cell 再定cell的高度xcode

    而是先根据内容一次肯定每个cell的高度,高度肯定后,再建立要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提早估算高度告诉编译器,编译器知道高度后,紧接着就会建立cell,这时再调用高度的具体计算方法,这样能够方式浪费时间去计算显示之外的cell缓存

  • 4.减小cell中控件的数量

    尽可能使cell得布局大体相同,不一样风格的cell可使用不用的重用标识符,初始化时添加控件,网络

    不适用的能够先隐藏数据结构

  • 5.不要使用ClearColor,无背景色,透明度也不要设置为0

    渲染耗时比较长多线程

  • 6.使用局部更新

    若是只是更新某组的话,使用reloadSection进行局部更

  • 7.加载网络数据,下载图片,使用异步加载,并缓存

  • 8.少使用addView 给cell动态添加view

  • 9.按需加载cell,cell滚动很快时,只加载范围内的cell

  • 10.不要实现无用的代理方法,tableView只遵照两个协议

  • 11.缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这二者同时存在才会出现“窜动”的bug。因此个人建议是:只要是固定行高就写预估行高来减小行高调用次数提高性能。若是是动态行高就不要写预估方法了,用一个行高的缓存字典来减小代码的调用次数便可

  • 12.不要作多余的绘制工做。在实现drawRect:的时候,它的rect参数就是须要绘制的区域,这个区域以外的不须要进行绘制。例如上例中,就能够用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否须要绘制image和text,而后再调用绘制方法。

  • 13.预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,而后再绘制到屏幕;

  • 14.使用正确的数据结构来存储数据。

2.如何提高 tableview 的流畅度?

  • 本质上是下降 CPU、GPU 的工做,从这两个大的方面去提高性能。

    CPU:对象的建立和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制

    GPU:纹理的渲染

  • 卡顿优化在 CPU 层面

    尽可能用轻量级的对象,好比用不到事件处理的地方,能够考虑使用 CALayer 取代 UIView

    不要频繁地调用 UIView 的相关属性,好比 frame、bounds、transform 等属性,尽可能减小没必要要的修改

    尽可能提早计算好布局,在有须要时一次性调整对应的属性,不要屡次修改属性

    Autolayout 会比直接设置 frame 消耗更多的 CPU 资源

    图片的 size 最好恰好跟 UIImageView 的 size 保持一致

    控制一下线程的最大并发数量

    尽可能把耗时的操做放到子线程

    文本处理(尺寸计算、绘制)

    图片处理(解码、绘制)

  • 卡顿优化在 GPU层面

    尽可能避免短期内大量图片的显示,尽量将多张图片合成一张进行显示

    GPU能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用 CPU 资源进行处理,因此纹理尽可能不要超过这个尺寸

    尽可能减小视图数量和层次

    减小透明的视图(alpha<1),不透明的就设置 opaque 为 YES

    尽可能避免出现离屏渲染

  • iOS 保持界面流畅的技巧

    1.预排版,提早计算

    在接收到服务端返回的数据后,尽可能将 CoreText 排版的结果、单个控件的高度、cell 总体的高度提早计算好,将其存储在模型的属性中。须要使用时,直接从模型中往外取,避免了计算的过程。

    尽可能少用 UILabel,可使用 CALayer 。避免使用 AutoLayout 的自动布局技术,采起纯代码的方式

    2.预渲染,提早绘制

    例如圆形的图标能够提早在,在接收到网络返回数据时,在后台线程进行处理,直接存储在模型数据里,回到主线程后直接调用就能够了

    避免使用 CALayer 的 Border、corner、shadow、mask 等技术,这些都会触发离屏渲染。

    3.异步绘制

    4.全局并发线程

    5.高效的图片异步加载

3.APP启动时间应从哪些方面优化?

App启动时间能够经过xcode提供的工具来度量,在Xcode的Product->Scheme-->Edit Scheme->Run->Auguments中,将环境变量DYLD_PRINT_STATISTICS设为YES,优化需如下方面入手

  • dylib loading time

    核心思想是减小dylibs的引用

    合并现有的dylibs(最好是6个之内)

    使用静态库

  • rebase/binding time

    核心思想是减小DATA块内的指针

    减小Object C元数据量,减小Objc类数量,减小实例变量和函数(与面向对象设计思想冲突)

    减小c++虚函数

    多使用Swift结构体(推荐使用swift)

  • ObjC setup time

    核心思想同上,这部份内容基本上在上一阶段优化事后就不会太过耗时

    initializer time

  • 使用initialize替代load方法

    减小使用c/c++的attribute((constructor));推荐使用dispatch_once() pthread_once() std:once()等方法

    推荐使用swift

    不要在初始化中调用dlopen()方法,由于加载过程是单线程,无锁,若是调用dlopen则会变成多线程,会开启锁的消耗,同时有可能死锁

    不要在初始化中建立线程

4.如何下降APP包的大小

下降包大小须要从两方面着手

  • 可执行文件

    编译器优化:Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES,去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO, Other C Flags 添加 -fno-exceptions 利用 AppCode 检测未使用的代码:菜单栏 -> Code -> Inspect Code

    编写LLVM插件检测出重复代码、未被调用的代码

  • 资源(图片、音频、视频 等)

    优化的方式能够对资源进行无损的压缩

    去除没有用到的资源

5.如何检测离屏渲染与优化

  • 检测,经过勾选Xcode的Debug->View Debugging-->Rendering->Run->Color Offscreen-Rendered Yellow项。
  • 优化,如阴影,在绘制时添加阴影的路径

6.怎么检测图层混合

一、模拟器debug中color blended layers红色区域表示图层发生了混合

二、Instrument-选中Core Animation-勾选Color Blended Layers

避免图层混合:

  • 确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明
  • 如无特殊须要,不要设置低于1的alpha值
  • 确保UIImage没有alpha通道

UILabel图层混合解决方法:

iOS8之后设置背景色为非透明色而且设置label.layer.masksToBounds=YES让label只会渲染她的实际size区域,就能解决UILabel的图层混合问题

iOS8 以前只要设置背景色为非透明的就行

为何设置了背景色可是在iOS8上仍然出现了图层混合呢?

UILabel在iOS8先后的变化,在iOS8之前,UILabel使用的是CALayer做为底图层,而在iOS8开始,UILabel的底图层变成了_UILabelLayer,绘制文本也有所改变。在背景色的四周多了一圈透明的边,而这一圈透明的边明显超出了图层的矩形区域,设置图层的masksToBounds为YES时,图层将会沿着Bounds进行裁剪 图层混合问题解决了

7.平常如何检查内存泄露?

  • 目前我知道的方式有如下几种

    Memory Leaks

    Alloctions

    Analyse

    Debug Memory Graph

    MLeaksFinder

  • 泄露的内存主要有如下两种:

    Laek Memory 这种是忘记 Release 操做所泄露的内存。

    Abandon Memory 这种是循环引用,没法释放掉的内存。



作者:iOS鑫
链接:https://www.jianshu.com/p/f9da4407c04b

收起阅读 »

UIScrollView属性及其代理方法

一、UIScrollView是什么?1、UIScrollView是滚动的view,UIView本身不能滚动,子类UIScrollview拓展了滚动方面的功能。2、UIScrollView是所有滚动视图的基类。以后的UITableView,UITextView等...
继续阅读 »

一、UIScrollView是什么?

1、UIScrollView是滚动的view,UIView本身不能滚动,子类UIScrollview拓展了滚动方面的功能。
2、UIScrollView是所有滚动视图的基类。以后的UITableView,UITextView等视图都是继承于该类。
使用场景:显示不下(单张大图);内容太多(图文混排);滚动头条(图片);相册等

二、UIScrollView使用

1、UIScrollview主要专长于两个方面:

      a、滚动:contentSize大于frame.size的时候,能够滚动。
b、 缩放:自带缩放,可以指定缩放倍数。
2、UIScrollView滚动相关属性contentSize

 //定义内容区域大小,决定是否能够滑动
contentOffset //视图左上角距离坐标原点的偏移量
scrollsToTop //滑动到顶部(点状态条的时候)
pagingEnabled //是否整屏翻动
bounces //边界是否回弹
scrollEnabled //是否能够滚动
showsHorizontalScrollIndicator //控制是否显示水平方向的滚动条
showVerticalScrollIndicator //控制是否显示垂直方向的滚动条
alwaysBounceVertical //控制垂直方向遇到边框是否反弹
alwaysBounceHorizontal //控制水平方向遇到边框是否反弹

3、UIScrollView缩放相关属性

minimumZoomScale  //  缩小的最小比例
maximumZoomScale //放大的最大比例
zoomScale //设置变化比例
zooming //判断是否正在进行缩放反弹
bouncesZoom //控制缩放的时候是否会反弹
要实现缩放,还需要实现delegate,指定缩放的视图是谁。

4.UIScrollView滚动实例应用
- (void)scrollView{
// 创建滚动视图,但我们现实的屏幕超过一屏时,就需要滚动视图
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
scrollView.backgroundColor = [UIColor yellowColor];
scrollView.tag = 1000;
// 设置滚动区域
scrollView.contentSize = CGSizeMake(4 * CGRectGetWidth(self.view.frame), self.view.frame.size.height);
[self.view addSubview:scrollView];
// 添加子视图
for (int i = 0; i < 4; i ++) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(CGRectGetWidth(self.view.frame) * i, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame))];
label.text = [NSString stringWithFormat:@"这是%d个视图",i];
label.font = [UIFont systemFontOfSize:30];
[scrollView addSubview:label];
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg",i]]]; // (有四张片分别取名0.jpg,1.jpg,2.jpg.3.jpg)
[imageView setFrame:self.view.frame];
[label addSubview:imageView];

// label.backgroundColor = [UIColor colorWithRed:arc4random()%256/255.0 green:
// arc4random()%256/255.0 blue:arc4random()%256/255.0 alpha:1];

}
// 设置分页效果 (默认值为NO)
scrollView.pagingEnabled = YES;
// 设置滚动条是否显示(默认值是YES)
scrollView.showsHorizontalScrollIndicator = YES;
// 设置边界是否有反弹效果(默认值是YES)
scrollView.bounces = YES;
// 设置滚动条的样式
scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
/*
indicatorStyle(枚举值)
UIScrollViewIndicatorStyleDefault, //白色
UIScrollViewIndicatorStyleBlack, // 黑色
*/


// 设置scrollView的代理
scrollView.delegate = self; // (记得导入协议代理 <UIScrollViewAccessibilityDelegate>)
}

5、UIScrollView滚动代理方法
// 滚动就会触发
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{ NSLog(@"只有scrollview是跟滚动状态就会调用此方法");
}
//开始拖拽时触发
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
NSLog(@"开始拖拽");

}
// 结束拖拽时触发
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
NSLog(@"结束拖拽");
}
// 开始减速时触发
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
NSLog(@"开始减速");

}
// 结束减速时触发(停止)
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
NSLog(@"结束减速(停止)");
}

6、UIScrollView缩放实例应用
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
// 初始化一个scrollView
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
scrollView.backgroundColor = [UIColor greenColor];
scrollView.delegate = self;


// 设置缩放比率
// 设置可缩小道德最小比例
scrollView.minimumZoomScale = 0.5;
// 设置可放大的最大比例
scrollView.maximumZoomScale = 2.0;
[self.view addSubview:scrollView];

// 使得要添加的图片宽高成比例
UIImage *myImage = [UIImage imageNamed:@"7.jpg"];
// 得到原始宽高
float imageWidth = myImage.size.width;
float imageHeight = myImage.size.height;
// 这里我们规定imageView的宽为200,根据此宽度得到等比例的高度
float imageViewWidth = 200;
float imageViewHeight = 200 *imageHeight/imageWidth;
// 初始化一个UIimageview
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, imageViewWidth, imageViewHeight)];
// 为imageView设置图片
imageView.image = myImage;
// 让imageView居中
imageView.center = self.view.center;
imageView.tag = 1000;
[scrollView addSubview:imageView];

}

7、UIScrollView缩放有关的代理

#pragma mark -- 滚动视图与缩放有关的代理方法
//指定scrollview的某一个子视图为可缩放视图,前提条件是次视图已经添加到scrollview上面
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView{
UIView *imageView = (UIView*)[scrollView viewWithTag:1000];
return imageView;
}

// 开始缩放的代理方法 第二个参数view:这个参数使我们将要缩放的视图(这里就是imageView)
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view{
NSLog(@"%@",view);

}

// 正在缩放的代理方法 只要在缩放就执行该方法,所以此方法会在缩放过程中多次调用
- (void)scrollViewDidZoom:(UIScrollView *)scrollView{
// 在缩放过程中为了使得该视图一直在屏幕中间,所以我们需要在他缩放的过程中一直调整他的center
// 得到scrollview的子视图
UIImageView *imageView = (UIImageView *)[scrollView viewWithTag:1000];
// 打印imageView的frame,分析为什么他的位置会改变
// NSLog(@"frame -- %@",NSStringFromCGRect(imageView.frame));

// 设置imageview的center,是他的位置一直在屏幕中央
imageView.center = scrollView.center;
// 打印contentSize 分析为什么缩放之后会滑动
NSLog(@"contentSize %@",NSStringFromCGSize(scrollView.contentSize));
}


// 缩放结束所执行的代理方法
/**
* @ view 当前正在缩放的视图
* @ scale 当前正在缩放视图的缩放比例
*/

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale{
// 缩放完成之后恢复原大小,这里运用到2D仿射变换函数中与捏合有关的函数
view.transform =CGAffineTransformMakeScale(1, 1);


}



作者:小猪也浪漫
链接:https://www.jianshu.com/p/62918c39b95e

收起阅读 »

JAVA中线程间通信的小故事

从掘金的大佬中偷学到一个技能,为了提升知识提炼与字面表达能力,斟酌贴代码的篇幅,尽量用文字表达清楚技术知识的本质。(简单点就是“多说人话”) 正文开始! 前情提要 关于“线程间通信”的这个叫法,没查到比较官方的定义,也许它是一个通俗词吧。下面是基于笔...
继续阅读 »

从掘金的大佬中偷学到一个技能,为了提升知识提炼与字面表达能力,斟酌贴代码的篇幅,尽量用文字表达清楚技术知识的本质。(简单点就是“多说人话”)



正文开始!


前情提要


关于“线程间通信”的这个叫法,没查到比较官方的定义,也许它是一个通俗词吧。下面是基于笔者个人理解所总结出的定义,重在严谨。


不同线程之间通过资源状态同步相互影响彼此的执行逻辑。


线程间的基本通信可以划分为启动与结束,线程等待与唤醒线程。在JAVA中他们都对应了固定的API与固定用法,是还存在其他的通信方式,但本文不做展开。


一、线程停止的性格差异


张三与小明的故事


1. thread.stop() 愚蠢且粗鲁的张三



立即强制停止某个线程的执行,中断代码执行指令,并退出线程。被停止的线程无法安全的进行善后处理。



代入角色举个栗子,愚蠢的张三安排他儿子小明烧开水。小明很聪明,已经牢记了烧水的步骤,拿锅接水,开火,水沸关火。


小明很听话,便进入厨房开始了忙碌。


半分钟后愚蠢张三的电话突然响了 ,有关部门通知马上会停止燃气供应,张三意识到小明不能再烧水了,决定停止小明的工作。


此时小明还在接水,但愚蠢的张三假装没看见,他一把将小明拉出了厨房。小明内心非常懵逼,但是他有苦说不出。


他们离开后,厨房的水还在哗哗的流,最后淹了厨房...


总结:完全不需要考虑善后的线程才能用stop()


2. thread.interrupt() 温柔的张三



通知某个线程中断当前在执行的任务,被中断的线程可以先进内部善后处理,再退出线程,或者不退出。



代入角色,还是上面的栗子。这不过这次张三并没后直接抱走小明,而是大声告诉小明,该离开厨房了。


小明此时有两种选择,第一中是丢下手上的事情,马上走出厨房,让水继续哗哗的流。第二种是关闭水龙头再走出厨房。


如果你是小明,你准备怎么做?


总结:中断线程用thread.interrupt()就对了,最起码温柔


3.Thread.interrupted() 可怜的小明,正确答案只能获取一次



每次在被中断后的第一次调用时返回true,之后在没有被在此中断前都一直返回false



代入角色,还是上面的栗子。小明知道张三有可能会通知他出现了例外情况,所以小明在每一个关键步骤前检查是否需要停止,如果发现被叫停就马上进行善后工作,离开厨房。因为他知道他基本只有一次机会。


总结:用于简单任务的中断判断,如果无法衡量是否简单,那就没必要用,除非你对中断次数是非常敏感的。


4.isInterrupted() 快乐的小明,获取正确答案不限次数



只要被中断过一次,之后获取到的状态都是true



小明的快乐你懂了吗


总结:小明的快乐你懂了吗


5.Thread.sleep(x)小明在厨房睡着了



当前线程进入挂起状态,挂起的过程中可能会被中断,被中断时则会被catch (InterruptedException e)捕获,可以进行善后处理,选择是否退出。



代入角色,没错小明真睡着了!如果温柔的张三大声告诉小明离开厨房,小明被惊醒后要是不犯迷糊就会有序的停止当且阶段的工作,比如关闭水龙头,然后离开厨房。


要是小明犯迷糊呢?小明一般不会犯迷糊


因为他知道


犯迷糊的小明会被张三暴揍!


总结:Thread.sleep(x)后需要捕获的异常catch (InterruptedException e),理解为例外更好些,因为它并不代表程序错误


二、等待的细节与唤醒的差别


小明与小芳的故事


1.wait() 小明的素质



当需要访问的资源不满足条件时,选择进入等待区。直到被唤醒后重新竞争锁,获取锁后接着之前的逻辑继续执行



小明和小芳一起看电视,小明先抢到了遥控器,他想看足球比赛,切到了足球频道,球员A准备射门,但是小明点的啤酒还没到,小明看比赛必须得有啤酒。


如果小明没礼貌,那么他就暂停电视,把遥控器坐在屁股下面,一直盯着电视,直到啤酒来了,小明恢复电视,继续看。


如果小明有礼貌,那么他就先让出了遥控器,小芳拿到遥控器开心的放起了甄嬛传。 小明呢则开始发呆(细节1),直到(细节2)有人告诉他啤酒来了,他便重新(细节3)去抢遥控器,抢到后遥控器后起到足球频道,电视机画面直接从球员A准备射门处(细节3)开始播放。


如果小明发呆的时候出现了意外怎么办呢?不用担心这会立即叫醒小明,他可以自主选择下一步怎么办。


这就是为什么wait()时也需要catch (InterruptedException e)


总结:用wait()让出锁和资源,减少兄弟线程的等待时间


2.notify() 幸运女神



由当前作为锁的对象随机从与当前锁相关且进入wait()的线程中唤醒一个,被唤醒的线程重新进行锁的竞争



从上帝视角看,当资源只能满足一个线程使用时,使用notify(),能节约不必要的额外开销。


而被选中的那个线程就是唯一的幸运儿~


3.notifyAll() 阳光普照



由当前作为锁的对象唤醒所有与当前锁相关且进入wait()的线程,被唤醒的线程重新进行锁的竞争



如果没有特殊考虑,为了世界和平,通常你应当唤醒所有进入等待的线程。


三、join 快来绑一绑timing



将多个并行线程任务,连成一个串行的线程任务,带头线程不管成功还是失败,跟随线程都会立即执行



再举个栗子吧,张三安排小芳做饭,并让小明负责打酱油。


接下来的情况就会变得非常有趣。


小芳炒完菜要出锅的时候需要酱油,但是此时小明还没有买回酱油。小芳便使用join大法将自己绑定到了小明买回酱油这件任务的结束timing上。


结果呢?如果小明顺利买回了酱油,小芳使用酱油提鲜后装盘出锅。


如果小明路上摔跤了,导致提前退出了任务。小芳则使用空酱油后装盘出锅


这不怪小芳,她哪知道小明没有带回酱油呢。


总结: join()之后应该在此判断条件是否满足,避免拿到NPE


四、yield



稍微让出一点时间片给同级别线程,又立即恢复自己的执行。



像是快速wait()(不用别人叫的那种),再快速自动恢复


缺少科学分析验证,不敢多说~    




END

收起阅读 »

一文带你实现遍历android内存模块

1.Android内存模块遍历原理 在android系统上,要遍历app进程中的内存模块数据,必须用到proc文件系统。 proc它是由linux内核挂着到内存中,它提供内核配置、进程状态输出等功能。 用adb命令方式可以进行查看app进程中所有加载的模块...
继续阅读 »

1.Android内存模块遍历原理


在android系统上,要遍历app进程中的内存模块数据,必须用到proc文件系统。
proc它是由linux内核挂着到内存中,它提供内核配置、进程状态输出等功能。


用adb命令方式可以进行查看app进程中所有加载的模块信息。
cat /proc/%d/maps : cat是查看的意思, %d表示要查看的APP的进程pid


maps文件中显示出来的各个列信息解释:


第1列:模块内容在内存中的地址范围,以16进制显示。


第2列:模块内容在内存中的读取权限,r代表可读,w代表可写,x代表可执行,p代表私有,s代码共享。


第3列:模块内容对应模块文件中的偏移。


第4列:模块文件在文件系统中的主次设备号。


第5列:模块文件在文件系统中的节点号。


第6列:模块文件在文件系统中的路径。 image.png


2.android内存模块遍历实现



//存储模块信息的结构体
struct ProcMap {
void *startAddr;
void *endAddr;
size_t length;
std::string perms;
long offset;
std::string dev;
int inode;
std::string pathname;

bool isValid() { return (startAddr != NULL && endAddr != NULL && !pathname.empty()); }
};

//获取模块信息函数
bool getAPPMod(int pid)
{

ProcMap retMap;
char line[512] = {0};
char mapPath[128] = {0};

sprintf(mapPath, "/proc/%d/maps", pid);

FILE *fp = fopen(mapPath, "r");
if (fp != NULL)
{

while (fgets(line, sizeof(line), fp)) {

char tmpPerms[5] = {}, tmpDev[12] = {}, tmpPathname[455] = {};

sscanf(line, "%llx-%llx %s %ld %s %d %s",
(long long unsigned *) &retMap.startAddr,
(long long unsigned *) &retMap.endAddr,
tmpPerms, &retMap.offset, tmpDev, &retMap.inode, tmpPathname);

}
}

return true;

}

收起阅读 »

官方推荐 Flow 取代 LiveData,有必要吗?

前言打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如Room,DataStore, Paging3,DataBinding 等都支持了FlowGoogle开发者账号最近也发布了几篇使用Flow的文章,比如:从...
继续阅读 »

前言

打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如RoomDataStorePaging3,DataBinding 等都支持了Flow
Google开发者账号最近也发布了几篇使用Flow的文章,比如:从 LiveData 迁移到 Kotlin 数据流
看起来官方在大力推荐使用Flow取代LiveData,那么问题来了,有必要吗?
LiveData用得好好的,有必要再学Flow吗?本文主要回答这个问题,具体包括以下内容
1.LiveData有什么不足?
2.Flow介绍以及为什么会有Flow
3.SharedFlowStateFlow的介绍与它们之间的区别

本文具体目录如下所示:

1. LiveData有什么不足?

1.1 为什么引入LiveData?

要了解LiveData的不足,我们先了解下LiveData为什么被引入

LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了

可以看出,LiveData就是一个简单易用的,具备感知生命周期能力的观察者模式
它使用起来非常简单,这是它的优点,也是它的不足,因为它面对比较复杂的交互数据流场景时,处理起来比较麻烦

1.2 LiveData的不足

我们上文说过LiveData结构简单,但是不够强大,它有以下不足
1.LiveData只能在主线程更新数据
2.LiveData的操作符不够强大,在处理复杂数据流时有些捉襟见肘

关于LiveData只能在主线程更新数据,有的同学可能要问,不是有postValue吗?其实postValue也是需要切换到到主线程的,如下图所示:

这意味着当我们想要更新LiveData对象时,我们会经常更改线程(工作线程→主线程),如果在修改LiveData后又要切换回到工作线程那就更麻烦了,同时postValue可能会有丢数据的问题。

2. Flow介绍

Flow 就是 Kotlin 协程与响应式编程模型结合的产物,你会发现它与 RxJava 非常像,二者之间也有相互转换的 API,使用起来非常方便。

2.1 为什么引入Flow

为什么引入Flow,我们可以从Flow解决了什么问题的角度切入

  1. LiveData不支持线程切换,所有数据转换都将在主线程上完成,有时需要频繁更改线程,面对复杂数据流时处理起来比较麻烦
  2. RxJava又有些过于麻烦了,有许多让人傻傻分不清的操作符,入门门槛较高,同时需要自己处理生命周期,在生命周期结束时取消订阅

可以看出,Flow是介于LiveDataRxJava之间的一个解决方案,它有以下特点

  • Flow 支持线程切换、背压
  • Flow 入门的门槛很低,没有那么多傻傻分不清楚的操作符
  • 简单的数据转换与操作符,如 map 等等
  • 冷数据流,不消费则不生产数据,这一点与LiveData不同:LiveData的发送端并不依赖于接收端。
  • 属于kotlin协程的一部分,可以很好的与协程基础设施结合

关于Flow的使用,比较简单,有兴趣的同学可参阅文档:Flow文档

3. SharedFlow介绍

我们上面介绍过,Flow 是冷流,什么是冷流?

  • 冷流 :只有订阅者订阅时,才开始执行发射数据流的代码。并且冷流订阅者只能是一对一的关系,当有多个不同的订阅者时,消息是重新完整发送的。也就是说对冷流而言,有多个订阅者的时候,他们各自的事件是独立的。
  • 热流:无论有没有订阅者订阅,事件始终都会发生。当 热流有多个订阅者时,热流订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。

3.1 为什么引入SharedFlow

上面其实已经说得很清楚了,冷流订阅者只能是一对一的关系,当我们要实现一个流,多个订阅者的需求时(这在开发中是很常见的),就需要热流
从命名上也很容易理解,SharedFlow即共享的Flow,可以实现一对多关系,SharedFlow是一种热流

3.2 SharedFlow的使用

我们来看看SharedFlow的构造函数

public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)
: MutableSharedFlow<T>

其主要有3个参数
1.replay表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据
2.extraBufferCapacity表示减去replayMutableSharedFlow还缓存多少数据,默认为0
3.onBufferOverflow表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起

简单使用如下:

//ViewModel
val sharedFlow=MutableSharedFlow<String>()

viewModelScope.launch{
sharedFlow.emit("Hello")
sharedFlow.emit("SharedFlow")
}

//Activity
lifecycleScope.launch{
viewMode.sharedFlow.collect {
print(it)
}
}

3.3 将冷流转化为SharedFlow

普通flow可使用shareIn扩展方法,转化成SharedFlow

    val sharedFlow by lazy {
flow<Int> {
//...
}.shareIn(viewModelScope, WhileSubscribed(500), 0)
}

shareIn主要也有三个参数:

@param scope 共享开始时所在的协程作用域范围
@param started 控制共享的开始和结束的策略
@param replay 状态流的重播个数

started 接受以下的三个值:
1.Lazily: 当首个订阅者出现时开始,在scope指定的作用域被结束时终止。
2.Eagerly: 立即开始,而在scope指定的作用域被结束时终止。
3.WhileSubscribed: 这种情况有些复杂,后面会详细讲解

对于那些只执行一次的操作,您可以使用Lazily或者Eagerly。然而,如果您需要观察其他的流,就应该使用WhileSubscribed来实现细微但又重要的优化工作

3.4 Whilesubscribed策略

WhileSubscribed策略会在没有收集器的情况下取消上游数据流,通过shareIn运算符创建的SharedFlow会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。
让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程。

public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)

如上所示,它支持两个参数:

  • 1.stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止).这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。
  • 2.replayExpirationMillis表示数据重播的过时时间,如果用户离开应用太久,此时您不想让用户看到陈旧的数据,你可以用到这个参数

4. StateFlow介绍

4.1 为什么引入StateFlow

我们前面刚刚看了SharedFlow,为什么又冒出个StateFlow?
StateFlow 是 SharedFlow 的一个比较特殊的变种,StateFlow 与 LiveData 是最接近的,因为:

  • 1.它始终是有值的。
  • 2.它的值是唯一的。
  • 3.它允许被多个观察者共用 (因此是共享的数据流)。
  • 4.它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。

可以看出,StateFlowLiveData是比较接近的,可以获取当前的值,可以想像之所以引入StateFlow就是为了替换LiveData
总结如下:
1.StateFlow继承于SharedFlow,是SharedFlow的一个特殊变种
2.StateFlowLiveData比较相近,相信之所以推出就是为了替换LiveData

4.2 StateFlow的简单使用

我们先来看看构造函数:

public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

1.StateFlow构造函数较为简单,只需要传入一个默认值
2.StateFlow本质上是一个replay为1,并且没有缓冲区的SharedFlow,因此第一次订阅时会先获得默认值
3.StateFlow仅在值已更新,并且值发生了变化时才会返回,即如果更新后的值没有变化,也没会回调Collect方法,这点与LiveData不同

StateFlow类似,我们也可以用stateIn将普通流转化成SharedFlow

val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)

shareIn类似,唯一不同的时需要传入一个默认值
同时之所以WhileSubscribed中传入了5000,是为了实现等待5秒后仍然没有订阅者存在就终止协程的功能,这个方法有以下功能

  • 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。
  • 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。
  • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。
  • 在屏幕旋转时,因为重新订阅的时间在5s内,因此上游流不会中止

4.3 在页面中观察StateFlow

LiveData类似,我们也需要经常在页面中观察StateFlow
观察StateFlow需要在协程中,因此我们需要协程构建器,一般我们会使用下面几种

  1. lifecycleScope.launch : 立即启动协程,并且在本 ActivityFragment 销毁时结束协程。
  2. LaunchWhenStarted 和 LaunchWhenResumed,它会在lifecycleOwner进入X状态之前一直等待,又在离开X状态时挂起协程


如上图所示:
1.使用launch是不安全的,在应用在后台时也会接收数据更新,可能会导致应用崩溃
2.使用launchWhenStartedlaunchWhenResumed会好一些,在后台时不会接收数据更新,但是,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源

这么说来,我们使用WhileSubscribed进行的配置岂不是无效了吗?订阅者一直存在,只有页面关闭时才会取消订阅
官方推荐repeatOnLifecycle来构建协程
在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程,如下图所示。

比如在某个Fragment的代码中:

onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}

当这个Fragment处于STARTED状态时会开始收集流,并且在RESUMED状态时保持收集,最终在Fragment进入STOPPED状态时结束收集过程。
结合使用repeatOnLifecycle APIWhileSubscribed,可以帮助您的应用妥善利用设备资源的同时,发挥最佳性能

4.4 页面中观察Flow的最佳方式

通过ViewModel暴露数据,并在页面中获取的最佳方式是:

  • ?? 使用带超时参数的 WhileSubscribed 策略暴露 Flow示例 1
  • ?? 使用 repeatOnLifecycle 来收集数据更新。示例 2


最佳实践如上图所示,如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费
当然,如果您并不需要使用到Kotlin Flow的强大功能,就用LiveData好了 :)

StateFlowSharedFlow有什么区别?

从上文其实可以看出,StateFlowSharedFlow其实是挺像的,让人有些傻傻分不清,有时候也挺难选择该用哪个的

我们总结一下,它们的区别如下:

  1. SharedFlow配置更为灵活,支持配置replay,缓冲区大小等,StateFlowSharedFlow的特化版本,replay固定为1,缓冲区大小默认为0
  2. StateFlowLiveData类似,支持通过myFlow.value获取当前状态,如果有这个需求,必须使用StateFlow
  3. SharedFlow支持发出和收集重复值,而StateFlowvalue重复时,不会回调collect
  4. 对于新的订阅者,StateFlow只会重播当前最新值,SharedFlow可配置重播元素个数(默认为0,即不重播)

可以看出,StateFlow为我们做了一些默认的配置,在SharedFlow上添加了一些默认约束,这些配置可能并不符合我们的要求

  1. 它忽略重复的值,并且是不可配置的。这会带来一些问题,比如当往List中添加元素并更新时,StateFlow会认为是重复的值并忽略
  2. 它需要一个初始值,并且在开始订阅时会回调初始值,这有可能不是我们想要的
  3. 它默认是粘性的,新用户订阅会获得当前的最新值,而且是不可配置的,而SharedFlow可以修改replay

StateFlow施加在SharedFlow上的约束可能不是最适合您,如果不需要访问myFlow.value,并且享受SharedFlow的灵活性,可以选择考虑使用SharedFlow

总结

简单往往意味着不够强大,而强大又常常意味着复杂,两者往往不能兼得,软件开发过程中常常面临这种取舍。
LiveData的简单并不是它的缺点,而是它的特点。StateFlowSharedFlow更加强大,但是学习成本也显著的更高.
我们应该根据自己的需求合理选择组件的使用

  1. 如果你的数据流比较简单,不需要进行线程切换与复杂的数据变换,LiveData对你来说相信已经足够了
  2. 如果你的数据流比较复杂,需要切换线程等操作,不需要发送重复值,需要获取myFlow.valueStateFlow对你来说是个好的选择
  3. 如果你的数据流比较复杂,同时不需要获取myFlow.value,需要配置新用户订阅重播无素的个数,或者需要发送重复的值,可以考虑使用SharedFlow


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

收起阅读 »

实战:5分钟搞懂OkHttp断点上传

1、前言 经常会有同学问:文件的断点上传如何实现? 断点上传/下载,这是在客户端经常遇到的场景,当我们需要上传或下载一个大文件时,都会考虑使用断点续传的方式。 断点上传相较于断点下载来说,最大的区别就在于断点位置的记录,上传记录在服务端,下载记录在客户端...
继续阅读 »

1、前言


经常会有同学问:文件的断点上传如何实现?


断点上传/下载,这是在客户端经常遇到的场景,当我们需要上传或下载一个大文件时,都会考虑使用断点续传的方式。


断点上传相较于断点下载来说,最大的区别就在于断点位置的记录,上传记录在服务端,下载记录在客户端,因此,客户端需要在上传前,通过接口去拿到文件的断点位置,然后在上传时,将文件输入流跳转到断点位置


2、准备工作


对于文件上传,其实就是打开文件的输入流,不停的读取数据到byte数组中,随后写出到服务端;那客户端要做的就是跳过已经上传的部分,也就是直接跳到断点位置,这样就可以从断点位置去读取数据,也就达到了断点上传的目的。


伪代码如下:


String filePath = "...";
long skipSize = 100; //假设断点位置是 100 byte
InputStream input = input = new FileInputStream(filePath);
input.skip(skipSize) //跳转到断点位置

然而,OkHttp并没有直接提供设置断点的方法,所以需要客户端自定义RequestBody,取名为FileRequestBody,如下:


//为简化阅读,已省略部分代码
public class FileRequestBody extends RequestBody {

private final File file;
private final long skipSize; //断点位置
private final MediaType mediaType;

public FileRequestBody(File file, long skipSize, @Nullable MediaType mediaType) {
this.file = file;
this.skipSize = skipSize;
this.mediaType = mediaType;
}

@Override
public long contentLength() throws IOException {
return file.length() - skipSize;
}

@Override
public void writeTo(@NotNull BufferedSink sink) throws IOException {
InputStream input = null;
Source source = null;
try {
input = new FileInputStream(file);
if (skipSize > 0) {
input.skip(skipSize); //跳到断点位置
}
source = Okio.source(input);
sink.writeAll(source);
} finally {
OkHttpCompat.closeQuietly(source, input);
}
}
}


为方便阅读,以上省略部分源码,FileRequestBody类完整源码



有了FileRequestBody类,我们只需要传入一个断点位置,剩下的工作就跟普通的文件上传一样。 接下来,直接进入代码实现。


3、代码实现


3.1 获取断点位置


首先,需要服务端提供一个接口,通过userId去查找该用户未上传完成的任务列表,代码如下:


RxHttp.get("/.../getToUploadTask")
.add("userId", "88888888")
.asList<ToUploadTask>()
.subscribe({
//成功回调,这里通过 it 拿到 List<ToUploadTask>
}, {
//异常回调
});

其中ToUploadTask类如下:


//待上传任务
data class ToUploadTask(
val md5: String, //文件的md5,用于验证文件的唯一性
val filePath: String, //文件在客户端的绝对路径
val skipSize: Long = 0 //断点位置
)

注:md5、filePath 这两个参数需要客户端在文件上传时传递给服务端,用于对文件的校验,防止文件错乱


3.2 断点上传


有了待上传任务,客户端就可以执行断点上传操作,OkHttp代码如下:


fun uploadFile(uploadTask: ToUploadTask) {
//1.校验文件是否存在
val file = File(uploadTask.filePath)
if (!file.exists() && !file.isFile) return
//2.校验文件的 md5 值
val fileMd5 = FileUtils.getFileMD5ToString(file)
if (!fileMd5.equals(uploadTask.md5)) return
//3.构建请求体
val fileRequestBody = FileRequestBody(file, uploadTask.skipSize, BuildUtil.getMediaType(file.name))
val multipartBody = MultipartBody.Builder()
.addFormDataPart("userId", "88888888")
.addFormDataPart("md5", fileMd5)
.addFormDataPart("filePath", file.absolutePath)
.addFormDataPart("file", file.name, fileRequestBody) //添加文件body
.build()
//4.构建请求
val request = Request.Builder()
.url("/.../uploadFile")
.post(multipartBody)
.build()
//5.执行请求
val okClient = OkHttpClient.Builder().build()
okClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
//异常回调
}
override fun onResponse(call: Call, response: Response) {
//成功回调
}
})
}


FIleUtils源码BuildUtil源码



当然,考虑到很少人会直接使用OkHttp,所以这里也贴出RxHttp的实现代码,很简单,仅需构建一个UpFile对象即可,就可很方便的监听上传进度,代码如下:


fun uploadFile(uploadTask: ToUploadTask) {                            
//1.校验文件是否存在
val file = File(uploadTask.filePath)
if (!file.exists() && !file.isFile) return
//2.校验文件的 md5 值
val fileMd5 = FileUtils.getFileMD5ToString(file)
if (!fileMd5.equals(uploadTask.md5)) return
val upFile = UpFile("file", file, file.name, uploadTask.skipSize)
//3.直接上传
RxHttp.postForm("/.../uploadFile")
.add("userId", "88888888")
.add("md5", fileMd5)
.add("filePath", file.absolutePath)
.addFile(upFile)
.upload(AndroidSchedulers.mainThread()) {
//上传进度回调
}
.asString()
.subscribe({
//成功回调
}, {
//异常回调
})
}

4、小结


断点上传相较普通的文件上传,客户端多了一个断点的设置,大部分工作量在服务端,服务端不仅需要处理文件的拼接逻辑,还需记录未上传完成的任务,并通过接口暴露给客户端。



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

iOS底层探索开发 必不可少的 clang插件

Clang插件LLVM下载由于国内的网络限制,我们需要借助镜像下载LLVM的源码https://mirror.tuna.tsinghua.edu.cn/help/llvm/下载llvm项目git clone https://mirrors.tuna....
继续阅读 »

Clang插件

LLVM下载

由于国内的网络限制,我们需要借助镜像下载LLVM的源码

https://mirror.tuna.tsinghua.edu.cn/help/llvm/

  • 下载llvm项目

git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git

  • 在LLVM的tools目录下下载Clang

cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git

  • 在 LLVM 的 projects 目录下下载 compiler-rt, libcxx, libcxxabi

cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git

  • 在Clang的tools下安装extra工具

cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/LLvm/cLang-tooLs-e xtra.git

LLVM编译

由于最新的LLVM只支持c make来编译了,我们还需要安装c make。

安装cmake

  • 查看brew是否安装cmake如果有就跳过下面步骤

brew list

  • 通过brew安装cmake

brew install cmake

编译LLVM

通过xcode编译LLVM

  • cmake编译成Xcode项目

mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm

  • 使用Xcode编译Clang。
    • 选择自动创建Schemes




  • 在HKPlugin目录下新建一个名为HKPlugin.cpp的文件和CMakeLists.txt的文件。在CMakeLists.txt中写上

add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY
HKPlugin.cpp
)
接下来利用cmake重新生成一下Xcode项目,在build_xcode中cmake -g Xcode ../llvm

  • 最后可以在LLVM的Xcode项目中可以看到Loadable modules目录下有自己 的Plugin目录了。我们可以在里面编写插件代码。


添加下自己的插件,等下编译

#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

namespace HKPlugin {
class HKConsumer: public ASTConsumer {
public:
//解析一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}

//整个文件都解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout << "文件解析完毕" << endl;
}
};

//继承PluginASTAction 实现我们自定义的Action
class HKASTAction:public PluginASTAction{
public:
//重写
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg){
return true;
}

std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer>(new HKConsumer);
}
};
}
//注册插件

static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> HK("HKPlugin","this is HKPlugin");

先简单写些测试代码,然后编译生成dylib



int sum(int a);
int a;
int sum(int a){
int b = 10;
return 10 + b;
}
int sum2(int a,int b){
int c = 10;
return a + b + c;
}


写些测试代码

自己编译的 clang 文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk/ -Xclang -load -Xclang 自己编译的 clang 文件路径 -Xclang -add-plugin -Xclang 自己编译的 clang 文件路径 -c 自己编译的 clang 文件路径

例: /Users/xxx/Desktop/LLVMENV/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk/ -Xclang -load -Xclang /Users/xxx/Desktop/LLVMENV/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin -c ./hello.m
注:iPhoneSimulator13.5.sdk换成自己目录下的sdk版本


正在解析...
正在解析...
正在解析...
正在解析...
文件解析完毕

现在在viewController中声明属性

#import "ViewController.h"

@interface ViewController ()
@property(nonatomic, strong) NSDictionary* dict;
@property(nonatomic, strong) NSArray* arr;
@property(nonatomic, strong) NSString* name;
@end

@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
@end

然后通过语法分析,查看抽象语法树

clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m




TranslationUnitDecl 0x7f9e57000008 <<invalid sloc>> <invalid sloc> <undeserialized declarations>
|-TypedefDecl 0x7f9e570008a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7f9e570005a0 '__int128'
|-TypedefDecl 0x7f9e57000910 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7f9e570005c0 'unsigned __int128'
|-TypedefDecl 0x7f9e570009b0 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7f9e57000970 'SEL *' imported
| `-BuiltinType 0x7f9e57000800 'SEL'
|-TypedefDecl 0x7f9e57000a98 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7f9e57000a40 'id' imported
| `-ObjCObjectType 0x7f9e57000a10 'id' imported
|-TypedefDecl 0x7f9e57000b78 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7f9e57000b20 'Class' imported
| `-ObjCObjectType 0x7f9e57000af0 'Class' imported
|-ObjCInterfaceDecl 0x7f9e57000bd0 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7f9e57000f48 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7f9e57000d40 'struct __NSConstantString_tag'
| `-Record 0x7f9e57000ca0 '__NSConstantString_tag'
|-TypedefDecl 0x7f9e58008400 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7f9e57000fa0 'char *'
| `-BuiltinType 0x7f9e570000a0 'char'
|-TypedefDecl 0x7f9e580086e8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7f9e58008690 'struct __va_list_tag [1]' 1
| `-RecordType 0x7f9e580084f0 'struct __va_list_tag'
| `-Record 0x7f9e58008458 '__va_list_tag'
|-ImportDecl 0x7f9e5852bc18 <./ViewController.h:9:1> col:1 implicit UIKit
|-ObjCInterfaceDecl 0x7f9e58541e00 <line:11:1, line:14:2> line:11:12 ViewController
| |-super ObjCInterface 0x7f9e5852be78 'UIViewController'
| `-ObjCImplementation 0x7f9e5857f460 'ViewController'
|-ObjCCategoryDecl 0x7f9e58541f30 <ViewController.m:11:1, line:15:2> line:11:12
| |-ObjCInterface 0x7f9e58541e00 'ViewController'
| |-ObjCPropertyDecl 0x7f9e58548a00 <line:12:1, col:44> col:44 dict 'NSDictionary *' readwrite nonatomic strong
| |-ObjCMethodDecl 0x7f9e58548a80 <col:44> col:44 implicit - dict 'NSDictionary *'
| |-ObjCMethodDecl 0x7f9e58548c28 <col:44> col:44 implicit - setDict: 'void'
| | `-ParmVarDecl 0x7f9e58548cb0 <col:44> col:44 dict 'NSDictionary *'
| |-ObjCPropertyDecl 0x7f9e58551cd0 <line:13:1, col:39> col:39 arr 'NSArray *' readwrite nonatomic strong
| |-ObjCMethodDecl 0x7f9e58551d50 <col:39> col:39 implicit - arr 'NSArray *'
| |-ObjCMethodDecl 0x7f9e58551ea8 <col:39> col:39 implicit - setArr: 'void'
| | `-ParmVarDecl 0x7f9e58551f30 <col:39> col:39 arr 'NSArray *'
| |-ObjCPropertyDecl 0x7f9e585650d0 <line:14:1, col:40> col:40 name 'NSString *' readwrite nonatomic strong
| |-ObjCMethodDecl 0x7f9e58565150 <col:40> col:40 implicit - name 'NSString *'
| `-ObjCMethodDecl 0x7f9e585652a8 <col:40> col:40 implicit - setName: 'void'
| `-ParmVarDecl 0x7f9e58565330 <col:40> col:40 name 'NSString *'
`-ObjCImplementationDecl 0x7f9e5857f460 <line:17:1, line:25:1> line:17:17 ViewController
|-ObjCInterface 0x7f9e58541e00 'ViewController'
|-ObjCMethodDecl 0x7f9e5857f580 <line:19:1, line:22:1> line:19:1 - viewDidLoad 'void'
| |-ImplicitParamDecl 0x7f9e585c9c08 <<invalid sloc>> <invalid sloc> implicit self 'ViewController *'
| |-ImplicitParamDecl 0x7f9e585c9c70 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
| `-CompoundStmt 0x7f9e585cf2b8 <col:21, line:22:1>
| `-ObjCMessageExpr 0x7f9e585c9cd8 <line:20:5, col:23> 'void' selector=viewDidLoad super (instance)
|-ObjCIvarDecl 0x7f9e585c8168 <line:12:44> col:44 implicit _dict 'NSDictionary *' synthesize private
|-ObjCPropertyImplDecl 0x7f9e585c81c8 <<invalid sloc>, col:44> <invalid sloc> dict synthesize
| |-ObjCProperty 0x7f9e58548a00 'dict'
| `-ObjCIvar 0x7f9e585c8168 '_dict' 'NSDictionary *'
|-ObjCIvarDecl 0x7f9e585c84e0 <line:13:39> col:39 implicit _arr 'NSArray *' synthesize private
|-ObjCPropertyImplDecl 0x7f9e585c8540 <<invalid sloc>, col:39> <invalid sloc> arr synthesize
| |-ObjCProperty 0x7f9e58551cd0 'arr'
| `-ObjCIvar 0x7f9e585c84e0 '_arr' 'NSArray *'
|-ObjCIvarDecl 0x7f9e585c9890 <line:14:40> col:40 implicit _name 'NSString *' synthesize private
|-ObjCPropertyImplDecl 0x7f9e585c98f0 <<invalid sloc>, col:40> <invalid sloc> name synthesize
| |-ObjCProperty 0x7f9e585650d0 'name'
| `-ObjCIvar 0x7f9e585c9890 '_name' 'NSString *'
|-ObjCMethodDecl 0x7f9e585c82f8 <line:12:44> col:44 implicit - dict 'NSDictionary *'
|-ObjCMethodDecl 0x7f9e585c8450 <col:44> col:44 implicit - setDict: 'void'
| `-ParmVarDecl 0x7f9e58548cb0 <col:44> col:44 dict 'NSDictionary *'
|-ObjCMethodDecl 0x7f9e585c8670 <line:13:39> col:39 implicit - arr 'NSArray *'
|-ObjCMethodDecl 0x7f9e585c9800 <col:39> col:39 implicit - setArr: 'void'
| `-ParmVarDecl 0x7f9e58551f30 <col:39> col:39 arr 'NSArray *'
|-ObjCMethodDecl 0x7f9e585c9a20 <line:14:40> col:40 implicit - name 'NSString *'
`-ObjCMethodDecl 0x7f9e585c9b78 <col:40> col:40 implicit - setName: 'void'
`-ParmVarDecl 0x7f9e58565330 <col:40> col:40 name 'NSString *'

我们可以找到其中的属性节点和他的修饰符

| |-ObjCPropertyDecl 0x7f9e585650d0 <line:14:1, col:40> col:40 name 'NSString *' readwrite nonatomic strong
完整代码如下

#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

namespace HKPlugin {

class HKMatchCallback:public MatchFinder::MatchCallback{
private:
CompilerInstance &CI;

bool isUserSourceCode(const string fileName){
if (fileName.empty()) return false;
//非xcode中的源码都认为是用户的
if(fileName.find("/Applications/Xcode.app/") == 0)return false;
return true;
}

//判断是否应该用copy修饰
bool isShouldUseCopy(const string typeStr){
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos ) {
return true;
}
return false;
}
public:
HKMatchCallback(CompilerInstance &CI):CI(CI){}
void run(const MatchFinder::MatchResult &Result) {
//通过Result获得节点
//之前绑定的标识
const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");


//获取文件名称
string fileName = CI.getSourceManager().getFilename(propertyDecl-> getSourceRange().getBegin()).str();

//判断节点有值并且是用户文件
if (propertyDecl && isUserSourceCode(fileName)) {
//节点类型转为字符串
string typeStr = propertyDecl->getType().getAsString();
//拿到及诶单的描述信息
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
//判断应该使用copy但是没有使用copy
if (isShouldUseCopy(typeStr) && !(attrKind & clang::ObjCPropertyDecl::OBJC_PR_copy)) {
cout << typeStr << "应该用copy修饰!但你没有" << endl;
//诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
//Report 报告
diag.Report(propertyDecl->getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "--- %0 这个地方推荐使用copy"))<<typeStr
}
// cout<<"--拿到了:"<<typeStr<<"---属于文件:"<<fileName<<endl;

}
}
};

class HKConsumer: public ASTConsumer {
private:
//AST节点的查找过滤器
MatchFinder matcher;
HKMatchCallback callback;
public:
HKConsumer(CompilerInstance &CI):callback(CI){
//添加一个MatchFinder去匹配objcPropertyDecl节点
//回调在HKMatchCallback里面run方法

matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
//解析一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}

//整个文件都解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout << "文件解析完毕" << endl;
matcher.matchAST(Ctx);//将语法树交给过滤器
}
};

//继承PluginASTAction 实现我们自定义的Action
class HKASTAction:public PluginASTAction{
public:
//重写
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg){
return true;
}

std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
//ASTConsumer是一个抽象类,这里返回一个自定义的类来继承
return unique_ptr<HKConsumer>(new HKConsumer(CI));
}
};
}
//注册插件

static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> HK("HKPlugin","this is HKPlugin");






作者:Mjs
链接:https://www.jianshu.com/p/d613d935662d



收起阅读 »

OC底层原理-动态方法决议

当lookupImpOrForward函数从cache和methodTable中找不到对应Method,继续向下执行就会来到resolveMethod_locked函数也就是我们常说的动态方法决议 if (slowpath(behavior & ...
继续阅读 »

当lookupImpOrForward函数从cache和methodTable中找不到对应Method,继续向下执行就会来到resolveMethod_locked函数也就是我们常说的动态方法决议


    if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}

resolveMethod_locked

    runtimeLock.assertLocked();
ASSERT(cls->isRealized());

runtimeLock.unlock();

if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}

// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);

只执行一次

behavior & LOOKUP_RESOLVER
behavior ^= LOOKUP_RESOLVER;
这俩步操作保证resolveMethod_locked只被执行一次

resolveInstanceMethod


static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);

if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
//根类NSObject有默认实现兜底,不会走到这里
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//向cls对象发送resolveInstanceMethod:消息,参数为当前的sel
bool resolved = msg(cls, resolve_sel, sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//从方法缓存中再快速查找一遍
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

resolveClassMethod


static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());

if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
// NSObject有兜底实现
return;
}

Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

resolveMethod_locked会发送resolveInstanceMethod:和resolveClassMethod:消息,为了减少程序的崩溃提用户体验,苹果在这里给开发者一次机会去补救,这个过程就叫做动态方法决议。这里也体现了aop编程思想,在objc_msg流程中给开发者提供了一个切面,切入自己想要处理,比如安全处理,日志收集等等。

resolveClassMethod


static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());

if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}

Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

看完源码思考俩个问题

1.为什么在resloveInstanceMethod函数中调用了一次lookUpImpOrNilTryCache,resolveMethod_locked函数最后又调用了一次lookUpImpOrNilTryCache?这俩次分别有什么作用?

  • 第一次TryCache流程分析

堆栈信息-->第一次tryCache会把我动态添加的方法存进cache

本次TryCache,会调用lookUpImpOrForWard函数查找MethodTable。入参behavior值为4,找不到imp的话不会再走动态决议和消息转发,直接return nil,分支如下:

    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
所以这次tryCache实际的作用就是在动态决议添加方法之后,找到方法,并调用log_and_fill_cache函数存进缓存(佐证了下面这段注释)

   // Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
  • 第二次TryCache流程分析

这次我们直接看注释吧

    // chances are that calling the resolver have populated the cache
// so attempt using it

调用动态决议可能填充了得缓存,并尝试使用它。嗯,第二次tryCache的作用已经简单明了。
本次调用入参behavior值为1,methodTable查找不到imp不会走动态决议流程,但会调用消息转发

  • 为什么分为俩次呢,一次不行吗?

为什么不最后查找方法,填充缓存再返回,反而要先填充缓存,再尝试从缓存中查找,这么做有什么好处呢?

有个关于多线程的猜想:

假如线程a发送消息s进入了动态决议流程,此时线程b也发送消息s,这时候如果缓存中有已添加的imp响应消息s,是不是就不会继续慢速查找,动态决议等后续流程。这么想,动态决议添加的方法是不是越先添加到缓存越好。

另外一点我们看到resolveClassMethod之后,也尝试从缓存中查找,而且找不到又调用了一遍resolveInstanceMethod。

可已看出苹果开发者在设计这段流程的思考🤔可能是:
既然你愿意通过动态方法决议去添加这个imp,费了这么大功夫,很显然你想使用该imp,而且使用的频率可能不低。既然如此在resolver方法调用完毕,我就帮你放进缓存吧。以后你想用直接从缓存中找。

2. 为什么类resolver之后会尝试调用instance的resolver?难道instance的resolver还能解决类方法缺失的问题?

关于这个问题,我们来看张经典的

如果我们查找一个类方法沿着继承链最终会找到NSObject(rootMetaClass的父类是NSObject),这会导致一个有意思的问题:我们的NSObject对象方法可以响应类方法的sel

看个实例

给NSObect添加个instaceMethod



是不是很惊喜,其实我们底层对classMethod和InstanceMethod根本没有区分,classMethod也是InstanceMethod

* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;

return class_getInstanceMethod(cls->getMeta(), sel);
}
只不过,找classMethod是从MetaClass查找InstanceMethod,找InstanceMethod是从class找InstanceMethod。
透过现象看本质,这里就可以解释,为什么resolveClass完毕,缓存中找不到imp,会再次调用resolveInstance。显然,我们给NSObject添加InstanceMethod可以解决问题,而且可以在这里我们也可以添加classMethod。毕竟classMethod也是InstanceMethod。






作者:可可先生_3083
链接:https://www.jianshu.com/p/2d1372b4d2c9





收起阅读 »

iOS 攻防 - DYLD_INSERT_LIBRARIES

Tweak是通过DYLD_INSERT_LIBRARIES来插入动态库的,那么它是怎么做到的呢?这就需要去dyld源码中探究了。一、 DYLD_INSERT_LIBRARIES原理由于dyld源码中b不同版本有变动,需要分别看下新老版本的实现1.1 dyld-...
继续阅读 »

Tweak是通过DYLD_INSERT_LIBRARIES来插入动态库的,那么它是怎么做到的呢?这就需要去dyld源码中探究了。

一、 DYLD_INSERT_LIBRARIES原理

由于dyld源码中b不同版本有变动,需要分别看下新老版本的实现

1.1 dyld-519.2.2 源码

打开dyld源码工程,搜索DYLD_INSERT_LIBRARIES关键字,在dyld.cpp5906行有如下代码:

// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}


这段代码是判断DYLD_INSERT_LIBRARIES不为空就循环加载插入动态库


if ( gLinkContext.processIsRestricted ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}


这里判断进程如果受限制(processIsRestricted不为空)执行pruneEnvironmentVariablespruneEnvironmentVariables会移除DYLD_INSERT_LIBRARIES中的数据,相当于被清空了。这样插入的动态库就不会被加载了。

既然越狱插件是通过DYLD_INSERT_LIBRARIES插入的,那么只要让自己的进程受限就能起到保护作用了。

搜索processIsRestricted = true是在4696行设置值的:

// any processes with setuid or setgid bit set or with __RESTRICT segment is restricted
if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {
gLinkContext.processIsRestricted = true;
}

issetugid不能在上架的App中设置,那么就只能设置hasRestrictedSegment了,这里传入的参数是主程序:

static bool hasRestrictedSegment(const macho_header* mh)
{
//load command 数量
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

//dyld::log("seg name: %s\n", seg->segname);
//读取__RESTRICT SEGMENT
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
//读取__restrict SECTION
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}

这段代码的意思是判断load commands中有没有__RESTRICT SECTIONSECTION中有没有__restrict SEGMENT

也就是说只要有这个SECTION就会开启进程受限了。

1.2 dyld-851.27源码

dyld2.cpp7120行中仍然有DYLD_INSERT_LIBRARIES的判断
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

processIsRestricted变成了一个函数

bool processIsRestricted()
{
#if TARGET_OS_OSX
return !gLinkContext.allowEnvVarsPath;
#else
return false;
#endif
}

这里可以看到只在OSX下才有效。

6667行也只有OSX下才有可能清空环境变量:

#if TARGET_OS_OSX
if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else
#endif
{
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}

hasRestrictedSegment也变成了OSX下专属:

#if TARGET_OS_OSX
static bool hasRestrictedSegment(const macho_header* mh)
{
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

//dyld::log("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}
#endif

结论:iOS 10以前dyld会判断主程序是否有__RESTRICT,__restrict来决定是否加载DYLD_INSERT_LIBRARIESiOS 10及以后并不会进行判断直接进行了加载。


二、 DYLD_INSERT_LIBRARIES 攻防

2.1 iOS10以前攻防

2.1.1 RESTRIC段防护


Other Linker Flags中输入-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null


这样通过DYLD_INSERT_LIBRARIES注入的库就无效了。越狱手机上的插件就无效了。(仅在iOS 10以下有效)。


2.1.2 修改二进制破解

针对RESTRIC的防护可以用二进制修改器将段名称修改掉,就可以绕过检测了。
修改Data中的任意一位这个值就变了:



修改后重签就可以了。


2.1.3 防止RESTRICT被修改


针对RESTRICT被修改可以在代码中判断MachO中是否有对应的RESTRIC,如果没有就证明被修改了。参考dyld源码修改判断如下:
#import <mach-o/dyld.h>

#if __LP64__
#define macho_header mach_header_64
#define LC_SEGMENT_COMMAND LC_SEGMENT_64
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT
#define LC_ENCRYPT_COMMAND LC_ENCRYPTION_INFO
#define macho_segment_command segment_command_64
#define macho_section section_64
#else
#define macho_header mach_header
#define LC_SEGMENT_COMMAND LC_SEGMENT
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT_64
#define LC_ENCRYPT_COMMAND LC_ENCRYPTION_INFO_64
#define macho_segment_command segment_command
#define macho_section section
#endif

static bool hp_hasRestrictedSegment(const struct macho_header* mh) {
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND: {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
printf("seg->segname: %s\n",seg->segname);
//dyld::log("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
printf("sect->sectname: %s\n",sect->sectname);
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
return false;
}
调用

+ (void)load {
//获取主程序 macho_header
const struct macho_header *header = _dyld_get_image_header(0);
if (hp_hasRestrictedSegment(header)) {
NSLog(@"没有修改");
} else {
NSLog(@"被修改了");
}
}
这样就能知道RESTRICT有没有被修改。要Hook检测逻辑就需要找到hp_hasRestrictedSegment函数的地址进行inline hook。或者找到调用hp_hasRestrictedSegment的地方,那么在检测过程中就不能有明显的特征。一般将结果告诉服务端。或者做一些破坏功能的逻辑,比如网络请求相关的内容。

2.2 iOS10及以后攻防

2.2.1 使用DYLD源码防护(黑白名单)

既然iOS10以上系统不进行判断检测了,那么我们可以自己扫描判断哪些应该被加载哪些不能被加载。


#import <mach-o/dyld.h>

const char *whiteListLibStrs =
"/usr/lib/substitute-inserter.dylib/System/Library/Frameworks/Foundation.framework/Foundation/usr/lib/libobjc.A.dylib/usr/lib/libSystem.B.dylib/System/Library/Frameworks/UIKit.framework/UIKit/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation/System/Library/PrivateFrameworks/CoreAutoLayout.framework/CoreAutoLayout/usr/lib/libcompression.dylib/System/Library/Frameworks/CFNetwork.framework/CFNetwork/usr/lib/libarchive.2.dylib/usr/lib/libicucore.A.dylib/usr/lib/libxml2.2.dylib/usr/lib/liblangid.dylib/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit/usr/lib/libCRFSuite.dylib/System/Library/PrivateFrameworks/SoftLinking.framework/SoftLinking/usr/lib/libc++abi.dylib/usr/lib/libc++.1.dylib/usr/lib/system/libcache.dylib/usr/lib/system/libcommonCrypto.dylib/usr/lib/system/libcompiler_rt.dylib/usr/lib/system/libcopyfile.dylib/usr/lib/system/libcorecrypto.dylib";

const char *blackListLibStrs =
"/usr/lib/libsubstitute.dylib/usr/lib/substitute-loader.dylib/usr/lib/libsubstrate.dylib/Library/MobileSubstrate/DynamicLibraries/RHRevealLoader";

void imageListCheck() {
//进程依赖的库数量
int count = _dyld_image_count();
//第一个为自己。过滤掉,因为每次执行的沙盒路径不一样。
for (int i = 1; i < count; i++) {
const char *image_name = _dyld_get_image_name(i);
// printf("%s",image_name);
//黑名单检测
if (strstr(blackListLibStrs, image_name)) {//不在白名单
printf("image_name in black list: %s\n",image_name);
break;
}
//白名单检测
if (!strstr(whiteListLibStrs, image_name)) {
printf("image_name not in white list: %s\n",image_name);
}
}
}

调用

+ (void)load {
imageListCheck();
}
  • 白名单可以直接通过_dyld_get_image_name获取,这里和系统版本有关。需要跑支持的系统版本获取得到并集。维护起来比较麻烦。
  • 黑名单中可以将一些越狱库和检测到的异常库放入其中。
  • 一般检测到问题直接上报服务端。不要直接表现出异常。

黑白名单一般都通过服务端下发,黑名单直接检测出问题上报服务端处理,白名单维护用来检测上报未知的库供分析更新黑白名单。

这种防护方式可以通过fishhook Hook _dyld_image_count_dyld_get_image_name来做排查是哪块做的检测从而去绕过。

  • 对于检测代码最好混淆函数名称。
  • 返回值不要返回一个布尔值,函数被hook之后或者被修改成返回YES 之后很多判断代码都没用了。最好返回特定字符串加密这种。
  • 检测到被注入时不要exit(0)完事,太明显了,这种很容易被绕过。攻防的核心不在于防护技术,而在于会不会被对方发现。微信的做法就是上报服务端封号处理。
  • 在检测到时可以悄悄对业务逻辑做一些处理,比如网络请求正常返回但是页面显示异常或者功能不全等。

没有绝对安全的代码,只不过在与会不会被对方发现以及破解的代价。如果破解代价大于收益很少有人去破解的。



作者:HotPotCat
链接:https://www.jianshu.com/p/79a24b728b99。


收起阅读 »

iOS 攻防 - ptrace

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且...
继续阅读 »

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。
ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且可以检测被控制进程的内存和寄存器里面的数据。ptrace可以用来实现断点调试和系统调用跟踪。

一、反调试ptrace

iOS#import 头文件不能直接导入,所以需要我们自己导出头文件引入调用。当然也可以声明ptrace函数直接调用。

1.1 ptrace 头文件

  1. 直接创建一个macOS程序导入#import 头文件,点进去拷贝生成一个.h文件就可以了:


/*
* Copyright (c) 2000-2005 Apple Computer, Inc. All rights reserved.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. The rights granted to you under the License
* may not be used to create, or enable the creation or redistribution of,
* unlawful or unlicensed copies of an Apple operating system, or to
* circumvent, violate, or enable the circumvention or violation of, any
* terms of an Apple operating system software license agreement.
*
* Please obtain a copy of the License at
*
http://www.opensource.apple.com/apsl/
and read it before using this file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
*/

/* Copyright (c) 1995 NeXT Computer, Inc. All Rights Reserved */
/*-
* Copyright (c) 1984, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* @(#)ptrace.h 8.2 (Berkeley) 1/4/94
*/


#ifndef _SYS_PTRACE_H_
#define _SYS_PTRACE_H_

#include
#include

enum {
ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};


#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_READ_I 1 /* read word in child's I space */
#define PT_READ_D 2 /* read word in child's D space */
#define PT_READ_U 3 /* read word in child's user structure */
#define PT_WRITE_I 4 /* write word in child's I space */
#define PT_WRITE_D 5 /* write word in child's D space */
#define PT_WRITE_U 6 /* write word in child's user structure */
#define PT_CONTINUE 7 /* continue the child */
#define PT_KILL 8 /* kill the child process */
#define PT_STEP 9 /* single step the child */
#define PT_ATTACH ePtAttachDeprecated /* trace some running process */
#define PT_DETACH 11 /* stop tracing a process */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define PT_THUPDATE 13 /* signal for thread# */
#define PT_ATTACHEXC 14 /* attach to running process with signal exception */

#define PT_FORCEQUOTA 30 /* Enforce quota for root */
#define PT_DENY_ATTACH 31

#define PT_FIRSTMACH 32 /* for machine-specific requests */

__BEGIN_DECLS


int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);


__END_DECLS

#endif /* !_SYS_PTRACE_H_ */

  1. 直接声明函数:
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
  • _request:要处理的事情
  • _pid:要操作的进程
  • _addr_data:取决于_pid参数,要传递的数据地址和数据本身。

1.2 ptrace调用

//告诉系统当前进程拒绝被debugserver附加
ptrace(PT_DENY_ATTACH, 0, 0, 0);
//ptrace(31, 0, 0, 0);

PT_DENY_ATTACH表示拒绝附加,值为31。如果仅仅是声明函数就传31就好了。_pid0表示当前进程。这里不传递任何数据。

分别在以下方法中调用

  1. load方法中调用:
+ (void)load {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. constructor中调用:
__attribute__((constructor)) static void entry() {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. main函数中调用:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}


  1. didFinishLaunchingWithOptions中调用:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
return YES;
}

123情况下Xcode启动调试后调试直接断开,App能正常操作不能调试。4在调试情况下App直接闪退,正常打开没问题。同时调用的情况下以第一次为准。

也就是说 :ptracemain函数之后调用App会直接闪退,main以及之前调用会停止进程附加,以第一次调用为准。正常打开App没有问题,只影响LLDB调试。

通过上面的验证说明在程序没有加载的时候调用ptrace会设置一个标志,后续程序就不会被附加了,如果在已经被附加了的情况下调用ptrace会直接退出(因为这里ptrace附加传递的pid0主程序本身)。


PT_DENY_ATTACH
This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent.
ENOTSUP含义如下:
define ENOTSUP 45 //Operation not supported
之前在手机端通过debugserver附加程序直接报错11,定义如下:
PT_DETACH 11 // stop tracing a process

二、 破解ptrace

ptrace的特征:附加不了、Xcode运行闪退/停止附加、使用正常。

既然ptrace可以组织调试,那么我们只要Hook了这个函数绕过PT_DENY_ATTACH的调用就可以了。首先想到的就是fishhook


#import "fishhook.h"

int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

int hp_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){
if (_request != 31) {//不是拒绝附加
return ptrace_p(_request, _pid, _addr, _data);
}
return 0;
}

void hp_hook_ptrace() {
struct rebinding ptrace_rb;
ptrace_rb.name ="ptrace";
ptrace_rb.replacement = hp_ptrace;
ptrace_rb.replaced = (void *)&ptrace_p;

struct rebinding bds[] = {ptrace_rb};
rebind_symbols(bds, 1);
}

+ (void)load {
hp_hook_ptrace();
}

这样就能够进行附加调试了。


三、防止ptrace被破解


3.1 提前Hook防止ptrace被Hook


既然ptrace能够被Hook,那么自己先Hookptrace。调用的时候直接调用自己存储的地址就可以了。我们可以在自己的项目中增加一个Framework。这个库在Link Binary With Libraries中尽可能的靠前。这与dyld加载动态库的顺序有关。
这样就可以不被ptrace Hook了。代码逻辑和1.2中相同,只不过调用要换成ptrace_p
记的头文件中导出ptrace_p

CF_EXPORT int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

创建一个Monkey工程,将3.1生成的.app包拖入工程重签名,这个时候主程序通过调用ptrace已经不能阻止我们调试了,但是调用ptrace_p的地方Monkey Hook不到了。

3.2 修改二进制破解提前Hook ptrace


Monkey的工程中打ptrace符号断点:

这个时候可以看到是didFinishLaunchingWithOptions中调用了ptrace_p函数:
Hopper打开MachO文件找到didFinishLaunchingWithOptions方法:

然后一直点下去找到ptrace_p是属于Inject.framework的:

.appFrameworks中找到Inject.frameworkHopper打开,可以看到_rebind_symbols,上面的参数是ptrace

这里我们可以直接修改ptrace让先Hook的变成另外一个函数,但是有风险点是App内部调用ptrace_p的时候如果没有判断空就crash了。如果判断了可以这么处理。
还有另外一个方式是修改didFinishLaunchingWithOptions代码中的汇编,修改blr x8NOP这样就绕过了ptrace_p的调用。





作者:HotPotCat
链接:https://www.jianshu.com/p/9ed2de5e7497












收起阅读 »

Android顶部悬浮条控件HoveringScroll

上滑停靠顶端悬浮框,下滑恢复原有位置滑动时,监听ScrollView的滚动Y值和悬浮区域以上的高度进行比较计算,对两个控件(布局)的显示隐藏来实现控件的顶部悬浮,通过addView和removeView来实现。###具体实现步骤:1.让ScrollView实现...
继续阅读 »


上滑停靠顶端悬浮框,下滑恢复原有位置

滑动时,监听ScrollView的滚动Y值和悬浮区域以上的高度进行比较计算,对两个控件(布局)的显示隐藏来实现控件的顶部悬浮,通过addViewremoveView来实现。

###具体实现步骤:

1.让ScrollView实现滚动监听

具体参见HoveringScrollview

2.布局实现

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin" >

<!-- zing定义view: HoveringScrollview -->
<com.steve.hovering.samples.HoveringScrollview
android:id="@+id/hoveringScrollview"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<RelativeLayout
android:id="@+id/rlayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" >

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="TOP信息\nTOP信息\nTOP信息\nTOP信息"
android:textColor="#d19275"
android:textSize="30sp" />
</RelativeLayout>

<!-- 这个悬浮条必须是固定高度:如70dp -->
<LinearLayout
android:id="@+id/search02"
android:layout_width="match_parent"
android:layout_height="70dp" >

<LinearLayout
android:id="@+id/hoveringLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#A8A8A8"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="10dp" >

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="10dp"
android:text="¥188\r\n原价:¥399"
android:textColor="#FF7F00" />

<Button
android:id="@+id/btnQiaBuy"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#FF7F00"
android:padding="10dp"
android:onClick="clickListenerMe"
android:text="立即抢购"
android:textColor="#FFFFFF" />
</LinearLayout>
</LinearLayout>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="1测试内容\n2测试内容\n3测试内容\n4测试内容\n5测试内容\n6测试内容\n7测试内容\n8测试内容\n9测试内容\n10测试内容\n11测试内容\n12测试内容\n13测试内容\n14测试内容\n15测试内容\n16测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n25测试内容"
android:textSize="40sp" />
</LinearLayout>
</com.steve.hovering.samples.HoveringScrollview>

<LinearLayout
android:id="@+id/search01"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical" >
</LinearLayout>

</RelativeLayout>

3.监听变化,实现悬停效果

通过search01和search02的addViewremoveView来实现


代码下载:ijustyce-HoveringScroll-master.zip

收起阅读 »

Android仿微信图片选择器-LQRImagePicker

LQRImagePicker完全仿微信的图片选择,并且提供了多种图片加载接口,选择图片后可以旋转,可以裁剪成矩形或圆形,可以配置各种其他的参数##一、简述:本项目是基于ImagePicker完善及界面修改。 主要工作:原项目中UI方面与微信有明显差别,如:文件...
继续阅读 »

LQRImagePicker

完全仿微信的图片选择,并且提供了多种图片加载接口,选择图片后可以旋转,可以裁剪成矩形或圆形,可以配置各种其他的参数

##一、简述:

本项目是基于ImagePicker完善及界面修改。 主要工作:

  1. 原项目中UI方面与微信有明显差别,如:文件夹选择菜单的样式就不是很美观,高度比例与微信的明显不同,故对其进行美化;

  2. 原项目在功能方面有一个致命的BUG,在一开始打开菜单后,随便点击一张图片就会直接崩溃(亲测4.4可用,但6.0直接崩溃),本项目已对此进行了解决;

  3. 编码方面,原项目中获取本地文件uri路径时,使用Uri.fromFile(),这种方式不好,控制台会一直报错(such file or directory no found),故使用Uri.parse()进行代替。

##二、使用:

不得不说,原项目是一个非常不错的项目,有很多地方值得我们学习,其中图片的加载方案让我受益匪浅,通过定义一个接口,由第三方开发者自己在自己项目中实现,避免了在库中强制使用指定图片加载工具的问题,使得本项目的扩展性增强。当然也有其他值得学习的地方,在 ImagePicker中有详细的配置方式,如有更多需求请前往原项目查看学习。这里我只记录下我自己项目中的使用配置:

###1、在自己项目中添加本项目依赖:

compile 'com.lqr.imagepicker:library:1.0.0'

###2、实现ImageLoader接口(注意不是com.nostra13.universalimageloader.core.ImageLoader),实现图片加载策略:

/**
* @创建者 CSDN_LQR
* @描述 仿微信图片选择控件需要用到的图片加载类
*/
public class UILImageLoader implements com.lqr.imagepicker.loader.ImageLoader {

@Override
public void displayImage(Activity activity, String path, ImageView imageView, int width, int height) {
ImageSize size = new ImageSize(width, height);
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(Uri.parse("file://" + path).toString(), imageView, size);
}

@Override
public void clearMemoryCache() {
}
}

###3、在自定义Application中初始化(别忘了在AndroidManifest.xml中使用该自定义Application):

/**
* @创建者 CSDN_LQR
* @描述 自定义Application类
*/
public class App extends Application {

@Override
public void onCreate() {
super.onCreate();
initUniversalImageLoader();
initImagePicker();
}

private void initUniversalImageLoader() {
//初始化ImageLoader
ImageLoader.getInstance().init(
ImageLoaderConfiguration.createDefault(getApplicationContext()));
}

/**
* 初始化仿微信控件ImagePicker
*/
private void initImagePicker() {
ImagePicker imagePicker = ImagePicker.getInstance();
imagePicker.setImageLoader(new UILImageLoader()); //设置图片加载器
imagePicker.setShowCamera(true); //显示拍照按钮
imagePicker.setCrop(true); //允许裁剪(单选才有效)
imagePicker.setSaveRectangle(true); //是否按矩形区域保存
imagePicker.setSelectLimit(9); //选中数量限制
imagePicker.setStyle(CropImageView.Style.RECTANGLE); //裁剪框的形状
imagePicker.setFocusWidth(800); //裁剪框的宽度。单位像素(圆形自动取宽高最小值)
imagePicker.setFocusHeight(800); //裁剪框的高度。单位像素(圆形自动取宽高最小值)
imagePicker.setOutPutX(1000);//保存文件的宽度。单位像素
imagePicker.setOutPutY(1000);//保存文件的高度。单位像素
}
}

###4、打开图片选择界面代码:

public static final int IMAGE_PICKER = 100;

Intent intent = new Intent(this, ImageGridActivity.class);
startActivityForResult(intent, IMAGE_PICKER);

###5、获取所选图片信息:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == ImagePicker.RESULT_CODE_ITEMS) {//返回多张照片
if (data != null) {
//是否发送原图
boolean isOrig = data.getBooleanExtra(ImagePreviewActivity.ISORIGIN, false);
ArrayList<ImageItem> images = (ArrayList<ImageItem>) data.getSerializableExtra(ImagePicker.EXTRA_RESULT_ITEMS);

Log.e("CSDN_LQR", isOrig ? "发原图" : "不发原图");//若不发原图的话,需要在自己在项目中做好压缩图片算法
for (ImageItem imageItem : images) {
Log.e("CSDN_LQR", imageItem.path);
}
}
} }

代码下载:ImagePicker-master.zip

收起阅读 »

Android高度自定义日历控件-CalenderView

CalenderViewAndroid上一个优雅、高度自定义、性能高效的日历控件,支持标记、自定义颜色、农历等。Canvas绘制,速度快、占用内存低Gradlecompile 'com.haibin:calendarview:1.0.4'<depende...
继续阅读 »

CalenderView

Android上一个优雅、高度自定义、性能高效的日历控件,支持标记、自定义颜色、农历等。Canvas绘制,速度快、占用内存低

Gradle

compile 'com.haibin:calendarview:1.0.4'
<dependency>
<groupId>com.haibin</groupId>
<artifactId>calendarview</artifactId>
<version>1.0.4</version>
<type>pom</type>
</dependency>

使用方法

 <com.haibin.calendarview.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:min_year="2004"
app:scheme_text="假"
app:scheme_theme_color="@color/colorPrimary"
app:selected_color="#30cfcfcf"
app:selected_text_color="#333333"
app:week_background="#fff"
app:week_text_color="#111" />

attrs

<declare-styleable name="CalendarView">
       <attr name="week_background" format="color" /> <!--星期栏的背景-->
       <attr name="week_text_color" format="color" /> <!--星期栏文本颜色-->
       <attr name="scheme_theme_color" format="color" /> <!--标记的颜色-->
       <attr name="current_day_color" format="color" /> <!--今天的文本颜色-->
<attr name="scheme_text" format="string" /> <!--标记文本-->
<attr name="selected_color" format="color" /> <!--选中颜色-->
<attr name="selected_text_color" format="color" /> <!--选中文本颜色-->
       <attr name="min_year" format="integer" />  <!--最小年份1900-->
       <attr name="max_year" format="integer" /> <!--最大年份2099-->
</declare-styleable>

api

public int getCurDay(); //今天
public int getCurMonth(); //当前的月份
public int getCurYear(); //今年
public void showSelectLayout(final int year); //快速弹出年份选择月份
public void closeSelectLayout(final int position); //关闭选择年份并跳转日期
public void setOnDateChangeListener(OnDateChangeListener listener);//添加事件
public void setOnDateSelectedListener(OnDateSelectedListener listener);//日期选择事件
public void setSchemeDate(List<Calendar> mSchemeDate);//标记日期
public void setStyle(int schemeThemeColor, int selectLayoutBackground, int lineBg);
public void update();//动态更新

代码下载:CalendarView.zip


收起阅读 »