Android 平台下的 Method Trace 实现解析

作者:卓_修武
转载地址:https://juejin.cn/post/7107137302043820039

Android 中的MethodTrace

对于开发者来说,Android的Java 层 提供了两种开发者可直接调用的 Method Trace 的API,一是 android.os.Debug类中的 startMethodTracing相关API,第二个 是android.os.Trace 类中的beginSection相关AP。 这两者的区别是 Debug类只能监控 Java函数调用, 而Trace类底层是使用 atrace 实现,其追踪的函数会包含了应用及系统的Java 和Native函数,并且底层基于ftrace还可以追踪cpu的详细活动信息。

本文主要是为了分析Java层的Method Trace,因此主要研究 Debug类的 startMethodTracing的底层实现。 在拓展部分 介绍的 Java堆栈采样生成火焰图的方式 已开源到GitHub上 供参考 github.com/Knight-ZXW/…

系统 Method Trace 实现解析

直接进入主题 ,本节会将Method Trace 分位 启动Trace、Trace进行中、结束Trace三个阶段进行分析。

启动Trace

当调用Debug.startMethodTracing 函数开始Trace时,其在native层的实际调用的API为 art/runtime/trace.cc 的 Trace::Start() 函数,该函数参数包含以下入参: 写入的File指针,buffer大小,flags信息、TraceOutputMode(输出模式)、TraceMode(trace实现方式)。 该函数核心的逻辑在于 需要根据TraceMode的值 采取不同的 函数调用监听方案。 TraceMode的定义只有两种,分为 kMethodTracing 以及kSampling, 分别对应在Java层调用Debug.startMethodTracing 以及 Debug.startMethodTracingSamping 。

void Trace::Start(std::unique_ptr<File>&& trace_file_in,size_t buffer_size,int flags,TraceOutputMode output_mode,TraceMode trace_mode,int interval_us)

  //.. 省略
  //create Trace  
  {
    // Required since EnableMethodTracing calls ConfigureStubs which visits class linker classes.
    gc::ScopedGCCriticalSection gcs(self,gc::kGcCauseInstrumentation,gc::kCollectorTypeInstrumentation);
    ScopedSuspendAll ssa(__FUNCTION__);
    MutexLock mu(self,*Locks::trace_lock_);
    if (the_trace_ != nullptr) {
      //已经存在trace 实例,忽略本次调用 
      LOG(ERROR) << "Trace already in progress,ignoring this request";

    } else {
      enable_stats = (flags & kTraceCountAllocs) != 0;
      the_trace_ = new Trace(trace_file.release(),buffer_size,flags,output_mode,trace_mode);
      if (trace_mode == TraceMode::kSampling) {
        CHECK_PTHREAD_CALL(pthread_create,(&sampling_pthread_,nullptr,&RunSamplingThread,reinterpret_cast<void*>(interval_us)),"Sampling profiler thread");
        the_trace_->interval_us_ = interval_us;
      } else {
        runtime->GetInstrumentation()->AddListener(
            the_trace_,instrumentation::Instrumentation::kMethodEntered |
                instrumentation::Instrumentation::kMethodExited |
                instrumentation::Instrumentation::kMethodUnwind);
        // TODO: In full-PIC mode,we don't need to fully deopt.
        // TODO: We can only use trampoline entrypoints if we are java-debuggable since in that case
        // we know that inlining and other problematic optimizations are disabled. We might just
        // want to use the trampolines anyway since it is faster. It makes the story with disabling
        // jit-gc more complex though.
        runtime->GetInstrumentation()->EnableMethodTracing(
            kTracerInstrumentationKey,/*needs_interpreter=*/!runtime->IsJavaDebuggable());
      }
    }
  }}

if (the_trace_ != nullptr) {
      //已经存在trace 实例,忽略本次调用 
      LOG(ERROR) << "Trace already in progress,ignoring this request";

    }

在实现中,可以看到 在创建Trace实例时,如果判断当前已经存在trace实例(the_trace 变量),则会忽略这次调用。如果不存在,才会调用Trace构造函数函数,创建出trace实例,因此在一次Trace流程未结束前,多次调用StartTrace是无效的。

当真正开始创建Trace实例时,是通用 new Trace()创建的,创建实例之后,开始根据不同的 TraceMode,进行真正的函数调用监听实现。 这里根据TraceMode 分为了 采样类型(TraceMode::kSampling) 以及 插桩类型(TraceMode::kMethodTracing)的方式。

Trace 过程

采样类型 Trace

先关注下采样类型的实现,首先会通过 pthread_create 创建一个采样工作线程,这个线程执行的是 Trace::RunSamplingThread(void* arg) 函数,在该函数内部会定期通过 Runtime对象的GetThreadList获取所有的线程,

之后遍历每个线程 执行 GetSample函数 获取每个线程当前的调用栈。

void* Trace::RunSamplingThread(void* arg) {
  Runtime* runtime = Runtime::Current();
  intptr_t interval_us = reinterpret_cast<intptr_t>(arg);

  while (true) {
    //..省略
    {
     //..
      runtime->GetThreadList()->ForEach(GetSample,the_trace);
    }
  }

  runtime->DetachCurrentThread();
  return nullptr;
}

继续追踪GetSample函数的具体实现,在该函数内部会通过 StackVisitor::WalkStack 进行栈回溯 从而获取当前的调用栈信息并保存在 stack_trace中,之后会调用 the_trace->CompareAndUpdateStackTrace(thread,stack_trace); 进行数据处理

static void GetSample(Thread* thread,void* arg) REQUIRES_SHARED(Locks::mutator_lock_) {
  std::vector<ArtMethod*>* const stack_trace = Trace::AllocStackTrace();
  StackVisitor::WalkStack(
      [&](const art::StackVisitor* stack_visitor) REQUIRES_SHARED(Locks::mutator_lock_) {
        ArtMethod* m = stack_visitor->GetMethod();
        // Ignore runtime frames (in particular callee save).
        if (!m->IsRuntimeMethod()) {
          stack_trace->push_back(m);
        }
        return true;
      },thread,/* context= */ nullptr,art::StackVisitor::StackWalkKind::kIncludeInlinedFrames);
  Trace* the_trace = reinterpret_cast<Trace*>(arg);

  //更新对应线程的 StackSapmle  
  the_trace->CompareAndUpdateStackTrace(thread,stack_trace);
}

继续分析 CompareAndUpdateStackTrace函数 ,在这个函数中,主要的工作是基于堆栈对比,来判断并写入函数的状态变更。

void Trace::CompareAndUpdateStackTrace(Thread* thread,std::vector<ArtMethod*>* stack_trace) {
  CHECK_EQ(pthread_self(),sampling_pthread_);
  std::vector<ArtMethod*>* old_stack_trace = thread->GetStackTraceSample();
  // Update the thread's stack trace sample.
  thread->SetStackTraceSample(stack_trace);
  // Read timer clocks to use for all events in this trace.
  uint32_t thread_clock_diff = 0;
  uint32_t wall_clock_diff = 0;
  ReadClocks(thread,&thread_clock_diff,&wall_clock_diff);
  if (old_stack_trace == nullptr) {
    // If there's no previous stack trace sample for this thread,log an entry event for all
    // methods in the trace.
    for (auto rit = stack_trace->rbegin(); rit != stack_trace->rend(); ++rit) {
      LogMethodTraceEvent(thread,*rit,instrumentation::Instrumentation::kMethodEntered,thread_clock_diff,wall_clock_diff);
    }
  } else {
    // If there's a previous stack trace for this thread,diff the traces and emit entry and exit
    // events accordingly.
    auto old_rit = old_stack_trace->rbegin();
    auto rit = stack_trace->rbegin();
    // Iterate bottom-up over both traces until there's a difference between them.
    while (old_rit != old_stack_trace->rend() && rit != stack_trace->rend() && *old_rit == *rit) {
      old_rit++;
      rit++;
    }
    // Iterate top-down over the old trace until the point where they differ,emitting exit events.
    for (auto old_it = old_stack_trace->begin(); old_it != old_rit.base(); ++old_it) {
      LogMethodTraceEvent(thread,*old_it,instrumentation::Instrumentation::kMethodExited,wall_clock_diff);
    }
    // Iterate bottom-up over the new trace from the point where they differ,emitting entry events.
    for (; rit != stack_trace->rend(); ++rit) {
      LogMethodTraceEvent(thread,wall_clock_diff);
    }
    FreeStackTrace(old_stack_trace);
  }
}

主要的逻辑如下

  • 首先通过 thread->GetStackSampler获取上一次记录的 stackTraceSample,并通过thread->SetStackTraceSample 记录该线程当前最新的stackTraceSample ;
  • 如果thread->GetStackSampler 获取上次的stackSample 为空,则直接遍历最新的stackTrace,并调用 LogMethodTraceEvent 记录函数事件,记录的函数事件类型 全部为kMethodEntered,表示这些函数已入栈
  • 如果thread->GetStackSampler 不为空,则通过比较最新的StackTrace 和上次的StackTrace,来记录 函数事件,主要逻辑如下
  • 从栈底遍历新旧StackTrace,分别找到栈帧不一致的开始点 记为 old_rit,rit
  • 针对旧的StackTrace ,从栈顶到old_rit 记录这部分函数的 EXIT 事件
  • 对于新的StackTrace ,从 rit 到栈顶 记录这部分函数的 Entered事件
  • 举个例子,比如上次的栈是 A B C D E F G (顺序为栈底到栈顶),最新的是 A B C H B C D,
    则认为 D E F G 函数POP了,并且又 PUSH了 H B C D函数,每次采样间隔,只要这个函数没有被 POP,
    就可以认为这些函数(A B C 函数)耗时又增加了采样间隔的ms数;
    假设 B C 在第5次采样才被POP,采样周期为50ms,则可以认为 B C 函数耗时在 150ms ~250ms之间,
    采样方式能够获取的函数耗时精度取决于采样的周期,采样周期越短 越精准。

LogMethodTraceEvent 函数内部 会执行实际 信息记录操作,每个函数事件所要记录的内容其所占的字节大小是固定的,在TraceClockSource 为kDual的情况下(表示同时使用WallTime 和 cpuTime记录), 每个函数事件通常会包含以下信息:

2个字节的线程ID + 4个字节的 EncodeTraceMethodAndAction + 4个字节的 thread_cpu_diff + 4个字节的 wall_clock_diff,也就是说每个函数事件记录 占用14个字节,每个信息都是以小端格式写入。

void Trace::LogMethodTraceEvent(Thread* thread,ArtMethod* method,instrumentation::Instrumentation::InstrumentationEvent event,uint32_t thread_clock_diff,uint32_t wall_clock_diff) {
  // Ensure we always use the non-obsolete version of the method so that entry/exit events have the
  // same pointer value.
  method = method->GetNonObsoleteMethod();

  // Advance cur_offset_ atomically.
  int32_t new_offset;
  int32_t old_offset = 0;

  // In the non-streaming case,we do a busy loop here trying to get
  // an offset to write our record and advance cur_offset_ for the
  // next use.
  if (trace_output_mode_ != TraceOutputMode::kStreaming) {
    old_offset = cur_offset_.load(std::memory_order_relaxed);  // Speculative read
    do {
      new_offset = old_offset + GetRecordSize(clock_source_);
      if (static_cast<size_t>(new_offset) > buffer_size_) {
        overflow_ = true;
        return;
      }
    } while (!cur_offset_.compare_exchange_weak(old_offset,new_offset,std::memory_order_relaxed));
  }

  TraceAction action = kTraceMethodEnter;
  switch (event) {
    case instrumentation::Instrumentation::kMethodEntered:
      action = kTraceMethodEnter;
      break;
    case instrumentation::Instrumentation::kMethodExited:
      action = kTraceMethodExit;
      break;
    case instrumentation::Instrumentation::kMethodUnwind:
      action = kTraceUnroll;
      break;
    default:
      UNIMPLEMENTED(FATAL) << "Unexpected event: " << event;
  }

  uint32_t method_value = EncodeTraceMethodAndAction(method,action);

  // Write data into the tracing buffer (if not streaming) or into a
  // small buffer on the stack (if streaming) which we'll put into the
  // tracing buffer below.
  //
  // These writes to the tracing buffer are synchronised with the
  // future reads that (only) occur under FinishTracing(). The callers
  // of FinishTracing() acquire locks and (implicitly) synchronise
  // the buffer memory.
  uint8_t* ptr;
  static constexpr size_t kPacketSize = 14U;  // The maximum size of data in a packet.
  uint8_t stack_buf[kPacketSize];             // Space to store a packet when in streaming mode.
  if (trace_output_mode_ == TraceOutputMode::kStreaming) {
    ptr = stack_buf;
  } else {
    ptr = buf_.get() + old_offset;
  }

  Append2LE(ptr,thread->GetTid());
  Append4LE(ptr + 2,method_value);
  ptr += 6;

  if (UseThreadCpuClock()) {
    Append4LE(ptr,thread_clock_diff);
    ptr += 4;
  }
  if (UseWallClock()) {
    Append4LE(ptr,wall_clock_diff);
  }
  static_assert(kPacketSize == 2 + 4 + 4 + 4,"Packet size incorrect.");

   WriteToBuf(stack_buf,sizeof(stack_buf));
  }
}

插桩类型 Trace

第二种追踪方式对应 TraceMode::kMethodTracing 模式,它使用了系统 art/runtime/instrumentation 的提供的

MethodTracing 功能,当调用instrumentation->EnableMethodTracing函数时 ,其内部会通过调用 Runtime->GetClassLinker->VisitClasses(&visitor) 遍历所有的Class 的函数,并为每个函数安装 Stub ,来监听函数的进入和退出。

遍历所有Class,visitor实现为 InstallStaubsClassVisitor 从而为所有类执行安装stub操作

void Instrumentation::UpdateStubs() {
   //...
  UpdateInstrumentationLevel(requested_level);
  if (requested_level > InstrumentationLevel::kInstrumentNothing) {
    InstallStubsClassVisitor visitor(this);
    runtime->GetClassLinker()->VisitClasses(&visitor);
    //...
  } else {
    InstallStubsClassVisitor visitor(this);
    runtime->GetClassLinker()->VisitClasses(&visitor);
    MaybeRestoreInstrumentationStack();
  }
}

最终会为每个函数调用 InstallSubtsForMethod,实现函数Hook

void Instrumentation::InstallStubsForMethod(ArtMethod* method) {
  if (!method->IsInvokable() || method->IsProxyMethod()) {
    return;
  }

  if (IsProxyInit(method)) {
    return;
  }

  if (InterpretOnly(method)) {
    UpdateEntryPoints(method,GetQuickToInterpreterBridge());
    return;
  }

  if (EntryExitStubsInstalled()) {
    // Install the instrumentation entry point if needed.
    if (CodeNeedsEntryExitStub(method->GetEntryPointFromQuickCompiledCode(),method)) {
      UpdateEntryPoints(method,GetQuickInstrumentationEntryPoint());
    }
    return;
  }

  // We're being asked to restore the entrypoints after instrumentation.
  CHECK_EQ(instrumentation_level_,InstrumentationLevel::kInstrumentNothing);
  // We need to have the resolution stub still if the class is not initialized.
  if (NeedsClinitCheckBeforeCall(method) && !method->GetDeclaringClass()->IsVisiblyInitialized()) {
    UpdateEntryPoints(method,GetQuickResolutionStub());
    return;
  }
  UpdateEntryPoints(method,GetOptimizedCodeFor(method));
}

安装 stubs的具体实现稍微有些复杂,其具体的实现在本文不做详细分析。 这里举个例子,对于Quick编译的代码,需预先通过 setMethodEntryHook及 setMethodExitHook 已经预留了钩子。

通过InstallSubtsForMethod 会为每个函数安装好对应的进入函数退出函数 的钩子 ,在钩子的实现中,会分别调用 instrumenttation的 MethodEnterEventMethodExitEvent,最终会遍历 instrumentation注册的所有监听者,通过事件的方式告知 函数进入 和退出的发生。

void Instrumentation::MethodEnterEventImpl(Thread* thread,ArtMethod* method) const {
  if (HasMethodEntryListeners()) {
    for (InstrumentationListener* listener : method_entry_listeners_) {
      if (listener != nullptr) {
        listener->MethodEntered(thread,method);
      }
    }
  }
}

因此,在Trace 实现中, 其通过 Instrumentation 提供的 AddListener函数 注册Listener,最终实现了 函数进入和退出的监控

{   //当不是采样类型追踪时,执行的逻辑

        runtime->GetInstrumentation()->AddListener(
            the_trace_,/*needs_interpreter=*/!runtime->IsJavaDebuggable());
 }

在监听到函数的Enter和Exit后,执行的逻辑和 采样类型一样,都是调用LogMethodTraceEvent来记录信息

void Trace::MethodEntered(Thread* thread,ArtMethod* method) {
  uint32_t thread_clock_diff = 0;
  uint32_t wall_clock_diff = 0;
  ReadClocks(thread,&wall_clock_diff);
  LogMethodTraceEvent(thread,method,wall_clock_diff);
}

void Trace::MethodExited(Thread* thread,instrumentation::OptionalFrame frame ATTRIBUTE_UNUSED,JValue& return_value ATTRIBUTE_UNUSED) {
  uint32_t thread_clock_diff = 0;
  uint32_t wall_clock_diff = 0;
  ReadClocks(thread,wall_clock_diff);
}

Finish Trace

当Java层调用 Debug.stopMethodTracing() 时,最终会调用到 nativce层 trace.cc 的 FiniishingTracing函数,在该函数内部,会先进行 trace文件 Header部分的信息组装。

这部分首先会记录 trace文件的版本号、trace追踪的时间、时间度量的类型(wallTime 还是 cpuTime),函数调用的次数 、虚拟机类型、进程号等基础信息。由于在记录函数创建事件时,并不是直接记录每个函数的名称 。而是记录内部生成的ID,因此需要将这部分映射关系记录在Header中, 具体的实现是通过调用DumpMethodList 来记录的

void Trace::FinishTracing() {
  size_t final_offset = 0;
  std::set<ArtMethod*> visited_methods;
  if (trace_output_mode_ == TraceOutputMode::kStreaming) {
    // Clean up.
    MutexLock mu(Thread::Current(),*streaming_lock_);
    STLDeleteValues(&seen_methods_);
  } else {
    final_offset = cur_offset_.load(std::memory_order_relaxed);
    GetVisitedMethods(final_offset,&visited_methods);
  }

  // Compute elapsed time.
  uint64_t elapsed = MicroTime() - start_time_;

  std::ostringstream os;
  //记录Trace版本
  os << StringPrintf("%cversion\n",kTraceTokenChar);
  os << StringPrintf("%d\n",GetTraceVersion(clock_source_));

  os << StringPrintf("data-file-overflow=%s\n",overflow_ ? "true" : "false");
  //记录时钟类型  时及race间
  if (UseThreadCpuClock()) {
    if (UseWallClock()) {
      os << StringPrintf("clock=dual\n");
    } else {
      os << StringPrintf("clock=thread-cpu\n");
    }
  } else {
    os << StringPrintf("clock=wall\n");
  }
  os << StringPrintf("elapsed-time-usec=%" PRIu64 "\n",elapsed);  
  if (trace_output_mode_ != TraceOutputMode::kStreaming) {
    size_t num_records = (final_offset - kTraceHeaderLength) / GetRecordSize(clock_source_);
    os << StringPrintf("num-method-calls=%zd\n",num_records);
  }
  os << StringPrintf("clock-call-overhead-nsec=%d\n",clock_overhead_ns_);
  os << StringPrintf("vm=art\n");
  os << StringPrintf("pid=%d\n",getpid());
  if ((flags_ & kTraceCountAllocs) != 0) {
    os << "alloc-count=" << Runtime::Current()->GetStat(KIND_ALLOCATED_OBJECTS) << "\n";
    os << "alloc-size=" << Runtime::Current()->GetStat(KIND_ALLOCATED_BYTES) << "\n";
    os << "gc-count=" <<  Runtime::Current()->GetStat(KIND_GC_INVOCATIONS) << "\n";
  }
  // 记录线程信息  
  os << StringPrintf("%cthreads\n",kTraceTokenChar);
  DumpThreadList(os);
  //记录函数信息  
  os << StringPrintf("%cmethods\n",kTraceTokenChar);
  DumpMethodList(os,visited_methods);
  os << StringPrintf("%cend\n",kTraceTokenChar);
  std::string header(os.str());
  //.....  
}

MethodList 记录的每个函数信息结构为 methodId prettyMethodDescritpor methodName methodSignature methodDeclaringClassSourceFile,这里的methodId 是 Trace追踪过程中生成的,为每个记录的函数分配的唯一ID。

同样的,由于在 method trace过程中记录的是线程ID,而不是线程名,因此需要通过 DumpThreadList记录 线程id和线程名的映射关系。由于头文件信息是在Trace结束时才开始记录的,对于线程来说,可能存在 Trace过程中有线程退出的情况, 因此为了保证 退出的线程也能被记录,在 art/runtime/thread_list.cc 中 ,当线程退出时,在 ThreadList::Unregister(Thread* self)内部, 会专门通过 调用Trace::StoreExistingThreadInfo 提前记录下来。

最终生成的 Trace Header 为类似的文本信息

image.png

Trace 方式对比

以上分析了ART虚拟机 methodTrace 在采样 和插桩模式下的实现方式,通过实现可以粗略对比出两种方式的优缺点。 对于插桩方式,由于是精准跟踪每个函数的Enter Exit,因此对于函数耗时的判断是最准确的,并且不会遗漏函数, 但是自身对性能的影响也更大; 而采样的方式对于函数的Enter Exit判断是基于两次样本的差异判断,对于一些耗时较低的函数,可能不会被记录在内,并且在计算函数耗时 时,精度也很采样间隔有关,不过其对性能的影响较低。

method trace方式 性能影响 精确度
插桩 较高 准确
采样 一般

拓展

Android Studio对 Trace文件的处理

通过 art/runtime/trace.cc 生成的Trace文件,在拖入AS后,可以直接以Top Down 或者火焰图的形式展示,因此研究了下这部分的代码。其中,对于Trace文件的解析源码位于 perflib项目中的 VmTraceParse类 。在解析Trace文件后,每个函数信息会被转化为 JavaMethodModel对象。 JavaMethodModel为CaputureNode 的子类, TopDownNode类 合并相同CaptureNode 对象,最终可以生成一个树结构,该结构即可以展示出 TopDown的效果.

AS中 FlameChart的数据结构 也是由TopDownNode 转换而来,因为本质上没什么区别,都是一个树形结构,

只不过,对于FlameChart展示方式来说,将更函数的耗时以更宽的长度展示出来

Method Trace的应用

从表现形式上来说,火焰图的展示方式通过为不同耗时函数展示不同的宽度,因此可以更快速的定位到耗时函数。我们通过上述的源码分析 已经通过指导,通过堆栈采样的方式可以实现耗时函数的监控。 本身在Java层,通过Thread.getStackTrace也可以实现堆栈采集的能力,那么不通过Native的能力,我们其实也可以实现一个简单的Method Trace方案。方案具体的实现很简单,开启后台线程,定时采集主线程的栈信息, 并默认保存最近 n秒的堆栈信息记录即可。

关于具体的应用场景,举几个目前已实践的案例,在APM系统中 当我们监测到 APP慢启动、慢消息处理、页面慢启动、ANR 时,很多情况下只能拿到最后时刻的一些信息,或者是一些简单的流程耗时,没法感知具体主线程在这阶段函数执行的情况, 通过补齐methodTrace 信息,将上述性能问题发生时间段的 堆栈样本一起上报,之后,在APM平台上,分析具体问题时,我们提供了展示性能问题发生时间段的火焰图。 这里以应用启动监控功能为例,对于慢启动的日志样本,可以展示出对于的火焰图信息

通过上面火焰图样例可以快速定位出,本次慢启动是 Webview.init 导致的。

卡顿监控案例分享

这里分享一个具体的代码实现案例,在线下场景中,以卡顿为例,我们可能更希望发生卡顿之后,能够立即受到通知,点击通知时,能够在手机上以火焰图的形式 直接展示卡顿时间段的堆栈。以下是最终实现的效果,代码已开源在 githubgithub.com/Knight-ZXW/…

在Demo中目前只采集了wallTime,没有记录Thread CpuTime,后面有时间的话会完善一下

总结

本文首先 通过源码分析 粗略了解了 Android 系统 在Native层实现 Method Trace的方式,对于插桩方式,其最终是通过 Instrementation AddListener 监听函数的进入、退出事件实现的, 而采样的方式是通过开启线程定时执行,通过StackVisitor 获取所有线程函数栈的方式。 对于这两种方案,在线上模式 我们可以参考系统采样的方式,通过 Hook 并调用 StackVisitor相关API 实现线程堆栈的高性能采集方式。

在拓展部分,简单分享了 AndroidStudio 对Method Trace文件的处理,不管是 插桩的方式 还是采样的方式,最终在表现形式上,我们都可以通过火焰图的方式 快速定位 阻塞函数, 对于在不太了解Native层的情况下,我们也可以直接使用Java层 Thread类的 getStackTrace方式,采集线程当前的栈信息。 在Method Trace 的应用方面,本文演示基于堆栈采样在 Android 卡顿监控 (Looper Message 角度)的一个样例 ,最终可以在设备上 直接以火焰图的形式展示函数调用情况。

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具