android中ndk的开发

简介: 前言(其实是吐槽)这是我看(android应用安全防护和逆向分析)遇到的第一个坑了,在章节2.1和2.2里,虽然作者很贴心的给了步骤教你如何搭建ndk的开发环境,但是,我要说的是,如果按照作者在2.1.2的五个步骤按部就班的来,你绝对!不可能!完成!主要的原因我就不再分析了,大约就是少了一堆乱七八糟的说明和步骤,这里我重新写一遍ndk开发相关。

前言(其实是吐槽)

这是我看(android应用安全防护和逆向分析)遇到的第一个坑了,在章节2.1和2.2里,虽然作者很贴心的给了步骤教你如何搭建ndk的开发环境,但是,我要说的是,如果按照作者在2.1.2的五个步骤按部就班的来,你绝对!不可能!完成!
主要的原因我就不再分析了,大约就是少了一堆乱七八糟的说明和步骤,这里我重新写一遍ndk开发相关。(如果你不信,可以尝试只按照2.1.2章节的五步来尝试)

搭建NDK开发环境

NDK相关概念

首先,普及一下ndk的概念,何谓ndk开发呢?
简而言之,就是让安卓(java)可以调用前人用c语言完成的库,这么做的好处主要有两个,
第一,节约代码量,提高工作(运行)效率,可以用之前c写好的很多很棒的库
第二,防止应用被逆向,因为java层的代码很容易被反编译逆向突破。
再介绍几个ndk里相关概念名词,
.c,.cpp,这几个不用多说了吧,就是c或者c++等文件的后缀名;
.so,这个是编译c文件得到的库文件的后缀名,.so文件大概就相当于windows上的.dll文件,他可以方便让别人调用;
.h,也就是通过javah命令编译类(class)文件编译出来的头文件,这玩意开始在c里用作声明管理,现在在jni里已经意义不大了,你可以当做没啥用;
jni,全名 java native interface,由他提供java和c之间的通信;

一个NDK开发的流程

编写Java代码(.java) —————> 编译生成字节码文件(.class) —————> 产生C头文件(.h) —————> 编写jni实现代码(.c) —————> 编译成链接库(.so)

基于Android Studio3 & CMAKE的NDK环境搭建
  1. 打开你的as(Android Studio),新建一个支持c++的项目(如图),然后一路next到结束;
    img_2b71c51a8d986f8344d02c2f0c9fc192.png
    新建一个支持c++的项目.png
  1. 此时一个基本工程环境已经新建好了,不需要任何改动,我们直接可以点击build生成一个系统自带的示例so库也就是cpp目录里的native-lib,但在此之前,我们先看一下工程目录:
    img_1221d7682c5f29a99521f0dc2411a321.png
    工程目录.png

可以看到,相对于普通项目,我们的ndk的项目其实就是多了cpp目录,多了CMakelists.txt文件,此外build.gradle里多了一些配置;

  1. 因为编译出so库,主要看的是CMakelists文件里的配置,下面我们重点关注一下CMakelists文件的内容,如图:
    img_e4b17953581764d2a4fdd14bffca9288.png
    CMakeLists.png

默认的CMakelists文件里,主要有三个配置需要关注,分别是
add_library:
主要用作给生成的so库做配置,在三个注释的下面,每一个都代表了一个配置项(其实注释的英文已经说明一切了),
Sets the name of the library下,填写你希望生成的so库的名称,这里注意的是,你填写的名称,最终会被自动加上lib的前缀和.so的后缀,因此你只需要基础的名称就够了,最终生成的so文件会在工程的app-build-intermediates-cmake-debug-obj目录下,每个abi环境都有个文件夹,里面放着对应的so库;
Sets the library as a shared library下,填写你要生成的so库的类型,这里的SHARED是指动态链接库类型;
Provides a relative path to your source file(s)下,填写你要编译成so文件的对应的c源文件的路径;
find_library:
主要是用作添加我们本地库的依赖库,
Sets the name of the path variable下,填写你引用的库的别名,便于引用;
you want CMake to locate下,填写你要引用的依赖库;
target_link_libraries:
是为了关联我们自己的库和一些第三方库或者系统库,
Specifies the target library下,填写本地库的名称;
included in the NDK下,填写你要关联的库的名称。

  1. ok,现在让我们点击一下工具栏的锤子图标,就可以生成so库了。
补充一些tips
关于环境

android ndk开发需要设置环境变量以及安装sdk,ndk,cmake,lldb,如果没有,可以在as的tools选项里,点击sdk manager,在sdk tools栏里,把这些都勾上,点击确定即可;

关于默认生成的例子

按照常规的ndk开发流程,应该是先写java类,这个类里可能会引用到native的方法也就是拿c实现的方法;然后javah命令编译这个java类,生成.h的头文件(这一步可以不用);再写一个.c或者.cpp实现在你java类里引用的这个native方法;最后编译你的c文件,生成最终的so库。
在系统默认的例子里,你点一下编译按钮就能得到so库,可能对这个流程不太明白,下面我们具体说一下:
先看一下MainActivity类的代码:


public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

你可以当做这是系统给你先写好的一个引用了native方法的java类,也就是说这就是ndk第一步编写java类的产物;
在static里,System.loadLibrary("native-lib")这句会先于onCreate函数触发,这句是载入我们生成的动态链接库.so文件,这里只需要填写CMakeLists里面填写的名称即可;然后你会看到下面引用了这个库里的native方法,也就是这句public native String stringFromJNI();这就是系统帮你做好的第一步编写java类;
然后我们需要编写实现这个native方法的.c文件,这里系统也帮你写好了就是在cpp文件夹里的那个native-lib.cpp的文件,这个文件里实现了stringFromJNI()这个方法;
最后,我们点击编译,成功生成了so文件,这就是系统的例子里,ndk开发的完整流程。

Hello World 工程引发的被坑事件

前面是介绍了如何在as3的环境下进行ndk开发以及解析了系统自带的例子,接下来我们肯定要仿照系统的例子,自己走一遍ndk开发的流程,当然第一个工程肯定就是通用的HelloWorld了。

在小黄书(android应用安全防护和逆向分析)里,也有对helloworld工程的举例,流程是:新建一个java类,在java类里申明一个native方法,再在main方法里载入动态链接库,新建一个实例调用native方法;然后使用vc6.0编译器编写好native方法,生成.dll动态链接库文件;最后把该dll文件写入环境变量内,运行java类;具体代码和流程参见小黄书p14-p15页。

不过,其实不用如此麻烦,之所以用vc写c的内容,是因为当时as编译器对c的编写支持的还不够好,但as3以后,这块已经开始逐步完善了,因此,我们可以不用按照书里的流程,直接使用as3来写c的内容,不过,这里有一个坑,开始我是想仿造小黄书的示例代码,单独写一个具有main方法的java类来调用本地方法,大概是这样:

package com.example.test.testndk;

public class HelloWorld {

    public native String stringFromJNITest();
    static {
        
        System.loadLibrary("JNITest");
    }


    public static void main(String[] args){
        HelloWorld hello = new HelloWorld();
        String hi = hello.stringFromJNITest();
        System.out.println(hi);
    }
}

JNITest是我事先写好的cpp文件编译成的.so库,里面实现了stringFromJNITest()方法,.cpp内容是这样的:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring

JNICALL
Java_com_example_test_testndk_MainActivity_stringFromJNITest(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello world";
    return env->NewStringUTF(hello.c_str());
}

以上其实就是我们自己实现一个ndk开发的流程,即先写一个java类,在类中声明一个native方法,然后写.cpp实现这个native方法,编译cpp成so库,在java类里用loadlibrary加载这个so库。

本例中,这是一个简单的native方法就是返回了一串字符串“Hello World”而已,原本我以为到此结束,点击运行这个java类我们的Hello World之旅就结束了,但是没想到报了个错:java.lang.UnsatisfiedLinkError: no JNITest in java.library.path。

开始我怀疑是不是CMakelists.txt的配置问题,但是我把这个so库在android工程的mainactivity里引用,居然没有问题,也就是说,应该不是配置的路径问题。经过在群里讨论以及百度,又开始怀疑是不是java.library.path的问题,因为据了解,其实你用as3运行android工程的mainactivity,编译器是会自动替你生成so库用的(只要你CMakelists配置好了),所以不会有路径问题,但是当你新建一个java类,写上main方法,独立运行这个java类的时候,其实是按java工程的方式找你的动态链接库文件的,而这个方式,在win下,是在环境变量path里找;在linux下,是在系统变量LD_LIBRARY_PATH里找,为了证明这个结论,我在系统的环境变量path里,添加了so库所在的目录路径。

但是,依旧报错。。no JNITest in java.library.path;这时群友又提醒我,我是win的系统,而win的动态链接库的格式是.dll,因此光配置了路径,但因为我的格式是 .so,因此也找不到;现搭一个linux系统来做实验成本太高,我采用了一个取巧的方式,直接把生成的.so文件后缀改为了dll,因为如果是后缀的原因,那么会报错,但肯定不会再报找不到的错,算是侧面论证。

我把后缀改为dll之后,再次运行!奇迹发生了,果然,还是报on JNITest in java.library.path,为什么呢,我猜可能是win下和android下,寻找规则不同,在android工程里,只需要写库名,不需要写前缀lib和后缀 .so,在win下,则需要写上前缀lib;最后,我把 System.loadLibrary("JNITest");改为System.loadLibrary("libJNITest");再次点击运行!

这次报错不同了,不再是找不到,而是Can't load this .dll (machine code=0x34) on a AMD 64-bit platform;终于,本次Hello World被坑事件的坑算是完美填上。

目录
相关文章
|
11天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
17天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
3天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
18天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
21天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
18天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
19天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
26 2
|
21天前
|
Android开发
布谷语音软件开发:android端语音软件搭建开发教程
语音软件搭建android端语音软件开发教程!
|
28天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
27天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
32 5