最近事情比较多,很遗憾这是一篇迟来的分享。
DiyCode技术沙龙 是由广州社区的成员合力举办的一场Android技术分享会,因为获得了一张免费的门票,加上分享的主题都比较感兴趣,所以去参加学习一下,总的来说干货满满,见识了很多的技术大牛。
内存管理优化
Low Memory Killder(LMK)
常见的进程优先级:
- 前台进程(Foreground Process)
- 可见进程(Visbile Process)
- 服务进程(Service Process)
- 后台进程(Background Process)
- 空进程(Empty Process)
oom_adj的值越高,优先级越低,越容易被系统回收
查看oom_adj
在adb shell 中,输入如下两个命令即可:
- ps | grep 包名
- cat /proc/进程pid/oom_adj
示例:
E:\asWorkSpace>adb shell
shell@hnSCL-Q:/ $ ps | grep com.android.browser
u0_a7 26894 353 1716624 138140 ffffffff 00000000 S com.android.browser
u0_a7 26963 353 1538696 49280 ffffffff 00000000 S com.android.browser:service
shell@hnSCL-Q:/ $ cat /proc/26963/oom_adj
1
shell@hnSCL-Q:/ $
其中的26894和26963分别是浏览器两个进程的pid
StrictMode(严苛模式)
什么是StrictMode ?
StrictMode(严苛模式)是用来检测程序中违例情况的一个开发者工具,最常用的场景是检测主线程中本地磁盘和网络读写等耗时操作。
能检测哪些?
严苛模式主要用来检测两个问题,一个是线程策略,即ThreadPolicy,另一个是VM策略,即VmPolicy。
ThreadPolicy
- 自定义的耗时调用,使用detectCustomSlowCalls()开启
- 磁盘读取操作,使用detectDiskReads()开启
- 磁盘写入操作,使用detectDiskWrites()开启
- 网络操作,使用detectNetwork()开启
VmPolicy
- Activity泄漏,使用detectActivityLeaks()开启
- 未关闭的Closable对象泄漏,使用detectLeakedClosableObjects()开启
- 泄漏的Sqlite对象,使用detectLeakedSqliteObjects()开启
- 检测实例数量,使用setClassInstanceLimit()开启
在哪里开启?
严苛模式的的开启可以放在Application或者Activity以及其他组件的onCreate方法。为了更好地分析应用中的问题,建议放在Application的onCreate方法。
简单的开启方式?
第一种,通过代码的方式:
if (IS_DEBUG && Build.VERSION.SDK_INT >= 9) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
StrictMode.setVmPolicy(new VmPolicy.Builder().detectAll().penaltyLog().build());
}
严苛模式需要在Debug模式开启,不要在Release版本中启用。同时,严苛模式自API19开始引入,某些方法也从API11开始引入,使用时需注意API级别。
第二种,通过在手机开发者选项中开启严苛模式,开启之后,如果主线程中有执行时间长的操作,屏幕会闪烁。
常见问题及优化方案
- 使用合适的Context,一般注册第三方框架或者SDK时采用Application的Context,除非特地要求传Activity的Context
- 多进程应用需要避免Appication onCreate多次执行引起的重复初始化
- 图片资源放对位置(优先考虑主流设备,优先考虑用户分布多的设备)
图片加载优化
1.inSampleSize(降低采样率) 2.BitmapRegionDecoder(加载超级大图) 3.Matrix(小图放大) 4.LruCache/LinkedHaspMap(缓存控制,避免oom) 5.选择合适的图片加载框架(UIL、Fresco、Glide、Picasso) 6.按需显示,优先显示缩略图,需要时显示大图 7.优化加载图片的时机
主动释放内存
关键函数: onTrimMemory(int level) ,这是4.0以后提供的一个API,系统提供的回调有 - Application.onTrimMemory() - Activity.onTrimMemory() - Fragement.OnTrimMemory() - Service.onTrimMemory() - ContentProvider.OnTrimMemory() 参数level代表不同的内存状态: - TRIM_MEMORY_COMPLETE:内存不足,并且该进程在后台进程列表最后一个,马上就要被清理 - TRIM_MEMORY_MODERATE:内存不足,并且该进程在后台进程列表的中部 - TRIM_MEMORY_BACKGROUND:内存不足,并且该进程是后台进程 - TRIM_MEMORY_UI_HIDDEN:内存不足,并且该进程的UI已经不可见了 以上4个是4.0新增 - TRIM_MEMORY_RUNNING_CRITICAL:内存不足(后台进程不足3个),并且该进程优先级比较高,需要清理内存 - TRIM_MEMORY_RUNNING_LOW:内存不足(后台进程不足5个),并且该进程优先级比较高,需要清理内存 - TRIM_MEMORY_RUNNING_MODERATE:内存不足(后台进程超过5个),并且该进程优先级比较高,需要清理内存 以上3个是4.1新增
选择合适的时机,对View和资源解绑,在合适的时机再恢复
- 释放缓存
- 小心未关闭的Dialog,在Activity和Fragment销毁时记得先dismiss
- 匿名内部类和非静态内部类泄漏,用静态外部类和弱引用的方式取而代之
避免创建大量的临时对象而造成内存抖动,考虑复用
- 高频执行函数中避免创建大量临时对象,如View的onDraw和onTouch等 - StingBuilder、StringBuffer代替String
WebView造成内存泄漏
Android系统和各家的ROM本身存在的问题造成的,最好的处理方式是将承载WebView的界面独立到其他进程,在合适的时机选择杀死WebView进程
自身内存监控(来自腾讯开发者提供的一种方案)
- Runtime.getRuntime().getMaxMemory() Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() - 定期检查上边的比例值,达到一定峰值时调用此API触发内存释放 WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
更多
- 注册和反注册(BroadcastReceiver、Observer) - 关闭资源(Cursor、IO流) - 使用优化过的数据结构SparseXXXX - 考虑使用Parcelable取代Serializable - 建立缓存池,如ListView复用View的思路 - 开辟多进程(当然,也不是越多越好) - 借助lint来规避一些常见的编码问题
动态更新
随着App日益庞大以及越来越复杂的逻辑,不仅需要团队多人的协作开发还有方法数越界等问题,因此,插件化以此衍生出而来,它的好处有以下几个方面:
- 插件模块动态升级
- 改进大型App的架构
- 实现多团队协作开发,各自发布
因为对这一块接触了解的并不是很多,在此不作更详细的记录。
hotfix
所谓热补丁(hotfix),它可以让应用在无须重新安装下能够自动更新。对于热补丁的应用,可能我们会存在几个等级的需求:
- Bugfix : 简单的bugfix,解决一些空指针之类的问题
- UI : 改变UI,支持资源的变更
- features : 功能的发布,甚至一定情况下面代替升级
对于现在市面上的热修复方案,大致可以分为两个流派,第一个是native派,它肯定是使用native方式实现,第二个是Java派,如kkfix可以hook系统的方法,robust是AOP的方式。
那么问题又来了,热补丁技术哪家强?其实各个方案都有自己的优缺点,世上没有最好的方案,只有只适合自己的方案
- AndFix方案
这套方案主要是在Native层替换方法的实现,关键挑战是兼容性问题,底层的代码每个版本都有改动,可能有些厂商也会去改这块代码,所以这套方案的使用场景存在较大的限制
优点:1、立刻生效 缺点:1、兼容性
2、性能影响小 2、Native定位复杂
3、补丁相对较小 3、应用场景首先
4、支持大部分的加固场景
总结: 适合Bugfix场景
- QZone方案
主要是利用了android classloader查找类的顺序,当出现重复类的时候,优先使用补丁中修改过的类。这套方案有个问题是dalvik在加载修改过的类的时候会出现一个crash,当时的解决方案是用插桩的方式。这套方案的特点是非常简单,而且兼容性不错。
优点:1、兼容性较好 缺点:1、性能损耗
2、应用场景广 2、补丁可能会过大
3、简单,成功率高 3、无法立刻生效
4、支持大部分的加固场景
总结: 可实现Features发布
- Tinker方案
在审视大量方案后,发现gradle的instant run 和 facebook buck exopacakage方案都使用了全量合成的做法,于是想到了推送差异Dex的方式。
优点:1、兼容性较好 缺点:1、Dalvik Rom体积较大
2、应用场景广 2、单独的合成过程
3、补丁较小 3、无法立刻生效
4、性能损耗小 4、不支持加固(通过回退Qzone方案)
总结: 可实现Features发布
- Robust方案
关键挑战:super方法调用问题;Proguard内联
优点:1、立即生效 缺点:1、应用场景受限
2、性能影响小 2、修改源码
3、补丁较小 3、安装包变大(5%左右)
4、支持大部分加固场景
总结: 适合Bugfix场景
各种方案的对比:
App安全
代码安全
- 完整性校验:检查classex.dex、apk的完整性,最好将哈希值存储到服务器
- 防逆向分析:代码混淆、加壳保护
- 防进程注入:防止ptrace进行so注入,并hook任意函数
传输安全
- HTTPS=HTTP+SSL/TLS
- 苹果已宣布从2017年起所有IOS应用将强制使用HTTPS
- HTTP/2.0也只支持HTTPS
- 防止中间人攻击:SSL Pinning
- 防止降级攻击
关于URL签名的一般处理方式:
- 将所有参数按参数名进行升序排序;
- 将排序后的参数名和值拼接成字符串stringParams,格式:key1value1key2value2…;
- 在上一步的字符串前面拼接上请求URL的Endpoint,字符串后面拼接上AppSecret,即stringUri+StringParams+AppSecret;
- 使用AppSecret为密钥,对上一步的结果字符串使用HMAC算法计算MAC值,这个MAC值就是签名。
URL示例:
http://api.domain.com/users/123?appKey=qwer&token=asfe×tamp=23456789
AppSecret:nmefj
参数排序:appKey、timestamp、token
参数拼接:appKeyqwertimestamp23456789tokenasfe
URL拼接:users/123appKeyqwertimestamp23456789tokenasfenmefj
计算MAC值:reRwV
最终URL:http://api.domain.com/users/123?appKey=qwer&token=asfe×tamp=23456789&sign=reRwV
存储安全
常用的存储方式:
- SharedPreferences
- Internal Storage
- External Storage
- SQlite Databases
- keystore/keychain
- 硬编码
- so文件
存储的安全标准:
- 敏感数据不能存在外部存储器上,无论是否加密
- 私有目录数据正确设置权限
- 铭感数据不能以明文存储在私有目录
敏感数据安全
密码:
- 不要在客户端本地保存
- 网络传输加密:MD5、加盐MD5、HMAC、AES、RSA
- 数据库保存:加盐MD5,盐值和MD5分开保存
密钥:
- 保存到SharedPreferences
- 硬编码到代码中
- 加密保存到配置文件中
- NDK开发,放在so文件中,加解密操作都在so文件,并添加签名认证
TOKEN
- 用户登录成功后返回Token,一般有两个:accessToken和和refreshToken
- Token需要设置有效期,accessToken的有效期不能设置太长,最好不超过一周,refreshToken可以长一点
- accessToken过期后,通过refreshToken更新accessToken
- refreshToken过期后,则需要用户重新登录
一些感受
额,做技术的人好像蛮容易秃顶的…