Java 基础知识
基础知识
基本类型 | 大小 | 最小值 | 最大值 | 包装器类型 |
---|---|---|---|---|
boolean | - | - | - | Boolean |
char | 16-bit | Unicode o | Unicode 2^16 -1 | Character |
byte | 8-bit | -128 | 127 | Byte |
short | 16-bit | -2^15 | 2^15-1 | Short |
int | 32-bit | -2^31 | 2^31-1 | Integer |
long | 64-bit | -2^63 | 2^63-1 | Long |
float | 32-bit | -2^31 | 2^31-1 | Float |
double | 64-bit | -2^63 | 2^63-1 | Double |
void | - | - | - | Void |
bit 位 byte 字节
1 字节 = 8 位
1 Byte = 8 Bits
1 KB = 1024 Bytes
1 MB = 1024 KB
1 GB = 1024 MB
集合
线程
IO
Android 基础知识整理
1、dp/sp
2、res/raw 与 assert
3、Android 数据存储方式:
- SharePreference
- Internal Storage
- External Storage
- SQLite Databases
- Network Connection
基础数据结构与算法
性能优化
1、性能检测工具
2、布局优化
- 避免 xml 布局文件过多层级
- 避免重绘
- 有的布局在需要显示的时候再加载 ViewStub (ViewStub 继承自 View,是 VIew 的一种,没有大小,也没有绘制,所以基本不消耗内存,只在需要显示的时候才会加载进内存显示)
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/activity_main"/>
- 重用布局使用 include、merge
3、内存优化
- 避免不必要对象的创建
-
- 在需要使用拼接字符串的情况下,使用 StringBuilder 或 StringBuffer 来进行拼接,而不是使用”+”进行拼接;因为使用加号连接符会创建多余的对象
-
- 在没有特殊说明下,使用基本数据类型替代封装类,int 比 Integer 更高效
-
- 在不需要访问一个对象中的某些字段时,只是想调用它的某些方法去完成一项通用的功能时,可以将该方法设置为静态方法,避免了为了调用发而创建对象
-
- 对常量使用 static final 修饰
避免内存泄漏
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费。
Java 内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期的对象已经不再需要但是因为长生命周期持有它的引用而导致不能被回收。具体主要有如下几大类:-
- 静态集合类引起内存泄漏:
像 HashMap、Vector 等的使用最容易发生内存泄漏,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象也不能被释放,因为他们一直被 Vector 引用着
- 静态集合类引起内存泄漏:
static ArrayList<Object> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Object o = new Object();
list.add(o);
o = null;
}
这个例子中,循环申请 Object 对象,并将申请的对象放到 ArrayList 中,如果仅仅释放引用本省( o = null ),那么 ArrayList 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加到了集合中,还必须从集合中删除,最简答的方法就是将集合对象设置为 null。
-
- 当集合里面的对象属性被修改后,在调用 remove() 方法是不起作用。
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧", "pwd1", 25);
Person p2 = new Person("孙悟空", "pwd2", 26);
Person p3 = new Person("猪八戒", "pwd3", 27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:" + set.size() + " 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:" + set.size() + " 个元素!"); //结果:总共有:4 个元素!
for (Person person : set) {
System.out.println(person);
}
-
- 监听器
在 Java 编程中,我们需要和监听器打交道,通常一个应用中会用到很多监听器,我们会调用一个控件的诸如 addXXXListener() 方法来增加监听器,但往往在释放对象的时候却没有记住要删除这些监听器,从而增加了内存泄漏的机会
- 监听器
-
- 各种连接
比如数据库连接,网络连接和 IO 连接,除非显示的调用了其 close() 方法将其连接关闭,否则是不会自动被 GC 回收的。
- 各种连接
-
- 内部类和外部模块的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外还要小心外部类不经意的引用,例如程序员A负责 A 模块,调用了 B 模块的一个方法如:public void registerMsg(Object o);这种调用就要非常小心了,传入了一个对象,很可能模块 B 就保持了对该对象的引用,这时候就需要注意模块 B 是否提供了相应的操作去除引用。
- 内部类和外部模块的引用
-
- 单例模式
不正常使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果到哪里对象持有外部的引用,那么这二个对象就不能被 JVM 正常回收,导致内存泄漏:
- 单例模式
class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}
显然B采用 singleTon 模式,它持有一个 A 对象的引用,而这个 A 类的对象将不能被回收。
-
- Application 的 Context 与 Activity 的 Context 的使用
其中,NO1 表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。而对于 Dialog 而言,只有在 Activity 中才能创建
- Application 的 Context 与 Activity 的 Context 的使用
-
- Handler 造成的内存泄漏
使用Handler 造成内存泄漏的问题是非常常见的,很多时候我们在进行线程间通信是会使用 Handler,但是对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外我们知道 Handler、Message、MessageQueue 都是互相关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程的 MessageQueue 一直持有。
由于 Handler 属于 TLS(Thread Local Storage)变量,生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。
举个例子:
- Handler 造成的内存泄漏
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message ,mLeakHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish 掉时,延迟执行的任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,此时 finish 掉的 Activity 就不会被回收了从而导致内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,这里就是指 SampleActivity)。
修改方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 Context 传进去
(在 AS 中,创建Handler 时会有提示信息:Handler reference leaks Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.)
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
综述,即推荐使用 静态内部类 + WeakReference 这种方式。每次使用前注意判空。
在使用Handler 时,在 Activity 执行了 onDestroy 或 onStop 时应该移除 MessageQueue 中的消息。
handler.removeCallbacksAndMessages(null);
图片加载优化
图片的编码格式
- ALPHA_8:每个像素占用1byte内存
- ARGB_4444:每个像素占用2byte内存
- ARGB_8888:每个像素占用4byte内存
- RGB_565:每个像素占用2byte内存
所以一个图片的分辨率是 3648*2736,采用 ARGB_8888,那么占用的内存空间就是 3648*2736*4 = 33MB,直接加载这张图肯定会内存溢出。那么应该如何处理呢?
- 降低图片加载到内存时的图片大小(分辨率)
- 采用更节省内存的编码,例如 ARGB_4444(RGB_565)
- 如果是加载大量图片的话,可以采用缓存
大图加载
对于大图的加载,可以先对大图进行缩放后在加载进内存:
- BitmapFactory.options 类介绍
BitmapFactory.Options 是 BitMapFactory 的一个内部类,它主要用于设定与存储 BitMapFactory 加载图片的一些信息:
- options.inJustDecodeBounds:如果设置为 true,将不把图片的像素数组加载到内存,仅加载一些额外的数据(如图片大小)到 Options 中。
- options.outHeight: 图片的高度
- options.outWeight: 图片的宽度
- options.inSampleSize:如果设置,图片将依据此采样率进行加载,不能设置小于1的数,例如设置为4,分辨率宽和高将变为原来的1/4,这时候整体所占的内存将是原来的1/16。
- options.inDither: 设置为 false 不进行图片抖动处理
- options.inPreferredConfig:设置为 null 让解码器以最佳方式解码
//大图片压缩为宽,高200px的图像展示
BitmapFactory.Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(rs, R.drawable.a2,options);
options.inPreferredConfig = Bitmap.Config.ARGB_4444;
options.inSampleSize = calculateInSampleSize(options, 200, 200);
//获取压缩倍数
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(rs,
R.drawable.a2,options);
iv.setImageBitmap(bitmap); //图片绑定
图片压缩
对图片进行质量压缩
//进行有损压缩 核心代码
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int options_ = 100;
actualOutBitmap.compress(Bitmap.CompressFormat.JPEG, options_, baos);//质量压缩方法,把压缩后的数据存放到baos中 (100表示不压缩,0表示压缩到最小)
int baosLength = baos.toByteArray().length;
while (baosLength / 1024 > maxFileSize) {//循环判断如果压缩后图片是否大于maxMemmorrySize,大于继续压缩
baos.reset();//重置baos即让下一次的写入覆盖之前的内容
options_ = Math.max(0, options_ - 10);//图片质量每次减少10
actualOutBitmap.compress(Bitmap.CompressFormat.JPEG, options_, baos);//将压缩后的图片保存到baos中
baosLength = baos.toByteArray().length;
if (options_ == 0)//如果图片的质量已降到最低则,不再进行压缩
break;
}
图片缓存
缓存
优雅的构建 Android 项目之磁盘缓存(DiskLruCache)
Android缓存机制&一个缓存框架推荐&LruCache源码分析
内存缓存
LruCache
LRU-Least Recently Used,即最近最少使用,是一种非常常用的置换算法,也即淘汰最长时间未使用的对象。LRU 在操作系统的页面置换算法中广泛使用,我们的内存或缓存空间是有限的,当新加入一个对象时,造成我们的缓存控件不足了,此时就需要根据某种算法对缓存中原有数据进行淘汰或删除,而 LRU 选择的是将最长时间未使用的对象进行淘汰。LurCache 实现原理
根据 LRU 算法的思想,要想实现 LRU 最核心的是要有一种数据结构能够基于访问顺序来保存缓存中的对象,这样我们就能欧很方便的知道哪个对象是最近访问的,哪个对象是最长时间未访问的。LRUCache 选择的是 LinkedHashMap 这个数据结构,LinkedHashMap 是一个双向循环链表,在构造 LinkedHashMap 时,通过一个 boolean 值来指定 LinkedHashMap 中保持数据的方式,
/*
* 初始化LinkedHashMap
* 第一个参数:initialCapacity,初始大小
* 第二个参数:loadFactor,负载因子=0.75f
* 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
init();
this.accessOrder = accessOrder;
}
显然,在 LruCache 中选择的是 accessOrder = true;此时当 accessOrder 设置为 true 时,每当我们更新(即调用 put 方法)或访问(即调用 get 方法)*map 中的结点时,LinkedHashMap 内部就会将这个结点移动到链表的尾部,因此,在链表的尾部是最近刚刚使用的结点,在链表的头部是最近最少使用的结点*,当我们的缓存空间不足时,就应该持续把链表头部结点移除掉,直到有剩余空间放置新结点。
磁盘缓存
- Google 提供的磁盘缓存解决方案:DiskLRUCache
这个类没有放到 Android 的源码中,在 https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
在使用了 DiskLRUCache 进行磁盘缓存的缓存目录中会有一个 journal 文件,这个 journal 文件是 DiskLruCache 的一个日志文件,即保存着每张缓存图片的操作记录,journal 文件正是实现 DiskLruCache 的和兴。看到出现了 journal 文件,基本可以说明这个 APP 使用了 DiskLruCache 缓存策略了。
- journal 文件
journal 日志文件到底保存了什么信息呢?一个标准的 journal 日志文件信息如下;
libcore.io.DiskLruCache //第一行,固定内容,声明
1 //第二行,cache的版本号,恒为1
1 //第三行,APP的版本号
2 //第四行,一个key,可以存放多少条数据valueCount
//第五行,空行分割行
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
前五行称为journal日志文件的头,下面部分的每一行会以四种前缀之一开始:DIRTY、CLEAN、REMOVE、READ。
以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。以DIRTY这个这个前缀开头,意味着这是一条脏数据。每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
在CLEAN前缀和key后面还有一个数值,代表的是该条缓存数据的大小。
DiskLruCache 工作流程:
-
- 初始化:通过 open() 方法,获取 DiskLruCache 实例,在 open 方法中通过 readJournal() 方法读取 journal 日志文件,根据 journal 日志文件信息建立 map 中的初始化数据;然后再调用 processJournal() 方法对刚刚建立起的 map 数据进行分析,根系的工作一个是计算当前有效缓存文件(即被 CLEAN 的)大小,一个是清理无用缓存文件
-
- 数据缓存与数据获取:上面的初始化工作完成后,我们就可以在程序中进行数据的缓存功能和获取缓存的功能了;缓存数据的操作是借助 DiskLruCache.Editor 这个类完成的,这个类也是不能 new 的,需要调用 DiskLruCache 的 edit() 方法 获取实例,在写完之后,需要进行 commit():
new Thread(new Runnable() {
@Override
public void run() {
try {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl); //MD5对url进行加密,这个主要是为了获得统一的16位字符
DiskLruCache.Editor editor = mDiskLruCache.edit(key); //拿到Editor,往journal日志中写入DIRTY记录
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imageUrl, outputStream)) { //downloadUrlToStream方法为下载图片的方法,并且将输出流放到outputStream
editor.commit(); //完成后记得commit(),成功后,再往journal日志中写入CLEAN记录
} else {
editor.abort(); //失败后,要remove缓存文件,往journal文件中写入REMOVE记录
}
}
mDiskLruCache.flush(); //将缓存操作同步到journal日志文件,不一定要在这里就调用
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
注意每次调用edit()时,会向journal日志文件写入DIRTY为前缀的一条记录;文件保存成功后,调用commit()时,也会向journal日志中写入一条CLEAN为前缀的一条记录,如果失败,需要调用abort(),abort()里面会向journal文件写入一条REMOVE为前缀的记录。
获取缓存数据是通过get()方法实现的,如下一个简单示例:
try {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl); //MD5对url进行加密,这个主要是为了获得统一的16位字符
//通过get拿到value的Snapshot,里面封装了输入流、key等信息,调用get会向journal文件写入READ为前缀的记录
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImage.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
第三方库简介与原理分析
常用控件
RecyclerView 分析
设计模式相关
- 单例模式
优点:系统内存中该类只存在一个对象,节省系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不能使用 new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。
使用场景:
- 需要频繁的进行创建和销毁的对象
- 创建对象时耗时过多或耗费的资源过多,但有经常用到的对象
- 工具类对象
- 频繁访问数据库或文件的对象
架构
MVC
MVP
MVP在Android项目中的简单体现
工具
检测OOM的工具
- LeakCanary
- AS 的 Memory Monitor
其他
参考文章
Android 开发工程师面试指南 https://www.diycode.cc/wiki/androidinterview
极客导航 http://www.jikedaohang.com/mianshi.html
ListView 图片加载优化 http://blog.csdn.net/lancees/article/details/8563680
面试题 https://hit-alibaba.github.io/interview/