在上一篇文章java中你確定用對單例了嗎?中提到單例可以被惡意的破壞,如序列化破壞和反射破壞單例的結構
,好的,這個有點偏,確實在實際開發中基本也不會在意到這個問題,但是誰叫我們搞的是java,所以這個問題我們有必要知道下,這算是提高下自己的安全意識,有句古話是這樣說的,居安思危嘛.
好,請帶著歡樂
的心情繼續往下看.
通過反射破解單例結構
java中你的單例是不是一直在裸奔,估計你用的是假的單例.
我們就使用普通懶漢式
來做示例吧.
public class SingletonDemo6 implements Serializable{
private static SingletonDemo6 s1;
//普通懶漢式寫法
public static synchronized SingletonDemo6 getInstance() {
if (s1 == null) {
s1 = new SingletonDemo6();
}
return s1;
}
看看下面測試結果.
在正常情況下,沒毛病,輸出結果一毛一樣.
@Test
public void test() throws Exception{
SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
SingletonDemo6 nomarlInstance2 = SingletonDemo6.getInstance();
//這兩個單例輸入的實例都是一樣
System.out.println(nomarlInstance1);
System.out.println(nomarlInstance2);
log:
com.relice.singleton.SingletonDemo6@5a10411
com.relice.singleton.SingletonDemo6@5a10411
當反射遇上單例
看下面反射破解單例的測試代碼,輸出兩個不同的結果.
@Test
public void test() throws Exception{
SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
Class<SingletonDemo6> forName = (Class<SingletonDemo6>) Class
.forName("com.relice.singleton.SingletonDemo6");
Constructor<SingletonDemo6> c = forName.getDeclaredConstructor();
//繞過權限管理,獲取private
c.setAccessible(true);
//通過反射拿到`SingletonDemo6`的實例
SingletonDemo6 reflectInsatnce = c.newInstance();
// 兩者的輸出結是不一樣的
System.out.println(nomarlInstance1);
System.out.println(reflectInsatnce);
log:
com.relice.singleton.SingletonDemo6@5a10411
com.relice.singleton.SingletonDemo6@2ef1e4fa
如何解決這種問題?
大神說遇到問題不要急,先分析問題出現的原因.
-
forName.getDeclaredConstructor();
主要就是獲取無參數構造. - 也就是說通過反射拿到了私有構造方法從而再次創建實例.
知道問題的原因那就好辦了.
我們可以在SingletonDemo6
的構造方法里做判斷,避免他再次創建實例.
// 解決反射 多獲取對象問題
private SingletonDemo6() {
if (s1 != null) {
try {
throw new RuntimeException("禁止反射獲取對象");
} catch (Exception e) {
e.printStackTrace();
}
}
}
這樣如果有人想要通過反射破壞單例結構,那就會拋出運行時異常.
log:
java.lang.RuntimeException: 禁止反射獲取對象 at
com.relice.singleton.SingletonDemo6.<init>(SingletonDemo6.java:24)
通過序列化破解單例結構
還是用SingletonDemo6
來測試,通過序列化獲取到實例,得出了兩個不一樣的結果.
憋說話,繼續看問題.
@Test
public void test() throws Exception{
SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
//把對象寫入文件
File file = new File(
"/xxx/xxx/xxx/xxx/xxx/SingletonDemo/a.txt");
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(nomarlInstance1);
oos.close();
fos.close();
//序列化把對象讀取
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
SingletonDemo6 serilizeInstance = (SingletonDemo6) ois.readObject();
System.out.println(nomarlInstance1);
System.out.println(serilizeInstance);
}
log:
com.relice.singleton.SingletonDemo6@68de145
com.relice.singleton.SingletonDemo6@27fa135a
如何解決這種問題?
老規矩我們還是先分析.
- 在序列化里我們可以通過流的方式將一個對象寫入內存中
oos.writeObject
,因此也就可以將這個對象從內存中讀取出來. - 但是當序列化遇到單例問題就發生了,在讀取對象時jvm會重新給序列化對象分配地址.
- 因此我們要考慮的問題就是反序列化
解決方法:
當反序列化的時候:
JVM會調用readObject方法,將我們剛剛在writeObject方法序列化好的屬性,反序列化回來. 然后在readResolve方法中,我們也可以指定JVM返回我們特定的對象(不是剛剛序列化回來的對象). .該方法的分析見
private Object readResolve() throws ObjectStreamException{
return SingletonDemo6.s1;
}
可能我們會考慮到一個問題,就是之前的反射不是在構造方法里處理解決問題嗎,那是不是序列化也可以?
要知道序列化和反序列化,在java中是使用字節碼技術生成對象,并不會執行構造器方法.
android開發中要注意的問題
接觸過android的都知道,在其中四大組件中就有三大組件是有生命周期的,生命周期最關鍵的的就是context
,連基本的Activty 和 Service都是從Context派生出來的,也因為這生命周期讓android應用在用戶體驗上附上了一些生命氣息,,如視頻播放根據生命周期來處理播放狀態;如我們想邊聽音樂邊干些別事情,這是Service的生命周期就有幫我們做到等..
我想說的就是.
android組件中的生命周期是尤其重要,因此我們要善待context
,在處理或者使用到組件的生命周期時也要注意規范,提高容錯率.
Android開發 單例模式導致內存泄露
實際開發中用到最多的設計模式,如果單例設計模式認第二,我想沒有敢認第一的.如工具類,application類,配置文件等.
不扯淡了,以工具類為例,存在內存泄露問題的一些代碼片段像下面這樣:
public class Util {
private Context mContext;
private static Util mInstance;
private Util(Context context) {
this.mContext = context;
}
public static Util getInstance(Context context) {
if (mInstance == null) {
synchronized (Util.class) {
if (mInstance == null) {
mInstance = new Util(context);
}
}
}
return mInstance;
}
}
其實實際開發中排查問題和定位問題一直是占據了大部分的工作時間,因此擁有一個好的開發方式可以減少很多不必要的時間浪費,這里有篇關于使用android studio檢查內存泄漏的文章覺得不錯.
分析下問題:
-
Util.getInstance(this);
這個this使用的就是Activity的context. - Activity的生命周期都是比較短暫的,當用戶切換頁面的時候基本都會把activity銷毀掉,因此貫穿整個生命周期的context類也會被相應的相會.
- 而
Util.getInstance(mContext);
在工具類里封裝一些耗時的操作也是常見的,當Activity生命周期結束,但Util類里面卻還存在A的引用 (mContext),這樣Activity占用的內存就一直不能回收,而Activity的對象也不會再被使用.從而造成內存泄漏
.
解決問題:
在Activity中,可以用
Util.getInstance(getApplicationContext());
或Util.getInstance(getApplication());
來代替。
因為Application的生命周期是貫穿整個程序的,所以Util類持有它的引用,也不會造成內存泄露問題。使用弱引用讓這個引用自動被回收
弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。
下面代碼是使用了弱引用之后
public class WeakRUtil {
private static Context mContext;
private static WeakRUtil mInstance;
private WeakRUtil(Context context) {
this.mContext=context;
}
public static WeakRUtil getInstance(Context context) {
if (mInstance == null) {
WeakReference<Context> actWeakRF = new WeakReference<Context>(context);
//通過get來獲取弱引用關聯對象,如果為null 則就是被回收了
mContext = actWeakRF.get();
synchronized (WeakRUtil.class) {
if (mInstance == null) {
mInstance = new WeakRUtil(mContext);
}
}
}
return mInstance;
}
public void test() {
System.out.println("util_test");
}
}
在java中,用java.lang.ref.WeakReference
類來表示。
當調用了System.gc(); 則即使內存足夠,該引用內的數據都會被回收;
好了,我們繼續總結下:
- 單例的優點就是提供了對唯一實例的受控訪問,減少內存分配,提高系統性能,也因為這個優點所以我們要避免單例被惡意的破壞掉了其結構.
- 單例在實際開發中經常使用到,而使用方法也是各種各種,為了讓代碼有更好的健壯性,因此一些開發中的編程習慣要養成,避免如oom異常.