不看不知道,原来经常写的代码存在很多的安全问题,安全这一块着实不简单(我是这么认为的),很多原理其实是看不太懂,只怪自己水平不够,买的几本Android进阶书还没看….哎~~意识到了时间的宝贵
Handler内存泄漏
在使用Handler的时候,我们经常会写下面的一段代码,但是这样会导致严重的内存泄漏问题
1 | public class SampleActivity extends Activity { |
问题分析
当Android应用程序启动时,framework会为该应用程序的主线程
创建一个Looper
对象。这个Looper对象包含一个简单的消息队列Message Queue
,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序framework事件,例如Activity生命周期方法调用、button点击等,这些消息都会被添加到消息队列中并被逐个处理。
另外,主线程的Looper对象会伴随该应用程序的整个生命周期。
然后,当主线程里,实例化一个Handler
对象后,它就会自动与主线程Looper
的消息队列关联
起来。所有发送到消息队列的消息Message都会拥有一个对Handler的引用
,所以当Looper来处理消息时,会据此回调[Handler#handleMessage(Message)]
当使用内部类(包括匿名类)来创建Handler的时候,Handler对象也会隐式地持有一个外部类对象(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)
这里补充一点java知识:
在java里,
非静态内部类
和匿名类
都会潜在的引用(持有)它们所属的外部类。但是,静态内部类
却不会。
个人理解:要不然你内部类怎么可以使用外部类的变量?还有.this这种形式
所以:
只要有未处理的消息,那么消息会引用handler(通常在后台线程中),非静态的handler又会引用外部类,即Activity,导致Activity无法被回收,造成泄漏。
同理,如果你用匿名Runnable来实现消息的发送,它属于非静态匿名类,同样会引用外部类。
一般情况下,我们是等耗时操作完成后进行发送消息,进行处理,这时候Activity应该要正常销毁的(也可能在等待过程中用户退出了Activity),上面的代码会造成其他的对象持有Activity的引用导致Activity无法被GC回收,导致内存泄漏。
解决方法
通过程序逻辑来进行保护
- 在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
- 如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
1 | public void onDestroy() { |
handler是线程通讯工具类。用于传递消息。它有两个队列:
1.消息队列
2.线程队列
消息队列使用sendMessage和HandleMessage的组合来发送和处理消息。
线程队列类似一段代码,或者说一个方法的委托,用户传递方法。使用post,postDelayed 添加委托,使用 removeCallbacks移除委托。
这里看到两张图不错:
将Handler声明为静态类
如果我们改为静态类,那么它不会持有外部的引用,Activity可以被GC回收了,就这样:
1 | static class MyHandler extends Handler { |
但是又出现了一个问题,应该注意到了,不会持有 Activity 的引用那么怎么可能操作 Activity 中的对象呢?我们可以加一个Activity 的弱引用 (WeakReference)
1 | static class MyHandler extends Handler { |
知识补充
什么是WeakReference?
WeakReference弱引用,与强引用(即我们常说的引用)相对,它的特点是,GC在回收时会忽略掉弱引用,即就算有弱引用指向某对象,但只要该对象没有被强引用指向(实际上多数时候还要求没有软引用,但此处软引用的概念可以忽略),该对象就会在被GC检查到时回收掉。对于上面的代码,用户在关闭Activity之后,就算后台线程还没结束,但由于仅有一条来自Handler的弱引用指向Activity,所以GC仍然会在检查的时候把Activity回收掉。这样,内存泄露的问题就不会出现了。
关于静态内部类
静态内部类只能访问外部类的静态成员。
静态内部类的对象可以直接生成:Outer.Inner in=new Outer.Inner();而不需要通过生成外部类对象来生成。静态内部类和非静态内部类一样,都是在被调用时才会被加载
所以可以这么理解:调用外部类的静态变量,静态方法可以让外部类得到加载,不过这里静态内部类没有被加载
其他地方ViewHolder内部类 定义为静态的,是一种好习惯。
其他
很多人认为,静态变量能不用就不用,一旦静态生命周期必然很长,大部分情况下Handler用来更新UI,既然Activity被关闭了,那么Handler也没必要存在了,这种情况下也许采用第一种方法比较更好。
但是也有说静态内部类和静态变量还是有区别的,这里并不会有什么大问题….
静态内部类编译的时候是跟外部类同一级别的,使用static就有了限制,占用资源也就小了
还有就是,貌似 Android 对弱引用的支持并不太好,所以还是尽量少用
参考
java静态内部类加载
https://my.oschina.net/rengwuxian/blog/181449
http://www.jianshu.com/p/cb9b4b71a820
代码混淆
一般情况下我们使用ProGuard工具来提供代码混淆
PS:因为特殊原因这里是在Eclipse下进行混淆,现在基本都是AS了,那个以后再来补充,其实都差不多,AS下更加简单吧..
ProGuard是什么
ProGuard是一个工具,用来混淆和优化Java代码。
工作方式:移除无效的代码,将代码中的类名、函数名替换为晦涩难懂的名字。
注意,它只能混淆Java代码,Android工程中Native代码,资源文件(图片、xml),它是无法混淆的。
玩过Android逆向的知道,经过混淆后的类反编译出来名都是单个字母,完全看不懂
如何开启混淆
修改Android工程根目录下的project.properties
文件,把proguard.config=….这一行前面的注释“#”去掉。
这一行指定了系统默认的proguard配置文件,位于Android SDK/tools/proguard目录下。
当然,你也可以自己编写配置文件,但不建议这样做,因此系统默认的配置已经涵盖了许多通用的细节,如果你还有额外的配置,可以添加在 proguard-project.txt
文件中。
注意: 只有在生成release版本的apk时,混淆配置才会起作用,debug版本的apk不会进行混淆。
那些需要手动配置
系统默认的配置已经涵盖了大部分的内容,但是如果你的工程中有如下内容,则需要手动添加配置到proguard-project.txt文件中。
- 只在 AndroidManifest.xml 引用的类
- 通过JNI回调方式被调用的函数
- 运行时动态调用的函数或者成员变量
- 反射用到的类
- WebView中JavaScript调用的方法
- Layout文件引用到的自定义View
- 一些引入的第三方库(一般都会有混淆说明的) 这里推荐两个开源项目,里面收集了一些第三方库的混淆规则
当然,如果你不确定哪些需要手动配置,可以以默认的配置生成程序,当运行中发现ClassNotFoundException异常时,即可找到哪个类不该被混淆。
手动配置的规则
手动添加的配置,一般以“-keep”开头
1 | //不混淆Test的构造函数 |
参考
http://ticktick.blog.51cto.com/823160/1413066
https://segmentfault.com/a/1190000004461614
Activity安全
有些说实话还没完全看懂,是太心急了么?逼不得已…
exported
不准备对外公开的activity一定要设置为非公开,以防止被人非法调用(反编译下很容易就能找到,至于非法调用用于什么坏事我的安全意识还想不出来,反正关掉就是了,也许是用来做钓鱼页面)
同时,一定要注意的是, 非公开的Activity不能设置intent-filter
因为,如果假设在同一机器上,有另外一个app有同样的intent-filter的话, 调用该Activity的intent会唤醒Android的选择画面, 让你选择使用那个app接受该intent。这样就会事实上绕过了非公开的设置。
默认值:如果包含有intent-filter 默认值为true
; 没有intent-filter默认值为false
。
1 | <activity |
如果设置了导出权限,都可能被系统或者第三方的应用程序直接调出并使用。 组件导出可能导致登录界面被绕过、信息泄露、数据库SQL注入、DOS、恶意调用等风险。
主要作用是:是否支持其它应用调用当前组件。
更多请参考:android:exported 属性详解
合理的使用exported
我们接下来谈谈在开发中如何更合理设置exported。
Activity被调用的场景分为3种:封闭式、半封闭式和开放式
封闭式
被调用的Activity与调用的Context必须在同一个App,其他任何App不能调用
这种是我们最常见的Activity,有2种情况:没有intent-filter情况
可以不设置exported或者设置exported为false1
2
3
4
5
6
7
8<activity
android:name=".SecondActivity"
android:label="@string/app_name" />
<!-- 或 -->
<activity
android:name=".SecondActivity"
android:label="@string/app_name"
android:exported="false"/>没有intent-filter情况
必须设置exported为false1
2
3
4<activity
android:name=".SecondActivity"
android:label="@string/app_name"
android:exported="false"/>
半封闭式
被调用的Activity只能被部分其他App调用,如同一个公司的2个App之间
这种场景下,除了满足封闭式设置外,还必须把调用App和被调用App设置相同的uid,即在2个App的AndroidManifest.xml添加相同的android:sharedUserId,如1
2
3<manifest xmlns:android="http://schemas.android.com/apk/res/android"
...
android:sharedUserId="com.example.categorytest">开放式
可以被任何App调用 这种场景主要是对外接口,如微信、微博的分享接口。大多数情况下,这样的Activity都会有intent-filter,因此也没必要显式地把exported设为true,不设是可以的,当然不能设为false。
但如果你没有intent-filter,那就必须显式地把exported设为true。 当然,对于三方app接口的intent-filter设置还有一些要求,如在隐式intent调用必须添加android.intent.category.DEFAULT
补充:
关于主Activity,应用程序需要包含至少一个Activity组件来支持MAIN操作和LAUNCHER种类,即为主Activity
暴露的Activity组件不包括主Activity,如果你把主Activity设置exported为false了,那你的应用就甭想运行了,正常的应用来说。
参考:
http://blog.csdn.net/gorgle/article/details/51420586
http://www.itdadao.com/articles/c15a12377p0.html
不要指定taskAffinity
Android中的activity全都归属于task管理 , 简单说来task是一种stack(堆栈)的数据结构, 先入后出。
一般来说, 如果不指明归属于什么task, 同一个app内部的所有Activity都会存续在一个task中,task的名字就是app的packageName。
因为在同一个andorid设备中,不会有两个同packageName的app存在,所以能保证Activity不被攻击。
1 | <activity android:name=".Activity1" |
恶意软件中的Activity如果也声明为同样的taskAffinity,那他的Activity就会启动到你的task中,就会有机会拿到你的intent(我们一般会在intent中对数据进行加密处理)
那么taskAffinity到底什么用呢?
它的作用是描述了不同Activity之间的亲密关系。拥有相同的taskAffinity的Activity是亲密的,它们之间在相互跳转时,会位于同一个task中,而不会新建一个task!
简单说就是意味着这activity更喜欢哪个TESK
一个新的activity,默认地启动到调用startActivity()方法的activity的task中。它和调用者放到同样的back stack中。然而,如果传递给startActivity()的intent包含
FLAG_ACTIVITY_NEW_TASK
标志,系统将会需找一个不同的task来容纳新的activity。通常,它是一个新的task。然而,不是必须都是如此的。如果已经存在一个和新的activity具有相同的affinity的task,新activity会启动到该task中。如果没有,它会启动一个新的task。当一个activity它的
allowTaskReparenting
属性设置为true
这种情况,activity可以从它启动的task移到和它有相同affinity的task,当该task来到前台的时候。
不要指定LaunchMode(默认standard模式)
Android中Activity的LaunchMode分成以下四种
- Standard
这种方式打开的Activity不会被当作rootActivity,会生成一个新的Activity的instance(实例),会和打开者在同一个task内 - singleTop
和standard基本一样,唯一的区别在于如果当前task第一个Activity就是该Activity的话,就不会生成新的instance - singleTask
系统会创建一个新task(如果没有启动应用)和一个activity新实例在新task根部,然后,如果activity实例已经存在单独的task中,系统会调用已经存在activity的 onNewIntent()方法,而不是存在新实例,仅有一个activity实例同时存在。 - singleInstance
和singleTask相似,除了系统不会让其他的activities运行在所有持有的task实例中,这个activity是独立的,并且task中的成员只有它,任何其他activities运行这个activity都将打开一个独立的task。
所有发送给root Activity(根Activiy)的intent都会在android中留下履历(入侵关键,大概)。所以一般来说严禁用singleTask或者singleInstance来启动画面。
然而,即使用了standard来打开画面,也可能会出问题,比如如果调用者的Activity是用singleInstance模式打开,即使用standard模式打开被调用Activity,因为调用者的Activity task是不能有其他task的, 所以android会被迫生成一个新的task,并且把被调用者塞进去,最后被调用者就成了rootActivity。
1 | <application |
FLAG_ACTIVITY_NEW_TASK
发给Activity的intent不要设定为FLAG_ACTIVITY_NEW_TASK
就算上面的Activity的lauchMode设置完善了, 在打开intent的时候还是能指定打开模式。
比如在intent中指明用FLAG_ACTIVITY_NEW_TASK模式的话,发现该activity不存在的话,就会强制新建一个task。如果同时设置了FLAG_ACTIVITY_MULTIPLE_TASK
+FLAG_ACTIVITY_NEW_TASK
,就无论如何都会生成新的task,该Activity就会变成rootActiviy,并且intent会被留成履历
Intent中数据的加密
这个前面提到过,Activity中数据的传递都依靠intent, 很容易被攻击, 所以 就算同一个app内部传递数据, 最好还是要加密, 加密算法很多。
明确ActivityName发送Intent
避免被恶意软件所截取,主要是注意在不同APP直接的数据发送
1 | Intent intent = new Intent(); |
不是指明了packageName和ActivityName就能避免所有的问题,如果有一个恶意软件故意做成和你发送目标同PackageName, 同ActivityName, 此时的intent就会被截取.
对于这两种方式经查看源码发现 Intent 的setClass() 方法的实现正是使用ComponentName 类:
1 | public Intent setClass(Context packageContext,Class<?> cls){ |
接收intent时明确对方的身份
一个好方法是比对对方的app的hashcode。
当前,前提是调用者要用startActivityForResult(),因为只有这个方法,被调用者才能得到调用者的packageName
intent数据泄漏到LogCat
1 | //Intent中发送的数据就会被自动写入LogCat |
其他
注意下这个权限<uses-permission android:name="android.permission.GET_TASKS" />
有了这个权限就能取出这台手机上所有task上所有根Activity接受到的intent,大概
所以:所有根Activity中的intent都能被所有app共享
补充
关于上面介绍的那些每一个其实都大有学问呐,不过都没深入进去追究,目前水平也是有限。
这里看到了LaunchMode,再补充下它的应用场景吧:
singleTop适合接收通知启动的内容显示页面。
例如,某个新闻客户端的新闻内容页面,如果收到10个新闻推送,每次都打开一个新闻内容页面是很烦人的。
singleTask适合作为程序入口点。
例如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
singleInstance适合需要与程序分离开的页面。
例如闹铃提醒,将闹铃提醒与闹铃设置分离。
singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。TaskAffinity对LaunchMode的影响:
不指定TaskAffinity,singleTask会在默认的task 中执行,这个符合预期,一般也都是这么用的,不需要指定。
不指定TaskAffinity,singleInstance之后启动的页面不能放倒singleInstance所在那个task中,会放倒默认的task中,不过一般singleInstance也不适合作为程序中间页。
参考
http://blog.csdn.net/thestoryoftony/article/details/9370427
http://droidyue.com/blog/2015/08/16/dive-into-android-activity-launchmode/index.html
评论框加载失败,无法访问 Disqus
你可能需要魔法上网~~