android persist apk 多次crash会进入recovery模式

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.分析

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和常驻进程监控,这里分开分析

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;

  }

}

常驻进程崩溃

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);

  }

}


禁止场景

(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;

}

其他场景

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);

    }


服务初始化

voidcrashApplicationInner(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo,

intcallingPid,intcallingUid) {

。。。

// If a persistent app is stuck in a crash loop, the device isn't very

// usable, so we want to consider sending out a rescue party.

if(r !=null&& r.persistent) {

RescueParty.notePersistentAppCrash(mContext, r.uid);

}

AppErrorResult result =newAppErrorResult();

TaskRecord task;

}


流程图


处理方式:

代码路径如下:

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

    关闭可以直接

        private static boolean isDisabled() {

            return true;

            ....

        }

        进入recovery 的命令:

      private static void executeRescueLevelInternal(Context context, int level) throws Exception {

            ....

        case LEVEL_FACTORY_RESET:

RecoverySystem.rebootPromptAndWipeUserData(context,TAG);

break;

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345