1.基本介绍

目前市场上的消费者包括小白用户,当他们的手机出现无限循环启动的异常时,用户没有办法修复异常只能通过设备商售后处理。

Google在Android 8.0加入该新功能,称之为rescue party救援程序。

主要监控系统核心程序出现循环崩溃的时候,会启动该程序,根据不同的救援级别做出一系列操作,看是否可恢复设备,最严重的时候则是通过进入recovery然后提供用户清空用户数据恢复出厂设置解决。

代码:

frameworks\base\services\core\java\com\android\server\RescueParty.java


1.救援级别

private static final int LEVEL_NONE = 0;

private static final intLEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1;

private static final intLEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2;

private static final intLEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3;

private static final intLEVEL_FACTORY_RESET = 4;

2.触发场景:

(1)system_server 在 5 分钟内重启 5 次以上调整一次级别。

(2)永久性系统应用在 30 秒内崩溃 5 次以上调整一次级别。

2.代码分析

2.1 Threshold 

类 Threshold :这个类主要实现对监控进程的崩溃次数的计数逻辑,每监控一个进程则实例化一个对应的对象,进程标识为uid。

主要变量:

private final int uid;监控进程的uid
private final int triggerCount; 监控进程崩溃次数
private final long triggerWindow;监控进程对应的时间边界
主要方法:
public abstract int getCount();获取崩溃次数
public abstract void setCount(int count);设置更新后的崩溃次数
public abstract long getStart();获取该统计周期的起始时间
public abstract void setStart(long start);设置该统计周期的起始时间
public void reset() {重置崩溃次数和起始时间
  setCount(0);
  setStart(0);
}
public boolean incrementAndTest() {//通过调用这个函数实现崩溃次数更新和判断是否超出该周期内边界时间限制
  finallong now = SystemClock.elapsedRealtime();//获取当前系统时间
  finallong window = now - getStart();//第一次的时候因为getstart为0,所以都会大于triggerWindow,之后则通过window判断目标进程是否已经超出该周期的边界时间限制。
  if(window > triggerWindow) {//时间超出限制,开启新统计周期
         setCount(1);
         setStart(now);
         returnfalse;
  }else {
         intcount = getCount() + 1;//崩溃统计次数加1
         setCount(count);
         EventLogTags.writeRescueNote(uid,count, window);
         Slog.w(TAG,"Noticed " + count + " events for UID " + uid + " inlast "
                       +(window / 1000) + " sec");
         return(count >= triggerCount);//当崩溃次数等于或者大于5次,返回true
  }
}
 
前文提到该救援程序主要实现对system_server和常驻进程监控,这里分开分析
 2.2 system_server进程重启监控
首先说下类BootThreshold继承了Threshold
几个需要说明的点
(1)监控uid为android.os.Process.ROOT_UID =0,即zygote 进程,因为system_server 重启必然导致zygote重启
    triggerCount = 5
         triggerWindow = 300 *DateUtils.SECOND_IN_MILLIS
  构造函数:
  publicBootThreshold() {
       super(android.os.Process.ROOT_UID, 5, 300 * DateUtils.SECOND_IN_MILLIS);
 }
综上:统计周期时间边界为300s即5分钟,次数限制5次
System_server重启次数和周期起始时间写入Settingsprovide
统计次数对应的键值     private static final StringPROP_RESCUE_BOOT_COUNT = "sys.rescue_boot_count";
统计周期的起始时间对应的键值private static final String PROP_RESCUE_BOOT_START ="sys.rescue_boot_start";
预编译的时候就实例BootThreshold给对象sBoot
private static final Threshold sBoot = newBootThreshold();
监控方法,在system_server每次启动过程中有如下调用
SystemServer.startBootstrapServices
 ==>RescueParty.noteBoot(mSystemContext);
 
 public static void noteBoot(Context context) {
  if(isDisabled()) return;
  if(sBoot.incrementAndTest()) {//如果5分钟内崩溃次数等于5次,则为true
         sBoot.reset();//首先重置统计信息
         incrementRescueLevel(sBoot.uid);//调整system_server的救援等级
         executeRescueLevel(context);//执行救援操作
  }
}
 
private static voidincrementRescueLevel(int triggerUid) 
   //每调用一次,救援等级+1,救援等级被写入到SettingsProvide的"sys.rescue_level" 键值对中保存,默认为LEVEL_NONE,最高级别为LEVEL_FACTORY_RESET
  finalint level = MathUtils.constrain(
                SystemProperties.getInt(PROP_RESCUE_LEVEL,LEVEL_NONE) + 1,
                LEVEL_NONE,LEVEL_FACTORY_RESET);
  SystemProperties.set(PROP_RESCUE_LEVEL,Integer.toString(level));
 
  EventLogTags.writeRescueLevel(level,triggerUid);
  //调用PKMS的接口logCriticalInfo,写入等级更新的log,并保存在PKMS的log信息记录文件中,目录/data/system/uiderrors.txt 
  PackageManagerService.logCriticalInfo(Log.WARN,"Incremented rescue level to "
                +levelToString(level) + " triggered by UID " + triggerUid);
}
 
private static voidexecuteRescueLevel(Context context) {
  finalint level = SystemProperties.getInt(PROP_RESCUE_LEVEL, LEVEL_NONE);//获取救援等级
  if(level == LEVEL_NONE) return;
 
  Slog.w(TAG,"Attempting rescue level " + levelToString(level));
  try{
         executeRescueLevelInternal(context,level);//根据不同等级执行相关救援操作
         EventLogTags.writeRescueSuccess(level);
         PackageManagerService.logCriticalInfo(Log.DEBUG,
                       "Finishedrescue level " + levelToString(level));//写入log到uiderrors.txt
  }catch (Throwable t) {
         finalString msg = ExceptionUtils.getCompleteMessage(t);
         EventLogTags.writeRescueFailure(level,msg);
         PackageManagerService.logCriticalInfo(Log.ERROR,
                       "Failedrescue level " + levelToString(level) + ": " + msg);
  }
}
 
private static voidexecuteRescueLevelInternal(Context context, int level) throws Exception {
  switch(level) {
      救援等级1-3通过更深入的重置Setting属性设置来实现,4等级即最高等级通过进入recovery,让客户重置data分区实现。
         caseLEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
                resetAllSettings(context,Settings.RESET_MODE_UNTRUSTED_DEFAULTS);//主要针对非系统进程的属性设置进行重置
                break;
         caseLEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
                resetAllSettings(context,Settings.RESET_MODE_UNTRUSTED_CHANGES);//针对非系统进程属性,来自系统默认的属性重置,其他删除
                break;
         caseLEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
                resetAllSettings(context,Settings.RESET_MODE_TRUSTED_DEFAULTS);//所有进程系统默认的属性重置,其他删除
                break;
         caseLEVEL_FACTORY_RESET://进入recovery
                RecoverySystem.rebootPromptAndWipeUserData(context,TAG);//进recovery
                break;
  }
}
 
private static voidresetAllSettings(Context context, int mode) throws Exception {
 
  Exceptionres = null;
  finalContentResolver resolver = context.getContentResolver();
  try{//重置系统级Setting 设置
         Settings.Global.resetToDefaultsAsUser(resolver,null, mode, UserHandle.USER_SYSTEM);
  }catch (Throwable t) {
         res= new RuntimeException("Failed to reset global settings", t);
  }
  for(int userId : getAllUserIds()) {//多用户的时候,所有用户的Setting设置都要重置
         try{
                Settings.Secure.resetToDefaultsAsUser(resolver,null, mode, userId);
         }catch (Throwable t) {
                res= new RuntimeException("Failed to reset secure settings for " +userId, t);
         }
  }
  if(res != null) {
         throwres;
  }
}
2.3 常驻进程崩溃监控
AppThreshold 继承Threshold,主要实现对常驻应用进程的监控
几个需要说明的点
(1)监控uid为传入崩溃的应用uid
    triggerCount = 5
   triggerWindow = 30 *DateUtils.SECOND_IN_MILLIS
   综上:统计周期时间边界为30s,次数限制5次
  publicAppThreshold(int uid) {
         super(uid,5, 30 * DateUtils.SECOND_IN_MILLIS);
  }
次数和周期统计交给对象自己的变量count和start保存
区别于system_server重启的监控,应用进程比较多,建立一个array列表去保存uid 和匹配的AppThreshold对象。
private static SparseArray<Threshold>sApps = new SparseArray<>();
 
当应用进程出现Crash的时候,都会回调到AMS,AMS调用appErrors.crashApplicationInner方法,这个方法里面有如下逻辑
ProcessRecord r
if (r != null && r.persistent) {//当前Crash的进程是否是常驻进程,是的话进入并传入uid
  RescueParty.notePersistentAppCrash(mContext,r.uid);
}
public static voidnotePersistentAppCrash(Context context, int uid) {
  if(isDisabled()) return;
  //为每一个崩溃过的常驻进程实例化一个AppThreshold,并放在sApps保存
  Thresholdt = sApps.get(uid);
  if(t == null) {
         t= new AppThreshold(uid);
         sApps.put(uid,t);
  }
  然后通过uid匹配获取的AppThreshold进行计数统计等操作,详情同上文,不再赘述。
  if(t.incrementAndTest()) {
         t.reset();
         incrementRescueLevel(t.uid);
         executeRescueLevel(context);
  }
}

2.4 救援程序的禁用场景

(1)PROP_ENABLE_RESCUE属性值为false,并且PROP_DISABLE_RESCUE为true

(2)eng版本下

(3)手机连接usb模式

private static boolean isDisabled() {

 

if(SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) {
         returnfalse;
  }
//是否为eng版本
  if(Build.IS_ENG) {
         Slog.v(TAG,"Disabled because of eng build");
         returntrue;
  }
//是否有连接usb
  if(Build.IS_USERDEBUG && isUsbActive()) {
         Slog.v(TAG,"Disabled because of active USB connection");
         returntrue;
  }
 
  if(SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) {
         Slog.v(TAG,"Disabled because of manual property");
         returntrue;
  }
 
  returnfalse;
}
2.5 其他更新救援级别的场景
SettingProvide public的时候也会更新一次救援级别
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
installSystemProviders()->RescueParty.onSettingsProviderPublished(mContext);
   public static void onSettingsProviderPublished(Context context) {
       executeRescueLevel(context);
    }