AndFix使用源码分析

hotfix-andfix:andfix是阿里巴巴推出的开源的android热修复的一个框架,主要是本地层方法替换。apkpatch将两个apk做一次对比,然后识别不同的部分。可以看到生成的apatch了文件,后缀改成zip再解压开,里面有一个dex文件。通过jadx查看一下原始文件,里面就是被修复的代码所在的类文件,这些更改过的类都加上了一个_CF的后缀,并且移动的方法都被加上一个一个叫@MethodReplace的注释,通过clazz和method指定了需要替换的方法。然后客户端sdk得到补丁文件后就 #AndFix-Demo有关使用AndFix 0.5.0 AndFix的集成使用的演示。 AndFix,全称是Android hot-fix。是阿里开源的一个Android热补丁框架,允许APP在不重新发布版本的情况下修复线上的错误。支持Android 2.3到6.0。 一,andfix使用 首先添加依赖编译'com.alipay.euler:andfix:0.3.1@aar' 然后在Application.onCreate()中添加以下代码:patchManager = new PatchManager(上下文); //初始化补丁包管理器patchManager.init(appversion); //版本字符串appversion = getPackageManager()。getPackageInfo(getPackageName(),0).versionName; patchMa 立即下载

AndFix使用源码分析

前言

关于热修复,我们从这三个问题开始:什么是热修复?目前主流热修复框架?为什么选择AndFix?

Answer Q1:

当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。
这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?
虽然Android系统并没有提供这个技术,但是很幸运的告诉大家,答案是:可以

Answer Q2:Dexposed、AndFix、ClassLoader 热补丁方案比较

Answer Q3:Andfix支持Art模式,应用Patch不需要重启… 详情请参照 answer Q2 中博文中的“比较”模块。

使用流程原理

流程图

借鉴了下andfix的官方给的流程图,大致可以理解为这样一个流程:
检测到线上的版本有bug,创建一个分支去修改bug,修改完成并且测试通过后生成release版本apk,利用工具和线上版本进行对比生成差分文件即.patch文件,然后将.patch文件上传到线上服务器,最后将修复bug的分支代码合并到主分支。

简单使用配置

  • 引入

    dependencies {
    compile ‘com.alipay.euler:andfix:0.4.0@aar’
    }

  • 混淆配置

    -keep class * extends java.lang.annotation.Annotation

    -keepclasseswithmembernames class * {
    native ;
    }

  • 开始使用了

Apkpatch工具

与普通的第三方工具的不同的是,andfix除了要在代码中引入library dependency外,还需要一个辅助工具apkpatch.jar来生成差分文件。
现在我们就来看看它是如何生成差分文件的。
首先,看看apkpatch如何使用的:
命令行进入apkpatch.jar所在目录后执行命令:apkpatch -f new.apk -t old.apk -o outputDir -k app.jks -p kpassword -a alias -e epassword
命令执行完成后会在输出目录下面生成下面的文件:

如果存在多个.patch文件的话,可以用下面这个命令将其合并为一个:
apkpatch -m

Java层源码分析

Andfix的使用代码配置可简单分为四步:

//第一步 创建PatchManager 对象
PatchManager patchManager = new PatchManager(this);
try {
    PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
//第二步
    patchManager.init(packageInfo.versionName);
//第三步
    patchManager.loadPatch();
    try {
//第四步
  patchManager.addPatch(.patch 文件的路径);
    } catch (IOException e) {
        e.printStackTrace();
    }
} catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
}

接下来我们安照这四步来分析andfix的源码,看看都是做了些什么操作。

创建PatchManager

首先我们来看看在new PatchManager(this)中都做了什么:

public PatchManager(Context context) {
   mContext = context;
   //1.创建AndFixManager对象
   mAndFixManager = new AndFixManager(mContext);
   //2.创建用来缓存.patch文件的目录 位置为/data/data/com.mime.andfixdemo/files/apatch
   mPatchDir = new File(mContext.getFilesDir(), DIR);
   //3.用来存放mPatchDir目录中.patch转换成Patch类的集合
   mPatchs = new ConcurrentSkipListSet<Patch>();
   //4.用来存放类加载器ClassLoader的集合,其实只有PathClassLoader这个元素
   mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}

我们到AndFixManager的构造方法中看看在创建它时都做了什么

public AndFixManager(Context context) {
   mContext = context;
   //查看是否支持 通过源码分析Andfix不支持阿里云系统,支持SDK<8,23>
   //其中AndFix.setup()还调用了一个native方法,意思是设置虚拟机类型(ART/Dalvik)及Sdk版本,
   //并返回是否置成功
   mSupport = Compat.isSupport();
   if (mSupport) {
      //创建安全校验类 这个类是用来对.patch文件进行签名校验的
      mSecurityChecker = new SecurityChecker(mContext);
      //又创建了个目录/data/data/com.mime.andfixdemo/files/apatch_opt
      //用来保存优化后的dex数据
      mOptDir = new File(mContext.getFilesDir(), DIR);
      if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
         mSupport = false;
         Log.e(TAG, "opt dir create error.");
      } else if (!mOptDir.isDirectory()) {// not directory
         mOptDir.delete();
         mSupport = false;
      }
   }
}

通过上面的分析可以知道第一步时做了一些对象,目录的创建工作和校验当前系统是否是AndFix所支持的类型

PatchManager.init(appVersion)

一起看看init方法里面做了什么

public void init(String appVersion) {
   if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
      Log.e(TAG, "patch dir create error.");
      return;
   } else if (!mPatchDir.isDirectory()) {// not directory
      mPatchDir.delete();
      return;
   }
   SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,Context.MODE_PRIVATE);
   String ver = sp.getString(SP_VERSION, null);
   if (ver == null || !ver.equalsIgnoreCase(appVersion)) { //首次安装或版本升级时走这里
      //清除所有旧版本的.patch文件,更新版本号存储信息
      cleanPatch();
      sp.edit().putString(SP_VERSION, appVersion).commit();
   } else {
      // 将mPatchDir缓存目录中的.patch文件转换成Patch对象添加到mPatchs集合中
      initPatchs();
   }
}

下面是cleanPatch和initPatchs方法

private void cleanPatch() {
   File[] files = mPatchDir.listFiles();
   for (File file : files) {
      mAndFixManager.removeOptFile(file);
      if (!FileUtil.deleteFile(file)) {
         Log.e(TAG, file.getName() + " delete error.");
      }
   }
}
private void initPatchs() {
   File[] files = mPatchDir.listFiles();
   for (File file : files) {
      addPatch(file);
   }
}
private Patch addPatch(File file) {
   Patch patch = null;
   if (file.getName().endsWith(SUFFIX)) {
      try {
         patch = new Patch(file);
         mPatchs.add(patch);
      } catch (IOException e) {
         Log.e(TAG, "addPatch", e);
      }
   }
   return patch;
}

下面来分析下如何将.patch文件转换成Patch类的,我们先看下构造函数中做了什么

public Patch(File file) throws IOException {
   mFile = file;
   init();
}

//擦,发现这个注解让我很疑惑,因为并没有找到替换的新方法,是不是方法里面用了过期的方法

@SuppressWarnings("deprecation")
private void init() throws IOException {
   JarFile jarFile = null;
   InputStream inputStream = null;
   try {
      //将.patch文件转化成jar文件,其实.patch文件就是个压缩文件,
      //我们可以将其后缀名改为.zip进行解压
      jarFile = new JarFile(mFile);
      //解压后你可以找到名为PATCH.MF的文件,文件的内容包括下面信息
      //Manifest-Version: 1.0
      //Patch-Name: app_1
      //Created-Time: 17 Jun 2016 07:26:46 GMT
      //From-File: app_1.0.1_01.apk
      //To-File: app_1.0.1_02.apk
      //Patch-Classes: com.mime.andfixdemo.MainActivity_CF,
    com.mime.andfixdemo.AndFixDemoApplication_CF
      //Created-By: 1.0 (ApkPatch)
      JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
      inputStream = jarFile.getInputStream(entry);
      Manifest manifest = new Manifest(inputStream);
      Attributes main = manifest.getMainAttributes();
      //获取Patch-Name对应的属性值
      mName = main.getValue(PATCH_NAME);
      //获取Created-Time对应的属性值
      mTime = new Date(main.getValue(CREATED_TIME));
      mClassesMap = new HashMap<String, List<String>>();
      Attributes.Name attrName;
      String name;
      List<String> strings;
      //获取到Patch-Classes中所有的.classes文件名称并添加到mClassesMap集合中
      for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
         attrName = (Attributes.Name) it.next();
         name = attrName.toString();
         if (name.endsWith(CLASSES)) {
            strings = Arrays.asList(main.getValue(attrName).split(","));
            if (name.equalsIgnoreCase(PATCH_CLASSES)) {
               mClassesMap.put(mName, strings);
            } else {
               mClassesMap.put(
                     name.trim().substring(0, name.length() - 8),// remove
                                                      // "-Classes"
                     strings);
            }
         }
      }
   } finally {
      if (jarFile != null) {
         jarFile.close();
      }
      if (inputStream != null) {
         inputStream.close();
      }
   }

}

通过上面分析可知init方法中做的事情是通过版本号判断是否是版本升级来处理缓存目录下的.patch文件
,那么缓存目录中的.patch文件是怎么产生的呢,下面来揭晓。

PatchManager.loadPatch()

我们来看看loadPatch方法里面做了什么事情

public void loadPatch() {
   //这里获取到PathClassLoader并添加到mLoaders集合中
   mLoaders.put("*", mContext.getClassLoader());// wildcard
   Set<String> patchNames;
   List<String> classes;
   //对所有的.classes文件依次进行修改替换(注意这里的.classes文件是经过apkPatch工具
     对比后发生修改的.java文件生成的,并且已经打上@MethodReplace注解标识)
   for (Patch patch : mPatchs) {
      patchNames = patch.getPatchNames();
      for (String patchName : patchNames) {
         classes = patch.getClasses(patchName);
         //修复bug的重点逻辑在这里
         mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),classes);
      }
   }
}

下面我们看看AndFixManager.fix()方法中具体做了哪些事情

public synchronized void fix(File file, ClassLoader classLoader,
      List<String> classes) {
   if (!mSupport) {//是否被andfix所支持
      return;
   }
   //文件签名校验
   if (!mSecurityChecker.verifyApk(file)) {// security check fail
      return;
   }

   try {
      //首次使用时mOptDir文件夹是空的,所有这个optfile文件不存在
      File optfile = new File(mOptDir, file.getName());
      boolean saveFingerprint = true;
      if (optfile.exists()) {
         // need to verify fingerprint when the optimize file exist,
         // prevent someone attack on jailbreak device with
         // Vulnerability-Parasyte.
         // btw:exaggerated android Vulnerability-Parasyte
         // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
         //如果optfile存在,进行文件校验,看看和上次保存的文件是否一致
         //如果一致则下面不需要保存了,如果不一致说明有新的文件需要保存,同时删除旧文件
         if (mSecurityChecker.verifyOpt(optfile)) {
            saveFingerprint = false;
         } else if (!optfile.delete()) {
            return;
         }
      }
      //打开dex文件,将优化后的dex数据保存到optfile文件中,如果优化过的格式已存在并且是最
      //新的就直接使用它,否则虚拟机将尸体重新创建一个
      final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
            optfile.getAbsolutePath(), Context.MODE_PRIVATE);
      //如果是首次进入或有新的.patch文件则将保存下指纹
      if (saveFingerprint) {
         mSecurityChecker.saveOptSig(optfile);
      }

      ClassLoader patchClassLoader = new ClassLoader(classLoader) {
         @Override
         protected Class<?> findClass(String className)
               throws ClassNotFoundException {
            Class<?> clazz = dexFile.loadClass(className, this);
            if (clazz == null
                  && className.startsWith("com.alipay.euler.andfix")) {
               return Class.forName(className);// annotation’s class
                                       // not found
            }
            if (clazz == null) {
               throw new ClassNotFoundException(className);
            }
            return clazz;
         }
      };
      Enumeration<String> entrys = dexFile.entries();
      Class<?> clazz = null;
      //遍历所有发生改变的类文件,找到对应的Class,调用fixClass方法进行修改操作
      while (entrys.hasMoreElements()) {
         String entry = entrys.nextElement();
         if (classes != null && !classes.contains(entry)) {
            continue;// skip, not need fix
         }
         clazz = dexFile.loadClass(entry, patchClassLoader);
         if (clazz != null) {
            fixClass(clazz, classLoader);
         }
      }
   } catch (IOException e) {
      Log.e(TAG, "pacth", e);
   }
}

下面我们看看fixClass()方法里是怎样进行修复操作的

private void fixClass(Class<?> clazz, ClassLoader classLoader) {
   //利用反射获取到当前类中所有声明的方法,然后找到带MethodReplace注解的方法,
   //即是发生变化或新增的方法
   Method[] methods = clazz.getDeclaredMethods();
   MethodReplace methodReplace;
   String clz;
   String meth;
   for (Method method : methods) {
      methodReplace = method.getAnnotation(MethodReplace.class);
      if (methodReplace == null)
         continue;
      clz = methodReplace.clazz();
      meth = methodReplace.method();
      if (!isEmpty(clz) && !isEmpty(meth)) {
         //将通过类名,方法名,新的Method,进行替换操作 
         (classLoader, clz, meth, method);
      }
   }
}

接下来我们看看replaceMethod()方法中是如果进行偷梁换柱的

private void replaceMethod(ClassLoader classLoader, String clz,
      String meth, Method method) {
   try {
      //这里用目标类名和pathClassLoader名称做key存入mFixedClass集合中感觉没有什么卵用
      String key = clz + "@" + classLoader.toString();
      //因为PatchManager.loadPatch方法是在Application的onCreate调用
      //所以这里的mFixedClass每次都是空集合所以clazz为null
      Class<?> clazz = mFixedClass.get(key);
      if (clazz == null) {// class not load
         //找到要替换方法的目标类
         Class<?> clzz = classLoader.loadClass(clz);
         // initialize target class
         //对目标类进行初始操作,这里面具体做的事情是找到该类中的所有成员变量并调用了一个
         //native方法setFiledFlag()做了些什么。。。
         clazz = AndFix.initTargetClass(clzz);
      }
      if (clazz != null) {// initialize class OK
         mFixedClass.put(key, clazz);
         //获取到目标类中的目标方法,然后调用addReplaceMethod方法进行替换操作
         Method src = clazz.getDeclaredMethod(meth,
               method.getParameterTypes());
         AndFix.addReplaceMethod(src, method);
      }
   } catch (Exception e) {
      Log.e(TAG, "replaceMethod", e);
   }
}

最后再看看addReplaceMethod()方法是如何进行旧方法的替换操作的

public static void addReplaceMethod(Method src, Method dest) {
   try {
      //native方法,最终的逻辑是在这里,暂时看不到。。。
      replaceMethod(src, dest);
      //和上面AndFix.initTargetClass()方法中所做的操作一样找到该类中的所有成员变量并调用了
      //一个native方法setFiledFlag()做了些什么。。。难道是首尾呼应
      initFields(dest.getDeclaringClass());
   } catch (Throwable e) {
      Log.e(TAG, "addReplaceMethod", e);
   }
}

上面的整个逻辑的三大步是必须要走的逻辑,其实如果是二次进入并且.patch文件没有发生改变的话上面三步已经完成了方法的修改和替换操作,而不用走最下面一步了

PatchManager.addPatch(patchPath)

这个才是将.patch文件加载到内存中的方法,我们来看看里面做了什么

public void addPatch(String path) throws IOException {
   //sd卡中的.patch文件
   File src = new File(path);
   //内存缓存目录下对应的.patch文件,首次进入肯定不存在
   File dest = new File(mPatchDir, src.getName());
   if(!src.exists()){
      throw new FileNotFoundException(path);
   }
   //二次进入的话内存缓存目录下对应的.patch文件则存在,直接返回
   if (dest.exists()) {
      Log.d(TAG, "patch [" + path + "] has be loaded.");
      return;
   }
   //首次进入和有新的.patch时则需要将.patch文件copy到内存
   FileUtil.copyFile(src, dest);// copy to patch's directory
   //将copy到内存缓存目录下.patch文件转换成Patch然后进行loadPatch的的修复之旅
   Patch patch = addPatch(dest);
   if (patch != null) {
      loadPatch(patch);
   }
}

这一步只有在首次打开app或者是.patch文件发生了改变会走进来,进行初次加载到内存或是替换文件操作,在这个流程中需要注意的是,这里判断的是否有新文件需要替换的依据是比较内存缓存中的.patch文件名称和sd卡中的.patch文件名称是否一致,这也印证了当我们用两个同名的.patch文件去操作时fix功能将失灵了。

总结:其实Java层的方法就是将.patch加载到内存,然后解析找到要替换的方法然后调用native层的replaceMethod方法去做具体的方法替换操作。

Native层代码分析

下面我们来看看native层都做了哪些事情,首先通过java层的方法调用可以看出主要调用了下面三个方法

//在创建AndFixManager的构造方法中判断是否是AndFix支持的系统的同时调用该方法设置了虚拟机类型
private static native boolean setup(boolean isArt, int apilevel);
//方法替换的核心操作
private static native void replaceMethod(Method dest, Method src);
//在AndFix的initFileds方法中找到所有的成员变量,然后循环遍历调用该方法
private static native void setFieldFlag(Field field);

我们用source insight打开Andfix的jni文件夹中的代码来看看
首先,我们看看setup()方法中做了什么

static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart,
        jint apilevel) {
    isArt = isart;
    LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"),
            (int )apilevel);
    if (isArt) {
        return art_setup(env, (int) apilevel);
    } else {
        return dalvik_setup(env, (int) apilevel);
    }
}

这段代码只是设置了成员变量isArt(是否是ART环境)和apilevel(api版本)的值,用于下面的判断。

按照调用顺序的话,接下来要看看setFieldFlag()方法中做了什么操作

static void setFieldFlag(JNIEnv* env, jclass clazz, jobject field) {
    if (isArt) {
        art_setFieldFlag(env, field);
    } else {
        dalvik_setFieldFlag(env, field);
    }
}

这里以Dalvik为例看看dalvik_setFieldFlag()中做了什么

extern void dalvik_setFieldFlag(JNIEnv* env, jobject field) {
    Field* dalvikField = (Field*) env->FromReflectedField(field);
    dalvikField->accessFlags = dalvikField->accessFlags & (~ACC_PRIVATE)
            | ACC_PUBLIC;
    LOGD("dalvik_setFieldFlag: %d ", dalvikField->accessFlags);
}

这里通过反射将传进来的成员变量进行了一系列操作,但是需要注意是AndFix并不支持成员变量的修改及增删操作,如类中成员变量发生变化,在生成.patch文件时就会报错,那么这些代码的意义何在呢?难道是为了以后功能的扩展

//不支持新增或修改成员变量(生成.patch会报异常)
 java.lang.RuntimeException: can,t add new Field:TAG(Ljava/lang/String;),
    in class :Lcom/mime/andfixdemo/MainActivity;
    at com.euler.patch.diff.DiffInfo.addAddedFields(DiffInfo.java:77)
    at com.euler.patch.diff.DexDiffer.compareField(DexDiffer.java:132)
    at com.euler.patch.diff.DexDiffer.compareField(DexDiffer.java:101)
    at com.euler.patch.diff.DexDiffer.compareField(DexDiffer.java:95)
    at com.euler.patch.diff.DexDiffer.diff(DexDiffer.java:32)
    at com.euler.patch.ApkPatch.doPatch(ApkPatch.java:68)
    at com.euler.patch.Main.main(Main.java:97)

最后我们来看看replaceMethod()方法中做了什么

static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
        jobject dest) {
    if (isArt) {
        art_replaceMethod(env, src, dest);
    } else {
        dalvik_replaceMethod(env, src, dest);
    }
}

同样的还是以dalvik环境为例来看看里面的实现逻辑

extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
        JNIEnv* env, jobject src, jobject dest) {
    jobject clazz = env->CallObjectMethod(dest, jClassMethod);
    ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
            dvmThreadSelf_fnPtr(), clazz);
    clz->status = CLASS_INITIALIZED;

    Method* meth = (Method*) env->FromReflectedMethod(src);
    Method* target = (Method*) env->FromReflectedMethod(dest);
    LOGD("dalvikMethod: %s", meth->name);

    meth->clazz = target->clazz;
    meth->accessFlags |= ACC_PUBLIC;
    meth->methodIndex = target->methodIndex;
    meth->jniArgInfo = target->jniArgInfo;
    meth->registersSize = target->registersSize;
    meth->outsSize = target->outsSize;
    meth->insSize = target->insSize;

    meth->prototype = target->prototype;
    meth->insns = target->insns;
    meth->nativeFunc = target->nativeFunc;
}

这里的meth是要替换掉的方法,而target是对应的新方法,而下面这一堆指针的作用是将meth的方法对应的一些属性全部替换为target所对应的属性,最终达到偷梁换柱的目的。

Ps1:native层的代码不是很熟悉,只能看懂大概的意思,如分析有不对之处请及时指出,谢谢!

Ps2: 这里只分析了主要的逻辑源码,还有一些细节问题笔者认为对于理解不太重要没有带大家看,比如把.patch文件后缀名改为.zip解压后会得到META-INF文件夹和diff.dex文件而META-INF中的那些文件都是怎么来的,里面的内容代表什么,有什么意义,把diff.dex反编译后会看到一些以“_CF”结尾的.class文件,文件中@MethodReplace 注解标签是什么时候打上去的…这些疑问有兴趣的小伙伴可以去看看,可以加深对AndFix的理解,总之只有理解他的内部实现才能很好地应用,遇到问题时能更好更快的找到问题的原因。

Ps3:小道消息得知andfix在github上的开源的版本和其内部使用的版本有些不同,阿里集团内部版本总共有3个大神维护,github上开源的版本只有1个大神兼职维护及更新。

问题汇总(持续更新…)

使用注意事项

1.仅仅只是支持方法的新增或修改
2.不支持新增或修改成员变量(生成.patch会报异常)
3.不支持新增类 (生成.patch时正常,运行时报错——崩溃掉了)
4.源生工具不支持 multidex (可以通过JavaAssist工具修改apkpath这个包,来达到支持mutidex的目的)
5.支持在新增方法中增加的局部变量
6.不支持资源文件的增删及修改
7.强烈欢迎大家一起讨论发掘问题

问题汇总

1.目前andfix只编译armeabi和x86架构的so

解决办法:在build.gradle中加入如下代码

defaultConfig {
    ndk {
        abiFilters 'armeabi','x86'
    }
}

2.与其他jar包之间的冲突问题

场景:修复app版本1后,顺利生成差分包,但是当app运行加载.patch文件时报错,分析查看.patch均正常,修复bug前后的apk版本均能正常运行。错误排查原因极有可能是与项目中其他jar包的冲突导致的

解决办法:分析中。。。

3.在修复bug时重写父类中回调的方法时无效

场景:修复bug,因没有实时申请获取联系人权限。所以在6.0以下手机系统中可正常运行,但是在6.0及以上系统会直接崩溃,修复bug时在类中重写了权限授权提示后的回调方法onRequestPermissionsResult(),但是在生成差分文件时,andfix认为该方法是新增方法,因此没有添加“MethodReplace”标签,所以在应用.patch文件时onRequestPermissionsResult被认为是一个新增的普通方法,而在授权回调时仍然走的是父类的没有被重写过的方法

智能座舱音频系统集成与调试:从芯片选型到音质优化实战 在智能座舱域控制器架构中,音频系统已从简单的功放加扬声器演进为SoC、数字音频总线、外部DSP与多通道功放协同的复杂子系统。TDM总线的时隙分配、PDM麦克风阵列的信号完整性,以及AEC回声消除算法如何与硬件协同,直接决定多音区体验和语音交互的可靠性。工程实践中,音频调试的关键在于先理顺供电、接地、时钟与数据路由,再针对功放削波、采样率失配、TDM slot错位等典型问题进行系统性排查。从芯片选型到系统级指标验收,只有将音频链路视为整车电子电气架构的一个协同整体,才能真正实现从“响”到“好听”的跨越。本文结 阅读详情

相关推荐

内网开发必备:银河麒麟V10离线安装全流程(含镜像挂载+yum源配置)

本文详细介绍了在内网隔离环境下,银河麒麟V10操作系统的离线安装与部署全流程。核心内容包括官方镜像的获取与校验、使用mount命令进行镜像挂载的详细步骤与问题排查,以及构建稳定、可维护的本地Yum软件仓库的完整方法论。通过实战案例演示了如何基于本地源部署LNMP环境,并提供了高级配置技巧与常见故障排除指南,旨在帮助运维人员系统性地构建高可用的内网软件供应链。

weixin_29268637的博客 561

为博客添加樱花飘落的效果

偶然看见别人博客中樱花飘落的效果,感觉很棒,于是我也给博客加上了樱花 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 新建 sakura.js 新建一个 sakura.js 文件,在里面填入下面的代码。代码来自于 折月’s Blog ,在此表示感谢。 var stop, staticx; var img = new Image(); img.src = "data:ima...

qq_36290227的博客 4万+

在 Ansys CFX Pre 中配置 RGP 表的分步指南

掌握在 Ansys CFX Pre 中设置 RGP 表的技巧,以优化仿真精度和效率。

jkh920184196的博客 1664

Spring MVC 获取方法参数的名称

如果我们希望Spring在@RequestParam(或@PathVariable或@RequestHeader)没有显式指定名称的情况下自动将查询参数名称与其进行匹配,即在@RequestParam(“myRequestParamName”)中未指定值,则代码在编译时需要包含调试信息(debugging information)或带有-parameters 编译标志(-parameters compiler flag)。 尝试不使用调试信息或-parameter标志进行显式编译的情况。 Controlle

CmdSmith的博客 934

ThingsBoard 3.1.1版本在window本地运行之TB-Gateway服务搭建(三)

TB-gateway服务是TB不可或缺的部分,很多的协议都需要TB-gateway来做分类处理,今天我们就来做TB-Gateway服务搭建。

晚点遇见你,余生都是你! 2474

从零搭建UVM验证环境:新手入门实践指南

在数字芯片验证领域,SystemVerilog和UVM(Universal Verification Methodology)是构建高效验证环境的核心技术。其原理基于面向对象编程和事务级建模,通过组件化架构实现验证的复用性和自动化。这一方法学的技术价值在于显著提升验证效率与质量,广泛应用于处理器、通信芯片等复杂设计的验证场景。本文聚焦于UVM验证环境的搭建实践,通过一个加法器实例,详细解析了uvm_driver、uvm_monitor、uvm_sequence等关键组件的协同工作机制,并提供了基于Quest

weixin_30015371的博客 446

安卓逆向系列教程 4.4 逆向云播 VIP

4.4 逆向云播 VIP 作者:飞龙 这次的软件是刀哥云播,在这里下载:http://www.xuepojie.com/thread-23860-1-1.html我们先分析一下行为:打开之后的界面是这样,并弹出一行消息:我们先点击“福利”,弹出这样一行字。然后我们点击那个“设置”,在设置界面上,我们如果点击“加入会员”,弹出这样一行消息。然后我们将其拖进 Android Killer,发现它是

龙哥盟 4万+

FastApi教程

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于万维网(WWW:World Wide Web )服务器与本地浏览器之间传输超文本的传送协议。HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

小石潭记丶 1562

spring源码下载安装,导入idea以及编译报错问题详细解决过程

参考 《spring源码深度解析 第2版》

m0_67394360的博客 2646

使用MATLAB和Simulink来设计并仿真一个智能家居火灾报警系统

智能家居火灾报警系统旨在通过烟雾传感器、温度传感器等设备监测家庭环境,一旦检测到异常情况(如烟雾或温度骤升),立即触发警报并采取相应措施。在这个教程中,我们将使用MATLAB和Simulink来设计并仿真一个智能家居火灾报警系统,包括数据采集、处理以及响应机制。

amy_mhd的博客 162

UNet改进(24):注意力机制-从基础原理到高级融合策略

本文系统探讨了如何通过引入各类注意力机制增强UNet在医学图像分割中的性能。文章分为基础篇、进阶篇和高级篇三个层级:基础篇介绍了时序注意力、可变形注意力、分层注意力等8种基础机制;进阶篇提出了5种混合注意力设计;高级篇则探讨了全局+局部+多尺度等复杂组合机制。实验表明,这些注意力模块在心脏MRI、肺部感染等任务中能提升2-15%的分割精度。文章还提供了硬件适配建议和完整实现代码,为研究者提供了全面的技术参考。未来方向包括NAS优化、量子注意力等前沿探索,为UNet在医疗影像等领域的应用革新提供了理论指导。

2501_91516851的博客 764

某地理位置模拟APP从壳流程分析到破解

工具与环境 Xposed IDA 6.8 JEB 2.2.5 Fiddler2 010Editor NEXUS 5 Android 4.4 好久不玩逆向怕调试器生锈,拿出来磨磨! 高手莫要见笑,仅供小菜玩乐,有不对或不足的地方还请多多指教,不胜感激! 0x00: 程序大概情况分析 在我们拿到一个APP准备破解时一般得安装运行,程序运行后须要注册用户,随便注册一个用户登录,以下...

weixin_30539835的博客 1053

装配完整性检查系统:基于YOLOv10的螺丝/零件缺失自动检测(含UI界面与完整代码)

建议自行采集装配场景数据,标注缺失螺丝、错误零件等,模拟真实缺陷。手机/工业相机采集图像;使用LabelImg进行目标标注;转换为 YOLO 格式。本文构建了一个完整的装配完整性检测系统,基于 YOLOv10 实现高精度螺丝缺失检测,并使用 PyQt5 构建了图形界面,实现一键导入与可视化检测流程,适用于工业质检自动化部署。项目可灵活扩展至零件缺失、错装、错位、顺序错误等检测场景,是深度学习在制造业中落地的典型应用案例之一。

YOLO 701

Android实战——AndFix的使用与源码分析

前言热修复是今年比较流行的话题,想要进阶高级工程师是必备的知识,其实他也要有一定的Android实力才能读懂源码,涉及到类加载机制,dex加载等等,不过想要进阶高级的你,是不可不学的一门技术哦AndFix介绍AndFix是阿里开源的一个Android热补丁框架,允许APP在不重新发布版本的情况下修复线上的bug,目前支持2.3-7.0,具体介绍可以查看github项目其实现步骤 集成AndFix 生

点击置顶文章查看博客目录(全站式导航) 1526

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

15.AndFix热修复的使用和源码分析(服务端生成差分包的逻辑)

未完.待续....

weixin_33806509的博客 124

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

AndFix源码分析

写在前面Github地址镇楼:AndFix首先介绍一下AndFix及其使用方法,然后根据使用流程对其内部实现进行分析。AndFix介绍首先我们看一下不同热修复框架的功能特性对比:特性A...

企鹅肖 263

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

14.AndFix热修复的使用和源码分析(客户端修复逻辑)

使用方法: 1.下载AndFix源码:https://github.com/alibaba/AndFix 2.生成两个apk文件,一个是含有bug的old.apk 一个是修复之后的new.apk 3.进入AndFix目录中的tools文件夹找到apkpatch工具,这个工具负责生成差分包,将它解压 4.在命令...

weixin_33831673的博客 161

Android中热修复框架AndFix原理解析及案例使用

一、前言 最近腾讯弄出一个Tinker热修复框架,那么本文先不介绍这个框架,先来介绍一下阿里的一个热修复框架AndFix,这个框架出来已经很长时间了,但是看网上没有太多非常详细的讲解,这里就来做一次分析。正好项目中要使用到。首先这个框架是开源的,可以在github上搜:AndFix。其实在最早的时候我已经分析了阿里的另外一个热修复框架:Dexposed框架,还不了解的同学可以点击这里查看:De...

尼古拉斯.赵四的博客 488

Android热修复框架AndFix原理解析及使用

一、前言 最近腾讯弄出一个Tinker热修复框架,那么本文先不介绍这个框架,先来介绍一下阿里的一个热修复框架AndFix,这个框架出来已经很长时间了,但是看网上没有太多非常详细的讲解,这里就来做一次分析。正好项目中要使用到。首先这个框架是开源的,可以在github上搜:AndFix。其实在最早的时候我已经分析了阿里的另外一个热修复框架:Dexposed框架,还不了解的同学可以点击这里查看:Dexposed框架原理解析以及使用当时介绍这个框架的时候发现他的实现原理很简单: ​ 他的思想完全来源于Xp..

尼古拉斯.赵四的博客 997

Android热修复实战之AndFix

写在前面 上一篇《Android热修复技术简介》中对Android的热修复技术的概念和常用的技术方案做了一个简单的介绍,那么今天就来实战一下热修复技术,我们使用的是AndFix,为什么是它?因为无论是从使用上还是原理上AndFix都是相对简单的,毕竟这是实战的第一篇,还是要有个由易到难的过程的,好了,话不多说,开始吧! 一、AndFix基本介绍 1.1、AndFix简介 AndFix项目地址:https://github.com/alibaba/AndFix,大家访问这个地址去看它的详细介绍,我这里

江湖人陆仁甲 829

android热修复--阿里热修复使用和源码分析

android热修复–阿里热修复使用和源码分析AndFix全称Android hot-fix,是alibaba的Android热修复框架,支持Android 2.3到7.0的版本,支持arm与X86系统架构,支持Dalvik和ART Runtime。原理AndFix的原理就是方法的替换,把有bug的方法替换成补丁文件中的方法。使用1.添加依赖和混肴maven:<dependency> <gr

a847427920的博客 608

第五章 Android动态加载、热更新、热修复、插件化系列文章 之 Andfix介绍

第五章 android热更新系列文章 之 Andfix介绍 目录 1. 概述 2. Andfix修复的整体流程 3. Andfix框架存在的缺陷 4. Andfix源码分析 1. 概述 -Andfix的基本介绍 AndFix,全称是Android hot-fix。是阿里开源的一个热补丁框架,允许APP在不重新发布版本的情况下修复线上的bug。支持Android...

bencheng06的博客 362

Alibaba-Dexposed Bug框架原理及源码解析

目录(?)[+] Alibaba的AndFix热修复:  Alibaba-AndFix Bug热修复框架的使用  Alibaba-AndFix Bug热修复框架原理及源码解析 上一篇中已经介绍了Alibaba-Dexposed框架在线热补丁修复的使用 ,这篇主要是了解框架的原理和源码解析。 原理: 在Dalvik虚拟机下,主要是通过改变一个方法对象方法在Dalvik虚拟

omnispace的博客 677

AndFix 源码分析之一

1, 概念 AndFix是阿里巴巴的一个开源热修复框架,该框架使用方便,结构简单。 开发时,首先使用eclipse打开该框架,在activity中添加如下代码就可以完成开发。 private static final String APATCH_PATH = "/fix.apatch"; public static PatchManager mPatchManager;// 最好是单

Jack的博客 673
上一篇: 获取Android应用专属缓存存储目录
下一篇: Android_IPC机制知识梳理
wangShanCode
博客等级 码龄10年 4粉丝 13原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值