前言

产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数String,day/night,然后切换之后postInvalidate 刷新重绘.
OK,可行,但是这种方式切换白天黑夜,只是单个View中有效,那么如果哪天产品又要另一个View换肤,难道我要一个一个去写toggle么?未免太low了.

那么能不能要实现一个全app内的一键换肤?一劳永逸~~~

正文大纲
  1. 什么是一键换肤

  2. 界面上哪些东西是可以换肤的

  3. 利用HOOK技术实现优雅的“一键换肤"

  4. 相关android源码一览

5. "全app一键换肤" Demo源码详解

1、什么是一键换肤

所谓"一键",就是通过"一个"接口的调用,就能实现全app范围内的所有资源文件的替换.包括 文本,颜色,图片等。

一些换肤实现方式的对比:

方案1: 自定义View中,要换肤,那如同引言中所述,toggle方法,invalidate重绘。弊端:换肤范围仅限于这个View.

方案2:给静态变量赋值,然后重启Activity. 如果一个Activity内用静态变量定义了两种色系,那么确实是可以通过关闭Activity,再启动的方式,实现 貌似换肤的效果(其实是重新启动了Activity)弊端:太low,而且很浪费资源

也许还有其他方案吧,View重绘,重启Activity,都能实现,但是仍然不是最优雅的方案,那么,有没有一种方案,能够实现全app内的换肤效果,又不会像重启 Activity 这样浪费资源呢?请看下图:


这个动态图中,首先看到的是Activity1,点击换肤,可直接更换界面上的background,图片的src,还有textViewtextColor,跳转Activity2之后的textView颜色,在我换肤之前,和换肤之后,是不同的。换肤的过程我并没有启动另外的Activity,界面也没有闪烁。我在Activity1里面换肤,直接影响了Activity2textView字体颜色。

既然给出了效果,那么肯定要给出Demo,不然太没诚意,嘿嘿嘿。

github地址奉上:https://github.com/18598925736/HookSkinDemoFromHank

2、界面上哪些东西是可以换肤的

上面的换肤动态图,我换了ImageView,换了background,换了TextView的字体颜色,那么到底哪些东西可以换?

答案其实就一句话: 我们项目代码里面 res目录下的所有东西,几乎都可以被替换。
(为什么说几乎?因为一些犄角旮旯的东西我没有时间一个一个去试验….囧)

具体而言就是如下这些:

  • 动画

  • 背景图片

  • 字体

  • 字体颜色

  • 字体大小

  • 音频

  • 视频

3、 利用HOOK技术实现优雅的“一键换肤"

什么是hook?

如题,我是用hook实现一键换肤。那么什么是hook?
hook,钩子. 安卓中的hook技术,其实是一个抽象概念:对系统源码的代码逻辑进行"劫持",插入自己的逻辑,然后放行。注意:hook可能频繁使用java反射机制···

"一键换肤"中的hook思路

1 、"劫持"系统创建View的过程,我们自己来创建View
系统原本自己存在创建View的逻辑,我们要了解这部分代码,以便为我所用。

2、收集我们需要换肤的View(用自定义view属性来标记一个view是否支持一键换肤),保存到变量中,劫持了系统创建view的逻辑之后,我们要把支持换肤的这些view保存起来。

3、加载外部资源包,调用接口进行换肤,外部资源包是.apk后缀的一个文件,是通过gradle打包形成的。里面包含需要换肤的资源文件,但是必须保证,要换的资源文件,和原工程里面的文件名完全相同

4、 相关android源码一览

1、Activity 的 setContentView(R.layout.XXX) 到底在做什么?

回顾我们写app的习惯,创建Activity,写xxx.xml,在Activity里面setContentView(R.layout.xxx) 。 我们写的是xml,最终呈现出来的是一个一个的界面上的UI控件,那么setContentView到底做了什么事,使得XML里面的内容,变成了UI控件呢?

请看下图:

源码索引:setContentView(R.layout.activity_main);  ->getDelegate().setContentView(layoutResID);

OK,这里暴露出了两个方法,getDelegate()setContentView()

先看getDelegate:

这里返回了一个AppCompatDelegate对象,跟踪到AppCompatDelegate内部,阅读源码,可以得出一个结论:AppCompatDelegate 是替Activity生成View对象的委托类,它提供了一系列setContentView方法,在Activity中加入UI控件。

2、那它的AppCompatDelegatesetContentView方法又做了什么?

找到setContentView的具体过程:

那么就进入下一个环节:LayoutInflater又做了什么?

LayoutInflater这个类是怎么把layout.xml的 变成TextView对象的?

我们知道,我们传入的是int,是xxx.xml这个布局文件,在R文件里面的对应int值。LayoutInflater拿到了这个int之后,又干了什么事呢?

一路索引进去:会发现这个方法:

发现一个关键方法:CreateViewFromTagtag是指的什么?其实就是 xml里面 的标签头:里的TextView

跟踪进去:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {        if (name.equals("view")) {            name = attrs.getAttributeValue(null, "class");        }

        // Apply a theme wrapper, if allowed and one is specified.        if (!ignoreThemeAttr) {            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);            final int themeResId = ta.getResourceId(0, 0);            if (themeResId != 0) {                context = new ContextThemeWrapper(context, themeResId);            }            ta.recycle();        }

        if (name.equals(TAG_1995)) {            // Let's party like it's 1995!            return new BlinkLayout(context, attrs);        }

        try {            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;            }

            if (view == null && mPrivateFactory != null) {                view = mPrivateFactory.onCreateView(parent, name, context, attrs);            }

            if (view == null) {                final Object lastContext = mConstructorArgs[0];                mConstructorArgs[0] = context;                try {                    if (-1 == name.indexOf('.')) {                        view = onCreateView(parent, name, attrs);                    } else {                        view = createView(name, null, attrs);                    }                } finally {                    mConstructorArgs[0] = lastContext;                }            }

            return view;        } catch (InflateException e) {            throw e;

        } catch (ClassNotFoundException e) {            final InflateException ie = new InflateException(attrs.getPositionDescription()                    + ": Error inflating class " + name, e);            ie.setStackTrace(EMPTY_STACK_TRACE);            throw ie;

        } catch (Exception e) {            final InflateException ie = new InflateException(attrs.getPositionDescription()                    + ": Error inflating class " + name, e);            ie.setStackTrace(EMPTY_STACK_TRACE);            throw ie;        }    }

这个方法有5个参数,意义分别是:

  • View parent 父组件

  • String name  xml标签名

  • Context context   上下文

  • AttributeSet attrs view属性

  • boolean ignoreThemeAttr 是否忽略theme属性

并且在这里,发现一段关键代码:

 if (mFactory2 != null) {                view = mFactory2.onCreateView(parent, name, context, attrs);            } else if (mFactory != null) {                view = mFactory.onCreateView(name, context, attrs);            } else {                view = null;            }

实际上,可能有人要问了,你怎么知道这边是走的哪一个if分支呢?
方法:新创建一个Project,跟踪MainActivity onCreate里面setContentView()一路找到这段代码debug你会发现:

答案很明确了,系统在默认情况下就会走Factory2onCreateView(),应该有人好奇:这个mFactory2对象是哪来的?是什么时候set进去的,答案如下:

这时,getDelegate()得到的对象,和 LayoutInflater里面mFactory2其实是同一个对象。

那么继续跟踪,一直到:AppCompatViewInflater 类:

这边利用了大量的switch case来进行系统控件的创建,例如:TextView

@NonNull    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {        return new AppCompatTextView(context, attrs);    }

都是new 出来一个具有兼容特性的TextView,返回出去。
但是,使用过switch的人都知道,这种case形式的分支,无法涵盖所有的类型怎么办呢?这里switch之后,view仍然可能是null。所以,switch之后,谷歌大佬加了一个if,但是很诡异,这段代码并未进入if,因为  originalContext != context并不满足….具体原因我也没查出来,(;´д`)ゞ

       if (view == null && originalContext != context) {            // If the original context does not equal our themed context, then we need to manually            // inflate it using the name so that android:theme takes effect.            view = createViewFromTag(context, name, attrs);        }

然而,这里的补救措施没有执行,那自然有地方有另外的补救措施,回到之前的LayoutInflater的下面这段代码:

 if (mFactory2 != null) {                view = mFactory2.onCreateView(parent, name, context, attrs);            } else if (mFactory != null) {                view = mFactory.onCreateView(name, context, attrs);            } else {                view = null;            }

这段代码的下面,如果view是空,补救措施如下:

 if (view == null) {                final Object lastContext = mConstructorArgs[0];                mConstructorArgs[0] = context;                try {                    if (-1 == name.indexOf('.')) {//包含.说明这不是权限定名的类名                        view = onCreateView(parent, name, attrs);                    } else {//权限定名走这里                        view = createView(name, null, attrs);                    }                } finally {                    mConstructorArgs[0] = lastContext;                }            }

这里的两个方法onCreateView(parent, name, attrs)createView(name, null, attrs);都最终索引到:

这么一大段好像有点让人害怕。其实真正需要关注的,就是反射的代码,最后的newInstance()
OK,Activity上那些丰富多彩的View的来源,就说到这里。

4、app中资源文件大管家 Resources / AssetManager 是怎么工作的

从我们的终极目的出发:我们要做的是“换肤”,如果我们拿到了要换肤的View,可以对他们进行setXXX属性来改变UI,那么属性值从哪里来?
界面元素丰富多彩,但是这些View,都是用资源文件来进行 "装扮"出来的,资源文件大致可以分为:
图片,文字,颜色,声音视频,字体等。如果我们控制了资源文件,那么是不是有能力对界面元素进行set某某属性来进行“再装扮”呢? 当然,这是可行的。因为,我们平时拿到一个TextView,就能对它进行setTextColor,这种操作,在view还存活的时候,都可以进行操作,并且这种操作,并不会造成Activity的重启。
这些资源文件,有一个统一的大管家。可能有人说是R.java文件,它里面统筹了所有的资源文件int值.没错,但是这个R文件是如何产生作用的呢? 答案:Resources.

一张图说明一切:

5、 "全app一键换肤" Demo源码详解(戳这里获得源码)

项目工程结构:

关键类 SkinFactory

SkinFactory类, 继承LayoutInflater.Factory2 ,它的实例,会负责创建View,收集 支持换肤的view

public class SkinFactory implements LayoutInflater.Factory2 {

    private AppCompatDelegate mDelegate;//预定义一个委托类,它负责按照系统的原有逻辑来创建view

    private List listCacheSkinView = new ArrayList<>();//我自定义的list,缓存所有可以换肤的View对象/**     * 给外部提供一个set方法     *     * @param mDelegate     */public void setDelegate(AppCompatDelegate mDelegate) {this.mDelegate = mDelegate;    }/**     * Factory2 是继承Factory的,所以,我们这次是主要重写Factory的onCreateView逻辑,就不必理会Factory的重写方法了     *     * @param name     * @param context     * @param attrs     * @return     */@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;    }/**     * @param parent     * @param name     * @param context     * @param attrs     * @return     */@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {// TODO: 关键点1:执行系统代码里的创建View的过程,我们只是想加入自己的思想,并不是要全盘接管        View view = mDelegate.createView(parent, name, context, attrs);//系统创建出来的时候有可能为空,你问为啥?请全文搜索 “标记标记,因为” 你会找到你要的答案if (view == null) {//万一系统创建出来是空,那么我们来补救try {if (-1 == name.indexOf('.')) {//不包含. 说明不带包名,那么我们帮他加上包名                    view = createViewByPrefix(context, name, prefixs, attrs);                } else {//包含. 说明 是权限定名的view name,                    view = createViewByPrefix(context, name, null, attrs);                }            } catch (Exception e) {                e.printStackTrace();            }        }//TODO: 关键点2 收集需要换肤的View        collectSkinView(context, attrs, view);return view;    }/**     * TODO: 收集需要换肤的控件     * 收集的方式是:通过自定义属性isSupport,从创建出来的很多View中,找到支持换肤的那些,保存到map中     */private void collectSkinView(Context context, AttributeSet attrs, View view) {// 获取我们自己定义的属性        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Skinable);boolean isSupport = a.getBoolean(R.styleable.Skinable_isSupport, false);if (isSupport) {//找到支持换肤的viewfinal int Len = attrs.getAttributeCount();            HashMap attrMap = new HashMap<>();for (int i = 0; i //遍历所有属性                String attrName = attrs.getAttributeName(i);                String attrValue = attrs.getAttributeValue(i);                attrMap.put(attrName, attrValue);//全部存起来            }            SkinView skinView = new SkinView();            skinView.view = view;            skinView.attrsMap = attrMap;            listCacheSkinView.add(skinView);//将可换肤的view,放到listCacheSkinView中        }    }/**     * 公开给外界的换肤入口     */public void changeSkin() {for (SkinView skinView : listCacheSkinView) {            skinView.changeSkin();        }    }static class SkinView {        View view;        HashMap attrsMap;/**         * 真正的换肤操作         */public void changeSkin() {if (!TextUtils.isEmpty(attrsMap.get("background"))) {//属性名,例如,这个background,text,textColor....int bgId = Integer.parseInt(attrsMap.get("background").substring(1));//属性值,R.id.XXX ,int类型,// 这个值,在app的一次运行中,不会发生变化                String attrType = view.getResources().getResourceTypeName(bgId); // 属性类别:比如 drawable ,colorif (TextUtils.equals(attrType, "drawable")) {//区分drawable和color                    view.setBackgroundDrawable(SkinEngine.getInstance().getDrawable(bgId));//加载外部资源管理器,拿到外部资源的drawable                } else if (TextUtils.equals(attrType, "color")) {                    view.setBackgroundColor(SkinEngine.getInstance().getColor(bgId));                }            }if (view instanceof TextView) {if (!TextUtils.isEmpty(attrsMap.get("textColor"))) {int textColorId = Integer.parseInt(attrsMap.get("textColor").substring(1));                    ((TextView) view).setTextColor(SkinEngine.getInstance().getColor(textColorId));                }            }//那么如果是自定义组件呢if (view instanceof ZeroView) {//那么这样一个对象,要换肤,就要写针对性的方法了,每一个控件需要用什么样的方式去换,尤其是那种,自定义的属性,怎么去set,// 这就对开发人员要求比较高了,而且这个换肤接口还要暴露给 自定义View的开发人员,他们去定义// ....            }        }    }/**     * 所谓hook,要懂源码,懂了之后再劫持系统逻辑,加入自己的逻辑。     * 那么,既然懂了,系统的有些代码,直接拿过来用,也无可厚非。     *///*******************************下面一大片,都是从源码里面抄过来的,并不是我自主设计******************************// 你问我抄的哪里的?到 AppCompatViewInflater类源码里面去搜索:view = createViewFromTag(context, name, attrs);static final Class>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};//final Object[] mConstructorArgs = new Object[2];//View的构造函数的2个"实"参对象private static final HashMap> sConstructorMap = new HashMap>();//用映射,将View的反射构造函数都存起来static final String[] prefixs = new String[]{//安卓里面控件的包名,就这么3种,这个变量是为了下面代码里,反射创建类的class而预备的"android.widget.","android.view.","android.webkit."    };/**     * 反射创建View     *     * @param context     * @param name     * @param prefixs     * @param attrs     * @return     */private final View createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs) {        Constructor extends View> constructor = sConstructorMap.get(name);        Class extends View> clazz = null;if (constructor == null) {try {if (prefixs != null && prefixs.length > 0) {for (String prefix : prefixs) {                        clazz = context.getClassLoader().loadClass(                                prefix != null ? (prefix + name) : name).asSubclass(View.class);//控件if (clazz != null) break;                    }                } else {if (clazz == null) {                        clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);                    }                }if (clazz == null) {return null;                }                constructor = clazz.getConstructor(mConstructorSignature);//拿到 构造方法,            } catch (Exception e) {                e.printStackTrace();return null;            }            constructor.setAccessible(true);//            sConstructorMap.put(name, constructor);//然后缓存起来,下次再用,就直接从内存中去取        }        Object[] args = mConstructorArgs;        args[1] = attrs;try {//通过反射创建View对象final View view = constructor.newInstance(args);//执行构造函数,拿到View对象return view;        } catch (Exception e) {            e.printStackTrace();        }return null;    }//**********************************************************************************************}

关键类 SkinEngine

public class SkinEngine {

    //单例    private final static SkinEngine instance = new SkinEngine();

    public static SkinEngine getInstance() {        return instance;    }

    private SkinEngine() {    }

    public void init(Context context) {        mContext = context.getApplicationContext();        //使用application的目的是,如果万一传进来的是Activity对象        //那么它被静态对象instance所持有,这个Activity就无法释放了    }

    private Resources mOutResource;// TODO: 资源管理器    private Context mContext;//上下文    private String mOutPkgName;// TODO: 外部资源包的packageName

    /**     * TODO: 加载外部资源包     */    public void load(final String path) {//path 是外部传入的apk文件名        File file = new File(path);        if (!file.exists()) {            return;        }        //取得PackageManager引用        PackageManager mPm = mContext.getPackageManager();        //“检索在包归档文件中定义的应用程序包的总体信息”,说人话,外界传入了一个apk的文件路径,这个方法,拿到这个apk的包信息,这个包信息包含什么?        PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);        mOutPkgName = mInfo.packageName;//先把包名存起来        AssetManager assetManager;//资源管理器        try {            //TODO: 关键技术点3 通过反射获取AssetManager 用来加载外面的资源包            assetManager = AssetManager.class.newInstance();//反射创建AssetManager对象,为何要反射?使用反射,是因为他这个类内部的addAssetPath方法是hide状态            //addAssetPath方法可以加载外部的资源包            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用            addAssetPath.invoke(assetManager, path);//反射执行方法            mOutResource = new Resources(assetManager,//参数1,资源管理器                    mContext.getResources().getDisplayMetrics(),//这个好像是屏幕参数                    mContext.getResources().getConfiguration());//资源配置            //最终创建出一个 "外部资源包"mOutResource ,它的存在,就是要让我们的app有能力加载外部的资源文件        } catch (Exception e) {            e.printStackTrace();        }

    }

    /**     * 提供外部资源包里面的颜色     * @param resId     * @return     */    public int getColor(int resId) {        if (mOutResource == null) {            return resId;        }        String resName = mOutResource.getResourceEntryName(resId);        int outResId = mOutResource.getIdentifier(resName, "color", mOutPkgName);        if (outResId == 0) {            return resId;        }        return mOutResource.getColor(outResId);    }

    /**     * 提供外部资源包里的图片资源     * @param resId     * @return     */    public Drawable getDrawable(int resId) {//获取图片        if (mOutResource == null) {            return ContextCompat.getDrawable(mContext, resId);        }        String resName = mOutResource.getResourceEntryName(resId);        int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);        if (outResId == 0) {            return ContextCompat.getDrawable(mContext, resId);        }        return mOutResource.getDrawable(outResId);    }

    //..... 这里还可以提供外部资源包里的String,font等等等,只不过要手动写代码来实现getXX方法}

关键类的调用方式:

1、 初始化"换肤引擎"

public class MyApp extends Application {

    @Override    public void onCreate() {        super.onCreate();        //初始化换肤引擎        SkinEngine.getInstance().init(this);    }}

2、劫持 系统创建view的过程

public class BaseActivity extends AppCompatActivity {

    ...

    @Override    protected void onCreate(Bundle savedInstanceState) {        // TODO: 关键点1:hook(劫持)系统创建view的过程        if (ifAllowChangeSkin) {            mSkinFactory = new SkinFactory();            mSkinFactory.setDelegate(getDelegate());            LayoutInflater layoutInflater = LayoutInflater.from(this);            layoutInflater.setFactory2(mSkinFactory);//劫持系统源码逻辑        }        super.onCreate(savedInstanceState);    }

3、 执行换肤操作

protected void changeSkin(String path) {        if (ifAllowChangeSkin) {            File skinFile = new File(Environment.getExternalStorageDirectory(), path);            SkinEngine.getInstance().load(skinFile.getAbsolutePath());//加载外部资源包            mSkinFactory.changeSkin();//执行换肤操作            mCurrentSkin = path;        }    }

效果展示:


注意事项:

1、 皮肤包skin_plugin module,里面,只提供需要换肤的资源即可,不需要换肤的资源,还有src目录下的源码
(只是删掉java源码文件,不要删目录结构啊….(●´∀`●)),不要放在这里,无端增大皮肤包的体积.

2、 皮肤包 skin_plugin modulegradle sdk版本最好和app module的保持完全一致,否则无法保证不会出现奇葩问题.

3、 用皮肤包skin_plugin module打包生成的apk文件,常规来说,是放在手机内存里面,然后由app module内的代码去加载。至于是手机内存里面的哪个位置,那就见仁见智了. 我是使用的mumu模拟器,我放在了最外层的根目录下面,然后读取这个位置的代码是:File skinFile = new File(Environment.getExternalStorageDirectory(), "skin.apk");

4、上图中,打了两个皮肤包,要注意:打两个皮肤包运行demo,打之前,一定要记得替换drawable图片资源为同名文件,以及

不然切换没有效果。

结语

hook技术是安卓高级层次的技能,学起来并不简单,demo里面的注释我自认为写的很清楚了,如果还有不懂的,欢迎留言评论。读源码也并不是这么轻松的事,可是还是那句话,太简单的东西,不值钱,有高难度才有高回报。为了百万年薪,fighting!

作者:波澜步惊
链接:https://www.jianshu.com/p/4c8d46f58c4f
本文经作者授权推送。

---完---

阅读推荐:

反对996的人,就是对于社会价值创造理解不够彻底?

Android百度地图轨迹回放

Android开发一年,你是不是还做着拖拽改样的活?

 2019 随手点好看 年薪上百万!

opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤相关推荐

  1. opengl源码 实现无缝切换图片过场_OpenGL学习笔记(六)变换

    本文为学习LearnOpenGL的学习笔记,如有书写和理解错误还请大佬扶正: 教程链接: https://learnopengl-cn.github.io/01%20Getting%20started ...

  2. 一款社区论坛小程序源码(修复登录图片发布上传问题)

    简介: 这是一款社区论坛小程序源码(修复登录图片发布上传问题) 内涵强大的功能 支持多种多样的发帖模式 比如发图文,发语音,发涂鸦,发视频等 另外也可以设置为只能会员才可以发 另外还拥有礼物功能,可以 ...

  3. 仿抖音短视频系统源码,获取系统图片

    仿抖音短视频系统源码,实现获取系统图片的相关代码如下: 首先开权限 <uses-permission android:name="android.permission.WRITE_EX ...

  4. 小程序源码:多功能图片处理器

    这是一款多功能的一款照片处理器 UI简洁,功能也还不错 免除服务器和域名即可搭建,特别的简单好上手 一键化功能支持: 人脸融合(人脸融合,两张脸融合成一张) 换底色(相当于就是给照片的底色换色,一般都 ...

  5. v42.05 鸿蒙内核源码分析(中断切换) | 系统因中断活力四射 | 百篇博客分析鸿蒙源码

    子曰:"知者不惑,仁者不忧,勇者不惧." <论语>:子罕篇 百篇博客系列篇.本篇为: v42.xx 鸿蒙内核源码分析(中断切换篇) | 系统因中断活力四射 硬件架构相关 ...

  6. 单机网页游戏的如何修改服务器数据库,页游源码【神创天下】单机版GM修改+人物数据修改教程+一键启动客户端服务端...

    页游源码[神创天下]单机版GM修改+人物数据修改教程+一键启动客户端服务端_站长下载 安装说明: 1.本资源为页游单机版一键启动服务端.默认单机架设,无需修改IP. 2.此资源原始版本为刀剑无双,为刀 ...

  7. java源码系列:HashMap底层存储原理详解——4、技术本质-原理过程-算法-取模具体解决什么问题

    目录 简介 取模具体解决什么问题? 通过数组特性,推导ascii码计算出来的下标值,创建数组非常占用空间 取模,可保证下标,在HashMap默认创建下标之内 简介 上一篇文章,我们讲到 哈希算法.哈希 ...

  8. TinkPHP内核仿每推推51领啦试客源码_PC源码+WAP端+APP原生代码_自带5套精美模板

    TinkPHP内核仿每推推51领啦试客源码_PC源码+WAP端+APP原生代码_自带5套精美模板 源码说明:TinkPHP内核上制作而成,是全国领先的免费试用网站!程序全开源无加密!带有wap手机端, ...

  9. 婚恋交友源码开发,实现图片的滑动切换

    前言 我们开始来介绍实现婚恋交友源码转换类的动画组件,实际上这类转换动画组件也可以自己通过 AnimatedBuilder 或 AnimatedWidget 完成, Flutter 为了简化开发,提供 ...

最新文章

  1. 维护人员的VMware日常工作
  2. Java 条件编译 Conditional Compilation
  3. 006——php字符串中的处理函数(五)
  4. Oracle 游标的练习
  5. echart 高度 不用 不撑满_注意厨房台面高度及细节 装出省心舒服 装出事半功倍...
  6. code dairy
  7. 爱奇艺取消剧集超前点播
  8. java 上传文件接口_Java接口实现文件上传
  9. 到底什么是前后端分离
  10. Apache和Apache Tomcat的区别是什么?
  11. jpress-项目升级
  12. python与分形0004 - 带刺的圆
  13. Android 画布使用之电子签名
  14. 使用FFMPEG制作gif图片
  15. MSSQL 如何删除字段的所有约束和索引
  16. 致远的OA软件有什么特点?
  17. jquery的$是什么意思与相关的
  18. 第四章 治病法要(2)
  19. Winpcap的安装使用方法和问题总结
  20. python通信工程定额_通信建设工程预算定额

热门文章

  1. linux命令之添加系统搜索动态库目录-ldconfig
  2. 基于深度学习的人脸检测与静默活体检测——C++实现
  3. 数字图像处理实验(10):PROJECT 05-01 [Multiple Uses],Noise Generators
  4. redis哨兵集群数据迁移_redis集群数据迁移—redis-migrate-tool神器
  5. 计算音频数据音量_【翻译】线性的音量推子……简直像一个个秤砣!
  6. python3 读取文本文件_python3文件的读写操作
  7. weka: naive bayes
  8. Spring MVC源码解析
  9. MyBatis的运行的核心原理解析(三)
  10. ArcGIS中QueryTask,FindTask,IndentifyTask 之间的区别