Android 一些基础知识整理(一)

1 在子线程可以刷新 UI 吗
 

不行,在子线程中报异常ViewRootImpl$CalledFromWErongThreadException

提示 Only the original thread that created a view hierarchy can touch its views 中文翻译为只有在创建视图层次的原始线程才能更改其视图。

Android 一些基础知识整理(一)_第1张图片

通过源码得知ViewRootImpI 是通过 Activity 创建的,并不是由 ViewGroup 创建,那么View 的刷新动作必须要在创建 ViewRootImpI 对象的线程中执行,而 ViewRootImpI 的创建时机是在 Activity 创建 DecorView (Window 中的第一个 View)的时候,也是系统帮我们启动 Activity,所以 ViewRootImpI 创建时所处的线程是主线程,所以我们不能在子线程刷新 UI,因为 View 是在主线程中创建的,源码已经很清楚地告诉我们了,View 的创建和改变必须要在同一个线程。

2 原生和 H5 交互的流程:


首先开启支持 JavaScript ,并且加载一遍网页,这个是前提条件
原生调用 H5:通过 loadUrl 来调用 JS 方法 (WebView.loadUrl("javascript:xxxxx");)
H5 调用原生:创建一个普通的方法并且添加注解(@JavascriptInterface),然后调用 addJavascriptInterface 将这个方法注入给 WebView

 

3 new Message 和 Message.obtain() 区别


这两种最本质的区别是,第一种是通过 new 的形式创建一个 Message 对象,而另一种是通过复用的形式来获取一个 Message 对象。在使用 Handler 发送 Message 的情况下,建议采用第二种方式,因为第二种方式在消息频繁的情况下,所表现的性能较优,如果每次都创建 Message 对象,会造成不必要的资源浪费。

4 IntentService 和 Service 有什么区别:


1. Service 运行在主线程,而 IntentService 运行子线程

2. Service 必须手动调用 stopSelf 才能关闭服务,而 IntentService 执行完毕之后会自动关闭服务

自定义打包名称

android.applicationVariants.all {
    variant->
        variant.outputs.all {
            output->
                def appName ="building_"
                def outputFile=output.outputFile
                if (outputFile!=null && outputFile.name.contains('release')){
                    def  fileName="building_v${variant.versionName}"+new Date().format("_MMddss")+".apk"
                    outputFileName=fileName;
                }else if (outputFile!=null && outputFile.name.contains('debug')){
                    def  fileName="building_v${variant.versionName}"+new Date().format("_MMdd")+".apk"
                    outputFileName=fileName;
                }
        }
}

 

5 数据存储的方式


大致分为两种:

1. 本地存储(文件、SP、SQLite、MMKV)

2. 网络存储(普通传输、加密传输)

在实际开发中,我们会将一些配置文件存放在本地,比如应用设置,用户信息等保存到本地中,而一些比较重要的数据,则通过接口发送给后台保存,对于登陆密码或者支付密码还需要进行加密操作。

 

6 Activity 四种启动模式


standard(标准模式):拿来主义,start 多少次就创建多少个 Activity

singleTop(单一栈顶):如果欲启动的 Activity 是栈顶,则不会重复创建。

singleTask(单一任务栈):如果欲启动的 Activity 有实例在任务栈,就会将在这个 Activity 上层的 Activity 清除,这样即能保证栈中的唯一性,又能达到复用的效果。

singleInstance(单一实例):这种模式比较特殊,Activity 会运行在单独的任务栈中,整个手机中只有一个实例存在。

7 请简述 View 事件分发机制


View 事件一般要经过三个流程:分发(dispatchTouchEvent)、拦截(onInterceptTouchEvent)、消费(onTouchEvent)
dispatchTouchEvent:View 会先执行 OnTouchListener 中 onTouch 方法,如果这个监听器的方法返回 true 则不会将事件交由自身的 onTouchEvent 进行消费。而 ViewGroup 跟 View 不同的是,它是直接将事件交由触摸位置上的子 View 的  dispatchTouchEvent 方法处理。

onInterceptTouchEvent:这个是 ViewGroup 独有的方法,在 dispatchTouchEvent 方法中调用,一般情况下返回 false(也就是不拦截),ViewGroup 如果想拦截触摸事件可以重写此方法返回 true,将事件交由自身的 onTouchEvent 方法处理。

onTouchEvent:一次触摸事件会产生 down(按下)、move(移动)、up(抬起)三个事件,这个方法将决定事件产生的效果,比如状态选择器的 Drawable 变换就是在这个方法中触发的,还有最常用的点击事件 onClick 也是在这里回调的。

总结:三个事件方法各有各的作用,分发是想把事件传递下去,拦截是不希望事件传递下去,而消费是对事件的一系列处理。通过这三个方法,我们可以控制触摸事件交由谁去处理,整个事件机制采用了责任链设计模式,事件会一层一层往下传递。

 

8 请简述 View 绘制流程


一般 View 绘制要经过三个步骤:测量、摆放、绘制

onMeasure:View 会测量自身的宽高、ViewGroup 会测量子 View 的宽高,其中测量有两个主要的值,测量模式和测量大小,这两个值其实由一个 32 位 int 值组成,高 2 位为测量模式,低 30 位为测量大小。而测量模式有三种:自适应(wrap_content)、精确值(match_parent、固定值)、未指明(View 想多大就多大,在 ListView、ScrollView 等在测量子布局的时候会用)。

onLayout:这个是 ViewGroup 独有的方法,用于决定子控件的位置,同时也可以对 View 的宽高进行调整。

onDraw:这个是 View 独有的方法,当 View 调用 invalidate 会先触发 draw 方法,然后依次绘制背景、内容、前景、滚动条。而我们平时最常用的 onDraw 方法就是这四个步骤的其中一个:绘制内容。

总结:onMeasure 得出是绘制区域的大小,onLayout 得出是绘制区域的位置,而 onDraw 是对指定区域进行绘制,这样 View 就能根据我们想要的方式绘制到屏幕上了。

9 内存优化


1. 不用的对象及时释放,即指向 null

2. 在遍历得到想要的位置之后要跳出循环

3. 尽量不要在循环或者递归中 new 对象

4. 在频繁 new 对象的地方使用享元设计模式

5. 第三方框架懒加载或者使用到的时候再初始化

6. 尽量使用 Parcelable(通过接口序列化),减少使用 Serializable(反射次数多)

7. 减少避免使用反射(EventBus APT 插件可以减少反射次数)

8. 减少 findViewById 次数(不要在 onBindViewHolder 中进行 findViewById,而是在 ViewHolder 构造函数中进行)

9. 在数据量小的时候,应该选用 ArrayMap 和 SparseArray 来代替 HashMap(因为它们的数据结构很简单,都是由数组组成,而 HashMap 涉及了链表和红黑树)

10. 在 ViewPager 中 Fragment 比较少的情况下,应当使用 FragmentPagerAdapter,否则应当使用 FragmentStatePagerAdapter( FragmentPagerAdapter 只是简单走了一遍 Fragment生命周期,并没有真正从 ViewPager 里面移除掉,而 FragmentStatePagerAdapter 会保存 Fragment 的状态,然后把它从 ViewPager 中移除掉,然后下次使用的时候重新添加并且恢复它原有的状态。可以看出 FragmentStatePagerAdapter 对 Fragment 内存管理机制还是做得比较完善的)

11. 及时释放资源(IO、SQLite)

12. 监听器反注册(EditText、BroadcastReceiver)

13. WebView 生命周期优化(回调 onResume、onPause、Destroy)

14. WebView 独立进程优化(WebView 本身就是一个复杂的 View,消耗内存和性能比较大,放在独立进程中可以减小 APP 有一定几率因为内存溢出导致的崩溃)

10 三级缓存是哪三级,分别有什么用


内存缓存:速度快,优先读取,但要管理好内存

本地缓存:速度其次,内存中没有,才读本地

网络缓存:速度最慢,本地也没有,才访问网络

11 简述一下 ANR 实现原理


ANR 检测是由系统服务来完成,每当主线程接收到操作之后,系统会使用 Handler 会发送一个延迟消息,当这个操作完成之后会将这个延迟消息移除,如果这个延迟消息没有被移除,那么就证明应用没有及时响应,同时也会触发系统向用户发送 ANR 警告。

12 如何避免内存泄漏:


出现内存的泄漏最多的两个原因:

1. 使用静态的 Activity

2. 不恰当使用 Handler

解决这两个内存泄漏的方案:

1. 尽量使用 Application 作为静态 Context 对象,如果一定要用 Activity,不能直接引用,而是需要使用弱引用或者软引用

2. Handler 是开发中最常用的类,伴随使用频率的增高,内存泄漏发生的情况也比较多,往往是我们使用不当导致的。我们应当在 Activity 销毁的时候 removeAllMessage,又或者将 Activity 弱引用或者软引用持有。

另外我们可以用第三方框架 LeakCanary(金丝鸟)来检测应用是否发生内存泄漏,这个框架的原理是通过监听 Activity 的生命周期,当 Activity 销毁之后一段时间,金丝鸟会检查弱引用中的 Activity 对象是否被置空(回收)了,如果是的话就证明这个 Activity 在使用的时候没有产生内存泄漏。

13 什么是内存抖动?如何避免内存抖动?


概念:内存抖动是因为短时间内有大量的对象进出(创建和回收),随着系统频繁的 GC,使渲染(UI)线程被阻塞,从而导致程序显示的画面有短暂卡顿。 

避免:尽量避免在频繁调用的地方 new 对象(比如循环递归,View.onDraw、RecyclerView.onBindView),如果需要最好把对象提取到外层(循环外或者提取成字段),针对一些可复用的对象(比如 Bitmap),建议使用对象池进行缓存。

14 请简述内存溢出和内存泄漏有什么区别


内存溢出是系统给应用分配的内存使用超标导致的,而分配的内存大小是根据屏幕大小而定的,一般屏幕越大分配的内存越大,我们需要做好内存优化和内存控制;而内存泄漏是指 GC 垃圾回收器无法回收对象,导致对象占用的内存空间无法释放,我们可以理解成这部分内存空间被占用着,无法被系统重复利用。

 

15 请简述 Handler 的工作原理


Handler 有两个重要的组成部分,Looper(消息轮询器) 和 MessageQueue(消息队列)

Looper 是 Handler 实现的核心,Looper 在构造方法中会创建 MessageQueue,而 Handler 处理消息的时候会交给 MessageQueue.enqueueMessage 方法,而 MessageQueue 会将消息链表进行重新排序,再判断 Looper 是否唤醒,如果 MessageQueue 中没有正在消息轮询,那么 Looper 会一直处于休眠状态,所以 MessageQueue 需要触发 Looper 对自己的轮询。(通过往管道写入数据通知 Looper.looper)

16 Java 回收算法


1. 引用计数算法:对象引用对象会进行计数,当这个对象没有被任何对象引用的时候,计数就为零,这个对象就会被回收掉。但是这种算法是有缺陷的,当两个对象相互引用的时候,会导致对象无法回收。

2. GC Root 可达分析算法:由于引用计数算法是有问题的,后面诞生了这种回收算法。当 GC Root (栈帧中的变量、静态变量、常量、JNI 引用的对象)不可用时,其他引用这个 GC Root 的对象也将作为无效对象被垃圾收集器回收。

 

17 Java 最常见的两种查找算法:


1. 线性查找:这个,对数组进行遍历操作

2. 二分法查找:这个算法必须要数组是有序的前提下,取数组中间的值进行比较,如果小于则取左边的数值进行查找(否则向右查找),再获取左边的数值中间的值进行比较,以此类推,直到找到对应的值为止。

 

18 代码优化


1. 对常用的功能模块进行封装

2. 对重复的代码考虑进行抽取

3. 关注编译器给出的警告并正确处理

4. 通过不断改进来优化代码的写法

5. 使用 AOP 降低一些代码的耦合性

6. 代码命名和文件命名要规范(阿里代码规范手册,驼峰命名)

7. 减少不必要的代码注释,尽量用规范的代码代替注释

8. 完善重点难点代码的注释,完善后台接口代码的注释(接口作用,参数含义)

9. 代码的摆放顺序要有一定的规律(根据代码类型和执行顺序来定义)

19 请简述 HashMap 的原理:


数据结构:HashMap 其实是一个对象数组,数据结构采用的是链表,当链表长度大于 8个 的时候,会切换成红黑树,如果红黑树长度小于 6 个会回退到链表。

存储流程:HashMap 是先计算 Key 对象的 hashCode 值,因为 hashCode 的值比较大,所以 HashMap 会用位运算对这个值压缩到 16 (对象数组长度)以内的值,得出来的结果就是链表的位置。

容量扩容:HashMap 默认的数组容量是 16,其负载因子是 0.75,如果超过了 12 (16 * 0.75)个元素,会对数组进行双倍扩容,也就是 32 (16 * 2)。扩容的过程比较简单,但扩容是一个密集操作,HashMap 会重新计算每个元素的位置,然后给这些元素重新排序。

你可能感兴趣的:(Android 一些基础知识整理(一))