性能优化之matrix学习-IO Canary(2)

在崩溃分析中,我说过有部分的 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/IOCanaryJniBridgeKaTeX parse error: Expected group after '_' at position 54: …ls == NULL) { _̲_android_log_pr…JavaContext
kJavaContextClass = reinterpret_cast(env->NewGlobalRef(temp_java_context_cls));
// IOCanaryJniBridge J a v a C o n t e x t . s t a c k k F i e l d I D S t a c k = e n v − > G e t F i e l d I D ( k J a v a C o n t e x t C l a s s , " s t a c k " , " L j a v a / l a n g / S t r i n g ; " ) ; / / I O C a n a r y J n i B r i d g e JavaContext.stack kFieldIDStack = env->GetFieldID(kJavaContextClass, "stack", "Ljava/lang/String;"); // IOCanaryJniBridge JavaContext.stackkFieldIDStack=env>GetFieldID(kJavaContextClass,"stack","Ljava/lang/String;");//IOCanaryJniBridgeJavaContext.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 < TARGET_MODULE_COUNT; ++i) {
const char* so_name = TARGET_MODULES[i];
__android_log_print(ANDROID_LOG_INFO, kTag, “try to hook function in %s.”, so_name);

void* soinfo = xhook_elf_open(so_name);
if (!soinfo) {
__android_log_print(ANDROID_LOG_WARN, kTag, “Failure to open %s, try next.”, so_name);
continue;
}

xhook_hook_symbol(soinfo, “open”, (void*)ProxyOpen, (void**)&original_open);
xhook_hook_symbol(soinfo, “open64”, (void*)ProxyOpen64, (void**)&original_open64);

bool is_libjavacore = (strstr(so_name, “libjavacore.so”) != nullptr);
if (is_libjavacore) {
if (xhook_hook_symbol(soinfo, “read”, (void*)ProxyRead, (void**)&original_read) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, “doHook hook read failed, try __read_chk”);
if (xhook_hook_symbol(soinfo, “__read_chk”, (void*)ProxyReadChk, (void**)&original_read_chk) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, “doHook hook failed: __read_chk”);
xhook_elf_close(soinfo);
return JNI_FALSE;
}
}

if (xhook_hook_symbol(soinfo, “write”, (void*)ProxyWrite, (void**)&original_write) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, “doHook hook write failed, try __write_chk”);
if (xhook_hook_symbol(soinfo, “__write_chk”, (void*)ProxyWriteChk, (void**)&original_write_chk) != 0) {
__android_log_print(ANDROID_LOG_WARN, kTag, “doHook hook failed: __write_chk”);
xhook_elf_close(soinfo);
return JNI_FALSE;
}
}
}

xhook_hook_symbol(soinfo, “close”, (void*)ProxyClose, (void**)&original_close);

xhook_elf_close(soinfo);
}

__android_log_print(ANDROID_LOG_INFO, kTag, “doHook done.”);
return JNI_TRUE;
}

在上面的代码中,分别 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);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后

光有这些思路和搞懂单个知识的应用是还远远不够的,在Android开源框架设计思想中的知识点还是比较多的,想要搞懂还得学会整理和规划:我们常见的**Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架,**这些都是属于Android开源框架设计思想的。如下图所示:

image

这位阿里P8大佬针对以上知识点,熬夜整理出了一本长达1042页的完整版如何解读开源框架设计思想PDF文档,内容详细,把Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。

由于文档内容过多,篇幅受限,只能截图展示部分,更为了不影响阅读,这份文档已经打包在GitHub,有需要的朋友可以直接点此处前往免费下载

image

image

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架,**这些都是属于Android开源框架设计思想的。如下图所示:

[外链图片转存中…(img-AuyurHpv-1710701032800)]

这位阿里P8大佬针对以上知识点,熬夜整理出了一本长达1042页的完整版如何解读开源框架设计思想PDF文档,内容详细,把Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。

由于文档内容过多,篇幅受限,只能截图展示部分,更为了不影响阅读,这份文档已经打包在GitHub,有需要的朋友可以直接点此处前往免费下载

[外链图片转存中…(img-soMIcsBR-1710701032800)]

[外链图片转存中…(img-EiVnsdkc-1710701032801)]

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!!!

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值