注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

细说浏览器输入URL后发生了什么

细说浏览器输入URL后发生了什么总体概览大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:DNS域名解析在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可...
继续阅读 »

细说浏览器输入URL后发生了什么

总体概览

大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:


DNS域名解析

在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可用、高并发和分布式的,它是树状结构,如图:


  • 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址

  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址

  • 权威 DNS 服务器 :返回相应主机的 IP 地址


DNS的域名查找,在客户端和浏览器,本地DNS之间的查询方式是递归查询;在本地DNS服务器与根域及其子域之间的查询方式是迭代查询;


递归过程:

GitHub


在客户端输入 URL 后,会有一个递归查找的过程,从浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找,这个过程中任何一步找到了都会结束查找流程。


如果本地DNS服务器无法查询到,则根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下图:

GitHub


结合起来的过程,可以用一个图表示:

GitHub

在查找过程中,有以下优化点:



  • DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。

  • 在域名和 IP 的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。


建立TCP连接


首先,判断是不是https的,如果是,则HTTPS其实是HTTP + SSL / TLS 两部分组成,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。


进行三次握手,建立TCP连接。




  1. 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;




  2. 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;




  3. 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。




SSL握手过程



  1. 第一阶段 建立安全能力 包括协议版本 会话Id 密码构件 压缩方法和初始随机数

  2. 第二阶段 服务器发送证书 密钥交换数据和证书请求,最后发送请求-相应阶段的结束信号

  3. 第三阶段 如果有证书请求客户端发送此证书 之后客户端发送密钥交换数据 也可以发送证书验证消息

  4. 第四阶段 变更密码构件和结束握手协议


完成了之后,客户端和服务器端就可以开始传送数据。更多 HTTPS 的资料可以看这里:



备注


ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0。TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1。


SYN(SYNchronization):在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。


FIN(finis)即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。


发送HTTP请求,服务器处理请求,返回响应结果


TCP连接建立后,浏览器就可以利用HTTP/HTTPS协议向服务器发送请求了。服务器接受到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200.


这里有发生的一个过程是HTTP缓存,是一个常考的考点,大致过程如图:

GitHub

其过程,比较多内容,可以参考我的这篇文章《浏览器相关原理(面试题)详细总结一》,这里我就不详细说了~


关闭TCP连接




  1. 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;




  2. 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我"同意"你的关闭请求;




  3. 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;




  4. 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。




浏览器渲染


按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、栅格化和显示。如图:

GitHub



  1. 渲染进程将 HTML 内容转换为能够读懂DOM 树结构。

  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。

  3. 创建布局树,并计算元素的布局信息。

  4. 对布局树进行分层,并生成分层树。

  5. 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。

  6. 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。


构建 DOM 树


浏览器从网络或硬盘中获得HTML字节数据后会经过一个流程将字节解析为DOM树,先将HTML的原始字节数据转换为文件指定编码的字符,然后浏览器会根据HTML规范来将字符串转换成各种令牌标签,如html、body等。最终解析成一个树状的对象模型,就是dom树。

GitHub


具体步骤:



  1. 转码(Bytes -> Characters)—— 读取接收到的 HTML 二进制数据,按指定编码格式将字节转换为 HTML 字符串

  2. Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为结构清晰的 Tokens,每个 Token 都有特殊的含义同时有自己的一套规则

  3. 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都添加特定的属性(或属性访问器),通过指针能够确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)

  4. 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建立起每个结点的父子兄弟关系


样式计算


渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。


CSS 样式来源主要有 3 种,分别是通过 link 引用的外部 CSS 文件、style标签内的 CSS、元素的 style 属性内嵌的 CSS。,其样式计算过程主要为:

GitHub

可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。处理完成后再处理样式的继承和层叠,有些文章将这个过程称为CSSOM的构建过程。


页面布局


布局过程,即排除 script、meta 等功能化、非视觉节点,排除 display: none 的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。如图:

GitHub

其中,这个过程需要注意的是回流和重绘,关于回流和重绘,详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


生成分层树


页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),如图:

GitHub

如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。在浏览器中,你可以打开 Chrome 的"开发者工具",选择"Layers"标签。渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。


并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


栅格化


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。如图:


GitHub


通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。


显示


最后,合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上,渲染过程完成。




链接:https://juejin.cn/post/6844904054074654728
收起阅读 »

浏览器工作原理&前端安全

网络安全 三原则 在传输中,不允许明文传输用户隐私数据; 在本地,不允许明文保存用户隐私数据; 在服务器,不允许明文保存用户隐私数据; http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全...
继续阅读 »

网络安全


三原则



  1. 在传输中,不允许明文传输用户隐私数据;

  2. 在本地,不允许明文保存用户隐私数据;

  3. 在服务器,不允许明文保存用户隐私数据;


http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全暴露,,这一攻击手法叫做MITM(Man In The Middle)中间人攻击。
在网络来说,我们知道不论 POST 请求和 GET 请求都会被抓包,在没有使用 HTTPS 的情况下,抓包我们是防不住的,如果明文传输用户隐私,那后果就不说了。


很多用户密码是通用的,一旦被不法分子窃取,去其他网站撞库,造成损失。
上文说到http传输因为有三大风险



  • 窃听风险(eavesdropping):第三方可以获知通信内容。

  • 篡改风险(tampering):第三方可以修改通信内容。

  • 冒充风险(pretending):第三方可以冒充他人身份参与通信。


所以提到了https
https 可以认为是 http + TLS TLS 是传输层加密协议,它的前身是 SSL 协议,如果没有特别说明,SSL 和 TLS 说的都是同一个协议。


加密传输(避免明文传输)


1. 对称加密

加解密使用同一个密钥
客户端和服务端进行通信,采用对称加密,如果只使用一个秘钥,很容易破解;如果每次用不同的秘钥,海量秘钥的管理和传输成本又会比较高。


2.非对称加密

需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)
非对称加密的模式则是:




  • 乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的




  • 甲方获取乙方的公钥,然后用它对信息加密




  • 乙方得到加密后的信息,用私钥解密。


    但当服务端要返回数据,如果用公钥加密,那么客户端并没有私钥用来解密,而如果用私钥加密,客户端虽然有公钥可以解密,但这个公钥之前就在互联网上传输过,很有可能已经有人拿到,并不安全,所以这一过程只用非对称加密是不能满足的。
    (严格来讲,私钥并不能用来加密,只能用作签名使用,这是由于密码学中生成公钥私钥时对不同变量的数学要求是不同的,因此公钥私钥抵抗攻击的能力也不同)
    所以为了满足即使非对称




image.png


https


HTTPS 的出发点是解决HTTP明文传输时信息被篡改和监听的问题。




  • 为了兼顾性能和安全性,使用了非对称加密+对称加密的方案。




  • 为了保证公钥传输中不被篡改,又使用了非对称加密的数字签名功能,借助CA机构和系统根证书的机制保证了HTTPS证书的公信力。


    只传递证书、明文信息、加签加密后的明文信息,注意不传递CA公钥(防止中间人攻击),客户端浏览器可以通过系统根证书拿到CA公钥。(系统或浏览器中内置的CA机构的证书和公钥成为了至关重要的环节)




加密存储
千万不要用明文存储密码
如果用明文存储密码(不管是存在数据库还是日志中),一旦数据泄露,所有用户的密码就毫无保留地暴露在黑客的面前,开头提到的风险就可能发生,那我们费半天劲加密传输密码也失去了意义。


总结
如果我们想要尽可能保证用户的信息安全,我们需要做以下的工作



  • 使用https请求

  • 利用RSA加密密码并传输数据

  • 用BCrypt或者PBKDF2单向加密,并存储


强制使用HTTPS


一些网站购买了SSL证书并将其配置到Web服务器上,以为这就算完事儿了。但这只是表明你启用了HTTPS选项,而用户很可能不会注意到。为确保每个用户都从HTTPS中受益,你应该将所有传入的HTTP请求重定向至HTTPS。这意味着任何一个访问你的网站的用户都将自动切换到HTTPS,从那以后他们的信息传输就安全了。


配合cookie的secure参数,禁止cookie在最初的http请求中被带出去(中间人拦截)。


TCP三次握手四次挥手


Tcp是传输控制协议(Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议


第一次握手:请求连接client->SYN=1, 随机seq=x(数据包首字节序列号)
第二次握手:同意应答,SYN和ACK都置为1,ack=x+1,随机seq=y,返回确认连接
第三次握手:client检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1;——>Server,Server检查ack是否为y+1,ACK是否为1,正确则连接成功!


认证授权+浏览器存储


什么是认证(Authentication)

验证当前用户的身份,证明“你是你自己”
互联网中的认证:



  • 用户名密码登录

  • 邮箱发送登录链接

  • 手机号接收验证码


什么是授权(Authorization)

用户授予第三方应用访问该用户某些资源的权限
安装手机应用时(是否允许访问相册、地理位置等权限)
登录微信小程序(是否允许获取昵称、头像、地区、性别等个人信息)



  • 实现授权的方式有:cookie、session、token、OAuth


什么是凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份
登录成功后,服务器给用户使用的浏览器颁发一个令牌,表明身份,每次请求时带上。


什么是 Cookie


  • HTTP 是无状态的协议,每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的:每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain


特点:
Cookie 的大小受限,一般为 4 KB;
同一个域名下存放 Cookie 的个数是有限制的,不同浏览器的个数不一样,一般为 20 个;
Cookie 支持设置过期时间,当过期时自动销毁;(max-age单位秒,如果是负数,为临时cookie关闭浏览器失效;默认是-1)
每次发起同域下的 HTTP 请求时,都会携带当前域名下的 Cookie;
支持设置为 HttpOnly,防止 Cookie 被客户端的 JavaScript 访问


什么是 Session


  • session 是另一种记录服务器和客户端会话状态的机制

  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中


SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。


什么是localStorage

特点



  • 大小限制为 5MB ~10MB;

  • 在同源的所有标签页和窗口之间共享数据;

  • 数据仅保存在客户端,不与服务器进行通信;

  • 数据持久存在且不会过期,重启浏览器后仍然存在;

  • 对数据的操作是同步的。


什么是sessionStorage


  • sessionStorage 的数据只存在于当前浏览器的标签页;

  • 数据在页面刷新后依然存在,但在关闭浏览器标签页之后数据就会被清除;

  • 与 localStorage 拥有统一的 API 接口;

  • 对数据的操作是同步的。


什么是 Token(令牌)


  • 访问资源接口(API)时所需要的资源凭证

  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)


特点:



  • 服务端无状态化、可扩展性好

  • 支持移动端设备

  • 安全

  • 支持跨程序调用


什么是 JWT


  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。(不使用cookie)


方式:通过Authorization;通过url;跨域的时候,可以把 JWT 放在 POST 请求的数据体里


和session、token的区别是JWT已经包含用户信息,所以不用再去数据库里查询了,而且


什么是 XSS

Cross-Site Scripting(跨站脚本攻击),是一种代码注入攻击



  • 存储性(任何可输入存入数据库的地方,注入脚本,服务端渲染时将脚本拼接html中返回给浏览器)

  • 反射性(脚本写入url,如路由传参,诱导用户点击,服务端渲染时将脚本拼接html中返回给浏览器)

  • DOM性(脚本写入url,前端 JavaScript 取出 URL 中的恶意代码并执行)


防范:cookie设置readOnly禁止js脚本访问cookie
前端服务端对输入框设置格式检查
转义 HTML(存储、反射)
改成纯前端渲染(存储、反射)
使用react就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患用.textContent、.setAttribute()。


什么是 CSRF
跨站请求伪造(英语:Cross-site request forgery)
用户已经登录了安全网站A,诱导用户访问网站B,B利用A获取的凭证去访问A,绕过用户验证



  • 1.登录受信任网站A,并在本地生成Cookie。

  • 2.在不登出A的情况下,访问危险网站B。


防范:同源策略(origin referrer) token samesite


Base64编码由来


因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就不能通过邮件传送。Base64就是一种基于64个可打印字符来表示二进制数据的表示方法。


ASCII码
在计算机中,所有的数据在存储和运算时都要使用二进制数表示,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,用来统一规定上述常用符号用哪些二进制数来表示


unicode、utf-8、ASCII、base64、哈希md5
ASCII美国信息互换标准代码,用一个字节存储128个字符(其中包括33个控制字符(具有某些特殊功能但是无法显示的字符)
产生原因:
在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示 [2]  。


Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。.Base64编码是从二进制到字符的过程


浏览器工作原理


异步编程


与同步相对的异步则可以理解为在异步操作完成后所要做的任务,它们通常以回调函数或者 Promise 的形式被放入事件队列,再由事件循环 ( Event Loop ) 机制在每次轮询时检查异步操作是否完成,若完成则按事件队列里面的执行规则来依次执行相应的任务。


javascript的运行机制(单线程、任务队列、EventLoop、微任务、宏任务)


单线程特点


单线程可以避免多线程操作带来的复杂的同步问题。


任务队列(JavaScript的运行机制)


  1. 所有同步任务都在主线程上执行,形成一个执行栈。

  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。


Event Loop

每次 Tick 会查看任务队列中是否有需要执行的任务。每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中
1. onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
2. setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
3. ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。


javascript是单线程的,浏览器是多线程的。
进程和线程都是操作系统的概念,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。


进程(process)


进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。
当我们启动一个应用,计算机会至少创建一个进程,cpu会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。


线程(thread)



  • 进程内部的一个执行单元,是被系统独立调度和分派的基本单位。系统创建好进程后,实际上就启动执行了该进程的主执行线程

  • 进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情,所以一个进程可以创建多个线程。

  • 线程自己不需要系统重新分配资源,它与同属一个进程的其它线程共享当前进程所拥有的全部资源。 PS: 进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。



Chrome 采用多进程架构


主要进程



  • Browser Process 浏览器的主进程(负责协调、主控) (1)负责包括地址栏,书签栏,前进后退按钮等部分的工作 (2)负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问 (3)负责各个页面的管理,创建和销毁其他进程

  • Renderer Process 负责一个 tab 内关于网页呈现的所有事情,页面渲染,脚本执行,事件处理等


image.png



  • Plugin Process 负责控制一个网页用到的所有插件,如 flash 每种类型的插件对应一个进程,仅当使用该插件时才创建

  • GPU Process 负责处理 GPU 相关的任务,3D 绘制等


优点
由于默认 新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器。
同样,第三方插件崩溃也不会影响到整个浏览器。
多进程可以充分利用现代 CPU 多核的优势。


缺点
系统为浏览器新开的进程分配内存、CPU 等资源,所以内存和 CPU 的资源消耗也会更大。
不过 Chrome 在内存释放方面做的不错,基本内存都是能很快释放掉给其他程序运行的。


一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。


1.JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JavaScript线程在运行JavaScript程序。


2.GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。但需要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时立即被执行。


3.事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其他线程如鼠标点击、Ajax异步请求等,但由于JavaScript的单线程关系所有这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)


问题



  1. 为什么 Javascript 要是单线程的 ?


JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。
如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。



  1. 为什么 JS 阻塞页面加载 ?


由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。所以为了防止渲染的不可预期结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。



  1. css 加载会造成阻塞吗 ?


CSS 加载不会阻塞 DOM 的解析(并行), Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的所以CSS 加载会阻塞 Dom 的渲染,同时css 会阻塞后面 js 的执行



  1. 什么是 CRP,即关键渲染路径(Critical Rendering Path)? 如何优化 ?


image.png


Html可以逐步解析,和css解析是并行的,但是css不行,因为css的每个属性都是可以改变cssom的,比如后面的把前面设置的font-size覆盖等,所以必须等cssom构建完毕才能进入下一个阶段。CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源。


通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。


优化围绕三因素


关键资源数量(js、css)


关键路径长度


关键字节的数量(字节越小、下载和处理速度都会更快——压缩)


具体做法:


优化dom


html文件尽可能小,删除冗余代码,压缩代码,使用缓存(http cache)


优化cssom


仅把首屏需要的css通过style标签内嵌到head里,其余的使用异步方式非阻塞加载(如Critical CSS)


避免使用@import


@import会把css引入从并行变成串行加载


异步js


所有文本资源都应该尽可能小,删除未使用的代码、缩小文件的尺寸(Minify)、使用gzip压缩(Compress)、使用缓存(HTTP Cache)


可以为script添加async属性异步加载


5.从输入url浏览器渲染的流程。


解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
绘制 RenderObject 树 (paint),绘制页面的像素信息
浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面


6.Event Loop至少包含两个队列,macrotask队列和microtask队列


async/await成对出现,async标记的函数会返回一个Promise对象,可以使用then方法添加回调函数。await后面的语句会同步执行。但 await 下面的语句会被当成微任务添加到当前任务队列的末尾异步执行。


先微后宏


回流 (Reflow)


当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:



  • 页面首次渲染

  • 浏览器窗口大小发生改变

  • 元素尺寸或位置发生改变

  • 元素内容变化(文字数量或图片大小等等)

  • 元素字体大小变化

  • 添加或者删除可见的DOM元素

  • 激活CSS伪类(例如::hover)

  • 查询某些属性或调用某些方法


重绘 (Repaint)


当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。


回流比重绘的代价要更高。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。



  1. 多线程的优点和缺点分别是什么?


优点:


1、将耗时较长的操作(网络请求、图片下载、音频下载、数据库访问等)放在子线程中执行,可以防止主线程的卡死;


2、可以发挥多核处理的优势,提升cpu的使用率。


缺点:


1、每开辟一个子线程就消耗一定的资源;


2、会造成代码的可读性变差;


3、如果出现多个线程同时访问一个资源,会出现资源争夺的情况。


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

收起阅读 »

使用transform和left改变位置的性能区别

使用transform和left改变位置的性能区别现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。F(Frames) P(Per) S(Second) 指的画面每秒钟传输的...
继续阅读 »

使用transform和left改变位置的性能区别

现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。

F(Frames) P(Per) S(Second) 指的画面每秒钟传输的帧数,60FPS指的是每秒钟60帧;换算下来每一帧差不多是16毫秒。 (1 秒 = 1000 毫秒) / 60 帧 = 16.66 毫秒/帧 复制代码但通常浏览器需要花费一些时间将每一帧的内容绘制到屏幕上(包括样式计算、布局、绘制、合成等工作),所以通常我们只有10毫秒来执行JS代码。

那么动画只要接近于60FPS就是比较流畅的,对比一下通过position:left 做动画和transform做动画的性能区别

假设每个人都是用性能最好的手机,浏览器,我们根本用不着去做性能优化,所以在这里为了效果明显,先将环境配置到较低,较差的情况下测试,动画也不能设置为单一的移动

1如何使用google开发者工具查看帧数

1.先按键盘F12, 然后点到performance

2.点击刷新按钮再按确定

image.png

3.把鼠标放在下面就是他对应的帧数

test5.gif

4.现在的浏览器(google为例)已经默认开启了硬件加速器,所以你去对比left和transform其实效果非常不明显,所以先把这个默认关掉

image.png

5.对比效果,应该是在低cpu的情况下测试,将他设置为6

test7.gif

6 查看GPU的使用

image.png

如果你是mac,勾选fps meter, 如果你是windows,勾选我上面写的

我是windows,但是我并看不到帧率的时时变化

7 如果你想查看层级

检查-> layers -> 选择那个可旋转的 -> 查看元素paint code的变化

如果你发现你没有layers, 可以看看三个点里面的more tools,把layers点出来

image.png

4transformcode.gif

2使用position:left (使用left并没有被提升到复合层)

<div class="ball-running"></div>
.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
position: absolute;
border-radius: 50%;
}
@keyframes run-around {
0% {
top: 0;
left: 0;
}
25% {
top: 0;
left: 200px;
}
50% {
top: 200px;
left: 200px;
}
75% {
top: 200px;
left: 0;
}
}

3transformcode.gif


test2.gif


在cpu 4slown down的情况下,我们可以看到上面的FPS刚开始在60左右,后面掉到了4FPS,这个动画是不够流畅的.
帧率呈现出锯齿型


这是对应的帧率


image.png


在cpu6 slow down的帧率下甚至会出现掉帧的情况(下面那些红色的就是dropped frame)


test5.gif


3.使用transform进行做动画(transform提升到了复合层)

.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
border-radius: 50%;
}
@keyframes run-around {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(200px, 0);
}
50% {
transform: translate(200px, 200px);
}
75% {
transform: translate(0, 200px);
}
}

1transformcode.gif


4.从层级方向解释transform性能优于left


建议看这篇文章:
浏览器层合成与页面渲染优化


基本的渲染流程:


image.png


从左往右边看,我们可以看到,浏览器渲染过程如下:


1.解析HTML,生成DOM树,解析CSS,生成CSSOM树
(CSS Object Model ,CSS 对象模型,里面有很多api,包括style rules-内部样式表中所有的CSS规则)
2.将DOM树和CSSOM树结合,生成渲染树(Render Tree)
3.Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
4.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
5.display:将像素发送给GPU,展示在页面上。
复制代码

先了解下什么是渲染层


渲染层: 在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),
当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。
复制代码

1先不涉及任何的层级问题

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
}
.small {
width: 100px;
height: 100px;
background-color: red;
}
</style>

1普通的代码.gif

从上面来看,只有一个渲染层

2加上index

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
}
</style>

1zindex.gif

从视觉上来看,small 的div确实是在big之上,但是和big在同一个渲染层上

3加上transform

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
transform: translateZ(0);
}

1transform.gif

如何形成合成层


上面产生了一个新的层级,也就是合成层


首先,如果你改了一个影响元素布局信息的CSS样式,比如width、height、left、top等(transform除外),那么浏览器会将当前的Layout标记为dirty,因为元素的位置信息变了,将可能会导致整个网页其他元素的位置情况都发生改变,所以需要执行Layout全局重新计算每个元素的位置,如果提升为合成层能够开启gpu加速,并且在渲染的时候不会影响其他的层


并且在使用left的时候,document的paint code一直在变化,而使用transform的paint code一直都是不变的,可看上面的动画gif


有关于层级方面的东西,希望大家共同交流,我觉得自己也没有深刻的了解有些定义,只写了自己会的理解的,希望在查看操作方面能帮到大家



链接:https://juejin.cn/post/6959089368212439076
收起阅读 »

5个 Chrome 调试混合应用的技巧

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。一、调试安卓应用 在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代...
继续阅读 »

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。

一、调试安卓应用


在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代码,这里我们就需要了解安卓应用如何在 Chrome 上进行调试。
接下来简单介绍一下,希望大家还是能实际进行调试看看:


1. 准备工作


需要准备有以下几个事项:



  1. 安卓包必须为可调试包,如果不可以调试,可以找原生的同事提供;

  2. 安卓手机通过数据线连接电脑,然后开启“开发者模式”,并启用“USB 调试”选项。


2. Chrome 启动调试页面


在 Chrome 浏览器访问“chrome://inspect/#devices”,然后在 WebView 列表中选择你要调试的页面,点击“ Inspect ”选项,跟调试 PC 网页一样,使用 Chrome 控制台进行调试。



然后就可以正常进行调试了,操作和平常 Chrome 上面调试页面是一样的。


3. 注意


如果访问 “chrome://inspect/#devices” 页面会一直提示 404,可以在翻墙情况下,先在 Chrome 访问 chrome-devtools-frontend.appspot.com,然后重新访问“chrome://inspect/#devices”即可。

二、筛选特定条件的请求


在 Network 面板中,我们可以在 Filter 输入框中,通过各种筛选条件,来查看满足条件的请求。



  1. 使用场景:


如只需要查看失败或者符合指定 URL 的请求。



  1. 使用方式:


在 Network 面板在 Filter 输入框中,输入各种筛选条件,支持的筛选条件包括:文本、正则表达式、过滤器和资源类型。
这里主要介绍“过滤器”,包括:


这里输入“-”目的是为了让大家能看到 Chrome 提供哪些高级选项,在使用的时候是不需要输入“-”。
如果输入“-.js -.css”则可以过滤掉“.js”和“.css”类型的文件。


关于过滤器更多用法,可以阅读《Chrome DevTools: How to Filter Network Requests》



三、快速断点报错信息


在 Sources 面板中,我们可以开启异常自动断点的开关,当我们代码抛出异常,会自动在抛出异常的地方断点,能帮助我们快速定位到错误信息,并提供完整的错误信息的方法调用栈。
3速断点报错信息.png



  1. 使用场景:


需要调试抛出异常的情况。



  1. 使用方式:


在 Sources 面板中,开启异常自动断点的开关。
3快速断点报错信息.gif


四、断点时修改代码


在 Sources 面板中,我们可以在需要断点的行数右击,选择“Add conditional breakpoint”,然后在输入框中输入表达式(如赋值操作等),后面代码将使用该结果。
4断点时修改代码1.png
4断点时修改代码2.png



  1. 使用场景:


需要在调试时,方便手动修改数据来完成后续调试的时候。



  1. 使用方式:


在 Sources 面板中,在需要断点的行数右击,选择“Add conditional breakpoint”。
4断点时修改代码.gif


五、自定义断点(事件、请求等)


当我们需要进行自定义断点的时候,比如需要拦截 DOM 事件、网络请求等,就可以在 Source 面板,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.png



  1. 使用场景:


需要在调试时,需要增加自定义断点时(如需要拦截 DOM 事件、网络请求等)。



  1. 使用方式:


在 Sources 面板中,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.gif




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



收起阅读 »

如何处理浏览器的断网情况?

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行 坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼 网络问题一直是一个很值得关注的问题。 比如在慢网情况下,增加loading避免重复发...
继续阅读 »

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行


坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼


网络问题一直是一个很值得关注的问题。


比如在慢网情况下,增加loading避免重复发请求,使用promise顺序处理请求的返回结果,或者是增加一些友好的上传进度提示等等。


那么大家有没有想过断网情况下该怎么做呢?比如说网络正常->断网->网络正常。


其实我一直也没想过,直到组里的测试测出一个断网导致的bug,让我意识到重度依赖网络请求的前端,在断网情况下可能会出现严重的bug。


因此我将在这里记录一下自己对系统断网情况下的处理,一方面避免bug产生,一方面保证用户及时在应用内知道网络已经断开连接

概览


为了构建一个 “断网(offline)可用”的web应用,你需要知道应用在什么时候是断网(offline)的。
不仅仅要知道什么时候断网,更要知道什么时候网络恢复正常(online)。
可以分解陈本下面两种常见情况:



  1. 你需要知道用户何时online,这样你可以与服务器之间re-sync(重新同步)。

  2. 你需要知道用户何时offline,这样你可以将你未发出的请求过一段时间再向服务器发出。


通常可以通过online/offline事件去做这个事情。


用于检测浏览器是否连网的navigator.onLine


navigator.onLine



  • true online

  • false offline


可以通过network的online选项切换为offline,打印navigator.onLine验证。


当浏览器不能连接到网络时,这个属性会更新。规范中是这样定义的:

The navigator.onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail)...

用于检测网络状况的navigator.connection


在youtube观看视频时,自动检测网络状况切换清晰度是如何做到的呢?
国内的视频网站也会给出一个切换网络的提醒,该如何去检测呢?
也就是说,有没有办法检测网络状况?判断当前网络是流畅,拥堵,繁忙呢?
可以通过navigator.connection,属性包括effectiveType,rtt,downlink和变更网络事件change。继承自NetworkInformation API。

navigator.connection

online状态下运行console.log(navigator.connection);

{
onchange: null,
effectiveType: "4g",
rtt: 50,
downlink: 2,
saveData: false
}

通过navigator.connection可以判断出online,fast 3g,slow 3g,和offline,这四种状态下的effectiveType分别为4g,3g,2g,4g(rtt,downlink均为0)。


rtt和downlink是什么?NetworkInformation是什么?


这是两个反映网络状况的参数,比type更加具象且更能反映当前网络的真实情况。


常见网络情况rtt和downlink表


注意:rtt和downlink不是定值,而是实时变化的。online时,可能它现在是rtt 100ms,2.2Mb/s,下一秒就变成125ms,2.1Mb/s了。


rtt


  • 连接预估往返时间

  • 单位为ms

  • 值为四舍五入到25毫秒的最接近倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 值越小网速越快。类似ping的time吧

  • 在Web Worker中可用


downlink


  • 带宽预估值

  • 单位为Mbit/s(注意是Mbit,不是MByte。)

  • 值也是四舍五入到最接近的25比特/秒的倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 一般越宽速度越快,也就是,信道上可以传输更多数。(吐槽一句,学过的通信原理还蛮有用。)

  • 值越大网速越快。类似高速一般比国道宽。

  • 在Web Worker中可用


草案(Draft)阶段NetworkInformation API

无论是rtt,还是downlink,都是这个草案中的内容。
除此之外还有downlinkMax,saveData,type等属性。
更多资料可以查询:NetworkInformation


如何检测网络变化去做出响应呢?


NetworkInformation继承自EventTarget,可以通过监听change事件去做一些响应。


例如可以获得网络状况的变更?

var connection = navigator.connection;
var type = connection.effectiveType;

function updateConnectionStatus() {
console.log("网络状况从 " + type + " 切换至" + connection.effectiveType);
type = connection.effectiveType;
}

connection.addEventListener('change', updateConnectionStatus);

监听变更之后,我们可以弹一个Modal提醒用户,也可以出一个Notice通知用户网络有变化,或者可以更高级得去自动切换清晰度(这个应该比较难)。


引出NetworkInformation的概念,只是想起一个抛砖引玉的作用。这种细粒度的网络状况检测,可以结合具体需求去具体实现。


在这篇博文中,我们只处理断网和连网两种情况,下面来看断网事件"offline"和连网事件"online"。


断网事件"offline"和连网事件"online"


浏览器有两个事件:"online" 和 "offline".
这两个事件会在浏览器在online mode和offline mode之间切换时,由页面的<body>发射出去。


事件会按照以下顺序冒泡:document.body -> document -> window。


事件是不能去取消的(开发者在代码上不能手动变为online或者offline,开发时使用开发者工具可以)。


注册上下线事件的几种方式


最最建议window+addEventListener的组合。



  • 通过window或document或document.body和addEventListener(Chrome80仅window有效)

  • 为document或document.body的.ononline或.onoffline属性设置一个js函数。(注意,使用window.ononline和window.onoffline会有兼容性的问题)

  • 也可以通过标签注册事件<body ononline="onlineCb" onoffline="offlineCb"></body>


例子

<div id="status"></div>
<div id="log"></div>
window.addEventListener('load', function() {
var status = document.getElementById("status");
var log = document.getElementById("log");

function updateOnlineStatus(event) {
var condition = navigator.onLine ? "online" : "offline";
status.innerHTML = condition.toUpperCase();

log.insertAdjacentHTML("beforeend", "Event: " + event.type + "; Status: " + condition);
}

window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
});

其中insertAdjacentHTML是在标签节点的邻近位置插入,可以查阅:DOM进阶之insertAdjacentHTML


断网处理项目实战


可以基于vue,react封装出离线处理组件,在需要到的页面引入即可。


思路和效果


只要做到断网提醒+遮罩,上线提醒-遮罩即可。



  • 监听offline,断网给出提醒和遮罩:网络已断开,请检查网络连接。

  • 监听online,连网给出提醒和遮罩:网络已连接。

断网处理组件使用

<OfflineHandle
offlineTitle = "断网处理标题"
desc="断网处理描述"
onlineTitle="连网提醒"
/>
Vue组件
<!--OfflineHandle.vue-->
<template>
<div v-if="mask" class="offline-mask">
<h2 class="offline-mask-title">{{ offlineTitle }}</h2>

<p class="offline-mask-desc">{{ desc }}</p >
</div>
</template>

<script>
export default {
name: "offline-handle",
props: {
offlineTitle: {
type: String,
default: "网络已断开,请检查网络连接。",
},
onlineTitle: {
type: String,
default: "网络已连接",
},
desc: {
type: String,
default: "",
},
duration: {
type: Number,
default: 4.5,
},
},
data() {
return {
mask: false,
};
},
mounted() {
window.addEventListener("offline", this.eventHandle);
window.addEventListener("online", this.eventHandle);
console.log(this.desc);
},
beforeDestroy() {
window.removeEventListener("offline", this.eventHandle);
window.removeEventListener("online", this.eventHandle);
},
methods: {
eventHandle(event) {
const type = event.type === "offline" ? "error" : "success";
this.$Notice[type]({
title: type === "error" ? this.offlineTitle : this.onlineTitle,
desc: type === "error" ? this.desc : "",
duration: this.duration,
});
setTimeout(() => {
this.mask = event.type === "offline";
}, 1500);
},
},
};
</script>

<style lang="css" scoped>
.offline-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
z-index: 9999;
transition: position 2s;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.offline-mask-title {
color: rgba(0, 0, 0, 0.8);
}
.offline-mask-desc {
margin-top: 20px;
color: red;
font-weight: bold;
}
</style>
React组件
// offlineHandle.js
import React, { useState, useEffect } from "react";
import { notification } from "antd";
import "antd/dist/antd.css";
import "./index.css";

const OfflineHandle = (props) => {
const {
offlineTitle = "网络已断开,请检查网络连接。",
onlineTitle = "网络已连接",
desc,
duration = 4.5
} = props;
const [mask, setMask] = useState(false);

const eventHandler = (event) => {
const type = event.type === "offline" ? "error" : "success";
console.log(desc, "desc");
openNotification({
type,
title: type === "error" ? offlineTitle : onlineTitle,
desc: type === "error" ? desc : "",
duration
});
setTimeout(() => {
setMask(event.type === "offline");
}, 1500);
};

const openNotification = ({ type, title, desc, duration }) => {
notification[type]({
message: title,
description: desc,
duration
});
};

useEffect(() => {
window.addEventListener("offline", eventHandler);
window.addEventListener("online", eventHandler);
return () => {
window.removeEventListener("offline", eventHandler);
window.removeEventListener("online", eventHandler);
};
}, []);

const renderOfflineMask = () => {
if (!mask) return null;
return (
<div className="offline-mask">
<h2 className="offline-mask-title">{offlineTitle}</h2>

<p className="offline-mask-desc">{desc}</p >
</div>
);
};

return <>{renderOfflineMask()}</>;
};

export default OfflineHandle;

发现



  • offline和online事件:window有效,document和document.body设置无效


手上的项目只运行在Chrome浏览器,只有为window设置offline和online才生效。
运行环境:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36



  • 为position增加2s的transition的避免屏闪


链接:https://juejin.cn/post/6953868764362309639
收起阅读 »

音视频学习从零到整-关于视频的一些概念

内容1、视频文件格式2、视频封装格式3、视频编解码方式4、音频编解码方式5、颜色模型一.视频相关概念1.1 视频文件格式文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于...
继续阅读 »

内容

1、视频文件格式
2、视频封装格式
3、视频编解码方式
4、音频编解码方式
5、颜色模型

一.视频相关概念

1.1 视频文件格式

文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于视频来说,
我们常见的文件格式则有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。文件格式通常表现为文件在操作系统上存储时的后缀名,它通常会被操作系统用来与相应的打开程序关联,比如你双击一个 test.doc 文件,系统会调用 Word 去打开它。你双击一个 test.avi 或者 test.mkv 系统会调用视频播放器去打开它。

同样是视频,为什么会有 .mov、.avi、.mpg 等等这么多种文件格式呢?****那是因为它们通过不同的方式实现了视频这件事情,至于这个不同在哪里,那就需要了解一下接下来要说的「视频封装格式」这个概念了。

1.2 视频封装格式

视频封装格式,简称视频格式,相当于一种储存视频信息的容器,它里面包含了封装视频文件所需要的视频信息、音频信息和相关的配置信息(比如:视频和音频的关联信息、如何解码等等)。一种视频封装格式的直接反映就是对应着相应的视频文件格式。


下面我们就列举一些文件封装格式:

1、AVI 格式,对应的文件格式为 .avi,英文全称 Audio Video Interleaved,是由 Microsoft 公司于 1992 年推出。这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。

2、DV-AVI 格式,对应的文件格式为 .avi,英文全称 Digital Video Format,是由索尼、松下、JVC 等多家厂商联合提出的一种家用数字视频格式。常见的数码摄像机就是使用这种格式记录视频数据的。它可以通过电脑的 IEEE 1394 端口传输视频数据到电脑,也可以将电脑中编辑好的的视频数据回录到数码摄像机中。

3、WMV 格式,对应的文件格式是 .wmv、.asf,英文全称 Windows Media Video,是微软推出的一种采用独立编码方式并且可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。

4、MPEG 格式,对应的文件格式有 .mpg、.mpeg、.mpe、.dat、.vob、.asf、.3gp、.mp4 等等,英文全称 Moving Picture Experts Group,是由运动图像专家组制定的视频格式,该专家组于 1988 年组建,专门负责视频和音频标准制定,其成员都是视频、音频以及系统领域的技术专家。MPEG 格式目前有三个压缩标准,分别是 MPEG-1、MPEG-2、和 MPEG-4。MPEG-4 是现在用的比较多的视频封装格式,它为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。

5、Matroska 格式,对应的文件格式是 .mkv,Matroska 是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。

6、Real Video 格式,对应的文件格式是 .rm、.rmvb,是 Real Networks 公司所制定的音频视频压缩规范称为 Real Media。用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。

7、QuickTime File Format 格式,对应的文件格式是 .mov,是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。

8、Flash Video 格式,对应的文件格式是 .flv,是由 Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。

从上面的介绍中,我们大概对视频文件格式以及对应的视频封装方式有了一个概念,接下来则需要了解一下关于视频更本质的东西,那就是视频编解码。

1.3 容器(视频封装格式)
封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.

通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.

常见的视频容器格式:
1、AVI: 是当时为对抗quicktime格式(mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件
2、MOV:是Quicktime封装
3、WMV:微软推出的,作为市场竞争
4、mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕
5、flv: 这种封装方式可以很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式
6、MP4:主要应用于mpeg4的封装,主要在手机上使用。

2.1视频编解码方式
视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.

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

2.2 常见视频编码方式:

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

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

MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。

介绍了上面这些「视频编解码方式」后,我们来说说它和上一节讲的「视频封装格式」的关系。

可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,

但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

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

AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。
MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。
WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

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

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

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

LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)
HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)
优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码

适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

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

图像


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

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

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

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




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

网络提取层单元(NALU, Network Abstraction Layer Unit),

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

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

4.2 颜色模型
我们开发场景中使用最多的应该是 RGB 模型


在 RGB 模型中每种颜色需要 3 个数字,分别表示 R、G、B,比如 (255, 0, 0) 表示红色,通常一个数字占用 1 字节,那么表示一种颜色需要 24 bits。那么有没有更高效的颜色模型能够用更少的 bit 来表示颜色呢?

现在我们假设我们定义一个「亮度(Luminance)」的概念来表示颜色的亮度,那它就可以用含 R、G、B 的表达式表示为:

Y = kr*R + kg*G + kb*B

Y 即「亮度」,kr、kg、kb 即 R、G、B 的权重值。

这时,我们可以定义一个「色度(Chrominance)」的概念来表示颜色的差异:

Cr = R – Y
Cg = G – Y
Cb = B – Y

Cr、Cg、Cb 分别表示在 R、G、B 上的色度分量。上述模型就是 YCbCr 颜色模型基本原理。

YCbCr 是属于 YUV 家族的一员,是在计算机系统中应用最为广泛的颜色模型,就比如在本文所讲的视频领域。在 YUV 中 Y 表示的是「亮度」,也就是灰阶值,U 和 V 则是表示「色度」。

YUV 的关键是在于它的亮度信号 Y 和色度信号 U、V 是分离的。那就是说即使只有 Y 信号分量而没有 U、V 分量,我们仍然可以表示出图像,只不过图像是黑白灰度图像。在YCbCr 中 Y 是指亮度分量,Cb 指蓝色色度分量,而 Cr 指红色色度分量。

现在我们从 ITU-R BT.601-7 标准中拿到推荐的相关系数,就可以得到 YCbCr 与 RGB 相互转换的公式

Y = 0.299R + 0.587G + 0.114B
Cb = 0.564(B - Y)
Cr = 0.713(R - Y)
R = Y + 1.402Cr
G = Y - 0.344Cb - 0.714Cr
B = Y + 1.772Cb

这样对于 YCbCr 这个颜色模型我们就有个初步认识了,但是我们会发现,这里 YCbCr 也仍然用了 3 个数字来表示颜色啊,有节省 bit 吗?为了回答这个问题,我们来结合视频中的图像和图像中的像素表示来说明

假设图片有如下像素组成


一副图片就是一个像素阵列.每个像素的 3 个分量的信息是完整的,YCbCr 4:4:4


下图中,对于每个像素点都保留「亮度」值,但是省略每行中偶素位像素点的「色度」值,从而节省了 bit。YCbCr4:2:2


上图,做了更多的省略,但是对图片质量的影响却不会太大.YCbCr4:2:0


转自:https://www.jianshu.com/p/15f28fe89329

收起阅读 »

RunLoop(一):源码与逻辑

简述什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。...
继续阅读 »

简述

什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。

那RunLoop是怎样保持程序的运行状态,到底处理了哪些事件?下面我们就从源码的层面来了解一下RunLoop。

RunLoop

获取runloop对象

NSRunLoop和CFRunLoopRef都代表RunLoop对象,NSRunLoop是对CFRunLoopRef的封装。

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

RunLoop相关类

从源码的代码结构中我们可以找出来一下5个跟RunLoop相关的结构

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef
CFRunLoopTimerRef

下面是CFRunLoopRef的结构代码

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

变量很多,我们不需要全部看,只需要注意这两个

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

每一个runloop里面有很多mode(存在一个set集合里面),然后之后后一个mode叫做currentMode,也就是说runloop一次只能处理一种mode。

然后我们再看CFRunLoopModeRef的结构,我已经给大家省略了里面那些我们不需要关注的变量

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};

根据上面这些我们大概的可以概括出来RunLoop这些相关类的关系。


CFRunLoopModeRef
由上面的源码我们可以稍微总结一下这个CFRunLoopModeRef:

1、CFRunLoopModeRef代表RunLoop的运行模式
2、一个RunLoop包含多个CFRunLoopModeRef,每个CFRunLoopModeRef又包含多个_sources0,_sources1,_observers,_timers。
3、RunLoop每次只能运行一种mode,切换mode的时候,要先退出之前的mode。
4、如果mode中没有_sources0、_sources1、_observers、_timers,程序会立刻退出。
常用的两种Mode

kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行

UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

CFRunLoopObserverRef
源码中给出了可以监听的RunLoop状态

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 进入RunLoop
kCFRunLoopEntry = (1UL << 0),
// 即将处理timers
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理Sources
kCFRunLoopBeforeSources = (1UL << 2),
// 即将休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出循环
kCFRunLoopExit = (1UL << 7),
// 所有状态
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

具体的怎么样添加observer来监听RunLoop状态我就不贴代码了,网上一搜有很多的。

RunLoop的运行逻辑

前面我们已经了解了RunLoop相关的结构的源码,知道了RunLoop大概的数据结构,那RunLoop到底是如何工作的呢?它的运行逻辑是什么?

我们了解过了每个mode中会存放不同的_sources0、_sources1、_observers、_timers,这些我们可以全部统称是RunLoop要处理的东西,那每一种具体对应我们了解的哪写事件呢?

Source0
触摸事件处理
performSelector:onThread:

Source1
基于系统Port(端口)的线程间通信
系统事件捕捉

Timers
NSTimer定时器
performSelector:withObject:afterDelay:

Observers
用于监听RunLoop的状态
UI刷新(BeforeWating)
Autorelease Pool (BeforWaiting)

注: UI的刷新并不是即时生效,比如说我们改变了view的backgroundColor,当执行到这行代码是并不是立刻生效,而是先记录下有这么一个任务,然后在RunLoop处理完所有的时间,进入休眠之前UI刷新。


这是大神总结的RunLoop的运行逻辑图,我直接拿过来用了。我们主要是看左边这部分,右边的这些标注是在源码中对应的主要方法名称。

这个图很容易理解,只有从06跳转到08这一步,单从图上看的话不是很清晰,这一块结合源码就比较明了了。第06步,如果存在Source1就直接跳转到08,在代码中使用了goto这个关键字,其实就是跳过了runloop休眠和唤醒这一部分的代码,直接跳转到了处理各种事件的这一部分。

下面我把源码做了一些删减,方便大家可以更清楚的梳理整个过程

// 这个是runloop入口函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */

// 通知Observers 即将进入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心方法
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers 即将退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return result;
}

下面是核心方法

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

int32_t retVal = 0;
do {

//通知Observers 即将处理Timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

//通知Observers 即将处理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

//处理Blocks
__CFRunLoopDoBlocks(rl, rlm);

//处理source0,根据返回值决定在处理一次blocks
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);


// source1相关
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
// 是否有Source1 有的话跳转到handle_msg
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}

didDispatchPortLastTime = false;

// 通知Observers: 即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//休眠
__CFRunLoopSetSleeping(rl);


//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);


__CFRunLoopUnsetSleeping(rl);

// 通知Observers: 即将醒来
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

// 标识标识 !!!!!
handle_msg:;

__CFRunLoopSetIgnoreWakeUps(rl);

//下面根据是什么唤醒的runloop来分别处理

if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}

// 被Timer唤醒
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被Timer唤醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被GCD唤醒
else if (livePort == dispatchPort) {

// 处理GCD相关
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

} else {
//被Source1唤醒
//处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;

}


//在处理一遍BLocks
__CFRunLoopDoBlocks(rl, rlm);


// 设置返回值 决定是否继续循环
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}

} while (0 == retVal);

return retVal;
}

图和源码结合来看,整个流程就清晰了很多。流程里面的有些东西不需要我们太过深入的研究,我们把这个流程掌握一下就OK了。

细节补充

第一点

我们都知道RunLoop有一个优势,那就是可以使线程在有工作的时候工作,没有工作的时候休眠,来减少占用CPU资源,提高程序性能。

这说明代码在执行到

//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

的时候,会阻塞当前的线程。但这种阻塞跟我们之前所用到过的阻塞线程不是一回事。

举个例子,我们可以使用while(1){};这句代码来阻塞线程,这句代码在底层会转换为汇编的代码,我们的线程一直在重读执行这几句代码,所以他仅仅是阻塞线程,并没有使线程休眠,我们的线程一直在工作。但是runloop,通过mach_msg使用了一些内核层的API,真的是实现了线程的休眠,让线程不再占用CPU资源。

第二点

RunLoop与线程的关系?

一个线程对应一个RunLoop对象。
RunLoop默认不创建,在第一次获取的时候创建,主线程中的默认存在RunLoop也是因为在底层代码中,提前获取过一次。
RunLoop储存在一个全局的字典中,线程是key,RunLoop是value。(源码中有所体现)
RunLoop会在线程结束时销毁。

链接:https://www.jianshu.com/p/705aa44405c0

收起阅读 »

微信小程序自定义实现toast进度百分比动画组件

目录结构wxml {{number}} {{ content }} 搭建组件结构jsComponent({ options: { multipleSlots: true // 在组件定义时的选项中...
继续阅读 »

目录结构


wxml



{{number}}



{{ content }}


搭建组件结构

js

Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
* 私有数据,组件的初始数据
* 可用于模版渲染
*/
data: { // 弹窗显示控制
animationData: {},
content: '提示内容',
number: 0,
level_box:-999,
},
/**
* 组件的方法列表
*/
methods: {
/**
* 显示toast,定义动画
*/
numberChange() {
let _this = this
for (let i = 0; i < 101; i++) {
(function () {
setTimeout(() => {
_this.setData({
number: i + '%'
})
}, 100 * i)
})()
}
},
showToast(val) {
this.setData({
level_box:999
})
this.numberChange()
var animation = wx.createAnimation({
duration: 300,
timingFunction: 'ease',
})
this.animation = animation
animation.opacity(1).step()
this.setData({
animationData: animation.export(),
content: val
})
/**
* 延时消失
*/
setTimeout(function () {
animation.opacity(0).step()
this.setData({
animationData: animation.export()
})
}.bind(this), 10000)
}
}
})

json

```javascript
{
"component": true,
"usingComponents": {}
}

wxss

.wx-toast-box {
display: flex;
width: 100%;
justify-content: center;
position: fixed;
top: 400rpx;
opacity: 0;
}

.wx-toast-content {
max-width: 80%;
border-radius: 30rpx;
padding: 30rpx;
background: rgba(0, 0, 0, 0.6);
}

.wx-toast-toast {
height: 100%;
width: 100%;
color: #fff;
font-size: 28rpx;
text-align: center;
}

.progress {
display: flex;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
text-align: CENTER;
color: #07c160;
}

.img_box {
display: flex;
justify-content: center;
margin: 20rpx 0;
}

@keyframes rotate {
from {
transform: rotate(360deg)
}

to {
transform: rotate(0deg)
}
}

.circle {
animation: 3s linear 0s normal none infinite rotate;
}

@keyframes translateBox {
0% {
transform: translateX(0px)
}

50% {
transform: translateX(10px)
}
100% {
transform: translateX(0px)
}
}

.anima_position {
animation: 3s linear 0s normal none infinite translateBox;
}

效果截图



原文:https://juejin.cn/post/6968731176492072968



收起阅读 »

让我们一起实现微信小程序国际化吧

常见的国际化方式官方方案官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面强依赖目录结构由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图特别好笑的一点官方示例里居然不是这个目录结构,不过依然是...
继续阅读 »

常见的国际化方式

官方方案

官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面

强依赖目录结构

由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图


特别好笑的一点官方示例里居然不是这个目录结构,不过依然是强依赖目录结构,因为gulp中路径是写死的

文档简陋

通过官方文档快速入门居然无法搭建起项目,暂时只用这种方式搭建起来了 官方github demo,通过对比发现好多必要代码都没有在文档中说明。


比如需要在app.js中开始便需要执行getI18nInstance(),否则全局都无法正常国际化。这么重要的信息居然在快速入门中没有说明


调试麻烦

每次修改代码都要重新执行npm run build,注意是每次


由于国际化必须通过npm run build来实现,而每次npm run build过后dist文件就会被覆盖,所以每次只能修改src,而小程序预览的却是dist文件,这也就导致必须频繁的执行build命令。下面演示一下增加一个表头的操作步骤

2021-05-22 07-57-21.2021-05-22 08_03_21.gif

说下优点

代码简洁。解释一下,从上图可以看到,他的书写方式和其他主流框架的国际化书写方式很类似(vue,react)。view层都是类似t(key,参数),对js侵入也很小,下面是上图页面对应的js部分。

import { I18nPage } from '@miniprogram-i18n/core'

I18nPage({
onLoad() {
this.onLocaleChange((locale) => {
console.log('current locale:', this.getLocale(), locale)
})

this.setLocale('zh-CN')
},

toggleLocale() {
this.setLocale(
this.getLocale() === 'zh-CN' ? 'en-US' : 'zh-CN'
)
},

nativate() {
wx.navigateTo({
url: '/pages/logs/logs'
})
}
})

可以说除了I18nPage以外没有别的侵入,剩下那些代码都是用于切换语言所需,如果只是最简单国际化,只需要I18nPage({})即可

聊一下为什每次都需build

其实咱们看下dist/i18n/locales.wxs文件即可

var fallbackLocale = "zh-CN";
var translations = {
"en-US": {
test: ["test messages"],
test2: ["test message 2, ", ["label"], ", ", ["label2"]],
nested: ["nested message: ", ["test"]],
toggle: ["Toggle locale"],
navigate: ["Navigate to Log"],
"window.title": ["I18n test"],
"index.test": ["Test fallback"],
navigate2: ["Navigation 2nd"],
},
"zh-CN": {
test: ["测试消息"],
test2: ["测试消息二, ", ["label"], ", ", ["label2"]],
nested: ["嵌套消息: ", ["test"]],
toggle: ["切换语言"],
navigate: ["跳转"],
"window.title": ["国际化测试"],
"index.test": ["备选"],
navigate2: ["导航2"],
},
};
var Interpreter = (function (r) {
var i = "";
function f(r, n) {
return r
? "string" == typeof r
? r
: r
.reduce(function (r, t) {
return r.concat([
(function (n, e) {
if (((e = e || {}), "string" == typeof n)) return n;
if (n[2] && "object" == typeof n[2]) {
var r = Object.keys(n[2]).reduce(function (r, t) {
return (r[t] = f(n[2][t], e)), r;
}, {}),
t = r[e[0]],
u = e[n[0]];
return void 0 !== u
? r[u.toString()] || r.other || i
: t || r.other || i;
}
if ("object" == typeof n && 0 < n.length) {
return (function r(t, n, e) {
void 0 === e && (e = 0);
if (!n || !t || t.length <= 0) return "";
var n = n[t[e]];
if ("string" == typeof n) return n;
if ("number" == typeof n) return n.toString();
if (!n) return "{" + t.join(".") + "}";
return r(t, n, ++e);
})(n[0].split("."), e, 0);
}
return "";
})(t, n),
]);
}, [])
.join("")
: i;
}
function c(r, t, n) {
t = r[t];
if (!t) return n;
t = t[n];
return t || n;
}
return (
(r.getMessageInterpreter = function (i, o) {
function e(r, t, n) {
var e, u;
return f(
((e = r),
(u = o),
((n = (r = i)[(n = n)]) && (n = n[e])) || c(r, u, e)),
t
);
}
return function (r, t, n) {
return 2 === arguments.length
? e(r, null, t)
: 3 !== arguments.length
? ""
: e(r, t, n);
};
}),
r
);
})({});

module.exports.t = Interpreter.getMessageInterpreter(
translations,
fallbackLocale
);
其实搞这么麻烦构建方式主要是为了生成这个wxs文件,我们之所以能在vue中看到{{t('key')}}的方式进行国际化,是因为wxml层本身支持函数调用且函数可以调用外部资源(国际化文件),而小程序只允许通过wxs(官方文档)的方式在页面使用函数,出于性能考虑又不允许wxs引用任何外部资源(除了其他的wxs),所以这个build最核心的诉求是把 国际化文件copy一份到wxs文件中。下面是wxs无法引用外部资源的官方说明

优化

我们国际化的核心诉求就是解决官方国际化问题同时保留他的优点,这里我们列下此次的目标

  •  路径灵活,不强依赖路径减少后期添加国际化的路径改动成本
  •  调试方便,和原始开发调试方式相同
  •  书写简洁,保持和vue一样的书写方式
2021-05-22 10-27-02.2021-05-22 10_28_39.gif

wxml代码

<wxs src="../wxs/i18n.wxs" module="i18n" />
<!-- 标题国际化 -->
<page-meta>
<navigation-bar title="{{i18n.t(locales['主页'])}}" />
</page-meta>
<!-- 一般国际化 -->
<view>{{i18n.t(locales['通往爱人家里的路总不会漫长。'])}}</view>
<!-- js国际化 -->
<view>{{jsMsg}}</view>
<!-- 支持变量国际化 -->
<view>{{i18n.t(locales['当前页面:{path}'],[{key:'path',value:'home-page/home-page'}])}}</view>
<!-- 切换中英文按钮 -->
<button bindtap="zhClick" type="default">{{i18n.t(locales['切换中文'])}}</button>
<button bindtap="enClick" type="warn">{{i18n.t(locales['切换英语'])}}</button>

js代码

const i18n = require('../behaviors/i18n');
// home-page/home-page.js
Component({
behaviors: [i18n],
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
zhClick() {
this.switchLanguage('zh_CN')
},
enClick() {
this.switchLanguage('en_US')
},
}
})

基本维持和官方相同的写法,尽可能少的代码写入

代码段链接

解决思路

利用behaviors(官方文档)将国际化文案进行引入每个页面。然后将所有国际化数据和key值以参数的形式传递给wxs函数,这样就可以避开wxs外部资源限制实现和vue i18n相同的效果。



  • behaviors负责将国际化方法和文案导入全局,以下是behaviors/i18n.js源码:

// behaviors/i18n.js

const {
t
} = require('../utils/index')
const i18n = Behavior({
data: {
language:{}, // 当前语种
locales: {}, // 当前语言的全部国际化信息
},
pageLifetimes: {
// 每次页面打开拉取对应语言国际化数据
show() {
if (this.data.language === 'en_US') {
this.setData({
locales: require('../i18n/en_US')
})
} else {
this.setData({
locales: require('../i18n/zh_CN')
})
}
}
},
methods: {
// 全局js国际化便捷调用
$t(key, option) {
return t(key, option)
},
// 由于tab只能通过js修改,所以每次语言切换需要重新更新tab国际化内容
refreshTab() {
wx.setTabBarItem({
index: 0,
text: this.data.locales['主页']
})
wx.setTabBarItem({
index: 1,
text: this.data.locales['我的']
})
},
// 切换语种
switchLanguage(language) {
this.setData({
language
})
if (language === 'zh_CN') {
this.setData({
locales: require('../i18n/zh_CN')
})
} else {
this.setData({
locales: require('../i18n/en_US')
})
}
// 切换下方tab
this.refreshTab()
},
}
})

module.exports = i18n
wxs负责为wxml层提供国际化方法,此处逻辑比较简单,先找到国际化文件中key值对应的语句,然后根据第二参数(arr)将变量进行替换,此处替换逻辑比较粗暴,使用{key}方式代表变量。如:"我的年龄是{age}",age代表变量,参数传递格式为 [{key:'xxx',value:'xxxx'}]

// 国际化.js
{
"ageText":"my age is {age}",
}
// wxml
<view>{{i18n.t(locales['ageText'],[{key:'age',value:'18'}])}}</view>

wxs源码如下:

var i18n = {
t: function (str, arr) {
var result = str;
if (arr) {
arr.forEach(function (item) {
if(result){
result = result.replace('{'+item.key+'}', item.value)
}
})
}
return result
}
}
module.exports = i18n

同时提供一个在js里获取国际化的util方法

// 国际化
const t = (key, option = {}) => {
const language = wx.getStorageSync('language');
let locales = null
if (language === 'en_US') {
locales = require('../i18n/en_US')
} else {
locales = require('../i18n/zh_CN')
}
let result = locales[key]
for (let optionKey in option) {
result = result.replace(`{${optionKey}}`, option[optionKey])
}
return result
}

module.exports = {
t
}

这样就基本实现了同时在wxml,js中进行国际化的基本需求,同时也解决了官方调试体验不足的缺点。

不足




  • 其实调试体验还不是那么完美,由于只有在show中初始化国际化文件内容,所以当开启“热重载”时对国际化文件进行修改,国际化内容不会自动进行更新




  • 每个page的js文件都需要引入behaviors/i18n.js,wxml文件引入<wxs src="../wxs/i18n.wxs" module="i18n" />有些略显繁琐




  • require('../i18n/xxx')整个项目引入了3遍略显繁琐,可以封装一下




  • i18n.t(locales['key']) 其中locals每次都要写一遍比较繁琐,不过由于wxs的限制也想不到太好的方法

使用建议


由于只是写一个demo很多逻辑并没有写完整,所以如果使用的话需要根据项目进行修改




  1. 如果i18n路径和命名方式不同需要同时修改behaviors/i18n.js,以及utils/index.js中的路径




  2. 现在每次刷新页面国际化都会被重置成中文,建议在behaviors/i18n.js的show方法中从全局获取当前的国际化语言。这样就不会每次都被重置成中文了。


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


收起阅读 »

要不要打造一个轻量的小程序引擎玩玩?

我们的小程序框架的底层,我把它分为四个部分,主要是多线程模型runtime 框架js 沙箱其他我们一个一个来多线程模型和线程通信多线程模型多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外 它们其实只是线...
继续阅读 »


我们的小程序框架的底层,我把它分为四个部分,主要是

  • 多线程模型
  • runtime 框架
  • js 沙箱
  • 其他

我们一个一个来

多线程模型和线程通信


多线程模型


多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外


它们其实只是线程主体的不同,比如 RN 主要是 shadow tree 和 jscore,而 flutter 则是 skia 和 dart engine,小程序则是 webview 充当渲染层,js engine(或 worker)充当逻辑层


尽管本质一样,但因为业务场景的不同,小程序的诉求却和 RN/flutter 完全不同


在 RN 中,app 作为一个主体,我们更乐意分配更多资源,以至于 RN 一直在跑 react 这种 runtime 浪费的框架,在过去,这被认为是值得的


但是小程序与之相反,它作为 app 的附属品,我们不乐意给小程序分配更多资源,不乐意分配内存,不乐意分配更多线程,所以我们这次分享的前提是

基于最小分配的前提,探讨小程序的每个环节

请记住前提,然后我们接着往下看


线程通信


说到多线程,我们首先想到的就是多线程的调度和通信,我们先讲通信,通常来说,多线程的非 UI 线程都是没有 dom 环境的,无论是 js 引擎还是 worker


所以为了能跑一个前端框架,我们不得另寻出路,主要方案有三种,其中幻灯片的第二种,写一个 dom 无关的 diff 算法,这是写不出来什么好算法的,所以我们主要看剩下两种思路



幻灯片中,左边的代码是 [ 使用 Proxy 劫持 dom ],右边的是 [ 模拟 dom API ]


这两种思路其实是类似的,模拟 dom API 是最为常见的,比如 react-reconciler,vue3 的 renderer,都是用的这个思路,就是它把用到的 dom API 暴露出来,你在不同的端去模拟 dom 的行为


甚至还有 taro-next,kbone 这种框架,他们模拟了一整个 dom/bom 层


这个思路好处是粗暴方便好用,坏处就是抽象程度低,比如 taro-next 中就用了千行代码做这件事,属于 case by case,没啥逼格


所以我提出了 Proxy 劫持 dom 的思路,其实这个思路在微前端中比较常用,只不过我们现在用 Proxy 不再是劫持一两个 dom 操作了,而是将所有 dom 操作通通记录下来,然后批量发送给 UI 线程


这个实现抽象程度非常高,我使用了不到 200 行代码就可以劫持所有 dom 操作


代码在这里:github.com/yisar/fard/…


除了线程通信,更重要的是线程的调度,因为很重要,我们放到最后说


前端框架


还记得小程序架构的前提吗?没错,就是最小资源分配


因为我们不想给小程序分配过多的资源,所以像 react、vue 这种 runtime 特别重的框架,其实是不适合用作小程序的


甚至 fre 也不适合,因为大家可能对“轻量”这个词有误解,不是代码越少就越轻量,代码量是很重要的一个方面,但是更重要的是代码的内存占用,以及算法的算力和复杂度


fre 虽然代码量少,但它的算法和 vue 是一样的,算力相同,内存占用也不少


所以我们不得不将目光转向 svelte 这类框架,幻灯片可以看到,svelte 通过编译,直接生成原生 dom 操作,没有多余的算法和 vdom


实际上,我们在做性能优化的时候,讲究一个“换”字,react 这种框架,通过浪费 runtime 去做算法,“换”一个最小结果,而 svelte 则是通过编译(浪费用户电脑),去换 runtime



JS 沙箱



然后我们来讲讲沙箱,也就是 js 引擎和 worker,这部分适合语言爱好者

选型


通常来说,一提到 js 引擎,大家都是 v8 v8 v8

但是实际上,v8 是一个高度优化的 JIT 引擎,它跑 js 确实是足够快的,但对于 UI 来说,我们更多要的不是跑得快


实际上,AOT 的语言或引擎更适合 UI 框架,比如 RN 的 hermes,dart 也支持 AOT,它可以编译成字节码,只需要一次构建即可,当然,AOT 也有缺点,就是 热更新 比较难做


另外除了 js 引擎,worker 也是一个非常赞的选择,方便好用,而且还提供了 bom 接口,比如 offscreen canvas,fetch,indexdb,requestAnimationFrame……

总结



哈哈哈总结,我们基于最小分配的前提去设计这个架构,每个环节都选择节省资源的方案


事实上写代码就是这样的,比如我写 fre,那么我追求 1kb,0 依赖,我写业务框架,我追求 0 配置,1mb 的 node_modules 总大小


我写小程序,我追求最小资源分配,不管做啥,有痛点然后有追求然后才有设计


其他

其实小程序还有很多东西可以做,比如现在的小程序都需要兼容微信小程序,也就是类似 wxml,wxss,wxs这些非标准的文件,还要得是个多 Page 的 mpa


比如 ide,我们可以使用 nobundle 的思路来加快构建速度


当然,为了服务业务,在我们公司我没有使用 nobundle


比如剧透一下,我在公司中为了兼容微信小程序,开的新坑


原理是将微信的文件(wxml,wxss,wxs)先编译成可识别的文件(jsx,css,js),然后使用 babel、postCss 去转移,形成一个个 umd 的子应用


然后通过 berial(微前端框架)的路由,沙箱,生命周期,将它们跑在 h5 端,这样就可以在浏览器中模拟和调试啦



最后我们通过三张图和一个问题,来补充和结束一下这次分享


第一张图是微信小程序的后台桌面,有没有感觉和操作系统有点像,但其实不是的,操作系统的软件是进程的关系,只能切换,不能共存,而小程序是多进程,这些小程序可以在后台留驻,随时保持唤醒


第二张图是钉钉的仪表盘,这也是小程序最常用的场景,就是和这种一堆子应用的 app


第三张图是 vscode 的插件系统,是的,想不到吧,这玩意也是小程序架构,而且也是同样的思想,我不让你操作 dom

然后最后的问题:canvas 怎么办?



这个问题实际上非常难搞,如果我们使用 worker 作为 js 沙箱还好,有 offscreen canvas 和 requestAnimationFrame


如果我们使用 js 引擎怎么办呢,走上面提到的线程通信成本就太高了,动画走这个通信,等接收到消息,动画已经卡死了


所以还有什么办法,这里不得不再次提多线程的特性,也就是多线程的内存是共享的,我们可以 js 引擎中,将 canvas 整个元素放到堆栈里,然后 UI 线程和 js 线程共享这一块内存


这样就不需要走线程通信了,适合 canvas,动画这种场景



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


收起阅读 »

微信小程序-自定义日期组件实现

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。微信小程序原生有提供一套日期组...
继续阅读 »

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?
思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。

微信小程序原生有提供一套日期组件, 大概如下:



跟UI预期不一致的点有如下几个:

A. 期望弹窗居中显示、而不是从底部弹出;

B. 期望小于10的展示为1月,1日这种, 而不是01月, 01日;

C. UI样式跟微信原生差别有点大;

D. 不需要头部的取消&确定按钮、预期底部整个确定按钮即可;

想着让产品接受原生日期组件的, But拗不过产品的思维, 只能开干、自己撸一个自定义日期组件, 造轮子=.= 

既然原生的不能用, 那么我们看看小程序是否有提供这种类似的滚动器, 查看官方文档发现: 



那就开干, 为尽可能保持代码的最小颗粒度(这里不考虑弹窗外壳的封装、纯日期组件).
话不多说、这里直接贴上代码、预留的坑位都会在代码内有备注, 请参考:

// 组件wxml
<!-- 预留坑位: 按道理该日期组件应该是做在弹窗上的、这里为了简化代码故直接写在了页面上;
后期使用者烦请自己做个弹窗套上、用showModal属性控制其显示隐藏-->
<view class="picker" wx:if="{{showModal}}">
<picker-view indicator-class="picker-indicator" value="{{pickerIndexList}}" bindchange="bindChangeDate">
<picker-view-column>
<view wx:for="{{yearList}}" wx:key="index" class="{{pickerIndexList[0]==index?'txt-active':''}}">{{item}}年</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{monthList}}" wx:key="index" class="{{pickerIndexList[1]==index?'txt-active':''}}">{{item}}月</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{dayList}}" wx:key="index" class="{{pickerIndexList[2]==index?'txt-active':''}}">{{item}}日</view>
</picker-view-column>
</picker-view>
<!-- 预留坑位: 日期组件可能仅允许数据回选、不允许修改。
思路: 通过自定义蒙层盖在日期控件上从而达到禁止控件滚动的效果.
-->
<view wx:if="{{!canEdit}}" class="disabled-picker"></view>
</view>

// 组件wxss
.picker{
position: relative;
height: 300rpx;
width: 600rpx;
margin: 0 auto;
border: 1rpx solid red;
}
.picker picker-view {
width: 100%;
height: 100%;
}
.picker-indicator {
height: 60rpx;
line-height: 60rpx;
}
.picker picker-view-column view {
font-size: 40rpx;
line-height: 60rpx;
text-align: center;
}
.txt-active {
color: #2c2c2c;
}
/* 预留坑位: 为便于区分真的有遮罩层盖住、特意加了个背景色、实际使用过程可改成透明色 */
.disabled-picker{
width: 600rpx;
position: absolute;
top: 0;
left: 0;
height: 300rpx;
z-index: 999;
background: rgb(255,222,173,0.7);
}


// 组件js
Component({
properties: {},
data: {
yearList: [],
monthList: [],
dayList: [],
pickerIndexList: [0, 0, 0]
},
methods: {
// dateString格式: 'YYYY-MM-DD'
initPicker (dateString) {
let nowDate = new Date()
// 预留个坑位: 若需要指定某一日期则从外面传入、否则默认当天
if(dateString){
// 预留个坑位: 判定传入的数据类型是否符合要求、若不符合该报错的报错
nowDate = new Date(dateString)
}

// 预留个坑位: 因为下面的日期指定在1900.01.01-2100.12.31、故这里最好校验下传入日期是否在区间内.
let nowYear = nowDate.getFullYear()
let nowMonth = nowDate.getMonth() + 1
let yearList = this.getYearList(nowYear)
let monthList = this.getMonthList()
let dayList = this.getDayList(nowYear, nowMonth)

// 获取多列选择器的选中值下标
let pickerIndexList = []
pickerIndexList[0] = yearList.findIndex(o => o === nowDate.getFullYear())
pickerIndexList[1] = monthList.findIndex(o => o === nowDate.getMonth()+1)
pickerIndexList[2] = dayList.findIndex(o => o === nowDate.getDate())
this.setData({
yearList,
monthList,
dayList,
pickerIndexList,
showModal: true
})
},
// 获取年份
getYearList (nowYear) {
let yearList = []
if(nowYear < 1900 || nowYear > 2100){
return false
}
for (let i = 1900; i <= 2100; i++) {
yearList.push(i)
}
return yearList
},
// 获取月份
getMonthList () {
let monthList = []
for (let i = 1; i <= 12; i++) {
monthList.push(i)
}
return monthList
},
// 获取日期 -> 根据年份&月份
getDayList (year, month) {
let dayList = []
month = parseInt(month, 10)
// 特别注意: 这里要根据年份&&月份去计算当月有多少天[切记切记]
let temp = new Date(year, month, 0)
let days = temp.getDate()
for (let i = 1; i <= days; i++) {
dayList.push(i)
}
return dayList
},
// 日期选择改变事件
bindChangeDate (e) {
let pickerColumnList = e.detail.value
const { yearList=[], monthList=[] } = this.data
const nowYear = yearList[pickerColumnList[0]]
const nowMonth = monthList[pickerColumnList[1]]
this.setData({
dayList: this.getDayList(nowYear, nowMonth),
pickerIndexList: pickerColumnList
})
},
show (birthday) {
// 预留坑位: 这里也许会有一定的逻辑判定是否允许编辑日期控件, 故预留canEdit属性去控制
this.setData({
canEdit: true
})
this.initPicker(birthday)
},
// 预留坑位、点击确定按钮获取到选中的日期
surePicker () {
const { pickerIndexList, yearList, monthList, dayList } = this.data
// 预留坑位: 月份&日期补0
let txtDate = `${yearList[pickerIndexList[0]]}-${monthList[pickerIndexList[1]]}-${dayList[pickerIndexList[2]]}`
console.log(txtDate)
},
}
})

接下来我们看看使用方是怎么使用的?

// 页面wxml
<!-- 预留坑位: 这里仅展示触发事件、开发者替换成实际业务即可-->
<view bind:tap="openPicker" style="margin:20rpx; text-align:center;">打开日期控件</view>

// 页面json: 记得在使用页面的json处引入该组件、配置组件路径

// 页面js
methods: {
openPicker (){
// 获取组件实例、这里可选择是否传入日期
this.date_picker = this.selectComponent && this.selectComponent('#date_picker')
this.date_picker && this.date_picker.show()
},
}

一切准备就绪、我们看看效果图!
这是日期可编辑时、你是可滚动选择器的:


我们看看日期不可编辑时、仅可查看的效果图:



样式是稍微有点丑、到时开发者按照实际UI去做微调即可、这不难的=.=.

这里预留了几个扩展点:

1.支持外部传入日期、默认选中预设值;

2.支持在弹窗内显示日期控件、需要使用者自行开发弹窗;

3.支持日期控件仅可查看、不可编辑;

4.支持日期控件的关闭、一般是弹窗上有个关闭按钮或者是点击弹窗的蒙层可关闭、使用者自行开发;

Tips: 具体的代码改动点都有在上面的code中有备注、欢迎对号改代码, 若有任何不懂的欢迎留言或者私信、很愿意帮您解答。


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

收起阅读 »

Onboard,迷人的引导页样式制作库

简介Onboard主要用于引导页制作,源码写的相当规范,值得参考.项目主页: https://github.com/mamaral/Onboard实例下载: https://github.com/mamaral/Onboard/archiv...
继续阅读 »

简介




Onboard主要用于引导页制作,源码写的相当规范,值得参考.

项目主页: https://github.com/mamaral/Onboard
实例下载: https://github.com/mamaral/Onboard/archive/master.zip

样式

设置背景图片或者背景movie,然后在它们之上生成数个ViewController,默认是顶部一张图片,下面是标题和详细介绍,最下面是按钮和page

导入

pod 'Onboard'

使用

导入头文件#import “OnboardingViewController.h”

图片为背景

蒙板控制器生成方法

1、title是标题
2、body是介绍
3、image是顶部图片
4、buttonText是按钮文本
5、block是按钮点击事件

OnboardingContentViewController *firstPage = [OnboardingContentViewController contentWithTitle:@"What A Beautiful Photo" body:@"This city background image is so beautiful." image:[UIImage imageNamed:@"blue"] buttonText:@"Enable Location Services" action:^{
}];

OnboardingContentViewController *secondPage = [OnboardingContentViewController contentWithTitle:@"I'm so sorry" body:@"I can't get over the nice blurry background photo." image:[UIImage imageNamed:@"red"] buttonText:@"Connect With Facebook" action:^{
}];
secondPage.movesToNextViewController = YES;
secondPage.viewDidAppearBlock = ^{
};

OnboardingContentViewController *thirdPage = [OnboardingContentViewController contentWithTitle:@"Seriously Though" body:@"Kudos to the photographer." image:[UIImage imageNamed:@"yellow"] buttonText:@"Get Started" action:^{
}];
```


#### 底部图片控制器

```objc
OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundImage:[UIImage imageNamed:@"milky_way.jpg"] contents:@[firstPage, secondPage, thirdPage]];




<div class="se-preview-section-delimiter"></div>

底部video控制器

NSBundle *bundle = [NSBundle mainBundle];
NSString *moviePath = [bundle pathForResource:@"yourVid" ofType:@"mp4"];
NSURL *movieURL = [NSURL fileURLWithPath:moviePath];
OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundVideoURL:movieURL contents:@[firstPage, secondPage, thirdPage]];




<div class="se-preview-section-delimiter"></div>

定制
1、默认的会给背景图片或者movie加一层黑色的蒙板,可以去掉它们:

onboardingVC.shouldFadeTransitions = YES;




<div class="se-preview-section-delimiter"></div>

2、可以给图片加上模糊效果(相当漂亮):

onboardingVC.shouldBlurBackground = YES;




<div class="se-preview-section-delimiter"></div>

3、可以给蒙板上的文字加上淡出效果:

onboardingVC.shouldFadeTransitions = YES;

转自:https://blog.csdn.net/sinat_30800357/article/details/50016319

收起阅读 »

RAC解析 - 自定义KVO

知识点概述1.KVO实现原理2.runtime使用目的给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。大体思路添加观察者的方法中- (void)SQ_addObserver:(NSObjec...
继续阅读 »

知识点概述

1.KVO实现原理
2.runtime使用

目的

给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。

大体思路

添加观察者的方法中

- (void)SQ_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;

会用runtime的方式手动创建一个其子类,并且将该对象变为该子类。该子类会复写观察方法中keyPath的setter方法,使这个setter被调用时利用runtime去调用observer的回调方法

-(void)observeValueForKeyPath:(NSString *)keyPath 
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;

实现

这里只做KVO的基本功能,当被观察者改变属性的时候通知观察者,所以定义如下方法

NSObject+SQKVO.h

/**
添加观察者

@param observer 观察者
@param keyPath 被观察的属性名
*/
- (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


/**
当被观察的观察属性改变的时候的回调函数

@param keyPath 所观察被观察者的属性名
@param object 被观察者
@param value 被观察的属性的新值
*/
- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value;

@end

因为这里要用到runtime所以需要添加runtime的头文件

#import <objc/message.h>

而且因为用到objc_msgSend所以要改变一下工程的环境变量


一.动态生成子类

在被观察者调用- SQ_addObserver:forKeyPath:时首先动态生成一个其子类。

// 1.生成子类
// 1.1获取名称
Class selfClass = [self class];
NSString *className = NSStringFromClass(selfClass);
NSString *KVOClassName = [className stringByAppendingString:@"_SQKVO"];
const char *KVOClassNameChar = [KVOClassName UTF8String];
// 1.2创建子类
Class KVOClass = objc_allocateClassPair(selfClass, KVOClassNameChar, 0);
// 1.3注册
objc_registerClassPair(KVOClass);

这里可以看到,我们将子类的类名命名为“类名”+“SQKVO”,譬如类名为“Person”,这个子类是“Person_SQKVO”。
这里有个注意点,一般为动态创建的类名应尽量复杂一些避免重复。最好加上“”。

二.根据KeyPath动态添加对应的setter

1 确定setter的名字
举个例子,如果用户给的keyPath是name,应该动态添加一个-setName:的方法。而这个setter的名字是 "set" + "把keyPath变为首字母大写" + ":"
所以可以得出

NSString *setterString =
[NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);

2 利用class_addMethod()给子类动态添加方法

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

1、cls:
给哪个类添加方法。即新生成的子类,上面生成的 KVOClass。
2、name:
所添加方法的名称。即上一步生成的字符串 setterString。
3、imp:
所添加方法的实现。即这个方法的C语言实现,首先在下面先写一个C语言的方法。稍后会讲具体实现。

void setValue(id self, SEL _cmd, id newVale) {
}

4、types:
所添加方法的编码类型。setter的返回值是void,参数是一个对象(id)。void用"v"表示,返回值和参数之间用“@:”隔开,对象用"@"表示。最后我们可以得出结果"v@:@"。
具体其他的编码类型可以参考苹果文档。
ps: 这里说下为什么返回值和参数之间用“@:”隔开。“:”代表字符串,所有的OC方法都有两个隐藏参数在参数列表的最前面,“发起者”和 “方法描述符”,“@”就是这个发起者,“:”是方法描述符。而这个types其实是imp返回值和参数的编码。因为OC方法中返回值和参数之间必然有“发起者”和“SEL”隔着,所以“@:”自然而然就成了返回值和参数之间的分隔符。
当然我们还可以用@encode来得到我们想要的编码类型

NSString *encodeString =
[NSString stringWithFormat:@"%s%s%s%s",
@encode(void),
@encode(id),
@encode(SEL),
@encode(id)];

3 将当前对象的类变为我们所创建的子类的类型,即更改isa指针

object_setClass(self, KVOClass);

4 将keyPath和观察者关联(associate)到我们的对象上

用下面这个函数可以很方便的将一个对象用键值对的方式绑定到一个目标对象上。
*如果想了解跟多可以查找《Effective Objective-C》的第10条

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

1、object
目标对象

2、key
绑定对象的键,相当于NSDictionary的key
这里的key一般采用下面的方式声明:

static const void *SQKVOObserverKey = &SQKVOObserverKey;
static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;

这样做是因为若想令两个键匹配到同一个值,则两者必须是完全相同的指针才行。

3、value
绑定对象,相当于NSDictionary的value

4、policy
绑定对象的缓存策略
@property (nonatomic, weak) :OBJC_ASSOCIATION_ASSIGN
@property (nonatomic, strong) :OBJC_ASSOCIATION_RETAIN_NONATOMIC
@property (nonatomic, copy) :OBJC_ASSOCIATION_COPY_NONATOMIC
@property (atomic, strong) :OBJC_ASSOCIATION_RETAIN
@property (atomic, weak) :OBJC_ASSOCIATION_COPY

最后关联的代码:

objc_setAssociatedObject(self, SQKVOObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, SQKVOKeyPathKey, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);

三.setValue()的实现

这个函数的目的主要是:
1.利用objc_msgSend触发原先类的setter
2.利用objc_msgSend触发观察者的回调方法

1. 触发原先的setter方法

// 保存子类
Class KVOClass = [self class];

// 变回原先的类型,去触发setter
object_setClass(self, class_getSuperclass(KVOClass));
NSString *keyPath = objc_getAssociatedObject(self, SQKVOKeyPathKey);
NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);
objc_msgSend(self, setter, newVale);

2. 调用观察者的回调方法

id observer = objc_getAssociatedObject(self, SQKVOObserverKey);
objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);

3.改回KVO类

object_setClass(self, KVOClass);

四.实现空的回调方法

- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {

}

五.调用自定义的KVO

恭喜你看到这里,并且恭喜你已经成功了!

六.代码

代码下载地址

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

收起阅读 »

Objective-C高级编程笔记一(自动引用计数)

示例代码下载手动引用计数MRC内存管理的思考方式1、自己生成的对象自己持有2、不是自己生成的对象,自己也能持有3、不在需要自己持有的对象时释放4、不是自己持有的对象无法释放对象操作与Objective-C方法的对应实现一个MRCObject类:@impleme...
继续阅读 »

示例代码下载

手动引用计数

MRC内存管理的思考方式

1、自己生成的对象自己持有
2、不是自己生成的对象,自己也能持有
3、不在需要自己持有的对象时释放
4、不是自己持有的对象无法释放

对象操作与Objective-C方法的对应


实现一个MRCObject类:

@implementation MRCObject
- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);

[super dealloc];
}
+ (instancetype)object {
MRCObject *obj = [self allocObject];
[obj autorelease];
return obj;
}

+ (instancetype)allocObject {
MRCObject *obj = [[MRCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);

return obj;
}

@end

自己生成并持有对象:

MRCObject *obj = [MRCObject allocObject];

不是自己生成的对象也能持有:

MRCObject *obj = [MRCObject object];
[obj retain];

不在需要自己持有的对象时释放:

MRCObject *obj = [self allocObject];
[obj release];

无法释放自己没有持有的对象:

MRCObject *obj = [self allocObject];
[obj release];
[obj release];//会奔溃

autorelease

autorelease像c语言的自动变量来对待对象实例,当超出其作用域(相当于变量作用域),对象实例的release方法被调用。与c语言自动变量不同的是,可以autorelease的作用域。

autorelease的使用方法:

1、生成NSAutoreleasePool对象
2、调用已分配对象实例的autorelease方法
3、废弃NSAutoreleasePool对象

在应用程序中,由于主线程的NSRunloop对NSAutoreleasePool对象进行生成、持有和废弃处理。因此开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。如下图:


在大量产生autorelease对象时,只要不废弃NSAutoreleasePool对象,autorelease对象就不会被释放,因此会产生内存不足的现象。如下两段代码:

for (int index = 0; index < 1000; index++) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
}
for (int index = 0; index < 1000; index++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
[pool drain];
}

ARC

ARC规则

ARC有效时,id类型和对象类型同c语言其他类型不同,必须添加所有权修饰符。共如下4种所有权修饰符:

1、__strong修饰符
2、__weak修饰符
3、__unsafe_unretained修饰符
4、__outoreleasing修饰符

import "ARCObject.h"

实现一个ARCObject类:

@interface ARCObject ()
{
__strong id _strongObj;
__weak id _weakObj;
}

@end

@implementation ARCObject

- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);
}
+ (instancetype)allocObject {
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
return obj;
}
- (void)setStrongObject:(id)obj {
_strongObj = obj;
}
- (void)setWeakObject:(id)obj {
_weakObj = obj;
}

@end

__strong修饰符

__strong修饰符是所有id类型和对象类型默认的所有权修饰符,表示对对象的强引用,在超出其作用域或被重新赋值时被废弃。

{
ARCObject *obj = [ARCObject allocObject];
NSLog(@"作用域最后一行%@", obj);
}
NSLog(@"作用域已经结束");
ARCObject *obj = [ARCObject allocObject];
NSLog(@"重新赋值前%@", obj);
obj = [ARCObject allocObject];
NSLog(@"重新赋值前后%@", obj);

__strong、__weak、__outoreleasing修饰符的自动变量默认初始化为nil。

__weak修饰符

__weak修饰符与__strong修饰符相反,提供弱引用,弱引用不持有对象实例。

循环引用容易发生内存泄漏,内存泄漏就是应当废弃的对象在超出其生存周期后依然存在。可以使用__weak修饰符来避免。

ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
[aObj setStrongObject:bObj];
[bObj setStrongObject:aObj];
ARCObject *obj = [ARCObject allocObject];
[obj setStrongObject:obj];
ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
ARCObject *cObj = [ARCObject allocObject];
[aObj setWeakObject:bObj];
[bObj setWeakObject:aObj];
[cObj setWeakObject:cObj];

__weak修饰符有一个优点就是:在持有某对象的弱引用时,如果该对象被废弃,则该对象弱引用自动失效且被置为nil。

__unsafe_unretained修饰符

__unsafe_unretained修饰符,正如其名一样是不安全的所有权修饰符。尽管ARC的内存管理是编译器的工作,但是这一点需要注意特别注意,__unsafe_unretained修饰符的变量不属于编译器内存管理的对象。

__unsafe_unretained修饰符和__weak修饰符的变量一样不会持有对象,但是__unsafe_unretained修饰符的变量在销毁时并不会自动置为nil,在其地址被覆盖后就会因为反问垂悬指正而造成奔溃。因此__unsafe_unretained修饰符变量赋值给__strong修饰符变量时要确保对象的真实存在。因为__weak修饰符是在iOS5中实现的,__unsafe_unretained修饰符存在的意义就是在iOS4中代替__weak修饰符的作用。

ARCObject __unsafe_unretained *obj = nil;
{
ARCObject *obj1 = [ARCObject allocObject];
obj = obj1;
}
NSLog(@"%@(%@)", NSStringFromClass(obj.class), obj);

__outoreleasing修饰符

ARC有效时不能使用outorelease方法,也不能使用NSAutoreleasePool类。这样就导致outorelease无法直接使用,但实际上outorelease功能是起作用的。使用@outoreleasepool{}块代码来代替NSAutoreleasePool类对象的生成持有以及废弃。通过赋值给__outoreleasing修饰符的变量来代替调用outorelease方法,也就是说对象被注册到autoreleasepool中。

@autoreleasepool {
ARCObject __autoreleasing *obj1 = [ARCObject allocObject];
NSLog(@"autoreleasepool块最后一行%@", obj1);
}
NSLog(@"autoreleasepool块已经结束");

ARC有效时,cocoa中由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到outoreleasepool中。所以非显示的使用__outoreleasing修饰符也是可以的。

NSMutableArray __weak *array = nil;
NSLog(@"作用域块开始前%@", array);
{
NSMutableArray *arr = [NSMutableArray arrayWithObject:@(1)];
array = arr;
NSLog(@"作用域块最后一行%@", array);
}
NSLog(@"作用域块已经结束%@", array);

打印结果:

2019-03-28 11:56:52.316360+0800 ProfessionalExample[82984:16680615] 作用域块开始前(null)
2019-03-28 11:56:52.316538+0800 ProfessionalExample[82984:16680615] 作用域块最后一行(
1
)
2019-03-28 11:56:52.316627+0800 ProfessionalExample[82984:16680615] 作用域块已经结束(
1
)

id的指针和对象的指针在没有显式指定修饰符时会被附加上__outoreleasing修饰符。

- (BOOL)performOperationWithError:(ARCObject **)obj {
*obj = [ARCObject object];
return NO;
}

调用方法则为如下所示,自动转化为__autoreleasing修饰符:

[self performOperationWithError:<#(ARCObject *__autoreleasing *)#>];

id的指针和对象的指针变量必须指明所有权修饰符,并且赋值的所有权修饰符必须一致:

NSObject **pObj;//编报错,没有所有权修饰符
NSObject *obj = [[NSObject alloc] init];
NSObject *__autoreleasing*pObj = &obj;//编译报错,会更改所有权属性

纠正一个比较普遍的错误认知,for循环中并不是循环结束才释放循环内的局部变量,并不是所有产生大量对象的for循环中都需要加NSAutoreleasePool,而是产生大量autorelease对象时才需要添加。如下示例代码:

for (int index = 0; index < 2; index++) {
if (index == 0) {
NSLog(@"-------------begin");
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
}
if (index == 1) {
NSLog(@"-------------end");
}
}

下面是这段代码的打印内容:

2019-03-28 15:27:19.179194+0800 ProfessionalExample[85692:16955598] -------------begin
2019-03-28 15:27:19.179366+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)生成了
2019-03-28 15:27:19.179449+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)销毁了
2019-03-28 15:27:19.179521+0800 ProfessionalExample[85692:16955598] -------------end

ARC编码规则

1、不能使用retain/release/retainCount/autorelease
2、不能使用NSAllocateObject/NSDeallocateObject
3、须遵守内存管理的方法命名规则
4、不能显式调用dealloc方法
5、使用@autoreleasepool{}代替NSAutoreleasePool
6、不能使用NSZone
7、对象变量不能作为c语言结构体的成员
8、显式转换id和void *

内存管理的方法命名规则

以alloc/new/copy/mutableCopy开头的方法返回对象时,必须返回给调用方应当持有的对象。这在ARC有效时是一样的,不同的是以init开头的方法必须是实例方法且需要返回对象,该返回对象并不注册到autoreleasepool上。

对象变量不能作为c语言结构体的成员

要把对象类型变量加入到结构体中,需强制转为void *或者前面附加__unsafe_unretained修饰符。

显式转换id和void *

可以使用(__bridge)转换void *和OC对象,但是其安全性和赋值给__unsafe_unretained修饰符相近或者更低。如果管理时不注意赋值对象的所有者就会因为垂悬指针而奔溃或者内存泄漏。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
obj = (__bridge NSObject *)p;

__bridge_retained转换可使要赋值的变量持有所赋值的变量。__bridge_transfer则与之相反。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj;
obj = (__bridge_transfer NSObject *)p;

NSObject对象与Core Fundation对象之间的相互转换,即免费桥(Toll-Freee-Bridge)转换。CFBridgingRetain函数(等价于__bridge_retained转换),CFBridgingRelease函数(等价于__bridge_transfer)。

NSObject *obj = [[NSObject alloc] init];
CFTypeRef ref = CFBridgingRetain(obj);
obj = CFBridgingRelease(ref);

属性

属性声明的属性与所有权修饰符对应关系


c数组

c静态数组,各修饰符的使用OC对象一样没有区别。

以__strong为例,其初始化为nil,超过作用域销毁:

{
ARCObject *array[2];
array[0] = [ARCObject allocObject];
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[1] = nil;
NSLog(@"array第二个元素:%@", array[1]);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:19:26.697408+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)生成了
2019-03-28 19:19:26.697661+0800 ProfessionalExample[88859:17353905] array第一个元素:<ARCObject: 0x6000005f8500>
2019-03-28 19:19:26.697761+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697845+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697930+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)销毁了
2019-03-28 19:19:26.697995+0800 ProfessionalExample[88859:17353905] 作用域块已经结束

c动态数组,c语言中动态数组声明用指针即id *array(NSObject **array)。需要注意如下几点:

1、_strong/__weak修饰符的OC变量初始化为nil,并不代表其指针初始化为nil。所以分配内存后,需要对其初始化为nil,否则非常危险。calloc函数分配的就是nil初始化后的内存,malloc函数分配内存后必须使用memset将内存填充为0(nil)。
2、必须置空_strong修饰符的态数数组内的元素,使其强引用失效,元素才能释放。因为动态数组的生命周期有开发者管理,编译器不能确定销毁动态数组内元素的时机。

{
ARCObject *__strong *array;
array = (ARCObject *__strong *)calloc(2, sizeof(ARCObject *));
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[0] = [ARCObject allocObject];
array[1] = [ARCObject allocObject];
array[0] = nil;
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
free(array);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:29:26.162245+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.162586+0800 ProfessionalExample[89048:17394552] array第二个元素:(null)
2019-03-28 19:29:26.162763+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)生成了
2019-03-28 19:29:26.162867+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a395c0>)生成了
2019-03-28 19:29:26.162945+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)销毁了
2019-03-28 19:29:26.163011+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.163083+0800 ProfessionalExample[89048:17394552] array第二个元素:<ARCObject: 0x600001a395c0>
2019-03-28 19:29:26.163160+0800 ProfessionalExample[89048:17394552] 作用域块已经结束

转自:https://www.jianshu.com/p/82849c350b0b

收起阅读 »

CYLTabBarController的使用

CYLTabBarController 是一个自定义的TabBarController, 集成非常简单https://github.com/ChenYilong/CYLTabBarController1.首先使用CocoaPods 进行集成: pod...
继续阅读 »

CYLTabBarController 是一个自定义的TabBarController, 集成非常简单

https://github.com/ChenYilong/CYLTabBarController

1.首先使用CocoaPods 进行集成: 

pod 'CYLTabBarController'
在终端上执行: 
pod install --verbose --no-repo-update

2. 创建TabBar对应的视图控制器


3.创建CYLTabBarControllerConfig

#import <Foundation/Foundation.h>  

#import "CYLTabBarController.h"
@interface CYLTabBarControllerConfig : NSObject

@property (nonatomic, retain) CYLTabBarController * tabBarController;

@end
#import "CYLTabBarControllerConfig.h"  

#import "FirstViewController.h"
#import "SecondViewController.h"
#import "ThirdViewController.h"
#import "FourthViewController.h"


@implementation CYLTabBarControllerConfig

- (CYLTabBarController *)tabBarController {
if (_tabBarController == nil) {
FirstViewController * firstViewController = [[FirstViewController alloc] init];
UIViewController * firstNavigationController = [[UINavigationController alloc] initWithRootViewController:firstViewController];

SecondViewController * secondViewController = [[SecondViewController alloc] init];
UIViewController * secondNavigationController = [[UINavigationController alloc] initWithRootViewController:secondViewController];

ThirdViewController * thirdViewController = [[ThirdViewController alloc] init];
UIViewController * thirdNavigationController = [[UINavigationController alloc] initWithRootViewController:thirdViewController];

FourthViewController * fourthViewController = [[FourthViewController alloc] init];
UIViewController * fourthNavigationController = [[UINavigationController alloc] initWithRootViewController:fourthViewController];


NSArray * tabBarItemsAttributes = [self tabBarItemsAttributes];
NSArray * viewControllers = @[firstNavigationController, secondNavigationController, thirdNavigationController, fourthNavigationController];

CYLTabBarController * tabBarController = [[CYLTabBarController alloc] init];

tabBarController.tabBarItemsAttributes = tabBarItemsAttributes;
tabBarController.viewControllers = viewControllers;

_tabBarController = tabBarController;

}

return _tabBarController;
}


- (NSArray *)tabBarItemsAttributes {
NSDictionary * tabBarItem1Attribute = @{
CYLTabBarItemTitle : @"首页",
CYLTabBarItemImage : @"home_normal",
CYLTabBarItemSelectedImage : @"home_highlight"
};
NSDictionary * tabBarItem2Attribute = @{
CYLTabBarItemTitle : @"同城",
CYLTabBarItemImage : @"mycity_normal",
CYLTabBarItemSelectedImage : @"mycity_highlight"
};
NSDictionary * tabBarItem3Attribute = @{
CYLTabBarItemTitle : @"消息",
CYLTabBarItemImage : @"message_normal",
CYLTabBarItemSelectedImage : @"message_highlight"
};
NSDictionary * tabBarItem4Attribute = @{
CYLTabBarItemTitle : @"我的",
CYLTabBarItemImage : @"account_normal",
CYLTabBarItemSelectedImage : @"account_highlight"
};
NSArray * tarBarItemsAttrbutes = @[tabBarItem1Attribute, tabBarItem2Attribute, tabBarItem3Attribute, tabBarItem4Attribute];

return tarBarItemsAttrbutes;
}


/**
* 更多TabBar自定义设置:比如:tabBarItem 的选中和不选中文字和背景图片属性、tabbar 背景图片属性
*/
+ (void)customizeTabBarAppearance {

//去除 TabBar 自带的顶部阴影
[[UITabBar appearance] setShadowImage:[[UIImage alloc] init]];

// set the text color for unselected state
// 普通状态下的文字属性
NSMutableDictionary *normalAttrs = [NSMutableDictionary dictionary];
normalAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

// set the text color for selected state
// 选中状态下的文字属性
NSMutableDictionary *selectedAttrs = [NSMutableDictionary dictionary];
selectedAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

// set the text Attributes
// 设置文字属性
UITabBarItem *tabBar = [UITabBarItem appearance];
[tabBar setTitleTextAttributes:normalAttrs forState:UIControlStateNormal];
[tabBar setTitleTextAttributes:selectedAttrs forState:UIControlStateSelected];

// Set the dark color to selected tab (the dimmed background)
// TabBarItem选中后的背景颜色
[[UITabBar appearance] setSelectionIndicatorImage:[self imageFromColor:[UIColor colorWithRed:26 / 255.0 green:163 / 255.0 blue:133 / 255.0 alpha:1] forSize:CGSizeMake([UIScreen mainScreen].bounds.size.width / 5.0f, 49) withCornerRadius:0]];

// set the bar background color
// 设置背景图片
// UITabBar *tabBarAppearance = [UITabBar appearance];
// [tabBarAppearance setBackgroundImage:[UIImage imageNamed:@"tabbar_background_ios7"]];
}

+ (UIImage *)imageFromColor:(UIColor *)color forSize:(CGSize)size withCornerRadius:(CGFloat)radius {
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContext(rect.size);

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// Begin a new image that will be the new image with the rounded corners
// (here with the size of an UIImageView)
UIGraphicsBeginImageContext(size);

// Add a clip before drawing anything, in the shape of an rounded rect
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius] addClip];
// Draw your image
[image drawInRect:rect];

// Get the image, here setting the UIImageView image
image = UIGraphicsGetImageFromCurrentImageContext();

// Lets forget about that we were drawing
UIGraphicsEndImageContext();
return image;
}

4. AppDelegate 设置根视图控制器

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
// TabBar
CYLTabBarControllerConfig * TabBarControllerConfig = [[CYLTabBarControllerConfig alloc] init];
self.window.rootViewController = TabBarControllerConfig.tabBarController;
[self customizeInterface];

return YES;
}
- (void)customizeInterface {
[self setUpNavigationBarAppearance];
}

/**
* 设置navigationBar样式
*/
- (void)setUpNavigationBarAppearance {
UINavigationBar *navigationBarAppearance = [UINavigationBar appearance];

UIImage *backgroundImage = nil;
NSDictionary *textAttributes = nil;
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
backgroundImage = [UIImage imageNamed:@"navigationbar_background_tall"];

textAttributes = @{
NSFontAttributeName: [UIFont boldSystemFontOfSize:18],
NSForegroundColorAttributeName: [UIColor blackColor],
};
} else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
backgroundImage = [UIImage imageNamed:@"navigationbar_background"];

textAttributes = @{
UITextAttributeFont: [UIFont boldSystemFontOfSize:18],
UITextAttributeTextColor: [UIColor blackColor],
UITextAttributeTextShadowColor: [UIColor clearColor],
UITextAttributeTextShadowOffset: [NSValue valueWithUIOffset:UIOffsetZero],
};
#endif
}

[navigationBarAppearance setBackgroundImage:backgroundImage
forBarMetrics:UIBarMetricsDefault];
[navigationBarAppearance setTitleTextAttributes:textAttributes];
}

运行即可实现效果,如果想实现凸起的加号效果需要 CYLPlusButtonSubclass

#import "CYLPlusButton.h"  

@interface CYLPlusButtonSubclass : CYLPlusButton <CYLPlusButtonSubclassing>

@end
#import "CYLPlusButtonSubclass.h"  

@interface CYLPlusButtonSubclass ()<UIActionSheetDelegate> {
CGFloat _buttonImageHeight;
}

@end

@implementation CYLPlusButtonSubclass

#pragma mark -
#pragma mark - Life Cycle

+ (void)load {
[super registerSubclass];
}

- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.adjustsImageWhenHighlighted = NO;
}
return self;
}

//上下结构的 button
- (void)layoutSubviews {
[super layoutSubviews];

// 控件大小,间距大小
CGFloat const imageViewEdge = self.bounds.size.width * 0.6;
CGFloat const centerOfView = self.bounds.size.width * 0.5;
CGFloat const labelLineHeight = self.titleLabel.font.lineHeight;
CGFloat const verticalMarginT = self.bounds.size.height - labelLineHeight - imageViewEdge;
CGFloat const verticalMargin = verticalMarginT / 2;

// imageView 和 titleLabel 中心的 Y 值
CGFloat const centerOfImageView = verticalMargin + imageViewEdge * 0.5;
CGFloat const centerOfTitleLabel = imageViewEdge + verticalMargin * 2 + labelLineHeight * 0.5 + 5;

//imageView position 位置
self.imageView.bounds = CGRectMake(0, 0, imageViewEdge, imageViewEdge);
self.imageView.center = CGPointMake(centerOfView, centerOfImageView);

//title position 位置
self.titleLabel.bounds = CGRectMake(0, 0, self.bounds.size.width, labelLineHeight);
self.titleLabel.center = CGPointMake(centerOfView, centerOfTitleLabel);
}

#pragma mark -
#pragma mark - Public Methods

/*
*
Create a custom UIButton with title and add it to the center of our tab bar
*
*/
+ (instancetype)plusButton {

CYLPlusButtonSubclass *button = [[CYLPlusButtonSubclass alloc] init];

[button setImage:[UIImage imageNamed:@"post_normal"] forState:UIControlStateNormal];
[button setTitle:@"发布" forState:UIControlStateNormal];

[button setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:9.5];
[button sizeToFit];

[button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
return button;
}
/*
*
Create a custom UIButton without title and add it to the center of our tab bar
*
*/
//+ (instancetype)plusButton
//{
//
// UIImage *buttonImage = [UIImage imageNamed:@"hood.png"];
// UIImage *highlightImage = [UIImage imageNamed:@"hood-selected.png"];
//
// CYLPlusButtonSubclass* button = [CYLPlusButtonSubclass buttonWithType:UIButtonTypeCustom];
//
// button.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
// button.frame = CGRectMake(0.0, 0.0, buttonImage.size.width, buttonImage.size.height);
// [button setBackgroundImage:buttonImage forState:UIControlStateNormal];
// [button setBackgroundImage:highlightImage forState:UIControlStateHighlighted];
// [button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
//
// return button;
//}

#pragma mark -
#pragma mark - Event Response

- (void)clickPublish {
UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
UIViewController *viewController = tabBarController.selectedViewController;

UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self
cancelButtonTitle:@"取消"
destructiveButtonTitle:nil
otherButtonTitles:@"拍照", @"从相册选取", @"淘宝一键转卖", nil nil];
[actionSheet showInView:viewController.view];
}

#pragma mark - UIActionSheetDelegate
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
NSLog(@"index: %ld", buttonIndex);
}


#pragma mark - CYLPlusButtonSubclassing
//+ (NSUInteger)indexOfPlusButtonInTabBar {
// return 3;
//}

+ (CGFloat)multiplerInCenterY {
return 0.3;
}

@end

Demo 下载地址:

http://download.csdn.net/detail/vbirdbest/9431253

实现效果如图:


转自:https://blog.csdn.net/man_liang/article/details/56671353

收起阅读 »

iOS 开源项目-FXBlurView

PurposeFXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It...
继续阅读 »

Purpose
FXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It is designed to be as fast and as simple to use as possible. FXBlurView offers two modes of operation: static, where the view is rendered only once when it is added to a superview (though it can be updated by calling setNeedsDisplay or updateAsynchronously:completion:) or dynamic, where it will automatically redraw itself on a background thread as often as possible.
FXBlurView 是一个 UIView 的子类,复制了 iOS7 的实时背景模糊效果,但是可以运行在 iOS5 以上的版本。它的设计简单易用。FXBlurView 提供两种模式:静态模糊和动态模糊。

FXBlurView methods

+(void)setBlurEnabled:(BOOL)blurEnabled;

This method can be used to globally enable/disable the blur effect on all FXBlurView instances. This is useful for testing, or if you wish to disable blurring on iPhone 4 and below (for consistency with iOS7 blur view behavior). By default blurring is enabled.

这个方法用来设置全局 使能/不使能 模糊效果在 FXBlurView 的实例上。默认情况下模糊效果是启动。

+(void)setUpdatesEnabled;
+(void)setUpdatesDisabled;

These methods can be used to enable and disable updates for all dynamic FXBlurView instances with a single command. Useful for disabling updates immediately before performing an animation so that the FXBlurView updates don’t cause the animation to stutter. Calls can be nested, but ensure that the enabled/disabled calls are balanced, or the updates will be left permanently enabled or disabled.

这两个方法用来设置所以的动态 FXBlurView 是否进行更新,通过一条指令执行。在展示动画之前立即对没有用的更新进行更新,使 FXBlurView 更新不会产生动画断断续续的效果。调用可以嵌套,但确保 启用/禁用 调用平衡,否则更新会留下永久 启用/禁用。

-(void)updateAsynchronously:(BOOL)async completion:(void (^)())completion;

This method can be used to trigger an update of the blur effect (useful when dynamic = NO). The async argument controls whether the blur will be redrawn on the main thread or in the background. The completion argument is an optional callback block that will be called when the blur is completed.

这个方法可以用于触发更新模糊效果。(在属性 "dynamic = NO"情况下有用)。异步参数控制是否模糊将要在主线程上或在后台进行重绘。完成参数是一个可供选择的回调块,将在模糊完成的时候进行调用。

-(void)setNeedsDisplay;

Inherited from UIView, this method can be used to trigger a (synchronous) update of the view. Calling this method is more-or-less equivalent to calling [view updateAsynchronously:NO completion:NULL].

继承 UIView,这个方法用于触发一个(同步)更新视图。调用这个方法或多或少等同于调用[view updateAsynchronously:NO completion:NULL].

FXBlurView properties

@property (nonatomic, getter = isBlurEnabled) BOOL blurEnabled;

This property toggles blurring on and off for an individual FXBlurView instance. Blurring is enabled by default. Note that if you disable blurring using the +setBlurEnabled method then that will override this setting.

这个属性用来切换 FXBlurView 单独实例模糊启动还是关闭。默认情况下模糊是使能的。请注意,如果您禁用模糊方法 setBlurEnabled,那么它将覆盖此设置。

@property (nonatomic, getter = isDynamic) BOOL dynamic;

This property controls whether the FXBlurView updates dynamically, or only once when the view is added to its superview. Defaults to YES. Note that if dynamic is set to NO, you can still force the view to update by calling setNeedsDisplay or updateAsynchronously:completion:. Dynamic blurring is extremely cpu-intensive, so you should always disable dynamic views immediately prior to performing an animation to avoid stuttering. However, if you have multiple FXBlurViews on screen then it is simpler to disable updates using the setUpdatesDisabledmethod rather than setting the dynamic property to NO.

这个属性控制 FXBlurView 是否动态更新,还是只有在视图加入到它的父视图中。默认情况下是 YES ,请注意,如果你设置 dynamic 属性为 NO,你可以强制视图更新通过调用 setNeedsDisplay或者updateAsynchronously:completion:。动态模糊非常消耗 CPU 内存,所以您应该禁用立即执行的动态视图避免出现断断续续的动画。然而,如果您在屏幕上有多个 FXBlurViews ,通过设置方法 setUpdatedsDisabled来禁止更新比用设置动态属性为 NO 更为简单。

@property (nonatomic, assign) NSUInteger iterations;

The number of blur iterations. More iterations improves the quality but reduces the performance. Defaults to 2 iterations.

模糊迭代的次数。更多的迭代提高质量,但会降低性能。默认值为 2 的迭代。

@property (nonatomic, assign) NSTimeInterval updateInterval;

This controls the interval (in seconds) between successive updates when the FXBlurView is operating in dynamic mode. This defaults to zero, which means that the FXBlurView will update as fast as possible. This yields the best frame rate, but is also extremely CPU intensive and may cause the rest of your app’s performance to degrade, especially on older devices. To alleviate this, try increasing the updateInterval value.

此属性控制 FXBlurView 在动态模式下,距离成功更新的时间间隔(以秒计)。默认值为 0 ,这表示 FXBlurView 更新越快越好。 这将生成最佳的帧速率,但是也是非常消耗 CPU内存,导致你的其他 apps 无法无法加载,特别是旧设备。为了减缓这些情况,尝试增加updateInterval 的值。

@property (nonatomic, assign) CGFloat blurRadius;

This property controls the radius of the blur effect (in points). Defaults to a 40 point radius, which is similar to the iOS 7 blur effect.

此属性控制模糊效果的半径 (以像素点计)。默认是半径为40个像素点,这个值与 iOS7 模糊效果相似。

@property (nonatomic, strong) UIColor *tintColor;

This in an optional tint color to be applied to the FXBlurView. The RGB components of the color will be blended with the blurred image, resulting in a gentle tint. To vary the intensity of the tint effect, use brighter or darker colors. The alpha component of the tintColor is ignored. If you do not wish to apply a tint, set this value to nil or [UIColor clearColor]. Note that if you are using Xcode 5 or above, FXBlurViews created in Interface Builder will have a blue tint by default.

这是应用在 FXBlurView 可选的色调选择。颜色的 RGB 分量将会掺入到模糊图像上,导致产生一个柔和的色调。为了验证色调效果,若要改变色调效果的强度,使用更亮或更暗的颜色。颜色的透明参数是被忽略的。如果您不想应用色调,设置[UIColor clearColor]。请注意,如果您现在使用 Xcode5及以上,FXBlurViews 产生一个接口生成器将默认有一个一个蓝色的色调。

@property (nonatomic, weak) UIView *underlyingView;

This property specifies the view that the FXBlurView will sample to create the blur effect. If set to nil (the default), this will be the superview of the blur view itself, but you can override this if you need to.

此属性表明该视图是 FXBlurView 产生模糊效果的子视图。如果设置为 nil(默认),则该视图的父视图是模糊视图本身,但是如果您有需要您可以进行覆盖它。

总结

今天通过学习 FXBlurView ,提高对英语文档的理解和翻译能力,增加了自己的学习兴趣,也懂得了如何去使用 FXBlurView 的模糊效果特效。在实际中提升自己的能力,年轻,就是资本!Oh Yeah!

参考

https://github.com/cnbin/FXBlurView

转自:https://cnbin.github.io/blog/2015/05/25/ioskai-yuan-xiang-mu-fxblurview/

收起阅读 »

你还没用Logger?用了他我才知道屌

Logger简单,漂亮,强大的android日志 配置下载 implementation 'com.orhanobut:logger:2.2.0' 初始化 Logger.addLogAdapter(new AndroidLogAdapter()); 使用 ...
继续阅读 »

Logger

简单,漂亮,强大的android日志


配置

下载


implementation 'com.orhanobut:logger:2.2.0'

初始化


Logger.addLogAdapter(new AndroidLogAdapter());

使用


Logger.d("hello");

输出


属性

Logger.d("debug");
Logger.e("error");
Logger.w("warning");
Logger.v("verbose");
Logger.i("information");
Logger.wtf("What a Terrible Failure");

支持字符串格式参数


Logger.d("hello %s", "world");

支持集合(仅适用于调试日志)


Logger.d(MAP);
Logger.d(SET);
Logger.d(LIST);
Logger.d(ARRAY);

Json和Xml支持(输出将处于调试级别)


Logger.json(JSON_CONTENT);
Logger.xml(XML_CONTENT);

高级用法

FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
.showThreadInfo(false) // (可选)是否显示线程信息。默认为 true
.methodCount(0) // (可选)要显示的方法行数。默认为 2
.methodOffset(7) // (可选)隐藏内部方法调用直到偏移量。默认值5
.logStrategy(customLog) // (可选)将日志策略更改为打印输出。默认LogCat
.tag("My custom tag") // (可选)每个日志的全局标记。默认PRETTY_LOGGER
.build();

Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));

日志开启

日志适配器通过检查此功能来检查日志是否应打印。


如果要禁用/隐藏输出日志,请重写isLoggable'方法。true会打印日志消息,false` 会忽略它。


Logger.addLogAdapter(new AndroidLogAdapter() {
@Override public boolean isLoggable(int priority, String tag) {
return BuildConfig.DEBUG;
}
});

将日志保存到文件

//TODO: 稍后将添加更多信息


Logger.addLogAdapter(new DiskLogAdapter());

将自定义标记添加到Csv格式策略


FormatStrategy formatStrategy = CsvFormatStrategy.newBuilder()
.tag("custom")
.build();

Logger.addLogAdapter(new DiskLogAdapter(formatStrategy));

工作原理


更多


  • 使用过滤器以获得更好的结果。或者你的自定义标签


  • 确保已禁用“环绕”选项


  • 也可以通过更改设置来简化输出。





  • Timber 集成
    // 将methodOffset设置为5以隐藏内部方法调用
    Timber.plant(new Timber.DebugTree() {
    @Override protected void log(int priority, String tag, String message, Throwable t) {
    Logger.log(priority, tag, message, t);
    }
    });


github地址:https://github.com/orhanobut/logger
下载地址:
master.zip


收起阅读 »

java设计模式:命令模式

前言在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。 在现...
继续阅读 »

前言

在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。


在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。


再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现。


定义与特点

命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。


优点

通过引入中间件(抽象接口)降低系统的耦合度。
扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。

缺点

可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。
命令模式的结构与实现
可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。

结构

抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
在这里插入图片描述

实现

命令模式的代码如下:


package command;
public class CommandPattern {
public static void main(String[] args) {
Command cmd = new ConcreteCommand();
Invoker ir = new Invoker(cmd);
System.out.println("客户访问调用者的call()方法...");
ir.call();
}
}

//调用者


class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void setCommand(Command command) {
this.command = command;
}
public void call() {
System.out.println("调用者执行命令command...");
command.execute();
}
}

//抽象命令


interface Command {
public abstract void execute();
}

//具体命令



class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand() {
receiver = new Receiver();
}
public void execute() {
receiver.action();
}
}

//接收者


class Receiver {
public void action() {
System.out.println("接收者的action()方法被调用...");
}
}

程序的运行结果如下:



客户访问调用者的call()方法...
调用者执行命令command...
接收者的action()方法被调用...

实例


假如我们开发一个播放器,播放器播放功能、拖动进度条功能、停止播放功能、暂停功能,我们在操作播发器的时候并不知道之间调用播放器
哪个功能,而是通过一个控制传达去传递指令给播放器内核,具体传达什么指令,会被封装成一个个按钮。那么每个按钮就相当于一条命令的封装。
用控制条实现了用户发送指令与播放器内核接收指令的解耦。下面来看代码,首先创建播放器内核类:



public class GPlayer {
public void play() {
System.out.println("正常播放");
}

public void speed() {
System.out.println("拖动进度条");
}

public void stop() {
System.out.println("停止播放");
}

public void pause() {
System.out.println("暂停播放");
}
}

创建命令接口:



public interface IAction {
void execute();
}

创建播放指令类:


public class PlayAction implements IAction {
private GPlayer gplayer;

public PlayAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.play();
}
}

创建暂停指令类:


public class PauseAction implements IAction {
private GPlayer gplayer;

public PauseAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.pause();
}
}

创建拖动进度条类:


public class SpeedAction implements IAction {
private GPlayer gplayer;

public SpeedAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.speed();
}
}

创建停止播放指令:


public class StopAction implements IAction {
private GPlayer gplayer;

public StopAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.stop();
}
}

创建控制条controller类:



public class Controller {
private List<IAction> actions = new ArrayList<IAction>();

public void addAction(IAction action) {
actions.add(action);
}

public void execute(IAction action) {
action.execute();
}

public void executes() {
for (IAction action : actions) {
action.execute();
}
actions.clear();
}
}

从上面代码来看,控制条可以执行单条命令,也可以批量执行多条命令。下面看客户端的测试代码:



public class Test {
public static void main(String[] args) {

GPlayer player = new GPlayer();
Controller controller = new Controller();
controller.execute(new PlayAction(player));

controller.addAction(new PauseAction(player));
controller.addAction(new PlayAction(player));
controller.addAction(new StopAction(player));
controller.addAction(new SpeedAction(player));
controller.executes();
}
}

由于控制条已经与播放器内核解耦了,以后如果想扩展新命令,只需要增加命令即可,控制条的结构无须改动。


java源码中的命令模式


首先来看 JDK 中的 Runnable 接口,Runnable 相当于命令模式中的抽象命令角色。Runnable 中的 run() 方法就当于 execute() 方法。



public interface Runnable {
public abstract void run();
}
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + lazySingleton);
}
}

public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("Program End");
}
}

只要是实现了 Runnable 接口的类都被认为是一个线程,相当于命令模式中的具体命令角色。


实际上调用线程的 start() 方法之后,就有资格去抢 CPU 资源,而不需要编写获得 CPU 资源的逻辑。而线程抢到 CPU 资源后,就会执行 run() 方法中的内容,用 Runnable 接口把用户请求和 CPU 执行进行解耦。


收起阅读 »

Java设计模式:迭代器模式

前言在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。 既然将遍历方法封装在聚合...
继续阅读 »

前言

在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。


既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:



  1. 暴露了聚合类的内部表示,使其数据不安全;
  2. 增加了客户的负担。

“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如 Java 中的 Collection、List、Set、Map 等都包含了迭代器。


迭代器模式在生活中应用的比较广泛,比如:物流系统中的传送带,不管传送的是什么物品,都会被打包成一个个箱子,并且有一个统一的二维码。这样我们不需要关心箱子里是什么,在分发时只需要一个个检查发送的目的地即可。再比如,我们平时乘坐交通工具,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性、是残疾人还是正常人等信息。


定义与特点

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式


优点


  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 遍历任务交由迭代器完成,这简化了聚合类。
  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

缺点

增加了类的个数,这在一定程度上增加了系统的复杂性。


在日常开发中,我们几乎不会自己写迭代器。除非需要定制一个自己实现的数据结构对应的迭代器,否则,开源框架提供的 API 完全够用。


结构

迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。现在我们来分析其基本结构与实现方法。



  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

在这里插入图片描述

模式的实现

package net.biancheng.c.iterator;
import java.util.*;
public class IteratorPattern {
public static void main(String[] args) {
Aggregate ag = new ConcreteAggregate();
ag.add("中山大学");
ag.add("华南理工");
ag.add("韶关学院");
System.out.print("聚合的内容有:");
Iterator it = ag.getIterator();
while (it.hasNext()) {
Object ob = it.next();
System.out.print(ob.toString() + "\t");
}
Object ob = it.first();
System.out.println("\nFirst:" + ob.toString());
}
}
//抽象聚合
interface Aggregate {
public void add(Object obj);
public void remove(Object obj);
public Iterator getIterator();
}
//具体聚合
class ConcreteAggregate implements Aggregate {
private List<Object> list = new ArrayList<Object>();
public void add(Object obj) {
list.add(obj);
}
public void remove(Object obj) {
list.remove(obj);
}
public Iterator getIterator() {
return (new ConcreteIterator(list));
}
}
//抽象迭代器
interface Iterator {
Object first();
Object next();
boolean hasNext();
}
//具体迭代器
class ConcreteIterator implements Iterator {
private List<Object> list = null;
private int index = -1;
public ConcreteIterator(List<Object> list) {
this.list = list;
}
public boolean hasNext() {
if (index < list.size() - 1) {
return true;
} else {
return false;
}
}
public Object first() {
index = 0;
Object obj = list.get(index);
;
return obj;
}
public Object next() {
Object obj = null;
if (this.hasNext()) {
obj = list.get(++index);
}
return obj;
}
}

运行结果


聚合的内容有:中山大学    华南理工    韶关学院   
First:中山大学

java源码分析

Iterator


public interface Iterator<E> {

boolean hasNext();

E next();

default void remove() {
throw new UnsupportedOperationException("remove");
}

//剩余元素迭代
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

上面是迭代器Iterator接口的代码,定义了一些需要子类实现的方法和默认的方法。在这里说一下上面两个default方法都是JDK1.8之后才有的接口新特性,在JDK1.8之前接口中不能有方法实体。


ArrayList


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}

上面是简化的ArrayList类,因为具体实现迭代器Itr的类在ArrayList中作为内部类存在,这个内部类将接口中的方法做了具体实现,并且是只对ArrayList这个类进行实现的。


public interface List<E> extends Collection<E> {
Iterator<E> iterator();
}

上面是简化的List接口,充当的是聚合接口,可以看见内部创建了相应迭代器接口的方法。


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public Iterator<E> iterator() {
return new Itr();
}
}

上面是简化的ArrayList类,充当的是具体聚合类角色,在这里是直接返回了一个具体实现迭代器的类。


public class Test1 {

public static void main(String[] args) {
List<Integer> a=new ArrayList<>();
a.add(1);
a.add(2);
a.add(3);
Iterator itr=a.iterator();
while(itr.hasNext()){
System.out.println(itr.next());
}
}
}

收起阅读 »

java设计模式:中介者模式

前言在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵...
继续阅读 »

前言

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。


如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参加工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。


在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。
模式的定义与特点

定义

定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。


优点

类之间各司其职,符合迪米特法则。
降低了对象之间的耦合性,使得对象易于独立地被复用。
将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

缺点

中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
模式的结构与实现
中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

结构

抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
在这里插入图片描述

实现

中介者模式的实现代码如下:


package net.biancheng.c.mediator;
import java.util.*;
public class MediatorPattern {
public static void main(String[] args) {
Mediator md = new ConcreteMediator();
Colleague c1, c2;
c1 = new ConcreteColleague1();
c2 = new ConcreteColleague2();
md.register(c1);
md.register(c2);
c1.send();
System.out.println("-------------");
c2.send();
}
}
//抽象中介者
abstract class Mediator {
public abstract void register(Colleague colleague);
public abstract void relay(Colleague cl); //转发
}
//具体中介者
class ConcreteMediator extends Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMedium(this);
}
}
public void relay(Colleague cl) {
for (Colleague ob : colleagues) {
if (!ob.equals(cl)) {
((Colleague) ob).receive();
}
}
}
}
//抽象同事类
abstract class Colleague {
protected Mediator mediator;
public void setMedium(Mediator mediator) {
this.mediator = mediator;
}
public abstract void receive();
public abstract void send();
}
//具体同事类
class ConcreteColleague1 extends Colleague {
public void receive() {
System.out.println("具体同事类1收到请求。");
}
public void send() {
System.out.println("具体同事类1发出请求。");
mediator.relay(this); //请中介者转发
}
}
//具体同事类
class ConcreteColleague2 extends Colleague {
public void receive() {
System.out.println("具体同事类2收到请求。");
}
public void send() {
System.out.println("具体同事类2发出请求。");
mediator.relay(this); //请中介者转发
}
}

程序的运行结果如下:


具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

应用场景

前面分析了中介者模式的结构与特点,下面分析其以下应用场景。
当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

java源码中的体现

在看其他人写的关于Timer 的中介者设计模式,我觉得写的都不是很清楚。我大概用源码来解释一下,顺便再分析一下Timer的所有关联类的源码:


private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}

说明:所有的schedule方法都调用了sched ,那这个类的主要作用是啥呢?



将timertask加入到队列里,然后从队列里取出min任务(二叉堆的数据结构,下面会说明),判断如果min任务等于当前任务的话让队列wait的状态变为运行状态,如果不等于的话,那么线程的mainloop方法肯定是一直再运行状态的,其他任务就可以依次执行



看如下的源码


private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die

// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}

Timer相当于中介者来执行队列里的任务,用户只管将任务抛给timer就可以了。


如下详细timer源码分析

在Java中,很常见的一个定时器的实现就是 Timer 类,用来实现定时、延迟执行、周期性执行任务的功能。


Timer 是定义在 java.util 中的一个工具类,提供简单的实现定时器的功能。和它配合使用的,是 TimerTask 类,这是对一个可以被调度的任务的封装。使用起来非常简单,如下示例:


// 定义一个可调度的任务,继承自 TimerTask
class FooTimerTask extends TimerTask {

@Override
public void run() {
// do your things
}
}

// 初始化Timer 定时器对象
Timer timer = new Timer("barTimer");

// 初始化需要被调度的任务对象
TimerTask task = new FooTimerTask();

// 调度任务。延迟1000毫秒后执行,之后每2000毫秒定时执行一次
timer.schedule(task, 1000, 2000);

以上,就是一个简单的使用Timer 的示例,下文将会分析Timer的源码实现。


概述

在Timer 机制中,涉及到的关键类如下:



  • Timer: 主要的调用的,提供对外的API;
  • TimerTask: 是一个抽象类,定义一个任务,继承自Runnable
  • TimerThread: 继承自 Thread,是一个自定义的线程类;
  • TaskQueue: 一个任务队列,包含有当前Timer的所有任务,内部使用二叉堆来实现。

以上几个关键类的引用关系如下:
在这里插入图片描述
简要描述的话,是:

1个 TimerThread —-> 实现1个 线程


1个 Timer对象 —-> 持有1个 TimerThread 对象


1个 Timer对象 —-> 持有1个 TimerQueue 对象


1个 TimerQueue 对象 —-> 持有 n个 TimerTask 对象


源码分析

Timer类的源码分析
源码分析的话,我们最好是按照Timer 的使用流程来分析。 首先,是Timer 的创建:

// Timer有四个构造方法,但是本质上其实是做的相同的事情,即
// 1. 使用name 和 isDeamon 两个参数给 thread 对象做了参数设置;
// 2. 调用 thread 的 start() 方法启动线程
public Timer() {
this("" + serialNumber());
}

public Timer(boolean isDaemon) {
this("" + serialNumber(), isDaemon);
}


public Timer(String name) {
thread.setName(name);
thread.start();
}

public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}

那么,或许大家会有一个疑问,thread 成员的初始化呢?这个时候,在代码里面找,就能发现:



// 这两个成员都是直接在声明的时候进行了初始化。
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);

可以看到 thread 和 queue两个成员都是在声明的时候直接初始化的,并且有意思的是,两个成员都是 final 类型的,这也就意味着这两个成员一旦创建就不会再改了,等于说把 thread、queue 和 Timer 对象这三者的生命周期强行绑定在一起了,大家一起创建,并且一经创建将会无法改变。


然后,创建了Timer 后,与之相关的队列也已经创建成功,而且相关联的线程也启动了,就可以进行任务的调度了,我们看下它的任务调度方法:



// Timer 包含有一组重载方法,参数为以下几个:
// 1. TimerTask task:需要被调度的任务
// 2. long delay: 指定延迟的时间;
// 3. long period: 指定调度的执行周期;
schedule(TimerTask task, long delay, long period)

多个重载的调度方法在经过一些一些列的状态判断、参数设置、以及把delay时间转换成实际的执行时间等之后, 最终完成该功能的是 sched 方法,详情见注释部分:


这里涉及到一个需要留意的点,是在调用schedule 方法的时候,会根据TimerTask 的类型来进行不同的计算,进而给TimerTask设置不同的 period 参数,TimerTask 的类型有以下几种:



  • 非周期性任务;对应 TimerTask.period 值为0;
  • 周期性任务,但是没有delay值,即立即执行;对应 TimerTask.period 值为正数;
  • 周期性任务,同时包含有 delay值;对应 TimerTask.period 值为负数;

在schedule 方法中,会



// 执行任务调度的方法
// 这里的 time 已经是经过转换的,表示该task 需要被执行的时间戳
private void sched(TimerTask task, long time, long period) {
// 参数的合法性检查
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

// 核心的调度逻辑
// 由于是在多线程环境中使用的,这里为了保证线程安全,使用的是 synchronized 代码段
// 对象锁使用的是在 Timer 对象中唯一存在的 queue 对象
synchronized(queue) {

// thread.newTasksMayBeScheduled 是一个标识位,在timer cancel之后 或者 thread 被停止后该标识位会被设为false
// newTasksMayBeScheduled 为false 则表示该timer 的关联线程已经停止了。
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

// 这里是把外部的参数,如执行时间点、执行周期、设置状态等等。
// 这里为了线程安全的考虑,使用对 task 内部的 lock 对象加锁来保证。
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

// 最后,把新的 task 添加到关联队列里面
queue.add(task);

// 这里,会使用打 TimerQueue 对象的 getMin() 方法,这个方法是获取到接下来将要被执行的TimerTask 对象
// 这里的逻辑是check 新添加的 task 对象是不是接下来马上会被执行
// 如果刚添加的对象是需要马上执行的话,会使用 queue.notify 来通知在等待的线程。

// 那么,会有谁在等待这个 notify 呢?是TimerThread 内部,TimerThread 会有一个死循环,在不停从queue中取任务来执行
// 当queue为空的时候,TimerThread 会进行 queue.wait() 来进行休眠的状态,直到有新的来任务来唤醒它
// 下面的代码就是,当queue为空的时候,这个判断条件会成立,然后就通知 TimerThread 重新唤醒
// 当然,下面的条件成立也不全是 queue 为空的情况下
if (queue.getMin() == task)
queue.notify();
}
}

TimerTask 的源码分析

接下来,本文将会分析 TimerTask 的源码。相对于Timer 来说,它的源码其实很简单,TimerTask 是实现了Runnable 接口,同时也是一个抽象类,它并没有对 Runnable 的 run() 方法提供实现,而是需要子类来实现。


它对外提供了以下几个功能:


包含有一段可以执行的代码(实现的Runnable 接口的run方法)
包含状态的定义。它有一个固定的状态:VIRGIN(新创建)、SCHEDULED(被调度进某一个 timer 的队列中了,但是还没有执行到)、EXECUTED(以及执行过了)、CANCELLED(任务被取消了)。
包含有取消的方法。
包含有获取下一次执行时间的方法。

相关的源码如下:



// 取消该任务
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}

// 根据执行周期,和设置的执行时间,来确定Task的下一次执行时间。
public long scheduledExecutionTime() {
synchronized(lock) {
// 其中,period 的值分为3种情况:
// 取值为0: 表示该Task是非周期性任务;
// 取值为正数: 表示该Task 是立即执行没有delay的周期性任务,period 的数值表示该Task 的周期
// 取值为负数: 表示该Task 是有 delay 的周期性任务,period 相反数是该Task 的周期
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}

TimerThread 的源码分析

TimerThread 首先是一个 Thread 的子类,而且我们知道,在Java中,一个Thread 的对象就是代表了一个JVM虚拟机线程。那么,这个 TimerThread 其实也就是一个线程。


对于一个线程来说,那么它的关键就是它的 run() 方法,在调用线程的 start() 方法启动线程之后,接下来就会执行线程的 run() 方法,我们看下 TimerThread 的run() 方法:



public void run() {
try {
// 启动 mainLoop() 方法,这是一个阻塞方法,正常情况下会一只阻塞在这里
// 当 mainLoop() 执行完毕的时候,也即是这个线程退出的时候。
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
// 做一些收尾工作
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}

从以上可以明确得看出,TimerThread 里的实现是调用 mainLoop() 启动了一个死循环,这个死循环内部的工作就是这个线程的具体工作了,一旦线程的死循环执行完毕,线程的 run 方法就执行完了,线程紧接着就退出了。熟悉Android的朋友可能已经觉得这里的实现非常眼熟了,没错,这里的实现和Android平台的 Handler + HandlerThread + Looper 的机制非常相像,可以认为Android平台最初研发这套机制的时候,就是参考的Timer 的机制,然后在上面做了些升级和适合Android平台的一些改动。


下面是 mainLoop() 方法:



private void mainLoop() {
// 一个死循环
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 会等待到队列不为空,结合上面章节的分析,我们可以确定在新添加 TimerTask 到queue中的时候
// 会触发到 queue.notify() 然后通知到这里。
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();

// queue 为空,说明 timer 被取消了
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die


long currentTime, executionTime;
// 又一次看到这个 queue.getMin() ,这个是根据接下来的执行时间来获取下一个需要被执行的任务
task = queue.getMin();

// 需要修改 task对象的内部数值,使用synchronized 保证线程安全
synchronized(task.lock) {
// TimerTask 有多种状态,一旦一个 TimerTask 被取消之后,它就不会被执行了。
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}

// 获取到当前时间,和这个取出来的task 的下一次执行时间
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;

// 这里会check 当前这个 task 是不是已经到时间了
// 这里会把是否到时间了这个状态保存在 taskFired 里面
if (taskFired = (executionTime<=currentTime)) {
// 根据上文的分析,TimerTask 根据 task.period 值的不同,被分为3种类型
// 这里的 task.period == 0 的情况,是对应于一个非周期性任务
if (task.period == 0) {
// 非周期性任务,处理完就完事了,改状态,移除队列
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
// 周期性任务,会被重新调度,也不会被移除队列
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}

// 这里是另一个会等待的地方,这个是为了等待任务的到期,等待时间就是距离到执行之间的时长
if (!taskFired)
queue.wait(executionTime - currentTime);
}

// taskFired 变量经过上面的步骤以及判断过了,如果是 true,说明task以及到时间了
// 到时间就运行完事。
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}

TimerThread 中除了上面的主要逻辑之外,还有一些需要关注的地方,那就是它持有一个 TimerQueue 的对象,这个对象是在创建的时候外部传进来的,也是和当前的Timer 关联的TimerQueue:



// 这里的官方注释,说明了为什么是在TimerThread 中引用了 TimerQueue而不是引用了 Timer。
// 这么做是为了避免循环引用(因为Timer中引用了TimerThread),进而避免循环引用可能导致的JVM gc 失败的问题
// 我们都知道,Java 是一门通用的语言,虽然官方的HotSpot JVM中是能解决循环引用的GC问题的,但是这并不意味着
// 其他第三方的JVM也能解决循环引用导致的GC问题,所以这里干脆就避免了循环引用。

/**
* Our Timer's queue. We store this reference in preference to
* a reference to the Timer so the reference graph remains acyclic.
* Otherwise, the Timer would never be garbage-collected and this
* thread would never go away.
*/
private TaskQueue queue;

TimerQueue 的源码分析(主要是实现一个二叉堆)

TimerQueue 的逻辑上是一个队列,所有它包含有一个队列常见的那些方法,如 size()、add()、clear()等方法。下面我们找一些重要的方法进行分析:


首先,在上文的分析中,我们以及见过TimeQueue 的 getMin() 方法了,这个方法是获取当前的队列里面,接下来应该被执行的TimerTask,也就是说,是执行时间点 数值最小的那一个,那么我们就先看下它的源码:


/**
* Return the "head task" of the priority queue. (The head task is an
* task with the lowest nextExecutionTime.)
*/
TimerTask getMin() {
return queue[1];
}

What??? 就这吗?为啥这么简单?为啥就返回 queue[1] 就对了?


你是不是也有这样的疑问,那么带着疑问往下看吧。


接下来,是添加一个TimerTask 到队列中:



// 内部存放TimerTask 数据的,是一个数组,设置的数组初始大小是128
private TimerTask[] queue = new TimerTask[128];

// 存放当前的TimerTask 的数量
// 而且 TimerTask 是存放在 [1 - size] 位置的,数组的第0位置没有数据
// 至于为什么要 存放在 [1 - size] 请看下文。
private int size = 0;

/**
* Adds a new task to the priority queue.
*/
void add(TimerTask task) {
// check 下数据的容量是否还够添加,不够的话会先进行数组的扩容
// 这扩容一次就是2倍增加
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);

// 把新的TimerTask 放在数组的最后一个位置
// size 的初始化值是0,从这里可以看出来,这里会先把size自增1,然后再添加到数组中
// 其实是从数组位置的 1 开始添加 TimerTask 的,0的位置是空的
queue[++size] = task;

// 然后调用了这个数据上浮的方法
fixUp(size);
}

从上文看出,add 方法本身也没什么奇特的,就是很简单地把新的 TimerTask 放在了数据的最新的位置,只是里面调用了一下另一个方法 fixUp() ,好,那么我们接着分析这个方法:





// 从上文可以看出,参数 k 是当前的数组size 值,也是最后一个TimerTask 的下标索引
private void fixUp(int k) {
// 首先,这是一个循环,循环条件是 k > 1
while (k > 1) {
// 位运算,操作,把 k 右移一位,得到的结果是:偶数相当于除以2,奇数相当于先减1再除以2
int j = k >> 1;

// 比较 j 和 k 两个位置的下次执行时间,j 不大于 k 的话,就停止循环了
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;

// j 大于 k 位置的时间的话,就要进行下面的动作
// 这是一个典型的交换操作
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;

// k 值缩小到j,去逼近循环条件 k>1
k = j;
}
}

看了上面对 fixUp() 的分析,是不是仍然一脸懵?或许也有些熟悉算法的朋友已经觉察出些什么了,那么这个地方的逻辑是什么呢?


有了右移一位、[1, size]的区间等蛛丝马迹,我想聪明的你已经猜出来了,这个数组queue 里面,是存放了一个完全二叉树。


在发现 queue 数组是一个二叉树之后,再去理解上面的 fixUp() 方法其实就很简单了,里面的过程是这样的:


从二叉树的最后一个叶子结点开始循环;
获取这个叶子结点的父结点(完全二叉树中对应的父结点的索引是:子结点位运算右移一位得到的)
判断父结点和子结点中对应的 TimerTask 的 nextExecutionTime 的大小,如果父比子的小,则停止循环;如果父比子的大,则交互负责结点;
重复以上循环,直到遍历到根结点;

通过以上分析,能发现在每一次新增一个结点后,使用 fixUp(),方法直接对整个二叉树进行了重排序,使得 TimerTask 的nextExecutionTime 值最小的结点,永远被放置在了二叉树的根结点上,也即是queue[1]。这也就搞明白了为什么 getMin 的实现,是直接获取的 queue[1] 。


同样的道理,在每一次执行 Timer.purge() 方法,清理了TimerQueue中已经取消的Task之后,会执行另一个 fixDown() 方法,它的逻辑正好是和 fixUp() 相反的,它是从根结点开始遍历的,然后到达每一个叶子结点以整理这个二叉树,这里就不再赘述。


回过头来,我们再看下TimerQueue中的实现,会发现它其实是一个二叉堆,二叉堆是一个带有权重的二叉树,这里不再多说。


总结

通过以上的分析,总的来说,就是每一个 Timer对象中,包含有一个线程(TaskThread)和一个队列(TaskQueue)。TaskQueue 的实现是一个二叉堆(Binary Heap)的结构,二叉堆的每一个节点,就是 TimerTask 的对象。


收起阅读 »

Android RecyclerView 通用适配器

使用方式【最新版本号以这里为准】由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关...
继续阅读 »

使用方式

【最新版本号以这里为准】

由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!

#last-version请查看上面的最新版本号

#只支持AndroidX

#从1.4.5版本开始GroupId、ArtifactId均有更新,请按如下方式引用
implementation "com.lwkandroid.library:rcvadapter:last-version"

基础功能

  • 快速实现适配器,支持多种ViewType模式
  • 支持添加HeaderView、FooterView、EmptyView
  • 支持滑到底部加载更多
  • 支持每条Item显示的动画
  • 支持嵌套Section(1.1.0版本新增)
  • 支持悬浮标签StickyLayout(1.2.0版本新增)

效果图






使用方式

1. 当Item样式一样时,只需继承RcvSingleAdapter<T>即可,示例:

public class TestSingleAdapter extends RcvSingleAdapter<TestData>
{
public TestSingleAdapter(Context context, List<TestData> datas)
{
super(context, android.R.layout.simple_list_item_1, datas);
}

@Override
public void onBindView(RcvHolder holder, TestData itemData, int position)
{
//在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
holder.setTvText(android.R.id.text1, itemData.getContent());
}
}


2. 当Item样式不一样时,即存在多种ViewType类型的Item,需要将每种ViewType的Item单独实现,再关联到RcvMultiAdapter<T>中,示例:

//第一步:每种Item分别继承RcvBaseItemView<T>
public class LeftItemView extends RcvBaseItemView<TestData>
{
@Override
public int getItemViewLayoutId()
{
//这里返回该Item的布局id
return R.layout.layout_item_left;
}

@Override
public boolean isForViewType(TestData item, int position)
{
//这里判断何时引用该Item
return position % 2 == 0;
}

@Override
public void onBindView(RcvHolder holder, TestData testData, int position)
{
//在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
holder.setTvText(R.id.tv_left, testData.getContent());
}
}

//第二步:将所有Item关联到适配器中
public class TestMultiAdapter extends RcvMultiAdapter<TestData>
{
public TestMultiAdapter(Context context, List<TestData> datas)
{
super(context, datas);
//只需在构造方法里将所有Item关联进来,无论多少种ViewType都轻轻松松搞定
addItemView(new LeftItemView());
addItemView(new RightItemView());
}
}


3.优雅的添加HeaderView、FooterView、EmptyView,只需要在RecyclerView设置LayoutManager后调用相关方法即可:

//要先设置LayoutManager
mRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));

//添加HeaderView(若干个)
mAdapter.addHeaderView(headerView01,headerView02,headerView03...);

//添加FooterView(若干个)
mAdapter.addFooterView(footerView01,footerView02,footerView03...);

//添加EmptyView(只能设置一个)
//设置了EmptyView后,当数据量为0的时候会显示EmptyView
mAdapter.setEmptyView(emptyView);
或者
mAdapter.setEmptyView(layoutId);


4.设置滑动到底部自动加载更多,先上示例代码吧:

自1.4.3版本开始删除了之前的调用方式

//可以先设置加载样式,继承RcvBaseLoadMoreView实现自定义样式
//不设置的话会使用默认的样式,参考RcvDefLoadMoreView源码
RcvDefLoadMoreView loadMoreView = new RcvDefLoadMoreView.Builder()
.setBgColor(Color.GREEN)
.setTextColor(Color.RED)
.build(this);
mAdapter.setLoadMoreLayout(loadMoreView);
//再开启并设置监听
mAdapter.enableLoadMore(true);
mAdapter.setOnLoadMoreListener(RcvLoadMoreListener listener);
//禁止加载更多,通常用在配合下拉刷新的过程中
mAdapter.enableLoadMore(false);

注:
① 默认的样式实现是类RcvDefLoadMoreView
② 如需自定义样式,只需继承RcvBaseLoadMoreView,只要重写各状态UI的实现,无须关心状态切换,可参考RcvDefLoadMoreView内的实现方式。

5.设置Item显示动画,先直接上代码:

//使用默认的动画(Alpha动画)
mAdapter.enableItemShowingAnim(true);

//使用自定义动画
mAdapter.enableItemShowingAnim(true, ? extends RcvBaseAnimation);

注:
①默认动画的实现是类RcvAlphaInAnim
②自定义样式需要继承RcvBaseAnimation,可参考RcvAlphaInAnim内部实现。

6.设置Item点击监听:

    //设置OnItemClickListener
mAdapter.setOnItemClickListener(new RcvItemViewClickListener<TestData>()
{
@Override
public void onItemViewClicked(RcvHolder holder, TestData testData, int position)
{
//onClick回调
}
});

//设置OnItemLongClickListener
mAdapter.setOnItemLongClickListener(new RcvItemViewLongClickListener<TestData>()
{
@Override
public void onItemViewLongClicked(RcvHolder holder, TestData testData, int position)
{
//onLongClick回调
}
});


7. 添加分割线,直接上代码:

1.2.9版本针对分割线进行了重写,原有方法不变,新增支持自定义颜色和部分快速创建的方法:

#适用于LinearLayoutManager
//创建默认竖直排列的分割线
RcvLinearDecoration.createDefaultVertical(Context context);
//创建自定义色值默认竖直排列的分割线
RcvLinearDecoration.createDefaultVertical(int color);
//创建默认水平排列的分割线
RcvLinearDecoration.createDefaultHorizontal(Context context);
//创建自定义色值默认水平排列的分割线
RcvLinearDecoration.createDefaultHorizontal(int color);
//构造方法:默认Drawable分割线
new RcvLinearDecoration(Context context, int orientation);
//构造方法:自定义Drawable分割线
new RcvLinearDecoration(Context context, Drawable drawable, int orientation);
//构造方法:自定义Drawable分割线
new RcvLinearDecoration(Context context, @DrawableRes int drawableResId, int orientation);
//构造方法:自定义Color分割线(宽度或者高度默认1px)
new RcvLinearDecoration(@ColorInt int color, int orientation);
//构造方法:自定义Color分割线
new RcvLinearDecoration(@ColorInt int color, int size, int orientation);

#适用于GridLayoutManager、StaggeredGridLayoutManager
//创建默认分割线
RcvGridDecoration.createDefault(Context context);
//创建自定义色值默认分割线
RcvGridDecoration.createDefault(int color);
//构造方法:默认Drawable的分割线
new RcvGridDecoration(Context context);
//构造方法:自定义Drawable的分割线
new RcvGridDecoration(Context context, Drawable drawable);
//构造方法:自定义Drawable的分割线
new RcvGridDecoration(Context context, @DrawableRes int drawableResId);
//构造方法:自定义Color的分割线(默认分割线宽高均为1px)
new RcvGridDecoration(@ColorInt int color);
//构造方法:自定义Color的分割线
new RcvGridDecoration(@ColorInt int color, int width, int height);

注:
①是直接设置给RecyclerView的,不是设置给适配器的,不要看错哦
②支持自定义drawable当分割线

8.嵌套Section,稍微复杂一点,配合代码讲解:

1.4.0版本开始删除以前的使用方法,采用下面的方式

带有Section功能的适配器为RcvSectionMultiLabelAdapterRcvSectionSingleLabelAdapter,需要指定两个泛型,第一个代表Section,第二个代表普通数据Data, 两者都支持多种Data类型的子布局,唯一不同的是,RcvSectionMultiLabelAdapter还支持多种Section类型的子布局,但不可以和RcvStickyLayout联动,而RcvSectionSingleLabelAdapter 仅支持一种Section类型的子布局,但是可以和RcvStickyLayout联动。需要注意的是,传给适配器的数据均需要自行预处理,用RcvSectionWrapper封装后才可传入适配器。

#只有一种Section类型,配合多种Data类型的适配器
public class TestSectionAdapter extends RcvSectionSingleLabelAdapter<TestSection, TestData>
{
public TestSectionAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
{
super(context, datas);
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
{
return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
}

@Override
public int getSectionLabelLayoutId()
{
return R.layout.layout_section_label;
}

@Override
public void onBindSectionLabelView(RcvHolder holder, TestSection section, int position)
{
holder.setTvText(R.id.tv_section_label, section.getSection());
}

//第一种Data ItemView
private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_long;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_long);
textView.setBackgroundColor(Color.GREEN);
textView.setText("第一种数据类型:" + wrapper.getData().getContent());
}
}

//第二种Data ItemView
private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_short;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_short);
textView.setBackgroundColor(Color.RED);
textView.setText("第二种数据类型:" + wrapper.getData().getContent());
}
}
}

#多种Section类型,配合多种Data类型的适配器
public class TestSectionMultiLabelAdapter extends RcvSectionMultiLabelAdapter<TestSection, TestData>
{
public TestSectionMultiLabelAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
{
super(context, datas);
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createLabelItemViews()
{
return new RcvBaseItemView[]{new LabelItemView01(), new LabelItemView02()};
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
{
return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
}


//第一种Label ItemView
private class LabelItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.layout_section_label;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return item.isSection() && item.getSection().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
}
}

//第二种Label ItemView
private class LabelItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.layout_section_label02;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return item.isSection() && item.getSection().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
}
}

//第一种Data ItemView
private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_long;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_long);
textView.setBackgroundColor(Color.GREEN);
textView.setText("第一种数据类型:" + wrapper.getData().getContent());
}
}

//第二种Data ItemView
private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_short;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_short);
textView.setBackgroundColor(Color.RED);
textView.setText("第二种数据类型:" + wrapper.getData().getContent());
}
}
}

注:
①传给适配器的数据集合内实体类必须经过RcvSectionWrapper包装。
②向外公布的方法(例如点击监听)的实体类泛型不能传错。

9.悬浮标签StickyLayout

适配器方面无需改动,直接使用RcvSectionSingleLabelAdapter即可,在RecyclerView同级布局下添加RcvStickyLayout,然后在代码中关联起来即可:

    // xml布局中添加RcvStickyLayout:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_sticky"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<com.lwkandroid.rcvadapter.ui.RcvStickyLayout
android:id="@+id/stickyLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</FrameLayout>



//代码中关联RecyclerView
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rcv_sticky);
/...省略设置RecyclerView的LayoutMananger和Adapter.../
RcvStickyLayout stickyLayout = (RcvStickyLayout) findViewById(R.id.stickyLayout);
stickyLayout.attachToRecyclerView(recyclerView);

上面就是大部分基础功能的使用方法了,想了解更多方法请看源码。

混淆配置

-dontwarn com.lwkandroid.rcvadapter.**
-keep class com.lwkandroid.rcvadapter.**{*;}


待实现功能

  • 暂时未想到

开源参考

  1. https://github.com/hongyangAndroid/baseAdapter
  2. https://github.com/CymChad/BaseRecyclerViewAdapterHelper
收起阅读 »

一行代码解决RxJava 内存泄漏

xLifeRxLife,相较于trello/RxLifecycle、uber/AutoDispose,具有如下优势:直接支持在主线程回调支持在子线程订阅观察者简单易用,学习成本低性能更优,在实现上更加简单友情提示: RxLife与RxHttp搭配使用,味道更佳...
继续阅读 »

xLife

RxLife,相较于trello/RxLifecycleuber/AutoDispose,具有如下优势:

  • 直接支持在主线程回调
  • 支持在子线程订阅观察者
  • 简单易用,学习成本低
  • 性能更优,在实现上更加简单

友情提示: RxLife与RxHttp搭配使用,味道更佳

RxLife详细介绍:https://juejin.im/post/5cf3e1235188251c064815f1

Gradle引用

jitpack添加到项目的build.gradle文件中,如下:

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

注:RxLife 2.1.0 版本起,已全面从JCenter迁移至jitpack

新版本仅支持AndroidX项目

dependencies {
//kotlin协程
implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0'

//rxjava2
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0'

//rxjava3
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0'
}

注意:RxJava2 使用Rxlife.asXxx方法;RxJava3使用Rxlife.toXxx方法

非AndroidX项目

非AndroidX项目,请使用旧版本RxLife

implementation 'com.rxjava.rxlife:rxlife:2.0.0'

由于Google在19年就停止了非AndroidX库的更新,故rxlife旧版本不再维护,请尽快将项目迁移至AndroidX

#Usage

1、FragmentActivity/Fragment

FragmentActivity/Fragment销毁时,自动关闭RxJava管道

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.as(this)) //此时的this FragmentActivity/Fragment对象
.subscribe(aLong -> {
Log.e("LJX", "accept =" + aLong);
});

2、View

View被移除时,自动关闭RxJava管道

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.as(this)) //此时的this 为View对象
.subscribe(aLong -> {
Log.e("LJX", "accept =" + aLong);
});

3、ViewModel

Activity/Fragment销毁时,自动关闭RxJava管道,ViewModel需要继承ScopeViewModel类,如下

public class MyViewModel extends ScopeViewModel {

public MyViewModel(@NonNull Application application) {
super(application);
}

public void test(){
Observable.interval(1, 1, TimeUnit.SECONDS)
.as(RxLife.asOnMain(this)) //继承ScopeViewModel后,就可以直接传this
.subscribe(aLong -> {
Log.e("LJX", "MyViewModel aLong=" + aLong);
});
}
}

注意: 一定要在Activity/Fragment通过以下方式获取ViewModel对象,否则RxLife接收不到生命周期的回调


MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);

4、任意类

Activity/Fragment销毁时,自动关闭RxJava管道,任意类需要继承BaseScope类,如P层:

public class Presenter extends BaseScope {

public Presenter(LifecycleOwner owner) {
super(owner); //添加生命周期监听
}

public void test(){
Observable.interval(1, 1, TimeUnit.SECONDS)
.as(RxLife.as(this)) //继承BaseScope后,就可以直接传this
.subscribe(aLong -> {
Log.e("LJX", "accept aLong=" + aLong);
});
}
}

5、kotlin用户

由于as是kotlin中的一个关键字,所以在kotlin中,我们并不能直接使用as(RxLife.as(this)),可以如下编写

Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
.`as`(RxLife.`as`(this))
.subscribe { aLong ->
Log.e("LJX", "accept=" + aLong)
}

当然,相信没多少人会喜欢这种写法,故,RxLife针对kotlin用户,新增更为便捷的写法,如下:

Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
.life(this)
.subscribe { aLong ->
Log.e("LJX", "accept=" + aLong)
}

使用life 操作符替代as操作符即可,其它均一样

6、小彩蛋

asOnMain操作符

RxLife还提供了asOnMain操作符,它可以指定下游的观察者在主线程中回调,如下:

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.asOnMain(this))
.subscribe(aLong -> {
//在主线程回调
Log.e("LJX", "accept =" + aLong);
});

//等价于
Observable.timer(5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.as(RxLife.as(this))
.subscribe(aLong -> {
//在主线程回调
Log.e("LJX", "accept =" + aLong);
});

kotlin 用户使用lifeOnMain替代asOnMain操作符,其它均一样

注意: RxLife类里面as操作符,皆适用于Flowable、ParallelFlowable、Observable、Single、Maybe、Completable这6个被观察者对象

混淆

RxLife作为开源库,可混淆,也可不混淆,如果不希望被混淆,请在proguard-rules.pro文件添加以下代码

-keep class com.rxjava.rxlife.**{*;}


代码下载:327744707-rxjava-RxLife-master.zip

收起阅读 »

Android替换系统dialog风格后的通用提示框工具类

DialogUtilsApp一、介绍替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整APP 使用示例项目,libs下含有已编译最新的aar资源。dialogutilslib a...
继续阅读 »

DialogUtilsApp

一、介绍

替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整

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

二、工程引入工具包准备

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

dependencies {
implementation files('libs/DialogUtilsLib-release.aar')
...
}

三、使用

注意下方只做了基础展示,dialog的都会返回对应的utils对象,registerActivityLifecycleCallbacks方法设置后,activity销毁时会自动把显示在此activity上的dialog一起关闭。

  • application初始化设置
public class App extends Application {

@Override
public void onCreate() {
super.onCreate();

//初始化dialog工具类设置
DialogLibInitSetting.getInstance()
//设置debug
.setDebug(BuildConfig.DEBUG)
//注册全局activity生命周期监听
.registerActivityLifecycleCallbacks(this);

}
}
  • 普通dialog
            DialogLibCommon.create(this)
.setMessage("普通对话框1")
.setAlias("text1")
.setOnBtnMessage(()->{
//描述区域点击时触发
})
.noShowCancel()
.show();
  • 自定义dialog
            ImageView imageView = new ImageView(this);
imageView.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher));
DialogLibCustom.create(this)
.noShowCancel()
.setAlias("text2")
.show(imageView);
  • 输入型dialog
            DialogLibInput.create(this)
.setMessage("输入信息")
.setAlias("text3")
//自动弹出键盘
.setPopupKeyboard()
.setOnBtnOk(str -> {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
return true;
})
.show();
  • 等待型dialog
            DialogLibLoading.create(this)
.setTimeoutClose(2000)
.setAlias("text4")
.setOnLoading(() -> {
Toast.makeText(MainActivity.this, "我是显示对话框前触发的", Toast.LENGTH_SHORT).show();
})
.show();
  • 完全自定义型dialog
            final DialogLibAllCustom dialog = DialogLibAllCustom.create(this)
.setCancelable(true)
.setAlias("text5");

TextView view = new TextView(this);
view.setBackgroundResource(R.color.design_default_color_secondary);
view.setText("这是一个完全自定义布局的对话框,对话框显示后需要手动关闭");
view.setOnClickListener(v2 -> {
dialog.closeDialog();
});

dialog.show(view);
  • 密码输入型dialog
              DialogLibInput.create(this)
.setMessage("123")
.setLength(6)
.setInputType(EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD)
.setAlias("text6")
//设置显示密码隐藏/显示图片
.setShowLookPassword()
//自动弹出键盘
.setPopupKeyboard()
.setOnBtnOk(str -> {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
return true;
})
.show();

四、资源覆盖,改变颜色、字体大小、默认文字

  • colors下可覆盖资源及注释,默认黑色和白色不建议覆盖,前景色:字体的颜色,背景色:布局的背景色
<resources>
<!--黑色-->
<color name="dialog_utils_lib_black">#FF000000</color>
<!--白色-->
<color name="dialog_utils_lib_white">#FFFFFFFF</color>

<!--dialog的标题文字的前景色,适用于所有带标题的dialog-->
<color name="dialog_utils_lib_title_fg">@color/dialog_utils_lib_black</color>
<!--dialog的 确认 按钮文字的前景色-->
<color name="dialog_utils_lib_ok_fg">@color/dialog_utils_lib_white</color>
<!--dialog的 取消 按钮文字的前景色-->
<color name="dialog_utils_lib_cancel_fg">@color/dialog_utils_lib_white</color>
<!--dialog的 确认 按钮文字的背景色-->
<color name="dialog_utils_lib_ok_bg">#22C5A3</color>
<!--dialog的 取消 按钮文字的背景色-->
<color name="dialog_utils_lib_cancel_bg">#F8A01A</color>
<!--dialog的输入框下方显示2个按钮时,中间分隔的背景色-->
<color name="dialog_utils_lib_button_split_bg">@color/dialog_utils_lib_white</color>

<!--dialog的内容文字的前景色,适用于 DialogLibCommonUtils-->
<color name="dialog_utils_lib_content_fg">@color/dialog_utils_lib_black</color>

<!--dialog的输入框文字的前景色,适用于 DialogLibInputUtils-->
<color name="dialog_utils_lib_input_fg">@color/dialog_utils_lib_black</color>
<!--dialog的输入框下方分割线的背景色,适用于 DialogLibInputUtils-->
<color name="dialog_utils_lib_input_split_line">@color/dialog_utils_lib_ok_bg</color>

<!--dialog的加载框加载等待区域的背景色-->
<color name="dialog_utils_lib_loading_content_bg">#FFc4c4c4</color>
<!--dialog的加载框加载等待区域文字提示的前景色-->
<color name="dialog_utils_lib_loading_content_text_fg">@color/dialog_utils_lib_white</color>
</resources>
  • dimens下字体大小资源
<resources>
<dimen name="dialog_utils_lib_text_size_normal">14sp</dimen>

<!--标题字体大小,统一设定-->
<dimen name="dialog_utils_lib_title_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--确定 字体大小,统一设定-->
<dimen name="dialog_utils_lib_ok_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--取消 字体大小,统一设定-->
<dimen name="dialog_utils_lib_cancel_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--内容 字体大小,适用于 DialogLibCommonUtils的提示内容区域-->
<dimen name="dialog_utils_lib_content_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--输入框 字体大小,适用于 DialogLibInputUtils 输入区域-->
<dimen name="dialog_utils_lib_input_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--加载框 字体大小,适用于 DialogLibLoadingUtils 提示内容区域-->
<dimen name="dialog_utils_lib_loading_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>

<!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,竖屏时的系数-->
<item name="dialog_utils_lib_portrait_width_factor" format="float" type="dimen">0.85</item>
<!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,横屏时的系数-->
<item name="dialog_utils_lib_landscape_width_factor" format="float" type="dimen">0.5</item>
</resources>
  • strings下资源定义,注意:如果你的项目存在多语言,则必须覆盖
<resources>
<string name="dialog_utils_lib_ok">确定</string>
<string name="dialog_utils_lib_cancel">取消</string>
<string name="dialog_utils_lib_default_title">提示</string>
<string name="dialog_utils_lib_data_processing">数据处理中…</string>
</resources>
  • mipmap下资源定义,注意:此2张图片为密码输入时显示/隐藏按钮的图片,png格式
dialog_utils_lib_password_hide 隐藏图片命名
dialog_utils_lib_password_show 显示图片命名

代码下载:mjsoftking-dialog-utils-app-master.zip

收起阅读 »

30秒上手的HTTP请求库

RxHttp主要优势1. 30秒即可上手,学习成本极低2. 史上最优雅的支持 Kotlin 协程3. 史上最优雅的处理多个BaseUrl及动态BaseUrl4. 史上最优雅的对错误统一处理,且不打破Lambda表达式5. 史上最优雅的文件上传/下载/断点下载/...
继续阅读 »

RxHttp


主要优势

1. 30秒即可上手,学习成本极低

2. 史上最优雅的支持 Kotlin 协程

3. 史上最优雅的处理多个BaseUrl及动态BaseUrl

4. 史上最优雅的对错误统一处理,且不打破Lambda表达式

5. 史上最优雅的文件上传/下载/断点下载/进度监听,已适配Android 10

6. 支持Gson、Xml、ProtoBuf、FastJson等第三方数据解析工具

7. 支持Get、Post、Put、Delete等任意请求方式,可自定义请求方式

8. 支持在Activity/Fragment/View/ViewModel/任意类中,自动关闭请求

9. 支持全局加解密、添加公共参数及头部、网络缓存,均支持对某个请求单独设置

请求三部曲

上手教程

30秒上手教程:30秒上手新一代Http请求神器RxHttp

协程文档:RxHttp ,比Retrofit 更优雅的协程体验

掘金详细文档:RxHttp 让你眼前一亮的Http请求框架

wiki详细文档:https://github.com/liujingxing/rxhttp/wiki (此文档会持续更新)

自动关闭请求用到的RxLife类,详情请查看RxLife库

更新日志      遇到问题,点击这里,99%的问题都能自己解决

上手准备

Maven依赖点击这里

1、RxHttp目前已适配OkHttp 3.12.0 - 4.9.1版本(4.3.0版本除外), 如你想要兼容21以下,请依赖OkHttp 3.12.x,该版本最低要求 API 9

2、asXxx方法内部是通过RxJava实现的,而RxHttp 2.2.0版本起,内部已剔除RxJava,如需使用,请自行依赖RxJava并告知RxHttp依赖的Rxjava版本

必须

jitpack添加到项目的build.gradle文件中,如下:

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

注:RxHttp 2.6.0版本起,已全面从JCenter迁移至jitpack

//使用kapt依赖rxhttp-compiler时必须
apply plugin: 'kotlin-kapt'

android {
//必须,java 8或更高
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation 'com.github.liujingxing.rxhttp:rxhttp:2.6.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.1' //rxhttp v2.2.2版本起,需要手动依赖okhttp
kapt 'com.github.liujingxing.rxhttp:rxhttp-compiler:2.6.1' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
}

可选

android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [
//使用asXxx方法时必须,告知RxHttp你依赖的rxjava版本,可传入rxjava2、rxjava3
rxhttp_rxjava: 'rxjava3',
rxhttp_package: 'rxhttp' //非必须,指定RxHttp类包名
]
}
}
}
}
dependencies {
implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0' //管理协程生命周期,页面销毁,关闭请求

//rxjava2 (RxJava2/Rxjava3二选一,使用asXxx方法时必须)
implementation 'io.reactivex.rxjava2:rxjava:2.2.8'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0' //管理RxJava2生命周期,页面销毁,关闭请求

//rxjava3
implementation 'io.reactivex.rxjava3:rxjava:3.0.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0' //管理RxJava3生命周期,页面销毁,关闭请求

//非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
implementation 'com.github.liujingxing.rxhttp:converter-fastjson:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-jackson:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-moshi:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-protobuf:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-simplexml:2.6.1'
}

最后,rebuild一下(此步骤是必须的) ,就会自动生成RxHttp类

混淆

RxHttp v2.2.8版本起,无需添加任何混淆规则(内部自带混淆规则),v2.2.8以下版本,请查看混淆规则,并添加到自己项目中

代码下载:327744707-okhttp-RxHttp-master.zip







收起阅读 »

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

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

Pod update注意

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

JSONSerialization

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

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

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

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

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

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

} else {

}

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

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

}

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

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

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

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

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

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

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

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

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

// TALPlayer+LogReport.h

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

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

// TALPlayer+InternalProperty.h

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


// TALPlayer+InternalProperty.m

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

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

// TALPlayer+LogReportDataInfoKeys.h

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


// TALPlayer+LogReportDataInfoKeys.m

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


// 使用
#import "TALPlayerLogReportDataInfoKeys.h"

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

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

// XXX.h

#pragma mark - Protocol

#pragma mark - Properties

#pragma mark - Methods

// XXX.m

#pragma mark - Consts

#pragma mark - UI Components

#pragma mark - Data Properties

#pragma mark - Initial Methods

#pragma mark - Lifecycle Methods

#pragma mark - Override Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - XXXDelegate

#pragma mark - Getters

#pragma mark - Setters

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


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

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

//TODO: todo here

//FIXME: fix me later

//???: What is this shit

//!!!: DO NOT TOUCH MY CODE

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

收起阅读 »

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

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

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

将原来的load方法换成initialize






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

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





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

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

  • 2.initialize的打印

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

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

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

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

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


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

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

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

initialize只会调用一次

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

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

}
return 0;
}

输出结果:

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


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

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

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

#import "TCStudent.h"

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

#import "TCStudent.h"

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

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

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




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


收起阅读 »

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

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

首先先看几个面试问题

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

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


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


Cateogry里面有load方法么?

  • 答:分类里面肯定有load

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{

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

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

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

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

}
@end

load方法什么时候调用?

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

#import <Foundation/Foundation.h>

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

}
return 0;
}

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


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

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

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

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


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

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

NS_ASSUME_NONNULL_END

#import "TCPerson.h"

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

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

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

在main里面我们调用test

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

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





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




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

我们打印TCPerson的类方法

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

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

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

// 释放
free(methodList);

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


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

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

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

loadMethodLock.assertLocked();

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

void *pool = objc_autoreleasePoolPush();

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

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

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

objc_autoreleasePoolPop(pool);

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

static void call_class_loads(void)
{
int I;

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

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

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

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

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

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

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

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

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

为什么test不一样了

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

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

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

@implementation TCStudent

@end

不写load方法调用

TCStudent写上load


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

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



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









收起阅读 »

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

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

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

demo源码

iOS UI开发的痛点

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

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

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

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

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

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

const

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

React的核心思想

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

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

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

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

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

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

1.2 React Components

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

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

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

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

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

怎么在iOS中运用React Component概念

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

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

@protocol ComponentProtocol <NSObject>

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

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

@end

@interface BaseComponent : NSObject <ComponentProtocol>

- (instancetype)initWithProps:(id)props;

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

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

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

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

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

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

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

Demo

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


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

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

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

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

总结

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

思考

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

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

收起阅读 »

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

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

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

背景

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

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

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

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

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

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

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

基本用法

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

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

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

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

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

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

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

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

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

可以判等

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

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

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

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

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

辅助的 API

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

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

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

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

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

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

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

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

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

let value = try? intResult.get()

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

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

enum CustomError: Error, Equatable {
case inputNotValid
}

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

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

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

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

收起阅读 »

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

//执行
testblock();

return 0;
}

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

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

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

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

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

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

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

return 0;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return 0;
}

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

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

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

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

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

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

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

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

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

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

收起阅读 »

性能超高的UI库-AsyncDisplayKit

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

AsyncDisplayKit 已移动并重命名:Texture

性能提升

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

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

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


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


详细的api介绍:

https://texturegroup.org/appledocs.html


常见问题及demo下载:

https://github.com/facebookarchive/AsyncDisplayKit


源码下载:




收起阅读 »

java设计模式:享元模式

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

前言

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


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


定义

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


优点

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


缺点

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

享元模式的结构与实现

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

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


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


结构

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

享元模式的实现

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


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


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


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

 public abstract class WebSite {

public abstract void use();

}

具体网站类


public class ConcreteWebSite extends WebSite {

private String name = "";

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

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

}

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

public class WebSiteFactory {

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

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

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

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

}

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

public class Client {

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

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

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

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

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

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

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

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

}

源码中的享元模式

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


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


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

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


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

String s0=”kvill”; 

String s1=”kvill”;

String s2=”kv” + “ill”;

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

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

结果为:


true 

true

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

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


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


看例2:


String s0=”kvill”; 

String s1=new String(”kvill”);

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

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

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

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

结果为:


false 

false

false

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


String.intern():

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


例3:


String s0= “kvill”; 

String s1=new String(”kvill”);

String s2=new String(“kvill”);

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

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

s1.intern();

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

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

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

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

结果为:


false 

**********

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

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

true

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


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


看例4:


String s1=new String("kvill"); 

String s2=s1.intern();

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

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

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

结果:


false 

kvill kvill

true

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


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


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


关于equals()和==:

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


关于String是不可变的

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


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


okhttp3 kotlin ConnectionPool 源码分析

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

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


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

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

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

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

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


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


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

return true
}
return false
}

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


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

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

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

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

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

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

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


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

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


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

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

return references.size
}

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

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

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

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

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

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


Integer中的享元模式

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

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

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

输出:


true
false

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


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

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


收起阅读 »

java设计模式:桥接模式

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

桥接模式的定义与特点

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


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


优点


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

缺点

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


桥接模式的结构与实现

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


模式的结构

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

桥接模式的应用场景

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


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



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

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


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


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

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

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

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

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

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



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

收起阅读 »

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

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

前言

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



开闭原则

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


开闭原则的含义

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


开闭原则的作用


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

里氏替换原则

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


里氏替换原则的作用

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

里氏替换原则的实现方法

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


依赖倒置原则

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


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


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


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


依赖、倒置原则的作用


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

单一职责原则

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


单一职责原则的优点

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



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

接口隔离原则

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

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


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



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

    接口隔离原则的优点

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


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

迪米特法则

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


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


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

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


合成复用原则

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


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


合成复用原则的重要性

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



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

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



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

收起阅读 »

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

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

写在前面:

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

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

必要说明:

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

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

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

消费者公测

包含机型:

Mate X2

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

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

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

MatePad Pro、MatePad Pro 5G

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

方法:

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

△我的华为

△花粉俱乐部

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

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

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

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

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

消费者内测

包含机型:

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

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

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

华为麦芒9 5G

MatePad 10.8、MatePad 5G 10.4、MatePad 10.4

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

渠道一

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

方法:

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

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

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

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

渠道二

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

方法:

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

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

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

HarmonyOS体验官(线下)

包含机型:

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

方法:

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

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

重要的补充说明

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

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

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

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

收起阅读 »

ZFPlayer 3.0解析

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

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

目前支持的功能如下:

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

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

ZFPlayerController(播放器的主要类)

初始化方式:

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

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

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

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

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

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

属性

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

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

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

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

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

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

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

ZFPlayerController (ZFPlayerTimeControl)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ZFPlayerController (ZFPlayerOrientationRotation)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ZFPlayerController (ZFPlayerViewGesture)

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

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

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

ZFPlayerController (ZFPlayerScrollView)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ZFPlayerMediaPlayback—播放器SDK遵守的协议

1、枚举类型:

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

2、协议属性:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3、协议方法:

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

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

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

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

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

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

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

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

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

ZFPlayerMediaControl—控制层遵守的协议

1、视频状态相关

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

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

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

2、播放进度

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

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

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

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

3、锁屏

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

4、屏幕旋转

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

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

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

5、手势方法

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

收起阅读 »

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

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

wxlibrary aar文件使用说明

一、项目介绍

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

二、工程引入工具包

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

引入微信工具包及微信SDK

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

三、工具包初始准备工作

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

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

 
...>



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


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



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

android {
...
defaultConfig {
...

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

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

1. 登录使用

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

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

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

五、测试说明

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

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

}


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

收起阅读 »

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

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

ecorderManager

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

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

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

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

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

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

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

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

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

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

一.效果展示

仿微信界面视频录制

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

二.引用

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

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

2.Add the dependency

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

三.使用

1.音频录制

采用默认配置录制

mRecorderManager.recordAudio(mFilePath);

自定义配置参数录制

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

2.视频录制

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

从0.2.18开始改为类似

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

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

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

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

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

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

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

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

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

从0.4.0-beta版本开始:

public interface IRecordVideoPageRequester extends IRMRequester {

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

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

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

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

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

public class RecordVideoRequestOption implements Parcelable {

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

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

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

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

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

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

interface RMRecordVideoResultCallback {

fun onResponseRecordVideoResult(info: RecordVideoResultInfo)

fun onFailure(e: RecorderManagerException)
}

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

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

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

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

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

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

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

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

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

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

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

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

1.创建RecordVideoFragment

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

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

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

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

public class RecordVideoOption:

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

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

interface RMOnRecordVideoListener {

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

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

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

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

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

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

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

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

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

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

public class RecorderManagerFactory {

private RecorderManagerFactory() {
}

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

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

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

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

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

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

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

public interface IRecorderManager extends IRecorderHelper {

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

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

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

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

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

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

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

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

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

}

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

public interface or extends ICameraInterceptor {}

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

public interface IRecorderHelper {

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

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

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

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

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

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

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

2.拿到后创建相机对象

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

3.录制

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

4.释放

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

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

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

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

MXVideo

介绍

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

功能特性

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

1、通过 dependence 引入MXVideo

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

2、页面集成

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

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

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

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

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


MXPlaySource 可选参数说明:

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

4、监听播放进度

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

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

5、全屏返回 + 释放资源

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

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

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

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

功能相关

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

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

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

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

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

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

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

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

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

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

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

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


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

iOS逆向(8)-Monkey、Logos

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

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

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

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

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

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

一、Monkey

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

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

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

环境要求

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

1、安装最新的theos

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

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

brew install ldid

安装

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

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

默认安装的Xcode为:

xcode-select -p

执行安装命令:

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

卸载

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

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

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

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


二、Logos

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

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

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

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

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

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


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

4、玩转Logos
在上一步建好的Monkey工程中,可以发现在目录有一个Logos目录:


默认有两个文件LogosDemoDylib.xm和LogosDemoDylib.mm。
其中Logos语句就是写在LogosDemoDylib.xm中的,LogosDemoDylib.mm是根据LogosDemoDylib.xm中的内容自动生成的。
接下来,咱们根据几个需求来介绍Logos的一些常用的用法。

1、更改点击按钮的弹框内容(hook)
由于需要更改弹窗,所以首先导入UIKit框架。

#import <UIKit/UIKit.h>

由于咱们手上有源码,所以可以直接跳过动态分析的这一步,直接就知道按钮所处的页面是叫做ViewController,按钮的响应方法是:

- (IBAction)tapAction:(id)sender

利用hook命令:

#import <UIKit/UIKit.h>

// hook + 类名
%hook ViewController
// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
}

%end

运行项目,发现按钮已经被成功hook了。


2、调用原方法(orig)

#import <UIKit/UIKit.h>

%hook ViewController

- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
// 调用原方法
%orig;
}

%end

3、新增一个方法,并且调用(new)

由于在Monkey工程里面是编译不到源码的,所以无论是新增的方法,还是调用原工程中的方法,都是无法通过编译的,所以都需要使用interface申明每一个方法。

#import <UIKit/UIKit.h>

// 这里只是为了申明
@interface ViewController

- (void)newFunC;

@end

%hook ViewController

// 新增方法关键字new
%new
- (void)newFunC{
NSLog(@"newFunC");
}

// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
[self newFunC];
// 调用原方法关键字orig
%orig;
}

%end

文中所有的Demo都在这可以下载到:Dmoe

Logos除了以上hook,end,orig,new这几种关键字,还有:
%subclass:增加一个类
%log:打印,类似NSLog
%group: 给代码分组,可以用于在不同环境加载不同的代码,比如iOS8加载group1,iOS9加载group2,如果部分中,默认所有代码在名为「_ungrouped」的隐藏分组中。
...

所有的Logos语法都可以在官方文档中查询得到。

5、给优酷加UI

首先在这里下载笔者自己砸壳后优酷ipa包(arm64架构的):优酷(砸壳).ipa 提取码: xtua

Step 1、新建工程YouKu

同样的新建一个Monkey工程,取名YouKu,将下载好的ipa包放入工程对应的TargetApp目录下。Run。同样是重签名成功。

在上面的Demo中,我们是对我们直接的工程进行HOOK,由于我们手上有源码,所以我们越过了最难的一个步骤:动态分析。
而我们现在要对优酷进行Hook,但我们手上是没有优酷的源码的,所以此时此刻就需要对其进行动态分析了。
下面我将结合Xcode和class dump对优酷的设置页面简单的进行分析。

Step 2、class dump

class-dump is a command-line utility for examining the Objective-C segment of Mach-O files. It generates declarations for the classes, categories and protocols. This is the same information provided by using 'otool -ov', but presented as normal Objective-C declarations.
简单说就是一个可以导出一个MachO文件的所有头文件信息(包括Extension)

在文首有提到Monkey除了重签名,还集成了class dump的功能,所以我们需要做的就仅仅是开启这个功能:


Run!成功之后可以发现在工程目录下多了一个文件夹Youkui4Phone_Headers,其中就是优酷的所有的头文件了。


Step 3、分析优酷设置页面

工程Run成功后,点击进入设置页面(不用登录),如下图:


我们现在要做的就是在这个页面的TableView的最后一行加上Cell,里面有个Switch,用于打开/关闭屏蔽广告功能(只是UI,这篇文章不牵扯到屏蔽广告的具体实现,如果你需要,点个小心心,持续关注我哦😀😀😀)。

利用伟大的Xcode我们可以非常清晰的看到,设置页面的DataSource和Delegate都是在SettingViewController中,


咱们就找到Hook的类名:SettingViewController
需要Hook的方法自然就是TableView的那些DataSource和Delegate了。

这里需要额外提到的一点是,在文章开始的时候就说了Monkey已经将Cydia Substrate集成进去了,所以我们可以直接使用Cydia Substrate的相关功能了。

在这里我们需要拿到这个页面TableView的对应的变量,我们就需要使用到Cydia Substrate的功能了。打开上文中获取到优酷的所有的头文件,所有SettingViewController,发现其只有一个TableView变量:_tabview。
那么毫无疑问,就是他了!
而获取它的方法是:

MSHookIvar <UITableView *>(self,"_tabview")

一个reloadData的简单使用:

[MSHookIvar <UITableView *>(self,"_tabview") reloadData];

其他的UI代码在这里就不一一解释了,全部代码如下,当然在Demo中也是有的,其中包括了数据的简单持久化功能:

#import <UIKit/UIKit.h>
#define FYDefaults [NSUserDefaults standardUserDefaults]
#define FYSwitchUserDefaultsKey @"FYSwitchUserDefaultsKey"

@interface SettingViewController
- (long long)numberOfSectionsInTableView:(id)arg1;
@end

%hook SettingViewController

%new
-(void)switchChangeAction:(UISwitch *)switchView{
[FYDefaults setBool:switchView.isOn forKey:FYSwitchUserDefaultsKey];
[FYDefaults synchronize];
[MSHookIvar <UITableView *>(self,"_tabview") reloadData];
}

//多少组
- (long long)numberOfSectionsInTableView:(id)arg1{
UITableView * tableView = MSHookIvar <UITableView *>(self,"_tabview");
NSLog(@"fy_numberOfSectionsInTableView:");
// 额外增加一个
return %orig+1;
}

//每组多少行
- (long long)tableView:(UITableView *)tableView numberOfRowsInSection:(long long)section{
NSLog(@"fy_numberOfRowsInSection:");
//定位设置界面,并且是最后一个
if(section == [self numberOfSectionsInTableView:tableView]-1){
return 1;
}
else{
return %orig;
}
}

//返回高度
- (double)tableView:
(UITableView *)tableView heightForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_heightForRowAtIndexPath:");
//定位设置界面,并且是最后一个
if([indexPath section] ==[self numberOfSectionsInTableView:tableView]-1){
return 44;
}
else{
return %orig;
}
}


//每一个Cell
- (id)tableView:(UITableView *)tableView cellForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_cellForRowAtIndexPath:");
//定位设置界面,并且是最后一组
if([indexPath section] == [self numberOfSectionsInTableView:tableView]-1){
UITableViewCell * cell = nil;
if([indexPath row] == 0){
static NSString *swCell = @"SwCellIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:swCell];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:nil];
}
cell.textLabel.text = @"免广告";
// 免广告开关
UISwitch *switchView = [[UISwitch alloc] init];
switchView.on = [FYDefaults boolForKey:FYSwitchUserDefaultsKey];
[switchView addTarget:self action:@selector(switchChangeAction:) forControlEvents:(UIControlEventValueChanged)];
cell.accessoryView = switchView;
cell.imageView.image = [UIImage imageNamed:([FYDefaults boolForKey:FYSwitchUserDefaultsKey] == 1) ? @"unlocked" : @"locked"];
}
cell.backgroundColor = [UIColor whiteColor];
return cell;

}else{
return %orig;
}
}

%end

最后的效果


6、为什么Monkey这么牛逼

查看重新编译后的app文件,可以发现其中的Framework多了很多东西:


从这可以得知,原来Monkey其实也是通过将诸多的动态库(包括自己的工程)注入的形式,实现了这些功能。

三、总结

在这片文章中主要介绍了Monkey的一些用法已经Logos的基本语法。而在上一篇其实留了一个小尾巴,就是Cycript,笔者将要在下一篇文章中重点讲解Cycript的安装,基础用法和高级用法。之所以放在下一篇,是因为Cycript配合Monkey将会有事半功倍的效果。

链接:https://www.jianshu.com/p/da6cb32a1416

收起阅读 »

iOS 优秀框架之TYAttributedLabel(基于coreText的图文混排)

TYAttributedLabel1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数3、支持添加高度自定义文本属性4、支持添加属性文本,自定义链接,...
继续阅读 »

TYAttributedLabel

1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)
2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数
3、支持添加高度自定义文本属性
4、支持添加属性文本,自定义链接,新增高亮效果显示(文字和背景)
5、支持添加UIImage和UIView控件

demo演示


重点类简介

TYAttributedLabel

创建label(可接受文本及富文本)
设置字体间距
设置行间距
设置字体大小
设置view的位置和宽,会自动计算高度
设置链接文本,并用代理(TYAttributedLabelDelegate)方法完成点击后需完成的任务

TYImageStorage

可创建一个append在TYAttributedLabel后的图片控件,可自定义图片大小,及对齐样式

TYTextStorage

文本文件,可设置文本大小及字体颜色

TYTextContainer

属性文本生成器(使用 RegexKitLite)
具体代码及使用细节请看作者的demo(作者是华人),讲的很详细,这里就不再赘述

链接:TYAttributedLabel

链接:https://www.jianshu.com/p/5d81bf7e79c8

收起阅读 »

如何让10万条数据的小程序列表如丝般顺滑

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。一...
继续阅读 »

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:

‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。

一.小程序页面限制多少个wxml节点?

写了个小dome做了个测试。 listData的数据结构为:

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//20条数据
]
}]

页面渲染效果:




{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



2.dome2,删除了不必要的dom嵌套



{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



通过大致计算,一个小程序页面大概可以渲染2万个wxml节点 而小程序官方的性能测评得分条件为少于1000个wxml节点官方链接



二.列表页面优化

1.减少不必要的标签嵌套


由上面的测试dome可知,在不影响代码运行和可读性的前提下,尽量减少标签的嵌套,可以大幅的增加页面数据的列表条数,毕竟公司不是按代码行数发工资的。如果你的列表数据量有限,可以用这种方法来增加列表渲染条数。如果数据量很大,再怎么精简也超过2万的节点,这个方法则不适用。


2.优化setData的使用


图五所示,小程序setDate的性能会受到setData数据量大小和调用频率限制。所以要围绕减少每一次setData数据量大小,降低setData调用频率进行优化。
#####(1)删除冗余字段
后端的同事经常把数据从数据库中取出就直接返回给前端,不经过任何处理,所以会导致数据大量的冗余,很多字段根本用不到,我们需要把这些字段删除,减少setDate的数据大小。
#####(2)setData的进阶用法
通常,我们对data中数据的增删改操作,是把原来的数据取出,处理,然后用setData整体去更新,比如我们列表中使用到的上拉加载更多,需要往listData尾部添加数据:

newList=[{...},{...}];
this.setData({
listData:[...this.data.listData,...newList]
})

这样会导致 setDate的数据量越来越大,页面也越来越卡。

setDate的正确使用姿势

  • setDate修改数据

比如我们要修改数组listData第一个元素的isDisplay属性,我们可以这样操作:

let index=0;
this.setData({
[`listData[${index}].isDisplay`]:false,
})

如果我们想同时修改数组listData中下标从0到9的元素的isDisplay属性,那要如何处理呢?你可能会想到用for循环来执行setData

for(let index=0;index<10;index++){
this.setData({
[`listData[${index}].isDisplay`]:false,
})
}

那么这样就会导致另外一个问题,那就是listData的调用过于频繁,也会导致性能问题,正确的处理方式是先把要修改的数据先收集起来,然后调用setData一次处理完成:

let changeData={};
for(let index=0;index<10;index++){
changeData[[`listData[${index}].isDisplay`]]=false;
}
this.setData(changeData);



这样我们就把数组listData中下标从0到9的元素的isDisplay属性改成了false

  • setDate往数组末尾添加数据

如果只添加一条数据

let newData={...};
this.setData({
[`listData[${this.data.listData.length}]`]:newData
})

如果是添加多条数据

let newData=[{...},{...},{...},{...},{...},{...}];
let changeData={};
let index=this.data.listData.length
newData.forEach((item) => {
changeData['listData[' + (index++) + ']'] = item //赋值,索引递增
})
this.setData(changeData)

三.使用自定义组件

可以把列表的一行或者多行封装到自定义组件里,在列表页使用一个组件,只算一个节点,这样你的列表能渲染的数据可以成倍数的增加。组件内的节点数也是有限制的,但是你可以一层层嵌套组件实现列表的无限加载,如果你不怕麻烦的话

四.使用虚拟列表

经过上面的一系列操作后,列表的性能会得到很大的提升,但是如果数据量实在太大,wxml节点数也会超出限制,导致页面发生错误。我们的处理方法是使用虚拟列表,页面只渲染当前可视区域以及可视区域上下若干条数据的节点,通过isDisplay控制节点的渲染。

  • 可视区域上方:above
  • 可视区域:screen
  • 可视区域下方:below

1.listData数组的结构

使用二维数组,因为如果是一维数组,页面滚动需要用setData设置大量的元素isDispaly属性来控制列表的的渲染。而二维数组可以这可以一次调用setData控制十条,二十条甚至更多的数据的渲染。

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//二维数组中的条数根据项目实际情况
]
}]

2.必要的参数

data{
itemHeight:4520,//列表第一层dom高度,单位为rpx
itemPxHeight:'',//转化为px高度,因为小程序获取的滚动条高度单位为px
aboveShowIndex:0,//已渲染数据的第一条的Index
belowShowNum:0,//显示区域下方隐藏的条数
oldSrollTop:0,//记录上一次滚动的滚动条高度,判断滚动方向
prepareNum:5,//可视区域上下方要渲染的数量
throttleTime:200,//滚动事件节流的时间,单位ms
}

3.wxml的dom结构






{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}




4.获取列表第一层dom的px高度

let query = wx.createSelectorQuery();
query.select('.content').boundingClientRect(rect=>{
let clientWidth = rect.width;
let ratio = 750 / clientWidth;
this.setData({
itemPxHeight:Math.floor(this.data.itemHeight/ratio),
})
}).exec();

5.页面滚动时间节流

function throttle(fn){
let valid = true
return function() {
if(!valid){
return false
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn.call(this,arguments);
valid = true;
}, this.data.throttleTime)
}
}

6.页面滚动事件处理

onPageScroll:throttle(function(e){
let scrollTop=e[0].scrollTop;//滚动条高度
let itemNum=Math.floor(scrollTop/this.data.itemPxHeight);//计算出可视区域的数据Index
let clearindex=itemNum-this.data.prepareNum+1;//滑动后需要渲染数据第一条的index
let oldSrollTop=this.data.oldSrollTop;//滚动前的scrotop,用于判断滚动的方向
let aboveShowIndex=this.data.aboveShowIndex;//获取已渲染数据第一条的index
let listDataLen=this.data.listData.length;
let changeData={}
//向下滚动
if(scrollTop-oldSrollTop>0){
if(clearindex>0){
//滚动后需要变更的条数
for(let i=aboveShowIndex;i changeData[[`listData[${i}].isDisplay`]]=false;
let belowShowIndex=i+2*this.data.prepareNum;
if(i+2*this.data.prepareNum changeData[[`listData[${belowShowIndex}].isDisplay`]]=true;
}
}
}
}else{//向上滚动
if(clearindex>=0){
let changeData={}
for(let i=aboveShowIndex-1;i>=clearindex;i--){
let belowShowIndex=i+2*this.data.prepareNum
if(i+2*this.data.prepareNum<=listDataLen-1){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=false;
}
changeData[[`listData[${i}].isDisplay`]]=true;
}
}else{
if(aboveShowIndex>0){
for(let i=0;i this.setData({
[`listData[${i}].isDisplay`]:true,
})
}
}
}
}
clearindex=clearindex>0?clearindex:0
if(clearindex>=0&&!(clearindex>0&&clearindex==this.data.aboveShowIndex)){
changeData.aboveShowIndex=clearindex;
let belowShowNum=this.data.listData.length-(2*this.data.prepareNum+clearindex)
belowShowNum=belowShowNum>0?belowShowNum:0
if(belowShowNum>=0){
changeData.belowShowNum=belowShowNum
}
this.setData(changeData)
}
this.setData({
oldSrollTop:scrollTop
})
}),

经过上面的处理后,页面的wxml节点数量相对稳定,可能因为可视区域数据的index计算误差,页面渲染的数据有小幅度的浮动,但是已经完全不会超过小程序页面的节点数量的限制。理论上100万条数据的列表也不会有问题,只要你有耐心和精力一直划列表加载这么多数据。

7.待优化事项



  • 列表每一行的高度需要固定,不然会导致可视区域数据的index的计算出现误差

  • 渲染玩列表后往回来列表,如果手速过快,会导致above,below区域的数据渲染不过来,会出现短暂的白屏,白屏问题可以调整 prepareNum, throttleTime两个参数改善,但是不能完全解决(经过测试对比发现,即使不对列表进行任何处理,滑动速度过快也会发生短暂白屏的情况)。

  • 如果列表中有图片,above,below区域重新渲染时,图片虽然以经缓存在本地,不需要重新去服务器请求,但是重新渲染还是需要时间,尤其当你手速特别快时。可以根据上面的思路, isDisplay时只销毁非的节点,这样重新渲染就不需要渲染图片,但是这样节点数还是会增加,不过应该能满足大部分项目需求了,看自己项目怎么取舍。



原文:https://juejin.cn/post/6966904317148299271


收起阅读 »

iOS 使用Moya网络请求

Moya最新版本11.0.2由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。Moya11.0.2DemoMoya简介Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)...
继续阅读 »

Moya最新版本11.0.2

由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。
Moya11.0.2Demo

Moya简介

Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)安放网络请求,Moya 替你管理。

Moya有几个比较好的特性:

1、编译时检查正确的API端点访问.

2、使你定义不同端点枚举值对应相应的用途更加明晰.

3、提高测试地位从而使单元测试更加容易.

Swift我们用Alamofire来做网络库.而Moya在Alamofire的基础上又封装了一层,如下流程图说明Moya的简单工作流程图:


** Moya**的官方下载地址点我强大的Moya,有具体的使用方法在demo里面有说明。

本文主要介绍一下Moya的用法

1、设置请求头部信息
2、设置超时时间
3、自定义插件
4、自签名证书
注意:以下所出现的NetAPIManager跟官网上demo的** GitHub**是一样类型的文件,都是这个enum实现一个协议TargetType,点进去可以看到TargetType定义了我们发送一个网络请求所需要的东西,什么baseURL,parameter,method等一些计算性属性,我们要做的就是去实现这些东西,当然有带默认值的我们可以不去实现,但是设置头部信息跟超时时间就要修改这些系统默认设置了。

为了看得更加清楚,贴上NetAPIManager文件的内容

//
// NetAPIManager.swift
// NN110
//
// Created by 陈亦海 on 2017/5/12.
// Copyright © 2017年 陈亦海. All rights reserved.
//

import Foundation
import Moya


enum NetAPIManager {
case Show
case upload(bodyData: Data)
case download
case request(isTouch: Bool, body: Dictionary<String, Any>? ,isShow: Bool)
}


extension NetAPIManager: TargetType {
var baseURL: URL {//服务器地址

switch self {
case .request( _, _, _):
return URL(string: "https://www.pmphmall.com")!
default:
return URL(string: "https://httpbin.org")!
}


}

var path: String {//具体某个方法的路径
switch self {
case .Show:
return ""
case .upload(_):
return ""
case .request(_, _, _):
return "/app/json.do"
case .download:
return ""
}
}

var method: Moya.Method {//请求的方法 get或者post之类的
switch self {
case .Show:
return .get
case .request(_, _, _):
return .post
default:
return .post
}
}

var parameters: [String: Any]? {//请求的get post给服务器的参数
switch self {
case .Show:
return nil
case .request(_, _, _):
return ["msg":"H4sIAAAAAAAAA11SSZJFIQi7EqPAEgTvf6TP62W7sMoSQhKSWDrs6ZUKVWogLwYV7RjHFBZJlNlzloN6LVqID4a+puxqRdUKVNLwE1TRcZIC/fjF2rPotuXmb84r1gMXbiASZIZbhQdKEewJlz41znDkujCHuQU3dU7G4/PmVRnwArMLXukBv0J23XVahNO3VX35wlgce6TLUzzgPQJFuHngAczl6VhaNXpmRLxJBlMml6gdLWiXxTdO7I+iEyC7XuTirCQXOk4dotgArgkH/InxVjfNTnE/uY46++hyAiLFuFL4cv1Z8WH5DgB2GnvFXMh5gm53Tr13vqqrEYtcdXfkNsMwKB+9sAQ77grNJmquFWOhfXA/DELlMB0KKFtHOc/ronj1ml+Z7qas82L3VWiCVQ+HEitjTVzoFw8RisFN/jJxBY4awvq427McXqnyrfCsl7oeEU6wYgW9yJtj1lOkx0ELL5Fw4z071NaVzRA9ebxWXkFyothgbB445cpRmTC+//F73r1kOyQ3lTpec12XNDR00nnq5/YmJItW3+w1z27lSOLqgVctrxG4xdL9WVPdkH1tkiZ/pUKBGhADAAA="]
default:
return nil

}
}

var sampleData: Data { //编码转义
return "{}".data(using: String.Encoding.utf8)!
}

var task: Task { //一个请求任务事件

switch self {


case let .upload(data):
return .upload(.multipart([MultipartFormData(provider: .data(data), name: "file", fileName: "gif.gif", mimeType: "image/gif")]))

default:
return .request

}

}

var parameterEncoding: ParameterEncoding {//编码的格式
switch self {
case .request(_, _, _):
return URLEncoding.default
default:
return URLEncoding.default
}

}
//以下两个参数是我自己写,用来控制网络加载的时候是否允许操作,跟是否要显示加载提示,这两个参数在自定义插件的时候会用到
var touch: Bool { //是否可以操作

switch self {
case .request(let isTouch, _, _):
return isTouch
default:
return false
}

}

var show: Bool { //是否显示转圈提示

switch self {
case .request( _, _,let isShow):
return isShow
default:
return false
}

}


}

如何设置Moya请求头部信息

头部信息的设置在开发过程中很重要,如服务器生成的token,用户唯一标识等
我们直接上代码,不说那么多理论的东西,哈哈

// MARK: - 设置请求头部信息
let myEndpointClosure = { (target: NetAPIManager) -> Endpoint<NetAPIManager> in


let url = target.baseURL.appendingPathComponent(target.path).absoluteString
let endpoint = Endpoint<NetAPIManager>(
url: url,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
parameters: target.parameters,
parameterEncoding: target.parameterEncoding
)

//在这里设置你的HTTP头部信息
return endpoint.adding(newHTTPHeaderFields: [
"Content-Type" : "application/x-www-form-urlencoded",
"ECP-COOKIE" : ""
])

}

如何设置请求超时时间

// MARK: - 设置请求超时时间
let requestClosure = { (endpoint: Endpoint<NetAPIManager>, done: @escaping MoyaProvider<NetAPIManager>.RequestResultClosure) in

guard var request = endpoint.urlRequest else { return }

request.timeoutInterval = 30 //设置请求超时时间
done(.success(request))
}

自定义插件

自定义插件必须PluginType协议的两个方法willSend与didReceive

//
// MyNetworkActivityPlugin.swift
// NN110
//
// Created by 陈亦海 on 2017/5/10.
// Copyright © 2017年 CocoaPods. All rights reserved.
//

import Foundation
import Result
import Moya


/// Network activity change notification type.
public enum MyNetworkActivityChangeType {
case began, ended
}

/// Notify a request's network activity changes (request begins or ends).
public final class MyNetworkActivityPlugin: PluginType {



public typealias MyNetworkActivityClosure = (_ change: MyNetworkActivityChangeType, _ target: TargetType) -> Void
let myNetworkActivityClosure: MyNetworkActivityClosure

public init(newNetworkActivityClosure: @escaping MyNetworkActivityClosure) {
self.myNetworkActivityClosure = newNetworkActivityClosure
}

// MARK: Plugin

/// Called by the provider as soon as the request is about to start
public func willSend(_ request: RequestType, target: TargetType) {
myNetworkActivityClosure(.began,target)
}

/// Called by the provider as soon as a response arrives, even if the request is cancelled.
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
myNetworkActivityClosure(.ended,target)
}
}

使用自定义插件方法

// MARK: - 自定义的网络提示请求插件
let myNetworkPlugin = MyNetworkActivityPlugin { (state,target) in
if state == .began {
// SwiftSpinner.show("Connecting...")

let api = target as! NetAPIManager
if api.show {
print("我可以在这里写加载提示")
}

if !api.touch {
print("我可以在这里写禁止用户操作,等待请求结束")
}

print("我开始请求\(api.touch)")

UIApplication.shared.isNetworkActivityIndicatorVisible = true
} else {
// SwiftSpinner.show("request finish...")
// SwiftSpinner.hide()
print("我结束请求")
UIApplication.shared.isNetworkActivityIndicatorVisible = false

}
}

自签名证书

在16年的WWDC中,Apple已表示将从2017年1月1日起,所有新提交的App必须强制性应用HTTPS协议来进行网络请求。默认情况下非HTTPS的网络访问是禁止的并且不能再通过简单粗暴的向Info.plist中添加NSAllowsArbitraryLoads
设置绕过ATS(App Transport Security)的限制(否则须在应用审核时进行说明并很可能会被拒)。所以还未进行相应配置的公司需要尽快将升级为HTTPS的事项提上进程了。本文将简述HTTPS及配置数字证书的原理并以配置实例和出现的问题进行说明,希望能对你提供帮助。(比心~)

HTTPS:
简单来说,HTTPS就是HTTP协议上再加一层加密处理的SSL协议,即HTTP安全版。相比HTTP,HTTPS可以保证内容在传输过程中不会被第三方查看、及时发现被第三方篡改的传输内容、防止身份冒充,从而更有效的保证网络数据的安全。
HTTPS客户端与服务器交互过程:
1、 客户端第一次请求时,服务器会返回一个包含公钥的数字证书给客户端;
2、 客户端生成对称加密密钥并用其得到的公钥对其加密后返回给服务器;
3、 服务器使用自己私钥对收到的加密数据解密,得到对称加密密钥并保存;
4、 然后双方通过对称加密的数据进行传输。


数字证书:
在HTTPS客户端与服务器第一次交互时,服务端返回给客户端的数字证书是让客户端验证这个数字证书是不是服务端的,证书所有者是不是该服务器,确保数据由正确的服务端发来,没有被第三方篡改。数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方身份。证书由公钥、证书主题(Subject)、数字签名(digital signature)等内容组成。其中数字签名就是证书的防伪标签,目前使用最广泛的SHA-RSA加密。
证书一般分为两种:

1、一种是向权威认证机构购买的证书,服务端使用该种证书时,因为苹果系统内置了其受信任的签名根证书,所以客户端不需额外的配置。为了证书安全,在证书发布机构公布证书时,证书的指纹算法都会加密后再和证书放到一起公布以防止他人伪造数字证书。而证书机构使用自己的私钥对其指纹算法加密,可以用内置在操作系统里的机构签名根证书来解密,以此保证证书的安全。
2、另一种是自己制作的证书,即自签名证书。好处是不需要花钱购2买,但使用这种证书是不会受信任的,所以需要我们在代码中将该证书配置为信任证书.

自签名证书具体实现:

我们在使用自签名证书来实现HTTPS请求时,因为不像机构颁发的证书一样其签名根证书在系统中已经内置了,所以我们需要在App中内置自己服务器的签名根证书来验证数字证书。首先将服务端生成的.cer格式的根证书添加到项目中,注意在添加证书要一定要记得勾选要添加的targets。这里有个地方要注意:苹果的ATS要求服务端必须支持TLS 1.2或以上版本;必须使用支持前向保密的密码;证书必须使用SHA-256或者更好的签名hash算法来签名,如果证书无效,则会导致连接失败。由于我在生成的根证书时签名hash算法低于其要求,在配置完请求时一直报NSURLErrorServerCertificateUntrusted = -1202错误,希望大家可以注意到这一点。

那么如何在Moya中使用自签名的证书来实现HTTPS网络请求呢,请期待下回我专门分享......需要自定义一个Manager管理

综合使用的方法如下

定义一个公用的Moya请求服务对象

let MyAPIProvider = MoyaProvider<NetAPIManager>(endpointClosure: myEndpointClosure,requestClosure: requestClosure, plugins: [NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter),myNetworkPlugin])

// MARK: -创建一个Moya请求
func sendRequest(_ postDict: Dictionary<String, Any>? = nil,
success:@escaping (Dictionary<String, Any>)->(),
failure:@escaping (MoyaError)->()) -> Cancellable? {

let request = MyAPIProvider.request(.Show) { result in
switch result {
case let .success(moyaResponse):


do {
let any = try moyaResponse.mapJSON()
let data = moyaResponse.data
let statusCode = moyaResponse.statusCode
MyLog("\(data) --- \(statusCode) ----- \(any)")

success(["":""])


} catch {

}



case let .failure(error):

print(error)
failure(error)
}
}

return request
}

取消所有的Moya请求

// MARK: -取消所有请求
func cancelAllRequest() {
// MyAPIProvider.manager.session.invalidateAndCancel() //取消所有请求
MyAPIProvider.manager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
}

//let sessionManager = Alamofire.SessionManager.default
//sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
// dataTasks.forEach { $0.cancel() }
// uploadTasks.forEach { $0.cancel() }
// downloadTasks.forEach { $0.cancel() }
//}

}

转自:https://www.jianshu.com/p/38fbc22a1e2b

收起阅读 »

一行代码完成http请求,bitmap异步加载,数据库增删查改!

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.##Welike带来了哪些特征?WelikeAndroid...
继续阅读 »

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,
使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.

##Welike带来了哪些特征?

WelikeAndroid目前包含五个大模块:

  • 异常安全隔离模块(实验阶段):当任何线程抛出任何异常,我们的异常隔离机制都会让UI线程继续运行下去.
  • Http模块: 一行代码完成POST、GET请求和Download,支持上传, 高度优化Disk的缓存加载机制,
    自由设置缓存大小、缓存时间(也支持永久缓存和不缓存).
  • Bitmap模块: 一行代码完成异步显示图片,无需考虑OOM问题,支持加载前对图片做自定义处理.
  • Database模块: 支持NotNull,Table,ID,Ignore等注解,Bean无需Getter和Setter,一键式部署数据库.
  • ui操纵模块: 我们为Activity基类做了完善的封装,继承基类可以让代码更加优雅.
  • :请不要认为功能相似,框架就不是原创,源码摆在眼前,何不看一看?

使用WelikeAndroid需要以下权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

##下文将教你如何圆润的使用WelikeAndroid:
###通过WelikeContext在任意处取得上下文:

  • WelikeContext.getApplication(); 就可以取得当前App的上下文
  • WelikeToast.toast("你好!"); 简单一步弹出Toast.

##WelikeGuard(异常安全隔离机制用法):

  • 第一步,开启异常隔离机制:
WelikeGuard.enableGuard();
  • 第二步,注册一个全局异常监听器:

WelikeGuard.registerUnCaughtHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {

WelikeGuard.newThreadToast("出现异常了: " + ex.getMessage() );

}
});
  • 你也可以自定义异常:

/**
*
* 自定义的异常,当异常被抛出后,会自动回调onCatchThrowable函数.
*/
@Catch(process = "onCatchThrowable")
public class CustomException extends IllegalAccessError {

public static void onCatchThrowable(Thread t){
WeLog.e(t.getName() + " 抛出了一个异常...");
}
}
  • 另外,继承自UncaughtThrowable的异常我们不会对其进行拦截.

使用Welike做屏幕适配:

Welike的ViewPorter类提供了屏幕适配的Fluent-API,我们可以通过一组流畅的API轻松做好屏幕适配.

        ViewPorter.from(button).ofScreen().divWidth(2).commit();//宽度变为屏幕的二分之一
ViewPorter.from(button).of(viewGroup).divHeight(2).commit();//高度变为viewGroup的二分之一
ViewPorter.from(button).div(2).commit();//宽度和高度变为屏幕的四分之一
ViewPorter.from(button).of(this).fillWidth().fillHeight().commit();//宽度和高度铺满Activity
ViewPorter.from(button).sameAs(imageView).commit();//button的宽度和高度和imageView一样

WelikeHttp入门:

首先来看看框架的调试信息,是不是一目了然. DEBUG DEBUG2

  • 第一步,取得WelikeHttp默认实例.
WelikeHttp welikeHttp = WelikeHttp.getDefault();
  • 第二步,发送一个Get请求.
HttpParams params = new HttpParams();
params.putParams("app","qr.get",
"data","Test");//一次性放入两对 参数 和 值

//发送Get请求
HttpRequest request = welikeHttp.get("http://api.k780.com:88", params, new HttpResultCallback() {
@Override
public void onSuccess(String content) {
super.onSuccess(content);
WelikeToast.toast("返回的JSON为:" + content);
}

@Override
public void onFailure(HttpResponse response) {
super.onFailure(response);
WelikeToast.toast("JSON请求发送失败.");
}

@Override
public void onCancel(HttpRequest request) {
super.onCancel(request);
WelikeToast.toast("请求被取消.");
}
});

//取消请求,会回调onCancel()
request.cancel();

当然,我们为满足需求提供了多种扩展的Callback,目前我们提供以下Callback供您选择:

  • HttpCallback(响应为byte[]数组)
  • FileUploadCallback(仅在上传文件时使用)
  • HttpBitmapCallback(建议使用Bitmap模块)
  • HttpResultCallback(响应为String)
  • DownloadCallback(仅在download时使用)

如需自定义Http模块的配置(如缓存时间),请查看HttpConfig.

WelikeBitmap入门:

  • 第一步,取得默认的WelikeBitmap实例:

//取得默认的WelikeBitmap实例
WelikeBitmap welikeBitmap = WelikeBitmap.getDefault();
  • 第二步,异步加载一张图片:
BitmapRequest request = welikeBitmap.loadBitmap(imageView,
"http://img0.imgtn.bdimg.com/it/u=937075122,1381619862&fm=21&gp=0.jpg",
android.R.drawable.btn_star,//加载中显示的图片
android.R.drawable.ic_delete,//加载失败时显示的图片
new BitmapCallback() {

@Override
public Bitmap onProcessBitmap(byte[] data) {
//如果需要在加载时处理图片,可以在这里处理,
//如果不需要处理,就返回null或者不复写这个方法.
return null;
}

@Override
public void onPreStart(String url) {
super.onPreStart(url);
//加载前回调
WeLog.d("===========> onPreStart()");
}

@Override
public void onCancel(String url) {
super.onCancel(url);
//请求取消时回调
WeLog.d("===========> onCancel()");
}

@Override
public void onLoadSuccess(String url, Bitmap bitmap) {
super.onLoadSuccess(url, bitmap);
//图片加载成功后回调
WeLog.d("===========> onLoadSuccess()");
}

@Override
public void onRequestHttp(HttpRequest request) {
super.onRequestHttp(request);
//图片需要请求http时回调
WeLog.d("===========> onRequestHttp()");
}

@Override
public void onLoadFailed(HttpResponse response, String url) {
super.onLoadFailed(response, url);
//请求失败时回调
WeLog.d("===========> onLoadFailed()");
}
});
  • 如果需要自定义Config,请看BitmapConfig这个类.

##WelikeDAO入门:

  • 首先写一个Bean.

/*表名,可有可无,默认为类名.*/
@Table(name="USER",afterTableCreate="afterTableCreate")
public class User{
@ID
public int id;//id可有可无,根据自己是否需要来加.

/*这个注解表示name字段不能为null*/
@NotNull
public String name;

public static void afterTableCreate(WelikeDao dao){
//在当前的表被创建时回调,可以在这里做一些表的初始化工作
}
}
  • 然后将它写入到数据库
WelikeDao db = WelikeDao.instance("Welike.db");
User user = new User();
user.name = "Lody";
db.save(user);
  • 从数据库取出Bean

User savedUser = db.findBeanByID(1);
  • SQL复杂条件查询
List<User> users = db.findBeans().where("name = Lody").or("id = 1").find();
  • 更新指定ID的Bean
User wantoUpdateUser = new User();
wantoUpdateUser.name = "NiHao";
db.updateDbByID(1,wantoUpdateUser);
  • 删除指ID定的Bean
db.deleteBeanByID(1);
  • 更多实例请看DEMO和API文档.

##十秒钟学会WelikeActivity

  • 我们将Activity的生命周期划分如下:

=>@initData(所有标有InitData注解的方法都最早在子线程被调用)
=>initRootView(bundle)
=>@JoinView(将标有此注解的View自动findViewByID和setOnClickListener)
=>onDataLoaded(数据加载完成时回调)
=>点击事件会回调onWidgetClick(View Widget)

###关于@JoinView的细节:

  • 有以下三种写法:
@JoinView(name = "welike_btn")
Button welikeBtn;
@JoinView(id = R.id.welike_btn)
Button welikeBtn;
@JoinView(name = "welike_btn",click = false)
Button welikeBtn;
  • clicktrue时会自动调用view的setOnClickListener方法,并在onWidgetClick回调.
  • 当需要绑定的是一个Button的时候, click属性默认为true,其它的View则默认为false.
收起阅读 »

一个简洁而优雅的Android原生UI框架,解放你的双手!

一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、...
继续阅读 »

一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!

涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、ProgressBar、LoadingView、StateLayout、FlowLayout、Switch、Actionbar、TabBar、Banner、GuideView、BadgeView、MarqueeView、WebView、SearchView等一系列的组件和丰富多彩的样式主题。

在提issue前,请先阅读【提问的智慧】,并严格按照issue模板进行填写,节约大家的时间。

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

X系列库快速集成

为了方便大家快速集成X系列框架库,我提供了一个空壳模版供大家参考使用: https://github.com/xuexiangjys/TemplateAppProject

除此之外,我还特别制作了几期视频教程供大家学习参考.


特征

  • 简洁优雅,尽可能少得引用资源文件的数量,项目库整体大小不足1M(打包后大约644k)。

  • 组件丰富,提供了绝大多数我们在开发者常用的功能组件。

  • 使用简单,为方便快速开发,提高开发效率,对api进行了优化,提供一键式接入。

  • 样式统一,框架提供了一系列统一的样式,使UI整体看上去美观和谐。

  • 兼容性高,框架还提供了3种不同尺寸设备的样式(4.5英寸、7英寸和10英寸),并且最低兼容到Android 17, 让UI兼容性更强。

  • 扩展性强,各组件提供了丰富的属性和样式API,可以通过设置不同的样式属性,构建不同风格的UI。


如何使用

在决定使用XUI前,你必须明确的一点是,此框架给出的是一整套UI的整体解决方案,如果你只是想使用其中的几个控件,那大可不必引入如此庞大的一个UI库,Github上会有更好的组件库。如果你是想拥有一套可以定制的、统一的UI整体解决方案的话,那么你就继续往下看吧!

添加Gradle依赖

1.先在项目根目录的 build.gradle 的 repositories 添加:

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

2.然后在dependencies添加:

dependencies {
...
//androidx项目
implementation 'com.github.xuexiangjys:XUI:1.1.7'

implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
}

【注意】如果你的项目目前还未使用androidx,请使用如下配置:

dependencies {
...
//support项目
implementation 'com.github.xuexiangjys:XUI:1.0.9-support'

implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.github.bumptech.glide:glide:4.8.0'
}

初始化XUI设置

1.调整应用的基础主题(必须)

必须设置应用的基础主题,否则组件将无法正常使用!必须保证所有用到XUI组件的窗口的主题都为XUITheme的子类,这非常重要!!!

基础主题类型:

  • 大平板(10英寸, 240dpi, 1920*1200):XUITheme.Tablet.Big

  • 小平板(7英寸, 320dpi, 1920*1200):XUITheme.Tablet.Small

  • 手机(4.5英寸, 320dpi, 720*1280):XUITheme.Phone

<style name="AppTheme" parent="XUITheme.Phone">

 <!-- 自定义自己的主题样式 -->

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

</style>

当然也可以在Activity刚开始时调用如下代码动态设置主题

@Override
protected void onCreate(Bundle savedInstanceState) {
XUI.initTheme(this);
super.onCreate(savedInstanceState);
...
}

2.调整字体库(对字体无要求的可省略)

(1)设置你需要修改的字体库路径(assets下)

//设置默认字体为华文行楷,这里写你的字体库
XUI.getInstance().initFontStyle("fonts/hwxk.ttf");

(2)在项目的基础Activity中加入如下代码注入字体.

注意:1.1.4版本之后使用如下设置注入

@Override
protected void attachBaseContext(Context newBase) {
//注入字体
super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase));
}

注意:1.1.3版本及之前的版本使用如下设置注入

@Override
protected void attachBaseContext(Context newBase) {
//注入字体
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}

混淆配置

-keep class com.xuexiang.xui.widget.edittext.materialedittext.** { *; }


代码下载:XUI.zip

收起阅读 »

Android支付组件

接入指南:1、导入libSdk 依赖工程2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)2....
继续阅读 »



接入指南:

1、导入libSdk 依赖工程

2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)

  • 2.1 拷贝assets/data.bin 文件到 项目中

3、项目中实际使用支付:具体使用看下文 ---> 调起支付 。


请配置正确的参数,否则支付宝和微信 会出现无法调起的情况。

//配置 AndroidManifest

    <activity
android:name="net.lbh.pay.PaymentActivity"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />

<activity-alias
android:name=".wxapi.WXPayEntryActivity"
android:exported="true"
android:targetActivity="net.lbh.pay.PaymentActivity" />
<!-- 微信支付 end -->


<!-- 支付宝 begin -->
<activity
android:name="com.alipay.sdk.app.H5PayActivity"
android:configChanges="orientation|keyboardHidden|navigation"
android:exported="false"
android:screenOrientation="behind"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!-- 支付宝 end -->


<!-- 银联支付 begin -->

<activity
android:name="com.unionpay.uppay.PayActivity"
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />

<activity
android:name="com.unionpay.UPPayWapActivity"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />

<!-- 银联支付 end -->


<!-- 微信 广播 start -->
<receiver android:name="net.lbh.pay.wxpay.AppRegister" >
<intent-filter>
<action android:name="com.tencent.mm.plugin.openapi.Intent.ACTION_REFRESH_WXAPP" />
</intent-filter>
</receiver>
<!-- 微信 广播 end -->


<!-- 微信支付 参数 appid, 需要替换成你自己的 -->
<meta-data
android:name="WXPAY_APP_ID"
android:value="替换成自己的 app id" >
</meta-data>
<meta-data
android:name="WXPAY_MCH_ID"
android:value="替换成自己的 MCH_ID" >
</meta-data>
<meta-data
android:name="WXPAY_API_KEY"
android:value="替换成自己的 api key" >
</meta-data>
<!-- 微信支付 参数 end 需要替换成你自己的 -->


<!-- 支付宝 参数 appid, 需要替换成你自己的 --> //如果是 超过10位数字,要在前边加 ,Eg: \0223987667567887653
<meta-data
android:name="ALIPAY_PARTNER_ID"
android:value="替换成自己的 partenr id" >
</meta-data>
<meta-data
android:name="ALIPAY_SELLER_ID"
android:value="替换成自己的 seller id" >
</meta-data>

<meta-data
android:name="ALIPAY_PRIVATE_KEY"
android:value="替换成自己的 private key" >
</meta-data>

<meta-data
android:name="ALIPAY_PUBLIC_KEY"
android:value="替换成自己的 public key" >
</meta-data>
<!-- 支付宝 参数 end 需要替换成你自己的 -->

// 初始化支付组件

	PayAgent payAgent = PayAgent.getInstance();
payAgent.setDebug(true);

// 代码初始化 参数, 此处针对场景,所有参数有 自己app server保管的时候,动态的支付配置下发参数
payAgent.initAliPayKeys(partnerId, sellerId, privateKey, publicKey);
payAgent.initWxPayKeys(appId, mchId, appKey)
// 初始化 银联支付 所需的 验签 参数
//payAgent.initUpPayKeys(PublicKeyPMModulus, publicExponent, PublicKeyProductModulus);
// 代码动态初始化为 可选

payAgent.initPay(this);

// 调起支付

    PayAgent.getInstance().onPay(payType, this, payInfo,
new OnPayListener() {

@Override
public void onStartPay() {

progressDialog.setTitle("加载中。。。");
progressDialog.show();
}

@Override
public void onPaySuccess() {

Toast.makeText(MainActivity.this,"支付成功!", 1).show();

if (null != progressDialog) {
progressDialog.dismiss();
}

}

@Override
public void onPayFail(String code, String msg) {
Toast.makeText(MainActivity.this,
"code:" + code + "msg:" + msg, 1).show();
Log.e(getClass().getName(), "code:" + code + "msg:" + msg);

if (null != progressDialog) {
progressDialog.dismiss();
}
}
});

支付参数说明:

PayType: 支付的支付方式,目前支持:

  • 1、PayAgent.PayType.WECHATPAY(微信支付);
  • 2、PayAgent.PayType.ALIPAY(支付宝);
  • 3、PayAgent.PayType.UPPAY(银联)。

Activity: 调起支付的 Activity

PayInfo:

/** 商品名称*/
private String subject;

/** 商品详细信息 商品的标题/交易标题/订单标题/订单关键字等。该参数最长为128个汉字*/
private String body;

/** 商品价格*/
private String price;

/** 商品订单号*/
private String orderNo;

/** 支付通知地址*/
private String notifyUrl;

OnPayListener: 支付监听器:

  • onStartPay() 开始支付,可以在此做 支付前准备提示
  • onPaySuccess(); 支付成功
  • onPayFail(String code, String msg); 支付失败,code和msg 均为第三方原样返回

配置第三方参数说明:

  • 1、支付宝:

注意:

  • 1、支付宝支付,调用支付宝时, 所有参数为必须向
  • 2、微信支付,orderNo 为必须项
  • 3、银联支付时,orderNo 为必须项 -4、关于支付后,通知回调,只有支付宝是 在客户端手动设置,其余都是在 后台配置。

注意事项:

  • 1、当测试时,可以使用Debug模式,开启方式为: PayAgent payAgent = PayAgent.getInstance(); payAgent.setDebug(true);

  • 2、调试模式(非正式环境,目前只有 银联): PayAgent payAgent = PayAgent.getInstance(); payAgent.setOnlieMode(false);

版本說明:

  • 1、银联支付:3.3.2
  • 2、支付宝:
  • 3、微信:

更新日志:

  • 2016.04.15更新:

  • 1、2016.4.14 银联更新sdk,更新银联支付控件为3.3.3

  • 2、去除银联客户端验签;添加银联需要权限(nfc等)

  • 1、更新银联支付控件为3.3.2

  • 2、添加调试模式(非正式环境模式、主要正对银联支付)

payAgent.setOnlieMode(false);

  • 3、添加银联 验证签名,初始化签名参数
  • 4、修改Demo ,测试 Demo能正常运行。

其他说明:



收起阅读 »

日志管理工具 - CocoaLumberjack

CocoaLumberjackCocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。首先,通过CocoaPods、Carthage、Swift Package Manager或手动安...
继续阅读 »

CocoaLumberjack

CocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。

首先,通过CocoaPodsCarthageSwift Package Manager或手动安装 CocoaLumberjack 然后使用DDOSLoggeriOS 10 及更高版本,或DDTTYLoggerDDASLLogger早期版本开始记录消息。

CocoaPods

platform :ios, '9.0'

target 'SampleTarget' do
use_frameworks!
pod 'CocoaLumberjack/Swift'
end


platform :ios, '9.0'

target 'SampleTarget' do
pod 'CocoaLumberjack'
end

Carthage

github "CocoaLumberjack/CocoaLumberjack"

用法

如果您使用 Lumberjack 作为框架,则可以@import CocoaLumberjack;除此以外,#import
[DDLog addLogger: [DDOSLogger sharedInstance ]]; //使用 os_log

DDFileLogger *fileLogger = [[DDFileLogger
alloc ] init ]; //文件记录器
fileLogger.rollingFrequency =
60 * 60 * 24 ; // 24 小时滚动
fileLogger.logFileManager.maximumNumberOfLogFiles =
7 ;
[DDLog
addLogger: fileLogger];

...


DDLogVerbose ( @"详细" );
DDLogDebug ( @"调试" );
DDLogInfo ( @"信息" );
DDLogWarn ( @"警告" );
DDLogError ( @"错误" );

Objective-C ARC 语义问题

将 Lumberjack 集成到现有的 Objective-C 中时,可能会遇到Multiple methods named 'tag' found with mismatched result, parameter type or attributes构建错误。

#define DD_LEGACY_MESSAGE_TAG 0在导入 CocoaLumberjack 之前添加或添加#define DD_LEGACY_MESSAGE_TAG 0或添加-DDD_LEGACY_MESSAGE_TAG=0Xcode 项目中的其他 C 标志OTHER_CFLAGS

快速日志后端

CocoaLumberjack 还附带了swift-log的后端实现只需将 CocoaLumberjack 作为依赖项添加到您的 SPM 目标(见上文),并将CocoaLumberjackSwiftLogBackend产品作为依赖项添加到您的目标。

然后,您可以将DDLogHandler其用作 swift-log 的后端,它将所有消息转发到 CocoaLumberjack 的DDLog您仍将通过 配置您想要的记录器和日志格式化程序DDLog,但将使用Loggerswift-log完成写入日志消息

在您自己的日志格式化程序中,您可以使用swiftLogInfoon 属性DDLogMessage来检索通过 swift-log 记录的消息的详细信息。


在大多数情况下,它比 NSLog 快一个数量级。

当您的应用程序启动时,只需一行代码即可配置 lumberjack。然后只需将您的 NSLog 语句替换为 DDLog 语句即可。(而且 DDLog 宏的格式和语法与 NSLog 完全相同,因此非常简单。)

一个日志语句可以发送到多个记录器,这意味着您可以同时记录到一个文件和控制台。想要更多?创建您自己的记录器(这很容易)并通过网络发送您的日志语句。或者到数据库或分布式文件系统。天空才是极限。

根据需要配置您的日志记录。更改每个文件的日志级别(非常适合调试)。更改每个记录器的日志级别(详细控制台,但简洁的日志文件)。更改每个 xcode 配置的日志级别(详细调试,但简洁发布)。将您的日志语句从发布版本中编译出来。为您的应用程序自定义日志级别的数量。添加您自己的细粒度日志记录。在运行时动态更改日志级别。选择您希望日志文件滚动的方式和时间。将您的日志文件上传到中央服务器。压缩归档日志文件以节省磁盘空间...


在以下情况下,此框架适合您:

  • 您正在寻找一种方法来追踪该领域不断出现的无法重现的错误。
  • 您对 iPhone 上超短的控制台日志感到沮丧。
  • 您希望将您的应用程序在支持和稳定性方面提升到一个新的水平。
  • 您正在为您的应用程序(Mac 或 iPhone)寻找企业级日志记录解决方案。

要求

当前版本的 Lumberjack 要求:

  • Xcode 12 或更高版本
  • Swift 5.3 或更高版本
  • iOS 9 或更高版本
  • macOS 10.10 或更高版本
  • watchOS 3 或更高版本
  • tvOS 9 或更高版本



收起阅读 »

视图添加闪烁效果的简单方法 - Shimmer

ShimmerShimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。Shimmer 最初是为了在Paper 中显示加载状态而开发的。用法要使用 Shimmer,请创建一个FBShimmeringView或FBS...
继续阅读 »

Shimmer

Shimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。

Shimmer 最初是为了在Paper 中显示加载状态而开发的

用法

要使用 Shimmer,请创建一个FBShimmeringViewFBShimmeringLayer并添加您的内容。要开始闪烁,请将shimmering属性设置YES

使标签闪烁的示例:

FBShimmeringView *shimmeringView = [[FBShimmeringView alloc ] initWithFrame: self .view.bounds];
[
self .view addSubview: shimmeringView];

UILabel *loadingLabel = [[UILabel
alloc ] initWithFrame: shimmeringView.bounds];
loadingLabel.textAlignment = NSTextAlignmentCenter;

loadingLabel.text = NSLocalizedString(
@" Shimmer " , nil );
shimmeringView.contentView = loadingLabel;


//开始闪烁。
shimmeringView.shimmering =
YES ;

还有一个示例项目。在示例中,您可以水平和垂直滑动以尝试各种闪烁参数,或点击以开始或停止闪烁。(要在本地构建示例,您需要打开FBShimmering.xcworkpace而不是.xcodeproj.)

安装

有两种选择:

  1. 微光ShimmerCocoapods 中可用
  2. 手动将文件添加到您的 Xcode 项目中。稍微简单一点,但更新也是手动的。

Shimmer 需要 iOS 6 或更高版本。


这个怎么运作

Shimmer 使用该-[CALayer mask]属性来启用闪烁,类似于 John Harper 2009 年 WWDC 演讲中所描述的内容(不幸的是不再在线)。Shimmer 使用 CoreAnimation 的计时功能在启动和停止微光时平滑过渡“节拍”。


demo及常见问题:https://github.com/facebookarchive/Shimmer

源码下载:Shimmer-master.zip



收起阅读 »

还不会用coil,你就out了

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语...
继续阅读 »

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语言,将协程、OKHttp、OKIO和AndroidX作为一等公民,以期打造成一个更加轻快、现代化的图片加载库。具体而言包含以下几个方面:



  • 发挥Kotlin的语言特性,利用扩展函数、内联、lambda参数以及密封类来创建简单优雅的API。

  • 利用了Kotlin协程强大的 可取消的非阻塞式异步编程和对线程最大化利用的特性。

  • 使用现代化的依赖库:OKHttp、OKIO基本上已经是目前大部分app的事实“标准”库,它们强大的特性让Coil避免了重复实现磁盘缓存和缓冲流;类似的,AndroidX-LifeCycle也是官方推荐的,Coil目前是唯一一个对其支持的图片加载库。

  • 轻量:Coil项目的代码量几乎只有Glide的1/8,更是远远小于Fresco;并且对APK仅增加了大约1500个方法(对于那些已经依赖的OKHttp和协程的app来说),和Picasso相当并显著低于Glide和Fresco。

  • 支持扩展:Coil的image-pipline主要由 Mappers , Fetchers , 和 Decoders 三个类组成,可以方便地用于自定义:扩展或覆盖默认行为,或增加对新的文件类型的支持。

  • 测试友好化:Coil的基础服务类是 ClassLoader ,它是一个接口,可以方便地编写对应的实现类进行测试;并且Coil同时提供了单例和非单例对象来支持依赖注入。

  • 没有annotation processing:annotation processing一般会降低编译速度,Coil通过Kotlin扩展函数来避免。


Coil目前支持其它图片加载库所包含的所有功能,除此之外它还有一个独特的特性:动态采样(Dynamic image sampling),简而言之就是可以在内存中只缓存了一个低质量的图片而此时需要显示同一个高质量的图片时,Coil可以先把低质量的图片作为 ImageView 的 placeHolder 并同时去磁盘缓存中读取对应的高质量图片最后以“渐进式”的方式替换并最终显示到视图中,例如最常见的从图片列表到预览大图的场景。
以上就是Coil目前的大致介绍,下面我们对Coil的API进行一个简单的使用预览和介绍。


API预览



// 加载一个基本的url(利用了扩展函数,对target无任何侵入)
imageView.load("https://www.website.com/image.jpg")

// Coil 支持加载 urls, uris, resources, drawables, bitmaps, files 等等
imageView.load(R.drawable.image)
imageView.load(File("/path/to/image.jpg"))
imageView.load(Uri.parse("content://com.android.externalstorage/image.jpg"))

// Requests的配置项可以通过load的lambda参数方式实现
imageView.load("https://www.website.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}

// 自定义targets,包含开始、成功和失败的回调
Coil.load(context, "https://www.website.com/image.jpg") {
target { drawable ->
// Handle the successful result.
}
}

// 通过使用挂起函数get来直接获取图片对象
val drawable = Coil.get("https://www.website.com/image.jpg")

github地址:https://github.com/coil-kt/coil

下载地址:main.zip

收起阅读 »

最快的图像加载库-FastImageCache

FastImageCache快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。对于像Path这样的图形丰富的应用程序,性能...
继续阅读 »

FastImageCache

快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。

对于像Path这样的图形丰富的应用程序,性能的一个重大负担是图像加载。从磁盘加载单个图像的传统方法太慢了,尤其是在滚动时。Fast Image Cache 就是专门为解决这个问题而创建的。

快速图像缓存的作用

  • 将相似大小和样式的图像存储在一起
  • 将图像数据保存到磁盘
  • 比传统方法更快地将图像返回给用户
  • 根据使用情况自动管理缓存过期
  • 利用基于模型的方法来存储和检索图像
  • 允许在将图像存储到缓存之前按模型处理图像


事实证明,从压缩的磁盘图像数据到用户可以实际看到的渲染核心动画层的过程非常昂贵。随着要显示的图像数量的增加,这种成本很容易导致帧速率显着下降。可滚动视图进一步加剧了这种情况,因为内容可以快速变化,需要快速处理时间才能保持 60FPS 的流畅。1

考虑从磁盘加载图像并将其显示在屏幕上时发生的工作流程:

  1. +[UIImage imageWithContentsOfFile:]使用Image I/OCGImageRef从内存映射数据创建一个此时,图像还没有被解码。
  2. 返回的图像被分配给一个UIImageView.
  3. 隐式CATransaction捕获这些层树修改。
  4. 在主运行循环的下一次迭代中,Core Animation 提交隐式事务,这可能涉及创建已设置为图层内容的任何图像的副本。根据图像,复制它涉及以下部分或全部步骤:2
    1. 缓冲区被分配用于管理文件 IO 和解压操作。
    2. 文件数据从磁盘读取到内存中。
    3. 压缩的图像数据被解码为其未压缩的位图形式,这通常是一个非常占用 CPU 的操作。3
    4. 然后 Core Animation 使用未压缩的位图数据来渲染图层。

这些成本很容易累积并扼杀感知的应用程序性能。特别是在滚动时,给用户呈现的用户体验不尽如人意,与 iOS 的整体体验不符。


解决方案

快速图像缓存使用各种技术最大限度地减少(或完全避免)上述大部分工作:

映射内存

快速图像缓存工作原理的核心是图像表。图像表类似于精灵表,通常用于 2D 游戏。图像表将相同尺寸的图像打包到一个文件中。该文件只打开一次,只要应用程序保留在内存中,就保持打开状态以供读取和写入。

图像表使用mmap系统调用将文件数据直接映射到内存中。没有memcpy发生。这个系统调用只是在磁盘上的数据和内存区域之间创建一个映射。

当请求图像缓存返回特定图像时,图像表(以恒定时间)在它维护的文件中找到所需图像数据的位置。该文件数据区域被映射到内存中,并创建一个新CGImageRef的后备存储映射的文件数据。

当返回的CGImageRef(包装成 a UIImage)准备好被绘制到屏幕上时,iOS 的虚拟内存系统页面中的实际文件数据。这是使用映射内存的另一个好处;VM 系统会自动为我们处理内存管理。此外,映射内存“不计入”应用程序的实际内存使用量。

以类似的方式,当图像数据被存储在图像表中时,会创建一个内存映射位图上下文。与原始图像一起,此上下文被传递到图像表的相应实体对象。该对象负责将图像绘制到当前上下文中,可选地进一步配置上下文(例如,将上下文裁剪为圆角矩形)或进行任何额外的绘制(例如,在原始图像上绘制叠加图像)。mmap将绘制的图像数据编组到磁盘,因此不会在内存中分配图像缓冲区。

未压缩的图像数据

为了避免昂贵的图像解压缩操作,图像表将未压缩的图像数据存储在它们的文件中。如果源图像被压缩,则必须首先解压缩图像表才能使用它。这是一次性成本。此外,可以利用图像格式系列为一组相似的图像格式只执行一次这种解压缩。

然而,这种方法有明显的后果。未压缩的图像数据需要更多的磁盘空间,压缩和未压缩文件大小之间的差异可能很大,尤其是对于 JPEG 等图像格式。出于这个原因,快速图像缓存最适用于较小的图像,尽管没有强制执行此操作的 API 限制。

字节对齐

对于高性能滚动,Core Animation 能够使用图像而无需首先创建副本是至关重要的。Core Animation 创建图像副本的原因之一是图像底层CGImageRef正确对齐的每行字节值必须是 的倍数8 pixels × bytes per pixel对于典型的 ARGB 图像,对齐的每行字节值是 64 的倍数。每个图像表都配置为从一开始就为 Core Animation 始终正确地对每个图像进行字节对齐。因此,当从图像表中检索图像时,它们已经处于 Core Animation 可以直接使用的形式,而无需创建副本。

为了便于项目集成,Fast Image Cache 可作为CocoaPod 使用

手动

创建图像格式

每个图像格式对应一个图像缓存将使用的图像表。可以使用相同的源图像来渲染它们存储在图像表中的图像的图像格式应该属于相同的图像格式系列有关如何确定适当的最大计数的更多信息,请参阅图像表大小

静态 NSString *XXImageFormatNameUserThumbnailSmall = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailSmall " ;
静态 NSString *XXImageFormatNameUserThumbnailMedium = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailMedium " ;
静态 NSString *XXImageFormatFamilyUserThumbnails = @" com.mycompany.myapp.XXImageFormatFamilyUserThumbnails " ;

FICImageFormat *smallUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
smallUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailSmall;

smallUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

smallUserThumbnailImageFormat.style = FICImageFormatStyle16BitBGR;

smallUserThumbnailImageFormat.imageSize = CGSizeMake(
50 , 50 );
smallUserThumbnailImageFormat.maximumCount =
250 ;
smallUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

smallUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


FICImageFormat *mediumUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
mediumUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailMedium;

mediumUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

mediumUserThumbnailImageFormat.style = FICImageFormatStyle32BitBGRA;

mediumUserThumbnailImageFormat.imageSize = CGSizeMake(
100 , 100 );
mediumUserThumbnailImageFormat.maximumCount =
250 ;
mediumUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

mediumUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


NSArray *imageFormats = @[smallUserThumbnailImageFormat, mediumUserThumbnailImageFormat];

配置图像缓存

一旦定义了一种或多种图像格式,就需要将它们分配给图像缓存。除了分配图像缓存的委托之外,没有其他可以在图像缓存本身上配置的内容。

FICImageCache *sharedImageCache = [FICImageCache sharedImageCache ];
sharedImageCache.delegate = self;

sharedImageCache.formats = imageFormats;


实体是符合FICEntity协议的对象实体唯一标识图像表中的条目,并且它们还负责绘制它们希望存储在图像缓存中的图像。已经定义了模型对象(可能由 Core Data 管理)的应用程序通常是合适的候选实体。

@interface  XXUser : NSObject 

@property ( nonatomic , assign , getter = isActive) BOOL active;
@property ( nonatomic , copy ) NSString *userID;
@属性非原子副本NSURL * userPhotoURL;

@结尾

这是该FICEntity协议的示例实现

- ( NSString *)UUID {
CFUUIDBytes UUIDBytes = FICUUIDBytesFromMD5HashOfString (_userID);
NSString *UUID = FICStringWithUUIDBytes (UUIDBytes);

返回UUID;
}


- (
NSString *)sourceImageUUID {
CFUUIDBytes sourceImageUUIDBytes = FICUUIDBytesFromMD5HashOfString ([_userPhotoURL absoluteString ]);
NSString *sourceImageUUID = FICStringWithUUIDBytes (sourceImageUUIDBytes);

返回sourceImageUUID;
}


- (
NSURL *)sourceImageURLWithFormatName:( NSString *)formatName {
return _sourceImageURL;
}


- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(
NSString *)formatName {
FICEntityImageDrawingBlockdrawingBlock = ^(
CGContextRef context, CGSize contextSize) {
CGRect contextBounds = CGRectZero ;
上下文边界。
大小= 上下文大小
CGContextClearRect(上下文,contextBounds);

//剪辑中等缩略图,使它们具有圆角
if ([formatName isEqualToString: XXImageFormatNameUserThumbnailMedium]) {
UIBezierPath clippingPath = [
self _clippingPath ];
[剪辑
路径添加剪辑];
}


UIGraphicsPushContext(上下文);
[图像
drawInRect: contextBounds];
UIGraphicsPopContext ();
};


返回绘图块;
}

理想情况下,实体的UUID永远不应该改变。这就是为什么在应用程序使用从 API 检索的资源的情况下,它与模型对象的服务器生成的 ID 很好地对应。

一个实体的可以改变。例如,如果用户更新了他们的个人资料照片,则该照片的 URL 也应更改。保持不变,标识相同的用户,但改变了个人资料照片网址将表明,有一个新的源图像。sourceImageUUID UUID

注意:通常,最好对用于定义UUID和 的任何标识符进行哈希处理sourceImageUUIDFast Image Cache 提供了实用功能来执行此操作。由于散列可能很昂贵,因此建议仅计算一次散列(或仅在标识符更改时)并存储在实例变量中。

当要求图像缓存为特定实体和格式名称提供图像时,该实体负责提供 URL。URL 甚至不需要指向实际资源——例如,URL 可能由自定义 URL 方案构成——但它必须是一个有效的 URL。

图像缓存仅使用这些 URL 来跟踪哪些图像请求已经在进行中;正确处理对同一图像的图像缓存的多个请求,而不会浪费任何精力。选择使用 URL 作为键控图像缓存请求的基础实际上补充了许多实际应用程序设计,其中图像资源(而不是图像本身)的 URL 包含在服务器提供的模型数据中。

注意:快速图像缓存不提供任何网络请求机制。这是图像缓存委托的责任。

最后,一旦源图像可用,实体就会被要求提供一个绘图块。将存储最终图像的图像表设置文件映射位图上下文并调用实体的绘图块。这使得每个实体可以方便地决定如何处理特定图像格式的源图像。

从图像缓存中请求图像

快速图像缓存在 Cocoa 常见的按需、延迟加载设计模式下工作。

XXUser *user = [self _currentUser];
NSString *formatName = XXImageFormatNameUserThumbnailSmall;
FICImageCacheCompletionBlock completionBlock = ^(id <FICEntity> entity, NSString *formatName, UIImage *image) {
_imageView.image = image;
[_imageView.layer addAnimation:[CATransition animation] forKey:kCATransition];
};

BOOL imageExists = [sharedImageCache retrieveImageForEntity:user withFormatName:formatName completionBlock:completionBlock];

if (imageExists == NO) {
_imageView.image = [self _userPlaceholderImage];
}

统计数据

以下统计数据是从演示应用程序的运行中测得的:

方法滚动性能磁盘使用情况RPRVT 1
传统的~35FPS568KB2.40MB1.06MB+1.34MB
快速图像缓存~59FPS2.2MB1.15MB1.06MB+0.09MB


demo下载及常见问题:https://github.com/path/FastImageCache

源码下载:FastImageCache-master.zip


收起阅读 »