Android 简单热修复(上)——Java类加载器

简介: 作为阳历新年的第一篇文章,本想把之前总结的用到实践中,简单写了个钟表,写着写着感觉索然无味(/ □ )。写完后,百无聊赖之际,随便翻看了些技术文章。让我眼前为之一亮的有两个:Android 破解跳一跳Android 简单热修复原理作为Android狗的我果断选择了热修复的介绍,在看完Android类加载器的源码后,对于简单的热修复原理算是了解了一些。

作为阳历新年的第一篇文章,本想把之前总结的用到实践中,简单写了个钟表,写着写着感觉索然无味(/ □ )。写完后,百无聊赖之际,随便翻看了些技术文章。让我眼前为之一亮的有两个:

  • Android 破解跳一跳
  • Android 简单热修复原理

作为Android狗的我果断选择了热修复的介绍,在看完Android类加载器的源码后,对于简单的热修复原理算是了解了一些。遂作此文,以谨记。


img_17ba92fc38170b7ff33b1afadb47cce1.png

在介绍Android热修复原理之前,有必要了解下关于Java的类加载器的相关知识。在《深入理解Java虚拟机》一书中关于类加载的可以分为五个过程:

  1. 加载
    在加载过程中需要完成3件事情:
    1.1 通过一个类的全限定名来获取定义此类的二进制字节流。
    1.2 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
    1.3 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  2. 验证
    这一阶段的主要目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  3. 准备
    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段 ,这些变量所使用的内存都将在方法区中进行分配。
  4. 解析
    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  5. 初始化
    初始化阶段是执行类构造器<clinit>()方法的过程。

关于详细介绍,还是乖乖看书吧。
OK,知道了类加载的过程,但是究竟是什么“东西”加载类呢?答案是类加载器(ClassLoader),也是今天的主题。
简单说下类加载器的分类:

  • 启动类加载器(BootStrap ClassLoader):启动类加载器负责将<JAVA_HOME>\lib目录下中的,或者被-Xbootclasspath参数所指定的路径中的,并且被虚拟机识别的类库加载到虚拟机内存中(有点拗口)。通过System.getProperty("sun.boot.class.path")可知默认情况加载如下类库:
C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_131\jre\classes
  • 扩展类加载器(Extension ClassLoader):扩展类加载器用于将<JAVA_HOME>\lib\ext中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。扩展类加载器加载的类库(默认情况),可以看到其就是<JAVA_HOME>\lib\ext中的类库:


    img_6eb66546fbd067eeafaff08c3dfa3aeb.png
    扩展类加载器加载的类库
  • 应用程序类加载器(Application ClassLoader):用于加载用户类路径上所指定的类库,如果程序没有自定义过自己的类加载器,一般情况下这个就是这个程序的默认类加载器。应用程序类加载器加载的类库(默认情况),可以看到其加载的类库包括了<JAVA_HOME>\lib和<JAVA_HOME>\lib\ext目录下的类库,也就是说如果前两个没有找到要加载的类,也可以通过AppClassLoader去加载:


    img_4a78220c0455d1325d284cdca40d716c.png
    应用程序类加载器加载的类库

启动类加载器

上面已经说过启动类加载器会加载的类库,下午我和一个大佬讨论了下关于java类是否按需加载。答案是:java类是按需加载,只有当需要用到这个类的时候才会加载这个类。在运行时添加-verbose:class参数,我们先看到被加载到内存中的类:

img_48b61215e85df1f4bf97d38686832333.png
启动时加载的类

启动类加载了rt.jar中的类,我们可以通过反向来证明某个类是由启动类加载器加载:

System.out.println(String.class.getClassLoader());

在上面我们只是输出了一下String这个类的类加载器,结果如下:

img_9515fa21a41a17b545a4cfc828a1ba9c.png
String类加载器

我们可以知道其类加载器是 null,这又是为什么呢?我们看下 getClassLoader()这个方法的注释:
img_bd136d534c6a4ee1543443c883289133.png
注释

从注释中我们可以知道如果返回值为 null,那么代表此时的类加载器是 BootStrap ClassLoader,所以上面所讲述的完全没毛病。
img_26000f6c6b9e0ec737f21c1b1220f554.png

扩展类加载器

先看下默认的<JAVA_HOME>\lib\ext路径下的类库有什么:


img_c106b4893dbd365a9d292b2fd859ca1a.png
ext类库

默认的路径下加载的类库并不是特别多,我们挑选其中的一个来测试下:

System.out.println(JarFileSystemProvider.class.getClassLoader());

img_50b3c95dfc00f5860154d854cfc948f2.png
测试扩展类加载器

从结果中我们可以知道加载扩展类的加载器是 sun.misc.Launcher类的内部类 ExtClassLoader

应用程序类加载器

应用程序加载器用于加载当前程序的类库(默认情况下),按照上面的测试我们同样测试下:

// UserModel为当前程序里的一个类
System.out.println(UserModel.class.getClassLoader());

运行结果:

img_281fc63d51ca670397da037693b6368d.png
应用程序类加载器

从结果中我们可以知道加载扩展类的加载器是 sun.misc.Launcher类的内部类 AppClassLoader
类关系图:
img_5da620c559ff4d2186a1aa3203de41e7.png
类关系图

讲下每个类加载器的父亲:

  • BootStrap ClassLoader:无父类加载器
  • ExtClassLoader:父类加载器BootStrap ClassLoader
  • AppClassLoader:父类加载器ExtClassLoader

关于三个类加载器的创建

BootStrap ClassLoader

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在Java代码中获取它的引用。

ExtClassLoader的创建

话不多少,还是先看下代码吧:

Launcher.java:
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        // 获得ExtClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        // 将ExtClassLoader作为参数传入AppClassLoader中
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    ......

}

static class ExtClassLoader extends URLClassLoader {
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
        // 获取了Ext的目录
        final File[] var0 = getExtDirs();

        try {
            return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                public Launcher.ExtClassLoader run() throws IOException {
                    int var1 = var0.length;

                    for(int var2 = 0; var2 < var1; ++var2) {
                        MetaIndex.registerDirectory(var0[var2]);
                    }
                    // 创建一个新的ExtClassLoader,传入文件数组
                    return new Launcher.ExtClassLoader(var0);
                }
            });
        } catch (PrivilegedActionException var2) {
            throw (IOException)var2.getException();
        }
    }

    void addExtURL(URL var1) {
        super.addURL(var1);
    }

    public ExtClassLoader(File[] var1) throws IOException {
        // 父类构造方法,其中第二个参数为parent也就是当前ClassLoader的父类加载器
        // 这里传入的是null,也就是其父类加载器是BootStrap ClassLoader
        super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
        SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
    }

    private static File[] getExtDirs() {
        String var0 = System.getProperty("java.ext.dirs");
        File[] var1;
        if(var0 != null) {
            StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
            int var3 = var2.countTokens();
            var1 = new File[var3];

            for(int var4 = 0; var4 < var3; ++var4) {
                var1[var4] = new File(var2.nextToken());
            }
        } else {
            var1 = new File[0];
        }

        return var1;
    }

    ......
}

从代码中我们可以知晓:

  • ExtClassLoader是在Launcher中创建,并且指定其父类加载器为null(BootStrap ClassLoader)
  • 通过getExtDirs获得扩展类的目录文件数组

我们看下getExtDirs输出:

C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext
C:\Windows\Sun\Java\lib\ext

这个输出一个代表了<JAVA_HOME>\lib\ext路径,另一个则是默认的扩展类路径。

AppClassLoader的创建

Launcher的部分代码中可以知道ExtClassLoader作为参数传入AppClassLoader中,这里看下AppClassLoader类:

static class AppClassLoader extends URLClassLoader {
    final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
                // 这里将传入的ExtClassLoader作为构造参数,说明其父类加载器为ExtClassLoader
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }

    AppClassLoader(URL[] var1, ClassLoader var2) {
        super(var1, var2, Launcher.factory);
        this.ucp.initLookupCache(this);
    }

    public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
        int var3 = var1.lastIndexOf(46);
        // 加载前的判断,检查包权限以及是否已经知道不存在
        if(var3 != -1) {
            SecurityManager var4 = System.getSecurityManager();
            if(var4 != null) {
                var4.checkPackageAccess(var1.substring(0, var3));
            }
        }

        if(this.ucp.knownToNotExist(var1)) {
            Class var5 = this.findLoadedClass(var1);
            if(var5 != null) {
                if(var2) {
                    this.resolveClass(var5);
                }

                return var5;
            } else {
                throw new ClassNotFoundException(var1);
            }
        } else {
            // 调用ClassLoader的loadClass
            return super.loadClass(var1, var2);
        }
    }

    ......
}

AppClassLoaderExtClassLoader作为父类加载器,并且重写了loadClass方法,用于校验。不过我在debug时发现System.getSecurityManager()返回值为null,所以推测这里需要自己实现安全管理。

验证:

ClassLoader classLoader = Main.class.getClassLoader();
while (classLoader.getParent() != null) {
    System.out.println(classLoader);
    classLoader = classLoader.getParent();
}
System.out.println(classLoader);

输出:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d

类加载器的双亲委派机制

img_60bf51a56c509176dfad9610d7ef1724.png
双亲委派机制模型

双亲委派机制:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
好处:使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
(《深入理解Java虚拟机》)

双亲委派机制的实现

废话不多说,先上代码为敬:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 第一步检查此类是否已经被加载,native层实现
        Class<?> c = findLoadedClass(name);
        // 如果没有被加载
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 获取其父类加载器,并且调用loadClass()方法
                // 如果父类加载器是BootStrap ClassLoader,则调用findBootstrapClassOrNull
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            
            // 如果没有加载此类,尝试通过类名查找此类
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

这里看下loadClass的过程:

  1. 查看类是否已经被加载过,通过native方法实现。如果已经加载过,直接返回此Class
  2. 类没有被加载过,如果其父类加载器存在,调用父类加载器的loadClass方法加载Class
  3. 父类加载器不存在,调用findBootstrapClassOrNull方法查找启动类加载器是否加载此类,如果有加载则返回;如果没有加载则调用findClass方法。
  4. 递归的过程中如果有一处得到了Class,那么将返回此Class

光说不练假把式,还是来举两个栗子吧

1. 启动类加载器加载类

测试代码:

System.out.println(Provider.class.getClassLoader());

接着将ClassLoader中的findClass设置断点,调试。执行结果如下:

img_34fef1b98db7e4868449c33be12a41e5.png
第一次

img_701dd836d0a9b0c3a26812713d86acd5.png
AppClassLoader

可以看到,第一次执行的时候是AppClassLoader进行loadClass方法的调用。接着进入parent.loadClass方法中:

img_f7646694934ee02e0c9d9106317cc208.png
parent.loadClass

img_b5bb8938d3f22ee3c6d9f3a0633d064f.png
ExtClassLoader

接着调用了ExtClassLoader中的loadClass方法,我们知道其父类加载器不存在,所以执行findBootstrapClassOrNull方法:

img_1d582bafa35951043d9e22835198d097.png
findBootstrapClassOrNull

因为我现在挑选的是启动类加载器加载的类,所以这里面返回值不为空,接着就把此值返回给 ExtClassLoaderExtClassLoader又把值返回给 AppClassLoader,最终将值返回,整个过程结束。

2. 应用程序类加载器

测试代码:

System.out.println(UserModel.class.getClassLoader());

接着将ClassLoader中的findClass设置断点,调试。其查找过程和上面一致,这里不多说,这里需要知道的是此时findBootstrapClassOrNull方法返回值为null,接着会调用findClass方法:

img_d38b558c9cbfbb10c9583d6269ed5dee.png
ExtClassLoader findClass

img_24ed6b5d57c045c4b2239a13fba05268.png
ExtClassLoader

ExtClassLoader中查找 UserModel没有找到,返回结果 null,紧接着就会调用 AppClassLoaderfindClass方法:
img_9d02989d59199042cd17102f8c9ec576.png
AppClassLoader findClass

img_a906b667b0eae3372600f323391b9407.png
AppClassLoader

通过 defineClass方法最终获取到UserModel类,并将结果返回。

破坏双亲委派机制的自定义类加载器

双亲委派机制是建立在不重写loadClass流程的基础上,如果某一个自定义类加载器重写了loadClass方法,并将其流程改变,那么所谓的双亲委派机制也就消失了。下面的自定义类加载器破坏了双亲委派机制:

public class CustomClassLoader extends ClassLoader {

    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }
    // 重写了loadClass方法,不用去查找是否加载,如果类文件存在,直接返回所需类
    // 否则按照原方式进行
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        File file = new File(classPath + name.replace(".", "\\") + ".class");
        if (file.exists()) {
            try {
                InputStream is = new FileInputStream(file);
                byte[] b = new byte[is.available()];
                is.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return super.loadClass(name);
    }

}
// 测试代码
private static void test() {
    CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\B-0137\\Desktop\\");
    try {
        Class<?> userModel = customClassLoader.loadClass("com.nick.model.UserModel");
        Object o = userModel.newInstance();
        System.out.println(o);
        System.out.println(o instanceof UserModel);
        IUser iUser = (IUser) o;
        iUser.test();

        Class<?> mC = Main.class.getClassLoader().loadClass("com.nick.model.UserModel");
        Object mainO = mC.newInstance();
        System.out.println(mainO);
        System.out.println(mainO instanceof UserModel);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
}

测试结果:

com.nick.model.UserModel@7f31245a
false
测试
com.nick.model.UserModel@6d6f6e28
true

在这也能看出通过破坏双亲委派机制可以由不同的类加载器加载相同的类,但是他们并不相等——类加载器不同

保持双亲委派机制的自定义类加载器

其实想要保持双亲委派机制很简单:只需要在自定义类加载器的时候重写findClass方法即可
自定义类加载器这里省略,就是重写了findClass方法,其他代码没变。测试代码:

private static void test() {
    CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\B-0137\\Desktop\\");
    try {
        Class<?> userModel = customClassLoader.loadClass("com.nick.model.UserModel");
        Object o = userModel.newInstance();
        System.out.println(o);
        System.out.println(o instanceof UserModel);
        System.out.println(userModel.getClassLoader());
        IUser iUser = (IUser) o;
        iUser.test();
        System.out.println();

        Class<?> mC = Main.class.getClassLoader().loadClass("com.nick.model.UserModel");
        Object mainO = mC.newInstance();
        System.out.println(mainO);
        System.out.println(mainO instanceof UserModel);
        System.out.println(mC.getClassLoader());
        System.out.println();
        
        Class<?> userModel2 = customClassLoader.loadClass("com.nick.model.UserModel2");
        Object o2 = userModel2.newInstance();
        System.out.println(o2);
        System.out.println(userModel2.getClassLoader());
        IUser iUser2 = (IUser) o;
        iUser2.test();

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
}

测试结果:

com.nick.model.UserModel@677327b6
true
sun.misc.Launcher$AppClassLoader@18b4aac2
测试

com.nick.model.UserModel@14ae5a5
true
sun.misc.Launcher$AppClassLoader@18b4aac2

com.nick.model.UserModel2@135fbaa4
com.nick.classloader.CustomClassLoader@7f31245a
测试

我们用自定义的类加载器去加载外部的一个和项目中同名的类,结果发现其是由应用程序类加载器加载,那么可以说明自定义类加载器重写findclass方法保持了双亲委派机制。

结尾

作为开年的第一篇文章,洋洋洒洒写了好多字。从论据到论点,详详细细全部写完。啰哩啰唆说了一大堆,结果还没进入正题(热修复)。这篇文章主要是为热修复打下些基础,下一篇将会讲述基于类加载器原理实现的热修复以及如何实现。
最后上个美女养养眼吧~

img_e4f27a7d57580f0ca53217bcf19f0f46.jpe
美女
目录
相关文章
|
2月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
34 1
|
1月前
|
前端开发 Java 测试技术
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
54 0
|
2月前
|
前端开发 Java 测试技术
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
27 2
|
3月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
166 1
|
4月前
|
存储 搜索推荐 Java
探索安卓开发中的自定义视图:打造个性化UI组件Java中的异常处理:从基础到高级
【8月更文挑战第29天】在安卓应用的海洋中,一个独特的用户界面(UI)能让应用脱颖而出。自定义视图是实现这一目标的强大工具。本文将通过一个简单的自定义计数器视图示例,展示如何从零开始创建一个具有独特风格和功能的安卓UI组件,并讨论在此过程中涉及的设计原则、性能优化和兼容性问题。准备好让你的应用与众不同了吗?让我们开始吧!
|
4月前
|
Java 调度 Android开发
Android经典实战之Kotlin的delay函数和Java中的Thread.sleep有什么不同?
本文介绍了 Kotlin 中的 `delay` 函数与 Java 中 `Thread.sleep` 方法的区别。两者均可暂停代码执行,但 `delay` 适用于协程,非阻塞且高效;`Thread.sleep` 则阻塞当前线程。理解这些差异有助于提高程序效率与可读性。
87 1
|
4月前
|
Android开发
Cannot create android app from an archive...containing both DEX and Java-bytecode content
Cannot create android app from an archive...containing both DEX and Java-bytecode content
46 2
|
4月前
|
Java Android开发
解决Android编译报错:Unable to make field private final java.lang.String java.io.File.path accessible
解决Android编译报错:Unable to make field private final java.lang.String java.io.File.path accessible
639 1
|
4月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
69 0
|
4月前
|
IDE Java Linux
探索安卓开发:从基础到进阶的旅程Java中的异常处理:从基础到高级
【8月更文挑战第30天】在这个数字时代,移动应用已经成为我们日常生活中不可或缺的一部分。安卓系统由于其开放性和灵活性,成为了开发者的首选平台之一。本文将带领读者踏上一段从零开始的安卓开发之旅,通过深入浅出的方式介绍安卓开发的基础知识、核心概念以及进阶技巧。我们将一起构建一个简单的安卓应用,并探讨如何优化代码以提高性能和应用的用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供宝贵的知识和启发。