灵魂画师,Android绘制流程——Android高级UI(1)

这篇具有很好参考价值的文章主要介绍了灵魂画师,Android绘制流程——Android高级UI(1)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  1. 绘制流程从何而起
  2. Activity 的界面结构在哪里开始形成
  3. 绘制流程如何运转起来

接下来我们就一个个目标来 conquer。

三、绘制流程从何而起

我们一说到绘制流程,就会想到或是听过onMeasureonLayoutonDraw这三个方法,但是有没想过为什么我们开启一个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 的 onCreateonStartonResume

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); 最终会调用 onCreateonStart 方法。眼见为实,耳听为虚,我们继续进入深入。来到下面这段代码

// 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 这行代码。进入该方法,方法内会调用 activityperformCreate 方法,而 performCreate 方法里会调用到我们经常重写的 Activity 生命周期的 onCreate 方法。😄至此,找到了 onCreate 的调用地方,这里需要立个 FLAG1,因为目标二需要的开启便是这里,我下一小节分享,勿急。

回过头来继续 performLaunchActivity 方法的执行,会调用到 activityperformStart 方法,而该方法又会调用到 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 的生命周期函数 onCreateonStartonResume 。按照这一套路,童鞋们可以看看 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();

}

调用了 mTraversalRunnablerun 方法之后,会发现也是一连串的方法调用,后来到 performTraversals,这里面就有我们一直提到三个绘制流程方法的起源地。这三个起源地就是我们在上面看到的三个方法 performMeasureperformLayoutperformDraw

而这三个方法会进行如下图的一个调用链(😄还是手绘,勿喷),从代码我们也知道,会按照 performMeasureperformLayoutperformDraw 的顺序依次调用。

performMeasure 会触发我们的测量流程,如图中所示,进入第一层的 ViewGroup,会调用 measureonMeasure,在 onMeasure 中调用下一层级,然后下一层级的 View或ViewGroup 会重复这样的动作,进行所有 View 的测量。(这一过程可以理解为书的深度遍历)

performLayoutperformMeasure 的流程大同小异,只是方法名不同,就不再赘述。

performDraw 稍微些许不同,当前控件为ViewGroup时,只有需要绘制背景或是我们通过 setWillNotDraw(false) 设置我们的ViewGroup需要进行绘制时,会进入 onDraw 方法,然后通过 dispatchDraw 进行绘制子View,如此循环。而如果为View,自然也就不需要绘制子View,只需绘制自身的内容即可。 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 至此,绘制流程的源头我们便了解清楚了, onMeasureonLayoutonDraw 三个方法我们会在后面进行详述并融入在实战中。

四、Activity 的界面结构在哪里开始形成

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 上图是 Activity 的结构。我们先进行大致的描述,然后在进入源码体会这一过程。

我们可以清晰的知道一个 Activity 会对应着有一个 Window,而 Window 的唯一实现类为 PhoneWindowPhoneWindow 的初始化是在 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移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

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

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

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

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

文末

我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。

以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持)

部分资料一览:

  • 330页PDF Android学习核心笔记(内含8大板块)

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

  • Android BAT大厂面试题(有解析)

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

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

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

灵魂画师,Android绘制流程——Android高级UI(1),程序员,android,ui

文末

我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。

以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持)

部分资料一览:

  • 330页PDF Android学习核心笔记(内含8大板块)

[外链图片转存中…(img-lGDAW1l0-1712016877230)]

[外链图片转存中…(img-xKs9SFPV-1712016877230)]

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

[外链图片转存中…(img-St6ZMdil-1712016877231)]

  • Android BAT大厂面试题(有解析)

[外链图片转存中…(img-GsEEvzLM-1712016877231)]文章来源地址https://www.toymoban.com/news/detail-854836.html

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

到了这里,关于灵魂画师,Android绘制流程——Android高级UI(1)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 零基础也能看得懂的AI绘画教程!灵魂画师MJ咒语案例展示!

    当谈到人工智能技术应用场景时,除了ChatGPT,另外让人着迷的应用就是AI绘画,随着图像生成器的不断升级和更迭,图片生成的质量也得到了显著提升。 目前画图支持Midjourney和Stable Diffusion,风景、人物、动物、建筑、梦境等等,都可以用MJ和SD生成,同聊天一样,只有掌握正

    2024年04月23日
    浏览(43)
  • 2023 程序员职业发展规划:除了写代码,程序员还能做什么工作?—— 有一个问题直击我的灵魂深处:如果我不想再当程序员了,该怎么办?

    有一个问题直击我的灵魂深处: 如果我不想再当程序员了,该怎么办?   关于这个问题我后来思考了很久。我带产品和技术团队将近十年了,我意识到自己遇到了新的挑战。所以今年夏天,我开始了自由技术写作。很快我便意识到,技术写作完全可以作为程序员转型后的职

    2024年02月05日
    浏览(35)
  • 程序员的520花式绘制爱心代码大全

    声明 :代码是祖传代码,我不知道原创是谁了,修修改改。主要是为了给情侣们用,虽然自己贵为单身狗。 matlab代码: matlab代码如下: python代码: python代码: 演示: 前端浪漫表白 网站可访问: 已部署到网址,可访问:https://yanghanwen.xyz/ai/ 此网站用不变更,致曾最爱的人

    2024年02月03日
    浏览(35)
  • 学习笔记-微服务高级(黑马程序员)

    测试软件 jmeter 雪崩问题 个微服务往往依赖于多个其它微服务,服务提供者I发生了故障,依赖于当前服务的其它服务随着时间的推移形成级联失败 超时处理 设定超时时间,请求超过一定时间没有响应就返回错误信息 仓壁模式 限定每个业务能使用的线程数,避免耗尽整个tom

    2024年04月25日
    浏览(39)
  • 黑马程序员JavaWeb开发|Maven高级

    将项目按照功能拆分成若干个子模块,方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享。 注意:分模块开发需要先对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分。 继承:描述的是两个工程间的关系,与java中的继承相似,子工程

    2024年01月23日
    浏览(31)
  • 初级程序员,到高级需要学习哪些东西

    很多初学者,在一开始学习IT时都不知道如何入手,小编今天就根据自己的经验给大家说说,初学者最重要的三个问题。 《计算机基础》学习 《计算机基础》,这是所有读者大学最开始都会上的课吧,我问了群里的仔,他们都说是的,我想大家也是。在计算机基础中我们会学

    2024年02月03日
    浏览(29)
  • 高级程序员和新手小白程序员区别你是那个等级看解决bug速度

    IT入门深似海 ,程序员行业,我觉得是最难做的。加不完的班,熬不完的夜。 和产品经理,扯不清,理还乱的宿命关系 一直都在 新需求-做项目-解决问题-解决bug-新需求 好像一直都是这么一个循环。(哈哈哈)我觉得一个好的程序员,判断根本取决于,遇到生产问题和bug,解决

    2024年02月07日
    浏览(30)
  • 超实用的 Linux 高级命令,程序员一定要懂

    在运维的坑里摸爬滚打好几年了,我还记得我刚开始的时候,我只会使用一些简单的命令,写脚本的时候,也是要多简单有多简单,所以有时候写出来的脚本又长又臭。 像一些高级点的命令,比如说 Xargs 命令、管道命令、自动应答命令等,如果当初我要是知道,那我也可能

    2023年04月17日
    浏览(33)
  • 高级Java程序员必问,Redis事务终极篇

    Redis事务(Transaction)通过将多个Redis操作封装为一个原子性的操作序列,确保在事务执行过程中,不会受到其他客户端的干扰。从而在保证数据一致性的同时,协调并发,提高数据操作的效率和性能。 在分布式系统和高并发场景下,事务处理具有重要意义。Redis事务可以确保

    2024年02月02日
    浏览(51)
  • UI绘制流程分析(前篇)--App与Activity的启动

    彻底搞懂UI绘制流程,看该系列就够了 作为安卓开发最重要的知识点之一,UI绘制无疑是必须掌握的,要想搞懂它的测量、布局和绘制,得先理解它的整个流程,但现在让我们把时间再往前拨一下,先要从App启动流程以及Activity启动流程讲起。 提示:以下是本篇文章正文内容

    2024年02月16日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包