注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android逆向之https

介绍HTTP协议发展史、状态码、方法,整理了几乎所有常见的头部,讲述TLS的单向认证流程,Android中HTTPS抓包方法、防抓包策略以及绕过防抓包策略思路。 HTTP协议 超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协...
继续阅读 »

介绍HTTP协议发展史、状态码、方法,整理了几乎所有常见的头部,讲述TLS的单向认证流程,Android中HTTPS抓包方法、防抓包策略以及绕过防抓包策略思路。



HTTP协议


超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用层最为广泛的一种网络协议。


发展史















































协议发展说明
HTTP/0.91991年定稿最早的HTTP协议,没有作为正式标准只有GET命令;没有请求头、请求体、返回头;服务器只能读取HTML文件以ASCII字符流返回给客户端;默认80端口
HTTP/1.01996年发布,正式作为标准引入了POST、HEAD等命令、请求头、响应头、状态码;提供了缓存、多字符集支持、multi-part、authorization、内容编码等;默认不是持久连接
HTTP/1.11997年发布,2015年前使用最广默认持久连接不必声明keep-alive;引入pipelining管道机制,同一TCP连接里同时发送多个请求,但服务器需按照请求顺序串行返回响应;请求头新增Host,使同一台物理服务器可以同时部署多个web服务
HTTPS互联网巨头大力推行在传统HTTP协议的TCP与HTTP之间加入一层SSL/TLS;通过混合加密、摘要算法、数字证书来保证安全性;使用443端口
SPDY2009年由Google公开,不是标准已逐渐被HTTP/2取代基于TLS,在HTTPS的SSL层与HTTP层之间增加一层SPDY层;支持多路复用,可在同一TCP连接并发处理多个HTTP请求;可以赋予请求的优先级顺序;支持请求头和响应头压缩;支持服务器向客户端主动推送、提示
HTTP/22015年发布,逐步覆盖市场基于SPDY的标准化协议,可在TCP上使用不是必须在TLS上;HTTPS连接时使用了NPN的规范版ALPN;消息头的压缩算法采用新算法HPACK,而SPDY采用DEFLATE;依然没有解决TCP对头阻塞问题
QUIC/HTTP32012由Google提出,2015年提交给IETF,下一代互联网标准传输协议不再是基于TCP而是通过UDP;使用 stream 进一步扩展了 HTTP/2 的多路复用;引入 Connection ID,使得 HTTP/3 支持连接迁移以及 NAT 的重绑定;含有一个包括验证、加密、数据及负载的 built-in 的TLS安全机制;将拥塞控制移出了内核,通过用户空间来实现;头部压缩更换成了兼容 HPACK的QPACK压缩方案

状态码


状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,共分为5种类型:































分类说明
1**表示请求已被接受,需要继续处理的临时响应
2**成功,操作被成功接收并处理
3**重定向,需要进一步的操作以完成请求
4**客户端错误,请求包含语法错误或无法完成请求
5**服务器错误,服务器在处理请求的过程中发生了错误

方法


方法的名称区分大小写,并且通常是一个简短的单词,由英文大写字母组成。


接收请求时,服务器尝试确定请求的方法,如果失败,则返回带有代码 501 和短语的响应消息 Not Implemented






























































方法说明1.0、1.1中支持的协议版本
GET获取资源1.0、1.1
POST传输实体主体1.0、1.1
PUT传输文件1.0、1.1
HEAD获得报文首部1.0、1.1
DELETE删除资源1.0、1.1
OPTIONS删除资源1.0、1.1
TRACE追踪路径1.1
CONNECT将服务器作为代理,让服务器代替用户去访问1.1
LINK建立和资源之间的联系1.0
UNLINK断开连接关系1.0

首部


http首部主要分为五大部分:



  1. 通用首部:各种类型的报文(请求、响应报文)都可以使用,提供有关报文最基本的信息。

  2. 请求首部:专用于请求报文的首部,用于给服务器提供相关信息,告诉服务器客户端的期望和能力。

  3. 响应首部:专用于响应报文的首部,用于告诉客户端是谁在响应以及响应者的能力。

  4. 实体首部:用于描述http报文的负荷(主体),提供了有关实体及其内容的相关信息。

  5. 扩展首部:非标准首部,由应用开发者定义的首部。


(以下整理的首部并非完全按照这五类划分,部分扩展首部也按照功能划入了请求首部、响应首部等部分)


通用首部

























































字段名说明示例
Cache-Control控制缓存的行为Cache-Control: no-cache
Connection逐跳首部、连接的管理(HTTP/1.1默认持久连接)Connection: close
Date创建报文的日期时间Date: Tue, 15 Nov 2010 08:12:31 GMT
PragmaHTTP/1.1之前版本的历史遗留字段,用来包含实现特定的指令Pragma: no-cache
Trailer说明传输中分块编码的编码信息Trailer: Max-Forwards
Transfer-Encoding逐跳首部,指定传输报文主体时使用的编码方式Transfer-Encoding: chunked
Upgrade升级为其他协议Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11
Via代理服务器的相关信息Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)
Warning错误通知Warning: 199 Miscellaneous warning

请求首部





























































































































































字段说明示例
Accept客户端能够接收的内容类型Accept: text/plain, text/html
Accept-Charset客户端可以接受的字符编码集Accept-Charset: iso-8859-5
Accept-Encoding端到端首部,告知服务器客户端能够处理的编码方式和相对优先级Accept-Encoding: compress, gzip
Accept-Language客户端可接受的自然语言Accept-Language: en,zh
AuthorizationWeb认证信息Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
DNT指示该用户的跟踪的偏好:0用户更喜欢在目标站点上进行跟踪;1用户不希望在目标站点上跟踪DNT: 1
Expect客户端要求的特殊服务器行为。若服务器不能理解或者满足,则须返回417状态,或者如果请求有其他问题,返回4xx状态Expect: 100-continue
Forwarded代理服务器的客户端的信息,此标头标准版本是X-Forwarded-For,X-Forwarded-Host与X-Forwarded-ProtoForwarded: for=192.0.2.60; proto=http; by=203.0.113.43
From用户的电子邮箱地址From: user@email.com
Host指定请求的服务器的域名和端口号Host: www.zcmhi.com
If-Match当客户端If-Match的值若与服务端的ETag一致,才会执行请求,否则会拒绝412If-Match: W/"67ab43", "54ed21", "7892dd"
If-Modified-Since若If-Modifed-Since字段值早于资源的更新时间,则希望服务端能处理该请求f-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT
If-None-Match如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变If-None-Match: “737060cd8c284d8af7ad3082f209582d”
If-Range告知服务器若指定的If-Range字段值和请求资源的ETag值一致时,则作为范围请求处理,否则返回全部资源If-Range: “737060cd8c284d8af7ad3082f209582d”
If-Unmodified-Since比较资源的更新时间,与If-Modified-Since相反If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT
Max-Forwards该字段以十进制整数形式指定可经过的服务器最大数目。服务器在往下一个服务器转发请求之前,会将Max-Forwards的值减1后重新赋值,当服务器接收到Max-Forwards值为0的请求时,则不再进行转发,而是直接返回响应Max-Forwards: 10
Proxy-Authorization代理服务器要求客户端的认证信息Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Public-Key-Pins将特定的加密公钥与特定的Web务器相关联,以降低伪造证书对MITM攻击的风险Public-Key-Pins: pin-sha256=""; pin-sha256=""; max-age=5184000; includeSubDomains; report-uri=""
Public-Key-Pins-Report-Only将针对违规的报告发送到头中report-uri指定的报告,但是,Public-Key-Pins如果违反了钉住规则,仍然允许浏览器连接到服务器Public-Key-Pins-Report-Only: pin-sha256="; pin-sha256=""; includeSubDomains; report-uri=""
Range实体的节点范围请求Range: bytes=5001-10000
Referer指定该请求是从哪个页面跳转页来的,常被用于分析用户来源等信息Referer: www.example.com/index.html
Referrer-Policy用于过滤Referrer报头的策略Referrer-Policy: origin-when-cross-origin
CookieHTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器Cookie: $Version=1; Skin=new;
TE逐跳首部,告知服务器客户端能够处理的编码方式和相对优先级TE: gzip, deflate; q=0.5
Upgrade-Insecure-Requests向服务器发送一个信号,表示客户对加密和认证响应的偏好,Upgrade-Insecure-Requests: 1
User-AgentHTTP 客户端程序的信息User-Agent: Mozilla/5.0 (Linux; X11)
X-Forwarded-For来表示 HTTP 请求端真实 IPX-Forwarded-For: IP0, IP1, IP2
X-Forwarded-Host可用于确定最初使用哪个主机X-Forwarded-Host: id42.example-cdn.com
X-Forwarded-Proto确定客户端和负载平衡器之间使用的协议确定客户端和负载平衡器之间使用的协议

响应首部






































































































字段说明示例
Accept-Ranges是否接受字节范围请求Accept-Ranges: bytes
Age从原始服务器到代理缓存形成的估算时间(以秒计,非负)Age: 12
ETag资源的匹配信息ETag: “737060cd8c284d8af7ad3082f209582d”
Expires响应过期的日期和时间Expires: Thu, 01 Dec 2010 16:00:00 GMT
Location配合 3xx : Redirection 的响应,提供重定向的 URILocation: www.example.com
Proxy-Authenticate代理服务器对客户端的认证方式Proxy-Authenticate: Basic
Retry-After如果实体暂时不可取,通知客户端在指定时间之后再次尝试Retry-After: 120
Set-CookieHttp CookieSet-Cookie: status-enable; expires=Tue, 05 Jul 2018 02:01:22 GMT; path=/; domain=.example.com;
Serverweb服务器信息Server: Apache/1.3.27 (Unix) (Red-Hat/Linux)
SourceMap响应报头链接生成的代码到一个源映射,使浏览器来重构原始源并在调试器呈现重构原始SourceMap: /path/to/file.js.map
Strict-Transport-Security通常缩写为HSTS,告诉客户端它应该只使用HTTPS,而不是使用HTTP进行通信Strict-Transport-Security: max-age=31536000; includeSubDomains
Tk显示了对相应请求的跟踪情况Tk: ! (under construction) Tk: ? (dynamic) Tk: G (gateway or multiple parties)
Vary告知下游的代理服务器,应当如何对以后的请求协议头进行匹配,以决定是否可使用已缓存的响应内容而不是重新从原服务器请求新的内容Vary: Accept-Encoding,User-Agent
WWW-Authenticate表明客户端请求实体应该使用的授权方案WWW-Authenticate: Basic
X-Content-Type-Options如果服务器发送响应头 "X-Content-Type-Options: nosniff",则script和styleSheet元素会拒绝包含错误的 MIME 类型的响应。这是一种安全功能,有助于防止基于 MIME 类型混淆的攻击X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control控制浏览器的DNS预读取功能X-DNS-Prefetch-Control: on
X-Frame-Options给浏览器指示允许一个页面可否在 , 或者 中展现的标记,网站可以使用此功能,来确保自己网站的内容没有被嵌套到别人的网站中去,也从而避免了点击劫持的攻击X-Frame-Options: ALLOW-FROM example.com/
X-XSS-Protection是 IE,Chrome 和 Safari 的一个特性,当检测到跨站脚本攻击 (XSS)时,浏览器将停止加载页面X-XSS-Protection: 1; mode=block

实体首部



































































字段说明示例
Allow服务器支持的HTTP请求方法Allow: GET, HEAD
Content-Disposition指示回复的内容是以内联的形式还是以附件的形式下载并保存到本地;也可在multipart/form-data 类型的应答消息体中,用来给出其对应字段的相关信息Content-Disposition: attachment; filename="filename.jpg"
Content-Encoding告知客户端服务器对实体的主体选用的内容编码方式Content-Encoding: gzip
Content-Language实体主体使用的自然语言Content-Language: zh-CN
Content-Length实体部分大小Content-Length: 15000
Content-Location返回报文主体返回资源对应的URIContent-Location: httpo://www.example.com/index.html
Content-MD5检查报文主体在传输过程中是否保持完整,对报文主体执行 MD5 算法获得218位二进制数,再通过 Base64 编码后将结果写入Content-MD5: ZTEwYWRjMzk0OWJhNTlhYmJlNTZlMDU3ZjIwZjg4M2U=
Content-Range针对范围请求,表示当前发送部分及整个实体大小Content-Range: bytes 5001-10000/10000
Content-type实体主体内对象的媒体类型Content-Type: text/html; charset=utf-8
Expires将资源失效日期告知客户端Expires: Wed, 04 Jul 2012 08:26:05 GMT
Last-Modified资源最终修改时间Last-Modified: wed, 25 May 2018 09:11:40 GMT

跨域资源共享首部


跨域资源共享标准新增了一组HTTP头部字段,属于扩展首部,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,对那些可能对服务器产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨域请求。服务器允许后才发起实际的HTTP请求。预检请求的返回中,服务器也可以通知客户端是否需要携带身份凭证。




















































字段说明示例
Access-Control-Allow-Credentials指示的请求的响应是否可以暴露于该页面,当true值返回时它可以被暴露,凭证是 Cookie ,授权标头或 TLS 客户端证书Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers用于预检请求中,列出了将会在正式请求的Access-Control-Request-Headers字段中出现的首部信息Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Methods在对预检请求的应答中明确了客户端所要访问的资源允许使用的方法或方法列表Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Origin指定了该响应的资源是否被允许与给定的origin共享Access-Control-Allow-Origin: developer.mozilla.org
Access-Control-Expose-Headers出了哪些首部可以作为响应的一部分暴露给外部Access-Control-Expose-Headers: Content-Length, X-Kuma-Revision
Access-Control-Max-Age表示预检请求的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息)可以被缓存多久Access-Control-Max-Age: 600
Access-Control-Request-Headers出现于预检请求中,用于通知服务器在真正的请求中会采用哪些请求头Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Request-Method出现于预检请求中,用于通知服务器在真正的请求中会采用哪种HTTP方法Access-Control-Request-Method: POST

安全性的不足



  1. 通信使用明文,内容可能会被窃听(可窃听)。

  2. 无法证明报文的完整性,内容有可能已遭篡改(可篡改)。

  3. 不验证通信方的身份,因此有可能遭遇伪装(可冒充)。


HTTPS协议


可以理解为HTTP+SSL/TLS, 即 HTTP 下加入 SSL/TLS 层,用于安全的 HTTP 数据传输,简单来说HTTP + 加密 + 认证 + 完整性保护 = HTTPS,用来解决HTTP协议安全性的不足。


HTTPS


SSL/TLS历史


HTTPS相比HTTP多出了SSL/TLS 层,SSL 协议原本由网景公司开发,后来被 IETF 标准化,正式名称叫做 TLS,TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3,TLS1.1和TLS1.0不支持HTTP2,目前应用最广泛的应该还是 TLS 1.2,SSL协议发展历史如下:















































协议发布时间状态
SSL 1.0未公布未公布
SSL 2.01995年已于2011年弃用 RFC6176
SSL 3.01996年已于2015年弃用 RFC7568
TLS 1.01999年RFC2246
TLS 1.12006年RFC4346
TLS 1.22008年RFC5246,目前最广泛应用
TLS 1.32018年RFC8446

TLS1.2单向认证流程


以目前使用最广泛的TLS1.2说明下认证流程即握手流程,认证分为单向和双向两种模式,单向认证客户端验证服务端证书合法即可访问,一般Web应用都是采用SSL单向认证的;双向认证需要客户端和服务器都需要持有证书,两者证书验证均合法才可以继续访问。


使用wireshark抓包TLS1.2,单向认证流程如下:
wireshark抓包TLS1.2




  1. 客户端发送Client Hello。将一个Unix时间戳、TLS版本、支持的所有加密套件、支持的签名算法、生成的随机数Random_C等发送给服务器。




  2. 服务器发送Server Hello。将服务器Unix时间戳、生成的随机数Random_S、协商的加密算法套件等发送给客户端。




  3. 服务器发送Certificate、Server Key Exchange、Server Hello Done。Certificate是数字证书,Server Key Exchange为公钥参数(有时也可不需要),Server Hello Done表明服务器已经将所有预计的握手消息发送完毕。




  4. 客户端发送Client Key Exchange、Change Cipher Spec、Encrypted Handshake Message。客户端首先需要校验证书,证书向上按照证书链逐级校验,每一级证书校验过程是通过拿到证书签发者(Issuer)的证书中的公钥(证书 = 使用者公钥 + 主体信息如公司名称等 + CA对信息的确认签名 + 指纹)对本级证书(Subject)的签名进行数学验证,并校验证书是否被吊销,是否在有效期,是否与域名匹配等,验证成功即证书有效,整个一级一级验证上去,形成信任链,如果校验不通过则中断连接,浏览器弹出警告,校验正确后解析得到服务器公钥,并发送Client Key Exchange、Change Cipher Spec、Encrypted Handshake Message。Client Key Exchange:生成一个随机数 Pre-master,并用证书公钥加密,通过Fuc(random_C, random_S, Pre-Master)生成一个协商密钥;Change Cipher Spec:通知服务器协商完成,以后就使用上面生成的协商密钥进行对称加密;Encrypted Handshake Message:结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥与算法进行加密。




  5. 服务器发送Change Cipher Spec、Encrypted Handshake Message。服务器使用私钥解密得到 Pre-master数值,基于之前交换的两个明文随机数 random_C 和 random_S,同样通过Fuc(random_C, random_S, Pre-Master)得到协商密钥,计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性,验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;encrypted_handshake_message:服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥与算法加密并发送到客户端。




  6. 客户端计算所有接收信息的hash值,并采用协商密钥解密encrypted_handshake_message,验证服务器发送的数据和密钥,验证通过则握手完成。




  7. 开始使用协商密钥与算法进行加密通信。(Encrypted Alert是由客户端或服务器发送,意味着加密通信因为某些原因需要中断,警告对方不要再发送敏感的数据)。




Android抓包HTTPS


HTTP/HTTPS抓包工具有不少,常见的电脑端抓包工具有fiddlerCharlesBurp SuitewhistleAnyProxy,Android端抓包APP也有HttpCanary,wireshark也可以抓包但是不可以解密HTTPS内容。


使用Charles抓包安卓HTTP很简单,将手机WIFI设置代理为Charles,步骤如下:



  1. 为方便设置代理,使手机与电脑处于同一局域网(连接同一个WIFI,或者电脑连到同一个路由器的LAN端口)。

  2. 电脑使用ipconfig查看局域网ip,并打开Charles。

  3. 手机连接的WIFI--高级设置--代理服务器,代理服务器填写电脑的局域网ip,Charles的代理端口默认8888。

  4. 电脑端Charles允许连接即可。

  5. Charles--Proxy--SSL Proxying Setting,Enable SSL Proxying打勾,add添加抓包的Host和Port,一般Port都是443,Host和Port都填*则抓包所有。


打开APP就能在Charles上看到抓包的HTTP,但是HTTPS都会显示为Unknown,因为还没有安装Charles的证书。手机设置代理以后,浏览器访问chls.pro/ssl下载Charles证书并安装。Android 7.0以下直接安装即可,但是Android 7.0及以上默认不信任用户自己安装的证书,而只信任系统预设的证书,解决方法有:




  • 手动制作Charles证书,按照Android系统预设的格式,并推到/system/etc/security/cacerts目录下,从而让系统把Charles证书当作系统证书




  • 如果使用Magisk实现的root,Magisk安装MagiskTrustUserCerts模块,模块原理同上,可以把自定义的用户证书当作系统证书




  • 反编译apk,资源文件中添加network_security_config.xml,修改AndroidManifest.xml(修改APP的网络安全配置,信任用户证书)


    AndroidManifest.xml修改:




    ...


    复制代码

    network_security_config.xml内容:











    复制代码



  • frida或者xposed hook实现证书信任


    在JSSE中证书信任管理器类实现了X509TrustManager接口,我们可以自己实现一个X509TrustManager,通过hook修改掉网络请求库的X509TrustManager配置。


    比如自定义X509TrustManager实现信任所有服务端的证书(无论是否过期、是否经过认证):


    public class TrustAllManager implements X509TrustManager {
    @Override
    public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType)
    throws java.security.cert.CertificateException {
    }

    @Override
    public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType)
    throws java.security.cert.CertificateException {
    }

    @Override
    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
    return new java.security.cert.X509Certificate[0];
    }
    }
    复制代码

    okhttp的X509TrustManager设置为:


    OkHttpClient.Builder builder = new OkHttpClient.Builder();
    builder.sslSocketFactory(createAllSSLSocketFactory(), new TrustAllManager());
    复制代码

    我们可以通过hook OkHttpClient.Builder类的sslSocketFactory方法,实现修改X509TrustManager配置,从而实现用户证书的信任。




HTTPS抓包原理


代理原理


Charles、fiddler、HttpCanary抓包都是基于代理实现的,对HTTPS的抓包原理其实也都差不多,类似于中间人攻击,原理如下(图片来源于谈移动端抓包方式和原理及如何防犯中间人攻击):


Charles抓包原理



  • TLS握手时拦截服务器证书,得到服务器公钥,并将Charles自己的证书发送给客户端,客户端原本校验Charles证书不通过,但我们可以手动使客户端信任Charles证书。

  • Charles拦截请求得到Random_S和Random_C等未加密信息,其他加密部分比如Pre-Master由于是使用Charles的证书公钥加密的,Charles可以使用自己的私钥解密得到内容,再使用服务器证书公钥重新加密后发送给服务器,从而与服务器完成TLS认证,并获取到密钥。

  • 每次发送HTTPS请求报文经过Charles,Charles再使用得到的对称密钥进行解密。


Android防抓包策略及绕过思路


Android上HTTPS抓包成本并不算高,使系统信任第三方证书就能够实现抓包,Android 7.0 (API 24)及以上虽默认不再信任用户CA,提高了安全性但抓包成本也不算高,还可添加其他防抓包策略进一步提高安全性。


设置无代理模式


由于Charles、fiddler这些抓包工具是基于代理实现的(wireshark不是基于代理,而是网卡抓包),所以可以将APP所用的HTTP客户端设置为无代理,设置之后HTTP客户端不会连接到代理服务器,这样的话Charles就无法直接抓包了,比如OkHttp配置无代理:


OkHttpClient.Builder()
.proxy(Proxy.NO_PROXY)
.build()
复制代码

绕过方案:



  1. 手动修改DNS。由上面的代理原理图可知,若请求不走代理,就要通过DNS解析获取ip地址再发送请求,我们可以直接修改Android的DNS配置,将请求域名解析到Charles代理服务器上,从而实现抓包。

  2. VPN流量转发。使用drony之类的APP先将手机请求导到VPN,再对VPN的网络进行Charles的代理。

  3. 使用frida或者xposed hookOkHttpClient.Builderproxy方法,使无代理配置不起作用。


增强本地证书校验


APP本地做证书校验时不仅仅校验公钥,并且设置更为严格的校验模式,直接安装的Charles证书将因为格式问题不能验证通过,可以通过实现X509TrustManager 接口实现。这种方式其实适用于Android 7.0以下,Android 7.0以上通过MagiskTrustUserCerts等方式安装的证书由于已经被当作系统内置证书,这种方式应该不再起作用。


SSL Pinning证书锁定


应用中只信任固定证书或是公钥,将可信 CA 限制在一个很小的 CA 集范围内,应用的服务器将使用这个集合,这样可以防止因泄露系统中其他 100 多个 CA 中的某个 CA 而破坏应用安全通道。


通常有两种锁定方式证书固定公钥固定。证书固定:将证书的某些字节码硬编码在用程序中,证书校验时检查证书中是否存在相同的字节码;公钥固定:网站会提供已授权公钥的哈希列表,指示客户端在后续通讯中只接受列表上的公钥。


OkHttp配置实现证书锁定,对特定的host做证书公钥验证,公钥经过Sha1算法hash一下,然后Base64加密一次,然后在结果前面加上字符串"sha256/"或者"sha1/":


CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(example.com, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();

OkHttpClient client = new OkHttpClient();
client.setCertificatePinner(certificatePinner);
复制代码

Android 7.0及以上实现证书锁定:





example.com

7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=

fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=



复制代码

绕过方案:



  1. 可以尝试Xposed+JustTrustMe模块或者JustTrustMePlus模块或者TrustMeAlready模块

  2. Frida+DroidSSLUnpinning脚本

  3. hook,需要反编译分析源码


TLS双向认证


TLS认证有单向认证和双向认证模式,双向认证除了客户端去验证服务器端的证书外,服务器也同时需要验证客户端的证书,如果没有通过验证,则会拒绝连接,如果通过验证,服务器获得用户的公钥。


绕过方案:双向认证需要客户端证书,所以APP内是要有证书的并且有操作证书的地方,hook这部分代码获取证书及密钥,再将证书格式转换一下,导入到Burp Suite或者Charles这些抓包软件中,实现抓包。


此外,APP内肯定有传输内容SSL解密的实现,针对这部分代码进行hook,也可直接拿到数据,这也是更为通用的获取请求内容的办法,实现方式可以参考Frida的脚本frida_ssl_logger


Android网络安全方面配置可以查看developer android,此外还可通过HTTPS请求的报文密、重要请求走Socket通信,APP加固防止hook等手段提高安全性。


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

zygote启动流程

对zygote的理解 在Android系统中,zygote是一个native进程,是所有应用进程的父进程。而zygote则是Linux系统用户空间的第一个进程——init进程,通过fork的方式创建并启动的。 作用 zygote进程在启动时,会创建一个Dalv...
继续阅读 »

对zygote的理解


在Android系统中,zygote是一个native进程,是所有应用进程的父进程。而zygote则是Linux系统用户空间的第一个进程——init进程,通过fork的方式创建并启动的。


作用


zygote进程在启动时,会创建一个Dalvik虚拟机实例,每次孵化新的应用进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面,从而使得每个应用程序进程都有一个独立的Dalvik虚拟机实例。


zygote进程的主要作用有两个:



  • 启动SystemServer。

  • 孵化应用进程。


启动流程


启动入口

Zygote进程在init进程中,通过解析init.zygote.rc配置文件,以service(服务)的方式启动并创建的。


以init.zygote32.rc为例来看下:


脚本讲解

//    system\core\rootdir\init.zygote32.rc
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
priority -20
user root
group root readproc reserved_disk
socket zygote stream 660 root system
socket usap_pool_primary stream 660 root system
onrestart write /sys/power/state on
onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart netd
onrestart restart wificond
writepid /dev/cpuset/foreground/tasks
复制代码


这段脚本要求 init 进程创建一个名为 zygote 的进程,该进程要执行的程序是“/system/bin/app_process”。并且为 zygote 进程创建一个 socket 资源 (用于进程间通信,ActivityManagerService 就是通过该 socket 请求 zygote 进程 fork 一个应用程序进程)。


后面的**--zygote**是参数,表示启动的是zygote进程。在app_process的main函数中会依据该参数决定执行ZygoteInit还是Java类。


启动过程

zygote要执行的程序便是system/bin/app_process,它的源代码在frameworks/base/cmds/app_process/app_main.cpp


App_main::main

int main(int argc, char* const argv[])
{
...
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {//是否有--zygote参数。这个是启动zygote进程的时候的参数
zygote = true;
//进程名称,设置为zygote
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {//是否有--start-system-server
startSystemServer = true;
....
if (zygote) {
//最最重要方法。。。如果是zygote进程,则启动ZygoteInit。
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
}
}
复制代码

AndroidRuntime::start

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
...
JNIEnv* env;
//重点方法 创建VM虚拟机,参数是指针,可以用于获取返回的值,可以使用env来和Java层来做交互
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
//重点方法 给虚拟机注册一些JNI函数,(系统so库、用户自定义so库 、加载函数等。)
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}

//找到类的main方法,并调用。如果是zygote的话,这里就会启动ZygoteInit类的main方法
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
/* keep going */
} else {
//调用main方法。这里通过JNI调用Java方法之后,Zygote(Native层)就进入了Java的世界,从而开启了Android中Java的世界。
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}
复制代码

App_main.main
AndroidRuntime.start
startVm//创建虚拟机
startReg//注册JNI函数
ZygoteInit.main//这里就进入到了Java层了
registerZygoteSocket//建立IPC的通讯机制
preload//预加载类和资源
startSystemServer//启动system_server
runSelectLoop//等待进程创建的请求
复制代码


对应的源码地址:
/frameworks/base/cmds/app_process/App_main.cpp (内含AppRuntime类)
/frameworks/base/core/jni/AndroidRuntime.cpp
/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
/frameworks/base/core/java/com/android/internal/os/Zygote.java
/frameworks/base/core/java/android/net/LocalServerSocket.java



Zygote进程的启动过程中,除了会创建一个Dalvik虚拟机实例之外,还会将Java运行时库加载到进程中,以及注册一些Android核心类的JNI方法到创建的Dalvik虚拟机实例中。


zygote进程初始化时启动虚拟,并加载一些系统资源。这样zygote fork出子进程之后,子进程也会继承能正常工作的虚拟机和各种系统资源,剩下的只需要装载APK文件的字节码就可以运行程序,。


Java应用程序不能以本地进程的形态运行,必须在一个独立的虚拟机中运行。如果每次都重新启动虚拟机,肯定就会拖慢应用程序的启动速度。


注意:APK应用程序进程被zygote进程孵化出来以后,不仅会获得Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库。


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

Android 开发也要懂得数据结构 - SparseArray源码

在之前分析 HashMap 就知道当容量达到 75% 时就需要扩容,那也就意味着 25% 的内存空间啥也不放,浪费掉了,为了解决这个问题,就有了 SparseArray。 本文章使用的是 JDK1.8 ,不同版本源码有差异。 可先食用 Android 开发也要...
继续阅读 »
  • 在之前分析 HashMap 就知道当容量达到 75% 时就需要扩容,那也就意味着 25% 的内存空间啥也不放,浪费掉了,为了解决这个问题,就有了 SparseArray

  • 本文章使用的是 JDK1.8 ,不同版本源码有差异。

  • 可先食用 Android 开发也要懂得数据结构 - HashMap源码


1.SparseArray特点



  • SparseArray的结构是 双数组 ,就是key和value都是数组,下标一一对应。

  • SparseArray虽然是 key-valye 结构,但是key只能是 int 类型,用于代替 HashMap<Integer,Object>,这也是缺点,还有 LongSparseArray

  • HashMap 处理 int 类型的key,是需要 装箱成Integer类型 ,消耗一些资源,而 SparseArray 就不需要装箱操作,更快一些。

  • HashMap 保存数据是以 Entry对象保存,还要计算hashCode,在内存方面是要大于 SparseArray。

  • SparseArray 的查找速度比较快,利用的是二分法查找,二分查找的要求就是key是有序排列的。

  • 二分查找虽然挺快的,数据量大的时候跟HashMap比就没有什么优势了,千级以下使用。


2.SparseArray常用的方法


2.1 基本参数



  • DELETED 是删除的位置放的东西,SparseArray 删除相当于是打上标记,就不需要移动数组,减少数组移动的耗时。

  • mGarbage 标志是如果有删除,就为true,用于后面的 gc() 方法的标记。

  • 可以看到 SparseArray 的 key 和 value 都是数组。

  • 还有一个长度mSize,与List,Map一样样,这个是实际数据的长度,不是容量的大小。


    private static final Object DELETED = new Object();
private boolean mGarbage = false;

@UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
private int[] mKeys;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
private Object[] mValues;
@UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
private int mSize;
复制代码

2.2 构造方法



  • 构造方法两种,可以选填容量大小或者不填,不填容量,容量默认就为10。

  • 如果填写的容量为0,那就会创建一个非常轻量的数组。


    /**
* Creates a new SparseArray containing no mappings.
*/
public SparseArray() {
this(10);
}

/**
* Creates a new SparseArray containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
//还没有放入数据,所以mSize为0。
mSize = 0;
}
复制代码

2.3 二分查找 ContainerHelpers.binarySearch(mKeys, mSize, key)



  • 显示利用二分法,找出要放入元素的 key 对应的下标,如果找不到就返回二分范围小值的取反。


    //比较简单的二分法查找
// This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;

while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];
//如果中间这个数小于目标值
if (midVal < value) {
//检索的范围最小就为中间+1
lo = mid + 1;
} else if (midVal > value) {
//如果大于,范围的最大就为中间-1
hi = mid - 1;
} else {
//找到就返回下标位置
return mid; // value found
}
}
//找不到就返回lo取反
return ~lo; // value not present
}
复制代码

2.4 放入元素 put(int key, E value)



  • 如果 key 位置已存在,直接覆盖。

  • 如果找不到 key 对应下标,且在范围内,有删除过闲置的位置,就把当前数据放在这个位置。

  • 如果这都不满足条件,就调用 gc() 方法整理一遍数据,把标记过删除的位置,干掉,再插入数据。

  • 插入元素如果容量不够,就扩容,如果原始容量小于4的,扩容成8,否则就以2倍的大小扩容。

  • 数组的插入需要移动后面位置的元素。


    /**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i >= 0) {
mValues[i] = value;
} else {
//取反
i = ~i;

//如果i小于长度,且i位置没东西,就放在i位置。
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}

//如果有垃圾(删除过东西),且数据容量长度大于等于key数组时
if (mGarbage && mSize >= mKeys.length) {
//整理一下数据
gc();

//整理后再次查找索引位置
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}

//插入数据
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}

//GrowingArrayUtils.insert
/**
* Primitive int version of {@link #insert(Object[], int, int, Object)}.
*/
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;

if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}

int[] newArray = new int[growSize(currentSize)];
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}

//GrowingArrayUtils.growSize
//扩容,如果容量小于4的,扩容成8,否则就以2倍的大小扩容。
/**
* Given the current size of an array, returns an ideal size to which the array should grow.
* This is typically double the given size, but should not be relied upon to do so in the
* future.
*/
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
复制代码

2.5 整理数据 gc()



  • 把非 DELETED 位置的数据,一个个往前移动。

  • mGarbage 设置为 false


    private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);

int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;

for (int i = 0; i < n; i++) {
Object val = values[i];

if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}

o++;
}
}

mGarbage = false;
mSize = o;

// Log.e("SparseArray", "gc end with " + mSize);
}
复制代码

2.6 删除元素 remove(int key)或者delete(int key)



  • 我们知道真正数组的删除,是要以移动后面的元素的,每次会造成大量的操作,所以改为标记清除,先打上标记,可以在放入元素时重新利用上空闲的位置,或者后面gc时再一次性清除掉。


    /**
* Alias for {@link #delete(int)}.
*/
public void remove(int key) {
delete(key);
}

/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
//用二分法,找到要删除数据对应的下标
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
复制代码

2.7 查找元素 get(int key)



  • 利用二分查找找出 key 对应的下标,然后返回同样下标位置的值。

  • 两个参数的方法,也可设置找不到放回的默认值,如果找不到就返回默认值,否则是null。


    /**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
public E get(int key) {
return get(key, null);
}

/**
* Gets the Object mapped from the specified key, or the specified Object
* if no such mapping has been made.
*/
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
复制代码

2.8 查找key对应的下标 indexOfKey(int key)



  • 如果有标记垃圾,先整理一遍再放回 key 对应的下标。


    /**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
* key is not mapped.
*/
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}

return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
复制代码

2.9 查找value对应的下标 indexOfValue(E value)



  • 查找之前依然会整理一次数据,不同位置都可能保存着这个value,所以遍历后,返回第一个,如果找不到就返回-1。


    /**
* Returns an index for which {@link #valueAt} would return the
* specified value, or a negative number if no keys map to the
* specified value.
* <p>Beware that this is a linear search, unlike lookups by key,
* and that multiple keys can map to the same value and this will
* find only one of them.
* <p>Note also that unlike most collections' {@code indexOf} methods,
* this method compares values using {@code ==} rather than {@code equals}.
*/
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}

for (int i = 0; i < mSize; i++) {
if (mValues[i] == value) {
return i;
}
}

return -1;
}
复制代码

2.10 长度size()



  • 返回数据的长度。


    /**
* Returns the number of key-value mappings that this SparseArray
* currently stores.
*/
public int size() {
if (mGarbage) {
gc();
}

return mSize;
}

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

View.post()为什么能准确拿到View的宽高?

老生常谈之------View.post() 起因:之前一群里的哥们问 Handler.post() 为什么会在 Activity 的 onResume() 之后执行,我找了一遍之后并没有找到原因,后来从这个问题我想起其他的问题 view.post() 为什...
继续阅读 »

老生常谈之------View.post()


起因:之前一群里的哥们问 Handler.post() 为什么会在 ActivityonResume() 之后执行,我找了一遍之后并没有找到原因,后来从这个问题我想起其他的问题 view.post() 为什么在 view.post() 之后为什么可以准确的获取到 view的宽高。


疑问🤔️:View.post()为什么会准确的获取到View的宽高?


public boolean post(Runnable action) {
//注释1:判断attachInfo如果不为空 直接调用attachInfo内部Handler.post()方法
//这样就有一个问题attachInfo在哪里赋值?这个问题先存疑。
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
//注释2:此时attachInfo是空的
getRunQueue().post(action);
return true;
}
复制代码

存疑1:attachInfo在哪里赋值?


我们点进去看一下 getRunQueue().post(action); 是何方神圣。


private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
复制代码

上面代码很简单,那么接下来就需要看看 HandlerActionQueue() 是什么玩意了。


public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;

//注释1
public void post(Runnable action) {
postDelayed(action, 0);
}
//注释2
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}

public void removeCallbacks(Runnable action) {}

//假装不知道这个是执行方法
public void executeActions(Handler handler) {}

public int size() {}

public Runnable getRunnable(int index) {}

public long getDelay(int index) {}

private static class HandlerAction {}
}

复制代码

先看注释1 可以看到这个post()方法 其实就是getRunQueue().post(action); 它内部调用了注释2方法 也就是postDelayed()
注释2简单看了里面的代码 发现大概逻辑只是对 我们postrunnable进行缓存起来,那么我们从哪里真正执行这个runnable呢?
我发现 我们一路跟进来直到postDelayed()方法时并没有看到对应的执行方法那么我们就放大招了


影·奥义!时光回溯。


我们看看getRunQueue()时 返回的mRunQueue在哪里调用的就好了。


	//不会上传图片...大小不懂得控制 所以用代码块 
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
//忽略的一些代码
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
复制代码

通过检查对mRunQueue的引用 可以看到view代码内只有这两个方法有所使用,那么第一个方法我们以及看过了 所以我们重点看下第二个方法。


从上面第二个方法 可以得知mAttachInfodispatchAttachedToWindow(AttachInfo info, int visibility)内的info赋值 那么 info又在哪里生成?先继续看下去。


通过对dispatchAttachedToWindow() 的调用关系可以发现以下方法会调用


private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
/.../
//注释1
host.dispatchAttachedToWindow(mAttachInfo, 0);
/.../
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量
performLayout(lp, mWidth, mHeight);//执行布局
performDraw();//执行绘制

}
复制代码

可以看到 host内会调用这个方法,并且将mAttachInfo作为参数传入,而这个host是一个DecorView


为什么是DecorView? 我们可以反推回去验证
通过源码我们得知 hostViewRootImpl 中的mView的成员变量
mView检查赋值的地方 可以看到以下代码:


 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;//对mView 赋值
/.../
}
}
}
复制代码

那什么时候能调用到setView()呢?
可以看到setView() 并不是静态方法,所以要调用并需要引用实例才可以。


那么我们可以看看构造方法


public ViewRootImpl(Context context, Display display) {
mContext = context;
/.../
//注释1
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
}
复制代码

可以看到注释1处的代码我们能够得知 mAttachInfo是在ViewRootImpl构造器中创建出来的。


我们再对构造器查看调用 可以发现再 WindowManagerGlobaladdView()方法 会创建ViewRootImpl


     public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
/.../
root = new ViewRootImpl(view.getContext(), display);
}
复制代码

WindowManagerGlobaladdView()方法 又被WindowManagerImpladdView()调用


   @Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
复制代码

再对这个方法进行调用检查可以看到


@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView(); //注释1
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l); //注释2
} else {
a.onWindowAttributesChanged(l);
}
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
}
复制代码

可以在注释1处:会返回一个DecorView 并且在注释2处添加进入。
至此 我们可以得知ViewRootImpl处的mView就是 DecorView


知识点:
并且我们可以从上面代码看出在onResumeview 才会被添加在window内并且执行view的测量布局绘制 这就是为什么在onCreate()时获取到view宽高会是0的原因,因为那时view都没有添加进window呢!!!


时间再次穿梭 回到ViewRootImplperformTraversals()


既然已知mView就是DecorView 那么这个DecorView是一个继承于FrameLayoutViewGroup
我们在DecorViewFrameLayout内没有找到对dispatchAttachedToWindow()方法的处理,就自然而然的来到了ViewGroup处。


 @Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
//遍历调用 这样最终会回到View的dispatchAttachedToWindow()
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
}
复制代码

这时候我们又回到了ViewdispatchAttachedToWindow()方法内的mRunQueue.executeActions(info.mHandler);并点击去看源码


  public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;//注释1
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);//注释2
}
mActions = null;
mCount = 0;
}
}
复制代码

注释1处 我们可以看到mActions其实是我们用view.post方法时 传入的runnable的存储数组。
注释2处 就会将runnable 交给handler.post()方法并且添加进这个Handler所持有的Looper内部的MessageQueue中。


至此我们可以大概的进行一次总结了。


总结:


1:View内部的mAttachInfo 会在ViewdispatchAttachedToWindow()方法内赋值
dispatchDetachedFromWindow()赋值为null,并且mAttachInfo的根本是在 ViewRootImpl的构造器内创建的,所以我们就可以知道当viewattchInfo不为空时 这个 view是已经被添加进窗口内的,如果为null就说明view没有在window内。


2: 我们能通过view.post()正确的获取View的宽高主要得益于Android内的生命周期是被Handler所驱动的,所以当ViewRootImplActivityonResume()生命周期内被创建时,其实主线程的Handler 是在执行处理一个Message的流程中,虽然我们从上面ViewRootImpl内的performTraversals()源码中看到 view缓存的runnable会在performMeasure(), performLayout(),performDraw()这些方法前先被post出去并且添加到MessageQueue链表中,但是这些runnable是属于下一个Message的,而performMeasure(), performLayout(),performDraw()这三个方法是属于本次Message的逻辑,只有本次消息处理完成Handler内部的Looper才会进行下一次消息的处理,最终保证了 View.post()能够正确的拿到View的宽高。



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

收起阅读 »

ARouter原理与缺陷解析

前言 本文主要包括以下内容 1.为什么需要ARouter及ARouter的基本原理 2.什么是APT及ARoutr注解是如何生效的? 3.ARouter有什么缺陷? 4.什么是字节码插桩,及如何利用字节码插桩优化ARouter? 为什么需要ARouter 我们...
继续阅读 »

前言


本文主要包括以下内容

1.为什么需要ARouterARouter的基本原理

2.什么是APTARoutr注解是如何生效的?

3.ARouter有什么缺陷?

4.什么是字节码插桩,及如何利用字节码插桩优化ARouter?


为什么需要ARouter


我们知道,传统的Activity之间通信,通过startActivity(intent),而在组件化的项目中,上层的module没有依赖关系(即便两个module有依赖关系,也只能是单向的依赖)

那么如何实现在没有依赖的情况下进行界面跳转呢?

ARoutr帮我们实现了这点


使用ARouter的原因就是为了解耦,即没有依赖时可以彼此跳转


什么是APT


APTAnnotation Processing Tool的简称,即注解处理工具。

它是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。

我们常用的ButterKnife,其原理就是通过注解处理器在编译期扫描代码中加入的@BindView@OnClick等注解进行扫描处理,然后生成XXX_ViewBinding类,实现了view的绑定。


ARouter中使用的注解处理器就是javapoet

(1)JavaPoet是square推出的开源java代码生成框架

(2)简洁易懂的API,上手快

(3)让繁杂、重复的Java文件,自动化生成,提高工作效率,简化流程

(4) 相比原始APT方法,JavaPoet是OOP的


ARoutr的注解是如何生效的?


我们在使用ARouter时都会在Activity上添加注解


@Route(path = "/kotlin/test")
class KotlinTestActivity : Activity() {
...
}

@Route(path = "/kotlin/java")
public class TestNormalActivity extends AppCompatActivity {
...
}
复制代码

这些注解在编译时会被arouter-compiler处理,使用JavaPoet在编译期生成类文件

生成的文件如下所示:


public class ARouter$$Group$$kotlin implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/kotlin/java", RouteMeta.build(RouteType.ACTIVITY, TestNormalActivity.class, "/kotlin/java", "kotlin", null, -1, -2147483648));
atlas.put("/kotlin/test", RouteMeta.build(RouteType.ACTIVITY, KotlinTestActivity.class, "/kotlin/test", "kotlin", new java.util.HashMap<String, Integer>(){{put("name", 8); put("age", 3); }}, -1, -2147483648));
}
}

public class ARouter$$Root$$modulekotlin implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("kotlin", ARouter$$Group$$kotlin.class);
}
}
复制代码

如上所示,将注解的key与类的路径通过一个Map关联起来了

只要我们拿到这个Map,即可在运行时通过注解的key拿到类的路径,实现在不依赖的情况下跳转


如何拿到这个Map呢?


ARouter缺陷


ARouter的缺陷就在于拿到这个Map的过程

我们在使用ARouter时都需要初始化,ARouter所做的即是在初始化时利用反射扫描指定包名下面的所有className,然后再添加map

源码如下


public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;

// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
// These class was generated by arouter-compiler.
//反射扫描对应包
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
//
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}

PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
logger.info(TAG, "Load router map from cache.");
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
....
}
}
复制代码

如上所示:

1.初次打开时会利用ClassUtils.getFileNameByPackageName来扫描对应包下的所有className

2.在初次扫描后会存储在SharedPreferences中,这样后续就不需要再扫描了,这也是一个优化

3.以上两个过程都是耗时操作,即是ARouter初次打开时可能会造成慢的原因

4.那有没有办法优化这个过程,让第一次打开也不需要扫描呢?


利用字节码插桩优化ARouter首次启动耗时


我们再看看上面的代码


public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
....
}
}

private static void loadRouterMap() {
//registerByPlugin一直被置为false
registerByPlugin = false;
}
复制代码

在初始化时,会在扫描之前,判断registerByPlugin,如果我们需要的map已经被插件注册了,那也就不需要进行下面的耗时操作了

但是我们可以看到在loadRouterMap中,registerByPlugin一直被设为false

registerByPlugin是不是一直没有生效?

这里面其实用到了字节码插桩来在loadRouterMap方法中插入代码


什么是编译插桩?


顾名思义,所谓编译插桩就是在代码编译期间修改已有的代码或者生成新代码。实际上,我们项目中经常用到的 Dagger、ButterKnife 甚至是 Kotlin 语言,它们都用到了编译插桩的技术。

理解编译插桩之前,需要先回顾一下Android项目中.java文件的编译过程:

image.png

从上图可以看出,我们可以在 1、2 两处对代码进行改造。

1.在.java文件编译成.class文件时,APTAndroidAnnotation 等就是在此处触发代码生成。

2.在.class文件进一步优化成.dex文件时,也就是直接操作字节码文件,这就是字码码插桩


ARouter注解生成用了第一种方法,而启动优化则用了第二种方法



ASM是一个十分强大的字节码处理框架,基本上可以实现任何对字节码的操作,也就是自由度和开发的掌控度很高.

但是其相对来说比AspectJ上手难度要高,需要对Java字节码有一定了解.

不过ASM为我们提供了访问者模式来访问字节码文件,这种模式下可以比较简单的做一些字节码操作,实现一些功能。

同时ASM可以精确的只注入我们想要注入的代码,不会额外生成一些包装代码,所以性能上影响比较微小。


关于ASM使用的具体细节可以参见:深入探索编译插桩技术(四、ASM 探秘)


字节码插桩对ARouter具体做了什么优化?


//源码代码,插桩前
private static void loadRouterMap() {
//registerByPlugin一直被置为false
registerByPlugin = false;
}
//插桩后反编译代码
private static void loadRouterMap() {
registerByPlugin = false;
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
}
复制代码

1.插桩前源码与插桩后反编译代码如上所示

2.插桩后代码即在编译期在loadRouterMap中插入了register代码

3.通过这种方式即可避免在运行时通过反射扫描className,优化了启动速度


插件使用


使用Gradle插件实现路由表的自动加载


apply plugin: 'com.alibaba.arouter'

buildscript {
repositories {
jcenter()
}

dependencies {
classpath "com.alibaba:arouter-register:?"
}
}
复制代码

1.可选使用,通过ARouter提供的注册插件进行路由表的自动加载

2.默认通过扫描dex的方式进行加载,通过gradle插件进行自动注册可以缩短初始化时间,同时解决应用加固导致无法直接访问dex文件,初始化失败的问题

3.需要注意的是,该插件必须搭配api 1.3.0以上版本使用!

4.ARouter插件基于AutoRegister进行开发,关于其原理的更多介绍可见:AutoRegister:一种更高效的组件自动注册方案


总结


本文主要讲述了

1.使用ARouter的根本原因是为在互相不依赖的情况下进行页面跳转以实现解藕

2.什么是APTARoutr注解生成的代码解析

3.ARouter的缺陷在于首次初始化时会通过反射扫描dex,同时将结果存储在SP中,会拖慢首次启动速度

4.ARouter提供了插件实现在编译期实现路由表的自动加载,从而避免启动耗时,其原理是字节码插桩


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

浅谈Constraints,Layout,Display的点点滴滴

浅谈Constraints,Layout,Display的点点滴滴神经骚栋关注赞赏支持前言这篇博客完全是因为 浅谈Masonry的使用技巧 才引出来的,如果不是内容太多,也不会单独写一篇博客来记录,在9102一整年中我基本与普通UI开发无缘,大部分工作是对La...
继续阅读 »

浅谈Constraints,Layout,Display的点点滴滴

神经骚栋

前言

这篇博客完全是因为 浅谈Masonry的使用技巧 才引出来的,如果不是内容太多,也不会单独写一篇博客来记录,在9102一整年中我基本与普通UI开发无缘,大部分工作是对Layout进行操作绘制,以及使用CoreGraphics框架绘制各种图形,所以对Layout和Display的系统方法还是比较了解,近期又开始使用Masonry,所以对Constraints相关系统方法需要有所了解,而且在 浅谈Masonry的使用技巧 这篇博客中的优化部分不得不提出Constraints相关系统方法对其的影响。那么我们依照惯例,从基础的API开始进行吧。这里我基本上是从苹果的API抄录过来的,各位大佬可以自行去苹果API中心查看。

基础API方法介绍

Constraints 部分

  • needsUpdateConstraints 这个方法主要是用来判断当前View是否需要调用 updateConstraints 方法,如果在这个方法调用之前有约束发生了改变,调用这个方法返回值就是YES,但是它不会触发updateConstraints 方法的执行,只用用来判别当前控件是否需要调用更新约束方法。

- (BOOL)needsUpdateConstraints;
  • setNeedsUpdateConstraints 这个方法主要是用来标记当前控件是否需要调用updateConstraints 方法,看好了,只是标记,而不是调用。 当给控件标记上需要刷新约束,如果程序没有直接强制刷新(调用updateConstraintsIfNeeded),那么会在系统RunLoop的下个周期开始时调用 updateConstraints 方法。

- (void)setNeedsUpdateConstraints;
  • updateConstraints 这个方法是更新约束方法,在自定义控件中我们可以重写这个方法来添加我们自己的约束,对于updateConstraints 调用时机只能会有两种,一个是系统RunLoop的下个周期当发现需要更新布局(needsUpdateConstraints的值为YES)的时候,会自动调用该方法,或者是大佬们手动调用 updateConstraintsIfNeeded 来进行直接刷新。

- (void)updateConstraints;
  • updateConstraintsIfNeeded 这个方法是直接强制立即调用 updateConstraints 方法,不需要等待Runloop下个周期,也不会管 needsUpdateConstraints 的返回值是否为YES,反正就是强制执行就对了。

- (void)updateConstraintsIfNeeded;
  • updateViewConstraints 这个方法是在iOS6之后添加到UIViewController中的,具体作用和updateConstraints 方法类似。调用时机是 self.view 的 needsUpdateConstraints 值为YES 或 [self.view updateConstraintsIfNeeded]; 都可。这个方法极大的方便了控制器本身的View的布局调整。

- (void)updateConstraintsIfNeeded;

Layout 部分

  • layoutSubviews 这个方法是动态调整子视图的布局,这个方法的调用时机是当前控件或者子控件的bounds 发生改变的时候就会调用。

- (void)layoutSubviews;
  • layoutIfNeeded 是立即强制执行layout操作的方法,但layoutSubviews 可能不会执行,因为如果控件或者子控件的bounds 没有发生改变时,layoutSubviews是不会执行的,所以说控件或者子控件的bounds 发生改变是 layoutIfNeeded 调起 layoutSubviews 的前提条件。

- (void)layoutIfNeeded;
  • setNeedsLayout 这个方法和上面的setNeedsUpdateConstraints作用类似,但它不是用来标记的,而是让布局失效的(可以看做间接导致了bounds的改变),所以如果在其调用下方调用 layoutIfNeeded 会立即调起 layoutSubviews 方法,或者等待Runloop下一个周期由系统调起 layoutSubviews。

- (void) setNeedsLayout;

Display部分

  • drawRect 这个方法主要是当View控件需要自定义绘制内容的时候,一般会写在这个方法中。绘制上下文对象需要通过 UIGraphicsGetCurrentContext() 函数来获取,官方文档 中都写的明明白白的了(内容太多了,懒癌发作,大家自行去看吧)。这里就不过多叙述了。

- (void)drawRect:(CGRect)rect;
  • drawInContext 这个方面主要是CALayer中自定义绘制内容的时候,一般都会写在这个方法中。ctx 这个参数是绘制上下文对象,不需要额外获取了。

- (void)drawInContext:(CGContextRef)ctx;
  • setNeedsDisplay 这个方面主要是标记UIView或CALayer是否要刷新,看好了,是标记!而不是直接刷新,其作用和 setNeedsUpdateConstraints 非常的类似。

- (void)setNeedsDisplay;
  • displayIfNeeded 这个方法主要让CALayer直接进行强制绘制,UIView中没有该方法。所以UIView的重新绘制只能先使用setNeedsDisplay来标记,等待系统RunLoop的下一个周期开始进行重绘。

- (void)displayIfNeeded;
  • needsDisplay 这个方法判别CALayer是否需要刷新,只是用来判断,没有别的作用。UIView没有该方法。

- (BOOL)needsDisplay;
  • display 官方建议不要直接调用这个方法,CALayer对象绘制适当的时机调用此方法来绘制CALayer对象其中的内容。

- (void)display;

Auto Layout Process 自动布局过程

那么三种是如何关联起来的呢?主要是通过 Auto Layout Process 来关联在一起的,网上这个资料很多,苹果官方的我没有找到的在哪,所以我找到了最开始的版本 ,具体内容如下所示。

与使用springs and struts(autoresizingMask)比较,Auto layout在view显示之前,多引入了两个步骤:updating constraints 和laying out views。每一个步骤都依赖于上一个。display依赖layout,而layout依赖updating constraints。 updating constraints→layout→display

第一步:updating constraints,被称为测量阶段,其从下向上(from subview to super view),为下一步layout准备信息。可以通过调用方法setNeedUpdateConstraints去触发此步。constraints的改变也会自动的触发此步。但是,当你自定义view的时候,如果一些改变可能会影响到布局的时候,通常需要自己去通知Auto layout,updateConstraintsIfNeeded。

自定义view的话,通常可以重写updateConstraints方法,在其中可以添加view需要的局部的contraints。

第二步:layout,其从上向下(from super view to subview),此步主要应用上一步的信息去设置view的center和bounds。可以通过调用setNeedsLayout去触发此步骤,此方法不会立即应用layout。如果想要系统立即的更新layout,可以调用layoutIfNeeded。另外,自定义view可以重写方法layoutSubViews来在layout的工程中得到更多的定制化效果。

第三步:display,此步时把view渲染到屏幕上,它与你是否使用Auto layout无关,其操作是从上向下(from super view to subview),通过调用setNeedsDisplay触发,

因为每一步都依赖前一步,因此一个display可能会触发layout,当有任何layout没有被处理的时候,同理,layout可能会触发updating constraints,当constraint system更新改变的时候。

需要注意的是,这三步不是单向的,constraint-based layout是一个迭代的过程,layout过程中,可能去改变constraints,有一次触发updating constraints,进行一轮layout过程。示意图如下所示。


原作者也提到了另外的一个坑,那就是如果你每一次调用自定义layoutSubviews都会导致另一个布局传递,那么你将会陷入一个无限循环中。 这其中主要原因还是constraint-based layout是一个迭代的过程,在上图的 updating constraints 和 layout 中成了一个死循环了。

视图渲染流程

由上一个模块我们可以得知一个View视图的调用顺序为 updating constraints→layout→display,那么对应到具体方法就是 updateConstraints→layoutSubViews→drawRect:

再详细的说一下,那就是,当我们修改View视图约束的时候,会触发 setNeedsUpdateConstraints 方法,然后触发 updateConstraints 方法,随后就紧接着触发 layoutSubViews,同时苹果官方已经为我们暴露了UIViewController中本身View视图的updateConstraints上层方法 updateViewConstraints,当UIViewController中本身View视图setNeedUpdate Constraints被调用的时候,这时候就会在合适的时机自动调用updateViewConstraints方法.

反观UIViewController的生命周期流程,我们可以具体到如下表格顺序.


触发时机分析

这个模块我们就触发时机再总结一下,其实在上面的基础API的方法中都介绍了,但是比较杂乱,

updateViewConstraints 与 updateConstraints
这两个的触发时机是一致的,那么就是当 调用 needsUpdateConstraints 值为YES 的时候,就肯定会调用updateViewConstraints 或者 updateConstraints.那么在View内部的这个判别布尔值又是由什么决定呢?情况一是添加,修改,删除约束的时机,二是手动调用 setNeedsUpdateConstraints 的时机.这两种时机都会造成布尔值发生改变从而调起 updateViewConstraints 或 updateConstraints .

layoutSubviews
layoutSubviews的触发时机只有一种情况,那就是 自身或者子视图的 bounds 发生了改变. .这也解释了当我们创建一个视图的时候如果使用的CGRectZero的时候实际上不会调用 layoutSubviews 方法.

drawRect 与 drawInContext
这两个方法的调用时机又和 updateViewConstraints 与 updateConstraints 非常的相似,只有当 调用 setNeedsDisplay 才会触发调用.但两者又有很大的区别.drawRect是UIView中的方法,drawInContext是CALayer中方法,drawRect调用时机智能是RunLoop的下一个周期开始,不能立即调用,但是drawInContext却可以通过直接调用displayIfNeeded开直接调用,不用等待RunLoop的下一个周期开始.而且 needsDisplay 只是CALayer中的方法,UIView没有此方法.

总结

OK,写到这里基本上系统的各种约束,布局,绘制API大家都了解的差不多了,这对我们后期代码时机的把握有着很好的帮助,欢迎各位大佬自己手动试验,欢迎大家在评论区指导批评.

转自:https://www.jianshu.com/p/983f2237cfa7

收起阅读 »

iOS 多线程之performSelector、死锁

1. performSelector//在当前线程延迟1s执行,响应了OC语言的动态性:延迟到运行时才绑定方法[self performSelector:@selector(aaa) withObject:nil afterDelay:1];// 回到主线程,...
继续阅读 »

1. performSelector

//在当前线程延迟1s执行,响应了OC语言的动态性:延迟到运行时才绑定方法
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
// 回到主线程,waitUntilDone:是否将该回调方法执行完再执行后面的代码
// 如果为YES:就必须等回调方法执行完成之后才能执行后面的代码,说白了就是阻塞当前的线程
// 如果是NO:就是不等回调方法结束,不会阻塞当前线程
[self performSelectorOnMainThread:@selector(aaa) withObject:nil waitUntilDone:YES];
// 开辟子线程
[self performSelectorInBackground:@selector(aaa) withObject:nil];
//在指定线程执行
[self performSelector:@selector(aaa) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];
  • 需要注意的是:如果是带afterDelay的延时函数,会在内部创建一个NSTimer,然后添加到当前线程的Runloop中。也就是如果当前线程没有开启runloop,该方法会失效。在子线程中,需要启动runloop(注意调用顺序)

[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];
  • performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行

  • 下面代码片段的test方法会去执行吗?

dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
});

这里的test方法是不会去执行的,原因在于- (void)performSelector: withObject: afterDelay:这个方法要创建提交任务到runloop上的,而gcd底层创建的线程是默认没有开启对应runloop的,所有这个方法就会失效。
而如果将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行(将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也是会去执行的)

2. 死锁

  • 死锁就是队列引起的循环等待,一个比较常见的死锁例子:主队列同步

- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"deallock");
});
}
  • 在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。
    想避免这种死锁,可以将同步改成异步dispatch_async或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决

  • 同样,下边的代码也会造成死锁:

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});

外面的函数无论是同步还是异步都会造成死锁。这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁。
解决方法也和上边一样,将里面的同步改成异步dispatch_async或者将serialQueue换成其他串行或并行队列,都可以解决

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

收起阅读 »

ios设计模式之简单工厂模式

最近一直在阅读OC编程之道(ios设计模式解析)一书(往期文章中我也将电子版的下载链接分享了出来)。其中包括23种设计模式和7种设计原则,如下图(此图为网络图片):在这里不过多的介绍设计模式和设计原则的问题了,感兴趣的同学可以自行去查阅资料,我在这里只介绍一种...
继续阅读 »

最近一直在阅读OC编程之道(ios设计模式解析)一书(往期文章中我也将电子版的下载链接分享了出来)。其中包括23种设计模式和7种设计原则,如下图(此图为网络图片):


  • 在这里不过多的介绍设计模式和设计原则的问题了,感兴趣的同学可以自行去查阅资料,我在这里只介绍一种设计模式及其代码,那就是简单工厂模式。

  • 何时使用工厂方法

     1、编译时无法准确预期要创建的对象的类;

     2、类想让其子类决定在运行时创建什么;

     3、类有若干辅助类为其子类,而你想将返回哪个子类这一信息局部化;

  • 在Cocoa Touch框架中应用的工厂方法

     工厂方法在Cocoa Touch框架中几乎随处可见,常见的两步对象创建法[[SomeClass alloc] init]。有时,我们已经注意到有一些便利方法返回类的实例。例如,NSNumber有很多numberWith *方法,其中两个是numberWithBool:和numberWithChar:。他们是类方法,也就是说我们向NSNumber发送[NSNumber numberWithBool:YES],以获得与传入参数同类型的各种NSNumber实例。与如何创建NSNumber的具体子类型的实例有关的所有细节,都由NSNumber的类工厂方法负责。[NSNumber numberWithBool:YES]的情况是,方法接受值YES,并把NSNumber的内部子类的一个实例初始化,让它能够反映传入的值YES。我们曾提到有个工厂方法模式的变体,抽象类用它生成具体子类。NSNumber中的这些numberWith *方法是这个变体的一个例子。它们不是用来被NSNumber的私有子类重载的,而是NSNumber创建合适对象的便利方式。

  • 简单的代码逻辑,提现一下工程方法的模式

     文章中我拿生活中日常饮用的饮料来做例子,我们首先会有一个父类,名字是:Drinks,代表饮料。还有三个继承Drinks的子类,名字分别为:可乐CocaCola芬达Fender矿泉水MineralWater。然后我们会有一个专门的工厂类DrinksFactory来管理生产何种饮料,DrinksFactory通过一个枚举和一个方法来生产。

  • 下面我将代码呈现给大家(gitHub工程地址:https://github.com/TianTeng6661/SimpleFactory.git,喜欢的给个星,谢谢)

     1、Drinks类

import UIKit
class Drinks: NSObject {
func drinksColor(){
NSLog("饮料颜色")
}
}

      2、CocaCola类

import UIKit
class CocaCola: Drinks {
override func drinksColor(){
NSLog("可口可乐是褐色")
}
}

     3、Fender类

import UIKit
class Fender: Drinks {
override func drinksColor(){
NSLog("芬达是橙色")
}
}

     4、MineralWater类

import UIKit
class MineralWater: Drinks {
override func drinksColor(){
NSLog("矿泉水是透明色")
}
}

     5、DrinksFactory类

import UIKit

enum DrinkType:Int {
case DrinkCocaCola = 0 //可口可乐
case DrinkFender = 1 //芬达
case DrinkMineralWater = 3 //矿泉水
}

class DrinksFactory: NSObject {

func createDrinksWithType(drinkstype:DrinkType) -> Drinks {
switch drinkstype {
case .DrinkCocaCola:do {
let color = CocaCola()
return color;
}
case .DrinkFender:do{
let fender = Fender()
return fender;
}
case .DrinkMineralWater:do{
let mineralWater = MineralWater()
return mineralWater;
}
}
}
}

当我们在需要生产的时候我们调用方法为

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = UIColor.red

let cocaCola:Drinks = DrinksFactory().createDrinksWithType(drinkstype: DrinkType.DrinkCocaCola)
print(cocaCola .drinksColor()) //可口可乐是褐色
let fender:Drinks = DrinksFactory().createDrinksWithType(drinkstype: DrinkType.DrinkFender)
print(fender .drinksColor()) //芬达是橙色
let mineralWater:Drinks = DrinksFactory().createDrinksWithType(drinkstype: DrinkType.DrinkMineralWater)
print(mineralWater .drinksColor()) //矿泉水是透明色

}
}

这样我们就简单实现了一个工厂方法,通过一个工厂类来管理所有饮料的生产,而且我们在需要多生产一种类型的饮料的时候,只需要创建此饮料的类,然后在工厂方法中实现就OK了。

我在这里只是抛砖引玉,只是在讲解模式,这里面我们还有好多可以扩展的,比如不同的饮料有不同的口味,配方等等,共有的属性我们可以写在父类来实现,但是每个子类也可以有自己特有的属性。

转自:https://www.jianshu.com/p/5c54c8ec0385

    

收起阅读 »

Cocoapods 1.8 版本改用 CDN 服务

Cocoapods 1.7.2 版本开始增加 CDN 支持但默认没有启用,1.8 版本的发布舍弃了原始完整克隆的 Specs 仓库改用 CDN 服务。CDN 利用的是免费且强大的 jsDelivr CDN 服务,该 CDN 网络在国内是有备案因此速度和稳定性都...
继续阅读 »


Cocoapods 1.7.2 版本开始增加 CDN 支持但默认没有启用,
1.8 版本的发布舍弃了原始完整克隆的 Specs 仓库改用 CDN 服务。

CDN 利用的是免费且强大的 jsDelivr CDN 服务,该 CDN 网络在国内是有备案因此速度和稳定性都会有很好的保证。该提案其实在去年已经有人使用 Cocoapods Plugin 的方式实现并向社区贡献 PR。


那么 CDN 支持相比之前的机制有啥优势呢?难道是把 Pods 的仓库和源码都托管到 CDN 网络了吗,其实并不是的。


友情提醒:本文只重点分析 Pods 下载的机制,不展开其他方面,以下只是 pod install 执行顺序中的一部分,如果你想了解 Cocoapods 都干了什么可以前往这篇文章查阅。


老的机制


第一步先检查本地 ~/.cocoapods/repo/master 目录是否存在,没有直接克隆 https://github.com/Cocoapods/Specs.git 仓库,这步在国内来说特别费时间正常下载下来目录应该是 2G+,如果有其他 source 源(比如私有源)会重复刚才的操作。


第二步安装 Podfile 每个 Pod 去在各个源中寻找对应的版本,从版本的 .podspec 文件解析获取组件的地址,这个可能是 http、git、svn、hg 中的任意一个,获取到之后开始下载(默认是在 ~/Library/Caches/CocoaPods 做缓存目录)


新的机制


第一步分析 Podfile 里面的 source ,如果没有走默认 Cocoapods 的配置(1.8 以上是 https://cdn.cocoapods.org ,之前的还是 Cocoapods/Spec), 如果本地不存在官方 cdn 的 repo 名字是 trunk 的保留字,自己无法创建。如果有自定义的 source 会追加上去 sources 列表。


$httpHEAD0Cache-Control:public,max-age=0,must-revalidateConnection:keep-aliveContent-Length:924280Content-Type:text/plain;charset=UTF-8Date:Sat,09Nov201907:06:15GMTEtag:"acf0d284f3a8e82e0d66ba1a91cd30b9-ssl"Server:NetlifyStrict-Transport-Security:max-age=31536000X-NF-Request-ID:50b466cd-ce9e-4326-b5bb-0d29a193ae4b-7809449">https://cdn.cocoapods.org/all_pods.txtHTTP/1.1200OKAccept-Ranges:bytesAge:0Cache-Control:public,max-age=0,must-revalidateConnection:keep-aliveContent-Length:924280Content-Type:text/plain;charset=UTF-8Date:Sat,09Nov201907:06:15GMTEtag:”acf0d284f3a8e82e0d66ba1a91cd30b9-ssl”Server:NetlifyStrict-Transport-Security:max-age=31536000X-NF-Request-ID:50b466cd-ce9e-4326-b5bb-0d29a193ae4b-7809449

第二步检查或下载每个 source,每个 source 会检查是否是 cdn 类型(使用 HEAD 请求检查是否包含 /all_pods.txt)文件:


cdn 类型,下面详细解释


其他类型,走原来的老的逻辑,不再赘述


第三步,下载 Cocoapods-version.yml 并缓存 etag,下载 /Cocoapods-version.yml 并取 headers 的第一个 etag 的值存为 /Cocoapods-version.yml.etag,如果存在 etag 会比对一样就不需要下载, 链接支持根目录和其他目录,支持 301 跳转。


Cocoapods-version.yml


—-min:1.0.0last:1.8.4prefix_lengths:-1-1-1


第四步,分析 Pod 并获取 pod 的版本信息,比如 Podfile 我增加了一个 pod “AFNetworking”,把 pod 名字做 MD5 后的值取 Cocoapods-version.yml 的 prefiexlength 数组长度的值单字母拆分用下划线分割按照规则拼成文件名 all_pods_versions({fragment}).txt (如果prefix_length 为 0 则只会去下载 /all_pods_versions.txt)


比如:prefix_lengths 数组大小为 3,AFNetworking MD5 后 a75d452377f396bdc4b623a5df25820 则匹配前三位 a75 拆分后 a_7_5 后查找 cdn url 路径的 /all_pods_versions_a_7_5.txt 下载下来后的内容:


Fuse/0.1.0/0.2.0/1.0.0/1.1.0/1.2.0


GXFlowView/1.0.0


JFCountryPicker/0.0.1/0.0.2


JVEmptyElement/0.1.0


第五步,下载 pod 的所有版本的 .podspec 文件,从上面的文件按照每行寻找第一段的名字,把后面的所有版本按照上面获取到的 prefix_lengths 的值(例如 AFNetworking 是 a, 7 , 5) /Specs/a/7/5/AFNetworking/{version}/AFNetworking.podspec.json 一次下载,并保存 etag 为 /Specs/a/7/5/AFNetworking/{version}/AFNetworking.podspec.json.etag,这个 etag 作用上面已经讲过,如果没有找到的话就会直接报错。


AddingspecrepotrunkwithCDNhttps://cdn.cocoapods.org/ CDN:trunkRelativepathdownloaded:CocoaPods-version.yml,saveETag:”031c25b97a0aca21900087e355dcf663-ssl” CDN:trunkRelativepath:CocoaPods-version.ymlexists!Returninglocalbecausecheckingisonlyperfomedinrepoupdate CDN:trunkRelativepathdownloaded:all_pods_versions_a_7_5.txt,saveETag:”5b32718ecbe82b0ae71ab3c77120213f-ssl” CDN:trunkRedirectingfromhttps://cdn.cocoapods.org/Specs/a/7/5/AFNetworking/0.10.0/AFNetworking.podspec.jsontohttps://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/a/7/5/AFNetworking/0.10.0/AFNetworking.podspec.json CDN:trunkRelativepathdownloaded:Specs/a/7/5/AFNetworking/0.10.0/AFNetworking.podspec.json,saveETag:W/“a5f00eb1fdfdcab00b89e96bb81d48c110f09220063fdcf0b269290bffc18cf5”

Cocoapods trunk 源的目录结构:


.cocoapods repo trunk .url#=> https://cdn.cocoapods.org/Cocoapods-version.yml# => 从 https://cdn.cocoapods.org/CocoaPods-version.yml 下载的文件Cocoapods-version.yml.etag# 上一个请求的第一个 etag 值存下来all_pods_versions_a_7_5.txt# 参考上面的备注all_pods_versions_a_7_5.txt.etag# 上一个请求的第一个 etag 值存下来


第六步和老的机制第二步一样同样最终还是会寻找 podspec 里面下载地址去下载, 也就是说真正 CDN 缓存加速的只有原有 Specs 必要的 podspec 文件,而不会加速 Pod 真正源地址,改机制只是减轻了本地更新官方 Specs 源的麻烦以及维护一个巨大的本地文件存储,这也是中心化机制的一个心结。


结语


这个机制大大减少了本地需要占一个较大存储的问题,尤其是初次 pod install 时间长的情况,但 Pod 库本身还是各自的 地址本质上无法解决安装 Pod 消耗时间过长的问题。


via icyleaf


链接:https://www.jianshu.com/p/79c004614d06


收起阅读 »

uniapp你是真的坑!!

最近要做一个锚点的效果,于是用到了这个方法,先获取节点信息,然后根据节点高度与页面滚动距离得到所需滑动的距离,但是这里有一个大坑,搞了两天,百思不得其解!就是h5端是正常的,app上不行,看了文档后也不存在兼容问题,于是,这里省下一百字骂人的话。。。uni.c...
继续阅读 »

最近要做一个锚点的效果,于是用到了这个方法,先获取节点信息,然后根据节点高度与页面滚动距离得到所需滑动的距离,但是这里有一个大坑,搞了两天,百思不得其解!就是h5端是正常的,app上不行,看了文档后也不存在兼容问题,于是,这里省下一百字骂人的话。。。

uni.createSelectorQuery().in(this).select("#one").boundingClientRect(data => {
uni.pageScrollTo({
duration:200,
scrollTop: that.scrollTop + data.top-44
});
}).exec();

问题:h5上一切正常,app上只有初次事件触发—页面滚动是正常的,再次触发时,就报错,是这样的报错:

//uniappnmsl
h.push is not a function

问题解决:

//设置duration  这里是页面滚动时的滚动效果
duration:200 => duration:0,

然后
就解决了,就解决了!

uniapp 你该长大了,要学会自己更新bug了

最后,祝uniapp长命百岁,新年快乐

原文链接:https://segmentfault.com/a/1190000021222154
收起阅读 »

h5转uniapp项目技术总结

h5项目转uniapp项目总结why先说一下为什么要用uniapp,主要是因为之前我们刚做完云闪付小程序(webview页面),老板又让我们做抖音小程序(后面还会做各种小程序),于是我们就想到了uniapp,之前也没做过想想也挺激动的项目目录├── READM...
继续阅读 »

h5项目转uniapp项目总结

why

先说一下为什么要用uniapp,主要是因为之前我们刚做完云闪付小程序(webview页面),老板又让我们做抖音小程序(后面还会做各种小程序),于是我们就想到了uniapp,之前也没做过想想也挺激动的

项目目录

├── README.md

├── babel.config.js

├── dist

├── node_modules

├── package-lock.json

├── package.json

├── postcss.config.js
├── public

├── src


├── App.vue
├── api
├── assets
├── components
├── config
├── main.js
├── manifest.json
├── mixins
├── pages
├── pages.json
├── pagesub
├── services
├── static
├── store
├── uni.scss
└── utils

├── tsconfig.json

├── vue.config.js

├── yarn-error.log

└── yarn.lock

条件编译

/** #ifdef 平台名称 **/ 

你的css或者js代码

/** #endif **/

样式

scoped 样式穿透
/deep/ 选择器 {}

// vue.config 配置less全局变量引入
let path = require('path');
module.exports = {
// 全局使用less变量
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [
path.resolve(__dirname, 'src/assets/theme.less') // 我的主题变量文件地址
]
}
}
}


插件

符合easycom命名规范可以省略引入组件的步骤

vuex

store数据改变页面未更新,我使用了一个mixin来解决,大概思路是混入需要更新的属性,在onShow钩子函数中执行
// mixin
export default {
data() {
return {
userInfo: {}
}
},
methods: {
getUserInfo() {
this.userInfo = this.$store.getters.userInfo
}
}
}

// 页面 重新赋值
onShow() {
this.getUserInfo()
}


路由

  • Vue的路由全写在pages里面
  • 路由跳转使用uniapp api
  • 页面title设置参考uniapp API

原生组件层级过高

页面的textarea层级过高盖住了popup弹窗

  • 使用cover-view提高popup组件层级(头条小程序不支持cover-view)
  • 使用hidden属性动态显示隐藏原生组件,popup组件弹出隐藏原生组件,反之亦然

最后

一开始写是愉悦的,改样式bug是痛苦的,结局是还算是好的。

收起阅读 »

ConstraintLayout 约束布局

约束布局 约束布局概念 ConstraintLayout 可让您使用扁平视图层次结构(无嵌套视图组)创建复杂的大型布局。它与 RelativeLayout相似,其中所有的视图均根据同级视图与父布局之间的关系进行布局,但其灵活性要高于 RelativeLayou...
继续阅读 »

约束布局


约束布局概念


ConstraintLayout 可让您使用扁平视图层次结构(无嵌套视图组)创建复杂的大型布局。它与 RelativeLayout相似,其中所有的视图均根据同级视图与父布局之间的关系进行布局,但其灵活性要高于 RelativeLayout,并且更易于与 Android Studio 的布局编辑器配合使用。


要在 ConstraintLayout 中定义某个视图的位置,您必须为该视图添加至少一个水平约束条件和一个垂直约束条件。每个约束条件均表示与其他视图、父布局或隐形引导线之间连接或对齐方式。每个约束条件均定义了视图在竖轴或者横轴上的位置;因此每个视图在每个轴上都必须至少有一个约束条件,但通常情况下会需要更多约束条件。


当您将视图拖放到布局编辑器中时,即使没有任何约束条件,它也会停留在您放置的位置。不过,这只是为了便于修改;当您在设备上运行布局时,如果视图没有任何约束条件,则会在位置 [0,0](左上角)处进行绘制。


从 Android Studio 2.3 起,官方的模板默认使用 ConstraintLayout


ConstraintLayout 添加到项目中


如需在项目中使用 ConstraintLayout,请按以下步骤操作:




  1. 确保您的 maven.google.com 代码库已在模块级 build.gradle 文件中声明


     repositories {
    google()
    }
    复制代码

    2将该库作为依赖项添加到同一个文件中,如以下示例所示。请注意,最新版本可能与示例中显示的不同:


    dependencies {
    implementation "androidx.constraintlayout:constraintlayout:2.0.4"
    }
    复制代码



转换布局


如需将现有布局转换为约束布局,请按以下步骤操作:




  1. Android Studio 中打开您的布局,然后点击编辑器窗口底部的 Design 标签页。




  2. ComponentTree 窗口中,右键点击该布局,然后点击 Convert layout to ConstraintLayout




img


Constraintlayout基本使用


相对定位,基线



  • layout_constraintLeft_toLeftOf

  • layout_constraintLeft_toRightOf

  • layout_constraintRight_toLeftOf

  • layout_constraintRight_toRightOf

  • layout_constraintTop_toTopOf

  • layout_constraintTop_toBottomOf

  • layout_constraintBottom_toTopOf

  • layout_constraintBottom_toBottomOf

  • layout_constraintBaseline_toBaselineOf //基础线

  • layout_constraintStart_toEndOf

  • layout_constraintStart_toStartOf

  • layout_constraintEnd_toStartOf

  • layout_constraintEnd_toEndOf


img


居中




  • app:layout_constraintBottom_toBottomOf=parent




  • app:layout_constraintLeft_toLeftOf=parent




  • app:layout_constraintRight_toRightOf=parent




  • app:layout_constraintTop_toTopOf=parent


    img




边距



  • android:layout_marginStart

  • android:layout_marginEnd

  • android:layout_marginLeft

  • android:layout_marginTop

  • android:layout_marginRight

  • android:layout_marginBottom


img


隐藏边距



  • layout_goneMarginStart

  • layout_goneMarginEnd

  • layout_goneMarginLeft

  • layout_goneMarginTop

  • layout_goneMarginRight

  • layout_goneMarginBottom


偏移量(Bias)



  • layout_constraintHorizontal_bias

  • layout_constraintVertical_bias


img


img


角度定位



  • layout_constraintCircle : 设置一个控件id

  • layout_constraintCircleRadius : 设置半径

  • layout_constraintCircleAngle :控件的角度 (in degrees, from 0 to 360)


imgimg


宽高约束Ratio


Raito可以根据控件一个边尺寸比重生成另一个边的尺寸。Ration必须设置一个控件宽高尺寸为odp(MATCH_CONSTRAINT)


 //根据宽的边生成高的边按比重1:1
<Button android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1" />
复制代码

 //高比16:9
<Button android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
复制代码


app:layout_constraintVertical_chainStyle="",app:layout_constraintHorizontal_chainStyle=""



  • CHAIN_SPREAD :有点像LinearLayout里面元素设置比重1:1:1 平均分布。

  • CHAIN_SPREAD_INSIDE:链的两边元素靠边,里面元素平均分配距离。

  • CHAIN_PACKED:链的元素挨到一起。


img


img


辅助工具


优化器Optimizer


app:layout_optimizationLevel对使用约束布局公开的优化配置项



  • none : 不启动优化

  • standard : 仅优化直接约束和屏障约束(默认)

  • direct : 优化直接约束

  • barrier : 优化屏障约束

  • chain : 优化链约束 (experimental)

  • dimensions :优化尺寸测量(experimental), 减少测量匹配约束布局的节点


障碍Barrier


app:barrierDirection=""


image-20210422184856292.png


有点像弱化(轻量级)的基础线通过设置指向的方向在此方向位置最远处生成一个虚拟线做一个阻挡作用的线。


<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="button9,button10,button11" />

<Button
android:id="@+id/button9"
android:layout_width="65dp"
android:layout_height="50dp"
android:text="Button"
tools:layout_editor_absoluteX="108dp"
tools:layout_editor_absoluteY="341dp" />

<Button
android:id="@+id/button10"
android:layout_width="203dp"
android:layout_height="49dp"
android:text="Button"
tools:layout_editor_absoluteX="84dp"
tools:layout_editor_absoluteY="242dp" />

<Button
android:id="@+id/button11"
android:layout_width="146dp"
android:layout_height="49dp"
android:text="Button"
tools:layout_editor_absoluteX="71dp"
tools:layout_editor_absoluteY="437dp" />


</androidx.constraintlayout.widget.ConstraintLayout>
复制代码

image-20210422172658792.png


Group


Group可以同意控制引用的控件集合的visible状态。


<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">


<androidx.constraintlayout.widget.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="button9,button10,button11" />

<Button
android:id="@+id/button9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
tools:layout_editor_absoluteX="46dp"
tools:layout_editor_absoluteY="241dp" />

<Button
android:id="@+id/button10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
tools:layout_editor_absoluteX="171dp"
tools:layout_editor_absoluteY="241dp" />

<Button
android:id="@+id/button11"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
tools:layout_editor_absoluteX="296dp"
tools:layout_editor_absoluteY="237dp" />


</androidx.constraintlayout.widget.ConstraintLayout>
复制代码

image-20210422160952772.png


指示线Guideline


//已parent start作为边开始计算


app:layout_constraintGuide_begin=""


//已parent end作为边开始计算


app:layout_constraintGuide_end=""


//百分比的位置


app:layout_constraintGuide_percent=""


<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="196dp" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="336dp" />
复制代码

image-20210422151939691.png


占位符Placeholder


<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.widget.Placeholder
android:id="@+id/placeholder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:content="@+id/textview"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_background"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:onClick="toSetGoneWithPlaceHolder"
/>


</androidx.constraintlayout.widget.ConstraintLayout>
复制代码

image-20210422160434685.png


placehoder提供了一个虚拟(空的)控件,但是它可以成一个布局已经存在的控件


使用setContent()方法在placehoder上设置一个其他控件的id,placehoder会成为设置控件id的内容,如果显示内容的控件存在屏幕内它会从隐藏


通过PlaceHolder的参数来显示内容.


设置约束布局


可以通过代码的方式设置约束布局的属性


//创建一个Constraint数据集合
ConstraintSet c = new ConstraintSet();
//Copy布局的配置
c.clone(mContext, R.layout.mine_info_view);
//新增或者替换行为参数
c.setVerticalChainStyle(userNameView.getId(),chain);
//执行
c.applyTo(this.<ConstraintLayout>findViewById(R.id.constraintlayout));

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

mpvue不维护了,已经成型的mpvue项目怎么办

mpvue作为美团技术团队的一个KPI产品,莫名其妙突然就不维护了,随着node版本和项目依赖不断更新,你会发现你原先的项目可能突然跑不起来了,此时你需要mpvue的替代品,没错,我要说的就是uni-app。只需几步,你可以很轻松的把mpvue项目迁移到uni...
继续阅读 »

mpvue作为美团技术团队的一个KPI产品,莫名其妙突然就不维护了,随着node版本和项目依赖不断更新,你会发现你原先的项目可能突然跑不起来了,此时你需要mpvue的替代品,没错,我要说的就是uni-app。

只需几步,你可以很轻松的把mpvue项目迁移到uni-app。

先去官网按步骤建好项目

https://uniapp.dcloud.io/quickstart

1、 把mpvue项目里src目录的文件复制到uni-app项目里


2、把main.js搬到uniapp的page.json里

mpvue的main.js


搬过来之后是这样的


3、运行,看看css是否跟原版有偏差,重新调整。此外要把api改成uni-app的,例如发请求api的要换成这个

https://uniapp.dcloud.io/api/request/request



收起阅读 »

Jetpack Compose TriStateCheckbox,Checkbox,Switch用法详解

这篇文章我们会通过分析TriStateCheckbox,Checkbox,Switch 他们的代码,并且讲解他们每个属性的含义以及用法。 一:TriStateCheckbox 我们来看下TriStateCheckbox的代码 @Composable fun T...
继续阅读 »

这篇文章我们会通过分析TriStateCheckbox,Checkbox,Switch 他们的代码,并且讲解他们每个属性的含义以及用法。


一:TriStateCheckbox


我们来看下TriStateCheckbox的代码


@Composable
fun TriStateCheckbox(
state: ToggleableState,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: CheckboxColors = CheckboxDefaults.colors()
){
...
}
复制代码


  • state 按钮的状态,有三种ToggleableState.On 中间打钩,ToggleableState.Off不选中,ToggleableState.Indeterminate 中间是横杠的。

  • onClick 点击回调

  • modifier 修饰符,我们在以前文章讲过Modifier用法详解

  • enabled 是否可用

  • interactionSource 处理状态的属性,比如按下的时候什么效果,正常时候什么效果,获取焦点时候什么效果等。类似之前再布局文件里写Selector。interactionSource.collectIsPressedAsState() 判断是否按下状态interactionSource.collectIsFocusedAsState() 判断是否获取焦点的状态interactionSource.collectIsDraggedAsState() 判断是否拖动

  • colors 设置颜色值CheckboxDefaults.colors(checkedColor,uncheckedColor,disabledColor,checkmarkColor,disabledIndeterminateColor) checkedColor表示选中时候的背景填充的颜色,uncheckedColor没有选中时候的背景颜色,disabledColor不可用时候的背景色,checkmarkColor这个指的是框里面的打钩,横杠图标的颜色。disabledIndeterminateColor当不可用时,且状态ToggleableState.Indeterminate的时候的颜色。


比如我们举例:当是打钩选中的时候,再点击我们变成横杠,当是横杠的时候点击变成不选中,当是不选中的时候,点击变成选中。并且当属于按下状态的时候,我们的背景色改成红色,否则选中背景色是绿色。代码如下


@Preview()
@Composable
fun triStateCheckboxTest(){
val context = LocalContext.current
val interactionSource = remember {
MutableInteractionSource()
}
val pressState = interactionSource.collectIsPressedAsState()
val borderColor = if (pressState.value) Color.Green else Color.Black
var isCheck = remember {
mutableStateOf(false)
}

var toggleState = remember {
mutableStateOf(ToggleableState(false))
}

Column(modifier = Modifier.padding(10.dp,10.dp)) {
TriStateCheckbox(
state = toggleState.value,
onClick = {
toggleState.value = when(toggleState.value){
ToggleableState.On->{
ToggleableState.Indeterminate
}
ToggleableState.Off->ToggleableState.On
else-> ToggleableState.Off
}
},
modifier = Modifier.size(50.dp),
enabled = true,
interactionSource = interactionSource,
colors = CheckboxDefaults.colors(
checkedColor= if(pressState.value) Color.Red else Color.Green,
uncheckedColor = Color.Gray,
disabledColor = Color.Gray,
checkmarkColor = Color.White,
disabledIndeterminateColor = Color.Yellow
)
)
}
}
复制代码

二:Checkbox


Checkbox其实内部就是new了个TriStateCheckbox。只是CheckBox没有ToggleableState.Indeterminate的情况,只有选中和不选中。代码如下:


@Composable
fun Checkbox(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: CheckboxColors = CheckboxDefaults.colors()
) {
TriStateCheckbox(
state = ToggleableState(checked),
onClick = if (onCheckedChange != null) { { onCheckedChange(!checked) } } else null,
interactionSource = interactionSource,
enabled = enabled,
colors = colors,
modifier = modifier
)
}
复制代码


  • checked 是否选中

  • onCheckedChange 选中改变的监听

  • modifier 修饰符

  • enabled 是否可用

  • interactionSource 跟上面一样

  • colors 跟上面一样


举例:


@Preview()
@Composable
fun checkBoxTest(){
val context = LocalContext.current
val interactionSource = remember {
MutableInteractionSource()
}
val pressState = interactionSource.collectIsPressedAsState()
val borderColor = if (pressState.value) Color.Green else Color.Black
var isCheck = remember {
mutableStateOf(false)
}

Column(modifier = Modifier.padding(10.dp,10.dp)) {
Checkbox(
checked = isCheck.value,
onCheckedChange = {
isCheck.value = it
},
modifier = Modifier.size(50.dp),
enabled = true,
interactionSource = interactionSource,
colors = CheckboxDefaults.colors(
checkedColor= Color.Red,
uncheckedColor = Color.Gray,
disabledColor = Color.Gray,
checkmarkColor = Color.White,
disabledIndeterminateColor = Color.Yellow
)
)
}
}
复制代码

三 Switch


Switch 开关控件,代码如下:


@Composable
@OptIn(ExperimentalMaterialApi::class)
fun Switch(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: SwitchColors = SwitchDefaults.colors()
) {
...
}
复制代码


  • checked 是否选中

  • onCheckedChange 选中改变的监听

  • modifier 修饰符

  • enabled 是否可用

  • interactionSource 跟上面一样

  • colors 设置各种颜色 通过SwitchDefaults.colors设置,SwitchDefaults.colors的代码如下
     @Composable
    fun colors(
    checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
    checkedTrackColor: Color = checkedThumbColor,
    checkedTrackAlpha: Float = 0.54f,
    uncheckedThumbColor: Color = MaterialTheme.colors.surface,
    uncheckedTrackColor: Color = MaterialTheme.colors.onSurface,
    uncheckedTrackAlpha: Float = 0.38f,
    disabledCheckedThumbColor: Color = checkedThumbColor
    .copy(alpha = ContentAlpha.disabled)
    .compositeOver(MaterialTheme.colors.surface),
    disabledCheckedTrackColor: Color = checkedTrackColor
    .copy(alpha = ContentAlpha.disabled)
    .compositeOver(MaterialTheme.colors.surface),
    disabledUncheckedThumbColor: Color = uncheckedThumbColor
    .copy(alpha = ContentAlpha.disabled)
    .compositeOver(MaterialTheme.colors.surface),
    disabledUncheckedTrackColor: Color = uncheckedTrackColor
    .copy(alpha = ContentAlpha.disabled)
    .compositeOver(MaterialTheme.colors.surface)
    ): SwitchColors = DefaultSwitchColors(
    checkedThumbColor = checkedThumbColor,
    checkedTrackColor = checkedTrackColor.copy(alpha = checkedTrackAlpha),
    uncheckedThumbColor = uncheckedThumbColor,
    uncheckedTrackColor = uncheckedTrackColor.copy(alpha = uncheckedTrackAlpha),
    disabledCheckedThumbColor = disabledCheckedThumbColor,
    disabledCheckedTrackColor = disabledCheckedTrackColor.copy(alpha = checkedTrackAlpha),
    disabledUncheckedThumbColor = disabledUncheckedThumbColor,
    disabledUncheckedTrackColor = disabledUncheckedTrackColor.copy(alpha = uncheckedTrackAlpha)
    )
    复制代码


    • checkedThumbColor 当没有设置checkedTrackColor的时候。表示选中的背景颜色(圆形部分,包括横的部分),当有设置checkedTrackColor则该属性只作用再圆形的部分

    • checkedTrackColor 这个是表示开关开起来的时候横线部分的背景

    • checkedTrackAlpha 这个是表示开关开起来的时候横线部分的背景的透明度

    • uncheckedThumbColor 当没有设置uncheckedTrackColor的时候。表示没有选中的背景颜色(圆形部分,包括横的部分),当有设置uncheckedTrackColor则该属性只作用再圆形的部分

    • uncheckedTrackColor 这个是表示开关关闭的时候横线部分的背景

    • uncheckedTrackAlpha 这个是表示开关关闭的时候横线部分的背景的透明度

    • disabledCheckedThumbColor 表示不可用的时候的并且选中时候背景颜色 有设置disabledCheckedTrackColor则只作用于圆形部分,没有设置disabledCheckedTrackColor则作用于圆形跟横线部分

    • disabledCheckedTrackColor 表示不可用的时候并且选中时候的横线部分的背景颜色

    • disabledUncheckedThumbColor 表示不可用的时候的并且开关关闭的时候背景颜色 有设置disabledUncheckedTrackColor则只作用于圆形部分,没有设置disabledUncheckedTrackColor则作用于圆形跟横线部分

    • disabledUncheckedTrackColor 表示不可用的时候的并且开关关闭的时候的横线部分的背景颜色




@Preview()
@Composable
fun switchTest(){
val context = LocalContext.current
val interactionSource = remember {
MutableInteractionSource()
}
val pressState = interactionSource.collectIsPressedAsState()
val checkedThumbColor = if (pressState.value) Color.Green else Color.Red
var isCheck = remember {
mutableStateOf(false)
}

Column(modifier = Modifier.padding(10.dp,10.dp)) {
Switch(
checked = isCheck.value,
onCheckedChange = {
isCheck.value = it
},
// modifier = Modifier.size(50.dp),
enabled = true,
interactionSource = interactionSource,
colors = SwitchDefaults.colors(checkedThumbColor= checkedThumbColor,checkedTrackColor=Color.Yellow,checkedTrackAlpha = 0.1f)
)
}
}

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

iOS - UIStackView 布局 详解

一、UIStackView简介概念:一个堆叠视图的容器,iOS9的新特性。用途:StackView及其子视图会自适应界面,减少我们设置约束的工作量。特点:类似ContainView,不会渲染到界面上。StackView中的子视图只能朝一个方向进行排布,要么水平...
继续阅读 »

一、UIStackView简介

概念:

一个堆叠视图的容器,iOS9的新特性。
用途:StackView及其子视图会自适应界面,减少我们设置约束的工作量。

特点:

  • 类似ContainView,不会渲染到界面上。
  • StackView中的子视图只能朝一个方向进行排布,要么水平要么垂直。
  • StackView支持多层嵌套
  • 约束比StackView的自适应优先级高,可以通过设置约束来调整StackView的布局
  • 支持属性动画
  • 不能滚动

属性:

axis轴: -> 用来设置子视图的排列方式(H/V)
aligement: -> 用来设置子视图的对齐方式
distribution -> 用来设置子视图的分布方式(fill-填充)
spacing -> 子视图之间的间距

二、属性详解

1. axis

主要设置UIStackView布局的方向:水平方向或垂直方向。

typedefNS_ENUM(NSInteger,UILayoutConstraintAxis) {
UILayoutConstraintAxisHorizontal =0, //水平
UILayoutConstraintAxisVertical =1 //垂直
};
2. alignment

主要设置非轴方向子视图的对齐方式。

typedef NS_ENUM(NSInteger, UIStackViewAlignment) {
UIStackViewAlignmentFill, // 子视图填充
UIStackViewAlignmentLeading, // 子视图左对齐(axis为垂直方向而言)
UIStackViewAlignmentTop = UIStackViewAlignmentLeading, // 子视图顶部对齐(axis为水平方向而言)
UIStackViewAlignmentFirstBaseline, // 按照第一个子视图的文字的第一行对齐,同时保证高度最大的子视图底部对齐(只在axis为水平方向有效)
UIStackViewAlignmentCenter, // 子视图居中对齐
UIStackViewAlignmentTrailing, // 子视图右对齐(axis为垂直方向而言)
UIStackViewAlignmentBottom = UIStackViewAlignmentTrailing, // 子视图底部对齐(axis为水平方向而言)
UIStackViewAlignmentLastBaseline, // 按照最后一个子视图的文字的最后一行对齐,同时保证高度最大的子视图顶部对齐(只在axis为水平方向有效)
} API_AVAILABLE(ios(9.0));

具体显示效果如下:







3. distribution

设置轴方向上子视图的分布比例(如果axis是水平方向,也即设置子视图的宽度,如果axis是垂直方向,则是设置子视图的高度)。

typedef NS_ENUM(NSInteger, UIStackViewDistribution) {
UIStackViewDistributionFill = 0,
UIStackViewDistributionFillEqually,
UIStackViewDistributionFillProportionally,
UIStackViewDistributionEqualSpacing,
UIStackViewDistributionEqualCentering,
} API_AVAILABLE(ios(9.0));

下面以
axis = UILayoutConstraintAxisHorizontal,
alignment = UIStackViewAlignmentCenter
为例:

往UIStackView中添加三个UIView:

  1. 第一个UIView设为40*100
  2. 第二个UIView设为80*80
  3. 第三个UIView设为120*60

通过实例来说明每个属性的区别:

(1)UIStackViewDistributionFill = 0,默认属性,轴方向上填充UIStackView。如果axis为水平方向,则所有子视图的宽度等于UIStackView的宽,所以如果只有一个子视图,则子视图的宽度就等于UIStackView的宽,如果有两个子视图,且优先级一样,则会拉伸或压缩某个子视图,使两个子视图的宽度之和等于UIStackView的宽……,如果axis是垂直方向,则所有子视图的高度等于UIStackView的高,必要时会拉伸或压缩某个子视图。

上面是在子视图优先级一致的情况下,如果子视图优先级不一致,则会按优先级从高到低设置子视图的位置,对优先级最低的子视图进行必要的拉伸或压缩。

设置distribution = UIStackViewDistributionFill后显示效果:


UIStackViewDistributionFill

如图所示,由于三个子视图的宽度之和不够UIStackView的宽度,优先级又一致,所以第三个子视图被拉伸了。当然,我们可以修改某个子视图的优先级来让其被拉伸。

(2)UIStackViewDistributionFillEqually,该属性设置后使所有子视图在轴方向上等宽或等高。即如果是水平方向,所有子视图都会被必要的拉伸或压缩,使得每个子视图的宽度一致,原来设置的子视图的宽度都会被忽略;如果是垂直方向,所有子视图的高度也会保持一致,如下所示:


UIStackViewDistributionFillEqually

(3)UIStackViewDistributionFillProportionally 该属性设置后会根据原先子视图的比例来拉伸或压缩子视图的宽或高,如实例中三个子视图原先设置的宽度是1:2:3,所以水平方向上显示时,会按照这个比例进行拉伸,如下图所示,拉伸后的宽度依然是1:2:3。


UIStackViewDistributionFillProportionally

(4)UIStackViewDistributionEqualSpacing 该属性会保持子视图的宽高,所有子视图中间的间隔保持一致。如下图所示,图中子视图的间隔(绿线所示的长度)都是一致的。


UIStackViewDistributionEqualSpacing

(5)UIStackViewDistributionEqualCentering 该属性是控制所有子视图的中心之间的距离保持一致,如下图所示,子视图中心点之间的间隔(绿线所示的长度)是一致的。


UIStackViewDistributionEqualCentering

4. spacing

该属性控制子视图之间的间隔大小,在distribution前三个属性值设置的情况下,子视图之间是没有间隔,我们可以通过spacing属性显式的设置,如下图在distribution=UIStackViewDistributionFillEqually情况下,设置子视图间隔为10,子视图之间间隔都为10,且子视图依然等宽。


三、subView和arrangedSubView

对于Stack View的子控件添加和移除,我们是这样描述的。

添加 --> (Stack View管理的subview)
addArrangedSubview:
insertArrangedSubview:atIndex: arrangedSubviews
数组是subviews属性的子集。
移除 --> (Stack View管理的subview)
removeArrangedSubview:–>移除是指移除Stack View内部子控件的约束,并没有真正的把控件从父视图上移除。
removeFromSuperview–>从视图层次结构中删除,从父视图上删除

四、知识点小结

1、Axis表示Stack View的subview是水平排布还是垂直排布。
2、Alignment控制subview对齐方式。
3、Distribution定义subview的分布方式。
4、Spacing 为subview间的最小间距。

五、使用技巧

**可以hidden指定子view,根据动态拉伸规则,灵活使用组件。

例如:


原文链接:https://blog.csdn.net/songzhuo1991/article/details/115626992

收起阅读 »

JS实现精确倒计时

实现倒计时对前端工程师来说,是很常见的需求。那么,要怎么实现精确的倒计时呢?首先,考虑到客户端时间和服务端时间有误差,所以计算倒计时的时候,应该读取服务端的时间。但是,只考虑到这一点还远远不够的。页面运行时间长了,新打开页面的倒计时和原打开页面的倒计时还是存在...
继续阅读 »

实现倒计时对前端工程师来说,是很常见的需求。那么,要怎么实现精确的倒计时呢?

首先,考虑到客户端时间和服务端时间有误差,所以计算倒计时的时候,应该读取服务端的时间。但是,只考虑到这一点还远远不够的。页面运行时间长了,新打开页面的倒计时和原打开页面的倒计时还是存在误差。要减少这里的误差,就要说到Javascript解释器的工作原理。

Javascript解释器工作原理
Javascript解释器是单线程工作的,它执行任务按照任务进入队列的先后顺序执行。这会造成什么影响呢?
打个比方,设置定时器的时候,按照理想状况,下面的程序应当稳定的输出0。

let start = new Date().getTime()
let count = 0
setInterval(function(){
count++
console.log(new Date().getTime() - (start + count * 1000))
},1000)


由于代码执行占用时间,以及其他事件的阻塞,导致有些事件的执行延迟了几ms,阻塞事件不多时,影响微乎其微。但当我们添加更多的阻塞事件时,这个影响就会被放大,如下面的代码

let start = new Date().getTime()
let count = 0
setInterval(function(){
let j = 0
while(j++ < 100000000){}
},1)
setInterval(function(){
count++
console.log(new Date().getTime() - (start + count * 1000))
},1000)


线程阻塞解决方案

那么我们要怎么解决线程阻塞的问题呢?
按照正常的思路,如果没有被阻塞,下面设置好的定时器应该每隔1s执行一次。

setInterval(function(){},1000)

但是,如果出现阻塞事件,定时器可能就要隔1000+ms才执行一次。要精确的实现每隔1s执行一次,必须要先获取阻塞的时间。这里要用到定时器函数setTimeout控制定时器的执行时间,代码实现如下

setInterval(function(){
let j = 0
while(j++ < 100000000){}
},1)
let interval = 1000,
ms = 50000, //从服务器和活动开始时间计算出的时间差,这里测试用50000ms
count = 0,
startTime = new Date().getTime();
if( ms >= 0){
var timeCounter = setTimeout(countDownStart,interval);
}

第一部分的setInterval是一段阻塞代码。然后,我们分别定义了interval作为定时器的执行时间,距活动结束的时间用ms表示(ms=活动结束时间-服务器时间),
count表示计数器,然后启动定时器timeCounter。其中,countDownStart函数的实现逻辑如下

function countDownStart(){
count++;
let offset = new Date().getTime() - (startTime + count * interval);
let nextTime = interval - offset;
if (nextTime < 0) { nextTime = 0 };
ms -= interval;
console.log("误差:" + offset + "ms" );
if(ms < 0){
clearTimeout(timeCounter);
}else{
timeCounter = setTimeout(countDownStart,nextTime);
}
}

countDownStart的实现原理是,首先定义一个变量offset用来记录阻塞导致延误的时间是多少。nextTime代表offset和interval的差距,根据nextTime修改定时器timeCounter的执行时间,使它nextTime(ms)执行一次。

打个比方,如果上一次执行过程中因为阻塞延误了100ms,那么下一次就提前100ms启动定时器,即

timeCounter = setTimeout(countDownStart,900)



原文链接:https://blog.csdn.net/weixin_41672178/article/details/88372553

收起阅读 »

iOS -SEL、Method 和 IMP区别及使用

Runtime中,SEL、Method 和 IMP有什么区别,使用场景?SEL:定义: typedef struct objc_selector *SEL,代表方法的名称。仅以名字来识别。翻译成中文叫做选择子或者选择器,选择子代表方法在 Runtime期间的标...
继续阅读 »

Runtime中,SEL、Method 和 IMP有什么区别,使用场景?

SEL:定义: typedef struct objc_selector *SEL,代表方法的名称。
仅以名字来识别。翻译成中文叫做选择子或者选择器,选择子代表方法在 Runtime期间的标识符。为 SEL类型,虽然 SEL是 objc_selector 结构体指针,但实际上它只是一个 C 字符串。

      在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C的 Runtime 运行系统。不论两个类是否存在依存关系,只要他们拥有相同的方法名,那么他们的SEL都是相同的。
     比如,有n个viewcontroller页面,每个页面都有一个viewdidload,每个页面的载入,肯定都是不尽相同的。但是我们可以通过打印,观察发现,这些viewdidload的SEL都是同一个
SEL sel = @selector(methodName); // 方法名字 NSLog(@"address = %p",sel);// log输出为 address = 0x1df807e29因此类方法定义时,尽量不要用相同的名字,就算是变量类型不同也不行。否则会引起重复,例如:
-(void)setWidth:(int)width; -(void)setWidth:(double)width;
IMP:定义:typedef id (*IMP)(id, SEL, ...),代表函数指针,即函数执行的入口。该函数使用标准的 C调用。第一个参数指向 self(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者;第二个参数代表方法的选择子;... 代表可选参数,前面的 id 代表返回值。

Method:定义:typedef struct objc_method *Method,Method对开发者来说是一种不透明的类型,被隐藏在我们平时书写的类或对象的方法背后。它是一个objc_method结构体指针,我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。 objc_method的定义为:

/// Method
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};

方法名 method_name 类型为 SEL,相同名字的方法即使在不同类中定义,它们的方法选择器也相同。

方法类型 method_types 是个 char 指针,其实存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。

method_imp 指向方法的实现,本质上是一个函数的指针

SEL selector的简写,俗称方法选择器,实质存储的是方法的名称
IMP implement的简写,俗称方法实现,看源码得知它就是一个函数指针
Method 对上述两者的一个包装结构.



转自:https://www.jianshu.com/p/3db9622209e2
收起阅读 »

iOS-通过Runtime防止重复点击-UIButton、UITableView

Gesture有系统处理单机双击,暂不去自定义时间间隔了。只处理UIButton、UITableView(UICollectionView)1、思路:UIButton hook sendActionUITableView hook setDelegate(sw...
继续阅读 »

Gesture有系统处理单机双击,暂不去自定义时间间隔了。只处理UIButton、UITableView(UICollectionView)

1、思路:

UIButton hook sendAction
UITableView hook setDelegate(swift中没有此方法,改用OC)
Gesture hook initWithTarget:action:

注意:
1、hook button的sendAction方法其实hook的是UIControl的的sendAction,点击UIControl子类(导航栏返回按钮)会崩溃。因此,把UIButton hook换为UIControl hook即可,同时为了避免对其他UIControl子类(UISlider/UISwitch)造成影响和最小影响原则,默认关闭防止重复点击功能

[_UIButtonBarButton xx_sendActionWithAction:to:forEvent:]: unrecognized selector sent to instance 0x7f8e2a41b380'

2、OC中的方法是NSDate.date.timeIntervalSince1970,不是[[NSDate date] timeIntervalSince1970]

3、由于setDelegate方法可能被多次调用,所以要判断是否已经swizzling了,防止重复执行。基类A中hook了tableview之后,子类B、C分别setDelegate的话会调用两次method_exchange...(didSelectRowAtIndexPath)。具体表现为:基类为UITableViewController时无影响,基类为自定义的有UITableView的VC时,子类A正常,子类B异常。解决办法1:didSelectRow基类里面不写,子类里面自己去实现,或基类里面写了但子类各自去重写。解决方法2:更安全的运行时方法交换库 Aspects。

2、代码

+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalAppearSelector = @selector(setDelegate:);
SEL swizzingAppearSelector = @selector(my_setDelegate:);
method_exchangeImplementations(class_getInstanceMethod([self class], originalAppearSelector), class_getInstanceMethod([self class], swizzingAppearSelector));
});
}
-(void)my_setDelegate:(id<UITableViewDelegate>)delegate{
[self my_setDelegate:delegate];

SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
SEL sel_t = @selector(my_tableView:didSelectRowAtIndexPath:);

//如果没实现tableView:didSelectRowAtIndexPath:就不需要hook
if (![delegate respondsToSelector:sel]){
return;
}
BOOL addsuccess = class_addMethod([delegate class],
sel_t,
method_getImplementation(class_getInstanceMethod([self class], sel_t)),
nil);

//如果添加成功了就直接交换实现, 如果没有添加成功,说明之前已经添加过并交换过实现了
if (addsuccess) {
Method selMethod = class_getInstanceMethod([delegate class], sel);
Method sel_Method = class_getInstanceMethod([delegate class], sel_t);
method_exchangeImplementations(selMethod, sel_Method);
}
}

// 由于我们交换了方法, 所以在tableview的 didselected 被调用的时候, 实质调用的是以下方法:
-(void)my_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

if(NSDate.date.timeIntervalSince1970 - tableView.acceptEventTime < tableView.accpetEventInterval) {
NSLog(@"点击太快了");
return;
}
if (tableView.accpetEventInterval > 0) {
tableView.acceptEventTime = NSDate.date.timeIntervalSince1970;
}
[self my_tableView:tableView didSelectRowAtIndexPath:indexPath];
}

3、注意点、风险点

1、避免交换父类方法
如果当前类未实现被交换的方法而父类实现了的情况下,此时父类的实现会被交换,若此父类的多个继承者都在交换时会导致方法被交换多次而混乱,同时当调用父类的方法时会因为找不到而发生崩溃。所以在交换前都应该先尝试为当前类添加被交换的函数的新的实现IMP,如果添加成功则说明类没有实现被交换的方法,则只需要替代分类交换方法的实现为原方法的实现,如果添加失败,则原类中实现了被交换的方法,则可以直接进行交换。
2、load方法的加载顺序和相互影响
一个类B可能有继承来的super类A,还有可能有自己的分类C,如果分类中也实现了load方法,它们的调用顺序是怎么样的呢?系统首先会调用super的load方法,然后再调用类B自身的load方法,再次才会调用类B的分类C的load方法,也即是说真个继承链包括分类扩展中的load方法都会被执行到,只是执行顺序需要关注一下。load方法不同于其他覆盖方法在分类中的体现,如果类B本身中的其他方法在分类C中被重写,则会优先执行分类C中的。但是load不同,都会被执行到,因为这是类加载设置的方法。
3、出问题难排查
文本长按-编辑-复制或剪切的点击事件,需要过滤


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

iOS-Crash文件的解析

开发程序的过程中不管我们已经如何小心,总是会在不经意间遇到程序闪退。脑补一下当你在一群人面前自信的拿着你的App做功能预演的时候,流畅的操作被无情地Crash打断。联想起老罗在发布Smartisan OS的时候说了,他准备了10个手机,如果一台有问题,就换一台...
继续阅读 »

开发程序的过程中不管我们已经如何小心,总是会在不经意间遇到程序闪退。脑补一下当你在一群人面前自信的拿着你的App做功能预演的时候,流畅的操作被无情地Crash打断。联想起老罗在发布Smartisan OS的时候说了,他准备了10个手机,如果一台有问题,就换一台,如果10台后挂了他就不做手机了。好了不闲扯了,今天就跟大家一起聊聊iOSCrash文件的组成以及常用的分析工具。

一、Crash文件结构

当程序运行Crash的时候,系统会把运行的最后时刻的运行信息记录下来,存储到一个文件中,也就是我们所说的Crash文件。iOS的Crash日志通常由以下6各部分组成。

1、Process Information(进程信息)


2、Basic Information


3、Exception(非常重要


4、Thread Backtrace


发生Crash的线程的Crash调用栈,从上到下分别代表调用顺序,最上面的一个表示抛出异常的位置,依次往下可以看到API的调用顺序。上图的信息表明本次Crash出现xxxViewController的323行,出错的函数调用为orderCountLoadFailed。

5、Thread State


Crash时发生时刻,线程的状态,通常我们根据Crash栈即可获取到相关信息,这部分一般不用关心。

6、Binary Images


Crash时刻App加载的所有的库,其中第一行是Crash发生时我们App可执行文件的信息,可以看出为armv7,可执行文件的包得uuid位c0f……cd65,解析Crash的时候dsym文件的uuid必须和这个一样才能完成Crash的符号化解析。

二、常见的Crash类型

1、Watchdog timeout

Exception Code:0x8badf00d, 不太直观,可以读成“eat bad food”,意思是don‘t block main thread

紧接着下面会有一段描述:

Application Specific Information:

com.xxx.yyy   failed to resume in time

对于此类Crash,我们应该去审视自己App初始化时做的事情是否正确,是否在主线程请求了网络,或者其他耗时的事情卡住了正常初始化流程。

通常系统允许一个App从启动到可以相应用户事件的时间最多为5S,如果超过了5S,App就会被系统终止掉。在Launch,resume,suspend,quit时都会有相应的时间要求。在Highlight Thread里面我们可以看到被终止时调用到的位置,xxxAppDelegate加上行号。 

PS. 在连接Xcode调试时为了便于调试,系统会暂时禁用掉Watchdog,所以此类问题的发现需要使用正常的启动模式。

2、User force-quit

Exception Codes: 0xdeadfa11, deadfall

这个强制退出跟我们平时所说的kill掉后台任务操作还不太一样,通常在程序bug造成系统无法响应时可以采用长按电源键,当屏幕出现关机确认画面时按下Home键即可关闭当前程序。

3、Low Memory termination

跟一般的Crash结构不太一样,通常有Free pages,Wired Pages,Purgeable pages,largest process 组成,同事会列出当前时刻系统运行所有进程的信息。

关于Memory warning可以参看我之前写的一篇文章IOS 内存警告 Memory warning level

App在运行过程中,系统内存紧张时通常会先发警告,同时把后台挂起的程序终止掉,最终如果还是内存不够的话就会终止掉当前前台的进程。

当接受到内存警告的事后,我们应该释放尽可能多的内存,Crash其实也可以看做是对App的一种保护。

4、Crash due to bugs

因为程序bug导致的Crash通常千奇百怪,很难一概而论。大部分情况通过Crash日志就可以定位出问题,当然也不排除部分疑难杂症看半天都不值问题出在哪儿。这个就只能看功底了,一点点找,总是能发现蛛丝马迹。是在看不出来时还可以求助于Google大神,总有人遇到和你一样的Bug 

三、常见的Exception Type & Exception Code

1、Exception Type

1)EXC_BAD_ACCESS

此类型的Excpetion是我们最长碰到的Crash,通常用于访问了不改访问的内存导致。一般EXC_BAD_ACCESS后面的"()"还会带有补充信息。

SIGSEGV: 通常由于重复释放对象导致,这种类型在切换了ARC以后应该已经很少见到了。

SIGABRT:  收到Abort信号退出,通常Foundation库中的容器为了保护状态正常会做一些检测,例如插入nil到数组中等会遇到此类错误。

SEGV:(Segmentation  Violation),代表无效内存地址,比如空指针,未初始化指针,栈溢出等;

SIGBUS:总线错误,与 SIGSEGV 不同的是,SIGSEGV 访问的是无效地址,而 SIGBUS 访问的是有效地址,但总线访问异常(如地址对齐问题)

SIGILL:尝试执行非法的指令,可能不被识别或者没有权限

2)EXC_BAD_INSTRUCTION

此类异常通常由于线程执行非法指令导致

3)EXC_ARITHMETIC

除零错误会抛出此类异常

2、Exception Code


三、获取Crash的途径

1、本机

通过xCode连接测试机器,直接在Device中即可读取到该机器上发生的所有Crash log。

2、itunes connect

通过itunes connect后台获取到用户上报的Crash日志。

3、第三方的Crash收集系统

有很多优秀的第三方Crash收集系统大大的方便了我们收集Crash,甚至还带了符号化Crash日志的功能。比较常用的有CrashlyticsFlurry等。


 Crash日志记录的时候是将Crash发生时刻,函数的调用栈,以及线程等信息写入文件。一般都是直接写的16进制地址,如果不经过符号化的话,基本上很难获取到有用信息,下一篇我们将聊一聊Crash日志的符号化,通俗点讲就是让Crash日志变成我们可读的格式。


转自:https://www.cnblogs.com/smileEvday/p/Crash1.html

收起阅读 »

Kotlin - 内部类

内部类 内部类就是定义在类内部的类,Kotlin 中的内部类大致分为 2 种: 静态内部类 非静态内部类 静态内部类 在某个类中像普通类一样声明即可,可以认为静态内部类与外部类没有关系,只是定义在了外部类"体内"而已,在使用静态内部类时需要"带上"外部类:...
继续阅读 »

内部类


内部类就是定义在类内部的类,Kotlin 中的内部类大致分为 2 种:



  • 静态内部类

  • 非静态内部类


静态内部类


在某个类中像普通类一样声明即可,可以认为静态内部类与外部类没有关系,只是定义在了外部类"体内"而已,在使用静态内部类时需要"带上"外部类:


class Outer {
val a: Int = 0

class Inner {
val a: Int = 5
}
}

fun main() {
val outer = Outer()
println(outer.a)
val inner = Outer.Inner()
println(inner.a)
}
复制代码


注意,在 Java 中,这种写法就是在定义 非静态内部类,而 静态内部类 需要在声明时使用 static 修饰。



非静态内部类


非静态内部类与静态内部类有的区别有:



  • 非静态内部类 会持有外部类的引用,而 静态内部类 不会(可以认为两者没有关系)。

  • 非静态内部类 使用时需要基于外部类对象(Outer().Inner()),而 静态内部类 则是基于外部类(Outer.Inner())。


因为 非静态内部类 会持有外部类的引用,所以内部类可以直接使用外部类成员;当非静态内部类与外部类存在同名成员时,可以使用 @标记 来解决歧义:


class Outer {
val b: Int = 3
val a: Int = 0

inner class Inner {
val a: Int = 5
fun test() {
println("Outer b = $b") // 3,因为持有外部类的引用,所以直接使用外部类成员
println("Outer a = ${this@Outer.a}") // 0,使用 @Outer 指定this是外部类对象
println("Inner a = ${this@Inner.a}") // 5,使用 @Inner 指定this是内部类对象
println("Inner a = ${this.a}") // 5,不使用 @标记 默认this就是内部类对象
}
}
}

fun main() {
val inner = Outer().Inner()
inner.test()
}
复制代码

为了区分 Kotlin 与 Java 中静态内部类和非静态内部类声明上的不同,以下分别使用 Kotlin 和 Java 各自编写内部类代码:


// Kotlin - 非静态内部类
class Outer {
inner class Inner {...}
}

// Kotlin - 静态内部类
class Outer {
class Inner {...}
}
复制代码

// Java - 非静态内部类
public class Outer {
public class Inner {...}
}

// Java - 静态内部类
public class Outer {
public static class Inner {...}
}
复制代码

匿名内部类


匿名内部类就是没有定义名字的内部类,一般格式为 object : 接口或(和)类,实际开发中,方法的回调接口(即 callback)一般不会专门声明一个类再创建对象来使用,而是直接使用匿名内部类:


val textArea = TextArea()
textArea.addTextListener(object : TextListener {
override fun textValueChanged(e: TextEvent?) {...}
})
复制代码

Kotlin 的匿名内部类很强大,在使用时,可以有多个接口或父类,如:


val textArea = TextArea()
textArea.addTextListener(object : TextField(), TextListener {
override fun textValueChanged(e: TextEvent?) {...}
})
复制代码

这个匿名内部类既是 TextField 的子类,也是 TextListener 的实现类,不过可能实际应用场景会比较少,了解即可。



Java 中的匿名内部类就没这么强大了,只能是 单个 接口(或父类) 的 实现类(子类)。


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

iOS上架unity工程包含UIWebView问题

在经过一系列的开发之后,来到了游戏上架的步骤,但是在上架的过程中,收到了被拒邮件ITMS-90809: Deprecated API Usage - New apps that use UIWebView are no longer accepted. Ins...
继续阅读 »

在经过一系列的开发之后,来到了游戏上架的步骤,但是在上架的过程中,收到了被拒邮件

ITMS-90809: Deprecated API Usage - New apps that use UIWebView are no longer accepted. 
Instead, use WKWebView for improved security and reliability.
Learn more (https://developer.apple.com/documentation/uikit/uiwebview).

Though you are not required to fix the following issues,
we wanted to make you aware of them:

收到这个邮件之后,我首先全局搜索了我的工程中,是否包含UIWebView,但是并没有找到对应的文件。此时,就需要通过命令行来搜索自己的工程中,到底哪些地方包含UIWebView

1、首先确定包含UIWebView的地方,在命令行中cd到你的工程文件夹下,输入以下命令:

grep -r UIWebView .



2、确定包含的UIWebView之后,我们需要处理掉lib.a中的UIWebView

由于这个是包含在游戏的lib.a的静态库里边,所以,我们要对这个库进行处理,可以通过Git上的自动脚本来处理:

处理lib.a静态库中的UIWebView

3、或者也可以自己手动处理(建议使用脚本)

1.新建URLUtility.mm文件,放在桌面
复制以下代码

#include <iostream>
#import <UIKit/UIKit.h>

using namespace std;

namespace core {
template <class type>
class StringStorageDefault {};
template <class type,class type2>
class basic_string {
public:
char *c_str(void);
};
}

void OpenURLInGame(core::basic_string< char,core::StringStorageDefault<char> > const&arg){}

void OpenURL(core::basic_string<char,core::StringStorageDefault<char> >const&arg){
const void *arg2= &arg;
UIApplication *app = [UIApplication sharedApplication];
NSString *urlStr = [NSString stringWithUTF8String:(char *)arg2];
NSURL *url = [NSURL URLWithString:urlStr];
[app openURL:url];
}

void OpenURL(std::string const&arg){
UIApplication *app = [UIApplication sharedApplication];
NSString *urlStr = [NSString stringWithUTF8String:arg.c_str()];
NSURL *url = [NSURL URLWithString:urlStr];
[app openURL:url];

}

2.查看Unity项目下libiPhone-lib.a架构

lipo -info libiPhone-lib.a

结果如下:
Architectures in the fat file:
/Users/xxx/Desktop/libiPhone-lib.a are: arm64 armv7
则该静态库包含两种框架

生成URLUtility.mm armv7架构对应的URLUtility.o

clang -c URLUtility.mm -arch armv7 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk

新建armv7文件夹,将生成的URLUtility.o放入该文件夹, 后面会用到
生成URLUtility.mm arm64架构对应的URLUtility.o

clang -c URLUtility.mm -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk

新建arm64文件夹,将新生成的URLUtility.o放入该文件夹,后面会用到

如果有更多架构只需要把armv7替换成对应的架构分别生成

拆分libiPhone-lib.a
该命令是拆分libiPhone-lib.a中armv7架构部分
输出到/Users/xxx/Desktop/armv7/libiPhone-armv7.a
拆分成armv7架构

lipo libiPhone-lib.a -thin armv7 -output /Users/xxx/Desktop/armv7/libiPhone-armv7.a

拆分成arm64架构

lipo libiPhone-lib.a -thin arm64 -output /Users/xxx/Desktop/arm64/libiPhone-arm64.a

替换libiPhone-lib.a中的URLUtility.o
将各自架构libiPhone-lib.a里的URLUtility.o替换为我们生成的

ar -d 是移除
ar -q是添加
移除并且替换armv7中的URLUtility.o

ar -d /Users/xxx/Desktop/armv7/libiPhone-armv7.a URLUtility.o
ar -q /Users/xxx/Desktop/armv7/libiPhone-armv7.a /Users/xxx/Desktop/armv7/URLUtility.o

移除并且替换arm64

ar -d /Users/xxx/Desktop/arm64/libiPhone-arm64.a URLUtility.o
ar -q /Users/xxx/Desktop/arm64/libiPhone-arm64.a /Users/xxx/Desktop/arm64/URLUtility.o

合并libiPhone-lib.a

该命令的意思是将libiPhone-arm64.a libiPhone-armv7.a合并成桌面上的libiPhone-lib2.a

lipo -create /Users/xxx/Desktop/arm64/libiPhone-arm64.a /Users/xxx/Desktop/armv7/libiPhone-armv7.a -output /Users/xxx/Desktop/libiPhone-lib2.a

然后将libiPhone-lib2.a替换进Unity项目中即可使用

3、替换之后,重新运行,打包工程,提交上架即可

收起阅读 »

Kotlin - 区间与数组

区间(Range) 区间是一个数学上的概念,表示范围。 区间的声明 Kotlin 中可以使用 .. 或 until 来声明区间: val range: IntRange = 0..1024 // 闭区间[0,1024],包括1024 val rangeExcl...
继续阅读 »

区间(Range)


区间是一个数学上的概念,表示范围。


区间的声明


Kotlin 中可以使用 ..until 来声明区间:


val range: IntRange = 0..1024 // 闭区间[0,1024],包括1024
val rangeExclusive: IntRange = 0 until 1024 // 半开区间[0,1024),不包括1024
val emptyRange: IntRange = 0..-1 // 空区间[]
复制代码

其实这里的 .. 操作符对应的是 Int 类中的一个 rangeTo() 方法:


/** Creates a range from this value to the specified [other] value. */
public operator fun rangeTo(other: Byte): IntRange
/** Creates a range from this value to the specified [other] value. */
public operator fun rangeTo(other: Short): IntRange
/** Creates a range from this value to the specified [other] value. */
public operator fun rangeTo(other: Int): IntRange
/** Creates a range from this value to the specified [other] value. */
public operator fun rangeTo(other: Long): LongRange
复制代码

区间常用操作


判断某个元素是否在区间内:


println(range.contains(50)) // true
println(500 in range) // true
复制代码


这里的 in 关键字对应的就是 IntRange 类中的 contains() 方法,因此上面的两行代码实质上是一样的。



判断区间是否为空:


println(rangeExclusive.isEmpty()) // false
println(emptyRange.isEmpty()) // true
复制代码

对区间进行遍历:


// 输出:0, 1, 2, 3 ..... 1020, 1021, 1022, 1023,
for (i in rangeExclusive) {
print("$i, ")
}
复制代码


这里的 infor 配合使用,就可以实现区间的遍历效果。



区间的类型


所有的区间都是 ClosedRange 的子类,IntRange 最常用。通过源码不难发现,除了 IntRangeClosedRange 的子类还有 LongRangeCharRange 等等。



以 CharRange 为例,我们还可以写出 26 个大小写字母的区间:


// a b c d e f g h i j k l m n o p q r s t u v w x y z
val lowerRange: CharRange = 'a'..'z'
// A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
val upperRange: CharRange = 'A'..'Z'
复制代码

数组(Array)


数组(Array)跟数(Number)没有关系,它指的是一系列对象。


创建数组


创建数组一般有 2 种方式:



  1. 使用 Array 类创建数组

  2. 使用库函数 arrayOfXXX() 创建数组


使用 Array 类创建数组


先看看 Array 的构造函数:


public class Array<T> {
/**
* Creates a new array with the specified [size], where each element is calculated by calling the specified
* [init] function. The [init] function returns an array element given its index.
*/
public inline constructor(size: Int, init: (Int) -> T)

...
}
复制代码

使用 Array 创建数组,需要指定元素类型(一般情况下可以省略),有 2 个必传参数,分别是数组长度 size,和元素初始化函数 init。


val array = Array<String>(5) { index -> "No.$index" }
println(array.size) // 5
for (str in array) { // No.0 No.1 No.2 No.3 No.4
print("$str ")
}
复制代码


当函数参数是最后一个形参时,可以把它写到括号外,这是 Kotlin 中的 lambda 写法,当然,你也可以不用 lambda 写法,就写在括号内:val array = Array<String>(5, { index -> "No.$index" }),关于 lambda 的相关知识在这里暂不细说。



使用库函数 arrayOfXXX() 创建数组


直接使用 Array 创建数组会稍稍有点麻烦,要指定个数,又要传入初始化函数, 而实际开发中,我们希望有更方便的写法来提高工作效率,Kotlin 就为此就提供了一系列创建数组的库函数 arrayOfXXX()


val arrayOfString: Array<String> = arrayOf("我", "是", "LQR")
val arrayOfHuman: Array<Human> = arrayOf(Boy("温和", "英俊", "浑厚"), Girl("温柔", "甜美", "动人"))
val arrayOfInt: IntArray = intArrayOf(1, 3, 5, 7)
val arrayOfChar: CharArray = charArrayOf('H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd')
复制代码

要注意,用于存放 String 类型或自定义类型的对象数组 的创建使用的是 arrayOf(),而基本数据类型数组的创建则有专门的库函数,如:intArrayOf()charArrayOf() 等。intArrayOf()charArrayOf() 等库函数是 Kotlin 为了避免基本数据装箱折箱的开销而专门创造出来的,比如:intArrayOf(1, 3, 5, 7) 创建出来的数组是 IntArray 类型,对应到 Java 中是 int[] ,而 arrayOf(1, 2, 3, 4) 创建出来的数组是 Array<Int> 类型,对应 Java 中是 Integer[]


基本数据类型数组


为了避免不必要的装箱和拆箱,基本数据类型的数组是定制的:



































JavaKotlin
int[]IntArray
short[]ShortArray
long[]LongArray
float[]FloatArray
double[]DoubleArray
char[]CharArray


注意:IntArrayArray<Int> 是完全不同的类型,无法直接相互转换!

原话:Kotlin 也有无装箱开销的专门的类来表示原生类型数组: ByteArray、 ShortArray、IntArray 等等。这些类与 Array 并没有继承关系,但是它们有同样的方法属性集。



了解更多 Kotlin 中数组的相关知识,请访问:www.kotlincn.net/docs/refere…


数组常用操作


可以使用 .size 获取数组长度,使用 for-in 遍历数组:


println(arrayOfInt.size) // 4
for (int in arrayOfInt) { // 1 3 5 7
print("$int ")
}
复制代码

Array 定义了 get 与 set 函数(按照运算符重载约定这会转变为 []),因此我们可以通过 [] 来获取或修改数组中的元素:


println(arrayOfHuman[1]) // 我是性格温柔,长相甜美,声音动人的人
arrayOfHuman[1] = Boy("温和1", "英俊1", "浑厚1")
println(arrayOfHuman[1]) // 我是性格温和1,长相英俊1,声音浑厚1的人
复制代码


注意:自定义类型对象使用 println() 默认输出的是对象地址信息,如:com.charylin.kotlinlearn.Boy@7440e464 ,需要重写类的 toString() 方法来修改输出日志内容。



CharArray 提供了 joinToString() 方法,用于将字符数组拼接成字符串,默认以 , 作为拼接符:


println(arrayOfChar.joinToString()) // H, e, l, l, o, W, o, r, l, d
println(arrayOfChar.joinToString("")) // HelloWorld
复制代码

可以使用 slice() 方法对数组进行切片:


println(arrayOfInt.slice(1..2)) // [3, 5]

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

【疯狂Android之Kotlin】关于Kotlin的高阶函数

高阶函数介绍 概念 相信许多同学都已经知道,所谓的高阶函数就是就是方法的参数 或 返回值 是函数类型的 函数 2. 通过例子说明 List 集合的 forEach( )循环 , 该方法就是接收一个高阶函数类型变量作为参数 , 有点类似于C/C++中的函数指...
继续阅读 »

高阶函数介绍



  1. 概念


相信许多同学都已经知道,所谓的高阶函数就是就是方法的参数 或 返回值 是函数类型的 函数
2. 通过例子说明
List 集合的 forEach( )循环 , 该方法就是接收一个高阶函数类型变量作为参数 , 有点类似于C/C++中的函数指针(指向函数的指针)


函数作为函数参数



  1. 这里先介绍下sumBy()这个高阶函数,通过这个我们来看下如何将函数用作参数,源码如下:


// sumBy函数的源码
public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
var sum: Int = 0
for (element in this) {
sum += selector(element)
}
return sum
}
复制代码


  1. 说明



  • 大家这里可以不必纠结inline,和sumBy函数前面的CharSequence.。因为这是Koltin中的内联函数与扩展功能。

  • 该函数返回一个Int类型的值。并且接受了一个selector()函数作为该函数的参数。其中,selector()函数接受一个Char类型的参数,并且返回一个Int类型的值。

  • 定义一个sum变量,并且循环这个字符串,循环一次调用一次selector()函数并加上sum。用作累加。其中this关键字代表字符串本身。



  1. 该函数的作用:把字符串中的每一个字符转换为Int的值,用于累加,最后返回累加的值



  • 举个例子


val testStr = "abc"
val sum = testStr.sumBy { it.toInt() }
println(sum)
复制代码


  • 输出结果


294  // 因为字符a对应的值为97,b对应98,c对应99,故而该值即为 97 + 98 + 99 = 294
复制代码

函数作为函数返回值



  1. 同样这里也是使用一个lock()函数来进行讲解,先看看源码:


fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}
复制代码


  1. 说明



  • 这其中用到了kotlin中泛型的知识点,这里暂且不考虑。同学我会在后续的文章进行介绍。

  • 从源码可以看出,该函数接受一个Lock类型的变量作为参数1,并且接受一个无参且返回类型为T的函数作为参数2.

  • 该函数的返回值为一个函数,我们可以看这一句代码return body()可以看出。



  1. 使用



  • 下面的代码都是伪代码,我就是按照官网的例子直接拿过来用的


fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)
复制代码

其中,::toBeSynchronized即为对函数toBeSynchronized()的引用,其中关于双冒号::的使用在这里不做讨论与讲解。



  • 上面的写法也可以写作:


val result = lock(lock, {sharedResource.operation()} )
复制代码

函数作为函数类型变量


这里同学我使用匿名函数来简单讲解一下



  1. 函数变量需求


在上面的forEach()函数中, 需要传入一个 (String) -> Unit 函数类型的变量, 该函数类型的函数 参数是 String 类型 , 返回值是Unit空类型 ;



  1. 普通的函数声明 : 下面定义的函数 , 参数类型是 String , 返回值是 Unit 空类型 , 这个函数是 (String) -> Unit 类型的 , 但是 study 不能当做参数传入到 forEach 方法中;list.forEach(study),是错误的调用,编译不通过 ;


fun study(student : String) : Unit{
println(student + " 在学习")
}
复制代码


  1. 函数类型变量 : 可以使用匿名函数 , 赋值给一个变量 , 然后将这个变量当做参数传递给 forEach 当做参数 ;



  • 指定变量 : 为 (String) -> Unit 类型函数指定一个引用变量 var study2 ;

  • 匿名函数 : 顾名思义,就是没有函数名称 , 省略调上面普通函数的名称,赋值给变量 ; 具体用法如下 :


var study2 = fun (student : String) : Unit{
println(student + " 在学习")
}
复制代码

高阶函数的使用与示例




  • 在上面的这些例子中,我们出现了str.sumBy{ it.toInt },这样的写法这里主要讲高阶函数中对Lambda语法的简写。




  • 从上面的例子我们的写法应该是这样的:




str.sumBy( { it.toInt } )
复制代码


  1. 但是根据Kotlin中的约定,即当函数中只有一个函数作为参数,并且您使用了lambda表达式作为相应的参数,则可以省略函数的小括号()。


故而我们可以写成:


str.sumBy{ it.toInt }
复制代码


  1. 还有一个约定,即当函数的最后一个参数是一个函数,并且你传递一个lambda表达式作为相应的参数,则可以在圆括号之外指定它。故而上面例2中的代码,所以我们可写成:


val result = lock(lock){
sharedResource.operation()
}
复制代码

Kotlin常用标准高阶函数介绍


介绍几个Kotlin中常用的标准高阶函数。如果用好下面的几个函数,能减少很多的代码量,并增加代码的可读性。下面的几个高阶函数的源码几乎上都出自Standard.kt文件


TODO函数


其实严格来说,该函数不是一个高阶函数,只是一个抛出异常以及测试错误的一个普通函数



  1. 看下它的源码如下:


@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()

@kotlin.internal.InlineOnly
public inline fun TODO(reason: String): Nothing =
throw NotImplementedError("An operation is not implemented: $reason")
复制代码


  • 作用:显示抛出NotImplementedError错误

  • NotImplementedError错误类继承至Java中的Error



  1. 举个例子:


fun main(args: Array<String>) {
TODO("测试TODO函数,是否显示抛出错误")
}
复制代码


  • 如果调用TODO()时,不传参数的,则会输出An operation is not implemented.


with()函数


对于with函数,其实简单来说就是可以让用户省略点号之前的对象引用,针对with对象,在Standard.kt中语法如下


public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
复制代码

看出with是一个全局函数,并没有作为任何类的扩展方法,仔细查看block会发现它又是一个带接收者的字面函数,这是一种临时的扩展方法,只在调用过程中有效,调用结束之后就不再生效,所以block就成了receiver临时的扩展函数,临时扩展函数的内部调换用上下文就是receiver对象。


举个栗子


class MyLogger {
var tag: String = "TAG"

fun e(msg: String) {
println("$tag $i")
}

fun tag(tagStr: String) {
tag = tagStr
}
}

fun main(args: Array<String>) {
val logger = MyLogger()
with(logger) {
tag("Kotlin")
e("It is a good language")
}
}
复制代码

apply()函数


关于apply()函数用于lambda表达式里切换上下文,可以查看


public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
复制代码


  • 可以看到block这个函数,它不是一个普通函数,block其实就是带接收者的字面函数,这样传入的lambda表达式就临时扩展了T类,调用lambda表达式时的上下文就是调用方法的T类对象。


/**
* @author : Jacky
* @date: : 2021/2/24
* 数据库链接
**/
class DbConfig {
var url: String = ""
var username: String = ""
var password: String = ""

override fun toString(): String {
return "url = $url, username = $username, password = $password"
}
}

class DbConnection {
fun config(conf: DbConfig) {
println(conf)
}
}

fun main(args: Array<String>) {
val conn = DbConnection()

//上下表达式
conn.config(DbConfig().apply {
url = "mysql://127.0.0.1:3306/hello"
username = "root"
password = "123456"
})
}
复制代码

这里使用apply函数,不但初始化了所有属性的值还可以把对象返回来用来配置数据库连接对象


also()函数



  1. 关于T.also函数来说,它和T.apply很相似。我们先看看其源码的实现:


public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
复制代码


  • 上面的源码在结合T.apply函数的源码我们可以看出: T.also函数中的参数block函数传入了自身对象

  • 这个函数的作用是用用block函数调用自身对象,最后在返回自身对象



  1. 下面举个例子,用实例来说明一下和apply的区别


"kotlin".also {
println("结果:${it.plus("-java")}")
}.also {
println("结果:${it.plus("-php")}")
}

"kotlin".apply {
println("结果:${this.plus("-java")}")
}.apply {
println("结果:${this.plus("-php")}")
}
复制代码


  • 输出结果如下


结果:kotlin-java
结果:kotlin-php

结果:kotlin-java
结果:kotlin-php
复制代码


  • 可以看出,他们的区别在于:



  • T.also中只能使用it调用自身,而T.apply中只能使用this调用自身。

  • 因为在源码中T.also是执行block(this)后在返回自身。而T.apply是执行block()后在返回自身。

  • 这就是为什么在一些函数中可以使用it,而一些函数中只能使用this的关键所在


let()函数



  1. 首先查看Standard.kt源文件let函数的源代码


public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
复制代码

接收了调用者作为参数并且返回任意的类型的lambda表达式,最后以自己为参数调用lambda表达式


val arr = intArrayOf(1, 2, 4, 6)
arr.let {
var sum = 0
// 这个it就是arr对象
for (i in it) {
sum += i
}
println(sum)
}
复制代码

对于系统标准高阶函数的总结



  • 一般我们使用最多就是also,let,apply这三个函数,一般在实际项目开发过程中都不会进行连贯着使用

  • 关于它们之间的区别,或者在什么情况下需要使用那个高阶函数,同学可以参考以下两篇文章



自定义高阶函数



  1. 多说不易,我们可以看下一个例子:


// 源代码
fun test(a : Int , b : Int) : Int{
return a + b
}

fun sum(num1 : Int , num2 : Int) : Int{
return num1 + num2
}

// 调用
test(10,sum(3,5)) // 结果为:18

// lambda
fun test(a : Int , b : (num1 : Int , num2 : Int) -> Int) : Int{
return a + b.invoke(3,5)
}

// 调用
test(10,{ num1: Int, num2: Int -> num1 + num2 }) // 结果为:18
复制代码

以上我们可以看到直接写死了值,这在开发中是非常不合理的,上面的例子在阐述Lambda的语法,另举一个例子



  1. 传入两个参数,并传入一个函数实现不同的逻辑


private fun resultByOpt(num1 : Int , num2 : Int , result : (Int ,Int) -> Int) : Int{
return result(num1,num2)
}

private fun testDemo() {
val result1 = resultByOpt(1,2){
num1, num2 -> num1 + num2
}

val result2 = resultByOpt(3,4){
num1, num2 -> num1 - num2
}

val result3 = resultByOpt(5,6){
num1, num2 -> num1 * num2
}

val result4 = resultByOpt(6,3){
num1, num2 -> num1 / num2
}

println("result1 = $result1")
println("result2 = $result2")
println("result3 = $result3")
println("result4 = $result4")
}
复制代码


  • 输出结果为


result1 = 3
result2 = -1
result3 = 30
result4 = 2
复制代码


  • 根据传入不同的Lambda表达式,实现了两个数的(+、- 、 * 、/)。

  • 当然了,在实际的项目开发中,自己去定义高阶函数的实现是很少了,因为用系统给我们提供的高阶函数已经够用了。

  • 不过,当我们掌握了Lambda语法以及怎么去定义高阶函数的用法后。在实际开发中有了这种需求的时候也难不倒我们了。fighting


总结



  • 既然我们选择了Kotlin这门编程语言。那其高阶函数时必须要掌握的一个知识点,因为,在系统的源码中,实现了大量的高阶函数操作。

  • 除了上面讲解到的标准高阶函数外,对于字符串(String)以及集合等,都用高阶函数去编写了他们的一些常用操作。比如,元素的过滤、排序、获取元素、分组等等

  • 对于上面讲述到的标准高阶函数,同学我要多实践,因为它们真的能在实际的项目开发中减少大量的代码编写量。

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

Android 开发经验谈:多线程你了解多少?

 i= i+1;如上代码很简单,在单线程中i就等于i+1,执行不会出问题。但是在多线程中就会有问题。在说多线程之前我从别人的博客里摘了一段文字:大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入...
继续阅读 »
 

i= i+1;

如上代码很简单,在单线程中i就等于i+1,执行不会出问题。

但是在多线程中就会有问题。

在说多线程之前我从别人的博客里摘了一段文字:

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

  也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

也就是说有两个线程来执行这段代码,在两个线程中都有缓存,计算时都会把i的值写入到自己的缓存中,计算后再进行同步,这就导致计算结果与预期不符。

为了解决每个线程中都用自己的缓存,于是就采用了关键字volatile

volatile关键字就会强制变量都从主存中获取

但是呢...由于volatile加减并非线程安全,volatile并不适用于计算。

当然也有其专用的使用范围。

volatile(java5):可以保证多线程下的可见性;

读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程重新拷贝一份,这样就保证子线程的会跟主线程的一致。

写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。

所以在Android 单例模式中,我大多都采用volatile关键字修饰

当然如何让i=i+1呢?

咱们可以采用如下几个synchronized, AtomicInteger,lock

AtomicInteger:

一个提供原子操作的Integer的类。 一种线程安全的加减操作接口, 相比 synchroized、lock 高效.

例子:

private final AtomicInteger mThreadNumber = new AtomicInteger(1);
mThreadNumber.getAndIncrement()

这便可以保证线程安全。

synchronized:

synchronized是java内置关键字,对象锁,在jvm层面的锁,只能把一块代码锁着,并不能获取到锁的状态。

悲观锁机制,线程获取的是独占锁,当一个线程进入时后,其他线程被阻塞等待。

synchronized修饰方法时要注意以下几点:
1. synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这

2. 在定义接口方法时不能使用synchronized关键字。

3. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

误区:

非静态:无论你是对方法标注synchronized还是对类标注synchronized,都是对对象的锁。

例:

public class Test {

int i = 0;
public synchronized void methon1(){
i++;
System.out.println("methon1 "+i);
}

public synchronized void methon2(){
i++;
System.out.println("methon2 "+i);
}

}

public static void main(String[] args){
Test test = new Test();
new Thread(new Runnable() {
@Override
public void run() {
int i =0;
do {
test.methon1();
i++;
}while (i<100);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int i =0;
do {
test.methon2();
i++;
}while (i<100);
}
}).start();
}
}

输出:
methon2 1
methon1 2
methon2 3
methon1 4
methon2 5
methon1 6
methon2 7
methon1 8
methon2 9
methon1 10
methon2 11
methon1 12
methon2 13

所以你要是想对某个方法单独进行方法锁,就必须锁另一个的对象,此时你用静态方法可能比较好呢。

例:


Object object = new Object();

public void methon2(){
synchronized (object){
i++;
System.out.println("methon2 "+i);
}
}

lock:

需要指定起始位置与终止位置 

lock 与unlock,锁与释放锁

一般为了健壮性在finally中调用unlock

相比synchronized 性能低效,操作比较重量级

乐观锁机制:假设没有锁,如果有冲突失败,则重试。

Lock可以知道线程有没有成功获取到锁。

  1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

  2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。


sleep/wait区别

sleep()方法,我们首先要知道该方法是属于Thread类中的native方法。而wait()方法,则是属于Object类中的。

wait方法需要锁来控制

sleep 方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

wait/notify机制

方法名称描述
notify()随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程”
notifyAll()使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现
wait()使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
wait(long,int)对于超时时间更细力度的控制,可以达到纳秒
收起阅读 »

Android添加联系人到通讯录

本周项目中遇到了需要添加联系人或者添加到已有联系人的需求,联系人中需要保存的字段有很多,之前不太熟悉,在这里总结一下。 字段 联系人名字 名字不知道为什么,值设置了之后传过去没有,于是自己通过Intent最后又单独传了一次 // 联系人名字 ContentVa...
继续阅读 »

本周项目中遇到了需要添加联系人或者添加到已有联系人的需求,联系人中需要保存的字段有很多,之前不太熟悉,在这里总结一下。


字段


联系人名字

名字不知道为什么,值设置了之后传过去没有,于是自己通过Intent最后又单独传了一次


// 联系人名字
ContentValues row1 = new ContentValues();String name = lastName + middleName + firstName;row1.put(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);row1.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
name);row1.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
firstName);row1.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
lastName);row1.put(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME,
middleName);
复制代码

联系人昵称

// 联系人昵称
ContentValues row2 = new ContentValues();
row2.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
row2.put(ContactsContract.CommonDataKinds.Nickname.NAME, nickName);
复制代码

联系人头像

这里需要将图片的byte数组传进去


ContentValues row3 = new ContentValues();
//添加头像
row3.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
Bitmap bitmap = BitmapFactory.decodeFile(photoFilePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
row3.put(ContactsContract.CommonDataKinds.Photo.PHOTO, baos.toByteArray());
复制代码

联系人备注

// 联系人备注
ContentValues row4 = new ContentValues();
row4.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE);
row4.put(ContactsContract.CommonDataKinds.Note.NOTE, remark);
复制代码

联系人号码

号码有很多种类型,电话,手机,传真,公司,家庭,等


ContentValues row5 = new ContentValues();
// 联系人的电话号码
addPhoneNumber(row5, values, mobilePhoneNumber,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);ContentValues row6 = new ContentValues();
// 联系人的公司电话
addPhoneNumber(row6, values, hostNumber,
ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN);ContentValues row7 = new ContentValues();
// 联系人的工作号码
addPhoneNumber(row7, values, workPhoneNumber,
ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE);ContentValues row8 = new ContentValues();
// 联系人的工作传真
addPhoneNumber(row8, values, workFaxNumber,
ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK);ContentValues row9 = new ContentValues();
// 联系人的住宅号码
addPhoneNumber(row9, values, homePhoneNumber,
ContactsContract.CommonDataKinds.Phone.TYPE_HOME);ContentValues row10 = new ContentValues();
// 联系人的住宅传真
addPhoneNumber(row10, values, homeFaxNumber,
ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME);

//封装的添加方法
private void addPhoneNumber(
ContentValues row, ArrayList<ContentValues> values, String phoneNumber, int type) {
row.put(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
row.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
row.put(ContactsContract.CommonDataKinds.Phone.TYPE, type);
values.add(row);
}
复制代码

联系人公司和职位

// 联系人公司和职位
ContentValues row11 = new ContentValues();
row11.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
row11.put(ContactsContract.CommonDataKinds.Organization.COMPANY, organization);
row11.put(ContactsContract.CommonDataKinds.Organization.TITLE, title);
复制代码

网站

// 联系人网站
ContentValues row12 = new ContentValues();
row12.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
row12.put(ContactsContract.CommonDataKinds.Website.URL, url);
复制代码

联系人邮箱

// 插入Email数据
ContentValues row13 = new ContentValues();
row13.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
row13.put(ContactsContract.CommonDataKinds.Email.DATA, email);
row13.put(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_WORK);
复制代码

联系人地址

地址分为家庭,工作和其他。有个问题,这里分段传入之后添加完成无法显示,只能自己将国家省市街道拼接,传入到了地址字段,这样就可以显示出来,但是邮政编码是无法显示的


//其他地址
ContentValues row14 = new ContentValues();
addAddress(row14, values, addressCountry, addressState, addressCity, addressStreet, addressPostalCode, ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER);
//家庭地址
ContentValues row15 = new ContentValues();
addAddress(row15, values, homeAddressCountry, homeAddressState, homeAddressCity, homeAddressStreet, homeAddressPostalCode, ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME);
//工作地址
ContentValues row16 = new ContentValues();
addAddress(row16, values, workAddressCountry, workAddressState, workAddressCity, workAddressStreet, workAddressPostalCode, ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK);

//添加地址方法
private void addAddress(ContentValues row, ArrayList<ContentValues> values, String country, String region, String city, String street, String addressPostalCode, int type) {
row.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
row.put(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, country + region + city + street);
row.put(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, country);
row.put(ContactsContract.CommonDataKinds.StructuredPostal.REGION, region);
row.put(ContactsContract.CommonDataKinds.StructuredPostal.CITY, city);
row.put(ContactsContract.CommonDataKinds.StructuredPostal.STREET, street);
row.put(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, addressPostalCode);
row.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, type);
values.add(row);
}
复制代码

添加方式


添加方式分为三种,一种是静默添加,直接存入数据库中,另外两种是跳转,直接新增或者添加到现有联系人中


1.静默添加

以添加名字为例,直接插入到数据库中


// 向RawContacts.CONTENT_URI空值插入,
// 先获取Android系统返回的rawContactId
// 后面要基于此id插入值
Uri rawContactUri = mActivity.getContentResolver().insert(ContactsContract.RawContacts.CONTENT_URI, values);
long rawContactId = ContentUris.parseId(rawContactUri);
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
// 内容类型
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
// 联系人名字
values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, firstName);
values.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, lastName);
values.put(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, middleName);
// 向联系人URI添加联系人名字
mActivity.getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
复制代码

2.跳转添加

将上边的所有row添加到数组中,一起传递


List<ContentValues> values = new ArrayList<>();
//添加需要设置的数据
...
Intent intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);
intent.putExtra(ContactsContract.Intents.Insert.NAME, name);
intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, values);
mActivity.startActivity(intent);
复制代码

2.添加到现有联系人

将上边的所有row添加到数组中,一起传递,跳转后需要选择联系人


List<ContentValues> values = new ArrayList<>();
//添加需要设置的数据
...
Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, values);
startActivity(intent);
复制代码

权限

权限不能忘了,不然会闪退报错的,分别是联系人的读写权限


    <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
复制代码

总结到此为止,还算比较详细,有补充欢迎评论。


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

iOS运行unity导出工程权限问题

最近公司新开发了一款游戏,分别导出了安卓工程和xcode工程,在运行的过程中,出现了权限问题提示:在这边提示我,有两个.sh文件是被禁止的,没有运行的权限。其实这个问题很好解决,1、首先找到你xcode所在工程的文件夹,找到这两个.sh文件2、打开你的命令行控...
继续阅读 »

最近公司新开发了一款游戏,分别导出了安卓工程和xcode工程,在运行的过程中,出现了权限问题提示:


在这边提示我,有两个.sh文件是被禁止的,没有运行的权限。

其实这个问题很好解决,

1、首先找到你xcode所在工程的文件夹,找到这两个.sh文件


2、打开你的命令行控制器,输入chmod 777 MapFileParser.sh(其实这个·sh文件,直接拖入到命令行就行了)


3、运行完命令之后,再次编译工程即可

收起阅读 »

Jetpack之Navigation(2)

Jetpack之Navigation(1)2.原理 初始化过程 NavHostFragment生命周期方法 1.create—NavHostFragment的创建 在NavHostFragment.create方法 初始化Bundle,并且将graphRes...
继续阅读 »

Jetpack之Navigation(1)


2.原理


初始化过程 NavHostFragment生命周期方法


1.create—NavHostFragment的创建


在NavHostFragment.create方法



  1. 初始化Bundle,并且将graphResId,startDestinationArgs存储在Bundle中。

  2. new NavHostFragment()返回NavHostFragment实例。


    //NavHostFragment.java
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId,
@Nullable Bundle startDestinationArgs)
{
Bundle b = null;
if (graphResId != 0) {
b = new Bundle();
b.putInt(KEY_GRAPH_ID, graphResId);
}
if (startDestinationArgs != null) {
if (b == null) {
b = new Bundle();
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
}

final NavHostFragment result = new NavHostFragment();
if (b != null) {
result.setArguments(b);
}
return result;
}
复制代码

2.onInflate—XML文件的解析


主要是解析布局文件的两个属性:defaultNavHost和navGraph,并且初始化全局变量。


NavHostFragment.onInflate方法 当Fragment以XML的方式静态加载时,最先会调用onInflate的方法(调用时机:Fragment所关联的Activity在执行setContentView时)。


    //NavHostFragment.java
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState)
{
super.onInflate(context, attrs, savedInstanceState);

final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId; //navigation的图布局
}
navHost.recycle();

final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true; //是否监听物理返回键
}
a.recycle();
}
复制代码

3.onCreateNavController—创建Navigator


在实现导航的时候,我们需要根据navigation配置文件生成NavGraph类,然后在根据每个不同的actionid,找到对应的NavDestination就可以实现页面导航跳转了。


创建Navigator


Navigator类的作用是:能够实例化对应的NavDestination,并且能够实现导航功能,拥有自己的回退栈。


    @CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));

//创建Navigator并绑定到NavigatorProvider中。
//mNavigatorProvider是NavController中的全局变量,内部通过HashMap键值对的形式保存Navigator类。
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
复制代码


  1. 其中mNavigatorProvider是NavController中的全局变量,内部通过HashMap键值对的形式保存Navigator类

  2. createFragmentNavigator方法,构建了FragmentNavigator对象,其中抽象类Navigator还有个重要的实现类ActivityNavigator和NavGraphNavigator。这个两个类的对象在NavController的构造方法中被添加。


//NavController.java
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
复制代码

4.onCreate—导航初始化


无论是XML实现还是代码实现,都会执行Fragment的onCreate方法。NavController在这里被创建,并且NavHostFragment中有一个NavController对象。


   @CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();

//1.初始化NavController,NavController为导航的控制类,核心类
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
mNavController.setViewModelStore(getViewModelStore());
onCreateNavController(mNavController);

Bundle navState = null;
//2.开始恢复状态
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
}

if (navState != null) {
mNavController.restoreState(navState);
}

//3.设置导航图信息
if (mGraphId != 0) {
mNavController.setGraph(mGraphId);
} else {
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
super.onCreate(savedInstanceState);
}
复制代码

5.onCreateView


NavHostFragment的视图就只有一个FragmentContainerView 继承 FrameLayout


    //NavHostFragment.java
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());

//这行主要用于以代码方式添加fragment
containerView.setId(getContainerId());
return containerView;
}
复制代码

6.onViewCreated


    @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
//把mNavController记录在view的tag中
Navigation.setViewNavController(view, mNavController);

if (view.getParent() != null) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
//把mNavController记录在view的tag中
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}
复制代码

获取NavController


1.获取NavController


NavHostFragment.findNavController(fragment)
复制代码

    //NavHostFragment.java
@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
Fragment findFragment = fragment;
while (findFragment != null) {
if (findFragment instanceof NavHostFragment) {
return ((NavHostFragment) findFragment).getNavController();
}
Fragment primaryNavFragment = findFragment.getParentFragmentManager()
.getPrimaryNavigationFragment();
if (primaryNavFragment instanceof NavHostFragment) {
return ((NavHostFragment) primaryNavFragment).getNavController();
}
findFragment = findFragment.getParentFragment();
}

View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}

Dialog dialog = fragment instanceof DialogFragment
? ((DialogFragment) fragment).getDialog()
: null;
if (dialog != null && dialog.getWindow() != null) {
return Navigation.findNavController(dialog.getWindow().getDecorView());
}

throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}
复制代码

2.Navigation中findNavController


3.findViewNavController


通过view.tag查找NavController。内部调用了getViewNavController方法。


4.getViewNavController


getViewNavController方法 通过获取view的Tag,获取NavController对象,这里的tag ID和setViewNavController都是nav_controller_view_tag。


    //Navigation.java
@NonNull
public static NavController findNavController(@NonNull View view) {
//3.
NavController navController = findViewNavController(view);
if (navController == null) {
throw new IllegalStateException("View " + view + " does not have a NavController set");
}
return navController;
}


@Nullable
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
if (controller != null) {
return controller;
}
ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
return null;
}

@Nullable
private static NavController getViewNavController(@NonNull View view) {
//4.这里的tag ID和setViewNavController都是nav_controller_view_tag。
Object tag = view.getTag(R.id.nav_controller_view_tag);
NavController controller = null;
if (tag instanceof WeakReference) {
controller = ((WeakReference) tag).get();
} else if (tag instanceof NavController) {
controller = (NavController) tag;
}
return controller;
}
复制代码

导航navigate


navigate


在构建和获取到NavController对象以及NavGraph之后。下面是使用它来实现真正的导航了。下面从navigate开始分析。在navigate方法内部会查询到NavDestination,然后根据不同的Navigator实现页面导航。


public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
//2.根据id,获取对应的NavAction
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
//3.通过NavAction获取目的地id
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}

if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
combinedArgs.putAll(args);
}

if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}

if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used"
+ " in conjunction with a valid navOptions.popUpTo");
}
//4.利用目的地ID属性,通过findDestination方法,找到准备导航的目的地
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
if (navAction != null) {
throw new IllegalArgumentException("Navigation destination " + dest
+ " referenced from action "
+ NavDestination.getDisplayName(mContext, resId)
+ " cannot be found from the current destination " + currentNode);
} else {
throw new IllegalArgumentException("Navigation action/destination " + dest
+ " cannot be found from the current destination " + currentNode);
}
}
//5.开始导航
navigate(node, combinedArgs, navOptions, navigatorExtras);
}


private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
boolean launchSingleTop = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
...
}
复制代码

findDestination


如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。


  NavDestination findDestination(@IdRes int destinationId) {
if (mGraph == null) {
return null;
}
if (mGraph.getId() == destinationId) {
return mGraph;
}
//1.如果回退栈为null返回NavGraph,不为null返回回退栈中的最后一项。
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
NavGraph currentGraph = currentNode instanceof NavGraph
? (NavGraph) currentNode
: currentNode.getParent();
return currentGraph.findNode(destinationId);
}
复制代码

FragmentNavigator的实现


通过以上的分析,又来到了Navigator 的子类FragmentNavigator类。下面来看看FragmentNavigator.navigate的方法。
(1)调用instantiateFragment,通过反射机制构建Fragment实例

(2)处理进出场等动画逻辑

(3)最终调用FragmentManager来处理导航逻辑。


@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
//通过反射机制构建Fragment实例
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();

//处理动画逻辑
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}

//FragmentManager来处理导航逻辑
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);

final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;

boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
if (mBackStack.size() > 1) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
复制代码

ActivityNavigator


ActivityNavigator最终也是调用了startActivity方法,请自己阅读源码。


    @Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
....
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
ActivityOptionsCompat activityOptions = extras.getActivityOptions();
if (activityOptions != null) {
ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
} else {
mContext.startActivity(intent);
}
} else {
mContext.startActivity(intent);
}
...
}
复制代码

3.总结



  1. NavHostFragment 作为导航载体,在Activity的layout文件里被引用(或者在代码中动态),并且持有导航控制类NavController引用。

  2. NavController 将导航任务委托给Navigator类,Navigator类有两个重要的子类FragmentNavigator和ActivityNavigator子类。NavController类持有NavInflater类引用。

  3. NavInflater 负责解析Navgation文件,负责构建NavGraph导航图。

  4. NavDestination 存有各个目的地信息,在FragmentNavigator和ActivityNavigator内部分别对应一个Destination类,该类继承NavDestination。

  5. 在页面导航时,fragment的操作还是交由FragmentManager在操作,activity交由startActivity执行。


image-20210421174444170


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

收起阅读 »

Jetpack之Navigation(1)

1.基本使用引入库 def nav_version = "2.3.2" // Java language implementation implementation "androidx.navigation:navigation-fragment:$nav_...
继续阅读 »

1.基本使用

引入库



def nav_version = "2.3.2"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
复制代码

Activity布局


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<!--
app:defaultNavHost="true"
拦截系统back键
-->

<androidx.fragment.app.FragmentContainerView
android:id="@+id/my_nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="9"
android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main"/>


</LinearLayout>
复制代码

在res/navigation目录下nav_graph_main.xml


<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph_main.xml"
app:startDestination="@id/page1Fragment">

<fragment
android:id="@+id/page1Fragment"
android:name="com.example.lsn4_navigationdemo.MainPage1Fragment"
android:label="fragment_page1"
tools:layout="@layout/fragment_main_page1">


<!--action:程序中使用id跳到destination对应的类-->
<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment" />

</fragment>

<fragment
android:id="@+id/page2Fragment"
android:name="com.example.lsn4_navigationdemo.MainPage2Fragment"
android:label="fragment_page2"
tools:layout="@layout/fragment_main_page2">

<action
android:id="@+id/action_page1"
app:destination="@id/page1Fragment" />

<action
android:id="@+id/action_page3"
app:destination="@id/page3Fragment" />

</fragment>

<!-- <navigation-->
<!-- android:id="@+id/nav_graph_page3"-->
<!-- app:startDestination="@id/page3Fragment">-->
<fragment
android:id="@+id/page3Fragment"
android:name="com.example.lsn4_navigationdemo.MainPage3Fragment"
android:label="fragment_page3"
tools:layout="@layout/fragment_main_page3"
>

<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment"/>

</fragment>


</navigation>
复制代码

Fragment中调用跳转


//方式一:直接跳入指定的fragment
Navigation.findNavController(view).navigate(R.id.page2Fragment);

//方式二:通过action
Navigation.findNavController(view).navigate(R.id.action_page2); //跳入page2
复制代码

Activity中调用跳转


//获取controller方式一
NavController controller=Navigation.findNavController(this,R.id.my_nav_host_fragment);

//获取controller方式二
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.my_nav_host_fragment);
NavController controller = navHostFragment.getNavController();

//跳转
controller.navigate(R.id.page2Fragment);
复制代码

Activity绑定navigation


除了在Activity的布局中指定navigation布局资源以外,还可以通过java代码进行设置


public class MainActivity extends AppCompatActivity {


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

//方式一
val finalHost = NavHostFragment.create(R.navigation.nav_graph_main)
supportFragmentManager.beginTransaction()
.replace(R.id.ll_fragment_navigation, finalHost)
.setPrimaryNavigationFragment(finalHost)
.commit();
}

//方式二
@Override
public boolean onSupportNavigateUp() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.my_nav_host_fragment);
return NavHostFragment.findNavController(fragment).navigateUp();
}
}
复制代码

底部导航


引入底部导航控件


implementation 'com.google.android.material:material:1.1.0'
复制代码

布局


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<!--
app:defaultNavHost="true"
拦截系统back键
-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/my_nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="9"
android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main"/>

<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="wrap_content"
app:itemTextColor="#ff0000"
app:menu="@menu/menu"/>

</LinearLayout>
复制代码

menu


<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/page1Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="1"/>
<item
android:id="@+id/page2Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="2"/>
<item
android:id="@+id/page3Fragment"
android:icon="@drawable/ic_launcher_foreground"
android:title="3"/>

</menu>
复制代码

Activity绑定导航


public class MainActivity extends AppCompatActivity {

BottomNavigationView bottomNavigationView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.action_page1);
bottomNavigationView=findViewById(R.id.nav_view);

//获取controller方式一
//NavController controller=Navigation.findNavController(this,R.id.my_nav_host_fragment);

//获取controller方式二
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.my_nav_host_fragment);
NavController controller = navHostFragment.getNavController();

//绑定导航
NavigationUI.setupWithNavController(bottomNavigationView,controller);

}

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

收起阅读 »

drawable用Kotlin应该这样写

前言 通常我们在res/drawable下面自定义shape和selector来满足一些UI的设计,但是由于xml最终转换为drawable需要经过IO或反射创建,会有一些性能损耗,另外随着项目的增大和模块化等,很多通用的样式并不能快速复用,需要合理的项目资源...
继续阅读 »

前言


通常我们在res/drawable下面自定义shapeselector来满足一些UI的设计,但是由于xml最终转换为drawable需要经过IO或反射创建,会有一些性能损耗,另外随着项目的增大和模块化等,很多通用的样式并不能快速复用,需要合理的项目资源管理规范才能实施。那么通过代码直接创建这些drawable,可以在一定程度上降低这些副作用。本篇介绍用kotlin DSL简洁的语法特性来实现常见的drawable.


代码对应效果预览


shape_line
RECTANGLE
OVAL
LayerList
Selector

集成和使用



  1. 在项目级的build.gradle文件种添加仓库Jitpack:


allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
复制代码


  1. 添加依赖


dependencies {		
implementation 'com.github.forJrking:DrawableDsl:0.0.1’
}
复制代码


  1. 抛弃xml创建方式示例(其他参见demo)


// infix用法用于去掉括号更加简洁,详细后面说明
image src = shapeDrawable {
//指定shape样式
shape(ShapeBuilder.Shape.RECTANGLE)
//圆角,支持4个角单独设置
corner(20f)
//solid 颜色
solid("#ABE2E3")
//stroke 颜色,边框dp,虚线设置
stroke(android.R.color.white, 2f, 5f, 8f)
}
//按钮点击样式
btn.background = selectorDrawable {
//默认样式
normal = shapeDrawable {
corner(20f)
gradient(90, R.color.F97794, R.color.C623AA2)
}
//点击效果
pressed = shapeDrawable {
corner(20f)
solid("#84232323")
}
}
复制代码

实现思路


xml如何转换成drawable


xml变成drawable,通过android.graphics.drawable.DrawableInflater这个类来IO解析标签创建:


//标签创建
private Drawable inflateFromTag(@NonNull String name) {
switch (name) {
case "selector":
return new StateListDrawable();
case "level-list":
return new LevelListDrawable();
case "layer-list":
return new LayerDrawable();
....
case "color":
return new ColorDrawable();
case "shape":
return new GradientDrawable();
case "vector":
return new VectorDrawable();
...
}
}
//反射创建
private Drawable inflateFromClass(@NonNull String className) {
try {
Constructor<? extends Drawable> constructor;
synchronized (CONSTRUCTOR_MAP) {
constructor = CONSTRUCTOR_MAP.get(className);
if (constructor == null) {
final Class<? extends Drawable> clazz = mClassLoader.loadClass(className).asSubclass(Drawable.class);
constructor = clazz.getConstructor();
CONSTRUCTOR_MAP.put(className, constructor);
}
}
return constructor.newInstance();
} catch (NoSuchMethodException e) {
...
}
复制代码

代码实现


由于创建shape等需要设置各种属性来构建,比较符合build设计模式,那我们首先封装build模式的shapeBuilder,这样做虽然代码比起直接使用apply{}多但是可以让纯java项目用起来很舒服,其他实现请查看源码:


class ShapeBuilder : DrawableBuilder {
private var mRadius = 0f
private var mWidth = 0f
private var mHeight = 0f
...
private var mShape = GradientDrawable.RECTANGLE
private var mSolidColor = 0

/**分别设置四个角的圆角*/
fun corner(leftTop: Float,rightTop: Float,leftBottom: Float,rightBottom: Float): ShapeBuilder {
....
return this
}

fun solid(@ColorRes colorId: Int): ShapeBuilder {
mSolidColor = ContextCompat.getColor(context, colorId)
return this
}
// 省略其他参数设置方法 详细代码查看源码
override fun build(): Drawable {
val gradientDrawable = GradientDrawable()
gradientDrawable = GradientDrawable()
gradientDrawable.setColor(mSolidColor)
gradientDrawable.shape = mShape
....其他参数设置
return gradientDrawable
}
}
复制代码

把build模式转换为dsl


理论上所有的build模式都可以轻松转换为dsl写法:


inline fun shapeDrawable(builder: ShapeBuilder.() -> Unit): Drawable {
return ShapeBuilder().also(builder).build()
}
//使用方法
val drawable = shapeDrawable{
...
}
复制代码

备注:dsl用法参见juejin.cn/post/695318… 中dsl小节


infix函数来去括号


通过上面封装已经实现了dsl的写法,通常setBackground可以通过setter简化,但是我发现由于有些api设计还需要加括号,这样不太kotlin:


//容易阅读
iv1.background = shapeDrawable {
shape(ShapeBuilder.Shape.RECTANGLE)
solid("#ABE2E3")
}
//多了括号看起来不舒服
iv2.setImageDrawable(shapeDrawable {
solid("#84232323")
})
复制代码

怎么去掉括号呢?这里就要用到infix函数特点和规范:



  • Kotlin允许在不使用括号和点号的情况下调用函数

  • 必须只有一个参数

  • 必须是成员函数或扩展函数

  • 不支持可变参数和带默认值参数


/**为所有ImageView添加扩展infix函数 来去掉括号*/
infix fun ImageView.src(drawable: Drawable?) {
this.setImageDrawable(drawable)
}
//使用如下 舒服了
iv2 src shapeDrawable {
shape(ShapeBuilder.Shape.OVAL)
solid("#E3ABC2")
}
复制代码

当然了代码是用来阅读的。个人认为如果我们大量使用infix函数,阅读困难会大大增加,所以建议函数命名必须可以直击函数功能,而且函数功能简单且单一。


优缺点


优点:



  • 代码直接创建比起xml方式可以提升性能

  • dsl方式比起build模式和调用方法设置更加简洁符合kotlin风格

  • 通过合适的代码管理可以复用这些代码,比xml管理方便


缺点:



  • 没有as的预览功能,只有通过上机观测

  • api还没有覆盖所有drawable属性


后语


上面把的DrawableDsl基础用法介绍完了,欢迎大家使用,欢迎提Issues,记得给个star哦。Github链接:https://github.com/forJrking/DrawableDsl


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

Android各版本的行为变更

本文的主要内容来自官方,摘出了Android开发者应该关注的重点变更,目前已经更新到Android 11,新版本发布时会持续更新,如果对您有帮助请不吝点赞! 一、Android 5.0——21——Lollipop(棒棒糖) ART 限制获取AppTask ...
继续阅读 »

本文的主要内容来自官方,摘出了Android开发者应该关注的重点变更,目前已经更新到Android 11,新版本发布时会持续更新,如果对您有帮助请不吝点赞!



一、Android 5.0——21——Lollipop(棒棒糖)



  1. ART


  2. 限制获取AppTask


  3. WebView默认阻止第三方内容:setMixedContentMode()


  4. Material Design


  5. 提出JobScheduler



  6. 屏幕采集和屏幕共享


    Android 5.0 引入了新的 android.media.projection API,让您可以为应用添加屏幕采集和屏幕共享功能。例如,如果您想在视频会议应用中启用屏幕共享,便可使用此功能。

    新增的 createVirtualDisplay() 方法允许您的应用将主屏幕(默认显示)的内容采集到一个 Surface 对象中,然后您的应用便可将其发送至整个网络。该 API 只允许采集非安全屏幕内容,不允许采集系统音频。要开始采集屏幕,您的应用必须先使用通过 createScreenCaptureIntent() 方法获得的 Intent 启动屏幕采集对话框,请求用户授予权限。



二、Android 6.0——23——Marshmallow(棉花糖)



  1. 省电机制Doze引入

  2. 运行时权限

  3. 移除了对设备本地硬件标识符的编程访问权

  4. 指纹API


三、Android 7.0——24——Nougat(牛轧糖)



  1. FileProvider

  2. 低耗电模式进一步优化

  3. 多窗口支持

  4. 添加JIT,属于对AOT的一种补充机制


四、Android 8.0——26——Oreo(奥利奥)



  1. 当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。


  2. 应用无法使用其清单注册大部分隐式广播


  3. 应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException


  4. 后台应用时,降低了后台应用接收位置更新的频率


  5. 画中画模式


  6. 应用快捷菜单


  7. 音频自动闪避,失去焦点时自动调小音量



  8. 多显示器支持(移动Activity到另一个窗口会onConfigChange)


    在多窗口模式下,在任意时间点都只有用户最近一次互动的 Activity 处于活动状态。此 Activity 被视为最顶层的 Activity,而且是唯一处于 RESUMED 状态的 Activity。所有其他可见的 Activity 均处于 STARTED 而非 RESUMED 状态。不过,这些可见但并不处于 RESUMED 状态的 Activity 在系统中享有比不可见 Activity 更高的优先级。如果用户与其中一个可见的 Activity 互动,那么该 Activity 将进入 RESUMED 状态,而之前的最顶层 Activity 将进入 STARTED 状态。



五、Android 9.0——28——Pie



  1. 后台应用无法访问用户输入和一些传感器数据,如麦克风相机,加速器陀螺仪等,除非使用前台服务

  2. 通话记录权限从Phone组移动到CALL_LOG组,plus,一个组的权限有了一个,其他的的权限也就有了(申请会自动授权)

  3. 不允许使用非SDKAPI,也就是hide的一些SDK方法不许用反射和JNI调用

  4. 支持检测显示屏缺口,非功能区域DisplayCutout,比如检测挖孔屏

  5. 利用 Wi-Fi RTT 进行室内定位

  6. ImageDecoder类可取代BitmapFactory


六、Android 10——29——Q



  1. 屏幕边缘手势导航(各大厂商早就实现了)



  2. 为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储空间的分区访问权限(即分区存储)。此类应用只能访问外部存储空间上的应用专属目录,以及本应用所创建的特定类型的媒体文件。

    注意:如果您的应用在运行时请求与存储空间相关的权限,面向用户的对话框会表明您的应用正在请求对外部存储空间的广泛访问,即使已启用分区存储也是如此。


  • 后台应用不能启动一个新的Activity


  • 新增后台访问位置信息权限,无则不可以(ACCESS_BACKGROUND_LOCATION)


  • 应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号),普通应用基本是不可以的


  • 除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。



  • 七、Android 11——30——R



    1. 单次授权(权限只给这一次)


    2. 自动重置权限——几个月用户未与应用交互,将回收应用权限



    3. 支持瀑布屏API


      现有的 DisplayCutout.getSafeInset…() 方法现在会返回能够避开瀑布区域以及刘海的安全边衬区



    4. 新增前台服务类型属性,必须指明为camera和microphone才能访问麦克风,在后台运行时启动的前台服务无法访问麦克风和相机,除非具备后台访问权限


      <manifest>
      ...
      <service ... android:foregroundServiceType="location|camera" />
      </manifest>



    5. 软件包可见性——不是所有应用都对软件可见了


      1.影响queryIntentActivities()的返回结果
      2.影响启动其他应用的Service
      3.不影响启动Activity
      4.别的应用和你发生交互,那它对你是可见的
      5.系统的一些支持程序是可见的
      6.ContentProvider是可见的,如果你具备权限的话
      7.用<queries>生命想要可见的一些应用





    1. 应用退出原因


      Android 11 引入了 ActivityManager.getHistoricalProcessExitReasons() 方法,用于报告近期任何进程终止的原因。应用可以使用此方法收集崩溃诊断信息,例如进程终止是由于 ANR、内存问题还是其他原因所致。此外,您还可以使用新的 setProcessStateSummary() 方法存储自定义状态信息,以便日后进行分析。



    2. 资源加载器ResourcesLoader——可用做换肤和插件化


      Android 11 引入了一个新 API,允许应用动态扩展资源的搜索和加载方式。新的 API 类 ResourcesLoader 和 ResourcesProvider 主要负责提供新功能。两者协同作用,可以提供额外的资源,或修改现有资源的值。

      ResourcesLoader 对象是向应用的 Resources 实例提供 ResourcesProvider 对象的容器,而 ResourcesProvider 对象提供从 APK 和资源表加载资源数据的方法。

      此 API 的一个主要用例是自定义资源加载。您可以使用 loadFromDirectory() 创建一个 ResourcesProvider,用于重定向基于文件的资源的解析,从而让其搜索特定目录,而不是应用 APK。您可以通过 AssetManager API 类中的 open() 系列方法访问这些资源,就像访问 APK 中绑定的资源一样。


    作者:Mr云台
    链接:https://www.jianshu.com/p/e9fbbd52065b
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。  

    收起阅读 »

    iOS内存管理

    将计算机上有限的物理内存分配给多个程序使用地址空间不隔离内存使用率低程序运行的地址不确定虚拟内存虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,每个进程只能访问自己的地址空间,这样就能有效的做到了进程的隔离。注:...
    继续阅读 »

    将计算机上有限的物理内存分配给多个程序使用

    • 地址空间不隔离

    • 内存使用率低

    • 程序运行的地址不确定

    虚拟内存
    虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,每个进程只能访问自己的地址空间,这样就能有效的做到了进程的隔离。
    注: 虚拟储存的实现需要依赖硬件的支持,对于不同的CPU来说不同,但是几乎所有的硬件都采用MMU(Memory Management Unit)的部件来进行页映射。


    分段
    把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。


    • 因为程序A和程序B被映射到了两块不同的物理空间区域,它们之间没有任何重叠,如果A程序访问虚拟空间的地址超出了0x00A00000这个范围,那么硬件就会判断这是一个非法访问,拒绝这个请求,所以做到了地址隔离。

    • 对于每个程序来说,无论它们被分配到物理地址的那一个区域,都是透明的,它们不关心物理地址的变化,只要按照从地址0x0000000到0x00A00000来编写程序,放置变量,所以程序不再需要重定位

    分页
    当一个程序运行时,在某个时间段内,它只是频繁的用到了一小部分数据,程序的很多数据其实在一个时间段内都不会被用到。人们很自然的想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。


    • 进程1(VP0,VP5)和进程2(VP0,VP3,VP4,VP5)的虚拟地址映射到物理地址(PP0,PP2,PP2,PP3,PP4),可以看到有些虚拟空间被映射到同一个物理页,这样就实现了内存共享。

    • 还有一部分位于磁盘中,如果进程需要用到这两个页时,操作系统就会接管进程,负责将虚拟地址从磁盘中读出来并装入内存,然后再与物理地址建立映射关系。

    • 保护也是页映射的目的之一,每个页可以设置权限属性,而只有操作系统有权限修改这些属性,那么操作系统就可以保护自己和保护进程了。

    PAE
    原先的32位地址只能访问最多4GB的物理内存,但是自从扩展至36位地址线以后,Intel修改了页映射方式,使得新的映射方式可以访问到更多的物理内存,Intel把这个地址扩展方式叫做PAE(Physical Address Extension)、

    AWE
    应用程序可以根据需求来选择申请和映射,比如一个应用程序0x10000000 ~0x20000000这一段256MB的虚拟地址空间用来做窗口,程序可以从高4GB的物理空间申请多个大小为256MB的物理空间,编号成A、B、C的等,然后根据需要将这个窗口映射到不同物理空间块,用到A时映射到A,用到B、C时再映射过去,叫做AWE(Address Windowing Extension),而Linux等UNIX类操作系统则采用mmap()系统调用来实现

    • 程序执行做需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将进程运行所需要的指令和数据全部装入内存中,这就是简单的静态装入。

    • 由于内存的昂贵稀有,所需的内存大余物理内存,所以我们将程序最常用的部分驻留在内存中,不太常用的数据存放在磁盘里,这就是动态装入。覆盖装入和页映射就是两种很典型的动态装载方法。

    覆盖装入(Overlay)


    • 这个树状结构从任何一个模块到树的根(main)模块都叫调用路径,当该模块被调用时,整个调用路径上的模块都必须在内存中,已确保执行完毕后能正确返回至模块。

    • 禁止跨树间调用。任意一个模块不允许跨过树状结构进行调用。

    • 由于跨模块间的调用需要经过覆盖管理器,已确保所有被调用到的模块都能够正确的驻留在内存,而且一旦模块没有在内存中,还需要从磁盘或者其他存储器读取响应的模块,所以覆盖装入的速度肯定比较慢,是典型的利用时间换取空间的方法

    页映射(Paging)


    • 将内存和所有磁盘中的数据和指令按照page为单位划分为若干个页,以后所有的装载和操作的单位都是页,硬件规定页的大小为4096字节,8192KB等,那么512MB的物理内存就拥有512 * 1024 * 1024 /4096 = 131072 页。

    • 假设32的机器有16KB的内存,每个页4096字节,则共有4个页(F0、F1、F2、F3),假设程序需有的指令和数据总和为32KB,那么程序被分为8个页(P0~P7)。很明显16KB的内存无法同时将32KB的程序装入,那么我们将按照动态装入的原理来进行整个装入过程。

    • 如果只有4个页,那么程序能一直执行下去,但问题很明显不是,如果超过4个页,装载管理器必须作出抉择,放弃目前正在使用的4个内存中的其中一个。至于放弃那个页有多种算法:比如F0,先进先出;比如很少访问的F2,最少使用法

    页错误(Page Fault)
    一些存储在磁盘中的数据,在CPU执行这个地址指令时,发现页面是一个空的页面,于是他就认为这是一个页错误,CPU将控制权交给操作系统,操作系统有专门处理例程来处理,操作系统将查询这个数据结构,然后找到空页面所在的VMA,计算相应的页面在可执行文件中的偏移,然后再物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射,然后再交给进程去执行。

    进程虚拟空间分布
    ELF文件被映射时,是以页长度为单位的,每个段在映射时的长度应该是系统页长度的整倍数,如果不是,多余的部分页将占领一个页,造成了内存空间的大量浪费。而在ELF文件中,段的权限直邮为数不多的几种组合:

    • 以代码段为代表的权限为可读可执行的段

    • 以数据段和BSS段为代表的权限为可读可写的段

    • 以只读数据段为代表的权限为只读的段

    那么对于相同的段,我们把他们合并在一起当成一个段来映射,ELF可执行文件引入一个概念叫做Segment,一个segment包含一个或多个section,这样很明显的减少了页面内部的碎片,节省了空间

    段地址对齐
    假设一个ELF执行文件,它有三个段需要装载,SEG0 、SEG1、SEG2,如图:


    可以看到这种对齐方式在文件段的内部会有很多内部碎片,浪费磁盘空间,可执行文件总长度只有12014字节,却占了5个页。为了解决这个问题,UNIX系统采用了让那些个个段接壤的部分共用一个物理页面,将该物理页面映射两次,系统将它们映射两份到虚拟地址空间,其他的都按照正常的页粒度进行映射。

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


    收起阅读 »

    iOS 中事件的响应链和传递链

    iOS事件链有两条:事件的响应链;Hit-Testing事件的传递链响应链:由离用户最近的view向系统传递。initial view –> super view –> ….. –> view controller –> window ...
    继续阅读 »

    iOS事件链有两条:事件的响应链;Hit-Testing事件的传递链

    • 响应链:由离用户最近的view向系统传递。initial view –> super view –> ….. –> view controller –> window –> Application –> AppDelegate

    • 传递链:由系统向离用户最近的view传递。UIKit –> active app's event queue –> window –> root view –> …… –> lowest view

    在iOS中只有继承UIResponder的对象才能够接收并处理事件,UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象,首先我们通过一张图来简单了解一下事件的传递以及响应


    1、传递链

    • 事件传递的两个核心方法

    - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
    - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
    • 第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件

    • 第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES

    • 其中UIView不接受事件处理的情况有

    1. alpha <0.01
    2. userInteractionEnabled = NO
    3. hidden = YES
    • 事件传递的流程图


    • 流程描述

         1、我们点击屏幕产生触摸事件,系统将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow

         2、在UIWindow中就会调用hitTest:withEvent:方法去返回一个最终响应的视图

         3、在hitTest:withEvent:方法中就会去调用pointInside: withEvent:去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图

         4、遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的hitTest:withEvent:方法,可以理解为是一个递归调用

         5、最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIWindow作为响应者

    2、相应链

    • 响应者链流程图


    • 响应者链的事件传递过程总结如下

         1、如果view的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图

         2、在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给UIWindow对象进行处理

         3、如果UIWindow对象也不处理,则将事件传递给UIApplication对象

         4、如果UIApplication也不能处理该事件,则将该事件丢弃

    3、实例场景

    • 在一个方形按钮中点击中间的圆形区域有效,而点击四角无效

    • 核心思想是在pointInside: withEvent:方法中修改对应的区域

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 如果控件不允许与用用户交互,那么返回nil
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
    return nil;
    }

    //判断当前视图是否在点击范围内
    if ([self pointInside:point withEvent:event]) {
    //遍历当前对象的子视图(倒序)
    __block UIView *hit = nil;
    [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    //坐标转换,把当前坐标系上的点转换成子控件坐标系上的点
    CGPoint convertPoint = [self convertPoint:point toView:obj];
    //调用子视图的hitTest方法,判断自己的子控件是不是最适合的View
    hit = [obj hitTest:convertPoint withEvent:event];
    //如果找到了就停止遍历
    if (hit) *stop = YES;
    }];

    //返回当前的视图对象
    return hit?hit:self;
    }else {
    return nil;
    }
    }

    // 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat x1 = point.x;
    CGFloat y1 = point.y;

    CGFloat x2 = self.frame.size.width / 2;
    CGFloat y2 = self.frame.size.height / 2;

    //判断是否在圆形区域内
    double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    if (dis <= self.frame.size.width / 2) {
    return YES;
    }
    else{
    return NO;
    }
    }

    转自:jianshu.com/p/cfcde82c67f6


    收起阅读 »

    iOS 中 如何从视频中提取音频

    .h文件/**提取视频中的音频@param videoPath 视频路径@param completionHandle 完成回调*/+(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void ...
    继续阅读 »

    .h文件
    /**

    提取视频中的音频

    @param videoPath 视频路径
    @param completionHandle 完成回调
    */

    +(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void (^)(NSURL *outputPath,BOOL isSucceed)) completionHandle;

    .m文件
    需要导入系统的#import <Photos/Photos.h>
    /**

    提取视频中的音频

    @param videoPath 视频路径
    @param completionHandle 完成回调
    */

    +(void)accessAudioFromVideo:(NSURL *)videoPath completion:(void (^)(NSURL *outputPath,BOOL isSucceed)) completionHandle{

    AVAsset *videoAsset = [AVAsset assetWithURL:videoPath];
    //1创建一个AVMutableComposition
    AVMutableComposition *mixComposition = [[AVMutableComposition alloc]init];
    //2 创建一个轨道,类型为AVMediaTypeAudio
    AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

    //获取videoPath的音频插入轨道
    [audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.duration) ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:nil];

    //4创建输出路径
    NSURL *outputURL = [self exporterPath:@"mp4"];

    //5创建输出对象
    AVAssetExportSession *exporter = [[AVAssetExportSession alloc]initWithAsset:mixComposition presetName:AVAssetExportPresetAppleM4A];
    exporter.outputURL = outputURL ;
    exporter.outputFileType = AVFileTypeAppleM4A;
    exporter.shouldOptimizeForNetworkUse = YES;
    [exporter exportAsynchronouslyWithCompletionHandler:^{
    if (exporter.status == AVAssetExportSessionStatusCompleted) {
    NSURL *outputURL = exporter.outputURL;
    completionHandle(outputURL,YES);
    }else {
    NSLog(@"失败%@",exporter.error.description);
    completionHandle(outputURL,NO);
    }
    }];

    }
    // 输出路径
    + (NSURL *)exporterPath:(NSString *)filename{
    NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
    formatter.dateFormat = @"yyyyMMddHHmmss";
    NSString *str = [formatter stringFromDate:[NSDate date]];
    NSString *fileName = [NSString stringWithFormat:@"selfMusic%@.%@",str,filename];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docPath = [paths firstObject];
    //这个是录制视频时存储到本地的video
    NSString *path = [NSString stringWithFormat:@"%@/KSYShortVideoCache",docPath];
    //判断文件夹是否存在,不存在就创建
    //创建附件存储目录
    if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
    [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
    }
    NSString *outputFilePath = [path stringByAppendingPathComponent:fileName];
    return [NSURL fileURLWithPath:outputFilePath];
    }

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

    收起阅读 »

    静态拦截iOS对象方法调用的简易实现

    最近出现了几篇关于二进制重排启动优化的文章。所有方案中都需要事先统计所有的函数调用情况,并根据函数调用的频次来进行代码的重排。这些函数调用中,OC对象的方法调用最多。统计OC对象的方法调用可以在运行时通过第三方库比如fishhook来Hook所有objc_ms...
    继续阅读 »

    最近出现了几篇关于二进制重排启动优化的文章。所有方案中都需要事先统计所有的函数调用情况,并根据函数调用的频次来进行代码的重排。
    这些函数调用中,OC对象的方法调用最多。统计OC对象的方法调用可以在运行时通过第三方库比如fishhook来Hook所有objc_msgSend调用来实现,也可以在编译后链接前通过静态插桩的方式来实现Hook拦截。
    对于静态插桩的实现一般有如下两个方案:
    1、助于LLVM语法树分析来实现代码插桩。
    2、将源码编译为静态库,并通过修改静态库中.o目标文件的代码段来实现代码插桩。
    上述的两个方法实现起来比较复杂,要么就要了解LLVM,要么就要熟悉目标文件中间字节码以及符号表相关的底层知识。
    本文所介绍的是第三种静态Hook方案,也是依赖于静态库这个前提来实现对objc_msgSend函数进行Hook,从而实现在编译前链接后的OC对象方法调用插桩。
    这个方案实现的原理很简单。因为静态库其实只是一个编译阶段的中间产物,静态库目标文件中的所有引用的外部符号会保存到一张字符串表中,所有函数调用都只是记录了函数名称在字符串表的索引位置,在链接时会才会根据符号名称来替换为真实的函数调用指令。因此我们可以将所有静态库字符串表中的objc_msgSend统一替换为另外一个长度相同的字符串:hook_msgSend(名字任意只要长度一致并唯一)即可。然后在主工程源代码中实现一个名字为hook_msgSend的函数即可。这个函数必须要和objc_msgSend的函数签名保持一致,这样在链接时所有静态库中的objc_msgSend调用都会统一转化为hook_msgSend调用。

    下面的是具体的实现步骤:

    1.在主工程中编写hook_msgSend的实现。
    hook_msgSend的函数签名要和objc_msgSend保持一致,并且要在主工程代码中实现,而且必须要用汇编代码实现。具体实现的逻辑和目前很多文章中介绍的对objc_msgSend函数的Hook实现保持一致即可。
    很多对objc_msgSend进行Hook的实现其实是不完整的,因此如果想完全掌握函数调用ABI规则的话请参考:《深入iOS系统底层之函数调用》

    2. 将所有其他代码都统一编译为一个或多个静态库。
    将源代码按功能编译为一个或多个静态库,并且主工程链接这些静态库。这种程序代码的组织方式已经很成熟了,最常用的方法是我们可以借助代码依赖集成工具cocoapods来实现,这里就不再赘述了。

    3.在主工程的Build Phases 中添加Run Script脚本。
    我们需要保证这个脚本一定要运行在链接所有静态库之前执行。因此可以放到Compile Sources 下面。

    4.实现静态库符号替换的Run Script脚本。
    这是最为关键的一步,我们可以实现一个符号替换的程序,然后在Run Script脚本中 执行这个符号替换程序。符号替换程序的输入参数就是主工程中所链接的所有静态库的路径。至于这个符号替换程序如何编写则没有限制,你可以用ruby编写也可以用python也可以用C语言编写。 无论用何种方法实现,你都需要首先了解一下静态库.a的文件结构。你可以从:《深入iOS系统底层之静态库》一文中掌握到一个静态库文件的组成结构。了解了静态库文件的组成结构后,你的符号替换程序要做的事情就可以按如下步骤实现:

    一)、 打开静态库.a文件。
    二)、找.a文件中定义的字符串表部分。字符串表的描述如下:

    struct stringtab
    {
    int size; //字符串表的尺寸
    char strings[0]; //字符串表的内容,每个字符串以\0分隔。
    };

    字符串表中的strings的内容就是一个个以\0分隔的字符串,这些字符串的内容其实就是这个目标文件所引用的所有外部和内部的符号名称。
    三)、将字符串表中的objc_msgSend字符串替换为hook_msgSend字符串。
    四)、保存并关闭静态库.a文件。

    5.编译、链接并运行你的主工程程序。
    采用本文中所介绍的静态Hook方法的好处是我们不必Hook所有的OC方法调用,而是可以有选择的进行特定对象和类的方法调用拦截。因此这种技术不仅可以应用代码重排统计上,还可以应用在其他的监控和统计应用中。因为这种机制可以避免程序在运行时进行objc_msgSend替换而产生的函数调用风暴问题。另外的一个点就是这个方法不局限于对objc_msgSend进行Hook,还可以对任意的其他函数进行Hook处理。因此这种技术也可以应用在其他方面。

    转自:https://www.jianshu.com/p/843642c9df32

    收起阅读 »

    webpack手写loader

    手写loader   我们在在Webpack配置基础篇介绍过,loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则: 单一原则: 每个Loader只做一件事,...
    继续阅读 »

    手写loader


      我们在在Webpack配置基础篇介绍过,loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则:



    1. 单一原则: 每个Loader只做一件事,简单易用,便于维护;

    2. 链式调用: Webpack 会按顺序链式调用每个Loader;

    3. 统一原则: 遵循Webpack制定的设计规则和结构,输入与输出均为字符串,各个Loader完全独立,即插即用;

    4. 无状态原则:在转换不同模块时,不应该在loader中保留状态;


      因此我们就来尝试写一个less-loaderstyle-loader,将less文件处理后通过style标签的方式渲染到页面上去。


    同步loader


      loader默认导出一个函数,接受匹配到的文件资源字符串和SourceMap,我们可以修改文件内容字符串后再返回给下一个loader进行处理,因此最简单的一个loader如下:


    module.exports = function(source, map){
    return source
    }


    导出的loader函数不能使用箭头函数,很多loader内部的属性和方法都需要通过this进行调用,比如this.cacheable()来进行缓存、this.sourceMap判断是否需要生成sourceMap等。



      我们在项目中创建一个loader文件夹,用来存放我们自己写的loader,然后新建我们自己的style-loader:


    //loader/style-loader.js
    function loader(source, map) {
    let style = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style)
    `
    ;
    return style;
    }
    module.exports = loader;

      这里的source就可以看做是处理后的css文件字符串,我们把它通过style标签的形式插入到head中;同时我们也发现最后返回的是一个JS代码的字符串,webpack最后会将返回的字符串打包进模块中。


    异步loader


      上面的style-loader都是同步操作,我们在处理source时,有时候会进行异步操作,一种方法是通过async/await,阻塞操作执行;另一种方法可以通过loader本身提供的回调函数callback


    //loader/less-loader
    const less = require("less");
    function loader(source) {
    const callback = this.async();
    less.render(source, function (err, res) {
    let { css } = res;
    callback(null, css);
    });
    }
    module.exports = loader;

      callback的详细传参方法如下:


    callback({
    //当无法转换原内容时,给 Webpack 返回一个 Error
    error: Error | Null,
    //转换后的内容
    content: String | Buffer,
    //转换后的内容得出原内容的Source Map(可选)
    sourceMap?: SourceMap,
    //原内容生成 AST语法树(可选)
    abstractSyntaxTree?: AST
    })

      有些时候,除了将原内容转换返回之外,还需要返回原内容对应的Source Map,比如我们转换less和scss代码,以及babel-loader转换ES6代码,为了方便调试,需要将Source Map也一起随着内容返回。


    //loader/less-loader
    const less = require("less");
    function loader(source) {
    const callback = this.async();
    less.render(source,{sourceMap: {}}, function (err, res) {
    let { css, map } = res;
    callback(null, css, map);
    });
    }
    module.exports = loader;

      这样我们在下一个loader就能接收到less-loader返回的sourceMap了,但是需要注意的是:



    Source Map生成很耗时,通常在开发环境下才会生成Source Map,其它环境下不用生成。Webpack为loader提供了this.sourceMap这个属性来告诉loader当前构建环境用户是否需要生成Source Map。



    加载本地loader


      loader文件准备好了之后,我们需要将它们加载到webpack配置中去;在基础篇中,我们加载第三方的loader只需要安装后在loader属性中写loader名称即可,现在加载本地loader需要把loader的路径配置上。


    module.exports = {
    module: {
    rules: [{
    test: /\.less/,
    use: [
    {
    loader: './loader/style-loader.js',
    },
    {
    loader: path.resolve(__dirname, "loader", "less-loader"),
    },
    ],
    }]
    }
    }

      我们可以在loader中配置本地loader的相对路径或者绝对路径,但是这样写起来比较繁琐,我们可以利用webpack提供的resolveLoader属性,来告诉webpack应该去哪里解析本地loader。


    module.exports = {
    module: {
    rules: [{
    test: /\.less/,
    use: [
    {
    loader: 'style-loader',
    },
    {
    loader: 'less-loader',
    },
    ],
    }]
    },
    resolveLoader:{
    modules: [path.resolve(__dirname, 'loader'), 'node_modules']
    }
    }

      这样webpack会先去loader文件夹下找loader,没有找到才去node_modules;因此我们写的loader尽量不要和第三方loader重名,否则会导致第三方loader被覆盖加载。


    处理参数


      我们在配置loader时,经常会给loader传递参数进行配置,一般是通过options属性来传递的,也有像url-loader通过字符串来传参:


    {
    test: /\.(jpg|png|gif|bmp|jpeg)$/,
    use: 'url-loader?limt=1024&name=[hash:8].[ext]'
    }

      webpack也提供了query属性来获取传参;但是query属性很不稳定,如果像上面的通过字符串来传参,query就返回字符串格式,通过options方式就会返回对象格式,这样不利于我们处理。因此我们借助一个官方的包loader-utils帮助处理,它还提供了很多有用的工具。


    const { 
    getOptions,
    parseQuery,
    stringifyRequest,
    } = require("loader-utils");

    module.exports = function (source, map) {
    //获取options参数
    const options = getOptions(this);
    //解析字符串为对象
    parseQuery("?param1=foo")
    //将绝对路由转换成相对路径
    //以便能在require或者import中使用以避免绝对路径
    stringifyRequest(this, "test/lib/index.js")
    }

      常用的就是getOptions将处理后的参数返回出来,它内部的实现逻辑也非常的简单,也是根据query属性进行处理,如果是字符串的话调用parseQuery方法进行解析,源码如下:


    //loader-utils/lib/getOptions.js
    'use strict';
    const parseQuery = require('./parseQuery');
    function getOptions(loaderContext) {
    const query = loaderContext.query;
    if (typeof query === 'string' && query !== '') {
    return parseQuery(loaderContext.query);
    }
    if (!query || typeof query !== 'object') {
    return {};
    }
    return query;
    }
    module.exports = getOptions;

      获取到参数后,我们还需要对获取到的options参数进行完整性校验,避免有些参数漏传,如果一个个判断校验比较繁琐,这就用到另一个官方包schema-utils


    const { getOptions } = require("loader-utils");
    const { validate } = require("schema-utils");
    const schema = require("./schema.json");
    module.exports = function (source, map) {
    const options = getOptions(this);
    const configuration = { name: "Loader Name"};
    validate(schema, options, configuration);
    //省略其他代码
    }

      validate函数并没有返回值,打印返回值发现是`undefined,因为如果参数不通过的话直接会抛出ValidationError异常,直接进程中断;这里引入了一个schema.json,就是我们对options``中参数进行校验的一个json格式的对应表:


    {
    "type": "object",
    "properties": {
    "source": {
    "type": "boolean"
    },
    "name": {
    "type": "string"
    },
    },
    "additionalProperties": false
    }

      properties中的健名就是我们需要检验的options中的字段名称,additionalProperties代表了是否允许options中还有其他额外的属性。


    less-loader源码分析


      写完我们自己简单的less-loader,让我们来看一下官方的less-loader源码到底是怎么样的,这里贴上部分源码:


    import less from 'less';
    import { getOptions } from 'loader-utils';
    import { validate } from 'schema-utils';
    import schema from './options.json';
    async function lessLoader(source) {
    const options = getOptions(this);
    //校验参数
    validate(schema, options, {
    name: 'Less Loader',
    baseDataPath: 'options',
    });
    const callback = this.async();
    //对options进一步处理,生成less渲染的参数
    const lessOptions = getLessOptions(this, options);
    //是否使用sourceMap,默认取options中的参数
    const useSourceMap =
    typeof options.sourceMap === 'boolean'
    ? options.sourceMap : this.sourceMap;
    //如果使用sourceMap,就在渲染参数加入
    if (useSourceMap) {
    lessOptions.sourceMap = {
    outputSourceFiles: true,
    };
    }
    let data = source;
    let result;
    try {
    result = await less.render(data, lessOptions);
    } catch (error) {
    }
    const { css, imports } = result;
    //有sourceMap就进行处理
    let map =
    typeof result.map === 'string'
    ? JSON.parse(result.map) : result.map;

    callback(null, css, map);
    }
    export default lessLoader;

      可以看到官方的less-loader和我们写的简单的loader本质上都是调用less.render函数,对文件资源字符串进行处理,然后将处理好后的字符串和sourceMap通过callback返回。


    loader依赖


      在loader中,我们有时候也会使用到外部的资源文件,我们需要在loader对这些资源文件进行声明;这些声明信息主要用于使得缓存loader失效,以及在观察模式(watch mode)下重新编译。


      我们尝试写一个banner-loader,在每个js文件资源后面加上我们自定义的注释内容;如果传了filename,就从文件中获取预设好的banner内容,首先我们预设两个banner的txt:


    //loader/banner1.txt
    /* build from banner1 */

    //loader/banner2.txt
    /* build from banner2 */

      然后在我们的banner-loader中根据参数来进行判断:


    //loader/banner-loader
    const fs = require("fs");
    const path = require("path");
    const { getOptions } = require("loader-utils");

    module.exports = function (source) {
    const options = getOptions(this);
    if (options.filename) {
    let txt = "";
    if (options.filename == "banner1") {
    this.addDependency(path.resolve(__dirname, "./banner1.txt"));
    txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
    } else if (options.filename == "banner2") {
    this.addDependency(path.resolve(__dirname, "./banner1.txt"));
    txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
    }
    return source + txt;
    } else if (options.text) {
    return source + `/* ${options.text} */`;
    } else {
    return source;
    }
    };

      这里使用了this.addDependency的API将当前处理的文件添加到文件依赖中(并不是项目的package.json)。如果在观察模式下,依赖的text文件发生了变化,那么打包生成的文件内容也随之变化。



    如果不添加this.addDependency的话项目并不会报错,只是在观察模式下,如果依赖的文件发生了变化生成的bundle文件并不能及时更新。



    缓存加速


      在有些情况下,loader处理需要大量的计算非常耗性能(比如babel-loader),如果每次构建都重新执行相同的转换操作每次构建都会非常慢。


      因此webpack默认会将loader的处理结果标记为可缓存,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,它的输出结果必然是相同的;如果不想让webpack缓存该loader,可以禁用缓存:


    module.exports = function(source) {
    // 强制不缓存
    this.cacheable(false);
    return source;
    };

    手写loader所有代码均在webpackdemo19



    收起阅读 »

    深入webpack打包原理

    本文讨论的核心内容如下: webpack进行打包的基本原理 如何自己实现一个loader和plugin 注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11,node版本是v12.14.1,npm版本v6.13....
    继续阅读 »

    本文讨论的核心内容如下:



    1. webpack进行打包的基本原理

    2. 如何自己实现一个loaderplugin


    注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11node版本是v12.14.1npm版本v6.13.4(如果你喜欢yarn也是可以的),演示用的chrome浏览器版本81.0.4044.129(正式版本) (64 位)


    1. webpack打包基本原理


    webpack的一个核心功能就是把我们写的模块化的代码,打包之后,生成可以在浏览器中运行的代码,我们这里也是从简单开始,一步步探索webpack的打包原理


    1.1 一个简单的需求


    我们首先建立一个空的项目,使用npm init -y快速初始化一个package.json,然后安装webpack webpack-cli


    接下来,在根目录下创建src目录,src目录下创建index.jsadd.jsminus.js,根目录下创建index.html,其中index.html引入index.js,在index.js引入add.jsminus.js


    目录结构如下:



    文件内容如下:

    // add.js
    export default (a, b) => {
    return a + b
    }
    // minus.js
    export const minus = (a, b) => {
    return a - b
    }
    // index.js
    import add from './add.js'
    import { minus } from './minus.js'

    const sum = add(1, 2)
    const division = minus(2, 1)
    console.log('sum>>>>>', sum)
    console.log('division>>>>>', division)
    <!--index.html-->

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>demo</title>
    </head>
    <body>
    <script src="./src/index.js"></script>
    </body>
    </html>

    <<span class="hljs-attribute">span</span> class=<span class="hljs-string">"hljs-attribute"</span>>demo</span>

    这样直接在index.html引入index.js的代码,在浏览器中显然是不能运行的,你会看到这样的错误


    Uncaught SyntaxError: Cannot use import statement outside a module

    是的,我们不能在script引入的js文件里,使用es6模块化语法


    1.2 实现webpack打包核心功能


    我们首先在项目根目录下再建立一个bundle.js,这个文件用来对我们刚刚写的模块化js代码文件进行打包


    我们首先来看webpack官网对于其打包流程的描述:


    it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle)


    在正式开始之前,结合上面webpack官网说明进行分析,明确我们进行打包工作的基本流程如下:



    1. 首先,我们需要读到入口文件里的内容(也就是index.js的内容)

    2. 其次,分析入口文件,递归的去读取模块所依赖的文件内容,生成依赖图

    3. 最后,根据依赖图,生成浏览器能够运行的最终代码


    1. 处理单个模块(以入口为例)


    1.1 获取模块内容


    既然要读取文件内容,我们需要用到node.js的核心模块fs,我们首先来看读到的内容是什么:

    // bundle.js
    const fs = require('fs')
    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    console.log(body)
    }
    getModuleInfo('./src/index.js')


    1.2 分析模块内容


    我们安装@babel/parser,演示时安装的版本号为^7.9.6


    这个babel模块的作用,就是把我们js文件的代码内容,转换成js对象的形式,这种形式的js对象,称做抽象语法树(Abstract Syntax Tree, 以下简称AST)

    // bundle.js
    const fs = require('fs')
    const parser = require('@babel/parser')
    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
    // 表示我们要解析的是es6模块
    sourceType: 'module'
    })
    console.log(ast)
    console.log(ast.program.body)
    }
    getModuleInfo('./src/index.js')



    入口文件内容被放到一个数组中,总共有六个Node节点,我们可以看到,每个节点有一个type属性,其中前两个的type属性是ImportDeclaration,这对应了我们入口文件的两条import语句,并且,每一个type属性是ImportDeclaration的节点,其source.value属性是引入这个模块的相对路径,这样我们就得到了入口文件中对打包有用的重要信息了。


    接下来要对得到的ast做处理,返回一份结构化的数据,方便后续使用。


    1.3 对模块内容做处理


    ast.program.body部分数据的获取和处理,本质上就是对这个数组的遍历,在循环中做数据处理,这里同样引入一个babel的模块@babel/traverse来完成这项工作。


    安装@babel/traverse,演示时安装的版本号为^7.9.6

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

    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
    sourceType: 'module'
    })
    const deps = {}
    traverse(ast, {
    ImportDeclaration({ node }) {
    const dirname = path.dirname(file);
    const absPath = './' + path.join(dirname, node.source.value)
    deps[node.source.value] = absPath
    }
    })
    console.log(deps)
    }
    getModuleInfo('./src/index.js')

    创建一个对象deps,用来收集模块自身引入的依赖,使用traverse遍历ast,我们只需要对ImportDeclaration的节点做处理,注意我们做的处理实际上就是把相对路径转化为绝对路径,这里我使用的是Mac系统,如果是windows系统,注意斜杠的区别


    获取依赖之后,我们需要对ast做语法转换,把es6的语法转化为es5的语法,使用babel核心模块@babel/core以及@babel/preset-env完成


    安装@babel/core @babel/preset-env,演示时安装的版本号均为^7.9.6

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const babel = require('@babel/core')

    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(body, {
    sourceType: 'module'
    })
    const deps = {}
    traverse(ast, {
    ImportDeclaration({ node }) {
    const dirname = path.dirname(file);
    const absPath = './' + path.join(dirname, node.source.value)
    deps[node.source.value] = absPath
    }
    })
    const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
    })
    const moduleInfo = { file, deps, code }
    console.log(moduleInfo)
    return moduleInfo
    }
    getModuleInfo('./src/index.js')


    2. 递归的获取所有模块的信息

    这个过程,也就是获取依赖图(dependency graph)的过程,这个过程就是从入口模块开始,对每个模块以及模块的依赖模块都调用getModuleInfo方法就行分析,最终返回一个包含所有模块信息的对象

    const parseModules = file => {
    // 定义依赖图
    const depsGraph = {}
    // 首先获取入口的信息
    const entry = getModuleInfo(file)
    const temp = [entry]
    for (let i = 0; i < temp.length; i++) {
    const item = temp[i]
    const deps = item.deps
    if (deps) {
    // 遍历模块的依赖,递归获取模块信息
    for (const key in deps) {
    if (deps.hasOwnProperty(key)) {
    temp.push(getModuleInfo(deps[key]))
    }
    }
    }
    }
    temp.forEach(moduleInfo => {
    depsGraph[moduleInfo.file] = {
    deps: moduleInfo.deps,
    code: moduleInfo.code
    }
    })
    console.log(depsGraph)
    return depsGraph
    }
    parseModules('./src/index.js')


    3. 生成最终代码


    在我们实现之前,观察上一节最终得到的依赖图,可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理


    我们首先调用之前说的parseModules方法,获得整个应用的依赖图对象:


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    }

    接下来我们应该把依赖图对象中的内容,转换成能够执行的代码,以字符串形式输出。
    我们把整个代码放在自执行函数中,参数是依赖图对象


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }

    接下来内容其实很简单,就是我们取得入口文件的code信息,去执行它就好了,使用eval函数执行,初步写出代码如下:


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    (function(code){
    eval(code)
    })(graph[file].code)
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }

    上面的写法是有问题的,我们需要对file做绝对路径转化,否则graph[file].code是获取不到的,定义adsRequire方法做相对路径转化为绝对路径


    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    function absRequire(relPath){
    return require(graph[file].deps[relPath])
    }
    (function(require, exports, code){
    eval(code)
    })(absRequire, exports, graph[file].code)
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }

    接下来,我们只需要执行bundle方法,然后把生成的内容写入一个JavaScript文件即可


    const content = bundle('./src/index.js')
    // 写入到dist/bundle.js
    fs.mkdirSync('./dist')
    fs.writeFileSync('./dist/bundle.js', content)

    4. bundle.js的完整代码

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const babel = require('@babel/core')

    const getModuleInfo = file => {
    const body = fs.readFileSync(file, 'utf-8')
    console.log(body)
    const ast = parser.parse(body, {
    sourceType: 'module'
    })
    // console.log(ast.program.body)
    const deps = {}
    traverse(ast, {
    ImportDeclaration({ node }) {
    const dirname = path.dirname(file);
    const absPath = './' + path.join(dirname, node.source.value)
    deps[node.source.value] = absPath
    }
    })
    const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
    })
    const moduleInfo = { file, deps, code }
    return moduleInfo
    }

    const parseModules = file => {
    // 定义依赖图
    const depsGraph = {}
    // 首先获取入口的信息
    const entry = getModuleInfo(file)
    const temp = [entry]
    for (let i = 0; i < temp.length; i++) {
    const item = temp[i]
    const deps = item.deps
    if (deps) {
    // 遍历模块的依赖,递归获取模块信息
    for (const key in deps) {
    if (deps.hasOwnProperty(key)) {
    temp.push(getModuleInfo(deps[key]))
    }
    }
    }
    }
    temp.forEach(moduleInfo => {
    depsGraph[moduleInfo.file] = {
    deps: moduleInfo.deps,
    code: moduleInfo.code
    }
    })
    // console.log(depsGraph)
    return depsGraph
    }


    // 生成最终可以在浏览器运行的代码
    const bundle = file => {
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function(graph){
    function require(file) {
    var exports = {};
    function absRequire(relPath){
    return require(graph[file].deps[relPath])
    }
    (function(require, exports, code){
    eval(code)
    })(absRequire, exports, graph[file].code)
    return exports
    }
    require('${file}')
    })(${depsGraph})`
    }


    const build = file => {
    const content = bundle(file)
    // 写入到dist/bundle.js
    fs.mkdirSync('./dist')
    fs.writeFileSync('./dist/bundle.js', content)
    }

    build('./src/index.js')



    收起阅读 »

    iOS runtime之--动态修改字体大小

    那么既然runtime可以进行方法交换,我们只要自定义一个方法,替换系统的方法不就可以实现了吗?话不多说,我们开始动手UIFont设置font的类方法替换#import "UIFont+Category.h"#import "NSObject+Category...
    继续阅读 »

    介绍一下runtime的实际应用场景之一:怎样利用runtime的方法交换,在不修改原有代码的基础上动态的根据屏幕尺寸修改字体大小,包括xib和storyboard中拖的控件。

    我们知道,通常代码设置字体大小用的是UIFont的几个类方法 :

    systemFontOfSize

    fontWithName:size

    boldSystemFontOfSize

    italicSystemFontOfSize

    ...

    那么既然runtime可以进行方法交换,我们只要自定义一个方法,替换系统的方法不就可以实现了吗?话不多说,我们开始动手

    实现NSObject类方法交换

    创建NSObject分类,并增加一个可进行“Method交换”的方法。Method交换的本质,其实就是imp指针的交换。系统给我们提供了一个C语言的函数method_exchangeImplementations可以进行交换。流程如下:

    1.根据原方法和目标方法的selector,获取方法的method。如果是类方法用class_getClassMethod获取method,如是对象方法则用class_getInstanceMethod获取method

    2.获取到method后,调用method_exchangeImplementations函数进行两个method的imp指针的交换

    #import "NSObject+Category.h"
    #import

    @implementation NSObject (Category)

    /**
    @brief 方法替换
    @param originselector 替换的原方法
    @param swizzleSelector 替换后的方法
    @param isClassMethod 是否为类方法,YES为类方法,NO为对象方法
    */
    + (void)runtimeReplaceFunctionWithSelector:(SEL)originselector
    swizzleSelector:(SEL)swizzleSelector
    isClassMethod:(BOOL)isClassMethod
    {
    Method originMethod;
    Method swizzleMethod;
    if (isClassMethod == YES) {
    originMethod = class_getClassMethod([self class], originselector);
    swizzleMethod = class_getClassMethod([self class], swizzleSelector);
    }else{
    originMethod = class_getInstanceMethod([self class], originselector);
    swizzleMethod = class_getInstanceMethod([self class], swizzleSelector);
    }
    method_exchangeImplementations(originMethod, swizzleMethod);
    }
    @end

    UIFont设置font的类方法替换

    新建一个UIFont分类,在+(void)load方法中进行UIFont系统方法的替换
    #import "UIFont+Category.h"
    #import "NSObject+Category.h"

    @implementation UIFont (Category)

    //+(void)load方法会在main函数之前自动调用,不需要手动调用
    + (void)load
    {
    //交换systemFontOfSize: 方法
    [[self class] runtimeReplaceFunctionWithSelector:@selector(systemFontOfSize:) swizzleSelector:@selector(customSystemFontOfSize:) isClassMethod:YES];
    //交换fontWithName:size:方法
    [[self class] runtimeReplaceFunctionWithSelector:@selector(fontWithName:size:) swizzleSelector:@selector(customFontWithName:size:) isClassMethod:YES];
    }

    //自定义的交换方法
    + (UIFont *)customSystemFontOfSize:(CGFloat)fontSize
    {
    CGFloat size = [UIFont transSizeWithFontSize:fontSize];
    ///这里并不会引起递归,方法交换后,此时调用customSystemFontOfSize方法,其实是调用了原来的systemFontOfSize方法
    return [UIFont customSystemFontOfSize:size];
    }

    //自定义的交换方法
    + (UIFont *)customFontWithName:(NSString *)fontName size:(CGFloat)fontSize
    {
    CGFloat size = [UIFont transSizeWithFontSize:fontSize];
    return [UIFont customFontWithName:fontName size:size];
    }

    ///屏幕宽度大于320的,字体加10。(此处可根据不同的需求设置字体大小)
    + (CGFloat)transSizeWithFontSize:(CGFloat)fontSize
    {
    CGFloat size = fontSize;
    CGFloat width = [UIFont getWidth];
    if (width > 320) {
    size += 10;
    }
    return size;
    }

    ///获取竖屏状态下的屏幕宽度
    + (CGFloat)getWidth
    {
    for (UIScreen *windowsScenes in UIApplication.sharedApplication.connectedScenes) {
    UIWindowScene * scenes = (UIWindowScene *)windowsScenes;
    UIWindow *window = scenes.windows.firstObject;
    if (scenes.interfaceOrientation == UIInterfaceOrientationPortrait) {
    return window.frame.size.width;
    }
    return window.frame.size.height;
    }
    return 0;
    }

    @end

    至此就实现了,动态改变字体大小的目的,那xib和storyboard拖的控件怎么修改呢?我们接着看

    动态修改xib和storyboard控件的字体大小

    xib和sb拖拽的控件,都会调用 initWithCoder方法,那么我们可以自定义一个方法,替换掉initWithCoder,并在此方法中修改控件的字体不就可以了吗。我们先用UILabel举例,先创建一个UILabel的分类,然后在+(void)load方法中进行initWithCoder方法的交换

    #import "UILabel+Category.h"
    #import "NSObject+Category.h"

    @implementation UILabel (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    ///此时调用fontWithName:size:方法,实际上调用的是方法交换后的customFontWithName:size:
    self.font = [UIFont fontWithName:self.font.familyName size:self.font.pointSize];
    }
    return self;
    }

    @end

    此时我们就实现了,UILabel字体大小的动态修改,同理我们实现其它几个开发中常用的几个控件修改

    UIButton的分类

    #import "UIButton+Category.h"
    #import "NSObject+Category.h"

    @implementation UIButton (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    if (self.titleLabel != nil) {
    self.titleLabel.font = [UIFont fontWithName:self.titleLabel.font.familyName size:self.titleLabel.font.pointSize];
    }
    }
    return self;
    }

    @end

    UITextField的分类

    #import "UITextField+Category.h"
    #import "NSObject+Category.h"

    @implementation UITextField (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    self.font = [UIFont fontWithName:self.font.familyName size:self.font.pointSize];
    }
    return self;
    }

    @end

    UITextView的分类

    #import "UITextView+Category.h"
    #import "NSObject+Category.h"

    @implementation UITextView (Category)

    + (void)load
    {
    [[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
    }

    - (instancetype)customInitWithCoder:(NSCoder *)coder
    {
    if ([self customInitWithCoder:coder]) {
    self.font = [UIFont fontWithName:self.font.familyName size:self.font.pointSize];
    }
    return self;
    }

    @end

    到此,我们就完成了控件字体大小的动态修改,我们在storyboard中拖几个控件,然后用代码创建几个控件,分别看一下修改前和修改后的效果

                                                                           设置前


                                                                                        设置后

    注:Swift语言做法类似,但是Swift不允许重写+(void)load方法。所以如果是Swift,文中的+ (void)load需要改为自己定义的方法,并在AppDelegate的
    didFinishLaunchingWithOptions方法中进行调用。


    原贴链接:https://www.jianshu.com/p/c1f206433809
    收起阅读 »

    iOS runtime之--动态添加属性和方法

    一、runtime添加属性在Objective-C中,category分类默认只能添加方法,不能添加属性。根本原因在于声明了@property后,category并不会自动生成set和get方法。如果有需要在category中添加属性,可以利用runtime的...
    继续阅读 »

    一、runtime添加属性

    在Objective-C中,category分类默认只能添加方法,不能添加属性。根本原因在于声明了@property后,category并不会自动生成set和get方法。如果有需要在category中添加属性,可以利用runtime的特性实现。
    //新建一个NSObject的category类,并添加一个customString属性
    @interface NSObject (Category)
    @property(nonatomic,copy)NSString *customString;
    @end

    //在.m文件中实现set、get方法,此时添加属性代码便完成了,就是如此简单
    #import "NSObject+Category.h"
    #import <objc/message.h>
    - (void)setCustomString:(NSString *)customString {
    objc_setAssociatedObject(self, &customStringKey, customString, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }

    - (NSString *)customString {
    return objc_getAssociatedObject(self, &customStringKey);
    }

    //测试一下,如果打印出1111,就代表添加属性成a国
    - (void)viewDidLoad {
    [super viewDidLoad];
    ///动态添加属性
    NSObject *objct = [[NSObject alloc] init];
    objct.customString = @"1111";
    NSLog(@"%@",objct.customString);
    }

    动态添加属性,主要用到了两个runtime函数:
    1.添加属性


    此函数有四个参数。
    第一个参数指给哪个对象添加属性,第二个参数指属性的key指针,第三个参数指属性的名字,第四个参数指引用类型和原子性。
    其中着重讲一下第四个参数,此参数有五个值:
    OBJC_ASSOCIATION_ASSIGN 代表生成一个弱类型属性,相当于@property(atomic,assign)
    OBJC_ASSOCIATION_RETAIN_NONATOMIC相当于@property(nonatomic,strong)
    OBJC_ASSOCIATION_COPY_NONATOMIC,相当于@property(nonatomic,copy)
    OBJC_ASSOCIATION_RETAIN,相当于@property(atomic,strong)
    OBJC_ASSOCIATION_COPY,
    相当于@property(atomic,copy)
    上面代码生成的是string对象,所以我们一般用OBJC_ASSOCIATION_COPY_NONATOMIC

    2.获取属性值

    第二个函数是获取动态生成的属性,此函数有两个参数,第一个参数指的是哪个对象的属性,第二个参数为属性的key指针(每个动态添加的属性都需要有一个唯一的key)

    二、runtime动态添加方法

    为People类添加方法,如果运行后打印出“在唱歌”,说明添加方法成功
    ///例如我们有一个people类,people类中没有任何属性和方法,//我们为之添加一个名为sing的方法
    - (void)viewDidLoad {
    [super viewDidLoad];
    People *people = [[People alloc] init];
    //添加方法
    class_addMethod([People class], @selector(sing), class_getMethodImplementation([self class], @selector(peopleSing)), "v@:");
    //people调用刚添加的方法
    [people performSelector:@selector(sing)];
    }

    - (void)peopleSing
    {
    NSLog(@"在唱歌");
    }

    添加方法主要用到两个runtime函数

    1.添加方法函数


    此函数有四个参数
    第一个参数代表为哪个类添加方法
    第二个参数代表添加的方法名称
    第三个参数已经实现的方法的imp指针

    第四个参数"v@:":v:表示添加方法返回值为void @表示是id类型(也就是要添加的类) :表示添加的方法SEL 如下图


    2.获取方法imp指针


    此函数为获取imp指针,做为第一个函数的参数传入,这个函数有两个参数。
    第一个参数为方法实现所在的类。
    第二个参数为实现的方法的SEL

    原文地址:https://www.jianshu.com/p/795091958f8f
    收起阅读 »

    iOS- Dealloc流程解析 Dealloc 实现原理

    当对象的引用计数为0时, 系统会调用对象的dealloc方法释放- (void)dealloc { _objc_rootDealloc(self);}在内部void_objc_rootDealloc(id obj){ assert(obj); ...
    继续阅读 »

    当对象的引用计数为0时, 系统会调用对象的dealloc方法释放

    - (void)dealloc {
    _objc_rootDealloc(self);
    }

    在内部

    void
    _objc_rootDealloc(id obj)
    {
    assert(obj);
    obj->rootDealloc();
    }

    继续调用了rootDealloc方法

    显然调用顺序为:先调用当前类的dealloc,然后调用父类的dealloc,最后到了NSObject的dealloc.

    inline void
    objc_object::rootDealloc()
    {
    //判断对象是否采用了Tagged Pointer技术
    if (isTaggedPointer()) return; // fixme necessary?
    //判断是否能够进行快速释放
    //这里使用了isa指针里的属性来进行判断.
    if (fastpath(isa.nonpointer && //对象是否采用了优化的isa计数方式
    !isa.weakly_referenced && //对象没有被弱引用
    !isa.has_assoc && //对象没有关联对象
    !isa.has_cxx_dtor && //对象没有自定义的C++析构函数
    !isa.has_sidetable_rc //对象没有用到sideTable来做引用计数
    ))
    {
    //如果以上判断都符合条件,就会调用C函数 free 将对象释放
    assert(!sidetable_present());
    free(this);
    }
    else {
    //如果以上判断没有通过,做下一步处理
    object_dispose((id)this);
    }
    }

    内部做了一些判断, 如果满足这五个条件,直接调用free函数,进行内存释放.

    当一个最简单的类(没有任何成员变量,没有任何引用的类),这五个判断条件都是成立的,直接free.

    id 
    object_dispose(id obj)
    {
    if (!obj) return nil;

    objc_destructInstance(obj);
    free(obj);

    return nil;
    }

    调用objc_destructInstance函数来析构对象obj,再free(obj)释放内存.

    objc_destructInstance内部函数会销毁C++析构函数以及移除关联对象的操作.

    继续调用objc_object的clearDeallocating函数做下一步处理

    objc_object::clearDeallocating()
    {
    if (slowpath(!isa.nonpointer)) {
    // Slow path for raw pointer isa.
    // 如果要释放的对象没有采用了优化过的isa引用计数
    sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
    // Slow path for non-pointer isa with weak refs and/or side table data.
    // 如果要释放的对象采用了优化过的isa引用计数,并且有弱引用或者使用了sideTable的辅助引用计数
    clearDeallocating_slow();
    }
    assert(!sidetable_present());
    }

    根据是否采用了优化过的isa做引用计数分为两种:

    1. 要释放的对象没有采用优化过的isa引用计数:

    会调用sidetable_clearDeallocating() 函数做进一步处理

    void 
    objc_object::sidetable_clearDeallocating()
    {
    // 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    //在散列表SideTable中找到对应的引用计数表RefcountMap,拿到要释放的对象的引用计数
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
    //如果要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
    if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
    weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //从引用计数表中擦除该对象的引用计数
    table.refcnts.erase(it);
    }

    table.unlock();
    }
    1. 如果该对象采用了优化过的isa引用计数

    并且该对象有弱引用或者使用了sideTable的辅助引用计数,就会调用clearDeallocating_slow()函数做进一步处理.

    NEVER_INLINE void

    objc_object::clearDeallocating_slow()

    {
    assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

    // 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
    //要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
    weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //使用了sideTable的辅助引用计数,直接在SideTable中擦除该对象的引用计数
    if (isa.has_sidetable_rc) {
    table.refcnts.erase(this);
    }
    table.unlock();
    }

    以上两种情况都涉及weak_clear_no_lock函数, 它的作用就是将被弱引用对象的弱引用指针置为nil.

    void 

    weak_clear_no_lock(weak_table_t *weak_table, id referent_id)

    {
    //获取被弱引用对象的地址
    objc_object *referent = (objc_object *)referent_id;
    // 根据对象地址找到被弱引用对象referent在weak_table中对应的weak_entry_t
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
    /// XXX shouldn't happen, but does with mismatched CF/objc
    //printf("XXX no entry for clear deallocating %p\n", referent);
    return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;

    // 找出弱引用该对象的所有weak指针地址数组
    if (entry->out_of_line()) {
    referrers = entry->referrers;
    count = TABLE_SIZE(entry);
    }
    else {
    referrers = entry->inline_referrers;
    count = WEAK_INLINE_COUNT;
    }
    // 遍历取出每个weak指针的地址
    for (size_t i = 0; i < count; ++i) {
    objc_object **referrer = referrers[i];
    if (referrer) {
    // 如果weak指针确实弱引用了对象 referent,则将weak指针设置为nil
    if (*referrer == referent) {
    *referrer = nil;
    }
    // 如果所存储的weak指针没有弱引用对象 referent,这可能是由于runtime代码的逻辑错误引起的,报错
    else if (*referrer) {
    _objc_inform("__weak variable at %p holds %p instead of %p. "
    "This is probably incorrect use of "
    "objc_storeWeak() and objc_loadWeak(). "
    "Break on objc_weak_error to debug.\n",
    referrer, (void*)*referrer, (void*)referent);
    objc_weak_error();
    }
    }
    }
    weak_entry_remove(weak_table, entry);
    }

    这里也表明了为什么被weak修饰的对象在释放时, 所有弱引用该对象的指针都被设置为nil.
    dealloc整个方法释放流程如下图:


    看流程图发现,如果五个条件不满足.内存无法进行快速释放.在上面中,我看到博客里关于 objc_destructInstance 这个方法只是概述而过,所以我找了相关资料来了解一下.

    void *objc_destructInstance(id obj) 
    {
    if (obj) {
    Class isa_gen = _object_getClass(obj);
    class_t *isa = newcls(isa_gen);


    // Read all of the flags at once for performance.
    bool cxx = hasCxxStructors(isa);
    bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);


    // This order is important.
    if (cxx) object_cxxDestruct(obj);
    if (assoc) _object_remove_assocations(obj);

    if (!UseGC) objc_clear_deallocating(obj);
    }
    return obj;
    }

    总共干了三件事::

    1. 执行了object_cxxDestruct 函数
    2. 执行_object_remove_assocations,去除了关联对象.(这也是为什么category添加属性时,在释放时没有必要remove)
    3. 就是上面写的那个,清空引用计数表并清除弱引用表,将weak指针置为nil
      object_cxxDestruct是由编译器生成,这个方法原本是为了++对象析构,ARC借用了这个方法插入代码实现了自动内存释放的工作.

    这个释放.

    现象:

    1. 当类拥有实例变量时,这个方法会出现,且父类的实例变量不会导致子类拥有这个方法.
    2. 出现这个方法和变量是否被赋值,赋值成什么没有关系.

    所以, 我们可以认为这个方法就是用来释放该类中的属性的. weak修饰的属性应该不包含在内。



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

    iOS- weak 原理

    一、weak 基本用法weak 是弱引用,用 weak 来修饰、描述所引用对象的计数器并不会增加,而且 weak 会在引用对象被释放的时候自动置为 nil,这也就避免了野指针访问坏内存而引起奔溃的情况,另外 weak 也可以解决循环引用。拓展:为什么修饰代理使...
    继续阅读 »

    一、weak 基本用法

    weak 是弱引用,用 weak 来修饰、描述所引用对象的计数器并不会增加,而且 weak 会在引用对象被释放的时候自动置为 nil,这也就避免了野指针访问坏内存而引起奔溃的情况,另外 weak 也可以解决循环引用。

    拓展:为什么修饰代理使用 weak 而不是用 assign?
    assign 可用来修饰基本数据类型,也可修饰 OC 的对象,但如果用 assign 修饰对象类型指向的是一个强指针,当指向的这个指针释放之后,它仍指向这块内存,必须要手动给置为 nil,否则会产生野指针,如果还通过此指针操作那块内存,会导致 EXC_BAD_ACCESS 错误,调用了已经被释放的内存空间;而 weak 只能用来修饰 OC 对象,而且相比 assign 比较安全,如果指向的对象消失了,那么它会自动置为 nil,不会导致野指针

    二、weak 原理概括

    weak 表其实是一个哈希表,key 是所指对象的指针,value 是 weak 指针的地址数组。(value 是数组的原因是:因为一个对象可能被多个弱引用指针指向)

    Runtime 维护了一张 weak 表,用来存储某个对象的所有的 weak 指针。

    weak 原理实现过程三步骤

    初始化开始时,会调用 objc_initWeak 函数,初始化新的 weak 指针指向对象的地址。


    然后 objc_initWeak 函数里面会调用 objc_storeWeak() 函数,objc_storeWeak() 函数的作用是用来更新指针的指向,创建弱引用表。


    在最后会调用 clearDeallocating 函数。而clearDeallocating 函数首先根据对象的地址获取 weak 指针地址的数组,然后紧接着遍历这个数组,将其中的数组开始置为 nil,把这个 entry 从 weak 表中删除,最后一步清理对象的记录。

    初始化开始时,会调用 objc_initWeak 函数,初始化新的 weak 指针指向对象的地址。当我们初始化 weak 变量时,runtime 会调用 NSObject.mm 中的 objc_initWeak,而 objc_initWeak 函数里面的实现如下:
    id objc_initWeak(id *location, id newObj) {
    // 查看对象实例是否有效,无效对象直接导致指针释放
    if (!newObj) {
    *location = nil;
    return nil;
    }
    // 这里传递了三个 Bool 数值
    // 使用 template 进行常量参数传递是为了优化性能
    return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
    (location, (objc_object*)newObj);
    }

    通过上面代码可以看出,objc_initWeak()函数首先判断指针指向的类对象是否有效,无效,直接返回;否则通过 storeWeak() 被注册为一个指向 value 的 _weak 对象

    objc_initWeak 函数里面会调用 objc_storeWeak() 函数,objc_storeWeak() 函数的作用是用来更新指针的指向,创建弱引用表。

    在最后会调用 clearDeallocating 函数。而 clearDeallocating 函数首先根据对象的地址获取 weak 指针地址的数组,然后紧接着遍历这个数组,将其中的数组开始置为 nil,把这个 entry 从 weak 表中删除,最后一步清理对象的记录。

    问:当 weak 指向的对象被释放时,如何让 weak 指针置为 nil 的呢?
    答:
    调用 objc_release
    因为对象的引用计数为0,所以执行 dealloc
    在 dealloc 中,调用了 _objc_rootDealloc 函数
    在 _objc_rootDealloc 中,调用了 object_dispose 函数
    调用 objc_destructInstance
    最后调用 objc_clear_deallocating,详细过程如下:
    a. 从 weak 表中获取废弃对象的地址为键值的记录
    b. 将包含在记录中的所有附有 weak 修饰符变量的地址,赋值为 nil
    c. 将 weak 表中该记录删除
    d. 从引用计数表中删除废弃对象的地址为键值的记录



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

    iOS- Copy和Strong修饰

    情况一(@property (nonatomic,copy)NSString *str;)(@property (nonatomic,strong)NSString *str;)self. str = NSString(实例)@interface ViewCo...
    继续阅读 »

    情况一(@property (nonatomic,copy)NSString *str;)(@property (nonatomic,strong)NSString *str;)self. str = NSString(实例)

    @interface ViewController ()

    @property (nonatomic,copy)NSString *str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *base_str = @"我是";//实例化分配堆内存
    self.str = base_str;//copy对NSString只是指针拷贝(浅拷贝)

    NSLog(@"str--%p+++%@",self.str,self.str);//0x1006a4020+++我是
    NSLog(@"base_str--%p+++%@",base_str,base_str);//0x1006a4020+++我是

    NSLog(@"分割线---------------------------------------------");
    base_str = @"haha";//重新实例化重新分配堆内存(但是对原来的地址不影响)
    NSLog(@"str--%p+++%@",self.str,self.str);//0x1006a4020+++我是
    NSLog(@"base_str--%p+++%@",base_str,base_str);//0x1006a40a0+++haha

    }
    2021-03-22 16:22:42.509744+0800 IOS--多继承[36010:335669] str--0x1006a4020+++我是
    2021-03-22 16:22:42.509955+0800 IOS--多继承[36010:335669] base_str--0x1006a4020+++我是
    2021-03-22 16:22:42.510093+0800 IOS--多继承[36010:335669] 分割线---------------------------------------------
    2021-03-22 16:22:42.510221+0800 IOS--多继承[36010:335669] str--0x1006a4020+++我是
    2021-03-22 16:22:42.510330+0800 IOS--多继承[36010:335669] base_str--0x1006a40a0+++haha

    情况二(@property (nonatomic,copy)NSString *str;)self. str = NSMutableString(实例)

    @interface ViewController ()

    @property (nonatomic,copy)NSString *str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *m_str = [NSMutableString stringWithString:@"nihao"];
    self.str = m_str;//copy对NSMutableString生成了新的地址(深拷贝)

    NSLog(@"str--%p+++%@",self.str,self.str);//0xbe2d07ae3dfa791b+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000558870+++nihao

    NSLog(@"分割线---------------------------------------------");
    [m_str appendFormat:@"修改后"];
    NSLog(@"str--%p+++%@",self.str,self.str);//0xdb33f1772ec1e5d1+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000724db0+++nihao修改后

    }

    情况三(@property (nonatomic,strong)NSString *str;)self. str = NSMutableString(实例)

    @interface ViewController ()

    @property (nonatomic,strong)NSString *str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *m_str = [NSMutableString stringWithString:@"nihao"];
    self.str = m_str;//strong对NSMutableString没有生成了新的地址(浅拷贝)

    NSLog(@"str--%p+++%@",self.str,self.str);//0xbe2d07ae3dfa791b+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000558870+++nihao

    NSLog(@"分割线---------------------------------------------");
    [m_str appendFormat:@"修改后"];
    NSLog(@"str--%p+++%@",self.str,self.str);//0xdb33f1772ec1e5d1+++nihao
    NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000724db0+++nihao修改后

    }
    2021-03-22 16:39:20.728281+0800 IOS--多继承[36287:351536] str--0x60000235e3d0+++nihao
    2021-03-22 16:39:20.728446+0800 IOS--多继承[36287:351536] m_str--0x60000235e3d0+++nihao
    2021-03-22 16:39:20.728574+0800 IOS--多继承[36287:351536] 分割线---------------------------------------------
    2021-03-22 16:39:20.728697+0800 IOS--多继承[36287:351536] str--0x60000235e3d0+++nihao修改后
    2021-03-22 16:39:20.728811+0800 IOS--多继承[36287:351536] m_str--0x60000235e3d0+++nihao修改后

    情况四(@property (nonatomic,strong)NSMutableString *m_str;)self.m_str = NSString

    @interface ViewController ()

    @property (nonatomic,strong)NSMutableString *m_str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *str = @"nihao";
    self.m_str = str;//strong对str只是引用计数+1(此时self.m_str还是不可变NSString)

    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    NSLog(@"分割线---------------------------------------------");
    str = @"修改后";
    [self.m_str appendFormat:@"修改"];//(编译能通过,运行时候Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendFormat:'*** First throw call stack:)
    //因为appendFormat是NSMutableString的方法
    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    }

    @interface ViewController ()

    @property (nonatomic,copy)NSMutableString *m_str;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *str = [NSMutableString stringWithString:@"nihao"];
    self.m_str = str;//strong对str只是引用计数+1(此时self.m_str还是不可变NSString)

    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    NSLog(@"分割线---------------------------------------------");
    [self.m_str appendFormat:@"修改"];//(编译能通过,运行时候Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendFormat:'*** First throw call stack:)
    //因为appendFormat是NSMutableString的方法
    NSLog(@"str--%p+++%@",str,str);
    NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);

    }


    当使用 strong 修饰属性的时候,属性的setter方法会直接强引用该对象,这样,当原object对象的值发生改变时,新对象的属性也改变;

    但是对于可变对象类型,如NSMutableString、NSMutableArray等则不可以使用copy修饰,因为Foundation框架提供的这些类都实现了NSCopying协议,使用copy方法返回的都是不可变对象,如果使用copy修饰符在对可变对象赋值时则会获取一个不可变对象,接下来如果对这个对象进行可变对象的操作则会产生异常,因为OC没有提供mutableCopy修饰符,对于可变对象使用strong修饰符即可。

    总结:

    针对不可变对象使用copy修饰,针对可变对象使用strong修饰。

    收起阅读 »

    Git 操作整理

    git 使用一般企业中使用代码管理工具Git开发时都是通过拉分支进行功能细致开发,所以掌握git的分支操作时必要的。使用git下载指定分支命令为: git clone-b分支名仓库地址。初始开发git操作流程:本地创建公钥:ssh-keygen-t ...
    继续阅读 »

    git 使用

    一般企业中使用代码管理工具Git开发时都是通过拉分支进行功能细致开发,所以掌握git的分支操作时必要的。
    使用git下载指定分支命令为: git clone-b分支名仓库地址
    初始开发git操作流程:

    本地创建公钥:ssh-keygen-t rsa-C"邮箱"并配置

    克隆最新主分支项目代码: git clone地址

    创建本地分支: git branch分支名

    查看本地分支:git branch

    查看远程分支: git branch-a

    切换分支: git checkout分支名(一般修改未提交则无法切换,大小写问题经常会有,可强制切换 git checkout分支名-f非必须慎用)

    将本地分支推送到远程分支:git push<远程仓库><本地分支>:<远程分支>

    必备知识点


    概念:
    1. Remote:远程主仓库

    2. Repository:本地仓库

    3. Index:Git追踪树,暂存区

    4. workspace:本地工作区(即你编辑器的代码)

    一般操作流程:《工作区》-> git status查看状态 -> git add.将所有修改加入暂存区-> git commit-m"提交描述"将代码提交到本地仓库-> git push将本地仓库代码更新到远程仓库。

    一、git remote

    为远程仓库指定别名,以便于管理远程主机,默认只有一个时为origin。

    1、查看主机名: git remote

    2、查看主机名即网址: git remote-v

    默认克隆远程仓库到本地时,远程主机为origin,如需指定别名可使用 git clone-o<别名><远程git地址>

    3、查看主机的详细信息: git remote show<主机名>

    4、添加远程主机: git remote add<主机名><网址>

    5、删除远程主机: git remote rm<主机名>

    6、修改远程主机的别名: git remote rename<原主机名><新主机名>

    二、git fetch

    将某个远程主机的更新,全部/分支 取回本地(此时之更新了Repository)它取回的代码对你本地的开发代码没有影响,如需彻底更新需合并或使用 git pull

    1. 远程主机的更新,全部取回本地: git fetch<远程主机名>

    2. 将远程仓库特定分支更新到本地: git fetch<远程主机名><分支名>

    如果需要将更新拉取但本地工作代码需要合并到本地某一分支: git merge<被合并的远程分支>,或者在此基础上创建出新分支并切换: git checkout-b<分支名><在此分支上创建>

    三、git pull

    拉取远程主机某分支的更新,再与本地的指定分支合并(相当与fetch加上了合并分支功能的操作)。

    1. 拉取远程某分支并与本地某一分支合并(没有则默认会创建): git pull<远程主机名><远程分支名>:<本地分支名>

    2. 如果远程分支是与当前所在分支合并,则冒号后面的部分可以省略: git pull<远程主机名><远程分支名>

    3. 如果当前分支与远程分支存在追踪关系,则可以省略远程分支名: git pull<远程主机名>

    4. 如果当前分支只有一个追踪分支,则远程主机名都可以省略: git pull

    四、git push

    将本地分支的更新,推送到远程主机,其命令格式与 git pull相似。

    1、将本地分支推送到远程分支: git push<远程主机名><本地分支名>:<远程分支名>

    2、如果省略远程分支名,则默认为将本地分支推送到与之关联的远程分支:(一般设置本地分支和与之关联的远程分支同名,防止混淆) git push<远程主机名><本地分支名>

    如果对应的远程分支不存在,则会被创建(m默认与本地分支同名)。

    3、如果省略本地分支名,则表示删除指定的远程分支,这等同于推送一个空的本地分支到对应远程分支: git push origin:<远程分支> 等同于 git push origin--delete<远程分支>

    4、如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略 git push origin

    5、如果当前分支只有一个追踪分支,那么主机名也可以省略: git push

    6、如果当前分支与多个主机存在追踪关系(使用场景相对来说较少),可以使用 -u 指定默认推送主机: git push-u origin<主机名>,设置时候需推送便可以直接使用 git push

    7、将本地的所有分支都推送到远程主机: git push--all origin

    8、如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做 git pull合并差异,然后再推送到远程主机。如果一定要推送,可以使用 --force选项(谨慎使用,除非你非常确认): git push--force origin

    注意:分支推送顺序的格式为<来源地>:<目的地>,所以 git pull格式:<远程分支>:<本地分支>, git push格式为:<本地分支>:<远程分支>

    五、分支操作

    1、创建本地分支: git branch test:(创建名为test的本地分支)。

    2、切换分支: git checkout test:(切换到test分支)。

    3、创建并切换分支 git branch-b test:(相当于以上两条命令的合并)。

    4、查看本地分支: git branch

    5、查看远程仓库所有分支: git branch-a

    6、删除本地分支: git branch-d test:(删除本地test分支)。

    7、分支合并: git merge master:(将master分支合并到当前分支)。

    8、本地分支重命名: git branch-m oldName newName

    9、远程分支重命名:

    1. 重命名远程分支对应的本地分支: git branch-m oldName newName;

    2. 删除远程分支: git push--deleteorigin oldName;

    3. 上传新命名的本地分支: git push origin newName;

    4. 把修改后的本地分支与远程分支关联: git branch--set-upstream-to origin/newName

    10、分支关联:

    查看当前的本地分支与远程分支的关联关系: git branch-vv


    把当前本地分支与远程origin的某分支进行关联处理(通过 --set-upstream-to 命令): git branch--set-upstream-to=origin/feature/clear-server-eslint-error_180713
    11、分支差异查看:

    查看本地当前分支与远程某一分支的差异: git diff origin/feature/reserve-3.4

    查看本地特定分支与远程分支的差异: git diff master origin/feature/reserve-3.4(查看本地master分支与远程feature/reserve-3.4分支的差异),如图:


    六、修改撤销

    1、 git checkout--<文件名>:丢弃工作区的修改,就是让这个文件回到最近一次 git commit或 git add时的状态。

    2、 git reset HEAD<文件名>:把暂存区的修改撤销掉(unstage),重新放回工作区。

    3、 git reset--hard commit_id:git版本回退,回退到特定的commit_id版本。

    流程: git log查看提交历史,以便确定要回退到哪个版本(commit 之后的即为ID)。


    4、 git reset--hard commit_id:回退到commit_id版本。

    5、 git reflog查看命令历史,以便确定要回到未来的哪个版本。更新远程代码到本地:

    • git fetch origin master(分支)

    • git pull// 将fetch下来的代码pull到本地

    • git diff master origin/master// 查看本地分支代码和远程仓库的差异

    6、拉取远程分支并创建本地分支:

    1. git checkout-b本地分支名origin/远程分支名:使用此方式会在本地新建分支,并自动切换到该本地分支;

    2. git fetch origin远程分支名:本地分支名:使用此方式会在本地新建分支,但是不会自动切换到该本地分支,需要手动checkout。

    七、配置

    1、 git config-l// 陈列出所有的git配置项。

    2、 git config core.ignorecasefalse //配置git不忽略大小写(默认忽略)参照(git 大小写)。


    原贴链接:https://www.jianshu.com/p/80252c51a70f

    收起阅读 »

    iOS------OpenGL 图形专有名词与坐标解析

    一.OpenGL简介OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操...
    继续阅读 »

    一.OpenGL简介

    OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D3D矢量图形的跨语言跨平台应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操作抽象为⼀个个的OpenGL指令,开发者可以在mac程序中使用OpenGl来实现图形渲染。图形API的目的就是实现图形的底层渲染,比如游戏场景/游戏人物的渲染,音视频解码后数据的渲染,地图上的渲染,动画绘制等。在iOS开发中,开发者唯一能够GPU的就是图形API。(GPU---图形处理器(英语:Graphics Processing Unit,缩写:GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑工作站、游戏机和一些移动设备(如平板电脑智能手机等)上做图像和图形相关运算工作的微处理器。)

    二.OpenGL专业名词解析

        1.OpenGL 上下⽂( context )

            OpenGL Context,中文解释就是OpenGL的上下文,因为OpenGL没有窗口的支持,我们在使用OpenGl的时候,一般是在main函数创建窗口: 

            //GLUT窗口大小、窗口标题

            glutInitWindowSize(800, 600);

            glutCreateWindow("Triangle");

            然后我们在创建的窗口里面绘制,个人理解上下文的意思就是指的是OpenGL的作用范围,当然OpenGL的Context不只是这个窗口,这个窗口我们可以理解为OpenGL的default framebuffer,所以Context还包含关于这个framebuffer的一些参数设置信息,具体内容可以查看OpenGL的Context的结构体,Context记录了OpenGL渲染需要的所有信息,它是一个大的结构体,它里面记录了当前绘制使用的颜色、是否有光照计算以及开启的光源等非常多我们使用OpenGL函数调用设置的状态和状态属性等等,你可以把它理解为是一个巨大的状态机,它里面保存OpenGl的指令,在图形渲染的时候,可以理解为这个状态机开始工作了,对某个属性或者开关发出指令。它的特点就是:有记忆功能,接收输入,根据输入的指令,修改当前的状态,并且可以输出内容,当停机的时候不再接收指令。

    2.渲染

            渲染就是把数据显示到屏幕上,在OpenGl中,渲染指的是将图形/图像数据转换为2D空间图像操作叫渲染

    3.顶点数组/顶点缓冲区

            在OpenGL中,基本图元有三种:点,线,三角形,复杂的图形由这三种图元组成,我们在画点/线/三角形的时候是不是应该先知道每个顶点的坐标,而这些坐标放在数组里,就叫顶点数组。顶点数组存在内存当中,但是为了提高性能,提前分配一块显存,将顶点数组预先存入到显存当中,这部分的显存就叫顶点缓冲区。

    4.着色器(shader)

            为什么要使用着色器?我们知道,OpenGL一般使用经典的固定渲染管线来渲染对象,OpenGL在实际调⽤绘制函数之前,还需指定⼀个由shader编译成的着⾊器程序。常⻅的着⾊器主要有顶点着⾊器(VertexShader),⽚段着⾊器(FragmentShader)/像素着⾊器(PixelShader),⼏何着⾊器(GeometryShader),曲⾯细分着⾊器(TessellationShader)。⽚段着⾊器和像素着⾊器只是在OpenGL和DX中的不同叫法⽽已。可惜的是,直到OpenGLES 3.0,依然只⽀持了顶点着⾊器和⽚段着⾊器这两个最基础的着⾊器。OpenGL在处理shader时,和其他编译器⼀样。通过编译、链接等步骤,⽣成了着⾊器程序(glProgram),着⾊器程序同时包含了顶点着⾊器和⽚段着⾊器的运算逻辑。在OpenGL进⾏绘制的时候,⾸先由顶点着⾊器对传⼊的顶点数据进⾏运算。再通过图元装配,将顶点转换为图元。然后进⾏光栅化,将图元这种⽮量图形,转换为栅格化数据。最后,将栅格化数据传⼊⽚段着⾊器中进⾏运算。⽚段着⾊器会对栅格化数据中的每⼀个像素进⾏运算,并决定像素的颜⾊(顶点着色器和片段/片元着色器会在下面讲解)

    5.管线

            OpenGL在渲染图形/图像的时候是按照特定的顺序来执行的,不能修改打破,管线的意思个人理解是读取顶点数据—>顶点着色器—>组装图元—>光栅化图元—>片元着色器—>写入帧缓冲区—>显示到屏幕上,类似这样的流水线,当图像/图形显示到屏幕上,这一条管线完成工作。下面是分步讲解:

           (1)读取顶点数据指的是将待绘制的图形的顶点数据传递给渲染管线中。

            (2)顶点着色器最终生成每个定点的最终位置,执行顶点的各种变换,它会针对每个顶点执行一次,确定了最终位置后,OpenGL就可以把这些顶点集合按照给定的参数类型组装成点,线或者三角形。


         (3)组装图元阶段包括两部分:图元的组装和图元处理,图元组装指的是顶点数据根据设置的绘制方式参数结合成完整的图元,例如点绘制方式中每个图元就只包含一个点,线段绘制方式中每个图源包含两个点;图元处理主要是剪裁以使得图元位于视景体内部的部分传递到下一个步骤,视景体外部的部分进行剪裁。视景体的概念与投影有关。

          (4)光栅化图元主要指的是将一个图元离散化成可显示的二维单元片段,这些小单元称为片元。一个片元对应了屏幕上的一个或多个像素,片元包括了位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。

          (5)片元着色器为每个片元生成最终的颜色,针对每个片元都会执行一次。一旦每个片元的颜色确定了,OpenGL就会把它们写入到帧缓冲区中。


    6.顶点着色器

             • ⼀般⽤来处理图形每个顶点变换(旋转/平移/投影等)                     

            • 顶点着⾊器是OpenGL中⽤于计算顶点属性的程序。顶点着⾊器是逐顶点运算的程序,也就是说每个顶点数据都会执⾏⼀次顶点着⾊器,当然这是并⾏的,并且顶点着⾊器运算过程中⽆法访问其他顶点的数据

            • ⼀般来说典型的需要计算的顶点属性主要包括顶点坐标变换、逐顶点光照运算等等。顶点坐标由⾃身坐标系转换到归⼀化坐标系的运算,就是在这⾥发⽣的。

    7.片元着色器(片段着色器)

            ⼀般⽤来处理图形中每个像素点颜⾊计算和填充⽚段着⾊器是OpenGL中⽤于计算⽚段(像素)颜⾊的程序。⽚段着⾊器是逐像素运算的程序,也就是说每个像素都会执⾏⼀次⽚段着⾊器,当然也是并⾏的

    8.光栅化Rasterization 

            • 是把顶点数据转换为⽚元的过程,具有将图转化为⼀个个栅格组成的图象的作⽤,特点是每个元素对应帧缓冲区中的⼀像素。

            • 光栅化就是把顶点数据转换为⽚元的过程。⽚元中的每⼀个元素对应于帧缓冲区中的⼀个像素。

            • 光栅化其实是⼀种将⼏何图元变为⼆维图像的过程。该过程包含了两部分的⼯作。第⼀部分⼯作:决定窗⼝坐标中的哪些整型栅格区域被基本图元占⽤;第⼆部分⼯作:分配⼀个颜⾊值和⼀个深度值到各个区域。光栅化过程产⽣的是⽚元

            • 把物体的数学描述以及与物体相关的颜⾊信息转换为屏幕上⽤于对应位置的像素及⽤于填充像素的颜⾊,这个过程称为光栅化,这是⼀个将模拟信号转化为离散信号的过程

    9.纹理

            纹理可以理解为图⽚. ⼤家在渲染图形时需要在其编码填充图⽚,为了使得场景更加逼真.⽽这⾥使⽤的图⽚,就是常说的纹理.但是在OpenGL,我们更加习惯叫纹理,⽽不是图⽚

    10.混合(Blending)

            在测试阶段之后,如果像素依然没有被剔除,那么像素的颜⾊将会和帧缓冲区中颜⾊附着上的颜⾊进⾏混合,混合的算法可以通过OpenGL的函数进⾏指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,⼀般可以通过像素着⾊器进⾏实现,当然性能会⽐原⽣的混合算法差⼀些,个人理解有点像iOS给RGB中红,绿,蓝设置不同的值得到不同的颜色,只是这里是操作片元着色器,来达到不同的显示。

    11.变换矩阵(Transformation)/投影矩阵Projection 

            在iOS核心动画中我们也会和矩阵打交到,变换矩阵顾名思义就是对图像/图形的放大/缩小/平移/选装等座处理。

            投影矩阵就是⽤于将3D坐标转换为⼆维屏幕坐标,实际线条也将在⼆维坐标下进⾏绘制    

    12.渲染上屏/交换缓冲区(SwapBuffer)     

        • 渲染缓冲区⼀般映射的是系统的资源⽐如窗⼝。如果将图像直接渲染到窗⼝对应的渲染缓冲区,则可以将图像显示到屏幕上。

        • 但是,值得注意的是,如果每个窗⼝只有⼀个缓冲区,那么在绘制过程中屏幕进⾏了刷新,窗⼝可能显示出不完整的图像

        • 为了解决这个问题,常规的OpenGL程序⾄少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在⼀个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。

        • 由于显示器的刷新⼀般是逐⾏进⾏的,因此为了防⽌交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换⼀般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进⾏交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步

        • 使⽤了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进⾏下⼀帧的渲染,使得帧率⽆法完全达到硬件允许的最⾼⽔平。为了解决这个问题,引⼊了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,⽽垂直同步发⽣时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利⽤硬件性能的⽬的

    13.坐标系

          OpenGl常见的坐标系有:

            1. Object or model coordinates(物体或模型坐标系)每一个实物都有自己的坐标系,在高中数学中,以自身建立的坐标系,自身坐标系由世界坐标系平移而来

            2. World coordinates(世界坐标系)个人理解为地球相对自己建立的坐标系,地球上所有生物都处于这个坐标系当中

            3. Eye (or Camera) coordinates(眼(或相机)坐标系)

            4. Normalized device coordinates(标准化的设备坐标系)

            5. Window (or screen) coordinates(.窗口(或屏幕)坐标系)个人理解为iOS下

            6.Clip coordinates(裁剪坐标系)主要作用是当图形/图像超出时,按照这个坐标系裁剪,裁剪好之后转换到screen坐标系

    14.正投影/透视投影

            正投影:类似于照镜子,1:1形成图形大小,这里不做重点讲解

            透视投影:在OpenGL中,如果想对模型进行操作,就要对这个模型的状态(当前的矩阵)乘上这个操作对应的一个矩阵.如果乘以变换矩阵(平移, 缩放, 旋转), 那相乘之后, 模型的位置被变换;如果乘以投影矩阵(将3D物体投影到2D平面), 相乘后, 模型的投影方式被设置;如果乘以纹理矩阵(), 模型的纹理方式被设置.而用来指定乘以什么类型的矩阵, 就是glMatriMode(GLenummode);glMatrixMode有3种模式: GL_PROJECTION 投影, GL_MODELVIEW 模型视图, GL_TEXTURE 纹理.所以,在操作投影矩阵以前,需要调用函数:glMatrixMode(GL_PROJECTION); //将当前矩阵指定为投影矩阵然后把矩阵设为单位矩阵




    作者:枫紫_6174
    链接:https://www.jianshu.com/p/03d3a5ab2db0






    收起阅读 »

    iOS Category---为什么只能加方法不能加属性

    一.面试题            相信大家在面试的时候经常会被问到Category的实现原理,以及Category为什么只能加方法不能加属性?个人理解这个问题本...
    继续阅读 »

    一.面试题

                相信大家在面试的时候经常会被问到Category的实现原理,以及Category为什么只能加方法不能加属性?个人理解这个问题本身问的就有问题,首先我们看分类的底层代码

                            struct category_t {

                                        const char *name;

                                        classref_t cls;

                                        struct method_list_t *instanceMethods; // 对象方法

                                        struct method_list_t *classMethods; // 类方法

                                        struct protocol_list_t *protocols; // 协议

                                        struct property_list_t *instanceProperties; // 属性

                                        // Fields below this point are not always present on disk.

                                        struct property_list_t *_classProperties;

                                        method_list_t*methodsForMeta(boolisMeta) {

                                    if(isMeta)returnclassMethods;

                                                elsereturninstanceMethods;

                                }

                            property_list_t*propertiesForMeta(boolisMeta,structheader_info *hi);

                        };

            通过上面的分类底层代码我们可以找到category_t 结构体,它里面包含了对象方法,类方法,协议,属性,既然分类的底层代码里面已经包含了属性,为什么我们面试的时候会被问到分类为什么不能添加属性?下面我们来揭开它的神秘面纱

            1.首先我们创建一个person类,再给penson类创建一个分类Person+TCText,在分类的.h文件




    这么写工程是不会报任何错误,给我们一种表面上其实是可以添加属性的,写上一个属性,系统会自动帮我们生成setter和getter方法,在分类里面写属性或者成员变量,系统只会帮我们做的一件事情就是它只会声明我们的setter和getter方法,不会帮我们实现,上面的这个属性等同于我们在分类里面写




    无论上面何种写法,我们在ViewController都能访问TCName这个属性


    但是,当我们在分类里面重写settet或者getter的时候,它就会出现:



    为什么什么会报错了?因为分类的属性,系统不会自动帮我们生成_TCName(相关属性赋值可以看苹果官网API,这里不做解释),在这里我们如果写:




    表面看上去不会报错,但是当外部访问TCName的时候,就会发现:



    程序一旦运行起来,就会因为递归,程序闪退。

    总结:不是说分类不能添加属性,是因为分类可以添加属性,但是由于系统不会自动帮分类的属性实现getter和setter方法,也不会帮其生成_TCName,无论你重写settet或者getter还是,你不能通过self.TCName去访问属性,重写了setter,这么访问就会发生递归,直接导致程序闪退。所以下次你该知道怎么回答面试官的问题了吧!!!



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/49d131c61348








    收起阅读 »

    iOS Metal语言规范深入

    一.函数修饰符Metal 有以下3种函数修饰符:        1)kernel , 表示该函数是⼀个数据并⾏计算着⾊函数. 它可以被分配在⼀维/⼆维/三维线程组中去执⾏      &nbs...
    继续阅读 »

    一.函数修饰符

    Metal 有以下3种函数修饰符:

            1)kernel , 表示该函数是⼀个数据并⾏计算着⾊函数. 它可以被分配在⼀维/⼆维/三维线程组中去执⾏

            2)vertex , 表示该函数是⼀个顶点着⾊函数 , 它将为顶点数据流中的每个顶点数据执⾏⼀次然后为每个顶 点⽣成数据输出到绘制管线

            3)fragment , 表示该函数是⼀个⽚元着⾊函数, 它将为⽚元数据流中的每个⽚元 和其关联执⾏⼀次然后 将每个⽚元⽣成的颜⾊数据输出到绘制管线中; 

    eg:

    //1.并行计算函数(kernel)

    kernelvoidCCTestKernelFunctionA(inta,intb)

    {

        /*

         注意:

         1. 使用kernel 修饰的函数返回值必须是void 类型

         2. 一个被函数修饰符修饰过的函数,不允许在调用其他的被函数修饰过的函数. 非法

         3. 被函数修饰符修饰过的函数,只允许在客户端对齐进行操作. 不允许被普通的函数调用.

         */

        //不可以的!

        //一个被函数修饰符修饰过的函数,不允许在调用其他的被函数修饰过的函数. 非法

        CCTestKernelFunctionB(1,2);//非法

        CCTestVertexFunctionB(1,2);//非法

        //可以! 你可以调用普通函数.而且在Metal 不仅仅只有这3种被修饰过的函数.普通函数也可以存在

        CCTest();

    }

    kernelvoidCCTestKernelFunctionB(inta,intb)

    {

    }

    //顶点函数

    vertexintCCTestVertexFunctionB(inta,intb){

    }

    //片元函数

    fragmentintCCTestVertexFunctionB(inta,intb){

    }

    //普通函数

    voidCCTest()

    {

    }

            说明:使⽤kernel 修饰的函数. 其返回值类型必须是void 类型; 只有图形着⾊函数才可以被 vertex 和 fragment 修饰. 对于图形着⾊函数, 返回值类型可以辨认出它是为 顶点做计算还是为每像素做计算. 图形着⾊函数的返回值可以为 void , 但是这也就意味着该函数不产⽣数 据输出到绘制管线; 这是⼀个⽆意义的动作; ⼀个被函数修饰符修饰的函数不能在调⽤其他也被函数修饰符修饰的函数; 这样会导致编译失败;

    二.⽤于变量或者参数的地址空间修饰符

            Metal 着⾊器语⾔使⽤ 地址空间修饰符 来表示⼀个函数变量或者参数变量 被分配于那⼀⽚内存区域. 所有的着⾊函数(vertex, fragment, kernel)的参数,如果是指针或是引⽤, 都必须带有地址空间修饰符号; 

            1) device: 设备地址空间

            Device Address Space(设备地址空间) ,在设备地址空间(Device) 指向设备内存池分配出来的缓存对象, 它是可读也是可写的; ⼀个缓存对象可 以被声明成⼀个标量,向量或是⽤户⾃定义结构体的指针或是引⽤. 

    eg:

            // 设备地址空间: device 用来修饰指针.引用

            //1.修饰指针变量

            device float4 *color;

            structCCStruct{

                floata[3];

                intb[2];

            };

            //2.修饰结构体类的指针变量

            device  CCStruct*my_CS;

            注意: 纹理对象总是在设备地址空间分配内存, device 地址空间修饰符不必出现在纹理类型定义中. ⼀个纹 理对象的内容⽆法直接访问. Metal 提供读写纹理的内建函数; 

            2)threadgroup: 线程组地址空间

            线程组地址空间⽤于为 并⾏计算着⾊函数分配内存变量. 这些变量被⼀个线程组的所有线程共享. 在线 程组地址空间分配的变量不能被⽤于图形绘制着⾊函数[顶点着⾊函数, ⽚元着⾊函数] 在并⾏计算着⾊函数中, 在线程组地址空间分配的变量为⼀个线程组使⽤, 声明周期和线程组相同; 

    eg:

            /*

             1. threadgroup 被并行计算计算分配内存变量, 这些变量被一个线程组的所有线程共享. 在线程组分配变量不能被用于图像绘制.

             2. thread 指向每个线程准备的地址空间. 在其他线程是不可见切不可用的

             */

            kernel void CCTestFouncitionF(threadgroup float *a)

            {

                //在线程组地址空间分配一个浮点类型变量x

                threadgroup float x;

                //在线程组地址空间分配一个10个浮点类型数的数组y;

                threadgroup float y[10];

            }

            constant float sampler[] = {1.0f,2.0f,3.0f,4.0f};

            kernel  void  CCTestFouncitionG(void)

            {

                //在线程空间分配空间给x,p

                float  x;

                thread  float  p = &x;

    }

           3) constant 常量地址空间

            常量地址空间指向的缓存对象也是从设备内存池分配存储, 但是它是只读的; 在程序域的变量必须定义在常量地址空间并且声明的时候初始化; ⽤来初始化的值必须是编译时的常 量. 在程序域的变量的⽣命周期和程序⼀样, 在程序中的并⾏计算着⾊函数或者图形绘制着⾊函数调⽤, 但 是constant 的值会保持不变; 

            注意: 常量地址空间的指针或是引⽤可以作为函数的参数. 向声明为常量的变量赋值会产⽣编译错误. 声明常量但是没有赋予初值也会产⽣编译错误; 

    eg:

            1 constant float samples[] = { 1.0f, 2.0f, 3.0f, 4.0f }; 

            2 //对⼀个常量地址空间的变量进⾏修改也会失败,因为它只读的 

            3 sampler[4] = {3,3,3,3}; //编译失败; 

            4 //定义为常量地址空间声明时不赋初值也会编译失败 

            5 constant float a; 

            4)thread 线程地址空间

            thread 地址空间指向每个线程准备的地址空间, 这个线程的地址空间定义的变量在其他线程不可⻅, 在 图形绘制着⾊函数或者并⾏计算着⾊函数中声明的变量thread 地址空间分配; 在图形绘制着色函数 或者 并行计算着色函数中声明的变量,在线程地址空间分配存储

    eg:

            kernel void CCTestFouncitionG(void){

            //在线程空间分配空间给x,p

            float x; 

            thread   float   p=&x;

            }

            对于图形着⾊器函数, 其指针或是引⽤类型的参数必须定义为 device 或是 constant 地址空间; 对于并⾏计算着⾊函数, 其指针或是引⽤类型的参数必须定义为 device 或是 threadgrounp 或是 constant 地址空间; 并不是所有的变量都需要修饰符,也可以定义普通变量(即无修饰符的变量)

    三.函数参数与变量

            图形绘制或者并⾏计算着⾊器函数的输⼊输出都是通过参数传递. 除了常量地址空间变量和程序域定义 的采样器以外.

            device buffer- 设备缓存, ⼀个指向设备地址空间的任意数据类型的指针或者引⽤; 

            constant buffer -常量缓存区, ⼀个指向常量地址空间的任意数据类型的指针或引⽤ 

            texture - 纹理对象; 

            sampler - 采样器对象; 

            threadGrounp - 在线程组中供各线程共享的缓存.

            注意: 被着⾊器函数的缓存(device 和 constant) 不能重名; 

            Attribute Qualifiers to Locate Buffers, Textures, and Samplers ⽤于寻址缓存,纹理, 采样器的属性修饰符;对于每个着⾊器函数来说, ⼀个修饰符是必须指定的. 他⽤来设定⼀个缓存,纹理, 采样器的位置; 

            device buffers/ constant buffer --> [[buffer (index)]] 

            texture -- [[texture (index)]] 

            sampler -- [[sampler (index)]] 

            threadgroup buffer -- [[threadgroup (index)]] 

            index是⼀个unsigned integer类型的值,它表示了⼀个缓存、纹理、采样器参数的位置(在函数参数索引 表中的位置)。 从语法上讲,属性修饰符的声明位置应该位于参数变量名之后 

            例⼦中展示了⼀个简单的并⾏计算着⾊函数 add_vectors,它把两个设备地址空间中的缓存inA和inB相 加,然后把结果写⼊到缓存out。属性修饰符 “(buffer(index))”为着⾊函数参数设定了缓存的位置。

    //属性修饰符

    /*

     1. device buffer(设备缓存)

     2. constant buffer(常量缓存)

     3. texture Object(纹理对象)

     4. sampler Object(采样器对象)

     5. 线程组 threadgroup


     属性修饰符目的:

     1. 参数表示资源如何定位? 可以理解为端口

     2. 在固定管线和可编程管线进行内建变量的传递

     3. 将数据沿着渲染管线从顶点函数传递片元函数.


     在代码中如何表现:

     1.已知条件:device buffer(设备缓存)/constant buffer(常量缓存)

     代码表现:[[buffer(index)]]

     解读:不变的buffer ,index 可以由开发者来指定.


     2.已知条件:texture Object(纹理对象)

     代码表现: [[texture(index)]]

     解读:不变的texture ,index 可以由开发者来指定.


     3.已知条件:sampler Object(采样器对象)

     代码表示: [[sampler(index)]]

     解读:不变的sampler ,index 可以由开发者来指定.


     4.已知条件:threadgroup Object(线程组对象)

     代码表示: [[threadgroup(index)]]

     解读:不变的threadgroup ,index 可以由开发者来指定.

     */

    //并行计算着色器函数add_vectros ,实现2个设备地址空间中的缓存A与缓存B相加.然后将结果写入到缓存out.

    //属性修饰符"(buffer(index))" 为着色函数参数设定了缓存的位置

    //并行计算着色器函数add_vectros ,实现2个设备地址空间中的缓存A与缓存B相加.然后将结果写入到缓存out.

    //属性修饰符"(buffer(index))" 为着色函数参数设定了缓存的位置

    kernelvoidadd_vectros(

                    const device float4*inA [[buffer(0)]],

                    const device float4*inB [[buffer(1)]],

                    device float4*out [[buffer(2)]]

                    uintid[[thread_position_in_grid]])

    {

        out[id] = inA[id] + inB[id];

    }

            注意:thread_position_in_grid : ⽤于表示当前节点在多线程⽹格中的位置; 

    四.内建变量属性修饰符 

            [[vertex_id]] 顶点id 标识符; 

            [[position]] 顶点信息(float4) /� 述了⽚元的窗⼝相对坐标(x, y, z, 1/w) 

            [[point_size]] 点的⼤⼩(float) 

            [[color(m)]] 颜⾊, m编译前得确定; 

            struct MyFragmentOutput { 

             // color attachment 0 

             float4 clr_f [[color(0)]]; // color attachment 1 

             int4 clr_i [[color(1)]]; // color attachment 2 

             uint4 clr_ui [[color(2)]]; }; 

             fragment MyFragmentOutput my_frag_shader( ... ) 

             { 

             MyFragmentOutput f; 

             .... 

             f.clr_f = ...; 

             ... 

             return f;

            }

            [[stage_in]] : ⽚元着⾊函数使⽤的单个⽚元输⼊数据是由顶点着⾊函数输出然后经过光栅化⽣成的.顶 点和⽚元着⾊函数都是只能有⼀个参数被声明为使⽤“stage_in”修饰符,对于⼀个使⽤ 了“stage_in”修饰符的⾃ 定义的结构体,其成员可以为⼀个整形或浮点标量,或是整形或浮点向量



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

    iOS Metal语言规范浅谈

    一.Metal简述            Metal着色器语言是用来编写3D图形渲染逻辑、并行Metal计算核心逻辑的一门编程语言,当你使用Metal...
    继续阅读 »

    一.Metal简述

                Metal着色器语言是用来编写3D图形渲染逻辑、并行Metal计算核心逻辑的一门编程语言,当你使用Metal框架来完成APP的实现时则需要使用Metal编程语言。

                Metal语言使用Clang 和LLVM进行编译处理,编译器对于在GPU上的代码执行效率有更好的控制

                Metal基于C++ 11.0语言设计的,在C++基础上多了一些扩展和限制,主要用来编写在GPU上执行的图像渲染逻辑代码以及通用并行计算逻辑代码

                Metal 像素坐标系统:Metal中纹理 或者 帧缓存区attachment的像素使用的坐标系统的原点是左上角

    1.1Metal 语⾔中不⽀持之处 

            Lambda 表达式;

            递归函数调⽤

            动态转换操作符

            类型识别

            对象创建new 和销毁delete 操作符;

            操作符 noexcept

            goto 跳转

            变量存储修饰符register 和 thread_local;

            虚函数修饰符;

            派⽣类

            异常处理

            C++ 标准库在Metal 语⾔中也不可使⽤;

    1.2Metal 语⾔中对于指针使⽤的限制

            Metal图形和并⾏计算函数⽤到的⼊参数; 如果是指针必须使⽤地址空间修饰符(device,threadgroup,constant)

            不⽀持函数指针;

            函数名不能出现main

    二.Metal的数据类型及语法

    2.1     Metal 数据类型--标量数据类型 

            bool 布尔类型, true/false

            char 有符号8位整数;

            unsigned char /uchar ⽆符号8-bit 整数;

            short 有符号16-bit整数;

            unsigned short / ushort ⽆符号32-bit 整数;

            half 16位bit 浮点数;

            float 32bit 浮点数;

            size_t 64 ⽆符号整数;

            void 该类型表示⼀个空的值集合

    说明:其中half 相当于OC中的float,float 相当于OC中的doublesize_t用来表示内存空间, 相当于 OC中 sizeof

    示例:boola=true;charb=5;intd=15;//用于表示内存空间size_t c=1;ptrdiff_t f=2;

    2.2Metal向量

            向量支持如下类型:- booln、charn、shortn、intn、ucharn、ushortn、uintn、halfn、floatn,其中 n 表示向量的维度,最多不超过4维向量示例:

            //直接赋值初始化

            bool2 A={1,2}

            ;//通过内建函数float4初始化

            float4 pos=float4(1.0,2.0,3.0,4.0);

            //通过下标从向量中获取某个值

            floatx=pos[0];floaty=pos[1];

            //通过for循环对一个向量进行运算

            float4 VB;

            for(inti=0;i<4;i++){

                VB[i]=pos[i]*2.0f;

            }

            说明:在OpenGL ES的GLSL语言中,例如2.0f,在着色器中书写时,是不能加f,写成2.0,而在Metal中则可以写成2.0f,其中f可以是大写,也可以是小写

    向量的访问规则:

    1.通过向量字母获取元素: 向量中的向量字母仅有2种,分别为xyzw、rgba

            int4 test=int4(0,1,2,3);

            inta=test.x; //获取的向量元素0

            intb=test.y; //获取的向量元素1

            intc=test.z; //获取的向量元素2

            intd=test.w;//获取的向量元素3

            inte=test.r; //获取的向量元素0

            intf=test.g;//获取的向量元素1

            intg=test.b; //获取的向量元素2

            inth=test.a; //获取的向量元素3

    2.多个分量同时访问

            float4 c;

            c.xyzw=float4(1.0f,2.0f,3.0f,4.0f);

            c.z=1.0f

            c.xy=float2(3.0f,4.0f);

            c.xyz=float3(3.0f,4.0f,5.0f);

            说明:赋值时分量不可重复,取值时分量可重复右边是取值 和 左边赋值都合法xyzw与rgba不能混合使用,GLSL中向量不能乱序访问,只是和Metal中的向量相似,并不是等价



    2.3矩阵

            矩阵支持如下类型- halfnxm、floatnxm,其中 nxm表示矩阵的行数和列数,最多4行4列,其中half、float相当于OC中的float、double- 普通的矩阵其本质就是一个数组

            float4x4 m;

            //将第二行的所有值都设置为2.0

            m[1]=float4(2.0f);

            //设置第一行/第一列为1.0f

            m[0][0]=1.0f;

            //设置第三行第四列的元素为3.0f

            m[2][3]=3.0f;

            float4 类型向量的构造方式

            1个float构成,表示一行都是这个值

            4个float构成

            2个float2构成

            1个float2+2个float构成(顺序可以任意组合)

            1个float2+1个float

            1个float4

    eg:

            //float4类型向量的所有可能构造方式//1个一维向量,表示一行都是xfloat4(floatx);

            ///4个一维向量 --> 4维向量float4(floatx,floaty,floatz,floatw);

            //2个二维向量 --> 4维向量float4(float2 a,float2 b);

            //1个二维向量+2个一维向量 --> 4维向量float4(float2 a,float b,float c);

            float4(floata,float2 b,floatc);float4(floata,floatb,float2 c);

            //1个三维向量+1个一维向量 --> 4维向量float4(float3 a,floatb);float4(floata,float3 b);

    //1个四维向量 --> 4维向量float4(float4 x);

    float3 类型向量的构造方式

            1个float构成,表示一行都是这个值

            3个float

            1个float+1个float2(顺序可以任意组合)

            1个float2

    eg:

            //float3类型向量的所有可能的构造的方式

            //1个一维向量float3(floatx);

            //3个一维向量float3(floatx,floaty,floatz);

            //1个一维向量 + 1个二维向量float3(floata,float2 b);

            /1个二维向量 + 1个一维向量float3(float2 a,floatb);

            //1个三维向量float3(float3 x);

            float2 类型向量的构造方式

     1个float构成,表示一行都是这个值

            2个float

            1个float2

    eg:

            //float2类型向量的所有可能的构造方式

            //1个一维向量float2(floatx);

            //2个一维向量float2(floatx,floaty);

            //1个二维向量float2(float2 x);

    三,Metal的其他类型


    1.纹理

    纹理类型

    纹理类型是一个句柄,指向一维/二维/三维纹理数据,而纹理数据对应一个纹理的某个level的mipmap的全部或者一部分

    纹理的访问权限

    在一个函数中描述纹理对象的类型

    access枚举值由Metal定义,定义了纹理的访问权利enum class access {sample, read, write};,有以下3种访问权利,当没写access时,默认的access 就是sample

    sample: 纹理对象可以被采样(即使用采样器去纹理中读取数据,相当于OpenGL ES的GLSL中sampler2D),采样一维这时使用 或者 不使用都可以从纹理中读取数据(即可读可写可采样)

    read:不使用采样器,一个图形渲染函数或者一个并行计算函数可以读取纹理对象(即仅可读)

    write:一个图形渲染函数 或者 一个并行计算可以向纹理对象写入数据(即可读可写)

    定义纹理类型

    描述一个纹理对象/类型,有以下三种方式,分别对应一维/二维/三维,

    其中T代表泛型,设定了从纹理中读取数据 或是 写入时的颜色类型,T可以是half、float、short、int等

    access表示纹理访问权限,当access没写时,默认是sample

    texture1d<T, access a = access::sample>

    texture2d<T, access a = access::sample>

    texture3d<T, access a = access::sample>

    eg:

    //类型 变量 修饰符

    /*

    类型

        - texture2d<float>,读取的数据类型是float,没写access,默认是sample

        - texture2d<float,access::read>,读取的数据类型是float,读取的方式是read

        - texture2d<float,access::write>,读取的数据类型是float,读取的方式是write

    变量名

        - imgA

        - imgB

        - imgC

    修饰符

        - [[texture(0)]] 对应纹理0

        - [[texture(1)]] 对应纹理1

        - [[texture(2)]] 对应纹理2

    */函数举例

    void foo (texture2d<float> imgA[[texture(0)]],

              texture2d<float,access::read> imgB[[texture(1)]],

              texture2d<float,access::write> imgC[[texture(2)]])

    {

        //...

    }

    2.采样器

            采样器类型决定了如何对一个纹理进行采样操作,在Metal框架中有一个对应着色器语言的采样器的对象MTLSamplerState,这个对象作为图形渲染着色器函数参数或是并行计算函数的参数传递,有以下几种状态:

    coord:从纹理中采样时,纹理坐标是否需要归一化

    enum class coord { normalized, pixel };

    filter:纹理采样过滤方式,放大/缩小过滤方式

    enum class filter { nearest, linear };

    min_filter:设置纹理采样的缩小过滤方式

    enum class min_filter { nearest, linear };

    mag_filter:设置纹理采样的放大过滤方式

    enum class mag_filter { nearest, linear };

    s_address、t_address、r_address:设置纹理s、t、r坐标(对应纹理坐标的x、y、z)的寻址方式

    s坐标:enum class s_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    t坐标:enum class t_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    r坐标:enum class r_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    address:设置所有纹理坐标的寻址方式

    enum class address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };

    mip_filter:设置纹理采样的mipMap过滤模式, 如果是none,那么只有一层纹理生效;

    enum class mip_filter { none, nearest, linear };





    作者:枫紫_6174
    链接:https://www.jianshu.com/p/17baccd48e77









    收起阅读 »

    iOS YUV与RGB&YUV算法

    数字媒体压缩        为缩小数字媒体文件的大小,我们需要对其使用压缩技术,一般来说我们所欣赏的媒体内容都进行过一定程度的压缩,无论在电视上的视频,蓝光碟片,网页上的视频流还是...
    继续阅读 »

    数字媒体压缩

            为缩小数字媒体文件的大小,我们需要对其使用压缩技术,一般来说我们所欣赏的媒体内容都进行过一定程度的压缩,无论在电视上的视频,蓝光碟片,网页上的视频流还是冲iTunes Store购买的资源,我们其实都是和这些内容的原始文件的压缩格式在打交到,对数字媒体进行压缩可以大幅度缩小文件的尺寸,但是通常会在资源的质量上有小幅可见的衰减,

    色彩的二次采样

            视频数据是使用称之为YCbCr颜色模式,它也常称为YUV,虽然YUV术语并不准确,但是读起来比较方便,大部分的软件开发者都更熟悉RGB颜色模式,即每个像素点都由红,绿,蓝三个颜色组合而成,YCbCr或者是YUV则使用色彩(颜色)通道UV替换了像素的亮度通道.




    从上图中我们可以看到图片的细节都保存在亮度的通道中,假设世界上没有阳光,我们的眼睛是看不到任何的东西,如果去除亮度,剩下的就是一副灰度图片,我们在看整合的色彩通道中关于图片的所有细节都丢失了,这是由于我们眼睛对亮度的敏感度要高于颜色,所以,在YUV中,我们可以通过大幅减少存储在每个像素点中的颜色信息,而不致于图片的质量严重受损,这个减少颜色数据的过程就称之为色彩的二次采样。

    我们平时所说的4:4:4和4:2:2以及4:2:0到底指的是什么,以及它的由来?

            色彩的二次采样一般发生在取样时,一些专业的相机以4:4:4的参数捕捉图像,但大部分情况下对于图片的拍摄使用4:2:2的方式进行的,面向消费者的摄像头装置,比如iPhone手机上的摄像头,通常是以4:2:0的方式进行拍摄,即使经过大量层级的二次抽样之后仍然可以捕捉到高质量的图片,iPhone手机上拍出来的高质量视频就是很好的例证,

    1.RGB的颜色编码

            RGB 三个字⺟分别代表了 红(Red)、绿(Green)、蓝(Blue),这三种颜⾊称为 三原⾊,将它们以不同的⽐例相加,可以产⽣多种多样的颜⾊。


     在图像显示中,⼀张1280 * 720 ⼤⼩的图⽚,就代表着它有1280 * 720 个像素点。其中每⼀个像素点的颜⾊显示都采⽤RGB 编码⽅法,将RGB 分别取不同的值,就会展示不同的颜⾊。

            RGB 图像中,每个像素点都有红、绿、蓝三个原⾊,其中每种原⾊都占⽤8 bit,也就是⼀个字节,那么⼀个像素点也就占⽤24 bit,也就是三个字节。

            ⼀张1280 * 720 ⼤⼩的图⽚,就占⽤1280 * 720 * 3 / 1024 / 1024 = 2.63 MB 存储空间

    2.YUV的颜色编码

            YUV 颜⾊编码采⽤的是 明亮度 和 ⾊度 来指定像素的颜⾊。其中,Y 表示明亮度(Luminance、Luma),⽽U 和V 表示⾊度(Chrominance、Chroma)。⽽⾊度⼜定义了颜⾊的两个⽅⾯:⾊调和饱和度



      和RGB 表示图像类似,每个像素点都包含Y、U、V 分量。但是它的Y 和UV 分量是可以分离的,如果没有UV 分量⼀样可以显示完整的图像,只不过是⿊⽩的。对于YUV 图像来说,并不是每个像素点都需要包含了Y、U、V 三个分量,根据不同的采样格式,可以每个Y 分量都对应⾃⼰的UV 分量,也可以⼏个Y 分量共⽤UV 分量

    传说中的4:4:4

            在4:4:4的模式下,色彩的全部信息被保全下来,如图:




    相邻的四个像素点ABCD,每个像素点有自己的YUV,在色彩的二次采样的过程中,分别保留自己的YUV,称之为4:4:4;

    传说中的4:2:2


    如图ABCD四个相邻的像素点,A(Y0,U0,V0),B(Y1,U1,V1),C(Y2,U2,V2),D(Y3,U3,V3),当二次采样的时候,A采样的时候保留(Y0,U0),B保留(Y1,V1),C保留(Y2,U2),D保留(Y3,V3);也就是说,每个像素点的Y(明亮度)保留其本身的值,而U和V的值是每间隔一个采样,而最终就变成:





     也就是说A借B的V1,B借A的U0,C借D的V3,D借C的U2,这就是传说中的4:2:2,⼀张1280 * 720 ⼤⼩的图⽚,在YUV 4:2:2 采样时的⼤⼩为:

    (1280 * 720 * 8 + 1280 * 720 * 0.5 * 8 * 2)/ 8 / 1024 / 1024 = 1.76 MB 。

            可以看到YUV 4:2:2 采样的图像⽐RGB 模型图像节省了三分之⼀的存储空间,在传输时占⽤的带宽也会随之减少

    传说中的4:2:0

            在上面说到的4:2:2中我们可以看到相邻的两个像素点的UV是左右互相借的,那可不可以上下左右借了,答案当然是可以的,






    YUV 4:2:0 采样,并不是指只采样U 分量⽽不采样V 分量。⽽是指,在每⼀⾏扫描时,只扫描⼀种⾊度分量(U 或者V),和Y 分量按照2 : 1 的⽅式采样。⽐如,第⼀⾏扫描时,YU 按照2 : 1 的⽅式采样,那么第⼆⾏扫描时,YV 分量按照2:1 的⽅式采样。对于每个⾊度分量来说,它的⽔平⽅向和竖直⽅向的采样和Y 分量相⽐都是2:1 。假设第⼀⾏扫描了U 分量,第⼆⾏扫描了V 分量,那么需要扫描两⾏才能够组成完整的UV 分量

            从映射出的像素点中可以看到,四个Y 分量是共⽤了⼀套UV 分量,⽽且是按照2*2 的⼩⽅格的形式分布的,相⽐YUV 4:2:2 采样中两个Y 分量共⽤⼀套UV 分量,这样更能够节省空间。⼀张1280 * 720 ⼤⼩的图⽚,在YUV 4:2:0 采样时的⼤⼩为:

    (1280 * 720 * 8 + 1280 * 720 * 0.25 * 8 * 2)/ 8 / 1024 / 1024 = 1.32 MB 相对于2.63M节省了一半的空间

    RGB — YUV 颜⾊编码转换

            对于图像显示器来说,它是通过RGB 模型来显示图像的,⽽在传输图像数据时⼜是使⽤YUV 模型,这是因为YUV 模型可以节省带宽。因此就需要采集图像时将RGB 模型转换到YUV 模型,显示时再将YUV 模型转换为RGB 模型。

            RGB 到YUV 的转换,就是将图像所有像素点的R、G、B 分量转换到Y、U、V 分量。

            Y = 0.299 * R + 0.587 * G + 0.114 * B 

            U = -0.147 * R - 0.289 * G + 0.436 * B 

            V = 0.615 * R - 0.515 * G - 0.100 * B

            R = Y + 1.14 * V 

            G = Y - 0.39 * U - 0.58 * V 

            B = Y + 2.03 * U



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/65cf8f073ee5


    收起阅读 »

    关于 webpack 的几个知识点

    随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,比如 sass 和 less 的代码浏览器是不支持的, 但如果摒弃了这些开发框架,那么开发的效率将大幅下降。在众多前端工程化工具中,webpack 脱颖而出成为了当今...
    继续阅读 »

    随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,比如 sass 和 less 的代码浏览器是不支持的, 但如果摒弃了这些开发框架,那么开发的效率将大幅下降。在众多前端工程化工具中,webpack 脱颖而出成为了当今最流行的前端构建工具。 然而大多数的使用者都只是单纯的会使用,而并不知道其深层的原理。希望通过以下的面试题总结可以帮助大家温故知新、查缺补漏,知其然而又知其所以然。

    1. webpack 与 grunt、gulp 的不同?

    三者都是前端构建工具,grunt 和 gulp 在早期比较流行,现在 webpack 相对来说比较主流,不过一些轻量化的任务还是会用 gulp 来处理,比如单独打包 CSS 文件等。

    grunt 和 gulp 是基于任务和流(Task、Stream)的。类似 jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个 web 的构建流程。

    webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。

    所以总结一下:

    • 从构建思路来说
      • gulp 和 grunt 需要开发者将整个前端构建过程拆分成多个 `Task`,并合理控制所有 `Task` 的调用关系。
        webpack 需要开发者找到入口,并需要清楚对于不同的资源应该使用什么 Loader 做何种解析和加工。
    • 对于知识背景来说
      • gulp 更像后端开发者的思路,需要对于整个流程了如指掌。 webpack 更倾向于前端开发者的思路。

    2. 你为什么最终选择使用 webpack?

    基于入口的打包工具除了 webpack 以外,主流的还有:rollup 和 parcel

    从应用场景上来看:

    • webpack 适用于大型复杂的前端站点构建
    • rollup 适用于基础库的打包,如 vue、react
    • parcel 适用于简单的实验性项目,他可以满足低门槛的快速看到效果

    由于 parcel 在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用 parcel

    3. 有哪些常见的 Loader?解决什么问题?

    • babel-loader:把 ES6 转换成 ES5
    • eslint-loader:通过 ESLint 检查 JavaScript 代码
    • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
    • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
    • image-loader:加载并且压缩图片文件
    • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
    • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
    • source-map-loader:加载额外的 Source Map 文件,以方便断点调试

    4. 有哪些常见的Plugin?解决什么问题?

    • define-plugin:定义环境变量
    • commons-chunk-plugin:提取公共代码
    • uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
    • HTMLWebpackPlugin:webpack 在自定生成 html 时需要用到它,能自动引入 js/css 文件
    • MiniCssExtractPlugin:将 css 代码抽成单独的文件,一般适用于发布环境,生产环境用 css-loader

    5. Loader 和 Plugin 的不同?

    不同的作用

    • Loader 直译为"加载器"。webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到 loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非JavaScript文件 的能力。
    • Plugin 直译为"插件"。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

    不同的用法

    • Loader 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载器 (loader) 和使用的参数(options
    • Plugin 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

    6. 如何利用 webpack 来优化前端性能?

    用 webpack 优化前端性能是指:优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

    • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPluginParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩 css
    • 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用 webpack 对于output参数和各loader 的 publicPath 参数来修改资源路径
    • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
    • 提取公共代码。

    7. 如何提高 webpack 的构建速度?

    1. 多入口情况下,使用 CommonsChunkPlugin 来提取公共代码
    2. 通过 externals 配置来提取常用库
    3. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过 DllReferencePlugin将预编译的模块加载进来。
    4. 使用 Happypack 实现多线程加速编译
    5. 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
    6. 使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码
    原文:https://blog.csdn.net/Marker__/article/details/107619259
    收起阅读 »

    Xcode11,Transporter上传卡在——正在验证 APP - 正在通过App Store进行认证

    1.当卡死在 “Authenticating with the iTunes store”解决办法:关闭上传,并打开命令行,依次调用这三行代码: cd ~ mv .itmstransporter/ .old_itmstransporter/ "/Applica...
    继续阅读 »

    1.当卡死在 “Authenticating with the iTunes store”
    解决办法:
    关闭上传,并打开命令行,依次调用这三行代码:


    cd ~
    mv .itmstransporter/ .old_itmstransporter/
    "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter"
    `&lt;/pre&gt;

    先说结论,此方法有效,但是对于Xcode11来说Application Loader已经移除了,那么路径就要改变到Transporter下,所以需要修改最后一个命令。为什么有效呢,因为本质上iTMSTransporter是所有上传工具真正使用的可执行文件。所以Transporter下也会发现这个文件。

    &lt;span style="font-weight: bold; font-size: medium;"&gt;1.首先找到文件位置,反键显示包内容。&lt;/span&gt;
    ![](https://upload-images.jianshu.io/upload_images/5276080-dd51fa3a174b994a.png?imageMogr2/auto-orient/strip|imageView2/2/w/828/format/webp)

    &lt;span style="font-weight: bold; font-size: medium;"&gt;2.将iTMSTransporter的路径找到&lt;/span&gt;
    ![](https://upload-images.jianshu.io/upload_images/5276080-f664e303dd5b0547.png?imageMogr2/auto-orient/strip|imageView2/2/w/1033/format/webp)

    &lt;span style="font-weight: bold; font-size: medium;"&gt;3.执行以下命令&lt;/span&gt;
    &lt;pre&gt;`cd ~
    mv .itmstransporter/ .old_itmstransporter/
    "/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter"`&lt;/pre&gt;

    有两个点可能会出问题
    &lt;span style="color: rgb(77, 128, 191);"&gt;3.1 rename .itmstransporter/ to .old_itmstransporter/.itmstransporter/: Directory not empty&lt;/span&gt;
    如果第二句命令报以上错误,输入以下命令
    &lt;pre&gt;`mv .old_itmstransporter/ .itmstransporter/
    mv .itmstransporter/ .old_itmstransporter/

    3.2 no such file or directory: xxxxxxxx
    如果第三句命令报以上错误,是因为直接复制我的路径,但是你的应用路径跟我的不一致,自己将iTMSTransporter的路径找到并拼接好。
    接下来会出现[2020-01-15 18:08:13 CST] <main> INFO: Configuring logging…,然后就开始无尽的等待,如果长时间没有进展,建议切换4G网络开热点给电脑使用,说不定有奇效。
    最后指令执行完会出现[2020-01-15 18:10:07 CST] <main> DBG-X: Returning 0
    对于我来说,之后再去用Transporter上传,第一步正在通过App Store进行认证很快就过去了,然后在App Store验证时卡住了几分钟,接着出现了将数据发送到App Store时出错。

    然后我看到了稍后重新启动决定多等待以下,结果过了大概3分钟,就开始上传了。

    然后瞬间就上传成功了。至此我折腾了一个下午的上传IPA,终于结束了。

    重大更新
    如果一直命令一直卡着,也无法上传成功,可以试试下面的办法。

    上传卡住的原因:
    Transporter安装上第一次打开后,会在硬盘目录:/用户/你的电脑登录账号名/资源库/Caches/com.apple.amp.itmstransporter/目录下下载一些缓存文件,这些缓存文件没有下载完,或者下载失败没下载完时,使用Transporter去提交应用这个页面就会卡住或者这个页面很慢。

    那么一直更新不成功的话,可以下载这个文件夹直接覆盖自己的原有com.apple.amp.itmstransporter文件夹,如果原本没有也直接复制进去相当于创建了。

    步骤如下:

    https://download.csdn.net/download/Walter_White/12207626


    1.下载链接里的文件,把解压后的"com.apple.amp.itmstransporter"目录放到"/用户/你的电脑登录账号名/资源库/Caches/"目录下,覆盖你原有的"com.apple.amp.itmstransporter"目录。
    2.将新的"com.apple.amp.itmstransporter"目录下/obr/2.0.0/目录下的repository.xml文件中的所有"Simpsons"修改为你自己电脑的登录账号名,否则Transporter执行时会在错误的路径下找资源文件。
    3.再次尝试Transporter上传。
    4.如果时间App Store认证时间超过两分钟,建议手机开4g热点,电脑连接后再上传试试。

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

    收起阅读 »

    【iOS】一个简单的人脸跟踪Demo

    1、sessionView - 相机画面的容器Viewself.detector - 脸部特征识别器- (void)viewDidLoad { [super viewDidLoad]; self.sessionView = [[UIView...
    继续阅读 »

    1、
    sessionView - 相机画面的容器View
    self.detector - 脸部特征识别器

    - (void)viewDidLoad {
    [super viewDidLoad];

    self.sessionView = [[UIView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.sessionView];

    self.faceView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"a"]];
    self.faceView.frame = CGRectZero;
    [self.view addSubview:self.faceView];

    self.leftEyeView = [[UIView alloc] init];
    self.leftEyeView.alpha = 0.4;
    self.leftEyeView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:self.leftEyeView];

    self.rightEyeView = [[UIView alloc] init];
    self.rightEyeView.alpha = 0.4;
    self.rightEyeView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:self.rightEyeView];

    self.mouthView = [[UIView alloc] init];
    self.mouthView.alpha = 0.4;
    self.mouthView.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.mouthView];

    self.context = [CIContext context];
    self.detector = [CIDetector detectorOfType:CIDetectorTypeFace context:self.context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
    }

    2、点击屏幕任意地方打开相机

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 避免重复打开,首先关闭原先的session
    [self.session stopRunning];
    self.session = [[AVCaptureSession alloc] init];

    // 移除原有的相机画面Layer
    [self.layer removeFromSuperlayer];

    NSError *error;

    // Device
    NSArray *devices = [AVCaptureDevice devices];
    NSLog(@"devices = %@", devices);
    AVCaptureDevice *defaultDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    // Input
    AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:defaultDevice error:&error];
    [self.session addInput:input];

    // Output
    AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
    [output setSampleBufferDelegate:(id)self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self.session addOutput:output];

    // 开始捕获相机画面
    [self.session startRunning];

    // 将相机画面添加到容器View中
    self.layer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
    self.layer.frame = self.view.bounds;
    [self.sessionView.layer addSublayer:self.layer];
    }

    3、脸部特征跟踪

    // AVCaptureAudioDataOutputSampleBufferDelegate
    - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    // printf("%s\n", __func__);
    // 1、获取当前帧图像
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CIImage *image = [[CIImage alloc] initWithCVImageBuffer:imageBuffer];

    CGFloat imageW = image.extent.size.width;
    CGFloat imageH = image.extent.size.height;

    2、对图像进行脸部特征识别
    CIFeature *feature = [[self.detector featuresInImage:image] lastObject];
    if (feature) {
    if (self.leftEyeView.frame.size.width == 0) {
    self.leftEyeView.frame = CGRectMake(0, 0, 20, 20);
    }
    if (self.rightEyeView.frame.size.width == 0) {
    self.rightEyeView.frame = CGRectMake(0, 0, 20, 20);
    }
    if (self.mouthView.frame.size.width == 0) {
    self.mouthView.frame = CGRectMake(0, 0, 20, 20);
    }
    NSLog(@"find");
    CIFaceFeature *face = (CIFaceFeature *)feature;
    dispatch_async(dispatch_get_main_queue(), ^{
    self.faceView.frame = CGRectMake(face.bounds.origin.y / imageW * self.sessionView.frame.size.height,
    face.bounds.origin.x / imageH * self.sessionView.frame.size.width,
    face.bounds.size.width / imageH * self.sessionView.frame.size.width,
    face.bounds.size.height / imageW * self.sessionView.frame.size.height);

    self.leftEyeView.center = CGPointMake(face.leftEyePosition.y / imageW * self.sessionView.frame.size.height,
    face.leftEyePosition.x / imageH * self.sessionView.frame.size.width);

    self.rightEyeView.center = CGPointMake(face.rightEyePosition.y / imageW * self.sessionView.frame.size.height,
    face.rightEyePosition.x / imageH * self.sessionView.frame.size.width);

    self.mouthView.center = CGPointMake(face.mouthPosition.y / imageW * self.sessionView.frame.size.height,
    face.mouthPosition.x / imageH * self.sessionView.frame.size.width);

    });
    }
    }

    大功告成
    手机记得横过来,home键在右边
    Demo地址:https://github.com/MagicBlind/Face-Detector

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

    收起阅读 »