性能优化之matrix学习-IO Canary,2024年上半年最接地气的Android面经

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移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

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

写在最后

对程序员来说,很多技术的学习都是“防御性”的。也就是说,我们是在为未来学习。我们学习新技术的目的,或是为了在新项目中应用,或仅仅是为了将来的面试。但不管怎样,一定不能“止步不前”,不能荒废掉。

![
[]


文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等,需要这些文档资料的,直接点击我的GitHub免费领取~

得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)**
[外链图片转存中…(img-i3bIfi7A-1710701067407)]

写在最后

对程序员来说,很多技术的学习都是“防御性”的。也就是说,我们是在为未来学习。我们学习新技术的目的,或是为了在新项目中应用,或仅仅是为了将来的面试。但不管怎样,一定不能“止步不前”,不能荒废掉。

[外链图片转存中…(img-tUeJkPzE-1710701067408)]

[外链图片转存中…(img-NfkQv4jo-1710701067408)]
[]

[外链图片转存中…(img-jbZVqG9K-1710701067409)]
[外链图片转存中…(img-bB1akrEw-1710701067409)]

文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等,需要这些文档资料的,直接点击我的GitHub免费领取~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值