パソコンではシングルスレッドで動くプログラムは珍しくはないが、スマホでは殆どの場合、 マルチスレッドが前提となる。 時間のかかる処理はメインスレッドでは実行できず、別のスレッドで実行することになる。
スマホはパソコンに比べてコア数が大きく高いパフォーマンスを得るためには、並列処理が必須である。 因みに、現在使用中の Androidスマホ、タブレットは共に8コアである。
地図画像は小さいタイル画像(256x256画素)を敷き詰めたものであり、タイル画像は独立して、 同時に複数準備できるため、並列処理がやりやすい。
国土地理院地図の場合、タイル画像ファイルをネット上のサーバーからダウンロードすればよいため、 並列処理も簡単である。OSM(OpenStreetMap)地図についても、同じやり方も可能であるが、地図アプリMapでは 自由にカスタマイズするために、OSMデータ(xml形式)からレンダリングによりタイル画像を生成する。
レンダリングにはワークエリアなどが必要なため、ダイナミックにスレッドを生成、消滅させる方法は向かない。
レンダリング用に6スレッドをスタンバイさせておき、タイル画像の生成に使うワークエリアは使いまわす。
Map4 でも、Map3 と同じ方法に落ち着く可能性があるが、異なる方法も検討したい。
地図アプリMap3 では国土地理院標高タイルから標高を得ている。ここでのマルチスレッド処理はレンダリングとは異なっている。
標高タイルには 5m メッシュ(dem5a, dem5b, dem5c) と 10m メッシュ(dem10b) がある。全タイルがあるとは限らない。 5m メッシュは精度の高い順に探す。一度ダウンロードしたものは、ストレージに残しておく。
5mメッシュの地点 256x256 分の標高値が 256x256画素の標高タイルになる。これは、zoom 15 に相当する。 10m メッシュは zoom 14 に当たる。
前回参照した標高タイルは lastBmp15、lastBmp14 に残っている。
参照する地点が前回と同じタイルにあれば、lastBmp15 / lastBmp14 から取り出すだけで終わりである。
タイルが異なる場合、ストレージにあれば、これを読み込んで、取り出す。 ストレージにない場合、ダウンロードが起きる。
メインスレッドで実行するのは setElevation である。この処理は別スレッドを立ち上げるだけで終わる。 従って、 if (busy) return; がなければ、次々とたくさんのスレッドが生まれ、同時進行になる恐れがある。
boolean busy を カウンタに変えれば、標高取得スレッドを複数にすることもできる。 ただし、カウンタの更新を排他にするなどの追加対策が必要となる。フラグの場合、タイミングにより、 一瞬、標高取得スレッドが二つ以上動くことがあっても、いずれ解消する。
static boolean busy = false;
void setElevation(int zoom, double x, double y) {
if (busy) return;
busy = true;
// Singleの別スレッドを立ち上げる
Executors.newSingleThreadExecutor().execute(() -> {
Elevation ev = Elevation.getElevation(zoom, x, y);
String ele = String.format("%.2fm[%.2fm]", ev.ele5, ev.ele10);
// 別スレッド内での処理を管理し実行する
HandlerCompat.createAsync(getMainLooper()).post(() ->
// Mainスレッドに渡す
ele_view.setText(ele)
);
busy = false;
});
}
// 任意の地点の標高を求める。
public class Elevation {
enum Source { dem5a, dem5b, dem5c, dem10b, dem5na }
static Bitmap lastBmp15;
static Bitmap lastBmp14;
static Source lastSrc5;
static int lastX15, lastY15;
//double lon, lat;
int x, y; // 世界XY平面座標 0 ~ 2の30乗
float ele5; // dem5a, dem5b, dem5c から得た値
float ele10; // dem10b から得た値
Source src5; // ele5 を得たソース dem5a, dem5b, dem5cのいずれか
public Elevation() {
ele5 = Float.MIN_VALUE;
ele10 = Float.MIN_VALUE;
src5 = Source.dem5na;
}
static void initialize() {
lastBmp15 = null;
lastBmp14 = null;
lastSrc5 = Source.dem5na;
lastX15 = lastY15 = -1;
}
// getElevation(zoom, (double)CX/PX, (double)CY/PX)
static Elevation getElevation(int zoom, double x, double y) {
double fact = zoom==23 ? 1.0 : zoom>23 ? 1.0 / (1<<(zoom-23)) : (1<<(23-zoom));
int x23 = (int)(fact != 1 ? x * fact : x);
int y23 = (int)(fact != 1 ? y * fact : y);
int X15 = x23/256; // 5m間隔標高タイルのX座標
int Y15 = y23/256; // 5m間隔標高タイルのY座標
Bitmap bmp15, bmp14;
Source src5;
if (X15 == lastX15 && Y15 == lastY15) {
bmp15 = lastBmp15;
bmp14 = lastBmp14;
src5 = lastSrc5;
} else {
lastBmp15 = bmp15 = getEleTile(Source.dem5a, X15, Y15);
lastSrc5 = src5 = Source.dem5a;
if (bmp15 == null) {
lastBmp15 = bmp15 = getEleTile(Source.dem5b, X15, Y15);
lastSrc5 = src5 = Source.dem5b;
}
if (bmp15 == null) {
lastBmp15 = bmp15 = getEleTile(Source.dem5c, X15, Y15);
lastSrc5 = src5 = Source.dem5c;
}
lastBmp14 = bmp14 = getEleTile(Source.dem10b, X15 / 2, Y15 / 2);
}
Elevation ele = new Elevation();
if (bmp15 != null) {
int pixel = bmp15.getPixel(x23%256, y23%256);
int v = Color.red(pixel)*(1<<16) + Color.green(pixel)*(1<<8) + Color.blue(pixel);
ele.ele5 = v == (1<<23) ? Float.MIN_VALUE : // 無効
v < (1<<23) ? v * 0.01f : (v - (1<<24)) * 0.01f;
ele.src5 = src5;
}
if (bmp14 != null) {
int pixel = bmp14.getPixel((x23/2)%256, (y23/2)%256);
int v = Color.red(pixel)*(1<<16) + Color.green(pixel)*(1<<8) + Color.blue(pixel);
ele.ele10 = v == (1<<23) ? Float.MIN_VALUE : // 無効
v < (1<<23) ? v * 0.01f : (v - (1<<24)) * 0.01f;
}
return ele;
}
static Bitmap getEleTile(Source src, int x, int y) {
String dir = Map.DIR + "GSI/dem/" + src.name() + "/" + x;
String path = dir + "/" + y + ".png";
Bitmap bmp = loadBitmap(path);
if (bmp != null) {
return bmp;
} else {
String name = src == Source.dem10b ? "dem_png/14/" : (src.name() + "_png/15/");
String url = "https://cyberjapandata.gsi.go.jp/xyz/" + name + x + "/" + y + ".png";
bmp = download(url, dir, path);
return bmp;
}
}
static Bitmap loadBitmap(String path) {
File file = new File(path);
if (!file.exists()) return null;
try {
InputStream stream = new FileInputStream(file);
Bitmap bitmap = BitmapFactory.decodeStream(new BufferedInputStream(stream));
stream.close();
return bitmap;
} catch (Exception ignored) {
return null;
}
}
static Bitmap download(String surl, String sdir, String path) {
Bitmap bmp = null;
try {
URL url = new URL(surl);
HttpURLConnection urlCon = (HttpURLConnection) url.openConnection();
urlCon.setReadTimeout(100 * 1000);
urlCon.setConnectTimeout(200 * 1000);
urlCon.setRequestMethod("GET");
InputStream is = urlCon.getInputStream();
bmp = BitmapFactory.decodeStream(is);
is.close();
File dir = new File(sdir);
if (!dir.exists() && !dir.mkdirs()) {
Log.d("mkdirs error", sdir);
}
FileOutputStream output = new FileOutputStream(path);
bmp.compress(Bitmap.CompressFormat.PNG, 100, output);
output.flush();
output.getFD().sync();
output.close();
} catch (Exception ignored) {
}
return bmp;
}
}
地図アプリのレンダリングでは古典的なマルチスレッド処理を行っている。
アプリ起動時に複数スレッドを起動し、スタンバイ状態とする。
Thread.sleep() は好ましくないが、実測では、オーバヘッドは問題ない。
public class RenderThread extends Thread {
static int NumThreads;
static Thread[] threads;
static boolean run; // 外部(終了処理)から false にするとスレッドが終了する
int thread_number;
Renderer renderer;
Map map;
public RenderThread(Map map, int num) {
this.map = map;
this.thread_number = num;
this.renderer = new Renderer(map, num);
System.out.printf("RenderThread #%d ready.\n", num);
}
public void run() {
run = true;
while (run) {
Tile tile = Tile.getRequest(map.src, map.zoom);
if (tile != null) {
tile.status = Tile.Status.busy; // lockすべきか
renderer.render(tile);
tile.status = Tile.Status.ready;
synchronized (map.objLock) {
map.invalidate();
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
}
}
}