Android--判斷App處於前臺還是後臺的方案
很多場景下,都需要判斷某個App處於前臺還是後臺。本文集網上編寫的前臺判斷方案於一體。
目前,有6種方案:
方 法 |
判斷原理 | 需要許可權 | 可以判斷其他應用位於前臺 | 特點 |
① | RunningTask | 否 | Andorid4.0系列可以,5.0以上機器不行 | Android5.0此方法被廢棄 |
② | RunningProcess | 否 | 當App存在後臺常駐的Service時失效 | 無 |
③ | ActivityLifecycleCallbacks | 否 | 否 | 簡單有效,程式碼最少 |
④ | UsageStatsManager | 是 | 是 | 需要使用者手動授權 |
⑤ | AccessibilityService | 否 | 是 | 需要使用者手動授權 |
⑥ | 自解析/process | 否 | 是 | 當/proc目錄下的檔案過多時,過多的IO操作會引起耗時 |
接下來,就對以上6種方法展開詳細說明:
目錄
1. RunningTask
1.1 原理
當一個App處於前臺時,會處於RunningTask這個棧的棧頂,所以可以取出RunningTask棧頂的任務程序,與需要判斷的App的包名進行比較,來達到目的。
1.2 程式碼實現
這種方法不僅能獲取到前臺程序的包名還能獲取到activity名稱。
public String getForegroundActivity() { ActivityManager mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); if (mActivityManager.getRunningTasks(1) == null) { Log.e(TAG, "running task is null, ams is abnormal!!!"); return null; } ActivityManager.RunningTaskInfo mRunningTask = mActivityManager.getRunningTasks(1).get(0); if (mRunningTask == null) { Log.e(TAG, "failed to get RunningTaskInfo"); return null; } String pkgName = mRunningTask.topActivity.getPackageName(); //String activityName = mRunningTask.topActivity.getClassName(); return pkgName; }
1.3 方案缺點
getRunningTask方法在5.0以上已經被廢棄,只會返回自己和系統的一些不敏感的task,不再返回其他應用的task,用CI方法來判斷自身App是否處於後臺仍然有效,但是無法判斷其他應用是否位於前臺,因為不能再獲取資訊。
2. RunningProcess
2.1原理
通過runningProcess獲取到一個當前正在執行的程序的List,我們遍歷這個List中的每一個程序,判斷這個程序的一個importance 屬性是否是前臺程序,並且包名是否與我們判斷的APP的包名一樣,如果這兩個條件都符合,那麼這個App就處於前臺。
2.2 程式碼實現
以下code是判斷當前應用是否在前臺:
private static boolean isAppForeground(Context context) {
ActivityManager activityManager =
(ActivityManager)context.getSystemService(Service.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcessInfoList =
activityManager.getRunningAppProcesses();
if (runningAppProcessInfoList == null) {
Log.d(TAG,"runningAppProcessInfoList is null!");
return false;
}
for(ActivityManager.RunningAppProcessInfo processInfo : runningAppProcessInfoList) {
if (processInfo.processName.equals(context.getPackageName())
&&(processInfo.importance ==
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND)) {
return true;
}
}
return false;
}
以下code是判斷在前臺的是哪個應用:
public String getForegroundApp(Context context) {
ActivityManager am =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningAppProcesInfo> lr = am.getRunningAppProcesses();
if (lr == null) {
return null;
}
for (RunningAppProcessInfo ra : lr) {
if (ra.importance == RunningAppProcessInfo.IMPORTANCE_VISIBLE
|| ra.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return ra.processName;
}
}
return null;
}
getRunningAppProcess方法只能獲取前臺包名。
2.3 方案缺點
Android5.0之後已經被廢棄。
例如,在聊天型別的App中,常常需要常駐後臺來不間斷地獲取伺服器的訊息,就需要把Service設定成START_STICKY,kill後會被重啟(等待5s左右)來保證Service常駐後臺。如果Service設定了這個屬性,這個App的程序就會被判斷為前臺。程式碼表現為
appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
上述code永遠成立,這樣就永遠無法判斷到底那個是前臺了。
3.ActivityLifecycleCallbacks
3.1 原理
AndroidSDK14在Application類裡增加了ActivityLifecycleCallbacks,我們可以通過這個Callback拿到App所有Activity的生命週期回撥。
public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity activity, Bundle savedInstanceState);
void onActivityStarted(Activity activity);
void onActivityResumed(Activity activity);
void onActivityPaused(Activity activity);
void onActivityStopped(Activity activity);
void onActivitySaveInstanceState(Activity activity, Bundle outState);
void onActivityDestroyed(Activity activity);
}
知道這些資訊,我們就可以用更官方的辦法來解決問題,只需要在Application的onCreate()裡去註冊上述介面,然後由Activity回調回來執行狀態即可。
Android應用開發中一般認為back鍵是可以捕獲的,而Home鍵是不能捕獲的(除非修改framework),但是上述方法從Activity生命週期著手解決問題,雖然這兩種方式的Activity生命週期並不相同,但是二者都會執行onStop();所以並不關心到底是觸發了哪個鍵切入後臺的。另外,Application是否被銷燬,都不會影響判斷的正確性。
3.2 程式碼實現
(1)AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mytest.example.com.broadcaststudy">
<application
android:name=".TestActivityLifecycleApplcation"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".SendBroadcastActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SecondActivity">
</activity>
</application>
</manifest>
(2) TestActivityLifecycleApplication.java
package mytest.example.com.broadcaststudy;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import android.util.Log;
/**
* Created by Maureen on 2018/1/18.
*/
public class TestActivityLifecycleApplcation extends Application {
private final String TAG = "TestActivityLifecycleApplcation";
private static TestActivityLifecycleApplcation mTestActivityLifecycleApplcation;
private int mActivityCount = 0;
@Override
public void onCreate() {
super.onCreate();
mTestActivityLifecycleApplcation = new TestActivityLifecycleApplcation();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
Log.d(TAG,"onActivityCreated");
}
@Override
public void onActivityStarted(Activity activity) {
Log.d(TAG,"onActivityStarted");
mActivityCount++;
}
@Override
public void onActivityResumed(Activity activity) {
Log.d(TAG,"onActivityResumed");
}
@Override
public void onActivityPaused(Activity activity) {
Log.d(TAG,"onActivityPaused");
}
@Override
public void onActivityStopped(Activity activity) {
Log.d(TAG,"onActivityStopped");
mActivityCount--;
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
Log.d(TAG,"onActivitySaveInstanceState");
}
@Override
public void onActivityDestroyed(Activity activity) {
Log.d(TAG,"onActivityDestroyed");
}
});
}
public static TestActivityLifecycleApplcation getInstance( ) {
if (null == mTestActivityLifecycleApplcation)
mTestActivityLifecycleApplcation = new TestActivityLifecycleApplcation();
return mTestActivityLifecycleApplcation;
}
public int getActivityCount( ) {
return mActivityCount;
}
}
(3) SendActivity.java
package mytest.example.com.broadcaststudy;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
/**
* Created by ATC6111 on 2018/1/18.
*/
public class SecondActivity extends Activity {
private final String TAG = "SecondActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG, "onRestart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
}
}
(4) SendBroadcastActivity.java
package mytest.example.com.broadcaststudy;
import android.app.Activity;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import java.util.List;
public class SendBroadcastActivity extends Activity {
private static final String TAG = "SendBroadcastActivity";
private static final String ACTION_MAUREEN_TEST = "com.maureen.test";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG,"onCreate");
setContentView(R.layout.activity_main);
Intent intent = new Intent();
intent.setAction(ACTION_MAUREEN_TEST);
//intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
Log.d(TAG,"Begin to sendBroadcast:");
sendBroadcast(intent);
Log.d(TAG,"Send broadcast end!");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG,"onStart");
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG,"onRestart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG,"onResume");
startActivity(new Intent(this, SecondActivity.class));
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG,"onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG,"onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
}
}
在講解為什麼不在onActivityResumed( )與onActivityPaused( )中對activity進行計數,而是在onActivityStarted()和onActivityStopped( )中對activity進行計數之前。
先看以下幾種情況的activity生命週期:
A、啟動App,進入SendBroadcastActivity:
01-01 05:33:56.401 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityCreated
01-01 05:33:56.401 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onCreate
01-01 05:33:56.427 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: Begin to sendBroadcast:
01-01 05:33:56.430 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: Send broadcast end!
01-01 05:33:56.431 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStarted
01-01 05:33:56.431 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStart
01-01 05:33:56.432 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityResumed
01-01 05:33:56.432 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onResume
B、點選back鍵退出SendBroadcastActivity:
點選back鍵退出App:
01-01 05:35:37.983 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityPaused
01-01 05:35:37.983 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:35:38.035 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStopped
01-01 05:35:38.036 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop
01-01 05:35:38.036 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityDestroyed
01-01 05:35:38.036 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onDestroy
C、在A情況下點選home鍵:
01-01 05:37:34.690 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityPaused
01-01 05:37:34.690 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:37:34.708 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivitySaveInstanceState
01-01 05:37:34.708 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStopped
01-01 05:37:34.708 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop
D、在A情況下點選recent鍵kill程序:
從recent中kill程序:
01-01 05:38:17.867 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityPaused
01-01 05:38:17.867 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:38:17.914 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivitySaveInstanceState
01-01 05:38:17.914 11816-11816/mytest.example.com.broadcaststudy D/TestActivityLifecycleApplcation: onActivityStopped
01-01 05:38:17.914 11816-11816/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop
E、新增code在SendBroadcastActivity的onResume( )中啟動SecondActivity:
在SendBroadcastActivity的onResume函式中啟動SecondActivity:
01-01 05:57:05.262 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onCreate
01-01 05:57:05.286 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStart
01-01 05:57:05.287 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onResume
01-01 05:57:05.324 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onPause
01-01 05:57:05.367 16836-16836/mytest.example.com.broadcaststudy D/SecondActivity: onCreate
01-01 05:57:05.370 16836-16836/mytest.example.com.broadcaststudy D/SecondActivity: onStart
01-01 05:57:05.370 16836-16836/mytest.example.com.broadcaststudy D/SecondActivity: onResume
01-01 05:57:05.605 16836-16836/mytest.example.com.broadcaststudy D/SendBroadcastActivity: onStop
其中尤其是E,可以看到activity切換時的生命週期。這裡暫且稱SendBroadcastActivity為A,SecondActivity為B。
如果在A.onResume( )時mActivityCount = 1; A.onPause()時mActivityCount--,
在B.onResume( )時mActivityCount++,此處就會有個短暫的延時,在跳轉過程中就會出現mActivityCount = 0,
即判斷App在後臺,就不正確了。
所以要在onActivityStart( )和onActivityStopped( )中 對activity進行計數。
即在A.onStart()時mActivity = 1; B.onStart( )時mActivity ++; A.onStop()時,mActivity--。
所以,當activity的計數為0時表示應用在後臺,否則就在前臺。
3.3 方案特點
(1)Android應用開發中,一般認為back鍵是可以捕獲的,而Home鍵不能捕獲(除非修改Framework),雖然這兩種方式的Activity生命週期並不相同,但是二者都會執行onStop( );所以並不關心到底是哪個鍵切入後臺的。另外,Application是否銷燬,都不會影響判斷的正確性;
(2)該方案除了用於判斷當前應用內的哪個activity位於前臺外,還可用於作為實現“程序完全退出”的一種很好的計數方案;
(3)該方案需要在Application中進行註冊相關Activity生命週期的回撥,上述code所示。只需要對mActivityCount計數進行判斷即可知道是否在前臺。
4. UsageStatsManager
4.1 原理
通過使用UsageStatsManager獲取,此方法是Android5.0之後提供的新API,可以獲取一個時間段內的應用統計資訊,但是
必須滿足以下要求。
使用前提
- 此方法只在android5.0以上有效
- AndroidManifest中加入此許可權
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
- 開啟手機設定,點選安全-高階,在有權檢視使用情況的應用中,為這個App打上勾
4.2 程式碼實現
UsageStatsManager mUsageStatsManager = (UsageStatsManager)context.getApplicationContext().getSystemService(Context.USAGE_STATS_SERVICE);
long time = System.currentTimeMillis();
List<UsageStats> stats ;
if (isFirst){
stats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - TWENTYSECOND, time);
}else {
stats = mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - THIRTYSECOND, time);
}
// Sort the stats by the last time used
if(stats != null) {
TreeMap<Long,UsageStats> mySortedMap = new TreeMap<Long,UsageStats>();
start=System.currentTimeMillis();
for (UsageStats usageStats : stats) {
mySortedMap.put(usageStats.getLastTimeUsed(),usageStats);
}
LogUtil.e(TAG,"isFirst="+isFirst+",mySortedMap cost:"+ (System.currentTimeMillis()-start));
if(mySortedMap != null && !mySortedMap.isEmpty()) {
topPackageName = mySortedMap.get(mySortedMap.lastKey()).getPackageName();
runningTopActivity=new ComponentName(topPackageName,"");
if (LogUtil.isDebug())LogUtil.d(TAG,topPackageName);
}
}
跳轉到“檢視應用使用許可權”介面的跳轉程式碼如下:
Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
startActivity(intent);
還要宣告許可權:
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
4.3 方案特點
1、該方案最大的缺點是需要使用者手動授權,因此在使用時要結合場景做適當引導;
2、該方案為Android5.0以後Google官方比較推薦的獲取程序資訊的方式,是最符合Google意圖的方式,不過在使用時會有一些延時需要小心處理。
5. AccessibilityService
5.1 原理
Android 輔助功能(AccessibilityService) 為我們提供了一系列的事件回撥,幫助我們指示一些使用者介面的狀態變化。
我們可以派生輔助功能類,進而對不同的 AccessibilityEvent 進行處理。同樣的,這個服務就可以用來判斷當前的前臺應用
優勢
- AccessibilityService 有非常廣泛的 ROM 覆蓋,特別是非國產手機,從 Android API Level 18(Android 2.2) 到 Android Api Level 23(Android 6.0)
- AccessibilityService 不再需要輪詢的判斷當前的應用是不是在前臺,系統會在視窗狀態發生變化的時候主動回撥,耗時和資源消耗都極小
- 不需要許可權請求
- 它是一個穩定的方法,與 “方法5”讀取 /proc 目錄不同,它並非利用 Android 一些設計上的漏洞,可以長期使用的可能很大
- 可以用來判斷任意應用甚至 Activity, PopupWindow, Dialog 物件是否處於前臺
劣勢
- 需要要使用者開啟輔助功能
- 輔助功能會伴隨應用被“強行停止”而剝奪
5.2 程式碼實現
步驟:
(1)派生AccessibilityService,建立視窗狀態探測服務
建立DetectionService.java
public class DetectionService extends AccessibilityService {
final static String TAG = "DetectionService";
static String foregroundPackageName;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return 0; // 根據需要返回不同的語義值
}
/**
* 過載輔助功能事件回撥函式,對視窗狀態變化事件進行處理
* @param event
*/
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
/*
* 如果 與 DetectionService 相同程序,直接比較 foregroundPackageName 的值即可
* 如果在不同程序,可以利用 Intent 或 bind service 進行通訊
*/
foregroundPackageName = event.getPackageName().toString();
/*
* 基於以下還可以做很多事情,比如判斷當前介面是否是 Activity,是否系統應用等,
* 與主題無關就不再展開。
*/
ComponentName cName = new ComponentName(event.getPackageName().toString(),
event.getClassName().toString());
}
}
@Override
public void onInterrupt() {
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
}
}
(2)建立Accessibility Service Info屬性檔案
建立res/xml/detection_service_config.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 根據的 Service 的不同功能需要,你可能需要不同的配置 -->
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagIncludeNotImportantViews" />
(3)註冊Detection service到AndroidManifest.xml
在AndroidManifest.xml中新增
<service
android:name="your_package.DetectionService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/detection_service_config"/>
</service>
(4)使用detection service判斷應用是否在前臺
建立isForegroundPkgViaDetectionService( )函式
/**
* 方法6:使用 Android AccessibilityService 探測視窗變化,跟據系統回傳的引數獲取 前臺物件 的包名與類名
*
* @param packageName 需要檢查是否位於棧頂的App的包名
*/
public static boolean isForegroundPkgViaDetectionService(String packageName) {
return packageName.equals(DetectingService.foregroundPackageName);
}
去設定裡開啟輔助功能,就可以通過isForegroundPkgDetectService( )判斷應用是否在前臺了,只需要傳入相應應用的包為引數即可。
當然,也可以參照以下方式引導使用者開啟輔助功能:
(1)引導使用者開啟輔助功能
final static String TAG = "AccessibilityUtil";
// 此方法用來判斷當前應用的輔助功能服務是否開啟
public static boolean isAccessibilitySettingsOn(Context context) {
int accessibilityEnabled = 0;
try {
accessibilityEnabled = Settings.Secure.getInt(context.getContentResolver(),
android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException e) {
Log.i(TAG, e.getMessage());
}
if (accessibilityEnabled == 1) {
String services = Settings.Secure.getString(context.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
if (services != null) {
return services.toLowerCase().contains(context.getPackageName().toLowerCase());
}
}
return false;
}
private void anyMethod() {
// 判斷輔助功能是否開啟
if (!isAccessibilitySettingsOn(getContext())) {
// 引導至輔助功能設定頁面
startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
} else {
// 執行輔助功能服務相關操作
}
}
效果如下:
5.3 方案特點
1. AccessibilityService有非常廣泛的ROM覆蓋,特別是非國產手機,從API Level 8 (Android2.2)到API Level 23(Android6.0)
2. AccessibilityService不再需要輪詢地判斷當前的應用是不是在前臺,系統會在視窗狀態發生變化的時候主動回撥,耗時和資源消耗都極小;
3.不需要許可權請求;
4.它是一個穩定的方法,並非利用Android一些設計上的 漏洞,可以長期使用的可能很大;
5.可以用來判斷任意應用甚至Activity,PopupWindow,Dialog物件是否處於前臺。
5.4 方案缺點
1、需要使用者手動開啟輔助功能;
2、輔助功能會伴隨應用被“強行停止”或第三方管理工具通過Root而剝奪,而且程序重啟需要對使用者進行重新引導開啟;
3、部分廠商可能對輔助功能進行限制,如已知的vivo部分機型。
6. 自解析/process
6.1 原理
無意中看到烏雲上有人提的一個漏洞,Linux系統核心會把process程序資訊儲存在/proc目錄下,Shell命令去獲取的他,再根據程序的屬性判斷是否為前臺
6.2 優點
- 不需要任何許可權
- 可以判斷任意一個應用是否在前臺,而不侷限在自身應用
6.3 用法
(1)獲取一系列正在執行的App的程序
List<AndroidAppProcess> processes = ProcessManager.getRunningAppProcesses();
(2)獲取任一正在執行的App程序的詳細資訊
AndroidAppProcess process = processes.get(location);
String processName = process.name;
Stat stat = process.stat();
int pid = stat.getPid();
int parentProcessId = stat.ppid();
long startTime = stat.stime();
int policy = stat.policy();
char state = stat.state();
Statm statm = process.statm();
long totalSizeOfProcess = statm.getSize();
long residentSetSize = statm.getResidentSetSize();
PackageInfo packageInfo = process.getPackageInfo(context, 0);
String appName = packageInfo.applicationInfo.loadLabel(pm).toString();
(3)判斷是否在前臺
if (ProcessManager.isMyProcessInTheForeground()) {
// do stuff
}
(4)獲取一系列正在執行的App程序的詳細資訊
List<ActivityManager.RunningAppProcessInfo> processes = ProcessManager.getRunningAppProcessInfo(ctx);
6.4 方案特點
1、不需要任何許可權;
2、可以判斷任意一個應用是否在前臺,而不侷限在自身應用;
3、當/proc下資料夾過多時,此方法是耗時操作;
4、該方案存在能耗問題;
5、在Android6.0.1以上版本或部分廠商版本受限於SEAndroid,只能獲取到第3方程序的資訊。
6.5 方案缺點
- 當/proc下資料夾過多時,此方法是耗時操作
2.該方案再6.0手機適配執行ok,但在最新的小米、華為6.0.1手機中發現受限於SELinux,無法讀取系統應用的裝置節點進行解析,只能解析第三方應用裝置節點。
6.6 能耗問題解決
1、Java層物件快取:對呼叫比較頻繁的Java層物件在JNI中建立全域性快取,這就避免了每次呼叫時都需要通過JNI介面獲取;
對一些判斷是需要的場景在初始化時由Java層傳入Jni層,並建立全域性快取。
2、過來的為Android程序:將pid小於1000的Native程序過濾掉;
3、只解析發生變化的程序:在每次輪詢解析/proc節點時先判斷程序的pid在快取中是否存在,如果存在只需要更新程序的優先
級資訊,其他資訊不會發生變化;如果程序之前不存在則需要全新解析:
(1)命中快取時的解析程式碼如下:
//Code,待補充
(2)未命中快取時,則進行全新解析:
//Code,待補充
4、在解析程序時,過來父程序為zygote的程序:Android中所有應用程序的父程序都是Zygote;
5、在Java層對呼叫做快取處理:對於呼叫比較頻繁的情況,如果當次Native呼叫沒有完成,則返回之前的值,不需要阻塞等待;
6、對於只關心前臺程序的場景進行特殊處理:
//code,待補充
通過優化,適配方案的能耗與系統介面基本保持一致。
7.作為系統程序的獲取方式
7.1 技術方案
雖然getRunningTask從Android5.0開始被系統廢棄,但是作為系統應用時,該介面依然是可用的。在使用者取得Root許可權,或者應用跟廠商合作時,應用本身可能會被內建在系統目錄,即:/system/app/或system/private-app/等目錄,因此對於這種情況,使用getRunningTask獲取依然是一種方便的實現。
1、需要判斷應用是否為系統應用:
//Code,待補充。
2、在AndroidManifest.xml中需要宣告如下許可權:
//Code,待補充。
參考連結: