当前位置: 首页 > news >正文

探索Android架构设计

Android 应用架构设计探索:MVC、MVP、MVVM和组件化

MVC、MVP和MVVM是常见的三种架构设计模式,当前MVP和MVVM的使用相对比较广泛,当然MVC也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和前面三种架构设计不是一个层次的。它们之间的关系是,组件化的各个组件可以使用前面三种架构设计。我们只有了解了这些架构设计的特点之后,才能在进行开发的时候选择适合自己项目的架构模式,这也是本文的目的。

1、MVC

MVC (Model-View-Controller, 模型-视图-控制器),标准的MVC是这个样子的:

  • 模型层 (Model):业务逻辑对应的数据模型,无View无关,而与业务相关;
  • 视图层 (View):一般使用XML或者Java对界面进行描述;
  • 控制层 (Controllor):在Android中通常指Activity和Fragment,或者由其控制的业务类。

Activity并非标准的Controller,它一方面用来控制了布局,另一方面还要在Activity中写业务代码,造成了Activity既像View又像Controller。

在Android开发中,就是指直接使用Activity并在其中写业务逻辑的开发方式。显然,一方面Activity本身就是一个视图,另一方面又要负责处理业务逻辑,因此逻辑会比较混乱。

这种开发方式不太适合Android开发。

2、MVP

2.1 概念梳理

MVP (Model-View-Presenter) 是MVC的演化版本,几个主要部分如下:

  • 模型层 (Model):主要提供数据存取功能。
  • 视图层 (View):处理用户事件和视图。在Android中,可能是指Activity、Fragment或者View。
  • 展示层 (Presenter):负责通过Model存取书数据,连接View和Model,从Model中取出数据交给View。

所以,对于MVP的架构设计,我们有以下几点需要说明:

  1. 这里的Model是用来存取数据的,也就是用来从指定的数据源中获取数据,不要将其理解成MVC中的Model。在MVC中Model是数据模型,在MVP中,我们用Bean来表示数据模型。
  2. Model和View不会直接发生关系,它们需要通过Presenter来进行交互。在实际的开发中,我们可以用接口来定义一些规范,然后让我们的View和Model实现它们,并借助Presenter进行交互即可。

为了说明MVP设计模式,我们给出一个示例程序。你可以在Github中获取到它的源代码。

2.2 示例程序

在该示例中,我们使用了:

  1. 开眼视频的API作为数据源;
  2. Retrofit进行数据访问;
  3. 使用ARouter进行路由;
  4. 使用MVP设计模式作为程序架构。

下面是该模块的基本的包结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里核心的代码是MVP部分。

这里我们首先定义了MVP模式中的最顶层的View和Presenter,在这里分别是BaseViewBasePresenter,它们在该项目中是两个空的接口,在一些项目中,我们可以根据自己的需求在这两个接口中添加自己需要的方法。

然后,我们定义了HomeContract。它是一个抽象的接口,相当于一层协议,用来规定指定的功能的View和Presenter分别应该具有哪些方法。通常,对于不同的功能,我们需要分别实现一个MVP,每个MVP都会又一个对应的Contract。笔者认为它的好处在于,将指定的View和Presenter的接口定义在一个接口中,更加集中。它们各自需要实现的方法也一目了然地展现在了我们面前。

这里根据我们的业务场景,该接口的定义如下:

public interface HomeContract {interface IView extends BaseView {void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists);void setNextPage(List<HomeBean.IssueList.ItemList> itemLists);void onError(String msg);}interface IPresenter extends BasePresenter {void requestFirstPage();void requestNextPage();}
}

HomeContract用来规定View和Presenter应该具有的操作,在这里它用来指定主页的View和Presenter的方法。从上面我们也可以看出,这里的IViewIPresenter分别实现了BaseViewBasePresenter

上面,我们定义了V和P的规范,MVP中还有一项Model,它用来从网络中获取数据。这里我们省去网络相关的具体的代码,你只需要知道APIRetrofit.getEyepetizerService()是用来获取Retrofit对应的Service,而getMoreHomeData()getFirstHomeData()是用来从指定的接口中获取数据就行。下面是HomeModel的定义:

public class HomeModel {public Observable<HomeBean> getFirstHomeData() {return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis());}public Observable<HomeBean> getMoreHomeData(String url) {return APIRetrofit.getEyepetizerService().getMoreHomeData(url);}
}

OK,上面我们已经完成了Model的定义和View及Presenter的规范的定义。下面,我们就需要具体去实现View和Presenter。

首先是Presenter,下面是我们的HomePresenter的定义。在下面的代码中,为了更加清晰地展示其中的逻辑,我删减了一部分无关代码:

public class HomePresenter implements HomeContract.IPresenter {private HomeContract.IView view;private HomeModel homeModel;private String nextPageUrl;// 传入View并实例化Modelpublic HomePresenter(HomeContract.IView view) {this.view = view;homeModel = new HomeModel();}// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调@Overridepublic void requestFirstPage() {Disposable disposable = homeModel.getFirstHomeData()// .....subscribe(itemLists -> { view.setFirstPage(itemLists); },throwable -> { view.onError(throwable.toString()); });}// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调@Overridepublic void requestNextPage() {Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)// .....subscribe(itemLists -> { view.setFirstPage(itemLists); },throwable -> { view.onError(throwable.toString()); });}
}

从上面我们可以看出,在Presenter需要将View和Model建立联系。我们需要在初始化的时候传入View,并实例化一个Model。Presenter通过Model获取数据,并在拿到数据的时候,通过View的方法通知给View层。

然后,就是我们的View层的代码,同样,我对代码做了删减:

@Route(path = BaseConstants.EYEPETIZER_MENU)
public class HomeActivity extends CommonActivity<ActivityEyepetizerMenuBinding> implements HomeContract.IView {// 实例化Presenterprivate HomeContract.IPresenter presenter;{presenter = new HomePresenter(this);}@Overrideprotected int getLayoutResId() {return R.layout.activity_eyepetizer_menu;}@Overrideprotected void doCreateView(Bundle savedInstanceState) {// ...// 使用Presenter请求数据presenter.requestFirstPage();loading = true;}private void configList() {// ...getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {@Overridepublic void onScrolled(RecyclerView recyclerView, int dx, int dy) {// 请求下一页的数据presenter.requestNextPage();}}});}// 当请求到结果的时候在页面上做处理,展示到页面上@Overridepublic void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists) {loading = false;homeAdapter.addData(itemLists);}// 当请求到结果的时候在页面上做处理,展示到页面上@Overridepublic void setNextPage(List<HomeBean.IssueList.ItemList> itemLists) {loading = false;homeAdapter.addData(itemLists);}@Overridepublic void onError(String msg) {ToastUtils.makeToast(msg);}// ...
}

从上面的代码中我们可以看出实际在View中也要维护一个Presenter的实例。
当需要请求数据的时候会使用该实例的方法来请求数据,所以,在开发的时候,我们需要根据请求数据的情况,在Presenter中定义接口方法。

实际上,MVP的原理就是View通过Presenter获取数据,获取到数据之后再回调View的方法来展示数据。

2.3 MVC 和 MVP 的区别

  1. MVC 中是允许 Model 和 View 进行交互的,而MVP中,Model 与 View 之间的交互由Presenter完成;
  2. MVP 模式就是将 P 定义成一个接口,然后在每个触发的事件中调用接口的方法来处理,也就是将逻辑放进了 P 中,需要执行某些操作的时候调用 P 的方法就行了。

2.4 MVP的优缺点

优点:

  1. 降低耦合度,实现了 Model 和 View 真正的完全分离,可以修改 View 而不影响 Modle;
  2. 模块职责划分明显,层次清晰;
  3. 隐藏数据;
  4. Presenter 可以复用,一个 Presenter 可以用于多个 View,而不需要更改 Presenter 的逻辑;
  5. 利于测试驱动开发,以前的Android开发是难以进行单元测试的;
  6. View 可以进行组件化,在MVP当中,View 不依赖 Model。

缺点:

  1. Presenter 中除了应用逻辑以外,还有大量的 View->Model,Model->View 的手动同步逻辑,造成 Presenter 比较笨重,维护起来会比较困难;
  2. 由于对视图的渲染放在了 Presenter 中,所以视图和 Presenter 的交互会过于频繁;
  3. 如果 Presenter 过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密,一旦视图需要变更,那么Presenter也需要变更了。

3、MVVM (分手大师)

3.1 基础概念

MVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

  • 模型层 (Model):负责从各种数据源中获取数据;
  • 视图层 (View):在 Android 中对应于 Activity 和 Fragment,用于展示给用户和处理用户交互,会驱动 ViewModel 从 Model 中获取数据;
  • ViewModel 层:用于将 Model 和 View 进行关联,我们可以在 View 中通过 ViewModel 从 Model 中获取数据;当获取到了数据之后,会通过自动绑定,比如 DataBinding,来将结果自动刷新到界面上。

使用 Google 官方的 Android Architecture Components ,我们可以很容易地将 MVVM 应用到我们的应用中。下面,我们就使用它来展示一下 MVVM 的实际的应用。你可以在Github中获取到它的源代码。

3.2 示例程序

在该项目中,我们使用了:

  1. 果壳网的 API 作为数据源;
  2. 使用 Retrofit 进行网络数据访问;
  3. 使用 ViewMdeol 作为整体的架构设计。

该项目的包结构如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里的model.data下面的类是对应于网络的数据实体的,由JSON自动生成,这里我们不进行详细描述。这里的model.repository下面的两个类是用来从网络中获取数据信息的,我们也忽略它的定义。

上面就是我们的 Model 的定义,并没有太多的内容,基本与 MVP 一致。

下面的是 ViewModel 的代码,我们选择了其中的一个方法来进行说明。当我们定义 ViewModel 的时候,需要继承 ViewModel 类。

public class GuokrViewModel extends ViewModel {public LiveData<Resource<GuokrNews>> getGuokrNews(int offset, int limit) {MutableLiveData<Resource<GuokrNews>> result = new MutableLiveData<>();GuokrRetrofit.getGuokrService().getNews(offset, limit).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<GuokrNews>() {@Overridepublic void onError(Throwable e) {result.setValue(Resource.error(e.getMessage(), null));}@Overridepublic void onComplete() { }@Overridepublic void onSubscribe(Disposable d) { }@Overridepublic void onNext(GuokrNews guokrNews) {result.setValue(Resource.success(guokrNews));}});return result;}
}

这里的 ViewModel 来自 android.arch.lifecycle.ViewModel,所以,为了使用它,我们还需要加入下面的依赖:

api "android.arch.lifecycle:runtime:$archVersion"
api "android.arch.lifecycle:extensions:$archVersion"
annotationProcessor "android.arch.lifecycle:compiler:$archVersion"

在 ViewModel 的定义中,我们直接使用 Retrofit 来从网络中获取数据。然后当获取到数据的时候,我们使用 LiveData 的方法把数据封装成一个对象返回给 View 层。在 View 层,我们只需要调用该方法,并对返回的 LiveData 进行"监听"即可。这里,我们将错误信息和返回的数据信息进行了封装,并且封装了一个代表当前状态的枚举信息,你可以参考源代码来详细了解下这些内容。

上面我们定义完了 Model 和 ViewModel,下面我们看下 View 层的定义,以及在 View 层中该如何使用 ViewModel。

@Route(path = BaseConstants.GUOKR_NEWS_LIST)
public class NewsListFragment extends CommonFragment<FragmentNewsListBinding> {private GuokrViewModel guokrViewModel;private int offset = 0;private final int limit = 20;private GuokrNewsAdapter adapter;@Overrideprotected int getLayoutResId() {return R.layout.fragment_news_list;}@Overrideprotected void doCreateView(Bundle savedInstanceState) {// ...guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);fetchNews();}private void fetchNews() {guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {if (guokrNewsResource == null) {return;}switch (guokrNewsResource.status) {case FAILED:ToastUtils.makeToast(guokrNewsResource.message);break;case SUCCESS:adapter.addData(guokrNewsResource.data.getResult());adapter.notifyDataSetChanged();break;}});}
}

以上就是我们的 View 层的定义,这里我们先使用了

这里的view.fragment包下面的类对应于实际的页面,这里我们 ViewModelProviders 的方法来获取我们需要使用的 ViewModel,然后,我们直接使用该 ViewModel 的方法获取数据,并对返回的结果进行“监听”即可。

以上就是 MVVM 的基本使用,当然,这里我们并没有使用 DataBinding 直接与返回的列表信息进行绑定,它被更多的用在了整个 Fragment 的布局中。

3.3 MVVM 的优点和缺点

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

  1. 低耦合:视图(View)可以独立于Model变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。
  2. 可重用性:你可以把一些视图逻辑放在一个 ViewModel 里面,让很多 view 重用这段视图逻辑。
  3. 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
  4. 可测试:界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写。

4、组件化

4.1 基础概念

所谓的组件化,通俗理解就是将一个工程分成各个模块,各个模块之间相互解耦,可以独立开发并编译成一个独立的 APP 进行调试,然后又可以将各个模块组合起来整体构成一个完整的 APP。它的好处是当工程比较大的时候,便于各个开发者之间分工协作、同步开发;被分割出来的模块又可以在项目之间共享,从而达到复用的目的。组件化有诸多好处,尤其适用于比较大型的项目。

简单了解了组件化之后,让我们来看一下如何实现组件化开发。你可能之前听说过组件化开发,或者被其高大上的称谓吓到了,但它实际应用起来并不复杂,至少借助了现成的框架之后并不复杂。这里我们先梳理一下,在应用组件化的时候需要解决哪些问题:

  1. 如何分成各个模块?我们可以根据业务来进行拆分,对于比较大的功能模块可以作为应用的一个模块来使用,但是也应该注意,划分出来的模块不要过多,否则可能会降低编译的速度并且增加维护的难度。
  2. 各个模块之间如何进行数据共享和数据通信?我们可以把需要共享的数据划分成一个单独的模块来放置公共数据。各个模块之间的数据通信,我们可以使用阿里的 ARouter 进行页面的跳转,使用封装之后的 RxJava 作为 EventBus 进行全局的数据通信。
  3. 如何将各个模块打包成一个独立的 APP 进行调试?首先这个要建立在2的基础上,然后,我们可以在各个模块的 gradle 文件里面配置需要加载的 AndroidManifest.xml 文件,并可以为每个应用配置一个独立的 Application 和启动类。
  4. 如何防止资源名冲突问题?遵守命名规约就能规避资源名冲突问题。
  5. 如何解决 library 重复依赖以及 sdk 和依赖的第三方版本号控制问题?可以将各个模块公用的依赖的版本配置到 settings.gradle 里面,并且可以建立一个公共的模块来配置所需要的各种依赖。

Talk is cheap,下面让我们动手实践来应用组件化进行开发。你可以在Github中获取到它的源代码。

4.2 组件化实践

包结构

首先,我们先来看整个应用的包的结构。如下图所示,该模块的划分是根据各个模块的功能来决定的。图的右侧白色的部分是各个模块的文件路径,我推荐使用这种方式,而不是将各个模块放置在 app 下面,因为这样看起来更加的清晰。为了达到这个目的,你只需要按照下面的方式在 settings.gralde 里面配置一下各个模块的路径即可。注意在实际应用的时候模块的路径的关系,不要搞错了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后,我们介绍一下这里的 commons 模块。它用来存放公共的资源和一些依赖,这里我们将两者放在了一个模块中以减少模块的数量。下面是它的 gradle 的部分配置。这里我们使用了 api 来引入各个依赖,以便在其他的模块中也能使用这些依赖。

dependencies {api fileTree(include: ['*.jar'], dir: 'libs')// ...// routerapi 'com.alibaba:arouter-api:1.3.1'annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'// walleapi 'com.meituan.android.walle:library:1.1.6'// umengapi 'com.umeng.sdk:common:1.5.3'api 'com.umeng.sdk:analytics:7.5.3'api files('libs/pldroid-player-1.5.0.jar')
}
路由

接着,我们来看一下路由框架的配置。这里,我们使用阿里的 ARouter 来进行页面之间的跳转,你可以在Github上面了解该框架的配置和使用方式。这里我们只讲解一下在组件化开发的时候需要注意的地方。注意到 ARouter 是通过注解来进行页面配置的,并且它的注解是在编译的时候进行处理的。所以,我们需要引入arouter-compiler来使用它的编译时处理功能。需要注意的地方是,我们只要在公共的模块中加入arouter-api就可以使用ARouter的API了,但是需要在每个模块中引入arouter-compiler才能使用编译时注解。也就是说,我们需要在每个模块中都加入arouter-compiler依赖。

模块独立

为了能够将各个模块编译成一个独立的 APP,我们需要在 Gradle 里面做一些配置。

首先,我们需要在gradle.properties定义一些布尔类型的变量用来判断各个模块是作为一个 library 还是 application 进行编译。这里我的配置如下面的代码所示。也就是,我为每个模块都定义了这么一个布尔类型的变量,当然,你也可以只定义一个变量,然后在各个模块中使用同一个变量来进行判断。

isGuokrModuleApp=false
isLiveModuleApp=false
isLayoutModuleApp=false
isLibraryModuleApp=false
isEyepetizerModuleApp=false

然后,我们来看一下各个模块中的 gradle 该如何配置,这里我们以开眼视频的功能模块作为例子来进行讲解。首先,一个模块作为 library 还是 application 是根据引用的 plugin 来决定的,所以,我们要根据之前定义的布尔变量来决定使用的 plugin:

if (isEyepetizerModuleApp.toBoolean()) {apply plugin: 'com.android.application'
} else {apply plugin: 'com.android.library'
}

假如我们要将某个模块作为一个独立的 APP,那么启动类你肯定需要配置。这就意味着你需要两个 AndroidManifest.xml 文件,一个用于 library 状态,一个用于 application 状态。所以,我们可以在 main 目录下面再定义一个 AndroidManifest.xml,然后,我们在该配置文件中不只指定启动类,还使用我们定义的 Application。指定 Application 有时候是必须的,比如你需要在各个模块里面初始化 ARouter 等等。这部分代码就不给出了,可以参考源码,这里我们给出一下在 Gradle 里面指定 AndroidManifest.xml 的方式。

如下所示,我们可以根据之前定义的布尔值来决定使用哪一个配置文件:

    sourceSets {main {jniLibs.srcDirs = ['libs']if (isEyepetizerModuleApp.toBoolean()) {manifest.srcFile "src/main/debug/AndroidManifest.xml"} else {manifest.srcFile "src/main/AndroidManifest.xml"}}}

此外,还需要注意的是,如果我们希望在每个模块中都能应用 DataBinding 和 Java 8 的一些特性,那么你需要在每个模块里面都加入下面的配置:

    // use data bindingdataBinding {enabled = true}// use java 8 languagecompileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}

对于编译时注解之类的配置,我们也需要在每个模块里面都进行声明。

完成了以上的配置,我们只要根据需要编译的类型,修改之前定义的布尔值,来决定是将该模块编译成 APP 还是作为类库来使用即可。

以上就是组件化在 Android 开发当中的应用。

总结

MVC、MVP和MVVM各有各自的特点,可以根据应用开发的需要选择适合自己的架构模式。组件化的目的就在于保持各个模块之间的独立从而便于分工协作。它们之间的关系就是,你可以在组件化的各个模块中应用前面三种架构模式的一种或者几种。

另外
有什么技术问题欢迎加我交流 qilebeaf
本人10多年大厂软件开发经验,精通Android,Java,Python,前端等开发,空余时间承接软件开发设计、课程设计指导、解决疑难bug、AI大模型搭建,AI绘图应用等。
欢迎砸单# 在 Android 中使用 JNI 的总结

最近在研究 Android 相机相关的东西,因为想要对相机做一个封装,于是想到要提供支持滤镜和图像动态识别相关的接口。在我找到一些资料中,它们的实现:一个是基于 OpenGL 的,一个是基于 OpenCV 的。两者都可以直接使用 Java 进行开发,受制于 Java 语言的限制,所以当对程序的性能要求很高的时候,Java 就有些心有余力不足了。所以,有些实现 OpenCV 的方式是在 Native 层进行处理的。这就需要涉及 JNI 的一些知识。

当然,JNI 并非 Android 中提出的概念,而是在 Java 中本来提供的。所以,在这篇文章中,我们先尝试在 IDEA 中使用 JNI 进行开发,以了解 JNI 运行的原理和一些基础知识。然后,再介绍下 AS 中使用更高效的开发方式。

1、声明 native 方法

1.1 静态注册

首先,声明 Java 类,

package me.shouheng.jni;public class JNIExample {static {// 函数System.loadLibrary()是加载dll(windows)或so(Linux)库,只需名称即可,// 无需加入文件名后缀(.dll或.so)System.loadLibrary("JNIExample");init_native();}private static native void init_native();public static native void hello_world();public static void main(String...args) {JNIExample.hello_world();}
}

native 的方法可以定义成 static 的和非 static 的,使用上和普通的方法没有区别。这里使用 System.loadLibrary("JNIExample") 加载 JNI 的库。在 Window 上面是 dll,在 Linux 上面是 so. 这里的 JNIExample 只是库的名称,甚至都没有包含文件类型的后缀,那么 IDEA 怎么知道到哪里加载库呢?这就需要我们在运行 JVM 的时候,通过虚拟机参数来指定。在 IDEA 中的方式是使用 Edit Configuration...,然后在 VM options 一栏中输入 -Djava.library.path=F:\Codes\Java\Project\Java-advanced\java-advanced\lib,这里的路径是我的库文件所在的位置。

使用 JNI 第一步是生成头文件,我们可以使用如下的指令,

javah -jni -classpath (搜寻类目录) -d (输出目录) (类名)

或者简单一些,先把 java 文件编译成 class,然后使用 class 生成 h 头文件,

javac me/shouheng/jni/JNIExample.java
javah me.shouheng.jni.JNIExample

上面的两个命令是可行的,只是要注意下文件的路径的问题。(也许我们可以使用 Java 或者其他的语言写些程序调用这些可执行文件来简化它的使用!)

生成的头文件代码如下,

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class me_shouheng_jni_JNIExample */#ifndef _Included_me_shouheng_jni_JNIExample
#define _Included_me_shouheng_jni_JNIExample
#ifdef __cplusplus
extern "C" {
#endif
/** Class:     me_shouheng_jni_JNIExample* Method:    init_native* Signature: ()V*/
JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_init_1native(JNIEnv *, jclass);/** Class:     me_shouheng_jni_JNIExample* Method:    hello_world* Signature: ()V*/
JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_hello_1world(JNIEnv *, jclass);#ifdef __cplusplus
}
#endif
#endif

可以看出,它跟普通的 c 头文件多了 JNIEXPORT 和 JNICALL 两个指令,剩下的东西完全符合一般 c 头文件的规则。这里的 Java_me_shouheng_jni_JNIExample_init_1native 对应 Java 层的代码,可见它的规则是 Java_Java层的方法路径 只是方法路径使用了下划线取代了逗号,并且 Java 层的下划线使用 _1 替代,这是因为 Native 层的下划线已经用来替代 Java 层的逗号了,所以 Java 层的下划线只能用 _1 表示了。

这里的 JNIEnv 是一个指针类型,我们可以用它访问 Java 层的代码,它不能跨进程被调用。你可以在 JDK 下面的 include 文件夹中的 jni.h 中找到它的定义。jclass 对应 Java 层的 Class 类。Java 层的类和 Native 层的类之间按照指定的规则进行映射,当然还有方法签名的映射关系。所谓方法签名,比如上面的 ()V,当你使用 javap 反编译 class 的时候可以看到这种符号。它们实际上是 class 文件中的一种简化的描述方式,主要是为了节省 class 文件的内存。此外,方法签名还被用来进行动态注册 JNI 方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

引用类型的对应关系如下,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面注册 JNI 的方式属于静态注册,可以理解为在 Java 层注册 Native 的方法;此外,还有动态注册,就是在 Native 层注册 Java 层的方法。

1.2 动态注册

除了按照上面的方式静态注册 native 方法,我们还可以动态进行注册。动态注册的方式需要我们使用方法的签名,下面是 Java 类型与方法签名之间的映射关系:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意这里的全限定类名以 / 分隔,而不是用 ._ 分隔。方法签名的规则是:(参数1类型签名参数2类型签名……参数n类型签名)返回类型签名。比如,long fun(int n, String str, int[] arr) 对应的方法签名为 (ILjava/lang/String;[I)J

一般 JNI 方法动态注册的流程是:

  1. 利用结构体 JNINativeMethod 数组记录 java 方法与 JNI 函数的对应关系;
  2. 实现 JNI_OnLoad 方法,在加载动态库后,执行动态注册;
  3. 调用 FindClass 方法,获取 java 对象;
  4. 调用 RegisterNatives 方法,传入 java 对象,以及 JNINativeMethod 数组,以及注册数目完成注册。

比如上面的代码如果使用动态注册将会是如下形式:

void init_native(JNIEnv *env, jobject thiz) {printf("native_init\n");return;
}void hello_world(JNIEnv *env, jobject thiz) {printf("Hello World!");return;
}static const JNINativeMethod gMethods[] = {{"init_native", "()V", (void*)init_native},{"hello_world", "()V", (void*)hello_world}
};JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {__android_log_print(ANDROID_LOG_INFO, "native", "Jni_OnLoad");JNIEnv* env = NULL;if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) // 从 JavaVM 获取JNIEnv,一般使用 1.4 的版本return -1;jclass clazz = env->FindClass("me/shouheng/jni/JNIExample");if (!clazz){__android_log_print(ANDROID_LOG_INFO, "native", "cannot get class: com/example/efan/jni_learn2/MainActivity");return -1;}if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]))){__android_log_print(ANDROID_LOG_INFO, "native", "register native method failed!\n");return -1;}return JNI_VERSION_1_4;
}

2、执行 JNI 程序

了解了如何加载,剩下的就是如何得到 dll 和 so. 在 Window 平台上面,我们使用 VS 或者 GCC 将代码编译成 dll. GCC 有两种选择,MinGW 和 Cygwin。这里注意下 GCC 和 JVM 的位数必须一致,即要么都是 32 位的要么都是 64 位的,否则将有可能抛出 Can't load IA 32-bit .dll on a AMD 64-bit platform 异常。

查看虚拟机的位数使用 java -version,其中有明确写明 64-bit 的是 64 位的,否则是 32 位的。(参考:如何识别JKD的版本号和位数,操作系统位数.)MinGW 的下载可以到如下的链接:MinGW Distro - nuwen.net。安装完毕之后输入 gcc -v,能够输出版本信息就说明安装成功。

有了头文件,我们还要实现 native 层的方法,我们新建一个 c 文件 JNIExample.c 然后实现各个函数如下,

#include<jni.h>
#include <stdio.h>
#include "me_shouheng_jni_JNIExample.h"JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_init_1native(JNIEnv * env, jclass cls) {printf("native_init\n");return;
}JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_hello_1world(JNIEnv * env, jclass cls) {printf("Hello World!");return;
}

看上去还是比较清晰的,除去 JNIEXPORT 和 JNICALL 两个符号之外,剩下的都是基本的 c 语言的东西。然后我们在方法中简单输出一个老朋友 Hello World. 注意下,这里除了基本的输入输出头文件 stdio.h 之外,我们还引入了刚才生成的头文件,以及 jni.h,后者定义在 JDK 当中,当我们使用 gcc 生成 dll 的时候就需要引用这个头文件。

我们使用如下的命令来先生成 o 文件,

gcc -c -I"E:\JDK\include" -I"E:\JDK\include\win32" jni/JNIExample.c

这里的两个 -I 后面指定的是 JDK 中的头文件的路径。因为,按照我们上面说的,我们在 c 文件中引用了 jni.h,而该文件就位于 JDK 的 include 目录中。因为 include 中的头文件又引用了目录 win32 中的头文件,所以,我们需要两个都引用进来(心累)。

然后,我们使用如下的命令将上述 o 文件转成 dll 文件,

gcc -Wl,--add-stdcall-alias -shared -o JNIExample.dll JNIExample.o

如果你发现使用了 , 之后 PowerShell 无法执行,那么可以将 , 替换为 "," 再执行。

生成 dll 之后,我们将其放入自定义的 lib 目录中。如我们上述所说的,需要在虚拟机的参数中指定这个目录。

然后运行并输出久违的 Hello world! 即可。

3、进一步接触 JNI:在 Native 中调用 Java 层的方法

我们定义如下的类,

public class JNIInteraction {static {System.loadLibrary("interaction");}private static native String outputStringFromJava();public static String getStringFromJava(String fromString) {return "String from Java " + fromString;}public static void main(String...args) {System.out.println(outputStringFromJava());}
}

这里我们希望的结果是,Java 层调用 Native 层的 outputStringFromJava() 方法。在 Native 层中,该方法调用到 Java 层的静态方法 getStringFromJava() 并传入字符串,最后整个拼接的字符串通过 outputStringFromJava() 传递给 Java 层。

以上是 Java 层的代码,下面是 Native 层的代码。Native 层去调用 Java 层的方法的步骤基本是固定的:

  1. 通过 JNIEnv 的 FindClass() 函数获取要调用的 Java 层的类;
  2. 通过 JNIEnv 的 GetStaticMethodID() 函数和上述 Java 层的类、方法名称和方法签名,得到 Java 层的方法的 id;
  3. 通过 JNIEnv 的 CallStaticObjectMethod() 函数、上述得到的类和上述方法的 id,调用 Java 层的方法。

这里有两点地方需要说明:

  1. 这里因为我们要调用 Java 层的静态函数,所以我们使用的函数是 GetStaticMethodID()CallStaticObjectMethod() 。如果你需要调用类的实例方法,那么你需要调用 GetMethodID()CallObjectMethod()。诸如此类,JNIEnv 中还有许多其他有用的函数,你可以通过查看 jni.h 头文件来了解。
  2. Java 层和 Native 层的方法相互调用本身并不难,使用的逻辑也是非常清晰的。唯一比较复杂的地方在于,你需要花费额外的时间去处理两个环境之间的数据类型转换的问题。比如,按照我们上述的目标,我们需要实现一个将 Java 层传入的字符串转换成 Native 层字符串的函数。其定义如下,
char* Jstring2CStr(JNIEnv* env, jstring jstr) {char* rtn = NULL;jclass clsstring = (*env)->FindClass(env, "java/lang/String");jstring strencode = (*env)->NewStringUTF(env,"GB2312");jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");// String.getByte("GB2312");jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode);jsize alen = (*env)->GetArrayLength(env, barr);jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);if(alen > 0) {rtn = (char*)malloc(alen+1); //"\0"memcpy(rtn, ba, alen);rtn[alen]=0;}(*env)->ReleaseByteArrayElements(env,barr,ba,0); //return rtn;
}

在上述函数中,我们通过调用 Java 层的 String.getBytes() 获取到 Java 层的字符数组,然后将其通过内存拷贝的方式复制到字符数组中。(通过 malloc() 函数申请内存,并将字符指针的指向申请的内存的首地址。)最后,还要调用 JNIEnv 的方法来释放字符数组的内存。这里也是一次 Native 调 Java 函数的过程,只是这里的调用 String 类的实例方法。(从这里也可以看出,Native 层写代码要考虑的因素比 Java 层多得多,好在这是 C 语言,如果 C++ 的化可能处理起来会好一些。)

回到之前的讨论中,我们需要继续实现 Native 层的函数:

JNIEXPORT jstring JNICALL Java_me_shouheng_jni_interaction_JNIInteraction_outputStringFromJava (JNIEnv *env, jclass _cls) {jclass clsJNIInteraction = (*env)->FindClass(env, "me/shouheng/jni/interaction/JNIInteraction"); // 得到类jmethodID mid = (*env)->GetStaticMethodID(env, clsJNIInteraction, "getStringFromJava", "(Ljava/lang/String;)Ljava/lang/String;"); // 得到方法jstring params = (*env)->NewStringUTF(env, "Hello World!");jstring result = (jstring)(*env)->CallStaticObjectMethod(env, clsJNIInteraction, mid, params);return result;
}

其实它的逻辑也是比较简单的了。跟我们上面调用 String 的实例方法的步骤基本一致,只是这里调用的是静态方法。

这样上述程序的效果是,当 Java 层调用 Native 层的 outputStringFromJava() 函数的时候:首先,Native 层通过调用 Java 层的 JNIInteraction 的静态方法 getStringFromJava() 并传入参数得到 String from Java Hello World! 之后将其作为 outputStringFromJava() 函数的结果返回。

4、在 Android Studio 中使用 JNI

上面在程序中使用 JNI 的方式可以说很笨拙了,还好在 Android Studio 中,许多过程被简化了。这让我们得以将跟多的精力放在实现 Native 层和 Java 层代码逻辑上,而无需过多关注编译环节这个复杂的问题。

在 AS 中启用 JNI 的方式很简单:在使用 AS 创建一个新项目的时候注意勾选 include C++ support 即可。其他的步骤与创建一个普通的 Android 项目并无二致。然后你需要对开发的环境进行简单的配置。你需要安装下面几个库,即 CMake, LLDB 和 NDK:

AS 环境需求

AS 之所以能够简化我们的编译流程,很大程度上是得益于编译工具 CMake。CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装 (编译过程)。我们只需要在它指定的 CMakeLists.txt 文件中使用它特定的语法描述整个编译流程,然后使用 CMake 的指令即可。你可以通过文档来了解如何在 AS 中使用 CMake:add-native-code. 或者通过下面这篇文章简单入门下 CMake:CMake 入门实战。

支持 JNI 开发的 Android 项目与普通的项目没有太大的区别,除了在 local.properties 中额外指定了 NDK 的目录之外,项目结构和 Gradle 的配置主要有如下的区别:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看出区别主要在于:

  1. main 目录下面多了个 cpp 目录用来编写 C++ 代码;
  2. app 目录下面多了各 CMakeLists.txt 就是我们上面提到的 CMake 的配置文件;
  3. 另外 Gradle 中里面一处指定了 CMakeLists.txt 文件的位置,另一处配置了 CMake 的编译;

在 AS 中进行 JNI 开发的优势除了 CMake 之外,还有:

  1. 无需手动对方法进行动态注册和静态注册,当你在 Java 层定义了一个 native 方法之后,可以通过右键直接生成 Native 层对应的方法;
  2. 此外,AS 中可以建立 Native 层和 Java 层方法之间的联系,你可以直接在两个方法之间跳转;
  3. 当使用 AS 进行编程的时候,调用 Native 层的类的时候也会给出提示选项,比如上面的 JNIEnv 就可以给出其内部各种方法的提示。

另外,从该初始化的项目以及 Android 的 Native 层的源码来看,Google 是支持我们使用 C++ 开发的。所以,吃了那么久灰的 C++ 书籍又可以派上用场了……

总结

以上。

Android 从基础到高级,关注作者及时获取更多知识

本系列以及其他系列的文章均维护在 Github 上面:Github / Android-notes,欢迎 Star & Fork. 如果你喜欢这篇文章,愿意支持作者的工作,请为这篇文章点个赞👍!

另外
有什么技术问题欢迎加我交流 qilebeaf
本人10多年大厂软件开发经验,精通Android,Java,Python,前端等开发,空余时间承接软件开发设计、课程设计指导、解决疑难bug、AI大模型搭建,AI绘图应用等。
欢迎砸单

相关文章:

探索Android架构设计

Android 应用架构设计探索&#xff1a;MVC、MVP、MVVM和组件化 MVC、MVP和MVVM是常见的三种架构设计模式&#xff0c;当前MVP和MVVM的使用相对比较广泛&#xff0c;当然MVC也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发&#xff0c;每个…...

基于matlab的不同边缘检测算子的边缘检测

1 原理 1.1 边缘检测概述 边缘检测是图像处理和计算机视觉中的基本问题&#xff0c;其目的在于标识数字图像中亮度变化明显的点。这些变化通常反映了图像属性的重要事件和变化&#xff0c;如深度不连续、表面方向不连续、物质属性变化和场景照明变化等。边缘检测在特征提取中…...

CentOS安装ntp时间同步服务

CentOS安装ntp时间同步服务 安装ntp 检查服务器是否安装ntp&#xff1a; rpm -q ntp安装ntp&#xff1a; yum install -y ntp服务端配置 配置文件路径&#xff1a;/etc/ntp.conf 设置ntp为开机启动 systemctl enable ntpd查看ntp开机启动状态 enabled:开启, disabled:关闭 …...

【Linux进阶】UNIX体系结构分解——操作系统,内核,shell

1.什么是操作系统&#xff1f; 从严格意义上说&#xff0c;可将操作系统定义为一种软件&#xff0c;它控制计算机硬件资源&#xff0c;提供程序运行环境。我们通常将这种软件称为内核&#xff08;kerel)&#xff0c;因为它相对较小&#xff0c;而且位于环境的核心。 从广义上…...

PageOffice国产版在线编辑word文件

PageOffice国产版支持统信UOS、银河麒麟等国产操作系统。调用客户端WPS在线编辑word、excel、ppt等文件。在线编辑效果与本地WPS一致。如图所示&#xff1a; web系统集成pageofficeV6.0国产版的文档&#xff1a;PageOfficeV6.0国产版最简集成代码(Springboot) PageOffice最简集…...

Bitmap位图数据排列方式

读取dicom C# 使用fo-dicom操作dicom文件-CSDN博客 创建位图 通过读取dicom得到像素内存&#xff0c;本例单指彩色图像。 Bitmap dataBmp new Bitmap(imageWidth, imageHeight, stride, PixelFormat.Format24bppRgb, dstBmp); 当像素的内存按照RGB的排列模式时&#xff0c…...

重磅消息:ONLYOFFICE8.1版本桌面编辑器发布:功能完善的 PDF 编辑器、幻灯片版式、改进从右至左显示、新的本地化选项等

目录 ONLYOFFICE介绍 PDF 编辑器 功能全面的 PDF 编辑器 文本编辑 页面处理 &#xff08;添加、旋转、删除&#xff09; 插入和调整各种对象&#xff0c;例如表格、形状、文本框、图像、TextArt、超链接、方程等。 此外 PDF 表单 文本文档编辑器更新内容 页面颜色 页面…...

16进制数按位修改

16进制数需要按位修改,特别是在修改寄存器的时候 16进制数转换为2进制 #16进制数转换为2进制 def hex_to_binary(hex_value):return bin((hex_value))二进制数转换为列表 def bin_to_array(bin_str):integer = int(bin_str, 2)array...

深度神经网络——什么是小样本学习?

引言 小样本学习是指使用极少量的训练数据来开发人工智能模型的各种算法和技术。小样本学习致力于让人工智能模型在接触相对较少的训练实例后识别和分类新数据。小样本训练与训练机器学习模型的传统方法形成鲜明对比&#xff0c;传统方法通常使用大量训练数据。小样本学习是 主…...

送物机器人电子方案定制

这是一款集娱乐、教育和互动于一身的高科技产品。 一、它的主要功能包括&#xff1a; 1. 智能对话&#xff1a;机器人可以进行简单的对话&#xff0c;回答用户的问题&#xff0c;提供有趣的互动体验。 2. 前进、后退、左转、右转、滑行&#xff1a;机器人可以通过遥控器或AP…...

chatgpt: linux 下用纯c 编写一按钮,当按钮按下在一新窗口显示本机主目录下图片子目录中的1.jpg图片

tmd,这chatgpt太强大了。 从下面的c程序与python程序对比&#xff0c;纯c的ui编程也不是太复杂。 再说一次&#xff0c;要想学好编程必须要用上这个chatgpt工具。 在 Linux 环境下使用纯 C 语言编写一个按钮&#xff0c;当按钮按下时&#xff0c;在一个新窗口中显示本机主目…...

SherlockChain:基于高级AI实现的智能合约安全分析框架

关于SherlockChain SherlockChain是一款功能强大的智能合约安全分析框架&#xff0c;该工具整合了Slither工具&#xff08;一款针对智能合约的安全工具&#xff09;的功能&#xff0c;并引入了高级人工智能模型&#xff0c;旨在辅助广大研究人员针对Solidity、Vyper和Plutus智…...

MySQL中Explain执行计划各参数的含义

EXPLAIN 语句输出的各个列的作用先大致罗列一下&#xff1a; 列名 描述 id 在一个大的查询语句中每个SELECT关键字都对应一个唯一的id select_type SELECT关键字对应的那个查询的类型 table 表名 partitions 匹配的分区信息 type 针对单表的访问方法 possible_keys…...

Redis队列自研组件

背景 年初的时候设计实践过一个课题&#xff1a;SpringBootRedis实现不重复消费的队列&#xff0c;并用它开发了一个年夜饭下单和制作的服务。不知道大家还有没有印象。完成这个课题后&#xff0c;我兴致勃勃的把它运用到了项目里面&#xff0c;可谁曾想&#xff0c;运行不久后…...

ArchLinux挑战安装(ZFS、Wayland、KDE、xero)

目录 0. 前言&#xff1a; 1. 先期准备 1.1 引导ArchLinx光盘。 1.2 禁用 reflector 服务 1.3 防止网卡禁用 1.4 wifi设置 1.5 测试网络是否连接 1.6 更新系统时间 1.7 更换源 1.8 下载ZFS模块 1.9 加载ZFS模块 2. 磁盘处理 2.1 查看磁盘分区 2.2 清除与整个磁盘…...

纯css写一个动态圣诞老人

效果预览 在这篇文章中&#xff0c;我们将学习如何使用CSS来创建一个生动的圣诞老人动画。通过CSS的魔力&#xff0c;我们可以让圣诞老人在网页上摇摆&#xff0c;仿佛在向我们招手庆祝圣诞节和新年。 实现思路 实现这个效果的关键在于CSS的keyframes动画规则以及各种CSS属性…...

百度Apollo的PublicRoadPlanner一些移植Ros2-foxy的思路(持续更新)

如今的PublicRoadPlanner就是之前耳熟能详的EM planner 计划 —— ROS2与CARLA联合仿真 结构化场景: 规划算法:EM-planner 控制算法:MPC和PID 非结构化场景: 规划算法采用Hybrid A* (1)小车模型搭建(计划参考Github上Hybrid上的黑车,比较炫酷) (2)车辆里程计: 位…...

Linux内存管理(七十三):cgroup v2 简介

版本基于: Linux-6.6 约定: 芯片架构:ARM64内存架构:UMACONFIG_ARM64_VA_BITS:39CONFIG_ARM64_PAGE_SHIFT:12CONFIG_PGTABLE_LEVELS :31. cgroup 简介 术语: cgroup:control group 的缩写,永不大写(never capitalized); 单数形式的 cgroup 用于指定整个特性,也用…...

c++习题01-ljc的暑期兼职

目录 一&#xff0c;题目描述 二&#xff0c;思路 三&#xff0c;伪代码 四&#xff0c;流程图 五&#xff0c;代码 一&#xff0c;题目描述 二&#xff0c;思路 1&#xff0c;根据题目要求需要声明4个变量&#xff1a;a,b,c,d ;牛奶价格a&#xff0c;活动要求b&…...

有哪些方法可以恢复ios15不小心删除的照片?

ios15怎么恢复删除的照片&#xff1f;在手机相册里意外删除了重要的照片&#xff1f;别担心&#xff01;本文将为你介绍如何在iOS 15系统中恢复已删除的照片。无需专业知识&#xff0c;只需要按照以下步骤操作&#xff0c;你就能轻松找回宝贵的回忆。 一、从iCloud云端恢复删除…...

nacos漏洞汇总

1 nacos介绍 1.1 nacos是啥 Alibaba Nacos是阿里巴巴推出来的一个新开源项目&#xff0c;是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。致力于帮助发现、配置和管理微服务。Nacos提供了一组简单易用的特性集&#xff0c;可以快速实现动态服务发现、服…...

React Antd ProTable 如何设置类似于Excel的筛选框

React Antd ProTable 如何设置类似于Excel的筛选框 目标&#xff1a;在web页面的table表格中完成类似于EXCEL的Filter筛选功能。 示例图&#xff1a;点击标题列上方的漏斗状图标&#xff0c;即可对数据进行筛选。 ProTable 前景提要 ProTable API中有说明&#xff0c;是有…...

句法分析概述

第1关&#xff1a;句法分析概述 任务描述 本关任务&#xff1a;通过对句法分析基本概念的学习&#xff0c;完成相应的选择题。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 句法分析的基础概念&#xff1b; 句法分析的数据集和评测方法。 句法分析简介…...

简单了解css的基本使用

CSS 一、基础认知 1、CSS引入方式 1.1、内嵌式&#xff08;CSS写在style标签中&#xff09; style标签虽然可以写在页面的任意位置&#xff0c;但是通常约定写在head标签中 2.2、外联式&#xff08;CSS写在一个单独的.css文件中&#xff09; 需要通过link标签在网页中引入…...

构建网络图 (JavaScript)

前序&#xff1a;在工作中难免有一些千奇百怪的需求&#xff0c;如果你遇到构建网络图&#xff0c;或者学习应对未来&#xff0c;请看这边文章&#xff0c;本文以代码为主。 网络图是数据可视化中实用而有效的工具&#xff0c;特别适用于说明复杂系统内的关系和连接。这些图表…...

洛谷U389682 最大公约数合并

这道题最后有一个性质没有想出来&#xff0c;感觉还是有一点遗憾。 性质一、贪心是不对的 8 11 11 16虽然第一次选择8和16合并是最优的&#xff0c;但是如果合并两次的话8 11 11是最优的。 性质二 、有1的情况就是前k1个&#xff0c;也就是说&#xff0c;很多情况下取前k1都…...

video_多个m3u文件合并成一个m3u文件

主要是用#EXT-X-DISCONTINUITY进行拼接,用简单的例子说明: 第一个文件: #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:69 #EXT-X-MEDIA-SEQUENCE:1001 #EXTINF:60.000000, xmt202406_11001.ts #EXTINF:60.000000, xmt202406_11002.ts #EXTINF:60.000000, xmt202406_11…...

x264 码率控制 MBtree 原理:i_propagate_cost计算过程

x264 码率控制 MBtree 原理 关于x264 码率控制中 MBtree 算法的原理具体可以参考:x264 码率控制MBtree原理。 i_propagate_cost介绍 该值在 frame.h 中 x264_frame_t结构体中声明。该值是一个 uint16_t型指针变量,在 MBtree 算法中用来存储每个宏块的传播代价。在*frame_ne…...

C语言基础笔记(全)

一、数据类型 数据的输入输出 1.数据类型 常量变量 1.1 数据类型 1.2 常量 程序运行中值不发生变化的量&#xff0c;常量又可分为整型、实型(也称浮点型)、字符型和字符串型 1.3 变量 变量代表内存中具有特定属性的存储单元&#xff0c;用来存放数据&#xff0c;即变量的值&a…...

通过注释语句,简化实体类的定义(省略get/set/toString的方法)

引用Java的lombok库&#xff0c;减少模板代码&#xff0c;如getters、setters、构造函数、toString、equals和hashCode方法等 import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;Data NoArgsConstructor AllArgsConstructorData&#xf…...

wordpress 站长/seo站长工具 论坛

Ubuntu 20.04 更新源系统版本更新源系统版本 # cat /etc/issue Ubuntu 20.04.2 LTS \n \l更新源 # 默认注释了源码镜像以提高 apt update 速度&#xff0c;如有需要可自行取消注释 deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multive…...

在对方网站做友情链接/大数据精准获客软件

protobuf介绍 由于网上关于protobuf的交互的资料比较零散&#xff0c;所以自己整理了一下关于protobuf前后端交互的资料&#xff0c;以作参考。 Google Protocol Buffers 简称 Protobuf&#xff0c;它提供了一种灵活、高效、自动序列化结构数据的机制&#xff0c;可以联想 XML&…...

企业网站建设费怎么账务处理/百度推广后台

一、概述 Java是天生就支持并发的语言&#xff0c;支持并发意味着多线程&#xff0c;线程的频繁创建在高并发及大数据量是非常消耗资源的&#xff0c;因为java提供了线程池。在jdk1.5以前的版本中&#xff0c;线程池的使用是及其简陋的&#xff0c;但是在JDK1.5后&#xff0c;有…...

wordpress视频无法播放视频教程/市场营销策划方案案例

Scala和Groovy都是基于JVM的语言&#xff0c;相比Java都有更加简明的语法和丰富的表达能力。对于那些既想不脱离开JVM又想避免Java繁琐的语句的开发人员来说&#xff0c;Scala和Groovy都是不错的选择。可是选择哪一个才能在未来发展过程中取得先机呢&#xff1f;哪一个是未来发…...

重庆市建设工程交易中心/搜索引擎优化培训免费咨询

2019独角兽企业重金招聘Python工程师标准>>> Zookeeper Watch机制 博客分类&#xff1a; zookeeper Znode发生变化&#xff08;Znode本身的增加&#xff0c;删除&#xff0c;修改&#xff0c;以及子Znode的变化&#xff09;可以通过Watch机制通知到客户端。那么要实…...

手机网站建设系统/2024疫情最新消息今天

先说说我首先体验的gitosis&#xff0c;用Python写成&#xff0c;主页&#xff0c;也是ProGit详叙的一种方案&#xff0c;目前基本上已经停止更新。我觉得最大的特色就是其怪异的配置文件和项目映射&#xff0c;国内有人对其做了改进&#xff0c;包括增加了管理员角色&#xff…...