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

app启动流程

文章目录

        • 0.开始
        • 1.ActivityTaskManagerService.startProcessAsync
        • 2.ActivityManagerService.startProcess
        • 3.ActivityManagerService.startProcessLocked
        • 4.ProcessList.startProcessLocked
        • 5.ProcessList.handleProcessStart
        • 6.ProcessList.startProcess
        • 7.ZygoteProcess.start
        • 8.ZygoteProcess.startViaZygote
        • 9.ZygoteProcess.zygoteSendArgsAndGetResult
        • 10.ZygoteProcess.attemptZygoteSendArgsAndGetResult

0.开始

Activity启动流程可以看到第12.ActivityTaskSupervisor.startSpecificActivity() 有启动进程的方法,下面开始进行分析

 // frameworks/base/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
final ActivityTaskManagerService mService;
void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) {
    ...
    final boolean isTop = andResume && r.isTopRunningActivity();
    // knownToBeDead = false,以新启动app为例,isTop = false
    mService.startProcessAsync(r, knownToBeDead, isTop, isTop ? "top-activity" : "activity");
}

启动进程流程图:
在这里插入图片描述

UDS流程图:

1.ActivityTaskManagerService.startProcessAsync

通过handler发送了消息,执行了ActivityManagerInternal::startProcess

// frameworks\base\services\core\java\com\android\server\wm\ActivityTaskManagerService.java
void startProcessAsync(ActivityRecord activity, boolean knownToBeDead, boolean isTop, String hostingType) {
    // Post message to start process to avoid possible deadlock of calling into AMS with theATMS lock held.
   final Message m = PooledLambda.obtainMessage(ActivityManagerInternal::startProcess,
           mAmInternal, activity.processName, activity.info.applicationInfo, knownToBeDead,
           isTop, hostingType, activity.intent.getComponent());
   mH.sendMessage(m);
}

2.ActivityManagerService.startProcess

ActivityManagerInternal是一个抽象类,ActivityManagerService内部类LocalService继承了它,调用了startProcessLocked

// frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java
public void startProcess(String processName, ApplicationInfo info, boolean knownToBeDead,
        boolean isTop, String hostingType, ComponentName hostingName) {
    try {
        synchronized (ActivityManagerService.this) {
            // If the process is known as top app, set a hint so when the process is
            // started, the top priority can be applied immediately to avoid cpu being
            // preempted by other processes before attaching the process of top app.
            startProcessLocked(processName, info, knownToBeDead, 0 /* intentFlags */,
                    new HostingRecord(hostingType, hostingName, isTop),
                    ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE, false /* allowWhileBooting */,
                    false /* isolated */);
        }
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    }
}

3.ActivityManagerService.startProcessLocked

// frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java
/**
 * Process management.
 */
final ProcessList mProcessList

final ProcessRecord startProcessLocked(String processName,
    ApplicationInfo info, boolean knownToBeDead, int intentFlags,
    HostingRecord hostingRecord, int zygotePolicyFlags, boolean allowWhileBooting,
    boolean isolated) {
    return mProcessList.startProcessLocked(processName, info, knownToBeDead, intentFlags,
         hostingRecord, zygotePolicyFlags, allowWhileBooting, isolated, 0 /* isolatedUid */,
         false /* isSdkSandbox */, 0 /* sdkSandboxClientAppUid */,
         null /* sdkSandboxClientAppPackage */,
         null /* ABI override */, null /* entryPoint */,
         null /* entryPointArgs */, null /* crashHandler */);
}

4.ProcessList.startProcessLocked

前面做了一些检查处理,有是否为isolated process、进程是否已启动、系统是否已启动等检查,最后调用了startProcessLocked重载方法。startProcessLocked()方法有几个重载

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MDZo05f-1664106555969)(assets/android/system/ProcessList_startProcessLocked.png)]​

// frameworks\base\services\core\java\com\android\server\am\ProcessList.java
ProcessRecord startProcessLocked(String processName, ApplicationInfo info,
        boolean knownToBeDead, int intentFlags, HostingRecord hostingRecord,
        int zygotePolicyFlags, boolean allowWhileBooting, boolean isolated, int isolatedUid,
        boolean isSdkSandbox, int sdkSandboxUid, String sdkSandboxClientAppPackage,
        String abiOverride, String entryPoint, String[] entryPointArgs, Runnable crashHandler) {
    long startTime = SystemClock.uptimeMillis();
    ProcessRecord app;
    // 前面一些检查逻辑
    // 判断isolated,是否为isolated process

    // We don't have to do anything more if:
    // (1) There is an existing application record; and
    // (2) The caller doesn't think it is dead, OR there is no thread
    //     object attached to it so we know it couldn't have crashed; and
    // (3) There is a pid assigned to it, so it is either starting or
    //     already running.

    // If the system is not ready yet, then hold off on starting this
    // process until it is.
   
    final boolean success =
            startProcessLocked(app, hostingRecord, zygotePolicyFlags, abiOverride);
    checkSlow(startTime, "startProcess: done starting proc!");
    return success ? app : null;
}

/**
 * @return {@code true} if process start is successful, false otherwise.
 */
boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
        int zygotePolicyFlags, boolean disableHiddenApiChecks, boolean disableTestApiChecks,
        String abiOverride) {
    if (app.isPendingStart()) {
        return true;
    }
    if (app.getPid() > 0 && app.getPid() != ActivityManagerService.MY_PID) {
        mService.removePidLocked(app.getPid(), app);
        app.setBindMountPending(false);
        mService.mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
        app.setPid(0);
        app.setStartSeq(0);
    }
    // Clear any residual death recipient link as the ProcessRecord could be reused.
    app.unlinkDeathRecipient();
    app.setDyingPid(0);
    mService.mProcessesOnHold.remove(app);
    mService.updateCpuStats();
    try {
        final int userId = UserHandle.getUserId(app.uid);
        try {
            AppGlobals.getPackageManager().checkPackageStartable(app.info.packageName, userId);
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
        int uid = app.uid;
        int[] gids = null;
        int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;
        // packageMananger 分区挂载处理
        // 进程的一些标志参数处理

        String instructionSet = null;
        if (app.info.primaryCpuAbi != null) {
            // If ABI override is specified, use the isa derived from the value of ABI override.
            // Otherwise, use the isa derived from primary ABI
            instructionSet = VMRuntime.getInstructionSet(requiredAbi);
        }
        app.setGids(gids);
        app.setRequiredAbi(requiredAbi);
        app.setInstructionSet(instructionSet);
        // If this was an external service, the package name and uid in the passed in
        // ApplicationInfo have been changed to match those of the calling package;
        // that will incorrectly apply compat feature overrides for the calling package instead
        // of the defining one.
        ApplicationInfo definingAppInfo;
        if (hostingRecord.getDefiningPackageName() != null) {
            definingAppInfo = new ApplicationInfo(app.info);
            definingAppInfo.packageName = hostingRecord.getDefiningPackageName();
            definingAppInfo.uid = uid;
        } else {
            definingAppInfo = app.info;
        }
        // Start the process.  It will either succeed and return a result containing
        // the PID of the new process, or else throw a RuntimeException.
        final String entryPoint = "android.app.ActivityThread";
        return startProcessLocked(hostingRecord, entryPoint, app, uid, gids,
                runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, requiredAbi,
                instructionSet, invokeWith, startUptime, startElapsedTime);
    } catch (RuntimeException e) {
        ...
        return false;
    }
}

boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, ProcessRecord app,
        int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags, int mountExternal,
        String seInfo, String requiredAbi, String instructionSet, String invokeWith,
        long startUptime, long startElapsedTime) {
    app.setPendingStart(true);
    app.setRemoved(false);
    synchronized (mProcLock) {
        app.setKilledByAm(false);
        app.setKilled(false);
    }
  
    app.setDisabledCompatChanges(null);
    if (mPlatformCompat != null) {
        app.setDisabledCompatChanges(mPlatformCompat.getDisabledChanges(app.info));
    }
    final long startSeq = ++mProcStartSeqCounter;
    app.setStartSeq(startSeq);
    app.setStartParams(uid, hostingRecord, seInfo, startUptime, startElapsedTime);
    app.setUsingWrapper(invokeWith != null
            || Zygote.getWrapProperty(app.processName) != null);
    mPendingStarts.put(startSeq, app);
    // 默认异步启动
    if (mService.mConstants.FLAG_PROCESS_START_ASYNC) {
        mService.mProcStartHandler.post(() -> handleProcessStart(
                app, entryPoint, gids, runtimeFlags, zygotePolicyFlags, mountExternal,
                requiredAbi, instructionSet, invokeWith, startSeq));
        return true;
    } else {  }
}

5.ProcessList.handleProcessStart

// frameworks\base\services\core\java\com\android\server\am\ProcessList.java
/**
 * Main handler routine to start the given process from the ProcStartHandler.
 *
 * <p>Note: this function doesn't hold the global AM lock intentionally.</p>
 */
private void handleProcessStart(final ProcessRecord app, final String entryPoint,
        final int[] gids, final int runtimeFlags, int zygotePolicyFlags,
        final int mountExternal, final String requiredAbi, final String instructionSet,
        final String invokeWith, final long startSeq) {
    final Runnable startRunnable = () -> {
        try {
            final Process.ProcessStartResult startResult = startProcess(app.getHostingRecord(),
                    entryPoint, app, app.getStartUid(), gids, runtimeFlags, zygotePolicyFlags,
                    mountExternal, app.getSeInfo(), requiredAbi, instructionSet, invokeWith,
                    app.getStartTime());
            synchronized (mService) {
                handleProcessStartedLocked(app, startResult, startSeq);
            }
        } catch (RuntimeException e) {
            synchronized (mService) {
                Slog.e(ActivityManagerService.TAG, "Failure starting process "
                        + app.processName, e);
                mPendingStarts.remove(startSeq);
                app.setPendingStart(false);
                mService.forceStopPackageLocked(app.info.packageName,
                        UserHandle.getAppId(app.uid),
                        false, false, true, false, false, app.userId, "start failure");
            }
        }
    };
    // Use local reference since we are not using locks here
    final ProcessRecord predecessor = app.mPredecessor;
    if (predecessor != null && predecessor.getDyingPid() > 0) {
        handleProcessStartWithPredecessor(predecessor, startRunnable);
    } else {
        // Kick off the process start for real.
        startRunnable.run();
    }
}

6.ProcessList.startProcess

startProcess()做了sharedUid,APP存储分区挂载等一些检查处理,接下来就是判断进程类型,由谁孵化。可以知道一般情况是由appZygote进行创建进程,appZygote.getProcess()的是ChildZygoteProcess,继承于ZygoteProcess

// frameworks\base\services\core\java\com\android\server\am\ProcessList.java
private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, String entryPoint,
        ProcessRecord app, int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags,
        int mountExternal, String seInfo, String requiredAbi, String instructionSet,
        String invokeWith, long startTime) {
            final Process.ProcessStartResult startResult;
            boolean regularZygote = false;
            if (hostingRecord.usesWebviewZygote()) {
              
            } else if (hostingRecord.usesAppZygote()) {
                final AppZygote appZygote = createAppZygoteForProcessIfNeeded(app);
                // We can't isolate app data and storage data as parent zygote already did that.
                startResult = appZygote.getProcess().start(entryPoint,
                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                        app.info.dataDir, null, app.info.packageName,
                        /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp,
                        app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap,
                        false, false,
                        new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
            } else {
              
            }
            if (!regularZygote) {
                // webview and app zygote don't have the permission to create the nodes
                if (Process.createProcessGroup(uid, startResult.pid) < 0) {
                    throw new AssertionError("Unable to create process group for " + app.processName
                            + " (" + startResult.pid + ")");
                }
            }
            // This runs after Process.start() as this method may block app process starting time
            // if dir is not cached. Running this method after Process.start() can make it
            // cache the dir asynchronously, so zygote can use it without waiting for it.
            if (bindMountAppStorageDirs) {
                storageManagerInternal.prepareStorageDirs(userId, pkgDataInfoMap.keySet(),
                        app.processName);
            }
            return startResult;
}

7.ZygoteProcess.start

// frameworks\base\core\java\android\os\ZygoteProcess.java
/**
 * Start a new process.
 *
 * <p>If processes are enabled, a new process is created and the
 * static main() function of a <var>processClass</var> is executed there.
 * The process will continue running after this function returns.
 *
 * <p>If processes are not enabled, a new thread in the caller's
 * process is created and main() of <var>processclass</var> called there.
 *
 * <p>The niceName parameter, if not an empty string, is a custom name to
 * give to the process instead of using processClass.  This allows you to
 * make easily identifyable processes even if you are using the same base
 * <var>processClass</var> to start them.
 *
 * When invokeWith is not null, the process will be started as a fresh app
 * and not a zygote fork. Note that this is only allowed for uid 0 or when
 * runtimeFlags contains DEBUG_ENABLE_DEBUGGER.
 *
 * @param processClass The class to use as the process's main entry
 *                     point.
 * @param niceName A more readable name to use for the process.
 * @param uid The user-id under which the process will run.
 * @param gid The group-id under which the process will run.
 * @param gids Additional group-ids associated with the process.
 * @param runtimeFlags Additional flags.
 * @param targetSdkVersion The target SDK version for the app.
 * @param seInfo null-ok SELinux information for the new process.
 * @param abi non-null the ABI this app should be started with.
 * @param instructionSet null-ok the instruction set to use.
 * @param appDataDir null-ok the data directory of the app.
 * @param invokeWith null-ok the command to invoke with.
 * @param packageName null-ok the name of the package this process belongs to.
 * @param zygotePolicyFlags Flags used to determine how to launch the application.
 * @param isTopApp Whether the process starts for high priority application.
 * @param disabledCompatChanges null-ok list of disabled compat changes for the process being
 *                             started.
 * @param pkgDataInfoMap Map from related package names to private data directory
 *                       volume UUID and inode number.
 * @param allowlistedDataInfoList Map from allowlisted package names to private data directory
 *                       volume UUID and inode number.
 * @param bindMountAppsData whether zygote needs to mount CE and DE data.
 * @param bindMountAppStorageDirs whether zygote needs to mount Android/obb and Android/data.
 *
 * @param zygoteArgs Additional arguments to supply to the Zygote process.
 * @return An object that describes the result of the attempt to start the process.
 * @throws RuntimeException on fatal start failure
 */
public final Process.ProcessStartResult start(@NonNull final String processClass,
                                              final String niceName,
                                              int uid, int gid, @Nullable int[] gids,
                                              int runtimeFlags, int mountExternal,
                                              int targetSdkVersion,
                                              @Nullable String seInfo,
                                              @NonNull String abi,
                                              @Nullable String instructionSet,
                                              @Nullable String appDataDir,
                                              @Nullable String invokeWith,
                                              @Nullable String packageName,
                                              int zygotePolicyFlags,
                                              boolean isTopApp,
                                              @Nullable long[] disabledCompatChanges,
                                              @Nullable Map<String, Pair<String, Long>>
                                                      pkgDataInfoMap,
                                              @Nullable Map<String, Pair<String, Long>>
                                                      allowlistedDataInfoList,
                                              boolean bindMountAppsData,
                                              boolean bindMountAppStorageDirs,
                                              @Nullable String[] zygoteArgs) {
    try {
        return startViaZygote(processClass, niceName, uid, gid, gids,
                runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                abi, instructionSet, appDataDir, invokeWith, /*startChildZygote=*/ false,
                packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges,
                pkgDataInfoMap, allowlistedDataInfoList, bindMountAppsData,
                bindMountAppStorageDirs, zygoteArgs);
    } catch (ZygoteStartFailedEx ex) {}
}

8.ZygoteProcess.startViaZygote

添加进程启动的各种参数后,调用了zygoteSendArgsAndGetResult

// frameworks\base\core\java\android\os\ZygoteProcess.java
private Process.ProcessStartResult startViaZygote(@NonNull final String processClass,
         @Nullable final String niceName,
         final int uid, final int gid,
         @Nullable final int[] gids,
         int runtimeFlags, int mountExternal,
         int targetSdkVersion,
         @Nullable String seInfo,
         @NonNull String abi,
         @Nullable String instructionSet,
         @Nullable String appDataDir,
         @Nullable String invokeWith,
         boolean startChildZygote,
         @Nullable String packageName,
         int zygotePolicyFlags,
         boolean isTopApp,
         @Nullable long[] disabledCompatChanges,
         @Nullable Map<String, Pair<String, Long>>
                 pkgDataInfoMap,
         @Nullable Map<String, Pair<String, Long>>
                 allowlistedDataInfoList,
         boolean bindMountAppsData,
         boolean bindMountAppStorageDirs,
         @Nullable String[] extraArgs)
         throws ZygoteStartFailedEx {
    ArrayList<String> argsForZygote = new ArrayList<>();
    // --runtime-args, --setuid=, --setgid=,
    // and --setgroups= must go first
    argsForZygote.add("--runtime-args");
    argsForZygote.add("--setuid=" + uid);
    argsForZygote.add("--setgid=" + gid);
    argsForZygote.add("--runtime-flags=" + runtimeFlags);
    // 添加各种参数信息
    synchronized(mLock) {
        // The USAP pool can not be used if the application will not use the systems graphics
        // driver.  If that driver is requested use the Zygote application start path.
        return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),
                                          zygotePolicyFlags,
                                          argsForZygote);
    }
}

9.ZygoteProcess.zygoteSendArgsAndGetResult

// frameworks\base\core\java\android\os\ZygoteProcess.java 
/**
 * Sends an argument list to the zygote process, which starts a new child
 * and returns the child's pid. Please note: the present implementation
 * replaces newlines in the argument list with spaces.
 *
 * @throws ZygoteStartFailedEx if process start failed for any reason
 */
private Process.ProcessStartResult zygoteSendArgsAndGetResult(
        ZygoteState zygoteState, int zygotePolicyFlags, @NonNull ArrayList<String> args)
        throws ZygoteStartFailedEx {
    // Throw early if any of the arguments are malformed. This means we can
    // avoid writing a partial response to the zygote.
    for (String arg : args) {
        // Making two indexOf calls here is faster than running a manually fused loop due
        // to the fact that indexOf is an optimized intrinsic.
        if (arg.indexOf('\n') >= 0) {
            throw new ZygoteStartFailedEx("Embedded newlines not allowed");
        } else if (arg.indexOf('\r') >= 0) {
            throw new ZygoteStartFailedEx("Embedded carriage returns not allowed");
        }
    }
    /*
     * See com.android.internal.os.ZygoteArguments.parseArgs()
     * Presently the wire format to the zygote process is:
     * a) a count of arguments (argc, in essence)
     * b) a number of newline-separated argument strings equal to count
     *
     * After the zygote process reads these it will write the pid of
     * the child or -1 on failure, followed by boolean to
     * indicate whether a wrapper process was used.
     */
    String msgStr = args.size() + "\n" + String.join("\n", args) + "\n";
    if (shouldAttemptUsapLaunch(zygotePolicyFlags, args)) {
        try {
            return attemptUsapSendArgsAndGetResult(zygoteState, msgStr);
        } catch (IOException ex) {
            // If there was an IOException using the USAP pool we will log the error and
            // attempt to start the process through the Zygote.
            Log.e(LOG_TAG, "IO Exception while communicating with USAP pool - "
                    + ex.getMessage());
        }
    }
    return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr);
}

10.ZygoteProcess.attemptZygoteSendArgsAndGetResult

看到这,发现只进行了数据流的写入和读取,并没有创建进程啊?其实这里使用UDS机制,后面继续深究

// frameworks\base\core\java\android\os\ZygoteProcess.java 
private Process.ProcessStartResult attemptZygoteSendArgsAndGetResult(
        ZygoteState zygoteState, String msgStr) throws ZygoteStartFailedEx {
    try {
        final BufferedWriter zygoteWriter = zygoteState.mZygoteOutputWriter;
        final DataInputStream zygoteInputStream = zygoteState.mZygoteInputStream;
        zygoteWriter.write(msgStr);
        zygoteWriter.flush();
        // Always read the entire result from the input stream to avoid leaving
        // bytes in the stream for future process starts to accidentally stumble
        // upon.
        Process.ProcessStartResult result = new Process.ProcessStartResult();
        result.pid = zygoteInputStream.readInt();
        result.usingWrapper = zygoteInputStream.readBoolean();
        if (result.pid < 0) {
            throw new ZygoteStartFailedEx("fork() failed");
        }
        return result;
    } catch (IOException ex) {
        zygoteState.close();
        Log.e(LOG_TAG, "IO Exception while communicating with Zygote - "
                + ex.toString());
        throw new ZygoteStartFailedEx(ex);
    }
}

相关文章:

  • 程序员的民宿情结
  • PD 重要监控指标详解
  • 数字集成电路(中)
  • 为什么Spring中的bean默认都是单例模式?
  • 【日常需求】一次使用EasyExcel而引发的问题与思考~
  • Docker 镜像拉取
  • Android 12 蓝牙打开
  • Linux常用基本命令详解(一)
  • 逻辑漏洞——业务逻辑问题
  • C++-vector的代码实现(超详细)
  • Linux之Platform设备驱动
  • Linux 入门篇
  • Linux驱动开发:字符设备驱动开发实战
  • 一、k8s的安装部署
  • VB.net:VB.net编程语言学习之ADO.net基本名称空间与类的简介、案例应用(实现与SQL数据库编程案例)之详细攻略
  • 《用数据讲故事》作者Cole N. Knaflic:消除一切无效的图表
  • Akka系列(七):Actor持久化之Akka persistence
  • canvas 五子棋游戏
  • git 常用命令
  • go语言学习初探(一)
  • JavaScript函数式编程(一)
  • Java到底能干嘛?
  • JAVA多线程机制解析-volatilesynchronized
  • leetcode46 Permutation 排列组合
  • Linux gpio口使用方法
  • PAT A1120
  • python_bomb----数据类型总结
  • QQ浏览器x5内核的兼容性问题
  • Storybook 5.0正式发布:有史以来变化最大的版本\n
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 服务器从安装到部署全过程(二)
  • 区块链将重新定义世界
  • 说说动画卡顿的解决方案
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 推荐一个React的管理后台框架
  • 学习HTTP相关知识笔记
  • Unity3D - 异步加载游戏场景与异步加载游戏资源进度条 ...
  • #includecmath
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (2)关于RabbitMq 的 Topic Exchange 主题交换机
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (C语言)输入自定义个数的整数,打印出最大值和最小值
  • (LeetCode) T14. Longest Common Prefix
  • (rabbitmq的高级特性)消息可靠性
  • (附源码)springboot教学评价 毕业设计 641310
  • (算法)Game
  • (一)Mocha源码阅读: 项目结构及命令行启动
  • (一)WLAN定义和基本架构转
  • (轉貼)《OOD启思录》:61条面向对象设计的经验原则 (OO)
  • .net core控制台应用程序初识
  • .net oracle 连接超时_Mysql连接数据库异常汇总【必收藏】
  • .NET 线程 Thread 进程 Process、线程池 pool、Invoke、begininvoke、异步回调
  • .NetCore部署微服务(二)
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • .NET实现之(自动更新)