Android面试题(32)-android编译过程和安装流程

2023-10-28

android app的编译过程

从网上拷了一张图

这张图很好的讲述了android的编译打包过程,接下来就具体的分析分析,大致分为7步:

(1)aapt(Android Asset Packaging Tool,android构建工具,在android-sdk的build-tool目录下)它的主要工作就是把项目中使用到的资源文件打包成R.java文件;

(2)aidl工具会将aidl接口转换为java接口

(3)java编译器就会将上述准备好的文件和我们在项目敲得java源文件打包成.class文件

       R.java文件+aidl接口+java源文件===>.class字节码文件;

(4)如果是java程序,把.class文件交给java虚拟机就可以了,但是android使用的不是java虚拟机,是davlik虚拟机,所以编译成.class文件还不行,还需要通过dex工具把.class文件打包成.dex文件,这里如果你项目中使用了第三方的库,也会在这里一起打包成.dex文件;

(5)通过apkbuilder工具将编译过的文件和那些没有编译过的文件(图片,视频等)加上上述的.dex文件一起打包成.apk文件;

(6)这时候的.apk文件还无法去使用,还需要通过Jarsigner这个工具对.apk进行签名,至于签名的原因:为了保证每个应用程序开发商合法ID,防止部分开放商可能通过使用相同的Package Name来混淆替换已经安装的程序,我们需要对我们发布的APK文件进行唯一签名,保证我们每次发布的版本的一致性(如自动更新不会因为版本不一致而无法安装)。

(7)签名过后的.apk文件其实就可以使用了,但是这时候的.apk文件太过杂乱,还需要Zipalign工具进行.apk文件的对其,减少内存,整理apk文件;

通过这七大步骤,一个apk文件就完整的生成出来了,那么我们知道apk文件是怎么生成的了,那么他又是怎么安装到我们的手机上呢?

android app的安装流程

android app的安装方式大致分为四种:

(1)系统应用安装---开机完成,没有安装界面;

(2)网络下载应用安装----通过mark应用完成,没有安装界面

(3)adb工具安装----没有安装界面

(4)第三方应用安装----通过sd卡中的apk文件安装,有安装界面,由Packageinstaller.apk应用处理完成及卸载

四大目录:

system/app-----系统自带的应用程序,获得adb root权限才能够访问

data /app-----用户程序安装目录,用户安装时将apk文件拷贝至此目录

data/data-----存放应用程序的数据

data/dalvik-cache----将apk中的dex文件安装到此目录下;

安装过程(用户程序):复制APK安装包到data/app目录下,解压并扫描安装包,把dex文件(Dalvik字节码)保存到dalvik-cache目录,并data/data目录下创建对应的应用数据目录。

安装步骤大致分为四步:

(1) 拷贝apk文件到指定目录
在Android系统中,apk安装文件是会被保存起来的,默认情况下,用户安装的apk首先会被拷贝到 /data/app 目录下。
/data/app目录是用户有权限访问的目录,在安装apk的时候会自动选择该目录存放用户安装的文件,而系统出厂的apk文件则被放到了 /system 分区下,包括 /system/app,/system/vendor/app,以及 /system/priv-app 等等,该分区只有Root权限的用户才能访问,这也就是为什么在没有Root手机之前,我们无法删除系统出厂的app的原因了。
(2) 解压apk,拷贝文件,创建应用的数据目录
为了加快app的启动速度,apk在安装的时候,会首先将app的可执行文件(dex)拷贝到 /data/dalvik-cache 目录,缓存起来。
然后,在/data/data/目录下创建应用程序的数据目录(以应用的包名命名),存放应用的相关数据,如数据库、xml文件、cache、二进制的so动态库等等。
(3) 解析apk的AndroidManifinest.xml文件
Android系统中,也有一个类似注册表的东西,用来记录当前所有安装的应用的基本信息,每次系统安装或者卸载了任何apk文件,都会更新这个文件。这个文件位于如下目录:
/data/system/packages.xml
系统在安装apk的过程中,会解析apk的AndroidManifinest.xml文件,提取出这个apk的重要信息写入到packages.xml文件中,这些信息包括:权限、应用包名、APK的安装位置、版本、userID等等。
由此,我们就知道了为啥一些应用市场和软件管理类的app能够很清楚地知道当前手机所安装的所有的app,以及这些app的详细信息了。
另外一件事就是Linux的用户Id和用户组Id,以便他可以获得合适的运行权限。
以上这些都是由PackageServiceManager完成的,下面我们会重点介绍PackageServiceManager。
(4) 显示快捷方式
这些应用程序只是相当于在PackageManagerService服务注册好了,如果我们想要在Android桌面上看到这些应用程序,还需要有一个Home应用程序,负责从PackageManagerService服务中把这些安装好的应用程序取出来,并以友好的方式在桌面上展现出来,例如以快捷图标的形式。在Android系统中,负责把系统中已经安装的应用程序在桌面中展现出来的Home应用程序就是Launcher了

接下来详细说说这几个步骤,再说之前,我们先弄清楚一个概念,在之前的app的启动流程中,我们说到,所有的app组件启动都是由AMS去实现的 ,那么安装当然也会有一个对应的服务,也就是PackageManagerService;

Android系统在启动的过程中,会启动一个应用程序管理服务PackageManagerService,这个服务负责扫描系统中特定的目录,找到里面的应用程序文件,即以Apk为后缀的文件,然后对这些文件进解析,得到应用程序的相关信息。应用程序管理服务PackageManagerService安装应用程序的过程,其实就是解析析应用程序配置文件AndroidManifest.xml的过程,并从里面得到得到应用程序的相关信息,例如得到应用程序的组件Activity、Service、Broadcast Receiver和Content Provider等信息,有了这些信息后,通过ActivityManagerService这个服务,我们就可以在系统中正常地使用这些应用程序了。应用程序管理服务PackageManagerService是系统启动的时候由SystemServer组件启动的,启后它就会执行应用程序安装的过程,因此,本文将从SystemServer启动PackageManagerService服务的过程开始分析系统中的应用程序安装的过程。

我们先分析SystemServer是怎样启动PackageManagerService的:
(1)由SystemServer.main方法开始
SystemServer组件是由Zygote进程负责启动的,启动的时候就会调用它的main函数,这个函数主要调用了JNI方法init1来做一些系统初始化的工作。
(2)SystemServer.init1()方法:(JNI方法)
这个函数很简单,只是调用了system_init函数来进一步执行操作。

(3)system_init()方法(c++方法)

这个函数首先会初始化SurfaceFlinger、SensorService、AudioFlinger、MediaPlayerService、CameraService和AudioPolicyService这几个服务,然后就通过系统全局唯一的AndroidRuntime实例变量runtime的callStatic来调用SystemServer的init2函数了。
(4)AndroidRuntime.callStatic方法(c++方法)

这个函数调用由参数className指定的java类的静态成员函数,这个静态成员函数是由参数methodName指定的。上面传进来的参数className的值为"com/android/server/SystemServer",而参数methodName的值为"init2",因此,接下来就会调用SystemServer类的init2函数了。
(5)SystemServer.init2()方法
这个函数创建了一个ServerThread线程,PackageManagerService服务就是这个线程中启动的了。这里调用了ServerThread实例thr的start函数之后,下面就会执行这个实例的run函数了。

(6)SystemServer.run()方法(PackageManagerService就是在这里启动的)

这个函数除了启动PackageManagerService服务之外,还启动了其它很多的服务

我们知道了PackageManagerService的启动流程之后,接下来就针对以上的四种情况一一进行分析,看看PMS是如何去进行app安装的;

(1)系统应用安装

A.扫描安装app

我们之前说过apk文件都是放在对应的文件目录下的,所以我们第一步做的事就是需要在对应文件夹中扫描对应的apk文件,PMS是通过调用它的scanDirLI方法:

private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {
    final File[] files = dir.listFiles();
    if (ArrayUtils.isEmpty(files)) {
        Log.d(TAG, "No files in app dir " + dir);
        return;
    }

    if (DEBUG_PACKAGE_SCANNING) {
        Log.d(TAG, "Scanning app dir " + dir + " scanFlags=" + scanFlags
                + " flags=0x" + Integer.toHexString(parseFlags));
    }
    ParallelPackageParser parallelPackageParser = new ParallelPackageParser(
            mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir,
            mParallelPackageParserCallback);

    // Submit files for parsing in parallel
    int fileCount = 0;
    for (File file : files) {
        final boolean isPackage = (isApkFile(file) || file.isDirectory())
                && !PackageInstallerService.isStageName(file.getName());
        if (!isPackage) {
            // Ignore entries which are not packages
            continue;
        }
        parallelPackageParser.submit(file, parseFlags);
        fileCount++;
    }

    // Process results one by one
    for (; fileCount > 0; fileCount--) {
        ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
        Throwable throwable = parseResult.throwable;
        int errorCode = PackageManager.INSTALL_SUCCEEDED;

        if (throwable == null) {
            // Static shared libraries have synthetic package names
            if (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) {
                renameStaticSharedLibraryPackage(parseResult.pkg);
            }
            try {
                if (errorCode == PackageManager.INSTALL_SUCCEEDED) {
                    scanPackageLI(parseResult.pkg, parseResult.scanFile, parseFlags, scanFlags,
                            currentTime, null);
                }
            } catch (PackageManagerException e) {
                errorCode = e.error;
                Slog.w(TAG, "Failed to scan " + parseResult.scanFile + ": " + e.getMessage());
            }
        } else if (throwable instanceof PackageParser.PackageParserException) {
            PackageParser.PackageParserException e = (PackageParser.PackageParserException)
                    throwable;
            errorCode = e.error;
            Slog.w(TAG, "Failed to parse " + parseResult.scanFile + ": " + e.getMessage());
        } else {
            throw new IllegalStateException("Unexpected exception occurred while parsing "
                    + parseResult.scanFile, throwable);
        }

        // Delete invalid userdata apps
        if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&
                errorCode == PackageManager.INSTALL_FAILED_INVALID_APK) {
            logCriticalInfo(Log.WARN,
                    "Deleting invalid package at " + parseResult.scanFile);
            removeCodePathLI(parseResult.scanFile);
        }
    }
    parallelPackageParser.close();
}

分别扫描以下五个文件夹:

    /system/framework

    /system/app

    /vendor/app

    /data/app

    /data/app-private
由于Android每次启动的时候都需要安装一次信息,但是有些信息是保持不变的,例如Linux用户组Id,PackageManagerService 每次安装程序之后,都会把这些程序的信息保存下来,以便下次使用,恢复上一次程序的安装信息是通过PackageManagerService 的成员变量mSetting的readLP()来实现的,恢复信息之后就开始扫描和安装app了。对于目录中的每一个文件,如果是以后Apk作为后缀名,那么就调用scanPackageLI函数来对它进行解析和安装。

B.调用PMS的scanPackageLI方法

private PackageParser.Package scanPackageLI(PackageParser.Package pkg, File scanFile,
        final int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user)
        throws PackageManagerException {
    // If the package has children and this is the first dive in the function
    // we scan the package with the SCAN_CHECK_ONLY flag set to see whether all
    // packages (parent and children) would be successfully scanned before the
    // actual scan since scanning mutates internal state and we want to atomically
    // install the package and its children.
    if ((scanFlags & SCAN_CHECK_ONLY) == 0) {
        if (pkg.childPackages != null && pkg.childPackages.size() > 0) {
            scanFlags |= SCAN_CHECK_ONLY;
        }
    } else {
        scanFlags &= ~SCAN_CHECK_ONLY;
    }

    // Scan the parent
    PackageParser.Package scannedPkg = scanPackageInternalLI(pkg, scanFile, policyFlags,
            scanFlags, currentTime, user);

    // Scan the children
    final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;
    for (int i = 0; i < childCount; i++) {
        PackageParser.Package childPackage = pkg.childPackages.get(i);
        scanPackageInternalLI(childPackage, scanFile, policyFlags, scanFlags,
                currentTime, user);
    }


    if ((scanFlags & SCAN_CHECK_ONLY) != 0) {
        return scanPackageLI(pkg, scanFile, policyFlags, scanFlags, currentTime, user);
    }

    return scannedPkg;
}
这个函数首先会为这个Apk文件创建一个PackageParser实例,接着调用这个实例的parsePackage函数来对这个Apk文件进行解析。这个函数最后还会调用另外一个版本的scanPackageLI函数把来解析后得到的应用程序信息保存在PackageManagerService中。
这有一个PackageParser,只需要记住这个类主要是对apk文件进行解析,主要就是解析AndroidManifest.xml文件;每一个Apk文件都是一个归档文件,它里面包含了Android应用程序的配置文件AndroidManifest.xml,这里主要就是要对这个配置文件就行解析了,从Apk归档文件中得到这个配置文件后,就调用另一外版本的parsePackage函数对这个应用程序进行解析了;
我们如果继续跟踪scanPackageLI方法,发现最终程序经过很多次的if else 的筛选,最后判定可以安装后调用了 mInstaller.install:
PackageManagerService通过套接字的方式访问installd服务进程,在Android启动脚本init.rc中通过服务配置启动了installd服务进程,具体的installd过程是调用底层的C代码,这里就不做分析了,如果需要可以看这篇文章   链接

这里我们把PMS的作用总结一下:

1)从apk, xml中载入pacakge信息, 存储到内部成员变量中, 用于后面的查找. 关键的方法是scanPackageLI().
2)各种查询操作, 包括query Intent操作.
3)install package和delete package的操作. 还有后面的关键方法是installPackageLI().

(2)从网络上下载应用:
其实这种安装方式最后都是调用installer的接口去进行安装,只不过在之前会进行一些逻辑处理:
首先,下载完成后,会自动调用PackageManager的installPackage方法:
public void installPackage( final Uri packageURI, final IPackageInstallObserver observer, final int flags, final String installerPackageName) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INSTALL_PACKAGES, null); Message msg = mHandler.obtainMessage(INIT_COPY); msg.obj = new InstallParams(packageURI, observer, flags, installerPackageName); mHandler.sendMessage(msg); } 

public void installPackage( final Uri packageURI, final IPackageInstallObserver observer, final int flags, final String installerPackageName) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INSTALL_PACKAGES, null); Message msg = mHandler.obtainMessage(INIT_COPY); msg.obj = new InstallParams(packageURI, observer, flags, installerPackageName); mHandler.sendMessage(msg); }

public void installPackage(final Uri packageURI,final IPackageInstallObserver observer,final int flags,final String installerPackageName){
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES,null);
        Message msg=mHandler.obtainMessage(INIT_COPY);
        msg.obj=new InstallParams(packageURI,observer,flags,installerPackageName);
        mHandler.sendMessage(msg);
}
这里通过PackageHandler的实例mhandler.sendMessage(msg)把信息发给继承Handler的类PackageHandler的HandleMessage()方法

在HandleMessage方法中调用doHandleMessage方法,doHandleMessage中使用了switch对传入的message进行判断,在其中会调用抽象类HandlerParams中的一个startCopy()方法,在startCopy()中会调用handleReturnCode()方法,这个方法复写了两次其中有一次是删除时要调用的,只列出安装调用的一个方法,在它里面调用了processPendingInstall方法,processPendingInstall的run方法中会调用installPacakgeLI,最后判断如果以前不存在那么调用installNewPackageLI(),这时候你会惊奇的发现,在installNewPackageLI方法中调用了PMS的scanPackageLI,这就回到了上面说的;

整个调用链:

下载完成自动调用->PackageManager.installPackage()->packageHandler.sendMessage(msg)将信息发送->PackageHandler.handlerMessage()->doHandlerMessage()->HandlerParams.startCopy()->handlerReturnCode()->ProcessPendingInstall()->installPackgeLI()->installerNewPackageLI()->scanPackageLI();

(3)从ADB工具安装:

其入口函数源文件为pm.java,安装时候会调用其 runInstall()方法

private int runInstall() throws RemoteException {
    long startedTime = SystemClock.elapsedRealtime();
    final InstallParams params = makeInstallParams();
    final String inPath = nextArg();
    if (params.sessionParams.sizeBytes == -1 && !STDIN_PATH.equals(inPath)) {
        File file = new File(inPath);
        if (file.isFile()) {
            try {
                ApkLite baseApk = PackageParser.parseApkLite(file, 0);
                PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null,
                        null, null);
                params.sessionParams.setSize(
                        PackageHelper.calculateInstalledSize(pkgLite, false,
                        params.sessionParams.abiOverride));
            } catch (PackageParserException | IOException e) {
                System.err.println("Error: Failed to parse APK file: " + e);
                return 1;
            }
        } else {
            System.err.println("Error: Can't open non-file: " + inPath);
            return 1;
        }
    }

    final int sessionId = doCreateSession(params.sessionParams,
            params.installerPackageName, params.userId);

    try {
        if (inPath == null && params.sessionParams.sizeBytes == -1) {
            System.err.println("Error: must either specify a package size or an APK file");
            return 1;
        }
        if (doWriteSession(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk",
                false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) {
            return 1;
        }
        Pair<String, Integer> status = doCommitSession(sessionId, false /*logSuccess*/);
        if (status.second != PackageInstaller.STATUS_SUCCESS) {
            return 1;
        }
        Log.i(TAG, "Package " + status.first + " installed in " + (SystemClock.elapsedRealtime()
                - startedTime) + " ms");
        System.out.println("Success");
        return 0;
    } finally {
        try {
            mInstaller.abandonSession(sessionId);
        } catch (Exception ignore) {
        }
    }
}
其中
IPackageManager mPm;
mPm = IpackageManager.Stub.asInterface(ServiceManager.getService("package"));

Stub是接口IPackageManage的静态抽象类,asInterface是返回IPackageManager代理的静态方法。

因为class PackageManagerService extends IPackageManager.Stub

所以mPm.installPackage 调用网络入口下载的installPackage方法;

(4)从sd卡中安装

系统调用PackageInstallerActivity.java,进入这个Activity会判断信息是否有错,然后调用initiateinstaller方法去判断是否曾经有过同名包或者包已经安装过,通过后执行private void startInstallConfirm() 点击OK按钮后经过一系列的安装信息的判断Intent跳转到InstallAppProcess这个活动,在其中调用了initView方法,方法再次调用安装接口完成安装。

讲到这里,我想我们可以回答一个android的一个面试题了:

Dalvik和art虚拟机有什么区别?

首先我们看看Dalvik和JVM有什么区别:

(1)Dalvik虚拟机支持的是.dex文件,而JVM支持的是.class文件

(2)Dalvik是基于寄存器的,而JVM是基于栈的。

(3)相比之下,Dalvik虚拟机占用更少的空间;

(4)Dalvik常量池只采用32位索引;

(5)标准Java字节码实行8位堆栈指令,Dalvik使用16位指令集直接作用于局部变量。局部变量通常来自4位的“虚拟寄存器”区。这样减少了Dalvik的指令计数,提高了翻译速度。

在来看看上面是art:

即Android Runtime
ART 的机制与 Dalvik 不同。在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率,而在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。这个过程叫做预编译(AOT,Ahead-Of-Time)。这样的话,应用的启动(首次)和执行都会变得更加快速。
ART有什么优缺点呢?
优点:
1、系统性能的显著提升。
2、应用启动更快、运行更快、体验更流畅、触感反馈更及时。
3、更长的电池续航能力。
4、支持更低的硬件。
缺点:
1.机器码占用的存储空间更大,字节码变为机器码之后,可能会增加10%-20%(不过在应用包中,可执行的代码常常只是一部分。比如最新的 Google+ APK 是 28.3 MB,但是代码只有 6.9 MB。)
2.应用的安装时间会变长。


另外关于Activity的加载流程和Android系统的启动流程的文章有两篇,可以看看;
Android系统的启动流程
Activity的加载流程

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android面试题(32)-android编译过程和安装流程 的相关文章

随机推荐

  • [转]vue3中使用vueQuill富文本编辑器详细教程,图片上传-图片压缩

    vueQuill是支持vue3的富文本编辑器组件 使用简单方便 官方网址 https vueup github io vue quill 效果图 1 安装 在官网有详细的安装教程 npm或者yran下载 npm install vueup
  • k8s 以statefulset方式部署zookeeper集群

    k8s 以statefulset方式部署zookeeper集群 参考 k8s官网zookeeper集群的部署 数据挂着方式改成通过本地方式创建的pv https kubernetes io docs tutorials stateful a
  • [深度解剖C语言] --关键字 static

    static 最名不副实的关键字 目录 1 static修饰全局变量 2 static修饰函数 3 static修饰局部变量 static的作用 1 static修饰全局变量 我们创建两个源文件 一个test c 一个main c 现在我们
  • 【日常问题记录】visual studio 出现 E0266 “data“ 不明确 错误

    解决方法 定义的变量和内部变量冲突 将变量的把名字改成其他名字即可
  • 【Kubernetes】mac 安装minikube

    1 一 官网安装 官网 https minikube sigs k8s io 搜了下网上如何通过Mac 安装看网上文章很多 但是受限于国内网络环境 很多都不行 各种下载失败 如果你有VPN 那么官方的步骤就容易走通 brew install
  • 小酌Django3——HTTP/URL/ORM简述

    小酌Django3 HTTP URL ORM简述 HTTP Hyper Text Transfer Protocol 超文本传输协议 是客户端与服务端请求和应答的标准 TCP 封装了Web服务的整个过程 通常 由客户端发起 建立到服务器指定
  • 矩阵基本知识

    1 identity matrix 单位矩阵 除了正对角线上是1 其它地方都是0 2 square matrix 方阵 行数和列数都相等的矩阵 3 diagonal matrix 对角矩阵 只在正对角线上有值 其它地方为0 4 matrix
  • js获取昨天,今天,明天,上周,本周,下周,上月,本月,下月,去年,本年,明年,上季度,本季度,下季度,向前推日期等

    配合的elementul的日期选择器使用的 参数是这样的 1代表 上 0代表 本 1代表 下 本月 1号到今天 意思就是比如今天是10月27号 那点击这个获取的是10月1号到10月27号得 效果如图 按顺序点了一遍 从左到右 上代码
  • 双十一前4小时,CentOS 6.5server启动错误排查

    11月10日晚上8点多 眼看要到双十一了 但我要说的这段经历却和双十一毫无关系 哈哈 这天准备向CentOS6 5server的svn上传一些文件 结果开机启动时 却出现了以下的界面 这是肿么回事 依据屏幕的提示 先使用root账号登录到系
  • JSP page指令errorPage属性起什么作用呢?

    转自 JSP page指令errorPage属性起什么作用呢 下文讲述JSP中page指令的errorPage功能简介说明 如下所示 errorPage功能 errorPage 属性用于设置JSP页面 当出现异常时的跳转页面 注意事项 er
  • STM32G431控制窗帘电机-窗帘电机拆机

    前言 最近做了一个电动窗帘的控制工程 简单点说就是实现电动窗帘的校准模式 窗帘移动到任意位置 说明 本文档为原创 转载请注明出处 要进行商业合作请联系本人1151313194 qq com 一 电机爆炸图拆解 窗帘电机主要时由三部分构成 电
  • Docker的隔离机制

    Docker的隔离性主要运用Namespace 技术 传统上Linux中的PID是唯一且独立的 在正常情况下 用户不会看见重复的PID 然而在Docker采用了Namespace 从而令相同的PID可于不同的Namespace中独立存在 如
  • 自动生成GFM目录

    自动生成GFM目录 自动生成GFM目录 参考文献 引言 使用方法 对于本地文档 对于远程文档 对于多个文档 本地与远程结合 自动插入和更新目录 输出目录文件 工作流程 目前工作流程注意事项及问题 参考文献 https github com
  • Go语言--Nohup后台启动

    linux的nohup命令用法 在应用Unix Linux时 我们一般想让某个程序在后台运行 于是我们将常会用 在程序结尾来让程序自动运行 nohup命令格式 nohup root start sh 在shell中回车后提示 appendi
  • HTML5(六)preserve and recover

    HTML5 六 preserve and recover 1 save the state of canvas context 2 So we have two method to save and restore the state of
  • run nerf

    install cuda install cudnn install anoconda 2022 08 18 wget https repo anaconda com archive Anaconda3 2022 05 Linux x86
  • 接口自动化get请求一些小知识点

    get 请求 requests get get请求的参数可以写在url里 写在url里时 参数和url之间需要加 get 请求的参数可以不写在url里 可以组织为字典传给参数param get 请求可以不传参数 获取响应信息 r reque
  • React之Hook(四)——使用 Effect Hook

    Effect Hook 可以让你在函数组件中执行副作用操作 import React useState useEffect from react function Example const count setCount useState
  • Doris数据划分

    数据划分 基本概念 Row Column 一张表包含行 Row 和列 Column Column可以分为两大类 Key和Value Tablet Partition 在Doris的存储引擎中 用户数据被水平划分为若干个数据分片 Tablet
  • Android面试题(32)-android编译过程和安装流程

    android app的编译过程 从网上拷了一张图 这张图很好的讲述了android的编译打包过程 接下来就具体的分析分析 大致分为7步 1 aapt Android Asset Packaging Tool android构建工具 在an