背景

换肤方案原理在网上已经很多了, 这里不再详细描述, 强迫症的我总是想让提供给别人使用的SDK尽量好用, 哪怕是给自己带来额外的工作量, 经过一段时间的奋斗, 实现了一个自我感觉良好的换肤框架.

这里主要来看看Android 源码中”com.android.support:appcompat-v7”包的实现, 以及源码思想在Android-skin-support中的应用 – 如何打造一款好用的换肤框架.

appcompat-v7包实现

首先来看一下源码的实现: 
AppCompatActivity源码

public class AppCompatActivity extends FragmentActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();delegate.installViewFactory();delegate.onCreate(savedInstanceState);...}@Overridepublic MenuInflater getMenuInflater() {return getDelegate().getMenuInflater();}@Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}@Overridepublic void setContentView(View view) {getDelegate().setContentView(view);}....
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

AppCompatActivity 将大部分生命周期委托给了AppCompatDelegate

再看看相关的类图 

AppCompateDelegate的子类AppCompatDelegateImplV9

class AppCompatDelegateImplV9 extends AppCompatDelegateImplBaseimplements MenuBuilder.Callback, LayoutInflaterFactory {@Overridepublic void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {LayoutInflaterCompat.setFactory(layoutInflater, this);} else {if (!(LayoutInflaterCompat.getFactory(layoutInflater)instanceof AppCompatDelegateImplV9)) {Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"+ " so we can not install AppCompat's");}}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

从这可以看出通过实现LayoutInflaterFactory接口来实现换肤至少可以支持到api 9以上

网上很多换肤框架的实现, 通过LayoutInflater.setFactory的方式, 在回调的onCreateView中解析每一个View的attrs, 判断是否有已标记需要换肤的属性, 比方说background, textColor, 或者说相应资源是否为skin_开头等等. 
然后保存到map中, 对每一个View做for循环去遍历所有的attr, 想要对更多的属性进行换肤, 需要Activity实现接口, 将需要换肤的View, 以及相应的属性收集到一起 
那么是不是能够寻求一种让使用者更方便的方式来实现, 做一个侵入性尽量小的框架呢?

本着开发者应有的好奇心, 深入的研究了一些v7包的实现 


AppCompatDelegateImplV9中, 在LayoutInflaterFactory的接口方法onCreateView 中将View的创建交给了AppCompatViewInflater

@Override
public final View onCreateView(View parent, String name,Context context, AttributeSet attrs) {// First let the Activity's Factory try and inflate the viewfinal View view = callActivityOnCreateView(parent, name, context, attrs);if (view != null) {return view;}// If the Factory didn't handle it, let our createView() method tryreturn createView(parent, name, context, attrs);
}@Override
public View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs) {final boolean isPre21 = Build.VERSION.SDK_INT < 21;if (mAppCompatViewInflater == null) {mAppCompatViewInflater = new AppCompatViewInflater();}// We only want the View to inherit its context if we're running pre-v21final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */true, /* Read read app:theme as a fallback at all times for legacy reasons */VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

再来看一下AppCompatViewInflater中createView的实现

public final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {......View view = null;switch (name) {case "TextView":view = new AppCompatTextView(context, attrs);break;case "ImageView":view = new AppCompatImageView(context, attrs);break;case "Button":view = new AppCompatButton(context, attrs);break;case "EditText":view = new AppCompatEditText(context, attrs);break;case "Spinner":view = new AppCompatSpinner(context, attrs);break;case "ImageButton":view = new AppCompatImageButton(context, attrs);break;case "CheckBox":view = new AppCompatCheckBox(context, attrs);break;......}......return view;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

再看一下其中一个类AppCompatTextView的实现

public class AppCompatTextView extends TextView implements TintableBackgroundView {public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(TintContextWrapper.wrap(context), attrs, defStyleAttr);mBackgroundTintHelper = new AppCompatBackgroundHelper(this);mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper = AppCompatTextHelper.create(this);mTextHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper.applyCompoundDrawablesTints();}@Overridepublic void setBackgroundResource(@DrawableRes int resId) {super.setBackgroundResource(resId);if (mBackgroundTintHelper != null) {mBackgroundTintHelper.onSetBackgroundResource(resId);}}......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

AppCompatBackgroundHelper.Java

void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,R.styleable.ViewBackgroundHelper, defStyleAttr, 0);......if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {mBackgroundResId = a.getResourceId(R.styleable.ViewBackgroundHelper_android_background, -1);ColorStateList tint = mDrawableManager.getTintList(mView.getContext(), mBackgroundResId);if (tint != null) {setInternalBackgroundTint(tint);}}......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

到这里我仿佛是发现了新大陆一样兴奋, 源码中可以通过拦截View创建过程, 替换一些基础的组件, 然后对一些特殊的属性(eg: background, textColor) 做处理, 那我们为什么不能将这种思想拿到换肤框架中来使用呢?

Android-skin-support换肤框架实现

抱着试一试不会少块肉的心情, 开始了我的换肤框架开发之路

先简单讲一下原理: 
1. 参照源码实现在Activity onCreate中为LayoutInflater setFactory, 将View的创建过程交给自定义的SkinCompatViewInflater类来实现 
2. 重写系统组件, 实现换肤接口, 表明该控件支持换肤, 并在View创建之后统一收集 
3. 在重写的View中解析出需要换肤的属性, 并保存ResId到成员变量 
4. 重写类似setBackgroundResource方法, 解析需要换肤的属性, 并保存变量 
5. applySkin 在切换皮肤的时候, 从皮肤资源中获取资源

下面说一个简单的例子(SkinCompatTextView): 
1. 实现SkinCompatSupportable接口 
2. 在构造方法中通过SkinCompatBackgroundHelper和SkinCompatTextHelper分别解析出background, textColor并保存 
3. 重写setBackgroundResource和setTextAppearance, 解析出对应的资源Id, 表明该控件支持从代码中设置资源, 且支持该资源换肤 
4. 在用户点击切换皮肤时调用applySkin方法设置皮肤

public interface SkinCompatSupportable {void applySkin();
}public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper = new SkinCompatTextHelper(this);mTextHelper.loadFromAttributes(attrs, defStyleAttr);}@Overridepublic void setBackgroundResource(@DrawableRes int resId) {super.setBackgroundResource(resId);if (mBackgroundTintHelper != null) {mBackgroundTintHelper.onSetBackgroundResource(resId);}}@Overridepublic void setTextAppearance(Context context, int resId) {super.setTextAppearance(context, resId);if (mTextHelper != null) {mTextHelper.onSetTextAppearance(context, resId);}}@Overridepublic void applySkin() {if (mBackgroundTintHelper != null) {mBackgroundTintHelper.applySkin();}if (mTextHelper != null) {mTextHelper.applySkin();}}
}public class SkinCompatTextHelper extends SkinCompatHelper {private static final String TAG = SkinCompatTextHelper.class.getSimpleName();private final TextView mView;private int mTextColorResId = INVALID_ID;private int mTextColorHintResId = INVALID_ID;public SkinCompatTextHelper(TextView view) {mView = view;}public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {final Context context = mView.getContext();// First read the TextAppearance style idTintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,R.styleable.SkinCompatTextHelper, defStyleAttr, 0);final int ap = a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);SkinLog.d(TAG, "ap = " + ap);a.recycle();if (ap != INVALID_ID) {a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.SkinTextAppearance);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);}a.recycle();}// Now read the style's valuesa = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.SkinTextAppearance,defStyleAttr, 0);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);}a.recycle();applySkin();}public void onSetTextAppearance(Context context, int resId) {final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context,resId, R.styleable.SkinTextAppearance);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);}a.recycle();applySkin();}public void applySkin() {mTextColorResId = checkResourceId(mTextColorResId);if (mTextColorResId != INVALID_ID) {ColorStateList color = SkinCompatResources.getInstance().getColorStateList(mTextColorResId);mView.setTextColor(color);}mTextColorHintResId = checkResourceId(mTextColorHintResId);if (mTextColorHintResId != INVALID_ID) {ColorStateList color = SkinCompatResources.getInstance().getColorStateList(mTextColorHintResId);mView.setHintTextColor(color);}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120

开发过程中遇到的一些问题

在5.0以上, 使用color为ImageView设置src, 可以通过getColorStateList获取资源, 而在5.0以下, 需要通过ColorDrawable setColor的方式实现

String typeName = mView.getResources().getResourceTypeName(mSrcResId);
if ("color".equals(typeName)) {if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {int color = SkinCompatResources.getInstance().getColor(mSrcResId);Drawable drawable = mView.getDrawable();if (drawable instanceof ColorDrawable) {((ColorDrawable) drawable.mutate()).setColor(color);} else {mView.setImageDrawable(new ColorDrawable(color));}} else {ColorStateList colorStateList = SkinCompatResources.getInstance().getColorStateList(mSrcResId);Drawable drawable = mView.getDrawable();DrawableCompat.setTintList(drawable, colorStateList);mView.setImageDrawable(drawable);}
} else if ("drawable".equals(typeName)) {Drawable drawable = SkinCompatResources.getInstance().getDrawable(mSrcResId);mView.setImageDrawable(drawable);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

还有很多问题, 有兴趣的同学可以来一起交流解决.

总结

  1. 这样的做法与网上其他框架相比优势在哪里, 为什么重复造轮子

    • 在增加框架开发成本的基础上降低了框架使用的成本, 我觉得更有意义, 一次开发, 所有Android 开发者都受用;
    • 换肤框架对业务代码的侵入性比较小, 业务代码只需要继承自SkinCompatActivity, 不需要实现接口重写方法, 不需要其他额外的代码, 接入方便, 假如将来不想再使用本框架, 只需要把SkinCompatActivity改为原生Activity即可;
    • 深入源码, 和源码实现方式类似, 兼容性更好.
  2. 为什么选择继承自AppCompatActivity, AppCompatTextView…而不是选择直接继承自Activity, TextView…

    • 本身appcompat-v7包是一个support包, 兼容原生控件, 同时符合Material design, 我们只需要获取我们想要换肤的属性就可以在不破坏support包属性的前提下进行换肤;
    • 参与开发的同学更多的话, 完全可以支持一套继承自Activity, TextView…的skin support包.
  3. 自定义View能否支持, 第三方控件是否支持换肤

    • 答案是肯定的, 完全可以参照SkinCompatTextView的实现, 自己去实现自定义控件, 对于使用者来说, 扩展性很好.

源码地址: https://github.com/ximsfei/Android-skin-support

Android 两行代码实现换肤 从appcompat-v7原理出发相关推荐

  1. android换肤的实现方案,一种基于Android平台的一键换肤方法与流程

    本发明属于互联网技术领域,尤其涉及一种基于Android平台的一键换肤方法. 背景技术: 换肤性能的添加在很大的程度上面提高了用户的体验感,用户可以根据自己喜好选择自己喜欢的皮肤颜色,并且我们针对不同 ...

  2. 三步实现Android应用内一键换肤,无需重启应用,无需编写多余代码

    古人学问无遗力,少壮工夫老始成. 纸上得来终觉浅,绝知此事要躬行. --<冬夜读书示子聿> · 陆游 换肤效果演示 四种皮肤切换:默认白色.蓝色.绿色和玫瑰红: 前言 先给大家介绍一下现在 ...

  3. android+皮肤机制,Android-skin-loader 换肤总结

    前言 最近有个换肤的需求.基于github上的这个开源框架Android-Skin-Loader.这个框架的换肤机制是使用动态加载的机制去加载皮肤包里面的内容,所谓的皮肤包是实际上是一个apk文件,里 ...

  4. android 换肤 字体颜色,android使用SkinManager实现换肤功能的示例

    试着用鸿洋大神写的SkinManager实现了换肤功能. 一.配置 在app下build.gradle中添加依赖: //换肤功能 compile 'com.zhy:changeskin:4.0.2' ...

  5. android 皮肤方案,Android布局流程及换肤原理

    Android 的布局流程 不考虑AMS binder机制,那么Android 的布局流程的最开始的入口(把前面的当作一个黑盒子,那么后续动作的第一个入口),则是在ActivityThread的per ...

  6. Android应用如何实现换肤功能

    Android免费培训QQ群(118949422)第一期 转载请注明出处,商用请与本人联系. 本系列专题培训适用范围:初级Android程序员,即有J2SE基础和Android初级水平.J2SE基础是 ...

  7. Leakcanary原理解析以及换肤框架skin的原理分析

    一.错误现场 java.lang.ClassCastException: androidx.appcompat.widget.TintContextWrapper cannot be cast to ...

  8. Android 应用换肤方案的总结

    虽然现在已经有很多不错的换肤方案,但是这些方案或多或少都存在自己的问题.在这篇文章中,我将对 Android 现有的一些动态换肤方案进行梳理,对其底层实现原理进行分析,然后对开发一个新的换肤方案的可能 ...

  9. 对 Android 应用换肤方案的总结

    作者:me 虽然现在已经有很多不错的换肤方案,但是这些方案或多或少都存在自己的问题.在这篇文章中,我将对 Android 现有的一些动态换肤方案进行梳理,对其底层实现原理进行分析,然后对开发一个新的换 ...

最新文章

  1. jdk8新特性_JDK8与JDK9新特性学习
  2. A*寻路算法与它的速度
  3. 新字符设备驱动实验(自动分配设备号、自动创建应用层设备节点、新字符设备注册到内核的结构体)
  4. java JFileChooser选择文件和保存文件
  5. SpringBoot和SpringCloud面试题
  6. python以二进制读取的文件显示b'b'_python - Python读取二进制文件并解码 - 堆栈内存溢出...
  7. java 中的static 用法
  8. 数字电子技术基础(六):译码器、数据选择器
  9. 盘点6款实用的文件对比工具,你都用过吗?
  10. python小论文范文3000字_论文范文3000字
  11. 计算机白板培训心得,电子白板培训心得体会
  12. Lmp7721通过跨阻运放方式采集荧光信号踩坑
  13. Java(二十二) -- 生产者消费者模式
  14. 如何拆分PDF成多个文件?这样拆分非常简单
  15. 西门子Smart 485数据通讯读取和写入程序,modbustcp,modbus主站从站通讯
  16. 使用 vue 和 canvas 制作的游戏
  17. Linux服务器各应用版本信息查看总结
  18. 博物馆RFID资产管理解决方案-RFID资产防盗管理-新导智能
  19. Python数据分析速成课程
  20. DSP下载器接口引脚以及烧录程序中的两个错误解决

热门文章

  1. windows下解决8080端口被占用
  2. 南开中学计算机实验员工资待遇,【重庆重庆南开中学工资】教师待遇-看准网...
  3. java属于什么意向应聘岗位_请针对你意向的岗位简单介绍自己。
  4. ESP32-WROOM-32D模组上传Arduino IDE示例编译的固件后总是重启,该如何解决
  5. Notability没办法连接到icloud时怎么做?
  6. Duplicated tag: ‘mirrors‘
  7. Python对Excel的常规操作 之 读取,写入(保留原格式写入)
  8. 个人设计web前端大作业 HTML期末大作业 学生个人网页设计作品 学生个人网页设计作品 学生个人网页模板 简单个人主页成品
  9. 华为荣耀服务器浏览网页卡,华为荣耀6PLUS手机信号很好但是网速很慢的解决办法...
  10. 2019最新三年php 面试题