App打包
APP打包封装 https://cloud.nextcli.com/app/package
App 应用打包使用说明
本服务可将任何网站 URL 打包为 Android 与 Windows 应用(iOS、Mac 版本正在开发中),并在应用内部集成了防屏蔽功能,使海内外用户均可自由访问目标网站,不受防火墙限制。
同时,您也可以在此生成并下载专用的 Android 与 Windows 防屏蔽 SDK,将其整合至您的 App,以便突破网络访问限制。以下示例以 X(推特) 为例,展示如何使用本功能。
一、APP打包
普通用户可选择打包Android APK、Windows EXE;
安卓AAR、Windows DLL 则是面向专业开发者的SDK。
APP安装在桌面上显示的图标;
Android推荐使用 512×512px 的图标以获得最佳效果;
Windows同样512×512px 的图标,也可以直接使用网站的favicon.ico链接。
1.2. 打包与下载
点击“打包”后,预计 1~5 分钟完成。需手动刷新查看进度。
打包产物下载链接有效期为 1小时,过期后需重新打包。
如果打包失败,请检查错误提示;如无法解决可联系客服。
1.3. 其他说明
新用户在创建第一个应用后,可获得 1年 的每月 1G 流量套餐。
2.1 流量加速
如果当月流量用完,需要再次购买套餐或流量包才能继续使用流量加速。
2.2 什么是加速白名单?
流量加速开启后,白名单中的域名会通过加速通道,消耗套餐流量;其他域名则使用普通网络。
创建应用后,系统会自动将应用主页链接的域名加入到加速白名单。
例如,若应用主页链接为 abc.x.com,则默认将 abc.x.com 添加到白名单。
您还可以手动添加更多域名到白名单,例如添加 123.x.com 、www.google.com,则该域名流量将走加速通道。
2.3 白名单规则
仅可添加域名(不能包含 http 或 https)。
若添加一级域名(如 x.com),其所有二级或子域名(如 m.x.com, www.x.com)皆自动纳入白名单。
若只添加二级域名(如 m.x.com),则不包含 x.com。
新用户创建第一个应用后,系统会免费提供 1年 的每月 1G 流量套餐。
您可多次购买同一套餐,后购买的套餐将覆盖之前的套餐,同时重置 App 的流量用量和到期时间。
⚠️注意,必须先拥有一个套餐,才能购买流量包。流量包的有效期与当月套餐同步。
四、开发者文档
完整示例可查看 GitHub 项目。
4.1 生成AAR
在创建应用时选择 AAR 作为目标产物,主页链接可先填后端 API 地址(后续可于白名单中修改)。
打包产物解压后将获得 checksum 文件与 xxx.aar 文件。
sdk调用前需要把checksum复制到app的内部存储文件夹下,因为sdk需要校验这个文件,然后初始化sdk,参考以下demo
// kotlin 示例
CoroutineScope(Dispatchers.IO).launch {
try {
moveAssetsFileToInternalStorage(context, "checksum")
// code=1 正常启动
// 这里sdk工作目录必须和checksum文件在同一个目录
val errCode = Safetunnel.newInstance(context.filesDir.absolutePath)
// code=1 正常启动
val startCode = Safetunnel.start()
// 获取代理端口,应用层流量转发到这个端口即可使用流量加速功能
val port = Safetunnel.getPort()
} catch (e: Exception) {
// log
}
// Android复制assets文件到内部存储
private suspend fun moveAssetsFileToInternalStorage(
context: Context,
fileName: String
): Boolean {
return withContext(Dispatchers.IO) {
var inputStream: InputStream? = null
try {
inputStream = context.assets.open(fileName)
// ⚠️注意:sdk工作目录必须和checksum文件在同一个目录
val outFile = File(context.filesDir, fileName).apply {
parentFile?.mkdirs()
}
if (!outFile.exists()) {
// 将文件内容从输入流复制到输出流
inputStream.copyTo(outFile.outputStream())
Log.d(
"TAG",
"File $fileName moved to internal storage: ${outFile.absolutePath}"
)
}
true
} catch (e: Exception) {
Log.e("TAG", "Failed to move file $fileName to internal storage: ${e.message}")
false
} finally {
// 关闭流
inputStream?.close()
}
}
}
// java 示例
new Thread(new Runnable() {
@Override
public void run() {
try {
// 将 assets 文件移动到内部存储
boolean isMoved = moveAssetsFileToInternalStorage(context, "checksum");
if (!isMoved) {
Log.e(TAG, "Failed to move checksum file.");
return;
}
// ⚠️注意:SDK 工作目录必须和 checksum 文件在同一个目录
int errCode = Safetunnel.newInstance(context.getFilesDir().getAbsolutePath());
if (errCode != 1) {
Log.e(TAG, "Safetunnel.newInstance failed with code: " + errCode);
return;
}
// 启动 Safetunnel
int startCode = Safetunnel.start();
if (startCode != 1) {
Log.e(TAG, "Safetunnel.start failed with code: " + startCode);
return;
}
// 获取代理端口,应用层流量转发到这个端口即可使用流量加速功能
int port = Safetunnel.getPort();
Log.d(TAG, "Proxy port: " + port);
} catch (Exception e) {
Log.e(TAG, "Exception occurred: " + e.getMessage());
}
}
}).start();
// Android复制assets文件到内部存储,java示例
public static boolean moveAssetsFileToInternalStorage(Context context, String fileName) {
InputStream inputStream = null;
try {
inputStream = context.getAssets().open(fileName);
// 注意:SDK工作目录必须和checksum文件在同一个目录
File outFile = new File(context.getFilesDir(), fileName);
File parentDir = outFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
if (!outFile.exists()) {
// 将文件内容从输入流复制到输出流
try (FileOutputStream outputStream = new FileOutputStream(outFile)) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
}
Log.d(TAG, "File " + fileName + " moved to internal storage: " + outFile.getAbsolutePath());
}
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to move file " + fileName + " to internal storage: " + e.getMessage());
return false;
} finally {
// 关闭输入流
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
Log.e(TAG, "Failed to close input stream: " + e.getMessage());
}
}
}
}
// C#复制assets文件到内部存储
private string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "com.safelink.test", "Cache");
// 导入dll
[DllImport("libcore.dll")]
private static extern int NewInstance(string dir);
[DllImport("libcore.dll")]
private static extern int Start();
[DllImport("libcore.dll")]
private static extern int GetPort();
private async void initSdk()
{
String filePath = Path.Combine(cachePath, "file");
if (!Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
if (!File.Exists(Path.Combine(filePath, "checksum")))
{
ExtractResourceToFile("pack://application:,,,/tmp/checksum", Path.Combine(filePath, "checksum"));
}
try
{
var ErrCode = NewInstance(filePath);
Debug.WriteLine("NewInstance ErrCode:"+ ErrCode);
if (ErrCode == 1)
{
ErrCode=Start();
Debug.WriteLine("Start ErrCode:" + ErrCode);
}
}
catch (Exception ex)
{
Debug.WriteLine("initSdk error:" + ex.ToString());
}
}
// 复制文件到内部存储
public static void ExtractResourceToFile(string resourceName, string filepath)
{
try
{
// 获取当前程序集
StreamResourceInfo info = Application.GetContentStream(new Uri(resourceName));
// 使用资源名称从程序集获取资源流
using (Stream resourceStream = info.Stream)
{
if (resourceStream == null)
{
throw new InvalidOperationException("Could not find resource: " + resourceName);
}
// 创建目标文件路径的目录
Directory.CreateDirectory(Path.GetDirectoryName(filepath));
// 创建文件流并写入从程序集提取的数据
using (FileStream fileStream = new FileStream(filepath, FileMode.Create, FileAccess.Write))
{
resourceStream.CopyTo(fileStream);
}
}
}
catch (Exception ex)
{
MessageBox.Show("Error extracting resource: " + ex.Message);
}
}
应用层示例,这里以网络请求为例,更多使用示例请查看demo;
# Android kotlin 示例
val proxy =
Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", Safetunnel.getPort()))
okHttpClient.proxy(proxy)
# Windows C# 示例
// 创建一个 HttpClientHandler,并设置代理信息
var httpClientHandler = new HttpClientHandler
{
Proxy = new WebProxy("127.0.0.1:"+GetPort()),
UseProxy = true,
};
// 使用 HttpClientHandler 创建 HttpClient
using (var httpClient = new HttpClient(httpClientHandler))
{
// 发送 HTTP GET 请求
HttpResponseMessage response = await httpClient.GetAsync("http://example.com");
if (response.IsSuccessStatusCode)
{
// 处理成功响应
string content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
else
{
// 处理错误响应
Console.WriteLine($"HTTP Error: {response.StatusCode}");
}
}
参考链接
• Android 开发者文档(英文)
• Windows 应用打包(Microsoft 官方文档)