Android AsyncTask 사용 및 소스 코드 분석
Android 에서 우 리 는 시간 이 걸 리 는 작업 을 해 야 합 니 다.이 작업 을 하위 스 레 드 에 두 고 진행 할 것 입 니 다.하위 스 레 드 작업 이 끝 난 후에 우 리 는 Handler 를 통 해 메 시 지 를 보 내 고 UI 에 업데이트 작업 을 하 라 고 통지 할 수 있다(구체 적 인 사용 과 원 리 는 볼 수 있다Android 의 메시지 메커니즘-Handler 의 작업 과정 이 글)물론 우리 의 조작 을 간소화 하기 위해 Android 1.5 이후 AsyncTask 류 를 제공 합 니 다.하위 스 레 드 처리 가 완 료 된 결 과 를 UI 스 레 드 로 되 돌 릴 수 있 습 니 다.그 후에 우 리 는 이러한 결과 에 따라 일련의 UI 작업 을 할 수 있 습 니 다.
AsyncTask 의 사용 방법
실제로 AsyncTask 내 부 는 Handler 와 스 레 드 탱크 를 한 번 밀봉 한 것 이다.그것 은 경량급 비동기 작업 클래스 로 배경 작업 이 온라인 풀 에서 진행 된다.그 후에 우 리 는 작업 수행 결 과 를 메 인 스 레 드 에 전달 할 수 있 습 니 다.이때 우 리 는 메 인 스 레 드 에서 UI 를 조작 할 수 있 습 니 다.
AsyncTask 는 추상 적 인 범 형 류 이기 때문에 우 리 는 하위 클래스 를 만들어 AsyncTask 의 추상 적 인 방법 을 실현 합 니 다.AsyncTask 에서 세 개의 범 형 매개 변 수 를 제 공 했 습 니 다.다음은 이 세 개의 범 형 매개 변 수 를 살 펴 보 겠 습 니 다.
1.Params:AsyncTask 를 실행 할 때 전달 하 는 매개 변 수 는 배경 스 레 드 에서 사 용 됩 니 다.
2.Progress:배경 작업 수행 진도 의 유형
3.Result:백 엔 드 퀘 스 트 수행 완료 후 돌아 오 는 결과 유형 입 니 다.
상기 세 개의 일반적인 매개 변 수 를 우리 가 사용 할 필요 가 없 을 때 Void 로 대체 할 수 있 습 니 다.Activity 라 이 프 사이클 과 유사 하 게 AsyncTask 에서 도 우리 에 게 몇 가지 방법 을 제공 해 주 었 습 니 다.우 리 는 이 몇 가지 방법 을 다시 써 서 전체 비동기 임 무 를 완성 합 니 다.우리 가 주로 사용 하 는 방법 은 네 가지 가 있다.
1.onPreExecute():이 방법 은 비동기 작업 전에 실 행 됩 니 다.주로 매개 변수 나 UI 초기 화 작업 에 사 용 됩 니 다.
2.doInBackground(Params...params):이 방법 은 온라인 풀 에서 실 행 됩 니 다.params 인 자 는 비동기 작업 시 입력 한 인 자 를 표시 합 니 다.이 방법 에서 우 리 는 Publish Progress 를 통 해 작업 진 도 를 알 립 니 다.
3.onProgressUpdate(Progress..values):배경 작업 의 진도 가 바 뀌 었 을 때 이 방법 을 사용 합 니 다.이 방법 에서 UI 의 진 도 를 보 여 드릴 수 있 습 니 다.values 매개 변 수 는 작업 진 도 를 표시 합 니 다.
4.post Result(Result result):비동기 작업 이 끝 난 후에 실 행 됩 니 다.result 인 자 는 비동기 작업 이 끝 난 후에 돌아 오 는 결과 입 니 다.
위의 네 가지 방법 중 doInBackground 만 하위 스 레 드 에서 실 행 됩 니 다.나머지 세 가지 방법 은 모두 메 인 스 레 드 에서 실 행 됩 니 다.그 중에서...매개 변수의 수량 이 일정 하지 않 음 을 나타 내 는 배열 형식의 매개 변수 입 니 다.
다음은 하나의 예 를 들 어 AsyncTask 의 용법 을 살 펴 보 겠 습 니 다.여기 서 우 리 는 하나의 다운로드 기능 으로 인터넷 에서 두 개의 파일 을 다운로드 합 니 다.우리 먼저 효과 시범 을 보 자.
효과 시범
코드 분석
우리 가 하 는 다운로드 작업 때문에,우선 네트워크 접근 권한 과 sd 카드 관련 권한 을 추가 해 야 합 니 다.
<!-- SD -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- SD -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- -->
<uses-permission android:name="android.permission.INTERNET"/>
다음은 Activity 코드 를 살 펴 보 겠 습 니 다.
package com.example.ljd.asynctask;
import android.app.ProgressDialog;
import android.os.AsyncTask;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private DownloadAsyncTask mDownloadAsyncTask;
private Button mButton;
private String[] path = {
"http://msoftdl.360.cn/mobilesafe/shouji360/360safesis/360MobileSafe_6.2.3.1060.apk",
"http://dlsw.baidu.com/sw-search-sp/soft/7b/33461/freeime.1406862029.exe",
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button)findViewById(R.id.button);
mButton.setOnClickListener(this);
}
@Override
protected void onDestroy() {
if (mDownloadAsyncTask != null){
mDownloadAsyncTask.cancel(true);
}
super.onDestroy();
}
@Override
public void onClick(View v) {
mDownloadAsyncTask = new DownloadAsyncTask();
mDownloadAsyncTask.execute(path);
}
class DownloadAsyncTask extends AsyncTask<String,Integer,Boolean>{
private ProgressDialog mPBar;
private int fileSize; //
@Override
protected void onPreExecute() {
super.onPreExecute();
mPBar = new ProgressDialog(MainActivity.this);
mPBar.setProgressNumberFormat("%1d KB/%2d KB");
mPBar.setTitle(" ");
mPBar.setMessage(" , ...");
mPBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mPBar.setCancelable(false);
mPBar.show();
}
@Override
protected Boolean doInBackground(String... params) {
//
for (int i=0;i<params.length;i++){
try{
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
URL url = new URL(params[i]);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//
conn.setConnectTimeout(5000);
//
fileSize = conn.getContentLength();
InputStream is = conn.getInputStream();
//
String fileName = path[i].substring(path[i].lastIndexOf("/") + 1);
File file = new File(Environment.getExternalStorageDirectory(), fileName);
FileOutputStream fos = new FileOutputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
byte[] buffer = new byte[1024];
int len ;
int total = 0;
while((len =bis.read(buffer))!=-1){
fos.write(buffer, 0, len);
total += len;
publishProgress(total);
fos.flush();
}
fos.close();
bis.close();
is.close();
}
else{
return false;
}
}catch (IOException e){
e.printStackTrace();
return false;
}
}
return true;
}
@Override
protected void onPostExecute(Boolean aBoolean) {
super.onPostExecute(aBoolean);
mPBar.dismiss();
if (aBoolean){
Toast.makeText(MainActivity.this," ",Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this," ",Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
mPBar.setMax(fileSize / 1024);
mPBar.setProgress(values[0]/1024);
}
}
}
상기 코드 중 몇 가지 주의 가 필요 합 니 다.1.AsyncTask 의 execute 방법 은 주 스 레 드 에서 실행 되 어야 합 니 다.
2.AsyncTask 대상 마다 execute 방법 을 한 번 만 수행 할 수 있 습 니 다.
3.우리 의 Activity 가 삭 제 될 때 취소 작업 을 해 야 합 니 다.그 중에서 boolean 형의 매개 변수 인 mayInterrupt IfRunning 은 배경 작업 을 중단 할 지 여 부 를 표시 합 니 다.
마지막 레이아웃 코드 는 매우 간단 합 니 다.버튼 만 있 을 뿐 입 니 다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context="com.example.ljd.asynctask.MainActivity">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="download"/>
</LinearLayout>
여기 에는 또 한 가지 설명 이 필요 합 니 다.서로 다른 안 드 로 이 드 버 전에 서 AsyncTask 를 여러 번 수 정 했 기 때문에 여러 개의 AsyncTask 대상 을 통 해 여러 번 execute 방법 을 실 행 했 을 때 이들 의 실행 순 서 는 직렬 인지 병렬 인지 시스템 의 서로 다른 버 전에 따라 차이 가 나 면 구체 적 으로 분석 하지 않 습 니 다.AsyncTask 소스 코드 분석
여기 서 우 리 는 Android 6.0 의 AsyncTask 소스 코드 를 이용 하여 분석 하 는데 서로 다른 시스템 의 AsyncTask 코드 에 대해 어느 정도 차이 가 있 을 것 이다.AsyncTask 대상 을 만 든 후에 우 리 는 execute 방법 을 통 해 모든 임 무 를 수행 할 수 있 습 니 다.그럼 여기 서 execute 방법 에 있 는 코드 부터 볼 게 요.
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
이 execute 방법의 코드 가 이렇게 간단 한 것 을 볼 수 있 습 니 다.execute OnExecutor 방법 을 실행 하고 AsyncTask 대상 으로 돌아 갈 뿐 입 니 다.다음은 execution Executor 라 는 방법 을 살 펴 보 겠 습 니 다.
@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
mStatus = Status.RUNNING;
onPreExecute();
mWorker.mParams = params;
exec.execute(mFuture);
return this;
}
이 방법 에서 우 리 는 먼저 AsyncTask 가 실행 한 상 태 를 판단 한다.AsyncTask 가 임 무 를 수행 하고 있 거나 임 무 를 이미 알 고 완성 했다 면 이상 을 던 져 주 고 위 에서 말 한 모든 AsyncTask 대상 은 execute 방법 을 한 번 만 수행 할 수 있 습 니 다.이 어 현재 작업 상 태 를 실행 중인 후 onPreExecute 방법 으로 변경 합 니 다.이것 은 위 에서 우리 가 다시 쓴 네 가지 방법 중 onPreExecute 방법 이 가장 먼저 가리 키 는 것 을 설명 한다.다음은 mWorker 와 mFuture 라 는 두 개의 전역 변 수 를 살 펴 보 겠 습 니 다.mFuture 는 Future Task 대상 이 고 mWorker 는 WorkerRunnable 대상 이 며 WorkerRunnable 은 AsyncTask 에서 Callable 인 터 페 이 스 를 실현 하 는 추상 적 인 내부 클래스 로 WorkerRunnable 에서 하나의 Params[]만 정의 합 니 다.
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
......
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
mFuture 와 mWorker 는 AsyncTask 의 구조 방법 에서 초기 화 되 었 습 니 다.AsyncTask 의 구조 방법 을 살 펴 보 자.
public AsyncTask() {
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
Result result = doInBackground(mParams);
Binder.flushPendingCommands();
return postResult(result);
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}
여기 서 먼저 WorkerRunnable 대상 mWorker 를 만 들 고 Callable 인터페이스의 call 방법 을 실현 하 였 으 며,이 call 의 내용 에 대해 서 는 뒤에서 상세 하 게 설명 할 것 입 니 다.그리고 mWorker 를 통 해 Future Task 대상 mFuture 를 만 들 고 그 안에 있 는 done 방법 을 다시 씁 니 다.AsyncTask 에 있 는 cancel 방법 을 호출 할 때 Future Task 에서 이 done 방법 을 호출 합 니 다.여기 서 mWorker 와 mFuture 를 소개 한 후에 우 리 는 다시 고 개 를 돌려 execution Executor 방법 을 살 펴 보 자.여기 서 우리 가 들 어 온 params 인 자 를 통 해 mWorker 의 mParams 를 초기 화 합 니 다.아래 exec 는 execute 에 들 어 오 는 sDefaultExecutor 이 며,sDefaultExecutor 의 execute 방법 을 실행 합 니 다.다음은 이 sDefaultExecutor 대상 을 다시 한 번 살 펴 보 겠 습 니 다.
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
......
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
......
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
여기 서 먼저 이 코드 를 전체적으로 소개 하 겠 습 니 다.이 코드 의 주요 기능 은 두 개의 스 레 드 탱크 SerialExecutor 와 THREAD 를 만 드 는 것 입 니 다.POOL_EXECUTOR。SerialExecutor 스 레 드 탱크 는 작업 의 줄 을 서 는 데 사용 되 며,THREADPOOL_EXECUTOR 는 임 무 를 수행 하 는 데 사 용 됩 니 다.sDefaultExecutor 는 SerialExecutor 스 레 드 탱크 입 니 다.SerialExecutor 코드 에 들 어가 서 내용 을 살 펴 보 겠 습 니 다.SerialExecutor 의 execute 방법 에 있 는 인자 Runnable 은 우리 가 들 어 오 는 mFuture 입 니 다.execute 방법 에서 Runnable 대상 을 만 들 고 이 대기 열 을 대상 의 끝 에 삽입 합 니 다.이 럴 때 임 무 를 처음 수행 하 는 거 라면mActive 는 반드시 null 입 니 다.이 때 scheduleNext 방법 으로 대기 열 에서 Runnable 대상 을 꺼 내 고 THREAD 를 통 해POOL_EXECUTOR 스 레 드 탱크 에서 작업 을 수행 합 니 다.대기 열 에 있 는 Runnable 의 run 방법 에서 mFuture 의 run 방법 을 먼저 실행 하고 실행 이 끝 난 후에 scheduleNext 방법 을 호출 하여 모든 Runnable 이 실 행 될 때 까지 대기 열 에서 Runnable 을 꺼 내 실행 하 는 것 을 볼 수 있 습 니 다.온라인 성 터 에서 이 Runnable 을 어떻게 수행 하 는 지 살 펴 보 겠 습 니 다.위의 코드 를 통 해 알 수 있 듯 이 온라인 도시 에서 Runnable 을 실행 하 는 가장 핵심 적 인 부분 은 mFuture 를 실행 하 는 run 방법 입 니 다.그럼 이 mFuture 의 run 방법 을 살 펴 보 겠 습 니 다.
public void run() {
if (state != NEW ||
!U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
이 방법 에서 callable 대상 은 우리 AsyncTask 의 mWorker 대상 입 니 다.이 안에서 도 mWorker 의 call 방법 을 실행 하여 시간 이 걸 리 는 임 무 를 수행 합 니 다.그래서 우 리 는 내 가 다시 쓴 doInBackground 가 이 call 방법 에서 실 행 될 것 이 라 고 생각 할 수 있 습 니 다.이제 AsyncTask 의 구조 방법 으로 돌아 가서 이 mWorker 의 call 방법 을 살 펴 보 겠 습 니 다.
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
Result result = doInBackground(mParams);
Binder.flushPendingCommands();
return postResult(result);
}
여기 서 우 리 는 그것 이 우리 가 다시 쓴 doInBackground 방법 을 실행 하고 돌아 온 결 과 를 post Result 방법 에 전달 하 는 것 을 잘 보 았 다.다음은 이 post Result 방법 을 살 펴 보 겠 습 니 다.
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
이 post Result 방법 에 대해 서 는 하위 스 레 드 의 작업 처리 가 끝 난 후에 Handler 대상 을 통 해 Message 를 메 인 스 레 드 에 보 내 고 메 인 스 레 드 에 맡 깁 니 다.AsyncTask Result 대상 에 저 장 된 것 은 하위 스 레 드 가 되 돌아 온 결과 와 현재 AsyncTask 대상 입 니 다.다음은 Handler 에서 어떤 일 을 처 리 했 는 지 살 펴 보 겠 습 니 다.
private static class InternalHandler extends Handler {
public InternalHandler() {
super(Looper.getMainLooper());
}
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
이 Handler 의 handle Message 에서 두 가지 Message 를 받 을 수 있 습 니 다.MESSAGE 에서POST_PROGRESS 이 메 시 지 는 주로 PublishProgress 방법 을 통 해 하위 스 레 드 가 실 행 된 진 도 를 메 인 스 레 드 에 보 내 고 onProgressUpdate 방법 을 통 해 진도 바 를 업데이트 하 는 것 입 니 다.MESSAGE 에서POST_RESULT 라 는 메시지 에서 현재 AsyncTask 대상 을 통 해 finish 방법 을 호출 했 습 니 다.이 finish 방법 을 살 펴 보 겠 습 니 다.
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
이 럴 때 우리 가 AsyncTask 를 취소 하면 onPost Execute 방법 을 실행 하지 않 고 onCanceled 방법 을 실행 하 는 것 을 알 수 있 습 니 다.따라서 onCanceled 방법 을 다시 써 서 취소 할 때 우리 가 처리 해 야 할 작업 을 수행 할 수 있 습 니 다.물론 AsyncTask 가 취소 되 지 않 았 다 면,이 때 는 onPost Execute 방법 을 다시 실행 합 니 다.여기까지.총결산
위의 SerialExecutor 스 레 드 탱크 에서 알 수 있 듯 이 여러 개의 비동기 작업 이 동시에 실 행 될 때 이들 이 수행 하 는 순 서 는 직렬 이 고 작업 이 만 든 선후 순서에 따라 한 번 실 행 됩 니 다.만약 우리 가 여러 작업 을 동시에 수행 하 기 를 원한 다 면 AsyncTask 의 setDefaultExecutor 방법 을 통 해 스 레 드 풀 을 THREAD 로 설정 할 수 있 습 니 다.POOL_EXECUTOR 면 됩 니 다.
AsyncTask 의 버 전 간 차이 점 에 대해 서 는 언급 할 수 밖 에 없습니다.Android 1.6 에서 AsyncTask 는 직렬 로 작업 을 수행 하고 Android 1.6 에 서 는 스 레 드 풀 로 병행 작업 을 처리 하 며 3.0 이후 에 야 SerialExecutor 스 레 드 풀 을 통 해 직렬 로 작업 을 처리 합 니 다.Android 4.1 이전에 AsyncTask 류 는 주 스 레 드 에 있어 야 하지만 다음 버 전에 서 는 시스템 에 의 해 자동 으로 완 료 됩 니 다.안 드 로 이 드 5.0 버 전에 서 는 Activity Thread 의 main 방법 에서 AsyncTask 의 init 방법 을 실행 하고 안 드 로 이 드 6.0 에 서 는 init 방법 을 삭제 합 니 다.따라서 이 AsyncTask 를 사용 할 때 더 많은 시스템 버 전이 적합 하 다 면 사용 할 때 주의해 야 합 니 다.
원본 다운로드:https://github.com/lijiangdong/asynctask-example
이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Bitrise에서 배포 어플리케이션 설정 테스트하기이 글은 Bitrise 광고 달력의 23일째 글입니다. 자체 또는 당사 등에서 Bitrise 구축 서비스를 사용합니다. 그나저나 며칠 전 Bitrise User Group Meetup #3에서 아래 슬라이드를 발표했...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.