• Android 开机动画的启动


    开机动画位置:device\xxx\common\logo\bootanimation\bootanimation.zip

     desc.txt用于描述动画如何显示

    文件格式如下:
    WIDTH HEIGHT FPS
    //WIDTH 图片宽度(px)
    //HEIGHT 图片高度(px)
    //FPS:每秒帧数
     
    TYPE COUNT PAUSE PATH
    //TYPE:动画类型,p:如果系统启动完毕,会中断播放。默认;

                                     c:一直播放完毕,无论系统有没启动完毕
    //COUNT:播放次数,0-无限循环
    //PAUSE:本组播放完毕后,停留的帧数。

     第一行的三个数字分别表示开机动画在屏幕中的显示宽度、高度以及帧速(fps)。

    剩余的每一行都用来描述一个动画片断,这些行必须要以指定字符开头,后面紧跟着两个数字以及一个文件目录路径名称。第一个数字表示一个片断的循环显示次数,如果它的值等于0,那么就表示无限循环地显示该动画片断。第二个数字表示每一个片断在两次循环显示之间的时间间隔。这个时间间隔是以一个帧的时间为单位的。文件目录下面保存的是一系列png文件,这些png文件会被依次显示在屏幕中 

    动画的Start和stop控制:

    动画的开始与结束是由属性控制的,由/system/bin/surfaceflinger来控制,然后相关的动画处理程序为/system/bin/bootanimation,在init.rc中指定。

    bootanimation 需要 由property_set(“ctl.start”, “bootanim”);来启动进程,

                                    由property_set(“ctl.stop”, “bootanim”);来关掉进程。

    ”service.bootanim.exit”:这个属性在bootanimation进程里会周期检查,=1时就退出动画,=0表示要播放动画。

    主要过程:SurfaceFlinger 服务启动的过程中会修改系统属性"ctl.start"的值,以通知init进程启动bootanim来显示开机动画。当系统关键服务启动完毕后,由AMS通知SurfaceFlinger修改系统属性"ctl.stop"来通知init进程停止执行bootanim关闭动画。

    bootanimation

    开机动画是由应用程序bootanimation来负责显示的,先看一下其rc文件。

    frameworks\base\cmds\bootanimation\bootanim.rc 

    1. service bootanim /system/bin/bootanimation
    2. class core animation
    3. user graphics
    4. group graphics audio
    5. disabled //系统启动时,不会自动启动bootanimation
    6. oneshot //只启动一次
    7. ioprio rt 0
    8. task_profiles MaxPerformance

    surfaceflinger

    frameworks\native\services\surfaceflinger\surfaceflinger.rc

    1. service surfaceflinger /system/bin/surfaceflinger
    2. class core animation
    3. user system
    4. group graphics drmrpc readproc
    5. capabilities SYS_NICE
    6. onrestart restart zygote
    7. task_profiles HighPerformance
    8. socket pdx/system/vr/display/client stream 0666 system graphics u:object_r:pdx_display_client_endpoint_socket:s0
    9. socket pdx/system/vr/display/manager stream 0666 system graphics u:object_r:pdx_display_manager_endpoint_socket:s0
    10. socket pdx/system/vr/display/vsync stream 0666 system graphics u:object_r:pdx_display_vsync_endpoint_socket:s0

    surfaceflinger的启动时机

    在高版本的Android上,如AndroidP,surfaceflinger进程并不是直接在init.rc文件中启动的,而是通过Android.bp文件去包含启动surfaceflinger.rc文件,然后在该文件中再去启动surfaceflinger:

    frameworks\native\services\surfaceflinger\Android.bp

    1. cc_binary {
    2. name: "surfaceflinger",
    3. defaults: ["libsurfaceflinger_binary"],
    4. init_rc: ["surfaceflinger.rc"],
    5. srcs: [
    6. ":surfaceflinger_binary_sources",
    7. // Note: SurfaceFlingerFactory is not in the filegroup so that it
    8. // can be easily replaced.
    9. "SurfaceFlingerFactory.cpp",
    10. ],
    11. shared_libs: [
    12. "libSurfaceFlingerProp",
    13. ],
    14. logtags: ["EventLog/EventLogTags.logtags"],
    15. }

    surfaceflinger启动了,就会跑到它的main函数:

    SurfaceFlinger服务的入口在main_surfaceflinger.cpp中

    frameworks\native\services\surfaceflinger\main_surfaceflinger.cpp

    1. int main(int, char**) {
    2. signal(SIGPIPE, SIG_IGN);
    3. hardware::configureRpcThreadpool(1 /* maxThreads */,
    4. false /* callerWillJoin */);
    5. startGraphicsAllocatorService();
    6. // When SF is launched in its own process, limit the number of
    7. // binder threads to 4.
    8. ProcessState::self()->setThreadPoolMaxThreadCount(4);
    9. // start the thread pool
    10. sp<ProcessState> ps(ProcessState::self());
    11. ps->startThreadPool();
    12. // instantiate surfaceflinger
    13. sp<SurfaceFlinger> flinger = surfaceflinger::createSurfaceFlinger();
    14. setpriority(PRIO_PROCESS, 0, PRIORITY_URGENT_DISPLAY);
    15. set_sched_policy(0, SP_FOREGROUND);
    16. // Put most SurfaceFlinger threads in the system-background cpuset
    17. // Keeps us from unnecessarily using big cores
    18. // Do this after the binder thread pool init
    19. if (cpusets_enabled()) set_cpuset_policy(0, SP_SYSTEM);
    20. // initialize before clients can connect
    21. flinger->init();
    22. // publish surface flinger
    23. sp<IServiceManager> sm(defaultServiceManager());
    24. sm->addService(String16(SurfaceFlinger::getServiceName()), flinger, false,
    25. IServiceManager::DUMP_FLAG_PRIORITY_CRITICAL | IServiceManager::DUMP_FLAG_PROTO);
    26. startDisplayService(); // dependency on SF getting registered above
    27. if (SurfaceFlinger::setSchedFifo(true) != NO_ERROR) {
    28. ALOGW("Couldn't set to SCHED_FIFO: %s", strerror(errno));
    29. }
    30. // run surface flinger in this thread
    31. flinger->run();
    32. return 0;
    33. }

    frameworks\native\services\surfaceflinger\SurfaceFlinger.cpp

    init方法中 start mStartPropertySetThread

    1. const bool presentFenceReliable =
    2. !getHwComposer().hasCapability(hal::Capability::PRESENT_FENCE_IS_NOT_RELIABLE);
    3. mStartPropertySetThread = getFactory().createStartPropertySetThread(presentFenceReliable);
    4. if (mStartPropertySetThread->Start() != NO_ERROR) {
    5. ALOGE("Run StartPropertySetThread failed!");
    6. }
    7. ALOGV("Done initializing");

    frameworks\native\services\surfaceflinger\SurfaceFlingerDefaultFactory.cpp

    1. sp DefaultFactory::createStartPropertySetThread(
    2. bool timestampPropertyValue) {
    3. return new StartPropertySetThread(timestampPropertyValue);
    4. }

    frameworks\native\services\surfaceflinger\StartPropertySetThread.cpp

    1. #include <cutils/properties.h>
    2. #include "StartPropertySetThread.h"
    3. namespace android {
    4. StartPropertySetThread::StartPropertySetThread(bool timestampPropertyValue):
    5. Thread(false), mTimestampPropertyValue(timestampPropertyValue) {}
    6. status_t StartPropertySetThread::Start() {
    7. return run("SurfaceFlinger::StartPropertySetThread", PRIORITY_NORMAL);
    8. }
    9. bool StartPropertySetThread::threadLoop() {
    10. // Set property service.sf.present_timestamp, consumer need check its readiness
    11. property_set(kTimestampProperty, mTimestampPropertyValue ? "1" : "0");
    12. // Clear BootAnimation exit flag
    13. property_set("service.bootanim.exit", "0");
    14. // Start BootAnimation if not started
    15. property_set("ctl.start", "bootanim");
    16. // Exit immediately
    17. return false;
    18. }
    19. } /

    这里设置属性【service.bootanim.exit】并采用【ctl.start】的方式启动开机动画:

    在这之后,开机动画就会启动,由bootanimation进程实现具体动画播放 

    bootAnimation的启动

    名称等于"bootanim"的服务所对应的应用程序为/system/bin/bootanimation,应用程序入口函数的实现在frameworks/base/cmds/bootanimation/bootanimation_main.cpp

    1. int main()
    2. {
    3. setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY);
    4. bool noBootAnimation = bootAnimationDisabled();
    5. ALOGI_IF(noBootAnimation, "boot animation disabled");
    6. if (!noBootAnimation) {
    7. sp<ProcessState> proc(ProcessState::self());
    8. ProcessState::self()->startThreadPool();
    9. // create the boot animation object (may take up to 200ms for 2MB zip)
    10. sp<BootAnimation> boot = new BootAnimation(audioplay::createAnimationCallbacks());
    11. waitForSurfaceFlinger();
    12. boot->run("BootAnimation", PRIORITY_DISPLAY);
    13. ALOGV("Boot animation set up. Joining pool.");
    14. IPCThreadState::self()->joinThreadPool();
    15. }
    16. return 0;
    17. }

    首先检查系统属性“debug.sf.nobootnimaition”的值是否等于0。如果不等于的话,那么接下来就会启动一个Binder线程池,并且创建一个BootAnimation对象。这个Binder线程用于同SurfaceFlinger服务通信。

    frameworks\base\cmds\bootanimation\BootAnimation.cpp

    BootAnimation类间接地继承了RefBase类,并且重写了RefBase类的成员函数onFirstRef,因此,当一个BootAnimation对象第一次被智能指针引用的时,这个BootAnimation对象的成员函数onFirstRef就会被调用。其中几个重要的函数说明如下:

    onFirstRef()—— 属于其父类RefBase,该函数在强引用sp新增引用计数時调用,就是当有sp包装的类初始化的时候调用;
    binderDied() ——当对象死掉或者其他情况导致该Binder结束时,就会回调binderDied()方法;
    readyToRun() ——Thread执行前的初始化工作;
    threadLoop() ——每个线程类都要实现的,在这里定义thread的执行内容。这个函数如果返回true,且没有调用requestExit(),则该函数会再次执行;如果返回false,则threadloop中的内容仅仅执行一次,线程就会退出。
    其他函数简述如下:

    android()——显示系统默认的开机画面;
    movie()——显示用户自定义的开机动画;
    loadAnimation(const String8&)——加载动画;
    playAnimation(const Animation&)——播放动画;
    checkExit()——检查是否退出动画;

    1. void BootAnimation::onFirstRef() {
    2. status_t err = mSession->linkToComposerDeath(this);
    3. SLOGE_IF(err, "linkToComposerDeath failed (%s) ", strerror(-err));
    4. if (err == NO_ERROR) {
    5. // Load the animation content -- this can be slow (eg 200ms)
    6. // called before waitForSurfaceFlinger() in main() to avoid wait
    7. ALOGD("%sAnimationPreloadTiming start time: %" PRId64 "ms",
    8. mShuttingDown ? "Shutdown" : "Boot", elapsedRealtime());
    9. preloadAnimation();
    10. ALOGD("%sAnimationPreloadStopTiming start time: %" PRId64 "ms",
    11. mShuttingDown ? "Shutdown" : "Boot", elapsedRealtime());
    12. }
    13. }

     mSession是BootAnimation类的一个成员变量,它的类型为SurfaceComposerClient,是用来和SurfaceFlinger执行Binder进程间通信的,它是在BootAnimation类的构造函数中创建的

    mSession = new SurfaceComposerClient();

    由于BootAnimation类引用了SurfaceFlinger服务,因此,当SurfaceFlinger服务意外死亡时,BootAnimation类就需要得到通知,这是通过调用成员变量mSession的成员函数linkToComposerDeath来注册SurfaceFlinger服务的死亡接收通知来实现的。
     
            BootAnimation类继承了Thread类,因此,当bootanimation_main.cpp调用了Thread的成员函数run之后,系统就会创建一个线程,这个线程在第一次运行之前,会调用BootAnimation类的成员函数readyToRun来执行一些初始化工作,后面再调用BootAnimation类的成员函数threadLoop来显示第三个开机画面。
     

    1. bool BootAnimation::threadLoop() {
    2. bool result;
    3. // We have no bootanimation file, so we use the stock android logo
    4. // animation.
    5. if (mZipFileName.isEmpty()) {
    6. result = android();
    7. } else {
    8. result = movie();
    9. }
    10. mCallbacks->shutdown();
    11. eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    12. eglDestroyContext(mDisplay, mContext);
    13. eglDestroySurface(mDisplay, mSurface);
    14. mFlingerSurface.clear();
    15. mFlingerSurfaceControl.clear();
    16. eglTerminate(mDisplay);
    17. eglReleaseThread();
    18. IPCThreadState::self()->stopProcess();
    19. return result;
    20. }

    如果mZipFileName不为空,那么接下来就会调用BootAnimation类的成员函数android来显示系统默认的开机动画,否则的话,就会调用BootAnimation类的成员函数movie来显示用户自定义的开机动画 

    1. bool BootAnimation::findBootAnimationFileInternal(const std::vector<std::string> &files) {
    2. for (const std::string& f : files) {
    3. if (access(f.c_str(), R_OK) == 0) {
    4. mZipFileName = f.c_str();
    5. return true;
    6. }
    7. }
    8. return false;
    9. }

     bootanim的关闭

    init启动zygote进程之后,由zygote孵化出了system_server,然后system_server启动了各种各种的系统所需的服务,其中就有AMS,AMS启动并ready后,会执行startHomeActivityLocked:

    1. void SurfaceFlinger::bootFinished()
    2. {
    3. if (mBootFinished == true) {
    4. ALOGE("Extra call to bootFinished");
    5. return;
    6. }
    7. mBootFinished = true;
    8. if (mStartPropertySetThread->join() != NO_ERROR) {
    9. ALOGE("Join StartPropertySetThread failed!");
    10. }
    11. const nsecs_t now = systemTime();
    12. const nsecs_t duration = now - mBootTime;
    13. ALOGI("Boot is finished (%ld ms)", long(ns2ms(duration)) );
    14. mFrameTracer->initialize();
    15. mTimeStats->onBootFinished();
    16. // wait patiently for the window manager death
    17. const String16 name("window");
    18. mWindowManager = defaultServiceManager()->getService(name);
    19. if (mWindowManager != 0) {
    20. mWindowManager->linkToDeath(static_cast<IBinder::DeathRecipient*>(this));
    21. }
    22. if (mVrFlinger) {
    23. mVrFlinger->OnBootFinished();
    24. }
    25. // stop boot animation
    26. // formerly we would just kill the process, but we now ask it to exit so it
    27. // can choose where to stop the animation.
    28. property_set("service.bootanim.exit", "1");
    29. const int LOGTAG_SF_STOP_BOOTANIM = 60110;
    30. LOG_EVENT_LONG(LOGTAG_SF_STOP_BOOTANIM,
    31. ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));
    32. sp<IBinder> input(defaultServiceManager()->getService(String16("inputflinger")));
    33. static_cast<void>(schedule([=] {
    34. if (input == nullptr) {
    35. ALOGE("Failed to link to input service");
    36. } else {
    37. mInputFlinger = interface_cast<IInputFlinger>(input);
    38. }
    39. readPersistentProperties();
    40. mPowerAdvisor.onBootFinished();
    41. mBootStage = BootStage::FINISHED;
    42. if (property_get_bool("sf.debug.show_refresh_rate_overlay", false)) {
    43. enableRefreshRateOverlay(true);
    44. }
    45. }));
    46. }

    AMS在systemReady后会启动launcher

    1. if (bootingSystemUser) {
    2. t.traceBegin("startHomeOnAllDisplays");
    3. mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady");
    4. t.traceEnd();
    5. }

    frameworks\base\services\core\java\com\android\server\wm\ActivityTaskManagerInternal.java

    public abstract boolean startHomeOnAllDisplays(int userId, String reason);

    frameworks\base\services\core\java\com\android\server\wm\ActivityTaskManagerService.java

    final class LocalService extends ActivityTaskManagerInternal {
    1. @Override
    2. public boolean startHomeOnAllDisplays(int userId, String reason) {
    3. synchronized (mGlobalLock) {
    4. return mRootWindowContainer.startHomeOnAllDisplays(userId, reason);
    5. }
    6. }

    frameworks\base\services\core\java\com\android\server\wm\RootWindowContainer.java 

    1. boolean startHomeOnAllDisplays(int userId, String reason) {
    2. boolean homeStarted = false;
    3. for (int i = getChildCount() - 1; i >= 0; i--) {
    4. final int displayId = getChildAt(i).mDisplayId;
    5. homeStarted |= startHomeOnDisplay(userId, reason, displayId);
    6. }
    7. return homeStarted;
    8. }
    9. void startHomeOnEmptyDisplays(String reason) {
    10. for (int i = getChildCount() - 1; i >= 0; i--) {
    11. final DisplayContent display = getChildAt(i);
    12. for (int tdaNdx = display.getTaskDisplayAreaCount() - 1; tdaNdx >= 0; --tdaNdx) {
    13. final TaskDisplayArea taskDisplayArea = display.getTaskDisplayAreaAt(tdaNdx);
    14. if (taskDisplayArea.topRunningActivity() == null) {
    15. startHomeOnTaskDisplayArea(mCurrentUser, reason, taskDisplayArea,
    16. false /* allowInstrumenting */, false /* fromHomeKey */);
    17. }
    18. }
    19. }
    20. }
    21. boolean startHomeOnDisplay(int userId, String reason, int displayId) {
    22. return startHomeOnDisplay(userId, reason, displayId, false /* allowInstrumenting */,
    23. false /* fromHomeKey */);
    24. }
    25. boolean startHomeOnDisplay(int userId, String reason, int displayId, boolean allowInstrumenting,
    26. boolean fromHomeKey) {
    27. // Fallback to top focused display or default display if the displayId is invalid.
    28. if (displayId == INVALID_DISPLAY) {
    29. final ActivityStack stack = getTopDisplayFocusedStack();
    30. displayId = stack != null ? stack.getDisplayId() : DEFAULT_DISPLAY;
    31. }
    32. final DisplayContent display = getDisplayContent(displayId);
    33. boolean result = false;
    34. for (int tcNdx = display.getTaskDisplayAreaCount() - 1; tcNdx >= 0; --tcNdx) {
    35. final TaskDisplayArea taskDisplayArea = display.getTaskDisplayAreaAt(tcNdx);
    36. result |= startHomeOnTaskDisplayArea(userId, reason, taskDisplayArea,
    37. allowInstrumenting, fromHomeKey);
    38. }
    39. return result;
    40. }

    frameworks\base\services\core\java\com\android\server\wm\ActivityTaskManagerService.java

    1. void postFinishBooting(boolean finishBooting, boolean enableScreen) {
    2. mH.post(() -> {
    3. if (finishBooting) {
    4. mAmInternal.finishBooting();
    5. }
    6. if (enableScreen) {
    7. mInternal.enableScreenAfterBoot(isBooted());
    8. }
    9. });
    10. }
    1. @Override
    2. public void enableScreenAfterBoot(boolean booted) {
    3. synchronized (mGlobalLock) {
    4. writeBootProgressEnableScreen(SystemClock.uptimeMillis());
    5. mWindowManager.enableScreenAfterBoot();
    6. updateEventDispatchingLocked(booted);
    7. }
    8. }

    frameworks\base\services\core\java\com\android\server\wm\WindowManagerService.java

    1. public void enableScreenAfterBoot() {
    2. synchronized (mGlobalLock) {
    3. ProtoLog.i(WM_DEBUG_BOOT, "enableScreenAfterBoot: mDisplayEnabled=%b "
    4. + "mForceDisplayEnabled=%b mShowingBootMessages=%b mSystemBooted=%b. "
    5. + "%s",
    6. mDisplayEnabled, mForceDisplayEnabled, mShowingBootMessages, mSystemBooted,
    7. new RuntimeException("here").fillInStackTrace());
    8. if (mSystemBooted) {
    9. return;
    10. }
    11. mSystemBooted = true;
    12. hideBootMessagesLocked();
    13. // If the screen still doesn't come up after 30 seconds, give
    14. // up and turn it on.
    15. mH.sendEmptyMessageDelayed(H.BOOT_TIMEOUT, 30 * 1000);
    16. }
    17. mPolicy.systemBooted();
    18. performEnableScreen();
    19. }

    enableScreenAfterBoot()经过多次调用就会执行WMS的performEnableScreen()方法,在此方法中我们就可以看到surfaceflinger的身影了,通过transact调用发送BOOT_FINISHED的消息给surfaceflinger。

    1.     frameworks\base\services\core\java\com\android\server\wm\WindowManagerService.java
    2.     private void performEnableScreen() {
    3.         ...
    4.  
    5.             try {
    6.                 IBinder surfaceFlinger = ServiceManager.getService("SurfaceFlinger");
    7.                 if (surfaceFlinger != null) {
    8.                     Slog.i(TAG_WM, "******* TELLING SURFACE FLINGER WE ARE BOOTED!");
    9.                     Parcel data = Parcel.obtain();
    10.                     data.writeInterfaceToken("android.ui.ISurfaceComposer");
    11.                     surfaceFlinger.transact(IBinder.FIRST_CALL_TRANSACTION, // BOOT_FINISHED
    12.                             data, null, 0);
    13.                     data.recycle();
    14.                 }
    15.             } catch (RemoteException ex) {
    16.                 Slog.e(TAG_WM, "Boot completed: SurfaceFlinger is dead!");
    17.             }
    18.         ...
    19.     }

    frameworks\native\libs\gui\include\gui\ISurfaceComposer.h 

    1. class BnSurfaceComposer: public BnInterface<ISurfaceComposer> {
    2. public:
    3. enum ISurfaceComposerTag {
    4. // Note: BOOT_FINISHED must remain this value, it is called from
    5. // Java by ActivityManagerService.
    6. BOOT_FINISHED = IBinder::FIRST_CALL_TRANSACTION,

    WMS最终通过binder调用,经过ISurfaceComposer处理,最终通知SurfaceFlinger关闭开机动画。

    1. frameworks\native\services\surfaceflinger\SurfaceFlinger.cpp
    2. void SurfaceFlinger::bootFinished()
    3. {
    4.     if (mStartPropertySetThread->join() != NO_ERROR) {
    5.         ALOGE("Join StartPropertySetThread failed!");
    6.     }
    7.     const nsecs_t now = systemTime();
    8.     const nsecs_t duration = now - mBootTime;
    9.     ALOGI("Boot is finished (%ld ms)", long(ns2ms(duration)) );
    10.  
    11.     // wait patiently for the window manager death
    12.     const String16 name("window");
    13.     sp<IBinder> window(defaultServiceManager()->getService(name));
    14.     if (window != 0) {
    15.         window->linkToDeath(static_cast<IBinder::DeathRecipient*>(this));
    16.     }
    17.  
    18.     if (mVrFlinger) {
    19.       mVrFlinger->OnBootFinished();
    20.     }
    21.  
    22.     // stop boot animation
    23.     // formerly we would just kill the process, but we now ask it to exit so it
    24.     // can choose where to stop the animation.
    25.     property_set("service.bootanim.exit", "1");
    26.  
    27.     const int LOGTAG_SF_STOP_BOOTANIM = 60110;
    28.     LOG_EVENT_LONG(LOGTAG_SF_STOP_BOOTANIM,
    29.                    ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));
    30.  
    31.     sp<LambdaMessage> readProperties = new LambdaMessage([&]() {
    32.         readPersistentProperties();
    33.     });
    34.     postMessageAsync(readProperties);
    35. }

    至此开机动画结束。

    frameworks\base\cmds\bootanimation\BootAnimation.cpp

    1. void BootAnimation::checkExit() {
    2. // Allow surface flinger to gracefully request shutdown
    3. char value[PROPERTY_VALUE_MAX];
    4. property_get(EXIT_PROP_NAME, value, "0");
    5. int exitnow = atoi(value);
    6. if (exitnow) {
    7. requestExit();
    8. }
    9. }

    requestExit(); kill掉bootanime进程

    system\core\libutils\Threads.cpp

    1. void Thread::requestExit()
    2. {
    3. Mutex::Autolock _l(mLock);
    4. mExitPending = true;
    5. }

    至此bootanime进程死亡。

  • 相关阅读:
    spring-boot + mybatis-enhance-actable实现自动创建表
    Win10系统固态硬盘开机慢的解决教程
    gitlab克隆本地切换p分支
    《Mycat分布式数据库架构》之Mycat管理
    Python优化算法02——遗传算法
    用Promise发起请求,失败后再次请求,几次后不再执行
    玩转代码|Google Map api国内正常使用该如何配置
    js选择器中:nth-of-child和:nth-of-type的区别
    [附源码]java毕业设计高校流动党员信息管理系统
    机器人内部传感器阅读梳理及心得-速度传感器-模拟式速度传感器
  • 原文地址:https://blog.csdn.net/xiaowang_lj/article/details/126436126