Android开发中Signal背后的bug与解决

目录
  • 背景
  • 出现SIGABRT的原因
  • SIGSEGV被捕获但是调用jni无法进行
  • 小结

背景

熟悉我的老朋友可能都知道,之前为了应对crash与anr,开源过一个“民间偏方”的库Signal,用于解决在发生crash或者anr时进行应用的重启,从而最大程度减少其坏影响。

在维护的过程中,发生过这样一件趣事,就是有位朋友发现在遇到信号为SIGSEGV时,再调用信号处理函数的时候

void SigFunc(int sig_num, siginfo *info, void *ptr) {
    // 这里判空并不代表这个对象就是安全的,因为有可能是脏内存
    if (currentEnv == nullptr || currentObj == nullptr) {
        return;
    }
    __android_log_print(ANDROID_LOG_INFO, TAG, "%d catch", sig_num);
    __android_log_print(ANDROID_LOG_INFO, TAG, "crash info pid:%d ", info->si_pid);
    jclass main = currentEnv->FindClass("com/example/lib_signal/SignalController");
    jmethodID id = currentEnv->GetMethodID(main, "callNativeException", "(ILjava/lang/String;)V");
    if (!id) {
        __android_log_print(ANDROID_LOG_INFO, TAG, "%d !id!id!id!id!id!id!id", sig_num);
        return;
    }
    __android_log_print(ANDROID_LOG_INFO, TAG, "%d 11111111111111111111", sig_num);
    jstring nativeStackTrace  = currentEnv->NewStringUTF(backtraceToLogcat().c_str());
    __android_log_print(ANDROID_LOG_INFO, TAG, "%d 22222222222222222222", sig_num);
    currentEnv->CallVoidMethod(currentObj, id, sig_num, nativeStackTrace);
    __android_log_print(ANDROID_LOG_INFO, TAG, "%d 33333333333333333333", sig_num);
    // 释放资源
    currentEnv->DeleteGlobalRef(currentObj);
    currentEnv->DeleteLocalRef(nativeStackTrace);
}

会遇到

从上文打印中看到,SIGSEGV被抛出,之后被我们的信号处理函数抓到了,但是却没有被回调到java层,反而变成了SIGABRT。还有就是SIGSEGV被捕获后,却无法通过jni回调给java层的重启处理。本文将从这个例子出发,从踩坑的过程中去学习更多jni知识。

出现SIGABRT的原因

首先呢,currentEnv是一个全局的变量,我们一般jni开发的时候,都习惯于保存一个JNIEnv全局的引用,用于后续的调用处理!但是!这样其实是一个风险的操作,比如我们在sigaction注册一个信号处理函数的时候,那么当信号来的时候,我们的信号处理运行在哪个线程呢?

答案是:不确定。当信号处理时,会根据当前内核的调度,可能会在当前发出信号的线程中进行处理,同时也可能会另外开出一个线程进行处理。而我们的JNIEnv,它其实是一个线程相关的资源,或者说是线程本地资源(TLS),如果我们在其他线程中调用到这个JNIEnv,那么会怎么样呢?比如上面例子中的currentEnv,创建在我们的java层的main线程,此时在信号处理函数中调用currentEnv->FindClass,那么不好意思,这个可不属于当前线程的资源,因此linux内核就会发出一个SIGABRT信号,提示着这个操作将被阻断

java_vm_ext.cc
void JavaVMExt::JniAbort(const char* jni_function_name, const char* msg) {
    Thread* self = Thread::Current();
    ScopedObjectAccess soa(self);
    ArtMethod* current_method = self->GetCurrentMethod(nullptr);
    std::ostringstream os;
    os << "JNI DETECTED ERROR IN APPLICATION: " << msg;
    if (jni_function_name != nullptr) {
        os << "\n    in call to " << jni_function_name;
    }
    // TODO: is this useful given that we're about to dump the calling thread's stack?
    if (current_method != nullptr) {
        os << "\n    from " << current_method->PrettyMethod();
    }
    if (check_jni_abort_hook_ != nullptr) {
        check_jni_abort_hook_(check_jni_abort_hook_data_, os.str());
    } else {
        // Ensure that we get a native stack trace for this thread.
        ScopedThreadSuspension sts(self, ThreadState::kNative);
        LOG(FATAL) << os.str();
        UNREACHABLE();
    }
}

JniAbort 调用会在所有的方法调用前进行检测,如果使用到了其他线程的JNIEnv,就会发出SIGABRT信号并打印堆栈信息,用于排查

java_vm_ext.cc:578]
JNI DETECTED ERROR IN APPLICATION:
thread Thread[3,tid=22651,Native,Thread*=0xb400007c96340270,peer=0x12c4d1a0,"Thread-3"]
using JNIEnv* from thread Thread[1,tid=22160,Runnable,Thread*=0xb400007c9630dbe0,peer=0x73467b00,"main"]

那么如果我们真的有场景需要通过在信号处理函数中调用到JNIEnv怎么办,其实也很简单,通过javaVm重新获取一个JNIEnv即可,javaVm保证是虚拟机中唯一的,因此可以放在全局变量中,当我们想要在信号处理函数时调用到jni方法,可重新获取当前线程的环境

信号处理函数中

if (javaVm->GetEnv((void **) &currentEnv, JNI_VERSION_1_4) != JNI_OK) {
    return ;
}

SIGSEGV被捕获但是调用jni无法进行

我们的例子是这样的,在java层调用一个jni函数,这个函数通过raise调用向自身发送一个SIGSEGV信号

raise(SIGSEGV);

此时我们的信号处理函数能够捕获到这个事件,但是通过currentEnv->CallVoidMethod却无法调用相应的java层方法了,同时log中出现一个StackOverflowError

Process: com.example.signal, PID: 24575
java.lang.StackOverflowError: stack size 8192KB
	at com.example.signal.MainActivity.throwNativeCrash(Native Method)
	at com.example.signal.MainActivity.onCreate$lambda-0(MainActivity.kt:23)
	at com.example.signal.MainActivity.$r8$lambda$__atZomnwlT46HKNaZgatRAAqwU(Unknown Source:0)
	at com.example.signal.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
	at android.view.View.performClick(View.java:8160)

那么这个究竟是怎么一回事呢?

首先我们要明白,我们真的是因为栈内存耗尽了出现StackOverflowError了吗?当然不是!我们只是在jni向自己线程发出了一个SIGSEGV信号罢了,怎么跟栈溢出扯上关系了?我们从art虚拟机开始说起

在art虚拟机中,出现SIGSEGV时,会默认先回调这个方法

# fault_handler.cc
// Signal handler called on SIGSEGV.
static bool art_fault_handler(int sig, siginfo_t* info, void* context) {
    return fault_manager.HandleFault(sig, info, context);
}

核心是方法

bool FaultManager::HandleFault(int sig, siginfo_t* info, void* context) {
    if (VLOG_IS_ON(signals)) {
        PrintSignalInfo(VLOG_STREAM(signals) << "Handling fault:" << "\n", info);
    }
#ifdef TEST_NESTED_SIGNAL
    // Simulate a crash in a handler.
  raise(SIGSEGV);
#endif
    针对生成机器码处理
    if (IsInGeneratedCode(info, context, true)) {
        VLOG(signals) << "in generated code, looking for handler";
        for (const auto& handler : generated_code_handlers_) {
            VLOG(signals) << "invoking Action on handler " << handler;
            if (handler->Action(sig, info, context)) {
                // We have handled a signal so it's time to return from the
                // signal handler to the appropriate place.
                return true;
            }
        }
    }
    // We hit a signal we didn't handle.  This might be something for which
    // we can give more information about so call all registered handlers to
    // see if it is.
    其他非机器码处理
    if (HandleFaultByOtherHandlers(sig, info, context)) {
        return true;
    }
    // Set a breakpoint in this function to catch unhandled signals.
    只是打印了一些log
    art_sigsegv_fault();
    return false;
}

我们可以留意到,在上面有这么一个判断IsInGeneratedCode,如果是则尝试遍历generated_code_handlers_里面的handler对信号处理,那么IsInGeneratedCode是个啥?其实它是指dex字节码编译成机器码这些代码,art虚拟会在编译成机器码的时候,生成一些虚拟机相关的指令,因此如果SIGSEGV是在这些机器码中生成的,那么就要通过generated_code_handlers_里面的处理器去处理,同时如果是非机器码生成的,则走到HandleFaultByOtherHandlers方法中进行处理

bool FaultManager::HandleFaultByOtherHandlers(int sig, siginfo_t* info, void* context) {
    if (other_handlers_.empty()) {
        return false;
    }
    Thread* self = Thread::Current();
    DCHECK(self != nullptr);
    DCHECK(Runtime::Current() != nullptr);
    DCHECK(Runtime::Current()->IsStarted());
    for (const auto& handler : other_handlers_) {
        if (handler->Action(sig, info, context)) {
            return true;
        }
    }
    return false;
}

因此我们特别关注一下generated_code_handlers_,other_handlers_(针对默认处理),它们都是一个集合std::vector<FaultHandler*> 我们看到它的添加元素方法,在FaultManager::AddHandler中

void FaultManager::AddHandler(FaultHandler* handler, bool generated_code) {
    DCHECK(initialized_);
    if (generated_code) {
        generated_code_handlers_.push_back(handler);
    } else {
        other_handlers_.push_back(handler);
    }
}

这里面添加的handler都是FaultHandler的子类,分别是NullPointerHandler,SuspensionHandler,StackOverflowHandler,JavaStackTraceHandler

虽然JavaStackTraceHandler被加入到了other_handlers_,但是依旧会判断是否处于虚拟机code中

在这里我们明白了SIGSEGV虚拟机的默认处理,一般SIGSEGV都会进入上述handler的判断,如果满足了条件就会先执行(之后才执行到我们的信号处理函数,如果系统栈溢出,那么有可能执行不到自己的信号处理器)。本例子中raise(SIGSEGV)向自己的线程抛出了SIGSEGV,如果信号处理器中没有采用Call系列调用到java层的话,那也不会有问题。

如果调用到了java层,那么就以栈溢出的形式打印log并重新发一个信号值为SIGKILL的信号杀死当前进程。(这里一直有个疑惑点,目前还没在art源码上看到为什么会这样,如果有知道的大佬可劳烦告知)

Sending signal. PID: 29066 SIG: 9

解决方法也比较简单,当我们异常处理器无法在栈异常情况下,我们可以事先采用sigaltstack分配一块栈空间

stack_t ss;
if(NULL == (ss.ss_sp = calloc(1, SIGNAL_CRASH_STACK_SIZE))){
    Handle_Exception();
    break;
}
ss.ss_size  = SIGNAL_CRASH_STACK_SIZE;
ss.ss_flags = 0;
if(0 != sigaltstack(&ss, NULL)) {
    Handle_Exception();
    break;
}

同时设置flag为SA_ONSTACK即可,让信号处理函数有一个安全的栈空间,得以进行后续调用

sigc.sa_flags = SA_SIGINFO|SA_ONSTACK;

小结

本次算是一个记录,以一个现象例子,更深入了解jni调用,希望读者有所收获,最后继续贴一下项目地址,如果有更多好点子的话,请多多pr!

github.com/TestPlanB/S…

以上就是Android开发中Signal背后的bug与解决的详细内容,更多关于Android Signal bug解决的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android 获取实时网速实现详解

    目录 正文 TrafficStats简介 实现获取网速 实时网速 正文 最近接到个需求,需要计算WebView加载网页时的网速.查询了一下,Android没有提供直接获取网速的Api,但是提供了获取流量的类TrafficStats.本文介绍如何使用Trafficstats来实现获取网速功能. TrafficStats简介 TrafficStats提供了一些获取设备从本次开机到目前为止传输/接收的流量的接口,如下: 方法 参数 说明 getTotalTxBytes - 获取设备本次开机到目前为止,

  • Android Jetpack导航组件Navigation创建使用详解

    目录 引言 依赖项 创建导航图 导航宿主 导航到目的地 传递参数 NavigationUI 多模块导航 引言 导航是指支持用户导航.进入和退出应用中不同内容片段的交互.Android Jetpack 的导航组件可实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对. 依赖项 def nav_version = "2.5.2" implementation "androidx.navigation:navigation-fragment-kt

  • Google 开发Android MVP架构Demo深入解析

    目录 1.什么是MVP? 2.Google官方的MVP 3.V1.1 My MVP V1 4.V1.2 My MVP V2 1.什么是MVP? Google在2016年推出了官方的Android MVP架构Demo,本文主要分析一下官方的MVP Demo,并且借由自己的一些经验,提出一些学习过程中,遇到的问题和自己的改进.封装措施. MVP架构已经推出很多年了,现在已经非常普及了,我在这里就不过多介绍,简单的说,它分为以下三个层次: Model:数据模型层,主要用来数据处理,获取数据: View

  • Android O对后台Service限制详解

    目录 Service问题 什么是前台应用 前台Service和后台Service 后台Service限制 解决后台Service限制 Service问题 Service没有界面,运行于后台,它会消耗设备资源,并且可能会导致不好的用户体验,例如资源占用过多,导致设备运行不流畅.为了缓解这个问题,Android O版本(Android 8.0, API 26)对后台Service强加了一些限制.注意,只是对后台Service加了限制,前台Service不受影响. 什么是前台应用 在解释后台Servi

  • Android ApplicationContext接口深入分析

    目录 需求 实现方法 代码 调用 Application getApplicationContext() 参考 需求 Android(Kotlin)获取应用全局上下文 ApplicationContext. 希望可以在应用内任意位置获取上下文,而不是仅在 Activity 或 Service 里才能获取. ApplicationContext 是和应用全局相关的. 实现方法 自定义 MyApplication,保存自身的 Application 实例. MyApplication 配置到 And

  • Android 控件自动贴边实现实例详解

    目录 正文 判断交互 隐藏与显示 示例 正文 最近接到个需求,需要在用户与App交互时,把SDK中之前实现过的悬浮控件贴边隐藏,结束交互后延迟一段时间再自动显示.本篇文章介绍一下实现的思路. 判断交互 用户与App交互.结束交互可以通过监听触摸事件来实现.建议使用的Activity的dispatchTouchEvent,Activity下的所有触摸事件分发时都会回调此方法,代码如下: class AutoEdgeHideActivity : BaseGestureDetectorActivity

  • Android 使用Toolbar实现应用栏实例详解

    目录 使用Toolbar实现应用栏 应用栏功能扩展 返回 菜单 使用Toolbar实现应用栏 App中应用栏是十分常见的,通常应用栏会显示当前页面的标题,还有一些操作按钮,例如返回.搜索.扫码等.本文介绍如何通过Toolbar实现应用栏. 使用Toolbar来实现应用栏,需要在AndroidManifest中设置NoActionBar的主题,并且Activity需要继承AppCompatActivity. <?xml version="1.0" encoding="ut

  • Android 搜索框架使用详解

    目录 搜索框架简介 使用搜索框架实现搜索功能 可搜索配置 搜索页面 使用SearchView 使用搜索弹窗 搜索弹窗对Activity生命周期的影响 附加额外的参数 语音搜索 搜索记录 创建SearchRecentSuggestionsProvider 修改可搜索配置 在搜索页面中保存查询 清除搜索历史 示例 搜索框架简介 App中搜索功能是必不可少的,搜索功能可以帮助用户快速获取想要的信息.对此,Android提供了一个搜索框架,本文介绍如何通过搜索框架实现搜索功能. Android 搜索框架

  • Android开发中Signal背后的bug与解决

    目录 背景 出现SIGABRT的原因 SIGSEGV被捕获但是调用jni无法进行 小结 背景 熟悉我的老朋友可能都知道,之前为了应对crash与anr,开源过一个“民间偏方”的库Signal,用于解决在发生crash或者anr时进行应用的重启,从而最大程度减少其坏影响. 在维护的过程中,发生过这样一件趣事,就是有位朋友发现在遇到信号为SIGSEGV时,再调用信号处理函数的时候 void SigFunc(int sig_num, siginfo *info, void *ptr) { // 这里判

  • Android开发中requestfocus()无效的原因及解决办法

    前言 最近做公司项目的时候,经常会遇到一个问题,就是我为某个控件如EditText设置requestfocus()的时候不管用,比如说登陆的时候,我判断下用户输入的密码,如果正确就登陆,错误就提示密码错误,并且输入框获取焦点,但是实际中确不起作用 package com.example.hfs.requestfocusdemo; import android.content.Intent; import android.support.v7.app.AppCompatActivity; impo

  • Android开发中听筒无法播放音乐的解决方法

    本文实例讲述了Android开发中听筒无法播放音乐的解决方法.分享给大家供大家参考,具体如下: 这个问题让我蛋疼了,既然百度也木有资料. 耗时的主要原因是因为权限不足时,而没有终止程序,只用了一小行日志提醒,没有看到 用听筒播放很简单 AudioManager.setMode(AudioManager.MODE_IN_CALL) //设定为通话中即可 还是这一句代码的事,不过记得要加上权限 Android.permission.MODIFY_AUDIO_SETTINGS 不然会像我一样蛋疼半天

  • Android开发中记一个SwipeMenuListView侧滑删除错乱的Bug

    做侧滑删除网上有很多方案,比如重写Listview实现滑动的监听,今天说下一个SwipeListView,这个是之前一个朋友在网上开源的一个封装组件,能够适用于多种情况,项目地址:https://github.com/baoyongzhang/SwipeMenuListView,我也采用了拿来主义直接拿来用了. 但是在调试运行的滑动删除数据的时候,却出现了一个问题,删除位置错乱,删除的第一个数据,却删除了最后一个,于是找问题呗,我首先用listview试了下,数据是没有问题的,那么说明是删除的时

  • RxJava入门指南及其在Android开发中的使用示例

    RxJava的GitHub主页,部署部分就没什么好说的了~ https://github.com/ReactiveX/RxJava 基础 RxJava最核心的两个东西是Observables(被观察者,事件源)和Subscribers(观察者).Observables发出一系列事件,Subscribers处理这些事件.这里的事件可以是任何你感兴趣的东西(触摸事件,web接口调用返回的数据...) 一个Observable可以发出零个或者多个事件,知道结束或者出错.每发出一个事件,就会调用它的Su

  • Android开发中使用颜色矩阵改变图片颜色,透明度及亮度的方法

    本文实例讲述了Android开发中使用颜色矩阵改变图片颜色,透明度及亮度的方法.分享给大家供大家参考,具体如下: 一.如图 二.代码实现 public class ColorImageActivity extends Activity { private ImageView mImageView; private SeekBar mSBRed,mSBGreen,mSBBlue,mSBAlpha,mSBLight; //修改后的图片 private Bitmap mModBitmap; //画布

  • Android开发中通过手机号+短信验证码登录的实例代码

    首先,需要一个电话号码,目前很多账户都是将账户名设置成手机号,然后点击按钮获取手机验证码. 其次,你需要后台给你手机短信的验证接口,各个公司用的不一样,这个身为前端,不需要你来考虑,你只要让你后台给你写好接口,你直接调用就好了. activity_login.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.andr

  • Android开发中如何解决Fragment +Viewpager滑动页面重复加载的问题

    前言 之前在做一个Viewpager上面加载多个Fragment时总会实例化已经创建好的Fragmnet对象类似 viewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public Fragment getItem(int position) { switch(position){ case 0: fragments=new Fragmnet01(); break; case

  • 详解Kotlin Android开发中的环境配置

    详解Kotlin Android开发中的环境配置 在Android Studio上面进行安装插件 在Settings ->Plugins ->Browse repositores.. ->kotlin 安装完成后重启Android Studio就生效了 如图所示: 在Android Studio中做Kotlin相关配置 (1)在根目录 的build.gradle中进行配置使用,代码如下: buildscript { ext.kotlin_version = '1.1.2-4' repos

  • 深入解读Android开发中Activity的生命周期

    什么是Activity        首先,Activity是Android系统中的四大组件之一,可以用于显示View.Activity是一个与用记交互的系统模块,几乎所有的Activity都是和用户进行交互的,但是如果这样就能说Activity主要是用来显示View就不太正确了. 在深入了解Activity之前,我们先要知道一下MVC设计模式,在JAVAEE 中MVC设计模式已经很经典了,而且分的也比较清晰了,但是在Android中,好多人对MVC在Android开发中的应用不是很清楚,下面我

随机推荐