【疑难杂症】解决了这些问题,你就迈进了安卓高级工程师门槛
导入
或许由于经历不同,很多的开发者并不再怎么关心性能优化和代码质量这一块,而在一个真正用心做产品的公司,在产品交付前进行的集中测试会暴露出来非常多平时难以解决的问题;主要原因是这些原因在一开始会被认为太难搞定而且在当时来说并不是那么重要,最后要到发布了,由于时间紧凑而且自身积累不够,导致很多疑难杂症得不到解决;
一般这个时候公司会着急招聘一个看起来很资深的程序员来解决这个问题,不过在我看来这种选择代价并不低,排除这种靠谱的人不太好找外,这个人的开发经历和项目的契合度也有很大关系,更可怕的是,在这位资深开发接管了这个项目后,他可能会觉得很难办,因为把一个快要上线的项目源码读懂就不是一件易事,少说也得花个半年时间吧;
最稳妥的办法当然是防范于未然,当然这并不容易,假如你所在的公司对软件质量的重视程度远没有功能迭代的重视速度,那么你可能整天写代码写到完全没有空去学习并应用,或者说你需要挤出额外的时间来学习时间,这样的效率其实并不高;当然,聊胜于无,这比起那些看完了这篇文章不去做任何实践的人来说要靠谱多了;
以下的内容全部来自我对自己公司产品优化过程中遇到的一些疑难杂症,既然是疑难杂症,自然也是花了不少精力去研究整理的,当然由于可能受限于自身的经验,有些地方描述不准确或者有些地方没有提到的,一方面我自己会在后续时间慢慢更新,另一方面也欢迎大家指正互相学习,我也会把大家建议的内容更新到这个博客上;
因此建议转载这篇文章的表明转载出处,本着开源的伟大精神,让更多的开发者受益;
目录
我猜到这篇文章后面会越来越长,因此先把架构搭好,然后再慢慢完善,其实本文结构和任何一本技术类的书籍结构一致,但是并不说明这篇文章会写的那么详细,因为这篇文章是给中级以上的安卓开发人员准备的;
1. 并发与多线程
2. 集合
3. UI交互
4. 编码习惯
5. 认知误区
6. 编程心态
类型1:并发与多线程
这一类问题是我们遇到最多的问题,这些问题对于新手来说也是最头疼的问题,如果处理不当会导致更多的问题;我们现在从原理把这些东西好好梳理一下;
定义: 并发是指在一段时间内同时做多个事情,多线程并发的本质问题源于多个线程使用同一个资源,这些资源包括不限于内存,cpu,对象等;
故事
以搬砖上楼为例,如果是一个人搬50块砖,正常来说他每次可以搬10块,那么他需要5次才能搬完。
如果有5个人搬,那么只需要一次就能搬完,大大节省了搬运时间,程序运行也是一样;
但是如果有50个人同时搬运,可能因为人太多拥挤楼道导致效率反而很低下,甚至堵死楼道;导致任务协调人员没办法解决问题;
问题
- 性能问题(资源过度消耗);
- 数据安全问题(读脏数据);
- 死锁问题(线程无法退出,UI卡死);
- 线程失控问题(sleep导致线程无法及时退出,阻塞读写无法关闭);
- 乱加锁问题(不该加锁加锁,该用不同锁的用同一把锁);
1. 资源过度消耗
本因
- Cpu占用过高,从而导致手机发热严重,同时导致系统卡顿;
- 内存消耗太大:在线程中既存在堆内存消耗(new 对象,局部变量),又存在栈内存消耗(方法运行都需要在栈中开辟内存空间存放需要运行的代码);
案例1
在请求Ftp或者http上传下载的时候,为了测试极限速度,采用了很多条线程同时进行,但是由于采用的线程数过多,导致手机发热严重,同时资源消耗也大,经常被一些手机强制关闭应用;
代码
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.testBtn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startUploadTest();
}
});
}
private void startDownloadTest() {
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 1. get st ate
int arg1 = 100;
double arg2 = 12.00;
String arg3 = getArg3();
SomeState state = new SomeState(arg1, arg2, arg3);
// 2. do something else
for (int i = 0; i < state.someCount; i++) {
doSomeReport(state);
}
//3. save Result
saveResult2File();
//4. show result
showTestResult();
}
}, 0, 200);
}
}
解决
1. 禁止使用new Thread的方式产生线程,使用RXJava的小伙伴在每次创建子线程后要保存该线程的引用以便管理生命周期;
2. 通过线程池统一管理线程生命周期,在一次任务完成后记得销毁对应的线程;
3. 命名每一条线程以便于跟踪线程生命周期;
4. 线程中如果有循环特别是死循环如While,do,for,注意留出一个可以由外部线程控制运行的接口,否则会导致线程无法停止
5. 控制每一次并发的数量,并发数量超过一定值不仅不能提升效率还会导致程序拥堵以至于手机卡顿;
2. 读脏数据
本因
多个线程同时操作同一个数据(对象,内存,文件),并且这些线程不全是读线程;
案例1 ConcurrentModificationException
多发于操作ListView,当适配器还没刷新完上个数据的显示下一个数据就过来了,会抛出异常同时修改异常;
代码
// TODO 邀请大家在这里补齐示例代码
解决
在这里填写解决方案;
案例2 变量被非法修改
循环代码的状态控制标志异常;一个变量控制着线程运行,本来被关闭了,但是被另一个线程打开了;
代码
private boolean isRun = false;
private void startTest(){
new Thread(){
@Override
public void run() {
super.run();
isRun = true;
doStartTest();
}
}.start();
}
private void testClean() throws InterruptedException {
while (!isRun){
isRun = doSomeClean();
Thread.sleep(200);
}
}
解决
使用原子锁,在这里填写解决方案;
死锁
本因
代码在执行的时候发现需要请求的锁不能被释放,导致线程阻塞死锁;
案例1 锁嵌套
相同锁的锁嵌套,只要其中一条线程运行在主线程就会导致UI卡死;
代码
// TODO 邀请大家在这里补齐示例代码
解决
synchronized尽量缩小锁的范围,锁的时间尽可能短,能锁对象就不要锁类,能锁代码块就不要锁方法;
线程池管理线程,掌握每一个线程的生命周期,线程命名便于跟踪;
案例2 锁被耗时线程持有
请求的锁所在的线程进入了死循环,进入了休眠,在执行耗时操作,导致锁不能及时释放;
代码
// TODO 邀请大家在这里补齐示例代码
解决
线程失控
本因
线程运行在加锁的代码块中,外部线程无法操作该加锁线程,导致线程必须在同步代码块执行完毕后才能被外部线程访问,如果锁住的代码块是耗时操作,且控制运行的变量也处于同步代码块中,就会出现线程失控;
案例1 阻塞线程无法关闭
阻塞读写线程无法关闭,只能等待阻塞线程自己释放,如socket阻塞,文件读写阻塞等;
代码
// TODO 邀请大家在这里补齐示例代码
解决
案例2 JNI中阻塞导致anr
在主线程中调用的JNI方法阻塞,java无法中断JNI调用,必须等到jni解除阻塞状态这个过程中就会导致anr异常以致界面卡死;
代码2
// TODO 邀请大家在这里补齐示例代码
解决2
案例3
控制循环的开关在同步代码块中;
代码3
// TODO 邀请大家在这里补齐示例代码
解决3
案例4
同步代码块中有睡眠,等待操作;
代码4
// TODO 邀请大家在这里补齐示例代码
解决4
案例5
线程中有睡眠操作又不能直接把线程停掉(有其他业务参与);
比如在执行Autotest任务过程中,有一个变量IsTestRun控制了任务执行是否继续,如果在任务中执行睡眠的睡眠间隔太长,会导致无法及时读到IsTestRun的改变从而停止任务;
线程在sleep或者处于join状态下,由于处于暂停状态,所以线程没办法继续往下执行完线程代码;
代码5
// TODO 邀请大家在这里补齐示例代码
解决5
将休眠时间设置短一点;
可以通过调用interrupt终端线程,使线程归位;
类型2:集合
由于集合是数据集,因此虽然集合本身指向的内存区域不变(比如使用final修饰了集合),但是其内部的数据还是可以发生改变,可以被任何线程随意读写;这就导致了数据的不安全和不可控;
线程安全和线程不安全集合
迭代器遍历导致异常
list浅拷贝深拷贝问题
值传递引用传递问题:基本数据类型(值)+对象类型(引用),集合属于复杂对象
基本数据类型需要加锁吗?
hashMap线程不安全,多线程操作容易发生cpu超高占用;使用ConcurrentHashMap替代;
类型3:UI
- 过度绘制问题(大量UI绘制时的频繁刷新使用定时器控制)
- 布局复杂嵌套问题
- 标题重复问题(每个页面都有各自的标题)
- 适配器刷新没有统一的入口;
- 适配器没做增量更新导致过度消耗;
- 使用dip而不是dp;
- 过多使用权重实现固定布局(有些时候需要使用固定dp);
- 定时器泛滥问题;
编码风格
权限检查问题:
1.缺乏统一的权限检查入口;
代码风格问题
- 不需要增加空格对齐变量;
2.大括号结束后必须换行;
3.意思相近的代码之间不需要空行,相反逻辑不相似的代码使用空行隔开; - 在方法体编写中,局部变量尽量定义靠近逻辑代码,不要一股脑定义在最上面,这回增加查找代码的负担;
- 独立的功能放在独立的方法,不应该把逻辑内聚度很低的代码写在同一个方法中,更有甚者既在一个方法中处理功能代码又处理逻辑代码;
7.代码嵌套不能超过3层,超过了看着眼花; - 尽量精简注释,使注释更简洁的描述代码逻辑,同时禁止尾行注释,应该写在代码之上;
过多的静态变量
过多的全局变量
过多的单例使用
过多的EventBus使用
数据没有统一管理接口不知道在哪里修改删除了
经验值缺少统一管理
注释过少问题
功能耦合问题(A功能的代码写在B功能)
类名定义问题
- Activity定义成Dialog;
- 工具类/功能类做成Sercvice
- 适配器写在Activity中;
配置混乱问题
4. 配置key不统一问题:读写Sp有时候不是统一的字符串作为key,导致写入读取不匹配;
5. activity中读写配置问题;
6. 常量配置中写实现问题;
7. 常量和类关系不大问题;
强行耦合问题:
8. 适配器中编写大量的功能代码;
9. 一个方法中实现了过多功能,导致方法臃肿;
资源管理问题
10. 线程无名称问题;
11. 对象不释放问题;
配置混乱问题
12. 配置key不统一问题:读写Sp有时候不是统一的字符串作为key,导致写入读取不匹配;
13. activity中读写配置问题;
14. 常量配置中写实现问题;
15. 常量和类关系不大问题;
强行耦合问题:
16. 适配器中编写大量的功能代码;
17. 一个方法中实现了过多功能,导致方法臃肿;
资源管理问题
18. 线程无名称问题;
19. 对象不释放问题;
20.
逻辑混乱问题:
9. if else嵌套过多;
10. 方法名和参数名随意书写;
11. for循环中嵌套复杂逻辑;
4.单句不加大括号;
5.工匠精神;
6.敢于表达,要有自大精神,并以开放心态迎接批评;
需求规范问题
1.把Answer命令做进了Call命令;
发布版本的测试应该写入周报,并预留时间自己测试;
安全问题
时间问题:用户修改系统时间则获取到错误时间;
内存泄漏问题,方法中Activity作为参数容易导致内存泄漏;
类型转换问题(特别是数字转换问题)
字符串截取中正则表达式的规范问题:
广播注册未销毁导致异常;
问题跟踪
日志频繁打印问题,频繁打印的日志建议取消打印;禁止在发布版本中频繁打印堆栈信息;
频繁调用出错的异常应立即处理,如果无法处理,使用能明确出错位置的提示信息替代堆栈打印(太消耗资源);
在功能代码中不要使用trycatch,而应该将异常抛出让业务代码来处理;在业务代码中,不应该把所有的异常一股脑用Exception捕获,而应该区别对待;
认知错误
Jni不是耗时操作,最典型的System.currentTimeMillion();
Jni具备和Native相同的能力和权限;
JNI层规范日志打印。因为jvm崩溃最多只能打印是哪个jni方法调用出错;
心态
我们再开发的时候经常有以下心态,导致产出质量不高,返工率高,维护困难,据我观察,初级程序员可能会花掉1/4时间写代码,剩下的时间基本和编写无关;这种情况下可能是由于上头要的急而自己又不懂得拒绝或者急于求成,反正最终结果就是自己交出去的产品自己都不太满意,同时让自己特别的忙,没有效率;
这种错误的心态有但不限于以下:
1.急于提交心态
2.绝对正确心态
3.直面错误心态;
4.逻辑错误心态;
5.着急完成心态;
上一篇: 那个记忆中酱肉包子的做法