一学就会,一面就过!🔥客户端校招高频面试问题+解析

关于客户端开发如果有问题可以私信或者看我之前的动态。

本人bg 211本,25届客户端校招生,主要大厂offer基本集齐了,一半是ssp。本次结合题主秋招经历,为大家带来客户端校招高频面试问题,吃透轻松进大厂!

不是说什么都不会光看这些就能面了,该学的还得学,只是对于我列出的问题可以更深入了解一下。

有问题欢迎评论,题主尽量解答。欢迎补充。

安卓篇

四大组件篇

  • Activity有哪几种启动模式,分别讲讲。

Activity的启动模式有四种。standard:这是默认模式。每次启动一个Activity,都会创建一个新的实例,即使这个Activity已经在任务栈里有了。singleTop:如果启动的Activity在任务栈的栈顶,那么就不会创建新的实例,而是直接复用栈顶的那个Activity,并调用它的onNewIntent方法。如果不在栈顶,就会创建新的实例。singleTask:这种模式比较特殊。它会先检查整个任务栈中是否存在该Activity的实例。如果存在,它会把这个Activity上面的所有Activity都清除掉,让它成为栈顶,并调用它的onNewIntent方法。如果不存在,就在新的任务栈中创建它的实例,或者在当前任务栈中创建。singleInstance:这是最严格的模式。它会创建一个新的任务栈,并且这个任务栈里只会有这一个Activity实例。后续再启动它,都会直接复用这个唯一的实例,并调用onNewIntent。

  • Activity和Fragment的区别是什么,怎么通信,谁更快。

Activity是一个完整的应用组件,通常对应一个屏幕界面。而Fragment可以理解为Activity界面的一部分,或者说是Activity的一个模块化组件。一个Activity可以包含多个Fragment,也可以在多个Activity中复用同一个Fragment。它们之间的通信方式有很多。Activity给Fragment传数据,通常是在创建Fragment时通过Bundle传参数。Fragment给Activity传数据,可以通过定义接口回调的方式。两者之间也可以通过ViewModel共享数据。更直接的也可以通过FragmentManager找到对方实例直接调用方法。 至于谁更快,这个说法不太准确。Activity和Fragment都是在应用进程中运行的,它们本身不是性能瓶颈。Fragment的引入主要是为了UI的模块化和复用,以及更好地适配不同屏幕尺寸,提升开发效率和用户体验,而不是为了执行速度。

  • Activity和Window的关系?

Activity和Window的关系可以说是一种包含关系。一个Activity在启动的时候,系统会为它创建一个Window对象。这个Window对象其实就是个抽象的概念,它代表了一个屏幕上的显示区域。所有的用户界面,比如我们看到的View组件,都是绘制在这个Window上的。Activity负责管理生命周期和事件处理,而Window负责把这些界面内容呈现出来。可以理解为Activity是舞台上的演员和导演,Window是舞台本身。

  • 全屏Activity A中启动了一个非全屏Activity B,然后返回,A, B的生命周期具体是怎么变化的?

首先,A是全屏的,所以它正常运行。当A启动B时: A会先调用 onPause()。B开始创建,依次调用 onCreate() -> onStart() -> onResume()。因为B是非全屏的,A并没有完全被覆盖,所以A只是暂停了,但它的 onStop() 不会被调用,它依然可见但处于非活动状态。当B返回(例如按返回键或调用 finish())时: B会先调用 onPause()。A重新回到前台,会调用 onRestart() -> onStart() -> onResume()。B接着调用 onStop() -> onDestroy(),被销毁。

  • Activity和Service怎么通信?

Activity和Service通信主要有几种方式:通过Intent:这是最简单的,Activity启动Service时可以把数据放在Intent里传过去。Service执行完任务,也可以通过广播通知Activity结果。通过Binder:这是最常用的方式。Activity通过 bindService() 方法绑定Service后,Service会返回一个Binder对象。Activity拿到这个Binder对象后,就可以调用Binder里定义的方法,直接与Service进行双向通信。这个Binder可以是一个IBinder接口的实现,通常是Service内部的一个内部类。通过广播:Activity发送广播,Service注册广播接收器来接收消息。反之,Service也可以发送广播,Activity注册接收器来接收。通过Messenger:这是基于Binder的封装,更简单,可以在不同的进程间进行通信。Activity创建一个Messenger,Service也创建一个Messenger,通过Message对象来传递数据。

  • Service的几种启动方式?生命周期分别是什么样的?

Service主要有两种启动方式:startService(): 生命周期:onCreate() -> onStartCommand()。一旦启动,Service就会一直在后台运行,直到显式调用 stopService() 或 stopSelf() 才会停止,或者系统内存不足时才可能被杀死。它和调用者之间没有直接绑定关系。即使启动它的组件被销毁了,它也可以继续运行。bindService(): 生命周期:onCreate() -> onBind()。当所有绑定的客户端都解绑后,Service会调用 onUnbind() -> onDestroy() 被销毁。它的生命周期是与绑定它的客户端绑定的,客户端存在它就存在,客户端都解绑了它就销毁。混合使用:一个Service可以先 startService() 后 bindService(),也可以反过来。如果先 startService() 再 bindService(),它的停止需要 stopService() 和所有 unbindService() 都执行了,它才会销毁。如果先 bindService() 再 startService(),同样需要两者都操作了才会销毁。通常,onStartCommand() 用于处理任务,onBind() 用于提供接口服务。

  • 广播的种类?有序广播怎么实现的?

广播主要有两种:标准广播:也叫无序广播。发送后所有的接收器几乎是同时收到,接收器之间没有顺序,也不能截断。有序广播:发送后,广播会按照接收器的优先级(在清单文件里配置,或者动态注册时设置)从高到低依次传递。优先级高的先收到,然后可以决定是否截断广播(调用 abortBroadcast()),或者修改传递给下一个接收器的数据。有序广播的实现原理就是,发送一个有序广播时,系统会根据接收器的优先级,把它们排成一个队列。广播会从优先级最高的接收器开始依次传递。每个接收器在 onReceive() 方法里收到广播后,可以调用 setResultData() 传递数据给下一个,或者调用 abortBroadcast() 来终止广播的继续传递。

  • 如果2个有序广播优先级一样,会怎么样?

如果两个有序广播接收器的优先级一样,系统会根据它们在清单文件中的声明顺序,或者动态注册的顺序来决定哪个先收到。通常是先声明或者先注册的那个会先收到广播。虽然优先级相同,但系统内部会有一个确定的顺序。

  • 静态Receiver和动态Receiver的区别?什么广播都能被静态接收到吗?

静态Receiver:是在 AndroidManifest.xml 文件中注册的。它的优点是即使应用程序没有启动,或者已经被杀死了,只要有匹配的广播发过来,它也能被唤醒并接收。但缺点是,它会占用一些系统资源,而且只能接收那些明确指定了Action的广播。动态Receiver:是在代码中通过 Context.registerReceiver() 方法注册的。它的优点是更灵活,可以根据需要随时注册和注销,生命周期与注册它的组件(如Activity或Service)绑定。缺点是,只有当注册它的组件处于活动状态时,它才能接收广播。一旦组件销毁,它就无法接收了。不是所有广播都能被静态接收到。Android 8.0(Oreo)之后,对静态广播接收器做了限制,大部分隐式广播(系统广播)都不能被静态接收器接收了,主要是为了优化系统性能和电池寿命。明确指定包名的显式广播或者一些特定的系统广播(比如 BOOT_COMPLETED)还是可以被静态接收的。

  • 简单介绍下ContentProvider。

ContentProvider是一个用于在不同应用程序之间共享数据的组件。你可以把它想象成一个公共的数据仓库,它提供了一套统一的接口(URI、CRUD操作),允许其他应用安全地访问和修改你的应用内部的数据,而不用关心你的数据具体是怎么存储的(比如是存储在数据库、文件还是网络)。它主要用于解决跨应用数据共享的问题,并且提供了权限管理机制,确保数据的安全性和完整性。使用的时候需要通过ContentResolver来与ContentProvider进行交互。

Framework篇

  • 桌面点击一个APP的图标,实际发生了什么?

桌面点击APP图标,会触发一系列复杂的系统操作。首先,Launcher应用(桌面应用)会捕获到点击事件。Launcher根据图标信息,通过IPC机制(比如Binder)向ActivityManagerService发起请求,请求启动目标应用的入口Activity。ActivityManagerService收到请求后,会检查目标应用进程是否已经存在。 如果不存在,Zygote进程会fork出一个新的进程给目标应用。新进程会加载应用程序的运行时环境,比如虚拟机。如果进程已存在,就直接在这个进程中启动Activity。新进程启动后,会初始化主线程的Looper和Handler。然后,ActivityThread这个类会作为应用的主线程,负责调度和管理Activity的生命周期。它会通过Binder调用ActivityManagerService,请求启动具体的Activity。ActivityManagerService会再通知ActivityThread去创建并初始化这个Activity,包括调用 onCreate()、onStart()、onResume() 等生命周期方法。最后,Activity的界面被渲染并显示在屏幕上,用户就可以看到应用了。

  • 点击屏幕到按钮做出响应,实际发生了什么?

这个过程涉及硬件、内核、系统服务和应用层。首先,当你点击屏幕时,触摸屏控制器(硬件)会检测到这个触摸事件,并生成中断信号。这个中断信号会发送给操作系统的内核。内核中的触摸驱动程序会读取触摸屏数据,把物理坐标转换成逻辑事件,比如按下、移动、抬起等,然后封装成InputEvent事件对象。InputEvent事件会被传递给InputManagerService。InputManagerService是一个系统服务,它会接收来自所有输入设备的事件。InputManagerService会根据当前焦点窗口,把这个InputEvent事件通过Binder机制派发给相应的应用程序进程。在应用程序进程中,事件首先会被ViewRootImpl接收。ViewRootImpl会把这个事件传递给顶层的DecorView。DecorView会向下层遍历整个View树,寻找能够处理这个事件的View。这个过程叫做事件分发,它会依次调用dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent() 等方法。如果目标View(比如你点击的按钮)的 onTouchEvent() 方法处理了这个事件,或者它设置了点击监听器(OnClickListener),那么监听器的 onClick() 方法就会被回调,从而执行按钮响应的逻辑。

  • 四大组件分别在什么情况下会ANR?

ANR是指Application Not Responding,也就是应用无响应。四大组件触发ANR的常见情况是:Activity:在主线程中执行耗时操作,导致 onCreate()、onResume() 或其他生命周期方法在5秒内没有完成。或者,事件处理(如点击事件的 onClick())在主线程中执行耗时任务,导致10秒内没有响应用户输入。Service:在主线程中执行耗时操作。对于前台Service,onStartCommand() 在20秒内没有执行完毕。对于后台Service,onStartCommand() 在200秒内没有执行完毕。BroadcastReceiver:在主线程的 onReceive() 方法中执行耗时操作,导致10秒内没有执行完毕。因为 onReceive() 是运行在主线程的。ContentProvider:在主线程中执行 query()、insert()、update()、delete() 等方法时,耗时过长,导致主线程阻塞。

  • 进程间通信机制?如果我要传一个Bitmap呢?

Android的进程间通信机制主要是基于Binder。Binder是Android特有的一种高效IPC机制,它是一种CS架构,通过系统内核提供的Binder驱动来实现。除了Binder,还有一些其他的IPC方式,比如:文件共享:通过读写同一份文件来实现数据共享,但效率较低,且有并发问题。共享内存:效率很高,但实现复杂,需要自己管理同步。Socket:可以进行网络通信,也能用于本地进程间通信,但通常更重。AIDL:Android接口定义语言,是基于Binder的,它简化了Binder的开发,允许你定义接口,系统会自动生成客户端和服务端的代理类和Stub类。Messenger:也是基于Binder的封装,更简单,通过Message对象来传递数据。ContentProvider:用于在不同应用间共享数据,底层也可能用到Binder。如果要传输一个Bitmap,因为Bitmap对象通常比较大,直接通过Binder传输会有一些限制。Binder对传输的数据大小有限制,通常是1MB左右。比较好的做法是:将Bitmap压缩后转为字节数组:把Bitmap压缩成PNG或JPEG格式的字节数组,然后通过Binder(比如通过AIDL接口的byte[]参数)传输。接收方再从字节数组解码回Bitmap。使用FileProvider或Uri:将Bitmap保存到文件,然后通过FileProvider生成一个Uri,将Uri传递给另一个进程。另一个进程通过ContentResolver和Uri去读取文件流,再加载成Bitmap。共享内存:如果两个进程都对Bitmap进行频繁操作,可以考虑使用共享内存,但实现会比较复杂。

  • View和ViewGroup的绘制流程?

View和ViewGroup的绘制流程主要分为三个阶段:测量、布局和绘制。

测量(Measure): 这个阶段决定了View或ViewGroup自身以及其子View的尺寸大小。从View树的根节点(通常是DecorView)开始,调用measure()方法。ViewGroup的onMeasure()方法会遍历所有子View,依次调用子View的measure()方法,并根据自身的MeasureSpec和子View的布局参数来计算子View的MeasureSpec,然后子View再计算自己的尺寸。最后,ViewGroup根据所有子View的尺寸和自身的布局要求,调用setMeasuredDimension()来确定自己的测量尺寸。

布局(Layout): 这个阶段确定了View或ViewGroup在父容器中的位置,以及子View在其内部的位置。同样从根节点开始,调用layout()方法。ViewGroup的onLayout()方法会遍历所有子View,并为每个子View计算它的左上角和右下角坐标,然后调用子View的layout()方法来设置其位置。View的onLayout()方法通常是空的,因为它没有子View,只需确定自身相对于父容器的位置。

绘制(Draw): 这个阶段将View或ViewGroup的内容真正地渲染到屏幕上。同样从根节点开始,调用draw()方法。ViewGroup的draw()方法会按照绘制顺序进行:绘制背景 -> 绘制自己 -> 绘制子View -> 绘制滚动条和前景。在绘制子View时,ViewGroup会遍历调用每个子View的draw()方法。View的onDraw()方法负责绘制自己的内容,比如文本、图片、形状等。绘制时会使用Canvas和Paint对象。整个绘制过程是自上而下、先父后子的顺序。

Handler篇

  • 简单讲解Handler的机制。

Handler是Android中一套用于处理线程间通信的机制,特别是用于将子线程的耗时操作结果更新到主线程UI。它的核心原理是消息队列。主线程会有一个Looper,它会不断地从一个MessageQueue(消息队列)中取出Message。每个Handler都与一个Looper和它的MessageQueue关联。当你创建一个Handler时,它会默认关联当前线程的Looper(如果是主线程,就是主线程的Looper)。当你通过Handler发送一个Message或Runnable时,这个消息会被加入到与Handler关联的MessageQueue中。Looper不断地循环,一旦从MessageQueue中取到消息,就会把这个消息分发给对应的Handler去处理。这样就实现了子线程发送消息,主线程接收并处理消息,从而安全地更新UI。

  • postDelayed是怎么实现的?

postDelayed() 的实现其实也是依赖于 MessageQueue 的一个特性。当你调用 handler.postDelayed(runnable, delayMillis) 时,它会把这个 Runnable 封装成一个 Message 对象。这个 Message 对象里会设置一个 when 字段,表示这个消息应该在未来的哪个时间点被处理。这个 when 值就是当前系统时间加上 delayMillis。然后,这个带有延时信息的 Message 会被插入到 MessageQueue 中。MessageQueue 并不是简单地先进先出,它会根据消息的 when 字段进行排序,确保延时消息在指定时间之后才会被 Looper 取出。Looper 在循环取消息时,如果发现队头消息的 when 时间还没到,它就会让当前线程休眠一段时间,直到消息的 when 时间到达或者有更紧急的消息插队。当时间到了,Looper 取出这个消息,再交回给 Handler 去执行 Runnable 的 run() 方法。

  • Handler和Thread的关系?

Handler和Thread的关系是密切的,Handler机制是用来解决多线程通信问题的。一个Thread可以没有Looper和Handler,它就是一个普通的线程,可以执行耗时任务。如果一个Thread想要通过Handler来发送和接收消息,那么这个线程就必须有自己的Looper和MessageQueue。通常情况下: 主线程(UI线程)天生就带有一个Looper和MessageQueue,所以我们可以在任何地方直接创建Handler来发送消息到主线程。子线程默认是没有Looper的。如果想在子线程中使用Handler来处理自己的消息(比如实现一个Looper线程),就需要手动调用Looper.prepare()来创建Looper,然后调用Looper.loop()来启动消息循环。这样,在这个子线程中创建的Handler就会和这个子线程的Looper关联起来。 所以,可以说Handler是Thread之间,尤其是子线程与主线程之间进行安全通信的桥梁。

  • MessageQueue是怎么实现的?怎么插入新的Message?

MessageQueue在底层是一个单链表来实现的,它不是一个真正的队列,更像是一个有序列表。插入新的Message:当你通过Handler发送一个Message时,实际上是调用了MessageQueue的enqueueMessage()方法。这个方法会根据Message的when字段(也就是消息的执行时间)来决定把它插入到链表的哪个位置,以保持链表的有序性(按执行时间从小到大排序)。如果是一个普通消息,when时间就是0,它会被插入到队尾。如果是一个延时消息,它会被插入到链表中合适的位置,确保比它先执行的消息在它前面,比它后执行的消息在它后面。MessageQueue还负责处理消息的同步屏障,但这是更深层的话题。

  • Looper的底层实现?

Looper的底层实现是一个无限循环。当你调用Looper.prepare()时,它会为当前线程创建一个Looper对象,并把它存放在ThreadLocal中,确保每个线程只有一个Looper。同时,也会为这个Looper创建一个MessageQueue。当你调用Looper.loop()时,这个方法就会进入一个死循环。在这个循环里,Looper会不断地调用MessageQueue的next()方法来取出消息。MessageQueue.next()方法会阻塞当前线程,直到有新的消息到来或者有延时消息达到执行时间。一旦next()方法取到消息,Looper就会把这个消息分发给对应的Handler去处理(通过调用msg.target.dispatchMessage(msg))。处理完一个消息后,Looper会清空这个消息,然后继续下一次循环,等待下一个消息。 这个无限循环保证了只要线程存在,并且有消息需要处理,Looper就能一直工作。当Looper的quit()方法被调用时,循环才会终止。

JNI篇

  • JNI的基本工作流程是怎样的?

JNI的基本工作流程可以概括为几个步骤。首先,在Java层声明一个native方法,这个方法没有实现体。然后,通过System.loadLibrary()方法加载对应的本地库(也就是包含C/C++实现代码的.so文件)。接下来,根据Java层native方法的签名,在C/C++层编写对应的实现函数。这个函数名有特定的格式,比如Java_包名_类名_方法名。在C/C++实现函数中,可以使用JNI提供的API来操作Java对象、调用Java方法、访问Java字段等。最后,将C/C++代码编译成动态链接库(.so文件),放到Android项目的jniLibs目录下。当Java层调用那个native方法时,实际上就是通过JNI找到了C/C++中的对应实现函数并执行它。

  • 如何在JNI中处理字符串?jstring 是什么?如何将其转换为C/C++字符串,又如何将C/C++字符串返回给Java?

在JNI中,jstring是Java字符串在C/C++层的一种表示,它是一个指向Java字符串对象的引用。你不能直接像C/C++字符串那样操作它。从Java jstring 转换为C/C++字符串:通常使用JNIEnv提供的函数。比如,GetStringUTFChars(jstring, isCopy)可以获取一个指向UTF-8编码的C风格字符串的指针。用完后,一定要调用ReleaseStringUTFChars(jstring, char*)来释放内存,避免内存泄漏。从C/C++字符串返回给Java jstring:使用JNIEnv的NewStringUTF(const char*)函数。这个函数会接收一个UTF-8编码的C风格字符串,然后在Java堆上创建一个新的String对象,并返回它的jstring引用。

  • JNIEnv是什么?它的生命周期是怎样的?

JNIEnv可以理解为JNI环境的上下文指针,它是一个非常重要的结构体指针。通过JNIEnv,我们才能调用JNI提供的各种函数,比如创建Java对象、调用Java方法、访问Java字段等等。它就像一座桥梁,连接着Java虚拟机和你的C/C++代码。JNIEnv的生命周期是与当前线程绑定的。每个通过JNI进入本地方法的Java线程都会有一个JNIEnv指针。JNIEnv只在当前线程中有效,不能跨线程使用。也就是说,你不能在一个线程中获取JNIEnv指针,然后在另一个线程中使用它。当线程退出时,它对应的JNIEnv也会失效。如果在C/C++层创建了一个新的线程,并且这个线程需要和Java虚拟机交互,那么它必须先通过JavaVM指针的AttachCurrentThread()方法来“附着”到Java虚拟机,才能获得一个有效的JNIEnv指针。用完后,还需要调用DetachCurrentThread()来“分离”。

  • Local Reference 和 Global Reference 的区别是什么?它们各自的用途和管理方式?

这是JNI中管理Java对象引用的两种方式。Local Reference(局部引用): 它是JNI函数的默认返回类型,比如NewObject()、FindClass()等。生命周期非常短,通常在本地方法执行完毕后,Java虚拟机就会自动释放所有局部引用。它们被存储在JVM的本地引用表中。如果在一个本地方法中创建了大量的局部引用,可能会导致本地引用表溢出。用途:主要用于在单个本地方法内部临时持有Java对象。管理:一般不需要手动管理,方法结束自动释放。但如果创建大量局部引用,可以在循环中手动调用DeleteLocalRef()提前释放。Global Reference(全局引用): 需要通过NewGlobalRef(JNIEnv* env, jobject obj)函数手动创建。生命周期较长,除非你手动调用DeleteGlobalRef(JNIEnv* env, jobject globalRef)释放,否则它会一直有效,即使本地方法已经返回了。全局引用可以跨方法、跨线程使用。用途:当你需要在多个本地方法调用之间,或者在C/C++线程中长期持有Java对象时使用,比如持有Java回调对象。管理:必须手动管理。创建后,当不再需要时,一定要调用DeleteGlobalRef()来释放,否则会导致Java对象无法被垃圾回收,造成内存泄漏。

  • 如何在JNI中访问Java对象的字段和方法?

在JNI中访问Java对象的字段和方法需要几个步骤。

访问字段:首先,你需要通过FindClass()函数获取目标Java类的jclass引用。然后,通过GetFieldID(jclass, fieldName, signature)函数获取字段的jfieldID。fieldName是字段名,signature是字段的类型签名(比如"I"代表int,"Ljava/lang/String;"代表String)。接着,根据字段的类型,使用Get<Type>Field(jobject, jfieldID)或Set<Type>Field(jobject, jfieldID, value)来获取或设置实例字段的值。对于静态字段,使用GetStatic<Type>Field()和SetStatic<Type>Field()。

访问方法:同样,先获取目标Java类的jclass引用。然后,通过GetMethodID(jclass, methodName, signature)函数获取方法的jmethodID。methodName是方法名,signature是方法的描述符,它包含了参数类型和返回类型。最后,根据方法的返回类型和参数,使用Call<Type>Method(jobject, jmethodID, ...)来调用实例方法。对于静态方法,使用CallStatic<Type>Method()。对于构造函数,使用NewObject()。

  • 如何加载Native库?System.loadLibrary() 和 System.load() 的区别?

加载Native库是使用JNI的第一步,这通常在Java层完成。System.loadLibrary(String libName): 这是更常用、推荐的方式。它接收的是库的名字,不带lib前缀和.so后缀。例如,你的库文件是libmylib.so,你就传"mylib"。它会在系统预定义的路径中查找对应的库文件(例如/system/lib、应用的jniLibs目录等),找到后加载。加载时会自动处理库文件的平台差异(比如32位或64位)。System.load(String pathName): 它接收的是库文件的完整路径。例如,你需要传入"/data/data/com.example.app/lib/libmylib.so"。这种方式比较灵活,你可以从任何路径加载库,但需要你知道确切的路径。通常在一些特殊场景下使用,比如从外部存储加载插件库,或者路径不固定的时候。总结来说,loadLibrary()更方便和平台无关,load()更灵活但需要指定完整路径。在Android开发中,通常推荐使用loadLibrary()。

  • JNI的线程模型?

JNI的线程模型是基于Java虚拟机对线程的管理。Java线程调用Native方法:当一个Java线程直接调用一个Native方法时,Java虚拟机(JVM)会自动为这个线程准备好JNIEnv指针,并作为第一个参数传递给Native方法。在这个Native方法内部,这个JNIEnv指针是有效的,并且只能在这个线程中使用。Native线程(C/C++创建的线程)与JVM交互: 如果C/C++代码自己创建了一个新的线程(比如通过pthread_create),并且这个Native线程需要调用Java方法或访问Java对象,那么这个线程就必须先“附着”到Java虚拟机。这通过JavaVM指针的AttachCurrentThread()或AttachCurrentThreadAsDaemon()方法来实现。成功附着后,会得到一个有效的JNIEnv指针。当Native线程完成与JVM的交互后,应该调用DetachCurrentThread()方法来“分离”自身,释放资源。 所以,JNI的线程模型要求每个需要与JVM交互的线程都必须有自己的JNIEnv指针,并且JNIEnv是线程局部的。跨线程使用JNIEnv会导致问题。同时,Native线程需要显式地附着和分离JVM。

最后如果想投腾讯,可以用我的内推码哦。可以帮查进度,也可以私信定向推。

@腾讯招聘

#腾讯大前端岗位热招中#
全部评论
无敌
点赞 回复 分享
发布于 08-27 12:00 河南
给佬跪了
点赞 回复 分享
发布于 08-26 22:28 重庆
佬,泪目
点赞 回复 分享
发布于 08-26 18:07 北京

相关推荐

08-27 12:02
已编辑
南京外国语学校 网络安全
再来一遍:实则劝各位不要all in华子,不要相信华为hr
点赞 评论 收藏
分享
评论
5
10
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务