Android NDK 开发:实战案例

2023-05-16

0. 前言

如果只学理论,不做实践,不踩踩坑,一般很难发现真正实践项目中的问题的,也比较难以加深对技术的理解。所以延续上篇 JNI 的实战Android NDK开发:JNI实战篇 ,这篇主要是一些 NDK 小项目的练习,由于这些项目网上都有 demo介绍,这里不会具体一步步介绍如何操作,只记录一些个人需要注意的地方或一些主要步骤,详细的介绍或代码可以点击里面的链接查看。

1. 文件加解密和分割合并

1.1 简介

所有文件都是二进制存储的,无论是文本,图片还是视频文件都是以二进制存储在磁盘中。所以可以通过对文件进行二进制运算进行加解密。下面用到的是比较简单的^异或运算来对文件加解密(算是一种对称加密算法)
附:加解密算法扩展:加解密算法 · 区块链技术指南

一般在大文件传输时,如音视频文件,会将文件分割后再传输,从而提高效率。当需要使用时,再将分割后的文件合并即可。

而文件加解密设计到安全,可以使用 NDK 增加反编译的难度。另外文件的分割合并都比较耗性能,可以放到 NDK 处理提高效率。

以下练习参考:NDK开发基础②文件加密解密与分割合并 - 简书

效果图如图,进入界面会拷贝两张 assets 的图片cats.jpgimage.jpg到本地 sdcard/NdkSample 目录下作为测试。加密后的图片 cats_encypt.jpg是无法直接查看的,合成的图片是 image.jpeg

效果图

1.2 文件加解密

Java 代码

public class FileUtils {
    private static final String FILE_PATH_PREFIX = Environment.getExternalStorageDirectory() + File.separator;
    private static final String FOLDER_NAME = "NdkSample" + File.separator;
    public static final String FILE_PATH = FILE_PATH_PREFIX + FOLDER_NAME;

    public static boolean fileEncrypt() {
        String normalFilePath = FILE_PATH + "cats.jpg";
        String encryptFilePath = FILE_PATH + "cats_encrypt.jpg";
        try {
            return fileEncrypt(normalFilePath, encryptFilePath);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    public static boolean fileDecode() {
        String encryptFilePath = FILE_PATH + "cats_encrypt.jpg";
        String decodeFilePath = FILE_PATH + "cats_decode.jpg";
        try {
            return fileDecode(encryptFilePath, decodeFilePath);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

       private static native boolean fileEncrypt(String normalFilePath, String encryptFilePath);

    private static native boolean fileDecode(String encryptFilePath, String decodeFilePath);

}

JNI 加密代码实现,注意加文件读写权限

const char *PASSWORD = "pw";
long getFileSize(char* filePath);

extern "C"
JNIEXPORT jboolean JNICALL
Java_cn_cfanr_ndksample_utils_FileUtils_fileEncrypt(JNIEnv *env, jclass type, jstring normalFilePath_,
                                                    jstring encryptFilePath_) {
    const char *normalFilePath = env->GetStringUTFChars(normalFilePath_, 0);
    const char *encryptFilePath = env->GetStringUTFChars(encryptFilePath_, 0);

    int passwordLen = strlen(PASSWORD);

    LOGE("要加密的文件的路径 = %s , 加密后的文件的路径 = %s", normalFilePath, encryptFilePath);

    //读文件指针
    FILE *frp = fopen(normalFilePath, "rb");
    // 写文件指针
    FILE *fwp = fopen(encryptFilePath, "wb");

    if (frp == NULL) {
        LOGE("文件不存在");
        return JNI_FALSE;
    }
    if (fwp == NULL) {
        LOGE("没有写权限");
        return JNI_FALSE;
    }

    // 边读边写边加密
    int buffer;
    int index = 0;
    while ((buffer = fgetc(frp)) != EOF) {
        // write
        fputc(buffer ^ *(PASSWORD + (index % passwordLen)), fwp);  //异或的方式加密
        index++;
    }
    // 关闭文件流
    fclose(fwp);
    fclose(frp);

    LOGE("文件加密成功");

    env->ReleaseStringUTFChars(normalFilePath_, normalFilePath);
    env->ReleaseStringUTFChars(encryptFilePath_, encryptFilePath);

    return JNI_TRUE;
}

解密代码类似。

1.3 文件分割合并

Java 代码实现

public static boolean fileSplit() {
        String splitFilePath = FILE_PATH + "image.jpg";
        String suffix = ".b";
        try {
            return fileSplit(splitFilePath, suffix, 4);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 文件合并
     *
     * @return
     */
    public static boolean fileMerge() {
        String splitFilePath = FILE_PATH + "image.jpg";
        String splitSuffix = ".b";
        String mergeSuffix = ".jpeg";
        try {
            return fileMerge(splitFilePath, splitSuffix, mergeSuffix, 4);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

 /**
     * 文件分割
     *
     * @param splitFilePath 要分割文件的路径
     * @param suffix        分割文件的扩展名
     * @param fileNum       分割文件的数量
     * @return
     */
    private static native boolean fileSplit(String splitFilePath, String suffix, int fileNum);

    /**
     * 文件合并
     *
     * @param splitFilePath 分割文件的路径
     * @param splitSuffix   分割文件的扩展名
     * @param mergeSuffix   合并文件的扩展名
     * @param fileNum       分割文件的数量
     * @return
     */
    private static native boolean fileMerge(String splitFilePath, String splitSuffix, String mergeSuffix, int fileNum);

注意,文件的分割合并需要设置文件扩展名后分割文件数量。分割时,分两种情况,
1)能整除的,直接平均分;
2)不能整除的,fileSize % ( n -1),前 n -1 个平均分,剩余的留给最后一个;
合并时,需要注意的是,必须按照分割的顺序合并

其余 JNI 实现代码略,可以到 GitHub 查看具体源码:NdkSample/native_file_handler.cpp

2. Android 增量更新

2.1 简介

所谓增量更新,是服务器将新旧版本的 apk 做差分处理,生成一个差分包 patch,下发到客户端;客户端再用 patch 包和本地的 apk 合并成新的 apk,再安装。很显然,这样在一定程度上可以减少更新 apk 时消耗的流量。目前在很多应用市场也有用到这种技术。增量更新技术主要解决是安装包文件过大的问题。

2.2 优缺点

优点:节省流量,下载 apk 时,只需要下载差分包,不用下载完整包;
缺点:

  • 客户端和服务端都需要加入相关的支持。每次新版本发布,服务器需要根据新版本对以前所有老版本生成对应的差分包,而且还要维护不同渠道的包;另外客户端请求时,上传当前版本号,服务器返回对应的差分包和新版本 apk的 md5 值,作为合并新 apk 后的校验;所以整体流程会有点繁琐;
  • 合成差分包会有点耗时(最好用单独线程处理)和耗内存的,内存不足的手机或本地 apk 损坏的 apk 无法进行增量更新;另外 apk 包之间差异比较小(2m 以下)时,生成的差分包仍然有几百 k;

2.3 差分包的生成与合并

需要用工具对文件进行 diff 和 patch 处理,一般可以通过 bsdiff实现

具体使用可以参考 Hongyang 的文章 Android 增量更新完全解析 是增量不是热修复 - Hongyang,在这里就不啰嗦了

注意,在执行 make 命令,可能报以下错误,(以下环境都是在 Mac 上)

bspatch.c:39:21: error: unknown type name 'u_char'; did you mean 'char'?
static off_t offtin(u_char *buf)
                    ^~~~~~
                    char

可以通过在bspatch.c 文件加上#include <sys/types.h>头文件(Hongyang 没说明清楚),参考:编译和使用bsdiff - 木头平 - 博客园

主要掌握几个命令:

  • 执行 make 命令,使 makefile 生成 bsdiff 和 bspatch 可执行文件;
  • 执行 ./bsdiff old.apk new.apk update.patch,生成差分包;
  • 执行 ./bspatch old.apk new2.apk update.patch,合成新 apk;
  • 执行 md5 xxx.apk 查看 apk 的 md5 值;

2.4 服务端操作

服务端需要返回一个文件和新版本 apk md5值给客户端。

  • 用 2.3 生成的 bsdiff 可执行文件生成新版本 apk 和老版本的 apk 的差分包 update.patch;(可以编写个脚本处理)
  • 使用 md5 命令查看新 apk 的 md5,并保存,之后需要返回给客户端;

2.5 客户端操作

主要实现是如何制造 bspatch 的 so 文件,由于网上详细的步骤都比较完善了,这里也不啰嗦了,只简单说下需要注意的问题。

由于按照 AS 的默认的 CMake 建 NDK 的方式,C/C++ 的文件是在 cpp 目录的,Hongyang 的是在 jni 目录,两者配置方式不太一样,如果同时使用,只会编译 CMake 的配置,所以需要将 bspatch.c放到 cpp 目录,同时还需要下载 bzip 源码

详细步骤可以参考,Android增量更新与CMake构建工具 - 亚特兰蒂斯 - CSDN博客

PS:这里的 CMakeLists.txt 是放在 cpp 目录下,特别需要注意的是,由于 CMakeLists.txt 目录改变了,必须修改 C/C++ 源文件的路径,去掉 src/main/cpp,同时也要修改build.gradle的 cmake 文件的路径,不然会编译失败

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)

#支持-std=gnu++11
set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wall -DGLM_FORCE_SIZE_T_LENGTH")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGLM_FORCE_RADIANS")

#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})

#添加bzip2目录,为构建添加一个子路径
set(bzip2_src_DIR ${CMAKE_SOURCE_DIR})
add_subdirectory(${bzip2_src_DIR}/bzip2)

add_library( native-lib
             SHARED
             # Provides a relative path to your source file(s). 注意,CMakeLists.txt 在 cpp 目录下,此处不需要加路径前缀 src/main/cpp
             native_file_handler.cpp
             bspatch.c
             )

find_library(log-lib log )

target_link_libraries(native-lib ${log-lib} )

build.gradle 文件

      externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }

另外,修改的 bspatch.c 文件增加的 JNI 代码中,第一个参数是 so 库的名字,注意一定要保持一致

//……
JNIEXPORT jint JNICALL Java_cn_cfanr_ndksample_utils_BsPatch_bspatch(JNIEnv *env, jclass jcls, jstring oldApk_,jstring newApk_, jstring patch_) {
    const char *oldApkPath = (*env)->GetStringUTFChars(env, oldApk_, 0);
    const char *newApkPath = (*env)->GetStringUTFChars(env, newApk_, 0);
    const char *patchPath = (*env)->GetStringUTFChars(env, patch_, 0);

    int argc = 4;
    char* argv[argc];
    argv[0] = "native-lib";  //注意此处是 so 库名字
    argv[1] = oldApkPath;
    argv[2] = newApkPath;
    argv[3] = patchPath;

    jint ret = patchMethod(argc, argv);

    (*env)->ReleaseStringUTFChars(env, oldApk_, oldApkPath);
    (*env)->ReleaseStringUTFChars(env, newApk_, newApkPath);
    (*env)->ReleaseStringUTFChars(env, patch_, patchPath);
    return ret;
}
//……

其他代码逻辑:

  • 1)从服务器下载差分包 update.patch 保存到本地,并请求获取新版 apk 的 md5值;
  • 2)提取本地的 apk 文件;
  • 3)使用 JNI 方法public static native int bspatch(String oldApk, String newApk, String patch)将 update.patch 和本地旧的 apk 合并成新的 apk;
  • 4)校验生成的新 apk 的 md 值是否和服务器返回的一样;
  • 5)检测新 apk 和服务器提供的一致后,安装新的 apk 文件

不过 demo 是没有写从服务器下载差分包的逻辑的,这里是将差分包通过 adb push patch路径 /sdcard/NdkSample 命令放到手机来测试的

具体代码可以查看 Github:NdkSample/PatchUpdateActivity.java

3. Android 封装 libjpeg 库

3.1 编译 libjpeg.so 库

  • 1.克隆 libjpeg-trubo Android 版到本地,并解压
git clone git://git.linaro.org/people/tomgall/libjpeg-turbo/libjpeg-turbo.git -b linaro-android
  • 2.在配置好 ndk-build 环境后(具体步骤略),开始编译 libjpeg-trubo 库
    按照网上大多数教程的步骤都是执行以下命令
ndk-build APP_ABI=armeabi-v7a,armeabi 

但可能由于我本地配置的版本是 ndk-14的,直接执行这个命令并没有奏效,以下是我遇到的一些错误:

1) 如果你没进入 libjpeg-turbo 目录就执行命令,可能会报以下错误,也就是找不到 Android.mk

Android NDK: Your APP_BUILD_SCRIPT points to an unknown file: ./Android.mk
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/add-application.mk:198: *** Android NDK: Aborting...    .  Stop.

2) 如果报找不到应用项目的目录,如下:

Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/build-local.mk:151: *** Android NDK: Aborting    .  Stop.

就需要设置下 NDK_PROJECT_PATH 指定需要编译的代码的工程目录,这里给出的是当前目录,还有,APP_BUILD_SCRIPT是Android makefile文件的路径,如果你还有 Application.mk 文件的话,则可以添加 NDK_APP_APPLICATION_MK=./Application.mk,参考:Android开发实践:在任意目录执行NDK编译 - Jhuster的专栏

3) 如果 NDK 版本过高,可能会报以下错误,

Android.mk:11: Extraneous text after `ifeq' directive
Android NDK: WARNING: Unsupported source file extensions in Android.mk for module jpeg
Android NDK:   turbojpeg-mapfile
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/build-binary.mk:687: Android NDK: Module jpeg depends on undefined modules: cutils
/Users/cfanr/Library/Android/sdk/ndk-bundle/build/core/build-binary.mk:700: *** Android NDK: Aborting (set APP_ALLOW_MISSING_DEPS=true to allow missing dependencies)    .  Stop.

所以,我最终的解决方法是用指定的低版本的 ndk (ndk-r11c)去编译,而不是用我在系统配置 ndk。正确的命令,使用指定NDK版本编译
~/NDK/android-ndk-r11c/ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_ABI=armeabi-v7a,armeabi

检测已编译成功:编译成功后,会在 libjpeg-turbo 生成 libs 和 obj 文件夹,里面分别会有你设置的 ABI 类型的 libjpeg.so 库和其他生成的文件,需要拷贝到项目中的是 libs 文件下的 libjpeg.so 库。

3.2 使用 libjpeg.so 库编写压缩图片的 native 方法

参考:Android使用libjpeg实现图片压缩 - BlueBerry的专栏 - CSDN博客

  • 1.拷贝 libjpeg.so 和头文件到项目中
    首先将不同 ABI 的 libjpeg.so 拷贝到项目的 libs 目录下,再将上面下载的 libjpeg-turbo 的源码的所有头文件拷贝到 cpp/include 目录下

  • 2.配置CMakeLists文件
    练习时,要注意 CMakeLists 的配置,不然可能会发生以下错误(博主就是因为没看清楚文章,没配置好,出错后,一直搜索,浪费不少时间 ��‍♀️)

1)如果 CMakeLists.txt 文件关联到Android的 Bitmap 相关库jnigraphics,会报以下错误,未定义 Bitmap

Error:(39) undefined reference to 'AndroidBitmap_getInfo'
Error:(43) undefined reference to 'AndroidBitmap_lockPixels'
Error:(85) undefined reference to 'AndroidBitmap_unlockPixels'
Error:error: linker command failed with exit code 1 (use -v to see invocation)

2)如果只是添加了 libjpeg.so 库add_library(libjpeg SHARED IMPORTED ),未设置关联库target_link_libraries(),会报类似以下未定义某些属性的错误:

Error:(99) undefined reference to 'jpeg_std_error'
Error:(106) undefined reference to 'jpeg_CreateCompress'
Error:(114) undefined reference to 'jpeg_stdio_dest'
Error:(122) undefined reference to 'jpeg_set_defaults'
Error:(126) undefined reference to 'jpeg_set_quality'

看来不熟悉理解 CMake 构建脚本还是挺容易踩坑的,下篇会详细介绍 CMake 构建脚本的使用。
完整构建脚本如下:(如果 libjpeg.so 或头文件放置的目录和我的不一样,下面就需要修改下)

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)

#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})

#指定要引用的libjpeg.so的头文件目录
set(LIBJPEG_INCLUDE_DIR src/main/cpp/include)
include_directories(${LIBJPEG_INCLUDE_DIR})

#导入libjpeg动态库 SHARED;静态库为STATIC
add_library(libjpeg SHARED IMPORTED)
#对应so目录,这里为了简单设置的是绝对路径(注意要先 add_library,再 set_target_properties)
set_target_properties(libjpeg PROPERTIES IMPORTED_LOCATION /Users/cfanr/AndroidStudioProjects/DemoProjects/NDKSample/compress/libs/${ANDROID_ABI}/libjpeg.so)

add_library(
             compress
             SHARED
             src/main/cpp/compress.c
             )

find_library(graphics jnigraphics)
find_library(log-lib log)

target_link_libraries(compress libjpeg ${log-lib} ${graphics})
  • 3.编写Java 层 native 方法
public class ImageCompress {
    static {
        System.loadLibrary("compress");
    }

    private ImageCompress(){
    }

    public static native boolean compressBitmap(Bitmap bitmap, String dstPath, int quality, boolean isOptimize);
}
  • 4.实现 JNI 逻辑
    • 1)将 Android 的 Bitmap 解码转化为 RGB 数据;
    • 2)为 JPEG 对象分配空间并初始化;
    • 3)获取文件信息,然后指定压缩数据源;
    • 4)为压缩设定参数,包括图像大小,颜色空间;
    • 5)开始压缩;
    • 6)压缩完毕后,释放资源;

具体代码查看: navyifanr/NdkSample: compress.c

效果图:
效果图

4. NDK技术在 Android 的应用场景简述

4.1 首先需要了解 NDk 有什么作用和特点?

NDK 作用是 Google 提供了交叉编译工具链,能够在 linux 平台编译出在 arm 平台下执行的二进制库文件;
NDK 特点:(来自:Android:JNI 与 NDK到底是什么?- Carson_Ho的博客 - CSDN博客)
NDK 特点

4.2 应用场景

  • 优化密集运算和消耗资源较大模块的性能,如音视频解码,图像操作等
  • 需要提高安全性的地方,编译成 so 库不容易被反编译;如文件加密、核心算法模块等;
  • 跨平台应用的需要;

一些 Android NDK 的具体应用场景:

  • 跨平台的音视频解码库 FFmpeg;
  • Android 增量更新技术;
  • Android 加固和防逆向技术;
  • 一些热修复技术;
  • 人脸识别,OpenCV 等
  • Android 平台的游戏开发等;

附:
C/C++代码被编译成库文件之后,才能执行,库文件分为动态库和静态库两种:
SO库类型

库文件来源:C/C++代码进行编译链接操作之后,才会生成库文件,不同类型的CPU 操作系统生成的库文件是不一样;

  • CPU分类:arm结构,嵌入式设备处理器; x86结构,pc 服务器处理器; 不同的CPU指令集不同;
  • 交叉编译:windows x86编译出来的库文件可以在arm平台运行的代码;
  • 交叉编译工具链:Google提供的 NDK 就是交叉编译工具链,可以在linux环境下编译出在ARM 平台下执行的二进制库文件;

参考资料:

  • NDK开发基础②文件加密解密与分割合并 - 简书
  • Android 增量更新完全解析 是增量不是热修复 - Hongyang - CSDN博客
  • Android增量更新与CMake构建工具 - 亚特兰蒂斯 - CSDN博客
  • JNI开发实例-封装libjpeg库 保证图片质量压缩图片 - 猫的阁楼 - CSDN博客
  • Android使用libjpeg实现图片压缩 - BlueBerry的专栏 - CSDN博客
  • Android:JNI 与 NDK到底是什么?- Carson_Ho的博客 - CSDN博客
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android NDK 开发:实战案例 的相关文章

随机推荐

  • js实现打字机效果---Day06

    我常想象这样一幅画面 xff1a 素雅的大背景 xff0c 伴着可心的音乐 xff0c 优雅旋转着的芭蕾舞者 xff0c 旁边那不断打出的文字 xff0c 仿佛就这样娓娓道来他们那美美的故事 xff1b 也常想象 xff1a 呼喇啦甩动的大
  • 纯css3实现漂亮的对话框----Day07

    姑且先不来讨论css3跟css的区别 xff0c 也不说html和html5的不同 xff0c 虽然这很关键 xff0c 但是现阶段还真的没整利落了 xff0c 姑且就这些应用先用着 xff0c 等自己有些见解了再来探讨那些深层次的问题 先
  • 纯css3实现饼状图-------Day21

    现代网站在商务应用中比较广泛 xff0c 什么oa xff0c 什么erp xff0c 除了导入导出 xff0c 就是数据统计 xff0c 再不然就来个做个报表 xff0c 而饼状图作为数据的一种直观统计显示 xff0c 应用是非常广泛的
  • 你是如何理解var e=e||window.event的------Day26

    你是如何理解var e 61 e window event的 xff1f 相信很多人都能给我个回答说是 xff1a 为了实现多种浏览器兼容 不错 xff0c 确实是为了实现浏览器兼容 xff0c 但是它又是如何实现浏览器兼容的呢 xff1f
  • js实现回放拖拽轨迹-------Day48

    今天有点小高兴 xff0c csdn博客浏览量过万了 xff0c 在过去还从来没有过这么高的浏览量呢 xff0c 不得不说 xff0c 太多时候还是有些矫情 xff0c 可看到这些鼓励还是忍不住高兴啊 xff0c 至少 xff0c 这样让我
  • js实现动态删除表格的行或者列-------Day57

    昨天记录了动态添加表格的一行 xff0c 当然这个一行是指一行数据 xff0c 也就是说一行多少列也是加上的 xff0c 并且第几列的内容都可以添加上 xff0c 先来回顾下它的实现的关键点 xff1a 1 var row 61 table
  • js实现表格的选中一行-------Day58

    最开始想更多的用js来动态操作表格 xff0c 是因为在应用了easyUI之后 xff0c 发现直接写一个 lt table id 61 34 tt 34 gt lt table gt xff0c 这就够了 xff0c 界面里面就剩下这么一
  • 积跬步,聚小流-------关于UML时序图

    uml时序图的存在 在上一篇中记录了uml类图 xff0c 用类图来描述代码中所需要的类以及相互之间的关系 xff0c 也就立体的将整个程序框架展现在了我们面前 xff0c 像一幅画 xff0c 有山有水有人 一张照片只能定格当时的美好 x
  • 积跬步,聚小流------用smartpaginator来做分页

    网络上的实例 xff08 jquery smarypaginator 例图 xff09 如果说是从 百度 上搜索过 jquery分页插件 的朋友 xff0c 相信对上面的图片不会陌生 xff0c 几乎所有介绍 jquery分页插件 的文章中
  • 我的2017-搭建个人网站,搭建PHP环境(2)

    上周确定了 xff0c 想要应用的后台语言 xff0c 面临的最大问题就是 xff1a php我不会啊 xff0c 哈哈哈哈 xff0c 所以接下来首先要做的就是了解 学习php的相关知识 接下来的第一步 xff1a 环境搭建 1 下载安装
  • 设计一个类:只能在堆上创建对象?只能在栈上创建对象?只能创建一个对象?

    在C 43 43 中 xff0c 类的对象建立分为两种 xff0c 一种是静态建立 xff0c 如A a xff1b 另一种是动态建立 xff0c 如A ptr 61 new A xff1b 这两种方式是有区别的 静态建立一个类对象 xff
  • 我的2017-搭建个人网站,hello PHP(2)

    学习一门语言 xff0c 例行惯例 xff0c 先来个 hello world 搭建好了php环境 xff0c 然后就可以运行php了 xff0c 首先用一种最简单的方法 xff0c 在wamp安装位置 xff08 相应的文件夹 xff09
  • 我的2017-搭建个人网站,自拟定代码根目录

    wampserver集成安装环境安装的php的运行根目录在wamp文件夹中的www文件夹下 xff0c 而为了有效的将代码和服务器进行分离 xff0c 可以采用自拟定代码根目录进行修改 1 确定代码编辑位置 xff0c 修改服务器默认指向
  • 编译原理:求First集与Follow集的方法

    明天就要考试了 xff0c 发现一直理解错了First集与Follow集的解法 xff0c 贴上比较好理解的 文法 xff1a S ABc A a B b First集合求法 能 由非终结符号推出的所有的开头符号或可能的 xff0c 但要求
  • 位运算n & (n-1)的妙用

    本文转自 xff1a http blog csdn net zheng0518 article details 8882394 按位与的知识 n amp n 1 作用 xff1a 将n的二进制表示中的最低位为1的改为0 xff0c 先看一个
  • 二分查找算法(Java版)

    二分查找算法是非常经典且基本的算法 1 二分查找又称折半查找 xff0c 优点是比较次数少 xff0c 查找速度快 xff0c 平均性能好 xff1b 其缺点是要求待查表为有序表 xff0c 且插入删除困难 因此 xff0c 折半查找方法适
  • 电脑眼睛保护色——绿豆沙色

    眼科医生建议电脑屏幕不要用白色 xff0c 因为白色对眼睛的刺激是最大的 xff0c 最损伤视力 xff01 眼科医生推荐的颜色是 柔和的淡绿色 xff0c 也有人叫做绿豆沙色 按HSU 设置为 xff1a 色度 HUE xff1a 85
  • Java中两个数交换的细节问题

    在码代码过程中 xff0c 两个数交换是经常用到的 xff0c 但在Java 中 xff0c 如果忽略了值传递和引用传递就很容易出错 看一下两个整数交换代码 xff1a public void swap int a int b int t
  • 微信 Mars Android Sample 源码分析

    注 xff1a 原文首发地址 零 前言 Mars 是微信官方开源的跨平台跨业务的终端基础组件 xff0c 具有高质量网络连接模块 长短连接 智能心跳机制 高性能日志模块和网络监测组件等 而整个 Android Sample 是基于 Mars
  • Android NDK 开发:实战案例

    0 前言 如果只学理论 xff0c 不做实践 xff0c 不踩踩坑 xff0c 一般很难发现真正实践项目中的问题的 xff0c 也比较难以加深对技术的理解 所以延续上篇 JNI 的实战Android NDK开发 xff1a JNI实战篇 x