Android 进阶
Android平台的优势和不足
Android平台手机 5大优势:
开放性:Android平台首先就是其开放性,开发的平台允许任何移动终端厂商加入到Android联盟中来。显著的开放性可以使其拥有更多的开发者;
挣脱运营商的束缚:在过去很长的一段时间,手机应用往往受到运营商制约,使用什么功能接入什么网络,几乎都受到运营商的控制,而Android用户可以更加方便地连接网络,运营商的制约减少;
丰富的硬件选择:由于Android的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色,却不会影响到数据同步、甚至软件的兼容;
开发商不受任何限制:Android平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰;
无缝结合的Google应用: Android平台手机将无缝结合这些优秀的Google服务如地图、邮件、搜索等;
Android平台手机几大不足:
安全和隐私:由于手机与互联网的紧密联系,个人隐私很难得到保守。除了上网过程中经意或不经意留下的个人足迹,Google这个巨人也时时站在你的身后,洞穿一切;
过分依赖开发商缺少标准配置:在Android平台中,由于其开放性,软件更多依赖第三方厂商,比如Android系统的SDK中就没有内置音乐播放器,全部依赖第三方开发,缺少了产品的统一性;
同类机型用户很少:在不少手机论坛都会有针对某一型号的子论坛,对一款手机的使用心得交流,并分享软件资源。而对于Android平台手机,由于厂商丰富,产品类型多样,这样使用同一款机型的用户越来越少,缺少统一机型的程序强化。
简述apk打包过程
1:打包资源文件,生成R.java文件 输入:res文件,Assets文件,AndroidManifest.xml文件,Android基础类库(Android.jar文件) 输出:R.java,resources.arsc 工具:aapt 工具位置:SDK\build-tools\29.0.0\aapt.exe
2:处理aidl文件,生成相应java文件 输入:源码文件,aidl文件,framework.aidl文件 输出:对应的.java文件 工具:aidl工具 工具位置:SDK\build-tools\29.0.0\aidl.exe
3:编译工程源代码,生成相应class文件 输入:源码文件(包括R.java和AIDL生成的.java文件),库文件(jar文件) 输出:.class文件 工具:javac工具 工具位置:Java\jdk1.8.0_201\bin\javac.exe
4:转换所有class文件,生成classes.dex文件 输入:.class文件(包括Aidl生成.class文件,R生成的.class文件,源文件生成的.class文件),库文件(.jar文件) 输出:.dex文件 工具:javac工具 工具位置:Java\jdk1.8.0_201\bin\javac.exe
5:打包生成apk文件 输入:打包后的资源文件,打包后类文件(.dex文件),libs文件(包括.so文件) 输出:未签名的.apk文件 工具:apkbuilder.bat工具已废弃,改为sdklib.jar工具 工具位置:E:\SDK\tools\lib\sdklib.jar
6:对apk文件进行签名 输入:未签名的.apk文件 输出:签名的.apk文件 工具: jarsigner工具 apksigner工具 工具位置: Java\jdk1.8.0_201\bin\jarsigner.exe SDK\build-tools\29.0.0\lib\apksigner.jar
7:对签名后的apk文件进行对齐处理 输入:签名后的.apk文件 输出:对齐后的.apk文件 工具:zipalign工具 工具位置:SDK\build-tools\29.0.0\zipalign.exe
注:工具位置基于win平台. 参考连接: developer.android.com/studio/buil… blog.csdn.net/jiangwei091…
LruCache 算法源码解析
我们先看下LruCache算法的构造方法。
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
从构造方法的源码我们可以看到,在这段代码中我们主要做了两件事。第一是判断下传递来的最大分配内存大小是否小于零,如果小于零则抛出异常,因为我们如果传入一个小于零的内存大小就没有意义了。之后在构造方法内存就new了一个LinkHashMap集合,从而得知LruCache内部实现原理果然是基于LinkHashMap来实现的。
之后我们再来看下存储缓存的put()方法。
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
从代码中我们可以看到,这个put方法内部其实没有做什么很特别的操作,就是对数据进行了一次插入操作。但是我们注意到最后的倒数第三行有一个trimToSize()方法,那么这个方法是做什么用的呐?我们点进去看下。
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {
break;
}
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
我们可以看到,这个方法原来就是对内存做了一次判断,如果发现内存已经满了,那么就调用map.eldest()方法获取到最后的数据,之后调用map.remove(key)方法,将这个最近最少使用的数据给剔除掉,从而达到我们内存不炸掉的目的。
我们再来看看get()方法。
public final V get(K key) {
//key为空抛出异常
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//获取对应的缓存对象
//get()方法会实现将访问的元素更新到队列头部的功能
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
get方法看起来就是很常规的操作了,就是通过key来查找value的操作,我们再来看看LinkHashMap的中get方法。
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
//实现排序的关键方法
e.recordAccess(this);
return e.value;
}
调用recordAccess()方法如下:
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
由此可见LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队尾元素,即最近最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队头。
谈谈热修复的原理
我们知道Java虚拟机 —— JVM 是加载类的class文件的,而Android虚拟机——Dalvik/ART VM 是加载类的dex文件, 而他们加载类的时候都需要ClassLoader,ClassLoader有一个子类BaseDexClassLoader,而BaseDexClassLoader下有一个数组——DexPathList,是用来存放dex文件,当BaseDexClassLoader通过调用findClass方法时,实际上就是遍历数组,找到相应的dex文件,找到,则直接将它return。而热修复的解决方法就是将新的dex添加到该集合中,并且是在旧的dex的前面,所以就会优先被取出来并且return返回。
简述app启动过程
App启动一般分为两种:
- 冷启动:当应用启动的时候,后台没有当前应用的进程,这时系统会创建一个新的进程分配给应用。
- 热启动:当前应用已经打开,但是被按下返回键或者Home键退出到桌面或者去到其他App,当再次回到应用时就是热启动。
这里主要介绍冷启动,大致分为5个步骤:
- 当点击桌面图标,就会利用Launcher通过Binder进程间通信机制通知ActivityManagerService(AMS),它要启动一个Activity;
- AMS得到Launcher的通知,就会新建一个Task去准备启动Activity,并通过Binder机制通知Launcher进入Paused状态;
- Launcher得到消息,就会直接挂起,并通过Binder告诉AMS我已经Paused了;AMS知道了Launcher已经挂起之后,就可以放心的为新的Activity准备启动工作了,首先,APP肯定需要一个新的进程去进行运行,所以需要创建一个新进程,这个过程是需要Zygote参与的,AMS通过Socket去和Zygote协商,然后利用Zygote.fork()创建一个新的进程,在这个进程里启动ActivityThread类,这就是每一个应用程序都有一个ActivityThread与之对应的原因;
- 进程创建好了,通过调用上述的ActivityThread的main方法,这是应用程序的入口,在这里开启Looper消息循环队列,这也是主线程默认绑定Looper的原因;(另外,ActivityThread通过Binder将一个ApplicationThread类型的Binder对象传递给AMS,以便以后AMS能够通过这个Binder对象和它进行通信);
- 这时候,App还没有启动完,要永远记住,四大组件的启动都需要AMS去启动,将上述的应用进程信息注册到AMS中,所以AMS在通过BinderActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。
补充知识: Zygote zygote名字翻译叫受精卵,zygote进程的创建是由Linux系统中init进程创建的,Android中所有的进程都是直接或者间接的由init进程fork出来的,Zygote进程负责其他进程的创建和启动,比如创建SystemServer进程。当需要启动一个新的android应用程序的时候,ActivityManagerService就会通过Socket通知Zygote进程为这个应用创建一个新的进程。
Launcher 我们要知道手机的桌面也是一个App我们叫它launcher,每一个手机应用都是在Launcher上显示,而Launcher的加载是在手机启动的时候加载Zygote,然后Zygote启动SystenServer,SystenServer会启动各种ManageService, 包括ActivityManagerService,并将这些ManageService注册到ServiceManage 容器中,然后ActivityManagerService就会启动Home应用程序Launcher.
ActivityManagerService ActivityManagerService我们简称AMS,四大组件都归它管,四大组件的跨进程通信都要和它合作。
Binder Binder是Android跨进程通信(IPC)的一种方式,也是Android系统中最重要的特性之一,android 四大组件以及不同的App都运行在不同的进程,它则是各个进程的桥梁将不同的进程粘合在一起。
ActivityThread 首先ActivityThread并不是一个Thread,其作用就是在main方法内做消息循环。那我们常说的主线程是什么?主线程就是承载ActivityThread的Zygote fork而创建的进程。 ActivityThread的调用是在ActivityManageService.startProcessLocked()方法里调用并创建,这个类主要做了这几个事:
- 创建Looper,开启Looper循环
- 创建内部类 H,H继承于Handler 用于跨进程通信切换线程
- 创建ApplicationThread跨进程Binder对象mAppThread。 这里要说一点,ActivityThread通过ApplicationThread与AMS进行通信,ApplicationThread通过H与ActivityThread进行通信(handler机制),处理Activity的事务。
From GavinCui12
1、点击桌面应用图标,Launcher进程将启动Activity(MainActivity)的请求以Binder的方式发送给了AMS。
2、AMS接收到启动请求后,交付ActivityStarter处理Intent和Flag等信息,然后再交给ActivityStackSupervisior/ActivityStack 处理Activity进栈相关流程。同时以Socket方式请求Zygote进程fork新进程。
3、Zygote接收到新进程创建请求后fork出新进程。
4、在新进程里创建ActivityThread对象,新创建的进程就是应用的主线程,在主线程里开启Looper消息循环,开始处理创建Activity。
5、ActivityThread利用ClassLoader去加载Activity、创建Activity实例,并回调Activity的onCreate()方法,这样便完成了Activity的启动。
Android虚拟机
ART和DVM(Dalvik)区别
什么是Dalvik: Dalvik是Google公司自己设计用于Android平台的Java虚拟机。 它可以支持已转换为.dex(即Dalvik Executable)格式的Java应用程序的运行。 .dex格式是专为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统。 Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik应用作为独立的Linux进程执行。 独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
什么是ART: 与Dalvik不同,ART使用预编译(AOT,Ahead-Of-Time)。 也就是在APK运行之前,就对其包含的Dex字节码进行翻译,得到对应的本地机器指令,于是就可以在运行时直接执行了。 ART应用安装的时候把dex中的字节码将被编译成本地机器码,之后每次打开应用,执行的都是本地机器码。 去除了运行时的解释执行,效率更高,启动更快。
区别:
- Dalvik每次都要编译再运行,Art只会首次启动编译
- Art占用空间比Dalvik大(原生代码占用的存储空间更大),就是用“空间换时间”
- Art减少编译,减少了CPU使用频率,使用明显改善电池续航
- Art应用启动更快、运行更快、体验更流畅、触感反馈更及时
参考官方文档: source.android.com/devices/tec…
Android基础
Serializable和Parcelable的区别
Serializable(Java自带): Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。
Parcelable(android 专用): 除了Serializable之外,使用Parcelable也可以实现相同的效果, 不过不同于将对象进行序列化,Parcelable方式的实现原理是将一个完整的对象进行分解, 而分解后的每一部分都是Intent所支持的数据类型,这样也就实现传递对象的功能了。
区别:
- 在使用内存的时候,Parcelable 类比Serializable性能高,所以推荐使用Parcelable类。
- Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的 GC。
- Parcelable 不能使用在要将数据存储在磁盘上的情况。尽管 Serializable 效率低点,但在这种情况下,还是建议你用Serializable 。
实现:
- Serializable 的实现,只需要继承Serializable 即可。这只是给对象打了一个标记,系统会自动将其序列化。
- Parcelabel 的实现,需要在类中添加一个静态成员变量 CREATOR,这个变量需要继承Parcelable.Creator 接口,(一般利用编译器可以自动生成)。
参考自简书:www.jianshu.com/p/a60b609ec…
From Noble_JIE
从出生来来说,Serializable 是java的方法,Parcelable 是android独有的序列化反序列化方法 从用法上来说,Serializable 比 Parcelable 简单,所有类实现Serializable即可,Parcelable需要对对所有属性及成员变量进行Creator 。 从性能上来说,Parcelable 性能要高于Serializable。
怎样避免和解决ANR
Application Not Responding,即应用无响应。避免ANR最核心的一点就是在主线程减少耗时操作。通常需要从那个以下几个方案下手:
a)使用子线程处理耗时IO操作
b)降低子线程优先级,使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同
c)使用Handler处理子线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程
d)Activity的onCreate和onResume回调中尽量避免耗时的代码
e)BroadcastReceiver中onReceiver代码也要尽量减少耗时操作,建议使用intentService处理。intentService是一个异步的,会自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止并销毁Service的问题
Broadcast 注册方式与区别
Broadcast广播,注册方式主要有两种.
第一种是静态注册,也可成为常驻型广播,这种广播需要在Androidmanifest.xml中进行注册,这中方式注册的广播,不受页面生命周期的影响,即使退出了页面,也可以收到广播这种广播一般用于想开机自启动啊等等,由于这种注册的方式的广播是常驻型广播,所以会占用CPU的资源。
第二种是动态注册,而动态注册的话,是在代码中注册的,这种注册方式也叫非常驻型广播,收到生命周期的影响,退出页面后,就不会收到广播,我们通常运用在更新UI方面。这种注册方式优先级较高。最后需要解绑,否会会内存泄露
广播是分为有序广播和无序广播。
From chenwentong
一、Brodcast注册方式:
1、静态注册:
2、动态注册:
二、静态注册:在清单文件manifest中注册,当程序退出之后还可以收到该广播。不能控制具体某个时间点接收和不接收广播。
三、动态注册:通过代码的方式注册context.registerReceiver(broadcastReceiver),注册的同时注意在不需要接受的时候进行反注册context.unregisterReceiver(broadcastReceiver);避免内存泄漏, 动态注册可以很好的控制广播接受。
四、从Android 8.0(API 26)开始,对于大部分隐式广播(广播的对象不是针对你开发的APP),不能在manifest中声明receiver,如果需要使用隐式广播,需要使用context.registerReceiver 的方法。
谈谈Android的安全机制
- Android 是基于Linux内核的,因此 Linux 对文件权限的控制同样适用于 Android。在 Android 中每个应用都有自己的/data/data/包名 文件夹,该文件夹只能该应用访问,而其他应用则无权访问。
- Android 的权限机制保护了用户的合法权益。如果我们的代码想拨打电话、发送短信、访问通信录、定位、访问、sdcard 等所有可能侵犯用于权益的行为都是必须要在 AndroidManifest.xml 中进行声明的,这样就给了用户一个知情权。
- Android 的代码混淆保护了开发者的劳动成果。
ListView如何提高效率
1、使用分页加载,不要一次性加载所有数据。
2、复用convertView。在getItemView中,判断converView是否为空,如果不为空,可复用。
3、异步加载图片。Item中如果包含有webimage,那么最好异步加载。
4、快速滑动时,不显示图片。当快速滑动列表(SCROLL_STATE_FLING),item中的图片或获取需要消耗资源的view,可以不显示出来;而处于其他两种状态(SCROLL_STATE_IDLE和SCROLL_STATE_TOUCH_SCROLL),则将那些view显示出来
ANR异常的产生条件及解决方案
ANR是什么? ANR全称:Application Not Responding,也就是应用程序无响应. 简单来说,就是应用跑着跑着,突然duang,界面卡住了,无法响应用户的操作如触摸事件等.
原因 Android系统中,ActivityManagerService(简称AMS)和WindowManagerService(简称WMS)会检测App的响应时间. 如果App在特定时间无法相应屏幕触摸或键盘输入时间,或者特定事件没有处理完毕,就会出现ANR.
ANR的产生需要同时满足三个条件
- 主线程:只有应用程序进程的主线程(UI线程)响应超时才会产生ANR
- 超时时间:产生ANR的上下文不同,超时时间也不同,但只要超过这个时间上限没有响应就会产生ANR
- 输入事件/特定操作:输入事件是指按键、触屏等设备输入事件,特定操作是指BroadcastReceiver和Service的生命周期中的各个函数调用
解决方案: 总结为一句话,即不要在主线程(UI线程)里面做繁重的操作
谈谈 RecyclerView 的性能优化
-
数据处理和视图加载分离
从远端拉取数据肯定是要放在异步的,在我们拉取下来数据之后可能就匆匆把数据丢给了 VH 处理,其实,数据的处理逻辑我们也应该放在异步处理,这样 Adapter 在 notify change 后,ViewHolder 就可以简单无压力地做数据与视图的绑定逻辑,比如:
mTextView.setText(Html.fromHtml(data).toString());这里的
Html.fromHtml(data)方法可能就是比较耗时的,存在多个TextView的话耗时会更为严重,这样便会引发掉帧、卡顿,而如果把这一步与网络异步线程放在一起,站在用户角度,最多就是网络刷新时间稍长一点。 -
数据优化
分页拉取远端数据,对拉取下来的远端数据进行缓存,提升二次加载速度;对于新增或者删除数据通过
DiffUtil来进行局部刷新数据,而不是一味地全局刷新数据。 -
布局优化
-
减少过渡绘制
减少布局层级,可以考虑使用自定义 View 来减少层级,或者更合理地设置布局来减少层级,不推荐在 RecyclerView 中使用
ConstraintLayout,有很多开发者已经反映了使用它效果更差,相关链接有:Is ConstraintLayout that slow?、constraintlayout 1.1.1 not work well in listview。 -
减少 xml 文件 inflate 时间
这里的 xml 文件不仅包括 layout 的 xml,还包括 drawable 的 xml,xml 文件 inflate 出 ItemView 是通过耗时的 IO 操作,尤其当 Item 的复用几率很低的情况下,随着 Type 的增多,这种 inflate 带来的损耗是相当大的,此时我们可以用代码去生成布局,即
new View()的方式,只要搞清楚 xml 中每个节点的属性对应的 API 即可。 -
减少 View 对象的创建
一个稍微复杂的 Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化 ItemView;设计 ItemType 时,对多 ViewType 能够共用的部分尽量设计成自定义 View,减少 View 的构造和嵌套。
-
-
其他
-
升级
RecycleView版本到 25.1.0 及以上使用 Prefetch 功能,可参考 RecyclerView 数据预取。 -
如果 Item 高度是固定的话,可以使用
RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源; -
设置
RecyclerView.addOnScrollListener(listener);来对滑动过程中停止加载的操作。 -
如果不要求动画,可以通过
((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false);把默认动画关闭来提神效率。 -
对
TextView使用String.toUpperCase来替代android:textAllCaps="true"。 -
对
TextView使用StaticLayout或者DynamicLayout的自定义View来代替它。 -
通过重写
RecyclerView.onViewRecycled(holder)来回收资源。 -
通过
RecycleView.setItemViewCacheSize(size);来加大RecyclerView的缓存,用空间换时间来提高滚动的流畅性。 -
如果多个
RecycledView的Adapter是一样的,比如嵌套的RecyclerView中存在一样的Adapter,可以通过设置RecyclerView.setRecycledViewPool(pool);来共用一个RecycledViewPool。 -
对
ItemView设置监听器,不要对每个 Item 都调用addXxListener,应该大家公用一个XxListener,根据ID来进行不同的操作,优化了对象的频繁创建带来的资源消耗。 -
通过 getExtraLayoutSpace 来增加 RecyclerView 预留的额外空间(显示范围之外,应该额外缓存的空间),如下所示:
new LinearLayoutManager(this) { @Override protected int getExtraLayoutSpace(RecyclerView.State state) { return size; } };
-
如何减小apk安装包体积
1.代码混淆 minifyEnabled true
2.资源压缩 1)shrinkResources true 2)微信的AndResGuard
3.图片压缩 1)tinypng 2)svg 3)webp
4.so库配置 只保留两个abi平台,即armeabi和armeabi-v7a
5.dex优化 Facebook的redex
四种LaunchMode及其使用场景
standard 模式 这是默认模式,每次激活Activity时都会创建Activity实例,并放入任务栈中。使用场景:大多数Activity。
singleTop 模式 如果在任务的栈顶正好存在该Activity的实例,就重用该实例( 会调用实例的 onNewIntent() ),否则就会创建新的实例并放入栈顶,即使栈中已经存在该Activity的实例,只要不在栈顶,都会创建新的实例。使用场景如新闻类或者阅读类App的内容页面。
singleTask 模式 如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈。如果栈中不存在该实例,将会创建新的实例放入栈中。使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
singleInstance 模式 在一个新栈中创建该Activity的实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity实例已经存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例( 会调用实例的 onNewIntent() )。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。使用场景如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。
Android的数据存储方式
Android提供了5中存储数据的方式,分别是以下几种:
1、使用Shared Preferences存储数据,用来存储key-value,pairs格式的数据,它是一个轻量级的键值存储机制,只可以存储基本数据类型。
2、使用文件存储数据,通过FileInputStream和FileOutputStream对文件进行操作。在Android中,文件是一个应用程序私有的,一个应用程序无法读写其他应用程序的文件。
3、使用SQLite数据库存储数据,Android提供的一个标准数据库,支持SQL语句。
4、使用Content Provider存储数据,是所有应用程序之间数据存储和检索的一个桥梁,它的作用就是使得各个应用程序之间实现数据共享。它是一个特殊的存储数据的类型,它提供了一套标准的接口用来获取数据,操作数据。系统也提供了音频、视频、图像和个人信息等几个常用的Content Provider。如果你想公开自己的私有数据,可以创建自己的Content Provider类,或者当你对这些数据拥有控制写入的权限时,将这些数据添加到Content Provider中实现共享。外部访问通过Content Resolver去访问并操作这些被暴露的数据。
5、使用网络存储数据
Android进程间通信的几种姿势
进程间通信即IPC,英文全称Inter-Process Communication,是指进程间数据交互的过程. Android底层是基于Linux,而Linux基于安全考虑,是不允许两个进程间直接操作对方的数据,这就是进程隔离. 六种常用姿势:
- Bundle
- 文件共享
- AIDL
- Messenger
- ContentProvider
- Socket
参考:Android开发艺术探索 第2章 2.4节
From lydlovexyz
理解Activity,View,Window三者关系
Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图)LayoutInflater像剪刀,Xml配置像窗花图纸。 1:Activity构造的时候会初始化一个Window,准确的说是PhoneWindow。 2:这个PhoneWindow有一个“ViewRoot”,这个“ViewRoot”是一个View或者说ViewGroup,是最初始的根视图。 3:“ViewRoot”通过addView方法来一个个的添加View。比如TextView,Button等 4:这些View的事件监听,是由WindowManagerService来接受消息,并且回调Activity函数。比如onClickListener,onKeyDown等
Android SharedPreference频繁操作有什么后果?能存多少数据
Android中 SP 的底层是由Xml来实现的,操作SP的过程就是Xml的序列化和解析的过程。Xml是存储在磁盘上的,因此当我们频繁进行SP操作时,就是频繁进行序列化与解析,这就频繁进行I/O的操作,所以肯定会导致性能消耗。同时序列化Xml是就是将内存中的数据写到Xml文件中,由于DVM的内存是很有限的,因此单个SP文件不建议太大,具体多大是没有一个具体的要求的,但是我们知道DVM 堆内存也就是16M,因此数据大小肯定不能超过这个数字的。其实 SP 设置的目的就是为了保存用户的偏好和配置信息的,因此不要保存太多的数据。
请介绍下Android中常用的五种布局
最常用的布局方式为Absolute Layout、Relative Layout、Linear Layout、FrameLayout、TableLayout。其中Linear Layout和Relative Layout是最常用的方式,他们可以通过在xml配置文件或者代码中进行布局。
1、Frame Layout是最简单的布局方式,放置的控件都只能罗列到左上角,控件会有重叠,不能进行复杂的布局。
2、Linear Layout可以通过orientation属性设置线性排列的方向是垂直还是纵向的,每行或每列只有一个元素,可以进行复杂的布局。
3、Absolute Layout可以让子元素指定准确的x、y坐标值,并显示在屏幕上。Absolute Layout没有页边框,允许元素之间相互重叠。它是绝对坐标,所以在实际中不提倡使用。
4、Relative Layout允许子元素制定他们相对于其他元素或父元素的位置(通过ID制定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其他元素将以屏幕中央的相对位置来排列。这个是相对于Absolute Layout的,采用相对坐标,所以在实际中比较常用。
5、Table Layout将以子元素的位置分配到行或列。一个Table Layout由许多的Table Row组成,每个Table Row都会定义一个row。Table Layout容器不会显示row、column或者cell的边线框。每个row拥有0个或多个的cell; 和html中的table差不多。在实际中也经常使用。
1、线性布局 (LinearLayout):是一种非常常用的布局,次布局会将它包含的控件在线性方向上依次排列。通过android:orientation属性来确定排列的方向是vertical(垂直)还是horizontal(水平)。 2、相对布局(RelativeLayout):也是一种非常常用的布局,通过相对定位的方式让控件出现在布局的任何位置。 3、帧布局(FrameLayout):由于定位方式的欠缺,所有的控件都会默认摆放在布局的左上角,应用场景比较少。 4、绝对布局(AbsoluteLayout):用x、y坐标来确定控件的位置。 5、表格布局(TableLayout):每一个TableLayout里面有表格行TableRow,TableRow里面可以具体定义每一个控件。
Android线程间通信有几种方法?
- Handler机制
- runOnUiThread(Runnable action)
- View.post(Runnable action)
- AsyncTask
- 广播
- 使用EventBus、RxJava等框架
From Taonce
-
通过
Handler来通信val handler = @SuppressLint("HandlerLeak") object : Handler(){ override fun handleMessage(msg: Message?) { Log.d("taonce","msg arg1: ${msg?.arg1}") } } thread { val msg: Message = handler.obtainMessage() msg.arg1 = 1 handler.sendMessage(msg) } -
通过
runOnUiThread()thread { val text = "runOnUiThread" runOnUiThread { tv.text = text } } -
通过
View.post()thread { val text = "post" tv.post { tv.text = text } } -
通过
AsyncTaskclass MyAsyncTask(val name: String) : AsyncTask<String, Int, Any>() { // 执行任务之前的准备工作,比如将进度条设置为Visible,工作在主线程 override fun onPreExecute() { Log.d("async", "onPreExecute") } // 在onPreExecute()执行完之后立即在后台线程中调用 override fun doInBackground(vararg params: String?): Any? { Log.d("async", "$name execute") Thread.sleep(1000) publishProgress(1) return null } // 调用了publishProgress()之后,会在主线程中被调用,用于更新整体进度 override fun onProgressUpdate(vararg values: Int?) { Log.d("async", "progress is: $values") } // 后台线程执行结束后,会把结果回调到这个方法中,并在主线程中被调用 override fun onPostExecute(result: Any?) { Log.d("async", "onPostExecute") } }
谈一谈对Android中Context理解
Context是一个抽象基类。在翻译为上下文,也可以理解为环境,是提供一些程序的运行环境基础信息。Context下有两个子类,ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。而ContextWrapper又有三个直接的子类, ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity,所以Activity和Service以及Application的Context是不一样的,只有Activity需要主题,Service不需要主题。Context一共有三种类型,分别是Application、Activity和Service。这三个类虽然分别各种承担着不同的作用,但它们都属于Context的一种,而它们具体Context的功能则是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。 getApplicationContext()和getApplication()方法得到的对象都是同一个application对象,只是对象的类型不一样。 Context数量 = Activity数量 + Service数量 + 1 (1为Application)
Android 的四大组件都需要在清单文件中注册吗?并简述四大组件
Activity、Service、ContentProvider 如 果 要 使 用 则 必 须 在AndroidManifest.xml 中 进 行 注 册 , 而BroadcastReceiver则有两种注册方式,静态注册和动态注册。其中静态注册就是指在AndroidManifest.xml中进行注册,而动态注册时通过代码注册。
Activity:通常展现为一个用户操作的可视化界面。它为用户提供了一个完成操作指令的窗口。 (mp.weixin.qq.com/s/CgfeMT9Yt…) (Activity的来由)
Service:Android系统的服务(不是一个线程,是主程序的一部分),与Activity不同,它是不能与用户交互的,不能自己启动的,须要调用Context.startService()来启动,执行后台,假设我们退出应用时,Service进程并没有结束,它仍然在后台行。
BroadcastReceiver:广播接收器是一个专注于接收广播通知信息,并做出相应处理的组件。
ContentProvider:(内容提供者)主要用于对外共享数据,也就是通过ContentProvider把应用中的数据共享给其它应用訪问,其它应用能够通过ContentProvider对指定应用中的数据进行操作。
Android 实现异步的几种方式,原理与各自特点
这边介绍三种:AsyncTask,HandlerThread和IntentService
AsyncTask原理:内部是Handler和两个线程池实现的,Handler用于将线程切换到主线程,两个线程池一个用于任务的排队,一个用于执行任务,当AsyncTask执行execute方法时会封装出一个FutureTask对象,将这个对象加入队列中,如果此时没有正在执行的任务,就执行它,执行完成之后继续执行队列中下一个任务,执行完成通过Handler将事件发送到主线程。AsyncTask必须在主线程初始化,因为内部的Handler是一个静态对象,在AsyncTask类加载的时候他就已经被初始化了。在Android3.0开始,execute方法串行执行任务的,一个一个来,3.0之前是并行执行的。如果要在3.0上执行并行任务,可以调用executeOnExecutor方法
HandlerThread原理:继承自Thread,start开启线程后,会在其run方法中会通过Looper创建消息队列并开启消息循环,这个消息队列运行在子线程中,所以可以将HandlerThread中的Looper实例传递给一个Handler,从而保证这个Handler的handleMessage方法运行在子线程中,Android中使用HandlerThread的一个场景就是IntentService
IntentService原理:继承自Service,它的内部封装了HandlerThread和Handler,可以执行耗时任务,同时因为它是一个服务,优先级比普通线程高很多,所以更适合执行一些高优先级的后台任务,HandlerThread底层通过Looper消息队列实现的,所以它是顺序的执行每一个任务。可以通过Intent的方式开启IntentService,IntentService通过handler将每一个intent加入HandlerThread子线程中的消息队列,通过looper按顺序一个个的取出并执行,执行完成后自动结束自己,不需要开发者手动关闭
既然RecyclerView在很多方面能取代ListView,Google为什么没把ListView划上一条过时的横线?
ListView采用的是RecyclerBin的回收机制,在一些轻量级的List显示时效率更高.
- ListView采用的是RecyclerBin的回收机制在一些轻量级的List时效率更高。
- 在处理少量数据使用 ListView
- 在处理大量数据的时候使用 RecyclerView
ScrollView嵌套ListView的解决方案及其原理
自定义ListView 解决:重写其中的onMeasure()方法
原因: ScrollView默认把Childview设置为UNSPEFEIED模式,而该模式下的ListView给自己的测量的高度就是第一个item的高度.原理: int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); 这个方法的作用是根据大小和模式来生成一个int值,这个int值封装了模式和大小信息. 首先MeasureSpec类是View的一个静态内部类,MeasureSpec类封装了从父布局到子布局传递的布局需求. 每个MeasureSpec对象代表了宽度和高度的要求. MeasureSpec用int类型表示,前2位代表模式,后30位代表大小. 第一个参数Integer.MAX_VALUE >> 2:Integer.MAX_VALUE获取到int的最大值,但是表示大小的值size是int数值的底30位,所以把这个值右移两位,留出高两位表示布局模式. 此时这个值仍旧是一个30位所能表示的最大数,用该数作为控件的size,应该足够满足控件大小的需求. 第二个参数MeasureSpec.AT_MOST:表示这个控件适配父控件的最大空间.
(以下三种仅供参考,不推荐使用) 2.手动设置ListView高度 3.使用单个ListView取代ScrollView中所有内容 4.使用LinearLayout取代ListView
参考资料:
juejin.cn/post/684490…
juejin.cn/post/684490…
为什么在子线程创建Handler会抛异常?如何正确使用
Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。因此Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。正确的使用方法是:
private final class WorkThread extends Thread {
private Handler mHandler;
public Handler getHandler() {
return mHandler;
}
public void quit() {
mHandler.getLooper().quit();
}
@Override
public void run() {
super.run();
//创建该线程对应的Looper,
// 内部实现
// 1。new Looper()
// 2。将1步中的lopper 放在ThreadLocal里,ThreadLocal是保存数据的,主要应用场景是:线程间数据互不影响的情况
// 3。在1步中的Looper的构造函数中new MessageQueue();
//其实就是创建了该线程对用的Looper,Looper里创建MessageQueue来实现消息机制
//对消息机制不懂得同学可以查阅资料,网上很多也讲的很不错。
Looper.prepare();
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d("WorkThread", (Looper.getMainLooper() == Looper.myLooper()) + "," + msg.what);
}
};
//开启消息的死循环处理即:dispatchMessage
Looper.loop();
//注意这3个的顺序不能颠倒
Log.d("WorkThread", "end");
}
}
自定义控件优化方案
- 为了加速你的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的层级结构来计算大小。这个PieChart 例子展示了如何继承ViewGroup作为自定义view的一部分。PieChart 有子views,但是它从来不测量它们。而是根据他自身的layout法则,直接设置它们的大小。
谈谈Android的事件分发机制
事件的传递流程: Activity(PhoneWindow)->DecorView->ViewGroup->View。 事件分发过程中三个重要的方法: dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent(); 事件传递规则 一般一次点击会有一系列的MotionEvent,可以简单分为:down->move->….->move->up,当一次event分发到ViewGroup时,ViewGroup收到事件后调用dispatchTouchEvent,在dispatchTouchEvent中先检查是否要拦截,若拦截则ViewGroup处理事件,否则交给有处理能力的子容器处理。
Android动画有几种,对其理解
- 视图动画。视图移动、view真真的位置并未移动。
- 帧动画。就和放电影一样,一帧一帧的播
- 属性动画。视图移动、其位置也会随着移动。
- 触摸返回动画。发生触摸事件时有反馈效果。比如波纹效果
- 揭露动画。从某一个点向四周展开或者从四周向某一点聚合起来。
- 转场动画 & 共享元素。比如切换activity。共享元素一般我们使用在转换的前后两个页面有共同元素时。
- 视图状态动画。就是 View 在状态改变时执行的动画效果
- 矢量图动画。在图片的基础上做动画。
- 约束布局实现的关键帧动画。就是给需要动画效果的属性,准备一组与时间相关的值。关键的几个值。
Android 内存泄漏的原因以及解决方案
-
内存泄漏指对象不再使用,本该被回收,却因为有其他正在使用的对象持有该对象的引用,而无法被JVM回收
-
内存泄漏的影响:
- 应用可用内存减少,增加堆内存压力
- 频繁触发GC,会降低了应用的性能
- 到一定程序会导致内存溢出错误
-
Android开发中常见内存泄漏及解决办法
- 静态变量生命周期与应用的生命周期一样,如果静态变量持有某个Activity的上下文,则对应Activity无法释放,导致内存泄漏(单例模式) 解决办法:使用Application的上下文
- 匿名内部类与非静态内部类因为都会持有外部类引用,当执行异步操作易导致内存泄漏 解决办法:将非静态内部类转为静态内部类+WeakReferenct的方式
- Handler消息队列存在延时消息导致内存泄漏 在onDestroy方法中调用Handler相应的方法移除回调和删除消息
- 各种注册的监听器忘记移除导致内存泄漏 解决办法:在onDestroy方法中取消注册
- 资源对象未关闭导致内存泄漏,如(IO,数据库,Bitmap等) 解决办法:及时关闭资源
- 属性动画未取消导致内存泄漏(如无限轮播图效果) 解决办法:onDestroy方法中取消动画
- 其他解决办法:使用AAC框架
-
内存泄漏排查工具: AS Monitor,MAT,LeakCanary
-
扩展: Java内存管理,GC
-
Handler引起的内存泄漏 原因:该线程持有Handler的引用,而Handler也持有Activity的引用,这就导致了Activity不再使用时,GC回收不了Activity 解决:Handler持有的引用最好使用弱引用,在Activity被释放的时候要记得清空Message,取消Handler对象的Runnable
-
单例模式引起的内存泄漏 原因:构建该单例的一个实例时需要传入一个Context,如果此时传入的是Activity,由于Context会被创建的实例一直持有,当Activity进入后台或者开启设置里面的不保留活动时,Activity会被销毁,但是单例持有它的Context引用,Activity没法销毁 解决:对于生命周期比Activity长的对象,要避免直接引用Activity的context,可以考虑使用ApplicationContext
-
非静态内部类创建静态实例引起的内存泄漏 原因:非静态的内部类会自动持有外部类的引用,创建的静态实例就会一直持有的引用 解决:可以考虑把内部类声明为静态的
-
非静态匿名内部类引起的内存泄漏 原因:如果匿名内部类被异步线程使用,可能会引起内存泄漏 解决:可以考虑把内部类声明为静态的
-
资源对象没有关闭引起的内存泄漏 原因:资源性对象比如Cursor、File、Bitmap、视频等,系统都用了一些缓冲技术,在使用这些资源之后没有关闭 解决:处理完资源对象的逻辑记得关闭,最好是形成习惯现写一开一关
-
集合对象没有及时清理引起的内存泄漏 原因:如果集合是static、不断的往里面添加东西、又忘记去清理,肯定会引起内存泄漏 解决:集合里面的东西、有加入就应该对应有相应的删除
不能通过 GC 来解决内存泄漏问题