Android 适配深色模式
Android 深色模式适配
Android 10 开始支持配置深色模式,如果系统是深色主题,但是打开APP又是浅色主题就会显得格格不入。下面介绍几种适配深色模式的方法。
一、forceDarkAllowed
样式中设置 android:forceDarkAllowed
属性,深色主题下系统会自动进行适配。
新建 values-v29 目录,因为
android:forceDarkAllowed
属性 Android 10开始才有。设置
android:forceDarkAllowed
属性为true
适配效果
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:minWidth="200dp"android:minHeight="100dp"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:text="深色模式"/>
</androidx.constraintlayout.widget.ConstraintLayout>
浅色主题:
深色主题:
从布局文件中可以看到,并没有设置任何背景色,但深色主题下,APP自动进行了适配。
这种适配方式十分简单,但是不够美观,无法自定义控件颜色样式,全凭系统控制,并不推荐这种自动化方式实现深色模式。
二、设置深色主题
官方推荐另外一种方法,即分别创建浅色和深色的主题样式。
新建 values-night 目录,存放深色主题的样式
适配效果
浅色主题:
深色主题:
与 forceDarkAllowed
最大的区别在于,深色主题可以手动设置颜色样式。
一些常用的方法:
- 判断深色主题
public static boolean isDarkTheme(Context context) {int flag = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;return flag == Configuration.UI_MODE_NIGHT_YES;}
- 代码中切换深色主题
if (isDarkTheme(MainActivity.this)) {AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);} else {AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);}
- 禁止界面适配深色主题
Activity 的 configChanges
属性当中配置 uiMode
避免Activity 重新创建,从而阻止界面适配深色主题。
<activityandroid:exported="true"android:name=".MainActivity"android:configChanges="uiMode"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity>
这时候虽然界面不会重新创建,但是会触发 onConfigurationChanged
方法回调,可以根据回调做一些处理。
@Overridepublic void onConfigurationChanged(@NonNull Configuration newConfig) {super.onConfigurationChanged(newConfig);int mSysThemeConfig = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;switch (mSysThemeConfig) {// 亮色主题case Configuration.UI_MODE_NIGHT_NO:break;// 深色主题case Configuration.UI_MODE_NIGHT_YES:break;default:break;}}
三、使用 Android-skin-support
上述两种方式只支持Android 10 系统,且系统切换深色主题界面会重新创建,并不是太灵活。如果想适配Android 10 以下的系统可以使用 Android-skin-support 框架实现。
Android-skin-support 是一个换肤框架,通过加载不同的皮肤包从而实现换肤,深色模式只需要创建对应的深色主题皮肤包,然后替换当前的默认样式就可以实现适配。
它的实现流程大致如下:
- 控制View的创建,将所有View替换为对应的SkinxxxView
- SkinxxxView中会根据布局中的属性ID值,找到皮肤包中对应的资源进行替换
- 动态换肤,即通知所有的SkinXXXView进行更新
3.1 Android View 创建流程
在使用 Android-skin-support 之前,可以先了解下 Android View 创建流程,有利于我们之后使用该库。
从 setContentView 方法开始,它作用就是设置界面布局资源。
//Activitypublic void setContentView(@LayoutRes int layoutResID) {getWindow().setContentView(layoutResID);initWindowDecorActionBar();}
调用的Window中的 setContentView 方法。
//PhoneWindow@Overridepublic void setContentView(int layoutResID) {......if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());transitionTo(newScene);} else {mLayoutInflater.inflate(layoutResID, mContentParent);}......}
调用 LayoutInflater 的 inflate 方法
//LayoutInflaterpublic View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {final Resources res = getContext().getResources();if (DEBUG) {Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("+ Integer.toHexString(resource) + ")");}View view = tryInflatePrecompiled(resource, res, root, attachToRoot);if (view != null) {return view;}XmlResourceParser parser = res.getLayout(resource);try {return inflate(parser, root, attachToRoot);} finally {parser.close();}}
可以看到根据布局资源创建一个Xml 解析器进行解析
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {......// Temp is the root view that was found in the xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);......// Inflate all children under temp against its context.rInflateChildren(parser, temp, attrs, true);......}}
先通过createViewFromTag方法创建rootView,然后再使用rInflateChildren解析子布局,最终都是通过createView创建View
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {......View view = tryCreateView(parent, name, context, attrs);......
}public final View tryCreateView(@Nullable View parent, @NonNull String name,@NonNull Context context,@NonNull AttributeSet attrs) {......View view;if (mFactory2 != null) {view = mFactory2.onCreateView(parent, name, context, attrs);} else if (mFactory != null) {view = mFactory.onCreateView(name, context, attrs);} else {view = null;}......}
可以看到如果mFactory2不为空则通过mFactory2来创建View,而mFactory2又是哪里进行初始化的呢?
通过查看代码发现,mFactory2初始化代码如下:
//AppCompatActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();delegate.installViewFactory();delegate.onCreate(savedInstanceState);super.onCreate(savedInstanceState);
}//AppCompatDelegateImpl@Overridepublic void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {LayoutInflaterCompat.setFactory2(layoutInflater, this);} else {if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"+ " so we can not install AppCompat's");}}}public static void setFactory2(@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {inflater.setFactory2(factory);......}public void setFactory2(Factory2 factory) {//mFactory2 不可以重复设置,否则会直接抛出异常if (mFactorySet) {throw new IllegalStateException("A factory has already been set on this LayoutInflater");}if (factory == null) {throw new NullPointerException("Given factory can not be null");}mFactorySet = true;if (mFactory == null) {mFactory = mFactory2 = factory;} else {mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);}}
上述代码可以发现,mFactory2其实就是AppCompatDelegateImpl,现在我们再看下AppCompatDelegateImpl的 onCreateView方法
@Override
public View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs) {......return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP, /* 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 */);
}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;// We need to 'inject' our tint aware Views in place of the standard framework versionsswitch (name) {case "TextView":view = createTextView(context, attrs);verifyNotNull(view, name);break;case "ImageView":view = createImageView(context, attrs);verifyNotNull(view, name);break;case "Button":view = createButton(context, attrs);verifyNotNull(view, name);break;case "EditText":view = createEditText(context, attrs);verifyNotNull(view, name);break;......default:// The fallback that allows extending class to take over view inflation// for other tags. Note that we don't check that the result is not-null.// That allows the custom inflater path to fall back on the default one// later in this method.view = createView(context, name, attrs);}......return view;}@NonNullprotected AppCompatTextView createTextView(Context context, AttributeSet attrs) {return new AppCompatTextView(context, attrs);}
上述代码可以看到,创建View其实是通过控件的名称,然后再new对应的控件,通过createTextView可以发现创建的也不是TextView,而是AppCompatTextView,所以只要能够修改mFactory2,就可以控制所有View的创建。
3.2 使用方法
集成 Android-skin-support
implementation 'skin.support:skin-support:4.0.5' // skin-supportimplementation 'skin.support:skin-support-appcompat:4.0.5' // skin-support 基础控件支持implementation 'skin.support:skin-support-design:4.0.5' // skin-support-design material design 控件支持[可选]implementation 'skin.support:skin-support-cardview:4.0.5' // skin-support-cardview CardView 控件支持[可选]implementation 'skin.support:skin-support-constraint-layout:4.0.5' // skin-support-constraint-layout ConstraintLayout 控件支持[可选]
初始化
//初始化换肤框架SkinCompatManager.withoutActivity(this)//添加各类控件的拦截器.addInflater(new SkinAppCompatViewInflater()).addInflater(new SkinConstraintViewInflater()).addInflater(new SkinCardViewInflater()).addInflater(new SkinMaterialViewInflater());//Activity中重写下面方法,可以放到BaseActivity中@NonNull@Overridepublic AppCompatDelegate getDelegate() {return SkinAppCompatDelegateImpl.get(this, this);}
withoutActivity 方法
public static SkinCompatManager withoutActivity(Application application) {init(application);SkinActivityLifecycle.init(application);return sInstance;
}
重点看 SkinActivityLifecycle.init 方法,主要做了两件事情:
- 注册Activity生命周期回调,
- 替换系统的mFactory2,控制View的创建
public static SkinActivityLifecycle init(Application application) {if (sInstance == null) {synchronized (SkinActivityLifecycle.class) {if (sInstance == null) {sInstance = new SkinActivityLifecycle(application);}}}return sInstance;
}private SkinActivityLifecycle(Application application) {//注册Activity生命周期回调application.registerActivityLifecycleCallbacks(this);//替换系统的mFactory2,控制View的创建installLayoutFactory(application);SkinCompatManager.getInstance().addObserver(getObserver(application));
}private void installLayoutFactory(Context context) {try {LayoutInflater layoutInflater = LayoutInflater.from(context);LayoutInflaterCompat.setFactory2(layoutInflater, getSkinDelegate(context));} catch (Throwable e) {Slog.i("SkinActivity", "A factory has already been set on this LayoutInflater");}
}
getSkinDelegate(context) 方法,初始化并返回 SkinCompatDelegate 对象
private SkinCompatDelegate getSkinDelegate(Context context) {if (mSkinDelegateMap == null) {mSkinDelegateMap = new WeakHashMap<>();}SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);if (mSkinDelegate == null) {mSkinDelegate = SkinCompatDelegate.create(context);mSkinDelegateMap.put(context, mSkinDelegate);}return mSkinDelegate;}
SkinCompatDelegate 实现了LayoutInflater.Factory2 接口,重写 onCreateView 方法,从而控制所有View的创建
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {//创建ViewView view = createView(parent, name, context, attrs);if (view == null) {return null;}//保存到列表中if (view instanceof SkinCompatSupportable) {mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));}return view;
}public View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs) {if (mSkinCompatViewInflater == null) {mSkinCompatViewInflater = new SkinCompatViewInflater();}......return mSkinCompatViewInflater.createView(parent, name, context, attrs);}public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {View view = createViewFromHackInflater(context, name, attrs);if (view == null) {//通过初始化的各个拦截器,根据控件名称创建对应的控件view = createViewFromInflater(context, name, attrs);}if (view == null) {//如果没有找到,就类似系统的实现方法view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check it's android:onClickcheckOnClickListener(view, attrs);}return view;}
这里的实现有些类似OkHttp的拦截器,View的创建会经过多个 “ViewInflater”,这边选一个拦截器 SkinConstraintViewInflater 看下内部是怎么实现的。
public class SkinConstraintViewInflater implements SkinLayoutInflater {@Overridepublic View createView(Context context, final String name, AttributeSet attrs) {View view = null;switch (name) {case "androidx.constraintlayout.widget.ConstraintLayout":view = new SkinCompatConstraintLayout(context, attrs);break;default:break;}return view;}
}
代码很简单,就是通过控件名称创建自己的 SkinCompatConstraintLayout
换肤控件,
public class SkinCompatConstraintLayout extends ConstraintLayout implements SkinCompatSupportable {private final SkinCompatBackgroundHelper mBackgroundTintHelper;......public SkinCompatConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);}@Overridepublic void setBackgroundResource(int resId) {super.setBackgroundResource(resId);if (mBackgroundTintHelper != null) {mBackgroundTintHelper.onSetBackgroundResource(resId);}}//收到换肤事件,@Overridepublic void applySkin() {if (mBackgroundTintHelper != null) {mBackgroundTintHelper.applySkin();}}
}
SkinCompatConstraintLayout 实现 SkinCompatSupportable接口,触发换肤时会调用applySkin方法替换控件的背景。
加载皮肤包
//加载皮肤包SkinCompatManager.getInstance().loadSkin(DARK_SKIN_NAME, new SkinLoaderListener() {@Overridepublic void onStart() {}@Overridepublic void onSuccess() {}@Overridepublic void onFailed(String errMsg) {}
}, SKIN_LOADER_STRATEGY_ASSETS);
/*** 加载皮肤包.** @param skinName 皮肤包名称.* @param listener 皮肤包加载监听.* @param strategy 皮肤包加载策略.SKIN_LOADER_STRATEGY_ASSETS从assets目录加载皮肤包*/
loadSkin(String skinName, SkinLoaderListener listener, int strategy)
重点看下loadSkin方法
public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy);if (loaderStrategy == null) {return null;}return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName);
}
使用AsynTask进行异步操作,加载皮肤包
private class SkinLoadTask extends AsyncTask<String, Void, String> {private final SkinLoaderListener mListener;private final SkinLoaderStrategy mStrategy;SkinLoadTask(@Nullable SkinLoaderListener listener, @NonNull SkinLoaderStrategy strategy) {mListener = listener;mStrategy = strategy;}@Overrideprotected void onPreExecute() {if (mListener != null) {mListener.onStart();}}@Overrideprotected String doInBackground(String... params) {synchronized (mLock) {while (mLoading) {try {mLock.wait();} catch (InterruptedException e) {e.printStackTrace();}}mLoading = true;}try {if (params.length == 1) {String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);if (TextUtils.isEmpty(skinName)) {SkinCompatResources.getInstance().reset(mStrategy);return "";}return params[0];}} catch (Exception e) {e.printStackTrace();}SkinCompatResources.getInstance().reset();return null;}@Overrideprotected void onPostExecute(String skinName) {synchronized (mLock) {// skinName 为""时,恢复默认皮肤if (skinName != null) {SkinPreference.getInstance().setSkinName(skinName).setSkinStrategy(mStrategy.getType()).commitEditor();notifyUpdateSkin();if (mListener != null) {mListener.onSuccess();}} else {SkinPreference.getInstance().setSkinName("").setSkinStrategy(SKIN_LOADER_STRATEGY_NONE).commitEditor();if (mListener != null) {mListener.onFailed("皮肤资源获取失败");}}mLoading = false;mLock.notifyAll();}}
}
doInBackground 通过皮肤包名称获取皮肤包资源,notifyUpdateSkin 通知所有 SkinCompatSupportable
对象更新皮肤
创建皮肤包
- 新建一个Application Module
- 创建对应皮肤的颜色等资源
- 资源名称和原工程一样,颜色值修改成对应皮肤包色值
- 打apk包,改为 .skin 文件,放在原工程的 assets目录下
实现效果:
浅色模式
浅色模式色值
点击深色模式按钮切换深色模式
皮肤包的色值
其他使用
- 恢复默认样式
SkinCompatManager.getInstance().restoreDefaultTheme();
- setBackgroundColor、setBackground、setTextColor等方法失效问题
因为框架是根据资源ID,如R.color.black,找到皮肤包中对应名称的资源,所以如果代码中直接设置颜色或者图片,是不支持换肤的。可以改为setBackgroundResource
- 适配Dialog
SkinXXXView 创建时会获取控件设置的颜色(textColor)、背景(background)等属性的ID,然后再根据ID加载皮肤包中对应的资源。
但是系统的Dialog的布局控件并没有设置背景属性 background等,所以默认情况下Dialog是不支持换肤的。
Skin-Support的解决方法就是替换系统默认的Dialog布局,然后再设置对应的颜色等属性,最终实现换肤效果。
在styles.xml中做如下的声明:
<item name="alertDialogStyle">@style/AlertDialog.SkinCompat</item><!-- 以下属性需要用户自行实现,并在皮肤包中提供对应值 --><item name="skinAlertDialogBackground"></item><item name="skinAlertDialogTitleTextColor"></item><item name="skinAlertDialogMessageTextColor"></item><item name="skinAlertDialogNeutralButtonTextColor"></item><item name="skinAlertDialogNegativeButtonTextColor"></item><item name="skinAlertDialogPositiveButtonTextColor"></item><item name="skinAlertDialogControlHighlightColor"></item><item name="skinAlertDialogListDivider"></item><item name="skinAlertDialogListItemTextColor"></item>
</style>
- 自定义控件换肤
Skin-Support 只提供系统组件的换肤,自定义控件需要实现 SkinCompatSupportable 接口或者直接继承 SkinXXXView,然后获取布局中属性的ID和重写 applySkin 方法。
public class SkinSwitchButton extends SwitchButton implements SkinCompatSupportable {private static final String TAG = "SkinSwitchButton";private int kswThumbDrawable;private int kswBackDrawable;public SkinSwitchButton(Context context, AttributeSet attrs,int defStyle) {super(context, attrs, defStyle);//获取SwitchButton的属性IDTypedArray ta = attrs == null ? null : context.obtainStyledAttributes(attrs, R.styleable.SwitchButton);if (ta != null) {kswThumbDrawable = ta.getResourceId(R.styleable.SwitchButton_kswThumbDrawable, INVALID_ID);kswBackDrawable = ta.getResourceId(R.styleable.SwitchButton_kswBackDrawable, INVALID_ID);ta.recycle();}applySkin();}public SkinSwitchButton(Context context, AttributeSet attrs) {this(context, attrs, 0);}public SkinSwitchButton(Context context) {this(context, null, 0);}@Overridepublic void applySkin() {//通过ID获取皮肤包对应的资源kswThumbDrawable = SkinCompatHelper.checkResourceId(kswThumbDrawable);if (kswThumbDrawable != INVALID_ID) {setThumbDrawable(SkinCompatResources.getDrawable(getContext(), kswThumbDrawable));}kswBackDrawable = SkinCompatHelper.checkResourceId(kswBackDrawable);if (kswBackDrawable != INVALID_ID) {setBackDrawable(SkinCompatResources.getDrawable(getContext(), kswBackDrawable));}}
}
动态修改颜色
Skin-Support 也支持代码中动态修改颜色,且优先级最高
//动态修改颜色
SkinCompatUserThemeManager.get().addColorState(colorId, newColor);
//动态修改图片
SkinCompatUserThemeManager.get().addDrawablePath(int drawableRes, String drawablePath);
//颜色修改完后需要调用该方法才可以生效
SkinCompatUserThemeManager.get().apply();//清除自定义的颜色和图片
SkinCompatUserThemeManager.get().clearColors();
SkinCompatUserThemeManager.get().clearDrawables();
参考:
https://www.jianshu.com/p/2c3833b8a1d2?utm_campaign=hugo
https://blog.csdn.net/c10WTiybQ1Ye3/article/details/119223672
Android 适配深色模式相关推荐
- android自动切换暗色,Android 适配深色模式的总结
Android Q 推出了深色模式,其实 Android 9 就有了,部分厂商小米,三星就在系统 Android 9 加入了深色模式的开关. Android 提供了一套夜间模式主题,继承 Theme. ...
- 实现页面适配_微信公众号文章页面适配深色模式
最近安卓微信7.0.10正式版发布,更新过后,很多用户发现,之前在测试版中对系统深色模式的适配功能被取消了,小伙伴们对此很是不满,好在Android 10系统手机用户占比很少,影响范围还不是很大,并且 ...
- Flutter适配深色模式(DarkMode)
1.瞎叨叨 也不知道写点什么,本来想写写Flutter的集成测试.因为前一阵子给flutter_deer写了一套,不过感觉也没啥内容,写不了几句话就放弃了.(其实本篇内容也不多...) 那就写写最近在 ...
- iOS13适配深色模式(Dark Mode)
原文博客地址: iOS13适配深色模式(Dark Mode) 好像大概也许是一年前, Mac OS系统发布了深色模式外观, 看着挺刺激, 时至今日用着也还挺爽的 终于, 随着iPhone11等新手机的 ...
- iOS13适配深色模式(Dark Mode)总结
iOS13适配深色模式(Dark Mode)总结 好像大概也许是一年前, Mac OS系统发布了深色模式外观, 看着挺刺激, 时至今日用着也还挺爽的 终于,随着iPhone11等新手机的发售, iOS ...
- android开发适配深色模式,手机不支持深色模式,如何用软件解决深色模式的问题?(附有系统全局深色模式实现方法...
本帖最后由 巷子口的你 于 2020-8-8 07:57 编辑 1.92允许通过设置为助手应用来饮捷切频深色模式(设置入口一般为系统默认应用-助手和语音输人, MIU需要设置为语音助手)提醒:稳定模式 ...
- Android 适配黑暗模式10.0 Q
首先刚开始 我开始使用了第三方得 Android-skin-support库 因为我的项目是databinding的,升级到最新版本后 库不支持了,所以也是抛弃了,可能是因为这个库的作者工作忙或者是没 ...
- Android切换深色模式导致布局字体变小的解决方案
切换深色模式导致布局字体变小问题困扰了我很久,一直排查自身代码问题却没发现并非自身代码导致,而是使用了今日头条屏幕适配方案AndroidAutoSize导致的,目前暂时在小米手机安卓11系统发现,切换 ...
- 开启Android Q DarkMode | 开启Android Q深色模式 夜间模式
1.首先下载Image 注意,这里最好是下载google APIs Intel x86 System Image 2.创建虚拟机,启动模拟器 如果报错HAXM 没有安装的话,请安装一下 注意,这个HA ...
- Android 适配暗黑模式
在样式中添加 <style name="MyAppTheme">.......<item name="android:forceDarkAllowed& ...
最新文章
- 安卓中运行报错Error:Execution failed for task ':app:transformClassesWithDexForDebug'解决
- python教学上机实验报告怎么写_Python基础(下)
- @override代表什么意思_混凝土中C20、HZS180都代表什么意思?
- mesos+marathon平台搭建
- 如何提高数据安全性与可用性——行云管家堡垒机
- Java多线程可以分组,还能这样玩!
- nessus重置密码
- caffe2安装篇(二) ubuntu16.04 安装方法
- php 输入汉字自动带出拼音和英文
- PHPCMS商城:模块_购物车+订单模块(资源合集)
- .NET下一种简单的调试诊断方法
- ffmpeg wav 转 mp3 以及其他音频转换
- android 微信浮窗实现_Android仿微信文章悬浮窗效果的实现代码
- mysql 1114错误_mysql – ERROR 1114(HY000):表’XXX’已满
- coldfusion_ColdFusion教程:第一部分
- 【s3.amazonaws.com】【github.com】拒绝了我们的连接请求-解决方案
- Matlab打开绘图工具
- java网络学习之 PKCS标准 X.509标准 证书等概念 的汇总(16)
- 祁隆乐凡短视频隔空宣战,和合国际收购祁隆歌曲《借我星光》版权
- App开发中适用的短信SDK