[Framework] 深入理解 Android ANR

[Framework] 深入理解 Android ANR

ANR 对于很多人来说熟悉又陌生,熟悉的是由于应用主线程过于忙碌导致某些重要任务延迟执行然后系统会弹出一个提示框;陌生的是 ANR 产生的流程是什么样的呢?又有哪些场景会产生呢?

这里直接给出结论四大组件中的 ServiceBroadcastReceiverContentProvider 他们的生命周期会检测 ANR 超时,还有 Input 事件 (屏幕触摸事件和键盘数据事件) 也会检测 ANR 超时,唯独少了我们最熟悉的 Activity 生命周期,哈哈,是否有点颠覆你的认知,有一个很常见的面试题就是问在 Activity#onCreate 的生命周期中调用 Thread.sleep(10000) 是否会导致 ANR,你是否也回答了 yes 呢?在你读懂了这篇文章后你就知道这道题该怎么回答了。

这里以 Service Create 流程做一个例子简单描述一下 ANR 是怎么产生的,首先调用 startService() 请求一个 Service,然后通过 binder 通知 AMS 启动 ServiceAMS 经过各种判断后可以启动,然后就会通过 binder 通知 Service 所在的应用进程创建一个 Service 同时执行 onCreate() 生命周期,关键点来了这时 AMS 会通过 Handler 发送一个延时任务,这个任务就是 ANR 任务,虽然会延时执行,那不是到时间了不是还是会执行?你先别急。在应用进程 Service 生命周期执行完了后就会通过 binder 通知 AMSAMS 收到通知后就会移除 ANR 任务。所以当应用进程处理 Service Create 的任务超过设定的延时后,AMSANR 任务就会执行,然后就看到了我们熟悉的应用未响应的弹窗。

ServiceANR 任务可以类比成以下的故事:绑匪劫持了一个人质,然后让你跑回家拿钱来赎人,超过 2 小时后就撕票。

场景一:收到绑匪的消息后,你就拼命的往家里赶然后拿钱又拼命返回劫匪处然后把钱给劫匪,终于在 2 小时的期限内把钱给了劫匪,然后皆大欢喜人质存活了。

场景二:同样收到消息后往家赶,然后非常不幸半路你脚崴了,拿完钱返回劫匪处时,时间已经超过了 2 小时,然后劫匪撕票了。

绑匪 = AMS, 你 = 应用进程,场景一正常,场景二触发 ANR

ServiceBroadcastRecieverContentProvider 他们处理 ANR 的方式都是类似的,Input 事件和其他组件的处理方式不一样,所以本文以 ServiceInput 事件来分析 ANR

在继续前,建议先了解 binderHandlerInput 事件下发流程。

理解 Android 中的 Input 事件
Android Handler 工作原理
Android Binder 工作原理

Service ANR 原理

源码分析基于 Android 9

源应用进程

调用 Context#startService() 方法后最终都会跳转到 ContextImpl#startService() 方法中去,我们也以它作为入口函数:

Java 复制代码
@Override
public ComponentName startService(Intent service) {
    warnIfCallingFromSystemProcess();
    return startServiceCommon(service, false, mUser);
}
Java 复制代码
private ComponentName startServiceCommon(Intent service, boolean requireForeground,
UserHandle user) {
    try {
        validateServiceIntent(service);
        service.prepareToLeaveProcess(this);
        ComponentName cn = ActivityManager.getService().startService(
            mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
                getContentResolver()), requireForeground,
            getOpPackageName(), user.getIdentifier());
        if (cn != null) {
            // ...
        }
        return cn;
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

ActivityManager 只是 AMS 在应用进程的一个 binderClient,最终会在 AMS#startService() 方法中执行。

AMS system_server 进程

Java 复制代码
@Override
public ComponentName startService(IApplicationThread caller, Intent service,
String resolvedType, boolean requireForeground, String callingPackage, int userId)
throws TransactionTooLargeException {
    // ...
    synchronized(this) {
        final int callingPid = Binder.getCallingPid();
        final int callingUid = Binder.getCallingUid();
        final long origId = Binder.clearCallingIdentity();
        ComponentName res;
        try {
            res = mServices.startServiceLocked(caller, service,
                resolvedType, callingPid, callingUid,
                requireForeground, callingPackage, userId);
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
        return res;
    }
}

这个 mServicesActiveServices 对象,然后这里调用了它的 startServiceLocked() 方法。

Java 复制代码
ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
throws TransactionTooLargeException {
    // ...
    ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting);
    return cmp;
}
Java 复制代码
ComponentName startServiceInnerLocked(ServiceMap smap, Intent service, ServiceRecord r,
boolean callerFg, boolean addToStarting) throws TransactionTooLargeException {
    // ...
    String error = bringUpServiceLocked(r, service.getFlags(), callerFg, false, false);
    // ...
}
Java 复制代码
private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
boolean whileRestarting, boolean permissionsReviewRequired)
throws TransactionTooLargeException {
    
    // ... 

    if (!isolated) {
        app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
        if (DEBUG_MU) Slog.v(TAG_MU, "bringUpServiceLocked: appInfo.uid=" + r.appInfo.uid
                + " app=" + app);
        if (app != null && app.thread != null) {
            try {
                app.addPackage(r.appInfo.packageName, r.appInfo.longVersionCode, mAm.mProcessStats);
                realStartServiceLocked(r, app, execInFg);
                return null;
            } catch (TransactionTooLargeException e) {
                throw e;
            } catch (RemoteException e) {
                Slog.w(TAG, "Exception when starting service " + r.shortName, e);
            }

            // If a dead object exception was thrown -- fall through to
            // restart the application.
        }
    } else {
        // ...
    }

    // Not running -- get it started, and enqueue this service record
    // to be executed when the app comes up.
    if (app == null && !permissionsReviewRequired) {
        if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
                hostingType, r.name, false, isolated, false)) == null) {
            // ...
            return msg;
        }
        if (isolated) {
            r.isolatedProc = app;
        }
    }
    
    // ...

    if (!mPendingServices.contains(r)) {
        mPendingServices.add(r);
    }
    
    // ...

    return null;
}

经过层层调用会走到 ActiveServices#bringUpServiceLocked() 方法,如果 Service 对应的进程已经启动就直接调用 realStartServiceLocked(),如果进程还没有启动,调用 AMS#startProcessLocked() 启动新的进程,当前要启动的 Service 就存在 mPendingServices 中,等进程启动完成后,继续处理没有完成的 Service 生命流程,这部分代码我就不分析了。

Java 复制代码
private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg) throws RemoteException {
    // ...
    bumpServiceExecutingLocked(r, execInFg, "create");
    // ...

    boolean created = false;
    try {
        // ...
        app.thread.scheduleCreateService(r, r.serviceInfo,
            mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
            app.repProcState);
        r.postNotification();
        created = true;
    } catch (DeadObjectException e) {
        // ...
    } finally {
        // ...
    }
    // ...
}

bumpServiceExecutingLocked() 就添加了一个 ANR 延迟任务,也就是劫持人质了,然后调用 app.thread.scheduleCreateService() 方法,这个 thread 也是一个 binderClient,它的 Server 也就是应用进程中的 ApplicationThread,也就是后续逻辑在应用进程中了。

再看看延迟的 ANR 任务:

Java 复制代码
private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
    // ...
    long now = SystemClock.uptimeMillis();
    if (r.executeNesting == 0) {
        // ...
    } else if (r.app != null && fg && !r.app.execServicesFg) {
        r.app.execServicesFg = true;
        if (timeoutNeeded) {
            scheduleServiceTimeoutLocked(r.app);
        }
    }
   // ...
}
Java 复制代码
// ...

// How long we wait for a service to finish executing.

static final int SERVICE_TIMEOUT = 20*1000;

// How long we wait for a service to finish executing.

static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
// ...


void scheduleServiceTimeoutLocked(ProcessRecord proc) {
    if (proc.executingServices.size() == 0 || proc.thread == null) {
        return;
    }
    Message msg = mAm.mHandler.obtainMessage(
            ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;
    mAm.mHandler.sendMessageDelayed(msg,
        proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}

这里的定时任务会判断是前台 Service 和后台 Service,前台的超时时间是 20s,后台的超时时间是 200s。

目标应用进程

在前面讲到 AMS 会通过 binder 调用 ApplicationThread#scheduleCreateService() 方法:

Java 复制代码
public final void scheduleCreateService(IBinder token,
        ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
    updateProcessState(processState, false);
    CreateServiceData s = new CreateServiceData();
    s.token = token;
    s.info = info;
    s.compatInfo = compatInfo;

    sendMessage(H.CREATE_SERVICE, s);
}

然后这里会把任务传递到主线程:

Java 复制代码
// ...
case CREATE_SERVICE:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, ("serviceCreate: " + String.valueOf(msg.obj)));
handleCreateService((CreateServiceData)msg.obj);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
// ...
Java 复制代码
private void handleCreateService(CreateServiceData data) {
    // ...
    try {
        if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);

        ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
        context.setOuterContext(service);

        Application app = packageInfo.makeApplication(false, mInstrumentation);
        service.attach(context, this, data.info.name, data.token, app,
            ActivityManager.getService());
        service.onCreate();
        mServices.put(data.token, service);
        try {
            ActivityManager.getService().serviceDoneExecuting(
                data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    } catch (Exception e) {
        // ...
    }
}

在这里看到执行完生命周期后,又通过 binder 调用 AMS#serviceDoneExecuting() 方法。

我们再来看看 AMS#serviceDoneExecuting() 方法中做了什么:

Java 复制代码
public void serviceDoneExecuting(IBinder token, int type, int startId, int res) {
    synchronized(this) {
        if (!(token instanceof ServiceRecord)) {
            Slog.e(TAG, "serviceDoneExecuting: Invalid service token=" + token);
            throw new IllegalArgumentException("Invalid service token");
        }
        mServices.serviceDoneExecutingLocked((ServiceRecord)token, type, startId, res);
    }
}

然后继续调用 ActiveServices#serviceDoneExecutingLocked() 方法:

Java 复制代码
void serviceDoneExecutingLocked(ServiceRecord r, int type, int startId, int res) {
    boolean inDestroying = mDestroyingServices.contains(r);
    if (r != null) {
        // ...
        final long origId = Binder.clearCallingIdentity();
        serviceDoneExecutingLocked(r, inDestroying, inDestroying);
        Binder.restoreCallingIdentity(origId);
    } else {
        Slog.w(TAG, "Done executing unknown service from pid "
                + Binder.getCallingPid());
    }
}
Java 复制代码
private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
boolean finishing) {
    // ...
    if (r.executeNesting <= 0) {
        if (r.app != null) {
            if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,
                "Nesting at 0 of " + r.shortName);
            r.app.execServicesFg = false;
            r.app.executingServices.remove(r);
            if (r.app.executingServices.size() == 0) {
                if (DEBUG_SERVICE || DEBUG_SERVICE_EXECUTING) Slog.v(TAG_SERVICE_EXECUTING,
                    "No more executingServices of " + r.shortName);
                mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
            } else if (r.executeFg) {
                // ..
            }
            }
            // ...
        }
        // ...
}

这里就看到移除了 ANR 任务,释放了人质。

Input ANR 原理

在这里我默认大家都熟悉 Input 事件的处理流程,Input 流程的主要源代码逻辑都是 C++ 写的,我没有贴源代码,可以参考这位大佬的文章:Input系统---事件处理全过程

InputDispatcher 在获取到处理事件后会通过 Socket 的通信方式发送给对应的 Window 来处理,也就是应用进程所对应的一个 Window。发送出去后就会把这个事件添加到 waitQueue 队列中,只有等应用进程把这个事件处理完毕了才会通过 Socket 通知 InputDispatcher,应用进程处理 Input 事件是在应用主线程。InputDispatcher 收到已经完成的事件后,会把对应的 waitQueue 中等待的事件移除。

在之前的讲 Input 原理的文章中提到,InputDispatcher 收到来自 InputReader 发送过来的事件后,回去找对应的 Window 来处理这个事件,在这个过程中它会检查各种 Window 的状态,来判断它是否有能力来处理这个事件。

C++ 复制代码
String8 InputDispatcher::checkWindowReadyForMoreInputLocked(nsecs_t currentTime,
const sp<InputWindowHandle>& windowHandle, const EventEntry* eventEntry,
const char* targetType) {
    //当窗口暂停的情况,则保持等待
    if (windowHandle->getInfo()->paused) {
        return String8::format("Waiting because the %s window is paused.", targetType);
    }

    //当窗口连接未注册,则保持等待
    ssize_t connectionIndex = getConnectionIndexLocked(windowHandle->getInputChannel());
    if (connectionIndex < 0) {
        return String8::format("Waiting because the %s window's input channel is not "
            "registered with the input dispatcher. The window may be in the process "
            "of being removed.", targetType);
    }

    //当窗口连接已死亡,则保持等待
    sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
    if (connection->status != Connection::STATUS_NORMAL) {
        return String8::format("Waiting because the %s window's input connection is %s."
            "The window may be in the process of being removed.", targetType,
            connection->getStatusLabel());
    }

    // 当窗口连接已满,则保持等待
    if (connection->inputPublisherBlocked) {
        return String8::format("Waiting because the %s window's input channel is full. "
            "Outbound queue length: %d. Wait queue length: %d.",
            targetType, connection->outboundQueue.count(), connection->waitQueue.count());
    }


    if (eventEntry->type == EventEntry::TYPE_KEY) {
        // 按键事件,输出队列或事件等待队列不为空
        if (!connection->outboundQueue.isEmpty() || !connection->waitQueue.isEmpty()) {
        return String8::format("Waiting to send key event because the %s window has not "
            "finished processing all of the input events that were previously "
            "delivered to it. Outbound queue length: %d. Wait queue length: %d.",
            targetType, connection->outboundQueue.count(), connection->waitQueue.count());
    }
    } else {
        // 非按键事件,事件等待队列不为空且头事件分发超时500ms
        if (!connection->waitQueue.isEmpty()
        && currentTime >= connection->waitQueue.head->deliveryTime
        + STREAM_AHEAD_EVENT_TIMEOUT) {
        return String8::format("Waiting to send non-key event because the %s window has not "
            "finished processing certain input events that were delivered to it over "
            "%0.1fms ago. Wait queue length: %d. Wait queue head age: %0.1fms.",
            targetType, STREAM_AHEAD_EVENT_TIMEOUT * 0.000001f,
            connection->waitQueue.count(),
        (currentTime - connection->waitQueue.head->deliveryTime) * 0.000001f);
    }
    }
    return String8::empty();
}

当上面的方法返回不为空时就表示当前的 Window 不可用,我们以触摸事件 waitQueue 那段逻辑作为我们的分析方向,当 waitQueue 中最旧的事件超过 500ms 时,它就认为目前的 Window 不可用。如果 Window 可用就走上述的事件下发到应用进程的逻辑,如果不可用就跳过这次事件下发,然后还会检查上次事件下发到当前时间的时间间隔,如果这个间隔超过了 5s 那就会触发 ANR,这个 ANR 消息会通过 jni 调用到 IMS,然后再到 AMS,最后执行 ANR 的处理流程。

总结

回到开头问题

为什么在 Activity 的生命周期中阻塞 10s 都不会出现 ANR ? 因为单纯的 Activity 的生命周期中,AMS 本来就没有做 ANR 处理,但是这会造成很大的 ANR 风险,为什么会这么说?因为还有其他四种情况下会导致 ANR,以 Input 事件为例,当事件到达后,由于主线程被 Activity#onCreate() 中阻塞 10s,然后 Input 任务的 MessageMessageQueue 中只能等待,等待 10s 就会导致 Input 事件的 ANR 触发。

应用层如何监听 ANR

在出现 ANR 后会发送一个 SIGQUIT 信号给应用进程,需要通过 C/C++ 代码监控,大家自己在去找一下怎么监听信号,有的时候 SIGQUIT 信号也不一定是 ANR,还需要通过一下代码判断当前进程的状态:

Java 复制代码
private static boolean checkErrorState() {
    try {
        Application application = sApplication == null ? Matrix.with().getApplication() : sApplication;
        ActivityManager am = (ActivityManager) application.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
        if (procs == null) return false;
        for (ActivityManager.ProcessErrorStateInfo proc : procs) {
            if (proc.pid != android.os.Process.myPid()) continue;
            if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) continue;
            return true;
        }
        return false;
    } catch (Throwable t){
        MatrixLog.e(TAG,"[checkErrorState] error : %s", t.getMessage());
    }
    return false;
}

在出现 ANR 后,系统会在 data/anr 目录下存放出现的 ANR 记录,里面有非常重要的 ANR 信息。

vbnet 复制代码
Subject: Input dispatching timed out (6b274b7 com.gmlive.common.xolmedia.demo/com.gmlive.common.xolmedia.demo.MainActivity (server) is not responding. Waited 5004ms for MotionEvent)

--- CriticalEventLog ---
capacity: 20
timestamp_ms: 1697608788397
window_ms: 300000

这就是一个标准的 Input 超时导致的 ANR 信息。后面还会有当时所有进程的信息,我找到我们自己的进程,然后看看当时主线程的堆栈:

php 复制代码
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 ucsCount=0 flags=1 obj=0x720110c0 self=0xb4000073589cd380
  | sysTid=10108 nice=-10 cgrp=system sched=0/0 handle=0x74a85b44f8
  | state=S schedstat=( 879996588 111079630 802 ) utm=77 stm=10 core=4 HZ=100
  | stack=0x7fc0b0e000-0x7fc0b10000 stackSize=8188KB
  | held mutexes=
  at java.lang.Thread.sleep(Native method)
  - sleeping on <0x01738b1a> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:450)
  - locked <0x01738b1a> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:355)
  at com.gmlive.common.xolmedia.demo.MainActivity.onTouchEvent(MainActivity.kt:13)
  at android.app.Activity.dispatchTouchEvent(Activity.java:4302)
  at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
  at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:458)
  at android.view.View.dispatchPointerEvent(View.java:15309)
  at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:6778)
  at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6578)
  at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6034)
  at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6091)
  at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6057)
  at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:6222)
  at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6065)
  at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:6279)
  at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6038)
  at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6091)
  at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6057)
  at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6065)
  at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6038)
  at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:9206)
  at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:9157)
  at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:9126)
  at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:9329)
  at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:267)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:335)
  at android.os.Looper.loopOnce(Looper.java:161)
  at android.os.Looper.loop(Looper.java:288)
  at android.app.ActivityThread.main(ActivityThread.java:7918)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

主要是卡在我们的 sleep 方法,这个文件里面还有其他非常重要的信息,包括 binder 状态,内存信息等等。

如果是高版本手机提示没有权限,可以通过 adb bugreport 命令把日志文件下载下来。

ANR 问题定位

其实 ANR 的问题十分复杂,它可能也是由于 AMS 本身处理不过来任务导致,也有可能是 CPU 本身发热降频导致处理任务的能力下降,我们先忽略这些原因只考虑我们自身应用的原因。当我们监听到应用发生 ANR 时,就去找当前主线程的方法栈时就能定位到导致 ANR 的罪魁祸手吗?当我们使用 Thread.sleep() 这种方法来测试时,是可行的,但是测试用的方法总是按照你所预想的结果来跑的,而线上用户实际的运行环境比你测试的代码要复杂非常多,我就举一个例子来反驳上诉的方案。

我这里先默认大家都对 Handler 的工作原理都清楚,当有 Input 事件到来时,主线程的的 MessageQueue 状态如下:

我们假如主线程处理 Input 事件的 MessageD,这时正插入 MessageQueue 尾。我们假如任务 ABC都需要执行 2s,那么 Input 处理的事件就需要至少等待 6s 才能执行,也就是在它执行前就会造成 ANR, 造成 ANR 时对应的 MessageQueue 状态如下:

这个时候你去拿主线程的方法栈信息,就会定位到 Message C 所对应的栈,所以你就认为是 C 所对应的代码有问题?这个就是刻舟求剑了,其实是 AB, C 这三个任务共同导致了这次 ANR

ANR 是一个非常复杂的综合性问题,我们只能尽最大的可能减小主线程的负担从而减少 ANR,和 OOM 一样,我们无法阻止它的发生。

相关推荐
小雨cc5566ru2 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng4 小时前
android 原生加载pdf
android·pdf
hhzz4 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
火红的小辣椒5 小时前
XSS基础
android·web安全
勿问东西7 小时前
【Android】设备操作
android
五味香7 小时前
C++学习,信号处理
android·c语言·开发语言·c++·学习·算法·信号处理
图王大胜9 小时前
Android Framework AMS(01)AMS启动及相关初始化1-4
android·framework·ams·systemserver
工程师老罗10 小时前
Android Button “No speakable text present” 问题解决
android
小雨cc5566ru11 小时前
hbuilderx+uniapp+Android健身房管理系统 微信小程序z488g
android·微信小程序·uni-app
小雨cc5566ru12 小时前
微信小程序hbuilderx+uniapp+Android 新农村综合风貌旅游展示平台
android·微信小程序·uni-app