面试官:简历上最好不要写Glide,不是问源码那么简单,2024年最新面试的时候突然遇到答不上的问题怎么办

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

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

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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注Android)
img

正文

看下LinkedHashMap 的 createEntry方法


void createEntry(int hash, K key, V value, int bucketIndex) {

    HashMapEntry<K,V> old = table[bucketIndex];

    LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);

    table[bucketIndex] = e; //数组的添加

    e.addBefore(header);  //处理链表

    size++;

}

复制代码



LinkedHashMap的数组里面放的是LinkedHashMapEntry对象

LinkedHashMapEntry


private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {

    // These fields comprise the doubly linked list used for iteration.

    LinkedHashMapEntry<K,V> before, after; //双向链表



	private void remove() {

        before.after = after;

        after.before = before;

    }



	private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {

        after  = existingEntry;

        before = existingEntry.before;

        before.after = this;

        after.before = this;

    }

复制代码



LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBeforeremove 方法,用于新增和删除链表节点。

LinkedHashMapEntry#addBefore

将一个数据添加到Header的前面


private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {

        after  = existingEntry;

        before = existingEntry.before;

        before.after = this;

        after.before = this;

}

复制代码



existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。

再看下LinkedHashMapEntry#remove


private void remove() {

        before.after = after;

        after.before = before;

    }

复制代码



链表节点的移除比较简单,改变指针指向即可。

再看下LinkHashMap的put 方法


public final V put(K key, V value) {



    V previous;

    synchronized (this) {

        putCount++;

        //size增加

        size += safeSizeOf(key, value);

        // 1、linkHashMap的put方法

        previous = map.put(key, value);

        if (previous != null) {

            //如果有旧的值,会覆盖,所以大小要减掉

            size -= safeSizeOf(key, previous);

        }

    }



    trimToSize(maxSize);

    return previous;

}

复制代码



LinkedHashMap 结构可以用这种图表示

LinkHashMap 的 put方法和get方法最后会调用trimToSize方法,LruCache 重写trimToSize方法,判断内存如果超过一定大小,则移除最老的数据

LruCache#trimToSize,移除最老的数据


public void trimToSize(int maxSize) {

    while (true) {

        K key;

        V value;

        synchronized (this) {



            //大小没有超出,不处理

            if (size <= maxSize) {

                break;

            }



            //超出大小,移除最老的数据

            Map.Entry<K, V> toEvict = map.eldest();

            if (toEvict == null) {

                break;

            }



            key = toEvict.getKey();

            value = toEvict.getValue();

            map.remove(key);

            //这个大小的计算,safeSizeOf 默认返回1;

            size -= safeSizeOf(key, value);

            evictionCount++;

        }



        entryRemoved(true, key, value, null);

    }

}

复制代码



对LinkHashMap 还不是很理解的话可以参考:

图解LinkedHashMap原理

LruCache小结:

  • LinkHashMap 继承HashMap,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。

  • LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。

2.3.2 磁盘缓存 DiskLruCache

依赖:

implementation ‘com.jakewharton:disklrucache:2.0.2’

DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。简单看下remove操作:


	// DiskLruCache 内部也是用LinkedHashMap

	private final LinkedHashMap<String, Entry> lruEntries =

      	new LinkedHashMap<String, Entry>(0, 0.75f, true);

	...



    public synchronized boolean remove(String key) throws IOException {

	    checkNotClosed();

	    validateKey(key);

	    Entry entry = lruEntries.get(key);

	    if (entry == null || entry.currentEditor != null) {

	      return false;

	    }



            //一个key可能对应多个value,hash冲突的情况

	    for (int i = 0; i < valueCount; i++) {

	      File file = entry.getCleanFile(i);

            //通过 file.delete() 删除缓存文件,删除失败则抛异常

	      if (file.exists() && !file.delete()) {

	        throw new IOException("failed to delete " + file);

	      }

	      size -= entry.lengths[i];

	      entry.lengths[i] = 0;

	    }

	    ...

	    return true;

  }

复制代码



可以看到 DiskLruCache 同样是利用LinkHashMap的特点,只不过数组里面存的 Entry 有点变化,Editor 用于操作文件。


private final class Entry {

    private final String key;



    private final long[] lengths;



    private boolean readable;



    private Editor currentEditor;



    private long sequenceNumber;

	...

}

复制代码



2.4 防止OOM

加载图片非常重要的一点是需要防止OOM,上面的LruCache缓存大小设置,可以有效防止OOM,但是当图片需求比较大,可能需要设置一个比较大的缓存,这样的话发生OOM的概率就提高了,那应该探索其它防止OOM的方法。

方法1:软引用

回顾一下Java的四大引用:

  • 强引用: 普通变量都属于强引用,比如 private Context context;

  • 软应用: SoftReference,在发生OOM之前,垃圾回收器会回收SoftReference引用的对象。

  • 弱引用: WeakReference,发生GC的时候,垃圾回收器会回收WeakReference中的对象。

  • 虚引用: 随时会被回收,没有使用场景。

怎么理解强引用:

强引用对象的回收时机依赖垃圾回收算法,我们常说的可达性分析算法,当Activity销毁的时候,Activity会跟GCRoot断开,至于GCRoot是谁?这里可以大胆猜想,Activity对象的创建是在ActivityThread中,ActivityThread要回调Activity的各个生命周期,肯定是持有Activity引用的,那么这个GCRoot可以认为就是ActivityThread,当Activity 执行onDestroy的时候,ActivityThread 就会断开跟这个Activity的联系,Activity到GCRoot不可达,所以会被垃圾回收器标记为可回收对象。

软引用的设计就是应用于会发生OOM的场景,大内存对象如Bitmap,可以通过 SoftReference 修饰,防止大对象造成OOM,看下这段代码


    private static LruCache<String, SoftReference<Bitmap>> mLruCache = new LruCache<String, SoftReference<Bitmap>>(10 * 1024){

        @Override

        protected int sizeOf(String key, SoftReference<Bitmap> value) {

            //默认返回1,这里应该返回Bitmap占用的内存大小,单位:K



            //Bitmap被回收了,大小是0

            if (value.get() == null){

                return 0;

            }

            return value.get().getByteCount() /1024;

        }

    };



复制代码



LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,也就是说通过SoftReference修饰的Bitmap就不会导致OOM。

当然,这段代码存在一些问题,Bitmap被回收的时候,LruCache剩余的大小应该重新计算,可以写个方法,当Bitmap取出来是空的时候,LruCache清理一下,重新计算剩余内存;

还有另一个问题,就是内存不足时软引用中的Bitmap被回收的时候,这个LruCache就形同虚设,相当于内存缓存失效了,必然出现效率问题。

方法2:onLowMemory

当内存不足的时候,Activity、Fragment会调用onLowMemory方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。


//Glide

public void onLowMemory() {

    clearMemory();

}



public void clearMemory() {

    // Engine asserts this anyway when removing resources, fail faster and consistently

    Util.assertMainThread();

    // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.

    memoryCache.clearMemory();

    bitmapPool.clearMemory();

    arrayPool.clearMemory();

  }

复制代码



方法3:从Bitmap 像素存储位置考虑

我们知道,系统为每个进程,也就是每个虚拟机分配的内存是有限的,早期的16M、32M,现在100+M,

虚拟机的内存划分主要有5部分:

  • 虚拟机栈

  • 本地方法栈

  • 程序计数器

  • 方法区

而对象的分配一般都是在堆中,堆是JVM中最大的一块内存,OOM一般都是发生在堆中。

Bitmap 之所以占内存大不是因为对象本身大,而是因为Bitmap的像素数据, Bitmap的像素数据大小 = 宽 * 高 * 1像素占用的内存。

1像素占用的内存是多少?不同格式的Bitmap对应的像素占用内存是不同的,具体是多少呢?

在Fresco中看到如下定义代码


  /**

   * Bytes per pixel definitions

   */

  public static final int ALPHA_8_BYTES_PER_PIXEL = 1;

  public static final int ARGB_4444_BYTES_PER_PIXEL = 2;

  public static final int ARGB_8888_BYTES_PER_PIXEL = 4;

  public static final int RGB_565_BYTES_PER_PIXEL = 2;

  public static final int RGBA_F16_BYTES_PER_PIXEL = 8;

复制代码



如果Bitmap使用 RGB_565 格式,则1像素占用 2 byte,ARGB_8888 格式则占4 byte。

在选择图片加载框架的时候,可以将内存占用这一方面考虑进去,更少的内存占用意味着发生OOM的概率越低。 Glide内存开销是Picasso的一半,就是因为默认Bitmap格式不同。

至于宽高,是指Bitmap的宽高,怎么计算的呢?看BitmapFactory.Options 的 outWidth


/**

     * The resulting width of the bitmap. If {@link #inJustDecodeBounds} is

     * set to false, this will be width of the output bitmap after any

     * scaling is applied. If true, it will be the width of the input image

     * without any accounting for scaling.

     *

     * <p>outWidth will be set to -1 if there is an error trying to decode.</p>

     */

    public int outWidth;

复制代码



看注释的意思,如果 BitmapFactory.Options 中指定 inJustDecodeBounds 为true,则为原图宽高,如果是false,则是缩放后的宽高。所以我们一般可以通过压缩来减小Bitmap像素占用内存

扯远了,上面分析了Bitmap像素数据大小的计算,只是说明Bitmap像素数据为什么那么大。那是否可以让像素数据不放在java堆中,而是放在native堆中呢?据说Android 3.0到8.0 之间Bitmap像素数据存在Java堆,而8.0之后像素数据存到native堆中,是不是真的?看下源码就知道了~

8.0 Bitmap

java层创建Bitmap方法


    public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,

            @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {

        ...

        Bitmap bm;

        ...

        if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {

            //最终都是通过native方法创建

            bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);

        } else {

            bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,

                    d50.getTransform(), parameters);

        }



        ...

        return bm;

    }



复制代码



Bitmap 的创建是通过native方法 nativeCreate

对应源码 8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp


//Bitmap.cpp

static const JNINativeMethod gBitmapMethods[] = {

    {   "nativeCreate",             "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;",

        (void*)Bitmap_creator },

...

复制代码



JNI动态注册,nativeCreate 方法 对应 Bitmap_creator


//Bitmap.cpp

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,

                              jint offset, jint stride, jint width, jint height,

                              jint configHandle, jboolean isMutable,

                              jfloatArray xyzD50, jobject transferParameters) {

    ...

    //1\. 申请堆内存,创建native层Bitmap

    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap, NULL);

    if (!nativeBitmap) {

        return NULL;

    }



    ...

    //2.创建java层Bitmap

    return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));

}

复制代码



主要两个步骤:

  1. 申请内存,创建native层Bitmap,看下allocateHeapBitmap方法

    8.0.0_r4/xref/frameworks/base/libs/hwui/hwui/Bitmap.cpp


//

static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,

        SkColorTable* ctable) {

    // calloc 是c++ 的申请内存函数

    void* addr = calloc(size, 1);

    if (!addr) {

        return nullptr;

    }

    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));

}

复制代码



可以看到通过c++的 calloc 函数申请了一块内存空间,然后创建native层Bitmap对象,把内存地址传过去,也就是native层的Bitmap数据(像素数据)是存在native堆中。

  1. 创建java 层Bitmap

//Bitmap.cpp

jobject createBitmap(JNIEnv* env, Bitmap* bitmap,

        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,

        int density) {

    ...

    BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);

     //通过JNI回调Java层,调用java层的Bitmap构造方法

    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,

            reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,

            isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);



   ...

    return obj;

}



复制代码



env->NewObject,通过JNI创建Java层Bitmap对象,gBitmap_class,gBitmap_constructorMethodID这些变量是什么意思,看下面这个方法,对应java层的Bitmap的类名和构造方法。


//Bitmap.cpp

int register_android_graphics_Bitmap(JNIEnv* env)

{

    gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap"));

    gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J");

    gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V");

    gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V");

    gBitmap_getAllocationByteCountMethodID = GetMethodIDOrDie(env, gBitmap_class, "getAllocationByteCount", "()I");

    return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods,

                                         NELEM(gBitmapMethods));

}

复制代码



8.0 的Bitmap创建就两个点:

  1. 创建native层Bitmap,在native堆申请内存。

  2. 通过JNI创建java层Bitmap对象,这个对象在java堆中分配内存。

像素数据是存在native层Bitmap,也就是证明8.0的Bitmap像素数据存在native堆中。

7.0 Bitmap

直接看native层的方法,

/7.0.0_r31/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp


//JNI动态注册

static const JNINativeMethod gBitmapMethods[] = {

    {   "nativeCreate",             "([IIIIIIZ)Landroid/graphics/Bitmap;",

        (void*)Bitmap_creator },

...



static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,

                              jint offset, jint stride, jint width, jint height,

                              jint configHandle, jboolean isMutable) {

    ... 

    //1.通过这个方法来创建native层Bitmap

    Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);

    ...



    return GraphicsJNI::createBitmap(env, nativeBitmap,

            getPremulBitmapCreateFlags(isMutable));

}



复制代码



native层Bitmap 创建是通过GraphicsJNI::allocateJavaPixelRef,看看里面是怎么分配的, GraphicsJNI 的实现类是Graphics.cpp


android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,

                                             SkColorTable* ctable) {

    const SkImageInfo& info = bitmap->info();



    size_t size;

    //计算需要的空间大小

    if (!computeAllocationSize(*bitmap, &size)) {

        return NULL;

    }



    // we must respect the rowBytes value already set on the bitmap instead of

    // attempting to compute our own.

    const size_t rowBytes = bitmap->rowBytes();

    // 1\. 创建一个数组,通过JNI在java层创建的

    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,

                                                             gVMRuntime_newNonMovableArray,

                                                             gByte_class, size);

    ...

    // 2\. 获取创建的数组的地址

    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);

    ...

    //3\. 创建Bitmap,传这个地址

    android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,

            info, rowBytes, ctable);

    wrapper->getSkBitmap(bitmap);

    // since we're already allocated, we lockPixels right away

    // HeapAllocator behaves this way too

    bitmap->lockPixels();



    return wrapper;

}

复制代码



可以看到,7.0 像素内存的分配是这样的:

  1. 通过JNI调用java层创建一个数组

  2. 然后创建native层Bitmap,把数组的地址传进去。

由此说明,7.0 的Bitmap像素数据是放在java堆的。

当然,3.0 以下Bitmap像素内存据说也是放在native堆的,但是需要手动释放native层的Bitmap,也就是需要手动调用recycle方法,native层内存才会被回收。这个大家可以自己去看源码验证。

native层Bitmap 回收问题

Java层的Bitmap对象由垃圾回收器自动回收,而native层Bitmap印象中我们是不需要手动回收的,源码中如何处理的呢?

记得有个面试题是这样的:

说说final、finally、finalize 的关系

三者除了长得像,其实没有半毛钱关系,final、finally大家都用的比较多,而 finalize 用的少,或者没用过,finalize 是 Object 类的一个方法,注释是这样的:


/**

     * Called by the garbage collector on an object when garbage collection

     * determines that there are no more references to the object.

     * A subclass overrides the {@code finalize} method to dispose of

     * system resources or to perform other cleanup.

     * <p>

     ...**/

  protected void finalize() throws Throwable { }

复制代码



意思是说,垃圾回收器确认这个对象没有其它地方引用到它的时候,会调用这个对象的finalize方法,子类可以重写这个方法,做一些释放资源的操作。

在6.0以前,Bitmap 就是通过这个finalize 方法来释放native层对象的。 6.0 Bitmap.java


Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,

            boolean isMutable, boolean requestPremultiplied,

            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {

        ...

        mNativePtr = nativeBitmap;

        //1.创建 BitmapFinalizer

        mFinalizer = new BitmapFinalizer(nativeBitmap);

        int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);

        mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);

}



 private static class BitmapFinalizer {

        private long mNativeBitmap;



        // Native memory allocated for the duration of the Bitmap,

        // if pixel data allocated into native memory, instead of java byte[]

        private int mNativeAllocationByteCount;



        BitmapFinalizer(long nativeBitmap) {

            mNativeBitmap = nativeBitmap;

        }



        public void setNativeAllocationByteCount(int nativeByteCount) {

            if (mNativeAllocationByteCount != 0) {

                VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);

            }

            mNativeAllocationByteCount = nativeByteCount;

            if (mNativeAllocationByteCount != 0) {

                VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);

            }

        }



        @Override

        public void finalize() {

            try {

                super.finalize();

            } catch (Throwable t) {

                // Ignore

            } finally {

                //2.就是这里了,

                setNativeAllocationByteCount(0);

                nativeDestructor(mNativeBitmap);

                mNativeBitmap = 0;

            }

        }

    }



复制代码



最后

**一个零基础的新人,我认为坚持是最最重要的。**我的很多朋友都找我来学习过,我也很用心的教他们,可是不到一个月就坚持不下来了。我认为他们坚持不下来有两点主要原因:

他们打算入行不是因为兴趣,而是因为所谓的IT行业工资高,或者说完全对未来没有任何规划。

刚开始学的时候确实很枯燥,这确实对你是个考验,所以说坚持下来也很不容易,但是如果你有兴趣就不会认为这是累,不会认为这很枯燥,总之还是贵在坚持。

技术提升遇到瓶颈了?缺高级Android进阶视频学习提升自己吗?还有大量大厂面试题为你面试做准备!

提升自己去挑战一下BAT面试难关吧

对于很多Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些知识图谱希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

不论遇到什么困难,都不应该成为我们放弃的理由!

如果有什么疑问的可以直接私我,我尽自己最大力量帮助你!

最后祝各位新人都能坚持下来,学有所成。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

@Override

    public void finalize() {

        try {

            super.finalize();

        } catch (Throwable t) {

            // Ignore

        } finally {

            //2.就是这里了,

            setNativeAllocationByteCount(0);

            nativeDestructor(mNativeBitmap);

            mNativeBitmap = 0;

        }

    }

}

复制代码



### 最后

**一个零基础的新人,我认为坚持是最最重要的。**我的很多朋友都找我来学习过,我也很用心的教他们,可是不到一个月就坚持不下来了。我认为他们坚持不下来有两点主要原因:

他们打算入行不是因为兴趣,而是因为所谓的IT行业工资高,或者说完全对未来没有任何规划。

刚开始学的时候确实很枯燥,这确实对你是个考验,所以说坚持下来也很不容易,但是如果你有兴趣就不会认为这是累,不会认为这很枯燥,总之还是贵在坚持。

**技术提升遇到瓶颈了?缺高级Android进阶视频学习提升自己吗?还有大量大厂面试题为你面试做准备!**

**提升自己去挑战一下BAT面试难关吧**

[外链图片转存中...(img-CuZe9Zdt-1713386795071)]

对于很多Android工程师而言,想要提升技能,往往是自己摸索成长,**不成体系的学习效果低效漫长且无助**。整理的这些知识图谱希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

> **不论遇到什么困难,都不应该成为我们放弃的理由!**

如果有什么疑问的可以直接私我,我尽自己最大力量帮助你!

最后祝各位新人都能坚持下来,学有所成。



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)**
[外链图片转存中...(img-e3rOR8Rx-1713386795072)]

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值