🔥 A awesome android expert interview questions and answers(continuous updating ...)java
从几十份顶级面试仓库和300多篇高质量面经中总结出一份全面成体系化的Android高级面试题集。android
欢迎来到2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂的Android高级篇(上)。git
随着项目的逐渐成熟,用户基数逐渐增多,DAU持续升高,咱们遇到了不少稳定性方面的问题,对于咱们技术同窗遇到了不少的挑战,用户常用咱们的App卡顿或者是功能不可用,所以咱们就针对稳定性开启了专项的优化,咱们主要优化了三项:github
经过这三方面的优化咱们搭建了移动端的高可用平台。同时,也作了不少的措施来让App真正地实现了高可用。面试
咱们针对启动速度,内存、布局加载、卡顿、瘦身、流量、电量等多个方面作了多维的优化。算法
咱们的优化主要分为了两个层次,即线上和线下,针对于线下呢,咱们侧重于发现问题,直接解决,将问题尽量在上线以前解决为目的。而真正到了线上呢,咱们最主要的目的就是为了监控,对于各个性能纬度的监控呢,可让咱们尽量早地获取到异常状况的报警。编程
同时呢,对于线上最严重的性能问题性问题:Crash,咱们作了专项的优化,不只优化了Crash的具体指标,并且也尽量地获取了Crash发生时的详细信息,结合后端的聚合、报警等功能,便于咱们快速地定位问题。json
移动端业务高可用它侧重于用户功能完整可用,主要是为了解决一些线上一些异常状况致使用户他虽然没有崩溃,也没有性能问题,可是呢,只是单纯的功能不可用的状况,咱们须要对项目的主流程、核心路径进行埋点监控,来计算每一步它真实的转换率是多少,同时呢,还须要知道在每一步到底发生了多少异常。这样咱们就知道了全部业务流程的转换率以及相应界面的转换率,有了大盘的数据呢,咱们就知道了,若是转换率或者是某些监控的成功率低于某个值,那颇有可能就是出现了线上异常,结合了相应的报警功能,咱们就不须要等用户来反馈了,这个就是业务稳定性保障的基础。canvas
同时呢,对于一些特殊状况,好比说,开发过程中或代码中出现了一些catch代码块,捕获住了异常,让程序不崩溃,这实际上是不合理的,程序虽然没有崩溃,当时程序的功能已经变得不可用,因此呢,这些被catch的异常咱们也须要上报上来,这样咱们才能知道用户到底出现了什么问题而致使的异常。此外,线上还有一些单点问题,好比说用户点击登陆一直进不去,这种就属于单点问题,其实咱们是没法找出其和其它问题的共性之处的,因此呢,咱们就必需要找到它对应的详细信息。小程序
最后,若是发生了异常状况,咱们还采起了一系列措施进行快速止损。(=>4)
首先,须要让App具有一些高级的能力,咱们对于任何要上线的新功能,要加上一个功能的开关,经过配置中心下发的开关呢,来决定是否要显示新功能的入口。若是有异常状况,能够紧急关闭新功能的入口,那就可让这个App处于可控的状态了。
而后,咱们须要给App设立路由跳转,全部的界面跳转都须要经过路由来分发,若是咱们匹配到须要跳转到有bug的这样一个新功能时,那咱们就不跳转了,或者是跳转到统一的异常正处理中的界面。若是这两种方式都不能够,那就能够考虑经过热修复的方式来动态修复,目前热修复的方案其实已经比较成熟了,咱们彻底能够低成本地在咱们的项目中添加热修复的能力,固然,若是有些功能是由RN或WeeX来实现就更好了,那就能够经过更新资源包的方式来实现动态更新。而这些若是都不能够的话呢,那就能够考虑本身去给应用加上一个自主修复的能力,若是App启动屡次的话,那就能够考虑清空全部的缓存数据,将App重置到安装的状态,到了最严重的等级呢,能够阻塞主线程,此时必定要等App热修复成功以后才容许用户进入。
须要更全面更深刻的理解请查看深刻探索Android稳定性优化
在某一个版本以后呢,咱们会发现这个启动速度变得特别慢,同时用户给咱们的反馈也愈来愈多,因此,咱们开始考虑对应用的启动速度来进行优化。而后,咱们就对启动的代码进行了代码层面的梳理,咱们发现应用的启动流程已经很是复杂,接着,咱们经过一系列的工具来确认是否在主线程中执行了太多的耗时操做。
咱们通过了细查代码以后,发现应用主线程中的任务太多,咱们就想了一个方案去针对性地解决,也就是进行异步初始化。(引导=>第2题) 而后,咱们还发现了另一个问题,也能够进行针对性的优化,就是在咱们的初始化代码当中有些的优先级并非那么高,它能够不放在Application的onCreate中执行,而彻底能够放在以后延迟执行的,由于咱们对这些代码进行了延迟初始化,最后,咱们还结合了idealHandler作了一个更优的延迟初始化的方案,利用它能够在主线程的空闲时间进行初始化,以减小启动耗时致使的卡顿现象。作完这些以后,咱们的启动速度就变得很快了。
最后,我简单说下咱们是怎么长期来保持启动优化的效果的。首先,咱们作了咱们的启动器,而且结合了咱们的CI,在线上加上了不少方面的监控。(引导=> 第4题)
咱们最初是采用的普通的一个异步的方案,即new Thread + 设置线程优先级为后台线程的方式在Application的onCreate方法中进行异步初始化,后来,咱们使用了线程池、IntentService的方式,可是,在咱们应用的演进过程中,发现代码会变得不够优雅,而且有些场景很是很差处理,好比说多个初始化任务直接的依赖关系,好比说某一个初始化任务须要在某一个特定的生命周期中初始化完成,这些都是使用线程池、IntentService没法实现的。因此说,咱们就开始思考一个新的解决方案,它可以完美地解决咱们刚刚所遇到的这些问题。
这个方案就是咱们目前所使用的启动器,在启动器的概念中,咱们将每个初始化代码抽象成了一个Task,而后,对它们进行了一个排序,根据它们之间的依赖关系排了一个有向无环图,接着,使用一个异步队列进行执行,而且这个异步队列它和CPU的核心数是强烈相关的,它可以最大程度地保证咱们的主线程和别的线程都可以执行咱们的任务,也就是你们几乎均可以同时完成。
首先,在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的频率在启动的时候去进行一个暴力的提高。
这种问题其实咱们以前也遇到过,这的确很是难以解决。可是,咱们后面对此进行了反复的思考与尝试,终于找到了一个比较好的解决方式。
首先,咱们使用了启动器去管理每个初始化任务,而且启动器中每个任务的执行都是被其自动进行分配的,也就是说这些自动分配的task咱们会尽可能保证它会平均分配在咱们每个线程当中的,这和咱们普通的异步是不同的,它能够很好地缓解咱们应用的启动变慢。
其次,咱们还结合了CI,好比说,咱们如今限制了一些类,如Application,若是有人修改了它,咱们不会让这部分代码合并到主干分支或者是修改以后会有一些内部的工具如邮件的形式发送到我,而后,我就会和他确认他加的这些代码究竟是耗时多少,可否异步初始化,不能异步的话就考虑延迟初始化,若是初始化时间太长,则能够考虑是否能进行懒加载,等用到的时候再去使用等等。
而后,咱们会将问题尽量地暴露在上线以前。同时,咱们真正已经到了线上的一个环境下时,咱们进行了监控的一个完善,咱们不只是监控了App的整个的启动时间,同时呢,咱们也将每个生命周期都进行了一个监控。好比说Application的onCreate与onAttachBaseContext方法的耗时,以及这两个生命周期之间间隔的时间,咱们都进行了一个监控,若是说下一次咱们发现了这个启动速度变慢了,咱们就能够去查找究竟是哪个环节变慢了,咱们会和之前的版本进行对比,对比完成以后呢,咱们就能够来找这一段新加的代码。
须要更全面更深刻的理解请查看深刻探索Android启动速度优化
一、分析现状、确认问题
咱们发现咱们的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总体卡顿。
避免发生内存抖动的几点建议:
须要更全面更深刻的理解请查看Android性能优化以内存优化、深刻探索Android内存优化
我在作布局优化的过程当中,用到了不少的工具,可是每个工具都有它不一样的使用场景,不一样的场景应该使用不一样的工具。下面我从线上和线下两个角度来进行分析。
好比说,我要统计线上的FPS,我使用的就是Choreographer这个类,它具备如下特性:
同时,在线下,若是要去优化布局加载带来的时间消耗,那就须要检测每个布局的耗时,对此我使用的是AOP的方式,它没有侵入性,同时也不须要别的开发同窗进行接入,就能够方便地获取每个布局加载的耗时。若是还要更细粒度地去检测每个控件的加载耗时,那么就须要使用LayoutInflaterCompat.setFactory2这个方法去进行Hook。
此外,我还使用了LayoutInspector和Systrace这两个工具,Systrace能够很方便地看到每帧的具体耗时以及这一帧在布局当中它真正作了什么。而LayoutInspector能够很方便地看到每个界面的布局层级,帮助咱们对层级进行优化。
分析完布局的加载流程以后,咱们发现有以下四点可能会致使布局卡顿:
对此,咱们的优化方式有以下几种:
从项目的初期到壮大期,最后再到成熟期,每个阶段都针对卡顿优化作了不一样的处理。各个阶段所作的事情以下所示:
我作卡顿优化也是经历了一些阶段,最初咱们的项目当中的一些模块出现了卡顿以后,我是经过系统工具进行了定位,我使用了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率等常规指标以外呢,咱们还计算了页面的秒开率、生命周期的执行时间等等。并且,在卡顿发生的时刻,咱们也尽量多地保存下来了当前的一个场景信息,这为咱们以后解决或者复现这个卡顿留下了依据。
须要更全面更深刻的理解请查看Android性能优化之绘制优化、深刻探索Android布局优化(上)、深刻探索Android布局优化(下)
一、链接复用:节省链接创建时间,如开启 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 服务器,有效的防止了域名劫持,提升域名解析的效率。
这是由于在客户端中,加载H5页面以前,须要先初始化WebView,在WebView彻底初始化完成以前,后续的界面加载过程都是被阻塞的。
优化手段围绕着如下两个点进行:
所以常见的方法是:
除此以外还有一些其余的优化手段:
为了加速你的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的层级结构来计算大小。
Error、OOM,StackOverFlowError、Runtime,好比说空指针异常
解决的办法:
传统日志打印有两个性能问题,一个是反复操做文件描述符表,一个是反复进入内核态。因此须要使用mmap的方式去直接读写内存。
Android 是一种基于 Linux 的开放源代码软件栈,为普遍的设备和机型而建立。下图所示为 Android 平台的五大组件:
1.应用程序
Android 随附一套用于电子邮件、短信、日历、互联网浏览和联系人等的核心应用。平台随附的应用与用户能够选择安装的应用同样,没有特殊状态。所以第三方应用可成为用户的默认网络浏览器、短信 Messenger 甚至默认键盘(有一些例外,例如系统的“设置”应用)。
系统应用可用做用户的应用,以及提供开发者可从其本身的应用访问的主要功能。例如,若是您的应用要发短信,您无需本身构建该功能,能够改成调用已安装的短信应用向您指定的接收者发送消息。
二、Java API 框架
您可经过以 Java 语言编写的 API 使用 Android OS 的整个功能集。这些 API 造成建立 Android 应用所需的构建块,它们可简化核心模块化系统组件和服务的重复使用,包括如下组件和服务:
开发者能够彻底访问 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 的部分主要功能包括:
在 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 利用主要安全功能,而且容许设备制造商为著名的内核开发硬件驱动程序。
一个Activity包含了一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindow将DecorView做为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域:一个是TitleView,另外一个是ContentView,而咱们平时所写的就是展现在ContentView中的。
触摸事件对应的是MotionEvent类,事件的类型主要有以下三种:
View事件分发本质就是对MotionEvent事件分发的过程。即当一个MotionEvent发生后,系统将这个点击事件传递到一个具体的View上。
事件分发过程由三个方法共同完成:
dispatchTouchEvent:方法返回值为true表示事件被当前视图消费掉;返回为super.dispatchTouchEvent表示继续分发该事件,返回为false表示交给父类的onTouchEvent处理。
onInterceptTouchEvent:方法返回值为true表示拦截这个事件并交由自身的onTouchEvent方法进行消费;返回false表示不拦截,须要继续传递给子视图。若是return super.onInterceptTouchEvent(ev), 事件拦截分两种状况:
注意:通常的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默认不拦截, 而 ScrollView、ListView等ViewGroup则可能拦截,得看具体状况。
onTouchEvent:方法返回值为true表示当前视图能够处理对应的事件;返回值为false表示当前视图不处理这个事件,它会被传递给父视图的onTouchEvent方法进行处理。若是return super.onTouchEvent(ev),事件处理分为两种状况:
注意:在Android系统中,拥有事件传递处理能力的类有如下三种:
三个方法的关系用伪代码表示以下:
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事件除外。
记住这个图的传递顺序,面试的时候可以画出来,就很详细了:
重写子类的requestDisallowInterceptTouchEvent()方法返回true就不会执行父类的onInterceptTouchEvent(),便可将点击事件传到下面的View。
常见开发中事件冲突的有ScrollView与RecyclerView的滑动冲突、RecyclerView内嵌同时滑动同一方向。
滑动冲突的处理规则:
滑动冲突的实现方法:
绘制会从根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每一个View控件负责绘制本身,而ViewGroup还须要负责通知本身的子View进行绘制操做。
MeasureSpec表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来讲明应该如何测量这个View。它由三种测量模式,以下:
MeasureSpec经过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操做,其提供了打包和解包的方法,打包方法为makeMeasureSpec,解包方法为getMode和getSize。
普通View的MeasureSpec的建立规则以下:
对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。
若是View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值能够为0;若是View设置了背景,则返回android:minWidth和背景的最小宽度这二者中的最大值。
直接继承View的控件须要重写onMeasure方法并设置wrap_content时的自身大小,不然在布局中使用wrap_content就至关于使用match_parent。此时,能够在wrap_content的状况下(对应MeasureSpec.AT_MOST)指定内部宽/高(mWidth和mHeight)。
系统会遍历子元素并对每一个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,而且系统会经过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增长,增长的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。
因为View的measure过程和Activity的生命周期方法不是同步执行的,若是View尚未测量完毕,那么得到的宽/高就是0。因此在onCreate、onStart、onResume中均没法正确获得某个View的宽高信息。解决方式以下:
首先,会经过setFrame方法来设定View的四个顶点的位置,即View在父容器中的位置。而后,会执行到onLayout空方法,子类若是是ViewGroup类型,则重写这个方法,实现ViewGroup中全部View控件布局流程。
其中会遍历调用每一个子View的setChildFrame方法为子元素肯定对应的位置。其中的childTop会逐渐增大,意味着后面的子元素会被放置在靠下的位置。
注意:在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高造成于View的measure过程,而最终宽/高造成于View的layout过程,即二者的赋值时机不一样,测量宽/高的赋值时机稍微早一些。在一些特殊的状况下则二者不相等:
绘制基本上能够分为六个步骤:
若是一个View不须要绘制任何内容,那么设置这个标记位为true之后,系统会进行相应的优化。
requestLayout()方法 :会致使调用 measure()过程 和 layout()过程,将会根据标志位判断是否须要ondraw。
onLayout()方法:若是该View是ViewGroup对象,须要实现该方法,对每一个子视图进行布局。
onDraw()方法:绘制视图自己 (每一个View都须要重载该方法,ViewGroup不须要实现该方法)。
drawChild():去从新回调每一个子视图的draw()方法。
invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用须要配合handler;而postInvalidate()可在子线程中直接调用。
在AndroidManifest中给四大组件指定属性android:process开启多进程模式,在内存容许的条件下能够开启N个进程。
全部运行在不一样进程的四大组件(Activity、Service、Receiver、ContentProvider)共享数据都会失败,这是因为Android为每一个应用分配了独立的虚拟机,不一样的虚拟机在内存分配上有不一样的地址空间,这会致使在不一样的虚拟机中访问同一个类的对象会产生多份副本。好比经常使用例子(经过开启多进程获取更大内存空间、两个或者多个应用之间共享数据、微信全家桶)。
通常来讲,使用多进程通讯会形成以下几方面的问题:
AIDL(Android Interface Definition Language,Android接口定义语言):若是在一个进程中要调用另外一个进程中对象的方法,可以使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,经过它客户端能够实现间接调用服务端对象的方法。
AIDL的本质是系统提供了一套可快速实现Binder的工具。关键类和方法:
当有多个业务模块都须要AIDL来进行IPC,此时须要为每一个模块建立特定的aidl文件,那么相应的Service就会不少。必然会出现系统资源耗费严重、应用过分重量级的问题。解决办法是创建Binder链接池,即将每一个业务模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复建立Service。
工做原理:每一个业务模块建立本身的AIDL接口并实现此接口,而后向服务端提供本身的惟一标识和其对应的Binder对象。服务端只须要一个Service并提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对象,不一样的业务模块拿到所需的Binder对象后就能够进行远程方法的调用了。
为何选用Binder,在讨论这个问题以前,咱们知道Android也是基于Linux内核,Linux现有的进程通讯手段有如下几种:
既然有现有的IPC方式,为何从新设计一套Binder机制呢。主要是出于以上三个方面的考量:
而对于Binder来讲,数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,节省了一次数据拷贝的过程,如图:
共享内存不须要拷贝,Binder的性能仅次于共享内存。
Linux系统将一个进程分为用户空间和内核空间。对于进程之间来讲,用户空间的数据不可共享,内核空间的数据可共享,为了保证安全性和独立性,一个进程不能直接操做或者访问另外一个进程,即Android的进程是相互独立、隔离的,这就须要跨进程之间的数据通讯方式。普通的跨进程通讯方式通常须要2次内存拷贝,以下图所示:
一次完整的 Binder IPC 通讯过程一般是这样:
Binder框架 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder驱动,其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。以下图所示:
最后,结合Android跨进程通讯:图文详解 Binder机制 的总结图来综合理解一下:
与Binder相关的几个类的职责:
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就能够通讯了。
从 Java 层来看就像访问本地接口同样,客户端基于 BinderProxy 服务端基于 IBinder 对象,从 native 层来看来看客户端基于 BpBinder 到 ICPThreadState 到 binder 驱动,服务端由 binder 驱动唤醒 IPCThreadSate 到 BbBinder 。跨进程通讯的原理最终是要基于内核的,因此最会会涉及到 binder_open 、binder_mmap 和 binder_ioctl这三种系统调用。
binder 确定是不行的,由于映射的最大内存只有 1M-8K,能够采用 binder + 匿名共享内存的形式,像跨进程传递大的 bitmap 须要打开系统底层的 ashmem 机制。
请按顺序仔细阅读下列文章提高对Binder机制的理解程度:
老罗Binder机制分析系列或Android系统源代码情景分析Binder章节
Android系统启动的核心流程以下:
Android系统启动流程之SystemServer进程启动
经过意图,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是相对单一的,从程序中跳转或者启动可能样式更多一些。本质是相同的。
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,那么对这概念应该不陌生。
点击应用图标后会去启动应用的Launcher Activity,若是Launcer Activity所在的进程没有建立,还会建立新进程,总体的流程就是一个Activity的启动流程。
Activity的启动流程图(放大可查看)以下所示:
整个流程涉及的主要角色有:
注:这里单独提一下ActivityStackSupervisior,这是高版本才有的类,它用来管理多个ActivityStack,早期的版本只有一个ActivityStack对应着手机屏幕,后来高版本支持多屏之后,就有了多个ActivityStack,因而就引入了ActivityStackSupervisior用来管理多个ActivityStack。
整个流程主要涉及四个进程:
有了以上的理解,整个流程能够归纳以下:
最后,再看看另外一幅启动流程图来加深理解:
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。
APK的安装流程以下所示:
复制APK到/data/app目录下,解压并扫描安装包。
资源管理器解析APK里的资源文件。
解析AndroidManifest文件,并在/data/data/目录下建立对应的应用数据目录。
而后对dex文件进行优化,并保存在dalvik-cache目录下。
将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
安装完成后,发送广播。
Android的包文件APK分为两个部分:代码和资源,因此打包方面也分为资源打包和代码打包两个方面,下面就来分析资源和代码的编译打包原理。
APK总体的的打包流程以下图所示:
具体说来:
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文件)进行对比,若是相符,则代表内容没有被修改。
在Apk中写入一个“指纹”。指纹写入之后,Apk中有任何修改,都会致使这个指纹无效,Android系统在安装Apk进行签名校验时就会不经过,从而保证了安全性。
对一个任意长度的数据,经过一个Hash算法计算后,均可以获得一个固定长度的二进制数据,这个数据就称为“摘要”。
补充:
特征:
签名就是在摘要的基础上再进行一次加密,对摘要加密后的数据就能够看成数字签名。
如何保证公钥的可靠性呢?答案是数字证书,数字证书是身份认证机构(Certificate Authority)颁发的,包含了如下信息:
接收方收到消息后,先向CA验证证书的合法性,再进行签名校验。
注意:Apk的证书一般是自签名的,也就是由开发者本身制做,没有向CA机构申请。Android在安装Apk时并无校验证书自己的合法性,只是从证书中提取公钥和加密算法,这也正是对第三方Apk从新签名后,还可以继续在没有安装这个Apk的系统中继续安装的缘由。
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格式。
base64编码,内容以-----BEGIN xxx----- 开头,以-----END xxx----- 结尾。
Android提供了两种对Apk的签名方式,一种是基于JAR的签名方式,另外一种是基于Apk的签名方式,它们的主要区别在于使用的签名文件不同:jarsigner使用keystore文件进行签名;apksigner除了支持使用keystore文件进行签名外,还支持直接指定pem证书文件和私钥进行签名。
keystore是一个密钥库,也就是说它能够存储多对密钥和证书,keystore的密码是用于保护keystore自己的,一对密钥和证书是经过alias来区分的。因此jarsigner是支持使用多个证书对Apk进行签名的,apksigner也一样支持。
JVM:.java -> javac -> .class -> jar -> .jar
架构: 堆和栈的架构.
DVM:.java -> javac -> .class -> dx.bat -> .dex
架构: 寄存器(cpu上的一块高速缓存)
什么是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缺点:
从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);
}
复制代码
这个要从 java 层去看源码分析,是从 ClassLoader 的 PathList 中去找到目标路径加载的,同时 so 是经过 mmap 加载映射到虚拟空间的。生命周期加载库和卸载库时分别调用 JNI_OnLoad 和 JNI_OnUnload() 方法。
若是这个库对您有很大帮助,您愿意支持这个项目的进一步开发和这个项目的持续维护。你能够扫描下面的二维码,让我喝一杯咖啡或啤酒。很是感谢您的捐赠。谢谢!
欢迎关注个人微信:
bcce5360
。因为微信群人数太多没法生成群邀二维码,因此麻烦你们想进微信群的朋友们,加我微信拉你进群(PS:微信群的学习氛围与各项福利将会超乎你的想象)。
2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~