【建议收藏】2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂(Android高级篇上)

前言

成为一名优秀的Android开发,须要一份完备的知识体系,在这里,让咱们一块儿成长为本身所想的那样~。

🔥 A awesome android expert interview questions and answers(continuous updating ...)java

从几十份顶级面试仓库和300多篇高质量面经中总结出一份全面成体系化的Android高级面试题集。android

欢迎来到2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂的Android高级篇(上)。git

Android高级面试题 (⭐⭐⭐)


1、性能优化

一、App稳定性优化

一、大家作了哪些稳定性方面的优化?

随着项目的逐渐成熟,用户基数逐渐增多,DAU持续升高,咱们遇到了不少稳定性方面的问题,对于咱们技术同窗遇到了不少的挑战,用户常用咱们的App卡顿或者是功能不可用,所以咱们就针对稳定性开启了专项的优化,咱们主要优化了三项:github

  • Crash专项优化(=>2)
  • 性能稳定性优化(=>2)
  • 业务稳定性优化(=>3)

经过这三方面的优化咱们搭建了移动端的高可用平台。同时,也作了不少的措施来让App真正地实现了高可用。面试

二、性能稳定性是怎么作的?

  • 全面的性能优化:启动速度、内存优化、绘制优化
  • 线下发现问题、优化为主
  • 线上监控为主
  • Crash专项优化

咱们针对启动速度,内存、布局加载、卡顿、瘦身、流量、电量等多个方面作了多维的优化。算法

咱们的优化主要分为了两个层次,即线上和线下,针对于线下呢,咱们侧重于发现问题,直接解决,将问题尽量在上线以前解决为目的。而真正到了线上呢,咱们最主要的目的就是为了监控,对于各个性能纬度的监控呢,可让咱们尽量早地获取到异常状况的报警。编程

同时呢,对于线上最严重的性能问题性问题:Crash,咱们作了专项的优化,不只优化了Crash的具体指标,并且也尽量地获取了Crash发生时的详细信息,结合后端的聚合、报警等功能,便于咱们快速地定位问题。json

三、业务稳定性如何保障?

  • 数据采集 + 报警
  • 须要对项目的主流程与核心路径进行埋点监控,
  • 同时还需知道每一步发生了多少异常,这样,咱们就知道了全部业务流程的转换率以及相应界面的转换率
  • 结合大盘,若是转换率低于某个值,进行报警
  • 异常监控 + 单点追查
  • 兜底策略

移动端业务高可用它侧重于用户功能完整可用,主要是为了解决一些线上一些异常状况致使用户他虽然没有崩溃,也没有性能问题,可是呢,只是单纯的功能不可用的状况,咱们须要对项目的主流程、核心路径进行埋点监控,来计算每一步它真实的转换率是多少,同时呢,还须要知道在每一步到底发生了多少异常。这样咱们就知道了全部业务流程的转换率以及相应界面的转换率,有了大盘的数据呢,咱们就知道了,若是转换率或者是某些监控的成功率低于某个值,那颇有可能就是出现了线上异常,结合了相应的报警功能,咱们就不须要等用户来反馈了,这个就是业务稳定性保障的基础。canvas

同时呢,对于一些特殊状况,好比说,开发过程中或代码中出现了一些catch代码块,捕获住了异常,让程序不崩溃,这实际上是不合理的,程序虽然没有崩溃,当时程序的功能已经变得不可用,因此呢,这些被catch的异常咱们也须要上报上来,这样咱们才能知道用户到底出现了什么问题而致使的异常。此外,线上还有一些单点问题,好比说用户点击登陆一直进不去,这种就属于单点问题,其实咱们是没法找出其和其它问题的共性之处的,因此呢,咱们就必需要找到它对应的详细信息。小程序

最后,若是发生了异常状况,咱们还采起了一系列措施进行快速止损。(=>4)

四、若是发生了异常状况,怎么快速止损?

  • 功能开关
  • 统跳中心
  • 动态修复:热修复、资源包更新
  • 自主修复:安全模式

首先,须要让App具有一些高级的能力,咱们对于任何要上线的新功能,要加上一个功能的开关,经过配置中心下发的开关呢,来决定是否要显示新功能的入口。若是有异常状况,能够紧急关闭新功能的入口,那就可让这个App处于可控的状态了。

而后,咱们须要给App设立路由跳转,全部的界面跳转都须要经过路由来分发,若是咱们匹配到须要跳转到有bug的这样一个新功能时,那咱们就不跳转了,或者是跳转到统一的异常正处理中的界面。若是这两种方式都不能够,那就能够考虑经过热修复的方式来动态修复,目前热修复的方案其实已经比较成熟了,咱们彻底能够低成本地在咱们的项目中添加热修复的能力,固然,若是有些功能是由RN或WeeX来实现就更好了,那就能够经过更新资源包的方式来实现动态更新。而这些若是都不能够的话呢,那就能够考虑本身去给应用加上一个自主修复的能力,若是App启动屡次的话,那就能够考虑清空全部的缓存数据,将App重置到安装的状态,到了最严重的等级呢,能够阻塞主线程,此时必定要等App热修复成功以后才容许用户进入。

image

须要更全面更深刻的理解请查看深刻探索Android稳定性优化

二、App启动速度优化

一、启动优化是怎么作的?

  • 分析现状、确认问题
  • 针对性优化(先归纳,引导其深刻)
  • 长期保持优化效果

在某一个版本以后呢,咱们会发现这个启动速度变得特别慢,同时用户给咱们的反馈也愈来愈多,因此,咱们开始考虑对应用的启动速度来进行优化。而后,咱们就对启动的代码进行了代码层面的梳理,咱们发现应用的启动流程已经很是复杂,接着,咱们经过一系列的工具来确认是否在主线程中执行了太多的耗时操做。

咱们通过了细查代码以后,发现应用主线程中的任务太多,咱们就想了一个方案去针对性地解决,也就是进行异步初始化。(引导=>第2题) 而后,咱们还发现了另一个问题,也能够进行针对性的优化,就是在咱们的初始化代码当中有些的优先级并非那么高,它能够不放在Application的onCreate中执行,而彻底能够放在以后延迟执行的,由于咱们对这些代码进行了延迟初始化,最后,咱们还结合了idealHandler作了一个更优的延迟初始化的方案,利用它能够在主线程的空闲时间进行初始化,以减小启动耗时致使的卡顿现象。作完这些以后,咱们的启动速度就变得很快了。

最后,我简单说下咱们是怎么长期来保持启动优化的效果的。首先,咱们作了咱们的启动器,而且结合了咱们的CI,在线上加上了不少方面的监控。(引导=> 第4题)

二、是怎么异步的,异步遇到问题没有?

  • 体现演进过程
  • 详细介绍启动器

咱们最初是采用的普通的一个异步的方案,即new Thread + 设置线程优先级为后台线程的方式在Application的onCreate方法中进行异步初始化,后来,咱们使用了线程池、IntentService的方式,可是,在咱们应用的演进过程中,发现代码会变得不够优雅,而且有些场景很是很差处理,好比说多个初始化任务直接的依赖关系,好比说某一个初始化任务须要在某一个特定的生命周期中初始化完成,这些都是使用线程池、IntentService没法实现的。因此说,咱们就开始思考一个新的解决方案,它可以完美地解决咱们刚刚所遇到的这些问题。

这个方案就是咱们目前所使用的启动器,在启动器的概念中,咱们将每个初始化代码抽象成了一个Task,而后,对它们进行了一个排序,根据它们之间的依赖关系排了一个有向无环图,接着,使用一个异步队列进行执行,而且这个异步队列它和CPU的核心数是强烈相关的,它可以最大程度地保证咱们的主线程和别的线程都可以执行咱们的任务,也就是你们几乎均可以同时完成。

三、启动优化有哪些容易忽略的注意点?

  • cpu time与wall time
  • 注意延迟初始化的优化
  • 介绍下黑科技

首先,在CPU Profiler和Systrace中有两个很重要的指标,即cpu time与wall time,咱们必须清楚cpu time与wall time之间的区别,wall time指的是代码执行的时间,而cpu time指的是代码消耗CPU的时间,锁冲突会形成二者时间差距过大。咱们须要以cpu time来做为咱们优化的一个方向。

其次,咱们不只只追求启动速度上的一个提高,也须要注意延迟初始化的一个优化,对于延迟初始化,一般的作法是在界面显示以后才去进行加载,可是若是此时界面须要进行滑动等与用户交互的一系列操做,就会有很严重的卡顿现象,所以咱们使用了idealHandler来实现cpu空闲时间来执行耗时任务,这极大地提高了用户的体验,避免了因启动耗时任务而致使的页面卡顿现象。

最后,对于启动优化,还有一些黑科技,首先,就是咱们采用了类预先加载的方式,咱们在MultiDex.install方法以后起了一个线程,而后用Class.forName的方式来预先触发类的加载,而后当咱们这个类真正被使用的时候,就不用再进行类加载的过程了。同时,咱们再看Systrace图的时候,有一部分手机其实并无给咱们应用去跑满cpu,好比说它有8核,可是却只给了咱们4核等这些状况,而后,有些应用对此作了一些黑科技,它会将cpu的核心数以及cpu的频率在启动的时候去进行一个暴力的提高。

四、版本迭代致使的启动变慢有好的解决方式吗?

  • 启动器
  • 结合CI
  • 监控完善

这种问题其实咱们以前也遇到过,这的确很是难以解决。可是,咱们后面对此进行了反复的思考与尝试,终于找到了一个比较好的解决方式。

首先,咱们使用了启动器去管理每个初始化任务,而且启动器中每个任务的执行都是被其自动进行分配的,也就是说这些自动分配的task咱们会尽可能保证它会平均分配在咱们每个线程当中的,这和咱们普通的异步是不同的,它能够很好地缓解咱们应用的启动变慢。

其次,咱们还结合了CI,好比说,咱们如今限制了一些类,如Application,若是有人修改了它,咱们不会让这部分代码合并到主干分支或者是修改以后会有一些内部的工具如邮件的形式发送到我,而后,我就会和他确认他加的这些代码究竟是耗时多少,可否异步初始化,不能异步的话就考虑延迟初始化,若是初始化时间太长,则能够考虑是否能进行懒加载,等用到的时候再去使用等等。

而后,咱们会将问题尽量地暴露在上线以前。同时,咱们真正已经到了线上的一个环境下时,咱们进行了监控的一个完善,咱们不只是监控了App的整个的启动时间,同时呢,咱们也将每个生命周期都进行了一个监控。好比说Application的onCreate与onAttachBaseContext方法的耗时,以及这两个生命周期之间间隔的时间,咱们都进行了一个监控,若是说下一次咱们发现了这个启动速度变慢了,咱们就能够去查找究竟是哪个环节变慢了,咱们会和之前的版本进行对比,对比完成以后呢,咱们就能够来找这一段新加的代码。

五、开放问题:若是提升启动速度,设计一个延迟加载框架或者sdk的方法和注意的问题

image

image

须要更全面更深刻的理解请查看深刻探索Android启动速度优化

三、App内存优化

一、大家内存优化项目的过程是怎么作的?

一、分析现状、确认问题

咱们发现咱们的APP在内存方面可能存在很大的问题,第一方面的缘由是咱们的线上的OOM率比较高。第二点呢,咱们常常会看到在咱们的Android Studio的Profiler工具中内存的抖动比较频繁。这是我一个初步的现状,而后在咱们知道了这个初步的现状以后,进行了问题的确认,咱们通过一系列的调研以及深刻研究,咱们最终发现咱们的项目中存在如下几点大问题,好比说:内存抖动、内存溢出、内存泄漏,还有咱们的Bitmap使用很是粗犷。

二、针对性优化

好比内存抖动的解决 -> Memory Profiler工具的使用(呈现了锯齿张图形) -> 分析到具体代码存在的问题(频繁被调用的方法中出现了日志字符串的拼接),也能够说说内存泄漏或内存溢出的解决。

三、效率提高

为了避免增长业务同窗的工做量,咱们使用了一些工具类或ARTHook这样的大图检测方案,没有任何的侵入性,同时,咱们将这些技术教给了你们,而后让你们一块儿进行工做效率上的提高。

咱们对内存优化工具Memory Profiler、MAT的使用比较熟悉,所以针对一系列不一样问题的状况,咱们写了一系列解决方案的文档,分享给你们。这样,咱们整个团队成员的内存优化意识就变强了。

二、你作了内存优化最大的感觉是什么?

一、磨刀不误砍柴工

咱们一开始并无直接去分析项目中代码哪些地方存在内存问题,而是先去学习了Google官方的一些文档,好比说学习了Memory Profiler工具的使用、学习了MAT工具的使用,在咱们将这些工具学习熟练以后,当在咱们的项目中遇到内存问题时,咱们就可以很快地进行排查定位问题进行解决。

二、技术优化必须结合业务代码

一开始,咱们作了总体APP运行阶段的一个内存上报,而后,咱们在一些重点的内存消耗模块进行了一些监控,可是后面发现这些监控并无紧密地结合咱们的业务代码,好比说在梳理完项目以后,发现咱们项目中存在使用多个图片库的状况,多个图片库的内存缓存确定是不公用的,因此致使咱们整个项目的内存使用量很是高。因此进行技术优化时必须结合咱们的业务代码。

三、系统化完善解决方案

咱们在作内存优化的过程当中,不只作了Android端的优化工做,还将咱们Android端一些数据的采集上报到了咱们的服务器,而后传到咱们的后台,这样,方便咱们的不管是Bug跟踪人员或者是Crash跟踪人员进行一系列问题的解决。

三、如何检测全部不合理的地方?

好比说大图片的检测,咱们最初的一个方案是经过继承ImageView,重写它的onDraw方法来实现。可是,咱们在推广它的过程当中,发现不少开发人员并不接受,由于不少ImageView以前已经写过了,你如今让他去替换,工做成本是比较高的。因此说,后来咱们就想,有没有一种方案能够免替换,最终咱们就找到了ARTHook这样一个Hook的方案。

如何避免内存抖动?(代码注意事项)

内存抖动是因为短期内有大量对象进出新生区致使的,它伴随着频繁的GC,gc会大量占用ui线程和cpu资源,会致使app总体卡顿。

避免发生内存抖动的几点建议:

  • 尽可能避免在循环体内建立对象,应该把对象建立移到循环体外。
  • 注意自定义View的onDraw()方法会被频繁调用,因此在这里面不该该频繁的建立对象。
  • 当须要大量使用Bitmap的时候,试着把它们缓存在数组或容器中实现复用。
  • 对于可以复用的对象,同理能够使用对象池将它们缓存起来。

image

须要更全面更深刻的理解请查看Android性能优化以内存优化深刻探索Android内存优化

四、App绘制优化

一、你在作布局优化的过程当中用到了哪些工具?

我在作布局优化的过程当中,用到了不少的工具,可是每个工具都有它不一样的使用场景,不一样的场景应该使用不一样的工具。下面我从线上和线下两个角度来进行分析。

好比说,我要统计线上的FPS,我使用的就是Choreographer这个类,它具备如下特性:

  • 一、可以获取总体的帧率。
  • 二、可以带到线上使用。
  • 三、它获取的帧率几乎是实时的,可以知足咱们的需求。

同时,在线下,若是要去优化布局加载带来的时间消耗,那就须要检测每个布局的耗时,对此我使用的是AOP的方式,它没有侵入性,同时也不须要别的开发同窗进行接入,就能够方便地获取每个布局加载的耗时。若是还要更细粒度地去检测每个控件的加载耗时,那么就须要使用LayoutInflaterCompat.setFactory2这个方法去进行Hook。

此外,我还使用了LayoutInspector和Systrace这两个工具,Systrace能够很方便地看到每帧的具体耗时以及这一帧在布局当中它真正作了什么。而LayoutInspector能够很方便地看到每个界面的布局层级,帮助咱们对层级进行优化。

二、布局为何会致使卡顿,你又是如何优化的?

分析完布局的加载流程以后,咱们发现有以下四点可能会致使布局卡顿:

  • 一、首先,系统会将咱们的Xml文件经过IO的方式映射的方式加载到咱们的内存当中,而IO的过程可能会致使卡顿。
  • 二、其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会致使卡顿。
  • 三、同时,这个布局的层级若是比较深,那么进行布局遍历的过程就会比较耗时。
  • 四、最后,不合理的嵌套RelativeLayout布局也会致使重绘的次数过多。

对此,咱们的优化方式有以下几种:

  • 一、针对布局加载Xml文件的优化,咱们使用了异步Inflate的方式,即AsyncLayoutInflater。它的核心原理是在子线程中对咱们的Layout进行加载,而加载完成以后会将View经过Handler发送到主线程来使用。因此不会阻塞咱们的主线程,加载的时间所有是在异步线程中进行消耗的。而这仅仅是一个从侧面缓解的思路。
  • 二、后面,咱们发现了一个从根源解决上述痛点的方式,即便用X2C框架。它的一个核心原理就是在开发过程咱们仍是使用的XML进行编写布局,可是在编译的时候它会使用APT的方式将XML布局转换为Java的方式进行布局,经过这样的方式去写布局,它有如下优势:一、它省去了使用IO的方式去加载XML布局的耗时过程。二、它是采用Java代码直接new的方式去建立控件对象,因此它也没有反射带来的性能损耗。这样就从根本上解决了布局加载过程当中带来的问题。
  • 三、而后,咱们能够使用ConstraintLayout去减小咱们界面布局的嵌套层级,若是原始布局层级越深,它能减小的层级就越多。而使用它也能避免嵌套RelativeLayout布局致使的重绘次数过多。
  • 四、最后,咱们能够使用AspectJ框架(即AOP)和LayoutInflaterCompat.setFactory2的方式分别去创建线下全局的布局加载速度和控件加载速度的监控体系。

三、作完布局优化有哪些成果产出?

  • 一、首先,咱们创建了一个体系化的监控手段,这里的体系还指的是线上加线下的一个综合方案,针对线下,咱们使用AOP或者ARTHook,能够很方便地获取到每个布局的加载耗时以及每个控件的加载耗时。针对线上,咱们经过Choreographer.getInstance().postFrameCallback的方式收集到了FPS,这样咱们能够知道用户在哪些界面出现了丢帧的状况。
  • 二、而后,对于布局监控方面,咱们设立了FPS、布局加载时间、布局层级等一系列指标。
  • 三、最后,在每个版本上线以前,咱们都会对咱们的核心路径进行一次Review,确保咱们的FPS、布局加载时间、布局层级等达到一个合理的状态。

四、你是怎么作卡顿优化的?

从项目的初期到壮大期,最后再到成熟期,每个阶段都针对卡顿优化作了不一样的处理。各个阶段所作的事情以下所示:

  • 一、系统工具定位、解决
  • 二、自动化卡顿方案及优化
  • 三、线上监控及线下监测工具的建设

我作卡顿优化也是经历了一些阶段,最初咱们的项目当中的一些模块出现了卡顿以后,我是经过系统工具进行了定位,我使用了Systrace,而后看了卡顿周期内的CPU情况,同时结合代码,对这个模块进行了重构,将部分代码进行了异步和延迟,在项目初期就是这样解决了问题。可是呢,随着咱们项目的扩大,线下卡顿的问题也愈来愈多,同时,在线上,也有卡顿的反馈,可是线上的反馈卡顿,咱们在线下难以复现,因而咱们开始寻找自动化的卡顿监测方案,其思路是来自于Android的消息处理机制,主线程执行任何代码都会回到Looper.loop方法当中,而这个方法中有一个mLogging对象,它会在每一个message的执行先后都会被调用,咱们就是利用这个先后处理的时机来作到的自动化监测方案的。同时,在这个阶段,咱们也完善了线上ANR的上报,咱们采起的方式就是监控ANR的信息,同时结合了ANR-WatchDog,做为高版本没有文件权限的一个补充方案。在作完这个卡顿检测方案以后呢,咱们还作了线上监控及线下检测工具的建设,最终实现了一整套完善,多维度的解决方案。

五、你是怎么样自动化的获取卡顿信息?

咱们的思路是来自于Android的消息处理机制,主线程执行任何代码它都会走到Looper.loop方法当中,而这个函数当中有一个mLogging对象,它会在每一个message处理先后都会被调用,而主线程发生了卡顿,那就必定会在dispatchMessage方法中执行了耗时的代码,那咱们在这个message执行以前呢,咱们能够在子线程当中去postDelayed一个任务,这个Delayed的时间就是咱们设定的阈值,若是主线程的messaege在这个阈值以内完成了,那就取消掉这个子线程当中的任务,若是主线程的message在阈值以内没有被完成,那子线程当中的任务就会被执行,它会获取到当前主线程执行的一个堆栈,那咱们就能够知道哪里发生了卡顿。

通过实践,咱们发现这种方案获取的堆栈信息它不必定是准确的,由于获取到的堆栈信息它极可能是主线程最终执行的一个位置,而真正耗时的地方其实已经执行完成了,因而呢,咱们就对这个方案作了一些优化,咱们采起了高频采集的方案,也就是在一个周期内咱们会屡次采集主线程的堆栈信息,若是发生了卡顿,那咱们就将这些卡顿信息压缩以后上报给APM后台,而后找出重复的堆栈信息,这些重复发生的堆栈大几率就是卡顿发生的一个位置,这样就提升了获取卡顿信息的一个准确性。

六、卡顿的一整套解决方案是怎么作的?

首先,针对卡顿,咱们采用了线上、线下工具相结合的方式,线下工具咱们册中医药尽量早地去暴露问题,而针对于线上工具呢,咱们侧重于监控的全面性、自动化以及异常感知的灵敏度。

同时呢,卡顿问题还有不少的难题。好比说有的代码呢,它不到你卡顿的一个阈值,可是执行过多,或者它错误地执行了不少次,它也会致使用户感官上的一个卡顿,因此咱们在线下经过AOP的方式对常见的耗时代码进行了Hook,而后对一段时间内获取到的数据进行分析,咱们就能够知道这些耗时的代码发生的时机和次数以及耗时状况。而后,看它是否是知足咱们的一个预期,不知足预期的话,咱们就能够直接到线下进行修改。同时,卡顿监控它还有不少容易被忽略的一个盲区,好比说生命周期的一个间隔,那对于这种特定的问题呢,咱们就采用了编译时注解的方式修改了项目当中全部Handler的父类,对于其中的两个方法进行了监控,咱们就能够知道主线程message的执行时间以及它们的调用堆栈。

对于线上卡顿,咱们除了计算App的卡顿率、ANR率等常规指标以外呢,咱们还计算了页面的秒开率、生命周期的执行时间等等。并且,在卡顿发生的时刻,咱们也尽量多地保存下来了当前的一个场景信息,这为咱们以后解决或者复现这个卡顿留下了依据。

七、TextView setText耗时的缘由,对TextView绘制层源码的理解?

八、开放问题:优化一个列表页面的打开速度和流畅性。

image

须要更全面更深刻的理解请查看Android性能优化之绘制优化深刻探索Android布局优化(上)深刻探索Android布局优化(下)

五、App瘦身

image

六、网络优化

一、移动端获取网络数据优化的几个点

  • 一、链接复用:节省链接创建时间,如开启 keep-alive。于Android来讲默认状况下HttpURLConnection和HttpClient都开启了keep-alive。只是2.2以前HttpURLConnection存在影响链接池的Bug。

  • 二、请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的CSS Image Sprites。若是某个页面内请求过多,也能够考虑作必定的请求合并。

  • 三、减小请求数据的大小:对于post请求,body能够作gzip压缩的,header也能够作数据压缩(不过只支持http 2.0)。 返回数据的body也能够作gzip压缩,body数据体积能够缩小到原来的30%左右(也能够考虑压缩返回的json数据的key数据的体积,尤为是针对返回数据格式变化不大的状况,支付宝聊天返回的数据用到了)。

  • 四、根据用户的当前的网络质量来判断下载什么质量的图片(电商用的比较多)。

  • 五、使用HttpDNS优化DNS:DNS存在解析慢和DNS劫持等问题,DNS 不只支持 UDP,它还支持 TCP,可是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。HTTPDNS 则不一样,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提升域名解析的效率。

image

参考文章

二、客户端网络安全实现

三、设计一个网络优化方案,针对移动端弱网环境。

七、App电量优化

image

八、安卓的安全优化

一、提升app安全性的方法?

二、安卓的app加固如何作?

三、安卓的混淆原理是什么?

四、谈谈你对安卓签名的理解。

九、为何WebView加载会慢呢?

这是由于在客户端中,加载H5页面以前,须要先初始化WebView,在WebView彻底初始化完成以前,后续的界面加载过程都是被阻塞的。

优化手段围绕着如下两个点进行:

  • 预加载WebView。
  • 加载WebView的同时,请求H5页面数据。

所以常见的方法是:

  • 全局WebView。
  • 客户端代理页面请求。WebView初始化完成后向客户端请求数据。
  • asset存放离线包。

除此以外还有一些其余的优化手段:

  • 脚本执行慢,可让脚本最后运行,不阻塞页面解析。
  • DNS连接慢,可让客户端复用使用的域名与连接。
  • React框架代码执行慢,能够将这部分代码拆分出来,提早进行解析。

十、如何优化自定义View

为了加速你的view,对于频繁调用的方法,须要尽可能减小没必要要的代码。先从onDraw开始,须要特别注意不该该在这里作内存分配的事情,由于它会致使GC,从而致使卡顿。在初始化或者动画间隙期间作分配内存的动做。不要在动画正在执行的时候作内存分配的事情。

你还须要尽量的减小onDraw被调用的次数,大多数时候致使onDraw都是由于调用了invalidate().所以请尽可能减小调用invaildate()的次数。若是可能的话,尽可能调用含有4个参数的invalidate()方法而不是没有参数的invalidate()。没有参数的invalidate会强制重绘整个view。

另一个很是耗时的操做是请求layout。任什么时候候执行requestLayout(),会使得Android UI系统去遍历整个View的层级来计算出每个view的大小。若是找到有冲突的值,它会须要从新计算好几回。另外须要尽可能保持View的层级是扁平化的,这样对提升效率颇有帮助。

若是你有一个复杂的UI,你应该考虑写一个自定义的ViewGroup来执行他的layout操做。与内置的view不一样,自定义的view能够使得程序仅仅测量这一部分,这避免了遍历整个view的层级结构来计算大小。

十一、FC(Force Close)何时会出现?

Error、OOM,StackOverFlowError、Runtime,好比说空指针异常

解决的办法:

  • 注意内存的使用和管理
  • 使用Thread.UncaughtExceptionHandler接口

十二、Java多线程引起的性能问题,怎么解决

1三、TraceView的实现原理,分析数据偏差来源。

1四、是否使用过SysTrace,原理的了解?

1五、mmap + native 日志优化?

传统日志打印有两个性能问题,一个是反复操做文件描述符表,一个是反复进入内核态。因此须要使用mmap的方式去直接读写内存。

2、Android Framework相关

一、Android系统架构

image

Android 是一种基于 Linux 的开放源代码软件栈,为普遍的设备和机型而建立。下图所示为 Android 平台的五大组件:

1.应用程序

Android 随附一套用于电子邮件、短信、日历、互联网浏览和联系人等的核心应用。平台随附的应用与用户能够选择安装的应用同样,没有特殊状态。所以第三方应用可成为用户的默认网络浏览器、短信 Messenger 甚至默认键盘(有一些例外,例如系统的“设置”应用)。

系统应用可用做用户的应用,以及提供开发者可从其本身的应用访问的主要功能。例如,若是您的应用要发短信,您无需本身构建该功能,能够改成调用已安装的短信应用向您指定的接收者发送消息。

二、Java API 框架

您可经过以 Java 语言编写的 API 使用 Android OS 的整个功能集。这些 API 造成建立 Android 应用所需的构建块,它们可简化核心模块化系统组件和服务的重复使用,包括如下组件和服务:

  • 丰富、可扩展的视图系统,可用以构建应用的 UI,包括列表、网格、文本框、按钮甚至可嵌入的网络浏览器
  • 资源管理器,用于访问非代码资源,例如本地化的字符串、图形和布局文件
  • 通知管理器,可以让全部应用在状态栏中显示自定义提醒
  • Activity 管理器,用于管理应用的生命周期,提供常见的导航返回栈
  • 内容提供程序,可以让应用访问其余应用(例如“联系人”应用)中的数据或者共享其本身的数据

开发者能够彻底访问 Android 系统应用使用的框架 API。

三、系统运行库

1)原生 C/C++ 库

许多核心 Android 系统组件和服务(例如 ART 和 HAL)构建自原生代码,须要以 C 和 C++ 编写的原生库。Android 平台提供 Java 框架 API 以向应用显示其中部分原生库的功能。例如,您能够经过 Android 框架的 Java OpenGL API 访问 OpenGL ES,以支持在应用中绘制和操做 2D 和 3D 图形。若是开发的是须要 C 或 C++ 代码的应用,能够使用 Android NDK 直接从原生代码访问某些原平生台库。

2)Android Runtime

对于运行 Android 5.0(API 级别 21)或更高版本的设备,每一个应用都在其本身的进程中运行,而且有其本身的 Android Runtime (ART) 实例。ART 编写为经过执行 DEX 文件在低内存设备上运行多个虚拟机,DEX 文件是一种专为 Android 设计的字节码格式,通过优化,使用的内存不多。编译工具链(例如 Jack)将 Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。

ART 的部分主要功能包括:

  • 预先 (AOT) 和即时 (JIT) 编译
  • 优化的垃圾回收 (GC)
  • 更好的调试支持,包括专用采样分析器、详细的诊断异常和崩溃报告,而且可以设置监视点以监控特定字段

在 Android 版本 5.0(API 级别 21)以前,Dalvik 是 Android Runtime。若是您的应用在 ART 上运行效果很好,那么它应该也可在 Dalvik 上运行,但反过来不必定。

Android 还包含一套核心运行时库,可提供 Java API 框架使用的 Java 编程语言大部分功能,包括一些 Java 8 语言功能。

四、硬件抽象层 (HAL)

硬件抽象层 (HAL) 提供标准界面,向更高级别的 Java API 框架显示设备硬件功能。HAL 包含多个库模块,其中每一个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载库模块。

五、Linux 内核

Android 平台的基础是 Linux 内核。例如,Android Runtime (ART) 依靠 Linux 内核来执行底层功能,例如线程和低层内存管理。使用 Linux 内核可以让 Android 利用主要安全功能,而且容许设备制造商为著名的内核开发硬件驱动程序。

对于Android应用开发来讲,最好能手绘下面的系统架构图:

image

二、View的事件分发机制?滑动冲突怎么解决?

了解Activity的构成

一个Activity包含了一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindow将DecorView做为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域:一个是TitleView,另外一个是ContentView,而咱们平时所写的就是展现在ContentView中的。

触摸事件的类型

触摸事件对应的是MotionEvent类,事件的类型主要有以下三种:

  • ACTION_DOWN
  • ACTION_MOVE(移动的距离超过必定的阈值会被断定为ACTION_MOVE操做)
  • ACTION_UP

View事件分发本质就是对MotionEvent事件分发的过程。即当一个MotionEvent发生后,系统将这个点击事件传递到一个具体的View上。

事件分发流程

事件分发过程由三个方法共同完成:

dispatchTouchEvent:方法返回值为true表示事件被当前视图消费掉;返回为super.dispatchTouchEvent表示继续分发该事件,返回为false表示交给父类的onTouchEvent处理。

onInterceptTouchEvent:方法返回值为true表示拦截这个事件并交由自身的onTouchEvent方法进行消费;返回false表示不拦截,须要继续传递给子视图。若是return super.onInterceptTouchEvent(ev), 事件拦截分两种状况:  

  • 1.若是该View存在子View且点击到了该子View, 则不拦截, 继续分发 给子View 处理, 此时至关于return false。
  • 2.若是该View没有子View或者有子View可是没有点击中子View(此时ViewGroup 至关于普通View), 则交由该View的onTouchEvent响应,此时至关于return true。

注意:通常的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默认不拦截, 而 ScrollView、ListView等ViewGroup则可能拦截,得看具体状况。

onTouchEvent:方法返回值为true表示当前视图能够处理对应的事件;返回值为false表示当前视图不处理这个事件,它会被传递给父视图的onTouchEvent方法进行处理。若是return super.onTouchEvent(ev),事件处理分为两种状况:

  • 1.若是该View是clickable或者longclickable的,则会返回true, 表示消费 了该事件, 与返回true同样;
  • 2.若是该View不是clickable或者longclickable的,则会返回false, 表示不 消费该事件,将会向上传递,与返回false同样。

注意:在Android系统中,拥有事件传递处理能力的类有如下三种:

  • Activity:拥有分发和消费两个方法。
  • ViewGroup:拥有分发、拦截和消费三个方法。
  • View:拥有分发、消费两个方法。

三个方法的关系用伪代码表示以下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        coonsume = child.dispatchTouchEvent(ev);
    }
    
    return consume;
}
复制代码

经过上面的伪代码,咱们能够大体了解点击事件的传递规则:对应一个根ViewGroup来讲,点击事件产生后,首先会传递给它,这是它的dispatchTouchEvent就会被调用,若是这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,这时若是它的mOnTouchListener被设置,则onTouch会被调用,不然onTouchEvent会被调用。在onTouchEvent中,若是设置了mOnCLickListener,则onClick会被调用。只要View的CLICKABLE和LONG_CLICKABLE有一个为true,onTouchEvent()就会返回true消耗这个事件。若是这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。

一些重要的结论:

一、事件传递优先级:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。

二、正常状况下,一个时间序列只能被一个View拦截且消耗。由于一旦一个元素拦截了此事件,那么同一个事件序列内的全部事件都会直接交给它处理(即不会再调用这个View的拦截方法去询问它是否要拦截了,而是把剩余的ACTION_MOVE、ACTION_DOWN等事件直接交给它来处理)。特例:经过将重写View的onTouchEvent返回false可强行将事件转交给其余View处理。

三、若是View不消耗除ACTION_DOWN之外的其余事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,而且当前View能够持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

四、ViewGroup默认不拦截任何事件(返回false)。

五、View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分状况,好比Button的clickable属性默认为true,而TextView的clickable默认为false。

六、View的enable属性不影响onTouchEvent的默认返回值。

七、经过requestDisallowInterceptTouchEvent方法能够在子元素中干预父元素的事件分发过程,可是ACTION_DOWN事件除外。

记住这个图的传递顺序,面试的时候可以画出来,就很详细了:

image

ACTION_CANCEL何时触发,触摸button而后滑动到外部抬起会触发点击事件吗,再滑动回去抬起会么?

  • 通常ACTION_CANCEL和ACTION_UP都做为View一段事件处理的结束。若是在父View中拦截ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。
  • 若是触摸某个控件,可是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现action_cancel。
点击事件被拦截,可是想传到下面的View,如何操做?

重写子类的requestDisallowInterceptTouchEvent()方法返回true就不会执行父类的onInterceptTouchEvent(),便可将点击事件传到下面的View。

如何解决View的事件冲突?举个开发中遇到的例子?

常见开发中事件冲突的有ScrollView与RecyclerView的滑动冲突、RecyclerView内嵌同时滑动同一方向。

滑动冲突的处理规则:

  • 对于因为外部滑动和内部滑动方向不一致致使的滑动冲突,能够根据滑动的方向判断谁来拦截事件。
  • 对于因为外部滑动方向和内部滑动方向一致致使的滑动冲突,能够根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。
  • 对于上面两种状况的嵌套,相对复杂,可一样根据需求在业务上找到突破点。

滑动冲突的实现方法:

  • 外部拦截法:指点击事件都先通过父容器的拦截处理,若是父容器须要此事件就拦截,不然就不拦截。具体方法:须要重写父容器的onInterceptTouchEvent方法,在内部作出相应的拦截。
  • 内部拦截法:指父容器不拦截任何事件,而将全部的事件都传递给子容器,若是子容器须要此事件就直接消耗,不然就交由父容器进行处理。具体方法:须要配合requestDisallowInterceptTouchEvent方法。

加深理解,GOGOGO

三、View的绘制流程?

DecorView被加载到Window中

  • 从Activity的startActivity开始,最终调用到ActivityThread的handleLaunchActivity方法来建立Activity,首先,会调用performLaunchActivity方法,内部会执行Activity的onCreate方法,从而完成DecorView和Activity的建立。而后,会调用handleResumeActivity,里面首先会调用performResumeActivity去执行Activity的onResume()方法,执行完后会获得一个ActivityClientRecord对象,而后经过r.window.getDecorView()的方式获得DecorView,而后会经过a.getWindowManager()获得WindowManager,最终调用其addView()方法将DecorView加进去。
  • WindowManager的实现类是WindowManagerImpl,它内部会将addView的逻辑委托给WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在WindowManagerGlobal的addView()方法中不只会将DecorView添加到Window中,同时会建立ViewRootImpl对象,并将ViewRootImpl对象和DecorView经过root.setView()把DecorView加载到Window中。这里的ViewRootImpl是ViewRoot的实现类,是链接WindowManager和DecorView的纽带。View的三大流程均是经过ViewRoot来完成的。

了解绘制的总体流程

绘制会从根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每一个View控件负责绘制本身,而ViewGroup还须要负责通知本身的子View进行绘制操做。

理解MeasureSpec

MeasureSpec表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来讲明应该如何测量这个View。它由三种测量模式,以下:

  • EXACTLY:精确测量模式,视图宽高指定为match_parent或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下View的测量值就是SpecSize的值。
  • AT_MOST:最大值测量模式,当视图的宽高指定为wrap_content时生效,此时子视图的尺寸能够是不超过父视图容许的最大尺寸的任何尺寸。
  • UNSPECIFIED:不指定测量模式, 父视图没有限制子视图的大小,子视图能够是想要的任何尺寸,一般用于系统内部,应用开发中不多用到。

MeasureSpec经过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操做,其提供了打包和解包的方法,打包方法为makeMeasureSpec,解包方法为getMode和getSize。

普通View的MeasureSpec的建立规则以下:

image

对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。

如何根据MeasureSpec去实现一个瀑布流的自定义ViewGroup?

View绘制流程之Measure

  • 首先,在ViewGroup中的measureChildren()方法中会遍历测量ViewGroup中全部的View,当View的可见性处于GONE状态时,不对其进行测量。
  • 而后,测量某个指定的View时,根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec。
  • 最后,将计算出的MeasureSpec传入View的measure方法,这里ViewGroup没有定义测量的具体过程,由于ViewGroup是一个抽象类,其测量过程的onMeasure方法须要各个子类去实现。不一样的ViewGroup子类有不一样的布局特性,这致使它们的测量细节各不相同,若是须要自定义测量过程,则子类能够重写这个方法。(setMeasureDimension方法用于设置View的测量宽高,若是View没有重写onMeasure方法,则会默认调用getDefaultSize来得到View的宽高)
getSuggestMinimumWidth分析

若是View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值能够为0;若是View设置了背景,则返回android:minWidth和背景的最小宽度这二者中的最大值。

自定义View时手动处理wrap_content时的情形

直接继承View的控件须要重写onMeasure方法并设置wrap_content时的自身大小,不然在布局中使用wrap_content就至关于使用match_parent。此时,能够在wrap_content的状况下(对应MeasureSpec.AT_MOST)指定内部宽/高(mWidth和mHeight)。

LinearLayout的onMeasure方法实现解析(这里仅分析measureVertical核心源码)

系统会遍历子元素并对每一个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,而且系统会经过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增长,增长的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。

在Activity中获取某个View的宽高

因为View的measure过程和Activity的生命周期方法不是同步执行的,若是View尚未测量完毕,那么得到的宽/高就是0。因此在onCreate、onStart、onResume中均没法正确获得某个View的宽高信息。解决方式以下:

  • Activity/View#onWindowFocusChanged:此时View已经初始化完毕,当Activity的窗口获得焦点和失去焦点时均会被调用一次,若是频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用。
  • view.post(runnable): 经过post能够将一个runnable投递到消息队列的尾部,始化好了而后等待Looper调用次runnable的时候,View也已经初始化好了。
  • ViewTreeObserver#addOnGlobalLayoutListener:当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调。
  • View.measure(int widthMeasureSpec, int heightMeasureSpec):match_parent时不知道parentSize的大小,测不出;具体数值时,直接makeMeasureSpec固定值,而后调用view..measure就能够了;wrap_content时,在最大化模式下,用View理论上能支持的最大值去构造MeasureSpec是合理的。

View的绘制流程之Layout

首先,会经过setFrame方法来设定View的四个顶点的位置,即View在父容器中的位置。而后,会执行到onLayout空方法,子类若是是ViewGroup类型,则重写这个方法,实现ViewGroup中全部View控件布局流程。

LinearLayout的onLayout方法实现解析(layoutVertical核心源码)

其中会遍历调用每一个子View的setChildFrame方法为子元素肯定对应的位置。其中的childTop会逐渐增大,意味着后面的子元素会被放置在靠下的位置。

注意:在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高造成于View的measure过程,而最终宽/高造成于View的layout过程,即二者的赋值时机不一样,测量宽/高的赋值时机稍微早一些。在一些特殊的状况下则二者不相等:

  • 重写View的layout方法,使最终宽度老是比测量宽/高大100px。
  • View须要屡次measure才能肯定本身的测量宽/高,在前几回测量的过程当中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来讲,测量宽/高仍是和最终宽/高相同。

View的绘制流程之Draw

Draw的基本流程

绘制基本上能够分为六个步骤:

  • 首先绘制View的背景;
  • 若是须要的话,保持canvas的图层,为fading作准备;
  • 而后,绘制View的内容;
  • 接着,绘制View的子View;
  • 若是须要的话,绘制View的fading边缘并恢复图层;
  • 最后,绘制View的装饰(例如滚动条等等)。
setWillNotDraw的做用

若是一个View不须要绘制任何内容,那么设置这个标记位为true之后,系统会进行相应的优化。

  • 默认状况下,View没有启用这个优化标记位,可是ViewGroup会默认启用这个优化标记位。
  • 当咱们的自定义控件继承于ViewGroup而且自己不具有绘制功能时,就能够开启这个标记位从而便于系统进行后续的优化。
  • 当明确知道一个ViewGroup须要经过onDraw来绘制内容时,咱们须要显示地关闭WILL_NOT_DRAW这个标记位。

Requestlayout,onlayout,onDraw,DrawChild区别与联系?

requestLayout()方法 :会致使调用 measure()过程 和 layout()过程,将会根据标志位判断是否须要ondraw。

onLayout()方法:若是该View是ViewGroup对象,须要实现该方法,对每一个子视图进行布局。

onDraw()方法:绘制视图自己 (每一个View都须要重载该方法,ViewGroup不须要实现该方法)。

drawChild():去从新回调每一个子视图的draw()方法。

invalidate() 和 postInvalidate()的区别 ?

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用须要配合handler;而postInvalidate()可在子线程中直接调用。

更详细的内容请点击这里

四、跨进程通讯。

Android中进程和线程的关系?区别?

  • 线程是CPU调度的最小单元,同时线程是一种有限的系统资源;而进程通常指一个执行单元,在PC和移动设备上指一个程序或者一个应用。
  • 通常来讲,一个App程序至少有一个进程,一个进程至少有一个线程(包含与被包含的关系),通俗来说就是,在App这个工厂里面有一个进程,线程就是里面的生产线,但主线程(即主生产线)只有一条,而子线程(即副生产线)能够有多个。
  • 进程有本身独立的地址空间,而进程中的线程共享此地址空间,均可以并发执行。

如何开启多进程?应用是否能够开启N个进程?

在AndroidManifest中给四大组件指定属性android:process开启多进程模式,在内存容许的条件下能够开启N个进程。

为什么须要IPC?多进程通讯可能会出现的问题?

全部运行在不一样进程的四大组件(Activity、Service、Receiver、ContentProvider)共享数据都会失败,这是因为Android为每一个应用分配了独立的虚拟机,不一样的虚拟机在内存分配上有不一样的地址空间,这会致使在不一样的虚拟机中访问同一个类的对象会产生多份副本。好比经常使用例子(经过开启多进程获取更大内存空间、两个或者多个应用之间共享数据、微信全家桶)。

通常来讲,使用多进程通讯会形成以下几方面的问题:

  • 静态成员和单例模式彻底失效:独立的虚拟机形成。
  • 线程同步机制彻底失效:独立的虚拟机形成。
  • SharedPreferences的可靠性降低:这是由于Sp不支持两个进程并发进行读写,有必定概率致使数据丢失。
  • Application会屡次建立:Android系统在建立新的进程时会分配独立的虚拟机,因此这个过程其实就是启动一个应用的过程,天然也会建立新的Application。

Android中IPC方式、各类方式优缺点?

image

讲讲AIDL?如何优化多模块都使用AIDL的状况?

AIDL(Android Interface Definition Language,Android接口定义语言):若是在一个进程中要调用另外一个进程中对象的方法,可以使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,经过它客户端能够实现间接调用服务端对象的方法。

AIDL的本质是系统提供了一套可快速实现Binder的工具。关键类和方法:

  • AIDL接口:继承IInterface。
  • Stub类:Binder的实现类,服务端经过这个类来提供服务。
  • Proxy类:服务端的本地代理,客户端经过这个类调用服务端的方法。
  • asInterface():客户端调用,将服务端返回的Binder对象,转换成客户端所须要的AIDL接口类型的对象。若是客户端和服务端位于同一进程,则直接返回Stub对象自己,不然返回系统封装后的Stub.proxy对象。
  • asBinder():根据当前调用状况返回代理Proxy的Binder对象。
  • onTransact():运行在服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会经过系统底层封装后交由此方法来处理。
  • transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。以后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

当有多个业务模块都须要AIDL来进行IPC,此时须要为每一个模块建立特定的aidl文件,那么相应的Service就会不少。必然会出现系统资源耗费严重、应用过分重量级的问题。解决办法是创建Binder链接池,即将每一个业务模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复建立Service。

工做原理:每一个业务模块建立本身的AIDL接口并实现此接口,而后向服务端提供本身的惟一标识和其对应的Binder对象。服务端只须要一个Service并提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对象,不一样的业务模块拿到所需的Binder对象后就能够进行远程方法的调用了。

为何选择Binder?

为何选用Binder,在讨论这个问题以前,咱们知道Android也是基于Linux内核,Linux现有的进程通讯手段有如下几种:

  • 管道:在建立时分配一个page大小的内存,缓存区大小比较有限;
  • 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通讯;
  • 共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操做系统没法实现,必须各进程利用同步工具解决;
  • 套接字:做为更通用的接口,传输效率低,主要用于不一样机器或跨网络的通讯;
  • 信号量:常做为一种锁机制,防止某进程正在访问共享资源时,其余进程也访问该资源。所以,主要做为进程间以及同一进程内不一样线程之间的同步手段。 不适用于信息交换,更适用于进程中断控制,好比非法内存访问,杀死某个进程等;

既然有现有的IPC方式,为何从新设计一套Binder机制呢。主要是出于以上三个方面的考量:

  • 一、效率:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。从Android进程架构角度分析:对于消息队列、Socket和管道来讲,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝,如图:

image

而对于Binder来讲,数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,节省了一次数据拷贝的过程,如图:

image

共享内存不须要拷贝,Binder的性能仅次于共享内存。

  • 二、稳定性:上面说到共享内存的性能优于Binder,那为何不采用共享内存呢,由于共享内存须要处理并发同步问题,容易出现死锁和资源竞争,稳定性较差。Socket虽然是基于C/S架构的,可是它主要是用于网络间的通讯且传输效率较低。Binder基于C/S架构 ,Server端与Client端相对独立,稳定性较好。
  • 三、安全性:传统Linux IPC的接收方没法得到对方进程可靠的UID/PID,从而没法鉴别对方身份;而Binder机制为每一个进程分配了UID/PID,且在Binder通讯时会根据UID/PID进行有效性检测。

Binder机制的做用和原理?

Linux系统将一个进程分为用户空间和内核空间。对于进程之间来讲,用户空间的数据不可共享,内核空间的数据可共享,为了保证安全性和独立性,一个进程不能直接操做或者访问另外一个进程,即Android的进程是相互独立、隔离的,这就须要跨进程之间的数据通讯方式。普通的跨进程通讯方式通常须要2次内存拷贝,以下图所示:

image

一次完整的 Binder IPC 通讯过程一般是这样:

  • 首先 Binder 驱动在内核空间建立一个数据接收缓存区。
  • 接着在内核空间开辟一块内核缓存区,创建内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系。
  • 发送方进程经过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,因为内核缓存区和接收进程的用户空间存在内存映射,所以也就至关于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通讯。

image

Binder框架中ServiceManager的做用?

Binder框架 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder驱动,其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。以下图所示:

image

  • Server&Client:服务器&客户端。在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通讯。
  • ServiceManager(如同DNS域名服务器)服务的管理者,将Binder名字转换为Client中对该Binder的引用,使得Client能够经过Binder名字得到Server中Binder实体的引用。
  • Binder驱动(如同路由器):负责进程之间binder通讯的创建,计数管理以及数据的传递交互等底层支持。

最后,结合Android跨进程通讯:图文详解 Binder机制 的总结图来综合理解一下:

image

Binder 的完整定义

  • 从进程间通讯的角度看,Binder 是一种进程间通讯的机制;
  • 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
  • 从 Client 进程的角度看,Binder 指的是 Binder 代理对象,是 Binder 实体对象的一个远程代理;
  • 从传输过程的角度看,Binder 是一个能够跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。

手写实现简化版AMS(AIDL实现)

与Binder相关的几个类的职责:

  • IBinder:跨进程通讯的Base接口,它声明了跨进程通讯须要实现的一系列抽象方法,实现了这个接口就说明能够进行跨进程通讯,Client和Server都要实现此接口。
  • IInterface:这也是一个Base接口,用来表示Server提供了哪些能力,是Client和Server通讯的协议。
  • Binder:提供Binder服务的本地对象的基类,它实现了IBinder接口,全部本地对象都要继承这个类。
  • BinderProxy:在Binder.java这个文件中还定义了一个BinderProxy类,这个类表示Binder代理对象它一样实现了IBinder接口,不过它的不少实现都交由native层处理。Client中拿到的其实是这个代理对象。
  • Stub:这个类在编译aidl文件后自动生成,它继承自Binder,表示它是一个Binder本地对象;它是一个抽象类,实现了IInterface接口,代表它的子类须要实现Server将要提供的具体能力(即aidl文件中声明的方法)。
  • Proxy:它实现了IInterface接口,说明它是Binder通讯过程的一部分;它实现了aidl中声明的方法,但最终仍是交由其中的mRemote成员来处理,说明它是一个代理对象,mRemote成员实际上就是BinderProxy。

aidl文件只是用来定义C/S交互的接口,Android在编译时会自动生成相应的Java类,生成的类中包含了Stub和Proxy静态内部类,用来封装数据转换的过程,实际使用时只关心具体的Java接口类便可。为何Stub和Proxy是静态内部类呢?这其实只是为了将三个类放在一个文件中,提升代码的聚合性。经过上面的分析,咱们其实彻底能够不经过aidl,手动编码来实现Binder的通讯,下面咱们经过编码来实现ActivityManagerService:

一、首先定义IActivityManager接口:

public interface IActivityManager extends IInterface {
    //binder描述符
    String DESCRIPTOR = "android.app.IActivityManager";
    //方法编号
    int TRANSACTION_startActivity = IBinder.FIRST_CALL_TRANSACTION + 0;
    //声明一个启动activity的方法,为了简化,这里只传入intent参数
    int startActivity(Intent intent) throws RemoteException;
}
复制代码

二、而后,实现ActivityManagerService侧的本地Binder对象基类:

// 名称随意,不必定叫Stub
public abstract class ActivityManagerNative extends Binder implements IActivityManager {

    public static IActivityManager asInterface(IBinder obj) {
        if (obj == null) {
            return null;
        }
        IActivityManager in = (IActivityManager) obj.queryLocalInterface(IActivityManager.DESCRIPTOR);
        if (in != null) {
            return in;
        }
        //代理对象,见下面的代码
        return new ActivityManagerProxy(obj);
    }

    @Override
    public IBinder asBinder() {
        return this;
    }

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {
            // 获取binder描述符
            case INTERFACE_TRANSACTION:
                reply.writeString(IActivityManager.DESCRIPTOR);
                return true;
            // 启动activity,从data中反序列化出intent参数后,直接调用子类startActivity方法启动activity。
            case IActivityManager.TRANSACTION_startActivity:
                data.enforceInterface(IActivityManager.DESCRIPTOR);
                Intent intent = Intent.CREATOR.createFromParcel(data);
                int result = this.startActivity(intent);
                reply.writeNoException();
                reply.writeInt(result);
                return true;
        }
        return super.onTransact(code, data, reply, flags);
    }
}
复制代码

三、接着,实现Client侧的代理对象:

public class ActivityManagerProxy implements IActivityManager {
    private IBinder mRemote;

    public ActivityManagerProxy(IBinder remote) {
        mRemote = remote;
    }

    @Override
    public IBinder asBinder() {
        return mRemote;
    }

    @Override
    public int startActivity(Intent intent) throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        int result;
        try {
            // 将intent参数序列化,写入data中
            intent.writeToParcel(data, 0);
            // 调用BinderProxy对象的transact方法,交由Binder驱动处理。
            mRemote.transact(IActivityManager.TRANSACTION_startActivity, data, reply, 0);
            reply.readException();
            // 等待server执行结束后,读取执行结果
            result = reply.readInt();
        } finally {
            data.recycle();
            reply.recycle();
        }
        return result;
    }
}
复制代码

四、最后,实现Binder本地对象(IActivityManager接口):

public class ActivityManagerService extends ActivityManagerNative {
    @Override
    public int startActivity(Intent intent) throws RemoteException {
        // 启动activity
        return 0;
    }
}
复制代码

简化版的ActivityManagerService到这里就已经实现了,剩下就是Client只须要获取到AMS的代理对象IActivityManager就能够通讯了。

简单讲讲 binder 驱动吧?

从 Java 层来看就像访问本地接口同样,客户端基于 BinderProxy 服务端基于 IBinder 对象,从 native 层来看来看客户端基于 BpBinder 到 ICPThreadState 到 binder 驱动,服务端由 binder 驱动唤醒 IPCThreadSate 到 BbBinder 。跨进程通讯的原理最终是要基于内核的,因此最会会涉及到 binder_open 、binder_mmap 和 binder_ioctl这三种系统调用。

跨进程传递大内存数据如何作?

binder 确定是不行的,由于映射的最大内存只有 1M-8K,能够采用 binder + 匿名共享内存的形式,像跨进程传递大的 bitmap 须要打开系统底层的 ashmem 机制。

请按顺序仔细阅读下列文章提高对Binder机制的理解程度:

写给 Android 应用工程师的 Binder 原理剖析

Binder学习指南

Binder设计与实现

老罗Binder机制分析系列或Android系统源代码情景分析Binder章节

五、Android系统启动流程是什么?(提示:init进程 -> Zygote进程 –> SystemServer进程 –> 各类系统服务 –> 应用进程)

Android系统启动的核心流程以下:

  • 一、启动电源以及系统启动:当电源按下时引导芯片从预约义的地方(固化在ROM)开始执行,加载引导程序BootLoader到RAM,而后执行。
  • 二、引导程序BootLoader:BootLoader是在Android系统开始运行前的一个小程序,主要用于把系统OS拉起来并运行。
  • 三、Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。
  • 四、init进程启动:初始化和启动属性服务,而且启动Zygote进程。
  • 五、Zygote进程启动:建立JVM并为其注册JNI方法,建立服务器端Socket,启动SystemServer进程。
  • 六、SystemServer进程启动:启动Binder线程池和SystemServiceManager,而且启动各类系统服务。
  • 七、Launcher启动:被SystemServer进程启动的AMS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。

须要更详细的分析请查看如下系列文章:

Android系统启动流程之init进程启动

Android系统启动流程之Zygote进程启动

Android系统启动流程之SystemServer进程启动

Android系统启动流程之Launcher进程启动

系统是怎么帮咱们启动找到桌面应用的?

经过意图,PMS 会解析全部 apk 的 AndroidManifest.xml ,若是解析过会存到 package.xml 中不会反复解析,PMS 有了它就能找到了。

六、启动一个程序,能够主界面点击图标进入,也能够从一个程序中跳转过去,两者有什么区别?

是由于启动程序(主界面也是一个app),发现了在这个程序中存在一个设置为的activity, 因此这个launcher会把icon提出来,放在主界面上。当用户点击icon的时候,发出一个Intent:

Intent intent = mActivity.getPackageManager().getLaunchIntentForPackage(packageName);
mActivity.startActivity(intent);   
复制代码

跳过去能够跳到任意容许的页面,如一个程序能够下载,那么真正下载的页面可能不是首页(也有多是首页),这时仍是构造一个Intent,startActivity。这个intent中的action可能有多种view,download都有可能。系统会根据第三方程序向系统注册的功能,为你的Intent选择能够打开的程序或者页面。因此惟一的一点 不一样的是从icon的点击启动的intent的action是相对单一的,从程序中跳转或者启动可能样式更多一些。本质是相同的。

七、AMS家族重要术语解释。

1.ActivityManagerServices,简称AMS,服务端对象,负责系统中全部Activity的生命周期。

2.ActivityThread,App的真正入口。当开启App以后,调用main()开始运行,开启消息循环队列,这就是传说的UI线程或者叫主线程。与ActivityManagerService一块儿完成Activity的管理工做。

3.ApplicationThread,用来实现ActivityManagerServie与ActivityThread之间的交互。在ActivityManagerSevice须要管理相关Application中的Activity的生命周期时,经过ApplicationThread的代理对象与ActivityThread通讯。

4.ApplicationThreadProxy,是ApplicationThread在服务器端的代理,负责和客户端的ApplicationThread通讯。AMS就是经过该代理与ActivityThread进行通讯的。

5.Instrumentation,每个应用程序只有一个Instrumetation对象,每一个Activity内都有一个对该对象的引用,Instrumentation能够理解为应用进程的管家,ActivityThread要建立或暂停某个Activity时,都须要经过Instrumentation来进行具体的操做。

6.ActivityStack,Activity在AMS的栈管理,用来记录经启动的Activity的前后关系,状态信息等。经过ActivtyStack决定是否须要启动新的进程。

7.ActivityRecord,ActivityStack的管理对象,每一个Acivity在AMS对应一个ActivityRecord,来记录Activity状态以及其余的管理信息。其实就是服务器端的Activit对象的映像。

8.TaskRecord,AMS抽象出来的一个“任务”的概念,是记录ActivityRecord的栈,一个“Task”包含若干个ActivityRecord。AMS用TaskRecord确保Activity启动和退出的顺序。若是你清楚Activity的4种launchMode,那么对这概念应该不陌生。

八、App启动流程(Activity的冷启动流程)。

点击应用图标后会去启动应用的Launcher Activity,若是Launcer Activity所在的进程没有建立,还会建立新进程,总体的流程就是一个Activity的启动流程。

Activity的启动流程图(放大可查看)以下所示:

image

整个流程涉及的主要角色有:

  • Instrumentation: 监控应用与系统相关的交互行为。
  • AMS:组件管理调度中心,什么都不干,可是什么都管。
  • ActivityStarter:Activity启动的控制器,处理Intent与Flag对Activity启动的影响,具体说来有:1 寻找符合启动条件的Activity,若是有多个,让用户选择;2 校验启动参数的合法性;3 返回int参数,表明Activity是否启动成功。
  • ActivityStackSupervisior:这个类的做用你从它的名字就能够看出来,它用来管理任务栈。
  • ActivityStack:用来管理任务栈里的Activity。
  • ActivityThread:最终干活的人,Activity、Service、BroadcastReceiver的启动、切换、调度等各类操做都在这个类里完成。

注:这里单独提一下ActivityStackSupervisior,这是高版本才有的类,它用来管理多个ActivityStack,早期的版本只有一个ActivityStack对应着手机屏幕,后来高版本支持多屏之后,就有了多个ActivityStack,因而就引入了ActivityStackSupervisior用来管理多个ActivityStack。

整个流程主要涉及四个进程:

  • 调用者进程,若是是在桌面启动应用就是Launcher应用进程。
  • ActivityManagerService等待所在的System Server进程,该进程主要运行着系统服务组件。
  • Zygote进程,该进程主要用来fork新进程。
  • 新启动的应用进程,该进程就是用来承载应用运行的进程了,它也是应用的主线程(新建立的进程就是主线程),处理组件生命周期、界面绘制等相关事情。

有了以上的理解,整个流程能够归纳以下:

  • 一、点击桌面应用图标,Launcher进程将启动Activity(MainActivity)的请求以Binder的方式发送给了AMS。
  • 二、AMS接收到启动请求后,交付ActivityStarter处理Intent和Flag等信息,而后再交给ActivityStackSupervisior/ActivityStack 处理Activity进栈相关流程。同时以Socket方式请求Zygote进程fork新进程。
  • 三、Zygote接收到新进程建立请求后fork出新进程。
  • 四、在新进程里建立ActivityThread对象,新建立的进程就是应用的主线程,在主线程里开启Looper消息循环,开始处理建立Activity。
  • 五、ActivityThread利用ClassLoader去加载Activity、建立Activity实例,并回调Activity的onCreate()方法,这样便完成了Activity的启动。

最后,再看看另外一幅启动流程图来加深理解:

image

九、ActivityThread工做原理。

十、说下四大组件的启动过程,四大组件的启动与销毁的方式。

广播发送和接收的原理了解吗?

  • 继承BroadcastReceiver,重写onReceive()方法。
  • 经过Binder机制向ActivityManagerService注册广播。
  • 经过Binder机制向ActivityMangerService发送广播。
  • ActivityManagerService查找符合相应条件的广播(IntentFilter/Permission)的BroadcastReceiver,将广播发送到BroadcastReceiver所在的消息队列中。
  • BroadcastReceiver所在消息队列拿到此广播后,回调它的onReceive()方法。

十一、AMS是如何管理Activity的?

十二、理解Window和WindowManager。

1.Window用于显示View和接收各类事件,Window有三种型:应用Window(每一个Activity对应一个Window)、子Widow(不能单独存在,附属于特定Window)、系统window(toast和状态栏)

2.Window分层级,应用Window在1-9九、子Window在1000-199九、系统Window在2000-2999.WindowManager提供了增改View的三个功能。

3.Window是个抽象概念:每个Window对应着一个ViewRootImpl,Window经过ViewRootImpl来和View创建联系,View是Window存在的实体,只能经过WindowManager来访问Window。

4.WindowManager的实现是WindowManagerImpl,其再委托WindowManagerGlobal来对Window进行操做,其中有四种List分别储存对应的View、ViewRootImpl、WindowManger.LayoutParams和正在被删除的View。

5.Window的实体是存在于远端的WindowMangerService,因此增删改Window在本端是修改上面的几个List而后经过ViewRootImpl重绘View,经过WindowSession(每Window个对应一个)在远端修改Window。

6.Activity建立Window:Activity会在attach()中建立Window并设置其回调(onAttachedToWindow()、dispatchTouchEvent()),Activity的Window是由Policy类建立PhoneWindow实现的。而后经过Activity#setContentView()调用PhoneWindow的setContentView。

1三、WMS是如何管理Window的?

1四、大致说清一个应用程序安装到手机上时发生了什么?

APK的安装流程以下所示:

image

复制APK到/data/app目录下,解压并扫描安装包。

资源管理器解析APK里的资源文件。

解析AndroidManifest文件,并在/data/data/目录下建立对应的应用数据目录。

而后对dex文件进行优化,并保存在dalvik-cache目录下。

将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。

安装完成后,发送广播。

1五、Android的打包流程?(即描述清点击 Android Studio 的 build 按钮后发生了什么?)apk里有哪些东西?签名算法的原理?

apk打包流程

Android的包文件APK分为两个部分:代码和资源,因此打包方面也分为资源打包和代码打包两个方面,下面就来分析资源和代码的编译打包原理。

APK总体的的打包流程以下图所示:

image

具体说来:

  • 经过AAPT工具进行资源文件(包括AndroidManifest.xml、布局文件、各类xml资源等)的打包,生成R.java文件。
  • 经过AIDL工具处理AIDL文件,生成相应的Java文件。
  • 经过Java Compiler编译R.java、Java接口文件、Java源文件,生成.class文件。
  • 经过dex命令,将.class文件和第三方库中的.class文件处理生成classes.dex,该过程主要完成Java字节码转换成Dalvik字节码,压缩常量池以及清除冗余信息等工做。
  • 经过ApkBuilder工具将资源文件、DEX文件打包生成APK文件。
  • 经过Jarsigner工具,利用KeyStore对生成的APK文件进行签名。
  • 若是是正式版的APK,还会利用ZipAlign工具进行对齐处理,对齐的过程就是将APK文件中全部的资源文件距离文件的起始距位置都偏移4字节的整数倍,这样经过内存映射访问APK文件的速度会更快,而且会减小其在设备上运行时的内存占用。

apk组成

  • dex:最终生成的Dalvik字节码。
  • res:存放资源文件的目录。
  • asserts:额外创建的资源文件夹。
  • lib:若是存在的话,存放的是ndk编出来的so库。
  • META-INF:存放签名信息

MANIFEST.MF(清单文件):其中每个资源文件都有一个SHA-256-Digest签名,MANIFEST.MF文件的SHA256(SHA1)并base64编码的结果即为CERT.SF中的SHA256-Digest-Manifest值。

CERT.SF(待签名文件):除了开头处定义的SHA256(SHA1)-Digest-Manifest值,后面几项的值是对MANIFEST.MF文件中的每项再次SHA256并base64编码后的值。

CERT.RSA(签名结果文件):其中包含了公钥、加密算法等信息。首先对前一步生成的MANIFEST.MF使用了SHA256(SHA1)-RSA算法,用开发者私钥签名,而后在安装时使用公钥解密。最后,将其与未加密的摘要信息(MANIFEST.MF文件)进行对比,若是相符,则代表内容没有被修改。

  • androidManifest:程序的全局清单配置文件。
  • resources.arsc:编译后的二进制资源文件。

签名算法的原理

为何要签名?
  • 确保Apk来源的真实性。
  • 确保Apk没有被第三方篡改。
什么是签名?

在Apk中写入一个“指纹”。指纹写入之后,Apk中有任何修改,都会致使这个指纹无效,Android系统在安装Apk进行签名校验时就会不经过,从而保证了安全性。

数字摘要

对一个任意长度的数据,经过一个Hash算法计算后,均可以获得一个固定长度的二进制数据,这个数据就称为“摘要”。

补充:

  • 散列算法的基础原理:将数据(如一段文字)运算变为另外一固定长度值。
  • SHA-1:在密码学中,SHA-1(安全散列算法1)是一种加密散列函数,它接受输入并产生一个160 位(20 字节)散列值,称为消息摘要 。
  • MD5:MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被普遍使用的密码散列函数,能够产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。
  • SHA-2:名称来自于安全散列算法2(英语:Secure Hash Algorithm 2)的缩写,一种密码散列函数算法标准,其下又可再分为六个不一样的算法标准,包括了:SHA-22四、SHA-25六、SHA-38四、SHA-5十二、SHA-512/22四、SHA-512/256。

特征:

  • 惟一性
  • 固定长度:比较经常使用的Hash算法有MD5和SHA1,MD5的长度是128拉,SHA1的长度是160位。
  • 不可逆性
签名和校验的主要过程

签名就是在摘要的基础上再进行一次加密,对摘要加密后的数据就能够看成数字签名。

签名过程:
  • 一、计算摘要:经过Hash算法提取出原始数据的摘要。
  • 二、计算签名:再经过基于密钥(私钥)的非对称加密算法对提取出的摘要进行加密,加密后的数据就是签名信息。
  • 三、写入签名:将签名信息写入原始数据的签名区块内。
校验过程:
  • 一、首先用一样的Hash算法从接收到的数据中提取出摘要。
  • 二、解密签名:使用发送方的公钥对数字签名进行解密,解密出原始摘要。
  • 三、比较摘要:若是解密后的数据和提取的摘要一致,则校验经过;若是数据被第三方篡改过,解密后的数据和摘要将会不一致,则校验不经过。
数字证书

如何保证公钥的可靠性呢?答案是数字证书,数字证书是身份认证机构(Certificate Authority)颁发的,包含了如下信息:

  • 证书颁发机构
  • 证书颁发机构签名
  • 证书绑定的服务器域名
  • 证书版本、有效期
  • 签名使用的加密算法(非对称算法,如RSA)
  • 公钥等

接收方收到消息后,先向CA验证证书的合法性,再进行签名校验。

注意:Apk的证书一般是自签名的,也就是由开发者本身制做,没有向CA机构申请。Android在安装Apk时并无校验证书自己的合法性,只是从证书中提取公钥和加密算法,这也正是对第三方Apk从新签名后,还可以继续在没有安装这个Apk的系统中继续安装的缘由。

keystore和证书格式

keystore文件中包含了私钥、公钥和数字证书。根据编码不一样,keystore文件分为不少种,Android使用的是Java标准keystore格式JKS(Java Key Storage),因此经过Android Studio导出的keystore文件是以.jks结尾的。

keystore使用的证书标准是X.509,X.509标准也有多种编码格式,经常使用的有两种:pem(Privacy Enhanced Mail)和der(Distinguished Encoding Rules)。jks使用的是der格式,Android也支持直接使用pem格式的证书进行签名。

两种证书编码格式的区别:

  • DER(Distinguished Encoding Rules)

二进制格式,全部类型的证书和私钥均可以存储为der格式。

  • PEM(Privacy Enhanced Mail)

base64编码,内容以-----BEGIN xxx----- 开头,以-----END xxx----- 结尾。

jarsigner和apksigner的区别

Android提供了两种对Apk的签名方式,一种是基于JAR的签名方式,另外一种是基于Apk的签名方式,它们的主要区别在于使用的签名文件不同:jarsigner使用keystore文件进行签名;apksigner除了支持使用keystore文件进行签名外,还支持直接指定pem证书文件和私钥进行签名。

在签名时,除了要指定keystore文件和密码外,也要指定alias和key的密码,这是为何呢?

keystore是一个密钥库,也就是说它能够存储多对密钥和证书,keystore的密码是用于保护keystore自己的,一对密钥和证书是经过alias来区分的。因此jarsigner是支持使用多个证书对Apk进行签名的,apksigner也一样支持。

Android Apk V1 签名原理
  • 一、解析出 CERT.RSA 文件中的证书、公钥,解密 CERT.RSA 中的加密数据。
  • 二、解密结果和 CERT.SF 的指纹进行对比,保证 CERT.SF 没有被篡改。
  • 三、而 CERT.SF 中的内容再和 MANIFEST.MF 指纹对比,保证 MANIFEST.MF 文件没有被篡改。
  • 四、MANIFEST.MF 中的内容和 APK 全部文件指纹逐一对比,保证 APK 没有被篡改。

1六、说下安卓虚拟机和java虚拟机的原理和不一样点?(JVM、Davilk、ART三者的原理和区别)

JVM 和Dalvik虚拟机的区别

JVM:.java -> javac -> .class -> jar -> .jar

架构: 堆和栈的架构.

DVM:.java -> javac -> .class -> dx.bat -> .dex

架构: 寄存器(cpu上的一块高速缓存)

Android2个虚拟机的区别(一个5.0以前,一个5.0以后)

什么是Dalvik:Dalvik是Google公司本身设计用于Android平台的Java虚拟机。Dalvik虚拟机是Google等厂商合做开发的Android移动设备平台的核心组成部分之一,它能够支持已转换为.dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik通过优化,容许在有限的内存中同时运行多个虚拟机的实例,而且每个Dalvik应用做为独立的Linux进程执行。独立的进程能够防止在虚拟机崩溃的时候全部程序都被关闭。

什么是ART:Android操做系统已经成熟,Google的Android团队开始将注意力转向一些底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快执行效率更高更省电的替代ART运行时。ART表明Android Runtime,其处理应用程序执行的方式彻底不一样于Dalvik,Dalvik是依靠一个Just-In-Time(JIT)编译器去解释字节码。开发者编译后的应用代码须要经过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不一样硬件和架构上运行。ART则彻底改变了这套作法,在应用安装的时候就预编译字节码为机器语言,这一机制叫Ahead-Of-Time(AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

ART优势:

  • 系统性能的显著提高。
  • 应用启动更快、运行更快、体验更流畅、触感反馈更及时。
  • 更长的电池续航能力。
  • 支持更低的硬件。

ART缺点:

  • 更大的存储空间占用,可能会增长10%-20%。
  • 更长的应用安装时间。

ART和Davlik中垃圾回收的区别?

1七、安卓采用自动垃圾回收机制,请说下安卓内存管理的原理?

开放性问题:如何设计垃圾回收算法?

1八、Android中App是如何沙箱化的,为什么要这么作?

1九、一个图片在app中调用R.id后是如何找到的

20、JNI

Java调用C++

  • 在Java中声明Native方法(即须要调用的本地方法)
  • 编译上述 Java源文件javac(获得 .class文件) 3。 经过 javah 命令导出JNI的头文件(.h文件)
  • 使用 Java须要交互的本地代码 实如今 Java中声明的Native方法
  • 编译.so库文件
  • 经过Java命令执行 Java程序,最终实现Java调用本地代码

C++调用Java

  • 从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象。

  • 获取类的默认构造方法ID。

  • 查找实例方法的ID。

  • 建立该类的实例。

  • 调用对象的实例方法。

    JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod  
    (JNIEnv *env, jclass cls)  
    {  
      jclass clazz = NULL;  
      jobject jobj = NULL;  
      jmethodID mid_construct = NULL;  
      jmethodID mid_instance = NULL;  
      jstring str_arg = NULL;  
      // 一、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象  
      clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");  
      if (clazz == NULL) {  
          printf("找不到'com.study.jnilearn.ClassMethod'这个类");  
          return;  
      }  
      
      // 二、获取类的默认构造方法ID  
      mid_construct = (*env)->GetMethodID(env,clazz, "<init>","()V");  
      if (mid_construct == NULL) {  
          printf("找不到默认的构造方法");  
          return;  
      }  
    
      // 三、查找实例方法的ID  
      mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");  
      if (mid_instance == NULL) {  
    
          return;  
      }  
    
      // 四、建立该类的实例  
      jobj = (*env)->NewObject(env,clazz,mid_construct);  
      if (jobj == NULL) {  
          printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法");  
          return;  
      }  
    
      // 五、调用对象的实例方法  
      str_arg = (*env)->NewStringUTF(env,"我是实例方法");  
      (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);  
    
      // 删除局部引用  
      (*env)->DeleteLocalRef(env,clazz);  
      (*env)->DeleteLocalRef(env,jobj);  
      (*env)->DeleteLocalRef(env,str_arg);  
    }  
    复制代码

如何在jni中注册native函数,有几种注册方式?

so 的加载流程是怎样的,生命周期是怎样的?

这个要从 java 层去看源码分析,是从 ClassLoader 的 PathList 中去找到目标路径加载的,同时 so 是经过 mmap 加载映射到虚拟空间的。生命周期加载库和卸载库时分别调用 JNI_OnLoad 和 JNI_OnUnload() 方法。

2一、请介绍一下NDK?

赞扬

若是这个库对您有很大帮助,您愿意支持这个项目的进一步开发和这个项目的持续维护。你能够扫描下面的二维码,让我喝一杯咖啡或啤酒。很是感谢您的捐赠。谢谢!


Contanct Me

● 微信 && 微信群:

欢迎关注个人微信:bcce5360。因为微信群人数太多没法生成群邀二维码,因此麻烦你们想进微信群的朋友们,加我微信拉你进群(PS:微信群的学习氛围与各项福利将会超乎你的想象)

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~

About me

很感谢您阅读这篇文章,但愿您能将它分享给您的朋友或技术群,这对我意义重大。

但愿咱们能成为朋友,在 Github掘金上一块儿分享知识。