Android热修复学习之旅——Andfix框架完全解析

2024-06-24 05:32

本文主要是介绍Android热修复学习之旅——Andfix框架完全解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Android热修复学习之旅开篇——热修复概述
Android热修复学习之旅——HotFix完全解析
Android热修复学习之旅——Tinker接入全攻略

在之前的博客《Android热修复学习之旅——HotFix完全解析》中,我们学习了热修复的实现方式之一,通过dex分包方案的原理还有HotFix框架的源码分析,本次我将讲解热修复的另外一种思路,那就是通过native方法,使用这种思路的框架代表就是阿里的Andfix,本篇博客,我们将深入分析Andfix的实现。

Andfix的使用

下面一段代码就是Andfix的使用代码,为了方便大家理解,重要内容已进行注释

public class MainApplication extends Application {private static final String TAG = "euler";private static final String APATCH_PATH = "/out.apatch";//被修复的文件都是以.apatch结尾/*** patch manager*/private PatchManager mPatchManager;@Overridepublic void onCreate() {super.onCreate();// initialize//初始化PatchManager,也就是修复包的管理器,因为修复包可能有多个,所以这里需要一个管理器进行管理mPatchManager = new PatchManager(this);mPatchManager.init("1.0");Log.d(TAG, "inited.");// load patch//开始加载修复包mPatchManager.loadPatch();Log.d(TAG, "apatch loaded.");// add patch at runtimetry {// .apatch file path//存放patch补丁文件的路径,这里使用的sd卡,真实项目中肯定是从服务器下载到sd卡中String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;mPatchManager.addPatch(patchFileString);Log.d(TAG, "apatch:" + patchFileString + " added.");} catch (IOException e) {Log.e(TAG, "", e);}}
}

其实就是通过一个PatchManager加载修复包,接下来我们分析一下PatchManager的代码

/*** @param context*            context*/
public PatchManager(Context context) {mContext = context;//初始化AndFixManagermAndFixManager = new AndFixManager(mContext);//初始化存放patch补丁文件的目录mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存在Patch类的集合mPatchs = new ConcurrentSkipListSet<Patch>();//初始化存放类对应的类加载器集合mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}

里面很重要的类就是AndFixManager,接下来我们看一下AndFixManager的初始化代码

public AndFixManager(Context context) {mContext = context;//判断Android机型是否适支持AndFixmSupport = Compat.isSupport();if (mSupport) {//初始化签名安全判断类,此类主要是进行修复包安全校验的工作mSecurityChecker = new SecurityChecker(mContext);//初始化patch文件存放的目录mOptDir = new File(mContext.getFilesDir(), DIR);if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory failmSupport = false;Log.e(TAG, "opt dir create error.");} else if (!mOptDir.isDirectory()) {// not directory//如果不是文件目录就删除mOptDir.delete();mSupport = false;}}

概括一下AndFixManager的初始化,主要做了以下的工作:
1.判断Android机型是否适支持AndFix,
2.初始化修复包安全校验的工作

Andfix源码分析

首先看一下isSupport方法内部的逻辑

public static synchronized boolean isSupport() {if (isChecked)return isSupport;isChecked = true;// not support alibaba's YunOsif (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) {isSupport = true;}if (inBlackList()) {isSupport = false;}return isSupport;
}

可以看到判断的条件主要是3个:
1.判断系统是否是YunOs系统

@SuppressLint("DefaultLocale")
private static boolean isYunOS() {String version = null;String vmName = null;try {Method m = Class.forName("android.os.SystemProperties").getMethod("get", String.class);version = (String) m.invoke(null, "ro.yunos.version");vmName = (String) m.invoke(null, "java.vm.name");} catch (Exception e) {// nothing todo}if ((vmName != null && vmName.toLowerCase().contains("lemur"))|| (version != null && version.trim().length() > 0)) {return true;} else {return false;}
}

2.判断是Dalvik还是Art虚拟机,来注册Native方法

/*** initialize* * @return true if initialize success*/
public static boolean setup() {try {final String vmVersion = System.getProperty("java.vm.version");boolean isArt = vmVersion != null && vmVersion.startsWith("2");int apilevel = Build.VERSION.SDK_INT;return setup(isArt, apilevel);} catch (Exception e) {Log.e(TAG, "setup", e);return false;}
}

如果版本符合的话,会调用native的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);}
}

同样在jboolean setup中分为art_setup和dalvik_setup

art_setup方法

extern jboolean __attribute__ ((visibility ("hidden"))) art_setup(JNIEnv* env,int level) {apilevel = level;return JNI_TRUE;
}

dalvik_setup方法

extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(JNIEnv* env, int apilevel) {//打开系统的"libdvm.so"文件void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);if (dvm_hand) {//获取dvmDecodeIndirectRef_fnPtr和dvmThreadSelf_fnPtr俩个函数//这两个函数可以通过类对象获取ClassObject结构体dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,apilevel > 10 ?"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :"dvmDecodeIndirectRef");if (!dvmDecodeIndirectRef_fnPtr) {return JNI_FALSE;}dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");if (!dvmThreadSelf_fnPtr) {return JNI_FALSE;}//通过Java层Method对象的getDeclaringClass方法//后续会调用该方法获取某个方法所属的类对象//因为Java层只传递了Method对象到native层jclass clazz = env->FindClass("java/lang/reflect/Method");jClassMethod = env->GetMethodID(clazz, "getDeclaringClass","()Ljava/lang/Class;");return JNI_TRUE;} else {return JNI_FALSE;}
}

主要做了两件事,准备后续的replaceMethod函数中使用:
1、在libdvm.so动态获取dvmDecodeIndirectRef_fnPtr函数指针和获取dvmThreadSelf_fnPtr函数指针。
2、调用dest的 Method.getDeclaringClass方法获取method的类对象clazz。

3.根据sdk版本判断是否支持(支持Android2.3-7.0系统版本)

// from android 2.3 to android 7.0
private static boolean isSupportSDKVersion() {if (android.os.Build.VERSION.SDK_INT >= 8&& android.os.Build.VERSION.SDK_INT <= 24) {return true;}return false;
}

然后我们看一下初始化签名安全判断类的代码

public SecurityChecker(Context context) {mContext = context;init(mContext);
}

init方法要是获取当前应用的签名及其他信息,为了判断与patch文件的签名是否一致

// initialize,and check debuggable
//主要是获取当前应用的签名及其他信息,为了判断与patch文件的签名是否一致
private void init(Context context) {try {PackageManager pm = context.getPackageManager();String packageName = context.getPackageName();PackageInfo packageInfo = pm.getPackageInfo(packageName,PackageManager.GET_SIGNATURES);CertificateFactory certFactory = CertificateFactory.getInstance("X.509");ByteArrayInputStream stream = new ByteArrayInputStream(packageInfo.signatures[0].toByteArray());X509Certificate cert = (X509Certificate) certFactory.generateCertificate(stream);mDebuggable = cert.getSubjectX500Principal().equals(DEBUG_DN);mPublicKey = cert.getPublicKey();} catch (NameNotFoundException e) {Log.e(TAG, "init", e);} catch (CertificateException e) {Log.e(TAG, "init", e);}
}

接下来是分析mPatchManager.init方法

public void init(String appVersion) {if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory failLog.e(TAG, "patch dir create error.");return;} else if (!mPatchDir.isDirectory()) {// not directorymPatchDir.delete();return;}//使用SP存储关于patch文件的信息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 {//初始化patch列表,把本地的patch文件加载到内存initPatchs();}
}

主要是进行版本号的对比,如果不一致则删除本地所有的patch文件,同时保存新的版本号,否则就直接把本地的patch文件加载到内存

private void cleanPatch() {File[] files = mPatchDir.listFiles();for (File file : files) {//删除所有的本地缓存patch文件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);}
}
/*** add patch file* * @param file* @return patch*/
private Patch addPatch(File file) {Patch patch = null;if (file.getName().endsWith(SUFFIX)) {try {//创建Patch对象patch = new Patch(file);//把patch实例存储到内存的集合中,在PatchManager实例化集合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 {//使用JarFile读取Patch文件jarFile = new JarFile(mFile);//获取META-INF/PATCH.MF文件JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);inputStream = jarFile.getInputStream(entry);Manifest manifest = new Manifest(inputStream);Attributes main = manifest.getMainAttributes();//获取PATCH.MF文件中的属性Patch-NamemName = main.getValue(PATCH_NAME);//获取PATCH.MF属性Created-TimemTime = new Date(main.getValue(CREATED_TIME));mClassesMap = new HashMap<String, List<String>>();Attributes.Name attrName;String name;List<String> strings;for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {attrName = (Attributes.Name) it.next();name = attrName.toString();//判断name的后缀是否是-Classes,并把name对应的值加入到集合中,对应的值就是class类名的列表if (name.endsWith(CLASSES)) {strings = Arrays.asList(main.getValue(attrName).split(","));if (name.equalsIgnoreCase(PATCH_CLASSES)) {mClassesMap.put(mName, strings);} else {mClassesMap.put(//为了移除掉"-Classes"的后缀name.trim().substring(0, name.length() - 8),// remove// "-Classes"strings);}}}} finally {if (jarFile != null) {jarFile.close();}if (inputStream != null) {inputStream.close();}}}

init方法主要的逻辑就是通过读取.patch文件,每个修复包apatch文件其实都是一个jarFile文件,然后获得其中META-INF/PATCH.MF文件,PATCH.MF文件中都是key-value的形式,获取key是-Classes的所有的value,这些value就是所有要修复的类,他们是以“,”进行分割的,将它们放入list列表,将其存储到一个集合中mClassesMap,list列表中存储的就是所有要修复的类名

还有另一个addpath方法,接受的是文件路径参数:

/*** add patch at runtime* * @param path*            patch path* @throws IOException*/
public void addPatch(String path) throws IOException {File src = new File(path);File dest = new File(mPatchDir, src.getName());if(!src.exists()){throw new FileNotFoundException(path);}if (dest.exists()) {Log.d(TAG, "patch [" + path + "] has be loaded.");return;}//把文件拷贝到专门存放patch文件的文件夹中FileUtil.copyFile(src, dest);// copy to patch's directoryPatch patch = addPatch(dest);if (patch != null) {//使用loadPatch进行加载loadPatch(patch);}
}

总结一下两个addPatch方法的不同之处:

addPatch(file)方法:需要结合上面的initPatchs方法一起使用,他调用的场景是:本地mPatchDir目录中已经有了修复包文件,并且版本号没有发生变化,这样每次启动程序的时候就会调用初始化操作,在这里会遍历mPatchDir目录中所有的修复包文件,然后调用这个方法添加到全局文件列表中,也即是mPatchs中。

addPatch(String path)方法:这个方法使用的场景是版本号发生变化,或者是本地目录中没有修复包文件。比如第一次操作的时候,会从网络上下载修复包文件,下载成功之后会把这个文件路径通过这个方法调用即可,执行完之后也会主动调用加载修复包的操作了,比如demo中第一次在SD卡中放了一个修复包文件:

// add patch at runtime
try {// .apatch file path//存放patch补丁文件的路径,这里使用的sd卡,真实项目中肯定是从服务器下载到sd卡中String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;mPatchManager.addPatch(patchFileString);Log.d(TAG, "apatch:" + patchFileString + " added.");
} catch (IOException e) {Log.e(TAG, "", e);
}

接下来,看一下mPatchManager.loadPatch();

/*** load patch,call when application start* */
public void loadPatch() {mLoaders.put("*", mContext.getClassLoader());// wildcardSet<String> patchNames;List<String> classes;for (Patch patch : mPatchs) {patchNames = patch.getPatchNames();for (String patchName : patchNames) {//获取patch对应的class类的集合Listclasses = patch.getClasses(patchName);//调用mAndFixManager.fix修复bugmAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),classes);}}
}

这个方法主要是通过Patch类获取修复包所有的修复类名称,之前已经介绍了Patch类的初始化操作,在哪里会解析修复包的MF文件信息,获取到修复包需要修复的类名然后保存到列表中,这里就通过getClasses方法来获取指定修复包名称对应的修复类名称列表,然后调用AndFixManager的fix方法

接下来就是分析mAndFixManager.fix方法

/*** fix* * @param patchPath*            patch path*/
public synchronized void fix(String patchPath) {fix(new File(patchPath), mContext.getClassLoader(), null);
}
*** fix* * @param file*            patch file* @param classLoader*            classloader of class that will be fixed* @param classes*            classes will be fixed*/
public synchronized void fix(File file, ClassLoader classLoader,List<String> classes) {if (!mSupport) {return;}//判断patch文件的签名,检查修复包的安全性if (!mSecurityChecker.verifyApk(file)) {// security check failreturn;}try {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.htmlif (mSecurityChecker.verifyOpt(optfile)) {saveFingerprint = false;} else if (!optfile.delete()) {return;}}//使用dexFile 加载修复包文件,所以说patch文件其实本质是dex文件final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),optfile.getAbsolutePath(), Context.MODE_PRIVATE);if (saveFingerprint) {mSecurityChecker.saveOptSig(optfile);}//这里重新new了一个ClasLoader,并重写findClass方法ClassLoader patchClassLoader = new ClassLoader(classLoader) {@Overrideprotected 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;while (entrys.hasMoreElements()) {String entry = entrys.nextElement();if (classes != null && !classes.contains(entry)) {continue;// skip, not need fix}//加载有bug的类文件clazz = dexFile.loadClass(entry, patchClassLoader);if (clazz != null) {//fixClass方法对有bug的文件进行替换fixClass(clazz, classLoader);}}} catch (IOException e) {Log.e(TAG, "pacth", e);}
}

概括一下fix方法做的几件事:
1.使用mSecurityChecker进行修复包的校验工作,这里的校验就是比对修复包的签名和应用的签名是否一致:

/*** @param path*            Apk file* @return true if verify apk success*/
public boolean verifyApk(File path) {if (mDebuggable) {Log.d(TAG, "mDebuggable = true");return true;}JarFile jarFile = null;try {jarFile = new JarFile(path);JarEntry jarEntry = jarFile.getJarEntry(CLASSES_DEX);if (null == jarEntry) {// no codereturn false;}loadDigestes(jarFile, jarEntry);Certificate[] certs = jarEntry.getCertificates();if (certs == null) {return false;}return check(path, certs);} catch (IOException e) {Log.e(TAG, path.getAbsolutePath(), e);return false;} finally {try {if (jarFile != null) {jarFile.close();}} catch (IOException e) {Log.e(TAG, path.getAbsolutePath(), e);}}
}

2.使用DexFile和自定义类加载器来加载修复包文件

//这里重新new了一个ClasLoader,并重写findClass方法
ClassLoader patchClassLoader = new ClassLoader(classLoader) {@Overrideprotected 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;
while (entrys.hasMoreElements()) {String entry = entrys.nextElement();if (classes != null && !classes.contains(entry)) {continue;// skip, not need fix}//加载修复包patch中的文件信息,获取其中要修复的类名,然后进行加载clazz = dexFile.loadClass(entry, patchClassLoader);if (clazz != null) {//fixClass方法对有bug的文件进行替换fixClass(clazz, classLoader);}
}

这里创建一个新的classLoader的原因是,我们需要获取修复类中bug的方法名称,而这个方法名称是通过修复方法的注解来获取到的,所以得先进行类的加载然后获取到他的方法信息,最后通过分析注解获取方法名,这里用的是反射机制来进行操作的。使用自定义的classLoader为了过滤我们需要加载的类

接下来是fixClass方法的逻辑

/*** fix class* * @param clazz*            class*/
private void fixClass(Class<?> clazz, ClassLoader classLoader) {Method[] methods = clazz.getDeclaredMethods();MethodReplace methodReplace;String clz;String meth;for (Method method : methods) {//遍历所有的方法,获取方法的注解,因为有bug的方法在生成的patch的类中的方法都是有注解的methodReplace = method.getAnnotation(MethodReplace.class);if (methodReplace == null)continue;//获取注解中clazz的值clz = methodReplace.clazz();//获取注解中method的值meth = methodReplace.method();if (!isEmpty(clz) && !isEmpty(meth)) {//进行替换replaceMethod(classLoader, clz, meth, method);}}
}

通过反射获取指定类名需要修复类中的所有方法类型,然后在获取对应的注解信息,上面已经分析了通过DexFile加载修复包文件,然后在加载上面Patch类中的getClasses方法获取到的修复类名称列表来进行类的加载,然后在用反射机制获取类中所有的方法对应的注解信息,通过注解信息获取指定修复的方法名称,看一下注解的定义:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodReplace {String clazz();String method();
}

两个方法:一个是获取当前类名称,一个是获取当前方法名称

/*** replace method* * @param classLoader classloader* @param clz class* @param meth name of target method * @param method source method*/
private void replaceMethod(ClassLoader classLoader, String clz,String meth, Method method) {try {String key = clz + "@" + classLoader.toString();//判断此类是否已经被fixClass<?> clazz = mFixedClass.get(key);if (clazz == null) {// class not loadClass<?> clzz = classLoader.loadClass(clz);// initialize target classclazz = AndFix.initTargetClass(clzz);//初始化class}if (clazz != null) {// initialize class OKmFixedClass.put(key, clazz);//根据反射获取到有bug的类的方法(有bug的apk)Method src = clazz.getDeclaredMethod(meth,method.getParameterTypes());//src是有bug的方法,method是补丁方法AndFix.addReplaceMethod(src, method);}} catch (Exception e) {Log.e(TAG, "replaceMethod", e);}
}

这里说明一下,获得有bug方法的这段代码:

Method src = clazz.getDeclaredMethod(meth,method.getParameterTypes());

通过方法名和本地已有的该方法的参数信息获取有bug的方法,然后将有bug的方法和修复的方法一起传入进行修复
注意:上面的操作,传入的是修复新的方法信息以及需要修复的旧方法名称,不过这里得先获取到旧方法类型,可以看到修复的新旧方法的签名必须一致,所谓签名就是方法的名称,参数个数,参数类型都必须一致,不然这里就报错的。进而也修复不了了。

接下来就是交给native方法了,由于Android4.4后才用的Art虚拟机,之前的系统都是Dalvik虚拟机,因此Natice层写了2个方法,对不同的系统做不同的处理方式。

#andfix.cpp
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 replaceMethod的实现:

extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(JNIEnv* env, jobject src, jobject dest) {jobject clazz = env->CallObjectMethod(dest, jClassMethod);//ClassObject结构体包含很多信息,在native中这个值很有用ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(dvmThreadSelf_fnPtr(), clazz);clz->status = CLASS_INITIALIZED;//更改状态为类初始化完成的状态//通过java层传递的方法对象,在native层获得他们的结构体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;
}

简单来说,就是通过上层传递过来的新旧方法类型对象,通过JNIEnv的FromReflectedMethod方法获取对应的方法结构体信息,然后将其信息进行替换即可

其余art的native方法,读者可以自行阅读,因为原理也是差不多.

如何生成patch包

细心的同学发现,我们还没说如何生成patch包,可以通过apatch进行生成
使用神器apatch进行线上发布的release包和这次修复的fix包进行比对,获取到修复文件apatch

java -jar apkpatch.jar -f app-release-fix.apk -t app-release-online.apk -o C:\Users\mayu-g\Desktop\apkpatch-1.0.3 -k myl.keystore -p 123456 -a mayunlong -e 123456

使用命令的时候需要用到签名文件,因为在前面分析代码的时候知道会做修复包的签名验证。这里得到了一个修复包文件如下:
这里写图片描述
而且会产生一个diff.dex文件和smali文件夹,而我们用压缩软件可以打开apatch文件看看:

这里写图片描述
可以看到这里的classes.dex文件其实就是上面的diff.dex文件,只是这里更像是Android中的apk文件目录格式,同样有一个META-INF目录,这里存放了签名文件以及需要修复类信息的PATCH.MF文件:
这里写图片描述

至此,Andfix框架已基本分析完毕。

这篇关于Android热修复学习之旅——Andfix框架完全解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089292

相关文章

51单片机学习记录———定时器

文章目录 前言一、定时器介绍二、STC89C52定时器资源三、定时器框图四、定时器模式五、定时器相关寄存器六、定时器练习 前言 一个学习嵌入式的小白~ 有问题评论区或私信指出~ 提示:以下是本篇文章正文内容,下面案例可供参考 一、定时器介绍 定时器介绍:51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。 定时器作用: 1.用于计数系统,可

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

[word] word设置上标快捷键 #学习方法#其他#媒体

word设置上标快捷键 办公中,少不了使用word,这个是大家必备的软件,今天给大家分享word设置上标快捷键,希望在办公中能帮到您! 1、添加上标 在录入一些公式,或者是化学产品时,需要添加上标内容,按下快捷键Ctrl+shift++就能将需要的内容设置为上标符号。 word设置上标快捷键的方法就是以上内容了,需要的小伙伴都可以试一试呢!

AssetBundle学习笔记

AssetBundle是unity自定义的资源格式,通过调用引擎的资源打包接口对资源进行打包成.assetbundle格式的资源包。本文介绍了AssetBundle的生成,使用,加载,卸载以及Unity资源更新的一个基本步骤。 目录 1.定义: 2.AssetBundle的生成: 1)设置AssetBundle包的属性——通过编辑器界面 补充:分组策略 2)调用引擎接口API

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

大学湖北中医药大学法医学试题及答案,分享几个实用搜题和学习工具 #微信#学习方法#职场发展

今天分享拥有拍照搜题、文字搜题、语音搜题、多重搜题等搜题模式,可以快速查找问题解析,加深对题目答案的理解。 1.快练题 这是一个网站 找题的网站海量题库,在线搜题,快速刷题~为您提供百万优质题库,直接搜索题库名称,支持多种刷题模式:顺序练习、语音听题、本地搜题、顺序阅读、模拟考试、组卷考试、赶快下载吧! 2.彩虹搜题 这是个老公众号了 支持手写输入,截图搜题,详细步骤,解题必备

解析 XML 和 INI

XML 1.TinyXML库 TinyXML是一个C++的XML解析库  使用介绍: https://www.cnblogs.com/mythou/archive/2011/11/27/2265169.html    使用的时候,只要把 tinyxml.h、tinystr.h、tinystr.cpp、tinyxml.cpp、tinyxmlerror.cpp、tinyxmlparser.

《offer来了》第二章学习笔记

1.集合 Java四种集合:List、Queue、Set和Map 1.1.List:可重复 有序的Collection ArrayList: 基于数组实现,增删慢,查询快,线程不安全 Vector: 基于数组实现,增删慢,查询快,线程安全 LinkedList: 基于双向链实现,增删快,查询慢,线程不安全 1.2.Queue:队列 ArrayBlockingQueue:

硬件基础知识——自学习梳理

计算机存储分为闪存和永久性存储。 硬盘(永久存储)主要分为机械磁盘和固态硬盘。 机械磁盘主要靠磁颗粒的正负极方向来存储0或1,且机械磁盘没有使用寿命。 固态硬盘就有使用寿命了,大概支持30w次的读写操作。 闪存使用的是电容进行存储,断电数据就没了。 器件之间传输bit数据在总线上是一个一个传输的,因为通过电压传输(电流不稳定),但是电压属于电势能,所以可以叠加互相干扰,这也就是硬盘,U盘

Eclipse+ADT与Android Studio开发的区别

下文的EA指Eclipse+ADT,AS就是指Android Studio。 就编写界面布局来说AS可以边开发边预览(所见即所得,以及多个屏幕预览),这个优势比较大。AS运行时占的内存比EA的要小。AS创建项目时要创建gradle项目框架,so,创建项目时AS比较慢。android studio基于gradle构建项目,你无法同时集中管理和维护多个项目的源码,而eclipse ADT可以同时打开