想要一個程式永遠的活著不被kill掉,這個觀點備受駁議。對於開發來說,想要實現的一些需求,保證訊息的到達率,這一項的實現是無可厚非;對於使用者來說,被不知名的服務一直生存在我的後臺程式。就像隔壁老王在藏在衣櫃裡偷偷輸出,是不是很難受!有些程式,又不能強制使用者給你加入白名單,那能怎麼辦呢?只能用一些黑科技了。
但是Android系統中在沒有白名單的情況下做一個任何情況下都不被殺死的應用是基本不可能的,只能大幅度提高程式的存活率。強制性的流氓手段,有些不道德。更何況我也不會QAQ~ 我也只不過看了大牛們的技術部落格,自己寫幾個demo,演示記錄一下而已。 #程式的級別 首先,要了解程式的級別。Android的應用中,程式可分為:
- 前臺程式,在手機頁面可被看見的。如:正在被onResume()的Activity;繫結該Activity的Service;正執行其 onReceive()的BroadcastReceiver。
- 可見程式,雖然可被看見,但是在執行onPause()方法中Activity。如:彈出dialog被遮擋的Activity;繫結該Activity的Service也級別也隨之降低。
- 服務程式,獨立於應用的程式或者是私有程式,且正在執行 startService() 方法啟動的服務。
- 後臺程式,不可見Activity,執行了onStop()方法。
- 空程式,不包含任何活躍的應用元件的程式
(盜用一下網上的圖)
對應的不同級別的程式,系統都有一個oom_adj的值來標識,不同程式的級別高低。
綠色標識不容易被系統回收的程式。系統當處於記憶體緊缺時候,需要通過Lowmemorykiller ,kill掉一些對系統來說無關緊要的程式。斬殺的規則就是,根據oom_adj大小。從最大的開始,當兩個oom_adj大小一樣時,先kill佔用記憶體大的那個。
#ADB Shell使用
哇,第一次使用adb命令,感覺low爆了。要看程式活著還是死的,總得有個地方可以檢視的嘛。原來Android Studio 命令框可以這麼用的。用真機除錯時候,adb shell 進入的並不是root,對應的角標“$”,其許可權也只能看到對應程式的列表,只有手機root之後或者用模擬器才能看到程式的詳細資訊。角標也是“#”
可以通過
cat /proc/程式id/oom_adj
來檢視。返回了一個值,對應上面那張表,可直接看出對應的程式所處在的級別。我們要做的是,對我們需要的程式,希望在其被置為不可見時候,其程式級別不會被降低,或者說不被降很多。所以也就有了一下的“黑科技”。
#一個畫素的Activity 這個方法,好像在網上已經人盡皆知了的樣子。實現原理,也就是監聽手機螢幕的開啟和關閉,來開啟和關閉這個1畫素的Activity。這個需求只能做到,螢幕熄黑後,程式的級別不會被降低。
//螢幕點亮關閉監聽
interface ScreenStateListener {
void onScreenOn();
void onScreenOff();
}
/**
* screen狀態廣播接收者
*/
private class ScreenBroadcastReceiver extends BroadcastReceiver {
private String action = null;
@Override
public void onReceive(Context context, Intent intent) {
action = intent.getAction();
if (Intent.ACTION_SCREEN_ON.equals(action)) { // 開屏
mListener.onScreenOn();
} else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // 鎖屏
mListener.onScreenOff();
}
}
}
複製程式碼
這個是1畫素Activity建立的管理類。因為該Activity頻繁被建立和銷燬,所以將其放置弱引用堆疊裡,便於記憶體回收。
public class ScreenManager {
private Context mContext;
//弱引用,LiveActivity在應用可視下finish,建立頻繁
private WeakReference<Activity> mActivityWref;
public static ScreenManager gDefualt;
public static ScreenManager getInstance(Context pContext) {
if (gDefualt == null) {
gDefualt = new ScreenManager(pContext.getApplicationContext());
}
return gDefualt;
}
private ScreenManager(Context pContext) {
this.mContext = pContext;
}
public void setActivity(Activity pActivity) {
mActivityWref = new WeakReference<Activity>(pActivity);
}
public void startActivity() {
LiveActivity.actionToLiveActivity(mContext);
}
public void finishActivity() {
//結束掉LiveActivity
if (mActivityWref != null) {
Activity activity = mActivityWref.get();
if (activity != null) {
activity.finish();
}
}
}
}
複製程式碼
LiveService 的具體操作,在應用的程式開啟時候同時開啟這個Service,對螢幕關閉開啟進行監控
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//螢幕關閉的時候啟動一個1畫素的Activity,開屏的時候關閉Activity
final ScreenManager screenManager = ScreenManager.getInstance(LiveService.this);
ScreenBroadcastListener listener = new ScreenBroadcastListener(this);
listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() {
@Override
public void onScreenOn() {
screenManager.finishActivity();
}
@Override
public void onScreenOff() {
screenManager.startActivity();
}
});
return START_REDELIVER_INTENT;
}
複製程式碼
主要的原理就熄屏時候,偷偷的開啟著一個無介面的Activity,來告訴手機我這個程式是活躍著的。在螢幕亮起時候,則銷燬這個Activity,由現有的介面UI取代。但是我發現當應用被置於後臺後,應用的oom_adj直接變為6(後臺程式),所以滿足不了程式級別一直保持。我也很可愛的,在程式進入後臺也開啟這個1畫素,我在Application生命週期中,對這個Activity進行一樣的操作。奈何。。當我進入後臺後,過了一會,app又被重新拉起,顯示在頁面。關不掉了哈哈~
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
final ScreenManager screenManager = ScreenManager.getInstance(this);
screenManager.finishActivity();
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
screenManager.startActivity();
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
}
複製程式碼
既然不能滿足,那隻能換種方法思路。。。。。 #設定為前臺服務 據說微信用的是這個方案,實現原理是利用了Android的前臺漏洞。對API<180時,呼叫startForeground(ID, new Notification()),這樣就不就有有圖示顯示。在API>18時,需要呼叫一個輔助的Service,繫結同一個ID,然後並且stop 這個輔助Service,這樣通知欄也不會有任何顯示
public class KeepLiveService extends Service {
public static final int NOTIFICATION_ID=0x11;
public KeepLiveService() {
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onCreate() {
super.onCreate();
//API 18以下,直接傳送Notification並將其置為前臺
if (Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN_MR2) {
startForeground(NOTIFICATION_ID, new Notification());
} else {
//API 18以上,傳送Notification並將其置為前臺後,啟動InnerService
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(NOTIFICATION_ID, builder.build());
startService(new Intent(this, FuZhuService.class));
}
}
}
複製程式碼
public class FuZhuService extends Service{
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//傳送與KeepLiveService中ID相同的Notification,然後將其取消並取消自己的前臺顯示
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(NOTIFICATION_ID, builder.build());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
stopForeground(true);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(NOTIFICATION_ID);
stopSelf();
}
},100);
}
}
複製程式碼
最後當應用點選後退後,退置後臺時候,該應用的程式保持0變為1的程度,也就保證了程式的存活率
#系統自帶的服務 在Service的onStartCommand方法中返回一個整型,系統識別這個整型,進行判斷,如果被kill將會進行說明操作。@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_REDELIVER_INTENT;
}
複製程式碼
-
START_STICKY 如果系統在onStartCommand返回後被銷燬,系統將會重新建立服務並依次呼叫onCreate和onStartCommand(注意:根據測試Android2.3.3以下版本只會呼叫onCreate根本不會呼叫onStartCommand,Android4.0可以辦到),這種相當於服務又重新啟動恢復到之前的狀態了)。
-
START_NOT_STICKY 如果系統在onStartCommand返回後被銷燬,如果返回該值,則在執行完onStartCommand方法後如果Service被殺掉系統將不會重啟該服務。
-
START_REDELIVER_INTENT START_STICKY的相容版本,不同的是其不保證服務被殺後一定能重啟。
使用後好像不怎麼靈巧,保留使用。。。。 #總結
總結一波,作為一個有情懷的開發者,我們都知道每當使用者關閉一個程式時,我們的程式就應該徹底地死去並釋放其所佔用的系統資源,這個淺顯的道理不僅適用於我們移動應用開發,也適用於任何桌面程式的開發。
但是作為開發者,我們更多的是面對產品經理開發,針對各種市場資源需要,總會想盡辦法。對於常駐型程式,在使用的時候,還是得三四而後行。當然特殊需求可以特殊考慮,為了使用者體驗更好的是沒話說的。 這篇文章更多的是站在網上各路大牛們的基礎上閱讀,尚未達到創造的級別。慢慢努力。自此,感謝網上的大們牛寫的黑科技1 黑科技2