热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

一个好的热修复技术,将为你的App助力百倍

前言热修复到现在2022年已经不是一个新名词,但是作为Android开发核心技术栈的一部分,我这里还得来一次冷饭热炒。随着移动端业务复杂程度的增加&

前言

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

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

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

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


1.什么是热修复

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

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

热修复优缺点:


  • 优点:
    • 1.只需要打补丁包,不需要重新发版本。
    • 2.用户无感知,不需要重新下载最新应用
    • 3.修复成功率高
  • 缺点:
    • 补丁包滥用,容易导致应用版本不可控,需要开发一套完整的补丁包更新机制,会增加一定的成本

2.热修复方案

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


  • 1.代码修复
  • 2.资源修复
  • 3.动态库修复

2.1:代码修复方案

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

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


2.1.1.类加载方案

之前分析类加载机制有说过: 加载流程先是遵循双亲委派原则,如果委派原则没有找到此前加载过此类, 则会调用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 }

类加载过程如下:

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

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


2.1.2:底层替换方案

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

结构如下:

// art/runtime/art_method.h
class ArtMethod FINAL {
...protected:GcRoot declaring_class_;GcRoot dex_cache_resolved_methods_;GcRoot> 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的风险,采用直接替换整个结构体。这样不管手机厂商如何更改系统,我们都可以正确定位到方法地址


2.4.3: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就是基于此


2.2:资源修复方案

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

public static void monkeyPatchExistingResources(Context context,String externalResourceFile, Collection 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> 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> 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> map = (HashMap) fMActiveResources.get(thread);references = map.values();}
//遍历并得到弱引用集合中的 Resources ,将 Resources mAssets 字段引用替换成新的 AssetManagerfor (WeakReference 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 的 m Assets 字段 的引用替换为新创建的 AssetManager 。
  • 紧接着 根据 SDK 版本的不同,用不同的方式得到 Resources 的弱引用集合,
  • 再遍历这个弱引用集合, 将弱引用集合中的 Resources 的 mAssets 字段引用都替换成新创建的 AssetManager 。

资源修复原理:


  • 1.创建新的AssetManager,通过反射调用addAssetPath方法,加载外部资源,这样新创建的AssetManager就含有了外部资源
  • 2.将AssetManager类型的mAsset字段全部用新创建的AssetManager对象替换。这样下次加载资源文件的时候就可以找到包含外部资源文件的AssetManager。


2.3:动态链接库so的修复


1.接口调用替换方案:

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

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

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

加载策略如下:

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

image.png

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

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


2、反射注入方案

前面介绍过 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 实现如下

image.png

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

sdk>&#61;23 DexPathList.findLibrary 实现如下

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


  • 优点&#xff1a;可以修复三方库的so库。同时接入方不需要像方案1 —样强制侵入用 户接口调用
  • 缺点&#xff1a;需要不断的对 sdk 进行适配&#xff0c;如上 sdk23 为分界线&#xff0c;findLibrary接口实现已经发生了变化。

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


常见热修复框架&#xff1f;

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


总结&#xff1a;

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

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



最后分享一份我自己收录整理的Android学习PDF&#43;架构视频&#43;面试文档&#43;核心笔记&#xff0c;高级架构技术进阶脑图、Android开发面试专题文档&#xff0c;高级进阶架构文档。这些都是我现在闲暇还会反复翻阅的精品学习文档。里面对近几年的大厂面试高频问题都有详细的讲解&#xff0c;也是对我这次面试通过有很大的帮助。相信可以有效的帮助大家掌握知识、理解原理。

当然你也可以拿去查漏补缺&#xff0c;提升自身的竞争力。↓↓↓

有需要的可以复制下方链接&#xff0c;传送直达&#xff01;&#xff01;&#xff01;
https://qr21.cn/CaZQLo?BIZ&#61;ECOMMERCE


推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 安卓select模态框样式改变_微软Office风格的多端(Web、安卓、iOS)组件库——Fabric UI...
    介绍FabricUI是微软开源的一套Office风格的多端组件库,共有三套针对性的组件,分别适用于web、android以及iOS,Fab ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • Google在I/O开发者大会详细介绍Android N系统的更新和安全性提升
    Google在2016年的I/O开发者大会上详细介绍了Android N系统的更新和安全性提升。Android N系统在安全方面支持无缝升级更新和修补漏洞,引入了基于文件的数据加密系统和移动版本的Chrome浏览器可以识别恶意网站等新的安全机制。在性能方面,Android N内置了先进的图形处理系统Vulkan,加入了JIT编译器以提高安装效率和减少应用程序的占用空间。此外,Android N还具有自动关闭长时间未使用的后台应用程序来释放系统资源的机制。 ... [详细]
  • 本文介绍了一个免费的asp.net控件,该控件具备数据显示、录入、更新、删除等功能。它比datagrid更易用、更实用,同时具备多种功能,例如属性设置、数据排序、字段类型格式化显示、密码字段支持、图像字段上传和生成缩略图等。此外,它还提供了数据验证、日期选择器、数字选择器等功能,以及防止注入攻击、非本页提交和自动分页技术等安全性和性能优化功能。最后,该控件还支持字段值合计和数据导出功能。总之,该控件功能强大且免费,适用于asp.net开发。 ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
  • 2016 linux发行版排行_灵越7590 安装 linux (manjarognome)
    RT之前做了一次灵越7590黑苹果炒作业的文章,希望能够分享给更多不想折腾的人。kawauso:教你如何给灵越7590黑苹果抄作业​zhuanlan.z ... [详细]
  • 精讲代理设计模式
    代理设计模式为其他对象提供一种代理以控制对这个对象的访问。代理模式实现原理代理模式主要包含三个角色,即抽象主题角色(Subject)、委托类角色(被代理角色ÿ ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 本文介绍了在Win10上安装WinPythonHadoop的详细步骤,包括安装Python环境、安装JDK8、安装pyspark、安装Hadoop和Spark、设置环境变量、下载winutils.exe等。同时提醒注意Hadoop版本与pyspark版本的一致性,并建议重启电脑以确保安装成功。 ... [详细]
author-avatar
学银先生_512
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有