- 绘制流程从何而起
- Activity 的界面结构在哪里开始形成
- 绘制流程如何运转起来
接下来我们就一个个目标来 conquer。
三、绘制流程从何而起
我们一说到绘制流程,就会想到或是听过onMeasure
、onLayout
、onDraw
这三个方法,但是有没想过为什么我们开启一个App或是点开一个Activity,就会触发这一系列流程呢?想知道绘制流程从何而起,我们就有必要先解释 App启动流程 和 Activity的启动流程。
我们都知道 ActivityThread 的 main
是一个App的入口。我们来到 main
方法看看他做了什么启动操作。
ActivityThread 的
main
方法是由 ZygoteInit 类中最终通过 RuntimeInit类的invokeStaticMain
方法进行反射调用。有兴趣的童鞋可以自行查阅下,限于篇幅,就不再展开分享。
// ActivityThread 类
public static void main(String[] args) {
// …省略不相关代码
// 准备主线程的 Looper
Looper.prepareMainLooper();
// 实例化 ActivityThread,用于管理应用程序进程中主线程的执行
ActivityThread thread = new ActivityThread();
// 进入 attach 方法
thread.attach(false);
// …省略不相关代码
// 开启 Looper
Looper.loop();
// …省略不相关代码
}
进入 main
方法,我们便看到很熟悉的 Handler机制。在安卓中都是以消息进行驱动,在这里也不例外,我们可以看到先进行 Looper 的准备,在最后开启 Looper 进行循环获取消息,用于处理传到主线程的消息。
这也是为什么我们在主线程不需要先进行 Looper 的准备和开启,emmm,有些扯远了。
回过头,可以看到夹杂在中间的 ActivityThread 类的实例化并且调用了 attach
方法。具体代码如下,我们接着往下走。
// ActivityThread 类
private void attach(boolean system) {
// …省略不相关代码
// system 此时为false,进入此分支
if (!system) {
// …省略不相关代码
// 获取系统的 AMS 服务的 Proxy,用于向 AMS 进程发送数据
final IActivityManager mgr = ActivityManager.getService();
try {
// 将我们的 mAppThread 传递给 AMS,AMS 便可控制我们 App 的 Activity
mgr.attachApplication(mAppThread);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
// …省略不相关代码
} else {
// …省略不相关代码
}
// …省略不相关代码
}
// ActivityManager 类
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
// ActivityManager 类
private static final Singleton IActivityManagerSingleton =
new Singleton() {
@Override
protected IActivityManager create() {
// 在这里获取 AMS 的binder
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
// 这里获取 AMS 的 proxy,可以进行发送数据
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
我们进入attach
方法,方法内主要是通过 ActivityManager 的 getService
方法获取到了 ActivityManagerService(也就是我们所说的AMS) 的 Proxy,达到与AMS 进行跨进程通信的目的。
文中所说的 Proxy 和 Stub,是以系统为我们自动生成AIDL时的类名进行类比使用,方便讲解。Proxy 代表着发送信息,Stub 代表着接收信息。
在 mgr.attachApplication(mAppThread);
代码中向 AMS 进程发送信息,携带了一个类型为 ApplicationThread 的 mAppThread
参数。这句代码的作用,其实就是把 我们应用的 “控制器” 上交给了 AMS,这样使得 AMS 能够来控制我们应用中的Activity的生命周期。为什么这么说呢?我们这就有必要来了解下 ApplicationThread 类的结构,其部分代码如下:
// ActivityThread$ApplicationThread 类
private class ApplicationThread extends IApplicationThread.Stub {
// 省略大量代码
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List pendingResults, List pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
// 会将 AMS 发来的信息封装在 ActivityClientRecord 中,然后发送给 Handler
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.ident = ident;
r.intent = intent;
r.referrer = referrer;
r.voiceInteractor = voiceInteractor;
r.activityInfo = info;
r.compatInfo = compatInfo;
r.state = state;
r.persistentState = persistentState;
r.pendingResults = pendingResults;
r.pendingIntents = pendingNewIntents;
r.startsNotResumed = notResumed;
r.isForward = isForward;
r.profilerInfo = profilerInfo;
r.overrideConfig = overrideConfig;
updatePendingConfiguration(curConfig);
sendMessage(H.LAUNCH_ACTIVITY, r);
}
// 省略大量代码
}
从 ApplicationThread 的方法名,我们会惊奇的发现大多方法名以 scheduleXxxYyyy
的形式命名,而且和我们熟悉的生命周期都挺接近。上面代码留下了我们需要的方法 scheduleLaunchActivity
,它们包含了我们 Activity 的 onCreate
、onStart
和 onResume
。
scheduleLaunchActivity
方法会对 AMS 发来的信息封装在 ActivityClientRecord 类中,最后通过 sendMessage(H.LAUNCH_ACTIVITY, r);
这行代码将信息以 H.LAUNCH_ACTIVITY
的信息标记发送至我们主线程中的 Handler。我们进入主线程的 Handler 实现类 H。具体代码如下:
// ActivityThread$H 类
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
// 省略大量代码
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “activityStart”);
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, “LAUNCH_ACTIVITY”);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
// 省略大量代码
}
}
// 省略大量代码
}
我们从上面的代码可以知道消息类型为 LAUNCH_ACTIVITY
,则会进入 handleLaunchActivity
方法,我们顺着往里走,来到下面这段代码
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;
if (r.profilerInfo != null) {
mProfiler.setProfiler(r.profilerInfo);
mProfiler.startProfiling();
}
// Make sure we are running with the most recent config.
handleConfigurationChanged(null, null);
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
// Initialize before creating the activity
WindowManagerGlobal.initialize();
// 获得一个Activity对象,会进行调用 Activity 的 onCreate 和 onStart 的生命周期
Activity a = performLaunchActivity(r, customIntent);
// Activity 不为空进入
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
reportSizeConfigurations®;
Bundle oldState = r.state;
// 该方法最终回调用到 Activity 的 onResume
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
if (!r.activity.mFinished && r.startsNotResumed) {
performPauseActivityIfNeeded(r, reason);
if (r.isPreHoneycomb()) {
r.state = oldState;
}
}
} else {
// If there was an error, for any reason, tell the activity manager to stop us.
try {
ActivityManager.getService()
.finishActivity(r.token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
我们先看这行代码 performLaunchActivity(r, customIntent);
最终会调用 onCreate
和 onStart
方法。眼见为实,耳听为虚,我们继续进入深入。来到下面这段代码
// ActivityThread 类
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// 省略不相关代码
// 创建 Activity 的 Context
ContextImpl appContext = createBaseContextForActivity®;
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
// ClassLoader 加载 Activity类,并创建 Activity
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
// 省略不相关代码
} catch (Exception e) {
// 省略不相关代码
}
try {
// 创建 Application
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
// 省略不相关代码
if (activity != null) {
// 省略不相关代码
// 调用了 Activity 的 attach
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
// 这个 intent 就是我们 getIntent 获取到的
if (customIntent != null) {
activity.mIntent = customIntent;
}
// 省略不相关代码
// 调用 Activity 的 onCreate
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
// 省略不相关代码
if (!r.activity.mFinished) {
// zincPower 调用 Activity 的 onStart
activity.performStart();
r.stopped = false;
}
if (!r.activity.mFinished) {
// zincPower 调用 Activity 的 onRestoreInstanceState 方法,数据恢复
if (r.isPersistable()) {
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
// 省略不相关代码
}
// 省略不相关代码
}
// 省略不相关代码
return activity;
}
// Instrumentation 类
public void callActivityOnCreate(Activity activity, Bundle icicle,
PersistableBundle persistentState) {
prePerformCreate(activity);
activity.performCreate(icicle, persistentState);
postPerformCreate(activity);
}
// Activity 类
final void performCreate(Bundle icicle) {
restoreHasCurrentPermissionRequest(icicle);
// 调用了 onCreate
onCreate(icicle);
mActivityTransitionState.readState(icicle);
performCreateCommon();
}
// Activity 类
final void performStart() {
// 省略不相关代码
// 进行调用 Activity 的 onStart
mInstrumentation.callActivityOnStart(this);
// 省略不相关代码
}
// Instrumentation 类
public void callActivityOnStart(Activity activity) {
// 调用了 Activity 的 onStart
activity.onStart();
}
进入 performLaunchActivity
方法后,我们会发现很多我们熟悉的东西,小盆友已经给关键点打上注释,因为不是文章的重点就不再细说,否则篇幅过长。
我们直接定位到 mInstrumentation.callActivityOnCreate
这行代码。进入该方法,方法内会调用 activity
的 performCreate
方法,而 performCreate
方法里会调用到我们经常重写的 Activity 生命周期的 onCreate
方法。😄至此,找到了 onCreate
的调用地方,这里需要立个 FLAG1,因为目标二需要的开启便是这里,我下一小节分享,勿急。
回过头来继续 performLaunchActivity
方法的执行,会调用到 activity
的 performStart
方法,而该方法又会调用到 mInstrumentation.callActivityOnStart
方法,最后在该方法内便调用了我们经常重写的 Activity 生命周期的 onStart
方法。😊至此,找到了 onStart
的调用地方。
找到了两个生命周期的调用地方,我们需要折回到 handleLaunchActivity
方法中,继续往下运行,便会来到 handleResumeActivity
方法,具体代码如下:
// ActivityThread 类
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
// 省略部分代码
r = performResumeActivity(token, clearHide, reason);
// 省略部分代码
if (r.window == null && !a.mFinished && willBeVisible) {
// 将 Activity 中的 Window 赋值给 ActivityClientRecord 的 Window
r.window = r.activity.getWindow();
// 获取 DecorView,这个 DecorView 在 Activity 的 setContentView 时就初始化了
View decor = r.window.getDecorView();
// 此时为不可见
decor.setVisibility(View.INVISIBLE);
// WindowManagerImpl 为 ViewManager 的实现类
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 往 WindowManager 添加 DecorView,并且带上 WindowManager.LayoutParams
// 这里面便触发真正的绘制流程
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
// 省略不相关代码
}
performResumeActivity
方法最终会调用到 Activity 的 onResume
方法,因为不是我们该小节的目标,就不深入了,童鞋们可以自行深入,代码也比较简单。至此我们就找齐了我们一直重写的三个 Acitivity 的生命周期函数 onCreate
、onStart
和 onResume
。按照这一套路,童鞋们可以看看 ApplicationThread 的其他方法,会发现 Activity 的生命周期均在其中可以找到影子,也就证实了我们最开始所说的 我们将应用 “遥控器” 交给了AMS。而值得一提的是,这一操作是处于一个跨进程的场景。
继续往下运行来到 wm.addView(decor, l);
这行代码,wm
的具体实现类为 WindowManagerImpl
,继续跟踪深入,来到下面这一连串的调用
// WindowManagerImpl 类
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// tag:进入这一行
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// WindowManagerGlobal 类
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// 省略不相关代码
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// 省略不相关代码
// 初始化 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
// 将 view 和 param 交于 root
// ViewRootImpl 开始绘制 view
// tag:进入这一行
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
// ViewRootImpl 类
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
// 省略不相关代码
// 进入绘制流程
// tag:进入这一行
requestLayout();
// 省略不相关代码
}
}
}
// ViewRootImpl 类
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// tag:进入这一行
scheduleTraversals();
}
}
// ViewRootImpl 类
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 提交给 编舞者,会在下一帧绘制时调用 mTraversalRunnable,运行其run
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
中间跳转的方法比较多,小盆友都打上了 // tag:进入这一行
注释,童鞋们可以自行跟踪,会发现最后会调用到编舞者,即 Choreographer 类的 postCallback方法。Choreographer 是一个会接收到垂直同步信号的类,所以当下一帧到达时,他会调用我们刚才提交的任务,即此处的 mTraversalRunnable
,并执行其 run
方法。
值得一提的是通过 Choreographer 的 postCallback 方法提交的任务并不是每一帧都会调用,而是只在下一帧到来时调用,调用完之后就会将该任务移除。简而言之,就是提交一次就会在下一帧调用一次。
我们继续来看 mTraversalRunnable
的具体内容,看看每一帧都做了写什么操作。
// ViewRootImpl 类
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
// ViewRootImpl 类
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
// ViewRootImpl 类
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing(“ViewAncestor”);
}
// 进入此处
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
// ViewRootImpl 类
private void performTraversals() {
// 省略不相关代码
if (!mStopped || mReportNextDraw) {
// 省略不相关代码
// FLAG2
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// 省略不相关代码
// 进行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 省略不相关代码
// 进行摆放
performLayout(lp, mWidth, mHeight);
// 省略不相关代码
// 布局完回调
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
// 省略不相关代码
// 进行绘制
performDraw();
}
调用了 mTraversalRunnable
的 run
方法之后,会发现也是一连串的方法调用,后来到 performTraversals
,这里面就有我们一直提到三个绘制流程方法的起源地。这三个起源地就是我们在上面看到的三个方法 performMeasure
、performLayout
、performDraw
。
而这三个方法会进行如下图的一个调用链(😄还是手绘,勿喷),从代码我们也知道,会按照 performMeasure
、performLayout
、performDraw
的顺序依次调用。
performMeasure
会触发我们的测量流程,如图中所示,进入第一层的 ViewGroup,会调用 measure
和 onMeasure
,在 onMeasure
中调用下一层级,然后下一层级的 View或ViewGroup 会重复这样的动作,进行所有 View 的测量。(这一过程可以理解为书的深度遍历)
performLayout
和 performMeasure
的流程大同小异,只是方法名不同,就不再赘述。
performDraw
稍微些许不同,当前控件为ViewGroup时,只有需要绘制背景或是我们通过 setWillNotDraw(false)
设置我们的ViewGroup需要进行绘制时,会进入 onDraw
方法,然后通过 dispatchDraw
进行绘制子View,如此循环。而如果为View,自然也就不需要绘制子View,只需绘制自身的内容即可。 至此,绘制流程的源头我们便了解清楚了, onMeasure
、 onLayout
、onDraw
三个方法我们会在后面进行详述并融入在实战中。
四、Activity 的界面结构在哪里开始形成
上图是 Activity 的结构。我们先进行大致的描述,然后在进入源码体会这一过程。
我们可以清晰的知道一个 Activity 会对应着有一个 Window,而 Window 的唯一实现类为 PhoneWindow,PhoneWindow 的初始化是在 Activity 的 attach
方法中,我们前面也有提到 attach
方法,感兴趣的童鞋可以自行深入。
在往下一层是一个 DecorView,被 PhoneWindow 持有着,DecorView 的初始化在 setContentView 中,这个我们待会会进行详细分析。DecorView 是我们的顶级View,我们设置的布局只是其子View。
DecorView 是一个 FrameLayout。但在 setContentView
中,会给他加入一个线性的布局(LinearLayout)。该线性布局的子View 则一般由 TitleBar 和 ContentView 进行组成。TitleBar 我们可以通过 requestWindowFeature(Window.FEATURE_NO_TITLE);
进行去除,而 ContentView 则是来装载我们设置的布局文件的 ViewGroup 了。
现在我们已经有一个大概的印象,接下来进行详细分析。在上一节中(FLAG1处),我们最先会进入的生命周期为onCreate
,在该方法中我们都会写上这样一句代码setContentView(R.layout.xxxx)
进行设置布局。经过上一节我们也知道,真正的绘制流程是在 onResume
之后(忘记的童鞋请倒回去看一下),那么 setContentView
起到一个什么作用呢?我进入源码一探究竟吧。
进入 Activity 的 setContentView
方法,可以看到下面这段代码。getWindow 返回的是一个 Window 类型的对象,而通过Window的官方注释可以知道其唯一的实现类为PhoneWindow, 所以我们进入 PhoneWindow 类查看其 setContentView
方法,这里值得我们注意有两行代码。我们一一进入,我们先进入 installDecor
方法。
// Activity 类
public void setContentView(@LayoutRes int layoutResID) {
// getWindow 返回的是 PhoneWindow
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
// Activity 类
public Window getWindow() {
return mWindow;
}
// PhoneWindow 类
@Override
public void setContentView(int layoutResID) {
// 此时 mContentParent 为空,mContentParent 是装载我们布局的容器
if (mContentParent == null) {
// 进行初始化 顶级View——DecorView 和 我们设置的布局的装载容器——ViewGroup(mContentParent)
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 {
// 加载我们设置的布局文件 到 mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
installDecor
方法的作用为初始化了我们的顶级View(即DecorView)和初始化装载我们布局的容器(即 mContentParent 属性)。具体代码如下
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 会进行实例化 一个mDecor
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
文末
我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。
以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持)
部分资料一览:
- 330页PDF Android学习核心笔记(内含8大板块)
-
Android学习的系统对应视频
-
Android进阶的系统对应学习资料
- Android BAT大厂面试题(有解析)
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
文末
我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。
以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持)
部分资料一览:
- 330页PDF Android学习核心笔记(内含8大板块)
[外链图片转存中…(img-lGDAW1l0-1712016877230)]
[外链图片转存中…(img-xKs9SFPV-1712016877230)]
-
Android学习的系统对应视频
-
Android进阶的系统对应学习资料
[外链图片转存中…(img-St6ZMdil-1712016877231)]文章来源:https://www.toymoban.com/news/detail-854836.html
- Android BAT大厂面试题(有解析)
[外链图片转存中…(img-GsEEvzLM-1712016877231)]文章来源地址https://www.toymoban.com/news/detail-854836.html
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
到了这里,关于灵魂画师,Android绘制流程——Android高级UI(1)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!