毕业5年了还不知道Android热修复?

cc608a167cca0314d4c36749073bfa08.jpeg

/   今日科技快讯   /

近日,谷歌抢在微软之前发布了ChatGPT竞品、自家人工智能聊天机器人Bard,但市场反应并不好。谷歌员工们纷纷批评包括首席执行官桑德尔·皮查伊在内的公司领导层,认为公司本周宣布推出Bard的方式过于“仓促”、简直是“一团糟”。

/   作者简介   /

本篇文章转自小余的自习室的博客,文章主要分享了热修复相关的知识,相信会对大家有所帮助!

原文地址:

https://juejin.cn/post/7142481619604111390

/   前言   /

热修复到现在2022年已经不是一个新名词,但是作为Android开发核心技术栈的一部分,我这里还得来一次冷饭热炒。

随着移动端业务复杂程度的增加,传统的版本更新流程显然无法满足业务和开发者的需求,热修复技术的推出在很大程度上改善了这一局面。国内大部分成熟的主流App都拥有自己的热更新技术,像手淘、支付宝、微信、QQ、饿了么、美团等。

可以说,一个好的热修复技术,将为你的App助力百倍。对于每一个想在Android开发领域有所造诣的开发者,掌握热修复技术更是必备的素质。

热修复是Android大厂面试中高频面试知识点,也是我们必须要掌握的知识点。热修复技术,可以看作Android平台发展成熟至一定阶段的必然产物。Android热修复了解吗?修复哪些东西?常见热修复框架对比以及各原理分析?

/   什么是热修复   /

热修复说白了就是不再使用传统的应用商店更新或者自更新方式,使用补丁包推送的方式在用户无感知的情况下,修复应用bug或者推送新的需求。

传统更新和热更新过程对比如下:

469f45c83897e53dd88bb8b09ac1bd8f.png

热修复优缺点。

优点

  1. 只需要打补丁包,不需要重新发版本。

  2. 用户无感知,不需要重新下载最新应用

  3. 修复成功率高

缺点

补丁包滥用,容易导致应用版本不可控,需要开发一套完整的补丁包更新机制,会增加一定的成本。

/   热修复方案   /

首先我们得知道热修复修复哪些东西?

  1. 代码修复

  2. 资源修复

  3. 动态库修复


代码修复方案

从技术角度来说,我们的目的是非常明确的,把错误的代码替换成正确的代码。注意这里的替换,并不是直接擦写dx文件,而是提供一份新的正确代码,让应用运行时绕过错误代码,执行新的正确代码。

8c27fe55db223a6e32c1d2531376a51a.png

想法简单直接,但实现起来并不容易。目前主要有三类技术方案:


类加载方案

之前分析类加载机制有说过,加载流程先是遵循双亲委派原则,如果委派原则没有找到此前加载过此类,则会调用CLassLoader的findClass方法,再去BaseDexClassLoader下面的dexElements数组中查找,如果没有找到,最终调用defineClassNative方法加载。

代码修复就是基于这点,将新的做了修复的dex文件,通过反射注入到BaseDexClassLoader的dexElements数组的第一个位置上dexElements[0],下次重新启动应用加载类的时候,会优先加载做了修复的dex文件,这样就达到了修复代码的目的。原理很简单,代码如下:

public class Hotfix {public static void patch(Context context, String patchDexFile, String patchClassName)throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {//获取系统PathClassLoader的"dexElements"属性值PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();Object origDexElements = getDexElements(pathClassLoader);//新建DexClassLoader并获取“dexElements”属性值String otpDir = context.getDir("dex", 0).getAbsolutePath();Log.i("hotfix", "otpdir=" + otpDir);DexClassLoader nDexClassLoader = new DexClassLoader(patchDexFile, otpDir, patchDexFile, context.getClassLoader());Object patchDexElements = getDexElements(nDexClassLoader);//将patchDexElements插入原origDexElements前面Object allDexElements = combineArray(origDexElements, patchDexElements);//将新的allDexElements重新设置回pathClassLoadersetDexElements(pathClassLoader, allDexElements);//重新加载类pathClassLoader.loadClass(patchClassName);
}
private static Object getDexElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {//首先获取ClassLoader的“pathList”实例Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");pathListField.setAccessible(true);//设置为可访问Object pathList = pathListField.get(classLoader);//然后获取“pathList”实例的“dexElements”属性Field dexElementField = pathList.getClass().getDeclaredField("dexElements");dexElementField.setAccessible(true);//读取"dexElements"的值Object elements = dexElementField.get(pathList);return elements;}//合拼dexElementsprivate static Object combineArray(Object obj, Object obj2) {Class componentType = obj2.getClass().getComponentType();//读取obj长度int length = Array.getLength(obj);//读取obj2长度int length2 = Array.getLength(obj2);Log.i("hotfix", "length=" + length + ",length2=" + length2);//创建一个新Array实例,长度为ojb和obj2之和Object newInstance = Array.newInstance(componentType, length + length2);for (int i = 0; i < length + length2; i++) {//把obj2元素插入前面if (i < length2) {Array.set(newInstance, i, Array.get(obj2, i));} else {//把obj元素依次放在后面Array.set(newInstance, i, Array.get(obj, i - length2));}}//返回新的Array实例return newInstance;}private static void setDexElements(ClassLoader classLoader, Object dexElements) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {//首先获取ClassLoader的“pathList”实例Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");pathListField.setAccessible(true);//设置为可访问Object pathList = pathListField.get(classLoader);//然后获取“pathList”实例的“dexElements”属性Field declaredField = pathList.getClass().getDeclaredField("dexElements");declaredField.setAccessible(true);//设置"dexElements"的值declaredField.set(pathList, dexElements);}
}

类加载过程如下:

3b0b96ec401011864a4974e33ff1f7e1.png

微信Tinker,QQ空间的超级补丁、手QQ的QFix 、饿了么的Amigo和Nuwa等都是使用这个方式。

缺点:因为类加载后无法卸载,所以类加载方案必须重启App,让bug类重新加载后才能生效。

底层替换方案

底层替换方案不会再次加载新类,而是直接在Native层修改原有类, 这里我们需要提到Art虚拟机中ArtMethod。每一个Java方法在Art虚拟机中都对应着一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等。

结构如下:

// art/runtime/art_method.h
class ArtMethod FINAL {
...protected:GcRoot<mirror::Class> declaring_class_;GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;uint32_t access_flags_;uint32_t dex_code_item_offset_;uint32_t dex_method_index_;uint32_t method_index_;struct PACKED(4) PtrSizedFields {void* entry_point_from_interpreter_;      // 1void* entry_point_from_jni_;void* entry_point_from_quick_compiled_code_;  //2} ptr_sized_fields_;...
}

在 ArtMethod结构体中,最重要的就是 注释1和注释2标注的内容,从名字可以看出来,他们就是方法的执行入口。我们知道,Java代码在Android中会被编译为Dex Code。

Art虚拟机中可以采用解释模式或者AOT机器码模式执行Dex Code。

  • 解释模式:就是去除Dex Code,逐条解释执行。如果方法的调用者是以解释模式运行的,在调用这个方法时,就会获取这个方法的entry_point_from_interpreter_,然后跳转执行

  • AOT模式:就会预先编译好Dex Code对应的机器码,然后在运行期直接执行机器码,不需要逐条解释执行Dex Code。如果方法的调用者是以AOT机器码方式执行的,在调用这个方法时,就是跳转到entry_point_from_quick_compiled_code_中执行

那是不是只需要替换这个几个entry_point_*入口地址就能够实现方法替换了呢?并没有那么简单,因为不论是解释模式还是AOT模式,在运行期间还会需要调用ArtMethod中的其他成员字段。

AndFix采用的是改变指针指向:

// AndFix/jni/art/art_method_replace_6_0.cpp
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {art::mirror::ArtMethod* smeth =(art::mirror::ArtMethod*) env->FromReflectedMethod(src);  // 1art::mirror::ArtMethod* dmeth =(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);  // 2...// 3smeth->declaring_class_ = dmeth->declaring_class_;smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;smeth->access_flags_ = dmeth->access_flags_ | 0x0001;smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;smeth->dex_method_index_ = dmeth->dex_method_index_;smeth->method_index_ = dmeth->method_index_;smeth->ptr_sized_fields_.entry_point_from_interpreter_ =dmeth->ptr_sized_fields_.entry_point_from_interpreter_;smeth->ptr_sized_fields_.entry_point_from_jni_ =dmeth->ptr_sized_fields_.entry_point_from_jni_;smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;LOGD("replace_6_0: %d , %d",smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}

缺点:存在一些兼容性问题,由于ArtMethod结构体是Android开源的一部分,所以每个手机厂商都可能会去更改这部分的内容,这就可能导致ArtMethod替换方案在某些机型上面出现未知错误。

Sophix为了规避上面的AndFix的风险,采用直接替换整个结构体。这样不管手机厂商如何更改系统,我们都可以正确定位到方法地址

install run方案

Instant Run方案的核心思想是——插桩,在编译时通过插桩在每一个方法中插入代码,修改代码逻辑,在需要时绕过错误方法,调用patch类的正确方法。

首先,在编译时Instant Run为每个类插入IncrementalChange变量。

IncrementalChange  $change;

为每一个方法添加类似如下代码:

public void onCreate(Bundle savedInstanceState) {IncrementalChange var2 = $change;//$change不为null,表示该类有修改,需要重定向if(var2 != null) {//通过access$dispatch方法跳转到patch类的正确方法var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});} else {super.onCreate(savedInstanceState);this.setContentView(2130968601);this.tv = (TextView)this.findViewById(2131492944);}
}

如上代码,当一个类被修改后,Instant Run会为这个类新建一个类,命名为xxx&override,且实现IncrementalChange接口,并且赋值给原类的$change变量。

public class MainActivity$override implements IncrementalChange {
}

此时,在运行时原类中每个方法的var2 != null,通过accessdispatch(参数是方法名和原参数)定位到patch类MainActivitydispatch(参数是方法名和原参数)定位到patch类MainActivityoverride中修改后的方法。

Instant Run是google在AS2.0时用来实现“热部署”的,同时也为“热修复”提供了一个绝佳的思路。美团的Robust就是基于此。

资源修复方案

这里我们来看看install run的原理即可,市面上的常见修复方案大部分都是基于此方法。

public static void monkeyPatchExistingResources(Context context,String externalResourceFile, Collection<Activity> activities) {if (externalResourceFile == null) {return;}try {
// 创建一个新的AssetManagerAssetManager newAssetManager = (AssetManager) AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]); // ... 1Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[] { String.class }); // ... 2mAddAssetPath.setAccessible(true);
// 通过反射调用addAssetPath方法加载外部的资源(SD卡资源)if (((Integer) mAddAssetPath.invoke(newAssetManager,new Object[] { externalResourceFile })).intValue() == 0) { // ... 3throw new IllegalStateException("Could not create new AssetManager");}Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);mEnsureStringBlocks.setAccessible(true);mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);if (activities != null) {for (Activity activity : activities) {Resources resources = activity.getResources(); // ... 4try { 
// 反射得到Resources的AssetManager类型的mAssets字段Field mAssets = Resources.class.getDeclaredField("mAssets"); // ... 5mAssets.setAccessible(true);
// 将mAssets字段的引用替换为新创建的newAssetManagermAssets.set(resources, newAssetManager); // ... 6} catch (Throwable ignore) {...}// 得到Activity的Resources.ThemeResources.Theme theme = activity.getTheme();try {try {
// 反射得到Resources.Theme的mAssets字段Field ma = Resources.Theme.class.getDeclaredField("mAssets");ma.setAccessible(true);
// 将Resources.Theme的mAssets字段的引用替换为新创建的newAssetManagerma.set(theme, newAssetManager); // ... 7} catch (NoSuchFieldException ignore) {...}...} catch (Throwable e) {Log.e("InstantRun","Failed to update existing theme for activity "+ activity, e);}pruneResourceCaches(resources);}}
/**
*  根据SDK版本的不同,用不同的方式得到Resources 的弱引用集合
*/ Collection<WeakReference<Resources>> references;if (Build.VERSION.SDK_INT >= 19) {Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);mGetInstance.setAccessible(true);Object resourcesManager = mGetInstance.invoke(null,new Object[0]);try {Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");fMActiveResources.setAccessible(true);ArrayMap<?, WeakReference<Resources>> arrayMap = (ArrayMap) fMActiveResources.get(resourcesManager);references = arrayMap.values();} catch (NoSuchFieldException ignore) {Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");mResourceReferences.setAccessible(true);references = (Collection) mResourceReferences.get(resourcesManager);}} else {Class<?> activityThread = Class.forName("android.app.ActivityThread");Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");fMActiveResources.setAccessible(true);Object thread = getActivityThread(context, activityThread);HashMap<?, WeakReference<Resources>> map = (HashMap) fMActiveResources.get(thread);references = map.values();}
//遍历并得到弱引用集合中的 Resources ,将 Resources mAssets 字段引用替换成新的 AssetManagerfor (WeakReference<Resources> wr : references) {Resources resources = (Resources) wr.get();if (resources != null) {try {Field mAssets = Resources.class.getDeclaredField("mAssets");mAssets.setAccessible(true);mAssets.set(resources, newAssetManager);} catch (Throwable ignore) {...}resources.updateConfiguration(resources.getConfiguration(),resources.getDisplayMetrics());}}} catch (Throwable e) {throw new IllegalStateException(e);}
}
  • 在注释1处创建一个新的AssetManager

  • 在注释2和注释3处通过反射调用 addAssetPath 方法加载外部(SD 卡)的资源。

  • 在注释4处遍历Activity列表,得到每个Activity的Resources

  • 在注释5处通过反射得到Resources的AssetManager类型的rnAssets字段

  • 注释6处改写mAssets字段的引用为新的AssetManager

采用同样的方式:

  • 在注释7处将Resources.Theme的mAssets字段的引用替换为新创建的 AssetManager 。

  • 紧接着根据SDK版本的不同,用不同的方式得到Resources的弱引用集合

  • 再遍历这个弱引用集合,将弱引用集合中的Resources的mAssets字段引用都替换成新创建的AssetManager 。

资源修复原理:

  • 创建新的AssetManager,通过反射调用addAssetPath方法,加载外部资源,这样新创建的AssetManager就含有了外部资源

  • 将AssetManager类型的mAsset字段全部用新创建的AssetManager对象替换。这样下次加载资源文件的时候就可以找到包含外部资源文件的AssetManager。


动态链接库so的修复

接口调用替换方案

sdk提供接口替换System默认加载so库接口。

SOPatchManager.loadLibrary(String libName) -> System.loadLibrary(String libName)

SOPatchManager.loadLibrary接口加载so库的时候优先尝试去加载sdk指定目录下的补丁so,加载策略如下:

如果存在则加载补丁so库而不会去加载安装apk安装目录下的so库。如果不存在补丁so,那么调用System.loadLibrary去加载安装apk目录下的so库。

fa59e4b9c4f8a9ee97d02432ebdaadd3.png

我们可以很清楚的看到这个方案的优缺点。优点是不需要对不同sdk版本进行兼容,因为所有的sdk版本都有System.loadLibrary这个接口。缺点是调用方需要替换掉System默认加载so库接口为sdk提供的接口,如果是已经编译混淆好的三方库的so库需要patch,那么是很难做到接口的替换。

虽然这种方案实现简单,同时不需要对不同sdk版本区分处理,但是有一定的局限性没法修复三方包的so库同时需要强制侵入接入方接口调用,接着我们来看下反射注入方案。

反射注入方案

前面介绍过System. loadLibrary ( "native-lib");加载so库的原理,其实native-lib这个so库最终传给native方法执行的参数是so库在磁盘中的完整路径,比如:/data/app-lib/com.taobao.jni-2/libnative-lib.so,so库会在 DexPathList.nativeLibraryDirectories/nativeLibraryPathElements变量所表示的目录下去遍历搜索。

sdk<23时DexPathList.findLibrary实现如下:

11f9683d34f8848db860b9c009344667.png

可以发现会遍历nativeLibraryDirectories数组,如果找到了loUtils.canOpenReadOnly(path)返回为true,那么就直接返回该path,loUtils.canOpenReadOnly(path)返回为true的前提肯定是需要path表示的so文件存在的。那么我们可以采取类似类修复反射注入方式,只要把我们的补丁so库的路径插入到nativeLibraryDirectories数组的最前面就能够达到加载so库的时候是补丁库而不是原来so库的目录,从而达到修复的目的。

sdk>=23时DexPathList.findLibrary实现如下:

4fc15f405712d94404887d35cf1b21b3.png

sdk23以上findLibrary实现已经发生了变化,如上所示,那么我们只需要把补丁so库的完整路径作为参数构建一个Element对象,然后再插入到nativeLibraryPathElements 数组的最前面就好了。

  • 优点:可以修复三方库的so库。同时接入方不需要像方案1—样强制侵入用户接口调用

  • 缺点:需要不断的对 sdk 进行适配,如上sdk23为分界线,findLibrary接口实现已经发生了变化。

对于so库的修复方案目前更多采取的是接口调用替换方式,需要强制侵入用户 接口调用。目前我们的so文件修复方案采取的是反射注入的方案,重启生效。具有更好的普遍性。如果有so文件修复实时生效的需求,也是可以做到的,只是有些限制情况。

/   常见热修复框架   /

8bd89688167084c9984f6c8b0df179e0.png

可以看出,阿里系多采用native底层方案,腾讯系多采用类加载机制。其中,Sophix是商业化方案;Tinker/Amigo支持特性较多,同时也更复杂,如果需要修复资源和so,可以选择;如果仅需要方法替换,且需要即时生效,Robust是不错的选择。

/   写在最后   /

尽管热修复(或热更新)相对于迭代更新有诸多优势,市面上也有很多开源方案可供选择,但目前热修复依然无法替代迭代更新模式。有如下原因,热修复框架多多少少会增加性能开销,或增加APK大小热修复技术本身存在局限,比如有些方案无法替换so或资源文件热修复方案的兼容性,有些方案无法同时兼顾Dalvik和ART,有些深度定制系统也无法正常工作 监管风险,比如苹果系统严格限制热修复。

所以,对于功能迭代和常规bug修复,版本迭代更新依然是主流。一般的代码修复,使用Robust可以解决,如果还需要修复资源或so库,可以考虑Tinker。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Kotlin Flow响应式编程,StateFlow和SharedFlow

Compose跨平台又来了,这次能开发iOS了

欢迎关注我的公众号

学习技术或投稿

b94c6e5212eb127cce2900ba77068670.png

5d237aec2499927cd9d06f4935f12b3e.jpeg

长按上图,识别图中二维码即可关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/58700.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

输出国际象棋棋盘

##1、程序分析 国际象棋是8*8的&#xff0c;i(07)代表行&#xff0c;j(07)代表列。当ij为奇数的时候&#xff0c;是黑色格子&#xff0c;反之&#xff0c;白色格子。 ##2、程序实现 方法一&#xff1a;两重循环 &#xff08;1&#xff09;程序&#xff1a; for i in range(8…

java简单实现中国象棋

java简单实现中国象棋 可以实现简单的人机对战功能&#xff0c;棋子移动会插入关键帧&#xff0c;可以悔棋等功能 运行效果 import java.awt.Canvas; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.event.ActionEvent; import ja…

Qt实现中国象棋:(七)悔棋

一、下棋步骤的保存 在设置保存下棋步骤之前&#xff0c;先定义一个bool类型的变量player用于判定是哪一方下子&#xff0c;设定playertrue时是红方下棋&#xff0c;playerfalse时是黑方下棋。 1:要想保存下棋的步骤&#xff0c;必须先弄明白需要保存的信息。一个下棋步骤需要…

三种版本的中国象棋

一.图片资源&#xff1a; 二.图片存放位置&#xff1a; 三.三种不同版本的中国象棋源代码 三种源代码运行之前都需要 点击项目-属性 找到这个地方&#xff0c;把字符集改成“使用多字节字符集”&#xff1a; 一.版本1&#xff1a;中国象棋简洁版&#xff08;部分特效无…

Pygame实战:中国象棋人机对抗赛今开战、谁占上风?要不要来一盘试试?

&#x1f333;导语 哈喽&#xff01;哈喽&#xff01;我是木木子&#xff01;今日游戏更新——中国象棋上线啦&#xff01; 中国象棋是一种古老的棋类游戏&#xff0c;大约有两千年的历史。 是中华文明非物质文化经典产物&#xff0c;艺术价值泛属于整个人类文明进化史的一个…

Java实现中国象棋(人机对战)

目录 简介 成品视频 实现思路 界面实现分为了三块 棋盘抽象类 按钮组抽象类 棋子绘制接口 棋盘界面实现 棋子的实现 按钮组的实现 监听工厂和监听类 棋盘绘制类的实现 开始游戏实现 停止游戏实现 游戏抽象类 游戏实现类 可走路线和吃棋判断实现 车(ju) 炮 …

Java国际象棋 棋子的走法和吃法

------ Oracle中文开发者社区 ------ 如果你想要学习编程,关注本博客,持续获得技术支持,持续获得技术咨询 java开发企业官方账号 Oracle中国官方账号 Java中国管理部 全网粉丝30万 华为云享专家 阿里专家博主 CSDN内容合伙人 CSDN原力计划作者 51CTO专家博主 CSDN博客V账号 …

Java 中国象棋

实现一个小游戏需要知道从哪里下手&#xff0c;一步步实现和完善&#xff0c;对于一个中国象棋的小游戏&#xff0c;我们可以按这样的顺序展开&#xff1a; 界面按钮加棋子实现棋子的移动判断胜负按钮“开始游戏”和“重新开始”的实现加规则轮次悔棋背景 及 提示 一、界面 …

简单的象棋开发

我们需要准备的知识是c语言基础和easyx图形: easyx官网&#xff1a; https://easyx.cn/ 首先头文件少不了: #include<stdio.h>(c语言的头文件) #include<graphics.h>&#xff08;easyx的&#xff09; #include<mmsystem.h>&#xff08;音乐播放的&#x…

中国象棋C++实现

使用C语言开发中国象棋的小游戏 Chess.cpp // includes #include<iostream> #include<graphics.h> using namespace std;// 使用到的 WCHAR 字符 class CKind{ public:WCHAR ROOKS *(_T("车"));WCHAR KNIGHTS *_T("马");WCHAR ELEPHANTS …

用C++实现中国象棋

项目介绍 最近学习到了STL库&#xff0c;了解到一些很实用的容器&#xff0c;同时我也是个象棋爱好者&#xff0c;想着能不能做个象棋的游戏小程序出来&#xff0c;运用一下所学到的知识点&#xff0c;于是动手做了这个项目&#xff0c;花了两天左右的时间基本完成&#xff0c;…

C++中国象棋

ssdut c的大作业&#xff0c;在控制台的界面实现人人对弈&#xff0c;比较适合初学&#xff0c;自己设计了一些简单算法&#xff0c;两百多行完成。 以下正文&#xff1a; 完成中国象棋游戏&#xff0c;实现如下功能&#xff1a; 1.实现人与人之间象棋的对弈。 2.每次走子之…

Java版本实现中国象棋

预览效果 中国象棋 游戏介绍&#xff1a;中国象棋是起源于中国的一种棋&#xff0c;属于二人对抗性游戏的一种&#xff0c;在中国有着悠久的历史&#xff0c;由于用具简单&#xff0c;趣味性强&#xff0c;成为流行极为广泛的棋艺活动。阿巴阿巴阿巴 代码结构&#xff1a;Butto…

数影周报:小米汽车供应商被罚100万,1688延迟下线“1688买家旺旺”

本周看点&#xff1a;小米汽车供应商被罚100万&#xff1b;特斯拉将在硅谷招聘AI 人才&#xff1b;阳光出行等25款 App涉违规收集使用个人信息等&#xff1b;1688延迟于2月8日下线“1688买家旺旺”&#xff1b;微蚁科技完成数千万元B轮融资...... 数据安全那些事 小米汽车供应商…

Coggle 30 Days of ML (23年7月)任务二:数据可视化

Coggle 30 Days of ML (23年7月&#xff09;任务二&#xff1a;数据可视化 任务二&#xff1a;对数据集字符进行可视化&#xff0c;统计标签和字符分布 说明&#xff1a;在这个任务中&#xff0c;需要使用Pandas库对数据集的字符进行可视化&#xff0c;并统计数据集中的标签和…

阿里云服务器ECS是什么?详细介绍

阿里云服务器ECS是什么&#xff1f;云服务器和传统的物理服务器有什么区别&#xff1f;云服务器有哪些优势&#xff1f;云服务器可以什么&#xff1f;云服务器架构及云服务器包含哪些功能组件&#xff1f;阿里云百科来详细说下什么是云服务器ECS&#xff1a; 目录 阿里云服务…

使用Chrome修改user agent模拟微信内置浏览器

很多时候&#xff0c;我们需要模拟微信内置浏览器&#xff0c;今天教大家用 chrome 简单模拟。如图设置&#xff1a; F12或者右键审查元素进入开发者模式&#xff0c;点击Emulation&#xff0c;然后点击Network&#xff0c;把Spoof user agent改成Other&#xff0c;并把下面…

数据分析案例-足球运动员分析

目录 加载数据 查看数据 数据详细 ​缺值处理 异常值处理 重复值处理 运动员身高和体重分布 左脚右脚使用数量 俱乐部球员评分分析 足球运动员数是否与出生日期相关 身高与体重是否具有相关性 加载数据 #加载足球运动员数据 import numpy as np import pandas as pd impor…

如何用算法预测世界杯?

预测2021欧洲世界杯 世界杯预测结果预测的原理是什么&#xff1f;周易算卦原理算命可以解决的问题善易者不卜 人工智能预测原理预测模型&#xff1a;逻辑回归算法可以预测的问题 可以单挑整个华尔街的算法现代足球 世界杯预测结果 预测2021年欧洲世界杯&#xff0c;也是一道考…

采用 Python 机器学习预测足球比赛结果

足球是世界上最火爆的运动之一,如何运用机器学习来预测足球比赛结果,是每一个足球爱好者所向往的! 本场 Chat 适合有 Python 基础的机器学习初学者,我们带你一起熟悉机器学习的开发流程,帮你快速建立起自己的英超比赛预测模型! 你将获取到如下内容: 人工智能在线建模平…