Android-JNI开发概论
JNI的全称是Java Native Interface,顾名思义,这是一种解决Java和C/C++相互调用的编程方式。它其实只解决两个方面的问题,怎么找到和怎么访问。 弄清楚这两个话题,我们就学会了JNI开发。需要注意的是,JNI开发只涉及到一小部分C/C++开发知识,遇到问题的时候我们首先要判断是C/C++的问题还是JNI的问题,这可以节省很多搜索和定位的时间。
我们知道Java程序是不能单独运行的,它需要运行在JVM上的,而JVM却又需要跑在物理机上,所以它的任务很重,既要处理Java代码,又要处理各种操作系统,硬件等问题。可以说了解了JVM,就了解了Java的全部,当然包括JNI。所以我们先以JVM的身份来看看Java代码是怎样跑起来的吧(只是粗略的内容,省去了很多步骤,为了突出我们在意的部分)。
运行Java代码前,会先启动一个JVM。在JVM启动后,会加载一些必要的类,这些类中包含一个叫主类的类,也就是含有一个静态成员函数,函数签名为public static void main(String[] args)的方法。资源加载完成后,JVM就会调用主类的main方法,开始执行Java代码。随着代码的执行,一个类依赖另一个类,层层依赖,共同完成了程序功能。这就是JVM的大概工作流程,可以说JVM就好比一座大桥,连接着Java大山和native大山。
现在问题来了,在Java程序中,某个类需要通过JNI技术访问JVM以外的东西,那么它需要怎样告诉我(我现在是JVM)呢?需要一种方法 把普通的Java方法标记成特殊,这个标记就是native关键字(使用Kotlin时虽然也可以使用这个关键字,但是Kotlin有自己的关键字external)。当我执行到这个方法时,看到它不一样的标记,我就会从其他地方而不是Class里面寻找执行体,这就是一次JNI调用。也就是说对于Java程序来说,只需要将一个方法标记为native,在需要的地方调用这个方法,就可以完成JNI调用了。但是对于我,该怎样处理这一次JNI调用呢?其实上面的寻找执行体的过程是一个跳转问题,在C/C++的世界,跳转问题就是指针问题。那么这个指针它应该指向哪里呢?
C/C++代码是一个个函数(下文会将Java方法直接用方法简称,而C/C++函数直接用函数简称)组合起来的,每一个函数都是一个指针,这个特性恰好满足我的需要。但是对于我,外面世界那么大,我并不知道从哪里,找什么东西,给我的信息还是不够。为了限定范围,我规定,只有通过System.loadLibrary(“xxx”)加载的函数,我才会查找,其余的我直接罢工(抛错)。这一下子减轻了我的工作量,至少我知道从哪里找了。
确定了范围,下一步就是在这个范围里确定真正的目标了。Java世界里怎样唯一标识一个类呢,有的人会脱口而出——类名,其实不全对,因为类名可能会重名,我们需要全限定的类名,也就是包名加类名,如String的全限定类名就是java.lang.String。但是这和我们查找native的方法有什么联系呢?
当然有联系,既然一个全限定的类名是唯一的,那么它的方法也是唯一的,那么假如我规定以这个类的全限定类名加上方法名作为native函数的函数名,这样我是不是就可以通过函数名的方式找到native的函数看呢,答案是肯定的,但是有瑕疵,因为Java系统支持方法重载,也就是一个类里面,同名的方法可能有多个。那么构成重载的条件是什么呢,是参数列表不同。所以,结果就很显然了,我在前面的基础上再加上参数列表,组合成查找条件,我是不是就可以唯一确定某一个native函数了呢,这就是JNI的静态注册。
不过,既然我只需要确定指针的指向,那么我能不能直接给指针赋值,而不是每次都去查找呢,虽然我不知道累,但是还是很耗费时间的。对于这种需求,我当然也是满足的啦,你直接告诉我,我就不找了,我还乐意呢。而且,既然你都给我找到了,我就不需要下那么多规定了,都放开,你说是我就相信你它是。这就是JNI的动态注册。
上一节我们通过化身JVM的方式了解了JNI函数注册的渊源,并且引出了两种函数注册方式。从例子上,我们也可以总结出两种注册方式的特点
那么具体怎么做呢?我们接着往下说。
虽然静态注册限制比较多,但是都是一些浅显的规则,更容易实施,所以先从静态注册开始讲解。
静态注册有着明确的开发步骤
- 编写Java类,声明native方法;
- 使用java xxx.java将Java源文件编译为class文件
- 使用javah xxx生成对应的.h文件
- 构建工具中引入.h文件
- 实现.h文件中的函数
上面的这个步骤是静态开发的基本步骤,但是其实在如今强大的IDE面前,这些都不需要我们手动完成了,在Android Studio中,定义好native方法后,在方法上按alt + enter就可以生成正确的函数签名,直接写函数逻辑就可以了。但是学习一门学问,我们还是要抱着求真,求实的态度,所以我用一个例子来阐述一下这些规则,以加深读者的理解。
Test.java
native-lib.cpp
上面就是一个JNI函数在两端声明的例子,不难发现
- 函数签名以Java_为前缀
- 前缀后面跟着类的全路径,也就是包含包名和类名
- 以_作为路径分隔符
- 函数的第一个参数永远是JNIEnv *类型,第二个参数根据函数类型的不同而不同,static类型的方法,对应的是jclass类型,否则对应的是jobject类型。类型系统后面会详细展开。
为什么Java方法对应到C/C++函数后,会多两个参数呢。我们知道JVM是多线程的,而我们的JNI方法可以在任何线程调用,那么怎样保证调用前后JVM能找到对应的线程呢,这就是函数第一个参数的作用,它是对线程环境的一种封装,和线程一一对应,也就是说不能用一个线程的JNIEnv对象在另一个线程里使用。另外,它是一个C/C++访问Java世界的窗口,JNI开发的绝大部分时间都是和JNIEnv打交道。
同样按照开发过程,我们一步一步来完成。 我们把前面的Java_me_hongui_demo_Test_jniString函数名改成jniString(当然不改也可以,毕竟没限制),参数列表保持不变,这时,我们就会发现Java文件报错了,说本地方法未实现。
其实我们是实现了的,只是JVM找不到。为了让JVM能找到,我们需要向JVM注册。 那么怎么注册,在哪注册呢,似乎哪里都可以,又似乎都不可以。 前面说过,JVM只会查找通过System.loadLibrary(“xxx”); 加载的库,所以要想使用native方法,首先要先加载包含该方法的库文件,之后,才可使用。加载了库,说明Java程序要开始使用本地方法了。在加载库之后,调用方法之前,理论上都是可以注册方法的,但是时机怎么确定呢,JNI早就给我们安排好了。
JVM在把库加载进虚拟机后,会调用函数jint JNI_OnLoad(JavaVM *vm, void *reserved),以确认JNI的版本,版本信息会以返回值的形式传递给JVM,目前可选的值有JNI_VERSION_1_1,JNI_VERSION_1_2,JNI_VERSION_1_4,JNI_VERSION_1_6。假如库没有定义这个函数,那么默认返回的是JNI_VERSION_1_1,库将会加载失败,所以,为了支持最新的特性我们通常返回较高的版本。既然有了这么好的注册时机,那么下一步就是实现注册了。
但事情并没有这么简单。由JNI_OnLoad函数参数列表可知,目前,可供使用的只有JVM,但是查阅JVM的API,我们并没有发现注册的函数——注册函数是写在JNIEnv类里面的。恰巧的是,JVM提供了获取JNIEnv对象的函数。
JVM有多个和JNIEnv相关的函数,在Android开发中,我们需要使用AttachCurrentThread来获取JNIEnv对象,这个函数会返回执行状态,当返回值等于JNI_OK的时候,说明获取成功。有了JNIEnv对象,我们就可以注册函数了。
先来看看注册函数的声明——jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods。返回值不用多说,和AttachCurrentThread一样,指示执行状态。难点在参数上,第一个参数是jclass类型,第二个是JNINativeMethod指针,都是没见过的主。
为什么需要这么多参数呢,JVM不只需要一个函数指针吗。还是唯一性的问题,记得前面的静态注册吗,静态注册用全限定类型和方法,参数列表,返回值的组合确定了函数的唯一性。但是对于动态注册,这些都是未知的,但是又是必须的。为了确定这些值,只能通过其他的方式。jclass就是限定方法的存在范围,获取jclass对象的方式也很简单,使用JNIEnv的jclass FindClass(const char* name)函数。参数需要串全限定符的类名,并且把.换成/,也就是类似me/hongui/demo/Test的形式,为啥这样写,后面会单独拿一节出来细说。
第二个和第三个参数组合起来就是常见的数组参数形式。先来看看JNINativeMethod的定义。
有个编写诀窍,按定义顺序,相关性是从Java端转到C/C++端,怎么理解呢?name是只的Java端对应的native函数的名字,这是纯Java那边的事,Java那边取啥名,这里就是啥名。第二个signature代表函数签名,签名信息由参数列表和返回值组成,形如(I)Ljava/lang/String;,这个签名就是和两边都有关系了。首先Java那边的native方法定义了参数列表和返回值的类型,也就是限定了签名的形式。其次Java的数据类型对应C/C++的转换需要在这里完成,也就是参数列表和返回值要写成C/C++端的形式,这就是和C/C++相关了。最后一个fnPtr由名字也可得知它是一个函数指针,这个函数指针就是纯C/C++的内容了,代表着Java端的native方法在C/C++对应的实现,也就是前文所说的跳转指针的。知道了这些,其实我们还是写不出代码,因为,我们还有JNI的核心没有说到,那就是类型系统。
由于涉及到Java和C/C++两个语言体系,JNI的类型系统很乱,但并非无迹可寻。首先需要明确的是,两端都有自己的类型系统,Java里的boolean,int,String,C/C++的bool,int,string等等,遗憾的是,它们并不一一对应。也就是说C/C++不能识别Java的类型。既然类型不兼容,谈何调用呢。这也就是JNI欲处理的问题。
为了解决类型不兼容的问题,JNI引入了自己的类型系统,类型系统里定义了和C/C++兼容的类型,并且还对Java到C/C++的类型转换关系做了规定。怎么转换的呢,这里有个表
乍一看,没什么特别的,不过就是加了j前缀(除了void),但是,这只是基本类型,我们应该没忘记Java是纯面向对象的语言吧。各种复杂对象才是Java的主战场啊。而对于复杂对象,情况就复杂起来了。我们知道在Java中,任何对象都是Object类的子类。那么我们是否可以把除上面的基本类型以外的所有复杂类型都当作Object类的对象来处理呢,可是可以,但是不方便,像数组,字符串,异常等常用类,假如不做转换使用起来比较繁琐。为了方便我们开发,JNI又将复杂类型分为下面这几种情况
两个表合起来就是Java端到C/C++的类型转换关系了。也就是说,当我们在Java里声明native代码时,native函数参数和返回值的对应关系,也是C/C++调用Java代码参数传递的对应关系。但是毕竟两套系统还是割裂的,类型系统只定义了兼容方式,并没有定义转换方式,双方的参数还是不能相互识别,所以,JNI又搞了个类型签名,欲处理类型的自动转换问题。
类型签名和类类型映射类似,也有对应关系,我们先来看个对应关系表
对于基本类型,也很简单,就是取了首字母,除了boolean(首字母被byte占用了),long(字母被用作了符合对象的前缀标识符)。 着重需要注意的是复合类型,也就是某个类的情况。它的签名包含三部分,前缀L,中间是类型的全限定名称,跟上后缀;,三者缺一不可,并且限定符的分隔符要用/替换, 。 注意,类型签名和类型系统不是一个概念。类型通常是纯字符串的,用在函数注册等地方,被JVM使用的。类型系统是和普通类型一样的,可以定义变量,作为参数列表,被用户使用的。 另外,数组对象也有自己的类型签名,也是有着类型前缀[,后面跟着类型的签名。最后的方法类型,也就是接下来我们着重要讲的地方,它也是由三部分组成()和包含在()里面的参数列表,()后面的返回值。这里用到的所有类型,都是指类型签名。
我们来看个例子
它的类型签名怎么写呢?我们来一步一步分析
- 确定它在Java里面的类型,在表中找出对应关系,确定签名形式。
- 用步骤1的方法确定它的组成部分的类型。
- 将确定好的签名组合在一起
此例是方法类型,对应表中最后一项,所以签名形式为(参数)返回值。该方法有三个参数,我们按照步骤1的方式逐一确定。
- int n对应int类型,签名是I;
- String s对应String类型,是复合类型,对应表中倒数第三项,所以它的基本签名形式是L全限定名;。而String的全限定名java.lang.String,用/替换,后变成java/lang/String。按步骤3,将它们组合在一起就是Ljava/lang/String;;
- boolean[] arr对应数组类型,签名形式是[类型,boolean的签名是Z。组合在一起就是[Z;
- 最后来看返回值,返回值是long类型,签名形式是J。
按照签名形式将这些信息组合起来就是(ILjava/lang/String;[Z)J,注意类型签名和签名之间没有任何分割符,也不需要,类型签名是紧密排列的。
有了JNI的类型系统的支持,回过头来接着看动态注册的例子,让我们接着完善它。
- 用JVM对象获取JNIEnv对象,即auto status=vm->AttachCurrentThread(&jniEnv, nullptr);
- 用步骤1获取的JNIEnv对象获取jclass对象,即auto cls=jniEnv->FindClass(\”me/hongui/demo/Test\”);
- 定义JNINativeMethod数组,即JNINativeMethod methods[]={{\”jniString\”, \”()Ljava/lang/String;\”,reinterpret_cast<void *>(jniString)}};,这里的方法签名可以参看上一节。
- 调用JNIEnv的RegisterNatives函数。即status=jniEnv->RegisterNatives(cls,methods,sizeof(methods)/sizeof(methods[0]));。
- 当然,别忘了实现对应的native函数,即这里的jniString——JNINativeMethod的第三个参数。
这五步就是动态注册中JNI_OnLoad函数的实现模板了,主要的变动还是来自jclass的获取参数和JNINativeMethod的签名等,必须做到严格的一一对应。如下面的例子
前面磨磨唧唧说了这么一大片,其实才讲了一个问题——怎么找到。虽然繁杂,但好在有迹可循,大不了运行奔溃。下面要讲的这个问题就棘手多了,需要一点点耐性和细心。这一部分也可以划分成两个小问题——***访问已知对象的数据,创建新对象。有一点还是要提一下,这里的访问还创建都是针对Java程序而言的,也就是说,对象是存在JVM虚拟机的堆上的,我们的操作都是基于堆对象的操作。***而在C/C++的代码里,操作堆对象的唯一途径就是通过JNIenv提供的方法。所以,这部分其实就是对JNIenv方法的应用讲解。
在面向对象的世界中,我们说访问对象,通常指两个方面的内容,访问对象的属性、调用对象的方法。这些操作在Java世界中,很好实现,但是在C/C++世界却并非如此。在JNI的类型系统那一节,我们也了解到,Java中的复杂对象在C/C++中都对应着jobject这个类,显然,无论Java世界中,那个对象如何牛逼,在C/C++中都是一视同仁的。为了实现C/C++访问Java的复杂对象,结合访问对象的方式,JNIEnv提供了两大类方法,一类是对应属性的,一类是对应方法的。借助JNIEnv,C/C++就能实现访问对象的目标了。而且它们还有一个较为统一的使用步骤:
- 根据要访问的内容准备好对应id(fieldid或者methodid)。
- 确定访问的对象和调用数据
- 通过JNIEnv的方法调用完成对象访问
可以看出来,这使用步骤和普通面向对象的方式多了一些准备阶段(步骤1,2)。之前提到过,这部分的内容需要的更多的是耐心和细心,不需要多少酷炫的操作,毕竟发挥空间也有限。这具体也体现在上面的步骤1,2。正是这个准备阶段让整个C/C++的代码变得丑陋和脆弱,但是——又不是不能用,是吧。
看一个例子,Java里定义了一个Person类,类定义如下
现在,我们在C/C++代码里该怎么访问这个类的对象呢。假定需要读取这个对象的age值,设置这个对象的name值。根据上面的步骤,我们有以下步骤
- 准备好age的fieldid,setName的methodid。根据JNIEnv的方法,我们可以看到四个相关的,fieldid,methodid各两个,分普通的和静态的。我们这里都是普通的,所以确定的方法是GetFieldID和GetMethodID。第一个参数就是jclass对象,获取方法前面已经说过,即通过JNIEnv的FindClass方法,参数是全限定类名,以/替换.。后面两个参数对应Java端的名称和类型签名,age属于field,int的类型签名是I,setName属于method,签名形式是(参数)返回值,这里参数的签名是Ljava/lang/String;,返回值的签名是V,组合起来就是\”(Ljava/lang/String;)V\”。
- 假定我们已经有了Person对象obj,通过Java传过来的。
- 分别需要调用两个方法,age是整形属性,要获取它的值,对应就需要使用GetIntField方法。setName是返回值为void的方法。所以应该使用CallVoidMethod。
通过上面的分析,得出下面的示例代码。
从上面的分析和示例来看,耐心和细心主要体现在
- 对要访问的属性或者方法要耐心确定类型和名称,并且要保持三个步骤中的类型要一一对应。即调用GetFieldID的类型要以GetXXXField的类型保持一致,方法也是一样。
- 对属性或方法的静态非静态修饰也要留心,通常静态的都需要使用带有static关键字的方法,普通的则不需要。如GetStaticIntField就是对应获取静态整型属性的值,而GetIntField则是获取普通对象的整型属性值。
- 属性相关的设置方法都是类似于SetXField的形式,里面的X代表着具体类型,和前面的类型系统中的类型一一对应,假如是复杂对象,则用Object表示,如SetObjectField。而访问属性只需要将前缀Set换成Get即可。对于静态属性,则是在Set和X之间加上固定的Static,即SetStaticIntField这种形式。
- 方法调用则是以Call为前缀,后面跟着返回值的类型,形如CallXMethod的形式。这里X代表返回值。如CallVoidMethod就表示调用对象的某个返回值为void类型的方法。同样对应的静态方法则是在Call和X之间加上固定的Static,如CallStaticVoidMethod。
向Java世界传递数据更需要耐心。因为我们需要不断地构造对象,组合对象,设置属性。而每一种都是上面Java对象的访问的一种形式。
C/C++构造Java对象和调用方法类似。但是,还是有很多值得关注的细节。根据前面的方法,我们构造对象,首先要知道构造方法的id,而得到id,我们需要得到jclass,构造方法的名字和签名。我们知道在Java世界里,构造方法是和类同名的,但是在C/C++里并不是这样,它有着特殊的名字——<init>,注意,这里的<>不能少。***也就是说无论这个类叫什么,它的构造函数的名字都是<init>。***而函数签名的关键点在于返回值,构造方法的返回值都是void也就是对应签名类型V。
接前面那个Person类的例子,要怎样构造一个Person对象呢。
- 通过JNIEnv的FindClass得到就jclass对象。记得将\’替换成/。
- 根据需要得到合适的构造方法的id。我没有定义构造方法,那么编译器会为它提供一个无参的构造方法。也就是函数签名为()V。调用JNIEnv的GetMethodID得到id。
- 调用JNIEnv的NewObject创建对象,记得传递构造参数。我这里不需要传递。
综上分析,这个创建过程类似于如下示例
上面的示例有个有意思的点,其实示例中创建了两个Java对象,一个是Person对象,另一个是String对象。因为在编程中,String出境的概率太大了,所以JNI提供了这个简便方法。同样特殊的还有数组对象的创建。并且因为数组类型不确定,还有多个版本的创建方法,如创建整型数组的方法是NewIntArray。方法签名也很有规律,都是NewXArray的形式,其中X代表数组的类型,这些方法都需要一个参数,即数组大小。
既然提到了数组,那么数组的设置方法就不得不提。设置数组元素的值也有对应的方法,形如SetXArrayRegion,如SetIntArrayRegion就是设置整型数组元素的值。和Java世界不同的是,这些方法都是支持同时设置多个值的。整形数组的签名是这样——void SetIntArrayRegion(jintArray array,jsize start, jsize len,const jint* buf)第二个参数代表设置值的开始索引,第三个参数是数目,第四个参数是指向真正值的指针。其余类型都是类似的。
有些时候,我们不是在调用native方法时访问对象,而是在将来的某个时间。这在Java世界很好实现,总能找到合适的类存放这个调用时传递进来的对象引用,在后面使用时直接用就可以了。native世界也是这样吗?从使用流程上是一样的,但是从实现方式上却是很大不同。
Java世界是带有GC的,也就是说,将某个临时对象X传递给某个对象Y之后,X的生命周期被转移到了Y上了,X不会在调用结束后被销毁,而是在Y被回收的时候才会一同回收。这种方式在纯Java的世界里没有问题,但是当我们把这个临时对象X传递给native世界,试图让它以Java世界那样工作时,应用却崩溃了,报错JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0xxxxx。为什么同样的操作在Java里面可以,在native却不行呢。问题的根源就是Java的GC。
GC可以通过各种垃圾检测算法判断某个对象是否需要标记为垃圾。而在native世界,不存在GC,为了不造成内存泄漏,只能采取最严格的策略,默认调用native方法的地方就是使用Java对象的地方。所以在native方法调用的作用域结束后,临时对象就被GC标记为垃圾,后面想再使用,可能已经被回收了。还好,强大的JNIEnv类同样提供了方法让我们改变这种默认策略——NewGlobalRef。对象只需要通过这种方式告诉JVM,它想活得更久一点,JVM在执行垃圾检测的时候就不会把它标记为垃圾,这个对象就会一直存。在,直到调用DeleteGlobalRef。这里NewGlobalRef,DeleteGlobalRef是一一对应的,而且最好是再不需要对象的时候就调用DeleteGlobalRef释放内存,避免内存泄漏。
JNI开发会涉及到Java和C/C++开发的知识,在用C/C++实现JNI时,基本思想就是用C/C++语法写出Java的逻辑,也就是一切为Java服务。JNI开发过程中,主要要处理两个问题,函数注册和数据访问。
函数注册推荐使用动态注册,在JNI_OnLoad函数中使用JNIEnv的RegisterNatives注册函数,注意保持Java的native方法和类型签名的一致性,复合类型不要忘记前缀L、后缀;,并将.替换为/。
数据访问首先需要确定访问周期,需要在多个地方或者不同时间段访问的对象,记得使用NewGlobalRef阻止对象被回收,当然还要记得DeleteGlobalRef。访问对象需要先拿到相应的id,然后根据访问类型确定访问方法。设置属性通常是SetXField的形式,获取属性值通常是GetXField的形式。调用方法,需要根据返回值的类型确定调用方法,通常是CallXMethod的形式。当然,这些都是针对普通对象的,假如需要访问静态属性或者方法,则是在普通版本的X前面加上Static。这里的所有X都是指代类型,除了基本类型外,其他对象都用Object替换。
在注册函数和访问数据的时候需要时刻关注的就是数据类型。C/C++数据类型除了基本类型外都不能直接传递到Java里,需要通过创建对象的方式传递。一般的创建对象方式NewObject可以创建任何对象,而对于使用频繁的字符串和数组有对应的快速方法NewStringUTF,NewXArray。向Java传递字符串和数组,这两个方法少不了。
青山不改,绿水长流,咱们下期见!
干了十年 Android 开发,为什么我再也不想继续了
本文最初发布于 Level Up Coding 博客。
在这篇文章中,我将谈谈为什么我在这个行业工作了近十年之后,永远地离开了 Android 开发。在开始之前,让我简单介绍下自己在这个领域的职业生涯。
我从 2013 年开始接触 Android 开发,在当时 Android 4.4 还是热门的新事物。AsyncTasks 还是标准,还有 OttoEventBus 和其他令人讨厌的东西。我见证了架构演进的过程,从 MVC 到 MVP/MVI,然后转向 MVVM,最后是 MVVM 和 MVI 的混合。
我记得,当 RxJava 出现时,一切都突然变成了反应性的,变成了流。我记得,l33t Android Devs(Hi Jake Wharton)在谈论那个新出现的黑马 Kotlin。我记得,Kotlin 兴起并接管了 Android 行业。我记得,Coroutines 出现了,并且起初被认为是“RxJava”的杀手(嘿,你现在可以用同步方式编写所有异步代码了!不需要流了!)。这个理念很吸引人,但很快就被证明,那仅仅是一个好主意而已,因此,像 Channels 这样的底层异步原语成为 Kotlin 的 Rx-Way。不过事实证明,很多使用 Channels 的人都是自断双臂。不得已,精益冷流(Cold Streams)和热流(Hot Streams)的概念被重新引入,请允许我向你介绍:StateFlows 和 SharedFlows,最后,我们得到了一个轻量级的、支持 Coroutines 的 Kotlin 版 RxJava2。
我记得,我和同事 David 围绕状态和事件展开的所有有趣的对话,到底什么是状态,什么是事件?事件对状态有什么作用,反之亦然?我记得,在 Dagger 2 被 Koin 和 Dagger Hilt 取代之前的几年,我熬夜学习 Dagger 2。我还记得,第一次阅读 Uncle Bob 的《架构整洁之道》,这是我在 Android 开发生涯中最开阔眼界的时刻之一。现在,我能够设计和编写几乎所有应用程序,而不需要考虑 MVVM/MVP/MVC 或任何其他特定于平台的细节。我知道为什么测试很重要,我尝试了 TDD,对它是又爱又恨,我还学习了 DDD 和 BDD。
(我选择这个副标题是因为我现在正在从瑞士到德国的火车上写这篇文章。)
后来,我加入了保时捷和 IBM 等大企业的领导团队,这是一段很好的旅程,经过 6-7 年的经验积累,我达到了目标。我曾开发过复杂的应用程序,涉及大量的 E2E 加密、传感器通信、NFC 芯片、BLE Beacon、高流量聊天应用,还有非常有名的待办事项应用,等等。
大约 6 年后,我开始以首席 Android 开发人员的身份参与项目。我学会了识别所参与的大多数项目的核心技术问题(架构和团队成员对某些模式有不同的理解),我还学会了如何指导团队解决这些问题,以及如何成功地完成项目。对我而言,现在新东西仅仅是学习新的 API 变化 / 框架,目的是解决我们已经解决了很多年的问题,只是新的框架 /API 做了更好的处理(不用再手动处理生命周期、Fragment Transaction、XML 布局等)。
很幸运,在过去的 4-5 年里,我在客户项目中从事后端工作(根据要求)。我花了很多时间去了解后端开发的来龙去脉,编写并发代码,创建分布式系统,纵向和横向扩展,处理分布式事务,编写可配置的代码,在不同阶段的环境表现出预期的效果。我研究了不同类型的数据库(图、关系、文档),以及什么样的数据应该使用什么样的数据库,我学习了 Docker 和 K8s,我用 Go 重构 Java EE 系统。看着由 Go 编译出来的二进制文件,它的内存使用率和几乎为零的内存占用,我明白了为什么 Go 如此令人惊叹。作为后端开发人员,我所解决的问题与我在 Android 开发中遇到的挑战无法相提并论(我很快就会讲到),作为后端开发者,我所解决的问题比在 Android 上推敲像素影响更大、更深远。
最终,我厌倦了与 UI/UX 设计师的所有会议,厌倦了向他们解释 Material Design 的基本原理,或者为什么我们不能触发应用程序 Y(不是我们开发的)中那样的行为 X,我记得,好几个小时的设计讨论都让我的大脑直接宕机了。不少项目都会出现这种情况,其中一些项目具有一定的复杂性,一旦团队理解了整洁架构和领域驱动开发,我们就能在很短的时间内写出领域 + 数据层。一旦你向团队解释清楚了各种身份认证流程,恰当处理令牌刷新逻辑就变成小菜一碟了。主要的挑战几乎总是出现在 UI 层,由于框架 API 不断发展变化,UI 层也在不断变化。在很大程度上,UI 层受 UI/UX 设计师和 PO 影响。最近,几乎所有的项目都变成了日常工作,对工程的关注少,对业务、实施的关注多,几乎一直在摆弄 Android API。在最好的情况下,会有一个令人耳目一新的任务,如编写一个自定义视图,并使用一些线性代数的知识。但通常情况下,几乎总是一些无聊的事情,反思这一切,我问自己:这对我有什么好处?当然我赚了很多钱,但我马上就要 30 岁了,几年后,我将在哪里?
作为一名经验丰富的 Android 开发人员,我只适合 Android 职位。我所有的技能都是为了能开发出可维护的、整洁的、能在 Android 平台上正常工作的代码。有些代码会被垃圾收集器如期杀死,而有些代码能在垃圾收集器中存活下来,因为它本该如此。如果 Android 很快消失了怎么办?看着像 Flutter 这样优秀的技术,人们已经用它开发出了一些很棒的应用,我不会再把任何新项目作为单独的原生 iOS 和原生 Android 应用来启动。老实说,你的 Android 技能对于大多数公司的首席 / 资深软件工程师职位来说价值并不高。
我成功地完成了自己的最后一个项目。现在是时候做一些改变了。我不想再花几天的时间讨论 CardView 的边框或反复出现的毫无意义的问题,比如是使用单选按钮还是复选框。我不想再为了更好地处理 Android 生命周期或导航而学习新的 Android 库,然后在未来 12 个多月内看它们再次被替换,在过去的 10 年里,这种事我已经做过好多次了。开发人员一代接一代,每一代中都会有人觉得自己有权力编写一个新的库来处理 UI 状态,或者编写一个新的导航库。测试?不,没有。可悲的是,有很多开发者会使用这些库。Android 开发正慢慢被吞噬 Web 开发的混乱所吞噬(你试过安装 create-react-app 吗?你会下载数以千计的库,包括一些易受攻击的库)。
幸运的是,在过去几年里,我曾在几个项目中从事后端工作,这使我有机会过渡到后端开发,彻底离开安卓,专注于开发每秒处理数十万用户请求的系统,这对我来说非常有吸引力。现在,路线图上有一些我作为 Android 开发者不了解的新东西:获得 K8s 认证,掌握多个云,深入学习特定数据库,深入理解 DevOps。我感觉,编程的神秘性再次激发了我,有复杂的工程问题需要处理,这让人兴奋。
让人难过的是,对于一个纯粹的 Android 开发者来说,架构师或首席 / 资深工程师的道路是封闭的。纯粹的 Android 开发人员根本不具备履行这些职位所需的技能。对我来说,这是一次很棒的旅程,但我再也不会以 Android 开发人员的身份参与项目了。
https://levelup.gitconnected.com/why-i-left-android-development-after-10-years-and-became-a-backend-developer-86ebf3595d43
在安卓系统上开发一款软件详细的流程
安卓app软件开发流程是一个系统而复杂的过程,涉及多个阶段和环节。以下是一个典型的安卓软件开发流程概述:
目的:了解用户需求,确定APP的目标、功能、特性和预期效果。
活动:开发团队与客户进行深入沟通,收集并分析需求,明确功能和设计方向。
输出:需求文档、功能列表等。
目的:创建APP的初始设计,包括界面布局、交互流程和业务流程等。
活动:根据需求分析结果,制作APP的原型图,如功能列表、用户体验流程等。
输出:原型图、交互设计文档等。
目的:进行界面设计,确保用户界面的可用性、交互性和视觉效果。
活动:设计师根据APP的类型、用户定位和企业标准色等,设计APP页面和各种元素。
输出:UI设计图、设计规范等。
振翕科技app开发
目的:为开发过程准备必要的开发环境和工具。
活动:下载并安装Android Studio等集成开发环境(IDE),配置JDK、Android SDK、Gradle等开发工具。
输出:配置好的开发环境。
目的:根据设计文档和原型图,编写代码实现APP的各项功能。
活动:开发人员使用Java或Kotlin等编程语言,在Android Studio中进行编码和调试。
输出:可运行的APP原型。
目的:确保APP的质量和稳定性,发现并修复潜在的问题和错误。
活动:进行功能测试、性能测试、兼容性测试等多个层面的测试工作,使用模拟器和真实设备对APP进行测试。
输出:测试报告、修复后的APP版本。
目的:将APP提交到应用商店进行审核和发布,供用户下载和使用。
活动:准备应用信息、图标、截图、描述和权限等,提交到Google Play Store等应用商店进行审核。
输出:上线后的APP链接和下载量等数据。
目的:持续监控APP的运行情况,收集用户反馈并进行更新和维护。
活动:关注APP的下载量、用户活跃度、崩溃率等指标,修复用户反馈的问题和bug,更新和改进应用的功能和性能。
输出:持续优化的APP版本和更好的用户体验。
开发环境:Android Studio(基于IntelliJ IDEA的IDE,集成了Gradle构建工具)
编程语言:Java、Kotlin(谷歌官方推荐)
设计工具:Photoshop、Illustrator、Axure等(用于UI设计和交互设计)
测试工具:Android SDK提供的测试框架、模拟器和真实设备等
在整个开发过程中,需要保持与开发团队的紧密沟通,确保需求的准确传达和实现,遵循代码规范和最佳实践,提高代码的可读性和可维护性。注重用户体验和性能优化,确保APP的流畅运行和良好反馈。
通过以上流程,可以系统地开发出一款符合用户需求和期望的安卓app软件。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。