【热修复】Andfix源码分析

转载请标注来源:http://www.cnblogs.com/charles04/p/8471301.html


 Andfix源码分析

0、目录

  1. 背景介绍
  2. 源码分析
  3. 方案评价
  4. 总结与思考
  5. 参考文献

1、背景介绍

热修复技术是移动端领域近年非常活跃的一项新技术,通过热修复技术可以在不发布应用市场版本,在用户无感知的情况下对线上Bug进行紧急修复。正所谓修复于千里之外,剿灭与无形之中,实乃移动端开发运营中一项必备之尖端技术。其主要的运行原理如下:

简而言之,热修复就是通过一定的技术手段,让用户在程序的实际运行操作中,走到修复的Patch逻辑序列,而绕开存在问题的逻辑片段,实现问题的紧急规避。目前实现的技术手段主要有腾讯系的基于ClassLoader的热修复方案(例如微信的Tinker,qq空间的超级补丁)以及阿里系的基于Method Hook的热修复方案(例如Andfix,Sophix等)。今天主要介绍的就是阿里巴巴的Andfix。

2、源码分析

如前所述,Andfix是阿里巴巴推出的一款基于Method Hook的热修复技术,目前Github点赞数5.7K,是一款安全性高,较为稳定,性能比较优异的方法级替换的热修复技术。代码实现上条理清晰,架构设计合理,可读性强,是一个实现上非常优雅的开源框架。下面我们重点介绍下Andfix的源码及其设计。

一个经典的开源框架首先要友好的对外暴露接口,这样才能更便于接入,实现快速启动。所以,在介绍核心源码之前,我们首先关注下Andfix的外部接口部分。

2.1. 初始化部分

为了尽可能的覆盖BUG修复的范围,和其他的热修复技术一样,Andfix选择在APP启动的时候对热补丁进行加载,也即Application的OnCreate过程。整体的外部接口调用如下所示:

 1 @Override
 2     public void onCreate() {
 3         super.onCreate();
 4         // patch的初始化
 5         mPatchManager = new PatchManager(this);
 6         mPatchManager.init("1.0");
 7         Log.d(TAG, "inited.");
 8 
 9         // 加载缓存中的patch
10         mPatchManager.loadPatch();
11         Log.d(TAG, "apatch loaded.");
12 
13         // 将外部存储中的patch加载到当前运行的ART中
14         try {
15             // .apatch file path
16             String patchFileString = Environment.getExternalStorageDirectory()
17                     .getAbsolutePath() + APATCH_PATH;
18             mPatchManager.addPatch(patchFileString);
19             Log.d(TAG, "apatch:" + patchFileString + " added.");
20         } catch (IOException e) {
21             Log.e(TAG, "", e);
22         }
23     }

这部分接口非常简洁,大概分为三步:patch的初始化,patch的缓存加载,patch的外部存储加载。

缓存加载是为了加载之前已经从外部存储载入到缓存(data目录下)中的patch,外部存储加载是为了从外部存储中加载patch到缓存。Andfix的整体外部调用就是上面的几步,下面我们来看下Andfix的具体实现部分。

2.2. 核心实现 

Andfix的具体实现上主要分为三部分:Patch管理部分,Fix管理部分,Native Hook部分,其整体的UML架构图如下所示:

//todo 增加整体UML

 

 

2.2.1. PatchManager

整体的初始化函数的源码如下:

 1   /**
 2      * Patch的初始化工作
 3      * @param appVersion App的版本号
 4      */
 5     public void init(String appVersion) {
 6         if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
 7             Log.e(TAG, "patch dir create error.");
 8             return;
 9         } else if (!mPatchDir.isDirectory()) {// not directory
10             mPatchDir.delete();
11             return;
12         }
13         SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
14                 Context.MODE_PRIVATE);
15         String ver = sp.getString(SP_VERSION, null);
16         if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
17             cleanPatch();
18             sp.edit().putString(SP_VERSION, appVersion).commit();
19         } else {
20             initPatchs();
21         }
22     }

其中mPatchDir表示data私有目录下存放Patch文件的文件夹。首先是关于mPatchDir的简单文件夹操作,在mPatchDir文件夹初始化完成之后,紧接着比较当前的APP版本和SharedPreferences中保存的Patch对应的APP版本,两者如果不相等的话,会直接清除掉本地缓存的Patch文件和对应Patch相关的数据。这是因为热补丁是跟APP强相关的,Patch只能精确的修复对应版本的Bug。清除的源码如下所示:

1     private void cleanPatch() {
2         File[] files = mPatchDir.listFiles();
3         for (File file : files) {
4             mAndFixManager.removeOptFile(file);
5             if (!FileUtil.deleteFile(file)) {
6                 Log.e(TAG, file.getName() + " delete error.");
7             }
8         }
9     }

在版本号匹配之后,紧接着是Patch文件的初始化部分(initPatchs()),其源码如下:

1     private void initPatchs() {
2         File[] files = mPatchDir.listFiles();
3         for (File file : files) {
4             addPatch(file);
5         }
6     }

在上述函数中,ART会遍历Patch文件,并将Patch文件通过addPatch方法添加到内存中。

addPatch方法有两种多态实现,分别如下:

  • private Patch addPatch(File file)
  • public void addPatch(String path) throws IOException

其中第一个方法是从Patch文件中获取Patch对象,具体的源码如下:

 1 /**
 2      * add patch file
 3      * 
 4      * @param file
 5      * @return patch
 6      */
 7     private Patch addPatch(File file) {
 8         Patch patch = null;
 9         if (file.getName().endsWith(SUFFIX)) {
10             try {
11                 patch = new Patch(file);
12                 mPatchs.add(patch);
13             } catch (IOException e) {
14                 Log.e(TAG, "addPatch", e);
15             }
16         }
17         return patch;
18     }

此方法中把Patch文件夹映射为Patch对象,然后将Patch对象统一存放在mPatchs数据集里面。

第二个方法是从本地路径中获取Patch文件,然后从Patch文件中解析出Patch对象,之后触发Patch的加载过程,具体源码如下: 

 1 public void addPatch(String path) throws IOException {
 2         File src = new File(path);
 3         File dest = new File(mPatchDir, src.getName());
 4         if(!src.exists()){
 5             throw new FileNotFoundException(path);
 6         }
 7         if (dest.exists()) {
 8             Log.d(TAG, "patch [" + path + "] has be loaded.");
 9             return;
10         }
11         FileUtil.copyFile(src, dest);// copy to patch's directory
12         Patch patch = addPatch(dest);
13         if (patch != null) {
14             loadPatch(patch);
15         }
16     }

获取完Patch的对象列表之后,接下来的内容就是加载Patch中的内容,并根据Patch中的内容进行Hotfix。此过程是通过Patchmanager类中的loadPatch方法实现的。loadPatch方法一共有三个多态,分别如下:

  • public void loadPatch(String patchName, ClassLoader classLoader)
  • public void loadPatch()
  • private void loadPatch(Patch patch)

三个方法入参不同,会通过不同的ClassLoader加载不同的Patch文件,已第三个方法为例,该函数中对数据进行封装之后,最终会循环调用AndfixManager中的fix方法,具体的源码如下:

 1 private void loadPatch(Patch patch) {
 2         Set<String> patchNames = patch.getPatchNames();
 3         ClassLoader cl;
 4         List<String> classes;
 5         for (String patchName : patchNames) {
 6             if (mLoaders.containsKey("*")) {
 7                 cl = mContext.getClassLoader();
 8             } else {
 9                 cl = mLoaders.get(patchName);
10             }
11             if (cl != null) {
12                 classes = patch.getClasses(patchName);
13                 mAndFixManager.fix(patch.getFile(), cl, classes);
14             }
15         }
16     }

PatchManager的源码基本就如上所述,主要是对Patch的管理与加载过程,代码简洁易懂,可读性强。

2.2.2. AndFixManager

接下来,我们重点分析下AndfixManager类,该类中主要介绍Andfix的BugFix的核心流程。通过之前的PatchManager类的源码分析可知,AndfixManager的关键入口函数为fix方法。其源码如下所示:

 1 public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {
 2         if (!mSupport) {
 3             return;
 4         }
 5 
 6         if (!mSecurityChecker.verifyApk(file)) {// security check fail
 7             return;
 8         }
 9 
10         try {
11             File optfile = new File(mOptDir, file.getName());
12             boolean saveFingerprint = true;
13             if (optfile.exists()) {
14                 // need to verify fingerprint when the optimize file exist,
15                 // prevent someone attack on jailbreak device with
16                 // Vulnerability-Parasyte.
17                 // btw:exaggerated android Vulnerability-Parasyte
18                 // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
19                 if (mSecurityChecker.verifyOpt(optfile)) {
20                     saveFingerprint = false;
21                 } else if (!optfile.delete()) {
22                     return;
23                 }
24             }
25 
26             final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
27                     optfile.getAbsolutePath(), Context.MODE_PRIVATE);
28 
29             if (saveFingerprint) {
30                 mSecurityChecker.saveOptSig(optfile);
31             }
32 
33             ClassLoader patchClassLoader = new ClassLoader(classLoader) {
34                 @Override
35                 protected Class<?> findClass(String className)
36                         throws ClassNotFoundException {
37                     Class<?> clazz = dexFile.loadClass(className, this);
38                     if (clazz == null
39                             && className.startsWith("com.alipay.euler.andfix")) {
40                         return Class.forName(className);// annotation’s class
41                                                         // not found
42                     }
43                     if (clazz == null) {
44                         throw new ClassNotFoundException(className);
45                     }
46                     return clazz;
47                 }
48             };
49             Enumeration<String> entrys = dexFile.entries();
50             Class<?> clazz = null;
51             while (entrys.hasMoreElements()) {
52                 String entry = entrys.nextElement();
53                 if (classes != null && !classes.contains(entry)) {
54                     continue;// skip, not need fix
55                 }
56                 clazz = dexFile.loadClass(entry, patchClassLoader);
57                 if (clazz != null) {
58                     fixClass(clazz, classLoader);
59                 }
60             }
61         } catch (IOException e) {
62             Log.e(TAG, "pacth", e);
63         }
64     }

在此方法中,主要包括安全校验,bugFix两部分,具体如下;

(1)安全校验

Andfix会对传进来的Patch文件进行安全校验,包括准确性校验和完整性校验。

安全校验的具体实现在SecurityChecker类中,其结构体如下:

//todo 补充SecurityChecker UML

其中准确性校验(签名校验)的具体实现如下:

 1 /**
 2      * @param path
 3      *            Apk file
 4      * @return true if verify apk success
 5      */
 6     public boolean verifyApk(File path) {
 7         if (mDebuggable) {
 8             Log.d(TAG, "mDebuggable = true");
 9             return true;
10         }
11 
12         JarFile jarFile = null;
13         try {
14             jarFile = new JarFile(path);
15 
16             JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX);
17             if (null == jarEntry) {// no code
18                 return false;
19             }
20             loadDigestes(jarFile, jarEntry);
21             Certificate[] certs = jarEntry.getCertificates();
22             if (certs == null) {
23                 return false;
24             }
25             return check(path, certs);
26         } catch (IOException e) {
27             Log.e(TAG, path.getAbsolutePath(), e);
28             return false;
29         } finally {
30             try {
31                 if (jarFile != null) {
32                     jarFile.close();
33                 }
34             } catch (IOException e) {
35                 Log.e(TAG, path.getAbsolutePath(), e);
36             }
37         }
38     }
39 
40     // verify the signature of the Apk
41     private boolean check(File path, Certificate[] certs) {
42         if (certs.length > 0) {
43             for (int i = certs.length - 1; i >= 0; i--) {
44                 try {
45                     certs[i].verify(mPublicKey);
46                     return true;
47                 } catch (Exception e) {
48                     Log.e(TAG, path.getAbsolutePath(), e);
49                 }
50             }
51         }
52         return false;
53     }

上述过程对APK进行证书签名校验,符合签名的APK为合法的APK,否则为非法的APK,中断热修复过程。

Andfix的过程不仅进行签名校验,还进行完整性校验。完整性校验是为了防止出现在进行patch下载的过程中下载不完整,导致修复出现异常的情况。完整性校验是通过校验MD4来实现的,具体如下;

 1 /**
 2      * @param path
 3      *            Dex file
 4      * @return true if verify fingerprint success
 5      */
 6     public boolean verifyOpt(File file) {
 7         String fingerprint = getFileMD5(file);
 8         String saved = getFingerprint(file.getName());
 9         if (fingerprint != null && TextUtils.equals(fingerprint, saved)) {
10             return true;
11         }
12         return false;
13

 (2)Bug Fix

Andfix热修复的核心实现中,分为两个步骤:

  1. 找到需要修复的Class;
  2. 替换需要进行修复的Method。

第一步的具体实现如下:

 1 /**
 2      * fix class
 3      * 
 4      * @param clazz
 5      *            class
 6      */
 7     private void fixClass(Class<?> clazz, ClassLoader classLoader) {
 8         Method[] methods = clazz.getDeclaredMethods();
 9         MethodReplace methodReplace;
10         String clz;
11         String meth;
12         for (Method method : methods) {
13             methodReplace = method.getAnnotation(MethodReplace.class);
14             if (methodReplace == null)
15                 continue;
16             clz = methodReplace.clazz();
17             meth = methodReplace.method();
18             if (!isEmpty(clz) && !isEmpty(meth)) {
19                 replaceMethod(classLoader, clz, meth, method);
20             }
21         }
22     }

第二部的具体实现如下:

 1 /**
 2      * replace method
 3      * 
 4      * @param classLoader classloader
 5      * @param clz class
 6      * @param meth name of target method 
 7      * @param method source method
 8      */
 9     private void replaceMethod(ClassLoader classLoader, String clz,
10             String meth, Method method) {
11         try {
12             String key = clz + "@" + classLoader.toString();
13             Class<?> clazz = mFixedClass.get(key);
14             if (clazz == null) {// class not load
15                 Class<?> clzz = classLoader.loadClass(clz);
16                 // initialize target class
17                 clazz = AndFix.initTargetClass(clzz);
18             }
19             if (clazz != null) {// initialize class OK
20                 mFixedClass.put(key, clazz);
21                 Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());
22                 AndFix.addReplaceMethod(src, method);
23             }
24         } catch (Exception e) {
25             Log.e(TAG, "replaceMethod", e);
26         }
27

其中核心函数AndFix.addReplaceMethod(src, method)的具体实现如下:

 1 /**
 2      * replace method's body
 3      * 
 4      * @param src
 5      *            source method
 6      * @param dest
 7      *            target method
 8      * 
 9      */
10     public static void addReplaceMethod(Method src, Method dest) {
11         try {
12             replaceMethod(src, dest);
13             initFields(dest.getDeclaringClass());
14         } catch (Throwable e) {
15             Log.e(TAG, "addReplaceMethod", e);
16         }
17

可以观察到,Andfix中函数的替换是通过Native方法replaceMethod(Method dest, Method src)实现的。从JNI中找到这部分的源码如下:

1 static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) {
2     if (isArt) {
3         art_replaceMethod(env, src, dest);
4     } else {
5         dalvik_replaceMethod(env, src, dest);
6     }
7

Native层面进行Method Hook的原理是将源方法中的各个属性替换为目标方法的属性。由于不同虚拟机,甚至同样虚拟机下不同API对应的方法结构体的不同,在进行Method Hook的过程中,对不同情况,要适配不同的方法。

不同的Android版本,对于的虚拟机不同:Android 4.4以下用的是Dalvik虚拟机,而Android 4.4以上用的是ART(Android Running Time)虚拟机。如上面代码实现,在进行热修复的过程中,ART虚拟机下调用的是art_replaceMethod(env, src, dest)方法;Dalvik虚拟机调用的是dalvik_replaceMethod(env, src, dest)方法。 

而对于ART虚拟机,不同Android API的系统,可能会对应不同的方法结构体(ArtMethod),所以会有对应的不同的适配实现,其代码如下:

 1 extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
 2         JNIEnv* env, jobject src, jobject dest) {
 3     if (apilevel > 23) {
 4         replace_7_0(env, src, dest);
 5     } else if (apilevel > 22) {
 6         replace_6_0(env, src, dest);
 7     } else if (apilevel > 21) {
 8         replace_5_1(env, src, dest);
 9     } else if (apilevel > 19) {
10         replace_5_0(env, src, dest);
11     }else{
12         replace_4_4(env, src, dest);
13     }
14 } 

不同API的实现类如下:

所以说Andfix可以兼容Android2.3到7.0版本,对于超过Android7.0的版本,如果ArtMethod相比较7.0有较大的改变,就可能存在兼容性问题,这是后话。

以7.0版本为例,Andfix中Method hook的属性替换的具体实现如下:

 1 void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
 2     art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
 3 
 4     art::mirror::ArtMethod* dmeth =
 5             (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
 6 
 7 //    reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_ =
 8 //            reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_; //for plugin classloader
 9     reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
10             reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
11     reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ =
12             reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_ -1;
13     //for reflection invoke
14     reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
15 
16     smeth->declaring_class_ = dmeth->declaring_class_;
17     smeth->access_flags_ = dmeth->access_flags_  | 0x0001;
18     smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
19     smeth->dex_method_index_ = dmeth->dex_method_index_;
20     smeth->method_index_ = dmeth->method_index_;
21     smeth->hotness_count_ = dmeth->hotness_count_;
22 
23     smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
24             dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
25     smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
26             dmeth->ptr_sized_fields_.dex_cache_resolved_types_;
27 
28     smeth->ptr_sized_fields_.entry_point_from_jni_ =
29             dmeth->ptr_sized_fields_.entry_point_from_jni_;
30     smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
31             dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
32 
33     LOGD("replace_7_0: %d , %d",
34             smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
35             dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
36 
37

首先调用ART的方法获取Andfix中源方法(smeth)和目标方法(dmeth)的句柄,然后将源方法的各个属性(例如declaring_class_:所属类,access_flags:访问权限,method_index_:代码执行地址等)替换为目标方法的各个属性,从而实现方法层面的Hook,实现Hotfix。

3.方案评价

3.1.优点

(1)即时生效

(2)基于Method Hook的实现,对原始APK侵入较小,性能影响几乎忽略不计

3.2.缺点

(1)只能用于方法级的修复

Andfix最为明显的缺点是只能实现方法级别的修复。而无法实现xml,资源文件级别的修复,也无法增加或者删除class类,这一点从原理分析上能够很明显的看到。但是,热修复的精髓就是在不重新发布版本,不影响性能和体验的前提下,实现对线上紧急Bug的灵活修复。在大多数情况下,通过方法级别的修复就能够达到热修复的目的,Andfix做到了小而精,改动小,影响小但性能优异,效果稳定,个人认为在一定程度上已经满足了热修复的需求。与Andfix形成鲜明对比的是微信推出的Tinker,Tinker追求的是广而博,能够实现类,xml,资源文件,so库等的修复,甚至可以新增export属性为false的Activity类,从某种意义上讲,甚至可以小型功能的发布,有点插件化的味道。

这里不过多评价两种插件化框架的优劣,和谈恋爱一样,没有最好的,只有最合适的,选择适合自己项目的热修复框架,然后用好,就可以了。

(2)兼容性问题

由于Java方法对应的底层数据结构体的差异,在进行native层面的Method Hook过程中,不同虚拟机之间要使用不同的方法,甚至在ART架构中,不同的API的Android版本间也可能要使用不同的适配方法。

目前Andfix在实现的时候,根据AOSP开源代码中不同API版本对ArtMethod的定义,将运行的Java Method强行地转换为art::mirror::ArtMethod,但是由于Android源码是公开的,在实际的设备上,不同的手机厂商可能会对ArtMethod做个性化修改,这样就有可能会导致基于开源标准代码实现的Method Hook无法兼容有些设备的情况。

为了解决Andfix的兼容性问题,阿里巴巴随后推出了Andfix的改进版热修复方案——Sophix。Sophix与Andfix的区别在于,在进行Method Hook的时候,不再进行ArtMethod属性的替换,而是直接将ArtMethod作为一个整体进行替换, 其Method Hook的核心实现如下:

  • memcpy(dmeth, smeth, sizeof(ArtMethod));

Sophix通过进行整体方法体的替换,完美的解决了Andfix中的兼容性问题,这样,不仅在不能的厂商的设备上可以达到兼容,而且对于后续发布的Android版本也能够做到向后兼容,保障了热修复方案的健壮性。

4.总结与思考

本文对Andfix的原理进行了分析介绍,并对Andfix客户端的源码实现进行了简要分析,其中重点介绍了客户端在获取Patch后进行Class匹配与Method替换的过程。

初次此外,在开发过程中,有几个技术细节也有较大的可挖掘性,具体如下:

(1)Andfix中热修复Patch的生成原理;

(2)Patch的下载流程(推荐自己搭建服务器框架,通过okhttp实现下载流程),更新,版本管理;

(3)MultiDex下的Andfix;

(4)ClassLoader的内核原理;

(5)Android Running Time与Dalvik;

(6)其他同类型的热修复框架,例如腾讯微信的Tinker,美团的Robust,饿了么的MiGo,大众点评的Nuwa等。

5.参考文献

(1)https://github.com/alibaba/AndFix
(2)http://blog.csdn.net/weelyy/article/details/78906537
(3)https://www.jianshu.com/p/633019c4970d
(4)http://blog.csdn.net/lixin88/article/details/72190240
(5)https://www.cnblogs.com/soaringEveryday/p/5338214.html

转载于:https://www.cnblogs.com/charles04/p/8471301.html

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

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

相关文章

已知矩阵 matlab,在MATLAB中,已知矩阵A,那么A(:,2:end)表示

摘要&#xff1a;已知供输工方、表示添资料准加剂、加及标应提原料有关的()的出国法等使用&#xff0c;品”“进办理报检时口食。已知信息系统模型不包逻辑括(。...已知信息系统构化中的结方法设计&#xff0c;矩阵细设和详总体计两阶段一般分为设计&#xff0c;总体主要建立其…

文件源码读取 php伪协议,include(文件包含漏洞,php伪协议)

点击tips查看元素&#xff0c;也并没有有用的信息&#xff0c;联想到题目,include想起了文件包含漏洞。构造payload?file/../../../../../../flag.php没有返回东西。看完wq学到了一个新姿势&#xff1a;php伪代码构造payload?filephp://filter/readconvert.base64-encode/res…

Echarts自定义折线图例,增加选中功能

用Echarts图表开发&#xff0c;原本的Echarts图例不一定能满足我们的视觉要求。 下面是Echarts 折线图自定义图例&#xff0c;图例checked选中&#xff0c;相应的折线线条会随之checked&#xff0c;其余未选中的图例对应的折线opacity会降低&#xff0c;&#xff08;柱状图&…

php产品效果图,jQuery_基于JQuery制作的产品广告效果,效果图.如下: 动画效果介绍 - phpStudy...

基于JQuery制作的产品广告效果效果图.如下&#xff1a;动画效果介绍&#xff1a;这组广告效果是打开页面后图片会自动播放&#xff0c;从1-5共计5张图片&#xff0c;如果属标放到右下角的1、2、3、4、5列表上&#xff0c;可以自由进行切换到自己想看的图片上去。图片切换是由下…

Python on the Way, Day1 - Python基础1

一、 Python介绍 python的创始人为吉多范罗苏姆&#xff08;Guido van Rossum&#xff09;。1989年的圣诞节期间&#xff0c;吉多范罗苏姆为了在阿姆斯特丹打发时间&#xff0c;决心开发一个新的脚本解释程序&#xff0c;作为ABC语言的一种继承 Python可以应用于众多领域&#…

python数据显示为什么只能显示最后一个变量,Python变量和简单数据类型,之,的

变量介绍。变量就是代表某个数据(值)的名称&#xff0c;简单点说变量就是给数据起个名字。变量的特点。1)变量是计算机内存中的一块区域&#xff0c;变量可以存储规定范围内的值&#xff0c;而且值是可变的。2)在创建变量时会在内存中开辟一个空间。基于变量的数据类型&#xf…

【BZOJ2095】【POI2010】Bridge 网络流

题目大意 ​  给你一个无向图&#xff0c;每条边的两个方向的边权可能不同。要求找出一条欧拉回路使得路径上的边权的最大值最小。无解输出"NIE"。   \(2\leq n\leq 1000,1\leq m\leq 2000\) 题解 ​  我们先二分答案\(ans\)&#xff0c;把边权大于\(ans\)的边…

space index.php 7-14,SpacePack高效部署PHP生产环境

SpacePack 基于 Docker 为了快速部署 PHP 生产环境而产生的项目&#xff0c;它包含了一般项目中常用的组件&#xff0c;能够在最短的时间内产生一个完善并且优化过的 PHP 生产环境。容器版本SpacePack 默认包含了 OpenResty 1.13、PHP 7.2、MariaDB 10.3、Memcached 1.5、Redis…

云播自带解析php,使用PHP SDK,web端的华为云视频点播接入,加密视频播放的坑与解决方案-全代码篇...

下载phpdemo算是跑起来了&#xff0c;现在就要考虑租户系统如自身验证token的问题了。1、先介绍下我的代码目录2、文件执行的时序图和流程图2、代码demotest.phpfunction curl_request($url,$post,$cookie, $returnCookie0){$curl curl_init();curl_setopt($curl, CURLOPT_URL…

php获取h5视频直链,一种H5播放实时视频的方法与系统与流程

本发明涉及播放实时视频&#xff0c;尤其涉及一种h5播放实时视频的方法与系统。背景技术&#xff1a;h5是指第5代html&#xff0c;也指用h5语言制作的一切数字产品。所谓html是“超文本标记语言”的英文缩写。“超文本”是指页面内可以包含图片、链接&#xff0c;甚至音乐、程序…

基础题

1&#xff0c;别名&#xff0c;内部&#xff0c;外部&#xff0c;hash优先级&#xff1f; 2&#xff0c;screen协助 1.一台screen -S 协助名称 2.另外一台screen -ls 列出目前开的协助会话&#xff08;session&#xff09;&#xff0c;找到上面协助名称对应的session号。 3. sc…

大数据笔记(十三)——常见的NoSQL数据库之HBase数据库(A)

一.HBase的表结构和体系结构 1.HBase的表结构 把所有的数据存到一张表中。通过牺牲表空间&#xff0c;换取良好的性能。 HBase的列以列族的形式存在。每一个列族包括若干列 2.HBase的体系结构 主从结构&#xff1a; 主节点&#xff1a;HBase 从节点&#xff1a;RegionServer 包…

linux内核网络钩子函数使用,Linux内核IOCTL网络控制框架实现实例分析

4.6、inet_ioctl函数由于inet_ioctl函数内容分支很多,但功能、处理不难理解,所以我把一些不常见的内容都省去,挑简单重要的说,完全在于抛砖引玉:static int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg){…switch(cmd){case FIOSETOWN://设置属主cas…

(转)递归转非递归的思路和例子

转自&#xff1a;http://blog.51cto.com/cnn237111/1241956 某些算法逻辑&#xff0c;用递归很好表述&#xff0c;程序也很好写。理论上所有的递归都是可以转换成非递归的。如果有些场合要求不得使用递归&#xff0c;那就只好改成非递归了。 通常改成非递归算法的思路&#xff…

iOS - 富文本

iOS--NSAttributedString超全属性详解及应用&#xff08;富文本、图文混排&#xff09; ios项目中经常需要显示一些带有特殊样式的文本&#xff0c;比如说带有下划线、删除线、斜体、空心字体、背景色、阴影以及图文混排&#xff08;一种文字中夹杂图片的显示效果&#xff09;。…

pdf.js 文字丢失问题 .cmaps

使用pdf.js 展示pdf文件 需求&#xff1a;电子发票类的pdf文件&#xff0c;以base64流的形式请求到&#xff0c;在浏览器中展示pdf文件 遇到的问题&#xff1a; 正常展示后&#xff0c;部分文字无法正常显示&#xff0c; 正常显示如下&#xff1a; 文件目录&#xff1a; js:fun…

超过4g的文件怎么上传到linux,怎么免费上传大于4G的文件到百度云 大于4G的文件不开会员怎么上传到百度云...

4G管家appv1.0 安卓版类型&#xff1a;系统工具大小&#xff1a;13.1M语言&#xff1a;中文 评分&#xff1a;10.0标签&#xff1a;立即下载百度云可以非常方便大家存储一些大文件资料&#xff0c;而且百度云的容量也非常高&#xff0c;不过如果你是普通用户的话要想上传大于4g…

android 屏幕坐标色彩,Android自定义View实现颜色选取器

Android 自定义View 颜色选取器&#xff0c;可以实现水平、竖直选择颜色类似 SeekBar 的方式通过滑动选择颜色。效果图xml 属性1.indicatorColor 指示点颜色2.indicatorEnable 是否使用指示点3.orientation 方向horizontal 水平vertical 竖直使用复制 \library\src…\ColorPick…

linux右键菜单的截图,Linux: 给右键菜单加一个“转换图片为jpg格式”

Linux上通常都会安装imagemagick这个小巧但又异常强大的工具。这个软件提供了一系列很好用的功能。这里说一说如何使用它的convert命令转换图片为jpg格式&#xff0c;以及如何把它添加到Thunar的右键菜单。convert转换图片为jpg格式用起来超简单&#xff1a;convert -format jp…

eclipse实现Android登录功能,eclipse开发安卓登录

划线的地方怎么解决啊&#xff1f;有没有大佬知道如何修改package com.example.login;import android.app.Activity;import android.content.Context;import android.content.Intent;import android.content.SharedPreferences;import android.content.SharedPreferences.Edito…