首先,new一个女儿,

var mDdaughter = new 女儿(“6岁”,“漂亮可爱”,“健康乖巧”,“最喜欢玩小天才电话手表和她的爸爸”)

好了,女儿有了,有一天,女儿问我:

“爸爸爸爸,你说我玩的这个小天才电话手表怎么这么厉害,随便点一下这个小图片,这个应用就冒出来了,就可以听儿歌了。好神奇啊。”

我心里一惊:

小天才电话手表的系统就是Android,所以这不就是。。面试官常考的应用启动流程嘛!女儿也要来面试我了吗!好了,既然女儿问了,那就答吧。但是,对付这个小小的0经验面试官,我该咋说呢?

解答小小面试官

女儿,你可以把手表里面想象成一个幼儿园,里面有一个老师,一个班长,一个班干部,以及一大堆小朋友。

一个老师:Z老师(Zygote进程)

一个班长:小A(ActivityManagerService)

一个班干部:小L(Launcher桌面应用)

一大堆小朋友:所有应用,包括音乐小朋友,聊天小朋友,日历小朋友等等。

应用启动过程就像一个小朋友被叫醒一样,开机之后呢,Z老师会依次叫醒班长和班干部(SystemServer#ActivityManagerService,Launcher),小L醒了之后就会去了解手表里有哪些小朋友,长什么样(icon,name),家庭信息(包名,androidmanifest)等等,然后一个个把小朋友的照片(icon)贴到自己的身上。比如有音乐小朋友,聊天小朋友,日历小朋友,其实也就是你手表上这个桌面啦。

这时候你要点开一个音乐小朋友呢(startActivity),小L就会通知班长小A(Binder),小A知道了之后,让小L自己休息下(Paused),然后就去找Z老师了。Z老师就负责叫音乐小朋友起床了(fork进程,启动ActivityThread),音乐小朋友起来后就又找小A带她去洗脸刷牙(启动ApplicationThread,Activity),都弄完了就可以进行各种表演了,唱歌啊,跳舞啊。

女儿似懂非懂的给我点了一个赞,爸爸你真棒。

十五年后

mDdaughter.grow(15)
mDdaughter.study(“Android”)
复制代码

过了十五年,女儿已经21岁了,正在学习Android,考虑要不要女从父业。

这天,她一脸疑惑的来找我: “爸,这个app启动到底是怎么个流程啊,我看了好久还是不大明白,要不你再跟我详细讲一遍吧?” “好嘞,别担心,我这次详细跟你说说”

解答Android程序媛

还记得我小时候跟你说过的故事吗,Android系统就像一个幼儿园,有一个大朋友叫Launcher,身上会贴很多其他小朋友的名片。这个Launcher就是我们的桌面了,它通过PackageManagerService获知了系统里所有应用的信息,并展示了出来,当然它本身也是一个应用。

通过点击一个应用图标,也就是触发了点击事件,最后会执行到startActivity方法。这里也就和启动Activity步骤重合上了。

那么这个startActivity干了啥?是怎么通过重重关卡唤醒这个应用的?

首先,介绍下系统中那些重要的成员,他们在app启动流程中都担任了重要的角色.

系统成员介绍

init进程,Android系统启动后,Zygote并不是第一个进程,而是linux的根进程init进程,然后init进程才会启动Zygote进程。

Zygote进程,所有android进程的父进程,当然也包括SystemServer进程

SystemServer进程,正如名字一样,系统服务进程,负责系统中大大小小的事物,为此也是启动了三员大将(ActivityManagerService,PackageManagerService,WindowManagerService)以及binder线程池。

ActivityManagerService,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,对于一些进程的启动,都会通过Binder通信机制传递给AMS,再处理给Zygote。

PackageManagerService,主要负责应用包的一些操作,比如安装,卸载,解析AndroidManifest.xml,扫描文件信息等等。

WindowManagerService,主要负责窗口相关的一些服务,比如窗口的启动,添加,删除等。

Launcher,桌面应用,也是属于应用,也有自己的Activity,一开机就会默认启动,通过设置Intent.CATEGORY_HOME的Category隐式启动。

搞清楚这些成员,就跟随我一起看看怎么过五关斩六将,最终启动了一个App。

第一关:跨进程通信,告诉系统我的需求

首先,要告诉系统,我Launcher要启动一个应用了,调用Activity.startActivityForResult方法,最终会转到mInstrumentation.execStartActivity方法。 由于Launcher自己处在一个单独的进程,所以它需要跨进程告诉系统服务我要启动App的需求。 找到要通知的Service,名叫ActivityTaskManagerService,然后使用AIDL,通过Binder与他进行通信。

这里的简单说下ActivityTaskManagerService(简称ATMS)。原来这些通信工作都是属于ActivityManagerService,现在分了一部分工作给到ATMS,主要包括四大组件的调度工作。也是由SystemServer进程直接启动的,相关源码可见ActivityManagerService.Lifecycle.startService方法,感兴趣朋友可以自己看看。

接着说跨进程通信,相关代码如下:

//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<IActivityTaskManager> IActivityTaskManagerSingleton =new Singleton<IActivityTaskManager>() {@Overrideprotected IActivityTaskManager create() {final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);return IActivityTaskManager.Stub.asInterface(b);}};//ActivityTaskManagerService.java
public class ActivityTaskManagerService extends IActivityTaskManager.Stubpublic static final class Lifecycle extends SystemService {private final ActivityTaskManagerService mService;public Lifecycle(Context context) {super(context);mService = new ActivityTaskManagerService(context);}@Overridepublic void onStart() {publishBinderService(Context.ACTIVITY_TASK_SERVICE, mService);mService.start();}
}

复制代码

startActivity我们都很熟悉,平时启动Activity都会使用,启动应用也是从这个方法开始的,也会同样带上intent信息,表示要启动的是哪个Activity。

另外要注意的一点是,startActivity之后有个checkStartActivityResult方法,这个方法是用作检查启动Activity的结果。当启动Activity失败的时候,就会通过这个方法抛出异常,比如有我们常见的问题:未在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”);
//…
}
}

第二关:通知Launcher可以休息了

ATMS收到要启动的消息后,就会通知上一个应用,也就是Launcher可以休息会了,进入Paused状态。

//ActivityStack.javaprivate 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()) {//应用已经启动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 {//冷启动流程mStackSupervisor.startSpecificActivityLocked(next, true, true);}
}

这里有两个类没有见过:

ActivityStack,是Activity的栈管理,相当于我们平时项目里面自己写的Activity管理类,用于管理Activity的状态啊,如栈出栈顺序等等。

ActivityRecord,代表具体的某一个Activity,存放了该Activity的各种信息。

startPausingLocked方法就是让上一个应用,这里也就是Launcher进入Paused状态。 然后就会判断应用是否启动,如果已经启动了,就会走ResumeActivityItem的方法,看这个名字,结合应用已经启动的前提,是不是已经猜到了它是干嘛的?没错,这个就是用来控制Activity的onResume生命周期方法的,不仅是onResume还有onStart方法,具体可见ActivityThread的handleResumeActivity方法源码。

如果应用没启动就会接着走到startSpecificActivityLocked方法,接着看。

第三关:是否已启动进程,否则创建进程

Launcher进入Paused之后,ActivityTaskManagerService就会判断要打开的这个应用进程是否已经启动,如果已经启动,则直接启动Activity即可,这也就是应用内的启动Activity流程。如果进程没有启动,则需要创建进程。

这里有两个问题:

怎么判断应用进程是否存在呢?如果一个应用已经启动了,会在ATMS里面保存一个WindowProcessController信息,这个信息包括processName和uid,uid则是应用程序的id,可以通过applicationInfo.uid获取。processName则是进程名,一般为程序包名。所以判断是否存在应用进程,则是根据processName和uid去判断是否有对应的WindowProcessController,并且WindowProcessController里面的线程不为空。代码如下:

//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()) {//应用进程存在try {realStartActivityLocked(r, wpc, andResume, checkConfig);return;} }
}//WindowProcessController.java
IApplicationThread getThread() {return mThread;
}boolean hasThread() {return mThread != null;
}

还有个问题就是怎么创建进程?还记得Z老师吗?对,就是Zygote进程。之前说了他是所有进程的父进程,所以就要通知Zygote去fork一个新的进程,服务于这个应用。

//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");}}
}

可以看到,这里其实是通过socket和Zygote进行通信,BufferedWriter用于读取和接收消息。这里将要新建进程的消息传递给Zygote,由Zygote进行fork进程,并返回新进程的pid。

可能又会有人问了?fork是啥?为啥这里又变成socket进行IPC通信,而不是Bindler了?

首先,fork()是一个方法,是类Unix操作系统上创建进程的主要方法。用于创建子进程(等同于当前进程的副本)。

那为什么fork的时候不用Binder而用socket了呢?主要是因为fork不允许存在多线程,Binder通讯偏偏就是多线程。

问题总是在不断产生,总有好奇的朋友会接着问,为什么fork不允许存在多线程?

防止死锁。其实你想想,多线程+多进程,听起就不咋靠谱是不。假设多线程里面线程A对某个锁lock,另外一个线程B调用fork创建了子进程,但是子进程却没有了线程A,但是锁本身却被fork了出来,那么这个锁没人可以打开了。一旦子进程中另外的线程又对这个锁进行lock,就死锁了。

第四关:ActivityThread闪亮登场

刚才说到由Zygote进行fork进程,并返回新进程的pid。其实这过程中也实例化ActivityThread对象。一起看看是怎么实现的:

//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);
}

复制代码

原来是反射!通过反射调用了ActivityThread 的 main 方法。ActivityThread大家应该都很熟悉了,代表了Android的主线程,而main方法也是app的主入口。这不对上了!新建进程的时候就调用了,可不是主入口嘛。来看看这个主入口。

public static void main(String[] args) {//...Looper.prepareMainLooper();ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);//...if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}//...Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");
}

main方法主要创建了ActivityThread,创建了主线程的Looper对象,并开始loop循环。除了这些,还要告诉AMS,我醒啦,进程创建好了!也就是上述代码中的attach方法,最后会转到AMSattachApplicationLocked方法,一起看看这个方法干了啥:

//ActivitymanagerService.java
private final boolean attachApplicationLocked(IApplicationThread thread,int pid, int callingUid, long startSeq) {//...ProcessRecord app;//...thread.bindApplication(processName, appInfo, providers, null, profilerInfo,null, null, 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);
}

这里主要做了三件事:

bindApplication方法,主要用来启动Application。

makeActive方法,设定WindowProcessController里面的线程,也就是上文中说过判断进程是否存在所用到的。

attachApplication方法,启动根Activity。

第五关:创建Application

接着上面看,按照我们所熟知的,应用启动后,应该就是启动Applicaiton,启动Activity。看看是不是怎么回事:

//ActivityThread#ApplicationThread
public final void bindApplication(String processName, ApplicationInfo appInfo,List<ProviderInfo> 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;}}

可以看到这里有个H,H是主线程的一个Handler类,用于处理需要主线程处理的各类消息,包括BIND_SERVICE,LOW_MEMORY,DUMP_HEAP等等。接着看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 classif (!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);}}}//...
}

这里信息量就多了,一点点的看:

首先,创建了Instrumentation,也就是上文一开始startActivity的第一步。每个应用程序都有一个Instrumentation,用于管理这个进程,比如要创建Activity的时候,首先就会执行到这个类里面。

makeApplication方法,创建了Application,终于到这一步了。最终会走到newApplication方法,执行Application的attach方法。

public Application newApplication(ClassLoader cl, String className, Context context)throws InstantiationException, IllegalAccessException, ClassNotFoundException {Application app = getFactory(context.getPackageName()).instantiateApplication(cl, className);app.attach(context);return app;
}

attach方法有了,onCreate方法又是何时调用的呢?马上来了:

instrumentation.callApplicationOnCreate(app);public void callApplicationOnCreate(Application app) {app.onCreate();
}

也就是创建Application->attach->onCreate调用顺序。

等等,在onCreate之前还有一句重要的代码:

installContentProviders

这里就是启动Provider的相关代码了,具体逻辑就不分析了。

第六关:启动Activity

说完bindApplication,该说说后续了,上文第五关说到,bindApplication方法之后执行的是attachApplication方法,最终会执行到ActivityThread的handleLaunchActivity方法:

public Activity handleLaunchActivity(ActivityClientRecord r,PendingTransactionActions pendingActions, Intent customIntent) {//...WindowManagerGlobal.initialize();//...final Activity a = performLaunchActivity(r, customIntent);//...return a;
}

首先,初始化了WindowManagerGlobal,这是个啥呢? 没错,就是WindowManagerService了,也为后续窗口显示等作了准备。

继续看performLaunchActivity:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {//创建ContextImplContextImpl appContext = createBaseContextForActivity(r);Activity activity = null;try {java.lang.ClassLoader cl = appContext.getClassLoader();//创建Activityactivity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);}try {if (activity != null) {//完成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;}//设置activity的主题int theme = r.activityInfo.getThemeResource();if (theme != 0) {activity.setTheme(theme);}//调用activity的onCreate方法if (r.isPersistable()) {mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);} else {mInstrumentation.callActivityOnCreate(activity, r.state);}}}return activity;
}

哇,终于看到onCreate方法了。稳住,还是一步步看看这段代码。

首先,创建了ContextImpl对象,ContextImpl可能有的朋友不知道是啥,ContextImpl继承自Context,其实就是我们平时用的上下文。有的同学可能表示,这不对啊,获取上下文明明获取的是Context对象。来一起跟随源码看看。

//Activity.java
Context mBase;@Override
public Executor getMainExecutor() {return mBase.getMainExecutor();
}@Override
public Context getApplicationContext() {return mBase.getApplicationContext();
}

这里可以看到,我们平时用的上下文就是这个mBase,那么找到这个mBase是啥就行了:

protected void attachBaseContext(Context base) {if (mBase != null) {throw new IllegalStateException("Base context already set");}mBase = base;
}//一层层往上找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);}}

这不就是刚才一开始performLaunchActivity方法里面的attach吗?太巧了,所以这个ContextImpl就是我们平时所用的上下文。

顺便看看attach还干了啥?新建了PhoneWindow,建立自己和Window的关联,并设置了setSoftInputMode等等。

ContextImpl创建完之后,会通过类加载器创建Activity的对象,然后设置好activity的主题,最后调用了activity的onCreate方法。

最后由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,如有需要获取资料文档的朋友,可以点击我GitHub免费领取!

惊呆了!女儿拿着小天才电话手表,问我Android启动流程!相关推荐

  1. andriod studio 启动service失败_惊呆了!女儿拿着小天才电话手表,问我Android启动流程!...

    阅读本文大概需要15分钟 作者:巨人大哥出处:https://blog.csdn.net/CHAMPION8888/article/details/108490500 首先,new一个女儿, var ...

  2. q1 login.php,实情使用曝光小天才电话手表q1和z1有啥区别?哪个好?老手解密诉说...

    这二款小天才电话手表q1和z1区别不是很大的哈,款式是差不多的,只是说小天才手表Q1更强一些,看个人喜欢吧,我自己用的是小天才手表Q1,款式是我喜欢的,多时尚的,整体看着相当大气,上档次,质感也蛮不错 ...

  3. 网易云信携手小天才电话手表 打造视频通话体验的行业标杆

    2018年第一季度,中国可穿戴设备市场出货量达到1200万台,同比增长 15.9%.处于市场发展初期的可穿戴设备能够保持这样的稳步增长,IDC明确指出,这主要源于4G儿童手表市场的迅猛增长.毋庸置疑, ...

  4. 堂堂小米手表竟比不上小天才电话手表?不支持视频和拍照...

    近日,小米手表疯狂刷存在感,为即将到来的小米新品发布会预热,此前,相继曝光了手表的宣传照,以及暗示了小米手表功能上的"不拘一格". 昨天,小米雷军在微博提问,什么才是真正的智能手表 ...

  5. 这4大决定性因素,让你明白孩子为什么只选择小天才电话手表

    在互联网时代的众多数码产品中,有一款产品是孩子们必不可少的,那就是儿童电话手表.儿童电话手表对于小孩子的重要性相当于成年人对手机的需求.有了电话手表,家长不仅可以跟孩子进行视频通话,还可以看到孩子的具 ...

  6. 艾永亮:是亲情还是束缚?小天才电话手表是怎么火爆市场的?

    小霸王游戏机.步步高点读机.步步高DVD.ViVo手机.拼多多.OPPO手机,这六款产品都是大家耳熟能详的存在. 但是他们并不属于超级产品,今天我要给大家讲一款超级产品--小天才电话手表! 小天才电话 ...

  7. 超级产品:小天才电话手表卖的不是手表?那它卖什么?

    步步高企业凭借着小天才这款超级产品牢牢占据儿童消费市场第一位,其实早在小天才之前,步步高企业还有点读机.家教机.儿童平板等一系列产品,在教育电子产品行业内造就了一定的影响力. 时至今日,在小学校园内, ...

  8. 小天才z6官方禁用怎么关闭_小天才电话手表z6功能介绍

    小天才电话手表的型号多,家长们在选购时需要进行了解后在购买,小天才电话手表z6的特色是有翻转式炫酷外观,很受孩子的喜欢,小天才电话手表z6功能怎么样,西西小编来为大家介绍. 小天才电话手表z6功能介绍 ...

  9. 小天才电话手表d2和q1a哪个好

    小天才电话手表d2和q1a的屏幕尺寸基本相同,可以为用户带来很好的屏幕显示,还为用户超高色域 小天才电话手表d2更多使用感受和评价:http://www.adiannao.cn/dn 小天才电话手表q ...

最新文章

  1. ehcache 手动刷新缓存_【第 21 期】一个架构师的缓存修炼之路
  2. Ajax发送formdata数据,SpringMVC后台处理
  3. Id都是“とつくとき”这样的怎么爬,在线等,急
  4. linux的基础知识——本地套接字
  5. 【Nginx那些事】nginx配置实例(二)负载均衡
  6. 对等通信_新的通信技术如何影响对等参与
  7. 统计学(检验、分布)的 python(numpy/pandas/scipy) 实现
  8. asp.net中用LinkButton取到gridview中当前行的ID值
  9. 立即释放.net下的com组件
  10. stosb, stosw, stosd 汇编指令
  11. Word引用参考文献
  12. 【Linux】文件及目录
  13. H5页面视觉效果设计技巧有哪些
  14. Libero逻辑分析仪的使用(基本篇)
  15. 手机通讯录式排序php,Android获取手机通讯录-根据排序方式进行
  16. bwa mem 报错处理:[mem_sam_pe] paired reads have different names
  17. UBound 函数 (Visual Basic)
  18. 小红书笔记下沉的方法和技巧
  19. phpstorm正则匹配清理被蠕虫病毒污染的html文件DropFileName = “svchost.exe” Ramnit
  20. ZigBee——在CC2530的ZStack中添加定时任务

热门文章

  1. 什么样的人才能做互联网产品经理【爬取拉勾网招聘信息】
  2. DDOS攻击的简单化解,追踪和防御
  3. unary operator expected解决方法
  4. Java 树结构 TreeNode 表格 工具类
  5. IntelliJ IDEA设置类注释和方法注释
  6. 前端设计 响应式设计_如何响应式设计
  7. 《现代交通报·信息周刊》面向全国招商
  8. WF4.0 基础篇 (十四) Delay 与WF4中的线程
  9. 小白学习web零基础,大佬给出全面建议,一步到位!
  10. 网站安全情况自查表怎么填?