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);
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;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
写在最后
对程序员来说,很多技术的学习都是“防御性”的。也就是说,我们是在为未来学习。我们学习新技术的目的,或是为了在新项目中应用,或仅仅是为了将来的面试。但不管怎样,一定不能“止步不前”,不能荒废掉。
![
文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等,需要这些文档资料的,直接点击我的GitHub免费领取~
得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)**
[外链图片转存中…(img-i3bIfi7A-1710701067407)]
写在最后
对程序员来说,很多技术的学习都是“防御性”的。也就是说,我们是在为未来学习。我们学习新技术的目的,或是为了在新项目中应用,或仅仅是为了将来的面试。但不管怎样,一定不能“止步不前”,不能荒废掉。
[外链图片转存中…(img-tUeJkPzE-1710701067408)]
[外链图片转存中…(img-NfkQv4jo-1710701067408)]
[外链图片转存中…(img-jbZVqG9K-1710701067409)]
[外链图片转存中…(img-bB1akrEw-1710701067409)]
文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等,需要这些文档资料的,直接点击我的GitHub免费领取~