[Flutter翻译]在没有Mac的情况下开发和调试iOS的Flutter应用

938 阅读5分钟

本文由 简悦SimpRead 转码,原文地址 medium.com

可以在你的iOS设备上热重载和热重启你的Flutter应用程序,而不必u......。

image.png

更新。Flutter 1.17.0现在使用XCode而不是libimobiledevice来列出设备。目前,你可以降级到Flutter 1.12.13+hotfix.9,或者使用Codemagic的新功能,允许你远程控制一个Mac虚拟机,并在那里运行一个虚拟设备(blog.codemagic.io/remote-acce…

可以在你的iOS设备上热重载和热重启你的Flutter应用程序,而不需要使用Mac,就像你在Android设备上一样!如何做?下面是方法。

在您的iOS设备上运行您的应用程序的调试版

你可以用Mac(或Hackintosh,或VM)来做,但由于我们无法使用macOS机器,我们可以通过Codemagic或Travis CI来远程使用--完全免费 (只要你的项目是在GitHub、Bitbucket或GitLab仓库中)。

推荐使用。用Codemagic构建应用程序

推荐使用Codemagic,因为它需要的设置较少,而且构建速度相当快

首先,创建一个账户或登录到codemagic.io。

然后,点击你的应用程序旁边的设置(齿轮)图标。向下滚动并点击 "构建"。确保模式被设置为调试,并在平台的构建下选择iOS

默认情况下,Codemagic会测试你的应用程序。除非你想使用这个功能,否则请禁用它。感谢Near Cz的反馈!(Thanks to Near Cz for the feedback!)

之后,构建应用程序(开始你的第一次构建)。

新:现在Codemagic让你远程控制它用来构建应用程序的macOS虚拟机。在那里,你可以运行一个模拟器,测试你的应用程序,并从XCode配置它。见 blog.codemagic.io/remote-acce…

Codemagic将通过电子邮件给你发送一个.app文件。

重命名它,使其以.zip结尾。

提取它,你会得到一个名为Runner.app的文件夹。

创建一个名为 "Payload "的文件夹,将 "Runner.app "放在那里。

最后,压缩名为 "Payload "的文件夹 - 这将是你的IPA文件(你可以把它重命名为".ipa")。

要继续,请跳到[安装和运行应用程序](#805c)。

替代方案。用Travis CI构建应用程序

注意:Travis CI的免费版本只支持公共GitHub仓库。

你需要在Travis CI上创建一个账户,让它访问你的GitHub账户。

然后,在你项目的根目录下创建.travis.yml,内容如下。

os: osx 
language: generic 
before_script: 
 - brew update 
 - brew install --HEAD usbmuxd 
 - brew unlink usbmuxd 
 - brew link usbmuxd 
 - brew install --HEAD libimobiledevice 
 - brew install ideviceinstaller 
 - brew install ios-deploy 
 - git clone https://github.com/flutter/flutter.git -b beta --depth 1 
script: 
 - flutter/bin/flutter build ios --debug --no-codesign
cache: 
  directories: 
  - $HOME/.pub-cache
before_deploy: 
  - pushd build/ios/iphoneos 
  - mkdir Payload 
  - cd Payload 
  - ln -s ../Runner.app 
  - cd .. 
  - zip -r app.ipa Payload 
  - popd

你需要安装Travis的命令行工具:(如果下面的命令没有找到,请从rubygems.org/安装RubyGems)

$ gem install travis # Makes sure to have the Travis CLI installed

然后,设置部署到GitHub版本。

$ cd your_project
$ travis setup releases

当提示上传文件时,输入build/ios/iphoneos/app.ipa

确保你的".travis.yml "像这样结束。

deploy:
  provider: releases
  api_key:
    secure: #your api key will be here
  file: build/ios/iphoneos/app.ipa
  skip_cleanup: true #important or your built app would be deleted
  on:
    repo: #your repo will be here

现在,把修改推送到你的GitHub repo。

$ git add .travis.yml
$ git commit
$ git push

现在,你的应用程序将被构建,并被添加到你的GitHub发布中。等待一下,让Travis注意到你已经添加了.travis.yml;当它开始时,你可以跟踪你的构建进度。当它完成后,从你的项目的GitHub版本中下载你的app.ipa

注意:如果你不想发布你的.ipa文件,你可以在deploy:中添加draft: true,它将作为草稿发布在GitHub上。

安装和运行应用程序

要安装IPA文件,你需要从下面的链接下载Cydia Impactor。

如果你运行的是Windows,你需要先安装iTunes。(确保安装非微软商店的版本:在 "寻找其他版本?"选择Windows,然后向上滚动并下载)。

编辑:本文的前一个版本建议绕过iTunes的安装来安装驱动程序。很明显,这并不奏效。(感谢Andreas Opferkuch指出了这一点)

更新:Cydia Impactor目前似乎已经坏了。如果你有一个开发者账户,你可以按照Weisser Zwerg的教程(感谢分享!)来签署和安装该应用程序。如果你有一个越狱的设备,你可以使用AppSync unifiedideviceinstaller。还有一些替代Cydia Impactor的工具,但我不能保证它们没有恶意软件,所以使用它们的风险由你自己承担。

  • 将你的iOS设备插入你的电脑
  • 运行Impactor的可执行文件
  • 点击 "Xcode",然后 "撤销证书"
  • 现在,将 "app.ipa"(或你创建的ZIP文件)拖到Cydia Impactor窗口(或使用设备>安装包......)。
  • 你会被提示输入你的苹果ID电子邮件和密码。如果它受到双因素认证的保护,或者你得到一个错误信息,你将需要创建一个应用程序特定的密码,并使用它代替你的密码。见support.apple.com/en-us/HT204…(感谢Near Cz的指出!)
  • 在你的iOS设备上,进入设置>通用>设备管理(或配置文件和设备管理),点击你刚才用来登录的Apple ID电子邮件,然后信任
  • 万岁! 我们的应用程序现在已经安装完毕。

准备好你的机器

安装依赖项

确保 "libimobiledevice "和 "ideviceinstaller "已经安装(后者并不使用,但如果不存在,Flutter会抱怨)。

在Windows上:

之前的链接已经没有二进制文件了,感谢Andreas Opferkuch分享的替代链接!

这里下载imobiledevice二进制文件。你还需要一个"which"二进制文件,你可以下载here。将这些二进制文件添加到flutterbin(或任何其他位置,但要确保它在你的 **PATH**)。

在Arch/Manjaro/Antergos上:

$ pacaur -S libimobiledevice ideviceinstaller-git

在Ubuntu/Mint/etc上:

$ sudo apt install libimobiledevice6 ideviceinstaller

你可能还需要安装这些软件包(感谢João Matheus)。

$ sudo apt install libimobiledevice-utils libusbmuxd-tools

修改Flutter

当检测设备时,Flutter在搜索iOS设备之前会检查你是否在运行macOS。它还会检查XCode。让我们删除这些检查。

注意:它在当前的稳定版(1.17.0)中无法工作。同时,你可以用Flutter 1.12.13+hotfix.9代替。

显然,现在Flutter使用XCode utilities而不是libimobiledevice,所以补丁不会那么简单。在此期间,请使用旧版本,或帮助贡献补丁或其他解决方案(fork,或撤消特定提交的修改的方法)。一旦这个问题得到解决,我就会更新这篇文章。

diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 9544a18f7..d72e28bd4 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:io';
+
 import 'package:meta/meta.dart';
 
 import 'base/context.dart';
@@ -256,17 +258,15 @@ class CachedArtifacts extends Artifacts {
       case Artifact.idevicescreenshot:
       case Artifact.idevicesyslog:
       case Artifact.idevicename:
-        final String artifactFileName = _artifactToFileName(artifact);
-        return cache.getArtifactDirectory('libimobiledevice').childFile(artifactFileName).path;
       case Artifact.iosDeploy:
-        final String artifactFileName = _artifactToFileName(artifact);
-        return cache.getArtifactDirectory('ios-deploy').childFile(artifactFileName).path;
       case Artifact.ideviceinstaller:
-        final String artifactFileName = _artifactToFileName(artifact);
-        return cache.getArtifactDirectory('ideviceinstaller').childFile(artifactFileName).path;
       case Artifact.iproxy:
         final String artifactFileName = _artifactToFileName(artifact);
-        return cache.getArtifactDirectory('usbmuxd').childFile(artifactFileName).path;
+        final ProcessResult whichResult = Process.runSync('which', <String>[artifactFileName]);
+        if (whichResult.exitCode != 0) {
+          throw UnsupportedError('$artifactFileName is not in the path, please install the dependencies following the article, and add it to the PATH if necessary');
+        }
+        return whichResult.stdout.toString().trimRight();
       default:
         return _getHostArtifactPath(artifact, platform, mode);
     }
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index b11a627b7..23659c95c 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -106,7 +106,7 @@ class IOSDevices extends PollingDeviceDiscovery {
   IOSDevices() : super('iOS devices');
 
   @override
-  bool get supportsPlatform => platform.isMacOS;
+  bool get supportsPlatform => true;
 
   @override
   bool get canListAnything => iosWorkflow.canListDevices;
@@ -172,10 +172,8 @@ class IOSDevice extends Device {
   bool get supportsStartPaused => false;
 
   static Future<List<IOSDevice>> getAttachedDevices() async {
-    if (!platform.isMacOS) {
-      throw UnsupportedError('Control of iOS devices or simulators only supported on Mac OS.');
-    }
     if (!iMobileDevice.isInstalled) {
+      printError("libimobiledevice doesn't seem to be installed, reread the instalation steps or leave a comment in the article");
       return <IOSDevice>[];
     }
 
@@ -208,9 +206,6 @@ class IOSDevice extends Device {
       apps = await processUtils.run(
         <String>[_installerPath, '--list-apps'],
         throwOnError: true,
-        environment: Map<String, String>.fromEntries(
-          <MapEntry<String, String>>[cache.dyLdLibEntry],
-        ),
       );
     } on ProcessException {
       return false;
@@ -233,9 +228,6 @@ class IOSDevice extends Device {
       await processUtils.run(
         <String>[_installerPath, '-i', app.deviceBundlePath],
         throwOnError: true,
-        environment: Map<String, String>.fromEntries(
-          <MapEntry<String, String>>[cache.dyLdLibEntry],
-        ),
       );
       return true;
     } on ProcessException catch (error) {
@@ -250,9 +242,6 @@ class IOSDevice extends Device {
       await processUtils.run(
         <String>[_installerPath, '-U', app.id],
         throwOnError: true,
-        environment: Map<String, String>.fromEntries(
-          <MapEntry<String, String>>[cache.dyLdLibEntry],
-        ),
       );
       return true;
     } on ProcessException catch (error) {
@@ -707,9 +696,6 @@ class IOSDevicePortForwarder extends DevicePortForwarder {
           devicePort.toString(),
           device.id,
         ],
-        environment: Map<String, String>.fromEntries(
-          <MapEntry<String, String>>[cache.dyLdLibEntry],
-        ),
       );
       // TODO(ianh): This is a flakey race condition, https://github.com/libimobiledevice/libimobiledevice/issues/674
       connected = !await process.stdout.isEmpty.timeout(_kiProxyPortForwardTimeout, onTimeout: () => false);
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
index ac9630ba9..5913050d4 100644
--- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -17,7 +17,7 @@ class IOSWorkflow implements Workflow {
 
   // We need xcode (+simctl) to list simulator devices, and libimobiledevice to list real devices.
   @override
-  bool get canListDevices => xcode.isInstalledAndMeetsVersionCheck && xcode.isSimctlInstalled;
+  bool get canListDevices => true;
 
   // We need xcode to launch simulator devices, and ideviceinstaller and ios-deploy
   // for real devices.
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index dd5597f62..bb2576520 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -97,7 +97,9 @@ class IMobileDevice {
         _ideviceinfoPath = artifacts.getArtifactPath(Artifact.ideviceinfo, platform: TargetPlatform.ios),
         _idevicenamePath = artifacts.getArtifactPath(Artifact.idevicename, platform: TargetPlatform.ios),
         _idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios),
-        _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios);
+        _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios) {
+          assert(artifacts.runtimeType == CachedArtifacts);
+        }
 
   final String _ideviceIdPath;
   final String _ideviceinfoPath;
@@ -111,9 +113,6 @@ class IMobileDevice {
         _ideviceIdPath,
         '-h',
       ],
-      environment: Map<String, String>.fromEntries(
-        <MapEntry<String, String>>[cache.dyLdLibEntry]
-      ),
     );
     return _isInstalled;
   }
@@ -176,11 +175,12 @@ class IMobileDevice {
           _ideviceIdPath,
           '-l',
         ],
-        environment: Map<String, String>.fromEntries(
-          <MapEntry<String, String>>[cache.dyLdLibEntry]
-        ),
       );
       if (result.exitCode != 0) {
+        if (result.stderr.toString().contains('ERROR: Unable to retrieve device list!')) {
+          // idevice_id might throw this error if there are no connected devices
+          return '';
+        }
         throw ToolExit('idevice_id returned an error:\n${result.stderr}');
       }
       return result.stdout as String;
@@ -199,9 +199,6 @@ class IMobileDevice {
           '-k',
           key,
         ],
-        environment: Map<String, String>.fromEntries(
-          <MapEntry<String, String>>[cache.dyLdLibEntry]
-        ),
       );
       final String stdout = result.stdout as String;
       final String stderr = result.stderr as String;
@@ -239,9 +236,6 @@ class IMobileDevice {
         '-u',
         deviceID,
       ],
-      environment: Map<String, String>.fromEntries(
-        <MapEntry<String, String>>[cache.dyLdLibEntry]
-      ),
     );
   }
 
@@ -253,9 +247,6 @@ class IMobileDevice {
         outputFile.path,
       ],
       throwOnError: true,
-      environment: Map<String, String>.fromEntries(
-        <MapEntry<String, String>>[cache.dyLdLibEntry]
-      ),
     );
   }
 }

(我们将把这些修改应用于Flutter)

把这个文件下载到你的flutter文件夹中(点击 "查看原始文件 "并按Ctrl+S)。然后应用它。(这是在4月底到5月初用稳定分支测试的)

$ cd flutter
$ git apply ios.diff

然后重建Flutter工具。

Linux:

$ bin/cache/dart-sdk/bin/dart --snapshot=./bin/cache/flutter_tools.snapshot --packages=./packages/flutter_tools/.packages ./packages/flutter_tools/bin/flutter_tools.dart

Windows:

bin\cache\dart-sdk\bin\dart --snapshot=.\bin\cache\flutter_tools.snapshot --packages=.\packages\flutter_tools\.packages .\packages\flutter_toolsbin\flutter_tools.dart

确保在你的项目里有一个名为build的文件夹。如果不存在,就创建它!

好戏来了!

使用IDE(VS Code)。

  • 打开命令调色板(Ctrl+Shift+P),并选择 "调试。附加到Flutter进程"
  • 打开应用程序,并等待Flutter同步文件。
  • 你现在可以像往常一样热重载和热重启了!

提示:如果热重载不起作用,请先热重启!

使用命令行。

  • 运行 "flutter attach"。这将等待应用程序启动并开始调试,就像我们使用flutter run一样。
  • 打开应用程序,并等待Flutter同步文件。
  • 你现在可以按r热重载,按R热重启!

学分

感谢Yegor Jbanov的帖子。

有任何问题或反馈?请留下评论!

如果你喜欢这个,不要忘了拍手,这将是极大的赞赏!


www.deepl.com 翻译