zoukankan      html  css  js  c++  java
  • Task

    一、Task是任务,不是线程,但是执行的时候是需要线程;任务跟线程不是一对一的关系,比如开3个任务并不是说会开3个线程

    在上一篇并行编程_Parallel文章中,反编译看了下源码Parallel.Invoke,会创建与调用的action[]数目一致的System.Threading.Tasks.Task的实例;Parallel.For、Parallel.ForEach的循环迭代的并行执行,也会创建Task实例

    对于构造函数,属性,方法介绍看微软这篇msdn文档:https://msdn.microsoft.com/zh-cn/library/system.threading.tasks.task(v=vs.110).aspx

    二、Task示例

    1.0 创建和执行任务

     1 #region 创建和执行任务
     2             // Task.Run
     3             Task task = Task.Run(() =>
     4                {
     5                    // Just loop.
     6                    int ctr = 0;
     7                    for (ctr = 0; ctr <= 1000000; ctr++) { }
     8                    Console.WriteLine("Finished {0} loop iterations", ctr);
     9                });
    10             Console.WriteLine("可能接着执行到这儿");
    11             task.Wait();
    12             Console.ReadKey();
    13 
    14 
    15            //Task.Factory.StartNew
    16              Task task = Task.Factory.StartNew(() =>
    17             {
    18                 // Just loop.
    19                 int ctr = 0;
    20                 for (ctr = 0; ctr <= 1000000; ctr++){ }
    21                 Console.WriteLine("Finished {0} loop iterations",ctr);
    22             });
    23             task.Wait();
    24             Console.ReadKey();
    25             #endregion 创建和执行任务
    View Code

    2.0等待一个或多个任务完成,还有 ContinueWhenAll 方法等待一组方法全部完成在继续工作;

     1 #region 等待一个或多个任务完成
     2             //1.0Task.Wait
     3             Task taskA = Task.Run(() => { Thread.Sleep(2000); });
     4             Console.WriteLine("taskA Status: {0}", taskA.Status);//WaitingToRun
     5             try
     6             {
     7                 taskA.Wait();
     8                 Console.WriteLine("taskA Status: {0}", taskA.Status);//RanToCompletion
     9             }
    10             catch (AggregateException)
    11             {
    12                 Console.WriteLine("Exception in taskA.");
    13 
    14             }
    15             Console.ReadKey();
    16 
    17 
    18 
    19           //2.0  Task.Wait(1000)
    20            Task taskA = Task.Run(() => { Thread.Sleep(3000);});
    21             try
    22             {
    23                 taskA.Wait(1000);  // Wait for 1 second.main thread continue
    24                 bool completed = taskA.IsCompleted;//false
    25                 Console.WriteLine("taskA Status: {0}", taskA.Status);
    26                 if (!completed)
    27                 {
    28                     Console.WriteLine("Time out before task A completed.");
    29                 }
    30             }
    31             catch (AggregateException)
    32             {
    33                 Console.WriteLine("Exception in taskA.");
    34 
    35             }
    36 
    37 
    38             //3.0Task.WaitAny,Task.WaitAll
    39              var tasks = new Task[3];
    40             var rnd = new Random();
    41             for (int i = 0; i < 3; i++)
    42             {
    43                 tasks[i] = Task.Run(() => { Thread.Sleep(rnd.Next(500, 3000)); });
    44             }
    45             try
    46             {
    47                 //等待哪个先完成就返回哪个任务的索引;
    48                 int index = Task.WaitAny(tasks);
    49                 Console.WriteLine("Task #{0} completed first.
    ", tasks[index].Id); //Task #1 completed first.
    50                 Console.WriteLine("Status of all tasks:");
    51                 foreach (var t in tasks)
    52                 {
    53                     //Task #3: Running
    54                     //Task #4: Running
    55                     //Task #1: RanToCompletion
    56                     Console.WriteLine("   Task #{0}: {1}", t.Id, t.Status);
    57                 }
    58                 Task.WaitAll(tasks);//等待所有任务完成
    59                 foreach (var t in tasks)
    60                 {
    61                     //Task #3: RanToCompletion
    62                     //Task #4: RanToCompletion
    63                     //Task #1: RanToCompletion
    64                     Console.WriteLine("   Task #{0}: {1}", t.Id, t.Status);
    65                 }
    66             }
    67             catch (AggregateException)
    68             {
    69                 Console.WriteLine("An exception occurred.");
    70             }
    71             Console.ReadKey();
    72 
    73 
    74 
    75 
    76  #endregion
    View Code

    3.0 异常处理示例, 参考:https://msdn.microsoft.com/zh-cn/library/dd997415(v=vs.110).aspx,try-catch,ContinueWith==>IsFaulted

    示例:其它可查看第二篇的TaskCompletionSource,有些:http://www.cnblogs.com/entclark/p/8047323.html

     1 // Create a cancellation token and cancel it.
     2             var source1 = new CancellationTokenSource();
     3             var token1 = source1.Token;
     4             source1.Cancel();
     5             // Create a cancellation token for later cancellation.
     6             var source2 = new CancellationTokenSource();
     7             var token2 = source2.Token;
     8 
     9             // Create a series of tasks that will complete, be cancelled, timeout, or throw an exception.
    10             Task[] tasks = new Task[12];
    11             for (int i = 0; i < 12; i++)
    12             {
    13                 switch (i % 4)
    14                 {
    15                     // Task should run to completion.
    16                     case 0:
    17                         tasks[i] = Task.Run(() => Thread.Sleep(2000));
    18                         break;
    19                     // Task should be set to canceled state.
    20                     case 1:
    21                         tasks[i] = Task.Run(() => Thread.Sleep(2000),
    22                                  token1);
    23                         break;
    24 
    25                     case 2:
    26                         // Task should throw an exception.
    27                         tasks[i] = Task.Run(() => { throw new NotSupportedException(); });
    28                         break;
    29 
    30                     case 3:
    31                         // Task should examine cancellation token.
    32                         tasks[i] = Task.Run(() =>
    33                         {
    34                             Thread.Sleep(2000);
    35                             if (token2.IsCancellationRequested)
    36                                 token2.ThrowIfCancellationRequested();
    37                             Thread.Sleep(500);
    38                         }, token2);
    39                         break;
    40                 }
    41             }
    42             Thread.Sleep(250);
    43             source2.Cancel();
    44 
    45             try
    46             {
    47                 Task.WaitAll(tasks);
    48             }
    49             catch (AggregateException ae)//一般使用累加异常,不用出现第一个异常就跳到这里面,影响其他任务继续运行
    50             {
    51                 Console.WriteLine("One or more exceptions occurred:");
    52                 foreach (var ex in ae.InnerExceptions)
    53                     Console.WriteLine("   {0}: {1}", ex.GetType().Name, ex.Message);
    54             }
    55 
    56             Console.WriteLine("
    Status of tasks:");
    57             foreach (var t in tasks)
    58             {
    59                 Console.WriteLine("   Task #{0}: {1}", t.Id, t.Status);
    60                 if (t.Exception != null)
    61                 {
    62                     foreach (var ex in t.Exception.InnerExceptions)
    63                         Console.WriteLine("      {0}: {1}", ex.GetType().Name,
    64                                           ex.Message);
    65                 }
    66             }
    67             Console.ReadKey();
    View Code

    4.0 任务和区域性

     1 //下面的示例创建并执行四项任务。 三个任务执行 Action< T > 委托名为 action, ,这样便可以接受类型的参数 Object。
     2             //任务 t1 实例化时通过调用任务类构造函数,但通过调用会启动其 Start() 方法仅在任务后的 t2 已启动。
     3             //任务 t2 会实例化并通过调用单个方法调用中启动 TaskFactory.StartNew(Action<Object>, Object) 方法。
     4             //任务 t3 会实例化并通过调用单个方法调用中启动 Run(Action) 方法。
     5             //任务 t4 上同步执行主线程通过调用 RunSynchronously() 方法。
     6             Action <object> action = (object obj) =>
     7             {
     8                 Console.WriteLine("Task={0}, obj={1}, Thread={2}",
     9                 Task.CurrentId, obj,
    10                 Thread.CurrentThread.ManagedThreadId);
    11             };
    12 
    13             // Create a task but do not start it.
    14             Task t1 = new Task(action, "alpha");
    15 
    16             // Construct a started task
    17             Task t2 = Task.Factory.StartNew(action, "beta");
    18             // Block the main thread to demonstate that t2 is executing
    19             t2.Wait();
    20 
    21             // Launch t1 
    22             t1.Start();
    23             Console.WriteLine("t1 has been launched. (Main Thread={0})",
    24                               Thread.CurrentThread.ManagedThreadId);
    25             // Wait for the task to finish.
    26             t1.Wait();
    27 
    28             // Construct a started task using Task.Run.
    29             String taskData = "delta";
    30             Task t3 = Task.Run(() => {
    31                 Console.WriteLine("Task={0}, obj={1}, Thread={2}",
    32                                   Task.CurrentId, taskData,
    33                                    Thread.CurrentThread.ManagedThreadId);
    34             });
    35             // Wait for the task to finish.
    36             t3.Wait();
    37 
    38             // Construct an unstarted task
    39             Task t4 = new Task(action, "gamma");
    40             // Run it synchronously
    41             t4.RunSynchronously();
    42             // Although the task was run synchronously, it is a good practice
    43             // to wait for it in the event exceptions were thrown by the task.
    44             t4.Wait();
    45             Console.ReadKey();
    View Code

    三、Task<TResult>示例,一个可以返回值的异步操作。获取结果的时候必须等待操作完成,t.Result

    //备注:Task<TResult> 类的表示单个操作通常返回一个值,并以异步方式执行。 Task<TResult> 对象是一种.NET Framework 4 中引入的第一个主要的组件。
    //因为由执行工作 Task<TResult> 对象通常以异步方式执行在线程池线程上而不是以同步方式在主应用程序线程,您可以使用 Status 属性,以及 IsCanceled, 
    //IsCompleted, ,和 IsFaulted 属性,以确定任务的状态。 大多数情况下,lambda 表达式用于指定的任务是执行的工作。
    //Task<TResult> 可以多种方式创建实例。 最常用的方法,是调用静态 Task.Run<TResult>(Func<TResult>) 或 Task.Run<TResult>(Func<TResult>, CancellationToken) 
    //方法。 这些方法提供简单的方法来启动任务,通过使用默认值,而无需获取其他参数。
    
     1 var t = Task.Run(() =>
     2             {
     3                 // Just loop.
     4                 int max = 1000000;
     5                 int ctr = 0;
     6                 for (ctr = 0; ctr <= max; ctr++)
     7                 {
     8                     if (ctr == max / 2 && DateTime.Now.Hour <= 12)
     9                     {
    10                         ctr++;
    11                         break;
    12                     }
    13                 }
    14                 Thread.Sleep(2000);
    15                 return ctr;
    16 
    17             });
    18             //主线程继续
    19             Console.WriteLine("....");
    20             //必须等待输出结果,阻塞主线程
    21             Console.WriteLine("Finished {0:N0} iterations.",t.Result);
    22             //主线程继续
    23             Console.WriteLine("....");
    24             Console.ReadKey();
    25 
    26 Task
    View Code
     1 var t = Task<int>.Factory.StartNew(() =>
     2             {
     3                 // Just loop.
     4                 int max = 1000000;
     5                 int ctr = 0;
     6                 for (ctr = 0; ctr <= max; ctr++)
     7                 {
     8                     if (ctr == max / 2 && DateTime.Now.Hour <= 12)
     9                     {
    10                         ctr++;
    11                         break;
    12                     }
    13                 }
    14                 Thread.Sleep(2000);
    15                 return ctr;
    16             });
    17             //主线程继续
    18             Console.WriteLine("....");
    19             //必须等待输出结果,阻塞主线程
    20             Console.WriteLine("Finished {0:N0} iterations.", t.Result);
    21             //主线程继续
    22             Console.WriteLine("....");
    23             Console.ReadKey();
    24 
    25 TaskFactory
    View Code
     1 //一次使用的方法,使用匿名方法或者委托或者lambda代替,多次只在本方法体中使用就用委托
     2             Func<double, double> doComputation = (start) =>
     3             {
     4                 Double sum = 0;
     5                 for (var value = start; value <= start + 10; value += .1)
     6                     sum += value;
     7 
     8                 return sum;
     9             };
    10             Task<double>[] taskArray = 
    11             {
    12                 Task<double>.Factory.StartNew(() => doComputation(1.0)),
    13                 Task<double>.Factory.StartNew(() => doComputation(100.0)),
    14                 Task<double>.Factory.StartNew(() => doComputation(1000.0))
    15             };
    16             var results = new double[taskArray.Length];
    17             double sum2 = 0.0;
    18             for (int i = 0; i < taskArray.Length; i++)
    19             {
    20                 results[i] = taskArray[i].Result;
    21                 Console.Write("{0:N1} {1}", results[i],
    22                                   i == taskArray.Length - 1 ? "= " : "+ ");
    23                 sum2 += results[i];
    24             }
    25             Console.WriteLine("{0:N1}", sum2);
    26             Console.ReadKey();
    View Code

    四、创建任务延续

     //使用 Task.ContinueWith 和 Task<TResult>.ContinueWith 方法可以指定在先行任务完成时要启动的任务。 延续任务的委托已传递了对先行任务的引用,因此它可以检查先行任务的状态,
    并通过检索 Task<TResult>.Result 属性的值将先行任务的输出用作延续任务的输入。可以直接在任务上ContinueWith或者用任务实例ContinueWith:
     1 var displayData = Task.Factory.StartNew(() =>
     2             {
     3                 Random rnd = new Random();
     4                 int[] values = new int[100];
     5                 for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
     6                     values[ctr] = rnd.Next();
     7                 return values;
     8             }).ContinueWith((x) =>
     9                      {
    10                          int n = x.Result.Length;
    11                          long sum = 0;
    12                          double mean;
    13 
    14                          for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
    15                              sum += x.Result[ctr];
    16 
    17                          mean = sum / (double)n;
    18                          return Tuple.Create(n, sum, mean);
    19                      }).ContinueWith((x) =>
    20                      {
    21                          return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
    22                                               x.Result.Item1, x.Result.Item2,
    23                                               x.Result.Item3);
    24                      });
    25             Console.WriteLine(displayData.Result);
    View Code
       //创建一个任务,在数组中的任务之一通过调用完成后开始 ContinueWhenAny 方法。
       //创建一个任务,在数组中的所有任务已都完成通过调用开始 ContinueWhenAll 方法。比较常用
    
     Task<string[]>[] tasks = new Task<string[]>[2];
                string docsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                tasks[0] = Task<string[]>.Factory.StartNew(() => Directory.GetFiles(docsDirectory));
                tasks[1] = Task<string[]>.Factory.StartNew(() => Directory.GetDirectories(docsDirectory));
                Task.Factory.ContinueWhenAll(tasks, completedTasks =>
                {
                    Console.WriteLine("{0} contains: ", docsDirectory);
                    Console.WriteLine("   {0} subdirectories", tasks[1].Result.Length);
                    Console.WriteLine("   {0} files", tasks[0].Result.Length);
                });
                Console.ReadKey();
    View Code

    五、创建子任务

    1、分离的主任务:如果在任务中运行的用户代码创建一个新任务,且未指定 AttachedToParent 选项,则该新任务不采用任何特殊方式与父任务同步。 这种不同步的任务类型称为“分离的嵌套任务”或“分离的子任务”;请注意,默认父任务不会等待分离子任务完成,因为跟我们在主线程中开一个任务一样,不等待宿主线程;

     1    var outer = Task.Factory.StartNew(() =>
     2             {
     3                 Console.WriteLine("Outer task beginning.");
     4 
     5                 var child = Task.Factory.StartNew(() =>
     6                 {
     7                     Thread.SpinWait(5000000);
     8                     Console.WriteLine("Detached task completed.");
     9                 });
    10               
    11             });
    12 
    13             outer.Wait();
    14             Console.WriteLine("Outer task completed.");
    15             Console.ReadKey();
    16   // The example displays the following output:
    17             //    Outer task beginning.
    18             //    Outer task completed.
    19             //    Detached task completed.
    View Code

    2、创建不分离的子任务(父任务等待子任务,创建或者调用静态方法开启任务时可以指定TaskCreationOptions.AttachedToParent附加到父类)

     1   var parent = Task.Factory.StartNew(() =>
     2             {
     3                 Console.WriteLine("Parent task beginning.");
     4                 for (int ctr = 0; ctr < 10; ctr++)
     5                 {
     6                     int taskNo = ctr;
     7                     Task.Factory.StartNew((x) =>
     8                     {
     9                         Thread.SpinWait(5000000);
    10                         Console.WriteLine("Attached child #{0} completed.", x);
    11                     },taskNo,TaskCreationOptions.AttachedToParent);
    12                 }
    13             });
    14 
    15             parent.Wait();
    16             Console.WriteLine("Parent task completed.");
    17             Console.ReadKey();
    18             // The example displays output like the following:
    19             //       Parent task beginning.
    20             //       Attached child #9 completed.
    21             //       Attached child #0 completed.
    22             //       Attached child #8 completed.
    23             //       Attached child #1 completed.
    24             //       Attached child #7 completed.
    25             //       Attached child #2 completed.
    26             //       Attached child #6 completed.
    27             //       Attached child #3 completed.
    28             //       Attached child #5 completed.
    29             //       Attached child #4 completed.
    30             //       Parent task completed.
    View Code

    六、组合任务介绍

     1   // Task.WhenAll
     2             //Task.WhenAll 方法异步等待多个 Task 或 Task<TResult> 对象完成。 通过它提供的重载版本可以等待非均匀任务组。 例如,你可以等待多个 Task 和 Task<TResult> 对象在一个方法调用中完成。
     3             //Task.WhenAny
     4             //Task.WhenAny 方法异步等待多个 Task 或 Task<TResult> 对象中的一个完成。 与在 Task.WhenAll 方法中一样,该方法提供重载版本,让你能等待非均匀任务组。 WhenAny 方法在下列情境中尤其有用。
     5             //冗余运算。 请考虑可以用多种方式执行的算法或运算。 你可使用 WhenAny 方法来选择先完成的运算,然后取消剩余的运算。
     6             //交叉运算。 你可启动必须全部完成的多项运算,并使用 WhenAny 方法在每项运算完成时处理结果。 在一项运算完成后,可以启动一个或多个其他任务。
     7             //受限制的运算。 你可使用 WhenAny 方法通过限制并发运算的数量来扩展前面的情境。
     8             //过期的运算。 你可使用 WhenAny 方法在一个或多个任务与特定时间后完成的任务(例如 Delay 方法返回的任务)间进行选择。 下节描述了 Delay 方法。
     9             //Task.Delay
    10             //Task.Delay 方法将生成在指定时间后完成的 Task 对象。 你可使用此方法来生成偶尔轮询数据的循环,引入超时,将对用户输入的处理延迟预定的一段时间等。
    11             //Task(T).FromResult
    12             //通过使用 Task.FromResult<TResult> 方法,你可以创建包含预计算结果的 Task<TResult> 对象。 执行返回 Task<TResult> 对象的异步运算,且已计算该 Task<TResult> 对象的结果时,此方法将十分有用。 对于使用 FromResult<TResult> 来检索缓存中包含的异步下载操作结果,可参考这篇文章:https://msdn.microsoft.com/zh-cn/library/hh228607(v=vs.110).aspx
    View Code

     七、取消任务

     1 var tokenSource2 = new CancellationTokenSource();
     2             CancellationToken ct = tokenSource2.Token;
     3 
     4             var task = Task.Factory.StartNew(() =>
     5             {
     6 
     7                 // Were we already canceled?
     8                 ct.ThrowIfCancellationRequested();
     9 
    10                 bool moreToDo = true;
    11                 while (moreToDo)
    12                 {
    13                     // Poll on this property if you have to do
    14                     // other cleanup before throwing.
    15                     if (ct.IsCancellationRequested)
    16                     {
    17                         // Clean up here, then...
    18                         ct.ThrowIfCancellationRequested();
    19                     }
    20 
    21                 }
    22             }, tokenSource2.Token); // Pass same token to StartNew.
    23 
    24             tokenSource2.Cancel();
    25 
    26             // Just continue on this thread, or Wait/WaitAll with try-catch:
    27             try
    28             {
    29                 task.Wait();
    30             }
    31             catch (AggregateException e)
    32             {
    33                 foreach (var v in e.InnerExceptions)
    34                     Console.WriteLine(e.Message + " " + v.Message);
    35             }
    36             finally
    37             {
    38                 tokenSource2.Dispose();
    39             }
    40 
    41             Console.ReadKey();
    View Code

    八、注意点

    1、闭包问题,仅捕获最终值,而不是它每次迭代后更改的值;

     1  Task[] taskArray = new Task[10];
     2             for (int i = 0; i < taskArray.Length; i++)
     3             {
     4                 taskArray[i] = Task.Factory.StartNew((Object obj) =>
     5                 {
     6                     var data = new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks };
     7                     data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
     8                     Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
     9                                       data.Name, data.CreationTime, data.ThreadNum);
    10                 },i);
    11             }
    12             Task.WaitAll(taskArray);
    13             Console.ReadKey();
    14 
    15  // The example displays output like the following:
    16             //Task #10 created at 636490370838847160 on thread #6.
    17             //Task #10 created at 636490370838857162 on thread #11.
    18             //Task #10 created at 636490370838857162 on thread #12.
    19             //Task #10 created at 636490370838857162 on thread #13.
    20             //Task #10 created at 636490370838867161 on thread #6.
    21             //Task #10 created at 636490370838957229 on thread #6.
    22             //Task #10 created at 636490370838957229 on thread #6.
    23             //Task #10 created at 636490370838957229 on thread #13.
    24             //Task #10 created at 636490370838947222 on thread #11.
    25             //Task #10 created at 636490370838947222 on thread #12
    View Code

    解决方案:通过使用构造函数向任务提供状态对象,可以在每次迭代时访问该值;不要引用外面的循环变量,应该使用传进去的

     1    // Create the task object by using an Action(Of Object) to pass in custom data
     2             // to the Task constructor. This is useful when you need to capture outer variables
     3             // from within a loop. 
     4             Task[] taskArray = new Task[10];
     5             for (int i = 0; i < taskArray.Length; i++)
     6             {
     7                 taskArray[i] = Task.Factory.StartNew((Object obj) =>
     8                 {
     9                     CustomData data = obj as CustomData;
    10                     if (data == null)
    11                         return;
    12 
    13                     data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
    14                     Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
    15                                      data.Name, data.CreationTime, data.ThreadNum);
    16                 },
    17                                                       new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
    18             }
    19             Task.WaitAll(taskArray);
    20             Console.ReadKey();
    View Code

    2、AsyncState  此状态作为参数传递给任务委托,并且可通过使用 Task.AsyncState 属性从任务对象访问。简单的说就是开始任务时传了委托的值,这个值可以在任务处理

       修改后可通过AsyncState 对象获取;对上例做下修改:

     1   Task[] taskArray = new Task[10];
     2             for (int i = 0; i < taskArray.Length; i++)
     3             {
     4                 taskArray[i] = Task.Factory.StartNew((Object obj) =>
     5                 {
     6                     CustomData data = obj as CustomData;
     7                     if (data == null)
     8                         return;
     9 
    10                     data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
    11                 },new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
    12             }
    13             Task.WaitAll(taskArray);
    14             foreach (var task in taskArray)
    15             {
    16                 var data = task.AsyncState as CustomData;
    17                 if (data!=null)
    18                 {
    19                       Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
    20                                       data.Name, data.CreationTime, data.ThreadNum);
    21                 }
    22 
    23             }
    24             Console.ReadKey();
    View Code
  • 相关阅读:
    浅析Vue Router中关于路由守卫的应用以及在全局导航守卫中检查元字段
    react-native 项目配置ts运行环境
    #mobx应用在rn项目中
    react-native TextInput输入框输入时关键字高亮
    react-native-亲测可用插件
    nodejs+express实现图片上传
    cordova图片上传,视频上传(上传多个图片,多个视频)
    cordova图片上传,视频上传(上传单个图片,单个视频)
    移动端如何测试(前端,cordova)
    在mac上将apk包安装到android手机上
  • 原文地址:https://www.cnblogs.com/entclark/p/8042736.html
Copyright © 2011-2022 走看看