Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

What is the startup process of App

2025-02-24 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

This article introduces the relevant knowledge of "what is the startup process of App". In the operation of actual cases, many people will encounter such a dilemma, so let the editor lead you to learn how to deal with these situations. I hope you can read it carefully and be able to achieve something!

Preface

First of all, new has a daughter.

Var mDdaughter = new daughter ("6 years old", "pretty and cute", "healthy and cute", "favorite to play with little genius phone watch and her father") to answer the little interviewer

Daughter, you can think of the watch as a kindergarten with a teacher, a monitor, a class cadre, and a lot of children.

One teacher: teacher Z (Zygote process)

A monitor: Xiao A (ActivityManagerService)

A class cadre: Xiao L (Launcher desktop application)

A lot of kids: all apps, including music kids, chat kids, calendar kids and so on.

The startup process of the app is like a child being woken up. After turning on the phone, teacher Z will wake up the monitor and the class cadre (SystemServer#ActivityManagerService,Launcher) in turn. After Xiao L wakes up, he will find out which children are in the watch, what they look like (icon,name), family information (package name, androidmanifest), and so on, and then paste the photos (icon) of the children on his body one by one. For example, there are music children, chat children, calendar children, in fact, it is the table on your watch.

At this time you click to open a music child (startActivity), Xiao L will inform the monitor Xiao A (Binder), Xiao A knows, let Xiao L have a rest (Paused), and then go to find teacher Z. Teacher Z is responsible for waking up the music children (fork process, starting ActivityThread). After getting up, the music children will find Xiao A to take her to wash her face and brush her teeth (start ApplicationThread,Activity). When it's all done, you can perform all kinds of performances, singing and dancing.

Fifteen years later mDdaughter.grow (15)

MDdaughter.study ("Android")

Fifteen years later, my daughter is 21 years old and is studying Android, considering whether she should follow her father's career.

On this day, she came to me with a puzzled look on her face: "Dad, what exactly is the process of starting this app? I still don't understand it after watching it for a long time. Why don't you tell me more about it?" "all right, don't worry. I'll tell you more about it this time."

Answer Android female programmer

Remember the story I told you when I was a child, the Android system is like a kindergarten, there is a big friend named Launcher, who will post a lot of other children's business cards. This Launcher is our desktop, it knows the information of all the applications in the system through PackageManagerService, and shows it, of course, it is also an application.

By clicking on an application icon, which triggers the click event, it is finally executed to the startActivity method. This coincides with the step of starting Activity.

So what did this startActivity do? How did you wake up this app through many hurdles?

First of all, introduce the important members of the system, who play an important role in the app startup process.

System members introduce the init process. After the Android system starts, Zygote is not the first process, but the root process of linux, the init process, and then the init process will start the Zygote process. The Zygote process, the parent process of all android processes, and, of course, the SystemServer process SystemServer process, as the name suggests, the system service process, which is responsible for everything large and small in the system, also starts the ActivityManagerService,PackageManagerService,WindowManagerService and the binder thread pool. ActivityManagerService is mainly responsible for the startup, switching, scheduling of the four major components of the system and the management and scheduling of application processes. For the startup of some processes, it will be passed to AMS through the Binder communication mechanism, and then processed to Zygote. PackageManagerService, mainly responsible for some operations of the application package, such as installing, uninstalling, parsing AndroidManifest.xml, scanning file information, and so on. WindowManagerService, mainly responsible for some window-related services, such as window startup, add, delete and so on. Launcher, a desktop application, also belongs to an application, and it also has its own Activity, which starts by default as soon as it is powered on, and starts implicitly by setting the Category of Intent.CATEGORY_HOME.

Figure out these members, follow me to see how to get through the five hurdles, and finally start an App.

The first level: cross-process communication, telling the system my requirements

First, I want to tell the system that my Launcher is going to start an application, call the Activity.startActivityForResult method, and eventually go to the mInstrumentation.execStartActivity method. Because Launcher itself is in a separate process, it needs to tell the system service across processes that I want to start App. Find the Service you want to notify, named ActivityTaskManagerService, and use AIDL to communicate with him through Binder.

Here is a brief introduction to ActivityTaskManagerService (ATMS for short). Originally, these communication work belongs to ActivityManagerService, but now part of the work is assigned to ATMS, which mainly includes the scheduling work of four major components. Is also started directly by the SystemServer process, the relevant source code can be seen in the ActivityManagerService.Lifecycle.startService method, interested friends can see for themselves.

Then let's talk about cross-process communication. The relevant code is as follows:

/ / Instrumentation.java

Int result = ActivityTaskManager.getService ()

.startActivity (whoThread, who.getBasePackageName (), intent

Intent.resolveTypeIfNeeded (who.getContentResolver ())

Token, target! = null? Target.mEmbeddedID: null

RequestCode, 0, null, options)

/ / ActivityTaskManager.java

Public static IActivityTaskManager getService () {

Return IActivityTaskManagerSingleton.get ()

}

Private static final Singleton IActivityTaskManagerSingleton =

New Singleton () {

@ Override

Protected IActivityTaskManager create () {

Final IBinder b = ServiceManager.getService (Context.ACTIVITY_TASK_SERVICE)

Return IActivityTaskManager.Stub.asInterface (b)

}

}

/ / ActivityTaskManagerService.java

Public class ActivityTaskManagerService extends IActivityTaskManager.Stub

Public static final class Lifecycle extends SystemService {

Private final ActivityTaskManagerService mService

Public Lifecycle (Context context) {

Super (context)

MService = new ActivityTaskManagerService (context)

}

@ Override

Public void onStart () {

PublishBinderService (Context.ACTIVITY_TASK_SERVICE, mService)

MService.start ()

}

}

We are all familiar with startActivity, and we usually use it to start Activity. This is the way to start an application. We also bring intent information to indicate which Activity is to be launched.

Another thing to note is that there is a checkStartActivityResult method after startActivity, which is used to check the results of starting Activity. When starting Activity fails, an exception is thrown through this method, such as our common problem: not registered with AndroidManifest.xml.

Public static void checkStartActivityResult (int res, Object intent) {

Switch (res) {

Case ActivityManager.START_INTENT_NOT_RESOLVED:

Case ActivityManager.START_CLASS_NOT_FOUND:

If (intent instanceof Intent & ((Intent) intent) .getComponent ()! = null)

Throw new ActivityNotFoundException (

"Unable to find explicit activity class"

+ ((Intent) intent) .getComponent () .toShortString ()

+ "; have you declared this activity in your AndroidManifest.xml?")

Throw new ActivityNotFoundException (

"No Activity found to handle" + intent)

Case ActivityManager.START_PERMISSION_DENIED:

Throw new SecurityException ("Not allowed to start activity"

+ intent)

Case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:

Throw new AndroidRuntimeException (

"FORWARD_RESULT_FLAG used while also requesting a result")

Case ActivityManager.START_NOT_ACTIVITY:

Throw new IllegalArgumentException (

"PendingIntent is not an activity")

/ /...

}

Second level: inform Launcher that you can have a rest.

When ATMS receives the message to start, it informs the previous application that Launcher can take a break and enter the Paused state.

/ / ActivityStack.java

Private boolean resumeTopActivityInnerLocked (ActivityRecord prev, ActivityOptions options) {

/ /...

ActivityRecord next = topRunningActivityLocked (true / * focusableOnly * /)

/ /...

Boolean pausing = getDisplay () .pauseBackStacks (userLeaving, next, false)

If (mResumedActivity! = null) {

If (DEBUG_STATES) Slog.d (TAG_STATES

"resumeTopActivityLocked: Pausing" + mResumedActivity)

Pausing | = startPausingLocked (userLeaving, false, next, false)

}

/ /...

If (next.attachedToProcess ()) {

/ / the application has been started

Try {

/ /...

Transaction.setLifecycleStateRequest (

ResumeActivityItem.obtain (next.app.getReportedProcState ()

GetDisplay () .mDisplayContent.isNextTransitionForward ())

MService.getLifecycleManager () scheduleTransaction (transaction)

/ /...

} catch (Exception e) {

/ /...

MStackSupervisor.startSpecificActivityLocked (next, true, false)

Return true

}

/ /...

/ / From this point on, if something goes wrong there is no way

/ / to recover the activity.

Try {

Next.completeResumeLocked ()

} catch (Exception e) {

/ / If any exception gets thrown, toss away this

/ / activity and try the next one.

Slog.w (TAG, "Exception thrown during resume of" + next, e)

RequestFinishActivityLocked (next.appToken, Activity.RESULT_CANCELED, null

"resume-exception", true)

Return true

}

} else {

/ / Cold start process

MStackSupervisor.startSpecificActivityLocked (next, true, true)

}

}

There are two classes that have not been seen before:

ActivityStack, is the stack management of Activity, which is equivalent to the Activity management class written by ourselves in our usual project, which is used to manage the state of Activity, such as stack order and so on. ActivityRecord, which represents a specific Activity, stores all kinds of information about that Activity.

The startPausingLocked method is to put the previous application, in this case, Launcher, into the Paused state. Then it will determine whether the application is started, and if it has already started, it will follow the ResumeActivityItem method. Look at this name, combined with the premise that the application has been started, whether you have guessed what it is? Yes, this is used to control the onResume lifecycle method of Activity, not only the onResume but also the onStart method. For more information, please see the handleResumeActivity method source code of ActivityThread.

If the application doesn't start, it will go on to the startSpecificActivityLocked method and take a look.

The third level: whether the process has been started, otherwise the process is created

After Launcher enters Paused, ActivityTaskManagerService will determine whether the application process to be opened has been started, and if so, just start Activity directly, that is, the Activity process within the application. If the process is not started, you need to create a process.

Here are two questions:

How to determine whether the application process exists? If an application has been started, a WindowProcessController message is saved in ATMS. This information, including processName and uid,uid, is the id of the application and can be obtained through applicationInfo.uid. ProcessName is the process name, usually the package name. Therefore, to determine whether there is an application process, it is based on processName and uid to determine whether there is a corresponding WindowProcessController, and the thread in WindowProcessController is not empty. The code is as follows: / / ActivityStackSupervisor.java

Void startSpecificActivityLocked (ActivityRecord r, boolean andResume, boolean checkConfig) {

/ / Is this activity's application already running?

Final WindowProcessController wpc =

MService.getProcessController (r.processName, r.info.applicationInfo.uid)

Boolean knownToBeDead = false

If (wpc! = null & & wpc.hasThread ()) {

/ / the application process exists

Try {

RealStartActivityLocked (r, wpc, andResume, checkConfig)

Return

}

}

}

/ / WindowProcessController.java

IApplicationThread getThread () {

Return mThread

}

Boolean hasThread () {

Return mThread! = null

}

Another question is how to create a process? Do you remember Mr. Z? Yes, it is the Zygote process. As mentioned earlier, he is the parent of all processes, so inform Zygote to fork a new process to serve this application. / / ZygoteProcess.java

Private Process.ProcessStartResult attemptUsapSendArgsAndGetResult (

ZygoteState zygoteState, String msgStr)

Throws ZygoteStartFailedEx, IOException {

Try (LocalSocket usapSessionSocket = zygoteState.getUsapSessionSocket ()) {

Final BufferedWriter usapWriter =

New BufferedWriter (

New OutputStreamWriter (usapSessionSocket.getOutputStream ())

Zygote.SOCKET_BUFFER_SIZE)

Final DataInputStream usapReader =

New DataInputStream (usapSessionSocket.getInputStream ())

UsapWriter.write (msgStr)

UsapWriter.flush ()

Process.ProcessStartResult result = new Process.ProcessStartResult ()

Result.pid = usapReader.readInt ()

/ / USAPs can't be used to spawn processes that need wrappers.

Result.usingWrapper = false

If (result.pid > = 0) {

Return result

} else {

Throw new ZygoteStartFailedEx ("USAP specialization failed")

}

}

}

As you can see, this is actually communicating through socket and Zygote, and BufferedWriter is used to read and receive messages. Here the message of the new process is passed to Zygote, and the Zygote performs the fork process and returns the pid of the new process.

Maybe someone will ask again? What is fork? Why is it that socket does IPC communication instead of Bindler?

First, fork () is a method that is the main method for creating processes on Unix-like operating systems. Used to create a child process (equivalent to a copy of the current process). So why do you use socket instead of Binder in fork? Mainly because fork does not allow multithreading, Binder communication happens to be multithreading.

Questions always arise, and curious friends will always ask, why doesn't fork allow multithreading?

Prevent deadlocks. In fact, if you think about it, multi-thread + multi-process, it doesn't sound very reliable. Suppose that thread An in multithreading calls a lock lock, and another thread B calls fork to create a child process, but the child process does not have thread A, but the lock itself is fork, then no one can open the lock. As soon as another thread in the sub-process lock the lock, it is deadlocked. Level 4: ActivityThread makes a brilliant debut.

I just mentioned that the fork process is performed by Zygote and the pid of the new process is returned. In fact, the ActivityThread object is also instantiated in the process. Let's take a look at how it works:

/ / RuntimeInit.java

Protected static Runnable findStaticMain (String className, String [] argv

ClassLoader classLoader) {

Class cl

Try {

Cl = Class.forName (className, true, classLoader)

} catch (ClassNotFoundException ex) {

Throw new RuntimeException (

"Missing class when invoking static main" + className

Ex)

}

Method m

Try {

M = cl.getMethod ("main", new Class [] {String [] .class})

} catch (NoSuchMethodException ex) {

Throw new RuntimeException (

"Missing static main on" + className, ex)

} catch (SecurityException ex) {

Throw new RuntimeException (

"Problem getting static main on" + className, ex)

}

/ /...

Return new MethodAndArgsCaller (m, argv)

}

It's a reflex! The main method of ActivityThread is called through reflection. ActivityThread should be familiar to all of you, representing the main thread of Android, and the main method is also the main entry of app. This is not right! It is called when you create a new process, but it is not the main entry. Take a look at this main entrance.

Public static void main (String [] args) {

/ /...

Looper.prepareMainLooper ()

ActivityThread thread = new ActivityThread ()

Thread.attach (false, startSeq)

/ /...

If (false) {

Looper.myLooper () .setMessageLogging (new

LogPrinter (Log.DEBUG, "ActivityThread"))

}

/ /...

Looper.loop ()

Throw new RuntimeException ("Main thread loop unexpectedly exited")

}

The main method mainly creates the ActivityThread, creates the Looper object of the main thread, and starts the loop loop. In addition to this, tell AMS that I am awake and that the process has been created! That is, the attach method in the above code, and finally go to the AMSattachApplicationLocked method to see what this method does:

/ / ActivitymanagerService.java

Private final boolean attachApplicationLocked (IApplicationThread thread

Int pid, int callingUid, long startSeq) {

/ /...

ProcessRecord app

/ /...

Thread.bindApplication (processName, appInfo, providers, null, profilerInfo

Null, testMode

MBinderTransactionTrackingEnabled, enableTrackAllocation

IsRestrictedBackupMode | |! normalMode, app.isPersistent ()

New Configuration (app.getWindowProcessController () .getConfiguration ())

App.compat, getCommonServicesLocked (app.isolated)

MCoreSettingsObserver.getCoreSettingsLocked ()

BuildSerial, autofillOptions, contentCaptureOptions)

/ /...

App.makeActive (thread, mProcessStats)

/ /...

/ / See if the top visible activity is waiting to run in this process...

If (normalMode) {

Try {

DidSomething = mAtmInternal.attachApplication (app.getWindowProcessController ())

} catch (Exception e) {

Slog.wtf (TAG, "Exception thrown launching activities in" + app, e)

BadApp = true

}

}

/ /...

}

/ / ProcessRecord.java

Public void makeActive (IApplicationThread _ thread, ProcessStatsService tracker) {

/ /...

Thread = _ thread

MWindowProcessController.setThread (thread)

}

Three main things have been done here:

The bindApplication method is mainly used to start Application. MakeActive method, which sets the thread in the WindowProcessController, which is used to determine whether the process exists or not. The attachApplication method starts the root Activity. Level 5: create an Application

Then look at the above, as we are familiar with, after the application starts, it should be to start Applicaiton, start Activity. See what's going on:

/ / ActivityThread#ApplicationThread

Public final void bindApplication (String processName, ApplicationInfo appInfo

List providers, ComponentName instrumentationName

ProfilerInfo profilerInfo, Bundle instrumentationArgs

IInstrumentationWatcher instrumentationWatcher

IUiAutomationConnection instrumentationUiConnection, int debugMode

Boolean enableBinderTracking, boolean trackAllocation

Boolean isRestrictedBackupMode, boolean persistent, Configuration config

CompatibilityInfo compatInfo, Map services, Bundle coreSettings

String buildSerial, AutofillOptions autofillOptions

ContentCaptureOptions contentCaptureOptions) {

AppBindData data = new AppBindData ()

Data.processName = processName

Data.appInfo = appInfo

Data.providers = providers

Data.instrumentationName = instrumentationName

Data.instrumentationArgs = instrumentationArgs

Data.instrumentationWatcher = instrumentationWatcher

Data.instrumentationUiAutomationConnection = instrumentationUiConnection

Data.debugMode = debugMode

Data.enableBinderTracking = enableBinderTracking

Data.trackAllocation = trackAllocation

Data.restrictedBackupMode = isRestrictedBackupMode

Data.persistent = persistent

Data.config = config

Data.compatInfo = compatInfo

Data.initProfilerInfo = profilerInfo

Data.buildSerial = buildSerial

Data.autofillOptions = autofillOptions

Data.contentCaptureOptions = contentCaptureOptions

SendMessage (H.BIND_APPLICATION, data)

}

Public void handleMessage (Message msg) {

If (DEBUG_MESSAGES) Slog.v (TAG, "> handling:" + codeToString (msg.what))

Switch (msg.what) {

Case BIND_APPLICATION:

Trace.traceBegin (Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication")

AppBindData data = (AppBindData) msg.obj

HandleBindApplication (data)

Trace.traceEnd (Trace.TRACE_TAG_ACTIVITY_MANAGER)

Break

}

}

You can see that there is a Handler class of the main thread, which is used to handle all kinds of messages that need to be processed by the main thread, including BIND_SERVICE,LOW_MEMORY,DUMP_HEAP, and so on. Then take a look at handleBindApplication:

Private void handleBindApplication (AppBindData data) {

/ /...

Try {

Final ClassLoader cl = instrContext.getClassLoader ()

MInstrumentation = (Instrumentation)

Cl.loadClass (data.instrumentationName.getClassName (). NewInstance ()

}

/ /...

Application app

Final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites ()

Final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy ()

Try {

/ / If the app is being launched for full backup or restore, bring it up in

/ / a restricted environment with the base application class.

App = data.info.makeApplication (data.restrictedBackupMode, null)

MInitialApplication = app

/ / don't bring up providers in restricted mode; they may depend on the

/ / app's custom Application class

If (! data.restrictedBackupMode) {

If (! ArrayUtils.isEmpty (data.providers)) {

InstallContentProviders (app, data.providers)

}

}

/ / Do this after providers, since instrumentation tests generally start their

/ / test thread at this point, and we don't want that racing.

Try {

MInstrumentation.onCreate (data.instrumentationArgs)

}

/ /...

Try {

MInstrumentation.callApplicationOnCreate (app)

} catch (Exception e) {

If (! mInstrumentation.onException (app, e)) {

Throw new RuntimeException (

"Unable to create application" + app.getClass () .getName ()

+ ":" + e.toString (), e)

}

}

}

/ /...

}

There is a lot of information here, look at it a little bit:

First, the Instrumentation is created, which is the first step in startActivity at the beginning of the above. Each application has an Instrumentation to manage the process, such as when you want to create an Activity, it will be executed into this class first. The makeApplication method, which created Application, has finally come to this point. Eventually you go to the newApplication method, which executes the attach method of Application. Public Application newApplication (ClassLoader cl, String className, Context context)

Throws InstantiationException, IllegalAccessException

ClassNotFoundException {

Application app = getFactory (context.getPackageName ())

.instantiate Application (cl, className)

App.attach (context)

Return app

}

With the attach method in place, when was the onCreate method called? Coming right away:

Instrumentation.callApplicationOnCreate (app)

Public void callApplicationOnCreate (Application app) {

App.onCreate ()

}

That is, create the order of Application- > attach- > onCreate calls.

Wait, there is an important line of code before onCreate:

InstallContentProviders

Here is the relevant code to start Provider, the specific logic will not be analyzed.

Level 6: start Activity

After talking about bindApplication, it's time to talk about the follow-up. As mentioned in the fifth pass above, the bindApplication method is followed by the attachApplication method, which will eventually be executed to the handleLaunchActivity method of ActivityThread:

Public Activity handleLaunchActivity (ActivityClientRecord r

PendingTransactionActions pendingActions, Intent customIntent) {

/ /...

WindowManagerGlobal.initialize ()

/ /...

Final Activity a = performLaunchActivity (r, customIntent)

/ /...

Return a

}

First of all, initialize the WindowManagerGlobal. What is this? Yes, it is WindowManagerService, and it is also ready for the follow-up window display.

Continue to look at performLaunchActivity:

Private Activity performLaunchActivity (ActivityClientRecord r, Intent customIntent) {

/ / create a ContextImpl

ContextImpl appContext = createBaseContextForActivity (r)

Activity activity = null

Try {

Java.lang.ClassLoader cl = appContext.getClassLoader ()

/ / create an Activity

Activity = mInstrumentation.newActivity (

Cl, component.getClassName (), r.intent)

}

Try {

If (activity! = null) {

/ / complete the initialization of some important data of activity

Activity.attach (appContext, this, getInstrumentation (), r.token

R.ident, app, r.intent, r.activityInfo, title, r.parent

R.embeddedID, r.lastNonConfigurationInstances, config

R.referrer, r.voiceInteractor, window, r.configCallback

R.assistToken)

If (customIntent! = null) {

Activity.mIntent = customIntent

}

/ / set the theme of activity

Int theme = r.activityInfo.getThemeResource ()

If (theme! = 0) {

Activity.setTheme (theme)

}

/ / call the onCreate method of activity

If (r.isPersistable ()) {

MInstrumentation.callActivityOnCreate (activity, r.state, r.persistentState)

} else {

MInstrumentation.callActivityOnCreate (activity, r.state)

}

}

}

Return activity

}

Wow, finally see the onCreate method. Steady, or take a look at this code step by step.

First of all, the ContextImpl object is created. Some friends of ContextImpl may not know what it is. ContextImpl inherits from Context, which is actually the context we usually use. Some students may say, this is not right, ah, access to the context is clearly the Context object. Let's follow the source code and have a look.

/ / Activity.java

Context mBase

@ Override

Public Executor getMainExecutor () {

Return mBase.getMainExecutor ()

}

@ Override

Public Context getApplicationContext () {

Return mBase.getApplicationContext ()

}

As you can see here, the context we usually use is this mBase, so just find out what this mBase is:

Protected void attachBaseContext (Context base) {

If (mBase! = null) {

Throw new IllegalStateException ("Base context already set")

}

MBase = base

}

/ / look up layer by layer

Final void attach (Context context, ActivityThread aThread

Instrumentation instr, IBinder token, int ident

Application application, Intent intent, ActivityInfo info

CharSequence title, Activity parent, String id

NonConfigurationInstances lastNonConfigurationInstances

Configuration config, String referrer, IVoiceInteractor voiceInteractor) {

AttachBaseContext (context)

MWindow = new PhoneWindow (this, window, activityConfigCallback)

MWindow.setWindowControllerCallback (this)

MWindow.setCallback (this)

MWindow.setOnWindowDismissedCallback (this)

MWindow.getLayoutInflater () setPrivateFactory (this)

If (info.softInputMode! = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {

MWindow.setSoftInputMode (info.softInputMode)

}

}

Isn't this the attach in the performLaunchActivity method at the beginning? What a coincidence, so this ContextImpl is the context we usually use.

By the way, what else did attach do? Create a new PhoneWindow, establish your own association with Window, and set up setSoftInputMode and so on.

After the ContextImpl is created, the object of the Activity is created through the class loader, the theme of the activity is set, and finally the onCreate method of activity is called.

This is the end of the content of "what is the startup process of App". Thank you for your reading. If you want to know more about the industry, you can follow the website, the editor will output more high-quality practical articles for you!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report