
从事Android开发6年,是一名为自己而活的码农!
前言 教育背景: 16届国防科技大学软件工程专业毕业主修:Android移动应用开发辅修:JAVA开发,C/C++等(数据库、jsp、数据结构与算法那些编程专业都要学的课程就不一一列举了)时间过得飞快,眨眼间又是一年毕业季了,今年情况比较特殊,疫情的影响导致各行业的工作都不好找。在这种严峻的形势下,怨天尤人,乃庸人做法;学习进阶、提高自身核心竞争力,才是上上之策! 每个人都拥有大厂梦,我也不例外,学习Android开发的初衷,便是进大厂。在众多互联网大厂中,最终选择了阿里妈妈。“年轻、活力、富有激情”是我听到得最多对它的形容词,所以毅然决然,投递了简历。希望自己能够在这个最大的舞台上施展拳脚。 以下分享这次面试蚂蚁金服的面试题,另外还对自己的面试做了一些总结,总结里含有一些学习方法和资料,需要的朋友可以直接(点击我)无偿分享! 蚂蚁金服四面斩获Offer,定级P5 一面(五十分钟左右)一面是一个电话面试,下午2点左右特地找了一个地方电话面试,2点准时电话就过来了,守时这方面还是专业的,时间有点远了,题目大致如下。。。1.自我介绍 2.基本学习经历教育背景。 3.有没有实习经历?我回答:有两家家小公司共7个月的实习经验。然后会顺着往下问为什么没有留下,做过哪些项目,学到了什么等等 4.问到了java 中==和 equals 和 hashCode 的区别(这个题感觉面试必问,好多面经都有这个题) 5.进程和线程的区别 6.什么是 ANR 如何避免它? 7.点击应用图标,打开应用的过程 8.activity周期,启动模式 9.Handler机制 一些比较基础的东西,对于书本的消化,基本上没有什么难度 二面(70分钟左右吧)二面是视频面试,是个周六下午1、HTTP & HTTPS区别(基本都要问的) 2、socket.accept()函数对应着TCP三次握手中哪一次握手,哪个函数代表连接建立完毕,四次挥手的过程?(这个也基本都问) 3、举例工作中用到的多线程的应用场景,线程同步的问题 4、图片加载框架?如何加载100M的图片却不撑爆内存?(这题网上我都看吐了) 5、Android 中进程的优先级? 6、自定义View、View 的绘制流程、MotionEvent 是什么?包含几种事件?什么条件下会产生? 7、说下你所知道的设计模式与使用场景 三面过了整整一周到了隔周五上午,接到了第三面的面试通知,约的是隔周三上午 10:30 。第三面面试官时不时的面带笑容,给人很轻松的感觉。1、Looper消息机制,postDelay的Message怎么处理,Looper中的消息是同步还是异步?什么情况下会有异步消息 2、对MVC和MVP的理解,Handler的内存泄漏问题具体是什么,解决方案有什么,空数据的时候Handler的阻塞问题 3、ListView和RecyclerView区别? 4、Handler机制?子线程可以用Handler吗? 5、HashMap原理 6、你在项目中碰到什么比较棘手的问题?怎么解决的? 7、找到一个无序数组中第一次出现最多次数的元素 8、问到framework,线程同步这些。 9、反问环节(一般都会有这个环节,个人建议如下)问技术面试官:问问技术团队多少人、技术氛围怎么样的、如果有可能问问他们的技术栈是什么,围绕着技术来问 问HR:转正的考核标准啊,薪资待遇啊,公司现有规模啊、发展情况啊,表现出对这个公司很感兴趣就行了 注意一点:考虑好你得目标公司的规模和行业。小微公司:不看项目随便问,没有逻辑性,闭了眼睛瞎问,对于这种会就会,不会就不会,别怂,反正也不想去。稍微大点的厂:一般是围绕项目去问,然后衍生出一些技术问题来细问,问的比较深的时候别慌,把你的理解说出来,然后说其他的就不知道了,体现出你的思考和应变能力。基本上面试就这些的,剩下的就是尽人事听天命了,记住,面试不上有时候不是你得问题,很可能是公司其实不急着找人,或者面试官不行。四面(HR)1、自我介绍。 2、自身有什么优势?什么劣势? 3、其他公司的面试体验怎么样? 4、学校参加过哪些活动、组织者or参与者 5、薪资预期,理想的工作模式等等 阿里巴巴面试题答案:点击【答案】即可领取! 学习经验总结 (一)调整好心态心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。 (二)时间挤一挤,制定好计划一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。 (三)不断学习技术知识,更新自己的知识储备对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。 结合自身的一个学习经历,总结了一套非常系统的复习包,包括思维脑图、Android基础知识、JAVA知识点汇总、Android扩展知识点、Android开源库源码分析、设计模式汇总、Gradle知识点汇总、常见面试算法题汇总等等。 1、Android基础知识:笔记里的知识点非常齐全,囊括了Activity、数据储存、屏幕适配、消息机制、线程异步、webview、进程、ipc、数据储存等大量知识点,每一个知识点都有非常详细的解析,这本万能宝典在手,不信还有搞不懂的面试题! 2、JAVA知识点汇总:笔记里的知识点非常齐全,囊括了JVM、static、并发、Java反射、Spring原理、微服务、异常处理、数据库、数据结构等大量知识点,每一个知识点都有非常详细的解析,这本万能宝典在手,不信还有搞不懂的面试题! 3、手撕架构技术篇该篇内容囊括了以下专题的高频面试题、实战文档以及使用总结。 4、 最新大厂面试专题这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等 5、 实战电子书关于实战,我想每一个做开发的都有话要说,对于小白而言,缺乏实战经验是通病,那么除了在实际工作过程当中,我们如何去更了解实战方面的内容呢?实际上,我们很有必要去看一些实战相关的电子书。目前,我手头上整理到的电子书还算比较全面,HTTP、自定义view、c++、MVP、Android源码设计模式、Android开发艺术探索、Java并发编程的艺术、Android基于Glide的二次封装、Android内存优化——常见内存泄露及优化方案、.Java编程思想 (第4版)等高级技术都囊括其中。 6、Android小白到Android工程师的系统学习视频关于视频这块,我也是自己搜集了一些,都按照Android学习路线做了一个分类。按照Android学习路线一共有八个模块,其中视频都有对应,就是为了帮助大家系统的学习。接下来看一下导图和对应系统视频吧!!! Android高级工程师进阶思维导图 对应导图的Android高级工程师进阶系统学习视频 写在最后我已经顺利拿到了offer,大家也要加油,希望都能找到自己想要的工作! 给大家一些建议 1.遇到问题,不要没有进行仔细分析,就直接百度和谷歌2.学习知识的时候,不要没有自己的思考和理解,死记硬背3.要记得深入追究一个问题的本质原因4.一定要有自己知识点总结和梳理5.学到知识点,没有很好的实践,动手能力不够6.要自己的学习的方法7.确定目标和找到有效的学习方法 最后提醒:以上整理的所有PDF,均可以免费分享,有需要的朋友, 直接点击【 我要成为Android高级工程师 】加入我们的圈子领取资料,和我们一起学习交流吧!
前言 都说金三银四是找工作的好时机,但是今年却不同,因为受疫情的影响很多公司都在裁员,而我就是被裁的一员,本以为是金三银四好找工作,可是连续投了十几份简历面试了十几次都没有下文。 当我面试十几次都没下文时,我停止了继续找投简历,开始静下心来寻找面试失败的原因,于是我闭关了60天把面试遇到的问题和不牢固的知识点全都复习了一遍。‘’皇天不负有心人‘’终于在闭关结束的十几天后拿下了腾讯、华为、字节跳动等多家大厂的offer。 今天就把我闭关期间复习的资料整理成了PDF文档分享给大家。 这份PDF包括了Android进阶架构师的核心知识,同时也有面试时面试官必问的知识点,篇章也是囊括了很多知识点,其中包括了java基础、Android核心知识、Android扩展知识、Android开源库源码分析、常见面试算法题汇总等等 由于pdf文档里的细节内容实在过多所以只编辑了部分知识点的章节粗略的介绍下,每个章节小节点里面都有更细化的内容!以下就是部分章节目录,由于简书的篇幅限制目录上的详细讲解也无法一一列出,文末底下有获取以下章节的所有详细知识讲解。 java基础知识 1.java的反射、泛型、注解2.容器中的设计模式3.源码分析4.object通用方法5.HashMap6.LRU缓存7.基础线程机制8.线程之间的协作9.java内存模型 由于篇幅限制,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙转发+关注私信(架构资料)获取哦 Android核心知识 1.Activity2.Fragment3.数据存储4.View4.Parcelable 接口5.IPC6.屏幕适配7.消息机制8.线程异步9.Webview Android扩展知识 1.ART2.APK包体优化3.Hook4.Proguard5.架构6.Jetpack7.NDK 开发8.计算机网络基础9.类加载器 Android开源库源码分析 1.Glide :加载、缓存、LRU 算法 (如何自己设计一个大图加载框架) (LRUCache 原理)2.EventBus3.LeakCanary4.ARouter5.插件化(不同插件化机制原理与流派,优缺点。局限性)6.热修复7.RXJava (RxJava 的线程切换原理)8.Retrofit (Retrofit 在 OkHttp 上做了哪些封装?动态代理和静态代理的区别,是怎么实现的)9.OkHttp 常见面试算法题汇总 1.排序2.二叉树3.链表4.栈 / 队列5.二分6.哈希表7.堆 / 优先队列8.二叉搜索树9.数组 / 双指针10.贪心11.字符串处理13.动态规划14.矩阵15.二进制 / 位运算16.LRU 缓存策略17.反转整数 最后 其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2020年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。[[Android学习PDF+学习视频+面试文档+知识点笔记]](https://shimo.im/docs/9TyYD8yccxkjG8WD/read) 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】免费地址:https://shimo.im/docs/9TyYD8yccxkjG8WD/read
由于最近要做一个安全性比较高的项目,因此需要用到HTTPS进行双向认证。由于设计项目架构的时候,客户端是采用MVVM架构,基于DataBinding + Retrofit + Rxjava来实现Android端。 查阅很多资料,基于原生HttpClient实现双向认证的例子很多,但对于Retrofit的资料网上还是比较少,官方文档也是一句带过,没有具体的介绍。 看了 《Android中https请求的单向认证和双向认证》,给了我很大的启发,于是尝试着博主的方式制作证书,再次尝试的时候果然成功了。 科普一下,什么是HTTPS? 简单来说,HTTPS就是“安全版”的HTTP, HTTPS = HTTP + SSL。HTTPS相当于在应用层和TCP层之间加入了一个SSL(或TLS),SSL层对从应用层收到的数据进行加密。TLS/SSL中使用了RSA非对称加密,对称加密以及HASH算法。 RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。 SSL:(Secure Socket Layer,安全套接字层),为Netscape所研发,用以保障在Internet上数据传输之安全,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取。它已被广泛地用于Web浏览器与服务器之间的身份认证和加密数据传输。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。 TLS:(Transport Layer Security,传输层安全协议),用于两个应用程序之间提供保密性和数据完整性。TLS 1.0是IETF(Internet Engineering Task Force,Internet工程任务组)制定的一种新的协议,它建立在SSL 3.0协议规范之上,是SSL 3.0的后续版本,可以理解为SSL 3.1,它是写入了 RFC的。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。 进入正文 基于Retrofit实现HTTPS思路 由于Retrofit是基于OkHttp实现的,因此想通过Retrofit实现HTTPS需要给Retrofit设置一个OkHttp代理对象用于处理HTTPS的握手过程。代理代码如下: OkHttpClient okHttpClient = new OkHttpClient.Builder() .sslSocketFactory(SSLHelper.getSSLCertifcation(context))//为OkHttp对象设置SocketFactory用于双向认证 .hostnameVerifier(new UnSafeHostnameVerifier()) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://10.2.8.56:8443") .addConverterFactory(GsonConverterFactory.create())//添加 json 转换器 .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 适配器 .client(okHttpClient)//添加OkHttp代理对象 .build(); 证书制作思路: 首先对于双向证书验证,也就是说,客户端持有服务端的公钥证书,并持有自己的私钥,服务端持有客户的公钥证书,并持有自己私钥,建立连接的时候,客户端利用服务端的公钥证书来验证服务器是否上是目标服务器;服务端利用客户端的公钥来验证客户端是否是目标客户端。(请参考RSA非对称加密以及HASH校验算法)服务端给客户端发送数据时,需要将服务端的证书发给客户端验证,验证通过才运行发送数据,同样,客户端请求服务器数据时,也需要将自己的证书发给服务端验证,通过才允许执行请求。 下面我画了一个图,来帮助大家来理解双向认证的过程,证书生成流程,以及各个文件的作用,大家可以对照具体步骤来看 相关格式说明JKS:数字证书库。JKS里有KeyEntry和CertEntry,在库里的每个Entry都是靠别名(alias)来识别的。P12:是PKCS12的缩写。同样是一个存储私钥的证书库,由.jks文件导出的,用户在PC平台安装,用于标示用户的身份。CER:俗称数字证书,目的就是用于存储公钥证书,任何人都可以获取这个文件 。BKS:由于Android平台不识别.keystore和.jks格式的证书库文件,因此Android平台引入一种的证书库格式,BKS。 有些人可能有疑问,为什么Tomcat只有一个server.keystore文件,而客户端需要两个库文件?因为有时客户端可能需要访问过个服务,而服务器的证书都不相同,因此客户端需要制作一个truststore来存储受信任的服务器的证书列表。因此为了规范创建一个truststore.jks用于存储受信任的服务器证书,创建一个client.jks来存储客户端自己的私钥。对于只涉及与一个服务端进行双向认证的应用,将server.cer导入到client.jks中也可。 具体步骤如下: 1.生成客户端keystore keytool -genkeypair -alias client -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore client.jks 2.生成服务端keystore keytool -genkeypair -alias server -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore server.keystore //注意:CN必须与IP地址匹配,否则需要修改host 3.导出客户端证书 keytool -export -alias client -file client.cer -keystore client.jks -storepass 123456 4.导出服务端证书 keytool -export -alias server -file server.cer -keystore server.keystore -storepass 123456 5.重点:证书交换 将客户端证书导入服务端keystore中,再将服务端证书导入客户端keystore中, 一个keystore可以导入多个证书,生成证书列表。 生成客户端信任证书库(由服务端证书生成的证书库): keytool -import -v -alias server -file server.cer -keystore truststore.jks -storepass 123456 将客户端证书导入到服务器证书库(使得服务器信任客户端证书): keytool -import -v -alias client -file client.cer -keystore server.keystore -storepass 123456 6.生成Android识别的BKS库文件 用Portecle工具转成bks格式,最新版本是1.10。 下载链接:https://sourceforge.net/projects/portecle/ 运行protecle.jar将client.jks和truststore.jks分别转换成client.bks和truststore.bks,然后放到android客户端的assert目录下 >File -> open Keystore File -> 选择证书库文件 -> 输入密码 -> Tools -> change keystore type -> BKS -> save keystore as -> 保存即可 这个操作很简单,如果不懂可自行百度。 我在Windows下生成BKS的时候会报错失败,后来我换到CentOS用OpenJDK1.7立马成功了,如果在这步失败的同学可以换到Linux或Mac下操作, 将生成的BKS拷贝回Windows即可。 7.配置Tomcat服务器 修改server.xml文件,配置8443端口 <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" clientAuth="true" sslProtocol="TLS" keystoreFile="${catalina.base}/key/server.keystore" keystorePass="123456" truststoreFile="${catalina.base}/key/server.keystore" truststorePass="123456"/> 备注: - keystoreFile:指定服务器密钥库,可以配置成绝对路径,本例中是在Tomcat目录中创建了一个名为key的文件夹,仅供参考。 - keystorePass:密钥库生成时的密码 - truststoreFile:受信任密钥库,和密钥库相同即可 - truststorePass:受信任密钥库密码 8.Android App编写BKS读取创建证书自定义的SSLSocketFactory private final static String CLIENT_PRI_KEY = "client.bks"; private final static String TRUSTSTORE_PUB_KEY = "truststore.bks"; private final static String CLIENT_BKS_PASSWORD = "123456"; private final static String TRUSTSTORE_BKS_PASSWORD = "123456"; private final static String KEYSTORE_TYPE = "BKS"; private final static String PROTOCOL_TYPE = "TLS"; private final static String CERTIFICATE_FORMAT = "X509"; public static SSLSocketFactory getSSLCertifcation(Context context) { SSLSocketFactory sslSocketFactory = null; try { // 服务器端需要验证的客户端证书,其实就是客户端的keystore KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);// 客户端信任的服务器端证书 KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);//读取证书 InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY); InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);//加载证书 keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray()); trustStore.load(tsIn, TRUSTSTORE_BKS_PASSWORD.toCharArray()); ksIn.close(); tsIn.close(); //初始化SSLContext SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_FORMAT); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_FORMAT); trustManagerFactory.init(trustStore); keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray()); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); sslSocketFactory = sslContext.getSocketFactory(); } catch (KeyStoreException e) {...}//省略各种异常处理,请自行添加 return sslSocketFactory; } 9.Android App获取SSLFactory实例进行网络访问 private void fetchData() { OkHttpClient okHttpClient = new OkHttpClient.Builder() .sslSocketFactory(SSLHelper.getSSLCertifcation(context))//获取SSLSocketFactory .hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName验证器 .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://10.2.8.56:8443")//填写自己服务器IP .addConverterFactory(GsonConverterFactory.create())//添加 json 转换器 .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 适配器 .client(okHttpClient) .build(); IUser userIntf = retrofit.create(IUser.class); userIntf.getUser(user.getPhone()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<UserBean>() { //省略onCompleted、onError、onNext } }); } private class UnSafeHostnameVerifier implements HostnameVerifier { @Override public boolean verify(String hostname, SSLSession session) { return true;//自行添加判断逻辑,true->Safe,false->unsafe } } 结束语 由于双向认证涉及的原理知识太多,有些地方我也是一笔带过,本文想着重介绍证书的制作以及应用。在此奉劝各位,如果不了解RSA非对称加密,对称加密以及HASH校验算法 的同学,最好还是先看书学习一下。了解原理对于进步来说是十分有帮助的,网上的资料鱼龙混杂,不了解原理的话你根本无从分辨网上文章的正误。(不过我这篇文章绝对是正确的双向认证,大家可以放心) 源码地址: GITHUB源码下载找不到包,或者版本不兼容的朋友可以参考一下demo。 作者:ChongmingLiu链接:https://www.jianshu.com/p/64172ccfb73b
从 API 1 开始,处理 Activity 的生命周期 (lifecycle) 就是个老大难的问题,基本上开发者们都看过这两张生命周期流程图: 随着 Fragment 的加入,这个问题也变得更加复杂: 而开发者们面对这个挑战,给出了非常稳健的解决方案: 分层架构。 分层架构 如上图所示,通过将应用分为三层,现在只有最上面的 Presentation 层 (以前叫 UI 层) 才知道生命周期的细节,而应用的其他部分则可以安全地忽略掉它。 而在 Presentation 层内部也有进一步的解决方案: 让一个对象可以在 Activity 和 Fragment 被销毁、重新创建时依然留存,这个对象就是架构组件的 ViewModel 类。下面让我们详细看看 ViewModel 工作的细节。 如上图,当一个视图 (View) 被创建,它有对应的 ViewModel 的引用地址 (注意 ViewModel 并没有 View 的引用地址)。ViewModel 会暴露出若干个 LiveData,视图会通过数据绑定或者手动订阅的方式来观察这些 LiveData。 当设备配置改变时 (比如屏幕发生旋转),之前的 View 被销毁,新的 View 被创建: 这时新的 View 会重新订阅 ViewModel 里的 LiveData,而 ViewModel 对这个变化的过程完全不知情。 归根到底,开发者在执行一个操作时,需要认真选择好这个操作的作用域 (scope)。这取决于这个操作具体是做什么,以及它的内容是否需要贯穿整个屏幕内容的生命周期。比如通过网络获取一些数据,或者是在绘图界面中计算一段曲线的控制锚点,可能所适用的作用域不同。如何取消该操作的时间太晚,可能会浪费很多额外的资源;而如果取消的太早,又会出现频繁重启操作的情况。 在实际应用中,以我们的 Android Dev Summit 应用为例,里面涉及到的作用域非常多。比如,我们这里有一个活动计划页面,里面包含多个 Fragment 实例,而与之对应的 ViewModel 的作用域就是计划页面。与之相类似的,日程和信息页面相关的 Fragment 以及 ViewModel 也是一样的作用域。 此外我们还有很多 Activity,而和它们相关的 ViewModel 的作用域就是这些 Activity。 您也可以自定义作用域。比如针对导航组件,您可以将作用域限制在登录流程或者结账流程中。我们甚至还有针对整个 Application 的作用域。 有如此多的操作会同时进行,我们需要有一个更好的方法来管理它们的取消操作。也就是 Kotlin 的协程 (Coroutine)。 协程的优势 协程的优点主要来自三个方面: 很容易离开主线程。我们试过很多方法来让操作远离主线程,AsyncTask、Loaders、ExecutorServices……甚至有开发者用到了 RxJava。但协程可以让开发者只需要一行代码就完成这个工作,而且没有累人的回调处理。 样板代码最少。协程完全活用了 Kotlin 语言的能力,包括 suspend 方法。编写协程的过程就和编写普通的代码块差不多,编译器则会帮助开发者完成异步化处理。 结构并发性。这个可以理解为针对操作的垃圾搜集器,当一个操作不再需要被执行时,协程会自动取消它。 如何启动和取消协程 在 Jetpack 组件里,我们为各个组件提供了对应的 scope,比如 ViewModel 就有与之对应的 viewModelScope,如果您想在这个作用域里启动协程,使用如下代码即可: class MainActivityViewModel : ViewModel { init { viewModelScope.launch { // Start } } } 如果您在使用 AppCompatActivity 或 Fragment,则可以使用 lifecycleScope,当 lifeCycle 被销毁时,操作也会被取消。代码如下: class MyActivity : AppCompatActivity() { override fun onCreate(state: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { // Run } } } 有些时候,您可能还需要在生命周期的某个状态 (启动时/恢复时等) 执行一些操作,这时您可以使用 launchWhenStarted、launchWhenResumed、launchWhenCreated 这些方法: class MyActivity : Activity { override fun onCreate(state: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { // Run } lifecycleScope.launchWhenResumed { // Run } } } 注意,如果您在 launchWhenStarted 中设置了一个操作,当 Activity 被停止时,这个操作也会被暂停,直到 Activity 被恢复 (Resume)。 最后一种作用域的情况是贯穿整个应用。如果这个操作非常重要,您需要确保它一定被执行,这时请考虑使用 WorkManager。比如您编写了一个发推的应用,希望撰写的推文被发送到服务器上,那这个操作就需要使用 WorkManager 来确保执行。而如果您的操作只是清理一下本地存储,那可以考虑使用 Application Scope,因为这个操作的重要性不是很高,完全可以等到下次应用启动时再做。 WorkManager 不是本文介绍的重点,感兴趣的朋友请参考 《WorkManager 进阶课堂 | AndroidDevSummit 中文字幕视频》。 接下来我们看看如何在 viewModelScope 里使用 LiveData。以前我们想在协程里做一些操作,并将结果反馈到 ViewModel 需要这么操作: class MyViewModel : ViewModel { private val _result = MutableLiveData<String>() val result: LiveData<String> = _result init { viewModelScope.launch { val computationResult = doComputation() _result.value = computationResult } } } 看看我们做了什么: 准备一个 ViewModel 私有的 MutableLiveData (MLD) 暴露一个不可变的 LiveData 启动协程,然后将其操作结果赋给 MLD 这个做法并不理想。在 LifeCycle 2.2.0 之后,同样的操作可以用更精简的方法来完成,也就是 LiveData 协程构造方法 (coroutine builder): class MyViewModel { val result = liveData { emit(doComputation()) } } 这个 liveData 协程构造方法提供了一个协程代码块,这个块就是 LiveData 的作用域,当 LiveData 被观察的时候,里面的操作就会被执行,当 LiveData 不再被使用时,里面的操作就会取消。而且该协程构造方法产生的是一个不可变的 LiveData,可以直接暴露给对应的视图使用。而 emit() 方法则用来更新 LiveData 的数据。 让我们来看另一个常见用例,比如当用户在 UI 中选中一些元素,然后将这些选中的内容显示出来。一个常见的做法是,把被选中的项目的 ID 保存在一个 MutableLiveData 里,然后运行 switchMap。现在在 switchMap 里,您也可以使用协程构造方法: private val itemId = MutableLiveData<String>() val result = itemId.switchMap { liveData { emit(fetchItem(it)) } } LiveData 协程构造方法还可以接收一个 Dispatcher 作为参数,这样您就可以将这个协程移至另一个线程。 liveData(Dispatchers.IO) { } 最后,您还可以使用 emitSource() 方法从另一个 LiveData 获取更新的结果: liveData(Dispatchers.IO) { emit(LOADING_STRING) emitSource(dataSource.fetchWeather()) } 接下来我们来看如何取消协程。绝大部分情况下,协程的取消操作是自动的,毕竟我们在对应的作用域里启动一个协程时,也同时明确了它会在何时被取消。但我们有必要讲一讲如何在协程内部来手动取消协程。 这里补充一个大前提: 所有 kotlin.coroutines 的 suspend 方法都是可取消的。比如这种: suspend fun printPrimes() { while(true) { // Compute delay(1000) } } 在上面这个无限循环里,每一个 delay 都会检查协程是否处于有效状态,一旦发现协程被取消,循环的操作也会被取消。 那问题来了,如果您在 suspend 方法里调用的是一个不可取消的方法呢?这时您需要使用 isActivate 来进行检查并手动决定是否继续执行操作: suspend fun printPrimes() { while(isActive) { // Compute } } LiveData 操作实践 在进入具体的操作实践环节之前,我们需要区分一下两种操作: 单次 (One-Shot) 操作和监听 (observers) 操作。比如 Twitter 的应用: 单次操作,比如获取用户头像和推文,只需要执行一次即可。 监听操作,比如界面下方的转发数和点赞数,就会持续更新数据。 让我们先看看单次操作时的内容架构: 如前所述,我们使用 LiveData 连接 View 和 ViewModel,而在 ViewModel 这里我们则使用刚刚提到的 liveData 协程构造方法来打通 LiveData 和协程,再往右就是调用 suspend 方法了。 如果我们想监听多个值的话,该如何操作呢? 第一种选择是在 ViewModel 之外也使用 LiveData: △ Reopsitory 监听 Data Source 暴露出来的 LiveData,同时自己也暴露出 LiveData 供 ViewModel 使用 但是这种实现方式无法体现并发性,比如每次用户登出时,就需要手动取消所有的订阅。LiveData 本身的设计并不适合这种情况,这时我们就需要使用第二种选择: 使用 Flow。 ViewModel 模式 当 ViewModel 监听 LiveData,而且没有对数据进行任何转换操作时,可以直接将 dataSource 中的 LiveData 赋值给 ViewModel 暴露出来的 LiveData: val currentWeather: LiveData<String> = dataSource.fetchWeather() 如果使用 Flow 的话就需要用到 liveData 协程构造方法。我们从 Flow 中使用 collect 方法获取每一个结果,然后 emit 出来给 liveData 协程构造方法使用: val currentWeatherFlow: LiveData<String> = liveData { dataSource.fetchWeatherFlow().collect { emit(it) } } 不过 Flow 给我们准备了更简单的写法: val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow().asLiveData() 接下来一个场景是,我们先发送一个一次性的结果,然后再持续发送多个数值: val currentWeather: LiveData<String> = liveData { emit(LOADING_STRING) emitSource(dataSource.fetchWeather()) } 在 Flow 中我们可以沿用上面的思路,使用 emit 和 emitSource: val currentWeatherFlow: LiveData<String> = liveData { emit(LOADING_STRING) emitSource( dataSource.fetchWeatherFlow().asLiveData() ) } 但同样的,这种情况 Flow 也有更直观的写法: val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow() .onStart { emit(LOADING_STRING) } .asLiveData() 接下来我们看看需要为接收到的数据做转换时的情况。 使用 LiveData 时,如果用 map 方法做转换,操作会进入主线程,这显然不是我们想要的结果。这时我们可以使用 switchMap,从而可以通过 liveData 协程构造方法获得一个 LiveData,而且 switchMap 的方法会在每次数据源 LiveData 更新时调用。而在方法体内部我们可以使用 heavyTransformation 函数进行数据转换,并发送其结果给 liveData 协程构造方法: val currentWeatherLiveData: LiveData<String> = dataSource.fetchWeather().switchMap { liveData { emit(heavyTransformation(it)) } } 使用 Flow 的话会简单许多,直接从 dataSource 获得数据,然后调用 map 方法 (这里用的是 Flow 的 map 方法,而不是 LiveData 的),然后转化为 LiveData 即可: val currentWeatherFlow: LiveData<String> = dataSource.fetchWeatherFlow() .map { heavyTransformation(it) } .asLiveData() Repository 模式 Repository 一般用来进行复杂的数据转换和处理,而 LiveData 没有针对这种情况进行设计。现在通过 Flow 就可以完成各种复杂的操作: val currentWeatherFlow: Flow<String> = dataSource.fetchWeatherFlow() .map { ... } .filter { ... } .dropWhile { ... } .combine { ... } .flowOn(Dispatchers.IO) .onCompletion { ... } ... 数据源模式 而在涉及到数据源时,情况变得有些复杂,因为这时您可能是在和其他代码库或者远程数据源进行交互,但是您又无法控制这些数据源。这里我们分两种情况介绍: 1. 单次操作 如果使用 Retrofit 从远程数据源获取数值,直接将方法标记为 suspend 方法即可*: suspend fun doOneShot(param: String) : String = retrofitClient.doSomething(param) Retrofit 从 2.6.0 开始支持 suspend 方法,Room 从 2.1.0 开始支持 suspend 方法。 如果您的数据源尚未支持协程,比如是一个 Java 代码库,而且使用的是回调机制。这时您可以使用 suspendCancellableCoroutine 协程构造方法,这个方法是协程和回调之间的适配器,会在内部提供一个 continuation 供开发者使用: suspend fun doOneShot(param: String) : Result<String> = suspendCancellableCoroutine { continuation -> api.addOnCompleteListener { result -> continuation.resume(result) }.addOnFailureListener { error -> continuation.resumeWithException(error) } } 如上所示,在回调方法取得结果后会调用 continuation.resume(),如果报错的话调用的则是 continuation.resumeWithException()。 注意,如果这个协程已经被取消,则 resume 调用也会被忽略。开发者可以在协程被取消时主动取消 API 请求。 2. 监听操作 如果数据源会持续发送数值的话,使用 flow 协程构造方法会很好地满足需求,比如下面这个方法就会每隔 2 秒发送一个新的天气值: override fun fetchWeatherFlow(): Flow<String> = flow { var counter = 0 while(true) { counter++ delay(2000) emit(weatherConditions[counter % weatherConditions.size]) } } 如果开发者使用的是不支持 Flow 而是使用回调的代码库,则可以使用 callbackFlow。比如下面这段代码,api 支持三个回调分支 onNextValue、onApiError 和 onCompleted,我们可以得到结果的分支里使用 offer 方法将值传给 Flow,在发生错误的分支里 close 这个调用并传回一个错误原因 (cause),而在顺利调用完成后直接 close 调用: fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow { val callback = object : Callback { override fun onNextValue(value: T) { offer(value) } override fun onApiError(cause: Throwable) { close(cause) } override fun onCompleted() = close() } api.register(callback) awaitClose { api.unregister(callback) } } 注意在这段代码的最后,如果 API 不会再有更新,则使用 awaitClose 彻底关闭这条数据通道。 相信看到这里,您对如何在实际应用中使用协程、LiveData 和 Flow 已经有了比较系统的认识。您可以重温 Android Dev Summit 上 Jose Alcérreca 和 Yigit Boyar 的演讲来巩固理解: 视频链接:v.qq.com/x/page/a302… 如果您对协程、LiveData 和 Flow 有任何疑问和想法,欢迎在评论区和我们分享。 点击这里进一步了解 LiveData 作者:Android_开发者链接:https://juejin.im/post/5ebb5c1ee51d454ddb0b4e1d来源:掘金
之前,也写过几篇关于 Flutter 的博文,最近,又花了一些时间学习研究 Flutter,完成了高仿大厂 App 项目 (项目使用的接口都是来自线上真实App抓包而来,可以做到和线上项目相同的效果),也总结积累了一些小技巧和知识点,所以,在这里记录分享出来,也希望 Flutter 生态越来越好 (flutter开发App效率真的很高,开发体验也是很好的 )。 以下博文会分为3个部分概述: 项目结构分析 项目功能详细概述(所用知识点) 小技巧积累总结 项目结构分析 其次,梳理下项目的目录结构,理解每个文件都是干什么的,我们先来看看一级目录,如下: ├── README.md # 描述文件 ├── android # android 宿主环境 ├── build # 项目构建目录,由flutter自动完成 ├── flutter_ctrip.iml ├── fonts # 自己创建的目录,用于存放字体 ├── images # 自己创建的目录,用于存放图片 ├── ios # iOS 宿主环境 ├── lib # flutter 执行文件,自己写的代码都在这 ├── pubspec.lock # 用来记录锁定插件版本 ├── pubspec.yaml # 插件及资源配置文件 └── test # 测试目录 这个就不用多解释,大多是 flutter 生成及管理的,我们需要关注的是 lib 目录。 我们再来看看二级目录,如下 (重点关注下lib目录) ├── README.md ├── android │ ├── android.iml ... │ └── settings.gradle ├── build │ ├── app ... │ └── snapshot_blob.bin.d.fingerprint ├── flutter_ctrip.iml ├── fonts │ ├── PingFang-Italic.ttf │ ├── PingFang-Regular.ttf │ └── PingFang_Bold.ttf ├── images │ ├── grid-nav-items-dingzhi.png ... │ └── yuyin.png ├── iOS │ ├── Flutter ... │ └── ServiceDefinitions.json ├── lib │ ├── dao # 请求接口的类 │ ├── main.dart # flutter 入口文件 │ ├── model # 实体类,把服务器返回的 json 数据,转换成 dart 类 │ ├── navigator # bottom bar 首页底部导航路由 │ ├── pages # 所以的页面 │ ├── plugin # 封装的插件 │ ├── util # 工具类,避免重复代码,封装成工具类以便各个 page 调用 │ └── widget # 封装的组件 ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart 再来看看,lib 目录下二级目录,看看整个项目创建了多少个文件,写了多少代码,如下 (其实,并不是很多) ├── dao/ │ ├── destination_dao.dart* │ ├── destination_search_dao.dart* │ ├── home_dao.dart │ ├── search_dao.dart* │ ├── trave_hot_keyword_dao.dart* │ ├── trave_search_dao.dart* │ ├── trave_search_hot_dao.dart* │ ├── travel_dao.dart* │ ├── travel_params_dao.dart* │ └── travel_tab_dao.dart* ├── main.dart ├── model/ │ ├── common_model.dart │ ├── config_model.dart │ ├── destination_model.dart │ ├── destination_search_model.dart │ ├── grid_nav_model.dart │ ├── home_model.dart │ ├── sales_box_model.dart │ ├── seach_model.dart* │ ├── travel_hot_keyword_model.dart │ ├── travel_model.dart* │ ├── travel_params_model.dart* │ ├── travel_search_hot_model.dart │ ├── travel_search_model.dart │ └── travel_tab_model.dart ├── navigator/ │ └── tab_navigater.dart ├── pages/ │ ├── destination_page.dart │ ├── destination_search_page.dart │ ├── home_page.dart │ ├── my_page.dart │ ├── search_page.dart │ ├── speak_page.dart* │ ├── test_page.dart │ ├── travel_page.dart │ ├── travel_search_page.dart │ └── travel_tab_page.dart* ├── plugin/ │ ├── asr_manager.dart* │ ├── side_page_view.dart │ ├── square_swiper_pagination.dart │ └── vertical_tab_view.dart ├── util/ │ └── navigator_util.dart* └── widget/ ├── grid_nav.dart ├── grid_nav_new.dart ├── loading_container.dart ├── local_nav.dart ├── sales_box.dart ├── scalable_box.dart ├── search_bar.dart* ├── sub_nav.dart └── webview.dart 整个项目就是以上这些文件了 (具体的就不一个一个分析了,如,感兴趣,大家可以 clone 源码运行起来,自然就清除了)。 项目功能详细概述(所用知识点) 首先,来看看首页功能及所用知识点,首页重点看下以下功能实现: 渐隐渐现的 appBbar 搜索组件的封装 语音搜索页面 banner组件 浮动的 icon 导航 渐变不规则带有背景图的网格导航 渐隐渐现的 appBbar 先来看看具体效果,一睹芳容,如图: 滚动的时候 appBar 背景色从透明变成白色或白色变成透明,这里主要用了 flutter 的 NotificationListener 组件,它会去监听组件树冒泡事件,当被它包裹的的组件(子组件) 发生变化时,Notification 回调函数会被触发,所以,通过它可以去监听页面的滚动,来动态改变 appBar 的透明度(alpha),代码如下: NotificationListener( onNotification: (scrollNotification) { if (scrollNotification is ScrollUpdateNotification && scrollNotification.depth == 0) { _onScroll(scrollNotification.metrics.pixels); } return true; }, child: ... Tips:scrollNotification.depth 的值 0 表示其子组件(只监听子组件,不监听孙组件);scrollNotification is ScrollUpdateNotification 来判断组件是否已更新,ScrollUpdateNotification 是 notifications 的生命周期一种情况,分别有一下几种: ScrollStartNotification 组件开始滚动 ScrollUpdateNotification 组件位置已经发生改变 ScrollEndNotification 组件停止滚动 UserScrollNotification 不清楚 这里,我们不探究太深入,如想了解可多查看源码。 _onScroll 方法代码如下: void _onScroll(offset) { double alpha = offset / APPBAR_SCROLL_OFFSET; // APPBAR_SCROLL_OFFSET 常量,值:100;offset 滚动的距离 //把 alpha 值控制值 0-1 之间 if (alpha < 0) { alpha = 0; } else if (alpha > 1) { alpha = 1; } setState(() { appBarAlpha = alpha; }); print(alpha); } 搜索组件的封装 搜索组件效果如图: 以下是首页调用 searchBar 的代码: SearchBar( searchBarType: appBarAlpha > 0.2 //searchBar 的类:暗色、亮色 ? SearchBarType.homeLight : SearchBarType.home, inputBoxClick: _jumpToSearch, //点击回调函数 defaultText: SEARCH_BAR_DEFAULT_TEXT, // 提示文字 leftButtonClick: () {}, //左边边按钮点击回调函数 speakClick: _jumpToSpeak, //点击话筒回调函数 rightButtonClick: _jumpToUser, //右边边按钮点击回调函数 ), 其实就是用 TextField 组件,再加一些样式,需要注意点是:onChanged,他是 TextField 用来监听文本框是否变化,通过它我们来监听用户输入,来请求接口数据;具体的实现细节,请查阅源码: 点击查看searchBar源码 语音搜索页面 语音搜索页面效果如图:由于模拟器无法录音,所以无法展示正常流程,如果录音识别成功后会返回搜索页面,在项目预览视频中可以看到正常流程。 [图片上传失败...(image-190ee2-1589860267422)] 语音搜索功能使用的是百度的语言识别SDK,原生接入之后,通过 MethodChannel 和原生Native端通信,这里不做重点讲述(这里会涉及原生Native的知识)。 重点看看点击录音按钮时的动画实现,这个动画用了 AnimatedWidget 实现的,代码如下: class AnimatedWear extends AnimatedWidget { final bool isStart; static final _opacityTween = Tween<double>(begin: 0.5, end: 0); // 设置透明度变化值 static final _sizeTween = Tween<double>(begin: 90, end: 260); // 设置圆形线的扩散值 AnimatedWear({Key key, this.isStart, Animation<double> animation}) : super(key: key, listenable: animation); @override Widget build(BuildContext context) { final Animation<double> animation = listenable; // listenable 继承 AnimatedWidget,其实就是控制器,会自动监听组件的变化 return Container( height: 90, width: 90, child: Stack( overflow: Overflow.visible, alignment: Alignment.center, children: <Widget>[ ... // 扩散的圆线,其实就是用一个圆实现的,设置圆为透明,设置border Positioned( left: -((_sizeTween.evaluate(animation) - 90) / 2), // 根据 _sizeTween 动态设置left偏移值 top: -((_sizeTween.evaluate(animation) - 90) / 2), // 根据 _sizeTween 动态设置top偏移值 child: Opacity( opacity: _opacityTween.evaluate(animation), // 根据 _opacityTween 动态设置透明值 child: Container( width: isStart ? _sizeTween.evaluate(animation) : 0, // 设置 宽 height: _sizeTween.evaluate(animation), // 设置 高 decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular( _sizeTween.evaluate(animation) / 2), border: Border.all( color: Color(0xa8000000), )), ), ), ), ], ), ); } } 其他细节,如:点击时提示录音,录音失败提示,点击录音按钮出现半透明黑色圆边框,停止后消失等,请查看源码。 banner组件 效果如图: banner使用的是flutter的 flutter_swiper 插件实现的,代码如下: Swiper( itemCount: bannerList.length, // 滚动图片的数量 autoplay: true, // 自动播放 pagination: SwiperPagination( // 指示器 builder: SquareSwiperPagination( size: 6, // 指示器的大小 activeSize: 6, // 激活状态指示器的大小 color: Colors.white.withAlpha(80), // 颜色 activeColor: Colors.white, // 激活状态的颜色 ), alignment: Alignment.bottomRight, // 对齐方式 margin: EdgeInsets.fromLTRB(0, 0, 14, 28), // 边距 ), itemBuilder: (BuildContext context, int index) { // 构造器 return GestureDetector( onTap: () { CommonModel model = bannerList[index]; Navigator.push( context, MaterialPageRoute( builder: (context) => WebView( url: model.url, ), ), ); }, child: Image.network( bannerList[index].icon, fit: BoxFit.fill, ), ); }, ), 具体使用方法,可以去 flutter的官方插件库 pub.dev 查看:点击flutter_swiper查看。 Tips:需要注意的是,我稍改造了一下指示器的样式,flutter_swiper 只提供了 3 种指示器样式,如下: dots = const DotSwiperPaginationBuilder(),圆形 fraction = const FractionPaginationBuilder(),百分数类型的,如:1/6,表示6页的第一页 rect = const RectSwiperPaginationBuilder(),矩形 并没有上图的激活状态的长椭圆形,其实就是按葫芦画瓢,自己实现一个长椭圆类型,如知详情,可点击查看长椭圆形指示器源码 浮动的 icon 导航 icon导航效果如图: icon导航浮动在banner之上,其实用的是 flutter 的 Stack 组件,Stack 组件能让其子组件堆叠显示,它通常和 Positioned 组件配合使用,布局结构代码如下: ListView( children: <Widget>[ Container( child: Stack( children: <Widget>[ Container( ... ), //这里放的是banner的代码 Positioned( ... ), //这个就是icon导航,通过 Positioned 固定显示位置 ], ), ), Container( ... ), // 这里放的网格导航及其他 ], ), 渐变不规则带有背景图的网格导航 网格导航效果如图: 如图,网格导航分为三行四栏,而第一行分为三栏,每一行的第一栏宽度大于其余三栏,其余三栏均等,每一行都有渐变色,而且第一、二栏都有背景图;flutter 里 Column 组件能让子组件竖轴排列, Row 组件能让子组件横轴排列,布局代码如下: Column( // 最外面放在 Column 组件 children: <Widget>[ Container( // 第一行包裹 Container 设置其渐变色 height: 72, decoration: BoxDecoration( gradient: LinearGradient(colors: [ //设置渐变色 Color(0xfffa5956), Color(0xffef9c76).withAlpha(45) ]), ), child: Row( ... ), // 第一行 ), Padding( padding: EdgeInsets.only(top: 1), // 设置行直接的间隔 ), Container( height: 72, decoration: BoxDecoration( gradient: LinearGradient(colors: [ //设置渐变色 Color(0xff4b8fed), Color(0xff53bced), ]), ), child: Row( ... ), // 第二行 ), Padding( padding: EdgeInsets.only(top: 1), // 设置行直接的间隔 ), Container( height: 72, decoration: BoxDecoration( gradient: LinearGradient(colors: [ //设置渐变色 Color(0xff34c2aa), Color(0xff6cd557), ]), ), child: Row( ... ), // 第三行 ), ], ), 其实,具体实现的细节还是很多的,比如: 怎么设置第一栏宽度偏大,其他均等; 第一行最后一栏宽度是其他的2倍; 第一、二栏的别截图及浮动的红色气泡tip等; 在这里就不细讲,否则篇幅太长,如想了解详情 点击查看源码 其次,再来看看目的地页面功能及所用知识点,重点看下以下功能实现: 左右布局tabBarListView 目的地搜索页面 左右布局tabBarListView 具体效果如图:点击左边标签可以切换页面,左右滑动也可切换页面,点击展开显示更多等 其实官方已经提供了 tabBar 和 TabBarView 组件可以实现上下布局的效果(旅拍页面就是用这个实现的),但是它无法实现左右布局,而且不太灵活,所以,我使用的是 vertical_tabs插件, 代码如下: VerticalTabView( tabsWidth: 88, tabsElevation: 0, indicatorWidth: 0, selectedTabBackgroundColor: Colors.white, backgroundColor: Colors.white, tabTextStyle: TextStyle( height: 60, color: Color(0xff333333), ), tabs: tabs, contents: tabPages, ), ), 具体使用方法,在这里就不赘述了,点击vertical_tabs查看 Tips:这里需要注意的是:展开显示更多span标签组件的实现,因为,这个组件在很多的其他组件里用到而且要根据接口数据动态渲染,且组件自身存在状态的变化,这种情况下,最好是把他单独封装成一个组件(widget),否则,很难控制自身状态的变化,出现点击没有效果,或点击影响其他组件。 目的地搜索页面 效果如图:点击搜索结果,如:点击‘一日游‘,会搜索到‘一日游‘的相关数据 目的地搜索页面,大多都是和布局和对接接口的代码,在这里就不再赘述。 然后就是旅拍页面功能及所用知识点,重点看下以下功能实现: 左右布局tabBarListView 瀑布流卡片 旅拍搜索页 左右布局tabBarListView 效果如图:可左右滑动切换页面,上拉加载更多,下拉刷新等 这个是flutter 提供的组件,tabBar 和 TabBarView,代码如下: Container( color: Colors.white, padding: EdgeInsets.only(left: 2), child: TabBar( controller: _controller, isScrollable: true, labelColor: Colors.black, labelPadding: EdgeInsets.fromLTRB(8, 6, 8, 0), indicatorColor: Color(0xff2FCFBB), indicatorPadding: EdgeInsets.all(6), indicatorSize: TabBarIndicatorSize.label, indicatorWeight: 2.2, labelStyle: TextStyle(fontSize: 18), unselectedLabelStyle: TextStyle(fontSize: 15), tabs: tabs.map<Tab>((Groups tab) { return Tab( text: tab.name, ); }).toList(), ), ), Flexible( child: Container( padding: EdgeInsets.fromLTRB(6, 3, 6, 0), child: TabBarView( controller: _controller, children: tabs.map((Groups tab) { return TravelTabPage( travelUrl: travelParamsModel?.url, params: travelParamsModel?.params, groupChannelCode: tab?.code, ); }).toList()), )), 瀑布流卡片 瀑布流卡片 用的是 flutter_staggered_grid_view 插件,代码如下: StaggeredGridView.countBuilder( controller: _scrollController, crossAxisCount: 4, itemCount: travelItems?.length ?? 0, itemBuilder: (BuildContext context, int index) => _TravelItem( index: index, item: travelItems[index], ), staggeredTileBuilder: (int index) => new StaggeredTile.fit(2), mainAxisSpacing: 2.0, crossAxisSpacing: 2.0, ), 如下了解更多相关信息,点击flutter_staggered_grid_view查看。 旅拍搜索页 效果如图:首先显示热门旅拍标签,点击可搜索相关内容,输入关键字可搜索相关旅拍信息,地点、景点、用户等 旅拍搜索页,大多也是和布局和对接接口的代码,在这里就不再赘述。 小技巧积累总结 以下都是我在项目里使用的知识点,在这里记录分享出来,希望能帮到大家。 PhysicalModel PhysicalModel 可以裁剪带背景图的容器,如,你在一个 Container 里放了一张图片,想设置图片圆角,设置 Container 的 decoration 的 borderRadius 是无效的,这时候就要用到 PhysicalModel,代码如下: PhysicalModel( borderRadius: BorderRadius.circular(6), // 设置圆角 clipBehavior: Clip.antiAlias, // 裁剪行为 color: Colors.transparent, // 颜色 elevation: 5, // 设置阴影 child: Container( child: Image.network( picUrl, fit: BoxFit.cover, ), ), ), LinearGradient 给容器添加渐变色,在网格导航、appBar等地方都使用到,代码如下: Container( height: 72, decoration: BoxDecoration( gradient: LinearGradient(colors: [ Color(0xff4b8fed), Color(0xff53bced), ]), ), child: ... ), Color(int.parse('0xff' + gridNavItem.startColor)) 颜色值转换成颜色,如果,没有变量的话,也可直接这样用 Color(0xff53bced), ox:flutter要求,可固定不变 ff:代表透明贴,不知道如何设置的话,可以用取色器,或者 withOpacity(opacity) 、 withAlpha(a) 53bced: 常规的6位RGB值 Expanded、FractionallySizedBox Expanded 可以让子组件撑满父容器,通常和 Row 及 Column 组件搭配使用;FractionallySizedBox 可以让子组件撑满或超出父容器,可以单独使用,大小受 widthFactor 和 heightFactor 宽高因子的影响 MediaQuery.removePadding MediaQuery.removePadding 可以移除组件的边距,有些组件自带有边距,有时候布局的时候,不需要边距,这时候就可以用 MediaQuery.removePadding,代码如下: MediaQuery.removePadding( removeTop: true, context: context, child: ... ) MediaQuery.of(context).size.width MediaQuery.of(context).size.width 获取屏幕的宽度,同理,MediaQuery.of(context).size.height 获取屏幕的高度;如,想一行平均3等分: 0.3 * MediaQuery.of(context).size.width,在目的地页面的标签组件就使用到它,代码如下: Container( alignment: Alignment.center, ... width: 0.3*MediaQuery.of(context).size.width - 12, // 屏幕平分三等分, - 12 是给每份中间留出空间 height: 40, ... child: ... ), Theme.of(context).platform == TargetPlatform.iOS 判断操作系统类型,有时候可能有给 Andorid 和 iOS 做出不同的布局,就需要用到它。 with AutomaticKeepAliveClientMixin flutter 在切换页面时候每次都会重新加载数据,如果想让页面保留状态,不重新加载,就需要使用 AutomaticKeepAliveClientMixin,代码如下:(在旅拍页面就有使用到它,为了让tabBar 和 tabBarView在切换时不重新加载) class TravelTabPage extends StatefulWidget { ... //需要重写 wantKeepAlive 且 设置成 true @override bool get wantKeepAlive => true; } 推荐阅读:2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中)字节跳动面试题 —— 水壶问题 重磅来袭!阿里P7“青春修炼手册”(全网独家首发!) 暂时只能想到这些常用的知识点,以后如有新的会慢慢补充。 作者:子木_lsy链接:https://www.jianshu.com/p/6ddeb5ee6619
前言 许多 Android 开发者经常会问我,要学会哪些东西才能成为一个优秀的 Android 工程师?对于这个问题,他们的描述或多或少都有些差异,但是,总体来说,我们都需要学习一系列的技能,才能成为一个优秀的 Android 工程师。 在我看来,存在这样的困惑是正常的。Android 是一个巨大并且动态的生态系统,你可能需要花好几周时间去了解并学习它相关的一些工具和概念,但是最后你会发现,它们有好多都不是很重要,或者说并不是非常有用。因此,在本文中,我将分享我在 Android 开发中所使用到的重要技能,希望能够帮到你,让你把你的精力集中到重要的事情上。 所以,今天,我将献上一份《Android知识图谱》,以自身的经验 & 所见所闻,旨在告诉大家,学习Android,实际上需要学习什么内容,希望你们会喜欢。 核心分析内容 面向Android初、中级开发者,对于要学习的Android理论知识,我认为主要包括: Android基础 & 常用Android进阶与时俱进、热门技术编程语言计算机基础下面,我将对上面的理论知识逐一介绍。 1. Android基础 & 常用 针对Android基础&常用知识,我认为对于初级开发者来说,按照优先级最主要的知识点主要包括:四大组件、布局使用、多线程 & 动画;具体介绍如下: 2. Android进阶 针对Android进阶知识,按照优先级最主要的知识点主要包括:自定义View、性能优化,具体介绍如下: 3. 与时俱进、热门技术 除了基础日常使用的Android知识,我们还需时刻关注行业动态,与时俱进的学习新技术,如近些年来较为热门的Android新兴技术包括:Flutter、热修复、插件化等;同时,了解 & 学习常用的开源库也十分重要,常用的开源库主要包括图片加载、网络请求、异步处理的开源库,具体类型如下: 4. 编程语言:Java与Java虚拟机 Android是基于Java的,所以学习Java和Java虚拟机(JVM)十分重要对于学习Java,我们移动端开发学习Java不需要后端那么深入,我认为作为Android开发者,学习的内容包括:语言特性、基础使用、集合类&机制。具体介绍如下: 近年来新兴的Kotlin大家也可以了解一下,但我认为短时间内是不会完全取代Java 对于Java虚拟机(JVM),属于底层 & 原理性的内容,具体介绍 & 学习的内容包括: 5. 计算机基础 除了学习Android特定技术外,对于程序员来说,计算机基础素养也是十分重要,即所有从事技术行业的程序员都该具备的基础知识。计算机基础主要包括:数据结构、算法和计算机网络,具体介绍如下: 6. 额外 当你学习完上述知识后,你应该已经能称得算是一个中级Android开发工程师了,可以尝试向高级Android开发工程师进阶。此时,我认为有3个方向可以尝试:技术专家、架构师 & 管理层,具体介绍如下: 7. 总结 至此,关于需学习的Android理论知识 & Android知识图谱介绍完毕,下面作一个简单总结:下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC 推荐阅读:做了5年Android,靠着这份面试题跟答案,我从12K变成了30K
前言 相信很多同学都会有这样的感受,前三天刚刚复习的知识点,今天问的时候怎么就讲不出个所以然了呢? 本文的目的就是致力于帮助大家尽可能的建立Android知识体系,希望大家会喜欢~ 考虑到上传完的脑图都被压缩过,高清脑图下载地址: 链接:pan.baidu.com/s/1bUQccZiu… 密码:wyc8 必读 知识结构 覆盖的知识点有Android、Java、Kotlin、Jvm、网络和设计模式。 面向人群 正在求职的中高级Android开发 食用指南 和大部分人一样,我在复习完第一遍Android知识的情况下,看到相关的知识回答的仍然不能够令自己满意。 在第二遍系统复习的时候,我着重记住每个知识点的关键字,根据这些关键字拼凑出大概的知识点,最后看到每个知识点的时候,就知道大概会问哪些内容,达到这种境界以后,你就可以从容的面对每次面试了。 简单的做法就是为每个知识点建立脑图,尽可能把自己想到的关键点罗列出来,也就是下面每个章节前面的脑图。 除此以外,我还为大家提供了可能会问到的面试题。 一、Android基础 Android基础知识点比较多,看图。 建议阅读: 《Android开发艺术探索》 1. Activity Activity的四大启动模式,以及应用场景? Activity的四大启动模式: standard:标准模式,每次都会在活动栈中生成一个新的Activity实例。通常我们使用的活动都是标准模式。 singleTop:栈顶复用,如果Activity实例已经存在栈顶,那么就不会在活动栈中创建新的实例。比较常见的场景就是给通知跳转的Activity设置,因为你肯定不想前台Activity已经是该Activity的情况下,点击通知,又给你再创建一个同样的Activity。 singleTask:栈内复用,如果Activity实例在当前栈中已经存在,就会将当前Activity实例上面的其他Activity实例都移除栈。常见于跳转到主界面。 singleInstance:单实例模式,创建一个新的任务栈,这个活动实例独自处在这个活动栈中。 Activity中onStart和onResume的区别?onPause和onStop的区别? 首先,Activity有三类: 前台Activity:活跃的Activity,正在和用户交互的Activity。 可见但非前台的Activity:常见于栈顶的Activity背景透明,处在其下面的Activity就是可见但是不可和用户交互。 后台Activity:已经被暂停的Activity,比如已经执行了onStop方法。 所以,onStart和onStop通常指的是当前活动是否位于前台这个角度,而onResume和onPause从是否可见这个角度来讲的。 2. 屏幕适配 平时如何有使用屏幕适配吗?原理是什么呢? 平时的屏幕适配一般采用的头条的屏幕适配方案。简单来说,以屏幕的一边作为适配,通常是宽。 原理:设备像素px和设备独立像素dp之间的关系是 px = dp * density 假设UI给的设计图屏幕宽度基于360dp,那么设备宽的像素点已知,即px,dp也已知,360dp,所以density = px / dp,之后根据这个修改系统中跟density相关的知识点即可。 3. Android消息机制 Android消息机制介绍? Android消息机制中的四大概念: ThreadLocal:当前线程存储的数据仅能从当前线程取出。 MessageQueue:具有时间优先级的消息队列。 Looper:轮询消息队列,看是否有新的消息到来。 Handler:具体处理逻辑的地方。 过程: 准备工作:创建Handler,如果是在子线程中创建,还需要调用Looper#prepare(),在Handler的构造函数中,会绑定其中的Looper和MessageQueue。 发送消息:创建消息,使用Handler发送。 进入MessageQueue:因为Handler中绑定着消息队列,所以Message很自然的被放进消息队列。 Looper轮询消息队列:Looper是一个死循环,一直观察有没有新的消息到来,之后从Message取出绑定的Handler,最后调用Handler中的处理逻辑,这一切都发生在Looper循环的线程,这也是Handler能够在指定线程处理任务的原因。 Looper在主线程中死循环为什么没有导致界面的卡死? 导致卡死的是在Ui线程中执行耗时操作导致界面出现掉帧,甚至ANR,Looper.loop()这个操作本身不会导致这个情况。 有人可能会说,我在点击事件中设置死循环会导致界面卡死,同样都是死循环,不都一样的吗?Looper会在没有消息的时候阻塞当前线程,释放CPU资源,等到有消息到来的时候,再唤醒主线程。 App进程中是需要死循环的,如果循环结束的话,App进程就结束了。 建议阅读: 《Android中为什么主线程不会因为Looper.loop()里的死循环卡死?》 IdleHandler介绍? 介绍: IdleHandler是在Hanlder空闲时处理空闲任务的一种机制。 执行场景: MessageQueue没有消息,队列为空的时候。 MessageQueue属于延迟消息,当前没有消息执行的时候。 会不会发生死循环: 答案是否定的,MessageQueue使用计数的方法保证一次调用MessageQueue#next方法只会使用一次的IdleHandler集合。 4. View事件分发机制和View绘制原理 刚哥的《Android开发艺术探索》已经很全面了,建议阅读。 5. Bitmap Bitmap的内存计算方式? 在已知图片的长和宽的像素的情况下,影响内存大小的因素会有资源文件位置和像素点大小。 像素点大小: 常见的像素点有: ARGB_8888:4个字节 ARGB_4444、ARGB_565:2个字节 资源文件位置: 不同dpi对应存放的文件夹 比如一个一张图片的像素为180*180px,dpi(设备独立像素密度)为320,如果它仅仅存放在drawable-hdpi,则有: 横向像素点 = 180 * 320/240 + 0.5f = 240 px 纵向像素点 = 180 * 320/240 + 0.5f = 240 px 如果 如果它仅仅存放在drawable-xxhdpi,则有: 横向像素点 = 180 * 320/480 + 0.5f = 120 px 纵向像素点 = 180 * 320/480 + 0.5f = 120 px 所以,对于一张180*180px的图片,设备dpi为320,资源图片仅仅存在drawable-hdpi,像素点大小为ARGB_4444,最后生成的文件内存大小为: 横向像素点 = 180 * 320/240 + 0.5f = 240 px 纵向像素点 = 180 * 320/240 + 0.5f = 240 px 内存大小 = 240 * 240 * 2 = 115200byte 约等于 112.5kb 建议阅读: 《Android Bitmap的内存大小是如何计算的?》 Bitmap的高效加载? Bitmap的高效加载在Glide中也用到了,思路: 获取需要的长和宽,一般获取控件的长和宽。 设置BitmapFactory.Options中的inJustDecodeBounds为true,可以帮助我们在不加载进内存的方式获得Bitmap的长和宽。 对需要的长和宽和Bitmap的长和宽进行对比,从而获得压缩比例,放入BitmapFactory.Options中的inSampleSize属性。 设置BitmapFactory.Options中的inJustDecodeBounds为false,将图片加载进内存,进而设置到控件中。 二、Android进阶 Android进阶中重点考察Android Framework、性能优化和第三方框架。 1. Binder Binder的介绍?与其他IPC方式的优缺点? Binder是Android中特有的IPC方式,引用《Android开发艺术探索》中的话(略有改动): 从IPC角度来说,Binder是Android中的一种跨进程通信方式;Binder还可以理解为虚拟的物理设备,它的设备驱动是/dev/binder;从Android Framework来讲,Binder是Service Manager连接各种Manager和对应的ManagerService的桥梁。从面向对象和CS模型来讲,Client通过Binder和远程的Server进行通讯。 基于Binder,Android还实现了其他的IPC方式,比如AIDL、Messenger和ContentProvider。 与其他IPC比较: 效率高:除了内存共享外,其他IPC都需要进行两次数据拷贝,而因为Binder使用内存映射的关系,仅需要一次数据拷贝。 安全性好:接收方可以从数据包中获取发送发的进程Id和用户Id,方便验证发送方的身份,其他IPC想要实验只能够主动存入,但是这有可能在发送的过程中被修改。 Binder的通信过程?Binder的原理? 图片: 其实这个过程也可以从AIDL生成的代码中看出。 原理: Binder的结构: Client:服务的请求方。 Server:服务的提供方。 Service Manager:为Server提供Binder的注册服务,为Client提供Binder的查询服务,Server、Client和Service Manager的通讯都是通过Binder。 Binder驱动:负责Binder通信机制的建立,提供一系列底层支持。 从上图中,Binder通信的过程是这样的: Server在Service Manager中注册:Server进程在创建的时候,也会创建对应的Binder实体,如果要提供服务给Client,就必须为Binder实体注册一个名字。 Client通过Service Manager获取服务:Client知道服务中Binder实体的名字后,通过名字从Service Manager获取Binder实体的引用。 Client使用服务与Server进行通信:Client通过调用Binder实体与Server进行通信。 更详细一点? Binder通信的实质是利用内存映射,将用户进程的内存地址和内核的内存地址映射为同一块物理地址,也就是说他们使用的同一块物理空间,每次创建Binder的时候大概分配128的空间。数据进行传输的时候,从这个内存空间分配一点,用完了再释放即可。 2. 序列化 Android有哪些序列化方式? 为了解决Android中内存序列化速度过慢的问题,Android使用了Parcelable。 对比 Serializable Parcelable 易用性 简单 不是很简单 效率 低 高 场景 IO、网络和数据库 内存中 3. Framework Zygote孕育进程过程? Activity的启动过程? 建议阅读: 《3分钟看懂Activity启动流程》 App的启动过程? 介绍一下App进程和System Server进程如何联系: ActivityThread:依赖于Ui线程,实际处理与AMS中交互的工作。 ActivityManagerService:负责Activity、Service等的生命周期工作。 ApplicationThread:System Server进程中ApplicatonThreadProxy的服务端,帮助System Server进程跟App进程交流。 System Server:Android核心的进程,掌管着Android系统中各种重要的服务。 具体过程: 用户点击App图标,Lanuacher进程通过Binder联系到System Server进程发起startActivity。 System Server通过Socket联系到Zygote,fork出一个新的App进程。 创建出一个新的App进程以后,Zygote启动App进程的ActivityThread#main()方法。 在ActivtiyThread中,调用AMS进行ApplicationThread的绑定。 AMS发送创建Application的消息给ApplicationThread,进而转交给ActivityThread中的H,它是一个Handler,接着进行Application的创建工作。 AMS以同样的方式创建Activity,接着就是大家熟悉的创建Activity的工作了。 Apk的安装过程? 建议阅读: 《Android Apk安装过程分析》 Activity启动过程跟Window的关系? 建议阅读: 《简析Window、Activity、DecorView以及ViewRoot之间的错综关系》 Activity、Window、ViewRoot和DecorView之间的关系? 建议阅读: 《总结UI原理和高级的UI优化方式》 4. Context 关于Context的理解? 建议阅读: 《Android Context 上下文 你必须知道的一切》 5. 断点续传 多线程断点续传? 基础知识: Http基础:在Http请求中,可以加入请求头Range,下载指定区间的文件数。 RandomAccessFile:支持随机访问,可以从指定位置进行数据的读写。 有了这个基础以后,思路就清晰了: 通过HttpUrlConnection获取文件长度。 自己分配好线程进行制定区间的文件数据的下载。 获取到数据流以后,使用RandomAccessFile进行指定位置的读写。 6. 性能优化 平时做了哪些性能优化? 建议阅读: 《Android 性能优化最佳实践》 7. 第三方库 一定要在熟练使用后再去查看原理。 Glide Glide考察的频率挺高的,常见的问题有: Glide和其他图片加载框架的比较? 如何设计一个图片加载框架? Glide缓存实现机制? Glide如何处理生命周期? ... 建议阅读: 《Glide最全解析》《面试官:简历上最好不要写Glide,不是问源码那么简单》 OkHttp OkHttp常见知识点: 责任链模式 interceptors和networkInterceptors的区别? 建议看一遍源码,过程并不复杂。 Retrofit Retrofit常见问题: 设计模式和封层解耦的理念 动态代理 建议看一遍源码,过程并不复杂。 RxJava RxJava难在各种操作符,我们了解一下大致的设计思想即可。 建议寻找一些RxJava的文章。 Android Jetpack(非必须) 我主要阅读了Android Jetpack中以下库的源码: Lifecycle:观察者模式,组件生命周期中发送事件。 DataBinding:核心就是利用LiveData或者Observablexxx实现的观察者模式,对16进制的状态位更新,之后根据这个状态位去更新对应的内容。 LiveData:观察者模式,事件的生产消费模型。 ViewModel:借用Activty异常销毁时存储隐藏Fragment的机制存储ViewModel,保证数据的生命周期尽可能的延长。 Paging:设计思想。 以后有时间再给大家做源码分析。 建议阅读: 《Android Jetpack源码分析系列》 8. 插件化和组件化 这个我基本没用过,等用过了,再和大家分享。 三、Java基础 Java基础中考察频率比较高的是Object、String、面向对象、集合、泛型和反射。 1. Object equals和==的区别?equals和hashcode的关系? ==:基本类型比较值,引用类型比较地址。 equals:默认情况下,equals作为对象中的方法,比较的是地址,不过可以根据业务,修改equals方法。 equals和hashcode之间的关系: 默认情况下,equals相等,hashcode必相等,hashcode相等,equals不是必相等。hashcode基于内存地址计算得出,可能会相等,虽然几率微乎其微。 2. String String、StringBuffer和StringBuilder的区别? String:String属于不可变对象,每次修改都会生成新的对象。 StringBuilder:可变对象,非多线程安全。 StringBuffer:可变对象,多线程安全。 大部分情况下,效率是:StringBuilder>StringBuffer>String。 3. 面向对象的特性 Java中抽象类和接口的特点? 共同点: 抽象类和接口都不能生成具体的实例。 都是作为上层使用。 不同点: 抽象类可以有属性和成员方法,接口不可以。 一个类只能继承一个类,但是可以实现多个接口。 抽象类中的变量是普通变量,接口中的变量是静态变量。 抽象类表达的是is-a的关系,接口表达的是like-a的关系。 关于多态的理解? 多态是面向对象的三大特性:继承、封装和多态之一。 多态的定义:允许不同类对同一消息做出响应。 多态存在的条件: 要有继承。 要有复写。 父类引用指向子类对象。 Java中多态的实现方式:接口实现,继承父类进行方法重写,同一个类中的方法重载。 4. 集合 HashMap的特点是什么?HashMap的原理? HashMap的特点: 基于Map接口,存放键值对。 允许key/value为空。 非多线程安全。 不保证有序,也不保证使用的过程中顺序不会改变。 简单来讲,核心是数组+链表/红黑树,HashMap的原理就是存键值对的时候: 通过键的Hash值确定数组的位置。 找到以后,如果该位置无节点,直接存放。 该位置有节点即位置发生冲突,遍历该节点以及后续的节点,比较key值,相等则覆盖。 没有就新增节点,默认使用链表,相连节点数超过8的时候,在jdk 1.8中会变成红黑树。 如果Hashmap中的数组使用情况超过一定比例,就会扩容,默认扩容两倍。 当然这是存入的过程,其他过程可以自行查阅。这里需要注意的是: key的hash值计算过程是高16位不变,低16位和高16位取抑或,让更多位参与进来,可以有效的减少碰撞的发生。 初始数组容量为16,默认不超过的比例为0.75。 5. 泛型 说一下对泛型的理解? 泛型的本质是参数化类型,在不创建新的类型的情况下,通过泛型指定不同的类型来控制形参具体限制的类型。也就是说在泛型的使用中,操作的数据类型被指定为一个参数,这种参数可以被用在类、接口和方法中,分别被称为泛型类、泛型接口和泛型方法。 泛型是Java中的一种语法糖,能够在代码编写的时候起到类型检测的作用,但是虚拟机是不支持这些语法的。 泛型的优点: 类型安全,避免类型的强转。 提高了代码的可读性,不必要等到运行的时候才去强制转换。 什么是类型擦除? 不管泛型的类型传入哪一种类型实参,对于Java来说,都会被当成同一类处理,在内存中也只占用一块空间。通俗一点来说,就是泛型只作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的信息擦除,也就是说,成功编译过后的class文件是不包含任何泛型信息的。 6. 反射 动态代理和静态代理 静态代理很简单,运用的就是代理模式: 声明一个接口,再分别实现一个真实的主题类和代理主题类,通过让代理类持有真实主题类,从而控制用户对真实主题的访问。 动态代理指的是在运行时动态生成代理类,即代理类的字节码在运行时生成并载入当前的ClassLoader。 动态代理的原理是使用反射,思路和上面的一致。 使用动态代理的好处: 不需要为RealSubject写一个形式完全一样的代理类。 使用一些动态代理的方法可以在运行时制定代理类的逻辑,从而提升系统的灵活性。 四、Java并发 Java并发中考察频率较高的有线程、线程池、锁、线程间的等待和唤醒、线程特性和阻塞队列等。 1. 线程 线程的状态有哪些(待修改)? 线程的状态有: new:新创建的线程 Ready:准备就绪的线程,由于CPU分配的时间片的关系,此时的任务不在执行过程中。 Running:正在执行的任务 Block:被阻塞的任务 Time Waiting:计时等待的任务 Terminated:终止的任务 附上一张状态转换的图: 线程中wait和sleep的区别? wait方法既释放cpu,又释放锁。 sleep方法只释放cpu,但是不释放锁。 线程和进程的区别? 线程是CPU调度的最小单位,一个进程中可以包含多个线程,在Android中,一个进程通常是一个App,App中会有一个主线程,主线程可以用来操作界面元素,如果有耗时的操作,必须开启子线程执行,不然会出现ANR,除此以外,进程间的数据是独立的,线程间的数据可以共享。 2. 线程池 线程池的地位十分重要,基本上涉及到跨线程的框架都使用到了线程池,比如说OkHttp、RxJava、LiveData以及协程等。 与新建一个线程相比,线程池的特点? 节省开销: 线程池中的线程可以重复利用。 速度快:任务来了就能开始,省去创建线程的时间。 线程可控:线程数量可空和任务可控。 功能强大:可以定时和重复执行任务。 线程池中的几个参数是什么意思,线程池的种类有哪些? 线程池的构造函数如下: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } 参数解释如下: corePoolSize:核心线程数量,不会释放。 maximumPoolSize:允许使用的最大线程池数量,非核心线程数量,闲置时会释放。 keepAliveTime:闲置线程允许的最大闲置时间。 unit:闲置时间的单位。 workQueue:阻塞队列,不同的阻塞队列有不同的特性。 线程池分为四个类型: CachedThreadPool:闲置线程超时会释放,没有闲置线程的情况下,每次都会创建新的线程。 FixedThreadPool:线程池只能存放指定数量的线程池,线程不会释放,可重复利用。 SingleThreadExecutor:单线程的线程池。 ScheduledThreadPool:可定时和重复执行的线程池。 线程池的工作流程? 图片来自《线程池是怎样工作的》 简而言之: 任务来了,优先考虑核心线程。 核心线程满了,进入阻塞队列。 阻塞队列满了,考虑非核心线程(图上好像少了这个过程)。 非核心线程满了,再触发拒绝任务。 3. 锁 死锁触发的四大条件? 互斥锁 请求与保持 不可剥夺 循环的请求与等待 synchronized关键字的使用?synchronized的参数放入对象和Class有什么区别? synchronized关键字的用法: 修饰方法 修饰代码块:需要自己提供锁对象,锁对象包括对象本身、对象的Class和其他对象。 放入对象和Class的区别是: 锁住的对象不同:成员方法锁住的实例对象,静态方法锁住的是Class。 访问控制不同:如果锁住的是实例,只会针对同一个对象方法进行同步访问,多线程访问同一个对象的synchronized代码块是串行的,访问不同对象是并行的。如果锁住的是类,多线程访问的不管是同一对象还是不同对象的synchronized代码块是都是串行的。 synchronized的原理? 任何一个对象都有一个monitor与之相关联,JVM基于进入和退出mointor对象来实现代码块同步和方法同步,两者实现细节不同: 代码块同步:在编译字节码的时候,代码块起始的地方插入monitorenter 指令,异常和代码块结束处插入monitorexit指令,线程在执行monitorenter指令的时候尝试获取monitor对象的所有权,获取不到的情况下就是阻塞 方法同步:synchronized方法在method_info结构有AAC_synchronized标记,线程在执行的时候获取对应的锁,从而实现同步方法 synchronized和Lock的区别? 主要区别: synchronized是Java中的关键字,是Java的内置实现;Lock是Java中的接口。 synchronized遇到异常会释放锁;Lock需要在发生异常的时候调用成员方法Lock#unlock()方法。 synchronized是不可以中断的,Lock可中断。 synchronized不能去尝试获得锁,没有获得锁就会被阻塞; Lock可以去尝试获得锁,如果未获得可以尝试处理其他逻辑。 synchronized多线程效率不如Lock,不过Java在1.6以后已经对synchronized进行大量的优化,所以性能上来讲,其实差不了多少。 悲观锁和乐观锁的举例?以及它们的相关实现? 悲观锁和乐观锁的概念: 悲观锁:悲观锁会认为,修改共享数据的时候其他线程也会修改数据,因此只在不会受到其他线程干扰的情况下执行。这样会导致其他有需要锁的线程挂起,等到持有锁的线程释放锁 乐观锁:每次不加锁,每次直接修改共享数据假设其他线程不会修改,如果发生冲突就直接重试,直到成功为止 举例: 悲观锁:典型的悲观锁是独占锁,有synchronized、ReentrantLock。 乐观锁:典型的乐观锁是CAS,实现CAS的atomic为代表的一系列类 CAS是什么?底层原理? CAS全称Compare And Set,核心的三个元素是:内存位置、预期原值和新值,执行CAS的时候,会将内存位置的值与预期原值进行比较,如果一致,就将原值更新为新值,否则就不更新。 底层原理:是借助CPU底层指令cmpxchg实现原子操作。 4. 线程间通信 notify和notifyAll方法的区别? notify随机唤醒一个线程,notifyAll唤醒所有等待的线程,让他们竞争锁。 wait/notify和Condition类实现的等待通知有什么区别? synchronized与wait/notify结合的等待通知只有一个条件,而Condition类可以实现多个条件等待。 5. 多线程间的特性 多线程间的有序性、可见性和原子性是什么意思? 原子性:执行一个或者多个操作的时候,要么全部执行,要么都不执行,并且中间过程中不会被打断。Java中的原子性可以通过独占锁和CAS去保证 可见性:指多线程访问同一个变量的时候,一个线程修改了变量的值,其他线程能够立刻看得到修改的值。锁和volatile能够保证可见性 有序性:程序执行的顺序按照代码先后的顺序执行。锁和volatile能够保证有序性 happens-before原则有哪些? Java内存模型具有一些先天的有序性,它通常叫做happens-before原则。 如果两个操作的先后顺序不能通过happens-before原则推倒出来,那就不能保证它们的先后执行顺序,虚拟机就可以随意打乱执行指令。happens-before原则有: 程序次序规则:单线程程序的执行结果得和看上去代码执行的结果要一致。 锁定规则:一个锁的lock操作一定发生在上一个unlock操作之后。 volatile规则:对volatile变量的写操作一定先行于后面对这个变量的对操作。 传递规则:A发生在B前面,B发生在C前面,那么A一定发生在C前面。 线程启动规则:线程的start方法先行发生于线程中的每个动作。 线程中断规则:对线程的interrupt操作先行发生于中断线程的检测代码。 线程终结原则:线程中所有的操作都先行发生于线程的终止检测。 对象终止原则:一个对象的初始化先行发生于他的finalize()方法的执行。 前四条规则比较重要。 volatile的原理? 可见性 如果对声明了volatile的变量进行写操作的时候,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写入到系统内存。 多处理器的环境下,其他处理器的缓存还是旧的,为了保证各个处理器一致,会通过嗅探在总线上传播的数据来检测自己的数据是否过期,如果过期,会强制重新将系统内存的数据读取到处理器缓存。 有序性 Lock前缀的指令相当于一个内存栅栏,它确保指令排序的时候,不会把后面的指令拍到内存栅栏的前面,也不会把前面的指令排到内存栅栏的后面。 6. 阻塞队列 通常的阻塞队列有哪几种,特点是什么? ArrayBlockQueue:基于数组实现的有界的FIFO(先进先出)阻塞队列。 LinkedBlockQueue:基于链表实现的无界的FIFO(先进先出)阻塞队列。 SynchronousQueue:内部没有任何缓存的阻塞队列。 PriorityBlockingQueue:具有优先级的无限阻塞队列。 ConcurrentHashMap的原理 数据结构的实现跟HashMap一样,不做介绍。 JDK 1.8之前采用的是分段锁,核心类是一个Segment,Segment继承了ReentrantLock,每个Segment对象管理若干个桶,多个线程访问同一个元素的时候只能去竞争获取锁。 JDK 1.8采用了CAS + synchronized,插入键值对的时候如果当前桶中没有Node节点,使用CAS方式进行更新,如果有Node节点,则使用synchronized的方式进行更新。 五、Jvm Jvm中考察频率较高的内容有:Jvm内存区域的划分、GC机制和类加载机制。 建议阅读: 《深入理解Java虚拟机》 1. Java内存模型 Jvm内存区域是如何划分的? 内存区域划分: 程序计数器:当前线程的字节码执行位置的指示器,线程私有。 Java虚拟机栈:描述的Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧,存储着局部变量、操作数栈、动态链接和方法出口等,线程私有。 本地方法栈:本地方法执行的内存模型,线程私有。 Java堆:所有对象实例分配的区域。 方法区:所有已经被虚拟机加载的类的信息、常量、静态变量和即时编辑器编译后的代码数据。 Jvm内存模型是怎么样的? Java规定所有变量的内存都需要存储在主内存。 每个线程都有自己的工作内存,线程中使用的所有变量以及对变量的操作都基于工作内存,工作内存中的所有变量都从主内存读取过来的。 不同线程间的工作内存无法进行直接交流,必须通过主内存完成。 主内存和工作内存之间的交互协议,即变量如何从主内存传递到工作内存、工作内存如何将变量传递到主内存,Java内存模型定义了8种操作来完成,并且每一种操作都是原子的,不可再分的。 类型 说明 lock 作用于主内存的变量,把一个变量标识一个线程独占的状态 unlock 作用于主内存的变量,把一个处于锁定状态的变量释放出来 read 把一个变量从主内存传输到工作内存,以便随后的load使用 load 把read操作读取的变量存储到工作内存的变量副本中 use 把工作内存中的变量的值传递给执行引擎,每当虚拟机执行到一个需要使用变量的字节码指令的时候都会执行这个操作 assign 把一个从执行引擎中接收到的变量赋值给工作内存中的变量,每当虚拟机遇到赋值的字节码指令都会执行这个操作 store 把工作内存中的一个变量的值传递给主内存,以便以后的write使用 write 把store传递过来的工作内存中的变量写入到主内存中的变量 String s1 = "abc"和String s2 = new String("abc")的区别,生成对象的情况 指向方法区:"abc"是常量,所以它会在方法区中分配内存,如果方法区已经给"abc"分配过内存,则s1会直接指向这块内存区域。 指向Java堆:new String("abc")是重新生成了一个Java实例,它会在Java堆中分配一块内存。 所以s1和s2的内存地址肯定不一样,但是内容一样。 2. GC机制 如何判断对象可回收? 判断一个对象可以回收通常采用的算法是引用几算法和可达性算法。由于互相引用导致的计数不好判断,Java采用的可达性算法。 可达性算法的思路是:通过一些列被成为GC Roots的对象作为起始点,自上往下从这些起点往下搜索,搜索所有走过的路径称为引用链,如果一个对象没有跟任何引用链相关联的时候,则证明该对象不可用,所以这些对象就会被判定为可以回收。 可以被当作GC Roots的对象包括: Java虚拟机栈中的引用的对象 方法区中静态属性引用的对象 方法区中常量引用的对象 本地方法中JNI引用的对象 GC的常用算法? 标记 - 清除:首先标记出需要回收的对象,标记完成后统一回收所有被标记的对象。容易产生碎片空间。 复制算法:它将可用的内存分为两块,每次只用其中的一块,当需要内存回收的时候,将存活的对象复制到另一块内存,然后将当前已经使用的内存一次性回收掉。需要浪费一半的内存。 标记 - 整理:让存活的对象向一端移动,之后清除边界外的内存。 分代搜集:根据对象存活的周期,Java堆会被分为新生代和老年代,根据不同年代的特性,选择合适的GC收集算法。 Minar GC和Full GC的区别? Minar GC:频率高、针对新生代。 Full GC:频率低、发生在老年代、通常会伴随一次Minar GC和速度慢。 说一下四种引用以及他们的区别? 强引用:强引用还在,垃圾搜集器就不会回收被引用的对象。 软引用:对于软引用关联的对象,在系统发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。 弱引用:被若引用关联的对象只能存活到下一次GC之前。 虚引用:为对象设置虚引用的目的仅仅是为了GC之前收到一个系统通知。 3. 类加载 类加载的过程? 类加载的过程可以分为: 加载:将类的全限定名转化为二进制流,再将二进制流转化为方法区中的类型信息,从而生成一个Class对象。 验证:对类的验证,包括格式、字节码、属性等。 准备:为类变量分配内存并设置初始值。 解析:将常量池的符号引用转化为直接引用。 初始化:执行类中定义的Java程序代码,包括类变量的赋值动作和构造函数的赋值。 使用 卸载 只有加载、验证、准备、初始化和卸载的这个五个阶段的顺序是确定的。 类加载的机制,以及为什么要这样设计? 类加载的机制是双亲委派模型。大部分Java程序需要使用的类加载器包括: 启动类加载器:由C++语言实现,负责加载Java中的核心类。 扩展类加载器:负责加载Java扩展的核心类之外的类。 应用程序类加载器:负责加载用户类路径上指定的类库。 双亲委派模型如下: 双亲委派模型要求出了顶层的启动类加载器之外,其他的类加载器都有自己的父加载器,通过组合实现。 双亲委派模型的工作流程: 当一个类加载的任务来临的时候,先交给父类加载器完成,父类加载器交给父父类加载器完成,知道传递给启动类加载器,如果完成不了的情况下,再依次往下传递类加载的任务。 这样设计的原因: 双亲委派模型能够保证Java程序的稳定运行,不同层次的类加载器具有不同优先级,所有的对象的父类Object,无论哪一个类加载器加载,最后都会交给启动类加载器,保证安全。 六、kotlin 建议阅读: 《Kotlin》实战 1. 基础 ==、===和equal的区别? ==和equal的作用相同,===比较内存地址 var和val的区别? var:可变引用,具有可读和可写权限,值可变,类型不可变 val:不可变引用,具有可读权限,值不可变,但是对象的属性可变 2. 函数 Kotlin中默认参数的作用以及原理? 作用:配合@JavaOverloads可以解决Java调用Kotlin函数重载的问题。 原理:Kotlin编译的默认参数是被编译到调用的函数中的,所以默认参数改变的时候,是需要重新编译这个函数的。 Kotlin中顶层函数的原理 顶层函数实质就是Java中的静态函数,可以通过Kotlin中的@Jvm:fileName自动生成对应的Java调用类名。 中缀函数是什么?注意点? 中缀函数需要是用infix关键字修饰,如downTo: public infix fun Int.downTo(to: Int): IntProgression { return IntProgression.fromClosedRange(this, to, -1) } 注意点是函数的参数只能有一个,函数的参与者只能有两个。 解构函数的本质? 解构声明将对象中的所有属性,解构成一组属性变量,而且这些变量可以单独使用,可以单数使用的原因是通过获取对应的component()方法对应着类中每个属性的值,这些属性的值被存储在局部变量中,所以解构声明的实质是局部变量。 扩展函数的本质? 扩展函数的本质就是对应Java中的静态函数,这个静态函数参数为接受者类型的对象,然后利用这个对象去访问对象中的属性和成员方法,最后返回这个对象的本身。 扩展函数和成员函数的区别? 实质不同:扩展函数实质是静态函数,是外部函数,成员函数是内部函数。 权限不同:扩展函数访问不了私有的属性和成员方法,成员函数可以。 继承:扩展函数不可复写,成员函数可以复写。 它们的使用方式类似。 3. 类、对象和接口 Kotlin中常用的类的修饰符有哪些? open:运行创建子类或者复写子类的方法。 final:不允许创建子类和复写子类的方法。 abstract:抽象类,必须复写子类的方法。 在Kotlin中,默认的类和方法的修饰符都是final的,如果想让类和方法能够被继承或者复写,需要显示的添加open修饰符。 Kotlin中可见性修饰符有哪些? public:所有地方可见 protected:子类中可见 private:类中可见 internal:模块中可见,一个模块就是一组一起编译的Kotlin文件 Java默认的访问权限是包访问权限,Kotlin中默认的访问权限是public。 Kotlin中的内部类和Java中的内部类有什么不同? Kotlin:默认相当于Java中的静态内部类,如果想访问类中的成员方法和属性,需要添加inner关键字修饰。 Java:默认持有外部类引用,可以访问成员方法和属性,如果想声明为静态内部类,需要添加static关键字修饰。 Kotlin属性代理背后原理? 可以简单理解为属性的settter、getter访问器内部实现交给了代理对象来实现,相当于使用一个代理对象代替了原来简单属性的读写过程,而暴露外部属性操作还是不变 的,照样是属性赋值和读取,只是setter、getter内部具体实现变了。 object和companion object的一些特点? 共同点: 定义单例的一种方式,提供静态成员和方法。 不同点: object:用来生成匿名内部类。 companion object:提供工厂方法,访问私有的构造方法。 4. lambda lambda表达式有几种? 普通表达式:()->R。 带接收者对象的表达式:T.()->R,可以访问接收者对象的属性和成员方法。如apply。 kotlin和Java内部类或者lambda表达式访问局部变量有什么不同? Java中的内部类:局部变量必须是final声明的,无法去修改局部变量的值。 Kotlin中lambda表达式:不要求final声明,对于非final修饰的lambda表达式,可以修改局部变量的值。 如果想在Java中的内部类修改外层局部变量的值,有两种方法:用数组包装或者提供包装类,Kotlin中lambda能够访问并修改局部变量的本质就是提供了一层包装类: class Ref<T>(var value:T) 修改局部变量的值就是修改value中的值。 使用lambda表达式访问的局部变量有什么不同? 默认情况下,局部变量的生命周期会被限制在声明这个变量的函数中,但是如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后执行。 class Apple { lateinit var num:(() -> Int) fun initCount(){ val count = 2 num = fun():Int{ return count * count } } fun res():Int{ return num() } } fun main(args: Array<String>) { val a = Apple() a.initCount() val res = a.res() println(res) } 如上面代码所示,局部变量count就被存储在lambda表达式中,最后通过Apple#res方法引用表达式。 原理:当你捕捉final变量的时候,它的值会和lambda代码一起存储。对于非final变量,它的值会被封装在一层包装器中,包装器的引用会和lambda代码一起被存储。 带来的问题:默认情况下,lambda表达式会生成匿名内部类,在非显示声明对象的情况下可以多次重用,但是如果捕获了局部变量,每次调用的时候都需要生成新的实例。 序列是什么?集合类和序列的操作符比较? Sequence(序列)是一种惰性集合,可以更高效地对元素进行链式操作,不需要创建额外的集合保存过程中产生的中间结果,简单来讲,就是序列中所有的操作都是按顺序应用在每一个元素中。比如: fun main(args: Array<String>) { val list = mutableListOf<String>("1","2","3","4","5","6","7","8","9") val l = list.asSequence() .filter { it.toCharArray()[0] < '4' } .map { it.toInt() * it.toInt() } .toList() } 对于上述序列中的"1",它会先执行filter,再执行map,之后再对"2"重复操作。除此以外,序列中所有的中间操作都是惰性的。 集合和序列操作符的比较: 集合类:map和filter方法是内联,不会生成匿名类的实例,但每次进行map和filter都会生成新的集合,当数据量大的时候,消耗的内存也比较大。 序列:map和fitler非内联,会生成匿名类实例,但不需要创建额外的集合保存中间操作的结果。 为什么要使用内联函数?内联函数的作用? 使用lambda表达式可能带来的开销: lambda表达式正常会被编译成匿名类。 正常情况下,使用lambda表达式至少会生成一个对象,如果很不幸的使用了局部变量,那么每次使用该lambda表达式都会生成一个新的对象,导致使用lambda的效率比不使用还要低。 使用内联函数可以减少运行时的开销。内联函数主要作用: 使用内联函数可以减少中间类和对象的创建,进而提升性能。主要原因是内联函数可以做到函数被使用的时候编译器不会生成函数调用的代码,而是使用函数实现的真实代码区替换每一次的调用。 结合reified实化类型参数,解决泛型类型运行时擦除的问题。 5. 类型系统 Kotlin中的基本数据类型的理解? 在Kotlin中,使用的时候是不区分基本类型的,统一如下: Int、Byte、Short、Long、Float、Double、Char和Boolean。 使用统一的类型并不意味着Kotlin中所有的基本类型都是引用类型,大多数情况下,对于变量、参数、返回类型和属性都会被编译成基本类型,泛型类会被编译成Java中的包装类,即引用类型。 只读集合和可变集合的区别? 在Kotlin中,集合会被分为两大类型,只读集合和可变集合。 只读集合:对集合只有读取权限。 可变集合:能够删除、新增、修改和读取元素。 但是有一点需要注意,只读集合不一定是不可变的,如果你使用的变量是只读集合,它可能是众多集合引用中的一个,任何一个集合引用都有可能是可变集合。 Array和IntArray的区别? Array<Int>相当于Java中的Integer[],IntArray对应Java中的int[]。 使用实化类型参数解决泛型擦除的原理是什么? 内联函数的原理是编译器把实现的字节码动态插入到每一次调用的地方。实化类型参数也正是基于这个原理,每次调用实化类型参数的函数的时候,编译器都知道此次作为泛型类型实参的具体类型,所以编译器每次调用的时候生成不同类型实参调用的字节码插入到调用点。 6. 协程 协程是什么?协程的有什么特点? Kotlin官方文档上说: 协程的本质是轻量级的线程。 为什么说它是轻量级的线程,因为从官方角度来讲,创建十万个协程没什么问题打印任务不会存在问题,创建十万个线程会造成内存问题,可能会造成内存溢出。但是这个对比有问题,因为协程本质上是基于Java的线程池的,你去用线程池创建十万个打印任务是不会造成内存溢出的。 从上面我们可以得出结果,协程就是基于线程实现的更上层的Api,只不过它可以用阻塞式的写法写出非阻塞式的代码,避免了大量的回调,核心就是协程可以帮我自动的切换线程。 协程的原理? 很多人都会讲,协程中处理耗时任务,协程会先挂起,执行完,再切回来。我在这就浅显的分析这两步。 挂起:协程挂起的时候会从挂起处将后面的代码封装成续体,协程挂起的时候,将挂起的任务根据调度器放到线程池中执行,会有一个线程监视任务的完成情况。 线程切回:监视线程看到任务结束以后,根据需要再切到指定的线程中(主线程or子线程),执行续体中剩余的代码。 详解请查看: 《Kotlin/JVM 协程实现原理》 七、网络 掌握网络知识其实是需要一个系统的过程,在时间充裕的情况下,建议还是系统化的学习。 高频网络知识有TCP、HTTP和HTTPS。 建议阅读: 《趣谈网络协议》《图解Http》 1. HTTP和HTTPS HTTP是哪一层的协议,常见的HTTP状态码有哪些,分别代表什么意思? HTTP协议是应用层的协议。 常见的HTTP状态码有: 类别 解释 1xx 请求已经接收,继续处理 2xx 服务器已经正确处理请求,比如200 3xx 重定向,需要做进一步的处理才能完成请求 4xx 服务器无法理解的请求,比如404,访问的资源不存在 5xx 服务器收到请求以后,处理错误 HTTP 1.1 和HTTP 2有什么区别? HTTP 2.0基于HTTP 1.1,与HTTP 2.0增加了: 二进制格式:HTTP 1.1使用纯文本进行通信,HTTP 2.0使用二进制进行传输。 Head压缩:对已经发送的Header使用键值建立索引表,相同的Header使用索引表示。 服务器推送:服务器可以进行主动推送 多路复用:一个TCP连接可以划分成多个流,每个流都会分配Id,客户端可以借助流和服务端建立全双工进行通信,并且流具有优先级。 HTTP和HTTPS有什么区别? 简单来说,HTTP和HTTPS的关系是这样的 HTTPS = HTTP + SSL/TLS 区别如下: HTTP作用于应用层,使用80端口,起始地址是http://,明文传输,消息容易被拦截,串改。 HTTPS作用域传输层,使用443端口,起始地址是https://,需要下载CA证书,传输的过程需要加密,安全性高。 SSL/TLS的握手过程? 这里借用《趣谈网络协议》的图片: HTTPS传输过程中是如何处理进行加密的?为什么有对称加密的情况下仍然需要进行非对称加密? 过程和上图类似,依次获取证书,公钥,最后生成对称加密的钥匙进行对称加密。 对称加密可以保证加密效率,但是不能解决密钥传输问题;非对称加密可以解决传输问题,但是效率不高。 2. TCP相关 TCP的三次握手过程,为什么需要三次,而不是两次或者四次? 只发送两次,服务端是不知道自己发送的消息能不能被客户端接收到。 因为TCP握手是三次,所以此时双方都已经知道自己发送的消息能够被对方收到,所以,第四次的发送就显得多余了。 TCP的四次挥手过程? 大致意思就是: Client:我要断开连接了 Server:我收到你的消息了 Server:我也要断开连接了 Client:收到你要断开连接的消息了 之后Client等待两个MSL(数据包在网络上生存的最长时间),如果服务端没有回消息就彻底断开了。 TCP和UDP有什么区别? TCP:基于字节流、面向连接、可靠、能够进行全双工通信,除此以外,还能进行流量控制和拥塞控制,不过效率略低 UDP:基于报文、面向无连接、不可靠,但是传输效率高。 总的来说,TCP适用于传输效率要求低,准确性要求高或要求有连接。而UDP适用于对准确性要求较低,传输效率要求较高的场景,比如语音通话、直播等。 TCP为什么是一种可靠的协议?如何做到流量控制和拥塞控制? TCP可靠:是因为可以做到数据包发送的有序、无差错和无重复。 流量控制:是通过滑动窗口实现的,因为发送发和接收方消息发送速度和接收速度不一定对等,所以需要一个滑动窗口来平衡处理效率,并且保证没有差错和有序的接收数据包。 拥塞控制:慢开始和拥塞避免、快重传和快恢复算法。这写算法主要是为了适应网络中的带宽而作出的调整。 八、设计模式 经常考察的设计模式不多,但是我们应该在平时业务中应该多多思考,用一些设计模式会不会更好。 建议阅读: 《Android 源码设计模式解析与实战》 1. 六大原则 设计模式的六大原则是: 单一职责:合理分配类和函数的职责 开闭原则:开放扩展,关闭修改 里式替换:继承 依赖倒置:面向接口 接口隔离:控制接口的粒度 迪米特:一个类应该对其他的类了解最少 2. 单例模式 单例模式被问到的几率很大,通常会问如下几种问题。 单例的常用写法有哪几种? 懒汉模式 public class SingleInstance { private static SingleInstance instance; private SingleInstance() {} public static synchronized SingleInstance getInstance() { if(instance == null) { instance = new SingleInstance(); } return instance; } } 该模式的主要问题是每次获取实例都需要同步,造成不必要的同步开销。 DCL模式 public class SingleInstance { private static SingleInstance instance; private SingleInstance() {} public static SingleInstance getInstance() { if(instance == null) { synchronized (SingleInstance.class) { if(instance == null) { instance = new SingleInstance(); } } } return instance; } } 高并发环境下可能会发生问题。 静态内部类单例 public class SingleInstance { private SingleInstance() {} public static SingleInstance getInstance() { return SingleHolder.instance; } private static class SingleHolder{ private static final SingleInstance instance = new SingleInstance(); } } 枚举单例 public enum SingletonEnum { INSTANCE } 优点:线程安全和反序列化不会生成新的实例 DCL模式会有什么问题? 对象生成实例的过程中,大概会经过以下过程: 为对象分配内存空间。 初始化对象中的成员变量。 将对象指向分配的内存空间(此时对象就不为null)。 由于Jvm会优化指令顺序,也就是说2和3的顺序是不能保证的。在多线程的情况下,当一个线程完成了1、3过程后,当前线程的时间片已用完,这个时候会切换到另一个线程,另一个线程调用这个单例,会使用这个还没初始化完成的实例。 解决方法是使用volatile关键字: public class SingleInstance { private static volatile SingleInstance instance; private SingleInstance() {} public static SingleInstance getInstance() { if(instance == null) { synchronized (SingleInstance.class) { if(instance == null) { instance = new SingleInstance(); } } } return instance; } } 3. 需要关注的设计模式 重点了解以下的几种常用的设计模式: 工厂模式和抽象工厂模式:注意他们的区别。 责任链模式:View的事件分发和OkHttp的调用过程都使用到了责任链模式。 观察者模式:重要性不言而喻。 代理模式:建议了解一下动态代理。 4. MVCMVPMVVM MVC、MVP和MVVM应该是设计模式中考察频率最高的知识点了,严格意义上来说,它们不能算是设计模式,而是框架。 MVC、MVP和MVVM是什么? 图片已有,不再给出 MVC:Model-View-Controller,是一种分层解偶的框架,Model层提供本地数据和网络请求,View层处理视图,Controller处理逻辑,存在问题是Controller层和View层的划分不明显,Model层和View层的存在耦合。 MVP:Model-View-Presenter,是对MVC的升级,Model层和View层与MVC的意思一致,但Model层和View层不再存在耦合,而是通过Presenter层这个桥梁进行交流。 MVVM:Model-View-ViewModel,不同于上面的两个框架,ViewModel持有数据状态,当数据状态改变的时候,会自动通知View层进行更新。 MVC和MVP的区别是什么? MVP是MVC的进一步解耦,简单来讲,在MVC中,View层既可以和Controller层交互,又可以和Model层交互;而在MVP中,View层只能和Presenter层交互,Model层也只能和Presenter层交互,减少了View层和Model层的耦合,更容易定位错误的来源。 MVVM和MVP的最大区别在哪? MVP中的每个方法都需要你去主动调用,它其实是被动的,而MVVM中有数据驱动这个概念,当你的持有的数据状态发生变更的时候,你的View你可以监听到这个变化,从而主动去更新,这其实是主动的。 ViewModel如何知道View层的生命周期? 事实上,如果你仅仅使用ViewModel,它是感知不了生命周期,它需要结合LiveData去感知生命周期,如果仅仅使用DataBinding去实现MVVM,它对数据源使用了弱引用,所以一定程度上可以避免内存泄漏的发生。 九、算法题 没什么好说的,Leetcode + 《剑指Offer》,着重记住一些解决问题的思路。 除此以外,你还得记住一些常用的算法:排序、反转链表、树的遍历和手写LruCache,这些都写不出来,就尴尬了。 如果你不想阅读书籍,可以参考一下这个Github,亲眼见证了从3k Star到34k Star,跪了: 【fucking-algorithm】:github.com/labuladong/… 十、简历 简历中最重要的是项目经历和技能掌握。 可能有的同学会说,我天天在公司拧螺丝,根本没什么东西可写。 所以我们在平时的工作中,不应该仅仅满足于写一些业务代码,而应该常常思考: 在结合的业务的情况下,我可以再做一点什么? 对于已经写完的代码,我还可以做哪一些优化? 最后其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2020年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。 【Android学习PDF+学习视频+面试文档+知识点笔记】 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】下载地址!作者:九心链接:https://juejin.im/post/5ea2aebce51d4546fd483065来源:掘金
阿里P7进阶系列学习视频教程:https://space.bilibili.com/474380680 同步更新ing!敬请持续关注! 第一章 移动架构师筑基必备Java技能 一、深入Java泛型 泛型的作用与定义通配符与嵌套泛型上下边界RxJava中泛型的使用分析 二、注解深入浅出 2.1 自定义注解 自定义注解与元注解注解参数与默认值 2.2 注解的使用 APT,编译时注解处理器插桩,编译后处理筛选反射,运行时动态获取注解信息 2.3 Retrofit中的注解 三、并发编程 3.1 线程共享和协作 CPU核心数,线程数,时间片轮转机制解读synchronized、Lock、volatile、ThreadLocal如何实现线程共享Wait,Notify/NotifyAll,Join方法如何实现线程间协作 3.2 站在巨人肩上操作CAS CAS的原理CAS带来的ABA问题之原子操作类的正确使用实战 3.3 仅会用线程池是不够的 Callbale、Future和FutureTask源码解读线程池底层实现分析线程池排队机制手写线程池实战Executor框架解读实战 3.4 Android AsyncTask原理解析 四、数据传输与序列化 4.1 Serializable原理 4.2 Parcelable接口原理解析 4.3 Json 五、Java虚拟机原理 5.1 垃圾回收器机制 对象存活及强、弱等各种引用辨析快速解读GC算法之标记-清除、复制及标记-整理算法正确姿势解读GC日志 5.2 内存分配策略 JVM栈桢及方法调用详解JMM,Java Memory Model 5.3 Dalvik虚拟机 六、反射与类加载 6.1 反射基本概念与Class 三种获取Class对象的方式获取构造器实例化对象与属性信息包信息和方法Hook技术动态编程 6.2 ClassLoader类加载器 动态代理模式Android Davilk与ARTPathClassLoader、DexClassLoader与BootClassLoader双亲委托机制 七、高效IO 7.1 Java IO 体系 装饰者模式InputStream与OutputStreamReader与Writer 7.2 File文件操作 FileChannel内存映射 7.3 IO操作Dex加密 移动架构师筑基必备Java技能视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=116549 第二章 Android框架体系架构 一、高级UI晋升 1.1 触摸事件分发机制 1.2 View渲染机制 1.2.1 onLayout与onMeasure 1.2.2 onDraw映射机制 1.3 常用View 1.3.1RecycleView 1.3.1.1 源码解析 1.3.1.2 布局管理器LayoutManager 1.3.1.3 条目装饰ItemDecoration 1.3.1.4 ViewHolder与回收复用机制 1.3.2 CardView 1.3.2.1 源码解析 1.3.2.2 圆角阴影实现原理 1.3.2.3 5.0以下阴影与边距的适配 1.3.3 ViewPager 1.3.3.1 加载机制与优化 1.3.3.2 与Fragment的结合 1.3.4 WebView 1.3.4.1 使用与原理 1.3.4.2 js与Java交互 1.3.4.3 多进程WebView使用实战 1.3.4.4 WebView和Native的通信框架手写实战 1.4 布局ViewGroup 1.4.1 ConstraintLayout 1.4.2 LinearLayout 1.4.3 RelativeLayout 1.4.4 FrameLayout 1.4.5 GridLayout 1.5 自定义View实战 1.5.1 Canvas与Paint高级使用 1.5.2 自定义属性与动画 1.5.3 自定义瀑布流实战 1.5.4 自定义流式布局 1.5.5 手机清屏动画 1.5.6 组合自定义View实战 1.5.7 继承自定义View实战 1.5.8 完全自定义view实战 二、Android组件内核 2.1 Activity与调用栈 2.1.1 四大启动模式与Intent Flag 2.1.2 APK启动流程与ActivityThread解析 2.1.3 Activity生命周期源码解析 2.1.4 实战Splash广告载入与延时跳转 2.2 Fragment的管理与内核 2.2.1 Fragment事务管理机制 2.2.2 Fragment转场动画 2.2.3 嵌套处理,ChildFragmentManager 2.3 Service 内核原理 2.3.1 start与bind区别与原理 2.3.2 自带工作线程的IntentService 2.3.3 前台服务与Notify 2.4 组件间通信方案 2.4.1 Activity和Fragment低耦通信设计 2.4.2 Android与Serivice通信 2.4.3 Intent数据传输与限制 2.4.4 ViewModel通信方案 2.4.5 事件总线EventBus源码解析 2.4.6 实战:自动感知生命周期事件总线LiveDataBus 三、大型项目必备IPC 3.1 Binder机制原理 3.1.1 AIDL配置文件 3.1.2 C/S架构Binder原理 3.1.3 Messager 3.1.4 实战告别繁琐的AIDL,进程通信框架原理与实现 3.2 其他IPC方式 3.2.1 Broadcast 3.2.2 ContentProvider 3.2.3 文件 3.2.4 Socket 3.2.5 共享内存与管道 四、数据持久化 4.1 Android文件系统 4.1.1 sdcard与内部存储 4.2 轻量级kv持久化 4.2.1 Shared Preference原理 4.2.2 微信MMKV原理与实现 4.2.2.1 MMAP内存映射 4.2.2.2 文件数据结构 4.2.2.3增量更新与全量更新 4.3 嵌入式Sqlite数据库 4.3.1 SqliteOpenHelper 4.3.2 Sqlite升级与数据迁移方案 4.3.3 实战注解ORM数据库框架 五、Framework内核解析 5.1 XMS内核管理 5.1.1 AMS 5.1.1 .1 Activity管理 5.1.1.2 实战插件化核心启动未安装Activity 5.1.2 WMS 5.1.2.1 Windows体系 5.1.2.2 悬浮窗工具实现 5.1.3 PackageMS面试锦囊 5.1.4 实战插件化框架原理与实现 5.2 Handler消息机制 5.2.1 Looper 5.2.2 Message链表与对象池 5.2.3 MessageQueue消息队列与epoll机制 5.3 布局加载与资源系统 5.3.1 LayoutManager加载布局流程 5.3.2 Resource与AssetManager 5.3.3实战海量网易云焕肤系统,加载外部APK资源 Android框架体系架构视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=116649 第三章 360°全方面性能调优 一、设计思想与代码质量优化 1.1 六大原则 (1)单一职责原则 (2)开闭原则 (3)里氏替换原则 (4)依赖倒置原则 (5)接口隔离原则 (6)迪米特法则 1.2 设计模式 1.2.1结构型模式 (1)桥接模式 (2)适配器模式 (3)装饰器模式 (4)代理模式 (5)组合模式 1.2.2创建型模式 (1)建造者模式 (2)单例模式 (3)抽象工厂模式 (4)工厂方法模式 (5)静态工厂模式 1.2.3行为型模式 (1)模板方法模式 (2)策略模式 (3)观察者模式 (4)责任链模式 (5)命令模式 (6)访问者模式 1.2.4实战设计模式解耦项目网络层框架 1.3 数据结构 1.3.1 线性表ArrayList 1.3.2 链表LinkedList 1.3.3 栈Stack 1.3.4 队列 (1)Queue (2)Deque (3)阻塞队列 1.3.5 Tree (1)平衡二叉树 (2)红黑树 1.3.6 映射表 (1)HashTable (2)HashMap (3)SparseArray (4)ArrayMap 1.4 算法 1.4.1 排序算法 (1)冒泡排序 (2)选择排序 (3)插入排序 (4)快速排序 (5)堆排序 (6)基数排序 1.4.2 查找算法 (1)折半查找 (2)二分查找 (3)树形查找 (4)hash查找 二、程序性能优化 2.1 启动速度与执行效率优化 2.1.1 冷暖热启动耗时检测与分析 2.1.2 启动黑白屏解决 2.1.3 卡顿分析 2.1.4 StickMode严苛模式 2.1.5 Systrace与TraceView工具 2.2 布局检测与优化 2.2.1 布局层级优化 2.2.2 过度渲染检测 2.2.3 Hierarchy Viewer与Layout Inspector工具 2.3 内存优化 2.3.1 内存抖动和内存泄漏 2.3.2 内存大户,Bitmap内存优化 2.3.3 Profile内存监测工具 2.3.4 Mat大对象与泄漏检测 2.4 耗电优化 2.4.1 Doze&Standby 2.4.2 Battery Historian 2.4.3 JobScheduler、WorkManager 2.5 网络传输与数据存储优化 2.5.1 google序列化工具protobuf 2.5.2 7z极限压缩 2.5.3使用webp图片 2.6 APK大小优化 2.6.1 APK瘦身 2.6.2 微信资源混淆原理 2.7 屏幕适配 三、开发效率优化 3.1 分布式版本控制系统Git 3.2自动化构建系统Gradle 3.2.1 Gradle与Android插件 3.2.2Transform API 3.2.3 自定义插件开发 3.2.4 插件实战 (1)多渠道打包 (1)发版自动钉钉 四、实战项目:全方位评测与解析腾讯新闻客户端性能 360°全方面性能调优视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=116643 第四章 设计思想解读开源框架 一、热修复设计 1.1 AOT/JIT、dexopt 与 dex2oat 1.2 CLASS_ISPREVERIFIED问题与解决 1.3 即时生效与重启生效热修复原理 1.4 Gradle自动补丁包生成 二、插件化框架解读 2.1 Class文件加载Dex原理 2.2 Android资源加载与管理 2.3 四大组件的加载与管理Activity、Service 2.4 so库的加载原理 2.5 Android系统服务的运行原理 三、组件化框架设计 3.1 组件化之集中式路由--阿里巴巴ARouter原理 3.2 APT技术自动生成代码与动态类加载 3.3 Java SPI机制实现组件服务调用 3.4 拦截器AOP编程(跳转前预处理--登录),路由参数传递与IOC注入 3.5 手写组件化式路由 四、图片加载框架 4.1 图片加载框架选型 4.1.1 Universal ImangeLoader、Glide、Picasso与Fresco 4.1.2 Glide 4.1.3 Picasso 4.1.4 Fresco 4.2 Glide原理分析 4.2.1 Glide的基本用法 4.2.2 从源码的角度理解Glide的执行流程上篇、下篇 4.2.3 深入探究Glide的缓存机制 4.2.4 玩转Glide的回调与监听 4.2.5 Glide强大的图片变换功能 4.2.6 探究Glide的自定义模块功能 4.2.7 实现带进度的Glide图片加载功能 4.2.8 带你全面了解Glide 4的用法 4.3 手写图片加载框架实战 五、网络访问框架设计 5.1 网络通信必备基础 5.1.1 Restful URL 5.1.2 HTTP协议& TCP/IP协议 5.1.3 SSL握手与加密 5.1.4 DNS解析 5.1.5 Socket通信原则 5.1.5.1 SOCKS代理 5.1.5.2 HTTP普通代理与隧道代理 5.2 OkHttp源码解读 5.2.1 Socket连接池复用机制 5.2.2 HTTP协议重定向与缓存处理 5.2.3 高并发请求队列:任务分发 5.2.4 责任链模式拦截器设计 5.3 Retrofit源码解析 六、RXJava响应式编程框架设计 6.1 链式调用 6.2 扩展的观察者模式 6.3 事件变换设计 6.4 Scheduler线程控制 七、IOC架构设计 7.1 依赖注入与控制反转 7.2 ButterKnife原理上篇、中篇、下篇 7.3 Dagger架构设计核心解密 八、Android架构组件Jetpack 8.1 LiveData原理 8.2 Navigation如何解决tabLayout问题 8.3 ViewModel如何感知View生命周期及内核原理 8.4 Room架构方式方法 8.5 dataBinding为什么能够支持MVVM 8.6 WorkManager内核揭秘 8.7 Lifecycles生命周期 设计思想解读开源框架视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=116640 第五章 NDK模块开发 一、NDK基础知识体系 1.1 C与C++ 1.1.1数据类型 1.1.2内存结构与管理 1.1.3预处理指令、Typedef别名 1.1.4结构体与共用体 1.1.5指针、智能指针、方法指针 1.1.6线程 1.1.7类 1.1.7.1函数、虚函数、纯虚函数与析构函数 1.1.7.2初始化列表 1.2JNI开发 1.2.1静态与动态注册 1.2.2方法签名、与Java通信 1.2.3本地引用与全局引用 1.3Native开发工具 1.3.1编译器、打包工具与分析器 1.3.2静态库与动态库 1.3.3CPU架构与注意事项 1.3.4构建脚本与构建工具 1.3.4.1Cmake 1.3.4.2Makefile 1.3.5交叉编译移植 1.3.4.2FFmpeg交叉编译 1.3.4.2X264、FAAC交叉编译 1.3.4.2解决所有移植问题 1.3.6AS构建NDK项目 1.4Linux编程 1.4.1Linux环境搭建,系统管理,权限系统和工具使用(vim等) 1.4.2Shell脚本编程 二、底层图片处理 2.1PNG/JPEG/WEBP图像处理与压缩 2.2微信图片压缩 2.3GIF合成原理与实现 三、音视频开发 3.1多媒体系统 3.1.1Camera与手机屏幕采集 3.1.2图像原始数据格式YUV420(NV21与YV12等) 3.1.3音频采集与播放系统 3.1.4编解码器MediaCodec 3.1.5MediaMuxer复用与MediaExtractor 3.2FFmpeg 3.2.1ffmpeg模块介绍 3.2.2音视频解码,音视频同步 3.2.3I帧,B帧,P帧解码原理 3.2.4x264视频编码与faac音频编码 3.2.5OpenGL绘制与NativeWindow绘制 3.3流媒体协议 3.3.1RTMP协议 3.3.2音视频通话P2P WebRtc 3.4音视频效果处理 3.4.1OpenGL ES滤镜开发之美颜效果 3.4.2抖音视频效果分析与实现 3.4.3音视频变速原理 3.5项目实战一:斗鱼直播app(用户端与主播端) 3.6实战项目二:抖音视频app 3.7缅怀音视频专家雷霄骅,音视频项目汇总 四、机器学习 4.1 Opencv 4.1.1图像预处理 4.1.1.1灰度化、二值化 4.1.1.2腐蚀与膨胀 4.1.2人脸检测 4.1.3身份证识别 NDK模块开发视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=116624 第六章 微信小程序 一、小程序介绍 背景与趋势 小程序技术方案 公众平台注册及配置 开发工具的使用 MINA框架架构剖析 应用程序配置详解 逻辑与界面分离架构 单向数据流 二、UI开发 复杂的页面布局 文字图片等内容的呈现 用户交互表单开发 对话框等交互元素开发 下拉刷新和上拉加载 图形与动画操作 页面之间的跳转过渡 用户界面事件处理 三、小程序项目实战 3.1 微信小程序的文件结构 —— 教程系列(1) 微信小程序的生命周期实例演示 —— 微信小程序教程系列(2) 微信小程序的动态修改视图层的数据 —— 微信小程序教程系列(3) 微信小程序如何新建页面 —— 微信小程序教程系列(4) 微信小程序的如何使用全局属性 —— 微信小程序教程系列(5) 微信小程序的页面跳转和参数传递 —— 微信小程序教程系列(6) 微信小程序标题栏和导航栏的设置 —— 微信小程序教程系列(7) 微信小程序的作用域和模块化 —— 微信小程序教程系列(8) 微信小程序视图层的数据绑定 —— 微信小程序教程系列(9) 微信小程序之wx:if视图层的条件渲染 —— 微信小程序教程系列(10) 微信小程序视图层的列表渲染 —— 微信小程序教程系列(11) 微信小程序视图层的模板 —— 微信小程序教程系列(12) 微信小程序之wxss —— 微信小程序教程系列(13) 微信小程序的网络请求 —— 微信小程序教程系列(14) 微信小程序的百度地图获取地理位置 —— 微信小程序教程系列(15) 微信小程序使用百度api获取天气信息 —— 微信小程序教程系列(16) 微信小程序获取系统日期和时间 —— 微信小程序教程系列(17) 微信小程序之上拉加载和下拉刷新 —— 微信小程序教程系列(18) 微信小程序之组件 —— 微信小程序教程系列(19) 微信小程序之微信登陆 —— 微信小程序教程系列(20) 微信小程序之顶部导航栏(选项卡)实例 —— 微信小程序实战系列(21) 微信小程序之加载更多(分页加载)实例 —— 微信小程序实战系列(22) 微信小程序之自定义轮播图实例 —— 微信小程序实战系列(23) 微信小程序之仿android fragment之可滑动的底部导航栏实例 —— 微信小程序实战系列(24) 微信小程序之登录页实例 —— 微信小程序实战系列(25) 微信小程序之自定义toast实例 —— 微信小程序实战系列(26) 微信小程序之自定义抽屉菜单(从下拉出)实例 —— 微信小程序实战系列(27) 微信小程序之自定义模态弹窗(带动画)实例 —— 微信小程序实战系列(28) 微信小程序之侧栏分类 —— 微信小程序实战商城系列(29) 微信小程序之仿淘宝分类入口 —— 微信小程序实战商城系列(30) 微信小程序之购物数量加减 —— 微信小程序实战商城系列(31) 微信小程序之商品属性分类 —— 微信小程序实战商城系列(32) 微信小程序之购物车 —— 微信小程序实战商城系列(33) 微信小程序视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=116624 第七章 Flutter 一、你好,Flutter 原生开发与跨平台技术 初识Flutter Flutter开发环境搭建 二、Flutter 编码语言Dart详解系列 Dart语法篇之基础语法(一) Dart语法篇之集合的使用与源码解析(二) Dart语法篇之集合操作符函数与源码分析(三) Dart语法篇之函数的使用(四) Dart语法篇之面向对象基础(五) Dart语法篇之面向对象继承和Mixins(六) Dart语法篇之类型系统与泛型(七)· 三、Flutter框架原理与使用技巧 widget控件详解:text,image,button 布局分析:Linear布局,弹性布局,流水布局 如何自定义View 动画/手势交互 多线程开发原理 网络请求原理 Flutter架构与原生代码的交互 实战发布自己的Flutter库 四、Flutter架构知识落地实现 干货集中营 gank app项目实战 WanAndroid API构建客户端项目实战 Flutter视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=121682 第八章 架构师炼成实战 一、架构设计 MVP、MVP与MVVM 模块化与组件化架构 二、 网上商城项目实战 三、新闻客户端项目实战 四、多格式播放器项目实战 五、 Gradle自动化项目实战 移动架构师视频学习地址:https://space.bilibili.com/474380680/channel/detail?cid=121680 第九章 数据结构与算法 从零开始学数据结构和算法(一)冒泡与选择排序 从零开始学数据结构和算法(二)线性表的链式存储结构 从零开始学数据结构和算法(三)栈与栈的应用栈 从零开始学数据结构和算法(四)哈希表的思想和二叉树入门 从零开始学数据结构和算法 (五) 分治法 (二分查找、快速排序、归并排序) 从零开始学数据结构和算法(六)二叉排序树 从零开始学数据结构和算法(七) huffman 树与 AVL 树 会夸奖别人是一种好的习惯,在云栖社区上看到有收获的文章,愿意点赞的人,一般都比其他人活的透彻。
一、概述[](http://gityuan.com/2019/07/13/flutter_animator/#一概述) 动画效果对于系统的用户体验非常重要,好的动画能让用户感觉界面更加顺畅,提升用户体验。 1.1 动画类型[](http://gityuan.com/2019/07/13/flutter_animator/#11-动画类型) Flutter动画大的分类来说主要分为两大类: 补间动画:给定初值与终值,系统自动补齐中间帧的动画 物理动画:遵循物理学定律的动画,实现了弹簧、阻尼、重力三种物理效果 在应用使用过程中常见动画模式: 动画列表或者网格:例如元素的添加或者删除操作; 转场动画Shared element transition:例如从当前页面打开另一页面的过渡动画; 交错动画Staggered animations:比如部分或者完全交错的动画。 1.2 类图[](http://gityuan.com/2019/07/13/flutter_animator/#12-类图) 核心类: Animation对象是整个动画中非常核心的一个类; AnimationController用于管理Animation; CurvedAnimation过程是非线性曲线; Tween补间动画 Listeners和StatusListeners用于监听动画状态改变。 AnimationStatus是枚举类型,有4个值; 取值 解释 dismissed 动画在开始时停止 forward 动画从头到尾绘制 reverse 动画反向绘制,从尾到头 completed 动画在结束时停止 1.3 动画实例[](http://gityuan.com/2019/07/13/flutter_animator/#13-动画实例) //[见小节2.1] AnimationController animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 1000)); Animation animation = Tween(begin: 0.0,end: 10.0).animate(animationController); animationController.addListener(() { setState(() {}); }); //[见小节2.2] animationController.forward(); 该过程说明: AnimationController作为Animation子类,在屏幕刷新时生成一系列值,默认情况下从0到1区间的取值。 Tween的animate()方法来自于父类Animatable,该方法返回的对象类型为_AnimatedEvaluation,而该对象最核心的工作就是通过value来调用Tween的transform(); 调用链: AnimationController.forward AnimationController.\_animateToInternal AnimationController.\_startSimulation Ticker.start() Ticker.scheduleTick() SchedulerBinding.scheduleFrameCallback() SchedulerBinding.scheduleFrame() ... Ticker.\_tick AnimationController.\_tick Ticker.scheduleTick 二、原理分析[](http://gityuan.com/2019/07/13/flutter_animator/#二原理分析) 2.1 AnimationController初始化[](http://gityuan.com/2019/07/13/flutter_animator/#21-animationcontroller初始化) [-> lib/src/animation/animation_controller.dart] AnimationController({ double value, this.duration, this.debugLabel, this.lowerBound = 0.0, this.upperBound = 1.0, this.animationBehavior = AnimationBehavior.normal, @required TickerProvider vsync, }) : _direction = _AnimationDirection.forward { _ticker = vsync.createTicker(_tick); //[见小节2.1.1] _internalSetValue(value ?? lowerBound); //[见小节2.1.3] } 该方法说明: AnimationController初始化过程,一般都设置duration和vsync初值; upperBound(上边界值)和lowerBound(下边界值)都不能为空,且upperBound必须大于等于lowerBound; 创建默认的动画方向为向前(_AnimationDirection.forward); 调用类型为TickerProvider的vsync对象的createTicker()方法来创建Ticker对象; TickerProvider作为抽象类,主要的子类有SingleTickerProviderStateMixin和TickerProviderStateMixin,这两个类的区别就是是否支持创建多个TickerProvider,这里SingleTickerProviderStateMixin为例展开。 2.1.1 createTicker[](http://gityuan.com/2019/07/13/flutter_animator/#211-createticker) [-> lib/src/widgets/ticker_provider.dart] mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider { Ticker _ticker; Ticker createTicker(TickerCallback onTick) { //[见小节2.1.2] _ticker = Ticker(onTick, debugLabel: 'created by $this'); return _ticker; } 2.1.2 Ticker初始化[](http://gityuan.com/2019/07/13/flutter_animator/#212-ticker初始化) [-> lib/src/scheduler/ticker.dart] class Ticker { Ticker(this._onTick, { this.debugLabel }) { } final TickerCallback _onTick; } 将AnimationControllerd对象中的_tick()方法,赋值给Ticker对象的_onTick成员变量,再来看看该_tick方法。 2.1.3 _internalSetValue[](http://gityuan.com/2019/07/13/flutter_animator/#213-_internalsetvalue) [-> lib/src/animation/animation_controller.dart ::AnimationController] void _internalSetValue(double newValue) { _value = newValue.clamp(lowerBound, upperBound); if (_value == lowerBound) { _status = AnimationStatus.dismissed; } else if (_value == upperBound) { _status = AnimationStatus.completed; } else { _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.forward : AnimationStatus.reverse; } } 根据当前的value值来初始化动画状态_status 2.2 forward[](http://gityuan.com/2019/07/13/flutter_animator/#22-forward) [-> lib/src/animation/animation_controller.dart ::AnimationController] TickerFuture forward({ double from }) { //默认采用向前的动画方向 _direction = _AnimationDirection.forward; if (from != null) value = from; return _animateToInternal(upperBound); //[见小节2.3] } _AnimationDirection是枚举类型,有forward(向前)和reverse(向后)两个值,也就是说该方法的功能是指从from开始向前滑动, 2.3 _animateToInternal[](http://gityuan.com/2019/07/13/flutter_animator/#23-_animatetointernal) [-> lib/src/animation/animation_controller.dart ::AnimationController] TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear }) { double scale = 1.0; if (SemanticsBinding.instance.disableAnimations) { switch (animationBehavior) { case AnimationBehavior.normal: scale = 0.05; break; case AnimationBehavior.preserve: break; } } Duration simulationDuration = duration; if (simulationDuration == null) { final double range = upperBound - lowerBound; final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0; //根据剩余动画的百分比来评估仿真动画剩余时长 simulationDuration = this.duration * remainingFraction; } else if (target == value) { //已到达动画终点,不再执行动画 simulationDuration = Duration.zero; } //停止老的动画[见小节2.3.1] stop(); if (simulationDuration == Duration.zero) { if (value != target) { _value = target.clamp(lowerBound, upperBound); notifyListeners(); } _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.completed : AnimationStatus.dismissed; _checkStatusChanged(); //当动画执行时间已到,则直接结束 return TickerFuture.complete(); } //[见小节2.4] return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale)); } 默认采用的是线性动画曲线Curves.linear。 2.3.1 AnimationController.stop[](http://gityuan.com/2019/07/13/flutter_animator/#231-animationcontrollerstop) void stop({ bool canceled = true }) { _simulation = null; _lastElapsedDuration = null; //[见小节2.3.2] _ticker.stop(canceled: canceled); } 2.3.2 Ticker.stop[](http://gityuan.com/2019/07/13/flutter_animator/#232-tickerstop) [-> lib/src/scheduler/ticker.dart] void stop({ bool canceled = false }) { if (!isActive) //已经不活跃,则直接返回 return; final TickerFuture localFuture = _future; _future = null; _startTime = null; //[见小节2.3.3] unscheduleTick(); if (canceled) { localFuture._cancel(this); } else { localFuture._complete(); } } 2.3.3 Ticker.unscheduleTick[](http://gityuan.com/2019/07/13/flutter_animator/#233-tickerunscheduletick) [-> lib/src/scheduler/ticker.dart] void unscheduleTick() { if (scheduled) { SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId); _animationId = null; } } 2.3.4 _InterpolationSimulation初始化[](http://gityuan.com/2019/07/13/flutter_animator/#234-_interpolationsimulation初始化) [-> lib/src/animation/animation_controller.dart ::_InterpolationSimulation] class _InterpolationSimulation extends Simulation { _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale) : _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond; final double _durationInSeconds; final double _begin; final double _end; final Curve _curve; } 该方法创建插值模拟器对象,并初始化起点、终点、动画曲线以及时长。这里用的Curve是线性模型,也就是说采用的是匀速运动。 2.4 _startSimulation[](http://gityuan.com/2019/07/13/flutter_animator/#24-_startsimulation) [-> lib/src/animation/animation_controller.dart] TickerFuture _startSimulation(Simulation simulation) { _simulation = simulation; _lastElapsedDuration = Duration.zero; _value = simulation.x(0.0).clamp(lowerBound, upperBound); //[见小节2.5] final TickerFuture result = _ticker.start(); _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.forward : AnimationStatus.reverse; //[见小节2.4.1] _checkStatusChanged(); return result; } 2.4.1 _checkStatusChanged[](http://gityuan.com/2019/07/13/flutter_animator/#241-_checkstatuschanged) [-> lib/src/animation/animation_controller.dart] void _checkStatusChanged() { final AnimationStatus newStatus = status; if (_lastReportedStatus != newStatus) { _lastReportedStatus = newStatus; notifyStatusListeners(newStatus); //通知状态改变 } } 这里会回调_statusListeners中的所有状态监听器,这里的状态就是指AnimationStatus的dismissed、forward、reverse以及completed。 2.5 Ticker.start[](http://gityuan.com/2019/07/13/flutter_animator/#25-tickerstart) [-> lib/src/scheduler/ticker.dart] TickerFuture start() { _future = TickerFuture._(); if (shouldScheduleTick) { scheduleTick(); //[见小节2.6] } if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index && SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index) _startTime = SchedulerBinding.instance.currentFrameTimeStamp; return _future; } 此处的shouldScheduleTick等于!muted && isActive && !scheduled,也就是没有调度过的活跃状态才会调用Tick。 2.6 Ticker.scheduleTick[](http://gityuan.com/2019/07/13/flutter_animator/#26-tickerscheduletick) [-> lib/src/scheduler/ticker.dart] void scheduleTick({ bool rescheduling = false }) { //[见小节2.7] _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling); } 此处的_tick会在下一次vysnc触发时回调执行,见小节2.10。 2.7 scheduleFrameCallback[](http://gityuan.com/2019/07/13/flutter_animator/#27-scheduleframecallback) [-> lib/src/scheduler/binding.dart] int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) { //[见小节2.8] scheduleFrame(); _nextFrameCallbackId += 1; _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling); return _nextFrameCallbackId; } 将前面传递过来的Ticker._tick()方法保存在_FrameCallbackEntry的callback中,然后将_FrameCallbackEntry记录在Map类型的_transientCallbacks, 2.8 scheduleFrame[](http://gityuan.com/2019/07/13/flutter_animator/#28-scheduleframe) [-> lib/src/scheduler/binding.dart] void scheduleFrame() { if (_hasScheduledFrame || !_framesEnabled) return; ui.window.scheduleFrame(); _hasScheduledFrame = true; } 从文章Flutter之setState更新机制,可知此处调用的ui.window.scheduleFrame(),会注册vsync监听。当当下一次vsync信号的到来时会执行handleBeginFrame()。 2.9 handleBeginFrame[](http://gityuan.com/2019/07/13/flutter_animator/#29-handlebeginframe) [-> lib/src/scheduler/binding.dart:: SchedulerBinding] void handleBeginFrame(Duration rawTimeStamp) { Timeline.startSync('Frame', arguments: timelineWhitelistArguments); _firstRawTimeStampInEpoch ??= rawTimeStamp; _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp); if (rawTimeStamp != null) _lastRawTimeStamp = rawTimeStamp; ... //此时阶段等于SchedulerPhase.idle; _hasScheduledFrame = false; try { Timeline.startSync('Animate', arguments: timelineWhitelistArguments); _schedulerPhase = SchedulerPhase.transientCallbacks; //执行动画的回调方法 final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks; _transientCallbacks = <int, _FrameCallbackEntry>{}; callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { if (!_removedIds.contains(id)) _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack); }); _removedIds.clear(); } finally { _schedulerPhase = SchedulerPhase.midFrameMicrotasks; } } 该方法主要功能是遍历_transientCallbacks,从前面小节[2.7],可知该过程会执行Ticker._tick()方法。 2.10 Ticker._tick[](http://gityuan.com/2019/07/13/flutter_animator/#210-ticker_tick) [-> lib/src/scheduler/ticker.dart] void _tick(Duration timeStamp) { _animationId = null; _startTime ??= timeStamp; //[见小节2.11] _onTick(timeStamp - _startTime); //根据活跃状态来决定是否再次调度 if (shouldScheduleTick) scheduleTick(rescheduling: true); } 该方法主要功能: 小节[2.1.2]的Ticker初始化中,可知此处_onTick便是AnimationController的_tick()方法; 小节[2.5]已介绍当仍处于活跃状态,则会再次调度,回到小节[2.6]的scheduleTick(),从而形成动画的连续绘制过程。 2.11 AnimationController._tick[](http://gityuan.com/2019/07/13/flutter_animator/#211-animationcontroller_tick) [-> lib/src/animation/animation_controller.dart] void _tick(Duration elapsed) { _lastElapsedDuration = elapsed; //获取已过去的时长 final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond; _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound); if (_simulation.isDone(elapsedInSeconds)) { _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.completed : AnimationStatus.dismissed; stop(canceled: false); //当动画已完成,则停止 } notifyListeners(); //通知监听器[见小节2.11.1] _checkStatusChanged(); //通知状态监听器[见小节2.11.2] } 2.11.1 notifyListeners[](http://gityuan.com/2019/07/13/flutter_animator/#2111-notifylisteners) [-> lib/src/animation/listener_helpers.dart ::AnimationLocalListenersMixin] void notifyListeners() { final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners); for (VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) listener(); } catch (exception, stack) { ... } } } AnimationLocalListenersMixin的addListener()会向_listeners中添加监听器 2.11.2 _checkStatusChanged[](http://gityuan.com/2019/07/13/flutter_animator/#2112-_checkstatuschanged) [-> lib/src/animation/listener_helpers.dart ::AnimationLocalStatusListenersMixin] void notifyStatusListeners(AnimationStatus status) { final List<AnimationStatusListener> localListeners = List<AnimationStatusListener>.from(_statusListeners); for (AnimationStatusListener listener in localListeners) { try { if (_statusListeners.contains(listener)) listener(status); } catch (exception, stack) { ... } } } 从前面的小节[2.4.1]可知,当状态改变时会调用notifyStatusListeners方法。AnimationLocalStatusListenersMixin的addStatusListener()会向_statusListeners添加状态监听器。 三、总结[](http://gityuan.com/2019/07/13/flutter_animator/#三总结) 3.1 动画流程图[](http://gityuan.com/2019/07/13/flutter_animator/#31-动画流程图) 推荐阅读:腾讯技术团队整理,万字长文轻松彻底入门 Flutter,秒变大前端 2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中) 原文作者:gityuan原文链接:http://gityuan.com/2019/07/13/flutter_animator/
序言 本文主要介绍校招,疫情原因今年的春招持续的时间拉长了,截止到目前我已经面试超过一百位应届生,批改过超过150份笔试的试卷,因为通过率较低,我也被同事吐槽许多次让我“手下留情”。今天我就想聊一聊,我自己的面试标准。这不是一篇什么攻略文,旨在抛砖引玉,大家一起探讨如何面试更有效率。 需要写在前面的是,“平等“和”高效”一直都是互斥的。信息从一个人传递到另一个人那里,由于表达能力和接收能力的限制,必然产生信息熵,所以花一天的时间好好了解一个人的技术实力和学习能力,自然要比一场半小时的面试要更加的全面。但很可惜的是,现实中没有那么长的时间,所以一个面试官最基本的任务,就是引导面试者在最有限的时间里,最大化展示出自己的能力。 简历原则 最近看到很多技术大牛放出来自己的简历,但就我个人而言,感觉参考意义真的不大——这些大牛不管简历写成啥样,模板用啥,哪怕是TXT格式不做排版,就光看项目内容和技术深度就足够拿到offer了,但是正常人来说并没有这种“底蕴”,看完可能只会觉得“握草牛逼啊”,然后自己写简历时候仍然一脸懵逼。其实应届生做简历真没那么复杂,能够突出的无非是: GPA、算法奖项名次、奖学金之类的信息等。实习经历:在哪里,做了什么,取得了什么成绩,解决了什么问题。有量化标准更好,比如说“降低了10%内存占用”这种。个人项目、Github等 笔试阅卷原则 很多人好奇,笔试不是都固定答案吗?那有啥可原则不原则的? 是的,有些题是有固定答案,但也有一些问题答案相对开放。比如说这样一个常见的#### 笔试题 简述Activity(或Service,ContentProvider)的使用。像这样的相对开放的问题,基本上是懂多少答多深,对于这种问题我个人的评分标准是这样的: 基本用法(Manifest配置、生命周期简述、重要API等)介绍没有错的话,至少给到总分85%特别有介绍到源码层面,或者运行机制之类的,会给到满分。如果答案中有错误的地方,会反而酌情扣分,所以基本上靠量取胜反而有更大风险,因为错误的内容也可能更多。这些标准中可能争议比较大的就是“答错内容扣分”这一点,但是我仍然认为这是必要的。比如对于Service,有人会写“由于也是运行在主线程,不能做任何耗时操作”,我认为这是需要扣分的,原因如下: “Service运行在主线程”是论据,“不能做任何耗时操作”是论点,但中间省略了论证过程,论据怎么推导出论点的?我感觉这是逻辑能力较差的一种表现。默认运行在应用进程的Service,可不可以指定Service的process将它定义在子进程里?Service在子进程里做耗时操作比如网络请求之类的,为何不可以呢?如果可以,为何要说“不能做任何耗时操作”呢?我个人会感觉这个面试者实际并没有使用过,只是懂得书上的理论知识。当然,只写一些基本用法拿到85%分的人,可能也有一些逻辑短板,也有一些技术能力不足,但像我上文说的,效率与公正无法兼得,并且隐藏自己短板也算是一种能力。 面试注意点 在面试的过程中,我印象最深的几点感受一定要告知大家: 1、回答问题不要过于着急,一定要耐心等待面试官把问题说完 2、回答问题要有逻辑、干练简洁 3、如果面试官打断你说话,此时一定要谨慎回答,因为很有可能你回答过于繁琐且他对你当下的回答不满意 4、一个问题不要纠结很久,尤其是让面试官感受到你在敲键盘..... 5、不会的面试题必须干脆利落的回答不会 6、面试的时间最好控制在 30 ~ 40 分钟左右,这样互相之间的体验不会很差 7、面试是一个挖掘面试者能力和潜力的过程 8、面试官不是全能的,面试一定是一个互相学习的过程 9、一定要提前准备好自己想要问的问题,最致命的是别人把你安排的明明白白结果你对别人一无所知..... 关于第 2 点还是要说明一下,很多面试者回答问题没有逻辑性,在回答之前可以先思考一下,然后告诉面试官将从 n 个方面进行讲解,首先第 1 个方面是...,其次第 2 个方面是... 友情提示:面试的时候发现大部分的面试者普遍存在 1 和 2 两个问题。如果你的回答没有逻辑还繁琐且没有命中要点,通常面试官对你的印象会非常差。相反,如果你回答问题过于简洁,通常情况下面试官会觉得你没 Get 到他的问题点(当然会怀疑自己的表述有没有问题),一般都会追加更详细的问题描述,毕竟这是一个挖掘的过程。 面试题解析 一般我在面试开始前,会根据应聘者的简历提前准备 8 道左右的面试题(在面试的过程中可能会有调整)。 接下来我会重点讲解一些面试题,面试题解答思路(可从答案解析PDF中找到答案),供大家参考。 第一章 Java 知识点汇总 JVM JVM 工作流程 运行时数据区(Runtime Data Area) 方法指令 类加载器 垃圾回收 gc 对象存活判断 垃圾收集算法 垃圾收集器 内存模型与回收策略 Object equals 方法 hashCode 方法 static final String、StringBuffer、StringBuilder 异常处理 内部类 匿名内部类 多态 抽象和接口 集合框架 HashMap 结构图 HashMap 的工作原理 HashMap 与 HashTable 对比 ConcurrentHashMap Base 1.7 Base 1.8 ArrayList LinkedList CopyOnWriteArrayList 反射 单例 饿汉式 双重检查模式 静态内部类模式 线程 状态 状态控制 volatile synchronized 根据获取的锁分类 原理 Lock 锁的分类 悲观锁、乐观锁 自旋锁、适应性自旋锁 死锁 引用类型 动态代理 元注解 答案解析 2020年Android开发最新全套面试题答案解析 第二章 Android 知识点汇总 Activity 生命周期 启动模式 启动过程 Fragment 特点 生命周期 与Activity通信 Service 启动过程 绑定过程 生命周期 启用前台服务 BroadcastReceiver 注册过程 ContentProvider 基本使用 数据存储 View MeasureSpec MotionEvent VelocityTracker GestureDetector Scroller View 的滑动 View 的事件分发 在 Activity 中获取某个 View 的宽高 Draw 的基本流程 自定义 View 进程 进程生命周期 多进程 进程存活 OOM_ADJ 进程被杀情况 进程保活方案 Parcelable 接口 使用示例 方法说明 Parcelable 与 Serializable 对比 IPC IPC方式 Binder AIDL 通信 Messenger Window / WindowManager Window 概念与分类 Window 的内部机制 Window 的创建过程 Activity 的 Window 创建过程 Dialog 的 Window 创建过程 Toast 的 Window 创建过程 Bitmap 配置信息与压缩方式 常用操作 裁剪、缩放、旋转、移动 Bitmap与Drawable转换 保存与释放 图片压缩 BitmapFactory Bitmap创建流程 Option类 基本使用 内存回收 屏幕适配 单位 头条适配方案 刘海屏适配 Context SharedPreferences 获取方式 getPreferences getDefaultSharedPreferences getSharedPreferences 架构 apply / commit 注意 消息机制 Handler 机制 工作原理 ThreadLocal MessageQueue Looper Handler 线程异步 AsyncTask 基本使用 工作原理 HandlerThread IntentService 线程池 RecyclerView 优化 Webview 基本使用 WebView WebSettings WebViewClient WebChromeClient Webview 加载优化 内存泄漏 答案解析 第三章 Android 扩展知识点汇总 ART ART 功能 预先 (AOT) 编译 垃圾回收优化 开发和调试方面的优化 ART GC Apk 包体优化 Apk 组成结构 整体优化 资源优化 代码优化 .arsc文件优化 lib目录优化 Hook 基本流程 使用示例 Proguard 公共模板 常用的自定义混淆规则 aar中增加独立的混淆配置 检查混淆和追踪异常 架构 MVC MVP MVVM Jetpack 架构 使用示例 NDK 开发 JNI 基础 数据类型 String 字符串函数操作 常用 JNI 访问 Java 对象方法 NDK 开发 基础开发流程 System.loadLibrary() CMake 构建 NDK 项目 常用的 Android NDK 原生 API 类加载器 双亲委托模式 DexPathList 2020年Android开发最新全套面试题答案解析 第四章 Android 开源库源码分析 LeakCanary 初始化注册 引用泄漏观察 Dump Heap EventBus 自定义注解 注册订阅者 发送事件 第五章设计模式汇总 设计模式分类 面向对象六大原则 工厂模式 单例模式 建造者模式 原型模式 适配器模式 观察者模式 代理模式 责任链模式 策略模式 备忘录模式 答案解析 第六章计算机网络基础 网络体系的分层结构 HTTP 相关 请求报文 请求行 请求头 响应报文 常见状态码 缓存机制 Https Http 2.0 TCP/IP 三次握手 四次挥手 TCP 与 UDP 的区别 Socket 使用示例 **答案解析** 2020年Android开发最新全套面试题答案解析 第七章 常见面试算法题汇总 排序 比较排序 冒泡排序 归并排序 快速排序 线性排序 计数排序 桶排序 二叉树 顺序遍历 层次遍历 左右翻转 最大值 最大深度 最小深度 平衡二叉树 链表 删除节点 翻转链表 中间元素 判断是否为循环链表 合并两个已排序链表 链表排序 删除倒数第N个节点 两个链表是否相交 栈 / 队列 带最小值操作的栈 有效括号 用栈实现队列 逆波兰表达式求值 二分 二分搜索 X的平方根 哈希表 两数之和 连续数组 最长无重复字符的子串 最多点在一条直线上 堆 / 优先队列 前K大的数 前K大的数II 第K大的数 二叉搜索树 验证二叉搜索树 第K小的元素 数组 / 双指针 加一 删除元素 删除排序数组中的重复数字 我的日程安排表 I 合并排序数组 贪心 买卖股票的最佳时机 买卖股票的最佳时机 II 最大子数组 主元素 字符串处理 生成括号 Excel表列标题 翻转游戏 翻转字符串中的单词 转换字符串到整数 最长公共前缀 回文数 动态规划 单词拆分 爬楼梯 打劫房屋 编辑距离 乘积最大子序列 矩阵 螺旋矩阵 判断数独是否合法 旋转图像 二进制 / 位运算 落单的数 格雷编码 其他 反转整数 LRU缓存策略 **答案解析** 第八章 Kotlin 相关知识点 from-java-to-kotlin kotlin_tips 从原理分析Kotlin的延迟初始化: lateinit var和by lazy 使用Kotlin Reified 让泛型更简单安全 Kotlin里的Extension Functions实现原理分析 Kotlin系列之顶层函数和属性 Kotlin 兼容 Java 遇到的最大的 “坑” Kotlin 的协程用力瞥一眼 Kotlin 协程「挂起」的本质 到底什么是「非阻塞式」挂起?协程真的更轻量级吗? 资源混淆是如何影响到Kotlin协程的 Kotlin Coroutines(协程) 完全解析 答案解析 第九章 Flutter 相关知识点汇总 Flutter原理与实践 揭秘Flutter Hot Reload(原理篇) Flutter 动态化探索 Flutter如何和Native通信-Android视角 深入理解Flutter Platform Channel Flutter Engine 编译指北 Flutter Engine 线程模型 深入理解Flutter多线程 Flutter状态管理 - 初探与总结 Flutter | 状态管理指南篇——Provider 深入理解Flutter应用启动 Flutter渲染机制—UI线程 Flutter渲染机制—GPU线程 深入理解Flutter应用启动 深入理解setState更新机制 深入理解Flutter消息机制 深入理解Flutter动画原理 Dart虚拟机运行原理 源码解读Flutter tools机制 源码解读Flutter run机制 答案解析 2020年Android开发最新全套面试题答案解析 最后 题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。 我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等录播视频免费分享出来。[Android学习PDF+学习视频+面试文档+知识点笔记] 点击 https://shimo.im/docs/Q6V8xPVxHpkrtRtD 即可免费领取!希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展~
作者:Focusing 链接:https://juejin.im/post/5c984e926fb9a070c975a9b4 1、如何进行单元测试,如何保证App稳定 ? 参考回答:要测试Android应用程序,通常会创建以下类型自动单元测试: 本地测试:只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如Google推荐的Mockito; Android官网-建立本地单元测试(https://developer.android.com/training/testing/unit-testing/local-unit-tests.html) 检测测试:真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式; Android官网-建立仪表单元测试(https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html) 注意:单元测试不适合测试复杂的UI交互事件 推荐文章:Android 单元测试只看这一篇就够了(https://juejin.im/post/5b57e3fbf265da0f47352618) App的稳定主要决定于整体的系统架构设计,同时也不可忽略代码编程的细节规范,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似无关紧要的代码片段可能会带来整体软件系统的崩溃,所以上线之前除了自己本地化测试之外还需要进行Monkey压力测试。 少部分面试官可能会延伸,如Gradle自动化测试、机型适配测试等 2、Android中如何查看一个对象的回收情况 ? 参考回答:首先要了解Java四种引用类型的场景和使用(强引用、软引用、弱引用、虛引用) 举个场景例子:SoftReference对象是用来保存软引用的,但它同时也是一个Java对象,所以当软引用对象被回收之后,虽然这个SoftReference对象的get方法返回null,但SoftReference对象本身并不是null,而此时这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄露。 因此,Java提供ReferenceQueue来处理引用对象的回收情况。当SoftReference所引用的对象被GC后,JVM会先将softReference对象添加到ReferenceQueue这个队列中。当我们调用ReferenceQueue的poll()方法,如果这个队列中不是空队列,那么将返回并移除前面添加的那个Reference对象。 推荐文章:Java中的四种引用类型:强引用、软引用、弱引用和虚引用(https://segmentfault.com/a/1190000015282652#articleHeader3) 3、Apk的大小如何压缩 ? 参考回答:一个完整APK包含以下目录(将APK文件拖到Android Studio): META-INF/:包含CERT.SF和CERT.RSA签名文件以及MANIFEST.MF 清单文件。 assets/:包含应用可以使用AssetManager对象检索的应用资源。 res/:包含未编译到的资源 resources.arsc。 lib/:包含特定于处理器软件层的编译代码。该目录包含了每种平台的子目录,像armeabi,armeabi-v7a, arm64-v8a,x86,x86_64,和mips。 resources.arsc:包含已编译的资源。该文件包含res/values/ 文件夹所有配置中的XML内容。打包工具提取此XML内容,将其编译为二进制格式,并将内容归档。此内容包括语言字符串和样式,以及直接包含在*resources.arsc8文件中的内容路径 ,例如布局文件和图像。 classes.dex:包含以Dalvik / ART虚拟机可理解的DEX文件格式编译的类。 AndroidManifest.xml:包含核心Android清单文件。该文件列出应用程序的名称,版本,访问权限和引用的库文件。该文件使用Android的二进制XML格式。 lib、class.dex和res占用了超过90%的空间,所以这三块是优化Apk大小的重点(实际情况不唯一) 减少res,压缩图文文件:图片文件压缩是针对jpg和png格式的图片。我们通常会放置多套不同分辨率的图片以适配不同的屏幕,这里可以进行适当的删减。在实际使用中,只保留一到两套就足够了(保留一套的话建议保留xxhdpi,两套的话就加上hdpi),然后再对剩余的图片进行压缩(jpg采用优图压缩,png尝试采用pngquant压缩) 减少dex文件大小: 添加资源混淆 shrinkResources为true表示移除未引用资源,和代码压缩协同工作。 minifyEnabled为true表示通过ProGuard启用代码压缩,配合proguardFiles的配置对代码进行混淆并移除未使用的代码。 代码混淆在压缩apk的同时,也提升了安全性。 推荐文章:Android混淆最佳实践(https://www.jianshu.com/p/cba8ca7fc36d) 减少lib文件大小:由于引用了很多第三方库,lib文件夹占用的空间通常都很大,特别是有so库的情况下。很多so库会同时引入armeabi、armeabi-v7a和x86这几种类型,这里可以只保留armeabi或armeabi-v7a的其中一个就可以了,实际上微信等主流app都是这么做的。 只需在build.gradle直接配置即可,NDK配置同理 推荐文章:APK瘦身(https://www.jianshu.com/p/5921e9561f5f) 4、如何通过Gradle配置多渠道包? 参考回答:首先要了解设置多渠道的原因。在安装包中添加不同的标识,配合自动化埋点,应用在请求网络的时候携带渠道信息,方便后台做运营统计,比如说统计我们的应用在不同应用市场的下载量等信息 这里以友盟统计为例: 首先在manifest.xml文件中设置动态渠道变量: 接着在app目录下的build.gradle中配置productFlavors,也就是配置打包的渠道: 最后在编辑器下方的Teminal输出命令行: 执行./gradlew assembleRelease ,将会打出所有渠道的release包; 执行./gradlew assembleVIVO,将会打出VIVO渠道的release和debug版的包; 执行./gradlew assembleVIVORelease将生成VIVO的release包。 推荐文章:美团Android自动化之旅—Walle生成渠道包(https://github.com/Meituan-Dianping/walle) 5、插件化原理分析 参考回答:插件化是指将 APK 分为宿主和插件的部分。把需要实现的模块或功能当做一个独立的提取出来,在 APP 运行时,我们可以动态的载入或者替换插件部分,减少宿主的规模 宿主: 就是当前运行的APP。 插件: 相对于插件化技术来说,就是要加载运行的apk类文件。 而热修复则是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。 类加载机制:Android中常用的两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader,两者区别在于PathClassLoader只能加载内部存储目录的dex/jar/apk文件。DexClassLoader支持加载指定目录(不限于内部)的dex/jar/apk文件 插件通信:通过给插件apk生成相应的DexClassLoader便可以访问其中的类,可分为单DexClassLoader和多DexClassLoader两种结构。 若使用多ClassLoader机制,主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。 若使用单ClassLoader机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊端,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错。 资源加载:原理在于通过反射将插件apk的路径加入AssetManager中并创建Resource对象加载资源,有两种处理方式: 合并式:addAssetPath时加入所有插件和主工程的路径;由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况,在访问时会产生资源冲突。 独立式:各个插件只添加自己apk路径,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的Resource对象。 推荐文章: Android动态加载技术 简单易懂的介绍方式(https://segmentfault.com/a/1190000004062866) 深入理解Android插件化技术(https://yq.aliyun.com/articles/361233) 为什么要做热更新(https://www.cnblogs.com/baiqiantao/p/9160806.html) 6、组件化原理 参考回答:引入组件化的原因:项目随着需求的增加规模变得越来越大,规模的增大导致了各种业务错中复杂的交织在一起, 每个业务模块之间,代码没有约束,带来了代码边界的模糊,代码冲突时有发生, 更改一个小问题可能引起一些新的问题, 牵一发而动全身,增加一个新需求,需要熟悉相关的代码逻辑,增加开发时间 避免重复造轮子,可以节省开发和维护的成本。 可以通过组件和模块为业务基准合理地安排人力,提高开发效率。 不同的项目可以共用一个组件或模块,确保整体技术方案的统一性。 为未来插件化共用同一套底层模型做准备。 组件化开发流程就是把一个功能完整的App或模块拆分成多个子模块(Module),每个子模块可以独立编译运行,也可以任意组合成另一个新的 App或模块,每个模块即不相互依赖但又可以相互交互,但是最终发布的时候是将这些组件合并统一成一个apk,遇到某些特殊情况甚至可以升级或者降级 举个简单的模型例子: App是主application,ModuleA和ModuleB是两个业务模块(相对独立,互不影响),Library是基础模块,包含所有模块需要的依赖库,以及一些工具类:如网络访问、时间工具等。 注意:提供给各业务模块的基础组件,需要根据具体情况拆分成 aar 或者 library,像登录,基础网络层这样较为稳定的组件,一般直接打包成 aar,减少编译耗时。而像自定义 View 组件,由于随着版本迭代会有较多变化,就直接以源码形式抽离成 Library 推荐文章:干货 | 从智行 Android 项目看组件化架构实践(https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697268363&idx=1&sn=3db2dce36a912936961c671dd1f71c78) 7、跨组件通信 跨组件通信场景: 第一种是组件之间的页面跳转 (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)。 第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)。 跨组件通信方案分析:第一种组件之间的页面跳转实现简单,跳转时想传递不同类型的数据提供有相应的 API即可。 第二种组件之间的自定义类和自定义方法的调用要稍微复杂点,需要 ARouter 配合架构中的 公共服务(CommonService) 实现: 提供服务的业务模块: 在公共服务(CommonService) 中声明 Service 接口 (含有需要被调用的自定义方法), 然后在自己的模块中实现这个 Service 接口, 再通过 ARouter API 暴露实现类。 使用服务的业务模块:通过 ARouter 的 API 拿到这个 Service 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互。 此外,可以使用 AndroidEventBus 其独有的 Tag, 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 Tag 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 EventBus。 如何管理过多的路由表? RouterHub 存在于基础库, 可以被看作是所有组件都需要遵守的通讯协议, 里面不仅可以放路由地址常量, 还可以放跨组件传递数据时命名的各种 Key 值, 再配以适当注释, 任何组件开发人员不需要事先沟通只要依赖了这个协议, 就知道了各自该怎样协同工作, 既提高了效率又降低了出错风险, 约定的东西自然要比口头上说强。 Tips: 如果您觉得把每个路由地址都写在基础库的 RouterHub 中, 太麻烦了, 也可以在每个组件内部建立一个私有 RouterHub, 将不需要跨组件的路由地址放入私有 RouterHub 中管理, 只将需要跨组件的路由地址放入基础库的公有 RouterHub 中管理, 如果您不需要集中管理所有路由地址的话, 这也是比较推荐的一种方式。 ARouter路由原理:ARouter维护了一个路由表Warehouse,其中保存着全部的模块跳转关系,ARouter路由跳转实际上还是调用了startActivity的跳转,使用了原生的Framework机制,只是通过apt注解的形式制造出跳转规则,并人为地拦截跳转和设置跳转条件。 常见的组件化方案如下: 8、组件化中路由、埋点的实现 参考回答:因为在组件化中,各个业务模块之间是各自独立的, 并不会存在相互依赖的关系, 所以一个业务模块是访问不了其他业务模块的代码的, 如果想从 A 业务模块的 A 页面跳转到 B 业务模块的 B 页面, 光靠模块自身是不能实现的,这就需要一种跨组件通信方案—— 路由(Router) 路由主要有以下两种场景: 第一种是组件之间的页面跳转 (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型) 第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务) 其原理在于将分布在不同组件module中的某些类按照一定规则生成映射表(数据结构通常是Map,Key为一个字符串,Value为类或对象),然后在需要用到的时候从映射表中根据字符串从映射表中取出类或对象,本质上是类的查找。 埋点则是在应用中特定的流程收集一些信息,用来跟踪应用使用的状况: 代码埋点:在某个事件发生时调用SDK里面相应的接口发送埋点数据,百度统计、友盟、TalkingData、Sensors Analytics等第三方数据统计服务商大都采用这种方案 全埋点:全埋点指的是将Web页面/App内产生的所有的、满足某个条件的行为,全部上报到后台服务器 可视化埋点:通过可视化工具(例如Mixpanel)配置采集节点,在Android端自动解析配置并上报埋点数据,从而实现所谓的自动埋点 无埋点:它并不是真正的不需要埋点,而是Android端自动采集全部事件并上报埋点数据,在后端数据计算时过滤出有用数据 推荐文章:安卓组件化开源方案实现(https://juejin.im/post/5a7ab8846fb9a0634514a2f5) 9、Hook以及插桩技术 参考回答:Hook是一种用于改变API执行结果的技术,能够将系统的API函数执行重定向(应用的触发事件和后台逻辑处理是根据事件流程一步步地向下执行。而Hook的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件,例如逆向破解App) Android 中的 Hook 机制,大致有两个方式: 要 root 权限,直接 Hook 系统,可以干掉所有的 App。 无 root 权限,但是只能 Hook 自身app,对系统其它 App 无能为力。 插桩是以静态的方式修改第三方的代码,也就是从编译阶段,对源代码(中间代码)进行编译,而后重新打包,是静态的篡改; 而Hook则不需要再编译阶段修改第三方的源码或中间代码,是在运行时通过反射的方式修改调用,是一种动态的篡改 推荐文章: Android插件化原理解析——Hook机制之动态代理(http://weishu.me/2016/01/28/understand-plugin-framework-proxy-hook/) android 插桩基本概念(https://blog.csdn.net/fei20121106/article/details/51879047) Android逆向之旅(http://www.520monkey.com/) 10、Android的签名机制? 参考回答:Android的签名机制包含有消息摘要、数字签名和数字证书 消息摘要:在消息数据上,执行一个单向的 Hash 函数,生成一个固定长度的Hash值 数字签名:一种以电子形式存储消息签名的方法,一个完整的数字签名方案应该由两部分组成:签名算法和验证算法 数字证书:一个经证书授权(Certificate Authentication)中心数字签名的包含公钥拥有者信息以及公钥的文件 推荐文章:一篇文章看明白 Android v1 & v2 签名机制(https://blog.csdn.net/freekiteyu/article/details/84849651) 11、v3签名key和v2还有v1有什么区别 参考回答:在v1版本的签名中,签名以文件的形式存在于apk包中,这个版本的apk包就是一个标准的zip包,V2和V1的差别是V2是对整个zip包进行签名,而且在zip包中增加了一个apk signature block,里面保存签名信息。 v2版本签名块(APK Signing Block)本身又主要分成三部分: SignerData(签名者数据):主要包括签名者的证书,整个APK完整性校验hash,以及一些必要信息 Signature(签名):开发者对SignerData部分数据的签名数据 PublicKey(公钥):用于验签的公钥数据 v3版本签名块也分成同样的三部分,与v2不同的是在SignerData部分,v3新增了attr块,其中是由更小的level块组成。每个level块中可以存储一个证书信息。前一个level块证书验证下一个level证书,以此类推。最后一个level块的证书,要符合SignerData中本身的证书,即用来签名整个APK的公钥所属于的证书 推荐文章: APK 签名方案 v3(https://source.android.google.cn/security/apksigning/v3) Android P v3签名新特性(https://xuanxuanblingbling.github.io/ctf/android/2018/12/30/signature/) 12、Android5.0~10.0之间大的变化 Android 5.0新特性: MaterialDesign设计风格 支持64位ART虚拟机(5.0推出的ART虚拟机,在5.0之前都是Dalvik。他们的区别是:Dalvik,每次运行,字节码都需要通过即时编译器转换成机器码(JIT)。ART,第一次安装应用的时候,字节码就会预先编译成机器码(AOT)) 通知详情可以用户自己设计 Android 6.0新特性 动态权限管理 支持快速充电的切换 支持文件夹拖拽应用 相机新增专业模式 Android 7.0新特性 多窗口支持 V2签名 增强的Java8语言模式 夜间模式 Android 8.0(O)新特性 优化通知:通知渠道 (Notification Channel) 通知标志 休眠 通知超时 通知设置 通知清除 画中画模式:清单中Activity设置android:supportsPictureInPicture 后台限制 自动填充框架 系统优化等等优化很多 Android 9.0(P)新特性 室内WIFI定位 “刘海”屏幕支持 安全增强等等优化很多 Android 10.0(Q)新特性 夜间模式:包括手机上的所有应用都可以为其设置暗黑模式。 桌面模式:提供类似于PC的体验,但是远远不能代替PC。 屏幕录制:通过长按“电源”菜单中的"屏幕快照"来开启。 推荐文章:Android Developers 官方文档(https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels) 13、说下Measurepec这个类 参考回答:作用:通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec决定View的大小 组成:一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize( 某种测量模式下的规格大小)。 三种模式: UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。 EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent或具体数值。 AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。 决定因素:值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定。具体规则见下图: 14、请例举Android中常用布局类型,并简述其用法以及排版效率 参考回答:Android中常用布局分为传统布局和新型布局 传统布局(编写XML代码、代码生成): 框架布局(FrameLayout): 线性布局(LinearLayout): 绝对布局(AbsoluteLayout): 相对布局(RelativeLayout): 表格布局(TableLayout): 新型布局(可视化拖拽控件、编写XML代码、代码生成):约束布局(ConstrainLayout): 注:图片出自Carson_Ho的Android:常用布局介绍&属性设置大全(https://blog.csdn.net/carson_ho/article/details/51719519) 对于嵌套多层View而言,其排版效率:LinearLayout = FrameLayout >> RelativeLayout 15、区别Animation和Animator的用法,概述其原理 动画的种类:前者只有透明度,旋转,平移,伸缩4种属性,而对于后者,只要是该控件的属性,且有setter该属性的方法就都可以对该属性执行一种动态变化的效果。 可操作的对象:前者只能对UI组件执行动画,但属性动画几乎可以对任何对象执行动画(不管它是否显示在屏幕上)。 动画播放顺序:在Animator中,AnimatorSet正是通过playTogether()、playSequentially()、animSet.play().with()、before()、after()这些方法来控制多个动画协同工作,从而做到对动画播放顺序的精确控制 16、使用过什么图片加载库? Glide的源码设计哪里很微妙? 参考回答:图片加载库:Fresco、Glide、Picasso等 Glide的设计微妙在于: Glide的生命周期绑定:可以控制图片的加载状态与当前页面的生命周期同步,使整个加载过程随着页面的状态而启动/恢复,停止,销毁 Glide的缓存设计:通过(三级缓存,Lru算法,Bitmap复用)对Resource进行缓存设计 Glide的完整加载过程:采用Engine引擎类暴露了一系列方法供Request操作 推荐文章:Glide 源码分析(https://user-gold-cdn.xitu.io/2019/4/24/16a4ec49c3af1f5c) 17、如何绕过9.0限制? 参考回答: 18、用过哪些网络加载库? OkHttp、Retrofit实现原理? 参考回答:网络加载库:OkHttp、Retrofit、xUtils、Volley等 推荐文章: Android OkHttp源码解析入门教程(一)(https://juejin.im/post/5c46822c6fb9a049ea394510) Android OkHttp源码解析入门教程(二)(https://juejin.im/post/5c4682d2f265da6130752a1d) 19、对于应用更新这块是如何做的? (灰度,强制更新、分区域更新) 内部更新: 通过接口获取线上版本号,versionCode 比较线上的versionCode 和本地的versionCode,弹出更新窗口 下载APK文件(文件下载) 安装APK 灰度更新: 找单一渠道投放特别版本。 做升级平台的改造,允许针对部分用户推送升级通知甚至版本强制升级。 开放单独的下载入口。 是两个版本的代码都打到app包里,然后在app端植入测试框架,用来控制显示哪个版本。测试框架负责与服务器端api通信,由服务器端控制app上A/B版本的分布,可以实现指定的一组用户看到A版本,其它用户看到B版本。服务端会有相应的报表来显示A/B版本的数量和效果对比。最后可以由服务端的后台来控制,全部用户在线切换到A或者B版本~ 无论哪种方法都需要做好版本管理工作,分配特别的版本号以示区别。 当然,既然是做灰度,数据监控(常规数据、新特性数据、主要业务数据)还是要做到位,该打的数据桩要打。 还有,灰度版最好有收回的能力,一般就是强制升级下一个正式版。 强制更新:一般的处理就是进入应用就弹窗通知用户有版本更新,弹窗可以没有取消按钮并不能取消。这样用户就只能选择更新或者关闭应用了,当然也可以添加取消按钮,但是如果用户选择取消则直接退出应用。 增量更新:二进制差分工具bsdiff是相应的补丁合成工具,根据两个不同版本的二进制文件,生成补丁文件.patch文件。通过bspatch使旧的apk文件与不定文件合成新的apk。 注意通过apk文件的md5值进行区分版本。 20、会用Kotlin、Fultter吗? 谈谈你的理解 Kotlin是一种具有类型推断的跨平台,静态类型的通用编程语言。Kotlin旨在与Java完全互操作,其标准库的JVM版本依赖于Java类库,但类型推断允许其语法更简洁。 Flutter是由Google创建的开源移动应用程序开发框架。它用于开发Android和iOS的应用程序,以及为Google Fuchsia创建应用程序的主要方法 关于kotlin的重要性,相信大家在日常开发可以体会到,应用到实际开发中,需要避免语法糖(例如单列模式、空值判断、高阶函数等) 至于Flutter,目前Google官方文档还不完善,市面上采用此语言编写的项目较少,如需要具体深入,请参考闲鱼和官方文档 最后 其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2020年的Android中高级面试题,这只是Android全套面试真题解析的小部分! 博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。 【[Android学习PDF+学习视频+面试文档+知识点笔记] 下载地址:https://shimo.im/docs/Q6V8xPVxHpkrtRtD! 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】可以点击:https://shimo.im/docs/Q6V8xPVxHpkrtRtD前往免费领取!
00. 前言 近期,有消息称,前阿里P10员工赵海平已加入字节跳动,职级为4+。王垠加入华为职级为 21级。 事情起因还是从两人乌龙说起,详细经过请看:王垠受邀面试阿里P9,被P10面跪后网上怒发文,惨打325的P10赵海平回应了! 2019年底,因为在面试王垠时引起双方不愉快导致网上舆论骂战,据传赵海平的直属领导多隆在绩效中打了3.25分。因此前阿里P10员工赵海平跳槽字节跳动也就顺理成章了! 其实跳槽不是一件简单轻松的事情,什么样的跳槽才真正有价值!本文带你深入了解跳槽相关的所有细节,建议收藏! 很多人以为“跳槽”就是写简历、换工作,但要想得到满意的 offer,其实需要思考、准备的内容很多,大概有这么几点: 跳槽前要思考清楚 掌握跳槽需要的知识点 写一份高质量的简历 了解简历投递的时机和方式 拿到面试邀请要做好功课 面试中要调整心态,努力表现 面试后及时思考总结 有多个 offer 如何选择 优雅的离职 这些应该是一次跳槽从开始到结束比较完整的流程了。接下来我们将详细了解每一部分。 01. 跳槽前要思考的问题 每一份工作都是以希冀开始,我们心怀憧憬,希望在公司里大展拳脚、做些成就。 然而是什么让我们走到今天这地步,即将与它分手? 一般离职有两个原因: 钱不到位 心委屈了 钱不到位怎么办 业界一种普遍的观念是“跳槽涨工资最快”,这也的确是现状。 为什么公司迟迟不给加薪,非得逼得我们跳槽呢? 答案很简单:因为公司没有意识到你价值的增长。 我们在入职时可能只是一个菜鸟,但是在工作一段时间后,由于个人的努力以及业务的锻炼,水平已经有了很大的不同,自己心里觉得自己不再是菜鸟了,希望能拿更高的工资。 但你可能一直在做一个项目,没有承担更多的责任,公司无法了解到你现在能力到什么程度了。 这个时候,如果领导比较好的话,你可以先主动要求承担任务,积极完成(就是加班加点),然后在一段时间后找领导谈加薪。这样就有理有据,让人信服,领导一般都会同意。这样就不用跳槽了,省去准备面试题的繁琐。Over。 如果领导不同意,那就没辙了。 心委屈怎么办 员工离职的另外一种原因就是:待着不爽,心里委屈了。 一些被动的原因比如领导更替、岗位调整、加班太多等,都会让人心里不舒服。拿我来说,公司业务转型,做的工作不是安卓应用开发,做久了心里不踏实,就想离职了。如果有条件的话,可以跟领导沟通能否恢复原样,如果无望,那就只好跳槽了。 但在真正提出离职之前,还需要思考几个问题。 离职前的思考 If you don't like something, change it. If you can't change it, change your attitude. Don't complain. 有的朋友可能会说:别废话,赶紧讲面试知识点,我干的不爽就跳槽了,还想什么想? 非也,每次跳槽的成本其实很大,除去面试来回的时间不说,新旧环境的适应、业务的熟悉、代码的掌握,都需要成本,如果两次跳槽间隔太短,其实时间都花在了适应新环境上,真正学习、沉淀的内容不会太多,尤其在刚开始工作,更忌讳这样子。 因此,在确定离职前,你需要先问自己 3 个问题: 现在的项目没有我可以学习的内容了吗? 离开这里我会失去什么? 到新环境可以得到什么? 拿我自己来说,3 月份时项目里的代码我没有掌握太扎实,框架层很多设计思路还搞不清楚,出去面试一问就问倒了。这时如果真的换了工作,恐怕又要接触新项目、新代码,根本没时间好好消化之前的代码。 除了代码,人际关系也需要沉淀,在这个公司待了那么久,看到了一些同事的优秀品质,学习、借鉴他们也需要一定的时间,如果忙着换工作,可能就失去了和同事深入交流的机会。 因此在第一次面试失败后,我把项目代码好好研究了一个多月,也跟同事请教很多学习、规划上的知识,为后面的路做好铺垫。 确定要走时需要做的准备 在确定要走后,尽量不要裸辞,现在的行情你懂得,没有准备就裸辞,等于断了自己后路。 有的网友问我:辞职在家好好复习一个多月怎么样。我劝他还是算了,都不是自觉的人,天天在家反而更学不进去。最好的状态就是一边工作,一边搜集要学习的知识点学习资料,下班路上、晚上、周末去学习,那样精神压力不会太大。 在开始复习知识点前,你需要确认以下 2 点: 定位 亮点 定位 “定位” 是指你要确定自己所处的阶段和想要的目标。主要包括: 目前是什么水平 有什么积累 工作项目里有什么难点 想找什么级别的工作 举个例子,你可以给自己的定位就是:现在是中级水平,常见应用开发需求基本都能解决,开源框架使用没问题,部分读了源码,工作项目里主要是混合开发比较复杂,想找有利于自己成长的、大公司中高级安卓开发工作。 亮点 有的朋友可能对 “亮点” 的定义不是很明白。“亮点” 其实就是能够把你和众多应聘者区分出来的优秀品质。 程序员面试主要考察这几点: 技能水平 学习能力 团队合作 工作心态等 如果你在其中任何一个方面都有自己的优势,就赶紧找出来,写到简历上,面试表达出来。 拿技能水平来说,根据做过项目的类型,确定自己当前擅长的点,比如你做的是电商应用,那可能就擅长混合开发、自定义 View;做的是电台应用,那可能就擅长后台进程保活;做的是大用户量应用,那可能就擅长监控、性能优化等等。 找到自己已有的或者该有的亮点,然后去努力掌握、精通。 行情怎么样 有的网友说早就想跳槽了,但是听说行情不好,不敢跳。其实吧,价格取决于价值,影响价格波动的是供需关系。 疫情原因,很多互联网企业生存艰难,招聘的岗位标准就比以要高一些了,去招聘软件上看也可以发现,大多要求 3 年以上经验。这是因为现在安卓初级太多了,各种应届生、培训班涌入市场,前几年会用 ListView 就可以找工作的日子一去不复还。不过真正的高级,或者有潜力的中级,还是很受企业欢迎的。 因此,我们在想要跳槽时,不要被所谓的“差行情”拦住,而是要审视自己的水平,确定自己的目标,究竟是要找初级岗、中级岗,还是高级岗。 面试前,面试官拿到你的简历,根据简历上的信息会建立第一印象。因此你想要呈现给对方一个怎样的定位,有什么亮点,都需要事先思考清楚。 确定了方向后就要开始努力准备,下一节我们介绍安卓跳槽需要准备复习的知识。 02. Android开发跳槽需要复习的知识 在介绍面试考察内容前,先来看看我认为的“中高级Android”需要掌握的知识点,这些这是我们集合了牛客网、掘金、简书、知乎、CSDN等网站的几十篇面经和群友自己面试的经历的合集,以及请教前辈总结而来的。整理的知识点会有 Java、Android SDK、Android 源码、常见面试算法题、其他的一些计算机基础以及常见的面试题等几个部分: 1.Java 知识点汇总2.Android 知识点汇总3.Android 扩展知识点汇总4.Android 开源库源码分析5.设计模式汇总6.计算机网络基础7.常见面试算法题汇总8.Kotlin知识点汇总9.Flutter知识点汇总 面试中级及以下岗位时,在电话面试、一面、或者你简历没有突出亮点时,一般都会先问基础,目的是确定你基本功扎不扎实。 如果基础知识有太多不会的就危险了,必须好好准备,这是躲不过的。电话面试、一面考察基本功过关后,就会进入下一阶段 --- 问些进阶的,看看你最高水平在哪里。 这个阶段有不会的很正常,因为对方正在确定你的水平,但态度一定要积极主动,即使之前没有研究过,也要结合一些经验说出推测的结果,切忌直接说“我不会”。 第一章 Java 知识点汇总 JVM JVM 工作流程 运行时数据区(Runtime Data Area) 方法指令 类加载器 垃圾回收 gc 对象存活判断 垃圾收集算法 垃圾收集器 内存模型与回收策略 Object equals 方法 hashCode 方法 static final String、StringBuffer、StringBuilder 异常处理 内部类 匿名内部类 多态 抽象和接口 集合框架 HashMap 结构图 HashMap 的工作原理 HashMap 与 HashTable 对比 ConcurrentHashMap Base 1.7 Base 1.8 ArrayList LinkedList CopyOnWriteArrayList 反射 单例 饿汉式 双重检查模式 静态内部类模式 线程 状态 状态控制 volatile synchronized 根据获取的锁分类 原理 Lock 锁的分类 悲观锁、乐观锁 自旋锁、适应性自旋锁 死锁 引用类型 动态代理 元注解 答案解析 2020年Android开发最新全套面试题答案解析 第二章 Android 知识点汇总 Activity 生命周期 启动模式 启动过程 Fragment 特点 生命周期 与Activity通信 Service 启动过程 绑定过程 生命周期 启用前台服务 BroadcastReceiver 注册过程 ContentProvider 基本使用 数据存储 View MeasureSpec MotionEvent VelocityTracker GestureDetector Scroller View 的滑动 View 的事件分发 在 Activity 中获取某个 View 的宽高 Draw 的基本流程 自定义 View 进程 进程生命周期 多进程 进程存活 OOM_ADJ 进程被杀情况 进程保活方案 Parcelable 接口 使用示例 方法说明 Parcelable 与 Serializable 对比 IPC IPC方式 Binder AIDL 通信 Messenger Window / WindowManager Window 概念与分类 Window 的内部机制 Window 的创建过程 Activity 的 Window 创建过程 Dialog 的 Window 创建过程 Toast 的 Window 创建过程 Bitmap 配置信息与压缩方式 常用操作 裁剪、缩放、旋转、移动 Bitmap与Drawable转换 保存与释放 图片压缩 BitmapFactory Bitmap创建流程 Option类 基本使用 内存回收 屏幕适配 单位 头条适配方案 刘海屏适配 Context SharedPreferences 获取方式 getPreferences getDefaultSharedPreferences getSharedPreferences 架构 apply / commit 注意 消息机制 Handler 机制 工作原理 ThreadLocal MessageQueue Looper Handler 线程异步 AsyncTask 基本使用 工作原理 HandlerThread IntentService 线程池 RecyclerView 优化 Webview 基本使用 WebView WebSettings WebViewClient WebChromeClient Webview 加载优化 内存泄漏 答案解析 第三章 Android 扩展知识点汇总 ART ART 功能 预先 (AOT) 编译 垃圾回收优化 开发和调试方面的优化 ART GC Apk 包体优化 Apk 组成结构 整体优化 资源优化 代码优化 .arsc文件优化 lib目录优化 Hook 基本流程 使用示例 Proguard 公共模板 常用的自定义混淆规则 aar中增加独立的混淆配置 检查混淆和追踪异常 架构 MVC MVP MVVM Jetpack 架构 使用示例 NDK 开发 JNI 基础 数据类型 String 字符串函数操作 常用 JNI 访问 Java 对象方法 NDK 开发 基础开发流程 System.loadLibrary() CMake 构建 NDK 项目 常用的 Android NDK 原生 API 类加载器 双亲委托模式 DexPathList 2020年Android开发最新全套面试题答案解析 第四章 Android 开源库源码分析 LeakCanary 初始化注册 引用泄漏观察 Dump Heap EventBus 自定义注解 注册订阅者 发送事件 第五章设计模式汇总 设计模式分类 面向对象六大原则 工厂模式 单例模式 建造者模式 原型模式 适配器模式 观察者模式 代理模式 责任链模式 策略模式 备忘录模式 答案解析 第六章计算机网络基础 网络体系的分层结构 HTTP 相关 请求报文 请求行 请求头 响应报文 常见状态码 缓存机制 Https Http 2.0 TCP/IP 三次握手 四次挥手 TCP 与 UDP 的区别 Socket 使用示例答案解析 2020年Android开发最新全套面试题答案解析 第七章 常见面试算法题汇总 排序 比较排序 冒泡排序 归并排序 快速排序 线性排序 计数排序 桶排序 二叉树 顺序遍历 层次遍历 左右翻转 最大值 最大深度 最小深度 平衡二叉树 链表 删除节点 翻转链表 中间元素 判断是否为循环链表 合并两个已排序链表 链表排序 删除倒数第N个节点 两个链表是否相交 栈 / 队列 带最小值操作的栈 有效括号 用栈实现队列 逆波兰表达式求值 二分 二分搜索 X的平方根 哈希表 两数之和 连续数组 最长无重复字符的子串 最多点在一条直线上 堆 / 优先队列 前K大的数 前K大的数II 第K大的数 二叉搜索树 验证二叉搜索树 第K小的元素 数组 / 双指针 加一 删除元素 删除排序数组中的重复数字 我的日程安排表 I 合并排序数组 贪心 买卖股票的最佳时机 买卖股票的最佳时机 II 最大子数组 主元素 字符串处理 生成括号 Excel表列标题 翻转游戏 翻转字符串中的单词 转换字符串到整数 最长公共前缀 回文数 动态规划 单词拆分 爬楼梯 打劫房屋 编辑距离 乘积最大子序列 矩阵 螺旋矩阵 判断数独是否合法 旋转图像 二进制 / 位运算 落单的数 格雷编码 其他 反转整数 LRU缓存策略答案解析 第八章 Kotlin 相关知识点 from-java-to-kotlin kotlin_tips 从原理分析Kotlin的延迟初始化: lateinit var和by lazy 使用Kotlin Reified 让泛型更简单安全 Kotlin里的Extension Functions实现原理分析 Kotlin系列之顶层函数和属性 Kotlin 兼容 Java 遇到的最大的 “坑” Kotlin 的协程用力瞥一眼 Kotlin 协程「挂起」的本质 到底什么是「非阻塞式」挂起?协程真的更轻量级吗? 资源混淆是如何影响到Kotlin协程的 Kotlin Coroutines(协程) 完全解析 答案解析 第九章 Flutter 相关知识点汇总 Flutter原理与实践 揭秘Flutter Hot Reload(原理篇) Flutter 动态化探索 Flutter如何和Native通信-Android视角 深入理解Flutter Platform Channel Flutter Engine 编译指北 Flutter Engine 线程模型 深入理解Flutter多线程 Flutter状态管理 - 初探与总结 Flutter | 状态管理指南篇——Provider 深入理解Flutter应用启动 Flutter渲染机制—UI线程 Flutter渲染机制—GPU线程 深入理解Flutter应用启动 深入理解setState更新机制 深入理解Flutter消息机制 深入理解Flutter动画原理 Dart虚拟机运行原理 源码解读Flutter tools机制 源码解读Flutter run机制 答案解析 2020年Android开发最新全套面试题答案解析 03.写一份高质量简历简历 简历的重要性就不言而喻了,怎么样写好简历是个技术活,当然如果你有很好的背景(学校或者公司),那么不管你怎么写,基本上都不刷掉你,我们作为一般的人还是需要下一番功夫的。拿我的简历作为例子,大概有以下几个部分: 个人信息:姓名、出生日期、教育背景、博客地址、github地址、联系方式(手机、邮箱和微信号) 工作经历:毕业后待过哪些公司,一般是倒序,项目尽量精简明了,可以参考SMART原则 专业技能:自己熟悉的一些技能,这个为什么我写到最后,因为对于工作三年的同学来说,面试官更加注重的是你的项目经历,大部分面试都是看你的项目经历来提问。 怎样写简历,这个开源网站不错,教你怎么写简历,而且有一个在线markdown在线网站,可以导出pdf。 04. 了解简历投递的时机和方式 一般HR都会集中处理简历,正序或者倒序都有可能,所以选择最合适的时间段去投递就好。 总体上来说,比较推荐的时间段就是周二至周四,上午9点以后。 除了投递时间之外,还有一些注意事项可以提高你的简历被阅读率: 1.尽量选择HR邮箱直投:不论是哪个招聘网站,整体的回复速度都要低于HR直投邮箱,大部分HR打开自己邮箱的频率也要高于其他招聘类网站。 2.邮箱实名:最好将发件人改为自己的名字,可以让HR一眼就看到,而不是乱七八糟的其他内容,不仅看起来更加整洁,也可以增加印象。 3.邮件名称固定格式:如果企业方有要求,要严格按照企业方的要求来,如果没有,应届生建议“姓名+学校+专业+应聘岗位+最快到岗时间(尤其是标注尽快到岗)”,言简意赅。 4.正文不要空着,记得添加附件.pdf:有些HR习惯下载一段时间内的所有简历附件,然后一起查看,如果你没有附件会直接被忽略,pdf是为了避免简历跑版的重要措施;也有些HR更倾向于一封邮件一封邮件地查看,这时正文如果有内容就会更加直接,正文和附件都有,双重保障。 不要认为一些小细节不重要~投递简历的很多小细节会决定第一印象的,看到一封简洁、信息明确的邮件,HR第一反应都会是“WOW,这个不错/还行,让我看看” 05. 拿到面试邀请要做好功课 1.提前准备 这是句废话,问题是怎么准备。 建议:如果你是那种在镜头前讲话并不自然、紧张的人,提前演练:录下自己的回答,然后再看一遍,看看你的体态和声音。同时,你还可以确认背景和灯光怎么样。 和任何面试一样,你还得调查公司,准备一些常见面试问题的答案。视频面试的另一个好处是你可以准备一些笔记。这些东西要远离镜头,面试时别玩纸,因为纸张的沙沙声会影响声音,分散你的注意力。 2.选好地方 提前计划好面试地点。地方一定要安静,不能被噪音和人打扰。房间整洁,背景干净简单,如此一来面试官才会关注你。你想想如果你背后是一整墙Banksy的壁画,面试官很容易就分神了。 关掉所有电子设备上任何可能播放通知声音的软件,将手机调为静音。如果你有室友之类的人,让家里的每个人都知道你要开始面试了。 3.职业着装 虽然你在家,但这是个工作面试。这是你给别人留下第一印象的机会——所以要穿着得体。实地的面试怎么穿,你就怎么穿。 唯一需要考虑的是你的衣服在屏幕上看起来会是什么样子,比如,太多图案和条纹可能上镜不太好。 4.肢体语言 切忌无精打采,也不要多动症、不要老摸自己的脸。面试官最想看的是眼神交流、微笑、倾听、对他们所说的话感兴趣、有回应。 所以,你的镜头应该与眼睛水平,你应该看镜头,而不是看屏幕。 回答宁可慢而清晰,不要快但是听不清。另外,小心不要打断对方,因为比起实地面试,互联网可能延迟。 5.技术问题 最后,还要处理好技术问题。 要考虑光线,到时候看不见正脸就尴尬了。为了保证你脸上没有阴影,可以用窗户进来的自然光,或者把灯放在相机前面,调整好距离即可。 还有电脑、相机和任何要求使用的软件。提前试一试,确保画面清晰,声音质量好。同样值得检查是你的网络,网一卡面试就有可能受影响。 视频面试当天,确保所有东西都充满电或插电。至少提前半小时打开所有设备,登录软件。 如果真的有什么技术问题,比如听不清楚问题。不用纠结,直接跟面试官提这个问题。面试官可以帮你解决,或者直接重新拨号。 06 面试中要调整心态,努力表现 可是如何克服面试的“紧张”情绪,调整好心态,努力表现呢? 解决办法:首先要精确地定位问题,知道问题背后隐藏的到底是什么; 把问题放在更长的尺度中,去俯视它; 只关注自己可以控制的事情。 1.分解问题 “紧张”所描述的,是一个复杂的“情绪混合体”。这个“情绪混合体”里,至少包含了以下的情绪: 对于面试被拒的“担忧”; 对于面试中没回答出问题的“尴尬”; 对于未知事情(面试)本能的恐惧。 这个混合体里还有很多其它的情绪,因人而异。主要来说,以上三种情绪是每个人都会有的。 2.俯视你“紧张”的事物 明白了,我们“紧张”的到底是什么,我们就要分别来解决它们。 解决这些问题,有一个总的纲领:“使它成为更大的事情的一部分”。 你首先要知道,“面试”不是你生活的全部。“面试”只不过是你人生中的一次体验。当你明白了这一点,你就会站在高处,俯视面试,而不是站在山脚下,抬头仰望它。 3.斯多葛学派二分法 有了一个解决问题的纲领,我们还需要一个解决问题的方法。 斯多葛派的哲学思想源远流长,其中一个主要思想是:斯多葛控制二分法。 它的含义是:在生活中,有些事情是你能控制的,有些事情是你无法控制的,你应该只关注那些你能控制的事情。 那这个思想,有什么意义呢?它可以帮助你把自己的“个人目标”从“外界目标”转换到“内部目标上”。 4.外界目标与内部目标 什么是“外界目标”? “外界目标”是你无法完全控制的事情。 比如说,你制作了精美的简历,然后把简历发给了理想公司,期待能获得offer。如果你把自己的“个人目标”定义为了“获得offer”,那这就是一个“外界目标”。 “内部目标”是你能完全控制的事情。 在这个路径中,制作一份最理想的简历、写一份最理想的申请邮件、面试时把自己打扮的最漂亮、面试结束后对老师最客气的道别,都可以作为你的“内部目标”。将注意力集中到这些自己能够完全控制的事情上,不必要的情绪就会减轻很多。 5.使用斯多葛二分法解决“紧张” (1)对于面试被拒的“担忧”—怎么办? 面试能否通过,决定权在别人手里,在别人的评判过程中,没有一件事是你能控制的。(2)对于面试中没回答出问题的“尴尬”—怎么办? 面试的问题分为两类: 基础知识类的问题; 考察解决问题能力的问题。 基础知识类的问题是你可以控制的,你没回答出来,是你自己的问题。 考察解决问题能力的问题,是不可控制的。你要知道回答不出来是正常的。 (3)对于未知事情(面试)本能的恐惧—怎么办? 多参加就不恐惧了,第一次害怕是难免的。 一定要事后复盘,不断调整自己对于面试的“认知模型”。 07. 面试后及时思考总结 面试后的复盘总结与面试前的准备同等重要: 1、对自己的表现有个总结,在总结中成长,发现自己的不足,下次能更好的提高 2、对自己的逻辑和文字编辑能力有个展现和提升 3、对自己的经历有个记录,人生比较重要的转折点 4、提升自己的个人影响力 5、分享出来,可能有更多人跟你一起交流,可能有意想不到的收获 面试复盘可以从以下几个角度展开: 1、复盘自己在面试现场的整体表现: 你的外表形象是否得体; 你是否全神贯注地倾听了对方的讲话; 你言谈举止是否得体,是否注意礼貌; 你是否表现得沉着、自信、自如; 你对面试官提问的反应是否恰当、准确、灵活; 2、复盘自己在面试现场的语言表达: 是否恰当地表达了自己的愿望和热情;对自己工作能力的申述是否充分、有条理、有例证;你计划要了解的情况是否都得到了答案;是否和面试官建立了和谐、有效的沟通; 3、复盘自己在面试中的所有问题及答案: 记录下面试官的问题,回顾整个面试过程;深入思考,哪些方面表现得最好,哪些地方失误最多;关于面试官的提问,你是否还可以做出更好的回答; 4、通过邮件或者微信,对面试官表达感谢: 通过文字表达感谢;对面试中表现不好的问题,可以进行补充说明。 08. 有多个 offer 如何选择 第一步:排列因素,找到影响选择的主要考量标准。 一般而言,在选择offer时,可从企业与自我认知两角度分析。 (1)从企业的角度去分析 基本上,当你在考虑一个offer的时候,都躲不开这些: 1.这个公司所在的行业发展前景怎么样? 2.这个公司的状况是怎样的? 3.你在这个公司,你能提升的空间有多少? 4.事先了解这个公司的领导层都是什么类型的人? 5.该公司的薪酬福利是怎样的? (2)从自身的角度去分析 基本上,在你考虑一个offer的时候,除了考虑公司的情况之外,你一定还会去考虑到个人的规划问题。 1.这个公司的岗位是否适合自己? 2.这个公司的岗位与自己的职业发展计划相匹配? 09. 优雅的离职 结合资深职场人的过来经验,我告诉你如何破除可能的阻力,避免那么多不胜其烦的是非,免受过多不必要的欺负,轻轻松松,优雅地离职。 1.不要跟BOSS说真话2.避免去竞业公司3.不要跟人说你离职,更不要怂恿别人离职4.总结经验,整理成文 如果公司不让你辞职?只需要提交“辞职信”而不是“辞职申请”,根据劳动法,就不需要等待领导的同意或批准。提交书面辞职信三十天,自动解除劳动合同关系。时间一满,HR再怎么着,都没办法接着卡你。
第一章 图片相关面试题目录 1、图片库对比2、LRUCache原理3、图片加载原理4、自己去实现图片库,怎么做?5、Glide源码解析6、Glide使用什么缓存?7、Glide内存缓存如何控制大小? 答案解析 第二章 网络和安全机制相关面试题目录 1.网络框架对比和源码分析2.自己去设计网络请求框架,怎么做?3.网络请求缓存处理,okhttp如何处理网络缓存的;4.从网络加载一个10M的图片,说下注意事项5.TCP的3次握手和四次挥手6.TCP与UDP的区别7.TCP与UDP的应用8.HTTP协议9.HTTP1.0与2.0的区别10.HTTP报文结构11.HTTP与HTTPS的区别以及如何实现安全性12.如何验证证书的合法性?13.https中哪里用了对称加密,哪里用了非对称加密,对加密算法(如RSA)等是否有了解?14.client如何确定自己发送的消息被server收到?15.谈谈你对WebSocket的理解16.WebSocket与socket的区别17.谈谈你对安卓签名的理解。18.请解释安卓为啥要加签名机制?19.视频加密传输20.App 是如何沙箱化,为什么要这么做?21.权限管理系统(底层的权限是如何进行 grant 的)? 答案解析 第三章 数据库相关面试题目录 1.sqlite升级,增加字段的语句2.数据库框架对比和源码分析3.数据库的优化4.数据库数据迁移问题5.Sqlite 常见异常 答案解析 第四章 .插件化、模块化、组件化、热修复、增量更新、Gradle相关面试题目录 1.对热修复和插件化的理解2.插件化原理分析3.模块化实现(好处,原因)4.热修复,插件化5.项目组件化的理解6.描述清点击 Android Studio 的 build 按钮后发生了什么 答案解析 第五章.架构设计和设计模式相关面试题目录 1.谈谈你对Android设计模式的理解2.MVC MVP MVVM原理和区别3.你所知道的设计模式有哪些?4.项目中常用的设计模式5.手写生产者/消费者模式6.写出观察者模式的代码7.适配器模式,装饰者模式,外观模式的异同?8.用到的一些开源框架,介绍一个看过源码的,内部实现过程。9.谈谈对RxJava的理解10.Rxjava发送事件步骤11.RxJava的作用,与平时使用的异步操作来比的优缺点12.说说EventBus作用,实现方式,代替EventBus的方式13.从0设计一款App整体架构,如何去做?14.说一款你认为当前比较火的应用并设计(比如:直播APP,P2P金融,小视频等)15.谈谈对java状态机理解16.Fragment如果在Adapter中使用应该如何解耦?17.Binder机制及底层实现18.对于应用更新这块是如何做的?(解答:灰度,强制更新,分区域更新)?19.实现一个Json解析器(可以通过正则提高速度)20.统计启动时长,标准 答案解析 第六章 性能优化相关面试题目录 1.启动app黑白屏优化2.稳定——内存优化3.流畅——卡顿优化4.节省——耗电优化5.安装包——APK瘦身6.冷启动与热启动7.内存泄漏的场景和解决办法 Bitmap优化 LRU 的原理 webview优化 如何避免OOM? ddms 和 traceView 性能优化如何分析systrace? 用IDE如何分析内存泄漏? Java多线程引发的性能问题,怎么解决? App启动崩溃异常捕捉 自定义View注意事项 现在下载速度很慢,试从网络协议的角度分析原因,并优化(提示:网络的5层都可以涉及)。 Https请求慢的解决办法(提示:DNS,携带数据,直接访问IP) 如何保持应用的稳定性 RecycleView优化 View渲染 Java中的四种引用的区别以及使用场景 强引用置为null,会不会被回收? 答案解析 第七章 Android Framework相关面试题目录 Android系统架构 View的事件分发机制?滑动冲突怎么解决? View的绘制流程? 跨进程通信 Android系统启动流程是什么?(提示:init进程 -> Zygote进程 启动一个程序,可以主界面点击图标进入,也可以从一个程序中 AMS家族重要术语解释 App启动流程(Activity的冷启动流程) ActivityThread工作原理 说下四大组件的启动过程,四大组件的启动与销毁的方式 AMS是如何管理Activity的? 理解Window和WindowManager WMS是如何管理Window的? 大体说清一个应用程序安装到手机上时发生了什么? Android的打包流程?(即描述清点击 Android Studio 的 build 按钮后发生了什么?)apk里有哪些东西?签名算法的原理? 说下安卓虚拟机和java虚拟机的原理和不同点?(JVM、 Davilk、ART三者的原理和区别) Android采用自动垃圾回收机制,请说下安卓内存管理的原理? Android中App是如何沙箱化的,为何要这么做? 一个图片在app中调用R.id后是如何找到的? JNI 请介绍一下NDK? 答案解析 第八章 Android优秀三方库源码相关面试题目录 网络底层框架:OkHttp实现原理 网络封装框架:Retrofifit实现原理 响应式编程框架:RxJava实现原理 图片加载框架:Glide实现原理 事件总线框架:EventBus实现原理 内存泄漏检测框架:LeakCanary实现原理 依赖注入框架:ButterKnife实现原理 依赖全局管理框架:Dagger2实现原理 数据库框架:GreenDao实现原理 ARouter 答案解析 第九章 算法相关面试题目录 1.排序算法有哪些?2.最快的排序算法是哪个?3.手写一个冒泡排序4.手写快速排序代码5.快速排序的过程、时间复杂度、空间复杂度6.手写堆排序7.堆排序过程、时间复杂度及空间复杂度8.写出你所知道的排序算法及时空复杂度,稳定性9.二叉树给出根节点和目标节点,找出从根节点到目标节点的路径10给阿里2万多名员工按年龄排序应该选择哪个算法?11.GC算法(各种算法的优缺点以及应用场景)12.蚁群算法与蒙特卡洛算法13.子串包含问题(KMP 算法)写代码实现14一个无序,不重复数组,输出N个元素,使得N个元素的和相加为M,给出时间复杂度、.空间复杂度。手写算法15.万亿级别的两个URL文件A和B,如何求出A和B的差集C(提示:Bit映射->hash分组->多文件读写效率->磁盘寻址以及应用层面对寻址的优化)16.百度POI中如何试下查找最近的商家功能(提示:坐标镜像+R树)。17.两个不重复的数组集合中,求共同的元素。18.两个不重复的数组集合中,这两个集合都是海量数据,内存中放不下,怎么求共同的元素?19.一个文件中有100万个整数,由空格分开,在程序中判断用户输入的整数是否在此文件中。说出最优的方法20.一张Bitmap所占内存以及内存占用的计算21.2000万个整数,找出第五十大的数字?22.烧一根不均匀的绳,从头烧到尾总共需要1个小时。现在有若干条材质相同的绳子,问如何用烧绳的方法来计时一个小时十五分钟呢?23.求1000以内的水仙花数以及40亿以内的水仙花数24.5枚硬币,2正3反如何划分为两堆然后通过翻转让两堆中正面向上的硬8币和反面向上的硬币个数相同25.时针走一圈,时针分针重合几次26.N*N的方格纸,里面有多少个正方形27.x个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完? 答案解析 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2020年的高级面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】可以下载:https://shimo.im/docs/3Tvytq686Yyv83KX免费领取!
01. 为什么选择计算机专业? 互联网随着智能手机的普及在近些年来野蛮生长,一个个融资千万的创业故事,坊间传说的“别人家年终奖”,各种“大几十万年薪”的虚位以待,都在深深刺痛着其他行业的你我他们。 网友也曾对程序员“为什么选择计算机专业?”发起了提问。选择计算机专业最主要的原因莫过于软件开发“钱景客观”!成了大家普遍认同的高赞回答! 网友高赞回复:“没有钱啊,肯定要读计算机, 不读没有钱用。就是读计算机这种东西,才能维持得了生活这样子。” 02.理想和现实 当我们怀着对软件行业高工资美好憧憬,开始撸起袖子加油干的时候,你会发现,你的人生很可能是这样的: 对不起,在互联网这个行业,高强度的工作负荷,不是只对程序员制定的,是对所有的人,包括测试、项目经理、架构师。 下面是某高龄程序员切身感受: 1.明显感觉脑力跟不上了,容易疲劳。以前可以专心想一个算法很长时间,现在想一会就觉得注意力难以集中,容易犯困。 2.记性没有以前好了。看过的资料,吸收起来没有年轻时那么快。就算记住了,也很容易忘掉。 3.体力也没有以前好了。以前我可以连续通宵编程一周,每天只睡4个小时;或者完全不睡觉72小时编程。现在如果敢通宵一晚的话,后面3天都觉得身心俱疲,缓不过来。 但显然,程序员不可能长久的健康生猛,被淘汰只是时间的问题。 最近在职场论坛就看到这样的一位朋友,这位朋友已经37岁了,没想到在这个年纪被公司裁员了,找了四个月的工作,大公司都不愿意要他,都是因为他的年纪太大,所以大公司都把他给拒绝了。 03. 如何转行 一位35岁的程序员分享了自己的转行经历,之前在传统IT和互联网行业工作了12年,如今跟着老婆一起做外贸,半年时间净利润有70多万! 当然并不是所有的转行都成功,也有现身说法的 “我之前的公司技术转岗的很多,我一个同事30多岁了,也不想做技术,成为我们公司第一个吃螃蟹的人,他选择的方向是做工程一类,因为他们亲戚有做这一块的。他走时撂下一句话,告诉你们,如果我再做技术,我就去吃屎。过了没两个月,我们联系他,他又做技术了,问他何故,甲醛中毒,实在受不了那气味!还不如吃屎哈哈哈。哪一个行业都很难做啊。隔行如隔山!如果你要去转,需要慎重。 还有的同事转行做金融去了,没过几天,辛辛苦苦做技术赚的几十万块钱都赔进去了。想买房的首付也没了,转行没转成,倒是赔的裤衩都没了。无一例外,都失败了。因为对外界行业一窍不通!理想很丰满,现实很骨感啊!” 建议转岗需要遵守的原则: 第一:不要转不熟悉的岗位 不熟悉的岗位再好的也不合适,你在一个行业想要不痛苦,起码需要三年实际工作经验,冒险去转这类的岗位,不管你出于爱好、兴趣、还是自身定位,都是不靠谱的转行。任何一个职业都没有那么容易做的。哪个职业都不会给你带来快乐,你不讨厌就很好了。像技术而言,有多少其他职位看着技术很好呢。实际上呢,只有自己真实做一下才知道不容易。 第二:选择本行业内的岗位转 这是一个优势,互联网是一个非常大的行业,越是相关性的职业越容易转,越容易上手,互联网职业是很多的,也不只有技术,也不只有产品经理,但是从技术转产品是一条非常近也是现实的一条路。 第三:不要相信别人说的 转行如同小马过河一样,松鼠说水很深,老牛说水很浅,你不能拿别人的成功或失败的经验而定位自己,这样极大的错误,千万不能找案例,案例是最害人的,别人的案例适合自己的很少。必须和他相似性越高你越容易转。 04. 总结 拒绝焦虑,热爱技术的你,35岁还在堆代码的你,只要做的是自己真心喜欢的工作就不算loser,如果你还在成长架构师的路上不妨看看下文。 题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料免费分享出来。 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 需要的朋友,可以点击:https://shimo.im/docs/Q6V8xPVxHpkrtRtD免费领取! 希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展~
这次来面试的是一个有着5年工作经验的小伙,截取了一段对话如下: 面试官:我看你写到Glide,为什么用Glide,而不选择其它图片加载框架?小伙:Glide 使用简单,链式调用,很方便,一直用这个。面试官:有看过它的源码吗?跟其它图片框架相比有哪些优势?小伙:没有,只是在项目中使用而已~面试官:假如现在不让你用开源库,需要你自己写一个图片加载框架,你会考虑哪些方面的问题,说说大概的思路。小伙:额~,压缩吧。面试官:还有吗?小伙:额~,这个没写过。 说到图片加载框架,大家最熟悉的莫过于Glide了,但我却不推荐简历上写熟悉Glide,除非你熟读它的源码,或者参与Glide的开发和维护。 在一般面试中,遇到图片加载问题的频率一般不会太低,只是问法会有一些差异,例如: 简历上写Glide,那么会问一下Glide的设计,以及跟其它同类框架的对比 ; 假如让你写一个图片加载框架,说说思路; 给一个图片加载的场景,比如网络加载一张或多张大图,你会怎么做; 带着问题进入正文~ 一、谈谈Glide 1.1 Glide 使用有多简单? Glide由于其口碑好,很多开发者直接在项目中使用,使用方法相当简单 github.com/bumptech/gl… 1、添加依赖: implementation 'com.github.bumptech.glide:glide:4.10.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0' 2、添加网络权限 <uses-permission android:name="android.permission.INTERNET" /> 3、一句代码加载图片到ImageView Glide.with(this).load(imgUrl).into(mIv1); 进阶一点的用法,参数设置 RequestOptions options = new RequestOptions() .placeholder(R.drawable.ic_launcher_background) .error(R.mipmap.ic_launcher) .diskCacheStrategy(DiskCacheStrategy.NONE) .override(200, 100); Glide.with(this) .load(imgUrl) .apply(options) .into(mIv2); 使用Glide加载图片如此简单,这让很多开发者省下自己处理图片的时间,图片加载工作全部交给Glide来就完事,同时,很容易就把图片处理的相关知识点忘掉。 1.2 为什么用Glide? 从前段时间面试的情况,我发现了这个现象:简历上写熟悉Glide的,基本都是熟悉使用方法,很多3年-6年工作经验,除了说Glide使用方便,不清楚Glide跟其他图片框架如Fresco的对比有哪些优缺点。 首先,当下流行的图片加载框架有那么几个,可以拿 Glide 跟Fresco对比,例如这些: Glide: 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video) 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求) 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力) 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半) Fresco: 最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区) 大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存) 适用于需要高性能加载大量图片的场景 对于一般App来说,Glide完全够用,而对于图片需求比较大的App,为了防止加载大量图片导致OOM,Fresco 会更合适一些。并不是说用Glide会导致OOM,Glide默认用的内存缓存是LruCache,内存不会一直往上涨。 二、假如让你自己写个图片加载框架,你会考虑哪些问题? 首先,梳理一下必要的图片加载框架的需求: 异步加载:线程池 切换线程:Handler,没有争议吧 缓存:LruCache、DiskLruCache 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置 内存泄露:注意ImageView的正确引用,生命周期管理 列表滑动加载的问题:加载错乱、队满任务过多问题 当然,还有一些不是必要的需求,例如加载动画等。 2.1 异步加载: 线程池,多少个? 缓存一般有三级,内存缓存、硬盘、网络。 由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。 读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。 Glide 必然也需要多个线程池,看下源码是不是这样 public final class GlideBuilder { ... private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载 private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池 ... private GlideExecutor animationExecutor; //动画线程池 Glide使用了三个线程池,不考虑动画的话就是两个。 2.2 切换线程: 图片异步加载成功,需要在主线程去更新ImageView, 无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。 看下Glide 相关源码: class EngineJob<R> implements DecodeJob.Callback<R>,Poolable { private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory(); //创建Handler private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper(), new MainThreadCallback()); 问RxJava是完全用Java语言写的,那怎么实现从子线程切换到Android主线程的? 依然有很多3-6年的开发答不上来这个很基础的问题,而且只要是这个问题回答不出来的,接下来有关于原理的问题,基本都答不上来。 有不少工作了很多年的Android开发不知道鸿洋、郭霖、玉刚说,不知道掘金是个啥玩意,内心估计会想是不是还有叫掘银掘铁的(我不知道有没有)。 我想表达的是,干这一行,真的是需要有对技术的热情,不断学习,不怕别人比你优秀,就怕比你优秀的人比你还努力,而你却不知道。 2.3 缓存 我们常说的图片三级缓存:内存缓存、硬盘缓存、网络。 2.3.1 内存缓存 一般都是用LruCache Glide 默认内存缓存用的也是LruCache,只不过并没有用Android SDK中的LruCache,不过内部同样是基于LinkHashMap,所以原理是一样的。 // -> GlideBuilder#build if (memoryCache == null) { memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize()); } 既然说到LruCache ,必须要了解一下LruCache的特点和源码: 为什么用LruCache? LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。 LruCache 源码分析 public class LruCache<K, V> { // 数据最终存在 LinkedHashMap 中 private final LinkedHashMap<K, V> map; ... public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; // 创建一个LinkedHashMap,accessOrder 传true this.map = new LinkedHashMap<K, V>(0, 0.75f, true); } ... LruCache 构造方法里创建一个LinkedHashMap,accessOrder 参数传true,表示按照访问顺序排序,数据存储基于LinkedHashMap。 先看看LinkedHashMap 的原理吧 LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构, LinkedHashMap重写了 createEntry 方法。 看下HashMap 的 createEntry 方法 void createEntry(int hash, K key, V value, int bucketIndex) { HashMapEntry<K,V> e = table[bucketIndex]; table[bucketIndex] = new HashMapEntry<>(hash, key, value, e); size++; } HashMap的数组里面放的是HashMapEntry 对象 看下LinkedHashMap 的 createEntry方法 void createEntry(int hash, K key, V value, int bucketIndex) { HashMapEntry<K,V> old = table[bucketIndex]; LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old); table[bucketIndex] = e; //数组的添加 e.addBefore(header); //处理链表 size++; } LinkedHashMap的数组里面放的是LinkedHashMapEntry对象 LinkedHashMapEntry private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> { // These fields comprise the doubly linked list used for iteration. LinkedHashMapEntry<K,V> before, after; //双向链表 private void remove() { before.after = after; after.before = before; } private void addBefore(LinkedHashMapEntry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBefore和remove 方法,用于新增和删除链表节点。 LinkedHashMapEntry#addBefore将一个数据添加到Header的前面 private void addBefore(LinkedHashMapEntry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。 再看下LinkedHashMapEntry#remove private void remove() { before.after = after; after.before = before; } 链表节点的移除比较简单,改变指针指向即可。 再看下LinkHashMap的put 方法 public final V put(K key, V value) { V previous; synchronized (this) { putCount++; //size增加 size += safeSizeOf(key, value); // 1、linkHashMap的put方法 previous = map.put(key, value); if (previous != null) { //如果有旧的值,会覆盖,所以大小要减掉 size -= safeSizeOf(key, previous); } } trimToSize(maxSize); return previous; } LinkedHashMap 结构可以用这种图表示 LinkHashMap 的 put方法和get方法最后会调用trimToSize方法,LruCache 重写trimToSize方法,判断内存如果超过一定大小,则移除最老的数据 LruCache#trimToSize,移除最老的数据 public void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { //大小没有超出,不处理 if (size <= maxSize) { break; } //超出大小,移除最老的数据 Map.Entry<K, V> toEvict = map.eldest(); if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); //这个大小的计算,safeSizeOf 默认返回1; size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } } 对LinkHashMap 还不是很理解的话可以参考:图解LinkedHashMap原理 LruCache小结: LinkHashMap 继承HashMap,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。 LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。 2.3.2 磁盘缓存 DiskLruCache 依赖: implementation 'com.jakewharton:disklrucache:2.0.2' DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。简单看下remove操作: // DiskLruCache 内部也是用LinkedHashMap private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true); ... public synchronized boolean remove(String key) throws IOException { checkNotClosed(); validateKey(key); Entry entry = lruEntries.get(key); if (entry == null || entry.currentEditor != null) { return false; } //一个key可能对应多个value,hash冲突的情况 for (int i = 0; i < valueCount; i++) { File file = entry.getCleanFile(i); //通过 file.delete() 删除缓存文件,删除失败则抛异常 if (file.exists() && !file.delete()) { throw new IOException("failed to delete " + file); } size -= entry.lengths[i]; entry.lengths[i] = 0; } ... return true; } 可以看到 DiskLruCache 同样是利用LinkHashMap的特点,只不过数组里面存的 Entry 有点变化,Editor 用于操作文件。 private final class Entry { private final String key; private final long[] lengths; private boolean readable; private Editor currentEditor; private long sequenceNumber; ... } 2.4 防止OOM 加载图片非常重要的一点是需要防止OOM,上面的LruCache缓存大小设置,可以有效防止OOM,但是当图片需求比较大,可能需要设置一个比较大的缓存,这样的话发生OOM的概率就提高了,那应该探索其它防止OOM的方法。 方法1:软引用 回顾一下Java的四大引用: 强引用: 普通变量都属于强引用,比如 private Context context; 软应用: SoftReference,在发生OOM之前,垃圾回收器会回收SoftReference引用的对象。 弱引用: WeakReference,发生GC的时候,垃圾回收器会回收WeakReference中的对象。 虚引用: 随时会被回收,没有使用场景。 怎么理解强引用: 强引用对象的回收时机依赖垃圾回收算法,我们常说的可达性分析算法,当Activity销毁的时候,Activity会跟GCRoot断开,至于GCRoot是谁?这里可以大胆猜想,Activity对象的创建是在ActivityThread中,ActivityThread要回调Activity的各个生命周期,肯定是持有Activity引用的,那么这个GCRoot可以认为就是ActivityThread,当Activity 执行onDestroy的时候,ActivityThread 就会断开跟这个Activity的联系,Activity到GCRoot不可达,所以会被垃圾回收器标记为可回收对象。 软引用的设计就是应用于会发生OOM的场景,大内存对象如Bitmap,可以通过 SoftReference 修饰,防止大对象造成OOM,看下这段代码 private static LruCache<String, SoftReference<Bitmap>> mLruCache = new LruCache<String, SoftReference<Bitmap>>(10 * 1024){ @Override protected int sizeOf(String key, SoftReference<Bitmap> value) { //默认返回1,这里应该返回Bitmap占用的内存大小,单位:K //Bitmap被回收了,大小是0 if (value.get() == null){ return 0; } return value.get().getByteCount() /1024; } }; LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,也就是说通过SoftReference修饰的Bitmap就不会导致OOM。 当然,这段代码存在一些问题,Bitmap被回收的时候,LruCache剩余的大小应该重新计算,可以写个方法,当Bitmap取出来是空的时候,LruCache清理一下,重新计算剩余内存; 还有另一个问题,就是内存不足时软引用中的Bitmap被回收的时候,这个LruCache就形同虚设,相当于内存缓存失效了,必然出现效率问题。 方法2:onLowMemory 当内存不足的时候,Activity、Fragment会调用onLowMemory方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。 //Glide public void onLowMemory() { clearMemory(); } public void clearMemory() { // Engine asserts this anyway when removing resources, fail faster and consistently Util.assertMainThread(); // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687. memoryCache.clearMemory(); bitmapPool.clearMemory(); arrayPool.clearMemory(); } 方法3:从Bitmap 像素存储位置考虑 我们知道,系统为每个进程,也就是每个虚拟机分配的内存是有限的,早期的16M、32M,现在100+M,虚拟机的内存划分主要有5部分: 虚拟机栈 本地方法栈 程序计数器 方法区 堆 而对象的分配一般都是在堆中,堆是JVM中最大的一块内存,OOM一般都是发生在堆中。 Bitmap 之所以占内存大不是因为对象本身大,而是因为Bitmap的像素数据, Bitmap的像素数据大小 = 宽 高 1像素占用的内存。 1像素占用的内存是多少?不同格式的Bitmap对应的像素占用内存是不同的,具体是多少呢?在Fresco中看到如下定义代码 /** * Bytes per pixel definitions */ public static final int ALPHA_8_BYTES_PER_PIXEL = 1; public static final int ARGB_4444_BYTES_PER_PIXEL = 2; public static final int ARGB_8888_BYTES_PER_PIXEL = 4; public static final int RGB_565_BYTES_PER_PIXEL = 2; public static final int RGBA_F16_BYTES_PER_PIXEL = 8; 如果Bitmap使用 RGB_565 格式,则1像素占用 2 byte,ARGB_8888 格式则占4 byte。在选择图片加载框架的时候,可以将内存占用这一方面考虑进去,更少的内存占用意味着发生OOM的概率越低。 Glide内存开销是Picasso的一半,就是因为默认Bitmap格式不同。 至于宽高,是指Bitmap的宽高,怎么计算的呢?看BitmapFactory.Options 的 outWidth /** * The resulting width of the bitmap. If {@link #inJustDecodeBounds} is * set to false, this will be width of the output bitmap after any * scaling is applied. If true, it will be the width of the input image * without any accounting for scaling. * * <p>outWidth will be set to -1 if there is an error trying to decode.</p> */ public int outWidth; 看注释的意思,如果 BitmapFactory.Options 中指定 inJustDecodeBounds 为true,则为原图宽高,如果是false,则是缩放后的宽高。所以我们一般可以通过压缩来减小Bitmap像素占用内存。 扯远了,上面分析了Bitmap像素数据大小的计算,只是说明Bitmap像素数据为什么那么大。那是否可以让像素数据不放在java堆中,而是放在native堆中呢?据说Android 3.0到8.0 之间Bitmap像素数据存在Java堆,而8.0之后像素数据存到native堆中,是不是真的?看下源码就知道了~ 8.0 Bitmap java层创建Bitmap方法 public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height, @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) { ... Bitmap bm; ... if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) { //最终都是通过native方法创建 bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null); } else { bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, d50.getTransform(), parameters); } ... return bm; } Bitmap 的创建是通过native方法 nativeCreate 对应源码 8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp //Bitmap.cpp static const JNINativeMethod gBitmapMethods[] = { { "nativeCreate", "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;", (void*)Bitmap_creator }, ... JNI动态注册,nativeCreate 方法 对应 Bitmap_creator; //Bitmap.cpp static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors, jint offset, jint stride, jint width, jint height, jint configHandle, jboolean isMutable, jfloatArray xyzD50, jobject transferParameters) { ... //1\. 申请堆内存,创建native层Bitmap sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap, NULL); if (!nativeBitmap) { return NULL; } ... //2.创建java层Bitmap return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable)); } 主要两个步骤: 申请内存,创建native层Bitmap,看下allocateHeapBitmap方法 [8.0.0_r4/xref/frameworks/base/libs/hwui/hwui/Bitmap.cpp](https://www.androidos.net.cn/android/8.0.0_r4/xref/frameworks/base/libs/hwui/hwui/Bitmap.cpp) // static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable) { // calloc 是c++ 的申请内存函数 void* addr = calloc(size, 1); if (!addr) { return nullptr; } return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable)); } 可以看到通过c++的 calloc 函数申请了一块内存空间,然后创建native层Bitmap对象,把内存地址传过去,也就是native层的Bitmap数据(像素数据)是存在native堆中。 创建java 层Bitmap //Bitmap.cpp jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, int density) { ... BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap); //通过JNI回调Java层,调用java层的Bitmap构造方法 jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets); ... return obj; } env->NewObject,通过JNI创建Java层Bitmap对象,gBitmap_class,gBitmap_constructorMethodID这些变量是什么意思,看下面这个方法,对应java层的Bitmap的类名和构造方法。 //Bitmap.cpp int register_android_graphics_Bitmap(JNIEnv* env) { gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap")); gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J"); gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V"); gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V"); gBitmap_getAllocationByteCountMethodID = GetMethodIDOrDie(env, gBitmap_class, "getAllocationByteCount", "()I"); return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods, NELEM(gBitmapMethods)); } 8.0 的Bitmap创建就两个点: 创建native层Bitmap,在native堆申请内存。 通过JNI创建java层Bitmap对象,这个对象在java堆中分配内存。 像素数据是存在native层Bitmap,也就是证明8.0的Bitmap像素数据存在native堆中。 7.0 Bitmap 直接看native层的方法, /7.0.0_r31/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp //JNI动态注册 static const JNINativeMethod gBitmapMethods[] = { { "nativeCreate", "([IIIIIIZ)Landroid/graphics/Bitmap;", (void*)Bitmap_creator }, ... static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors, jint offset, jint stride, jint width, jint height, jint configHandle, jboolean isMutable) { ... //1.通过这个方法来创建native层Bitmap Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL); ... return GraphicsJNI::createBitmap(env, nativeBitmap, getPremulBitmapCreateFlags(isMutable)); } native层Bitmap 创建是通过GraphicsJNI::allocateJavaPixelRef,看看里面是怎么分配的, GraphicsJNI 的实现类是Graphics.cpp android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap, SkColorTable* ctable) { const SkImageInfo& info = bitmap->info(); size_t size; //计算需要的空间大小 if (!computeAllocationSize(*bitmap, &size)) { return NULL; } // we must respect the rowBytes value already set on the bitmap instead of // attempting to compute our own. const size_t rowBytes = bitmap->rowBytes(); // 1\. 创建一个数组,通过JNI在java层创建的 jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime, gVMRuntime_newNonMovableArray, gByte_class, size); ... // 2\. 获取创建的数组的地址 jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj); ... //3\. 创建Bitmap,传这个地址 android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable); wrapper->getSkBitmap(bitmap); // since we're already allocated, we lockPixels right away // HeapAllocator behaves this way too bitmap->lockPixels(); return wrapper; } 可以看到,7.0 像素内存的分配是这样的: 通过JNI调用java层创建一个数组 然后创建native层Bitmap,把数组的地址传进去。 由此说明,7.0 的Bitmap像素数据是放在java堆的。 当然,3.0 以下Bitmap像素内存据说也是放在native堆的,但是需要手动释放native层的Bitmap,也就是需要手动调用recycle方法,native层内存才会被回收。这个大家可以自己去看源码验证。 native层Bitmap 回收问题 Java层的Bitmap对象由垃圾回收器自动回收,而native层Bitmap印象中我们是不需要手动回收的,源码中如何处理的呢? 记得有个面试题是这样的: 说说final、finally、finalize 的关系 三者除了长得像,其实没有半毛钱关系,final、finally大家都用的比较多,而 finalize 用的少,或者没用过,finalize 是 Object 类的一个方法,注释是这样的: /** * Called by the garbage collector on an object when garbage collection * determines that there are no more references to the object. * A subclass overrides the {@code finalize} method to dispose of * system resources or to perform other cleanup. * <p> ...**/ protected void finalize() throws Throwable { } 意思是说,垃圾回收器确认这个对象没有其它地方引用到它的时候,会调用这个对象的finalize方法,子类可以重写这个方法,做一些释放资源的操作。 在6.0以前,Bitmap 就是通过这个finalize 方法来释放native层对象的。 6.0 Bitmap.java Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) { ... mNativePtr = nativeBitmap; //1.创建 BitmapFinalizer mFinalizer = new BitmapFinalizer(nativeBitmap); int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0); mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount); } private static class BitmapFinalizer { private long mNativeBitmap; // Native memory allocated for the duration of the Bitmap, // if pixel data allocated into native memory, instead of java byte[] private int mNativeAllocationByteCount; BitmapFinalizer(long nativeBitmap) { mNativeBitmap = nativeBitmap; } public void setNativeAllocationByteCount(int nativeByteCount) { if (mNativeAllocationByteCount != 0) { VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount); } mNativeAllocationByteCount = nativeByteCount; if (mNativeAllocationByteCount != 0) { VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount); } } @Override public void finalize() { try { super.finalize(); } catch (Throwable t) { // Ignore } finally { //2.就是这里了, setNativeAllocationByteCount(0); nativeDestructor(mNativeBitmap); mNativeBitmap = 0; } } } 在Bitmap构造方法创建了一个 BitmapFinalizer类,重写finalize 方法,在java层Bitmap被回收的时候,BitmapFinalizer 对象也会被回收,finalize 方法肯定会被调用,在里面释放native层Bitmap对象。 6.0 之后做了一些变化,BitmapFinalizer 没有了,被NativeAllocationRegistry取代。 例如 8.0 Bitmap构造方法 Bitmap(long nativeBitmap, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) { ... mNativePtr = nativeBitmap; long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount(); // 创建NativeAllocationRegistry这个类,调用registerNativeAllocation 方法 NativeAllocationRegistry registry = new NativeAllocationRegistry( Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize); registry.registerNativeAllocation(this, nativeBitmap); } NativeAllocationRegistry 就不分析了, 不管是BitmapFinalizer 还是NativeAllocationRegistry,目的都是在java层Bitmap被回收的时候,将native层Bitmap对象也回收掉。 一般情况下我们无需手动调用recycle方法,由GC去盘它即可。 上面分析了Bitmap像素存储位置,我们知道,Android 8.0 之后Bitmap像素内存放在native堆,Bitmap导致OOM的问题基本不会在8.0以上设备出现了(没有内存泄漏的情况下),那8.0 以下设备怎么办?赶紧升级或换手机吧~ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n0KxaBb4-1586487920417)(https://upload-images.jianshu.io/upload_images/18452536-b7c1fbc78c5905b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)] 我们换手机当然没问题,但是并不是所有人都能跟上Android系统更新的步伐,所以,问题还是要解决~ Fresco 之所以能跟Glide 正面交锋,必然有其独特之处,文中开头列出 Fresco 的优点是:“在5.0以下(最低2.3)系统,Fresco将图片放到一个特别的内存区域(Ashmem区)” 这个Ashmem区是一块匿名共享内存,Fresco 将Bitmap像素放到共享内存去了,共享内存是属于native堆内存。 Fresco 关键源码在 PlatformDecoderFactory 这个类 public class PlatformDecoderFactory { /** * Provide the implementation of the PlatformDecoder for the current platform using the provided * PoolFactory * * @param poolFactory The PoolFactory * @return The PlatformDecoder implementation */ public static PlatformDecoder buildPlatformDecoder( PoolFactory poolFactory, boolean gingerbreadDecoderEnabled) { //8.0 以上用 OreoDecoder 这个解码器 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads(); return new OreoDecoder( poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads)); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //大于5.0小于8.0用 ArtDecoder 解码器 int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads(); return new ArtDecoder( poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads)); } else { if (gingerbreadDecoderEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { //小于4.4 用 GingerbreadPurgeableDecoder 解码器 return new GingerbreadPurgeableDecoder(); } else { //这个就是4.4到5.0 用的解码器了 return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool()); } } } } 8.0 先不看了,看一下 4.4 以下是怎么得到Bitmap的,看下GingerbreadPurgeableDecoder这个类有个获取Bitmap的方法 //GingerbreadPurgeableDecoder private Bitmap decodeFileDescriptorAsPurgeable( CloseableReference<PooledByteBuffer> bytesRef, int inputLength, byte[] suffix, BitmapFactory.Options options) { // MemoryFile :匿名共享内存 MemoryFile memoryFile = null; try { //将图片数据拷贝到匿名共享内存 memoryFile = copyToMemoryFile(bytesRef, inputLength, suffix); FileDescriptor fd = getMemoryFileDescriptor(memoryFile); if (mWebpBitmapFactory != null) { // 创建Bitmap,Fresco自己写了一套创建Bitmap方法 Bitmap bitmap = mWebpBitmapFactory.decodeFileDescriptor(fd, null, options); return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null"); } else { throw new IllegalStateException("WebpBitmapFactory is null"); } } } 捋一捋,4.4以下,Fresco 使用匿名共享内存来保存Bitmap数据,首先将图片数据拷贝到匿名共享内存中,然后使用Fresco自己写的加载Bitmap的方法。 Fresco对不同Android版本使用不同的方式去加载Bitmap,至于4.4-5.0,5.0-8.0,8.0 以上,对应另外三个解码器,大家可以从PlatformDecoderFactory 这个类入手,自己去分析,思考为什么不同平台要分这么多个解码器,8.0 以下都用匿名共享内存不好吗?期待你在评论区跟大家分享~ 2.5 ImageView 内存泄露 曾经在Vivo驻场开发,带有头像功能的页面被测出内存泄漏,原因是SDK中有个加载网络头像的方法,持有ImageView引用导致的。 当然,修改也比较简单粗暴,将ImageView用WeakReference修饰就完事了。 事实上,这种方式虽然解决了内存泄露问题,但是并不完美,例如在界面退出的时候,我们除了希望ImageView被回收,同时希望加载图片的任务可以取消,队未执行的任务可以移除。 Glide的做法是监听生命周期回调,看 RequestManager 这个类 public void onDestroy() { targetTracker.onDestroy(); for (Target<?> target : targetTracker.getAll()) { //清理任务 clear(target); } targetTracker.clear(); requestTracker.clearRequests(); lifecycle.removeListener(this); lifecycle.removeListener(connectivityMonitor); mainHandler.removeCallbacks(addSelfToLifecycle); glide.unregisterRequestManager(this); } 在Activity/fragment 销毁的时候,取消图片加载任务,细节大家可以自己去看源码。 2.6 列表加载问题 图片错乱 由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。 常规的做法是给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。 当然,可以在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架做还是放在UI做比较合适。 线程池任务过多 列表滑动,会有很多图片请求,如果是第一次进入,没有缓存,那么队列会有很多任务在等待。所以在请求网络图片之前,需要判断队列中是否已经存在该任务,存在则不加到队列去。 总结 本文通过Glide开题,分析一个图片加载框架必要的需求,以及各个需求涉及到哪些技术和原理。 异步加载:最少两个线程池 切换到主线程:Handler 缓存:LruCache、DiskLruCache,涉及到LinkHashMap原理 防止OOM:软引用、LruCache、图片压缩没展开讲、Bitmap像素存储位置源码分析、Fresco部分源码分析 内存泄露:注意ImageView的正确引用,生命周期管理 列表滑动加载的问题:加载错乱用tag、队满任务存在则不添加 最后 学习技术是一条漫长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持! 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2020年的高频面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】点击下载https://shimo.im/docs/w6cyqyXqKRPDGcrr前往免费领取! 作者:蓝师傅_Android链接:https://juejin.im/post/5dbeda27e51d452a161e00c8来源:掘金
背景 2020年1月5号,朋友辞去了北京一家小创公司Android开发的工作,准备春节过后寻找新的工作的时候,突然遇到了新冠疫情的爆发,至今赋闲在家。昨天接到同行好友的电话,要和我聊聊天。告诉我再找不到工作就考虑转行了! 话语间无不感叹安卓有点强弩之末的味道!聊天过程中,朋友也顺带分享了一波阿里饿了么、百度Android岗的面经。我也帮他内推了几份工作岗位,却因为技术欠缺和经验不足等问题被友好的回绝了! 饿了么Android岗一面1:双亲委托机制?2:插件化原理?3:垃圾回收机制及其优缺点?4:PathClassLoader和DexClassLoader区别?5:说下Binder?6:Android旋转屏幕后Activity生命周期,Bundle是存储在哪儿的?7:事件分发机制?8:Handler实现?9:Java内存?10:OkHttp设计模式?11:算法。 Http和Https的区别 HashMap的具体实施原理,HashMap和Hashset的区别 Java的垃圾回收机制 JVM的原理及线上调优 string,stringbulider,stringbuffer的区别 Java的设计模式 在白纸上手写二分法排序算法,这道题我在lintcode上面做过原题 有哪些可以保持进程同步的方法 如何避免死锁 常见的数据结构有哪些 leetcode 原题:查找单链表中倒数第K个节点的算法,面试官要求手写 百度Android岗一面1:算法:寻找出现超过一半的数字?2:HashMap原理?3:热更新原理?4:AstncTask+HttpClient 与 AsyncHttpClient有什么区别?5:Https握手过程?6:RecycleView原理?7:RecycleView的使用?8:Hybrid通信原理是什么,有做研究吗?9:ListView图片加载错乱的原理和解决方案?10:对称加密与非对称加密?11:TCP、UDP差别?12:TCP保证可靠的手段? 初级、中级 Android 工程师可能因离职而失业吗? 针对朋友找工作难的问题,我特意咨询了我在腾讯负责面试的朋友,为何Android开发普遍感觉找工作难。最后我们得出的结论基本一致:供需极度的不平衡。 这段时间他都在帮部门招人,在拉钩上也挂了JD,一个多月的时间收到的简历估计有几百份,他发起了面试的不超过5个,最后硬着头皮递上去1个还被刷了。 可问题就在这里,据他反馈大公司都在招Android开发,很多部门还非常着急,但就是招不到合适的人。别跟他说现在做Android的人很多,质量是关键,数量一点意义都没有。每年池子里的人就那么多,一份好的简历出来不止公司之间,部门之间甚至是部门内都在抢,但这批人之外,绝大部分人的简历能占用的时间不会超过5秒。 以前我们沟通时他就提到过这个问题,在现在这个阶段,公司之间的技术鸿沟已经非常明显,开发者身在其中,也因为自身学习能力,环境,项目等等的差别,技术上的差距越拉越大,造成了工作几年之后的两级分化,并且这种差距的拉开不是靠自己的努力就能弥补的。 我这个Android开发八年的朋友,离职后缺乏大型项目经验,至今未找到合适的工作!属于一个典型温水煮青蛙的案例! Android学习路线指南 那面对这种情况,作为开发者,我们能做的是什么?最基本的,脱离舒适区,不停磨练自己的技术。 工作前三年是职业生涯中成长最快的几年,在这段时间里你会充满激情,做事专注,也容易养成良好的习惯。在大公司有些同学在前三年中就快速成为某一个领域的技术专家,有些同学也可能止步不前。接下来和大家一起探讨下如何在三年内快速成长为一名技术专家。 目录 学习方法 1:掌握良好的学习心态2:掌握系统化的学习方法3:知识如何内化成能力4:广度和深度的选择 1.掌握良好的学习心态 空杯心态首先要有空杯的学习心态,而不是傲娇自满,故步自封,空杯子才可以装下更多的东西。首先要学会取百家之长,带着欣赏的眼光看团队的同事或学校的同学,欣赏每位同事或同学的优点,然后吸取他们的优点,每个同事都有其擅长的能力,比如有的同事技术能力强,那么可以观察下他如何学习的(或者找他请教学习方法),有的同学擅长解决线上问题,那么观察他是如何解决线上问题的,解决思路是什么?如果他解决不了时,他是如何寻求帮助。有的同学擅长使用IDE或MAC的快捷键,那么可以向他学习提高工作效率。有的同学能快速理解业务知识,观察他是如何做到的,自己如何达到他的程度。沟通能力,解决问题能力以及规划能力都可以向同事学习。 坚持学习有的同学可能工作了五年,但是学习的时间可能一年都不到。学技术不能急于求成,只要学习方法正确,量变一定会引起质变。 2.掌握系统化的学习方法如果学习到的知识不成体系,那么遇到问题时就会非常难解决。有些同学会出现这些情况,比如编码时遇到问题百度搜索,如果百度上找不到答案,这个问题就解决不了。再比如,在开发中要用到某个技术点,就学习下API,程序调通后就不再深入研究,浅尝辄止,如果程序遇到其他问题也不知道如何解决。 以上情况我认为叫点状学习。遇到一个问题,解决一个问题,需要一项技术,学习一项技术。那么如何由点到面,由面到体,形成系统化学习呢。 首先要确定学习的知识领域,需要达成的学习目标,针对目标制定学习计划,就像你要写一本书一样,先把目录写出来,然后根据目录上的知识点逐步去学习,最后把这些知识点关联起来,形成一个系统化的知识体系。学习的时候,可以制定一个计划,以周为单位,比如第一周学什么,第二周学什么。 比如我们Android开发,学习进阶路线是: 3.知识如何内化成能力成长必须经历一个步骤,就是把知识内化成能力。知识是用脑记住的,能力是用手练习出来的。在工作的几年里,我们可能看过很多书,听过很多技术讲座和视频,但是通过听和看只是让你能记住这些知识,这些知识还不能转换成你的能力。 听和看只是第一步,更重要的是实践,通过刻意练习把听到和看到的知识内化成你的能力。 刻意练习,就是有目的的练习,先规划好,再去练习。 4.广度和深度的选择技术人员的学习路径有两个维度,深度和广度。很多程序员都有这个疑问,是先深后广,还是先广后深呢? 通过这么多年的学习和思考,我的建议先深后广,因为当技术学到一定深度后,就会有触类旁通的能力,自己掌握的广度也自然有了深度。但是在实际学习过程中,深度和广度相互穿插着学习,比如学习并发编程时,首先学习JDK源码,然后学进去之后,开始看JVM源码,最后看CPU架构,在技术点逐渐深度研究的过程中,广度也得到了完善。 所以无论哪种学习方式,学习态度才是最重要的,在广度学习的时候有深入研究的态度就能达到一定的深度,在深度学习的时候,主动学习相关的技术点,广度也得到拓宽。 最后 题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。 我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料免费分享出来。 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 需要的朋友,可以点击https://shimo.im/docs/Q6V8xPVxHpkrtRtD前往免费领取! 希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展~
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 原文出处:https://dzone.com/articles/cross-platform-mobile-development-2020-trends-and 多年来,跨平台移动开发已经获得了最流行软件开发趋势之一的声誉。这并不令人意外,因为采用跨平台开发技术使得软件工程师使用同一代码就能为不同平台构建应用程序,从而节省时间、金钱以及不必要的工作。 移动市场的现状 截至2019年12月,全球活跃网民已超45亿。他们每人平均上网时间为6小时42分钟,相当于每年上网超过100天。再加上人们越来越渴望从掌上设备中获取海量的信息,也就为之所以移动应用程序会如此受到欢迎提供了合理的解释。截至 2019 年,全球移动应用收入达 4610 亿美元,预计到 2023 年,付费下载和应用内广告的收入预计将超过 9350 亿美元。 移动开发的技术选型 十年前,老板们必须决定他们的产品将涵盖哪些移动操作系统:Android、iOS、微软、RIM或Symbian。而今天,初创公司的创始人正面临着一个不同的两难抉择,由于Android和iOS占据了移动操作系统市场份额的98%,很显然这两个系统不容忽视,覆盖什么平台不再是问题。但问题是,构建一个在两个平台上都可以使用的应用程序应该采用什么方法? 每个操作系统对应一种开发环境 顾名思义,用于开发Android用的是Java或Kotlin,用于开发iOS则是Objective-C或SWIFT。作为开发不同应用而使用不同的开发语言,对开发者而言并不是一个好消息。虽然特定的开发环境对特定的操作系统拥有对资源更高效的调配效率,可防止发生性能问题。但缺点也很显而易见,你的开发人员需要使用不同的开发语言构建两个独立的应用程序,这需要付出更多的时间、金钱和精力。 渐进式Web应用程序(PWA) 其中一个能解决问题的例子是渐进式 Web 应用(PWA),它基本上是模仿原生应用程序行为的一个网站(例如,在发送推送通知、脱机工作,或者只是添加到移动设备的主屏幕上)。然而,就像任何其他选项一样,PWA也不是完美无缺的,因为它们消耗更多的电池,并且不能授予应用使用设备的所有功能。 跨平台应用程序开发 但还好我们还有一个跨平台开发的选项,它允许用一段代码同时为两个操作系统开发应用。它并不固定使用某一种平台的编程语言编写代码。而且,由于直接使用了系统原生控件来呈现界面,它能为用户提供近乎原生平台应用的使用体验。 我要不要使用跨平台开发这项技术? 下面,我会通过一系列维度来帮助你去评估你是否应该采用跨平台开发这种形式来适配你的业务。 平台 首先,也是最重要的,您需要决定您的应用程序是需要在一个还是多个操作系统上可用。如果您的目标群是由不同平台的用户组成的,那么跨平台开发将是首选的解决方案。 另一方面,如果你的用户群体只是Android或iOS的某一支,那么用原生解决方案来开发是你的首选。 复杂性 此标准涉及你希望与产品走多远。解决此问题的一种方法是你的目标是使用MVP测试你的愿景,或是你准备使用成熟的应用程序开始运行。您需要回答的另一个问题是产品的功能(例如,访问移动设备的硬件或特定于平台的功能)。 原生体验 你的用户是否需要使用原生或近似原生的体验。使用Material Design(Android)或Human Interface Guidance(iOS)来设计的移动应用程序是移动产品对用户直观且友好的原因所在。在设计移动应用程序时应要考虑这些,但是,你可以使用跨平台框架来实现类似的效果。 时间和成本 有一点是肯定的,原生开发成本不低、效率也不高。为不同的平台构建不同的应用程序需要雇佣更多的开发人员,这可能会导致初创公司在项目初期就超出紧张的项目预算。同时,如果采用跨平台的方法,你可以将项目外包给一个规模较小但同样专业的团队,这既是一个省时的解决方案,也是一个具有成本效益的解决方案。 跨平台移动应用开发的优点(和缺点) 假设你已经得出结论,你更倾向于跨平台的移动应用程序开发,但是在下决心之前,你需要对此解决方案的优缺点进行彻底的了解,没关系,下面我逐一为你列举。 跨平台移动应用程序开发的好处 更广泛的市场覆盖范围 虽然我们每个人都有自己喜欢的移动操作系统,但个人喜好不会妨碍你业务的成功。让Android和iOS用户同时可以使用您的移动应用,能在未来提升更高的收录打下基础。 一套代码 跨平台开发允许您同时编写包含多个操作系统的代码(有时也会有处理平台差异)。尽管如此,一套代码肯定会影响软件开发过程中的所有阶段,因为它可能为你节省通常花在修复和升级两组独立代码上的成本。 更高效的发布流程 尽管只需要一套代码,但跨平台应用程序开发仍然需要开发人员考虑处理系统差异的方法,例如发布应用到平台商店的过程。 这种方法将缩短从设计到发布的时间。换句话讲,这可以为你节省很大一笔初始项目预算。 平台一致性 毫无疑问,Android和iOS在用户体验和用户界面方面都有很大的不同,这些差异中的大多数部分都能通过跨平台开发框架帮你默认处理,这使得设计和实际表现不一致的情况发生的可能性进一步降低。 有什么缺点? 尽管有上述各种优点,但它也绝不是一点缺点没有,它的主要缺点包括性能可能较低及略差的用户体验和用户界面等。 2020年还有哪些跨平台移动开发框架值得考虑 虽然跨平台的移动APP开发有利有弊。但从业务初创的角度来看,优点应该是大于缺点的。而且,随着对跨平台移动应用需求的不断增长,现在可用的工具和框架数量也已经很可观了。 但选择过多会令人头疼,这就是为什么我们只关注最突出的跨平台移动开发框架的原因:React Native, Flutter, NativeScript, 和Xamarin。 为了让你更深入地了解是什么使这些工具成为2020年软件开发的可选选项,我们将根据以下标准对它们进行打分:社区支持、基于的编程语言、代码可重用性、性能、界面以及使用它们构建的重要应用程序。 React Native Reaction Native是Facebook于2015年发布的开源、跨平台的应用开发框架。作为2013年举办的一场内部黑客马拉松的产物,它已经成为最受欢迎的原生App开发替代方案之一,拥有2043名GitHub贡献者,获得了超过82900 GitHub标星。不断增长的社区认知度使得找到一支可靠且经验丰富的开发团队来承接你的项目变得相对容易。 Learn Once and Write Anywhere 基于React.JS,React Native利用JavaScript(根据2019年Stack Overflow的调查,JavaScript成为了最受欢迎的编程语言),为Android和iOS用户提供真正原生的应用外观和体验。另外,使该框架脱颖而出的是,如果你需要,React Native允许你使用Java、Objective-C或SWIFT编写部分原生模块来顺利处理复杂的操作,如视频播放或图像编辑。 虽然这些组件不能在不同的平台之间共享,并且需要开发人员做更多的工作,但多达90%的React Native代码是可以重用的。很好地表明该框架的座右铭不是“Write Once, Use Anywhere”,而是“learn once, write anywhere”。 就GUI而言,React Native可以提供接近原生的用户体验,这要归功于它使用了Android和iOS的本地控制器。它还使用带有UI元素的ReactJS库,这有助于加快UI设计过程。在开发移动应用程序时,使此框架值得考虑的另一个原因是,它可用在不丢失应用程序状态的情况下对UI进行更改。 另一个使React Native成为2020年跨平台移动开发框架的首选之一,是因为持续的更新,例如近期的版本 0.60 和 0.61 : 多项辅助功能改进。 更清晰、更人性化的开始屏幕。 快速刷新,融合了实时和热重新加载,从而显著加快了开发进程。 如上的Release Note只是React Native适应不断变化的需求其中一个很小的样本。 Flutter 2020年值得考虑的第二个框架是Flutter。它在Google I/O 2017上宣布,并于2018年发布,对于跨平台的世界来说,它现在仍然是一个“新人”。但尽管如此,它已经获得了超过80500 GitHub星标和绝大多数工程师将其称为2019年Stack Overflow调查中最受欢迎的三个框架之一,Flutter无疑是一股不可忽视的力量。 Dart是如何使Flutter变得独一无二的 Flutter 背后的编程语言是 Dart,谷歌称之为"客户端优化",适合在任何平台上"快速构建应用程序"。它于 2011 年推出,是一种响应式面向对象的语言,被开发者认为相对容易学习,其中原因有二:第一,语法上它借鉴了C/C++ 和 Java; 第二,在官方网站上,您可以找到内容广泛且相当简单的文档。值得一提的是,Dart 附带了大量Flutter 兼容软件包的软件包,允许您使应用程序更加复杂。 Flutter的一个主要优势是,它的性能比本文提到的任何其他跨平台移动开发框架都要好。这归功于Dart的编译器和Flutter拥有自己的一套小部件。结果是它能更快、更直接地与平台直接通信,而不需要JavaScript桥(例如,Reaction Native就是这种情况)。说到小部件:通过Flutter的“UI-as-a-code”方法,它们只用DART编写,这就提高了代码的可重用性。 效率与用户体验和界面密不可分。如前所述,Flutter不依赖于一组原生组件,而是利用可视化、结构化、平台性和交互式小部件进行UI的设计,所有这些都由框架的图形引擎呈现。更重要的是,Flutter留下了很大的定制空间,如果你想要设计一个很完美的UI,它是个很好的选择。 说到Flutter的更新,最新的稳定版本是在12月12日发布的,根据官方发布说明,它合并了来自188个贡献者的近2000个pull。例如,版本1.12.13中包括的改进: 重大的API变动。 新功能,例如SliverOpacity小部件和SliverAnimatedList。 修复了崩溃和性能问题。 Beta版中的Web支持。 这不是一个完整的清单,因为Flutter的目标是让每年发布的四个版本中的每一个版本都能为框架的可用性提升一个台阶。 Flutter是一个年轻的跨平台移动应用程序开发框架,所以它没有像React Native受到众多的大公司青睐也是不足为奇的。然而,这并不意味着它不好,截至2019年12月,它也为阿里巴巴、谷歌广告、Groupon等众多公司和业务所采用。 NativeScript 如果你要开始开发你的产品,“React Native”和“Flutter”绝不是唯一的解决方案。在 2020 年初,适合您的企业的替代框架也可能是 NativeScript。 这个开源框架于2015年3月公开发布,并迅速成为广受欢迎的解决方案。例如,在发布后的短短两个月内,它就获得了3000颗GitHub星标,并在Twitter上吸引了1500多名粉丝的关注。到今天为止,市场上已有超过700个插件可供选择。 在使用NativeScript构建跨平台应用程序时,开发人员首先用JavaScript及其超集TypeScript编写代码。然后,将代码库编译成各自平台原生的编程语言。 另外值得一提的是,使用 NativeScript 的开发人员也可以使用第三方库(CocoaPods 和 Android SDK),而无需包装。 与React Native类似,NativeScript允许访问Android和iOS原生API,这对跨平台应用程序有明显的积极影响。然而,不同之处在于,前者需要构建桥接API,而后者(用Progress首席开发者倡导者TJ VanToll的话说是“将所有iOS和Android API注入JavaScript虚拟机”)。与Facebook框架的另一个相似之处在于代码重用,在这两种情况下都可以达到90%。 Xamarin Xamarin开源框架创建于2011年,这使它成为了这个列表中最“古老“的框架,但直到五年前它被微软收购时,它才获得了发展势头。截至今天,它号称拥有超过6万名贡献者的社区。 从技术上讲,要用Xamarin构建跨平台的移动应用,需要很好地掌握.NET和C#两种技术,前者是使用多种语言(包括C#编程语言)、编辑器和库的开发平台。Xamarin用一组工具补充了上述平台,这些工具有助于构建跨平台应用程序,例如库、编辑器扩展和XAML。第二种技术是C#,这是一种面向对象的编程语言,它被认为比JavaScript学习起来稍难。Xamarin利用这种编程语言编写整个应用程序,从后端到原生API,再到业务逻辑。 Xamarin.Native和Xamarin.Forms Xamarin与其他框架的不同之处在于,它提供了两种编译跨平台移动应用的方式:Xamarin Native(也称为Xamarin.Android/iOS)和Xamarin.Forms。前一种方法优先考虑共享业务逻辑,并通过使用本机接口控件实现近乎本机的性能。 后者侧重于共享代码,而不是业务原理,这一方面会导致代码重用比例增加(使用Xamarin,开发人员可以重用高达96%的C#代码),但另一方面这样会降低代码性能。 您可能已经注意到,跨平台移动应用程序的性能和GUI密切相关,所以如果我说Xamarin构建应用程序的两种方法对界面的最终外观有很大影响,我可能不会感到惊讶。 Xamarin.Android/iOS允许开发人员使用原生控件和布局,而Xamarin.Forms基于标准UI元素,允许从单个API设计应用程序,但如果你需要更完美的原生UI,则可能还不够。 2020年跨平台应用程序开发还值得考虑吗? 不论如何,跨平台确实是一个值得考虑和极具前景的方向,特别是我们上面提到的 “React Native”和“Flutter”。 前者是一个成熟而稳定的框架,利用了最流行的编程语言之一,并拥有成熟的大型开发人员社区。后者是一个快速发展的技术,尽管它比React Native年轻的多,它也已经赢得了世界各地许多开发人员的青睐。 但无论您选择的是“React Native”、“Flutter”还是任何其他框架,跨平台方法都一定会为您节省时间和金钱,同时能为你最大限度地扩大市场覆盖范围。 最后,值不值得考虑,最终还是取决于你的业务目标、预算和时限。 文末 我们移动开发程序员不管移动市场环境技术如何变化,一定要清楚和明白自己的核心竞争力是什么?为什么? 学习能力,尤其是自学能力,我们很少看到那些有名的程序高手在论坛上问“学习XX该看什么书,如何快速学习XXX,学习XXX有什么代码推荐”之类的问题,他们想学什么很快就能自己找到相关资料。这个行业发展太快,技术淘汰的速度也很快,3年不学新东西就可能落伍了。 动手能力,都是看书看资料,当别人还在纠结看什么书,还在纠结书里的字句是什么意思的时候,有些人的几百上千行代码都已经能运行了。 耐心和毅力,做程序员兴趣固然重要,写自己喜欢的代码那是相当愉快的事情,但是程序开发中无论如何还有大量乏味无趣的事情,要能坚持,咬牙把这些做完。 作为一名移动开发者,很多人也都曾学习困惑!学什么,怎么学,通过什么渠道查找筛选资料。在这里我整理了一份阿里P7级别的Android架构师全套学习资料,通过借鉴学习成功的人整理的学习资料,避免少走弯路,快速实现架构进阶。 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC
在博主认为,对于Android面试以及进阶的最佳学习方法莫过于刷题+博客+书籍+总结,前三者博主将淋漓尽致地挥毫于这篇博客文章中,至于总结在于个人,实际上越到后面你会发现面试并不难,其次就是在刷题的过程中有没有去思考,刷题只是次之,这又是一个层次了,这里暂时不提后面再谈。 博主总结了一系列大厂面试中常问的面试技术点,深入解析以及答案,将为最近准备面试的各开发者去大厂保驾护航, 何谓面试? 博主所理解的面试,它是一个过程,是不断沉淀、不断总结、善于传达自己的专业领域技术以及解决问题能力的过程。以下是博主总结的一些面试题,文中如有错误,恳请批评指正! Java相关 容器(HashMap、HashSet、LinkedList、ArrayList、数组等) 内存模型 垃圾回收算法(JVM) 4、垃圾回收机制和调用 System.gc()的区别? 类加载过程(需要多看看,重在理解,对于热修复和插件化比较重要) 反射 多线程和线程池 设计模式(六大基本原则、项目中常用的设计模式、手写单例等) Java 四大引用 Java 的泛型 final、finally、finalize 的区别 接口、抽象类的区别 Android 相关 自定义 View 事件拦截分发 解决过的一些性能问题,在项目中的实际运用 性能优化工具 性能优化 (讲讲你自己项目中做过的性能优化) Http[s]请求慢的解决办法(DNS、携带数据、直接访问 IP) 缓存自己如何实现(LRUCache 原理) 图形图像相关:OpenGL ES 管线流程、EGL 的认识、Shader 相关 SurfaceView、TextureView、GLSurfaceView 区别及使用场景 动画、差值器、估值器(Android中的View动画和属性动画 - 简书、Android 动画 介绍与使用) MVC、MVP、MVVM Handler、ThreadLocal、AsyncTask、IntentService 原理及应用 Gradle(Groovy 语法、Gradle 插件开发基础) 热修复、插件化 组件化架构思路 系统打包流程 Android 有哪些存储数据的方式。 SharedPrefrence 源码和问题点; sqlite 相关 如何判断一个 APP 在前台还是后台? AMS 、PMS Activity 启动流程,App 启动流程 Binder 机制(IPC、AIDL 的使用) 为什么使用 Parcelable,好处是什么? Android 图像显示相关流程,Vsync 信号等 算法与数据结构 时间复杂度 / 空间复杂度 常用的排序算法有哪些? 字符串反转 链表反转(头插法) 如何查找第一个只出现一次的字符(Hash查找) 如何查找两个子视图的共同父视图? 无序数组中的中位数(快排思想) 如何给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。 二叉树前序、中序、后序遍历 最大 K 问题 广度、深度优先搜索算法 String 转 int。核心算法就三行代码,不过临界条件很多,除了判空,还需要注意负数、Integer 的最大最小值边界等; 如何判断一个单链表有环? 100 亿个单词,找出出现频率最高的单词。要求几种方案; 链表每 k 位逆序; 镜像二叉树; 找出一个无序数组中出现超过一半次数的数字; 计算二叉树的最大深度,要求非递归算法。 String 方式计算加法。 网络 1.网络框架对比和源码分析 网络七层协议有哪些? Http 和 Https 的区别?Https为什么更加安全? HTTPS的连接建立流程 解释一下 三次握手 和 四次挥手 TCP 和 UDP的区别 Cookie和Session DNS是什么? DNS解析过程 10.HTTP报文结构 11.HTTP与HTTPS的区别以及如何实现安全性 12.如何验证证书的合法性? 13.https中哪里用了对称加密,哪里用了非对称加密,对加密算法(如RSA)等是否有了解? 14.client如何确定自己发送的消息被server收到? 15.谈谈你对WebSocket的理解 16.WebSocket与socket的区别 17.谈谈你对安卓签名的理解。 18.请解释安卓为啥要加签名机制? 19.视频加密传输 20.App 是如何沙箱化,为什么要这么做? 21.权限管理系统(底层的权限是如何进行 grant 的)? 源码理解 Glide :加载、缓存、LRU 算法 (如何自己设计一个大图加载框架) (LRUCache 原理) EventBus LeakCanary ARouter 插件化(不同插件化机制原理与流派,优缺点。局限性) 热修复 RXJava (RxJava 的线程切换原理) Retrofit (Retrofit 在 OkHttp 上做了哪些封装?动态代理和静态代理的区别,是怎么实现的) OkHttp Kotlin 相关 1.从原理分析Kotlin的延迟初始化: lateinit var和by lazy 2.使用Kotlin Reified 让泛型更简单安全 3.Kotlin里的Extension Functions实现原理分析 4.Kotlin系列之顶层函数和属性 5.Kotlin 兼容 Java 遇到的最大的 “坑” 6.Kotlin 的协程用力瞥一眼 7.Kotlin 协程「挂起」的本质 8.到底什么是「非阻塞式」挂起?协程真的更轻量级吗? 9.资源混淆是如何影响到Kotlin协程的 10.Kotlin Coroutines(协程) 完全解析 11.破解 Kotlin 协程 Flutter相关 Dart 当中的 「..」表示什么意思? Dart 的作用域 Dart 是不是单线程模型?是如何运行的? Dart 是如何实现多任务并行的? 说一下Dart异步编程中的 Future关键字? 说一下Dart异步编程中的 Stream数据流? Stream 有哪两种订阅模式?分别是怎么调用的? await for 如何使用? 说一下 mixin机制? 请简单介绍下Flutter框架,以及它的优缺点? 介绍下Flutter的理念架构 介绍下FFlutter的FrameWork层和Engine层,以及它们的作用 介绍下Widget、State、Context 概念 - Widget 14.简述Widget的StatelessWidget和StatefulWidget两种状态组件类 15.StatefulWidget 的生命周期 16.简述Widgets、RenderObjects 和 Elements的关系 17.什么是状态管理,你了解哪些状态管理框架? 18.简述Flutter的绘制流程 19.简述Flutter的线程管理模型 20.Flutter 是如何与原生Android、iOS进行通信的? 21.简述Flutter 的热重载 最后 其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2020年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。 【Android学习PDF+学习视频+面试文档+知识点笔记】 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】下载地址!
作者:他的大姨父链接:https://www.jianshu.com/p/317b2d6bde1b 本文是Glide源码解析系列的第一篇,通过这篇文档,将可以了解到: 1.Glide如何绑定Activity、Fragment生命周期。 2.Glide如何监听内存变化、网络变化。 3.Glide如何处理请求的生命周期。 1.0 生命周期相关UML类图 2.0 生命周期绑定 Glide生命周期绑定是从入口单例类Glide开始的,通过with()多个重载方法来实现对生命周期的绑定工作。 public static RequestManager with(Fragment fragment) public static RequestManager with(FragmentActivity activity) public static RequestManager with(Activity activity) public static RequestManager with(Context context) 以Activity的参数为例: public static RequestManager with(Activity activity) { RequestManagerRetriever retriever = RequestManagerRetriever.get(); return retriever.get(activity); } RequestManagerRetriever是一个单例类,可以理解为一个工厂类,通过get方法接收不同的参数,来创建RequestManager。 public RequestManager get(Activity activity) { if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { return get(activity.getApplicationContext()); } else { assertNotDestroyed(activity); android.app.FragmentManager fm = activity.getFragmentManager(); return fragmentGet(activity, fm); } } public RequestManager get(android.app.Fragment fragment) { if (fragment.getActivity() == null) { throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached"); } if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { return get(fragment.getActivity().getApplicationContext()); } else { android.app.FragmentManager fm = fragment.getChildFragmentManager(); return fragmentGet(fragment.getActivity(), fm); } } 如果是在子线程进行的with操作,那么Glide将默认使用ApplicationContext,可以理解为不对请求的生命周期进行管理,通过Activity拿到FragmentManager,并将创建RequestManager的任务传递下去。最终都走到了fragmentGet方法,注意细微区别是Activity传的参数的是Activity的FragmentManager,Fragment传的参数的是ChildFragmentManager,这两者不是一个东西。 RequestManager fragmentGet(Context context, android.app.FragmentManager fm) { //获取RequestManagerFragment,并获取绑定到这个fragment的RequestManager RequestManagerFragment current = getRequestManagerFragment(fm); RequestManager requestManager = current.getRequestManager(); if (requestManager == null) { //如果获取RequestManagerFragment还没有绑定过RequestManager,那么就创建RequestManager并绑定到RequestManagerFragment requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode()); current.setRequestManager(requestManager); } return requestManager; } 2.0.1 创建RequestManagerFragment 这个方法创建了一个fragment,并且创建并绑定了一个RequestManager,看看getRequestManagerFragment如何获取的RequestManagerFragment。 RequestManagerFragment getRequestManagerFragment(final android.app.FragmentManager fm) { //尝试根据id去找到此前创建的RequestManagerFragment RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG); if (current == null) { //如果没有找到,那么从临时存储中寻找 current = pendingRequestManagerFragments.get(fm); if (current == null) { //如果仍然没有找到,那么新建一个RequestManagerFragment,并添加到临时存储中。 //然后开启事务绑定fragment并使用handler发送消息来将临时存储的fragment移除。 current = new RequestManagerFragment(); pendingRequestManagerFragments.put(fm, current); fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget(); } } return current; } 这里有个问题,为什么需要使用pendingRequestManagerFragments这样一个集合来临时存储一下fragment,然后又马上通过handler发送消息移除?这其实是跟主线程的Looper机制和Fragment的事务机制有关的(点击这里查看Fragment事务流程分析)。我们知道,android中的主线程是一个闭环,通过Handler发送消息到MessageQueue,然后通过Looper轮询获取消息并交给Handler处理(点击这里查看Activity启动流程分析)。如下面一个常见场景: Glide.with(this).load(url_1).into(mImageView_1); Glide.with(this).load(url_2).into(mImageView_2); 这段代码通过Glide加载了两张图片并设置到了两个ImageView上,当以上代码块执行时,其所属的代码群的Message刚刚从MessageQueue中取出正在被处理,我们假设这个Message为m1,并且这个MessageQueue中没有其他消息。此时情形是这样的: 当代码执行到getRequestManagerFragment这个方法时,会通过开启事务的方式来绑定这个fragment到activity,相关源码如下(有兴趣了解的点击这里查看Fragment事务流程分析),这个方法在FragmentManagerImpl.java中: public void enqueueAction(Runnable action, boolean allowStateLoss) { if (!allowStateLoss) { checkStateLoss(); } synchronized (this) { if (mDestroyed || mHost == null) { throw new IllegalStateException("Activity has been destroyed"); } if (mPendingActions == null) { mPendingActions = new ArrayList<Runnable>(); } mPendingActions.add(action); if (mPendingActions.size() == 1) { mHost.getHandler().removeCallbacks(mExecCommit); mHost.getHandler().post(mExecCommit); } } } 这里的mHost其实就是activity创建的,并且持有activity以及mMainHandler的引用,根据上述代码可以知道,其实绑定fragment的操作最终是通过主线程的handler发送消息处理的,我们假设这个消息为m2。然后handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();这句代码发送的消息为m3。那么当Glide.with(this).load(url_1).into(mImageView_1);这句代码执行这里时,消息队列有了变化: 但是m2这个消息并不会马上被处理,这是因为m1还有代码还没有执行完毕,也就是说这个fragment并不会马上被绑定,此时m1继续向下执行到第二句代码Glide.with(this).load(url_2).into(mImageView_2);当这句代码走到getRequestManagerFragment时,如果在m1时,我们不将fragment临时存储在pendingRequestManagerFragments中,由于m2还没有被处理,那么 RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG); 必然是找不到这个fragment的,那么就会导致重新创建一个新的重复的fragment,并开启事务绑定,这显然是不合情理的,因为Glide需要保证rootFragment的唯一性,rootFragment即fragment依附或者没有fragment依附的activity所创建的最上层RequestManagerFragment。接着往下看RequestManagerFragment的构造方法做了什么。 public RequestManagerFragment() { this(new ActivityFragmentLifecycle()); } 直接创建一个ActivityFragmentLifecycle,这个类实际是一个生命周期回调的管理类,实现了Lifecycle接口。所有的LifecycleListener会添加到一个集合中,当RequestManagerFragment生命周期方法触发时,会调用ActivityFragmentLifecycle相应生命周期方法,这个方法然后再遍历调用所有LifecycleListener的生命周期方法,以onStart生命周期方法为例,RequestManagerFragment中: public void onStart() { super.onStart(); lifecycle.onStart(); } 然后ActivityFragmentLifecycle中: void onStart() { isStarted = true; for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStart(); } } 2.0.2 rootRequestManagerFragment 上面UML图上,可以知道RequestManagerFragment还有一个rootRequestManagerFragment的成员变量,Glide每创建一个RequestManagerFragment,都会尝试实例化rootRequestManagerFragment,这个fragment即顶级的Activity所创建的RequestManagerFragment,相关代码: public void onAttach(Activity activity) { super.onAttach(activity); rootRequestManagerFragment = RequestManagerRetriever.get() .getRequestManagerFragment(getActivity().getFragmentManager()); if (rootRequestManagerFragment != this) { rootRequestManagerFragment.addChildRequestManagerFragment(this); } } @Override public void onDetach() { super.onDetach(); if (rootRequestManagerFragment != null) { rootRequestManagerFragment.removeChildRequestManagerFragment(this); rootRequestManagerFragment = null; } } 可以看到,不管当前的RequestManagerFragment是通过何种方式创建的,都会在OnAttach时,拿到当前所绑定的Activity的FragmentManager来初始化一个RequestManagerFragment,这个RequestManagerFragment有可能是自身,有可能已经被初始化过了,比如是通过with(Activity activity)的方式初始化的,那么很显然 RequestManagerRetriever.get().getRequestManagerFragment(getActivity().getFragmentManager()); 这句代码拿到的会是自己本身,而如果是通过with(Fragment fragment)的形式创建的,rootRequestManagerFragment将指向当前fragment绑定到Activity所绑定的RequestManagerFragment,如果该Activity没有绑定过,那么会开启事务绑定一个RequestManagerFragment。并且如果自己不是rootRequestManagerFragment的话,那么将会把自己保存到rootRequestManagerFragment中的一个集合: private void addChildRequestManagerFragment(RequestManagerFragment child) { childRequestManagerFragments.add(child); } 简而言之,Glide会为Activity创建一个RequestManagerFragment做为rootFragment,并保存该Activity底下所有Fragment(如果有的话)所创建的RequestManagerFragment。 2.0.3 RequestManagerTreeNode RequestManagerFragment初始化时,还会初始化RequestManagerTreeNode,顾名思义,这个类是用来保存请求树节点的,比如一个Activity采用Viewpager + Fragment的形式,而里面的Fragment又是一个ViewPager + Fragment的形式,这个时候,假设其中一个RequestManagerFragment生命周期方法走了,怎么知道哪些RequestManagerFragment绑定的LifeCycle应该得到调用呢?理想的情况是,应该让绑定该RequestManagerFragment的Fragment所有的子Fragment的RequestManagerFragment的生命周期得到调用,比如如下场景中,Activity中各有两个Fragment,两个Fragment又各有两个子Fragment,在所有Fragment中,均通过with(this)的方式来加载图片,经过之前的分析我们可以知道的是,ROOT RMF 中会保存有6个RMF(RMF即RequestManagerFragment): 当如果F1 RMF生命周期做出反应时,因为RequestManagerFragment是无界面的,所以可以理解为F1的生命周期做出反应。我们希望F11和F12所绑定的RequestManagerFragment也要立即做出反应。但是F2以及其底下的RequestManagerFragment则不应响应对应生命周期事件,我们知道任何一个RequestManagerFragment可以通过rootRequestManagerFragment拿到这6个RMF,继而拿到其所对应的RequestManager,那么怎么去确定F11 RMF 和 F12 RMF呢?这就是RequestManagerTreeNode干的事情了,RequestManagerFragment中的非静态内部类FragmentRequestManagerTreeNode实现了RequestManagerTreeNode: private class FragmentRequestManagerTreeNode implements RequestManagerTreeNode { @Override public Set<RequestManager> getDescendants() { Set<RequestManagerFragment> descendantFragments = getDescendantRequestManagerFragments(); HashSet<RequestManager> descendants = new HashSet<RequestManager>(descendantFragments.size()); for (RequestManagerFragment fragment : descendantFragments) { if (fragment.getRequestManager() != null) { descendants.add(fragment.getRequestManager()); } } return descendants; } } 这个类做的事情比较简单,调用外部类RequestManagerFragment的方法getDescendantRequestManagerFragments拿到所有的“后裔”Fragment,然后再取出它的RequestManager,然后集合装起来返回,这里的后裔在前面的例子中,指的就是F11 RMF 和 F12 RMF,看看getDescendantRequestManagerFragments是怎么拿到的F11和F12: @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public Set<RequestManagerFragment> getDescendantRequestManagerFragments() { //如果自己是rootFragment,那么直接返回childRequestManagerFragments if (rootRequestManagerFragment == this) { return Collections.unmodifiableSet(childRequestManagerFragments); } else if (rootRequestManagerFragment == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { // Pre JB MR1 doesn't allow us to get the parent fragment so we can't introspect hierarchy, so just // return an empty set. return Collections.emptySet(); } else { HashSet<RequestManagerFragment> descendants = new HashSet<RequestManagerFragment>(); for (RequestManagerFragment fragment //遍历取出rootFragment中的RMF,并获取到其parentFragment,找出后裔。 : rootRequestManagerFragment.getDescendantRequestManagerFragments()) { if (isDescendant(fragment.getParentFragment())) { descendants.add(fragment); } } return Collections.unmodifiableSet(descendants); } } 看看isDescendant方法是如何判断的: private boolean isDescendant(Fragment fragment) { Fragment root = this.getParentFragment(); while (fragment.getParentFragment() != null) { if (fragment.getParentFragment() == root) { return true; } fragment = fragment.getParentFragment(); } return false; } 依上面的例子,当遍历到F11 RMF时,参数传递过来的是F11,root 则为F1,F11再拿到parent,也是F1,返回true,F12 RMF类似也返回true;当遍历到F21 RMF时,参数传入F21,root仍是F1,此时F21再怎么拿Parent也不可能是root,返回false。简而言之,RequestManagerTreeNode用来获取绑定该RequestManagerFragment的Fragment的所有子Fragment所绑定的RequestManagerFragment所绑定的RequestManager 2.0.4 RequestManager 上面一直在说RequestManagerFragment,下面回到FragmentGet方法中,再贴一次,免得上翻麻烦: RequestManager fragmentGet(Context context, android.app.FragmentManager fm) { //获取RequestManagerFragment,并获取绑定到这个fragment的RequestManager RequestManagerFragment current = getRequestManagerFragment(fm); RequestManager requestManager = current.getRequestManager(); if (requestManager == null) { //如果获取RequestManagerFragment还没有绑定过RequestManager,那么就创建RequestManager并绑定到RequestManagerFragment requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode()); current.setRequestManager(requestManager); } return requestManager; } 根据上面的UML图,可以知道RequestManager是一个非常核心的类,并且还实现了LifecycleListener来处理请求的生命周期。上述代码在创建RequestManager时,传递了3个参数,分别是context,前面分析过的初始化RequestManagerFragment所创建的LifeCycle和RequestManagerTreeNode。直接看RequestManager的构造函数: public RequestManager(Context context, Lifecycle lifecycle, RequestManagerTreeNode treeNode) { this(context, lifecycle, treeNode, new RequestTracker(), new ConnectivityMonitorFactory()); } 调用的另一个构造方法,并增加了两个新的参数RequestTracker和ConnectivityMonitorFactory。 RequestManager(Context context, final Lifecycle lifecycle, RequestManagerTreeNode treeNode, RequestTracker requestTracker, ConnectivityMonitorFactory factory) { this.context = context.getApplicationContext(); this.lifecycle = lifecycle; this.treeNode = treeNode; this.requestTracker = requestTracker; this.glide = Glide.get(context); this.optionsApplier = new OptionsApplier(); ConnectivityMonitor connectivityMonitor = factory.build(context, new RequestManagerConnectivityListener(requestTracker)); // If we're the application level request manager, we may be created on a background thread. In that case we // cannot risk synchronously pausing or resuming requests, so we hack around the issue by delaying adding // ourselves as a lifecycle listener by posting to the main thread. This should be entirely safe. if (Util.isOnBackgroundThread()) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { lifecycle.addListener(RequestManager.this); } }); } else { lifecycle.addListener(this); } lifecycle.addListener(connectivityMonitor); } RequestTracker即所有请求操作的真正处理者,所有Request的暂停取消执行操作都由RequestTracker来完成,如RequestManager暂停请求的实现: public void pauseRequests() { Util.assertMainThread(); requestTracker.pauseRequests(); } 2.0.5 网络状态监测 请求生命周期的实现细节后面再说,暂时埋坑,先来看看ConnectivityMonitorFactory这个工厂生产了什么。 public class ConnectivityMonitorFactory { public ConnectivityMonitor build(Context context, ConnectivityMonitor.ConnectivityListener listener) { final int res = context.checkCallingOrSelfPermission("android.permission.ACCESS_NETWORK_STATE"); final boolean hasPermission = res == PackageManager.PERMISSION_GRANTED; if (hasPermission) { return new DefaultConnectivityMonitor(context, listener); } else { return new NullConnectivityMonitor(); } } } 很简单,接收一个ConnectivityListener根据是否有监控网络状态的权限来创建相应的网络监控器。DefaultConnectivityMonitor也比较简单,就是内部定义了一个广播接收者,并且也实现了lifeCycleListener。在上面RequestManager的构造方法中,创建了一个RequestManagerConnectivityListener: private static class RequestManagerConnectivityListener implements ConnectivityMonitor.ConnectivityListener { private final RequestTracker requestTracker; public RequestManagerConnectivityListener(RequestTracker requestTracker) { this.requestTracker = requestTracker; } @Override public void onConnectivityChanged(boolean isConnected) { if (isConnected) { requestTracker.restartRequests(); } } } 这个listener很简单,收到网络状态连接就重启请求。然后通过工厂创建出了DefaultConnectivityMonitor,并把它添加到了lifecycle中。到这里,Glide监测网络状态来重启请求的实现方式就呼之欲出了,大体步骤如下:在相应的生命周期方法中,会调用lifecycle的生命周期方法,lifecycle会调用DefaultConnectivityMonitor所实现的相应生命周期方法来注册及解除注册网络状态的广播接收者,收到广播后,会回调之前传递的参数ConnectivityListener的onConnectivityChanged方法来处理Request。 2.0.6 内存状态监测 RequestManager中还存有Glide这个入口类的实例,构造方法中直接获取到的,用来对内存状态的变更作出处理,比较简单,看看流程便可以了,以onTrimMemory为例,当RequestManagerFragment的onTrimMemory被调用时,会调用其绑定的RequetManager的相应方法来处理: @Override public void onTrimMemory(int level) { // If an activity is re-created, onTrimMemory may be called before a manager is ever set. // See #329. if (requestManager != null) { requestManager.onTrimMemory(level); } } 然后RequestManager再调用Glide入口类的trimMemory来释放更多内存: public void onTrimMemory(int level) { glide.trimMemory(level); } 2.0.7 生命周期回调流程总结 在RequestManager构造方法中,还会将自身添加到LifeCycle中,这样,整个流程就畅通了: 细心的可以发现,虽然在构造RequestManager时传递了参数RequestManagerTreeNode,但是在这个回调流程中,并没有对所有后裔RMF的RequestManager进行调用,Glide默认确实是不会去调用,但这里并不意味着这些RequestManager不会被调用到,事实上,当前RMF生命周期被调用时,就意味后裔Fragment生命周期也会被调用,那么后裔Fragment这个流程仍然会走一遍,那么RequestManagerTreeNode到底有什么用呢?答案是没用,完全没用,如果只是简单使用Glide的话。当然,RequestManager暴露了相关接口给开发者使用: public void resumeRequestsRecursive() { Util.assertMainThread(); resumeRequests(); for (RequestManager requestManager : treeNode.getDescendants()) { requestManager.resumeRequests(); } } 调用这个方法将会把所有后裔的请求同时一起处理。 **推荐阅读:[2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中)](https://www.jianshu.com/p/7f9ade51232e)**2020最新Android大厂高频面试题解析大全(BAT TMD JD 小米)
我这篇文章并不是简单的描述一些面试中的真题,或者总结一些Android的知识,而是一个普通人经历过各种面试后的一个总结与反思。对技术面试这回事有一些体会,在此分享。(从我的角度,当然肯定有不合理的地方,大家借鉴就好)。 从两个月前开始面试,先后面试了腾讯、抖音、美团等,最后只拿到了阿里钉钉一个offer;坦白说,我对我个人在这次求职中的表现并不十分满意,面试前没有做足够充分的准备——数次被面试官出的题目“虐”、应对面试的压力时没能做到沉着冷静、在面试中未能完整地把自己的积累与优势表现出来……所以本篇文章并不能完全算一个“成功者”的经验分享! 一、面试前的准备 1.1 简历 简历的重要性就不言而喻了,怎么样写好简历是个技术活,当然如果你有很好的背景(学校或者公司),那么不管你怎么写,基本上都不刷掉你,我们作为一般的人还是需要下一番功夫的。拿我的简历作为例子,大概有以下几个部分: 个人信息:姓名、出生日期、教育背景、博客地址、github地址、联系方式(手机、邮箱和微信号) 工作经历:毕业后待过哪些公司,一般是倒序,项目尽量精简明了,可以参考SMART原则 专业技能:自己熟悉的一些技能,这个为什么我写到最后,因为对于工作三年的同学来说,面试官更加注重的是你的项目经历,大部分面试都是看你的项目经历来提问。 怎样写简历,这个开源网站不错,教你怎么写简历,而且有一个在线markdown在线网站,可以导出pdf。 另外,我个人在写简历时有一个还未做到的点是及时更新简历。对于几年前的项目早就记忆模糊了,几年后再尝试回忆项目细节写进简历其实很难。所以最佳方案是项目结束后及时把收获更新进简历里。 1.2 基础复习 对于基础复习我这次最大的感触就是,一定要早点做准备同时也要做全面完整的准备。 举个例子, Java 中非常基础的的四大引用。对 Android 开发来说平时可能用弱引用比较多,但真正作为面试题来问时面试官希望你能马上回答出四大引用分别是什么以及各自的使用场景。假如你能马上回答出四大引用的特点及使用场景当然是合格的回答,假如你不止回答出四大引用的特点还能联系到 ReferenceQueue,继而延伸到在 leakcanary 的使用,那就是优秀的回答了 —— 但假如你被提问后一脸懵逼,说自己只记得弱引用,就会比较尬(当然就这个知识点而言,我作为面试官的角色时还会尝试“抢救一下”,由弱引用的使用延伸到内存泄露去,不会直接判定应聘者)。 因为在“面试”这个场景里,面试官会默认你做了足够的准备,对于一些中高端职位基础题其实是作为送分题问的,当然希望你能快速反应、快速回答。而人不是机器,许久没用或者没复习的技术点想要在短时间内回忆起来并归纳成 N 个点说出来难度非常大。 所以基础技术的面试其实就跟应试一样,任你功力再高,也有必要好好复习一下。毕竟“武功再高,也怕菜刀”嘛(不恰当的比喻,哈哈)。 基础复习可以分为两大块,一块是 Android 和 Java 基础,另一块是计算机基础,也就是算法、计算机网络、计算机原理等。对于前一块,经验丰富的你一般花半个月就可以搞定;但对于后一块,时间上就不好估计了(网上有非常完整的各种面经和题库,聪明的你肯定具备最基本的信息检索能力,这里我就不贴链接了)。 这里我把我自己作为反面教材:由于前期对是否要跳槽犹豫不决,所以没能早点进行充分的准备,导致后面碰壁后需要在短期内急急忙忙去复习,其中的压力可想而知。 个人认为比较舒服的姿势是,不管跳槽与否,一些基础的东西在平时就可以有计划地复习,特别是刷算法题 —— 任你算法功力再高,没有经过一定的训练想要在面试这种场景下快速手写出 bug free 的代码也几乎不可能。 1.3 项目复习 社招跟校招的一大差别是,社招中的基础题部分只是前菜,招聘方会非常重视你的项目经历,通过询问项目经历会扩展到对技术、学习能力、沟通能力等的考察。 关于如何复习项目,从面试情况看,可以从总体架构、项目细节、项目亮点、碰到的问题以及场景复述等方面入手。 总体架构和项目细节不用过多解释,前者是从宏观角度向对方介绍你的项目的架构,用最短的时间让对方理解项目通过哪些模块或组件间的协作去实现功能的;后者是对方可能会提出一些感兴趣的点询问你项目细节 —— 所以千万记得认真掌握项目关键细节,否则答不出来会很尴尬。 项目亮点和难题则是面试必备,基本大部分面试都会问到这块,无他,对方不了解你的项目的情况下肯定希望你能展示出可以为自己加分的点。 1.4 简历投递 准备得差不多后就可以开始进入简历投递环节了,我觉得简历投递的途径的优先级是这样的:熟人内推 > 优秀猎头推荐 > 普通网友内推 > 普通猎头推荐 > 官网投递。 假如你的简历光芒闪闪,阿里星那种级别,那随便投递都可以很快有响应,否则投递的途径还是很重要的。 熟人内推当然是第一选择,通过熟人你不止可以知道部门内部的业务发展、晋升、加班等情况,在走流程时也可以通过他直接接触到你的未来 leader。而为什么优秀猎头的内推会比普通网友内推要好呢?我个人的感受是优秀猎头会比普通网友更了解招聘情况且能更积极得帮你催流程,而真正优秀的猎头,在对公司整体信息的掌握上是高于普通员工的。 二、面试中常见的考察方向 面试中要沉着冷静、面试前要确认面试时间并提前到……这些啰嗦的小 Tip 我就不说了,聪明的你一定能注意。这里我尝试总结一下碰到过的常见的考察方向(或者说“题型”)。 2.1 算法 对于算法的考察,从个人有限的经验上看,貌似难度都是适中的。特别是对于我们客户端开发而言,考察的算法都比较常规。(呃,某些很注重这块的公司除外 —— 当然注重这块也是好事,我们只能去适应公司的风格而不能要求公司适应我们) 算法这块我也是“低手”(这块强的同学可以留言教授一下比较好的学习方案),多学习多练习吧。 这次求职中,比较高频的题目是"第 TopK 大的数"(快排思想、能提到线性查找算法 BFPAT 更佳) 和 “前 TopN 个数” (堆排序、先分治再堆排序)。 2.2 技术基础 就像面试前我们准备的,基础题基本是必问的,就算不深究 Android 的基础,问你一些计算机网络的东西不为过吧。这块我们必须拿出校招时的劲头来,老老实实复习。至于具体的题目什么的我就不罗列了,网上有一堆面经,github 上也有很多整理好的题库。 对 Android 开发来说,可以分成两块,第一块是 Android 相关基础。跟初级开发的面试不同的是,这里的基础不会是简单的“四大组件是哪些”,而是会问你具体的使用和碰到的问题。比如四大组件的考察会结合 ANR(四大组件是否都会产生 ANR、时间是多少等)、进程优先级、启动模式 等等一起问。网上的面经和题库命中概率还是蛮高的,大部分题目都似曾相识,毕竟 Android 常用知识点也就这些。当然不要因此掉以轻心,优秀的面试官是会针对细节深入挖掘的,所以不止要“知道”,还要“理解和掌握”。 另一块是计算机网络、计算机原理等。对客户端开发来说,计算机网络的考察会比较多,TCP 和 UDP 的区别、TCP 的拥塞控制、TCP 的握手与挥手流程、HTTP 与 HTTPS 的差别等等。基本面的所有公司都问到这块了。 这块需要特别注意的点就是你的覆盖面是否足够,因为不同公司的不同部门的不同面试官都可能会有不同的提问姿势。你不完整系统得把基础过一遍,真不能保证你能信心十足(一两个问题被问倒其实没什么,但能不被问倒更好不是)。 我可以举几个例子,比如在问大图加载时顺口问一下“同一个文件,放到 drawable 目录下和放到 SD 卡中,加载到内存时内存占用一样么” (这里涉及到了 Bitmap decode 时的过程以及 Bitmap 内存占用的计算),比如 HashMap put 方法调用时内部的流程是怎样(方法内部的流程、HashMap 的扩容等),比如 Http 1.1 和 2.0 的特点和区别 —— 这些例子都是我或者我朋友真实碰到的面试题,在没经过充分的面试准备之前,你能答出多少呢? 2.3 技术原理 一般这类问题是在问基础题时顺势往底层问,或者是你自己在回答时顺便带出来,比如屏幕绘制原理、几种动画的原理、布局加载原理等等,是体现个人的技术深度的。 我觉得这类题目不是死记硬背可以解决的,作为面试官,自然有办法考察出你是“了解”还是“理解”。 其实系统地复习这些内容本身也是挺有趣的,你会很容易发现技术背后的实现存在深层的联系。所以这块不只是面试题那么简单,它也是我们以后往“资深开发者”走的一个方向。 回答这类问题,主动比被动更好。一般面试官问你很基础的问题时,你当然可以惜字如金只回答对应的答案,但假如你能主动扩展到原理层面、甚至隐晦地表示你看过源码,要我是面试官也会喜欢你(斜眼笑)。 2.4 项目介绍 我一开始也没有经验,面爱奇艺时让我介绍项目我就简单介绍了下项目需求是怎样,可以看出面试官并不满意。后面专门向一个牛逼的前同事请教了这个,他的建议是注意项目细节准备,保持自信!后面面其他家时,果然感觉轻松了一些。 2.4.1 在面试前准备项目描述,别害怕,因为面试官什么都不知道 面试官拿到我们的简历的时候,是没法核实你的项目细节的(一般公司会到录用后,用背景调查的方式来核实)。更何况,我们做的项目是以月为单位算的,而面试官最多用30分钟来从我们的简历上了解项目经验,我们对项目的熟悉程度要远远超过面试官,所以大可不用紧张。如果我们的工作经验比面试官还丰富的话,甚至还可以控制整个面试流程。 既然面试官无法了解我们的底细,那么他们怎么来验证我们的项目经验和技术?下面总结了一些常用的提问方式。 2.4.2准备项目的各种细节,一旦被问倒了,就说明我们没做过 一般来说,在面试前,大家应当准备项目描述的说辞,自信些,因为这部分我们自己说了算,流利些,因为我们在经过充分准备后,可以知道自己要说些什么。而且这些是基于我们实际的项目经验,那么一旦让面试官感觉我们都说不上来,那么可信度就很低了。 不少人是拘泥于“项目里做了什么业务,以及代码实现的细节”,这就相当于把后续提问权直接交给面试官。下表列出了一些不好的回答方式。 在避免上述不好的回答的同时,大家可以按下表所给出的要素准备项目介绍。 面试前,我们一定要准备,一定要有自信,但也要避免如下的一些情况。 三、阿里钉钉面经(已拿offer) 阿里钉钉一面(面试时长80min) 1.自我介绍,对自己项目的介绍,架构图呈现2.因为说自己以后的发展方向是音视频,所以问了我用过哪些现有框架3.项目中的一些优化问题,MVC -> MVP,Handler的内存泄漏情况分析等,对MVP和MVVM的理解。4.项目中的第三方库选择的问题,因为简历中写了我对第三方库选择的问题,比如选ObjectBox和greenDao的问题,图片加载框架问题5.HashMap和HashTable,引申ConCurrentHashmap的深入,version1.7和1,8的区别,以及高并发下HashMap发生的问题四大启动模式,以及场景对应6.Handler的机制介绍,不存在消息时的IdleHandler的运作机制,为什么不能在子线程初始化问题7.设计模式中的单例介绍,使用场景(Okhttp的Seesion存储等等),在线编写快排算法8.OkHttp的源码分析,及整体架构的流程图绘制9.四大组件的完整介绍,及深入,答了IntentService、LocalBroadcast10.四大引用的问题和MVP框架相结合进行回答11.网络中的响应码对大体进行回答,具体回答了200、404、500、304等12.View绘制流程问题,如何不使用xml,来实现中间位置的定位13.事件分发机制流程讲解,以及如何实现单击事件和长按事件的判定14.关于为什么选用mqtt协议的问题,优势,原理等等(没答上来,只说针对性做过测压,以及oceanlink和mqtt的对比)阿里钉钉二面(面试时长30min) 1.自我介绍,优缺点的,以及未来希望的发展方向2.目整体介绍,如何做到分压啊之类的问题3.如果给我阿里、腾讯、头条、谷歌的offer的一个选择(不掺杂地域性的问题),首先直接否定了google我就的一个企业的商业模式进行分析。我对钉钉这个产品的理解,我的回答是就的是钉钉前身的对标项目微信,已经后期转型的商业模式作出的分析,最后给出的我的结论是一个企业性质的办公软件4.给出了钉钉一个mac平台的关于共享屏幕的耗CPU的问题5.让我提问,提的是关于音视频发展方向的问题,具体是一个发展空间。阿里钉钉三面(面试时长60min) 1.自我介绍呗,还是一样,项目介绍,项目的优缺点对比2.对MVC和MVP的理解,还有Handler的内存泄漏问题具体是什么,解决方案知道有什么,空数据的时候Handler的阻塞问题,但是我还是没在Looper的源码中找到,这里让我好好再看一下。3.另外为什么使用MVP,他的优势是什么,内存泄漏是什么样的。4.http的长连接和短连接这两个概念,怎么去理解。我把它理解为持久化连接是什么,然后对http的3个版本的主要区别做一个介绍5.http一整个流程,什么Baidu.com输入,经过了什么。比较简单的问题了,必答内容6.DNS、TCP的三次握手、四次挥手,当然我再答一些IP路由、链路还有物理层的内容7.RecyclerView的一个复用机制,和ListView的一个区别在哪里8.HashMap、ConcurrentHashMap、Hashtable的问题,数据结构,线程安全啊之类的问题了,当然还是考了version 1.7和1.8。9.一个app的启动流程,冷启动和热启动,我说底层我不太了解,只知道会有AMS去调一些东西,但是具体内容不知道,后面就是一些初始化和Activitiy生命周期问题了。10.一道算法题三值之和求目标值,没写暴力,因为大家都会写,但是浪费了很多时间,刚开始是通过二值求和的方式,但是想做成O(n),其实不太可能,后面改成了O(n^2) HR面1.你对阿里面试官的印象如何?2.你从面试官上学到了哪些东西* 3.你每天的生活安排是什么样子的?* 4.你为什么选择来阿里?* 5.你以后的技术规划是什么样的?* 6.你最有成就的项目是哪个?* 7.为什么选择android开发?* 8.你有什么要问我的吗? 最后 学习技术是一条漫长而又艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持! 此外我也把近期搜集整理的腾讯、头条、阿里、美团、字节跳动等公司2020年的高频面试真题解析分享出来。 把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。 【Android学习PDF+学习视频+面试文档+知识点笔记】 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】免费下载地址:https://shimo.im/docs/Q6V8xPVxHpkrtRtD
我们为什么要优化内存 在 Android 中我们写的 .java 文件,最终会编译成 .class 文件, class 又由类装载器加载后,在 JVM 中会形成一份描述 class 结构的元信息对象,通过该元信息对象可以知道 class 的结构信息 (构造函数、属性、方法)等。JVM 会把描述类的数据从 class 文件加载到内存,Java 有一个很好的管理内存的机制,垃圾回收机制 GC 。为什么 Java 都给我们提供了垃圾回收机制,程序有时还会导致内存泄漏,内存溢出 OOM,甚至导致程序 Crash 。接下来我们就对实际开发中出现的这些内存问题,来进行优化。 JAVA 虚拟机 我们先来大概了解一下 Java 虚拟机里面运行时的数据区域有哪些,如果想深入了解 Java 虚拟机 建议可以购买<<深入理解 Java 虚拟机>> 或者直接点击我这里的 PDF 版本 密码: jmnf 线程独占区 程序计数器 相当于一个执行代码的指示器,用来确认下一行执行的地址 每个线程都有一个 没有 OOM 的区 虚拟机栈 我们平时说的栈就是这块区域 java 虚拟机规范中定义了 OutOfMemeory , stackoverflow 异常 本地方法栈 java 虚拟机规范中定义了 OutOfMemory ,stackoverflow 异常 注意 在 hotspotVM 中把虚拟机栈和本地方法栈合为了一个栈区 线程共享区 方法区 ClassLoader 加载类信息 常量、静态变量 编译后的代码 会出现 OOM 运行时常量池 public static final 符号引用类、接口全名、方法名 java 堆 (本次需要优化的地方) 虚拟机能管理的最大的一块内存 GC 主战场 会出现 OOM 对象实例 数据的内容 JAVA GC 如何确定内存回收 随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。 目前虚拟机基本都是采用可达性分析算法,为什么不采用引用计数算法呢?下面就说说引用计数法是如果统计所有对象的引用计数的,再对比可达性分析算法是如何解决引用计数算法的不足。下面就来看下这 2 个算法: 引用计数算法 每个对象有一个引用计数器,当对象被引用一次则计数器加一,当对象引用一次失效一次则计数器减一,对于计数器为 0 的时候就意味着是垃圾了,可以被 GC 回收。 下面通过一段代码来实际看下 public class GCTest { private Object instace = null; public static void onGCtest() { //step 1 GCTest gcTest1 = new GCTest(); //step 2 GCTest gcTest2 = new GCTest(); //step 3 gcTest1.instace = gcTest2; //step 4 gcTest2.instace = gcTest1; //step 5 gcTest1 = null; //step 6 gcTest2 = null; } public static void main(String[] arg) { onGCtest(); } } 分析代码 //step 1 gcTest1 引用 + 1 = 1 //step 2 gcTest2 引用 + 1 = 1 //step 3 gcTest1 引用 + 1 = 2 //step 4 gcTest2 引用 + 1 = 2 //step 5 gcTest1 引用 - 1 = 1 //step 6 gcTest2 引用 - 1 = 1 很明显现在 2 个对象都不能用了都为 null 了,但是 GC 确不能回收它们,因为它们本身的引用计数不为 0 。不能满足被回收的条件,尽管调用 System.gc() 也还是不能得到回收, 这就造成了 内存泄漏 。当然,现在虚拟机基本上都不采用此方式。 可达性分析算法 从 GC Roots 作为起点开始搜索,那么整个连通图中额对象边都是活对象,对于 GC Roots 无法到达的对象便成了垃圾回收的对象,随时可能被 GC 回收。 可以作为 GC Roots 的对象 虚拟机栈正在运行使用的引用 静态属性 常量 JNI 引用的对象 GC 是需要 2 次扫描才回收对象,所以我们可以使用 finalize 去救活丢失的引用 @Override protected void finalize() throws Throwable { super.finalize(); instace = this; } 到了这里,相信大家已经能够弄明白这 2 个算法的区别了吧?反正对于对象之间循环引用的情况,引用计数算法无法回收这 2 个对象,而可达性是从 GC Roots 开始搜索,所以能够正确的回收。 不同引用类型的回收状态 强引用 Object strongReference = new Object() 如果一个对象具有强引用,那垃圾回收器绝不会回收它,当内存空间不足, Java 虚拟机宁愿抛出 OOM 错误,使程序异常 Crash ,也不会靠随意回收具有强引用的对象来解决内存不足的问题.如果强引用对象不再使用时,需要弱化从而使 GC 能够回收,需要: strongReference = null; //等 GC 来回收 还有一种情况,如果: public void onStrongReference(){ Object strongReference = new Object() } 在 onStrongReference() 内部有一个强引用,这个引用保存在 java 栈 中,而真正的引用内容 (Object)保存在 java 堆中。当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为 0 ,这个对象会被回收。 但是如果 mStrongReference 引用是全局时,就需要在不用这个对象时赋值为 null ,因为 强引用 不会被 GC 回收。 软引用 (SoftReference) 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。 注意: 软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用, JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到了 str 这个对象也不会回收,只有内存不足才会回收。 弱引用 (WeakReference) 弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 可见 weakReference 对象的生命周期基本由 GC 决定,一旦 GC 线程发现了弱引用就标记下来,第二次扫描到就直接回收了。 注意这里的 referenceQueuee 是装的被回收的对象。 虚引用 (PhantomReference) @Test public void onPhantomReference()throws InterruptedException{ String str = new String("123456"); ReferenceQueue queue = new ReferenceQueue(); // 创建虚引用,要求必须与一个引用队列关联 PhantomReference pr = new PhantomReference(str, queue); System.out.println("PhantomReference:" + pr.get()); System.out.printf("ReferenceQueue:" + queue.poll()); } 虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 总结 引用类型 调用方式 GC 是否内存泄漏 强引用 直接调用 不回收 是 软引用 .get() 视内存情况回收 否 弱引用 .get() 回收 不可能 虚引用 null 任何时候都可能被回收,相当于没有引用一样 否 分析内存常用工具 工具很多,掌握原理方法,工具随意挑选使用。 top/procrank meinfo Procstats DDMS MAT Finder - Activity LeakCanary LeakInspector 内存泄漏 产生的原因: 一个长生命周期的对象持有一个短生命周期对象的引用,通俗点讲就是该回收的对象,因为引用问题没有被回收,最终会产生 OOM。 下面我们来利用 Profile 来检查项目是否有内存泄漏 怎么利用 profile 来查看项目中是否有内存泄漏 在 AS 中项目以 profile 运行 在 MEMORY 界面中选择要分析的一段内存,右键 export **Allocations:** 动态分配对象个数 **Deallocation:** 解除分配的对象个数 **Total count:** 对象的总数 **Shalow Size:** 对象本身占用的内存大小 **Retained Size:** GC 回收能收走的内存大小 转换 profile 文件格式 将 export 导出的 dprof 文件转换为 Mat 的 dprof 文件 cd /d 进入到 Android sdk/platform-tools/hprof-conv.exe //转换命令 hprof-conv -z src des D:\Android\AndroidDeveloper-sdk\android-sdk-windows\platform-tools>hprof-conv -z D:\temp_\temp_6.hprof D:\temp_\memory6.hprof 下载 Mat 工具 打开 MemoryAnalyzer.exe 点击左上角 File 菜单中的 Open Heap Dupm 查看内存泄漏中的 GC Roots 强引用 这里我们得知是一个 ilsLoginListener 引用了 LoginView,我们来看下代码最后怎么解决的。 代码中我们找到了 LoginView 这个类,发现是一个单例中的回调引起的内存泄漏,下面怎么解决勒,请看第七小点。 2种解决单例中的内存泄漏 将引用置为 null /** * 销毁监听 */ public void unRemoveRegisterListener(){ mMessageController.unBindListener(); } public void unBindListener(){ if (listener != null){ listener = null; } } 2. 使用弱引用 //将监听器放入弱引用中 WeakReference<IBinderServiceListener> listenerWeakReference = new WeakReference<>(listener); //从弱引用中取出回调 listenerWeakReference.get(); 通过第七小点就能完美的解决单例中回调引起的内存泄漏。 Android 中常见的内存泄漏经典案例及解决方法 单例 示例 : public class AppManager { private static AppManager sInstance; private CallBack mCallBack; private Context mContext; private AppManager(Context context) { this.mContext = context; } public static AppManager getInstance(Context context) { if (sInstance == null) { sInstance = new AppManager(context); } return sInstance; } public void addCallBack(CallBack call){ mCallBack = call; } } 1. 通过上面的单列,如果 context 传入的是 Activity , Service 的 this,那么就会导致内存泄漏。 以 Activity 为例,当 Activity 调用 getInstance 传入 this ,那么 sInstance 就会持有 Activity 的引用,当 Activity 需要关闭的时候需要 回收的时候,发现 sInstance 还持有 没有用的 Activity 引用,导致 Activity 无法被 GC 回收,就会造成内存泄漏 2. addCallBack(CallBack call) 这样写看起来是没有毛病的。但是当这样调用在看一下勒。 //在 Activity 中实现单例的回调 AppManager.getInstance(getAppcationContext()).addCallBack(new CallBack(){ @Override public void onStart(){ } }); 这里的 new CallBack() 匿名内部类 默认持有外部的引用,造成 CallBack 释放不了,那么怎么解决了,请看下面解决方法 **解决方法**: 1. getInstance(Context context) context 都传入 Appcation 级别的 Context,或者实在是需要传入 Activity 的引用就用 WeakReference 这种形式。 2. 匿名内部类建议大家单独写一个文件或者 public void addCallBack(CallBack call){ WeakReference<CallBack> mCallBack= new WeakReference<CallBack>(call); } Handler 示例: //在 Activity 中实现 Handler class MyHandler extends Handler{ private Activity m; public MyHandler(Activity activity){ m=activity; } // class..... } 这里的 MyHandler 持有 activity 的引用,当 Activity 销毁的时候,导致 GC 不会回收造成 内存泄漏。 **解决方法**: 1.使用静态内部类 + 弱引用 2.在 Activity onDestoty() 中处理 removeCallbacksAndMessages() @Override protected void onDestroy() { super.onDestroy(); if(null != handler){ handler.removeCallbacksAndMessages(null); handler = null; } } 静态变量 示例: public class MainActivity extends AppCompatActivity { private static Police sPolice; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (sPolice != null) { sPolice = new Police(this); } } } class Police { public Police(Activity activity) { } } 这里 Police 持有 activity 的引用,会造成 activity 得不到释放,导致内存泄漏。 **解决方法**: //1\. sPolice 在 onDestory()中 sPolice = null; //2\. 在 Police 构造函数中 将强引用 to 弱引用; 非静态内部类参考 第二点 Handler 的处理方式 匿名内部类 示例: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(){ @Override public void run() { super.run(); } }; } } 很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程`Thread`和`AsyncTask`都是匿名内部类对象,默认就隐式的持有外部`Activity`的引用,导致`Activity`内存泄露。 **解决方法**: //静态内部类 + 弱引用 //单独写一个文件 + onDestory = null; 未取消注册或回调 示例: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); registerReceiver(mReceiver, new IntentFilter()); } private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // TODO ------ } }; } 在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用`Retrofit + RxJava`注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。 **解决方法**: //Activity 中实现 onDestory()反注册广播得到释放 @Override protected void onDestroy() { super.onDestroy(); this.unregisterReceiver(mReceiver); } 定时任务 示例: public class MainActivity extends AppCompatActivity { /**模拟计数*/ private int mCount = 1; private Timer mTimer; private TimerTask mTimerTask; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); mTimer.schedule(mTimerTask, 1000, 1000); } private void init() { mTimer = new Timer(); mTimerTask = new TimerTask() { @Override public void run() { MainActivity.this.runOnUiThread(new Runnable() { @Override public void run() { addCount(); } }); } }; } private void addCount() { mCount += 1; } } 当我们`Activity`销毁的时,有可能`Timer`还在继续等待执行`TimerTask`,它持有Activity 的引用不能被 GC 回收,因此当我们 Activity 销毁的时候要立即`cancel`掉`Timer`和`TimerTask`,以避免发生内存泄漏。 **解决方法**: //当 Activity 关闭的时候,停止一切正在进行中的定时任务,避免造成内存泄漏。 private void stopTimer() { if (mTimer != null) { mTimer.cancel(); mTimer = null; } if (mTimerTask != null) { mTimerTask.cancel(); mTimerTask = null; } } @Override protected void onDestroy() { super.onDestroy(); stopTimer(); } 资源未关闭 **示例:** ArrayList,HashMap,IO,File,SqLite,Cursor 等资源用完一定要记得 clear remove 等关闭一系列对资源的操作。 **解决方法**: 用完即刻销毁 属性动画 **示例:** 动画同样是一个耗时任务,比如在 Activity 中启动了属性动画 (ObjectAnimator) ,但是在销毁的时候,没有调用 cancle 方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用 Activity ,这就造成 Activity 无法正常释放。因此同样要在Activity 销毁的时候 cancel 掉属性动画,避免发生内存泄漏。 **解决方法**: @Override protected void onDestroy() { super.onDestroy(); //当关闭 Activity 的时候记得关闭动画的操作 mAnimator.cancel(); } Android 源码或者第三方 SDK **示例:** //如果在开发调试中遇见 Android 源码或者 第三方 SDK 持有了我们当前的 Activity 或者其它类,那么现在怎么办了。 **解决方法**: //当前是通过 Java 中的反射找到某个类或者成员,来进行手动 = null 的操作。 内存抖动 什么是内存抖动 内存频繁的分配与回收,(分配速度大于回收速度时) 最终产生 OOM 。 也许下面的录屏更能解释什么是内存抖动 可以看出当我点击了一下 Button 内存就频繁的创建并回收(注意看垃圾桶)。 那么我们找出代码中具体那一块出现问题了勒,请看下面一段录屏 mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { imPrettySureSortingIsFree(); } }); /** *&emsp;排序后打印二维数组,一行行打印 */ public void imPrettySureSortingIsFree() { int dimension = 300; int[][] lotsOfInts = new int[dimension][dimension]; Random randomGenerator = new Random(); for (int i = 0; i < lotsOfInts.length; i++) { for (int j = 0; j < lotsOfInts[i].length; j++) { lotsOfInts[i][j] = randomGenerator.nextInt(); } } for (int i = 0; i < lotsOfInts.length; i++) { String rowAsStr = ""; //排序 int[] sorted = getSorted(lotsOfInts[i]); //拼接打印 for (int j = 0; j < lotsOfInts[i].length; j++) { rowAsStr += sorted[j]; if (j < (lotsOfInts[i].length - 1)) { rowAsStr += ", "; } } Log.i("ricky", "Row " + i + ": " + rowAsStr); } } 最后我们之后是 onClick 中的 imPrettySureSortingIsFree() 函数里面的 rowAsStr += sorted[j]; 字符串拼接造成的 内存抖动 ,因为每次拼接一个 String 都会申请一块新的堆内存,那么怎么解决这个频繁开辟内存的问题了。其实在 Java 中有 2 个更好的 API 对 String 的操作很友好,相信应该有人猜到了吧。没错就是将 此处的 String 换成 StringBuffer 或者 StringBuilder,就能很完美的解决字符串拼接造成的内存抖动问题。 修改后 /** *&emsp;打印二维数组,一行行打印 */ public void imPrettySureSortingIsFree() { int dimension = 300; int[][] lotsOfInts = new int[dimension][dimension]; Random randomGenerator = new Random(); for(int i = 0; i < lotsOfInts.length; i++) { for (int j = 0; j < lotsOfInts[i].length; j++) { lotsOfInts[i][j] = randomGenerator.nextInt(); } } // 使用StringBuilder完成输出,我们只需要创建一个字符串即可, 不需要浪费过多的内存 StringBuilder sb = new StringBuilder(); String rowAsStr = ""; for(int i = 0; i < lotsOfInts.length; i++) { // 清除上一行 sb.delete(0, rowAsStr.length()); //排序 int[] sorted = getSorted(lotsOfInts[i]); //拼接打印 for (int j = 0; j < lotsOfInts[i].length; j++) { sb.append(sorted[j]); if(j < (lotsOfInts[i].length - 1)){ sb.append(", "); } } rowAsStr = sb.toString(); Log.i("jason", "Row " + i + ": " + rowAsStr); } } 这里可以看见没有垃圾桶出现,说明内存抖动解决了。 注意: 实际开发中如果在 LogCat 中发现有这些 Log 说明也发生了 内存抖动 (Log 中出现 concurrent copying GC freed ....) 回收算法 ps:我觉得这个只是为了应付面试,那么可以参考这里,我也只了解概念这里就不用在多写了,点击看这个帖子吧 也可以参考掘金的这一篇 GC 回收算法 标记清除算法 Mark-Sweep 复制算法 Copying 标记压缩算法 Mark-Compact 分代收集算法 总结 (只要养成这样的习惯,至少可以避免 90 % 以上不会造成内存异常) 数据类型: 不要使用比需求更占用空间的基本数据类型 循环尽量用 foreach ,少用 iterator, 自动装箱也尽量少用 数据结构与算法的解度处理 (数组,链表,栈树,树,图) 数据量千级以内可以使用 Sparse 数组 (Key为整数),ArrayMap (Key 为对象) 虽然性能不如 HashMap ,但节约内存。 枚举优化 缺点: 每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存 较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的 IO 开销,使我们的应用需要更多的空间 特别是分 Dex 多的大型 APP,枚举的初始化很容易导致 ANR 优化后的代码:可以直接限定传入的参数个数 public class SHAPE { public static final int TYPE_0=0; public static final int TYPE_1=1; public static final int TYPE_2=2; public static final int TYPE_3=3; @IntDef(flag=true,value={TYPE_0,TYPE_1,TYPE_2,TYPE_3}) @Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.SOURCE) public @interface Model{ } private @Model int value=TYPE_0; public void setShape(@Model int value){ this.value=value; } @Model public int getShape(){ return this.value; } } static , static final 的问题 static 会由编译器调用 clinit 方法进行初始化 static final 不需要进行初始化工作,打包在 dex 文件中可以直接调用,并不会在类初始化申请内存 基本数据类型的成员,可以全写成 static final 字符串的拼接尽量少用 += 重复申请内存问题 同一个方法多次调用,如递归函数 ,回调函数中 new 对象 不要在 onMeause() onLayout() ,onDraw() 中去刷新UI(requestLayout) 避免 GC 回收将来要重新使用的对象 (内存设计模式对象池 + LRU 算法) Activity 组件泄漏 非业务需要不要把 activity 的上下文做参数传递,可以传递 application 的上下文 非静态内部类和匿名内部内会持有 activity 引用(静态内部类 或者 单独写文件) 单例模式中回调持有 activity 引用(弱引用) handler.postDelayed() 问题 如果开启的线程需要传入参数,用弱引接收可解决问题 handler 记得清除 removeCallbacksAndMessages(null) Service 耗时操作尽量使用 IntentService,而不是 Service 最后思维导图做一个总结:**推荐阅读:[2020最新Android大厂高频面试题解析大全(BAT TMD JD 小米)](https://www.jianshu.com/p/0d7808bdffec)**2020最新BAT Android高端技术面试145题详解2019年鸿洋大神最新整理一线互联网公司Android中高级面试题总结(附答案解析)**[2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中)](https://www.jianshu.com/p/7f9ade51232e)** 作者:DevYK链接:https://juejin.im/post/5cd82a3ee51d456e781f20ce来源:掘金
一声叹息 从去年9月3号,到今年3月20号,耗时6个月多的找工作经历终于是画上圆满的句号了,近200个日日夜夜的酸甜苦辣想必裸辞的亲尝者都能体会得到,下面想来复盘或者说总结一下这段经历。但不管怎么总结,核心还是那一句话:一定要充分的准备!!! 简历准备阶段 大家都知道,学历就是个敲门砖,所以对于一些背景比较好的同学,我就不告诉你怎么写简历了自由发挥吧 1. 那么对于一些学历背景一般般的同学要怎么让自己的简历更有亮点呢? 建议先分为两种,第一种是在校期间没有什么实习经验的同学,那简历中可以着重突出自己在校期间做过跟岗位相关的项目经验,我参考过很多同学的简历里面只有过项目的名字,导致面试官很难去判断,所以大家写项目和实习经验的时候,可以参考一下star法则~ “STAR法则是情境(situation)、任务(task)、行动(action)、结果(result)四项的缩写。STAR法则是一种常常被面试官使用的工具,用来收集面试者与工作相关的具体信息和能力。” 对于应聘研发岗位的朋友,在描述自己的项目经验的时候建议详细的说明一下,比如自己用了什么语言,什么框架去实现blablabla,时刻要记得我们在求职第一步的时候别人只能通过这个来评估你的能力呀~~~ 所以一定要突出自己的优点和能力! 2. 对于在校期间有过实习经验的朋友来说是稍微有优势一些的,那写自己的实习经验的时候其实要点也是跟刚刚讲的一样,要写出最能体现你的能力的项目,也是建议参考star的法则去写。 之前有一个朋友,学的后端开发,但是投递的是客户端的岗位,简历里写的项目经验也全都是后端的,面试官很难从他后端的经验里面衡量你能不能胜任客户端的这个岗位的呀!然后就没有然后啦! 我建议大家可以根据你要投递的岗位去跟着修改你的简历哦,像他这样的情况,面试官不会单靠他有这个意向转岗就可以的~ 对于校招的同学来说,如果已经有拿到了别的公司的offer的,建议大家在自己的简历里标注一下~这样也有利于评估的通过率哦 初期准备阶段 刚开始一个月还能耐得住性子在家里老老实实的复习,一个知识点一个知识点的过。第二个月便开始着急了,觉得这样复习效率太低。于是草草把没有复习完的内容快速过了一遍,着急开始找前同事和猎头推简历,面试机会确实是来了,而且是一线互联网公司。结果可想而知都很不理想:支付宝的第一轮电话面试就败下阵来、vivo内推勉强冲到第二轮也没能通关成功。发现自身问题后于是停止了推简历,又老老实实的复习剩下的知识点,并做好复习笔记。虽然从失败中总结到了经验,但白白浪费了机会,得不偿失。 中期阶段 基础知识点梳理完后,这个时候主要是去熟悉源码了。阿里腾讯这些大厂面试必问源码分析,可以结合项目中用到的开源框架有针对性的阅读下源码,面试过程中一般会根据你在项目中用到的框架,询问你对这些框架的原理是否熟练掌握。 通用框架一般无外乎网络库、图片库、工具类、插件化或热更新库等。这些知识点应该平时多去积累和练习为好,此时只要稍加复习即可。如果不是特别熟悉的可以去参考别人总结比较好的文章对着源码梳理,一定要在自己脑中形成知识结构,基本的实现细节要陈述出来。 另外复习面试高频知识点,做针对性的突击训练了! 该阶段复习可以参考知识点列表:2019年鸿洋大神最新整理一线互联网公司Android中高级面试题总结(附答案解析),基本涵盖到安卓和Java的绝大部分的基础知识点了,后续阶段的复习也可以参考这里的知识点:2017-2020历年字节跳动Android面试真题解析 另外一个总结得不错的列表可以作为补充:2020最新Android大厂高频面试题解析大全(BAT TMD JD 小米) 后期阶段 有了上面两个阶段的准备后,前两轮的基础面试基本没什么问题了。如果目标岗位是资深开发或者架构师的话,一般还会问到更底层原理和更抽象的宏观层面问题。 底层原理方面:比如虚拟机的内存区域和gc流程、tcp的流量和拥塞控制、https建立连接的交互流程等,这里可以去找对应的技术文章熟悉了解。 宏观层面:一般是架构模式(MVC、MVP、MVVM)、开发模式(模块化、组件化、模块组件化)以及设计模式相关问题,要能熟练掌握到灵活运用的层度,并总结出它们之间的异同特点。 另一大块就是算法了,某些一线公司比较喜欢考,比如今日头条在面试邮件中就明确指明要考算法。因此要对标你的目标公司是不是要考来进行复习。具体考哪些内容,以我面试的那些历程来看,基本都没超出《剑指offer》那六十几道题的范围(可能有对应题的变形),因此花一个礼拜左右的时间把那六十几道题弄懂并自己动手实现一遍基本ok,当然一些基础算法还要自己认真去总结学习,比如排序、二分查找、链表和树的基本操作等。 面试经历 主要是根据回忆总结的(会有遗漏点)。 1. 腾讯(QQ音乐) 腾讯面试涉及到的范围也很广,甚至问到了C++、Kotlin +Flutter ,也具有一定挑战性的,以下包括腾讯腾讯安卓客户端三面,最终拿到了测开岗位offer,腾讯面试过程中的感觉就是很多我不太熟悉的知识点都被问到了,甚至是不知道的知识点,但整体面试官给人的体验还不错,一般会提前打电话沟通面试时间。 C++:class与struct区别 项目:介绍项目,有什么难点; Java:HashMap;ArrayList,LinkedList用法有什么要注意的;注解介绍下;泛型中类型擦除是什么 算法&数据结构:字符串中出现频率中位数;最长公共子串问题LCS;线段树;B+树;快排及时间复杂度多少;七大排序;二叉树原理;红黑树 Android:OkHttp,OkHttp使用需要注意什么;RxJava介绍下;Activity四种启动模式;一个APP怎么退出所有Activity,如果有第三方SDK Activity,又怎么退出;EventBus原理;app卡顿; Kotlin :协程 Flutter :生命周期 2. 支付宝(海外版)仍是电话面的,还是没有找到感觉,回答不在状态。最后猎头反馈的本次面评是:过往项目功能较简单、某些技术细节掌握不到位。算是浪费了机会。 3. 今日头条 是所有参加的面试里比较专业的面试体验吧,面试官体现了很好的技术素养。总共参加了3轮视频面试(技术面全部面完),现已入职上海字节跳动。当然这里也花了很长时间准备(5个礼拜左右),主要是因为要考算法,从头头复习了算法,《剑指Offer》+ 《LeetCode》也是刷的我很痛苦, 还把所有知识点重头捋了一遍。 头条一面:tcp三次握手 4次挥手aidl 对象的在两个进程间通信leakcanery 为什么不能100%检测内存泄漏包内广播和包间广播handler 机制mvvmokHttp 有哪些拦截器,平时项目中如何使用如何自己设计一个内存检测工具检测Activity和fragment内存泄漏数据库用到哪些详细说说Java基础题。。。。。乐观锁悲观锁相关算法题:二叉树深度。。。 头条二面:说一下你最熟悉的项目,launcher3 上面的小点事怎么回事remotview 是如何加载在launcher 上面的jobsheduler的原理数据库为什么使用greendaofanal关键字在什么情况下设置内容子类和父类静态成员 静态方法 和父亲静态成员和构造方法执行顺序prebuffer 有使用过吗!原理是什么retoryfit相关直接说源码handler 相关直接源码sparryArray和HashMap 相关为什么性能强插件化相关图片加载库相关直接源码 头条三面:谈谈HashMap(为什么不适用基础数据类型、添加的时候需要注意什么、添加的key有什么特殊性)重写equals方法数据库范式扑克牌三带二(算一算出现的概率)为什么 Android 要采用 Binder 作为 IPC 机制工作项目难点,如何克服。反问环节 头条HR面:自我介绍未来的职业规划说一下自己平时的学习方法你认为这些学习方法里最有效的是哪一种?评价一下之前的面试官,或者说之前的面试官有没有给你留下印象最深刻的一点为什么想要来今日头条?你平时都用字节的哪些产品?有什么好的建议吗?期望薪资你有没有什么想问的? 总结 其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。 然而Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持! 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2020年的面试真题解析大全,小编还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。 【Android学习PDF+学习视频+面试文档+知识点笔记】 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】下载地址!https://shimo.im/docs/w6cyqyXqKRPDGcrr
Android开发中,Bitmap是经常会遇到的对象,特别是在列表图片展示、大图显示等界面。而Bitmap实实在在是内存使用的“大客户”。如何更好的使用Bitmap,减少其对App内存的使用,是Android优化方面不可回避的问题。因此,本文从常规的Bitmap使用,到Bitmap内存计算进行了介绍,最后分析了Bitmap的源码和其内存模型在不同版本上的变化。 Bitmap的使用 一般来说,一个对象的使用,我们会尝试利用其构造函数去生成这个对象。在Bitmap中,其构造函数: // called from JNI Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) 通过构造函数的注释,得知这是一个给native层调用的方法,因此可以知道Bitmap的创建将会涉及到底层库的支持。为了方便从不同来源来创建Bitmap,Android中提供了BitmapFactory工具类。BitmapFactory类中有一系列的decodeXXX方法,用于解析资源文件、本地文件、流等方式,基本流程都很类似,读取目标文件,转换成输入流,调用native方法解析流,虽然Java层代码没有体现,但是我们可以猜想到,最后native方法解析完成后,必然会通过JNI调用Bitmap的构造函数,完成Java层的Bitmap对象创建。 // BitmapFactory部分代码: public static Bitmap decodeResource(Resources res, int id) public static Bitmap decodeStream(InputStream is) private static native Bitmap nativeDecodeStream native层的代码稍后我们在看,先从Java层来看看常规的使用。典型的一个例子是,当我们需要从本地Resource中加载一个图片,并展示出来,我们可以通过BitmapFacotry来完成: Bitmap bitmapDecode = BitmapFactory.decodeResource(getResources(), resId); imageView.setImageBitmap(bitmapDecode); 当然,这里简单的使用imageView.setImageResource(int resId)也能实现一样的效果,实际上setImageResource方法只是封装了bitmap的读入、解析的过程,并且这个过程是在UI线程完成的,对于性能是有所影响的。另外,也对接下来讨论的内容,Bitmap占用的内存有影响。 Bitmap到底占用多大的内存 Bitmap作为位图,需要读入一张图片每一个像素点的数据,其主要占用内存的地方也正是这些像素数据。对于像素数据总大小,我们可以猜想为:像素总数量 × 每个像素的字节大小,而像素总数量在矩形屏幕表现下,应该是:横向像素数量 × 纵向像素数量,结合得到: Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小 单个像素的字节大小 单个像素的字节大小由Bitmap的一个可配置的参数Config来决定。Bitmap中,存在一个枚举类Config,定义了Android中支持的Bitmap配置: Config 占用字节大小(byte) 说明 ALPHA_8 (1) 1 单透明通道 RGB_565 (3) 2 简易RGB色调 ARGB_4444 (4) 4 已废弃 ARGB_8888 (5) 4 24位真彩色 RGBA_F16 (6) 8 Android 8.0 新增(更丰富的色彩表现HDR) HARDWARE (7) Special Android 8.0 新增 (Bitmap直接存储在graphic memory)注1 注1:关于Android 8.0中新增的这个配置,stackoverflow已经有相关问题,可以关注下。 之前我们分析到,Bitmap的decode实际上是在native层完成的,因此在native层也存在对应的Config枚举类。一般使用时,我们并未关注这个配置,在BitmapFactory中,有: * Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by default. */ public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888; 因此,Android系统中,默认Bitmap加载图片,使用24位真彩色模式。 Bitmap占用内存大小实例 首先准备了一张800×600分辨率的jpg图片,大小约135k,放置于res/drawable文件夹下: 并将其加载到一个200dp×300dp大小的ImageView中,使用BitmapFactory。 Bitmap bitmapDecode = BitmapFactory.decodeResource(getResources(), resId); imageView.setImageBitmap(bitmapDecode); 打印出相关信息: 图中显示了从资源文件中decode得到的bitmap的长、宽和占用内存大小(byte)等信息。首先,从数据上可以验证: 17280000 = 2400 1800 4 这意味着,为了将单张800 * 600 的图片加载到内存当中,付出了近17.28M的代价,即使现在手机运存普遍上涨,这样的开销也是无法接受的,因此,对于Bitmap的使用,是需要非常小心的。好在,目前主流的图像加载库(Glide、Fresco等)基本上都不在需要开发者去关心Bitmap内存占用问题。先暂时回到Bitmap占用内存的计算上来,对比之前定义的公式和源图片的尺寸数据,我们会发现,这张800 600大小的图片,decode到内存中的Bitmap的横纵像素数量实际是:2400 1800,相当于缩放了3倍大小。为了探究这缩放来自何处,我们开始跟踪源码:之前提到过,Bitmap的decode过程实际上是在native层完成的,为此,需要从BitmapFactory.cpp#nativeDecodeXXX方法开始跟踪,这里省略其他decode代码,直接贴出和缩放相关的代码如下: if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } ... int scaledWidth = decoded->width(); int scaledHeight = decoded->height(); if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } ... if (willScale) { const float sx = scaledWidth / float(decoded->width()); const float sy = scaledHeight / float(decoded->height()); bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight); bitmap->allocPixels(&javaAllocator, NULL); bitmap->eraseColor(0); SkPaint paint; paint.setFilterBitmap(true); SkCanvas canvas(*bitmap); canvas.scale(sx, sy); canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint); } 从上述代码中,我们看到bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操作,scale的值由 scale = (float) targetDensity / density; 这一行代码决定,即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中获取到的。这时候,需要回到Java层,看看options这个对象的定义和赋值。 BitmapFactory#Options Options是BitmapFactory中的一个静态内部类,用于配置Bitmap在decode时的一些参数。 // native层doDecode方法,传入了Options参数 static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) 其内部有很多可配置的参数,下面的类图,列举出了部分常用的参数。 我们先关注之前提到的几个密度相关的参数,通过阅读源码的注释,大概可以知道这三个密度参数代表的涵义: inDensity:Bitmap位图自身的密度、分辨率 inTargetDensity: Bitmap最终绘制的目标位置的分辨率 inScreenDensity: 设备屏幕分辨率 其中inDensity和图片存放的资源文件的目录有关,同一张图片放置在不同目录下会有不同的值: density 0.75 1 1.5 2 3 3.5 4 densityDpi 120 160 240 320 480 560 640 DpiFolder ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi xxxxhdpi inTargetDensity和inScreenDensity一般来说,很少手动去赋值,默认情况下,是和设备分辨率保持一致。为此,我在手机(红米4,Android 6.0系统,设备dpi 480)上测试加载不同资源文件下的bitmap的参数,结果见下图: 以上可以验证几个结论: 同一张图片,放在不同资源目录下,其分辨率会有变化, bitmap分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少 图片不特别放置任何资源目录时,其默认使用mdpi分辨率:160 资源目录分辨率和设备分辨率一致时,图片尺寸不会缩放 因此,关于Bitmap占用内存大小的公式,从之前: Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小 可以更细化为: Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (设备分辨率/资源目录分辨率)^2 × 每个像素的字节大小 对于本节中最开始的例子,如下: 17,280,000 = 800 600 (480 / 160 )^2 * 4 Bitmap内存优化 图片占用的内存一般会分为运行时占用的运存和存储时本地开销(反映在包大小上),这里我们只关注运行时占用内存的优化。在上一节中,我们看到对于一张800 * 600 大小的图片,不加任何处理直接解析到内存中,将近占用了17.28M的内存大小。想象一下这样的开销发生在一个图片列表中,内存占用将达到非常夸张的地步。从之前Bitmap占用内存的计算公式来看,减少内存主要可以通过以下几种方式: 使用低色彩的解析模式,如RGB565,减少单个像素的字节大小 资源文件合理放置,高分辨率图片可以放到高分辨率目录下 图片缩小,减少尺寸 第一种方式,大约能减少一半的内存开销。Android默认是使用ARGB8888配置来处理色彩,占用4字节,改用RGB565,将只占用2字节,代价是显示的色彩将相对少,适用于对色彩丰富程度要求不高的场景。第二种方式,和图片的具体分辨率有关,建议开发中,高分辨率的图像应该放置到合理的资源目录下,注意到Android默认放置的资源目录是对应于160dpi,目前手机屏幕分辨率越来越高,此处能节省下来的开销也是很可观的。理论上,图片放置的资源目录分辨率越高,其占用内存会越小,但是低分辨率图片会因此被拉伸,显示上出现失真。另一方面,高分辨率图片也意味着其占用的本地储存也变大。第三种方式,理论上根据适用的环境,是可以减少十几倍的内存使用的,它基于这样一个事实:源图片尺寸一般都大于目标需要显示的尺寸,因此可以通过缩放的方式,来减少显示时的图片宽高,从而大大减少占用的内存。 前两种方式,相对比较简单。第三种方式会涉及到一些编码,目前也有很多典型的使用方式,如下: BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), resId,options); options.inJustDecodeBounds = false; options.inSampleSize = BitmapUtil.computeSampleSize(options, -1, imageView.getWidth() * imageView.getHeight()); Bitmap newBitmap = BitmapFactory.decodeResource(getResources(), resId, options); 原理很简单,充分利用了Options类里的参数设置,也可以从native底层源码上看到对应的逻辑。第一次解析bitmap只获取尺寸信息,不生成像素数据,继而比较bitmap尺寸和目标尺寸得到缩放倍数,第二次根据缩放倍数去解析我们实际需要的尺寸大小。 // Apply a fine scaling step if necessary. if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) { willScale = true; scaledWidth = codec->getInfo().width() / sampleSize; scaledHeight = codec->getInfo().height() / sampleSize; } 上图是使用上述手段优化后的结果,可以看到现在占用的内存大小大约为960KB,从优化后的宽高来看,第三种方式并没有效果。应为目标ImageView尺寸也不小,而inSampleSize的值必须是2的整数幂,因此计算得到的值还是1。 PS: Bitmap内存占用的优化还有一个方式是复用和缓存 不同Android版本时的Bitmap内存模型 我们知道Android系统中,一个进程的内存可以简单分为Java内存和native内存两部分,而Bitmap对象占用的内存,有Bitmap对象内存和像素数据内存两部分,在不同的Android系统版本中,其所存放的位置也有变化。Android Developers上列举了从API 8 到API 26之间的分配方式: API级别 API 10 - API 11 ~ API 25 API 26 + Bitmap对象存放 Java heap Java heap Java heap 像素(pixel data)数据存放 native heap Java heap native heap 可以看到,最新的Android O之后,谷歌又把像素存放的位置,从java 堆改回到了 native堆。API 11的那次改动,是源于native的内存释放不及时,会导致OOM,因此才将像素数据保存到Java堆,从而保证Bitmap对象释放时,能够同时把像素数据内存也释放掉。 上面两幅图展示了不同系统,加载图片后,内存的变化,8.0的截图比较模糊。途中浅蓝色对应的是Java heap使用,深蓝色对应的是native heap的使用。跟踪一下8.0的native源码来看看具体的变化: // BitmapFactory.cpp if (!decodingBitmap.setInfo(bitmapInfo) || !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) { // SkAndroidCodec should recommend a valid SkImageInfo, so setInfo() // should only only fail if the calculated value for rowBytes is too // large. // tryAllocPixels() can fail due to OOM on the Java heap, OOM on the // native heap, or the recycled javaBitmap being too small to reuse. return nullptr; } // Graphics.cpp bool HeapAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) { mStorage = android::Bitmap::allocateHeapBitmap(bitmap, sk_ref_sp(ctable)); return !!mStorage; } // https://android.googlesource.com/platform/frameworks/base/+/master/libs/hwui/hwui/Bitmap.cpp static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) { void* addr = calloc(size, 1); if (!addr) { return nullptr; } return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes)); } 还是通过BitmapFactory.cpp#doDecode方法来跟踪,发现其中tryAllocPixels方法,应该是尝试去进行内存分配,其中decodeAllocator会被赋值为HeapAllocator,通过一系列的调用,最终通过calloc方法,在native分配内存。至于为什么Google 在8.0上改变了Bitmap像素数据的存放方式,我猜想和8.0中的GC算法调整有关系。GC算法的优化,使得Bitmap占用的大内存区域,在GC后也能够比较快速的回收、压缩,重新使用。 (native存放) 退出Activity 退出App onStop中主动调用gc()和recycler() 内存不释放 内存释放 无调用 内存不释放 内存不释放 (gpu存放) 退出Activity 退出App onStop中主动调用gc()和recycler() 内存释放 内存释放 无调用 内存释放 内存释放 总结 // 8.0源码 Bitmap(long nativeBitmap, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) // 7.0源码 Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) 一开始看两者java代码不同,少了存放像素的buffer字段,查阅相关资料到native源码对比,最终总结了下Bitmap内存相关的知识。另外,在Android 8.0中,关于Bitmap的改动有两方面还需深入探究的:1、Config配置为Hardware时的优劣。Hardware配置实际上没有改变像素的位储存大小(还是默认的ARGB8888),但是改变了bitmap像素的存储位置(存放到GPU内存中),对实际应用的影响会如何?;2、Bitmap在8.0后又回归到native存放bitmap像素数据,而这部分数据的回收时机和触发方式又是如何?一般测试下,可以通过native分配Bitmap超过1G的内存数据而不发生崩溃。 作者:Dragon_Boat链接:https://www.jianshu.com/p/3f6f6e4f1c88来源:简书
本文首发于简书——Alvin老师,搬运转载请注明出处,否则将追究版权责任。 上次搜集整理字节跳动面试专题文章的时候也过去了差不多一个月了,期间收到了面试交流群友的积极响应,纷纷表示获益匪浅。部分同学也因此收获了理想的offer! 但看到还有很多人最近还在找Android开发工作, 而且很多人都感觉今年受疫情影响,找工作比去年难很多, 竞争力也增加不少, 因此激发我整理这份Android大厂高频面试资料, 希望能帮到还在找或者准备找工作的同学们。 这是我们集合了牛客网、掘金、简书、知乎、CSDN等网站的几十篇面经和群友自己面试的经历的合集,希望大家喜欢。 致谢:《2020最新Android大厂高频面试题解析大全》需特别感谢:群友 simpleeeeee搜集整理。面试题解析采用了阿里云云栖号、 美团技术团队、字节跳动技术团队、腾讯Bugly、gityuan、郭霖、张鸿洋、任玉刚等一线互联网技术团队大咖分享的源码解析、项目实战案例应用。确保了这份面试资料的专业性和权威性。同时也能让我们更加深入的去理解Android高频面试要点,尽快拿到理想的offer。当然这里面可能还会有纰漏,如果有问题欢迎大家留言或者点击我讨论并领取这份资料! 目录如下:第一章 Android 相关第二章 性能优化第三章 Java 相关第四章 Kotlin 相关第五章 网络相关第六章 插件化&热修复&模块化&组件化&增量更新&Gradle第七章 图片相关第八章 Flutter 相关 第一章 Android 相关 Android 高级面试经常会有很多原理分析、主要源于大公司应该会根据公司的实际情况去写框架。但基本上没有谁能像天才一样从零写出一个框架,很多人写框架其实都是从模仿开始的。而你要模仿,那么你首先得看得懂框架源码才行。所以说阅读源码才显得那么重要。这也是为什么阿里腾讯Android面试会如此注重源码原理分析。 我们第一章也着重从Android面试常见的Framework、binder、EventBus、线程和线程池、SharedPreferences等众多知识要点进行原理解析。帮助大家深刻理解源码原理。 第二章 性能优化 为什么性能优化如此重要? 只要做Android 应用开发人员都知道,APP开发过程中非常影响产品品质和用户留存率就是性能优化问题。因此几乎所有互联网企业都会注重对开发人员性能优化技能的考察! 本章分别从绘制(UI)、内存、存储、稳定性、耗电以及安装包等几个方面进行优化,从系统上深入分析绘制和内存的原理,一步步深入了解导致性能问题的本质原因,同时讲述了多种性能优化工具的使用,通过分析典型案例,得到有效的优化方案,从而实现更高质量的应用。 第三章 Java 相关 Java是Android开发的基础,同时也是大厂面试的第一道门槛。泛型、多线程、反射、JVM、Java IO 、注解、序列化等这些并不是关乎用不用得上的问题,主要是考察你的基础技能是否扎实,也在考察你的技能深度。 第四章 Kotlin 相关 Kotlin 应用于 Android 开发相比传统 Java优势,在于依赖于 Kotlin 大量的语法糖以及更简洁易表现的语法风格能够大大提高开发效率,减少代码量,降低维护成本。因此美团、阿里、腾讯等技术团队均已经在使用kotlin,因此会kotlin开发的你,肯定更受面试官的青睐! 第五章 网络相关 关于计算机网络,HTTP网络通信协议在任何的开发工作中都非常重要!Android开发面试也会经常被问及计算机网络知识,主要考察我们是否系统的学习了操作系统和计算机组成原理,因为只有我们看完操作系统后才能系统的认识计算机的原理。 第六章 插件化&热修复&模块化&组件化&增量更新&Gradle 插件化技术可以说是Android高级工程师所必须具备的技能之一,从2012年插件化概念的提出(Android版本),到2016年插件化的百花争艳,可以说,插件化技术引领着Android技术的进步。热修复:让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。大厂面试需要我们掌握通过设计思想解读开源框架! 第七章 图片相关 现在Android上的图片加载框架非常成熟,从最早的老牌图片加载框架UniversalImageLoader,到后来Google推出的Volley,再到后来的新兴军Glide和Picasso,当然还有Facebook的Fresco。每一个都非常稳定,功能也都十分强大。并了解各个图片库的特点。但是它们的使用场景基本都是重合的,也就是说我们基本只需要关注Glide进行学习和使用就足够了。 第八章 Flutter 相关 全球已经有很多大家熟悉的品牌采用了 Flutter,包括很多国内的知名公司。比如阿里巴巴有多款移动应用已经上线 Flutter 版本。Flutter以其美观、快速、高效、开放等特点,在国内Flutter 的开发者社区非常活跃。社区贡献了大量高质量的技术文章,Flutter技术日益更新迭代速度极快,同样各大互联网公司对优秀Flutter技术人员也是甘之若饴。 最后 学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持! 上面分享的腾讯、头条、阿里、美团、字节跳动等公司2020年的高频面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。 【Android思维脑图(技能树)】 知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。【Android学习PDF+学习视频+面试文档+知识点笔记】 【Android高级架构视频学习资源】 Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧! 【Android进阶学习视频】、【全套Android面试秘籍】可以私信我【学习】查看免费领取方式!
相信大家都遇到过手机图片滑动卡顿问题,由于不断的GC导致的丢帧卡顿的问题让我们想了很多方案去解决,所以就打算详细的看看内存分配和GC的原理,为什么会不断的GC,GC ALLOC和GC COCURRENT有什么区别,能不能想办法扩大堆内存减少GC的频次等等。 1、JVM内存回收机制 1.1 回收算法 标记回收算法(Mark and Sweep GC) 从"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,这个算法需要中断进程内其它组件的执行并且可能产生内存碎片 复制算法 (Copying) 将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。 标记-压缩算法 (Mark-Compact) 先需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。 分代 将所有的新建对象都放入称为年轻代的内存区域,年轻代的特点是对象会很快回收,因此,在年轻代就选择效率较高的复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老生代的内存空间。对于新生代适用于复制算法,而对于老年代则采取标记-压缩算法。 1.2 复制和标记-压缩算法的区别 乍一看这两个算法似乎并没有多大的区别,都是标记了然后挪到另外的内存地址进行回收,那为什么不同的分代要使用不同的回收算法呢? 其实2者最大的区别在于前者是用空间换时间后者则是用时间换空间。 前者的在工作的时候是不没有独立的“mark”与“copy”阶段的,而是合在一起做一个动作,就叫scavenge(或evacuate,或者就叫copy)。也就是说,每发现一个这次收集中尚未访问过的活对象就直接copy到新地方,同时设置forwarding pointer。这样的工作方式就需要多一份空间。 后者在工作的时候则需要分别的mark与compact阶段,mark阶段用来发现并标记所有活的对象,然后compact阶段才移动对象来达到compact的目的。如果compact方式是sliding compaction,则在mark之后就可以按顺序一个个对象“滑动”到空间的某一侧。因为已经先遍历了整个空间里的对象图,知道所有的活对象了,所以移动的时候就可以在同一个空间内而不需要多一份空间。 所以新生代的回收会更快一点,老年代的回收则会需要更长时间,同时压缩阶段是会暂停应用的,所以给我们应该尽量避免对象出现在老年代。 2、Dalvik虚拟机 2.1 java堆 Java堆实际上是由一个Active堆和一个Zygote堆组成的,其中,Zygote堆用来管理Zygote进程在启动过程中预加载和创建的各种对象,而Active堆是在Zygote进程fork第一个子进程之前创建的。以后启动的所有应用程序进程是被Zygote进程fork出来的,并都持有一个自己的Dalvik虚拟机。在创建应用程序的过程中,Dalvik虚拟机采用COW策略复制Zygote进程的地址空间。 COW策略:一开始的时候(未复制Zygote进程的地址空间的时候),应用程序进程和Zygote进程共享了同一个用来分配对象的堆。当Zygote进程或者应用程序进程对该堆进行写操作时,内核就会执行真正的拷贝操作,使得Zygote进程和应用程序进程分别拥有自己的一份拷贝,这就是所谓的COW。因为copy是十分耗时的,所以必须尽量避免copy或者尽量少的copy。 为了实现这个目的,当创建第一个应用程序进程时,会将已经使用了的那部分堆内存划分为一部分,还没有使用的堆内存划分为另外一部分。前者就称为Zygote堆,后者就称为Active堆。这样只需把zygote堆中的内容复制给应用程序进程就可以了。以后无论是Zygote进程,还是应用程序进程,当它们需要分配对象的时候,都在Active堆上进行。这样就可以使得Zygote堆尽可能少地被执行写操作,因而就可以减少执行写时拷贝的操作。在Zygote堆里面分配的对象其实主要就是Zygote进程在启动过程中预加载的类、资源和对象了。这意味着这些预加载的类、资源和对象可以在Zygote进程和应用程序进程中做到长期共享。这样既能减少拷贝操作,还能减少对内存的需求。 2.2 和GC有关的一些指标 记得我们之前在优化魅族某手机的gc卡顿问题时,发现他很容易触发GC_FOR_MALLOC,这个GC类别后续会说到,是分配对象内存不足时导致的。可是我们又设置了很大的堆Size为什么还会内存不够呢,这里需要了解以下几个概念:分别是Java堆的起始大小(Starting Size)、最大值(Maximum Size)和增长上限值(Growth Limit)。 在启动Dalvik虚拟机的时候,我们可以分别通过-Xms、-Xmx和-XX:HeapGrowthLimit三个选项来指定上述三个值,以上三个值分别表示表示 Starting Size : Dalvik虚拟机启动的时候,会先分配一块初始的堆内存给虚拟机使用。 Growth Limit:是系统给每一个程序的最大堆上限,超过这个上限,程序就会OOM Maximum Size:不受控情况下的最大堆内存大小,起始就是我们在用largeheap属性的时候,可以从系统获取的最大堆大小 同时除了上面的这个三个指标外,还有几个指标也是值得我们关注的,那就是堆最小空闲值(Min Free)、堆最大空闲值(Max Free)和堆目标利用率(Target Utilization)。假设在某一次GC之后,存活对象占用内存的大小为LiveSize,那么这时候堆的理想大小应该为(LiveSize / U)。但是(LiveSize / U)必须大于等于(LiveSize + MinFree)并且小于等于(LiveSize + MaxFree),每次GC后垃圾回收器都会尽量让堆的利用率往目标利用率靠拢。所以当我们尝试手动去生成一些几百K的对象,试图去扩大可用堆大小的时候,反而会导致频繁的GC,因为这些对象的分配会导致GC,而GC后会让堆内存回到合适的比例,而我们使用的局部变量很快会被回收理论上存活对象还是那么多,我们的堆大小也会缩减回来无法达到扩充的目的。 与此同时这也是产生CONCURRENT GC的一个因素,后文我们会详细讲到。 2.3 GC的类型 GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。 GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存。 GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。 GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。 实际上,GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。而并发和非并发GC的区别主要在于前者在GC过程中,有条件地挂起和唤醒非GC线程,而后者在执行GC的过程中,一直都是挂起非GC线程的。并行GC通过有条件地挂起和唤醒非GC线程,就可以使得应用程序获得更好的响应性。但是同时并行GC需要多执行一次标记根集对象以及递归标记那些在GC过程被访问了的对象的操作,所以也需要花费更多的CPU资源。后文在Art的并发和非并发GC中我们也会着重说明下这两者的区别。 2.4 对象的分配和GC触发时机 1. 调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存。如果分配成功,那么就将分配得到的地址直接返回给调用者了。函数dvmHeapSourceAlloc在不改变Java堆当前大小的前提下进行内存分配,这是属于轻量级的内存分配动作。 2. 如果上一步内存分配失败,这时候就需要执行一次GC了。不过如果GC线程已经在运行中,即gDvm.gcHeap->gcRunning的值等于true,那么就直接调用函数dvmWaitForConcurrentGcToComplete等到GC执行完成就是了。否则的话,就需要调用函数gcForMalloc来执行一次GC了,参数false表示不要回收软引用对象引用的对象。 3. GC执行完毕后,再次调用函数dvmHeapSourceAlloc尝试轻量级的内存分配操作。如果分配成功,那么就将分配得到的地址直接返回给调用者了。 4. 如果上一步内存分配失败,这时候就得考虑先将Java堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,再进行内存分配了。这是通过调用函数dvmHeapSourceAllocAndGrow来实现的。 5. 如果调用函数dvmHeapSourceAllocAndGrow分配内存成功,则直接将分配得到的地址直接返回给调用者了。 6. 如果上一步内存分配还是失败,这时候就得出狠招了。再次调用函数gcForMalloc来执行GC。参数true表示要回收软引用对象引用的对象。 7. GC执行完毕,再次调用函数dvmHeapSourceAllocAndGrow进行内存分配。这是最后一次努力了,成功与事都到此为止。 示例图如下: 通过这个流程可以看到,在对象的分配中会导致GC,第一次分配对象失败我们会触发GC但是不回收Soft的引用,如果再次分配还是失败我们就会将Soft的内存也给回收,前者触发的GC是GC_FOR_MALLOC类型的GC,后者是GC_BEFORE_OOM类型的GC。而当内存分配成功后,我们会判断当前的内存占用是否是达到了GC_CONCURRENT的阀值,如果达到了那么又会触发GC_CONCURRENT。 那么这个阀值又是如何来的呢,上面我们说到的一个目标利用率,GC后我们会记录一个目标值,这个值理论上需要再上述的范围之内,如果不在我们会选取边界值做为目标值。虚拟机会记录这个目标值,当做当前允许总的可以分配到的内存。同时根据目标值减去固定值(200~500K),当做触发GC_CONCURRENT事件的阈值。 2.5 回收算法和内存碎片 主流的大部分Davik采取的都是标注与清理(Mark and Sweep)回收算法,也有实现了拷贝GC的,这一点和HotSpot是不一样的,具体使用什么算法是在编译期决定的,无法在运行的时候动态更换。如果在编译dalvik虚拟机的命令中指明了"WITH_COPYING_GC"选项,则编译"/dalvik/vm/alloc/Copying.cpp"源码 – 此是Android中拷贝GC算法的实现,否则编译"/dalvik/vm/alloc/HeapSource.cpp" – 其实现了标注与清理GC算法。 由于Mark and Sweep算法的缺点,容易导致内存碎片,所以在这个算法下,当我们有大量不连续小内存的时候,再分配一个较大对象时,还是会非常容易导致GC,比如我们在该手机上decode图片,具体情况如下: 所以对于Dalvik虚拟机的手机来说,我们首先要尽量避免掉频繁生成很多临时小变量(比如说:getView,onDraw等函数),另一个又要尽量去避免产生很多长生命周期的大对象。 3、ART内存回收机制 3.1 Java堆 ART运行时内部使用的Java堆的主要组成包括Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,Image Space用来存在一些预加载的类, Zygote Space和Allocation Space与Dalvik虚拟机垃圾收集机制中的Zygote堆和Active堆的作用是一样的, Large Object Space就是一些离散地址的集合,用来分配一些大对象从而提高了GC的管理效率和整体性能,类似如下图: 在下文的GC Log中,我们也能看到在art的GC Log中包含了LOS的信息,方便我们查看大内存的情况。 3.2 GC的类型 kGcCauseForAlloc ,当要分配内存的时候发现内存不够的情况下引起的GC,这种情况下的GC会stop world kGcCauseBackground,当内存达到一定的阀值的时候会去出发GC,这个时候是一个后台gc,不会引起stop world kGcCauseExplicit,显示调用的时候进行的gc,如果art打开了这个选项的情况下,在system.gc的时候会进行gc 其他更多 3.3 对象的分配和GC触发时机 由于Art下内存分配和Dalvik下基本没有任何区别,我直接贴图带过了。 3.4 并发和非并发GC Art在GC上不像Dalvik仅有一种回收算法,Art在不同的情况下会选择不同的回收算法,比如Alloc内存不够的时候会采用非并发GC,而在Alloc后发现内存达到一定阀值的时候又会触发并发GC。同时在前后台的情况下GC策略也不尽相同,后面我们会一一给大家说明。 非并发GC 步骤1. 调用子类实现的成员函数InitializePhase执行GC初始化阶段。 步骤2. 挂起所有的ART运行时线程。 步骤3. 调用子类实现的成员函数MarkingPhase执行GC标记阶段。 步骤4. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。 步骤5. 恢复第2步挂起的ART运行时线程。 步骤6. 调用子类实现的成员函数FinishPhase执行GC结束阶段。 并发GC 步骤1. 调用子类实现的成员函数InitializePhase执行GC初始化阶段。 步骤2. 获取用于访问Java堆的锁。 步骤3. 调用子类实现的成员函数MarkingPhase执行GC并行标记阶段。 步骤4. 释放用于访问Java堆的锁。 步骤5. 挂起所有的ART运行时线程。 步骤6. 调用子类实现的成员函数HandleDirtyObjectsPhase处理在GC并行标记阶段被修改的对象。。 步骤7. 恢复第4步挂起的ART运行时线程。 步骤8. 重复第5到第7步,直到所有在GC并行阶段被修改的对象都处理完成。 步骤9. 获取用于访问Java堆的锁。 步骤10. 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。 步骤11. 释放用于访问Java堆的锁。 步骤12. 调用子类实现的成员函数FinishPhase执行GC结束阶段。 所以不论是并发还是非并发,都会引起stopworld的情况出现,并发的情况下单次stopworld的时间会更短,基本区别和。 3.5 Art并发和Dalvik并发GC的差异 首先可以通过如下2张图来对比下 Dalvik GC: Art GC Art的并发GC和Dalvik的并发GC有什么区别呢,初看好像2者差不多,虽然没有一直挂起线程,但是也会有暂停线程去执行标记对象的流程。通过阅读相关文档可以了解到Art并发GC对于Dalvik来说主要有三个优势点: 1、标记自身 Art在对象分配时会将新分配的对象压入到Heap类的成员变量allocation_stack_描述的Allocation Stack中去,从而可以一定程度缩减对象遍历范围。 2、预读取 对于标记Allocation Stack的内存时,会预读取接下来要遍历的对象,同时再取出来该对象后又会将该对象引用的其他对象压入栈中,直至遍历完毕。 3、减少Pause时间 在Mark阶段是不会Block其他线程的,这个阶段会有脏数据,比如Mark发现不会使用的但是这个时候又被其他线程使用的数据,在Mark阶段也会处理一些脏数据而不是留在最后Block的时候再去处理,这样也会减少后面Block阶段对于脏数据的处理的时间。 3.6 前后台GC 前台Foreground指的就是应用程序在前台运行时,而后台Background就是应用程序在后台运行时。因此,Foreground GC就是应用程序在前台运行时执行的GC,而Background就是应用程序在后台运行时执行的GC。 应用程序在前台运行时,响应性是最重要的,因此也要求执行的GC是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。因此,Mark-Sweep GC适合作为Foreground GC,而Mark-Compact GC适合作为Background GC。 由于有Compact的能力存在,碎片化在Art上可以很好的被避免,这个也是Art一个很好的能力。 3.7 Art大法好 总的来看,art在gc上做的比dalvik好太多了,不光是gc的效率,减少pause时间,而且还在内存分配上对大内存的有单独的分配区域,同时还能有算法在后台做内存整理,减少内存碎片。对于开发者来说art下我们基本可以避免很多类似gc导致的卡顿问题了。另外根据谷歌自己的数据来看,Art相对Dalvik内存分配的效率提高了10倍,GC的效率提高了2-3倍。 4、GC Log 当我们想要根据GC日志来追查一些GC可能造成的卡顿时,我们需要了解GC日志的组成,不同信息代表了什么含义。 4.1 Dalvik GC日志 dalvik的日志格式基本如下: D/dalvikvm: , , , gc_reason:就是我们上文提到的,是gc_alloc还是gc_concurrent,了解到不同的原因方便我们做不同的处理。 amount_freed:表示系统通过这次GC操作释放了多少内存 Heap_stats:中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存) Pause_time:表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,所以还有后续的一个total_time。 Total_time:表示本次GC所花费的总时间和上面的Pause_time,也就是stop all是不一样的,卡顿时间主要看上面的pause_time。 4.2 Art GC日志 I/art: , , , , 基本情况和Dalvik没有什么差别,GC的Reason更多了,还多了一个OS_Space_Status LOS_Space_Status:Large Object Space,大对象占用的空间,这部分内存并不是分配在堆上的,但仍属于应用程序内存空间,主要用来管理 bitmap 等占内存大的对象,避免因分配大内存导致堆频繁 GC。 **推荐阅读:字节跳动面试题 —— 水壶问题2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中)** 原文作者:Tmacchen原文链接:https://zhuanlan.zhihu.com/p/24835977
1.简介 CoordinatorLayout遵循Material 风格,包含在 support Library中,结合AppbarLayout, CollapsingToolbarLayout等 可 产生各种炫酷的折叠悬浮效果。 作为最上层的View 作为一个 容器与一个或者多个子View进行交互 2.AppBarLayout 它是继承与LinearLayout的,默认 的 方向 是Vertical appbarLayout的滑动flag 类型 说明 int SCROLL_FLAG_ENTER_ALWAYS W((entering) / (scrolling on screen))下拉的时候,这个View也会跟着滑出。 int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED 另一种enterAlways,但是只显示折叠后的高度。 int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED ((exiting) / (scrolling off screen))上拉的时候,这个View会跟着滑动直到折叠。 int SCROLL_FLAG_SCROLL 这个View将会响应Scroll事件 int SCROLL_FLAG_SNAP 在Scroll滑动事件结束以前 ,如果这个View部分可见,那么这个View会停在最接近当前View的位置 我们可以通过两种 方法设置这个Flag 方法一 setScrollFlags(int) 方法二 app:layout_scrollFlags="scroll|enterAlways" AppBarLayout必须作为CoordinatorLayout的直接子View,否则它的大部分功能将不会生效,如layout_scrollFlags等。 效果图一: 布局: <android.support.design.widget.CoordinatorLayout android:id="@+id/main_content" 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"> <android.support.design.widget.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:layout_scrollFlags="scroll|enterAlways" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> . </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:layout_margin="15dp" android:src="@drawable/add_2"/> </android.support.design.widget.CoordinatorLayout> 思路分析: 那如果当我们的toolBar 等于 app:layout_scrollFlags=”scroll|snap”的时候 , layout_scrollFlags=scroll的时候,这个View会 跟着 滚动 事件响应, layout_scrollFlags=“snap”的时候 在Scroll滑动事件结束以前 ,如果这个View部分可见,那么这个View会停在最接近当前View的位置。 效果如下: 结合ViewPage,布局代码如下: <android.support.design.widget.CoordinatorLayout android:id="@+id/main_content" 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"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="250dp"> <ImageView android:layout_width="match_parent" android:layout_height="200dp" android:background="?attr/colorPrimary" android:scaleType="fitXY" android:src="@drawable/tangyan" app:layout_scrollFlags="scroll|enterAlways"/> <android.support.design.widget.TabLayout android:id="@+id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="?attr/colorPrimary" app:tabIndicatorColor="@color/colorAccent" app:tabIndicatorHeight="4dp" app:tabSelectedTextColor="#000" app:tabTextColor="#fff"/> </android.support.design.widget.AppBarLayout> <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:layout_margin="15dp" android:src="@drawable/add_2"/> </android.support.design.widget.CoordinatorLayout> 思路分析: 其实相对于前 一个例子,只是把 摆放RecyclerView 的位置替换成ViewPager而已,为了有页面导航器的效果,再使用 TabLayout而已,而TabLayout 在我们滑动的时候最终会停靠在 最顶部,是因为我们没有设置其layout_scrollFlags,即TabLayout是静态的 运行以后,即可看到以下的结果 3.CollapsingToolbarLayout 简单来说 ,CollapsingToolbarLayout是工具栏的包装器,它通常作为AppBarLayout的孩子。主要实现以下功能 Collapsing title(可以折叠 的 标题 ) Content scrim(内容装饰),当我们滑动的位置 到达一定阀值的时候,内容 装饰将会被显示或者隐藏 Status bar scrim(状态栏布) Parallax scrolling children,滑动的时候孩子呈现视觉特差效果 Pinned position children,固定位置的 孩子 常量 解释说明 int COLLAPSE_MODE_OFF The view will act as normal with no collapsing behavior.(这个 View将会 呈现正常的结果,不会表现出折叠效果) int COLLAPSE_MODE_PARALLAX The view will scroll in a parallax fashion. See setParallaxMultiplier(float) to change the multiplier used.(在滑动的时候这个View 会呈现 出 视觉特差效果 ) int COLLAPSE_MODE_PIN The view will pin in place until it reaches the bottom of the CollapsingToolbarLayout.(当这个View到达 CollapsingToolbarLayout的底部的时候,这个View 将会被放置,即代替整个CollapsingToolbarLayout) 我们有两种方法可以设置这个常量, 方法一:在代码中使用这个方法 setCollapseMode(int collapseMode) 方法 二:在布局文件中使用自定义属性 app:layout_collapseMode="pin" 结合ViewPager的视觉特差 布局代码: <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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" android:background="@android:color/background_light" android:fitsSystemWindows="true" > <android.support.design.widget.AppBarLayout android:id="@+id/main.appbar" android:layout_width="match_parent" android:layout_height="300dp" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" > <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/main.collapsing" android:layout_width="match_parent" android:layout_height="250dp" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:expandedTitleMarginEnd="64dp" app:expandedTitleMarginStart="48dp" app:layout_scrollFlags="scroll|exitUntilCollapsed" > <ImageView android:id="@+id/main.backdrop" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:scaleType="centerCrop" android:src="@drawable/tangyan" app:layout_collapseMode="parallax" /> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CollapsingToolbarLayout> <android.support.design.widget.TabLayout android:id="@+id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="?attr/colorPrimary" app:tabIndicatorColor="@color/colorAccent" app:tabIndicatorHeight="4dp" app:tabSelectedTextColor="#000" app:tabTextColor="#fff"/> </android.support.design.widget.AppBarLayout> <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> </android.support.v4.view.ViewPager> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:layout_margin="15dp" android:src="@drawable/add_2"/> </android.support.design.widget.CoordinatorLayout> 效果图如下: 思路解析: 结构图如图片所示,先说明CollapsingToolbarLayout的变化 CollapsingToolbarLayout里面 包含ImageView 和ToolBar,ImageView的app:layout_collapseMode=”parallax”,表示视差效果,ToolBar的 app:layout_collapseMode=”pin”,当这个TooBar到达 CollapsingToolbarLayout的底部的时候,会代替整个CollapsingToolbarLayout显示 接着说明TabLayout的变化 从前面的描述我们已经知道当 没有指定app:layout_scrollFlags的时候,最终TabLayout会静止,不会随着滑动的 时候消失不见 这篇博客主要讲解了CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout的一些相关属性。 对于AppBarLayout,我们主要 讲解了这个属性app:layout_scrollFlags,设置不同 的属性我们可以在滚动的时候显示不同 的效果 对于CollapsingToolbarLayout,我们主要讲解了app:layout_collapseMode这个属性,设置不同的值,我们可以让其子View呈现不同的 炫酷效果,如parallax和pin等推荐阅读:2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中) **[2019年鸿洋大神最新整理一线互联网公司Android中高级面试题总结(附答案解析)](https://www.jianshu.com/p/100e8044ce90)** 原文作者:只是一条程序狗原文链接:https://blog.csdn.net/jxf_access/article/details/79564669
前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。 众所周知,优秀源码的阅读与理解是最能提升自身功力的途径,如果想要成为一名优秀的Android工程师,那么Android中优秀三方库源码的分析和理解则是必备技能。就拿比较热门的图片加载框架Glide来说,相信很多同学都使用过,那么,当别人问你下面这些问题时你是否能回答出来呢?(Glide五连发) 1、为什么要在项目中使用这个库? 2、这个库都有哪些用法?对应什么样的使用场景? 3、这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现? 4、Glide源码机制的核心思想是什么? 5、Glide中是如何计算一张图片的大小的? 相信能全部回答出来的同学并不多,下面我来解答一下上面几个问题。 1、为什么要在项目中使用这个库? 1、多样化媒体加载:不仅可以进行图片缓存,还支持Gif、WebP、缩略图,甚至是Video。 2、通过设置绑定生命周期:可以使加载图片的生命周期动态管理起来。 3、高效的缓存策略:支持内存、Disk缓存,并且Picasso只会缓存原始尺寸的图片,而Glide缓存的是多种规格,也就是Glide会根据你ImageView的大小来缓存相应大小的图片尺寸。 4、内存开销小:默认的Bitmap格式是RGB_565格式,而Picasso默认的是ARGB_8888格式,内存开销小一半。 2、这个库都有哪些用法?对应什么样的使用场景? 1、图片加载:Glide.with(this).load(imageUrl).override(800, 800).placeholder().error().animate().into()。 2、多样式媒体加载:asBitamp、asGif。 3、生命周期集成。 4、可以配置磁盘缓存策略ALL、NONE、SOURCE、RESULT。 3、这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现? 要想了解Glide的核心实现原理,就必须先从它的加载API Glide.with().into()来进行分析。 1、Glide&with: 1、初始化各式各样的配置信息(包括缓存,请求线程池,大小,图片格式等等)以及glide对象。 2、将glide请求和application/SupportFragment/Fragment的生命周期绑定在一块。 2、Glide&load: 设置请求url,并记录url已设置的状态。 3、Glide&into: 1、首先根据转码类transcodeClass类型返回不同的ImageViewTarget:BitmapImageViewTarget、DrawableImageViewTarget。 2、递归建立缩略图请求,没有缩略图请求,则直接进行正常请求。 3、如果没指定宽高,会根据ImageView的宽高计算出图片宽高,最终执行到onSizeReay()方法中的engine.load()方法。 4、engine是一个负责加载和管理缓存资源的类 其中Glide的三层缓存机制是值得我们去反复学习揣摩的,这里我们先了解下常规的三级缓存是怎样的。 常规三级缓存的流程:强引用->软引用->硬盘缓存 当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找(软引用适合当cache,当内存吃紧的时候才会被回收。而weakReference在每次system.gc()就会被回收)(当LruCache存储紧张时,会把最近最少使用的数据放到SoftReference中),如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去硬盘缓存中中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到硬盘缓存中,然后放到LruCache中。 Glide的三层缓存机制 Glide缓存机制大致分为三层:内存缓存、弱引用缓存、磁盘缓存。 取的顺序是:内存、弱引用、磁盘。 存的顺序是:弱引用、内存、磁盘。 三层存储的机制在Engine中实现的。先说下Engine是什么?Engine这一层负责加载时做管理内存缓存的逻辑。持有MemoryCache、Map>>。通过load()来加载图片,加载前后会做内存存储的逻辑。如果内存缓存中没有,那么才会使用EngineJob这一层来进行异步获取硬盘资源或网络资源。EngineJob类似一个异步线程或observable。Engine是一个全局唯一的,通过Glide.getEngine()来获取。 需要一个图片资源,如果Lrucache中有相应的资源图片,那么就返回,同时从Lrucache中清除,放到activeResources中。activeResources map是盛放正在使用的资源,以弱引用的形式存在。同时资源内部有被引用的记录。如果资源没有引用记录了,那么再放回Lrucache中,同时从activeResources中清除。如果Lrucache中没有,就从activeResources中找,找到后相应资源引用加1。如果Lrucache和activeResources中没有,那么进行资源异步请求(网络/diskLrucache),请求成功后,资源放到diskLrucache和activeResources中。 4、Glide源码机制的核心思想: 使用一个弱引用map activeResources来盛放项目中正在使用的资源。Lrucache中不含有正在使用的资源。资源内部有个计数器来显示自己是不是还有被引用的情况,把正在使用的资源和没有被使用的资源分开有什么好处呢??因为当Lrucache需要移除一个缓存时,会调用resource.recycle()方法。注意到该方法上面注释写着只有没有任何consumer引用该资源的时候才可以调用这个方法。那么为什么调用resource.recycle()方法需要保证该资源没有任何consumer引用呢?glide中resource定义的recycle()要做的事情是把这个不用的资源(假设是bitmap或drawable)放到bitmapPool中。bitmapPool是一个bitmap回收再利用的库,在做transform的时候会从这个bitmapPool中拿一个bitmap进行再利用。这样就避免了重新创建bitmap,减少了内存的开支。而既然bitmapPool中的bitmap会被重复利用,那么肯定要保证回收该资源的时候(即调用资源的recycle()时),要保证该资源真的没有外界引用了。这也是为什么glide花费那么多逻辑来保证Lrucache中的资源没有外界引用的原因。 5、Glide中是如何计算一张图片的大小的? 图片占用内存的计算公式:图片高度 图片宽度 一个像素占用的内存大小。所以,计算图片占用内存大小的时候,要考虑图片所在的目录跟设备密度,这两个因素其实影响的是图片的宽高,android会对图片进行拉升跟压缩。 上面笔者只是简单地讲解一下下Glide的内部实现机制,但是这是远远不够的,如果想要对Glide或其它热门三方库有足够具象地了解,就必须深入源码去感受其中的艺术。 助力一份Android热门三方库源码面试宝典 因此,为了将热门三方库涉及的知识成体系地融合起来,笔者创建了Awesome-Third-Library-Source-Analysis这个项目,为的就是让每一个Android工程师能够从以下七个方面全方位地提升自己的技术实力。 项目地址:Awesome-Third-Library-Source-Analysis 深入理解热门三方库实现原理,从七个角度全方位提升你的功力~ Contents 网络 OkHttp (已完成) Android最优秀的网络底层框架,没有之一。 Retrofit (已完成) Android最优秀的网络封装框架,内含九种常用设计模式的灵活运用。 图片 Glide (已完成) Android使用最广泛的图片加载框架。 数据库 GreenDao (已完成) Android中数据库操作综合效率最高的框架。 响应式编程 RxJava (已完成) 来一起探究RxJava的异步、简洁、优雅和它强大的操作符吧! 内存泄露 LeakCanary (已完成) LeakCanary究竟是如何检测出内存泄露的呢? 依赖注入 ButterKnife(已完成) 使用APT + 注解攻破了findViewByid(),JW大神之作。 Dagger2(已完成) Dagger就一把匕首,在中大型项目中,它能提升开发效率、自动管理类的实例、解耦,是如此的干脆。 事件总线 EventBus(已完成) 使用扩展的观察者模式实现的组件间通信框架,广播的替代者。 推荐阅读:2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中) 作者:jsonchao链接:https://juejin.im/post/5e65ad276fb9a07cc01a3264来源:掘金
Android 10 引入了对外部存储权限的更改,旨在更好地保护用户数据以及降低应用的存储空间。Android 11 开发者预览版里加入了更多改进,以帮助开发者更好地适应这些权限修改。 在 Google Play 上发布的大部分应用都会请求 (READ_EXTERNAL_STORAGE) 存储权限,来做一些诸如在 SD 卡中存储文件或者读取多媒体文件等常规操作。这些应用可能会在磁盘中存储大量文件,即使应用被卸载了还会依然存在。另外,这些应用还可能会读取其他应用的一些敏感文件数据。在 Android 10 中,我们调整了存储权限的工作方式,仅为应用提供其所需的访问权限。这也是在鼓励应用在指定目录下进行文件存储以限制文件混乱。当应用被卸载后,这些相关的目录也会被删除。 Android 10 所带来的关于存储上的变更遵循了以下三个基本原则 更好的从属性: 系统知道哪些文件属于哪些应用,这可以让用户更方便地管理他们的文件。当应用被卸载后,除非用户需要,否则应用之前所创建的文件也不应该保留在设备上; 保护应用数据: 当一个应用将它所属的文件写入外部存储时,这些文件是不应该被其他应用所访问的; 保护用户数据: 当用户下载了一些文件,比如带有敏感信息的邮件附件,这些文件应该对其他应用不可见。 目标 API 级别 (Target SDK Level) 设定为 Android 10 的应用无需请求 Storage 权限,就可以使用自己的外部存储目录并管理媒体集合 (音频、视频、图片和下载数据)。Storage 权限仅允许读取其他应用共享的音频、视频和图片集合,但并不允许访问非本应用创建的下载数据。在 Android 10 里唯一一种访问其他应用创建的非媒体文件的途径是使用存储访问框架 (Storage Access Framework) 提供的文档选择器。 在 Android 11 中,我们会通过下述的几点来继续优化分区存储 (Scoped Storage) 的开发者体验。 Android 10 对外部存储权限行为https://developer.android.google.cn/training/data-storage#scoped-storage 请求 (READ_EXTERNAL_STORAGE) 存储权限https://developer.android.google.cn/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE 它所属的文件https://developer.android.google.cn/training/data-storage/app-specific 存储访问框架 (Storage Access Framework)https://developer.android.google.cn/guide/topics/providers/document-provider 改进媒体存储 Android 10 中要求所有应用都使用 MediaStore API 来访问照片、视频和音乐文件,我们也将继续秉承这个原则。但是我们也知道,很多深度依赖基于原始文件路径 API 的应用和第三方库是很难切换到使用文件描述符 (File Descriptor) 的。因此在 Android 11 里,依赖原始文件路径的 API 和库可以再次使用了。您需要在应用的 Manifest 文件里添加 requestLegacyExternalStorage 属性,以保证 Android 10 的用户也可以使用该特性。在实际的运行中,依赖原始文件路径的 I/O 请求会被重定向到使用 MediaStore API,当使用这种方式访问本应用存储空间之外的文件时,这次重定向会造成性能影响。而且直接使用原始文件路径,并不会比使用 MediaStore API 有更多优势,因此我们强烈建议直接使用 MediaStore API。在 Android 10 中,应用在对每一个文件请求编辑或删除时都必须得到用户的确认。而在 Android 11 中,应用可以一次请求修改或者删除多个媒体文件。系统的默认图库应用 (Gallery) 将不再展示这些对话框。我们希望这项改进能够使用户体验更加顺畅。 可以再次使用https://developer.android.google.cn/preview/privacy/storage#media-files-raw-paths 修改或者删除多个媒体文件https://developer.android.com/preview/privacy/storage#media-file-access 对 Storage Access Framework 的更新 当我们对广泛的存储访问进行限制后,一些开发者试图使用 Storage Access Framework (SAF) 遍历整个文件系统。但是,SAF 并不适用于广泛地访问共享存储内容。因此,我们对其进行了更新,限制了它对某些路径的可见性。 在 Android 11 中,将不再允许用户授权访问 Downloads 的根目录、每个可用 SD 卡的根目录以及其它应用的目录。应用仍然可以通过 Storage Access Framework API 或者文件选择器来帮助用户从共享存储中选取个别文件。 对其进行了更新 https://developer.android.google.cn/preview/privacy/storage#file-directory-restrictions 针对文件管理应用的特殊权限 针对文件管理器以及一些备份类的应用,它们需要获得共享存储的更广泛的访问权限。Android 11 里将会引入一个特别的权限叫做 MANAGE_EXTERNAL_STORAGE,该权限将授权读写所有共享存储内容,这也将同时包含非媒体类型的文件。但是获得这个权限的应用还是无法访问其他应用的应用专属目录 (app-specific directory),无论是外部存储还是内部存储。 我们希望继续允许一些确实有广泛访问外部存储文件需求的应用。在 Android 11 中,已获得MANAGE_EXTERNAL_STORAGE 权限的应用,可以将用户引导至系统设置页面,让用户选择是否允许该应用 "访问所有文件" (All Files Access)。下面的两种应用示例是可以使用该权限的: 文件管理器 —— 该类应用的主要功能是管理文件; 备份和恢复 —— 该类应用需要访问大批量的文件 (比如切换设备的时候进行数据迁移,或者将数据备份到云端)。 如果您的应用需要访问单个文件,比如文字处理应用,则应该使用 Storage Access Framework (SAF)。 如果您的应用需要 MANAGE_EXTERNAL_STORAGE 权限或者调用了依赖原始文件路径的 API,那么您必须在 AndroidManifest 文件中添加 requestLegacyExternalStorage=true,这样您的应用才能够在搭载 Android 10 的设备上正常运行。更多相关信息请查看我们在去年 Android 开发者峰会的分享视频《准备好使用分区存储》: 腾讯视频链接 https://v.qq.com/x/page/d3026c1bpr3.html Bilibili 视频链接 https://www.bilibili.com/video/av77198618/
前言 由于疫情关系,最近在各大网络技术交流平台看到很多同学的工作情况内心还是蛮触动的(降薪、变相裁员、辞退等)。可能这并不是当下一个普遍的现象,但仍然使我感受到Android开发这碗青春饭不好混。于此同时联系我内推的同学很多都处于待业状态,能感受到他们内心的迷茫和焦灼。于是内心一直有声音督促我,赶紧写点面试相关的东西出来吧,哪怕对大家只有一丝丝的帮助。当然这次我会以面试官的角度出发(可能不是一个优秀的面试官),让大家更加了解字节跳动的面试注意事项、重点面试题解析等。接下来我会从以下两个模块来讲解: 面试注意点 面试题解析 面试注意点 01 代码能力 是研发面试考察的核心! 其实说句老实话,研发面试所考察的最核心的能力就是......代码能力! 代码能力是计算机专业的基础。能否在有限时间内写出清晰简洁、逻辑清晰的代码,不仅可以考察出候选人是否有扎实的基本功,也可以让面试官对候选人在未来是否能够胜任相应的岗位工作有一个基础判断。 面试两场之后我发现,有的候选人刚开始聊项目聊得非常开心,一些细节问题回答得都挺不错的,可一旦到了手写代码这一关,连二分查找都写不出来的大有人在。 平时写的代码多不多,面试之前有没有做过准备,有经验的面试官一眼就可以判断出来。所以这里给大家分享三点建议: 1、加强基本功,增加代码量 多看优秀的源代码,认真从效率、逻辑等方面分析他人如何简洁明晰地实现一个函数,这对提升自己的基本代码能力有很大的帮助。 2、面试前多做题,保持手感很重要 面试之前多做些练习,这不仅可以加快答题速度,也会让自己养成较为规范的答题习惯。 3、复盘笔试答案,思考更优解 最后,不少面试官喜欢在面试的时候复盘笔试题目,与候选人讨论题目的更优解,从而考察考生是否具备主动思考能力。 02 我们不反对刷题 不过更希望大家举一反三 在面试过程中,经常会有同学非常骄傲地坦白道:“我没有刷题。” 而在这里我想代表广大面试官表个态:我们不反对刷题,甚至希望同学们在大量做题之后,能够灵活运用、举一反三。 大量做题不仅可以在短时间内提高同学们的解题速度,也会在一定程度上帮助你拓展自己的答题思路。 当然,如果发现同学刷题,面试官也会变种问题,从侧面考察同学是否死记硬背答案。作为春招的面试官、你未来的潜在同事,我们更希望同学们能够灵活贯通。 所以说,该准备准备,该刷题刷题,临时抱佛脚至少体现了你对面试重视是不是?等你题目刷到一定程度,你就会发现自己的能力有了一个量变到质变的提升。 03 项目描述切忌花哨 突出项目重点,表述逻辑要清晰 参与面试的同学们大多都有实习经历。在公司的大环境下,你会在实践中不断夯实代码基础,也会有更多机会接触到最新的技术。 我们希望大家对过往的实习经历做更深入的思考,不是简简单单描述你在哪里做了什么。毕竟面试官需要在短时间内看到你的个人能力。发挥主观能动性,多在几种不同方法之间做比较,给出在思考之后得出的最优解,会显得你格外与众不同。 除此之外,思考的逻辑性和表达能力也是面试时考察的重点。在面试时切记要简化答案,能表达清楚想法就好,项目描述时只需要简单介绍项目背景,并着重突出项目亮点就可以啦!千万不要过度包装,要知道坦诚清晰可是「字节范儿」中很重要的一点哦~ 04 跨专业面试不会受到区别对待 技术实力最重要 面试官们常常会被问到:非计算机专业的同学跨专业面试会不会受到区别对待? 在这里我想强调:完全不会! 不同岗位对具体的专业能力要求不同,拿算法岗位举例,只要你代码基础功底过硬,数理能力够强,又对所面试岗位是发自内心的热爱,就算你是学挖掘机技术的,面试官们也是想给你发offer的。 算法行业的通用能力就是代码硬实力。如果不具备这种能力,在实际工作中就总会感觉不自信,受人制肘。所以非科班出身的同学们不用受自己的专业所限,但一定要多多锻炼,努力提升自己。 不过这里也想真心地提醒大家几句,千万不要盲从跟风选择自己的职业方向,每个领域都有自己广阔的发展空间,适合自己的才是最好的。越热门的岗位对基础的要求越高,面试官在面试的时候是看得到笔试成绩的,所以还希望大家在面试过程中诚实一点,不懂装懂可是会减分的哦! 05 哪些行为一定是减分项? 除了上述几点,我还想多嘱咐几句: 虽然面试考察最多的是技术基础和代码能力,但是从心理学的角度分析,第一印象也是相当重要的啊喂! 为了这场面试,逗比的我收敛了表情包,穿上了帅气的格子衫,还把头发梳成了面试官的模样,你说你穿着拖鞋、脸都不洗就来面试是不是有点伤我心了。 如果同学们选择的是视频面试,希望大家在面试前做好充分的准备工作。面试开始之后,如果遇到候选人迟到、没有调试设备、网络不畅通、麦克风关掉、面试环境嘈杂等情况,面试官会给一定时间调试,但是面试体验会受到一定影响,心理上可能会减分的哦! 诚实守信是做人第一要务,也是字节跳动用人的底线。你们要知道,即使是视频面试,面试官也是可以看到考生行为的!答应我,像类似「一边考试一边用ipad查答案」这种事情千万不要干好嘛!作为考官的我在摄像头另一边看到了也是很尴尬的...... 面试题解析 1、网络 网络协议模型 应用层:负责处理特定的应用程序细节HTTP、FTP、DNS 传输层:为两台主机提供端到端的基础通信TCP、UDP 网络层:控制分组传输、路由选择等IP 链路层:操作系统设备驱动程序、网卡相关接口 TCP 和 UDP 区别 TCP 连接;可靠;有序;面向字节流;速度慢;较重量;全双工;适用于文件传输、浏览器等 全双工:A 给 B 发消息的同时,B 也能给 A 发 半双工:A 给 B 发消息的同时,B 不能给 A 发 UDP 无连接;不可靠;无序;面向报文;速度快;轻量;适用于即时通讯、视频通话等 TCP 三次握手 A:你能听到吗?B:我能听到,你能听到吗?A:我能听到,开始吧 A 和 B 两方都要能确保:我说的话,你能听到;你说的话,我能听到。所以需要三次握手 TCP 四次挥手 A:我说完了B:我知道了,等一下,我可能还没说完B:我也说完了A:我知道了,结束吧 B 收到 A 结束的消息后 B 可能还没说完,没法立即回复结束标示,只能等说完后再告诉 A :我说完了。 POST 和 GET 区别 Get 参数放在 url 中;Post 参数放在 request Body 中Get 可能不安全,因为参数放在 url 中 HTTPS HTTP 是超文本传输协议,明文传输;HTTPS 使用 SSL 协议对 HTTP 传输数据进行了加密 HTTP 默认 80 端口;HTTPS 默认 443 端口 优点:安全缺点:费时、SSL 证书收费,加密能力还是有限的,但是比 HTTP 强多了 2、Java 基础&容器&同步&设计模式 StringBuilder、StringBuffer、+、String.concat 链接字符串: StringBuffer 线程安全,StringBuilder 线程不安全 +实际上是用 StringBuilder 来实现的,所以非循环体可以直接用 +,循环体不行,因为会频繁创建 StringBuilder String.concat 实质是 new String ,效率也低,耗时排序:StringBuilder < StringBuffer < concat < + Java 泛型擦除 修饰成员变量等类结构相关的泛型不会被擦除 容器类泛型会被擦除 ArrayList、LinkedList ArrayList 基于数组实现,查找快:o(1),增删慢:o(n)初始容量为10,扩容通过 System.arrayCopy 方法 LinkedList 基于双向链表实现,查找慢:o(n),增删快:o(1)封装了队列和栈的调用 HashMap 、HashTable HashMap 基于数组和链表实现,数组是 HashMap 的主体;链表是为解决哈希冲突而存在的 当发生哈希冲突且链表 size 大于阈值时会扩容,JAVA 8 会将链表转为红黑树提高性能 允许 key/value 为 null HashTable 数据结构和 HashMap 一样 不允许 value 为 null 线程安全 ArrayMap、SparseArray ArrayMap 1.基于两个数组实现,一个存放 hash;一个存放键值对。扩容的时候只需要数组拷贝,不需要重建哈希表2.内存利用率高3.不适合存大量数据,因为会对 key 进行二分法查找(1000以下) SparseArray 1.基于两个数组实现,int 做 key2.内存利用率高3.不适合存大量数据,因为会对 key 进行二分法查找(1000以下) volatile 关键字 只能用来修饰变量,适用修饰可能被多线程同时访问的变量 相当于轻量级的 synchronized,volatitle 能保证有序性(禁用指令重排序)、可见性;后者还能保证原子性 变量位于主内存中,每个线程还有自己的工作内存,变量在自己线程的工作内存中有份拷贝,线程直接操作的是这个拷贝 被 volatile 修饰的变量改变后会立即同步到主内存,保持变量的可见性。 双重检查单例,为什么要加 volatile? 1.volatile想要解决的问题是,在另一个线程中想要使用instance,发现instance!=null,但是实际上instance还未初始化完毕这个问题 2.将instance =newInstance();拆分为3句话是。1.分配内存2.初始化3.将instance指向分配的内存空 3.volatile可以禁止指令重排序,确保先执行2,后执行3 wait 和 sleep sleep 是 Thread 的静态方法,可以在任何地方调用 wait 是 Object 的成员方法,只能在 synchronized 代码块中调用,否则会报 IllegalMonitorStateException 非法监控状态异常 sleep 不会释放共享资源锁,wait 会释放共享资源锁 lock 和 synchronized synchronized 是 Java 关键字,内置特性;Lock 是一个接口 synchronized 会自动释放锁;lock 需要手动释放,所以需要写到 try catch 块中并在 finally 中释放锁 synchronized 无法中断等待锁;lock 可以中断 Lock 可以提高多个线程进行读/写操作的效率 竞争资源激烈时,lock 的性能会明显的优于 synchronized 可重入锁 定义:已经获取到锁后,再次调用同步代码块/尝试获取锁时不必重新去申请锁,可以直接执行相关代码 ReentrantLock 和 synchronized 都是可重入锁 公平锁 定义:等待时间最久的线程会优先获得锁 非公平锁无法保证哪个线程获取到锁,synchronized 就是非公平锁 ReentrantLock 默认时非公平锁,可以设置为公平锁 乐观锁和悲观锁 悲观锁:线程一旦得到锁,其他线程就挂起等待,适用于写入操作频繁的场景;synchronized 就是悲观锁 乐观锁:假设没有冲突,不加锁,更新数据时判断该数据是否过期,过期的话则不进行数据更新,适用于读取操作频繁的场景 乐观锁 CAS:Compare And Swap,更新数据时先比较原值是否相等,不相等则表示数据过去,不进行数据更新 乐观锁实现:AtomicInteger、AtomicLong、AtomicBoolean 死锁 4 个必要条件 互斥 占有且等待 不可抢占 循环等待 synchronized 原理 每个对象都有一个监视器锁:monitor,同步代码块会执行 monitorenter 开始,motnitorexit 结束 wait/notify 就依赖 monitor 监视器,所以在非同步代码块中执行会报 IllegalMonitorStateException 异常 3、Java 虚拟机&内存结构&GC&类加载&四种引用&动态代理 JVM 定义:可以理解成一个虚构的计算机,解释自己的字节码指令集映射到本地 CPU 或 OS 的指令集,上层只需关注 Class 文件,与操作系统无关,实现跨平台 Kotlin 就是能解释成 Class 文件,所以可以跑在 JVM 上 JVM 内存模型 Java 多线程之间是通过共享内存来通信的,每个线程都有自己的本地内存 共享变量存放于主内存中,线程会拷贝一份共享变量到本地内存 volatile 关键字就是给内存模型服务的,用来保证内存可见性和顺序性 JVM 内存结构 线程私有: 1.程序计数器:记录正在执行的字节码指令地址,若正在执行 Native 方法则为空2.虚拟机栈:执行方法时把方法所需数据存为一个栈帧入栈,执行完后出栈3.本地方法栈:同虚拟机栈,但是针对的是 Native 方法 线程共享: 1.堆:存储 Java 实例,GC 主要区域,分代收集 GC 方法会吧堆划分为新生代、老年代2.方法区:存储类信息,常量池,静态变量等数据 GC 回收区域:只针对堆、方法区;线程私有区域数据会随线程结束销毁,不用回收 回收类型: 1.堆中的对象 分代收集 GC 方法会吧堆划分为新生代、老年代 新生代:新建小对象会进入新生代;通过复制算法回收对象 老年代:新建大对象及老对象会进入老年代;通过标记-清除算法回收对象 2.方法区中的类信息、常量池 判断一个对象是否可被回收: 1.引用计数法缺点:循环引用 2.可达性分析法定义:从 GC ROOT 开始搜索,不可达的对象都是可以被回收的 GC ROOT 1.虚拟机栈/本地方法栈中引用的对象2.方法区中常量/静态变量引用的对象 四种引用 强引用:不会被回收 软引用:内存不足时会被回收 弱引用:gc 时会被回收 虚引用:无法通过虚引用得到对象,可以监听对象的回收 ClassLoader 类的生命周期: 1.加载;2.验证;3.准备;4.解析;5.初始化;6.使用;7.卸载 类加载过程: 1.加载:获取类的二进制字节流;生成方法区的运行时存储结构;在内存中生成 Class 对象2.验证:确保该 Class 字节流符合虚拟机要求3.准备:初始化静态变量4.解析:将常量池的符号引用替换为直接引用5.初始化:执行静态块代码、类变量赋值 类加载时机: 1.实例化对象2.调用类的静态方法3.调用类的静态变量(放入常量池的常量除外) 类加载器:负责加载 class 文件 分类: 1.引导类加载器 - 没有父类加载器2.拓展类加载器 - 继承自引导类加载器3.系统类加载器 - 继承自拓展类加载器 双亲委托模型: 当要加载一个 class 时,会先逐层向上让父加载器先加载,加载失败才会自己加载 为什么叫双亲?不考虑自定义加载器,系统类加载器需要网上询问两层,所以叫双亲 判断是否是同一个类时,除了类信息,还必须时同一个类加载器 优点: 防止重复加载,父加载器加载过了就没必要加载了 安全,防止篡改核心库类 动态代理原理及实现 InvocationHandler 接口,动态代理类需要实现这个接口 Proxy.newProxyInstance,用于动态创建代理对象 Retrofit 应用: Retrofit 通过动态代理,为我们定义的请求接口都生成一个动态代理对象,实现请求 4、Android 基础&性能优化&Framework Activity 启动模式 standard 标准模式 singleTop 栈顶复用模式, 推送点击消息界面 singleTask 栈内复用模式, 首页 singleInstance 单例模式,单独位于一个任务栈中 拨打电话界面 细节: taskAffinity:任务相关性,用于指定任务栈名称,默认为应用包名 allowTaskReparenting:允许转移任务栈 View 工作原理 DecorView (FrameLayout) LinearLayout titlebar Content 调用 setContentView 设置的 View ViewRoot 的 performTraversals 方法调用触发开始 View 的绘制,然后会依次调用: performMeasure:遍历 View 的 measure 测量尺寸 performLayout:遍历 View 的 layout 确定位置 performDraw:遍历 View 的 draw 绘制 事件分发机制 一个 MotionEvent 产生后,按 Activity -> Window -> decorView -> View 顺序传递,View 传递过程就是事件分发,主要依赖三个方法: dispatchTouchEvent:用于分发事件,只要接受到点击事件就会被调用,返回结果表示是否消耗了当前事件 onInterceptTouchEvent:用于判断是否拦截事件,当 ViewGroup 确定要拦截事件后,该事件序列都不会再触发调用此 ViewGroup 的 onIntercept onTouchEvent:用于处理事件,返回结果表示是否处理了当前事件,未处理则传递给父容器处理 细节: 一个事件序列只能被一个 View 拦截且消耗 View 没有 onIntercept 方法,直接调用 onTouchEvent 处理 OnTouchListener 优先级比 OnTouchEvent 高,onClickListener 优先级最低 requestDisallowInterceptTouchEvent 可以屏蔽父容器 onIntercet 方法的调用 Window 、 WindowManager、WMS、SurfaceFlinger Window:抽象概念不是实际存在的,而是以 View 的形式存在,通过 PhoneWindow 实现 WindowManager:外界访问 Window 的入口,内部与 WMS 交互是个 IPC 过程 WMS:管理窗口 Surface 的布局和次序,作为系统级服务单独运行在一个进程 SurfaceFlinger:将 WMS 维护的窗口按一定次序混合后显示到屏幕上 View 动画、帧动画及属性动画 View 动画: 作用对象是 View,可用 xml 定义,建议 xml 实现比较易读 支持四种效果:平移、缩放、旋转、透明度 帧动画: 通过 AnimationDrawable 实现,容易 OOM 属性动画: 可作用于任何对象,可用 xml 定义,Android 3 引入,建议代码实现比较灵活 包括 ObjectAnimator、ValuetAnimator、AnimatorSet 时间插值器:根据时间流逝的百分比计算当前属性改变的百分比 系统预置匀速、加速、减速等插值器 类型估值器:根据当前属性改变的百分比计算改变后的属性值 系统预置整型、浮点、色值等类型估值器 使用注意事项: 避免使用帧动画,容易OOM 界面销毁时停止动画,避免内存泄漏 开启硬件加速,提高动画流畅性 ,硬件加速: 将 cpu 一部分工作分担给 gpu ,使用 gpu 完成绘制工作 从工作分摊和绘制机制两个方面优化了绘制速度 Handler、MessageQueue、Looper Handler:开发直接接触的类,内部持有 MessageQueue 和 Looper MessageQueue:消息队列,内部通过单链表存储消息 Looper:内部持有 MessageQueue,循环查看是否有新消息,有就处理,没就阻塞 如何实现阻塞:通过 nativePollOnce 方法,基于 Linux epoll 事件管理机制 为什么主线程不会因为 Looper 阻塞:系统每 16ms 会发送一个刷新 UI 消息唤醒 MVC、MVP、MVVM MVP:Model:处理数据;View:控制视图;Presenter:分离 Activity 和 Model MVVM:Model:处理获取保存数据;View:控制视图;ViewModel:数据容器 使用 Jetpack 组件架构的 LiveData、ViewModel 便捷实现 MVVM Serializable、Parcelable Serializable :Java 序列化方式,适用于存储和网络传输,serialVersionUID 用于确定反序列化和类版本是否一致,不一致时反序列化回失败 Parcelable :Android 序列化方式,适用于组件通信数据传递,性能高,因为不像 Serializable 一样有大量反射操作,频繁 GC Binder Android 进程间通信的中流砥柱,基于客户端-服务端通信方式 使用 mmap 一次数据拷贝实现 IPC,传统 IPC:用户A空间->内核->用户B空间;mmap 将内核与用户B空间映射,实现直接从用户A空间->用户B空间 BinderPool 可避免创建多 Service IPC 方式 Intent extras、Bundle:要求传递数据能被序列化,实现 Parcelable、Serializable ,适用于四大组件通信 文件共享:适用于交换简单的数据实时性不高的场景 AIDL:AIDL 接口实质上是系统提供给我们可以方便实现 BInder 的工具 Android Interface Definition Language,可实现跨进程调用方法 服务端:将暴漏给客户端的接口声明在 AIDL 文件中,创建 Service 实现 AIDL 接口并监听客户端连接请求 客户端:绑定服务端 Service ,绑定成功后拿到服务端 Binder 对象转为 AIDL 接口调用 RemoteCallbackList 实现跨进程接口监听,同个 Binder 对象做 key 存储客户端注册的 listener 监听 Binder 断开:1.Binder.linkToDeath 设置死亡代理;2. onServiceDisconnected 回调 Messenger:基于 AIDL 实现,服务端串行处理,主要用于传递消息,适用于低并发一对多通信 ContentProvider:基于 Binder 实现,适用于一对多进程间数据共享 Socket:TCP、UDP,适用于网络数据交换 Android 系统启动流程 按电源键 -> 加载引导程序 BootLoader 到 RAM -> 执行 BootLoader 程序启动内核 -> 启动 init 进程 -> 启动 Zygote 和各种守护进程 -> 启动 System Server 服务进程开启 AMS、WMS 等 -> 启动 Launcher 应用进程 App 启动流程 Launcher 中点击一个应用图标 -> 通过 AMS 查找应用进程,若不存在就通过 Zygote 进程 fork 进程保活 进程优先级:1.前台进程 ;2.可见进程;3.服务进程;4.后台进程;5.空进程 进程被 kill 场景:1.切到后台内存不足时被杀;2.切到后台厂商省电机制杀死;3.用户主动清理 保活方式: 1.Activity 提权:挂一个 1像素 Activity 将进程优先级提高到前台进程 2.Service 提权:启动一个前台服务(API>18会有正在运行通知栏) 3.广播拉活 4.Service 拉活 5.JobScheduler 定时任务拉活 6.双进程拉活 网络优化及检测 速度:1.GZIP 压缩(okhttp 自动支持);2.Protocol Buffer 替代 json;3.优化图片/文件流量;4.IP 直连省去 DNS 解析时间 成功率:1.失败重试策略; 流量:1.GZIP 压缩(okhttp 自动支持);2.Protocol Buffer 替代 json;3.优化图片/文件流量;5.文件下载断点续传 ;6.缓存 协议层的优化,比如更优的 http 版本等 监控:Charles 抓包、Network Monitor 监控流量 UI卡顿优化 减少布局层级及控件复杂度,避免过度绘制 使用 include、merge、viewstub 优化绘制过程,避免在 Draw 中频繁创建对象、做耗时操作 内存泄漏场景及规避 1.静态变量、单例强引跟生命周期相关的数据或资源,包括 EventBus2.游标、IO 流等资源忘记主动释放3.界面相关动画在界面销毁时及时暂停4.内部类持有外部类引用导致的内存泄漏 handler 内部类内存泄漏规避:1.使用静态内部类+弱引用 2.界面销毁时清空消息队列 检测:Android Studio Profiler LeakCanary 原理 通过弱引用和引用队列监控对象是否被回收 比如 Activity 销毁时开始监控此对象,检测到未被回收则主动 gc ,然后继续监控 OOM 场景及规避 加载大图:减小图片 内存泄漏:规避内存泄漏 5、Android 模块化&热修复&热更新&打包&混淆&压缩 Dalvik 和 ART Dalvik 谷歌设计专用于 Android 平台的 Java 虚拟机,可直接运行 .dex 文件,适合内存和处理速度有限的系统 JVM 指令集是基于栈的;Dalvik 指令集是基于寄存器的,代码执行效率更优 ART Dalvik 每次运行都要将字节码转换成机器码;ART 在应用安装时就会转换成机器码,执行速度更快 ART 存储机器码占用空间更大,空间换时间 APK 打包流程 1.aapt 打包资源文件生成 R.java 文件;aidl 生成 java 文件2.将 java 文件编译为 class 文件3.将工程及第三方的 class 文件转换成 dex 文件4.将 dex 文件、so、编译过的资源、原始资源等打包成 apk 文件5.签名6.资源文件对齐,减少运行时内存 App 安装过程 首先要解压 APK,资源、so等放到应用目录 Dalvik 会将 dex 处理成 ODEX ;ART 会将 dex 处理成 OAT; OAT 包含 dex 和安装时编译的机器码 组件化路由实现 ARoute:通过 APT 解析 @Route 等注解,结合 JavaPoet 生成路由表,即路由与 Activity 的映射关系 6、音视频&FFmpeg&播放器 FFmpeg 基于命令方式实现了一个音视频编辑 App:https://github.com/yhaolpz/FFmpegCmd 集成编译了 AAC、MP3、H264 编码器 播放器原理 视频播放原理:(mp4、flv)-> 解封装 -> (mp3/aac、h264/h265)-> 解码 -> (pcm、yuv)-> 音视频同步 -> 渲染播放 音视频同步: 选择参考时钟源:音频时间戳、视频时间戳和外部时间三者选择一个作为参考时钟源(一般选择音频,因为人对音频更敏感,ijk 默认也是音频) 通过等待或丢帧将视频流与参考时钟源对齐,实现同步 IjkPlayer 原理 集成了 MediaPlayer、ExoPlayer 和 IjkPlayer 三种实现,其中 IjkPlayer 基于 FFmpeg 的 ffplay 音频输出方式:AudioTrack、OpenSL ES;视频输出方式:NativeWindow、OpenGL ES 如何做好面试突击,规划学习方向? 对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们! 这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。 面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。 建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。 学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。我们搜集整理过这几年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。 我们在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多; 上述【Android面试题解析】以及【Android高级架构进阶视频】可以 免费下载获取 下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC 当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
我们知道,Android 低版本(4.X 及以下,SDK < 21)的设备,采用的 Java 运行环境是 Dalvik 虚拟机。它相比于高版本,最大的问题就是在安装或者升级更新之后,首次冷启动的耗时漫长。这常常需要花费几十秒甚至几分钟,用户不得不面对一片黑屏,熬过这段时间才能正常使用 APP。 这是非常影响用户的使用体验的。我们从线上数据也可以发现,Android 4.X 及以下机型,其新增用户也占了一定的比例,但留存用户数相比新增则要少非常多。尤其在海外,像东南亚以及拉美等地区,还存有着很大量的低端机。4.X 以下低版本用户虽然比较少,但对于抖音及 TikTok 这样有着亿级规模的用户的 APP,即使占比 10%,数目也有上千万。因此如果想要打通下沉市场,这部分用户的使用和升级体验是绝对无法忽视的。这个问题的根本原因就在于,安装或者升级后首次 MultiDex 花费的时间过于漫长。为了解决这个问题,我们挖掘了 Dalvik 虚拟机的底层系统机制,对 DEX 相关处理逻辑进行了重新设计,最终推出了 BoostMultiDex 方案,它能够减少 80%以上的黑屏等待时间,挽救低版本 Android 用户的升级安装体验。我们先来简单看一个安装后首次冷启动加载 DEX 时间的对比数据: 可以看到原始 MultiDex 方案竟然花了半分钟以上才能完成 DEX 加载,而 BoostMultiDex 方案的时间仅需要 5 秒以内。优化效果极为显著!接下来,我们就来详细讲解整个 BoostMultiDex 方案的研发过程与解决思路。 起因 我们先来看下导致这个问题的根本原因。这里面是有多个原因共同引起的。首先需要清楚的是,在 Java 里面想要访问一个类,必然是需要通过 ClassLoader 来加载它们才能访问到。在 Android 上,APP 里面的类都是由PathClassLoader负责加载的。而类都是依附于 DEX 文件而存在的,只有加载了相应的 DEX,才能对其中的类进行使用。Android 早期对于 DEX 的指令格式设计并不完善,单个 DEX 文件中引用的 Java 方法总数不能超过 65536 个。对于现在的 APP 而言,只要功能逻辑多一些,很容易就会触达这个界限。这样,如果一个 APP 的 Java 代码的方法数超过了 65536 个,这个 APP 的代码就无法被一个 DEX 文件完全装下,那么,我们在编译期间就不得不生成多个 DEX 文件。我们解开抖音的 APK 就可以看到,里面确实包含了很多个 DEX 文件: 8035972 00-00-1980 00:00 classes.dex 8476188 00-00-1980 00:00 classes2.dex 7882916 00-00-1980 00:00 classes3.dex 9041240 00-00-1980 00:00 classes4.dex 8646596 00-00-1980 00:00 classes5.dex 8644640 00-00-1980 00:00 classes6.dex 5888368 00-00-1980 00:00 classes7.dex Android 4.4 及以下采用的是 Dalvik 虚拟机,在通常情况下,Dalvik 虚拟机只能执行做过 OPT 优化的 DEX 文件,也就是我们常说的 ODEX 文件。一个 APK 在安装的时候,其中的classes.dex会自动做 ODEX 优化,并在启动的时候由系统默认直接加载到 APP 的PathClassLoader里面,因此classes.dex中的类肯定能直接访问,不需要我们操心。除它之外的 DEX 文件,也就是classes2.dex、classes3.dex、classes4.dex等 DEX 文件(这里我们统称为 Secondary DEX 文件),这些文件都需要靠我们自己进行 ODEX 优化,并加载到 ClassLoader 里,才能正常使用其中的类。否则在访问这些类的时候,就会抛出ClassNotFound异常从而引起崩溃。因此,Android 官方推出了 MultiDex 方案。只需要在 APP 程序执行最早的入口,也就是Application.attachBaseContext里面直接调MultiDex.install,它会解开 APK 包,对第二个以后的 DEX 文件做 ODEX 优化并加载。这样,带有多个 DEX 文件的 APK 就可以顺利执行下去了。这个操作会在 APP 安装或者更新后首次冷启动的时候发生,正是由于这个过程耗时漫长,才导致了我们最开始提到的耗时黑屏问题。 原始实现 了解了这个背景之后,我们再来看 MultiDex 的实现,逻辑就比较清晰了。首先,APK 里面的所有classes2.dex、classes3.dex、classes4.dex等 DEX 文件都会被解压出来。然后,对每个 dex 进行 ZIP 压缩。生成 classesN.zip 文件。接着,对每个 ZIP 文件做 ODEX 优化,生成 classesN.zip.odex 文件。具体而言,我们可以看到 APP 的 code_cache 目录下有这些文件: com.bytedance.app.boost_multidex-1.apk.classes2.dex com.bytedance.app.boost_multidex-1.apk.classes2.zip com.bytedance.app.boost_multidex-1.apk.classes3.dex com.bytedance.app.boost_multidex-1.apk.classes3.zip com.bytedance.app.boost_multidex-1.apk.classes4.dex com.bytedance.app.boost_multidex-1.apk.classes4.zip 这一步是通过DexFile.loadDex方法实现的,只需要指定原始 ZIP 文件和 ODEX 文件的路径,就能够根据 ZIP 中的 DEX 生成相应的 ODEX 产物,这个方法会最终返回一个DexFile对象。最后,APP 把这些DexFile对象都添加到PathClassLoader的pathList里面,就可以让 APP 在运行期间,通过ClassLoader加载使用到这些 DEX 中的类。在这整个过程中,生成 ZIP 和 ODEX 文件的过程都是比较耗时的,如果一个 APP 中有很多个 Secondary DEX 文件,就会加剧这一问题。尤其是生成 ODEX 的过程,Dalvik 虚拟机会把 DEX 格式的文件进行遍历扫描和优化重写处理,从而转换为 ODEX 文件,这就是其中最大的耗时瓶颈。 普遍采用的优化方式 目前业界已经有了一些对 MultiDex 进行优化的方法,我们先来看下大家通常是怎么优化这一过程的。 异步化加载 把启动阶段要使用的类尽可能多地打包到主 Dex 里面,尽量多地不依赖 Secondary DEX 来跑业务代码。然后异步调用MultiDex.install,而在后续某个时间点需要用到 Secondary DEX 的时候,如果 MultiDex 还没执行完,就停下来同步等待它完成再继续执行后续的代码。这样确实可以在 install 的同时往下执行部分代码,而不至于被完全堵住。然而要做到这点,必须首先梳理好启动逻辑的代码,明确知道哪些是可以并行执行的。另外,由于主 Dex 能放的代码本身就比较有限,业务在启动阶段如果有太多依赖,就不能完全放入主 Dex 里面,因此就需要合理地剥离依赖。因此现实情况下这个方案效果比较有限,如果启动阶段牵扯了太多业务逻辑,很可能并行执行不了太多代码,就很快又被 install 堵住了。 模块懒加载 这个方案最早见于美团的文章,可以说是前一个方案的升级版。它也是做异步 DEX 加载,不过不同之处在于,在编译期间就需要对 DEX 按模块进行拆分。一般是把一级界面的 Activity、Service、Receiver、Provider 涉及到的代码都放到第一个 DEX 中,而把二级、三级页面的 Activity 以及非高频界面的代码放到了 Secondary DEX 中。当后面需要执行某个模块的时候,先判断这个模块的 Class 是否已经加载完成,如果没有完成,就等待 install 完成后再继续执行。可见,这个方案对业务的改造程度相当巨大,而且已经有了一些插件化框架的雏形。另外,想要做到能对模块的 Class 的加载情况进行判断,还得通过反射 ActivityThread 注入自己的 Instrumentation,在执行 Activity 之前插入自己的判断逻辑。这也会相应地引入机型兼容性问题。 多线程加载 原生的 MultiDex 是顺序依次对每个 DEX 文件做 ODEX 优化的。而多线程的思路是,把每个 DEX 分别用各自线程做 OPT。这么乍看起来,似乎是能够并行地做 ODEX 来起到优化效果。然而我们项目中一共有 6 个 Secondary DEX 文件,实测发现,这种方式几乎没有优化效果。原因可能是 ODEX 本身其实是重度 I/O 类型的操作,对于并发而言,多个线程同时进行 I/O 操作并不能带来明显收益,并且多线程切换本身也会带来一定损耗。 后台进程加载 这个方案主要是防止主进程做 ODEX 太久导致 ANR。当点击 APP 的时候,先单独启动了一个非主进程来先做 ODEX,等非主进程做完 ODEX 后再叫起主进程,这样主进程起来直接取得做好的 ODEX 就可以直接执行。不过,这只是规避了主进程 ANR 的问题,第一次启动的整体等待时间并没有减少。 一个更彻底的优化方案 上述几个方案,在各个层面都尝试做了优化,然而仔细分析便会发现,它们都没有触及这个问题中根本,也就是就MultiDex.install操作本身。MultiDex.install生成 ODEX 文件的过程,调用的方法是DexFile.loadDex,它会启动一个 dexopt 进程对输入的 DEX 文件进行 ODEX 转化。那么,这个 ODEX 优化的时间是否可以避免呢?我们的 BoostMultiDex 方案,正是从这一点入手,从本质上优化 install 的耗时。我们的做法是,在第一次启动的时候,直接加载没有经过 OPT 优化的原始 DEX,先使得 APP 能够正常启动。然后在后台启动一个单独进程,慢慢地做完 DEX 的 OPT 工作,尽可能避免影响到前台 APP 的正常使用。突破口这里的难点,自然是——如何做到可以直接加载原始 DEX,避免 ODEX 优化带来的耗时阻塞。如果要避免 ODEX 优化,又想要 APP 能够正常运行,就意味着 Dalvik 虚拟机需要直接执行没有做过 OPT 的、原始的 DEX 文件。虚拟机是否支持直接执行 DEX 文件呢?毕竟 Dalvik 虚拟机是可以直接执行原始 DEX 字节码的,ODEX 相比 DEX 只是做了一些额外的分析优化。因此即使 DEX 不通过优化,理论上应该是可以正常执行的。功夫不负有心人,经过我们的一番挖掘,在系统的 dalvik 源码里面果然找到了这一隐藏入口: /* * private static int openDexFile(byte[] fileContents) throws IOException * * Open a DEX file represented in a byte[], returning a pointer to our * internal data structure. * * The system will only perform "essential" optimizations on the given file. * */ static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args, JValue* pResult) { ArrayObject* fileContentsObj = (ArrayObject*) args[0]; u4 length; u1* pBytes; RawDexFile* pRawDexFile; DexOrJar* pDexOrJar = NULL; if (fileContentsObj == NULL) { dvmThrowNullPointerException("fileContents == null"); RETURN_VOID(); } /* TODO: Avoid making a copy of the array. (note array *is* modified) */ length = fileContentsObj->length; pBytes = (u1*) malloc(length); if (pBytes == NULL) { dvmThrowRuntimeException("unable to allocate DEX memory"); RETURN_VOID(); } memcpy(pBytes, fileContentsObj->contents, length); if (dvmRawDexFileOpenArray(pBytes, length, &pRawDexFile) != 0) { ALOGV("Unable to open in-memory DEX file"); free(pBytes); dvmThrowRuntimeException("unable to open in-memory DEX file"); RETURN_VOID(); } ALOGV("Opening in-memory DEX"); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = true; pDexOrJar->pRawDexFile = pRawDexFile; pDexOrJar->pDexMemory = pBytes; pDexOrJar->fileName = strdup("<memory>"); // Needs to be free()able. addToDexFileTable(pDexOrJar); RETURN_PTR(pDexOrJar); } 这个方法可以做到对原始 DEX 文件做加载,而不依赖 ODEX 文件,它其实就做了这么几件事: 接受一个byte[]参数,也就是原始 DEX 文件的字节码。调用dvmRawDexFileOpenArray函数来处理byte[],生成RawDexFile对象由RawDexFile对象生成一个DexOrJar,通过addToDexFileTable添加到虚拟机内部,这样后续就可以正常使用它了返回这个DexOrJar的地址给上层,让上层用它作为 cookie 来构造一个合法的DexFile对象 这样,上层在取得所有 Seconary DEX 的DexFile对象后,调用 makeDexElements 插入到 ClassLoader 里面,就完成 install 操作了。如此一来,我们就能完美地避过 ODEX 优化,让 APP 正常执行下去了。寻找入口看起来似乎很顺利,然而在我们却遇到了一个意外状况。我们从Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数的名字可以明显看出,这是一个 JNI 方法,从 4.0 到 4.3 版本都能找到它的 Java 原型: /* * Open a DEX file based on a {@code byte[]}. The value returned * is a magic VM cookie. On failure, a RuntimeException is thrown. */ native private static int openDexFile(byte[] fileContents); 然而我们在 4.4 版本上,Java 层它并没有对应的 native 方法。这样我们便无法直接在上层调用了。当然,我们很容易想到,可以用 dlsym 来直接搜寻这个函数的符号来调用。但是可惜的是,Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个方法是static的,因此它并没有被导出。我们实际去解析libdvm.so的时候,也确实没有找到Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个符号。不过,由于它是 JNI 函数,也是通过正常方式注册到虚拟机里面的。因此,我们可以找到它对应的函数注册表: const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { { "openDexFileNative", "(Ljava/lang/String;Ljava/lang/String;I)I", Dalvik_dalvik_system_DexFile_openDexFileNative }, { "openDexFile", "([B)I", Dalvik_dalvik_system_DexFile_openDexFile_bytearray }, { "closeDexFile", "(I)V", Dalvik_dalvik_system_DexFile_closeDexFile }, { "defineClassNative", "(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;", Dalvik_dalvik_system_DexFile_defineClassNative }, { "getClassNameList", "(I)[Ljava/lang/String;", Dalvik_dalvik_system_DexFile_getClassNameList }, { "isDexOptNeeded", "(Ljava/lang/String;)Z", Dalvik_dalvik_system_DexFile_isDexOptNeeded }, { NULL, NULL, NULL }, }; dvm_dalvik_system_DexFile这个数组需要被虚拟机在运行时动态地注册进去,因此,这个符号是一定会被导出的。这么一来,我们也就可以通过 dlsym 取得这个数组,按照逐个元素字符串匹配的方式来搜寻openDexFile对应的Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法了。具体代码实现如下: const char *name = "openDexFile"; JNINativeMethod* func = (JNINativeMethod*) dlsym(handler, "dvm_dalvik_system_DexFile");; size_t len_name = strlen(name); while (func->name != nullptr) { if ((strncmp(name, func->name, len_name) == 0) && (strncmp("([B)I", func->signature, len_name) == 0)) { return reinterpret_cast<func_openDexFileBytes>(func->fnPtr); } func++; } 捋清步骤小结一下,绕过 ODEX 直接加载 DEX 的方案,主要有以下步骤: 从 APK 中解压获取原始 Secondary DEX 文件的字节码通过 dlsym 获取dvm_dalvik_system_DexFile数组在数组中查询得到Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数调用该函数,逐个传入之前从 APK 获取的 DEX 字节码,完成 DEX 加载,得到合法的DexFile对象把DexFile对象都添加到 APP 的PathClassLoader的 pathList 里 完成了上述几步操作,我们就可以正常访问到 Secondary DEX 里面的类了getDex 问题然而,正当我们顺利注入原始 DEX 往下执行的时候,却在 4.4 的机型上马上遇到了一个必现的崩溃: JNI WARNING: JNI function NewGlobalRef called with exception pending in Ljava/lang/Class;.getDex:()Lcom/android/dex/Dex; (NewGlobalRef) Pending exception is: java.lang.IndexOutOfBoundsException: index=0, limit=0 at java.nio.Buffer.checkIndex(Buffer.java:156) at java.nio.DirectByteBuffer.get(DirectByteBuffer.java:157) at com.android.dex.Dex.create(Dex.java:129) at java.lang.Class.getDex(Native Method) at libcore.reflect.AnnotationAccess.getSignature(AnnotationAccess.java:447) at java.lang.Class.getGenericSuperclass(Class.java:824) at com.google.gson.reflect.TypeToken.getSuperclassTypeParameter(TypeToken.java:82) at com.google.gson.reflect.TypeToken.<init>(TypeToken.java:62) at com.google.gson.Gson$1.<init>(Gson.java:112) at com.google.gson.Gson.<clinit>(Gson.java:112) ... ... 可以看到,Gson 里面使用到了Class.getGenericSuperclass方法,而它最终调用了Class.getDex,它是一个 native 方法,对应实现如下: JNIEXPORT jobject JNICALL Java_java_lang_Class_getDex(JNIEnv* env, jclass javaClass) { Thread* self = dvmThreadSelf(); ClassObject* c = (ClassObject*) dvmDecodeIndirectRef(self, javaClass); DvmDex* dvm_dex = c->pDvmDex; if (dvm_dex == NULL) { return NULL; } // Already cached? if (dvm_dex->dex_object != NULL) { return dvm_dex->dex_object; } jobject byte_buffer = env->NewDirectByteBuffer(dvm_dex->memMap.addr, dvm_dex->memMap.length); if (byte_buffer == NULL) { return NULL; } jclass com_android_dex_Dex = env->FindClass("com/android/dex/Dex"); if (com_android_dex_Dex == NULL) { return NULL; } jmethodID com_android_dex_Dex_create = env->GetStaticMethodID(com_android_dex_Dex, "create", "(Ljava/nio/ByteBuffer;)Lcom/android/dex/Dex;"); if (com_android_dex_Dex_create == NULL) { return NULL; } jvalue args[1]; args[0].l = byte_buffer; jobject local_ref = env->CallStaticObjectMethodA(com_android_dex_Dex, com_android_dex_Dex_create, args); if (local_ref == NULL) { return NULL; } // Check another thread didn't cache an object, if we've won install the object. ScopedPthreadMutexLock lock(&dvm_dex->modLock); if (dvm_dex->dex_object == NULL) { dvm_dex->dex_object = env->NewGlobalRef(local_ref); } return dvm_dex->dex_object; } 结合堆栈和代码来看,崩溃的点是在 JNI 里面执行com.android.dex.Dex.create的时候: jobject local_ref = env->CallStaticObjectMethodA(com_android_dex_Dex, com_android_dex_Dex_create, args); 由于是 JNI 方法,这个调用发生异常后如果没有 check,在后续执行到env->NewGlobalRef调用的时候会检查到前面发生了异常,从而抛出。而com.android.dex.Dex.create之所以会执行失败,主要原因是入参有问题,这里的参数是dvm_dex->memMap取到的一块 map 内存。dvm_dex 是从这个 Class 里面取得的。虚拟机代码里面,每个 Class 对应是结构是ClassObject中,其中有这个字段: struct ClassObject : Object { ... ... /* DexFile from which we came; needed to resolve constant pool entries */ /* (will be NULL for VM-generated, e.g. arrays and primitive classes) */ DvmDex* pDvmDex; ... ... 这里的pDvmDex是在这里加载类的过程中赋值的: static void Dalvik_dalvik_system_DexFile_defineClassNative(const u4* args, JValue* pResult) { ... ... if (pDexOrJar->isDex) pDvmDex = dvmGetRawDexFileDex(pDexOrJar->pRawDexFile); else pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile); ... ... pDvmDex是从dvmGetRawDexFileDex方法里面取得的,而这里的参数pDexOrJar->pRawDexFile正是我们前面openDexFile_bytearray里面创建的,pDexOrJar是之前返回给上层的 cookie。再根据dvmGetRawDexFileDex: INLINE DvmDex* dvmGetRawDexFileDex(RawDexFile* pRawDexFile) { return pRawDexFile->pDvmDex; } 可以最终推得,dvm_dex->memMap对应的正是openDexFile_bytearray时拿到的pDexOrJar->pRawDexFile->pDvmDex->memMap。我们在当初加载 DEX 字节数组的时候,是否遗漏了对memMap进行赋值呢?我们通过分析代码,发现的确如此,memMap这个字段只在 ODEX 的情况下才会赋值: /* * Given an open optimized DEX file, map it into read-only shared memory and * parse the contents. * * Returns nonzero on error. */ int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex) { ... ... // 构造memMap if (sysMapFileInShmemWritableReadOnly(fd, &memMap) != 0) { ALOGE("Unable to map file"); goto bail; } ... ... // 赋值memMap /* tuck this into the DexFile so it gets released later */ sysCopyMap(&pDvmDex->memMap, &memMap); ... ... } 而只加载 DEX 字节数组的情况下并不会走这个方法,因此也就没法对 memMap 进行赋值了。看来,Android 官方从一开始对openDexFile_bytearray就没支持好,系统代码里面也没有任何使用的地方,所以当我们强制使用这个方法的时候就会暴露出这个问题。虽然这个是官方的坑,但我们既然需要使用,就得想办法填上。再次分析Java_java_lang_Class_getDex方法,我们注意到了这段: if (dvm_dex->dex_object != NULL) { return dvm_dex->dex_object; } dvm_dex->dex_object如果非空,就会直接返回,不会再往下执行到取 memMap 的地方,因此就不会引发异常。这样,解决思路就很清晰了,我们在加载完 DEX 数组之后,立即自己生成一个dex_object对象,并注入pDvmDex里面。详细代码如下: jclass clazz = env->FindClass("com/android/dex/Dex"); jobject dex_object = env->NewGlobalRef( env->NewObject(clazz), env->GetMethodID(clazz, "<init>", "([B)V"), bytes)); dexOrJar->pRawDexFile->pDvmDex->dex_object = dex_object; 这样设置进去之后,果然不再出现 getDex 异常了。 小结 至此,无需等待 ODEX 优化的直接 DEX 加载方案已经完全打通,APP 的首次启动时间由此可以大幅减少。我们距离最终的极致完整解决方案还有一小段路,然而,正是这一小段路,才最为艰险严峻。更大的挑战还在后面,我们将在下一篇文章为大家细细分解,同时也会详细展示最终方案带来的收益情况。大家也可以先思考一下这里还有哪些问题没有考虑到。 推荐阅读:2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中) 作者:字节跳动技术团队链接:https://juejin.im/post/5e5b9466518825494b3cd5aa来源:掘金
音视频趋势 随着5G时代的到来,音视频领域将会大放异彩。 5G让所有人兴奋,用户期待,因为5G网络更快更稳定延迟更低。运营商和上下游产业也期待,大家都想在5G时代分一杯羹。 近几年抖音快手B站等App的火热,已经说明问题了。随着WiFi设施的全面普及,流量费用的进一步下降,使得我们随时随地刷视频成为了可能。回想起我大学时代,那个时候流量很贵,贵到什么程度呢?1M流量要10块钱!大家想一想,1M流量10块钱,1G流量1万块钱,你还敢用4G刷视频么?4G时代,大家刷短视频,5G时代,大家刷长视频。基于这个判断,音视频相关技术是未来几年的热点,除了抖音快手,新的现象级客户端有可能会出现。 作为移动开发人员,如何跟上热点学习音视频技术呢? 今天主要介绍视频入门基础知识 视频编码基础知识 视频和图像和关系 好了,刚才说了图像,现在,我们开始说视频。所谓视频,大家从小就看动画,都知道视频是怎么来的吧?没错,大量的图片连续起来,就是视频。 衡量视频,又是用的什么指标参数呢?最主要的一个,就是帧率(Frame Rate)。在视频中,一个帧(Frame)就是指一幅静止的画面。帧率,就是指视频每秒钟包括的画面数量(FPS,Frame per second)。 帧率越高,视频就越逼真、越流畅。 未经编码的视频数据量会有多大? 有了视频之后,就涉及到两个问题: 一个是存储; 二个是传输。 而之所以会有视频编码,关键就在于此:一个视频,如果未经编码,它的体积是非常庞大的。 以一个分辨率1920×1280,帧率30的视频为例: 共:1920×1280=2,073,600(Pixels 像素),每个像素点是24bit(前面算过的哦); 也就是:每幅图片2073600×24=49766400 bit,8 bit(位)=1 byte(字节); 所以:49766400bit=6220800byte≈6.22MB。 这是一幅1920×1280图片的原始大小,再乘以帧率30。 也就是说:每秒视频的大小是186.6MB,每分钟大约是11GB,一部90分钟的电影,约是1000GB。。。 吓尿了吧?就算你现在电脑硬盘是4TB的(实际也就3600GB),也放不下几部大姐姐啊!不仅要存储,还要传输,不然视频从哪来呢?如果按照100M的网速(12.5MB/s),下刚才那部电影,需要22个小时。。。再次崩溃。。。 正因为如此,屌丝工程师们就提出了,必须对视频进行编码。 什么是编码? 编码:就是按指定的方法,将信息从一种形式(格式),转换成另一种形式(格式)。视频编码:就是将一种视频格式,转换成另一种视频格式。 编码的终极目的,说白了,就是为了压缩。各种五花八门的视频编码方式,都是为了让视频变得体积更小,有利于存储和传输。 我们先来看看,视频从录制到播放的整个过程,如下: 首先是视频采集。通常我们会使用摄像机、摄像头进行视频采集。限于篇幅,我就不打算和大家解释CCD成像原理了。 采集了视频数据之后,就要进行模数转换,将模拟信号变成数字信号。其实现在很多都是摄像机(摄像头)直接输出数字信号。信号输出之后,还要进行预处理,将RGB信号变成YUV信号。 前面我们介绍了RGB信号,那什么是YUV信号呢? 简单来说,YUV就是另外一种颜色数字化表示方式。视频通信系统之所以要采用YUV,而不是RGB,主要是因为RGB信号不利于压缩。在YUV这种方式里面,加入了亮度这一概念。在最近十年中,视频工程师发现,眼睛对于亮和暗的分辨要比对颜色的分辨更精细一些,也就是说,人眼对色度的敏感程度要低于对亮度的敏感程度。 所以,工程师认为,在我们的视频存储中,没有必要存储全部颜色信号。我们可以把更多带宽留给黑—白信号(被称作“亮度”),将稍少的带宽留给彩色信号(被称作“色度”)。于是,就有了YUV。 YUV里面的“Y”,就是亮度(Luma),“U”和“V”则是色度(Chroma)。 大家偶尔会见到的Y'CbCr,也称为YUV,是YUV的压缩版本,不同之处在于Y'CbCr用于数字图像领域,YUV用于模拟信号领域,MPEG、DVD、摄像机中常说的YUV其实就是Y'CbCr。 ▲ YUV(Y'CbCr)是如何形成图像的 YUV码流的存储格式其实与其采样的方式密切相关。(采样,就是捕捉数据) 主流的采样方式有三种: 1)YUV4:4:4; 2)YUV4:2:2; 3)YUV4:2:0。 具体解释起来有点繁琐,大家只需记住,通常用的是YUV4:2:0的采样方式,能获得1/2的压缩率。 这些预处理做完之后,就是正式的编码了。 5、视频编码的实现原理 5.1 视频编码技术的基本原理 前面我们说了,编码就是为了压缩。要实现压缩,就要设计各种算法,将视频数据中的冗余信息去除。当你面对一张图片,或者一段视频的时候,你想一想,如果是你,你会如何进行压缩呢? ▲ 对于新垣女神,我一bit也不舍得压缩… 我觉得,首先你想到的,应该是找规律。是的,寻找像素之间的相关性,还有不同时间的图像帧之间,它们的相关性。 举个例子:如果一幅图(1920×1080分辨率),全是红色的,我有没有必要说2073600次[255,0,0]?我只要说一次[255,0,0],然后再说2073599次“同上”。 如果一段1分钟的视频,有十几秒画面是不动的,或者,有80%的图像面积,整个过程都是不变(不动)的。那么,是不是这块存储开销,就可以节约掉了? ▲ 以上图为例,只有部分元素在动,大部分是不动的 是的,所谓编码算法,就是寻找规律,构建模型。谁能找到更精准的规律,建立更高效的模型,谁就是厉害的算法。 通常来说,视频里面的冗余信息包括: 视频编码技术优先消除的目标,就是空间冗余和时间冗余。 接下来,就和大家介绍一下,究竟是采用什么样的办法,才能干掉它们。以下内容稍微有点高能,不过我相信大家耐心一些还是可以看懂的。 视频编码技术的实现方法 视频是由不同的帧画面连续播放形成的。 这些帧,主要分为三类,分别是: 1)I帧; 2)B帧; 3)P帧。 I帧:是自带全部信息的独立帧,是最完整的画面(占用的空间最大),无需参考其它图像便可独立进行解码。视频序列中的第一个帧,始终都是I帧。 P帧:“帧间预测编码帧”,需要参考前面的I帧和/或P帧的不同部分,才能进行编码。P帧对前面的P和I参考帧有依赖性。但是,P帧压缩率比较高,占用的空间较小。 ▲ P帧 B帧:“双向预测编码帧”,以前帧后帧作为参考帧。不仅参考前面,还参考后面的帧,所以,它的压缩率最高,可以达到200:1。不过,因为依赖后面的帧,所以不适合实时传输(例如视频会议)。 ▲ B帧 通过对帧的分类处理,可以大幅压缩视频的大小。毕竟,要处理的对象,大幅减少了(从整个图像,变成图像中的一个区域)。 如果从视频码流中抓一个包,也可以看到I帧的信息,如下: 我们来通过一个例子看一下。 这有两个帧: 好像是一样的? 不对,我做个GIF动图,就能看出来,是不一样的: 人在动,背景是没有在动的。 第一帧是I帧,第二帧是P帧。两个帧之间的差值,就是如下: 也就是说,图中的部分像素,进行了移动。移动轨迹如下: 这个,就是运动估计和补偿。 当然了,如果总是按照像素来算,数据量会比较大,所以,一般都是把图像切割为不同的“块(Block)”或“宏块(MacroBlock)”,对它们进行计算。一个宏块一般为16像素×16像素。 ▲ 将图片切割为宏块 好了,我来梳理一下。 对I帧的处理,是采用帧内编码方式,只利用本帧图像内的空间相关性。对P帧的处理,采用帧间编码(前向运动估计),同时利用空间和时间上的相关性。简单来说,采用运动补偿(motion compensation)算法来去掉冗余信息。 需要特别注意,I帧(帧内编码),虽然只有空间相关性,但整个编码过程也不简单。 如上图所示,整个帧内编码,还要经过DCT(离散余弦变换)、量化、编码等多个过程。限于篇幅,加之较为复杂,今天就放弃解释了。 那么,视频经过编码解码之后,如何衡量和评价编解码的效果呢? 一般来说,分为客观评价和主观评价。客观评价,就是拿数字来说话。例如计算“信噪比/峰值信噪比”。 信噪比的计算,我就不介绍了,丢个公式,有空可以自己慢慢研究... 除了客观评价,就是主观评价了。主观评价,就是用人的主观感知直接测量,额,说人话就是——“好不好看我说了算”。 学习分享 音视频,人工智能,这些是未来没办法阻挡的发展大趋势。我在猎聘网上看那些招聘岗位,要求精通NDK的薪资都在30-60K。追求高薪岗位的小伙伴,NDK开发一定要掌握并且去深挖。 题外话,虽然我在大厂工作多年,但也指导过不少同行。深知学习分享的重要性。 当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。 以下是今天给大家分享的一些独家干货: 【Android高级架构思维脑图(技能树)】 【NDK学习视频】、【NDK资料包】下载地址https://shimo.im/docs/YHJtVkC3y6qgp9xC
前言 在前几年兴起了MVVM架构设计模式,最具有代表的框架就是DataBinding,虽然这种设计架构非常新颖,但是在使用中仍然还有很多痛点,所以我当时觉得短时间这个设计架构可能不会太流行。 最近接手了新项目,使用的就是MVVM,才发现只一两年的功夫MVVM的发展竟然这么快,已经是Android开发者必备的技能之一了。 正文 DataBinding在刚开始阶段,最令我头疼的就是数据处理的问题,往往为了显示数据,我要在XML中绑定N多个字段,如果是一个中等以上的工程,还有更蛋疼的问题,例如: 你的XML可能迫切的需要if或者switch这样的判断; 意想不到的空指针 在2018年,Google推出JetPack库,其中的ViewModel+LIveData终于把MVVM推上了新的高度。 ViewModel 使用ViewModel需要依赖lifecycle库: implementation "android.arch.lifecycle:viewmodel:x.x.x" implementation "android.arch.lifecycle:extensions:x.x.x" ViewModel的创建方法主要有两种: // 获取FragmentActivity共享的ViewModel ViewModelProviders.of(FragmentActivity).get(ViewModel::class.java) // 获取FragmentActivity共享的ViewModel ViewModelProviders.of(Fragment).get(ViewModel::class.java) ViewModel的共享范围主要有两种:一种是FragmentActivity,一种是Fragment,可以根据自己的需要选择共享的范围。如果你想要一个Application级别的ViewModel,目前是不支持的,你可以自定义Application持有一个ViewModel,或者使用单例模式。 ViewModel解决的问题 1、扩大数据共享的应用场景。 一般的数据共享是Activity与Fragment的数据传递,传统做法是使用setArguments(Bundle),这种方法有以下弊端: 可能无法预测setArguments会在Fragment的哪个周期完成,要进行异常判断; setArguments中的数据可能会发现改变,如果是Activity直接设置Fragment的数据,耦合性很高; 数据较多时,Fragment会有很多的变量,影响可读性和维护性。 使用ViewModel,可以避免以上的尴尬情况,需要什么数据就从ViewModel中取: 新加数据传递,不用修改Activity的setArguments代码,Fragment也不用编写数据接收的方法; 减少数据传递,不必考虑是否要删除暂时无用的代码; 取数据时,请注意数据的有效性,做好判断即可; 除此之外,自定义View也可以得到ViewModel,这样某些功能耦合性非常强的自定义View开发更加便捷。不过需要注意的是View的context的上下文是Activity类型(不会是Fragment)的,所以只能使用Activity级别的数据共享。 2、解决DataBinding的视图显示问题。 如果视图的显示需要很多的数据,那么XML就会变得越来越臃肿,并且迫切需要添加一些简单的判断,例如: 如果A为空就显示B,如果B为空就先是C,如果是C为空... 虽然DataBinding支持三元运算符,能够满足if判断的需要,但是很显然在XML维护逻辑要比Java或者Kotlin要困难的多(无拼写错误提示等)。所以我们非常需要把部分代码从XML分离出来,ViewModel就非常适合担任这个角色。 修改前: <?xml version="1.0" encoding="utf-8"?> <layout> <data> <variable name="A" type="String" /> <variable name="B" type="String" /> <variable name="C" type="String" /> </data> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:maxLines="4" android:ellipsize="middle" android:text="A != null ? A : B != null ? B : C" /> ... </layout> 修改后: <?xml version="1.0" encoding="utf-8"?> <layout> <data> <variable name="viewModel" type="ViewModel" /> </data> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:maxLines="4" android:ellipsize="middle" android:text="@{viewModel.getShowContent()}" /> ... </layout> LiveData 刚才我们已经讨论了ViewModel的用法,但是还有一个问题没有解决,那就是数据更新的问题,解决这个问题的最佳方式就是观察者模式,但是如果没有处理好观察者的注册和解绑很容易出现内存溢出。LiveData就可以完美的解决这个问题。 我们需要添加LiveData的依赖: implementation "androidx.lifecycle:lifecycle-livedata:2.1.0" 下面是一个简单的示例: // 名为openDrawer的Boolean类型的LiveData public final MutableLiveData<Boolean> openDrawer = new MutableLiveData<>(); // 更新openDrawer openDrawer.setValue(true) // 观察openDrawer 的值的变化 openDrawer.observe(this, aBoolean -> { Toast.makeText(this, "${aBoolean}", Toast.LENGTH_SHORT).show(); }); LiveData的子类是MutableLiveData,内部有value属性保存最新的值,订阅LiveData的变化,直接调用LiveData.observe(): public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer)owner:注册的周期,会在owner销毁的时候,解绑观察者。observer:观察的值发生变化的回调函数 owner直接使用Activity或者Fragment即可。如果你还不了解Lifecycle的使用,可以查看一下相关的资料。 总结 最后我画了一张架构图,总结了一下最新的MVVM的使用架构: Activity:处理UI问题,但是应当尽量避免这样做,尽量统一使用DataBinding。ViewModel:保存页面需要的数据,功能复杂的话可以拆分成多个。DataBinding:处理UI视图,持有ViewModel做数据展示。如果页面功能比较复杂,可以对ViewModel和DataBinding再次细分。 如果大家对MVVM有更棒的理解,欢迎留言共同学习。推荐阅读:2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中) 作者:珠穆朗玛小王子链接:https://www.jianshu.com/p/a2eb8e1807ef
面向标准化开发已成现实 金三银四,相信有不少读者在抓紧机会面试。 Android 市场已今非昔比。在过去,迫于招人的压力,应试者只需了解四大组件、视图、网络请求,即可谋得一份满意的工作。 现如今,Jetpack 架构组件 及 标准化开发模式 的确立,意味着 Android 开发已步入成熟阶段: 许多 样板代码 不再需要开发者手写,而是可以通过模版工具 自动生成,在取缔繁杂耗时的重复工作的同时,避免因人工操作的疏忽,而造成难以排查、不可预期的错误。 这十分符合企业的利益,因而面试官在招人的时候,也更加看重应试者对 架构组件 —— 至少是 MVVM 的理解程度。 像“解耦”等 含糊其辞的说法,已经不能够被面试官所认可,稍微对 MVVM 有一点经验的面试官都会请你举例说明,好证明你确实对 MVVM 有着正确、深入的理解,能够自然而然地写出标准化、规范化的代码,能够迅速适应 各家公司自制的 自动化模版工具。 本文的目标 本人拥有 3 年的 移动端架构 践行和设计经验,领导团队重构的中大型项目多达十数个,对 Jetpack MVVM 架构在确立规范化、标准化 开发模式 以减少不可预期的错误 所作的努力,有着深入的理解。 因而本文的目标,就是结合前几期我们分别 深入浅出 介绍过的 Lifecycle、LiveData、ViewModel、DataBinding,来融汇贯通地演绎一下: 作为 应用开发骨架 的 标准化状态管理框架,究竟为 快速开发过程中 减少不可预期的错误 做了哪些努力。 不同于 东拼西凑、人云亦云、徒添困扰 的网文,愿意将 标准化开发模式的 深度思考知识 和 实战反思经验 无保留地分享,全网仅此一家。这样的文章可以说是 看一篇、少一篇,因此,就算不去 hold 住面试官,也请务必跟随本文的脚步,无障碍地将 Jetpack MVVM 过一遍! 文章目录一览 前言 面向标准化开发已成现实 本文的目标 Jetpack Lifecycle Lifecycle 存在前的混沌世界 Lifecycle 为什么能解决上述这些问题? Jetpack LiveData LiveData 存在前的混沌世界 LiveData 为什么能解决上述这些问题? LiveData 有个坑需要注意 Jetpack ViewModel ViewModel 存在前的混沌世界 ViewModel 为什么能做到这几点? Jetpack DataBinding DataBinding 存在前的混沌世界 DataBinding 就是来解决这些问题 综上 Jetpack Lifecycle Lifecycle 的存在,主要是为了解决 生命周期管理 的一致性问题 Lifecycle 存在前的混沌世界 在 Lifecycle 面市前,生命周期管理 纯靠手工维持,这样就容易滋生大量的一致性问题。 例如跨页面共享的 GpsManager 组件,在每个依赖它的 Activity 的 onResume 和 onPause 中都需要 手工 激活、解绑 和 叫停。 那么 随着 Activity 的增多,这种手工操作 埋下的一致性隐患 就会指数级增长: 一方面,凡是手工维持的,开发者容易疏忽,特别是工作交接给其他同事时,同事并不能及时注意到这些细节。 另一方面,分散的代码不利于修改,日后除了激活、叫停,若有其他操作需要补充(例如状态监听),那么每个 Activity 都需要额外书写一遍。 Lifecycle 为什么能解决上述这些问题? Lifecycle 通过 模板方法模式 和 观察者模式,将生命周期管理的复杂操作,全部在作为 LifecycleOwner 的基类中(例如视图控制器的基类)封装好,默默地在背后为开发者运筹帷幄, 开发者因而得以在视图控制器(子类)中只需一句 getLifecycle().addObserver(GpsManager.getInstance) ,优雅地完成 第三方组件在自己内部 对 LifecycleOwner 生命周期的感知。 除了解决一致性问题,这样做还 顺带地提供了其他 2 个好处: 1.规避 为监听状态 而 注入视图控制器 的做法 当需要监听状态时,以往我们的做法是 通过方法手工注入 Activity 等参数,这埋下了内存泄漏的隐患 —— 因为团队中的新手容易因这是个 Activity,而在日后误将其依赖给组件中的其他成员。 现如今,我们可以直接在组件内部 点到为止 地监听 LifecycleOwner 的状态,从而规避这种不恰当的使用。 2.规避 为追溯事故来源 而 注入视图控制器 的做法 当发生事故时,以往我们若想在组件中 追溯事故来源,同样不得不从方法中直接注入 Activity 等,这同样埋下了内存泄漏的隐患。现如今组件因实现了 DefaultLifecycleObserver,而得以通过生命周期回调方法中的 LifecycleOwner 参数,在方法作用域中 即可得知事故来源,无需更多带有隐患的操作。 如果这么说还不理解的话,可具体参考我在 《为你还原一个真实的 Jetpack Lifecycle》 中提供的 GpsManager 案例,本文不再累述。 Jetpack LiveData LiveData 的存在,主要是为了帮助 新手老手 都能不假思索地遵循 通过唯一可信源分发状态 的标准化开发理念,从而使在快速开发过程中 难以追溯、难以排查、不可预期 的问题所发生的概率降低到最小。 LiveData 存在前的混沌世界 在 LiveData 面市前,我们分发状态,多是通过 EventBus 或 Java Interface 来完成的。不管你是用于网络请求回调的情况,还是跨页面通信的情况。 那这造成了什么问题呢?首先,EventBus 只是纯粹的 Bus,它 缺乏上述提到的 标准化开发理念 的约束,那么人们在使用这个框架时,容易因 去中心化 地滥用,而造成 诸如 毫无防备地收到 预期外的 不明来源的推送、拿到过时的数据 及 事件源追溯复杂度 为 n² 的局面。 并且,EventBus 本身缺乏 Lifecycle 的加持,存在生命周期管理的一致性问题。这是 EventBus 的硬伤,也是我拒绝使用 EventBus 的最主要因素。 对上述状况不理解的,可具体参考我在 《LiveData 鲜为人知的 身世背景 和 独特使命》 中提供的 播放器状态全局通知 的案例 LiveData 为什么能解决上述这些问题? 首先,LiveData 是在 Google 希望确立 标准化、规范化 的开发模式 —— 这样一种背景下诞生的,因而为了达成这个艰巨的 使命,Google 十分克制地将其设计为,仅支持状态的输入和监听,从而,它不得不 在单例的配合下,承上启下地完成 状态 从 唯一可信源 到 视图控制器 的输送。 (ViewModel 姑且也算是一种单例,一种工厂模式实现的伪单例。唯一可信源是指 生命周期独立于 视图控制器的 数据组件,通常是 单例 或共享 ViewModel) 这使得任何一次状态推送,都可预期、都能方便地追溯来源,而不至于在 事件追溯复杂度为 n² 的迷宫中白费时间。(即,无论是从哪个视图控制器发起的 对某个共享状态改变的请求,状态最终的改变 都由 作为唯一可信源的 单例或 SharedViewModel 来一对多地通知改变) 并且,这种承上启下的方式,使得单向依赖成为可能:单例无需通过 Java Interface 回调通知视图控制器,从而规避了视图控制器 被生命周期更长的单例 依赖 所埋下的内存泄漏的隐患。 LiveData 有个坑需要注意 不过,LiveData 的设计有个坑,这里我顺带提一下。 为了在视图控制器发生重建后,能够 自动倒灌 所观察的 LiveData 的最后一次数据,LiveData 被设计为粘性事件。 —— 我姑且认为这是个拓展性不佳的设计,甚至可以说是一个 bug, 因为 MVVM 是一个整体,既然 ViewModel 支持共享作用域,并且官方文档都承认了通过 共享 ViewModel 来实现跨页面通信的需求, 那么基于 “开闭原则”,LiveData 理应提供一个与 MutableLiveData 平级的底层支持,专门用于非粘性的事件通信的情况,否则直接在跨页面通信中使用 MutableLiveData 必造成 事件回调的一致性问题 及 难以预期的错误。 关于非粘性 LiveData 的实现,网上存在通过 “事件包装类”(只适合 kotlin 的情况) 和 “反射干预 LastVersion” (适用于 Java 的情况)两种方式来解决: juejin.im/post/5b2b1b… blog.csdn.net/geyuecang/a… 无论是使用哪一种实现,我都建议 遵循传统 LiveData 所遵循的开发理念,通过唯一可信源分发状态,来方便事件源头的追溯。对于 “去中心化” 的 Bus 方式,我拒绝在项目中这样使用。 (具体我会在未来开源的最佳实践项目中 展示 UnPeekLiveData 的使用) Jetpack ViewModel ViewModel 的存在,主要是为了解决 状态管理 和 页面通信 的问题。 ViewModel 存在前的混沌世界 ViewModel 的本职工作是 状态托管 和 状态管理的分治,也即当视图控制器重建时, 对于轻量的状态,可以通过视图控制器基类的 saveInstanceState 机制,以序列化的方式完成存储和恢复。 对于重量级的状态,例如通过网络请求得到的 List,可以通过生命周期长于视图控制器的 ViewModel 持有,从而得以直接从 ViewModel 恢复,而不是以效率较低的序列化方式。 在 Jetpack ViewModel 面市之前,MVP 的 Presenter 和 MVVM - Clean 的 ViewModel 都不具备状态管理分治的能力。 Presenter 和 Clean ViewModel 的生命周期都与视图控制器同生共死,因而它们顶多是为 DataBinding 提供状态的托管,而无法实现状态的分治。 到了 Jetpack 这一版,ViewModel 以精妙的设计,达成了状态管理,以及可共享的作用域。 ViewModel 为什么能做到这几点? 其实这版主要是基于 工厂模式,使得 ViewModel 被 LifecycleOwner 所持有、通过 ViewModelProvider 来引用, 所以 它既类似于单例: —— 当被作为 LifecycleOwner 的 Activity 持有时,能够脱离 Activity 旗下 Fragment 的生命周期,从而实现作用域共享, 实际上又不是单例: —— 生命周期跟随 作为 LifecycleOwner 的视图控制器,当 Owner(Activity 或 Fragment)被销毁时,它也被 clear。 此外,出于对视图控制器重建的考虑,Google 在视图控制器基类中通过 retain 机制对 ViewModel 进行了保留。 因此,对于 作用域共享 和 视图重建 的情况,状态因完好地被保留,而得以被视图控制器在恢复时直接使用。 再者,由于存在 共享作用域的考虑,所以 ViewModel 本身也承担了跨页面通信(例如事件回调)的职责。前面在介绍 LiveData 时,对于 LiveData 在事件通信时粘性设计的问题已经介绍过了,这里不再累述。 截至 2020.2.1,ViewModel 在 Fragment 中的 retain 设计已发生剧变,具体缘由可参考我在 《有了 Jetpack ViewModel . . . 真的可以为所欲为!》 文末及评论区的最新补充。 Jetpack DataBinding DataBinding 的存在,主要是为了解决 视图调用 的一致性问题。 DataBinding 存在前的混沌世界 在 DataBinding 面市前,我们若要改变视图的状态,首先就要引用该视图,例如 textView.setText(), 这造成什么问题呢? 当页面存在横、竖布局,且两种布局的控件存在差异,例如横屏存在 textView 控件,而竖屏没有,那么我们就不得不在视图控制器中为 textView 做判空处理,这就造成了一致性问题 —— 容易疏忽而忘记判空,毕竟页面多达数十个、每个页面的控件也无数。 那怎么办呢? DataBinding 就是来解决这些问题 通过在布局中与可观察的数据发生绑定,那么当该数据被 set 新的内容时,控件也将得到通知和刷新。 换言之,在使用 DataBinding 后,唯一的改变是,你无需手工调用视图来 set 新状态,你只需 set 数据本身。 因而,DataBinding 并非许多人不假思索认为的,将 UI 逻辑搬到 XML 中写 从而难以调试 —— 事实根本不是这样的: DataBinding 只负责绑定数据、负责作为 UI 逻辑末端的状态的改变(也即它是一个不可再分的原子操作,本来就不需要调试),原本在视图控制器中 UI 逻辑怎么写,现在还是怎么写,只不过不再需要 textView.setText(xxx),而是直接 xxx.set()。 所以在 DataBinding 的帮助下,好处总共有多少个呢? 1.规避了视图状态的 一致性问题 —— 无需手工判空。 2.规避了视图状态的 一致性问题,乃至无需视图调用,从而完全不用编写 findViewById。 3.就算要调用视图,也不用 findViewById,而是直接通过 binding 来引用。 4.先前的 UI 逻辑基本不用改动,改的只是作为末端的状态改变的方式。 …… 此外,DataBinding 有个大杀器就是,能为控件提供自定义属性的 BindingAdapter,它不仅可以解决 圆角 Drawable 复用的问题(你懂得),还可以实现 imageView 直接绑定 url 等需求,总之,没有它办不到的,只有你想不到的,DataBinding 的好处等着你挖掘。 关于 DataBinding 的注意事项,以及屡试不爽的排坑技巧,可具体参考 《从 被误解 到 真香 的 Jetpack DataBinding!》,这里不做累述。 综上 Lifecycle 的存在,主要是为了解决 生命周期管理 的一致性问题。 LiveData 的存在,主要是为了帮助 新手老手 都能不假思索地 遵循 通过唯一可信源分发状态 的标准化开发理念,从而在快速开发过程中 规避一系列 难以追溯、难以排查、不可预期 的问题。 ViewModel 的存在,主要是为了解决 状态管理 和 页面通信 的问题。 DataBinding 的存在,主要是为了解决 视图调用 的一致性问题。 它们的存在 大都是为了 在软件工程的背景下 解决一致性的问题、将容易出错的操作在后台封装好,方便使用者快速、稳定、不产生预期外错误地编码。 这样说,你理解了吗? GitHub : Jetpack-MVVM-Best-Practice 作者:KunMinX链接:https://juejin.im/post/5dafc49b6fb9a04e17209922
本文主要内容: 作用介绍 核心类介绍 基本使用 源码分析 -- 横竖屏切换恢复 -- 后台销毁恢复 * ViewModel的主要工作: 本身主要是一个数据维护工具将数据维护的工作从Activity上剥离,提供一个储存数据环境,自身机制可以解决开发中,数据受Activity生命周期影响产生数据丢失的问题 (主要为横竖屏切换以及在后台被销毁)。通常结合LiveData使用。 作为一个纯数据维护工具,可以加入到MVP架构中负责数据保存。而官方推选作为AAC架构MVVM中的VM层。 * ViewModel的主要类: ViewModel (下称VM):数据储存类,架构的核心类。使用时,直接继承该类,根据需求选择重写onCleared()方法。如需在Activity被系统销毁后依然保存数据,定义一个参数为(SavedStateHandle)的构造方法,并将数据保存SavedStateHandle中。实际上通过SavedInstanceState存取 AndroidViewModel:VM子类,维护了Application的引用,由架构中的SavedStateViewModelFactory创建时传入。同样,需要接收SavedStateHandle时,需要定义参数为 (Application, SavedStateHandle)的构造方法。 ViewModelStore:用于保存VM,内部维护了一个用于储存VM的HashMap。一般情况下,直接使用本类创建实例。 ViewModelStoreOwner:接口,实现该接口的类,表示自身能够向外提供VM。androidx 的AppCompatActivity/Fragment实现了该接口。 ViewModelProvider:VM的提供者,获取VM的基本入口。实际依赖ViewModelStore存取VM,Factory生成/恢复VM。 Factory:接口,实现该接口的类主要用于创建VM实例。不建议直接实现该接口,除非你清楚框架内容和自己的需求。一般情况下,如果无需SavedStateHandle机制,可以使用AndroidViewModelFactory。否则应该使用或继承SavedStateViewModelFactory。 * ViewModel的基本使用: 一般使用: // VM class ViewModelA : ViewModel() // AVM class ViewModelB(app: Application) : AndroidViewModel(app) // Activity/Fragment .onCreate中 override fun onCreate() { ... val provider = ViewModelProvider(this) val vmA = provider.get(ViewModelA::class.java) val vmB = provider.get(ViewModelB::class.java) ... } 接受 SavedStateHandle // VM class ViewModelC( val handle: SavedStateHandle ) : ViewModel() // AVM class ViewModelD( app: Application, val handle: SavedStateHandle ) : AndroidViewModel(app) 跨 Fragment 共享数据 Fragment中直接以Activity作为ViewModel的Key ... val provider = ViewModelProvider(requireActivity()) val vmA = provider.get(ViewModelA::class.java) 通过 Application 创建全局共享的 VM class App : Application(), ViewModelStoreOwner { private lateinit var mAppViewModelStore: ViewModelStore private lateinit var mFactory: ViewModelProvider.Factory override fun onCreate() { super.onCreate() mAppViewModelStore = ViewModelStore() mFactory = ViewModelProvider .AndroidViewModelFactory .getInstance(this) } override fun getViewModelStore(): ViewModelStore { return mAppViewModelStore } private fun getAppFactory(): ViewModelProvider.Factory { return mFactory } fun getAppViewModelProvider(activity: Activity): ViewModelProvider { val app = checkApplication(activity) as App return ViewModelProvider(app, app.getAppFactory()) } fun getAppViewModelProvider(fragment: Fragment): ViewModelProvider { return getAppViewModelProvider(fragment.requireActivity()) } private fun checkApplication(activity: Activity): Application { return activity.application ?: throw IllegalStateException( "Your activity is not yet attached to the Application instance." + "You can't request ViewModel before onCreate call.") } } * ViewModel的关键源码分析: 以下源码分析将会去除非相关代码以简化 ViewModelProvider 实现相关: 前面提到,ViewModelProvider的工作完全依赖传入的ViewModelStore和Factory,可以直接从构造方法得知: ViewModelProvider.java ---------------------- private final Factory mFactory; private final ViewModelStore mViewModelStore; public ViewModelProvider(ViewModelStoreOwner owner) { this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory ? ((HasDefaultViewModelProviderFactory) owner) .getDefaultViewModelProviderFactory() : NewInstanceFactory.getInstance()); } public ViewModelProvider(ViewModelStoreOwner owner, Factory factory) { this(owner.getViewModelStore(), factory); } public ViewModelProvider(ViewModelStore store, Factory factory) { mFactory = factory; mViewModelStore = store; } // 简单的反射创建实例的工厂 public static class NewInstanceFactory implements Factory { public <T extends ViewModel> T create(Class<T> modelClass) { return modelClass.newInstance() } } 而androidx.activity.ComponentActivity,androidx.fragment.app.Fragment都实现了ViewModelStoreOwner,HasDefaultViewModelProviderFactory接口。 public class AppCompatActivity extends FragmentActivity...{} public class FragmentActivity extends ComponentActivity...{} public class ComponentActivity extends ... implements ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner ... {} public class Fragment implements ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner ... {} ViewModelProvider的get()方法中返回VM实例,其中mFactory为 SavedStateViewModelFactory: ViewModelProvider.java ---------------------- private static final String DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"; public <T extends ViewModel> T get(@NonNull Class<T> modelClass) { String canonicalName = modelClass.getCanonicalName(); return get(DEFAULT_KEY + ":" + canonicalName, modelClass); } public <T extends ViewModel> T get(String key, Class<T> modelClass) { ViewModel viewModel = mViewModelStore.get(key); // 一个确保机制 if (modelClass.isInstance(viewModel)) { if (mFactory instanceof OnRequeryFactory) { ((OnRequeryFactory) mFactory).onRequery(viewModel); } return (T) viewModel; } // 正常以及基本的逻辑 if (mFactory instanceof KeyedFactory) { viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass); } else { viewModel = (mFactory).create(modelClass); } mViewModelStore.put(key, viewModel); return (T) viewModel; } SavedStateViewModelFactory在后面解析。 * ViewModel 的屏幕横竖屏切换恢复机制: 前面说到,创建VM是通过ViewModelProvider实现的,而ViewModelProvider又是依赖ViewModelStore进行VM的保存。当使用ComponentActivity/Fragment作为ViewModelProvider的初始化参数时,实际VM的储存容器是参数提供的。 ComponentActivity 实现: 从源码中,可以看出横竖屏切换是直接通过NonConfigurationInstances进行恢复的。 ComponentActivity包含一个NonConfigurationInstances类,其中持有ViewModelStore的引用: ComponentActivity.java ---------------------- static final class NonConfigurationInstances { Object custom; ViewModelStore viewModelStore; } 保存ViewModelStore:通过onRetainNonConfigurationInstance()在横竖屏切换中保存ViewModelStore ComponentActivity.java ---------------------- public final Object onRetainNonConfigurationInstance() { Object custom = onRetainCustomNonConfigurationInstance(); // 从上一个 NonConfigurationInstances 中恢复 ViewModelStore ViewModelStore viewModelStore = mViewModelStore; if (viewModelStore == null) { NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { viewModelStore = nc.viewModelStore; } } if (viewModelStore == null && custom == null) { return null; } // 保存当前 ViewModelStore NonConfigurationInstances nci = new NonConfigurationInstances(); nci.custom = custom; nci.viewModelStore = viewModelStore; return nci; } 在使用时尝试通过getLastNonConfigurationInstance()恢复ViewModelStore: ComponentActivity.java ---------------------- public ViewModelStore getViewModelStore() { if (mViewModelStore == null) { NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { // 通过 NonConfigurationInstances 对象直接恢复 ViewModelStore mViewModelStore = nc.viewModelStore; } if (mViewModelStore == null) { mViewModelStore = new ViewModelStore(); } } return mViewModelStore; } Fragment 实现: 要看懂该部分源码,需要对FragmentManager有基础了解,参考:深入理解FragmentManager Fragment的ViewModelStore由FragmentManager维护的FragmentManagerViewModel管理。 注意这里使用了一个VM来维护一个ViewModelStore Fragment.java ------------- public ViewModelStore getViewModelStore() { return mFragmentManager.getViewModelStore(this); } FragmentManager.java -------------------- private FragmentManagerViewModel mNonConfig; ViewModelStore getViewModelStore(Fragment f) { return mNonConfig.getViewModelStore(f); } 处理FragmentManagerViewModel的实例化: FragmentManager.java -------------------- void attachController(FragmentHostCallback<?> host, FragmentContainer container, final Fragment parent) { mHost = host; mParent = parent; if (parent != null) { // 从父类的FM中获取 mNonConfig = parent.mFragmentManager.getChildNonConfig(parent); } else if (host instanceof ViewModelStoreOwner) { // 假如 host 对象是实现了 ViewModelStoreOwner // 则使用这个ViewModelStoreOwner的viewModelStore创建一个 FragmentManagerViewModel ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore(); mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore); } else { // 生成一个不支持自动保存ViewModel的 FragmentManagerViewModel mNonConfig = new FragmentManagerViewModel(false); } } 但从源码注释可以了解到,第三种情况已废弃,理想情况下并不支持。 所以基本上,出现的应该为第一、第二种情况。 host对象实际是实现的ViewModelStoreOwner接口的FragmentActivity$HostCallbacks: FragmentActivity.java --------------------- class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements ViewModelStoreOwner ... {...} 第二种情况,attachController()传入参数为null,可以理解为直接附着在FragmentActivity上的Fragment: FragmentActivity.java --------------------- protected void onCreate(@Nullable Bundle savedInstanceState) { mFragments.attachHost(null /*parent*/); } FragmentController.java ----------------------- public void attachHost(Fragment parent) { mHost.mFragmentManager.attachController(mHost, mHost, parent); } 而第一种情况,attachController()传入参数为Fragment,在Fragment的performAttach()中调用: Fragment.java ------------- void performAttach() { mChildFragmentManager.attachController(mHost, new FragmentContainer(), this) } ...解决了FragmentManagerViewModel的来源,下面看看它的作用。上文提到,FragmentManagerViewModel是一个VM,实际上可以联想到可能是通过Activity的ViewModelStore,使用相同的NonConfigurationInstances机制实现的恢复。 先看第二种情况: FragmentManager.java -------------------- ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore(); mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore); 其中的host为FragmentActivity$HostCallbacks,而getViewModelStore()返回的实际上是FragmentActivity的ViewModelStore: FragmentActivity$HostCallbacks.java ----------------------------------- public ViewModelStore getViewModelStore() { return FragmentActivity.this.getViewModelStore(); } 而FragmentManagerViewModel.getInstance()内部实际上是通过ViewModelProvider返回一个本类VM实例: FragmentManagerViewModel .java ------------------------------ static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) { ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore, FACTORY); return viewModelProvider.get(FragmentManagerViewModel.class); } 由于VM在创建时,会被储存到对应的ViewModelStore,所以该VM会存放到FragmentActivity的ViewModelStore中。 第一种情况:实际上是顶级FragmentManager的FragmentManagerViewModel中,维护一个子级的FragmentManagerViewModel仓库,然后通过顶级FragmentManagerViewModel直接维护所有子级FragmentManagerViewModel。 FragmentManagerViewModel .java ------------------------------ private final HashMap<String, FragmentManagerViewModel> mChildNonConfigs = new HashMap<>(); FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) { FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho); if (childNonConfig == null) { childNonConfig = new FragmentManagerViewModel(mStateAutomaticallySaved); mChildNonConfigs.put(f.mWho, childNonConfig); } return childNonConfig; } ...从以上源码中可以看出,Fragment的横竖屏切换恢复机制实际上是: 通过一个持有自身ViewModelStore引用的VM,依附到Activity的ViewModelStore中,通过Activity的机制进行恢复。 其实这里引申一点的是,源码中提及到,NonConfigurationInstances机制有可能在调用getLastNonConfigurationInstance时返回null,如需确保横竖屏切换时的数据保存,可以使用Fragment的onSaveInstanceState(true),以Fragment作为保存数据的容器。 而事实上,在旧版的ViewModel中,确实是通过Fragment的onSaveInstanceState(true)进行的。 * ViewModel 的后台销毁恢复机制: 前文提到,SavedStateViewModelFactory是实现该机制的一部分,由SavedStateViewModelFactory生成的VM才具有在后台销毁前后通过SavedStateHandle存取数据的特性。 * 先看SavedStateViewModelFactory的构造方法: SavedStateViewModelFactory.java ------------------------------- public SavedStateViewModelFactory(Application application, SavedStateRegistryOwner owner, Bundle defaultArgs) { mSavedStateRegistry = owner.getSavedStateRegistry(); mLifecycle = owner.getLifecycle(); mDefaultArgs = defaultArgs; mApplication = application; mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(application); } SavedStateViewModelFactory在ComponentActivity中实例化传入的参数为: ComponentActivity.java ---------------------- mDefaultFactory = new SavedStateViewModelFactory( getApplication(), this, getIntent() != null ? getIntent().getExtras() : null); ComponentActivity实现了SavedStateRegistryOwner接口,该接口的实现类可以提供SavedStateRegistry实例。 * SavedStateRegistry即是流程的关键对象之一。这里涉及到androidx提供的一个新的组件androidx.savedstate:"该组件允许以插件方式,将组件添加到SaveInstanceState过程中"。 个人理解:这是一套针对SavedState操作Bundle的封装工具,但仅适用于系统实现。因为流程繁琐,系统源码在实现的过程中还包含了自动重建,自动还原数据,生命周期确保等一系列操作。而且当你实现关键的SavedStateProvider接口时,同样要编写Bundle,这和传统的onSaveInstanceState()区别不大。因为系统实现了对VM提供的存取操作,建议直接使用VM,或者直接在onSaveInstanceState()对数据进行操作。 链接:https://developer.android.google.cn/jetpack/androidx/releases/savedstate?hl=zh-cn 我后面有空会另行写一篇文章去讨论系统对该组件的实现。 * ComponentActivity实例化时,创建成员SavedStateRegistryController,后者实例化时,创建成员SavedStateRegistry: ComponentActivity.java ---------------------- private final SavedStateRegistryController mSavedStateRegistryController = SavedStateRegistryController.create(this); SavedStateRegistryController.java --------------------------------- public static SavedStateRegistryController create(SavedStateRegistryOwner owner) { return new SavedStateRegistryController(owner); } private SavedStateRegistryController(SavedStateRegistryOwner owner) { mOwner = owner; mRegistry = new SavedStateRegistry(); } ComponentActivity在onSaveInstanceState()中调用SavedStateRegistryController.performSave(),内部实际调用SavedStateRegistry.performSave(): ComponentActivity.java ---------------------- protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mSavedStateRegistryController.performSave(outState); } SavedStateRegistryController.java --------------------------------- public void performSave(Bundle outBundle) { mRegistry.performSave(outBundle); } SavedStateRegistry会把所有注册到自身的SavedStateProvider,通过saveState()提取数据,并保存到一个Bundle中: SavedStateRegistry.java ----------------------- void performSave(@NonNull Bundle outBundle) { Bundle components = new Bundle(); for (Iterator<Map.Entry<String, SavedStateProvider>> it = mComponents.iteratorWithAdditions(); it.hasNext(); ) { Map.Entry<String, SavedStateProvider> entry1 = it.next(); components.putBundle(entry1.getKey(), entry1.getValue().saveState()); } outBundle.putBundle(SAVED_COMPONENTS_KEY, components); } 至此,说明数据保存的发起,最终通知到SavedStateRegistry。 * 先看SavedStateProvider接口: 注册到SavedStateRegistry中实现类,会在SavedStateRegistry保存过程将会调用saveState()获取数据。而稍后(在恢复数据时),将会通过SavedStateRegistry.consumeRestoredStateForKey()取出保存的数据。 SavedStateProvider通过SavedStateRegistry.registerSavedStateProvider()注册到SavedStateRegistry: SavedStateRegistry.java ----------------------- public void registerSavedStateProvider(String key, SavedStateProvider provider) { SavedStateProvider previous = mComponents.putIfAbsent(key, provider); if (previous != null) { throw new IllegalArgumentException( "SavedStateProvider with the given key is already registered"); } } 到此,说明储存数据的提供者,是注册到SavedStateRegistry中的SavedStateProvider。 * 前文提及,VM的存取核心是SavedStateHandle,那么说明SavedStateProvider和SavedStateHandle存在必然的关联。实际上,SavedStateHandle实例中,维护一个SavedStateProvider匿名内部类实例,而SavedStateHandle的读写和SavedStateProvider实例的数据读取操作,都是对实际数据容器mRegular读写。 先看SavedStateHandle: 前文提到,最终该机制的实现,实际为SaveInstanceState机制,则反映VM并不会对数据进行自动存取。 事实上`VM`确实需要手动将后台销毁前保存的数据放到`SaveInstanceState`中,`SavedStateHandle`确实是这么用的,所以提供了一系列的`get`/`set`操作,而最终还要编写`SavedStateProvider`的`Bundle`转换操作。 同时可以看出,SavedStateHandle提供了对LiveData的存取支持。 `SavedStateHandle`对`LiveData`的支持,来自对`LiveData`的内部的静态包装类`SavingStateLiveData`。 `SavingStateLiveData`包装了`setValue()`,传入的参数会被优先储存到`mRegular`中。 链接 :https://developer.android.google.cn/reference/androidx/lifecycle/SavedStateHandle?hl=zh-cn 用于提取数据的SavedStateProvider: 需要编写SavedStateProvider数据的Bundle转换操作。所以如无必要,无需自定义该组件,建议在onSaveInstanceState()中直接操作。 SavedStateHandle.java --------------------- // 最终的数据容器主体 // initialState为构造时参入的上次SavedInstanceState产生的旧数据 // 既 SavedStateHandle.createHandle 时传入的参数,下面会说明 final Map<String, Object> mRegular = new HashMap<>(initialState); // SavedStateProvider 对数据主体 mRegular 进行处理并生成一个Bundle private final SavedStateProvider mSavedStateProvider = new SavedStateProvider() { public Bundle saveState() { Set<String> keySet = mRegular.keySet(); ArrayList keys = new ArrayList(keySet.size()); ArrayList value = new ArrayList(keys.size()); for (String key : keySet) { keys.add(key); value.add(mRegular.get(key)); } Bundle res = new Bundle(); // "parcelable" arraylists - lol res.putParcelableArrayList("keys", keys); res.putParcelableArrayList("values", value); return res; } }; 至此,SavedStateHandle和SavedStateProvider实现关联。 * 前文提及,SavedStateProvider通过SavedStateRegistry.registerSavedStateProvider()注册到SavedStateRegistry: 而该方法的调用,则是通过SavedStateHandleController进行的。SavedStateHandleController的实例化则是通过SavedStateViewModelFactory进行的,最终回到了SavedStateViewModelFactory。 先看SavedStateViewModelFactory: 通过调用SavedStateHandleController.create()返回SavedStateHandleController实例。同时,VM在创建时,传入了SavedStateHandleController的SavedStateHandle实例作为参数,即VM和SavedStateHandle进行了绑定。 SavedStateViewModelFactory.java ------------------------------- public <T extends ViewModel> T create(String key, Class<T> modelClass) { // 判断是否是AVM boolean isAndroidViewModel = AndroidViewModel.class.isAssignableFrom(modelClass); Constructor<T> constructor; if (isAndroidViewModel) { constructor = findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE); } else { constructor = findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE); } // 如果不需要SavedStateHandle,则直接创建一个普通的VM/AVM if (constructor == null) { return mFactory.create(modelClass); } // 创建了 SavedStateHandleController SavedStateHandleController controller = SavedStateHandleController.create( mSavedStateRegistry, mLifecycle, key, mDefaultArgs); // VM 在创建的时候持有了SavedStateHandleController内维护的SavedStateHandle对象 try { T viewmodel; if (isAndroidViewModel) { viewmodel = constructor.newInstance(mApplication, controller.getHandle()); } else { viewmodel = constructor.newInstance(controller.getHandle()); } viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller); return viewmodel; ... } 再看SavedStateHandleController .create(): SavedStateHandle在实例化时,通过consumeRestoredStateForKey()传入上次保存的数据,此时SavedStateHandle数据已完成恢复。SavedStateHandleController创建后,通过attachToLifecycle()方法,在方法内部将SavedStateHandle维护的SavedStateProvider注册到SavedStateRegistry。 前面说到SavedStateViewModelFactory是框架骨架之一,实际就是通过这个过程,最终允许VM能够把数据的存取加入到SaveInstanceState流程。 SavedStateHandleController.java ------------------------------- static SavedStateHandleController create(SavedStateRegistry registry, Lifecycle lifecycle, String key, Bundle defaultArgs) { // 通过consumeRestoredStateForKey获取前次状态 Bundle restoredState = registry.consumeRestoredStateForKey(key); // 创建一个Handle SavedStateHandle handle = SavedStateHandle.createHandle(restoredState, defaultArgs); // 生成SavedStateHandleController实例 SavedStateHandleController controller = new SavedStateHandleController(key, handle); // 这里进行了绑定 controller.attachToLifecycle(registry, lifecycle); tryToAddRecreator(registry, lifecycle); return controller; } void attachToLifecycle(SavedStateRegistry registry, Lifecycle lifecycle) { registry.registerSavedStateProvider(mKey, mHandle.savedStateProvider()); } 至此,SavedStateProvider和SavedStateRegistry实现关联。 * 以上就是整个保存流程的概况,而还原流程差距不大,只是在onCreate()中调用SavedStateRegistryController.performRestore(),最终通知SavedStateRegistry在Bundle中恢复设置的数据。 * 忽略SavedStateHandleController对VM的确保机制 (包含Recreator),最后总结: 简化称呼以缩短阅读量: -- VM组: `ViewModel` = `VM` `SavedStateViewModelFactory` = `Factory` -- Registry组: `SavedStateRegistryController` = `RegistryController` `SavedStateRegistry` = `Registry` -- Handle组: `SavedStateHandleController` = `HandleController` `SavedStateHandle` = `Handle` -- Provider组: `SavedStateProvider` = `Provider` -- 初始化阶段: `ComponentActivity`创建了`RegistryController`实例 `RegistryController`创建了内部的`Registry`实例 `ComponentActivity`创建了`Factory`,并传入`RegistryController`维护的`Registry` 恢复阶段 `ComponentActivity`在`onCreate`调用`RegistryController`的`performRestore` `RegistryController`的`performRestore`中,调用`Registry`的`performRestore` `Registry`的`performRestore`中,把数据从`savedInstanceState (Bundle)`取出,并储存到`mRestoredState`中 ***在此处`Registry`已把数据恢复*** VM生成阶段: `VM`需要通过`Factory`创建,创建过程: -- 创建`VM`对应的`HandleController`实例,创建过程: ------ 通过`Registry`的`consumeRestoredStateForKey`把数据取出生成`Handle` ------ `Handle`内部维护一个`Provider`实例,共享数据容器`mRegular` ------ `HandleController`被注入`Handle` ------ `Provider`绑定到`Registry` ------ ***在此处实现了 (`RegistryController`-`Registry`-`Provider`-`Handle`) 绑定*** --`VM`被`Factory`注入`Handle` --***在此处实现了 (`RegistryController`-`Registry`-`Provider`-`Handle-VM`) 绑定*** --***`VM`数据最终通过`Registry`恢复*** 写数据阶段 `VM`读写的数据最终存放到`Handle`的`mRegular`中 ***`VM`数据最终通过`Registry`保存*** 保存阶段 `ComponentActivity`在`onSaveInstanceState`调用`RegistryController`的`performSave` `RegistryController`的`performSave`中,调用`Registry`的`performSave` `Registry`的`performSave`中,将所有注册的`Provider`数据打包成一个`Bundle`保存到`onSaveInstanceState`的`Bundle`中 ***在此处`Registry`已把数据保存*** 作者:七零八落问号链接:https://www.jianshu.com/p/4a65ee05e6a1
大家好!给大家介绍一下,这是我们持续更新整理的2017-2020字节跳动Android面试真题解析! 早在2017年我们就建了第一个字节跳动的面试群给大家讨论面试的东西。期间累计有1825个群友分享了自己的Android面试真经,并提供了参考答案。 这其中就有很多成员已经斩获今日头条、抖音等岗位的offer。当然也有很多成员面试虽然失败了,但也分享了很多失败的经验教训。在这里一并对他们表示感谢!正是因为大家的奉献和支持,让我们的这份面试真题解析已经累计下载1082万次! 今年虽然受疫情影响,大部分同行都放慢和减缓了跳槽的计划和节奏,可仍有很多年前已经辞职的朋友,这部分朋友需要面临岗位减少、空前的竞争压力和心理压力的影响。 但是字节跳动2020春招依然火热,我的个人博客也收到了很多朋友的私信,让我们出字节跳动最新、最全的Android岗位面试题。疫情期间正好有时间,我们就收集整理出涵盖群友、以及网上大部分的字节跳动面试题以及答案给大家。 收集反馈的面经资料比较乱,全是针对字节跳动的面试题整理的,我们进行了分类,循序渐进,由基础到深入,由易到简。将内容整理成了五个章节、计算机基础面试题、数据结构和算法面试题、Java面试题、Android面试题、其他扩展面试题、非技术面试题总共6个章节354页。 字节跳动Android面试真题解析目录如下: 第一章计算机基础面试题 1 第一节、网络面试题 1 第二节、操作系统面试题 (⭐⭐⭐) 21 第三节、数据库面试题 (⭐) 23 第二章 数据结构和算法面试题 25 数据结构与算法 25 第三章Java面试题 33 第一节Java基础面试题 33 第二节 Java并发面试题 81 第三节Java虚拟机面试题 (⭐⭐⭐) 121 第四章 Android面试题 140 第一节 Android基础面试题 (⭐⭐⭐) 140 第二节Android高级面试题 (⭐⭐⭐) 208 第五章 其他扩展面试题 346 一、Kotlin (⭐⭐) 346 二、大前端 (⭐⭐) 346 三、脚本语言 (⭐⭐) 349 第六章非技术面试题 350 一、高频题集 (⭐⭐⭐) 350 二、次高频题集 (⭐⭐) 352 每个问题我们都附上1个标准参考答案,都是我们反复摸索消化(真心花了很多时间),觉得写的比较好的文章作为答案。这样就可以节省大家自己去搜索的时间,把时间用在正确的东西上。 其实我们也可以直接以简易的、群友分享的答案写出来,但是这并帮助不了同学们去深刻理解,三思之下还是采用标准答案作为参考。不明白或者想通俗了解的,可以点击我一起讨论,加入我们字节跳动Android面试群给大家讨论长篇or精简的答案,希望大家理解。下面是我们每章知识点的概述: 第一章 计算机基础面试题字节跳动面试也会考察计算机基础,主要考察我们是否系统的学习了操作系统和计算机组成原理,因为只有我们看完操作系统后才能系统的认识计算机的原理。 第二章 数据结构和算法面试题 对于算法面试准备,无疑就是刷《剑指Offer》+ LeetCode 效果最佳。刷《剑指Offer》是为了建立全面的算法面试思维,打下坚实的基础,刷LeetCode则是为了不断强化与开阔我们自己的算法思想。这两块 CS-Notes 中已经实现地很完美了,建议大家将《剑指Offer》刷完,然后再至少刷100道LeetCode题目以上。 第三章 Java面试题Java 是 Android App 开发默认的语言, Android Framework 也是默认使用 Java 语言,熟练掌握 Java 语言是 Android 开发者的必备技能。当然也是我们字节跳动青睐的考题选择方向! 第四章 Android面试题Android面试分为基础面试题+高级面试题两个部分。其中高级面试题部分的性能优化、Framework、三方源码属于我们考察的重点、难点方向! 第五章、第六章 其他扩展面试题+非技术面试题 Google 几年前就开始走 “Kotlin First” 的路线,目前很多官方的文档和 Demo 都是使用 Kotlin 语言作为默认,Kotlin 的重要性不言而喻。下载地址:2017-2020字节跳动Android面试真题解析 简历制作+春招困惑解答+经典HR面试解析 以上是我们整理总结字节跳动Android面试遇到的历年真题解析,希望对大家有帮助;同时我们经常也会遇到很多关于简历制作,职业困惑、HR经典面试问题回答等有关面试的问题。同样的我们搜集整理了全套简历制作、春招困惑、HR面试等问题解析参考建议。 上述字节跳动面试真题解析&简历制作PDF模板可以点赞+私信我免费获取! 分享不易!喜欢的朋友别忘了关注+点赞!
疫情还没结束,在你以为今年春招将推迟的时候,刚刚发现很多大厂已经在发面试内推通知了!字节跳动、华为、阿里巴巴等名企近期都已开始面试!相比往年春招,今年时间紧、节奏快! 很多企业笔试一结束就会紧跟着发出面试,留给你准备的时间非常有限! 据往年数据,大厂面试淘汰率平均在80%以上,线上面试/专业面/BOSS面/HR面轮番轰炸 想通关?首先你要避开面试中的那些高频雷区! 本文对程序员面试中经常被问到的一些典型项目人事等综合问题进行了整理,并给出相应的回答思路和参考答案。读者无需过分关注分析的细节,关键是要从这些分析中“悟”出面试的规律及回答问题的思维方式,达到“活学活用”。 问题1:“请你自我介绍一下” 思路: 1、这是面试的必考题目。 2、介绍内容要与个人简历相一致。 3、表述方式上尽量口语化。 4、要切中要害,不谈无关、无用的内容。 5、条理要清晰,层次要分明。 6、事先最好以文字的形式写好背熟。 问题2:“你有什么业余爱好?” 思路: 1、 业余爱好能在一定程度上反映应聘者的性格、观念、心态,这是招聘单位问该问题的主要原因。 2、 最好不要说自己没有业余爱好。 3、 不要说自己有那些庸俗的、令人感觉不好的爱好。 4、 最好不要说自己仅限于读书、听音乐、上网,否则可能令面试官怀疑应聘者性格孤僻。 5、 最好能有一些户外的业余爱好来“点缀”你的形象。 问题3:“谈谈你的缺点” 思路: 1、 不宜说自己没缺点。 2、 不宜把那些明显的优点说成缺点。 3、 不宜说出严重影响所应聘工作的缺点。 4、 不宜说出令人不放心、不舒服的缺点。 5、 可以说出一些对于所应聘工作“无关紧要”的缺点,甚至是一些表面上看是缺点,从工作的角度看却是优点的缺点。 问题4:“谈一谈你的一次失败经历” 思路: 1、 不宜说自己没有失败的经历。 2、 不宜把那些明显的成功说成是失败。 3、 不宜说出严重影响所应聘工作的失败经历, 4、 所谈经历的结果应是失败的。 5、 宜说明失败之前自己曾信心白倍、尽心尽力。 6、 说明仅仅是由于外在客观原因导致失败。 7、 失败后自己很快振作起来,以更加饱满的热情面对以后的工作。 问题5:“你为什么选择我们公司?” 思路: 1、 面试官试图从中了解你求职的动机、愿望以及对此项工作的态度。 2、 建议从行业、企业和岗位这三个角度来回答。 3、 参考答案——“我十分看好贵公司所在的行业,我认为贵公司十分重视人才,而且这项工作很适合我,相信自己一定能做好。” 问题6:“项目开发中遇到的最大的一个难题和挑战,你是如何解决的。” 思路: 1、 不宜直接说出具体的困难,否则可能令对方怀疑应聘者不行。 2、 可以尝试迂回战术,说出应聘者对困难所持有的态度——“工作中出现一些困难是正常的,也是难免的,但是只要有坚忍不拔的毅力、良好的合作精神以及事前周密而充分的准备,任何困难都是可以克服的。” 问题7:“你为什么会离开上家公司?” 思路: 1、 最重要的是:应聘者要使找招聘单位相信,应聘者在过往的单位的“离职原因”在此家招聘单位里不存在。 2、 避免把“离职原因”说得太详细、太具体。 3、 不能掺杂主观的负面感受,如“太幸苦”、“人际关系复杂”、“管理太混乱”、“公司不重视人才”、“公司排斥我们某某的员工”等。 4、 但也不能躲闪、回避,如“想换换环境”、“个人原因”等。 5、 不能涉及自己负面的人格特征,如不诚实、懒惰、缺乏责任感、不随和等。 6、 尽量使解释的理由为应聘者个人形象添彩。 7、 如“我离职是因为这家公司倒闭。我在公司工作了三年多,有较深的感情。从去年始,由于市场形势突变,公司的局面急转直下。到眼下这一步我觉得很遗憾,但还要面对显示,重新寻找能发挥我能力的舞台。” 问题7:“你能为公司带来什么经济效益?” 思路: 1、 基本原则上“投其所好”。 2、 回答这个问题前应聘者最好能“先发制人”,了解招聘单位期待这个职位所能发挥的作用。 3、 应聘者可以根据自己的了解,结合自己在专业领域的优势来回答这个问题。 问题8:你对未来的职业规划? 思路:很多面试官都会问,“你的职业规划是什么?”这个问题往往会难倒很多求职者。今天我跟大家分享,求职者怎样回答,才能更给自己加分。(一)什么是职业规划? 想回答好这个问题,首先要清楚,什么是职业规划。职业规划(Career Planning)是指对职业生涯乃至人生进行持续的、系统的计划过程,它包括职业定位、目标设定和通道设计三个要素。 职业规划也叫“职业生涯规划”,规划的好坏可能将影响整个生命历程。 职业规划有三个要素: 1、个人内在要素,包括职业性格、兴趣、职业价值观等,也就是“我想做什么”;2、商业价值要素,包括已具备的知识,技能,经历,人脉,也就是“我能做什么”;3、外在环境要素,包括宏观产业、组织、家庭等方面,也就是“环境支持我做什么”; 综上,职业规划就是在综合分析与权衡的基础上,确定出一个人当下最适合的职业发展方向,并为实现这一目标做出有效的合理的安排、计划与努力。 所以说,谈到职业规划时,求职者要综合考虑到自己的兴趣、技能,职业的目标,以及为实现目标所需要的计划。 (二)提及职业规划,面试官想考核什么? 所谓“知己知彼,百战百胜。”要想知道怎样回复更合适,首先应该知道面试官这么问,是为了考核什么? 一般来说,面试官考核内容不外乎以下几点: 1、求职者对自我的认知2、求职者对岗位的了解程度,对职业的理解程度3、求职者的反应能力、逻辑能力和语言能力4、考察求职者工作的稳定性5、考察求职者的上进心、目标感和自我驱动力 对于应届毕业生来说,实习经验不多,社会阅历尚浅,对自己认知不足,很多想法都不成熟,让他们说清楚自己的职业规划,其实是一件很有难度的事情。在某些场合,求职者说的“内容”并不重要,“怎么说”才是更重要的。 大多数面试官主要是希望通过求职者的回答,了解求职者对自我的认知,对该行业的看法,对应聘岗位的认知,判断求职者的逻辑思维和语言表述能力,从而对求职者的性格和价值观有个大致的了解。 对于有一定工作经验的求职者来说,这个问题主要考核稳定性、上进心、目标感和驱动力。没有职业规划的人对自己定位不清晰,没有发展方向,很可能遇到问题就会退缩,一不开心就要辞职。没有自己目标的人,在工作业绩上也难出彩,他无法实现自我驱动,只能靠外在的动力来驱动,比如只能完成领导确定交代的任务,不能突破既定任务的天花板。而目标感强、自我驱动力强的人是能够充分利用资源、充分提升自我的,能够在工作任务之外也持续为这个职业目标而奋斗。 (三)如何回复面试官? 在了解面试官心理状态的基础上,回复职业规划问题可从以下几个角度考虑: 1、充分认知自己的性格、兴趣、爱好、特长、知识、能力等,并结合当下的环境,选择可以将个人爱好与职业发展结合起来的行业/职业。2、了解应聘公司的背景、现状与未来,在谈到规划时,可以适当的与公司发展相贴合。3、不要说“我想几年当主管,几年当经理”,这种毫无意义的答案。职业规划更应该考虑专业技能方面的提升计划和步骤,而不是仅仅在于职级提升。4、说明自己有长远规划的能力,但在表述中主要着眼于最近的3年5年即可,说明自己当下会努力做好应聘的岗位。5、一个有竞争力的应聘者对于职业规划问题一定要有清晰的想法,大的方向和短期的目标必须明确,同时尽可能给自己预留调整的空间。 根据以上原则,对于职业规划这个问题的回复,可以参考下面这个表述: “感谢你提出这么深刻的问题。我的兴趣是XXX,优势是XXX,因此我选择了XXX行业/职业,这是一个可以将我的兴趣和工作结合起来的行业,是我非常喜欢的,所以我会很用心对待XXX岗位。“ “说到职业规划,近期三到五年,我打算在XXX行业做到XXX,希望可以稳定提升,持续学到更多的知识,后续可以在XXX行业/XXX岗位独当一面,独立负责XXXX事务,解决XXXX问题。” “谈到远期规划,我会根据环境的变化,工作内容的变化,以及我自身能力的变化,不断进行调整的。对于职业规划,我暂时的考虑是这样子的。谢谢!”节选自:面试求职那些事儿 文末 对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们! 近期我们搜集了 N 套阿里、腾讯、美团、网易等公司 19 年的面试题,把技术点梳理成一份大而全的“Android高级工程师”面试题库(实际上比预期多花了不少精力),包含标准答案解析,由于篇幅有限,这里以图片的形式给大家展示一部分。 这份资料尤其适合: 1.近期想跳槽,要面试的Android程序员,查漏补缺,以便尽快弥补短板; 2.想了解“一线互联网公司”最新技术要求,对比找出自身的长处和弱点所在,评估自己在现有市场上的竞争力如何; 3.做了几年Android开发,但还没形成系统的Android知识体系,缺乏清晰的提升方向和学习路径的程序员。 相信它会给大家带来很多收获。++++维信,(壹叁贰零叁壹陆叁陆零玖)就可以免费领取了 除面试资料外,这里还整理了一份最近刚录制的视频——BAT大牛解秘Android面试,对于面试,是个不错的补充。 视频围绕“BAT大牛解密Android面试?”的主题,内容由浅入深,同时,对于开源框架相关面试问题也作出重点解读。 视频具体内容如下: 第1章 课程介绍 第2章 一线互联网公司初中高Android开发工程师的技能要求 第3章 Android基础相关面试题 第4章 异步消息处理机制相关面试问题 第5章 View相关面试问题 第6章 Android项目构建相关面试问题 第7章 开源框架相关面试问题 第8章 Android异常与性能优化相关面试问题 第9章 热门前沿知识相关面试问题 需要获取更全面的面试资料,或专题视频,++++维信:(壹叁贰零叁壹陆叁陆零玖)。前往免费领取!
**编写高质量可维护的代码既是程序员的基本修养,也是能决定项目成败的关键因素,本文试图总结出问题项目普遍存在的共性问题并给出相应的解决方案。 1. 程序员的宿命? 程序员的职业生涯中难免遇到烂项目,有些项目是你加入时已经烂了,有些是自己从头开始亲手做成了烂项目,有些是从里到外的烂,有些是表面光鲜等你深入进去发现是个“焦油坑”,有些是此时还没烂但是已经出现问题征兆走在了腐烂的路上。 国内基本上是这样,国外情况我了解不多,不过从英文社区和技术媒体上老外同行的抱怨程度看,应该是差不多的,虽然整体素质可能更高,但是也因更久的信息化而积累了更多问题。毕竟“焦油坑、Shit_Mountain 屎山”这些舶来的术语不是无缘无故被发明出来的。 Any way,这大概就是我们这个行业的宿命——要么改行,要么就是与烂项目烂代码长相伴。就像宇宙的“熵增加定律”一样: 孤立系统的一切自发过程均向着令其状态更无序的方向发展,如果要使系统恢复到原先的有序状态是不可能的,除非外界对它做功。 面对这宿命的阴影,有些人认命了麻木了,逐渐对这个行业失去热情。 那些不认命的选择与之抗争,但是地上并没有路,当年软件危机的阴云也从未真正散去,人月神话仍然是神话,于是人们做出了各自不同的判断和尝试: 掀桌子另起炉灶派: 很多人把项目做烂的原因归咎于项目前期的基础没打好、需求不稳定一路打补丁、前面的架构师和程序员留下的烂摊子难以收拾。 他们要么没有信心去收拾烂摊子,要么觉得这是费力不讨好,于是要放弃掉项目,寄希望于出现一个机会能重头再来。 但是他们对于如何避免重蹈覆辙、做出另一个烂项目是没有把握也没有深入思考的,只是盲目乐观的认为自己比前任更高明。 激进改革派: 这个派别把原因归结于烂项目当初没有采用正确的编程语言、最新最强大的技术栈或工具。 他们中一部分人也想着有机会另起炉灶,用上时下最流行最热门的技术栈(spring boot、springcloud、redis、nosql、docker、vue)。 或者即便不另起炉灶,也认为现有技术栈太过时无法容忍了(其实可能并不算过时),不用微服务不用分布式就不能接受,于是激进的引入新技术栈,鲁莽的对项目做大手术。 这种对刚刚流行还不成熟技术的盲目跟风、技术选型不慎重的情况非常普遍,今天在他们眼中落伍的技术栈,其实也不过是几年前另一批人赶的时髦。 我不反对技术上的追新,但是同样的,这里的问题是:他们对于大手术的风险和副作用,对如何避免重蹈覆辙用新技术架构做出另一个烂项目,没有把握也没有深入思考的,只是盲目乐观的认为新技术能带来成功。 也没人能阻止这种简历驱动的技术选型浮躁风气,毕竟花的是公司的资源,用新东西显得自己很有追求,失败了也不影响简历美化,简历上只会增加一段项目履历和几种精通技能,不会提到又做烂了一个项目,名利双收稳赚不赔。 保守改良派: 还有一类人他们不愿轻易放弃这个有问题但仍在创造效益的项目,因为他们看到了项目仍然有维护的价值,也看到了另起炉灶的难度(万事开头难,其实项目的冷启动存在很多外部制约因素)、大手术对业务造成影响的代价、系统迁移的难度和风险。 同时他们尝试用温和渐进的方式逐步改善项目质量,采用一系列工程实践(主要包括重构热点代码、补自动化测试、补文档)来清理“技术债”,消除制约项目开发效率和交付质量的瓶颈。 如果把一个问题项目比作病入膏肓的病人,那么这三种做法分别相当于是放弃治疗、截肢手术、保守治疗。 2. 一个 35+ 程序员的反思 年轻时候我也是掀桌子派和激进派的,新工程新框架大开大合,一路走来经验值技能树蹭蹭的涨,跳槽加薪好不快活。 但是近几年随着年龄增长,一方面新东西学不动了,另一方面对经历过的项目反思的多了观念逐渐改变了。 对我触动最大的一件事是那个我在 2016 年初开始从零搭建起的项目,在我 2018 年底离开的时候(仅从代码质量角度)已经让我很不满意了。只是,这一次没有任何借口了: 从技术选型到架构设计到代码规范,都是我自己做的,团队不大,也是我自己组建和一手带出来的; 最开始的半年进展非常顺利,用着我最趁手的技术和工具一路狂奔,年底前替换掉了之前采购的那个垃圾产品(对的,有个前任在业务上做参照也算是个很大的有利因素); 做的过程我也算是全力以赴,用尽毕生所学——前面 13 年工作的经验值和走过的弯路、教训,使得公司只用其它同类公司同类项目 20% 的资源就把平台做起来了; 如果说多快好省是最高境界,那么当时的我算是做到了多、快、省——交付的功能非常丰富且贴近业务需求、开发节奏快速、对公司开发资源很节省; 但是现在看来,“好”就远远没有达到了,到了项目中期,简单优先级高的需求都已经做完了,公司业务上出现了新的挑战——接入另一个核心系统以及外部平台,真正的考验来了。 那个改造工程影响面比较大,需要对我们的系统做大面积修改,最麻烦的是这意味着从一个简单的单体系统变成了一个分布式的系统,而且业务涉及资金交易,可靠性要求较高,是难上加难。 于是问题开始出现了:我之前架构的优点——简单直接——这个时候不再是优点了,简单直接的架构在业务环境、技术环境都简单的情况下可以做到多快好省,但是当业务、技术环境都陡然复杂起来时,就不行了; 具体的表现就是:架构和代码层面的结构都快速的变得复杂、混乱起来了——熵急剧增加; 后面的事情就一发不可收拾:代码改起来越来越吃力、测试问题变多、生产环境故障和问题变多、于是消耗在排查测试问题生产问题和修复数据方面的精力急剧增加、出现恶性循环。。。 到了这个境地,项目就算是做烂了!一个我从头开始做起的没有任何借口的失败! 于是我意识到一个非常浅显的道理:拥有一张空白的画卷、一支最高级的画笔、一间专业的画室,无法保证你可以画出美丽的画卷。如果你不善于画画,那么一切都是空想和意淫。 然后我变成了一个“保守改良派”,因为我意识到掀桌子和激进的改革都是不负责任的,说不好听的那样其实是掩耳盗铃、逃避困难,人不可能逃避一辈子,你总要面对。 即便掀了桌子另起炉灶了,你还是需要找到一种办法把这个新的炉灶烧好,因为随着项目发展之前的老问题还是会一个一个冒出来,还是需要面对现实、不逃避、找办法。 面对问题不仅有助于你把当前项目做好,也同样有助于将来有新的项目时更好的把握住机会。 无论是职业生涯还是自然年龄,人到了这个阶段都开始喜欢回顾和总结,也变得比过去更在乎项目、产品乃至公司的商业成败。 软件开发作为一种商业活动,判断其成败的依据应该是:能否以可接受的成本、可预期的时间节奏、稳定的质量水平、持续交付满足业务需要的功能市场需要的产品。 其实就是项目管理四要素——成本、进度、范围、质量,传统项目管理理论认为这四要素彼此制约难以兼得,项目管理的艺术在于四要素的平衡取舍。 关于软件工程和项目管理的理论和著作已经很多很成熟,这里我从程序员的视角提出一个新的观点——质量不可妥协: 质量要素不是一个可以被牺牲和妥协的要素——牺牲质量会导致其它三要素全都受损,反之同理,追求质量会让你在其它三个方面同时受益。 在保持一个质量水平的前提下,成本、进度、范围三要素确确实实是互相制约关系——典型的比如牺牲成本(加班加点)来加快进度交付急需的功能。 正如著名的“破窗效应”所启示的那样:任何一种不良现象的存在,都在传递着一种信息,这种信息会导致不良现象的无限扩展,同时必须高度警觉那些看起来是偶然的、个别的、轻微的“过错”,如果对这种行为不闻不问、熟视无睹、反应迟钝或纠正不力,就会纵容更多的人“去打烂更多的窗户玻璃”,就极有可能演变成“千里之堤,溃于蚁穴”的恶果——质量不佳的代码之于一个项目,正如一扇破了的窗之于一幢建筑、一个蚂蚁巢之于一座大堤。 好消息是,只要把质量提上去项目就会逐渐走上健康的轨道,其它三个方面也都会改善。管好了质量,你就很大程度上把握住了项目成败的关键因素。 坏消息是,项目的质量很容易失控,现实中质量不佳、越做越臃肿混乱的项目比比皆是,质量改善越做越好的案例闻所未闻,以至于人们将其视为如同物理学中“熵增加定律”一样的必然规律了。 当然任何事情都有一个度的问题,当质量低于某个水平时才会导致其它三要素同时受损。反之当质量高到某个水平以后,继续追求质量不仅得不到明显收益,而且也会损害其它三要素——边际效用递减定律。 这个度需要你为自己去评估和测量,如果目前的质量水平还在两者之间,那么就应该重点改进项目质量。当然,现实世界中很少看到哪个项目质量高到了不需要重视的程度。 3. 项目走向衰败的最常见诱因——代码质量不佳 一个项目的衰败一如一个人健康状况的恶化,当然可能有多种多样的原因——比如需求失控、业务调整、人员变动流失。但是作为我们技术人,如果能做好自己分内的工作——编写出可维护的代码、减少技术债利息成本、交付一个健壮灵活的应用架构,那也绝对是功德无量的。 虽然很难估算出这究竟能挽救多少项目,但是在我十多年职业生涯中,经历的和近距离观察的几十个项目,确实看到了大量的项目正是由于代码质量不佳导致的失败和遗憾,同时我也发现其实失败项目的很多问题、症结也确确实实都可以归因到项目代码的混乱和质量低下,比如一个常见的项目腐烂恶性循环:代码乱》bug 多》排查问题耗时》复用度低》加班 996》士气低落…… 所谓“千里之堤,毁于蚁穴”,代码问题就是蚁穴。 接下来,让我们从项目管理聚焦到项目代码质量这个相对小的领域来深入剖析。编写高质量可维护的代码是程序员的基本修养,本文试图在代码层面找到一些失败项目中普遍存在的症结问题,同时基于个人十几年开发经验总结出的一些设计模式作为药方分享出来。 关于代码质量的话题其实很难通过一篇文章阐述明白,甚至需要一本书的篇幅,里面涉及到的很多概念关注点之间存在复杂微妙关系。 推荐《设计模式之美》的第二章节《从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?》,这是我看到的关于代码质量主题最精彩深刻的论述。 4. 一个失败项目复盘 先贴几张代码截图,看一下这个重病缠身的项目的病灶和症状: 这是该项目中一个最核心、最复杂也是最经常要被改动的 class,代码行数 4881; 结果就是冗长的 API 列表(列表需要滚动 4 屏才能到底,公有私有 API 180 个); 还是那个 Class,头部的 import 延绵到了 139 行,去掉第一行 package 声明和少量空行总共 import 引入了 130 个 class! 还是那个坑爹的组件,从 156 行开始到 235 行声明了 Spring 依赖注入的组件 40 个! 这里先不去分析这个类的问题,只是初步展示一下病情严重程度。 我相信这应该不算是特别糟糕的情况,比这个严重的项目俯拾皆是,但是这也应该足够拿来暴露问题、剖析成因了。 4.1 症结 1:组件粒度过大、API 泛滥 分层的理念早已深入人心,尤其是业务逻辑层的独立,彻底杜绝了之前(不分层的年代)业务逻辑与展现逻辑、持久化逻辑等混杂的问题。 但是好景不长,随着业务的复杂和变更,在业务逻辑层的复杂性也急剧增加,成为了新的开发效率瓶颈, 问题就出在了业务逻辑组件的划分方式——按领域模型划分业务逻辑组件: 业界关于如何设计业务逻辑层 并没有标准和最佳实践,绝大多数项目(我自己经历过的项目以及我有机会深入了解的项目)中大家都是想当然的按照业务领域对象来设计; 例如:领域实体对象有 Account、Order、Delivery、Campaign。于是业务逻辑层就设计出 AccountService、OrderService、DeliveryService、CampaignService 这种做法在项目简单是没什么问题,事实上项目简单时 你随便怎么设计都问题不大。 但是当项目变大和复杂以后,就会出现问题了: 组件臃肿:Service 组件的个数跟领域实体对象个数基本相当,必然造成个别 Service 组件变得非常臃肿——API 非常多,代码行数达到几千行; 职责模糊:业务逻辑往往跨多个领域实体,无论放在哪个 Service 都不合适,同样的,要找一个功能的实现逻辑也无法确定在哪个 Service 中; 代码重复 or 逻辑纠缠的两难选择:当遇到一个业务逻辑,其中的某个环节在另一个业务逻辑 API 中已经实现,这时如果不想忍受重复实现和代码,就只能去调用那个 API。但这样就造成了业务逻辑组件之间的耦合与依赖,这种耦合与依赖很快会扩散——新的 API 又会被其它业务逻辑依赖,最终形成蜘蛛网一样的复杂依赖甚至循环依赖; 复用代码、减少重复虽然是好的,但是复杂耦合依赖的害处也很大——赶走一只狼引来了一只虎。两杯毒酒给你选! 前面截图的那个问题组件 ContractService 就是一个典型案例,这样的组件往往是热点代码以及整个项目的开发效率的瓶颈。 4.2 药方 1:倒金字塔结构——业务逻辑组件职责单一、禁止层内依赖 问题根源的反面其实就藏着解决方案,只是需要我们有意识的去改变习惯、遵循新的设计风格,而不是凭直觉去设计: 业务逻辑层应该被设计成一个个功能非常单一的小组件,所谓小是指 API 数量少、代码行数少; 由于职责单一因此必然组件数量多,每一个组件对应一个很具体的业务功能点(或者几个相近的); 复用(调用、依赖)只应该发生在相邻的两层之间——上层调用下层的 API 来实现对下层功能的复用; 于是系统架构就自然呈现出倒立的金字塔形状:越接近顶层的业务场景组件数量越多,越往下层的复用性高,于是组件数量越少。 4.3 症结 2:低内聚、高耦合 经典面向对象理论告诉我们,好的代码结构应该是“高内聚、低耦合”的: 高内聚:组件本身应该尽可能的包含其所实现功能的所有重要信息和细节,以便让维护者无需跳转到其它多个地方去了解必要的知识。 低耦合:组件之间的互相依赖和了解尽可能少,以便在一个组件需要改动时其它组件不受影响。 其实这两者就是一体两面,做到了高内聚基本也就做到了低耦合,相反如果内聚度很低,势必存在大量高耦合的组件。 我观察发现,很多项目都存在低内聚、高耦合的问题。根本原因在于很多程序员,甚至是很多经验丰富的程序员也缺少这方面的意识——对“内聚性”概念不甚清楚,对内聚性被破坏的危害没有意识,对如何避免更是无从谈起。 很多人从一开始就凭直觉写程序,有了一定经验以后一般能认识到重复代码的危害,对复用性有很强的认识,于是就会掉进一个陷阱——盲目追求复用,结果破坏了内聚性。 业界关于“复用性”的认识存在一个误区——认为包括业务逻辑组件在内的任何层面的组件都应该追求最大限度的可复用性; 复用当然是好的,但那应该有个前提条件:不增加系统复杂度的情况下的复用,才是好的。 什么样的复用会增加系统复杂性、是不好的呢?前面提到的,一个业务逻辑 API 被另一个业务逻辑 API 复用——就是不好的: 损害了稳定性:因为业务逻辑本身是跟现实世界的业务挂钩的,而业务会发生变化;当你复用一个会发生变化的 API,相当于在沙子上建高楼——地基是松动的; 增加了复杂性:这样的依赖还造成代码可读性降低——在一个本就复杂的业务逻辑代码中,包含了对另一个复杂业务逻辑的调用,复杂度会急剧增加,而且会不断泛滥和传递; 内聚性被破坏:由于业务逻辑被打散在了多个组件的方法内,变得支离破碎,无法在一个地方看清整体逻辑脉络和实现步骤——内聚性被破坏,同时也意味着,这个调用链条上涉及的所有组件之间存在高耦合。 4.4 药方 2:复用的两种正确姿势——打造自己的 lib 和 framework 软件架构中有两种东西来实现复用——lib 和 framework, lib 库是供你(应用程序)调用的,它帮你实现特定的能力(比如日志、数据库驱动、json 序列化、日期计算、http 请求)。 framework 框架是供你扩展的,它本身就是半个应用程序,定义好了组件划分和交互机制,你需要按照其规则扩展出特定的实现并绑定集成到其中,来完成一个应用程序。 lib 就是组合方式的复用,framework 则是继承式的复用,继承的 Java 关键字是 extends,所以本质上是扩展。 过去有个说法:“组合优于继承,能用组合解决的问题尽量不要继承”。我不同意这个说法,这容易误导初学者以为组合优于继承,其实继承才是面向对象最强大的地方,当然任何东西都不能乱用。 典型的继承乱用就是为了获得父类的某个 API 而去继承,继承一定是为了扩展,而不是为了直接获得一个能力,获得能力应该调用 lib,父类不应该去实现具体功能,那是 lib 该做的事。 也不应该为了使用 lib 而去继承 lib 中的 Class。lib 就是用来被组合被调用的,framework 就是用来被继承、扩展的。 再展开一下:lib 既可以是第三方的(log4j、httpclient、fastjson),也可是你自己工程的(比如你的持久层 Dao、你的 utils); framework 同理,既可以是第三方的(springmvc、jpa、springsecurity),也可以是你项目内封装的面向具体业务领域的(比如 report、excel 导出、paging 或任何可复用的算法、流程)。 从这个意义上说,一个项目中的代码其实只有 3 种:自定义的 lib class、自定义的 framework 相关 class、扩展第三方或自定义 framework 的组件 class。 再扩展一下:相对于过去,现在我们已经有了足够多的第三方 lib 和 framework 来复用,来帮助项目节省大量代码,开发工作似乎变成了索然无味、没技术含量的 CRUD。但是对于业务非常复杂的项目,则需要有经验、有抽象思维、懂设计模式的人,去设计面向业务的 framework 和面向业务的 lib,只有这样才能交付可维护、可扩展、可复用的软件架构——高质量架构,帮助项目或产品取得成功。 4.5 症结 3:抽象不够、逻辑纠缠——High Level 业务逻辑和 Low Level 实现逻辑纠缠 当我们说“代码中包含的业务逻辑”的时候,我们到底在说什么?业界并没有一个标准,大家经常讲的 CRUD 增删改查其实属于更底层的数据访问逻辑。 我的观点是:所谓代码中的业务逻辑,是指这段代码所表现出的所有输入输出规则、算法和行为,通常可以分为以下 5 类: 输入合法性校验; 业务规则校验:典型的如检查交易记录状态、金额、时限、权限等,通常包含数据库或外部接口的查询作为参考; 数据持久化行为:数据库、缓存、文件、日志等任何形式的数据写入行为; 外部接口调用行为; 输出/返回值准备。 当然具体到某一个组件实例,可能不会包括上述全部 5 类业务逻辑,但是也可能每一类业务逻辑存在多个。 单这样看你可能觉得并不是特别复杂,但是现实中上述 5 类业务逻辑中的每一个通常还包含着一到多个底层实现逻辑,如 CRUD 数据访问逻辑或第三方 API 的调用。 例如输入合法性校验,通常需要查询对应记录是否存在,外部接口调用前通常需要查询相关记录以获得调用接口需要的参数,调用接口后还需要根据结果更新相关记录状态。 显然这里存在两个 Level 的逻辑——High Level 的与业务需求对应且关联紧密的逻辑、Low Level 的实现逻辑。 如果对两个 Level 的逻辑不加以区分、混为一谈,代码质量立刻就会遭到严重损害: 可读性变差:两个维度的复杂性——业务复杂性和底层实现的技术复杂性——被掺杂在了一起,复杂度 1+1>2 剧增,给其他人阅读代码增加很大负担; 可维护性差:可维护性通常指排查和解决问题所需花费的代价高低,当两个 level 的逻辑纠缠在一起,会使排查问题变的更困难,修复问题时也更容易出错; 可扩展性无从谈起:扩展性通常指为系统增加一个特性所需花费的代价高低,代价越高扩展性越差;与排查修复问题类似,逻辑纠缠显然也会使添加新特性变得困难、一不小心就破坏了已有功能。 下面这段代码就是一个典型案例——High Level 的逻辑流程(参数获取、反序列化、参数校验、缓存写入、数据库持久化、更新相关交易记录)完全淹没在了 Low Level 的实现逻辑(字符串比较、Json 反序列化、redis 操作、dao 操作以及前后各种琐碎的参数准备和返回值处理)。下一节我会针对这段问题代码给出重构方案。 @Override 4.6 药方 3:控制逻辑分离——业务模板 Pattern of NestedBusinessTemplate 解决“逻辑纠缠”最关键是要找到一种隔离机制,把两个 Level 的逻辑分开——控制逻辑分离,分离的好处很多: 根据经验,当我们着手维护一段代码时,一定是想先弄清楚它的整体流程、算法和行为,而不是一上来就去研究它的细枝末节; 控制逻辑分离后,只需要去看 High Level 部分就能了解到上述内容,阅读代码的负担大幅度降低,代码可读性显著增强; 读懂代码是后续一切维护、重构工作的前提,而且一份代码被读的次数远远高于被修改的次数(高一个数量级),因此代码对人的可读性再怎么强调都不为过,可读性增强可以大幅度提高系统可维护性,也是重构的最主要目标。 同时,根据我的经验,High Level 业务逻辑的变更往往比 Low Level 实现逻辑变更要来的频繁,毕竟前者跟业务直接对应。当然不同类型项目情况不一样,另外它们发生变更的时间点往往也不同; 在这样的背景下,控制逻辑分离的好处就更明显了:每次维护、扩充系统功能只需改动一个 Levle 的代码,另一个 Level 不受影响或影响很小,这会大幅降低修改成本和风险。 我在总结过去多个项目中的教训和经验后,总结出了一项最佳实践或者说是设计模式——业务模板 Pattern of NestedBusinessTemplat,可以非常简单、有效的分离两类逻辑,先看代码: public class XyzService { @SuppressWarnings({ "unchecked", "rawtypes" }) 如果你熟悉经典的 GOF23 种设计模式,很容易发现上面的代码示例其实就是 Template Method 设计模式的运用,没什么新鲜的。 没错,我这个方案没有提出和创造任何新东西,我只是在实践中偶然发现 Template Method 设计模式真的非常适合解决广泛存在的逻辑纠缠问题,而且也发现很少有程序员能主动运用这个设计模式;一部分原因可能是意识到“逻辑纠缠”问题的人本就不多,同时熟悉这个设计模式并能自如运用的人也不算多,两者的交集自然就是少得可怜;不管是什么原因,结果就是这个问题广泛存在成了通病。 我看到一部分对代码质量有追求的程序员 他们的解决办法是通过"结构化编程"和“模块化编程”: 把 Low Level 逻辑提取成 private function,被 High Level 代码所在的 function 直接调用; 问题 1 硬连接不灵活:首先,这样虽然起到了一定的隔离效果,但是两个 level 之间是静态的硬关联,Low Level 无法被简单的替换,替换时还是需要修改和影响到 High Level 部分; 问题 2 组件内可见性造成混乱:提取出来的 private function 在当前组件内是全局可见的——对其它无关的 High Level function 也是可见的,各个模块之间仍然存在逻辑纠缠。这在很多项目中的热点代码中很常见,问题也很突出:试想一个包含几十个 API 的组件,每个 API 的 function 存在一两个关联的 private function,那这个组件内部的混乱程度、维护难度是难以承受的。 把 Low Level 逻辑抽取到新的组件中,供 High Level 代码所在的组件依赖和调用;更有经验的程序员可能会增加一层接口并且借助 Spring 依赖注入; 问题 1 API 泛滥:提取出新的组件似乎避免了“结构化编程”的局限性,但是带来了新的问题——API 泛滥:因为组件之间调用只能走 public 方法,而这个 API 其实没有太多复用机会根本没必要做成 public 这种最高可见性。 问题 2 同层组件依赖失控:组件和 API 泛滥后必然导致组件之间互相依赖成为常态,慢慢变得失控以后最终变成所有组件都依赖其它大部分组件,甚至出现循环依赖;比如那个拥有 130 个 import 和 40 个 Spring 依赖组件的 ContractService。 下面介绍一下 Template Method 设计模式的运用,简单归纳就是: High Level逻辑封装在抽象父类AbsUpdateFromMQ的一个final function中,形成一个业务逻辑的模板; final function保证了其中逻辑不会被子类有意或无意的篡改破坏,因此其中封装的一定是业务逻辑中那些相对固定不变的东西。至于那些可变的部分以及暂时不确定的部分,以abstract protected function形式预留扩展点; 子类(一个匿名内部类)像“做填空题”一样,填充模板实现Low Level逻辑——实现那些protected function扩展点;由于扩展点在父类中是abstract的,因此编译器会提醒子类的程序员该扩展什么。 那么它是如何避免上面两个方案的 4 个局限性的: Low Level 需要修改或替换时,只需从父类扩展出一个新的子类,父类全然不知无需任何改动; 无论是父类还是子类,其中的 function 对外层的 XyzService 组件都是不可见的,即便是父类中的 public function 也不可见,因为只有持有类的实例对象才能访问到其中的 function; 无论是父类还是子类,它们都是作为 XyzService 的内部类存在的,不会增加新的 java 类文件更不会增加大量无意义的 API(API 只有在被项目内复用或发布出去供外部使用才有意义,只有唯一的调用者的 API 是没有必要的); 组件依赖失控的问题当然也就不存在了。 SpringFramework 等框架型的开源项目中,其实早已大量使用 Template Method 设计模式,这本该给我们这些应用开发程序员带来启发和示范,但是很可惜业界没有注意到和充分发挥它的价值。 NestedBusinessTemplat 模式就是对其充分和积极的应用,前面一节提到过的复用的两种正确姿势——打造自己的 lib 和 framework,其实 NestedBusinessTemplat 就是项目自身的 framework。 4.7 症结 4:无处不在的 if else 牛皮癣 无论你的编程启蒙语言是什么,最早学会的逻辑控制语句一定是 if else,但是不幸的是它在你开始真正的编程工作以后,会变成一个损害项目质量的坏习惯。 几乎所有的项目都存在 if else 泛滥的问题,但是却没有引起足够重视警惕,甚至被很多程序员认为是正常现象。 首先我来解释一下为什么 if else 这个看上去人畜无害的东西是有害的、是需要严格管控的: if else if ...else 以及类似的 switch 控制语句,本质上是一种 hard coding 硬编码行为,如果你同意“magic number 魔法数字”是一种错误的编程习惯,那么同理,if else 也是错误的 hard coding 编程风格; hard coding 的问题在于当需求发生改变时,需要到处去修改,很容易遗漏和出错; 以一段代码为例来具体分析: if ("3".equals(object.getString("type"))){ if ("3".equals(object.getString("type"))) 显然这里的"3"是一个 magic number,没人知道 3 是什么含义,只能推测; 但是仅仅将“3”重构成常量 ABC_XYZ 并不会改善多少,因为 if (ABC_XYZ.equals(object.getString("type"))) 仍然是面向过程的编程风格,无法扩展; 到处被引用的常量 ABC_XYZ 并没有比到处被 hard coding 的 magic number 好多少,只不过有了含义而已; 把常量升级成 Enum 枚举类型呢,也没有好多少,当需要判断的类型增加了或判断的规则改变了,还是需要到处修改——Shotgun Surgery(霰弹式修改) 并非所有的 if else 都有害,比如上面示例中的 if (list1 !=null) { 就是无害的,没有必要去消除,也没有消除它的可行性。判断是否有害的依据: 如果 if 判断的变量状态只有两种可能性(比如 boolean、比如 null 判断)时,是无伤大雅的; 反之,如果 if 判断的变量存在多种状态,而且将来可能会增加新的状态,那么这就是个问题; switch 判断语句无疑是有害的,因为使用 switch 的地方往往存在很多种状态。 4.8 药方 4:充血枚举类型——Rich Enum Type 正如前面分析呈现的那样,对于代码中广泛存在的状态、类型 if 条件判断,仅仅把被比较的值重构成常量或 enum 枚举类型并没有太大改善——使用者仍然直接依赖具体的枚举值或常量,而不是依赖一个抽象。 于是解决方案就自然浮出水面了:在 enum 枚举类型基础上进一步抽象封装,得到一个所谓的“充血”的枚举类型,代码说话: 实现多种系统通知机制,传统做法: enum NOTIFY_TYPE { email,sms,wechat; } //先定义一个enum——一个只定义了值不包含任何行为的“贫血”的枚举类型 实现多种系统通知方式,充血枚举类型——Rich Enum Type 模式: enum NOTIFY_TYPE { //1、定义一个包含通知实现机制的“充血”的枚举类型 充血枚举类型——Rich Enum Type 模式的优势: 不难发现,这其实就是 enum 枚举类型和 Strategy Pattern 策略模式的巧妙结合运用; 当需要增加新的通知方式时,只需在枚举类 NOTIFY_TYPE 增加一个值,同时在策略接口 NotifyMechanismInterface 中增加一个 by 方法返回对应的策略实现; 当需要修改某个通知机制的实现细节,只需修改 NotifyMechanismInterface 中对应的策略实现; 无论新增还是修改通知机制,调用方完全不受影响,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg); 与传统 Strategy Pattern 策略模式的比较优势:常见的策略模式也能消灭 if else 判断,但是实现起来比较麻烦,需要开发更多的 class 和代码量: 每个策略实现需单独定义成一个 class; 还需要一个 Context 类来做初始化——用 Map 把类型与对应的策略实现做映射; 使用时从 Context 获取具体的策略; Rich Enum Type 的进一步的充血: 上面的例子中的枚举类型包含了行为,因此已经算作充血模型了,但是还可以为其进一步充血; 例如有些场景下,只是要对枚举值做个简单的计算获得某种 flag 标记,那就没必要把计算逻辑抽象成 NotifyMechanismInterface 那样的接口,杀鸡用了牛刀; 这时就可以在枚举类型中增加 static function 封装简单的计算逻辑; 策略实现的进一步抽象: 当各个策略实现(byEmail bySms byWechat)存在共性部分、重复逻辑时,可以将其抽取成一个抽象父类; 然后就像前一章节——业务模板 Pattern of NestedBusinessTemplate 那样,在各个子类之间实现优雅的逻辑分离和复用。 5. 重构前的火力侦察:为你的项目编制一套代码库目录/索引——CODEX 以上就是我总结出的最常见也最影响代码质量的 4 个问题及其解决方案: 职责单一、小颗粒度、高内聚、低耦合的业务逻辑层组件——倒金字塔结构; 打造项目自身的 lib 层和 framework——正确的复用姿势; 业务模板 Pattern of NestedBusinessTemplate——控制逻辑分离; 充血的枚举类型 Rich Enum Type——消灭硬编码风格的 if else 条件判断; 接下来就是如何动手去针对这 4 个方面进行重构了,但是事情还没有那么简单。 上面所有的内容虽然来自实践经验,但是要应用到你的具体项目,还需要一个步骤——火力侦察——弄清楚你要重构的那个模块的逻辑脉络、算法以致实现细节,否则贸然动手,很容易遗漏关键细节造成风险,重构的效率更难以保证,陷入进退两难的尴尬境地。 我 2019 年一整年经历了 3 个代码十分混乱的项目,最大的收获就是摸索出了一个梳理烂代码的最佳实践——CODEX: 在阅读代码过程中,在关键位置添加结构化的注释,形如://CODEX ProjectA 1 体检预约流程 1 预约服务 API 入口 所谓结构化注释,就是在注释内容中通过规范命名的编号前缀、分隔符等来体现出其所对应的项目、模块、流程步骤等信息,类似文本编辑中的标题 1、2、3; 然后设置 IDE 工具识别这种特殊的注释,以便结构化的显示。Eclipse 的 Tasks 显示效果类似下图; 这个结构化视图,本质上相对于是代码库的索引、目录,不同于 javadoc 文档,CODEX 具有更清晰的逻辑层次和更强的代码查找便利性,在 Eclipse Tasks 中点击就能跳转到对应的代码行; 这些结构化注释随着代码一起提交后就实现了团队共享; 这样的一份精确无误、共享的、活的源代码索引,无疑会对整个团队的开发维护工作产生巨大助力; 进一步的,如果在 CODEX 中添加 Markdown 关键字,甚至可以将导出的 CODEX 简单加工后,变成一张业务逻辑的 Sequence 序列图,如下所示。 6. 总结陈词——不要辜负这个程序员最好的时代 毫无疑问这是程序员最好的时代,互联网浪潮已经席卷了世界每个角落,各行各业正在越来越多的依赖 IT。过去只有软件公司、互联网公司和银行业会雇佣程序员,随着云计算的普及、产业互联网和互联网+兴起,已经有越来越多的传统企业开始雇佣程序员搭建 IT 系统来支撑业务运营。 资本的推动 IT 需求的旺盛,使得程序员成了稀缺人才,各大招聘平台上,程序员的岗位数量和薪资水平长期名列前茅。 但是我们这个群体的整体表现怎么样呢,扪心自问,我觉得很难令人满意,我所经历过的以及近距离观察到的项目,鲜有能够称得上成功的。这里的成功不是商业上的成功,仅限于作为一个软件项目和工程是否能够以可接受的成本和质量长期稳定的交付。 商业的短期成功与否,很多时候与项目工程的成功与否没有必然联系,一个商业上很成功的项目可能在工程上做的并不好,只是通过巨量的资金资源投入换来的暂时成功而已。 归根结底,我们程序员群体需要为自己的声誉负责,长期来看也终究会为自己的声誉获益或受损。 我认为程序员最大的声誉、最重要的职业素养,就是通过写出高质量的代码做好一个个项目、产品,来帮助团队、帮助公司、帮助组织创造价值、增加成功的机会。 希望本文分享的经验和方法能够对此有所帮助! 推荐阅读:程序员你所害怕的中年危机,恰恰是人生的转机!来源:四猿外作者:李英权原文链接:https://mp.weixin.qq.com/s/UBE6rPunwuEq5biI0EU2Lw
背景 疫情之下,刚刚结束了在家办公的日子,准备开展新年的工作的时候,突然接到同行好友的电话,要和我聊聊天。 他说他们部门调整,虽然最后他留了下来,但还是非常焦虑。人无远虑必有近忧,他这次被刺激到了,想提高一下自己,以免下次再有类似的心惊肉跳。但怎么提高呢? 程序员第一考虑的当然是技术,但现在真不知道学什么了:干了三四年的开发,手头的工作马马虎虎都没什么问题了。就算有问题,也是业务逻辑上的问题,系统太复杂,吃不透;或者系统里一些过时的/生僻的技术,真心觉得没必要花时间花精力去研究,能对付就行了,是不是?反正下家单位99.99%的几率是不会用到这些东西的的。 那么就是一些新技术了。新技术杂得很,不知道学什么,框架类库啥的其实没什么技术含量,一两个星期就可以上手,上手了之后呢?往深里学?其实和老旧技术一样的问题,谁知道下一份工作用不用得上呢!而且没趣,大概是因为没有挑战性吧,反正就那个样,还能咋的? 学习瓶颈 一直聊到这里,我都非常理解。我这个十年以上的老码农,体会比他还深。其实稍微干上一些年头,就是这个样子,看起来像是“学习热情下降”“懒得学习”,但本质上呢,两个原因: 1、本身的技能增强,能够应付日常工作,没有刚入行时那么大的压力了。 2、学习的边际效益递减,所以开始考虑投资/收益比了。 刚入行的时候,技术一丁点的进步,都能带来非常大的成就感,至少能少挨点骂,是不是?积累到一定时候,就可以跳个槽涨点工资啥的。但三五年过后, 我已经会了: 熟悉Android OS系统体系结构、framework层; 良好的Java技术功底,精通多线程、socket通信、文件操作等java底层技术; 精通Android的基本组件使用,熟练使用Android各种布局与控件,熟练运用各种动画特效; 熟悉View的绘制原理,精通自定义动画以及自定View的开发 有丰富的Android性能优化经验,善于解决系统崩溃,内存溢出和兼容性问题; 熟悉JNI技术和代码混淆 有单元测试、自动化测验及相关框架经验; 有良好的代码习惯,要求结构清晰,命名规范,逻辑性强,代码冗余率低,代码注释清晰; 学会优雅转身 接下来和大家分享一下我这么多年的转型之路。姑妄听之: 1、转型技术管理 2007 年下半年到 2009 年初,我慢慢转向技术管理角色,开始管理一个研发部门。我走的路线是“技而优则管”。 简单讲,就是你技术牛 X ,轻松搞定各种问题,开始带人,接下来带团队管项目,再接下来,顺理成章就会被公司推着向技术管理者转型。 这种路线,也是一大半技术管理者走过的路线。如果你想转型技术管理,可以考虑这种。 “技而优则管”的要点,就是:行有余力。 什么意思呢,就是聚焦当下,高效搞定你的任务。当你体现出绰绰有余的样子,领导就会给你更多更难的任务。当你还能高效搞定,还看起来有余力,领导就会再给你更重要的任务…… 如此循环,你就越来越重要,就会带人、带项目的机会。 假如你总是手上的活儿都做不完、做不好,就基本走不了这条路线。不过也还有其他路线。 2、加入创业者行列 2014 年 9 月份,我因某种契机,离开工作 7 年的公司,重新考虑自己的方向。到阳历年底时,接受朋友邀请,和他一起创业。 我之所以愿意去尝试,是因为: 创业和上班的未来可能性大不一样,万一创业成功,你的影响力、经济回报,都会上一个台阶。 一起做事的人靠谱。 做的产品,盈利模式明确。 当然,不幸的事总会发生——我们失败了。 我个人并没因为这样的失败经历而后悔,因为当你站在主人翁的角度和高度去为公司负责时,你对工作的认知,会发生巨大变化!这种变化,会对后续的工作和人生产生重要的正向影响。 如果你技术过硬为人靠谱,一定会有不少创业者邀请你加入他们的队伍。不要头脑发热,要仔细考察一下: 评估要做的产品是否靠谱 看看即将一起共事的人是否靠谱 设想近 2 年没有收入,自己和家人的生活水平能否维持 设想全力投入创业对自己和家人的生活有什么影响,自己和家人能否接受 3、 重回开发岗位 我从 2009 年开始做技术管理,到 2015 年底创业失败,历时 7 年。可是我在多个更好的管理机会面前,毅然选择回到技术岗位继续做开发。 为什么? 这是因为我们工作的目的,除了赚钱,还有自我实现。 自我实现包括几方面: 成长 成就 意愿 成长是指你做一件事之前和之后,有你想要的、积极的变化。 假如你做了十年开发,技术水平、解决问题的能力,还是和刚入行时差不多,那就叫没成长! 成就是指你做出了成绩并获得了相关干系人的认可。 你负责一个模块,用了最新的技术最牛逼的设计,也能 run ,实现了用户需求。你个人觉得很有成就,可是你用的技术框架过于复杂,维护成本很高,运维团队、二次开发团队都不认可,那就不是真正的成就。 意愿很好理解,就是你愿意在什么事情上投入你的时间和精力、你做什么事情时会感到开心。 比如我,在创业失败重新找工作时,就是因为觉得亲力亲为用技术去开发一个个软件、解决一个个问题比较令我兴奋和投入,所以才选择回到开发岗位上。 很多开发者都会遇到要不要转管理、要不要一直做技术这种问题,怎么选择答案,关键就在于你的个人意愿和你做某件事的感受。 可能有人会问,如果你一直做开发,年龄大了,怎么和年轻人拼? 我之前画过一张图,再贴出来给大家看看: 由这张图可以看到,开发者做软件分两次创造,第一次创造在头脑中完成,属于思考层面;第二次创造,是编码实现,是脑力劳动体力化。 如果你想要超越年龄,就要多在第一次创造所需要的能力上下功夫: 构建起来自己围绕着特定业务领域的知识体系 淬炼想象力、抽象、归纳、分析、整合、设计等 这样你就能思考得多、做得少、做得关键、做得好,就可以超越年龄的限制。否则如果你整天和年轻人一样只关注噼里啪啦敲代码,肯定没价值,很快被清退。 所谓高级工程师就是在技术上逐步沉淀,逐步体系掌握核心技术得来的。 我们程序员提升的方向无非管理者与架构师两种。要成为管理者,就应具备一定的管理知识、较高的情商以及良好的组织协调能力。 而想要成为移动架构师,就要肩负技术和组织两个层面的重任,构建自己完整的技术体系就尤为重要了。 当然从结果来论,能力突出架构师的薪资也同样会水涨船高,一个Android架构师能够拿到40万的年薪都再正常不过了。 对于Android架构师职责的介绍,网上已经铺天盖地,就不再赘述。今天我主要给大家分享一下成为一名Android架构师应该掌握的技术能力。 阿里公司注重的7大主流技术专题与移动架构师项目实战 深度对接阿里P8高级工程师级别的主流技术体系,并且综合了目前的各大互联网公司如华为、抖音、OPPO、阿里等主流技术(即使你不想选择阿里,其它的大厂照样适合) 主流技术专题 移动架构师项目实战 音视频开发、网上商城、新兴自媒体等都是时下热点技术与专题,深入了解项目源码、参与项目开发过程中问题解决、组织协调与人际关系沟通均是大厂对人才素质的基本要求。 最后 题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。 我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等录播视频免费分享出来。 下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC 希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展~
目录: 1.Android基础 2.网络 3.Java 基础&数据结构&设计模式 4.Android 性能优化&Framework 5.Android 模块化&热修复&热更新&打包&混淆&压缩 6.音视频&FFmpeg&播放器 7.项目&HR 1.Android基础 1、什么是ANR 如何避免它? 答:在Android 上,如果你的应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应用程序无响应(ANR:Application Not Responding)对话框。用户可以选择让程序继续运行,但是,他们在使用你的应用程序时,并不希望每次都要处理这个对话框。因此,在程序里对响应性能的设计很重要,这样,系统不会显示ANR 给用户。不同的组件发生ANR 的时间不一样,主线程(Activity、Service)是5 秒,BroadCastReceiver 是10 秒。解决方案:将所有耗时操作,比如访问网络,Socket 通信,查询大量SQL 语句,复杂逻辑计算等都放在子线程中去,然后通过handler.sendMessage、runonUITread、AsyncTask 等方式更新UI。无论如何都要确保用户界面操作的流畅度。如果耗时操作需要让用户等待,那么可以在界面上显示进度条。 2、View的绘制流程;自定义View如何考虑机型适配;自定义View的事件3、分发机制;View和ViewGroup分别有哪些事件分发相关的回调方法;自定义View如何提供获取View属性的接口; 3、Art和Dalvik对比;虚拟机原理,如何自己设计一个虚拟机(内存管理,类加载,双亲委派);JVM内存模型及类加载机制;内存对象的循环引用及避免; 4、ddms 和 traceView; 5、内存回收机制与GC算法(各种算法的优缺点以及应用场景);GC原理时机以及GC对象;内存泄露场景及解决方法; 6、四大组件及生命周期;ContentProvider的权限管理(读写分离,权限控制-精确到表级,URL控制);Activity的四种启动模式对比;Activity状态保存于恢复; 7、什么是AIDL 以及如何使用; 8、请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系; 9、Fragment生命周期;Fragment状态保存startActivityForResult是哪个类的方法,在什么情况下使用,如果在Adapter中使用应该如何解耦; 10、AsyncTask原理及不足;ntentService原理; 11、Activity 怎么和Service 绑定,怎么在Activity 中启动自己对应的Service; 12、请描述一下Service 的生命周期; 13、AstncTask+HttpClient与AsyncHttpClient有什么区别; 14、如何保证一个后台服务不被杀死;比较省电的方式是什么; 15、如何通过广播拦截和abort一条短信;广播是否可以请求网络;广播引起anr的时间限制; 16、进程间通信,AIDL; 17、事件分发中的onTouch 和onTouchEvent 有什么区别,又该如何使用? 18、说说ContentProvider、ContentResolver、ContentObserver 之间的关系; 19、请介绍下ContentProvider 是如何实现数据共享的; 20、Handler机制及底层实现; 21、Binder机制及底层实现; 22、ListView 中图片错位的问题是如何产生的; 23、在manifest 和代码中如何注册和使用BroadcastReceiver; 24、说说Activity、Intent、Service 是什么关系; 25、ApplicationContext和ActivityContext的区别; 26、一张Bitmap所占内存以及内存占用的计算; 27、Serializable 和Parcelable 的区别; 28、请描述一下BroadcastReceiver; 29、请描述一下Android 的事件分发机制; 30、请介绍一下NDK; 31、什么是NDK库,如何在jni中注册native函数,有几种注册方式; 32、AsyncTask 如何使用; 33、对于应用更新这块是如何做的?(灰度,强制更新,分区域更新); 34、混合开发,RN,weex,H5,小程序(做Android的了解一些前端js等还是很有好处的); 35、什么情况下会导致内存泄露; 36、如何对Android 应用进行性能分析以及优化; 37、说一款你认为当前比较火的应用并设计(直播APP); 38、OOM的避免异常及解决方法; 39、屏幕适配的处理技巧都有哪些; 40、两个Activity 之间跳转时必然会执行的是哪几个方法? 答:一般情况下比如说有两个activity,分别叫A,B,当在A 里面激活B 组件的时候, A 会调用onPause()方法,然后B 调用onCreate() ,onStart(), onResume()。这个时候B 覆盖了窗体, A 会调用onStop()方法. 如果B 是个透明的,或者是对话框的样式, 就不会调用A 的onStop()方法。 2.网络 网络协议模型 应用层:负责处理特定的应用程序细节 HTTP、FTP、DNS 传输层:为两台主机提供端到端的基础通信 TCP、UDP 网络层:控制分组传输、路由选择等 IP 链路层:操作系统设备驱动程序、网卡相关接口 TCP 和 UDP 区别 TCP 连接;可靠;有序;面向字节流;速度慢;较重量;全双工;适用于文件传输、浏览器等 全双工:A 给 B 发消息的同时,B 也能给 A 发 半双工:A 给 B 发消息的同时,B 不能给 A 发 UDP 无连接;不可靠;无序;面向报文;速度快;轻量;适用于即时通讯、视频通话等 TCP 三次握手 A:你能听到吗? B:我能听到,你能听到吗? A:我能听到,开始吧 A 和 B 两方都要能确保:我说的话,你能听到;你说的话,我能听到。所以需要三次握手 TCP 四次挥手 A:我说完了 B:我知道了,等一下,我可能还没说完 B:我也说完了 A:我知道了,结束吧 B 收到 A 结束的消息后 B 可能还没说完,没法立即回复结束标示,只能等说完后再告诉 A :我说完了。 POST 和 GET 区别 Get 参数放在 url 中;Post 参数放在 request Body 中 Get 可能不安全,因为参数放在 url 中 HTTPS HTTP 是超文本传输协议,明文传输;HTTPS 使用 SSL 协议对 HTTP 传输数据进行了加密 HTTP 默认 80 端口;HTTPS 默认 443 端口 优点:安全 缺点:费时、SSL 证书收费,加密能力还是有限的,但是比 HTTP 强多了 3.Java基础&数据结构&设计模式 1、集合类以及集合框架;HashMap与HashTable实现原理,线程安全性,hash冲突及处理算法;ConcurrentHashMap; 2、进程和线程的区别; 3、Java的并发、多线程、线程模型; 4、什么是线程池,如何使用? 答:线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。 5、数据一致性如何保证;Synchronized关键字,类锁,方法锁,重入锁; 6、Java中实现多态的机制是什么; 7、如何将一个Java对象序列化到文件里; 8、说说你对Java反射的理解; 答:Java 中的反射首先是能够获取到Java 中要反射类的字节码, 获取字节码有三种方法,1.Class.forName(className) 2.类名.class 3.this.getClass()。 然后将字节码中的方法,变量,构造函数等映射成相应的Method、Filed、Constructor 等类,这些类提供了丰富的方法可以被我们所使用。 4、同步的方法;多进程开发以及多进程应用场景; 5、在Java中wait和seelp方法的不同; 答:最大的不同是在等待时wait 会释放锁,而sleep 一直持有锁。wait 通常被用于线程间交互,sleep 通常被用于暂停执行。 synchronized 和volatile 关键字的作用; 答:1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 2)禁止进行指令重排序。 volatile 本质是在告诉jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。1.volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的2.volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则可以保证变量的修改可见性和原子性3.volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。4.volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化 6、服务器只提供数据接收接口,在多线程或多进程条件下,如何保证数据的有序到达; 7、ThreadLocal原理,实现及如何保证Local属性; 8、String StringBuilder StringBuffer对比; 9、你所知道的设计模式有哪些; 答:Java 中一般认为有23 种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。总体来说设计模式分为三大类:创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 10、Java如何调用c、c++语言; 11、接口与回调;回调的原理;写一个回调demo; 12、泛型原理,举例说明;解析与分派; 13、抽象类与接口的区别;应用场景;抽象类是否可以没有方法和属性; 14、静态属性和静态方法是否可以被继承?是否可以被重写?以及原因? 15、修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法; 16、说说你对泛型的了解; 17、Java的异常体系; 18、如何控制某个方法允许并发访问线程的个数; 19、动态代理的区别,什么场景使用; 数据结构与算法 1、堆和栈在内存中的区别是什么(数据结构方面以及实际实现方面); 2、最快的排序算法是哪个?给阿里2万多名员工按年龄排序应该选择哪个算法?堆和树的区别;写出快排代码;链表逆序代码; 3、求1000以内的水仙花数以及40亿以内的水仙花数; 4、子串包含问题(KMP 算法)写代码实现; 5、万亿级别的两个URL文件A和B,如何求出A和B的差集C,(Bit映射->hash分组->多文件读写效率->磁盘寻址以及应用层面对寻址的优化) 6蚁群算法与蒙特卡洛算法; 7、写出你所知道的排序算法及时空复杂度,稳定性; 8、百度POI中如何试下查找最近的商家功能(坐标镜像+R树)。 4.Android性能优化&Framwork Activity 启动模式 standard 标准模式 singleTop 栈顶复用模式, 推送点击消息界面 singleTask 栈内复用模式, 首页 singleInstance 单例模式,单独位于一个任务栈中 拨打电话界面 细节: taskAffinity:任务相关性,用于指定任务栈名称,默认为应用包名 allowTaskReparenting:允许转移任务栈 View 工作原理 DecorView (FrameLayout) LinearLayout titlebar Content 调用 setContentView 设置的 View ViewRoot 的 performTraversals 方法调用触发开始 View 的绘制,然后会依次调用: performMeasure:遍历 View 的 measure 测量尺寸 performLayout:遍历 View 的 layout 确定位置 performDraw:遍历 View 的 draw 绘制 事件分发机制 一个 MotionEvent 产生后,按 Activity -> Window -> decorView -> View 顺序传递,View 传递过程就是事件分发,主要依赖三个方法: dispatchTouchEvent:用于分发事件,只要接受到点击事件就会被调用,返回结果表示是否消耗了当前事件 onInterceptTouchEvent:用于判断是否拦截事件,当 ViewGroup 确定要拦截事件后,该事件序列都不会再触发调用此 ViewGroup 的 onIntercept onTouchEvent:用于处理事件,返回结果表示是否处理了当前事件,未处理则传递给父容器处理 细节: 一个事件序列只能被一个 View 拦截且消耗 View 没有 onIntercept 方法,直接调用 onTouchEvent 处理 OnTouchListener 优先级比 OnTouchEvent 高,onClickListener 优先级最低 requestDisallowInterceptTouchEvent 可以屏蔽父容器 onIntercet 方法的调用 Window 、 WindowManager、WMS、SurfaceFlinger Window:抽象概念不是实际存在的,而是以 View 的形式存在,通过 PhoneWindow 实现 WindowManager:外界访问 Window 的入口,内部与 WMS 交互是个 IPC 过程 WMS:管理窗口 Surface 的布局和次序,作为系统级服务单独运行在一个进程 SurfaceFlinger:将 WMS 维护的窗口按一定次序混合后显示到屏幕上 View 动画、帧动画及属性动画 View 动画: 作用对象是 View,可用 xml 定义,建议 xml 实现比较易读 支持四种效果:平移、缩放、旋转、透明度 帧动画: 通过 AnimationDrawable 实现,容易 OOM 属性动画: 可作用于任何对象,可用 xml 定义,Android 3 引入,建议代码实现比较灵活 包括 ObjectAnimator、ValuetAnimator、AnimatorSet 时间插值器:根据时间流逝的百分比计算当前属性改变的百分比 系统预置匀速、加速、减速等插值器 类型估值器:根据当前属性改变的百分比计算改变后的属性值 系统预置整型、浮点、色值等类型估值器 使用注意事项: 避免使用帧动画,容易OOM 界面销毁时停止动画,避免内存泄漏 开启硬件加速,提高动画流畅性 ,硬件加速: 将 cpu 一部分工作分担给 gpu ,使用 gpu 完成绘制工作 从工作分摊和绘制机制两个方面优化了绘制速度 Handler、MessageQueue、Looper Handler:开发直接接触的类,内部持有 MessageQueue 和 Looper MessageQueue:消息队列,内部通过单链表存储消息 Looper:内部持有 MessageQueue,循环查看是否有新消息,有就处理,没就阻塞 如何实现阻塞:通过 nativePollOnce 方法,基于 Linux epoll 事件管理机制 为什么主线程不会因为 Looper 阻塞:系统每 16ms 会发送一个刷新 UI 消息唤醒 MVC、MVP、MVVM MVP:Model:处理数据;View:控制视图;Presenter:分离 Activity 和 Model MVVM:Model:处理获取保存数据;View:控制视图;ViewModel:数据容器 使用 Jetpack 组件架构的 LiveData、ViewModel 便捷实现 MVVM Serializable、Parcelable Serializable :Java 序列化方式,适用于存储和网络传输,serialVersionUID 用于确定反序列化和类版本是否一致,不一致时反序列化回失败 Parcelable :Android 序列化方式,适用于组件通信数据传递,性能高,因为不像 Serializable 一样有大量反射操作,频繁 GC Binder Android 进程间通信的中流砥柱,基于客户端-服务端通信方式 使用 mmap 一次数据拷贝实现 IPC,传统 IPC:用户A空间->内核->用户B空间;mmap 将内核与用户B空间映射,实现直接从用户A空间->用户B空间 BinderPool 可避免创建多 Service IPC 方式 Intent extras、Bundle:要求传递数据能被序列化,实现 Parcelable、Serializable ,适用于四大组件通信 文件共享:适用于交换简单的数据实时性不高的场景 AIDL:AIDL 接口实质上是系统提供给我们可以方便实现 BInder 的工具 Android Interface Definition Language,可实现跨进程调用方法 服务端:将暴漏给客户端的接口声明在 AIDL 文件中,创建 Service 实现 AIDL 接口并监听客户端连接请求 客户端:绑定服务端 Service ,绑定成功后拿到服务端 Binder 对象转为 AIDL 接口调用 RemoteCallbackList 实现跨进程接口监听,同个 Binder 对象做 key 存储客户端注册的 listener 监听 Binder 断开:1.Binder.linkToDeath 设置死亡代理;2. onServiceDisconnected 回调 Messenger:基于 AIDL 实现,服务端串行处理,主要用于传递消息,适用于低并发一对多通信 ContentProvider:基于 Binder 实现,适用于一对多进程间数据共享 Socket:TCP、UDP,适用于网络数据交换 Android 系统启动流程 按电源键 -> 加载引导程序 BootLoader 到 RAM -> 执行 BootLoader 程序启动内核 -> 启动 init 进程 -> 启动 Zygote 和各种守护进程 -> 启动 System Server 服务进程开启 AMS、WMS 等 -> 启动 Launcher 应用进程 App 启动流程 Launcher 中点击一个应用图标 -> 通过 AMS 查找应用进程,若不存在就通过 Zygote 进程 fork 进程保活 进程优先级:1.前台进程 ;2.可见进程;3.服务进程;4.后台进程;5.空进程 进程被 kill 场景:1.切到后台内存不足时被杀;2.切到后台厂商省电机制杀死;3.用户主动清理 保活方式: 1.Activity 提权:挂一个 1像素 Activity 将进程优先级提高到前台进程 2.Service 提权:启动一个前台服务(API>18会有正在运行通知栏) 3.广播拉活 4.Service 拉活 5.JobScheduler 定时任务拉活 6.双进程拉活 网络优化及检测 速度:1.GZIP 压缩(okhttp 自动支持);2.Protocol Buffer 替代 json;3.优化图片/文件流量;4.IP 直连省去 DNS 解析时间 成功率:1.失败重试策略; 流量:1.GZIP 压缩(okhttp 自动支持);2.Protocol Buffer 替代 json;3.优化图片/文件流量;5.文件下载断点续传 ;6.缓存 协议层的优化,比如更优的 http 版本等 监控:Charles 抓包、Network Monitor 监控流量 UI卡顿优化 减少布局层级及控件复杂度,避免过度绘制 使用 include、merge、viewstub 优化绘制过程,避免在 Draw 中频繁创建对象、做耗时操作 内存泄漏场景及规避 1.静态变量、单例强引跟生命周期相关的数据或资源,包括 EventBus 2.游标、IO 流等资源忘记主动释放 3.界面相关动画在界面销毁时及时暂停 4.内部类持有外部类引用导致的内存泄漏 handler 内部类内存泄漏规避:1.使用静态内部类+弱引用 2.界面销毁时清空消息队列 检测:Android Studio Profiler LeakCanary 原理 通过弱引用和引用队列监控对象是否被回收 比如 Activity 销毁时开始监控此对象,检测到未被回收则主动 gc ,然后继续监控 OOM 场景及规避 加载大图:减小图片 内存泄漏:规避内存泄漏 5.Android 模块化&热修复&热更新&打包&混淆&压缩 Dalvik 和 ART Dalvik 谷歌设计专用于 Android 平台的 Java 虚拟机,可直接运行 .dex 文件,适合内存和处理速度有限的系统 JVM 指令集是基于栈的;Dalvik 指令集是基于寄存器的,代码执行效率更优 ART Dalvik 每次运行都要将字节码转换成机器码;ART 在应用安装时就会转换成机器码,执行速度更快 ART 存储机器码占用空间更大,空间换时间 APK 打包流程 1.aapt 打包资源文件生成 R.java 文件;aidl 生成 java 文件 2.将 java 文件编译为 class 文件 3.将工程及第三方的 class 文件转换成 dex 文件 4.将 dex 文件、so、编译过的资源、原始资源等打包成 apk 文件 5.签名 6.资源文件对齐,减少运行时内存 App 安装过程 首先要解压 APK,资源、so等放到应用目录 Dalvik 会将 dex 处理成 ODEX ;ART 会将 dex 处理成 OAT; OAT 包含 dex 和安装时编译的机器码 组件化路由实现 ARoute:通过 APT 解析 @Route 等注解,结合 JavaPoet 生成路由表,即路由与 Activity 的映射关系 6.音视频&FFmpeg&播放器 FFmpeg 基于命令方式实现了一个音视频编辑 App: https://github.com/yhaolpz/FFmpegCmd 集成编译了 AAC、MP3、H264 编码器 播放器原理 视频播放原理:(mp4、flv)-> 解封装 -> (mp3/aac、h264/h265)-> 解码 -> (pcm、yuv)-> 音视频同步 -> 渲染播放 音视频同步: 选择参考时钟源:音频时间戳、视频时间戳和外部时间三者选择一个作为参考时钟源(一般选择音频,因为人对音频更敏感,ijk 默认也是音频) 通过等待或丢帧将视频流与参考时钟源对齐,实现同步 IjkPlayer 原理 集成了 MediaPlayer、ExoPlayer 和 IjkPlayer 三种实现,其中 IjkPlayer 基于 FFmpeg 的 ffplay 音频输出方式:AudioTrack、OpenSL ES;视频输出方式:NativeWindow、OpenGL ES 7.项目&HR 1. 项目开发中遇到的最大的一个难题和挑战,你是如何解决的。(95% 会问到) 2. 说说你开发最大的优势点(95% 会问到) 3. 你为什么会离开上家公司 4. 你的缺点是什么? 5. 你能给公司带来什么效益? 6. 你对未来的职业规划? 文末 对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们! 近期我们搜集了 N 套阿里、腾讯、美团、网易等公司 19 年的面试题,把技术点梳理成一份大而全的“Android高级工程师”面试题库(实际上比预期多花了不少精力),包含标准答案解析,由于篇幅有限,这里以图片的形式给大家展示一部分。 这份资料尤其适合: 1.近期想跳槽,要面试的Android程序员,查漏补缺,以便尽快弥补短板; 2.想了解“一线互联网公司”最新技术要求,对比找出自身的长处和弱点所在,评估自己在现有市场上的竞争力如何; 3.做了几年Android开发,但还没形成系统的Android知识体系,缺乏清晰的提升方向和学习路径的程序员。 相信它会给大家带来很多收获。++++维信,(壹叁贰零叁壹陆叁陆零玖)就可以免费领取了 除面试资料外,这里还整理了一份最近刚录制的视频——BAT大牛解密Android面试,对于面试,是个不错的补充。 视频围绕“BAT大牛解密Android面试?”的主题,内容由浅入深,同时,对于开源框架相关面试问题也作出重点解读。 视频具体内容如下: 第1章 课程介绍 第2章 一线互联网公司初中高Android开发工程师的技能要求 第3章 Android基础相关面试题 第4章 异步消息处理机制相关面试问题 第5章 View相关面试问题 第6章 Android项目构建相关面试问题 第7章 开源框架相关面试问题 第8章 Android异常与性能优化相关面试问题 第9章 热门前沿知识相关面试问题 需要获取更全面的面试资料,或专题视频,++++维信:(壹叁贰零叁壹陆叁陆零玖)。前往免费领取!
马上快到金三银四都春招阶段了,在这本就是跳槽、找工作的年后黄金时间,大多数求职者都早早做好年后求职的准备,其中不乏有年前早早辞了工作准备年后跳槽的有经验的职场老人们,也有一批即将毕业的应届毕业生的职场新人们。 但是受此次“新冠肺炎”疫情影响之后,“金三银四”逐渐演变成千军万马过独木桥,一边是摩拳擦掌有经验的职场老人们,而另一边则是即将毕业跃跃欲试的新鲜血液,只会让求职人才们越积越多,面对这样岗位少求职者多的情况下,竞争力可想而知,再加上企业的招聘计划调整,侧面也是加剧了求职的难度。 当然也有少部分公司现在也已经开始了远程面试,对于才能怎样拿到腾讯、阿里、字节跳动等大厂的offer,我问了一些在大厂工作的朋友。 大概总结一下,技术基础扎实是通过一、二面的首要条件,但是接下来的三四面就不是这么一帆风顺。 由于一些程序员们只重视技术,而在工作经验、项目经验等等方面有所欠缺。而在职业发展方面,你需要得到他人的帮助,学会“站在巨人的肩膀上。” 比如说通过系统性课程的学习和有丰富项目经验的导师辅导,从工具、思维、方法论、逻辑等双管齐下,才能离想要的offer更进一步。 在这里我分享一份我们整理的《BAT大牛解密Android面试》视频解析,为你精准剖析各大厂本次春招的关键点。包含项目构建相关面试专题。 需要的小伙伴可以直接++++维信(壹叁贰零叁壹陆叁陆零玖 就可以获取了)
截止到2.19日,疫情发展已呈现下降态势,很多年前已经辞职,或者有想法的离职的朋友,大家在关心疫情好转的同时,不免担心年后找工作的事情。 那么究竟疫情会给年后找工作的求职者们带来哪些影响呢?1.整个求职周期拉长 当前这一阶段正处于肺炎疫情的爆发期,正如前几天网上传的段子,几时能动?钟南山爷爷说动才动!显然当下为了避免疫情的进一步蔓延,国家延长了假期,前面也说到过,各大企业的复工时间也拉长,自然影响企业HR的招聘计划啊,无疑这是个漫漫求职路啊。 而从正常的时间周期来看,从面试到上岗,至少需要几个星期,慢则甚至几个月,其中还不乏企业必须要走的流程,层层审核,何况搁置了的人员调动等其他工作,从面试到正式上岗,找工作除了等还是等啊,对求职者来说这是场长久拉锯战啊。 2.企业招聘需求缩减 不用说,受疫情的影响,企业原定的初八甚至初六就开始上班的,这么一耽搁,加上整个社会的国民消费经济寒冬,无论什么企业受到的影响不可估量。尤其像旅游业和餐饮业,过年这段时间本就是旺季,一年当中营业额最高的时候,全国商场的统一关门,春节档院线的相继撤档,饭店集体地低价出售肉菜,旅游业更是无人问津,一般小公司小企业哪能受这么折腾,相信可能直接面对亏损甚至倒闭。 在整体大环境的影响下,企业出于自身的战略考虑,肯定是能裁减就裁减,能不招就不招,从而消减人力成本,进而将损失降到最低。所以此时企业的招聘需求缩减,自然也是在情理之中,不得以而为之。 3.求职者竞争增大 在这本就是跳槽、找工作的年后黄金时间,大多数求职者都早早做好年后求职的准备,其中不乏有年前早早辞了工作准备年后跳槽的有经验的职场老人们,也有一批即将毕业的应届毕业生的职场新人们,所以在受疫情影响之后,这本就是千军万马过独木桥,一边是摩拳擦掌有经验的职场老人们,而另一边则是即将毕业跃跃欲试的新鲜血液,只会让求职人才们越积越多,面对这样岗位少求职者多的情况下,竞争力可想而知,再加上企业的招聘计划调整,侧面也是加剧了求职的难度。 那么面对这样的局面,程序员到底该怎么做,才能尽可能地减小损失,增加竞争力呢? 01 每个人都要学会演练“自我攻击” 自我攻击,这不是自虐么,为什么还要刻意主动去演练? 因为如果你身上有漏洞,迟早别人会来攻击你,如果你经常演练自我攻击,就可以在别人攻击你之前发现漏洞,然后自我修复。 这是我从企业经营中得到的启示。 罗振宇在一期音频中提到,他们公司后台有个软件,叫“混沌猴”,之所以起这个名字是基于它的工作原理。“混沌猴”每天的工作就是快速随机点击得到APP的所有按钮,就像一只上蹿下跳、毫无章法的猴子那样给得到APP的系统制造各种混乱,直到把软件搞崩溃。而后它会自动生成系统故障报告发送给程序员,程序员第二天上班时就会根据报告内容修补漏洞、完善系统。 无独有偶,阿里巴巴也有类似举措。他们专门组建了一支蓝军程序员队伍,日常工作就是想尽一切办法攻击自家公司的系统,找到bug,而后修复。 无论得到还是阿里巴巴,他们企业中存在的漏洞,如果不是自己及时发现,那么一定是被别人发现,被用户发现,这样的话不如自己发现,然后自我修复,这就是通过刻意自我攻击的演练在周而复始的“自我攻击-发现漏洞-修补完善”中不断提高内部系统的稳定性与体验感。 每个人做事,也是如此。 02.提前做好准备,及时抢占先机 机会总是会留给有准备的人,趁在家的日子,不如找点大厂里流行的技术来充电学习。 我们整理了阿里P7视频干货,讲解很透彻。今天免费分享给大家。这份资料尤其适合以下人群 主要包括阿里、腾讯,以及字节跳动,华为,小米,等一线互联网公司主流架构技术。如果你有需要,尽管拿走好了。 这些技术相信大家都不陌生,都是近年来进大厂所必需的硬技能,但要说真正搞明白的恐怕不多,毕竟市面上系统教授这方面的课程非常少见。 学完这份视频你将获得哪些收获? 理解当下最火热的NDK、Flutter技术; 触及一线大厂所配备的性能优化、移动架构项目实战等核心技术内幕知识; 对照自己掌握知识点进行查漏补缺,帮助扫除知识盲区、重构知识体系; 结识业界大佬的机会。 具体内容如下:1.高级UI UI这块知识是现今使用者最多的。当年火爆一时的Android入门培训,学会这小块知识就能随便找到不错的工作了。 不过很显然现在远远不够了,所以很多人会觉得大环境不好了安卓开发要凉了。 这些人如果能自身反省;企业要你们这些CV工程师的意义在哪呢? 你要自己亲自去项目实战,读源码,研究原理的呀。 下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC 2.性能优化 如果我是老板,我招你来是写代码的,不是写bug的。如果你的代码太烂,各种bug。我把你开了后重新招个人进来接手维护,甚至推到重新做,后面那个接盘的是不是要骂街? 如果你会性能调优,能解决项目中各种性能问题。那么拿20K真的不过分。你得具备深厚的代码功底,深入学习源码原理以及使用工具进行测试和检查调优。 下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC 3.NDK开发 音视频,人工智能,这些是未来没办法阻挡的发展大趋势。我在猎聘网上看那些招聘岗位,要求精通NDK的薪资都在30-60K。追求高薪岗位的小伙伴,NDK开发一定要掌握并且去深挖 下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC 4.Flutter 火了一年多了,你工作不一定要用到。但是你出去面试(初级很少要求会),肯定会问到的。 关于Flutter是不是未来,我没法确定告诉你,我能确定的就是你要去面试高薪岗位,你得掌握这种主流的新技术(大厂最看重的除了基础,技术水平外,就是你的学习能力。) 下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC 5.移动架构实战项目 架构师不是天生的,是在项目中磨练起来的,所以,我们学了技术就需要结合项目进行实战训练,那么在Android里面最常用的架构无外乎 MVC,MVP,MVVM,但是这些思想如果和模块化,层次化,组件化混和在一起,那就不是一件那么简单的事了。 架构师尤其是移动开发,数量太少了。可能很多Android开发的小伙伴都没见过移动架构师。架构师薪资是什么样的水平呢? 阿里P6处于高级工程师,年薪四五十万左右 阿里P7处于资深高级,年薪百万左右 阿里P8属于架构师了,年薪可达170万以上 阿里的标准和薪资都是很高的,其它公司会有差距,但不会太大。 你是否有想过自己以后能达到架构师水平,突破百万年薪,实现财富自由呢? 下载地址:https://shimo.im/docs/YHJtVkC3y6qgp9xC
【背景】 2月6日,知名IT培训机构“兄弟连教育”创始人李超,发表了《致兄弟连全体学员、员工、股东的一封信》,他表示即日起,兄弟连北京校区停止招生,员工全部遣散。 2.10春节之后复工的第一天。在上班第一天的员工大会上,新潮传媒宣布减员500人。新潮传媒高管向媒体透露上午开会宣布减员500人,占总员工数的10%,高管集体降薪20%。 而更残酷的是,有些人还没等到复工,就已经被裁了。这真应了网上的一个段子: 公司通知一,假期延迟到2月2日; 公司通知二,假期延长到2月10日; 公司通知三,假期延长到2月17日; 公司通知四,公司没有了,不用回来了。 【疫情下,每个职场人都要思考这三点 】 01 为什么疫情后一定会有一波裁员? 很多时候,我们不需要过多分析,看政策就知道了,国家比我们敏感得多。 拿北京举例吧。2月6日北京出台了促进中小微企业发展的政策。 我拿关键词“裁员”检索出两条相关信息: 1、减免中小微企业房租:中小微企业承租京内市及区属国有企业房产从事生产经营活动,按照政府要求坚持营业或依照防疫规定关闭停业且不裁员、少裁员的,免收2月份房租;承租用于办公用房的,给予2月份租金50%的减免。 2、实施援企稳岗政策:对受疫情影响较大,面临暂时性生产经营困难且恢复有望、坚持不裁员或少裁员**的参保企业,可按6个月的上年度本市月人均失业保险金标准和参保职工人数,返还失业保险费。 政策都出来了,就说明一定会有一波裁员。 为什么1月25日左右时,整个网上各地的网友都在留言呼吁自己所在的城市延期复工,而政府却一直在等一直在等?比如北京就是,快到不得已的时候才出台了延期复工的政策,之前一直是2月3日复工。为什么? 因为国家比你着急,国家知道每多一天数亿人不工作对国家和社会的影响,一天不搞生产,一天不搞经济,都是很严重的事情,我今天刚去了超市买菜,两个西红柿15元,两根黄瓜15元,一个小西瓜50元,连两块姜都超过10元。 疫情当下的影响是显性的,不搞生产不搞经济对国家和社会的影响是隐性的,收入下降,失业,各种社会问题、社会矛盾就会出现。 这也是为什么,国家在各种出台政策减少企业裁员。 02 一些行业尤为明显,但各方都会波及 旅游业、酒店、餐饮、商场、线下零售、交通出行、影视、线下娱乐场所(影院、KTV、网吧、健身房、游泳馆等)、线下培训教育等等。 这些行业影响很大,一定有裁员,比如旅游业,去年春节假期,全国总旅游人次是4.15亿,收入是5139亿,今年这5000亿去哪找? 这些行业成本高但利润率不高,需要源源不断的现金流进来才能持续经营,能赚大钱的一定是少数,很多都是平衡略有盈利,如果几个月没有现金进来,恐怕要裁员节流,甚至有的直接关掉,一部分人失业。 还有一种裁员,是转型裁。 比如很多餐饮转型成外卖的了,企业可能会很好的活着,但线下很多员工就不需要了;比如很多培训转型线上了,线下关停了,但并不是每一个线下的员工都能做线上;还有一些企业,这段时期会把不赚钱的好死不如赖活着的业务砍掉,会把探索型的未来一年内不见钱的业务砍掉不少,等等。 各方都有波及是什么意思? 就是你不一定会失业,但你需要换工作。因为不少本来就经营不善的企业可能要走到头了,做得好的会更好,行业内部整合重组,你还是可以在其它企业找到工作,此时你需要考虑的是:重新找工作,我还能拿到这个收入么?更少,还是更高? 03 如果你的公司必须裁员,会轮到你么? 这是一个很值得思考的假设: 假设你们公司有50人,要裁掉5—10人,会轮到你头上么? 这个问题的价值在于,如果是裁1%,那么大部分人都会觉得自己是安全的,但是如果是5%、10%、20%呢? 你还觉得安全么?你还自信一定不会轮到你头上么? 如果回答是好的,那么恭喜你。如果是否,那你要认真对待。 因此这个问题本质是: 在你们公司,你个人的安全线在哪里? 希望你是最安全的那20%。因为那时最核心的,除非公司彻底倒掉,否则轮不到你头上。 既然讲到这,你也可以再多问一句: 你们公司在行业的安全线在哪里?如果这个行业要洗牌重组,如果一定会倒闭20%的公司,会轮到你们公司么? 如果你回答“是”,那么疫情过后,即使公司不裁你,你也应该主动换一家,换一艘更安全的船。 【疫情之下,程序员该何去何从】 对于互联网行业,美团王兴曾说:“2019年可能会是过去十年里最差的一年,却是未来十年里最好的一年”。 没想到预言竟然快成真了?经过了2019年互联网寒冬的肆虐,2020年这场席卷全国的新冠疫情对于互联网人将更是一次雪上加霜的考验。 而此刻身处风暴中心的的程序员们该如何逆势而上? 唯有不断学习,不断更新自己的知识和技能,在一家公司找到自己独特的价值,让自己拥有不可替代性,永远居于前位,不被末位淘汰。 即使被清退,也有过硬的本领迅速找到更合适的工作。 在这里我整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。 主要包括腾讯,以及字节跳动,华为,小米,等一线互联网公司主流架构技术。如果你有需要,尽管拿走好了。 以下为我的整理,粉丝免费分享; 【阿里P7级全套高级学习视频】 全套部分展示;7大专题 点赞在评论区留言或者私信我,Android高级教程,我看到都会回复的 1.高级UI UI这块知识是现今使用者最多的。当年火爆一时的Android入门培训,学会这小块知识就能随便找到不错的工作了。 不过很显然现在远远不够了,所以很多人会觉得大环境不好了安卓开发要凉了。 这些人如果能自身反省;企业要你们这些CV工程师的意义在哪呢? 你要自己亲自去项目实战,读源码,研究原理的呀。 2.性能优化 如果我是老板,我招你来是写代码的,不是写bug的。如果你的代码太烂,各种bug。我把你开了后重新招个人进来接手维护,甚至推到重新做,后面那个接盘的是不是要骂街? 如果你会性能调优,能解决项目中各种性能问题。那么拿20K真的不过分。你得具备深厚的代码功底,深入学习源码原理以及使用工具进行测试和检查调优。 3.NDK开发 音视频,人工智能,这些是未来没办法阻挡的发展大趋势。我在猎聘网上看那些招聘岗位,要求精通NDK的薪资都在30-60K。 追求高薪岗位的小伙伴,NDK开发一定要掌握并且去深挖 4.Flutter Flutter火了一年多了,虽然你工作不一定要用到,但是你出去面试(初级可能不要求会),肯定会问到的。 关于Flutter是不是未来,我没法确定告诉你,我能确定的就是你要去面试高薪岗位,你得掌握这种主流的新技术(因为大厂最看重的除了基础,技术水平外,就是你的学习能力。) 5.移动架构实战项目 架构师不是天生的,是在项目中磨练起来的,所以,我们学了技术就需要结合项目进行实战训练,那么在Android里面最常用的架构无外乎 MVC,MVP,MVVM,但是这些思想如果和模块化,层次化,组件化混和在一起,那就不是一件那么简单的事了。 架构师尤其是移动开发,数量太少了。可能很多Android开发的小伙伴都没见过移动架构师。架构师薪资是什么样的水平呢? 阿里P6处于高级工程师,年薪四五十万左右阿里P7处于资深高级,年薪百万左右阿里P8属于架构师了,年薪可达170万以上 阿里的标准和薪资都是很高的,其它公司会有差距,但不会太大。 你有没有敢去想过,自己以后能达到架构师水平,突破百万年薪,实现财富自由呢?可能你觉得很遥远很可笑,那么你就真的遥不可及了。 点击石墨文档,免费领取以上视频教程: 阿里P7级Android架构视频+BAT面试专题PDF+学习笔记
在Flutter应用开发过程中,除了使用Flutter官方提供的路由外,还可以使用一些第三方路由框架来实现页面管理和导航,如Fluro、Frouter等。Fluro作为一款优秀的Flutter企业级路由框架,Fluro的使用比官方提供的路由框架要复杂一些,但是却非常适合中大型项目。因为它具有层次分明、条理化、方便扩展和便于整体管理路由等优点。使用Fluro之前需要先在pubspec.yaml文件中添加Fluro依赖,如下所示。 dependencies: fluro: "^1.5.1" 如果无法使用上面的方式添加Fluro依赖,还可以使用git的方式添加Fluro依赖,如下所示。 dependencies: fluro: git: git://github.com/theyakka/fluro.git 成功添加Fluro库依赖后,就可以使用Fluro进行应用的路由管理与导航开发了。为了方便对路由进行统一的管理,首先需要新建一个路由映射文件,用来对每个路由进行管理。如下所示,是路由配置文件route_handles.dart的示例代码。 import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:flutter_demo/page_a.dart'; import 'package:flutter_demo/page_b.dart'; import 'package:flutter_demo/page_empty.dart'; //空页面 var emptyHandler = new Handler( handlerFunc: (BuildContext context, Map<String, List<String>> params) { return PageEmpty(); }); //A页面 var aHandler = new Handler( handlerFunc: (BuildContext context, Map<String, List<Object>> params) { return PageA(); }); //B页面 var bHandler = new Handler( handlerFunc: (BuildContext context, Map<String, List<Object>> params) { return PageB(); }); 完成基本的路由配置后,还需要一个静态的路由总体配置文件,方便我们在路由页面中使用。如下所示,是路由总体配置文件routes.dart的示例代码。 import 'package:fluro/fluro.dart'; import 'package:flutter_demo/route_handles.dart'; class Routes { static String page_a = "/"; //需要注意 static String page_b = "/b"; static void configureRoutes(Router router) { router.define(page_a, handler: aHandler); router.define(page_b, handler: bHandler); router.notFoundHandler =emptyHandler; //空页面 } } 在进行路由的总体配置时,还需要处理不存在的路径情况,即使用空页面或者默认页面进行代替。同时,需要注意的是应用的首页一定要用“/”进行配置。为了方便使用,还需要把Router进行静态化,这样在任何一个页面都可以直接调用它。如下所示,是application.dart文件的示例代码。 import 'package:fluro/fluro.dart'; class Application{ static Router router; } 完成上述操作后,就可以在main.dart文件中引入路由配置文件和静态化文件了,如下所示。 import 'package:fluro/fluro.dart'; import 'package:flutter_demo/routes.dart'; import 'application.dart'; void main() { Router router = Router(); Routes.configureRoutes(router); Application.router = router; runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Demo App', onGenerateRoute: Application.router.generator, ); } } 如果要在跳转到某个页面,只需要使用Application.router.navigateTo()方法即可,如下所示。 Application.router.navigateTo(context,"/b"); //b为配置路由 运行上面的示例代码,效果如下图所示。 可以发现,Fluro虽然使用上相比Flutter的Navigator要繁琐,但是对于中大型项目却非常适合,它的分层架构也非常方便项目后期的升级和维护,使用时可以根据实际情况进行合理的选择。 原文作者:xiangzhihong 原文链接:人类身份验证 - SegmentFault 来源:思否
本人2016年6月毕业,转眼间毕业已经三年多了,2019年1月份开始找自己的第四份工作。回顾2019年前面两年多的蹉跎时间,总是为自己当时的不成熟而悔恨,要是早点醒悟,早点努力,说不定可以达到另一个层次。 刚毕业校招去了一个公司给华为做外包。当时对外包没什么概念,身边同学一个劲的说不好不好,但是当时感觉工资也不错,自己也只面试过了这家所以就去了。其实后来想想,这竟是我前三份工作中最好的公司,除了技术方面。 当时我校招面试的是Android,但是后来去了公司没岗位之后,又把我换到了web后台,刚毕业,技术又菜,虽说都是Java,但是当时对我来说还是太难了,启动个项目都启动不来。因为技术菜,所以对分的需求总是抱着畏惧,总怕万一做不出来怎么办,前几个月每天都生活在恐惧中。 然后工作肯定比在学校累,对加班的抵触情绪也很大,每天睁眼一想到又要去公司了,就心慌。平时下来自己也没怎么专研技术,导致一直没有进步。而且在这里虽说是Java后台,但是写的东西都是html css js 后端就接触到controller层,当时又没有主动专研的习惯,技术一直停留在原地。 到了17年三月份,就离职,当时想的是去考研继续读书。当时其实只是一种逃避罢了,辞职全职准备了一个多月,发现坚持不下去了,就又去找工作了。又没有什么准备,当时映像最深的就是面试中移物联网,在成都算是挺不错的公司了,一面电话面试过了之后高兴得不得了,好像自己马上就要踏进去了似的。二面的时候就问得要难一些了,当时自己写了那么久的html css js。对后端技术完全没什么概念。去了问了我熟悉lunix吗?我说不熟悉。问了我熟悉JVM吗?我说不知道。当时真是啥都不知道。可想而知,几分钟就叫我出来了。当时那种心情,哎。 时间回到2019年一月份,我已经开始了自己第4份工作,当然这也看得出我是有多浮躁,前面两个公司都是自己太不成熟了,遇到一点困难就想退缩,遇到一点不爽就想离职,真正成熟是在2017年的12月份开始的第三份工作,经历了前面的两份工作,自己对待加班,对待不合理需求抵抗情绪也基本没有了,基本能保持一颗平常心了。也开始认识到技术的重要性了。 当时看着自己的同学进到一线互联网企业,自己却还是在小公司,先不说钱多钱少,感觉事业上的差距也已经越来越明显了。内心有了一丝心慌。 然后朋友内推去了一个创业公司,这个创业公司接触到的后端的技术就还是挺多了,但是毕竟是创业公司,很多都是摸着石头过河,并且我从Java开发工程师变成了一个售后工程师,每天和客户打交道,处理问题,周末也是电话不断,所以没干几个月就又辞职了。经历了两个公司之后,自己也慢慢成熟了很多,首先感觉自己很不能吃苦,遇到问题就想逃避,所以在进入第三个公司的时候,自己就告诫自己一定要沉下心来,学习技术,不怕吃苦。 节选自(不穿裤子的衣服:https://www.cnblogs.com/softjiang/p/10382183.html) 很多程序员朋友都曾陷入迷茫困惑,想深耕技术,却不知如何下手。那么作为一名Android开发人员,我们究竟应该学什么? 对于Android架构师职责的介绍,网上已经铺天盖地,就不再赘述。今天我主要给大家分享一下成为一名Android架构师应该掌握的技术能力。 阿里公司注重的7大主流技术专题与移动架构师项目实战 本人花了一年多时间最新整理出一份阿里P7级别的Android架构师全套学习资料,特别适合工作3-5年以上经验的小伙伴深入学习提升。 主要包括腾讯,以及字节跳动,华为,小米,等一线互联网公司主流架构技术。旨在帮助Android架构进阶陷入迷茫困惑的小伙伴。同时本人也非常欢迎大家补充建议、批评指正、互相交流技术,共同成长。 1.阿里P7级别Android架构师技术脑图;查漏补缺,体系化深入学习提升 2..全套体系化高级架构视频(七大主流技术模块) 3.Android架构师精编解析大全(含答案解析) 4.设计模式和数据结构算法专题;大厂必会,巩固基础 设计模式专题; 数据结构算法专题; 免费分享(下载地址) https://shimo.im/docs/vrvxvW8DY3RTDGGg 为什么免费分享? 很多开发人员工作几年,技术薪资均没有提升。 程序开发是吃青春饭的工作,有很多志在学习提升,却又苦于找不到学习方向和路线的开发人员。 希望大家通过我分享的这套高级架构资料,结合自身不足、重点学习、系统学习、早日进阶成为Android高级架构师。实现个人理想和创造更多价值。 不负青春对我们的期待,不负时代对我们鞭策。 Android架构师之路很漫长,一起共勉吧!喜欢的话可以添加我微信好友,一起交流讨论。
首先实现一个头部固定的ExpandedListView,然后在它的基础上实现:在头部加一个背景图片,默认状态下他处于展开状态,往上滑的时候背景图片逐渐的折叠起来,往下滑的时候背景图片慢慢的展开效果图如下: 通过CoordinateLayout实现的折叠式布局 有人可能会说这不就是折叠式布局吗?是的,这就是Android 5.0给我们提供的材料设计库中的CoordinateLayout就是解决这个问题的,使用CoordinateLayout来协调ScrollView,NestedScrollView,ListView,RecycleView和顶部的背景图片、ToolBar之间的滚动关系、在很多的手机应用中,时不时会看到关于折叠布局的效果,现在我们先看看CoordinateLayout是怎么实现的然后在讲我们自定义实现一个折叠式布局,直接上代码: <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="200dp"> <android.support.design.widget.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:titleEnabled="false"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:src="@mipmap/homepage_pic_banner" app:layout_collapseMode="parallax" /> <android.support.v7.widget.Toolbar android:id="@+id/view_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="新闻详情" /> </android.support.v7.widget.Toolbar> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:text="hello world" /> ... ... </LinearLayout> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout> 以上就是实现一个折叠式布局的典型模板布局代码,一个简简单单的布局就实现了这样的效果,但是必须要注意在AndroidMnifest.xml必须要给Activity指定它的theme为NoActionBar的样式代码如下: <activity android:name=".test.CoordinatorLayoutTestActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> 否则会出现ActionBar和ToolBar共存的情况,的显示效果如下: 另外还需要把自己自定义的ToolBar告诉给系统,即第9行的setSupportActionBar(toolbar),否则我们的ToolBar会作为一个普通的View而存在 public class CoordinatorLayoutTestActivity extends AppCompatActivity { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_coordinator_layout_test); Toolbar toolbar = findViewById(R.id.view_toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayShowTitleEnabled(false); toolbar.setNavigationIcon(R.mipmap.callback_white_icon); toolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onBackPressed(); } }); } } 如果只指定了 setSupportActionBar(toolbar),没有AndroidMnifest.xml在指定Activity的theme为NoActionBar,那就运行就直接崩溃了,会报错如下: Caused by: java.lang.IllegalStateException: This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead. at android.support.v7.app.AppCompatDelegateImpl.setSupportActionBar(AppCompatDelegateImpl.java:345) at android.support.v7.app.AppCompatActivity.setSupportActionBar(AppCompatActivity.java:130) 意思是说Activity已经有一个ActionBar了,请在你的样式中使用ToolBar替代 在上面的布局文件代码中,根布局CoordinatorLayout 就是用来协调AppBarLayout和NestedScrollView之间滚动的,40行的NestedScrollView是我们要滚动的内容,在11行的CollapsingToolbarLayout标签的内部就是要折叠的内容 其中43行的 app:layout_behavior不配置的效果:  NestedScrollView的内容在ToolBar之上滚动 其中13行app:layout_scrollFlags="scroll|exitUntilCollapsed"如果不配置效果图如下:  如果没有配置则CollapsingToolbarLayout包裹内容内容就会固定在顶部,不会滚动 28行 app:layout_collapseMode="pin"不配置,效果图:  ToolBar会跟随NestedScrollView的滚动而滚动,而不会固定在布局顶部位置 14行app:titleEnabled="false"不配置,效果图:  即使33行的TextView配置了android:layout_gravity="center",title也不会居中显示 我们感觉折叠式布局就是给我们的View设置相关的属性配置,不需要进行任何编码就能完成我们的折叠效果,我们不得的赞叹android 5.0给我们提供这一强大的功能 我们来总结一下:CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout结合起来才能产生这么神奇的效果,不要幻想使用其中的一个控件就能完成这样的效果 ToolBar的设置 系统默认使用的就是系统自带的ActionBar,如果我们要使用自定义的ToolBar,就必须明确的告诉Activity不需要使用系统自带的ActionBar即要给activity设置NoActionBar的样式,另外必须调用setSupportActionBar(toolbar)将自己定义的ToolBar设置给Activity。 CoordinatorLayout下可滑动控件的设置 CoordinatorLayout作为整个布局的父布局容器。给你的可以滑动的控件例如RecyclerView设置如下属性:app:layout_behavior=@string/appbar_scrolling_view_behaviorCoordinatorLayout还提供了layout_anchor 和 layout_anchorGravity属性一起配合使用,可以用于设置FloatingActionButton的位置,此处我是放在appBar的右下角。app:layout_anchor="@id/appbar"app:layout_anchorGravity="bottom|right|end" CollapsingToolbarLayout的layout_scrollFlags属性 AppBarLayout里面定义的子view只要设置了app:layout_scrollFlags属性,就可以在RecyclerView滚动事件发生的时候被触发某种行为例如我给CollapsingToolbarLayout控件设置了 app:layout_scrollFlags="scroll|exitUntilCollapsed"此刻如果没有这个属性,CollapsingToolbarLayout是不会折叠的那么问题来了,layout_scrollFlags中的属性值除了可以触发折叠的行为,还有其它的属性值吗?并且各个属性的意义是什么?scroll至少有一个scroll,即可滚动。 属性 作用 scroll 必须要给其至少有设置一个scroll,即可滚动 enterAlways 向下滚动即可见。例如下拉时,立即显示Toolbar exitUntilCollapsed 这个flag是定义何时收缩。当你定义了一个minHeight,这个view将在滚动到达这个最小高度的时候消失 enterAlwaysCollapsed 这个flag是定义何时展开。当你定义了一个最小高度minHeight, 同时enterAlways也定义了,那么view将在到达这个最小高度的时候开始展示 snap 当一个滚动事件结束,它将根据显示百分比的大小自动滚动到收缩或展开。 如果不设置该属性,则该布局不能滑动 CollapsingToolbarLayout的其他属性 另外还可以给CollapsingToolbarLayout设置以下属性: 属性 作用 contentScrim 设置当完全折叠(收缩)后的背景颜色。 expandedTitleMarginEnd 没有扩张的时候标题显示的位置 expandedTitleMarginStart 扩张的时候标题向左填充的距离。 statusBarScrim 设置折叠时状态栏的颜色 CollapsingToolbarLayout下的view的layout_collapseMode属性 CollapsingToolbarLayout里面定义的view只要设置了app:layout_collapseMode属性,就可以控制子视图的折叠模式。折叠模式分为两种: 属性 作用 pin 固定模式。在收缩的时候最后固定在顶端(例如向上滚动的时候就固定toolBar) parallax 视差模式,在折叠的时候会有个视差折叠的效果。(例如向下滚动的时候就展开ImageView) CoordinatorLayout 的fitsSystemWindows属性 fitsSystemWindows属性可以让view根据系统窗口来调整自己的布局,简单点说就是我们在设置应用布局时是否考虑系统窗口布局,这里系统窗口包括系统状态栏、导航栏、输入法等,包括一些手机系统带有的底部虚拟按键。android:fitsSystemWindows=”true” (触发View的padding属性来给系统窗口留出空间) 这个属性可以给任何view设置,只要设置了这个属性此view的其他所有padding属性失效,同时该属性的生效条件是只有在设置了透明状态栏(StatusBar)或者导航栏(NavigationBar)此属性才会生效 如何监听CollapsingToolbarLayout的展开与折叠 使用官方提供的 AppBarLayout.OnOffsetChangedListener就能实现了,不过要封装一下才好用,自定义一个继承了 AppBarLayout.OnOffsetChangedListener的类这里命名为AppBarStateChangeListener public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener { public enum State { EXPANDED, COLLAPSED, IDLE } private State mCurrentState = State.IDLE; @Override public final void onOffsetChanged(AppBarLayout appBarLayout, int i) { if (i == 0) { if (mCurrentState != State.EXPANDED) { onStateChanged(appBarLayout, State.EXPANDED); } mCurrentState = State.EXPANDED; } else if (Math.abs(i) >= appBarLayout.getTotalScrollRange()) { if (mCurrentState != State.COLLAPSED) { onStateChanged(appBarLayout, State.COLLAPSED); } mCurrentState = State.COLLAPSED; } else { if (mCurrentState != State.IDLE) { onStateChanged(appBarLayout, State.IDLE); } mCurrentState = State.IDLE; } } public abstract void onStateChanged(AppBarLayout appBarLayout, State state); } 然后我们这样使用它: mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() { @Override public void onStateChanged(AppBarLayout appBarLayout, State state) { Log.d("STATE", state.name()); if( state == State.EXPANDED ) { //展开状态 }else if(state == State.COLLAPSED){ //折叠状态 }else { //中间状态 } } }); 这样就可以在不同的状态下根据自己的业务需求去实现相关的逻辑了 StickyLayout自定折叠式布局的实现 好了,上面就是关于通过CoordinateLayout实现的折叠式布局所有的知识点,如果说前面只是开胃菜,现在我们就开始上主菜了,我们能不能自己实现这样一个折叠式的布局,利用上一篇我们所讲的头部固定的ExpandedListView,把它作为具有滑动功能的主View,在它的顶部添加具有背景图片Header,随着ExpandedListView的滑动header实现扩展和收缩的效果,效果如下: 功能分析 其实这个效果图在文章的一开始就展示过了,整个布局分为上下两部分:上分部分为可折叠的Header,下半部分就是我们头部固定的ExpandedListView,他们公共父view就是今天我们要实现的折叠式布局StickyLayout,ExpandedListView是自身所具备滑动功能的,而我们在整个屏幕上,往上滑动的时候如果header处于展开状态则Header慢慢的要折叠起来,往下滑动的时候如果ExpandedListView顶部数据都显示出来的情况下再往下拉的时候Header就慢慢的展开,其他的状态就是我们的ExpandedListView在上下滑动,也就是说我们的Header在折叠和展开的状态下的这些事件被StickyLayout拦截了,其他的事件就交给ExpandedListView进行处理从而实现了他的上下滑动,这就属于典型的滑动冲突问题,简言之就是我们在上下滑动的过程中的有些事件需要被StickyLayout拦截消掉来实现Header的折叠和展开效果,其他的事件就交给ExpandedListView来实现它的滑动效果现在我们要思考的是哪些情况下被拦截: 左右滑动的不需要处理,只处理上下滑动的事件 在展开的状态下,上滑事件需要拦截 ExpandedListView的第0个元素处于可见状态,此时的下滑事件需要拦截 在事件拦截方法中处理滑动冲突 public class StickyLayout extends LinearLayout { ... @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: mLastInterceptX = x; mLastInterceptY = y; intercept = false; break; case MotionEvent.ACTION_MOVE: int dx = x - mLastInterceptX; int dy = y - mLastInterceptY; if(y <= mCurrHeaderHeight){ intercept = false; }else if(Math.abs(dx) > Math.abs(dy)){ intercept = false; }else if(mState == mStateExpand && dy <= - mScaledTouchSlop){ //上滑 intercept = true; }else if(mGiveUpTouchEventListener.giveUpTouchEvent() && dy > mScaledTouchSlop){ //下滑 intercept = true; }else{ intercept = false; } break; case MotionEvent.ACTION_UP: mLastInterceptX = 0; mLastInterceptY = 0; intercept = false; break; } return intercept; } ... } 上面就是关于事件拦截的核心代码,首先我们看17行:y <= mCurrHeaderHeight 如果触摸事件是在Header之上也就不拦截了,再看19行Math.abs(dx) > Math.abs(dy),如果是横向滑动也不是我们所需要的事件也不拦截,否则上就是上下滑动的事件了,在这个状态下状态Header处于展开状态且是上滑那就需要拦截处理,也就是21行:mState == mStateExpand && dy <= - mScaledTouchSlop所处理的逻辑,在看24行:mGiveUpTouchEventListener.giveUpTouchEvent() && dy > mScaledTouchSlop,giveUpTouchEvent方法表示如果ExpandedListView的第一个可见元素是0且dy > mScaledTouchSlop(表示是上滑)此时的事件也是需要拦截的 View滑动距离常量TouchSlop 在21行细心的同学可能会看到这么一句dy <= - mScaledTouchSlop,dy指的是滑动的距离,mScaledTouchSlop到底是什么?其实他是Android系统给我们提供的View滑动最小距离常量TouchSlop,也就是说两个Move事件之间的滑动距离如果小于这个常量就系统不认为他是滑动,因为滑动距离太短,反之就认为它是滑动,这个常量值和设备有关,不同的设置上这个值可能是不相同的,我们可以通过如下方式即可获取这个常量: int mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 折叠展开的事件消费 上面的17到29行就是处理事件拦截的核心处理逻辑,事件拦截完毕,事件就交给TouchEvent方法进行消费了,下面看看Header到底具体是怎么折叠的?其实很简单就是不用重置Header的height就OK了,我们看看代码: public class StickyLayout extends LinearLayout { ... @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int dx = x - mLastX; int dy = y - mLastY; mCurrHeaderHeight += dy; setHeaderHeight(mCurrHeaderHeight); break; case MotionEvent.ACTION_UP: int dest = 0; if(mCurrHeaderHeight <= mOriginHeaderHeight * 0.5){ dest = 0; mState = mStateCollapsed; }else{ dest = mOriginHeaderHeight; mState = mStateExpand; } smoothSetHeaderHeight(mCurrHeaderHeight,dest,500); break; } mLastX = x; mLastY = y; return super.onTouchEvent(event); } ... } 其中12行到13行就是手指拖动状态下的核心逻辑 ,12行计算两次Move事件所移动的距离,13行根据手指滑动的距离来计算Header当前的高度,计算完毕就可以调用setHeaderHeight设置Header的高了 设置Header的高来实现折叠效果 private void setHeaderHeight(int height) { if(height <= 0){ height = 0; }else if(height >= mOriginHeaderHeight){ height = mOriginHeaderHeight; } if(height == 0 ){ mState = mStateCollapsed; }else{ mState = mStateExpand; } headerView.getLayoutParams().height = height; headerView.requestLayout(); } 其中第2行到第6行对Header高度的越界处理,第7行到11行是设置Header的状态,第12行到13行给Header的高赋值并刷新Header来变它的位置与大小 手指抬起的自动回弹折叠展开效果 如果当前Header的高小于原始高度的一半手指抬起的时候Header进行收缩,反之就进行展开操作,核心代码在上面的onTouchEvent(MotionEvent event)方法的的17行到25行: int dest = 0; if(mCurrHeaderHeight <= mOriginHeaderHeight * 0.5){ dest = 0; mState = mStateCollapsed; }else{ dest = mOriginHeaderHeight; mState = mStateExpand; } smoothSetHeaderHeight(mCurrHeaderHeight,dest,500); 最后在调用smoothSetHeaderHeight实现弹性展开,折叠 private void smoothSetHeaderHeight(int from,int to,int duration) { ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to).setDuration(duration); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setHeaderHeight((Integer) animation.getAnimatedValue()); } }); valueAnimator.start(); } 总结 截止目前整个折叠式自定义View就全部讲完了,事件拦截这块的判断逻辑是整个代码的核心,找到了判断折叠、展开的的算法那么其他的问题也就不是什么大问题了,解决滑动冲突问题也是我们在自定义View开发过程中的常见问题,也是难点问题,只要多练习,多思考就能孰能生巧最后我将整个测试代码传到了github上,欢迎学习下载https://github.com/mxdldev/android-custom-view/,其中StickyLayout.java就是我们本例中的自定义View的全部代码实现,下载完整项目后直接运行安装完毕,点击StickyLayout按钮就进入了我们的测试页面,效果图如下: 作者:门心叼龙链接:https://www.jianshu.com/p/1ba947bc0a98
本文用于记录自定义View的基础步骤以及一些基础的信息,后期可能针对具体的点写一些补充性的文章。 一 、View中关于四个构造函数参数 自定义View中View的构造函数有四个 // 主要是在java代码中生命一个View时所调用,没有任何参数,一个空的View对象 public ChildrenView(Context context) { super(context); } // 在布局文件中使用该自定义view的时候会调用到,一般会调用到该方法 public ChildrenView(Context context, AttributeSet attrs) { this(context, attrs,0); } //如果你不需要View随着主题变化而变化,则上面两个构造函数就可以了 //下面两个是与主题相关的构造函数 public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } // public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } 四个参数解释: context:上下文 AttributeSet attrs:从xml中定义的参数 intdefStyleAttr:主题中优先级最高的属性 intdefStyleRes: 优先级次之的内置于View的style(这里就是自定义View设置样式的地方) 多个地方定义属性,优先级排序 Xml直接定义 > xml中style引用 > defStyleAttr>defStyleRes > theme直接定义 (参考文章:www.jianshu.com/p/7389287c0…) 二、自定义属性说明 除了基本类型的不说 讲一下其它几个吧: color :引用颜色 dimension: 引用字体大小 //定义 <attr name = "text_size" format = "dimension" /> //使用: app:text_size = "28sp" 或者 app:text_size = "@android:dimen/app_icon_size" enum:枚举值 //定义 <attr name="orientation"> <enum name="horizontal" value="0" /> <enum name="vertical" value="1" /> </attr> //使用: app:orientation = "vertical" flags:标志 (位或运行) 主要作用=可以多个值 //定义 <attr name="gravity"> <flag name="top" value="0x01" /> <flag name="bottom" value="0x02" /> <flag name="left" value="0x04" /> <flag name="right" value="0x08" /> <flag name="center_vertical" value="0x16" /> </attr> // 使用 app:gravity = Top|left fraction:百分数: //定义: <attr name = "transparency" format = "fraction" /> //使用: app:transparency = "80%" reference:参考/引用某一资源ID //定义: <attr name="leftIcon" format="reference" /> //使用: app:leftIcon = "@drawable/图片ID" 混合类型:属性定义时指定多种类型值 //属性定义 <attr name = "background" format = "reference|color" /> //使用 android:background = "@drawable/图片ID" //或者 android:background = "#FFFFFF" 三、自定义控件类型 自定义组合控件步骤 1. 自定义属性 在res/values目录下的attrs.xml文件中 <resources> <declare-styleable name="CustomView"> <attr name="leftIcon" format="reference" /> <attr name="state" format="boolean"/> <attr name="name" format="string"/> </declare-styleable> </resources> 2. 布局中使用自定义属性 在布局中使用 <com.myapplication.view.CustomView android:layout_width="wrap_content" android:layout_height="wrap_content" app:leftIcon="@mipmap/ic_temp" app:name="温度" app:state="false" /> 3. view的构造函数获取自定义属性 class DigitalCustomView : LinearLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { LayoutInflater.from(context).inflate(R.layout.view_custom, this) var ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView) mIcon = ta.getResourceId(R.styleable.CustomView_leftIcon, -1) //左图像 mState = ta.getBoolean(R.styleable.DigitalCustomView_state, false) mName = ta.getString(R.styleable.CustomView_name) ta.recycle() initView() } } 上面给出大致的代码 记得获取context.obtainStyledAttributes(attrs, R.styleable.CustomView)最后要调用ta.recycle()利用对象池回收ta加以复用 继承系统控件 就是继承系统已经提供好给我们的控件例如TextView、LinearLayout等,分为View类型或者ViewGroup类型的两种。主要根据业务需求进行实现,实现重写的空间也很大 主要看需求。 比如需求 :在文字后面加个颜色背景 根据需要一般这种情况下我们是希望可以复用系统的onMeaseur和onLayout流程.直接复写onDraw方法 class Practice02BeforeOnDrawView : AppCompatTextView { internal var paint = Paint(Paint.ANTI_ALIAS_FLAG) internal var bounds = RectF() constructor(context: Context) : super(context) {} constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {} constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} init { paint.color = Color.parseColor("#FFC107") } override fun onDraw(canvas: Canvas) { // 把下面的绘制代码移到 super.onDraw() 的上面,就可以让原主体内容盖住你的绘制代码了 // (或者你也可以把 super.onDraw() 移到这段代码的下面) val layout = layout bounds.left = layout.getLineLeft(1) bounds.right = layout.getLineRight(1) bounds.top = layout.getLineTop(1).toFloat() bounds.bottom = layout.getLineBottom(1).toFloat() //绘制方形背景 canvas.drawRect(bounds, paint) super.onDraw(canvas) } } 这里会涉及到画笔Paint()、画布canvas、路径Path、绘画顺序等的一些知识点,后面再详细说明 直接继承View 这种就是类似TextView等,不需要去轮训子View只需要根据自己的需求重写onMeasure()、onLayout()、onDraw()等方法便可以,要注意点就是记得Padding等值要记得加入运算 private int getCalculateSize(int defaultSize, int measureSpec) { int finallSize = defaultSize; int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); // 根据模式对 switch (mode) { case MeasureSpec.EXACTLY: { ... break; } case MeasureSpec.AT_MOST: { ... break; } case MeasureSpec.UNSPECIFIED: { ... break; } } return finallSize; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getCalculateSize(120, widthMeasureSpec); int height = getCalculateSize(120, heightMeasureSpec); setMeasuredDimension(width, height); } //画一个圆 @Override protected void onDraw(Canvas canvas) { //调用父View的onDraw函数,因为View这个类帮我们实现了一些基本的而绘制功能,比如绘制背景颜色、背景图片等 super.onDraw(canvas); int r = getMeasuredWidth() / 2; //圆心的横坐标为当前的View的左边起始位置+半径 int centerX = getLeft() + r; //圆心的纵坐标为当前的View的顶部起始位置+半径 int centerY = getTop() + r; Paint paint = new Paint(); paint.setColor(Color.RED); canvas.drawCircle(centerX, centerY, r, paint); } 直接继承ViewGroup 类似实现LinearLayout等,可以去看那一下LinearLayout的实现 基本的你可能要重写onMeasure()、onLayout()、onDraw()方法,这块很多问题要处理包括轮训childView的测量值以及模式进行大小逻辑计算等,这个篇幅过大后期加多个文章写详细的 这里写个简单的需求,模仿LinearLayout的垂直布局 class CustomViewGroup :ViewGroup{ constructor(context:Context):super(context) constructor(context: Context,attrs:AttributeSet):super(context,attrs){ //可获取自定义的属性等 } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) //将所有的子View进行测量,这会触发每个子View的onMeasure函数 measureChildren(widthMeasureSpec, heightMeasureSpec) val widthMode = MeasureSpec.getMode(widthMeasureSpec) val widthSize = MeasureSpec.getSize(widthMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) val childCount = childCount if (childCount == 0) { //没有子View的情况 setMeasuredDimension(0, 0) } else { //如果宽高都是包裹内容 if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { //我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度 val height = getTotalHeight() val width = getMaxChildWidth() setMeasuredDimension(width, height) } else if (heightMode == MeasureSpec.AT_MOST) { //如果只有高度是包裹内容 //宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和 setMeasuredDimension(widthSize, getTotalHeight()) } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容 //宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值 setMeasuredDimension(getMaxChildWidth(), heightSize) } } /*** * 获取子View中宽度最大的值 */ private fun getMaxChildWidth(): Int { val childCount = childCount var maxWidth = 0 for (i in 0 until childCount) { val childView = getChildAt(i) if (childView.measuredWidth > maxWidth) maxWidth = childView.measuredWidth } return maxWidth } /*** * 将所有子View的高度相加 */ private fun getTotalHeight(): Int { val childCount = childCount var height = 0 for (i in 0 until childCount) { val childView = getChildAt(i) height += childView.measuredHeight } return height } } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { val count = childCount var currentHeight = t for (i in 0 until count) { val child = getChildAt(i) val h = child.measuredHeight val w = child.measuredWidth //摆放子view child.layout(l, currentHeight, l + w, currentHeight + h) currentHeight += h } } } 主要两点 先 measureChildren()轮训遍历子View获取宽高,并根据测量模式逻辑计算最后所有的控件的所需宽高,最后setMeasuredDimension()保存一下 ###四、 View的绘制流程相关 最基本的三个相关函数 measure() ->layout()->draw() 五、onMeasure()相关的知识点 1. MeasureSpec MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。 MeasureSpec 的数据是int类型,有32位。 高两位表示模式,后面30位表示大小size。则MeasureSpec = mode+size 三种模式分别为:EXACTLY,AT_MOST,UNSPECIFIED EXACTLY: (match_parent或者 精确数据值)精确模式,对应的数值就是MeasureSpec当中的size AT_MOST:(wrap_content)最大值模式,View的尺寸有一个最大值,View不超过MeasureSpec当中的Size值 UNSPECIFIED:(一般系统使用)无限制模式,View设置多大就给他多大 //获取测量模式 val widthMode = MeasureSpec.getMode(widthMeasureSpec) //获取测量大小 val widthSize = MeasureSpec.getSize(widthMeasureSpec) //通过Mode和Size构造MeasureSpec val measureSpec = MeasureSpec.makeMeasureSpec(size, mode); 2. View #onMeasure()源码 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); } public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; } private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; } setMeasuredDimension(int measuredWidth, int measuredHeight) :用来设置View的宽高,在我们自定义View保存宽高也会要用到。 getSuggestedMinimumWidth():当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0. 如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。 getDefaultSize(int size, int measureSpec):用来获取View默认的宽高,在getDefaultSize()中对MeasureSpec.AT_MOST,MeasureSpec.EXACTLY两个的处理是一样的,我们自定义View的时候 要对两种模式进行处理。 3. ViewGroup中并没有measure()也没有onMeasure() 因为ViewGroup除了测量自身的宽高,还需要测量各个子View的宽高,不同的布局测量方式不同 (例如 LinearLayout跟RelativeLayout等布局),所以直接交由继承者根据自己的需要去复写。但是里面因为子View的测量是相对固定的,所以里面已经提供了基本的measureChildren()以及measureChild()来帮助我们对子View进行测量 这个可以看一下我另一篇文章:LinearLayout # onMeasure()LinearLayout onMeasure源码阅读 六、onLayout()相关 View.java的onLayout方法是空实现:因为子View的位置,是由其父控件的onLayout方法来确定的。 onLayout(int l, int t, int r, int b)中的参数l、t、r、b都是相对于其父 控件的位置。 自身的mLeft, mTop, mRight, mBottom都是相对于父控件的位置。 1. Android坐标系 2. 内部View坐标系跟点击坐标 3. 看一下View#layout(int l, int t, int r, int b)源码 public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); // ....省略其它部分 } private boolean setOpticalFrame(int left, int top, int right, int bottom) { Insets parentInsets = mParent instanceof View ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets(); return setFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); } protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; // ....省略其它部分 if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; int drawn = mPrivateFlags & PFLAG_DRAWN; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); mPrivateFlags |= PFLAG_HAS_BOUNDS; if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) { mPrivateFlags |= PFLAG_DRAWN; invalidate(sizeChanged); invalidateParentCaches(); } mPrivateFlags |= drawn; mBackgroundSizeChanged = true; mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } notifySubtreeAccessibilityStateChangedIfNeeded(); } return changed; } 四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。 在调用onLayout(changed, l, t, r, b);之前都会调用到setFrame()确定View在父容器当中的位置,赋值给mLeft,mTop,mRight,mBottom。 在ViewGroup#onLayout()跟View#onLayout()都是空实现,交给继承者根据自身需求去定位 部分零散知识点: getMeasureWidth()与getWidth() getMeasureWidth()返回的是mMeasuredWidth,而该值是在setMeasureDimension()中的setMeasureDimensionRaw()中设置的。因此onMeasure()后的所有方法都能获取到这个值。 getWidth返回的是mRight-mLeft,这两个值,是在layout()中的setFrame()中设置的. getMeasureWidthAndState中有一句: This should be used during measurement and layout calculations only. Use {@link #getWidth()} to see how wide a view is after layout. 总结:只有在测量过程中和布局计算时,才用getMeasuredWidth()。在layout之后,用getWidth()来获取宽度 七、draw()绘画过程 /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1\. Draw the background * 2\. If necessary, save the canvas' layers to prepare for fading * 3\. Draw view's content * 4\. Draw children * 5\. If necessary, draw the fading edges and restore layers * 6\. Draw decorations (scrollbars for instance) */ 上面是draw()里面写的绘画顺序。 绘制背景。 如果必要的话,保存当前canvas 绘制View的内容 绘制子View 如果必要的话,绘画边缘重新保存图层 画装饰(例如滚动条) 1. 看一下View#draw()源码的实现 public void draw(Canvas canvas) { // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); drawAutofilledHighlight(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; } } 由上面可以看到 先调用drawBackground(canvas) ->onDraw(canvas)->dispatchDraw(canvas)->onDrawForeground(canvas)越是后面绘画的越是覆盖在最上层。 drawBackground(canvas):画背景,不可重写 onDraw(canvas):画主体 代码写在super.onDraw()前:会被父类的onDraw覆盖 代码写在super.onDraw()后:不会被父类的onDraw覆盖 dispatchDraw() :绘制子 View 的方法 代码写在super.dispatchDraw(canvas)前:把绘制代码写在 super.dispatchDraw() 的上面,这段绘制就会在 onDraw() 之后、 super.dispatchDraw() 之前发生,也就是绘制内容会出现在主体内容和子 View 之间。而这个…… 其实和重写 onDraw() 并把绘制代码写在 super.onDraw() 之后的做法,效果是一样的。 代码写在super.dispatchDraw(canvas)后:只要重写 dispatchDraw(),并在 super.dispatchDraw() 的下面写上你的绘制代码,这段绘制代码就会发生在子 View 的绘制之后,从而让绘制内容盖住子 View 了。 onDrawForeground(canvas):包含了滑动边缘渐变和滑动条跟前景 一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。 2. 注意事项 2.1 在 ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false); 出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)。 2.2 在重写的方法有多个选择时,优先选择 onDraw() 一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。 八、在Activity中获取View的宽高的几种方式 Activity 获取 view 的宽高, 在 onCreate , onResume 等方法中获取到的都是0, 因为 View 的测量过程并不是和 Activity 的声明周期同步执行的 1. view.post post 可以将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候, View 也已经初始化好了 view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); 2. ViewTreeObserver 使用 addOnGlobalLayoutListener 接口, 当 view 树的状态发生改变或者 View 树内部的 view 的可见性发生改变时, onGlobalLayout 都会被调用, 需要注意的是, onGlobalLayout 方法可能被调用多次, 代码如下: view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); 3. onWindowFocusChanged 这个方法的含义是 View 已经初始化完毕了, 宽高已经准备好了, 需要注意的就是这个方法可能会调用多次, 在 Activity onResume 和onPause的时候都会调用, 也会有多次调用的情况 @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if(hasWindowFocus){ int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } } 节选自:未扬帆的小船https://juejin.im/post/5dde44dc5188250e8b235d83 最后 题外话,虽然我在阿里工作时间不长,但也指导过不少同行。很少跟大家一起探讨,今年春节受武汉新型冠状病毒影响,大家都只能在家办公学习,待疫情过去,大家重归工作岗位,面试和岗位流动多起来,因此充分利用这段时间复习,未来在寻找工作过程中占领有利位置就显得尤为重要了。在这里我分享两本由我们阿里同事总结《Android面试指导》,以及《Android架构师面试题精编解析大全》两本电子书分享给读者,需要的朋友,点击下方链接,前往免费领取! 下载地址:https://shimo.im/docs/3Tvytq686Yyv83KX/read下载地址:https://shimo.im/docs/3Tvytq686Yyv83KX/read
从我毕业至今已一年半,毕业就想进阿里,因此这一年多来一直在准备和学习,同时也很关心阿里面试的动向。今天跟大家分享一下我的阿里巴巴安卓客户端面试经历,部分题目忘记了,另外只列出相关题目,部分提供思路,具体的答案请大家自行点击这份全套最新1612页Android面试指导PDF下载参考解答,毕竟大部分朋友距离开工还有几天时间,这几天可以不用数大米了,一起来备战金三银四做做题吧。 突如其来的一面 电话预约:阿里的电话总是那么突然,经常在上班上的好好的时候,就突然来了说个杭州的座机电话,接的多了看到就可以猜到。大家习惯就好,一般面试官会提前打电话预约时间,可以预约一个自己合适的时间,因为我加班比较多,所以预约的都是晚上 9,10 点~,不得不说,阿里的面试官也挺辛苦的。 介绍自己常规流程,简单介绍自己的毕业院校,工作经历以及一些兴趣爱好,提交准备好,多背几遍。 介绍自己做的项目按照自己熟悉的项目流程框架去逐步介绍,项目主要做了什么,用了什么,这里一定要讲自己熟悉的部分,因为面试官会根据你提到的技术点展开后续的问题,千万不要给自己挖填不了的坑。我这边介绍的时候提到了开发,所以后面面试官问了好几个开发 的问题,所以一定要说自己熟悉的技术。 项目中最有难度和记忆最深刻的项目这个问题按照实际去回答就好,可以说自己做过的但不一定是最难的,可以是自己最熟悉的,觉得有亮点可以说的,避免讲不了几句就没话可说的情况。 开源框架 – Volley,Gilde,RxJava源码分析ConcurrentHash,以及线程安全等问题。 底层红黑树是什么?什么是红黑树这个问题就是对上面问题一个很好的补充,Hashmap 在 Java8 的时候,会将链表在长度大于8的时候进行链表转红黑树,那么同样这也是一个延伸问题。红黑树:是一种平衡二叉查找树, 常用排序算法和时间复杂度 快排的实现原理双指针,建议在纸上自行手写实现,这样才容易记牢。 Android内存优化 Android中的类加载器 Android中的动画有哪几类,它们的特点和区别是什么 热修复原理 图片加载如何避免OOM 你觉得你的能力大概在什么方面?一面相对来说是比较偏技术细节的,十分注重原理和源码解析。这里有一份互联网一线大咖整理的源码PDF手册,我也从中获益匪浅。现在把它分享出来。 下载地址:https://shimo.im/docs/3Tvytq686Yyv83KX 二面 二面的电话,同样还是那么的突如其来。。。二面的内部比较宽泛,偏项目和个人发展,没有标准答案,大家自行思考即可。 介绍自己,并介绍一下自己做的项目 自己主要负责哪块内容,其中印象最深刻的项目是什么 项目中遇到的最难的问题是什么,怎么解决的, 项目在生产上有没有遇到过问题,是什么样子的问题以及如何解决的 有没有因为 bug 或者其他原因导致的线上问题 tcp udp区别 framelayout relativelayout有什么区别 两个线程交替打印 三次握手 第二个包丢了会咋样? 对android什么地方最熟悉 bitmap存储的位置 安卓几个版本有什么不同? 本人觉得工作这么久对业务有什么重大的贡献 项目小组有多少人,个人觉得自己在小组中是什么样的位置 工作这么久觉得业务上有什么缺陷,怎么优化 是否有参与项目架构的讨论和设计 业余时间一般做些什么 有 Github 账号,那GitHub 上印象最深刻的项目是什么从二面的题目上可以看得出,考察的是各方面的能力,项目经历和设计能力,沟通能力以及学校能力,可谓是方方面面都在考察。 结语 二面过去了很久还没接到电话,当然有网友会喷已经凉了,但是即便凉凉也没关系,生活总归要继续,学习工作也要继续前行,我并没有气馁,只能说明还有很大的进步空间,下次再战!最近疫情严重,在家待着哪里都不能去,刚好要我好好学习学习,争取等到疫情结束过后再次冲击!
作者 | Guru99 译者 | 刘雅梦 策划 | 小智来源:GitHubDaily原文链接:https://mp.weixin.qq.com/s/xwqVD69o6_qrgV0dxj_2aw Code Review 工具自动化了代码审核过程。它们有助于静态代码分析,这对于交付可靠的软件应用程序至关重要。市场上有太多的 Code Review 工具了,以至于为我们的项目选择一个合适的 Code Review 工具都会成为一种挑战。本文精选了 16 个 Code Review 工具,它们都具有最受欢迎的特性和最新的下载链接。该列表既包含了开源工具也包含了商业工具。 1. Review Assistant Review Assistant 是 Visual Studio 的一个扩展。它支持 Visual Studio 2019、2017、2015、2013、2012 和 2010。Review Assistant 可以帮助创建审查请求并能在不离开 IDE 的情况下对请求做出响应。它支持 TFS、Subversion、Git、Mercurial 以及 Perforce。Review Assistant 将“代码审查板(Code Review Board)”窗口添加到 IDE 中。该窗口可用于管理用户所有可用的审查。主要特性:灵活的代码审查支持在代码中讨论带有缺陷修复的迭代审查电子邮件通知丰富的集成功能报告和统计该插件可替换 Visual Studio 代码审查功能(Visual Studio Code Review Feature)。下载链接:https://bit.ly/2Uw0a6M 2. Reshift Reshift 是一个基于 SaaS(Software-as-a-Service,软件即服务)的软件平台,它可以帮助软件开发团队在部署代码到生产环境之前,更快地识别出代码中更多的漏洞。可以减少发现和修复漏洞的成本和时间,可以识别数据泄露的潜在风险,并能帮助软件公司达到合规性和法规要求。主要特性:可以与 Github 和 Bitbucket 集成通过拉取请求(pull-request)这个工作流为团队的处理流程提供安全性,并可以避免切换到其他面板智能筛选,通过标记问题来减少超时误报跟踪每个开发人员功能分支的漏洞在合并到主干之前了解关键的漏洞如果引入了新漏洞,则关闭构建。下载链接:https://bit.ly/33Oubj8 3. Gerrit 这是一个开源的轻量级工具,它是基于“Git 版本控制系统”来进行构建的。在所有用户都是受信提交者的项目环境中,该工具非常有用,因为该工具允许用户检查项目中所做的总体变更。主要特性:Gerrit 阻止用户直接推送到 Git 库允许我们在源代码中查找错误可以帮助我们创建新变更或更新现有的变更允许我们在开发者模式和 Git 库之间进行转换。下载链接:https://www.gerritcodereview.com/ 4. Codestriker Codestriker 是一个开源的在线源码审查 Web 应用程序。此代码审查工具可以帮助我们在数据库中记录问题、注释和决策。它也可以用于代码检查(Code Inspections)。主要特性:支持传统的文档审查它可以与 Bugzilla、ClearCase、CVS 等集成Codestriker 已获得 GPL 许可。下载链接:http://codestriker.sourceforge.net/ 5. Phabricator Phabricator 是一个开源的源码扫描程序。它还包括了基于 Web 的轻量级代码审查、规划、测试、bug 发现等功能。主要特性:提交前(Pre-Commit)的代码审查可以跟踪大量的 bug可以帮助我们为每个部门构建独立的任务表单可定制任务管理允许我们编写有用的注释和备注信息。下载链接:https://www.phacility.com/ 6. Crucible Crucible 是一个基于 Web 的代码质量工具。开发人员利用它来进行代码审查、bug 及缺陷发现、变更讨论和知识共享。该工具可以帮助他们捕获主要缺陷并改进他们的代码架构。主要特性:通过内联注释、线程引用和对话来协作开发正确的代码允许使用活动流(活动流可以显示最新的注释)实时跟踪项目和审查更新当代码在整个审查过程中被重构和修改时,可以确保我们正在审查的所有文件都是最新的可以根据审查活动自动更新 Jira 软件问题,并且通过单击即可将审查意见转换为问题。下载链接:https://www.atlassian.com/software/crucible 7. Review Board Review Board 是一个安全的代码审查工具。它可用于开源项目和公司的代码审查和文档审查。主要特性:Review Board 可以与 ClearCase、Performce、CVS、Plastic 等集成该代码是语法高亮显示的,这使得它更具可读性支持提交前(pre-commit)审查和提交后(post-commit)审查。下载链接:https://www.reviewboard.org/ 8. Barkeep Barkeep 是一个友好的代码审查系统工具。它提供了一种最简单的方法来审查代码。它允许我们查看任何 Git 库的提交、查看差异点并编写注释。主要特性:该工具允许我们发送电子邮件给相关的提交者支持提交后(post-commit)的工作流提供了干净的用户界面,易于浏览。下载链接:http://getbarkeep.org/ 9. Reviewable Reviewable 是一个轻量级的、功能强大的代码分析工具,它使代码审查更快、更全面。它通过用户界面清理、bug 发现以及语法高亮显示来帮助我们提高代码质量。主要特性:完全可定制的逻辑可以帮助我们确定何时能完成审查仅适用于 GitHub 和 GitHub Enterprise,可与它们进行无缝集成,最小化管理工作行注释可以跨文件版本进行映射,并会一直保留到问题解决为止可以帮助我们完整地跟踪审查人(每个文件的每个修改版本都是谁审查的),以确保没有遗漏任何变更。下载链接:https://reviewable.io/ 10. Peer Review Plugin Peer Review Plugin 消除了耗时的代码审查会议的需求,因为它使我们可以在基于 Web 的用户友好的环境中审查代码。主要特性:改善的知识转移体验可以帮助我们审查存储库中的文件并对其进行注释以 MS Word docx 格式导出数据更好的代码以及更少的缺陷支持 Git、SVN 和 GitHub.下载链接:https://trac-hacks.org/wiki/PeerReviewPlugin 11. Codacy Codacy 工具通过静态代码分析自动识别问题。在每个拉取(pull)和提交(commit)请求中,它能快速地告诉用户有关安全性问题、代码重复和代码复杂度的信息。主要特性:可以帮助我们在流程中及早发现新问题,并防止线上受到影响代码质量可视化可以无缝地集成到我们的工作流中自我托管的解决方案,在服务器上提供了一流的安全性.下载链接:https://www.codacy.com/ 12. CodeFactor.io 使用 Code Factor 工具,我们可以了解整个项目的代码质量、最近提交的内容以及问题最多的文件。我们可以针对每次提交(commit)和拉取(pull)的请求进行跟踪及问题修复。主要特性:可以概述我们的代码库可以与我们的开发过程无缝集成我们可以控制要分析的内容帮助我们捕获每一行代码简化代码审查流程并改进可操作的报告它提供了分析数据,可以帮助我们理解、贡献并与同行交流.下载链接:https://www.codefactor.io/ 13. Helix Swarm Helix swarm 是一个代码审查工具,它可以安排审查、共享内容并查看代码审查变更。它支持持续集成部署。它可以帮助我们监控进度、自动化设计过程并提高项目的发布质量。主要特性:允许我们按照优先级进行筛选可以在个人资料页自定义通知设置可以通过将多个变更分支附加到提交前(pre-commit)审查中来同时审查独立的组件通过将 Helix Core 与 Ping Identity、Okta 或其他工具集成来帮助我们确保代码是安全的。下载链接:https://www.perforce.com/products/helix-swarm 14. Rhodecode Rhodecode 是一个开源的、安全的企业级源码管理工具。该工具可作为 Git、Subversion 和 Mercurial 的集成工具。主要特性:团队协作可以提高代码质量Rhodode 提供了工作流自动化,可以加快协作权限管理使软件开发更安全可以帮助我们将现有代码库与新的问题跟踪工具集成在一起。下载链接:https://rhodecode.com/ 15. Veracode Veracode 是一个代码审查和静态分析工具。它是基于 SaaS 模型构建的。此工具允许我们从安全性的角度分析代码。该工具使用二进制代码 / 字节码,并能保证 100% 的测试覆盖率。主要特性:即使源码不可用,也可以通过一致的流程和策略测试桌面、Web 或任何大小的桌面应用程序不需要手动及自动配置就可以测试多个应用程序通过在 SDLC 中简化和集成测试来自动化不同的工作流通过持续审查过程来提高代码的生产效率。下载链接:https://www.veracode.com/products/binary-static-analysis-sast 16. JArchitect JArchitect 是一款易于使用的代码审查工具,可用于分析 Java 代码。每次审查后,它都会发送一份项目开发相关的报告。该工具还可以帮助我们提高代码的可维护性。主要特性:JArchitect 代码规则是 LINQ 查询,可以在第二个查询中生成JArchitect 可以帮助我们发现数百个甚至数千个影响实际代码库的问题当发现新问题时,它会立即通知开发人员。下载链接:http://www.jarchitect.com/
作者:Keriy链接:https://juejin.im/post/5e1c5d7ef265da3df860f9bf来源:掘金 缘起 周末得空,逛了dribbble,发现了好多好看的设计,馋的不行。相信每个前端都有这样一个梦想:能把设计稿直接生成代码该多好,忽而想起了Flutter Interact上大佬们演示的插件,感觉有得搞 sketch准备 没有vip不能下载,就自己照着预览图画一个,丑莫怪~ Spuernova or xd-to-flutter xd-to-flutter 在我准备安装的时候,得到了这样的提示: 呵呵~ 好在Spuernova(这货是收费的,哈哈哈)可以同时支持XD和sketch,那么话不多说,下载安装,导入 这里直接选择全部页面 搞定~,so easy 生成的代码可以直接点击右上角的到处图标到处成项目或者单个文件, 这样就完工啦~ 生成的项目结构大致如下: 运行 生成的代码在安装完成后可以直接运行。用VSCode打开刚刚生成的项目,flutter pub get一波没有问题, flutter run起来看看 海星,感觉哪里不对?字体图标和字体怎么都这样样子的?? Spuernova中点击字体图标看看, 原来这里的字体图标被转成了图片,但是字体并没有问题,看来字体阴影的识别还是有一定问题。 不过 Spuernova提供了修改工具,并且可以实现hot-reload(但是无论怎样都不能hot-reload...) 代码品读 简单来看看生成的list组件: 整个页面全部是stack,额~,又不是不能用。 虽然做成这样不太智能,但是我们可以手动改生成组件的类型,点击选中要更改类型的组件,右键选择Convert to Component -> Text Field,我们尝试将它转换成一个输入框。 /// 更改前的代码 Container( width: 57, height: 57, decoration: BoxDecoration( color: Color.fromARGB(255, 111, 124, 132), boxShadow: [ BoxShadow( color: Color.fromARGB(44, 29, 30, 32), offset: Offset(2, 2), blurRadius: 3, ), ], borderRadius: BorderRadius.all(Radius.circular(8)), ), child: Container(), ) /// 更改后的代码 Container( width: 57, height: 57, decoration: BoxDecoration( color: Color.fromARGB(255, 111, 124, 132), boxShadow: [ BoxShadow( color: Color.fromARGB(44, 29, 30, 32), offset: Offset(2, 2), blurRadius: 3, ), ], borderRadius: BorderRadius.all(Radius.circular(8)), ), child: TextField( style: TextStyle( color: Color.fromARGB(255, 0, 0, 0), fontWeight: FontWeight.w400, fontSize: 12, ), maxLines: 1, autocorrect: false, ), ) 还是可以的,只要稍加修改就可以使用。 从上面的代码来看,Spuernova虽然生成的代码不能直接使用,但是到小组件级别还是可是省不少气力的。 个人认为最好用的其实是帮我们把UI里面的样式全部提取了出来,放在values目录下: // colors.dart import 'dart:ui'; class AppColors { static const Color primaryBackground = Color.fromARGB(255, 38, 173, 211); static const Color secondaryBackground = Color.fromARGB(255, 36, 38, 44); static const Color ternaryBackground = Color.fromARGB(255, 74, 78, 122); static const Color primaryElement = Color.fromARGB(255, 38, 43, 47); static const Color secondaryElement = Color.fromARGB(255, 243, 64, 61); static const Color accentElement = Color.fromARGB(255, 47, 52, 57); static const Color primaryText = Color.fromARGB(255, 93, 99, 106); static const Color secondaryText = Color.fromARGB(255, 183, 190, 199); static const Color accentText = Color.fromARGB(255, 137, 145, 152); } // gradients.dart import 'package:flutter/rendering.dart'; class Gradients { static const Gradient primaryGradient = LinearGradient( begin: Alignment(0.5, 0), end: Alignment(0.5, 1), stops: [ 0, 1, ], colors: [ Color.fromARGB(255, 41, 44, 49), Color.fromARGB(255, 49, 54, 59), ], ); static const Gradient secondaryGradient = LinearGradient( begin: Alignment(0.5, 0), end: Alignment(0.5, 1), stops: [ 0, 1, ], colors: [ Color.fromARGB(255, 51, 54, 59), Color.fromARGB(255, 37, 40, 45), ], ); } 实际项目中我们可能不止一套主题,那么将上面的生成的样式稍加组织,就可以生成符合项目需求的主题: // custom_theme.dart // 蠢蠢的写法,大佬们勿笑 import 'package:flutter/material.dart'; class CustomTheme { CustomTheme({ this.lightShadowColor, this.darkShadowColor, this.lightShadowBlur, this.weightShadowBlur, this.lightShadowOffset, this.weightShadowOffset, }); Color lightShadowColor; Color darkShadowColor; double lightShadowBlur; double weightShadowBlur; Offset lightShadowOffset; Offset weightShadowOffset; factory CustomTheme.light() => CustomTheme( ... ); factory CustomTheme.dark() => CustomTheme( lightShadowColor: Color.fromARGB(255, 46, 42, 53), darkShadowColor: Color.fromARGB(255, 85, 59, 60), lightShadowOffset: Offset.zero, weightShadowOffset: Offset.zero, lightShadowBlur: 3, weightShadowBlur: 3, ); static ThemeData darkTheme = ThemeData( appBarTheme: AppBarTheme(elevation: 0), scaffoldBackgroundColor: Color(0xFF2E3439), primarySwatch: MaterialColor( 0xFF2E3439, { 50: Color(0xFF8293A1), 100: Color(0xFF768693), 200: Color(0xFF6D7B87), 300: Color(0xFF606D78), 400: Color(0xFF515C66), 500: Color(0xFF48535C), 600: Color(0xFF3F4850), 700: Color(0xFF384046), 800: Color(0xFF30383E), 900: Color(0xFF2E3439), }, ), ); static ThemeData lightTheme = ThemeData( appBarTheme: AppBarTheme(elevation: 0), scaffoldBackgroundColor: Color(0xFF2E3439), ..., ); static CustomTheme of(BuildContext context) { Brightness brightness = MediaQuery.of(context).platformBrightness; return brightness == Brightness.dark ? CustomTheme.dark() : CustomTheme.light(); } static ThemeData systemTheme(BuildContext context, [Brightness brightness]) { Brightness _brightness = brightness ?? MediaQuery.of(context).platformBrightness; return _brightness == Brightness.dark ? darkTheme : lightTheme; } } 到这里我们基本就结束了,都学会了吗 总结 UI直接生成UI代码可行,但离完美还有很长一段路 Spuernova是目前唯一可用的工具,缺点是收费 图标字体会直接生成图片,并引入 带阴影的字体阴影想过不理想 生成的代码不能直接用在项目中,只有个别组件可以直接应用 生成的样式可利用价值比较高 学习分享,共勉题外话,毕竟我在三星小米工作多年,深知技术改革和创新的方向,Flutter作为跨平台开发技术、Flutter以其美观、快速、高效、开放等优势迅速俘获人心,但很多FLutter兴趣爱好者进阶学习确实资料,今天我把我搜集和整理的这份学习资料分享给有需要的人,若有关Flutter学习进阶可以与我在Flutter跨平台开发终极之选交流群一起讨论交流。下载地址:https://shimo.im/docs/yTD3t8Pjq3XJtGv8下载地址:https://shimo.im/docs/yTD3t8Pjq3XJtGv8
作者:_yuanhao链接:https://www.jianshu.com/p/86c0a4afd28e 前言 学 Android 有一段时间了,一直都只顾着学新的东西,最近发现很多平常用的少的东西竟让都忘了,趁着这两天,打算把有关 Activity 的内容以问题的形式梳理出来,也供大家查缺补漏。 本文中,我将一改往日写博客的习惯,全文用 XMind 将所有知识点以思维导图的形式呈现,欢迎大家食用~~ 文章目录 * 方便大家学习,我在 GitHub 上建立个 仓库 * 仓库内容与博客同步更新。由于我在 稀土掘金 简书 CSDN 博客园 等站点,都有新内容发布。所以大家可以直接关注该仓库,以免错过精彩内容! 仓库地址: [超级干货!精心归纳 `Android` 、`JVM` 、算法等,各位帅气的老铁支持一下!给个 Star !](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FFishInWater-1999%2Fandroid_interviews) 神图 * 在开始之前,先让我们看看 Android 的 activity 到底都有哪些东西? 借一张网上很火的图带你了解 Activity 一、 生命周期 * 先贴一张闻名遐迩的图 我们生命周期先看看具体有哪些方法回调,在逐一攻破: 1.1 Dialog 弹出时 如果是单纯是创建的 dialog ,Activity 并不会执行生命周期的方法 但是如果是跳转到一个不是全屏的 Activity 的话, 当然就是按照正常的生命周期来执行了 即 onPasue() -> onPause() ( 不会执行原 Activity 的 onStop() , 否则上个页面就不显示了 ) 1.2 横竖屏切换时 不设置 Activity 的 android:configChanges 时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次 设置 Activity 的 android:configChanges="orientation" 时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次 设置 Activity 的 android:configChanges="orientation|keyboardHidden" 时,切屏不会重新调用各个生命周期,只会执行 onConfigurationChanged 方法 注意:还有一点,非常重要,一个 Android 的变更细节!当 API >12 时,需要加入 screenSize 属性,否则屏幕切换时即使你设置了 orientation 系统也会重建 Activity ! 横竖屏切换生命周期的执行 1.3 不同场景下 Activity 生命周期的变化过程 启动 Activity : onCreate() ---> onStart() ---> onResume() ,Activity 进入运行状态。 锁屏时会执行 onPause() 和 onStop() , 而开屏时则应该执行 onStart() onResume() Activity 退居后台: 当前 Activity 转到新的 Activity 界面或按 Home 键回到主屏: onPause() ---> onStop() ,进入停滞状态。 Activity 返回前台: onRestart() ---> onStart() ---> onResume() ,再次回到运行状态。 Activity 退居后台: 且系统内存不足, 系统会杀死这个后台状态的 Activity ,若再次回到这个 Activity ,则会走 onCreate() --> onStart() ---> onResume() 1.4 将一个 Activity 设置成窗口的样式 只需要给我们的 Activity 配置如下属性即可。android:theme="@android:style/Theme.Dialog" 1.5 退出已调用多个 Activity 的 Application 通常情况用户退出一个 Activity 只需按返回键,我们写代码想退出 activity 直接调用 finish() 方法就行。 发送特定广播: 在需要结束应用时, 发送一个特定的广播,每个 Activity 收到广播后,关闭 即可。 给某个 activity 注册接受接受广播的意图 registerReceiver(receiver, filter) 如果过接受到的是 关闭 activity 的广播 activity finish() 掉 递归退出 就调用 finish() 方法 把当前的 Activity 退出 在打开新的 Activity 时使用 startActivityForResult , 然后自己加标志, 在 onActivityResult 中处理, 递归关闭。 其实 也可以通过 intent 的 flag 来实现 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 激活一个新的 activity。 此时如果该任务栈中已经有该 Activity , 那么系统会把这个 Activity 上面的所有 Activity 干掉。 其实相当于给 Activity 配置的启动模式为 singleTask 。 记录打开的 Activity 每打开一个 Activity , 就记录下来。 在需要退出时 , 关闭每一个 Activity 1.6 锁定屏与解锁屏幕,Activity 如何执行生命周期 锁屏时会执行 onPause() 和 onStop() , 而开屏时则应该执行 onStart() onResume() 1.7 修改 Activity 进入和退出动画 可以通过两种方式 , 一是通过定义 Activity 的主题 ,二是通过覆写 Activity 的 overridePendingTransition 方法。 通过设置主题样式在 styles.xml 中编辑代码 , 添加 themes.xml 文件:在 AndroidManifest.xml 中给指定的 Activity 指定 theme。 覆写 overridePendingTransition 方法:overridePendingTransition(R.anim.fade, R.anim.hold); 1.8 Activity 的四种状态 runnig :用户可以点击,activity 处于栈顶状态。 paused :activity 失去焦点的时候,被一个非全屏的 activity 占据或者被一个透明的 activity 覆盖,这个状态的 activity 并没有销毁,它所有的状态信息和成员变量仍然存在,只是不能够被点击。(内存紧张的情况,这个 activity 有可能被回收) stopped :这个 activity 被另外一个 activity 完全覆盖,但是这个 activity 的所有状态信息和成员变量仍然存在(除了内存紧张) killed :这个 activity 已经被销毁,其所有的状态信息和成员变量已经不存在了。 1.9 如何处理异常退出 Activity 异常退出的时候 --> onPause() --> onSaveInstanceState() --> onStop() --> onDestory() 需要注意的是 onSaveInstanceState() 方法与 onPause 并没有严格的先后关系,有可能在 onPause 之前,也有可能在其后面调用,但会在 onStop() 方法之前调用 异常退出后又重新启动该 Activity --> onCreate() --> onStart() --> onRestoreInstanceState() --> onResume() 搞懂这个生命周期的执行后就可以回答了,首先要知道面试官的意思:是要重新启动并恢复这个 Activity 还是说直接退出整个 app 如果要恢复则要在 onSaveInstanceState() 中进行保存数据并在 onRestoreInstanceState() 中进行恢复 如果是要退出 app 的话就要捕获全局的异常信息,并退出 app 当然个人建议是使用 UncaughtExceotionHandler 来捕获全局异常进行退出 app 的操作,这样会减少之前崩溃所造成的后遗症! 1.10 什么是 onNewIntent 如果 IntentActivity 处于任务栈的顶端,也就是说之前打开过的 Activity ,现在处于 onPause 、 onStop 状态的话,其他应用再发送 Intent 的话 执行顺序为:onNewIntent,onRestart,onStart,onResume。 二、 启动模式 * 2.1 启动模式 Activity 一共有四种 launchMode :standard 、singleTop 、singleTask 、singleInstance 。 Standard 模式(默认模式) 说明: 每次启动一个 Activity 都会又一次创建一个新的实例入栈,无论这个实例是否存在。 生命周期:每次被创建的实例 Activity 的生命周期符合典型情况,它的 onCreate 、onStart 、onResume 都会被调用。 举例:此时 Activity 栈中以此有 A 、B 、C 三个 Activity ,此时C处于栈顶,启动模式为 Standard 模式。若在 C Activity 中加入点击事件,须要跳转到还有一个同类型的 C Activity 。结果是还有一个 C Activity 进入栈中,成为栈顶。 SingleTop 模式(栈顶复用模式) 说明:分两种处理情况:须要创建的 Activity 已经处于栈顶时,此时会直接复用栈顶的 Activity 。不会再创建新的 Activity ;若须要创建的 Activity 不处于栈顶,此时会又一次创建一个新的 Activity 入栈,同 Standard 模式一样。 生命周期:若情况一中栈顶的 Activity 被直接复用时,它的 onCreate 、onStart 不会被系统调用,由于它并没有发生改变。可是一个新的方法 onNewIntent 会被回调( Activity 被正常创建时不会回调此方法)。 举例:此时 Activity 栈中以此有 A 、B 、C 三个 Activity ,此时 C 处于栈顶,启动模式为 SingleTop 模式。情况一:在 C Activity 中加入点击事件,须要跳转到还有一个同类型的 C Activity 。结果是直接复用栈顶的 C Activity。情况二:在 C Activity 中加入点击事件,须要跳转到还有一个 A Activity。结果是创建一个新的 Activity 入栈。成为栈顶。 SingleTask 模式(栈内复用模式) 说明:若须要创建的 Activity 已经处于栈中时,此时不会创建新的 Activity ,而是将存在栈中的 Activity 上面的其他 Activity 所有销毁,使它成为栈顶。 如果是在别的应用程序中启动它,则会新建一个 task ,并在该task中启动这个 Activity ,singleTask 允许别的 Activity 与其在一个 task 中共存,也就是说,如果我在这个 singleTask 的实例中再打开新的 Activity ,这个新的 Activity 还是会在 singleTask 的实例的 task 中。 生命周期:同 SingleTop 模式中的情况一同样。仅仅会又一次回调 Activity 中的 onNewIntent 方法 举例:此时 Activity 栈中以此有 A 、B 、C 三个 Activity 。此时 C 处于栈顶,启动模式为 SingleTask 模式。情况一:在 C Activity 中加入点击事件,须要跳转到还有一个同类型的 C Activity 。结果是直接用栈顶的 C Activity 。情况二:在 C Activity 中加入点击事件,须要跳转到还有一个 A Activity 。结果是将 A Activity 上面的 B 、C 所有销毁,使 A Activity 成为栈顶。 SingleInstance 模式(单实例模式) 说明: SingleInstance 比较特殊,是全局单例模式,是一种加强的 SingleTask 模式。它除了具有它所有特性外,还加强了一点:只有一个实例,并且这个实例独立运行在一个 task 中,这个 task 只有这个实例,不允许有别的 Activity 存在。 这个经常使用于系统中的应用,比如 Launch 、锁屏键的应用等等,整个系统中仅仅有一个!所以在我们的应用中一般不会用到。了解就可以。 举例:比方 A Activity 是该模式,启动 A 后。系统会为它创建一个单独的任务栈,由于栈内复用的特性。兴许的请求均不会创建新的 Activity ,除非这个独特的任务栈被系统销毁。 2.2 启动模式的使用方式 在 Manifest.xml 中指定 Activity 启动模式 一种静态的指定方法 在 Manifest.xml 文件里声明 Activity 的同一时候指定它的启动模式 这样在代码中跳转时会依照指定的模式来创建 Activity 。 启动 Activity 时。在 Intent 中指定启动模式去创建 Activity 一种动态的启动模式 在 new 一个 Intent 后 通过 Intent 的 addFlags 方法去动态指定一个启动模式。 注意:以上两种方式都能够为 Activity 指定启动模式,可是二者还是有差别的。 优先级:动态指定方式即另外一种比第一种优先级要高,若两者同一时候存在,以另外一种方式为准。 限定范围:第一种方式无法为 Activity 直接指定 FLAG_ACTIVITY_CLEAR_TOP 标识,另外一种方式无法为 Activity 指定 singleInstance 模式。 2.3 启动模式的实际应用场景 这四种模式中的 Standard 模式是最普通的一种,没有什么特别注意。而 SingleInstance 模式是整个系统的单例模式,在我们的应用中一般不会应用到。所以,这里就具体解说 SingleTop 和 SingleTask 模式的运用场景: SingleTask 模式的运用场景 最常见的应用场景就是保持我们应用开启后仅仅有一个 Activity 的实例。 最典型的样例就是应用中展示的主页( Home 页)。 假设用户在主页跳转到其他页面,运行多次操作后想返回到主页,假设不使用 SingleTask 模式,在点击返回的过程中会多次看到主页,这明显就是设计不合理了。 SingleTop 模式的运用场景 假设你在当前的 Activity 中又要启动同类型的 Activity 此时建议将此类型 Activity 的启动模式指定为 SingleTop ,能够降低Activity的创建,节省内存! 注意:复用 Activity 时的生命周期回调 这里还须要考虑一个 Activity 跳转时携带页面參数的问题。 由于当一个 Activity 设置了 SingleTop 或者 SingleTask 模式后,跳转此 Activity 出现复用原有 Activity 的情况时,此 Activity 的 onCreate 方法将不会再次运行。onCreate 方法仅仅会在第一次创建 Activity 时被运行。 而一般 onCreate 方法中会进行该页面的数据初始化、UI 初始化,假设页面的展示数据无关页面跳转传递的參数,则不必操心此问题 若页面展示的数据就是通过 getInten() 方法来获取,那么问题就会出现:getInten() 获取的一直都是老数据,根本无法接收跳转时传送的新数据! 以下,通过一个样例来具体解释: 以上代码中的 CourseDetailActivity 在配置文件里设置了启动模式是 SingleTop 模式,依据上面启动模式的介绍可得知,当 CourseDetailActivity 处于栈顶时。 再次跳转页面到 CourseDetailActivity 时会直接复用原有的 Activity ,并且此页面须要展示的数据是从 getIntent() 方法得来,可是 initData() 方法不会再次被调用,此时页面就无法显示新的数据。 当然这样的情况系统早就为我们想过了,这时我们须要另外一个回调 onNewIntent(Intent intent)方法。此方法会传入最新的 intent ,这样我们就能够解决上述问题。这里建议的方法是又一次去 setIntent 。然后又一次去初始化数据和 UI 。代码例如以下所看到的: 这样,在一个页面中能够反复跳转并显示不同的内容。 2.4 快速启动一个 Activity 这个问题其实也是比较简单的,就是不要在 Activity 的 onCreate 方法中执行过多繁重的操作,并且在 onPasue 方法中同样不能做过多的耗时操作。 2.5 启动流程 注意!这里并不是要回答 Activity 的生命周期! 3 分钟看懂 Activity 启动流程 2.6 Activity 的 Flags 标记位既能够设定Activity的启动模式,如同上面介绍的,在动态指定启动模式,比方 FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_SINGLE_TOP 等。它还能够影响 Activity 的运行状态 ,比方 FLAG_ACTIVITY_CLEAN_TOP 和 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 等。 以下介绍几个基本的标记位,切勿死记,理解几个就可以,须要时再查官方文档。 FLAG_ACTIVITY_NEW_TASK 作用是为 Activity 指定 “SingleTask” 启动模式。跟在 AndroidMainfest.xml 指定效果同样 FLAG_ACTIVITY_SINGLE_TOP 作用是为 Activity 指定 “SingleTop” 启动模式,跟在 AndroidMainfest.xml 指定效果同样。 FLAG_ACTIVITY_CLEAN_TOP 具有此标记位的 Activity ,启动时会将与该 Activity 在同一任务栈的其他 Activity 出栈。 一般与 SingleTask 启动模式一起出现。 它会完毕 SingleTask 的作用。 但事实上 SingleTask 启动模式默认具有此标记位的作用 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 具有此标记位的 Activity 不会出如今历史 Activity 的列表中 使用场景:当某些情况下我们不希望用户通过历史列表回到 Activity 时,此标记位便体现了它的效果。 它等同于在 xml 中指定 Activity 的属性. 2.7 onNewInstent()方法什么时候执行 这个是启动模式中的了,当此 Activity 的实例已经存在,并且此时的启动模式为 SingleTask 和 SingleInstance ,另外当这个实例位于栈顶且启动模式为 SingleTop 时也会触发 onNewInstent() 。 三、 数据 * 3.1 Activity 间通过 Intent 传递数据大小限制 Intent 在传递数据时是有大小限制的,这里官方并未详细说明,不过通过实验的方法可以测出数据应该被限制在 1MB 之内( 1024KB ) 我们采用传递 Bitmap 的方法,发现当图片大小超过 1024(准确地说是 1020 左右)的时候,程序就会出现闪退、停止运行等异常(不同的手机反应不同) 因此可以判断 Intent 的传输容量在 1MB 之内。 3.2 内存不足时系统会杀掉后台的Activity,若需要进行一些临时状态的保存,在哪个方法进行 Activity 的 onSaveInstanceState() 和 onRestoreInstanceState() 并不是生命周期方法,它们不同于 onCreate() 、onPause() 等生命周期方法,它们并不一定会被触发。 onSaveInstanceState() 方法,当应用遇到意外情况(如:内存不足、用户直接按 Home 键)由系统销毁一个 Activity ,onSaveInstanceState() 会被调用。 但是当用户主动去销毁一个 Activity 时,例如在应用中按返回键,onSaveInstanceState() 就不会被调用。 除非该 activity 不是被用户主动销毁的,通常 onSaveInstanceState() 只适合用于保存一些临时性的状态,而 onPause() 适合用于数据的持久化保存。 3.3 onSaveInstanceState() 被执行的场景 系统不知道你按下 HOME 后要运行多少其他的程序,自然也不知道 activity A 是否会被销毁 因此系统都会调用 onSaveInstanceState() ,让用户有机会保存某些非永久性的数据。以下几种情况的分析都遵循该原则: 当用户按下 HOME 键时 长按 HOME 键,选择运行其他的程序时 锁屏时 从 activity A 中启动一个新的 activity 时 屏幕方向切换时 3.4 两个 Activity 之间跳转时必然会执行的方法 一般情况下比如说有两个 activity , 分别叫 A , B ,当在 A 里面激活 B 组件的时候, A 会调用 onPause() 方法,然后 B 调用 onCreate() , onStart() , onResume() 。 这个时候 B 覆盖了窗体, A 会调用 onStop() 方法. 如果 B 是个透明的,或者 是对话框的样式, 就不会调用 A 的 onStop() 方法。 3.5 用 Intent 去启动一个Activity 之外的方法 使用 adb shell am 命令 am 启动一个 activity adb shell am start com.example.fuchenxuan/.MainActivity am 发送一个广播,使用 action adb shell am broadcast -a magcomm.action.TOUCH_LETTER 3.6 scheme 跳转协议 3.6.1 定义 服务器可以定制化跳转 app 页面 app 可以通过 Scheme 跳转到另一个 app 页面 可以通过 h5 页面跳转 app 原生页面 3.6.2 协议格式: qh 代表 Scheme 协议名称 test 代表 Scheme 作用的地址域 8080 代表改路径的端口号 /goods 代表的是指定页面(路径) goodsId 和 name 代表传递的两个参数 3.6.3 Scheme使用 定义一个 Scheme 获取 Scheme 跳转的参数 调用方式 原生调用 html调用 判断某个Scheme是否有效 关于scheme跳转协议,可以查看下面的博客,站在巨人的肩膀上,才能看得更远 [Android产品研发(十一)-->应用内跳转Scheme协议](https://links.jianshu.com/go?to=http%3A%2F%2Fblog.csdn.net%2Fqq_23547831%2Farticle%2Fdetails%2F51685310) 四、 Context * 4.1 Context , Activity , Appliction 的区别 相同:Activity 和 Application 都是 Context 的子类。 Context 从字面上理解就是上下文的意思, 在实际应用中它也确实是起到了管理 上下文环境中各个参数和变量的总用, 方便我们可以简单的访问到各种资源。 不同:维护的生命周期不同。Context 维护的是当前的 Activity 的生命周期, Application 维护的是整个项目的生命周期。 使用 context 的时候, 小心内存泄露, 防止内存泄露 4.2 Context 是什么 它描述的是一个应用程序环境的信息,即上下文。 该类是一个抽象( abstract class )类, Android 提供了该抽象类的具体实 现类( ContextIml )。 通过它我们可以获取应用程序的资源和类, 也包括一些应用级别操作, 例如:启动一个 Activity ,发送广播,接受 Intent ,信息,等。 4.2.1 附加一张 Context 继承关系图 4.3 获取当前屏幕 Activity 的对象 使用 ActivityLifecycleCallbacks [Android 如何获取当前Activity实例对象?](https://links.jianshu.com/go?to=http%3A%2F%2Fblog.csdn.net%2Fvfush%2Farticle%2Fdetails%2F51483436) 4.4 Activity 的管理机制 Activity的管理机制 面试官问这个问题,想看看大家对Activity了解是否深入: 什么是 ActivityRecord 什么是 TaskRecord 什么是 ActivityManagerService 4.5 什么是 Activity 四大组件之一,通常一个用户交互界面对应一个 activity 。 activity 是 Context 的子类,同时实现了 window.callback 和 keyevent.callback ,可以处理与窗体用户交互的事件。 开发中常用的有 FragmentActivity 、ListActivity 、TabActivity( Android 4.0 被 Fragment 取代) 五、 进程 * 5.1 Android 进程优先级 前台 / 可见 / 服务 / 后台 / 空 5.1.1 前台进程:Foreground process 用户正在交互的 Activity( onResume() ) 当某个 Service 绑定正在交互的 Activity 被主动调用为前台 Service( startForeground() ) 组件正在执行生命周期的回调( onCreate() 、onStart() 、onDestory() ) BroadcastReceiver 正在执行 onReceive() 5.1.2 可见进程:Visible process 我们的 Activity 处在 onPause()(没有进入 onStop() ) 绑定到前台 Activity 的 Service 5.1.3 服务进程:Service process 简单的 startService() 启动。 5.1.4 后台进程:Background process 对用户没有直接影响的进程 --- Activity 处于 onStop() 的时候。 android:process=":xxx" 5.1.5 空进程:Empty process 不含有任何的活动的组件。( Android 设计的,处于缓存的目的,为了第二次启动更快,采取的一个权衡) 5.2 可见进程 可见进程指部分程序界面能够被用户看见,却不在前台与用户交互的进程。例如,我们在一个界面上弹出一个对话框(该对话框是一个新的 Activity ),那么在对话框后面的原界面是可见的,但是并没有与用户进行交互,那么原界面就是可见进程。 一个进程满足下面任何一个条件都被认为是可视的: 寄宿着一个不是前台的活动,但是它对用户仍可见(它的 onPause() 方法已经被调用)。举例来说,这可能发生在,如果一个前台活动在一个对话框(其他进程的)运行之后仍然是可视的,比如输入法的弹出时。 寄宿着一个服务,该服务绑定到一个可视的活动。 一个可视进程被认为是及其重要的且不会被杀死,除非为了保持前台进程运行。 5.3 服务进程 服务进程是通过 startService() 方法启动的进程,但不属于前台进程和可见进程。例如,在后台播放音乐或者在后台下载就是服务进程。 系统保持它们运行,除非没有足够内存来保证所有的前台进程和可视进程。 5.4 后台进程 后台进程是一个保持着一个当前对用户不可视的活动(已经调用 Activity 对象的 onStop() 方法)(如果还有除了 UI 线程外其他线程在运行话,不受影响)。 例如我正在使用 qq 和别人聊天,这个时候 qq 是前台进程,但是当我点击 Home 键让 qq 界面消失的时候,这个时候它就转换成了后台进程。 这些进程没有直接影响用户体验,并且可以在任何时候被杀以收回内存用于一个前台、可视、服务进程。 一般地有很多后台进程运行着,因此它们保持在一个 LRU( least recently used ,即最近最少使用,如果您学过操作系统的话会觉得它很熟悉,跟内存的页面置换算法 LRU 一样)列表以确保最近使用最多的活动的进程最后被杀。 5.5 空进程 空进程是一个没有保持活跃的应用程序组件的进程,不包含任何活跃组件。 保持这个进程可用的唯一原因是作为一个 cache 以提高下次启动组件的速度。系统进程杀死这些进程,以在进程 cache 和潜在的内核 cache 之间平衡整个系统资源。 android 进程的回收顺序从先到后分别是:空进程,后台进程,服务进程,可见进程,前台进程。 5.6 什么是 ANR,如何避免 5.6.1 什么是ANR ANR ,全称为 Application Not Responding 。 在 Android 中,如果你的应用程序有一段时间没有响应,系统会向用户显示一个对话框,这个对话框称作应用程序无响应对话框。 5.6.2 用户行为 用户可以选择让程序继续运行,也可以让程序停止运行。 他们在使用你的应用程序时,并不希望每次都要处理这个对话框。 因此,在程序里对响应性能的设计很重要,这样,系统不会显示 ANR 给用户。 5.6.3 Android不同组件ANR超时时间不同 不同的组件发生 ANR 的时间不一样,主线程( Activity 、Service )是 5 秒,BroadCastReceiver 是 10 秒。 5.6.4 解决方案 将所有耗时操作,比如访问网络,Socket 通信,查询大量 SQL 语句,复杂逻辑计算等都放在子线程中去,然后通过 handler.sendMessage 、runonUITread 、AsyncTask 等方式更新 UI ,以确保用户界面操作的流畅度。 如果耗时操作需要让用户等待,那么可以在界面上显示进度条。 5.7 android的任务栈 Task 一个 Task 包含的就是 activity 集合,android 系统可以通过任务栈有序的管理 activity 一个app当中可能不止一个任务栈,在某些情况下,一个 activity 也可以独享一个任务栈( singleInstance 模式启动的 activity ) 总结 * 本文基本涵盖了 Android Activity 的所有知识点。对于 App 启动、AMS 希望大家能根据文中链接或者 Google 搜索的形式继续展开学习。 重点:关于 Android 的四大组件,到现在为止我才总结完 Activity ,马上我将继续针对,Service ,BroadcastRecevier 等,以及事件分发、滑动冲突、新能优化等重要模块,进行全面总结,欢迎大家关注 _yuanhao 的 简书 ,方便及时接收更新 推荐阅读:2019年鸿洋大神最新整理一线互联网公司Android中高级面试题总结(附答案解析)临近毕业,2020春招困惑你的十大问题,你中招了吗?Android社招最全面试题
2019年11月