注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android移动端IM开发之应用层实现TCP长连接多路复用

IM
这里只是提供一个长连接多路复用的实现思路 什么是长连接多路复用 从字面意思看就是一台设备只有一条长连接连向服务器,其他集成这个IM SDK的app都会共享这条长连接,TCP长连接的维护是比较耗资源的,而且也会增加耗电,所以实现长连接共享就表示在一定程度...
继续阅读 »


这里只是提供一个长连接多路复用的实现思路


什么是长连接多路复用



  1. 从字面意思看就是一台设备只有一条长连接连向服务器,其他集成这个IM SDK的app都会共享这条长连接,TCP长连接的维护是比较耗资源的,而且也会增加耗电,所以实现长连接共享就表示在一定程度上降低了耗电也节省了内存

  2. java里面还有一个IO多路复用,这个用在服务端的比较多,用来解决并发问题,服务器的IM框架netty也用到了IO多路复用,原先是一个客户端对应一个IO,服务器要为每个客户端单独开一个线程来处理客户端的消息请求,但是使用了IO多路复用后,可能多个客户端共享一条IO链路,节省了资源,不必为每个客户端来单独开一个线程处理,具体服务端怎么实现我也没去深究,想多了解就上网找找资料或者看下netty的源码实现


IM的长连接多路复用的实现


IM的长连接多路复用的实现我觉得分为两部分,一部分是服务端的实现,一部分是客户端的实现
1. 一般一台手机设备会对应一个唯一的设备id,这个设备id对于服务器来说是绝对唯一的,是标识设备唯一性的一个标志,所以IM的接入服务器可以根据设备id来检测并控制每台设备连到服务器的长连接数量,接入服务器可以让同一台设备如果有两条不同的tcp连接连进来,那么可以断开其中一条tcp而只保持唯一一条tcp连接,这是从服务端保证单台设备TCP连接的唯一性
2. android客户端会集成响应的IM SDK,一般tcp长连接由service来维护,至于进程保活其实也就是service保活,因为push进程一般是和app的进程分开的;那么要实现在android手机设备上tcp连接的唯一性其实也就是保证维护tcp连接的service的唯一性,在android sdk中是可以有办法来判断service是否在运行的:


public static boolean isServiceRunning(Context mContext, String clsName) {
boolean isRunning = false;
ActivityManager myAM = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningServiceInfo> myList = myAM.getRunningServices(Integer.MAX_VALUE);
if (myList == null || myList.size() <= 0) {
return false;
}
for (int i = 0; i < myList.size(); i++) {
String mName = myList.get(i).service.getClassName().toString();
if (mName.equals(clsName)) {
isRunning = true;
break;
}
}
return isRunning;
}

其实思路很简单,检测到这个service已经运行了,那么就不start service了,而只要bind service来获取binder对象就足够了,然后可以通过aidl来实现跨进程通信了,但是这里有两个概念,就是必须指定宿主app和寄生app,宿主app就是负责维护唯一的tcp长连接,寄生app的收发消息就通过宿主app来路由,这样就做到了tcp长连接共享,所以在检测到宿主app维护长连接的service没有启动的时候寄生app就可以start service并且bind service,但是注意了,这里不能通过简单的action来启动service,要指定正确的ComponentName,或者把宿主app的context传进来:


Intent serviceIntent = new Intent();
serviceIntent.setComponent(new ComponentName(componentPckName, ConnectionService.class.getName()));
serviceIntent.setPackage(context.getPackageName());
serviceIntent.setAction(ConnectionService.ACTION);

如上componentPckName必须是宿主app的package name或者宿主app的context,那么在寄生app里面启动service才能正确启动宿主app的service,这就相当于唤醒了宿主app了,如果寄生app只是简单的通过action来start service那么它将会为自己启动一个service,而不会去启动宿主app的service,到这里寄生app其实已经开始公用宿主app的service,我已经实现了,aidl调用正常,这样也省却了im连接成功后的初始化操作,只需要在长连接第一次连接成功的时候初始化以下,后面其他的app直接bind service获取binder来直接调用aidl接口就行了。这里有一个可以优化的地方,多进程之间的通信我采用的是aidl来实现,aidl代码冗余,而且传输数据比较慢,比什么慢?我觉得比tcp长连接慢,也就是说可以用本地的tcp长连接来实现进程间的通信,把宿主app当作一个服务器,寄生app相当于客户端,然后每个寄生app都建立一条和宿主app的长连接来实现数据传输,但是要使用长连接来传输数据就必须要制定数据编码格式,因为长连接的数据传输的流传输,有粘包问题,所以要对数据进行编码校验然后解码,而且要维护这条长连接也要有一些工作量,例如长连接断线重连等,所以有利有弊,目前我暂时选择的是aidl来实现进程之间的通信,有可能以后会改成tcp长连接的通信,tcp长连接可以实现宿主app和寄生app互相监听,让service的存活率更高,我同时也不得不承认,这很流氓。。。

收起阅读 »

Android中开发IM即时通讯功能的方案

IM
最近准备做IM功能,在技术预研当中,其实可行方案还是比较多的,就看怎么选了这里有必要普及一下xmpp是个什么东西?引用某娘的定义:      XMPP是一种基于标准通用标记语言的子集XML的协议,它继承了在XML环境中灵活的发展性...
继续阅读 »

最近准备做IM功能,在技术预研当中,其实可行方案还是比较多的,就看怎么选了


这里有必要普及一下xmpp是个什么东西?

引用某娘的定义:

      XMPP是一种基于标准通用标记语言的子集XML的协议,它继承了在XML环境中灵活的发展性。因此,基于XMPP的应用具有超强的可扩展性。经过扩展以后的XMPP可以通过发送扩展的信息来处理用户的需求,以及在XMPP的顶端建立如内容发布系统和基于地址的服务等应用程序。而且,XMPP包含了针对服务器端的软件协议,使之能与另一个进行通话,这使得开发者更容易建立客户应用程序或给一个配好系统添加功能。

网络结构:

      XMPP中定义了三个角色,客户端,服务器,网关。通信能够在这三者的任意两个之间双向发生。服务器同时承担了客户端信息记录,连接管理和信息的路由功能。网关承担着与异构即时通信系统的互联互通,异构系统可以包括SMS(短信),MSNICQ等。基本的网络形式是单客户端通过TCP/IP连接到单服务器,然后在之上传输XML。

功能:

      传输的是与即时通讯相关的指令。在以前这些命令要么用2进制的形式发送(比如QQ),要么用纯文本指令加空格加参数加换行符的方式发送(比如MSN)。而XMPP传输的即时通讯指令的逻辑与以往相仿,只是协议的形式变成了XML格式的纯文本。


一句话总结就是一个可以用于IM功能的协议,传输的是xml数据


移动端IM大概有以下这些方案,但不仅于这些

1.第三方平台

比如环信,融云,leancloud,容联云等等。直接使用sdk就可以实现了,最简单最直接,而且稳定性已经不错了,连UI界面都带有了,可以自行修改,缺点是要收费,比如环信日活跃用户30万以上开始收费,融云基础功能不收费,VIP级别的功能要收费。如果让我选应该会在环信和融云之间选,环信的话一个是市场占有率比较大,二个是技术客服基本都能找到在线的,融云客服的话需要像写邮件一样找技术解决问题,不是在线的交流,我觉得挺不好的,融云的demo倒是做得比环信好看,环信的看上去很粗糙的感觉。据使用过的朋友反馈融云sdk比较好接入,环信稍微麻烦点,使用的时候都会有一些坑的地方要处理。


2.spark+smack+openfire套餐

安卓使用asmack,测试使用spark,服务器使用openfire。asmack可以说是smack的Android平台的支持版提供xmpp协议的实现,就是一些个api,spark就是一个可以用来在pc相互同信的客户端,openfire部署也比较简单,next,next就差不多了。这套方案还算比较成熟的了,当然没有第三方的方便,工作量也增多了,这套也是基于xmpp协议同时也就有xmpp协议的一些缺点。


3.使用第三方推送的sdk

利用推送的及时性来做im也是可以的,推送也不收费吧。


4.Socket长链接

socket大家都懂了的,感觉做个小东西还可以,但是正真商用要做的处理还是挺多的,网络优化,稳定性等等。


5.基于xmpp自己做

xmpp协议本来就是可以坐im功能的,据了解环信就是自己基于xmpp来做的,50人上下的团队14年开始做的,现在两年左右已经融了两三轮资了。不过xmpp基于xml,冗长直接导致费流量费电,特别是移动端,可以考虑在传输的时候自己加一层二进制协议,如果环信真是xmpp来做应该也会在这方面做优化的。还有mqtt,sip等协议也是可以自己做。如果im不是核心功能不推荐自己做,费时费力。



收起阅读 »

IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的

IM
1、引言 好久没写技术文章了,今天这篇不是原理性文章,而是为大家分享一下由笔者主导开发实施的IM即时通讯聊天系统,针对大量离线消息(包括消息漫游)导致的用户体验问题的升级改造全过程。 文章中,我将从如下几个方面进行介绍: 1)这款IM产品的主要业务及...
继续阅读 »

1、引言


好久没写技术文章了,今天这篇不是原理性文章,而是为大家分享一下由笔者主导开发实施的IM即时通讯聊天系统,针对大量离线消息(包括消息漫游)导致的用户体验问题的升级改造全过程。


文章中,我将从如下几个方面进行介绍:



  • 1)这款IM产品的主要业务及特点;

  • 2)IM系统业务现状和痛点;

  • 3)升级改造之路;

  • 4)消息ACK逻辑的优化。


下述内容都是根据笔者开发IM的亲身经历总结下来的宝贵经验,干货满满,期待你的点赞。


学习交流:



- 即时通讯/推送技术开发交流5群:215477170 [推荐]


- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM





2、IM开发干货系列文章


本文是系列文章中的第25篇,总目录如下:



IM消息送达保证机制实现(一):保证在线实时消息的可靠投递


IM消息送达保证机制实现(二):保证离线消息的可靠投递


如何保证IM实时消息的“时序性”与“一致性”?


IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?


IM群聊消息如此复杂,如何保证不丢不重?


一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)


移动端IM登录时拉取数据如何作到省流量?


通俗易懂:基于集群的移动端IM接入层负载均衡方案分享


浅谈移动端IM的多点登陆和消息漫游原理


IM开发基础知识补课(一):正确理解前置HTTP SSO单点登陆接口的原理


IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?


IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议


IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token


IM群聊消息的已读回执功能该怎么实现?


IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?


IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列


一个低成本确保IM消息时序的方法探讨


IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!


IM里“附近的人”功能实现原理是什么?如何高效率地实现它?


IM开发基础知识补课(七):主流移动端账号登录方式的原理及设计思路


IM开发基础知识补课(八):史上最通俗,彻底搞懂字符乱码问题的本质


IM的扫码登功能如何实现?一文搞懂主流应用的扫码登陆技术原理


IM要做手机扫码登陆?先看看微信的扫码登录功能技术原理


IM开发基础知识补课(九):想开发IM集群?先搞懂什么是RPC!


IM开发实战干货:我是如何解决大量离线聊天消息导致客户端卡顿的》(本文)



另外,如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。


3、此IM产品的主要业务及特点


和传统互联网行业有所不同,笔者所在的公司(名字就不透露了)是一家做娱乐社交app的公司,包括小游戏、聊天、朋友圈feed等。


大家应该都有体会:游戏业务在技术上和产品形态上与电商、旅游等行业有着本质上的区别。


大部分做后端开发的朋友,都在开发接口。客户端或浏览器h5通过HTTP请求到我们后端的Controller接口,后端查数据库等返回JSON给客户端。大家都知道,HTTP协议有短连接、无状态、三次握手四次挥手等特点。而像游戏、实时通信等业务反而很不适合用HTTP协议。


原因如下:



  • 1)HTTP达不到实时通信的效果,可以用客户端轮询但是太浪费资源;

  • 2)三次握手四次挥手有严重的性能问题;

  • 3)无状态。


比如说,两个用户通过App聊天,一方发出去的消息,对方要实时感知到消息的到来。两个人或多个人玩游戏,玩家要实时看到对方的状态,这些场景用HTTP根本不可能实现!因为HTTP只能pull(即“拉”),而聊天、游戏业务需要push(即“推”)。


4、IM系统业务现状和痛点


4.1 业务现状


笔者负责整个公司的实时聊天系统,类似与微信、QQ那样,有私聊、群聊、发消息、语音图片、红包等功能。


下面我详细介绍一下,整个聊天系统是如何运转的。


首先:为了达到实时通信的效果,我们基于Netty开发了一套长链接网关gateway(扩展阅读:《Netty干货分享:京东京麦的生产级TCP网关技术实践总结》),采用的协议是MQTT协议,客户端登录时App通过MQTT协议连接到gateway(NettyServer),然后通过MQTT协议把聊天消息push给NettyServer,NettyServer与NettyClient保持长链接,NettyClient用于处理业务逻辑(如敏感词拦截、数据校验等)处理,最后将消息push给NettyServer,再由NettyServer通过MQTT push给客户端。


其次:客户端与服务端想要正常通信,我们需要制定一套统一的协议。拿聊天举例,我们要和对方聊天,需要通过uid等信息定位到对方的Channel(Netty中的通道,相当于一条socket连接),才能将消息发送给正确的客户端,同时客户端必须通过协议中的数据(uid、groupId等),将消息显示在私聊或者群聊的会话中。


协议中主要字段如下(我们将数据编码成protobuf格式进行传输):



{


    "cmd":"chat",


    "time":1554964794220,


    "uid":"69212694",


    "clientInfo":{


        "deviceId":"b3b1519c-89ec",


        "deviceInfo":"MI 6X"


    },


    "body":{


        "v":1,


        "msgId":"5ab2fe83-59ec-44f0-8adc-abf26c1e1029",


        "chatType":1,


        "ackFlg":1,


        "from":"69212694",


        "to":"872472068",


        "time":1554964793813,


        "msg":{


            "message":"聊天消息"


        }


    }


}



补充说明:如果你不了Protobuf格式是什么,请详读《Protobuf通信协议详解:代码演示、详细原理介绍等》。


如上json,协议主要字段包括: 



如果客户端不在线,我们服务端需要把发送的消息存储在离线消息表中,等下次对方客户端上线,服务端NettyServer通过长链接把离线消息push给客户端。


4.2 业务痛点


随着业务蓬勃发展,用户的不断增多,用户创建的群、加入的群和好友不断增多和聊天活跃度的上升,某些用户不在线期间,产生大量的离线消息(尤其是针对群聊,离线消息特别多)。


等下次客户端上线时,服务端会给客户端强推全部的离线消息,导致客户端卡死在登录后的首页。并且产品提出的需求,要扩大群成员的人数(由之前的百人群扩展到千人群、万人群等)。


这样一来,某些客户端登录后必定会因为大量离线消息而卡死,用户体验极为不好。


和客户端的同事一起分析了一下原因:



  • 1)用户登录,服务端通过循环分批下发所有离线消息,数据量较大;

  • 2)客户端登录后进入首页,需要加载的数据不光有离线消息,还有其他初始化数据;

  • 3)不同价位的客户端处理数据能力有限,处理聊天消息时,需要把消息存储到本地数据库,并且刷新UI界面,回复给服务端ack消息,整个过程很耗性能。


(庆幸的是,在线消息目前没有性能问题)。


所以针对上述问题,结合产品对IM系统的远大规划,我们服务端决定优化离线消息(稍微吐槽一下,客户端处理能力不够,为什么要服务端做优化?服务端的性能远没达到瓶颈。。。)。


5、升级改造之路


值得庆幸的是,笔者100%参与这次系统优化的全部过程,包括技术选型、方案制定和最后的代码编写。在此期间,笔者思考出多种方案,然后和服务端、客户端同事一起讨论,最后定下来一套稳定的方案。


5.1 方案一(被pass掉的一个方案)


? 【问题症状】:


客户端登录卡顿的主要原因是,服务端会强推大量离线消息给客户端,客户端收到离线消息后会回复服务端ack,然后将消息存储到本地数据库、刷新UI等。客户端反馈,即使客户端采用异步方式也会有比较严重的性能问题。


? 【于是我想】:


为什么客户端收到消息后还没有将数据存储到数据库就回复给服务端ack?很有可能存储失败,这本身不合理,这是其一。其二,服务端强推导致客户端卡死,不关心客户端的处理能力,不合理。


? 【伪代码如下】:



int max = 100;


//从新库读


while(max > 0) {


    List<OfflineMsgInfo> offlineMsgListNew = shardChatOfflineMsgDao.getByToUid(uid, 20);


    if(CollectionUtils.isEmpty(offlineMsgListNew)) {


        break;


    }


    handleOfflineMsg(uid, offlineMsgListNew, checkOnlineWhenSendingOfflineMsg);


    max--;


}



? 【初步方案】:


既然强推不合理,我们可以换一种方式,根据客户端不同机型的处理能力的不同,服务端采用不同的速度下发。


我们可以把整个过程当成一种生产者消费者模型,服务端是消息生产者,客户端是消息消费者。客户端收到消息,将消息存储在本地数据库,刷新UI界面后,再向服务端发送ack消息,服务端收到客户端的ack消息后,再推送下一批消息。


这么一来,消息下发速度完全根据客户端的处理能力,分批下发。但这种方式仍然属于推方式。


? 【悲剧结果】:


然而,理想很丰满,现实却很骨感。


针对这个方案,客户端提出一些问题:



  • 1)虽然这种方案,客户端不会卡死,但是如果当前用户的离线消息特别多,那么收到所有离线消息的时间会非常长;

  • 2)客户端每次收到消息后会刷新界面,很有可能客户端会发生,界面上下乱跳的画面。


so,这个方案被否定了。。。


5.2 方案二


? 【我的思考】:


既然强推的数据量过大,我们是否可以做到,按需加载?客户端需要读取离线消息的时候服务端给客户端下发,不需要的时候,服务端就不下发。


? 【技术方案】:针对离线消息,我们做了如下方案的优化


1)我们增加了离线消息计数器的概念:保存了每个用户的每个会话,未读的消息的元数据(包括未读消息数,最近的一条未读消息、时间戳等数据),这个计数器用于客户端显示未读消息的的红色气泡。这个数据属于增量数据,只保留离线期间收到的消息元数据。


消息格式如下:



{


    "sessionId1":{


        "count":20,


        "lastMsg":[


            "最后N条消息"


        ],


        "timestamp":1234567890


    },


    "sessionId2":{


    }


}



 


 


2)客户端每次登录时,服务端不推送全量离线消息,只推送离线消息计数器(这部分数据存储在redis里,并且数据量很小),这个数量用户显示在客户端消息列表的未读消息小红点上。


3)客户端拿到这些离线消息计数器数据,遍历会话列表,依次将未读消息数量累加(注意:不是覆盖,服务端保存客户端离线后的增量数据),然后通知服务端清空离线消息计数器的增量数据。


4)当客户端进入某会话后,上拉加载时,通过消息的msgId等信息发送HTTP请求给服务端,服务端再去分页查询离线消息返回给客户端。


5)客户端收到消息并保存在本地数据库后,向服务端发送ack,然后服务端删除离线消息表的离线消息。


? 【预期结果】:


客户端、服务端的技术人员认可这个方案。我们通过推拉结合的方式,解决了客户端加载离线消息卡顿的问题。(改造前是强推,改造后采用推拉结合的方式)


流程图如下:



? 【新的问题】:


方案虽然通过了,但是引发了一个新问题:即客户端消息衔接问题。


问题描述如下:客户端登录后进入会话页面,因为客户端本身就保存着历史消息,那么客户端下拉加载新消息时,到底怎么判断要加载本地历史消息?还是要请求服务端加载离线消息呢?


经过一番思考,服务端和客户端最终达成了一致的方案:



  • 1)在未读消息计数器的小红点逻辑中,服务端会把每个会话的最近N条消息一起下发给客户端;

  • 2)客户端进入会话时,会根据未读消息计数器的最近N条消息展示首页数据;

  • 3)客户端每次下拉加载时,请求服务端,服务端按时间倒排离线消息返回当前会话最近一页离线消息,直到离线消息库中的数据全部返回给客户端;

  • 4)当离线消息库中没有离线消息后,返回给客户端一个标识,客户端根据这个标识,在会话页面下一次下拉加载时不请求服务端的离线消息,直接请求本地数据库。


6、消息ACK逻辑的优化


最后,我们也对消息ack的逻辑进行了优化。


优化前:服务端采用push模型给客户端推消息,不论是在线消息还是离线消息,ack的逻辑都一样,其中还用到了kafka、redis等中间件,流程很复杂(我在这里就不详细展开介绍ack的具体流程了,反正不合理)。


离线消息和在线消息不同的是,我们不存储在线消息,而离线消息会有一个单独的库存储。完全没必要用在线消息的ack逻辑去处理离线消息,反而很不合理,不仅流程上有问题,也浪费kafka、redis等中间件性能。


优化后:我们和客户端决定在每次下拉加载离线消息时,将收到的上一批离线消息的msgId或消息偏移量等信息发送给服务端,服务端直接根据msgId删除离线库中已经发送给客户端的离线消息,再返回给客户端下一批离线消息。


另外:我们还增加了消息漫游功能,用户切换手机登录后仍然可以查到历史消息,这部分内容我就不展开详细介绍给大家了。


7、设计优化方案时的文档截图(仅供参考)


下面是优化的方案文档截图,请大家参考。


 



 



 


 



 


 



 


附录:更多IM开发相关文章汇总



[1] IM开发热门文章:


新手入门一篇就够:从零开发移动端IM


移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”


移动端IM开发者必读(二):史上最全移动弱网络优化方法总结


从客户端的角度来谈谈移动端IM的消息可靠性和送达机制


现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障


腾讯技术分享:社交网络图片的带宽压缩技术演进之路


小白必读:闲话HTTP短连接中的Session和Token


IM开发基础知识补课:正确理解前置HTTP SSO单点登录接口的原理


移动端IM中大规模群消息的推送如何保证效率、实时性?


移动端IM开发需要面对的技术问题


开发IM是自己设计协议用字节流好还是字符流好?


请问有人知道语音留言聊天的主流实现方式吗?


IM消息送达保证机制实现(一):保证在线实时消息的可靠投递


IM消息送达保证机制实现(二):保证离线消息的可靠投递


如何保证IM实时消息的“时序性”与“一致性”?


一个低成本确保IM消息时序的方法探讨


IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?


IM群聊消息如此复杂,如何保证不丢不重?


谈谈移动端 IM 开发中登录请求的优化


移动端IM登录时拉取数据如何作到省流量?


浅谈移动端IM的多点登录和消息漫游原理


完全自已开发的IM该如何设计“失败重试”机制?


通俗易懂:基于集群的移动端IM接入层负载均衡方案分享


微信对网络影响的技术试验及分析(论文全文)


即时通讯系统的原理、技术和应用(技术论文)


开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀


QQ音乐团队分享:Android中的图片压缩技术详解(上篇)


QQ音乐团队分享:Android中的图片压缩技术详解(下篇)


腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率


腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)


腾讯原创分享(三):如何大幅压缩移动网络下APP的流量消耗(下篇)


如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源


基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?


腾讯技术分享:腾讯是如何大幅降低带宽和网络流量的(图片压缩篇)


腾讯技术分享:腾讯是如何大幅降低带宽和网络流量的(音视频技术篇)


字符编码那点事:快速理解ASCII、Unicode、GBK和UTF-8


全面掌握移动端主流图片格式的特点、性能、调优等


子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践


IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列


微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)


自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)


融云技术分享:解密融云IM产品的聊天消息ID生成策略


IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!


适合新手:从零开发一个IM服务端(基于Netty,有完整源码)


拿起键盘就是干:跟我一起徒手开发一套分布式IM系统


适合新手:手把手教你用Go快速搭建高性能、可扩展的IM系统(有源码)


IM里“附近的人”功能实现原理是什么?如何高效率地实现它?


IM开发基础知识补课(七):主流移动端账号登录方式的原理及设计思路


IM开发基础知识补课(八):史上最通俗,彻底搞懂字符乱码问题的本质


IM“扫一扫”功能很好做?看看微信“扫一扫识物”的完整技术实现


IM的扫码登录功能如何实现?一文搞懂主流应用的扫码登录技术原理


IM要做手机扫码登录?先看看微信的扫码登录功能技术原理


IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)


IM消息ID技术专题(二):微信的海量IM聊天消息序列号生成实践(容灾方案篇)


IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略


IM消息ID技术专题(四):深度解密美团的分布式ID生成算法


IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现


IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总


IM开发干货分享:我是如何解决大量离线聊天消息导致客户端卡顿的


>> 更多同类文章 …… 


[2] IM群聊相关的技术文章:


快速裂变:见证微信强大后台架构从0到1的演进历程(一)


如何保证IM实时消息的“时序性”与“一致性”?


IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?


IM群聊消息如此复杂,如何保证不丢不重?


微信后台团队:微信后台异步消息队列的优化升级实践分享


移动端IM中大规模群消息的推送如何保证效率、实时性?


现代IM系统中聊天消息的同步和存储方案探讨


关于IM即时通讯群聊消息的乱序问题讨论


IM群聊消息的已读回执功能该怎么实现?


IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?


一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践


[技术脑洞] 如果把14亿中国人拉到一个微信群里技术上能实现吗?


IM群聊机制,除了循环去发消息还有什么方式?如何优化?


网易云信技术分享:IM中的万人群聊技术方案实践总结


阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处


>> 更多同类文章 ……


[3] 有关IM架构设计的文章:


浅谈IM系统的架构设计


简述移动端IM开发的那些坑:架构设计、通信协议和客户端


一套海量在线用户的移动端IM架构设计实践分享(含详细图文)


一套原创分布式即时通讯(IM)系统理论架构方案


从零到卓越:京东客服即时通讯系统的技术架构演进历程


蘑菇街即时通讯/IM服务器开发之架构选择


腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT


微信后台基于时间序的海量数据冷热分级架构设计实践


微信技术总监谈架构:微信之道——大道至简(演讲全文)


如何解读《微信技术总监谈架构:微信之道——大道至简》


快速裂变:见证微信强大后台架构从0到1的演进历程(一)


17年的实践:腾讯海量产品的技术方法论


移动端IM中大规模群消息的推送如何保证效率、实时性?


现代IM系统中聊天消息的同步和存储方案探讨


IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?


IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议


IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token


WhatsApp技术实践分享:32人工程团队创造的技术神话


微信朋友圈千亿访问量背后的技术挑战和实践总结


王者荣耀2亿用户量的背后:产品定位、技术架构、网络方案等


IM系统的MQ消息中间件选型:Kafka还是RabbitMQ?


腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面


以微博类应用场景为例,总结海量社交系统的架构设计步骤


快速理解高性能HTTP服务端的负载均衡技术原理


子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践


知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路


IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列


微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)


微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)


新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践


一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践


阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史


阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路


社交软件红包技术解密(一):全面解密QQ红包技术方案——架构、技术实现等


社交软件红包技术解密(二):解密微信摇一摇红包从0到1的技术演进


社交软件红包技术解密(三):微信摇一摇红包雨背后的技术细节


社交软件红包技术解密(四):微信红包系统是如何应对高并发的


社交软件红包技术解密(五):微信红包系统是如何实现高可用性的


社交软件红包技术解密(六):微信红包系统的存储层架构演进实践


社交软件红包技术解密(七):支付宝红包的海量高并发技术实践


社交软件红包技术解密(八):全面解密微博红包技术方案


社交软件红包技术解密(九):谈谈手Q红包的功能逻辑、容灾、运维、架构等


社交软件红包技术解密(十):手Q客户端针对2020年春节红包的技术实践


即时通讯新手入门:一文读懂什么是Nginx?它能否实现IM的负载均衡?


即时通讯新手入门:快速理解RPC技术——基本概念、原理和用途


多维度对比5款主流分布式MQ消息队列,妈妈再也不担心我的技术选型了


从游击队到正规军(一):马蜂窝旅游网的IM系统架构演进之路


从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结


IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!


瓜子IM智能客服系统的数据架构设计(整理自现场演讲,有配套PPT)


阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处


从游击队到正规军(三):基于Go的马蜂窝旅游网分布式IM系统技术实践


微信后台基于时间序的新一代海量数据存储架构的设计实践


IM开发基础知识补课(九):想开发IM集群?先搞懂什么是RPC!


>> 更多同类文章 ……



(本文同步发布于:http://www.52im.net/thread-3036-1-1.html


收起阅读 »

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM(有源码)

IM
本文由作者FreddyChen原创分享,为了更好的体现文章价值,引用时有少许改动,感谢原作者。 1、写在前面 一直想写一篇关于im即时通讯分享的文章,无奈工作太忙,很难抽出时间。今天终于从公司离职了,打算好好休息几天再重新找工作,趁时间空闲,决定静下心...
继续阅读 »

本文由作者FreddyChen原创分享,为了更好的体现文章价值,引用时有少许改动,感谢原作者。



1、写在前面


一直想写一篇关于im即时通讯分享的文章,无奈工作太忙,很难抽出时间。今天终于从公司离职了,打算好好休息几天再重新找工作,趁时间空闲,决定静下心来写一篇文章,毕竟从前辈那里学到了很多东西。


工作了五年半,这三四年来一直在做社交相关的项目,有直播、即时通讯、短视频分享、社区论坛等产品,深知即时通讯技术在一个项目中的重要性,本着开源分享的精神,也趁这机会总结一下,所以写下了这篇文章。


* 重要提示:本文不是一篇即时通讯理论文章,文章内容全部由实战代码组织而成,如果你对即时通讯(IM)技术理论了解的太少,建议先详细阅读:《新手入门一篇就够:从零开发移动端IM》。


本文实践内容将涉及以下即时通讯技术内容:



1)Protobuf序列化;


2)TCP拆包与粘包;


3)长连接握手认证;


4)心跳机制;


5)重连机制;


6)消息重发机制;


7)读写超时机制;


8)离线消息;


9)线程池。



不想看文章的同学,可以直接到Github下载本文源码:



1)原始地址:https://github.com/FreddyChen/NettyChat


2)备用地址:https://github.com/52im/NettyChat



接下来,让我们进入正题。


(本文同步发布于:http://www.52im.net/thread-2671-1-1.html


2、本文阅读对象


本文适合没有任何即时通讯(IM)开发经验的小白开发者阅读,文章将教你从零开始,围绕一个典型即时通讯(IM)系统的方方面面,手把手为你展示如何基于Netty+TCP+Protobuf来开发出这样的系统。非常适合从零入门的Android开发者。


本文不适合没有编程的准开发者阅读,因为即时通讯(IM)系统属于特定的业务领域,如果你连一般的逻辑代码都很难编写出来,不建议阅读本文。本文显然不是一个编程语言入门教程。


3、关于作者



本文原文内容由FreddyChen原创分享,作者现从事Android程序开发,他的技术博客地址:https://juejin.im/user/5bd7affbe51d4547f763fe72 


4、为什么使用TCP?


这里需要简单解释一下,TCP/UDP的区别,简单地总结一下。


优点:


1)TCP:优点体现在稳定、可靠上,在传输数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完之后,还会断开连接用来节约系统资源。


2)UDP:优点体现在快,比TCP稍安全,UDP没有TCP拥有的各种机制,是一个无状态的传输协议,所以传递数据非常快,没有TCP的这些机制,被攻击利用的机制就少一些,但是也无法避免被攻击。


缺点:


1)TCP:缺点就是慢,效率低,占用系统资源高,易被攻击,TCP在传递数据之前要先建立连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞机制等都会消耗大量时间,而且要在每台设备上维护所有的传输连接。


2)UDP:缺点就是不可靠,不稳定,因为没有TCP的那些机制,UDP在传输数据时,如果网络质量不好,就会很容易丢包,造成数据的缺失。


适用场景:


1)TCP:当对网络通讯质量有要求时,比如HTTP、HTTPS、FTP等传输文件的协议, POP、SMTP等邮件传输的协议。


2)UDP:对网络通讯质量要求不高时,要求网络通讯速度要快的场景。


至于WebSocket,后续可能会专门写一篇文章来介绍。综上所述,决定采用TCP协议。


关于TCP和UDP的对比和选型的详细文章,请见:



简述传输层协议TCP和UDP的区别


为什么QQ用的是UDP协议而不是TCP协议?


移动端即时通讯协议选择:UDP还是TCP?


网络编程懒人入门(四):快速理解TCP和UDP的差异


网络编程懒人入门(五):快速理解为什么说UDP有时比TCP更有优势


Android程序员必知必会的网络通信传输层协议——UDP和TCP



或者,如果你对TCP、UDP协议了解的太少,可以阅读一下文章:



TCP/IP详解 - 第11章·UDP:用户数据报协议


TCP/IP详解 - 第17章·TCP:传输控制协议


TCP/IP详解 - 第18章·TCP连接的建立与终止


TCP/IP详解 - 第21章·TCP的超时与重传


脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手


技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)


通俗易懂-深入理解TCP协议(上):理论基础


网络编程懒人入门(三):快速理解TCP协议一篇就够


迈向高阶:优秀Android程序员必知必会的网络基础



5、为什么使用Protobuf?


对于App网络传输协议,我们比较常见的、可选的,有三种,分别是json/xml/protobuf,老规矩,我们先分别来看看这三种格式的优缺点。


PS:如果你不了解protobuf是什么,建议详细阅读:《Protobuf通信协议详解:代码演示、详细原理介绍等》。


优点:


1)json:优点就是较XML格式更加小巧,传输效率较xml提高了很多,可读性还不错。


2)xml:优点就是可读性强,解析方便。


3)protobuf:优点就是传输效率快(据说在数据量大的时候,传输效率比xml和json快10-20倍),序列化后体积相比Json和XML很小,支持跨平台多语言,消息格式升级和兼容性还不错,序列化反序列化速度很快。


缺点:


1)json:缺点就是传输效率也不是特别高(比xml快,但比protobuf要慢很多)。


2)xml:缺点就是效率不高,资源消耗过大。


3)protobuf:缺点就是使用不太方便。


在一个需要大量的数据传输的场景中,如果数据量很大,那么选择protobuf可以明显的减少数据量,减少网络IO,从而减少网络传输所消耗的时间。考虑到作为一个主打社交的产品,消息数据量会非常大,同时为了节约流量,所以采用protobuf是一个不错的选择。


更多有关IM相关的协议格式选型方面的文章,可进一步阅读:



如何选择即时通讯应用的数据传输格式


强列建议将Protobuf作为你的即时通讯应用数据传输格式


全方位评测:Protobuf性能到底有没有比JSON快5倍?


移动端IM开发需要面对的技术问题(含通信协议选择)


简述移动端IM开发的那些坑:架构设计、通信协议和客户端


理论联系实际:一套典型的IM通信协议设计详解


58到家实时消息系统的协议设计等技术实践分享


详解如何在NodeJS中使用Google的Protobuf


技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解


金蝶随手记团队分享:还在用JSON? Protobuf让数据传输更省更快(原理篇)


金蝶随手记团队分享:还在用JSON? Protobuf让数据传输更省更快(实战篇)


>> 更多同类文章 ……



6、为什么使用Netty?


首先,我们来了解一下,Netty到底是个什么东西。网络上找到的介绍:Netty是由JBOSS提供的基于Java NIO的开源框架,Netty提供异步非阻塞、事件驱动、高性能、高可靠、高可定制性的网络应用程序和工具,可用于开发服务端和客户端。


PS:如果你对Java的经典IO、NIO或者Netty框架不了解,请阅读以下文章:



史上最强Java NIO入门:担心从入门到放弃的,请读这篇!


少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别


写给初学者:Java高性能NIO框架Netty的学习方法和进阶策略


NIO框架详解:Netty的高性能之道



为什么不用Java BIO?


1)一连接一线程:由于线程数是有限的,所以这样非常消耗资源,最终也导致它不能承受高并发连接的需求。


2)性能低:因为频繁的进行上下文切换,导致CUP利用率低。


3)可靠性差:由于所有的IO操作都是同步的,即使是业务线程也如此,所以业务线程的IO操作也有可能被阻塞,这将导致系统过分依赖网络的实时情况和外部组件的处理能力,可靠性大大降低。


为什么不用Java NIO?


1)NIO的类库和API相当复杂,使用它来开发,需要非常熟练地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel等。


2)需要很多额外的编程技能来辅助使用NIO,例如,因为NIO涉及了Reactor线程模型,所以必须必须对多线程和网络编程非常熟悉才能写出高质量的NIO程序。


3)想要有高可靠性,工作量和难度都非常的大,因为服务端需要面临客户端频繁的接入和断开、网络闪断、半包读写、失败缓存、网络阻塞的问题,这些将严重影响我们的可靠性,而使用原生NIO解决它们的难度相当大。


4)JDK NIO中著名的BUG--epoll空轮询,当select返回0时,会导致Selector空轮询而导致CUP100%,官方表示JDK1.6之后修复了这个问题,其实只是发生的概率降低了,没有根本上解决。


为什么用Netty?


1)API使用简单,更容易上手,开发门槛低;


2)功能强大,预置了多种编解码功能,支持多种主流协议;


3)定制能力高,可以通过ChannelHandler对通信框架进行灵活地拓展;


4)高性能,与目前多种NIO主流框架相比,Netty综合性能最高;


5)高稳定性,解决了JDK NIO的BUG;


6)经历了大规模的商业应用考验,质量和可靠性都有很好的验证。


为什么不用第三方SDK,如:融云、环信、腾讯TIM?


这个就见仁见智了,有的时候,是因为公司的技术选型问题,因为用第三方的SDK,意味着消息数据需要存储到第三方的服务器上,再者,可扩展性、灵活性肯定没有自己开发的要好,还有一个小问题,就是收费。比如,融云免费版只支持100个注册用户,超过100就要收费,群聊支持人数有限制等等...



▲ 以上截图内容来自某云IM官网


Mina其实跟Netty很像,大部分API都相同,因为是同一个作者开发的。但感觉Mina没有Netty成熟,在使用Netty的过程中,出了问题很轻易地可以找到解决方案,所以,Netty是一个不错的选择。


PS:有关MINA和Netty框架的关系和对比,详见以下文章:



有关“为何选择Netty”的11个疑问及解答


开源NIO框架八卦——到底是先有MINA还是先有Netty?


选Netty还是Mina:深入研究与对比(一)


选Netty还是Mina:深入研究与对比(二)



好了,废话不多说,直接开始吧。


7、准备工作


首先,我们新建一个Project,在Project里面再新建一个Android Library,Module名称暂且叫做im_lib,如图所示:



然后,分析一下我们的消息结构,每条消息应该会有一个消息唯一id,发送者id,接收者id,消息类型,发送时间等,经过分析,整理出一个通用的消息类型,如下:



msgId:消息id


fromId:发送者id


toId:接收者id


msgType:消息类型


msgContentType:消息内容类型


timestamp:消息时间戳


statusReport:状态报告


extend:扩展字段



根据上述所示,我整理了一个思维导图,方便大家参考:



这是基础部分,当然,大家也可以根据自己需要自定义比较适合自己的消息结构。


我们根据自定义的消息类型来编写proto文件:



syntax = "proto3";// 指定protobuf版本


option java_package = "com.freddy.im.protobuf";// 指定包名


option java_outer_classname = "MessageProtobuf";// 指定生成的类名


 


message Msg {


    Head head = 1;// 消息头


    string body = 2;// 消息体


}


 


message Head {


    string msgId = 1;// 消息id


    int32 msgType = 2;// 消息类型


    int32 msgContentType = 3;// 消息内容类型


    string fromId = 4;// 消息发送者id


    string toId = 5;// 消息接收者id


    int64 timestamp = 6;// 消息时间戳


    int32 statusReport = 7;// 状态报告


    string extend = 8;// 扩展字段,以key/value形式存放的json


}



然后执行命令(我用的mac,windows命令应该也差不多):



然后就会看到,在和proto文件同级目录下,会生成一个java类,这个就是我们需要用到的东东:



我们打开瞄一眼:



东西比较多,不用去管,这是google为我们生成的protobuf类,直接用就行,怎么用呢?


直接用这个类文件,拷到我们开始指定的项目包路径下就可以啦:



添加依赖后,可以看到,MessageProtobuf类文件已经没有报错了,顺便把netty的jar包也导进来一下,还有fastjson的:



建议用netty-all-x.x.xx.Final的jar包,后续熟悉了,可以用精简的jar包。


至此,准备工作已结束,下面,我们来编写java代码,实现即时通讯的功能。


8、代码封装


为什么需要封装呢?说白了,就是为了解耦,为了方便日后切换到不同框架实现,而无需到处修改调用的地方。


举个栗子,比如Android早期比较流行的图片加载框架是Universal ImageLoader,后期因为某些原因,原作者停止了维护该项目,目前比较流行的图片加载框架是Picasso或Glide,因为图片加载功能可能调用的地方非常多,如果不作一些封装,早期使用了Universal ImageLoader的话,现在需要切换到Glide,那改动量将非常非常大,而且还很有可能会有遗漏,风险度非常高。


那么,有什么解决方案呢?


很简单,我们可以用工厂设计模式进行一些封装,工厂模式有三种:简单工厂模式、抽象工厂模式、工厂方法模式。在这里,我采用工厂方法模式进行封装,具体区别,可以参见:《通俗讲讲我对简单工厂、工厂方法、抽象工厂三种设计模式的理解》。


我们分析一下,ims(IM Service,下文简称ims)应该是有初始化、建立连接、重连、关闭连接、释放资源、判断长连接是否关闭、发送消息等功能。


基于上述分析,我们可以进行一个接口抽象:




OnEventListener是与应用层交互的listener:



IMConnectStatusCallback是ims连接状态回调监听器:



然后写一个Netty tcp实现类:




接下来,写一个工厂方法:



封装部分到此结束,接下来,就是实现了。


9、初始化


我们先实现init(Vector serverUrlList, OnEventListener listener, IMSConnectStatusCallback callback)方法,初始化一些参数,以及进行第一次连接等:



其中,MsgDispatcher是消息转发器,负责将接收到的消息转发到应用层:



ExecutorServiceFactory是线程池工厂,负责调度重连及心跳线程:





10、连接及重连


resetConnect()方法作为连接的起点,首次连接以及重连逻辑,都是在resetConnect()方法进行逻辑处理。


我们来瞄一眼:



可以看到,非首次进行连接,也就是连接一个周期失败后,进行重连时,会先让线程休眠一段时间,因为这个时候也许网络状况不太好,接着,判断ims是否已关闭或者是否正在进行重连操作,由于重连操作是在子线程执行,为了避免重复重连,需要进行一些并发处理。


开始重连任务后,分四个步骤执行:



1)改变重连状态标识;


2)回调连接状态到应用层;


3)关闭之前打开的连接channel;


4)利用线程池执行一个新的重连任务。



ResetConnectRunnable是重连任务,核心的重连逻辑都放到这里执行:





toServer()是真正连接服务器的地方:



initBootstrap()是初始化Netty Bootstrap:



注:NioEventLoopGroup线程数设置为4,可以满足QPS是一百多万的情况了,至于应用如果需要承受上千万上亿流量的,需要另外调整线程数。(参考自:《netty实战之百万级流量NioEventLoopGroup线程数配置》)


接着,我们来看看TCPChannelInitializerHanlder



其中,ProtobufEncoderProtobufDecoder是添加对protobuf的支持,LoginAuthRespHandler是接收到服务端握手认证消息响应的处理handler,HeartbeatRespHandler是接收到服务端心跳消息响应的处理handler,TCPReadHandler是接收到服务端其它消息后的处理handler,先不去管,我们重点来分析下LengthFieldPrependerLengthFieldBasedFrameDecoder,这就需要引申到TCP的拆包与粘包啦。


11、TCP的拆包与粘包


什么是TCP拆包?为什么会出现TCP拆包?


简单地说,我们都知道TCP是以“流”的形式进行数据传输的,而且TCP为提高性能,发送端会将需要发送的数据刷入缓冲区,等待缓冲区满了之后,再将缓冲区中的数据发送给接收方,同理,接收方也会有缓冲区这样的机制,来接收数据。拆包就是在socket读取时,没有完整地读取一个数据包,只读取一部分。


什么是TCP粘包?为什么会出现TCP粘包?


同上。粘包就是在socket读取时,读到了实际意义上的两个或多个数据包的内容,同时将其作为一个数据包进行处理。


引用一张图片来解释一下在TCP出现拆包、粘包以及正常状态下的三种情况:



了解了TCP出现拆包/粘包的原因,那么,如何解决呢?


通常来说,有以下四种解决方式:



1)消息定长;


2)用回车换行符作为消息结束标志;


3)用特殊分隔符作为消息结束标志,如\t、\n等,回车换行符其实就是特殊分隔符的一种;


4)将消息分为消息头和消息体,在消息头中用字段标识消息总长度。



netty针对以上四种场景,给我们封装了以下四种对应的解码器:



1)FixedLengthFrameDecoder,定长消息解码器;


2)LineBasedFrameDecoder,回车换行符消息解码器;


3)DelimiterBasedFrameDecoder,特殊分隔符消息解码器;


4)LengthFieldBasedFrameDecoder,自定义长度消息解码器。



我们用到的就是LengthFieldBasedFrameDecoder自定义长度消息解码器,同时配合LengthFieldPrepender编码器使用,关于参数配置,建议参考《netty--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender》这篇文章,讲解得比较细致。


我们配置的是消息头长度为2个字节,所以消息包的最大长度需要小于65536个字节,netty会把消息内容长度存放消息头的字段里,接收方可以根据消息头的字段拿到此条消息总长度,当然,netty提供的LengthFieldBasedFrameDecoder已经封装好了处理逻辑,我们只需要配置lengthFieldOffset、lengthFieldLength、lengthAdjustment、initialBytesToStrip即可,这样就可以解决TCP的拆包与粘包,这也就是netty相较于原生nio的便捷性,原生nio需要自己处理拆包/粘包等问题。


12、长连接握手认证


接着,我们来看看LoginAuthHandler和HeartbeatRespHandler。


LoginAuthRespHandler:是当客户端与服务端长连接建立成功后,客户端主动向服务端发送一条登录认证消息,带入与当前用户相关的参数,比如token,服务端收到此消息后,到数据库查询该用户信息,如果是合法有效的用户,则返回一条登录成功消息给该客户端,反之,返回一条登录失败消息给该客户端,这里,就是在接收到服务端返回的登录状态后的处理handler。


比如:



可以看到,当接收到服务端握手消息响应后,会从扩展字段取出status,如果status=1,则代表握手成功,这个时候就先主动向服务端发送一条心跳消息,然后利用Netty的IdleStateHandler读写超时机制,定期向服务端发送心跳消息,维持长连接,以及检测长连接是否还存在等。


HeartbeatRespHandler:是当客户端接收到服务端登录成功的消息后,主动向服务端发送一条心跳消息,心跳消息可以是一个空包,消息包体越小越好,服务端收到客户端的心跳包后,原样返回给客户端,这里,就是收到服务端返回的心跳消息响应的处理handler。


比如:



这个就比较简单,收到心跳消息响应,无需任务处理,直接打印一下方便我们分析即可。


13、心跳机制及读写超时机制


心跳包是定期发送,也可以自己定义一个周期,比如:《移动端IM实践:实现Android版微信的智能心跳机制》,为了简单,此处规定应用在前台时,8秒发送一个心跳包,切换到后台时,30秒发送一次,根据自己的实际情况修改一下即可。心跳包用于维持长连接以及检测长连接是否断开等。


PS:更多心跳保活方面的文章请见:



Android端消息推送总结:实现原理、心跳保活、遇到的问题等


为何基于TCP协议的移动端IM仍然需要心跳保活机制?


微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)


移动端IM实践:WhatsApp、Line、微信的心跳策略分析



接着,我们利用Netty的读写超时机制,来实现一个心跳消息管理handler:



可以看到,利用userEventTriggered()方法回调,通过IdleState类型,可以判断读超时/写超时/读写超时,这个在添加IdleStateHandler时可以配置,下面会贴上代码。


首先我们可以在READER_IDLE事件里,检测是否在规定时间内没有收到服务端心跳包响应,如果是,那就触发重连操作。在WRITER_IDEL事件可以检测客户端是否在规定时间内没有向服务端发送心跳包,如果是,那就主动发送一个心跳包。发送心跳包是在子线程中执行,我们可以利用之前写的work线程池进行线程管理。


addHeartbeatHandler()代码如下:



从图上可看到,在IdleStateHandler里,配置的读超时为心跳间隔时长的3倍,也就是3次心跳没有响应时,则认为长连接已断开,触发重连操作。写超时则为心跳间隔时长,意味着每隔heartbeatInterval会发送一个心跳包。读写超时没用到,所以配置为0。


onConnectStatusCallback(int connectStatus)为连接状态回调,以及一些公共逻辑处理:



连接成功后,立即发送一条握手消息,再次梳理一下整体流程:


1)客户端根据服务端返回的host及port,进行第一次连接;


2)连接成功后,客户端向服务端发送一条握手认证消息(1001);


3)服务端在收到客户端的握手认证消息后,从扩展字段里取出用户token,到本地数据库校验合法性;


4)校验完成后,服务端把校验结果通过1001消息返回给客户端,也就是握手消息响应;


5)客户端收到服务端的握手消息响应后,从扩展字段取出校验结果。若校验成功,客户端向服务端发送一条心跳消息(1002),然后进入心跳发送周期,定期间隔向服务端发送心跳消息,维持长连接以及实时检测链路可用性,若发现链路不可用,等待一段时间触发重连操作,重连成功后,重新开始握手/心跳的逻辑。


看看TCPReadHandler收到消息是怎么处理的:




可以看到,在channelInactive()exceptionCaught()方法都触发了重连,channelInactive()方法在当链路断开时会调用,exceptionCaught()方法在当出现异常时会触发,另外,还有诸如channelUnregistered()channelReadComplete()等方法可以重写,在这里就不贴了,相信聪明的你一眼就能看出方法的作用。


我们仔细看一下channelRead()方法的逻辑,在if判断里,先判断消息类型,如果是服务端返回的消息发送状态报告类型,则判断消息是否发送成功,如果发送成功,从超时管理器中移除,这个超时管理器是干嘛的呢?


下面讲到消息重发机制的时候会详细地讲。在else里,收到其他消息后,会立马给服务端返回一个消息接收状态报告,告诉服务端,这条消息我已经收到了,这个动作,对于后续需要做的离线消息会有作用。如果不需要支持离线消息功能,这一步可以省略。最后,调用消息转发器,把接收到的消息转发到应用层即可。


代码写了这么多,我们先来看看运行后的效果,先贴上缺失的消息发送代码及ims关闭代码以及一些默认配置项的代码。


发送消息:



关闭ims:



ims默认配置:



还有,应用层实现的ims client启动器:



由于代码有点多,不太方便全部贴上,如果有兴趣可以下载本文的完整demo进行体验。


额,对了,还有一个简易的服务端代码,如下:





14、运行调试


我们先来看看连接及重连部分(由于录制gif比较麻烦,体积较大,所以我先把重连间隔调小成3秒,方便看效果)。


启动服务端:



启动客户端:



可以看到,正常的情况下已经连接成功了,接下来,我们来试一下异常情况。


比如服务端没启动,看看客户端的重连情况:



这次我们先启动的是客户端,可以看到连接失败后一直在进行重连,由于录制gif比较麻烦,在第三次连接失败后,我启动了服务端,这个时候客户端就会重连成功。


然后,我们再来调试一下握手认证消息即心跳消息:



可以看到,长连接建立成功后,客户端会给服务端发送一条握手认证消息(1001),服务端收到握手认证消息会,给客户端返回了一条握手认证状态消息,客户端收到握手认证状态消息后,即启动心跳机制。gif不太好演示,下载demo就可以直观地看到。


接下来,在讲完消息重发机制及离线消息后,我会在应用层做一些简单的封装,以及在模拟器上运行,这样就可以很直观地看到运行效果。


15、消息重发机制


消息重发,顾名思义,即使对发送失败的消息进行重发。考虑到网络环境的不稳定性、多变性(比如从进入电梯、进入地铁、移动网络切换到wifi等),在消息发送的时候,发送失败的概率其实不小,这时消息重发机制就很有必要了。


有关即时通讯(IM)应用中的消息送达保证机制,可以详细阅读以下文章:



IM消息送达保证机制实现(一):保证在线实时消息的可靠投递


IM群聊消息如此复杂,如何保证不丢不重?


完全自已开发的IM该如何设计“失败重试”机制?



我们先来看看实现的代码逻辑。


MsgTimeoutTimer:




MsgTimeoutTimerManager:




然后,我们看看收消息的TCPReadHandler的改造:



最后,看看发送消息的改造:



说一下逻辑吧:发送消息时,除了心跳消息、握手消息、状态报告消息外,消息都加入消息发送超时管理器,立马开启一个定时器,比如每隔5秒执行一次,共执行3次,在这个周期内,如果消息没有发送成功,会进行3次重发,达到3次重发后如果还是没有发送成功,那就放弃重发,移除该消息,同时通过消息转发器通知应用层,由应用层决定是否再次重发。如果消息发送成功,服务端会返回一个消息发送状态报告,客户端收到该状态报告后,从消息发送超时管理器移除该消息,同时停止该消息对应的定时器即可。


另外,在用户握手认证成功时,应该检查消息发送超时管理器里是否有发送超时的消息,如果有,则全部重发:



16、离线消息


由于离线消息机制,需要服务端数据库及缓存上的配合,代码就不贴了,太多太多。


我简单说一下实现思路吧:客户端A发送消息到客户端B,消息会先到服务端,由服务端进行中转。


这个时候,客户端B存在两种情况:



1)长连接正常,就是客户端网络环境良好,手机有电,应用处在打开的情况;


2)废话,那肯定就是长连接不正常咯。这种情况有很多种原因,比如wifi不可用、用户进入了地铁或电梯等网络不好的场所、应用没打开或已退出登录等,总的来说,就是没有办法正常接收消息。



如果是长连接正常,那没什么可说的,服务端直接转发即可。


如果长连接不正常,需要这样处理:


服务端接收到客户端A发送给客户端B的消息后,先给客户端A回复一条状态报告,告诉客户端A,我已经收到消息,这个时候,客户端A就不用管了,消息只要到达服务端即可。然后,服务端先尝试把消息转发到客户端B,如果这个时候客户端B收到服务端转发过来的消息,需要立马给服务端回一条状态报告,告诉服务端,我已经收到消息,服务端在收到客户端B返回的消息接收状态报告后,即认为此消息已经正常发送,不需要再存库。


如果客户端B不在线,服务端在做转发的时候,并没有收到客户端B返回的消息接收状态报告,那么,这条消息就应该存到数据库,直到客户端B上线后,也就是长连接建立成功后,客户端B主动向服务端发送一条离线消息询问,服务端在收到离线消息询问后,到数据库或缓存去查客户端B的所有离线消息,并分批次返回,客户端B在收到服务端的离线消息返回后,取出消息id(若有多条就取id集合),通过离线消息应答把消息id返回到服务端,服务端收到后,根据消息id从数据库把对应的消息删除即可。


以上是单聊离线消息处理的情况,群聊有点不同,群聊的话,是需要服务端确认群组内所有用户都收到此消息后,才能从数据库删除消息,就说这么多,如果需要细节的话,可以私信我。


更多有关离线消息处理思路的文章,可以详细阅读:



IM消息送达保证机制实现(二):保证离线消息的可靠投递


IM群聊消息如此复杂,如何保证不丢不重?


浅谈移动端IM的多点登陆和消息漫游原理



不知不觉,NettyTcpClient中定义了很多变量,为了防止大家不明白变量的定义,还是贴上代码吧:



18、最终运行


运行一下,看看效果吧:



运行步骤是:



1)首先,启动服务端。


2)然后,修改客户端连接的ip地址为192.168.0.105(这是我本机的ip地址),端口号为8855,fromId,也就是userId,定义成100001,toId为100002,启动客户端A。


3)再然后,fromId,也就是userId,定义成100002,toId为100001,启动客户端B。


4)客户端A给客户端B发送消息,可以看到在客户端B的下面,已经接收到了消息。


5)用客户端B给客户端A发送消息,也可以看到在客户端A的下面,也已经接收到了消息。



至于,消息收发测试成功。至于群聊或重连等功能,就不一一演示了,还是那句话,下载demo体验一下吧:https://github.com/52im/NettyChat


由于gif录制体积较大,所以只能简单演示一下消息收发,具体下载demo体验吧。如果有需要应用层UI实现(就是聊天页及会话页的封装)的话,我再分享出来吧。


19、写在最后


终于写完了,这篇文章大概写了10天左右,有很大部分的原因是自己有拖延症,每次写完一小段,总静不下心来写下去,导致一直拖到现在,以后得改改。第一次写技术分享文章,有很多地方也许逻辑不太清晰,由于篇幅有限,也只是贴了部分代码,建议大家把源码下载下来看看。一直想写这篇文章,以前在网上也尝试过找过很多im方面的文章,都找不到一篇比较完善的,本文谈不上完善,但包含的模块很多,希望起到一个抛砖引玉的作用,也期待着大家跟我一起发现更多的问题并完善,最后,如果这篇文章对你有用,希望在github上给我一个star哈。。。


应大家要求,精简了netty-all-4.1.33.Final.jar包,原netty-all-4.1.33.Final.jar包大小为3.9M。


经测试发现目前im_lib库只需要用到以下jar包:



netty-buffer-4.1.33.Final.jar


netty-codec-4.1.33.Final.jar


netty-common-4.1.33.Final.jar


netty-handler-4.1.33.Final.jar


netty-resolver-4.1.33.Final.jar


netty-transport-4.1.33.Final.jar



所以,抽取以上jar包,重新打成了netty-tcp-4.1.33-1.0.jar(已经上传到github工程了),目前自测没有问题,如果发现bug,请告诉我,谢谢。


附上原jar及裁剪后jar包的大小对比:




代码已更新到Github:


https://github.com/52im/NettyChat


附录:更多网络编程/即时通讯/消息推送的实战入门文章



手把手教你用Netty实现网络通信程序的心跳机制、断线重连机制
NIO框架入门(一):服务端基于Netty4的UDP双向通信Demo演示
NIO框架入门(二):服务端基于MINA2的UDP双向通信Demo演示
NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战
NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战
微信小程序中如何使用WebSocket实现长连接(含完整源码)
Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)
解决MINA数据传输中TCP的粘包、缺包问题(有源码)
开源IM工程“蘑菇街TeamTalk”2015年5月前未删减版完整代码 [附件下载]
用于IM中图片压缩的Android工具类源码,效果可媲美微信 [附件下载]
高仿Android版手机QQ可拖拽未读数小气泡源码 [附件下载]
一个WebSocket实时聊天室Demo:基于node.js+socket.io [附件下载]
Android聊天界面源码:实现了聊天气泡、表情图标(可翻页) [附件下载]
高仿Android版手机QQ首页侧滑菜单源码 [附件下载]
开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载]
分享java AMR音频文件合并源码,全网最全
微信团队原创Android资源混淆工具:AndResGuard [有源码]
一个基于MQTT通信协议的完整Android推送Demo [附件下载]
Android版高仿微信聊天界面源码 [附件下载]
高仿手机QQ的Android版锁屏聊天消息提醒功能 [附件下载]
高仿iOS版手机QQ录音及振幅动画完整实现 [源码下载]
Android端社交应用中的评论和回复功能实战分享[图文+源码]
Android端IM应用中的@人功能实现:仿微博、QQ、微信,零入侵、高可扩展[图文+源码]
仿微信的IM聊天时间显示格式(含iOS/Android/Web实现)[图文+源码]



(本文同步发布于:http://www.52im.net/thread-2671-1-1.html

收起阅读 »

从零开始开发IM(即时通讯)服务端

IM
精选:★→2020年最新的常问企业面试题大全以及答案 作者:yuanrw 原文地址 https://juejin.im/post/5d6b3949f265da03c34c13e5 好消息:IM1.0.0 版本已经上线啦,支持特性: ...
继续阅读 »

精选:★→2020年最新的常问企业面试题大全以及答案



作者:yuanrw


原文地址 https://juejin.im/post/5d6b3949f265da03c34c13e5



好消息:IM1.0.0 版本已经上线啦,支持特性



  • 私聊发送文本 / 文件


  • 已发送 / 已送达 / 已读回执


  • 支持使用 ldap 登录


  • 支持接入外部的登录认证系统


  • 提供客户端 jar 包,方便客户端开发
    github 链接: github.com/yuanrw/IM



前言


首先讲讲 IM(即时通讯)技术可以用来做什么:
聊天:qq、微信
直播:斗鱼直播、抖音
实时位置共享、游戏多人互动等等
可以说几乎所有高实时性的应用场景都需要用到 IM 技术。


本篇将带大家从零开始搭建一个轻量级的 IM 服务端,麻雀虽小,五脏俱全,我们搭建的 IM 服务端实现以下功能



  1. 一对一的文本消息、文件消息通信


  2. 每个消息有 “已发送”/“已送达”/“已读” 回执


  3. 存储离线消息


  4. 支持用户登录,好友关系等基本功能。


  5. 能够方便地水平扩展



通过这个项目能学到什么?


这个项目涵盖了很多后端必备知识



  • rpc 通信


  • 数据库


  • 缓存


  • 消息队列


  • 分布式、高并发的架构设计


  • docker 部署



消息通信


文本消息


我们先从最简单的特性开始实现:一个普通消息的发送
消息格式如下:


message ChatMsg{
id = 1;
//消息id
fromId = Alice
//发送者userId
destId = Bob
//接收者userId
msgBody = hello
//消息体
}
复制代码


如上图,我们现在有两个用户:Alice 和 Bob 连接到了服务器,当 Alice 发送消息


message(hello)


给 Bob,服务端接收到消息,根据消息的 destId 进行转发,转发给 Bob。


发送回执


那我们要怎么来实现回执的发送呢?
我们定义一种回执数据格式 ACK,MsgType 有三种,分别是 sent(已发送), delivered(已送达), read(已读):


message AckMsg {
id;
//消息id
fromId;
//发送者id
destId;
//接收者id
msgType;
//消息类型
ackMsgId;
//确认的消息id
}
enum MsgType {
DELIVERED;
READ;
}
复制代码

当服务端接受到 Alice 发来的消息时:



  1. 向 Alice 发送一个 sent(hello)表示消息已经被发送到服务器。



message AckMsg {
id = 2;
fromId = Alice;
destId = Bob;
msgType = SENT;
ackMsgId = 1;
}
复制代码



  1. 服务器把



hello


转发给 Bob 后,立刻向 Alice 发送


delivered(hello)


表示消息已经发送给 Bob。


message AckMsg {
id = 3;
fromId = Bob;
destId = Alice;
msgType = DELIVERED;
ackMsgId = 1;
}
复制代码



  1. Bob 阅读消息后,客户端向服务器发送



read(hello)


表示消息已读


message AckMsg {
id = 4;
fromId = Bob;
destId = Alice;
msgType = READ;
ackMsgId = 1;
}
复制代码

这个消息会像一个普通聊天消息一样被服务器处理,最终发送给 Alice。



在服务器这里不区分 ChatMsg和 AckMsg,处理过程都是一样的:解析消息的 destId并进行转发。


水平扩展


当用户量越来越大,必然需要增加服务器的数量,用户的连接被分散在不同的机器上。此时,就需要存储用户连接在哪台机器上。
我们引入一个新的模块来管理用户的连接信息。


管理用户状态



模块叫做 user status,共有三个接口:


public interface UserStatusService {
/**
* 用户上线,存储userId与机器id的关系
*
* @param userId
* @param connectorId
* @return 如果当前用户在线,则返回他连接的机器id,否则返回null
*/
String online(String userId, String connectorId);
/**
* 用户下线
*
* @param userId
*/
void offline(String userId);
/**
* 通过用户id查找他当前连接的机器id
*
* @param userId
* @return
*/
String getConnectorId(String userId);
}
复制代码

这样我们就能够对用户连接状态进行管理了,具体的实现应考虑服务的用户量、期望性能等进行实现。
此处我们使用 redis 来实现,将 userId 和 connectorId 的关系以 key-value 的形式存储。


消息转发


除此之外,还需要一个模块在不同的机器上转发消息,如下结构:



此时我们的服务被拆分成了 connector和 transfer两个模块, connector模块用于维持用户的长链接,而 transfer的作用是将消息在多个 connector之间转发。
现在 Alice 和 Bob 连接到了两台 connector 上,那么消息要如何传递呢?



  1. Alice 上线,连接到 机器[1]上时




  • 将 Alice 和它的连接存入内存中。


  • 调用 user status的 online方法记录 Alice 上线。




  • Alice 发送了一条消息给 Bob




    • 机器[1]收到消息后,解析 destId,在内存中查找是否有 Bob。


    • 如果没有,代表 Bob 未连接到这台机器,则转发给 transfer



  • transfer调用 user status的 getConnectorId(Bob)方法找到 Bob 所连接的 connector,返回 机器[2],则转发给 机器[2]


  • 流程图:

    总结:



    • 引入 user status模块管理用户连接, transfer模块在不同的机器之间转发,使服务可以水平扩展。


    • 为了满足实时转发, transfer需要和每台 connector机器都保持长链接。


    离线消息

    如果用户当前不在线,就必须把消息持久化下来,等待用户下次上线再推送,这里使用 mysql 存储离线消息。
    为了方便地水平扩展,我们使用消息队列进行解耦



    • transfer接收到消息后如果发现用户不在线,就发送给消息队列入库。


    • 用户登录时,服务器从库里拉取离线消息进行推送。


    用户登录、好友关系

    用户的注册登录、账户管理、好友关系链等功能更适合使用 http 协议,因此我们将这个模块做成一个 restful 服务,对外暴露 http 接口供客户端调用。

    至此服务端的基本架构就完成了:

    总结

    以上就是这篇博客的所有内容,本篇帮大家构建了 IM 服务端的架构,但还有很多细节需要我们去思考,例如:



    • 如何保证消息的顺序和唯一


    • 多个设备在线如何保证消息一致性


    • 如何处理消息发送失败


    • 消息的安全性


    • 如果要存储聊天记录要怎么做


    • 数据库分表分库


    • 服务高可用
      ……


    更多细节实现就留到下一篇啦~

    IM1.0.0 版本已上线,github 链接: github.com/yuanrw/IM
    觉得对你有帮助请点个 star 吧~!

    (完)

    推荐阅读

    Spring Boot整合JWT实现用户认证(附源码)

    IDEA新特性:提前知道代码怎么走

    还在担心写的一手烂SQL,送你4款工

    瞬间几千次的重复提交,我用 SpringBoot+Redis 扛住了

    爬了知乎“神回复”,笑得根本停不下来

    这 17 个 JVM 参数,高级 Java 必须掌握!


    好文!必须点赞


收起阅读 »

iOS-编译过程

编译器iOS编译和打包时,编译器直接将代码编译成机器码,然后直接在CPU上运行。而不用使用解释器运行代码。因为这样执行效率更高,运行速度更快。C,C++,OC都是使用的编译器生成相关的可执行文件。解释器:解释器会在运行时解释执行代码,获取一段代码后就会将其翻译...
继续阅读 »

编译器

iOS编译和打包时,编译器直接将代码编译成机器码,然后直接在CPU上运行。而不用使用解释器运行代码。因为这样执行效率更高,运行速度更快。C,C++,OC都是使用的编译器生成相关的可执行文件。

解释器:解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码(就是字节码(Bytecode)),然后一句一句地执行目标代码。也就是说是在运行时才去解析代码,比直接运行编译好的可执行文件自然效率就低,但是跑起来之后可以不用重启启动编译,直接修改代码即可看到效果,类似热更新,可以帮我们缩短整个程序的开发周期和功能更新周期。

编译器:把一种编程语言(原始语言)转换为另一种编程语言(目标语言)的程序叫做编译器

采用编译器生成机器码执行的好处是效率高,缺点是调试周期长。
解释器执行的好处是编写调试方便,缺点是执行效率低。

编译器分为前端和后端
  • 前端:前端负责语法分析、词法分析,生成中间代码
  • 后端:后端以中间代码作为输入,进行架构无关的代码优化,接着针对不同架构生成不同的机器码
    在2007年之前LLVM使用GCC作为前端来对用户程序进行语义分析产生 IF(Intermidiate Format)。GCC系统庞大而笨重,因此,Apple决定从零开始写C、C++、Objective-C语言的前端Clang,以求完全替代掉GCC。
现在苹果公司使用的编译器是 LLVM,前端是Clang,相比于 Xcode 5 版本前使用的 GCC,编译速度提高了 3 倍。同时,苹果公司也反过来主导了 LLVM 的发展,让 LLVM 可以针对苹果公司的硬件进行更多的优化。
Clang于2007年开始开发,C编译器最早完成,在2009年的时候,Objective-C编译器已经完全可以用于生产环境,而在一年之后,Clang基本实现了对C++编译的支持。

对于Apple来说Objective C/C/C++使用的编译器前端是clang,后端都是LLVM

LLVM 是编译器工具链技术的一个集合。而其中的 lld 项目,就是内置链接器。编译器会对每个文件进行编译,生成 Mach-O(可执行文件);链接器会将项目中的多个 Mach-O 文件合并成一个。

编译过程

  • 预处理:Clang会预处理你的代码,比如把宏嵌入到对应的位置、注释被删除,条件编译被处理
  • 词法分析:词法分析器读入源文件的字符流,将他们组织称有意义的词素(lexeme)序列,对于每个词素,此法分析器产生词法单元(token)作为输出。并且会用Loc来记录位置。
  • 语法分析:这一步是把词法分析生成的标记流,解析成一个抽象语法树(abstract syntax tree -- AST),同样地,在这里面每一节点也都标记了其在源码中的位置。
    AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查。
  • 静态分析:把源码转化为抽象语法树之后,编译器就可以对这个树进行静态分析处理。静态分析会对代码进行错误检查,如出现方法被调用但是未定义、定义但是未使用的变量等,以此提高代码质量。当然,还可以通过使用 Xcode 自带的静态分析工具(Product -> Analyze)进行手动分析。最后 AST 会生成 IR,IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。静态分析的阶段会进行类型检查,比如给属性设置一个与其自身类型不相符的对象,编译器会给出一个可能使用不正确的警告。在此阶段也会检查时候有未使用过的变量等。
  • 中间代码生成和优化:此阶段LLVM 会对代码进行编译优化,例如针对全局变量优化、循环优化、尾递归优化等,最后输出汇编代码xx.ll文件。
    生成汇编代码: 汇编器LLVM会将汇编码转为机器码。此时的代码就是.o文件,即二进制文件。
  • 链接:连接器把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件。mach-o文件级可执行文件。编译过程全部结束,生成了可执行文件Mach-O

连接器

Mach-O 文件里面的内容,主要就是代码和数据:代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。不管是代码还是数据,它们的实例都需要由符号将其关联起来。

为什么呢?因为 Mach-O 文件里的那些代码,比如 if、for、while 生成的机器指令序列,要操作的数据会存储在某个地方,变量符号就需要绑定到数据的存储地址。你写的代码还会引用其他的代码,引用的函数符号也需要绑定到该函数的地址上。而链接器的作用,就是完成变量、函数符号和其地址绑定这样的任务。而这里我们所说的符号,就可以理解为变量名和函数名。

那为什么要让链接器做符号和地址绑定这样一件事儿呢?

如果地址和符号不做绑定的话,要让机器知道你在操作什么内存地址,你就需要在写代码时给每个指令设好内存地址。写这样的代码的过程,就像你直接在和不同平台的机器沟通,连编译生成 AST 和 IR 的步骤都省掉了,甚至优化平台相关的代码都需要你自己编写。

可读性和可维护性都会很差,比如修改代码后对地址的维护就会让你崩溃。而这种“崩溃”的罪魁祸首就是代码和内存地址绑定得太早。
用汇编语言来让这种绑定滞后。随着编程语言的进化,我们很快就发现,采用任何一种高级编程语言,都可以解决代码和内存绑定过早产生的问题,同时还能扫掉使用汇编写程序的烦恼。

链接器为什么还要把项目中的多个 Mach-O 文件合并成一个

项目中文件之间的变量和接口函数都是相互依赖的,所以这时我们就需要通过链接器将项目中生成的多个 Mach-O 文件的符号和地址绑定起来。

没有这个绑定过程的话,单个文件生成的 Mach-O 文件是无法正常运行起来的。因为,如果运行时碰到调用在其他文件中实现的函数的情况时,就会找不到这个调用函数的地址,从而无法继续执行。

链接器在链接多个目标文件的过程中,会创建一个符号表,用于记录所有已定义的和所有未定义的符号。链接时如果出现相同符号的情况,就会出现“ld: dumplicate symbols”的错误信息;如果在其他目标文件里没有找到符号,就会提示“Undefined symbols”的错误信息。

链接器做了什么
  • 在项目文件中查找目标代码文件里没有定义的变量
  • 扫描项目中的不同文件,将所有符号定义和引用地址收集起来,并放到全局符号表中
  • 计算合并后长度及位置,生成同类型的段进行合并,建立绑定
  • 对项目中不同文件里的变量进行地址重定位
  • 去除无用函数:链接器在整理函数的调用关系时,会以 main 函数为源头,跟随每个引用,并将其标记为 live。跟随完成后,那些未被标记 live 的函数,就是无用函数。然后,链接器可以通过打开 Dead code stripping 开关,来开启自动去除无用代码的功能。并且,这个开关是默认开启的。

动态库链接

在真实的 iOS 开发中,你会发现很多功能都是现成可用的,比如 系统库、GUI 框架、I/O、网络等。链接这些共享库到你的 Mach-O 文件,也是通过链接器来完成的。

链接的共用库分为静态库和动态库:静态库是编译时链接的库,需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新;而动态库是运行时链接的库,使用 dyld 就可以实现动态加载。
Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen 也可以选择是立刻解析所有引用还是滞后去做。dlopen 打开动态库后返回的是引用的指针,dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用。

使用 dyld 加载动态库,有两种方式:有程序启动加载时绑定和符号第一次被用到时绑定。为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。
加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。

dylib 这种格式,表示是动态链接的,编译的时候不会被编译到执行文件中,在程序执行的时候才 link,这样就不用算到包大小里,而且不更新执行程序就能够更新库。
系统上的动态链接器会使用共享缓存,共享缓存在 /var/db/dyld/。当加载 Mach-O 文件时,动态链接器会先检查是否有共享缓存。每个进程都会在自己的地址空间映射这些共享缓存,这样做可以起到优化 App 启动速度的作用。

编译和启动速度

编译阶段由于有了链接器,代码可以写在不同的文件里,每个文件都能够独立编成 Mach-O 文件进行标记。编译器可以根据你修改的文件范围来减少编译,通过这种方式提高每次编译的速度。

这也是为什么文件越多,链接器链接 Mach-O 文件所需绑定的遍历操作就会越多,编译速度也会越慢。
开发时启动优化:对于大型APP项目在开发调试阶段,是不是代码改完以后可以先不去链接项目里的所有文件,只编译当前修改的文件动态库,通过运行时加载动态库及时更新,看到修改的结果。这样调试的速度,不就能够得到质的提升了么?



转自链接:https://www.jianshu.com/p/d946961551b0
收起阅读 »

iOS- 核心动画分类以及基本使用

1、UIView和核心动画区别?核心动画只能添加到CALayer, 核心动画一切都是假象,并不会改变真实的值。如果需要与用户交互就使用UIView的动画. 不需要与用户交互可以使用核心动画。在转场动画中,核心动画的类型比较多。根据⼀个路径做动画,只能用核心动画...
继续阅读 »

1、UIView和核心动画区别?

核心动画只能添加到CALayer, 核心动画一切都是假象,并不会改变真实的值。如果需要与用户交互就使用UIView的动画. 不需要与用户交互可以使用核心动画。

在转场动画中,核心动画的类型比较多。根据⼀个路径做动画,只能用核心动画(帧动画) 、动画组:同时做多个动画。

2、核心动画的分类

核心动画继承结构
图中的黑色虚线代表“继承”某个类,红色虚线代表“遵守”某个协议


CAAnimation是所有动画对象的父类,负责控制动画的持续时间和速度,是个抽象类,不能直接使用,应该使用它具体的子类
属性说明:(红色代表来自CAMediaTiming协议的属性)

  • duration:动画的持续时间
  • repeatCount:重复次数,无限循环可以设置HUGE_VALF或者MAXFLOAT
  • repeatDuration:重复时间
  • removedOnCompletion:默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillModekCAFillModeForwards

    -fillMode:决定当前对象在非active时间段的行为。比如动画开始之前或者动画结束之后


  • beginTime:可以用来设置动画延迟执行时间,若想延迟2s,就设置为CACurrentMediaTime()+2CACurrentMediaTime()为图层的当前时间


  • timingFunction:速度控制函数,控制动画运行的节奏


  • delegate:动画代理

  • fillMode属性值(要想fillMode有效,最好设置removedOnCompletion = NO
    kCAFillModeRemoved这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
    kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态
    kCAFillModeBackwards 在动画开始前,只需要将动画加入了一个layer,layer便立即进入动画的初始状态并等待动画开始。
    kCAFillModeBoth这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态

    速度控制函数(CAMediaTimingFunction)
    kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉
    kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开
    kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地
    kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。
    CAAnimationDelegate代理方法只是监听动画的开始和结束
@protocol CAAnimationDelegate <NSObject>
@optional

/* Called when the animation begins its active duration. */

- (void)animationDidStart:(CAAnimation *)anim;

/* Called when the animation either completes its active duration or
* is removed from the object it is attached to (i.e. the layer). 'flag'
* is true if the animation reached the end of its active duration
* without being removed. */

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;

@end

CALayer上动画的暂停和恢复

- (void)pauseLayerAnimation:(CALayer *)layer {
CFTimeInterval pauseTimes = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
// speed动画的运行速度,当为0时会停止动画,speed越大说明动画执行速度越快
layer.speed = 0.0;
// 让layer的时间停留在pauseTimes
// 动画的时间偏移,也就是上次动画的暂停/继续 距离本次动画的继续/暂停的时间差
layer.timeOffset = pauseTimes;
NSLog(@":pauseTimes:%f", pauseTimes);
}
- (void)resumeLayerAnimation:(CALayer *)layer {
CFTimeInterval pauseTimes = layer.timeOffset;
//让CALayer的时间继续行走
layer.speed = 1;
// 取消上次记录的停留时刻
layer.timeOffset = 0.0;
//取消上次设置的时间
layer.beginTime = 0.0;

CFTimeInterval timeSincePause = CACurrentMediaTime()-pauseTimes;
layer.beginTime = timeSincePause;

NSLog(@":timeSincePause:%f", timeSincePause);

}


第一次暂停和开始是正常的,之后就会出现偏移,暂时搞不清楚是什么原因???????????????

原因:

-(CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer)l; -(CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer)l;这两个方法混用了

2、CABasicAnimation——基本动画


基本动画,是CAPropertyAnimation的子类


属性说明:

fromValue:keyPath相应属性的初始值

toValue:keyPath相应属性的结束值

动画过程说明:
随着动画的进行,在长度为duration的持续时间内,keyPath相应属性的值从fromValue渐渐地变为toValue
keyPath内容是CALayer的可动画Animatable属性
如果fillMode==kCAFillModeForwards同时removedOnComletion=NO,那么在动画执行完毕后,图层会保持显示动画执行后的状态。但在实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变。
CABasicAnimation *basicAnim = [CABasicAnimation animation];

basicAnim.keyPath = @"position.y";
basicAnim.fromValue = @(self.redView.layer.position.y);
basicAnim.toValue = @(self.redView.layer.position.y + 300);

basicAnim.duration = 2.0;
// 动画完成时不移除动画
basicAnim.removedOnCompletion = NO;
// 动画完成时保持最后的状态
/**
kCAFillModeRemoved 这个是默认值,也就是说当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态
kCAFillModeBackwards 在动画开始前,只需要将动画加入了一个layer,layer便立即进入动画的初始状态并等待动画开始。
kCAFillModeBoth 这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态
*/
basicAnim.fillMode = kCAFillModeForwards;
// 动画重复次数
basicAnim.repeatCount = MAXFLOAT;
// 自动返转(怎么去,怎么返回)
basicAnim.autoreverses = YES;

basicAnim.delegate = self;
// 延迟2S开始动画
basicAnim.beginTime = CACurrentMediaTime()+2;

[self.redView.layer addAnimation:basicAnim forKey:nil];

3、CAKeyframeAnimation——关键帧动画


关键帧动画,也是CAPropertyAnimation的子类,与CABasicAnimation的区别是:

CABasicAnimation只能从一个数值(fromValue)变到另一个数值(toValue),而

CAKeyframeAnimation会使用一个NSArray保存这些数值。

属性说明:values:上述的·NSArray·对象。里面的元素称为“关键帧”(keyframe)。动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧


path:可以设置一个CGPathRef、CGMutablePathRef,让图层按照路径轨迹移动。path只对CALayeranchorPointposition起作用。如果设置了path,那么values将被忽略

keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0,keyTimes中的每一个时间值都对应values中的每一帧。如果没有设置keyTimes,各个关键帧的时间是平分的

CABasicAnimation可看做是只有2个关键帧的CAKeyframeAnimation
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:self.iconImageView.center radius:100 startAngle:0 endAngle:M_PI clockwise:YES];

CAKeyframeAnimation *keyFrameAnima = [CAKeyframeAnimation animation];
keyFrameAnima.path = path.CGPath;

keyFrameAnima.keyPath = @"position";
keyFrameAnima.duration = 2;
keyFrameAnima.repeatCount = MAXFLOAT;
keyFrameAnima.autoreverses = YES;
[self.iconImageView.layer addAnimation:keyFrameAnima forKey:nil];

4、CAAnimationGroup——动画组

动画组,是CAAnimation的子类,可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行

  • 属性说明:
    animations:用来保存一组动画对象的NSArray
    默认情况下,一组动画对象是同时运行的,也可以通过设置动画对象的beginTime属性来更改动画的开始时间
- (void)addAnimationGrounp {
// 创建一个基础动画
CABasicAnimation *basicAnima = [CABasicAnimation animation];
basicAnima.keyPath = @"transform.scale";
basicAnima.fromValue = @1.0;
basicAnima.toValue = @0;

// 创建一个帧动画
CAKeyframeAnimation *keyframeAnim = [CAKeyframeAnimation animation];
keyframeAnim.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(170, self.redView.center.y) radius:100 startAngle:M_PI endAngle:M_PI*2 clockwise:YES].CGPath;

keyframeAnim.keyPath = @"position";

// 动画组
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = @[basicAnima,keyframeAnim];
animGroup.duration = 2.0;
animGroup.repeatCount = MAXFLOAT;
animGroup.autoreverses = YES;
[self.redView.layer addAnimation:animGroup forKey:nil];
}

5、CATransition——转场动画

CATransition是CAAnimation的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果。UINavigationController就是通过CATransition`实现了将控制器的视图推入屏幕的动画效果

如果父视图中的两个子视图互相切换,转场动画应加给父视图!

动画属性:

  • type:动画过渡类型
  • subtype:动画过渡方向
  • startProgress:动画起点(在整体动画的百分比)
  • endProgress:动画终点(在整体动画的百分比)
CATransition *transiont = [CATransition animation];
/**
fade , //淡入淡出
push, //推挤
reveal, //揭开
moveIn, //覆盖
cube, //立方体
suckEffect, //吮吸
oglFlip, //翻转
rippleEffect, //波纹(水滴)
pageCurl, //翻页
pageUnCurl, //反翻页
cameraIrisHollowOpen, //开镜头
cameraIrisHollowClose, //关镜头
*/
transiont.type =@"pageCurl";
transiont.subtype = @"fromBottom";
transiont.duration = 0.5;

// 动画从哪个点开始
transiont.startProgress = 0;
// 动画到哪个点结束
transiont.endProgress = 0.5;


[self.imageView.layer addAnimation:transiont forKey:nil];

转场动画的过渡效果如下:


收起阅读 »

iOS--开发中遇到的der,csr,crt,p12,pem文件到底是什么

关于pem文件的话,上一篇文章已经有提到:iOS---利用OpenSSL演示RSA加密解密,PEM在工作中,pem文件,我们是不会直接使用的,需要从pem文件里面提取csr文件。csr文件步骤1:从private.pem文件里,提取rsacert.csr文件,...
继续阅读 »

关于pem文件的话,上一篇文章已经有提到:iOS---利用OpenSSL演示RSA加密解密,PEM

在工作中,pem文件,我们是不会直接使用的,需要从pem文件里面提取csr文件。

csr文件

步骤1:从private.pem文件里,提取rsacert.csr文件,终端命令“openssl req -new -key private.pem -out rsacert.csr”


这个步骤是不是似曾相识,这个步骤我们也可以在钥匙串里面,从证书颁发机构创建这个文件。

总结。csr文件,:是请求证书文件,是用于从证书颁发机构请求证书用的文件。
(趣味:麻省理工设计RSA算法的三位数学家成立了一个机构,收费帮别的组织签名赚钱,一般收费是5000一年)

crt文件

步骤2:利用命令“openssl x509 -req -days 3650 -in rsacert.csr -signkey private.pem -out rsacert.crt”自己签名。
-days 3650 代表有效期10年,(5万块到手)
-in rsacert.csr 传递一个文件
-signkey private.pem 代表用私钥private.pem文件进行签名。


总结。crt文件,是用于从证书颁发机构签过名的文件。https就需要这个文件,放在自己服务器上用于别人接收。是一个base64格式的。

der文件

步骤3:利用命令“openssl x509 -outform der -in rsacert.crt -out rsacert.der”生成一个rsacert.der文件

总结。der文件主要包括就是公钥和一些信息。

p12文件

步骤4:利用命令“openssl pkcs12 -export -out p.p12 -inkey private.pem -in rsacert.crt”生成一个p.p12文件
-in rsacert.crt 我们从rsacert.crt 生成了 der文件, 同时,也从rsacert.crt 里面生成p12文件。
pkcs12 原来,p12是pkcs12的缩写。

输入密码的时候,是不是想起来,导出p12文件的时候,有一个输入密码的操作。


总结。p12文件主要包括就是私钥和一些信息。

这便是我们经常接触到的,钥匙串帮我们做的事情,通过命令行演示,如果有问题,请指出来,及时修改。

转自:https://www.jianshu.com/p/83b67244458a

收起阅读 »

Swift高级分享 - 在Swift中提取视图控制器操作

视图控制器往往在为Apple平台构建的大多数应用程序中起着非常重要的作用。他们管理我们UI的关键方面,提供系统功能的桥梁,如设备方向和状态栏外观,并经常响应用户交互 - 如按钮点击和文本输入。由于它们通常具有这样的关键作用,因此许多视图控制器最终遭受常见的大规...
继续阅读 »

视图控制器往往在为Apple平台构建的大多数应用程序中起着非常重要的作用。他们管理我们UI的关键方面,提供系统功能的桥梁,如设备方向和状态栏外观,并经常响应用户交互 - 如按钮点击和文本输入。

由于它们通常具有这样的关键作用,因此许多视图控制器最终遭受常见的大规模视图控制器问题并不奇怪- 当它们最终承担太多责任时,导致大量交织在一起的逻辑,通常与视图混合在一起和布局代码。

虽然我们已经探索了多种减轻和分解大视图控制器的方法 - 例如使用合成,将导航代码移动到专用类型,重用数据源和使用逻辑控制器 - 本周,我们来看一下技术这让我们可以提取视图控制器的核心操作,而无需引入任何其他抽象或架构概念。

尴尬的意识

许多类型的架构和结构问题的一个非常常见的根本原因是某些类型只是意识到太多的域和细节。当给定类型的“意识领域”增长时,通常会履行其职责,并且 - 作为直接影响 - 它包含的代码量。

假设我们正在为消息传递应用程序构建一个作曲家视图,为了能够从用户的联系人中添加收件人并启用消息发送,我们目前允许我们的视图控制器直接访问我们的数据库和网络代码:

class MessageComposerViewController: UIViewController {
private var message: Message
private let userDatabase: UserDatabase
private let networking: Networking

init(recipients: [Recipient],
userDatabase: UserDatabase,
networking: Networking) {
self.message = Message(recipients: recipients)
self.userDatabase = userDatabase
self.networking = networking
super.init(nibName: nil, bundle: nil)
}
}

上面可能看起来不是什么大问题 - 我们使用依赖注入,并不像我们的视图控制器有大量依赖。然而,当我们的视图控制器还没有变成一个巨大的一个,只是还没有,它确实有它需要处理的动作相当多的数量-如添加收件人,取消和发送邮件-它目前正在对所有的它自己的:

private extension MessageComposerViewController {
func handleAddRecipientButtonTap() {
let picker = RecipientPicker(database: userDatabase)

picker.present(in: self) { [weak self] recipient in
self?.message.recipients.append(recipient)
self?.renderRecipientsView()
}
}

func handleCancelButtonTap() {
if message.text.isEmpty {
dismiss(animated: true)
} else {
dismissAfterAskingForConfirmation()
}
}

func handleSendButtonTap() {
let sender = MessageSender(networking: networking)

sender.send(message) { [weak self] error in
if let error = error {
self?.display(error)
} else {
self?.dismiss(animated: true)
}
}
}
}

让视图控制器执行自己的操作可能非常方便,对于更简单的视图控制器,它很可能不会导致任何问题 - 但正如我们只看到上面的摘录所看到的那样MessageComposerViewController,它通常需要我们的视图控制器知道他们理想情况下不应过于关注的事情 - 例如网络,创建逻辑对象,以及对父母如何呈现它们做出假设。

由于大多数视图控制器已经非常忙于创建和管理视图,设置布局约束以及检测用户交互等内容 - 让我们看看我们是否可以提取上述操作,并使我们的视图控制器更简单(并且不太清楚)处理。

操作

动作通常有两种不同的变体 - 同步和异步。某些操作只需要我们快速处理或转换给定值,并直接返回,而其他操作则需要更多时间来执行。

为了对这两种动作进行建模,让我们创建一个通用Action枚举 - 实际上并没有任何情况 - 但是包含两个类型别名,一个用于同步动作,一个用于异步动作:

enum Action<I, O> {
typealias Sync = (UIViewController, I) -> O
typealias Async = (UIViewController, I, @escaping (O) -> Void) -> Void
}

我们使用的原因,enum我们的Action包装上面,以防止它被实例化的类型,而不是只充当一个“抽象的命名空间”。

使用上面的类型别名,我们现在可以定义一个元组,其中包含我们MessageComposerViewController可以执行的所有操作- 如下所示:

private extension MessageComposerViewController {
func handleAddRecipientButtonTap() {
actions.addRecipient(self, message) { [weak self] newMessage in
self?.message = newMessage
self?.renderRecipientsView()
}
}

func handleCancelButtonTap() {
actions.cancel(self, message)
}

func handleSendButtonTap() {
let loadingVC = add(LoadingViewController())

actions.finish(self, message) { [weak self] error in
loadingVC.remove()
error.map { self?.display($0) }
}
}
}

值得注意的是,作为此重构的一部分,我们还改进了收件人添加到邮件的方式。我们不是让视图控制器本身执行其模型的变异,而是简单地返回一个新Message值作为其addRecipient动作的结果。

上述方法的优点在于我们的视图控制器现在可以专注于视图控制器最擅长的 - 控制视图 - 并让创建它的上下文处理网络和呈现等细节RecipientPicker。以下是我们现在可以在另一个视图控制器上呈现消息编写器的方法,例如在协调器或导航器中:

func presentMessageComposerViewController(
for recipients: [Recipient],
in presentingViewController: UIViewController
) {
let composer = MessageComposerViewController(
recipients: recipients,
actions: (
addRecipient: { [userDatabase] vc, message, handler in
let picker = RecipientPicker(database: userDatabase)

picker.present(in: vc) { recipient in
var message = message
message.recipients.append(recipient)
handler(message)
}
},
cancel: { vc, message in
if message.text.isEmpty {
vc.dismiss(animated: true)
} else {
vc.dismissAfterAskingForConfirmation()
}
},
finish: { [networking] vc, message, handler in
let sender = MessageSender(networking: networking)

sender.send(message) { error in
handler(error)

if error == nil {
vc.dismiss(animated: true)
}
}
}
)
)

presentingViewController.present(composer, animated: true)
}

太可爱了!由于我们所有的视图控制器的操作现在都只是函数,因此我们的代码变得更加灵活,更容易测试 - 因为我们可以轻松地模拟行为并验证在各种情况下调用正确的操作。

可操作的概述

从私有方法和专用集合中提取操作的另一大好处是,可以更容易地了解给定视图控制器执行的操作类型 - 例如ProductViewController,这具有四个同步的非常清晰的列表和异步操作:

extension ProductViewController {
typealias Actions = (
load: Action<Product.ID, Result<Product, Error>>.Async,
purchase: Action<Product.ID, Error?>.Async,
favorite: Action<Product.ID, Void>.Sync,
share: Action<Product, Void>.Sync
)
}

添加对新操作的支持通常也变得非常简单,因为我们不必为每个视图控制器注入新的依赖项并编写特定的实现,我们可以更轻松地利用共享逻辑并简单地向我们的Actions元组添加新成员- 然后在调用时调用它发生了相应的用户交互。

最后,操作可以实现类型自定义和更简单的重构等功能,而无需通常需要的“仪式”来解锁此类功能 - 例如,在使用协议时,或切换到新的,更严格的架构设计模式时。

例如,假设我们想要MessageComposerViewController从之前返回到我们,并添加对保存未完成消息草稿的支持。我们现在可以实现整个功能,甚至无需触及我们的实际视图控制器代码 - 我们所要做的就是更新其cancel操作:

let composer = MessageComposerViewController(
recipients: recipients,
actions: (
...
cancel: { [draftManager] vc, message in
if message.text.isEmpty {
vc.dismiss(animated: true)
} else {
vc.presentConfirmation(forReason: .saveDraft) {
outcome in
switch outcome {
case .accepted:
draftManager.saveDraft(message)
vc.dismiss(animated: true)
case .rejected:
vc.dismiss(animated: true)
case .cancelled:
break
}
}
}
},
...
)
)

结论


在处理复杂的视图控制器时,没有“银色子弹” - 特别是那些已经超出其原始意识和责任范围的控制器。像往常一样,拥有多种不同的技术 - 并在最合适的地方部署它们 - 通常是以高效和务实的方式创建真正强大的系统的关键。


虽然更复杂的技术(如使用逻辑控制器视图模型,或使用协议分离关注点)非常适合我们想要在整个代码库中进行更加结构化的,根本性的更改 - 提取操作可以大大有助于我们的视图控制器更简单,不需要任何重大改变或新的抽象。

链接:https://www.jianshu.com/p/558dfb843688
收起阅读 »

ES6 exports 与 import 使用

在创建JavaScript模块时,export 用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import使用它们。被导出的绑定值依然可以在本地进行修改。在使用import 进行导入时,这些绑定值只能被导入模块所读取,...
继续阅读 »

在创建JavaScript模块时,export 用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import使用它们。
被导出的绑定值依然可以在本地进行修改。
在使用import 进行导入时,这些绑定值只能被导入模块所读取,但在 export 导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。

exports

ES6模块只支持静态导出,只可以在模块的最外层作用域使用export,不可在条件语句与函数作用域中使用。

Named exports (命名导出)

这种方式主要用于导出多个函数或者变量, 明确知道导出的变量名称。
使用:只需要在变量或函数前面加 export 关键字即可。
使用场景:比如 utils、tools、common 之类的工具类函数集,或者全站统一变量

  1. export 后面不可以是表达式,因为表达式只有值,没有名字。
  2. 每个模块包含任意数量的导出。
// lib.js
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}


// index.js 使用方式1
import { square, diag } from 'lib';
console.log(square(11)); // 121

// index.js 使用方式2
import * as lib from 'lib';
console.log(lib.square(11)); // 121

简写格式,统一列出需要输出的变量,例如上面的lib.js可以改写成

// lib.js
const sqrt = Math.sqrt;
function square(x) {
return x * x;
}
function add (x, y) {
return x + y;
}
export { sqrt, square, add };

Default exports (默认导出)

这种方式主要用于导出类文件或一个功能比较单一的函数文件;
使用:只需要在变量或函数前面加 export default 关键字即可。

  1. 每个模块最多只能有一个默认导出;
  2. 默认导出可以视为名字是default的模块输出变量;
  3. 默认导出后面可以是表达式,因为它只需要值。

导出一个值:

export default 123;

导出一个函数:

// myFunc.js
export default function () { ... };

// index.js
import myFunc from 'myFunc';
myFunc();

导出一个类:

// MyClass.js
class MyClass{
constructor() {}
}
export default MyClass;
// 或者
export { MyClass as default, … };

// index.js
import MyClass from 'MyClass';

export default 与 export 的区别:

  • 不需要知道导出的具体变量名;
  • 导入【import】时不需要 { } 包裹;

Combinations exports (混合导出)

混合导出是 Named exports 和 Default exports 组合导出。

混合导出后,默认导入一定放在命名导入前面;
// lib.js
export const myValue = '';
export const MY_CONST = '';
export function myFunc() {
...
}
export function* myGeneratorFunc() {
...
}
export default class MyClass {
...
}

// index.js
import MyClass, { myValue, myFunc } from 'lib';

Re-exporting (别名导出)

一般情况下,export 导出的变量名是原文件中的变量名,但也可以用 as 关键字来指定别名。这样做是为了简化或者语义化 export 的函数名。

同一个变量允许使用不同名字输出多次
// lib.js
function getName() {
...
};
function setName() {
...
};

export {
getName as get,
getName as getUserName,
setName as set
}

Module Redirects (中转模块导出)

为了方便使用模块导入,在一个父模块中“导入-导出”不同模块。简单来说:创建单个模块,集中多个模块的多个导出。
使用:使用 export from 语法实现;

export * from 'lib'; // 没有设置 export default
export * as myFunc2 from 'myFunc'; // 【ES2021】没有设置 export default
export { default as function1, function2 } from 'bar.js';

上述例子联合使用导入和导出:

import { default as function1, function2 } from 'bar.js';
export { function1, function2 };

尽管此时 export 与 import 等效,但以下语法在语法上无效:

import DefaultExport from 'bar.js'; // 有效的
export DefaultExport from 'bar.js'; // 无效的

正确的做法是重命名这个导出:

export { default as DefaultExport } from 'bar.js';

Importing

// Named imports
import { foo, bar as b } from './some-module.mjs';

// Namespace import
import * as someModule from './some-module.mjs';

// Default import
import someModule from './some-module.mjs';

// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, { foo, bar as b } from './some-module.mjs';

// Empty import (for modules with side effects)
import './some-module.mjs';


原文:https://segmentfault.com/a/1190000039957496

收起阅读 »

开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现(4)

调试 我们先来看看连接及重连部分(由于录制gif比较麻烦,体积较大,所以我先把重连间隔调小成3秒,方便看效果)。 启动服务端: 启动客户端: 可以看到,正常的情况下已经连接成功了,接下来,我们来试一下异常情况,比如服务端没启动,看看客户端的重连情况: 这...
继续阅读 »

调试


我们先来看看连接及重连部分(由于录制gif比较麻烦,体积较大,所以我先把重连间隔调小成3秒,方便看效果)。



  • 启动服务端:启动服务端

  • 启动客户端:启动客户端


可以看到,正常的情况下已经连接成功了,接下来,我们来试一下异常情况,比如服务端没启动,看看客户端的重连情况:
调试重连
这次我们先启动的是客户端,可以看到连接失败后一直在进行重连,由于录制gif比较麻烦,在第三次连接失败后,我启动了服务端,这个时候客户端就会重连成功。


然后,我们再来调试一下握手认证消息即心跳消息:
握手消息及心跳消息测试
可以看到,长连接建立成功后,客户端会给服务端发送一条握手认证消息(1001),服务端收到握手认证消息会,给客户端返回了一条握手认证状态消息,客户端收到握手认证状态消息后,即启动心跳机制。gif不太好演示,下载demo就可以直观地看到。


接下来,在讲完消息重发机制及离线消息后,我会在应用层做一些简单的封装,以及在模拟器上运行,这样就可以很直观地看到运行效果。




消息重发机制


消息重发,顾名思义,即使对发送失败的消息进行重发。考虑到网络环境的不稳定性、多变性(比如从进入电梯、进入地铁、移动网络切换到wifi等),在消息发送的时候,发送失败的概率其实不小,这时消息重发机制就很有必要了。

我们先来看看实现的代码逻辑。
MsgTimeoutTimer:
MsgTimeoutTimer1
MsgTimeoutTimer2
MsgTimeoutTimerManager:
MsgTimeoutTimerManager1
MsgTimeoutTimerManager2

然后,我们看看收消息的TCPReadHandler的改造:
加入消息重发机制的TCPReadHandler
最后,看看发送消息的改造:
加入消息重发机制的发送消息


说一下逻辑吧:发送消息时,除了心跳消息、握手消息、状态报告消息外,消息都加入消息发送超时管理器,立马开启一个定时器,比如每隔5秒执行一次,共执行3次,在这个周期内,如果消息没有发送成功,会进行3次重发,达到3次重发后如果还是没有发送成功,那就放弃重发,移除该消息,同时通过消息转发器通知应用层,由应用层决定是否再次重发。如果消息发送成功,服务端会返回一个消息发送状态报告,客户端收到该状态报告后,从消息发送超时管理器移除该消息,同时停止该消息对应的定时器即可。

另外,在用户握手认证成功时,应该检查消息发送超时管理器里是否有发送超时的消息,如果有,则全部重发:
握手认证成功检查是否有发送超时的消息




离线消息


由于离线消息机制,需要服务端数据库及缓存上的配合,代码就不贴了,太多太多,我简单说一下实现思路吧:
客户端A发送消息到客户端B,消息会先到服务端,由服务端进行中转。这个时候,客户端B存在两种情况:



  • 1.长连接正常,就是客户端网络环境良好,手机有电,应用处在打开的情况。

  • 2.废话,那肯定就是长连接不正常咯。这种情况有很多种原因,比如wifi不可用、用户进入了地铁或电梯等网络不好的场所、应用没打开或已退出登录等,总的来说,就是没有办法正常接收消息。


如果是长连接正常,那没什么可说的,服务端直接转发即可。

如果长连接不正常,需要这样处理:服务端接收到客户端A发送给客户端B的消息后,先给客户端A回复一条状态报告,告诉客户端A,我已经收到消息,这个时候,客户端A就不用管了,消息只要到达服务端即可。然后,服务端先尝试把消息转发到客户端B,如果这个时候客户端B收到服务端转发过来的消息,需要立马给服务端回一条状态报告,告诉服务端,我已经收到消息,服务端在收到客户端B返回的消息接收状态报告后,即认为此消息已经正常发送,不需要再存库。如果客户端B不在线,服务端在做转发的时候,并没有收到客户端B返回的消息接收状态报告,那么,这条消息就应该存到数据库,直到客户端B上线后,也就是长连接建立成功后,客户端B主动向服务端发送一条离线消息询问,服务端在收到离线消息询问后,到数据库或缓存去查客户端B的所有离线消息,并分批次返回,客户端B在收到服务端的离线消息返回后,取出消息id(若有多条就取id集合),通过离线消息应答把消息id返回到服务端,服务端收到后,根据消息id从数据库把对应的消息删除即可。

以上是单聊离线消息处理的情况,群聊有点不同,群聊的话,是需要服务端确认群组内所有用户都收到此消息后,才能从数据库删除消息,就说这么多,如果需要细节的话,可以私信我。




不知不觉,NettyTcpClient中定义了很多变量,为了防止大家不明白变量的定义,还是贴上代码吧:
定义了很多变量的NettyTcpClient


应用层封装


这个就见仁见智啦,每个人代码风格不同,我把自己简单封装的代码贴上来吧:

MessageProcessor消息处理器:
MessageProcessor1
MessageProcessor2
IMSEventListener与ims交互的listener:
IMSEventListener1
IMSEventListener2
IMSEventListener3
MessageBuilder消息转换器:
MessageBuilder1
MessageBuilder2
MessageBuilder3
AbstractMessageHandler抽象的消息处理handler,每个消息类型对应不同的messageHandler:
AbstractMessageHandler
SingleChatMessageHandler单聊消息处理handler:
SingleChatMessageHandler
GroupChatMessageHandler群聊消息处理handler:
GroupChatMessageHandler
MessageHandlerFactory消息handler工厂:
MessageHandlerFactory
MessageType消息类型枚举:
MessageType
IMSConnectStatusListenerIMS连接状态监听器:
IMSConnectStatusListener
由于每个人代码风格不同,封装代码都有自己的思路,所以,在此就不过多讲解,只是把自己简单封装的代码全部贴上来,作一个参考即可。只需要知道,接收到消息时,会回调OnEventListener的dispatchMsg(MessageProtobuf.Msg msg)方法:
应用层接收ims消息入口
发送消息需要调用imsClient的sendMsg(MessageProtobuf.Msg msg)方法:
应用层调用ims发送消息入口
即可,至于怎样去封装得更好,大家自由发挥吧。




最后,为了测试消息收发是否正常,我们需要改动一下服务端:
改动后的服务端1
改动后的服务端2
改动后的服务端3
改动后的服务端4
改动后的服务端5
可以看到,当有用户握手成功后,会保存该用户对应的channel到容器里,给用户发送消息时,根据用户id从容器里取出对应的channel,利用该channel发送消息。当用户断开连接后,会把该用户对应的channel从容器里移除掉。


运行一下,看看效果吧:
最终运行效果



  • 首先,启动服务端。

  • 然后,修改客户端连接的ip地址为192.168.0.105(这是我本机的ip地址),端口号为8855,fromId,也就是userId,定义成100001,toId为100002,启动客户端A。

  • 再然后,fromId,也就是userId,定义成100002,toId为100001,启动客户端B。

  • 客户端A给客户端B发送消息,可以看到在客户端B的下面,已经接收到了消息。

  • 用客户端B给客户端A发送消息,也可以看到在客户端A的下面,也已经接收到了消息。


至于,消息收发测试成功。至于群聊或重连等功能,就不一一演示了,还是那句话,下载demo体验一下吧。。。


由于gif录制体积较大,所以只能简单演示一下消息收发,具体下载demo体验吧。。。


如果有需要应用层UI实现(就是聊天页及会话页的封装)的话,我再分享出来吧。



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

开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现(3)

TCP的拆包与粘包 什么是TCP拆包?为什么会出现TCP拆包? 简单地说,我们都知道TCP是以“流”的形式进行数据传输的,而且TCP为提高性能,发送端会将需要发送的数据刷入缓冲区,等待缓冲区满了之后,再将缓冲区中的数据发送给接收方,同理,接收方也会有缓冲区...
继续阅读 »

TCP的拆包与粘包




  • 什么是TCP拆包?为什么会出现TCP拆包?


    简单地说,我们都知道TCP是以“流”的形式进行数据传输的,而且TCP为提高性能,发送端会将需要发送的数据刷入缓冲区,等待缓冲区满了之后,再将缓冲区中的数据发送给接收方,同理,接收方也会有缓冲区这样的机制,来接收数据。

    拆包就是在socket读取时,没有完整地读取一个数据包,只读取一部分。




  • 什么是TCP粘包?为什么会出现TCP粘包?


    同上。

    粘包就是在socket读取时,读到了实际意义上的两个或多个数据包的内容,同时将其作为一个数据包进行处理。




引用网上一张图片来解释一下在TCP出现拆包、粘包以及正常状态下的三种情况,如侵请联系我删除:
TCP拆包、粘包、正常状态
了解了TCP出现拆包/粘包的原因,那么,如何解决呢?通常来说,有以下四种解决方式:



  • 消息定长

  • 用回车换行符作为消息结束标志

  • 用特殊分隔符作为消息结束标志,如\t、\n等,回车换行符其实就是特殊分隔符的一种。

  • 将消息分为消息头和消息体,在消息头中用字段标识消息总长度。


netty针对以上四种场景,给我们封装了以下四种对应的解码器:



  • FixedLengthFrameDecoder,定长消息解码器

  • LineBasedFrameDecoder,回车换行符消息解码器

  • DelimiterBasedFrameDecoder,特殊分隔符消息解码器

  • LengthFieldBasedFrameDecoder,自定义长度消息解码器。


我们用到的就是LengthFieldBasedFrameDecoder自定义长度消息解码器,同时配合LengthFieldPrepender编码器使用,关于参数配置,建议参考netty--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender这篇文章,讲解得比较细致。我们配置的是消息头长度为2个字节,所以消息包的最大长度需要小于65536个字节,netty会把消息内容长度存放消息头的字段里,接收方可以根据消息头的字段拿到此条消息总长度,当然,netty提供的LengthFieldBasedFrameDecoder已经封装好了处理逻辑,我们只需要配置lengthFieldOffset、lengthFieldLength、lengthAdjustment、initialBytesToStrip即可,这样就可以解决TCP的拆包与粘包,这也就是netty相较于原生nio的便捷性,原生nio需要自己处理拆包/粘包等问题。




长连接握手认证


接着,我们来看看LoginAuthHandlerHeartbeatRespHandler



  • LoginAuthRespHandler是当客户端与服务端长连接建立成功后,客户端主动向服务端发送一条登录认证消息,带入与当前用户相关的参数,比如token,服务端收到此消息后,到数据库查询该用户信息,如果是合法有效的用户,则返回一条登录成功消息给该客户端,反之,返回一条登录失败消息给该客户端,这里,就是在接收到服务端返回的登录状态后的处理handler,比如:LoginAuthRespHandler


可以看到,当接收到服务端握手消息响应后,会从扩展字段取出status,如果status=1,则代表握手成功,这个时候就先主动向服务端发送一条心跳消息,然后利用Netty的IdleStateHandler读写超时机制,定期向服务端发送心跳消息,维持长连接,以及检测长连接是否还存在等。



  • HeartbeatRespHandler是当客户端接收到服务端登录成功的消息后,主动向服务端发送一条心跳消息,心跳消息可以是一个空包,消息包体越小越好,服务端收到客户端的心跳包后,原样返回给客户端,这里,就是收到服务端返回的心跳消息响应的处理handler,比如:HeartbeatRespHandler


这个就比较简单,收到心跳消息响应,无需任务处理,直接打印一下方便我们分析即可。




心跳机制及读写超时机制


心跳包是定期发送,也可以自己定义一个周期,比如Android微信智能心跳方案,为了简单,此处规定应用在前台时,8秒发送一个心跳包,切换到后台时,30秒发送一次,根据自己的实际情况修改一下即可。心跳包用于维持长连接以及检测长连接是否断开等。


接着,我们利用Netty的读写超时机制,来实现一个心跳消息管理handler:
HeartbeatHandler
可以看到,利用userEventTriggered()方法回调,通过IdleState类型,可以判断读超时/写超时/读写超时,这个在添加IdleStateHandler时可以配置,下面会贴上代码。首先我们可以在READER_IDLE事件里,检测是否在规定时间内没有收到服务端心跳包响应,如果是,那就触发重连操作。在WRITER_IDEL事件可以检测客户端是否在规定时间内没有向服务端发送心跳包,如果是,那就主动发送一个心跳包。发送心跳包是在子线程中执行,我们可以利用之前写的work线程池进行线程管理。

addHeartbeatHandler()代码如下:
addHeartbeatHandler
从图上可看到,在IdleStateHandler里,配置的读超时为心跳间隔时长的3倍,也就是3次心跳没有响应时,则认为长连接已断开,触发重连操作。写超时则为心跳间隔时长,意味着每隔heartbeatInterval会发送一个心跳包。读写超时没用到,所以配置为0。


onConnectStatusCallback(int connectStatus)为连接状态回调,以及一些公共逻辑处理:
onConnectStatusCallback
连接成功后,立即发送一条握手消息,再次梳理一下整体流程:



  • 客户端根据服务端返回的host及port,进行第一次连接。

  • 连接成功后,客户端向服务端发送一条握手认证消息(1001)

  • 服务端在收到客户端的握手认证消息后,从扩展字段里取出用户token,到本地数据库校验合法性。

  • 校验完成后,服务端把校验结果通过1001消息返回给客户端,也就是握手消息响应。

  • 客户端收到服务端的握手消息响应后,从扩展字段取出校验结果。若校验成功,客户端向服务端发送一条心跳消息(1002),然后进入心跳发送周期,定期间隔向服务端发送心跳消息,维持长连接以及实时检测链路可用性,若发现链路不可用,等待一段时间触发重连操作,重连成功后,重新开始握手/心跳的逻辑。


看看TCPReadHandler收到消息是怎么处理的:
TCPReadHandler1
TCPReadHandler2
可以看到,在channelInactive()及exceptionCaught()方法都触发了重连,channelInactive()方法在当链路断开时会调用,exceptionCaught()方法在当出现异常时会触发,另外,还有诸如channelUnregistered()、channelReadComplete()等方法可以重写,在这里就不贴了,相信聪明的你一眼就能看出方法的作用。

我们仔细看一下channelRead()方法的逻辑,在if判断里,先判断消息类型,如果是服务端返回的消息发送状态报告类型,则判断消息是否发送成功,如果发送成功,从超时管理器中移除,这个超时管理器是干嘛的呢?下面讲到消息重发机制的时候会详细地讲。在else里,收到其他消息后,会立马给服务端返回一个消息接收状态报告,告诉服务端,这条消息我已经收到了,这个动作,对于后续需要做的离线消息会有作用。如果不需要支持离线消息功能,这一步可以省略。最后,调用消息转发器,把接收到的消息转发到应用层即可。


代码写了这么多,我们先来看看运行后的效果,先贴上缺失的消息发送代码及ims关闭代码以及一些默认配置项的代码。

发送消息:发送消息
关闭ims:关闭ims
ims默认配置:ims默认配置
还有,应用层实现的ims client启动器:
IMSClientBootstrap
由于代码有点多,不太方便全部贴上,如果有兴趣可以下载demo体验。
额,对了,还有一个简易的服务端代码,如下:
NettyServerDemo1
NettyServerDemo2
NettyServerDemo3





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

开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现(2)

封装 为什么需要封装呢?说白了,就是为了解耦,为了方便日后切换到不同框架实现,而无需到处修改调用的地方。举个栗子,比如Android早期比较流行的图片加载框架是Universal ImageLoader,后期因为某些原因,原作者停止了维护该项目,目前比较流行的...
继续阅读 »

封装


为什么需要封装呢?说白了,就是为了解耦,为了方便日后切换到不同框架实现,而无需到处修改调用的地方。举个栗子,比如Android早期比较流行的图片加载框架是Universal ImageLoader,后期因为某些原因,原作者停止了维护该项目,目前比较流行的图片加载框架是Picasso或Glide,因为图片加载功能可能调用的地方非常多,如果不作一些封装,早期使用了Universal ImageLoader的话,现在需要切换到Glide,那改动量将非常非常大,而且还很有可能会有遗漏,风险度非常高。


那么,有什么解决方案呢?


很简单,我们可以用工厂设计模式进行一些封装,工厂模式有三种:简单工厂模式、抽象工厂模式、工厂方法模式。在这里,我采用工厂方法模式进行封装,具体区别,可以参见:通俗讲讲我对简单工厂、工厂方法、抽象工厂三种设计模式的理解


我们分析一下,ims(IM Service,下文简称ims)应该是有初始化建立连接重连关闭连接释放资源判断长连接是否关闭发送消息等功能,基于上述分析,我们可以进行一个接口抽象:
抽象的ims接口1
抽象的ims接口2
OnEventListener是与应用层交互的listener:
OnEventListener
IMConnectStatusCallback是ims连接状态回调监听器:
IMConnectStatusCallback


然后写一个Netty tcp实现类:
Netty tcp ims1
Netty tcp ims2


接下来,写一个工厂方法:
ims实例工厂方法


封装部分到此结束,接下来,就是实现了。




初始化


我们先实现init(Vector serverUrlList, OnEventListener listener, IMSConnectStatusCallback callback)方法,初始化一些参数,以及进行第一次连接等:
初始化参数


其中,MsgDispatcher是消息转发器,负责将接收到的消息转发到应用层:
MsgDispatcher


ExecutorServiceFactory是线程池工厂,负责调度重连及心跳线程:
ExecutorServiceFactory1
ExecutorServiceFactory2
ExecutorServiceFactory3




连接及重连


resetConnect()方法作为连接的起点,首次连接以及重连逻辑,都是在resetConnect()方法进行逻辑处理,我们来瞄一眼:
resetConnect
可以看到,非首次进行连接,也就是连接一个周期失败后,进行重连时,会先让线程休眠一段时间,因为这个时候也许网络状况不太好,接着,判断ims是否已关闭或者是否正在进行重连操作,由于重连操作是在子线程执行,为了避免重复重连,需要进行一些并发处理。开始重连任务后,分四个步骤执行:



  • 改变重连状态标识

  • 回调连接状态到应用层

  • 关闭之前打开的连接channel

  • 利用线程池执行一个新的重连任务


ResetConnectRunnable是重连任务,核心的重连逻辑都放到这里执行:
ResetConnectRunnable1
ResetConnectRunnable2
ResetConnectRunnable3


toServer()是真正连接服务器的地方:
toServer


initBootstrap()是初始化Netty Bootstrap:
initBootstrap
注:NioEventLoopGroup线程数设置为4,可以满足QPS是一百多万的情况了,至于应用如果需要承受上千万上亿流量的,需要另外调整线程数。参考自:netty实战之百万级流量NioEventLoopGroup线程数配置


接着,我们来看看TCPChannelInitializerHanlder
TCPChannelInitializerHandler
其中,ProtobufEncoderProtobufDecoder是添加对protobuf的支持,LoginAuthRespHandler是接收到服务端握手认证消息响应的处理handler,HeartbeatRespHandler是接收到服务端心跳消息响应的处理handler,TCPReadHandler是接收到服务端其它消息后的处理handler,先不去管,我们重点来分析下LengthFieldPrependerLengthFieldBasedFrameDecoder,这就需要引申到TCP的拆包与粘包啦。


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

开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现(1)

写在前面 一直想写一篇关于im即时通讯分享的文章,无奈工作太忙,很难抽出时间。今天终于从公司离职了,打算好好休息几天再重新找工作,趁时间空闲,决定静下心来写一篇文章,毕竟从前辈那里学到了很多东西。工作了五年半,这三四年来一直在做社交相关的项目,有 直播、 即时...
继续阅读 »

写在前面


一直想写一篇关于im即时通讯分享的文章,无奈工作太忙,很难抽出时间。今天终于从公司离职了,打算好好休息几天再重新找工作,趁时间空闲,决定静下心来写一篇文章,毕竟从前辈那里学到了很多东西。工作了五年半,这三四年来一直在做社交相关的项目,有
直播
即时通讯
短视频分享
社区论坛
等产品,深知即时通讯技术在一个项目中的重要性,本着开源分享的精神,也趁这机会总结一下,所以写下这篇文章,文中有不对之处欢迎批评与指正。


本文将介绍:



  • Protobuf序列化

  • TCP拆包与粘包

  • 长连接握手认证

  • 心跳机制

  • 重连机制

  • 消息重发机制

  • 读写超时机制

  • 离线消息

  • 线程池

  • AIDL跨进程通信


本想花一部分时间介绍一下利用AIDL实现多进程通信,提升应用保活率,无奈这种方法在目前大部分Android新版本上已失效,而且也比较复杂,所以考虑再三,把AIDL这一部分去掉,需要了解的童鞋可以私信我。


先来看看效果:
最终运行效果


不想看文章的同学可以直接移步到Github fork源码:github地址


接下来,让我们进入正题。




为什么使用TCP?


这里需要简单解释一下,TCP/UDP/WebSocket的区别。
这里就很好地解释了TCP/UDP的优缺点和区别,以及适用场景,简单地总结一下:




  • 优点:



    • TCP的优点体现在稳定可靠上,在传输数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完之后,还会断开连接用来节约系统资源。

    • UDP的优点体现在比TCP稍安全,UDP没有TCP拥有的各种机制,是一个无状态的传输协议,所以传递数据非常快,没有TCP的这些机制,被攻击利用的机制就少一些,但是也无法避免被攻击。




  • 缺点:



    • TCP缺点就是效率低占用系统资源高易被攻击,TCP在传递数据之前要先建立连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞机制等都会消耗大量时间,而且要在每台设备上维护所有的传输连接。

    • UDP缺点就是不可靠不稳定,因为没有TCP的那些机制,UDP在传输数据时,如果网络质量不好,就会很容易丢包,造成数据的缺失。




  • 适用场景:



    • TCP:当对网络通讯质量有要求时,比如HTTP、HTTPS、FTP等传输文件的协议, POP、SMTP等邮件传输的协议。

    • UDP:对网络通讯质量要求不高时,要求网络通讯速度要快的场景。




至于WebSocket,后续可能会专门写一篇文章来介绍。
综上所述,决定采用TCP协议。




为什么使用Protobuf?


对于App网络传输协议,我们比较常见的、可选的,有三种,分别是json/xml/protobuf,老规矩,我们先分别来看看这三种格式的优缺点:




  • 优点:



    • json优点就是较XML格式更加小巧,传输效率较xml提高了很多,可读性还不错。

    • xml优点就是可读性强,解析方便。

    • protobuf优点就是传输效率快(据说在数据量大的时候,传输效率比xml和json快10-20倍),序列化后体积相比Json和XML很小,支持跨平台多语言,消息格式升级和兼容性还不错,序列化反序列化速度很快。




  • 缺点:



    • json缺点就是传输效率也不是特别高(比xml快,但比protobuf要慢很多)。

    • xml缺点就是效率不高,资源消耗过大。

    • protobuf缺点就是使用不太方便。




在一个需要大量的数据传输的场景中,如果数据量很大,那么选择protobuf可以明显的减少数据量,减少网络IO,从而减少网络传输所消耗的时间。考虑到作为一个主打社交的产品,消息数据量会非常大,同时为了节约流量,所以采用protobuf是一个不错的选择。




为什么使用Netty?


首先,我们来了解一下,Netty到底是个什么东西。网络上找到的介绍:Netty是由JBOSS提供的基于Java NIO的开源框架,Netty提供异步非阻塞、事件驱动、高性能、高可靠、高可定制性的网络应用程序和工具,可用于开发服务端和客户端。




  • 为什么不用Java BIO?



    • 一连接一线程,由于线程数是有限的,所以这样非常消耗资源,最终也导致它不能承受高并发连接的需求。

    • 性能低,因为频繁的进行上下文切换,导致CUP利用率低。

    • 可靠性差,由于所有的IO操作都是同步的,即使是业务线程也如此,所以业务线程的IO操作也有可能被阻塞,这将导致系统过分依赖网络的实时情况和外部组件的处理能力,可靠性大大降低。




  • 为什么不用Java NIO?



    • NIO的类库和API相当复杂,使用它来开发,需要非常熟练地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel等。

    • 需要很多额外的编程技能来辅助使用NIO,例如,因为NIO涉及了Reactor线程模型,所以必须必须对多线程和网络编程非常熟悉才能写出高质量的NIO程序。

    • 想要有高可靠性,工作量和难度都非常的大,因为服务端需要面临客户端频繁的接入和断开、网络闪断、半包读写、失败缓存、网络阻塞的问题,这些将严重影响我们的可靠性,而使用原生NIO解决它们的难度相当大。

    • JDK NIO中著名的BUG--epoll空轮询,当select返回0时,会导致Selector空轮询而导致CUP100%,官方表示JDK1.6之后修复了这个问题,其实只是发生的概率降低了,没有根本上解决。




  • 为什么用Netty?



    • API使用简单,更容易上手,开发门槛低

    • 功能强大,预置了多种编解码功能,支持多种主流协议

    • 定制能力高,可以通过ChannelHandler对通信框架进行灵活地拓展

    • 高性能,与目前多种NIO主流框架相比,Netty综合性能最高

    • 高稳定性,解决了JDK NIO的BUG

    • 经历了大规模的商业应用考验,质量和可靠性都有很好的验证。




以上摘自:为什么要用Netty开发



  • 为什么不用第三方SDK,如:融云、环信、腾讯TIM?


这个就见仁见智了,有的时候,是因为公司的技术选型问题,因为用第三方的SDK,意味着消息数据需要存储到第三方的服务器上,再者,可扩展性、灵活性肯定没有自己开发的要好,还有一个小问题,就是收费。比如,融云免费版只支持100个注册用户,超过100就要收费,群聊支持人数有限制等等...
融云收费


Mina其实跟Netty很像,大部分API都相同,因为是同一个作者开发的。但感觉Mina没有Netty成熟,在使用Netty的过程中,出了问题很轻易地可以找到解决方案,所以,Netty是一个不错的选择。


好了,废话不多说,直接开始吧。




准备工作



  • 首先,我们新建一个Project,在Project里面再新建一个Android Library,Module名称暂且叫做im_lib,如图所示:


新建项目




  • 然后,分析一下我们的消息结构,每条消息应该会有一个消息唯一id,发送者id,接收者id,消息类型,发送时间等,经过分析,整理出一个通用的消息类型,如下:



    • msgId 消息id

    • fromId 发送者id

    • toId 接收者id

    • msgType 消息类型

    • msgContentType 消息内容类型

    • timestamp 消息时间戳

    • statusReport 状态报告

    • extend 扩展字段


    根据上述所示,我整理了一个思维导图,方便大家参考:
    消息结构

    这是基础部分,当然,大家也可以根据自己需要自定义比较适合自己的消息结构。


    我们根据自定义的消息类型来编写proto文件。
    编写proto文件
    然后执行命令(我用的mac,windows命令应该也差不多):
    执行protoc命令
    然后就会看到,在和proto文件同级目录下,会生成一个java类,这个就是我们需要用到的东东:
    生成的protobuf java类文件
    我们打开瞄一眼:
    打开的protobuf java类文件
    东西比较多,不用去管,这是google为我们生成的protobuf类,直接用就行,怎么用呢?直接用这个类文件,拷到我们开始指定的项目包路径下就可以啦:
    导入protobuf java类文件到项目中
    添加依赖后,可以看到,MessageProtobuf类文件已经没有报错了,顺便把netty的jar包也导进来一下,还有fastjson的:
    导入protobuf以及netty的依赖
    建议用netty-all-x.x.xx.Final的jar包,后续熟悉了,可以用精简的jar包。


    至此,准备工作已结束,下面,我们来编写java代码,实现即时通讯的功能。


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

一个小型合作的流水线——Android Handler

当我们遇到多线程的问题,考虑到线程间消息传递的时候,首先想到的肯定是 Handler。虽然写这篇文章的初衷并不是想探究 Handler 的机制,但我们还是先从这个被说烂了的话题开始。 Handler 的工作原理 首先,在了解 Handler 之前,我们需要了解...
继续阅读 »

当我们遇到多线程的问题,考虑到线程间消息传递的时候,首先想到的肯定是 Handler。虽然写这篇文章的初衷并不是想探究 Handler 的机制,但我们还是先从这个被说烂了的话题开始。


Handler 的工作原理


首先,在了解 Handler 之前,我们需要了解有四个关键的类是组成 Handler 的基础。它们分别是



  • Handler 负责协调安排未来某个时间点的消息或可运行状态,以及对不同线程的运行机制进行合理的排队

  • Looper 主要作用如其名,一个循环的机制,为线程运行消息循环分发

  • MessageQueue 一个链式队列数据结构,将消息实体串联成链

  • Message 消息实体,存储我们需要传递的消息的内容和信息等


Looper 和 MessageQueue——Handler 的流水线


ActivityThread 类中,作为入口方法的 main() 方法中,通过调用 Looperloop() 方法,启动 Looper 的循环机制(这里我们注意到,在方法的最后,抛出了一个主线程循环意外退出的异常,说明 Android 的主流程都是通过 Handler 来驱动的)。


/**
* ActivityThread
*/
public static void main(String[] args) {

// ...
Looper.prepareMainLooper();
// ...
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}
复制代码

进入 loop() 方法,这里我们可以看到一个死循环,传说中的死循环这么快就跟我们见面了吗?其实不然,我们平时面试时更关注的死循环并不是这个,或者说它只是其中的一部分。废话先不说,这段代码精简后的大致作用可以归纳为:从 MessageQueue 的对象队列里取出一个未处理的消息,即 Message 实例,然后获取 Message 对象的 target 属性,它是一个 Handler 对象,然后通过 dispatchMessage() 方法来将消息进行分发。


/**
* Looper
*/
public static void loop() {
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
// 我最开始读到这段源码的时候,很困惑这个方法为什么调用了两遍,后来经过思索想明白了原因,这里稍作记录。
// 这个方法调用的是 native 的代码,源码如下:
// int64_t IPCThreadState::clearCallingIdentity()
// {
// int64_t token = ((int64_t)mCallingUid<<32) | mCallingPid;
// clearCaller();
// return token;
// }
// void IPCThreadState::clearCaller()
// {
// mCallingPid = getpid(); //当前进程pid赋值给mCallingPid
// mCallingUid = getuid(); //当前进程uid赋值给mCallingUid
// }
// 具体作用可以网上自行搜索,这个方法的作用,简而言之,就是将(可能是)其他进程的 pid 和 uid 清除,更换为自己的,
// 而 token 是用来存储原来进程的 pid 和 uid 的64位整型,所以第一遍调用时返回的是之前进程的 pid 和 uid 信息,
// 再次调用时,返回的才是当前进程的,而被我精简掉的源码里需要通过这个 token 来判断进程是否切换过,所以这个方法在这里会调用两遍
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
try {
msg.target.dispatchMessage(msg);

} catch (Exception exception) {
throw exception;
}

msg.recycleUnchecked();
}
}
复制代码

因为 dispatchMessage() 方法比较简单,所以我们先越过过程看结果,看看这个方法的实现。


/**
* Handler
*/
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
复制代码

这里就直接调用了 Handler 对象的 handleMessage() 方法,并传递 Message 的实例,所以我们在使用 Handler 时在这个方法中就可以接收到我们需要的消息实体(callback 默认不实现,实现后变更为调用相应的方法)。


好,结果我们已经知道了,那现在我们回过头来,研究一下上面 Looper 类的 loop() 方法中调用的 queue.next() 方法是如何拿到消息实体的(后面的注释已经提醒我们这个方法可能会阻塞)。


/**
* MessageQueue
*/
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
// 这个变量作为 nativePollOnce 方法的参数表示休眠的时间
// 当值为 -1 时,表示无限休眠,直到有线程唤醒
// 当值为 0 时,表示立即唤醒
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

// 根据 nextPollTimeoutMillis 变量的值进行休眠
nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 如果 Message 的 target 为 null,则说明它是 Looper synchronization barrier 的临界点
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;and the message is the earliest asynchronous message in the queue
} while (msg != null && !msg.isAsynchronous());
}
// 经过上面的循环后,到达这里的 Message 要么是 null,要么是 isAsynchronous() 方法返回 true
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
// 消息的发送时间未到,此时的 nextPollTimeoutMillis 为距离 msg 的发送时间的时间间隔,
// 那 nativePollOnce() 方法休眠相应的时间后,msg 即到了它该发送的时间
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
// 没有更多的消息,此时 nextPollTimeoutMillis 赋值为 -1,
// 那 nativePollOnce() 方法将导致线程永久休眠,直到有其他线程将其唤醒
nextPollTimeoutMillis = -1;
}

// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
}
}
}
复制代码

next() 方法看起来很长,但是它的主要工作只有一件事,就是找到符合要求的 Message 实例并返回。但是这个方法又特别重要,有一个常问的重要的面试考点。我们上面已经提到了,Looperloop() 方法中有一个死循环,作用是源源不断地从 MessageQueue 中「打捞」 Message 实体,而「打捞」的动作正是通过 next() 方法完成的。在 next() 方法中,也有一个死循环,完成上面的「打捞」工作。具体的细节我在代码中作了部分注释,可以帮助理解。其中提到了一个概念——「Looper synchronization barrier」,关于它的介绍我们放在下面的内容里。


好了,介绍完了 Handler 机制中的死循环,它是死循环双重嵌套的形式,那么面试问题来了:请问 Handler 机制中的死循环是如何做到不阻塞主线程的呢?网上搜索到的答案通常是死循环也未必会阻塞主线程,只要不在 onCreate()onStart() 等生命周期中阻塞就不会导致界面的卡死,其次在 MessageQueue 中没有 Message 实体时,线程会进入到一个休眠的状态,在有新消息来临时线程才会被唤醒,balabala小魔仙……我们看到 next() 方法的死循环的一开始有一句代码 nativePollOnce(),它是一个 native 的方法,通过执行 Linux 中的 epoll 机制来是线程休眠和运行,它和 nativeWake() 方法配对使用,在类文件的开头均有声明。所以每次在执行完一遍 next() 方法后,都会根据 nextPollTimeoutMillis 变量的值来决定休眠的时间。如果没有可被「打捞」的消息,那么线程将被永久休眠,等待被唤醒。那么在哪里唤醒的呢,我们暂时不管,在这里先记住线程休眠,主线程被阻塞,等待一个白马王子将其唤醒。至于白马王子何时到来,我们静待。


Handler——消息操作台


现在,我们再从消息发送的源头追溯——通过 Handler 的一系列 sendMessage() 方法,将消息发送出去。


我们以 sendEmptyMessage() 方法为例,经过一系列的调用后,最终会执行 enqueueMessage() 方法,该方法又会调用 MessageQueueenqueueMessage() 方法,该方法代码如下:


/**
* MessageQueue
*/
boolean enqueueMessage(Message msg, long when) {

synchronized (this) {

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}

// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
复制代码

好耶,「白马王子」来了!看到了吧,nativeWake() 方法显真身了,当有新的消息压入队列,消息需要被处理,此时就需要唤醒睡眠的线程。但是「白马王子」


的到来是需要条件的,即 needWake,那到底是怎样的条件呢?想想无非是判断当前的线程是否处于可能阻塞的状态,我们来看看。


在第一个条件 p == null || when == 0 || when < p.when 下,相比于罗列所有的满足条件的情况,更简单的方法是判断我们前面的线程被阻塞的情况是不是在这里被判定为 needWake, 因为在等待新的消息,所以 mMessage 值为 null,此时的 needWake = mBlocked,而 mBlocked 的线程被阻塞的情况下值是为 true 的,所以这里会被判定为需要被唤醒。而在 else 分支中,其实条件为p != null && when != 0 && when >= p.when,这说明消息队列中的消息并没有被取完,而是正在一个循环中,通常情况下是不需要再唤醒它,除非像注释中所说的 there is a barrier at the head of the queue and the message is the earliest asynchronous message in the queue


到这里,Handler 的大概工作流程就可以串联起来了——循环队列相当于物流,消息相当于商品,物流无时无刻在运转,当你需要新的商品时,商品被商家发送至物流,然后分发到目标客户即你的手中。


Looper synchronization barrier


在看源码的时候,不止一次会接触到这个概念,而且在上面我们也已经率先使用了这个概念,那么这个概念到底是个什么?搞清楚这个问题,我们需要从它的特征入手,在 MessageQueuenext() 方法中,我们说如果 mMessages.target == null,那么它就是一个 barrier 的临界点,我们通过查找 mMessage 的写引用,最终定位到 MessageQueue#postSyncBarrier() 这个方法。我这里摘录它的注释,相信大家对这个概念就会有一个清晰的认识。



Posts a synchronization barrier to the Looper's message queue.


Message processing occurs as usual until the message queue encounters the synchronization barrier that has been posted. When the barrier is encountered, later synchronous messages in the queue are stalled (prevented from being executed) until the barrier is released by calling {@link #removeSyncBarrier} and specifying the token that identifies the synchronization barrier.


This method is used to immediately postpone execution of all subsequently posted synchronous messages until a condition is met that releases the barrier. Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier and continue to be processed as usual.


This call must be always matched by a call to {@link #removeSyncBarrier} with the same token to ensure that the message queue resumes normal operation. Otherwise the application will probably hang!



在了解这个概念之前还需要知道一个属性的存在,那就是 Message#isAsynchronous()


好了,总结一下就是 Looper synchronization barrierMessageQueue 中那些 target == nullMessage,它们不需要被发送,只作为一种队列状态的判断标识。当 Message.isAsynchronous() == true 时,遇到 Looper synchronization barrier 时,Looper 会被阻塞,直到 removeSyncBarrier() 方法(和 postSyncBarrier() 方法成对使用)移除这个标识。但是如果 Message.isAsynchronous() == false 时,则不会被 barrier 阻断,具体使用场景见上方注释。


太多的代码和解说赶不上一张图片更能让人形成概念,那我从网上找了一张图片稍作加工,希望可以比较形象地说明 Handler 机制中各个类之间的分工。



映射关系


为了话题的自然过渡,这里我们思考一个问题,一个线程可以有多个 Looper 吗?一个 Looper 可以对应多个 MessageQueue 吗?从源码中看,一个线程是无法创建多个 Looper 和多个 MessageQueue 的,那么多个 LooperMessageQueue 会导致什么问题呢?最主要的就是我们上面说的消息同步性的问题了,多个消息队列和循环体如何保证消息的次序限制以及同步分发就是一个很复杂的问题。那么系统又是如何保证每个线程的 Looper 的唯一性的呢?那就是使用 ThreadLocal 了。


ThreadLocal


由于本篇内容旨在讨论 Handler 的相关机制,所以对于 ThreadLocal 的机制不做过多讨论。


Looper#prepare() 方法在 Looper 使用前必须调用,在这个方法里可以看到 ThreadLocal 的应用。


/**
* Looper
*/
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
复制代码

sThreadLocal 对象是一个全局的静态对象,通过使用 sThreadLocal#set() 方法来存储 Looper 的实例,而 ThreadLocal 把真正的对象存储交给了它的静态内部类 ThreadLocalMap,这是一个自定义的 hash map,具体内部实现请自行阅读源码。


/**
* ThreadLocal
*/

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
复制代码

可以看到,ThreadLocalMap 又和 Thread 绑定,每个 Thread 对应一个唯一的 ThreadLocalMap 实例, ThreadLocalMapkey 的类型是 ThreadLocal,而在 Looper 中的 sThreadLocal 作为静态对象,进程内唯一,通过这样的关系,可以唯一对应到 TreadLocalMap 中的某个元素,实现读取。


碎碎念


前面两个月经历找工作和工作后的一堆琐事,导致很久没有更新。这篇也是匆忙赶工,逻辑上和图文代码编排上都有一些问题,还请多多包涵。之前做的是 Flutter 的工作,现在又回到了 Android,Flutter 的内容也会继续带着更新,后面我会尽量保持正常的更新频率,但是水平确实有限,最后还是请大家雅正和包涵。


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

Jetpack Compose 初体验(二)

Jetpack Compose 初体验(一)二、主题 基本布局已经差不多啦,那么我们再来搞一些共性的东西,就像我们黄种人都有一样的肤色——散在土地里的黄,有种顽强,非常东方…… 以前的 View 系统其实也有关于 theme 的定义,那些被定义的 style,...
继续阅读 »

Jetpack Compose 初体验(一)

二、主题


基本布局已经差不多啦,那么我们再来搞一些共性的东西,就像我们黄种人都有一样的肤色——散在土地里的黄,有种顽强,非常东方……


以前的 View 系统其实也有关于 theme 的定义,那些被定义的 style,在官方定义的一系列 theme 的基础上加以扩展,形成我们 app 的主题。


Compose 框架提供了 Material Design 的实现,Material Design Theme 自然也被应用到 Compose 中,Material Design Theme 包括了对颜色、文本样式和形状等属性的定义,咱们自定义这些属性后,包括 button、cards、switches 等控件都会相应的改变它们的默认样式。


1.颜色


颜色在前端开发中真的是无处不在了,Color 可以帮助我们快速地构建颜色模型。


你可以泡着吃:


val red = Color(0xffff0000)
复制代码

可以扭着吃:


val blue = Color(red = 0f, green = 0f, blue = 1f)
复制代码

欸,你还可以干吃:


val black = Color.Black
复制代码

只要你喜欢,你甚至可以空翻360度加转体一周半的时候吃:


// 我不会空翻,也不会转体,期待你的表现,加油!
复制代码

Compose 提供了 Colors数来创建成套的浅色或深色:


val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200,
onPrimary = Color.Green
)

private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant Customize= Purple700,
secondary = Teal200,
onPrimary = Color.Green

/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
复制代码

然后,就可以传递给 MaterialTheme 使用喽:


@Composable
fun TestComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}

MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
复制代码

怎么样,还自动适配深色模式。


而且,我们也可以随时随地获取到主题色:


Text(
text = "Hello theming",
color = MaterialTheme.colors.primary
)
复制代码

表面颜色和内容颜色又是另一个概念了,许多组件都接受一对颜色和「内容颜色」:


Surface(
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),


TopAppBar(
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),

复制代码

这样一来,您不仅可以设置可组合项的颜色,而且还能为包含在可组合项中的内容提供默认颜色。默认情况下,许多可组合项都使用这种内容颜色。例如,Text 的颜色基于其父项的内容颜色,而 Icon :「俺也一样」,它可以使用该颜色来设置其色调。


contentColorFor() 方法可以为任何主题颜色检索适当的“on”颜色。例如,如果您设置 primary 背景,就会将 onPrimary 设置内容颜色。如果您设置非主题背景颜色,还应指定合理的内容颜色。使用 LocalContentColor 可检索与当前背景形成对比的当前内容颜色。


我们以上面自定义的 Theme 来试验,使用它作为我们的主题:


@Preview
@Composable
fun TestColor() {
TestComposeTheme {
Button(onClick = {}) {
Text(
"hello world"
)
}
}
}Customize
复制代码

效果:


image-20210429183615573.png


2.字体排版


字体排版主要通过 TypographyTextStyle 类来完成。Typography 构造函数可以提供每种样式的默认值,因此您可以省略不希望自定义的任何样式:


val Rubik = FontFamily(
Font(R.font.rubik_regular),
Font(R.font.rubik_medium, FontWeight.W500),
Font(R.font.rubik_bold, FontWeight.Bold)
)

val MyTypography = Typography(
h1 = TextStyle(
fontFamily = Rubik,
fontWeight = FontWeight.W300,
fontSize = 96.sp
),
body1 = TextStyle(
fontFamily = Rubik,
fontWeight = FontWeight.W600,
fontSize = 16.sp
)
/*...*/
)
MaterialTheme(typography = MyTypography, /*...*/)
复制代码

如果您希望自始至终使用同一字体,请指定 defaultFontFamily 参数,并省略所有 TextStyle 元素的 fontFamily


val typography = Typography(defaultFontFamily = Rubik)
MaterialTheme(typography = typography, /*...*/)
复制代码

使用时,可以从主题检索 TextStyle,如以下示例所示:


Text(
text = "Subtitle2 styled",
style = MaterialTheme.typography.subtitle2
)
复制代码

3.形状


Compose 中可以轻松地定义各种形状,比如圆角或者操场跑道形状,在传统 View 系统中实现都比较麻烦。


我们现在修改一下上面的 Button 的形状来看看效果:


val Shapes = Shapes(
small = CutCornerShape(
topStart = 16.dp,
topEnd = 0.dp,
bottomStart = 16.dp,
bottomEnd = 0.dp
),
medium = RoundedCornerShape(percent = 50),
large = RoundedCornerShape(0.dp)
)
复制代码

image-20210429192726472.png


这里有一点需要注意的是,默认情况下,许多组件使用这些形状。例如,Button、TextField 和 FloatingActionButton 默认为 small,AlertDialog 默认为 medium,而 ModalDrawerLayout 默认为 large。如需查看完整的对应关系,请参阅形状方案参考文档。


三、列表


列表也是个常见的家伙,Android View 系统中早期的 ListView 和后来的 RecyclerView, Flutter 里的 ListView 等。


一个列表就是许多个元素排排站,整齐笔直。那一个纵向(或横向)的布局中动态地添加进许多的元素不就好了。


@Composable
fun MessageList(messages: List) {
Column {
messages.forEach { message ->
MessageRow(message)
}
}
}
复制代码

来,你猜,RecyclerView 是不是这么写的。这里有个最大的问题,假如你是个交际花,好友从这里排到法国,列表多到滑一晚上滑不到头,那么一次加载是不是要耗费巨大的资源,搞不好卡死了王思聪联系不上你那就太不给面了,不好。


RecyclerView 最大的一个优点是它可以懒加载列表项,一次只加载一个屏幕的条目(四舍五入就是我对)。Compose 中可没有 RecyclerView,但是同样有针对这一问题优化的组件,LazyColumnLazyRow 是垂直和水平方向的懒加载列表控件。我们先来看一下效果:


@Preview
@Composable
fun TestList() {
LazyColumn(modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Add a single item
item {
Text(text = "First item")
}

// Add 5 items
items(10000) { index ->
Text(text = "Item: $index")
}

// Add another single item
item {
Text(text = "Last item")
}
}
}
复制代码

这里加载了一万个元素的列表,看看这丝滑的效果吧(建议就着德芙食用)。


lazy_column.gif


我们还可以像上面一样,通过 contentPadding 设置内容边距,verticalArrangement 则是可以设置 item 间间距,以及均匀地排列元素以充满父空间。


比较遗憾地是 LazyColumnLazyRow 暂时无法设置例如添加元素时地动画,期待后续的加入吧。


LazyColumn 可以轻松地实现粘性标题,只需使用 stickyHeader() 函数即可:


// TODO: This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map>) {
LazyColumn {
grouped.forEach { (initial, contactsForInitial) ->
stickyHeader {
CharacterHeader(initial)
}

items(contactsForInitial) { contact ->
ContactListItem(contact)
}
}
}
}
复制代码

上面的代码演示了如何通过 Map 数据结构实现粘性标题的数据展示。


既然有列表,那么肯定会有宫格列表,LazyVerticalGrid 则能够帮助我们实现需求。更多用法查看相关 API(没错,我就是 LazyBoy,但我的尊严决定了我不会滚)。


在实际项目开发中,我们经常会遇到将数据分页展示的情况,以减少数据请求压力。借助 Paging 3.0 库 可以来进行分页,Paging 库是 Jetpack 中重要的一项新特性,可帮助您一次加载和显示多个小的数据块。按需载入部分数据会减少网络带宽和系统资源的使用量。


如需显示分页内容列表,可以使用 collectAsLazyPagingItems() 扩展函数,然后将返回的 LazyPagingItems 传入 LazyColumn 中的 items()。与视图中的 Paging 支持类似,您可以通过检查 item 是否为 null,在加载数据时显示占位符:


import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun MessageList(pager: Pager) {
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

LazyColumn {
items(lazyPagingItems) { message ->
if (message != null) {
MessageRow(message)
} else {
MessagePlaceholder()
}
}
}
}

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

Jetpack Compose 初体验(一)

你是否受够了 Android 中 UI 编写的体验——在 xml 文件中编写复杂的层级结构和繁多的属性,动态化的视图逻辑又被分裂到 Activity 中?哦,这该死的友好度和割裂感! 这两年,Flutter 大行其道,不论是网上的讨论度还是实际的落地项目,风头...
继续阅读 »

你是否受够了 Android 中 UI 编写的体验——在 xml 文件中编写复杂的层级结构和繁多的属性,动态化的视图逻辑又被分裂到 Activity 中?哦,这该死的友好度和割裂感!


这两年,Flutter 大行其道,不论是网上的讨论度还是实际的落地项目,风头一时无两。所以从这个角度来说,作为 UI 框架的 Flutter,无疑是成功的。本着借鉴的思想(或许吧,谁知道呢),Android 在 Jetpack 项目中新增了一套全新的视图开发套件——Compose。它有着和 Flutter 一样好看的(姑且这么认为吧)外表,但究竟只是一个好看的花瓶还是才貌双全,这得我们自己去寻找答案。


Compose 当前还处于测试版本,想要使用它,我们需要首先下载 Android studio 的 canary 版本以提供支持。你可以在这里下载或者在你现有的 Android studio 中打开 File -> Settings -> Appearance & Behavior -> System Settings -> Updates 菜单,然后切换到 canary 渠道再点击 Check Now 按钮即可更新到最新的 canary 版本。


一、布局


1.可组合函数


使用 Compose 创建一个界面是简单的,只需通过 @Composable 注解定义一个可组合函数,在函数中返回界面元素组件即可。


@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
复制代码

该函数可以接收参数。函数内可以是一个组件,也可以是多个组件的组合。


通过 setContent 方法块可以设置页面内容,类似于之前的 setContentView() 方法。


setContent {
Text(text = "Hello Compose!")
}
复制代码

相比于之前的界面书写过程,Compose 更「神奇」的一个体现在于它可以直接在 Android studio 中预览我们编写的界面和组件,而无需让程序运行在设备中。


我们只需要在可组合函数的基础上再新增一个 @Preview 注解,但是需要注意的是,预览函数不接受参数,所以比较好的做法是在可组合函数的基础上编写其对应的预览函数。


@Preview
@Composable
fun DefaultPreview() {
Greeting("Android")
}
复制代码

预览函数对你的应用在设备上的最终呈现不会产生影响,Android studio 提供了一个预览窗口可以实时看到预览函数所呈现的效果。


image-20210412225335487.png


2.布局


我们编写的应用界面几乎任何时候都不会是简简单单的单一的控件,而是一定数量的独立控件在空间上的一种组合。


首先,我们就盲猜,如果我想竖直方向排列三个文字组件,肯定不是像下面这样随便组合三个 Text 控件。它怎么可能那么聪明,能知道你是想横着排还是竖着排,想并排排还是旋转开。怎么可能有人比苏菲更懂你!


@Composable
fun VerticalText() {
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
复制代码

image-20210413003422129.png


那,就组合喽。


@Composable
fun VerticalText() {
Column {
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码

给三个 Text 约定个竖框框,它们就能乖乖地排起队。


image-20210413005809783.png


这里,悄摸摸地说一句,这要是没有偷瞄 Flutter 的考卷 向优秀的思想借鉴,我把三个 Text 布局在我脑门上!


当然,只有这么生硬的排列可不行,我们还需要加点属性,使得整个布局更和谐点——例如,加点边距。


我们希望给 Column 加一个内边距,那么我们就应该给 Column 添加一个属性。Modifier 类用来给组件添加装饰或者行为,如背景、边距、点击事件等。


@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码

image-20210413210728702.png


现在,为了让界面看起来不那么单调,我们给这个界面加上下面这一张图片。


![](Compose 初体验.assets/hello_world_new_black.png)


将这张图片拷贝到 drawable 资源文件夹下面,然后通过下面的方式引用。


@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null
)
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码

Image 的其中一个构造函数支持以下参数,其中 painter 参数和 contentDescription 参数没有默认值,为必传参数。


image-20210420220142848.png


这样,图片就被构造出来啦,看一下效果:


image-20210420220432840.png


那怎么该对图片进行一些约束呢?作为一个头图,我不希望它这么哗众取宠,做图片要低调一点。


在上面,我们认识了 Modifier,那就寻求它的帮助,让我们的图片小一些吧。


Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp),
contentScale = ContentScale.Inside
)
复制代码

借助 Modifier 将图片的高度和宽度分别进行限定。然后通过 contentScale 参数对图片的缩放方式进行约束。ContentScale.Inside 保持图片比例不变的情况下尽可能地充满父控件的体积。


把上面的 Image 放入 preview 方法,看一下效果:


image-20210420221651702.png


现在头图就被我们拿捏得死死的,但是它还不是很好看,没脖子,加个脖子。


@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp),
contentScale = ContentScale.Inside
)

Spacer(modifier = Modifier.height(16.dp))

Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码

image-20210420222219906.png


这样是不是好看多了,嗯,是的。


3.Material Design


谷歌霸霸的产品当然是支持 Material Design 的,那咱就看看。


做头图不要锋芒毕露,做图处事要圆滑一点。给头图加个圆角是个不错的想法。


在 Android 传统的 UI 编写中,圆角图片一直没有很简单的解决方案,需要通过诸如自定义 ImageView 的方式来实现。但是,朋友们,当你使用 Compose 框架的时候,只需要一行代码就可以圆角图片的显示!家祭无忘告乃翁。


@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp)
.clip(shape = RoundedCornerShape(4.dp)),
contentScale = ContentScale.Inside
)

Spacer(modifier = Modifier.height(16.dp))

Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码

这里还是通过 Modifier 来实现需求,怎么样,现在的头图是不是圆滑可爱了很多。


image-20210420223334597.png


头图这么求上进,文字也不能落后,一篇好的文章要主次分明,错落有致。


声明 Typography 对象,然后给 Text 添加 style 属性,来控制文字的样式。


@Preview(showBackground = true)
@Composable
fun VerticalText() {
val typography = MaterialTheme.typography
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp)
.clip(shape = RoundedCornerShape(4.dp)),
contentScale = ContentScale.Inside
)

Spacer(modifier = Modifier.height(16.dp))

Text("Hello World!", style = typography.h3)
Text("Hello Again World!", style = typography.body1)
Text("How old are you, World!", style = typography.body2)
}
}
复制代码

Typography 提供如下预设属性,囊括标题、子标题、段落体、按钮等。


image-20210420225524291.png


最终效果如下:


image-20210420225406851.png


怎么样,是不是主次开始变得分明了?结构变得清晰了?情节展开得顺滑了?故事开始自然了?……


当然,其他的诸如最大行数、字体、对齐方式等都可以被配置。


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

iOS 列表界面如何优雅实现模块化与动态化

前言去年做了一个小组件,前些时间考虑到项目中可能会大规模实施,完善简化后新开了一个 repo: YBHandyList 。有些朋友抛出了 nimbus、IGListKit 等业界应用很广的库,前些时间网易工程师也推出了 M80TableViewComponen...
继续阅读 »

前言

去年做了一个小组件,前些时间考虑到项目中可能会大规模实施,完善简化后新开了一个 repo: YBHandyList 。

有些朋友抛出了 nimbus、IGListKit 等业界应用很广的库,前些时间网易工程师也推出了 M80TableViewComponent。理论上这些组件的原理大同小异,虽然它们各有优势,但却不太能满足笔者对架构清晰度的要求。

本文分析 YBHandyList 的应用价值,希望能解开一些朋友的疑惑。

业务痛点

iOS 界面开发中 UITableView / UICollectionView 的出场率极高,它们都是使用代理方法配置数据源,虽然这样的设计理念符合了单一职责原则,但在列表变得复杂时代理方法的处理将变得力不从心:

  • 同一个 Cell / Header / Footer 处理逻辑分散在各个代理方法中,不便于管理。

  • 当列表数据动态变化时,每一个代理方法里的判断逻辑都将变得复杂,且这些逻辑很可能会相互关联。

显然,在这样的场景下将是维护的灾难,特别是当你接手别人的代码发现每个 UITableView 代理方法里都有几十个if-else,它们人多势众,量你不敢动它们任何一个。

由此可见,若想维护性高需要解开每一个 Cell 之间的逻辑耦合,也就是通常意义的模块化,由此才能更轻易的实现动态化。解决方案其实很简单,只需要一个中间类,将分散的配置集中起来(在代理方法里取这个中间类的对应值):

@interface Config : NSObject
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, strong) Class cls;
@property (nonatomic, strong) id model;
@end

然而对于业务工程师来说,每次写这样的代码都意味着时间成本,所以制作一个基础组件是很有必要的,它需要满足以下特性:

  • 模块化配置 Cell / Header / Footer。

  • 更容易实施列表动态化。

  • 能拓展原生能实现的所有场景。

为此,YBHandyList 应运而生,它足够简单以至于从设计到编码基本就花了一天时间。

YBHandyList 的优势

原理:


代码简单轻量

YBHandyList 保留最小功能,代码量很少,核心思路就一句话:将 UITableView / UICollectionView 的数据源从代理方法配置转化为数组配置。

在其它库当中可以看到高度缓存、访问迭代器等逻辑,笔者认为这样的基础设施不应该侵入过多业务,它们本应该是业务关注的逻辑,这样的语法糖只能在简单场景下少写些代码,当业务变得复杂时往往这样的优势就不存在了。

YBHandyList 的语法糖非常收敛,简单的一个延展,你甚至可以选择不使用语法糖,直接使用代理实现类。

由此,新手工程师也能对实施代码充满信心。

业务侵入性低

YBHandyList 采用 IOP 设计,最大限度的降低了业务侵入性,只需要在 Cell / Header / Footer 中实现几个代理方法就行了。

去基类化设计让数据流动过程更加纯粹,不需要考虑父类做了什么,没做什么。在老业务中可能存在类似BaseTableViewCell 的东西,YBHandyList 也能优雅的接入,这种场景下继承的设计范式将力不从心。

这种架构规范类组件接入的成本非常重要,而舍弃的成本也不容忽视,由于 IOP 天然的优势,YBHandyList 结构代码的舍弃将轻而易举,不拖泥带水。

直观的动态化控制

构建界面只需要关注所有id<Config>在数据源数组中的顺序,就像搭积木一样拼接起来,数组中的顺序就是对应 Cell 在界面中的显示顺序,由此就能通过改变数据源数组的顺序轻易的实现动态化控制。

在 MVVM 架构中实施

YBHandyList 的设计方式让它在各种架构中都能无障碍实施,下面以 MVVM 举例(仅说明 UITableViewCell 的实施,具体可以看 DEMO):


可以看到,Cell 与 UITableView 非直接耦合,所以若需要将 Cell 的事件传递出来最好通过 Cell 的 ViewModel,ViewModel 作为连接 Cell 与外界的桥梁。

Cell 的 ViewModel 也可以在主 ViewModel 中构建,这样 Controller 中就不用导入这些类,不过当 Cell 的 ViewModel 需要将事件传递到 Controller 时,就会需要一些胶水代码通过主 ViewModel 间接传递。

数据绑定并非必须做的事情,你可以用 RAC,或者另外一个选择:EasyReact,可以参考笔者的文章:美团 EasyReact 源码剖析:图论与响应式编程。

更安全和优雅的复用

很多时候,我们会将具体业务的处理逻辑放 Cell 中或者其 ViewModel 中,那么它们就很难复用,因为复用是建立在无具体业务侵入的前提下。

实际上只需要将具体业务的处理逻辑抽离出来,处理过后再放在 ViewModel 中,Cell 拿到 ViewModel 再进行具体业务无关的界面刷新。如此,ViewModel 将可以在任何地方复用。

使用 YBHandyList 后,ViewModel 把 Cell 与外部业务解开耦合,只把需要暴露的东西写在ViewModel .h中,外部业务无需导入 Cell 便能通过 ViewModel 直接复用,更加的安全。

能拓展原生支持的场景

一个基础设施最怕的就是不能满足所有场景的情况下还封闭了拓展的入口。YBHandyList 通过继承默认代理实现类就能拓展实现其它的 UITableView / UICollectionView 代理方法。

这看起来有些繁琐,使用多代理技术能避免额外的创建代理实现类,但这样会导致代码不再简单和透明。换个角度想,代理实现类中将大量复杂逻辑处理过后,仅仅回调给外部业务一个简单的方法,达到为外部模块瘦身的目的。

后语

笔者一直偏好简洁的代码设计,让核心功能最小化实现,当它无法覆盖所有的场景时一定要有原生拓展能力。语法糖的主要意义是减少使用者的思考成本而不单单是为了少写两句代码,它不应该侵入功能收敛的核心代码。要做好这一切,就一定要透过现象看清问题的本质。

链接:https://www.jianshu.com/p/f0a74d5744b8

收起阅读 »

iOS 应用内打开三方地图app直接导航

当然因为有需求喽。疯狂试探- (BOOL)canOpenURL:(NSURL *)url NS_AVAILABLE_IOS(3_0);常用地图应用的url Scheme://百度地图 baidumap //高德地图 iosamap //谷歌地图 comgo...
继续阅读 »

当然因为有需求喽。

疯狂试探
- (BOOL)canOpenURL:(NSURL *)url NS_AVAILABLE_IOS(3_0);

常用地图应用的url Scheme:

//百度地图  
baidumap
//高德地图
iosamap
//谷歌地图
comgooglemaps
//腾讯地图
qqmap
//其他地图省略
….

苹果地图不需要,iOS API提供了一个跳转打开方法。
注意IOS9之后,plist里面设置url scheme白名单

<key>LSApplicationQueriesSchemes</key>
<array>
<string>qqmap</string>
<string>comgooglemaps</string>
<string>iosamap</string>
<string>baidumap</string>
</array>

在下用的是高德坐标
高德转坐标类型枚举

//        AMapCoordinateTypeBaidu = 0,    ///<Baidu
// AMapCoordinateTypeMapBar, ///<MapBar
// AMapCoordinateTypeMapABC, ///<MapABC
// AMapCoordinateTypeSoSoMap, ///<SoSoMap
// AMapCoordinateTypeAliYun, ///<AliYun
// AMapCoordinateTypeGoogle, ///<Google
// AMapCoordinateTypeGPS, ///<GPS

在下试过转百度用AMapCoordinateTypeBaidu,这样一一对应的方式转,但跳转之后误差很大,后来我试着杂交匹配一下,所有地图使用Google转法最准,所以除高德地图都用了Google转出的坐标

重点来了!!!!

- (void)pushMapLan:(CGFloat)lan Lon:(CGFloat)lon pointName:(NSString *)title {

UIAlertController *alertSheet = [UIAlertController alertControllerWithTitle:title message:@"请选择以下驾车导航方式" preferredStyle:UIAlertControllerStyleActionSheet];

// 高德坐标转换百度坐标
CLLocationCoordinate2D gps = AMapCoordinateConvert(CLLocationCoordinate2DMake(lan,lon), AMapCoordinateTypeGoogle);
// --------------------------------------------------

if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"baidumap://"]]) {
NSMutableDictionary *baiduMapDic = [NSMutableDictionary dictionary];
baiduMapDic[@"title"] = @"百度地图";
NSString *urlString = [[NSString stringWithFormat:@"baidumap://map/direction?origin={{我的位置}}&destination=latlng:%f,%f|name=北京&mode=driving&coord_type=gcj02",gps.latitude,gps.longitude] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
baiduMapDic[@"url"] = urlString;
[alertSheet addAction:[UIAlertAction actionWithTitle:baiduMapDic[@"title"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:baiduMapDic[@"url"]]];
}]];
}

//高德地图
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"iosamap://"]]) {
NSMutableDictionary *gaodeMapDic = [NSMutableDictionary dictionary];
gaodeMapDic[@"title"] = @"高德地图";
NSString *urlString = [[NSString stringWithFormat:@"iosamap://navi?sourceApplication=%@&backScheme=%@&lat=%f&lon=%f&dev=0&style=2",@"导航功能",@"poapoaaldoerccbadersvsruhdk",lan,lon] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
gaodeMapDic[@"url"] = urlString;
[alertSheet addAction:[UIAlertAction actionWithTitle:gaodeMapDic[@"title"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:gaodeMapDic[@"url"]]];
}]];
}

//谷歌地图
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"comgooglemaps://"]]) {
NSMutableDictionary *googleMapDic = [NSMutableDictionary dictionary];
googleMapDic[@"title"] = @"谷歌地图";
NSString *urlString = [[NSString stringWithFormat:@"comgooglemaps://?x-source=%@&x-success=%@&saddr=&daddr=%f,%f&directionsmode=driving",@"驾车导航",@"poapoaaldoerccbadersvsruhdk",gps.latitude,gps.longitude] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
googleMapDic[@"url"] = urlString;
[alertSheet addAction:[UIAlertAction actionWithTitle:googleMapDic[@"title"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:googleMapDic[@"url"]]];
}]];
}

//腾讯地图
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"qqmap://"]]) {
NSMutableDictionary *qqMapDic = [NSMutableDictionary dictionary];

qqMapDic[@"title"] = @"腾讯地图";
NSString *urlString = [[NSString stringWithFormat:@"qqmap://map/routeplan?from=我的位置&type=drive&tocoord=%f,%f&to=终点&coord_type=1&policy=0",gps.latitude,gps.longitude] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
qqMapDic[@"url"] = urlString;
[alertSheet addAction:[UIAlertAction actionWithTitle:qqMapDic[@"title"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:qqMapDic[@"url"]]];
}]];
}
NSMutableDictionary *iosMapDic = [NSMutableDictionary dictionary];
iosMapDic[@"title"] = @"苹果地图";
[alertSheet addAction:[UIAlertAction actionWithTitle:iosMapDic[@"title"] style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
CLLocationCoordinate2D gps = AMapCoordinateConvert(CLLocationCoordinate2DMake(lan,lon), AMapCoordinateTypeGoogle);

MKMapItem *currentLoc = [MKMapItem mapItemForCurrentLocation];
MKMapItem *toLocation = [[MKMapItem alloc] initWithPlacemark:[[MKPlacemark alloc] initWithCoordinate:gps addressDictionary:nil]];
toLocation.name = title;
NSArray *items = @[currentLoc,toLocation];
NSDictionary *dic = @{
MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving,
MKLaunchOptionsMapTypeKey : @(MKMapTypeStandard),
MKLaunchOptionsShowsTrafficKey : @(YES)
};

[MKMapItem openMapsWithItems:items launchOptions:dic];
}]];

[alertSheet addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
}]];

[self presentViewController:alertSheet animated:YES completion:nil];

}

关于转换坐标,如果你用的是高德坐标系,除了高德地图不用转坐标系外,其他的都以高德sdk里转Google的方式转,保证目的地精确。
其他的就自己探索咯!
建议 第三方的转换方法【github】地址,找不到合适的尝试这个。

链接:https://www.jianshu.com/p/8622b6ff83b3

收起阅读 »

Swift之构建非常优雅的便利API—Swift中的计算属性

使Swift成为如此强大且通用的语言的主要原因在于,当我们选择在为特定问题形成解决方案时选择使用哪种语言功能时,我们通常可以使用多种选项。然而,这种多样性也可能引起混淆和争论,特别是当我们正在考虑的功能的关键用例之间没有明确的界限时。本周,我们来看看一个这样的...
继续阅读 »

使Swift成为如此强大且通用的语言的主要原因在于,当我们选择在为特定问题形成解决方案时选择使用哪种语言功能时,我们通常可以使用多种选项。然而,这种多样性也可能引起混淆和争论,特别是当我们正在考虑的功能的关键用例之间没有明确的界限时。

本周,我们来看看一个这样的语言特性 - 计算属性 - 以及它们如何让我们构建非常优雅的便利API,如何避免在部署它们时意外隐藏性能问题,以及在计算属性之间进行选择的一些不同策略和方法。

属性用于数据

理想情况下,属性是计算还是存储应该只是一个实现细节 - 特别是因为只要查看它所使用的代码就无法确切地知道属性是如何存储的。因此,就像存储的属性如何构成类型存储的数据一样,计算属性可以被视为在需要时计算类型数据的方法。

假设我们正在制作一个用于收听播客的应用程序,并且使用如下所示的State枚举来模拟给定播客剧集所处的状态(是否已经下载,收听等)。

extension Episode {
enum State {
case awaitingDownload
case downloaded
case listening(progress: Double)
case finished
}
}

然后,我们为我们的Episode模型提供一个存储的state属性,我们可以根据给定的剧集状态来制作决策 - 例如,能够向用户显示是否已经下载了一集。但是,由于该特定用例在我们的代码库中非常常见,我们不希望必须state在许多不同的地方手动打开- 所以我们还提供Episode了一个isDownloaded我们可以在需要的地方重用的计算属性:

extension Episode {
var isDownloaded: Bool {
switch state {
case .awaitingDownload:
return false
case .downloaded, .listening, .finished:
return true
}
}
}

我们在state上面开启而不是使用if或guard声明的原因是,如果我们在我们的State枚举中添加一个新案例,那么“强迫”我们自己更新这个代码- 否则我们可能会以不正确的方式处理这个新案例了解它。

上面的实现可以说是计算属性的一个很好的用例 - 它消除了样板,增加了便利性,并且就像它是一个只读存储属性一样 - 它的全部意义在于让我们访问模型数据的特定部分。

意外瓶颈

现在让我们来看看硬币的另一面 - 如果我们不小心,计算属性虽然非常方便,但有时最终会导致意外的性能瓶颈。继续上面的播客应用程序示例,假设我们为用户的播客订阅库建模的方式是通过Library结构,该结构还包含类似上次服务器同步发生时的元数据:

struct Library {
var lastSyncDate: Date
var downloadNewEpisodes: Bool
var podcasts: [Podcast]
}

虽然Podcast我们需要在我们的应用程序中呈现上述大多数视图的所有模型,但我们确实有一些地方可以将所有用户的播客显示为平面列表。就像我们之前Episode使用isDownloaded属性扩展一样,最初的想法可能是在这里做同样的事情 - 添加一个计算allEpisodes属性,收集用户库中每个播客的所有剧集 - 如下所示:

extension Library {
var allEpisodes: [Episode] {
return podcasts.flatMap { $0.episodes }
}
}

要了解更多信息flatMap,请查看“Map,FlatMap和CompactMap”基础知识文章。

上面的API可能看起来非常简单 - 但它有一个相当大的缺陷 - 它的时间复杂度是线性的(或O(N)),因为为了计算我们的allEpisodes属性,我们需要一次遍历所有播客。一开始这似乎不是什么大不了的事 - 但是在我们每次我们在一个单元格中出列单元格时访问上述属性时,在这种情况下可能会出现问题UITableView:

class AllEpisodesViewController: UITableViewController {
...

override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: reuseIdentifier,
for: indexPath
)

// Here we're accessing allEpisodes just as if it was a
// stored property, and there's no way of telling that this
// will actually cause an O(N) evaluation under the hood:
let episode = library.allEpisodes[indexPath.row]
cell.textLabel?.text = episode.title
cell.detailTextLabel?.text = episode.duration

return cell
}
}

由于表格视图单元格可以以非常快的速度出列 - 当用户滚动时 - 上述代码迟早会成为性能瓶颈,因为我们当前的allEpisodes实现将在每次访问时不断迭代所有播客。不是很好。

虽然收集每个播客的所有剧集本身就是O(N)我们当前模型结构的一个操作,但我们可以改进我们通过API 发出复杂信号的方式。我们不是将它allEpisodes看作另一个属性,而是让它成为一种方法。这样一来,它看起来更像是一个行动正在执行(这是),而不是访问一个数据块的只是一个快速的方法:

extension Library {
func allEpisodes() -> [Episode] {
return podcasts.flatMap { $0.episodes }
}
}

如果我们还更新我们AllEpisodesViewController接受一组剧集作为其初始化程序的一部分,而不是Library直接访问我们的模型,那么我们得到以下调用站点 - 它看起来比我们之前的实现更清晰:

let vc = AllEpisodesViewController(episodes: library.allEpisodes())

在我们的视图控制器中,我们仍然可以像以前一样继续访问所有剧集 - 只是现在该阵列只构建一次,而不是每次单元格出列时都是如此,这是一个很大的胜利。

方便懒惰

将任何无法在常规时间内执行的计算属性转换为方法通常会提高API的整体清晰度 - 因为我们现在强烈表示存在与访问它们相关的某种形式的成本。但在这样做的过程中,我们也失去了一些使用属性给我们带来的“优雅”。

然而,在许多这样的情况下,实际上有一种方法可以实现清晰,优雅和性能 - 所有这些都在同一时间。为了能够继续使用属性,而不必通过使用延迟评估来预先完成所有处理工作。

就像我们在“Swift序列:懒惰的艺术”和“Swift中的字符串解析”中看到的那样,推迟迭代序列直到它实际需要可以给我们带来显着的性能提升 - 所以让我们来看看如何我们可以使用该技术将其allEpisodes转变为属性。

我们将首先Library使用两种新类型扩展我们的模型 - 一种用于我们的剧集序列,另一种用于迭代该序列中的元素:

extension Library {
struct AllEpisodesSequence {
fileprivate let library: Library
}

struct AllEpisodesIterator {
private let library: Library
private var podcastIndex = 0
private var episodeIndex = 0

fileprivate init(library: Library) {
self.library = library
}
}
}

要变成AllEpisodesSequence第一类Swift序列,我们所要做的就是Sequence通过实现makeIterator工厂方法使其符合:

extension Library.AllEpisodesSequence: Sequence {
func makeIterator() -> Library.AllEpisodesIterator {
return Library.AllEpisodesIterator(library: library)
}
}

接下来,让我们的迭代器符合要求IteratorProtocol,并实现我们的实际迭代代码。我们将通过阅读播客中的每一集来做到这一点,当没有更多的剧集可以找到时,我们将继续播放下一个播客 - 直到所有剧集都被退回,如下所示:

extension Library.AllEpisodesIterator: IteratorProtocol {
mutating func next() -> Episode? {
guard podcastIndex < library.podcasts.count else {
return nil
}

let podcast = library.podcasts[podcastIndex]

guard episodeIndex < podcast.episodes.count else {
episodeIndex = 0
podcastIndex += 1
return next()
}

let episode = podcast.episodes[episodeIndex]
episodeIndex += 1
return episode
}
}

有了上述内容,我们现在可以自由地allEpisodes转回计算属性 - 因为它不再需要任何前期评估,只需AllEpisodesSequence在常量时间内返回一个新实例:

extension Library {
var allEpisodes: AllEpisodesSequence {
return AllEpisodesSequence(library: self)
}
}

虽然上述方法需要的代码多于我们之前的代码,但它有一些关键的好处。第一个是现在完全不可能简单地下标到allEpisodes返回的序列,因为Sequence这并不意味着随机访问任何底层元素:

// Compiler error: Library.AllEpisodesSequence has no subscripts
let episode = library.allEpisodes[indexPath.row]

这看起来似乎不是一个好处,但它阻止我们意外地造成我们之前遇到的那种性能瓶颈 - 迫使我们将我们的allEpisodes序列复制到一个Array之前我们将能够随机访问其中的剧集它:

let episodes = Array(library.allEpisodes)
let vc = AllEpisodesViewController(episodes: episodes)

虽然每次我们想要阅读一集时都没有什么能阻止我们执行上面的数组转换 - 但是当我们意外地订阅一个看起来像是存储的数组时,这是一个更加慎重的选择。比计算的。

另一个好处是,如果我们所寻找的只是一小部分,我们不再需要从每个播客中不必要地收集所有剧集。例如,如果我们只想向用户展示他们下一个即将到来的剧集 - 我们现在可以简单地这样做:

let nextEpisode = library.allEpisodes.first

使用延迟评估的好处在于,即使allEpisodes返回序列,上述操作也具有恒定的时间复杂度 - 就像您期望访问first任何其他序列一样。太棒了!

这都是关于语义的
既然我们能够将复杂的操作转换为计算属性,而无需任何前期评估,那么最大的问题是 - 无参数方法的剩余用例是什么?

答案很大程度上取决于我们希望给定API具有哪种语义。属性非常意味着某种形式的访问值或对象的当前状态 - 而不更改它。所以修改状态的任何东西,例如通过返回一个新值,很可能更好地用一个方法表示 - 比如这个,它更新了state我们Episode之前的一个模型:

extension Episode {
func finished() -> Episode {
var episode = self
episode.state = .finished
return episode
}
}

将上述API与使用属性的情况进行比较 - 很明显,一种方法为这种情况提供了恰当的语义:

// Looks like we're performing an action to finish the episode:
let finishedEpisode = episode.finished()
// Looks like we're accessing some form of "finished" data:
let finishedEpisode = episode.finished

许多相同的逻辑也可以应用于静态API,但我们可能会选择做出某些例外,特别是如果我们要优化使用点语法调用的API 。有关设计此类静态API的一些示例,请参阅“Swift中的静态工厂方法”和Swift中基于规则的逻辑”。

结论

计算属性非常有用 - 并且可以使我们能够设计更简单,更轻量级的API。但是,重要的是要确保这种简单性不仅被感知,而且还反映在底层实现中。否则,我们冒着隐藏性能瓶颈的风险,在这种情况下,通常更好的选择方法 - 或者在适当时部署延迟评估。

链接:https://www.jianshu.com/p/315e4522c7c8

收起阅读 »

HashMap原理底层剖析

注意以下文章可能有描述和理解上的错误,如果出现错误请到评论区指出,我会第一时间修改问题。也希望文章能解决你的疑惑。 HashMap结构图 HashMap底层数据结构:Entry数组+链表+红黑树(JDK1.8版本) Entry+链表(JDK1.7版本)...
继续阅读 »



注意以下文章可能有描述和理解上的错误,如果出现错误请到评论区指出,我会第一时间修改问题。也希望文章能解决你的疑惑。



HashMap结构图


HashMap底层数据结构:Entry数组+链表+红黑树(JDK1.8版本) Entry+链表(JDK1.7版本)
在这里插入图片描述




代码分析


常见的参数及意义


	//默认的Hash表的长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//Hash表的最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表的长度为8的时候转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶中元素个数小于6的时候红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
//只有当数组的长度大于等于64并且链表个数大于8才会转换为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//Hash表
transient Node<K,V>[] table;
//遍历的时候使用返回一个K-V集合
transient Set<Map.Entry<K,V>> entrySet;
//表中K-V的个数
transient int size;
//对集合的修改次数,主要是后面出现的集合校验
transient int modCount;
//阈值当size大于threshold时就会进行resize
int threshold;
//加载因子
final float loadFactor;

源码解释


构造方法


//传入初始化容量,和指定的加载因子
public HashMap(int initialCapacity, float loadFactor) {
//参数校验
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果传入的值大于最大容量,就将最大的值赋给他
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//参数校验
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//返回的是2的整数次幂
this.threshold = tableSizeFor(initialCapacity);
}

//指定HashMap的容量
public HashMap(int initialCapacity) {
//调用如上的双参构造函数
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//无参构造函数
public HashMap() {
//初始化加载因子为默认的加载因子
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//构造一个映射关系与指定 Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
//初始化加载因子为默认的加载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
//构造的过程函数
putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取m集合中元素个数
int s = m.size();
//如果m集合元素个数是0个那么下面这些操作也就没有必要了
if (s > 0) {
if (table == null) { //表示的拷贝构造函数调用putMapEntries函数,或者是构造了HashMap但是还没有存放元素
//计算的值存在小数所以+1.0F向上取整
float ft = ((float)s / loadFactor) + 1.0F;
//将ft强制转换为整形
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果计算出来的值大于当前HashMap的阈值更新新的阈值为2次方
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)//如果Map集合元素大于当前集合HashMap的阈值则进行扩容
resize();
//将Map集合中元素存放到当前集合中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}

size函数


 //返回key-val的数量
public int size() {
return size;
}

isEmpty函数


   //当前的集合是否为null
public boolean isEmpty() {
return size == 0;
}

get具体过程函数


//根据key获取对应的val
public V get(Object key) {
Node<K,V> e;
//通过hash值,key找到目标节点再返回对应的val
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//获取key对应的节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//如果集合为空和对应的下标数组中的值为空直接返回null
//first = tab[(n - 1) & hash]数组的长度是2n次方减1后对应位全部变为1,这样为与操作永远都会在数组下标范围内不会越界
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // 如果第一个节点hash与对应hash相等,并且key也相等则返回当前节点
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//第一个节点的下一个节点不为null
if ((e = first.next) != null) {
//判断节点是否为树形
if (first instanceof TreeNode)
//在树形结构中查找节点并返回
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//通过do...while结构遍历找对应key的节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//找到节点并返回
return e;
} while ((e = e.next) != null);
}
}
//未找到对应的节点
return null;
}

containsKey函数


	//查看是否包含指定key    
public boolean containsKey(Object key) {
//通过getNode返回是否为null判断是否存在key
return getNode(hash(key), key) != null;
}

put函数


在此之前先看一下put的过程
在这里插入图片描述


//调用putVal向当前集合中存放元素并返回对应的val
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

//存放对应的key-val
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前集合为null则将集合扩容并且将新的存放结构赋值给tab
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//找到key存放的链表,如果为空直接将当前节点存放链表在第一个位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { //当前为链表不为null
Node<K,V> e; K k;
//表示当前链表第一个位置key已经存在,将当前节点赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//查看当前的节点是否属于树形结构如果是则在TreeNode中查找并将赋值给e
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
//找到当前存放位置节点的最后一个节点的next并将当前要插入的节点插入
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 链表的长度为8的时候转化为红黑树减一是因为元素从0开始
treeifyBin(tab, hash);
//跳出死循环
break;
}
//表示的是当前链表已经存在当前要插入的key,HashMap不存在重复的key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//将节点后移
p = e;
}
}
if (e != null) { // 当前节点不为null将e.val存放在oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//不管oldValue是否为null都会发生value赋值给e.value
//当出现重复的key之后上面会将节点保存给e并未修改新的val值,在此更新
e.value = value;
//将结点向后调整到最后面
afterNodeAccess(e);
//如果为null返回null,不为null返回对应的val
return oldValue;
}
}
//++modCount对其集合操作的次数+1
++modCount;
if (++size > threshold)//如果在放入元素以后大于阈值则进行2倍扩容
resize();
afterNodeInsertion(evict);
return null;
}


resize函数


 //将集合扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//旧表的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//之前的阈值
int oldThr = threshold;
int newCap, newThr = 0;
//这里也可以说集合不为空
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//如果集合现在数组的长度大于等于最大容量
threshold = Integer.MAX_VALUE;//将整型最大的值赋值给threshold
return oldTab;
}
//当前集合数组长度扩大二倍赋值给newCap小于MAXIMUM_CAPACITY
//并且集合的容量大于等于默认容量将当前阈值扩大二倍赋值给新的阈值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//若没有经历过初始化,通过构造函数指定了initialCapcity,将当前容量设置为大于它最小的2的n次方
else if (oldThr > 0)
newCap = oldThr;
else { // 初始的时候长度和阈值都使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//重新计算threshold
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新当前集合阈值
threshold = newThr;
//从这里开始便是将oldTab数据重新hash放入扩容后的newTab
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将table指向的oldTab指向newTab
table = newTab;
if (oldTab != null) {
//遍历哈希表
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//当前链表是否为null、并且将就链表赋值给e
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//将原来位置的链表置为null方便垃圾回收
if (e.next == null)//链表的长度为1直接将链表中的一个节点重新hash存放到相应的位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) //表示节点类型为树形结构
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //链表是非树形结构,并且节点数量是大于1
//将链表拆分为两个子链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do { //通过do...while遍历链表
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null) //设置头节点
loHead = e;
else //设置尾结点
loTail.next = e;
loTail = e;//将尾结点变为最后一个节点
}
else {
if (hiTail == null)//同上都是设置头节点下面也一样是设置尾结点
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {//在新表的j位置存放链表
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//在新表的j+oldCap位置存放链表
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

remove函数


 	// 移除指向key返回对应的val          
public V remove(Object key) {
Node<K,V> e;
//返回如果为空返回null否则返回e.val
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//常规的判断表不为null,key有对应的存储位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//表示的是key存储在当前链表的第一个位置
node = p;
else if ((e = p.next) != null) {//表示的是链表的长度大于1
if (p instanceof TreeNode)//判断是否是树的实列
//返回对应key在红黑树存储的位置
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {//当前结构为链表
do {//遍历链表
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {//找到对应的节点保存并跳出循环
node = e;
break;
}
//将节点后移
p = e;
} while ((e = e.next) != null);
}
}
//表示要删除的key存在并且找到
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)//如果是树形在树型结构中移除当前节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)//表示的链表中的第一个节点
tab[index] = node.next;
else
p.next = node.next;//移除节点
++modCount;//操作+1
--size;//长度-1
afterNodeRemoval(node);
//返回节点
return node;
}
}
return null;
}

clear函数


//清除集合中的所有key-value      
public void clear() {
Node<K,V>[] tab;
//集合操作+1
modCount++;
if ((tab = table) != null && size > 0) {//表不为null才进行遍历
size = 0;
for (int i = 0; i < tab.length; ++i)//遍历集合所有元素都置为null,方便垃圾回收
tab[i] = null;
}
}

containsValue函数


 	//查看集合是否包含指定value
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {//表不为null
for (int i = 0; i < tab.length; ++i) {//遍历数组
for (Node<K,V> e = tab[i]; e != null; e = e.next) {//遍历链表
if ((v = e.value) == value ||
(value != null && value.equals(v)))
//存在指定的value直接返回true
return true;
}
}
}
//集合中不存在指定value返回false
return false;
}

keySet函数


	//返回key的所有集合set
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}

values函数


	//返回所有的value集合    
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}

entrySet函数


   // 返回所有的key-value集合
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

面试常见的问题



  1. 为什么HashMap默认的长度为2的整数次幂?



    就是因为获取索引h&(length-1)可以保证散列的均匀,避免不必要的hash冲突。



  2. 为什么加载因子是0.75?大了会怎么样?小了会怎么样?



    首先加载因子是表示hash表的填满程度,当为0.75的时候是在对提高空间利用率和减少查询成本的折中,当大于0.75的时候填满的元素越多,空间利用率越高,但是冲突的概率变大;当小于0.75的时候填满的元素越少,空间利用率越低,但是冲突的概率变小。



  3. 什么是哈希冲突?如何解决?



    哈希冲突是指hash出来的地址被其他元素所占用;
    解决的方法
    1.链地址法
    解决的思路就是当出现冲突的时候将冲突的元素加入当前的链表之中
    在这里插入图片描述
    2.开放地址法
    开放地址法也称之为再散列。
    思路:如果映射的地址被占用了,在哈希函数值的基础上加上指定数值,这样就可以把冲突的地址给错开,然后重新开辟新的地址用来存储。根据增量值的不同,分为线性探测再散列和二次探测再散列
    在这里插入图片描述
    3.再哈希法
    这种方法就是构造多个不同的哈希函数,当哈希地址Hi=RH1(Key)发生冲突时,再计算Hi=RH2(Key)…直到哈希不冲突,这样的方法增加了计算的时间。
    4.建立公共溢区
    就是哈希表分成了两个表:一个是基础表,另外一个则是溢出表,凡是与基础表发生冲突的数据都会被添加到溢出表。



  4. 什么是扰动函数?怎么设计的?为什么这个设计?



    扰动函数是hash函数拿到k的hashcode值,这个值是一个32位的int,让高16位与低16位进行异或。
    理论上来说字符串的hashCode是一个int类型值,那可以直接作为数组下标了,且不会出现碰撞。但是这个hashCode的取值范围是[-2147483648, 2147483647],有将近40亿的长度,谁也不能把数组初始化的这么大,内存也是放不下的。
    混合原始哈希码的高位和低位,以此来加大低位的随机性,这样设计在一定的程度上减少了hash碰撞,优化了散列的效果 。



  5. JDK1.8在对HashMap较1.7有什么优化?



    1.首先是最重要的就是底层的数据结构,1.7的时候底层数据结构是数组+链表;而在1.8的时候变成了数组+链表+红黑树
    2.在哈希上1.7扰动四次,1.8做了一次扰动,可以提高效率
    3.1.7在进行resize扩容的时候是重新哈希,1.8的时候采用的是索引位置不变或者就是就哈希表的容量+当前索引。
    4.1.7采用插入方式是头插法,1.8采用的是尾插法。



  6. 为什么1.8扩容不用重新哈希?
    在这里插入图片描述


  7. HashMap线程安全吗?为什么不安全?怎么解决不安全?



    首先HashMap是线程不安全的。JDK1.7的时候采用头插法,多线程同时插入的时候,A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环。JDK1.8采用尾插法,会造成两种情况两个线程同时插入只有一个成功插入,还有就是可能会造成两次resize(++size > threshold) 。解决的方案:一、使用HashTable效率比较差。二、使用ConcurrentHashMap比较常用的。三、使用Collections.synchronizedMap() 以上三种线程安全。



  8. HashMap内部节点是有序的吗?



    不是有序的。有序的Map集合有LinkedHashMap、TreeMap



  9. HashMap一般采用什么作为key?



    HashMap一般采用String、Integer 等类作为key、因为这些类底层已经重写了hashcode、equals方法,用的是final修饰类在多线程情况下相对安全。



  10. 为什么重写equals还要重写hashcode?



    比如HashMap中不允许存在相同的key,当重写了equals方法没有重写hashcode方法,当两个对象中的值相同,但是他们hashcode不同会造成比如
    class newInstance1 = new class(1);
    class newInstabce2 = new class(2);
    以上的比较对象的时候hashcode不同,equal方法比较返回false;但是重写Hashcode后,可以达到返回true。





如果看完觉得得到帮助就留下你的一键三连,谢谢 注意如果有错误的地方评论区提出来我立即更正谢谢大佬的指正。


收起阅读 »

自学编程的人,90%以上都会掉进这些坑,避开这些误区能提高N倍学习效率

前言 几乎每一个程序员都会走上那么一段自学的道路,尤其是在校生或进入工作岗位之后,技术的提升基本都靠自学,有的虽然是网上报班学习,但更多时候还是自己在学习,师傅引进门,修行靠个人。 有的人自学很快,几乎一个多月就能掌握一门技术,而有的人苦苦坚持,最后还是半...
继续阅读 »


前言


几乎每一个程序员都会走上那么一段自学的道路,尤其是在校生或进入工作岗位之后,技术的提升基本都靠自学,有的虽然是网上报班学习,但更多时候还是自己在学习,师傅引进门,修行靠个人。


有的人自学很快,几乎一个多月就能掌握一门技术,而有的人苦苦坚持,最后还是半途而废,很大的原因就在于在学习的时候掉进了一些误区没能走出来。


今天我们就来讲讲自学编程常见的十大误区,避开这些误区我们定能在自学之路上一往无前!


呕心沥血所写8000字文章,希望对你们有所帮助。
在这里插入图片描述


误区一:不重基础,什么火我就学什么


我们经常碰到一些爱学习的小伙伴,他们热衷于学习各种潮流技术,而且是跳着过去学的,因为基础知识往往是比较枯燥无味且成就感比较低的,于是就跳跃式性学习,什么技术火就去学习什么技术。


比如你的java se基础不牢,比如说你连最基本的oop、网络操作系统、基本的数据结构和算法,还有常用的设计模式,最基本的多线程高并发这些概念都没有搞懂,那么你就一味的去追求这些所谓的火的、时髦的技术,你可能会陷入一个迷茫。


建议:我们不反对学习新技术,但在学习新技术之前,应该先把基础知识牢牢掌握,切勿心急吃热豆腐最后烫嘴,初学编程的小伙伴儿一定要扎扎实实、老老实实地把基础知识弄懂。
Python建议掌握的计算机基础


误区二:总纠结于学最好的编程语言


编程语言本身没有好与坏之分,只有什么语言适合什么样的应用场景。


我举几个例子,比如说PHP这个语言,它比较适合做外部开发,java它适合做安卓、大数据,或者是我们的java EE,C和C++它比较适合做底层开发,比如说像游戏引擎、系统软件,你再比如说汇编语言适合做驱动开发,比如python它比较适合做人工智能,go语言它是区块链的主力开发语言,适合做也可以适合做外部后台等等。


所以说从上面我所讲的,你有没有发现就没有最好的编程语言这种说法。


那么你究竟学什么样的编程语言,主要是要依据你学习的目标和兴趣。你学完这个编程语言你想干什么,然后你再决定我学什么编程语言,用倒推法来看问题你就会发现你的出发点是否正确,同一个目标,路可以有很多条,但如果你一来就开始选定出发点(编程语言),你可以能就错过了一条更省时省力的路线。
在这里插入图片描述


忠告大家两点:


第一点,编程语言它从逻辑语法上其实都是非常相似的。你比如说java、C、C++或者其他编程语言,其实逻辑语法和结构其实都大同小异。你只要学会了或者精通了一门语言,你再去学别的编程语言,可以这么说,触类旁通,比较轻松,而且学习成本会大大降低,但前提是你要先学精通一门。


第二点,如果你学编程就是为了进入到IT行业或者进入到互联网,就是为了找一份工作。而你们自己目前并不知道学什么语言,如果是这种诉求的话,我建议可以考虑Python,原因特别简单,一是Python容易上手,很多人自学的时候因为入门困难就半途而废了,别想那么多,先把你自己想做的东西用编程思维呈现出来;二是Python的发展趋势很迅猛,已经连续四年在所有编程语言中增长率第一,成为最受欢迎的编程语言,未来很长,我们要有前瞻性地进行学习。


误区三:喜欢看不喜欢动手,听懂了但不会用


喜欢看但不喜欢动手,这几乎是绝大多数初学者的通病,我依稀记得我初学编程的时候也是这样,学着学着也学了蛮久的,可一上手代码就不知所措,感觉很难驾驭。


听懂和能使用是两码事,初学者看书或者听视频,他很容易感觉这个老师讲的我听懂了。但是一旦独立让他去做一个项目或者去完成一道题,他立马就没有思路了,马上蒙圈,结果都不明白了,我不知道大家有没有这种感受。


有的同学就说“老师我在学的时候我就不知道写什么”,那么我可以建议大家,你就把你看的书或者是教程什么的,你把看过的项目案例给我敲一遍并且理解了,然后你根据你想做什么项目,实现一个什么功能,再具体地去写相应的代码。


我举个例子,比如你想去这个健身,练一身迷人的肌肉或腹肌,我问大家一个问题,**如果你只是看教练健身的视频,你能否长出健壮的肌肉?**其实你是长不出来的,你光看怎么可能长肌肉呢?你必须要自己去天天这个举哑铃或者是运动,各种运动按照教练的要求,对不对?你的饮食上还要注意,才能长出迷人的肌肉。


其实我们学编程也是一样的道理,你光看视频是不行的,你听懂了并不代表你会用。
在这里插入图片描述


忠告:


编程它是一门做中学的学科,什么叫做中学?是在做的过程中学会的,而不是说我已经知道了我理解了我再去做,因为我们的计算机学科它是一门工科性质的,特别强调是动手能力。过程出错不要紧,多做几遍,多调试几遍,再不懂就去查资料或者找人问问,一点点地融会贯通。


纸上得来终觉浅,绝知此事要躬行。出现错误不可怕,出现错误就是你提高的关键点了。


误区四:学习时很少做笔记或思维导图


这基本等于说没有将学到的知识转成自己的知识。很多自学者都出现这种情况,学编程时非常努力和勤奋,但是技术提升其实很慢。你让他说今天学了什么东西?他一脸茫然,他只知道我好像看了一些书、看一些视频。
在这里插入图片描述
那么为什么会出现这个情况?我给大家分析一下。因为有些小伙伴儿他在学技术的时候只是听,知识就像流水一样,从书本或者视频流出来,然后知识并没有沉淀在他的大脑,整个人就变成了一个知识的一个传输器。


因此当把这个书本一合上,就跟没有学过这个一样,毫无印象,全部忘了这个我们把它称之为无效学习


学知识不管你是听视频还是看书,一定要明白一个道理,这些知识不是你的,是作者的,那么你应该怎么办呢?你最好是学完一个知识或者学完一个技术以后,你要用自己的语言把它总结下来,用自己的语言描述一下你是怎么理解这个技术的,这样你才能真正地把这个知识点拿下来,甚至最好能够学完一个知识体系的。


比如说把java的面向对象学完了,或者把java的多线程学完了,你最好画一个思维导图,把这些知识点精华浓缩一下,把这个知识体系建立在你的大脑里面。


自己总结和建立知识体系的方法,你学一年,甚至比别人学两年、三年效果都好。
在这里插入图片描述


误区五:喜欢死记硬背而不是理解原理


我们在学编程的时候,的确有很多东西是需要死记硬背的,比如说语法规则和规范,比如说像java变量的命名,还有这个程序的主体结构,包括它语法的一些规范,怎么用才是一个高效的用法,而这些呢你没有办法,你只能把它背一背,因为是规则,这是规定好的。


但是涉及到功能的实现、算法、设计模式、底层机制、优化效率等等,你就不要死记硬背了,一定要去理解了。


还有一点就是不要去纠结编程的时候我记不住代码,小伙伴们只需要掌握某一个功能,用什么、用怎样一个方式或者使用代码去实现就可以了,你要知道在哪儿去找这段代码就行。
在这里插入图片描述


误区六:出错时选择逃避


程序出错了,没有积极的去调错,而是逃避,这个现象我相信很多小伙伴,尤其是初学者都遇到过,新手写代码的时候只要看到报错信念就发慌了。


其实告诉大家,大可不必,因为程序一旦出错了,正是我们提高自己技术的时候。你想一想,对一个初学者他怎么可能写代码一行都不出错呢?这是不可能的,对吧?你比如说一些基本语法出错了,或者是字母大小写写错了,这是很容易出现的。


其实这些错误它本身并不难,你只要把每一个错误都排除了,其实这个时候你的能力就越来越强。正是因为这些错误你看得多了,你下次就不会犯,慢慢你就感觉到编程其实挺快乐的,以后你就会达到这样一个水平。累了困了不是喝红牛,而是写两段代码放松放松,达到这个水平就很好了。
在这里插入图片描述
借用电影《头文字D》里面的一句话“神其实也是人,只是他做了人做不到的事情,所以成为了神。”


那么什么是编程大神、调试大神?就是因为他们犯了足够多的错误,什么地方会犯错,什么错误他都见过了,别人看起来是错误的东西,在他手里就不会出现,或者出现了解决起来也是轻而易举,这就是大神了。


你如果犯了一万个错误,你也会成为编程大神。


误区七:孤军奋战,闭门造车


在程序员这条路上总有那么一些人喜欢孤军奋战、闭门造车,不愿意,也不敢分享。包括有些已经学过一两年甚至更久的人都会出现这个问题。


在学习编程的道路上,你一定要给自己营造一个学习的氛围,你需要同伴或者是高手跟你一同成长。反之,如果你脱离了交流,往往会让学习的问题越积越多,最后你可能就放弃了。


因为问题太多了嘛,你又没解决,那肯定慢慢就放弃了,孤军奋战很容易让新手成为井底之蛙。在迷茫的时候,大家知道有时候一个朋友或者一个高手的一句话、一个插件、一本书或者一个提示,它就会让你有一种豁然开朗的感觉。你会说“诶原来还有这样的一种操作”,那么这就可能让你的学习效率大大提升。
在这里插入图片描述
忠告


初学编程的小友千万不要孤军奋战、闭门造车,要敢于分享,敢于去总结,敢于把自己所学到的东西给别人说出来,说错了又能咋滴?人家又不会顺着网线来打你;说错了别人会不会笑我?其实你只要是第一次犯,别人不会笑话,如果你提出来,别人反而会觉得你很勤奋,反而更愿意帮助你,你下次不要犯就好了。


误区八:学的很杂,不精通一门语言


很多小友在网上去搜这个资料,什么技术火,我就学什么技术,不分重点,也没有目标,很容易迷失自己,什么都想学,什么都没学精通,天天疲于奔命学各种技术,最后整个知识体系没有建立起来。


比如有些小友学java EE,但javaEE的整个体系没有建立起来;比如有些小友学大数据,整个体系也没有建立出来,它只是学了其中某一小块。如果这样的话,你会发现你在真正的工作中,当别人问你精通什么的时候,你答不上来。


那自然你的薪资肯定上不去,因为你没有没有真正能够吃饭的家伙,没有真正能拿得出手的东西,没有什么东西可以去跟别人竞争,所以说西瓜、芝麻什么都捡,肯定是要吃亏的。
在这里插入图片描述


忠告


不管你是聪明还是迟钝,不管你是勤奋还是懒惰,每个人的时间其实都是有限的,好钢要用在刀刃上,把自己有限的时间高效地利用起来,千万不要什么都去学,没有必要。


我建议小友们专注于一门技术或者语言,比如说我们就专注于Java或者专注于Python,那都无所谓,你就专注于这一门语言,你精通这一门语言以后,你再去学别的编程语言或者别的技术,你会发现一马平川,学习成本大大降低,触类旁通,学期会会非常的快。


学习编程技术的时候应该有一个明确的方向和目标。不管你是学java、Python、PHP、.net还是区块链,你要有一个学习目标。


比如说如果是为了工作,那么我2个月内要学完什么内容,半年后学到什么水平,一年后我要开始找工作,我希望我的薪资是多少,一年到两年后我要达到什么技术水平,三年后我的薪资要达到几万…给自己制定一个清晰的规划,不要自己给自己打马虎眼,你才会看清自己的成长。如果你自己无法制定,那么可以找一些人帮你参考,这都可以。


误区九:专业不对口,不适合编程?


这个误区其实我也经常遇到过,很多小友说:“龙叔,我以前是学管理的、学金融的、学建筑的、学化学的,我以后找工作是不是会吃亏呀?或者说我是不是就学不好编程了?”


我告诉大家,IT行业里面的程序员是非科班出身的人太多太多,各个行业的都有,太多都转行了,而且做得很成功,所以说这跟学专业也没有关系,为什么呢?


编程本身其实就是一个技能,跟你以前的专业有什么关系呢?没关系,你只要你的逻辑思维正常,然后你比较勤奋,那这个学编程就没问题,就是学一个技能而已,别想得太复杂。
在这里插入图片描述


而且我认为有些时候你跨专业进入到这个程序员这个圈,在某些时候还是有优势的。为什么这么说?


举个例子,公司给了你一个任务,让你去做一个项目是关于财务的一个项目,如果你以前是金融相关专业的,那你理解这个项目的业务逻辑和业务流程肯定要比以前没有学过金融的要快。这就是为什么像用友这样的软件公司每一年都会在什么财经大学里面招一帮学生去做程序员,为什么?做业务逻辑的。


所以我们写软件或者做项目不单是技术本身,还有业务逻辑、业务流程在里边,所以不用担心这个事儿。如果你确实还担心,那怎么办?也很简单,你大不了就把大学计算机专业的课本内容学一遍嘛,这有什么大不了的,它是ok的。


误区十:数学不好,不适合编程?


很多小伙伴没有搞清楚数学和编程之间的关系,他们往往把数学和编程化等号,他们认为我数学好,我编程就学得好,我数学不好那么我编程就学不好。


如果真的是这样子的话,那我们大学里面就不需要再分两个专业了,一个是数学专业,一个是计算机专业,对吧?数学专业和计算机专业是分开的,这说明这两个学科它肯定是不一样的。


那么为什么会造成这样一个认识?因为有些小伙伴认为我们在处理这个业务逻辑的时候,可能会去用到数学。这个是不假的,可能会用到一些关于数学的公式、数学的推断,这些可能会用到,但是对于我们绝大部分的程序员来讲,我们是站在应用层面来编程的,换言之,我们是用别人已经学好的一个算法,然后应用到我们的一个业务模块里面去解决这个问题。我们很少让一个程序员,尤其是应用层面的程序员去自己独立开发一个算法、解决一个问题。


当然话也说回来了,假如你从事的这个岗位是算法工程师或者是高级数据分析师,那么对数学会要求高一些,往往要求这个人既懂计算机又是数学专业的。但对我们绝大部分的程序员来说,数学要求其实没有那么高,所以大家不要去恐惧这个事情。
在这里插入图片描述


在这里插入图片描述
文章到这里就结束了,感谢你的阅读,只是有些话想对读者们说说。


我退休后一直在学习如何写文章,说实在的,每次在后台看到一些读者的回应都觉得很欣慰,对于自媒体我是个刚入门的人,还是个迟钝的大叔…为了感谢读者们,我想把我收藏的一些编程干货贡献给大家,回馈每一个读者,希望能帮到你们。


干货主要有:


 2000多本Python电子书(主流和经典的书籍应该都有了)


 Python标准库资料(最全中文版)


 项目源码(四五十个有趣且经典的练手项目及源码)


 Python基础入门、爬虫、web开发、大数据分析方面的视频(适合小白学习)


⑤Python学习路线图(可以弄清楚Python的所有方向和技术)


*如果你用得到的话可以直接拿走,在我的QQ技术交流群里(纯技术交流和资源共享,广告勿入)可以自助拿走,群号是980758007。*
在这里插入图片描述

收起阅读 »

Tomcat源码学习第4篇-Servlet请求分析

前段时间家里有事忙,停更了好长一段时间,这里跟等待更新的小伙伴们说一声抱歉,没能提前说明一下,让小伙伴们等了这么久,真的不好意思! 前面说完了Tomcat的初始化和启动步骤,那么接下来就要进入重头戏了!在本篇文章中,我会跟前面一样,通过图文的方式来带着小...
继续阅读 »



前段时间家里有事忙,停更了好长一段时间,这里跟等待更新的小伙伴们说一声抱歉,没能提前说明一下,让小伙伴们等了这么久,真的不好意思!



前面说完了Tomcat的初始化和启动步骤,那么接下来就要进入重头戏了!在本篇文章中,我会跟前面一样,通过图文的方式来带着小伙伴们了解一个 Servlet是如何被tomcat处理的,具体的处理链路都有哪些。


一、请求分析


在《Tomcat源码学习第2篇》中备注了各个组件的说明。


当一个servlet请求到来的时候,首先经过的是connector组件,它是用来接收请求的。


该组件接收到请求之后,会把相关请求进行封装,然后传递到engine组件中。


紧跟着,engine组件会锁定对应的hostcontext以及wrapper,一层层的传递下去,找到最终处理请求的servlet实例。


请求链路


二、深入探索


不知道大家还有没有印象,在前面的文章中,我们在NioEndpoint类中,启动Accepter线程的入口处上方还有着一个线程组在启动运行,然而却没有讲解该线程是用来干嘛的~


NioEndpoint.startInternal()


NioEndpoint.startInternal()


点击跳转到该类过来,我们可以看到他实现了Runnable接口,那么我们直接查看他的run()方法,看看它的运行逻辑。


Poller.run()


Poller


通过注释我们可以知道,该线程主要用于轮询已连接的套接字,检查是否触发了事件,并在事件发生时将关联的套接字移交给对应的处理器。在源码中我们可以看到keyCount变量记录着待处理请求数,提供给后面做相应判断。


Poller.run()


继续往下走,通过keyCount判断是否有请求需要进行处理,需要的话则通过selector.selectedKeys()拿到需要被处理的channel集合,进行循环处理。在while循环中我们看到,所有就绪的通道都调用的是processKey(sk, socketWrapper)方法进行处理。


image-20210503200131828


点击跳转过来该方法,在这里可以看到他对该sk做了读写判断,既然是请求进来,那肯定是做读操作,我们先进读相关的方法看一下。


NioEndpoint.processKey()


NioEndpoint.processKey()


进来之后我们可以看到它首先在缓存池中尝试去获取一个处理线程,当缓存池中没有线程时,就创建一个新的线程,如果有的话就直接使用。


AbstractEndpoint.processSocket()


AbstractEndpoint.processSocket()


既然是线程了,那么我们就关心线程的核心方法即可。点击SocketProcessorBase跳转查看run()方法。


SocketProcessorBase.run()


SocketProcessorBase.run()


doRun()处打上断点,单击下一步,跳转到NioEndpoint.doRun()方法中。Poller线程移交到这边的线程进行处理,在该线程中需要得到当前的socket,做进一步的处理。


NioEndpoint.doRun()


image-20210503212817648


进入该方法之后,我们可以看到它首先对wrapper进行判断,不为空再取出socket,然后尝试着在connections中去获取对应的processor,如果获取不到,再尝试获取已经处理过连接,但是尚未销毁的processor中去获取,还获取不到才进行创建。这样可以避免频繁的创建和销毁对象。


AbstractProtocol.process()


AbstractProtocol.process()


AbstractProtocol.process()


得到processor之后,调用process方法对报文进行解析。


AbstractProtocol.process()


进入该方法之后,我们可以看到这里面是对socketEvent的状态进行判断,我们当前请求主要是读状态,在此处打上断点,跳到该方法进来看一下。


AbstractProcessorLight.process()


AbstractProcessorLight.process()


这里我们可以看到是进入到了 http11类中,在该类里面对报文进行解析,封装原生的requestresponse对象。这里的response因为我们还没有到返回的步骤,所以只是做个初步的参数设置。后续要传入Adapter进行下一步操作。


Http11Processor.service()


Http11Processor.service()


Http11Processor.service()


Http11Processor.service()


在这里对原生的requestresponse进行转换,得到HttpServletRequestHttpServletResponse。然后根据请求信息找到能够处理当前请求的hostcontextwrapper


CoyoteAdapter.service()


CoyoteAdapter.service()


在这方法可以看到它会通过getMapper()方法去匹配能够处理当前请求的 host,context,wrapper。到这里可能有的小伙伴会奇怪,为什么是从mapper中去匹配呢?这个问题留给你们去探索一下,等下篇再给你们解答。


CoyoteAdapter.postParseRequest()


CoyoteAdapter.postParseRequest()


上一方法中,通过connector获取service之后再取得对应的mapper,可是进来之后却没有看到对该mapper对象的构建,那该对象是哪里来的呢?


Mapper.map()


Mapper.map()


不知道大家还记不记得在第二篇中,在StandardService类中initInternal()startInternal()方法中有mapperListener方法的初始化和启动。


StandardService.initInternal()


StandardService.startInternal()


在该方法中查找到对应的host, context, wrapper


Mapper.internalMap()


Mapper.internalMap()


Mapper.internalMap()


回到CoyoteAdapter.postParseRequest(),通过Evaluste我们可以看到当前请求对应的host, context, wrapper以及实例的映射均已找到。


CoyoteAdapter.postParseRequest()


接下来要做的就是根据链路组件进行一级级的调用,直至最后取出servlet执行。


CoyoteAdapter.service()


CoyoteAdapter.service()


先得到host,在通过host继续调用下一级组件


StandardEngineValve.invoke()


StandardEngineValve.invoke()


AbstractAccessLogValve.invoke()


AbstractAccessLogValve.invoke()


ErrorReportValve.invoke()


ErrorReportValve.invoke()


这里拿到context,继续invoke()


StandardHostValve.invoke()


StandardHostValve.invoke()


AuthenticatorBase.invoke()


AuthenticatorBase.invoke()


StandardContextValve.invoke()


StandardContextValve.invoke()


拿到wrapper之后,继续向下执行,从wrapper容器中得到servlet对象。


StandardWrapperValve.invoke()


StandardWrapperValve.invoke()


紧接着,把得到的servlet加入过滤器链中(可能有其它的处理,这里不直接进行处理),留待下面调用过滤器链再统一进行处理。


StandardWrapperValve.invoke()


StandardWrapperValve.invoke()


ApplicationFilterChain.doFilter()


ApplicationFilterChain.doFilter()


终于找到具体的实例了,太不容易了!!!


ApplicationFilterChain.internalDoFilter()


ApplicationFilterChain.internalDoFilter()


三、总结


Servlet请求链路



我收集有众多的 计算机电子书籍,有需要的小伙伴自提哦~


收起阅读 »

iOS 类簇(class clusters)

类簇(class clusters)类簇是Foundation framework框架下广泛使用的一种设计模式。它管理了一组隐藏在公共抽象父类下的具体私有子类。没有使用类簇(Simple Concept but Complex Interface)为了说明类簇...
继续阅读 »

类簇(class clusters)

类簇是Foundation framework框架下广泛使用的一种设计模式。它管理了一组隐藏在公共抽象父类下的具体私有子类。

没有使用类簇(Simple Concept but Complex Interface)

为了说明类簇的结构体系和好处,我们先思考一个问题:如何构建一个类的结构体系用它来定义一个对象存储不同数据类型的数字(char,intfloatdouble)。因为不同数据类型的数字有很多共同点(例如:它们都能从一种类型转换成另一种类型,都能用字符串表示),所以可以用一个类来表示它们。然而,不同的数据类型的数字的存储空间是不同的,所以用一个类来表示它们是很低效的。考虑到这个问题,我们设计了如下图1-1的结构解决这个问题。


Number是一个抽象父类,在其方法声明中声明了子类的共有操作。但是,Number不会声明一个实例变量存储不同类型的数据,而是由其子类创建对应类型的实例变量并将调用接口共享给抽象父类Number到目前为止,这个类结构的设计十分简单。然而,如果C语言的基本数据类型被修改了(例如:加入了些新的数据类型),那么我们Number类结构如下图1-2所示:


这种创建一个类保存一种类型数据的概念很容易扩展成十几个类。类簇的体系结构展示了一种概念简洁性的设计。

使用类簇(Simple Concept and Simple Interface)

使用类簇的设计模式来解决这个问题,类结构设计如图1-3所示:


使用类簇我们只能看到一个公共父类Number,它是如何创建正确子类的实例的呢?解决方式是利用抽象父类来处理实例化。

创建实例(Creating Instances)

在类簇中的抽象父类必须声明创建私有子类变量的方法。抽象父类的主要职责是当调用创建实例对象的方法时,根据调用的方法去分配合适的子类对象(不能选择创建实例对象的类)。

在Foundation framework中,你可能调用类方法或者allocinit创建对象。以Foundation framework的NSNumber创建数字对象为例:

NSNumber *aChar = [NSNumber numberWithChar:’a’];
NSNumber *anInt = [NSNumber numberWithInt:1];
NSNumber *aFloat = [NSNumber numberWithFloat:1.0];
NSNumber *aDouble = [NSNumber numberWithDouble:1.0];

使用上面方法返回的对象aCharanIntaFloat,  aDouble是由不同的私有字类创建的。尽管每个对象的从属关系(class membership)被隐藏了,但是它的接口是公开的,能够通过抽象父类NSNumber声明的接口来访问。当然这种做法是及其不严谨的,某种意义上是不正确的,因为用NSNumber方法创建的对象并不是一个NSNumber的对象,而是返回了一个被隐藏了的私有子类的对象。但是我们可以很方便的使用抽象类NSNumber接口中声明的方法来实例化对象和操作它们。

拥有多个公共抽象父类的类簇(Class Clusters with Multiple Public Superclasses)

在上面的例子中,使用一个公共抽象父类声明多个私有子类的接口。但是在Foundation framework框架中也有很多使用两个或两个以上的公共抽象父类声明私有子类接口的例子,如表1-1所示:


还存在这种类型的类簇,但这些清楚说明了两个公共抽象父类是如何协同工作来声明类簇的编程接口的。一个公共抽象父类声明了所有类簇对象都能相应的方法,而另一个公共抽象父类声明的方法只适合允许修改内容的类簇对象。

创建子类(Creating Subclasses Within a Class Cluster)

类蔟的体系结构是在易用性和可扩展性之间均衡的结果:类簇的应用使得学习和使用框架中的类十分简单,但是在类簇中创建子类是困难的。但是很少情况下需要在类簇中创建子类,因为类簇的好处是显而易见的。

如果你发现类簇提供的功能不能满足你的变成需要,那么在类簇创建子类是一种选择。例如:假如你想在NSArray的类簇中创建一个基于文件存储而不是基于内存存储的数组。因为改变了类的底层存储机制,就不得不在类簇中创建子类。

另一方面,在某些情况下我们创建一个类内嵌类簇对象就足够了。例如:如果你的程序需要被提醒,当某些数据没被修改的时候。在这种情况下,创建一个包装Foundation framework框架定义的数据对象的类可能是最好的方法。这个类的对象能干预修改数据的消息,拦截这个消息,对这个消息采取相应的行动,然后重定向给内嵌的数据对象。

综上所述,如果你想管理对象的存储空间,就在类簇中创建子类。否则,创建一个复合对象(composite object),复合对象:你自己设计的类对象内嵌一个标准Foundation framework框架的对象。

真正子类(A True Subclass)

在类簇中创建一个子类,你必须:

  • 创建类簇中抽象超类的子类
  • 声明自己的存储空间
  • 重写父类的所有初始化方法
  • 重写父类的所有原始方法(primitive methods)

第一点:因为在类簇的体系结构中只有类簇中的抽象父类是公开可见的节点。第二点:子类会继承类簇的接口但没有实例变量,因为抽象父类没有声明,所以子类必须声明它所需要的任意实例变量。最后:子类必须重写继承的所有方法。

一个类的原始方法(primitive methods)是构成其接口的基础。NSArray为例,它声明类管理数组对象的接口。在概念上,一个数据保存了很多数据项,它们都能通过下标(index)访问。NSArray通过这两个原始方法表达了这一抽象概念,countobjectAtIndex:,以这些方法为基础可以实现其它派生方法。


原始方法(primitive methods)和派生方法(derived methods)的接口区分使创建子类更简单。子类必须重写所有继承的原始方法(primitive methods),这样做可以确保所有继承的派生方法(derived methods)都能正常运行。

原始和派生的区别同样适用于完全初始化对象接口。子类中需要解决如何处理init…方法的问题。

通常,一个类簇的抽象父类方法声明了一系列init…方法和+ className类方法。基于你选择的init…方法或+ className类方法,抽象类决定用哪个具体的子类来实例化。你可以认为抽象类是为子类服务的,因为抽象类没有实例变量,它也不需要初始化方法。

自定义的子类应该声明自己的init…+ className方法,不应该依赖继承的方法。为了保持初始化链,它应该在自己的指定初始化函数里调用父类的指定初始化函数(designated initializers)。在类簇中它也应该以合理方式重写继承的所有初始化方法,抽象父类的指定初始化函数总是init

复合对象(A Composite Object)

在你自定义的对象中内嵌一个私有的类簇对象称为复合对象。复合对象可以利用类簇对象来实现基本的功能,只拦截复合对象想要特殊处理的消息。这种结构减少了代码量,利用了Foundation framework的测试代码。如图1-4所示:


复合对象必须声明它自己是类簇抽象父类的子类,必须重写父类的所有原始方法,也可以重写派生方法但不是必须的。

总结

在Cocoa中,实际上许多类都是以类簇的方式实现的,即它们是一群隐藏在通用接口之下与实现相关的类。例如创建数组时可能是__NSArray0,__NSSingleObjectArray__NSArrayI,所以请不要轻易尝试创建NSStringNSArray,NSDictionary的子类。对类簇使用isKindOfClassisMemberOfClass的结果可能是不正确的。因为类簇是由公共抽象类管理的一组私有类,公共抽象类并不是实例对应的真正的类,类簇中真正的类的从属关系被隐藏了。


转自链接:https://www.jianshu.com/p/86ef3ca9810d

收起阅读 »

iOS -开发SDK的技巧

本文目标:掌握封装及开发SDK的全部技巧内容提要:不同场景下如何封装及开发SDK.a静态库创建直接创建Framework库在已有工程中创建创建Framework工程进行封装创建Bundle资源库文件含界面SDK如何进行依赖开发使用脚本创建Framework库,...
继续阅读 »

本文目标:掌握封装及开发SDK的全部技巧

内容提要:不同场景下如何封装及开发SDK

  • .a静态库创建
  • 直接创建Framework库
  • 在已有工程中创建
  • 创建Framework工程进行封装
  • 创建Bundle资源库文件
  • 含界面SDK如何进行依赖开发
  • 使用脚本创建Framework库,解决合并的烦恼
  • Swift 如何创建Framework库

知识准备

  • 终端命令
真机和模拟器上的库文件合并 
Framework库合并的是Framework内包含的二进制文件,合并后替换库中的文件,没有.a后缀
lipo -create xxx.a(真机) xxx.a(模拟器) -output 新名字.a
查看SDK支持的架构
lipo -info XXX.a 输出: i386 armv7 x86_64 arm64
arm7: 在最老的支持iOS7的设备上使用
arm7s: 在iPhone5和5C上使用
arm64: 运行于iPhone5S的64位 ARM 处理器 上
i386: 32位模拟器上使用
x86_64: 64为模拟器上使用
注意: 高位兼容地位(32位兼容16位),arm7版本可以在arm7s上运行
需要在对应架构设备上运行,才能生成对应架构的包
  • category的处理
category是项目开发中经常用到的,把category打包成静态库是没有问题的,但是在使用这个静态库时,
调用category中的方法时会发生找不到该方法的运行时错误(selector not recognized),
解决的办法是在使用静态库的工程中配置other linker flags的值为 -ObjC -all_load
  • 对图片资源和UI界面xib或nib文件的处理
.a和.framework两种静态库,通常都是把需要用的到图片或者xib文件存放在一个bundle文件中,而该bundle文件的名字和.a或.framework的名字相同。
.a文件中无法存放图片或xib文件,很容易理解,但是.framework从本质上说也是一个bundle文件,为什么不把图片或者xib文件直接放在.framework中而单独再创建个bundle文件呢?
那是因为iOS系统不会去扫描.framework下的图片等资源文件,也不会在项目中显示,也就是说即使放在 .framework目录下,系统根本就不会去扫描,因此也无法发现使用
  • Debug和Release
Debug和Release,在我看来主要是针对其面向的目标不同的而进行区分的。
Debug通常称为调试版本,通过一系列编译选项的配合,编译的结果通常包含调试信息,而且不做任何优化,以为开发人员提供强大的应用程序调试能力。
Release通常称为发布版本,是为用户使用的,一般客户不允许在发布版本上进行调试。所以不保存调试信息,同时,它往往进行了各种优化,以期达到代码最小和速度最优。为用户的使用提供便利

开发指南

网上找了一个动画工程,作为我们开刀的对象
下载原始工程
如果你有耐性,可以和我一起走完整个流程。当然每个模块都是独立的,你可以进行针对性的阅读

一、.a静态库创建

  • 创建静态库工程 >> 删除自动创建的.m文件 >> 清空头文件里的信息 >> 导入你要封装的系统库文件




  • 点击目标工程 >> Build Phases >> Editor >> add build Phases(是否公开头文件选项) >> 设置公开访问的头文件(或在Target Membership中直接设置)

    目标工程 > Build Phases > 点击左侧加号 > add build Phases(是否公开头文件选项) > 设置公开访问的头文件(或在Target Membership中直接设置)







  • 在设备 和 模拟器 下分别按下command + B进行编译 >> 查看Product目录 >> Show in Finder 查看编译成功的静态库
  • debug模式下运行生成 Debug-iphoneos 和 Debug-iphonesimulator两个文件夹
  • release模式下运行生成 Release-iphoneos 和 Release-iphonesimulator文件夹


    • 合并Debug模式下的真机和模拟器下的静态库文件
    使用终端进行合并
    cd 文件保存目录
    lipo -create 模拟器.a(路径) 真机.a(路径) -output 重命名.a
    查看架构模式
    lipo -info XXX.a 查看是否满足运行要求
    • 使用.a库文件
      创建文件夹libAdvanced用于保存静态库信息 >> 替换刚刚合并的.a文件 >> 添加用到的图片等资源文件 >> 导入工程验证

    • 创建新工程验证
    • 如果架构报错 Build Settings >> BuildActiveArchitecture Only Debug改为NO

    二、直接创建Framework库

    Framework是资源的集合,将静态库和其头文件包含到一个结构中,让Xcode可以方便地把它纳入到你的项目中。本质也是一个bundle文件
    在已有工程中创建
    • 创建Framework
      点击目标工程 >> 点击下面左下角加号 >> 创建


    • 参数配置
    点击目标工程 >> 选择你创建的Framework >> 点击工程设置 >> 做出如下修改

    Build Settings >> Dead Code Stripping >> 设置为NO

    Build Settings >> Strip Debug Symbol During Copy >> 全部设置为NO

    Build Settings >> Strip Style >> 设置为Non-Global Symbols

    Build Settings >> Base SDK >> Latest iOS(iOS 选择最新的)

    Build Settings >> Link With Standard Libraries >> 设置为 NO

    Build Settings >> Mach-O Type >> Static Library

    对于Mach-O Type有两种情况:(1)选择 Static Library 打出来的是静态库;(2)选择 Relocatable Object File 打出来是动态库。


    • 选择framework支持的系统版本

    • 将需要打包的文件拖入到Framework中


    设置需要公开的文件

    • 选择运行模式(debug 或 Release)分别在真机和模拟器下common + B 编译生成对应的Framework库

    • 合并二进制文件并替换

    cd 到保存文件目录下
    lipo -create xxx/Debug-iphoneos/LibLoaderFramework.framework/LibLoaderFramework xxx/Debug-iphonesimulator/LibLoaderFramework.framework/LibLoaderFramework -output LibLoaderFramework
    lipo -info LibLoaderFramework
    输出
    Architectures in the fat file: LibLoaderFramework are: i386 arm64
    然后替换二进制文件


    删除Framework


    • 如果没有用到info.plist文件可以删除,避免在工程中发生冲突
    • 验证
      导入完整Framework到工程,移除打包前的代码,对库进行验证
    • 建立Framework工程进行创建
      • 使用xcode直接创建Framework工程

      • 把需要编译的文件导入到工程中

      • 设置需要公开的头文件

    • 选择Framework支持的系统版本
    • 配置参数

    参数配置基本都一样
    点击目标工程 >> 选择你创建的Framework >> 点击工程设置 >> 做出如下修改
    Build Settings >> Dead Code Stripping >> 设置为NO
    Build Settings >> Strip Debug Symbol During Copy >> 全部设置为NO
    Build Settings >> Strip Style >> 设置为Non-Global Symbols
    Build Settings >> Base SDK >> Latest iOS(iOS 选择最新的)
    Build Settings >> Link With Standard Libraries >> 设置为 NO
    Build Settings >> Mach-O Type >> Static Library
    对于Mach-O Type有两种情况:(1)选择 Static Library 打出来的是静态库;(2)选择 Relocatable Object File 打出来是动态库。
    -  选择Debug(或Release)模式分别在模拟器和真机上 command + B 编译

    - 合并真机和模拟器下编译的二进制文件
    cd到你想要保存合并后文件的目录下
    lipo -create xxx.framework/xxx ooo.framework/ooo -output ooo。
    查看文件支持的架构
    lipo -info LibLoaderFramework
    将合并成功的二进制文件替换为framework中的二进制文件,如果没有用到info.plist文件,可以删除,避免在工程中发生冲突
    • 创建新的工程,导入Framework进行验证
    如果工程无法联想出Framework头文件,导入路径形式如下:
    #import <LibLoaderFramework/PublicHeader.h>

    三、创建Bundle资源库文件

    创建Bundle工程



    • 配置几个编译设置

    因为你正在创建一个在iOS上使用的bundle,这与默认的OS X不同。
    Build Settings >> Base SDK >> Latest iOS (iOS 10.2选择最新)
    Build Settings >> Product Name >> ${TARGET_NAME}替换为你的工程名XXXX(直接写工程名就好)
    ################注意事项######################
    默认情况下,有两种resolutions(分辨率)的图片可以产生一些有趣的现象。
    例如,当你导入一个retina@2x版本的图片时,普通版的和Retina版的将会合并成一个多resolution的TIFF(标签图像文件格式,Tagged Image File Format)。
    Build Settings > COMBINE_HIDPI_IMAGES设置为NO
    • 如何添加资源文件

      • 直接拖入
      • 选择图片或其他资源文件 > Target Membership > 选择bundle目标
    • 编译工程并查看


    • Bundle文件使用时需要真实路径

    NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:@"LoaderBundle" withExtension:@"bundle"]];
    NSString *resourceStr = [bundle pathForResource:@"IMG_0017" ofType:@"JPG"];
    我们可以创建NSBundle分类避免重复书写
    • 创建工程验证

    四、含界面SDK如何进行依赖开发

    在无法看到真实效果的情况下为iOS开发一个UI控件库是极其困难的,所以我们需要掌握依赖开发的知识

  • 创建Framework工程
  • 参数设置:参考如上第二章节
  • 创建验证工程
  • 关闭Framework工程
  • 添加Framework工程的xxxx.xcodeproj到验证工程并连接到静态库如图操作:若未找到库,对库进行编译


    • 导入库的公开头文件,对验证工程进行编译
    如果工程无法联想出Framework头文件,导入路径形式如下:
    #import <LibLoaderFramework/PublicHeader.h>
    像这样使用嵌套工程的好处是你可以对库本身做出修改,而不用离开示例工程,即使你同时改变两个地方的代码也一样。每次你编译工程,你都要检查是否将头文件的public/project关系设置正确。如果实例工程中缺失了任何需要的头文件,它都不能被编译。

    五、使用脚本创建Framework库

  • 创建.a的静态库工程
    (创建方式与参数配置参照第一节不再赘述)
  • 使用脚本创建Framework目录结构,此时不包含二进制文件
  • 添加 New Run Script Phases
  • 双击面板标题栏Run Script,重命名为Build Framework。
    • 这个面板允许你在构建时运行一个Bash脚本
    • 你希望让脚本在build的过程中何时执行,就把这个面板拖动到列表中相对应的那一位置。
    • 对于该framework工程来说,脚本最后执行,因此你可以让它保留在默认的位置即可。


  • #set –e确保脚本的任何地方执行失败,则整个脚本都执行失败。
    set -e

    #导出framework路径
    export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"

    # 创建当前版本真实头文件夹
    mkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers"

    # 创建引用路径
    /bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current"

    /bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers"

    /bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \
    "${FRAMEWORK_LOCN}/${PRODUCT_NAME}"

    # 拷贝公共头文件到framework中
    /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
    "${FRAMEWORK_LOCN}/Versions/A/Headers"

    #######################简化目录也可用使用如下脚本#######################

    #set –e确保脚本的任何地方执行失败,则整个脚本都执行失败。
    set -e

    #导出的文件路径
    export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"

    # 创建真是文件路径
    mkdir -p "${FRAMEWORK_LOCN}/Headers"

    # 拷贝公共头文件到framework中
    /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
    "${FRAMEWORK_LOCN}/Headers"
    这个脚本做了如下三个操作:
    1.创建了libLoader.framework/Versions/A/Headers目录
    2.创建了一个framework所需要的三个连接符号([symbolic links](http://en.wikipedia.org/wiki/Symbolic_link))

    当前版本文件夹
    Versions/Current => A
    头文件夹
    Headers => Versions/Current/Headers
    二进制文件
    libLoader => Versions/Current/libLoader

    3.将公共头文件从你之前定义的公共头文件路径拷贝到Versions/A/Headers目录下,-a参数确保修饰次数作为拷贝的一部分不会改变,防止不必要的重新编译。


    • 多架构(Multi-Architecture)编译
      解决Framework或.a库合并的烦恼,此处用的是.a工程,使用Framework工程类似
    • iOS app需要在许多不同的CPU架构下运行:
    arm7: 在最老的支持iOS7的设备上使用
    arm7s: 在iPhone5和5C上使用
    arm64: 运行于iPhone5S的64位 ARM 处理器 上
    i386: 32位模拟器上使用
    x86_64: 64为模拟器上使用


    每个CPU架构都需要不同的二进制数据,当你编译一个应用时,无论你目前正在使用那种架构,Xcode都会正确地依照对应的架构编译。例如,如果你想跑在虚拟机上,Xcode只会编译i386版本(或者是64位机的x86_64版本)。
    这意味着编译会尽可能快地进行,当你归档一款app或者构建app的发布版本(release mode)时,Xcode会构建上述三个用于真机的ARM架构。因此这样app就可以跑在所有设备上了。不过,其他的编译架构又如何呢?让我们一起往下走
    创建 Aggregate 集合
    点击目标工程 > 添加新目标 > Cross-Platform > Aggregate > next > 命名为Framework'

    为什么使用集合(Aggregate)目标来创建一个framework呢?为什么这么不直接?因为OS X对库的支持更好一些,事实上,Xcode直接为每一个OS X工程提供一个Cocoa Framework编译目标。基于此,你将使用集合编译目标,作为Bash脚本的连接串来创建神奇的framework目录结构。
    你是不是开始觉得这个方法有些懵逼了?

    添加依赖库
    为了确保每当这个新的framework目标被创建时,静态链接库都会被编译,你需要往静态库目标中添加依赖(Dependency)。在库工程中选择Framework目标,在Build Phases中添加一个依赖。展开Target Dependencies面板,添加依赖库

    添加多平台编译脚本
    这个目标的主要编译部分是多平台编译,你将使用一个脚本来做到这一点。和你之前做的一样
    选择Framework目标 >> Build Phases >> 左侧 “+” 按钮 >> New Run Script Phases >> 命名MultiPlatform Build
    ![MultiPlatform Build

    • 写入编译framewo编译脚本

    #set –e确保脚本的任何地方执行失败,则整个脚本都执行失败。
    set -e

    #标示 如果已经插入脚本 退出
    if [ -n "$IYQ_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then
    exit 0
    fi
    export IYQ_MULTIPLATFORM_BUILD_IN_PROGRESS=1

    # 自定义变量
    IYQ_FRAMEWORK_NAME=${PROJECT_NAME}
    IYQ_INPUT_STATIC_LIB="lib${PROJECT_NAME}.a"
    IYQ_FRAMEWORK_LOCATION="${BUILT_PRODUCTS_DIR}/${IYQ_FRAMEWORK_NAME}.framework"

    #构建静态库 传参 "${1}"
    function build_static_library {
    # 重新构建库
    xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \
    -target "${TARGET_NAME}" \
    -configuration "${CONFIGURATION}" \
    -sdk "${1}" \
    ONLY_ACTIVE_ARCH=NO \
    BUILD_DIR="${BUILD_DIR}" \
    OBJROOT="${OBJROOT}" \
    BUILD_ROOT="${BUILD_ROOT}" \
    SYMROOT="${SYMROOT}" $ACTION
    }

    #合并
    function make_fat_library {

    xcrun lipo -create "${1}" "${2}" -output "${3}"
    }

    # 1 正则判断 真机还是模拟器 (iphoneos/iphonesimulator)
    if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then
    IYQ_SDK_PLATFORM=${BASH_REMATCH[1]}
    else
    echo "Could not find platform name from SDK_NAME: $SDK_NAME"
    exit 1
    fi

    # 2 SDK版本
    if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then
    IYQ_SDK_VERSION=${BASH_REMATCH[1]}
    else
    echo "Could not find sdk version from SDK_NAME: $SDK_NAME"
    exit 1
    fi

    # 3 其他平台判断 如果 则 否则
    if [ "$IYQ_SDK_PLATFORM" == "iphoneos" ]; then
    IYQ_OTHER_PLATFORM=iphonesimulator
    else
    IYQ_OTHER_PLATFORM=iphoneos
    fi

    # 4 其他平台路径
    if [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$IYQ_SDK_PLATFORM$ ]]; then
    IYQ_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${IYQ_OTHER_PLATFORM}"
    else
    echo "Could not find other platform build directory."
    exit 1
    fi

    #调用上面构建函数 如果当前运行的是真机则构建模拟器
    build_static_library "${IYQ_OTHER_PLATFORM}${IYQ_SDK_VERSION}"

    # 如果你现在正在为模拟器编译,那么Xcode会默认只在该系统对应的结构下编译,例如i386 或 x86_64。为了在这两个结构下都进行编译,这里调用了build_static_library,基于iphonesimulator SDK重新编译,确保这两个结构都进行了编译。
    if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then
    build_static_library "${SDK_NAME}"
    fi

    # 合并库
    make_fat_library "${BUILT_PRODUCTS_DIR}/${IYQ_INPUT_STATIC_LIB}" \
    "${IYQ_OTHER_BUILT_PRODUCTS_DIR}/${IYQ_INPUT_STATIC_LIB}" \
    "${IYQ_FRAMEWORK_LOCATION}/${IYQ_FRAMEWORK_NAME}"

    # 确保文件存在 相当于-dpR,保持文件的连接(d),保持原文件的属性(p)并作递归处理(R)
    cp -a "${IYQ_FRAMEWORK_LOCATION}/${IYQ_FRAMEWORK_NAME}" \
    "${IYQ_OTHER_BUILT_PRODUCTS_DIR}/${IYQ_FRAMEWORK_NAME}.framework/${IYQ_FRAMEWORK_NAME}"

    # 拷贝到指定目录下
    ditto "${IYQ_FRAMEWORK_LOCATION}" "${SRCROOT}/BuildFramework/${IYQ_FRAMEWORK_NAME}.framework"

    在工程目录下的BuildFramework文件下查看,并导入工程验证
    #import <libLoader/LoaderProgressView.h>


    • SDK存在图片,xib等资源文件的情况

    • 添加bundle目标工程

    • bundle创建详细操作参考第三节内容

    • bundle目标工程进行编译

    • 添加bundle资源库依赖


    • 如果想把你的编译包copy到指定位置,在脚本后面加入如下代码

    # 拷贝bundle到指定目录下
    ditto "${BUILT_PRODUCTS_DIR}/${IYQ_FRAMEWORK_NAME}.bundle" \
    "${SRCROOT}/BuildFramework/${IYQ_FRAMEWORK_NAME}.bundle"



    原贴链接:https://www.jianshu.com/p/cbb1f54b89d2
    收起阅读 »

    7大程序设计原则

    编程的工作既是技术活,也是体力活,而编写优秀的软件,更是一件比较难的事情。 初级程序员只希望代码不出错,顶级程序员却把写代码当成艺术,当年雷军以过人的能力成为求伯君的左膀右臂,其早年的代码被说成“像诗一样优美”。 很多大牛,在着手写代码时并不是直接上手...
    继续阅读 »

    编程的工作既是技术活,也是体力活,而编写优秀的软件,更是一件比较难的事情。


    初级程序员只希望代码不出错,顶级程序员却把写代码当成艺术,当年雷军以过人的能力成为求伯君的左膀右臂,其早年的代码被说成“像诗一样优美”。


    很多大牛,在着手写代码时并不是直接上手编写,而是根据需求进行设计,不但将代码中 Bug 出现的机率降到最低,还让代码具有高可读性,高安全性等等。


    那大牛们都遵循怎样的原则呢,我们能不能学习一下?


    将大牛们的经验总结到一起,可以得到以下「7 大程序设计原则」 。这些设计原理源于对实际软件开发现场的分析,是提高代码质量的经验结晶。


    让我们一起一探究竟吧!


    01 简单性原则


    Simplicity Principle


    What:追求简单
    简单性原则就是追求简单。


    说得极端一点,就是自始至终都以最简单的逻辑编写代码,让编程初学者一眼就能看懂。


    因此,在编程时我们要重视的是局部的完整性,而不是复杂的整体关联性。


    Why:Bug 喜欢出现在复杂的地方
    软件故障常集中在某一个区域,而这些区域都有一个共同的特点,那就是复杂。编写代码时如果追求简单易懂,代码就很难出现问题。


    不过,简单易懂的代码往往给人一种不够专业的感觉。这也是经验老到的程序员喜欢写老练高深的代码的原因。所以我们要有足够的定力来抵挡这种诱惑。


    Do:编写自然的代码
    努力写出自然的代码。放下高超的技巧,坚持用简单的逻辑编写代码。


    既然故障集中在代码复杂的区域,那我们只要让代码简单到让故障无处可藏即可。不要盲目地让代码复杂化、臃肿化,要保证代码简洁。


    02 同构原则


    Isomorphism Principle


    What:力求规范
    同构原则就是力求规范。


    同等对待相同的东西,坚持不搞特殊。同等对待,举例来说就 是同一个模块管理的数值全部采用同一单位、公有函数的参数个数统一等。


    Why:不同的东西会更显眼
    相同的东西用相同的形式表现能够使不同的东西更加突出。不同的 东西往往容易产生 bug。遵循同构原则能让我们更容易嗅出代码的异样, 从而找出问题所在。


    图表和工业制品在设计上追求平衡之美,在这一点上,同构原则也 有着相似之处。统一的代码颇具美感,而美的东西一般更容易让人接 受,因此统一的代码有较高的可读性。


    Do:编写符合规范的代码
    我们要让代码符合一定的规范。不过,这会与程序员的自我表现欲相冲突。


    为了展现自己的实力,有些程序员会无视编程规范,编写独特的代码。可靠与简单是代码不可或缺的性质,但这些程序员常常在无意间让代码变得复杂。


    这就把智慧与个性用错了地方。小小的自我满足远不及代码质量重要。所以在编写代码时,务必克制住自己的表现欲,以规范为先。


    03 对称原则


    Symmetry Principle


    What:讲究形式上的对称
    讲究形式上的对称。


    对称原则就是讲究形式上的对称,比如有上就有下,有左就有右, 有主动就有被动。


    也就是说,我们在思考一个处理时,也要想到与之成对的处理。比 如有给标志位置 1 的处理,就要有给标志位置 0 的处理。


    Why:帮助读代码的人推测后面的代码
    具有对称性的代码能够帮助读代码的人推测后面的代码,提高其理解代码的速度。同时,对称性会给代码带来美感,这同样有助于他人理解代码。


    此外,设计代码时将对称性纳入考虑的范围能防止我们在思考问题时出现遗漏。如果说代码的条件分支是故障的温床,那么对称性就是思考的框架,能有效阻止条件遗漏。


    Do:编写有对称性的代码
    在出现“条件”的时候,我们要注意它的“反条件”。每个控制条件都存在与之成对的反条件(与指示条件相反的条件)。要注意条件与反条件的统一,保证控制条件具有统一性。


    我们还要考虑到例外情况并极力避免其发生。例外情况的特殊性会破坏对称性,成为故障的温床。特殊情况过多意味着需求没有得到整理。此时应重新审视需求,尽量从代码中剔除例外情况。


    命名也要讲究对称性。命名时建议使用 set/get、start/stop、begin/ end 和 push/pop 等成对的词语。


    04 层次原则


    Hierarchy Principle


    What:讲究层次
    注意事物的主从关系、前后关系和本末关系等层次关系,整理事物的关联性。


    不同层次各司其职,同种处理不跨越多个层次,这一点非常重要。比如执行了获取资源的处理,那么释放资源的处理就要在相同的层次进行。又比如互斥控制的标志位置 1 和置 0 的处理要在同一层次进行。


    Why:层次结构有助于提高代码的可读性
    有明确层次结构的代码能帮助读代码的人抽象理解代码的整体结构。读代码的人可以根据自身需要阅读下一层次的代码,掌握更加详细的信息。


    这样一来就可以提高代码的可读性,帮助程序员表达编码意图,降低 bug 发生的概率。


    Do:编写有抽象层次结构的代码
    在编写代码时设计各部分的抽象程度,构建层次结构。保证同一个层次中的所有代码抽象程度相同。另外,高层次的代码要通过外部视角描述低层次的代码。这样做能让调用低层次代码的高层次代码更加简单易懂。


    05 线性原则


    Linearity Principle


    What:处理流程尽量走直线
    线性原则就是让处理流程尽量走直线。


    一个功能如果可以通过多个功能的线性结合来实现,那它的结构就会非常简单。


    反过来,用条件分支控制代码、毫无章法地增加状态数等行为会让代码变得难以理解。我们要避免做出这些行为,提高代码的可读性。


    Why:直线处理可提高代码的可读性
    复杂的处理流程是故障的温床。


    故障多出现在复杂的条件语句和循环语句中。另外,goto 等让流程出现跳跃的语句也是故障的多发地。


    如果能让处理由高层次流向低层次,一气呵成,代码的可读性就会大幅提高。与此同时,可维护性也将提高,添加功能等改良工作将变得更加容易。


    一般来说,自上而下的处理流程简单明快,易于理解。我们应避开复杂反复的处理流程。


    Do:尽量不在代码中使用条件分支
    尽量减少条件分支的数量,编写能让代码阅读者线性地看完整个处理流程的代码。


    为此,我们需要把一些特殊的处理拿到主处理之外。保证处理的统一性,注意处理的流程。记得时不时俯瞰代码整体,检查代码是否存在过于复杂的部分。


    另外,对于经过长期维护而变得过于复杂的部分,我们可以考虑对其进行重构。明确且可靠的设计不仅对我们自身有益,还可以给负责维护的人带来方便。


    06 清晰原则


    Clarity Principle


    What:注意逻辑的清晰性
    清晰原则就是注意逻辑的清晰性。


    逻辑具有清晰性就代表逻辑能清楚证明自身的正确性。也就是说,我们编写的代码要让人一眼就能判断出没有问题。任何不明确的部分都 要附有说明。


    保证逻辑的清晰性要“不择手段”。在无法用代码证明逻辑正确性的情况下,我们也可以通过写注释、附文档或画图等方法来证明。不过,证明逻辑的正确性是一件麻烦的事,时间一长,人们就会懒得用辅助手段去证明,转而编写逻辑清晰的代码了。


    Why:消除不确定性
    代码免不了被人一遍又一遍地阅读,所以代码必须保持较高的可读性。编写代码时如果追求高可读性,我们就不会采用取巧的方式编写代码,编写出的代码会非常自然。


    采用取巧的方式编写的代码除了能让计算机运行以外没有任何意义。代码是给人看的,也是由人来修改的,所以我们必须以人为对象来编写代码。


    消除代码的不确定性是对自己的作品负责,这么做也可以为后续负责维护的人提供方便。


    Do:编写逻辑清晰的代码
    我们要编写逻辑清晰的代码。


    为此,我们应选用直观易懂的逻辑。会给读代码的人带来疑问的部分要么消除,要么加以注释。


    另外,我们应使用任何人都能立刻理解且不存在歧义的术语。要特别注意变量名等一定不能没有意义。


    07 安全原则


    Safty Principle


    What:注意安全性
    安全原则就是注意安全性,采用相对安全的方法来对具有不确定性的、模糊的部分进行设计和编程。


    说得具体一点,就是在编写代码时刻意将不可能的条件考虑进去。比如即便某个 i f 语句一定成立,我们也要考虑 else 语句的情况;即便某个 case 语句一定成立,我们也要考虑 default 语句的情况;即便某个变量不可能为空,我们也要检查该变量是否为 NULL。


    Why:防止故障发展成重大事故
    硬件提供的服务必须保证安全,软件也一样。


    硬件方面,比如取暖器,为防止倾倒起火,取暖器一般会配有倾倒自动断电装置。同样,设计软件时也需要考虑各种情况,保证软件在各种情况下都能安全地运行。这一做法在持续运营服务和防止数据损坏等方面有着积极的意义。


    Do:编写安全的代码
    选择相对安全的方法对具有不确定性的部分进行设计。列出所有可能的运行情况,确保软件在每种情况下都能安全运行。理解需求和功能,将各种情况正确分解到代码中,这样能有效提高软件安全运行的概率。


    为此,我们也要将不可能的条件视为考察对象,对其进行设计和编程。不过,为了统一标准,我们在编写代码前最好规定哪些条件需要写,哪些条件不需要写。


    摘自:《编程的原则:程序员改善代码质量的 101 个方法》
    作者:[日]上田勋


    收起阅读 »

    Android界面左右滑动切换

    Android 界面左右滑动切换 1.界面布局 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://...
    继续阅读 »


    Android 界面左右滑动切换


    1.界面布局


    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <androidx.viewpager.widget.ViewPager
    android:id="@+id/viewPager"
    android:layout_weight="13"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_below="@id/scrollbar">
    </androidx.viewpager.widget.ViewPager>

    <LinearLayout
    android:layout_weight="1"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:orientation="horizontal"
    android:background="#FFFFFF">
    <LinearLayout
    android:layout_weight="1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">
    <ImageView
    android:id="@+id/i1"
    android:layout_weight="1"
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    android:layout_gravity="center"
    android:src="@mipmap/photo2" />
    <TextView
    android:id="@+id/t1"
    android:layout_weight="1"
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    android:gravity="center"
    android:text="主页"
    android:textSize="20sp"
    android:textColor="#000" />
    </LinearLayout>
    <LinearLayout
    android:layout_weight="1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">
    <ImageView
    android:id="@+id/i2"
    android:layout_weight="1"
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    android:layout_gravity="center"
    android:src="@mipmap/photo3" />
    <TextView
    android:id="@+id/t2"
    android:layout_weight="1"
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    android:gravity="center"
    android:text="活动"
    android:textSize="20sp"/>
    </LinearLayout>

    </LinearLayout>
    <ImageView
    android:layout_width="match_parent"
    android:layout_height="10dp"
    android:id="@+id/scrollbar"
    android:scaleType="matrix"
    android:layout_marginTop="5dp"
    android:src="@mipmap/scrollbar"/>

    </LinearLayout>


    界面展示
    在这里插入图片描述
    2.功能实现
    绑定ID
    在这里插入图片描述
    在onCreate函数中初始化滑块位置


    		bmpW = BitmapFactory.decodeResource(getResources(), R.mipmap.scrollbar).getWidth();
    //为了获取屏幕宽度,新建一个DisplayMetrics对象
    DisplayMetrics displayMetrics = new DisplayMetrics();
    //将当前窗口的一些信息放在DisplayMetrics类中
    getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    //得到屏幕的宽度
    int screenW = displayMetrics.widthPixels;
    //计算出滚动条初始的偏移量
    offset = (screenW / 2 - bmpW) / 2;
    //计算出切换一个界面时,滚动条的位移量
    one = offset * 2 + bmpW;
    Matrix matrix = new Matrix();
    matrix.postTranslate(offset, 0);
    //将滚动条的初始位置设置成与左边界间隔一个offset
    scrollbar.setImageMatrix(matrix);

    在onCreate函数中,ViewPage添加两个Fragment界面


    		FragmentManager fragmentManager=getSupportFragmentManager();
    fragments=new ArrayList<Fragment>();
    fragments.add(new BlankFragment1());
    fragments.add(new BlankFragment2());

    定义MyPagerAdapter类


     public class MyPagerAdapter extends FragmentPagerAdapter
    {

    public MyPagerAdapter(@NonNull FragmentManager fm) {
    super(fm);
    }

    @NonNull
    @Override
    public Fragment getItem(int position) {
    return fragments.get(position);
    }

    @Override
    public int getCount() {
    return fragments.size();
    }
    }

    定义MyOnPageChangeListener类


    public class MyOnPageChangeListener implements ViewPager.OnPageChangeListener {

    @Override
    public void onPageSelected(int arg0) {
    Animation animation = null;
    switch (arg0) {
    case 0:
    /**
    * TranslateAnimation的四个属性分别为
    * float fromXDelta 动画开始的点离当前View X坐标上的差值
    * float toXDelta 动画结束的点离当前View X坐标上的差值
    * float fromYDelta 动画开始的点离当前View Y坐标上的差值
    * float toYDelta 动画开始的点离当前View Y坐标上的差值
    **/

    animation = new TranslateAnimation(one, 0, 0, 0);
    t1.setTextColor(Color.rgb(0,0,0));
    t2.setTextColor(Color.rgb(117,117,117));
    i1.setImageResource(R.mipmap.photo2);
    i2.setImageResource(R.mipmap.photo3);
    break;
    case 1:
    animation = new TranslateAnimation(offset, one, 0, 0);
    t1.setTextColor(Color.rgb(117,117,117));
    t2.setTextColor(Color.rgb(0,0,0));
    i1.setImageResource(R.mipmap.photo1);
    i2.setImageResource(R.mipmap.photo4);
    break;
    }
    //arg0为切换到的页的编码
    currIndex = arg0;
    // 将此属性设置为true可以使得图片停在动画结束时的位置
    animation.setFillAfter(true);
    //动画持续时间,单位为毫秒
    animation.setDuration(200);
    //滚动条开始动画
    scrollbar.startAnimation(animation);
    }

    @Override
    public void onPageScrolled(int arg0, float arg1, int arg2) {
    }

    @Override
    public void onPageScrollStateChanged(int arg0) {
    }
    }

    使用方法


    		MyPagerAdapter myPagerAdapter=new MyPagerAdapter(fragmentManager);
    viewPager.setAdapter(myPagerAdapter);
    viewPager.addOnPageChangeListener(new MyOnPageChangeListener());

    3.源代码


    点击下载


    4.有的软件开发不需要左右滑动屏幕切换界面,只需要点击按钮切换,这时候我们只需要定义一个类,禁止滑动即可。


    (1) 新建命名为CustomViewPager的类
    在这里插入图片描述


    package com.example.day_05;

    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;

    import androidx.viewpager.widget.ViewPager;

    /**
    * Created by Administrator on 2017/5/19.
    */


    public class CustomViewPager extends ViewPager {

    private boolean isCanScroll = true;

    public CustomViewPager(Context context) {
    super(context);
    }

    public CustomViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    /**
    * 设置其是否能滑动换页
    * @param isCanScroll false 不能换页, true 可以滑动换页
    */

    public void setScanScroll(boolean isCanScroll) {
    this.isCanScroll = isCanScroll;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    return isCanScroll && super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
    return isCanScroll && super.onTouchEvent(ev);

    }
    }


    这时候ViewPage全部替换成我们定义这个类的名称
    在这里插入图片描述
    替换
    在这里插入图片描述
    在这里插入图片描述
    调用方法


     viewPager.setScanScroll(false);

    这时候屏幕就禁止滑动了,可以点击按钮进行切换


    源代码
    点击下载

    收起阅读 »

    乐观锁VS悲观锁

    乐观锁 VS 悲观锁 悲观锁:总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。 乐观锁:总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此...
    继续阅读 »


    乐观锁 VS 悲观锁


    悲观锁:总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。


    乐观锁:总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改。
    乐观锁在Java中通过使用无锁来实现,常用的是CAS,Java中原子类的递增就是通过CAS自旋实现。
    在这里插入图片描述


    CAS


    CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。


    一个 CAS 涉及到以下操作:
    我们假设内存中的原数据V,旧的预期值A,需要修改的新值B,



    1. 比较 A 与 V 是否相等。(比较)

    2. 如果比较相等,将 B 写入 V。(交换)

    3. 返回操作是否成功。


    CAS的底层原理



    • 调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令

    • 这是一种完全依赖于硬件的功能,通过它实现原子操作

    • 原语的执行必须是连续的,在执行过程中不允许被中断,CAS 是 CUP 的一条原子指令


    CAS的三大问题



    • 如果 CAS 长时间一直不成功,会给 CPU 带来很大的开销,在Java的实现中是一只通过while循环自旋CAS获取锁。

    • 只能保证一个共享变量的原子操作

    • 引出了 ABA 问题


    ABA问题


    CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。


    如何解决ABA问题
    加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会出现老的值。


    UnSafe


    Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。


    使用Unsafe可用来直接访问系统内存资源并进行自主管理,Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。


    Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等。


    这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是Unsafe的,它所分配的内存需要手动free(不被GC回收)。如果对Unsafe类理解的不够透彻,就进行使用的话,就等于给自己挖了无形之坑,最为致命。

    收起阅读 »

    面试官:说说什么是Java内存模型(JMM)?

    1. 为什么要有内存模型? 1.1. 硬件内存架构 1.2. 缓存一致性问题 1.3. 处理器优化和指令重排序 2. 并发编程的问题 3. Java 内存模型 3.1. Java 运行时内存区域与硬件内存的关...
    继续阅读 »







    在面试中,面试官经常喜欢问:『说说什么是Java内存模型(JMM)?』


    面试者内心狂喜,这题刚背过:『Java内存主要分为五大块:堆、方法区、虚拟机栈、本地方法栈、PC寄存器,balabala……』


    面试官会心一笑,露出一道光芒:『好了,今天的面试先到这里了,回去等通知吧』


    一般听到等通知这句话,这场面试大概率就是凉凉了。为什么呢?因为面试者弄错了概念,面试官是想考察JMM,但是面试者一听到Java内存这几个关键字就开始背诵八股文了。Java内存模型(JMM)和Java运行时内存区域区别可大了呢,不要走开接着往下看,答应我要看完。


    1. 为什么要有内存模型?


    要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。


    1.1. 硬件内存架构


    在这里插入图片描述
    (1)CPU


    去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个,这就意味着多个CPU或者多个核可以同时(并发)工作。如果使用Java 起了一个多线程的任务,很有可能每个 CPU 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。


    (2)CPU Register


    CPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。


    (3)CPU Cache Memory


    CPU Cache Memory也就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。


    (4)Main Memory


    Main Memory 就是主存,主存比 L1、L2 缓存要大很多。


    注意:部分高端机器还有 L3 三级缓存。


    1.2. 缓存一致性问题


    由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。


    使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。
    在这里插入图片描述
    在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。


    因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。


    1.3. 处理器优化和指令重排序


    为了提升性能在 CPU 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到缓存一致性问题。那还有没有办法进一步提升 CPU 的执行效率呢?答案是:处理器优化。



    为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。



    除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序。
    在这里插入图片描述



    处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型:



    • 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。

    • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

    • 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。



    2. 并发编程的问题


    上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 Java 内存模型有啥关系吗?不要急咱们慢慢往下看。


    熟悉 Java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。


    在这里插入图片描述
    缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。


    出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 CPU 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。


    所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障


    3. Java 内存模型


    同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。


    3.1. Java 运行时内存区域与硬件内存的关系


    了解过 JVM 的同学都知道,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。
    在这里插入图片描述
    从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。


    3.2. Java 线程与主内存的关系


    Java 内存模型是一种规范,定义了很多东西:



    • 所有的变量都存储在主内存(Main Memory)中。

    • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。

    • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。

    • 不同的线程之间无法直接访问对方本地内存中的变量。


    看文字太枯燥了,我又画了一张图:
    在这里插入图片描述


    3.3. 线程间通信


    如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。
    在这里插入图片描述
    为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:



    • lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。

    • unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    • read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

    • load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

    • use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

    • assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

    • store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

    • write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。



    注意:工作内存也就是本地内存的意思。



    4. 有态度的总结


    由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。


    数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。


    Java 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。


    为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write


    – End –


    关于Java 内存模型还有很多东西没有展开讲,比如说:内存屏障happens-before锁机制CAS等等。要肝一个系列了,加油!



    作者:雷小帅


    推荐一个Github 开源项目,『Java八股文』Java面试套路,Java进阶学习,打破内卷拿大厂Offer,升职加薪!https://github.com/CoderLeixiaoshuai/java-eight-part


    作者简介: ?读过几年书:华中科技大学硕士毕业;
    ?浪过几个大厂:华为、网易、百度……
    ?一直坚信技术能改变世界,愿保持初心,加油技术人!


    微信搜索公众号【爱笑的架构师】,关注这个对技术有追求且有趣的打工人。


    收起阅读 »

    TIOBE5月编程语言榜单:Python超越Java重回第二,Rust崛起

    作者 | 苏宓 出品 | CSDN(ID:CSDNnews) TIOBE 官方最新发布了 5 月的编程语言榜单,不妨一起来看一下本月榜单中又有哪些最新的变化呢? Python 重回第二 和 4 月相比,本月榜单的 TOP 10 ...
    继续阅读 »

    作者 | 苏宓


    出品 | CSDN(ID:CSDNnews)


    TIOBE 官方最新发布了 5 月的编程语言榜单,不妨一起来看一下本月榜单中又有哪些最新的变化呢?



    Python 重回第二


    和 4 月相比,本月榜单的 TOP 10 中变化最大的非 Python 与 Java 莫属。


    现实来看,曾经的铁三角 Java、C、C++ 如今已被彻底瓦解,犹记得 2020 年 5 月,Java 被 C 超越后,于 11 月份再次被 Python 短暂碾压,一路跌到了第三位,虽然后来 Java 再次追上 Python,可还是无法拯救其下滑的趋势。


    据最新的榜单显示,Python 以 0.13% 的差异再次领先 Java,位居第二,Java 排名第三。



    面对市场份额一直在缩减的 Java,轻芒联合创始人、前豌豆荚技术负责人范怀宇在《2020-2021开发者大调查》中剖析道,“Kotlin 虽然在统计中总的比例不高,但在 Android 开发上进一步在取代 Java。Kotlin 作为另一个基于 JVM 的编程语言,一方面可以保持 Java 使用 JVM 稳定和高性能的好处,享用 JVM 已有的生态;另一方面,通过其更为灵活的语法特性,越来越广泛地被 Android 开发者使用,已然成为 Android 的首选编程语言,对于新参与 Android 的开发者而言,Kotlin 是更好的学习对象。”


    因此,Java 一直处于下滑的趋势,也在情理之中。


    与之形成鲜明对比且处于蒸蒸日上的 Python,TIOBE CEO Paul Jansen 评估道,“去年 11 月,Python 短暂地超越了 Java,位居 TIOBE 榜单的第二位。本月中,Python 再次成功上位,有理由相信它将在这一位置上待得更久。在未来半年内,Python 或许能成为 TIOBE 榜单上的第一名,因为 C(宛如 Java 一样)的流行度正在下降。”



    被大厂拥抱的 Rust,属于它的时代已至!


    至于 C 语言为何会不再受到重用?我们也从另一种编程语言 Rust 身上找到了答案。


    在本月榜单中,Rust 从上个月的第 29 名上升到了本月的 24 名,其生态在各个科技大厂的支持下,也大有进入 TOP 20 的潜力。


    不久前,Facebook 正式宣布加入 Rust 基金会,与其他成员共同负责 Rust 开源生态以及社区的运作和发展,与此同时,其承诺将进一步加大对 Rust 语言的采用。事实上,Facebook 是继 AWS、Google、华为、微软、Mozilla 后最新加入 Rust 基金会的成员,此前,很多科技公司早已深度地拥抱了 Rust。


    凭借安全、高性能、可靠性和高生产率等特性,AWS 在众多产品中应用 Rust 的同时,不惜花重金聘用 Rust 编译器联合创始人 Felix Klock 来加码 Rust 的应用;Google 将其应用到了 Android 系统中以及基于此重新实现一些重要的安全组件;微软在寻求替代 C、C++ 语言之际,不仅将 Rust 整合到了 Azure 服务中,也正在用 Rust 来实现一种新的编程语言;就连 Linux 内核开发者也开始在 Linux 内核中添加 Rust 支持的 RFC......


    Rust 下一步,未来可期。



    其他编程语言排名


    下面列出了完整的 21-50 名,因为是非官方发布的,所以可能存在遗漏:



    第 51-100 名如下,由于它们之间的数值差异较小,仅以文本形式列出(按字母排序):



    • ActionScript, Arc, B4X, bc, Boo, C shell, CFML, Clojure, Common Lisp, Eiffel, Erlang, F#, Hack, Icon, IDL, Inform, Io, J, JScript.NET, Korn shell, Lasso, Maple, MEL, ML, MQL4, MUMPS, NATURAL, OCaml, OpenCL, OpenEdge ABL, Oz, PL/I, PostScript, Pure Data, Q, Racket, Ring, RPG, Scheme, Simulink, Smalltalk, SPARK, SPSS, Stata, Tcl, Vala/Genie, Verilog, XC, Xojo, Zig




    Top 10 编程语言 TIOBE 指数走势(2002-2020)




    历史排名(1986-2021)


    注:以下排名位次取决于12个月的平均值。




    编程语言“名人榜”(2003-2020)



    【说明】:


    TIOBE 编程语言社区排行榜是编程语言流行趋势的一个指标,每月更新,这份排行榜排名基于全球技术工程师、课程和第三方供应商的数量,其中包括了流行的搜索引擎以及技术社区,如 Google、百度、维基百科、CSDN、必应、Hao 123 等等。具体的计算方式详见:https://www.tiobe.com/tiobe-index/programming-languages-definition/。请注意这个排行榜只是反映某个编程语言的热门程度,并不能说明一门编程语言好不好,或者一门语言所编写的代码数量多少。


    这个排行榜可以用来考察你的编程技能是否与时俱进,也可以在开发新系统时作为一个语言选择依据。


    详细榜单信息可参考 TIOBE 官网:https://www.tiobe.com/tiobe-index



    60+专家,13个技术领域,CSDN 《IT 人才成长路线图》重磅来袭!


    直接扫码或微信搜索「CSDN」公众号,后台回复关键词「路线图」,即可获取完整路线图!



    收起阅读 »

    Android应用架构之MVVM模式

    前言 早期的Android应用开发中,Activity/Fragment承担了过多的职责,它们不仅负责了应用界面的显示,而且负责了业务逻辑的处理。这样一来,Activity/Fragment很容易就变得臃肿、复杂,造成应用难以测试、维护和扩展。随着Andro...
    继续阅读 »


    前言


    早期的Android应用开发中,Activity/Fragment承担了过多的职责,它们不仅负责了应用界面的显示,而且负责了业务逻辑的处理。这样一来,Activity/Fragment很容易就变得臃肿、复杂,造成应用难以测试、维护和扩展。随着Android应用开发技术的不断发展和成熟,Android应用架构的设计得到了越来越多开发人员的关注和重视。目前,Android的应用架构主要有MVC、MVP和MVVM模式,本文将介绍一下MVVM模式。


    相关知识



    学习项目



    MVP模式


    MVVM模式可以说是MVP模式的进一步发展,所以先来了解一下MVP模式。


    MVP (Model-View-Presenter) 模式的结构如下图所示:


    MVP模式.png


    MVP模式将应用分为三层:Model层主要负责数据的提供,View层主要负责界面的显示,Presenter层主要负责业务逻辑的处理。


    在MVP模式中,Model层和View层不能直接通信,Presenter层负责充当中间人,实现Model层和View层之间的间接通信。View层和Presenter层互相持有对方的引用,实现View层和Presenter层之间的通信。


    MVP模式的主要优点是:分离了Model层和View层,分离了视图操作和业务逻辑,降低了耦合。


    MVVM模式


    MVVM (Model-View-ViewModel) 模式的结构如下图所示:


    MVVM模式.png


    MVVM模式与MVP模式一样,也将应用分为三层,并且各个对应的层的职责相似:



    • Model层,主要负责数据的提供。Model层提供业务逻辑的数据结构(比如,实体类),提供数据的获取(比如,从本地数据库或者远程网络获取数据),提供数据的存储。

    • View层,主要负责界面的显示。View层不涉及任何的业务逻辑处理,它持有ViewModel层的引用,当需要进行业务逻辑处理时通知ViewModel层。

    • ViewModel层,主要负责业务逻辑的处理。ViewModel层不涉及任何的视图操作。通过官方提供的Data Binding库,View层和ViewModel层中的数据可以实现绑定,ViewModel层中数据的变化可以自动通知View层进行更新,因此ViewModel层不需要持有View层的引用。ViewModel层可以看作是View层的数据模型和Presenter层的结合。


    MVVM模式与MVP模式最大的区别在于:ViewModel层不持有View层的引用。这样进一步降低了耦合,View层代码的改变不会影响到ViewModel层。


    MVVM模式相对于MVP模式主要有如下优点:



    • 进一步降低了耦合。ViewModel层不持有View层的引用,当View层发生改变时,只要View层绑定的数据不变,那么ViewModel层就不需要改变。而在MVP模式下,当View层发生改变时,操作视图的接口就要进行相应的改变,那么Presenter层就需要修改了。

    • 不用再编写很多样板代码。通过官方的Data Binding库,UI和数据之间可以实现绑定,不用再编写大量的findViewById()和操作视图的代码了。总之,Activity/Fragment的代码可以做到相当简洁。


    例子


    下面举一个简单的例子来实践MVVM模式。完整的项目代码可以去GitHub上查看:



    https://github.com/chongyucaiyan/MVVMDemo



    例子实现的主要功能是:点击按钮网络查询天气,查询成功后在界面上显示天气信息。主界面如下图所示:


    MVVMDemo界面.png


    MVVM模式的代码组织结构建议按照 业务功能 进行划分,具体操作是:每个业务功能独立一个包存放,每个业务功能包下面再按Model、View、ViewModel分包存放。所有的Model存放在model包下面,所有的Activity和Fragment存放在activity包下面,所有的ViewModel存放在viewmodel包下面。该例子比较简单,只有一个weather业务功能模块,最终的代码组织结构如下图所示:


    MVVMDemo代码组织结构.png


    编写Model


    查询杭州天气的URL为:



    http://www.weather.com.cn/data/cityinfo/101210101.html



    访问该URL将返回一串JSON字符串,如下所示:


    {"weatherinfo":{"city":"杭州","cityid":"101210101","temp1":"5℃","temp2":"20℃","weather":"晴转多云","img1":"n0.gif","img2":"d1.gif","ptime":"18:00"}}

    按照此JSON字符串,可以编写相应的实体类。WeatherData类的代码如下所示:


    public class WeatherData {

    private WeatherInfo weatherinfo;

    public WeatherInfo getWeatherinfo() {
    return weatherinfo;
    }

    public void setWeatherinfo(WeatherInfo weatherinfo) {
    this.weatherinfo = weatherinfo;
    }
    }

    WeatherInfo类的代码如下所示:


    public class WeatherInfo {

    private String city;

    private String cityid;

    private String temp1;

    private String temp2;

    private String weather;

    private String img1;

    private String img2;

    private String ptime;

    public String getCity() {
    return city;
    }

    public void setCity(String city) {
    this.city = city;
    }

    public String getCityid() {
    return cityid;
    }

    public void setCityid(String cityid) {
    this.cityid = cityid;
    }

    public String getTemp1() {
    return temp1;
    }

    public void setTemp1(String temp1) {
    this.temp1 = temp1;
    }

    public String getTemp2() {
    return temp2;
    }

    public void setTemp2(String temp2) {
    this.temp2 = temp2;
    }

    public String getWeather() {
    return weather;
    }

    public void setWeather(String weather) {
    this.weather = weather;
    }

    public String getImg1() {
    return img1;
    }

    public void setImg1(String img1) {
    this.img1 = img1;
    }

    public String getImg2() {
    return img2;
    }

    public void setImg2(String img2) {
    this.img2 = img2;
    }

    public String getPtime() {
    return ptime;
    }

    public void setPtime(String ptime) {
    this.ptime = ptime;
    }
    }

    编写ViewModel


    ViewModel不涉及任何的视图操作,只进行业务逻辑的处理。通过官方提供的Data Binding库,当ViewModel中的数据发生变化时,UI将自动更新。QueryWeatherViewModel的代码如下所示:


    public class QueryWeatherViewModel {

    private static final String TAG = "QueryWeatherViewModel";

    public final ObservableBoolean loading = new ObservableBoolean(false);

    public final ObservableBoolean loadingSuccess = new ObservableBoolean(false);

    public final ObservableBoolean loadingFailure = new ObservableBoolean(false);

    public final ObservableField<String> city = new ObservableField<>();

    public final ObservableField<String> cityId = new ObservableField<>();

    public final ObservableField<String> temp1 = new ObservableField<>();

    public final ObservableField<String> temp2 = new ObservableField<>();

    public final ObservableField<String> weather = new ObservableField<>();

    public final ObservableField<String> time = new ObservableField<>();

    private Call<WeatherData> mCall;

    public QueryWeatherViewModel() {

    }

    public void queryWeather() {
    loading.set(true);
    loadingSuccess.set(false);
    loadingFailure.set(false);

    mCall = RetrofitManager.get()
    .create(QueryWeatherRequest.class)
    .queryWeather();
    mCall.enqueue(new Callback<WeatherData>() {

    @Override
    public void onResponse(Call<WeatherData> call, Response<WeatherData> response) {
    WeatherInfo weatherInfo = response.body().getWeatherinfo();
    city.set(weatherInfo.getCity());
    cityId.set(weatherInfo.getCityid());
    temp1.set(weatherInfo.getTemp1());
    temp2.set(weatherInfo.getTemp2());
    weather.set(weatherInfo.getWeather());
    time.set(weatherInfo.getPtime());

    loading.set(false);
    loadingSuccess.set(true);
    }

    @Override
    public void onFailure(Call<WeatherData> call, Throwable t) {
    if (call.isCanceled()) {
    Log.i(TAG, "call is canceled.");
    } else {
    loading.set(false);
    loadingFailure.set(true);
    }
    }
    });
    }

    public void cancelRequest() {
    if (mCall != null) {
    mCall.cancel();
    }
    }
    }

    编写View


    View不涉及任何的业务逻辑处理,只进行界面的显示。在xml布局文件中,通过官方提供的Data Binding库,将UI与ViewModel中的数据进行绑定,当ViewModel中的数据发生变化时,UI将自动更新。xml布局文件的代码如下所示:


    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">


    <data>

    <import type="android.view.View" />

    <variable
    name="viewModel"
    type="com.github.cyc.mvvmdemo.weather.viewmodel.QueryWeatherViewModel" />

    </data>

    <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/default_content_padding"
    tools:context="com.github.cyc.mvvmdemo.weather.activity.QueryWeatherActivity">


    <Button
    android:id="@+id/btn_query_weather"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:text="@string/query_weather"
    android:enabled="@{viewModel.loading ? false : true}"
    android:onClick="@{() -> viewModel.queryWeather()}" />


    <RelativeLayout
    android:id="@+id/vg_weather_info"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/btn_query_weather"
    android:layout_marginTop="@dimen/query_weather_margin"
    android:visibility="@{viewModel.loadingSuccess ? View.VISIBLE : View.GONE}">


    <TextView
    android:id="@+id/tv_city"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:text="@string/city" />


    <TextView
    android:id="@+id/tv_city_value"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/tv_city"
    android:layout_alignBottom="@id/tv_city"
    android:text="@{viewModel.city}"
    tools:text="杭州" />


    <TextView
    android:id="@+id/tv_city_id"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/tv_city"
    android:layout_marginTop="@dimen/query_weather_margin"
    android:textStyle="bold"
    android:text="@string/city_id" />


    <TextView
    android:id="@+id/tv_city_id_value"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/tv_city_id"
    android:layout_alignBottom="@id/tv_city_id"
    android:text="@{viewModel.cityId}"
    tools:text="101210101" />


    <TextView
    android:id="@+id/tv_temp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/tv_city_id"
    android:layout_marginTop="@dimen/query_weather_margin"
    android:textStyle="bold"
    android:text="@string/temperature" />


    <TextView
    android:id="@+id/tv_temp1_value"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/tv_temp"
    android:layout_alignBottom="@id/tv_temp"
    android:text="@{viewModel.temp1}"
    tools:text="5℃" />


    <TextView
    android:id="@+id/tv_tilde"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/tv_temp1_value"
    android:layout_alignBottom="@id/tv_temp"
    android:text="@string/tilde" />


    <TextView
    android:id="@+id/tv_temp2_value"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/tv_tilde"
    android:layout_alignBottom="@id/tv_temp"
    android:text="@{viewModel.temp2}"
    tools:text="10℃" />


    <TextView
    android:id="@+id/tv_weather"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/tv_temp"
    android:layout_marginTop="@dimen/query_weather_margin"
    android:textStyle="bold"
    android:text="@string/weather" />


    <TextView
    android:id="@+id/tv_weather_value"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/tv_weather"
    android:layout_alignBottom="@id/tv_weather"
    android:text="@{viewModel.weather}"
    tools:text="" />


    <TextView
    android:id="@+id/tv_time"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/tv_weather"
    android:layout_marginTop="@dimen/query_weather_margin"
    android:textStyle="bold"
    android:text="@string/release_time" />


    <TextView
    android:id="@+id/tv_time_value"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/tv_time"
    android:layout_alignBottom="@id/tv_time"
    android:text="@{viewModel.time}"
    tools:text="10:00" />

    </RelativeLayout>

    <ProgressBar
    android:id="@+id/pb_progress"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" />


    <TextView
    android:id="@+id/tv_query_failure"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:text="@string/query_failure"
    android:visibility="@{viewModel.loadingFailure ? View.VISIBLE : View.GONE}" />

    </RelativeLayout>
    </layout>

    在Activity中,通过官方提供的Data Binding库加载布局文件,创建ViewModel,并绑定View和ViewModel。QueryWeatherActivity的代码如下所示:


    public class QueryWeatherActivity extends AppCompatActivity {

    // ViewModel
    private QueryWeatherViewModel mViewModel;

    // DataBinding
    private ActivityQueryWeatherBinding mDataBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_query_weather);
    // 创建ViewModel
    mViewModel = new QueryWeatherViewModel();
    // 绑定View和ViewModel
    mDataBinding.setViewModel(mViewModel);
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    // 取消请求
    mViewModel.cancelRequest();
    }
    }

    总结


    MVVM模式将应用分为三层:Model层主要负责数据的提供,View层主要负责界面的显示,ViewModel层主要负责业务逻辑的处理。各个层职责单一,结构清晰,应用可以很方便地进行测试、维护和扩展。


    参考


    收起阅读 »

    Android仿微信红包动画平移动画

    Android 仿微信红包动画 平移动画先看效果图:简单思路:先找好素材,一张红包封面和 “开”这个图片,先用ps将红包图片P成两部分,两个椭圆的样子。“开”要有不同角度的,因为要由帧动画完成。开完之后背景设个随机数。红包封面可以用ps软件p出来,用椭圆选框工...
    继续阅读 »

    Android 仿微信红包动画 平移动画

    先看效果图:

    在这里插入图片描述

    简单思路:

    先找好素材,一张红包封面和 “开”这个图片,先用ps将红包图片P成两部分,两个椭圆的样子。“开”要有不同角度的,因为要由帧动画完成。开完之后背景设个随机数。红包封面可以用ps软件p出来,用椭圆选框工具即可。

    素材:

    在这里插入图片描述

    目录图:

    在这里插入图片描述

    详细完全代码:
    1. xml开动画——用帧动画完成。

    这节课有详细讲解 mooc讲解的视频教程保证能听懂,简单易学

    <?xml version="1.0" encoding="utf-8"?>

    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">

    <item android:drawable="@drawable/open1" android:duration="150"></item>
    <item android:drawable="@drawable/open2" android:duration="150"></item>
    <item android:drawable="@drawable/open3" android:duration="150"></item>
    <item android:drawable="@drawable/open5" android:duration="150"></item>
    <item android:drawable="@drawable/open6" android:duration="150"></item>
    <item android:drawable="@drawable/open1" android:duration="150"></item>
    <item android:drawable="@drawable/open2" android:duration="150"></item>
    <item android:drawable="@drawable/open3" android:duration="150"></item>
    <item android:drawable="@drawable/open5" android:duration="150"></item>
    <item android:drawable="@drawable/open6" android:duration="150"></item>
    <item android:drawable="@drawable/open1" android:duration="150"></item>
    </animation-list>`
    2. 布局代码:
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainActivity">


    <TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="50dp"
    android:layout_alignParentStart="true"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_alignParentBottom="true"
    android:layout_marginStart="59dp"
    android:layout_marginLeft="59dp"
    android:layout_marginTop="193dp"
    android:layout_marginEnd="55dp"
    android:layout_marginRight="55dp"
    android:layout_marginBottom="268dp"
    android:gravity="center"
    android:text="恭喜同学,获得随机红包:"
    android:textSize="20dp" />


    <TextView
    android:id="@+id/text"
    android:layout_width="130dp"
    android:layout_height="50dp"
    android:layout_alignParentStart="true"
    android:layout_alignParentLeft="true"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_alignParentBottom="true"
    android:layout_marginStart="130dp"
    android:layout_marginLeft="130dp"
    android:layout_marginEnd="124dp"
    android:layout_marginRight="124dp"
    android:layout_marginBottom="231dp"
    android:gravity="center"
    android:text="TextView"
    android:textColor="@color/colorAccent"
    android:textSize="25dp" />


    <ImageView
    android:id="@+id/bg1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentStart="true"
    android:layout_alignParentTop="true"
    android:layout_marginTop="0dp"
    android:scaleType="fitXY"
    app:srcCompat="@drawable/start55" />


    <ImageView
    android:id="@+id/bg2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentStart="true"
    android:layout_alignParentBottom="true"
    android:layout_marginStart="0dp"
    android:layout_marginBottom="0dp"
    android:background="@drawable/start66"
    android:scaleType="fitXY" />


    <ImageView
    android:id="@+id/open"
    android:layout_width="wrap_content"
    android:layout_height="43dp"
    android:layout_alignTop="@+id/bg2"
    android:layout_alignParentStart="true"
    android:layout_alignParentLeft="true"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_alignParentBottom="true"
    android:layout_marginStart="133dp"
    android:layout_marginLeft="133dp"
    android:layout_marginTop="323dp"
    android:layout_marginEnd="134dp"
    android:layout_marginRight="134dp"
    android:layout_marginBottom="145dp"
    android:background="@drawable/open_rotate" />



    </RelativeLayout>

    3.最后Maintivity详细代码:
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.drawable.AnimationDrawable;
    import android.os.Handler;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.animation.Animation;
    import android.view.animation.TranslateAnimation;
    import android.widget.ImageView;
    import android.widget.TextView;
    import java.util.Random;

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private ImageView open,bg1,bg2;
    private AnimationDrawable animationDrawable;
    private Animation animation = null;
    private Bitmap bitmap;
    private TextView text;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    open=(ImageView)findViewById(R.id.open);
    bg1 =(ImageView)findViewById(R.id.bg1);
    bg2 =(ImageView)findViewById(R.id.bg2);

    //设置随机数---用作金额
    text=(TextView) (findViewById(R.id.text));
    Random ra =new Random();
    int bb= ra.nextInt(88889+1);
    text.setText(String.valueOf(bb));
    text.setTextSize(40);

    //"开"——帧动画
    animationDrawable =(AnimationDrawable) open.getBackground();
    open.setOnClickListener(this);
    }
    public void onClick(View v) {
    // TODO Auto-generated method stub
    switch (v.getId()) {
    case R.id.open:
    animationDrawable.start();//开——帧动画
    //线程延时运行
    Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
    @Override
    public void run() {
    //上+开 背景动画
    final TranslateAnimation openAnimation = new TranslateAnimation(
    TranslateAnimation.RELATIVE_TO_SELF, 0, TranslateAnimation.RELATIVE_TO_SELF, 0,
    TranslateAnimation.RELATIVE_TO_SELF, 0, TranslateAnimation.RELATIVE_TO_SELF, -0.5f);
    openAnimation.setDuration(1000); //设置动画的时间
    bg1.postDelayed(new Runnable() {
    @Override
    public void run() {
    bg1.setVisibility(View.VISIBLE);
    openAnimation.setFillAfter(true);//设置动画结束后位置保持不东
    bg1.startAnimation(openAnimation);
    }
    }, 0);//设置开始动画的准备时间
    open.postDelayed(new Runnable() {
    @Override
    public void run() {
    open.setVisibility(View.VISIBLE);
    openAnimation.setFillAfter(true);//设置动画结束后位置保持不东
    open.startAnimation(openAnimation);
    }
    }, 0);//设置开始动画的准备时间

    //下 背景动画
    final TranslateAnimation ctrlAnimation = new TranslateAnimation(
    TranslateAnimation.RELATIVE_TO_SELF, 0, TranslateAnimation.RELATIVE_TO_SELF, 0,
    TranslateAnimation.RELATIVE_TO_SELF, 0, TranslateAnimation.RELATIVE_TO_SELF, 0.4f);
    ctrlAnimation.setDuration(1000); //设置动画的时间
    bg2.postDelayed(new Runnable() {
    @Override
    public void run() {
    bg2.setVisibility(View.VISIBLE);
    ctrlAnimation.setFillAfter(true);//设置动画结束后位置保持不东
    bg2.startAnimation(ctrlAnimation);

    }
    }, 0);//设置开始动画的准备时间
    }
    }, 1500);//3秒后执行Runnable中的run方法
    break;
    }
    }
    }

    复制到Android studio 里面应该是可以直接运行的,关键部分代码有备注,不懂的代码可以复制问du娘。其中有用到线程作用是延时运行代码,是为了先让“开”动画运行完,然后“开”和上背景同时上移,代码很简单,祝各位能get到知识。

    收起阅读 »

    炫酷动画统计图表库:CurveGraphView

    CurveGraphView 是一个带有炫酷动画统计图表库,除了性能出色并具有许多样式选项之外,该库还支持单个平面内的多个线图。多个折线图对于比较不同股票,共同基金,加密货币等的价格非常有用。10.1 如何使用?1、在build.gradle 中添加如下依赖:...
    继续阅读 »

    CurveGraphView 是一个带有炫酷动画统计图表库,除了性能出色并具有许多样式选项之外,该库还支持单个平面内的多个线图。

    多个折线图对于比较不同股票,共同基金,加密货币等的价格非常有用。

    10.1 如何使用?

    1、在build.gradle 中添加如下依赖:

    dependencies {
    implementation 'com.github.swapnil1104:CurveGraphView:{current_lib_ver}'
    }

    2、在xml文件中添加布局:

     

    然后在代码中添加各种配置项

    curveGraphView = findViewById(R.id.cgv);

    curveGraphView.configure(
    new CurveGraphConfig.Builder(this)
    .setAxisColor(R.color.Blue) // Set X and Y axis line color stroke.
    .setIntervalDisplayCount(7) // Set number of values to be displayed in X ax
    .setGuidelineCount(2) // Set number of background guidelines to be shown.
    .setGuidelineColor(R.color.GreenYellow) // Set color of the visible guidelines.
    .setNoDataMsg(" No Data ") // Message when no data is provided to the view.
    .setxAxisScaleTextColor(R.color.Black) // Set X axis scale text color.
    .setyAxisScaleTextColor(R.color.Black) // Set Y axis scale text color
    .build()
    ););

    3、 提供数据集

    PointMap pointMap = new PointMap();
    pointMap.addPoint(0, 100);
    pointMap.addPoint(1, 500);
    pointMap.addPoint(5, 800);
    pointMap.addPoint(4, 600);
    10.2 效果图
    效果1效果2

    更多详细使用方式请看Github: https://github.com/swapnil1104/CurveGraphView

    下载地址:CurveGraphView-master.zip

    收起阅读 »

    数据集的圆弧形控件:Donut

    这个一个可以展示多个数据集的圆弧形控件,具有精细的颗粒控制、间隙功能、动画选项以及按比例缩放其值的功能。可以用于项目中的一些数据统计。9.1 如何使用?在build.gradle 中添加如下依赖:dependencies { implementatio...
    继续阅读 »

    这个一个可以展示多个数据集的圆弧形控件,具有精细的颗粒控制、间隙功能、动画选项以及按比例缩放其值的功能。可以用于项目中的一些数据统计。

    9.1 如何使用?

    build.gradle 中添加如下依赖:

    dependencies {
    implementation("app.futured.donut:library:$version")
    }

    然后在布局文件中添加View:


    然后在代码中设置数据:

    val dataset1 = DonutDataset(
    name = "dataset_1",
    color = Color.parseColor("#FB1D32"),
    amount = 1f
    )

    val dataset2 = DonutDataset(
    name = "dataset_2",
    color = Color.parseColor("#FFB98E"),
    amount = 1f
    )

    donut_view.cap = 5f
    donut_view.submitData(listOf(dataset1, dataset2))
    9.2 效果图

    更多用法请看Github: https://github.com/futuredapp/donut

    下载地址:donut-master.zip

    收起阅读 »

    View切换的过渡动画库:TransformationLayout

    这是一个用于Activity或者Fragment 以及View切换的过渡动画库,效果非常炫,它使用Material Design的运动系统过渡模式来创建变形动画。该库提供了用于绑定目标视图,设置淡入淡出和路径运动方向以及许多其他自定义选项的属性。8.1 如何使...
    继续阅读 »

    这是一个用于Activity或者Fragment 以及View切换的过渡动画库,效果非常炫,它使用Material Design的运动系统过渡模式来创建变形动画。该库提供了用于绑定目标视图,设置淡入淡出和路径运动方向以及许多其他自定义选项的属性。

    8.1 如何使用?

    build.gradle 中添加如下依赖:

    dependencies {
    implementation "com.github.skydoves:transformationlayout:1.0.4"
    }

    然后,需要将我们需要添加过渡动画的View包裹到 TransformationLayout:






    比如我们要将一个fab 过渡到一个card卡片,布局如下:







    重点来了,绑定视图,将一个targetView绑定到TransformationLayout有2种方式:

    • 通过在xml中指定属性:
    app:transformation_targetView="@+id/myCardView"
    • 在代码中绑定
    transformationLayout.bindTargetView(myCardView)

    当我们点击fab时,在监听器中调用startTransform()开始过渡动画,finishTransform()开始结束动画。

    // start transformation when touching the fab.
    fab.setOnClickListener {
    transformationLayout.startTransform()
    }

    // finish transformation when touching the myCardView.
    myCardView.setOnClickListener {
    transformationLayout.finishTransform()
    }

    更多使用方式请看Github: https://github.com/skydoves/TransformationLayout

    下载地址:TransformationLayout-main.zip

    收起阅读 »

    底部缩略库:RateBottomSheet

    有时候,为了推广我们的应用,我们需要让用户跳转到应用商店为我们的APP打分,传统的对话框用户体验很不好,而本库则是用BottomSheet来进行提示,它位于底部缩略区域,用户体验很好。7.1 如何使用呢?在build.gradle 中添加如下依赖:depend...
    继续阅读 »

    有时候,为了推广我们的应用,我们需要让用户跳转到应用商店为我们的APP打分,传统的对话框用户体验很不好,而本库则是用BottomSheet来进行提示,它位于底部缩略区域,用户体验很好。

    7.1 如何使用呢?

    build.gradle 中添加如下依赖:

    dependencies {
    implementation 'com.mikhaellopez:ratebottomsheet:1.1.0'
    }

    然后修改默认的string资源文件来改变显示文案:


    Like this App?
    Do you like using this application?
    Yes I do
    Not really

    Rate this app
    Would you mind taking a moment to rate it? It won\'t take more than a minute. Thanks for your support!
    Rate it now
    Remind me later
    No, thanks

    代码中使用:

    RateBottomSheetManager(this)
    .setInstallDays(1) // 3 by default
    .setLaunchTimes(2) // 5 by default
    .setRemindInterval(1) // 2 by default
    .setShowAskBottomSheet(false) // True by default
    .setShowLaterButton(false) // True by default
    .setShowCloseButtonIcon(false) // True by default
    .monitor()

    // Show bottom sheet if meets conditions
    // With AppCompatActivity or Fragment
    RateBottomSheet.showRateBottomSheetIfMeetsConditions(this)
    7.2 效果图

    更多详情请看Github:https://github.com/lopspower/RateBottomSheet

    下载地址:RateBottomSheet-master.zip

    收起阅读 »

    带动画的底部导航栏库:AnimatedBottomBar

    这是一个带动画的底部导航栏库。它使你可以以编程方式以及通过XML添加和删除选项卡。此外,我们可以轻松地从BottomBar拦截选项卡。限制访问应用程序导航中的高级区域时,“拦截”标签非常有用。流畅的动画提供了许多自定义选项,从动画插值器到设置波纹效果。6.1 ...
    继续阅读 »

    这是一个带动画的底部导航栏库。它使你可以以编程方式以及通过XML添加和删除选项卡。此外,我们可以轻松地从BottomBar拦截选项卡。限制访问应用程序导航中的高级区域时,“拦截”标签非常有用。流畅的动画提供了许多自定义选项,从动画插值器到设置波纹效果。

    6.1 如何使用?

    build.gradle 中添加如下依赖:

    dependencies {
    implementation 'nl.joery.animatedbottombar:library:1.0.8'
    }

    在xml文件中添加AnimatedBottomBar和自定义属性


    res/menu目录下定义tabs.xml文件:







    最后,代码中添加tab

    // Creating a tab by passing values
    val bottomBarTab1 = AnimatedBottomBar.createTab(drawable, "Tab 1")

    // Creating a tab by passing resources
    val bottomBarTab2 = AnimatedBottomBar.createTab(R.drawable.ic_home, R.string.tab_2, R.id.tab_home)
    6.2 效果图
    tab1tab2
    tab1.giftab2.gif

    详情信息请看Github: https://github.com/Droppers/AnimatedBottomBar

    下载地址:AnimatedBottomBar-master.zip

    收起阅读 »

    Android 颜色库:ColorX

    Android ColorX 以Kotlin 扩展函数的形式提供了一些重要的获取颜色的方法。通过提供不同颜色格式(RGB,HSV,CYMK等)的转换功能,它使开发变得更加轻松。该库的USP具有以下功能:颜色的不同阴影和色调。较深和较浅的阴影。颜色的补码5.1 ...
    继续阅读 »

    Android ColorX 以Kotlin 扩展函数的形式提供了一些重要的获取颜色的方法。
    通过提供不同颜色格式(RGB,HSV,CYMK等)的转换功能,它使开发变得更加轻松。该库的USP具有以下功能:

    • 颜色的不同阴影和色调。
    • 较深和较浅的阴影。
    • 颜色的补码
    5.1 如何使用?

    build.gradle 中添加如下依赖:

    dependencies {
    implementation 'me.jorgecastillo:androidcolorx:0.2.0'
    }

    在代码中,一系列的转换方法:

    val color = Color.parseColor("#e91e63")

    val rgb = color.asRgb()
    val argb = color.asArgb()
    val hex = color.asHex()
    val hsl = color.asHsl()
    val hsla = color.asHsla()
    val hsv = color.asHsv()
    val cmyk = color.asCmyk()

    val colorHsl = HSLColor(hue = 210f, saturation = 0.5f, lightness = 0.5f)

    val colorInt = colorHsl.asColorInt()
    val rgb = colorHsl.asRgb()
    val argb = colorHsl.asArgb()
    val hex = colorHsl.asHex()
    val cmyk = colorHsl.asCmyk()
    val hsla = colorHsl.asHsla()
    val hsv = colorHsl.asHsv()
    5.2 效果图

    更多详细使用信息请看Github:https://github.com/JorgeCastilloPrz/AndroidColorX

    下载地址:AndroidColorX-master.zip

    收起阅读 »

    reveal动画效果的库:EasyReveal

    从名字就知道,这是一个提供reveal动画效果的库,它的厉害之处在于可以提供不同尺寸、不同形状的reveal动画,并且还可以在定义它在屏幕任意位置开始和结束动画。4.1 如何使用?在build.gradle 中添加如下依赖:dependencies { .....
    继续阅读 »

    从名字就知道,这是一个提供reveal动画效果的库,它的厉害之处在于可以提供不同尺寸、不同形状的reveal动画,并且还可以在定义它在屏幕任意位置开始和结束动画。

    4.1 如何使用?

    build.gradle 中添加如下依赖:

    dependencies {
    ...
    implementation 'com.github.Chrisvin:EasyReveal:1.2'
    }

    然后,xml中,需要添加显示或者隐藏动画的View应该包裹在EasyRevealLinearLayout中:







    也可以在代码中添加:

    val revealLayout = EasyRevealLinearLayout(this)
    // Set the ClipPathProvider that is used to clip the view for reveal animation
    revealLayout.clipPathProvider = StarClipPathProvider(numberOfPoints = 6)
    // Set the duration taken for reveal animation
    revealLayout.revealAnimationDuration = 1500
    // Set the duration taken for hide animation
    revealLayout.hideAnimationDuration = 2000
    // Set listener to get updates during reveal/hide animation
    revealLayout.onUpdateListener = object: RevealLayout.OnUpdateListener {
    override fun onUpdate(percent: Float) {
    Toast.makeText(this@MainActivity, "Revealed percent: $percent", Toast.LENGTH_SHORT).show()
    }
    }
    // Start reveal animation
    revealLayout.reveal()
    // Start hide animation
    revealLayout.hide()
    4.2效果图
    Emotion DialogDrake DialogEmoji Dialog

    更多详细使用信息请看Github: https://github.com/Chrisvin/EasyReveal

    下载地址:EasyReveal-master.zip

    收起阅读 »

    美观而时尚的AlterDialog库:AestheticDialogs

    这是一个美观而时尚的AlterDialog库,目前可支持六种不同的对话框,如:Flash DialogConnectify DialogToaster DialogEmotion DialogDrake DialogEmoji Dialog并且啊,还提供了暗黑...
    继续阅读 »

    这是一个美观而时尚的AlterDialog库,目前可支持六种不同的对话框,如:

    • Flash Dialog
    • Connectify Dialog
    • Toaster Dialog
    • Emotion Dialog
    • Drake Dialog
    • Emoji Dialog
      并且啊,还提供了暗黑模式的适配。
    3.1 如何使用?

    build.gradle 中添加如下依赖:

    dependencies {
    ...
    implementation 'com.github.gabriel-TheCode:AestheticDialogs:1.1.0'
    }

    代码中,显示不同种类的对话框则调用对应的方法就好

    Flash:

    AestheticDialog.showFlashDialog(this, "Your dialog Title", "Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showFlashDialog(this, "Your dialog Title", "Your message", AestheticDialog.ERROR);

    Connectify:

    AestheticDialog.showConnectify(this,"Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showConnectify(this, "Your message", AestheticDialog.ERROR);

    /// Dark Theme
    AestheticDialog.showConnectifyDark(this,"Your message",AestheticDialog.SUCCESS);
    AestheticDialog.showConnectifyDark(this, "Your message", AestheticDialog.ERROR);

    Toaster:

     AestheticDialog.showToaster(this, "Your dialog Title", "Your message", AestheticDialog.ERROR);
    AestheticDialog.showToaster(this, "Your dialog Title", "Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showToaster(this, "Your dialog Title", "Your message", AestheticDialog.WARNING);
    AestheticDialog.showToaster(this, "Your dialog Title", "Your message", AestheticDialog.INFO);

    /// Dark Theme
    AestheticDialog.showToasterDark(this, "Your dialog Title", "Your message", AestheticDialog.ERROR);
    AestheticDialog.showToasterDark(this, "Your dialog Title", "Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showToasterDark(this, "Your dialog Title", "Your message", AestheticDialog.WARNING);
    AestheticDialog.showToasterDark(this, "Your dialog Title", "Your message", AestheticDialog.INFO);

    Drake :

     AestheticDialog.showDrake(this, AestheticDialog.SUCCESS);
    AestheticDialog.showDrake(this, AestheticDialog.ERROR);

    Emoji :

     AestheticDialog.showEmoji(this,"Your dialog Title", "Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showEmoji(this, "Your dialog Title", "Your message", AestheticDialog.ERROR);

    /// Dark Theme
    AestheticDialog.showEmojiDark(this,"Your dialog Title", "Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showEmojiDark(this, "Your dialog Title", "Your message", AestheticDialog.ERROR);

    Emotion :

     AestheticDialog.showEmotion(this,"Your dialog Title", "Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showEmotion(this, "Your dialog Title", "Your message", AestheticDialog.ERROR);

    Rainbow :

     AestheticDialog.showRainbow(this,"Your dialog Title", "Your message", AestheticDialog.SUCCESS);
    AestheticDialog.showRainbow(this,"Your dialog Title", "Your message", AestheticDialog.ERROR);
    AestheticDialog.showRainbow(this,"Your dialog Title", "Your message", AestheticDialog.WARNING);
    AestheticDialog.showRainbow(this,"Your dialog Title", "Your message", AestheticDialog.INFO);
    3.2 效果如下
    Flash DialogConnectify DialogToaster Dialog
    d1.gifd2.gifd3.gif
    Emotion DialogDrake DialogEmoji Dialog

    d5.gifd6.gif

    更多详情使用方法请看Github:https://github.com/gabriel-TheCode/AestheticDialogs

    下载地址:AestheticDialogs-master.zip

    收起阅读 »

    炫酷的显示或者隐藏一个布局:Flourish

    Flourish提供了一个炫酷的方式来显示或者隐藏一个布局,实现方式也很简单,就是对View或者布局进行了包装,通过构建者模式来提供api给上层调用。就像使用dialog一样,调用show和dissmiss方法来显示和隐藏。此外,通过这些类,我们还可以自定义动...
    继续阅读 »

    Flourish提供了一个炫酷的方式来显示或者隐藏一个布局,实现方式也很简单,就是对View或者布局进行了包装,通过构建者模式来提供api给上层调用。就像使用dialog一样,调用showdissmiss方法来显示和隐藏。此外,通过这些类,我们还可以自定义动画(正常,加速,反弹),或为布局方向设置我们自己的起点(左上,右下等)。

    2.1 如何使用?

    在build.gradle 中添加如下依赖:

    dependencies {
    implementation "com.github.skydoves:flourish:1.0.0"
    }

    然后在代码中,构建布局:

    Flourish flourish = new Flourish.Builder(parentLayout)
    // sets the flourish layout for showing and dismissing on the parent layout.
    .setFlourishLayout(R.layout.layout_flourish_main)
    // sets the flourishing animation for showing and dismissing.
    .setFlourishAnimation(FlourishAnimation.BOUNCE)
    // sets the orientation of the starting point.
    .setFlourishOrientation(FlourishOrientation.TOP_LEFT)
    // sets a flourishListener for listening changes.
    .setFlourishListener(flourishListener)
    // sets the flourish layout should be showed on start.
    .setIsShowedOnStart(false)
    // sets the duration of the flourishing.
    .setDuration(800L)
    .build();

    还提供有更简介的DSL:

    val myFlourish = createFlourish(parentLayout) {
    setFlourishLayout(R.layout.layout_flourish_main)
    setFlourishAnimation(FlourishAnimation.ACCELERATE)
    setFlourishOrientation(FlourishOrientation.TOP_RIGHT)
    setIsShowedOnStart(true)
    setFlourishListener { }
    }
    2.2 效果图
    效果1效果2

    更多详细使用请看Github:https://github.com/skydoves/Flourish

    下载地址:Flourish-master.zip

    收起阅读 »

    动画ViewPager库:LiquidSwipe

    这是一个很棒的ViewPager库,它在浏览ViewPager的不同页面时,显示波浪的滑动动画,效果非常炫酷。该库的USP是触摸交互的。这意味着在视图中显示类似液体的显示过渡时,应考虑触摸事件。1.1如何使用呢?导入以下Gradle依赖项:implementa...
    继续阅读 »

    这是一个很棒的ViewPager库,它在浏览ViewPager的不同页面时,显示波浪的滑动动画,效果非常炫酷。该库的USP是触摸交互的。这意味着在视图中显示类似液体的显示过渡时,应考虑触摸事件。

    1.1如何使用呢?

    导入以下Gradle依赖项:

    implementation 'com.github.Chrisvin:LiquidSwipe:1.3'

    然后将LiquidSwipeLayout添加为保存fragment布局的容器的根布局:






    1.2 效果图
    效果1效果2

    更多详细使用方法请看Github: https://github.com/Chrisvin/LiquidSwipe

    下载地址:LiquidSwipe-master.zip

    收起阅读 »

    总是听到有人说AndroidX,到底什么是AndroidX?

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。 Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出...
    继续阅读 »



    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。



    Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。





    Android系统在刚刚面世的时候,可能连它的设计者也没有想到它会如此成功,因此也不可能在一开始的时候就将它的API考虑的非常周全。随着Android系统版本不断地迭代更新,每个版本中都会加入很多新的API进去,但是新增的API在老版系统中并不存在,因此这就出现了一个向下兼容的问题。


    举个例子,当Android系统发布到3.0版本的时候,突然意识到了平板电脑的重要性,因此为了让Android可以更好地兼容平板,Android团队在3.0系统(API 11)中加入了Fragment功能。但是Fragment的作用并不只局限于平板,以前的老系统中也想使用这个功能该怎么办?于是Android团队推出了一个鼎鼎大名的Android Support Library,用于提供向下兼容的功能。比如我们每个人都熟知的support-v4库,appcompat-v7库都是属于Android Support Library的,这两个库相信任何做过Android开发的人都使用过。


    但是可能很多人并没有考虑过support-v4库的名字到底是什么意思,这里跟大家解释一下。4在这里指的是Android API版本号,对应的系统版本是1.6。那么support-v4的意思就是这个库中提供的API会向下兼容到Android 1.6系统。它对应的包名如下:



    类似地,appcompat-v7指的是将库中提供的API向下兼容至API 7,也就是Android 2.1系统。它对应的包名如下:



    可以发现,Android Support Library中提供的库,它们的包名都是以android.support.*开头的。


    但是慢慢随着时间的推移,什么1.6、2.1系统早就已经被淘汰了,现在Android官方支持的最低系统版本已经是4.0.1,对应的API版本号是15。support-v4、appcompat-v7库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。


    那么很明显,Android团队也意识到这种命名已经非常不合适了,于是对这些API的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX本质上其实就是对Android Support Library进行的一次升级,升级内容主要在于以下两个方面。


    第一,包名。之前Android Support Library中的API,它们的包名都是在android.support.*下面的,而AndroidX库中所有API的包名都变成了在androidx.*下面。这是一个很大的变化,意味着以后凡是android.*包下面的API都是随着Android操作系统发布的,而androidx.*包下面的API都是随着扩展库发布的,这些API基本不会依赖于操作系统的具体版本。


    第二,命名规则。吸取了之前命名规则的弊端,AndroidX所有库的命名规则里都不会再包含具体操作系统API的版本号了。比如,像appcompat-v7库,在AndroidX中就变成了appcompat库。


    一个AndroidX完整的依赖库格式如下所示:


    implementation 'androidx.appcompat:appcompat:1.0.2'

    了解了AndroidX是什么之后,现在你应该放轻松了吧?它其实并不是什么全新的东西,而是对Android Support Library的一次升级。因此,AndroidX上手起来也没有任何困难的地方,比如之前你经常使用的RecyclerView、ViewPager等等库,在AndroidX中都会有一个对应的版本,只要改一下包名就可以完全无缝使用,用法方面基本上都没有任何的变化。


    但是有一点需要注意,AndroidX和Android Support Library中的库是非常不建议混合在一起使用的,因为它们可能会产生很多不兼容的问题。最好的做法是,要么全部使用AndroidX中的库,要么全部使用Android Support Library中的库。


    而现在Android团队官方的态度也很明确,未来都会为AndroidX为主,Android Support Library已经不再建议使用,并会慢慢停止维护。另外,从Android Studio 3.4.2开始,我发现新建的项目已经强制勾选使用AndroidX架构了。





    那么对于老项目的迁移应该怎么办呢?由于涉及到了包名的改动,如果从Android Support Library升级到AndroidX需要手动去改每一个文件的包名,那可真得要改死了。为此,Android Studio提供了一个一键迁移的功能,只需要对着你的项目名右击 → Refactor → Migrate to AndroidX,就会弹出如下图所示的窗口。





    这里点击Migrate,Android Studio就会自动检查你项目中所有使用Android Support Library的地方,并将它们全部改成AndroidX中对应的库。另外Android Studio还会将你原来的项目备份成一个zip文件,这样即使迁移之后的代码出现了问题你还可以随时还原回之前的代码。


    好了,关于AndroidX的内容就讲到这里,相信也是解决了不少朋友心中的疑惑。由于这段时间以来一直在努力赶《第一行代码 第3版》的进度,所以原创文章的数量偏少了一些,也请大家见谅。





    关注我的技术公众号,每个工作日都有优质技术文章推送。


    微信扫一扫下方二维码即可关注:



    收起阅读 »

    Android kotlin+协程+Room数据库的简单使用

    Room Room是Google为了简化旧版的SQLite操作专门提供的 1.拥有了SQLite的所有操作功能 2.使用简单(类似于Retrofit),通过注解的方式实现相关功能。编译时自动生成实现类impl 3.LiveData,LifeCycle,Pag...
    继续阅读 »


    Room


    Room是Google为了简化旧版的SQLite操作专门提供的
    1.拥有了SQLite的所有操作功能
    2.使用简单(类似于Retrofit),通过注解的方式实现相关功能。编译时自动生成实现类impl
    3.LiveData,LifeCycle,Paging天然融合支持


    导入


    ...

    plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
    }

    dependencies {
    //room数据库
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5" // Kotlin 使用 kapt
    implementation "androidx.room:room-ktx:2.2.5"//Coroutines support for Room 协程操作库

    //lifecycle
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
    }

    User


    package com.zhangyu.myroom.data

    import android.os.Parcelable
    import androidx.room.Entity
    import androidx.room.PrimaryKey
    import kotlinx.android.parcel.Parcelize

    @Parcelize
    @Entity(tableName = "User")
    data class User(
    @PrimaryKey
    var id: String,
    var name: String
    ) : Parcelable

    UserDao


    package com.zhangyu.myroom.data

    import androidx.room.*

    @Dao
    interface UserDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun putUser(cacheBean: User)

    @Query("select * from User where id =:id")
    suspend fun getUser(id: String): User?

    @Query("select * from User")
    suspend fun getAllUser(): List<User>?

    @Delete
    fun delete(user: User)

    @Update(onConflict = OnConflictStrategy.REPLACE)
    fun update(user: User)

    }

    UserDatabase


    package com.zhangyu.myroom.data

    import android.util.Log
    import androidx.room.Database
    import androidx.room.Room
    import androidx.room.RoomDatabase
    import androidx.sqlite.db.SupportSQLiteDatabase
    import com.zhangyu.myroom.App

    private const val TAG = "CacheDataBase"

    //后续的数据库升级是根据这个version来比较的,exportSchema导出架构
    @Database(entities = [User::class], version = 1, exportSchema = false)
    abstract class UserDatabase : RoomDatabase() {
    companion object {
    var dataBase: UserDatabase

    init {
    //如果databaseBuilder改为inMemoryDatabaseBuilder则创建一个内存数据库(进程销毁后,数据丢失)
    dataBase = Room.databaseBuilder(App.context, UserDatabase::class.java, "db_user")
    //是否允许在主线程进行查询
    .allowMainThreadQueries()
    //数据库创建和打开后的回调,可以重写其中的方法
    .addCallback(object : Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
    super.onCreate(db)
    Log.d(TAG, "onCreate: db_user")
    }
    })
    //数据库升级异常之后的回滚
    .fallbackToDestructiveMigration()
    .build()
    }

    }

    abstract fun getUserDao(): UserDao
    }

    MainActivity


    package com.zhangyu.myroom

    import android.os.Bundle
    import android.util.Log
    import androidx.appcompat.app.AppCompatActivity
    import androidx.lifecycle.lifecycleScope
    import com.zhangyu.myroom.data.User
    import com.zhangyu.myroom.data.UserDatabase
    import kotlinx.coroutines.launch

    private const val TAG = "MainActivity"

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    testCache()
    }

    private fun testCache() {
    val userDao = UserDatabase.dataBase.getUserDao()
    userDao.putUser(User("1001", "zhangyu"))
    userDao.putUser(User("1002", "liming"))

    lifecycleScope.launch {
    val users = userDao.getAllUser()
    Log.e(TAG, "users: $users")
    val user = userDao.getUser("1001")
    Log.e(TAG, "user: $user")
    Log.e(TAG, "testCache: 协程执行完毕")
    }

    Log.e(TAG, "testCache: ")

    }


    }

    结果


    E/MainActivity: testCache: 
    E/MainActivity: users: [User(id=1001, name=zhangyu), User(id=1002, name=liming)]
    E/MainActivity: user: User(id=1001, name=zhangyu)
    E/MainActivity: testCache: 协程执行完毕
    收起阅读 »

    Android开发基础之控件RadioButton、RadioGroup

    目录 一、基础属性 RadioButton RadioGroup 二、自定义样式 三、监听事件       &nb...
    继续阅读 »





           


    一、基础属性


    RadioButton











































    1、layout_width 宽度
    2、layout_height 高度
    3、id 设置组件id
    4、text 设置显示的内容
    5、textColor 设置字体颜色
    6、textStyle 设置字体风格:normal(无效果)、bold(加粗)、italic(斜体)
    7、textSize 字体大小,单位常用sp
    8、background 控件背景颜色
    9、checked 默认选中该选项

           
    1、layout_width
    2、layout_height


            组件宽度和高度有4个可选值,如下图:
    在这里插入图片描述


           
    3、id


    // activity_main.xml
    android:id="@+id/btn1" // 给当前控件取个id叫btn1

    // MainActivity.java
    Button btn1=findViewById(R.id.btn1); // 按id获取控件
    btn1.setText("hh"); // 对这个控件设置显示内容

            如果在.java和.xml文件中对同一属性进行了不同设置,比如.java中设置控件内容hh,.xml中设置内容为aa,最后显示的是.java中的内容hh。


           
    4、text
           可以直接在activity_main.xml中写android:text="嘻嘻",也可以在strings.xml中定义好字符串,再在activity_main.xml中使用这个字符串。


    // strings.xml
    <string name="str1">嘻嘻</string>

    // activity_main.xml
    android:text="@string/str1"

           
    5、textColor
           与text类似,可以直接在activity_main.xml中写android:textColor="#FF0000FF",也可以在colors.xml中定义好颜色,再在activity_main.xml中使用这个颜色。


           
           


    9、checked
           checked=“true”,默认这个RadioButton是选中的。该属性只有在RadioGroup中每个RadioButton都设置了id的条件下才有效。


           


    程序示例:


        <RadioButton
    android:id="@+id/rb1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text=""
    android:textColor="@color/blue_700"
    android:textSize="50sp"
    android:background="@color/blue_50">

    </RadioButton>
    <RadioButton
    android:id="@+id/rb2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text=""
    android:textColor="@color/i_purple_500"
    android:textSize="50sp"
    android:background="@color/i_purple_200">

    </RadioButton>
    <RadioButton
    android:id="@+id/rb3"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="其他"
    android:textColor="@color/green_700"
    android:textSize="50sp"
    android:background="@color/green_100">

    </RadioButton>

            如果仅使用RadioButton而不使用RadioGroup,那么每个RadioButton都是可以选中的,如图:


            要实现仅能选中一个,应将几个RadioButton添加进一个组RadioGroup。
           
           


    RadioGroup































    1、layout_width 宽度
    2、layout_height 高度
    3、id 设置组件id
    4、orientation 内部控件排列的方向,例如水平排列或垂直排列
    5、paddingXXX 内边距,该控件内部控件间的距离
    6、background 控件背景颜色

           


    4、orientation


    内部控件的排列方式:



    • orientation=“vertical”,垂直排列

    • orientation=“horizontal”,水平排列


           


    5、paddingXXX


    内边距,该控件与内部的控件间的距离,常用的padding有以下几种:



    • padding,该控件与内部的控件间的距离

    • paddingTop,该控件与内部的控件间的上方的距离

    • paddingBottom,该控件与内部的控件间的下方的距离

    • paddingRight,该控件与内部的控件间的左侧的距离

    • paddingLeft,该控件与内部的控件间的右侧的距离


           


    程序示例:


    <RadioGroup
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/rg1"
    android:orientation="vertical"
    android:background="@color/yellow_100"
    android:padding="10dp">

    <RadioButton
    android:id="@+id/rb1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text=""
    android:textColor="@color/blue_700"
    android:textSize="50sp"
    android:background="@color/blue_50">

    </RadioButton>
    <RadioButton
    android:id="@+id/rb2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text=""
    android:textColor="@color/i_purple_500"
    android:textSize="50sp"
    android:background="@color/i_purple_200">

    </RadioButton>
    <RadioButton
    android:id="@+id/rb3"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="其他"
    android:textColor="@color/green_700"
    android:textSize="50sp"
    android:background="@color/green_100">

    </RadioButton>
    </RadioGroup>

    效果:


           


           


    二、自定义样式


           
    1、去掉RadioButton的圆圈
           在RadioButton的属性里写上button="@null"


           
    2、自定义背景


    新建一个选择器selector
    在这里插入图片描述
    在这里插入图片描述


           


    你取的名字.xml文件内编写代码:



    • item android:state_checked=“false” , 未选中这个RadioButton时的样式

    • item android:state_checked=“true” ,选中这个RadioButton时的样式

    • solid android:color="@color/yellow_100" ,设置实心的背景颜色

    • stroke android:width=“10dp” ,设置边框粗细
                 android:color="@color/i_purple_700" ,设置边框颜色

    • corners android:radius=“50dp” ,设置边框圆角大小


    程序示例:
    blue_selector.xml


    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="false">
    <shape android:shape="rectangle">
    <solid android:color="@color/blue_100"></solid>
    <stroke android:color="@color/blue_700" android:width="5dp"></stroke>
    <corners android:radius="30dp"></corners>
    </shape>
    </item>
    <item android:state_checked="true">
    <shape android:shape="rectangle">
    <solid android:color="@color/blue_500"></solid>
    <corners android:radius="30dp"></corners>
    </shape>
    </item>
    </selector>

    activity_main.xml


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

    <RadioGroup
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/rg1"
    android:orientation="horizontal"
    android:gravity="center"
    android:layout_marginTop="50dp">

    <RadioButton
    android:id="@+id/rb1"
    android:layout_width="130dp"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text=""
    android:textSize="50sp"
    android:background="@drawable/blue_selector"
    android:layout_marginRight="10dp"
    android:button="@null">

    </RadioButton>
    <RadioButton
    android:id="@+id/rb2"
    android:layout_width="130dp"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text=""
    android:textSize="50sp"
    android:background="@drawable/purple_selector"
    android:button="@null">

    </RadioButton>
    <RadioButton
    android:id="@+id/rb3"
    android:layout_width="130dp"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text="其他"
    android:textSize="50sp"
    android:background="@drawable/green_selector"
    android:layout_marginLeft="10dp"
    android:button="@null">

    </RadioButton>
    </RadioGroup>
    </LinearLayout>



    都未选:


    选中男:


    选中女:


    选中其他:


           


           


           


    三、监听事件


            在MainActivity.java内添加监听,当选中的按钮变化时,就会执行写好的操作:


    public class MainActivity extends AppCompatActivity {
    private RadioGroup rg1;

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

    // 获取控件id
    rg1=findViewById(R.id.rg1);
    // 监听事件
    rg1.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
    // 得到当前组被选中的RadioButton
    RadioButton rb=group.findViewById(checkedId);
    // 显示当前选中的RadioButton的内容
    Toast.makeText(MainActivity.this,rb.getText(),Toast.LENGTH_SHORT).show();
    }
    });
    }
    }

            监听到选中的按钮变化,并弹出选中按钮的内容:


           

           

    收起阅读 »

    [干货]手把手教你写一个安卓app

    摘要:最近有很多小伙伴在后台留言:Android Studio。我想大家是想写一个手机app,前面已经分享了在QT上如何写一个安卓蓝牙app,虽然qt可以做app,但是比起Android Studio还是差很多。这里介绍一种快速入门的方法来制作一款app,就算...
    继续阅读 »


    摘要:最近有很多小伙伴在后台留言:Android Studio。我想大家是想写一个手机app,前面已经分享了在QT上如何写一个安卓蓝牙app,虽然qt可以做app,但是比起Android Studio还是差很多。这里介绍一种快速入门的方法来制作一款app,就算你是零基础小白没有学习过java语言也没有关系,相信看完我的文章,半天时间也能做一个安卓app。本文针对初学者,大佬勿喷啊


    1. 创建HelloWorld项目


    这里我就不介绍如何安装这个Android Studio软件了,网上有很多教程或者去B站找对应的安装视频就可以了。安装好软件之后就开始按照下面的步骤新建工程了。
     选择一个空应用
     按照图片的配置方法,设置好工程名和路径


    2. 修改阿里云镜像源


    这一步一定要需要,不然的话你需要编译很久,因为在sync的过程中要下载的很多资源是在外网的,这里使用阿里云镜像源就会很快。修改后只对本项目有效:
     第一处代码


    maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
    maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
    maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
    maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }

    第二处代码


    maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
    maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
    maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
    maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }

     这样编译起来就会快很多,建议这样修改,不然很可能下载失败导致编译不成功!


    3. 真机调试


    我们可以编译完成后打包成apk文件发送到你的手机进行安装运行,但我建议还是手机连上数据线在线调试比较好,省去很多时间也非常方便。手机连接电脑后打开USB调试,这里以华为荣耀V10手机作为参考。



    • 1.选择USB连接方式是MIDI(将设备用做MIDI输入设备)

    • 2.在设置的“系统和更新”—>开发人员选项—>打开USB调试











    设备作为MIDI设备


    开启USB调试


    然后点击这个三角形,就可以看到手机上的APP显示了。
















    运行结果和上图一样。到这里我们已经完成了一个app的制作怎么样是不是很简单啊!


    接下来介绍一下代码目录,方便大家能够快速的掌握和了解项目所生成文件功能和用途!


    4. Android代码目录


    这里有两种文件架构,所打开的也是两种不同的目录文件。


    5. Android应用程序大致启动流程


    5.1. APP配置文件



    5.2. 活动文件(Java)



    5.3. 布局文件(XML)


    Android设计讲究前后端分离设计,上面的java文件是后端,引入了activity_main这个前端界面布局文件,如果想再设计一个界面就在layout文件夹下再新建一个 .xml文件就可以了。


    5.4. res资源目录(统一管理)



    5.4.1. colors.xml



    三个颜色有点少我们可以在加一些颜色但这里面来。


        <color name="white">#FFFFFF</color> <!--白色 -->
    <color name="ivory">#FFFFF0</color> <!--象牙色 -->
    <color name="lightyellow">#FFFFE0</color> <!--亮黄色 -->
    <color name="yellow">#FFFF00</color> <!--黄色 -->
    <color name="snow">#FFFAFA</color> <!--雪白色 -->
    <color name="floralwhite">#FFFAF0</color> <!--花白色 -->
    <color name="lemonchiffon">#FFFACD</color> <!--柠檬绸色 -->
    <color name="cornsilk">#FFF8DC</color> <!--米绸色 -->

    5.4.2. strings.xml



    5.4.3. styles.xml


     ***


    5、主界面布置


    5.1线性布局(LinearLayout)


    线性布局的形式可以分为两种,第一种横向线性布局,第二种纵向线性布局,总而言之都是以线性的形式一个个排列出来的,纯线性布局的缺点是很不方便修改控件的显示位置,所以开发中经常会以线性布局与相对布局嵌套的形式设置布局。


    5.2相对布局(RelativeLayout)


    相对布局是android布局中最为强大的,首先它可以设置的属性是最多了,其次它可以做的事情也是最多的。android手机屏幕的分辨率五花八门,为了考虑屏幕自适应的情况,在开发中建议大家都去使用相对布局,它的坐标取值范围都是相对的,所以使用它来做自适应屏幕是正确的。


    5.3帧布局(FrameLayout)


    帧布局原理是在控件中绘制任何一个控件都可以被后绘制的控件覆盖,最后绘制的控件会盖住之前的控件。界面中先绘制的ImageView 然后再绘制的TextView和EditView,后者就会覆盖在前者上面。


    5.4绝对布局(AbsoluteLayout)


    使用绝对布局可以设置任意控件在屏幕中XY坐标点,和帧布局一样绘制的控件会覆盖住之前绘制的控件,不建议大家使用绝对布局。android的手机分辨率五花八门,使用绝对布局的话在其它分辨率的手机上就无法正常的显示了。


    5.5表格布局(TableLayout)


    在表格布局中可以设置TableRow,可以设置表格中每一行显示的内容以及位置 ,可以设置显示的缩进,对齐的方式。


    在实际应用中线行布局和相对布局是最常用的,一般自己写的app布局都相对比较简单,所以这里我们使用线性布局。打开APP配置文件中的activity_main.xml,就可以在这里面愉快的编程了。如果你之前没有玩过Android Studio也没有关系,左边修改右边预览多试试几次就大概明白了。


     在这里我们可以修改点击图片所转换的网址,大家打开源码就知道如何修改了,这里就不在赘述!


     activity_main.xml文件中我们可以修改界面的布局。
     到这里基本上一个简单的安卓应用就完成了。只要你安装了Android Studio软件并且拿到我的源码就可以愉快的玩耍了。什么?你拿到我的代码却不能正常编译通过?下面就教大家如何把别人的源码拿到自己的软件中编译通过!


    6、代码移植


    以下是需要修改文件的地方,具体修改成啥样,可以参考一个你可以打的开的工程中的配置,参考对应的文件即可。


    1.修改build.gradle文件



    2.修改app/build.gradle文件


    修改版本号


    3.修改gradle/wrapper/gradle-wrapper.properties


    这个地方修改成你可以打开的工程的 . zip


    4.修改local.properties


    这个地方是你的软件安装路径所在的位置,要修改成你自己的安装路径

    公众号后台回复:firstapp,即可获取源码和教程文档!

    收起阅读 »

    Android开发杂记--打包release(发行版)App,并将其体积压缩至最小

    #Android开发杂记--打包 release(发行版)App,并将其体积压缩至最小 引言 生成签名文件 配置build.gradle文件 执行 Release 打包脚本 引言 &...
    继续阅读 »




    #Android开发杂记--打包 release(发行版)App,并将其体积压缩至最小





    引言


           我们在 Android Studio 中开发完App,直接点击右上角的 Run 会发现,App的大小至少10MB左右,且没有任何签名。
           这是因为我们直接 Run 的时候,生成的是 Debug 版本,为了开发时的编译速度,因此其体积比较大。但当我们想要将 App 正式上线时,不可能拿着 Debug 版本给人用,因此需要生成 Release(发行) 版。




    生成签名文件


           想要生成 Release 版,首先需要一个签名文件,制作工具很多,这里不重点介绍,我这里使用腾讯云·移动安全制作签名文件。如下所示,填写相关信息,点击制作签名即可。




    配置build.gradle文件


           首先在项目的根build.gradle中,添加一个依赖:


    buildscript {
    ...
    dependencies {
    classpath "com.android.tools.build:gradle:4.1.2"
    // 需要新添加的依赖
    classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.20'
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }
    ...

           随后在app目录下的build.gradle中添加签名和打包配置,有注释的地方表示要自己进行配置:


    ...
    // 添加打包插件
    apply plugin: 'AndResGuard'

    android {
    // 填写签名文件信息
    signingConfigs {
    key {
    storeFile file('D:\\Projects\\AndroidStudio\\key.keystore')
    storePassword '123456'
    keyAlias 'key'
    keyPassword '123456'
    }
    }
    ...
    defaultConfig {
    ...
    // 添加刚刚配置的签名文件
    signingConfig signingConfigs.key
    }

    buildTypes {
    release {
    // 修改为 true
    minifyEnabled true
    // 允许打包成多Dex文件
    multiDexEnabled true
    ...
    }
    }
    ...
    }

    dependencies {
    ...
    }

    // 以下配置直接复制过去即可
    andResGuard {
    // mappingFile = file("./resource_mapping.txt")
    mappingFile = null
    use7zip = true
    useSign = true
    // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
    keepRoot = false
    // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
    fixedResName = "arg"
    // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
    mergeDuplicatedRes = true
    whiteList = [
    // for your icon
    "R.drawable.icon",
    // for fabric
    "R.string.com.crashlytics.*",
    // for google-services
    "R.string.google_app_id",
    "R.string.gcm_defaultSenderId",
    "R.string.default_web_client_id",
    "R.string.ga_trackingId",
    "R.string.firebase_database_url",
    "R.string.google_api_key",
    "R.string.google_crash_reporting_api_key"
    ]
    compressFilePattern = [
    "*.png",
    "*.jpg",
    "*.jpeg",
    "*.gif",
    ]
    sevenzip {
    artifact = 'com.tencent.mm:SevenZip:1.2.20'
    //path = "/usr/local/bin/7za"
    }

    /**
    * 可选: 如果不设置则会默认覆盖assemble输出的apk
    **/
    // finalApkBackupPath = "${project.rootDir}/final.apk"

    /**
    * 可选: 指定v1签名时生成jar文件的摘要算法
    * 默认值为“SHA-1”
    **/
    // digestalg = "SHA-256"
    }



    执行 Release 打包脚本


           在 Android Studio 中点击 Gradle 选项卡,默认在 Android Studio 的右上角,如图所示。

           找到如下界面,右击 resguardRelease,再单击 Run 即可自动打包完成 Release(发行) 版本。

           等待打包完成(需要一点儿时间),在项目路径下的 app\build\outputs\apk\release中即可找到打包完成的apk,可以很明显的看出来,大小相比 Debug 版已经小了很多了。

    收起阅读 »

    RecyclerView 动画原理 | 如何存储并应用动画属性值?(2)

    RecyclerView 动画原理 | 如何存储并应用动画属性值?(1)存预布局动画属性值 InfoRecord中除了postInfo还有一个preInfo,分别表示后布局和预布局表项的动画信息。想必还有一个addToPreLayout()与addToPost...
    继续阅读 »

    RecyclerView 动画原理 | 如何存储并应用动画属性值?(1)


    存预布局动画属性值


    InfoRecord中除了postInfo还有一个preInfo,分别表示后布局和预布局表项的动画信息。想必还有一个addToPreLayout()addToPostLayout()对应:


    class ViewInfoStore {
    // 存储预布局表项与其动画信息
    void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
    record = InfoRecord.obtain();
    mLayoutHolderMap.put(holder, record);
    }
    record.preInfo = info; // 将后布局表项动画信息存储在 preInfo 字段中
    record.flags |= FLAG_PRE; // 追加 FLAG_PRE 到标志位
    }
    }
    复制代码

    addToPreLayout()在预布局阶段被调用:


    public class RecyclerView {
    private void dispatchLayoutStep1() {
    ...
    // 遍历可见表项
    int count = mChildHelper.getChildCount();
    for (int i = 0; i < count; ++i) {
    final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
    ...
    // 构建表项动画信息
    final ItemHolderInfo animationInfo = mItemAnimator
    .recordPreLayoutInformation(mState, holder,
    ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
    holder.getUnmodifiedPayloads());
    // 将表项动画信息保存到 mViewInfoStore
    mViewInfoStore.addToPreLayout(holder, animationInfo);
    ...
    }
    ...
    // 预布局
    mLayout.onLayoutChildren(mRecycler, mState);
    }
    }
    复制代码

    RecyclerView 布局的第一个阶段中,在第一次执行onLayoutChildren()之前,即预布局之前,遍历了所有的表项并逐个构建动画信息。以 Demo 为例,预布局之前,表项 1、2 的动画信息被构建并且标志位追加了FLAG_PRE,这些信息都被保存到mViewInfoStore实例中。


    紧接着RecyclerView执行了onLayoutChildren(),即进行预布局。


    public class RecyclerView {
    private void dispatchLayoutStep1() {
    // 遍历预布局前所有表项
    int count = mChildHelper.getChildCount();
    for (int i = 0; i < count; ++i) {
    final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
    ...
    final ItemHolderInfo animationInfo = mItemAnimator
    .recordPreLayoutInformation(mState, holder,
    ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
    holder.getUnmodifiedPayloads());
    mViewInfoStore.addToPreLayout(holder, animationInfo);
    ...
    }
    ...
    // 预布局
    mLayout.onLayoutChildren(mRecycler, mState);
    // 遍历预布局之后所有的表项
    for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
    final View child = mChildHelper.getChildAt(i);
    final ViewHolder viewHolder = getChildViewHolderInt(child);
    ...
    // 如果 ViewInfoStore 中没有对应的 ViewHolder 信息
    if (!mViewInfoStore.isInPreLayout(viewHolder)) {
    ...
    // 构建表项动画信息
    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
    ...
    // 将表项 ViewHolder 和其动画信息绑定并保存在 mViewInfoStore 中
    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);

    }
    }
    }
    }
    复制代码

    RecyclerView 在预布局之后再次遍历了所有表项。因为预布局会把表项 3 也填充到列表中,所以表项 3 的动画信息也会被存入mViewInfoStore,不过调用的是ViewInfoStore.addToAppearedInPreLayoutHolders()


    class ViewInfoStore {
    void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
    record = InfoRecord.obtain();
    mLayoutHolderMap.put(holder, record);
    }
    record.flags |= FLAG_APPEAR; // 追加 FLAG_APPEAR 到标志位
    record.preInfo = info; // 将预布局表项动画信息存储在 preInfo 字段中
    }
    }
    复制代码

    addToAppearedInPreLayoutHolders()addToPreLayout()的实现几乎一摸一样,唯一的不同是,标志位追加了FLAG_APPEAR,用于标记表项 3 是即将出现在屏幕中的表项。


    分析至此,可以得出下面的结论:



    RecyclerView 经历了预布局、后布局及布局第三阶段后,ViewInfoStore中就记录了每一个参与动画表项的三重信息:预布局位置信息 + 后布局位置信息 + 经历过的布局阶段。



    以 Demo 为例,表项 1、2、3 的预布局和后布局位置信息都被记录在ViewInfoStore中,其中表项 1 在预布局和后布局中均出现了,所以标志位中包含了FLAG_PRE | FLAG_POSTInfoRecord中用一个新的常量表示了这种状态FLAG_PRE_AND_POST


    class ViewInfoStore {
    static class InfoRecord {
    static final int FLAG_PRE = 1 << 2;
    static final int FLAG_POST = 1 << 3;
    static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
    }
    }
    复制代码

    而表项 2 只出现在预布局阶段,所以标志位仅包含了FLAG_PRE。表项 3 出现在预布局之后及后布局中,所以标志位中包含了FLAG_APPEAR | FLAG_POST


    应用动画属性值


    public class RecyclerView {
    private void dispatchLayoutStep3() {
    // 遍历后布局表项并构建动画信息再存储到 mViewInfoStore
    for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
    ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
    long key = getChangedHolderKey(holder);
    final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
    ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
    mViewInfoStore.addToPostLayout(holder, animationInfo);
    }
    // 触发表项执行动画
    mViewInfoStore.process(mViewInfoProcessCallback);
    ...
    }
    }
    复制代码

    RecyclerView 布局的第三个阶段中,在遍历完后布局表项后,调用了mViewInfoStore.process(mViewInfoProcessCallback)来触发表项执行动画:


    class ViewInfoStore {
    void process(ProcessCallback callback) {
    // 遍历所有参与动画表项的位置信息
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
    // 获取表项 ViewHolder
    final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
    // 获取与 ViewHolder 对应的动画信息
    final InfoRecord record = mLayoutHolderMap.removeAt(index);
    // 根据动画信息的标志位确定动画类型以执行对应的 ProcessCallback 回调
    if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
    callback.unused(viewHolder);
    } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
    if (record.preInfo == null) {
    callback.unused(viewHolder);
    } else {
    callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
    }
    } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
    callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
    } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
    callback.processPersistent(viewHolder, record.preInfo, record.postInfo);// 保持
    } else if ((record.flags & FLAG_PRE) != 0) {
    callback.processDisappeared(viewHolder, record.preInfo, null); // 消失动画
    } else if ((record.flags & FLAG_POST) != 0) {
    callback.processAppeared(viewHolder, record.preInfo, record.postInfo);// 出现动画
    } else if ((record.flags & FLAG_APPEAR) != 0) {
    }
    // 回收动画信息实例到池中
    InfoRecord.recycle(record);
    }
    }
    }
    复制代码

    ViewInfoStore.process()中遍历了包含所有表项动画信息的mLayoutHolderMap结构,并根据每个表项的标志位来确定执行的动画类型:




    • 表项 1 的标志位为FLAG_PRE_AND_POST所以会命中callback.processPersistent()




    • 表项 2 的标志位中只包含FLAG_PRE,所以(record.flags & FLAG_PRE) != 0成立,callback.processDisappeared()会命中。




    • 表项 3 的标志位中只包含FLAG_APPEAR | FLAG_POST,所以(record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST不成立,而(record.flags & FLAG_POST) != 0成立,callback.processAppeared()会命中。




    作为参数传入ViewInfoStore.process()ProcessCallback是 RecyclerView 中预定义的动画回调:


    class ViewInfoStore {
    // 动画回调
    interface ProcessCallback {
    // 消失动画
    void processDisappeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo,RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
    // 出现动画
    void processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo,RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
    ...
    }
    }

    public class RecyclerView {
    // RecyclerView 动画回调默认实现
    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
    new ViewInfoStore.ProcessCallback() {
    @Override
    public void processDisappeared(ViewHolder viewHolder, ItemHolderInfo info, ItemHolderInfo postInfo) {
    mRecycler.unscrapView(viewHolder);
    animateDisappearance(viewHolder, info, postInfo);//消失动画
    }
    @Override
    public void processAppeared(ViewHolder viewHolder,ItemHolderInfo preInfo, ItemHolderInfo info) {
    animateAppearance(viewHolder, preInfo, info);//出现动画
    }
    ...
    };
    // 表项动画执行器
    ItemAnimator mItemAnimator = new DefaultItemAnimator();
    // 出现动画
    void animateAppearance(@NonNull ViewHolder itemHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
    itemHolder.setIsRecyclable(false);
    if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
    postAnimationRunner();
    }
    }
    // 消失动画
    void animateDisappearance(@NonNull ViewHolder holder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
    addAnimatingView(holder);
    holder.setIsRecyclable(false);
    if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
    postAnimationRunner();
    }
    }
    }
    复制代码

    RecyclerView 执行表项动画的代码结构如下:


    if (mItemAnimator.animateXXX(holder, preLayoutInfo, postLayoutInfo)) {
    postAnimationRunner();
    }
    复制代码

    根据ItemAnimator.animateXXX()的返回值来决定是否要在下一帧执行动画,以 Demo 中表项 3 的出现动画为例:


    public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
    @Override
    public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
    // 如果预布局和后布局中表项左上角的坐标有变化 则执行位移动画
    if (preLayoutInfo != null
    && (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top)) {
    // 执行位移动画,并传入动画起点坐标(预布局表项左上角坐标)和终点坐标(后布局表项左上角坐标)
    return animateMove(viewHolder,
    preLayoutInfo.left,
    preLayoutInfo.top,
    postLayoutInfo.left,
    postLayoutInfo.top);
    } else {
    return animateAdd(viewHolder);
    }
    }
    }
    复制代码

    之前存储的表项位置信息,终于在这里被用上了,它作为参数传入animateMove(),这是一个定义在SimpleItemAnimator中的抽象方法,DefaultItemAnimator实现了它:


    public class DefaultItemAnimator extends SimpleItemAnimator {
    @Override
    public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY,
    int toX, int toY)
    {
    final View view = holder.itemView;
    fromX += (int) holder.itemView.getTranslationX();
    fromY += (int) holder.itemView.getTranslationY();
    resetAnimation(holder);
    int deltaX = toX - fromX;
    int deltaY = toY - fromY;
    if (deltaX == 0 && deltaY == 0) {
    dispatchMoveFinished(holder);
    return false;
    }
    // 表项水平位移
    if (deltaX != 0) {
    view.setTranslationX(-deltaX);
    }
    // 表项垂直位移
    if (deltaY != 0) {
    view.setTranslationY(-deltaY);
    }
    // 将待移动的表项动画包装成 MoveInfo 并存入 mPendingMoves 列表
    mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
    // 表示在下一帧执行动画
    return true;
    }
    }
    复制代码

    如果水平或垂直方向的位移增量不为 0,则将待移动的表项动画包装成MoveInfo并存入mPendingMoves列表,然后返回 true,表示在下一帧执行动画:


    public class RecyclerView {  
    // 出现动画
    void animateAppearance(ViewHolder itemHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
    itemHolder.setIsRecyclable(false);
    if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
    postAnimationRunner();// 触发动画执行
    }
    }

    // 将动画执行代码抛到 Choreographer 中的动画队列中
    void postAnimationRunner() {
    if (!mPostedAnimatorRunner && mIsAttached) {
    ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
    mPostedAnimatorRunner = true;
    }
    }
    // 动画执行代码
    private Runnable mItemAnimatorRunner = new Runnable() {
    @Override
    public void run() {
    if (mItemAnimator != null) {
    // 在下一帧执行动画
    mItemAnimator.runPendingAnimations();
    }
    mPostedAnimatorRunner = false;
    }
    };
    }
    复制代码

    通过将一个Runnable抛到Choreographer的动画队列中来触发动画执行,当下一个垂直同步信号到来时,Choreographer会从动画队列中获取待执行的Runnable实例,并将其抛到主线程执行(关于Choreographer的详细解析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?)。执行的内容定义在ItemAnimator.runPendingAnimations()中:


    public class DefaultItemAnimator extends SimpleItemAnimator {
    @Override
    public void runPendingAnimations() {
    // 如果位移动画列表不空,则表示有待执行的位移动画
    boolean movesPending = !mPendingMoves.isEmpty();
    // 是否有待执行的删除动画
    boolean removalsPending = !mPendingRemovals.isEmpty();
    ...
    // 处理位移动画
    if (movesPending) {
    final ArrayList moves = new ArrayList<>();
    moves.addAll(mPendingMoves);
    mMovesList.add(moves);
    mPendingMoves.clear();
    Runnable mover = new Runnable() {
    @Override
    public void run() {
    for (MoveInfo moveInfo : moves) {
    // 位移动画具体实现
    animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
    moveInfo.toX, moveInfo.toY);
    }
    moves.clear();
    mMovesList.remove(moves);
    }
    };
    // 若存在删除动画,则延迟执行位移动画,否则立刻执行
    if (removalsPending) {
    View view = moves.get(0).holder.itemView;
    ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
    } else {
    mover.run();
    }
    }
    ...
    }
    }
    复制代码

    遍历mPendingMoves列表,为每一个待执行的位移动画调用animateMoveImpl()构建动画:


    public class DefaultItemAnimator extends SimpleItemAnimator {
    void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
    final View view = holder.itemView;
    final int deltaX = toX - fromX;
    final int deltaY = toY - fromY;
    if (deltaX != 0) {
    view.animate().translationX(0);
    }
    if (deltaY != 0) {
    view.animate().translationY(0);
    }

    // 获取动画实例
    final ViewPropertyAnimator animation = view.animate();
    mMoveAnimations.add(holder);
    // 设置动画参数并启动
    animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationStart(Animator animator) {
    dispatchMoveStarting(holder);
    }

    @Override
    public void onAnimationCancel(Animator animator) {
    if (deltaX != 0) {
    view.setTranslationX(0);
    }
    if (deltaY != 0) {
    view.setTranslationY(0);
    }
    }

    @Override
    public void onAnimationEnd(Animator animator) {
    animation.setListener(null);
    dispatchMoveFinished(holder);
    mMoveAnimations.remove(holder);
    dispatchFinishedWhenDone();
    }
    }).start();
    }
    }
    复制代码

    原来默认的表项动画是通过ViewPropertyAnimator实现的。


    总结



    1. RecyclerView 将表项动画数据封装了两层,依次是ItemHolderInfoInfoRecord,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。

    2. InfoRecord被集中存放在一个商店类ViewInfoStore中。所有参与动画的表项的ViewHolderInfoRecord都会以键值对的形式存储其中。

    3. RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以InfoRecord中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator,以触发动画。

    4. RecyclerView.ItemAnimator收到动画指令和数据后,又将他们封装为MoveInfo,不同类型的动画被存储在不同的MoveInfo列表中。然后将执行动画的逻辑抛到 Choreographer 的动画队列中,当下一个垂直同步信号到来时,Choreographer 从动画队列中取出并执行表项动画,执行动画即遍历所有的MoveInfo列表,为每一个MoveInfo构建 ViewPropertyAnimator 实例并启动动画。

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

    RecyclerView 动画原理 | 如何存储并应用动画属性值?(1)

    RecyclerView 表项动画的属性值是怎么获取的,又存储在哪里?这一篇继续通过 走查源码 的方式解答这个疑问。 通过上两篇的分析得知,为了做动画 RecyclerView 会布局两次:预布局+后布局,依次将动画前与动画后的表项填充到列表。表项被填充后,就...
    继续阅读 »

    RecyclerView 表项动画的属性值是怎么获取的,又存储在哪里?这一篇继续通过 走查源码 的方式解答这个疑问。


    通过上两篇的分析得知,为了做动画 RecyclerView 会布局两次:预布局+后布局,依次将动画前与动画后的表项填充到列表。表项被填充后,就确定了它相对于 RecyclerView 左上角的位置,在两次布局过程中,这些位置信息是如何被保存的?


    引子


    这一篇源码分析还是基于下面这个 Demo 场景:



    列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。


    为了实现该效果,RecyclerView的策略是:为动画前的表项先执行一次pre-layout,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。


    在此援引上一篇已经得出的结论:





    1. RecyclerView为了实现表项动画,进行了 2 次布局(预布局 + 后布局),在源码上表现为LayoutManager.onLayoutChildren()被调用 2 次。




    2. 预布局的过程始于RecyclerView.dispatchLayoutStep1(),终于RecyclerView.dispatchLayoutStep2()




    3. 在预布局阶段,循环填充表项时,若遇到被移除的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。





    其中第三点表现在源码上,是这样的:


    public class LinearLayoutManager {
    // 布局表项
    public void onLayoutChildren() {
    // 不断填充表项
    fill() {
    while(列表有剩余空间){
    // 填充单个表项
    layoutChunk(){
    // 让表项成为子视图
    addView(view)
    }
    if (表项没有被移除) {
    剩余空间 -= 表项占用空间
    }
    ...
    }
    }
    }
    }
    复制代码

    这是RecyclerView填充表项的伪码。以 Demo 为例,预布局阶段,第一次执行onLayoutChildren(),因表项 2 被删除,所以它占用的空间不会被扣除,导致while循环多执行一次,这样表项 3 就被填充进列表。


    后布局阶段,会再次执行onLayoutChildren(),再把表项 1、3 填入列表。那此时列表中不是得有两个表项 1,两个表项 3,和一个表项 2 吗?


    这显然是不可能的,用上一篇介绍的断点调试,运行 Demo,把断点断在addView(),发现后布局阶段再次调用该方法时,RecyclerView的子控件个数为 0。


    先清空表项再填充


    难道每次布局之前都会删掉现有布局中所有的表项?


    fill()开始,往上走查代码,果然发现了一个线索:


    public class LinearLayoutManager {
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    // detach 并 scrap 表项
    detachAndScrapAttachedViews(recycler);
    ...
    // 填充表项
    fill()
    }
    复制代码

    在填充表项之前,有一个 detach 操作:


    public class RecyclerView {
    public abstract static class LayoutManager {
    public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    // 遍历所有子表项
    final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
    final View v = getChildAt(i);
    // 回收子表项
    scrapOrRecycleView(recycler, i, v);
    }
    }
    }
    }
    复制代码

    果不其然,在填充表项之前会遍历所有子表项,并逐个回收它们:


    public class RecyclerView {
    public abstract static class LayoutManager {
    // 回收表项
    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()&& !mRecyclerView.mAdapter.hasStableIds()) {
    removeViewAt(index);
    recycler.recycleViewHolderInternal(viewHolder);
    } else {
    // detach 表项
    detachViewAt(index);
    // scrap 表项
    recycler.scrapView(view);
    ...
    }
    }
    }
    }
    复制代码

    回收表项时,根据viewHolder的不同状态执行不同分支。硬看源码很难快速判断会走哪个分支,果断运行 Demo,断点调试一把。在上述场景中,所有表项都走了第二个分支,即在布局表项之前,对现有表项做了两个关键的操作:



    1. detach 表项detachViewAt(index)

    2. scrap 表项recycler.scrapView(view)


    detach 表项


    先看看 detach 表项是个什么操作:


    public class RecyclerView {
    public abstract static class LayoutManager {
    ChildHelper mChildHelper;
    // detach 指定索引的表项
    public void detachViewAt(int index) {
    detachViewInternal(index, getChildAt(index));
    }

    // detach 指定索引的表项
    private void detachViewInternal(int index, @NonNull View view) {
    ...
    // 将 detach 委托给 ChildHelper
    mChildHelper.detachViewFromParent(index);
    }
    }
    }

    // RecyclerView 子表项管理类
    class ChildHelper {
    // 将指定位置的表项从 RecyclerView detach
    void detachViewFromParent(int index) {
    final int offset = getOffset(index);
    mBucket.remove(offset);
    // 最终实现 detach 操作的回调
    mCallback.detachViewFromParent(offset);
    }
    }
    复制代码

    LayoutManager会将 detach 任务委托给ChildHelperChildHelper再执行detachViewFromParent()回调,它在初始化ChildHelper时被实现:


    public class RecyclerView {
    // 初始化 ChildHelper
    private void initChildrenHelper() {
    // 构建 ChildHelper 实例
    mChildHelper = new ChildHelper(new ChildHelper.Callback() {
    @Override
    public void detachViewFromParent(int offset) {
    final View view = getChildAt(offset);
    ...
    // 调用 ViewGroup.detachViewFromParent()
    RecyclerView.this.detachViewFromParent(offset);
    }
    ...
    }
    }
    }
    复制代码

    RecyclerView detach 表项的最后一步调用了ViewGroup.detachViewFromParent()


    public abstract class ViewGroup {
    // detach 子控件
    protected void detachViewFromParent(int index) {
    removeFromArray(index);
    }

    // 删除子控件的最后一步
    private void removeFromArray(int index) {
    final View[] children = mChildren;
    // 将子控件持有的父控件引用置空
    if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
    children[index].mParent = null;
    }
    final int count = mChildrenCount;
    // 将父控件持有的子控件引用置空
    if (index == count - 1) {
    children[--mChildrenCount] = null;
    } else if (index >= 0 && index < count) {
    System.arraycopy(children, index + 1, children, index, count - index - 1);
    children[--mChildrenCount] = null;
    }
    ...
    }
    }
    复制代码

    ViewGroup.removeFromArray()是容器控件移除子控件的最后一步(ViewGroup.removeView()也会调用这个方法)。


    至此可以得出结论:



    在每次向RecyclerView填充表项之前都会先清空现存表项。



    目前看来,detach viewremove view差不多,它们都会将子控件从父控件的孩子列表中删除,唯一的区别是detach更轻量,不会触发重绘。而且detach是短暂的,被detach的 View 最终必须被彻底 remove 或者重新 attach。(下面就会马上把他们重新 attach)


    scrap 表项


    scrap 表项的意思是回收表项并将其存入mAttachedScrap列表,它是回收器Recycler中的成员变量:


    public class RecyclerView {
    public final class Recycler {
    // scrap 列表
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    }
    }
    复制代码

    mAttachedScrap是一个 ArrayList 结构,用于存储ViewHolder实例。


    RecyclerView 填充表项前,除了会 detach 所有可见表项外,还会同时 scrap 它们:


    public class RecyclerView {
    public abstract static class LayoutManager {
    // 回收表项
    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    ...
    // detach 表项
    detachViewAt(index);
    // scrap 表项
    recycler.scrapView(view);
    ...
    }
    }
    }
    复制代码

    scrapView()是回收器Recycler的方法,正是这个方法将表项回收到了mAttachedScrap列表中:


    public class RecyclerView {
    public final class Recycler {
    void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    // 表项不需要更新,或被移除,或者表项索引无效时,将被会收到 mAttachedScrap
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
    holder.setScrapContainer(this, false);
    // 将表项回收到 mAttachedScrap 结构中
    mAttachedScrap.add(holder);
    } else {
    // 只有当表项没有被移除且有效且需要更新时才会被回收到 mChangedScrap
    if (mChangedScrap == null) {
    mChangedScrap = new ArrayList<ViewHolder>();
    }
    holder.setScrapContainer(this, true);
    mChangedScrap.add(holder);
    }
    }
    }
    }
    复制代码

    scrapView()中根据ViewHolder状态将其会收到不同的结构中,同样地,硬看源码很难快速判断执行了那个分支,继续断点调试,Demo 场景中所有的表项都会被回收到mAttachedScrap结构中。(关于 mAttachedScrap 和 mChangedScrap 的区别会在后续文章分析)


    分析至此,进一步细化刚才得到的结论:



    在每次向RecyclerView填充表项之前都会先清空 LayoutManager 中现存表项,将它们 detach 并同时缓存入 mAttachedScrap列表中。



    将结论应用在 Demo 的场景,即是:RecyclerView 在预布局阶段准备向列表中填充表项前,会清空现有的表项 1、2,把它们都 detach 并回收对应的 ViewHolder 到 mAttachedScrap列表中。


    从缓存拿填充表项


    预布局与 scrap 缓存的关系


    缓存定是为了复用,啥时候用呢?紧接着的“填充表项”中就立马会用到:


    public class LinearLayoutManager {
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    // detach 表项
    detachAndScrapAttachedViews(recycler);
    ...
    // 填充表项
    fill()
    }

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
    // 计算剩余空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 不停的往列表中填充表项,直到没有剩余空间
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
    // 填充单个表项
    layoutChunk(recycler, state, layoutState, layoutChunkResult);
    ...
    }
    }

    // 填充单个表项
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
    // 获取下一个被填充的视图
    View view = layoutState.next(recycler);
    ...
    // 填充视图
    addView(view);
    ...
    }
    }
    复制代码

    填充表项时,通过layoutState.next(recycler)获取下一个该被填充的表项视图:


    public class LinearLayoutManager {
    static class LayoutState {
    View next(RecyclerView.Recycler recycler) {
    ...
    // 委托 Recycler 获取下一个该填充的表项
    final View view = recycler.getViewForPosition(mCurrentPosition);
    ...
    return view;
    }
    }
    }

    public class RecyclerView {
    public final class Recycler {
    public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
    }
    }

    View getViewForPosition(int position, boolean dryRun) {
    // 调用链最终传递到 tryGetViewHolderForPositionByDeadline()
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    }
    复制代码

    沿着调用链一直往下,最终走到了Recycler.tryGetViewHolderForPositionByDeadline(),在RecyclerView缓存机制(咋复用?)中对其做过详细介绍,援引结论如下:



    1. 在 RecyclerView 中,并不是每次绘制表项,都会重新创建 ViewHolder 对象,也不是每次都会重新绑定 ViewHolder 数据。

    2. RecyclerView 填充表项前,会通过Recycler获取表项的 ViewHolder 实例。

    3. RecyclertryGetViewHolderForPositionByDeadline()方法中,前后尝试 5 次,从不同缓存中获取可复用的 ViewHolder 实例,其中第一优先级的缓存即是scrap结构。

    4. scrap缓存获取的表项不需要重新构建,也不需要重新绑定数据。


    从 scrap 结构获取 ViewHolder 的源码如下:


    public class RecyclerView {
    public final class Recycler {
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
    ViewHolder holder = null;
    ...
    // 从 scrap 结构中获取指定 position 的 ViewHolder 实例
    if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    ...
    }
    ...
    }

    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();
    // 遍历 mAttachedScrap 列表中所有的 ViewHolder 实例
    for (int i = 0; i < scrapCount; i++) {
    final ViewHolder holder = mAttachedScrap.get(i);
    // 校验 ViewHolder 是否满足条件,若满足,则缓存命中
    if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
    && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
    }
    }
    ...
    }
    }
    }
    复制代码

    mAttachedScrap列表中获取的ViewHolder实例后,得进行校验。校验的内容很多,其中最重要的的是:ViewHolder索引值和当前填充表项的位置值是否相等,即:


    scrap 结构缓存的 ViewHolder 实例,只能复用于和它回收时相同位置的表项。


    也就是说,若当前列表正准备填充 Demo 中的表项 2(position == 1),即使 scrap 结构中有相同类型 ViewHolder,只要viewHolder.getLayoutPosition()的值不为 1,缓存不会命中。


    分析至此,可以把上面得到的结论进一步拓展:



    在每次向RecyclerView填充表项之前都会先清空 LayoutManager 中现存表项,将它们 detach 并同时缓存入 mAttachedScrap列表中。在紧接着的填充表项阶段,就立马从mAttachedScrap中取出刚被 detach 的表项并重新 attach 它们。



    (弱弱地问一句,这样折腾意义何在?可能接着往下看就知道了。。)


    将结论应用在 Demo 的场景,即是:RecyclerView 在预布局阶段准备向列表中填充表项前,会清空现有的表项 1、2,把它们都 detach 并回收对应的 ViewHolder 到 mAttachedScrap 列表中。然后又在填充表项阶段从 mAttachedScrap 中重新获取了表项 1、2 并填入列表。


    上一篇的结论说“Demo 场景中,预布局阶段还会额外加载列表第三个位置的表项 3”,但mAttachedScrap只缓存了表项 1、2。所以在填充表项 3 时,scrap 缓存未命中。不仅如此,因表项 3 是从未被加载过的表项,遂所有的缓存都不会命中,最后只能沦落到重新构建表项并绑定数据


    public class RecyclerView {
    public final class Recycler {
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
    if (holder == null) {
    ...
    // 构建 ViewHolder
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    ...
    }
    // 获取表项偏移的位置
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    // 绑定 ViewHolder 数据
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    }
    }
    }
    复制代码

    沿着上述代码的调用链往下走查,就能找到熟悉的onCreateViewHolder()onBindViewHolder()


    在绑定 ViewHolder 数据之前,先调用了mAdapterHelper.findPositionOffset(position)获取了“偏移位置”。断点调试告诉我,此时它会返回 1,即表项 2 被移除后,表项 3 在列表中的位置。


    AdapterHelper将所有对表项的操作都抽象成UpdateOp并保存在列表中,当获取表项 3 偏移位置时,它发现有一个表项 2 的删除操作,所以表项 3 的位置会 -1。(有关 AdapterHelper 的内容就不展开了~)


    至此,预布局阶段的填充表项结束了,LayoutManager 中现有表项 1、2、3,形成了第一张快照(1,2,3)。


    后布局与 scrap 缓存的关系


    再次援引上一篇的结论:




    1. RecyclerView 为了实现表项动画,进行了 2 次布局,第一次预布局,第二次后布局,在源码上表现为 LayoutManager.onLayoutChildren() 被调用 2 次。




    2. 预布局的过程始于 RecyclerView.dispatchLayoutStep1(),终于 RecyclerView.dispatchLayoutStep2()。




    在紧接着执行的dispatchLayoutStep2()中,开始了后布局


    public class RecyclerView {
    void dispatchLayout() {
    ...
    dispatchLayoutStep1();// 预布局
    mLayout.setExactMeasureSpecsFrom(this);
    dispatchLayoutStep2();// 后布局
    ...
    }

    private void dispatchLayoutStep2() {
    mState.mInPreLayout = false;// 预布局结束
    mLayout.onLayoutChildren(mRecycler, mState); // 第二次 onLayoutChildren()
    }
    复制代码

    布局子表项的老花样要再来一遍,即先 detach 并 scrap 现有表项,然后再填充。


    但这次会有一些不同:



    1. 因为 LayoutManager 中现有表项 1、2、3,所以 scrap 完成后,mAttachedScrap中存有表项1、2、3 的 ViewHolder 实例(position 依次为 0、0、1,被移除表项的 position 会被置 0)。

    2. 因为第二次执行onLayoutChildren()已不属于预布局阶段,所以不会加载额外的表项,即LinearLayoutManager.layoutChunk()只会执行 2 次,分别填充位置为 0 和 1 的表项。

    3. mAttachedScrap缓存的 ViewHolder 中,有 2 个 position 为 0,1 个 position 为 1。毫无疑问,填充列表位置 1 的表项时,表项 3 必会命中(因为 position 相等)。但填充列表位置 0 的表项时,是表项 1 还是 表项 2 命中?(它们的 position 都为 0)再回看一遍,缓存命中前的校验逻辑:


    public class RecyclerView {
    public final class Recycler {
    // 从 缓存中获取 ViewHolder 实例
    ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();
    // 遍历 mAttachedScrap
    for (int i = 0; i < scrapCount; i++) {
    final ViewHolder holder = mAttachedScrap.get(i);
    if (!holder.wasReturnedFromScrap()
    && holder.getLayoutPosition() == position // 位置相等
    && !holder.isInvalid()
    && (mState.mInPreLayout || !holder.isRemoved()) // 在预布局阶段 或 表项未被移除
    ) {
    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
    return holder;
    }
    }
    }
    }
    }
    复制代码

    当遍历到mAttachedScrap的表项 2 时,虽然它的位置满足了要求,但校验的最后一个条件把它排除了,因为现在已经不再是预布局阶段,且表项 2 是被移除的。所以列表的位置 0 只能被剩下的表项 1 填充。


    分别用表项 1、3 填充了列表的位置 0、1 ,后布局的填充表项也结束了。


    此时就形成第二张快照(1,3),和预布局形成的快照(1,2,3)比对之后,就知道表项 2 需要做消失动画,而表项 3 需要做移入动画。那动画具体是怎么实现的?限于篇幅,下次再析。


    总结


    回到篇中的那个问题:“何必这样折腾?即先 detach 并 缓存表项到 scrap 结构中,然后紧接着又在填充表项时从中取出?”


    因为 RecyclerView 要做表项动画,


    为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”,


    为了获得两张快照,就得布局两次,分别是预布局和后布局(布局即是往列表中填充表项),


    为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),


    但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),


    RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在 scrap 结构中,以便在填充表项可以命中缓存,以缩短填充表项耗时。



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

    RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?(2)

    RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?(1)4. mCachedViews 中缓存的表项被删除 表项移出屏幕后,立刻被回收到mCachedViews结构中。若恰巧该表项又被删除了,则表项对应的 ViewHolder 从mCach...
    继续阅读 »

    RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?(1)


    4. mCachedViews 中缓存的表项被删除


    表项移出屏幕后,立刻被回收到mCachedViews结构中。若恰巧该表项又被删除了,则表项对应的 ViewHolder 从mCachedViews结构中移除,并添加到缓存池中:


    public class RecyclerView {
    public final class Recycler {
    void recycleCachedViewAt(int cachedViewIndex) {
    // 从 mCacheViews 结构中获取指定位置的 ViewHolder 实例
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    // 将 ViewHolder 存入缓存池
    addViewHolderToRecycledViewPool(viewHolder, true);
    // 将 ViewHolder 从 mCacheViews 中移除
    mCachedViews.remove(cachedViewIndex);
    }

    void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
    ...
    getRecycledViewPool().putRecycledView(holder);
    }
    }
    }
    复制代码

    5. pre-layout 中额外填充的表项在 post-layout 中被移除


    pre-layout & post-layout


    pre-layoutpost-layoutRecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系有介绍过,援引如下:



    RecyclerView 要做表项动画,


    为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”


    为了获得两张快照,就得布局两次,分别是 pre-layout 和 post-layout(布局即是往列表中填充表项),


    为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),


    但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),


    RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在 scrap 缓存中,以便在填充表项可以命中缓存,以缩短填充表项耗时。




    Gif 的场景中,在 pre-layout 阶段,item 1、item 2、item 3 被填充到列表中,形成一张动画前的表项快照。而 post-layout 将 item 1、item 3 填充到列表中,形成一张动画后的表项快照。


    对比这两张快照中的 item 3 的位置就能知道它该从哪里平移到哪里,也知道 item 2 需要做消失动画,当动画结束后,item 2 的 ViewHolder 会被回收到缓存池,回收的调用链和“表项被挤出屏幕”是一样的,都是由动画结束来触发的。


    在 pre-layout 阶段填充额外表项


    考虑另外一种场景,这次不是移除 item 2,而是更新它,比如把 item 2 更新成 item 2.1,那 pre-layout 还会将 item 3 填充进列表吗?


    RecyclerView 动画原理 | 换个姿势看源码(pre-layout) 详细分析了,在 pre-layout 阶段,额外的表项是如何被填充到列表,其中关键源码再拿出来看一下:


    public class LinearLayoutManager{
    // 向列表中填充表项
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    ...
    // 计算剩余空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 循环填充表项,直到没有剩余空间
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
    layoutChunkResult.resetInternal();
    // 填充单个表项
    layoutChunk(recycler, state, layoutState, layoutChunkResult);
    ...
    // 在列表剩余空间中扣除刚填充表项所消耗的空间
    if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) {
    layoutState.mAvailable -= layoutChunkResult.mConsumed;
    remainingSpace -= layoutChunkResult.mConsumed;
    }
    ...
    }
    ...
    }
    }
    复制代码

    直觉上,每填充一个表项都应该将其消耗的空间扣除,但扣除逻辑套在了一个 if 中,即扣除是有条件的。


    条件表达式中一共有三个条件,在预布局阶段!state.isPreLayout()必然是 false,layoutState.mScrapList != null也是 false(断点告诉我的),最后一个条件!layoutChunkResult.mIgnoreConsumed起了决定性的作用,它在填充单个表项时被赋值:


    public class LinearLayoutManager {
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
    // 获取下一个该被填充的表项视图
    View view = layoutState.next(recycler);
    ...// 省略了实施填充的具体逻辑
    // 如果表项被移除或被更新 则 mIgnoreConsumed 置为 true
    if (params.isItemRemoved() || params.isItemChanged()) {
    result.mIgnoreConsumed = true;
    }
    ...
    }
    }
    复制代码

    layoutChunkResult被作为参数传入layoutChunk(),并且当填充表项是被删除的或是被更新的,就将layoutChunkResult.mIgnoreConsumed置为 true。表示该表项虽然被填充进了列表但是它占用的空间应该呗忽略。至此可以得出结论:



    在预布局阶段,循环填充表项时,若遇到被移除的或是被更新的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。



    虽然这结论就是代码的本意,但还是有一点让我不太明白。忽略被移除表项占用的空间容易理解,那为啥更新的表项也一同被忽略?


    那是因为,更新表项时,表项的布局可能发生变化(取决于onBindViewHolder()的实现),万一表项布局变长,则会造成其他表项被挤出屏幕,或是表项变短,造成新表项移入屏幕。


    记录表项动画信息


    RecyclerView 动画原理 | 如何存储并应用动画属性值?中介绍了 RecyclerView 是如何存储动画属性值的,现援引如下:





    1. RecyclerView 将表项动画数据封装了两层,依次是ItemHolderInfoInfoRecord,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。




    2. InfoRecord被集中存放在一个商店类ViewInfoStore中。所有参与动画的表项的ViewHolderInfoRecord都会以键值对的形式存储其中。




    3. RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以InfoRecord中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator,以触发动画。





    在 pre-layout 阶段,存储动画信息的代码如下:


    public class RecyclerView {
    private void dispatchLayoutStep1() {
    ...
    // 遍历列表中现有表项
    int count = mChildHelper.getChildCount();
    for (int i = 0; i < count; ++i) {
    final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
    // 为表项构建 ItemHolderInfo 实例
    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),holder.getUnmodifiedPayloads());
    // 将 ItemHolderInfo 实例存入 ViewInfoStore
    mViewInfoStore.addToPreLayout(holder, animationInfo);
    }
    ...
    // 预布局
    mLayout.onLayoutChildren(mRecycler, mState);
    // 预布局后,再次遍历所有孩子(预布局可能填充额外的表项)
    for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
    final View child = mChildHelper.getChildAt(i);
    final ViewHolder viewHolder = getChildViewHolderInt(child);
    // 过滤掉带有 FLAG_PRE 标志位的表项
    if (!mViewInfoStore.isInPreLayout(viewHolder)) {
    // 为额外填充的表项构建 ItemHolderInfo 实例
    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
    // 将 ItemHolderInfo 实例存入 ViewInfoStore
    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
    }
    }
    ...
    }
    }

    class ViewInfoStore {
    void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
    record = InfoRecord.obtain();
    mLayoutHolderMap.put(holder, record);
    }
    record.preInfo = info;
    // 添加 FLAG_PRE 标志位
    record.flags |= FLAG_PRE;
    }

    void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
    record = InfoRecord.obtain();
    mLayoutHolderMap.put(holder, record);
    }
    // 添加 FLAG_APPEAR 标志位
    record.flags |= FLAG_APPEAR;
    record.preInfo = info;
    }
    }
    复制代码

    在 pre-layout 的前后,遍历了两次表项。


    对于 Demo 的场景来说,第一次遍历,item 1 和 2 的动画属性被存入 ViewInfoStore 并添加了FLAG_PRE标志位。遍历结束后执行预布局,把屏幕之外的 item 3 也填充到列表中。再紧接着的第二次遍历中,item 3 的动画属性也会被存入 ViewInfoStore 并添加了FLAG_APPEAR标志位,表示该表项是在预布局过程中额外被填充的。


    在 post-layout 阶段,为了形成动画后的表项快照,得清空列表,重新填充表项,出于时间性能的考虑,被移除表项的 ViewHolder 缓存到了 scrap 结构中(item 1 2 3的 ViewHodler 实例)。


    重新向列表中填充 item 1 和更新后的 item 2,它们的 ViewHolder 实例可以从 scrap 结构中快速获取,不必再执行 onCreateViewHolder()。填充完后,列表的空间已经用完,而 scrap 结构中还剩一个 item 3 的 ViewHolder 实例。它会在 post-layout 阶段被添加新的标志位:


    public class LinearLayoutManager {
    // 在 dispatchLayoutStep2() 中第二次调用 onLayoutChildren() 进行 post-layout
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    // 为动画而进行布局
    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    }

    private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,RecyclerView.State state, int startOffset,int endOffset) {
    final List scrapList = recycler.getScrapList();
    final int scrapSize = scrapList.size();
    // 遍历 scrap 结构
    for (int i = 0; i < scrapSize; i++) {
    RecyclerView.ViewHolder scrap = scrapList.get(i);
    final int position = scrap.getLayoutPosition();
    final int direction = position < firstChildPos != mShouldReverseLayout? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
    // 计算 scrap 结构中对应表项所占用的空间
    if (direction == LayoutState.LAYOUT_START) {
    scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
    } else {
    scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
    }
    }
    // mLayoutState.mScrapList 被赋值
    mLayoutState.mScrapList = scrapList;
    // 再次尝试填充表项
    if (scrapExtraStart > 0) {
    ...
    fill(recycler, mLayoutState, state, false);
    }

    if (scrapExtraEnd > 0) {
    ...
    fill(recycler, mLayoutState, state, false);
    }
    mLayoutState.mScrapList = null;
    }

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    // 分支1:把表项填充到列表中
    if (layoutState.mScrapList == null) {
    if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
    addView(view);
    } else {
    addView(view, 0);
    }
    }
    // 分支2:把表项动画信息存储到 ViewInfoStore 中
    else {
    if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
    // 委托给父类 LayoutManger
    addDisappearingView(view);
    } else {
    addDisappearingView(view, 0);
    }
    }
    ...
    }
    }
    复制代码

    这次填充表项的layoutChunk()因为layoutState.mScrapList不为空,会走不一样的分支,即调用addDisappearingView()


    public class RecyclerView {
    public abstract static class LayoutManager {
    public void addDisappearingView(View child) {
    addDisappearingView(child, -1);
    }

    public void addDisappearingView(View child, int index) {
    addViewInt(child, index, true);
    }

    private void addViewInt(View child, int index, boolean disappearing) {
    final ViewHolder holder = getChildViewHolderInt(child);
    if (disappearing || holder.isRemoved()) {
    // 置 FLAG_DISAPPEARED 标志位
    mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
    } else {
    mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
    }
    ...
    }
    }
    }

    class ViewInfoStore {
    // 置 FLAG_DISAPPEARED 标志位
    void addToDisappearedInLayout(RecyclerView.ViewHolder holder) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
    record = InfoRecord.obtain();
    mLayoutHolderMap.put(holder, record);
    }
    record.flags |= FLAG_DISAPPEARED;
    }

    复制代码

    至此 item 3 在经历了 pre-layout 和 post-layout 后,它的动画信息被存储在ViewInfoStore中,且添加了两个标志位,分别是FLAG_APPEARFLAG_DISAPPEARED


    在布局的第三阶段,会调用ViewInfoStore.process()触发动画:


    public class RecyclerView {
    private void dispatchLayoutStep3() {
    ...
    // 触发表项执行动画
    mViewInfoStore.process(mViewInfoProcessCallback);
    ...
    }
    }

    class ViewInfoStore {
    void process(ProcessCallback callback) {
    // 遍历所有参与动画表项的位置信息
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
    // 获取表项 ViewHolder
    final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
    // 获取与 ViewHolder 对应的动画信息
    final InfoRecord record = mLayoutHolderMap.removeAt(index);
    // 根据动画信息的标志位确定动画类型以执行对应的 ProcessCallback 回调
    if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
    callback.unused(viewHolder);
    } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
    ...
    }
    }
    }
    }
    复制代码

    Demo 中的 item 3 会命中第一个 if 条件,因为:


    class ViewInfoStore {
    static class InfoRecord {
    // 在 post-layout 中消失
    static final int FLAG_DISAPPEARED = 1;
    // 在 pre-layout 中出现
    static final int FLAG_APPEAR = 1 << 1;
    // 上两者的合体
    static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
    }
    }
    复制代码

    回收 item 3 到缓存池的逻辑就在callback.unused(viewHolder)中:


    public class RecyclerView {
    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = new ViewInfoStore.ProcessCallback() {
    ...
    @Override
    public void unused(ViewHolder viewHolder) {
    // 回收没有用的表项
    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
    }
    };

    public abstract static class LayoutManager {
    public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) {
    removeView(child);
    // 委托给 Recycler
    recycler.recycleView(child);
    }
    }

    public final class Recycler {
    public void recycleView(@NonNull View view) {
    // 回收表项到缓存池
    recycleViewHolderInternal()
    }
    }
    }
    复制代码

    至此可以得出结论:



    所有在 pre-layout 阶段被额外填充的表项,若最终没能在 post-layout 阶段也填充到列表中,就都会被回到到缓存池。


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