Android应用setContentView与LayoutInflater加载解析机制源码分析,Android基础入门教程

}

final Callback cb = getCallback();

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

cb.onContentChanged();

}

}

看见没有,我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。

所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。

所以说在我们的应用程序里可以多次调用setContentView()来显示界面,因为会removeAllViews。

2-3 窗口PhoneWindow类的installDecor方法

回过头,我们继续看上面PhoneWindow类setContentView方法的第6行installDecor();代码,在PhoneWindow中查看installDecor源码如下:

private void installDecor() {

if (mDecor == null) {

mDecor = generateDecor();

mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

mDecor.setIsRootNamespace(true);

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

mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);

}

}

if (mContentParent == null) {

//根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent

mContentParent = generateLayout(mDecor);

//…

//初始化一堆属性值

}

}

我勒个去!又是一个死长的方法,抓重点分析吧。第2到9行可以看出,首先判断mDecor对象是否为空,如果为空则调用generateDecor()创建一个DecorView(该类是

FrameLayout子类,即一个ViewGroup视图),然后设置一些属性,我们看下PhoneWindow的generateDecor方法,如下:

protected DecorView generateDecor() {

return new DecorView(getContext(), -1);

}

可以看见generateDecor方法仅仅是new一个DecorView的实例。

回到installDecor方法继续往下看,第10行开始到方法结束都需要一个if (mContentParent == null)判断为真才会执行,当mContentParent对象不为空则调用generateLayout()方法去创建mContentParent对象。所以我们看下generateLayout方法源码,如下:

protected ViewGroup generateLayout(DecorView decor) {

// Apply data from current theme.

TypedArray a = getWindowStyle();

//…

//依据主题style设置一堆值进行设置

// Inflate the window decor.

int layoutResource;

int features = getLocalFeatures();

//…

//根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值

//把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值

View in = mLayoutInflater.inflate(layoutResource, null);

decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

mContentRoot = (ViewGroup) in;

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

if (contentParent == null) {

throw new RuntimeException(“Window couldn’t find content container view”);

}

//…

//继续一堆属性设置,完事返回contentParent

return contentParent;

}

可以看见上面方法主要作用就是根据窗口的风格修饰类型为该窗口选择不同的窗口根布局文件。mDecor做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。

在这里顺带提一下:还记得我们平时写应用Activity时设置的theme或者feature吗(全屏啥的,NoTitle等)?我们一般是不是通过XML的android:theme属性或者java的requestFeature()方法来设置的呢?譬如:

通过java文件设置:

requestWindowFeature(Window.FEATURE_NO_TITLE);

通过xml文件设置:

android:theme=“@android:style/Theme.NoTitleBar”

对的,其实我们平时requestWindowFeature()设置的值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。

所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。

我们继续关注一下generateLayout方法的layoutResource变量赋值情况。因为它最终通过View in = mLayoutInflater.inflate(layoutResource, null);decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));将in添加到PhoneWindow的mDecor对象。为例验证这一段代码分析我们用一个实例来进行说明,如下是一个简单的App主要代码:

AndroidManifest.xml文件

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android=“http://schemas.android.com/apk/res/android”

package=“com.yanbober.myapplication” >

<application

//看重点,我们将主题设置为NoTitleBar

android:theme=“@android:style/Theme.Black.NoTitleBar” >

主界面布局文件:

<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:layout_width=“match_parent”

android:layout_height=“match_parent”>

<TextView android:text=“@string/hello_world”

android:layout_width=“wrap_content”

android:layout_height=“wrap_content” />

APP运行界面:

这里写图片描述

看见没有,上面我们将主题设置为NoTitleBar,所以在generateLayout方法中的layoutResource变量值为R.layout.screen_simple,所以我们看下系统这个screen_simple.xml布局文件,如下:

<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:fitsSystemWindows=“true”

android:orientation=“vertical”>

<ViewStub android:id=“@+id/action_mode_bar_stub”

android:inflatedId=“@+id/action_mode_bar”

android:layout=“@layout/action_mode_bar”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:theme=“?attr/actionBarTheme” />

<FrameLayout

android:id=“@android:id/content”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:foregroundInsidePadding=“false”

android:foregroundGravity=“fill_horizontal|top”

android:foreground=“?android:attr/windowContentOverlay” />

布局中,一般会包含ActionBar,Title,和一个id为content的FrameLayout,这个布局是NoTitle的。

再来看下上面这个App的hierarchyviewer图谱,如下:

这里写图片描述

看见了吧,通过这个App的hierarchyviewer和系统screen_simple.xml文件比较就验证了上面我们分析的结论,不再做过多解释。

然后回过头可以看见上面PhoneWindow类的setContentView方法最后通过调运mLayoutInflater.inflate(layoutResID, mContentParent);或者mContentParent.addView(view, params);语句将我们的xml或者java View插入到了mContentParent(id为content的FrameLayout对象)ViewGroup中。最后setContentView还会调用一个Callback接口的成员函数onContentChanged来通知对应的Activity组件视图内容发生了变化。

2-4 Window类内部接口Callback的onContentChanged方法

上面刚刚说了PhoneWindow类的setContentView方法中最后调运了onContentChanged方法。我们这里看下setContentView这段代码,如下:

public void setContentView(int layoutResID) {

final Callback cb = getCallback();

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

cb.onContentChanged();

}

}

看着没有,首先通过getCallback获取对象cb(回调接口),PhoneWindow没有重写Window的这个方法,所以到抽象类Window中可以看到:

/**

  • Return the current Callback interface for this window.

*/

public final Callback getCallback() {

return mCallback;

}

这个mCallback在哪赋值的呢,继续看Window类发现有一个方法,如下:

public void setCallback(Callback callback) {

mCallback = callback;

}

Window中的mCallback是通过这个方法赋值的,那就回想一下,Window又是Activity的组合成员,那就是Activity一定调运这个方法了,回到Activity发现在Activity的attach方法中进行了设置,如下:

final void attach(Context context, ActivityThread aThread,

mWindow.setCallback(this);

}

也就是说Activity类实现了Window的Callback接口。那就是看下Activity实现的onContentChanged方法。如下:

public void onContentChanged() {

}

咦?onContentChanged是个空方法。那就说明当Activity的布局改动时,即setContentView()或者addContentView()方法执行完毕时就会调用该方法。

所以当我们写App时,Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。

2-5 setContentView源码分析总结

可以看出来setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,上面的过程可以重点概括为:

  1. 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。

  2. 依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。

  3. 将Activity的布局文件添加至id为content的FrameLayout内。

至此整个setContentView的主要流程就分析完毕。你可能这时会疑惑,这么设置完一堆View关系后系统是怎么知道该显示了呢?下面我们就初探一下关于Activity的setContentView在onCreate中如何显示的(声明一下,这里有些会暂时直接给出结论,该系列文章后面会详细分析的)。

2-6 setContentView完以后Activity显示界面初探

这一小部分已经不属于sentContentView的分析范畴了,只是简单说明setContentView之后怎么被显示出来的(注意:Activity调运setContentView方法自身不会显示布局的)。

记得前面有一篇文章《Android异步消息处理机制详解及源码分析》的3-1-2小节说过,一个Activity的开始实际是ActivityThread的main方法(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。

那在这一篇我们再直接说一个知识点(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。

当启动Activity调运完ActivityThread的main方法之后,接着调用ActivityThread类performLaunchActivity来创建要启动的Activity组件,在创建Activity组件的过程中,还会为该Activity组件创建窗口对象和视图对象;接着Activity组件创建完成之后,通过调用ActivityThread类的handleResumeActivity将它激活。

所以我们先看下handleResumeActivity方法一个重点,如下:

final void handleResumeActivity(IBinder token,

boolean clearHide, boolean isForward, boolean reallyResume) {

// If we are getting ready to gc after going to the background, well

// we are back active so skip it.

// TODO Push resumeArgs into the activity for consideration

ActivityClientRecord r = performResumeActivity(token, clearHide);

if (r != null) {

// If the window hasn’t yet been added to the window manager,

// and this guy didn’t finish itself or start another activity,

// then go ahead and add the window.

// If the window has already been added, but during resume

// we started another activity, then don’t yet make the

// window visible.

// The window is now visible if it has been added, we are not

// simply finishing, and we are not starting another activity.

if (!r.activity.mFinished && willBeVisible

&& r.activity.mDecor != null && !r.hideForNow) {

if (r.activity.mVisibleFromClient) {

r.activity.makeVisible();

}

}

} else {

// If an exception was thrown when trying to resume, then

// just end this activity.

}

}

看见r.activity.makeVisible();语句没?调用Activity的makeVisible方法显示我们上面通过setContentView创建的mDecor视图族。所以我们看下Activity的makeVisible方法,如下:

void makeVisible() {

if (!mWindowAdded) {

ViewManager wm = getWindowManager();

wm.addView(mDecor, getWindow().getAttributes());

mWindowAdded = true;

}

mDecor.setVisibility(View.VISIBLE);

}

看见没有,通过DecorView(FrameLayout,也即View)的setVisibility方法将View设置为VISIBLE,至此显示出来。

到此setContentView的完整流程分析完毕。

【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】

3 Android5.1.1(API 22)看看LayoutInflater机制原理


上面在分析setContentView过程中可以看见,在PhoneWindow的setContentView中调运了mLayoutInflater.inflate(layoutResID, mContentParent);,在PhoneWindow的generateLayout中调运了View in = mLayoutInflater.inflate(layoutResource, null);,当时我们没有详细分析,只是告诉通过xml得到View对象。现在我们就来分析分析这一问题。

3-1 通过实例引出问题

在开始之前我们先来做一个测试,我们平时最常见的就是ListView的Adapter中使用LayoutInflater加载xml的item布局文件,所以咱们就以ListView为例,如下:

省略掉Activity代码等,首先给出Activity的布局文件,如下:

<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:orientation=“vertical”>

<ListView

android:id=“@+id/listview”

android:dividerHeight=“5dp”

android:layout_width=“match_parent”

android:layout_height=“match_parent”>

给出两种不同的ListView的item布局文件。

textview_layout.xml文件:

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android=“http://schemas.android.com/apk/res/android”

android:layout_width=“100dp”

android:layout_height=“40dp”

android:text=“Text Test”

android:background=“#ffa0a00c”/>

textview_layout_parent.xml文件:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

android:layout_height=“wrap_content”

android:layout_width=“wrap_content”

xmlns:android=“http://schemas.android.com/apk/res/android”>

<TextView xmlns:android=“http://schemas.android.com/apk/res/android”

android:layout_width=“100dp”

android:layout_height=“40dp”

android:text=“Text Test”

android:background=“#ffa0a00c”/>

ListView的自定义Adapter文件:

public class InflateAdapter extends BaseAdapter {

private LayoutInflater mInflater = null;

public InflateAdapter(Context context) {

mInflater = LayoutInflater.from(context);

}

@Override

public int getCount() {

return 8;

}

@Override

public Object getItem(int position) {

return null;

}

@Override

public long getItemId(int position) {

return 0;

}

@Override

public View getView(int position, View convertView, ViewGroup parent) {

//说明:这里是测试inflate方法参数代码,不再考虑性能优化等TAG处理

return getXmlToView(convertView, position, parent);

}

private View getXmlToView(View convertView, int position, ViewGroup parent) {

View[] viewList = {

mInflater.inflate(R.layout.textview_layout, null),

// mInflater.inflate(R.layout.textview_layout, parent),

mInflater.inflate(R.layout.textview_layout, parent, false),

// mInflater.inflate(R.layout.textview_layout, parent, true),

mInflater.inflate(R.layout.textview_layout, null, true),

mInflater.inflate(R.layout.textview_layout, null, false),

mInflater.inflate(R.layout.textview_layout_parent, null),

// mInflater.inflate(R.layout.textview_layout_parent, parent),

mInflater.inflate(R.layout.textview_layout_parent, parent, false),

// mInflater.inflate(R.layout.textview_layout_parent, parent, true),

mInflater.inflate(R.layout.textview_layout_parent, null, true),

mInflater.inflate(R.layout.textview_layout_parent, null, false),

};

convertView = viewList[position];

return convertView;

}

}

当前代码运行结果:

这里写图片描述

PS:当打开上面viewList数组中任意一行注释都会抛出异常(java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView)。

你指定有些蒙圈了,而且比较郁闷,同时想弄明白inflate的这些参数都是啥意思。运行结果为何有这么大差异呢?

那我告诉你,你现在先别多想,记住这回事,咱们先看源码,下面会告诉你为啥。

3-2 从LayoutInflater源码实例化说起

我们先看一下源码中LayoutInflater实例化获取的方法:

public static LayoutInflater from(Context context) {

LayoutInflater LayoutInflater =

(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

if (LayoutInflater == null) {

throw new AssertionError(“LayoutInflater not found.”);

}

return LayoutInflater;

}

看见没有?是否很熟悉?我们平时写应用获取LayoutInflater实例时不也就两种写法吗,如下:

LayoutInflater lif = LayoutInflater.from(Context context);

LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

可以看见from方法仅仅是对getSystemService的一个安全封装而已。

3-3 LayoutInflater源码的View inflate(…)方法族剖析

得到LayoutInflater对象之后我们就是传递xml然后解析得到View,如下方法:

public View inflate(int resource, ViewGroup root) {

return inflate(resource, root, root != null);

}

继续看inflate(int resource, ViewGroup root, boolean attachToRoot)方法,如下:

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

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

if (DEBUG) {

Log.d(TAG, “INFLATING from resource: “” + res.getResourceName(resource) + “” (”

  • Integer.toHexString(resource) + “)”);

}

final XmlResourceParser parser = res.getLayout(resource);

try {

return inflate(parser, root, attachToRoot);

} finally {

parser.close();

}

}

这个方法的第8行获取到XmlResourceParser接口的实例(Android默认实现类为Pull解析XmlPullParser)。接着看第10行inflate(parser, root, attachToRoot);,你会发现无论哪个inflate重载方法最后都调运了inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)方法,如下:

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

synchronized (mConstructorArgs) {

Trace.traceBegin(Trace.TRACE_TAG_VIEW, “inflate”);

final AttributeSet attrs = Xml.asAttributeSet(parser);

Context lastContext = (Context)mConstructorArgs[0];

mConstructorArgs[0] = mContext;

//定义返回值,初始化为传入的形参root

View result = root;

try {

// Look for the root node.

int type;

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

type != XmlPullParser.END_DOCUMENT) {

// Empty

}

//如果一开始就是END_DOCUMENT,那说明xml文件有问题

if (type != XmlPullParser.START_TAG) {

throw new InflateException(parser.getPositionDescription()

  • “: No start tag found!”);

}

//有了上面判断说明这里type一定是START_TAG,也就是xml文件里的root node

final String name = parser.getName();

if (DEBUG) {

System.out.println(“**************************”);

System.out.println("Creating root view: "

  • name);

System.out.println(“**************************”);

}

if (TAG_MERGE.equals(name)) {

//处理merge tag的情况(merge,你懂的,APP的xml性能优化)

//root必须非空且attachToRoot为true,否则抛异常结束(APP使用merge时要注意的地方,

//因为merge的xml并不代表某个具体的view,只是将它包起来的其他xml的内容加到某个上层

//ViewGroup中。)

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

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

  • “ViewGroup root and attachToRoot=true”);

}

//递归inflate方法调运

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

} else {

// Temp is the root view that was found in the xml

//xml文件中的root view,根据tag节点创建view对象

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

ViewGroup.LayoutParams params = null;

if (root != null) {

if (DEBUG) {

System.out.println("Creating params from root: " +

root);

}

// Create layout params that match root, if supplied

//根据root生成合适的LayoutParams实例

params = root.generateLayoutParams(attrs);

if (!attachToRoot) {

// Set the layout params for temp if we are not

// attaching. (If we are, we use addView, below)

//如果attachToRoot=false就调用view的setLayoutParams方法

temp.setLayoutParams(params);

}

}

if (DEBUG) {

System.out.println(“-----> start inflating children”);

}

// Inflate all children under temp

//递归inflate剩下的children

rInflate(parser, temp, attrs, true, true);

if (DEBUG) {

System.out.println(“-----> done inflating children”);

}

// We are supposed to attach all the views we found (int temp)

// to root. Do that now.

if (root != null && attachToRoot) {

//root非空且attachToRoot=true则将xml文件的root view加到形参提供的root里

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) {

//返回xml里解析的root view

result = temp;

}

}

} catch (XmlPullParserException e) {

InflateException ex = new InflateException(e.getMessage());

ex.initCause(e);

throw ex;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

img

img

img

img

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?

Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

6)]

[外链图片转存中…(img-3cILWyw6-1712421448596)]

[外链图片转存中…(img-7dR1OD2v-1712421448596)]

[外链图片转存中…(img-tWdxOsHU-1712421448597)]

[外链图片转存中…(img-gunXYNKO-1712421448597)]

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?

Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

[外链图片转存中…(img-Ivlq3mSq-1712421448597)]

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 25
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值