Android走进Framework之AppCompatActivity.setContentView
来源:互联网 发布:sqoop mysql导入hbase 编辑:程序博客网 时间:2024/06/07 23:27
转载请标明出处: http://www.weyye.me/detail/framework-appcompatactivity-setcontentview/
本文出自:【Wey Ye的博客】
前言
在上一篇Android走进Framework之app是如何被启动的中讲到了从我们点击app一直到调用Activity.onCreate()
的整个流程,今天来研究下我们最熟悉的一行代码setContentView()
。网上也有很多关于setContentView的源码解析,但是都是基于Activity
源码,而我们现在都是继承的AppCompatActivity
,看源码发现改动还不少,所以我打算来研究下AppCompatActivity
里是如何把我们的布局添加进去的。你是否也曾有过同样的疑惑,为什么创建Activity
就要在onCreate()
里面调用setContentView()
?那就让我们来RTFSC (Read the fucking source code )。
学前疑惑
setContentView
中到底做了什么?为什么我们调用后就可以显示到我们想到的布局?PhoneWindow
是个什么鬼?Window
和它又有什么关系?DecorView
什么干嘛的?和我们的布局有什么联系?- 在我们调用
requestFeature
的时候为什么要在setContentView
之前?
接下来,我们就来解决这些疑惑!以下源码基于Api24
AppCompatActivity.setContentView
我们先来看下AppCompatActivity
中setContentView
中做了什么
AppCompatActivity.java
//这个是我们最常用的 @Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); } @Override public void setContentView(View view) { getDelegate().setContentView(view); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { getDelegate().setContentView(view, params); }
可以看到3个重载的方法都调用getDelegate()
,而其他的方法也都是调用了getDelegate()
,很显然这个是代理模式。那么这个getDelegate()
返回的是什么呢?
AppCompatActivity.java
/** * @return The {@link AppCompatDelegate} being used by this Activity. */ @NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { //第一次为空,创建了AppCompatDelegate mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; }
我们来看下AppCompatDelegate
是怎么创建的
AppCompatDelegate.java
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) { final int sdk = Build.VERSION.SDK_INT; if (BuildCompat.isAtLeastN()) { //7.0以及7.0以上创建AppCompatDelegateImplN return new AppCompatDelegateImplN(context, window, callback); } else if (sdk >= 23) { //6.0创建AppCompatDelegateImplV23 return new AppCompatDelegateImplV23(context, window, callback); } else if (sdk >= 14) { //... return new AppCompatDelegateImplV14(context, window, callback); } else if (sdk >= 11) { //... return new AppCompatDelegateImplV11(context, window, callback); } else { return new AppCompatDelegateImplV9(context, window, callback); } }
哦~原来根据不同的api版本返回不同的Delegate,我们先来看看AppCompatDelegateImplN
,里面是否有setContentView
AppCompatDelegateImplN.java
@RequiresApi(24)@TargetApi(24)class AppCompatDelegateImplN extends AppCompatDelegateImplV23 { AppCompatDelegateImplN(Context context, Window window, AppCompatCallback callback) { super(context, window, callback); } @Override Window.Callback wrapWindowCallback(Window.Callback callback) { return new AppCompatWindowCallbackN(callback); } class AppCompatWindowCallbackN extends AppCompatWindowCallbackV23 { AppCompatWindowCallbackN(Window.Callback callback) { super(callback); } @Override public void onProvideKeyboardShortcuts( List<KeyboardShortcutGroup> data, Menu menu, int deviceId) { final PanelFeatureState panel = getPanelState(Window.FEATURE_OPTIONS_PANEL, true); if (panel != null && panel.menu != null) { // The menu provided is one created by PhoneWindow which we don't actually use. // Instead we'll pass through our own... super.onProvideKeyboardShortcuts(data, panel.menu, deviceId); } else { // If we don't have a menu, jump pass through the original instead super.onProvideKeyboardShortcuts(data, menu, deviceId); } } }}
发现并没有setContentView
,那么肯定在父类。诶,它继承AppCompatDelegateImplV23
,而AppCompatDelegateImplV23
又继承AppCompatDelegateImplV14
,AppCompatDelegateImplV14
又继承AppCompatDelegateImplV11
,AppCompatDelegateImplV11
又继承AppCompatDelegateImplV9
,好,知道关系后我有点懵逼了,搞什么鬼?客官别急,我们先来画个图
ok,最后我在V9里找到setContentView
,我们来看下
AppCompatDelegateImplV9.java
@Override public void setContentView(int resId) { //这个很关键,稍后会讲 ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); //把我们的布局放到contentParent里面 LayoutInflater.from(mContext).inflate(resId, contentParent); mOriginalWindowCallback.onContentChanged(); } @Override public void setContentView(View v) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); contentParent.addView(v); mOriginalWindowCallback.onContentChanged(); } @Override public void setContentView(View v, ViewGroup.LayoutParams lp) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); contentParent.addView(v, lp); mOriginalWindowCallback.onContentChanged(); }
这是对应的3个实现的方法,发现都会调用ensureSubDecor();
并且都会找到contentParent
,然后把我们的布局放入进去
ok,到这里我们来捋一捋流程。
private void ensureSubDecor() { if (!mSubDecorInstalled) { //这个mSubDecor其实就ViewGroup,调用createSubDecor()后,此时存放我们的布局的容器已经准备好了 mSubDecor = createSubDecor();//核心代码! // If a title was set before we installed the decor, propagate it now CharSequence title = getTitle(); if (!TextUtils.isEmpty(title)) { onTitleChanged(title); } applyFixedSizeWindow(); //SubDecor 安装后的回调 onSubDecorInstalled(mSubDecor); //设置标记位 mSubDecorInstalled = true; // Invalidate if the panel menu hasn't been created before this. // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu // being called in the middle of onCreate or similar. // A pending invalidation will typically be resolved before the posted message // would run normally in order to satisfy instance state restoration. PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); if (!isDestroyed() && (st == null || st.menu == null)) { invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR); } } }
调用了createSubDecor()
,看字面意思创建了一个SubDecor
,看似跟DecorView
有联系。我们看下里面做了什么操作
private ViewGroup createSubDecor() { TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) { a.recycle(); //还记得我们使用AppCompatActivity如果不设置AppCompat主题报的错误吗?就是在这里抛出来的 throw new IllegalStateException( "You need to use a Theme.AppCompat theme (or descendant) with this activity."); } //初始化相关特征标志 if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) { //一般我们的主题默认都是NoTitle requestWindowFeature(Window.FEATURE_NO_TITLE); } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) { // Don't allow an action bar if there is no title. requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR); } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) { requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY); } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) { requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY); } mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false); a.recycle(); //重点!在这里就创建DecorView,至于DecorView到底是什么以及如何创建的,稍后会讲到 mWindow.getDecorView(); final LayoutInflater inflater = LayoutInflater.from(mContext); //可以看到其实就是个ViewGroup,我们接着往下看,跟DecorView到底有啥关系 ViewGroup subDecor = null; if (!mWindowNoTitle) { //上面说了主题默认都是NoTitle,所以不会走里面的方法 if (mIsFloating) { // If we're floating, inflate the dialog title decor subDecor = (ViewGroup) inflater.inflate( R.layout.abc_dialog_title_material, null); ... } else if (mHasActionBar) { TypedValue outValue = new TypedValue(); mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true); ... // Now inflate the view using the themed context and set it as the content view subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); /** * Propagate features to DecorContentParent */ if (mOverlayActionBar) { mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY); } if (mFeatureProgress) { mDecorContentParent.initFeature(Window.FEATURE_PROGRESS); } if (mFeatureIndeterminateProgress) { mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS); } } } else { //我们进入else if (mOverlayActionMode) { //调用了requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY)会走进来 subDecor = (ViewGroup) inflater.inflate( R.layout.abc_screen_simple_overlay_action_mode, null); } else { //ok,所以如果这些我们都没设置,默认就走到这里来了,在这里映射出了subDecor,稍后我们来看下这个布局是啥 subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null); } ... } if (subDecor == null) { throw new IllegalArgumentException( "AppCompat does not support the current theme features: { " + "windowActionBar: " + mHasActionBar + ", windowActionBarOverlay: "+ mOverlayActionBar + ", android:windowIsFloating: " + mIsFloating + ", windowActionModeOverlay: " + mOverlayActionMode + ", windowNoTitle: " + mWindowNoTitle + " }"); } if (mDecorContentParent == null) { mTitleView = (TextView) subDecor.findViewById(R.id.title); } // Make the decor optionally fit system windows, like the window's decor ViewUtils.makeOptionalFitsSystemWindows(subDecor); //这个contentView很重要,是我们布局的父容器,你可以把它直接当成FrameLayout final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); //看过相关知识的同学应该知道android.R.id.content这个Id在以前是我们布局的父容器的Id final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } //注意!原来windowContentView的Id是android.R.id.content,现在设置成NO_ID windowContentView.setId(View.NO_ID); //在之前这个id是我们的父容器,现在将contentView设置成android.R.id.content,那么可以初步判定,这个contentView将会是我的父容器 contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } } // Now set the Window's content view with the decor //注意!重要!将subDecor放入到了这个Window里面,这个Window是个抽象类,其实现类是PhoneWindow,稍后会讲到 mWindow.setContentView(subDecor); .... return subDecor; }
看到了requestWindowFeature是不是很熟悉?还记得我们是怎么让Activity全屏的吗?
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FILL_PARENT ,WindowManager.LayoutParams.FILL_PARENT); setContentView(R.layout.activity_test); }
而且这两行代码必须在setContentView()之前调用,知道为啥了吧?因为在这里就把Window的相关特征标志给初始化了,在setContentView()之后调用就不起作用了!
在代码里其他比较重要的地方已写了注释,我们来看下这个abc_screen_simple.xml
的布局到底是什么样子的
abc_screen_simple.xml
<android.support.v7.internal.widget.FitWindowsLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/action_bar_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:fitsSystemWindows="true"> <android.support.v7.internal.widget.ViewStubCompat android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/abc_action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" /> <include layout="@layout/abc_screen_content_include" /></android.support.v7.internal.widget.FitWindowsLinearLayout>
abc_screen_content_include.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <android.support.v7.internal.widget.ContentFrameLayout android:id="@id/action_bar_activity_content" android:layout_width="match_parent" android:layout_height="match_parent" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /></merge>
原来这个subDecor
就是FitWindowsLinearLayout
看到这2个布局,我们先把这2个布局用图画出来。
(图不在美,能懂就行~)
从AppCompatActivity到现在布局,在我的脑海里浮现出这样的的画面。。。
那这是不是我们app最终的布局呢?当然不是,因为我们还没讲到非常重要的两行代码
mWindow.getDecorView();
mWindow.setContentView(subDecor);
注释中说道Window是个抽象类,其实现类是PhoneWindow。那么我们先来看PhoneWindow的getDecorView做了什么
PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback { @Override public final View getDecorView() { if (mDecor == null || mForceDecorInstall) { installDecor(); } return mDecor; } private void installDecor() { //mDecor是DecorView,第一次mDecor=null,所以调用generateDecor if (mDecor == null) { mDecor = generateDecor(); ... } //第一次mContentParent也等于null if (mContentParent == null) { //可以看到把DecorView传入进去了 mContentParent = generateLayout(mDecor); } }}
在generateDecor()做了什么?其实返回了一个DecorView对象。
protected DecorView generateDecor(int featureId) { // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity. Context context; if (mUseDecorContext) { Context applicationContext = getContext().getApplicationContext(); if (applicationContext == null) { context = getContext(); } else { context = new DecorContext(applicationContext, getContext().getResources()); if (mTheme != -1) { context.setTheme(mTheme); } } } else { context = getContext(); } return new DecorView(context, featureId, this, getAttributes()); }
DecorView是啥呢?
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks { ...}
哦~原来继承FrameLayout,起到了装饰的作用。
我们在来看看generateLayout()
做了什么。
protected ViewGroup generateLayout(DecorView decor) { TypedArray a = getWindowStyle(); //设置一堆标志位... ... if (!mForcedStatusBarColor) { //获取主题状态栏的颜色 mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000); } if (!mForcedNavigationBarColor) { //获取底部NavigationBar颜色 mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000); } //获取主题一些资源 ... // Inflate the window decor. int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { ...我们设置不同的主题以及样式,会采用不同的布局文件... } else { //记住这个布局,之后我们会来验证下布局的结构 layoutResource = R.layout.screen_simple; // System.out.println("Simple!"); } //要开始更改mDecor啦~ mDecor.startChanging(); //注意,此时把screen_simple放到了DecorView里面 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); //这里的ID_ANDROID_CONTENT就是R.id.content; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } ... //这里的getContainer()返回的是个Window类,也就是父Window,一般为空 if (getContainer() == null) { final Drawable background; if (mBackgroundResource != 0) { background = getContext().getDrawable(mBackgroundResource); } else { background = mBackgroundDrawable; } //设置背景 mDecor.setWindowBackground(background); final Drawable frame; if (mFrameResource != 0) { frame = getContext().getDrawable(mFrameResource); } else { frame = null; } mDecor.setWindowFrame(frame); mDecor.setElevation(mElevation); mDecor.setClipToOutline(mClipToOutline); if (mTitle != null) { setTitle(mTitle); } if (mTitleColor == 0) { mTitleColor = mTextColor; } setTitleColor(mTitleColor); } mDecor.finishChanging(); return contentParent; }
可以看到根据不同主题属性使用的不同的布局,然后返回了这个布局contentParent
。
我们来看看这个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" /> <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" /></LinearLayout>
咦,这个布局结构跟subDecor
好相似啊。。
好了,到目前为止我们知道了,当我们调用mWindow.getDecorView();
的时候里面创建DecorView,然后又根据不同主题属性添加不同布局放到DecorView下,然后找到这个布局的R.id.content
,也就是mContentParent
。ok,搞清楚mWindow.getDecorView();
之后,我们在来看看mWindow.setContentView(subDecor);
(注意:此时把subDecor传入进去)
@Override public void setContentView(View view) { //调用下面的重载方法 setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { //在mWindow.getDecorView()已经创建了mContentParent if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } //是否有transitions动画。没有,进入else if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { view.setLayoutParams(params); final Scene newScene = new Scene(mContentParent, view); transitionTo(newScene); } else { //重要!!将这个subDecor也就是FitWindowsLinearLayout添加到这个mContentParent里面了 //mContentParent是FrameLayout,在之前设置的View.NO_ID mContentParent.addView(view, params); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }
当调用了mWindow.getDecorView();
创建了DecorView以及mContentParent
,并且把subDecor
放到了mContentParent
里面。我们再来回头看看AppCompatDelegateImplV9
,还记得它吗?当我们在AppCompatActivity
的setContentView
的时候会去调用AppCompatDelegateImplV9
的setContentView
AppCompatDelegateImplV9.java
@Override public void setContentView(View v) { //此时DecorView和subDecor都创建好了 ensureSubDecor(); //还记得调用createSubDecor的时候把原本是R.id.content的windowContentView设置成了NO_ID,并且将contentView也就是ContentFrameLayout设置成了R.id.content吗?也就是说此时的contentParent就是ContentFrameLayout ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); //将我的布局放到contentParent里面 contentParent.addView(v); mOriginalWindowCallback.onContentChanged(); } @Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); //将我们的布局id映射成View并且放到contentParent下 LayoutInflater.from(mContext).inflate(resId, contentParent); mOriginalWindowCallback.onContentChanged(); } @Override public void setContentView(View v, ViewGroup.LayoutParams lp) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); contentParent.addView(v, lp); mOriginalWindowCallback.onContentChanged(); }
完整布局
ok,看到这里,想必大家在脑海里也有个大致布局了吧,我们再来把整个app初始布局画出来
验证布局
接下来我们来验证下我们布局结构是否正确
新建一个Activity
public class TestAcitivty extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); }}
主题
<!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <item name="android:listDivider">@color/divider_dddddd</item> </style>
为了演示布局非常简单,就是一个textview
<?xml version="1.0" encoding="utf-8"?><TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"></TextView>
运行后,我们在用hierarchyviewer
查看下
看来我们的脑补的布局是对的!
学后总结
整个流程就是这样。看到这里我们明白了,当我们调用setContentView
的时候加载了2次系统布局,在PhoneWindow
里面创建了DecorView
,DecorView
是我们的最底层的View,并且将我们的布局放入到一个ContentFrameLayout
里,我们还知道在setContentView
的时候进行了相关特征标志初始化,所以在它之后调用requestWindowFeature
就会不起作用然后报错。
setContentView时序图
知道这些之后我们不妨用时序图来梳理下整个调用的流程
查看高清无码大图
致谢
【凯子哥带你学Framework】Activity界面显示全解析(上)
结语
当然,在这篇文章中,因为篇幅问题,也有许多没有讲的重要知识点,比如:
PhoneWindow
在哪里初始化?它做了哪些事?view
树是如何被管理的?findViewById
到底是怎么找到对应的View的?- 为什么说
setContentView
在onResume
在对用户可见? - 等等…
在下一篇中我会详细讲解这些问题。
最后
文中如有错误,希望大家指出!
博主整理不易,转载请注明出处:
http://www.weyye.me/detail/framework-appcompatactivity-setcontentview/
- Android走进Framework之AppCompatActivity.setContentView
- 走进Android之AppCompatActivity.setContentView源码分析
- Android FrameWork之旅 --- setContentView
- Android走进Framework之app是如何被启动的
- android-类学习之AppCompatActivity
- Android之setContentView和LayoutInflater
- Android之setContentView和LayoutInflater
- Android之setContentView和LayoutInflater
- Android之setContentView和LayoutInflater
- Android 源码解析 之 setContentView
- Android 源码解析 之 setContentView
- Android 源码解析 之 setContentView
- android之深入了解setContentView
- Android | AppCompatActivity
- Android之AppCompatActivity与Activity区别
- Android Activity应用之 两次调用setContentView
- Android UI 详解之LayoutInflater和setContentView
- Android布局加载之setContentView源码分析
- 状态模式
- 复杂的整数划分
- 前端开发工具Sublime Text3的使用配置
- 最短路径(Dijkstra)
- C#调用PB写的com组件dll
- Android走进Framework之AppCompatActivity.setContentView
- 序列化和反序列化
- windows 窗口消息
- PAT L1-025. 正整数A+B
- java反射:获取父类中的所有属性和方法工具类
- CSS学习笔记(二)--CSS框模型/CSS定位
- [AngularJS系列(3)] View-Model双向绑定背后的故事~
- 自己需要进一步学习的JAVA技术之招聘对照
- 【视觉-摄像机1】opencv 调用工业摄像机(USB接口和GigE接口)