如何手动解析iphone crash logLog

AsyncTask 解析 - 推酷
AsyncTask 解析
什么是 AsyncTask
AsyncTask 是一种轻量级异步任务类,内部封装了 Thread 和 Handler,但是它不适合做特别耗时的后台任务(建议采用线程池)。实现异步通常有两种方式(现在很火的 RxJava 也是一种实现异步的方式):
Thread 配合 Handler
前者控制精细,但代码臃肿,且异步任务较多时不易控制,因此 Android 提供了 AsyncTask 类来实现异步,AsyncTask 是对 Handler,Thread 和线程池的良好封装。
AsyncTask 的具体使用不再赘述。
解析 AsyncTask
AsyncTask 内部线程池的配置
既然 AsyncTask 内部封装了线程池,那么我们可以很清楚的看到线程池的配置:
– 核心线程数 = CPU核心数 + 1
– 最大线程数 = 2 * CPU核心数 + 1
– keepAliveTime = 1 s
– 任务队列长度为 128
为什么必须在主线程中加载 AsyncTask?
首先这句话不是绝对的,我们看一下不同版本 AsyncTask 的源码就知道为什么了。
先上结论:API 16 以前必须在主线程加载 AsyncTask,API 16 以后就不用了。
首先,所有版本的 AsyncTask 都是在成员位置直接声明并创建了 Handler 对象,它是一个静态变量,也就是说它是在类加载的时候创建的:
private static final InternalHandlersHandler = new
InternalHandler();
API 16 以前的 AsyncTask
在 Android 4.1(API 16)之前,如果是在子线程加载的 AsyncTask,那么就会导致静态的 Handler 对象创建并初始化(如上),而 Handler 默认采用的是当前线程的 Looper,于是 Handler 用了子线程的 Looper,处理消息时也就无法切换到主线程,而是被 Handler post 到创建它的那个线程执行(即子线程)。
所以那个年代,我们不能在子线程加载 AsyncTask。
API 16 以后,API 22 之前的 AsyncTask
在 API 16 以后,API 22 之前,Google 进行了改动:在 ActivityThread 的 main 函数里面(App启动的入口),直接调用了 AsyncTask 的 init() 方法,该方法代码如下:
public static void init() {
sHandler.getLooper();
main 函数运行在主线程,这样就确保了 AsyncTask 类是在主线程加载。因此 AsyncTask 的 Handler 用的也是主线程的 Looper。
所以在 API 16 以后,我们是可以在子线程中加载 AsyncTask的。但是不管我们需不需要 AsyncTask,它都会在我们 App 启动时就加载,那如果我们完全没有用到它,岂不是很浪费资源?
API 22 之后
在 API 22 之后 Google 又换了一种实现:
删除了 AsyncTask 的 init 方法,main 函数中也不再调用它。
在 AsyncTask 成员变量位置仅仅声明了静态的 Handler,并没有创建对象:
private static InternalHandlersH // 还去掉了final关键字
在 Handler 的实现中,添加了一个无参构造:
public InternalHandler() {
super(Looper.getMainLooper());
在 Handler 的实现中,添加了 getHandler() 方法:在需要 Handler 时会调用此方法。
private static HandlergetHandler() {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler();
可以看到,这样既保证了 Handler 采用主线程的 Looper 构建,又使得 AsyncTask 在需要时才被加载。
以上,Google 在 API 16 以后通过不同的实现,确保了线程的正常切换。注意一点,onPreExecute()不受 Handler 影响,运行在执行 execute 方法的线程,原因我们后面分析。
AsyncTask 是并行执行的吗?
既然封装了线程池,那么 AsyncTask 执行任务是并行的吗?
结论是:在Android 1.6 之前,是串行执行,从 Android 1.6 开始采用线程池处理并行任务。在 3.0 以后默认串行执行,但提供了 executeOnExecutor()方法并行执行。
为什么默认不并行?
因为 AsyncTask 内部的线程池是静态、单例的,所以在同一进程中所有用到 AsyncTask 的地方都是同一个线程池,如果我们创建了多个 AsyncTask 且在其中访问了共同资源,并且没做同步处理,那么并行时就会出现同步问题。Google 可能觉得这很麻烦,为了避免各种问题,便在 3.0 以后改成了默认串行执行。
如果我们需要并行执行,并自己处理了同步问题,那么可以使用 executeOnExecutor(AsyncTask.THREAD
EXECUTOR, Params…..)来并行执行任务,第一个参数这里用的是 AsyncTask 自带的线程池,当然也可以传入自己写的线程池。注意,这个方法是API 11 引入的。
AsyncTask 的缺点
通过以上分析,我们可以发现 AsyncTask 具有如下缺点:
容易崩溃。AsyncTask里面的线程池的核心线程数是 CPU + 1 个(4.4以前是5个),最大线程数是 CPU * 2 + 1 个,任务缓存队列长度为 128。任务缓存满时,使用默认的 Handler 处理(即抛异常)。因为同一个进程里所有用到 AsyncTask 的地方都是同一个线程池,导致任务缓存队列就容易满,一旦满了,再往里面添任务,就抛异常挂掉了。
资源浪费。在 AsyncTask 全部执行完毕之后,进程中还是会常驻 corePoolSize 个线程
必须在主线程中加载,不然在API 16以下完蛋
默认是串行执行任务。API 11 以后才能通过 executeOnExecutor 方法来并行执行
实际使用中我还发现了一些缺点:
一个 AsyncTask 对象只能执行一次,多次执行就报错。
必须在主线程执行 execute 方法,不然会导致 onPreExecute 不在主线程执行
出现错误只能在 doInBackground 里 try…catch。后续处理也很麻烦
cancel 方法不好用。调用 cancel 后只是不会去执行 onPostExecute 和 onProgressUpdate,而任务依然在 doInBackGround 中执行。调用cancel(true)虽然会使任务尽早结束,但是如果 doInBackground() 中有不可打断的方法就失效了,比如BitmapFactory.decodeStream()。
结果丢失。屏幕旋转等原因造成 AsyncTask 销毁并重建时,正在运行的 AsyncTask 持有的是之前的 Activity 实例,这样任务执行完后 onPostExecute 方法就没用了。
AsyncTask 工作原理
分析工作原理,那我们就从执行任务的起点:execute 方法开始分析,该方法就一行代码:
return executeOnExecutor(sDefaultExecutor, params);
可以看到这里使用了 AsyncTask 自带的线程池。这里先提一下:sDefaultExecutor 是个串行的线程池,一个进程的所有 AsyncTask 都在该线程池排队执行。
再来看看 executeOnExecutor 方法:
public final AsyncTask&Params, Progress, Result& executeOnExecutor(Executorexec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw …..
case FINISHED:
throw …..
mStatus = Status.RUNNING;
onPreExecute();
mWorker.mParams =
exec.execute(mFuture);
可以看到,该方法先进行了一些状态上的判断和更新,如果该任务正在执行或已经完成,都会抛异常。然后就调用了 onPreExecute 方法。这段代码充分解释了为什么必须在主线程执行 execute 方法,以及为什么一个 AsyncTask 对象只能执行一次。
然后它又调用了线程池的 execute 执行任务,并传入了 mFuture 对象(这是一个 FutureTask 对象)。
接下来再看看这个线程池的 execute 方法是如何执行的
public synchronizedvoid execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
} finally {
scheduleNext();
if (mActive == null) {
scheduleNext();
protected synchronizedvoid scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
可以看到,传进来的 mFuture 被当做 Runnable 使用。这里的 mTasks 就是任务队列,调用 offer 方法将 mFuture 对象插入到任务队列尾部。如果没有正在执行的任务,就调用 scheduleNext 执行下一个任务,finally 语句块确保了每个任务执行完后自动执行下一个任务。这段代码证明了 AsyncTask 默认串行执行任务。
等等,这个 sDefaultExecutor 只是给任务进行排队了啊,并没有真正的执行任务!
那我们再进入 scheduleNext 方法看看是怎么回事。
THREAD_POOL_EXECUTOR.execute(mActive);
该方法内部调用了 THREAD
EXECUTOR 这个线程池来执行,这个线程池才是真正执行任务的,我们再来看看这个线程池的构造。
public static final ExecutorTHREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
可以看到该线程池的配置:CPU数 + 1 的核心线程数,128 长度的工作队列等等。
任务的执行就这么开始了。我们再来看看 AsyncTask 的构造方法,了解一下那个 mFuture 是个啥。
public AsyncTask() {
mWorker = new WorkerRunnable&Params, Result&() {
public Resultcall() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
Resultresult = doInBackground(mParams);
Binder.flushPendingCommands();
return postResult(result);
mFuture = new FutureTask&Result&(mWorker) {
protected void done() {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
构造 mFuture 时传入了 mWorker 对象,而通过查看 FutureTask 源码可以看到,它的 run 方法会调用这个 mWorker 的 call 方法,线程池最终执行任务时就是调用的这个 call 方法。
再来看看这个 mWorker 的 call 方法是如何执行的:先执行 mTaskInvoked.set(true);将该任务标记为已经被调用。然后执行 doInBackGround 传入参数并执行任务。然后将返回值作为参数,调用 postResult 方法。postResult 方法不再贴代码了,它内部就是创建了一个 Message,将当前 AsyncTask 对象和形参 result 包装起来作为 obj,然后调用 Handler 发送消息。
运行在主线程的 Handler 接收到这个消息时会调用 AsyncTask 的 finish 方法。
private void finish(Resultresult) {
if (isCancelled()) {
onCancelled(result);
onPostExecute(result);
mStatus = Status.FINISHED;
该方法根据 AsyncTask 是否被取消,取消就执行 onCancelled(),没取消就执行 onPostExecute(),然后更新状态为 finish。至此 AsyncTask 的整个执行流程就走完了。
哦对了,还有 onProgressUpdate 去哪了?。。因为我们更新进度都是在 doInBackGround 方法中手动调用 publishProgress 方法来更新的,那么我们看一下这个方法:
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult&Progress&(this, values)).sendToTarget();
很简单,就是在任务未被取消时创建 Message,将当前 AsyncTask 对象和形参 progress 包装起来作为 obj,然后用 Handler 发送。运行在主线程的 Handler 接收到该消息后就调用了抽象的 onProgressUpdate 方法。
优化 AsyncTask
这是我平常使用 AsyncTask 时对其进行的一些优化(现在已经不怎么用它了),是直接在最新的 API AsyncTask 类基础上进行的优化:
在 executeOnExecutor 方法中对 exec.execute(mFuture); 进行 try…catch
exec.execute(mFuture);
} catch (RejectedExecutionException e) {
// Report.report()
// report this except
e.printStackTrace();
取消任务队列的大小限制: 这里不传入参数表示队列长度为 Integer 的上限,可以视为无上限了。
private static final BlockingQueue&Runnable& sPoolWorkQueue = new LinkedBlockingQueue&Runnable&();
适当提高 keepAliveTime
已发表评论数()
已收藏到推刊!
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
没有分页内容
图片无法显示
视频无法显示
与原文不一致Monkey&log异常分析说明
以下主要针对在Phone项目中进行Monke&log进行分析和说明,可以对bug提交作为参考。
要求熟悉,应用的包名。也就是说那个应用包出现问题,该属于那个模块,应用包名是判断依据。
Null指针异常
空指针异常主要是有异常提示,在Monkey测试过程中,该信息一般记录在plog.log&(locat中也有该信息)中。
如果Monkey命令被中断,说明有异常信息并且有对应的打印信息,可以看到如下信息,说明几个问题:
、该进程出现异常
、异常信息主要是由于引起的,也就是出现了空指针,导致了acore进程进入debug
、具体的可以继续分析是由哪个函数的那一行导致的,如可以从文件中的275行查找等。
、可能同一应用中存在多处的空指针异常,所以一旦出现空指异常后,可以分析和对比log信息,如果不一致的话,需要把新的log信息也一同添加到bug中,如果log信息一直的话,则不需要继续补充没有必要的log信息。
**************************************************************************
debug异常主要是由于应用程序本身的错误导致的异常。在Monkey测试过程中,该信息同样记录在plog.log&(locat中也有该信息)中。一般情况下,出现的该问题,很可能在手动测试时也可以测试到。
该进程进入debug
出现的是异常,该异常一般多是传递的参数非法或被多次调用时出现的异常
ug信息提交时,详细信息中一般应该包含如下的log信息,方便开发人员分析和定位。
************************************************************************
低内存异常
低内存异常一般情况下,主要表现在出现异常或者提示,其后果同样表现为抛出异常或者是通过来杀掉部分进程以释放内存空间,当然如果被kill点关键的进程的话,也就可能导致部分应用会自动的退出。出现该情况时,主要是在进行频繁的进行大量的操作导致的,所以使用手动的方式也是可能进行重现的。
该进程出现的异常
出现的是异常,该异常一般多频繁的操作(即多次调用某个函数,存在申请变量空间未释放)导致的
ug信息提交时,详细信息中同样应该包含如下的log信息,方便开发人员分析和定位。
或者出现plog信息的最后出现如下log信息也可能存在同样问题:
操作无响应异常
操作无响应的问题,主要表现在Monkey运行过程中,出现某功能无响应,提示是否“强制关闭“或“等待“,同时会打印出相应的log信息,如下:
可以说明该进程出现的无响应
出现的是TimerOut异常,该异常一般出现时一般都会又提示
目前,uPhone手机中经常出现操作无响应的几个模块包括:clock、camera、、broser等,根据开发人员分析其他模块也是有可能出现操作无响应的,测试过程中,请多注意。
出现无响应异常,一般情况下很难被修复,和发送事件的频率也是有关系的,从质量上来要求的话,要求尽可能的减少出现操作无响应的频率和时间。
estart&异常(系统重新启动),即log信息的最后几行会看到estart&的打印信息,说明手机被自动的重启或断掉,有几个方面的原因
运行过程中,存在异常手机被自动重启
手机在运行过程中,进入设置中的恢复出场设置,导致手机重启
其他原因,如果手机后盖移动等
等异常,同样的会在plog.log中可以看到相应的打印信息,该问题多出现在操作无响应之后,或者是某应用的服务无法启动或连接时,需要具体问题具体的分析
、等异常,&在plog.log中可以看到相应的打印信息,也会有相应的信息:&,该信息说明android的数据库操作出现异常,&需要具体问题具体的分析
等异常,多少情况下为说明向函数传递了一个不正确或不合法的参数需要具体问题具体的分析
其他,带补充和分析说明
说明几点:
log信息的分析,希望在bug信息的简述中能够明确说明清楚出现的是什么异常,具体的log信息要单独添加到详细信息描述中,方便相关人员定位和查看。
bug的提交,对于Monkey测试出现的问题:
首先要求分析log信息
再次要确认该bug是否已经提交?已经提交的bug和新运行出来的问题是否一致?
如果不一致的话,是否属于同一问题,如空指针异常,是的话就把该问题的log信息一起注释到bug信息中,并添加所有的log信息
如果不一致的,也不属于同一问题的话,可以提交新的bug
在java编程中,一般的异常都会抛出xxx&Exception的信息,可以跟进xxx进行相关的判断和分析,具体的异常则需要具体问题具体分析。
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
您的访问请求被拒绝 403 Forbidden - ITeye技术社区
您的访问请求被拒绝
亲爱的会员,您的IP地址所在网段被ITeye拒绝服务,这可能是以下两种情况导致:
一、您所在的网段内有网络爬虫大量抓取ITeye网页,为保证其他人流畅的访问ITeye,该网段被ITeye拒绝
二、您通过某个代理服务器访问ITeye网站,该代理服务器被网络爬虫利用,大量抓取ITeye网页
请您点击按钮解除封锁&手动解析CrashLog之——原理篇
招聘信息:
在上篇文章里介绍了手动解析CrashLog的方法,接下来再说说dwarfdump、atos等解析工具是如何从符号表文件中获取到崩溃位置信息的。一切还得从.dSYM符号表文件开始说起。一、.dSYM文件的生成符号表文件.dSYM实际上是从Mach-O文件中抽取调试信息而得到的文件目录,实际用于保存调试信息的问价是DWARF,其出身可以从苹果员工的文章《Apple’s “Lazy” DWARF Scheme》了解一二。1、Xcode自动生成Xcode会在编译工程或者归档时自动为我们生成.dSYM文件,当然我们也可以通过更改Xcode的若干项Build Settings来阻止它那么干。2、手动生成另一种方式是通过命令行从Mach-O文件中手工提取,比如:$&/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil&/Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp&-o&YourApp.dSYM该方式通过Xcode提供的工具dsymutil,从项目编译结果.app目录下的Mach-O文件中提取出调试符号表文件。实际上Xcode也是通过这种方式来生成符号表文件。二、DWARF简介DWARF(DebuggingWith Arbitrary Record Formats),是ELF和Mach-O等文件格式中用来存储和处理调试信息的标准格式,.dSYM中真正保存符号表数据的是DWARF文件。DWARF中不同的数据都保存在相应的section(节)中,ELF文件里所有的section名称都以".debug_"开头,如下表所示:|&Section&Name&&&&&&&&&|&Contents&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&|
|&--------------------&|&------------------------------------------------&&|
|&.debug_abbrev&&&&&&&&|&Abbreviations&used&in&the&.debug_info&section&&&&&|
|&.debug_aranges&&&&&&&|&A&mapping&between&memory&address&and&compilation&&|
|&.debug_frame&&&&&&&&&|&Call&Frame&Information&&&&&&&&&&&&&&&&&&&&&&&&&&&&|
|&.debug_info&&&&&&&&&&|&The&core&DWARF&data&containing&DIEs&&&&&&&&&&&&&&&|
|&.debug_line&&&&&&&&&&|&Line&Number&Program&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&|
|&.debug_loc&&&&&&&&&&&|&Macro&descriptions&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&|
|&.debug_macinfo&&&&&&&|&A&lookup&table&for&global&objects&and&functions&&&|
|&.debug_pubnames&&&&&&|&A&lookup&table&for&global&objects&and&functions&&&|&
|&.debug_pubtypes&&&&&&|&A&lookup&table&for&global&types&&&&&&&&&&&&&&&&&&&|
|&.debug_ranges&&&&&&&&|&Address&ranges&referenced&by&DIEs&&&&&&&&&&&&&&&&&|
|&.debug_str&&&&&&&&&&&|&String&table&used&by&.debug_info&&&&&&&&&&&&&&&&&&|Mach-O中关于section的命名和ELF稍有区别,把名称前的.换成了_,例如.debug_info变成了_debug_info。三、section信息提取保存在DAWARF中的信息是高度压缩的,可以通过dwarfdump命令从中提取出可读信息。前文所述的那些section中,定位CrashLog只需要用到.debug_info和.debug_line。由于解析出来的数据量较大,为了方便查看,就将其保存在文本中。两个section的数据提取方式如下:.debug_info$&dwarfdump&-e&--debug-info&YourPath/YourApp.dSYM/Contents/Resources/DWARF&>&info-e.txt.debug_line$&dwarfdump&-e&--debug-line&YourPath/YourApp.dSYM/Contents/Resources/DWARF&>&line-e.txt命令中的-e可以增加解析结果的可读性;其它section的提取方式类似,详情请参考dwarfdump命令帮助信息。四、解析崩溃地址1、计算崩溃地址对应符号表中的地址在上篇文章中,介绍了如何根据崩溃地址计算得到对应符号表中的地址,并得到了最终数值:0x52846,接下来我们就通过这个值来介绍dwarfdump、atos等工具是如何解析崩溃日志的。2、解析过程.debug_info.debug_info中最基本的描述单元为DIE(Debug Information Entry),详情请参考DWARF官方网站,首先我们要根据符号表崩溃地址0x52846从.debug_info中取出包含这个地址的DIE单元。为了简单起见,直接贴出了从info-e.txt中取出的对应DIE,其部分内容如下:0x:&&&&&function&[99]&*
&&&&&&&&&&&&&&&&low&pc(&0x&)
&&&&&&&&&&&&&&&&high&pc(&0x&)
&&&&&&&&&&&&&&&&frame&base(&r7&)
&&&&&&&&&&&&&&&&object&pointer(&{0x0006212a}&)
&&&&&&&&&&&&&&&&name(&"-[OBDFirstConnectViewController&showOilPricePickerView]"&)
&&&&&&&&&&&&&&&&decl&file(&"/YourSourcePath/OBDFirstConnectViewController.m"&)
&&&&&&&&&&&&&&&&decl&line(&870&)
&&&&&&&&&&&&&&&&prototyped(&0x01&)
&&&&&&&&&&&&&&&&APPLE&instruction&set&architecture(&0x01&)可以看出,该DIE包含是方法-[OBDFirstConnectViewController showOilPricePickerView]的内容,其地址范围是0x–0x,我们的目标地址0x52846正是在这个范围内,所以可以判定崩溃发生在该方法的某一行中。需要指出的是,上面这段DIE是我为了介绍方便直接贴出来的,实际应用的时候需要通过搜索算法找出包含目标符号表崩溃地址(这里是0x52846)的DIE。从上述DIE中我们可以获取到这些信息:崩溃所在源码文件:/YourSourcePath/OBDFirstConnectViewController.m
发生崩溃的方法:-[OBDFirstConnectViewController&showOilPricePickerView]
发生崩溃的方法在源文件中的行号:870. debug_line截止目前,我们可以获取到发生了崩溃的方法的相关信息,但要想确定崩溃发生的具体行号,还需要.debug_line的帮助。.debug_line以一个方法为基本块,急了该方法中每一行对应的符号表地址。通过.debug_info得知崩溃发生的方法地址范围是0x–0x,通过起始地址0x在解析. debug_line得到的line-e.txt中直接搜索即可得到崩溃所在方法的. debug_line数据,其中部分内容如下:0x02e0&&&&870&/YourSourcePath/OBDFirstConnectViewController.m
0x02e0&&&&&&0
0x02f0&&&&872
0x033c&&&&873
0x0374&&&&874
0x039e&&&&875
0x03c8&&&&876
0x2812&&&&880
0x283e&&&&881
0x2846&&&&882
0x28c8&&&&883
.... debug_line段的第一行内容标识了该方法的起始符号表地址,行号及方法所在文件路径,通过之前得到的崩溃地址0x52846即可得知崩溃发生在882行。至此我们已经根据崩溃地址解析出了崩溃发生位置的详细信息:崩溃所在源码文件:/YourSourcePath/OBDFirstConnectViewController.m
发生崩溃的方法:-[OBDFirstConnectViewController&showOilPricePickerView]
发生崩溃的方法在源文件中的行号:870
崩溃发生在源文件中得行号:882以上内容为本人工作学习中所得,如有理解错误之处,还请指出!五、参考文档
微信扫一扫
订阅每日移动开发及APP推广热点资讯公众号:CocoaChina
点击量20802点击量12965点击量12895点击量9553点击量7010点击量6040点击量5907点击量5299点击量4882
&2015 Chukong Technologies,Inc.
京公网安备89

我要回帖

更多关于 crash log 的文章

 

随机推荐