Android-举一反三:12个View绘制流程高频面试题,带你全面理解View的绘制流程

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

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

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

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

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

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

正文

// do this last because it fires off messages to start doing things

try {

root.setView(view, wparams, panelParentView);

} catch (RuntimeException e) {

}

}

}

// ViewRootImpl.setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

requestLayout();

}

最终通过 WindowManagerImpl.addView -> WindowManagerGlobal.addView -> ViewRootImpl.setView -> ViewRootImpl.requestLayout 就触发了第一次 View 的绘制。

2. ViewRootImpl 创建的时机?

从上面流程里可以看到,ViewRootImpl 也是在 ActivityThread.handleResumeActivity 里创建的。

3. ViewRootImpl 和 DecorView 的关系是什么?

// ViewRootImpl.setView

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

requestLayout();

// …

// 这里的 view 是 DecorView

view.assignParent(this);

}

接着上面的代码看,在 ViewRootImpl.setView 里,通过 DecorView.assignParent 把 ViewRootImpl 设置为 DecorView 的 parent。

所以 ViewRootImpl 和 DecorView 的关系就是 ViewRootImpl 是 DecorView 的 parent。

因为 DecorView 是我们布局的顶层,现在我们就知道层层调用 requestLayout 等方法是怎么调用到 ViewRootImpl 里的了。

4. DecorView 的布局是什么样的?

对于 Activity 的层级,大家应该都看过一张图的描述,Activity -> PhoneWindow -> DecorView -> [title_bar, content],其中 DecorView 里包括了 title_bar 和 content 两个 View,不过这个是默认的布局,实际上根据不同的主题样式,DecorView 对应有不同的布局。

图中所包含的 title_bar 和 content 对应的是 R.layout.screen_simple 布局。

那么这么多布局,是在什么时候设置的呢?

是在 PhoneWindow.installDecor -> generateLayout 中设置的。

// PhoneWindow

private void installDecor() {

mForceDecorInstall = false;

if (mDecor == null) {

// 生成 DecorView

mDecor = generateDecor(-1);

mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

mDecor.setIsRootNamespace(true);

if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {

mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);

}

} else {

mDecor.setWindow(this);

}

if (mContentParent == null) {

mContentParent = generateLayout(mDecor); // 生成 DecorView 子View

}

}

protected ViewGroup generateLayout(DecorView decor) {

// 根据不同的 window feature 给 DecorView 设置不同的布局

int layoutResource;

int features = getLocalFeatures();

if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {

layoutResource = R.layout.screen_swipe_dismiss;

setCloseOnSwipeEnabled(true);

} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {

if (mIsFloating) {

TypedValue res = new TypedValue();

getContext().getTheme().resolveAttribute(

R.attr.dialogTitleIconsDecorLayout, res, true);

layoutResource = res.resourceId;

} else {

layoutResource = R.layout.screen_title_icons;

}

removeFeature(FEATURE_ACTION_BAR);

} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0

&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {

layoutResource = R.layout.screen_progress;

} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {

if (mIsFloating) {

TypedValue res = new TypedValue();

getContext().getTheme().resolveAttribute(

R.attr.dialogCustomTitleDecorLayout, res, true);

layoutResource = res.resourceId;

} else {

layoutResource = R.layout.screen_custom_title;

}

removeFeature(FEATURE_ACTION_BAR);

} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {

if (mIsFloating) {

TypedValue res = new TypedValue();

getContext().getTheme().resolveAttribute(

R.attr.dialogTitleDecorLayout, res, true);

layoutResource = res.resourceId;

} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {

layoutResource = a.getResourceId(

R.styleable.Window_windowActionBarFullscreenDecorLayout,

R.layout.screen_action_bar);

} else {

layoutResource = R.layout.screen_title;

}

} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {

layoutResource = R.layout.screen_simple_overlay_action_mode;

} else {

// 默认布局

layoutResource = R.layout.screen_simple;

}

mDecor.startChanging();

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

}

// DecorView

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

// 根据 上一步选择的 layout 生成 View

final View root = inflater.inflate(layoutResource, null);

if (mDecorCaptionView != null) {

if (mDecorCaptionView.getParent() == null) {

addView(mDecorCaptionView,

new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

mDecorCaptionView.addView(root,

new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));

} else {

// 添加到 DecorView 里

addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

mContentRoot = (ViewGroup) root;

initializeElevation();

}

5. DecorView 的创建时机?

上面说 DecorView 布局的时候,其实我们也看到了,在 PhoneWindow.installDecor -> generateDecor 其实就是创建 DecorView。

那 installDecor 是什么时候调用的呢?

调用链是 Activity.setContentView -> PhoneWindow.setContentView -> installDecor

说到这里那就继续会想到,Activity.setContentView 的流程是什么呢?

6. setContentView 的流程

setContentView 流程比较简单,会调用 PhoneWindow.setContentView。

其中做的事情是两个:

  1. 创建 DecorView

  2. 根据 layoutResId 创建 View 并添加到 DecorView 中

@Override

public void setContentView(int layoutResID) {

if (mContentParent == null) {

// 创建 DecorView

installDecor();

} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

mContentParent.removeAllViews();

}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,

getContext());

transitionTo(newScene);

} else {

// 根据 layoutResId 创建 ContentView

mLayoutInflater.inflate(layoutResID, mContentParent);

}

mContentParent.requestApplyInsets();

final Callback cb = getCallback();

if (cb != null && !isDestroyed()) {

cb.onContentChanged();

}

mContentParentExplicitlySet = true;

}

7. LayoutInflate 的流程

既然上一步用到了 LayoutInflate.inflate,那使用 LayoutInflate.inflate 加载一个布局的流程是什么样的呢?

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {

final Resources res = getContext().getResources();

// 通过 resourceId 获取 xml 布局内容

final XmlResourceParser parser = res.getLayout(resource);

try {

return inflate(parser, root, attachToRoot);

} finally {

parser.close();

}

}

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {

synchronized (mConstructorArgs) {

// …

View result = root;

try {

// Look for the root node.

int type;

// 找到 xml start 或者 xml end

while ((type = parser.next()) != XmlPullParser.START_TAG &&

type != XmlPullParser.END_DOCUMENT) {

// Empty

}

if (type != XmlPullParser.START_TAG) {

throw new InflateException(parser.getPositionDescription()

  • “: No start tag found!”);

}

final String name = parser.getName();

// 处理 merge 标签

if (TAG_MERGE.equals(name)) {

if (root == null || !attachToRoot) {

throw new InflateException(" can be used only with a valid "

  • “ViewGroup root and attachToRoot=true”);

}

// merge 标签传入的 parent 是 rootView

rInflate(parser, root, inflaterContext, attrs, false);

} else {

// 通过 tag 创建 View

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {

// 使用 rootView 默认的 LayoutParams

params = root.generateLayoutParams(attrs);

if (!attachToRoot) {

temp.setLayoutParams(params);

}

}

// 创建子 View

rInflateChildren(parser, temp, attrs, true);

if (root != null && attachToRoot) {

// 添加到 rootView

root.addView(temp, params);

}

// Decide whether to return the root that was passed in or the

// top view found in xml.

if (root == null || !attachToRoot) {

result = temp;

}

}

} catch (XmlPullParserException e) {

} finally {

}

return result;

}

}

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,

boolean finishInflate) throws XmlPullParserException, IOException {

rInflate(parser, parent, parent.getContext(), attrs, finishInflate);

}

void rInflate(XmlPullParser parser, View parent, Context context,

AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

while (((type = parser.next()) != XmlPullParser.END_TAG ||

parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

if (type != XmlPullParser.START_TAG) {

continue;

}

final String name = parser.getName();

if (TAG_REQUEST_FOCUS.equals(name)) {

pendingRequestFocus = true;

consumeChildElements(parser);

} else if (TAG_TAG.equals(name)) {

parseViewTag(parser, parent, attrs);

} else if (TAG_INCLUDE.equals(name)) {

// 处理 include 标签

if (parser.getDepth() == 0) {

throw new InflateException(“ cannot be the root element”);

}

parseInclude(parser, context, parent, attrs);

} else if (TAG_MERGE.equals(name)) {

throw new InflateException(“ must be the root element”);

} else {

// 通过 xml 标签生成 View

final View view = createViewFromTag(parent, name, context, attrs);

final ViewGroup viewGroup = (ViewGroup) parent;

final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);

rInflateChildren(parser, view, attrs, true);

viewGroup.addView(view, params);

}

}

}

上面的流程可以看到,LayoutInflate.inflate 最终是调用 createViewFromTag 从 xml 生成 View 的,其实这里才是关键。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,

boolean ignoreThemeAttr) {

/** 如果是 view 标签的话,就取其 class 属性作为 name

  • 比如

  • 最终生成的会是一个 LinearLayout

  • 是不是又学会了一种 view 的写法 _

*/

if (name.equals(“view”)) {

name = attrs.getAttributeValue(null, “class”);

}

// 处理 blink 标签

if (name.equals(TAG_1995)) {

return new BlinkLayout(context, attrs);

}

try {

// 通过 mFactory2、mFactory、mPrivateFactory 创建 View

View view;

if (mFactory2 != null) {

view = mFactory2.onCreateView(parent, name, context, attrs);

} else if (mFactory != null) {

view = mFactory.onCreateView(name, context, attrs);

} else {

view = null;

}

if (view == null && mPrivateFactory != null) {

view = mPrivateFactory.onCreateView(parent, name, context, attrs);

}

// 没有设置 Factory,走默认的创建 View 的流程

if (view == null) {

final Object lastContext = mConstructorArgs[0];

mConstructorArgs[0] = context;

try {

if (-1 == name.indexOf(‘.’)) {

view = onCreateView(parent, name, attrs);

} else {

view = createView(name, null, attrs);

}

} finally {

mConstructorArgs[0] = lastContext;

}

}

return view;

} catch (InflateException e) {

}

}

这里我们需要了解一下,mFactory、mFactory2、mPrivateFactory 都是什么?

private Factory mFactory;

private Factory2 mFactory2;

private Factory2 mPrivateFactory;

public interface Factory {

public View onCreateView(String name, Context context, AttributeSet attrs);

}

public interface Factory2 extends Factory {

public View onCreateView(View parent, String name, Context context, AttributeSet attrs);

}

mFactory、mFactory2、mPrivateFactory 分别对应 Factory 和 Factory2 方法,对应的是两个 onCreateView 方法,Factory.onCreateView 没有传入 parent 参数,Factory2.onCreateView 传入了 parent 参数。

而 mFactory 和 mFactory2 我们是可以设置的,当然不能重复设置,重复设置会抛出异常。

如果已经有 mFactory 的值,则生成一个 FactoryMerger,这个也是继承了 Factory2,用来控制一下调用顺序。

具体代码如下

public void setFactory(Factory factory) {

if (mFactorySet) {

throw new IllegalStateException(“A factory has already been set on this LayoutInflater”);

}

if (factory == null) {

throw new NullPointerException(“Given factory can not be null”);

}

mFactorySet = true;

if (mFactory == null) {

mFactory = factory;

} else {

mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);

}

}

public void setFactory2(Factory2 factory) {

if (mFactorySet) {

throw new IllegalStateException(“A factory has already been set on this LayoutInflater”);

}

if (factory == null) {

throw new NullPointerException(“Given factory can not be null”);

}

mFactorySet = true;

if (mFactory == null) {

mFactory = mFactory2 = factory;

} else {

mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);

}

}

private static class FactoryMerger implements Factory2 {

private final Factory mF1, mF2;

private final Factory2 mF12, mF22;

FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {

mF1 = f1;

mF2 = f2;

mF12 = f12;

mF22 = f22;

}

public View onCreateView(String name, Context context, AttributeSet attrs) {

View v = mF1.onCreateView(name, context, attrs);

if (v != null) return v;

return mF2.onCreateView(name, context, attrs);

}

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
mF1.onCreateView(name, context, attrs);

if (v != null) return v;

return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
mF2.onCreateView(name, context, attrs);

}

}

然后我们再看 mPrivateFactory,看名称就知道是系统的隐藏方法。

调用时机是在 Activity.attach 中,Activity 其实是实现了 Factory2 的 onCreateView 方法,其中对 fragment 做了处理,如果是 fragment 标签,就调用 fragment 的 onCreateView,这里就不详细往下面看了,如果是非 fragment 的标签,就返回 null,走默认的创建 View 的方法。

/**

  • @hide for use by framework

*/

public void setPrivateFactory(Factory2 factory) {

if (mPrivateFactory == null) {

mPrivateFactory = factory;

} else {

mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);

}

}

// Activity

final void attach(…)

mWindow.getLayoutInflater().setPrivateFactory(this);

}

public View onCreateView(String name, Context context, AttributeSet attrs) {

return null;

}

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

if (!“fragment”.equals(name)) {

return onCreateView(name, context, attrs);

}

return mFragments.onCreateView(parent, name, context, attrs);

}

所以上面的 Factory 和 Factory2,是系统留给我们的 hook View 创建流程的接口。

如果都没有设置,那就走到默认的创建 View 的方法。

默认创建 View 的方法比较简单,就是反射调用 View 的构造函数,然后做一个缓存,然后创建 View。

具体代码如下

// LayoutInflate

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,

boolean ignoreThemeAttr) {

// 前面的 mFactory、mFactory2、mPrivateFactory 都没有去创建 View

if (view == null) {

final Object lastContext = mConstructorArgs[0];

mConstructorArgs[0] = context;

try {

if (-1 == name.indexOf(‘.’)) {

// 如果名称里没有 “.”,也就是系统的 View,需要添加 android.view. 前缀,比如 ,最终去创建的名称是 android.view.LinearLayout

view = onCreateView(parent, name, attrs);

} else {

// 如果是自定义 View,则直接去创建

view = createView(name, null, attrs);

}

} finally {

mConstructorArgs[0] = lastContext;

}

}

// …

}

protected View onCreateView(String name, AttributeSet attrs)

throws ClassNotFoundException {

return createView(name, “android.view.”, attrs);

}

public final View createView(String name, String prefix, AttributeSet attrs)

throws ClassNotFoundException, InflateException {

Constructor<? extends View> constructor = sConstructorMap.get(name);

if (constructor != null && !verifyClassLoader(constructor)) {

constructor = null;

sConstructorMap.remove(name);

}

Class<? extends View> clazz = null;

try {

if (constructor == null) {

// 加载对应的类

clazz = mContext.getClassLoader().loadClass(

prefix != null ? (prefix + name) : name).asSubclass(View.class);

// 反射获取构造函数

constructor = clazz.getConstructor(mConstructorSignature);

constructor.setAccessible(true);

// 做个缓存,下次直接使用,提高效率

sConstructorMap.put(name, constructor);

} else {

}

Object lastContext = mConstructorArgs[0];

if (mConstructorArgs[0] == null) {

// Fill in the context if not already within inflation.

mConstructorArgs[0] = mContext;

}

Object[] args = mConstructorArgs;

args[1] = attrs;

// 调用构造函数创建 View

final View view = constructor.newInstance(args);

if (view instanceof ViewStub) {

// 处理 ViewStub

final ViewStub viewStub = (ViewStub) view;

viewStub.setLayoutInflater(cloneInContext((Context) args[0]));

}

mConstructorArgs[0] = lastContext;

return view;

} catch (NoSuchMethodException e) {

}

}

所以上面就是 LayoutInflate.inflate 的整个流程。

8. Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?

其实上面的问题中,我们经常会说到 PhoneWindow 这个角色,PhoneWindow 其实是 Window 的唯一子类,是 Activity 和 View 交互系统的中间层,而 DecorView 是整个 View 层级的最顶层,ViewRootImpl 是 DecorView 的 parent,但是他并不是一个真正的 View,只是继承了 ViewParent 接口,用来掌管 View 的各种事件,包括 requestLayout、invalidate、dispatchInputEvent 等等。

9. PhoneWindow 的创建时机?

既然上面又提到了 PhoneWindow,那么 PhoneWindow 是什么时候创建的呢?是在 Activity.attach 里创建的,而 Activity.attach 又是在 ActivityThread.performLaunchActivity 里创建的。

这里就又能引申出 Activity 的启动流程,这里就先不讲了。

10. 如何触发重新绘制?

既然上面说到 View 的绘制流程,那我们怎么触发 View 的重新绘制呢?

就是调用 requestLayout 和 invalidate。

11. requestLayout 和 invalidate 的流程

requestLayout 流程

// View

public void requestLayout() {

if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {

ViewRootImpl viewRoot = getViewRootImpl();

if (viewRoot != null && viewRoot.isInLayout()) {

if (!viewRoot.requestLayoutDuringLayout(this)) {

// 如果当前在 layout 流程中,并且是在处理 requestLayout,那么就直接返回,这个时候需要注意,mPrivateFlags 并没有设置 FORCE_LAYOUT

// 这个时候 reqeustLayout 会在下一个 frame 里执行

return;

}

}

mAttachInfo.mViewRequestingLayout = this;

}

// 如果当前在 layout 流程中,但是没有处理 requestLayout,那么就继续后面的流程,这个时候 mPrivateFlags 是设置为 FORCE_LAYOUT

// 这个时候 requestLayout 会在下一次 layout 过程中进行执行

// 设置 FORCE_LAYOUT 和 INVALIDETED flag

mPrivateFlags |= PFLAG_FORCE_LAYOUT;

mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {

// 层层调用 parent 的 requestLayout

mParent.requestLayout();

}

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {

mAttachInfo.mViewRequestingLayout = null;

}

}

从上面代码可以看到,会一层层调用 parent 的 requestLayout,而上面的问题中我们也分析到了,DecorView 是整个 View 层级的最顶层,ViewRootImpl 又是 DecorView 的 parent,所以最终调用到 ViewRootImpl 的 requestLayout。

// ViewRootImpl

public void requestLayout() {

最后

题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

Android开发8年,阿里、百度一面惨被吊打!我是否应该转行了?

【Android进阶学习视频】、【全套Android面试秘籍】

希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展

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

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

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

ORCE_LAYOUT 和 INVALIDETED flag

mPrivateFlags |= PFLAG_FORCE_LAYOUT;

mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {

// 层层调用 parent 的 requestLayout

mParent.requestLayout();

}

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {

mAttachInfo.mViewRequestingLayout = null;

}

}

从上面代码可以看到,会一层层调用 parent 的 requestLayout,而上面的问题中我们也分析到了,DecorView 是整个 View 层级的最顶层,ViewRootImpl 又是 DecorView 的 parent,所以最终调用到 ViewRootImpl 的 requestLayout。

// ViewRootImpl

public void requestLayout() {

最后

题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-g9jwH1KE-1713227431015)]

【Android进阶学习视频】、【全套Android面试秘籍】

希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值