热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

性能优化之matrix学习IOCanary

性能优化之matrix学习-IOCanary-IOCanary大体上从JavaHook、NativeHook两个角度来检测应用的IO行为;并根据不同的策略细化了IOIssue的种类

IOCanary大体上从Java Hook、Native Hook两个角度来检测应用的IO行为;并根据不同的策略细化了IO Issue的种类。

在Android中I/O的操作最终都会通过native层的open、read、write以及close函数。所以,我们只需要hook这几个函数,然后获取到与函数相关的入参、返回值等,基于这些信息我们就可以进行I/O操作的检测。最后检测完成之后进行上报。

I/O 监控流程图如下:

IOCanaryPlugin

我们首先看看 IOCanaryPlugin 的初始化、start、stop等函数,发现实现很简单,基本都代理给了 IOCanaryCore 这个类。

public class IOCanaryPlugin extends Plugin {
    private static final String TAG = "Matrix.IOCanaryPlugin";

    private final IOConfig     mIOConfig;
    private IOCanaryCore mCore;

    public IOCanaryPlugin(IOConfig ioConfig) {
        mIOCOnfig= ioConfig;
    }

    @Override
    public void init(Application app, PluginListener listener) {
        super.init(app, listener);
        IOCanaryUtil.setPackageName(app);
        mCore = new IOCanaryCore(this);
    }

    @Override
    public void start() {
        super.start();
        mCore.start();
    }

    @Override
    public void stop() {
        super.stop();
        mCore.stop();
    }

    public IOConfig getConfig() {
        return mIOConfig;
    }
    ...
}

在 IOCanaryCore 中会根据 IOConfig 的配置项进行四个场景的 hook;同时该类还作为四个场景检测出来的问题的 reporter 传递给 hooker。下面是配置 hook 相关的代码,由 IOCanaryCore#start 进行调用:

private void initDetectorsAndHookers(IOConfig ioConfig) {
    assert ioConfig != null;

    if (ioConfig.isDetectFileIOInMainThread()
        || ioConfig.isDetectFileIOBufferTooSmall()
        || ioConfig.isDetectFileIORepeatReadSameFile()) {
        IOCanaryJniBridge.install(ioConfig, this);
    }

    //if only detect io closeable leak use CloseGuardHooker is Better
    if (ioConfig.isDetectIOClosableLeak()) {
        mCloseGuardHooker = new CloseGuardHooker(this);
        mCloseGuardHooker.hook();
    }
}

我们可以看到,对于 主线程I/O、读写Buffer过小、重复读 这三种 native hook 的场景,由 IOCanaryJniBridge 进行进一步的 hook,而 Closeable泄漏监控 则由 CloseGuardHooker 进行具体的 hook。

Java Hook

Java Hook的hook点是系统类CloseGuard,hook的方式是使用动态代理。

在崩溃分析中,我说过有部分的 OOM 是由于文件句柄泄漏导致。资源泄漏是指打开资源包括文件、Cursor 等没有及时 close,从而引起泄露。这属于非常低级的编码错误,但却非常普遍存在。

如何有效的监控资源泄漏?这里我利用了 Android 框架中的 StrictMode,StrictMode 利用CloseGuard.java类在很多系统代码已经预置了埋点。

到了这里,接下来还是查看源码寻找可以利用的 Hook 点。这个过程非常简单,CloseGuard 中的 REPORTER 对象就是一个可以利用的点。具体步骤如下:

利用反射,把 CloseGuard 中的 ENABLED 值设为 true。 利用动态代理,把 REPORTER 替换成我们定义的 proxy。

public final class CloseGuardHooker {
    ...

    /**
     * set to true when a certain thread try hook once; even failed.
     */
    public void hook() {
        MatrixLog.i(TAG, "hook sIsTryHook=%b", mIsTryHook);
        if (!mIsTryHook) {
            boolean hookRet = tryHook();
            MatrixLog.i(TAG, "hook hookRet=%b", hookRet);
            mIsTryHook = true;
        }
    }

    /**
     * TODO comment
     * Use a way of dynamic proxy to hook
     * 

* warn of sth: detectLeakedClosableObjects may be disabled again after this tryHook once called * * @return */ private boolean tryHook() { try { Class closeGuardCls = Class.forName("dalvik.system.CloseGuard"); Class closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter"); // CloseGuard#getReporter方法 Method methodGetReporter = closeGuardCls.getDeclaredMethod("getReporter"); // CloseGuard#setReporter方法 Method methodSetReporter = closeGuardCls.getDeclaredMethod("setReporter", closeGuardReporterCls); // CloseGuard#setEnabled方法 Method methodSetEnabled = closeGuardCls.getDeclaredMethod("setEnabled", boolean.class); // 保存原始的reporter对象 sOriginalReporter = methodGetReporter.invoke(null); // 调用CloseGuard#setEnabled方法,设置为true methodSetEnabled.invoke(null, true); // 开启MatrixCloseGuard,这是类似于CloseGuard的一个东西,但是没有用到 // open matrix close guard also MatrixCloseGuard.setEnabled(true); ClassLoader classLoader = closeGuardReporterCls.getClassLoader(); if (classLoader == null) { return false; } // 将动态代理的对象设置为REPORTER methodSetReporter.invoke(null, Proxy.newProxyInstance(classLoader, new Class[]{closeGuardReporterCls}, new IOCloseLeakDetector(issueListener, sOriginalReporter))); return true; } catch (Throwable e) { MatrixLog.e(TAG, "tryHook exp=%s", e); } return false; } }

然后我们看下 IOCloseLeakDetector 这个类,这是一个典型的动态代理的用法,重点在于其 invoke 方法里面的处理,我们可以根据 method 的方法名以及 args 参数列表来匹配需要 hook 的方法。

IOCloseLeakDetector 里面只 hook 了 report 方法,然后处理了 args[1] 这个 Throwable,将其作为参数进行上报。代码如下:

public class IOCloseLeakDetector extends IssuePublisher implements InvocationHandler {
    private static final String TAG = "Matrix.CloseGuardInvocationHandler";

    private final Object originalReporter;

    public IOCloseLeakDetector(OnIssueDetectListener issueListener, Object originalReporter) {
        super(issueListener);
        this.originalReporter = originalReporter;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MatrixLog.i(TAG, "invoke method: %s", method.getName());
        if (method.getName().equals("report")) {
            if (args.length != 2) {
                MatrixLog.e(TAG, "closeGuard report should has 2 params, current: %d", args.length);
                return null;
            }
            if (!(args[1] instanceof Throwable)) {
                MatrixLog.e(TAG, "closeGuard report args 1 should be throwable, current: %s", args[1]);
                return null;
            }
            Throwable throwable = (Throwable) args[1];

            String stackKey = IOCanaryUtil.getThrowableStack(throwable);
            if (isPublished(stackKey)) {
                MatrixLog.d(TAG, "close leak issue already published; key:%s", stackKey);
            } else {
                Issue ioIssue = new Issue(SharePluginInfo.IssueType.ISSUE_IO_CLOSABLE_LEAK);
                ioIssue.setKey(stackKey);
                JSONObject cOntent= new JSONObject();
                try {
                    content.put(SharePluginInfo.ISSUE_FILE_STACK, stackKey);
                } catch (JSONException e) {
//                e.printStackTrace();
                    MatrixLog.e(TAG, "json content error: %s", e);
                }
                ioIssue.setContent(content);
                publishIssue(ioIssue);
                MatrixLog.i(TAG, "close leak issue publish, key:%s", stackKey);
                markPublished(stackKey);
            }


            return null;
        }
        return method.invoke(originalReporter, args);
    }
}

对于 Closeable 泄露监控来说,在 Android 10 及上无法兼容的原因是 CloseGuard#getReporter 无法直接通过反射获取, reporter 字段也是无法直接通过反射获取。如果无法获取到原始的 reporter,那么原始的 reporter 在我们 hook 之后就会失效。如果我们狠下决心,这也是可以接受的,但是对于这种情况我们应该尽量避免。

那么我们现在的问题就是如何在高版本上获取到原始的 reporter,那么有办法吗?有的,因为我们前面说到了无法直接通过反射获取,但是可以间接获取到。这里我们可以通过 反射的反射 来获取。实例如下:

private static void doHook() throws Exception {
    Class clazz = Class.forName("dalvik.system.CloseGuard");
    Class reporterClass = Class.forName("dalvik.system.CloseGuard$Reporter");

    Method setEnabledMethod = clazz.getDeclaredMethod("setEnabled", boolean.class);
    setEnabledMethod.invoke(null, true);

    // 直接反射获取reporter
//        Method getReporterMethod = clazz.getDeclaredMethod("getReporter");
//        final Object originalReporter = getReporterMethod.invoke(null);

    // 反射的反射获取
    Method getDeclaredMethodMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
    Method getReporterMethod = (Method) getDeclaredMethodMethod.invoke(clazz, "getReporter", null);
    final Object originalReporter = getReporterMethod.invoke(null);

    Method setReporterMethod = clazz.getDeclaredMethod("setReporter", reporterClass);
    Object proxy = Proxy.newProxyInstance(
            reporterClass.getClassLoader(),
            new Class[]{reporterClass},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    return method.invoke(originalReporter, args);
                }
            }
    );
    setReporterMethod.invoke(null, proxy);
}

系统CloseGuard的实现原理是在一些资源类中预埋一些代码,从而使CloseGuard感知到资源是否被正常关闭。例如系统类FileOutputStream中有如下代码:

  private final CloseGuard guard = CloseGuard.get();
  ...
  public FileOutputStream(File file, boolean append)
        throws FileNotFoundException
    {
        ...
        guard.open("close");
    }
  ...
  public void close() throws IOException {
        ...
        guard.close();
        ...
    } 
  ...
  protected void finalize() throws IOException {
        // Android-added: CloseGuard support.
        if (guard != null) {
            guard.warnIfOpen();
        }

        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {
                // Android-removed: Obsoleted comment about shared FileDescriptor handling.
                close();
            }
        }
    }   

可以看到在调用finalize之前未调用close方法会走到CloseGuard的warnIfOpen方法,从而检测到这次资源未正常关闭的行为。

当然应用也有一些自定义的资源类,对于这种情况Matrix建议使用MatrixCloseGuard这个类模拟系统埋点的方式,达到资源监控的目的。

虽然在 Android 源码中,StrictMode 已经预埋了很多的资源埋点。不过肯定还有埋点是没有的,比如 MediaPlayer、程序内部的一些资源模块。所以在程序中也写了一个 MyCloseGuard 类,对希望增加监控的资源,可以手动增加埋点代码。

Native Hook

IOCanaryJniBridge 负责三种需要 native hook 的检测场景。在 IOCanaryJniBridge#install 操作中,会先加载对应的 so,然后根据配置启动 detector 并设置对应的上报阈值,最后调用 doHook 这个 native 方法进行 native 层面的 hook。

public static void install(IOConfig config, OnJniIssuePublishListener listener) {
    MatrixLog.v(TAG, "install sIsTryInstall:%b", sIsTryInstall);
    if (sIsTryInstall) {
        return;
    }

    //load lib
    if (!loadJni()) {
        MatrixLog.e(TAG, "install loadJni failed");
        return;
    }

    //set listener
    sOnIssuePublishListener= listener;

    try {
        //set config
        if (config != null) {
            if (config.isDetectFileIOInMainThread()) {
                enableDetector(DetectorType.MAIN_THREAD_IO);
                // ms to μs
                setConfig(ConfigKey.MAIN_THREAD_THRESHOLD, config.getFileMainThreadTriggerThreshold() * 1000L);
            }

            if (config.isDetectFileIOBufferTooSmall()) {
                enableDetector(DetectorType.SMALL_BUFFER);
                setConfig(ConfigKey.SMALL_BUFFER_THRESHOLD, config.getFileBufferSmallThreshold());
            }

            if (config.isDetectFileIORepeatReadSameFile()) {
                enableDetector(DetectorType.REPEAT_READ);
                setConfig(ConfigKey.REPEAT_READ_THRESHOLD, config.getFileRepeatReadThreshold());
            }
        }

        //hook
        doHook();

        sIsTryInstall = true;
    } catch (Error e) {
        MatrixLog.printErrStackTrace(TAG, e, "call jni method error");
    }
}

private static boolean loadJni() {
    if (sIsLoadJniLib) {
        return true;
    }

    try {
        System.loadLibrary("io-canary");
    } catch (Exception e) {
        MatrixLog.e(TAG, "hook: e: %s", e.getLocalizedMessage());
        sIsLoadJniLib = false;
        return false;
    }

    sIsLoadJniLib = true;
    return true;
}

/**
  *  enum DetectorType {
  *    kDetectorMainThreadIO = 0,
  *    kDetectorSmallBuffer,
  *    kDetectorRepeatRead
  *  };
  */
private static final class DetectorType {
    static final int MAIN_THREAD_IO = 0;
    static final int SMALL_BUFFER = 1;
    static final int REPEAT_READ = 2;
}
private static native void enableDetector(int detectorType);

/**
  * enum IOCanaryConfigKey {
  *    kMainThreadThreshold = 0,
  *    kSmallBufferThreshold,
  *    kRepeatReadThreshold,
  * };
  */
private static final class ConfigKey {
    static final int MAIN_THREAD_THRESHOLD = 0;
    static final int SMALL_BUFFER_THRESHOLD = 1;
    static final int REPEAT_READ_THRESHOLD = 2;
}

private static native void setConfig(int key, long val);

private static native boolean doHook();

上面的 DetectorType 以及 ConfigKey 的值的定义,与注释中定义在 C 层的枚举一致; config.getFileXX获取到的默认值,也与 C 层的默认值一致。如果在 Java 层修改了 detector 的触发阈值, 那么 C 层检测时会以自定义的值为准。

注意到 IOCanaryJniBridge 中还有一个私有静态类 JavaContext 以及一个私有静态方法 getJavaContext,这两个东西是给 C++ 部分进行调用的。该类中有两个参数 threadName 以及 stack,这会作为底层 detector 进行判断的参数,同时上报 IO 问题时也会带上这两个参数。

private static final class JavaContext {
    private final String stack;
    private final String threadName;

    private JavaContext() {
        stack = IOCanaryUtil.getThrowableStack(new Throwable());
        threadName = Thread.currentThread().getName();
    }
}

/**
    * 声明为private,给c++部分调用!!!不要干掉!!!
    * @return
    */
private static JavaContext getJavaContext() {
    try {
        return new JavaContext();
    } catch (Throwable th) {
        MatrixLog.printErrStackTrace(TAG, th, "get javacontext exception");
    }

    return null;
}

上面就是 Java 层的主要代码,下面我们看看 native 层干的事情,native 层的入口位于 io_canary_jni.cc 中。在加载 so 时,首先被调用的就是 JNI_OnLoad 方法。

在 JNI_OnLoad 方法会持有 Java 层一些方法、成员变量的句柄,供后续使用。相关代码以及对应关系如下:

namespace iocanary {
    static jclass kJavaBridgeClass;
    static jmethodID kMethodIDOnIssuePublish;

    static jclass kJavaContextClass;
    static jmethodID kMethodIDGetJavaContext;
    static jfieldID kFieldIDStack;
    static jfieldID kFieldIDThreadName;

    static jclass kIssueClass;
    static jmethodID kMethodIDIssueConstruct;

    static jclass kListClass;
    static jmethodID kMethodIDListConstruct;
    static jmethodID kMethodIDListAdd;

    extern "C" {

        JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){
            __android_log_print(ANDROID_LOG_DEBUG, kTag, "JNI_OnLoad");
            kInitSuc = false;

            // 获取Java层一些方法、成员变脸的句柄
            if (!InitJniEnv(vm)) {
                return -1;
            }

            // 设置上报回调为OnIssuePublish函数
            iocanary::IOCanary::Get().SetIssuedCallback(OnIssuePublish);

            kInitSuc = true;
            __android_log_print(ANDROID_LOG_DEBUG, kTag, "JNI_OnLoad done");
            return JNI_VERSION_1_6;
        }

        static bool InitJniEnv(JavaVM *vm) {
            kJvm = vm;
            JNIEnv* env = NULL;
            if (kJvm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK){
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv GetEnv !JNI_OK");
                return false;
            }

            jclass temp_cls = env->FindClass("com/tencent/matrix/iocanary/core/IOCanaryJniBridge");
            if (temp_cls == NULL)  {
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv kJavaBridgeClass NULL");
                return false;
            }
            // IOCanaryJniBridge
            kJavaBridgeClass = reinterpret_cast(env->NewGlobalRef(temp_cls));

            jclass temp_java_context_cls = env->FindClass("com/tencent/matrix/iocanary/core/IOCanaryJniBridge$JavaContext");
            if (temp_java_context_cls == NULL)  {
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv kJavaBridgeClass NULL");
                return false;
            }
            // IOCanaryJniBridge$JavaContext
            kJavaCOntextClass= reinterpret_cast(env->NewGlobalRef(temp_java_context_cls));
            // IOCanaryJniBridge$JavaContext.stack
            kFieldIDStack = env->GetFieldID(kJavaContextClass, "stack", "Ljava/lang/String;");
            // IOCanaryJniBridge$JavaContext.threadName
            kFieldIDThreadName = env->GetFieldID(kJavaContextClass, "threadName", "Ljava/lang/String;");
            if (kFieldIDStack == NULL || kFieldIDThreadName == NULL) {
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv kJavaContextClass field NULL");
                return false;
            }

            // IOCanaryJniBridge#onIssuePublish
            kMethodIDOnIssuePublish= env->GetStaticMethodID(kJavaBridgeClass, "onIssuePublish", "(Ljava/util/ArrayList;)V");
            if (kMethodIDOnIssuePublish== NULL) {
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv kMethodIDOnIssuePublish NULL");
                return false;
            }

            // IOCanaryJniBridge#getJavaContext
            kMethodIDGetJavaCOntext= env->GetStaticMethodID(kJavaBridgeClass, "getJavaContext", "()Lcom/tencent/matrix/iocanary/core/IOCanaryJniBridge$JavaContext;");
            if (kMethodIDGetJavaCOntext== NULL) {
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv kMethodIDGetJavaContext NULL");
                return false;
            }

            jclass temp_issue_cls = env->FindClass("com/tencent/matrix/iocanary/core/IOIssue");
            if (temp_issue_cls == NULL)  {
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv kIssueClass NULL");
                return false;
            }
            // IOIssue
            kIssueClass = reinterpret_cast(env->NewGlobalRef(temp_issue_cls));

            // IOIssue#init
            kMethodIDIssueCOnstruct= env->GetMethodID(kIssueClass, "", "(ILjava/lang/String;JIJJIJLjava/lang/String;Ljava/lang/String;I)V");
            if (kMethodIDIssueCOnstruct== NULL) {
                __android_log_print(ANDROID_LOG_ERROR, kTag, "InitJniEnv kMethodIDIssueConstruct NULL");
                return false;
            }

            jclass list_cls = env->FindClass("java/util/ArrayList");
            // ArrayList
            kListClass = reinterpret_cast(env->NewGlobalRef(list_cls));
            // ArrayList#init
            kMethodIDListCOnstruct= env->GetMethodID(list_cls, "", "()V");
            // ArrayList#add
            kMethodIDListAdd = env->GetMethodID(list_cls, "add", "(Ljava/lang/Object;)Z");

            return true;
        }
    }

然后在 Java层 中会进行调用 native 层的 enableDetector 以及 setConfig 函数,后者这个方法就不说了。enableDetector 函数会向 IOCanary 这个单例对象中添加对应的 detector 实例。

// matrix/matrix-android/matrix-io-canary/src/main/cpp/io_canary_jni.cc
JNIEXPORT void JNICALL
Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_enableDetector(JNIEnv *env, jclass type, jint detector_type) {
    iocanary::IOCanary::Get().RegisterDetector(static_cast(detector_type));
}

// matrix/matrix-android/matrix-io-canary/src/main/cpp/core/io_canary.cc
void IOCanary::RegisterDetector(DetectorType type) {
        switch (type) {
            case DetectorType::kDetectorMainThreadIO:
                detectors_.push_back(new FileIOMainThreadDetector());
                break;
            case DetectorType::kDetectorSmallBuffer:
                detectors_.push_back(new FileIOSmallBufferDetector());
                break;
            case DetectorType::kDetectorRepeatRead:
                detectors_.push_back(new FileIORepeatReadDetector());
                break;
            default:
                break;
        }
    }

上面出现的三个 Detector 就是对应三种场景的了,我们后面分析具体检测算法的时候再讨论。下面再看看 Java 调用的 doHook 方法,在该方法的实现中,会调用 xHook 来 hook 对应 so 的对应函数。

Native Hook是采用PLT(GOT) Hook的方式hook了系统so中的IO相关的open、read、write、close方法。在代理了这些系统方法后,Matrix做了一些逻辑上的细分,从而检测出不同的IO Issue。

const static char* TARGET_MODULES[] = {
    "libopenjdkjvm.so",
    "libjavacore.so",
    "libopenjdk.so"
};
const static size_t TARGET_MODULE_COUNT = sizeof(TARGET_MODULES) / sizeof(char*);
...
JNIEXPORT jboolean JNICALL
Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
    __android_log_print(ANDROID_LOG_INFO, kTag, "doHook");

    for (int i = 0; i 

在上面的代码中,分别 hook libopenjdkjvm.so、libjavacore.so、libopenjdk.so 中的 open、open64、close函数,此外还会额外 hook libjavacore.so 的 read、__read_chk、write、__write_chk 的方法。这样打开、读写、关闭全流程都可以 hook 到了。hook 之后,调用被 hook 的函数都会先被 matrix 拦截处理。

此外,我们还可以看到 xHook 的使用是非常简单的,流程如下:

  • 调用 xhook_elf_open 打开对应的 so
  • 调用 xhook_hook_symbol hook 对应的方法
  • 调用 xhook_elf_close close 资源,防止资源泄漏
  • 如果需要还原 hook,也是调用 xhook_hook_symbol 进行 hook 点的还原

open

matrix IO 模块目前只检测主线的 IO 问题,当 open 等操作执行成功时,才会进入统计、检测流程。

在 open 操作中,会将入参与出参一起作为参数向下层传递,这里的返回值 ret 实际上是指文件描述符 fd。

int ProxyOpen64(const char *pathname, int flags, mode_t mode) {
    if(!IsMainThread()) {
        return original_open64(pathname, flags, mode);
    }

    int ret = original_open64(pathname, flags, mode);

    if (ret != -1) {
        DoProxyOpenLogic(pathname, flags, mode, ret);
    }

    return ret;
}

在捕获到 open 操作后,下面就转入了 IOCanary 的处理逻辑了。在 DoProxyOpenLogic 函数中,首先调用 Java 层的 IOCanaryJniBridge#getJavaContext 方法获取当前的上下文环境 JavaContext,然后将 Java 层的 JavaContext 转为 C 层的 java_context;最后调用了 IOCanary#OnOpen 方法。

static void DoProxyOpenLogic(const char *pathname, int flags, mode_t mode, int ret) {
    JNIEnv* env = NULL;
    kJvm->GetEnv((void**)&env, JNI_VERSION_1_6);
    if (env == NULL || !kInitSuc) {
        __android_log_print(ANDROID_LOG_ERROR, kTag, "ProxyOpen env null or kInitSuc:%d", kInitSuc);
    } else {
        jobject java_context_obj = env->CallStaticObjectMethod(kJavaBridgeClass, kMethodIDGetJavaContext);
        if (NULL == java_context_obj) {
            return;
        }

        jstring j_stack = (jstring) env->GetObjectField(java_context_obj, kFieldIDStack);
        jstring j_thread_name = (jstring) env->GetObjectField(java_context_obj, kFieldIDThreadName);

        char* thread_name = jstringToChars(env, j_thread_name);
        char* stack = jstringToChars(env, j_stack);
        JavaContext java_context(GetCurrentThreadId(), thread_name == NULL ? "" : thread_name, stack == NULL ? "" : stack);
        free(stack);
        free(thread_name);

        iocanary::IOCanary::Get().OnOpen(pathname, flags, mode, ret, java_context);

        env->DeleteLocalRef(java_context_obj);
        env->DeleteLocalRef(j_stack);
        env->DeleteLocalRef(j_thread_name);
    }
}

IOCanary#OnOpen 代理调用了 IOInfoCollector#OnOpen 方法。在后者的实现中,会以 fd 为 key, pathname、java_context 等值组成的对象 IOInfo 作为 value,保存到 info_map_ 这个 map 中。 IOInfo 这个对象里面的字段很多,包含了 IOCanary 对 IO 问题检测的各方面所需的字段,具体里面有什么我们下面遇到再说。 IOCanary#OnOpen 代码如下:

// matrix/matrix-android/matrix-io-canary/src/main/cpp/core/io_canary.cc
void IOCanary::OnOpen(const char *pathname, int flags, mode_t mode,
                        int open_ret, const JavaContext& java_context) {
    collector_.OnOpen(pathname, flags, mode, open_ret, java_context);
}

// matrix/matrix-android/matrix-io-canary/src/main/cpp/core/io_info_collector.cc
void IOInfoCollector::OnOpen(const char *pathname, int flags, mode_t mode
        , int open_ret, const JavaContext& java_context) {
    //__android_log_print(ANDROID_LOG_DEBUG, kTag, "OnOpen fd:%d; path:%s", open_ret, pathname);

    if (open_ret == -1) {
        return;
    }

    if (info_map_.find(open_ret) != info_map_.end()) {
        //__android_log_print(ANDROID_LOG_WARN, kTag, "OnOpen fd:%d already in info_map_", open_ret);
        return;
    }

    std::shared_ptr info = std::make_shared(pathname, java_context);
    info_map_.insert(std::make_pair(open_ret, info));
}

至此,open 流程相关的代码我们梳理了一下,就是以 open 操作中的 fd 为 key,对应的 IOInfo 为 value 保存到哈希表中备用。

read/write

read 操作 hook 了 read、__read_chk 两个函数,函数定义如下:

// read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.
ssize_t read(int fd, void *buf, size_t count);

// The interface __read_chk() shall function in the same way as the interface read(), except that __read_chk() shall check for buffer overflow before computing a result. If an overflow is anticipated, the function shall abort and the program calling it shall exit.
// 
// The parameter buflen specifies the size of the buffer buf. If nbytes exceeds buflen, the function shall abort, and the program calling it shall exit.
// 
// The __read_chk() function is not in the source standard; it is only in the binary standard.
ssize_t __read_chk(int fd, void * buf, size_t nbytes, size_t buflen);

因此,读写操作的 buffer_size 都应该对应第三个参数才是。

接着,我们看看代理函数。在读的代理函数中,依旧是只处理主线程的调用。这里面 ret 表示的是本次操作中读取到的字节长度,同时还记录本次读的操作耗时 read_cost_us。收集到入参、出参以及耗时这五项参数后,作为入参调用 IOCanary#OnRead 函数。

/**
  *  Proxy for read: callback to the java layer
  */
ssize_t ProxyRead(int fd, void *buf, size_t size) {
    if(!IsMainThread()) {
        return original_read(fd, buf, size);
    }

    int64_t start = GetTickCountMicros();

    size_t ret = original_read(fd, buf, size);

    long read_cost_us = GetTickCountMicros() - start;

    //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyRead fd:%d buf:%p size:%d ret:%d cost:%d", fd, buf, size, ret, read_cost_us);

    iocanary::IOCanary::Get().OnRead(fd, buf, size, ret, read_cost_us);

    return ret;
}

ssize_t ProxyReadChk(int fd, void* buf, size_t count, size_t buf_size) {
    if(!IsMainThread()) {
        return original_read_chk(fd, buf, count, buf_size);
    }

    int64_t start = GetTickCountMicros();

    ssize_t ret = original_read_chk(fd, buf, count, buf_size);

    long read_cost_us = GetTickCountMicros() - start;

    //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyRead fd:%d buf:%p size:%d ret:%d cost:%d", fd, buf, size, ret, read_cost_us);

    iocanary::IOCanary::Get().OnRead(fd, buf, count, ret, read_cost_us);

    return ret;
}

在 IOCanary#OnRead 函数中,还是代理调用了 IOInfoCollector#OnRead 函数:

void IOCanary::OnRead(int fd, const void *buf, size_t size,
                        ssize_t read_ret, long read_cost) {
    collector_.OnRead(fd, buf, size, read_ret, read_cost);
}

void IOInfoCollector::OnRead(int fd, const void *buf, size_t size,
                            ssize_t read_ret, long read_cost) {

    if (read_ret == -1 || read_cost <0) {
        return;
    }

    if (info_map_.find(fd) == info_map_.end()) {
            //__android_log_print(ANDROID_LOG_DEBUG, kTag, "OnRead fd:%d not in info_map_", fd);
        return;
    }

    CountRWInfo(fd, FileOpType::kRead, size, read_cost);
}

如果 fd 在 map 中,也就是说如果 open 时被捕获到了,那么才会进入 CountRWInfo 这个函数。CountRWInfo 会记录 IOInfo 所代表的文件的累计读写操作次数、累计buffer size、累计操作耗时、单次读写最大耗时、当前连续读写操作耗时、最大连续读写操作耗时、本次操作时间戳、最大操作buffer size、操作类型这些数据,具体看下面代码即可,一目了然。

void IOInfoCollector::CountRWInfo(int fd, const FileOpType &fileOpType, long op_size, long rw_cost) {
    if (info_map_.find(fd) == info_map_.end()) {
        return;
    }

    // 获取系统的当前时间,单位微秒(us)
    const int64_t now = GetSysTimeMicros();

    // 累计读写操作次数累加
    info_map_[fd]->op_cnt_ ++;
    // 累计buffer size
    info_map_[fd]->op_size_ += op_size;
    // 累计文件读写耗时
    info_map_[fd]->rw_cost_us_ += rw_cost;

    // 单次文件读写最大耗时
    if (rw_cost > info_map_[fd]->max_once_rw_cost_time_μs_) {
        info_map_[fd]->max_once_rw_cost_time_μs_ = rw_cost;
    }

    //__android_log_print(ANDROID_LOG_DEBUG, kTag, "CountRWInfo rw_cost:%d max_once_rw_cost_time_:%d current_continual_rw_time_:%d;max_continual_rw_cost_time_:%d; now:%lld;last:%lld",
        //      rw_cost, info_map_[fd]->max_once_rw_cost_time_μs_, info_map_[fd]->current_continual_rw_time_μs_, info_map_[fd]->max_continual_rw_cost_time_μs_, now, info_map_[fd]->last_rw_time_ms_);

    // 连续读写耗时,若两次操作超过阈值(8000us,约为一帧耗时16.6667ms的一半),则不累计
    if (info_map_[fd]->last_rw_time_μs_ > 0 && (now - info_map_[fd]->last_rw_time_μs_) current_continual_rw_time_μs_ += rw_cost;
    } else {
        info_map_[fd]->current_continual_rw_time_μs_ = rw_cost;
    }

    // 最大连续读写耗时
    if (info_map_[fd]->current_continual_rw_time_μs_ > info_map_[fd]->max_continual_rw_cost_time_μs_) {
        info_map_[fd]->max_continual_rw_cost_time_μs_ = info_map_[fd]->current_continual_rw_time_μs_;
    }
    // 本次读写记录的时间戳
    info_map_[fd]->last_rw_time_μs_ = now;

    // 最大读写操作buffer size
    if (info_map_[fd]->buffer_size_ buffer_size_ = op_size;
    }

    // 读写操作类型
    if (info_map_[fd]->op_type_ == FileOpType::kInit) {
        info_map_[fd]->op_type_ = fileOpType;
    }
}

我们可以看到 read 时就将对 IOInfo 里面的字段进行了赋值。实际上对 write 操作的统计也和 read 操作类似,最后也是调用的 CountRWInfo 函数对写操作进行统计,这里不做更多赘述。

/**
 *  Proxy for write: callback to the java layer
 */
ssize_t ProxyWrite(int fd, const void *buf, size_t size) {
    if(!IsMainThread()) {
        return original_write(fd, buf, size);
    }

    int64_t start = GetTickCountMicros();

    size_t ret = original_write(fd, buf, size);

    long write_cost_μs = GetTickCountMicros() - start;

    //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyWrite fd:%d buf:%p size:%d ret:%d cost:%d", fd, buf, size, ret, write_cost_μs);

    iocanary::IOCanary::Get().OnWrite(fd, buf, size, ret, write_cost_μs);

    return ret;
}

close

close 时我们会对整个文件生命周期的一些操作进行最后的统计并通知 detector 进行检测上报。

我们先看看 close 的代理方法,如下所示。可以看到,只是调用 IOCanary#OnClose 方法。

/**
 *  Proxy for close: callback to the java layer
 */
int ProxyClose(int fd) {
    if(!IsMainThread()) {
        return original_close(fd);
    }

    int ret = original_close(fd);

    //__android_log_print(ANDROID_LOG_DEBUG, kTag, "ProxyClose fd:%d ret:%d", fd, ret);
    iocanary::IOCanary::Get().OnClose(fd, ret);

    return ret;
}

接着看看 IOCanary#OnClose 方法的实现,这里先调用了 IOInfoCollector#OnClose 方法进行最后的统计操作。 具体操作为:通过当前系统时间减去 IOInfo 创建的时间得到的文件操作的生命周期的总时间,以及 stat 函数获取文件的 size。最后从 map 中移除并返回该对象。

然后通过 OfferFileIOInfo 方法将此 IOInfo 提交给检测线程中让各个 detector 进行检测。

// matrix/matrix-android/matrix-io-canary/src/main/cpp/core/io_canary.cc
void IOCanary::OnClose(int fd, int close_ret) {
    std::shared_ptr info = collector_.OnClose(fd, close_ret);
    if (info == nullptr) {
        return;
    }

    OfferFileIOInfo(info);
}

// matrix/matrix-android/matrix-io-canary/src/main/cpp/core/io_info_collector.cc
std::shared_ptr IOInfoCollector::OnClose(int fd, int close_ret) {

    if (info_map_.find(fd) == info_map_.end()) {
        //__android_log_print(ANDROID_LOG_DEBUG, kTag, "OnClose fd:%d not in info_map_", fd);
        return nullptr;
    }

    // 系统当前时间减去IOInfo对象初始化的时间,则为整个文件的生命周期时间
    info_map_[fd]->total_cost_μs_ = GetSysTimeMicros() - info_map_[fd]->start_time_μs_;
    // 通过stat函数获取文件的实际尺寸
    info_map_[fd]->file_size_ = GetFileSize(info_map_[fd]->path_.c_str());
    std::shared_ptr info = info_map_[fd];
    info_map_.erase(fd);

    return info;
}

// matrix/matrix-android/matrix-io-canary/src/main/cpp/comm/io_canary_utils.cc
int GetFileSize(const char* file_path) {
    struct stat stat_buf;
    if (-1 == stat(file_path, &stat_buf)) {
        return -1;
    }
    return stat_buf.st_size;
}

下面我们看看 OfferFileIOInfo 的相关实现,这是 C++ 实现的一个生产者消费者模型。具体代码如下,这里不做过多讲解。 我们看到 Detect 函数,这个函数运行在 IOCanary 初始化时就创建的工作线程中,在取到 IOInfo 之后,就挨个调用 detector 进行检测,并将检测结果添加到 published_issues 这个数组中。最后调用 issued_callback_ 这个函数指针进行上报。实际上这里的 issued_callback_ 就是 io_canary_jni.cc 中的 OnIssuePublish 函数。

// io_canary.h
class IOCanary {
    ...
private:
    ...
    std::deque> queue_;
    std::mutex queue_mutex_;
    std::condition_variable queue_cv_;
}

// io_canary.cc
// 生产者
void IOCanary::OfferFileIOInfo(std::shared_ptr file_io_info) {
    std::unique_lock lock(queue_mutex_);
    queue_.push_back(file_io_info);
    queue_cv_.notify_one();
    lock.unlock();
}

IOCanary::IOCanary() {
    exit_ = false;
    std::thread detect_thread(&IOCanary::Detect, this);
    detect_thread.detach();
}

void IOCanary::Detect() {
    std::vector published_issues;
    std::shared_ptr file_io_info;
    while (true) {
        published_issues.clear();

        // 阻塞直到获取到IOInfo
        int ret = TakeFileIOInfo(file_io_info);

        if (ret != 0) {
            break;
        }

        // 将IOInfo交给各个detector进行检测
        for (auto detector : detectors_) {
            detector->Detect(env_, *file_io_info, published_issues);
        }

        // 若可以上报,则进行上报
        if (issued_callback_ && !published_issues.empty()) {
            issued_callback_(published_issues);
        }

        file_io_info = nullptr;
    }
}

// 消费者
int IOCanary::TakeFileIOInfo(std::shared_ptr &file_io_info) {
    std::unique_lock lock(queue_mutex_);

    while (queue_.empty()) {
        queue_cv_.wait(lock);
        if (exit_) {
            return -1;
        }
    }

    file_io_info = queue_.front();
    queue_.pop_front();
    return 0;
}

IO检测策略

主线程IO-FileIOMainThreadDetector

void FileIOMainThreadDetector::Detect(const IOCanaryEnv &env, const IOInfo &file_io_info,
                                          std::vector& issues) {

        if (GetMainThreadId() == file_io_info.java_context_.thread_id_) {
            int type = 0;
            //可能引起卡顿的主线程IO,默认值13ms
            if (file_io_info.max_continual_rw_cost_time_μs_ > IOCanaryEnv::kPossibleNegativeThreshold) {
                type = 1;
            }
            //引起主线程严重性能问题的IO,默认500ms
            if(file_io_info.max_continual_rw_cost_time_μs_ > env.GetMainThreadThreshold()) {
                type |= 2;
            }

            if (type != 0) {
                Issue issue(kType, file_io_info);
                issue.repeat_read_cnt_ = type;  //use repeat to record type
                PublishIssue(issue, issues);
            }
        }
    }

Small Buffer IO-FileIOSmallBufferDetector

void FileIOSmallBufferDetector::Detect(const IOCanaryEnv &env, const IOInfo &file_io_info,
                                           std::vector& issues) {
        //单次操作的字节数小于阈值                                   
        if (file_io_info.op_cnt_ > env.kSmallBufferOpTimesThreshold && (file_io_info.op_size_ / file_io_info.op_cnt_) = env.kPossibleNegativeThreshold) {

            PublishIssue(Issue(kType, file_io_info), issues);
        }
    }

Repeat Read IO-FileIORepeatReadDetector

 void FileIORepeatReadDetector::Detect(const IOCanaryEnv &env,
                                          const IOInfo &file_io_info,
                                          std::vector& issues) {

        const std::string& path = file_io_info.path_;
        if (observing_map_.find(path) == observing_map_.end()) {
            if (file_io_info.max_continual_rw_cost_time_μs_ ()));
        }

        std::vector& repeat_infos = observing_map_[path];
        //有write行为,清空repeat_info
        if (file_io_info.op_type_ == FileOpType::kWrite) {
            repeat_infos.clear();
            return;
        }

        RepeatReadInfo repeat_read_info(file_io_info.path_, file_io_info.java_context_.stack_, file_io_info.java_context_.thread_id_,
                                      file_io_info.op_size_, file_io_info.file_size_);

        if (repeat_infos.size() == 0) {
            repeat_infos.push_back(repeat_read_info);
            return;
        }
        
        //read操作间隔17ms,清空repeat_info
        if((GetTickCount() - repeat_infos[repeat_infos.size() - 1].op_timems) > 17) {   //17ms todo astrozhou add to params
            repeat_infos.clear();
        }

        bool found = false;
        int repeatCnt;
        for (auto& info : repeat_infos) {
            if (info == repeat_read_info) {
                found = true;

                info.IncRepeatReadCount();

                repeatCnt = info.GetRepeatReadCount();
                break;
            }
        }

        if (!found) {
            repeat_infos.push_back(repeat_read_info);
            return;
        }
        //重复read次数达到阈值,上报IO Issue
        if (repeatCnt >= env.GetRepeatReadThreshold()) {
            Issue issue(kType, file_io_info);
            issue.repeat_read_cnt_ = repeatCnt;
            issue.stack = repeat_read_info.GetStack();
            PublishIssue(issue, issues);
        }
    }

PLT(GOT)Hook介绍

Native Hook大体上可以分为PLT(GOT) Hook、ART Hook(基于ART虚拟机)、Dalvik Hook(基于Dalvik虚拟机)、inline Hook这几类Hook手段。

PLT(GOT) Hook是基于so(实际是一个elf格式的文件)的GOT跳转表实现的。

对于PLT(GOT) HOOK,需要关注的是ELF文件链接视图下名为.plt和.got的Section。

plt Section说明:

got Section说明:

先来介绍一下Android PLT Hook的基本原理。Linux在执行动态链接的ELF的时候,为了优化性能使用了一个叫延时绑定的策略。相关资料有很多,这边简述一下:这个策略是为了解决原本静态编译时要把各种系统API的具体实现代码都编译进当前ELF文件里导致文件巨大臃肿的问题。所以当在动态链接的ELF程序里调用共享库的函数时,第一次调用时先去查找PLT表中相应的项目,而PLT表中再跳跃到GOT表中希望得到该函数的实际地址,但这时GOT表中指向的是PLT中那条跳跃指令下面的代码,最终会执行_dl_runtime_resolve()并执行目标函数。第二次调用时也是PLT跳转到GOT表,但是GOT中对应项目已经在第一次_dl_runtime_resolve()中被修改为函数实际地址,因此第二次及以后的调用直接就去执行目标函数,不用再去执行_dl_runtime_resolve()了。因此,PLT Hook通过直接修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。

PLT(GOT) Hook代码实现

解析需要hook的so文件,封装一个loaded_soinfo对象。

查找GOT表中是否有对应的方法声明。

locate_symbol内部调用locate_symbol_hash。

备选方案, locate_symbol_hash失败后会走到这个方法。

实际替换对应的函数地址。

Android系统加载so的过程

源码地址位于android / platform / bionic / froyo / . / linker / linker.c

链接so文件(elf文件格式)

so文件(ELF文件)中的Section包括三种状态:

  • Alloc:Section在进程执行过程中占用内存;
  • Write:Section包含进程执行过程中可写的数据;
  • Execute:Section包含可执行的机器指令;

PLT(GOT) Hook总结

技术特点:

  • 由于修改的是GOT表中的数据,因此修改后,所有对该函数进行调用的地方就都会被Hook到。这个效果的影响范围是该PLT和GOT所处的整个so库。因此,当目标so库中多行被执行代码都调用了该PLT项所对应的函数,那它们都会去执行Hook功能。
  • PLT与GOT表中仅仅包含本ELF需要调用的共享库函数项目,因此不在PLT表中的函数无法Hook到。

应用场景:

  • 可以大量Hook那些系统API,但是难以精准Hook住某次函数调用。这比较适用于开发者对于自家APP性能监控的需求。比如Hook住malloc使其输出参数,这样就能大量统计评估该APP对于内存的需求。但是对于一些对Hook对象有一定精准度要求的需求来说很不利,比如说是安全测试或者逆向分析的工作需求,这些工作中往往需要对于目标so中的某些关键点有准确的观察。

  • 对于一些so内部自定义的函数无法Hook到。因为这些函数不在PLT表和GOT表里。这个缺点对于不少软件分析者来说可能是无法忍受的。因为许多关键或核心的代码逻辑往往都是自定义的。例如NDK中实现的一些加密工作,即使使用了共享库中的加密函数,但秘钥的保存管理等依然需要进一步分析,而这些工作对于自定义函数甚至是某行汇编代码的监控能力要求是远远超出PLT Hook所能提供的范围。


推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
author-avatar
wz44_798
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有