Senior Android Development

App研发录

#1.重构

1.1重新规划Android项目结构

将业务与逻辑分离,建立包结构。

###1.4 实体化编程

#####1.4.1使用fastJSON或GSON
坑:代码混淆时带来的混乱

  1. 加了符号Annotation的实体属性,一使用就崩溃。
  2. 当有泛型属性时,一使用就崩溃。
1
2
-keepattributes Signature //避免混淆泛型
-keepattributes *Annotation* //不混淆注释

###1.5 Adapter模板

使用ViewHolder机制

###1.6 类型安全转换函数
类型转换可能会带来的crash。
尤其是网络中json数据的转换,有可能为空,或者类型发生变化。
substring时,start和end与length之间的关系。

#2. Android网络底层框架设计

###2.1 网络底层封装

####2.1.2 AsyncTask的优缺点
用AsyncTask封装网络请求

  • 缺点:屏蔽了线程池,无法取消已经发出的网络请求;所以,当快速在App页面之间跳转时,导致网络请求队列积压,造成堵塞和延时。

###2.2 App数据缓存设计
根据实际需要对网络请求的数据进行缓存。
GET类型可以进行缓存,而POST为增删改数据,所以,不可以进行缓存。
直接写入文件缓存。

###2.3 MockService

###2.5 HTTP头
19700101 long
GZIP压缩:在HTTPRequest的头中的Accept-Encoding字段添加gzip,同样,在解析HTTPResponse时,也要判断是否存在gzip,并做相应的解析。

#3. Android经典场景设计

###3.1 App图片缓存设计
UIL:在onDestroy()里手动清理内存缓存;

1
imageLoader.clearMemoryCache();

###3.2对网络流量进行优化

#####3.2.1通信层的优化

  1. MobileAPI接口返回的数据,要使用gzip进行压缩。
  2. APP与MoblieAPI之间的数据传递,通常都是遵守JSON协议的。JSON是XML格式的,并且以字符存在,所以存在压缩空间。ProtoBuffer传输协议,是二进制格式,在大数据量传输时有有时。
  3. 避免频繁调用MobileAPI,可以一次获取数据的MobileAPI调用,尽量一次完成。
  4. MobileAPI使用的是HTTP无状态短连接。TCP协议是长连接,速度会更快。但server能支持的长连接个数不多,所以需要更多的服务器集成。
  5. 要建立取消网络请求的机制。提供cancelRequest()方法,用于在页面跳转时,清空网络请求队列。
  6. 增加重试机制。在请求失败时,进行3次重试;只对GET进行重试,POST不可以进行重试,以避免表单的重复提交。

#####3.2.2图片策略优化
server提供根据要求返回不同分辨率的图片机制,并在不同的网络环境中请求不同质量的图片。

###3.4 App与HTML5的交互
PhoneGap

###3.5 消灭全局变量
存在的问题:当内存不足的时候,系统会回收一部分闲置的资源,由于App被切换到后台,所以之前存放的全局变量很容易被回收,这时再切换到前台继续使用,在使用某个全局变量的时候,就会因为全局变量的值为空而崩溃。

#4. Android命名规范和编码规范
命名规范

编码规范

小工具:checkstyle

#5. Crash异常收集与统计

收集:把Crash收集到本地数据库。
统计:对每天线上大量的Crash进行去重、分类

5.1 异常收集

UncaughtExceptionHandler

第三方统计工具:腾讯Bugly

###5.2异常收集与统计
数据分析类型:

  1. 重复的Crash:

    • 不同设备在不同时间发出来的重复Crash。要检查是否只对某些机型或Android版本才会发生类似问题。
    • 不同设备在一个时间段发出来的重复Crash。要检查MobileAPI是否返回了脏数据而App没有使用try...catch...语句捕获到。
    • 相同设备在短时间内频繁发送重复Crash。可能是因为App没有做好本贵后的善后工作,导致在试图重启崩溃的Activity过程中发生了循环的Crash。这时需要去除重复数据。
  2. 每笔异常包含的数据信息:

    • exception_name: Crash对应的异常名称。
    • exception_stack:Crash的详细信息。

      exception_name只能作为Crash的参考标准,而产生Crash的真正原因,则隐藏在exception_stack中。

  3. exception_stack中的OutOfMemory,都是内存溢出导致的。但不包含OutOfMemory的异常,也是有可能是因为OOM导致的,例如ResourcesNotFoundException,有时就是因为OOM引起的。
  4. NullPointerException,要仔细观察后面的信息。
  5. 窗体泄露这类问题,基本都是想关闭弹出框时,发现承载它的宿主已经不在。
  6. ListView和Adapter相关的Crash基本都发生在分页获取数据的场景,数据源发生了改变,却没有及时通知ListView和Adapter。

#6. Crash异常分析

###6.1 Java语法相关的异常

#####6.1.10 不能随便使用asList
Arrays.asList()的返回值类型为java.util.Arrays$ArrayList,而不是ArrayListArrays$ArrayList没有实现addremove方法,所以在调用时就会报错,可以使用以下解决方案:

1
2
3
4
String str = "1,2,3,4,5";
List<String> list = Arrays.asList(str.split(","));
List arrayList = new ArrayList(list);
arrayList.remove("1");

#####6.1.12 NoClassDefFoundError

1
2
//In class B, we declare:
ClassA obj = new ClassA();

如果打包时,B和A分别位于不同的dex中,这是如果在A所在的dex中把A类删除了,那么在运行时执行到这句话时就会抛出NoClassDefFoundError的异常信息。

插件化编程的时候会容易产生此类异常,因为要使用DexClassLoader。

当使用第三方SDK时,也会有可能产生该类异常。

###6.2 Activity相关的异常

#####6.2.4 无法再非Activity的Context中启动Activity(不能启动BroadcastReceiver)
在service、application的Context中启动Activity时就会抛出此类异常,这是可以为intent添加flag,在新的task中启动,从而解决:

1
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

#####6.2.5 startActivityForResult不能回传
异常中关键字:

1
Failure delivering result ResultInfo{who=null,request=0,result=-1}

当在onActivityResult()中发生某些异常时,特别是和回传的值相关的操作,就会表现为此类异常,而实际上可能是空指针异常等。

###6.3序列化相关的异常

#####6.3.2序列化时未指定ClassLoader
当实现Parcelable的类中包含自定义的类时,如:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass implements Parcelable{
...
//ClassA也实现了Parcelable接口
private ClassA a;
...
private MyClass(Parcel in){
...
a = in.readParcelable(null);//有可能会引发异常
...
}
}

所以将有可能引发异常的代码修改为:

1
a = in.readParcelable(ClassA.class.getClassLoader());

ClassLoader相关概念:

当ClassLoader为空时,系统会采取默认的ClassLoader。

Android有两种不同的ClassLoader:framework ClassLoader和apk ClassLoader,其中framework ClassLoader知道怎么加载Android系统内部的类;apk ClassLoader知道怎么加载我们自己写的类,也知道怎么加载Android系统内部的类。

在App刚启动时,默认ClassLoader是apk ClassLoader,但在系统内存不足应用被系统回收会再次启动,这个默认ClassLoader会变为framework ClassLoader,所以对于我们自己的类会报ClassNotFoundException

#####6.3.3 反序列化时发现类找不到:被ProGuard混淆导致的崩溃
在ProGuard文件中keep这个类。

###6.4 列表相关的异常

#####6.4.1 Adapter数据源变化但是没通知ListView

#####6.4.2 ListView滚动时点击刷新按钮后崩溃

###6.5 窗体相关的异常

#####6.5.8 AlertDialog.resolveDialogTheme

#####6.5.10 子线程不能修改UI
子线程不能修改UI,这句话不是绝对的,当在onCreate中使用了新的线程去操作UI控件时,有可能会正常运行,也有可能会产生错误。这是因为,对于线程的检查机制还没有起作用。查看源码,可以发现,对于线程的检查实在viewRootrequestLayout()中去checkThread,而在onCreate中,可能还没有执行requestLayout()方法。

如果需要在其它线程中修改UI,则可以使用Handler或者ActivityrunOnUiThread或者使用AsyncTask

在线程中调用Looper.prepare()Looper.loop(),可以用于解决DialogToast相关的操作问题,但对于UI空间的操作,可能是无效的。

###6.6资源相关的异常

#####6.6.2 StackOverflowError
第一种原因,发生这种事情,主要是因为Layout布局文件结构嵌套层次太深。应尽量控制在5层以内。

第二种原因,在App退出的时候,如果App中有多个线程,那么在退出App的时候可能不能完全关闭App,即使使用finish方法也做不到,必须使用System.exit(0)这样的语句才可以。这是因为finish方法只能退出当前Activity,但还可能有其他Activity未关闭,这些Activity中有没结束的线程,从而会有一些资源没有释放。

无论哪种情况,最终都是由无限递归引起的。应避免这种情况的发生。

#####6.6.3 UnsatisfiedLinkError
这是因为在特定平台上的so没有被加载到而引起的,应检查对应平台的so是否存在。

#####6.6.7 TransactionTooLargeException
Binder最大通常限制为1MB,如果大于1MB的话,就会抛出TransactionTooLargeException的异常。

避免传输过大的数据量,尤其是图片以及数据采集可能带来的大的数据量。

###6.7 系统碎片化相关的异常

#####6.7.2 RemoteViews
使用场景:

  • AppWidget
  • Notification

#####6.7.8 DeadObjectException
当某对象已经被系统回收,我们却还在使用它,就会产生此类异常。

此类异常可能引起系统级错误,从而迫使Android重启,在未Root的机器上,可以达到重启Android设备的目的。

#####6.7.10 ViewFlipper引发的血案
在Activity中使用ViewFlipper控件,进行横竖屏切换操作时,就会发生这种异常。这是由于onDetachedFromWindow()onAttachedToWindow()之前被调用所致。
解决方案是,重写ViewFlipper的onDetachedFromWindow(),对super的调用进行try...catch...:

1
2
3
4
5
6
7
8
@Override
protected void onDetachedFromWindow(){
try{
super.onDetachedFromWindow();
}cathc(IllegalArgumentException e){
stopFlipping();
}
}

###6.10 其他情况的异常

#####6.10.8 Monkey点击过快导致的崩溃
增加延迟函数:

1
2
3
4
5
6
7
8
public boolean isWindowLocked(){
long current = SystemClock.elapsedRealtime();
if(current - mLastOnClickTime > 500){
mLastOnClickTime = current;
return false;
}
return true;
}

使用方法:

1
2
3
4
5
6
7
public void onClick(View v){
if(isWindowLocked()){
return;
}
// real code
...
}

#7. ProGuard技术

###7.1 ProGuard简介
ProGuard是一个开源项目,在SourceForge上进行维护。

ProGuard的四个功能:

  1. 压缩(Shrink):侦测并移除代码中无用的类、字段、方法、和特性(Attribute)。
  2. 优化(Optimize):对字节码进行优化,移除无用的指令。
  3. 混淆(Obfuscate):使用a、b、c、d这样简短而无意义的名称,对类、字段和方法进行重命名。
  4. 预检(Preveirfy):在Java平台上对处理后的代码进行预检。
1
DexGuard,可以专门用于做代码混淆,并且可以混淆字符串常量,ProGuard不会混淆字符串常量。

使用了某一版本ProGuard后,尽量不要升级,以稳定为首。

#8. 持续集成

###8.4 自动打包
搭建自动打包服务器,并提供定时打包以及手动打包页面,同时提供打包列表页,方便随时查看历史打包记录。方便测试人员测试。
选择合适的自动打包工具。

###8.9 单元测试

#9. APP竞品技术分析

###9.1 竞品分析概述

#####9.1.1 App竞品定义
参考意义:

  • 社区类和视频类App,他们的广告系统做得是最好的。

  • 电商类(OTA 和O2O)App的产品详情页和订单填写页具有很好的参考价值。

  • 活动运营做得最好的仍然是电商类App。

  • 社交类App的聊天功能做得是最好的,尤其是高并发的架构实现。

  • 新闻类App的推送的及时性和到达率是比较有价值的。

拿来主义:拿来不等于生搬硬套,理解其用意,才能取长补短。

###9.2 App安装包的结构

#####9.2.1 Android安装包的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── [-rwxr-xr-x] AndroidManifest.xml
├── [drwxr-xr-x] META-INF
├── [drwxr-xr-x] assets
├── [-rwxr-xr-x] classes.dex
├── [-rwxr-xr-x] classes2.dex
├── [drwxr-xr-x] com
├── [drwxr-xr-x] data
├── [drwxr-xr-x] lib
├── [-rwxr-xr-x] messages.properties
├── [drwxr-xr-x] org
├── [drwxr-xr-x] res
└── [-rwxr-xr-x] resources.arsc

  • resources.arscz, 这个文件是编译后的二进制资源文件的索引,也就是apk文件的资源表(索引)。

  • lib目录下的子目录存放个各个ABI相关的so文件。

  • META-INF目录下存放的是签名信息,用来保证apk包的完整性和系统的安全。但这个目录下的文件却不会被签名,从而给力我们无限的想象空间。

  • assets目录下面可以看到很多基础数据,以及一些本地会使用到的HTML、CSS和JavaScript文件。

  • res目录下的anim子目录很值得研究,这个目录存放app的所有动画效果。

res目录中的很多XML文件打开后是乱码,AndroidManifest.xml也是如此,那是因为打包的时候对XML文件进行了压缩,所以看到的往往是全角的字符和乱码,不便于查找到我们想要看的内容。有一款神器用于看到apk包中正常的内容,AXMLPrinter2.jar,它可以将apk中已经处理过的XML还原为可读格式。命令如下:

1
java -jar AXMLPrinter2.jar AndroidManifest.xml

###9.3 竞品技术一:开启速度

流程:
Splash广告 --> 引导页 --> 首页

  1. Splash广告,首先加载默认图,同时异步下载下一次将要显示的图片。下一次打开时,则直接显示上次下载的图,并同时去检查是否需要更新图片。
  2. 引导页,建议不要超过4页。可做一些动画,建议使用gif,原生动画太费时费力。
  3. 进入首页之前,可以引导用户选择一些城市或其它有价值的个别个人信息。
  4. App的首页设计,目前较为流行的做法是尽可能多的把产品放在首页。统计分析的结果是,首页的内容点击率较高。

还可以做的事情:

  1. 友盟打点统计,统计激活数。
  2. 注册推送。
  3. 如果是从消息推送点击进入的App,则要根据推送协议,跳转到具体的页面。
  4. 初始化崩溃收集机制,如果上次崩溃时没有来得及发送崩溃信息,那么这次发送。

分析工具:

嗅探器:WireShark

###9.4 竞品技术二:HTML5打开速度

#####9.4.1 把HTML5页面嵌入到Zip包中
使用WebView打开HTML5页面时,可使用以下方法加快打开速度:

  • 在app中较为频繁打开的页面,嵌入要打开的HTML5相关文件,把HTML5文件、图片、CSS和JS文件放进压缩包,嵌入到app包中。App每次启动的时候,启动一个线程异步把Zip包解压到本地目录下,然后每次从本地读取HTML5页面。
  • 为HTML5文件加入版本控制机制,每次加载之前,向server请求最新版本号并与本地做对比,如果HTML5有更新,则重新下载新的Zip包,并替换掉本地的数据。
  • 在每次App发版之前都要嵌入最新的Zip包,以防止App中的HTML5包太老,而需要每个打开app的人都去下载新的Zip包。

#####9.4.2 Zip包的增量更新机制

#####9.4.3 使用WebView预先加载HTML5并缓存到本地

###9.5 竞品技术三:安装包的大小

#####9.5.3 png和jpg的区别及使用场景
png有透明通道,而jpg没有,此外png是无损压缩的,而jpg是有损压缩的,所以png中存储的信息会很多,体积自然就大了。

对于png,一般可以对其做硬件加速,所以对于同一张图,png虽然体积比jpg大,但是加载速度却要快一些。

所以,在设备中,App包里的图推荐用png,从网络中下载的图考虑流量问题应使用jpg。

但对于使用不会特别频繁,尺寸又特别大的图,也可以考虑使用jpg。

Google发布的WebP,压缩率要高于jpg,可以使用,但在iOS中需要引入WebP解码器才能使用。

iOS中,启动页的图片必须是png的,否则会通不过审核。

#####9.5.9 字体文件的学问
字体是矢量的,所以将某些简单图案设计为字体可以有效的减小安装包体积。
可以使用下述网站将单色icon转换成字体文件:http://icomoon.io。或者使用FontLab这样的工具来制作。
然后将ttf文件放在assets目录下,将icon和十六进制编码的映射关系保存在drawable资源文件中,如:

1
<string name="font_icon_1_normal">&#xe606;</string>

或者设计成Drawble对象,然后设置给ImageView这样的控件。

###9.6 竞品技术四:性能优化

#####9.6.1 App自动选取最佳服务器的策略
遍历配置文件中的服务器列表,多次测算同一个服务器的来回走路时间,取其平均,选择最佳服务器,同时,服务器可返回服务器处理能力参考值,以CPU占用率作为参考结合网络请求返回时间,进行服务器的选择与判断。

#####9.6.2 使用TCP+Protobuf
区别于HTTP+JSON的连接方式。在一定程度上可以改善用户体验,但同时会增加服务器压力,所以也要做好切换回HTTP+JSON的机制,App端架构采用Build模式和策略模式以适应这两种机制的切换。

###9.7 竞品技术五:数据采集工具

#####9.7.2 打点统计
需要解决的两个问题:

  • 如何在发版前就能检查出漏打的和打错的点。
  • 如果在筏板后发现漏打的和打错的店,快速修复快速上线,而不必等新版本发布。

打点的分类:

  • 页面打点
  • 事件打点

#####9.7.3 ABTest
定义:对某一页面的UI样式修改或者数据呈现,使用两种方案测试效果,经数据采集比较后,确定使用哪一种方案。

###9.8 竞品技术六:热修复
Android可以使用插件化编程,但比较费时费力,iOS不可以使用插件化编程。

所以较为合适的方案是在Native和HTML5之间灵活的切换。

#####9.8.2 使用脚本编程
在iOS中使用Lua进行编程。
Android中也有AndroLua开源项目、Dexposed等可以做热修复。

###9.9 竞品技术七:曲径通幽

#####9.9.1 一切皆可配置

  1. 使用XML或JSON配置首页,防止因加载不到数据而没有入口。
  2. 配置页面的公共行为,如:请求网络时是否需要显示进度条,进度条中是否有取消按钮,点击取消按钮后退到上一页还是停留在当前页面,请求错误时是否显示错误提示等。

#####9.9.2 APP后门
用途:

  • 做一个能切换服务器的页面,在开发期间可以在测试环境和线上环境之间切换。
  • 测试某个页面请求了哪些网络请求,打印出调用这些接口时输入的参数和返回JSON数据。
  • 收集App崩溃信息,方便测试期间追踪错误来源。
  • 提供一个后门页面供HTML5团队进行调试。
  • 对APP进行流量统计。
  • 对APPP进行电池电量消耗测试。

#####9.9.3 使用META-INF目录打渠道包
打包混淆签名等耗费时间过长,如果在AndroidManifest.xml中定义meta的方式来获取渠道名称,需要每次重新打包,如果打包数量过多,则耗费数小时时间。

可以在META-INF目录下放置一个空文件,文件名则为渠道名,每次去读这个文件的名字即可。因为META-INF目录下文件的修改不需要重新签名,所以使用脚本批量操作就会很快。

###9.11 竞品技术九:第三方SDK

#####9.11.1 HTML5

  • PhoneGap
  • WebViewJavascriptBridge.js
  • zepto.js
  • CryptoJS
  • mraid.js

#####9.11.2 iOS

#####9.11.3 Android

  • aSmack XMPP协议的开源库
  • EventBus

#####9.11.4 others

  • Pinyin4j 汉字转换为拼音
  • Countly 统计分析平台

#10. 项目管理决定了开发速度

###10.1 团队建设:

  • 产品经理
  • 开发
  • 测试

开发与测试的比例应大约在6:1

#####10.1.1 测试团队的工作:

  • 召开测试用例评审会
  • 手动测试
  • 全功能回归测试
  • 探索性测试
  • 渠道包测试
  • MobileAPI发布上线前的测试工作
  • 压力测试
  • Monkey测试
  • 客户投诉回访

#####10.1.3 开发人员
抱怨最多的:

  • 一句话的需求。
  • 开发过程中,产品需求频繁变动。
  • 产品经理搞不清楚业务逻辑,直到开发过程中才发现有问题。
  • UI设计图、切图、标注图不到位。

#####10.2 优化团队结构,让敏捷流程跑得更快

#####10.3 App敏捷开发
Lua

#####10.7 总结
团队管理:弱管理,有一个主题,自由发挥。

项目管理:强管理,实时跟进项目进度,保证项目进度。

20个技术点

  1. Activity相关。App应用开发,以Activity使用最多,涉及LaunchMode、onSaveInstanceState、生命周期等技术。
  2. Fragment相关技术。用的人不少,想明白咋回事的人不多。这里推荐一本书《Creating Dynamic UI with Android Fragments》.
  3. 序列化技术。有Parcelable和Serializable两种。前者是基于Service的,后者是基于Bundle的,二者实现原理不同,但是达到的效果差不多。
  4. ImageLoader的原理和使用。类似的,还可以学习Facebook新近开源的Fresco,它对图片的处理会更好一些。
  5. fastJSON或GSON的使用。做App不会用实体自动匹配JSON,相当于白做。
  6. 多线程相关。包括Handler、Looper、ExecutorService等。
  7. Adapter和ListView。这两个技术捆在一起,经常容易崩溃,尤其是分页的时候,要仔细研究深刻领会。
  8. 用户Cookie设计。需要把登录机制彻底搞清楚,包括在HttpRequest头中夹带Cookie来进行用户身份验证的技术。
  9. 网络请求封装。使用AsyncTask的网络底层封装,使用Handler+Runnable的网络底层封装。
  10. Android与HTML5的交互。包括Android调用HTML5的方法,以及HTML5调用Android的方法。
  11. 代码混淆。没用过ProGuard,不知道keep相关语法,就还是初级水平。
  12. Android打包机制。涉及Android SDK中的若干命令。对Android打包过程做的每一件事都很清楚。进一步是Android多项目依赖的打包技术。Ant、Gradle或者Maven,掌握其中一种打包机制即可。
  13. 线上Crash分析并修复。要具备分析Crash信息修复线上Crash的能力。
  14. 内存泄露。包括内存优化、内存泄露的场景、MAT工具的使用。
  15. 调试工具。包括DDMS、Eclipse或者Android Studio的调试功能。
  16. Monkey机制。Android开发人员如何对一款App进行Monkey测试。这算是附加技能吧。
  17. 单元测试。这里指的是JUnit。对复杂的算法写过单元测试以保证其没有问题。
  18. GIT的高级功能。包括Stage、Rebase、Revert、Stash、Cherry Pick和Sub Module等概念。如果项目中使用的是SVN,那么要掌握SVN的版本管理策略。
  19. 插件化编程。哪怕知道一点DexClassLoader的概念也好。这年头,没做过插件化编程,出门面试都不好意思说自己是做Android开发的。
  20. 设计模式。对常见的设计模式如工厂、生成器、适配器、代理、策略模式耳熟能详。