在崩溃分析中,我说过有部分的 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移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
光有这些思路和搞懂单个知识的应用是还远远不够的,在Android开源框架设计思想中的知识点还是比较多的,想要搞懂还得学会整理和规划:我们常见的**Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架,**这些都是属于Android开源框架设计思想的。如下图所示:
这位阿里P8大佬针对以上知识点,熬夜整理出了一本长达1042页的完整版如何解读开源框架设计思想PDF文档,内容详细,把Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。
由于文档内容过多,篇幅受限,只能截图展示部分,更为了不影响阅读,这份文档已经打包在GitHub,有需要的朋友可以直接点此处前往免费下载。
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架,**这些都是属于Android开源框架设计思想的。如下图所示:
[外链图片转存中…(img-AuyurHpv-1710701032800)]
这位阿里P8大佬针对以上知识点,熬夜整理出了一本长达1042页的完整版如何解读开源框架设计思想PDF文档,内容详细,把Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。
由于文档内容过多,篇幅受限,只能截图展示部分,更为了不影响阅读,这份文档已经打包在GitHub,有需要的朋友可以直接点此处前往免费下载。
[外链图片转存中…(img-soMIcsBR-1710701032800)]
[外链图片转存中…(img-EiVnsdkc-1710701032801)]
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
你的支持,我的动力;祝各位前程似锦,offer不断!!!