【35】Android 虚拟机与类加载机制

(1)一个人只要自己不放弃自己,整个世界也不会放弃你.
(2)天生我才必有大用
(3)不能忍受学习之苦就一定要忍受生活之苦,这是多么痛苦而深刻的领悟.
(4)做难事必有所得
(5)精神乃真正的刀锋
(6)战胜对手有两次,第一次在内心中.
(7)好好活就是做有意义的事情.
(8)亡羊补牢,为时未晚
(9)科技领域,没有捷径与投机取巧。
(10)有实力,一年365天都是应聘的旺季,没实力,天天都是应聘的淡季。
(11)基础不牢,地动天摇
(12)写博客初心:成长自己,辅助他人。当某一天离开人世,希望博客中的思想还能帮人指引方向.
(13)编写实属不易,若喜欢或者对你有帮助记得点赞+关注或者收藏哦~

Android 虚拟机与类加载机制

1.JVM与Dalvik

(1)Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应一个单独的Dalvik虚拟机实例。

(2)Dalvik虚拟机实则是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。

(3)Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基本寄存器的,而后者的指令集是基于堆栈的。

在这里插入图片描述

那什么是基于栈的虚拟机,什么又是基于寄存器的虚拟机呢?

1.1基于栈的虚拟机

(1)对于基于栈的虚拟机来说,每一个运行时的线程,都有一个独立的栈。

(2)栈中记录了方法调用的历史,每一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,其代表着当前执行的方法。基于栈的虚拟机通过操作数栈进行所有操作。

在这里插入图片描述

1.2字节码指令

在这里插入图片描述

(1)ICONST_1 : 将int类型常量1压入操作数栈;

(2)ISTORE 0 : 将栈顶int类型值存入局部变量0;

  • 0与L4 LOCALVARIABLE a I L1 L4 0
  • a与b是存放在局部变量表中的。
  • 要做加法操作,就必须将其放到操作数栈里面去
  • 放入操作数栈之后由IADD指令将运行结果放到c变量。

(3)IADD : 执行int类型的加法 ;

(4)小结:

指令先压栈,压栈之后再放到局部变量表,然后再压栈,再放到局部变量表,然后再从局部变量表中拿到操作数栈,再到CPU中计算。

1.2.1学习字节码指令有什么意义

(1)字节码插桩

1.3执行过程

在这里插入图片描述

1.4寄存器

(1)寄存器是CPU的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。

在这里插入图片描述

  • CPU从指令集中拿到指令,放到指令寄存器中,转化成CPU指令
  • CPU执行LOAD 100 指令,将100从内存地址中加载到数据寄存器中
  • 再将数据寄存器中的数据经过ALU(算数逻辑单元)

(2)简单说寄存器就是用来存指令与数据的。

1.5基于寄存器的虚拟机

(1)基于寄存器的虚拟机中没有操作数栈,但是有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。

在这里插入图片描述

(2)与JVM版相比,可以发现Dalvik版程序的指令数明显减少了,数据移动次数也明显减少了。

(3)它相当于是将JVM虚拟机中的局部变量表与操作数栈命并了,所以减少了数据在局部变量表与操作数栈中的移动,从而提升了虚拟机的执行效率。

1.6ART与Dalvik

(1)Dalvik虚拟机执行的是dex字节码,解释执行。

  • 从Android 2.2版本开始,支持JIT即时编译(Just In Time)
  • 在程序运行的过程中进行选择热点代码(经常执行的代码)进行编译或者优化。

(2)而ART(Android Runtime) 是在 Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默认 Android 运行时。

  • ART虚拟机执行的是本地机器码。
  • Android的运行时从Dalvik虚拟机替换成ART虚拟机,并不要求开发者将自己的应用直接编译成目标机器码,APK仍然是一个包含dex字节码的文件。

(3)那么,ART虚拟机执行的本地机器码是从哪里来?

1.7dex2aot

(1)Dalvik下应用在安装的过程,会执行一次优化,将dex字节码进行优化生成odex文件。

(2)而Art下将应用的dex字节码翻译成本地机器码的最恰当AOT时机也就发生在应用安装的时候。

(3)ART 引入了预先编译机制(Ahead Of Time),在安装时,ART 使用设备自带的 dex2oat 工具来编译应用,dex中的字节码将被编译成本地机器码。

在这里插入图片描述

  • JIT是运行时才编译
  • AOT是运行之前编译。

Android5.0以上的版本安装之前之所以慢,就在于其需要多做一些事情导致的,即AOT,而到了Android9.0,10.0又不再那么慢了,因为又再次做了修改。

1.8Android N的运作方式

ART 使用预先 (AOT) 编译,并且从 Android N混合使用AOT编译,解释和JIT。

(1)最初安装应用时不进行任何 AOT 编译(安装又快了),运行过程中解释执行,对经常执行的方法进行JIT,经过 JIT 编译的方法将会记录到Profile配置文件中。

(2)当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行 AOT 编译。待下次运行时直接使用。

在这里插入图片描述

(3)个人理解:就是先通过JIT编译,并且保存编译记录,然后在设备空闲,由编译守护进程根据 Profile文件中的编译记录对常用代码进行AOT编译,这样再次打开APP运行时,就可以直接使用。

2.Classloader

类是如何使用起来的?

在这里插入图片描述

在这里插入图片描述

(1)BootClassLoader是用于加载Android Framework层的class文件的。
(2)PathClassLoader是Android应用程序类加载器,比如AppCompatActivity,Android官方提供的依赖库,就是使用PathClassLoader加载的。
(3)BootClassLoader与PathClassLoader有什么联系?
PathClassLoader的父类加载器是BootClassLoader.

2.1Android类加载器

2.2双亲委托机制

(1)某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

  • 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

  • 安全性考虑,防止核心API库被随意篡改。

(2)为什么要有双亲委托机制?

  • 为了安全性,防止核心API库被随意修改。
  • 避免重复加载

2.3loadClass与findClass

在这里插入图片描述

在这里插入图片描述

(1)Android中类加载的过程

3.热修复流程

在这里插入图片描述
在这里插入图片描述

(1)把补丁包dex文件下载之后,其加载的顺序放在APP所有dex文件加载之前,保补丁dex文件中的类最先加载,由于Android双亲委托机制的存在,由于父类加载器已经加载过补丁包中的类,所以后续有bug的类将不再被加载进来。

(2)可以在Application的attachBaseContext插入补丁dex文件,执行热修复,目的是保证需要修复的类没有加载之前先加载补丁中的类。

(3)如何插入到dexElements数组中去,使用反射技术。

(4)以上是通过Android类加载机制,实现最基础的热修复原理.

public class KnowladgeApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        /**
         * 1.插入补丁dex文件,执行热修复
         * 1.1/data/data/xxx/files/xxxx.dex
         * 1.2/sdcard/xxx.dex
         */
        Hotfix.installPatch(this,new File("/sdcard/patch.dex"));
    }
}
/**
 * @author XiongJie
 * @version appVer
 * @Package com.gdc.knowledge.art.hotfix
 * @file
 * @Description: Android利用双亲委托机制实现热修复
 * 1.1热修复思想
 * (1)把补丁包dex文件下载之后,其加载的顺序放在APP所有dex文件加载之前,
 * 保证补丁dex文件中的类最先加载
 * (2)由于Android双亲委托机制的存在,由于父类加载器已经加载过补丁包中的类,所以后续有bug的
 * 类将不再被加载进来。
 * @date 2021-4-29 11:07
 * @since appVer
 */

public class Hotfix {

    private static final String TAG = "Hotfix";

    /**
     * 安装补丁
     * @param application
     * @param patch
     */
    public static void installPatch(Application application, File patch) {
        //1.获得classloader,PathClassLoader
        ClassLoader classLoader = application.getClassLoader();

        //2.将补丁文件添加到集合
        List<File> files = new ArrayList<>();
        if (patch.exists()) {
            files.add(patch);
        }

        //3.获取dex缓存目录
        File dexOptDir = application.getCacheDir();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            try {
                NewClassLoaderInjector.inject(application, classLoader, files);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } else {
            try {
                //23 6.0及以上
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    V23.install(classLoader, files, dexOptDir);
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    //4.4以上
                    V19.install(classLoader, files, dexOptDir);
                } else {
                    // >= 14
                    V14.install(classLoader, files, dexOptDir);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static final class V23 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            //找到 pathList
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);

            ArrayList<IOException> suppressedExceptions = new ArrayList<>();
            // 从 pathList找到 makePathElements 方法并执行
            // 得到补丁创建的 Element[]
            Object[] patchElements = makePathElements(dexPathList,
                    new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions);

            //将原本的 dexElements 与 makePathElements生成的数组合并
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", patchElements);
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makePathElement", e);
                    throw e;
                }

            }
        }

        /**
         * 把dex转化为Element数组
         */
        private static Object[] makePathElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            //通过阅读android6、7、8、9源码,都存在makePathElements方法
            Method makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements",
                    List.class, File.class,
                    List.class);
            return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }

    private static final class V19 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                IOException {
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements",
                    makeDexElements(dexPathList,
                            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                            suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makeDexElement", e);
                    throw e;
                }
            }
        }

        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            Method makeDexElements = ShareReflectUtil.findMethod(dexPathList, "makeDexElements",
                    ArrayList.class, File.class,
                    ArrayList.class);


            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }

    /**
     * 14, 15, 16, 17, 18.
     */
    private static final class V14 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException {

            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);

            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements",
                    makeDexElements(dexPathList,
                            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
        }

        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory)
                throws IllegalAccessException, InvocationTargetException,
                NoSuchMethodException {
            Method makeDexElements =
                    ShareReflectUtil.findMethod(dexPathList, "makeDexElements", ArrayList.class,
                            File.class);
            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
        }
    }

}
/**
 * @author XiongJie
 * @version appVer
 * @Package com.gdc.knowledge.art.hotfix
 * @file
 * @Description:加载器的创建
 * (1)创建分发加载任务加载器,作为父加载器
 * (2)创建补丁的加载器
 * @date 2021-4-29 11:13
 * @since appVer
 */

public class NewClassLoaderInjector {

    public static ClassLoader inject(Application app, ClassLoader oldClassLoader,
            List<File> patchs) throws Throwable {
        //1.分发加载任务的加载器,作为我们自己的加载器的父加载器
        DispatchClassLoader dispatchClassLoader
                = new DispatchClassLoader(app.getClass().getName(), oldClassLoader);
        //2.创建我们自己的加载器
        ClassLoader newClassLoader
                = createNewClassLoader(app, oldClassLoader, dispatchClassLoader,patchs);

        dispatchClassLoader.setNewClassLoader(newClassLoader);

        doInject(app, newClassLoader);
        return newClassLoader;
    }

    /**
     * 1.分发加载任务的加载器,作为我们自己的加载器的父加载器
     */
    private static final class DispatchClassLoader extends ClassLoader {

        private final String mApplicationClassName;
        private final ClassLoader mOldClassLoader;

        private ClassLoader mNewClassLoader;
        private final ThreadLocal<Boolean> mCallFindClassOfLeafDirectly = new ThreadLocal<Boolean>() {
            @Override
            protected Boolean initialValue() {
                return false;
            }
        };

        DispatchClassLoader(String applicationClassName,ClassLoader oldClassLoader){
            super(ClassLoader.getSystemClassLoader());
            mApplicationClassName = applicationClassName;
            mOldClassLoader = oldClassLoader;
        }

        void setNewClassLoader(ClassLoader classLoader) {
            mNewClassLoader = classLoader;
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            System.out.println("find:" + name);
            if (mCallFindClassOfLeafDirectly.get()) {
                return null;
            }
            // 1、Application类不需要修复,使用原本的类加载器获得
            if (name.equals(mApplicationClassName)) {
                return findClass(mOldClassLoader, name);
            }
            // 2、加载热修复框架的类 因为不需要修复,就用原本的类加载器获得
            if (name.startsWith("com.enjoy.patch.")) {
                return findClass(mOldClassLoader, name);
            }

            try {
                return findClass(mNewClassLoader, name);
            } catch (ClassNotFoundException ignored) {
                return findClass(mOldClassLoader, name);
            }
        }

        private Class<?> findClass(ClassLoader classLoader, String name) throws ClassNotFoundException {
            try {
                //双亲委托,所以可能会stackoverflow死循环,防止这个情况
                mCallFindClassOfLeafDirectly.set(true);
                return classLoader.loadClass(name);
            } finally {
                mCallFindClassOfLeafDirectly.set(false);
            }
        }

    }

    //2.创建我们自己的加载器
    private static ClassLoader createNewClassLoader(Context context, ClassLoader oldClassLoader,
            ClassLoader dispatchClassLoader,List<File> patchs) throws Throwable {
        //1.得到pathList
        Field pathListField = ShareReflectUtil.findField(oldClassLoader, "pathList");
        Object oldPathList = pathListField.get(oldClassLoader);

        //2.dexElements
        Field dexElementsField = ShareReflectUtil.findField(oldPathList, "dexElements");
        Object[] oldDexElements = (Object[]) dexElementsField.get(oldPathList);

        //3.从Element上得到 dexFile
        Field dexFileField = ShareReflectUtil.findField(oldDexElements[0], "dexFile");

        //4.获得原始的dexPath用于构造classloader
        StringBuilder dexPathBuilder = new StringBuilder();
        String packageName = context.getPackageName();

        //5.补丁文件遍历
        boolean isFirstItem = true;
        for (File patch : patchs) {
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                dexPathBuilder.append(File.pathSeparator);
            }
            dexPathBuilder.append(patch.getAbsolutePath());
        }

        //6.
        for (Object oldDexElement : oldDexElements) {
            String dexPath = null;
            DexFile dexFile = (DexFile) dexFileField.get(oldDexElement);
            if (dexFile != null) {
                dexPath = dexFile.getName();
            }
            if (dexPath == null || dexPath.isEmpty()) {
                continue;
            }
            if (!dexPath.contains("/" + packageName)) {
                continue;
            }
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                dexPathBuilder.append(File.pathSeparator);
            }
            dexPathBuilder.append(dexPath);
        }
        final String combinedDexPath = dexPathBuilder.toString();

        //7.app的native库(so) 文件目录 用于构造classloader
        Field nativeLibraryDirectoriesField = ShareReflectUtil.findField(oldPathList, "nativeLibraryDirectories");
        List<File> oldNativeLibraryDirectories = (List<File>) nativeLibraryDirectoriesField.get(oldPathList);

        //8.
        StringBuilder libraryPathBuilder = new StringBuilder();
        isFirstItem = true;
        for (File libDir : oldNativeLibraryDirectories) {
            if (libDir == null) {
                continue;
            }
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                libraryPathBuilder.append(File.pathSeparator);
            }
            libraryPathBuilder.append(libDir.getAbsolutePath());
        }

        String combinedLibraryPath = libraryPathBuilder.toString();

        //9.创建自己的类加载器
        ClassLoader result = new PathClassLoader(combinedDexPath, combinedLibraryPath, dispatchClassLoader);
        ShareReflectUtil.findField(oldPathList, "definingContext").set(oldPathList, result);
        ShareReflectUtil.findField(result, "parent").set(result, dispatchClassLoader);


        return result;
    }

    //3.注入
    private static void doInject(Application app, ClassLoader classLoader) throws Throwable {
        Thread.currentThread().setContextClassLoader(classLoader);

        Context baseContext = (Context) ShareReflectUtil.findField(app, "mBase").get(app);
        Object basePackageInfo = ShareReflectUtil.findField(baseContext, "mPackageInfo").get(baseContext);
        ShareReflectUtil.findField(basePackageInfo, "mClassLoader").set(basePackageInfo, classLoader);

        if (Build.VERSION.SDK_INT < 27) {
            Resources res = app.getResources();
            try {
                ShareReflectUtil.findField(res, "mClassLoader").set(res, classLoader);

                final Object drawableInflater = ShareReflectUtil.findField(res, "mDrawableInflater").get(res);
                if (drawableInflater != null) {
                    ShareReflectUtil.findField(drawableInflater, "mClassLoader").set(drawableInflater, classLoader);
                }
            } catch (Throwable ignored) {
                // Ignored.
            }
        }
    }

}

/**
 * @author XiongJie
 * @version appVer
 * @Package com.gdc.knowledge.art.hotfix
 * @file
 * @Description: 热修复工具类
 * @date 2021-4-29 11:27
 * @since appVer
 */

public class ShareReflectUtil {

    /**
     * oldClassLoader到其父类查找pathList属性
     * @param instance
     * @param name
     * @return
     */
    public static Field findField(Object instance, String name) throws NoSuchFieldException {

        for (Class<?> clazz = instance.getClass(); clazz != null; clazz =
                clazz.getSuperclass()) {
            try {
                //1.查找当前类的属性(不包括父类)
                Field field = clazz.getDeclaredField(name);
                if(!field.isAccessible()){
                    field.setAccessible(true);
                }
                return field;

            } catch (NoSuchFieldException e) {
                //e.printStackTrace();
            }

        }

        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

    /**
     * 1.使用反射查找方法
     * (1)从instance到其父类 找  name 方法
     * @param instance
     * @param name
     * @param parameterTypes
     * @return
     * @throws NoSuchMethodException
     */
    public static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
            throws NoSuchMethodException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Method method = clazz.getDeclaredMethod(name, parameterTypes);

                if (!method.isAccessible()) {
                    method.setAccessible(true);
                }

                return method;
            } catch (NoSuchMethodException e) {
            }
        }
        throw new NoSuchMethodException("Method "
                + name
                + " with parameters "
                + Arrays.asList(parameterTypes)
                + " not found in " + instance.getClass());
    }

    /**
     * @param instance
     * @param fieldName
     * @param patchElements 补丁的Element数组
     * @throws NoSuchFieldException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    public static void expandFieldArray(Object instance, String fieldName, Object[] patchElements)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {

        //1.拿到 classloader中的dexelements 数组
        Field dexElementsField = findField(instance, fieldName);
        //2.old Element[]
        Object[] dexElements = (Object[]) dexElementsField.get(instance);

        //3.合并后的数组
        Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
                dexElements.length + patchElements.length);

        //4.先拷贝新数组
        System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
        System.arraycopy(dexElements, 0, newElements, patchElements.length, dexElements.length);

        //5.修改 classLoader中 pathList的 dexelements
        dexElementsField.set(instance, newElements);
    }
}

3.2如果是JIT与AOT混合时热修复思想

(1)要解决AOT运行前编译成机器码的过程.
Tinker:自定义ClassLoader替换系统创建的PathClassLoader.

4.存储权限访问权限问题

<!--
        1.存储访问权限
        1.1Android 10.0之后,即使给了此权限,也无法访问。google官网给出的原因是:
        (1)为了让用户更好地管理自己的文件并减少混乱,以Android 10(API级别29)
        及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储)。
        此类应用只能看到本应用专有的目录(通过Context.getExternalFilesDir()访问)以及特定类型的媒体。
        除非您的应用需要访问存放在应用的专有目录以及MediaStore之外的文件,否则最好使用分区存储。

        也就是其无法看到本应用之外的任何SD卡文件

        (2)在application的标签中添加android:requestLegacyExternalStorage="true",禁用分区权限
    -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:requestLegacyExternalStorage="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true">

5.打赏鼓励

感谢您的细心阅读,您的鼓励是我写作的不竭动力!!!

5.1微信打赏

在这里插入图片描述

5.2支付宝打赏

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值